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.
Files changed (71) hide show
  1. package/README.md +25 -23
  2. package/dist/index.js +9226 -0
  3. package/dist/index.js.map +65 -0
  4. package/dist/scripts/status.js +280 -0
  5. package/dist/scripts/status.js.map +10 -0
  6. package/package.json +22 -8
  7. package/src/built-in-reviews/code-quality.md +0 -25
  8. package/src/built-in-reviews/index.ts +0 -28
  9. package/src/bun-plugins.d.ts +0 -4
  10. package/src/cli-adapters/claude.ts +0 -327
  11. package/src/cli-adapters/codex.ts +0 -290
  12. package/src/cli-adapters/cursor.ts +0 -128
  13. package/src/cli-adapters/gemini.ts +0 -510
  14. package/src/cli-adapters/github-copilot.ts +0 -141
  15. package/src/cli-adapters/index.ts +0 -250
  16. package/src/cli-adapters/thinking-budget.ts +0 -23
  17. package/src/commands/check.ts +0 -311
  18. package/src/commands/ci/index.ts +0 -15
  19. package/src/commands/ci/init.ts +0 -96
  20. package/src/commands/ci/list-jobs.ts +0 -90
  21. package/src/commands/clean.ts +0 -54
  22. package/src/commands/detect.ts +0 -173
  23. package/src/commands/health.ts +0 -169
  24. package/src/commands/help.ts +0 -34
  25. package/src/commands/index.ts +0 -13
  26. package/src/commands/init.ts +0 -1878
  27. package/src/commands/list.ts +0 -33
  28. package/src/commands/review.ts +0 -311
  29. package/src/commands/run.ts +0 -29
  30. package/src/commands/shared.ts +0 -267
  31. package/src/commands/stop-hook.ts +0 -567
  32. package/src/commands/validate.ts +0 -20
  33. package/src/commands/wait-ci.ts +0 -518
  34. package/src/config/ci-loader.ts +0 -33
  35. package/src/config/ci-schema.ts +0 -28
  36. package/src/config/global.ts +0 -87
  37. package/src/config/loader.ts +0 -301
  38. package/src/config/schema.ts +0 -165
  39. package/src/config/stop-hook-config.ts +0 -130
  40. package/src/config/types.ts +0 -65
  41. package/src/config/validator.ts +0 -592
  42. package/src/core/change-detector.ts +0 -137
  43. package/src/core/diff-stats.ts +0 -442
  44. package/src/core/entry-point.ts +0 -190
  45. package/src/core/job.ts +0 -96
  46. package/src/core/run-executor.ts +0 -621
  47. package/src/core/runner.ts +0 -290
  48. package/src/gates/check.ts +0 -118
  49. package/src/gates/resolve-check-command.ts +0 -21
  50. package/src/gates/result.ts +0 -54
  51. package/src/gates/review.ts +0 -1333
  52. package/src/hooks/adapters/claude-stop-hook.ts +0 -99
  53. package/src/hooks/adapters/cursor-stop-hook.ts +0 -122
  54. package/src/hooks/adapters/types.ts +0 -94
  55. package/src/hooks/stop-hook-handler.ts +0 -748
  56. package/src/index.ts +0 -47
  57. package/src/output/app-logger.ts +0 -214
  58. package/src/output/console-log.ts +0 -168
  59. package/src/output/console.ts +0 -359
  60. package/src/output/logger.ts +0 -126
  61. package/src/output/sinks/console-sink.ts +0 -59
  62. package/src/output/sinks/file-sink.ts +0 -110
  63. package/src/scripts/status.ts +0 -433
  64. package/src/templates/workflow.yml +0 -79
  65. package/src/types/gauntlet-status.ts +0 -79
  66. package/src/utils/debug-log.ts +0 -392
  67. package/src/utils/diff-parser.ts +0 -103
  68. package/src/utils/execution-state.ts +0 -472
  69. package/src/utils/log-parser.ts +0 -696
  70. package/src/utils/sanitizer.ts +0 -3
  71. package/src/utils/session-ref.ts +0 -91
@@ -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
- }
@@ -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
- }
@@ -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
- }