agent-gauntlet 0.1.10 → 0.1.12

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 (55) hide show
  1. package/README.md +55 -87
  2. package/package.json +4 -2
  3. package/src/bun-plugins.d.ts +4 -0
  4. package/src/cli-adapters/claude.ts +139 -108
  5. package/src/cli-adapters/codex.ts +141 -117
  6. package/src/cli-adapters/cursor.ts +152 -0
  7. package/src/cli-adapters/gemini.ts +171 -139
  8. package/src/cli-adapters/github-copilot.ts +153 -0
  9. package/src/cli-adapters/index.ts +77 -48
  10. package/src/commands/check.test.ts +24 -20
  11. package/src/commands/check.ts +86 -59
  12. package/src/commands/ci/index.ts +15 -0
  13. package/src/commands/ci/init.ts +96 -0
  14. package/src/commands/ci/list-jobs.ts +78 -0
  15. package/src/commands/detect.test.ts +38 -32
  16. package/src/commands/detect.ts +89 -61
  17. package/src/commands/health.test.ts +67 -53
  18. package/src/commands/health.ts +167 -145
  19. package/src/commands/help.test.ts +37 -37
  20. package/src/commands/help.ts +31 -22
  21. package/src/commands/index.ts +10 -9
  22. package/src/commands/init.test.ts +120 -107
  23. package/src/commands/init.ts +514 -417
  24. package/src/commands/list.test.ts +87 -70
  25. package/src/commands/list.ts +28 -24
  26. package/src/commands/rerun.ts +157 -119
  27. package/src/commands/review.test.ts +26 -20
  28. package/src/commands/review.ts +86 -59
  29. package/src/commands/run.test.ts +22 -20
  30. package/src/commands/run.ts +85 -58
  31. package/src/commands/shared.ts +44 -35
  32. package/src/config/ci-loader.ts +33 -0
  33. package/src/config/ci-schema.ts +52 -0
  34. package/src/config/loader.test.ts +112 -90
  35. package/src/config/loader.ts +132 -123
  36. package/src/config/schema.ts +48 -47
  37. package/src/config/types.ts +28 -13
  38. package/src/config/validator.ts +521 -454
  39. package/src/core/change-detector.ts +122 -104
  40. package/src/core/entry-point.test.ts +60 -62
  41. package/src/core/entry-point.ts +120 -74
  42. package/src/core/job.ts +69 -59
  43. package/src/core/runner.ts +264 -230
  44. package/src/gates/check.ts +78 -69
  45. package/src/gates/result.ts +7 -7
  46. package/src/gates/review.test.ts +277 -138
  47. package/src/gates/review.ts +724 -561
  48. package/src/index.ts +18 -15
  49. package/src/output/console.ts +253 -214
  50. package/src/output/logger.ts +66 -52
  51. package/src/templates/run_gauntlet.template.md +18 -0
  52. package/src/templates/workflow.yml +77 -0
  53. package/src/utils/diff-parser.ts +64 -62
  54. package/src/utils/log-parser.ts +227 -206
  55. package/src/utils/sanitizer.ts +1 -1
@@ -1,112 +1,130 @@
1
- import { exec } from 'node:child_process';
2
- import { promisify } from 'node:util';
1
+ import { exec } from "node:child_process";
2
+ import { promisify } from "node:util";
3
3
 
4
4
  const execAsync = promisify(exec);
5
5
 
6
6
  export interface ChangeDetectorOptions {
7
- commit?: string; // If provided, get diff for this commit vs its parent
8
- uncommitted?: boolean; // If true, only get uncommitted changes (staged + unstaged)
7
+ commit?: string; // If provided, get diff for this commit vs its parent
8
+ uncommitted?: boolean; // If true, only get uncommitted changes (staged + unstaged)
9
9
  }
10
10
 
