agent-gauntlet 0.2.2 → 0.3.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 +3 -3
- package/package.json +1 -1
- package/src/cli-adapters/claude.ts +13 -1
- package/src/cli-adapters/gemini.ts +17 -2
- package/src/commands/check.ts +98 -12
- package/src/commands/ci/list-jobs.ts +3 -2
- package/src/commands/clean.ts +29 -0
- package/src/commands/help.ts +1 -1
- package/src/commands/index.ts +1 -1
- package/src/commands/init.ts +4 -4
- package/src/commands/review.ts +98 -12
- package/src/commands/run.ts +98 -12
- package/src/commands/shared.ts +56 -10
- package/src/config/schema.ts +4 -0
- package/src/config/validator.ts +6 -13
- package/src/core/change-detector.ts +1 -0
- package/src/core/entry-point.ts +48 -7
- package/src/core/runner.ts +57 -47
- package/src/gates/result.ts +32 -0
- package/src/gates/review.ts +323 -51
- package/src/index.ts +2 -2
- package/src/output/console.ts +96 -9
- package/src/output/logger.ts +40 -7
- package/src/templates/run_gauntlet.template.md +20 -13
- package/src/utils/log-parser.ts +409 -165
- package/src/utils/session-ref.ts +82 -0
- package/src/commands/check.test.ts +0 -29
- package/src/commands/detect.test.ts +0 -43
- package/src/commands/health.test.ts +0 -93
- package/src/commands/help.test.ts +0 -44
- package/src/commands/init.test.ts +0 -130
- package/src/commands/list.test.ts +0 -121
- package/src/commands/rerun.ts +0 -160
- package/src/commands/review.test.ts +0 -31
- package/src/commands/run.test.ts +0 -27
- package/src/config/loader.test.ts +0 -151
- package/src/core/entry-point.test.ts +0 -61
- package/src/gates/review.test.ts +0 -291
package/README.md
CHANGED
|
@@ -51,9 +51,9 @@ The use cases below illustrate when each of these patterns may be used.
|
|
|
51
51
|
2. Run `/gauntlet` from chat
|
|
52
52
|
3. Gauntlet detects changed files and runs configured checks (linter, tests, type checking, etc.)
|
|
53
53
|
4. Simultaneously, Gauntlet invokes AI CLIs for code review
|
|
54
|
-
5. Assistant reviews results, fixes identified issues, and runs `agent-gauntlet
|
|
55
|
-
6. Gauntlet
|
|
56
|
-
7. Process repeats automatically (up to 3
|
|
54
|
+
5. Assistant reviews results, fixes identified issues, and runs `agent-gauntlet run` again
|
|
55
|
+
6. Gauntlet detects existing logs, switches to verification mode, and checks fixes
|
|
56
|
+
7. Process repeats automatically (up to 3 iterations) until all gates pass
|
|
57
57
|
|
|
58
58
|
### 3. Agentic Implementation
|
|
59
59
|
|
package/package.json
CHANGED
|
@@ -40,7 +40,7 @@ export class ClaudeAdapter implements CLIAdapter {
|
|
|
40
40
|
// We use a simple "hello" prompt to avoid "No messages returned" errors from empty input
|
|
41
41
|
const { stdout, stderr } = await execAsync(
|
|
42
42
|
'echo "hello" | claude -p --max-turns 1',
|
|
43
|
-
{ timeout:
|
|
43
|
+
{ timeout: 30000 },
|
|
44
44
|
);
|
|
45
45
|
|
|
46
46
|
const combined = (stdout || "") + (stderr || "");
|
|
@@ -58,7 +58,19 @@ export class ClaudeAdapter implements CLIAdapter {
|
|
|
58
58
|
stderr?: string;
|
|
59
59
|
stdout?: string;
|
|
60
60
|
message?: string;
|
|
61
|
+
code?: number | string;
|
|
62
|
+
signal?: string;
|
|
61
63
|
};
|
|
64
|
+
|
|
65
|
+
// Check for timeout
|
|
66
|
+
if (execError.signal === "SIGTERM" && execError.code === null) {
|
|
67
|
+
return {
|
|
68
|
+
available: true,
|
|
69
|
+
status: "unhealthy",
|
|
70
|
+
message: "Error: Health check timed out",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
62
74
|
const stderr = execError.stderr || "";
|
|
63
75
|
const stdout = execError.stdout || "";
|
|
64
76
|
const combined = stderr + stdout;
|
|
@@ -38,7 +38,7 @@ export class GeminiAdapter implements CLIAdapter {
|
|
|
38
38
|
try {
|
|
39
39
|
const { stdout, stderr } = await execAsync(
|
|
40
40
|
'echo "hello" | gemini --sandbox --output-format text',
|
|
41
|
-
{ timeout:
|
|
41
|
+
{ timeout: 30000 },
|
|
42
42
|
);
|
|
43
43
|
|
|
44
44
|
const combined = (stdout || "") + (stderr || "");
|
|
@@ -56,7 +56,19 @@ export class GeminiAdapter implements CLIAdapter {
|
|
|
56
56
|
stderr?: string;
|
|
57
57
|
stdout?: string;
|
|
58
58
|
message?: string;
|
|
59
|
+
code?: number | string;
|
|
60
|
+
signal?: string;
|
|
59
61
|
};
|
|
62
|
+
|
|
63
|
+
// Check for timeout
|
|
64
|
+
if (execError.signal === "SIGTERM" && execError.code === null) {
|
|
65
|
+
return {
|
|
66
|
+
available: true,
|
|
67
|
+
status: "unhealthy",
|
|
68
|
+
message: "Error: Health check timed out",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
60
72
|
const stderr = execError.stderr || "";
|
|
61
73
|
const stdout = execError.stdout || "";
|
|
62
74
|
const combined = stderr + stdout;
|
|
@@ -159,7 +171,10 @@ ${escapedBody}
|
|
|
159
171
|
|
|
160
172
|
// Write to a temporary file to avoid shell escaping issues
|
|
161
173
|
const tmpDir = os.tmpdir();
|
|
162
|
-
const tmpFile = path.join(
|
|
174
|
+
const tmpFile = path.join(
|
|
175
|
+
tmpDir,
|
|
176
|
+
`gauntlet-gemini-${process.pid}-${Date.now()}.txt`,
|
|
177
|
+
);
|
|
163
178
|
await fs.writeFile(tmpFile, fullContent);
|
|
164
179
|
|
|
165
180
|
try {
|
package/src/commands/check.ts
CHANGED
|
@@ -7,7 +7,17 @@ import { JobGenerator } from "../core/job.js";
|
|
|
7
7
|
import { Runner } from "../core/runner.js";
|
|
8
8
|
import { ConsoleReporter } from "../output/console.js";
|
|
9
9
|
import { Logger } from "../output/logger.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
findPreviousFailures,
|
|
12
|
+
type PreviousViolation,
|
|
13
|
+
} from "../utils/log-parser.js";
|
|
14
|
+
import { readSessionRef, writeSessionRef } from "../utils/session-ref.js";
|
|
15
|
+
import {
|
|
16
|
+
acquireLock,
|
|
17
|
+
cleanLogs,
|
|
18
|
+
hasExistingLogs,
|
|
19
|
+
releaseLock,
|
|
20
|
+
} from "./shared.js";
|
|
11
21
|
|
|
12
22
|
export function registerCheckCommand(program: Command): void {
|
|
13
23
|
program
|
|
@@ -24,14 +34,14 @@ export function registerCheckCommand(program: Command): void {
|
|
|
24
34
|
"Use diff for current uncommitted changes (staged and unstaged)",
|
|
25
35
|
)
|
|
26
36
|
.action(async (options) => {
|
|
37
|
+
let config: Awaited<ReturnType<typeof loadConfig>> | undefined;
|
|
38
|
+
let lockAcquired = false;
|
|
27
39
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
await rotateLogs(config.project.log_dir);
|
|
40
|
+
config = await loadConfig();
|
|
41
|
+
await acquireLock(config.project.log_dir);
|
|
42
|
+
lockAcquired = true;
|
|
32
43
|
|
|
33
44
|
// Determine effective base branch
|
|
34
|
-
// Priority: CLI override > CI env var > config
|
|
35
45
|
const effectiveBaseBranch =
|
|
36
46
|
options.baseBranch ||
|
|
37
47
|
(process.env.GITHUB_BASE_REF &&
|
|
@@ -40,10 +50,73 @@ export function registerCheckCommand(program: Command): void {
|
|
|
40
50
|
: null) ||
|
|
41
51
|
config.project.base_branch;
|
|
42
52
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
// Detect rerun mode
|
|
54
|
+
const logsExist = await hasExistingLogs(config.project.log_dir);
|
|
55
|
+
const isRerun = logsExist && !options.uncommitted && !options.commit;
|
|
56
|
+
|
|
57
|
+
let failuresMap:
|
|
58
|
+
| Map<string, Map<string, PreviousViolation[]>>
|
|
59
|
+
| undefined;
|
|
60
|
+
let changeOptions:
|
|
61
|
+
| { commit?: string; uncommitted?: boolean; fixBase?: string }
|
|
62
|
+
| undefined;
|
|
63
|
+
|
|
64
|
+
if (isRerun) {
|
|
65
|
+
console.log(
|
|
66
|
+
chalk.dim(
|
|
67
|
+
"Existing logs detected — running in verification mode...",
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
const previousFailures = await findPreviousFailures(
|
|
71
|
+
config.project.log_dir,
|
|
72
|
+
options.gate,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
failuresMap = new Map();
|
|
76
|
+
for (const gateFailure of previousFailures) {
|
|
77
|
+
const adapterMap = new Map<string, PreviousViolation[]>();
|
|
78
|
+
for (const af of gateFailure.adapterFailures) {
|
|
79
|
+
adapterMap.set(af.adapterName, af.violations);
|
|
80
|
+
}
|
|
81
|
+
failuresMap.set(gateFailure.jobId, adapterMap);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (previousFailures.length > 0) {
|
|
85
|
+
const totalViolations = previousFailures.reduce(
|
|
86
|
+
(sum, gf) =>
|
|
87
|
+
sum +
|
|
88
|
+
gf.adapterFailures.reduce(
|
|
89
|
+
(s, af) => s + af.violations.length,
|
|
90
|
+
0,
|
|
91
|
+
),
|
|
92
|
+
0,
|
|
93
|
+
);
|
|
94
|
+
console.log(
|
|
95
|
+
chalk.yellow(
|
|
96
|
+
`Found ${previousFailures.length} gate(s) with ${totalViolations} previous violation(s)`,
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
changeOptions = { uncommitted: true };
|
|
102
|
+
const fixBase = await readSessionRef(config.project.log_dir);
|
|
103
|
+
if (fixBase) {
|
|
104
|
+
changeOptions.fixBase = fixBase;
|
|
105
|
+
}
|
|
106
|
+
} else if (options.commit || options.uncommitted) {
|
|
107
|
+
changeOptions = {
|
|
108
|
+
commit: options.commit,
|
|
109
|
+
uncommitted: options.uncommitted,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const changeDetector = new ChangeDetector(
|
|
114
|
+
effectiveBaseBranch,
|
|
115
|
+
changeOptions || {
|
|
116
|
+
commit: options.commit,
|
|
117
|
+
uncommitted: options.uncommitted,
|
|
118
|
+
},
|
|
119
|
+
);
|
|
47
120
|
const expander = new EntryPointExpander();
|
|
48
121
|
const jobGen = new JobGenerator(config);
|
|
49
122
|
|
|
@@ -52,6 +125,7 @@ export function registerCheckCommand(program: Command): void {
|
|
|
52
125
|
|
|
53
126
|
if (changes.length === 0) {
|
|
54
127
|
console.log(chalk.green("No changes detected."));
|
|
128
|
+
await releaseLock(config.project.log_dir);
|
|
55
129
|
process.exit(0);
|
|
56
130
|
}
|
|
57
131
|
|
|
@@ -72,6 +146,7 @@ export function registerCheckCommand(program: Command): void {
|
|
|
72
146
|
|
|
73
147
|
if (jobs.length === 0) {
|
|
74
148
|
console.log(chalk.yellow("No applicable checks for these changes."));
|
|
149
|
+
await releaseLock(config.project.log_dir);
|
|
75
150
|
process.exit(0);
|
|
76
151
|
}
|
|
77
152
|
|
|
@@ -83,14 +158,25 @@ export function registerCheckCommand(program: Command): void {
|
|
|
83
158
|
config,
|
|
84
159
|
logger,
|
|
85
160
|
reporter,
|
|
86
|
-
|
|
87
|
-
|
|
161
|
+
failuresMap,
|
|
162
|
+
changeOptions,
|
|
88
163
|
effectiveBaseBranch,
|
|
89
164
|
);
|
|
90
165
|
|
|
91
166
|
const success = await runner.run(jobs);
|
|
167
|
+
|
|
168
|
+
if (success) {
|
|
169
|
+
await cleanLogs(config.project.log_dir);
|
|
170
|
+
} else {
|
|
171
|
+
await writeSessionRef(config.project.log_dir);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await releaseLock(config.project.log_dir);
|
|
92
175
|
process.exit(success ? 0 : 1);
|
|
93
176
|
} catch (error: unknown) {
|
|
177
|
+
if (config && lockAcquired) {
|
|
178
|
+
await releaseLock(config.project.log_dir);
|
|
179
|
+
}
|
|
94
180
|
const err = error as { message?: string };
|
|
95
181
|
console.error(chalk.red("Error:"), err.message);
|
|
96
182
|
process.exit(1);
|
|
@@ -34,8 +34,9 @@ export async function listJobs(): Promise<void> {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const workingDirectory = checkDef.working_directory || ep.path;
|
|
37
|
-
//
|
|
38
|
-
|
|
37
|
+
// Dedupe by check name + working directory only - if two entry points
|
|
38
|
+
// both trigger e.g. "test" with working_directory: ".", run it once
|
|
39
|
+
const jobKey = `${check.name}:${workingDirectory}`;
|
|
39
40
|
|
|
40
41
|
// Skip if we've already created a job for this exact entry point/check combination
|
|
41
42
|
if (seenJobs.has(jobKey)) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
import { loadConfig } from "../config/loader.js";
|
|
4
|
+
import { acquireLock, cleanLogs, releaseLock } from "./shared.js";
|
|
5
|
+
|
|
6
|
+
export function registerCleanCommand(program: Command): void {
|
|
7
|
+
program
|
|
8
|
+
.command("clean")
|
|
9
|
+
.description("Archive logs (move current logs into previous/)")
|
|
10
|
+
.action(async () => {
|
|
11
|
+
let config: Awaited<ReturnType<typeof loadConfig>> | undefined;
|
|
12
|
+
let lockAcquired = false;
|
|
13
|
+
try {
|
|
14
|
+
config = await loadConfig();
|
|
15
|
+
await acquireLock(config.project.log_dir);
|
|
16
|
+
lockAcquired = true;
|
|
17
|
+
await cleanLogs(config.project.log_dir);
|
|
18
|
+
await releaseLock(config.project.log_dir);
|
|
19
|
+
console.log(chalk.green("Logs archived successfully."));
|
|
20
|
+
} catch (error: unknown) {
|
|
21
|
+
if (config && lockAcquired) {
|
|
22
|
+
await releaseLock(config.project.log_dir);
|
|
23
|
+
}
|
|
24
|
+
const err = error as { message?: string };
|
|
25
|
+
console.error(chalk.red("Error:"), err.message);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
package/src/commands/help.ts
CHANGED
|
@@ -15,9 +15,9 @@ export function registerHelpCommand(program: Command): void {
|
|
|
15
15
|
);
|
|
16
16
|
console.log(chalk.bold("Commands:\n"));
|
|
17
17
|
console.log(" run Run gates for detected changes");
|
|
18
|
-
console.log(" rerun Rerun gates with previous failure context");
|
|
19
18
|
console.log(" check Run only applicable checks");
|
|
20
19
|
console.log(" review Run only applicable reviews");
|
|
20
|
+
console.log(" clean Archive logs (move current logs into previous/)");
|
|
21
21
|
console.log(
|
|
22
22
|
" detect Show what gates would run (without executing them)",
|
|
23
23
|
);
|
package/src/commands/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export { registerCheckCommand } from "./check.js";
|
|
2
2
|
export { registerCICommand } from "./ci/index.js";
|
|
3
|
+
export { registerCleanCommand } from "./clean.js";
|
|
3
4
|
export { registerDetectCommand } from "./detect.js";
|
|
4
5
|
export { registerHealthCommand } from "./health.js";
|
|
5
6
|
export { registerHelpCommand } from "./help.js";
|
|
6
7
|
export { registerInitCommand } from "./init.js";
|
|
7
8
|
export { registerListCommand } from "./list.js";
|
|
8
|
-
export { registerRerunCommand } from "./rerun.js";
|
|
9
9
|
export { registerReviewCommand } from "./review.js";
|
|
10
10
|
export { registerRunCommand } from "./run.js";
|
package/src/commands/init.ts
CHANGED
|
@@ -35,12 +35,12 @@ Execute the autonomous verification suite.
|
|
|
35
35
|
- If fix instructions are available, they will be in the log file under a "--- Fix Instructions ---" section—carefully read and apply them FIRST before attempting other fixes.
|
|
36
36
|
3. Fix any code or logic errors found by the tools or AI reviewers, prioritizing higher-priority violations (critical > high > medium > low).
|
|
37
37
|
4. Apply the trust level above when deciding whether to act on AI reviewer feedback. If you skip an issue due to the trust threshold, report it with a brief explanation (e.g., "Skipped: [issue summary] — reason: [stylistic/subjective/disagree]").
|
|
38
|
-
5. Do NOT commit your changes yet—keep them uncommitted so the
|
|
39
|
-
6. Run \`agent-gauntlet
|
|
38
|
+
5. Do NOT commit your changes yet—keep them uncommitted so the next run can verify them.
|
|
39
|
+
6. Run \`agent-gauntlet run\` again to verify your fixes. It will detect existing logs and automatically switch to verification mode (uncommitted changes + previous failure context).
|
|
40
40
|
7. Repeat steps 2-6 until one of the following termination conditions is met:
|
|
41
|
-
- All gates pass
|
|
41
|
+
- All gates pass (logs are automatically archived)
|
|
42
42
|
- You disagree with remaining failures (ask the human how to proceed)
|
|
43
|
-
- Still failing after 3
|
|
43
|
+
- Still failing after 3 attempts
|
|
44
44
|
8. Once all gates pass, do NOT commit or push your changes—await the human's review and explicit instruction to commit.
|
|
45
45
|
`;
|
|
46
46
|
|
package/src/commands/review.ts
CHANGED
|
@@ -7,7 +7,17 @@ import { JobGenerator } from "../core/job.js";
|
|
|
7
7
|
import { Runner } from "../core/runner.js";
|
|
8
8
|
import { ConsoleReporter } from "../output/console.js";
|
|
9
9
|
import { Logger } from "../output/logger.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
findPreviousFailures,
|
|
12
|
+
type PreviousViolation,
|
|
13
|
+
} from "../utils/log-parser.js";
|
|
14
|
+
import { readSessionRef, writeSessionRef } from "../utils/session-ref.js";
|
|
15
|
+
import {
|
|
16
|
+
acquireLock,
|
|
17
|
+
cleanLogs,
|
|
18
|
+
hasExistingLogs,
|
|
19
|
+
releaseLock,
|
|
20
|
+
} from "./shared.js";
|
|
11
21
|
|
|
12
22
|
export function registerReviewCommand(program: Command): void {
|
|
13
23
|
program
|
|
@@ -24,14 +34,14 @@ export function registerReviewCommand(program: Command): void {
|
|
|
24
34
|
"Use diff for current uncommitted changes (staged and unstaged)",
|
|
25
35
|
)
|
|
26
36
|
.action(async (options) => {
|
|
37
|
+
let config: Awaited<ReturnType<typeof loadConfig>> | undefined;
|
|
38
|
+
let lockAcquired = false;
|
|
27
39
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
await rotateLogs(config.project.log_dir);
|
|
40
|
+
config = await loadConfig();
|
|
41
|
+
await acquireLock(config.project.log_dir);
|
|
42
|
+
lockAcquired = true;
|
|
32
43
|
|
|
33
44
|
// Determine effective base branch
|
|
34
|
-
// Priority: CLI override > CI env var > config
|
|
35
45
|
const effectiveBaseBranch =
|
|
36
46
|
options.baseBranch ||
|
|
37
47
|
(process.env.GITHUB_BASE_REF &&
|
|
@@ -40,10 +50,73 @@ export function registerReviewCommand(program: Command): void {
|
|
|
40
50
|
: null) ||
|
|
41
51
|
config.project.base_branch;
|
|
42
52
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
// Detect rerun mode
|
|
54
|
+
const logsExist = await hasExistingLogs(config.project.log_dir);
|
|
55
|
+
const isRerun = logsExist && !options.uncommitted && !options.commit;
|
|
56
|
+
|
|
57
|
+
let failuresMap:
|
|
58
|
+
| Map<string, Map<string, PreviousViolation[]>>
|
|
59
|
+
| undefined;
|
|
60
|
+
let changeOptions:
|
|
61
|
+
| { commit?: string; uncommitted?: boolean; fixBase?: string }
|
|
62
|
+
| undefined;
|
|
63
|
+
|
|
64
|
+
if (isRerun) {
|
|
65
|
+
console.log(
|
|
66
|
+
chalk.dim(
|
|
67
|
+
"Existing logs detected — running in verification mode...",
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
const previousFailures = await findPreviousFailures(
|
|
71
|
+
config.project.log_dir,
|
|
72
|
+
options.gate,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
failuresMap = new Map();
|
|
76
|
+
for (const gateFailure of previousFailures) {
|
|
77
|
+
const adapterMap = new Map<string, PreviousViolation[]>();
|
|
78
|
+
for (const af of gateFailure.adapterFailures) {
|
|
79
|
+
adapterMap.set(af.adapterName, af.violations);
|
|
80
|
+
}
|
|
81
|
+
failuresMap.set(gateFailure.jobId, adapterMap);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (previousFailures.length > 0) {
|
|
85
|
+
const totalViolations = previousFailures.reduce(
|
|
86
|
+
(sum, gf) =>
|
|
87
|
+
sum +
|
|
88
|
+
gf.adapterFailures.reduce(
|
|
89
|
+
(s, af) => s + af.violations.length,
|
|
90
|
+
0,
|
|
91
|
+
),
|
|
92
|
+
0,
|
|
93
|
+
);
|
|
94
|
+
console.log(
|
|
95
|
+
chalk.yellow(
|
|
96
|
+
`Found ${previousFailures.length} gate(s) with ${totalViolations} previous violation(s)`,
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
changeOptions = { uncommitted: true };
|
|
102
|
+
const fixBase = await readSessionRef(config.project.log_dir);
|
|
103
|
+
if (fixBase) {
|
|
104
|
+
changeOptions.fixBase = fixBase;
|
|
105
|
+
}
|
|
106
|
+
} else if (options.commit || options.uncommitted) {
|
|
107
|
+
changeOptions = {
|
|
108
|
+
commit: options.commit,
|
|
109
|
+
uncommitted: options.uncommitted,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const changeDetector = new ChangeDetector(
|
|
114
|
+
effectiveBaseBranch,
|
|
115
|
+
changeOptions || {
|
|
116
|
+
commit: options.commit,
|
|
117
|
+
uncommitted: options.uncommitted,
|
|
118
|
+
},
|
|
119
|
+
);
|
|
47
120
|
const expander = new EntryPointExpander();
|
|
48
121
|
const jobGen = new JobGenerator(config);
|
|
49
122
|
|
|
@@ -52,6 +125,7 @@ export function registerReviewCommand(program: Command): void {
|
|
|
52
125
|
|
|
53
126
|
if (changes.length === 0) {
|
|
54
127
|
console.log(chalk.green("No changes detected."));
|
|
128
|
+
await releaseLock(config.project.log_dir);
|
|
55
129
|
process.exit(0);
|
|
56
130
|
}
|
|
57
131
|
|
|
@@ -72,6 +146,7 @@ export function registerReviewCommand(program: Command): void {
|
|
|
72
146
|
|
|
73
147
|
if (jobs.length === 0) {
|
|
74
148
|
console.log(chalk.yellow("No applicable reviews for these changes."));
|
|
149
|
+
await releaseLock(config.project.log_dir);
|
|
75
150
|
process.exit(0);
|
|
76
151
|
}
|
|
77
152
|
|
|
@@ -83,14 +158,25 @@ export function registerReviewCommand(program: Command): void {
|
|
|
83
158
|
config,
|
|
84
159
|
logger,
|
|
85
160
|
reporter,
|
|
86
|
-
|
|
87
|
-
|
|
161
|
+
failuresMap,
|
|
162
|
+
changeOptions,
|
|
88
163
|
effectiveBaseBranch,
|
|
89
164
|
);
|
|
90
165
|
|
|
91
166
|
const success = await runner.run(jobs);
|
|
167
|
+
|
|
168
|
+
if (success) {
|
|
169
|
+
await cleanLogs(config.project.log_dir);
|
|
170
|
+
} else {
|
|
171
|
+
await writeSessionRef(config.project.log_dir);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await releaseLock(config.project.log_dir);
|
|
92
175
|
process.exit(success ? 0 : 1);
|
|
93
176
|
} catch (error: unknown) {
|
|
177
|
+
if (config && lockAcquired) {
|
|
178
|
+
await releaseLock(config.project.log_dir);
|
|
179
|
+
}
|
|
94
180
|
const err = error as { message?: string };
|
|
95
181
|
console.error(chalk.red("Error:"), err.message);
|
|
96
182
|
process.exit(1);
|
package/src/commands/run.ts
CHANGED
|
@@ -7,7 +7,17 @@ import { JobGenerator } from "../core/job.js";
|
|
|
7
7
|
import { Runner } from "../core/runner.js";
|
|
8
8
|
import { ConsoleReporter } from "../output/console.js";
|
|
9
9
|
import { Logger } from "../output/logger.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
findPreviousFailures,
|
|
12
|
+
type PreviousViolation,
|
|
13
|
+
} from "../utils/log-parser.js";
|
|
14
|
+
import { readSessionRef, writeSessionRef } from "../utils/session-ref.js";
|
|
15
|
+
import {
|
|
16
|
+
acquireLock,
|
|
17
|
+
cleanLogs,
|
|
18
|
+
hasExistingLogs,
|
|
19
|
+
releaseLock,
|
|
20
|
+
} from "./shared.js";
|
|
11
21
|
|
|
12
22
|
export function registerRunCommand(program: Command): void {
|
|
13
23
|
program
|
|
@@ -24,14 +34,14 @@ export function registerRunCommand(program: Command): void {
|
|
|
24
34
|
"Use diff for current uncommitted changes (staged and unstaged)",
|
|
25
35
|
)
|
|
26
36
|
.action(async (options) => {
|
|
37
|
+
let config: Awaited<ReturnType<typeof loadConfig>> | undefined;
|
|
38
|
+
let lockAcquired = false;
|
|
27
39
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
await rotateLogs(config.project.log_dir);
|
|
40
|
+
config = await loadConfig();
|
|
41
|
+
await acquireLock(config.project.log_dir);
|
|
42
|
+
lockAcquired = true;
|
|
32
43
|
|
|
33
44
|
// Determine effective base branch
|
|
34
|
-
// Priority: CLI override > CI env var > config
|
|
35
45
|
const effectiveBaseBranch =
|
|
36
46
|
options.baseBranch ||
|
|
37
47
|
(process.env.GITHUB_BASE_REF &&
|
|
@@ -40,10 +50,73 @@ export function registerRunCommand(program: Command): void {
|
|
|
40
50
|
: null) ||
|
|
41
51
|
config.project.base_branch;
|
|
42
52
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
// Detect rerun mode: if logs exist and no explicit diff flags, use uncommitted + inject failures
|
|
54
|
+
const logsExist = await hasExistingLogs(config.project.log_dir);
|
|
55
|
+
const isRerun = logsExist && !options.uncommitted && !options.commit;
|
|
56
|
+
|
|
57
|
+
let failuresMap:
|
|
58
|
+
| Map<string, Map<string, PreviousViolation[]>>
|
|
59
|
+
| undefined;
|
|
60
|
+
let changeOptions:
|
|
61
|
+
| { commit?: string; uncommitted?: boolean; fixBase?: string }
|
|
62
|
+
| undefined;
|
|
63
|
+
|
|
64
|
+
if (isRerun) {
|
|
65
|
+
console.log(
|
|
66
|
+
chalk.dim(
|
|
67
|
+
"Existing logs detected — running in verification mode...",
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
const previousFailures = await findPreviousFailures(
|
|
71
|
+
config.project.log_dir,
|
|
72
|
+
options.gate,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
failuresMap = new Map();
|
|
76
|
+
for (const gateFailure of previousFailures) {
|
|
77
|
+
const adapterMap = new Map<string, PreviousViolation[]>();
|
|
78
|
+
for (const af of gateFailure.adapterFailures) {
|
|
79
|
+
adapterMap.set(af.adapterName, af.violations);
|
|
80
|
+
}
|
|
81
|
+
failuresMap.set(gateFailure.jobId, adapterMap);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (previousFailures.length > 0) {
|
|
85
|
+
const totalViolations = previousFailures.reduce(
|
|
86
|
+
(sum, gf) =>
|
|
87
|
+
sum +
|
|
88
|
+
gf.adapterFailures.reduce(
|
|
89
|
+
(s, af) => s + af.violations.length,
|
|
90
|
+
0,
|
|
91
|
+
),
|
|
92
|
+
0,
|
|
93
|
+
);
|
|
94
|
+
console.log(
|
|
95
|
+
chalk.yellow(
|
|
96
|
+
`Found ${previousFailures.length} gate(s) with ${totalViolations} previous violation(s)`,
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
changeOptions = { uncommitted: true };
|
|
102
|
+
const fixBase = await readSessionRef(config.project.log_dir);
|
|
103
|
+
if (fixBase) {
|
|
104
|
+
changeOptions.fixBase = fixBase;
|
|
105
|
+
}
|
|
106
|
+
} else if (options.commit || options.uncommitted) {
|
|
107
|
+
changeOptions = {
|
|
108
|
+
commit: options.commit,
|
|
109
|
+
uncommitted: options.uncommitted,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const changeDetector = new ChangeDetector(
|
|
114
|
+
effectiveBaseBranch,
|
|
115
|
+
changeOptions || {
|
|
116
|
+
commit: options.commit,
|
|
117
|
+
uncommitted: options.uncommitted,
|
|
118
|
+
},
|
|
119
|
+
);
|
|
47
120
|
const expander = new EntryPointExpander();
|
|
48
121
|
const jobGen = new JobGenerator(config);
|
|
49
122
|
|
|
@@ -52,6 +125,7 @@ export function registerRunCommand(program: Command): void {
|
|
|
52
125
|
|
|
53
126
|
if (changes.length === 0) {
|
|
54
127
|
console.log(chalk.green("No changes detected."));
|
|
128
|
+
await releaseLock(config.project.log_dir);
|
|
55
129
|
process.exit(0);
|
|
56
130
|
}
|
|
57
131
|
|
|
@@ -69,6 +143,7 @@ export function registerRunCommand(program: Command): void {
|
|
|
69
143
|
|
|
70
144
|
if (jobs.length === 0) {
|
|
71
145
|
console.log(chalk.yellow("No applicable gates for these changes."));
|
|
146
|
+
await releaseLock(config.project.log_dir);
|
|
72
147
|
process.exit(0);
|
|
73
148
|
}
|
|
74
149
|
|
|
@@ -80,14 +155,25 @@ export function registerRunCommand(program: Command): void {
|
|
|
80
155
|
config,
|
|
81
156
|
logger,
|
|
82
157
|
reporter,
|
|
83
|
-
|
|
84
|
-
|
|
158
|
+
failuresMap,
|
|
159
|
+
changeOptions,
|
|
85
160
|
effectiveBaseBranch,
|
|
86
161
|
);
|
|
87
162
|
|
|
88
163
|
const success = await runner.run(jobs);
|
|
164
|
+
|
|
165
|
+
if (success) {
|
|
166
|
+
await cleanLogs(config.project.log_dir);
|
|
167
|
+
} else {
|
|
168
|
+
await writeSessionRef(config.project.log_dir);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
await releaseLock(config.project.log_dir);
|
|
89
172
|
process.exit(success ? 0 : 1);
|
|
90
173
|
} catch (error: unknown) {
|
|
174
|
+
if (config && lockAcquired) {
|
|
175
|
+
await releaseLock(config.project.log_dir);
|
|
176
|
+
}
|
|
91
177
|
const err = error as { message?: string };
|
|
92
178
|
console.error(chalk.red("Error:"), err.message);
|
|
93
179
|
process.exit(1);
|