agent-gauntlet 0.1.4

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 (44) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +106 -0
  3. package/package.json +51 -0
  4. package/src/cli-adapters/claude.ts +114 -0
  5. package/src/cli-adapters/codex.ts +123 -0
  6. package/src/cli-adapters/gemini.ts +149 -0
  7. package/src/cli-adapters/index.ts +79 -0
  8. package/src/commands/check.test.ts +25 -0
  9. package/src/commands/check.ts +67 -0
  10. package/src/commands/detect.test.ts +37 -0
  11. package/src/commands/detect.ts +69 -0
  12. package/src/commands/health.test.ts +79 -0
  13. package/src/commands/health.ts +148 -0
  14. package/src/commands/help.test.ts +44 -0
  15. package/src/commands/help.ts +24 -0
  16. package/src/commands/index.ts +9 -0
  17. package/src/commands/init.test.ts +105 -0
  18. package/src/commands/init.ts +330 -0
  19. package/src/commands/list.test.ts +104 -0
  20. package/src/commands/list.ts +29 -0
  21. package/src/commands/rerun.ts +118 -0
  22. package/src/commands/review.test.ts +25 -0
  23. package/src/commands/review.ts +67 -0
  24. package/src/commands/run.test.ts +25 -0
  25. package/src/commands/run.ts +64 -0
  26. package/src/commands/shared.ts +10 -0
  27. package/src/config/loader.test.ts +129 -0
  28. package/src/config/loader.ts +130 -0
  29. package/src/config/schema.ts +63 -0
  30. package/src/config/types.ts +23 -0
  31. package/src/config/validator.ts +493 -0
  32. package/src/core/change-detector.ts +112 -0
  33. package/src/core/entry-point.test.ts +63 -0
  34. package/src/core/entry-point.ts +80 -0
  35. package/src/core/job.ts +74 -0
  36. package/src/core/runner.ts +226 -0
  37. package/src/gates/check.ts +82 -0
  38. package/src/gates/result.ts +9 -0
  39. package/src/gates/review.ts +501 -0
  40. package/src/index.ts +38 -0
  41. package/src/output/console.ts +201 -0
  42. package/src/output/logger.ts +66 -0
  43. package/src/utils/log-parser.ts +228 -0
  44. package/src/utils/sanitizer.ts +3 -0