11
11
  export class ChangeDetector {
12
- constructor(
13
- private baseBranch: string = 'origin/main',
14
- private options: ChangeDetectorOptions = {}
15
- ) {}
16
-
17
- async getChangedFiles(): Promise<string[]> {
18
- // If commit option is provided, use that
19
- if (this.options.commit) {
20
- return this.getCommitChangedFiles(this.options.commit);
21
- }
22
-
23
- // If uncommitted option is provided, only get uncommitted changes
24
- if (this.options.uncommitted) {
25
- return this.getUncommittedChangedFiles();
26
- }
27
-
28
- const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
29
-
30
- if (isCI) {
31
- return this.getCIChangedFiles();
32
- } else {
33
- return this.getLocalChangedFiles();
34
- }
35
- }
36
-
37
- private async getCIChangedFiles(): Promise<string[]> {
38
- // In GitHub Actions, GITHUB_BASE_REF is the target branch (e.g., main)
39
- // GITHUB_SHA is the commit being built
40
- const baseRef = process.env.GITHUB_BASE_REF || this.baseBranch;
41
- const headRef = process.env.GITHUB_SHA || 'HEAD';
42
-
43
- // We might need to fetch first in some shallow clones, but assuming strictly for now
44
- // git diff --name-only base...head
45
- try {
46
- const { stdout } = await execAsync(`git diff --name-only ${baseRef}...${headRef}`);
47
- return this.parseOutput(stdout);
48
- } catch (error) {
49
- console.warn('Failed to detect changes via git diff in CI, falling back to HEAD^...HEAD', error);
50
- // Fallback for push events where base ref might not be available
51
- const { stdout } = await execAsync('git diff --name-only HEAD^...HEAD');
52
- return this.parseOutput(stdout);
53
- }
54
- }
55
-
56
- private async getLocalChangedFiles(): Promise<string[]> {
57
- // 1. Committed changes relative to base branch
58
- const { stdout: committed } = await execAsync(`git diff --name-only ${this.baseBranch}...HEAD`);
59
-
60
- // 2. Uncommitted changes (staged and unstaged)
61
- const { stdout: uncommitted } = await execAsync('git diff --name-only HEAD');
62
-
63
- // 3. Untracked files
64
- const { stdout: untracked } = await execAsync('git ls-files --others --exclude-standard');
65
-
66
- const files = new Set([
67
- ...this.parseOutput(committed),
68
- ...this.parseOutput(uncommitted),
69
- ...this.parseOutput(untracked)
70
- ]);
71
-
72
- return Array.from(files);
73
- }
74
-
75
- private async getCommitChangedFiles(commit: string): Promise<string[]> {
76
- // Get diff for commit vs its parent
77
- try {
78
- const { stdout } = await execAsync(`git diff --name-only ${commit}^..${commit}`);
79
- return this.parseOutput(stdout);
80
- } catch (error) {
81
- // If commit has no parent (initial commit), just get files in that commit
82
- try {
83
- const { stdout } = await execAsync(`git diff --name-only --root ${commit}`);
84
- return this.parseOutput(stdout);
85
- } catch {
86
- throw new Error(`Failed to get changes for commit ${commit}`);
87
- }
88
- }
89
- }
90
-
91
- private async getUncommittedChangedFiles(): Promise<string[]> {
92
- // Get uncommitted changes (staged + unstaged) and untracked files
93
- const { stdout: staged } = await execAsync('git diff --name-only --cached');
94
- const { stdout: unstaged } = await execAsync('git diff --name-only');
95
- const { stdout: untracked } = await execAsync('git ls-files --others --exclude-standard');
96
-
97
- const files = new Set([
98
- ...this.parseOutput(staged),
99
- ...this.parseOutput(unstaged),
100
- ...this.parseOutput(untracked)
101
- ]);
102
-
103
- return Array.from(files);
104
- }
105
-
106
- private parseOutput(stdout: string): string[] {
107
- return stdout
108
- .split('\n')
109
- .map(line => line.trim())
110
- .filter(line => line.length > 0);
111
- }
12
+ constructor(
13
+ private baseBranch: string = "origin/main",
14
+ private options: ChangeDetectorOptions = {},
15
+ ) {}
16
+
17
+ async getChangedFiles(): Promise<string[]> {
18
+ // If commit option is provided, use that
19
+ if (this.options.commit) {
20
+ return this.getCommitChangedFiles(this.options.commit);
21
+ }
22
+
23
+ // If uncommitted option is provided, only get uncommitted changes
24
+ if (this.options.uncommitted) {
25
+ return this.getUncommittedChangedFiles();
26
+ }
27
+
28
+ const isCI =
29
+ process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
30
+
31
+ if (isCI) {
32
+ return this.getCIChangedFiles();
33
+ } else {
34
+ return this.getLocalChangedFiles();
35
+ }
36
+ }
37
+
38
+ private async getCIChangedFiles(): Promise<string[]> {
39
+ // In GitHub Actions, GITHUB_SHA is the commit being built
40
+ // Base branch priority is already resolved by caller
41
+ const baseRef = this.baseBranch;
42
+ const headRef = process.env.GITHUB_SHA || "HEAD";
43
+
44
+ // We might need to fetch first in some shallow clones, but assuming strictly for now
45
+ // git diff --name-only base...head
46
+ try {
47
+ const { stdout } = await execAsync(
48
+ `git diff --name-only ${baseRef}...${headRef}`,
49
+ );
50
+ return this.parseOutput(stdout);
51
+ } catch (error) {
52
+ console.warn(
53
+ "Failed to detect changes via git diff in CI, falling back to HEAD^...HEAD",
54
+ error,
55
+ );
56
+ // Fallback for push events where base ref might not be available
57
+ const { stdout } = await execAsync("git diff --name-only HEAD^...HEAD");
58
+ return this.parseOutput(stdout);
59
+ }
60
+ }
61
+
62
+ private async getLocalChangedFiles(): Promise<string[]> {
63
+ // 1. Committed changes relative to base branch
64
+ const { stdout: committed } = await execAsync(
65
+ `git diff --name-only ${this.baseBranch}...HEAD`,
66
+ );
67
+
68
+ // 2. Uncommitted changes (staged and unstaged)
69
+ const { stdout: uncommitted } = await execAsync(
70
+ "git diff --name-only HEAD",
71
+ );
72
+
73
+ // 3. Untracked files
74
+ const { stdout: untracked } = await execAsync(
75
+ "git ls-files --others --exclude-standard",
76
+ );
77
+
78
+ const files = new Set([
79
+ ...this.parseOutput(committed),
80
+ ...this.parseOutput(uncommitted),
81
+ ...this.parseOutput(untracked),
82
+ ]);
83
+
84
+ return Array.from(files);
85
+ }
86
+
87
+ private async getCommitChangedFiles(commit: string): Promise<string[]> {
88
+ // Get diff for commit vs its parent
89
+ try {
90
+ const { stdout } = await execAsync(
91
+ `git diff --name-only ${commit}^..${commit}`,
92
+ );
93
+ return this.parseOutput(stdout);
94
+ } catch (_error) {
95
+ // If commit has no parent (initial commit), just get files in that commit
96
+ try {
97
+ const { stdout } = await execAsync(
98
+ `git diff --name-only --root ${commit}`,
99
+ );
100
+ return this.parseOutput(stdout);
101
+ } catch {
102
+ throw new Error(`Failed to get changes for commit ${commit}`);
103
+ }
104
+ }
105
+ }
106
+
107
+ private async getUncommittedChangedFiles(): Promise<string[]> {
108
+ // Get uncommitted changes (staged + unstaged) and untracked files
109
+ const { stdout: staged } = await execAsync("git diff --name-only --cached");
110
+ const { stdout: unstaged } = await execAsync("git diff --name-only");
111
+ const { stdout: untracked } = await execAsync(
112
+ "git ls-files --others --exclude-standard",
113
+ );
114
+
115
+ const files = new Set([
116
+ ...this.parseOutput(staged),
117
+ ...this.parseOutput(unstaged),
118
+ ...this.parseOutput(untracked),
119
+ ]);
120
+
121
+ return Array.from(files);
122
+ }
123
+
124
+ private parseOutput(stdout: string): string[] {
125
+ return stdout
126
+ .split("\n")
127
+ .map((line) => line.trim())
128
+ .filter((line) => line.length > 0);
129
+ }
112
130
  }
