agent-gauntlet 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +25 -23
  2. package/dist/index.js +9226 -0
  3. package/dist/index.js.map +65 -0
  4. package/dist/scripts/status.js +280 -0
  5. package/dist/scripts/status.js.map +10 -0
  6. package/package.json +22 -8
  7. package/src/built-in-reviews/code-quality.md +0 -25
  8. package/src/built-in-reviews/index.ts +0 -28
  9. package/src/bun-plugins.d.ts +0 -4
  10. package/src/cli-adapters/claude.ts +0 -327
  11. package/src/cli-adapters/codex.ts +0 -290
  12. package/src/cli-adapters/cursor.ts +0 -128
  13. package/src/cli-adapters/gemini.ts +0 -510
  14. package/src/cli-adapters/github-copilot.ts +0 -141
  15. package/src/cli-adapters/index.ts +0 -250
  16. package/src/cli-adapters/thinking-budget.ts +0 -23
  17. package/src/commands/check.ts +0 -311
  18. package/src/commands/ci/index.ts +0 -15
  19. package/src/commands/ci/init.ts +0 -96
  20. package/src/commands/ci/list-jobs.ts +0 -90
  21. package/src/commands/clean.ts +0 -54
  22. package/src/commands/detect.ts +0 -173
  23. package/src/commands/health.ts +0 -169
  24. package/src/commands/help.ts +0 -34
  25. package/src/commands/index.ts +0 -13
  26. package/src/commands/init.ts +0 -1878
  27. package/src/commands/list.ts +0 -33
  28. package/src/commands/review.ts +0 -311
  29. package/src/commands/run.ts +0 -29
  30. package/src/commands/shared.ts +0 -267
  31. package/src/commands/stop-hook.ts +0 -567
  32. package/src/commands/validate.ts +0 -20
  33. package/src/commands/wait-ci.ts +0 -518
  34. package/src/config/ci-loader.ts +0 -33
  35. package/src/config/ci-schema.ts +0 -28
  36. package/src/config/global.ts +0 -87
  37. package/src/config/loader.ts +0 -301
  38. package/src/config/schema.ts +0 -165
  39. package/src/config/stop-hook-config.ts +0 -130
  40. package/src/config/types.ts +0 -65
  41. package/src/config/validator.ts +0 -592
  42. package/src/core/change-detector.ts +0 -137
  43. package/src/core/diff-stats.ts +0 -442
  44. package/src/core/entry-point.ts +0 -190
  45. package/src/core/job.ts +0 -96
  46. package/src/core/run-executor.ts +0 -621
  47. package/src/core/runner.ts +0 -290
  48. package/src/gates/check.ts +0 -118
  49. package/src/gates/resolve-check-command.ts +0 -21
  50. package/src/gates/result.ts +0 -54
  51. package/src/gates/review.ts +0 -1333
  52. package/src/hooks/adapters/claude-stop-hook.ts +0 -99
  53. package/src/hooks/adapters/cursor-stop-hook.ts +0 -122
  54. package/src/hooks/adapters/types.ts +0 -94
  55. package/src/hooks/stop-hook-handler.ts +0 -748
  56. package/src/index.ts +0 -47
  57. package/src/output/app-logger.ts +0 -214
  58. package/src/output/console-log.ts +0 -168
  59. package/src/output/console.ts +0 -359
  60. package/src/output/logger.ts +0 -126
  61. package/src/output/sinks/console-sink.ts +0 -59
  62. package/src/output/sinks/file-sink.ts +0 -110
  63. package/src/scripts/status.ts +0 -433
  64. package/src/templates/workflow.yml +0 -79
  65. package/src/types/gauntlet-status.ts +0 -79
  66. package/src/utils/debug-log.ts +0 -392
  67. package/src/utils/diff-parser.ts +0 -103
  68. package/src/utils/execution-state.ts +0 -472
  69. package/src/utils/log-parser.ts +0 -696
  70. package/src/utils/sanitizer.ts +0 -3
  71. package/src/utils/session-ref.ts +0 -91
@@ -1,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
- }
@@ -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
- }