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 +344 -287
- package/dist/index.js.map +10 -10
- package/package.json +1 -1
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.
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
3186
|
+
const tmpFile = path13.join(tmpDir, `gauntlet-gemini-${process.pid}-${Date.now()}.txt`);
|
|
3105
3187
|
await fs13.writeFile(tmpFile, fullContent);
|
|
3106
|
-
const telemetryFile =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
4778
|
+
failure = await parseJsonReviewFile(path16.join(logDir, jsonFile));
|
|
4694
4779
|
} else if (logFile) {
|
|
4695
|
-
failure = await parseLogFile(
|
|
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 =
|
|
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:
|
|
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(
|
|
4980
|
+
failure = await parseJsonReviewFile(path16.join(logDir, `${prefix}.${latestRun}.json`));
|
|
4896
4981
|
} else if (exts.has("log")) {
|
|
4897
|
-
failure = await parseLogFile(
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
5477
|
-
const toPath =
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
5773
|
+
async function fileExists3(path21) {
|
|
5689
5774
|
try {
|
|
5690
|
-
const stat = await fs22.stat(
|
|
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 =
|
|
5782
|
-
const workflowPath =
|
|
5783
|
-
const gauntletDir =
|
|
5784
|
-
const ciConfigPath =
|
|
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(
|
|
5920
|
+
async function fileExists4(path22) {
|
|
5836
5921
|
try {
|
|
5837
|
-
const stat = await fs23.stat(
|
|
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
|
|
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
|
|
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 =
|
|
6139
|
+
const gauntletPath = path22.join(rootDir, GAUNTLET_DIR3);
|
|
6055
6140
|
const existingCheckNames = new Set;
|
|
6056
6141
|
const existingReviewNames = new Set;
|
|
6057
|
-
const configPath =
|
|
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 =
|
|
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 =
|
|
6202
|
+
const filePath = path22.join(checksPath, file);
|
|
6118
6203
|
filesChecked.push(filePath);
|
|
6119
|
-
const name =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
6198
|
-
const reviewName =
|
|
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 =
|
|
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 =
|
|
6223
|
-
const reviewName =
|
|
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] ||
|
|
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(
|
|
6569
|
+
async function fileExists5(path23) {
|
|
6485
6570
|
try {
|
|
6486
|
-
const stat = await fs24.stat(
|
|
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(
|
|
6577
|
+
async function dirExists2(path23) {
|
|
6493
6578
|
try {
|
|
6494
|
-
const stat = await fs24.stat(
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
6727
|
+
var __dirname2 = path24.dirname(fileURLToPath(import.meta.url));
|
|
6644
6728
|
function readSkillTemplate(filename) {
|
|
6645
|
-
const templatePath =
|
|
6729
|
+
const templatePath = path24.join(__dirname2, "skill-templates", filename);
|
|
6646
6730
|
return readFileSync(templatePath, "utf-8");
|
|
6647
6731
|
}
|
|
6648
|
-
var
|
|
6649
|
-
|
|
6650
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
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
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
await
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
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
|
-
|
|
6851
|
-
|
|
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 =
|
|
6914
|
-
const statusScriptPath =
|
|
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 =
|
|
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
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
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 =
|
|
7018
|
-
const skillPath =
|
|
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 :
|
|
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 :
|
|
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 =
|
|
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 =
|
|
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 :
|
|
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 =
|
|
7100
|
+
const filePath = path24.join(commandDir, fileName);
|
|
7045
7101
|
if (await exists(filePath)) {
|
|
7046
|
-
const relPath2 = ctx.isUserLevel ? 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 :
|
|
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 :
|
|
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 :
|
|
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 =
|
|
7130
|
-
const settingsPath =
|
|
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 =
|
|
7162
|
-
const hooksPath =
|
|
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
|
|
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 =
|
|
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 ?
|
|
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=
|
|
8361
|
+
//# debugId=D6CA917DC551041A64756E2164756E21
|