@@ -1,63 +1,61 @@
1
- import { describe, it, expect } from 'bun:test';
2
- import { EntryPointExpander } from './entry-point.js';
3
- import { EntryPointConfig } from '../config/types.js';
4
-
5
- describe('EntryPointExpander', () => {
6
- const expander = new EntryPointExpander();
7
-
8
- it('should include root entry point if there are any changes', async () => {
9
- const entryPoints: EntryPointConfig[] = [{ path: '.' }];
10
- const changes = ['some/file.ts'];
11
-
12
- const result = await expander.expand(entryPoints, changes);
13
-
14
- expect(result).toHaveLength(1);
15
- expect(result[0].path).toBe('.');
16
- });
17
-
18
- it('should match fixed directory entry points', async () => {
19
- const entryPoints: EntryPointConfig[] = [
20
- { path: 'apps/api' },
21
- { path: 'apps/web' }
22
- ];
23
- const changes = ['apps/api/src/index.ts'];
24
-
25
- const result = await expander.expand(entryPoints, changes);
26
-
27
- // Result should have root (implicit or explicit fallback in code) + matched
28
- // Looking at code: "if (changedFiles.length > 0) ... results.push({ path: '.', ... })"
29
- // Wait, the code creates a default root config if one isn't provided in the list?
30
- // Code: "const rootConfig = rootEntryPoint ?? { path: '.' }; results.push({ path: '.', config: rootConfig });"
31
- // Yes, it always pushes root if changes > 0.
32
-
33
- expect(result.some(r => r.path === 'apps/api')).toBe(true);
34
- expect(result.some(r => r.path === 'apps/web')).toBe(false);
35
- });
36
-
37
- it('should match wildcard entry points', async () => {
38
- const entryPoints: EntryPointConfig[] = [
39
- { path: 'packages/*' }
40
- ];
41
- const changes = [
42
- 'packages/ui/button.ts',
43
- 'packages/utils/helper.ts',
44
- 'other/file.ts'
45
- ];
46
-
47
- const result = await expander.expand(entryPoints, changes);
48
-
49
- const paths = result.map(r => r.path);
50
- expect(paths).toContain('packages/ui');
51
- expect(paths).toContain('packages/utils');
52
- expect(paths).not.toContain('packages/other');
53
- });
54
-
55
- it('should handle no changes', async () => {
56
- const entryPoints: EntryPointConfig[] = [{ path: '.' }];
57
- const changes: string[] = [];
58
-
59
- const result = await expander.expand(entryPoints, changes);
60
-
61
- expect(result).toHaveLength(0);
62
- });
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { EntryPointConfig } from "../config/types.js";
3
+ import { EntryPointExpander } from "./entry-point.js";
4
+
5
+ describe("EntryPointExpander", () => {
6
+ const expander = new EntryPointExpander();
7
+
8
+ it("should include root entry point if there are any changes", async () => {
9
+ const entryPoints: EntryPointConfig[] = [{ path: "." }];
10
+ const changes = ["some/file.ts"];
11
+
12
+ const result = await expander.expand(entryPoints, changes);
13
+
14
+ expect(result).toHaveLength(1);
15
+ expect(result[0].path).toBe(".");
16
+ });
17
+
18
+ it("should match fixed directory entry points", async () => {
19
+ const entryPoints: EntryPointConfig[] = [
20
+ { path: "apps/api" },
21
+ { path: "apps/web" },
22
+ ];
23
+ const changes = ["apps/api/src/index.ts"];
24
+
25
+ const result = await expander.expand(entryPoints, changes);
26
+
27
+ // Result should have root (implicit or explicit fallback in code) + matched
28
+ // Looking at code: "if (changedFiles.length > 0) ... results.push({ path: '.', ... })"
29
+ // Wait, the code creates a default root config if one isn't provided in the list?
30
+ // Code: "const rootConfig = rootEntryPoint ?? { path: '.' }; results.push({ path: '.', config: rootConfig });"
31
+ // Yes, it always pushes root if changes > 0.
32
+
33
+ expect(result.some((r) => r.path === "apps/api")).toBe(true);
34
+ expect(result.some((r) => r.path === "apps/web")).toBe(false);
35
+ });
36
+
37
+ it("should match wildcard entry points", async () => {
38
+ const entryPoints: EntryPointConfig[] = [{ path: "packages/*" }];
39
+ const changes = [
40
+ "packages/ui/button.ts",
41
+ "packages/utils/helper.ts",
42
+ "other/file.ts",
43
+ ];
44
+
45
+ const result = await expander.expand(entryPoints, changes);
46
+
47
+ const paths = result.map((r) => r.path);
48
+ expect(paths).toContain("packages/ui");
49
+ expect(paths).toContain("packages/utils");
50
+ expect(paths).not.toContain("packages/other");
51
+ });
52
+
53
+ it("should handle no changes", async () => {
54
+ const entryPoints: EntryPointConfig[] = [{ path: "." }];
55
+ const changes: string[] = [];
56
+
57
+ const result = await expander.expand(entryPoints, changes);
58
+
59
+ expect(result).toHaveLength(0);
60
+ });
63
61
  });
@@ -1,80 +1,126 @@
1
- import path from 'node:path';
2
- import { EntryPointConfig } from '../config/types.js';
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import type { EntryPointConfig } from "../config/types.js";
3
4
 
4
5
  export interface ExpandedEntryPoint {
5
- path: string; // The specific directory (e.g., "engines/billing")
6
- config: EntryPointConfig; // The config that generated this (e.g., "engines/*")
6
+ path: string; // The specific directory (e.g., "engines/billing")
7
+ config: EntryPointConfig; // The config that generated this (e.g., "engines/*")
7
8
  }
8
9
 
9
10
  export class EntryPointExpander {
10
- constructor(private rootDir: string = process.cwd()) {}
11
-
12
- async expand(entryPoints: EntryPointConfig[], changedFiles: string[]): Promise<ExpandedEntryPoint[]> {
13
- const results: ExpandedEntryPoint[] = [];
14
- const rootEntryPoint = entryPoints.find(ep => ep.path === '.');
15
-
16
- // Always include root entry point if configured and there are ANY changes
17
- // Or should it only run if files match root patterns?
18
- // Spec says: "A root entry point always exists and applies to repository-wide gates."
19
- // Usually root gates run on any change or specific files in root.
20
- // For simplicity, if root is configured, we'll include it if there are any changed files.
21
- if (changedFiles.length > 0) {
22
- const rootConfig = rootEntryPoint ?? { path: '.' };
23
- results.push({ path: '.', config: rootConfig });
24
- }
25
-
26
- for (const ep of entryPoints) {
27
- if (ep.path === '.') continue; // Handled above
28
-
29
- if (ep.path.endsWith('*')) {
30
- // Wildcard directory (e.g., "engines/*")
31
- const parentDir = ep.path.slice(0, -2); // "engines"
32
- const expandedPaths = await this.expandWildcard(parentDir, changedFiles);
33
-
34
- for (const subDir of expandedPaths) {
35
- results.push({
36
- path: subDir,
37
- config: ep
38
- });
39
- }
40
- } else {
41
- // Fixed directory (e.g., "apps/api")
42
- if (this.hasChangesInDir(ep.path, changedFiles)) {
43
- results.push({
44
- path: ep.path,
45
- config: ep
46
- });
47
- }
48
- }
49
- }
50
-
51
- return results;
52
- }
53
-
54
- private async expandWildcard(parentDir: string, changedFiles: string[]): Promise<string[]> {
55
- const affectedSubDirs = new Set<string>();
56
-
57
- // Filter changes that are inside this parent directory
58
- const relevantChanges = changedFiles.filter(f => f.startsWith(parentDir + '/'));
59
-
60
- for (const file of relevantChanges) {
61
- // file: "engines/billing/src/foo.ts", parentDir: "engines"
62
- // relPath: "billing/src/foo.ts"
63
- const relPath = file.slice(parentDir.length + 1);
64
- const subDirName = relPath.split('/')[0];
65
-
66
- if (subDirName) {
67
- affectedSubDirs.add(path.join(parentDir, subDirName));
68
- }
69
- }
70
-
71
- return Array.from(affectedSubDirs);
72
- }
73
-
74
- private hasChangesInDir(dirPath: string, changedFiles: string[]): boolean {
75
- // Check if any changed file starts with the dirPath
76
- // Need to ensure exact match or subdirectory (e.g. "app" should not match "apple")
77
- const dirPrefix = dirPath.endsWith('/') ? dirPath : dirPath + '/';
78
- return changedFiles.some(f => f === dirPath || f.startsWith(dirPrefix));
79
- }
11
+ async expand(
12
+ entryPoints: EntryPointConfig[],
13
+ changedFiles: string[],
14
+ ): Promise<ExpandedEntryPoint[]> {
15
+ const results: ExpandedEntryPoint[] = [];
16
+ const rootEntryPoint = entryPoints.find((ep) => ep.path === ".");
17
+
18
+ // Always include root entry point if configured and there are ANY changes
19
+ // Or should it only run if files match root patterns?
20
+ // Spec says: "A root entry point always exists and applies to repository-wide gates."
21
+ // Usually root gates run on any change or specific files in root.
22
+ // For simplicity, if root is configured, we'll include it if there are any changed files.
23
+ if (changedFiles.length > 0) {
24
+ const rootConfig = rootEntryPoint ?? { path: "." };
25
+ results.push({ path: ".", config: rootConfig });
26
+ }
27
+
28
+ for (const ep of entryPoints) {
29
+ if (ep.path === ".") continue; // Handled above
30
+
31
+ if (ep.path.endsWith("*")) {
32
+ // Wildcard directory (e.g., "engines/*")
33
+ const parentDir = ep.path.slice(0, -2); // "engines"
34
+ const expandedPaths = await this.expandWildcard(
35
+ parentDir,
36
+ changedFiles,
37
+ );
38
+
39
+ for (const subDir of expandedPaths) {
40
+ results.push({
41
+ path: subDir,
42
+ config: ep,
43
+ });
44
+ }
45
+ } else {
46
+ // Fixed directory (e.g., "apps/api")
47
+ if (this.hasChangesInDir(ep.path, changedFiles)) {
48
+ results.push({
49
+ path: ep.path,
50
+ config: ep,
51
+ });
52
+ }
53
+ }
54
+ }
55
+
56
+ return results;
57
+ }
58
+
59
+ async expandAll(
60
+ entryPoints: EntryPointConfig[],
61
+ ): Promise<ExpandedEntryPoint[]> {
62
+ const results: ExpandedEntryPoint[] = [];
63
+
64
+ for (const ep of entryPoints) {
65
+ if (ep.path === ".") {
66
+ results.push({ path: ".", config: ep });
67
+ continue;
68
+ }
69
+
70
+ if (ep.path.endsWith("*")) {
71
+ const parentDir = ep.path.slice(0, -2);
72
+ const subDirs = await this.listSubDirectories(parentDir);
73
+ for (const subDir of subDirs) {
74
+ results.push({ path: subDir, config: ep });
75
+ }
76
+ } else {
77
+ results.push({ path: ep.path, config: ep });
78
+ }
79
+ }
80
+
81
+ return results;
82
+ }
83
+
84
+ private async expandWildcard(
85
+ parentDir: string,
86
+ changedFiles: string[],
87
+ ): Promise<string[]> {
88
+ const affectedSubDirs = new Set<string>();
89
+
90
+ // Filter changes that are inside this parent directory
91
+ const relevantChanges = changedFiles.filter((f) =>
92
+ f.startsWith(`${parentDir}/`),
93
+ );
94
+
95
+ for (const file of relevantChanges) {
96
+ // file: "engines/billing/src/foo.ts", parentDir: "engines"
97
+ // relPath: "billing/src/foo.ts"
98
+ const relPath = file.slice(parentDir.length + 1);
99
+ const subDirName = relPath.split("/")[0];
100
+
101
+ if (subDirName) {
102
+ affectedSubDirs.add(path.join(parentDir, subDirName));
103
+ }
104
+ }
105
+
106
+ return Array.from(affectedSubDirs);
107
+ }
108
+
109
+ private async listSubDirectories(parentDir: string): Promise<string[]> {
110
+ try {
111
+ const dirents = await fs.readdir(parentDir, { withFileTypes: true });
112
+ return dirents
113
+ .filter((d) => d.isDirectory())
114
+ .map((d) => path.join(parentDir, d.name));
115
+ } catch {
116
+ return [];
117
+ }
118
+ }
119
+
120
+ private hasChangesInDir(dirPath: string, changedFiles: string[]): boolean {
121
+ // Check if any changed file starts with the dirPath
122
+ // Need to ensure exact match or subdirectory (e.g. "app" should not match "apple")
123
+ const dirPrefix = dirPath.endsWith("/") ? dirPath : `${dirPath}/`;
124
+ return changedFiles.some((f) => f === dirPath || f.startsWith(dirPrefix));
125
+ }
80
126
  }