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,137 +0,0 @@
1
- import { exec } from "node:child_process";
2
- import { promisify } from "node:util";
3
-
4
- const execAsync = promisify(exec);
5
-
6
- /** Validate that a string is a safe git ref (hex SHA or branch-like name). */
7
- function isValidGitRef(ref: string): boolean {
8
- return /^[a-zA-Z0-9._\-/]+$/.test(ref);
9
- }
10
-
11
- export interface ChangeDetectorOptions {
12
- commit?: string; // If provided, get diff for this commit vs its parent
13
- uncommitted?: boolean; // If true, only get uncommitted changes (staged + unstaged)
14
- fixBase?: string; // If provided, get diff from this ref to current working tree
15
- }
16
-
17
- export class ChangeDetector {
18
- constructor(
19
- private baseBranch: string = "origin/main",
20
- private options: ChangeDetectorOptions = {},
21
- ) {}
22
-
23
- async getChangedFiles(): Promise<string[]> {
24
- // Priority 1: If commit option is provided, use that
25
- if (this.options.commit) {
26
- return this.getCommitChangedFiles(this.options.commit);
27
- }
28
-
29
- // Priority 2: If uncommitted option is provided, only get uncommitted changes
30
- if (this.options.uncommitted) {
31
- return this.getUncommittedChangedFiles();
32
- }
33
-
34
- // Priority 3: If fixBase is provided, diff against it
35
- if (this.options.fixBase && isValidGitRef(this.options.fixBase)) {
36
- return this.getFixBaseChangedFiles(this.options.fixBase);
37
- }
38
-
39
- // Priority 4: CI detection / local base branch diff
40
- const isCI =
41
- process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
42
-
43
- if (isCI) {
44
- return this.getCIChangedFiles();
45
- } else {
46
- return this.getLocalChangedFiles();
47
- }
48
- }
49
-
50
- private async getCIChangedFiles(): Promise<string[]> {
51
- // In GitHub Actions, GITHUB_SHA is the commit being built
52
- // Base branch priority is already resolved by caller
53
- const baseRef = this.baseBranch;
54
- const headRef = process.env.GITHUB_SHA || "HEAD";
55
-
56
- // We might need to fetch first in some shallow clones, but assuming strictly for now
57
- // git diff --name-only base...head
58
- try {
59
- const { stdout } = await execAsync(
60
- `git diff --name-only ${baseRef}...${headRef}`,
61
- );
62
- return this.parseOutput(stdout);
63
- } catch (error) {
64
- console.warn(
65
- "Failed to detect changes via git diff in CI, falling back to HEAD^...HEAD",
66
- error,
67
- );
68
- // Fallback for push events where base ref might not be available
69
- const { stdout } = await execAsync("git diff --name-only HEAD^...HEAD");
70
- return this.parseOutput(stdout);
71
- }
72
- }
73
-
74
- /** Collect uncommitted (staged + unstaged) and untracked file paths. */
75
- private async getWorkingTreeFiles(): Promise<string[]> {
76
- const { stdout: staged } = await execAsync("git diff --name-only --cached");
77
- const { stdout: unstaged } = await execAsync("git diff --name-only");
78
- const { stdout: untracked } = await execAsync(
79
- "git ls-files --others --exclude-standard",
80
- );
81
- return [
82
- ...this.parseOutput(staged),
83
- ...this.parseOutput(unstaged),
84
- ...this.parseOutput(untracked),
85
- ];
86
- }
87
-
88
- /** Combine committed diff (against a base ref) with working tree changes. */
89
- private async getDiffWithWorkingTree(baseRef: string): Promise<string[]> {
90
- const { stdout: committed } = await execAsync(
91
- `git diff --name-only ${baseRef}...HEAD`,
92
- );
93
- const files = new Set([
94
- ...this.parseOutput(committed),
95
- ...(await this.getWorkingTreeFiles()),
96
- ]);
97
- return Array.from(files);
98
- }
99
-
100
- private async getLocalChangedFiles(): Promise<string[]> {
101
- return this.getDiffWithWorkingTree(this.baseBranch);
102
- }
103
-
104
- private async getCommitChangedFiles(commit: string): Promise<string[]> {
105
- try {
106
- const { stdout } = await execAsync(
107
- `git diff --name-only ${commit}^..${commit}`,
108
- );
109
- return this.parseOutput(stdout);
110
- } catch (_error) {
111
- try {
112
- const { stdout } = await execAsync(
113
- `git diff --name-only --root ${commit}`,
114
- );
115
- return this.parseOutput(stdout);
116
- } catch {
117
- throw new Error(`Failed to get changes for commit ${commit}`);
118
- }
119
- }
120
- }
121
-
122
- private async getFixBaseChangedFiles(fixBase: string): Promise<string[]> {
123
- return this.getDiffWithWorkingTree(fixBase);
124
- }
125
-
126
- private async getUncommittedChangedFiles(): Promise<string[]> {
127
- const files = new Set(await this.getWorkingTreeFiles());
128
- return Array.from(files);
129
- }
130
-
131
- private parseOutput(stdout: string): string[] {
132
- return stdout
133
- .split("\n")
134
- .map((line) => line.trim())
135
- .filter((line) => line.length > 0);
136
- }
137
- }
@@ -1,442 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { promisify } from "node:util";
3
-
4
- const execFileAsyncOriginal = promisify(execFile);
5
- export let execFileAsync = execFileAsyncOriginal;
6
-
7
- export function setExecFileAsync(fn: typeof execFileAsyncOriginal) {
8
- execFileAsync = fn;
9
- }
10
-
11
- /**
12
- * Run a git command safely using execFile (no shell interpolation).
13
- */
14
- async function gitExec(args: string[]): Promise<string> {
15
- const { stdout } = await execFileAsync("git", args);
16
- return stdout;
17
- }
18
-
19
- export interface DiffStats {
20
- baseRef: string; // e.g., "origin/main", "abc123", "uncommitted"
21
- total: number; // Total files changed
22
- newFiles: number; // Files added
23
- modifiedFiles: number; // Files modified
24
- deletedFiles: number; // Files deleted
25
- linesAdded: number; // Total lines added
26
- linesRemoved: number; // Total lines removed
27
- }
28
-
29
- export interface DiffStatsOptions {
30
- commit?: string; // If provided, get diff for this commit vs its parent
31
- uncommitted?: boolean; // If true, only get uncommitted changes
32
- fixBase?: string; // If provided, get diff from this ref to current working tree
33
- }
34
-
35
- /**
36
- * Compute diff statistics for changed files.
37
- */
38
- export async function computeDiffStats(
39
- baseBranch: string,
40
- options: DiffStatsOptions = {},
41
- ): Promise<DiffStats> {
42
- // Determine what we're diffing
43
- if (options.commit) {
44
- return computeCommitDiffStats(options.commit);
45
- }
46
-
47
- // If fixBase is provided, compute diff from that ref to current working tree
48
- // This is used in verification mode to show only NEW changes since the snapshot
49
- if (options.fixBase) {
50
- return computeFixBaseDiffStats(options.fixBase);
51
- }
52
-
53
- if (options.uncommitted) {
54
- return computeUncommittedDiffStats();
55
- }
56
-
57
- const isCI =
58
- process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
59
-
60
- if (isCI) {
61
- return computeCIDiffStats(baseBranch);
62
- }
63
-
64
- return computeLocalDiffStats(baseBranch);
65
- }
66
-
67
- /**
68
- * Compute diff stats for a specific commit vs its parent.
69
- */
70
- async function computeCommitDiffStats(commit: string): Promise<DiffStats> {
71
- try {
72
- // Get numstat for line counts
73
- const numstat = await gitExec([
74
- "diff",
75
- "--numstat",
76
- `${commit}^..${commit}`,
77
- ]);
78
- const lineStats = parseNumstat(numstat);
79
-
80
- // Get name-status for file categorization
81
- const nameStatus = await gitExec([
82
- "diff",
83
- "--name-status",
84
- `${commit}^..${commit}`,
85
- ]);
86
- const fileStats = parseNameStatus(nameStatus);
87
-
88
- return {
89
- baseRef: `${commit}^`,
90
- ...fileStats,
91
- ...lineStats,
92
- };
93
- } catch {
94
- // If commit has no parent (initial commit), try --root
95
- try {
96
- const numstat = await gitExec(["diff", "--numstat", "--root", commit]);
97
- const lineStats = parseNumstat(numstat);
98
-
99
- const nameStatus = await gitExec([
100
- "diff",
101
- "--name-status",
102
- "--root",
103
- commit,
104
- ]);
105
- const fileStats = parseNameStatus(nameStatus);
106
-
107
- return {
108
- baseRef: "root",
109
- ...fileStats,
110
- ...lineStats,
111
- };
112
- } catch {
113
- return emptyDiffStats(commit);
114
- }
115
- }
116
- }
117
-
118
- /**
119
- * Compute diff stats for uncommitted changes (staged + unstaged + untracked).
120
- */
121
- async function computeUncommittedDiffStats(): Promise<DiffStats> {
122
- // Get stats for staged changes
123
- const stagedNumstat = await gitExec(["diff", "--numstat", "--cached"]);
124
- const stagedLines = parseNumstat(stagedNumstat);
125
-
126
- const stagedStatus = await gitExec(["diff", "--name-status", "--cached"]);
127
- const stagedFiles = parseNameStatus(stagedStatus);
128
-
129
- // Get stats for unstaged changes
130
- const unstagedNumstat = await gitExec(["diff", "--numstat"]);
131
- const unstagedLines = parseNumstat(unstagedNumstat);
132
-
133
- const unstagedStatus = await gitExec(["diff", "--name-status"]);
134
- const unstagedFiles = parseNameStatus(unstagedStatus);
135
-
136
- // Get untracked files (all count as new, lines unknown)
137
- const untrackedList = await gitExec([
138
- "ls-files",
139
- "--others",
140
- "--exclude-standard",
141
- ]);
142
- const untrackedFiles = untrackedList
143
- .split("\n")
144
- .filter((f) => f.trim().length > 0);
145
-
146
- return {
147
- baseRef: "uncommitted",
148
- total:
149
- stagedFiles.total +
150
- unstagedFiles.total +
151
- untrackedFiles.length -
152
- countOverlap(stagedStatus, unstagedStatus),
153
- newFiles:
154
- stagedFiles.newFiles + unstagedFiles.newFiles + untrackedFiles.length,
155
- modifiedFiles: stagedFiles.modifiedFiles + unstagedFiles.modifiedFiles,
156
- deletedFiles: stagedFiles.deletedFiles + unstagedFiles.deletedFiles,
157
- linesAdded: stagedLines.linesAdded + unstagedLines.linesAdded,
158
- linesRemoved: stagedLines.linesRemoved + unstagedLines.linesRemoved,
159
- };
160
- }
161
-
162
- /**
163
- * Compute diff stats from a fixBase ref (stash or commit) to current working tree.
164
- * Used in verification mode to show only NEW changes since the snapshot.
165
- * This includes staged changes, unstaged changes, and new untracked files.
166
- */
167
- async function computeFixBaseDiffStats(fixBase: string): Promise<DiffStats> {
168
- try {
169
- // Get line stats for tracked file changes since fixBase
170
- // We need to diff against working tree (staged + unstaged changes)
171
- const numstat = await gitExec(["diff", "--numstat", fixBase]);
172
- const lineStats = parseNumstat(numstat);
173
-
174
- // Get file categorization for tracked file changes
175
- const nameStatus = await gitExec(["diff", "--name-status", fixBase]);
176
- const fileStats = parseNameStatus(nameStatus);
177
-
178
- // Handle untracked files: only count NEW untracked files that weren't in fixBase
179
- // Current untracked files
180
- const currentUntracked = (
181
- await gitExec(["ls-files", "--others", "--exclude-standard"])
182
- )
183
- .split("\n")
184
- .filter((f) => f.trim().length > 0);
185
-
186
- // Files that existed in fixBase
187
- let fixBaseFiles: Set<string>;
188
- try {
189
- const treeFiles = await gitExec([
190
- "ls-tree",
191
- "-r",
192
- "--name-only",
193
- fixBase,
194
- ]);
195
- fixBaseFiles = new Set(
196
- treeFiles.split("\n").filter((f) => f.trim().length > 0),
197
- );
198
- } catch {
199
- // If fixBase is invalid or has no tree, assume empty
200
- fixBaseFiles = new Set();
201
- }
202
-
203
- // New untracked files = current untracked - files that existed in fixBase
204
- const newUntrackedFiles = currentUntracked.filter(
205
- (f) => !fixBaseFiles.has(f),
206
- );
207
-
208
- return {
209
- baseRef: fixBase,
210
- total: fileStats.total + newUntrackedFiles.length,
211
- newFiles: fileStats.newFiles + newUntrackedFiles.length,
212
- modifiedFiles: fileStats.modifiedFiles,
213
- deletedFiles: fileStats.deletedFiles,
214
- linesAdded: lineStats.linesAdded,
215
- linesRemoved: lineStats.linesRemoved,
216
- };
217
- } catch {
218
- return emptyDiffStats(fixBase);
219
- }
220
- }
221
-
222
- /**
223
- * Compute diff stats in CI environment.
224
- */
225
- async function computeCIDiffStats(baseBranch: string): Promise<DiffStats> {
226
- const headRef = process.env.GITHUB_SHA || "HEAD";
227
-
228
- try {
229
- const numstat = await gitExec([
230
- "diff",
231
- "--numstat",
232
- `${baseBranch}...${headRef}`,
233
- ]);
234
- const lineStats = parseNumstat(numstat);
235
-
236
- const nameStatus = await gitExec([
237
- "diff",
238
- "--name-status",
239
- `${baseBranch}...${headRef}`,
240
- ]);
241
- const fileStats = parseNameStatus(nameStatus);
242
-
243
- return {
244
- baseRef: baseBranch,
245
- ...fileStats,
246
- ...lineStats,
247
- };
248
- } catch {
249
- // Fallback for push events
250
- try {
251
- const numstat = await gitExec(["diff", "--numstat", "HEAD^...HEAD"]);
252
- const lineStats = parseNumstat(numstat);
253
-
254
- const nameStatus = await gitExec([
255
- "diff",
256
- "--name-status",
257
- "HEAD^...HEAD",
258
- ]);
259
- const fileStats = parseNameStatus(nameStatus);
260
-
261
- return {
262
- baseRef: "HEAD^",
263
- ...fileStats,
264
- ...lineStats,
265
- };
266
- } catch {
267
- return emptyDiffStats(baseBranch);
268
- }
269
- }
270
- }
271
-
272
- /**
273
- * Compute diff stats for local development.
274
- */
275
- async function computeLocalDiffStats(baseBranch: string): Promise<DiffStats> {
276
- // 1. Committed changes relative to base branch
277
- const committedNumstat = await gitExec([
278
- "diff",
279
- "--numstat",
280
- `${baseBranch}...HEAD`,
281
- ]);
282
- const committedLines = parseNumstat(committedNumstat);
283
-
284
- const committedStatus = await gitExec([
285
- "diff",
286
- "--name-status",
287
- `${baseBranch}...HEAD`,
288
- ]);
289
- const committedFiles = parseNameStatus(committedStatus);
290
-
291
- // 2. Uncommitted changes (staged and unstaged)
292
- const uncommittedNumstat = await gitExec(["diff", "--numstat", "HEAD"]);
293
- const uncommittedLines = parseNumstat(uncommittedNumstat);
294
-
295
- const uncommittedStatus = await gitExec(["diff", "--name-status", "HEAD"]);
296
- const uncommittedFiles = parseNameStatus(uncommittedStatus);
297
-
298
- // 3. Untracked files
299
- const untrackedList = await gitExec([
300
- "ls-files",
301
- "--others",
302
- "--exclude-standard",
303
- ]);
304
- const untrackedFiles = untrackedList
305
- .split("\n")
306
- .filter((f) => f.trim().length > 0);
307
-
308
- // Combine counts (with overlap detection)
309
- const totalNew =
310
- committedFiles.newFiles + uncommittedFiles.newFiles + untrackedFiles.length;
311
- const totalModified =
312
- committedFiles.modifiedFiles + uncommittedFiles.modifiedFiles;
313
- const totalDeleted =
314
- committedFiles.deletedFiles + uncommittedFiles.deletedFiles;
315
-
316
- return {
317
- baseRef: baseBranch,
318
- total: totalNew + totalModified + totalDeleted,
319
- newFiles: totalNew,
320
- modifiedFiles: totalModified,
321
- deletedFiles: totalDeleted,
322
- linesAdded: committedLines.linesAdded + uncommittedLines.linesAdded,
323
- linesRemoved: committedLines.linesRemoved + uncommittedLines.linesRemoved,
324
- };
325
- }
326
-
327
- /**
328
- * Parse git diff --numstat output for line counts.
329
- * Format: <added>\t<removed>\t<file>
330
- * Binary files show as "-\t-\t<file>"
331
- */
332
- function parseNumstat(output: string): {
333
- linesAdded: number;
334
- linesRemoved: number;
335
- } {
336
- let linesAdded = 0;
337
- let linesRemoved = 0;
338
-
339
- for (const line of output.split("\n")) {
340
- if (!line.trim()) continue;
341
- const parts = line.split("\t");
342
- if (parts.length < 3) continue;
343
-
344
- const added = parts[0];
345
- const removed = parts[1];
346
- // Binary files show as "-"
347
- if (added && added !== "-") {
348
- linesAdded += parseInt(added, 10) || 0;
349
- }
350
- if (removed && removed !== "-") {
351
- linesRemoved += parseInt(removed, 10) || 0;
352
- }
353
- }
354
-
355
- return { linesAdded, linesRemoved };
356
- }
357
-
358
- /**
359
- * Parse git diff --name-status output for file categorization.
360
- * Format: <status>\t<file> (and optionally \t<new-file> for renames)
361
- * Status codes: A=added, M=modified, D=deleted, R=renamed, C=copied, T=type-change
362
- */
363
- function parseNameStatus(output: string): {
364
- total: number;
365
- newFiles: number;
366
- modifiedFiles: number;
367
- deletedFiles: number;
368
- } {
369
- let newFiles = 0;
370
- let modifiedFiles = 0;
371
- let deletedFiles = 0;
372
-
373
- for (const line of output.split("\n")) {
374
- if (!line.trim()) continue;
375
- const status = line[0];
376
-
377
- switch (status) {
378
- case "A":
379
- newFiles++;
380
- break;
381
- case "M":
382
- case "R":
383
- case "C":
384
- case "T":
385
- modifiedFiles++;
386
- break;
387
- case "D":
388
- deletedFiles++;
389
- break;
390
- }
391
- }
392
-
393
- return {
394
- total: newFiles + modifiedFiles + deletedFiles,
395
- newFiles,
396
- modifiedFiles,
397
- deletedFiles,
398
- };
399
- }
400
-
401
- /**
402
- * Count overlapping files between two name-status outputs.
403
- * Used to avoid double-counting files that appear in both staged and unstaged.
404
- */
405
- function countOverlap(status1: string, status2: string): number {
406
- const files1 = new Set<string>();
407
- for (const line of status1.split("\n")) {
408
- if (!line.trim()) continue;
409
- const parts = line.split("\t");
410
- const file = parts[1];
411
- if (parts.length >= 2 && file) {
412
- files1.add(file);
413
- }
414
- }
415
-
416
- let overlap = 0;
417
- for (const line of status2.split("\n")) {
418
- if (!line.trim()) continue;
419
- const parts = line.split("\t");
420
- const file = parts[1];
421
- if (parts.length >= 2 && file && files1.has(file)) {
422
- overlap++;
423
- }
424
- }
425
-
426
- return overlap;
427
- }
428
-
429
- /**
430
- * Return empty diff stats with the given base ref.
431
- */
432
- function emptyDiffStats(baseRef: string): DiffStats {
433
- return {
434
- baseRef,
435
- total: 0,
436
- newFiles: 0,
437
- modifiedFiles: 0,
438
- deletedFiles: 0,
439
- linesAdded: 0,
440
- linesRemoved: 0,
441
- };
442
- }