agent-gauntlet 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -23
- package/dist/index.js +9226 -0
- package/dist/index.js.map +65 -0
- package/dist/scripts/status.js +280 -0
- package/dist/scripts/status.js.map +10 -0
- package/package.json +22 -8
- package/src/built-in-reviews/code-quality.md +0 -25
- package/src/built-in-reviews/index.ts +0 -28
- package/src/bun-plugins.d.ts +0 -4
- package/src/cli-adapters/claude.ts +0 -327
- package/src/cli-adapters/codex.ts +0 -290
- package/src/cli-adapters/cursor.ts +0 -128
- package/src/cli-adapters/gemini.ts +0 -510
- package/src/cli-adapters/github-copilot.ts +0 -141
- package/src/cli-adapters/index.ts +0 -250
- package/src/cli-adapters/thinking-budget.ts +0 -23
- package/src/commands/check.ts +0 -311
- package/src/commands/ci/index.ts +0 -15
- package/src/commands/ci/init.ts +0 -96
- package/src/commands/ci/list-jobs.ts +0 -90
- package/src/commands/clean.ts +0 -54
- package/src/commands/detect.ts +0 -173
- package/src/commands/health.ts +0 -169
- package/src/commands/help.ts +0 -34
- package/src/commands/index.ts +0 -13
- package/src/commands/init.ts +0 -1878
- package/src/commands/list.ts +0 -33
- package/src/commands/review.ts +0 -311
- package/src/commands/run.ts +0 -29
- package/src/commands/shared.ts +0 -267
- package/src/commands/stop-hook.ts +0 -567
- package/src/commands/validate.ts +0 -20
- package/src/commands/wait-ci.ts +0 -518
- package/src/config/ci-loader.ts +0 -33
- package/src/config/ci-schema.ts +0 -28
- package/src/config/global.ts +0 -87
- package/src/config/loader.ts +0 -301
- package/src/config/schema.ts +0 -165
- package/src/config/stop-hook-config.ts +0 -130
- package/src/config/types.ts +0 -65
- package/src/config/validator.ts +0 -592
- package/src/core/change-detector.ts +0 -137
- package/src/core/diff-stats.ts +0 -442
- package/src/core/entry-point.ts +0 -190
- package/src/core/job.ts +0 -96
- package/src/core/run-executor.ts +0 -621
- package/src/core/runner.ts +0 -290
- package/src/gates/check.ts +0 -118
- package/src/gates/resolve-check-command.ts +0 -21
- package/src/gates/result.ts +0 -54
- package/src/gates/review.ts +0 -1333
- package/src/hooks/adapters/claude-stop-hook.ts +0 -99
- package/src/hooks/adapters/cursor-stop-hook.ts +0 -122
- package/src/hooks/adapters/types.ts +0 -94
- package/src/hooks/stop-hook-handler.ts +0 -748
- package/src/index.ts +0 -47
- package/src/output/app-logger.ts +0 -214
- package/src/output/console-log.ts +0 -168
- package/src/output/console.ts +0 -359
- package/src/output/logger.ts +0 -126
- package/src/output/sinks/console-sink.ts +0 -59
- package/src/output/sinks/file-sink.ts +0 -110
- package/src/scripts/status.ts +0 -433
- package/src/templates/workflow.yml +0 -79
- package/src/types/gauntlet-status.ts +0 -79
- package/src/utils/debug-log.ts +0 -392
- package/src/utils/diff-parser.ts +0 -103
- package/src/utils/execution-state.ts +0 -472
- package/src/utils/log-parser.ts +0 -696
- package/src/utils/sanitizer.ts +0 -3
- package/src/utils/session-ref.ts +0 -91
package/src/config/loader.ts
DELETED
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import matter from "gray-matter";
|
|
4
|
-
import YAML from "yaml";
|
|
5
|
-
import {
|
|
6
|
-
isBuiltInReview,
|
|
7
|
-
loadBuiltInReview,
|
|
8
|
-
} from "../built-in-reviews/index.js";
|
|
9
|
-
import {
|
|
10
|
-
checkGateSchema,
|
|
11
|
-
gauntletConfigSchema,
|
|
12
|
-
reviewPromptFrontmatterSchema,
|
|
13
|
-
reviewYamlSchema,
|
|
14
|
-
} from "./schema.js";
|
|
15
|
-
import type {
|
|
16
|
-
CheckGateConfig,
|
|
17
|
-
LoadedCheckGateConfig,
|
|
18
|
-
LoadedConfig,
|
|
19
|
-
LoadedReviewGateConfig,
|
|
20
|
-
} from "./types.js";
|
|
21
|
-
|
|
22
|
-
const GAUNTLET_DIR = ".gauntlet";
|
|
23
|
-
const CONFIG_FILE = "config.yml";
|
|
24
|
-
const CHECKS_DIR = "checks";
|
|
25
|
-
const REVIEWS_DIR = "reviews";
|
|
26
|
-
|
|
27
|
-
export async function loadConfig(
|
|
28
|
-
rootDir: string = process.cwd(),
|
|
29
|
-
): Promise<LoadedConfig> {
|
|
30
|
-
const gauntletPath = path.join(rootDir, GAUNTLET_DIR);
|
|
31
|
-
const configPath = path.join(gauntletPath, CONFIG_FILE);
|
|
32
|
-
|
|
33
|
-
// 1. Load project config
|
|
34
|
-
if (!(await fileExists(configPath))) {
|
|
35
|
-
throw new Error(`Configuration file not found at ${configPath}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const configContent = await fs.readFile(configPath, "utf-8");
|
|
39
|
-
const projectConfigRaw = YAML.parse(configContent);
|
|
40
|
-
const projectConfig = gauntletConfigSchema.parse(projectConfigRaw);
|
|
41
|
-
|
|
42
|
-
// 2. Load checks
|
|
43
|
-
const checksPath = path.join(gauntletPath, CHECKS_DIR);
|
|
44
|
-
const checks: Record<string, LoadedCheckGateConfig> = {};
|
|
45
|
-
|
|
46
|
-
if (await dirExists(checksPath)) {
|
|
47
|
-
const checkFiles = await fs.readdir(checksPath);
|
|
48
|
-
for (const file of checkFiles) {
|
|
49
|
-
if (file.endsWith(".yml") || file.endsWith(".yaml")) {
|
|
50
|
-
const filePath = path.join(checksPath, file);
|
|
51
|
-
const content = await fs.readFile(filePath, "utf-8");
|
|
52
|
-
const raw = YAML.parse(content);
|
|
53
|
-
const name = path.basename(file, path.extname(file));
|
|
54
|
-
const parsed: CheckGateConfig = checkGateSchema.parse(raw);
|
|
55
|
-
|
|
56
|
-
// Normalize deprecated alias in loader (not schema) for reliability
|
|
57
|
-
const fixFile = parsed.fix_instructions_file || parsed.fix_instructions;
|
|
58
|
-
|
|
59
|
-
const loadedCheck: LoadedCheckGateConfig = {
|
|
60
|
-
...parsed,
|
|
61
|
-
name,
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
// Load fix instructions file if specified
|
|
65
|
-
if (fixFile) {
|
|
66
|
-
loadedCheck.fixInstructionsContent = await loadPromptFile(
|
|
67
|
-
fixFile,
|
|
68
|
-
gauntletPath,
|
|
69
|
-
`check "${name}"`,
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Store fix_with_skill if specified
|
|
74
|
-
if (parsed.fix_with_skill) {
|
|
75
|
-
loadedCheck.fixWithSkill = parsed.fix_with_skill;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
checks[name] = loadedCheck;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// 3. Load reviews (prompts + frontmatter from .md, or .yml/.yaml configs)
|
|
84
|
-
const reviewsPath = path.join(gauntletPath, REVIEWS_DIR);
|
|
85
|
-
const reviews: LoadedConfig["reviews"] = {};
|
|
86
|
-
|
|
87
|
-
if (await dirExists(reviewsPath)) {
|
|
88
|
-
const reviewFiles = await fs.readdir(reviewsPath);
|
|
89
|
-
|
|
90
|
-
// Detect duplicate names across formats
|
|
91
|
-
const reviewNameSources = new Map<string, string[]>();
|
|
92
|
-
for (const file of reviewFiles) {
|
|
93
|
-
if (
|
|
94
|
-
file.endsWith(".md") ||
|
|
95
|
-
file.endsWith(".yml") ||
|
|
96
|
-
file.endsWith(".yaml")
|
|
97
|
-
) {
|
|
98
|
-
const name = path.basename(file, path.extname(file));
|
|
99
|
-
|
|
100
|
-
// Reject user-defined review files with the reserved built-in: prefix
|
|
101
|
-
if (isBuiltInReview(name)) {
|
|
102
|
-
throw new Error(
|
|
103
|
-
`Review file "${file}" uses the reserved "built-in:" prefix. Rename the file to avoid conflicts with built-in reviews.`,
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const sources = reviewNameSources.get(name) || [];
|
|
108
|
-
sources.push(file);
|
|
109
|
-
reviewNameSources.set(name, sources);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
for (const [name, sources] of reviewNameSources) {
|
|
113
|
-
if (sources.length > 1) {
|
|
114
|
-
throw new Error(
|
|
115
|
-
`Duplicate review name "${name}" found across files: ${sources.join(", ")}. Each review name must be unique.`,
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
for (const file of reviewFiles) {
|
|
121
|
-
if (file.endsWith(".md")) {
|
|
122
|
-
// Markdown review file
|
|
123
|
-
const filePath = path.join(reviewsPath, file);
|
|
124
|
-
const content = await fs.readFile(filePath, "utf-8");
|
|
125
|
-
const { data: frontmatter, content: promptBody } = matter(content);
|
|
126
|
-
|
|
127
|
-
const parsedFrontmatter =
|
|
128
|
-
reviewPromptFrontmatterSchema.parse(frontmatter);
|
|
129
|
-
const name = path.basename(file, ".md");
|
|
130
|
-
|
|
131
|
-
const review: LoadedReviewGateConfig = {
|
|
132
|
-
name,
|
|
133
|
-
prompt: file,
|
|
134
|
-
promptContent: promptBody,
|
|
135
|
-
model: parsedFrontmatter.model,
|
|
136
|
-
cli_preference: parsedFrontmatter.cli_preference,
|
|
137
|
-
num_reviews: parsedFrontmatter.num_reviews,
|
|
138
|
-
parallel: parsedFrontmatter.parallel,
|
|
139
|
-
run_in_ci: parsedFrontmatter.run_in_ci,
|
|
140
|
-
run_locally: parsedFrontmatter.run_locally,
|
|
141
|
-
timeout: parsedFrontmatter.timeout,
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
// If prompt_file is specified, override the markdown body
|
|
145
|
-
if (parsedFrontmatter.prompt_file) {
|
|
146
|
-
review.promptContent = await loadPromptFile(
|
|
147
|
-
parsedFrontmatter.prompt_file,
|
|
148
|
-
gauntletPath,
|
|
149
|
-
`review "${name}"`,
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// If skill_name is specified, ignore body and set skillName
|
|
154
|
-
if (parsedFrontmatter.skill_name) {
|
|
155
|
-
review.promptContent = undefined;
|
|
156
|
-
review.skillName = parsedFrontmatter.skill_name;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
reviews[name] = review;
|
|
160
|
-
} else if (file.endsWith(".yml") || file.endsWith(".yaml")) {
|
|
161
|
-
// YAML review file
|
|
162
|
-
const filePath = path.join(reviewsPath, file);
|
|
163
|
-
const content = await fs.readFile(filePath, "utf-8");
|
|
164
|
-
const raw = YAML.parse(content);
|
|
165
|
-
const parsed = reviewYamlSchema.parse(raw);
|
|
166
|
-
const name = path.basename(file, path.extname(file));
|
|
167
|
-
|
|
168
|
-
const review: LoadedReviewGateConfig = {
|
|
169
|
-
name,
|
|
170
|
-
prompt: file,
|
|
171
|
-
model: parsed.model,
|
|
172
|
-
cli_preference: parsed.cli_preference,
|
|
173
|
-
num_reviews: parsed.num_reviews,
|
|
174
|
-
parallel: parsed.parallel,
|
|
175
|
-
run_in_ci: parsed.run_in_ci,
|
|
176
|
-
run_locally: parsed.run_locally,
|
|
177
|
-
timeout: parsed.timeout,
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
if (parsed.prompt_file) {
|
|
181
|
-
review.promptContent = await loadPromptFile(
|
|
182
|
-
parsed.prompt_file,
|
|
183
|
-
gauntletPath,
|
|
184
|
-
`review "${name}"`,
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (parsed.skill_name) {
|
|
189
|
-
review.skillName = parsed.skill_name;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (parsed.builtin) {
|
|
193
|
-
review.promptContent = loadBuiltInReview(parsed.builtin);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
reviews[name] = review;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// 3b. Merge default CLI preference if not specified (applies to all reviews)
|
|
202
|
-
for (const [name, review] of Object.entries(reviews)) {
|
|
203
|
-
if (!review.cli_preference) {
|
|
204
|
-
review.cli_preference = projectConfig.cli.default_preference;
|
|
205
|
-
} else {
|
|
206
|
-
const allowedTools = new Set(projectConfig.cli.default_preference);
|
|
207
|
-
for (const tool of review.cli_preference) {
|
|
208
|
-
if (!allowedTools.has(tool)) {
|
|
209
|
-
throw new Error(
|
|
210
|
-
`Review "${name}" uses CLI tool "${tool}" which is not in the project-level allowed list (cli.default_preference).`,
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// 4. Validate entry point references
|
|
218
|
-
const checkNames = new Set(Object.keys(checks));
|
|
219
|
-
const reviewNames = new Set(Object.keys(reviews));
|
|
220
|
-
|
|
221
|
-
for (const entryPoint of projectConfig.entry_points) {
|
|
222
|
-
if (entryPoint.checks) {
|
|
223
|
-
for (const checkName of entryPoint.checks) {
|
|
224
|
-
if (!checkNames.has(checkName)) {
|
|
225
|
-
throw new Error(
|
|
226
|
-
`Entry point "${entryPoint.path}" references non-existent check gate: "${checkName}"`,
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
if (entryPoint.reviews) {
|
|
232
|
-
for (const reviewName of entryPoint.reviews) {
|
|
233
|
-
if (!reviewNames.has(reviewName)) {
|
|
234
|
-
throw new Error(
|
|
235
|
-
`Entry point "${entryPoint.path}" references non-existent review gate: "${reviewName}"`,
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return {
|
|
243
|
-
project: projectConfig,
|
|
244
|
-
checks,
|
|
245
|
-
reviews,
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
async function loadPromptFile(
|
|
250
|
-
filePath: string,
|
|
251
|
-
gauntletPath: string,
|
|
252
|
-
source: string,
|
|
253
|
-
): Promise<string> {
|
|
254
|
-
let resolvedPath: string;
|
|
255
|
-
if (path.isAbsolute(filePath)) {
|
|
256
|
-
console.warn(
|
|
257
|
-
`Warning: ${source} uses absolute path "${filePath}". Prefer relative paths for portability.`,
|
|
258
|
-
);
|
|
259
|
-
resolvedPath = filePath;
|
|
260
|
-
} else {
|
|
261
|
-
resolvedPath = path.resolve(gauntletPath, filePath);
|
|
262
|
-
}
|
|
263
|
-
// Warn if resolved path escapes the .gauntlet/ directory (including via relative traversal)
|
|
264
|
-
const normalizedGauntletPath = path.resolve(gauntletPath);
|
|
265
|
-
const relativeToDotGauntlet = path.relative(
|
|
266
|
-
normalizedGauntletPath,
|
|
267
|
-
resolvedPath,
|
|
268
|
-
);
|
|
269
|
-
if (
|
|
270
|
-
relativeToDotGauntlet.startsWith("..") ||
|
|
271
|
-
path.isAbsolute(relativeToDotGauntlet)
|
|
272
|
-
) {
|
|
273
|
-
console.warn(
|
|
274
|
-
`Warning: ${source} references file outside .gauntlet/ directory: "${filePath}" (resolves to ${resolvedPath}). Review .gauntlet/ config changes carefully in PRs.`,
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
if (!(await fileExists(resolvedPath))) {
|
|
278
|
-
throw new Error(
|
|
279
|
-
`File not found: ${resolvedPath} (referenced by ${source})`,
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
return fs.readFile(resolvedPath, "utf-8");
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async function fileExists(path: string): Promise<boolean> {
|
|
286
|
-
try {
|
|
287
|
-
const stat = await fs.stat(path);
|
|
288
|
-
return stat.isFile();
|
|
289
|
-
} catch {
|
|
290
|
-
return false;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
async function dirExists(path: string): Promise<boolean> {
|
|
295
|
-
try {
|
|
296
|
-
const stat = await fs.stat(path);
|
|
297
|
-
return stat.isDirectory();
|
|
298
|
-
} catch {
|
|
299
|
-
return false;
|
|
300
|
-
}
|
|
301
|
-
}
|
package/src/config/schema.ts
DELETED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
export const adapterConfigSchema = z.object({
|
|
4
|
-
allow_tool_use: z.boolean().default(true),
|
|
5
|
-
thinking_budget: z.enum(["off", "low", "medium", "high"]).optional(),
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
export const cliConfigSchema = z.object({
|
|
9
|
-
default_preference: z.array(z.string().min(1)).min(1),
|
|
10
|
-
adapters: z.record(z.string(), adapterConfigSchema).optional(),
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
export const checkGateSchema = z
|
|
14
|
-
.object({
|
|
15
|
-
command: z.string().min(1),
|
|
16
|
-
rerun_command: z.string().min(1).optional(),
|
|
17
|
-
working_directory: z.string().optional(),
|
|
18
|
-
parallel: z.boolean().default(false),
|
|
19
|
-
run_in_ci: z.boolean().default(true),
|
|
20
|
-
run_locally: z.boolean().default(true),
|
|
21
|
-
timeout: z.number().optional(),
|
|
22
|
-
fail_fast: z.boolean().optional(),
|
|
23
|
-
fix_instructions: z.string().optional(), // Deprecated alias for fix_instructions_file
|
|
24
|
-
fix_instructions_file: z.string().optional(),
|
|
25
|
-
fix_with_skill: z.string().optional(),
|
|
26
|
-
})
|
|
27
|
-
.superRefine((data, ctx) => {
|
|
28
|
-
// fail_fast can only be used when parallel is false
|
|
29
|
-
if (data.fail_fast === true && data.parallel === true) {
|
|
30
|
-
ctx.addIssue({
|
|
31
|
-
code: z.ZodIssueCode.custom,
|
|
32
|
-
message: "fail_fast can only be used when parallel is false",
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
// Cannot specify both deprecated fix_instructions and fix_instructions_file
|
|
36
|
-
if (data.fix_instructions && data.fix_instructions_file) {
|
|
37
|
-
ctx.addIssue({
|
|
38
|
-
code: z.ZodIssueCode.custom,
|
|
39
|
-
message:
|
|
40
|
-
"Cannot specify both 'fix_instructions' (deprecated) and 'fix_instructions_file'. Use only 'fix_instructions_file'.",
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
// fix_instructions_file (or its deprecated alias) and fix_with_skill are mutually exclusive
|
|
44
|
-
const effectiveFixFile =
|
|
45
|
-
data.fix_instructions_file || data.fix_instructions;
|
|
46
|
-
if (effectiveFixFile && data.fix_with_skill) {
|
|
47
|
-
ctx.addIssue({
|
|
48
|
-
code: z.ZodIssueCode.custom,
|
|
49
|
-
message:
|
|
50
|
-
"'fix_instructions_file' and 'fix_with_skill' are mutually exclusive. Specify only one.",
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
export const reviewGateSchema = z.object({
|
|
56
|
-
name: z.string().min(1),
|
|
57
|
-
prompt: z.string().min(1), // Path relative to .gauntlet/reviews/
|
|
58
|
-
cli_preference: z.array(z.string().min(1)).optional(),
|
|
59
|
-
num_reviews: z.number().default(1),
|
|
60
|
-
parallel: z.boolean().default(true),
|
|
61
|
-
run_in_ci: z.boolean().default(true),
|
|
62
|
-
run_locally: z.boolean().default(true),
|
|
63
|
-
timeout: z.number().optional(),
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
export const reviewPromptFrontmatterSchema = z
|
|
67
|
-
.object({
|
|
68
|
-
model: z.string().optional(),
|
|
69
|
-
cli_preference: z.array(z.string().min(1)).optional(),
|
|
70
|
-
num_reviews: z.number().default(1),
|
|
71
|
-
parallel: z.boolean().default(true),
|
|
72
|
-
run_in_ci: z.boolean().default(true),
|
|
73
|
-
run_locally: z.boolean().default(true),
|
|
74
|
-
timeout: z.number().optional(),
|
|
75
|
-
prompt_file: z.string().optional(),
|
|
76
|
-
skill_name: z.string().optional(),
|
|
77
|
-
})
|
|
78
|
-
.refine((data) => !(data.prompt_file && data.skill_name), {
|
|
79
|
-
message:
|
|
80
|
-
"'prompt_file' and 'skill_name' are mutually exclusive. Specify only one.",
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
export const reviewYamlSchema = z
|
|
84
|
-
.object({
|
|
85
|
-
model: z.string().optional(),
|
|
86
|
-
cli_preference: z.array(z.string().min(1)).optional(),
|
|
87
|
-
num_reviews: z.number().default(1),
|
|
88
|
-
parallel: z.boolean().default(true),
|
|
89
|
-
run_in_ci: z.boolean().default(true),
|
|
90
|
-
run_locally: z.boolean().default(true),
|
|
91
|
-
timeout: z.number().optional(),
|
|
92
|
-
prompt_file: z.string().optional(),
|
|
93
|
-
skill_name: z.string().optional(),
|
|
94
|
-
builtin: z.string().optional(),
|
|
95
|
-
})
|
|
96
|
-
.superRefine((data, ctx) => {
|
|
97
|
-
const sources = [data.prompt_file, data.skill_name, data.builtin].filter(
|
|
98
|
-
Boolean,
|
|
99
|
-
);
|
|
100
|
-
if (sources.length > 1) {
|
|
101
|
-
ctx.addIssue({
|
|
102
|
-
code: z.ZodIssueCode.custom,
|
|
103
|
-
message:
|
|
104
|
-
"'prompt_file', 'skill_name', and 'builtin' are mutually exclusive. Specify only one.",
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
if (sources.length === 0) {
|
|
108
|
-
ctx.addIssue({
|
|
109
|
-
code: z.ZodIssueCode.custom,
|
|
110
|
-
message:
|
|
111
|
-
"YAML review files must specify exactly one of 'prompt_file', 'skill_name', or 'builtin'.",
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
export const entryPointSchema = z.object({
|
|
117
|
-
path: z.string().min(1),
|
|
118
|
-
exclude: z.array(z.string().min(1)).optional(),
|
|
119
|
-
checks: z.array(z.string().min(1)).optional(),
|
|
120
|
-
reviews: z.array(z.string().min(1)).optional(),
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
export const debugLogConfigSchema = z.object({
|
|
124
|
-
enabled: z.boolean().default(false),
|
|
125
|
-
max_size_mb: z.number().default(10),
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
export const loggingConsoleConfigSchema = z.object({
|
|
129
|
-
enabled: z.boolean().default(true),
|
|
130
|
-
format: z.enum(["pretty", "json"]).default("pretty"),
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
export const loggingFileConfigSchema = z.object({
|
|
134
|
-
enabled: z.boolean().default(true),
|
|
135
|
-
format: z.enum(["text", "json"]).default("text"),
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
export const loggingConfigSchema = z.object({
|
|
139
|
-
level: z.enum(["debug", "info", "warning", "error"]).default("debug"),
|
|
140
|
-
console: loggingConsoleConfigSchema.optional(),
|
|
141
|
-
file: loggingFileConfigSchema.optional(),
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
export const stopHookConfigSchema = z.object({
|
|
145
|
-
enabled: z.boolean().optional(),
|
|
146
|
-
run_interval_minutes: z.number().int().min(0).optional(),
|
|
147
|
-
auto_push_pr: z.boolean().optional(),
|
|
148
|
-
auto_fix_pr: z.boolean().optional(),
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
export const gauntletConfigSchema = z.object({
|
|
152
|
-
base_branch: z.string().min(1).default("origin/main"),
|
|
153
|
-
log_dir: z.string().min(1).default("gauntlet_logs"),
|
|
154
|
-
allow_parallel: z.boolean().default(true),
|
|
155
|
-
max_retries: z.number().default(3),
|
|
156
|
-
max_previous_logs: z.number().int().min(0).default(3),
|
|
157
|
-
rerun_new_issue_threshold: z
|
|
158
|
-
.enum(["critical", "high", "medium", "low"])
|
|
159
|
-
.default("medium"),
|
|
160
|
-
cli: cliConfigSchema,
|
|
161
|
-
entry_points: z.array(entryPointSchema).min(1),
|
|
162
|
-
debug_log: debugLogConfigSchema.optional(),
|
|
163
|
-
logging: loggingConfigSchema.optional(),
|
|
164
|
-
stop_hook: stopHookConfigSchema.optional(),
|
|
165
|
-
});
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import type { z } from "zod";
|
|
2
|
-
import type { GlobalConfig } from "./global.js";
|
|
3
|
-
import type { stopHookConfigSchema } from "./schema.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Environment variable names for stop hook configuration.
|
|
7
|
-
*/
|
|
8
|
-
export const GAUNTLET_STOP_HOOK_ENABLED = "GAUNTLET_STOP_HOOK_ENABLED";
|
|
9
|
-
export const GAUNTLET_STOP_HOOK_INTERVAL_MINUTES =
|
|
10
|
-
"GAUNTLET_STOP_HOOK_INTERVAL_MINUTES";
|
|
11
|
-
export const GAUNTLET_AUTO_PUSH_PR = "GAUNTLET_AUTO_PUSH_PR";
|
|
12
|
-
export const GAUNTLET_AUTO_FIX_PR = "GAUNTLET_AUTO_FIX_PR";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Resolved stop hook configuration.
|
|
16
|
-
*/
|
|
17
|
-
export interface StopHookConfig {
|
|
18
|
-
enabled: boolean;
|
|
19
|
-
run_interval_minutes: number;
|
|
20
|
-
auto_push_pr: boolean;
|
|
21
|
-
auto_fix_pr: boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
type ProjectStopHookConfig = z.infer<typeof stopHookConfigSchema> | undefined;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Parse a boolean environment variable (accepts "true", "1", "false", "0").
|
|
28
|
-
* Returns undefined for unset or invalid values.
|
|
29
|
-
*/
|
|
30
|
-
function parseBooleanEnv(envVar: string | undefined): boolean | undefined {
|
|
31
|
-
if (envVar === undefined) return undefined;
|
|
32
|
-
const normalized = envVar.toLowerCase().trim();
|
|
33
|
-
if (normalized === "true" || normalized === "1") return true;
|
|
34
|
-
if (normalized === "false" || normalized === "0") return false;
|
|
35
|
-
return undefined;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Parse an integer environment variable (accepts non-negative integers only).
|
|
40
|
-
* Returns undefined for unset or invalid values.
|
|
41
|
-
*/
|
|
42
|
-
function parseIntegerEnv(envVar: string | undefined): number | undefined {
|
|
43
|
-
if (envVar === undefined) return undefined;
|
|
44
|
-
const normalized = envVar.trim();
|
|
45
|
-
const parsed = Number(normalized);
|
|
46
|
-
if (normalized.length > 0 && Number.isInteger(parsed) && parsed >= 0) {
|
|
47
|
-
return parsed;
|
|
48
|
-
}
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Parse environment variables for stop hook configuration.
|
|
54
|
-
* Returns undefined for fields that are not set or have invalid values.
|
|
55
|
-
*/
|
|
56
|
-
export function parseStopHookEnvVars(): {
|
|
57
|
-
enabled?: boolean;
|
|
58
|
-
run_interval_minutes?: number;
|
|
59
|
-
auto_push_pr?: boolean;
|
|
60
|
-
auto_fix_pr?: boolean;
|
|
61
|
-
} {
|
|
62
|
-
return {
|
|
63
|
-
enabled: parseBooleanEnv(process.env[GAUNTLET_STOP_HOOK_ENABLED]),
|
|
64
|
-
run_interval_minutes: parseIntegerEnv(
|
|
65
|
-
process.env[GAUNTLET_STOP_HOOK_INTERVAL_MINUTES],
|
|
66
|
-
),
|
|
67
|
-
auto_push_pr: parseBooleanEnv(process.env[GAUNTLET_AUTO_PUSH_PR]),
|
|
68
|
-
auto_fix_pr: parseBooleanEnv(process.env[GAUNTLET_AUTO_FIX_PR]),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Resolve a single config field with 3-tier precedence: env > project > global.
|
|
74
|
-
*/
|
|
75
|
-
function resolveField<T>(
|
|
76
|
-
envValue: T | undefined,
|
|
77
|
-
projectValue: T | undefined,
|
|
78
|
-
globalValue: T,
|
|
79
|
-
): T {
|
|
80
|
-
if (envValue !== undefined) return envValue;
|
|
81
|
-
if (projectValue !== undefined) return projectValue;
|
|
82
|
-
return globalValue;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Resolve stop hook configuration from three sources with precedence:
|
|
87
|
-
* 1. Environment variables (highest)
|
|
88
|
-
* 2. Project config (.gauntlet/config.yml)
|
|
89
|
-
* 3. Global config (~/.config/agent-gauntlet/config.yml) (lowest)
|
|
90
|
-
*
|
|
91
|
-
* Each field is resolved independently.
|
|
92
|
-
*/
|
|
93
|
-
export function resolveStopHookConfig(
|
|
94
|
-
projectConfig: ProjectStopHookConfig,
|
|
95
|
-
globalConfig: GlobalConfig,
|
|
96
|
-
): StopHookConfig {
|
|
97
|
-
const envVars = parseStopHookEnvVars();
|
|
98
|
-
const globalStop = globalConfig.stop_hook;
|
|
99
|
-
|
|
100
|
-
const enabled = resolveField(
|
|
101
|
-
envVars.enabled,
|
|
102
|
-
projectConfig?.enabled,
|
|
103
|
-
globalStop.enabled,
|
|
104
|
-
);
|
|
105
|
-
const run_interval_minutes = resolveField(
|
|
106
|
-
envVars.run_interval_minutes,
|
|
107
|
-
projectConfig?.run_interval_minutes,
|
|
108
|
-
globalStop.run_interval_minutes,
|
|
109
|
-
);
|
|
110
|
-
const auto_push_pr = resolveField(
|
|
111
|
-
envVars.auto_push_pr,
|
|
112
|
-
projectConfig?.auto_push_pr,
|
|
113
|
-
globalStop.auto_push_pr,
|
|
114
|
-
);
|
|
115
|
-
let auto_fix_pr = resolveField(
|
|
116
|
-
envVars.auto_fix_pr,
|
|
117
|
-
projectConfig?.auto_fix_pr,
|
|
118
|
-
globalStop.auto_fix_pr,
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
// Validation: auto_fix_pr requires auto_push_pr
|
|
122
|
-
if (auto_fix_pr && !auto_push_pr) {
|
|
123
|
-
console.error(
|
|
124
|
-
"[gauntlet] Warning: auto_fix_pr=true requires auto_push_pr=true. Treating auto_fix_pr as false.",
|
|
125
|
-
);
|
|
126
|
-
auto_fix_pr = false;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return { enabled, run_interval_minutes, auto_push_pr, auto_fix_pr };
|
|
130
|
-
}
|
package/src/config/types.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import type { z } from "zod";
|
|
2
|
-
import type {
|
|
3
|
-
ciCheckConfigSchema,
|
|
4
|
-
ciConfigSchema,
|
|
5
|
-
ciSetupStepSchema,
|
|
6
|
-
runtimeConfigSchema,
|
|
7
|
-
serviceConfigSchema,
|
|
8
|
-
} from "./ci-schema.js";
|
|
9
|
-
import type {
|
|
10
|
-
adapterConfigSchema,
|
|
11
|
-
checkGateSchema,
|
|
12
|
-
cliConfigSchema,
|
|
13
|
-
entryPointSchema,
|
|
14
|
-
gauntletConfigSchema,
|
|
15
|
-
reviewGateSchema,
|
|
16
|
-
reviewPromptFrontmatterSchema,
|
|
17
|
-
reviewYamlSchema,
|
|
18
|
-
} from "./schema.js";
|
|
19
|
-
|
|
20
|
-
export type CheckGateConfig = z.infer<typeof checkGateSchema>;
|
|
21
|
-
export type ReviewGateConfig = z.infer<typeof reviewGateSchema>;
|
|
22
|
-
export type ReviewPromptFrontmatter = z.infer<
|
|
23
|
-
typeof reviewPromptFrontmatterSchema
|
|
24
|
-
>;
|
|
25
|
-
export type EntryPointConfig = z.infer<typeof entryPointSchema>;
|
|
26
|
-
export type GauntletConfig = z.infer<typeof gauntletConfigSchema>;
|
|
27
|
-
export type CLIConfig = z.infer<typeof cliConfigSchema>;
|
|
28
|
-
export type AdapterConfig = z.infer<typeof adapterConfigSchema>;
|
|
29
|
-
|
|
30
|
-
export type CIConfig = z.infer<typeof ciConfigSchema>;
|
|
31
|
-
export type CICheckConfig = z.infer<typeof ciCheckConfigSchema>;
|
|
32
|
-
export type CISetupStep = z.infer<typeof ciSetupStepSchema>;
|
|
33
|
-
export type RuntimeConfig = z.infer<typeof runtimeConfigSchema>;
|
|
34
|
-
export type ServiceConfig = z.infer<typeof serviceConfigSchema>;
|
|
35
|
-
|
|
36
|
-
export type ReviewYamlConfig = z.infer<typeof reviewYamlSchema>;
|
|
37
|
-
|
|
38
|
-
// Extended check config with loaded content
|
|
39
|
-
export interface LoadedCheckGateConfig extends CheckGateConfig {
|
|
40
|
-
name: string;
|
|
41
|
-
fixInstructionsContent?: string;
|
|
42
|
-
fixWithSkill?: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Extended review config with loaded content
|
|
46
|
-
export interface LoadedReviewGateConfig {
|
|
47
|
-
name: string;
|
|
48
|
-
prompt: string; // filename or source identifier
|
|
49
|
-
promptContent?: string; // loaded prompt content (undefined when skill_name is used)
|
|
50
|
-
skillName?: string; // CLI skill name for prompt delegation
|
|
51
|
-
model?: string;
|
|
52
|
-
cli_preference?: string[];
|
|
53
|
-
num_reviews: number;
|
|
54
|
-
parallel: boolean;
|
|
55
|
-
run_in_ci: boolean;
|
|
56
|
-
run_locally: boolean;
|
|
57
|
-
timeout?: number;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Combined type for the fully loaded configuration
|
|
61
|
-
export interface LoadedConfig {
|
|
62
|
-
project: GauntletConfig;
|
|
63
|
-
checks: Record<string, LoadedCheckGateConfig>;
|
|
64
|
-
reviews: Record<string, LoadedReviewGateConfig>;
|
|
65
|
-
}
|