agent-gauntlet 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -23
- package/dist/index.js +9226 -0
- package/dist/index.js.map +65 -0
- package/dist/scripts/status.js +280 -0
- package/dist/scripts/status.js.map +10 -0
- package/package.json +22 -8
- package/src/built-in-reviews/code-quality.md +0 -25
- package/src/built-in-reviews/index.ts +0 -28
- package/src/bun-plugins.d.ts +0 -4
- package/src/cli-adapters/claude.ts +0 -327
- package/src/cli-adapters/codex.ts +0 -290
- package/src/cli-adapters/cursor.ts +0 -128
- package/src/cli-adapters/gemini.ts +0 -510
- package/src/cli-adapters/github-copilot.ts +0 -141
- package/src/cli-adapters/index.ts +0 -250
- package/src/cli-adapters/thinking-budget.ts +0 -23
- package/src/commands/check.ts +0 -311
- package/src/commands/ci/index.ts +0 -15
- package/src/commands/ci/init.ts +0 -96
- package/src/commands/ci/list-jobs.ts +0 -90
- package/src/commands/clean.ts +0 -54
- package/src/commands/detect.ts +0 -173
- package/src/commands/health.ts +0 -169
- package/src/commands/help.ts +0 -34
- package/src/commands/index.ts +0 -13
- package/src/commands/init.ts +0 -1878
- package/src/commands/list.ts +0 -33
- package/src/commands/review.ts +0 -311
- package/src/commands/run.ts +0 -29
- package/src/commands/shared.ts +0 -267
- package/src/commands/stop-hook.ts +0 -567
- package/src/commands/validate.ts +0 -20
- package/src/commands/wait-ci.ts +0 -518
- package/src/config/ci-loader.ts +0 -33
- package/src/config/ci-schema.ts +0 -28
- package/src/config/global.ts +0 -87
- package/src/config/loader.ts +0 -301
- package/src/config/schema.ts +0 -165
- package/src/config/stop-hook-config.ts +0 -130
- package/src/config/types.ts +0 -65
- package/src/config/validator.ts +0 -592
- package/src/core/change-detector.ts +0 -137
- package/src/core/diff-stats.ts +0 -442
- package/src/core/entry-point.ts +0 -190
- package/src/core/job.ts +0 -96
- package/src/core/run-executor.ts +0 -621
- package/src/core/runner.ts +0 -290
- package/src/gates/check.ts +0 -118
- package/src/gates/resolve-check-command.ts +0 -21
- package/src/gates/result.ts +0 -54
- package/src/gates/review.ts +0 -1333
- package/src/hooks/adapters/claude-stop-hook.ts +0 -99
- package/src/hooks/adapters/cursor-stop-hook.ts +0 -122
- package/src/hooks/adapters/types.ts +0 -94
- package/src/hooks/stop-hook-handler.ts +0 -748
- package/src/index.ts +0 -47
- package/src/output/app-logger.ts +0 -214
- package/src/output/console-log.ts +0 -168
- package/src/output/console.ts +0 -359
- package/src/output/logger.ts +0 -126
- package/src/output/sinks/console-sink.ts +0 -59
- package/src/output/sinks/file-sink.ts +0 -110
- package/src/scripts/status.ts +0 -433
- package/src/templates/workflow.yml +0 -79
- package/src/types/gauntlet-status.ts +0 -79
- package/src/utils/debug-log.ts +0 -392
- package/src/utils/diff-parser.ts +0 -103
- package/src/utils/execution-state.ts +0 -472
- package/src/utils/log-parser.ts +0 -696
- package/src/utils/sanitizer.ts +0 -3
- package/src/utils/session-ref.ts +0 -91
|
@@ -1,567 +0,0 @@
|
|
|
1
|
-
import fsSync from "node:fs";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import type { Command } from "commander";
|
|
5
|
-
import { loadGlobalConfig } from "../config/global.js";
|
|
6
|
-
import { ClaudeStopHookAdapter } from "../hooks/adapters/claude-stop-hook.js";
|
|
7
|
-
import { CursorStopHookAdapter } from "../hooks/adapters/cursor-stop-hook.js";
|
|
8
|
-
import type {
|
|
9
|
-
StopHookAdapter,
|
|
10
|
-
StopHookResult,
|
|
11
|
-
} from "../hooks/adapters/types.js";
|
|
12
|
-
import {
|
|
13
|
-
getDebugLogConfig,
|
|
14
|
-
getLogDir,
|
|
15
|
-
getPushPRInstructions,
|
|
16
|
-
getStatusMessage,
|
|
17
|
-
getStopReasonInstructions,
|
|
18
|
-
StopHookHandler,
|
|
19
|
-
} from "../hooks/stop-hook-handler.js";
|
|
20
|
-
import {
|
|
21
|
-
getCategoryLogger,
|
|
22
|
-
initLogger,
|
|
23
|
-
resetLogger,
|
|
24
|
-
} from "../output/app-logger.js";
|
|
25
|
-
import {
|
|
26
|
-
type GauntletStatus,
|
|
27
|
-
isBlockingStatus,
|
|
28
|
-
} from "../types/gauntlet-status.js";
|
|
29
|
-
import { DebugLogger, mergeDebugLogConfig } from "../utils/debug-log.js";
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Timeout for reading stdin (in milliseconds).
|
|
33
|
-
* Claude Code sends JSON input immediately on hook invocation.
|
|
34
|
-
* The 5-second timeout is a safety net for edge cases where stdin is delayed.
|
|
35
|
-
*/
|
|
36
|
-
const STDIN_TIMEOUT_MS = 5000;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Environment variable to prevent stop-hook recursion in child Claude processes.
|
|
40
|
-
*
|
|
41
|
-
* **How it works:**
|
|
42
|
-
* When the gauntlet runs review gates, it spawns child Claude processes to analyze code.
|
|
43
|
-
* These child processes inherit environment variables. If a child Claude tries to stop,
|
|
44
|
-
* its stop hook would normally run the gauntlet again, potentially creating infinite
|
|
45
|
-
* recursion or redundant checks.
|
|
46
|
-
*
|
|
47
|
-
* **Where it's set:**
|
|
48
|
-
* - In `src/cli-adapters/claude.ts` when spawning Claude for review execution
|
|
49
|
-
* - Set to "1" in the spawn/exec environment: `{ [GAUNTLET_STOP_HOOK_ACTIVE_ENV]: "1" }`
|
|
50
|
-
*
|
|
51
|
-
* **Effect:**
|
|
52
|
-
* When this env var is set, stop-hooks exit immediately with "approve" decision,
|
|
53
|
-
* skipping all validation. This is safe because:
|
|
54
|
-
* 1. The parent gauntlet process is already running validation
|
|
55
|
-
* 2. Child processes are short-lived review executors, not user sessions
|
|
56
|
-
* 3. Debug logging is skipped to avoid polluting logs with child process entries
|
|
57
|
-
*/
|
|
58
|
-
export const GAUNTLET_STOP_HOOK_ACTIVE_ENV = "GAUNTLET_STOP_HOOK_ACTIVE";
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Marker file to detect nested stop-hook invocations.
|
|
62
|
-
*
|
|
63
|
-
* **Why this exists:**
|
|
64
|
-
* When the gauntlet spawns child Claude processes for code reviews, those child
|
|
65
|
-
* processes may trigger stop hooks when they exit. Claude Code does NOT pass
|
|
66
|
-
* environment variables to hooks, so GAUNTLET_STOP_HOOK_ACTIVE_ENV doesn't work.
|
|
67
|
-
*
|
|
68
|
-
* **How it works:**
|
|
69
|
-
* 1. Stop-hook creates this file (containing PID) before running the gauntlet
|
|
70
|
-
* 2. If another stop-hook fires during execution, it sees this file and fast-exits
|
|
71
|
-
* 3. Stop-hook removes this file when complete (success, failure, or error)
|
|
72
|
-
*
|
|
73
|
-
* This prevents nested stop-hooks from attempting to run concurrent gauntlets
|
|
74
|
-
* (which would hit lock_conflict anyway, but this is faster and quieter).
|
|
75
|
-
*/
|
|
76
|
-
const STOP_HOOK_MARKER_FILE = ".stop-hook-active";
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Hard ceiling for the stop hook process.
|
|
80
|
-
* If the process runs longer than this, it outputs an allow response and exits.
|
|
81
|
-
* This prevents zombie processes when Claude Code times out reading stdout
|
|
82
|
-
* but the process keeps running.
|
|
83
|
-
*/
|
|
84
|
-
const STOP_HOOK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Available adapters in detection order.
|
|
88
|
-
* Cursor adapter is checked first because it has a positive detection (cursor_version present).
|
|
89
|
-
* Claude adapter is the fallback (detected by absence of cursor_version).
|
|
90
|
-
*/
|
|
91
|
-
const adapters: StopHookAdapter[] = [
|
|
92
|
-
new CursorStopHookAdapter(),
|
|
93
|
-
new ClaudeStopHookAdapter(),
|
|
94
|
-
];
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Read hook input from stdin with a timeout.
|
|
98
|
-
*
|
|
99
|
-
* **Claude Code Hook Protocol:**
|
|
100
|
-
* Claude Code invokes stop hooks as shell commands and passes context via stdin
|
|
101
|
-
* as newline-terminated JSON. The input includes:
|
|
102
|
-
* - `cwd`: The project working directory (where Claude Code is running)
|
|
103
|
-
* - `stop_hook_active`: True if already inside a stop hook context (see below)
|
|
104
|
-
* - `session_id`, `transcript_path`: Session context (not currently used)
|
|
105
|
-
*
|
|
106
|
-
* **The `stop_hook_active` field (stdin):**
|
|
107
|
-
* This is set by Claude Code itself when invoking a stop hook while already inside
|
|
108
|
-
* a stop hook context. This is a second layer of infinite loop prevention (in addition
|
|
109
|
-
* to the GAUNTLET_STOP_HOOK_ACTIVE env var). If true, we allow stop immediately.
|
|
110
|
-
*
|
|
111
|
-
* **Timeout behavior:**
|
|
112
|
-
* This function reads stdin with a 5-second timeout to handle cases where:
|
|
113
|
-
* - Claude Code sends input quickly (normal case - resolves on newline)
|
|
114
|
-
* - No input is sent (timeout returns empty string, allowing stop)
|
|
115
|
-
* - stdin is already closed (returns immediately)
|
|
116
|
-
*
|
|
117
|
-
* The timeout ensures the stop hook doesn't hang indefinitely waiting for input.
|
|
118
|
-
*/
|
|
119
|
-
async function readStdin(): Promise<string> {
|
|
120
|
-
return new Promise((resolve) => {
|
|
121
|
-
let data = "";
|
|
122
|
-
let resolved = false;
|
|
123
|
-
|
|
124
|
-
const onEnd = () => cleanup(data.trim());
|
|
125
|
-
const onError = () => cleanup("");
|
|
126
|
-
|
|
127
|
-
const cleanup = (result: string) => {
|
|
128
|
-
if (!resolved) {
|
|
129
|
-
resolved = true;
|
|
130
|
-
clearTimeout(timeout);
|
|
131
|
-
process.stdin.removeListener("data", onData);
|
|
132
|
-
process.stdin.removeListener("end", onEnd);
|
|
133
|
-
process.stdin.removeListener("error", onError);
|
|
134
|
-
resolve(result);
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const timeout = setTimeout(() => {
|
|
139
|
-
cleanup(data.trim());
|
|
140
|
-
}, STDIN_TIMEOUT_MS);
|
|
141
|
-
|
|
142
|
-
const onData = (chunk: Buffer) => {
|
|
143
|
-
data += chunk.toString();
|
|
144
|
-
// Claude Code sends newline-terminated JSON
|
|
145
|
-
if (data.includes("\n")) {
|
|
146
|
-
cleanup(data.trim());
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
process.stdin.on("data", onData);
|
|
151
|
-
process.stdin.on("end", onEnd);
|
|
152
|
-
process.stdin.on("error", onError);
|
|
153
|
-
|
|
154
|
-
// Handle case where stdin is already closed or empty
|
|
155
|
-
if (process.stdin.readableEnded) {
|
|
156
|
-
cleanup(data.trim());
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Check if a file exists at the given path.
|
|
163
|
-
*/
|
|
164
|
-
async function fileExists(filePath: string): Promise<boolean> {
|
|
165
|
-
try {
|
|
166
|
-
await fs.stat(filePath);
|
|
167
|
-
return true;
|
|
168
|
-
} catch {
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Get a logger for stop-hook operations.
|
|
175
|
-
*/
|
|
176
|
-
function getStopHookLogger() {
|
|
177
|
-
return getCategoryLogger("stop-hook");
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Output a result using the given adapter's format.
|
|
182
|
-
*/
|
|
183
|
-
function outputResult(adapter: StopHookAdapter, result: StopHookResult): void {
|
|
184
|
-
console.log(adapter.formatOutput(result));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Create a simple result for early exit conditions.
|
|
189
|
-
*/
|
|
190
|
-
function createEarlyExitResult(
|
|
191
|
-
status: GauntletStatus,
|
|
192
|
-
options?: { intervalMinutes?: number; errorMessage?: string },
|
|
193
|
-
): StopHookResult {
|
|
194
|
-
return {
|
|
195
|
-
status,
|
|
196
|
-
shouldBlock: false,
|
|
197
|
-
message: getStatusMessage(status, options),
|
|
198
|
-
intervalMinutes: options?.intervalMinutes,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Output a hook response to stdout using Claude protocol format.
|
|
204
|
-
* This is the legacy API for backward compatibility.
|
|
205
|
-
* Uses the Claude Code hook protocol format:
|
|
206
|
-
* - decision: "block" | "approve" - whether to block or allow the stop
|
|
207
|
-
* - reason: string - when blocking, this becomes the prompt fed back to Claude automatically
|
|
208
|
-
* - stopReason: string - always displayed to user regardless of decision
|
|
209
|
-
* - status: machine-readable status code for transparency (unified GauntletStatus)
|
|
210
|
-
* - message: human-friendly explanation of the outcome
|
|
211
|
-
*/
|
|
212
|
-
export function outputHookResponse(
|
|
213
|
-
status: GauntletStatus,
|
|
214
|
-
options?: {
|
|
215
|
-
reason?: string;
|
|
216
|
-
intervalMinutes?: number;
|
|
217
|
-
errorMessage?: string;
|
|
218
|
-
},
|
|
219
|
-
): void {
|
|
220
|
-
const claudeAdapter = new ClaudeStopHookAdapter();
|
|
221
|
-
const shouldBlock = isBlockingStatus(status);
|
|
222
|
-
const message = getStatusMessage(status, {
|
|
223
|
-
intervalMinutes: options?.intervalMinutes,
|
|
224
|
-
errorMessage: options?.errorMessage,
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
const result: StopHookResult = {
|
|
228
|
-
status,
|
|
229
|
-
shouldBlock,
|
|
230
|
-
message,
|
|
231
|
-
instructions: options?.reason,
|
|
232
|
-
pushPRReason: status === "pr_push_required" ? options?.reason : undefined,
|
|
233
|
-
intervalMinutes: options?.intervalMinutes,
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
console.log(claudeAdapter.formatOutput(result));
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Export for testing
|
|
240
|
-
export { getStopReasonInstructions, getStatusMessage, getPushPRInstructions };
|
|
241
|
-
export type {
|
|
242
|
-
GauntletStatus as StopHookStatus,
|
|
243
|
-
StopHookResult as HookResponse,
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
// Re-export PRStatusResult from handler for backward compatibility
|
|
247
|
-
export type { PRStatusResult } from "../hooks/stop-hook-handler.js";
|
|
248
|
-
|
|
249
|
-
// Re-export checkPRStatus for testing
|
|
250
|
-
export { checkPRStatus } from "../hooks/stop-hook-handler.js";
|
|
251
|
-
|
|
252
|
-
export function registerStopHookCommand(program: Command): void {
|
|
253
|
-
program
|
|
254
|
-
.command("stop-hook")
|
|
255
|
-
.description("Claude Code stop hook - validates gauntlet completion")
|
|
256
|
-
.action(async () => {
|
|
257
|
-
// Default to Claude adapter for error handling before detection
|
|
258
|
-
let adapter: StopHookAdapter = adapters[1]!;
|
|
259
|
-
let debugLogger: DebugLogger | null = null;
|
|
260
|
-
let loggerInitialized = false;
|
|
261
|
-
let markerFilePath: string | null = null;
|
|
262
|
-
const log = getStopHookLogger();
|
|
263
|
-
|
|
264
|
-
// Self-timeout: kill this process if it runs too long.
|
|
265
|
-
// Claude Code may timeout reading stdout, but the process keeps running
|
|
266
|
-
// as a zombie holding the lock and marker file.
|
|
267
|
-
const selfTimeout = setTimeout(() => {
|
|
268
|
-
// Clean up marker file synchronously before exiting
|
|
269
|
-
if (markerFilePath) {
|
|
270
|
-
try {
|
|
271
|
-
fsSync.rmSync(markerFilePath, { force: true });
|
|
272
|
-
} catch {
|
|
273
|
-
// Best-effort cleanup
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
outputResult(
|
|
277
|
-
adapter,
|
|
278
|
-
createEarlyExitResult("error", {
|
|
279
|
-
errorMessage: "stop hook timed out",
|
|
280
|
-
}),
|
|
281
|
-
);
|
|
282
|
-
process.exit(0);
|
|
283
|
-
}, STOP_HOOK_TIMEOUT_MS);
|
|
284
|
-
selfTimeout.unref();
|
|
285
|
-
|
|
286
|
-
// Capture diagnostic info early for later logging
|
|
287
|
-
const diagnostics = {
|
|
288
|
-
pid: process.pid,
|
|
289
|
-
ppid: process.ppid,
|
|
290
|
-
envVarSet: !!process.env[GAUNTLET_STOP_HOOK_ACTIVE_ENV],
|
|
291
|
-
processCwd: process.cwd(),
|
|
292
|
-
rawStdin: "",
|
|
293
|
-
stdinSessionId: undefined as string | undefined,
|
|
294
|
-
stdinStopHookActive: undefined as boolean | undefined,
|
|
295
|
-
stdinCwd: undefined as string | undefined,
|
|
296
|
-
stdinHookEventName: undefined as string | undefined,
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
try {
|
|
300
|
-
// ============================================================
|
|
301
|
-
// FAST EXIT CHECKS (no stdin read, minimal logging)
|
|
302
|
-
// These checks allow quick exit without the 5-second stdin timeout
|
|
303
|
-
// ============================================================
|
|
304
|
-
|
|
305
|
-
// 1. Check env var FIRST - fast exit for child Claude processes
|
|
306
|
-
if (process.env[GAUNTLET_STOP_HOOK_ACTIVE_ENV]) {
|
|
307
|
-
outputResult(adapter, createEarlyExitResult("stop_hook_active"));
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// 2. Check if this is a gauntlet project BEFORE reading stdin
|
|
312
|
-
const quickConfigCheck = path.join(
|
|
313
|
-
process.cwd(),
|
|
314
|
-
".gauntlet",
|
|
315
|
-
"config.yml",
|
|
316
|
-
);
|
|
317
|
-
if (!(await fileExists(quickConfigCheck))) {
|
|
318
|
-
outputResult(adapter, createEarlyExitResult("no_config"));
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// ============================================================
|
|
323
|
-
// EARLY DEBUG LOGGER INIT (before marker/stdin checks)
|
|
324
|
-
// ============================================================
|
|
325
|
-
const earlyLogDir = path.join(
|
|
326
|
-
process.cwd(),
|
|
327
|
-
await getLogDir(process.cwd()),
|
|
328
|
-
);
|
|
329
|
-
try {
|
|
330
|
-
const globalConfig = await loadGlobalConfig();
|
|
331
|
-
const projectDebugLogConfig = await getDebugLogConfig(process.cwd());
|
|
332
|
-
const debugLogConfig = mergeDebugLogConfig(
|
|
333
|
-
projectDebugLogConfig,
|
|
334
|
-
globalConfig.debug_log,
|
|
335
|
-
);
|
|
336
|
-
debugLogger = new DebugLogger(earlyLogDir, debugLogConfig);
|
|
337
|
-
} catch (initErr: unknown) {
|
|
338
|
-
log.warn(
|
|
339
|
-
`Debug logger init failed: ${(initErr as { message?: string }).message ?? "unknown"}`,
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
await debugLogger?.logCommand("stop-hook", []);
|
|
344
|
-
|
|
345
|
-
// 3. Check marker file - fast exit for nested stop-hooks
|
|
346
|
-
const markerLogDir = await getLogDir(process.cwd());
|
|
347
|
-
const markerPath = path.join(
|
|
348
|
-
process.cwd(),
|
|
349
|
-
markerLogDir,
|
|
350
|
-
STOP_HOOK_MARKER_FILE,
|
|
351
|
-
);
|
|
352
|
-
if (await fileExists(markerPath)) {
|
|
353
|
-
const STALE_MARKER_MS = 10 * 60 * 1000;
|
|
354
|
-
try {
|
|
355
|
-
const stat = await fs.stat(markerPath);
|
|
356
|
-
const ageMs = Date.now() - stat.mtimeMs;
|
|
357
|
-
if (ageMs > STALE_MARKER_MS) {
|
|
358
|
-
await debugLogger?.logStopHookEarlyExit(
|
|
359
|
-
"marker_stale",
|
|
360
|
-
"proceeding",
|
|
361
|
-
`age=${Math.round(ageMs / 1000)}s threshold=${Math.round(STALE_MARKER_MS / 1000)}s`,
|
|
362
|
-
);
|
|
363
|
-
await fs.rm(markerPath, { force: true });
|
|
364
|
-
} else {
|
|
365
|
-
await debugLogger?.logStopHookEarlyExit(
|
|
366
|
-
"marker_fresh",
|
|
367
|
-
"stop_hook_active",
|
|
368
|
-
`age=${Math.round(ageMs / 1000)}s`,
|
|
369
|
-
);
|
|
370
|
-
outputResult(adapter, createEarlyExitResult("stop_hook_active"));
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
} catch (markerErr: unknown) {
|
|
374
|
-
const errMsg =
|
|
375
|
-
(markerErr as { message?: string }).message ?? "unknown";
|
|
376
|
-
await debugLogger?.logStopHookEarlyExit(
|
|
377
|
-
"marker_stat_error",
|
|
378
|
-
"stop_hook_active",
|
|
379
|
-
`error=${errMsg}`,
|
|
380
|
-
);
|
|
381
|
-
outputResult(adapter, createEarlyExitResult("stop_hook_active"));
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// ============================================================
|
|
387
|
-
// STDIN PARSING AND ADAPTER DETECTION
|
|
388
|
-
// ============================================================
|
|
389
|
-
|
|
390
|
-
const input = await readStdin();
|
|
391
|
-
diagnostics.rawStdin = input;
|
|
392
|
-
|
|
393
|
-
let parsed: Record<string, unknown> = {};
|
|
394
|
-
try {
|
|
395
|
-
if (input.trim()) {
|
|
396
|
-
parsed = JSON.parse(input);
|
|
397
|
-
// Capture parsed fields for diagnostics
|
|
398
|
-
diagnostics.stdinSessionId = parsed.session_id as
|
|
399
|
-
| string
|
|
400
|
-
| undefined;
|
|
401
|
-
diagnostics.stdinStopHookActive = parsed.stop_hook_active as
|
|
402
|
-
| boolean
|
|
403
|
-
| undefined;
|
|
404
|
-
diagnostics.stdinCwd = parsed.cwd as string | undefined;
|
|
405
|
-
diagnostics.stdinHookEventName = parsed.hook_event_name as
|
|
406
|
-
| string
|
|
407
|
-
| undefined;
|
|
408
|
-
}
|
|
409
|
-
} catch (parseErr: unknown) {
|
|
410
|
-
const errMsg =
|
|
411
|
-
(parseErr as { message?: string }).message ?? "unknown";
|
|
412
|
-
log.info(`Invalid hook input (${errMsg}), allowing stop`);
|
|
413
|
-
await debugLogger?.logStopHookEarlyExit(
|
|
414
|
-
"stdin_parse_error",
|
|
415
|
-
"invalid_input",
|
|
416
|
-
`error=${errMsg}`,
|
|
417
|
-
);
|
|
418
|
-
outputResult(adapter, createEarlyExitResult("invalid_input"));
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Detect protocol and select adapter
|
|
423
|
-
adapter = adapters.find((a) => a.detect(parsed)) ?? adapters[1]!;
|
|
424
|
-
|
|
425
|
-
// Parse input using selected adapter
|
|
426
|
-
const ctx = adapter.parseInput(parsed);
|
|
427
|
-
|
|
428
|
-
// Check for adapter-specific early exit (e.g., Cursor loop_count)
|
|
429
|
-
const skipResult = adapter.shouldSkipExecution(ctx);
|
|
430
|
-
if (skipResult) {
|
|
431
|
-
await debugLogger?.logStopHookEarlyExit(
|
|
432
|
-
"adapter_skip",
|
|
433
|
-
skipResult.status,
|
|
434
|
-
`adapter=${adapter.name}`,
|
|
435
|
-
);
|
|
436
|
-
outputResult(adapter, skipResult);
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// ============================================================
|
|
441
|
-
// GAUNTLET EXECUTION
|
|
442
|
-
// ============================================================
|
|
443
|
-
|
|
444
|
-
log.info("Starting gauntlet validation...");
|
|
445
|
-
|
|
446
|
-
// Re-check config if cwd differs from process.cwd()
|
|
447
|
-
const projectCwd = ctx.cwd;
|
|
448
|
-
if (ctx.cwd !== process.cwd()) {
|
|
449
|
-
const configPath = path.join(projectCwd, ".gauntlet", "config.yml");
|
|
450
|
-
if (!(await fileExists(configPath))) {
|
|
451
|
-
log.info("No gauntlet config found at hook cwd, allowing stop");
|
|
452
|
-
await debugLogger?.logStopHookEarlyExit(
|
|
453
|
-
"no_config_at_cwd",
|
|
454
|
-
"no_config",
|
|
455
|
-
`cwd=${projectCwd}`,
|
|
456
|
-
);
|
|
457
|
-
outputResult(adapter, createEarlyExitResult("no_config"));
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Get log directory from project config
|
|
463
|
-
const logDir = path.join(projectCwd, await getLogDir(projectCwd));
|
|
464
|
-
|
|
465
|
-
// Initialize app logger in stop-hook mode
|
|
466
|
-
await initLogger({
|
|
467
|
-
mode: "stop-hook",
|
|
468
|
-
logDir,
|
|
469
|
-
});
|
|
470
|
-
loggerInitialized = true;
|
|
471
|
-
|
|
472
|
-
// Re-init debug logger with the final logDir if cwd differed
|
|
473
|
-
if (logDir !== earlyLogDir) {
|
|
474
|
-
try {
|
|
475
|
-
const globalCfg = await loadGlobalConfig();
|
|
476
|
-
const projDbgCfg = await getDebugLogConfig(projectCwd);
|
|
477
|
-
const dbgCfg = mergeDebugLogConfig(projDbgCfg, globalCfg.debug_log);
|
|
478
|
-
debugLogger = new DebugLogger(logDir, dbgCfg);
|
|
479
|
-
} catch (reinitErr: unknown) {
|
|
480
|
-
log.warn(
|
|
481
|
-
`Debug logger re-init failed: ${(reinitErr as { message?: string }).message ?? "unknown"}`,
|
|
482
|
-
);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// Log diagnostic info
|
|
487
|
-
await debugLogger?.logStopHookDiagnostics(diagnostics);
|
|
488
|
-
|
|
489
|
-
// Create marker file to signal nested stop-hooks to fast-exit
|
|
490
|
-
markerFilePath = path.join(logDir, STOP_HOOK_MARKER_FILE);
|
|
491
|
-
try {
|
|
492
|
-
await fs.writeFile(markerFilePath, `${process.pid}`, "utf-8");
|
|
493
|
-
} catch (mkErr: unknown) {
|
|
494
|
-
const errMsg = (mkErr as { message?: string }).message ?? "unknown";
|
|
495
|
-
log.warn(`Failed to create marker file: ${errMsg}`);
|
|
496
|
-
markerFilePath = null;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// Execute handler (includes gauntlet run + post-gauntlet PR check)
|
|
500
|
-
log.info("Running gauntlet gates...");
|
|
501
|
-
const handler = new StopHookHandler(debugLogger ?? undefined);
|
|
502
|
-
handler.setLogDir(logDir); // Pass logDir for execution state refresh
|
|
503
|
-
let result: StopHookResult;
|
|
504
|
-
try {
|
|
505
|
-
result = await handler.execute(ctx);
|
|
506
|
-
} finally {
|
|
507
|
-
// Clean up marker file regardless of success/failure
|
|
508
|
-
if (markerFilePath) {
|
|
509
|
-
try {
|
|
510
|
-
await fs.rm(markerFilePath, { force: true });
|
|
511
|
-
} catch (rmErr: unknown) {
|
|
512
|
-
const errMsg =
|
|
513
|
-
(rmErr as { message?: string }).message ?? "unknown";
|
|
514
|
-
log.warn(`Failed to remove marker file: ${errMsg}`);
|
|
515
|
-
}
|
|
516
|
-
markerFilePath = null;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// Output result using adapter format
|
|
521
|
-
outputResult(adapter, result);
|
|
522
|
-
|
|
523
|
-
// Clean up logger
|
|
524
|
-
if (loggerInitialized) {
|
|
525
|
-
try {
|
|
526
|
-
await resetLogger();
|
|
527
|
-
} catch (resetErr: unknown) {
|
|
528
|
-
const resetMsg =
|
|
529
|
-
(resetErr as { message?: string }).message ?? "unknown";
|
|
530
|
-
log.warn(`Logger reset failed: ${resetMsg}`);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
} catch (error: unknown) {
|
|
534
|
-
// On any unexpected error, allow stop to avoid blocking indefinitely
|
|
535
|
-
const err = error as { message?: string };
|
|
536
|
-
const errorMessage = err.message || "unknown error";
|
|
537
|
-
log.error(`Stop hook error: ${errorMessage}`);
|
|
538
|
-
await debugLogger?.logStopHook("allow", `error: ${errorMessage}`);
|
|
539
|
-
outputResult(adapter, createEarlyExitResult("error", { errorMessage }));
|
|
540
|
-
|
|
541
|
-
// Clean up marker file if it was created
|
|
542
|
-
if (markerFilePath) {
|
|
543
|
-
try {
|
|
544
|
-
await fs.rm(markerFilePath, { force: true });
|
|
545
|
-
} catch (rmErr: unknown) {
|
|
546
|
-
const rmMsg = (rmErr as { message?: string }).message ?? "unknown";
|
|
547
|
-
log.warn(`Failed to remove marker file in error handler: ${rmMsg}`);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// Clean up logger
|
|
552
|
-
if (loggerInitialized) {
|
|
553
|
-
try {
|
|
554
|
-
await resetLogger();
|
|
555
|
-
} catch (resetErr: unknown) {
|
|
556
|
-
const resetMsg =
|
|
557
|
-
(resetErr as { message?: string }).message ?? "unknown";
|
|
558
|
-
process.stderr.write(
|
|
559
|
-
`stop-hook: logger reset failed: ${resetMsg}\n`,
|
|
560
|
-
);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
} finally {
|
|
564
|
-
clearTimeout(selfTimeout);
|
|
565
|
-
}
|
|
566
|
-
});
|
|
567
|
-
}
|
package/src/commands/validate.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import type { Command } from "commander";
|
|
3
|
-
import { loadConfig } from "../config/loader.js";
|
|
4
|
-
|
|
5
|
-
export function registerValidateCommand(program: Command): void {
|
|
6
|
-
program
|
|
7
|
-
.command("validate")
|
|
8
|
-
.description("Validate .gauntlet/ config files against schemas")
|
|
9
|
-
.action(async () => {
|
|
10
|
-
try {
|
|
11
|
-
await loadConfig();
|
|
12
|
-
console.log(chalk.green("All config files are valid."));
|
|
13
|
-
process.exitCode = 0;
|
|
14
|
-
} catch (error: unknown) {
|
|
15
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
16
|
-
console.error(chalk.red("Validation failed:"), message);
|
|
17
|
-
process.exitCode = 1;
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
}
|