@@ -0,0 +1,118 @@
1
+ import type { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { loadConfig } from '../config/loader.js';
4
+ import { ChangeDetector } from '../core/change-detector.js';
5
+ import { EntryPointExpander } from '../core/entry-point.js';
6
+ import { JobGenerator } from '../core/job.js';
7
+ import { Runner } from '../core/runner.js';
8
+ import { Logger } from '../output/logger.js';
9
+ import { ConsoleReporter } from '../output/console.js';
10
+ import { findPreviousFailures, GateFailures, PreviousViolation } from '../utils/log-parser.js';
11
+
12
+ export function registerRerunCommand(program: Command): void {
13
+ program
14
+ .command('rerun')
15
+ .description('Rerun gates (checks & reviews) with previous failures as context (defaults to uncommitted changes)')
16
+ .option('-g, --gate <name>', 'Run specific gate only')
17
+ .option('-c, --commit <sha>', 'Use diff for a specific commit (overrides default uncommitted mode)')
18
+ .action(async (options) => {
19
+ try {
20
+ const config = await loadConfig();
21
+
22
+ // Parse previous failures from log files (only for review gates)
23
+ console.log(chalk.dim('Analyzing previous runs...'));
24
+
25
+ // findPreviousFailures handles errors internally and returns empty array on failure
26
+ const previousFailures = await findPreviousFailures(
27
+ config.project.log_dir,
28
+ options.gate
29
+ );
30
+
31
+ // Create a map: jobId -> (adapterName -> violations)
32
+ const failuresMap = new Map<string, Map<string, PreviousViolation[]>>();
33
+ for (const gateFailure of previousFailures) {
34
+ const adapterMap = new Map<string, PreviousViolation[]>();
35
+ for (const adapterFailure of gateFailure.adapterFailures) {
36
+ adapterMap.set(adapterFailure.adapterName, adapterFailure.violations);
37
+ }
38
+ failuresMap.set(gateFailure.jobId, adapterMap);
39
+ }
40
+
41
+ if (previousFailures.length > 0) {
42
+ const totalViolations = previousFailures.reduce(
43
+ (sum, gf) => sum + gf.adapterFailures.reduce(
44
+ (s, af) => s + af.violations.length, 0
45
+ ), 0
46
+ );
47
+ console.log(chalk.yellow(
48
+ `Found ${previousFailures.length} gate(s) with ${totalViolations} previous violation(s)`
49
+ ));
50
+ } else {
51
+ console.log(chalk.dim('No previous failures found. Running as normal...'));
52
+ }
53
+
54
+ // Detect changes (default to uncommitted unless --commit is specified)
55
+ // Note: Rerun defaults to uncommitted changes for faster iteration loops,
56
+ // unlike 'run' which defaults to base_branch comparison.
57
+ const changeOptions = {
58
+ commit: options.commit,
59
+ uncommitted: !options.commit // Default to uncommitted unless commit is specified
60
+ };
61
+
62
+ const changeDetector = new ChangeDetector(
63
+ config.project.base_branch,
64
+ changeOptions
65
+ );
66
+ const expander = new EntryPointExpander();
67
+ const jobGen = new JobGenerator(config);
68
+
69
+ const modeDesc = options.commit
70
+ ? `commit ${options.commit}`
71
+ : 'uncommitted changes';
72
+ console.log(chalk.dim(`Detecting changes (${modeDesc})...`));
73
+
74
+ const changes = await changeDetector.getChangedFiles();
75
+
76
+ if (changes.length === 0) {
77
+ console.log(chalk.green('No changes detected.'));
78
+ process.exit(0);
79
+ }
80
+
81
+ console.log(chalk.dim(`Found ${changes.length} changed files.`));
82
+
83
+ const entryPoints = await expander.expand(config.project.entry_points, changes);
84
+ let jobs = jobGen.generateJobs(entryPoints);
85
+
86
+ if (options.gate) {
87
+ jobs = jobs.filter(j => j.name === options.gate);
88
+ }
89
+
90
+ if (jobs.length === 0) {
91
+ console.log(chalk.yellow('No applicable gates for these changes.'));
92
+ process.exit(0);
93
+ }
94
+
95
+ console.log(chalk.dim(`Running ${jobs.length} gates...`));
96
+ if (previousFailures.length > 0) {
97
+ console.log(chalk.dim('Previous failures will be injected as context for matching reviewers.'));
98
+ }
99
+
100
+ const logger = new Logger(config.project.log_dir);
101
+ const reporter = new ConsoleReporter();
102
+ const runner = new Runner(
103
+ config,
104
+ logger,
105
+ reporter,
106
+ failuresMap, // Pass previous failures map
107
+ changeOptions // Pass change detection options
108
+ );
109
+
110
+ const success = await runner.run(jobs);
111
+ process.exit(success ? 0 : 1);
112
+
113
+ } catch (error: any) {
114
+ console.error(chalk.red('Error:'), error.message);
115
+ process.exit(1);
116
+ }
117
+ });
118
+ }
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect, beforeEach } from 'bun:test';
2
+ import { Command } from 'commander';
3
+ import { registerReviewCommand } from './review.js';
4
+
5
+ describe('Review Command', () => {
6
+ let program: Command;
7
+
8
+ beforeEach(() => {
9
+ program = new Command();
10
+ registerReviewCommand(program);
11
+ });
12
+
13
+ it('should register the review command', () => {
14
+ const reviewCmd = program.commands.find(cmd => cmd.name() === 'review');
15
+ expect(reviewCmd).toBeDefined();
16
+ expect(reviewCmd?.description()).toBe('Run only applicable reviews for detected changes');
17
+ });
18
+
19
+ it('should have correct options', () => {
20
+ const reviewCmd = program.commands.find(cmd => cmd.name() === 'review');
21
+ expect(reviewCmd?.options.some(opt => opt.long === '--gate')).toBe(true);
22
+ expect(reviewCmd?.options.some(opt => opt.long === '--commit')).toBe(true);
23
+ expect(reviewCmd?.options.some(opt => opt.long === '--uncommitted')).toBe(true);
24
+ });
25
+ });
@@ -0,0 +1,67 @@
1
+ import type { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { loadConfig } from '../config/loader.js';
4
+ import { ChangeDetector } from '../core/change-detector.js';
5
+ import { EntryPointExpander } from '../core/entry-point.js';
6
+ import { JobGenerator } from '../core/job.js';
7
+ import { Runner } from '../core/runner.js';
8
+ import { Logger } from '../output/logger.js';
9
+ import { ConsoleReporter } from '../output/console.js';
10
+
11
+ export function registerReviewCommand(program: Command): void {
12
+ program
13
+ .command('review')
14
+ .description('Run only applicable reviews for detected changes')
15
+ .option('-g, --gate <name>', 'Run specific review gate only')
16
+ .option('-c, --commit <sha>', 'Use diff for a specific commit')
17
+ .option('-u, --uncommitted', 'Use diff for current uncommitted changes (staged and unstaged)')
18
+ .action(async (options) => {
19
+ try {
20
+ const config = await loadConfig();
21
+ const changeDetector = new ChangeDetector(config.project.base_branch, {
22
+ commit: options.commit,
23
+ uncommitted: options.uncommitted
24
+ });
25
+ const expander = new EntryPointExpander();
26
+ const jobGen = new JobGenerator(config);
27
+
28
+ console.log(chalk.dim('Detecting changes...'));
29
+ const changes = await changeDetector.getChangedFiles();
30
+
31
+ if (changes.length === 0) {
32
+ console.log(chalk.green('No changes detected.'));
33
+ process.exit(0);
34
+ }
35
+
36
+ console.log(chalk.dim(`Found ${changes.length} changed files.`));
37
+
38
+ const entryPoints = await expander.expand(config.project.entry_points, changes);
39
+ let jobs = jobGen.generateJobs(entryPoints);
40
+
41
+ // Filter to only reviews
42
+ jobs = jobs.filter(j => j.type === 'review');
43
+
44
+ if (options.gate) {
45
+ jobs = jobs.filter(j => j.name === options.gate);
46
+ }
47
+
48
+ if (jobs.length === 0) {
49
+ console.log(chalk.yellow('No applicable reviews for these changes.'));
50
+ process.exit(0);
51
+ }
52
+
53
+ console.log(chalk.dim(`Running ${jobs.length} review(s)...`));
54
+
55
+ const logger = new Logger(config.project.log_dir);
56
+ const reporter = new ConsoleReporter();
57
+ const runner = new Runner(config, logger, reporter);
58
+
59
+ const success = await runner.run(jobs);
60
+ process.exit(success ? 0 : 1);
61
+
62
+ } catch (error: any) {
63
+ console.error(chalk.red('Error:'), error.message);
64
+ process.exit(1);
65
+ }
66
+ });
67
+ }
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect, beforeEach } from 'bun:test';
2
+ import { Command } from 'commander';
3
+ import { registerRunCommand } from './run.js';
4
+
5
+ describe('Run Command', () => {
6
+ let program: Command;
7
+
8
+ beforeEach(() => {
9
+ program = new Command();
10
+ registerRunCommand(program);
11
+ });
12
+
13
+ it('should register the run command', () => {
14
+ const runCmd = program.commands.find(cmd => cmd.name() === 'run');
15
+ expect(runCmd).toBeDefined();
16
+ expect(runCmd?.description()).toBe('Run gates for detected changes');
17
+ });
18
+
19
+ it('should have correct options', () => {
20
+ const runCmd = program.commands.find(cmd => cmd.name() === 'run');
21
+ expect(runCmd?.options.some(opt => opt.long === '--gate')).toBe(true);
22
+ expect(runCmd?.options.some(opt => opt.long === '--commit')).toBe(true);
23
+ expect(runCmd?.options.some(opt => opt.long === '--uncommitted')).toBe(true);
24
+ });
25
+ });
@@ -0,0 +1,64 @@
1
+ import type { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { loadConfig } from '../config/loader.js';
4
+ import { ChangeDetector } from '../core/change-detector.js';
5
+ import { EntryPointExpander } from '../core/entry-point.js';
6
+ import { JobGenerator } from '../core/job.js';
7
+ import { Runner } from '../core/runner.js';
8
+ import { Logger } from '../output/logger.js';
9
+ import { ConsoleReporter } from '../output/console.js';
10
+
11
+ export function registerRunCommand(program: Command): void {
12
+ program
13
+ .command('run')
14
+ .description('Run gates for detected changes')
15
+ .option('-g, --gate <name>', 'Run specific gate only')
16
+ .option('-c, --commit <sha>', 'Use diff for a specific commit')
17
+ .option('-u, --uncommitted', 'Use diff for current uncommitted changes (staged and unstaged)')
18
+ .action(async (options) => {
19
+ try {
20
+ const config = await loadConfig();
21
+ const changeDetector = new ChangeDetector(config.project.base_branch, {
22
+ commit: options.commit,
23
+ uncommitted: options.uncommitted
24
+ });
25
+ const expander = new EntryPointExpander();
26
+ const jobGen = new JobGenerator(config);
27
+
28
+ console.log(chalk.dim('Detecting changes...'));
29
+ const changes = await changeDetector.getChangedFiles();
30
+
31
+ if (changes.length === 0) {
32
+ console.log(chalk.green('No changes detected.'));
33
+ process.exit(0);
34
+ }
35
+
36
+ console.log(chalk.dim(`Found ${changes.length} changed files.`));
37
+
38
+ const entryPoints = await expander.expand(config.project.entry_points, changes);
39
+ let jobs = jobGen.generateJobs(entryPoints);
40
+
41
+ if (options.gate) {
42
+ jobs = jobs.filter(j => j.name === options.gate);
43
+ }
44
+
45
+ if (jobs.length === 0) {
46
+ console.log(chalk.yellow('No applicable gates for these changes.'));
47
+ process.exit(0);
48
+ }
49
+
50
+ console.log(chalk.dim(`Running ${jobs.length} gates...`));
51
+
52
+ const logger = new Logger(config.project.log_dir);
53
+ const reporter = new ConsoleReporter();
54
+ const runner = new Runner(config, logger, reporter);
55
+
56
+ const success = await runner.run(jobs);
57
+ process.exit(success ? 0 : 1);
58
+
59
+ } catch (error: any) {
60
+ console.error(chalk.red('Error:'), error.message);
61
+ process.exit(1);
62
+ }
63
+ });
64
+ }
@@ -0,0 +1,10 @@
1
+ import fs from 'node:fs/promises';
2
+
3
+ export async function exists(path: string): Promise<boolean> {
4
+ try {
5
+ await fs.stat(path);
6
+ return true;
7
+ } catch {
8
+ return false;
9
+ }
10
+ }
@@ -0,0 +1,129 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { loadConfig } from './loader.js';
5
+
6
+ const TEST_DIR = path.join(process.cwd(), 'test-env-' + Date.now());
7
+ const GAUNTLET_DIR = path.join(TEST_DIR, '.gauntlet');
8
+ const CHECKS_DIR = path.join(GAUNTLET_DIR, 'checks');
9
+ const REVIEWS_DIR = path.join(GAUNTLET_DIR, 'reviews');
10
+
11
+ describe('Config Loader', () => {
12
+ beforeAll(async () => {
13
+ // Setup directory structure
14
+ await fs.mkdir(TEST_DIR);
15
+ await fs.mkdir(GAUNTLET_DIR);
16
+ await fs.mkdir(CHECKS_DIR);
17
+ await fs.mkdir(REVIEWS_DIR);
18
+
19
+ // Write config.yml
20
+ await fs.writeFile(path.join(GAUNTLET_DIR, 'config.yml'), `
21
+ base_branch: origin/dev
22
+ log_dir: test_logs
23
+ cli:
24
+ default_preference:
25
+ - claude
26
+ - gemini
27
+ check_usage_limit: false
28
+ entry_points:
29
+ - path: src/
30
+ checks:
31
+ - lint
32
+ reviews:
33
+ - security
34
+ `);
35
+
36
+ // Write a check definition
37
+ await fs.writeFile(path.join(CHECKS_DIR, 'lint.yml'), `
38
+ name: lint
39
+ command: npm run lint
40
+ working_directory: .
41
+ `);
42
+
43
+ // Write a review definition
44
+ await fs.writeFile(path.join(REVIEWS_DIR, 'security.md'), `---
45
+ cli_preference:
46
+ - gemini
47
+ ---
48
+
49
+ # Security Review
50
+ Check for vulnerabilities.
51
+ `);
52
+
53
+ // Write a review definition without preference
54
+ await fs.writeFile(path.join(REVIEWS_DIR, 'style.md'), `---
55
+ num_reviews: 1
56
+ ---
57
+
58
+ # Style Review
59
+ Check style.
60
+ `);
61
+ });
62
+
63
+ afterAll(async () => {
64
+ // Cleanup
65
+ await fs.rm(TEST_DIR, { recursive: true, force: true });
66
+ });
67
+
68
+ it('should load project configuration correctly', async () => {
69
+ const config = await loadConfig(TEST_DIR);
70
+
71
+ expect(config.project.base_branch).toBe('origin/dev');
72
+ expect(config.project.log_dir).toBe('test_logs');
73
+ expect(config.project.entry_points).toHaveLength(1);
74
+ expect(config.project.entry_points[0].path).toBe('src/');
75
+ });
76
+
77
+ it('should load check gates correctly', async () => {
78
+ const config = await loadConfig(TEST_DIR);
79
+
80
+ expect(Object.keys(config.checks)).toContain('lint');
81
+ expect(config.checks['lint'].command).toBe('npm run lint');
82
+ });
83
+
84
+ it('should load review gates correctly', async () => {
85
+ const config = await loadConfig(TEST_DIR);
86
+
87
+ expect(Object.keys(config.reviews)).toContain('security');
88
+ expect(config.reviews['security'].name).toBe('security');
89
+ expect(config.reviews['security'].cli_preference).toEqual(['gemini']);
90
+ expect(config.reviews['security'].promptContent).toContain('Check for vulnerabilities.');
91
+ });
92
+
93
+ it('should merge default cli preference', async () => {
94
+ const config = await loadConfig(TEST_DIR);
95
+
96
+ expect(Object.keys(config.reviews)).toContain('style');
97
+ expect(config.reviews['style'].cli_preference).toEqual(['claude', 'gemini']);
98
+ });
99
+
100
+ it('should reject check gate with fail_fast when parallel is true', async () => {
101
+ await fs.writeFile(path.join(CHECKS_DIR, 'invalid.yml'), `
102
+ name: invalid
103
+ command: echo test
104
+ parallel: true
105
+ fail_fast: true
106
+ `);
107
+
108
+ await expect(loadConfig(TEST_DIR)).rejects.toThrow(/fail_fast can only be used when parallel is false/);
109
+ });
110
+
111
+ it('should accept check gate with fail_fast when parallel is false', async () => {
112
+ // Clean up the invalid file first
113
+ try {
114
+ await fs.unlink(path.join(CHECKS_DIR, 'invalid.yml'));
115
+ } catch {}
116
+
117
+ await fs.writeFile(path.join(CHECKS_DIR, 'valid.yml'), `
118
+ name: valid
119
+ command: echo test
120
+ parallel: false
121
+ fail_fast: true
122
+ `);
123
+
124
+ const config = await loadConfig(TEST_DIR);
125
+ expect(config.checks['valid']).toBeDefined();
126
+ expect(config.checks['valid'].fail_fast).toBe(true);
127
+ expect(config.checks['valid'].parallel).toBe(false);
128
+ });
129
+ });
@@ -0,0 +1,130 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import YAML from 'yaml';
4
+ import matter from 'gray-matter';
5
+ import {
6
+ gauntletConfigSchema,
7
+ checkGateSchema,
8
+ reviewPromptFrontmatterSchema
9
+ } from './schema.js';
10
+ import { LoadedConfig, CheckGateConfig } from './types.js';
11
+
12
+ const GAUNTLET_DIR = '.gauntlet';
13
+ const CONFIG_FILE = 'config.yml';
14
+ const CHECKS_DIR = 'checks';
15
+ const REVIEWS_DIR = 'reviews';
16
+
17
+ export async function loadConfig(rootDir: string = process.cwd()): Promise<LoadedConfig> {
18
+ const gauntletPath = path.join(rootDir, GAUNTLET_DIR);
19
+ const configPath = path.join(gauntletPath, CONFIG_FILE);
20
+
21
+ // 1. Load project config
22
+ if (!(await fileExists(configPath))) {
23
+ throw new Error(`Configuration file not found at ${configPath}`);
24
+ }
25
+
26
+ const configContent = await fs.readFile(configPath, 'utf-8');
27
+ const projectConfigRaw = YAML.parse(configContent);
28
+ const projectConfig = gauntletConfigSchema.parse(projectConfigRaw);
29
+
30
+ // 2. Load checks
31
+ const checksPath = path.join(gauntletPath, CHECKS_DIR);
32
+ const checks: Record<string, CheckGateConfig> = {};
33
+
34
+ if (await dirExists(checksPath)) {
35
+ const checkFiles = await fs.readdir(checksPath);
36
+ for (const file of checkFiles) {
37
+ if (file.endsWith('.yml') || file.endsWith('.yaml')) {
38
+ const filePath = path.join(checksPath, file);
39
+ const content = await fs.readFile(filePath, 'utf-8');
40
+ const raw = YAML.parse(content);
41
+ // Ensure name matches filename if not provided or just use filename as key
42
+ const parsed = checkGateSchema.parse(raw);
43
+ checks[parsed.name] = parsed;
44
+ }
45
+ }
46
+ }
47
+
48
+ // 3. Load reviews (prompts + frontmatter)
49
+ const reviewsPath = path.join(gauntletPath, REVIEWS_DIR);
50
+ const reviews: Record<string, any> = {};
51
+
52
+ if (await dirExists(reviewsPath)) {
53
+ const reviewFiles = await fs.readdir(reviewsPath);
54
+ for (const file of reviewFiles) {
55
+ if (file.endsWith('.md')) {
56
+ const filePath = path.join(reviewsPath, file);
57
+ const content = await fs.readFile(filePath, 'utf-8');
58
+ const { data: frontmatter, content: promptBody } = matter(content);
59
+
60
+ const parsedFrontmatter = reviewPromptFrontmatterSchema.parse(frontmatter);
61
+ const name = path.basename(file, '.md');
62
+
63
+ reviews[name] = {
64
+ name,
65
+ prompt: file, // Store filename relative to reviews dir
66
+ promptContent: promptBody, // Store the actual prompt content for easy access
67
+ ...parsedFrontmatter
68
+ };
69
+
70
+ // Merge default CLI preference if not specified
71
+ if (!reviews[name].cli_preference) {
72
+ reviews[name].cli_preference = projectConfig.cli.default_preference;
73
+ } else {
74
+ // Validate that specified preferences are allowed by project config
75
+ const allowedTools = new Set(projectConfig.cli.default_preference);
76
+ for (const tool of reviews[name].cli_preference) {
77
+ if (!allowedTools.has(tool)) {
78
+ throw new Error(`Review "${name}" uses CLI tool "${tool}" which is not in the project-level allowed list (cli.default_preference).`);
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ // 4. Validate entry point references
87
+ const checkNames = new Set(Object.keys(checks));
88
+ const reviewNames = new Set(Object.keys(reviews));
89
+
90
+ for (const entryPoint of projectConfig.entry_points) {
91
+ if (entryPoint.checks) {
92
+ for (const checkName of entryPoint.checks) {
93
+ if (!checkNames.has(checkName)) {
94
+ throw new Error(`Entry point "${entryPoint.path}" references non-existent check gate: "${checkName}"`);
95
+ }
96
+ }
97
+ }
98
+ if (entryPoint.reviews) {
99
+ for (const reviewName of entryPoint.reviews) {
100
+ if (!reviewNames.has(reviewName)) {
101
+ throw new Error(`Entry point "${entryPoint.path}" references non-existent review gate: "${reviewName}"`);
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ return {
108
+ project: projectConfig,
109
+ checks,
110
+ reviews,
111
+ };
112
+ }
113
+
114
+ async function fileExists(path: string): Promise<boolean> {
115
+ try {
116
+ const stat = await fs.stat(path);
117
+ return stat.isFile();
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
122
+
123
+ async function dirExists(path: string): Promise<boolean> {
124
+ try {
125
+ const stat = await fs.stat(path);
126
+ return stat.isDirectory();
127
+ } catch {
128
+ return false;
129
+ }
130
+ }
@@ -0,0 +1,63 @@
1
+ import { z } from 'zod';
2
+
3
+ export const cliConfigSchema = z.object({
4
+ default_preference: z.array(z.string().min(1)).min(1),
5
+ check_usage_limit: z.boolean().default(false),
6
+ });
7
+
8
+ export const checkGateSchema = z.object({
9
+ name: z.string().min(1),
10
+ command: z.string().min(1),
11
+ working_directory: z.string().optional(),
12
+ parallel: z.boolean().default(false),
13
+ run_in_ci: z.boolean().default(true),
14
+ run_locally: z.boolean().default(true),
15
+ timeout: z.number().optional(),
16
+ fail_fast: z.boolean().optional(),
17
+ }).refine(
18
+ (data) => {
19
+ // fail_fast can only be used when parallel is false
20
+ if (data.fail_fast === true && data.parallel === true) {
21
+ return false;
22
+ }
23
+ return true;
24
+ },
25
+ {
26
+ message: "fail_fast can only be used when parallel is false",
27
+ }
28
+ );
29
+
30
+ export const reviewGateSchema = z.object({
31
+ name: z.string().min(1),
32
+ prompt: z.string().min(1), // Path relative to .gauntlet/reviews/
33
+ cli_preference: z.array(z.string().min(1)).optional(),
34
+ num_reviews: z.number().default(1),
35
+ parallel: z.boolean().default(true),
36
+ run_in_ci: z.boolean().default(true),
37
+ run_locally: z.boolean().default(true),
38
+ timeout: z.number().optional(),
39
+ });
40
+
41
+ export const reviewPromptFrontmatterSchema = z.object({
42
+ model: z.string().optional(),
43
+ cli_preference: z.array(z.string().min(1)).optional(),
44
+ num_reviews: z.number().default(1),
45
+ parallel: z.boolean().default(true),
46
+ run_in_ci: z.boolean().default(true),
47
+ run_locally: z.boolean().default(true),
48
+ timeout: z.number().optional(),
49
+ });
50
+
51
+ export const entryPointSchema = z.object({
52
+ path: z.string().min(1),
53
+ checks: z.array(z.string().min(1)).optional(),
54
+ reviews: z.array(z.string().min(1)).optional(),
55
+ });
56
+
57
+ export const gauntletConfigSchema = z.object({
58
+ base_branch: z.string().min(1).default('origin/main'),
59
+ log_dir: z.string().min(1).default('.gauntlet_logs'),
60
+ allow_parallel: z.boolean().default(true),
61
+ cli: cliConfigSchema,
62
+ entry_points: z.array(entryPointSchema).min(1),
63
+ });
@@ -0,0 +1,23 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ checkGateSchema,
4
+ reviewGateSchema,
5
+ reviewPromptFrontmatterSchema,
6
+ entryPointSchema,
7
+ gauntletConfigSchema,
8
+ cliConfigSchema
9
+ } from './schema.js';
10
+
11
+ export type CheckGateConfig = z.infer<typeof checkGateSchema>;
12
+ export type ReviewGateConfig = z.infer<typeof reviewGateSchema>;
13
+ export type ReviewPromptFrontmatter = z.infer<typeof reviewPromptFrontmatterSchema>;
14
+ export type EntryPointConfig = z.infer<typeof entryPointSchema>;
15
+ export type GauntletConfig = z.infer<typeof gauntletConfigSchema>;
16
+ export type CLIConfig = z.infer<typeof cliConfigSchema>;
17
+
18
+ // Combined type for the fully loaded configuration
19
+ export interface LoadedConfig {
20
+ project: GauntletConfig;
21
+ checks: Record<string, CheckGateConfig>;
22
+ reviews: Record<string, ReviewGateConfig & ReviewPromptFrontmatter>; // Merged with frontmatter
23
+ }