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/wait-ci.ts
DELETED
|
@@ -1,518 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import type { Command } from "commander";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Output structure from wait-ci command.
|
|
6
|
-
*/
|
|
7
|
-
export interface WaitCIResult {
|
|
8
|
-
ci_status: "passed" | "failed" | "pending" | "error";
|
|
9
|
-
pr_number?: number;
|
|
10
|
-
pr_url?: string;
|
|
11
|
-
failed_checks: Array<{
|
|
12
|
-
name: string;
|
|
13
|
-
conclusion: string;
|
|
14
|
-
details_url: string;
|
|
15
|
-
/** Actual error output from GitHub Actions logs (if available) */
|
|
16
|
-
log_output?: string;
|
|
17
|
-
}>;
|
|
18
|
-
review_comments: Array<{
|
|
19
|
-
author: string;
|
|
20
|
-
body: string;
|
|
21
|
-
path?: string;
|
|
22
|
-
line?: number;
|
|
23
|
-
}>;
|
|
24
|
-
elapsed_seconds: number;
|
|
25
|
-
error_message?: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Check if gh CLI is available.
|
|
30
|
-
*/
|
|
31
|
-
async function isGhAvailable(): Promise<boolean> {
|
|
32
|
-
return new Promise((resolve) => {
|
|
33
|
-
const proc = spawn("gh", ["--version"], { stdio: "pipe" });
|
|
34
|
-
proc.on("close", (code) => resolve(code === 0));
|
|
35
|
-
proc.on("error", () => resolve(false));
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Run a gh command and return the output.
|
|
41
|
-
*/
|
|
42
|
-
async function runGh(
|
|
43
|
-
args: string[],
|
|
44
|
-
cwd?: string,
|
|
45
|
-
): Promise<{ code: number; stdout: string; stderr: string }> {
|
|
46
|
-
return new Promise((resolve) => {
|
|
47
|
-
const proc = spawn("gh", args, { stdio: "pipe", cwd });
|
|
48
|
-
let stdout = "";
|
|
49
|
-
let stderr = "";
|
|
50
|
-
|
|
51
|
-
proc.stdout.on("data", (data) => {
|
|
52
|
-
stdout += data.toString();
|
|
53
|
-
});
|
|
54
|
-
proc.stderr.on("data", (data) => {
|
|
55
|
-
stderr += data.toString();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
proc.on("close", (code) => {
|
|
59
|
-
resolve({ code: code ?? 1, stdout, stderr });
|
|
60
|
-
});
|
|
61
|
-
proc.on("error", (err) => {
|
|
62
|
-
resolve({ code: 1, stdout: "", stderr: err.message });
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Get PR info for the current branch.
|
|
69
|
-
* Returns null if no PR found or on error.
|
|
70
|
-
*/
|
|
71
|
-
async function getPRInfo(
|
|
72
|
-
cwd?: string,
|
|
73
|
-
): Promise<{ number: number; url: string; headRefName: string } | null> {
|
|
74
|
-
const result = await runGh(
|
|
75
|
-
["pr", "view", "--json", "number,url,headRefName"],
|
|
76
|
-
cwd,
|
|
77
|
-
);
|
|
78
|
-
if (result.code !== 0) {
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
try {
|
|
82
|
-
return JSON.parse(result.stdout.trim());
|
|
83
|
-
} catch {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Get CI check statuses for a PR.
|
|
90
|
-
* Returns null on error, empty array if no checks.
|
|
91
|
-
* Note: gh pr checks uses 'state' (FAILURE/SUCCESS/PENDING) and 'link' (not conclusion/detailsUrl)
|
|
92
|
-
*/
|
|
93
|
-
async function getChecks(cwd?: string): Promise<Array<{
|
|
94
|
-
name: string;
|
|
95
|
-
state: string;
|
|
96
|
-
link: string;
|
|
97
|
-
}> | null> {
|
|
98
|
-
const result = await runGh(
|
|
99
|
-
["pr", "checks", "--json", "name,state,link"],
|
|
100
|
-
cwd,
|
|
101
|
-
);
|
|
102
|
-
const output = result.stdout.trim();
|
|
103
|
-
if (!output) {
|
|
104
|
-
return result.code === 0 ? [] : null;
|
|
105
|
-
}
|
|
106
|
-
try {
|
|
107
|
-
return JSON.parse(output) || [];
|
|
108
|
-
} catch {
|
|
109
|
-
return result.code === 0 ? [] : null;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Get reviews for a PR.
|
|
115
|
-
* Returns null on error, empty array if no reviews.
|
|
116
|
-
*/
|
|
117
|
-
async function getReviews(
|
|
118
|
-
prNumber: number,
|
|
119
|
-
cwd?: string,
|
|
120
|
-
): Promise<Array<{
|
|
121
|
-
author: { login: string };
|
|
122
|
-
state: string;
|
|
123
|
-
body: string;
|
|
124
|
-
}> | null> {
|
|
125
|
-
// Get owner/repo from gh
|
|
126
|
-
const repoResult = await runGh(["repo", "view", "--json", "owner,name"], cwd);
|
|
127
|
-
if (repoResult.code !== 0) {
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
let owner: string;
|
|
131
|
-
let repo: string;
|
|
132
|
-
try {
|
|
133
|
-
const repoInfo = JSON.parse(repoResult.stdout.trim());
|
|
134
|
-
owner = repoInfo.owner.login;
|
|
135
|
-
repo = repoInfo.name;
|
|
136
|
-
} catch {
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const result = await runGh(
|
|
141
|
-
[
|
|
142
|
-
"api",
|
|
143
|
-
"--paginate",
|
|
144
|
-
`repos/${owner}/${repo}/pulls/${prNumber}/reviews?per_page=100`,
|
|
145
|
-
],
|
|
146
|
-
cwd,
|
|
147
|
-
);
|
|
148
|
-
if (result.code !== 0) {
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
try {
|
|
152
|
-
// GitHub API returns 'user' not 'author', so we transform the response
|
|
153
|
-
const rawReviews = JSON.parse(result.stdout.trim()) || [];
|
|
154
|
-
return rawReviews
|
|
155
|
-
.filter(
|
|
156
|
-
(r: { user?: { login: string }; state: string; body: string }) =>
|
|
157
|
-
r.user?.login,
|
|
158
|
-
)
|
|
159
|
-
.map((r: { user: { login: string }; state: string; body: string }) => ({
|
|
160
|
-
author: { login: r.user.login },
|
|
161
|
-
state: r.state,
|
|
162
|
-
body: r.body || "",
|
|
163
|
-
}));
|
|
164
|
-
} catch {
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Get the latest review state per author.
|
|
171
|
-
* GitHub API returns all historical reviews, so we need to deduplicate
|
|
172
|
-
* to find each reviewer's current state.
|
|
173
|
-
*/
|
|
174
|
-
function getLatestReviewsByAuthor(
|
|
175
|
-
reviews: Array<{ author: { login: string }; state: string; body: string }>,
|
|
176
|
-
): Array<{ author: { login: string }; state: string; body: string }> {
|
|
177
|
-
const latestByAuthor = new Map<
|
|
178
|
-
string,
|
|
179
|
-
{ author: { login: string }; state: string; body: string }
|
|
180
|
-
>();
|
|
181
|
-
// Process in order - later reviews override earlier ones
|
|
182
|
-
for (const review of reviews) {
|
|
183
|
-
latestByAuthor.set(review.author.login, review);
|
|
184
|
-
}
|
|
185
|
-
return Array.from(latestByAuthor.values());
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Sleep for a given number of milliseconds.
|
|
190
|
-
*/
|
|
191
|
-
function sleep(ms: number): Promise<void> {
|
|
192
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Extract GitHub Actions run ID from a check link.
|
|
197
|
-
* Links look like: https://github.com/owner/repo/actions/runs/RUN_ID/job/JOB_ID
|
|
198
|
-
* Returns null if the link is not a GitHub Actions link.
|
|
199
|
-
*/
|
|
200
|
-
function extractRunId(link: string): string | null {
|
|
201
|
-
const match = link.match(/\/actions\/runs\/(\d+)/);
|
|
202
|
-
return match ? match[1]! : null;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Fetch failed job logs for a GitHub Actions run.
|
|
207
|
-
* Uses `gh run view <run-id> --log-failed` to get actual error output.
|
|
208
|
-
* Returns null if logs can't be fetched.
|
|
209
|
-
*/
|
|
210
|
-
async function getFailedRunLogs(
|
|
211
|
-
runId: string,
|
|
212
|
-
cwd?: string,
|
|
213
|
-
): Promise<string | null> {
|
|
214
|
-
const result = await runGh(["run", "view", runId, "--log-failed"], cwd);
|
|
215
|
-
if (result.code !== 0 || !result.stdout.trim()) {
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
// Limit output to avoid huge payloads (keep last ~100 lines)
|
|
219
|
-
const lines = result.stdout.trim().split("\n");
|
|
220
|
-
const maxLines = 100;
|
|
221
|
-
if (lines.length > maxLines) {
|
|
222
|
-
return `... (${lines.length - maxLines} lines truncated)\n${lines.slice(-maxLines).join("\n")}`;
|
|
223
|
-
}
|
|
224
|
-
return result.stdout.trim();
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/** Group checks by run ID, separating GitHub Actions from external checks */
|
|
228
|
-
function groupChecksByRunId(
|
|
229
|
-
failedChecks: Array<{ name: string; state: string; link: string }>,
|
|
230
|
-
): Map<string, Array<{ name: string; state: string; link: string }>> {
|
|
231
|
-
const runIdToChecks = new Map<
|
|
232
|
-
string,
|
|
233
|
-
Array<{ name: string; state: string; link: string }>
|
|
234
|
-
>();
|
|
235
|
-
|
|
236
|
-
for (const check of failedChecks) {
|
|
237
|
-
const runId = extractRunId(check.link);
|
|
238
|
-
// Use empty string for external checks (no run ID)
|
|
239
|
-
const key = runId ?? "";
|
|
240
|
-
const existing = runIdToChecks.get(key) ?? [];
|
|
241
|
-
existing.push(check);
|
|
242
|
-
runIdToChecks.set(key, existing);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return runIdToChecks;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Fetch failure logs for failed checks.
|
|
250
|
-
* Only works for GitHub Actions checks; external checks (CodeScene, etc.) return null.
|
|
251
|
-
* Fetches logs in parallel for better performance.
|
|
252
|
-
*/
|
|
253
|
-
async function enrichFailedChecksWithLogs(
|
|
254
|
-
failedChecks: Array<{ name: string; state: string; link: string }>,
|
|
255
|
-
cwd?: string,
|
|
256
|
-
): Promise<
|
|
257
|
-
Array<{ name: string; state: string; link: string; log_output?: string }>
|
|
258
|
-
> {
|
|
259
|
-
const runIdToChecks = groupChecksByRunId(failedChecks);
|
|
260
|
-
|
|
261
|
-
// Fetch logs in parallel for all unique run IDs
|
|
262
|
-
const entries = Array.from(runIdToChecks.entries());
|
|
263
|
-
const logResults = await Promise.all(
|
|
264
|
-
entries.map(([runId]) =>
|
|
265
|
-
runId ? getFailedRunLogs(runId, cwd) : Promise.resolve(null),
|
|
266
|
-
),
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
// Build results with fetched logs
|
|
270
|
-
const results: Array<{
|
|
271
|
-
name: string;
|
|
272
|
-
state: string;
|
|
273
|
-
link: string;
|
|
274
|
-
log_output?: string;
|
|
275
|
-
}> = [];
|
|
276
|
-
|
|
277
|
-
for (let i = 0; i < entries.length; i++) {
|
|
278
|
-
const [, checks] = entries[i]!;
|
|
279
|
-
const logs = logResults[i];
|
|
280
|
-
for (const check of checks) {
|
|
281
|
-
results.push({ ...check, log_output: logs ?? undefined });
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return results;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/** Options for creating a WaitCIResult */
|
|
289
|
-
interface ResultOptions {
|
|
290
|
-
status: WaitCIResult["ci_status"];
|
|
291
|
-
startTime: number;
|
|
292
|
-
prInfo?: { number: number; url: string };
|
|
293
|
-
errorMessage?: string;
|
|
294
|
-
failedChecks?: Array<{
|
|
295
|
-
name: string;
|
|
296
|
-
state: string;
|
|
297
|
-
link: string;
|
|
298
|
-
log_output?: string;
|
|
299
|
-
}>;
|
|
300
|
-
reviewComments?: Array<{ author: string; body: string }>;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/** Create a WaitCIResult with the given options */
|
|
304
|
-
function createResult(opts: ResultOptions): WaitCIResult {
|
|
305
|
-
const elapsed = Math.round((Date.now() - opts.startTime) / 1000);
|
|
306
|
-
return {
|
|
307
|
-
ci_status: opts.status,
|
|
308
|
-
pr_number: opts.prInfo?.number,
|
|
309
|
-
pr_url: opts.prInfo?.url,
|
|
310
|
-
failed_checks:
|
|
311
|
-
opts.failedChecks?.map((c) => ({
|
|
312
|
-
name: c.name,
|
|
313
|
-
conclusion: c.state.toLowerCase(),
|
|
314
|
-
details_url: c.link,
|
|
315
|
-
log_output: c.log_output,
|
|
316
|
-
})) || [],
|
|
317
|
-
review_comments: opts.reviewComments || [],
|
|
318
|
-
elapsed_seconds: elapsed,
|
|
319
|
-
error_message: opts.errorMessage,
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/** Poll outcome from a single CI status check */
|
|
324
|
-
interface PollOutcome {
|
|
325
|
-
error?: string;
|
|
326
|
-
noChecksYet?: boolean;
|
|
327
|
-
noChecksConfigured?: boolean;
|
|
328
|
-
shouldFail?: boolean;
|
|
329
|
-
shouldPass?: boolean;
|
|
330
|
-
failedChecks?: Array<{
|
|
331
|
-
name: string;
|
|
332
|
-
state: string;
|
|
333
|
-
link: string;
|
|
334
|
-
log_output?: string;
|
|
335
|
-
}>;
|
|
336
|
-
reviewComments?: Array<{ author: string; body: string }>;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/** Poll CI status and reviews once, returning the outcome */
|
|
340
|
-
async function pollCIStatus(
|
|
341
|
-
cwd: string | undefined,
|
|
342
|
-
prNumber: number,
|
|
343
|
-
isFirstPoll: boolean,
|
|
344
|
-
): Promise<PollOutcome> {
|
|
345
|
-
const checks = await getChecks(cwd);
|
|
346
|
-
const reviews = await getReviews(prNumber, cwd);
|
|
347
|
-
|
|
348
|
-
if (checks === null || reviews === null) {
|
|
349
|
-
return { error: "Failed to fetch CI status or reviews from GitHub" };
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Handle zero checks case
|
|
353
|
-
if (checks.length === 0) {
|
|
354
|
-
return isFirstPoll ? { noChecksYet: true } : { noChecksConfigured: true };
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Process checks and reviews
|
|
358
|
-
const latestReviews = getLatestReviewsByAuthor(reviews);
|
|
359
|
-
const failedChecks = checks.filter((c) => c.state === "FAILURE");
|
|
360
|
-
const blockingReviews = latestReviews.filter(
|
|
361
|
-
(r) => r.state === "CHANGES_REQUESTED",
|
|
362
|
-
);
|
|
363
|
-
const reviewComments = blockingReviews.map((r) => ({
|
|
364
|
-
author: r.author.login,
|
|
365
|
-
body: r.body || "",
|
|
366
|
-
}));
|
|
367
|
-
const pendingChecks = checks.filter(
|
|
368
|
-
(c) =>
|
|
369
|
-
c.state === "PENDING" ||
|
|
370
|
-
c.state === "QUEUED" ||
|
|
371
|
-
c.state === "IN_PROGRESS",
|
|
372
|
-
);
|
|
373
|
-
|
|
374
|
-
const shouldFail = failedChecks.length > 0 || blockingReviews.length > 0;
|
|
375
|
-
const shouldPass = pendingChecks.length === 0;
|
|
376
|
-
|
|
377
|
-
return { shouldFail, shouldPass, failedChecks, reviewComments };
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* Wait for CI to complete and check for blocking reviews.
|
|
382
|
-
* @param timeoutSeconds Maximum time to wait for CI
|
|
383
|
-
* @param pollIntervalSeconds Time between polls
|
|
384
|
-
* @param cwd Working directory for gh commands (defaults to process.cwd())
|
|
385
|
-
*/
|
|
386
|
-
export async function waitForCI(
|
|
387
|
-
timeoutSeconds: number,
|
|
388
|
-
pollIntervalSeconds: number,
|
|
389
|
-
cwd?: string,
|
|
390
|
-
): Promise<WaitCIResult> {
|
|
391
|
-
const startTime = Date.now();
|
|
392
|
-
let isFirstPoll = true;
|
|
393
|
-
|
|
394
|
-
if (!(await isGhAvailable())) {
|
|
395
|
-
return createResult({
|
|
396
|
-
status: "error",
|
|
397
|
-
startTime,
|
|
398
|
-
errorMessage: "gh CLI is not installed or not authenticated",
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const prInfo = await getPRInfo(cwd);
|
|
403
|
-
if (!prInfo) {
|
|
404
|
-
return createResult({
|
|
405
|
-
status: "error",
|
|
406
|
-
startTime,
|
|
407
|
-
errorMessage: "No PR found for current branch",
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const timeoutMs = timeoutSeconds * 1000;
|
|
412
|
-
|
|
413
|
-
while (Date.now() - startTime < timeoutMs) {
|
|
414
|
-
const pollOutcome = await pollCIStatus(cwd, prInfo.number, isFirstPoll);
|
|
415
|
-
isFirstPoll = false;
|
|
416
|
-
|
|
417
|
-
if (pollOutcome.error) {
|
|
418
|
-
return createResult({
|
|
419
|
-
status: "error",
|
|
420
|
-
startTime,
|
|
421
|
-
prInfo,
|
|
422
|
-
errorMessage: pollOutcome.error,
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
if (pollOutcome.noChecksYet) {
|
|
427
|
-
await sleep(pollIntervalSeconds * 1000);
|
|
428
|
-
continue;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
if (pollOutcome.noChecksConfigured) {
|
|
432
|
-
return createResult({ status: "passed", startTime, prInfo });
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
if (pollOutcome.shouldFail && pollOutcome.failedChecks) {
|
|
436
|
-
// Enrich failed checks with actual log output from GitHub Actions
|
|
437
|
-
const enrichedChecks = await enrichFailedChecksWithLogs(
|
|
438
|
-
pollOutcome.failedChecks,
|
|
439
|
-
cwd,
|
|
440
|
-
);
|
|
441
|
-
return createResult({
|
|
442
|
-
status: "failed",
|
|
443
|
-
startTime,
|
|
444
|
-
prInfo,
|
|
445
|
-
failedChecks: enrichedChecks,
|
|
446
|
-
reviewComments: pollOutcome.reviewComments,
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
if (pollOutcome.shouldPass) {
|
|
451
|
-
return createResult({ status: "passed", startTime, prInfo });
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
await sleep(pollIntervalSeconds * 1000);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
return createResult({ status: "pending", startTime, prInfo });
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
export function registerWaitCICommand(program: Command): void {
|
|
461
|
-
program
|
|
462
|
-
.command("wait-ci")
|
|
463
|
-
.description(
|
|
464
|
-
"Wait for CI checks to complete and check for blocking reviews",
|
|
465
|
-
)
|
|
466
|
-
.option(
|
|
467
|
-
"--timeout <seconds>",
|
|
468
|
-
"Maximum time to wait for CI (default: 270)",
|
|
469
|
-
"270",
|
|
470
|
-
)
|
|
471
|
-
.option(
|
|
472
|
-
"--poll-interval <seconds>",
|
|
473
|
-
"Time between CI status checks (default: 15)",
|
|
474
|
-
"15",
|
|
475
|
-
)
|
|
476
|
-
.action(async (options) => {
|
|
477
|
-
const timeout = Number.parseInt(options.timeout, 10);
|
|
478
|
-
const pollInterval = Number.parseInt(options.pollInterval, 10);
|
|
479
|
-
|
|
480
|
-
if (Number.isNaN(timeout) || timeout <= 0) {
|
|
481
|
-
console.log(
|
|
482
|
-
JSON.stringify({
|
|
483
|
-
ci_status: "error",
|
|
484
|
-
failed_checks: [],
|
|
485
|
-
review_comments: [],
|
|
486
|
-
elapsed_seconds: 0,
|
|
487
|
-
error_message: "Invalid timeout value",
|
|
488
|
-
}),
|
|
489
|
-
);
|
|
490
|
-
process.exit(1);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
if (Number.isNaN(pollInterval) || pollInterval <= 0) {
|
|
494
|
-
console.log(
|
|
495
|
-
JSON.stringify({
|
|
496
|
-
ci_status: "error",
|
|
497
|
-
failed_checks: [],
|
|
498
|
-
review_comments: [],
|
|
499
|
-
elapsed_seconds: 0,
|
|
500
|
-
error_message: "Invalid poll-interval value",
|
|
501
|
-
}),
|
|
502
|
-
);
|
|
503
|
-
process.exit(1);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
const result = await waitForCI(timeout, pollInterval);
|
|
507
|
-
console.log(JSON.stringify(result));
|
|
508
|
-
|
|
509
|
-
// Exit codes: 0=passed, 1=failed/error, 2=pending (timeout)
|
|
510
|
-
if (result.ci_status === "passed") {
|
|
511
|
-
process.exit(0);
|
|
512
|
-
} else if (result.ci_status === "pending") {
|
|
513
|
-
process.exit(2);
|
|
514
|
-
} else {
|
|
515
|
-
process.exit(1);
|
|
516
|
-
}
|
|
517
|
-
});
|
|
518
|
-
}
|
package/src/config/ci-loader.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import YAML from "yaml";
|
|
4
|
-
import { ciConfigSchema } from "./ci-schema.js";
|
|
5
|
-
import type { CIConfig } from "./types.js";
|
|
6
|
-
|
|
7
|
-
const GAUNTLET_DIR = ".gauntlet";
|
|
8
|
-
const CI_FILE = "ci.yml";
|
|
9
|
-
|
|
10
|
-
export async function loadCIConfig(
|
|
11
|
-
rootDir: string = process.cwd(),
|
|
12
|
-
): Promise<CIConfig> {
|
|
13
|
-
const ciPath = path.join(rootDir, GAUNTLET_DIR, CI_FILE);
|
|
14
|
-
|
|
15
|
-
if (!(await fileExists(ciPath))) {
|
|
16
|
-
throw new Error(
|
|
17
|
-
`CI configuration file not found at ${ciPath}. Run 'agent-gauntlet ci init' to create it.`,
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const content = await fs.readFile(ciPath, "utf-8");
|
|
22
|
-
const raw = YAML.parse(content);
|
|
23
|
-
return ciConfigSchema.parse(raw);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function fileExists(path: string): Promise<boolean> {
|
|
27
|
-
try {
|
|
28
|
-
const stat = await fs.stat(path);
|
|
29
|
-
return stat.isFile();
|
|
30
|
-
} catch {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
}
|
package/src/config/ci-schema.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
// Runtime and service schemas use z.any() to allow flexibility for different CI providers
|
|
4
|
-
// Each provider (GitHub Actions, GitLab CI, etc.) has its own configuration structure
|
|
5
|
-
export const runtimeConfigSchema = z.record(z.string(), z.any());
|
|
6
|
-
|
|
7
|
-
export const serviceConfigSchema = z.record(z.string(), z.any());
|
|
8
|
-
|
|
9
|
-
export const ciSetupStepSchema = z.object({
|
|
10
|
-
name: z.string().min(1),
|
|
11
|
-
run: z.string().min(1),
|
|
12
|
-
working_directory: z.string().optional(),
|
|
13
|
-
if: z.string().optional(),
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
export const ciCheckConfigSchema = z.object({
|
|
17
|
-
name: z.string().min(1),
|
|
18
|
-
requires_runtimes: z.array(z.string()).optional(),
|
|
19
|
-
requires_services: z.array(z.string()).optional(),
|
|
20
|
-
setup: z.array(ciSetupStepSchema).optional(),
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
export const ciConfigSchema = z.object({
|
|
24
|
-
runtimes: runtimeConfigSchema.nullable().optional(),
|
|
25
|
-
services: serviceConfigSchema.nullable().optional(),
|
|
26
|
-
setup: z.array(ciSetupStepSchema).nullable().optional(),
|
|
27
|
-
checks: z.array(ciCheckConfigSchema).nullable().optional(),
|
|
28
|
-
});
|
package/src/config/global.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import YAML from "yaml";
|
|
5
|
-
import { z } from "zod";
|
|
6
|
-
|
|
7
|
-
const GLOBAL_CONFIG_PATH = path.join(
|
|
8
|
-
os.homedir(),
|
|
9
|
-
".config",
|
|
10
|
-
"agent-gauntlet",
|
|
11
|
-
"config.yml",
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
export const debugLogConfigSchema = z.object({
|
|
15
|
-
enabled: z.boolean().default(false),
|
|
16
|
-
max_size_mb: z.number().default(10),
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
export type DebugLogConfig = z.infer<typeof debugLogConfigSchema>;
|
|
20
|
-
|
|
21
|
-
const globalConfigSchema = z.object({
|
|
22
|
-
stop_hook: z
|
|
23
|
-
.object({
|
|
24
|
-
enabled: z.boolean().default(true),
|
|
25
|
-
run_interval_minutes: z.number().default(5),
|
|
26
|
-
auto_push_pr: z.boolean().default(false),
|
|
27
|
-
auto_fix_pr: z.boolean().default(false),
|
|
28
|
-
})
|
|
29
|
-
.default({
|
|
30
|
-
enabled: true,
|
|
31
|
-
run_interval_minutes: 5,
|
|
32
|
-
auto_push_pr: false,
|
|
33
|
-
auto_fix_pr: false,
|
|
34
|
-
}),
|
|
35
|
-
debug_log: debugLogConfigSchema.default({ enabled: false, max_size_mb: 10 }),
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
export type GlobalConfig = z.infer<typeof globalConfigSchema>;
|
|
39
|
-
|
|
40
|
-
export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = {
|
|
41
|
-
stop_hook: {
|
|
42
|
-
enabled: true,
|
|
43
|
-
run_interval_minutes: 5,
|
|
44
|
-
auto_push_pr: false,
|
|
45
|
-
auto_fix_pr: false,
|
|
46
|
-
},
|
|
47
|
-
debug_log: {
|
|
48
|
-
enabled: false,
|
|
49
|
-
max_size_mb: 10,
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Load the global agent-gauntlet configuration.
|
|
55
|
-
* Returns default values if the file doesn't exist or is invalid.
|
|
56
|
-
*/
|
|
57
|
-
export async function loadGlobalConfig(): Promise<GlobalConfig> {
|
|
58
|
-
try {
|
|
59
|
-
const content = await fs.readFile(GLOBAL_CONFIG_PATH, "utf-8");
|
|
60
|
-
const raw = YAML.parse(content);
|
|
61
|
-
return globalConfigSchema.parse(raw);
|
|
62
|
-
} catch (error) {
|
|
63
|
-
// Check if file doesn't exist (expected case)
|
|
64
|
-
if (
|
|
65
|
-
typeof error === "object" &&
|
|
66
|
-
error !== null &&
|
|
67
|
-
"code" in error &&
|
|
68
|
-
(error as { code: string }).code === "ENOENT"
|
|
69
|
-
) {
|
|
70
|
-
return DEFAULT_GLOBAL_CONFIG;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// File exists but is invalid - log warning and use defaults
|
|
74
|
-
console.error(
|
|
75
|
-
`[gauntlet] Warning: Failed to parse global config at ${GLOBAL_CONFIG_PATH}, using defaults`,
|
|
76
|
-
);
|
|
77
|
-
return DEFAULT_GLOBAL_CONFIG;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Get the path to the global config file.
|
|
83
|
-
* Useful for debugging or documentation.
|
|
84
|
-
*/
|
|
85
|
-
export function getGlobalConfigPath(): string {
|
|
86
|
-
return GLOBAL_CONFIG_PATH;
|
|
87
|
-
}
|