agent-gauntlet 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +902 -565
- package/dist/index.js.map +16 -13
- package/dist/scripts/status.js +10 -2
- package/dist/scripts/status.js.map +3 -3
- package/package.json +2 -1
- package/skills/gauntlet-check/SKILL.md +22 -0
- package/skills/gauntlet-run/SKILL.md +67 -0
- package/skills/gauntlet-run/extract-prompt.md +121 -0
- package/skills/gauntlet-run/update-prompt.md +113 -0
- package/{dist/skill-templates/status.md → skills/gauntlet-status/SKILL.md} +1 -1
- /package/{dist/skill-templates/fix-pr.md → skills/gauntlet-fix-pr/SKILL.md} +0 -0
- /package/{dist/skill-templates/help-skill.md → skills/gauntlet-help/SKILL.md} +0 -0
- /package/{dist/skill-templates/help-ref-adapter-troubleshooting.md → skills/gauntlet-help/references/adapter-troubleshooting.md} +0 -0
- /package/{dist/skill-templates/help-ref-ci-pr-troubleshooting.md → skills/gauntlet-help/references/ci-pr-troubleshooting.md} +0 -0
- /package/{dist/skill-templates/help-ref-config-troubleshooting.md → skills/gauntlet-help/references/config-troubleshooting.md} +0 -0
- /package/{dist/skill-templates/help-ref-gate-troubleshooting.md → skills/gauntlet-help/references/gate-troubleshooting.md} +0 -0
- /package/{dist/skill-templates/help-ref-lock-troubleshooting.md → skills/gauntlet-help/references/lock-troubleshooting.md} +0 -0
- /package/{dist/skill-templates/help-ref-stop-hook-troubleshooting.md → skills/gauntlet-help/references/stop-hook-troubleshooting.md} +0 -0
- /package/{dist/skill-templates/push-pr.md → skills/gauntlet-push-pr/SKILL.md} +0 -0
- /package/{dist/skill-templates/setup-skill.md → skills/gauntlet-setup/SKILL.md} +0 -0
- /package/{dist/skill-templates → skills/gauntlet-setup/references}/check-catalog.md +0 -0
- /package/{dist/skill-templates/setup-ref-project-structure.md → skills/gauntlet-setup/references/project-structure.md} +0 -0
package/dist/index.js
CHANGED
|
@@ -2,12 +2,292 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
4
|
|
|
5
|
+
// src/scripts/status.ts
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
function parseKeyValue(text) {
|
|
9
|
+
const result = {};
|
|
10
|
+
for (const match of text.matchAll(/(\w+)=(\S+)/g)) {
|
|
11
|
+
const key = match[1];
|
|
12
|
+
const value = match[2];
|
|
13
|
+
if (key && value)
|
|
14
|
+
result[key] = value;
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
function parseTimestamp(line) {
|
|
19
|
+
const m = line.match(/^\[([^\]]+)\]/);
|
|
20
|
+
return m?.[1] ?? "";
|
|
21
|
+
}
|
|
22
|
+
function parseEventType(line) {
|
|
23
|
+
const m = line.match(/^\[[^\]]+\]\s+(\S+)/);
|
|
24
|
+
return m?.[1] ?? "";
|
|
25
|
+
}
|
|
26
|
+
function parseEventBody(line) {
|
|
27
|
+
const m = line.match(/^\[[^\]]+\]\s+\S+\s*(.*)/);
|
|
28
|
+
return m?.[1] ?? "";
|
|
29
|
+
}
|
|
30
|
+
function parseDebugLog(content, sessionStartTime) {
|
|
31
|
+
const lines = content.split(`
|
|
32
|
+
`).filter((l) => l.trim());
|
|
33
|
+
const sessions = [];
|
|
34
|
+
let current = null;
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
const event = parseEventType(line);
|
|
37
|
+
const body = parseEventBody(line);
|
|
38
|
+
const ts = parseTimestamp(line);
|
|
39
|
+
switch (event) {
|
|
40
|
+
case "RUN_START": {
|
|
41
|
+
if (sessionStartTime && new Date(ts) < sessionStartTime) {
|
|
42
|
+
current = null;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
const kv = parseKeyValue(body);
|
|
46
|
+
current = {
|
|
47
|
+
start: {
|
|
48
|
+
timestamp: ts,
|
|
49
|
+
mode: kv.mode ?? "unknown",
|
|
50
|
+
baseRef: kv.base_ref,
|
|
51
|
+
filesChanged: Number(kv.files_changed ?? kv.changes ?? 0),
|
|
52
|
+
linesAdded: Number(kv.lines_added ?? 0),
|
|
53
|
+
linesRemoved: Number(kv.lines_removed ?? 0),
|
|
54
|
+
gates: Number(kv.gates ?? 0)
|
|
55
|
+
},
|
|
56
|
+
gates: []
|
|
57
|
+
};
|
|
58
|
+
sessions.push(current);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
case "GATE_RESULT": {
|
|
62
|
+
if (!current)
|
|
63
|
+
break;
|
|
64
|
+
const gateIdMatch = body.match(/^(\S+)/);
|
|
65
|
+
const kv = parseKeyValue(body);
|
|
66
|
+
current.gates.push({
|
|
67
|
+
timestamp: ts,
|
|
68
|
+
gateId: gateIdMatch?.[1] ?? "unknown",
|
|
69
|
+
cli: kv.cli,
|
|
70
|
+
status: kv.status ?? "unknown",
|
|
71
|
+
duration: kv.duration ?? "?",
|
|
72
|
+
violations: kv.violations !== undefined ? Number(kv.violations) : undefined
|
|
73
|
+
});
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case "RUN_END": {
|
|
77
|
+
if (!current)
|
|
78
|
+
break;
|
|
79
|
+
const kv = parseKeyValue(body);
|
|
80
|
+
current.end = {
|
|
81
|
+
timestamp: ts,
|
|
82
|
+
status: kv.status ?? "unknown",
|
|
83
|
+
fixed: Number(kv.fixed ?? 0),
|
|
84
|
+
skipped: Number(kv.skipped ?? 0),
|
|
85
|
+
failed: Number(kv.failed ?? 0),
|
|
86
|
+
iterations: Number(kv.iterations ?? 0),
|
|
87
|
+
duration: kv.duration ?? "?"
|
|
88
|
+
};
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
case "STOP_HOOK": {
|
|
92
|
+
if (!current)
|
|
93
|
+
break;
|
|
94
|
+
const kv = parseKeyValue(body);
|
|
95
|
+
current.stopHook = {
|
|
96
|
+
timestamp: ts,
|
|
97
|
+
decision: kv.decision ?? "unknown",
|
|
98
|
+
reason: kv.reason ?? "unknown"
|
|
99
|
+
};
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return sessions;
|
|
105
|
+
}
|
|
106
|
+
function getSessionStartTime(logDir) {
|
|
107
|
+
const entries = fs.readdirSync(logDir).filter((f) => !f.startsWith(".") && f !== "previous");
|
|
108
|
+
let earliest;
|
|
109
|
+
for (const entry of entries) {
|
|
110
|
+
const mtime = fs.statSync(path.join(logDir, entry)).mtimeMs;
|
|
111
|
+
if (earliest === undefined || mtime < earliest) {
|
|
112
|
+
earliest = mtime;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return earliest !== undefined ? new Date(earliest) : undefined;
|
|
116
|
+
}
|
|
117
|
+
function formatFileInventory(logDir) {
|
|
118
|
+
const lines = [];
|
|
119
|
+
const entries = fs.readdirSync(logDir).filter((f) => !f.startsWith(".") && f !== "previous");
|
|
120
|
+
if (entries.length === 0)
|
|
121
|
+
return lines;
|
|
122
|
+
const checks = [];
|
|
123
|
+
const reviews = [];
|
|
124
|
+
const other = [];
|
|
125
|
+
for (const entry of entries.sort()) {
|
|
126
|
+
const fullPath = path.join(logDir, entry);
|
|
127
|
+
const stat = fs.statSync(fullPath);
|
|
128
|
+
const sizeKB = (stat.size / 1024).toFixed(1);
|
|
129
|
+
const line = `- ${fullPath} (${sizeKB} KB)`;
|
|
130
|
+
if (entry.startsWith("review_")) {
|
|
131
|
+
reviews.push(line);
|
|
132
|
+
} else if (entry.startsWith("check_")) {
|
|
133
|
+
checks.push(line);
|
|
134
|
+
} else {
|
|
135
|
+
other.push(line);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
lines.push("### Log Files");
|
|
139
|
+
lines.push("");
|
|
140
|
+
if (checks.length > 0) {
|
|
141
|
+
lines.push("**Check logs:**");
|
|
142
|
+
lines.push(...checks);
|
|
143
|
+
}
|
|
144
|
+
if (reviews.length > 0) {
|
|
145
|
+
lines.push("**Review logs/JSON:**");
|
|
146
|
+
lines.push(...reviews);
|
|
147
|
+
}
|
|
148
|
+
if (other.length > 0) {
|
|
149
|
+
lines.push("**Other:**");
|
|
150
|
+
lines.push(...other);
|
|
151
|
+
}
|
|
152
|
+
lines.push("");
|
|
153
|
+
return lines;
|
|
154
|
+
}
|
|
155
|
+
function formatStatusLine(end) {
|
|
156
|
+
return end.status === "pass" ? "PASSED" : end.status === "fail" ? "FAILED" : end.status.toUpperCase();
|
|
157
|
+
}
|
|
158
|
+
function formatAllRuns(sessions) {
|
|
159
|
+
const lines = [];
|
|
160
|
+
lines.push("### All Runs in Session");
|
|
161
|
+
lines.push("");
|
|
162
|
+
for (let i = 0;i < sessions.length; i++) {
|
|
163
|
+
const s = sessions[i];
|
|
164
|
+
if (!s)
|
|
165
|
+
continue;
|
|
166
|
+
const status = s.end ? s.end.status : "in-progress";
|
|
167
|
+
const duration = s.end ? s.end.duration : "?";
|
|
168
|
+
lines.push(`${i + 1}. [${s.start.timestamp}] mode=${s.start.mode} status=${status} duration=${duration}`);
|
|
169
|
+
}
|
|
170
|
+
lines.push("");
|
|
171
|
+
return lines;
|
|
172
|
+
}
|
|
173
|
+
function formatSession(sessions, logDir) {
|
|
174
|
+
if (sessions.length === 0) {
|
|
175
|
+
return "No gauntlet runs found in logs.";
|
|
176
|
+
}
|
|
177
|
+
const lastComplete = [...sessions].reverse().find((s) => s.end);
|
|
178
|
+
const session = lastComplete ?? sessions[sessions.length - 1];
|
|
179
|
+
if (!session)
|
|
180
|
+
return "No gauntlet runs found in logs.";
|
|
181
|
+
const lines = [];
|
|
182
|
+
lines.push("## Gauntlet Session Summary");
|
|
183
|
+
lines.push("");
|
|
184
|
+
if (session.end) {
|
|
185
|
+
lines.push(`**Status:** ${formatStatusLine(session.end)}`);
|
|
186
|
+
lines.push(`**Iterations:** ${session.end.iterations}`);
|
|
187
|
+
lines.push(`**Duration:** ${session.end.duration}`);
|
|
188
|
+
lines.push(`**Fixed:** ${session.end.fixed} | **Skipped:** ${session.end.skipped} | **Failed:** ${session.end.failed}`);
|
|
189
|
+
} else {
|
|
190
|
+
lines.push("**Status:** In Progress (no RUN_END found)");
|
|
191
|
+
}
|
|
192
|
+
lines.push("");
|
|
193
|
+
lines.push("### Diff Stats");
|
|
194
|
+
lines.push(`- Mode: ${session.start.mode}`);
|
|
195
|
+
if (session.start.baseRef) {
|
|
196
|
+
lines.push(`- Base ref: ${session.start.baseRef}`);
|
|
197
|
+
}
|
|
198
|
+
lines.push(`- Files changed: ${session.start.filesChanged}`);
|
|
199
|
+
lines.push(`- Lines: +${session.start.linesAdded} / -${session.start.linesRemoved}`);
|
|
200
|
+
lines.push(`- Gates: ${session.start.gates}`);
|
|
201
|
+
lines.push("");
|
|
202
|
+
lines.push("### Gate Results");
|
|
203
|
+
lines.push("");
|
|
204
|
+
lines.push("| Gate | CLI | Status | Duration | Violations |");
|
|
205
|
+
lines.push("|------|-----|--------|----------|------------|");
|
|
206
|
+
for (const gate of session.gates) {
|
|
207
|
+
const violations = gate.violations !== undefined ? String(gate.violations) : "-";
|
|
208
|
+
const statusIcon = gate.status === "pass" ? "pass" : "FAIL";
|
|
209
|
+
lines.push(`| ${gate.gateId} | ${gate.cli ?? "-"} | ${statusIcon} | ${gate.duration} | ${violations} |`);
|
|
210
|
+
}
|
|
211
|
+
lines.push("");
|
|
212
|
+
if (session.stopHook) {
|
|
213
|
+
lines.push("### Stop Hook");
|
|
214
|
+
lines.push(`- Decision: ${session.stopHook.decision}`);
|
|
215
|
+
lines.push(`- Reason: ${session.stopHook.reason}`);
|
|
216
|
+
lines.push("");
|
|
217
|
+
}
|
|
218
|
+
lines.push(...formatFileInventory(logDir));
|
|
219
|
+
if (sessions.length > 1) {
|
|
220
|
+
lines.push(...formatAllRuns(sessions));
|
|
221
|
+
}
|
|
222
|
+
return lines.join(`
|
|
223
|
+
`);
|
|
224
|
+
}
|
|
225
|
+
function getLogDir(cwd) {
|
|
226
|
+
const configPath = path.join(cwd, ".gauntlet", "config.yml");
|
|
227
|
+
try {
|
|
228
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
229
|
+
const match = content.match(/^log_dir:\s*(.+)$/m);
|
|
230
|
+
if (match?.[1])
|
|
231
|
+
return match[1].trim();
|
|
232
|
+
} catch {}
|
|
233
|
+
return "gauntlet_logs";
|
|
234
|
+
}
|
|
235
|
+
function resolveLogPaths(activeDir) {
|
|
236
|
+
const previousDir = path.join(activeDir, "previous");
|
|
237
|
+
const debugLogPath = path.join(activeDir, ".debug.log");
|
|
238
|
+
const activeHasLogs = fs.existsSync(activeDir) && fs.readdirSync(activeDir).some((f) => !f.startsWith(".") && f !== "previous");
|
|
239
|
+
if (activeHasLogs) {
|
|
240
|
+
return { logDir: activeDir, debugLogPath };
|
|
241
|
+
}
|
|
242
|
+
if (!fs.existsSync(previousDir)) {
|
|
243
|
+
console.log("No gauntlet_logs directory found.");
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const logDir = resolvePreviousLogDir(previousDir);
|
|
247
|
+
if (!logDir)
|
|
248
|
+
return null;
|
|
249
|
+
return { logDir, debugLogPath };
|
|
250
|
+
}
|
|
251
|
+
function resolvePreviousLogDir(previousDir) {
|
|
252
|
+
const prevEntries = fs.readdirSync(previousDir);
|
|
253
|
+
const hasDirectFiles = prevEntries.some((f) => f.endsWith(".log") || f.endsWith(".json"));
|
|
254
|
+
if (hasDirectFiles)
|
|
255
|
+
return previousDir;
|
|
256
|
+
const prevDirs = prevEntries.map((d) => path.join(previousDir, d)).filter((d) => fs.statSync(d).isDirectory()).sort().reverse();
|
|
257
|
+
if (prevDirs.length === 0) {
|
|
258
|
+
console.log("No gauntlet logs found.");
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
return prevDirs[0];
|
|
262
|
+
}
|
|
263
|
+
function main() {
|
|
264
|
+
const cwd = process.cwd();
|
|
265
|
+
const logDirName = getLogDir(cwd);
|
|
266
|
+
const activeDir = path.join(cwd, logDirName);
|
|
267
|
+
const paths = resolveLogPaths(activeDir);
|
|
268
|
+
if (!paths) {
|
|
269
|
+
process.exit(0);
|
|
270
|
+
}
|
|
271
|
+
let sessions = [];
|
|
272
|
+
if (fs.existsSync(paths.debugLogPath)) {
|
|
273
|
+
const debugContent = fs.readFileSync(paths.debugLogPath, "utf-8");
|
|
274
|
+
const sessionStart = getSessionStartTime(paths.logDir);
|
|
275
|
+
sessions = parseDebugLog(debugContent, sessionStart);
|
|
276
|
+
}
|
|
277
|
+
const output = formatSession(sessions, paths.logDir);
|
|
278
|
+
console.log(output);
|
|
279
|
+
}
|
|
280
|
+
var isDirectRun = (import.meta.url === `file://${process.argv[1]}` || typeof Bun !== "undefined" && import.meta.url === `file://${Bun.main}`) && (process.argv[1]?.endsWith("status.ts") || process.argv[1]?.endsWith("status.js"));
|
|
281
|
+
if (isDirectRun) {
|
|
282
|
+
main();
|
|
283
|
+
}
|
|
284
|
+
|
|
5
285
|
// src/index.ts
|
|
6
286
|
import { Command } from "commander";
|
|
7
287
|
// package.json
|
|
8
288
|
var package_default = {
|
|
9
289
|
name: "agent-gauntlet",
|
|
10
|
-
version: "0.
|
|
290
|
+
version: "0.15.1",
|
|
11
291
|
description: "A CLI tool for testing AI coding agents",
|
|
12
292
|
license: "MIT",
|
|
13
293
|
author: "Paul Caplan",
|
|
@@ -28,6 +308,7 @@ var package_default = {
|
|
|
28
308
|
],
|
|
29
309
|
files: [
|
|
30
310
|
"dist",
|
|
311
|
+
"skills",
|
|
31
312
|
"README.md",
|
|
32
313
|
"LICENSE"
|
|
33
314
|
],
|
|
@@ -85,12 +366,12 @@ var package_default = {
|
|
|
85
366
|
import chalk3 from "chalk";
|
|
86
367
|
|
|
87
368
|
// src/config/global.ts
|
|
88
|
-
import
|
|
369
|
+
import fs2 from "node:fs/promises";
|
|
89
370
|
import os from "node:os";
|
|
90
|
-
import
|
|
371
|
+
import path2 from "node:path";
|
|
91
372
|
import YAML from "yaml";
|
|
92
373
|
import { z } from "zod";
|
|
93
|
-
var GLOBAL_CONFIG_PATH =
|
|
374
|
+
var GLOBAL_CONFIG_PATH = path2.join(os.homedir(), ".config", "agent-gauntlet", "config.yml");
|
|
94
375
|
var debugLogConfigSchema = z.object({
|
|
95
376
|
enabled: z.boolean().default(false),
|
|
96
377
|
max_size_mb: z.number().default(10)
|
|
@@ -123,7 +404,7 @@ var DEFAULT_GLOBAL_CONFIG = {
|
|
|
123
404
|
};
|
|
124
405
|
async function loadGlobalConfig() {
|
|
125
406
|
try {
|
|
126
|
-
const content = await
|
|
407
|
+
const content = await fs2.readFile(GLOBAL_CONFIG_PATH, "utf-8");
|
|
127
408
|
const raw = YAML.parse(content);
|
|
128
409
|
return globalConfigSchema.parse(raw);
|
|
129
410
|
} catch (error) {
|
|
@@ -136,30 +417,59 @@ async function loadGlobalConfig() {
|
|
|
136
417
|
}
|
|
137
418
|
|
|
138
419
|
// src/config/loader.ts
|
|
139
|
-
import
|
|
140
|
-
import
|
|
420
|
+
import fs3 from "node:fs/promises";
|
|
421
|
+
import path3 from "node:path";
|
|
141
422
|
import matter from "gray-matter";
|
|
142
423
|
import YAML2 from "yaml";
|
|
143
424
|
|
|
144
425
|
// src/built-in-reviews/code-quality.md
|
|
145
426
|
var code_quality_default = `# Code Quality Review
|
|
146
427
|
|
|
147
|
-
You are a senior software engineer performing a code review. Your
|
|
428
|
+
You are a senior software engineer performing a code review. Your goal is to identify **real problems** that could cause bugs, security vulnerabilities, performance issues, or silent failures in production.
|
|
429
|
+
|
|
430
|
+
## Review Strategy
|
|
431
|
+
|
|
432
|
+
Use a multi-lens approach covering three analysis areas. For each lens, first check whether the corresponding pr-review-toolkit agent is available. If it is, dispatch that agent against the diff. If it is not available, perform the analysis inline using the framework below.
|
|
433
|
+
|
|
434
|
+
### Lens 1: Code Quality, Bugs & Security
|
|
148
435
|
|
|
149
|
-
|
|
436
|
+
**Agent:** \`code-reviewer\` (from pr-review-toolkit)
|
|
150
437
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
438
|
+
If the \`code-reviewer\` agent is unavailable, review inline for:
|
|
439
|
+
- **Logic errors** — off-by-one, null/undefined, race conditions, unhandled edge cases
|
|
440
|
+
- **Security flaws** — injection, auth/authz gaps, sensitive data exposure, input validation
|
|
441
|
+
- **Performance** — algorithmic complexity, N+1 queries, blocking operations, memory leaks
|
|
442
|
+
- **Resource leaks** — unclosed handles, missing cleanup in error paths
|
|
443
|
+
|
|
444
|
+
### Lens 2: Silent Failures & Error Handling
|
|
445
|
+
|
|
446
|
+
**Agent:** \`silent-failure-hunter\` (from pr-review-toolkit)
|
|
447
|
+
|
|
448
|
+
If the \`silent-failure-hunter\` agent is unavailable, review inline for:
|
|
449
|
+
- **Swallowed errors** — empty catch blocks, catch-and-return-default, ignored promise rejections
|
|
450
|
+
- **Missing logging** — error paths with no observability, failures that disappear silently
|
|
451
|
+
- **Inadequate error handling** — overly broad catch, lost error context, fallbacks that hide bugs
|
|
452
|
+
|
|
453
|
+
### Lens 3: Type Design
|
|
454
|
+
|
|
455
|
+
**Agent:** \`type-design-analyzer\` (from pr-review-toolkit)
|
|
456
|
+
|
|
457
|
+
If the \`type-design-analyzer\` agent is unavailable, review inline for:
|
|
458
|
+
- **Type invariants** — types that permit invalid states, missing constraints
|
|
459
|
+
- **Encapsulation** — exposed internals, mutable shared state, leaky abstractions
|
|
460
|
+
- **Enforcement** — runtime validation gaps at system boundaries
|
|
461
|
+
|
|
462
|
+
## Merging Results
|
|
463
|
+
|
|
464
|
+
After completing all three lenses (whether via agents or inline analysis), merge the findings. Deduplicate any overlapping issues. For each violation, report it exactly once under whichever lens found it first.
|
|
155
465
|
|
|
156
466
|
## Do NOT Report
|
|
157
467
|
|
|
158
468
|
- Style, formatting, or naming preferences
|
|
159
469
|
- Missing documentation, comments, or type annotations
|
|
160
|
-
- Suggestions for "better" abstractions
|
|
161
|
-
- Hypothetical issues
|
|
162
|
-
- Issues in code
|
|
470
|
+
- Suggestions for "better" abstractions that aren't broken
|
|
471
|
+
- Hypothetical issues requiring unlikely preconditions
|
|
472
|
+
- Issues in code not changed in this diff
|
|
163
473
|
|
|
164
474
|
## Guidelines
|
|
165
475
|
|
|
@@ -189,7 +499,8 @@ function loadBuiltInReview(name) {
|
|
|
189
499
|
import { z as z2 } from "zod";
|
|
190
500
|
var adapterConfigSchema = z2.object({
|
|
191
501
|
allow_tool_use: z2.boolean().default(true),
|
|
192
|
-
thinking_budget: z2.enum(["off", "low", "medium", "high"]).optional()
|
|
502
|
+
thinking_budget: z2.enum(["off", "low", "medium", "high"]).optional(),
|
|
503
|
+
model: z2.string().optional()
|
|
193
504
|
});
|
|
194
505
|
var cliConfigSchema = z2.object({
|
|
195
506
|
default_preference: z2.array(z2.string().min(1)).min(1),
|
|
@@ -326,24 +637,24 @@ var CONFIG_FILE = "config.yml";
|
|
|
326
637
|
var CHECKS_DIR = "checks";
|
|
327
638
|
var REVIEWS_DIR = "reviews";
|
|
328
639
|
async function loadConfig(rootDir = process.cwd()) {
|
|
329
|
-
const gauntletPath =
|
|
330
|
-
const configPath =
|
|
640
|
+
const gauntletPath = path3.join(rootDir, GAUNTLET_DIR);
|
|
641
|
+
const configPath = path3.join(gauntletPath, CONFIG_FILE);
|
|
331
642
|
if (!await fileExists(configPath)) {
|
|
332
643
|
throw new Error(`Configuration file not found at ${configPath}`);
|
|
333
644
|
}
|
|
334
|
-
const configContent = await
|
|
645
|
+
const configContent = await fs3.readFile(configPath, "utf-8");
|
|
335
646
|
const projectConfigRaw = YAML2.parse(configContent);
|
|
336
647
|
const projectConfig = gauntletConfigSchema.parse(projectConfigRaw);
|
|
337
|
-
const checksPath =
|
|
648
|
+
const checksPath = path3.join(gauntletPath, CHECKS_DIR);
|
|
338
649
|
const checks = {};
|
|
339
650
|
if (await dirExists(checksPath)) {
|
|
340
|
-
const checkFiles = await
|
|
651
|
+
const checkFiles = await fs3.readdir(checksPath);
|
|
341
652
|
for (const file of checkFiles) {
|
|
342
653
|
if (file.endsWith(".yml") || file.endsWith(".yaml")) {
|
|
343
|
-
const filePath =
|
|
344
|
-
const content = await
|
|
654
|
+
const filePath = path3.join(checksPath, file);
|
|
655
|
+
const content = await fs3.readFile(filePath, "utf-8");
|
|
345
656
|
const raw = YAML2.parse(content);
|
|
346
|
-
const name =
|
|
657
|
+
const name = path3.basename(file, path3.extname(file));
|
|
347
658
|
const parsed = checkGateSchema.parse(raw);
|
|
348
659
|
const fixFile = parsed.fix_instructions_file || parsed.fix_instructions;
|
|
349
660
|
const loadedCheck = {
|
|
@@ -360,14 +671,14 @@ async function loadConfig(rootDir = process.cwd()) {
|
|
|
360
671
|
}
|
|
361
672
|
}
|
|
362
673
|
}
|
|
363
|
-
const reviewsPath =
|
|
674
|
+
const reviewsPath = path3.join(gauntletPath, REVIEWS_DIR);
|
|
364
675
|
const reviews = {};
|
|
365
676
|
if (await dirExists(reviewsPath)) {
|
|
366
|
-
const reviewFiles = await
|
|
677
|
+
const reviewFiles = await fs3.readdir(reviewsPath);
|
|
367
678
|
const reviewNameSources = new Map;
|
|
368
679
|
for (const file of reviewFiles) {
|
|
369
680
|
if (file.endsWith(".md") || file.endsWith(".yml") || file.endsWith(".yaml")) {
|
|
370
|
-
const name =
|
|
681
|
+
const name = path3.basename(file, path3.extname(file));
|
|
371
682
|
if (isBuiltInReview(name)) {
|
|
372
683
|
throw new Error(`Review file "${file}" uses the reserved "built-in:" prefix. Rename the file to avoid conflicts with built-in reviews.`);
|
|
373
684
|
}
|
|
@@ -383,11 +694,11 @@ async function loadConfig(rootDir = process.cwd()) {
|
|
|
383
694
|
}
|
|
384
695
|
for (const file of reviewFiles) {
|
|
385
696
|
if (file.endsWith(".md")) {
|
|
386
|
-
const filePath =
|
|
387
|
-
const content = await
|
|
697
|
+
const filePath = path3.join(reviewsPath, file);
|
|
698
|
+
const content = await fs3.readFile(filePath, "utf-8");
|
|
388
699
|
const { data: frontmatter, content: promptBody } = matter(content);
|
|
389
700
|
const parsedFrontmatter = reviewPromptFrontmatterSchema.parse(frontmatter);
|
|
390
|
-
const name =
|
|
701
|
+
const name = path3.basename(file, ".md");
|
|
391
702
|
const review = {
|
|
392
703
|
name,
|
|
393
704
|
prompt: file,
|
|
@@ -409,11 +720,11 @@ async function loadConfig(rootDir = process.cwd()) {
|
|
|
409
720
|
}
|
|
410
721
|
reviews[name] = review;
|
|
411
722
|
} else if (file.endsWith(".yml") || file.endsWith(".yaml")) {
|
|
412
|
-
const filePath =
|
|
413
|
-
const content = await
|
|
723
|
+
const filePath = path3.join(reviewsPath, file);
|
|
724
|
+
const content = await fs3.readFile(filePath, "utf-8");
|
|
414
725
|
const raw = YAML2.parse(content);
|
|
415
726
|
const parsed = reviewYamlSchema.parse(raw);
|
|
416
|
-
const name =
|
|
727
|
+
const name = path3.basename(file, path3.extname(file));
|
|
417
728
|
const review = {
|
|
418
729
|
name,
|
|
419
730
|
prompt: file,
|
|
@@ -476,33 +787,33 @@ async function loadConfig(rootDir = process.cwd()) {
|
|
|
476
787
|
}
|
|
477
788
|
async function loadPromptFile(filePath, gauntletPath, source) {
|
|
478
789
|
let resolvedPath;
|
|
479
|
-
if (
|
|
790
|
+
if (path3.isAbsolute(filePath)) {
|
|
480
791
|
console.warn(`Warning: ${source} uses absolute path "${filePath}". Prefer relative paths for portability.`);
|
|
481
792
|
resolvedPath = filePath;
|
|
482
793
|
} else {
|
|
483
|
-
resolvedPath =
|
|
794
|
+
resolvedPath = path3.resolve(gauntletPath, filePath);
|
|
484
795
|
}
|
|
485
|
-
const normalizedGauntletPath =
|
|
486
|
-
const relativeToDotGauntlet =
|
|
487
|
-
if (relativeToDotGauntlet.startsWith("..") ||
|
|
796
|
+
const normalizedGauntletPath = path3.resolve(gauntletPath);
|
|
797
|
+
const relativeToDotGauntlet = path3.relative(normalizedGauntletPath, resolvedPath);
|
|
798
|
+
if (relativeToDotGauntlet.startsWith("..") || path3.isAbsolute(relativeToDotGauntlet)) {
|
|
488
799
|
console.warn(`Warning: ${source} references file outside .gauntlet/ directory: "${filePath}" (resolves to ${resolvedPath}). Review .gauntlet/ config changes carefully in PRs.`);
|
|
489
800
|
}
|
|
490
801
|
if (!await fileExists(resolvedPath)) {
|
|
491
802
|
throw new Error(`File not found: ${resolvedPath} (referenced by ${source})`);
|
|
492
803
|
}
|
|
493
|
-
return
|
|
804
|
+
return fs3.readFile(resolvedPath, "utf-8");
|
|
494
805
|
}
|
|
495
|
-
async function fileExists(
|
|
806
|
+
async function fileExists(path4) {
|
|
496
807
|
try {
|
|
497
|
-
const stat = await
|
|
808
|
+
const stat = await fs3.stat(path4);
|
|
498
809
|
return stat.isFile();
|
|
499
810
|
} catch {
|
|
500
811
|
return false;
|
|
501
812
|
}
|
|
502
813
|
}
|
|
503
|
-
async function dirExists(
|
|
814
|
+
async function dirExists(path4) {
|
|
504
815
|
try {
|
|
505
|
-
const stat = await
|
|
816
|
+
const stat = await fs3.stat(path4);
|
|
506
817
|
return stat.isDirectory();
|
|
507
818
|
} catch {
|
|
508
819
|
return false;
|
|
@@ -601,8 +912,8 @@ class ChangeDetector {
|
|
|
601
912
|
}
|
|
602
913
|
|
|
603
914
|
// src/core/entry-point.ts
|
|
604
|
-
import
|
|
605
|
-
import
|
|
915
|
+
import fs4 from "node:fs/promises";
|
|
916
|
+
import path4 from "node:path";
|
|
606
917
|
import picomatch from "picomatch";
|
|
607
918
|
|
|
608
919
|
class EntryPointExpander {
|
|
@@ -695,15 +1006,15 @@ class EntryPointExpander {
|
|
|
695
1006
|
const relPath = file.slice(parentDir.length + 1);
|
|
696
1007
|
const subDirName = relPath.split("/")[0];
|
|
697
1008
|
if (subDirName) {
|
|
698
|
-
affectedSubDirs.add(
|
|
1009
|
+
affectedSubDirs.add(path4.join(parentDir, subDirName));
|
|
699
1010
|
}
|
|
700
1011
|
}
|
|
701
1012
|
return Array.from(affectedSubDirs);
|
|
702
1013
|
}
|
|
703
1014
|
async listSubDirectories(parentDir) {
|
|
704
1015
|
try {
|
|
705
|
-
const dirents = await
|
|
706
|
-
return dirents.filter((d) => d.isDirectory()).map((d) =>
|
|
1016
|
+
const dirents = await fs4.readdir(parentDir, { withFileTypes: true });
|
|
1017
|
+
return dirents.filter((d) => d.isDirectory()).map((d) => path4.join(parentDir, d.name));
|
|
707
1018
|
} catch {
|
|
708
1019
|
return [];
|
|
709
1020
|
}
|
|
@@ -905,25 +1216,25 @@ ${config.fixInstructionsContent}
|
|
|
905
1216
|
|
|
906
1217
|
// src/gates/review.ts
|
|
907
1218
|
import { exec as exec8 } from "node:child_process";
|
|
908
|
-
import
|
|
909
|
-
import
|
|
1219
|
+
import fs17 from "node:fs/promises";
|
|
1220
|
+
import path16 from "node:path";
|
|
910
1221
|
import { promisify as promisify9 } from "node:util";
|
|
911
1222
|
|
|
912
1223
|
// src/cli-adapters/index.ts
|
|
913
1224
|
import { spawn as spawn2 } from "node:child_process";
|
|
914
|
-
import
|
|
1225
|
+
import fs16 from "node:fs/promises";
|
|
915
1226
|
|
|
916
1227
|
// src/cli-adapters/claude.ts
|
|
917
1228
|
import { exec as exec3 } from "node:child_process";
|
|
918
|
-
import
|
|
1229
|
+
import fs11 from "node:fs/promises";
|
|
919
1230
|
import os2 from "node:os";
|
|
920
|
-
import
|
|
1231
|
+
import path11 from "node:path";
|
|
921
1232
|
import { promisify as promisify4 } from "node:util";
|
|
922
1233
|
|
|
923
1234
|
// src/commands/stop-hook.ts
|
|
924
1235
|
import fsSync from "node:fs";
|
|
925
|
-
import
|
|
926
|
-
import
|
|
1236
|
+
import fs10 from "node:fs/promises";
|
|
1237
|
+
import path10 from "node:path";
|
|
927
1238
|
|
|
928
1239
|
// src/hooks/adapters/claude-stop-hook.ts
|
|
929
1240
|
class ClaudeStopHookAdapter {
|
|
@@ -1017,8 +1328,8 @@ class CursorStopHookAdapter {
|
|
|
1017
1328
|
|
|
1018
1329
|
// src/hooks/stop-hook-handler.ts
|
|
1019
1330
|
import { execFile } from "node:child_process";
|
|
1020
|
-
import
|
|
1021
|
-
import
|
|
1331
|
+
import fs9 from "node:fs/promises";
|
|
1332
|
+
import path9 from "node:path";
|
|
1022
1333
|
import { promisify as promisify3 } from "node:util";
|
|
1023
1334
|
import YAML3 from "yaml";
|
|
1024
1335
|
|
|
@@ -1077,9 +1388,9 @@ function resolveStopHookConfig(projectConfig, globalConfig) {
|
|
|
1077
1388
|
}
|
|
1078
1389
|
|
|
1079
1390
|
// src/output/app-logger.ts
|
|
1080
|
-
import
|
|
1391
|
+
import fs5 from "node:fs";
|
|
1081
1392
|
import fsPromises from "node:fs/promises";
|
|
1082
|
-
import
|
|
1393
|
+
import path5 from "node:path";
|
|
1083
1394
|
import {
|
|
1084
1395
|
configure,
|
|
1085
1396
|
getLogger
|
|
@@ -1137,8 +1448,8 @@ function safeStringify2(value) {
|
|
|
1137
1448
|
}
|
|
1138
1449
|
}
|
|
1139
1450
|
function createDebugLogSink(logDir) {
|
|
1140
|
-
const debugLogPath =
|
|
1141
|
-
debugLogFd =
|
|
1451
|
+
const debugLogPath = path5.join(logDir, ".debug.log");
|
|
1452
|
+
debugLogFd = fs5.openSync(debugLogPath, fs5.constants.O_WRONLY | fs5.constants.O_CREAT | fs5.constants.O_APPEND);
|
|
1142
1453
|
return (record) => {
|
|
1143
1454
|
if (debugLogFd === null)
|
|
1144
1455
|
return;
|
|
@@ -1149,7 +1460,7 @@ function createDebugLogSink(logDir) {
|
|
|
1149
1460
|
const line = `[${timestamp}] ${level} [${category}] ${message}
|
|
1150
1461
|
`;
|
|
1151
1462
|
try {
|
|
1152
|
-
|
|
1463
|
+
fs5.writeSync(debugLogFd, line);
|
|
1153
1464
|
} catch {}
|
|
1154
1465
|
};
|
|
1155
1466
|
}
|
|
@@ -1192,7 +1503,7 @@ async function initLogger(config) {
|
|
|
1192
1503
|
} catch (error) {
|
|
1193
1504
|
if (debugLogFd !== null) {
|
|
1194
1505
|
try {
|
|
1195
|
-
|
|
1506
|
+
fs5.closeSync(debugLogFd);
|
|
1196
1507
|
} catch {}
|
|
1197
1508
|
debugLogFd = null;
|
|
1198
1509
|
}
|
|
@@ -1202,7 +1513,7 @@ async function initLogger(config) {
|
|
|
1202
1513
|
async function resetLogger() {
|
|
1203
1514
|
if (debugLogFd !== null) {
|
|
1204
1515
|
try {
|
|
1205
|
-
|
|
1516
|
+
fs5.closeSync(debugLogFd);
|
|
1206
1517
|
} catch {}
|
|
1207
1518
|
debugLogFd = null;
|
|
1208
1519
|
}
|
|
@@ -1227,17 +1538,17 @@ function isLoggerConfigured() {
|
|
|
1227
1538
|
}
|
|
1228
1539
|
|
|
1229
1540
|
// src/hooks/stop-hook-state.ts
|
|
1230
|
-
import
|
|
1231
|
-
import
|
|
1541
|
+
import fs8 from "node:fs/promises";
|
|
1542
|
+
import path8 from "node:path";
|
|
1232
1543
|
|
|
1233
1544
|
// src/utils/execution-state.ts
|
|
1234
1545
|
import { spawn } from "node:child_process";
|
|
1235
|
-
import
|
|
1236
|
-
import
|
|
1546
|
+
import fs7 from "node:fs/promises";
|
|
1547
|
+
import path7 from "node:path";
|
|
1237
1548
|
|
|
1238
1549
|
// src/utils/debug-log.ts
|
|
1239
|
-
import
|
|
1240
|
-
import
|
|
1550
|
+
import fs6 from "node:fs/promises";
|
|
1551
|
+
import path6 from "node:path";
|
|
1241
1552
|
var DEBUG_LOG_FILENAME = ".debug.log";
|
|
1242
1553
|
var DEBUG_LOG_BACKUP_FILENAME = ".debug.log.1";
|
|
1243
1554
|
function getDebugLogFilename() {
|
|
@@ -1254,8 +1565,8 @@ class DebugLogger {
|
|
|
1254
1565
|
enabled;
|
|
1255
1566
|
runStartTime;
|
|
1256
1567
|
constructor(logDir, config) {
|
|
1257
|
-
this.logPath =
|
|
1258
|
-
this.backupPath =
|
|
1568
|
+
this.logPath = path6.join(logDir, DEBUG_LOG_FILENAME);
|
|
1569
|
+
this.backupPath = path6.join(logDir, DEBUG_LOG_BACKUP_FILENAME);
|
|
1259
1570
|
this.maxSizeBytes = config.maxSizeMb * 1024 * 1024;
|
|
1260
1571
|
this.enabled = config.enabled;
|
|
1261
1572
|
}
|
|
@@ -1358,18 +1669,18 @@ class DebugLogger {
|
|
|
1358
1669
|
`;
|
|
1359
1670
|
try {
|
|
1360
1671
|
await this.rotateIfNeeded();
|
|
1361
|
-
await
|
|
1362
|
-
await
|
|
1672
|
+
await fs6.mkdir(path6.dirname(this.logPath), { recursive: true });
|
|
1673
|
+
await fs6.appendFile(this.logPath, entry, "utf-8");
|
|
1363
1674
|
} catch {}
|
|
1364
1675
|
}
|
|
1365
1676
|
async rotateIfNeeded() {
|
|
1366
1677
|
try {
|
|
1367
|
-
const stat = await
|
|
1678
|
+
const stat = await fs6.stat(this.logPath);
|
|
1368
1679
|
if (stat.size >= this.maxSizeBytes) {
|
|
1369
1680
|
try {
|
|
1370
|
-
await
|
|
1681
|
+
await fs6.rm(this.backupPath, { force: true });
|
|
1371
1682
|
} catch {}
|
|
1372
|
-
await
|
|
1683
|
+
await fs6.rename(this.logPath, this.backupPath);
|
|
1373
1684
|
}
|
|
1374
1685
|
} catch {}
|
|
1375
1686
|
}
|
|
@@ -1427,8 +1738,8 @@ function isValidStateData(data) {
|
|
|
1427
1738
|
}
|
|
1428
1739
|
async function readExecutionState(logDir) {
|
|
1429
1740
|
try {
|
|
1430
|
-
const statePath =
|
|
1431
|
-
const content = await
|
|
1741
|
+
const statePath = path7.join(logDir, EXECUTION_STATE_FILENAME);
|
|
1742
|
+
const content = await fs7.readFile(statePath, "utf-8");
|
|
1432
1743
|
const data = JSON.parse(content);
|
|
1433
1744
|
if (!isValidStateData(data))
|
|
1434
1745
|
return null;
|
|
@@ -1483,7 +1794,7 @@ async function createWorkingTreeRef() {
|
|
|
1483
1794
|
});
|
|
1484
1795
|
}
|
|
1485
1796
|
async function writeExecutionState(logDir) {
|
|
1486
|
-
const statePath =
|
|
1797
|
+
const statePath = path7.join(logDir, EXECUTION_STATE_FILENAME);
|
|
1487
1798
|
const [branch, commit, workingTreeRef, rawState] = await Promise.all([
|
|
1488
1799
|
getCurrentBranch(),
|
|
1489
1800
|
getCurrentCommit(),
|
|
@@ -1515,11 +1826,11 @@ async function writeExecutionState(logDir) {
|
|
|
1515
1826
|
changes.working_tree_ref = workingTreeRef;
|
|
1516
1827
|
}
|
|
1517
1828
|
await getDebugLogger()?.logStateWrite(changes);
|
|
1518
|
-
await
|
|
1519
|
-
await
|
|
1829
|
+
await fs7.mkdir(logDir, { recursive: true });
|
|
1830
|
+
await fs7.writeFile(statePath, JSON.stringify(state, null, 2), "utf-8");
|
|
1520
1831
|
try {
|
|
1521
|
-
const sessionRefPath =
|
|
1522
|
-
await
|
|
1832
|
+
const sessionRefPath = path7.join(logDir, SESSION_REF_FILENAME);
|
|
1833
|
+
await fs7.rm(sessionRefPath, { force: true });
|
|
1523
1834
|
} catch {}
|
|
1524
1835
|
}
|
|
1525
1836
|
async function getCurrentBranch() {
|
|
@@ -1650,13 +1961,13 @@ function isAdapterCoolingDown(entry) {
|
|
|
1650
1961
|
return Date.now() - markedAt < COOLDOWN_MS;
|
|
1651
1962
|
}
|
|
1652
1963
|
async function getUnhealthyAdapters(logDir) {
|
|
1653
|
-
const statePath =
|
|
1964
|
+
const statePath = path7.join(logDir, EXECUTION_STATE_FILENAME);
|
|
1654
1965
|
const rawState = await readRawState(statePath);
|
|
1655
1966
|
return extractUnhealthyAdapters(rawState) ?? {};
|
|
1656
1967
|
}
|
|
1657
1968
|
async function readRawState(statePath) {
|
|
1658
1969
|
try {
|
|
1659
|
-
const content = await
|
|
1970
|
+
const content = await fs7.readFile(statePath, "utf-8");
|
|
1660
1971
|
return JSON.parse(content);
|
|
1661
1972
|
} catch {
|
|
1662
1973
|
return null;
|
|
@@ -1664,7 +1975,7 @@ async function readRawState(statePath) {
|
|
|
1664
1975
|
}
|
|
1665
1976
|
async function markAdapterUnhealthy(logDir, adapterName, reason) {
|
|
1666
1977
|
await getDebugLogger()?.logAdapterHealthChange(adapterName, false, reason);
|
|
1667
|
-
const statePath =
|
|
1978
|
+
const statePath = path7.join(logDir, EXECUTION_STATE_FILENAME);
|
|
1668
1979
|
const rawData = await readRawState(statePath) ?? {};
|
|
1669
1980
|
const adapters = rawData.unhealthy_adapters ?? {};
|
|
1670
1981
|
adapters[adapterName] = {
|
|
@@ -1672,12 +1983,12 @@ async function markAdapterUnhealthy(logDir, adapterName, reason) {
|
|
|
1672
1983
|
reason
|
|
1673
1984
|
};
|
|
1674
1985
|
rawData.unhealthy_adapters = adapters;
|
|
1675
|
-
await
|
|
1676
|
-
await
|
|
1986
|
+
await fs7.mkdir(logDir, { recursive: true });
|
|
1987
|
+
await fs7.writeFile(statePath, JSON.stringify(rawData, null, 2), "utf-8");
|
|
1677
1988
|
}
|
|
1678
1989
|
async function markAdapterHealthy(logDir, adapterName) {
|
|
1679
1990
|
await getDebugLogger()?.logAdapterHealthChange(adapterName, true);
|
|
1680
|
-
const statePath =
|
|
1991
|
+
const statePath = path7.join(logDir, EXECUTION_STATE_FILENAME);
|
|
1681
1992
|
const rawData = await readRawState(statePath);
|
|
1682
1993
|
if (!rawData)
|
|
1683
1994
|
return;
|
|
@@ -1690,13 +2001,13 @@ async function markAdapterHealthy(logDir, adapterName) {
|
|
|
1690
2001
|
} else {
|
|
1691
2002
|
rawData.unhealthy_adapters = adapters;
|
|
1692
2003
|
}
|
|
1693
|
-
await
|
|
2004
|
+
await fs7.writeFile(statePath, JSON.stringify(rawData, null, 2), "utf-8");
|
|
1694
2005
|
}
|
|
1695
2006
|
async function deleteExecutionState(logDir) {
|
|
1696
2007
|
try {
|
|
1697
2008
|
await getDebugLogger()?.logStateDelete();
|
|
1698
|
-
const statePath =
|
|
1699
|
-
await
|
|
2009
|
+
const statePath = path7.join(logDir, EXECUTION_STATE_FILENAME);
|
|
2010
|
+
await fs7.rm(statePath, { force: true });
|
|
1700
2011
|
} catch {}
|
|
1701
2012
|
}
|
|
1702
2013
|
|
|
@@ -1708,14 +2019,14 @@ var BLOCK_TIMESTAMPS_LOCK = ".block-timestamps.lock";
|
|
|
1708
2019
|
var LOCK_TIMEOUT_MS = 2000;
|
|
1709
2020
|
var LOCK_RETRY_MS = 50;
|
|
1710
2021
|
async function acquireTimestampLock(logDir) {
|
|
1711
|
-
const lockPath =
|
|
2022
|
+
const lockPath = path8.join(logDir, BLOCK_TIMESTAMPS_LOCK);
|
|
1712
2023
|
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
1713
2024
|
while (Date.now() < deadline) {
|
|
1714
2025
|
try {
|
|
1715
|
-
const handle = await
|
|
2026
|
+
const handle = await fs8.open(lockPath, "wx");
|
|
1716
2027
|
await handle.close();
|
|
1717
2028
|
return async () => {
|
|
1718
|
-
await
|
|
2029
|
+
await fs8.rm(lockPath, { force: true }).catch(() => {});
|
|
1719
2030
|
};
|
|
1720
2031
|
} catch (err) {
|
|
1721
2032
|
const code = err.code;
|
|
@@ -1728,7 +2039,7 @@ async function acquireTimestampLock(logDir) {
|
|
|
1728
2039
|
}
|
|
1729
2040
|
async function hasFailedRunLogs(logDir) {
|
|
1730
2041
|
try {
|
|
1731
|
-
const entries = await
|
|
2042
|
+
const entries = await fs8.readdir(logDir);
|
|
1732
2043
|
return entries.some((f) => (f.endsWith(".log") || f.endsWith(".json")) && f !== "previous" && !f.startsWith("console.") && !f.startsWith("."));
|
|
1733
2044
|
} catch {
|
|
1734
2045
|
return false;
|
|
@@ -1777,8 +2088,8 @@ async function getLastRunStatus(logDir) {
|
|
|
1777
2088
|
}
|
|
1778
2089
|
async function readBlockTimestamps(logDir) {
|
|
1779
2090
|
try {
|
|
1780
|
-
const filePath =
|
|
1781
|
-
const content = await
|
|
2091
|
+
const filePath = path8.join(logDir, BLOCK_TIMESTAMPS_FILE);
|
|
2092
|
+
const content = await fs8.readFile(filePath, "utf-8");
|
|
1782
2093
|
const parsed = JSON.parse(content);
|
|
1783
2094
|
if (!Array.isArray(parsed))
|
|
1784
2095
|
return [];
|
|
@@ -1794,8 +2105,8 @@ async function recordBlockTimestamp(logDir) {
|
|
|
1794
2105
|
const existing = await readBlockTimestamps(logDir);
|
|
1795
2106
|
const recent = existing.filter((ts) => now - ts < LOOP_WINDOW_MS);
|
|
1796
2107
|
recent.push(now);
|
|
1797
|
-
const filePath =
|
|
1798
|
-
await
|
|
2108
|
+
const filePath = path8.join(logDir, BLOCK_TIMESTAMPS_FILE);
|
|
2109
|
+
await fs8.writeFile(filePath, JSON.stringify(recent), "utf-8");
|
|
1799
2110
|
return recent;
|
|
1800
2111
|
} finally {
|
|
1801
2112
|
await release();
|
|
@@ -1803,8 +2114,8 @@ async function recordBlockTimestamp(logDir) {
|
|
|
1803
2114
|
}
|
|
1804
2115
|
async function resetBlockTimestamps(logDir) {
|
|
1805
2116
|
try {
|
|
1806
|
-
const filePath =
|
|
1807
|
-
await
|
|
2117
|
+
const filePath = path8.join(logDir, BLOCK_TIMESTAMPS_FILE);
|
|
2118
|
+
await fs8.rm(filePath, { force: true });
|
|
1808
2119
|
} catch {}
|
|
1809
2120
|
}
|
|
1810
2121
|
|
|
@@ -1820,8 +2131,8 @@ var SKILL_INSTRUCTIONS = {
|
|
|
1820
2131
|
};
|
|
1821
2132
|
async function readProjectConfig(projectCwd) {
|
|
1822
2133
|
try {
|
|
1823
|
-
const configPath =
|
|
1824
|
-
const content = await
|
|
2134
|
+
const configPath = path9.join(projectCwd, ".gauntlet", "config.yml");
|
|
2135
|
+
const content = await fs9.readFile(configPath, "utf-8");
|
|
1825
2136
|
return YAML3.parse(content);
|
|
1826
2137
|
} catch {
|
|
1827
2138
|
return;
|
|
@@ -1858,7 +2169,7 @@ function getStatusMessage(status, context) {
|
|
|
1858
2169
|
}
|
|
1859
2170
|
return STATUS_MESSAGES[status] ?? `Unknown status: ${status}`;
|
|
1860
2171
|
}
|
|
1861
|
-
async function
|
|
2172
|
+
async function getLogDir2(projectCwd) {
|
|
1862
2173
|
const config = await readProjectConfig(projectCwd);
|
|
1863
2174
|
return config?.log_dir || DEFAULT_LOG_DIR;
|
|
1864
2175
|
}
|
|
@@ -1868,8 +2179,8 @@ async function getDebugLogConfig(projectCwd) {
|
|
|
1868
2179
|
}
|
|
1869
2180
|
async function getResolvedStopHookConfig(projectCwd) {
|
|
1870
2181
|
try {
|
|
1871
|
-
const configPath =
|
|
1872
|
-
const content = await
|
|
2182
|
+
const configPath = path9.join(projectCwd, ".gauntlet", "config.yml");
|
|
2183
|
+
const content = await fs9.readFile(configPath, "utf-8");
|
|
1873
2184
|
const raw = YAML3.parse(content);
|
|
1874
2185
|
const projectStopHookConfig = raw?.stop_hook;
|
|
1875
2186
|
const globalConfig = await loadGlobalConfig();
|
|
@@ -2126,7 +2437,7 @@ async function readStdin() {
|
|
|
2126
2437
|
}
|
|
2127
2438
|
async function fileExists2(filePath) {
|
|
2128
2439
|
try {
|
|
2129
|
-
await
|
|
2440
|
+
await fs10.stat(filePath);
|
|
2130
2441
|
return true;
|
|
2131
2442
|
} catch {
|
|
2132
2443
|
return false;
|
|
@@ -2203,12 +2514,12 @@ function registerStopHookCommand(program) {
|
|
|
2203
2514
|
outputResult(adapter, createEarlyExitResult("stop_hook_active"));
|
|
2204
2515
|
return;
|
|
2205
2516
|
}
|
|
2206
|
-
const quickConfigCheck =
|
|
2517
|
+
const quickConfigCheck = path10.join(process.cwd(), ".gauntlet", "config.yml");
|
|
2207
2518
|
if (!await fileExists2(quickConfigCheck)) {
|
|
2208
2519
|
outputResult(adapter, createEarlyExitResult("no_config"));
|
|
2209
2520
|
return;
|
|
2210
2521
|
}
|
|
2211
|
-
const earlyLogDir =
|
|
2522
|
+
const earlyLogDir = path10.join(process.cwd(), await getLogDir2(process.cwd()));
|
|
2212
2523
|
try {
|
|
2213
2524
|
const globalConfig = await loadGlobalConfig();
|
|
2214
2525
|
const projectDebugLogConfig = await getDebugLogConfig(process.cwd());
|
|
@@ -2218,16 +2529,16 @@ function registerStopHookCommand(program) {
|
|
|
2218
2529
|
log.warn(`Debug logger init failed: ${initErr.message ?? "unknown"}`);
|
|
2219
2530
|
}
|
|
2220
2531
|
await debugLogger?.logCommand("stop-hook", []);
|
|
2221
|
-
const markerLogDir = await
|
|
2222
|
-
const markerPath =
|
|
2532
|
+
const markerLogDir = await getLogDir2(process.cwd());
|
|
2533
|
+
const markerPath = path10.join(process.cwd(), markerLogDir, STOP_HOOK_MARKER_FILE);
|
|
2223
2534
|
if (await fileExists2(markerPath)) {
|
|
2224
2535
|
const STALE_MARKER_MS = 10 * 60 * 1000;
|
|
2225
2536
|
try {
|
|
2226
|
-
const stat = await
|
|
2537
|
+
const stat = await fs10.stat(markerPath);
|
|
2227
2538
|
const ageMs = Date.now() - stat.mtimeMs;
|
|
2228
2539
|
if (ageMs > STALE_MARKER_MS) {
|
|
2229
2540
|
await debugLogger?.logStopHookEarlyExit("marker_stale", "proceeding", `age=${Math.round(ageMs / 1000)}s threshold=${Math.round(STALE_MARKER_MS / 1000)}s`);
|
|
2230
|
-
await
|
|
2541
|
+
await fs10.rm(markerPath, { force: true });
|
|
2231
2542
|
} else {
|
|
2232
2543
|
await debugLogger?.logStopHookEarlyExit("marker_fresh", "stop_hook_active", `age=${Math.round(ageMs / 1000)}s`);
|
|
2233
2544
|
outputResult(adapter, createEarlyExitResult("stop_hook_active"));
|
|
@@ -2269,7 +2580,7 @@ function registerStopHookCommand(program) {
|
|
|
2269
2580
|
log.info("Starting gauntlet validation...");
|
|
2270
2581
|
const projectCwd = ctx.cwd;
|
|
2271
2582
|
if (ctx.cwd !== process.cwd()) {
|
|
2272
|
-
const configPath =
|
|
2583
|
+
const configPath = path10.join(projectCwd, ".gauntlet", "config.yml");
|
|
2273
2584
|
if (!await fileExists2(configPath)) {
|
|
2274
2585
|
log.info("No gauntlet config found at hook cwd, allowing stop");
|
|
2275
2586
|
await debugLogger?.logStopHookEarlyExit("no_config_at_cwd", "no_config", `cwd=${projectCwd}`);
|
|
@@ -2277,7 +2588,7 @@ function registerStopHookCommand(program) {
|
|
|
2277
2588
|
return;
|
|
2278
2589
|
}
|
|
2279
2590
|
}
|
|
2280
|
-
const logDir =
|
|
2591
|
+
const logDir = path10.join(projectCwd, await getLogDir2(projectCwd));
|
|
2281
2592
|
await initLogger({
|
|
2282
2593
|
mode: "stop-hook",
|
|
2283
2594
|
logDir
|
|
@@ -2294,9 +2605,9 @@ function registerStopHookCommand(program) {
|
|
|
2294
2605
|
}
|
|
2295
2606
|
}
|
|
2296
2607
|
await debugLogger?.logStopHookDiagnostics(diagnostics);
|
|
2297
|
-
markerFilePath =
|
|
2608
|
+
markerFilePath = path10.join(logDir, STOP_HOOK_MARKER_FILE);
|
|
2298
2609
|
try {
|
|
2299
|
-
await
|
|
2610
|
+
await fs10.writeFile(markerFilePath, `${process.pid}`, "utf-8");
|
|
2300
2611
|
} catch (mkErr) {
|
|
2301
2612
|
const errMsg = mkErr.message ?? "unknown";
|
|
2302
2613
|
log.warn(`Failed to create marker file: ${errMsg}`);
|
|
@@ -2311,7 +2622,7 @@ function registerStopHookCommand(program) {
|
|
|
2311
2622
|
} finally {
|
|
2312
2623
|
if (markerFilePath) {
|
|
2313
2624
|
try {
|
|
2314
|
-
await
|
|
2625
|
+
await fs10.rm(markerFilePath, { force: true });
|
|
2315
2626
|
} catch (rmErr) {
|
|
2316
2627
|
const errMsg = rmErr.message ?? "unknown";
|
|
2317
2628
|
log.warn(`Failed to remove marker file: ${errMsg}`);
|
|
@@ -2337,7 +2648,7 @@ function registerStopHookCommand(program) {
|
|
|
2337
2648
|
outputResult(adapter, createEarlyExitResult("error", { errorMessage }));
|
|
2338
2649
|
if (markerFilePath) {
|
|
2339
2650
|
try {
|
|
2340
|
-
await
|
|
2651
|
+
await fs10.rm(markerFilePath, { force: true });
|
|
2341
2652
|
} catch (rmErr) {
|
|
2342
2653
|
const rmMsg = rmErr.message ?? "unknown";
|
|
2343
2654
|
log.warn(`Failed to remove marker file in error handler: ${rmMsg}`);
|
|
@@ -2539,13 +2850,13 @@ class ClaudeAdapter {
|
|
|
2539
2850
|
return ".claude/commands";
|
|
2540
2851
|
}
|
|
2541
2852
|
getUserCommandDir() {
|
|
2542
|
-
return
|
|
2853
|
+
return path11.join(os2.homedir(), ".claude", "commands");
|
|
2543
2854
|
}
|
|
2544
2855
|
getProjectSkillDir() {
|
|
2545
2856
|
return ".claude/skills";
|
|
2546
2857
|
}
|
|
2547
2858
|
getUserSkillDir() {
|
|
2548
|
-
return
|
|
2859
|
+
return path11.join(os2.homedir(), ".claude", "skills");
|
|
2549
2860
|
}
|
|
2550
2861
|
getCommandExtension() {
|
|
2551
2862
|
return ".md";
|
|
@@ -2564,24 +2875,24 @@ class ClaudeAdapter {
|
|
|
2564
2875
|
|
|
2565
2876
|
--- DIFF ---
|
|
2566
2877
|
${opts.diff}`;
|
|
2567
|
-
const
|
|
2568
|
-
|
|
2569
|
-
await fs10.writeFile(tmpFile, fullContent);
|
|
2878
|
+
const tmpFile = path11.join(os2.tmpdir(), `gauntlet-claude-${process.pid}-${Date.now()}.txt`);
|
|
2879
|
+
await fs11.writeFile(tmpFile, fullContent);
|
|
2570
2880
|
const args = ["-p"];
|
|
2571
2881
|
if (opts.allowToolUse === false) {
|
|
2572
|
-
args.push("--
|
|
2882
|
+
args.push("--allowedTools", "Task");
|
|
2573
2883
|
} else {
|
|
2574
|
-
args.push("--allowedTools", "Read,Glob,Grep");
|
|
2884
|
+
args.push("--allowedTools", "Read,Glob,Grep,Task");
|
|
2575
2885
|
}
|
|
2576
|
-
args.push("--max-turns", "
|
|
2886
|
+
args.push("--max-turns", "25");
|
|
2577
2887
|
const otelEnv = buildOtelEnv();
|
|
2578
2888
|
const thinkingEnv = {};
|
|
2579
2889
|
if (opts.thinkingBudget && opts.thinkingBudget in CLAUDE_THINKING_TOKENS) {
|
|
2580
2890
|
thinkingEnv.MAX_THINKING_TOKENS = String(CLAUDE_THINKING_TOKENS[opts.thinkingBudget]);
|
|
2581
2891
|
}
|
|
2582
|
-
const cleanup = () =>
|
|
2892
|
+
const cleanup = () => fs11.unlink(tmpFile).catch(() => {});
|
|
2893
|
+
const { CLAUDECODE: _, ...parentEnv } = process.env;
|
|
2583
2894
|
const execEnv = {
|
|
2584
|
-
...
|
|
2895
|
+
...parentEnv,
|
|
2585
2896
|
[GAUNTLET_STOP_HOOK_ACTIVE_ENV]: "1",
|
|
2586
2897
|
...otelEnv,
|
|
2587
2898
|
...thinkingEnv
|
|
@@ -2619,9 +2930,9 @@ ${opts.diff}`;
|
|
|
2619
2930
|
|
|
2620
2931
|
// src/cli-adapters/codex.ts
|
|
2621
2932
|
import { exec as exec4 } from "node:child_process";
|
|
2622
|
-
import
|
|
2933
|
+
import fs12 from "node:fs/promises";
|
|
2623
2934
|
import os3 from "node:os";
|
|
2624
|
-
import
|
|
2935
|
+
import path12 from "node:path";
|
|
2625
2936
|
import { promisify as promisify5 } from "node:util";
|
|
2626
2937
|
var execAsync4 = promisify5(exec4);
|
|
2627
2938
|
var MAX_BUFFER_BYTES3 = 10 * 1024 * 1024;
|
|
@@ -2741,7 +3052,7 @@ class CodexAdapter {
|
|
|
2741
3052
|
return null;
|
|
2742
3053
|
}
|
|
2743
3054
|
getUserCommandDir() {
|
|
2744
|
-
return
|
|
3055
|
+
return path12.join(os3.homedir(), ".codex", "prompts");
|
|
2745
3056
|
}
|
|
2746
3057
|
getProjectSkillDir() {
|
|
2747
3058
|
return null;
|
|
@@ -2787,10 +3098,10 @@ class CodexAdapter {
|
|
|
2787
3098
|
--- DIFF ---
|
|
2788
3099
|
${opts.diff}`;
|
|
2789
3100
|
const tmpDir = os3.tmpdir();
|
|
2790
|
-
const tmpFile =
|
|
2791
|
-
await
|
|
3101
|
+
const tmpFile = path12.join(tmpDir, `gauntlet-codex-${Date.now()}.txt`);
|
|
3102
|
+
await fs12.writeFile(tmpFile, fullContent);
|
|
2792
3103
|
const args = this.buildArgs(opts.allowToolUse, opts.thinkingBudget);
|
|
2793
|
-
const cleanup = () =>
|
|
3104
|
+
const cleanup = () => fs12.unlink(tmpFile).catch(() => {});
|
|
2794
3105
|
if (opts.onOutput) {
|
|
2795
3106
|
const raw = await runStreamingCommand({
|
|
2796
3107
|
command: "codex",
|
|
@@ -2822,12 +3133,58 @@ ${opts.diff}`;
|
|
|
2822
3133
|
|
|
2823
3134
|
// src/cli-adapters/cursor.ts
|
|
2824
3135
|
import { exec as exec5 } from "node:child_process";
|
|
2825
|
-
import
|
|
3136
|
+
import fs13 from "node:fs/promises";
|
|
2826
3137
|
import os4 from "node:os";
|
|
2827
|
-
import
|
|
3138
|
+
import path13 from "node:path";
|
|
2828
3139
|
import { promisify as promisify6 } from "node:util";
|
|
3140
|
+
|
|
3141
|
+
// src/cli-adapters/model-resolution.ts
|
|
3142
|
+
var TIER_SUFFIXES = ["-low", "-high", "-xhigh", "-fast"];
|
|
3143
|
+
var SAFE_MODEL_ID_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
3144
|
+
function applyThinkingFilter(candidates, preferThinking) {
|
|
3145
|
+
if (preferThinking) {
|
|
3146
|
+
const thinking = candidates.filter((id) => id.endsWith("-thinking"));
|
|
3147
|
+
return thinking.length > 0 ? thinking : candidates;
|
|
3148
|
+
}
|
|
3149
|
+
return candidates.filter((id) => !id.endsWith("-thinking"));
|
|
3150
|
+
}
|
|
3151
|
+
function compareVersionsDesc(a, b) {
|
|
3152
|
+
if (!a && !b)
|
|
3153
|
+
return 0;
|
|
3154
|
+
if (!a)
|
|
3155
|
+
return 1;
|
|
3156
|
+
if (!b)
|
|
3157
|
+
return -1;
|
|
3158
|
+
if (a[0] !== b[0])
|
|
3159
|
+
return b[0] - a[0];
|
|
3160
|
+
return b[1] - a[1];
|
|
3161
|
+
}
|
|
3162
|
+
function resolveModelFromList(allModels, opts) {
|
|
3163
|
+
const candidates = allModels.filter((id) => id.split("-").includes(opts.baseName)).filter((id) => !TIER_SUFFIXES.some((s) => id.endsWith(s)));
|
|
3164
|
+
if (candidates.length === 0)
|
|
3165
|
+
return;
|
|
3166
|
+
const filtered = applyThinkingFilter(candidates, opts.preferThinking);
|
|
3167
|
+
if (filtered.length === 0)
|
|
3168
|
+
return;
|
|
3169
|
+
filtered.sort((a, b) => {
|
|
3170
|
+
const vA = a.match(/(\d+)\.(\d+)/);
|
|
3171
|
+
const vB = b.match(/(\d+)\.(\d+)/);
|
|
3172
|
+
return compareVersionsDesc(vA ? [Number(vA[1]), Number(vA[2])] : null, vB ? [Number(vB[1]), Number(vB[2])] : null);
|
|
3173
|
+
});
|
|
3174
|
+
return filtered[0];
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
// src/cli-adapters/cursor.ts
|
|
2829
3178
|
var execAsync5 = promisify6(exec5);
|
|
2830
3179
|
var MAX_BUFFER_BYTES4 = 10 * 1024 * 1024;
|
|
3180
|
+
var log = getCategoryLogger("cursor");
|
|
3181
|
+
function parseModelList(output) {
|
|
3182
|
+
return output.split(`
|
|
3183
|
+
`).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => {
|
|
3184
|
+
const dashIndex = line.indexOf(" - ");
|
|
3185
|
+
return dashIndex >= 0 ? line.substring(0, dashIndex).trim() : line.trim();
|
|
3186
|
+
}).filter((id) => id.length > 0);
|
|
3187
|
+
}
|
|
2831
3188
|
|
|
2832
3189
|
class CursorAdapter {
|
|
2833
3190
|
name = "cursor";
|
|
@@ -2874,19 +3231,57 @@ class CursorAdapter {
|
|
|
2874
3231
|
supportsHooks() {
|
|
2875
3232
|
return true;
|
|
2876
3233
|
}
|
|
3234
|
+
async resolveModel(baseName, thinkingBudget) {
|
|
3235
|
+
try {
|
|
3236
|
+
const stdout = await new Promise((resolve, reject) => {
|
|
3237
|
+
exec5("agent --list-models", { timeout: 1e4 }, (error, stdout2) => {
|
|
3238
|
+
if (error)
|
|
3239
|
+
reject(error);
|
|
3240
|
+
else
|
|
3241
|
+
resolve(stdout2);
|
|
3242
|
+
});
|
|
3243
|
+
});
|
|
3244
|
+
const models = parseModelList(stdout);
|
|
3245
|
+
const preferThinking = thinkingBudget !== undefined && thinkingBudget !== "off";
|
|
3246
|
+
const resolved = resolveModelFromList(models, {
|
|
3247
|
+
baseName,
|
|
3248
|
+
preferThinking
|
|
3249
|
+
});
|
|
3250
|
+
if (resolved === undefined) {
|
|
3251
|
+
log.warn(`No matching model found for "${baseName}"`);
|
|
3252
|
+
return;
|
|
3253
|
+
}
|
|
3254
|
+
if (!SAFE_MODEL_ID_PATTERN.test(resolved)) {
|
|
3255
|
+
log.warn(`Resolved model "${resolved}" contains unsafe characters`);
|
|
3256
|
+
return;
|
|
3257
|
+
}
|
|
3258
|
+
return resolved;
|
|
3259
|
+
} catch (err) {
|
|
3260
|
+
log.warn(`Failed to resolve model "${baseName}": ${err instanceof Error ? err.message : String(err)}`);
|
|
3261
|
+
return;
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
2877
3264
|
async execute(opts) {
|
|
2878
3265
|
const fullContent = `${opts.prompt}
|
|
2879
3266
|
|
|
2880
3267
|
--- DIFF ---
|
|
2881
3268
|
${opts.diff}`;
|
|
2882
3269
|
const tmpDir = os4.tmpdir();
|
|
2883
|
-
const tmpFile =
|
|
2884
|
-
await
|
|
2885
|
-
|
|
3270
|
+
const tmpFile = path13.join(tmpDir, `gauntlet-cursor-${process.pid}-${Date.now()}.txt`);
|
|
3271
|
+
await fs13.writeFile(tmpFile, fullContent);
|
|
3272
|
+
let resolvedModel;
|
|
3273
|
+
if (opts.model) {
|
|
3274
|
+
resolvedModel = await this.resolveModel(opts.model, opts.thinkingBudget);
|
|
3275
|
+
}
|
|
3276
|
+
const cleanup = () => fs13.unlink(tmpFile).catch(() => {});
|
|
3277
|
+
const args = ["--trust"];
|
|
3278
|
+
if (resolvedModel) {
|
|
3279
|
+
args.push("--model", resolvedModel);
|
|
3280
|
+
}
|
|
2886
3281
|
if (opts.onOutput) {
|
|
2887
3282
|
return runStreamingCommand({
|
|
2888
3283
|
command: "agent",
|
|
2889
|
-
args
|
|
3284
|
+
args,
|
|
2890
3285
|
tmpFile,
|
|
2891
3286
|
timeoutMs: opts.timeoutMs,
|
|
2892
3287
|
onOutput: opts.onOutput,
|
|
@@ -2894,7 +3289,8 @@ ${opts.diff}`;
|
|
|
2894
3289
|
});
|
|
2895
3290
|
}
|
|
2896
3291
|
try {
|
|
2897
|
-
const
|
|
3292
|
+
const modelFlag = resolvedModel ? ` --model ${resolvedModel}` : "";
|
|
3293
|
+
const cmd = `cat "${tmpFile}" | agent --trust${modelFlag}`;
|
|
2898
3294
|
const { stdout } = await execAsync5(cmd, {
|
|
2899
3295
|
timeout: opts.timeoutMs,
|
|
2900
3296
|
maxBuffer: MAX_BUFFER_BYTES4
|
|
@@ -2908,9 +3304,9 @@ ${opts.diff}`;
|
|
|
2908
3304
|
|
|
2909
3305
|
// src/cli-adapters/gemini.ts
|
|
2910
3306
|
import { exec as exec6 } from "node:child_process";
|
|
2911
|
-
import
|
|
3307
|
+
import fs14 from "node:fs/promises";
|
|
2912
3308
|
import os5 from "node:os";
|
|
2913
|
-
import
|
|
3309
|
+
import path14 from "node:path";
|
|
2914
3310
|
import { promisify as promisify7 } from "node:util";
|
|
2915
3311
|
var execAsync6 = promisify7(exec6);
|
|
2916
3312
|
var MAX_BUFFER_BYTES5 = 10 * 1024 * 1024;
|
|
@@ -3042,7 +3438,7 @@ async function parseGeminiTelemetry(filePath) {
|
|
|
3042
3438
|
const usage = {};
|
|
3043
3439
|
let content;
|
|
3044
3440
|
try {
|
|
3045
|
-
content = await
|
|
3441
|
+
content = await fs14.readFile(filePath, "utf-8");
|
|
3046
3442
|
} catch {
|
|
3047
3443
|
return usage;
|
|
3048
3444
|
}
|
|
@@ -3103,7 +3499,7 @@ class GeminiAdapter {
|
|
|
3103
3499
|
return ".gemini/commands";
|
|
3104
3500
|
}
|
|
3105
3501
|
getUserCommandDir() {
|
|
3106
|
-
return
|
|
3502
|
+
return path14.join(os5.homedir(), ".gemini", "commands");
|
|
3107
3503
|
}
|
|
3108
3504
|
getProjectSkillDir() {
|
|
3109
3505
|
return null;
|
|
@@ -3160,12 +3556,12 @@ ${summary}
|
|
|
3160
3556
|
releaseLock = resolve;
|
|
3161
3557
|
});
|
|
3162
3558
|
await prev;
|
|
3163
|
-
const settingsPath =
|
|
3559
|
+
const settingsPath = path14.join(process.cwd(), ".gemini", "settings.json");
|
|
3164
3560
|
let backup = null;
|
|
3165
3561
|
let existed = false;
|
|
3166
3562
|
try {
|
|
3167
3563
|
try {
|
|
3168
|
-
backup = await
|
|
3564
|
+
backup = await fs14.readFile(settingsPath, "utf-8");
|
|
3169
3565
|
existed = true;
|
|
3170
3566
|
} catch {}
|
|
3171
3567
|
const existing = backup ? JSON.parse(backup) : {};
|
|
@@ -3173,8 +3569,8 @@ ${summary}
|
|
|
3173
3569
|
...existing,
|
|
3174
3570
|
thinkingConfig: { ...existing.thinkingConfig, thinkingBudget: budget }
|
|
3175
3571
|
};
|
|
3176
|
-
await
|
|
3177
|
-
await
|
|
3572
|
+
await fs14.mkdir(path14.dirname(settingsPath), { recursive: true });
|
|
3573
|
+
await fs14.writeFile(settingsPath, JSON.stringify(merged, null, 2));
|
|
3178
3574
|
} catch (err) {
|
|
3179
3575
|
releaseLock();
|
|
3180
3576
|
throw err;
|
|
@@ -3182,9 +3578,9 @@ ${summary}
|
|
|
3182
3578
|
return async () => {
|
|
3183
3579
|
try {
|
|
3184
3580
|
if (existed && backup !== null) {
|
|
3185
|
-
await
|
|
3581
|
+
await fs14.writeFile(settingsPath, backup);
|
|
3186
3582
|
} else {
|
|
3187
|
-
await
|
|
3583
|
+
await fs14.unlink(settingsPath).catch(() => {});
|
|
3188
3584
|
}
|
|
3189
3585
|
} finally {
|
|
3190
3586
|
releaseLock();
|
|
@@ -3223,14 +3619,14 @@ ${summary}
|
|
|
3223
3619
|
--- DIFF ---
|
|
3224
3620
|
${opts.diff}`;
|
|
3225
3621
|
const tmpDir = os5.tmpdir();
|
|
3226
|
-
const tmpFile =
|
|
3227
|
-
await
|
|
3228
|
-
const telemetryFile =
|
|
3622
|
+
const tmpFile = path14.join(tmpDir, `gauntlet-gemini-${process.pid}-${Date.now()}.txt`);
|
|
3623
|
+
await fs14.writeFile(tmpFile, fullContent);
|
|
3624
|
+
const telemetryFile = path14.join(process.cwd(), `.gauntlet-gemini-telemetry-${process.pid}-${Date.now()}.log`);
|
|
3229
3625
|
const telemetryEnv = this.buildTelemetryEnv(telemetryFile);
|
|
3230
3626
|
const args = this.buildArgs(opts.allowToolUse);
|
|
3231
3627
|
const cleanupThinking = await this.maybeApplyThinking(opts.thinkingBudget);
|
|
3232
|
-
const cleanup = () =>
|
|
3233
|
-
const cleanupTelemetry = () =>
|
|
3628
|
+
const cleanup = () => fs14.unlink(tmpFile).catch(() => {});
|
|
3629
|
+
const cleanupTelemetry = () => fs14.unlink(telemetryFile).catch(() => {});
|
|
3234
3630
|
try {
|
|
3235
3631
|
if (opts.onOutput) {
|
|
3236
3632
|
try {
|
|
@@ -3270,12 +3666,19 @@ ${opts.diff}`;
|
|
|
3270
3666
|
|
|
3271
3667
|
// src/cli-adapters/github-copilot.ts
|
|
3272
3668
|
import { exec as exec7 } from "node:child_process";
|
|
3273
|
-
import
|
|
3669
|
+
import fs15 from "node:fs/promises";
|
|
3274
3670
|
import os6 from "node:os";
|
|
3275
|
-
import
|
|
3671
|
+
import path15 from "node:path";
|
|
3276
3672
|
import { promisify as promisify8 } from "node:util";
|
|
3277
3673
|
var execAsync7 = promisify8(exec7);
|
|
3278
3674
|
var MAX_BUFFER_BYTES6 = 10 * 1024 * 1024;
|
|
3675
|
+
var log2 = getCategoryLogger("github-copilot");
|
|
3676
|
+
function parseCopilotModels(helpOutput) {
|
|
3677
|
+
const match = helpOutput.match(/choices:\s*(.+?)\)/);
|
|
3678
|
+
if (!match?.[1])
|
|
3679
|
+
return [];
|
|
3680
|
+
return [...match[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]).filter((id) => id !== undefined);
|
|
3681
|
+
}
|
|
3279
3682
|
|
|
3280
3683
|
class GitHubCopilotAdapter {
|
|
3281
3684
|
name = "github-copilot";
|
|
@@ -3322,14 +3725,47 @@ class GitHubCopilotAdapter {
|
|
|
3322
3725
|
supportsHooks() {
|
|
3323
3726
|
return false;
|
|
3324
3727
|
}
|
|
3728
|
+
async resolveModel(baseName, _thinkingBudget) {
|
|
3729
|
+
try {
|
|
3730
|
+
const stdout = await new Promise((resolve, reject) => {
|
|
3731
|
+
exec7("copilot --help", { timeout: 1e4 }, (error, stdout2) => {
|
|
3732
|
+
if (error)
|
|
3733
|
+
reject(error);
|
|
3734
|
+
else
|
|
3735
|
+
resolve(stdout2);
|
|
3736
|
+
});
|
|
3737
|
+
});
|
|
3738
|
+
const models = parseCopilotModels(stdout);
|
|
3739
|
+
const resolved = resolveModelFromList(models, {
|
|
3740
|
+
baseName,
|
|
3741
|
+
preferThinking: false
|
|
3742
|
+
});
|
|
3743
|
+
if (resolved === undefined) {
|
|
3744
|
+
log2.warn(`No matching model found for "${baseName}"`);
|
|
3745
|
+
return;
|
|
3746
|
+
}
|
|
3747
|
+
if (!SAFE_MODEL_ID_PATTERN.test(resolved)) {
|
|
3748
|
+
log2.warn(`Resolved model "${resolved}" contains unsafe characters`);
|
|
3749
|
+
return;
|
|
3750
|
+
}
|
|
3751
|
+
return resolved;
|
|
3752
|
+
} catch (err) {
|
|
3753
|
+
log2.warn(`Failed to resolve model "${baseName}": ${err instanceof Error ? err.message : String(err)}`);
|
|
3754
|
+
return;
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3325
3757
|
async execute(opts) {
|
|
3326
3758
|
const fullContent = `${opts.prompt}
|
|
3327
3759
|
|
|
3328
3760
|
--- DIFF ---
|
|
3329
3761
|
${opts.diff}`;
|
|
3330
3762
|
const tmpDir = os6.tmpdir();
|
|
3331
|
-
const tmpFile =
|
|
3332
|
-
await
|
|
3763
|
+
const tmpFile = path15.join(tmpDir, `gauntlet-copilot-${process.pid}-${Date.now()}.txt`);
|
|
3764
|
+
await fs15.writeFile(tmpFile, fullContent);
|
|
3765
|
+
let resolvedModel;
|
|
3766
|
+
if (opts.model) {
|
|
3767
|
+
resolvedModel = await this.resolveModel(opts.model, opts.thinkingBudget);
|
|
3768
|
+
}
|
|
3333
3769
|
const args = [
|
|
3334
3770
|
"--allow-tool",
|
|
3335
3771
|
"shell(cat)",
|
|
@@ -3344,7 +3780,10 @@ ${opts.diff}`;
|
|
|
3344
3780
|
"--allow-tool",
|
|
3345
3781
|
"shell(tail)"
|
|
3346
3782
|
];
|
|
3347
|
-
|
|
3783
|
+
if (resolvedModel) {
|
|
3784
|
+
args.push("--model", resolvedModel);
|
|
3785
|
+
}
|
|
3786
|
+
const cleanup = () => fs15.unlink(tmpFile).catch(() => {});
|
|
3348
3787
|
if (opts.onOutput) {
|
|
3349
3788
|
return runStreamingCommand({
|
|
3350
3789
|
command: "copilot",
|
|
@@ -3356,7 +3795,8 @@ ${opts.diff}`;
|
|
|
3356
3795
|
});
|
|
3357
3796
|
}
|
|
3358
3797
|
try {
|
|
3359
|
-
const
|
|
3798
|
+
const modelFlag = resolvedModel ? ` --model ${resolvedModel}` : "";
|
|
3799
|
+
const cmd = `cat "${tmpFile}" | copilot --allow-tool "shell(cat)" --allow-tool "shell(grep)" --allow-tool "shell(ls)" --allow-tool "shell(find)" --allow-tool "shell(head)" --allow-tool "shell(tail)"${modelFlag}`;
|
|
3360
3800
|
const { stdout } = await execAsync7(cmd, {
|
|
3361
3801
|
timeout: opts.timeoutMs,
|
|
3362
3802
|
maxBuffer: MAX_BUFFER_BYTES6
|
|
@@ -3389,7 +3829,7 @@ ${output}` : ""}`);
|
|
|
3389
3829
|
async function runStreamingCommand(opts) {
|
|
3390
3830
|
return new Promise((resolve, reject) => {
|
|
3391
3831
|
const chunks = [];
|
|
3392
|
-
const inputStream =
|
|
3832
|
+
const inputStream = fs16.open(opts.tmpFile, "r").then((handle) => {
|
|
3393
3833
|
const stream = handle.createReadStream();
|
|
3394
3834
|
return { stream, handle };
|
|
3395
3835
|
});
|
|
@@ -3554,7 +3994,7 @@ function isValidViolationLocation(file, line, diffRanges) {
|
|
|
3554
3994
|
}
|
|
3555
3995
|
|
|
3556
3996
|
// src/gates/review.ts
|
|
3557
|
-
var
|
|
3997
|
+
var log3 = getCategoryLogger("gate", "review");
|
|
3558
3998
|
var execAsync8 = promisify9(exec8);
|
|
3559
3999
|
var MAX_BUFFER_BYTES7 = 10 * 1024 * 1024;
|
|
3560
4000
|
var MAX_LOG_BUFFER_SIZE = 1e4;
|
|
@@ -3646,7 +4086,7 @@ ${JSON_SYSTEM_INSTRUCTION}`;
|
|
|
3646
4086
|
return logger;
|
|
3647
4087
|
};
|
|
3648
4088
|
try {
|
|
3649
|
-
|
|
4089
|
+
log3.debug(`Starting review: ${config.name} | entry=${entryPointPath}`);
|
|
3650
4090
|
await mainLogger(`Starting review: ${config.name}
|
|
3651
4091
|
`);
|
|
3652
4092
|
await mainLogger(`Entry point: ${entryPointPath}
|
|
@@ -3661,11 +4101,11 @@ ${JSON_SYSTEM_INSTRUCTION}`;
|
|
|
3661
4101
|
const diffFileRanges = parseDiff(diff);
|
|
3662
4102
|
const diffFiles = diffFileRanges.size;
|
|
3663
4103
|
const diffSizeMsg = `[diff-stats] files=${diffFiles} lines=${diffLines} chars=${diffChars} est_tokens=${diffEstTokens}`;
|
|
3664
|
-
|
|
4104
|
+
log3.debug(diffSizeMsg);
|
|
3665
4105
|
await mainLogger(`${diffSizeMsg}
|
|
3666
4106
|
`);
|
|
3667
4107
|
if (!diff.trim()) {
|
|
3668
|
-
|
|
4108
|
+
log3.debug(`Empty diff after trim, returning pass`);
|
|
3669
4109
|
await mainLogger(`No changes found in entry point, skipping review.
|
|
3670
4110
|
`);
|
|
3671
4111
|
await mainLogger(`Result: pass - No changes to review
|
|
@@ -3682,31 +4122,31 @@ ${JSON_SYSTEM_INSTRUCTION}`;
|
|
|
3682
4122
|
const outputs = [];
|
|
3683
4123
|
const preferences = config.cli_preference || [];
|
|
3684
4124
|
const parallel = config.parallel ?? false;
|
|
3685
|
-
|
|
4125
|
+
log3.debug(`Checking adapters: ${preferences.join(", ") || "(none configured)"}`);
|
|
3686
4126
|
const healthyAdapters = [];
|
|
3687
4127
|
const unhealthyMap = logDir ? await getUnhealthyAdapters(logDir) : {};
|
|
3688
4128
|
for (const toolName of preferences) {
|
|
3689
4129
|
const adapter = getAdapter(toolName);
|
|
3690
4130
|
if (!adapter) {
|
|
3691
|
-
|
|
4131
|
+
log3.debug(`Adapter ${toolName}: not found`);
|
|
3692
4132
|
continue;
|
|
3693
4133
|
}
|
|
3694
4134
|
const unhealthyEntry = unhealthyMap[toolName];
|
|
3695
4135
|
if (unhealthyEntry) {
|
|
3696
4136
|
if (isAdapterCoolingDown(unhealthyEntry)) {
|
|
3697
|
-
|
|
4137
|
+
log3.debug(`Adapter ${toolName}: cooling down`);
|
|
3698
4138
|
await mainLogger(`Skipping ${toolName}: cooling down (${unhealthyEntry.reason})
|
|
3699
4139
|
`);
|
|
3700
4140
|
continue;
|
|
3701
4141
|
}
|
|
3702
4142
|
const health = await adapter.checkHealth();
|
|
3703
4143
|
if (health.status === "healthy") {
|
|
3704
|
-
|
|
4144
|
+
log3.debug(`Adapter ${toolName}: cooldown expired, binary available, clearing unhealthy flag`);
|
|
3705
4145
|
if (logDir) {
|
|
3706
4146
|
await markAdapterHealthy(logDir, toolName);
|
|
3707
4147
|
}
|
|
3708
4148
|
} else {
|
|
3709
|
-
|
|
4149
|
+
log3.debug(`Adapter ${toolName}: cooldown expired but binary missing`);
|
|
3710
4150
|
await mainLogger(`Skipping ${toolName}: ${health.message || "Missing"}
|
|
3711
4151
|
`);
|
|
3712
4152
|
continue;
|
|
@@ -3714,7 +4154,7 @@ ${JSON_SYSTEM_INSTRUCTION}`;
|
|
|
3714
4154
|
} else {
|
|
3715
4155
|
const health = await adapter.checkHealth();
|
|
3716
4156
|
if (health.status !== "healthy") {
|
|
3717
|
-
|
|
4157
|
+
log3.debug(`Adapter ${toolName}: ${health.status}${health.message ? ` - ${health.message}` : ""}`);
|
|
3718
4158
|
await mainLogger(`Skipping ${toolName}: ${health.message || "Unhealthy"}
|
|
3719
4159
|
`);
|
|
3720
4160
|
continue;
|
|
@@ -3724,7 +4164,7 @@ ${JSON_SYSTEM_INSTRUCTION}`;
|
|
|
3724
4164
|
}
|
|
3725
4165
|
if (healthyAdapters.length === 0) {
|
|
3726
4166
|
const msg = "Review dispatch failed: no healthy adapters available";
|
|
3727
|
-
|
|
4167
|
+
log3.error(`ERROR: ${msg}`);
|
|
3728
4168
|
await mainLogger(`Result: error - ${msg}
|
|
3729
4169
|
`);
|
|
3730
4170
|
return {
|
|
@@ -3735,7 +4175,7 @@ ${JSON_SYSTEM_INSTRUCTION}`;
|
|
|
3735
4175
|
logPaths
|
|
3736
4176
|
};
|
|
3737
4177
|
}
|
|
3738
|
-
|
|
4178
|
+
log3.debug(`Healthy adapters: ${healthyAdapters.join(", ")}`);
|
|
3739
4179
|
const assignments = [];
|
|
3740
4180
|
for (let i = 0;i < required; i++) {
|
|
3741
4181
|
const adapter = healthyAdapters[i % healthyAdapters.length];
|
|
@@ -3785,12 +4225,12 @@ ${JSON_SYSTEM_INSTRUCTION}`;
|
|
|
3785
4225
|
}
|
|
3786
4226
|
}
|
|
3787
4227
|
const dispatchMsg = `Dispatching ${required} review(s) via round-robin: ${assignments.map((a) => `${a.adapter}@${a.reviewIndex}`).join(", ")}`;
|
|
3788
|
-
|
|
4228
|
+
log3.debug(dispatchMsg);
|
|
3789
4229
|
await mainLogger(`${dispatchMsg}
|
|
3790
4230
|
`);
|
|
3791
4231
|
const runningAssignments = assignments.filter((a) => !a.skip);
|
|
3792
4232
|
const skippedAssignments = assignments.filter((a) => a.skip);
|
|
3793
|
-
|
|
4233
|
+
log3.debug(`Running: ${runningAssignments.length}, Skipped: ${skippedAssignments.length}`);
|
|
3794
4234
|
const skippedSlotOutputs = [];
|
|
3795
4235
|
for (const assignment of skippedAssignments) {
|
|
3796
4236
|
const { logger, logPath } = await loggerFactory(assignment.adapter, assignment.reviewIndex);
|
|
@@ -3812,7 +4252,7 @@ ${JSON_SYSTEM_INSTRUCTION}`;
|
|
|
3812
4252
|
violations: [],
|
|
3813
4253
|
passIteration: assignment.passIteration
|
|
3814
4254
|
};
|
|
3815
|
-
await
|
|
4255
|
+
await fs17.writeFile(jsonPath, JSON.stringify(skippedOutput, null, 2));
|
|
3816
4256
|
if (!logPathsSet.has(logPath)) {
|
|
3817
4257
|
logPathsSet.add(logPath);
|
|
3818
4258
|
logPaths.push(logPath);
|
|
@@ -3879,7 +4319,7 @@ ${JSON_SYSTEM_INSTRUCTION}`;
|
|
|
3879
4319
|
}
|
|
3880
4320
|
const subResults = outputs.map((out) => {
|
|
3881
4321
|
const specificLog = logPaths.find((p) => {
|
|
3882
|
-
const filename =
|
|
4322
|
+
const filename = path16.basename(p);
|
|
3883
4323
|
return filename.includes(`_${out.adapter}@${out.reviewIndex}.`) && filename.endsWith(".log");
|
|
3884
4324
|
});
|
|
3885
4325
|
let logPath = specificLog;
|
|
@@ -3901,7 +4341,7 @@ ${JSON_SYSTEM_INSTRUCTION}`;
|
|
|
3901
4341
|
});
|
|
3902
4342
|
for (const skipped of skippedSlotOutputs) {
|
|
3903
4343
|
const specificLog = logPaths.find((p) => {
|
|
3904
|
-
const filename =
|
|
4344
|
+
const filename = path16.basename(p);
|
|
3905
4345
|
return filename.includes(`_${skipped.adapter}@${skipped.reviewIndex}.`) && filename.endsWith(".log");
|
|
3906
4346
|
});
|
|
3907
4347
|
subResults.push({
|
|
@@ -3920,7 +4360,7 @@ ${JSON_SYSTEM_INSTRUCTION}`;
|
|
|
3920
4360
|
const bIndex = parseInt(b.nameSuffix.match(/@(\d+)/)?.[1] || "0", 10);
|
|
3921
4361
|
return aIndex - bIndex;
|
|
3922
4362
|
});
|
|
3923
|
-
|
|
4363
|
+
log3.debug(`Complete: ${status} - ${message}`);
|
|
3924
4364
|
await mainLogger(`Result: ${status} - ${message}
|
|
3925
4365
|
`);
|
|
3926
4366
|
return {
|
|
@@ -3936,7 +4376,7 @@ ${JSON_SYSTEM_INSTRUCTION}`;
|
|
|
3936
4376
|
const err = error;
|
|
3937
4377
|
const errMsg = err.message || "Unknown error";
|
|
3938
4378
|
const errStack = err.stack || "";
|
|
3939
|
-
|
|
4379
|
+
log3.error(`CRITICAL ERROR: ${errMsg} ${errStack}`);
|
|
3940
4380
|
await mainLogger(`Critical Error: ${errMsg}
|
|
3941
4381
|
`);
|
|
3942
4382
|
await mainLogger(`Result: error
|
|
@@ -3985,7 +4425,7 @@ ${diff}
|
|
|
3985
4425
|
const output = await adapter.execute({
|
|
3986
4426
|
prompt: finalPrompt,
|
|
3987
4427
|
diff,
|
|
3988
|
-
model: config.model,
|
|
4428
|
+
model: adapterCfg?.model ?? config.model,
|
|
3989
4429
|
timeoutMs: config.timeout ? config.timeout * 1000 : REVIEW_ADAPTER_TIMEOUT_MS,
|
|
3990
4430
|
onOutput: (chunk) => {
|
|
3991
4431
|
adapterLogger(chunk);
|
|
@@ -4002,7 +4442,7 @@ ${output}
|
|
|
4002
4442
|
const reason = "Usage limit exceeded";
|
|
4003
4443
|
if (logDir) {
|
|
4004
4444
|
await markAdapterUnhealthy(logDir, adapter.name, reason);
|
|
4005
|
-
|
|
4445
|
+
log3.debug(`Adapter ${adapter.name} marked unhealthy for 1 hour: ${reason}`);
|
|
4006
4446
|
await mainLogger(`${adapter.name} marked unhealthy for 1 hour: ${reason}
|
|
4007
4447
|
`);
|
|
4008
4448
|
}
|
|
@@ -4120,7 +4560,7 @@ ${output}
|
|
|
4120
4560
|
} catch (error) {
|
|
4121
4561
|
const err = error;
|
|
4122
4562
|
const errorMsg = `Error running ${adapter.name}@${reviewIndex}: ${err.message}`;
|
|
4123
|
-
|
|
4563
|
+
log3.error(errorMsg);
|
|
4124
4564
|
await adapterLogger(`${errorMsg}
|
|
4125
4565
|
`);
|
|
4126
4566
|
await mainLogger(`${errorMsg}
|
|
@@ -4129,7 +4569,7 @@ ${output}
|
|
|
4129
4569
|
const reason = "Usage limit exceeded";
|
|
4130
4570
|
if (logDir) {
|
|
4131
4571
|
await markAdapterUnhealthy(logDir, adapter.name, reason);
|
|
4132
|
-
|
|
4572
|
+
log3.debug(`Adapter ${adapter.name} marked unhealthy for 1 hour: ${reason}`);
|
|
4133
4573
|
await mainLogger(`${adapter.name} marked unhealthy for 1 hour: ${reason}
|
|
4134
4574
|
`);
|
|
4135
4575
|
}
|
|
@@ -4147,7 +4587,7 @@ ${output}
|
|
|
4147
4587
|
}
|
|
4148
4588
|
}
|
|
4149
4589
|
async getDiff(entryPointPath, baseBranch, options) {
|
|
4150
|
-
|
|
4590
|
+
log3.debug(`getDiff: entryPoint=${entryPointPath}, fixBase=${options?.fixBase ?? "none"}, uncommitted=${options?.uncommitted ?? false}, commit=${options?.commit ?? "none"}`);
|
|
4151
4591
|
if (options?.fixBase) {
|
|
4152
4592
|
if (!/^[a-f0-9]+$/.test(options.fixBase)) {
|
|
4153
4593
|
throw new Error(`Invalid session ref: ${options.fixBase}`);
|
|
@@ -4177,15 +4617,15 @@ ${output}
|
|
|
4177
4617
|
}
|
|
4178
4618
|
const scopedDiff = [diff, ...newUntrackedDiffs].filter(Boolean).join(`
|
|
4179
4619
|
`);
|
|
4180
|
-
|
|
4620
|
+
log3.debug(`Scoped diff via fixBase: ${scopedDiff.split(`
|
|
4181
4621
|
`).length} lines`);
|
|
4182
4622
|
return scopedDiff;
|
|
4183
4623
|
} catch (error) {
|
|
4184
|
-
|
|
4624
|
+
log3.warn(`Failed to compute diff against fixBase ${options.fixBase}, falling back to full uncommitted diff. ${error instanceof Error ? error.message : error}`);
|
|
4185
4625
|
}
|
|
4186
4626
|
}
|
|
4187
4627
|
if (options?.uncommitted) {
|
|
4188
|
-
|
|
4628
|
+
log3.debug(`Using full uncommitted diff (no fixBase)`);
|
|
4189
4629
|
const pathArg = this.pathArg(entryPointPath);
|
|
4190
4630
|
const staged = await this.execDiff(`git diff --cached${pathArg}`);
|
|
4191
4631
|
const unstaged = await this.execDiff(`git diff${pathArg}`);
|
|
@@ -4411,7 +4851,7 @@ The following violations were NOT marked as fixed or skipped and are still activ
|
|
|
4411
4851
|
rawOutput,
|
|
4412
4852
|
violations: json.violations || []
|
|
4413
4853
|
};
|
|
4414
|
-
await
|
|
4854
|
+
await fs17.writeFile(jsonPath, JSON.stringify(fullOutput, null, 2));
|
|
4415
4855
|
return jsonPath;
|
|
4416
4856
|
}
|
|
4417
4857
|
parseLines(stdout) {
|
|
@@ -4597,13 +5037,13 @@ class Runner {
|
|
|
4597
5037
|
}
|
|
4598
5038
|
|
|
4599
5039
|
// src/output/console.ts
|
|
4600
|
-
import
|
|
5040
|
+
import fs19 from "node:fs/promises";
|
|
4601
5041
|
import chalk2 from "chalk";
|
|
4602
5042
|
|
|
4603
5043
|
// src/utils/log-parser.ts
|
|
4604
|
-
import
|
|
4605
|
-
import
|
|
4606
|
-
var
|
|
5044
|
+
import fs18 from "node:fs/promises";
|
|
5045
|
+
import path17 from "node:path";
|
|
5046
|
+
var log4 = getCategoryLogger("log-parser");
|
|
4607
5047
|
function parseReviewFilename(filename) {
|
|
4608
5048
|
const m = filename.match(/^(.+)_([^@]+)@(\d+)\.(\d+)\.(log|json)$/);
|
|
4609
5049
|
if (!m)
|
|
@@ -4621,9 +5061,9 @@ function parseReviewFilename(filename) {
|
|
|
4621
5061
|
}
|
|
4622
5062
|
async function parseJsonReviewFile(jsonPath) {
|
|
4623
5063
|
try {
|
|
4624
|
-
const content = await
|
|
5064
|
+
const content = await fs18.readFile(jsonPath, "utf-8");
|
|
4625
5065
|
const data = JSON.parse(content);
|
|
4626
|
-
const filename =
|
|
5066
|
+
const filename = path17.basename(jsonPath);
|
|
4627
5067
|
const parsed = parseReviewFilename(filename);
|
|
4628
5068
|
const jobId = parsed ? parsed.jobId : filename.replace(/\.\d+\.json$/, "");
|
|
4629
5069
|
if (data.status === "pass" || data.status === "skipped_prior_pass") {
|
|
@@ -4657,7 +5097,7 @@ async function parseJsonReviewFile(jsonPath) {
|
|
|
4657
5097
|
logPath: jsonPath.replace(/\.json$/, ".log")
|
|
4658
5098
|
};
|
|
4659
5099
|
} catch (error) {
|
|
4660
|
-
|
|
5100
|
+
log4.warn(`Failed to parse JSON review file: ${jsonPath} - ${error}`);
|
|
4661
5101
|
return null;
|
|
4662
5102
|
}
|
|
4663
5103
|
}
|
|
@@ -4669,8 +5109,8 @@ function extractPrefix(filename) {
|
|
|
4669
5109
|
}
|
|
4670
5110
|
async function parseLogFile(logPath) {
|
|
4671
5111
|
try {
|
|
4672
|
-
const content = await
|
|
4673
|
-
const filename =
|
|
5112
|
+
const content = await fs18.readFile(logPath, "utf-8");
|
|
5113
|
+
const filename = path17.basename(logPath);
|
|
4674
5114
|
const parsed = parseReviewFilename(filename);
|
|
4675
5115
|
const jobId = parsed ? parsed.jobId : extractPrefix(filename);
|
|
4676
5116
|
if (content.includes("--- Review Output")) {
|
|
@@ -4794,7 +5234,7 @@ async function parseLogFile(logPath) {
|
|
|
4794
5234
|
}
|
|
4795
5235
|
async function reconstructHistory(logDir) {
|
|
4796
5236
|
try {
|
|
4797
|
-
const files = await
|
|
5237
|
+
const files = await fs18.readdir(logDir);
|
|
4798
5238
|
const runNumbers = new Set;
|
|
4799
5239
|
for (const file of files) {
|
|
4800
5240
|
const m = file.match(/\.(\d+)\.(log|json)$/);
|
|
@@ -4818,9 +5258,9 @@ async function reconstructHistory(logDir) {
|
|
|
4818
5258
|
const logFile = runFiles.find((f) => f.startsWith(`${prefix}.${runNum}.`) && f.endsWith(".log"));
|
|
4819
5259
|
let failure = null;
|
|
4820
5260
|
if (jsonFile) {
|
|
4821
|
-
failure = await parseJsonReviewFile(
|
|
5261
|
+
failure = await parseJsonReviewFile(path17.join(logDir, jsonFile));
|
|
4822
5262
|
} else if (logFile) {
|
|
4823
|
-
failure = await parseLogFile(
|
|
5263
|
+
failure = await parseLogFile(path17.join(logDir, logFile));
|
|
4824
5264
|
}
|
|
4825
5265
|
if (failure) {
|
|
4826
5266
|
for (const af of failure.adapterFailures) {
|
|
@@ -4878,7 +5318,7 @@ async function reconstructHistory(logDir) {
|
|
|
4878
5318
|
}
|
|
4879
5319
|
async function isJsonReviewPassing(jsonPath) {
|
|
4880
5320
|
try {
|
|
4881
|
-
const content = await
|
|
5321
|
+
const content = await fs18.readFile(jsonPath, "utf-8");
|
|
4882
5322
|
const data = JSON.parse(content);
|
|
4883
5323
|
return data.status === "pass" || data.status === "skipped_prior_pass";
|
|
4884
5324
|
} catch {
|
|
@@ -4887,7 +5327,7 @@ async function isJsonReviewPassing(jsonPath) {
|
|
|
4887
5327
|
}
|
|
4888
5328
|
async function isLogReviewPassing(logPath) {
|
|
4889
5329
|
try {
|
|
4890
|
-
const content = await
|
|
5330
|
+
const content = await fs18.readFile(logPath, "utf-8");
|
|
4891
5331
|
if (content.includes("Status: skipped_prior_pass")) {
|
|
4892
5332
|
return true;
|
|
4893
5333
|
}
|
|
@@ -4901,7 +5341,7 @@ async function isLogReviewPassing(logPath) {
|
|
|
4901
5341
|
}
|
|
4902
5342
|
async function findPreviousFailures(logDir, gateFilter, includePassedSlots) {
|
|
4903
5343
|
try {
|
|
4904
|
-
const files = await
|
|
5344
|
+
const files = await fs18.readdir(logDir);
|
|
4905
5345
|
const gateFailures = [];
|
|
4906
5346
|
const passedSlots = new Map;
|
|
4907
5347
|
const reviewSlotMap = new Map;
|
|
@@ -4952,7 +5392,7 @@ async function findPreviousFailures(logDir, gateFilter, includePassedSlots) {
|
|
|
4952
5392
|
const reviewIndex = parseInt(slotKey.substring(sepIdx + 1), 10);
|
|
4953
5393
|
const parsed = parseReviewFilename(fileInfo.filename);
|
|
4954
5394
|
const adapter = parsed?.adapter || "unknown";
|
|
4955
|
-
const filePath =
|
|
5395
|
+
const filePath = path17.join(logDir, fileInfo.filename);
|
|
4956
5396
|
let isPassing = false;
|
|
4957
5397
|
if (fileInfo.ext === "json") {
|
|
4958
5398
|
isPassing = await isJsonReviewPassing(filePath);
|
|
@@ -4987,7 +5427,7 @@ async function findPreviousFailures(logDir, gateFilter, includePassedSlots) {
|
|
|
4987
5427
|
if (status === "skipped")
|
|
4988
5428
|
continue;
|
|
4989
5429
|
if (status !== "new" && status !== "fixed" && status !== "skipped") {
|
|
4990
|
-
|
|
5430
|
+
log4.warn(`Unexpected status "${status}" for violation in ${jobId}. Treating as "new".`);
|
|
4991
5431
|
v.status = "new";
|
|
4992
5432
|
}
|
|
4993
5433
|
filteredViolations.push(v);
|
|
@@ -5010,7 +5450,7 @@ async function findPreviousFailures(logDir, gateFilter, includePassedSlots) {
|
|
|
5010
5450
|
gateName: "",
|
|
5011
5451
|
entryPoint: "",
|
|
5012
5452
|
adapterFailures,
|
|
5013
|
-
logPath:
|
|
5453
|
+
logPath: path17.join(logDir, `${jobId}.log`)
|
|
5014
5454
|
});
|
|
5015
5455
|
}
|
|
5016
5456
|
for (const [prefix, runMap] of checkPrefixMap.entries()) {
|
|
@@ -5020,9 +5460,9 @@ async function findPreviousFailures(logDir, gateFilter, includePassedSlots) {
|
|
|
5020
5460
|
continue;
|
|
5021
5461
|
let failure = null;
|
|
5022
5462
|
if (exts.has("json")) {
|
|
5023
|
-
failure = await parseJsonReviewFile(
|
|
5463
|
+
failure = await parseJsonReviewFile(path17.join(logDir, `${prefix}.${latestRun}.json`));
|
|
5024
5464
|
} else if (exts.has("log")) {
|
|
5025
|
-
failure = await parseLogFile(
|
|
5465
|
+
failure = await parseLogFile(path17.join(logDir, `${prefix}.${latestRun}.log`));
|
|
5026
5466
|
}
|
|
5027
5467
|
if (failure) {
|
|
5028
5468
|
for (const af of failure.adapterFailures) {
|
|
@@ -5032,7 +5472,7 @@ async function findPreviousFailures(logDir, gateFilter, includePassedSlots) {
|
|
|
5032
5472
|
if (status === "skipped")
|
|
5033
5473
|
continue;
|
|
5034
5474
|
if (status !== "new" && status !== "fixed" && status !== "skipped") {
|
|
5035
|
-
|
|
5475
|
+
log4.warn(`Unexpected status "${status}" for violation in ${failure.jobId}. Treating as "new".`);
|
|
5036
5476
|
v.status = "new";
|
|
5037
5477
|
}
|
|
5038
5478
|
filteredViolations.push(v);
|
|
@@ -5056,6 +5496,26 @@ async function findPreviousFailures(logDir, gateFilter, includePassedSlots) {
|
|
|
5056
5496
|
return includePassedSlots ? { failures: [], passedSlots: new Map } : [];
|
|
5057
5497
|
}
|
|
5058
5498
|
}
|
|
5499
|
+
async function hasSkippedViolationsInLogs(opts) {
|
|
5500
|
+
const { logDir } = opts;
|
|
5501
|
+
try {
|
|
5502
|
+
const files = await fs18.readdir(logDir);
|
|
5503
|
+
for (const file of files) {
|
|
5504
|
+
if (!file.endsWith(".json"))
|
|
5505
|
+
continue;
|
|
5506
|
+
try {
|
|
5507
|
+
const content = await fs18.readFile(path17.join(logDir, file), "utf-8");
|
|
5508
|
+
const data = JSON.parse(content);
|
|
5509
|
+
if (data.violations?.some((v) => v.status === "skipped")) {
|
|
5510
|
+
return true;
|
|
5511
|
+
}
|
|
5512
|
+
} catch {}
|
|
5513
|
+
}
|
|
5514
|
+
return false;
|
|
5515
|
+
} catch {
|
|
5516
|
+
return false;
|
|
5517
|
+
}
|
|
5518
|
+
}
|
|
5059
5519
|
|
|
5060
5520
|
// src/output/console.ts
|
|
5061
5521
|
class ConsoleReporter {
|
|
@@ -5173,7 +5633,7 @@ ${chalk2.bold("━━━━━━━━━━━━━━━━━━━━━
|
|
|
5173
5633
|
const allDetails = [];
|
|
5174
5634
|
for (const logPath of logPaths) {
|
|
5175
5635
|
try {
|
|
5176
|
-
const logContent = await
|
|
5636
|
+
const logContent = await fs19.readFile(logPath, "utf-8");
|
|
5177
5637
|
const details = this.parseLogContent(logContent, result.jobId);
|
|
5178
5638
|
allDetails.push(...details);
|
|
5179
5639
|
} catch (_error) {
|
|
@@ -5283,9 +5743,9 @@ ${chalk2.bold("━━━━━━━━━━━━━━━━━━━━━
|
|
|
5283
5743
|
}
|
|
5284
5744
|
|
|
5285
5745
|
// src/output/console-log.ts
|
|
5286
|
-
import
|
|
5746
|
+
import fs20 from "node:fs";
|
|
5287
5747
|
import fsPromises2 from "node:fs/promises";
|
|
5288
|
-
import
|
|
5748
|
+
import path18 from "node:path";
|
|
5289
5749
|
import { inspect } from "node:util";
|
|
5290
5750
|
var ANSI_REGEX = /\x1b(?:\[[0-9;?]*[A-Za-z]|[78])/g;
|
|
5291
5751
|
function stripAnsi(text) {
|
|
@@ -5295,9 +5755,9 @@ function formatArgs(args) {
|
|
|
5295
5755
|
return args.map((a) => typeof a === "string" ? a : inspect(a, { depth: 4 })).join(" ");
|
|
5296
5756
|
}
|
|
5297
5757
|
function openLogFileExclusive(logDir, runNum) {
|
|
5298
|
-
const logPath =
|
|
5758
|
+
const logPath = path18.join(logDir, `console.${runNum}.log`);
|
|
5299
5759
|
try {
|
|
5300
|
-
const fd =
|
|
5760
|
+
const fd = fs20.openSync(logPath, fs20.constants.O_WRONLY | fs20.constants.O_CREAT | fs20.constants.O_EXCL);
|
|
5301
5761
|
return { fd, logPath };
|
|
5302
5762
|
} catch (e) {
|
|
5303
5763
|
const error = e;
|
|
@@ -5311,9 +5771,9 @@ function openLogFileExclusive(logDir, runNum) {
|
|
|
5311
5771
|
function openLogFileFallback(logDir, startNum) {
|
|
5312
5772
|
let runNum = startNum;
|
|
5313
5773
|
for (let attempts = 0;attempts < 100; attempts++) {
|
|
5314
|
-
const logPath =
|
|
5774
|
+
const logPath = path18.join(logDir, `console.${runNum}.log`);
|
|
5315
5775
|
try {
|
|
5316
|
-
const fd =
|
|
5776
|
+
const fd = fs20.openSync(logPath, fs20.constants.O_WRONLY | fs20.constants.O_CREAT | fs20.constants.O_EXCL);
|
|
5317
5777
|
return { fd, logPath };
|
|
5318
5778
|
} catch (e) {
|
|
5319
5779
|
const error = e;
|
|
@@ -5334,7 +5794,7 @@ async function startConsoleLog(logDir, runNumber) {
|
|
|
5334
5794
|
if (isClosed)
|
|
5335
5795
|
return;
|
|
5336
5796
|
try {
|
|
5337
|
-
|
|
5797
|
+
fs20.writeSync(fd, stripAnsi(text));
|
|
5338
5798
|
} catch {}
|
|
5339
5799
|
};
|
|
5340
5800
|
const originalLog = console.log;
|
|
@@ -5382,7 +5842,7 @@ async function startConsoleLog(logDir, runNumber) {
|
|
|
5382
5842
|
process.stdout.write = originalStdoutWrite;
|
|
5383
5843
|
process.stderr.write = originalStderrWrite;
|
|
5384
5844
|
try {
|
|
5385
|
-
|
|
5845
|
+
fs20.closeSync(fd);
|
|
5386
5846
|
} catch {}
|
|
5387
5847
|
},
|
|
5388
5848
|
writeToLogOnly: (text) => {
|
|
@@ -5390,20 +5850,20 @@ async function startConsoleLog(logDir, runNumber) {
|
|
|
5390
5850
|
}
|
|
5391
5851
|
};
|
|
5392
5852
|
} catch (error) {
|
|
5393
|
-
|
|
5853
|
+
fs20.closeSync(fd);
|
|
5394
5854
|
throw error;
|
|
5395
5855
|
}
|
|
5396
5856
|
}
|
|
5397
5857
|
|
|
5398
5858
|
// src/output/logger.ts
|
|
5399
|
-
import
|
|
5400
|
-
import
|
|
5859
|
+
import fs21 from "node:fs/promises";
|
|
5860
|
+
import path19 from "node:path";
|
|
5401
5861
|
function formatTimestamp() {
|
|
5402
5862
|
return new Date().toISOString();
|
|
5403
5863
|
}
|
|
5404
5864
|
async function computeGlobalRunNumber(logDir) {
|
|
5405
5865
|
try {
|
|
5406
|
-
const files = await
|
|
5866
|
+
const files = await fs21.readdir(logDir);
|
|
5407
5867
|
let max = 0;
|
|
5408
5868
|
for (const file of files) {
|
|
5409
5869
|
if (!file.endsWith(".log") && !file.endsWith(".json"))
|
|
@@ -5429,7 +5889,7 @@ class Logger {
|
|
|
5429
5889
|
this.logDir = logDir;
|
|
5430
5890
|
}
|
|
5431
5891
|
async init() {
|
|
5432
|
-
await
|
|
5892
|
+
await fs21.mkdir(this.logDir, { recursive: true });
|
|
5433
5893
|
this.globalRunNumber = await computeGlobalRunNumber(this.logDir);
|
|
5434
5894
|
}
|
|
5435
5895
|
async close() {}
|
|
@@ -5447,14 +5907,14 @@ class Logger {
|
|
|
5447
5907
|
} else {
|
|
5448
5908
|
filename = `${safeName}.${runNum}.log`;
|
|
5449
5909
|
}
|
|
5450
|
-
return
|
|
5910
|
+
return path19.join(this.logDir, filename);
|
|
5451
5911
|
}
|
|
5452
5912
|
async initFile(logPath) {
|
|
5453
5913
|
if (this.initializedFiles.has(logPath)) {
|
|
5454
5914
|
return;
|
|
5455
5915
|
}
|
|
5456
5916
|
this.initializedFiles.add(logPath);
|
|
5457
|
-
await
|
|
5917
|
+
await fs21.writeFile(logPath, "");
|
|
5458
5918
|
}
|
|
5459
5919
|
async createJobLogger(jobId) {
|
|
5460
5920
|
const logPath = await this.getLogPath(jobId);
|
|
@@ -5466,7 +5926,7 @@ class Logger {
|
|
|
5466
5926
|
if (lines.length > 0) {
|
|
5467
5927
|
lines[0] = `[${timestamp}] ${lines[0]}`;
|
|
5468
5928
|
}
|
|
5469
|
-
await
|
|
5929
|
+
await fs21.appendFile(logPath, lines.join(`
|
|
5470
5930
|
`) + (text.endsWith(`
|
|
5471
5931
|
`) ? "" : `
|
|
5472
5932
|
`));
|
|
@@ -5483,7 +5943,7 @@ class Logger {
|
|
|
5483
5943
|
if (lines.length > 0) {
|
|
5484
5944
|
lines[0] = `[${timestamp}] ${lines[0]}`;
|
|
5485
5945
|
}
|
|
5486
|
-
await
|
|
5946
|
+
await fs21.appendFile(logPath, lines.join(`
|
|
5487
5947
|
`) + (text.endsWith(`
|
|
5488
5948
|
`) ? "" : `
|
|
5489
5949
|
`));
|
|
@@ -5494,8 +5954,8 @@ class Logger {
|
|
|
5494
5954
|
}
|
|
5495
5955
|
|
|
5496
5956
|
// src/commands/shared.ts
|
|
5497
|
-
import
|
|
5498
|
-
import
|
|
5957
|
+
import fs22 from "node:fs/promises";
|
|
5958
|
+
import path20 from "node:path";
|
|
5499
5959
|
var LOCK_FILENAME = ".gauntlet-run.lock";
|
|
5500
5960
|
var SESSION_REF_FILENAME2 = ".session_ref";
|
|
5501
5961
|
async function shouldAutoClean(logDir, baseBranch) {
|
|
@@ -5530,17 +5990,17 @@ async function performAutoClean(logDir, result, maxPreviousLogs = 3) {
|
|
|
5530
5990
|
}
|
|
5531
5991
|
async function exists(filePath) {
|
|
5532
5992
|
try {
|
|
5533
|
-
await
|
|
5993
|
+
await fs22.stat(filePath);
|
|
5534
5994
|
return true;
|
|
5535
5995
|
} catch {
|
|
5536
5996
|
return false;
|
|
5537
5997
|
}
|
|
5538
5998
|
}
|
|
5539
5999
|
async function acquireLock(logDir) {
|
|
5540
|
-
await
|
|
5541
|
-
const lockPath =
|
|
6000
|
+
await fs22.mkdir(logDir, { recursive: true });
|
|
6001
|
+
const lockPath = path20.resolve(logDir, LOCK_FILENAME);
|
|
5542
6002
|
try {
|
|
5543
|
-
await
|
|
6003
|
+
await fs22.writeFile(lockPath, String(process.pid), { flag: "wx" });
|
|
5544
6004
|
} catch (err) {
|
|
5545
6005
|
if (typeof err === "object" && err !== null && "code" in err && err.code === "EEXIST") {
|
|
5546
6006
|
console.error(`Error: A gauntlet run is already in progress (lock file: ${lockPath}).`);
|
|
@@ -5551,14 +6011,14 @@ async function acquireLock(logDir) {
|
|
|
5551
6011
|
}
|
|
5552
6012
|
}
|
|
5553
6013
|
async function releaseLock(logDir) {
|
|
5554
|
-
const lockPath =
|
|
6014
|
+
const lockPath = path20.resolve(logDir, LOCK_FILENAME);
|
|
5555
6015
|
try {
|
|
5556
|
-
await
|
|
6016
|
+
await fs22.rm(lockPath, { force: true });
|
|
5557
6017
|
} catch {}
|
|
5558
6018
|
}
|
|
5559
6019
|
async function hasExistingLogs(logDir) {
|
|
5560
6020
|
try {
|
|
5561
|
-
const entries = await
|
|
6021
|
+
const entries = await fs22.readdir(logDir);
|
|
5562
6022
|
return entries.some((f) => (f.endsWith(".log") || f.endsWith(".json")) && f !== "previous" && !f.startsWith("console.") && !f.startsWith("."));
|
|
5563
6023
|
} catch {
|
|
5564
6024
|
return false;
|
|
@@ -5577,7 +6037,7 @@ function getPersistentFiles() {
|
|
|
5577
6037
|
}
|
|
5578
6038
|
async function hasCurrentLogs(logDir) {
|
|
5579
6039
|
try {
|
|
5580
|
-
const files = await
|
|
6040
|
+
const files = await fs22.readdir(logDir);
|
|
5581
6041
|
const persistentFiles = getPersistentFiles();
|
|
5582
6042
|
return files.some((f) => (f.endsWith(".log") || f.endsWith(".json")) && f !== "previous" && !persistentFiles.has(f));
|
|
5583
6043
|
} catch {
|
|
@@ -5589,23 +6049,23 @@ function getCurrentLogFiles(files) {
|
|
|
5589
6049
|
return files.filter((file) => !file.startsWith("previous") && !persistentFiles.has(file));
|
|
5590
6050
|
}
|
|
5591
6051
|
async function deleteCurrentLogs(logDir) {
|
|
5592
|
-
const files = await
|
|
5593
|
-
await Promise.all(getCurrentLogFiles(files).map((file) =>
|
|
6052
|
+
const files = await fs22.readdir(logDir);
|
|
6053
|
+
await Promise.all(getCurrentLogFiles(files).map((file) => fs22.rm(path20.join(logDir, file), { recursive: true, force: true })));
|
|
5594
6054
|
}
|
|
5595
6055
|
async function rotatePreviousDirs(logDir, maxPreviousLogs) {
|
|
5596
6056
|
const oldestSuffix = maxPreviousLogs - 1;
|
|
5597
6057
|
const oldestDir = oldestSuffix === 0 ? "previous" : `previous.${oldestSuffix}`;
|
|
5598
|
-
const oldestPath =
|
|
6058
|
+
const oldestPath = path20.join(logDir, oldestDir);
|
|
5599
6059
|
if (await exists(oldestPath)) {
|
|
5600
|
-
await
|
|
6060
|
+
await fs22.rm(oldestPath, { recursive: true, force: true });
|
|
5601
6061
|
}
|
|
5602
6062
|
for (let i = oldestSuffix - 1;i >= 0; i--) {
|
|
5603
6063
|
const fromName = i === 0 ? "previous" : `previous.${i}`;
|
|
5604
6064
|
const toName = `previous.${i + 1}`;
|
|
5605
|
-
const fromPath =
|
|
5606
|
-
const toPath =
|
|
6065
|
+
const fromPath = path20.join(logDir, fromName);
|
|
6066
|
+
const toPath = path20.join(logDir, toName);
|
|
5607
6067
|
if (await exists(fromPath)) {
|
|
5608
|
-
await
|
|
6068
|
+
await fs22.rename(fromPath, toPath);
|
|
5609
6069
|
}
|
|
5610
6070
|
}
|
|
5611
6071
|
}
|
|
@@ -5620,12 +6080,12 @@ async function cleanLogs(logDir, maxPreviousLogs = 3) {
|
|
|
5620
6080
|
return;
|
|
5621
6081
|
}
|
|
5622
6082
|
await rotatePreviousDirs(logDir, maxPreviousLogs);
|
|
5623
|
-
const previousDir =
|
|
5624
|
-
await
|
|
5625
|
-
const files = await
|
|
5626
|
-
await Promise.all(getCurrentLogFiles(files).map((file) =>
|
|
6083
|
+
const previousDir = path20.join(logDir, "previous");
|
|
6084
|
+
await fs22.mkdir(previousDir, { recursive: true });
|
|
6085
|
+
const files = await fs22.readdir(logDir);
|
|
6086
|
+
await Promise.all(getCurrentLogFiles(files).map((file) => fs22.rename(path20.join(logDir, file), path20.join(previousDir, file))));
|
|
5627
6087
|
try {
|
|
5628
|
-
await
|
|
6088
|
+
await fs22.rm(path20.join(logDir, SESSION_REF_FILENAME2), { force: true });
|
|
5629
6089
|
} catch {}
|
|
5630
6090
|
} catch (error) {
|
|
5631
6091
|
console.warn("Failed to clean logs in", logDir, ":", error instanceof Error ? error.message : error);
|
|
@@ -5769,14 +6229,14 @@ function registerCheckCommand(program) {
|
|
|
5769
6229
|
});
|
|
5770
6230
|
}
|
|
5771
6231
|
// src/commands/ci/init.ts
|
|
5772
|
-
import
|
|
5773
|
-
import
|
|
6232
|
+
import fs24 from "node:fs/promises";
|
|
6233
|
+
import path22 from "node:path";
|
|
5774
6234
|
import chalk4 from "chalk";
|
|
5775
6235
|
import YAML5 from "yaml";
|
|
5776
6236
|
|
|
5777
6237
|
// src/config/ci-loader.ts
|
|
5778
|
-
import
|
|
5779
|
-
import
|
|
6238
|
+
import fs23 from "node:fs/promises";
|
|
6239
|
+
import path21 from "node:path";
|
|
5780
6240
|
import YAML4 from "yaml";
|
|
5781
6241
|
|
|
5782
6242
|
// src/config/ci-schema.ts
|
|
@@ -5806,17 +6266,17 @@ var ciConfigSchema = z3.object({
|
|
|
5806
6266
|
var GAUNTLET_DIR2 = ".gauntlet";
|
|
5807
6267
|
var CI_FILE = "ci.yml";
|
|
5808
6268
|
async function loadCIConfig(rootDir = process.cwd()) {
|
|
5809
|
-
const ciPath =
|
|
6269
|
+
const ciPath = path21.join(rootDir, GAUNTLET_DIR2, CI_FILE);
|
|
5810
6270
|
if (!await fileExists3(ciPath)) {
|
|
5811
6271
|
throw new Error(`CI configuration file not found at ${ciPath}. Run 'agent-gauntlet ci init' to create it.`);
|
|
5812
6272
|
}
|
|
5813
|
-
const content = await
|
|
6273
|
+
const content = await fs23.readFile(ciPath, "utf-8");
|
|
5814
6274
|
const raw = YAML4.parse(content);
|
|
5815
6275
|
return ciConfigSchema.parse(raw);
|
|
5816
6276
|
}
|
|
5817
|
-
async function fileExists3(
|
|
6277
|
+
async function fileExists3(path22) {
|
|
5818
6278
|
try {
|
|
5819
|
-
const stat = await
|
|
6279
|
+
const stat = await fs23.stat(path22);
|
|
5820
6280
|
return stat.isFile();
|
|
5821
6281
|
} catch {
|
|
5822
6282
|
return false;
|
|
@@ -5907,13 +6367,13 @@ jobs:
|
|
|
5907
6367
|
|
|
5908
6368
|
// src/commands/ci/init.ts
|
|
5909
6369
|
async function initCI() {
|
|
5910
|
-
const workflowDir =
|
|
5911
|
-
const workflowPath =
|
|
5912
|
-
const gauntletDir =
|
|
5913
|
-
const ciConfigPath =
|
|
6370
|
+
const workflowDir = path22.join(process.cwd(), ".github", "workflows");
|
|
6371
|
+
const workflowPath = path22.join(workflowDir, "gauntlet.yml");
|
|
6372
|
+
const gauntletDir = path22.join(process.cwd(), ".gauntlet");
|
|
6373
|
+
const ciConfigPath = path22.join(gauntletDir, "ci.yml");
|
|
5914
6374
|
if (!await fileExists4(ciConfigPath)) {
|
|
5915
6375
|
console.log(chalk4.yellow("Creating starter .gauntlet/ci.yml..."));
|
|
5916
|
-
await
|
|
6376
|
+
await fs24.mkdir(gauntletDir, { recursive: true });
|
|
5917
6377
|
const starterContent = `# CI Configuration for Agent Gauntlet
|
|
5918
6378
|
# Define runtimes, services, and which checks to run in CI.
|
|
5919
6379
|
|
|
@@ -5935,7 +6395,7 @@ checks:
|
|
|
5935
6395
|
# - name: linter
|
|
5936
6396
|
# requires_runtimes: [ruby]
|
|
5937
6397
|
`;
|
|
5938
|
-
await
|
|
6398
|
+
await fs24.writeFile(ciConfigPath, starterContent);
|
|
5939
6399
|
} else {
|
|
5940
6400
|
console.log(chalk4.dim("Found existing .gauntlet/ci.yml"));
|
|
5941
6401
|
}
|
|
@@ -5946,7 +6406,7 @@ checks:
|
|
|
5946
6406
|
console.warn(chalk4.yellow("Could not load CI config to inject services. Workflow will have no services defined."));
|
|
5947
6407
|
}
|
|
5948
6408
|
console.log(chalk4.dim(`Generating ${workflowPath}...`));
|
|
5949
|
-
await
|
|
6409
|
+
await fs24.mkdir(workflowDir, { recursive: true });
|
|
5950
6410
|
let templateContent = workflow_default;
|
|
5951
6411
|
if (ciConfig?.services && Object.keys(ciConfig.services).length > 0) {
|
|
5952
6412
|
const servicesYaml = YAML5.stringify({ services: ciConfig.services });
|
|
@@ -5958,12 +6418,12 @@ checks:
|
|
|
5958
6418
|
templateContent = templateContent.replace(` # Services will be injected here by agent-gauntlet
|
|
5959
6419
|
`, "");
|
|
5960
6420
|
}
|
|
5961
|
-
await
|
|
6421
|
+
await fs24.writeFile(workflowPath, templateContent);
|
|
5962
6422
|
console.log(chalk4.green("Successfully generated GitHub Actions workflow!"));
|
|
5963
6423
|
}
|
|
5964
|
-
async function fileExists4(
|
|
6424
|
+
async function fileExists4(path23) {
|
|
5965
6425
|
try {
|
|
5966
|
-
const stat = await
|
|
6426
|
+
const stat = await fs24.stat(path23);
|
|
5967
6427
|
return stat.isFile();
|
|
5968
6428
|
} catch {
|
|
5969
6429
|
return false;
|
|
@@ -6164,12 +6624,12 @@ function printJobsByWorkDir(jobs) {
|
|
|
6164
6624
|
}
|
|
6165
6625
|
}
|
|
6166
6626
|
// src/commands/health.ts
|
|
6167
|
-
import
|
|
6627
|
+
import path24 from "node:path";
|
|
6168
6628
|
import chalk7 from "chalk";
|
|
6169
6629
|
|
|
6170
6630
|
// src/config/validator.ts
|
|
6171
|
-
import
|
|
6172
|
-
import
|
|
6631
|
+
import fs25 from "node:fs/promises";
|
|
6632
|
+
import path23 from "node:path";
|
|
6173
6633
|
import matter2 from "gray-matter";
|
|
6174
6634
|
import YAML6 from "yaml";
|
|
6175
6635
|
import { ZodError } from "zod";
|
|
@@ -6180,10 +6640,10 @@ var REVIEWS_DIR2 = "reviews";
|
|
|
6180
6640
|
async function validateConfig(rootDir = process.cwd()) {
|
|
6181
6641
|
const issues = [];
|
|
6182
6642
|
const filesChecked = [];
|
|
6183
|
-
const gauntletPath =
|
|
6643
|
+
const gauntletPath = path23.join(rootDir, GAUNTLET_DIR3);
|
|
6184
6644
|
const existingCheckNames = new Set;
|
|
6185
6645
|
const existingReviewNames = new Set;
|
|
6186
|
-
const configPath =
|
|
6646
|
+
const configPath = path23.join(gauntletPath, CONFIG_FILE2);
|
|
6187
6647
|
let projectConfig = null;
|
|
6188
6648
|
const checks = {};
|
|
6189
6649
|
const reviews = {};
|
|
@@ -6191,7 +6651,7 @@ async function validateConfig(rootDir = process.cwd()) {
|
|
|
6191
6651
|
try {
|
|
6192
6652
|
if (await fileExists5(configPath)) {
|
|
6193
6653
|
filesChecked.push(configPath);
|
|
6194
|
-
const configContent = await
|
|
6654
|
+
const configContent = await fs25.readFile(configPath, "utf-8");
|
|
6195
6655
|
try {
|
|
6196
6656
|
const raw = YAML6.parse(configContent);
|
|
6197
6657
|
projectConfig = gauntletConfigSchema.parse(raw);
|
|
@@ -6237,17 +6697,17 @@ async function validateConfig(rootDir = process.cwd()) {
|
|
|
6237
6697
|
message: `Error reading file: ${err.message}`
|
|
6238
6698
|
});
|
|
6239
6699
|
}
|
|
6240
|
-
const checksPath =
|
|
6700
|
+
const checksPath = path23.join(gauntletPath, CHECKS_DIR2);
|
|
6241
6701
|
if (await dirExists2(checksPath)) {
|
|
6242
6702
|
try {
|
|
6243
|
-
const checkFiles = await
|
|
6703
|
+
const checkFiles = await fs25.readdir(checksPath);
|
|
6244
6704
|
for (const file of checkFiles) {
|
|
6245
6705
|
if (file.endsWith(".yml") || file.endsWith(".yaml")) {
|
|
6246
|
-
const filePath =
|
|
6706
|
+
const filePath = path23.join(checksPath, file);
|
|
6247
6707
|
filesChecked.push(filePath);
|
|
6248
|
-
const name =
|
|
6708
|
+
const name = path23.basename(file, path23.extname(file));
|
|
6249
6709
|
try {
|
|
6250
|
-
const content = await
|
|
6710
|
+
const content = await fs25.readFile(filePath, "utf-8");
|
|
6251
6711
|
const raw = YAML6.parse(content);
|
|
6252
6712
|
const parsed = checkGateSchema.parse(raw);
|
|
6253
6713
|
existingCheckNames.add(name);
|
|
@@ -6299,14 +6759,14 @@ async function validateConfig(rootDir = process.cwd()) {
|
|
|
6299
6759
|
});
|
|
6300
6760
|
}
|
|
6301
6761
|
}
|
|
6302
|
-
const reviewsPath =
|
|
6762
|
+
const reviewsPath = path23.join(gauntletPath, REVIEWS_DIR2);
|
|
6303
6763
|
if (await dirExists2(reviewsPath)) {
|
|
6304
6764
|
try {
|
|
6305
|
-
const reviewFiles = await
|
|
6765
|
+
const reviewFiles = await fs25.readdir(reviewsPath);
|
|
6306
6766
|
const reviewNameSources = new Map;
|
|
6307
6767
|
for (const file of reviewFiles) {
|
|
6308
6768
|
if (file.endsWith(".md") || file.endsWith(".yml") || file.endsWith(".yaml")) {
|
|
6309
|
-
const name =
|
|
6769
|
+
const name = path23.basename(file, path23.extname(file));
|
|
6310
6770
|
const sources = reviewNameSources.get(name) || [];
|
|
6311
6771
|
sources.push(file);
|
|
6312
6772
|
reviewNameSources.set(name, sources);
|
|
@@ -6323,12 +6783,12 @@ async function validateConfig(rootDir = process.cwd()) {
|
|
|
6323
6783
|
}
|
|
6324
6784
|
for (const file of reviewFiles) {
|
|
6325
6785
|
if (file.endsWith(".md")) {
|
|
6326
|
-
const filePath =
|
|
6327
|
-
const reviewName =
|
|
6786
|
+
const filePath = path23.join(reviewsPath, file);
|
|
6787
|
+
const reviewName = path23.basename(file, ".md");
|
|
6328
6788
|
existingReviewNames.add(reviewName);
|
|
6329
6789
|
filesChecked.push(filePath);
|
|
6330
6790
|
try {
|
|
6331
|
-
const content = await
|
|
6791
|
+
const content = await fs25.readFile(filePath, "utf-8");
|
|
6332
6792
|
const { data: frontmatter, content: _promptBody } = matter2(content);
|
|
6333
6793
|
if (!frontmatter || Object.keys(frontmatter).length === 0) {
|
|
6334
6794
|
issues.push({
|
|
@@ -6340,7 +6800,7 @@ async function validateConfig(rootDir = process.cwd()) {
|
|
|
6340
6800
|
}
|
|
6341
6801
|
validateCliPreferenceTools(frontmatter, filePath, issues);
|
|
6342
6802
|
const parsedFrontmatter = reviewPromptFrontmatterSchema.parse(frontmatter);
|
|
6343
|
-
const name =
|
|
6803
|
+
const name = path23.basename(file, ".md");
|
|
6344
6804
|
reviews[name] = parsedFrontmatter;
|
|
6345
6805
|
reviewSourceFiles[name] = filePath;
|
|
6346
6806
|
validateReviewSemantics(parsedFrontmatter, filePath, issues);
|
|
@@ -6348,12 +6808,12 @@ async function validateConfig(rootDir = process.cwd()) {
|
|
|
6348
6808
|
handleReviewValidationError(error, filePath, issues);
|
|
6349
6809
|
}
|
|
6350
6810
|
} else if (file.endsWith(".yml") || file.endsWith(".yaml")) {
|
|
6351
|
-
const filePath =
|
|
6352
|
-
const reviewName =
|
|
6811
|
+
const filePath = path23.join(reviewsPath, file);
|
|
6812
|
+
const reviewName = path23.basename(file, path23.extname(file));
|
|
6353
6813
|
existingReviewNames.add(reviewName);
|
|
6354
6814
|
filesChecked.push(filePath);
|
|
6355
6815
|
try {
|
|
6356
|
-
const content = await
|
|
6816
|
+
const content = await fs25.readFile(filePath, "utf-8");
|
|
6357
6817
|
const raw = YAML6.parse(content);
|
|
6358
6818
|
validateCliPreferenceTools(raw, filePath, issues);
|
|
6359
6819
|
const parsed = reviewYamlSchema.parse(raw);
|
|
@@ -6484,7 +6944,7 @@ async function validateConfig(rootDir = process.cwd()) {
|
|
|
6484
6944
|
for (const [reviewName, reviewConfig] of Object.entries(reviews)) {
|
|
6485
6945
|
const pref = reviewConfig.cli_preference;
|
|
6486
6946
|
if (pref && Array.isArray(pref)) {
|
|
6487
|
-
const reviewFile = reviewSourceFiles[reviewName] ||
|
|
6947
|
+
const reviewFile = reviewSourceFiles[reviewName] || path23.join(reviewsPath, `${reviewName}.md`);
|
|
6488
6948
|
for (let i = 0;i < pref.length; i++) {
|
|
6489
6949
|
const tool = pref[i];
|
|
6490
6950
|
if (!allowedTools.has(tool)) {
|
|
@@ -6610,17 +7070,17 @@ function handleReviewValidationError(error, filePath, issues) {
|
|
|
6610
7070
|
}
|
|
6611
7071
|
}
|
|
6612
7072
|
}
|
|
6613
|
-
async function fileExists5(
|
|
7073
|
+
async function fileExists5(path24) {
|
|
6614
7074
|
try {
|
|
6615
|
-
const stat = await
|
|
7075
|
+
const stat = await fs25.stat(path24);
|
|
6616
7076
|
return stat.isFile();
|
|
6617
7077
|
} catch {
|
|
6618
7078
|
return false;
|
|
6619
7079
|
}
|
|
6620
7080
|
}
|
|
6621
|
-
async function dirExists2(
|
|
7081
|
+
async function dirExists2(path24) {
|
|
6622
7082
|
try {
|
|
6623
|
-
const stat = await
|
|
7083
|
+
const stat = await fs25.stat(path24);
|
|
6624
7084
|
return stat.isDirectory();
|
|
6625
7085
|
} catch {
|
|
6626
7086
|
return false;
|
|
@@ -6636,7 +7096,7 @@ function registerHealthCommand(program) {
|
|
|
6636
7096
|
console.log(chalk7.yellow(" No config files found"));
|
|
6637
7097
|
} else {
|
|
6638
7098
|
for (const file of validationResult.filesChecked) {
|
|
6639
|
-
const relativePath =
|
|
7099
|
+
const relativePath = path24.relative(process.cwd(), file);
|
|
6640
7100
|
console.log(chalk7.dim(` ${relativePath}`));
|
|
6641
7101
|
}
|
|
6642
7102
|
if (validationResult.valid && validationResult.issues.length === 0) {
|
|
@@ -6644,7 +7104,7 @@ function registerHealthCommand(program) {
|
|
|
6644
7104
|
} else {
|
|
6645
7105
|
const issuesByFile = new Map;
|
|
6646
7106
|
for (const issue of validationResult.issues) {
|
|
6647
|
-
const relativeFile =
|
|
7107
|
+
const relativeFile = path24.relative(process.cwd(), issue.file);
|
|
6648
7108
|
if (!issuesByFile.has(relativeFile)) {
|
|
6649
7109
|
issuesByFile.set(relativeFile, []);
|
|
6650
7110
|
}
|
|
@@ -6763,16 +7223,15 @@ function registerHelpCommand(program) {
|
|
|
6763
7223
|
});
|
|
6764
7224
|
}
|
|
6765
7225
|
// src/commands/init.ts
|
|
6766
|
-
import
|
|
6767
|
-
import
|
|
6768
|
-
import path25 from "node:path";
|
|
7226
|
+
import fs27 from "node:fs/promises";
|
|
7227
|
+
import path26 from "node:path";
|
|
6769
7228
|
import { fileURLToPath } from "node:url";
|
|
6770
7229
|
import chalk10 from "chalk";
|
|
6771
7230
|
|
|
6772
7231
|
// src/commands/init-checksums.ts
|
|
6773
7232
|
import { createHash } from "node:crypto";
|
|
6774
|
-
import
|
|
6775
|
-
import
|
|
7233
|
+
import fs26 from "node:fs/promises";
|
|
7234
|
+
import path25 from "node:path";
|
|
6776
7235
|
async function computeSkillChecksum(skillDir) {
|
|
6777
7236
|
const files = await collectFiles(skillDir);
|
|
6778
7237
|
files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
@@ -6783,26 +7242,6 @@ async function computeSkillChecksum(skillDir) {
|
|
|
6783
7242
|
}
|
|
6784
7243
|
return hash.digest("hex");
|
|
6785
7244
|
}
|
|
6786
|
-
function computeExpectedSkillChecksum(content, references) {
|
|
6787
|
-
const entries = [
|
|
6788
|
-
{ relativePath: "SKILL.md", content }
|
|
6789
|
-
];
|
|
6790
|
-
if (references) {
|
|
6791
|
-
for (const [name, refContent] of Object.entries(references)) {
|
|
6792
|
-
entries.push({
|
|
6793
|
-
relativePath: path24.join("references", name),
|
|
6794
|
-
content: refContent
|
|
6795
|
-
});
|
|
6796
|
-
}
|
|
6797
|
-
}
|
|
6798
|
-
entries.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
6799
|
-
const hash = createHash("sha256");
|
|
6800
|
-
for (const entry of entries) {
|
|
6801
|
-
hash.update(entry.relativePath);
|
|
6802
|
-
hash.update(entry.content);
|
|
6803
|
-
}
|
|
6804
|
-
return hash.digest("hex");
|
|
6805
|
-
}
|
|
6806
7245
|
function computeHookChecksum(entries) {
|
|
6807
7246
|
const gauntletEntries = entries.filter((entry) => isGauntletHookEntry(entry));
|
|
6808
7247
|
const hash = createHash("sha256");
|
|
@@ -6825,14 +7264,14 @@ function isGauntletHookEntry(entry) {
|
|
|
6825
7264
|
async function collectFiles(dir, baseDir) {
|
|
6826
7265
|
const base = baseDir ?? dir;
|
|
6827
7266
|
const results = [];
|
|
6828
|
-
const entries = await
|
|
7267
|
+
const entries = await fs26.readdir(dir, { withFileTypes: true });
|
|
6829
7268
|
for (const entry of entries) {
|
|
6830
|
-
const fullPath =
|
|
7269
|
+
const fullPath = path25.join(dir, entry.name);
|
|
6831
7270
|
if (entry.isDirectory()) {
|
|
6832
7271
|
results.push(...await collectFiles(fullPath, base));
|
|
6833
7272
|
} else if (entry.isFile()) {
|
|
6834
|
-
const content = await
|
|
6835
|
-
results.push({ relativePath:
|
|
7273
|
+
const content = await fs26.readFile(fullPath, "utf-8");
|
|
7274
|
+
results.push({ relativePath: path25.relative(base, fullPath), content });
|
|
6836
7275
|
}
|
|
6837
7276
|
}
|
|
6838
7277
|
return results;
|
|
@@ -6896,11 +7335,26 @@ async function promptHookOverwrite(hookFile, skipPrompts) {
|
|
|
6896
7335
|
}
|
|
6897
7336
|
|
|
6898
7337
|
// src/commands/init.ts
|
|
6899
|
-
var __dirname2 =
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
7338
|
+
var __dirname2 = path26.dirname(fileURLToPath(import.meta.url));
|
|
7339
|
+
var SKILLS_SOURCE_DIR = path26.join(__dirname2, "..", "..", "skills");
|
|
7340
|
+
var SKILL_ACTIONS = [
|
|
7341
|
+
"run",
|
|
7342
|
+
"check",
|
|
7343
|
+
"push-pr",
|
|
7344
|
+
"fix-pr",
|
|
7345
|
+
"status",
|
|
7346
|
+
"help",
|
|
7347
|
+
"setup"
|
|
7348
|
+
];
|
|
7349
|
+
var SKILL_DESCRIPTIONS = {
|
|
7350
|
+
run: "Run the verification suite",
|
|
7351
|
+
check: "Run checks only (no reviews)",
|
|
7352
|
+
"push-pr": "Commit, push, and create a PR",
|
|
7353
|
+
"fix-pr": "Fix PR review comments and CI failures",
|
|
7354
|
+
status: "Show gauntlet status",
|
|
7355
|
+
help: "Diagnose and explain gauntlet behavior",
|
|
7356
|
+
setup: "Configure checks and reviews interactively"
|
|
7357
|
+
};
|
|
6904
7358
|
var CLI_PREFERENCE_ORDER = [
|
|
6905
7359
|
"codex",
|
|
6906
7360
|
"claude",
|
|
@@ -6911,156 +7365,19 @@ var CLI_PREFERENCE_ORDER = [
|
|
|
6911
7365
|
var ADAPTER_CONFIG = {
|
|
6912
7366
|
claude: { allow_tool_use: false, thinking_budget: "high" },
|
|
6913
7367
|
codex: { allow_tool_use: false, thinking_budget: "low" },
|
|
6914
|
-
gemini: { allow_tool_use: false, thinking_budget: "low" }
|
|
6915
|
-
}
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
const command = isRun ? "agent-gauntlet run" : "agent-gauntlet check";
|
|
6921
|
-
const heading = isRun ? "Execute the autonomous verification suite." : "Run the gauntlet checks only — no AI reviews.";
|
|
6922
|
-
const disableModelInvocation = isRun ? "false" : "true";
|
|
6923
|
-
const frontmatter = `---
|
|
6924
|
-
name: gauntlet-${name}
|
|
6925
|
-
description: >-
|
|
6926
|
-
${description}
|
|
6927
|
-
disable-model-invocation: ${disableModelInvocation}
|
|
6928
|
-
allowed-tools: Bash
|
|
6929
|
-
---`;
|
|
6930
|
-
const steps = [
|
|
6931
|
-
`1. Run \`agent-gauntlet clean\` to archive any previous log files`,
|
|
6932
|
-
`2. Run \`${command}\``
|
|
6933
|
-
];
|
|
6934
|
-
if (isRun) {
|
|
6935
|
-
steps.push(`3. If it fails:
|
|
6936
|
-
- Identify the failed gates from the console output.
|
|
6937
|
-
- For CHECK failures: Read the \`.log\` file path provided in the output. If the log contains a \`--- Fix Instructions ---\` section, follow those instructions to fix the issue. If it contains a \`--- Fix Skill: <name> ---\` section, invoke that skill.
|
|
6938
|
-
- For REVIEW failures: Read the \`.json\` file path provided in the "Review: <path>" output.
|
|
6939
|
-
4. Address the violations:
|
|
6940
|
-
- For REVIEW violations: You MUST update the \`"status"\` and \`"result"\` fields in the provided \`.json\` file for EACH violation.
|
|
6941
|
-
- Set \`"status": "fixed"\` and add a brief description to \`"result"\` for issues you fix.
|
|
6942
|
-
- Set \`"status": "skipped"\` and add a brief reason to \`"result"\` for issues you skip (based on the trust level).
|
|
6943
|
-
- Do NOT modify any other attributes (file, line, issue, priority) in the JSON file.
|
|
6944
|
-
- Apply the trust level above when deciding whether to act on AI reviewer feedback.
|
|
6945
|
-
5. Run \`${command}\` again to verify your fixes. Do NOT run \`agent-gauntlet clean\` between retries. The tool detects existing logs and automatically switches to verification mode.
|
|
6946
|
-
6. Repeat steps 3-5 until one of the following termination conditions is met:
|
|
6947
|
-
- "Status: Passed" appears in the output (logs are automatically archived)
|
|
6948
|
-
- "Status: Passed with warnings" appears in the output (remaining issues were skipped)
|
|
6949
|
-
- "Status: Retry limit exceeded" appears in the output -> Run \`agent-gauntlet clean\` to archive logs for the session record. Do NOT retry after cleaning.
|
|
6950
|
-
7. Provide a summary of the session:
|
|
6951
|
-
- Issues Fixed: (list key fixes)
|
|
6952
|
-
- Issues Skipped: (list skipped items and reasons)
|
|
6953
|
-
- Outstanding Failures: (if retry limit exceeded, list unverified fixes and remaining issues)`);
|
|
6954
|
-
} else {
|
|
6955
|
-
steps.push(`3. If any checks fail:
|
|
6956
|
-
- Read the \`.log\` file path provided in the output for each failed check. If the log contains a \`--- Fix Instructions ---\` section, follow those instructions. If it contains a \`--- Fix Skill: <name> ---\` section, invoke that skill.
|
|
6957
|
-
- Fix the issues found.
|
|
6958
|
-
4. Run \`${command}\` again to verify your fixes. Do NOT run \`agent-gauntlet clean\` between retries.
|
|
6959
|
-
5. Repeat steps 3-4 until all checks pass or you've made 3 attempts.
|
|
6960
|
-
6. Provide a summary of the session:
|
|
6961
|
-
- Checks Passed: (list)
|
|
6962
|
-
- Checks Failed: (list with brief reason)
|
|
6963
|
-
- Fixes Applied: (list key fixes)`);
|
|
6964
|
-
}
|
|
6965
|
-
if (isRun) {
|
|
6966
|
-
return `${frontmatter}
|
|
6967
|
-
<!--
|
|
6968
|
-
REVIEW TRUST LEVEL
|
|
6969
|
-
Controls how aggressively the agent acts on AI reviewer feedback.
|
|
6970
|
-
Change the trust_level value below to one of: high, medium, low
|
|
6971
|
-
|
|
6972
|
-
- high: Fix all issues unless you strongly disagree or have low confidence the human wants the change.
|
|
6973
|
-
- medium: Fix issues you reasonably agree with or believe the human wants fixed. (DEFAULT)
|
|
6974
|
-
- low: Fix only issues you strongly agree with or are confident the human wants fixed.
|
|
6975
|
-
-->
|
|
6976
|
-
<!-- trust_level: medium -->
|
|
6977
|
-
|
|
6978
|
-
# /gauntlet-${name}
|
|
6979
|
-
${heading}
|
|
6980
|
-
|
|
6981
|
-
**Review trust level: medium** — Fix issues you reasonably agree with or believe the human wants to be fixed. Skip issues that are purely stylistic, subjective, or that you believe the human would not want changed. When you skip an issue, briefly state what was skipped and why.
|
|
6982
|
-
|
|
6983
|
-
${steps.join(`
|
|
6984
|
-
`)}
|
|
6985
|
-
`;
|
|
6986
|
-
}
|
|
6987
|
-
return `${frontmatter}
|
|
6988
|
-
|
|
6989
|
-
# /gauntlet-${name}
|
|
6990
|
-
${heading}
|
|
6991
|
-
|
|
6992
|
-
${steps.join(`
|
|
6993
|
-
`)}
|
|
6994
|
-
`;
|
|
6995
|
-
}
|
|
6996
|
-
var GAUNTLET_RUN_SKILL_CONTENT = buildGauntletSkillContent("run");
|
|
6997
|
-
var GAUNTLET_CHECK_SKILL_CONTENT = buildGauntletSkillContent("check");
|
|
6998
|
-
var PUSH_PR_SKILL_CONTENT = readSkillTemplate("push-pr.md");
|
|
6999
|
-
var FIX_PR_SKILL_CONTENT = readSkillTemplate("fix-pr.md");
|
|
7000
|
-
var GAUNTLET_STATUS_SKILL_CONTENT = readSkillTemplate("status.md");
|
|
7001
|
-
var HELP_SKILL_BUNDLE = {
|
|
7002
|
-
content: readSkillTemplate("help-skill.md"),
|
|
7003
|
-
references: {
|
|
7004
|
-
"stop-hook-troubleshooting.md": readSkillTemplate("help-ref-stop-hook-troubleshooting.md"),
|
|
7005
|
-
"config-troubleshooting.md": readSkillTemplate("help-ref-config-troubleshooting.md"),
|
|
7006
|
-
"gate-troubleshooting.md": readSkillTemplate("help-ref-gate-troubleshooting.md"),
|
|
7007
|
-
"lock-troubleshooting.md": readSkillTemplate("help-ref-lock-troubleshooting.md"),
|
|
7008
|
-
"adapter-troubleshooting.md": readSkillTemplate("help-ref-adapter-troubleshooting.md"),
|
|
7009
|
-
"ci-pr-troubleshooting.md": readSkillTemplate("help-ref-ci-pr-troubleshooting.md")
|
|
7368
|
+
gemini: { allow_tool_use: false, thinking_budget: "low" },
|
|
7369
|
+
cursor: { allow_tool_use: false, thinking_budget: "low", model: "codex" },
|
|
7370
|
+
"github-copilot": {
|
|
7371
|
+
allow_tool_use: false,
|
|
7372
|
+
thinking_budget: "low",
|
|
7373
|
+
model: "codex"
|
|
7010
7374
|
}
|
|
7011
7375
|
};
|
|
7012
|
-
var SETUP_SKILL_CONTENT = readSkillTemplate("setup-skill.md");
|
|
7013
|
-
var CHECK_CATALOG_REFERENCE = readSkillTemplate("check-catalog.md");
|
|
7014
|
-
var PROJECT_STRUCTURE_REFERENCE = readSkillTemplate("setup-ref-project-structure.md");
|
|
7015
|
-
var SKILL_DEFINITIONS = [
|
|
7016
|
-
{
|
|
7017
|
-
action: "run",
|
|
7018
|
-
content: GAUNTLET_RUN_SKILL_CONTENT,
|
|
7019
|
-
description: "Run the verification suite"
|
|
7020
|
-
},
|
|
7021
|
-
{
|
|
7022
|
-
action: "check",
|
|
7023
|
-
content: GAUNTLET_CHECK_SKILL_CONTENT,
|
|
7024
|
-
description: "Run a single check gate"
|
|
7025
|
-
},
|
|
7026
|
-
{
|
|
7027
|
-
action: "push-pr",
|
|
7028
|
-
content: PUSH_PR_SKILL_CONTENT,
|
|
7029
|
-
description: "Commit, push, and create a PR"
|
|
7030
|
-
},
|
|
7031
|
-
{
|
|
7032
|
-
action: "fix-pr",
|
|
7033
|
-
content: FIX_PR_SKILL_CONTENT,
|
|
7034
|
-
description: "Fix PR review comments and CI failures"
|
|
7035
|
-
},
|
|
7036
|
-
{
|
|
7037
|
-
action: "status",
|
|
7038
|
-
content: GAUNTLET_STATUS_SKILL_CONTENT,
|
|
7039
|
-
description: "Show gauntlet status"
|
|
7040
|
-
},
|
|
7041
|
-
{
|
|
7042
|
-
action: "help",
|
|
7043
|
-
content: HELP_SKILL_BUNDLE.content,
|
|
7044
|
-
references: HELP_SKILL_BUNDLE.references,
|
|
7045
|
-
skillsOnly: true,
|
|
7046
|
-
description: "Diagnose and explain gauntlet behavior"
|
|
7047
|
-
},
|
|
7048
|
-
{
|
|
7049
|
-
action: "setup",
|
|
7050
|
-
content: SETUP_SKILL_CONTENT,
|
|
7051
|
-
references: {
|
|
7052
|
-
"check-catalog.md": CHECK_CATALOG_REFERENCE,
|
|
7053
|
-
"project-structure.md": PROJECT_STRUCTURE_REFERENCE
|
|
7054
|
-
},
|
|
7055
|
-
skillsOnly: true,
|
|
7056
|
-
description: "Configure checks and reviews interactively"
|
|
7057
|
-
}
|
|
7058
|
-
];
|
|
7059
7376
|
var NATIVE_CLIS = new Set(["claude", "cursor"]);
|
|
7060
7377
|
function registerInitCommand(program) {
|
|
7061
7378
|
program.command("init").description("Initialize .gauntlet configuration").option("-y, --yes", "Skip prompts and use defaults").action(async (options) => {
|
|
7062
7379
|
const projectRoot = process.cwd();
|
|
7063
|
-
const targetDir =
|
|
7380
|
+
const targetDir = path26.join(projectRoot, ".gauntlet");
|
|
7064
7381
|
const skipPrompts = options.yes ?? false;
|
|
7065
7382
|
console.log("Detecting available CLI agents...");
|
|
7066
7383
|
const availableAdapters = await detectAvailableCLIs();
|
|
@@ -7108,47 +7425,50 @@ async function scaffoldGauntletDir(_projectRoot, targetDir, reviewCLINames, numR
|
|
|
7108
7425
|
console.log(chalk10.dim(".gauntlet/ already exists, skipping scaffolding"));
|
|
7109
7426
|
return;
|
|
7110
7427
|
}
|
|
7111
|
-
await
|
|
7112
|
-
await
|
|
7113
|
-
await
|
|
7428
|
+
await fs27.mkdir(targetDir);
|
|
7429
|
+
await fs27.mkdir(path26.join(targetDir, "checks"));
|
|
7430
|
+
await fs27.mkdir(path26.join(targetDir, "reviews"));
|
|
7114
7431
|
await writeConfigYml(targetDir, reviewCLINames);
|
|
7115
|
-
await
|
|
7432
|
+
await fs27.writeFile(path26.join(targetDir, "reviews", "code-quality.yml"), `builtin: code-quality
|
|
7116
7433
|
num_reviews: ${numReviews}
|
|
7117
7434
|
`);
|
|
7118
7435
|
console.log(chalk10.green("Created .gauntlet/reviews/code-quality.yml"));
|
|
7119
7436
|
}
|
|
7120
|
-
async function
|
|
7121
|
-
await
|
|
7122
|
-
await
|
|
7123
|
-
|
|
7124
|
-
const
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
await
|
|
7437
|
+
async function copyDirRecursive(opts) {
|
|
7438
|
+
await fs27.mkdir(opts.dest, { recursive: true });
|
|
7439
|
+
const entries = await fs27.readdir(opts.src, { withFileTypes: true });
|
|
7440
|
+
for (const entry of entries) {
|
|
7441
|
+
const srcPath = path26.join(opts.src, entry.name);
|
|
7442
|
+
const destPath = path26.join(opts.dest, entry.name);
|
|
7443
|
+
if (entry.isDirectory()) {
|
|
7444
|
+
await copyDirRecursive({ src: srcPath, dest: destPath });
|
|
7445
|
+
} else {
|
|
7446
|
+
await fs27.copyFile(srcPath, destPath);
|
|
7128
7447
|
}
|
|
7129
7448
|
}
|
|
7130
7449
|
}
|
|
7131
7450
|
async function installSkillsWithChecksums(projectRoot, skipPrompts) {
|
|
7132
|
-
const skillsDir =
|
|
7133
|
-
for (const
|
|
7134
|
-
const
|
|
7135
|
-
const
|
|
7136
|
-
const
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7451
|
+
const skillsDir = path26.join(projectRoot, ".claude", "skills");
|
|
7452
|
+
for (const action of SKILL_ACTIONS) {
|
|
7453
|
+
const dirName = `gauntlet-${action}`;
|
|
7454
|
+
const sourceDir = path26.join(SKILLS_SOURCE_DIR, dirName);
|
|
7455
|
+
const targetDir = path26.join(skillsDir, dirName);
|
|
7456
|
+
const relativeDir = `${path26.relative(projectRoot, targetDir)}/`;
|
|
7457
|
+
if (!await exists(targetDir)) {
|
|
7458
|
+
await copyDirRecursive({ src: sourceDir, dest: targetDir });
|
|
7459
|
+
console.log(chalk10.green(`Created ${relativeDir}`));
|
|
7140
7460
|
continue;
|
|
7141
7461
|
}
|
|
7142
|
-
const
|
|
7143
|
-
const
|
|
7144
|
-
if (
|
|
7462
|
+
const sourceChecksum = await computeSkillChecksum(sourceDir);
|
|
7463
|
+
const targetChecksum = await computeSkillChecksum(targetDir);
|
|
7464
|
+
if (sourceChecksum === targetChecksum)
|
|
7145
7465
|
continue;
|
|
7146
|
-
const shouldOverwrite = await promptFileOverwrite(
|
|
7466
|
+
const shouldOverwrite = await promptFileOverwrite(dirName, skipPrompts);
|
|
7147
7467
|
if (!shouldOverwrite)
|
|
7148
7468
|
continue;
|
|
7149
|
-
await
|
|
7150
|
-
await
|
|
7151
|
-
console.log(chalk10.green(`Updated ${
|
|
7469
|
+
await fs27.rm(targetDir, { recursive: true, force: true });
|
|
7470
|
+
await copyDirRecursive({ src: sourceDir, dest: targetDir });
|
|
7471
|
+
console.log(chalk10.green(`Updated ${relativeDir}`));
|
|
7152
7472
|
}
|
|
7153
7473
|
}
|
|
7154
7474
|
async function installHookWithChecksums(target, skipPrompts) {
|
|
@@ -7156,7 +7476,7 @@ async function installHookWithChecksums(target, skipPrompts) {
|
|
|
7156
7476
|
let existingConfig = {};
|
|
7157
7477
|
if (await exists(spec.config.filePath)) {
|
|
7158
7478
|
try {
|
|
7159
|
-
existingConfig = JSON.parse(await
|
|
7479
|
+
existingConfig = JSON.parse(await fs27.readFile(spec.config.filePath, "utf-8"));
|
|
7160
7480
|
} catch {
|
|
7161
7481
|
existingConfig = {};
|
|
7162
7482
|
}
|
|
@@ -7193,14 +7513,13 @@ async function installHookWithChecksums(target, skipPrompts) {
|
|
|
7193
7513
|
[spec.config.hookKey]: newEntries
|
|
7194
7514
|
}
|
|
7195
7515
|
};
|
|
7196
|
-
await
|
|
7197
|
-
await
|
|
7516
|
+
await fs27.mkdir(path26.dirname(spec.config.filePath), { recursive: true });
|
|
7517
|
+
await fs27.writeFile(spec.config.filePath, `${JSON.stringify(merged, null, 2)}
|
|
7198
7518
|
`);
|
|
7199
7519
|
console.log(chalk10.green(spec.installedMsg));
|
|
7200
7520
|
}
|
|
7201
7521
|
async function installExternalFiles(projectRoot, devAdapters, skipPrompts) {
|
|
7202
7522
|
await installSkillsWithChecksums(projectRoot, skipPrompts);
|
|
7203
|
-
await copyStatusScript(path25.join(projectRoot, ".gauntlet"));
|
|
7204
7523
|
for (const adapter of devAdapters) {
|
|
7205
7524
|
if (!adapter.supportsHooks())
|
|
7206
7525
|
continue;
|
|
@@ -7228,8 +7547,8 @@ function printPostInitInstructions(devCLINames) {
|
|
|
7228
7547
|
console.log(chalk10.bold("To complete setup, reference the setup skill in your CLI: @.claude/skills/gauntlet-setup/SKILL.md. This will guide you through configuring the static checks (unit tests, linters, etc.) that Agent Gauntlet will run."));
|
|
7229
7548
|
console.log();
|
|
7230
7549
|
console.log("Available skills:");
|
|
7231
|
-
for (const
|
|
7232
|
-
console.log(` @.claude/skills/gauntlet-${
|
|
7550
|
+
for (const action of SKILL_ACTIONS) {
|
|
7551
|
+
console.log(` @.claude/skills/gauntlet-${action}/SKILL.md — ${SKILL_DESCRIPTIONS[action]}`);
|
|
7233
7552
|
}
|
|
7234
7553
|
}
|
|
7235
7554
|
}
|
|
@@ -7294,14 +7613,14 @@ entry_points: []
|
|
|
7294
7613
|
# enabled: true
|
|
7295
7614
|
# format: text # Options: text, json
|
|
7296
7615
|
`;
|
|
7297
|
-
await
|
|
7616
|
+
await fs27.writeFile(path26.join(targetDir, "config.yml"), content);
|
|
7298
7617
|
console.log(chalk10.green("Created .gauntlet/config.yml"));
|
|
7299
7618
|
}
|
|
7300
7619
|
async function addToGitignore(projectRoot, entry) {
|
|
7301
|
-
const gitignorePath =
|
|
7620
|
+
const gitignorePath = path26.join(projectRoot, ".gitignore");
|
|
7302
7621
|
let content = "";
|
|
7303
7622
|
if (await exists(gitignorePath)) {
|
|
7304
|
-
content = await
|
|
7623
|
+
content = await fs27.readFile(gitignorePath, "utf-8");
|
|
7305
7624
|
const lines = content.split(`
|
|
7306
7625
|
`).map((l) => l.trim());
|
|
7307
7626
|
if (lines.includes(entry)) {
|
|
@@ -7311,7 +7630,7 @@ async function addToGitignore(projectRoot, entry) {
|
|
|
7311
7630
|
const suffix = content.length > 0 && !content.endsWith(`
|
|
7312
7631
|
`) ? `
|
|
7313
7632
|
` : "";
|
|
7314
|
-
await
|
|
7633
|
+
await fs27.appendFile(gitignorePath, `${suffix}${entry}
|
|
7315
7634
|
`);
|
|
7316
7635
|
console.log(chalk10.green(`Added ${entry} to .gitignore`));
|
|
7317
7636
|
}
|
|
@@ -7346,9 +7665,14 @@ function buildAdapterSettingsBlock(adapterNames) {
|
|
|
7346
7665
|
return "";
|
|
7347
7666
|
const lines = items.map((name) => {
|
|
7348
7667
|
const c = ADAPTER_CONFIG[name];
|
|
7349
|
-
|
|
7668
|
+
let block = ` ${name}:
|
|
7350
7669
|
allow_tool_use: ${c?.allow_tool_use}
|
|
7351
7670
|
thinking_budget: ${c?.thinking_budget}`;
|
|
7671
|
+
if (c?.model) {
|
|
7672
|
+
block += `
|
|
7673
|
+
model: ${c.model}`;
|
|
7674
|
+
}
|
|
7675
|
+
return block;
|
|
7352
7676
|
});
|
|
7353
7677
|
return ` # Recommended settings (see docs/eval-results.md)
|
|
7354
7678
|
adapters:
|
|
@@ -7370,20 +7694,6 @@ async function detectAvailableCLIs() {
|
|
|
7370
7694
|
}
|
|
7371
7695
|
return available;
|
|
7372
7696
|
}
|
|
7373
|
-
async function copyStatusScript(targetDir) {
|
|
7374
|
-
const statusScriptDir = path25.join(targetDir, "scripts");
|
|
7375
|
-
const statusScriptPath = path25.join(statusScriptDir, "status.ts");
|
|
7376
|
-
await fs26.mkdir(statusScriptDir, { recursive: true });
|
|
7377
|
-
if (await exists(statusScriptPath))
|
|
7378
|
-
return;
|
|
7379
|
-
const bundledScript = path25.join(path25.dirname(new URL(import.meta.url).pathname), "..", "scripts", "status.ts");
|
|
7380
|
-
if (await exists(bundledScript)) {
|
|
7381
|
-
await fs26.copyFile(bundledScript, statusScriptPath);
|
|
7382
|
-
console.log(chalk10.green("Created .gauntlet/scripts/status.ts"));
|
|
7383
|
-
} else {
|
|
7384
|
-
console.log(chalk10.yellow("Warning: bundled status script not found; /gauntlet-status may fail."));
|
|
7385
|
-
}
|
|
7386
|
-
}
|
|
7387
7697
|
function hookHasCommand(entries, cmd) {
|
|
7388
7698
|
return entries.some((hook) => {
|
|
7389
7699
|
if (hook.command === cmd)
|
|
@@ -7401,11 +7711,11 @@ async function mergeHookConfig(opts) {
|
|
|
7401
7711
|
wrapInHooksArray,
|
|
7402
7712
|
baseConfig
|
|
7403
7713
|
} = opts;
|
|
7404
|
-
await
|
|
7714
|
+
await fs27.mkdir(path26.dirname(filePath), { recursive: true });
|
|
7405
7715
|
let existing = {};
|
|
7406
7716
|
if (await exists(filePath)) {
|
|
7407
7717
|
try {
|
|
7408
|
-
existing = JSON.parse(await
|
|
7718
|
+
existing = JSON.parse(await fs27.readFile(filePath, "utf-8"));
|
|
7409
7719
|
} catch {
|
|
7410
7720
|
existing = {};
|
|
7411
7721
|
}
|
|
@@ -7425,7 +7735,7 @@ async function mergeHookConfig(opts) {
|
|
|
7425
7735
|
[hookKey]: newEntries
|
|
7426
7736
|
}
|
|
7427
7737
|
};
|
|
7428
|
-
await
|
|
7738
|
+
await fs27.writeFile(filePath, `${JSON.stringify(merged, null, 2)}
|
|
7429
7739
|
`);
|
|
7430
7740
|
return true;
|
|
7431
7741
|
}
|
|
@@ -7500,7 +7810,7 @@ function buildHookSpec(target) {
|
|
|
7500
7810
|
const purpose = isStop ? "gauntlet will run automatically when agent stops" : "agent will be primed with gauntlet instructions at session start";
|
|
7501
7811
|
return {
|
|
7502
7812
|
config: {
|
|
7503
|
-
filePath:
|
|
7813
|
+
filePath: path26.join(projectRoot, cfg.dir, cfg.file),
|
|
7504
7814
|
hookKey: cfg.hookKey,
|
|
7505
7815
|
hookEntry: cfg.entry,
|
|
7506
7816
|
deduplicateCmd: cfg.cmd,
|
|
@@ -7679,8 +7989,8 @@ function registerReviewCommand(program) {
|
|
|
7679
7989
|
});
|
|
7680
7990
|
}
|
|
7681
7991
|
// src/core/run-executor.ts
|
|
7682
|
-
import
|
|
7683
|
-
import
|
|
7992
|
+
import fs28 from "node:fs/promises";
|
|
7993
|
+
import path27 from "node:path";
|
|
7684
7994
|
|
|
7685
7995
|
// src/core/diff-stats.ts
|
|
7686
7996
|
import { execFile as execFile2 } from "node:child_process";
|
|
@@ -7989,25 +8299,25 @@ function isProcessAlive(pid) {
|
|
|
7989
8299
|
}
|
|
7990
8300
|
}
|
|
7991
8301
|
async function tryAcquireLock(logDir) {
|
|
7992
|
-
await
|
|
7993
|
-
const lockPath =
|
|
8302
|
+
await fs28.mkdir(logDir, { recursive: true });
|
|
8303
|
+
const lockPath = path27.resolve(logDir, LOCK_FILENAME2);
|
|
7994
8304
|
try {
|
|
7995
|
-
await
|
|
8305
|
+
await fs28.writeFile(lockPath, String(process.pid), { flag: "wx" });
|
|
7996
8306
|
return true;
|
|
7997
8307
|
} catch (err) {
|
|
7998
8308
|
if (typeof err === "object" && err !== null && "code" in err && err.code === "EEXIST") {
|
|
7999
8309
|
try {
|
|
8000
|
-
const lockContent = await
|
|
8310
|
+
const lockContent = await fs28.readFile(lockPath, "utf-8");
|
|
8001
8311
|
const lockPid = parseInt(lockContent.trim(), 10);
|
|
8002
|
-
const lockStat = await
|
|
8312
|
+
const lockStat = await fs28.stat(lockPath);
|
|
8003
8313
|
const lockAgeMs = Date.now() - lockStat.mtimeMs;
|
|
8004
8314
|
const pidValid = !Number.isNaN(lockPid);
|
|
8005
8315
|
const pidDead = pidValid && !isProcessAlive(lockPid);
|
|
8006
8316
|
const lockStale = !pidValid && lockAgeMs > STALE_LOCK_MS;
|
|
8007
8317
|
if (pidDead || lockStale) {
|
|
8008
|
-
await
|
|
8318
|
+
await fs28.rm(lockPath, { force: true });
|
|
8009
8319
|
try {
|
|
8010
|
-
await
|
|
8320
|
+
await fs28.writeFile(lockPath, String(process.pid), {
|
|
8011
8321
|
flag: "wx"
|
|
8012
8322
|
});
|
|
8013
8323
|
return true;
|
|
@@ -8023,7 +8333,7 @@ async function tryAcquireLock(logDir) {
|
|
|
8023
8333
|
}
|
|
8024
8334
|
async function findLatestConsoleLog(logDir) {
|
|
8025
8335
|
try {
|
|
8026
|
-
const files = await
|
|
8336
|
+
const files = await fs28.readdir(logDir);
|
|
8027
8337
|
let maxNum = -1;
|
|
8028
8338
|
let latestFile = null;
|
|
8029
8339
|
for (const file of files) {
|
|
@@ -8039,7 +8349,7 @@ async function findLatestConsoleLog(logDir) {
|
|
|
8039
8349
|
}
|
|
8040
8350
|
}
|
|
8041
8351
|
}
|
|
8042
|
-
return latestFile ?
|
|
8352
|
+
return latestFile ? path27.join(logDir, latestFile) : null;
|
|
8043
8353
|
} catch {
|
|
8044
8354
|
return null;
|
|
8045
8355
|
}
|
|
@@ -8090,7 +8400,7 @@ async function executeRun(options = {}) {
|
|
|
8090
8400
|
let lockAcquired = false;
|
|
8091
8401
|
let consoleLogHandle;
|
|
8092
8402
|
let loggerInitializedHere = false;
|
|
8093
|
-
const
|
|
8403
|
+
const log5 = getRunLogger();
|
|
8094
8404
|
try {
|
|
8095
8405
|
config = await loadConfig(cwd);
|
|
8096
8406
|
if (!isLoggerConfigured()) {
|
|
@@ -8115,7 +8425,7 @@ async function executeRun(options = {}) {
|
|
|
8115
8425
|
if (options.checkInterval) {
|
|
8116
8426
|
const stopHookConfig = resolveStopHookConfig(config.project.stop_hook, globalConfig);
|
|
8117
8427
|
if (!stopHookConfig.enabled) {
|
|
8118
|
-
|
|
8428
|
+
log5.debug("Stop hook is disabled via configuration, skipping");
|
|
8119
8429
|
if (loggerInitializedHere) {
|
|
8120
8430
|
await resetLogger();
|
|
8121
8431
|
}
|
|
@@ -8129,7 +8439,7 @@ async function executeRun(options = {}) {
|
|
|
8129
8439
|
const intervalMinutes = stopHookConfig.run_interval_minutes;
|
|
8130
8440
|
const shouldRun = await shouldRunBasedOnInterval(config.project.log_dir, intervalMinutes);
|
|
8131
8441
|
if (!shouldRun) {
|
|
8132
|
-
|
|
8442
|
+
log5.debug(`Run interval (${intervalMinutes} min) not elapsed, skipping`);
|
|
8133
8443
|
if (loggerInitializedHere) {
|
|
8134
8444
|
await resetLogger();
|
|
8135
8445
|
}
|
|
@@ -8144,7 +8454,7 @@ async function executeRun(options = {}) {
|
|
|
8144
8454
|
const effectiveBaseBranch = options.baseBranch || (process.env.GITHUB_BASE_REF && (process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true") ? process.env.GITHUB_BASE_REF : null) || config.project.base_branch;
|
|
8145
8455
|
const autoCleanResult = await shouldAutoClean(config.project.log_dir, effectiveBaseBranch);
|
|
8146
8456
|
if (autoCleanResult.clean) {
|
|
8147
|
-
|
|
8457
|
+
log5.debug(`Auto-cleaning logs (${autoCleanResult.reason})...`);
|
|
8148
8458
|
await debugLogger?.logClean("auto", autoCleanResult.reason || "unknown");
|
|
8149
8459
|
await performAutoClean(config.project.log_dir, autoCleanResult, config.project.max_previous_logs);
|
|
8150
8460
|
}
|
|
@@ -8169,7 +8479,7 @@ async function executeRun(options = {}) {
|
|
|
8169
8479
|
let changeOptions;
|
|
8170
8480
|
let passedSlotsMap;
|
|
8171
8481
|
if (isRerun) {
|
|
8172
|
-
|
|
8482
|
+
log5.debug("Existing logs detected — running in verification mode...");
|
|
8173
8483
|
const { failures: previousFailures, passedSlots } = await findPreviousFailures(config.project.log_dir, options.gate, true);
|
|
8174
8484
|
failuresMap = new Map;
|
|
8175
8485
|
for (const gateFailure of previousFailures) {
|
|
@@ -8183,7 +8493,7 @@ async function executeRun(options = {}) {
|
|
|
8183
8493
|
passedSlotsMap = passedSlots;
|
|
8184
8494
|
if (previousFailures.length > 0) {
|
|
8185
8495
|
const totalViolations = previousFailures.reduce((sum, gf) => sum + gf.adapterFailures.reduce((s, af) => s + af.violations.length, 0), 0);
|
|
8186
|
-
|
|
8496
|
+
log5.warn(`Found ${previousFailures.length} gate(s) with ${totalViolations} previous violation(s)`);
|
|
8187
8497
|
}
|
|
8188
8498
|
changeOptions = { uncommitted: true };
|
|
8189
8499
|
const executionState = await readExecutionState(config.project.log_dir);
|
|
@@ -8195,7 +8505,7 @@ async function executeRun(options = {}) {
|
|
|
8195
8505
|
if (executionState) {
|
|
8196
8506
|
const resolved = await resolveFixBase(executionState, effectiveBaseBranch);
|
|
8197
8507
|
if (resolved.warning) {
|
|
8198
|
-
|
|
8508
|
+
log5.warn(`Warning: ${resolved.warning}`);
|
|
8199
8509
|
}
|
|
8200
8510
|
if (resolved.fixBase) {
|
|
8201
8511
|
changeOptions = { fixBase: resolved.fixBase };
|
|
@@ -8215,10 +8525,30 @@ async function executeRun(options = {}) {
|
|
|
8215
8525
|
});
|
|
8216
8526
|
const expander = new EntryPointExpander;
|
|
8217
8527
|
const jobGen = new JobGenerator(config);
|
|
8218
|
-
|
|
8528
|
+
log5.debug("Detecting changes...");
|
|
8219
8529
|
const changes = await changeDetector.getChangedFiles();
|
|
8220
8530
|
if (changes.length === 0) {
|
|
8221
|
-
|
|
8531
|
+
if (isRerun && failuresMap && failuresMap.size === 0) {
|
|
8532
|
+
const hasSkipped = await hasSkippedViolationsInLogs({
|
|
8533
|
+
logDir: config.project.log_dir
|
|
8534
|
+
});
|
|
8535
|
+
const status2 = hasSkipped ? "passed_with_warnings" : "passed";
|
|
8536
|
+
if (status2 === "passed") {
|
|
8537
|
+
await debugLogger?.logClean("auto", "all_passed");
|
|
8538
|
+
await cleanLogs(config.project.log_dir, config.project.max_previous_logs);
|
|
8539
|
+
}
|
|
8540
|
+
log5.info(getStatusMessage2(status2));
|
|
8541
|
+
consoleLogHandle?.restore();
|
|
8542
|
+
if (loggerInitializedHere) {
|
|
8543
|
+
await resetLogger();
|
|
8544
|
+
}
|
|
8545
|
+
return {
|
|
8546
|
+
status: status2,
|
|
8547
|
+
message: getStatusMessage2(status2),
|
|
8548
|
+
gatesRun: 0
|
|
8549
|
+
};
|
|
8550
|
+
}
|
|
8551
|
+
log5.info("No changes detected.");
|
|
8222
8552
|
consoleLogHandle?.restore();
|
|
8223
8553
|
if (loggerInitializedHere) {
|
|
8224
8554
|
await resetLogger();
|
|
@@ -8229,14 +8559,14 @@ async function executeRun(options = {}) {
|
|
|
8229
8559
|
gatesRun: 0
|
|
8230
8560
|
};
|
|
8231
8561
|
}
|
|
8232
|
-
|
|
8562
|
+
log5.debug(`Found ${changes.length} changed files.`);
|
|
8233
8563
|
const entryPoints = await expander.expand(config.project.entry_points, changes);
|
|
8234
8564
|
let jobs = jobGen.generateJobs(entryPoints);
|
|
8235
8565
|
if (options.gate) {
|
|
8236
8566
|
jobs = jobs.filter((j) => j.name === options.gate);
|
|
8237
8567
|
}
|
|
8238
8568
|
if (jobs.length === 0) {
|
|
8239
|
-
|
|
8569
|
+
log5.warn("No applicable gates for these changes.");
|
|
8240
8570
|
consoleLogHandle?.restore();
|
|
8241
8571
|
if (loggerInitializedHere) {
|
|
8242
8572
|
await resetLogger();
|
|
@@ -8247,7 +8577,7 @@ async function executeRun(options = {}) {
|
|
|
8247
8577
|
gatesRun: 0
|
|
8248
8578
|
};
|
|
8249
8579
|
}
|
|
8250
|
-
|
|
8580
|
+
log5.debug(`Running ${jobs.length} gates...`);
|
|
8251
8581
|
const runMode = isRerun ? "verification" : "full";
|
|
8252
8582
|
const diffStats = await computeDiffStats(effectiveBaseBranch, changeOptions || {
|
|
8253
8583
|
commit: options.commit,
|
|
@@ -8320,8 +8650,8 @@ function registerRunCommand(program) {
|
|
|
8320
8650
|
});
|
|
8321
8651
|
}
|
|
8322
8652
|
// src/commands/start-hook.ts
|
|
8323
|
-
import
|
|
8324
|
-
import
|
|
8653
|
+
import fs29 from "node:fs/promises";
|
|
8654
|
+
import path28 from "node:path";
|
|
8325
8655
|
import YAML7 from "yaml";
|
|
8326
8656
|
var START_HOOK_MESSAGE = `<IMPORTANT>
|
|
8327
8657
|
This project uses Agent Gauntlet for automated quality verification.
|
|
@@ -8369,9 +8699,9 @@ function isValidConfig(content) {
|
|
|
8369
8699
|
}
|
|
8370
8700
|
function registerStartHookCommand(program) {
|
|
8371
8701
|
program.command("start-hook").description("Session start hook - primes agent with gauntlet verification instructions").option("--adapter <adapter>", "Output format: claude or cursor", "claude").action(async (options) => {
|
|
8372
|
-
const configPath =
|
|
8702
|
+
const configPath = path28.join(process.cwd(), ".gauntlet", "config.yml");
|
|
8373
8703
|
try {
|
|
8374
|
-
const content = await
|
|
8704
|
+
const content = await fs29.readFile(configPath, "utf-8");
|
|
8375
8705
|
if (!isValidConfig(content)) {
|
|
8376
8706
|
return;
|
|
8377
8707
|
}
|
|
@@ -8381,7 +8711,7 @@ function registerStartHookCommand(program) {
|
|
|
8381
8711
|
const adapter = options.adapter;
|
|
8382
8712
|
try {
|
|
8383
8713
|
const cwd = process.cwd();
|
|
8384
|
-
const logDir =
|
|
8714
|
+
const logDir = path28.join(cwd, await getLogDir2(cwd));
|
|
8385
8715
|
const globalConfig = await loadGlobalConfig();
|
|
8386
8716
|
const projectDebugLogConfig = await getDebugLogConfig(cwd);
|
|
8387
8717
|
const debugLogConfig = mergeDebugLogConfig(projectDebugLogConfig, globalConfig.debug_log);
|
|
@@ -8392,6 +8722,12 @@ function registerStartHookCommand(program) {
|
|
|
8392
8722
|
console.log(output);
|
|
8393
8723
|
});
|
|
8394
8724
|
}
|
|
8725
|
+
// src/commands/status.ts
|
|
8726
|
+
function registerStatusCommand(program) {
|
|
8727
|
+
program.command("status").description("Show a summary of the most recent gauntlet session").action(() => {
|
|
8728
|
+
main();
|
|
8729
|
+
});
|
|
8730
|
+
}
|
|
8395
8731
|
// src/commands/validate.ts
|
|
8396
8732
|
import chalk13 from "chalk";
|
|
8397
8733
|
function registerValidateCommand(program) {
|
|
@@ -8686,6 +9022,7 @@ registerHealthCommand(program);
|
|
|
8686
9022
|
registerInitCommand(program);
|
|
8687
9023
|
registerValidateCommand(program);
|
|
8688
9024
|
registerStartHookCommand(program);
|
|
9025
|
+
registerStatusCommand(program);
|
|
8689
9026
|
registerStopHookCommand(program);
|
|
8690
9027
|
registerWaitCICommand(program);
|
|
8691
9028
|
registerHelpCommand(program);
|
|
@@ -8694,4 +9031,4 @@ if (process.argv.length < 3) {
|
|
|
8694
9031
|
}
|
|
8695
9032
|
program.parse(process.argv);
|
|
8696
9033
|
|
|
8697
|
-
//# debugId=
|
|
9034
|
+
//# debugId=349F4DCD1CB50C0464756E2164756E21
|