agent-gauntlet 0.12.0 → 0.13.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
@@ -7,7 +7,7 @@ import { Command } from "commander";
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "agent-gauntlet",
10
- version: "0.12.0",
10
+ version: "0.13.0",
11
11
  description: "A CLI tool for testing AI coding agents",
12
12
  license: "Apache-2.0",
13
13
  author: "Paul Caplan",
@@ -96,12 +96,12 @@ var debugLogConfigSchema = z.object({
96
96
  });
97
97
  var globalConfigSchema = z.object({
98
98
  stop_hook: z.object({
99
- enabled: z.boolean().default(true),
99
+ enabled: z.boolean().default(false),
100
100
  run_interval_minutes: z.number().default(5),
101
101
  auto_push_pr: z.boolean().default(false),
102
102
  auto_fix_pr: z.boolean().default(false)
103
103
  }).default({
104
- enabled: true,
104
+ enabled: false,
105
105
  run_interval_minutes: 5,
106
106
  auto_push_pr: false,
107
107
  auto_fix_pr: false
@@ -110,7 +110,7 @@ var globalConfigSchema = z.object({
110
110
  });
111
111
  var DEFAULT_GLOBAL_CONFIG = {
112
112
  stop_hook: {
113
- enabled: true,
113
+ enabled: false,
114
114
  run_interval_minutes: 5,
115
115
  auto_push_pr: false,
116
116
  auto_fix_pr: false
@@ -905,7 +905,7 @@ ${config.fixInstructionsContent}
905
905
  // src/gates/review.ts
906
906
  import { exec as exec8 } from "node:child_process";
907
907
  import fs16 from "node:fs/promises";
908
- import path14 from "node:path";
908
+ import path15 from "node:path";
909
909
  import { promisify as promisify9 } from "node:util";
910
910
 
911
911
  // src/cli-adapters/index.ts
@@ -916,13 +916,13 @@ import fs15 from "node:fs/promises";
916
916
  import { exec as exec3 } from "node:child_process";
917
917
  import fs10 from "node:fs/promises";
918
918
  import os2 from "node:os";
919
- import path9 from "node:path";
919
+ import path10 from "node:path";
920
920
  import { promisify as promisify4 } from "node:util";
921
921
 
922
922
  // src/commands/stop-hook.ts
923
923
  import fsSync from "node:fs";
924
924
  import fs9 from "node:fs/promises";
925
- import path8 from "node:path";
925
+ import path9 from "node:path";
926
926
 
927
927
  // src/hooks/adapters/claude-stop-hook.ts
928
928
  class ClaudeStopHookAdapter {
@@ -1014,7 +1014,7 @@ class CursorStopHookAdapter {
1014
1014
  // src/hooks/stop-hook-handler.ts
1015
1015
  import { execFile } from "node:child_process";
1016
1016
  import fs8 from "node:fs/promises";
1017
- import path7 from "node:path";
1017
+ import path8 from "node:path";
1018
1018
  import { promisify as promisify3 } from "node:util";
1019
1019
  import YAML3 from "yaml";
1020
1020
 
@@ -1224,6 +1224,7 @@ function isLoggerConfigured() {
1224
1224
 
1225
1225
  // src/hooks/stop-hook-state.ts
1226
1226
  import fs7 from "node:fs/promises";
1227
+ import path7 from "node:path";
1227
1228
 
1228
1229
  // src/utils/execution-state.ts
1229
1230
  import { spawn } from "node:child_process";
@@ -1672,6 +1673,31 @@ async function deleteExecutionState(logDir) {
1672
1673
  }
1673
1674
 
1674
1675
  // src/hooks/stop-hook-state.ts
1676
+ var LOOP_WINDOW_MS = 60000;
1677
+ var LOOP_THRESHOLD = 3;
1678
+ var BLOCK_TIMESTAMPS_FILE = ".block-timestamps";
1679
+ var BLOCK_TIMESTAMPS_LOCK = ".block-timestamps.lock";
1680
+ var LOCK_TIMEOUT_MS = 2000;
1681
+ var LOCK_RETRY_MS = 50;
1682
+ async function acquireTimestampLock(logDir) {
1683
+ const lockPath = path7.join(logDir, BLOCK_TIMESTAMPS_LOCK);
1684
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
1685
+ while (Date.now() < deadline) {
1686
+ try {
1687
+ const handle = await fs7.open(lockPath, "wx");
1688
+ await handle.close();
1689
+ return async () => {
1690
+ await fs7.rm(lockPath, { force: true }).catch(() => {});
1691
+ };
1692
+ } catch (err) {
1693
+ const code = err.code;
1694
+ if (code !== "EEXIST")
1695
+ throw err;
1696
+ await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
1697
+ }
1698
+ }
1699
+ throw new Error("Could not acquire block-timestamps lock within timeout");
1700
+ }
1675
1701
  async function hasFailedRunLogs(logDir) {
1676
1702
  try {
1677
1703
  const entries = await fs7.readdir(logDir);
@@ -1721,6 +1747,38 @@ async function getLastRunStatus(logDir) {
1721
1747
  return null;
1722
1748
  return null;
1723
1749
  }
1750
+ async function readBlockTimestamps(logDir) {
1751
+ try {
1752
+ const filePath = path7.join(logDir, BLOCK_TIMESTAMPS_FILE);
1753
+ const content = await fs7.readFile(filePath, "utf-8");
1754
+ const parsed = JSON.parse(content);
1755
+ if (!Array.isArray(parsed))
1756
+ return [];
1757
+ return parsed.filter((ts) => typeof ts === "number");
1758
+ } catch {
1759
+ return [];
1760
+ }
1761
+ }
1762
+ async function recordBlockTimestamp(logDir) {
1763
+ const release = await acquireTimestampLock(logDir);
1764
+ try {
1765
+ const now = Date.now();
1766
+ const existing = await readBlockTimestamps(logDir);
1767
+ const recent = existing.filter((ts) => now - ts < LOOP_WINDOW_MS);
1768
+ recent.push(now);
1769
+ const filePath = path7.join(logDir, BLOCK_TIMESTAMPS_FILE);
1770
+ await fs7.writeFile(filePath, JSON.stringify(recent), "utf-8");
1771
+ return recent;
1772
+ } finally {
1773
+ await release();
1774
+ }
1775
+ }
1776
+ async function resetBlockTimestamps(logDir) {
1777
+ try {
1778
+ const filePath = path7.join(logDir, BLOCK_TIMESTAMPS_FILE);
1779
+ await fs7.rm(filePath, { force: true });
1780
+ } catch {}
1781
+ }
1724
1782
 
1725
1783
  // src/hooks/stop-hook-handler.ts
1726
1784
  var execFileAsync = promisify3(execFile);
@@ -1734,7 +1792,7 @@ var SKILL_INSTRUCTIONS = {
1734
1792
  };
1735
1793
  async function readProjectConfig(projectCwd) {
1736
1794
  try {
1737
- const configPath = path7.join(projectCwd, ".gauntlet", "config.yml");
1795
+ const configPath = path8.join(projectCwd, ".gauntlet", "config.yml");
1738
1796
  const content = await fs8.readFile(configPath, "utf-8");
1739
1797
  return YAML3.parse(content);
1740
1798
  } catch {
@@ -1759,6 +1817,7 @@ var STATUS_MESSAGES = {
1759
1817
  validation_required: "✗ Validation required — changes detected that need validation before stopping.",
1760
1818
  no_config: "○ Not a gauntlet project — no .gauntlet/config.yml found.",
1761
1819
  stop_hook_active: "↺ Stop hook cycle detected — allowing stop to prevent infinite loop.",
1820
+ loop_detected: "↺ Loop detected — stop hook blocked 3 times within 60s. Allowing stop to prevent infinite loop.",
1762
1821
  stop_hook_disabled: "○ Stop hook is disabled via configuration.",
1763
1822
  invalid_input: "⚠ Invalid hook input — could not parse JSON, allowing stop."
1764
1823
  };
@@ -1781,7 +1840,7 @@ async function getDebugLogConfig(projectCwd) {
1781
1840
  }
1782
1841
  async function getResolvedStopHookConfig(projectCwd) {
1783
1842
  try {
1784
- const configPath = path7.join(projectCwd, ".gauntlet", "config.yml");
1843
+ const configPath = path8.join(projectCwd, ".gauntlet", "config.yml");
1785
1844
  const content = await fs8.readFile(configPath, "utf-8");
1786
1845
  const raw = YAML3.parse(content);
1787
1846
  const projectStopHookConfig = raw?.stop_hook;
@@ -2059,6 +2118,28 @@ function createEarlyExitResult(status, options) {
2059
2118
  intervalMinutes: options?.intervalMinutes
2060
2119
  };
2061
2120
  }
2121
+ async function applyLoopDetection(result, logDir, log, debugLogger) {
2122
+ if (!result.shouldBlock) {
2123
+ resetBlockTimestamps(logDir).catch(() => {});
2124
+ return result;
2125
+ }
2126
+ try {
2127
+ const timestamps = await recordBlockTimestamp(logDir);
2128
+ if (timestamps.length >= LOOP_THRESHOLD) {
2129
+ log.info(`Loop detected: ${timestamps.length} blocks within window — overriding to allow`);
2130
+ await debugLogger?.logStopHook("allow", "loop_detected");
2131
+ return {
2132
+ status: "loop_detected",
2133
+ shouldBlock: false,
2134
+ message: getStatusMessage("loop_detected")
2135
+ };
2136
+ }
2137
+ } catch (loopErr) {
2138
+ const errMsg = loopErr.message ?? "unknown";
2139
+ log.warn(`Loop detection error: ${errMsg} — proceeding with original result`);
2140
+ }
2141
+ return result;
2142
+ }
2062
2143
  function registerStopHookCommand(program) {
2063
2144
  program.command("stop-hook").description("Claude Code stop hook - validates gauntlet completion").action(async () => {
2064
2145
  let adapter = adapters[1];
@@ -2094,12 +2175,12 @@ function registerStopHookCommand(program) {
2094
2175
  outputResult(adapter, createEarlyExitResult("stop_hook_active"));
2095
2176
  return;
2096
2177
  }
2097
- const quickConfigCheck = path8.join(process.cwd(), ".gauntlet", "config.yml");
2178
+ const quickConfigCheck = path9.join(process.cwd(), ".gauntlet", "config.yml");
2098
2179
  if (!await fileExists2(quickConfigCheck)) {
2099
2180
  outputResult(adapter, createEarlyExitResult("no_config"));
2100
2181
  return;
2101
2182
  }
2102
- const earlyLogDir = path8.join(process.cwd(), await getLogDir(process.cwd()));
2183
+ const earlyLogDir = path9.join(process.cwd(), await getLogDir(process.cwd()));
2103
2184
  try {
2104
2185
  const globalConfig = await loadGlobalConfig();
2105
2186
  const projectDebugLogConfig = await getDebugLogConfig(process.cwd());
@@ -2110,7 +2191,7 @@ function registerStopHookCommand(program) {
2110
2191
  }
2111
2192
  await debugLogger?.logCommand("stop-hook", []);
2112
2193
  const markerLogDir = await getLogDir(process.cwd());
2113
- const markerPath = path8.join(process.cwd(), markerLogDir, STOP_HOOK_MARKER_FILE);
2194
+ const markerPath = path9.join(process.cwd(), markerLogDir, STOP_HOOK_MARKER_FILE);
2114
2195
  if (await fileExists2(markerPath)) {
2115
2196
  const STALE_MARKER_MS = 10 * 60 * 1000;
2116
2197
  try {
@@ -2160,7 +2241,7 @@ function registerStopHookCommand(program) {
2160
2241
  log.info("Starting gauntlet validation...");
2161
2242
  const projectCwd = ctx.cwd;
2162
2243
  if (ctx.cwd !== process.cwd()) {
2163
- const configPath = path8.join(projectCwd, ".gauntlet", "config.yml");
2244
+ const configPath = path9.join(projectCwd, ".gauntlet", "config.yml");
2164
2245
  if (!await fileExists2(configPath)) {
2165
2246
  log.info("No gauntlet config found at hook cwd, allowing stop");
2166
2247
  await debugLogger?.logStopHookEarlyExit("no_config_at_cwd", "no_config", `cwd=${projectCwd}`);
@@ -2168,7 +2249,7 @@ function registerStopHookCommand(program) {
2168
2249
  return;
2169
2250
  }
2170
2251
  }
2171
- const logDir = path8.join(projectCwd, await getLogDir(projectCwd));
2252
+ const logDir = path9.join(projectCwd, await getLogDir(projectCwd));
2172
2253
  await initLogger({
2173
2254
  mode: "stop-hook",
2174
2255
  logDir
@@ -2185,7 +2266,7 @@ function registerStopHookCommand(program) {
2185
2266
  }
2186
2267
  }
2187
2268
  await debugLogger?.logStopHookDiagnostics(diagnostics);
2188
- markerFilePath = path8.join(logDir, STOP_HOOK_MARKER_FILE);
2269
+ markerFilePath = path9.join(logDir, STOP_HOOK_MARKER_FILE);
2189
2270
  try {
2190
2271
  await fs9.writeFile(markerFilePath, `${process.pid}`, "utf-8");
2191
2272
  } catch (mkErr) {
@@ -2210,6 +2291,7 @@ function registerStopHookCommand(program) {
2210
2291
  markerFilePath = null;
2211
2292
  }
2212
2293
  }
2294
+ result = await applyLoopDetection(result, logDir, log, debugLogger);
2213
2295
  outputResult(adapter, result);
2214
2296
  if (loggerInitialized) {
2215
2297
  try {
@@ -2429,13 +2511,13 @@ class ClaudeAdapter {
2429
2511
  return ".claude/commands";
2430
2512
  }
2431
2513
  getUserCommandDir() {
2432
- return path9.join(os2.homedir(), ".claude", "commands");
2514
+ return path10.join(os2.homedir(), ".claude", "commands");
2433
2515
  }
2434
2516
  getProjectSkillDir() {
2435
2517
  return ".claude/skills";
2436
2518
  }
2437
2519
  getUserSkillDir() {
2438
- return path9.join(os2.homedir(), ".claude", "skills");
2520
+ return path10.join(os2.homedir(), ".claude", "skills");
2439
2521
  }
2440
2522
  getCommandExtension() {
2441
2523
  return ".md";
@@ -2452,7 +2534,7 @@ class ClaudeAdapter {
2452
2534
  --- DIFF ---
2453
2535
  ${opts.diff}`;
2454
2536
  const tmpDir = os2.tmpdir();
2455
- const tmpFile = path9.join(tmpDir, `gauntlet-claude-${process.pid}-${Date.now()}.txt`);
2537
+ const tmpFile = path10.join(tmpDir, `gauntlet-claude-${process.pid}-${Date.now()}.txt`);
2456
2538
  await fs10.writeFile(tmpFile, fullContent);
2457
2539
  const args = ["-p"];
2458
2540
  if (opts.allowToolUse === false) {
@@ -2508,7 +2590,7 @@ ${opts.diff}`;
2508
2590
  import { exec as exec4 } from "node:child_process";
2509
2591
  import fs11 from "node:fs/promises";
2510
2592
  import os3 from "node:os";
2511
- import path10 from "node:path";
2593
+ import path11 from "node:path";
2512
2594
  import { promisify as promisify5 } from "node:util";
2513
2595
  var execAsync4 = promisify5(exec4);
2514
2596
  var MAX_BUFFER_BYTES3 = 10 * 1024 * 1024;
@@ -2628,7 +2710,7 @@ class CodexAdapter {
2628
2710
  return null;
2629
2711
  }
2630
2712
  getUserCommandDir() {
2631
- return path10.join(os3.homedir(), ".codex", "prompts");
2713
+ return path11.join(os3.homedir(), ".codex", "prompts");
2632
2714
  }
2633
2715
  getProjectSkillDir() {
2634
2716
  return null;
@@ -2671,7 +2753,7 @@ class CodexAdapter {
2671
2753
  --- DIFF ---
2672
2754
  ${opts.diff}`;
2673
2755
  const tmpDir = os3.tmpdir();
2674
- const tmpFile = path10.join(tmpDir, `gauntlet-codex-${Date.now()}.txt`);
2756
+ const tmpFile = path11.join(tmpDir, `gauntlet-codex-${Date.now()}.txt`);
2675
2757
  await fs11.writeFile(tmpFile, fullContent);
2676
2758
  const args = this.buildArgs(opts.allowToolUse, opts.thinkingBudget);
2677
2759
  const cleanup = () => fs11.unlink(tmpFile).catch(() => {});
@@ -2708,7 +2790,7 @@ ${opts.diff}`;
2708
2790
  import { exec as exec5 } from "node:child_process";
2709
2791
  import fs12 from "node:fs/promises";
2710
2792
  import os4 from "node:os";
2711
- import path11 from "node:path";
2793
+ import path12 from "node:path";
2712
2794
  import { promisify as promisify6 } from "node:util";
2713
2795
  var execAsync5 = promisify6(exec5);
2714
2796
  var MAX_BUFFER_BYTES4 = 10 * 1024 * 1024;
@@ -2761,7 +2843,7 @@ class CursorAdapter {
2761
2843
  --- DIFF ---
2762
2844
  ${opts.diff}`;
2763
2845
  const tmpDir = os4.tmpdir();
2764
- const tmpFile = path11.join(tmpDir, `gauntlet-cursor-${process.pid}-${Date.now()}.txt`);
2846
+ const tmpFile = path12.join(tmpDir, `gauntlet-cursor-${process.pid}-${Date.now()}.txt`);
2765
2847
  await fs12.writeFile(tmpFile, fullContent);
2766
2848
  const cleanup = () => fs12.unlink(tmpFile).catch(() => {});
2767
2849
  if (opts.onOutput) {
@@ -2791,7 +2873,7 @@ ${opts.diff}`;
2791
2873
  import { exec as exec6 } from "node:child_process";
2792
2874
  import fs13 from "node:fs/promises";
2793
2875
  import os5 from "node:os";
2794
- import path12 from "node:path";
2876
+ import path13 from "node:path";
2795
2877
  import { promisify as promisify7 } from "node:util";
2796
2878
  var execAsync6 = promisify7(exec6);
2797
2879
  var MAX_BUFFER_BYTES5 = 10 * 1024 * 1024;
@@ -2984,7 +3066,7 @@ class GeminiAdapter {
2984
3066
  return ".gemini/commands";
2985
3067
  }
2986
3068
  getUserCommandDir() {
2987
- return path12.join(os5.homedir(), ".gemini", "commands");
3069
+ return path13.join(os5.homedir(), ".gemini", "commands");
2988
3070
  }
2989
3071
  getProjectSkillDir() {
2990
3072
  return null;
@@ -3038,7 +3120,7 @@ ${summary}
3038
3120
  releaseLock = resolve;
3039
3121
  });
3040
3122
  await prev;
3041
- const settingsPath = path12.join(process.cwd(), ".gemini", "settings.json");
3123
+ const settingsPath = path13.join(process.cwd(), ".gemini", "settings.json");
3042
3124
  let backup = null;
3043
3125
  let existed = false;
3044
3126
  try {
@@ -3051,7 +3133,7 @@ ${summary}
3051
3133
  ...existing,
3052
3134
  thinkingConfig: { ...existing.thinkingConfig, thinkingBudget: budget }
3053
3135
  };
3054
- await fs13.mkdir(path12.dirname(settingsPath), { recursive: true });
3136
+ await fs13.mkdir(path13.dirname(settingsPath), { recursive: true });
3055
3137
  await fs13.writeFile(settingsPath, JSON.stringify(merged, null, 2));
3056
3138
  } catch (err) {
3057
3139
  releaseLock();
@@ -3101,9 +3183,9 @@ ${summary}
3101
3183
  --- DIFF ---
3102
3184
  ${opts.diff}`;
3103
3185
  const tmpDir = os5.tmpdir();
3104
- const tmpFile = path12.join(tmpDir, `gauntlet-gemini-${process.pid}-${Date.now()}.txt`);
3186
+ const tmpFile = path13.join(tmpDir, `gauntlet-gemini-${process.pid}-${Date.now()}.txt`);
3105
3187
  await fs13.writeFile(tmpFile, fullContent);
3106
- const telemetryFile = path12.join(process.cwd(), `.gauntlet-gemini-telemetry-${process.pid}-${Date.now()}.log`);
3188
+ const telemetryFile = path13.join(process.cwd(), `.gauntlet-gemini-telemetry-${process.pid}-${Date.now()}.log`);
3107
3189
  const telemetryEnv = this.buildTelemetryEnv(telemetryFile);
3108
3190
  const args = this.buildArgs(opts.allowToolUse);
3109
3191
  const cleanupThinking = await this.maybeApplyThinking(opts.thinkingBudget);
@@ -3150,7 +3232,7 @@ ${opts.diff}`;
3150
3232
  import { exec as exec7 } from "node:child_process";
3151
3233
  import fs14 from "node:fs/promises";
3152
3234
  import os6 from "node:os";
3153
- import path13 from "node:path";
3235
+ import path14 from "node:path";
3154
3236
  import { promisify as promisify8 } from "node:util";
3155
3237
  var execAsync7 = promisify8(exec7);
3156
3238
  var MAX_BUFFER_BYTES6 = 10 * 1024 * 1024;
@@ -3203,7 +3285,7 @@ class GitHubCopilotAdapter {
3203
3285
  --- DIFF ---
3204
3286
  ${opts.diff}`;
3205
3287
  const tmpDir = os6.tmpdir();
3206
- const tmpFile = path13.join(tmpDir, `gauntlet-copilot-${process.pid}-${Date.now()}.txt`);
3288
+ const tmpFile = path14.join(tmpDir, `gauntlet-copilot-${process.pid}-${Date.now()}.txt`);
3207
3289
  await fs14.writeFile(tmpFile, fullContent);
3208
3290
  const args = [
3209
3291
  "--allow-tool",
@@ -3754,7 +3836,7 @@ ${JSON_SYSTEM_INSTRUCTION}`;
3754
3836
  }
3755
3837
  const subResults = outputs.map((out) => {
3756
3838
  const specificLog = logPaths.find((p) => {
3757
- const filename = path14.basename(p);
3839
+ const filename = path15.basename(p);
3758
3840
  return filename.includes(`_${out.adapter}@${out.reviewIndex}.`) && filename.endsWith(".log");
3759
3841
  });
3760
3842
  let logPath = specificLog;
@@ -3776,7 +3858,7 @@ ${JSON_SYSTEM_INSTRUCTION}`;
3776
3858
  });
3777
3859
  for (const skipped of skippedSlotOutputs) {
3778
3860
  const specificLog = logPaths.find((p) => {
3779
- const filename = path14.basename(p);
3861
+ const filename = path15.basename(p);
3780
3862
  return filename.includes(`_${skipped.adapter}@${skipped.reviewIndex}.`) && filename.endsWith(".log");
3781
3863
  });
3782
3864
  subResults.push({
@@ -3852,6 +3934,9 @@ ${JSON_SYSTEM_INSTRUCTION}`;
3852
3934
  const totalEstTokens = promptEstTokens + diffEstTokens;
3853
3935
  const inputSizeMsg = `[input-stats] prompt_chars=${promptChars} diff_chars=${diffChars} total_chars=${totalInputChars} prompt_est_tokens=${promptEstTokens} diff_est_tokens=${diffEstTokens} total_est_tokens=${totalEstTokens}`;
3854
3936
  await adapterLogger(`${inputSizeMsg}
3937
+ `);
3938
+ await adapterLogger(`[diff]
3939
+ ${diff}
3855
3940
  `);
3856
3941
  const adapterCfg = adapterConfigs?.[toolName];
3857
3942
  const output = await adapter.execute({
@@ -4474,7 +4559,7 @@ import chalk2 from "chalk";
4474
4559
 
4475
4560
  // src/utils/log-parser.ts
4476
4561
  import fs17 from "node:fs/promises";
4477
- import path15 from "node:path";
4562
+ import path16 from "node:path";
4478
4563
  var log2 = getCategoryLogger("log-parser");
4479
4564
  function parseReviewFilename(filename) {
4480
4565
  const m = filename.match(/^(.+)_([^@]+)@(\d+)\.(\d+)\.(log|json)$/);
@@ -4495,7 +4580,7 @@ async function parseJsonReviewFile(jsonPath) {
4495
4580
  try {
4496
4581
  const content = await fs17.readFile(jsonPath, "utf-8");
4497
4582
  const data = JSON.parse(content);
4498
- const filename = path15.basename(jsonPath);
4583
+ const filename = path16.basename(jsonPath);
4499
4584
  const parsed = parseReviewFilename(filename);
4500
4585
  const jobId = parsed ? parsed.jobId : filename.replace(/\.\d+\.json$/, "");
4501
4586
  if (data.status === "pass" || data.status === "skipped_prior_pass") {
@@ -4542,7 +4627,7 @@ function extractPrefix(filename) {
4542
4627
  async function parseLogFile(logPath) {
4543
4628
  try {
4544
4629
  const content = await fs17.readFile(logPath, "utf-8");
4545
- const filename = path15.basename(logPath);
4630
+ const filename = path16.basename(logPath);
4546
4631
  const parsed = parseReviewFilename(filename);
4547
4632
  const jobId = parsed ? parsed.jobId : extractPrefix(filename);
4548
4633
  if (content.includes("--- Review Output")) {
@@ -4690,9 +4775,9 @@ async function reconstructHistory(logDir) {
4690
4775
  const logFile = runFiles.find((f) => f.startsWith(`${prefix}.${runNum}.`) && f.endsWith(".log"));
4691
4776
  let failure = null;
4692
4777
  if (jsonFile) {
4693
- failure = await parseJsonReviewFile(path15.join(logDir, jsonFile));
4778
+ failure = await parseJsonReviewFile(path16.join(logDir, jsonFile));
4694
4779
  } else if (logFile) {
4695
- failure = await parseLogFile(path15.join(logDir, logFile));
4780
+ failure = await parseLogFile(path16.join(logDir, logFile));
4696
4781
  }
4697
4782
  if (failure) {
4698
4783
  for (const af of failure.adapterFailures) {
@@ -4824,7 +4909,7 @@ async function findPreviousFailures(logDir, gateFilter, includePassedSlots) {
4824
4909
  const reviewIndex = parseInt(slotKey.substring(sepIdx + 1), 10);
4825
4910
  const parsed = parseReviewFilename(fileInfo.filename);
4826
4911
  const adapter = parsed?.adapter || "unknown";
4827
- const filePath = path15.join(logDir, fileInfo.filename);
4912
+ const filePath = path16.join(logDir, fileInfo.filename);
4828
4913
  let isPassing = false;
4829
4914
  if (fileInfo.ext === "json") {
4830
4915
  isPassing = await isJsonReviewPassing(filePath);
@@ -4882,7 +4967,7 @@ async function findPreviousFailures(logDir, gateFilter, includePassedSlots) {
4882
4967
  gateName: "",
4883
4968
  entryPoint: "",
4884
4969
  adapterFailures,
4885
- logPath: path15.join(logDir, `${jobId}.log`)
4970
+ logPath: path16.join(logDir, `${jobId}.log`)
4886
4971
  });
4887
4972
  }
4888
4973
  for (const [prefix, runMap] of checkPrefixMap.entries()) {
@@ -4892,9 +4977,9 @@ async function findPreviousFailures(logDir, gateFilter, includePassedSlots) {
4892
4977
  continue;
4893
4978
  let failure = null;
4894
4979
  if (exts.has("json")) {
4895
- failure = await parseJsonReviewFile(path15.join(logDir, `${prefix}.${latestRun}.json`));
4980
+ failure = await parseJsonReviewFile(path16.join(logDir, `${prefix}.${latestRun}.json`));
4896
4981
  } else if (exts.has("log")) {
4897
- failure = await parseLogFile(path15.join(logDir, `${prefix}.${latestRun}.log`));
4982
+ failure = await parseLogFile(path16.join(logDir, `${prefix}.${latestRun}.log`));
4898
4983
  }
4899
4984
  if (failure) {
4900
4985
  for (const af of failure.adapterFailures) {
@@ -5157,7 +5242,7 @@ ${chalk2.bold("━━━━━━━━━━━━━━━━━━━━━
5157
5242
  // src/output/console-log.ts
5158
5243
  import fs19 from "node:fs";
5159
5244
  import fsPromises2 from "node:fs/promises";
5160
- import path16 from "node:path";
5245
+ import path17 from "node:path";
5161
5246
  import { inspect } from "node:util";
5162
5247
  var ANSI_REGEX = /\x1b\[[0-9;]*m/g;
5163
5248
  function stripAnsi(text) {
@@ -5167,7 +5252,7 @@ function formatArgs(args) {
5167
5252
  return args.map((a) => typeof a === "string" ? a : inspect(a, { depth: 4 })).join(" ");
5168
5253
  }
5169
5254
  function openLogFileExclusive(logDir, runNum) {
5170
- const logPath = path16.join(logDir, `console.${runNum}.log`);
5255
+ const logPath = path17.join(logDir, `console.${runNum}.log`);
5171
5256
  try {
5172
5257
  const fd = fs19.openSync(logPath, fs19.constants.O_WRONLY | fs19.constants.O_CREAT | fs19.constants.O_EXCL);
5173
5258
  return { fd, logPath };
@@ -5183,7 +5268,7 @@ function openLogFileExclusive(logDir, runNum) {
5183
5268
  function openLogFileFallback(logDir, startNum) {
5184
5269
  let runNum = startNum;
5185
5270
  for (let attempts = 0;attempts < 100; attempts++) {
5186
- const logPath = path16.join(logDir, `console.${runNum}.log`);
5271
+ const logPath = path17.join(logDir, `console.${runNum}.log`);
5187
5272
  try {
5188
5273
  const fd = fs19.openSync(logPath, fs19.constants.O_WRONLY | fs19.constants.O_CREAT | fs19.constants.O_EXCL);
5189
5274
  return { fd, logPath };
@@ -5269,7 +5354,7 @@ async function startConsoleLog(logDir, runNumber) {
5269
5354
 
5270
5355
  // src/output/logger.ts
5271
5356
  import fs20 from "node:fs/promises";
5272
- import path17 from "node:path";
5357
+ import path18 from "node:path";
5273
5358
  function formatTimestamp() {
5274
5359
  return new Date().toISOString();
5275
5360
  }
@@ -5319,7 +5404,7 @@ class Logger {
5319
5404
  } else {
5320
5405
  filename = `${safeName}.${runNum}.log`;
5321
5406
  }
5322
- return path17.join(this.logDir, filename);
5407
+ return path18.join(this.logDir, filename);
5323
5408
  }
5324
5409
  async initFile(logPath) {
5325
5410
  if (this.initializedFiles.has(logPath)) {
@@ -5367,7 +5452,7 @@ class Logger {
5367
5452
 
5368
5453
  // src/commands/shared.ts
5369
5454
  import fs21 from "node:fs/promises";
5370
- import path18 from "node:path";
5455
+ import path19 from "node:path";
5371
5456
  var LOCK_FILENAME = ".gauntlet-run.lock";
5372
5457
  var SESSION_REF_FILENAME2 = ".session_ref";
5373
5458
  async function shouldAutoClean(logDir, baseBranch) {
@@ -5409,7 +5494,7 @@ async function exists(filePath) {
5409
5494
  }
5410
5495
  async function acquireLock(logDir) {
5411
5496
  await fs21.mkdir(logDir, { recursive: true });
5412
- const lockPath = path18.resolve(logDir, LOCK_FILENAME);
5497
+ const lockPath = path19.resolve(logDir, LOCK_FILENAME);
5413
5498
  try {
5414
5499
  await fs21.writeFile(lockPath, String(process.pid), { flag: "wx" });
5415
5500
  } catch (err) {
@@ -5422,7 +5507,7 @@ async function acquireLock(logDir) {
5422
5507
  }
5423
5508
  }
5424
5509
  async function releaseLock(logDir) {
5425
- const lockPath = path18.resolve(logDir, LOCK_FILENAME);
5510
+ const lockPath = path19.resolve(logDir, LOCK_FILENAME);
5426
5511
  try {
5427
5512
  await fs21.rm(lockPath, { force: true });
5428
5513
  } catch {}
@@ -5461,20 +5546,20 @@ function getCurrentLogFiles(files) {
5461
5546
  }
5462
5547
  async function deleteCurrentLogs(logDir) {
5463
5548
  const files = await fs21.readdir(logDir);
5464
- await Promise.all(getCurrentLogFiles(files).map((file) => fs21.rm(path18.join(logDir, file), { recursive: true, force: true })));
5549
+ await Promise.all(getCurrentLogFiles(files).map((file) => fs21.rm(path19.join(logDir, file), { recursive: true, force: true })));
5465
5550
  }
5466
5551
  async function rotatePreviousDirs(logDir, maxPreviousLogs) {
5467
5552
  const oldestSuffix = maxPreviousLogs - 1;
5468
5553
  const oldestDir = oldestSuffix === 0 ? "previous" : `previous.${oldestSuffix}`;
5469
- const oldestPath = path18.join(logDir, oldestDir);
5554
+ const oldestPath = path19.join(logDir, oldestDir);
5470
5555
  if (await exists(oldestPath)) {
5471
5556
  await fs21.rm(oldestPath, { recursive: true, force: true });
5472
5557
  }
5473
5558
  for (let i = oldestSuffix - 1;i >= 0; i--) {
5474
5559
  const fromName = i === 0 ? "previous" : `previous.${i}`;
5475
5560
  const toName = `previous.${i + 1}`;
5476
- const fromPath = path18.join(logDir, fromName);
5477
- const toPath = path18.join(logDir, toName);
5561
+ const fromPath = path19.join(logDir, fromName);
5562
+ const toPath = path19.join(logDir, toName);
5478
5563
  if (await exists(fromPath)) {
5479
5564
  await fs21.rename(fromPath, toPath);
5480
5565
  }
@@ -5491,12 +5576,12 @@ async function cleanLogs(logDir, maxPreviousLogs = 3) {
5491
5576
  return;
5492
5577
  }
5493
5578
  await rotatePreviousDirs(logDir, maxPreviousLogs);
5494
- const previousDir = path18.join(logDir, "previous");
5579
+ const previousDir = path19.join(logDir, "previous");
5495
5580
  await fs21.mkdir(previousDir, { recursive: true });
5496
5581
  const files = await fs21.readdir(logDir);
5497
- await Promise.all(getCurrentLogFiles(files).map((file) => fs21.rename(path18.join(logDir, file), path18.join(previousDir, file))));
5582
+ await Promise.all(getCurrentLogFiles(files).map((file) => fs21.rename(path19.join(logDir, file), path19.join(previousDir, file))));
5498
5583
  try {
5499
- await fs21.rm(path18.join(logDir, SESSION_REF_FILENAME2), { force: true });
5584
+ await fs21.rm(path19.join(logDir, SESSION_REF_FILENAME2), { force: true });
5500
5585
  } catch {}
5501
5586
  } catch (error) {
5502
5587
  console.warn("Failed to clean logs in", logDir, ":", error instanceof Error ? error.message : error);
@@ -5641,13 +5726,13 @@ function registerCheckCommand(program) {
5641
5726
  }
5642
5727
  // src/commands/ci/init.ts
5643
5728
  import fs23 from "node:fs/promises";
5644
- import path20 from "node:path";
5729
+ import path21 from "node:path";
5645
5730
  import chalk4 from "chalk";
5646
5731
  import YAML5 from "yaml";
5647
5732
 
5648
5733
  // src/config/ci-loader.ts
5649
5734
  import fs22 from "node:fs/promises";
5650
- import path19 from "node:path";
5735
+ import path20 from "node:path";
5651
5736
  import YAML4 from "yaml";
5652
5737
 
5653
5738
  // src/config/ci-schema.ts
@@ -5677,7 +5762,7 @@ var ciConfigSchema = z3.object({
5677
5762
  var GAUNTLET_DIR2 = ".gauntlet";
5678
5763
  var CI_FILE = "ci.yml";
5679
5764
  async function loadCIConfig(rootDir = process.cwd()) {
5680
- const ciPath = path19.join(rootDir, GAUNTLET_DIR2, CI_FILE);
5765
+ const ciPath = path20.join(rootDir, GAUNTLET_DIR2, CI_FILE);
5681
5766
  if (!await fileExists3(ciPath)) {
5682
5767
  throw new Error(`CI configuration file not found at ${ciPath}. Run 'agent-gauntlet ci init' to create it.`);
5683
5768
  }
@@ -5685,9 +5770,9 @@ async function loadCIConfig(rootDir = process.cwd()) {
5685
5770
  const raw = YAML4.parse(content);
5686
5771
  return ciConfigSchema.parse(raw);
5687
5772
  }
5688
- async function fileExists3(path20) {
5773
+ async function fileExists3(path21) {
5689
5774
  try {
5690
- const stat = await fs22.stat(path20);
5775
+ const stat = await fs22.stat(path21);
5691
5776
  return stat.isFile();
5692
5777
  } catch {
5693
5778
  return false;
@@ -5778,10 +5863,10 @@ jobs:
5778
5863
 
5779
5864
  // src/commands/ci/init.ts
5780
5865
  async function initCI() {
5781
- const workflowDir = path20.join(process.cwd(), ".github", "workflows");
5782
- const workflowPath = path20.join(workflowDir, "gauntlet.yml");
5783
- const gauntletDir = path20.join(process.cwd(), ".gauntlet");
5784
- const ciConfigPath = path20.join(gauntletDir, "ci.yml");
5866
+ const workflowDir = path21.join(process.cwd(), ".github", "workflows");
5867
+ const workflowPath = path21.join(workflowDir, "gauntlet.yml");
5868
+ const gauntletDir = path21.join(process.cwd(), ".gauntlet");
5869
+ const ciConfigPath = path21.join(gauntletDir, "ci.yml");
5785
5870
  if (!await fileExists4(ciConfigPath)) {
5786
5871
  console.log(chalk4.yellow("Creating starter .gauntlet/ci.yml..."));
5787
5872
  await fs23.mkdir(gauntletDir, { recursive: true });
@@ -5832,9 +5917,9 @@ checks:
5832
5917
  await fs23.writeFile(workflowPath, templateContent);
5833
5918
  console.log(chalk4.green("Successfully generated GitHub Actions workflow!"));
5834
5919
  }
5835
- async function fileExists4(path21) {
5920
+ async function fileExists4(path22) {
5836
5921
  try {
5837
- const stat = await fs23.stat(path21);
5922
+ const stat = await fs23.stat(path22);
5838
5923
  return stat.isFile();
5839
5924
  } catch {
5840
5925
  return false;
@@ -6035,12 +6120,12 @@ function printJobsByWorkDir(jobs) {
6035
6120
  }
6036
6121
  }
6037
6122
  // src/commands/health.ts
6038
- import path22 from "node:path";
6123
+ import path23 from "node:path";
6039
6124
  import chalk7 from "chalk";
6040
6125
 
6041
6126
  // src/config/validator.ts
6042
6127
  import fs24 from "node:fs/promises";
6043
- import path21 from "node:path";
6128
+ import path22 from "node:path";
6044
6129
  import matter2 from "gray-matter";
6045
6130
  import YAML6 from "yaml";
6046
6131
  import { ZodError } from "zod";
@@ -6051,10 +6136,10 @@ var REVIEWS_DIR2 = "reviews";
6051
6136
  async function validateConfig(rootDir = process.cwd()) {
6052
6137
  const issues = [];
6053
6138
  const filesChecked = [];
6054
- const gauntletPath = path21.join(rootDir, GAUNTLET_DIR3);
6139
+ const gauntletPath = path22.join(rootDir, GAUNTLET_DIR3);
6055
6140
  const existingCheckNames = new Set;
6056
6141
  const existingReviewNames = new Set;
6057
- const configPath = path21.join(gauntletPath, CONFIG_FILE2);
6142
+ const configPath = path22.join(gauntletPath, CONFIG_FILE2);
6058
6143
  let projectConfig = null;
6059
6144
  const checks = {};
6060
6145
  const reviews = {};
@@ -6108,15 +6193,15 @@ async function validateConfig(rootDir = process.cwd()) {
6108
6193
  message: `Error reading file: ${err.message}`
6109
6194
  });
6110
6195
  }
6111
- const checksPath = path21.join(gauntletPath, CHECKS_DIR2);
6196
+ const checksPath = path22.join(gauntletPath, CHECKS_DIR2);
6112
6197
  if (await dirExists2(checksPath)) {
6113
6198
  try {
6114
6199
  const checkFiles = await fs24.readdir(checksPath);
6115
6200
  for (const file of checkFiles) {
6116
6201
  if (file.endsWith(".yml") || file.endsWith(".yaml")) {
6117
- const filePath = path21.join(checksPath, file);
6202
+ const filePath = path22.join(checksPath, file);
6118
6203
  filesChecked.push(filePath);
6119
- const name = path21.basename(file, path21.extname(file));
6204
+ const name = path22.basename(file, path22.extname(file));
6120
6205
  try {
6121
6206
  const content = await fs24.readFile(filePath, "utf-8");
6122
6207
  const raw = YAML6.parse(content);
@@ -6170,14 +6255,14 @@ async function validateConfig(rootDir = process.cwd()) {
6170
6255
  });
6171
6256
  }
6172
6257
  }
6173
- const reviewsPath = path21.join(gauntletPath, REVIEWS_DIR2);
6258
+ const reviewsPath = path22.join(gauntletPath, REVIEWS_DIR2);
6174
6259
  if (await dirExists2(reviewsPath)) {
6175
6260
  try {
6176
6261
  const reviewFiles = await fs24.readdir(reviewsPath);
6177
6262
  const reviewNameSources = new Map;
6178
6263
  for (const file of reviewFiles) {
6179
6264
  if (file.endsWith(".md") || file.endsWith(".yml") || file.endsWith(".yaml")) {
6180
- const name = path21.basename(file, path21.extname(file));
6265
+ const name = path22.basename(file, path22.extname(file));
6181
6266
  const sources = reviewNameSources.get(name) || [];
6182
6267
  sources.push(file);
6183
6268
  reviewNameSources.set(name, sources);
@@ -6194,8 +6279,8 @@ async function validateConfig(rootDir = process.cwd()) {
6194
6279
  }
6195
6280
  for (const file of reviewFiles) {
6196
6281
  if (file.endsWith(".md")) {
6197
- const filePath = path21.join(reviewsPath, file);
6198
- const reviewName = path21.basename(file, ".md");
6282
+ const filePath = path22.join(reviewsPath, file);
6283
+ const reviewName = path22.basename(file, ".md");
6199
6284
  existingReviewNames.add(reviewName);
6200
6285
  filesChecked.push(filePath);
6201
6286
  try {
@@ -6211,7 +6296,7 @@ async function validateConfig(rootDir = process.cwd()) {
6211
6296
  }
6212
6297
  validateCliPreferenceTools(frontmatter, filePath, issues);
6213
6298
  const parsedFrontmatter = reviewPromptFrontmatterSchema.parse(frontmatter);
6214
- const name = path21.basename(file, ".md");
6299
+ const name = path22.basename(file, ".md");
6215
6300
  reviews[name] = parsedFrontmatter;
6216
6301
  reviewSourceFiles[name] = filePath;
6217
6302
  validateReviewSemantics(parsedFrontmatter, filePath, issues);
@@ -6219,8 +6304,8 @@ async function validateConfig(rootDir = process.cwd()) {
6219
6304
  handleReviewValidationError(error, filePath, issues);
6220
6305
  }
6221
6306
  } else if (file.endsWith(".yml") || file.endsWith(".yaml")) {
6222
- const filePath = path21.join(reviewsPath, file);
6223
- const reviewName = path21.basename(file, path21.extname(file));
6307
+ const filePath = path22.join(reviewsPath, file);
6308
+ const reviewName = path22.basename(file, path22.extname(file));
6224
6309
  existingReviewNames.add(reviewName);
6225
6310
  filesChecked.push(filePath);
6226
6311
  try {
@@ -6355,7 +6440,7 @@ async function validateConfig(rootDir = process.cwd()) {
6355
6440
  for (const [reviewName, reviewConfig] of Object.entries(reviews)) {
6356
6441
  const pref = reviewConfig.cli_preference;
6357
6442
  if (pref && Array.isArray(pref)) {
6358
- const reviewFile = reviewSourceFiles[reviewName] || path21.join(reviewsPath, `${reviewName}.md`);
6443
+ const reviewFile = reviewSourceFiles[reviewName] || path22.join(reviewsPath, `${reviewName}.md`);
6359
6444
  for (let i = 0;i < pref.length; i++) {
6360
6445
  const tool = pref[i];
6361
6446
  if (!allowedTools.has(tool)) {
@@ -6481,17 +6566,17 @@ function handleReviewValidationError(error, filePath, issues) {
6481
6566
  }
6482
6567
  }
6483
6568
  }
6484
- async function fileExists5(path22) {
6569
+ async function fileExists5(path23) {
6485
6570
  try {
6486
- const stat = await fs24.stat(path22);
6571
+ const stat = await fs24.stat(path23);
6487
6572
  return stat.isFile();
6488
6573
  } catch {
6489
6574
  return false;
6490
6575
  }
6491
6576
  }
6492
- async function dirExists2(path22) {
6577
+ async function dirExists2(path23) {
6493
6578
  try {
6494
- const stat = await fs24.stat(path22);
6579
+ const stat = await fs24.stat(path23);
6495
6580
  return stat.isDirectory();
6496
6581
  } catch {
6497
6582
  return false;
@@ -6507,7 +6592,7 @@ function registerHealthCommand(program) {
6507
6592
  console.log(chalk7.yellow(" No config files found"));
6508
6593
  } else {
6509
6594
  for (const file of validationResult.filesChecked) {
6510
- const relativePath = path22.relative(process.cwd(), file);
6595
+ const relativePath = path23.relative(process.cwd(), file);
6511
6596
  console.log(chalk7.dim(` ${relativePath}`));
6512
6597
  }
6513
6598
  if (validationResult.valid && validationResult.issues.length === 0) {
@@ -6515,7 +6600,7 @@ function registerHealthCommand(program) {
6515
6600
  } else {
6516
6601
  const issuesByFile = new Map;
6517
6602
  for (const issue of validationResult.issues) {
6518
- const relativeFile = path22.relative(process.cwd(), issue.file);
6603
+ const relativeFile = path23.relative(process.cwd(), issue.file);
6519
6604
  if (!issuesByFile.has(relativeFile)) {
6520
6605
  issuesByFile.set(relativeFile, []);
6521
6606
  }
@@ -6636,19 +6721,21 @@ function registerHelpCommand(program) {
6636
6721
  // src/commands/init.ts
6637
6722
  import { readFileSync } from "node:fs";
6638
6723
  import fs25 from "node:fs/promises";
6639
- import path23 from "node:path";
6640
- import readline from "node:readline";
6724
+ import path24 from "node:path";
6641
6725
  import { fileURLToPath } from "node:url";
6642
6726
  import chalk9 from "chalk";
6643
- var __dirname2 = path23.dirname(fileURLToPath(import.meta.url));
6727
+ var __dirname2 = path24.dirname(fileURLToPath(import.meta.url));
6644
6728
  function readSkillTemplate(filename) {
6645
- const templatePath = path23.join(__dirname2, "skill-templates", filename);
6729
+ const templatePath = path24.join(__dirname2, "skill-templates", filename);
6646
6730
  return readFileSync(templatePath, "utf-8");
6647
6731
  }
6648
- var MAX_PROMPT_ATTEMPTS = 10;
6649
- function makeQuestion(rl) {
6650
- return (prompt) => new Promise((resolve) => rl.question(prompt, (a) => resolve(a?.trim() ?? "")));
6651
- }
6732
+ var CLI_PREFERENCE_ORDER = [
6733
+ "codex",
6734
+ "claude",
6735
+ "cursor",
6736
+ "github-copilot",
6737
+ "gemini"
6738
+ ];
6652
6739
  var ADAPTER_CONFIG = {
6653
6740
  claude: { allow_tool_use: false, thinking_budget: "high" },
6654
6741
  codex: { allow_tool_use: false, thinking_budget: "low" },
@@ -6770,9 +6857,9 @@ var SKILL_DEFINITIONS = [
6770
6857
  }
6771
6858
  ];
6772
6859
  function registerInitCommand(program) {
6773
- program.command("init").description("Initialize .gauntlet configuration").option("-y, --yes", "Skip prompts and use defaults (all available CLIs)").action(async (options) => {
6860
+ program.command("init").description("Initialize .gauntlet configuration").option("-y, --yes", "Skip prompts and use defaults").action(async (options) => {
6774
6861
  const projectRoot = process.cwd();
6775
- const targetDir = path23.join(projectRoot, ".gauntlet");
6862
+ const targetDir = path24.join(projectRoot, ".gauntlet");
6776
6863
  if (await exists(targetDir)) {
6777
6864
  console.log(chalk9.yellow(".gauntlet directory already exists."));
6778
6865
  return;
@@ -6780,76 +6867,144 @@ function registerInitCommand(program) {
6780
6867
  console.log("Detecting available CLI agents...");
6781
6868
  const availableAdapters = await detectAvailableCLIs();
6782
6869
  if (availableAdapters.length === 0) {
6783
- console.log();
6784
- console.log(chalk9.red("Error: No CLI agents found. Install at least one:"));
6785
- console.log(" - Claude: https://docs.anthropic.com/en/docs/claude-code");
6786
- console.log(" - Gemini: https://github.com/google-gemini/gemini-cli");
6787
- console.log(" - Codex: https://github.com/openai/codex");
6788
- console.log();
6870
+ printNoCLIsMessage();
6789
6871
  return;
6790
6872
  }
6791
- const baseBranch = await detectBaseBranch();
6792
- await fs25.mkdir(targetDir);
6793
- await fs25.mkdir(path23.join(targetDir, "checks"));
6794
- await fs25.mkdir(path23.join(targetDir, "reviews"));
6795
- const commands = SKILL_DEFINITIONS.map((skill) => ({
6796
- action: skill.action,
6797
- content: skill.content,
6798
- ..."references" in skill ? { references: skill.references } : {},
6799
- ..."skillsOnly" in skill ? { skillsOnly: skill.skillsOnly } : {}
6800
- }));
6801
- let installedNames;
6802
- if (options.yes) {
6803
- installedNames = availableAdapters.map((a) => a.name);
6804
- const adaptersToInstall = availableAdapters.filter((a) => a.getProjectCommandDir() !== null || a.getProjectSkillDir() !== null);
6805
- if (adaptersToInstall.length > 0) {
6806
- await installCommands({
6807
- level: "project",
6808
- agentNames: adaptersToInstall.map((a) => a.name),
6809
- projectRoot,
6810
- commands
6811
- });
6812
- }
6813
- } else {
6814
- installedNames = await promptAndInstallCommands({
6815
- projectRoot,
6816
- commands,
6817
- availableAdapters
6818
- });
6873
+ await scaffoldProject({
6874
+ projectRoot,
6875
+ targetDir,
6876
+ availableAdapters,
6877
+ skipPrompts: options.yes ?? false
6878
+ });
6879
+ if (availableAdapters.some((a) => a.name === "claude")) {
6880
+ await installStopHook(projectRoot);
6881
+ }
6882
+ if (availableAdapters.some((a) => a.name === "cursor")) {
6883
+ await installCursorStopHook(projectRoot);
6819
6884
  }
6820
- const cliList = availableAdapters.map((a) => ` - ${a.name}`).join(`
6885
+ await addToGitignore(projectRoot, "gauntlet_logs");
6886
+ console.log();
6887
+ console.log(chalk9.bold("Run /gauntlet-setup to configure your checks and reviews"));
6888
+ });
6889
+ }
6890
+ function printNoCLIsMessage() {
6891
+ console.log();
6892
+ console.log(chalk9.red("Error: No CLI agents found. Install at least one:"));
6893
+ console.log(" - Claude: https://docs.anthropic.com/en/docs/claude-code");
6894
+ console.log(" - Gemini: https://github.com/google-gemini/gemini-cli");
6895
+ console.log(" - Codex: https://github.com/openai/codex");
6896
+ console.log();
6897
+ }
6898
+ async function scaffoldProject(options) {
6899
+ const { projectRoot, targetDir, availableAdapters, skipPrompts } = options;
6900
+ await fs25.mkdir(targetDir);
6901
+ await fs25.mkdir(path24.join(targetDir, "checks"));
6902
+ await fs25.mkdir(path24.join(targetDir, "reviews"));
6903
+ const commands = SKILL_DEFINITIONS.map((skill) => ({
6904
+ action: skill.action,
6905
+ content: skill.content,
6906
+ ..."references" in skill ? { references: skill.references } : {},
6907
+ ..."skillsOnly" in skill ? { skillsOnly: skill.skillsOnly } : {}
6908
+ }));
6909
+ if (skipPrompts) {
6910
+ await installCommands({
6911
+ level: "project",
6912
+ agentNames: ["claude"],
6913
+ projectRoot,
6914
+ commands
6915
+ });
6916
+ } else {
6917
+ await promptAndInstallCommands({ projectRoot, commands });
6918
+ }
6919
+ await writeConfigYml(targetDir, availableAdapters);
6920
+ await fs25.writeFile(path24.join(targetDir, "reviews", "code-quality.yml"), `builtin: code-quality
6921
+ num_reviews: 1
6821
6922
  `);
6822
- const adapterSettings = buildAdapterSettingsBlock(availableAdapters);
6823
- const configContent = `base_branch: ${baseBranch}
6824
- log_dir: gauntlet_logs
6825
-
6826
- # Run gates in parallel when possible (default: true)
6827
- # allow_parallel: true
6828
-
6923
+ console.log(chalk9.green("Created .gauntlet/reviews/code-quality.yml"));
6924
+ await copyStatusScript(targetDir);
6925
+ }
6926
+ async function writeConfigYml(targetDir, adapters3) {
6927
+ const baseBranch = await detectBaseBranch();
6928
+ const sortedAdapters = [...adapters3].sort((a, b) => CLI_PREFERENCE_ORDER.indexOf(a.name) - CLI_PREFERENCE_ORDER.indexOf(b.name));
6929
+ const cliList = sortedAdapters.map((a) => ` - ${a.name}`).join(`
6930
+ `);
6931
+ const adapterSettings = buildAdapterSettingsBlock(adapters3);
6932
+ const content = `# Ordered list of CLI agents to try for reviews
6829
6933
  cli:
6830
6934
  default_preference:
6831
6935
  ${cliList}
6832
6936
  ${adapterSettings}
6833
6937
  # entry_points configured by /gauntlet-setup
6834
6938
  entry_points: []
6939
+
6940
+ # -------------------------------------------------------------------
6941
+ # All settings below are optional. Uncomment and change as needed.
6942
+ # -------------------------------------------------------------------
6943
+
6944
+ # Git ref for detecting local changes via git diff (default: origin/main)
6945
+ # base_branch: ${baseBranch}
6946
+
6947
+ # Directory for per-job logs (default: gauntlet_logs)
6948
+ # log_dir: gauntlet_logs
6949
+
6950
+ # Run gates in parallel when possible (default: true)
6951
+ # allow_parallel: true
6952
+
6953
+ # Maximum retry attempts before declaring "Retry limit exceeded" (default: 3)
6954
+ # max_retries: 3
6955
+
6956
+ # Archived session directories to keep during log rotation (default: 3, 0 = disable)
6957
+ # max_previous_logs: 3
6958
+
6959
+ # Priority threshold for filtering new violations during reruns (default: medium)
6960
+ # Options: critical, high, medium, low
6961
+ # rerun_new_issue_threshold: medium
6962
+
6963
+ # Stop hook — auto-run gauntlet when the agent stops
6964
+ # Precedence: env vars > project config > global config (~/.config/agent-gauntlet/config.yml)
6965
+ # Env overrides: GAUNTLET_STOP_HOOK_ENABLED, GAUNTLET_STOP_HOOK_INTERVAL_MINUTES,
6966
+ # GAUNTLET_AUTO_PUSH_PR, GAUNTLET_AUTO_FIX_PR
6967
+ # stop_hook:
6968
+ # enabled: false
6969
+ # run_interval_minutes: 5 # Minimum minutes between runs (0 = always run)
6970
+ # auto_push_pr: false # Check/create PR after gates pass
6971
+ # auto_fix_pr: false # Wait for CI checks after PR (requires auto_push_pr)
6972
+
6973
+ # Debug log — persistent debug logging to .debug.log
6974
+ # debug_log:
6975
+ # enabled: false
6976
+ # max_size_mb: 10 # Max size before rotation to .debug.log.1
6977
+
6978
+ # Structured logging via LogTape
6979
+ # logging:
6980
+ # level: debug # Options: debug, info, warning, error
6981
+ # console:
6982
+ # enabled: true
6983
+ # format: pretty # Options: pretty, json
6984
+ # file:
6985
+ # enabled: true
6986
+ # format: text # Options: text, json
6835
6987
  `;
6836
- await fs25.writeFile(path23.join(targetDir, "config.yml"), configContent);
6837
- console.log(chalk9.green("Created .gauntlet/config.yml"));
6838
- const reviewYamlContent = `builtin: code-quality
6839
- num_reviews: 1
6840
- `;
6841
- await fs25.writeFile(path23.join(targetDir, "reviews", "code-quality.yml"), reviewYamlContent);
6842
- console.log(chalk9.green("Created .gauntlet/reviews/code-quality.yml"));
6843
- await copyStatusScript(targetDir);
6844
- if (installedNames.includes("claude")) {
6845
- await installStopHook(projectRoot);
6846
- }
6847
- if (installedNames.includes("cursor")) {
6848
- await installCursorStopHook(projectRoot);
6988
+ await fs25.writeFile(path24.join(targetDir, "config.yml"), content);
6989
+ console.log(chalk9.green("Created .gauntlet/config.yml"));
6990
+ }
6991
+ async function addToGitignore(projectRoot, entry) {
6992
+ const gitignorePath = path24.join(projectRoot, ".gitignore");
6993
+ let content = "";
6994
+ if (await exists(gitignorePath)) {
6995
+ content = await fs25.readFile(gitignorePath, "utf-8");
6996
+ const lines = content.split(`
6997
+ `).map((l) => l.trim());
6998
+ if (lines.includes(entry)) {
6999
+ return;
6849
7000
  }
6850
- console.log();
6851
- console.log(chalk9.bold("Run /gauntlet-setup to configure your checks and reviews"));
6852
- });
7001
+ }
7002
+ const suffix = content.length > 0 && !content.endsWith(`
7003
+ `) ? `
7004
+ ` : "";
7005
+ await fs25.appendFile(gitignorePath, `${suffix}${entry}
7006
+ `);
7007
+ console.log(chalk9.green(`Added ${entry} to .gitignore`));
6853
7008
  }
6854
7009
  async function detectBaseBranch() {
6855
7010
  try {
@@ -6878,7 +7033,7 @@ ${lines.join(`
6878
7033
  `;
6879
7034
  }
6880
7035
  async function detectAvailableCLIs() {
6881
- const allAdapters = getAllAdapters();
7036
+ const allAdapters = [...getAllAdapters()].sort((a, b) => CLI_PREFERENCE_ORDER.indexOf(a.name) - CLI_PREFERENCE_ORDER.indexOf(b.name));
6882
7037
  const available = [];
6883
7038
  for (const adapter of allAdapters) {
6884
7039
  const isAvailable = await adapter.isAvailable();
@@ -6891,31 +7046,13 @@ async function detectAvailableCLIs() {
6891
7046
  }
6892
7047
  return available;
6893
7048
  }
6894
- function parseSelections(selections, adapters3) {
6895
- const chosen = [];
6896
- for (const sel of selections) {
6897
- const num = parseInt(sel, 10);
6898
- if (Number.isNaN(num) || num < 1 || num > adapters3.length + 1) {
6899
- console.log(chalk9.yellow(`Invalid selection: ${sel}`));
6900
- return null;
6901
- }
6902
- if (num === adapters3.length + 1) {
6903
- chosen.push(...adapters3);
6904
- } else {
6905
- const adapter = adapters3[num - 1];
6906
- if (adapter)
6907
- chosen.push(adapter);
6908
- }
6909
- }
6910
- return [...new Set(chosen)];
6911
- }
6912
7049
  async function copyStatusScript(targetDir) {
6913
- const statusScriptDir = path23.join(targetDir, "skills", "gauntlet", "status", "scripts");
6914
- const statusScriptPath = path23.join(statusScriptDir, "status.ts");
7050
+ const statusScriptDir = path24.join(targetDir, "skills", "gauntlet", "status", "scripts");
7051
+ const statusScriptPath = path24.join(statusScriptDir, "status.ts");
6915
7052
  await fs25.mkdir(statusScriptDir, { recursive: true });
6916
7053
  if (await exists(statusScriptPath))
6917
7054
  return;
6918
- const bundledScript = path23.join(path23.dirname(new URL(import.meta.url).pathname), "..", "scripts", "status.ts");
7055
+ const bundledScript = path24.join(path24.dirname(new URL(import.meta.url).pathname), "..", "scripts", "status.ts");
6919
7056
  if (await exists(bundledScript)) {
6920
7057
  await fs25.copyFile(bundledScript, statusScriptPath);
6921
7058
  console.log(chalk9.green("Created .gauntlet/skills/gauntlet/status/scripts/status.ts"));
@@ -6923,117 +7060,36 @@ async function copyStatusScript(targetDir) {
6923
7060
  console.log(chalk9.yellow("Warning: bundled status script not found; /gauntlet-status may fail."));
6924
7061
  }
6925
7062
  }
6926
- async function promptInstallLevel(questionFn) {
6927
- console.log("Where would you like to install the /gauntlet command?");
6928
- console.log(" 1) Don't install commands");
6929
- console.log(" 2) Project level (in this repo's .claude/skills, .gemini/commands, etc.)");
6930
- console.log(" 3) User level (in ~/.claude/skills, ~/.gemini/commands, etc.)");
6931
- console.log();
6932
- let answer = await questionFn("Select option [1-3]: ");
6933
- let attempts = 0;
6934
- while (true) {
6935
- attempts++;
6936
- if (attempts > MAX_PROMPT_ATTEMPTS)
6937
- throw new Error("Too many invalid attempts");
6938
- if (answer === "1")
6939
- return "none";
6940
- if (answer === "2")
6941
- return "project";
6942
- if (answer === "3")
6943
- return "user";
6944
- console.log(chalk9.yellow("Please enter 1, 2, or 3"));
6945
- answer = await questionFn("Select option [1-3]: ");
6946
- }
6947
- }
6948
- async function promptAgentSelection(questionFn, installableAdapters) {
6949
- console.log();
6950
- console.log("Which CLI agents would you like to install the command for?");
6951
- installableAdapters.forEach((adapter, i) => {
6952
- console.log(` ${i + 1}) ${adapter.name}`);
6953
- });
6954
- console.log(` ${installableAdapters.length + 1}) All of the above`);
6955
- console.log();
6956
- const promptText = `Select options (comma-separated, e.g., 1,2 or ${installableAdapters.length + 1} for all): `;
6957
- let answer = await questionFn(promptText);
6958
- let attempts = 0;
6959
- while (true) {
6960
- attempts++;
6961
- if (attempts > MAX_PROMPT_ATTEMPTS)
6962
- throw new Error("Too many invalid attempts");
6963
- const selections = answer.split(",").map((s) => s.trim()).filter((s) => s);
6964
- if (selections.length === 0) {
6965
- console.log(chalk9.yellow("Please select at least one option"));
6966
- answer = await questionFn(promptText);
6967
- continue;
6968
- }
6969
- const chosen = parseSelections(selections, installableAdapters);
6970
- if (chosen)
6971
- return chosen.map((a) => a.name);
6972
- answer = await questionFn(promptText);
6973
- }
6974
- }
6975
7063
  async function promptAndInstallCommands(options) {
6976
- const { projectRoot, commands, availableAdapters } = options;
6977
- if (availableAdapters.length === 0)
6978
- return [];
6979
- const rl = readline.createInterface({
6980
- input: process.stdin,
6981
- output: process.stdout
7064
+ const { projectRoot, commands } = options;
7065
+ await installCommands({
7066
+ level: "project",
7067
+ agentNames: ["claude"],
7068
+ projectRoot,
7069
+ commands
6982
7070
  });
6983
- const question = makeQuestion(rl);
6984
- try {
6985
- console.log();
6986
- console.log(chalk9.bold("CLI Agent Command Setup"));
6987
- console.log(chalk9.dim("The gauntlet command can be installed for CLI agents so you can run /gauntlet directly."));
6988
- console.log();
6989
- const installLevel = await promptInstallLevel(question);
6990
- if (installLevel === "none") {
6991
- console.log(chalk9.dim(`
6992
- Skipping command installation.`));
6993
- rl.close();
6994
- return [];
6995
- }
6996
- const installableAdapters = installLevel === "project" ? availableAdapters.filter((a) => a.getProjectCommandDir() !== null || a.getProjectSkillDir() !== null) : availableAdapters.filter((a) => a.getUserCommandDir() !== null || a.getUserSkillDir() !== null);
6997
- if (installableAdapters.length === 0) {
6998
- console.log(chalk9.yellow(`No available agents support ${installLevel}-level commands.`));
6999
- rl.close();
7000
- return [];
7001
- }
7002
- const selectedAgents = await promptAgentSelection(question, installableAdapters);
7003
- rl.close();
7004
- await installCommands({
7005
- level: installLevel,
7006
- agentNames: selectedAgents,
7007
- projectRoot,
7008
- commands
7009
- });
7010
- return selectedAgents;
7011
- } catch (error) {
7012
- rl.close();
7013
- throw error;
7014
- }
7015
7071
  }
7016
7072
  async function installSkill(skillDir, ctx, command) {
7017
- const actionDir = path23.join(skillDir, `gauntlet-${command.action}`);
7018
- const skillPath = path23.join(actionDir, "SKILL.md");
7073
+ const actionDir = path24.join(skillDir, `gauntlet-${command.action}`);
7074
+ const skillPath = path24.join(actionDir, "SKILL.md");
7019
7075
  await fs25.mkdir(actionDir, { recursive: true });
7020
7076
  if (await exists(skillPath)) {
7021
- const relPath2 = ctx.isUserLevel ? skillPath : path23.relative(ctx.projectRoot, skillPath);
7077
+ const relPath2 = ctx.isUserLevel ? skillPath : path24.relative(ctx.projectRoot, skillPath);
7022
7078
  console.log(chalk9.dim(` claude: ${relPath2} already exists, skipping`));
7023
7079
  return;
7024
7080
  }
7025
7081
  await fs25.writeFile(skillPath, command.content);
7026
- const relPath = ctx.isUserLevel ? skillPath : path23.relative(ctx.projectRoot, skillPath);
7082
+ const relPath = ctx.isUserLevel ? skillPath : path24.relative(ctx.projectRoot, skillPath);
7027
7083
  console.log(chalk9.green(`Created ${relPath}`));
7028
7084
  if (command.references) {
7029
- const refsDir = path23.join(actionDir, "references");
7085
+ const refsDir = path24.join(actionDir, "references");
7030
7086
  await fs25.mkdir(refsDir, { recursive: true });
7031
7087
  for (const [fileName, fileContent] of Object.entries(command.references)) {
7032
- const refPath = path23.join(refsDir, fileName);
7088
+ const refPath = path24.join(refsDir, fileName);
7033
7089
  if (await exists(refPath))
7034
7090
  continue;
7035
7091
  await fs25.writeFile(refPath, fileContent);
7036
- const refRelPath = ctx.isUserLevel ? refPath : path23.relative(ctx.projectRoot, refPath);
7092
+ const refRelPath = ctx.isUserLevel ? refPath : path24.relative(ctx.projectRoot, refPath);
7037
7093
  console.log(chalk9.green(`Created ${refRelPath}`));
7038
7094
  }
7039
7095
  }
@@ -7041,19 +7097,19 @@ async function installSkill(skillDir, ctx, command) {
7041
7097
  async function installFlatCommand(adapter, commandDir, ctx, command) {
7042
7098
  const name = command.action === "run" ? "gauntlet" : command.action;
7043
7099
  const fileName = `${name}${adapter.getCommandExtension()}`;
7044
- const filePath = path23.join(commandDir, fileName);
7100
+ const filePath = path24.join(commandDir, fileName);
7045
7101
  if (await exists(filePath)) {
7046
- const relPath2 = ctx.isUserLevel ? filePath : path23.relative(ctx.projectRoot, filePath);
7102
+ const relPath2 = ctx.isUserLevel ? filePath : path24.relative(ctx.projectRoot, filePath);
7047
7103
  console.log(chalk9.dim(` ${adapter.name}: ${relPath2} already exists, skipping`));
7048
7104
  return;
7049
7105
  }
7050
7106
  const transformedContent = adapter.transformCommand(command.content);
7051
7107
  await fs25.writeFile(filePath, transformedContent);
7052
- const relPath = ctx.isUserLevel ? filePath : path23.relative(ctx.projectRoot, filePath);
7108
+ const relPath = ctx.isUserLevel ? filePath : path24.relative(ctx.projectRoot, filePath);
7053
7109
  console.log(chalk9.green(`Created ${relPath}`));
7054
7110
  }
7055
7111
  async function installSkillsForAdapter(adapter, skillDir, ctx, commands) {
7056
- const resolvedSkillDir = ctx.isUserLevel ? skillDir : path23.join(ctx.projectRoot, skillDir);
7112
+ const resolvedSkillDir = ctx.isUserLevel ? skillDir : path24.join(ctx.projectRoot, skillDir);
7057
7113
  try {
7058
7114
  for (const command of commands) {
7059
7115
  await installSkill(resolvedSkillDir, ctx, command);
@@ -7064,7 +7120,7 @@ async function installSkillsForAdapter(adapter, skillDir, ctx, commands) {
7064
7120
  }
7065
7121
  }
7066
7122
  async function installFlatCommandsForAdapter(adapter, commandDir, ctx, commands) {
7067
- const resolvedCommandDir = ctx.isUserLevel ? commandDir : path23.join(ctx.projectRoot, commandDir);
7123
+ const resolvedCommandDir = ctx.isUserLevel ? commandDir : path24.join(ctx.projectRoot, commandDir);
7068
7124
  try {
7069
7125
  await fs25.mkdir(resolvedCommandDir, { recursive: true });
7070
7126
  const flatCommands = commands.filter((c) => c.action !== "check" && c.action !== "status" && !c.skillsOnly);
@@ -7126,8 +7182,8 @@ var CURSOR_STOP_HOOK_CONFIG = {
7126
7182
  }
7127
7183
  };
7128
7184
  async function installStopHook(projectRoot) {
7129
- const claudeDir = path23.join(projectRoot, ".claude");
7130
- const settingsPath = path23.join(claudeDir, "settings.local.json");
7185
+ const claudeDir = path24.join(projectRoot, ".claude");
7186
+ const settingsPath = path24.join(claudeDir, "settings.local.json");
7131
7187
  await fs25.mkdir(claudeDir, { recursive: true });
7132
7188
  let existingSettings = {};
7133
7189
  if (await exists(settingsPath)) {
@@ -7158,8 +7214,8 @@ async function installStopHook(projectRoot) {
7158
7214
  console.log(chalk9.green("Stop hook installed - gauntlet will run automatically when agent stops"));
7159
7215
  }
7160
7216
  async function installCursorStopHook(projectRoot) {
7161
- const cursorDir = path23.join(projectRoot, ".cursor");
7162
- const hooksPath = path23.join(cursorDir, "hooks.json");
7217
+ const cursorDir = path24.join(projectRoot, ".cursor");
7218
+ const hooksPath = path24.join(cursorDir, "hooks.json");
7163
7219
  await fs25.mkdir(cursorDir, { recursive: true });
7164
7220
  let existingConfig = {};
7165
7221
  if (await exists(hooksPath)) {
@@ -7362,7 +7418,7 @@ function registerReviewCommand(program) {
7362
7418
  }
7363
7419
  // src/core/run-executor.ts
7364
7420
  import fs26 from "node:fs/promises";
7365
- import path24 from "node:path";
7421
+ import path25 from "node:path";
7366
7422
 
7367
7423
  // src/core/diff-stats.ts
7368
7424
  import { execFile as execFile2 } from "node:child_process";
@@ -7672,7 +7728,7 @@ function isProcessAlive(pid) {
7672
7728
  }
7673
7729
  async function tryAcquireLock(logDir) {
7674
7730
  await fs26.mkdir(logDir, { recursive: true });
7675
- const lockPath = path24.resolve(logDir, LOCK_FILENAME2);
7731
+ const lockPath = path25.resolve(logDir, LOCK_FILENAME2);
7676
7732
  try {
7677
7733
  await fs26.writeFile(lockPath, String(process.pid), { flag: "wx" });
7678
7734
  return true;
@@ -7721,7 +7777,7 @@ async function findLatestConsoleLog(logDir) {
7721
7777
  }
7722
7778
  }
7723
7779
  }
7724
- return latestFile ? path24.join(logDir, latestFile) : null;
7780
+ return latestFile ? path25.join(logDir, latestFile) : null;
7725
7781
  } catch {
7726
7782
  return null;
7727
7783
  }
@@ -7750,6 +7806,7 @@ var statusMessages = {
7750
7806
  error: "Unexpected error occurred.",
7751
7807
  no_config: "No .gauntlet/config.yml found.",
7752
7808
  stop_hook_active: "Stop hook already active.",
7809
+ loop_detected: "Loop detected — rapid blocks overridden.",
7753
7810
  interval_not_elapsed: "Run interval not elapsed.",
7754
7811
  invalid_input: "Invalid input.",
7755
7812
  stop_hook_disabled: "Stop hook is disabled via configuration.",
@@ -8301,4 +8358,4 @@ if (process.argv.length < 3) {
8301
8358
  }
8302
8359
  program.parse(process.argv);
8303
8360
 
8304
- //# debugId=5D4AF6110EC21D0564756E2164756E21
8361
+ //# debugId=D6CA917DC551041A64756E2164756E21