agent-gauntlet 1.0.0 → 1.2.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.
package/dist/index.js CHANGED
@@ -299,7 +299,7 @@ import { Command } from "commander";
299
299
  // package.json
300
300
  var package_default = {
301
301
  name: "agent-gauntlet",
302
- version: "1.0.0",
302
+ version: "1.2.0",
303
303
  description: "A CLI tool for testing AI coding agents",
304
304
  license: "MIT",
305
305
  author: "Paul Caplan",
@@ -552,7 +552,8 @@ var reviewGateSchema = z2.object({
552
552
  parallel: z2.boolean().default(true),
553
553
  run_in_ci: z2.boolean().default(true),
554
554
  run_locally: z2.boolean().default(true),
555
- timeout: z2.number().optional()
555
+ timeout: z2.number().optional(),
556
+ enabled: z2.boolean().default(true)
556
557
  });
557
558
  var reviewPromptFrontmatterSchema = z2.object({
558
559
  model: z2.string().optional(),
@@ -563,7 +564,8 @@ var reviewPromptFrontmatterSchema = z2.object({
563
564
  run_locally: z2.boolean().default(true),
564
565
  timeout: z2.number().optional(),
565
566
  prompt_file: z2.string().optional(),
566
- skill_name: z2.string().optional()
567
+ skill_name: z2.string().optional(),
568
+ enabled: z2.boolean().default(true)
567
569
  }).refine((data) => !(data.prompt_file && data.skill_name), {
568
570
  message: "'prompt_file' and 'skill_name' are mutually exclusive. Specify only one."
569
571
  });
@@ -577,7 +579,8 @@ var reviewYamlSchema = z2.object({
577
579
  timeout: z2.number().optional(),
578
580
  prompt_file: z2.string().optional(),
579
581
  skill_name: z2.string().optional(),
580
- builtin: z2.string().optional()
582
+ builtin: z2.string().optional(),
583
+ enabled: z2.boolean().default(true)
581
584
  }).superRefine((data, ctx) => {
582
585
  const sources = [data.prompt_file, data.skill_name, data.builtin].filter(Boolean);
583
586
  if (sources.length > 1) {
@@ -742,7 +745,8 @@ async function loadMarkdownReview(file, reviewsPath, gauntletPath) {
742
745
  parallel: parsedFrontmatter.parallel,
743
746
  run_in_ci: parsedFrontmatter.run_in_ci,
744
747
  run_locally: parsedFrontmatter.run_locally,
745
- timeout: parsedFrontmatter.timeout
748
+ timeout: parsedFrontmatter.timeout,
749
+ enabled: parsedFrontmatter.enabled
746
750
  };
747
751
  if (parsedFrontmatter.prompt_file) {
748
752
  review.promptContent = await loadPromptFile(parsedFrontmatter.prompt_file, gauntletPath, `review "${name}"`);
@@ -768,7 +772,8 @@ async function loadYamlReview(file, reviewsPath, gauntletPath) {
768
772
  parallel: parsed.parallel,
769
773
  run_in_ci: parsed.run_in_ci,
770
774
  run_locally: parsed.run_locally,
771
- timeout: parsed.timeout
775
+ timeout: parsed.timeout,
776
+ enabled: parsed.enabled
772
777
  };
773
778
  if (parsed.prompt_file) {
774
779
  review.promptContent = await loadPromptFile(parsed.prompt_file, gauntletPath, `review "${name}"`);
@@ -849,9 +854,10 @@ async function dirExists(path4) {
849
854
  }
850
855
 
851
856
  // src/core/change-detector.ts
852
- import { exec } from "node:child_process";
857
+ import { exec, execFile } from "node:child_process";
853
858
  import { promisify } from "node:util";
854
859
  var execAsync = promisify(exec);
860
+ var execFileAsync = promisify(execFile);
855
861
  function isValidGitRef(ref) {
856
862
  return /^[a-zA-Z0-9._\-/]+$/.test(ref);
857
863
  }
@@ -924,8 +930,40 @@ class ChangeDetector {
924
930
  ]);
925
931
  return Array.from(files);
926
932
  }
933
+ async resolveLocalBaseBranch() {
934
+ const base = this.baseBranch;
935
+ if (base.startsWith("origin/"))
936
+ return base;
937
+ const remoteRef = `origin/${base}`;
938
+ const [localExists, remoteExists] = await Promise.all([
939
+ execFileAsync("git", ["rev-parse", "--verify", base]).then(() => true, () => false),
940
+ execFileAsync("git", ["rev-parse", "--verify", remoteRef]).then(() => true, () => false)
941
+ ]);
942
+ if (!remoteExists)
943
+ return base;
944
+ if (!localExists)
945
+ return remoteRef;
946
+ try {
947
+ const [{ stdout: remoteAhead }, { stdout: localAhead }] = await Promise.all([
948
+ execFileAsync("git", [
949
+ "rev-list",
950
+ "--count",
951
+ `${base}..${remoteRef}`
952
+ ]),
953
+ execFileAsync("git", [
954
+ "rev-list",
955
+ "--count",
956
+ `${remoteRef}..${base}`
957
+ ])
958
+ ]);
959
+ return parseInt(localAhead.trim(), 10) > parseInt(remoteAhead.trim(), 10) ? base : remoteRef;
960
+ } catch {
961
+ return remoteRef;
962
+ }
963
+ }
927
964
  async getLocalChangedFiles() {
928
- return this.getDiffWithWorkingTree(this.baseBranch);
965
+ const resolvedBase = await this.resolveLocalBaseBranch();
966
+ return this.getDiffWithWorkingTree(resolvedBase);
929
967
  }
930
968
  async getCommitChangedFiles(commit) {
931
969
  try {
@@ -1081,8 +1119,10 @@ function shouldRunGate(gateConfig, isCI) {
1081
1119
 
1082
1120
  class JobGenerator {
1083
1121
  config;
1084
- constructor(config) {
1122
+ enableReviews;
1123
+ constructor(config, enableReviews = new Set) {
1085
1124
  this.config = config;
1125
+ this.enableReviews = enableReviews;
1086
1126
  }
1087
1127
  generateJobs(expandedEntryPoints) {
1088
1128
  const jobs = [];
@@ -1131,6 +1171,8 @@ class JobGenerator {
1131
1171
  }
1132
1172
  if (!shouldRunGate(reviewConfig, isCI))
1133
1173
  continue;
1174
+ if (!(reviewConfig.enabled || this.enableReviews.has(reviewName)))
1175
+ continue;
1134
1176
  jobs.push({
1135
1177
  id: `review:${ep.path}:${reviewName}`,
1136
1178
  type: "review",
@@ -1663,9 +1705,10 @@ import fs9 from "node:fs/promises";
1663
1705
  import path8 from "node:path";
1664
1706
 
1665
1707
  // src/utils/execution-state.ts
1666
- import { spawn as spawn2 } from "node:child_process";
1708
+ import { execFile as execFile2, spawn as spawn2 } from "node:child_process";
1667
1709
  import fs8 from "node:fs/promises";
1668
1710
  import path7 from "node:path";
1711
+ import { StringDecoder } from "node:string_decoder";
1669
1712
 
1670
1713
  // src/utils/debug-log.ts
1671
1714
  import fs7 from "node:fs/promises";
@@ -1837,6 +1880,35 @@ function getDebugLogger() {
1837
1880
  // src/utils/execution-state.ts
1838
1881
  var EXECUTION_STATE_FILENAME = ".execution_state";
1839
1882
  var SESSION_REF_FILENAME = ".session_ref";
1883
+ function getStatePath(logDir) {
1884
+ return path7.join(logDir, EXECUTION_STATE_FILENAME);
1885
+ }
1886
+ function spawnGit(args) {
1887
+ return new Promise((resolve, reject) => {
1888
+ const child = spawn2("git", args, { stdio: ["ignore", "pipe", "pipe"] });
1889
+ const stdoutDecoder = new StringDecoder("utf8");
1890
+ const stderrDecoder = new StringDecoder("utf8");
1891
+ let stdout = "";
1892
+ child.stdout.on("data", (chunk) => {
1893
+ stdout += typeof chunk === "string" ? chunk : stdoutDecoder.write(chunk);
1894
+ });
1895
+ let stderr = "";
1896
+ child.stderr.on("data", (chunk) => {
1897
+ stderr += typeof chunk === "string" ? chunk : stderrDecoder.write(chunk);
1898
+ });
1899
+ child.on("close", (code) => {
1900
+ stdout += stdoutDecoder.end();
1901
+ stderr += stderrDecoder.end();
1902
+ if (code === 0)
1903
+ resolve(stdout.trim());
1904
+ else {
1905
+ const detail = stderr.trim();
1906
+ reject(new Error(detail ? `git ${args.join(" ")} failed with code ${code}: ${detail}` : `git ${args.join(" ")} failed with code ${code}`));
1907
+ }
1908
+ });
1909
+ child.on("error", reject);
1910
+ });
1911
+ }
1840
1912
  function isPlainRecord(value) {
1841
1913
  if (value == null)
1842
1914
  return false;
@@ -1859,7 +1931,7 @@ function isValidStateData(data) {
1859
1931
  }
1860
1932
  async function readExecutionState(logDir) {
1861
1933
  try {
1862
- const statePath = path7.join(logDir, EXECUTION_STATE_FILENAME);
1934
+ const statePath = getStatePath(logDir);
1863
1935
  const content = await fs8.readFile(statePath, "utf-8");
1864
1936
  const data = JSON.parse(content);
1865
1937
  if (!isValidStateData(data))
@@ -1880,38 +1952,52 @@ async function readExecutionState(logDir) {
1880
1952
  return null;
1881
1953
  }
1882
1954
  }
1883
- async function resolveStashResult(code, stdout) {
1884
- if (code === 0) {
1885
- const sha = stdout.trim();
1886
- if (sha)
1887
- return sha;
1955
+ async function createWorkingTreeRef() {
1956
+ const hasChanges = await hasWorkingTreeChanges();
1957
+ if (!hasChanges) {
1888
1958
  return getCurrentCommit();
1889
1959
  }
1890
- return getCurrentCommit().catch(() => {
1891
- throw new Error(`git stash create failed with code ${code}`);
1892
- });
1893
- }
1894
- async function createWorkingTreeRef() {
1895
- return new Promise((resolve, reject) => {
1896
- const child = spawn2("git", ["stash", "create", "--include-untracked"], {
1897
- stdio: ["ignore", "pipe", "pipe"]
1898
- });
1899
- let stdout = "";
1900
- child.stdout.on("data", (data) => {
1901
- stdout += data.toString();
1902
- });
1903
- child.on("close", (code) => {
1904
- resolveStashResult(code, stdout).then(resolve, reject);
1960
+ const runGit = (args) => new Promise((resolve, reject) => {
1961
+ execFile2("git", args, (error, stdout) => {
1962
+ if (error)
1963
+ reject(error);
1964
+ else
1965
+ resolve(stdout.trim());
1905
1966
  });
1906
- child.on("error", reject);
1907
1967
  });
1968
+ const prevTop = await runGit(["rev-parse", "--verify", "stash@{0}"]).catch(() => "");
1969
+ try {
1970
+ await runGit([
1971
+ "stash",
1972
+ "push",
1973
+ "--include-untracked",
1974
+ "-m",
1975
+ "gauntlet-snapshot"
1976
+ ]);
1977
+ } catch {
1978
+ return getCurrentCommit();
1979
+ }
1980
+ const newTop = await runGit(["rev-parse", "stash@{0}"]).catch(() => "");
1981
+ const createdStash = !!newTop && newTop !== prevTop;
1982
+ if (!createdStash) {
1983
+ if (prevTop && !newTop) {
1984
+ console.error("gauntlet: unable to verify stash snapshot; leaving stash untouched. Run `git stash pop` manually if needed.");
1985
+ }
1986
+ return getCurrentCommit();
1987
+ }
1988
+ try {
1989
+ await runGit(["stash", "pop"]);
1990
+ } catch {
1991
+ console.error("gauntlet: stash pop failed — run `git stash pop` manually to restore your working tree");
1992
+ }
1993
+ return newTop;
1908
1994
  }
1909
1995
  async function writeExecutionState(logDir) {
1910
- const statePath = path7.join(logDir, EXECUTION_STATE_FILENAME);
1911
- const [branch, commit, workingTreeRef, rawState] = await Promise.all([
1996
+ const statePath = getStatePath(logDir);
1997
+ const workingTreeRef = await createWorkingTreeRef();
1998
+ const [branch, commit, rawState] = await Promise.all([
1912
1999
  getCurrentBranch(),
1913
2000
  getCurrentCommit(),
1914
- createWorkingTreeRef(),
1915
2001
  readRawState(statePath)
1916
2002
  ]);
1917
2003
  const existingUnhealthy = extractUnhealthyAdapters(rawState);
@@ -1946,91 +2032,23 @@ async function writeExecutionState(logDir) {
1946
2032
  await fs8.rm(sessionRefPath, { force: true });
1947
2033
  } catch {}
1948
2034
  }
1949
- async function getCurrentBranch() {
1950
- return new Promise((resolve, reject) => {
1951
- const child = spawn2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
1952
- stdio: ["ignore", "pipe", "pipe"]
1953
- });
1954
- let stdout = "";
1955
- child.stdout.on("data", (data) => {
1956
- stdout += data.toString();
1957
- });
1958
- child.on("close", (code) => {
1959
- if (code === 0) {
1960
- resolve(stdout.trim());
1961
- } else {
1962
- reject(new Error(`git rev-parse failed with code ${code}`));
1963
- }
1964
- });
1965
- child.on("error", reject);
1966
- });
2035
+ function getCurrentBranch() {
2036
+ return spawnGit(["rev-parse", "--abbrev-ref", "HEAD"]);
1967
2037
  }
1968
- async function getCurrentCommit() {
1969
- return new Promise((resolve, reject) => {
1970
- const child = spawn2("git", ["rev-parse", "HEAD"], {
1971
- stdio: ["ignore", "pipe", "pipe"]
1972
- });
1973
- let stdout = "";
1974
- child.stdout.on("data", (data) => {
1975
- stdout += data.toString();
1976
- });
1977
- child.on("close", (code) => {
1978
- if (code === 0) {
1979
- resolve(stdout.trim());
1980
- } else {
1981
- reject(new Error(`git rev-parse failed with code ${code}`));
1982
- }
1983
- });
1984
- child.on("error", reject);
1985
- });
2038
+ function getCurrentCommit() {
2039
+ return spawnGit(["rev-parse", "HEAD"]);
1986
2040
  }
1987
- async function isCommitInBranch(commit, branch) {
1988
- return new Promise((resolve) => {
1989
- const child = spawn2("git", ["merge-base", "--is-ancestor", commit, branch], { stdio: ["ignore", "pipe", "pipe"] });
1990
- child.on("close", (code) => {
1991
- resolve(code === 0);
1992
- });
1993
- child.on("error", () => {
1994
- resolve(false);
1995
- });
1996
- });
2041
+ function isCommitInBranch(commit, branch) {
2042
+ return spawnGit(["merge-base", "--is-ancestor", commit, branch]).then(() => true).catch(() => false);
1997
2043
  }
1998
2044
  function getExecutionStateFilename() {
1999
2045
  return EXECUTION_STATE_FILENAME;
2000
2046
  }
2001
- async function hasWorkingTreeChanges() {
2002
- return new Promise((resolve) => {
2003
- const child = spawn2("git", ["status", "--porcelain"], {
2004
- stdio: ["ignore", "pipe", "pipe"]
2005
- });
2006
- let stdout = "";
2007
- child.stdout.on("data", (data) => {
2008
- stdout += data.toString();
2009
- });
2010
- child.on("close", (code) => {
2011
- if (code === 0) {
2012
- resolve(stdout.trim().length > 0);
2013
- } else {
2014
- resolve(true);
2015
- }
2016
- });
2017
- child.on("error", () => {
2018
- resolve(true);
2019
- });
2020
- });
2047
+ function hasWorkingTreeChanges() {
2048
+ return spawnGit(["status", "--porcelain"]).then((out) => out.length > 0).catch(() => true);
2021
2049
  }
2022
- async function gitObjectExists(sha) {
2023
- return new Promise((resolve) => {
2024
- const child = spawn2("git", ["cat-file", "-t", sha], {
2025
- stdio: ["ignore", "pipe", "pipe"]
2026
- });
2027
- child.on("close", (code) => {
2028
- resolve(code === 0);
2029
- });
2030
- child.on("error", () => {
2031
- resolve(false);
2032
- });
2033
- });
2050
+ function gitObjectExists(sha) {
2051
+ return spawnGit(["cat-file", "-t", sha]).then(() => true).catch(() => false);
2034
2052
  }
2035
2053
  async function resolveFixBaseForMergedCommit(working_tree_ref) {
2036
2054
  if (!working_tree_ref) {
@@ -2074,7 +2092,7 @@ function isAdapterCoolingDown(entry) {
2074
2092
  return Date.now() - markedAt < COOLDOWN_MS;
2075
2093
  }
2076
2094
  async function getUnhealthyAdapters(logDir) {
2077
- const statePath = path7.join(logDir, EXECUTION_STATE_FILENAME);
2095
+ const statePath = getStatePath(logDir);
2078
2096
  const rawState = await readRawState(statePath);
2079
2097
  return extractUnhealthyAdapters(rawState) ?? {};
2080
2098
  }
@@ -2088,7 +2106,7 @@ async function readRawState(statePath) {
2088
2106
  }
2089
2107
  async function markAdapterUnhealthy(logDir, adapterName, reason) {
2090
2108
  await getDebugLogger()?.logAdapterHealthChange(adapterName, false, reason);
2091
- const statePath = path7.join(logDir, EXECUTION_STATE_FILENAME);
2109
+ const statePath = getStatePath(logDir);
2092
2110
  const rawData = await readRawState(statePath) ?? {};
2093
2111
  const adapters = rawData.unhealthy_adapters ?? {};
2094
2112
  adapters[adapterName] = {
@@ -2101,7 +2119,7 @@ async function markAdapterUnhealthy(logDir, adapterName, reason) {
2101
2119
  }
2102
2120
  async function markAdapterHealthy(logDir, adapterName) {
2103
2121
  await getDebugLogger()?.logAdapterHealthChange(adapterName, true);
2104
- const statePath = path7.join(logDir, EXECUTION_STATE_FILENAME);
2122
+ const statePath = getStatePath(logDir);
2105
2123
  const rawData = await readRawState(statePath);
2106
2124
  if (!rawData)
2107
2125
  return;
@@ -2119,7 +2137,7 @@ async function markAdapterHealthy(logDir, adapterName) {
2119
2137
  async function deleteExecutionState(logDir) {
2120
2138
  try {
2121
2139
  await getDebugLogger()?.logStateDelete();
2122
- const statePath = path7.join(logDir, EXECUTION_STATE_FILENAME);
2140
+ const statePath = getStatePath(logDir);
2123
2141
  await fs8.rm(statePath, { force: true });
2124
2142
  } catch {}
2125
2143
  }
@@ -2183,11 +2201,11 @@ async function checkRunInterval(logDir, intervalMinutes) {
2183
2201
  return elapsedMinutes >= intervalMinutes;
2184
2202
  }
2185
2203
  async function hasChangesVsBaseBranch(cwd, baseBranch) {
2186
- const { execFile } = await import("node:child_process");
2204
+ const { execFile: execFile3 } = await import("node:child_process");
2187
2205
  const { promisify: promisify3 } = await import("node:util");
2188
- const execFileAsync = promisify3(execFile);
2206
+ const execFileAsync2 = promisify3(execFile3);
2189
2207
  try {
2190
- const { stdout } = await execFileAsync("git", ["diff", "--name-only", `${baseBranch}...HEAD`], { cwd });
2208
+ const { stdout } = await execFileAsync2("git", ["diff", "--name-only", `${baseBranch}...HEAD`], { cwd });
2191
2209
  return stdout.trim().length > 0;
2192
2210
  } catch {
2193
2211
  return true;
@@ -3014,6 +3032,7 @@ import os3 from "node:os";
3014
3032
  import path12 from "node:path";
3015
3033
  import { promisify as promisify4 } from "node:util";
3016
3034
  var execAsync4 = promisify4(exec4);
3035
+ var _tmpCounter = 0;
3017
3036
  function parseJsonlLine(line) {
3018
3037
  try {
3019
3038
  const obj = JSON.parse(line);
@@ -3176,7 +3195,7 @@ class CodexAdapter {
3176
3195
  --- DIFF ---
3177
3196
  ${opts.diff}`;
3178
3197
  const tmpDir = os3.tmpdir();
3179
- const tmpFile = path12.join(tmpDir, `gauntlet-codex-${Date.now()}.txt`);
3198
+ const tmpFile = path12.join(tmpDir, `gauntlet-codex-${process.pid}-${Date.now()}-${_tmpCounter++}.txt`);
3180
3199
  await fs13.writeFile(tmpFile, fullContent);
3181
3200
  const args = this.buildArgs(opts.allowToolUse, opts.thinkingBudget);
3182
3201
  const cleanup = () => fs13.unlink(tmpFile).catch(() => {});
@@ -3254,6 +3273,7 @@ function resolveModelFromList(allModels, opts) {
3254
3273
 
3255
3274
  // src/cli-adapters/cursor.ts
3256
3275
  var execAsync5 = promisify5(exec5);
3276
+ var _tmpCounter2 = 0;
3257
3277
  var log = getCategoryLogger("cursor");
3258
3278
  function parseModelList(output) {
3259
3279
  return output.split(`
@@ -3344,7 +3364,7 @@ class CursorAdapter {
3344
3364
  --- DIFF ---
3345
3365
  ${opts.diff}`;
3346
3366
  const tmpDir = os4.tmpdir();
3347
- const tmpFile = path13.join(tmpDir, `gauntlet-cursor-${process.pid}-${Date.now()}.txt`);
3367
+ const tmpFile = path13.join(tmpDir, `gauntlet-cursor-${process.pid}-${Date.now()}-${_tmpCounter2++}.txt`);
3348
3368
  await fs14.writeFile(tmpFile, fullContent);
3349
3369
  let resolvedModel;
3350
3370
  if (opts.model) {
@@ -3747,6 +3767,7 @@ import os6 from "node:os";
3747
3767
  import path15 from "node:path";
3748
3768
  import { promisify as promisify7 } from "node:util";
3749
3769
  var execAsync7 = promisify7(exec7);
3770
+ var _tmpCounter3 = 0;
3750
3771
  var log2 = getCategoryLogger("github-copilot");
3751
3772
  function parseCopilotModels(helpOutput) {
3752
3773
  const match = helpOutput.match(/choices:\s*(.+?)\)/);
@@ -3835,7 +3856,7 @@ class GitHubCopilotAdapter {
3835
3856
  --- DIFF ---
3836
3857
  ${opts.diff}`;
3837
3858
  const tmpDir = os6.tmpdir();
3838
- const tmpFile = path15.join(tmpDir, `gauntlet-copilot-${process.pid}-${Date.now()}.txt`);
3859
+ const tmpFile = path15.join(tmpDir, `gauntlet-copilot-${process.pid}-${Date.now()}-${_tmpCounter3++}.txt`);
3839
3860
  await fs16.writeFile(tmpFile, fullContent);
3840
3861
  let resolvedModel;
3841
3862
  if (opts.model) {
@@ -4068,98 +4089,504 @@ async function handleCriticalError(error, jobId, startTime, logPaths, mainLogger
4068
4089
 
4069
4090
  // src/gates/review-diff.ts
4070
4091
  import { exec as exec8 } from "node:child_process";
4092
+ import fs18 from "node:fs/promises";
4093
+ import os7 from "node:os";
4094
+ import path16 from "node:path";
4095
+ import { promisify as promisify9 } from "node:util";
4096
+
4097
+ // src/core/diff-stats.ts
4098
+ import { execFile as execFile3 } from "node:child_process";
4071
4099
  import { promisify as promisify8 } from "node:util";
4072
- var log4 = getCategoryLogger("gate", "review");
4073
- var execAsync8 = promisify8(exec8);
4074
- function parseLines(stdout) {
4075
- return stdout.split(`
4076
- `).map((line) => line.trim()).filter((line) => line.length > 0);
4100
+ var execFileAsyncOriginal = promisify8(execFile3);
4101
+ var execFileAsync2 = execFileAsyncOriginal;
4102
+ async function gitExec(args) {
4103
+ const { stdout } = await execFileAsync2("git", args, {
4104
+ maxBuffer: MAX_BUFFER_BYTES
4105
+ });
4106
+ return stdout;
4077
4107
  }
4078
- function pathArg(entryPointPath) {
4079
- return ` -- ${quoteArg(entryPointPath)}`;
4108
+ async function computeDiffStats(baseBranch, options = {}) {
4109
+ if (options.commit) {
4110
+ return computeCommitDiffStats(options.commit);
4111
+ }
4112
+ if (options.fixBase) {
4113
+ return computeFixBaseDiffStats(options.fixBase);
4114
+ }
4115
+ if (options.uncommitted) {
4116
+ return computeUncommittedDiffStats();
4117
+ }
4118
+ const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
4119
+ if (isCI) {
4120
+ return computeCIDiffStats(baseBranch);
4121
+ }
4122
+ return computeLocalDiffStats(baseBranch);
4080
4123
  }
4081
- function quoteArg(value) {
4082
- return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
4124
+ async function computeCommitDiffStats(commit) {
4125
+ try {
4126
+ const numstat = await gitExec([
4127
+ "diff",
4128
+ "--numstat",
4129
+ `${commit}^..${commit}`
4130
+ ]);
4131
+ const lineStats = parseNumstat(numstat);
4132
+ const nameStatus = await gitExec([
4133
+ "diff",
4134
+ "--name-status",
4135
+ `${commit}^..${commit}`
4136
+ ]);
4137
+ const fileStats = parseNameStatus(nameStatus);
4138
+ return {
4139
+ baseRef: `${commit}^`,
4140
+ ...fileStats,
4141
+ ...lineStats
4142
+ };
4143
+ } catch {
4144
+ try {
4145
+ const numstat = await gitExec(["diff", "--numstat", "--root", commit]);
4146
+ const lineStats = parseNumstat(numstat);
4147
+ const nameStatus = await gitExec([
4148
+ "diff",
4149
+ "--name-status",
4150
+ "--root",
4151
+ commit
4152
+ ]);
4153
+ const fileStats = parseNameStatus(nameStatus);
4154
+ return {
4155
+ baseRef: "root",
4156
+ ...fileStats,
4157
+ ...lineStats
4158
+ };
4159
+ } catch {
4160
+ return emptyDiffStats(commit);
4161
+ }
4162
+ }
4083
4163
  }
4084
- async function execDiff(command) {
4164
+ async function computeUncommittedDiffStats() {
4165
+ const stagedNumstat = await gitExec(["diff", "--numstat", "--cached"]);
4166
+ const stagedLines = parseNumstat(stagedNumstat);
4167
+ const stagedStatus = await gitExec(["diff", "--name-status", "--cached"]);
4168
+ const stagedFiles = parseNameStatus(stagedStatus);
4169
+ const unstagedNumstat = await gitExec(["diff", "--numstat"]);
4170
+ const unstagedLines = parseNumstat(unstagedNumstat);
4171
+ const unstagedStatus = await gitExec(["diff", "--name-status"]);
4172
+ const unstagedFiles = parseNameStatus(unstagedStatus);
4173
+ const untrackedList = await gitExec([
4174
+ "ls-files",
4175
+ "--others",
4176
+ "--exclude-standard"
4177
+ ]);
4178
+ const untrackedFiles = untrackedList.split(`
4179
+ `).map((f) => f.trim()).filter((f) => f.length > 0);
4180
+ return {
4181
+ baseRef: "uncommitted",
4182
+ total: stagedFiles.total + unstagedFiles.total + untrackedFiles.length - countOverlap(stagedStatus, unstagedStatus),
4183
+ newFiles: stagedFiles.newFiles + unstagedFiles.newFiles + untrackedFiles.length,
4184
+ modifiedFiles: stagedFiles.modifiedFiles + unstagedFiles.modifiedFiles,
4185
+ deletedFiles: stagedFiles.deletedFiles + unstagedFiles.deletedFiles,
4186
+ linesAdded: stagedLines.linesAdded + unstagedLines.linesAdded,
4187
+ linesRemoved: stagedLines.linesRemoved + unstagedLines.linesRemoved
4188
+ };
4189
+ }
4190
+ async function getStashUntrackedFiles(fixBase, pathFilter) {
4085
4191
  try {
4086
- const { stdout } = await execAsync8(command, {
4087
- maxBuffer: MAX_BUFFER_BYTES
4088
- });
4089
- return stdout;
4090
- } catch (error) {
4091
- const err = error;
4092
- if (typeof err.code === "number" && err.stdout) {
4093
- return err.stdout;
4192
+ const args = ["ls-tree", "-r", "--name-only", `${fixBase}^3`];
4193
+ if (pathFilter) {
4194
+ args.push("--", pathFilter);
4094
4195
  }
4095
- throw error;
4196
+ const treeFiles = await gitExec(args);
4197
+ return new Set(treeFiles.split(`
4198
+ `).map((f) => f.trim()).filter((f) => f.length > 0));
4199
+ } catch {
4200
+ return new Set;
4096
4201
  }
4097
4202
  }
4098
- async function untrackedDiff(entryPointPath) {
4099
- const pArg = pathArg(entryPointPath);
4100
- const { stdout } = await execAsync8(`git ls-files --others --exclude-standard${pArg}`, { maxBuffer: MAX_BUFFER_BYTES });
4101
- const files = parseLines(stdout);
4102
- const diffs = [];
4203
+ async function countChangedStashUntracked(files, fixBase) {
4204
+ let changed = 0;
4103
4205
  for (const file of files) {
4104
4206
  try {
4105
- const diff = await execDiff(`git diff --no-index -- /dev/null ${quoteArg(file)}`);
4106
- if (diff.trim())
4107
- diffs.push(diff);
4108
- } catch (error) {
4109
- const err = error;
4110
- const msg = [err.message, err.stderr].filter(Boolean).join(`
4111
- `);
4112
- if (msg.includes("Could not access") || msg.includes("ENOENT") || msg.includes("No such file")) {
4113
- continue;
4207
+ const [oldHash, newHash] = await Promise.all([
4208
+ gitExec(["rev-parse", `${fixBase}^3:${file}`]),
4209
+ gitExec(["hash-object", "--", file])
4210
+ ]);
4211
+ if (oldHash.trim() !== newHash.trim()) {
4212
+ changed++;
4114
4213
  }
4115
- throw error;
4214
+ } catch {
4215
+ changed++;
4116
4216
  }
4117
4217
  }
4118
- return diffs.join(`
4119
- `);
4120
- }
4121
- async function getDiff(entryPointPath, baseBranch, options) {
4122
- log4.debug(`getDiff: entryPoint=${entryPointPath}, fixBase=${options?.fixBase ?? "none"}, uncommitted=${options?.uncommitted ?? false}, commit=${options?.commit ?? "none"}`);
4123
- if (options?.fixBase) {
4124
- const result = await getFixBaseDiff(entryPointPath, options.fixBase);
4125
- if (result !== null)
4126
- return result;
4127
- }
4128
- if (options?.uncommitted) {
4129
- return getUncommittedDiff(entryPointPath);
4130
- }
4131
- if (options?.commit) {
4132
- return getCommitDiff(entryPointPath, options.commit);
4133
- }
4134
- const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
4135
- return isCI ? getCIDiff(entryPointPath, baseBranch) : getLocalDiff(entryPointPath, baseBranch);
4218
+ return changed;
4136
4219
  }
4137
- async function getFixBaseDiff(entryPointPath, fixBase) {
4138
- if (!/^[a-f0-9]+$/.test(fixBase)) {
4139
- throw new Error(`Invalid session ref: ${fixBase}`);
4140
- }
4141
- const pArg = pathArg(entryPointPath);
4220
+ async function computeFixBaseDiffStats(fixBase) {
4142
4221
  try {
4143
- const diff = await execDiff(`git diff ${fixBase}${pArg}`);
4144
- const { stdout: untrackedStdout } = await execAsync8(`git ls-files --others --exclude-standard${pArg}`, { maxBuffer: MAX_BUFFER_BYTES });
4145
- const currentUntracked = new Set(parseLines(untrackedStdout));
4146
- const { stdout: snapshotFilesStdout } = await execAsync8(`git ls-tree -r --name-only ${fixBase}${pArg}`, { maxBuffer: MAX_BUFFER_BYTES });
4147
- const snapshotFiles = new Set(parseLines(snapshotFilesStdout));
4148
- const newUntracked = [...currentUntracked].filter((f) => !snapshotFiles.has(f));
4149
- const newUntrackedDiffs = await collectUntrackedDiffs(newUntracked);
4150
- const scopedDiff = [diff, ...newUntrackedDiffs].filter(Boolean).join(`
4151
- `);
4152
- log4.debug(`Scoped diff via fixBase: ${scopedDiff.split(`
4153
- `).length} lines`);
4154
- return scopedDiff;
4155
- } catch (error) {
4156
- log4.warn(`Failed to compute diff against fixBase ${fixBase}, falling back to full uncommitted diff. ${error instanceof Error ? error.message : error}`);
4157
- return null;
4158
- }
4159
- }
4160
- async function collectUntrackedDiffs(files) {
4161
- const diffs = [];
4162
- for (const file of files) {
4222
+ const numstat = await gitExec(["diff", "--numstat", fixBase]);
4223
+ const lineStats = parseNumstat(numstat);
4224
+ const nameStatus = await gitExec(["diff", "--name-status", fixBase]);
4225
+ const fileStats = parseNameStatus(nameStatus);
4226
+ const currentUntracked = (await gitExec(["ls-files", "--others", "--exclude-standard"])).split(`
4227
+ `).map((f) => f.trim()).filter((f) => f.length > 0);
4228
+ let fixBaseTrackedFiles;
4229
+ try {
4230
+ const treeFiles = await gitExec([
4231
+ "ls-tree",
4232
+ "-r",
4233
+ "--name-only",
4234
+ fixBase
4235
+ ]);
4236
+ fixBaseTrackedFiles = new Set(treeFiles.split(`
4237
+ `).map((f) => f.trim()).filter((f) => f.length > 0));
4238
+ } catch {
4239
+ fixBaseTrackedFiles = new Set;
4240
+ }
4241
+ const fixBaseUntrackedFiles = await getStashUntrackedFiles(fixBase);
4242
+ const allSnapshotFiles = new Set([
4243
+ ...fixBaseTrackedFiles,
4244
+ ...fixBaseUntrackedFiles
4245
+ ]);
4246
+ const newUntrackedFiles = currentUntracked.filter((f) => !allSnapshotFiles.has(f));
4247
+ const knownUntrackedFiles = currentUntracked.filter((f) => fixBaseUntrackedFiles.has(f));
4248
+ const changedKnownUntracked = await countChangedStashUntracked(knownUntrackedFiles, fixBase);
4249
+ return {
4250
+ baseRef: fixBase,
4251
+ total: fileStats.total + newUntrackedFiles.length + changedKnownUntracked,
4252
+ newFiles: fileStats.newFiles + newUntrackedFiles.length,
4253
+ modifiedFiles: fileStats.modifiedFiles + changedKnownUntracked,
4254
+ deletedFiles: fileStats.deletedFiles,
4255
+ linesAdded: lineStats.linesAdded,
4256
+ linesRemoved: lineStats.linesRemoved
4257
+ };
4258
+ } catch {
4259
+ return emptyDiffStats(fixBase);
4260
+ }
4261
+ }
4262
+ async function computeCIDiffStats(baseBranch) {
4263
+ const headRef = process.env.GITHUB_SHA || "HEAD";
4264
+ try {
4265
+ const numstat = await gitExec([
4266
+ "diff",
4267
+ "--numstat",
4268
+ `${baseBranch}...${headRef}`
4269
+ ]);
4270
+ const lineStats = parseNumstat(numstat);
4271
+ const nameStatus = await gitExec([
4272
+ "diff",
4273
+ "--name-status",
4274
+ `${baseBranch}...${headRef}`
4275
+ ]);
4276
+ const fileStats = parseNameStatus(nameStatus);
4277
+ return {
4278
+ baseRef: baseBranch,
4279
+ ...fileStats,
4280
+ ...lineStats
4281
+ };
4282
+ } catch {
4283
+ try {
4284
+ const numstat = await gitExec(["diff", "--numstat", "HEAD^...HEAD"]);
4285
+ const lineStats = parseNumstat(numstat);
4286
+ const nameStatus = await gitExec([
4287
+ "diff",
4288
+ "--name-status",
4289
+ "HEAD^...HEAD"
4290
+ ]);
4291
+ const fileStats = parseNameStatus(nameStatus);
4292
+ return {
4293
+ baseRef: "HEAD^",
4294
+ ...fileStats,
4295
+ ...lineStats
4296
+ };
4297
+ } catch {
4298
+ return emptyDiffStats(baseBranch);
4299
+ }
4300
+ }
4301
+ }
4302
+ async function computeLocalDiffStats(baseBranch) {
4303
+ const committedNumstat = await gitExec([
4304
+ "diff",
4305
+ "--numstat",
4306
+ `${baseBranch}...HEAD`
4307
+ ]);
4308
+ const committedLines = parseNumstat(committedNumstat);
4309
+ const committedStatus = await gitExec([
4310
+ "diff",
4311
+ "--name-status",
4312
+ `${baseBranch}...HEAD`
4313
+ ]);
4314
+ const committedFiles = parseNameStatus(committedStatus);
4315
+ const uncommittedNumstat = await gitExec(["diff", "--numstat", "HEAD"]);
4316
+ const uncommittedLines = parseNumstat(uncommittedNumstat);
4317
+ const uncommittedStatus = await gitExec(["diff", "--name-status", "HEAD"]);
4318
+ const uncommittedFiles = parseNameStatus(uncommittedStatus);
4319
+ const untrackedList = await gitExec([
4320
+ "ls-files",
4321
+ "--others",
4322
+ "--exclude-standard"
4323
+ ]);
4324
+ const untrackedFiles = untrackedList.split(`
4325
+ `).map((f) => f.trim()).filter((f) => f.length > 0);
4326
+ const totalNew = committedFiles.newFiles + uncommittedFiles.newFiles + untrackedFiles.length;
4327
+ const totalModified = committedFiles.modifiedFiles + uncommittedFiles.modifiedFiles;
4328
+ const totalDeleted = committedFiles.deletedFiles + uncommittedFiles.deletedFiles;
4329
+ return {
4330
+ baseRef: baseBranch,
4331
+ total: totalNew + totalModified + totalDeleted,
4332
+ newFiles: totalNew,
4333
+ modifiedFiles: totalModified,
4334
+ deletedFiles: totalDeleted,
4335
+ linesAdded: committedLines.linesAdded + uncommittedLines.linesAdded,
4336
+ linesRemoved: committedLines.linesRemoved + uncommittedLines.linesRemoved
4337
+ };
4338
+ }
4339
+ function parseNumstat(output) {
4340
+ let linesAdded = 0;
4341
+ let linesRemoved = 0;
4342
+ for (const line of output.split(`
4343
+ `)) {
4344
+ if (!line.trim())
4345
+ continue;
4346
+ const parts = line.split("\t");
4347
+ if (parts.length < 3)
4348
+ continue;
4349
+ const added = parts[0];
4350
+ const removed = parts[1];
4351
+ if (added && added !== "-") {
4352
+ linesAdded += parseInt(added, 10) || 0;
4353
+ }
4354
+ if (removed && removed !== "-") {
4355
+ linesRemoved += parseInt(removed, 10) || 0;
4356
+ }
4357
+ }
4358
+ return { linesAdded, linesRemoved };
4359
+ }
4360
+ function parseNameStatus(output) {
4361
+ let newFiles = 0;
4362
+ let modifiedFiles = 0;
4363
+ let deletedFiles = 0;
4364
+ for (const line of output.split(`
4365
+ `)) {
4366
+ if (!line.trim())
4367
+ continue;
4368
+ const status = line[0];
4369
+ switch (status) {
4370
+ case "A":
4371
+ newFiles++;
4372
+ break;
4373
+ case "M":
4374
+ case "R":
4375
+ case "C":
4376
+ case "T":
4377
+ modifiedFiles++;
4378
+ break;
4379
+ case "D":
4380
+ deletedFiles++;
4381
+ break;
4382
+ }
4383
+ }
4384
+ return {
4385
+ total: newFiles + modifiedFiles + deletedFiles,
4386
+ newFiles,
4387
+ modifiedFiles,
4388
+ deletedFiles
4389
+ };
4390
+ }
4391
+ function countOverlap(status1, status2) {
4392
+ const files1 = new Set;
4393
+ for (const line of status1.split(`
4394
+ `)) {
4395
+ if (!line.trim())
4396
+ continue;
4397
+ const parts = line.split("\t");
4398
+ const file = parts[1];
4399
+ if (parts.length >= 2 && file) {
4400
+ files1.add(file);
4401
+ }
4402
+ }
4403
+ let overlap = 0;
4404
+ for (const line of status2.split(`
4405
+ `)) {
4406
+ if (!line.trim())
4407
+ continue;
4408
+ const parts = line.split("\t");
4409
+ const file = parts[1];
4410
+ if (parts.length >= 2 && file && files1.has(file)) {
4411
+ overlap++;
4412
+ }
4413
+ }
4414
+ return overlap;
4415
+ }
4416
+ function emptyDiffStats(baseRef) {
4417
+ return {
4418
+ baseRef,
4419
+ total: 0,
4420
+ newFiles: 0,
4421
+ modifiedFiles: 0,
4422
+ deletedFiles: 0,
4423
+ linesAdded: 0,
4424
+ linesRemoved: 0
4425
+ };
4426
+ }
4427
+
4428
+ // src/gates/review-diff.ts
4429
+ var log4 = getCategoryLogger("gate", "review");
4430
+ var execAsync8 = promisify9(exec8);
4431
+ function parseLines(stdout) {
4432
+ return stdout.split(`
4433
+ `).map((line) => line.trim()).filter((line) => line.length > 0);
4434
+ }
4435
+ function pathArg(entryPointPath) {
4436
+ return ` -- ${quoteArg(entryPointPath)}`;
4437
+ }
4438
+ function quoteArg(value) {
4439
+ return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
4440
+ }
4441
+ async function execDiff(command) {
4442
+ try {
4443
+ const { stdout } = await execAsync8(command, {
4444
+ maxBuffer: MAX_BUFFER_BYTES
4445
+ });
4446
+ return stdout;
4447
+ } catch (error) {
4448
+ const err = error;
4449
+ if (typeof err.code === "number" && err.stdout) {
4450
+ return err.stdout;
4451
+ }
4452
+ throw error;
4453
+ }
4454
+ }
4455
+ async function untrackedDiff(entryPointPath) {
4456
+ const pArg = pathArg(entryPointPath);
4457
+ const { stdout } = await execAsync8(`git ls-files --others --exclude-standard${pArg}`, { maxBuffer: MAX_BUFFER_BYTES });
4458
+ const files = parseLines(stdout);
4459
+ const diffs = [];
4460
+ for (const file of files) {
4461
+ try {
4462
+ const diff = await execDiff(`git diff --no-index -- /dev/null ${quoteArg(file)}`);
4463
+ if (diff.trim())
4464
+ diffs.push(diff);
4465
+ } catch (error) {
4466
+ const err = error;
4467
+ const msg = [err.message, err.stderr].filter(Boolean).join(`
4468
+ `);
4469
+ if (msg.includes("Could not access") || msg.includes("ENOENT") || msg.includes("No such file")) {
4470
+ continue;
4471
+ }
4472
+ throw error;
4473
+ }
4474
+ }
4475
+ return diffs.join(`
4476
+ `);
4477
+ }
4478
+ async function getDiff(entryPointPath, baseBranch, options) {
4479
+ log4.debug(`getDiff: entryPoint=${entryPointPath}, fixBase=${options?.fixBase ?? "none"}, uncommitted=${options?.uncommitted ?? false}, commit=${options?.commit ?? "none"}`);
4480
+ if (options?.fixBase) {
4481
+ const result = await getFixBaseDiff(entryPointPath, options.fixBase);
4482
+ if (result !== null)
4483
+ return result;
4484
+ }
4485
+ if (options?.uncommitted) {
4486
+ return getUncommittedDiff(entryPointPath);
4487
+ }
4488
+ if (options?.commit) {
4489
+ return getCommitDiff(entryPointPath, options.commit);
4490
+ }
4491
+ const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
4492
+ return isCI ? getCIDiff(entryPointPath, baseBranch) : getLocalDiff(entryPointPath, baseBranch);
4493
+ }
4494
+ async function hasFileChangedSinceStash(file, fixBase) {
4495
+ const [{ stdout: oldHashOut }, { stdout: newHashOut }] = await Promise.all([
4496
+ execAsync8(`git rev-parse ${quoteArg(`${fixBase}^3:${file}`)}`, {
4497
+ maxBuffer: MAX_BUFFER_BYTES
4498
+ }),
4499
+ execAsync8(`git hash-object -- ${quoteArg(file)}`, {
4500
+ maxBuffer: MAX_BUFFER_BYTES
4501
+ })
4502
+ ]);
4503
+ return oldHashOut.trim() !== newHashOut.trim();
4504
+ }
4505
+ async function writeStashFileToTemp(file, fixBase, tmpDir, counter) {
4506
+ const { stdout: oldContent } = await execAsync8(`git show ${quoteArg(`${fixBase}^3:${file}`)}`, { maxBuffer: MAX_BUFFER_BYTES, encoding: "buffer" });
4507
+ const tmpFile = path16.join(tmpDir, `${counter}-${path16.basename(file)}`);
4508
+ await fs18.writeFile(tmpFile, oldContent);
4509
+ return tmpFile;
4510
+ }
4511
+ function placeholderDiff(file) {
4512
+ return `diff --git a/${file} b/${file}
4513
+ --- /dev/null
4514
+ +++ b/${file}
4515
+ @@ -0,0 +1 @@
4516
+ +[gauntlet: diff unavailable for this file]`;
4517
+ }
4518
+ async function collectStashUntrackedDiffs(files, fixBase) {
4519
+ if (files.length === 0)
4520
+ return [];
4521
+ const diffs = [];
4522
+ const tmpDir = await fs18.mkdtemp(path16.join(os7.tmpdir(), "gauntlet-"));
4523
+ try {
4524
+ let counter = 0;
4525
+ for (const file of files) {
4526
+ const d = await diffSingleStashFile(file, fixBase, tmpDir, counter++);
4527
+ if (d)
4528
+ diffs.push(d);
4529
+ }
4530
+ } finally {
4531
+ await fs18.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
4532
+ }
4533
+ return diffs;
4534
+ }
4535
+ async function diffSingleStashFile(file, fixBase, tmpDir, counter) {
4536
+ try {
4537
+ if (!await hasFileChangedSinceStash(file, fixBase)) {
4538
+ return null;
4539
+ }
4540
+ const tmpFile = await writeStashFileToTemp(file, fixBase, tmpDir, counter);
4541
+ const d = await execDiff(`git diff --no-index -- ${quoteArg(tmpFile)} ${quoteArg(file)}`);
4542
+ return d.trim() || null;
4543
+ } catch (outerErr) {
4544
+ log4.debug(`Stash diff failed for ${file}, falling back to full diff: ${outerErr instanceof Error ? outerErr.message : outerErr}`);
4545
+ return diffFallbackToDevNull(file);
4546
+ }
4547
+ }
4548
+ async function diffFallbackToDevNull(file) {
4549
+ try {
4550
+ const d = await execDiff(`git diff --no-index -- /dev/null ${quoteArg(file)}`);
4551
+ return d.trim() || null;
4552
+ } catch (innerErr) {
4553
+ log4.warn(`Failed to compute any diff for ${file}: ${innerErr instanceof Error ? innerErr.message : innerErr}`);
4554
+ return placeholderDiff(file);
4555
+ }
4556
+ }
4557
+ async function getFixBaseDiff(entryPointPath, fixBase) {
4558
+ if (!/^[a-f0-9]+$/.test(fixBase)) {
4559
+ throw new Error(`Invalid session ref: ${fixBase}`);
4560
+ }
4561
+ const pArg = pathArg(entryPointPath);
4562
+ try {
4563
+ const diff = await execDiff(`git diff ${fixBase}${pArg}`);
4564
+ const { stdout: untrackedStdout } = await execAsync8(`git ls-files --others --exclude-standard${pArg}`, { maxBuffer: MAX_BUFFER_BYTES });
4565
+ const currentUntracked = new Set(parseLines(untrackedStdout));
4566
+ const { stdout: snapshotFilesStdout } = await execAsync8(`git ls-tree -r --name-only ${fixBase}${pArg}`, { maxBuffer: MAX_BUFFER_BYTES });
4567
+ const snapshotTrackedFiles = new Set(parseLines(snapshotFilesStdout));
4568
+ const snapshotUntrackedFiles = await getStashUntrackedFiles(fixBase, entryPointPath);
4569
+ const allSnapshotFiles = new Set([
4570
+ ...snapshotTrackedFiles,
4571
+ ...snapshotUntrackedFiles
4572
+ ]);
4573
+ const newUntracked = [...currentUntracked].filter((f) => !allSnapshotFiles.has(f));
4574
+ const knownUntracked = [...currentUntracked].filter((f) => snapshotUntrackedFiles.has(f));
4575
+ const newUntrackedDiffs = await collectUntrackedDiffs(newUntracked);
4576
+ const knownUntrackedDiffs = await collectStashUntrackedDiffs(knownUntracked, fixBase);
4577
+ const scopedDiff = [diff, ...newUntrackedDiffs, ...knownUntrackedDiffs].filter(Boolean).join(`
4578
+ `);
4579
+ log4.debug(`Scoped diff via fixBase: ${scopedDiff.split(`
4580
+ `).length} lines (${newUntracked.length} new, ${knownUntracked.length} known untracked)`);
4581
+ return scopedDiff;
4582
+ } catch (error) {
4583
+ log4.warn(`Failed to compute diff against fixBase ${fixBase}, falling back to full uncommitted diff. ${error instanceof Error ? error.message : error}`);
4584
+ return null;
4585
+ }
4586
+ }
4587
+ async function collectUntrackedDiffs(files) {
4588
+ const diffs = [];
4589
+ for (const file of files) {
4163
4590
  try {
4164
4591
  const d = await execDiff(`git diff --no-index -- /dev/null ${quoteArg(file)}`);
4165
4592
  if (d.trim())
@@ -4725,7 +5152,7 @@ async function handleUsageLimit(adapter, logDir, mainLogger) {
4725
5152
  }
4726
5153
 
4727
5154
  // src/gates/review-helpers.ts
4728
- import fs18 from "node:fs/promises";
5155
+ import fs19 from "node:fs/promises";
4729
5156
  var log6 = getCategoryLogger("gate", "review");
4730
5157
  async function collectHealthyAdapters(preferences, mainLogger, logDir) {
4731
5158
  const healthyAdapters = [];
@@ -4853,7 +5280,7 @@ async function writeSkippedSlotLog(assignment, loggerFactory, logPathsSet, logPa
4853
5280
  violations: [],
4854
5281
  passIteration: assignment.passIteration
4855
5282
  };
4856
- await fs18.writeFile(jsonPath, JSON.stringify(skippedOutput, null, 2));
5283
+ await fs19.writeFile(jsonPath, JSON.stringify(skippedOutput, null, 2));
4857
5284
  if (!logPathsSet.has(logPath)) {
4858
5285
  logPathsSet.add(logPath);
4859
5286
  logPaths.push(logPath);
@@ -5250,19 +5677,19 @@ class Runner {
5250
5677
  }
5251
5678
 
5252
5679
  // src/output/console.ts
5253
- import fs21 from "node:fs/promises";
5680
+ import fs22 from "node:fs/promises";
5254
5681
  import chalk2 from "chalk";
5255
5682
 
5256
5683
  // src/utils/log-parser.ts
5257
- import fs20 from "node:fs/promises";
5258
- import path18 from "node:path";
5684
+ import fs21 from "node:fs/promises";
5685
+ import path19 from "node:path";
5259
5686
 
5260
5687
  // src/utils/log-parser-find-helpers.ts
5261
- import fs19 from "node:fs/promises";
5262
- import path17 from "node:path";
5688
+ import fs20 from "node:fs/promises";
5689
+ import path18 from "node:path";
5263
5690
 
5264
5691
  // src/utils/log-parser-helpers.ts
5265
- import path16 from "node:path";
5692
+ import path17 from "node:path";
5266
5693
  function parseReviewFilename(filename) {
5267
5694
  const m = filename.match(/^(.+)_([^@]+)@(\d+)\.(\d+)\.(log|json)$/);
5268
5695
  if (!m)
@@ -5434,9 +5861,9 @@ async function parseRunFile(logDir, runFiles, prefix, runNum, parseJsonFn, parse
5434
5861
  const jsonFile = runFiles.find((f) => f.startsWith(`${prefix}.${runNum}.`) && f.endsWith(".json"));
5435
5862
  const logFile = runFiles.find((f) => f.startsWith(`${prefix}.${runNum}.`) && f.endsWith(".log"));
5436
5863
  if (jsonFile)
5437
- return parseJsonFn(path16.join(logDir, jsonFile));
5864
+ return parseJsonFn(path17.join(logDir, jsonFile));
5438
5865
  if (logFile)
5439
- return parseLogFn(path16.join(logDir, logFile));
5866
+ return parseLogFn(path17.join(logDir, logFile));
5440
5867
  return null;
5441
5868
  }
5442
5869
  async function collectIterationFailures(logDir, files, runNum, parseJsonFn, parseLogFn) {
@@ -5553,7 +5980,7 @@ function addCheckFile(checkPrefixMap, file) {
5553
5980
  }
5554
5981
  async function isJsonReviewPassing(jsonPath) {
5555
5982
  try {
5556
- const content = await fs19.readFile(jsonPath, "utf-8");
5983
+ const content = await fs20.readFile(jsonPath, "utf-8");
5557
5984
  const data = JSON.parse(content);
5558
5985
  return data.status === "pass" || data.status === "skipped_prior_pass";
5559
5986
  } catch {
@@ -5562,7 +5989,7 @@ async function isJsonReviewPassing(jsonPath) {
5562
5989
  }
5563
5990
  async function isLogReviewPassing(logPath) {
5564
5991
  try {
5565
- const content = await fs19.readFile(logPath, "utf-8");
5992
+ const content = await fs20.readFile(logPath, "utf-8");
5566
5993
  if (content.includes("Status: skipped_prior_pass"))
5567
5994
  return true;
5568
5995
  if (content.includes("--- Review Output")) {
@@ -5618,7 +6045,7 @@ async function processReviewSlots(logDir, reviewSlotMap, includePassedSlots, par
5618
6045
  const reviewIndex = parseInt(slotKey.substring(sepIdx + 1), 10);
5619
6046
  const parsed = parseReviewFilename(fileInfo.filename);
5620
6047
  const adapter = parsed?.adapter || "unknown";
5621
- const filePath = path17.join(logDir, fileInfo.filename);
6048
+ const filePath = path18.join(logDir, fileInfo.filename);
5622
6049
  const isPassing = fileInfo.ext === "json" ? await isJsonReviewPassing(filePath) : await isLogReviewPassing(filePath);
5623
6050
  if (isPassing && includePassedSlots) {
5624
6051
  recordPassedSlot(passedSlots, jobId, reviewIndex, fileInfo.runNumber, adapter);
@@ -5640,9 +6067,9 @@ async function processCheckFiles(logDir, checkPrefixMap, parseJsonFn, parseLogFn
5640
6067
  continue;
5641
6068
  let failure = null;
5642
6069
  if (exts.has("json")) {
5643
- failure = await parseJsonFn(path17.join(logDir, `${prefix}.${latestRun}.json`));
6070
+ failure = await parseJsonFn(path18.join(logDir, `${prefix}.${latestRun}.json`));
5644
6071
  } else if (exts.has("log")) {
5645
- failure = await parseLogFn(path17.join(logDir, `${prefix}.${latestRun}.log`));
6072
+ failure = await parseLogFn(path18.join(logDir, `${prefix}.${latestRun}.log`));
5646
6073
  }
5647
6074
  if (!failure)
5648
6075
  continue;
@@ -5660,9 +6087,9 @@ async function processCheckFiles(logDir, checkPrefixMap, parseJsonFn, parseLogFn
5660
6087
  var log9 = getCategoryLogger("log-parser");
5661
6088
  async function parseJsonReviewFile(jsonPath) {
5662
6089
  try {
5663
- const content = await fs20.readFile(jsonPath, "utf-8");
6090
+ const content = await fs21.readFile(jsonPath, "utf-8");
5664
6091
  const data = JSON.parse(content);
5665
- const filename = path18.basename(jsonPath);
6092
+ const filename = path19.basename(jsonPath);
5666
6093
  const parsed = parseReviewFilename(filename);
5667
6094
  const jobId = parsed ? parsed.jobId : filename.replace(/\.\d+\.json$/, "");
5668
6095
  if (data.status === "pass" || data.status === "skipped_prior_pass") {
@@ -5702,8 +6129,8 @@ async function parseJsonReviewFile(jsonPath) {
5702
6129
  }
5703
6130
  async function parseLogFile(logPath) {
5704
6131
  try {
5705
- const content = await fs20.readFile(logPath, "utf-8");
5706
- const filename = path18.basename(logPath);
6132
+ const content = await fs21.readFile(logPath, "utf-8");
6133
+ const filename = path19.basename(logPath);
5707
6134
  const parsed = parseReviewFilename(filename);
5708
6135
  const jobId = parsed ? parsed.jobId : extractPrefix(filename);
5709
6136
  if (content.includes("--- Review Output")) {
@@ -5716,7 +6143,7 @@ async function parseLogFile(logPath) {
5716
6143
  }
5717
6144
  async function reconstructHistory(logDir) {
5718
6145
  try {
5719
- const files = await fs20.readdir(logDir);
6146
+ const files = await fs21.readdir(logDir);
5720
6147
  const sortedRuns = collectRunNumbers(files);
5721
6148
  const iterations = [];
5722
6149
  let previousFailuresByJob = new Map;
@@ -5733,7 +6160,7 @@ async function reconstructHistory(logDir) {
5733
6160
  }
5734
6161
  async function findPreviousFailures(logDir, gateFilter, includePassedSlots) {
5735
6162
  try {
5736
- const files = await fs20.readdir(logDir);
6163
+ const files = await fs21.readdir(logDir);
5737
6164
  const { reviewSlotMap, checkPrefixMap } = categorizeFiles(files, gateFilter);
5738
6165
  const { jobReviewFailures, passedSlots } = await processReviewSlots(logDir, reviewSlotMap, includePassedSlots, parseJsonReviewFile, parseLogFile);
5739
6166
  const gateFailures = [];
@@ -5743,7 +6170,7 @@ async function findPreviousFailures(logDir, gateFilter, includePassedSlots) {
5743
6170
  gateName: "",
5744
6171
  entryPoint: "",
5745
6172
  adapterFailures,
5746
- logPath: path18.join(logDir, `${jobId}.log`)
6173
+ logPath: path19.join(logDir, `${jobId}.log`)
5747
6174
  });
5748
6175
  }
5749
6176
  const checkFailures = await processCheckFiles(logDir, checkPrefixMap, parseJsonReviewFile, parseLogFile);
@@ -5762,12 +6189,12 @@ async function findPreviousFailures(logDir, gateFilter, includePassedSlots) {
5762
6189
  async function hasSkippedViolationsInLogs(opts) {
5763
6190
  const { logDir } = opts;
5764
6191
  try {
5765
- const files = await fs20.readdir(logDir);
6192
+ const files = await fs21.readdir(logDir);
5766
6193
  for (const file of files) {
5767
6194
  if (!file.endsWith(".json"))
5768
6195
  continue;
5769
6196
  try {
5770
- const content = await fs20.readFile(path18.join(logDir, file), "utf-8");
6197
+ const content = await fs21.readFile(path19.join(logDir, file), "utf-8");
5771
6198
  const data = JSON.parse(content);
5772
6199
  if (data.violations?.some((v) => v.status === "skipped")) {
5773
6200
  return true;
@@ -6025,7 +6452,7 @@ ${chalk2.bold(SEPARATOR)}`);
6025
6452
  const allDetails = [];
6026
6453
  for (const logPath of logPaths) {
6027
6454
  try {
6028
- const logContent = await fs21.readFile(logPath, "utf-8");
6455
+ const logContent = await fs22.readFile(logPath, "utf-8");
6029
6456
  const details = this.parseLogContent(logContent, result.jobId);
6030
6457
  allDetails.push(...details);
6031
6458
  } catch (_error) {
@@ -6064,9 +6491,9 @@ ${chalk2.bold(SEPARATOR)}`);
6064
6491
  }
6065
6492
 
6066
6493
  // src/output/console-log.ts
6067
- import fs22 from "node:fs";
6494
+ import fs23 from "node:fs";
6068
6495
  import fsPromises2 from "node:fs/promises";
6069
- import path19 from "node:path";
6496
+ import path20 from "node:path";
6070
6497
  import { inspect } from "node:util";
6071
6498
  var ANSI_REGEX = /\x1b(?:\[[0-9;?]*[A-Za-z]|[78])/g;
6072
6499
  function stripAnsi(text) {
@@ -6076,9 +6503,9 @@ function formatArgs(args) {
6076
6503
  return args.map((a) => typeof a === "string" ? a : inspect(a, { depth: 4 })).join(" ");
6077
6504
  }
6078
6505
  function openLogFileExclusive(logDir, runNum) {
6079
- const logPath = path19.join(logDir, `console.${runNum}.log`);
6506
+ const logPath = path20.join(logDir, `console.${runNum}.log`);
6080
6507
  try {
6081
- const fd = fs22.openSync(logPath, fs22.constants.O_WRONLY | fs22.constants.O_CREAT | fs22.constants.O_EXCL);
6508
+ const fd = fs23.openSync(logPath, fs23.constants.O_WRONLY | fs23.constants.O_CREAT | fs23.constants.O_EXCL);
6082
6509
  return { fd, logPath };
6083
6510
  } catch (e) {
6084
6511
  const error = e;
@@ -6092,9 +6519,9 @@ function openLogFileExclusive(logDir, runNum) {
6092
6519
  function openLogFileFallback(logDir, startNum) {
6093
6520
  let runNum = startNum;
6094
6521
  for (let attempts = 0;attempts < 100; attempts++) {
6095
- const logPath = path19.join(logDir, `console.${runNum}.log`);
6522
+ const logPath = path20.join(logDir, `console.${runNum}.log`);
6096
6523
  try {
6097
- const fd = fs22.openSync(logPath, fs22.constants.O_WRONLY | fs22.constants.O_CREAT | fs22.constants.O_EXCL);
6524
+ const fd = fs23.openSync(logPath, fs23.constants.O_WRONLY | fs23.constants.O_CREAT | fs23.constants.O_EXCL);
6098
6525
  return { fd, logPath };
6099
6526
  } catch (e) {
6100
6527
  const error = e;
@@ -6115,7 +6542,7 @@ async function startConsoleLog(logDir, runNumber) {
6115
6542
  if (isClosed)
6116
6543
  return;
6117
6544
  try {
6118
- fs22.writeSync(fd, stripAnsi(text));
6545
+ fs23.writeSync(fd, stripAnsi(text));
6119
6546
  } catch {}
6120
6547
  };
6121
6548
  const originalLog = console.log;
@@ -6163,7 +6590,7 @@ async function startConsoleLog(logDir, runNumber) {
6163
6590
  process.stdout.write = originalStdoutWrite;
6164
6591
  process.stderr.write = originalStderrWrite;
6165
6592
  try {
6166
- fs22.closeSync(fd);
6593
+ fs23.closeSync(fd);
6167
6594
  } catch {}
6168
6595
  },
6169
6596
  writeToLogOnly: (text) => {
@@ -6171,20 +6598,20 @@ async function startConsoleLog(logDir, runNumber) {
6171
6598
  }
6172
6599
  };
6173
6600
  } catch (error) {
6174
- fs22.closeSync(fd);
6601
+ fs23.closeSync(fd);
6175
6602
  throw error;
6176
6603
  }
6177
6604
  }
6178
6605
 
6179
6606
  // src/output/logger.ts
6180
- import fs23 from "node:fs/promises";
6181
- import path20 from "node:path";
6607
+ import fs24 from "node:fs/promises";
6608
+ import path21 from "node:path";
6182
6609
  function formatTimestamp() {
6183
6610
  return new Date().toISOString();
6184
6611
  }
6185
6612
  async function computeGlobalRunNumber(logDir) {
6186
6613
  try {
6187
- const files = await fs23.readdir(logDir);
6614
+ const files = await fs24.readdir(logDir);
6188
6615
  let max = 0;
6189
6616
  for (const file of files) {
6190
6617
  if (!(file.endsWith(".log") || file.endsWith(".json")))
@@ -6210,7 +6637,7 @@ class Logger {
6210
6637
  this.logDir = logDir;
6211
6638
  }
6212
6639
  async init() {
6213
- await fs23.mkdir(this.logDir, { recursive: true });
6640
+ await fs24.mkdir(this.logDir, { recursive: true });
6214
6641
  this.globalRunNumber = await computeGlobalRunNumber(this.logDir);
6215
6642
  }
6216
6643
  async close() {}
@@ -6228,14 +6655,14 @@ class Logger {
6228
6655
  } else {
6229
6656
  filename = `${safeName}.${runNum}.log`;
6230
6657
  }
6231
- return path20.join(this.logDir, filename);
6658
+ return path21.join(this.logDir, filename);
6232
6659
  }
6233
6660
  async initFile(logPath) {
6234
6661
  if (this.initializedFiles.has(logPath)) {
6235
6662
  return;
6236
6663
  }
6237
6664
  this.initializedFiles.add(logPath);
6238
- await fs23.writeFile(logPath, "");
6665
+ await fs24.writeFile(logPath, "");
6239
6666
  }
6240
6667
  async createJobLogger(jobId) {
6241
6668
  const logPath = await this.getLogPath(jobId);
@@ -6247,7 +6674,7 @@ class Logger {
6247
6674
  if (lines.length > 0) {
6248
6675
  lines[0] = `[${timestamp}] ${lines[0]}`;
6249
6676
  }
6250
- await fs23.appendFile(logPath, lines.join(`
6677
+ await fs24.appendFile(logPath, lines.join(`
6251
6678
  `) + (text.endsWith(`
6252
6679
  `) ? "" : `
6253
6680
  `));
@@ -6264,7 +6691,7 @@ class Logger {
6264
6691
  if (lines.length > 0) {
6265
6692
  lines[0] = `[${timestamp}] ${lines[0]}`;
6266
6693
  }
6267
- await fs23.appendFile(logPath, lines.join(`
6694
+ await fs24.appendFile(logPath, lines.join(`
6268
6695
  `) + (text.endsWith(`
6269
6696
  `) ? "" : `
6270
6697
  `));
@@ -6280,8 +6707,8 @@ function resolveBaseBranch(options, config) {
6280
6707
  }
6281
6708
 
6282
6709
  // src/commands/shared.ts
6283
- import fs24 from "node:fs/promises";
6284
- import path21 from "node:path";
6710
+ import fs25 from "node:fs/promises";
6711
+ import path22 from "node:path";
6285
6712
  var LOCK_FILENAME = ".gauntlet-run.lock";
6286
6713
  var SESSION_REF_FILENAME2 = ".session_ref";
6287
6714
  async function shouldAutoClean(logDir, baseBranch) {
@@ -6316,17 +6743,17 @@ async function performAutoClean(logDir, result, maxPreviousLogs = 3) {
6316
6743
  }
6317
6744
  async function exists(filePath) {
6318
6745
  try {
6319
- await fs24.stat(filePath);
6746
+ await fs25.stat(filePath);
6320
6747
  return true;
6321
6748
  } catch {
6322
6749
  return false;
6323
6750
  }
6324
6751
  }
6325
6752
  async function acquireLock(logDir) {
6326
- await fs24.mkdir(logDir, { recursive: true });
6327
- const lockPath = path21.resolve(logDir, LOCK_FILENAME);
6753
+ await fs25.mkdir(logDir, { recursive: true });
6754
+ const lockPath = path22.resolve(logDir, LOCK_FILENAME);
6328
6755
  try {
6329
- await fs24.writeFile(lockPath, String(process.pid), { flag: "wx" });
6756
+ await fs25.writeFile(lockPath, String(process.pid), { flag: "wx" });
6330
6757
  } catch (err) {
6331
6758
  if (typeof err === "object" && err !== null && "code" in err && err.code === "EEXIST") {
6332
6759
  console.error(`Error: A gauntlet run is already in progress (lock file: ${lockPath}).`);
@@ -6337,14 +6764,14 @@ async function acquireLock(logDir) {
6337
6764
  }
6338
6765
  }
6339
6766
  async function releaseLock(logDir) {
6340
- const lockPath = path21.resolve(logDir, LOCK_FILENAME);
6767
+ const lockPath = path22.resolve(logDir, LOCK_FILENAME);
6341
6768
  try {
6342
- await fs24.rm(lockPath, { force: true });
6769
+ await fs25.rm(lockPath, { force: true });
6343
6770
  } catch {}
6344
6771
  }
6345
6772
  async function hasExistingLogs(logDir) {
6346
6773
  try {
6347
- const entries = await fs24.readdir(logDir);
6774
+ const entries = await fs25.readdir(logDir);
6348
6775
  return entries.some((f) => (f.endsWith(".log") || f.endsWith(".json")) && f !== "previous" && !f.startsWith("console.") && !f.startsWith("."));
6349
6776
  } catch {
6350
6777
  return false;
@@ -6363,7 +6790,7 @@ function getPersistentFiles() {
6363
6790
  }
6364
6791
  async function hasCurrentLogs(logDir) {
6365
6792
  try {
6366
- const files = await fs24.readdir(logDir);
6793
+ const files = await fs25.readdir(logDir);
6367
6794
  const persistentFiles = getPersistentFiles();
6368
6795
  return files.some((f) => (f.endsWith(".log") || f.endsWith(".json")) && f !== "previous" && !persistentFiles.has(f));
6369
6796
  } catch {
@@ -6375,23 +6802,23 @@ function getCurrentLogFiles(files) {
6375
6802
  return files.filter((file) => !(file.startsWith("previous") || persistentFiles.has(file)));
6376
6803
  }
6377
6804
  async function deleteCurrentLogs(logDir) {
6378
- const files = await fs24.readdir(logDir);
6379
- await Promise.all(getCurrentLogFiles(files).map((file) => fs24.rm(path21.join(logDir, file), { recursive: true, force: true })));
6805
+ const files = await fs25.readdir(logDir);
6806
+ await Promise.all(getCurrentLogFiles(files).map((file) => fs25.rm(path22.join(logDir, file), { recursive: true, force: true })));
6380
6807
  }
6381
6808
  async function rotatePreviousDirs(logDir, maxPreviousLogs) {
6382
6809
  const oldestSuffix = maxPreviousLogs - 1;
6383
6810
  const oldestDir = oldestSuffix === 0 ? "previous" : `previous.${oldestSuffix}`;
6384
- const oldestPath = path21.join(logDir, oldestDir);
6811
+ const oldestPath = path22.join(logDir, oldestDir);
6385
6812
  if (await exists(oldestPath)) {
6386
- await fs24.rm(oldestPath, { recursive: true, force: true });
6813
+ await fs25.rm(oldestPath, { recursive: true, force: true });
6387
6814
  }
6388
6815
  for (let i = oldestSuffix - 1;i >= 0; i--) {
6389
6816
  const fromName = i === 0 ? "previous" : `previous.${i}`;
6390
6817
  const toName = `previous.${i + 1}`;
6391
- const fromPath = path21.join(logDir, fromName);
6392
- const toPath = path21.join(logDir, toName);
6818
+ const fromPath = path22.join(logDir, fromName);
6819
+ const toPath = path22.join(logDir, toName);
6393
6820
  if (await exists(fromPath)) {
6394
- await fs24.rename(fromPath, toPath);
6821
+ await fs25.rename(fromPath, toPath);
6395
6822
  }
6396
6823
  }
6397
6824
  }
@@ -6406,12 +6833,12 @@ async function cleanLogs(logDir, maxPreviousLogs = 3) {
6406
6833
  return;
6407
6834
  }
6408
6835
  await rotatePreviousDirs(logDir, maxPreviousLogs);
6409
- const previousDir = path21.join(logDir, "previous");
6410
- await fs24.mkdir(previousDir, { recursive: true });
6411
- const files = await fs24.readdir(logDir);
6412
- await Promise.all(getCurrentLogFiles(files).map((file) => fs24.rename(path21.join(logDir, file), path21.join(previousDir, file))));
6836
+ const previousDir = path22.join(logDir, "previous");
6837
+ await fs25.mkdir(previousDir, { recursive: true });
6838
+ const files = await fs25.readdir(logDir);
6839
+ await Promise.all(getCurrentLogFiles(files).map((file) => fs25.rename(path22.join(logDir, file), path22.join(previousDir, file))));
6413
6840
  try {
6414
- await fs24.rm(path21.join(logDir, SESSION_REF_FILENAME2), { force: true });
6841
+ await fs25.rm(path22.join(logDir, SESSION_REF_FILENAME2), { force: true });
6415
6842
  } catch {}
6416
6843
  } catch (error) {
6417
6844
  console.warn("Failed to clean logs in", logDir, ":", error instanceof Error ? error.message : error);
@@ -6511,7 +6938,7 @@ async function detectChangesAndGenerateJobs(config, effectiveBaseBranch, changeO
6511
6938
  uncommitted: options.uncommitted
6512
6939
  });
6513
6940
  const expander = new EntryPointExpander;
6514
- const jobGen = new JobGenerator(config);
6941
+ const jobGen = new JobGenerator(config, options.enableReviews);
6515
6942
  console.log(chalk3.dim("Detecting changes..."));
6516
6943
  const changes = await changeDetector.getChangedFiles();
6517
6944
  if (changes.length === 0) {
@@ -6620,14 +7047,14 @@ function registerCheckCommand(program) {
6620
7047
  program.command("check").description("Run only applicable checks for detected changes").option("-b, --base-branch <branch>", "Override base branch for change detection").option("-g, --gate <name>", "Run specific check gate only").option("-c, --commit <sha>", "Use diff for a specific commit").option("-u, --uncommitted", "Use diff for current uncommitted changes (staged and unstaged)").action((options) => executeGateCommand("check", options));
6621
7048
  }
6622
7049
  // src/commands/ci/init.ts
6623
- import fs26 from "node:fs/promises";
6624
- import path23 from "node:path";
7050
+ import fs27 from "node:fs/promises";
7051
+ import path24 from "node:path";
6625
7052
  import chalk4 from "chalk";
6626
7053
  import YAML5 from "yaml";
6627
7054
 
6628
7055
  // src/config/ci-loader.ts
6629
- import fs25 from "node:fs/promises";
6630
- import path22 from "node:path";
7056
+ import fs26 from "node:fs/promises";
7057
+ import path23 from "node:path";
6631
7058
  import YAML4 from "yaml";
6632
7059
 
6633
7060
  // src/config/ci-schema.ts
@@ -6657,17 +7084,17 @@ var ciConfigSchema = z3.object({
6657
7084
  var GAUNTLET_DIR2 = ".gauntlet";
6658
7085
  var CI_FILE = "ci.yml";
6659
7086
  async function loadCIConfig(rootDir = process.cwd()) {
6660
- const ciPath = path22.join(rootDir, GAUNTLET_DIR2, CI_FILE);
7087
+ const ciPath = path23.join(rootDir, GAUNTLET_DIR2, CI_FILE);
6661
7088
  if (!await fileExists3(ciPath)) {
6662
7089
  throw new Error(`CI configuration file not found at ${ciPath}. Run 'agent-gauntlet ci init' to create it.`);
6663
7090
  }
6664
- const content = await fs25.readFile(ciPath, "utf-8");
7091
+ const content = await fs26.readFile(ciPath, "utf-8");
6665
7092
  const raw = YAML4.parse(content);
6666
7093
  return ciConfigSchema.parse(raw);
6667
7094
  }
6668
- async function fileExists3(path23) {
7095
+ async function fileExists3(path24) {
6669
7096
  try {
6670
- const stat = await fs25.stat(path23);
7097
+ const stat = await fs26.stat(path24);
6671
7098
  return stat.isFile();
6672
7099
  } catch {
6673
7100
  return false;
@@ -6758,15 +7185,15 @@ jobs:
6758
7185
 
6759
7186
  // src/commands/ci/init.ts
6760
7187
  async function initCI() {
6761
- const workflowDir = path23.join(process.cwd(), ".github", "workflows");
6762
- const workflowPath = path23.join(workflowDir, "gauntlet.yml");
6763
- const gauntletDir = path23.join(process.cwd(), ".gauntlet");
6764
- const ciConfigPath = path23.join(gauntletDir, "ci.yml");
7188
+ const workflowDir = path24.join(process.cwd(), ".github", "workflows");
7189
+ const workflowPath = path24.join(workflowDir, "gauntlet.yml");
7190
+ const gauntletDir = path24.join(process.cwd(), ".gauntlet");
7191
+ const ciConfigPath = path24.join(gauntletDir, "ci.yml");
6765
7192
  if (await fileExists4(ciConfigPath)) {
6766
7193
  console.log(chalk4.dim("Found existing .gauntlet/ci.yml"));
6767
7194
  } else {
6768
7195
  console.log(chalk4.yellow("Creating starter .gauntlet/ci.yml..."));
6769
- await fs26.mkdir(gauntletDir, { recursive: true });
7196
+ await fs27.mkdir(gauntletDir, { recursive: true });
6770
7197
  const starterContent = `# CI Configuration for Agent Gauntlet
6771
7198
  # Define runtimes, services, and which checks to run in CI.
6772
7199
 
@@ -6788,7 +7215,7 @@ checks:
6788
7215
  # - name: linter
6789
7216
  # requires_runtimes: [ruby]
6790
7217
  `;
6791
- await fs26.writeFile(ciConfigPath, starterContent);
7218
+ await fs27.writeFile(ciConfigPath, starterContent);
6792
7219
  }
6793
7220
  let ciConfig;
6794
7221
  try {
@@ -6797,7 +7224,7 @@ checks:
6797
7224
  console.warn(chalk4.yellow("Could not load CI config to inject services. Workflow will have no services defined."));
6798
7225
  }
6799
7226
  console.log(chalk4.dim(`Generating ${workflowPath}...`));
6800
- await fs26.mkdir(workflowDir, { recursive: true });
7227
+ await fs27.mkdir(workflowDir, { recursive: true });
6801
7228
  let templateContent = workflow_default;
6802
7229
  if (ciConfig?.services && Object.keys(ciConfig.services).length > 0) {
6803
7230
  const servicesYaml = YAML5.stringify({ services: ciConfig.services });
@@ -6809,12 +7236,12 @@ checks:
6809
7236
  templateContent = templateContent.replace(` # Services will be injected here by agent-gauntlet
6810
7237
  `, "");
6811
7238
  }
6812
- await fs26.writeFile(workflowPath, templateContent);
7239
+ await fs27.writeFile(workflowPath, templateContent);
6813
7240
  console.log(chalk4.green("Successfully generated GitHub Actions workflow!"));
6814
7241
  }
6815
- async function fileExists4(path24) {
7242
+ async function fileExists4(path25) {
6816
7243
  try {
6817
- const stat = await fs26.stat(path24);
7244
+ const stat = await fs27.stat(path25);
6818
7245
  return stat.isFile();
6819
7246
  } catch {
6820
7247
  return false;
@@ -7024,18 +7451,18 @@ function printJobsByWorkDir(jobs) {
7024
7451
  }
7025
7452
  }
7026
7453
  // src/commands/health.ts
7027
- import path26 from "node:path";
7454
+ import path27 from "node:path";
7028
7455
  import chalk7 from "chalk";
7029
7456
 
7030
7457
  // src/config/validator.ts
7031
- import fs28 from "node:fs/promises";
7032
- import path25 from "node:path";
7458
+ import fs29 from "node:fs/promises";
7459
+ import path26 from "node:path";
7033
7460
  import YAML7 from "yaml";
7034
7461
  import { ZodError as ZodError2 } from "zod";
7035
7462
 
7036
7463
  // src/config/validate-reviews.ts
7037
- import fs27 from "node:fs/promises";
7038
- import path24 from "node:path";
7464
+ import fs28 from "node:fs/promises";
7465
+ import path25 from "node:path";
7039
7466
  import matter2 from "gray-matter";
7040
7467
  import YAML6 from "yaml";
7041
7468
  import { ZodError } from "zod";
@@ -7044,7 +7471,7 @@ async function validateReviewGates(reviewsPath, issues, filesChecked) {
7044
7471
  const reviewSourceFiles = {};
7045
7472
  const existingReviewNames = new Set;
7046
7473
  try {
7047
- const reviewFiles = await fs27.readdir(reviewsPath);
7474
+ const reviewFiles = await fs28.readdir(reviewsPath);
7048
7475
  detectDuplicateReviewNames2(reviewFiles, reviewsPath, issues);
7049
7476
  for (const file of reviewFiles) {
7050
7477
  if (file.endsWith(".md")) {
@@ -7067,7 +7494,7 @@ function detectDuplicateReviewNames2(reviewFiles, reviewsPath, issues) {
7067
7494
  const reviewNameSources = new Map;
7068
7495
  for (const file of reviewFiles) {
7069
7496
  if (file.endsWith(".md") || file.endsWith(".yml") || file.endsWith(".yaml")) {
7070
- const name = path24.basename(file, path24.extname(file));
7497
+ const name = path25.basename(file, path25.extname(file));
7071
7498
  const sources = reviewNameSources.get(name) || [];
7072
7499
  sources.push(file);
7073
7500
  reviewNameSources.set(name, sources);
@@ -7084,12 +7511,12 @@ function detectDuplicateReviewNames2(reviewFiles, reviewsPath, issues) {
7084
7511
  }
7085
7512
  }
7086
7513
  async function validateMarkdownReview(file, reviewsPath, reviews, reviewSourceFiles, existingReviewNames, issues, filesChecked) {
7087
- const filePath = path24.join(reviewsPath, file);
7088
- const reviewName = path24.basename(file, ".md");
7514
+ const filePath = path25.join(reviewsPath, file);
7515
+ const reviewName = path25.basename(file, ".md");
7089
7516
  existingReviewNames.add(reviewName);
7090
7517
  filesChecked.push(filePath);
7091
7518
  try {
7092
- const content = await fs27.readFile(filePath, "utf-8");
7519
+ const content = await fs28.readFile(filePath, "utf-8");
7093
7520
  const { data: frontmatter, content: _promptBody } = matter2(content);
7094
7521
  if (!frontmatter || Object.keys(frontmatter).length === 0) {
7095
7522
  issues.push({
@@ -7101,7 +7528,7 @@ async function validateMarkdownReview(file, reviewsPath, reviews, reviewSourceFi
7101
7528
  }
7102
7529
  validateCliPreferenceTools(frontmatter, filePath, issues);
7103
7530
  const parsedFrontmatter = reviewPromptFrontmatterSchema.parse(frontmatter);
7104
- const name = path24.basename(file, ".md");
7531
+ const name = path25.basename(file, ".md");
7105
7532
  reviews[name] = parsedFrontmatter;
7106
7533
  reviewSourceFiles[name] = filePath;
7107
7534
  validateReviewSemantics(parsedFrontmatter, filePath, issues);
@@ -7110,12 +7537,12 @@ async function validateMarkdownReview(file, reviewsPath, reviews, reviewSourceFi
7110
7537
  }
7111
7538
  }
7112
7539
  async function validateYamlReview(file, reviewsPath, reviews, reviewSourceFiles, existingReviewNames, issues, filesChecked) {
7113
- const filePath = path24.join(reviewsPath, file);
7114
- const reviewName = path24.basename(file, path24.extname(file));
7540
+ const filePath = path25.join(reviewsPath, file);
7541
+ const reviewName = path25.basename(file, path25.extname(file));
7115
7542
  existingReviewNames.add(reviewName);
7116
7543
  filesChecked.push(filePath);
7117
7544
  try {
7118
- const content = await fs27.readFile(filePath, "utf-8");
7545
+ const content = await fs28.readFile(filePath, "utf-8");
7119
7546
  const raw = YAML6.parse(content);
7120
7547
  validateCliPreferenceTools(raw, filePath, issues);
7121
7548
  const parsed = reviewYamlSchema.parse(raw);
@@ -7248,8 +7675,8 @@ var CONFIG_FILE2 = "config.yml";
7248
7675
  var CHECKS_DIR2 = "checks";
7249
7676
  var REVIEWS_DIR2 = "reviews";
7250
7677
  async function validateConfig(rootDir = process.cwd()) {
7251
- const gauntletPath = path25.join(rootDir, GAUNTLET_DIR3);
7252
- const configPath = path25.join(gauntletPath, CONFIG_FILE2);
7678
+ const gauntletPath = path26.join(rootDir, GAUNTLET_DIR3);
7679
+ const configPath = path26.join(gauntletPath, CONFIG_FILE2);
7253
7680
  const ctx = {
7254
7681
  gauntletPath,
7255
7682
  configPath,
@@ -7273,7 +7700,7 @@ async function validateProjectConfig(ctx) {
7273
7700
  try {
7274
7701
  if (await fileExists5(ctx.configPath)) {
7275
7702
  ctx.filesChecked.push(ctx.configPath);
7276
- const configContent = await fs28.readFile(ctx.configPath, "utf-8");
7703
+ const configContent = await fs29.readFile(ctx.configPath, "utf-8");
7277
7704
  return parseProjectConfig(configContent, ctx);
7278
7705
  }
7279
7706
  ctx.issues.push({
@@ -7315,12 +7742,12 @@ function parseProjectConfig(configContent, ctx) {
7315
7742
  async function validateCheckGates(ctx) {
7316
7743
  const checks = {};
7317
7744
  const existingCheckNames = new Set;
7318
- const checksPath = path25.join(ctx.gauntletPath, CHECKS_DIR2);
7745
+ const checksPath = path26.join(ctx.gauntletPath, CHECKS_DIR2);
7319
7746
  if (!await dirExists2(checksPath)) {
7320
7747
  return { checks, existingCheckNames };
7321
7748
  }
7322
7749
  try {
7323
- const checkFiles = await fs28.readdir(checksPath);
7750
+ const checkFiles = await fs29.readdir(checksPath);
7324
7751
  for (const file of checkFiles) {
7325
7752
  if (file.endsWith(".yml") || file.endsWith(".yaml")) {
7326
7753
  await parseCheckFile(file, checksPath, checks, existingCheckNames, ctx);
@@ -7337,11 +7764,11 @@ async function validateCheckGates(ctx) {
7337
7764
  return { checks, existingCheckNames };
7338
7765
  }
7339
7766
  async function parseCheckFile(file, checksPath, checks, existingCheckNames, ctx) {
7340
- const filePath = path25.join(checksPath, file);
7767
+ const filePath = path26.join(checksPath, file);
7341
7768
  ctx.filesChecked.push(filePath);
7342
- const name = path25.basename(file, path25.extname(file));
7769
+ const name = path26.basename(file, path26.extname(file));
7343
7770
  try {
7344
- const content = await fs28.readFile(filePath, "utf-8");
7771
+ const content = await fs29.readFile(filePath, "utf-8");
7345
7772
  const raw = YAML7.parse(content);
7346
7773
  const parsed = checkGateSchema.parse(raw);
7347
7774
  existingCheckNames.add(name);
@@ -7371,7 +7798,7 @@ async function parseCheckFile(file, checksPath, checks, existingCheckNames, ctx)
7371
7798
  }
7372
7799
  }
7373
7800
  async function validateReviewGatesWrapper(ctx) {
7374
- const reviewsPath = path25.join(ctx.gauntletPath, REVIEWS_DIR2);
7801
+ const reviewsPath = path26.join(ctx.gauntletPath, REVIEWS_DIR2);
7375
7802
  if (!await dirExists2(reviewsPath)) {
7376
7803
  return {
7377
7804
  reviews: {},
@@ -7515,12 +7942,12 @@ function validateDefaultPreferenceTools(defaults, ctx) {
7515
7942
  }
7516
7943
  }
7517
7944
  function validateReviewPreferencesAgainstDefaults(defaults, reviews, reviewSourceFiles, ctx) {
7518
- const reviewsPath = path25.join(ctx.gauntletPath, REVIEWS_DIR2);
7945
+ const reviewsPath = path26.join(ctx.gauntletPath, REVIEWS_DIR2);
7519
7946
  const allowedTools = new Set(defaults);
7520
7947
  for (const [reviewName, reviewConfig] of Object.entries(reviews)) {
7521
7948
  const pref = reviewConfig.cli_preference;
7522
7949
  if (pref && Array.isArray(pref)) {
7523
- const reviewFile = reviewSourceFiles[reviewName] || path25.join(reviewsPath, `${reviewName}.md`);
7950
+ const reviewFile = reviewSourceFiles[reviewName] || path26.join(reviewsPath, `${reviewName}.md`);
7524
7951
  for (let i = 0;i < pref.length; i++) {
7525
7952
  const tool = pref[i];
7526
7953
  if (!allowedTools.has(tool)) {
@@ -7553,7 +7980,7 @@ function pushYamlOrParseError(error, filePath, issues) {
7553
7980
  }
7554
7981
  async function fileExists5(filePath) {
7555
7982
  try {
7556
- const stat = await fs28.stat(filePath);
7983
+ const stat = await fs29.stat(filePath);
7557
7984
  return stat.isFile();
7558
7985
  } catch {
7559
7986
  return false;
@@ -7561,7 +7988,7 @@ async function fileExists5(filePath) {
7561
7988
  }
7562
7989
  async function dirExists2(dirPath) {
7563
7990
  try {
7564
- const stat = await fs28.stat(dirPath);
7991
+ const stat = await fs29.stat(dirPath);
7565
7992
  return stat.isDirectory();
7566
7993
  } catch {
7567
7994
  return false;
@@ -7582,7 +8009,7 @@ function formatHealthResult(health) {
7582
8009
  function displayValidationIssues(validationResult) {
7583
8010
  const issuesByFile = new Map;
7584
8011
  for (const issue of validationResult.issues) {
7585
- const relativeFile = path26.relative(process.cwd(), issue.file);
8012
+ const relativeFile = path27.relative(process.cwd(), issue.file);
7586
8013
  if (!issuesByFile.has(relativeFile)) {
7587
8014
  issuesByFile.set(relativeFile, []);
7588
8015
  }
@@ -7605,7 +8032,7 @@ async function validateAndDisplayConfig() {
7605
8032
  return;
7606
8033
  }
7607
8034
  for (const file of validationResult.filesChecked) {
7608
- const relativePath = path26.relative(process.cwd(), file);
8035
+ const relativePath = path27.relative(process.cwd(), file);
7609
8036
  console.log(chalk7.dim(` ${relativePath}`));
7610
8037
  }
7611
8038
  if (validationResult.valid && validationResult.issues.length === 0) {
@@ -7712,15 +8139,15 @@ function registerHelpCommand(program) {
7712
8139
  // src/commands/init.ts
7713
8140
  import { execFileSync } from "node:child_process";
7714
8141
  import { statSync } from "node:fs";
7715
- import fs31 from "node:fs/promises";
7716
- import path29 from "node:path";
8142
+ import fs32 from "node:fs/promises";
8143
+ import path30 from "node:path";
7717
8144
  import { fileURLToPath } from "node:url";
7718
8145
  import chalk11 from "chalk";
7719
8146
 
7720
8147
  // src/commands/init-checksums.ts
7721
8148
  import { createHash } from "node:crypto";
7722
- import fs29 from "node:fs/promises";
7723
- import path27 from "node:path";
8149
+ import fs30 from "node:fs/promises";
8150
+ import path28 from "node:path";
7724
8151
  async function computeSkillChecksum(skillDir) {
7725
8152
  const files = await collectFiles(skillDir);
7726
8153
  files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
@@ -7753,22 +8180,22 @@ function isGauntletHookEntry(entry) {
7753
8180
  async function collectFiles(dir, baseDir) {
7754
8181
  const base = baseDir ?? dir;
7755
8182
  const results = [];
7756
- const entries = await fs29.readdir(dir, { withFileTypes: true });
8183
+ const entries = await fs30.readdir(dir, { withFileTypes: true });
7757
8184
  for (const entry of entries) {
7758
- const fullPath = path27.join(dir, entry.name);
8185
+ const fullPath = path28.join(dir, entry.name);
7759
8186
  if (entry.isDirectory()) {
7760
8187
  results.push(...await collectFiles(fullPath, base));
7761
8188
  } else if (entry.isFile()) {
7762
- const content = await fs29.readFile(fullPath, "utf-8");
7763
- results.push({ relativePath: path27.relative(base, fullPath), content });
8189
+ const content = await fs30.readFile(fullPath, "utf-8");
8190
+ results.push({ relativePath: path28.relative(base, fullPath), content });
7764
8191
  }
7765
8192
  }
7766
8193
  return results;
7767
8194
  }
7768
8195
 
7769
8196
  // src/commands/init-hooks.ts
7770
- import fs30 from "node:fs/promises";
7771
- import path28 from "node:path";
8197
+ import fs31 from "node:fs/promises";
8198
+ import path29 from "node:path";
7772
8199
  import chalk10 from "chalk";
7773
8200
 
7774
8201
  // src/commands/init-prompts.ts
@@ -7846,11 +8273,11 @@ async function mergeHookConfig(opts) {
7846
8273
  wrapInHooksArray,
7847
8274
  baseConfig
7848
8275
  } = opts;
7849
- await fs30.mkdir(path28.dirname(filePath), { recursive: true });
8276
+ await fs31.mkdir(path29.dirname(filePath), { recursive: true });
7850
8277
  let existing = {};
7851
8278
  if (await exists(filePath)) {
7852
8279
  try {
7853
- existing = JSON.parse(await fs30.readFile(filePath, "utf-8"));
8280
+ existing = JSON.parse(await fs31.readFile(filePath, "utf-8"));
7854
8281
  } catch {
7855
8282
  existing = {};
7856
8283
  }
@@ -7867,7 +8294,7 @@ async function mergeHookConfig(opts) {
7867
8294
  ...existing,
7868
8295
  hooks: { ...existingHooks, [hookKey]: newEntries }
7869
8296
  };
7870
- await fs30.writeFile(filePath, `${JSON.stringify(merged, null, 2)}
8297
+ await fs31.writeFile(filePath, `${JSON.stringify(merged, null, 2)}
7871
8298
  `);
7872
8299
  return true;
7873
8300
  }
@@ -7944,7 +8371,7 @@ function buildHookSpec(target) {
7944
8371
  const purpose = isStop ? "gauntlet will run automatically when agent stops" : "agent will be primed with gauntlet instructions at session start";
7945
8372
  return {
7946
8373
  config: {
7947
- filePath: path28.join(projectRoot, cfg.dir, cfg.file),
8374
+ filePath: path29.join(projectRoot, cfg.dir, cfg.file),
7948
8375
  hookKey: cfg.hookKey,
7949
8376
  hookEntry: cfg.entry,
7950
8377
  deduplicateCmd: cfg.cmd,
@@ -7960,7 +8387,7 @@ async function installHookWithChecksums(target, skipPrompts) {
7960
8387
  let existingConfig = {};
7961
8388
  if (await exists(spec.config.filePath)) {
7962
8389
  try {
7963
- existingConfig = JSON.parse(await fs30.readFile(spec.config.filePath, "utf-8"));
8390
+ existingConfig = JSON.parse(await fs31.readFile(spec.config.filePath, "utf-8"));
7964
8391
  } catch {
7965
8392
  existingConfig = {};
7966
8393
  }
@@ -7994,8 +8421,8 @@ async function installHookWithChecksums(target, skipPrompts) {
7994
8421
  ...existingConfig,
7995
8422
  hooks: { ...existingHooks, [spec.config.hookKey]: newEntries }
7996
8423
  };
7997
- await fs30.mkdir(path28.dirname(spec.config.filePath), { recursive: true });
7998
- await fs30.writeFile(spec.config.filePath, `${JSON.stringify(merged, null, 2)}
8424
+ await fs31.mkdir(path29.dirname(spec.config.filePath), { recursive: true });
8425
+ await fs31.writeFile(spec.config.filePath, `${JSON.stringify(merged, null, 2)}
7999
8426
  `);
8000
8427
  console.log(chalk10.green(spec.installedMsg));
8001
8428
  }
@@ -8013,10 +8440,10 @@ async function installHooksForAdapters(projectRoot, devAdapters, skipPrompts) {
8013
8440
  }
8014
8441
 
8015
8442
  // src/commands/init.ts
8016
- var __dirname2 = path29.dirname(fileURLToPath(import.meta.url));
8443
+ var __dirname2 = path30.dirname(fileURLToPath(import.meta.url));
8017
8444
  var SKILLS_SOURCE_DIR = (() => {
8018
- const bundled = path29.join(__dirname2, "..", "skills");
8019
- const dev = path29.join(__dirname2, "..", "..", "skills");
8445
+ const bundled = path30.join(__dirname2, "..", "skills");
8446
+ const dev = path30.join(__dirname2, "..", "..", "skills");
8020
8447
  try {
8021
8448
  statSync(bundled);
8022
8449
  return bundled;
@@ -8027,14 +8454,10 @@ var SKILLS_SOURCE_DIR = (() => {
8027
8454
  throw err;
8028
8455
  }
8029
8456
  })();
8030
- var SKILL_ACTIONS = ["run", "check", "status", "help", "setup"];
8031
- var SKILL_DESCRIPTIONS = {
8032
- run: "Run the verification suite",
8033
- check: "Run checks only (no reviews)",
8034
- status: "Show gauntlet status",
8035
- help: "Diagnose and explain gauntlet behavior",
8036
- setup: "Configure checks and reviews interactively"
8037
- };
8457
+ async function getSkillDirNames() {
8458
+ const entries = await fs32.readdir(SKILLS_SOURCE_DIR, { withFileTypes: true });
8459
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
8460
+ }
8038
8461
  var CLI_PREFERENCE_ORDER = [
8039
8462
  "codex",
8040
8463
  "claude",
@@ -8061,7 +8484,7 @@ function registerInitCommand(program) {
8061
8484
  }
8062
8485
  async function runInit(options) {
8063
8486
  const projectRoot = process.cwd();
8064
- const targetDir = path29.join(projectRoot, ".gauntlet");
8487
+ const targetDir = path30.join(projectRoot, ".gauntlet");
8065
8488
  const skipPrompts = options.yes ?? false;
8066
8489
  console.log("Detecting available CLI agents...");
8067
8490
  const availableAdapters = await detectAvailableCLIs();
@@ -8093,7 +8516,7 @@ async function runInit(options) {
8093
8516
  }
8094
8517
  await installExternalFiles(projectRoot, hookAdapters, skipPrompts);
8095
8518
  await addToGitignore(projectRoot, "gauntlet_logs");
8096
- printPostInitInstructions(instructionCLINames);
8519
+ await printPostInitInstructions(instructionCLINames);
8097
8520
  }
8098
8521
  function printNoCLIsMessage() {
8099
8522
  console.log();
@@ -8108,35 +8531,34 @@ async function scaffoldGauntletDir(_projectRoot, targetDir, reviewCLINames, numR
8108
8531
  console.log(chalk11.dim(".gauntlet/ already exists, skipping scaffolding"));
8109
8532
  return;
8110
8533
  }
8111
- await fs31.mkdir(targetDir);
8112
- await fs31.mkdir(path29.join(targetDir, "checks"));
8113
- await fs31.mkdir(path29.join(targetDir, "reviews"));
8534
+ await fs32.mkdir(targetDir);
8535
+ await fs32.mkdir(path30.join(targetDir, "checks"));
8536
+ await fs32.mkdir(path30.join(targetDir, "reviews"));
8114
8537
  await writeConfigYml(targetDir, reviewCLINames);
8115
- await fs31.writeFile(path29.join(targetDir, "reviews", "code-quality.yml"), `builtin: code-quality
8538
+ await fs32.writeFile(path30.join(targetDir, "reviews", "code-quality.yml"), `builtin: code-quality
8116
8539
  num_reviews: ${numReviews}
8117
8540
  `);
8118
8541
  console.log(chalk11.green("Created .gauntlet/reviews/code-quality.yml"));
8119
8542
  }
8120
8543
  async function copyDirRecursive(opts) {
8121
- await fs31.mkdir(opts.dest, { recursive: true });
8122
- const entries = await fs31.readdir(opts.src, { withFileTypes: true });
8544
+ await fs32.mkdir(opts.dest, { recursive: true });
8545
+ const entries = await fs32.readdir(opts.src, { withFileTypes: true });
8123
8546
  for (const entry of entries) {
8124
- const srcPath = path29.join(opts.src, entry.name);
8125
- const destPath = path29.join(opts.dest, entry.name);
8547
+ const srcPath = path30.join(opts.src, entry.name);
8548
+ const destPath = path30.join(opts.dest, entry.name);
8126
8549
  if (entry.isDirectory()) {
8127
8550
  await copyDirRecursive({ src: srcPath, dest: destPath });
8128
8551
  } else {
8129
- await fs31.copyFile(srcPath, destPath);
8552
+ await fs32.copyFile(srcPath, destPath);
8130
8553
  }
8131
8554
  }
8132
8555
  }
8133
8556
  async function installSkillsWithChecksums(projectRoot, skipPrompts) {
8134
- const skillsDir = path29.join(projectRoot, ".claude", "skills");
8135
- for (const action of SKILL_ACTIONS) {
8136
- const dirName = `gauntlet-${action}`;
8137
- const sourceDir = path29.join(SKILLS_SOURCE_DIR, dirName);
8138
- const targetDir = path29.join(skillsDir, dirName);
8139
- const relativeDir = `${path29.relative(projectRoot, targetDir)}/`;
8557
+ const skillsDir = path30.join(projectRoot, ".claude", "skills");
8558
+ for (const dirName of await getSkillDirNames()) {
8559
+ const sourceDir = path30.join(SKILLS_SOURCE_DIR, dirName);
8560
+ const targetDir = path30.join(skillsDir, dirName);
8561
+ const relativeDir = `${path30.relative(projectRoot, targetDir)}/`;
8140
8562
  if (!await exists(targetDir)) {
8141
8563
  await copyDirRecursive({ src: sourceDir, dest: targetDir });
8142
8564
  console.log(chalk11.green(`Created ${relativeDir}`));
@@ -8149,7 +8571,7 @@ async function installSkillsWithChecksums(projectRoot, skipPrompts) {
8149
8571
  const shouldOverwrite = await promptFileOverwrite(dirName, skipPrompts);
8150
8572
  if (!shouldOverwrite)
8151
8573
  continue;
8152
- await fs31.rm(targetDir, { recursive: true, force: true });
8574
+ await fs32.rm(targetDir, { recursive: true, force: true });
8153
8575
  await copyDirRecursive({ src: sourceDir, dest: targetDir });
8154
8576
  console.log(chalk11.green(`Updated ${relativeDir}`));
8155
8577
  }
@@ -8158,7 +8580,7 @@ async function installExternalFiles(projectRoot, devAdapters, skipPrompts) {
8158
8580
  await installSkillsWithChecksums(projectRoot, skipPrompts);
8159
8581
  await installHooksForAdapters(projectRoot, devAdapters, skipPrompts);
8160
8582
  }
8161
- function printPostInitInstructions(devCLINames) {
8583
+ async function printPostInitInstructions(devCLINames) {
8162
8584
  const hasNative = devCLINames.some((name) => NATIVE_CLIS.has(name));
8163
8585
  const nonNativeNames = devCLINames.filter((name) => !NATIVE_CLIS.has(name));
8164
8586
  const hasNonNative = nonNativeNames.length > 0;
@@ -8170,8 +8592,8 @@ function printPostInitInstructions(devCLINames) {
8170
8592
  console.log(chalk11.bold("To complete setup, reference the setup skill in your CLI: @.claude/skills/gauntlet-setup/SKILL.md. This will guide you through configuring the static checks (unit tests, linters, etc.) that Agent Gauntlet will run."));
8171
8593
  console.log();
8172
8594
  console.log("Available skills:");
8173
- for (const action of SKILL_ACTIONS) {
8174
- console.log(` @.claude/skills/gauntlet-${action}/SKILL.md — ${SKILL_DESCRIPTIONS[action]}`);
8595
+ for (const dirName of await getSkillDirNames()) {
8596
+ console.log(` @.claude/skills/${dirName}/SKILL.md`);
8175
8597
  }
8176
8598
  }
8177
8599
  }
@@ -8233,14 +8655,14 @@ entry_points: []
8233
8655
  # enabled: true
8234
8656
  # format: text # Options: text, json
8235
8657
  `;
8236
- await fs31.writeFile(path29.join(targetDir, "config.yml"), content);
8658
+ await fs32.writeFile(path30.join(targetDir, "config.yml"), content);
8237
8659
  console.log(chalk11.green("Created .gauntlet/config.yml"));
8238
8660
  }
8239
8661
  async function addToGitignore(projectRoot, entry) {
8240
- const gitignorePath = path29.join(projectRoot, ".gitignore");
8662
+ const gitignorePath = path30.join(projectRoot, ".gitignore");
8241
8663
  let content = "";
8242
8664
  if (await exists(gitignorePath)) {
8243
- content = await fs31.readFile(gitignorePath, "utf-8");
8665
+ content = await fs32.readFile(gitignorePath, "utf-8");
8244
8666
  const lines = content.split(`
8245
8667
  `).map((l) => l.trim());
8246
8668
  if (lines.includes(entry))
@@ -8249,7 +8671,7 @@ async function addToGitignore(projectRoot, entry) {
8249
8671
  const suffix = content.length > 0 && !content.endsWith(`
8250
8672
  `) ? `
8251
8673
  ` : "";
8252
- await fs31.appendFile(gitignorePath, `${suffix}${entry}
8674
+ await fs32.appendFile(gitignorePath, `${suffix}${entry}
8253
8675
  `);
8254
8676
  console.log(chalk11.green(`Added ${entry} to .gitignore`));
8255
8677
  }
@@ -8298,348 +8720,411 @@ ${lines.join(`
8298
8720
  `;
8299
8721
  }
8300
8722
  async function detectAvailableCLIs() {
8301
- const allAdapters = [...getAllAdapters()].sort((a, b) => CLI_PREFERENCE_ORDER.indexOf(a.name) - CLI_PREFERENCE_ORDER.indexOf(b.name));
8302
- const available = [];
8303
- for (const adapter of allAdapters) {
8304
- const isAvailable = await adapter.isAvailable();
8305
- if (isAvailable) {
8306
- console.log(chalk11.green(` ✓ ${adapter.name}`));
8307
- available.push(adapter);
8308
- } else {
8309
- console.log(chalk11.dim(` ✗ ${adapter.name} (not installed)`));
8310
- }
8311
- }
8312
- return available;
8313
- }
8314
- // src/commands/list.ts
8315
- import chalk12 from "chalk";
8316
- function registerListCommand(program) {
8317
- program.command("list").description("List configured gates").action(async () => {
8318
- try {
8319
- const config = await loadConfig();
8320
- console.log(chalk12.bold("Check Gates:"));
8321
- for (const c of Object.values(config.checks)) {
8322
- console.log(` - ${c.name}`);
8323
- }
8324
- console.log(chalk12.bold(`
8325
- Review Gates:`));
8326
- for (const r of Object.values(config.reviews)) {
8327
- console.log(` - ${r.name} (Tools: ${r.cli_preference?.join(", ")})`);
8328
- }
8329
- console.log(chalk12.bold(`
8330
- Entry Points:`));
8331
- for (const ep of config.project.entry_points) {
8332
- console.log(` - ${ep.path}`);
8333
- if (ep.checks)
8334
- console.log(` Checks: ${ep.checks.join(", ")}`);
8335
- if (ep.reviews)
8336
- console.log(` Reviews: ${ep.reviews.join(", ")}`);
8337
- }
8338
- } catch (error) {
8339
- const err = error;
8340
- console.error(chalk12.red("Error:"), err.message);
8341
- }
8342
- });
8343
- }
8344
- // src/commands/review.ts
8345
- function registerReviewCommand(program) {
8346
- program.command("review").description("Run only applicable reviews for detected changes").option("-b, --base-branch <branch>", "Override base branch for change detection").option("-g, --gate <name>", "Run specific review gate only").option("-c, --commit <sha>", "Use diff for a specific commit").option("-u, --uncommitted", "Use diff for current uncommitted changes (staged and unstaged)").action((options) => executeGateCommand("review", options));
8347
- }
8348
- // src/core/diff-stats.ts
8349
- import { execFile } from "node:child_process";
8350
- import { promisify as promisify9 } from "node:util";
8351
- var execFileAsyncOriginal = promisify9(execFile);
8352
- var execFileAsync = execFileAsyncOriginal;
8353
- async function gitExec(args) {
8354
- const { stdout } = await execFileAsync("git", args);
8355
- return stdout;
8356
- }
8357
- async function computeDiffStats(baseBranch, options = {}) {
8358
- if (options.commit) {
8359
- return computeCommitDiffStats(options.commit);
8360
- }
8361
- if (options.fixBase) {
8362
- return computeFixBaseDiffStats(options.fixBase);
8363
- }
8364
- if (options.uncommitted) {
8365
- return computeUncommittedDiffStats();
8366
- }
8367
- const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
8368
- if (isCI) {
8369
- return computeCIDiffStats(baseBranch);
8370
- }
8371
- return computeLocalDiffStats(baseBranch);
8372
- }
8373
- async function computeCommitDiffStats(commit) {
8374
- try {
8375
- const numstat = await gitExec([
8376
- "diff",
8377
- "--numstat",
8378
- `${commit}^..${commit}`
8379
- ]);
8380
- const lineStats = parseNumstat(numstat);
8381
- const nameStatus = await gitExec([
8382
- "diff",
8383
- "--name-status",
8384
- `${commit}^..${commit}`
8385
- ]);
8386
- const fileStats = parseNameStatus(nameStatus);
8387
- return {
8388
- baseRef: `${commit}^`,
8389
- ...fileStats,
8390
- ...lineStats
8391
- };
8392
- } catch {
8393
- try {
8394
- const numstat = await gitExec(["diff", "--numstat", "--root", commit]);
8395
- const lineStats = parseNumstat(numstat);
8396
- const nameStatus = await gitExec([
8397
- "diff",
8398
- "--name-status",
8399
- "--root",
8400
- commit
8401
- ]);
8402
- const fileStats = parseNameStatus(nameStatus);
8403
- return {
8404
- baseRef: "root",
8405
- ...fileStats,
8406
- ...lineStats
8407
- };
8408
- } catch {
8409
- return emptyDiffStats(commit);
8410
- }
8411
- }
8412
- }
8413
- async function computeUncommittedDiffStats() {
8414
- const stagedNumstat = await gitExec(["diff", "--numstat", "--cached"]);
8415
- const stagedLines = parseNumstat(stagedNumstat);
8416
- const stagedStatus = await gitExec(["diff", "--name-status", "--cached"]);
8417
- const stagedFiles = parseNameStatus(stagedStatus);
8418
- const unstagedNumstat = await gitExec(["diff", "--numstat"]);
8419
- const unstagedLines = parseNumstat(unstagedNumstat);
8420
- const unstagedStatus = await gitExec(["diff", "--name-status"]);
8421
- const unstagedFiles = parseNameStatus(unstagedStatus);
8422
- const untrackedList = await gitExec([
8423
- "ls-files",
8424
- "--others",
8425
- "--exclude-standard"
8426
- ]);
8427
- const untrackedFiles = untrackedList.split(`
8428
- `).filter((f) => f.trim().length > 0);
8429
- return {
8430
- baseRef: "uncommitted",
8431
- total: stagedFiles.total + unstagedFiles.total + untrackedFiles.length - countOverlap(stagedStatus, unstagedStatus),
8432
- newFiles: stagedFiles.newFiles + unstagedFiles.newFiles + untrackedFiles.length,
8433
- modifiedFiles: stagedFiles.modifiedFiles + unstagedFiles.modifiedFiles,
8434
- deletedFiles: stagedFiles.deletedFiles + unstagedFiles.deletedFiles,
8435
- linesAdded: stagedLines.linesAdded + unstagedLines.linesAdded,
8436
- linesRemoved: stagedLines.linesRemoved + unstagedLines.linesRemoved
8437
- };
8438
- }
8439
- async function computeFixBaseDiffStats(fixBase) {
8440
- try {
8441
- const numstat = await gitExec(["diff", "--numstat", fixBase]);
8442
- const lineStats = parseNumstat(numstat);
8443
- const nameStatus = await gitExec(["diff", "--name-status", fixBase]);
8444
- const fileStats = parseNameStatus(nameStatus);
8445
- const currentUntracked = (await gitExec(["ls-files", "--others", "--exclude-standard"])).split(`
8446
- `).filter((f) => f.trim().length > 0);
8447
- let fixBaseFiles;
8448
- try {
8449
- const treeFiles = await gitExec([
8450
- "ls-tree",
8451
- "-r",
8452
- "--name-only",
8453
- fixBase
8454
- ]);
8455
- fixBaseFiles = new Set(treeFiles.split(`
8456
- `).filter((f) => f.trim().length > 0));
8457
- } catch {
8458
- fixBaseFiles = new Set;
8723
+ const allAdapters = [...getAllAdapters()].sort((a, b) => CLI_PREFERENCE_ORDER.indexOf(a.name) - CLI_PREFERENCE_ORDER.indexOf(b.name));
8724
+ const available = [];
8725
+ for (const adapter of allAdapters) {
8726
+ const isAvailable = await adapter.isAvailable();
8727
+ if (isAvailable) {
8728
+ console.log(chalk11.green(` ✓ ${adapter.name}`));
8729
+ available.push(adapter);
8730
+ } else {
8731
+ console.log(chalk11.dim(` ✗ ${adapter.name} (not installed)`));
8459
8732
  }
8460
- const newUntrackedFiles = currentUntracked.filter((f) => !fixBaseFiles.has(f));
8461
- return {
8462
- baseRef: fixBase,
8463
- total: fileStats.total + newUntrackedFiles.length,
8464
- newFiles: fileStats.newFiles + newUntrackedFiles.length,
8465
- modifiedFiles: fileStats.modifiedFiles,
8466
- deletedFiles: fileStats.deletedFiles,
8467
- linesAdded: lineStats.linesAdded,
8468
- linesRemoved: lineStats.linesRemoved
8469
- };
8470
- } catch {
8471
- return emptyDiffStats(fixBase);
8472
8733
  }
8734
+ return available;
8473
8735
  }
8474
- async function computeCIDiffStats(baseBranch) {
8475
- const headRef = process.env.GITHUB_SHA || "HEAD";
8476
- try {
8477
- const numstat = await gitExec([
8478
- "diff",
8479
- "--numstat",
8480
- `${baseBranch}...${headRef}`
8481
- ]);
8482
- const lineStats = parseNumstat(numstat);
8483
- const nameStatus = await gitExec([
8484
- "diff",
8485
- "--name-status",
8486
- `${baseBranch}...${headRef}`
8487
- ]);
8488
- const fileStats = parseNameStatus(nameStatus);
8489
- return {
8490
- baseRef: baseBranch,
8491
- ...fileStats,
8492
- ...lineStats
8493
- };
8494
- } catch {
8736
+ // src/commands/list.ts
8737
+ import chalk12 from "chalk";
8738
+ function registerListCommand(program) {
8739
+ program.command("list").description("List configured gates").action(async () => {
8495
8740
  try {
8496
- const numstat = await gitExec(["diff", "--numstat", "HEAD^...HEAD"]);
8497
- const lineStats = parseNumstat(numstat);
8498
- const nameStatus = await gitExec([
8499
- "diff",
8500
- "--name-status",
8501
- "HEAD^...HEAD"
8502
- ]);
8503
- const fileStats = parseNameStatus(nameStatus);
8504
- return {
8505
- baseRef: "HEAD^",
8506
- ...fileStats,
8507
- ...lineStats
8508
- };
8509
- } catch {
8510
- return emptyDiffStats(baseBranch);
8741
+ const config = await loadConfig();
8742
+ console.log(chalk12.bold("Check Gates:"));
8743
+ for (const c of Object.values(config.checks)) {
8744
+ console.log(` - ${c.name}`);
8745
+ }
8746
+ console.log(chalk12.bold(`
8747
+ Review Gates:`));
8748
+ for (const r of Object.values(config.reviews)) {
8749
+ console.log(` - ${r.name} (Tools: ${r.cli_preference?.join(", ")})`);
8750
+ }
8751
+ console.log(chalk12.bold(`
8752
+ Entry Points:`));
8753
+ for (const ep of config.project.entry_points) {
8754
+ console.log(` - ${ep.path}`);
8755
+ if (ep.checks)
8756
+ console.log(` Checks: ${ep.checks.join(", ")}`);
8757
+ if (ep.reviews)
8758
+ console.log(` Reviews: ${ep.reviews.join(", ")}`);
8759
+ }
8760
+ } catch (error) {
8761
+ const err = error;
8762
+ console.error(chalk12.red("Error:"), err.message);
8511
8763
  }
8512
- }
8764
+ });
8513
8765
  }
8514
- async function computeLocalDiffStats(baseBranch) {
8515
- const committedNumstat = await gitExec([
8516
- "diff",
8517
- "--numstat",
8518
- `${baseBranch}...HEAD`
8519
- ]);
8520
- const committedLines = parseNumstat(committedNumstat);
8521
- const committedStatus = await gitExec([
8522
- "diff",
8523
- "--name-status",
8524
- `${baseBranch}...HEAD`
8525
- ]);
8526
- const committedFiles = parseNameStatus(committedStatus);
8527
- const uncommittedNumstat = await gitExec(["diff", "--numstat", "HEAD"]);
8528
- const uncommittedLines = parseNumstat(uncommittedNumstat);
8529
- const uncommittedStatus = await gitExec(["diff", "--name-status", "HEAD"]);
8530
- const uncommittedFiles = parseNameStatus(uncommittedStatus);
8531
- const untrackedList = await gitExec([
8532
- "ls-files",
8533
- "--others",
8534
- "--exclude-standard"
8535
- ]);
8536
- const untrackedFiles = untrackedList.split(`
8537
- `).filter((f) => f.trim().length > 0);
8538
- const totalNew = committedFiles.newFiles + uncommittedFiles.newFiles + untrackedFiles.length;
8539
- const totalModified = committedFiles.modifiedFiles + uncommittedFiles.modifiedFiles;
8540
- const totalDeleted = committedFiles.deletedFiles + uncommittedFiles.deletedFiles;
8766
+ // src/commands/review.ts
8767
+ function registerReviewCommand(program) {
8768
+ program.command("review").description("Run only applicable reviews for detected changes").option("-b, --base-branch <branch>", "Override base branch for change detection").option("-g, --gate <name>", "Run specific review gate only").option("-c, --commit <sha>", "Use diff for a specific commit").option("-u, --uncommitted", "Use diff for current uncommitted changes (staged and unstaged)").option("-e, --enable-review <name>", "Activate a disabled review for this run (repeatable)", (value, prev = []) => [...prev, value], []).action((options) => executeGateCommand("review", {
8769
+ ...options,
8770
+ enableReviews: new Set(options.enableReview ?? [])
8771
+ }));
8772
+ }
8773
+ // src/scripts/review-audit.ts
8774
+ import fs33 from "node:fs";
8775
+ import path31 from "node:path";
8776
+ import readline from "node:readline";
8777
+ function parseKeyValue2(text) {
8778
+ const result = {};
8779
+ for (const [, key, value] of text.matchAll(/(\w+)=(\S+)/g))
8780
+ if (key && value)
8781
+ result[key] = value;
8782
+ return result;
8783
+ }
8784
+ var parseTimestamp2 = (line) => line.match(/^\[([^\]]+)\]/)?.[1] ?? "";
8785
+ var parseEventType2 = (line) => line.match(/^\[[^\]]+\]\s+(\S+)/)?.[1] ?? "";
8786
+ var parseEventBody2 = (line) => line.match(/^\[[^\]]+\]\s+\S+\s*(.*)/)?.[1] ?? "";
8787
+ var safeNum = (v) => {
8788
+ const n = Number(v ?? 0);
8789
+ return Number.isNaN(n) ? 0 : n;
8790
+ };
8791
+ var parseDuration = (d) => {
8792
+ const m = d.match(/^([\d.]+)(ms|s|m)?$/);
8793
+ const val = safeNum(m?.[1]);
8794
+ if (m?.[2] === "ms")
8795
+ return val / 1000;
8796
+ if (m?.[2] === "m")
8797
+ return val * 60;
8798
+ return val;
8799
+ };
8800
+ function getLogDir3(cwd) {
8801
+ const configPath = path31.join(cwd, ".gauntlet", "config.yml");
8802
+ try {
8803
+ const content = fs33.readFileSync(configPath, "utf-8");
8804
+ const match = content.match(/^log_dir:\s*(.+)$/m);
8805
+ if (match?.[1])
8806
+ return match[1].trim();
8807
+ } catch {}
8808
+ return "gauntlet_logs";
8809
+ }
8810
+ function handleRunStart(ts, body) {
8811
+ const kv = parseKeyValue2(body);
8541
8812
  return {
8542
- baseRef: baseBranch,
8543
- total: totalNew + totalModified + totalDeleted,
8544
- newFiles: totalNew,
8545
- modifiedFiles: totalModified,
8546
- deletedFiles: totalDeleted,
8547
- linesAdded: committedLines.linesAdded + uncommittedLines.linesAdded,
8548
- linesRemoved: committedLines.linesRemoved + uncommittedLines.linesRemoved
8813
+ timestamp: ts,
8814
+ mode: kv.mode ?? "unknown",
8815
+ linesAdded: safeNum(kv.lines_added),
8816
+ linesRemoved: safeNum(kv.lines_removed),
8817
+ reviewGates: [],
8818
+ priorPassSkips: 0,
8819
+ telemetry: []
8549
8820
  };
8550
8821
  }
8551
- function parseNumstat(output) {
8552
- let linesAdded = 0;
8553
- let linesRemoved = 0;
8554
- for (const line of output.split(`
8555
- `)) {
8556
- if (!line.trim())
8557
- continue;
8558
- const parts = line.split("\t");
8559
- if (parts.length < 3)
8560
- continue;
8561
- const added = parts[0];
8562
- const removed = parts[1];
8563
- if (added && added !== "-") {
8564
- linesAdded += parseInt(added, 10) || 0;
8565
- }
8566
- if (removed && removed !== "-") {
8567
- linesRemoved += parseInt(removed, 10) || 0;
8568
- }
8822
+ function handleGateResult(current, body) {
8823
+ const gateIdMatch = body.match(/^(\S+)/);
8824
+ const gateId = gateIdMatch?.[1] ?? "";
8825
+ if (!gateId.startsWith("review:"))
8826
+ return;
8827
+ const kv = parseKeyValue2(body);
8828
+ if (kv.cli) {
8829
+ current.reviewGates.push({
8830
+ reviewType: gateId.split(":").at(-1) ?? "other",
8831
+ cli: kv.cli,
8832
+ durationS: parseDuration(kv.duration ?? "0s"),
8833
+ violations: safeNum(kv.violations)
8834
+ });
8835
+ } else {
8836
+ current.priorPassSkips++;
8569
8837
  }
8570
- return { linesAdded, linesRemoved };
8571
8838
  }
8572
- function parseNameStatus(output) {
8573
- let newFiles = 0;
8574
- let modifiedFiles = 0;
8575
- let deletedFiles = 0;
8576
- for (const line of output.split(`
8577
- `)) {
8578
- if (!line.trim())
8579
- continue;
8580
- const status = line[0];
8581
- switch (status) {
8582
- case "A":
8583
- newFiles++;
8584
- break;
8585
- case "M":
8586
- case "R":
8587
- case "C":
8588
- case "T":
8589
- modifiedFiles++;
8590
- break;
8591
- case "D":
8592
- deletedFiles++;
8593
- break;
8594
- }
8595
- }
8596
- return {
8597
- total: newFiles + modifiedFiles + deletedFiles,
8598
- newFiles,
8599
- modifiedFiles,
8600
- deletedFiles
8839
+ function handleTelemetry(current, body) {
8840
+ const kv = parseKeyValue2(body);
8841
+ if (!kv.adapter)
8842
+ return;
8843
+ current.telemetry.push({
8844
+ adapter: kv.adapter,
8845
+ inTokens: safeNum(kv.in),
8846
+ cacheTokens: safeNum(kv.cache),
8847
+ outTokens: safeNum(kv.out),
8848
+ thoughtTokens: safeNum(kv.thought),
8849
+ toolTokens: safeNum(kv.tool),
8850
+ apiRequests: safeNum(kv.api_requests),
8851
+ cacheRead: safeNum(kv.cacheRead),
8852
+ cacheWrite: safeNum(kv.cacheWrite)
8853
+ });
8854
+ }
8855
+ function handleRunEnd(current, body) {
8856
+ const kv = parseKeyValue2(body);
8857
+ current.end = {
8858
+ status: kv.status ?? "unknown",
8859
+ fixed: safeNum(kv.fixed),
8860
+ skipped: safeNum(kv.skipped),
8861
+ failed: safeNum(kv.failed)
8601
8862
  };
8602
8863
  }
8603
- function countOverlap(status1, status2) {
8604
- const files1 = new Set;
8605
- for (const line of status1.split(`
8606
- `)) {
8607
- if (!line.trim())
8608
- continue;
8609
- const parts = line.split("\t");
8610
- const file = parts[1];
8611
- if (parts.length >= 2 && file) {
8612
- files1.add(file);
8613
- }
8864
+ var emptyStat = () => ({
8865
+ count: 0,
8866
+ totalDuration: 0,
8867
+ totalViolations: 0
8868
+ });
8869
+ function addGate(s, g) {
8870
+ s.count++;
8871
+ s.totalDuration += g.durationS;
8872
+ s.totalViolations += g.violations;
8873
+ }
8874
+ function getOrCreate(map, key, init) {
8875
+ if (!map.has(key))
8876
+ map.set(key, init());
8877
+ return map.get(key);
8878
+ }
8879
+ var REVIEW_TYPES = ["code-quality", "task-compliance", "artifact-review"];
8880
+ function accumulateBlock(block, a) {
8881
+ for (const g of block.reviewGates) {
8882
+ const inner = getOrCreate(a.cells, g.reviewType, () => new Map);
8883
+ addGate(getOrCreate(inner, g.cli, emptyStat), g);
8884
+ addGate(getOrCreate(a.cliTotals, g.cli, emptyStat), g);
8885
+ addGate(getOrCreate(a.typeTotals, g.reviewType, emptyStat), g);
8886
+ addGate(a.grandTotal, g);
8887
+ }
8888
+ const diff = block.linesAdded + block.linesRemoved;
8889
+ if (diff <= 0)
8890
+ return;
8891
+ for (const cli of new Set(block.reviewGates.map((g) => g.cli))) {
8892
+ const dur = block.reviewGates.filter((g) => g.cli === cli).reduce((s, g) => s + g.durationS, 0);
8893
+ const p = getOrCreate(a.per100, cli, () => ({ dur: 0, diff: 0 }));
8894
+ p.dur += dur;
8895
+ p.diff += diff;
8896
+ }
8897
+ }
8898
+ function buildCrossTab(blocks) {
8899
+ const a = {
8900
+ cells: new Map,
8901
+ cliTotals: new Map,
8902
+ typeTotals: new Map,
8903
+ per100: new Map,
8904
+ grandTotal: emptyStat()
8905
+ };
8906
+ for (const block of blocks)
8907
+ accumulateBlock(block, a);
8908
+ const allTypes = [
8909
+ ...REVIEW_TYPES.filter((t) => a.typeTotals.has(t)),
8910
+ ...[...a.typeTotals.keys()].filter((t) => !REVIEW_TYPES.includes(t))
8911
+ ];
8912
+ return { ...a, allTypes, allClis: [...a.cliTotals.keys()] };
8913
+ }
8914
+ var emptyTokenStats = (adapter) => ({
8915
+ adapter,
8916
+ inTokens: 0,
8917
+ cacheTokens: 0,
8918
+ outTokens: 0,
8919
+ thoughtTokens: 0,
8920
+ toolTokens: 0,
8921
+ apiRequests: 0,
8922
+ cacheRead: 0,
8923
+ cacheWrite: 0,
8924
+ runsWithTelemetry: 0
8925
+ });
8926
+ function accumulateTelemetryEntry(t, statsMap) {
8927
+ const s = statsMap.get(t.adapter) ?? emptyTokenStats(t.adapter);
8928
+ statsMap.set(t.adapter, s);
8929
+ s.inTokens += t.inTokens;
8930
+ s.cacheTokens += t.cacheTokens;
8931
+ s.outTokens += t.outTokens;
8932
+ s.thoughtTokens += t.thoughtTokens;
8933
+ s.toolTokens += t.toolTokens;
8934
+ s.apiRequests += t.apiRequests;
8935
+ s.cacheRead += t.cacheRead;
8936
+ s.cacheWrite += t.cacheWrite;
8937
+ }
8938
+ function aggregateTokenStats(blocks) {
8939
+ const statsMap = new Map;
8940
+ for (const block of blocks) {
8941
+ const adaptersInBlock = new Set(block.telemetry.map((t) => t.adapter));
8942
+ for (const t of block.telemetry)
8943
+ accumulateTelemetryEntry(t, statsMap);
8944
+ for (const adapter of adaptersInBlock) {
8945
+ const s = statsMap.get(adapter);
8946
+ if (s)
8947
+ s.runsWithTelemetry++;
8948
+ }
8949
+ }
8950
+ return Array.from(statsMap.values());
8951
+ }
8952
+ var formatNum = (n) => n.toLocaleString("en-US");
8953
+ var padRight = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
8954
+ var padLeft = (s, w) => " ".repeat(Math.max(0, w - s.length)) + s;
8955
+ var capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
8956
+ var fmtType = (t) => t.split("-").map(capitalize).join("-");
8957
+ function formatCrossTable(title, rowLabels, colLabels, cell, rowTotal, colTotal, grandTotal) {
8958
+ const rlW = Math.max(17, ...rowLabels.map((r) => fmtType(r).length)) + 2;
8959
+ const cW = 12;
8960
+ const hdr = padRight("", rlW) + colLabels.map((c) => padRight(capitalize(c), cW)).join("") + "Total";
8961
+ const rows = rowLabels.map((r) => padRight(fmtType(r), rlW) + colLabels.map((c) => padRight(cell(r, c), cW)).join("") + rowTotal(r));
8962
+ const totalRow = padRight("Total", rlW) + colLabels.map((c) => padRight(colTotal(c), cW)).join("") + grandTotal;
8963
+ return [title, hdr, ...rows, totalRow, ""];
8964
+ }
8965
+ function formatRunCounts(ct) {
8966
+ return formatCrossTable("=== Run Counts ===", ct.allTypes, ct.allClis, (t, c) => String(ct.cells.get(t)?.get(c)?.count ?? 0), (t) => String(ct.typeTotals.get(t)?.count ?? 0), (c) => String(ct.cliTotals.get(c)?.count ?? 0), String(ct.grandTotal.count));
8967
+ }
8968
+ function formatTiming(ct) {
8969
+ const avg = (s) => s && s.count > 0 ? `${(s.totalDuration / s.count).toFixed(1)}s` : "n/a";
8970
+ const lines = formatCrossTable("=== Timing ===", ct.allTypes, ct.allClis, (t, c) => avg(ct.cells.get(t)?.get(c)), (t) => avg(ct.typeTotals.get(t)), (c) => avg(ct.cliTotals.get(c)), avg(ct.grandTotal));
8971
+ const p100parts = ct.allClis.map((c) => {
8972
+ const p = ct.per100.get(c);
8973
+ return p && p.diff > 0 ? `${c}=${(p.dur / p.diff * 100).toFixed(1)}s` : "";
8974
+ }).filter(Boolean);
8975
+ if (p100parts.length > 0)
8976
+ lines.splice(lines.length - 1, 0, `Per 100 diff lines (excl. zero-diff): ${p100parts.join(" ")}`);
8977
+ return lines;
8978
+ }
8979
+ function formatViolations(ct) {
8980
+ const avg = (s) => s && s.count > 0 ? (s.totalViolations / s.count).toFixed(2) : "n/a";
8981
+ return formatCrossTable("=== Violations (avg per run) ===", ct.allTypes, ct.allClis, (t, c) => avg(ct.cells.get(t)?.get(c)), (t) => avg(ct.typeTotals.get(t)), (c) => avg(ct.cliTotals.get(c)), avg(ct.grandTotal));
8982
+ }
8983
+ function formatTokenEntry(t, totalRuns) {
8984
+ const out = [
8985
+ `${capitalize(t.adapter)} (${t.runsWithTelemetry} of ${totalRuns} runs had telemetry):`
8986
+ ];
8987
+ if (t.inTokens > 0 || t.cacheTokens > 0) {
8988
+ const total = t.inTokens + t.cacheTokens;
8989
+ out.push(` Input: ${padLeft(formatNum(total), 12)} (non-cached: ${formatNum(t.inTokens)} | cached: ${formatNum(t.cacheTokens)})`);
8990
+ }
8991
+ if (t.outTokens > 0)
8992
+ out.push(` Output: ${padLeft(formatNum(t.outTokens), 12)}`);
8993
+ if (t.thoughtTokens > 0)
8994
+ out.push(` Thinking: ${padLeft(formatNum(t.thoughtTokens), 12)}`);
8995
+ if (t.toolTokens > 0)
8996
+ out.push(` Tool tokens: ${padLeft(formatNum(t.toolTokens), 12)}`);
8997
+ if (t.cacheRead > 0 || t.cacheWrite > 0) {
8998
+ out.push(` Cache reads: ${padLeft(formatNum(t.cacheRead), 12)}`);
8999
+ out.push(` Cache writes: ${padLeft(formatNum(t.cacheWrite), 12)}`);
9000
+ }
9001
+ if (t.apiRequests > 0) {
9002
+ const avg = t.runsWithTelemetry > 0 ? (t.apiRequests / t.runsWithTelemetry).toFixed(1) : "?";
9003
+ out.push(` API requests: ${padLeft(formatNum(t.apiRequests), 12)} (avg ${avg}/run)`);
9004
+ }
9005
+ out.push("");
9006
+ return out;
9007
+ }
9008
+ function formatTokenUsage(ts, m) {
9009
+ if (ts.length === 0)
9010
+ return ["=== Token Usage ===", "No telemetry data found.", ""];
9011
+ return [
9012
+ "=== Token Usage ===",
9013
+ ...ts.flatMap((t) => formatTokenEntry(t, m.get(t.adapter) ?? t.runsWithTelemetry))
9014
+ ];
9015
+ }
9016
+ function formatFixSkip(blocks) {
9017
+ const withEnd = blocks.filter((b) => b.end);
9018
+ const total = withEnd.length;
9019
+ const passed = withEnd.filter((b) => b.end?.status === "pass").length;
9020
+ const fixed = withEnd.reduce((s, b) => s + (b.end?.fixed ?? 0), 0);
9021
+ const skipped = withEnd.reduce((s, b) => s + (b.end?.skipped ?? 0), 0);
9022
+ const failed = withEnd.reduce((s, b) => s + (b.end?.failed ?? 0), 0);
9023
+ const priorPass = blocks.reduce((s, b) => s + b.priorPassSkips, 0);
9024
+ const lines = [
9025
+ "=== Fix / Skip ===",
9026
+ `Gauntlet runs: ${total} total (${passed} passed, ${total - passed} failed)`,
9027
+ ` Violations fixed: ${fixed}`,
9028
+ ` Violations skipped: ${skipped}`,
9029
+ ` Gates failed: ${failed}`,
9030
+ ` Review gates skipped (prior pass): ${priorPass}`
9031
+ ];
9032
+ const totalFixedSkipped = fixed + skipped;
9033
+ if (totalFixedSkipped > 0) {
9034
+ const fp = (fixed / totalFixedSkipped * 100).toFixed(1);
9035
+ const sp = (skipped / totalFixedSkipped * 100).toFixed(1);
9036
+ lines.push(` (fixed: ${fp}% | skipped: ${sp}% of fixed+skipped)`);
8614
9037
  }
8615
- let overlap = 0;
8616
- for (const line of status2.split(`
8617
- `)) {
9038
+ return lines;
9039
+ }
9040
+ function formatAuditReport(blocks, date) {
9041
+ if (blocks.length === 0)
9042
+ return `Review Execution Audit — ${date}
9043
+
9044
+ No gauntlet runs found for this date.`;
9045
+ const ct = buildCrossTab(blocks);
9046
+ const tokenStats = aggregateTokenStats(blocks);
9047
+ const cliBlockCount = new Map;
9048
+ for (const block of blocks) {
9049
+ for (const cli of new Set(block.reviewGates.map((g) => g.cli)))
9050
+ cliBlockCount.set(cli, (cliBlockCount.get(cli) ?? 0) + 1);
9051
+ }
9052
+ return [
9053
+ `Review Execution Audit — ${date}`,
9054
+ "",
9055
+ ...formatRunCounts(ct),
9056
+ ...formatTiming(ct),
9057
+ ...formatViolations(ct),
9058
+ ...formatTokenUsage(tokenStats, cliBlockCount),
9059
+ ...formatFixSkip(blocks)
9060
+ ].join(`
9061
+ `);
9062
+ }
9063
+ function todayLocalDate() {
9064
+ const now = new Date;
9065
+ const y = now.getFullYear();
9066
+ const mo = String(now.getMonth() + 1).padStart(2, "0");
9067
+ const d = String(now.getDate()).padStart(2, "0");
9068
+ return `${y}-${mo}-${d}`;
9069
+ }
9070
+ async function readBlocks(filePath, date) {
9071
+ const rl = readline.createInterface({
9072
+ input: fs33.createReadStream(filePath)
9073
+ });
9074
+ const blocks = [];
9075
+ let current = null;
9076
+ for await (const line of rl) {
8618
9077
  if (!line.trim())
8619
9078
  continue;
8620
- const parts = line.split("\t");
8621
- const file = parts[1];
8622
- if (parts.length >= 2 && file && files1.has(file)) {
8623
- overlap++;
9079
+ const ts = parseTimestamp2(line);
9080
+ if (!ts.startsWith(date))
9081
+ continue;
9082
+ const event = parseEventType2(line);
9083
+ const body = parseEventBody2(line);
9084
+ if (event === "RUN_START") {
9085
+ current = handleRunStart(ts, body);
9086
+ blocks.push(current);
9087
+ continue;
8624
9088
  }
9089
+ if (!current)
9090
+ continue;
9091
+ if (event === "GATE_RESULT")
9092
+ handleGateResult(current, body);
9093
+ else if (event === "TELEMETRY")
9094
+ handleTelemetry(current, body);
9095
+ else if (event === "RUN_END")
9096
+ handleRunEnd(current, body);
8625
9097
  }
8626
- return overlap;
9098
+ return blocks;
8627
9099
  }
8628
- function emptyDiffStats(baseRef) {
8629
- return {
8630
- baseRef,
8631
- total: 0,
8632
- newFiles: 0,
8633
- modifiedFiles: 0,
8634
- deletedFiles: 0,
8635
- linesAdded: 0,
8636
- linesRemoved: 0
8637
- };
9100
+ async function main2(date) {
9101
+ const cwd = process.cwd();
9102
+ if (date && !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
9103
+ console.error("Invalid --date. Expected YYYY-MM-DD");
9104
+ process.exit(1);
9105
+ }
9106
+ const targetDate = date ?? todayLocalDate();
9107
+ const debugLogPath = path31.join(cwd, getLogDir3(cwd), ".debug.log");
9108
+ if (!fs33.existsSync(debugLogPath)) {
9109
+ console.log(`No debug log found. (looked in ${getLogDir3(cwd)}/.debug.log)`);
9110
+ process.exit(0);
9111
+ }
9112
+ const blocks = await readBlocks(debugLogPath, targetDate);
9113
+ console.log(formatAuditReport(blocks, targetDate));
8638
9114
  }
9115
+ var isDirectRun2 = (import.meta.url === `file://${process.argv[1]}` || typeof Bun !== "undefined" && import.meta.url === `file://${Bun.main}`) && (process.argv[1]?.endsWith("review-audit.ts") || process.argv[1]?.endsWith("review-audit.js"));
9116
+ if (isDirectRun2)
9117
+ main2();
8639
9118
 
9119
+ // src/commands/review-audit.ts
9120
+ function registerReviewAuditCommand(program) {
9121
+ program.command("review-audit").description("Audit review execution for a given date from the debug log").option("--date <YYYY-MM-DD>", "Date to filter (default: today)").action(async (opts) => {
9122
+ await main2(opts.date);
9123
+ });
9124
+ }
8640
9125
  // src/core/run-executor-lock.ts
8641
- import fs32 from "node:fs/promises";
8642
- import path30 from "node:path";
9126
+ import fs34 from "node:fs/promises";
9127
+ import path32 from "node:path";
8643
9128
  var LOCK_FILENAME2 = ".gauntlet-run.lock";
8644
9129
  var STALE_LOCK_MS = 10 * 60 * 1000;
8645
9130
  function isProcessAlive(pid) {
@@ -8655,9 +9140,9 @@ function isProcessAlive(pid) {
8655
9140
  }
8656
9141
  async function isLockStale(lockPath) {
8657
9142
  try {
8658
- const lockContent = await fs32.readFile(lockPath, "utf-8");
9143
+ const lockContent = await fs34.readFile(lockPath, "utf-8");
8659
9144
  const lockPid = Number.parseInt(lockContent.trim(), 10);
8660
- const lockStat = await fs32.stat(lockPath);
9145
+ const lockStat = await fs34.stat(lockPath);
8661
9146
  const lockAgeMs = Date.now() - lockStat.mtimeMs;
8662
9147
  const pidValid = !Number.isNaN(lockPid);
8663
9148
  if (pidValid && !isProcessAlive(lockPid)) {
@@ -8672,10 +9157,10 @@ async function isLockStale(lockPath) {
8672
9157
  }
8673
9158
  }
8674
9159
  async function tryAcquireLock(logDir) {
8675
- await fs32.mkdir(logDir, { recursive: true });
8676
- const lockPath = path30.resolve(logDir, LOCK_FILENAME2);
9160
+ await fs34.mkdir(logDir, { recursive: true });
9161
+ const lockPath = path32.resolve(logDir, LOCK_FILENAME2);
8677
9162
  try {
8678
- await fs32.writeFile(lockPath, String(process.pid), { flag: "wx" });
9163
+ await fs34.writeFile(lockPath, String(process.pid), { flag: "wx" });
8679
9164
  return true;
8680
9165
  } catch (err) {
8681
9166
  const isExist = typeof err === "object" && err !== null && "code" in err && err.code === "EEXIST";
@@ -8686,9 +9171,9 @@ async function tryAcquireLock(logDir) {
8686
9171
  if (!stale) {
8687
9172
  return false;
8688
9173
  }
8689
- await fs32.rm(lockPath, { force: true });
9174
+ await fs34.rm(lockPath, { force: true });
8690
9175
  try {
8691
- await fs32.writeFile(lockPath, String(process.pid), { flag: "wx" });
9176
+ await fs34.writeFile(lockPath, String(process.pid), { flag: "wx" });
8692
9177
  return true;
8693
9178
  } catch {
8694
9179
  return false;
@@ -8697,7 +9182,7 @@ async function tryAcquireLock(logDir) {
8697
9182
  }
8698
9183
  async function findLatestConsoleLog(logDir) {
8699
9184
  try {
8700
- const files = await fs32.readdir(logDir);
9185
+ const files = await fs34.readdir(logDir);
8701
9186
  let maxNum = -1;
8702
9187
  let latestFile = null;
8703
9188
  for (const file of files) {
@@ -8713,7 +9198,7 @@ async function findLatestConsoleLog(logDir) {
8713
9198
  }
8714
9199
  }
8715
9200
  }
8716
- return latestFile ? path30.join(logDir, latestFile) : null;
9201
+ return latestFile ? path32.join(logDir, latestFile) : null;
8717
9202
  } catch {
8718
9203
  return null;
8719
9204
  }
@@ -8894,7 +9379,7 @@ async function detectAndPrepareChanges(ctx, isRerun, failuresMap, changeOptions)
8894
9379
  };
8895
9380
  const changeDetector = new ChangeDetector(ctx.effectiveBaseBranch, effectiveChangeOptions);
8896
9381
  const expander = new EntryPointExpander;
8897
- const jobGen = new JobGenerator(ctx.config);
9382
+ const jobGen = new JobGenerator(ctx.config, ctx.options.enableReviews);
8898
9383
  log10.debug("Detecting changes...");
8899
9384
  const changes = await changeDetector.getChangedFiles();
8900
9385
  if (changes.length === 0 && isRerun) {
@@ -9071,12 +9556,13 @@ async function executeRun(options = {}) {
9071
9556
 
9072
9557
  // src/commands/run.ts
9073
9558
  function registerRunCommand(program) {
9074
- program.command("run").description("Run gates for detected changes").option("-b, --base-branch <branch>", "Override base branch for change detection").option("-g, --gate <name>", "Run specific gate only").option("-c, --commit <sha>", "Use diff for a specific commit").option("-u, --uncommitted", "Use diff for current uncommitted changes (staged and unstaged)").action(async (options) => {
9559
+ program.command("run").description("Run gates for detected changes").option("-b, --base-branch <branch>", "Override base branch for change detection").option("-g, --gate <name>", "Run specific gate only").option("-c, --commit <sha>", "Use diff for a specific commit").option("-u, --uncommitted", "Use diff for current uncommitted changes (staged and unstaged)").option("-e, --enable-review <name>", "Activate a disabled review for this run (repeatable)", (value, prev = []) => [...prev, value], []).action(async (options) => {
9075
9560
  const result = await executeRun({
9076
9561
  baseBranch: options.baseBranch,
9077
9562
  gate: options.gate,
9078
9563
  commit: options.commit,
9079
- uncommitted: options.uncommitted
9564
+ uncommitted: options.uncommitted,
9565
+ enableReviews: new Set(options.enableReview ?? [])
9080
9566
  });
9081
9567
  const code = isSuccessStatus(result.status) ? 0 : 1;
9082
9568
  process.exit(code);
@@ -9114,8 +9600,8 @@ function registerSkipCommand(program) {
9114
9600
  });
9115
9601
  }
9116
9602
  // src/commands/start-hook.ts
9117
- import fs33 from "node:fs/promises";
9118
- import path31 from "node:path";
9603
+ import fs35 from "node:fs/promises";
9604
+ import path33 from "node:path";
9119
9605
  import YAML8 from "yaml";
9120
9606
  var START_HOOK_MESSAGE = `<IMPORTANT>
9121
9607
  This project uses Agent Gauntlet for automated quality verification.
@@ -9163,9 +9649,9 @@ function isValidConfig(content) {
9163
9649
  }
9164
9650
  function registerStartHookCommand(program) {
9165
9651
  program.command("start-hook").description("Session start hook - primes agent with gauntlet verification instructions").option("--adapter <adapter>", "Output format: claude or cursor", "claude").action(async (options) => {
9166
- const configPath = path31.join(process.cwd(), ".gauntlet", "config.yml");
9652
+ const configPath = path33.join(process.cwd(), ".gauntlet", "config.yml");
9167
9653
  try {
9168
- const content = await fs33.readFile(configPath, "utf-8");
9654
+ const content = await fs35.readFile(configPath, "utf-8");
9169
9655
  if (!isValidConfig(content)) {
9170
9656
  return;
9171
9657
  }
@@ -9175,7 +9661,7 @@ function registerStartHookCommand(program) {
9175
9661
  const adapter = options.adapter;
9176
9662
  try {
9177
9663
  const cwd = process.cwd();
9178
- const logDir = path31.join(cwd, await getLogDir2(cwd));
9664
+ const logDir = path33.join(cwd, await getLogDir2(cwd));
9179
9665
  const globalConfig = await loadGlobalConfig();
9180
9666
  const projectDebugLogConfig = await getDebugLogConfig(cwd);
9181
9667
  const debugLogConfig = mergeDebugLogConfig(projectDebugLogConfig, globalConfig.debug_log);
@@ -9214,6 +9700,7 @@ registerRunCommand(program);
9214
9700
  registerCheckCommand(program);
9215
9701
  registerCICommand(program);
9216
9702
  registerCleanCommand(program);
9703
+ registerReviewAuditCommand(program);
9217
9704
  registerReviewCommand(program);
9218
9705
  registerDetectCommand(program);
9219
9706
  registerListCommand(program);
@@ -9230,4 +9717,4 @@ if (process.argv.length < 3) {
9230
9717
  }
9231
9718
  program.parse(process.argv);
9232
9719
 
9233
- //# debugId=E93D541AB16DB2CC64756E2164756E21
9720
+ //# debugId=CAFA132ED75F0B9564756E2164756E21