agent-gauntlet 0.2.2 → 0.4.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 (40) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/src/cli-adapters/claude.ts +13 -1
  4. package/src/cli-adapters/gemini.ts +17 -2
  5. package/src/commands/check.ts +108 -12
  6. package/src/commands/ci/list-jobs.ts +3 -2
  7. package/src/commands/clean.ts +29 -0
  8. package/src/commands/help.ts +1 -1
  9. package/src/commands/index.ts +2 -1
  10. package/src/commands/init.ts +4 -4
  11. package/src/commands/review.ts +108 -12
  12. package/src/commands/run.ts +109 -12
  13. package/src/commands/shared.ts +56 -10
  14. package/src/commands/validate.ts +20 -0
  15. package/src/config/schema.ts +5 -0
  16. package/src/config/validator.ts +6 -13
  17. package/src/core/change-detector.ts +1 -0
  18. package/src/core/entry-point.ts +48 -7
  19. package/src/core/runner.ts +90 -56
  20. package/src/gates/result.ts +32 -0
  21. package/src/gates/review.ts +428 -162
  22. package/src/index.ts +4 -2
  23. package/src/output/console-log.ts +146 -0
  24. package/src/output/console.ts +103 -9
  25. package/src/output/logger.ts +52 -8
  26. package/src/templates/run_gauntlet.template.md +20 -13
  27. package/src/utils/log-parser.ts +498 -162
  28. package/src/utils/session-ref.ts +82 -0
  29. package/src/commands/check.test.ts +0 -29
  30. package/src/commands/detect.test.ts +0 -43
  31. package/src/commands/health.test.ts +0 -93
  32. package/src/commands/help.test.ts +0 -44
  33. package/src/commands/init.test.ts +0 -130
  34. package/src/commands/list.test.ts +0 -121
  35. package/src/commands/rerun.ts +0 -160
  36. package/src/commands/review.test.ts +0 -31
  37. package/src/commands/run.test.ts +0 -27
  38. package/src/config/loader.test.ts +0 -151
  39. package/src/core/entry-point.test.ts +0 -61
  40. package/src/gates/review.test.ts +0 -291
@@ -0,0 +1,82 @@
1
+ import { exec } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+
6
+ const SESSION_REF_FILENAME = ".session_ref";
7
+
8
+ // Exported for testing - allows injection of mock exec
9
+ export let execFn: (
10
+ cmd: string,
11
+ ) => Promise<{ stdout: string; stderr: string }> = promisify(exec);
12
+
13
+ /**
14
+ * Set the exec function (for testing)
15
+ */
16
+ export function setExecFn(
17
+ fn: (cmd: string) => Promise<{ stdout: string; stderr: string }>,
18
+ ): void {
19
+ execFn = fn;
20
+ }
21
+
22
+ /**
23
+ * Reset the exec function to the real implementation
24
+ */
25
+ export function resetExecFn(): void {
26
+ execFn = promisify(exec);
27
+ }
28
+
29
+ /**
30
+ * Captures the current git state (working tree) as a commit SHA
31
+ * and writes it to the log directory.
32
+ * Uses `git stash create --include-untracked` to capture the state without modifying it.
33
+ */
34
+ export async function writeSessionRef(logDir: string): Promise<void> {
35
+ try {
36
+ // Create a stash of the current state (including untracked files)
37
+ // This returns a commit SHA but doesn't modify the working tree
38
+ const { stdout } = await execFn("git stash create --include-untracked");
39
+ let sha = stdout.trim();
40
+
41
+ if (!sha) {
42
+ // If no changes to stash (clean working tree), use HEAD
43
+ const { stdout: headSha } = await execFn("git rev-parse HEAD");
44
+ sha = headSha.trim();
45
+ }
46
+
47
+ // Ensure log directory exists
48
+ await fs.mkdir(logDir, { recursive: true });
49
+ await fs.writeFile(path.join(logDir, SESSION_REF_FILENAME), sha);
50
+ } catch (error) {
51
+ console.warn(
52
+ "Failed to create session reference:",
53
+ error instanceof Error ? error.message : String(error),
54
+ );
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Reads the stored session reference SHA from the log directory.
60
+ * Returns null if the file doesn't exist.
61
+ */
62
+ export async function readSessionRef(logDir: string): Promise<string | null> {
63
+ try {
64
+ const refPath = path.join(logDir, SESSION_REF_FILENAME);
65
+ const content = await fs.readFile(refPath, "utf-8");
66
+ return content.trim();
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Removes the session reference file from the log directory.
74
+ */
75
+ export async function clearSessionRef(logDir: string): Promise<void> {
76
+ try {
77
+ const refPath = path.join(logDir, SESSION_REF_FILENAME);
78
+ await fs.rm(refPath, { force: true });
79
+ } catch {
80
+ // Ignore errors
81
+ }
82
+ }
@@ -1,29 +0,0 @@
1
- import { beforeEach, describe, expect, it } from "bun:test";
2
- import { Command } from "commander";
3
- import { registerCheckCommand } from "./check.js";
4
-
5
- describe("Check Command", () => {
6
- let program: Command;
7
-
8
- beforeEach(() => {
9
- program = new Command();
10
- registerCheckCommand(program);
11
- });
12
-
13
- it("should register the check command", () => {
14
- const checkCmd = program.commands.find((cmd) => cmd.name() === "check");
15
- expect(checkCmd).toBeDefined();
16
- expect(checkCmd?.description()).toBe(
17
- "Run only applicable checks for detected changes",
18
- );
19
- });
20
-
21
- it("should have correct options", () => {
22
- const checkCmd = program.commands.find((cmd) => cmd.name() === "check");
23
- expect(checkCmd?.options.some((opt) => opt.long === "--gate")).toBe(true);
24
- expect(checkCmd?.options.some((opt) => opt.long === "--commit")).toBe(true);
25
- expect(checkCmd?.options.some((opt) => opt.long === "--uncommitted")).toBe(
26
- true,
27
- );
28
- });
29
- });
@@ -1,43 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
- import { Command } from "commander";
3
- import { registerDetectCommand } from "./detect.js";
4
-
5
- describe("Detect Command", () => {
6
- let program: Command;
7
- const originalConsoleLog = console.log;
8
- const originalConsoleError = console.error;
9
- let logs: string[];
10
- let errors: string[];
11
-
12
- beforeEach(() => {
13
- program = new Command();
14
- registerDetectCommand(program);
15
- logs = [];
16
- errors = [];
17
- console.log = (...args: unknown[]) => {
18
- logs.push(args.join(" "));
19
- };
20
- console.error = (...args: unknown[]) => {
21
- errors.push(args.join(" "));
22
- };
23
- });
24
-
25
- afterEach(() => {
26
- console.log = originalConsoleLog;
27
- console.error = originalConsoleError;
28
- });
29
-
30
- it("should register the detect command", () => {
31
- const detectCmd = program.commands.find((cmd) => cmd.name() === "detect");
32
- expect(detectCmd).toBeDefined();
33
- expect(detectCmd?.description()).toBe(
34
- "Show what gates would run for detected changes (without executing them)",
35
- );
36
- expect(detectCmd?.options.some((opt) => opt.long === "--commit")).toBe(
37
- true,
38
- );
39
- expect(detectCmd?.options.some((opt) => opt.long === "--uncommitted")).toBe(
40
- true,
41
- );
42
- });
43
- });
@@ -1,93 +0,0 @@
1
- import {
2
- afterAll,
3
- afterEach,
4
- beforeAll,
5
- beforeEach,
6
- describe,
7
- expect,
8
- it,
9
- } from "bun:test";
10
- import fs from "node:fs/promises";
11
- import path from "node:path";
12
- import { Command } from "commander";
13
- import { registerHealthCommand } from "./health.js";
14
-
15
- const TEST_DIR = path.join(process.cwd(), `test-health-${Date.now()}`);
16
- const GAUNTLET_DIR = path.join(TEST_DIR, ".gauntlet");
17
- const REVIEWS_DIR = path.join(GAUNTLET_DIR, "reviews");
18
-
19
- describe("Health Command", () => {
20
- let program: Command;
21
- const originalConsoleLog = console.log;
22
- const originalCwd = process.cwd();
23
- let logs: string[];
24
-
25
- beforeAll(async () => {
26
- // Setup test directory structure
27
- await fs.mkdir(TEST_DIR, { recursive: true });
28
- await fs.mkdir(GAUNTLET_DIR, { recursive: true });
29
- await fs.mkdir(REVIEWS_DIR, { recursive: true });
30
-
31
- // Write config.yml
32
- await fs.writeFile(
33
- path.join(GAUNTLET_DIR, "config.yml"),
34
- `
35
- base_branch: origin/main
36
- log_dir: gauntlet_logs
37
- cli:
38
- default_preference:
39
- - gemini
40
- check_usage_limit: false
41
- entry_points:
42
- - path: .
43
- `,
44
- );
45
-
46
- // Write review definition with CLI preference
47
- await fs.writeFile(
48
- path.join(REVIEWS_DIR, "security.md"),
49
- `---
50
- cli_preference:
51
- - gemini
52
- ---
53
-
54
- # Security Review
55
- Review for security.
56
- `,
57
- );
58
- });
59
-
60
- afterAll(async () => {
61
- await fs.rm(TEST_DIR, { recursive: true, force: true });
62
- });
63
-
64
- beforeEach(() => {
65
- program = new Command();
66
- registerHealthCommand(program);
67
- logs = [];
68
- console.log = (...args: unknown[]) => {
69
- logs.push(args.join(" "));
70
- };
71
- process.chdir(TEST_DIR);
72
- });
73
-
74
- afterEach(() => {
75
- console.log = originalConsoleLog;
76
- process.chdir(originalCwd);
77
- });
78
-
79
- it("should register the health command", () => {
80
- const healthCmd = program.commands.find((cmd) => cmd.name() === "health");
81
- expect(healthCmd).toBeDefined();
82
- expect(healthCmd?.description()).toBe("Check CLI tool availability");
83
- });
84
-
85
- it("should run health check", async () => {
86
- const healthCmd = program.commands.find((cmd) => cmd.name() === "health");
87
- await healthCmd?.parseAsync(["health"]);
88
-
89
- const output = logs.join("\n");
90
- expect(output).toContain("Config validation:");
91
- expect(output).toContain("CLI Tool Health Check:");
92
- });
93
- });
@@ -1,44 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
- import { Command } from "commander";
3
- import { registerHelpCommand } from "./help.js";
4
-
5
- describe("Help Command", () => {
6
- let program: Command;
7
- const originalConsoleLog = console.log;
8
- let logs: string[];
9
-
10
- beforeEach(() => {
11
- program = new Command();
12
- registerHelpCommand(program);
13
- logs = [];
14
- console.log = (...args: unknown[]) => {
15
- logs.push(args.join(" "));
16
- };
17
- });
18
-
19
- afterEach(() => {
20
- console.log = originalConsoleLog;
21
- });
22
-
23
- it("should register the help command", () => {
24
- const helpCmd = program.commands.find((cmd) => cmd.name() === "help");
25
- expect(helpCmd).toBeDefined();
26
- expect(helpCmd?.description()).toBe("Show help information");
27
- });
28
-
29
- it("should output help information when executed", async () => {
30
- const helpCmd = program.commands.find((cmd) => cmd.name() === "help");
31
- await helpCmd?.parseAsync(["help"]);
32
-
33
- const output = logs.join("\n");
34
- expect(output).toContain("Agent Gauntlet");
35
- expect(output).toContain("Commands:");
36
- expect(output).toContain("run");
37
- expect(output).toContain("check");
38
- expect(output).toContain("review");
39
- expect(output).toContain("detect");
40
- expect(output).toContain("list");
41
- expect(output).toContain("health");
42
- expect(output).toContain("init");
43
- });
44
- });
@@ -1,130 +0,0 @@
1
- import {
2
- afterAll,
3
- afterEach,
4
- beforeAll,
5
- beforeEach,
6
- describe,
7
- expect,
8
- it,
9
- mock,
10
- } from "bun:test";
11
- import fs from "node:fs/promises";
12
- import path from "node:path";
13
- import { Command } from "commander";
14
-
15
- const TEST_DIR = path.join(process.cwd(), `test-init-${Date.now()}`);
16
-
17
- // Mock adapters
18
- const mockAdapters = [
19
- {
20
- name: "mock-cli-1",
21
- isAvailable: async () => true,
22
- getProjectCommandDir: () => ".mock1",
23
- getUserCommandDir: () => null,
24
- getCommandExtension: () => ".sh",
25
- canUseSymlink: () => false,
26
- transformCommand: (content: string) => content,
27
- },
28
- {
29
- name: "mock-cli-2",
30
- isAvailable: async () => false, // Not available
31
- getProjectCommandDir: () => ".mock2",
32
- getUserCommandDir: () => null,
33
- getCommandExtension: () => ".sh",
34
- canUseSymlink: () => false,
35
- transformCommand: (content: string) => content,
36
- },
37
- ];
38
-
39
- mock.module("../cli-adapters/index.js", () => ({
40
- getAllAdapters: () => mockAdapters,
41
- getProjectCommandAdapters: () => mockAdapters,
42
- getUserCommandAdapters: () => [],
43
- getAdapter: (name: string) => mockAdapters.find((a) => a.name === name),
44
- getValidCLITools: () => mockAdapters.map((a) => a.name),
45
- }));
46
-
47
- // Import after mocking
48
- const { registerInitCommand } = await import("./init.js");
49
-
50
- describe("Init Command", () => {
51
- let program: Command;
52
- const originalConsoleLog = console.log;
53
- const originalCwd = process.cwd();
54
- let logs: string[];
55
-
56
- beforeAll(async () => {
57
- await fs.mkdir(TEST_DIR, { recursive: true });
58
- });
59
-
60
- afterAll(async () => {
61
- await fs.rm(TEST_DIR, { recursive: true, force: true });
62
- });
63
-
64
- beforeEach(() => {
65
- program = new Command();
66
- registerInitCommand(program);
67
- logs = [];
68
- console.log = (...args: unknown[]) => {
69
- logs.push(args.join(" "));
70
- };
71
- process.chdir(TEST_DIR);
72
- });
73
-
74
- afterEach(() => {
75
- console.log = originalConsoleLog;
76
- process.chdir(originalCwd);
77
- // Cleanup any created .gauntlet directory
78
- return fs
79
- .rm(path.join(TEST_DIR, ".gauntlet"), { recursive: true, force: true })
80
- .catch(() => {});
81
- });
82
-
83
- it("should register the init command", () => {
84
- const initCmd = program.commands.find((cmd) => cmd.name() === "init");
85
- expect(initCmd).toBeDefined();
86
- expect(initCmd?.description()).toBe("Initialize .gauntlet configuration");
87
- expect(initCmd?.options.some((opt) => opt.long === "--yes")).toBe(true);
88
- });
89
-
90
- it("should create .gauntlet directory structure with --yes flag", async () => {
91
- // We expect it to use the available mock-cli-1
92
- await program.parseAsync(["node", "test", "init", "--yes"]);
93
-
94
- // Check that files were created
95
- const gauntletDir = path.join(TEST_DIR, ".gauntlet");
96
- const configFile = path.join(gauntletDir, "config.yml");
97
- const reviewsDir = path.join(gauntletDir, "reviews");
98
- const checksDir = path.join(gauntletDir, "checks");
99
- const runGauntletFile = path.join(gauntletDir, "run_gauntlet.md");
100
-
101
- expect(await fs.stat(gauntletDir)).toBeDefined();
102
- expect(await fs.stat(configFile)).toBeDefined();
103
- expect(await fs.stat(reviewsDir)).toBeDefined();
104
- expect(await fs.stat(checksDir)).toBeDefined();
105
- expect(await fs.stat(runGauntletFile)).toBeDefined();
106
-
107
- // Verify config content
108
- const configContent = await fs.readFile(configFile, "utf-8");
109
- expect(configContent).toContain("base_branch");
110
- expect(configContent).toContain("log_dir");
111
- expect(configContent).toContain("mock-cli-1"); // Should be present
112
- expect(configContent).not.toContain("mock-cli-2"); // Should not be present (unavailable)
113
-
114
- // Verify review file content
115
- const reviewFile = path.join(reviewsDir, "code-quality.md");
116
- const reviewContent = await fs.readFile(reviewFile, "utf-8");
117
- expect(reviewContent).toContain("mock-cli-1");
118
- });
119
-
120
- it("should not create directory if .gauntlet already exists", async () => {
121
- // Create .gauntlet directory first
122
- const gauntletDir = path.join(TEST_DIR, ".gauntlet");
123
- await fs.mkdir(gauntletDir, { recursive: true });
124
-
125
- await program.parseAsync(["node", "test", "init", "--yes"]);
126
-
127
- const output = logs.join("\n");
128
- expect(output).toContain(".gauntlet directory already exists");
129
- });
130
- });
@@ -1,121 +0,0 @@
1
- import {
2
- afterAll,
3
- afterEach,
4
- beforeAll,
5
- beforeEach,
6
- describe,
7
- expect,
8
- it,
9
- } from "bun:test";
10
- import fs from "node:fs/promises";
11
- import path from "node:path";
12
- import { Command } from "commander";
13
- import { registerListCommand } from "./list.js";
14
-
15
- const TEST_DIR = path.join(process.cwd(), `test-list-${Date.now()}`);
16
- const GAUNTLET_DIR = path.join(TEST_DIR, ".gauntlet");
17
- const CHECKS_DIR = path.join(GAUNTLET_DIR, "checks");
18
- const REVIEWS_DIR = path.join(GAUNTLET_DIR, "reviews");
19
-
20
- describe("List Command", () => {
21
- let program: Command;
22
- const originalConsoleLog = console.log;
23
- const originalConsoleError = console.error;
24
- const originalCwd = process.cwd();
25
- let logs: string[];
26
- let errors: string[];
27
-
28
- beforeAll(async () => {
29
- // Setup test directory structure
30
- await fs.mkdir(TEST_DIR, { recursive: true });
31
- await fs.mkdir(GAUNTLET_DIR, { recursive: true });
32
- await fs.mkdir(CHECKS_DIR, { recursive: true });
33
- await fs.mkdir(REVIEWS_DIR, { recursive: true });
34
-
35
- // Write config.yml
36
- await fs.writeFile(
37
- path.join(GAUNTLET_DIR, "config.yml"),
38
- `
39
- base_branch: origin/main
40
- log_dir: gauntlet_logs
41
- cli:
42
- default_preference:
43
- - gemini
44
- check_usage_limit: false
45
- entry_points:
46
- - path: src/
47
- checks:
48
- - lint
49
- reviews:
50
- - security
51
- `,
52
- );
53
-
54
- // Write check definition
55
- await fs.writeFile(
56
- path.join(CHECKS_DIR, "lint.yml"),
57
- `
58
- name: lint
59
- command: npm run lint
60
- working_directory: .
61
- `,
62
- );
63
-
64
- // Write review definition
65
- await fs.writeFile(
66
- path.join(REVIEWS_DIR, "security.md"),
67
- `---
68
- cli_preference:
69
- - gemini
70
- ---
71
-
72
- # Security Review
73
- Review for security.
74
- `,
75
- );
76
- });
77
-
78
- afterAll(async () => {
79
- await fs.rm(TEST_DIR, { recursive: true, force: true });
80
- });
81
-
82
- beforeEach(() => {
83
- program = new Command();
84
- registerListCommand(program);
85
- logs = [];
86
- errors = [];
87
- console.log = (...args: unknown[]) => {
88
- logs.push(args.join(" "));
89
- };
90
- console.error = (...args: unknown[]) => {
91
- errors.push(args.join(" "));
92
- };
93
- process.chdir(TEST_DIR);
94
- });
95
-
96
- afterEach(() => {
97
- console.log = originalConsoleLog;
98
- console.error = originalConsoleError;
99
- process.chdir(originalCwd);
100
- });
101
-
102
- it("should register the list command", () => {
103
- const listCmd = program.commands.find((cmd) => cmd.name() === "list");
104
- expect(listCmd).toBeDefined();
105
- expect(listCmd?.description()).toBe("List configured gates");
106
- });
107
-
108
- it("should list check gates, review gates, and entry points", async () => {
109
- const listCmd = program.commands.find((cmd) => cmd.name() === "list");
110
- await listCmd?.parseAsync(["list"]);
111
-
112
- const output = logs.join("\n");
113
- expect(output).toContain("Check Gates:");
114
- expect(output).toContain("lint");
115
- expect(output).toContain("Review Gates:");
116
- expect(output).toContain("security");
117
- expect(output).toContain("gemini");
118
- expect(output).toContain("Entry Points:");
119
- expect(output).toContain("src/");
120
- });
121
- });
@@ -1,160 +0,0 @@
1
- import chalk from "chalk";
2
- import type { Command } from "commander";
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 { ConsoleReporter } from "../output/console.js";
9
- import { Logger } from "../output/logger.js";
10
- import {
11
- findPreviousFailures,
12
- type PreviousViolation,
13
- } from "../utils/log-parser.js";
14
- import { rotateLogs } from "./shared.js";
15
-
16
- export function registerRerunCommand(program: Command): void {
17
- program
18
- .command("rerun")
19
- .description(
20
- "Rerun gates (checks & reviews) with previous failures as context (defaults to uncommitted changes)",
21
- )
22
- .option(
23
- "-b, --base-branch <branch>",
24
- "Override base branch for change detection",
25
- )
26
- .option("-g, --gate <name>", "Run specific gate only")
27
- .option(
28
- "-c, --commit <sha>",
29
- "Use diff for a specific commit (overrides default uncommitted mode)",
30
- )
31
- .action(async (options) => {
32
- try {
33
- const config = await loadConfig();
34
-
35
- // Parse previous failures from log files (only for review gates)
36
- console.log(chalk.dim("Analyzing previous runs..."));
37
-
38
- // findPreviousFailures handles errors internally and returns empty array on failure
39
- const previousFailures = await findPreviousFailures(
40
- config.project.log_dir,
41
- options.gate,
42
- );
43
-
44
- // Create a map: jobId -> (adapterName -> violations)
45
- const failuresMap = new Map<string, Map<string, PreviousViolation[]>>();
46
- for (const gateFailure of previousFailures) {
47
- const adapterMap = new Map<string, PreviousViolation[]>();
48
- for (const adapterFailure of gateFailure.adapterFailures) {
49
- adapterMap.set(
50
- adapterFailure.adapterName,
51
- adapterFailure.violations,
52
- );
53
- }
54
- failuresMap.set(gateFailure.jobId, adapterMap);
55
- }
56
-
57
- if (previousFailures.length > 0) {
58
- const totalViolations = previousFailures.reduce(
59
- (sum, gf) =>
60
- sum +
61
- gf.adapterFailures.reduce((s, af) => s + af.violations.length, 0),
62
- 0,
63
- );
64
- console.log(
65
- chalk.yellow(
66
- `Found ${previousFailures.length} gate(s) with ${totalViolations} previous violation(s)`,
67
- ),
68
- );
69
- } else {
70
- console.log(
71
- chalk.dim("No previous failures found. Running as normal..."),
72
- );
73
- }
74
-
75
- // Rotate logs before starting the new run
76
- await rotateLogs(config.project.log_dir);
77
-
78
- // Determine effective base branch
79
- // Priority: CLI override > CI env var > config
80
- const effectiveBaseBranch =
81
- options.baseBranch ||
82
- (process.env.GITHUB_BASE_REF &&
83
- (process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true")
84
- ? process.env.GITHUB_BASE_REF
85
- : null) ||
86
- config.project.base_branch;
87
-
88
- // Detect changes (default to uncommitted unless --commit is specified)
89
- // Note: Rerun defaults to uncommitted changes for faster iteration loops,
90
- // unlike 'run' which defaults to base_branch comparison.
91
- const changeOptions = {
92
- commit: options.commit,
93
- uncommitted: !options.commit, // Default to uncommitted unless commit is specified
94
- };
95
-
96
- const changeDetector = new ChangeDetector(
97
- effectiveBaseBranch,
98
- changeOptions,
99
- );
100
- const expander = new EntryPointExpander();
101
- const jobGen = new JobGenerator(config);
102
-
103
- const modeDesc = options.commit
104
- ? `commit ${options.commit}`
105
- : "uncommitted changes";
106
- console.log(chalk.dim(`Detecting changes (${modeDesc})...`));
107
-
108
- const changes = await changeDetector.getChangedFiles();
109
-
110
- if (changes.length === 0) {
111
- console.log(chalk.green("No changes detected."));
112
- process.exit(0);
113
- }
114
-
115
- console.log(chalk.dim(`Found ${changes.length} changed files.`));
116
-
117
- const entryPoints = await expander.expand(
118
- config.project.entry_points,
119
- changes,
120
- );
121
- let jobs = jobGen.generateJobs(entryPoints);
122
-
123
- if (options.gate) {
124
- jobs = jobs.filter((j) => j.name === options.gate);
125
- }
126
-
127
- if (jobs.length === 0) {
128
- console.log(chalk.yellow("No applicable gates for these changes."));
129
- process.exit(0);
130
- }
131
-
132
- console.log(chalk.dim(`Running ${jobs.length} gates...`));
133
- if (previousFailures.length > 0) {
134
- console.log(
135
- chalk.dim(
136
- "Previous failures will be injected as context for matching reviewers.",
137
- ),
138
- );
139
- }
140
-
141
- const logger = new Logger(config.project.log_dir);
142
- const reporter = new ConsoleReporter();
143
- const runner = new Runner(
144
- config,
145
- logger,
146
- reporter,
147
- failuresMap, // Pass previous failures map
148
- changeOptions, // Pass change detection options
149
- effectiveBaseBranch, // Pass effective base branch
150
- );
151
-
152
- const success = await runner.run(jobs);
153
- process.exit(success ? 0 : 1);
154
- } catch (error: unknown) {
155
- const err = error as { message?: string };
156
- console.error(chalk.red("Error:"), err.message);
157
- process.exit(1);
158
- }
159
- });
160
- }