agent-gauntlet 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -299,7 +299,7 @@ import { Command } from "commander";
299
299
  // package.json
300
300
  var package_default = {
301
301
  name: "agent-gauntlet",
302
- version: "1.2.1",
302
+ version: "1.3.0",
303
303
  description: "A CLI tool for testing AI coding agents",
304
304
  license: "MIT",
305
305
  author: "Paul Caplan",
@@ -321,6 +321,9 @@ var package_default = {
321
321
  files: [
322
322
  "dist",
323
323
  "skills",
324
+ ".claude-plugin",
325
+ ".cursor-plugin",
326
+ "hooks",
324
327
  "README.md",
325
328
  "LICENSE"
326
329
  ],
@@ -1233,62 +1236,61 @@ ${stderr}`);
1233
1236
  jobId,
1234
1237
  status: "pass",
1235
1238
  duration: Date.now() - startTime,
1236
- message: "Command exited with code 0"
1239
+ message: "Command exited with code 0",
1240
+ command,
1241
+ workingDirectory
1237
1242
  };
1238
1243
  await logger(`Result: ${result.status} - ${result.message}
1239
1244
  `);
1240
1245
  return result;
1241
1246
  } catch (error) {
1242
- const err = error;
1243
- if (err.stdout)
1244
- await logger(err.stdout);
1245
- if (err.stderr)
1246
- await logger(`
1247
+ return this.handleExecutionError(error, jobId, command, workingDirectory, config, startTime, logger);
1248
+ }
1249
+ }
1250
+ async handleExecutionError(error, jobId, command, workingDirectory, config, startTime, logger) {
1251
+ const err = error;
1252
+ if (err.stdout)
1253
+ await logger(err.stdout);
1254
+ if (err.stderr)
1255
+ await logger(`
1247
1256
  STDERR:
1248
1257
  ${err.stderr}`);
1249
- await logger(`
1258
+ await logger(`
1250
1259
  Command failed: ${err.message}`);
1251
- if (err.signal === "SIGTERM" && config.timeout) {
1252
- const result2 = {
1253
- jobId,
1254
- status: "fail",
1255
- duration: Date.now() - startTime,
1256
- message: `Timed out after ${config.timeout}s`,
1257
- fixInstructions: config.fixInstructionsContent,
1258
- fixWithSkill: config.fixWithSkill
1259
- };
1260
- await logger(`Result: ${result2.status} - ${result2.message}
1260
+ const result = this.buildErrorResult(err, jobId, command, workingDirectory, config, startTime);
1261
+ await logger(`Result: ${result.status} - ${result.message}
1261
1262
  `);
1262
- await this.logFixInfo(config, logger);
1263
- return result2;
1264
- }
1265
- if (typeof err.code === "number") {
1266
- const result2 = {
1267
- jobId,
1268
- status: "fail",
1269
- duration: Date.now() - startTime,
1270
- message: `Exited with code ${err.code}`,
1271
- fixInstructions: config.fixInstructionsContent,
1272
- fixWithSkill: config.fixWithSkill
1273
- };
1274
- await logger(`Result: ${result2.status} - ${result2.message}
1275
- `);
1276
- await this.logFixInfo(config, logger);
1277
- return result2;
1278
- }
1279
- const result = {
1280
- jobId,
1281
- status: "error",
1282
- duration: Date.now() - startTime,
1283
- message: err.message || "Unknown error",
1284
- fixInstructions: config.fixInstructionsContent,
1285
- fixWithSkill: config.fixWithSkill
1263
+ await this.logFixInfo(config, logger);
1264
+ return result;
1265
+ }
1266
+ buildErrorResult(err, jobId, command, workingDirectory, config, startTime) {
1267
+ const base = {
1268
+ jobId,
1269
+ duration: Date.now() - startTime,
1270
+ command,
1271
+ workingDirectory,
1272
+ fixInstructions: config.fixInstructionsContent,
1273
+ fixWithSkill: config.fixWithSkill
1274
+ };
1275
+ if (err.signal === "SIGTERM" && config.timeout) {
1276
+ return {
1277
+ ...base,
1278
+ status: "fail",
1279
+ message: `Timed out after ${config.timeout}s`
1280
+ };
1281
+ }
1282
+ if (typeof err.code === "number") {
1283
+ return {
1284
+ ...base,
1285
+ status: "fail",
1286
+ message: `Exited with code ${err.code}`
1286
1287
  };
1287
- await logger(`Result: ${result.status} - ${result.message}
1288
- `);
1289
- await this.logFixInfo(config, logger);
1290
- return result;
1291
1288
  }
1289
+ return {
1290
+ ...base,
1291
+ status: "error",
1292
+ message: err.message || "Unknown error"
1293
+ };
1292
1294
  }
1293
1295
  async logFixInfo(config, logger) {
1294
1296
  if (config.fixInstructionsContent) {
@@ -2694,28 +2696,83 @@ async function handleStopHookAction(ctx) {
2694
2696
  await executeGauntlet(ctx, projectCwd, logDir, diagnostics, parsed);
2695
2697
  }
2696
2698
 
2697
- // src/cli-adapters/thinking-budget.ts
2698
- var CLAUDE_THINKING_TOKENS = {
2699
- off: 0,
2700
- low: 8000,
2701
- medium: 16000,
2702
- high: 31999
2703
- };
2704
- var CODEX_REASONING_EFFORT = {
2705
- off: "minimal",
2706
- low: "low",
2707
- medium: "medium",
2708
- high: "high"
2709
- };
2710
- var GEMINI_THINKING_BUDGET = {
2711
- off: 0,
2712
- low: 4096,
2713
- medium: 8192,
2714
- high: 24576
2715
- };
2699
+ // src/plugin/claude-cli.ts
2700
+ import { execFileSync } from "node:child_process";
2701
+ function runClaudeCommand(args) {
2702
+ try {
2703
+ const stdout = execFileSync("claude", args, {
2704
+ encoding: "utf-8",
2705
+ stdio: ["pipe", "pipe", "pipe"],
2706
+ timeout: 60000
2707
+ });
2708
+ return { success: true, stdout };
2709
+ } catch (error) {
2710
+ const err = error;
2711
+ let stderr = err.message;
2712
+ if (typeof err.stderr === "string") {
2713
+ stderr = err.stderr;
2714
+ } else if (err.stderr instanceof Buffer) {
2715
+ stderr = err.stderr.toString("utf-8");
2716
+ }
2717
+ return { success: false, stderr: stderr.trim() };
2718
+ }
2719
+ }
2720
+ function runClaudePluginCommand(args) {
2721
+ const result = runClaudeCommand(args);
2722
+ return {
2723
+ success: result.success,
2724
+ stderr: result.stderr
2725
+ };
2726
+ }
2727
+ async function addMarketplace() {
2728
+ return runClaudePluginCommand([
2729
+ "plugin",
2730
+ "marketplace",
2731
+ "add",
2732
+ "pcaplan/agent-gauntlet"
2733
+ ]);
2734
+ }
2735
+ async function installPlugin(scope) {
2736
+ return runClaudePluginCommand([
2737
+ "plugin",
2738
+ "install",
2739
+ "agent-gauntlet",
2740
+ "--scope",
2741
+ scope
2742
+ ]);
2743
+ }
2744
+ async function listPlugins() {
2745
+ const result = runClaudeCommand(["plugin", "list", "--json"]);
2746
+ if (!result.success) {
2747
+ throw new Error(result.stderr ?? "Failed to list Claude plugins");
2748
+ }
2749
+ try {
2750
+ const parsed = JSON.parse(result.stdout ?? "");
2751
+ if (Array.isArray(parsed)) {
2752
+ return parsed;
2753
+ }
2754
+ return parsed?.plugins ?? [];
2755
+ } catch {
2756
+ throw new Error("Failed to parse `claude plugin list --json` output");
2757
+ }
2758
+ }
2759
+ async function updateMarketplace() {
2760
+ return runClaudePluginCommand([
2761
+ "plugin",
2762
+ "marketplace",
2763
+ "update",
2764
+ "agent-gauntlet"
2765
+ ]);
2766
+ }
2767
+ async function updatePlugin() {
2768
+ return runClaudePluginCommand([
2769
+ "plugin",
2770
+ "update",
2771
+ "agent-gauntlet@pcaplan/agent-gauntlet"
2772
+ ]);
2773
+ }
2716
2774
 
2717
- // src/cli-adapters/claude.ts
2718
- var execAsync3 = promisify3(exec3);
2775
+ // src/cli-adapters/claude-otel.ts
2719
2776
  function countBraceChange(line) {
2720
2777
  const stripped = line.replace(/\\./g, "").replace(/"[^"]*"/g, "").replace(/'[^']*'/g, "");
2721
2778
  let depth = 0;
@@ -2914,6 +2971,29 @@ function buildOtelEnv() {
2914
2971
  }
2915
2972
  return env;
2916
2973
  }
2974
+
2975
+ // src/cli-adapters/thinking-budget.ts
2976
+ var CLAUDE_THINKING_TOKENS = {
2977
+ off: 0,
2978
+ low: 8000,
2979
+ medium: 16000,
2980
+ high: 31999
2981
+ };
2982
+ var CODEX_REASONING_EFFORT = {
2983
+ off: "minimal",
2984
+ low: "low",
2985
+ medium: "medium",
2986
+ high: "high"
2987
+ };
2988
+ var GEMINI_THINKING_BUDGET = {
2989
+ off: 0,
2990
+ low: 4096,
2991
+ medium: 8192,
2992
+ high: 24576
2993
+ };
2994
+
2995
+ // src/cli-adapters/claude.ts
2996
+ var execAsync3 = promisify3(exec3);
2917
2997
  var POST_PROCESS_BUFFER_MS = 30000;
2918
2998
 
2919
2999
  class ClaudeAdapter {
@@ -2961,6 +3041,44 @@ class ClaudeAdapter {
2961
3041
  supportsHooks() {
2962
3042
  return true;
2963
3043
  }
3044
+ async detectPlugin(projectRoot) {
3045
+ try {
3046
+ const entries = await listPlugins();
3047
+ const pluginEntries = entries.filter((entry) => {
3048
+ const e = entry;
3049
+ const name = e.name ?? e.id;
3050
+ return name === "agent-gauntlet" || typeof name === "string" && name.startsWith("agent-gauntlet@");
3051
+ });
3052
+ const resolved = path11.resolve(projectRoot);
3053
+ if (pluginEntries.some((entry) => {
3054
+ const e = entry;
3055
+ return e.scope === "project" && typeof e.projectPath === "string" && path11.resolve(e.projectPath) === resolved;
3056
+ })) {
3057
+ return "project";
3058
+ }
3059
+ if (pluginEntries.some((entry) => entry.scope === "user")) {
3060
+ return "user";
3061
+ }
3062
+ } catch {}
3063
+ return null;
3064
+ }
3065
+ async installPlugin(scope, _projectRoot) {
3066
+ const addResult = await addMarketplace();
3067
+ if (!addResult.success) {
3068
+ return { success: false, error: addResult.stderr };
3069
+ }
3070
+ const installResult = await installPlugin(scope);
3071
+ if (!installResult.success) {
3072
+ return { success: false, error: installResult.stderr };
3073
+ }
3074
+ return { success: true };
3075
+ }
3076
+ getManualInstallInstructions(scope) {
3077
+ return [
3078
+ "claude plugin marketplace add pcaplan/agent-gauntlet",
3079
+ `claude plugin install agent-gauntlet --scope ${scope}`
3080
+ ];
3081
+ }
2964
3082
  async execute(opts) {
2965
3083
  const totalTimeout = (opts.timeoutMs ?? 300000) + POST_PROCESS_BUFFER_MS;
2966
3084
  let timer;
@@ -3152,7 +3270,7 @@ class CodexAdapter {
3152
3270
  return path12.join(os3.homedir(), ".codex", "prompts");
3153
3271
  }
3154
3272
  getProjectSkillDir() {
3155
- return null;
3273
+ return ".agents/skills";
3156
3274
  }
3157
3275
  getUserSkillDir() {
3158
3276
  return null;
@@ -3230,9 +3348,11 @@ ${opts.diff}`;
3230
3348
 
3231
3349
  // src/cli-adapters/cursor.ts
3232
3350
  import { exec as exec5 } from "node:child_process";
3351
+ import { statSync } from "node:fs";
3233
3352
  import fs14 from "node:fs/promises";
3234
3353
  import os4 from "node:os";
3235
3354
  import path13 from "node:path";
3355
+ import { fileURLToPath } from "node:url";
3236
3356
  import { promisify as promisify5 } from "node:util";
3237
3357
 
3238
3358
  // src/cli-adapters/model-resolution.ts
@@ -3311,10 +3431,10 @@ class CursorAdapter {
3311
3431
  return null;
3312
3432
  }
3313
3433
  getProjectSkillDir() {
3314
- return null;
3434
+ return ".cursor/skills";
3315
3435
  }
3316
3436
  getUserSkillDir() {
3317
- return null;
3437
+ return path13.join(os4.homedir(), ".cursor", "skills");
3318
3438
  }
3319
3439
  getCommandExtension() {
3320
3440
  return ".md";
@@ -3397,6 +3517,79 @@ ${opts.diff}`;
3397
3517
  await cleanup();
3398
3518
  }
3399
3519
  }
3520
+ async detectPlugin(projectRoot) {
3521
+ const projectPluginPath = path13.join(projectRoot, ".cursor", "plugins", "agent-gauntlet", ".cursor-plugin", "plugin.json");
3522
+ try {
3523
+ await fs14.access(projectPluginPath);
3524
+ return "project";
3525
+ } catch {}
3526
+ const userPluginPath = path13.join(os4.homedir(), ".cursor", "plugins", "agent-gauntlet", ".cursor-plugin", "plugin.json");
3527
+ try {
3528
+ await fs14.access(userPluginPath);
3529
+ return "user";
3530
+ } catch {}
3531
+ return null;
3532
+ }
3533
+ async installPlugin(scope, projectRoot) {
3534
+ try {
3535
+ const baseDir = scope === "user" ? path13.join(os4.homedir(), ".cursor", "plugins", "agent-gauntlet") : path13.join(projectRoot ?? ".", ".cursor", "plugins", "agent-gauntlet");
3536
+ const packageRoot = this.findPackageRoot();
3537
+ await this.copyPluginAssets(packageRoot, baseDir);
3538
+ return { success: true };
3539
+ } catch (err) {
3540
+ return {
3541
+ success: false,
3542
+ error: err instanceof Error ? err.message : String(err)
3543
+ };
3544
+ }
3545
+ }
3546
+ async updatePlugin(scope, projectRoot) {
3547
+ return this.installPlugin(scope, projectRoot);
3548
+ }
3549
+ getManualInstallInstructions(scope) {
3550
+ const targetDir = scope === "user" ? "~/.cursor/plugins/agent-gauntlet/" : ".cursor/plugins/agent-gauntlet/";
3551
+ return [
3552
+ `Copy plugin files to ${targetDir}`,
3553
+ "Or install via /add-plugin in Cursor or at the Cursor marketplace"
3554
+ ];
3555
+ }
3556
+ findPackageRoot() {
3557
+ const moduleDir = path13.dirname(fileURLToPath(import.meta.url));
3558
+ const bundled = path13.join(moduleDir, "..");
3559
+ const dev = path13.join(moduleDir, "..", "..");
3560
+ try {
3561
+ statSync(path13.join(bundled, ".cursor-plugin", "plugin.json"));
3562
+ return bundled;
3563
+ } catch {
3564
+ return dev;
3565
+ }
3566
+ }
3567
+ async copyPluginAssets(packageRoot, targetDir) {
3568
+ await fs14.mkdir(targetDir, { recursive: true });
3569
+ const pluginSrc = path13.join(packageRoot, ".cursor-plugin");
3570
+ const pluginDest = path13.join(targetDir, ".cursor-plugin");
3571
+ await this.copyDirRecursive(pluginSrc, pluginDest);
3572
+ const skillsSrc = path13.join(packageRoot, "skills");
3573
+ const skillsDest = path13.join(targetDir, "skills");
3574
+ await this.copyDirRecursive(skillsSrc, skillsDest);
3575
+ const hooksSrc = path13.join(packageRoot, "hooks", "cursor-hooks.json");
3576
+ const hooksDest = path13.join(targetDir, "hooks");
3577
+ await fs14.mkdir(hooksDest, { recursive: true });
3578
+ await fs14.copyFile(hooksSrc, path13.join(hooksDest, "hooks.json"));
3579
+ }
3580
+ async copyDirRecursive(src, dest) {
3581
+ await fs14.mkdir(dest, { recursive: true });
3582
+ const entries = await fs14.readdir(src, { withFileTypes: true });
3583
+ for (const entry of entries) {
3584
+ const srcPath = path13.join(src, entry.name);
3585
+ const destPath = path13.join(dest, entry.name);
3586
+ if (entry.isDirectory()) {
3587
+ await this.copyDirRecursive(srcPath, destPath);
3588
+ } else {
3589
+ await fs14.copyFile(srcPath, destPath);
3590
+ }
3591
+ }
3592
+ }
3400
3593
  }
3401
3594
 
3402
3595
  // src/cli-adapters/gemini.ts
@@ -7465,7 +7658,7 @@ function registerDetectCommand(program) {
7465
7658
  const changes = await changeDetector.getChangedFiles();
7466
7659
  if (changes.length === 0) {
7467
7660
  console.log(chalk6.green("No changes detected."));
7468
- return;
7661
+ process.exit(2);
7469
7662
  }
7470
7663
  console.log(chalk6.dim(`Found ${changes.length} changed files:`));
7471
7664
  for (const file of changes) {
@@ -7476,7 +7669,7 @@ function registerDetectCommand(program) {
7476
7669
  const jobs = jobGen.generateJobs(entryPoints);
7477
7670
  if (jobs.length === 0) {
7478
7671
  console.log(chalk6.yellow("No applicable gates for these changes."));
7479
- return;
7672
+ process.exit(2);
7480
7673
  }
7481
7674
  console.log(chalk6.bold(`Would run ${jobs.length} gate(s):
7482
7675
  `));
@@ -8201,12 +8394,12 @@ function registerHelpCommand(program) {
8201
8394
  });
8202
8395
  }
8203
8396
  // src/commands/init.ts
8204
- import { execFileSync } from "node:child_process";
8205
- import { statSync } from "node:fs";
8397
+ import { execFileSync as execFileSync2 } from "node:child_process";
8398
+ import { statSync as statSync3 } from "node:fs";
8206
8399
  import fs32 from "node:fs/promises";
8207
- import path30 from "node:path";
8208
- import { fileURLToPath } from "node:url";
8209
- import chalk11 from "chalk";
8400
+ import path31 from "node:path";
8401
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
8402
+ import chalk12 from "chalk";
8210
8403
 
8211
8404
  // src/commands/init-checksums.ts
8212
8405
  import { createHash } from "node:crypto";
@@ -8222,25 +8415,6 @@ async function computeSkillChecksum(skillDir) {
8222
8415
  }
8223
8416
  return hash.digest("hex");
8224
8417
  }
8225
- function computeHookChecksum(entries) {
8226
- const gauntletEntries = entries.filter((entry) => isGauntletHookEntry(entry));
8227
- const hash = createHash("sha256");
8228
- hash.update(JSON.stringify(gauntletEntries));
8229
- return hash.digest("hex");
8230
- }
8231
- function computeExpectedHookChecksum(hookEntries) {
8232
- return computeHookChecksum(hookEntries);
8233
- }
8234
- function isGauntletHookEntry(entry) {
8235
- if (typeof entry.command === "string" && entry.command.startsWith("agent-gauntlet")) {
8236
- return true;
8237
- }
8238
- const nested = entry.hooks;
8239
- if (Array.isArray(nested)) {
8240
- return nested.some((h) => typeof h.command === "string" && h.command.startsWith("agent-gauntlet"));
8241
- }
8242
- return false;
8243
- }
8244
8418
  async function collectFiles(dir, baseDir) {
8245
8419
  const base = baseDir ?? dir;
8246
8420
  const results = [];
@@ -8257,19 +8431,44 @@ async function collectFiles(dir, baseDir) {
8257
8431
  return results;
8258
8432
  }
8259
8433
 
8260
- // src/commands/init-hooks.ts
8261
- import fs31 from "node:fs/promises";
8434
+ // src/commands/init-plugin.ts
8435
+ import os8 from "node:os";
8262
8436
  import path29 from "node:path";
8263
- import chalk10 from "chalk";
8437
+ import chalk9 from "chalk";
8438
+ function getCodexSkillsBaseDir(scope) {
8439
+ if (scope === "project") {
8440
+ return path29.join(".agents", "skills");
8441
+ }
8442
+ const homeDir = process.env.HOME?.trim() || os8.homedir();
8443
+ return path29.join(homeDir, ".agents", "skills");
8444
+ }
8445
+ async function installAdapterPlugin(adapter, projectRoot, installScope) {
8446
+ if (!adapter.installPlugin)
8447
+ return;
8448
+ const result = await adapter.installPlugin(installScope, projectRoot);
8449
+ if (!result.success) {
8450
+ console.warn(chalk9.yellow(`${adapter.name} plugin installation failed. Continuing init.`));
8451
+ if (result.error) {
8452
+ console.warn(chalk9.yellow(result.error.trim()));
8453
+ }
8454
+ const instructions = adapter.getManualInstallInstructions?.(installScope);
8455
+ if (instructions) {
8456
+ console.warn("Run these steps manually:");
8457
+ for (const step of instructions) {
8458
+ console.warn(` ${step}`);
8459
+ }
8460
+ }
8461
+ }
8462
+ }
8264
8463
 
8265
8464
  // src/commands/init-prompts.ts
8266
- import { checkbox, confirm, number } from "@inquirer/prompts";
8267
- import chalk9 from "chalk";
8465
+ import { checkbox, confirm, number, select } from "@inquirer/prompts";
8466
+ import chalk10 from "chalk";
8268
8467
  async function promptDevCLIs(detectedNames, skipPrompts) {
8269
8468
  if (skipPrompts)
8270
8469
  return detectedNames;
8271
8470
  console.log();
8272
- console.log(chalk9.bold("Select your development CLI(s). These are the main tools you work in."));
8471
+ console.log(chalk10.bold("Select your development CLI(s). These are the main tools you work in."));
8273
8472
  const selected = await checkbox({
8274
8473
  message: "Development CLIs:",
8275
8474
  choices: detectedNames.map((name) => ({ name, value: name })),
@@ -8281,7 +8480,7 @@ async function promptReviewCLIs(detectedNames, skipPrompts) {
8281
8480
  if (skipPrompts)
8282
8481
  return detectedNames;
8283
8482
  console.log();
8284
- console.log(chalk9.bold("Select your reviewer CLI(s). These are the CLIs that will be used for AI code reviews."));
8483
+ console.log(chalk10.bold("Select your reviewer CLI(s). These are the CLIs that will be used for AI code reviews."));
8285
8484
  const selected = await checkbox({
8286
8485
  message: "Review CLIs:",
8287
8486
  choices: detectedNames.map((name) => ({ name, value: name })),
@@ -8289,6 +8488,18 @@ async function promptReviewCLIs(detectedNames, skipPrompts) {
8289
8488
  });
8290
8489
  return selected;
8291
8490
  }
8491
+ async function promptInstallScope(skipPrompts) {
8492
+ if (skipPrompts)
8493
+ return "project";
8494
+ console.log();
8495
+ return select({
8496
+ message: "Install scope for Claude plugin and Codex skills:",
8497
+ choices: [
8498
+ { name: "Local (project)", value: "project" },
8499
+ { name: "Global (user)", value: "user" }
8500
+ ]
8501
+ });
8502
+ }
8292
8503
  async function promptNumReviews(reviewCliCount, skipPrompts) {
8293
8504
  if (reviewCliCount === 1)
8294
8505
  return 1;
@@ -8304,212 +8515,200 @@ async function promptNumReviews(reviewCliCount, skipPrompts) {
8304
8515
  }
8305
8516
  async function promptFileOverwrite(name, skipPrompts) {
8306
8517
  if (skipPrompts)
8307
- return true;
8308
- return confirm({
8518
+ return "yes";
8519
+ return select({
8309
8520
  message: `Skill \`${name}\` has changed, update it?`,
8310
- default: true
8311
- });
8312
- }
8313
- async function promptHookOverwrite(hookFile, skipPrompts) {
8314
- if (skipPrompts)
8315
- return true;
8316
- return confirm({
8317
- message: `Hook configuration in ${hookFile} has changed, update it?`,
8318
- default: true
8521
+ choices: [
8522
+ { name: "Yes", value: "yes" },
8523
+ { name: "No", value: "no" },
8524
+ { name: "Yes to all remaining", value: "all" }
8525
+ ]
8319
8526
  });
8320
8527
  }
8321
8528
 
8322
- // src/commands/init-hooks.ts
8323
- function hookHasCommand(entries, cmd) {
8324
- return entries.some((hook) => {
8325
- if (hook.command === cmd)
8326
- return true;
8327
- const nested = hook.hooks;
8328
- return Array.isArray(nested) && nested.some((h) => h.command === cmd);
8329
- });
8529
+ // src/commands/plugin-update.ts
8530
+ import { realpathSync, statSync as statSync2 } from "node:fs";
8531
+ import fs31 from "node:fs/promises";
8532
+ import os9 from "node:os";
8533
+ import path30 from "node:path";
8534
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
8535
+ import chalk11 from "chalk";
8536
+ var __dirname2 = path30.dirname(fileURLToPath2(import.meta.url));
8537
+ var SKILLS_SOURCE_DIR = (() => {
8538
+ const bundled = path30.join(__dirname2, "..", "skills");
8539
+ const dev = path30.join(__dirname2, "..", "..", "skills");
8540
+ try {
8541
+ statSync2(bundled);
8542
+ return bundled;
8543
+ } catch (err) {
8544
+ const code = err.code;
8545
+ if (code === "ENOENT" || code === "ENOTDIR")
8546
+ return dev;
8547
+ throw err;
8548
+ }
8549
+ })();
8550
+ async function getSkillDirNames() {
8551
+ const entries = await fs31.readdir(SKILLS_SOURCE_DIR, { withFileTypes: true });
8552
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
8330
8553
  }
8331
- async function mergeHookConfig(opts) {
8332
- const {
8333
- filePath,
8334
- hookKey,
8335
- hookEntry,
8336
- deduplicateCmd,
8337
- wrapInHooksArray,
8338
- baseConfig
8339
- } = opts;
8340
- await fs31.mkdir(path29.dirname(filePath), { recursive: true });
8341
- let existing = {};
8342
- if (await exists(filePath)) {
8343
- try {
8344
- existing = JSON.parse(await fs31.readFile(filePath, "utf-8"));
8345
- } catch {
8346
- existing = {};
8554
+ async function copyDirRecursive(opts) {
8555
+ await fs31.mkdir(opts.dest, { recursive: true });
8556
+ const entries = await fs31.readdir(opts.src, { withFileTypes: true });
8557
+ for (const entry of entries) {
8558
+ const srcPath = path30.join(opts.src, entry.name);
8559
+ const destPath = path30.join(opts.dest, entry.name);
8560
+ if (entry.isDirectory()) {
8561
+ await copyDirRecursive({ src: srcPath, dest: destPath });
8562
+ } else {
8563
+ await fs31.copyFile(srcPath, destPath);
8347
8564
  }
8348
8565
  }
8349
- const existingHooks = existing.hooks || {};
8350
- const existingEntries = Array.isArray(existingHooks[hookKey]) ? existingHooks[hookKey] : [];
8351
- if (hookHasCommand(existingEntries, deduplicateCmd)) {
8352
- return false;
8353
- }
8354
- const entryToAdd = wrapInHooksArray ? { hooks: [hookEntry] } : hookEntry;
8355
- const newEntries = [...existingEntries, entryToAdd];
8356
- const merged = {
8357
- ...baseConfig ?? {},
8358
- ...existing,
8359
- hooks: { ...existingHooks, [hookKey]: newEntries }
8360
- };
8361
- await fs31.writeFile(filePath, `${JSON.stringify(merged, null, 2)}
8362
- `);
8363
- return true;
8364
8566
  }
8365
- var START_HOOK_ENTRY = {
8366
- matcher: "startup|resume|clear|compact",
8367
- hooks: [
8368
- { type: "command", command: "agent-gauntlet start-hook", async: false }
8369
- ]
8370
- };
8371
- var CURSOR_START_HOOK_ENTRY = {
8372
- command: "agent-gauntlet start-hook --adapter cursor"
8373
- };
8374
- var STOP_HOOK_ENTRY = {
8375
- type: "command",
8376
- command: "agent-gauntlet stop-hook",
8377
- timeout: 300
8378
- };
8379
- var CURSOR_STOP_HOOK_ENTRY = {
8380
- command: "agent-gauntlet stop-hook",
8381
- loop_limit: 10
8382
- };
8383
- async function installHookWithLog(config, installedMsg, existsMsg) {
8384
- const added = await mergeHookConfig(config);
8385
- console.log(added ? chalk10.green(installedMsg) : chalk10.dim(existsMsg));
8386
- }
8387
- function buildHookSpec(target) {
8388
- const { projectRoot, variant, kind } = target;
8389
- const isCursor = variant === "cursor";
8390
- const isStop = kind === "stop";
8391
- const hookConfigs = {
8392
- "claude-stop": {
8393
- dir: ".claude",
8394
- file: "settings.local.json",
8395
- hookKey: "Stop",
8396
- entry: STOP_HOOK_ENTRY,
8397
- cmd: "agent-gauntlet stop-hook",
8398
- wrap: true
8399
- },
8400
- "cursor-stop": {
8401
- dir: ".cursor",
8402
- file: "hooks.json",
8403
- hookKey: "stop",
8404
- entry: CURSOR_STOP_HOOK_ENTRY,
8405
- cmd: "agent-gauntlet stop-hook",
8406
- wrap: false
8407
- },
8408
- "claude-start": {
8409
- dir: ".claude",
8410
- file: "settings.local.json",
8411
- hookKey: "SessionStart",
8412
- entry: START_HOOK_ENTRY,
8413
- cmd: "agent-gauntlet start-hook",
8414
- wrap: false
8415
- },
8416
- "cursor-start": {
8417
- dir: ".cursor",
8418
- file: "hooks.json",
8419
- hookKey: "sessionStart",
8420
- entry: CURSOR_START_HOOK_ENTRY,
8421
- cmd: "agent-gauntlet start-hook --adapter cursor",
8422
- wrap: false
8423
- }
8424
- };
8425
- const key = `${variant}-${kind}`;
8426
- const cfg = hookConfigs[key];
8427
- const prefix = isCursor ? "Cursor " : "";
8428
- let kindLabel;
8429
- if (isCursor)
8430
- kindLabel = kind;
8431
- else if (isStop)
8432
- kindLabel = "Stop";
8433
- else
8434
- kindLabel = "Start";
8435
- const purpose = isStop ? "gauntlet will run automatically when agent stops" : "agent will be primed with gauntlet instructions at session start";
8436
- return {
8437
- config: {
8438
- filePath: path29.join(projectRoot, cfg.dir, cfg.file),
8439
- hookKey: cfg.hookKey,
8440
- hookEntry: cfg.entry,
8441
- deduplicateCmd: cfg.cmd,
8442
- wrapInHooksArray: cfg.wrap,
8443
- ...isCursor ? { baseConfig: { version: 1 } } : {}
8444
- },
8445
- installedMsg: `${prefix}${kindLabel} hook installed - ${purpose}`,
8446
- existsMsg: `${prefix}${kindLabel} hook already installed`
8447
- };
8567
+ function isInProjectScope(cwd, projectPath) {
8568
+ const absoluteCwd = path30.resolve(cwd);
8569
+ const absoluteProjectPath = path30.resolve(projectPath);
8570
+ const normalizedCwd = normalizePathForMatch(absoluteCwd);
8571
+ const normalizedProjectPath = normalizePathForMatch(absoluteProjectPath);
8572
+ return normalizedCwd === normalizedProjectPath || normalizedCwd.startsWith(`${normalizedProjectPath}${path30.sep}`);
8448
8573
  }
8449
- async function installHookWithChecksums(target, skipPrompts) {
8450
- const spec = buildHookSpec(target);
8451
- let existingConfig = {};
8452
- if (await exists(spec.config.filePath)) {
8453
- try {
8454
- existingConfig = JSON.parse(await fs31.readFile(spec.config.filePath, "utf-8"));
8455
- } catch {
8456
- existingConfig = {};
8457
- }
8574
+ function normalizePathForMatch(inputPath) {
8575
+ try {
8576
+ return realpathSync.native(inputPath);
8577
+ } catch {
8578
+ return inputPath;
8458
8579
  }
8459
- const existingHooks = existingConfig.hooks || {};
8460
- const existingEntries = Array.isArray(existingHooks[spec.config.hookKey]) ? existingHooks[spec.config.hookKey] : [];
8461
- const gauntletEntries = existingEntries.filter((e) => isGauntletHookEntry(e));
8462
- if (gauntletEntries.length === 0) {
8463
- await installHookWithLog(spec.config, spec.installedMsg, spec.existsMsg);
8464
- return;
8580
+ }
8581
+ function detectInstalledScope(entries, cwd) {
8582
+ const pluginEntries = entries.filter((entry) => {
8583
+ const name = entry.name ?? entry.id;
8584
+ return name === "agent-gauntlet" || typeof name === "string" && name.startsWith("agent-gauntlet@");
8585
+ });
8586
+ const projectEntries = pluginEntries.filter((entry) => entry.scope === "project" && typeof entry.projectPath === "string" && isInProjectScope(cwd, entry.projectPath));
8587
+ if (projectEntries.length > 0) {
8588
+ return "project";
8465
8589
  }
8466
- const expectedEntry = spec.config.wrapInHooksArray ? { hooks: [spec.config.hookEntry] } : spec.config.hookEntry;
8467
- const expectedChecksum = computeExpectedHookChecksum([
8468
- expectedEntry
8469
- ]);
8470
- const actualChecksum = computeHookChecksum(existingEntries);
8471
- if (expectedChecksum === actualChecksum) {
8472
- console.log(chalk10.dim(spec.existsMsg));
8473
- return;
8590
+ const hasUserInstall = pluginEntries.some((entry) => entry.scope === "user");
8591
+ if (hasUserInstall) {
8592
+ return "user";
8474
8593
  }
8475
- const shouldOverwrite = await promptHookOverwrite(spec.config.filePath, skipPrompts);
8476
- if (!shouldOverwrite) {
8477
- console.log(chalk10.dim(spec.existsMsg));
8594
+ return null;
8595
+ }
8596
+ function printManualUpdateInstructions() {
8597
+ console.error("Run these commands manually:");
8598
+ console.error(" claude plugin marketplace update agent-gauntlet");
8599
+ console.error(" claude plugin update agent-gauntlet@pcaplan/agent-gauntlet");
8600
+ }
8601
+ async function refreshCodexSkills(cwd) {
8602
+ const localBase = path30.join(cwd, ".agents", "skills");
8603
+ const localMarker = path30.join(localBase, "gauntlet-run");
8604
+ const homeDir = process.env.HOME?.trim() || os9.homedir();
8605
+ const globalBase = path30.join(homeDir, ".agents", "skills");
8606
+ const globalMarker = path30.join(globalBase, "gauntlet-run");
8607
+ let targetBase = null;
8608
+ if (await exists(localMarker)) {
8609
+ targetBase = localBase;
8610
+ } else if (await exists(globalMarker)) {
8611
+ targetBase = globalBase;
8612
+ }
8613
+ if (!targetBase) {
8478
8614
  return;
8479
8615
  }
8480
- const nonGauntletEntries = existingEntries.filter((e) => !isGauntletHookEntry(e));
8481
- const entryToAdd = spec.config.wrapInHooksArray ? { hooks: [spec.config.hookEntry] } : spec.config.hookEntry;
8482
- const newEntries = [...nonGauntletEntries, entryToAdd];
8483
- const merged = {
8484
- ...spec.config.baseConfig ?? {},
8485
- ...existingConfig,
8486
- hooks: { ...existingHooks, [spec.config.hookKey]: newEntries }
8487
- };
8488
- await fs31.mkdir(path29.dirname(spec.config.filePath), { recursive: true });
8489
- await fs31.writeFile(spec.config.filePath, `${JSON.stringify(merged, null, 2)}
8490
- `);
8491
- console.log(chalk10.green(spec.installedMsg));
8492
- }
8493
- async function installHooksForAdapters(projectRoot, devAdapters, skipPrompts) {
8494
- for (const adapter of devAdapters) {
8495
- if (!adapter.supportsHooks())
8616
+ for (const dirName of await getSkillDirNames()) {
8617
+ const sourceDir = path30.join(SKILLS_SOURCE_DIR, dirName);
8618
+ const targetDir = path30.join(targetBase, dirName);
8619
+ if (!await exists(targetDir)) {
8620
+ await copyDirRecursive({ src: sourceDir, dest: targetDir });
8496
8621
  continue;
8497
- if (adapter.name !== "claude" && adapter.name !== "cursor")
8622
+ }
8623
+ const sourceChecksum = await computeSkillChecksum(sourceDir);
8624
+ const targetChecksum = await computeSkillChecksum(targetDir);
8625
+ if (sourceChecksum === targetChecksum) {
8498
8626
  continue;
8499
- for (const kind of ["stop", "start"]) {
8500
- const target = { projectRoot, variant: adapter.name, kind };
8501
- await installHookWithChecksums(target, skipPrompts);
8627
+ }
8628
+ await fs31.rm(targetDir, { recursive: true, force: true });
8629
+ await copyDirRecursive({ src: sourceDir, dest: targetDir });
8630
+ }
8631
+ }
8632
+ function isCliUnavailableError(err) {
8633
+ if (err != null && typeof err === "object") {
8634
+ const code = err.code;
8635
+ if (code === "ENOENT") {
8636
+ return true;
8502
8637
  }
8503
8638
  }
8639
+ const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();
8640
+ return msg.includes("command not found") || msg.includes("not installed");
8641
+ }
8642
+ async function detectClaudeScope(cwd) {
8643
+ try {
8644
+ const parsedPlugins = await listPlugins();
8645
+ return detectInstalledScope(parsedPlugins, cwd);
8646
+ } catch (error) {
8647
+ if (isCliUnavailableError(error)) {
8648
+ return null;
8649
+ }
8650
+ const message = error instanceof Error ? error.message : String(error);
8651
+ throw new Error(`Failed to detect Claude plugin installation: ${message}`);
8652
+ }
8653
+ }
8654
+ async function updateClaudePlugin(scope) {
8655
+ console.log(chalk11.cyan(`Updating agent-gauntlet Claude plugin (${scope} scope)...`));
8656
+ const marketplaceResult = await updateMarketplace();
8657
+ if (!marketplaceResult.success) {
8658
+ console.error(chalk11.red("Plugin update failed."));
8659
+ if (marketplaceResult.stderr) {
8660
+ console.error(chalk11.red(marketplaceResult.stderr.trim()));
8661
+ }
8662
+ printManualUpdateInstructions();
8663
+ throw new Error(marketplaceResult.stderr ?? "Failed to update plugin marketplace entry");
8664
+ }
8665
+ const pluginResult = await updatePlugin();
8666
+ if (!pluginResult.success) {
8667
+ console.error(chalk11.red("Plugin update failed."));
8668
+ if (pluginResult.stderr) {
8669
+ console.error(chalk11.red(pluginResult.stderr.trim()));
8670
+ }
8671
+ printManualUpdateInstructions();
8672
+ throw new Error(pluginResult.stderr ?? "Failed to update plugin");
8673
+ }
8674
+ }
8675
+ async function updateCursorPlugin(adapter, scope, cwd) {
8676
+ console.log(chalk11.cyan(`Updating agent-gauntlet Cursor plugin (${scope} scope)...`));
8677
+ const cursorResult = await adapter.updatePlugin(scope, cwd);
8678
+ if (cursorResult.success) {
8679
+ console.log(chalk11.green("Cursor plugin updated successfully."));
8680
+ console.log(chalk11.yellow("Restart any open Cursor sessions to use the updated plugin."));
8681
+ } else {
8682
+ console.error(chalk11.yellow(`Cursor plugin update failed: ${cursorResult.error ?? "unknown error"}`));
8683
+ }
8684
+ }
8685
+ async function runPluginUpdate(options) {
8686
+ options?.skipPrompts;
8687
+ const cwd = process.cwd();
8688
+ const claudeScope = await detectClaudeScope(cwd);
8689
+ const cursorAdapter = new CursorAdapter;
8690
+ const cursorScope = await cursorAdapter.detectPlugin(cwd);
8691
+ if (!(claudeScope || cursorScope)) {
8692
+ throw new Error("No agent-gauntlet plugin is installed for this project. Please run `agent-gauntlet init` first.");
8693
+ }
8694
+ if (claudeScope) {
8695
+ await updateClaudePlugin(claudeScope);
8696
+ }
8697
+ if (cursorScope) {
8698
+ await updateCursorPlugin(cursorAdapter, cursorScope, cwd);
8699
+ }
8700
+ await refreshCodexSkills(cwd);
8701
+ console.log(chalk11.green("Plugin update completed successfully."));
8702
+ console.log(chalk11.yellow("Restart any open agent sessions to use the updated plugin."));
8504
8703
  }
8505
8704
 
8506
8705
  // src/commands/init.ts
8507
- var __dirname2 = path30.dirname(fileURLToPath(import.meta.url));
8508
- var SKILLS_SOURCE_DIR = (() => {
8509
- const bundled = path30.join(__dirname2, "..", "skills");
8510
- const dev = path30.join(__dirname2, "..", "..", "skills");
8706
+ var __dirname3 = path31.dirname(fileURLToPath3(import.meta.url));
8707
+ var SKILLS_SOURCE_DIR2 = (() => {
8708
+ const bundled = path31.join(__dirname3, "..", "skills");
8709
+ const dev = path31.join(__dirname3, "..", "..", "skills");
8511
8710
  try {
8512
- statSync(bundled);
8711
+ statSync3(bundled);
8513
8712
  return bundled;
8514
8713
  } catch (err) {
8515
8714
  const code = err.code;
@@ -8518,8 +8717,8 @@ var SKILLS_SOURCE_DIR = (() => {
8518
8717
  throw err;
8519
8718
  }
8520
8719
  })();
8521
- async function getSkillDirNames() {
8522
- const entries = await fs32.readdir(SKILLS_SOURCE_DIR, { withFileTypes: true });
8720
+ async function getSkillDirNames2() {
8721
+ const entries = await fs32.readdir(SKILLS_SOURCE_DIR2, { withFileTypes: true });
8523
8722
  return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
8524
8723
  }
8525
8724
  var CLI_PREFERENCE_ORDER = [
@@ -8546,9 +8745,20 @@ function registerInitCommand(program) {
8546
8745
  await runInit(options);
8547
8746
  });
8548
8747
  }
8748
+ async function handleRerun(projectRoot, availableAdapters, skipPrompts) {
8749
+ try {
8750
+ await runPluginUpdate({ skipPrompts });
8751
+ } catch (error) {
8752
+ const message = error instanceof Error ? error.message : String(error);
8753
+ if (!message.includes("Claude plugin is not installed"))
8754
+ throw error;
8755
+ console.log(chalk12.yellow("Plugin not installed yet, running fresh install..."));
8756
+ await installExternalFiles(projectRoot, availableAdapters, skipPrompts);
8757
+ }
8758
+ }
8549
8759
  async function runInit(options) {
8550
8760
  const projectRoot = process.cwd();
8551
- const targetDir = path30.join(projectRoot, ".gauntlet");
8761
+ const targetDir = path31.join(projectRoot, ".gauntlet");
8552
8762
  const skipPrompts = options.yes ?? false;
8553
8763
  console.log("Detecting available CLI agents...");
8554
8764
  const availableAdapters = await detectAvailableCLIs();
@@ -8558,33 +8768,33 @@ async function runInit(options) {
8558
8768
  }
8559
8769
  const detectedNames = availableAdapters.map((a) => a.name);
8560
8770
  const gauntletExists = await exists(targetDir);
8561
- let hookAdapters;
8771
+ let devAdapters;
8562
8772
  let instructionCLINames;
8563
8773
  if (gauntletExists) {
8564
- console.log(chalk11.dim(".gauntlet/ already exists, skipping scaffolding"));
8565
- hookAdapters = availableAdapters;
8774
+ console.log(chalk12.dim(".gauntlet/ already exists, skipping scaffolding"));
8566
8775
  instructionCLINames = detectedNames;
8776
+ await handleRerun(projectRoot, availableAdapters, skipPrompts);
8567
8777
  } else {
8568
8778
  const devCLINames = await promptDevCLIs(detectedNames, skipPrompts);
8569
- hookAdapters = availableAdapters.filter((a) => devCLINames.includes(a.name));
8570
- for (const adapter of hookAdapters) {
8779
+ devAdapters = availableAdapters.filter((a) => devCLINames.includes(a.name));
8780
+ for (const adapter of devAdapters) {
8571
8781
  if (!adapter.supportsHooks()) {
8572
- console.log(chalk11.yellow(` ${adapter.name} doesn't support hooks yet, skipping hook installation`));
8782
+ console.log(chalk12.yellow(` ${adapter.name} doesn't support hooks yet, skipping hook installation`));
8573
8783
  }
8574
8784
  }
8575
8785
  const reviewCLINames = await promptReviewCLIs(detectedNames, skipPrompts);
8576
8786
  const numReviews = await promptNumReviews(reviewCLINames.length, skipPrompts);
8577
- console.log(chalk11.cyan("Agent Gauntlet's built-in code quality reviewer will be installed."));
8787
+ console.log(chalk12.cyan("Agent Gauntlet's built-in code quality reviewer will be installed."));
8578
8788
  await scaffoldGauntletDir(projectRoot, targetDir, reviewCLINames, numReviews);
8579
8789
  instructionCLINames = devCLINames;
8790
+ await installExternalFiles(projectRoot, devAdapters, skipPrompts);
8580
8791
  }
8581
- await installExternalFiles(projectRoot, hookAdapters, skipPrompts);
8582
8792
  await addToGitignore(projectRoot, "gauntlet_logs");
8583
8793
  await printPostInitInstructions(instructionCLINames);
8584
8794
  }
8585
8795
  function printNoCLIsMessage() {
8586
8796
  console.log();
8587
- console.log(chalk11.red("Error: No CLI agents found. Install at least one:"));
8797
+ console.log(chalk12.red("Error: No CLI agents found. Install at least one:"));
8588
8798
  console.log(" - Claude: https://docs.anthropic.com/en/docs/claude-code");
8589
8799
  console.log(" - Gemini: https://github.com/google-gemini/gemini-cli");
8590
8800
  console.log(" - Codex: https://github.com/openai/codex");
@@ -8592,71 +8802,127 @@ function printNoCLIsMessage() {
8592
8802
  }
8593
8803
  async function scaffoldGauntletDir(_projectRoot, targetDir, reviewCLINames, numReviews) {
8594
8804
  if (await exists(targetDir)) {
8595
- console.log(chalk11.dim(".gauntlet/ already exists, skipping scaffolding"));
8805
+ console.log(chalk12.dim(".gauntlet/ already exists, skipping scaffolding"));
8596
8806
  return;
8597
8807
  }
8598
8808
  await fs32.mkdir(targetDir);
8599
- await fs32.mkdir(path30.join(targetDir, "checks"));
8600
- await fs32.mkdir(path30.join(targetDir, "reviews"));
8809
+ await fs32.mkdir(path31.join(targetDir, "checks"));
8810
+ await fs32.mkdir(path31.join(targetDir, "reviews"));
8601
8811
  await writeConfigYml(targetDir, reviewCLINames);
8602
- await fs32.writeFile(path30.join(targetDir, "reviews", "code-quality.yml"), `builtin: code-quality
8812
+ await fs32.writeFile(path31.join(targetDir, "reviews", "code-quality.yml"), `builtin: code-quality
8603
8813
  num_reviews: ${numReviews}
8604
8814
  `);
8605
- console.log(chalk11.green("Created .gauntlet/reviews/code-quality.yml"));
8815
+ console.log(chalk12.green("Created .gauntlet/reviews/code-quality.yml"));
8606
8816
  }
8607
- async function copyDirRecursive(opts) {
8817
+ async function copyDirRecursive2(opts) {
8608
8818
  await fs32.mkdir(opts.dest, { recursive: true });
8609
8819
  const entries = await fs32.readdir(opts.src, { withFileTypes: true });
8610
8820
  for (const entry of entries) {
8611
- const srcPath = path30.join(opts.src, entry.name);
8612
- const destPath = path30.join(opts.dest, entry.name);
8821
+ const srcPath = path31.join(opts.src, entry.name);
8822
+ const destPath = path31.join(opts.dest, entry.name);
8613
8823
  if (entry.isDirectory()) {
8614
- await copyDirRecursive({ src: srcPath, dest: destPath });
8824
+ await copyDirRecursive2({ src: srcPath, dest: destPath });
8615
8825
  } else {
8616
8826
  await fs32.copyFile(srcPath, destPath);
8617
8827
  }
8618
8828
  }
8619
8829
  }
8620
- async function installSkillsWithChecksums(projectRoot, skipPrompts) {
8621
- const skillsDir = path30.join(projectRoot, ".claude", "skills");
8622
- for (const dirName of await getSkillDirNames()) {
8623
- const sourceDir = path30.join(SKILLS_SOURCE_DIR, dirName);
8624
- const targetDir = path30.join(skillsDir, dirName);
8625
- const relativeDir = `${path30.relative(projectRoot, targetDir)}/`;
8830
+ async function installSkillsWithChecksums(projectRoot, targetBaseDir, skipPrompts, updateAllState) {
8831
+ const skillsDir = path31.isAbsolute(targetBaseDir) ? targetBaseDir : path31.join(projectRoot, targetBaseDir);
8832
+ for (const dirName of await getSkillDirNames2()) {
8833
+ const sourceDir = path31.join(SKILLS_SOURCE_DIR2, dirName);
8834
+ const targetDir = path31.join(skillsDir, dirName);
8835
+ const relativeDir = `${path31.relative(projectRoot, targetDir)}/`;
8626
8836
  if (!await exists(targetDir)) {
8627
- await copyDirRecursive({ src: sourceDir, dest: targetDir });
8628
- console.log(chalk11.green(`Created ${relativeDir}`));
8837
+ await copyDirRecursive2({ src: sourceDir, dest: targetDir });
8838
+ console.log(chalk12.green(`Created ${relativeDir}`));
8629
8839
  continue;
8630
8840
  }
8631
8841
  const sourceChecksum = await computeSkillChecksum(sourceDir);
8632
8842
  const targetChecksum = await computeSkillChecksum(targetDir);
8633
8843
  if (sourceChecksum === targetChecksum)
8634
8844
  continue;
8635
- const shouldOverwrite = await promptFileOverwrite(dirName, skipPrompts);
8636
- if (!shouldOverwrite)
8845
+ let choice;
8846
+ if (skipPrompts || updateAllState.updateAll) {
8847
+ choice = "yes";
8848
+ } else {
8849
+ choice = await promptFileOverwrite(dirName, skipPrompts);
8850
+ if (choice === "all") {
8851
+ updateAllState.updateAll = true;
8852
+ }
8853
+ }
8854
+ if (choice === "no")
8637
8855
  continue;
8638
8856
  await fs32.rm(targetDir, { recursive: true, force: true });
8639
- await copyDirRecursive({ src: sourceDir, dest: targetDir });
8640
- console.log(chalk11.green(`Updated ${relativeDir}`));
8857
+ await copyDirRecursive2({ src: sourceDir, dest: targetDir });
8858
+ console.log(chalk12.green(`Updated ${relativeDir}`));
8859
+ }
8860
+ }
8861
+ async function detectAdaptersNeedingInstall(devAdapters, projectRoot) {
8862
+ const result = [];
8863
+ for (const adapter of devAdapters) {
8864
+ if (!adapter.installPlugin)
8865
+ continue;
8866
+ if (adapter.detectPlugin) {
8867
+ const existingScope = await adapter.detectPlugin(projectRoot);
8868
+ if (existingScope) {
8869
+ console.log(chalk12.dim(`${adapter.name} plugin already installed at ${existingScope} scope, skipping install`));
8870
+ continue;
8871
+ }
8872
+ }
8873
+ result.push(adapter);
8874
+ }
8875
+ return result;
8876
+ }
8877
+ async function installOtherAdapterSkills(projectRoot, devAdapters, skipPrompts, updateAllState) {
8878
+ const seen = new Set;
8879
+ for (const adapter of devAdapters) {
8880
+ if (adapter.name === "claude" || adapter.name === "codex")
8881
+ continue;
8882
+ const dir = adapter.getProjectSkillDir();
8883
+ if (dir && !seen.has(dir)) {
8884
+ seen.add(dir);
8885
+ await installSkillsWithChecksums(projectRoot, dir, skipPrompts, updateAllState);
8886
+ }
8641
8887
  }
8642
8888
  }
8643
8889
  async function installExternalFiles(projectRoot, devAdapters, skipPrompts) {
8644
- await installSkillsWithChecksums(projectRoot, skipPrompts);
8645
- await installHooksForAdapters(projectRoot, devAdapters, skipPrompts);
8890
+ const updateAllState = { updateAll: false };
8891
+ const devAdapterNames = new Set(devAdapters.map((adapter) => adapter.name));
8892
+ const adaptersNeedingInstall = await detectAdaptersNeedingInstall(devAdapters, projectRoot);
8893
+ const needsScope = adaptersNeedingInstall.length > 0 || devAdapterNames.has("codex");
8894
+ const installScope = needsScope ? await promptInstallScope(skipPrompts) : "project";
8895
+ for (const adapter of adaptersNeedingInstall) {
8896
+ await installAdapterPlugin(adapter, projectRoot, installScope);
8897
+ }
8898
+ if (devAdapterNames.has("codex")) {
8899
+ const codexBaseDir = getCodexSkillsBaseDir(installScope);
8900
+ await installSkillsWithChecksums(projectRoot, codexBaseDir, skipPrompts, updateAllState);
8901
+ }
8902
+ await installOtherAdapterSkills(projectRoot, devAdapters, skipPrompts, updateAllState);
8646
8903
  }
8647
8904
  async function printPostInitInstructions(devCLINames) {
8648
8905
  const hasNative = devCLINames.some((name) => NATIVE_CLIS.has(name));
8649
- const nonNativeNames = devCLINames.filter((name) => !NATIVE_CLIS.has(name));
8650
- const hasNonNative = nonNativeNames.length > 0;
8906
+ const hasCodex = devCLINames.includes("codex");
8907
+ const otherNonNativeNames = devCLINames.filter((name) => !NATIVE_CLIS.has(name) && name !== "codex");
8908
+ const hasOtherNonNative = otherNonNativeNames.length > 0;
8651
8909
  console.log();
8652
8910
  if (hasNative) {
8653
- console.log(chalk11.bold("To complete setup, run /gauntlet-setup in your CLI. This will guide you through configuring the static checks (unit tests, linters, etc.) that Agent Gauntlet will run."));
8911
+ console.log(chalk12.bold("To complete setup, run /gauntlet-setup in your CLI. This will guide you through configuring the static checks (unit tests, linters, etc.) that Agent Gauntlet will run."));
8654
8912
  }
8655
- if (hasNonNative) {
8656
- console.log(chalk11.bold("To complete setup, reference the setup skill in your CLI: @.claude/skills/gauntlet-setup/SKILL.md. This will guide you through configuring the static checks (unit tests, linters, etc.) that Agent Gauntlet will run."));
8913
+ if (hasCodex) {
8914
+ console.log(chalk12.bold("To complete setup in Codex, reference the setup skill: .agents/skills/gauntlet-setup/SKILL.md. This will guide you through configuring the static checks (unit tests, linters, etc.) that Agent Gauntlet will run."));
8915
+ console.log();
8916
+ console.log("Available Codex skills:");
8917
+ for (const dirName of await getSkillDirNames2()) {
8918
+ console.log(` .agents/skills/${dirName}/SKILL.md`);
8919
+ }
8920
+ }
8921
+ if (hasOtherNonNative) {
8922
+ console.log(chalk12.bold("To complete setup, reference the setup skill in your CLI: @.claude/skills/gauntlet-setup/SKILL.md. This will guide you through configuring the static checks (unit tests, linters, etc.) that Agent Gauntlet will run."));
8657
8923
  console.log();
8658
8924
  console.log("Available skills:");
8659
- for (const dirName of await getSkillDirNames()) {
8925
+ for (const dirName of await getSkillDirNames2()) {
8660
8926
  console.log(` @.claude/skills/${dirName}/SKILL.md`);
8661
8927
  }
8662
8928
  }
@@ -8719,11 +8985,11 @@ entry_points: []
8719
8985
  # enabled: true
8720
8986
  # format: text # Options: text, json
8721
8987
  `;
8722
- await fs32.writeFile(path30.join(targetDir, "config.yml"), content);
8723
- console.log(chalk11.green("Created .gauntlet/config.yml"));
8988
+ await fs32.writeFile(path31.join(targetDir, "config.yml"), content);
8989
+ console.log(chalk12.green("Created .gauntlet/config.yml"));
8724
8990
  }
8725
8991
  async function addToGitignore(projectRoot, entry) {
8726
- const gitignorePath = path30.join(projectRoot, ".gitignore");
8992
+ const gitignorePath = path31.join(projectRoot, ".gitignore");
8727
8993
  let content = "";
8728
8994
  if (await exists(gitignorePath)) {
8729
8995
  content = await fs32.readFile(gitignorePath, "utf-8");
@@ -8737,11 +9003,11 @@ async function addToGitignore(projectRoot, entry) {
8737
9003
  ` : "";
8738
9004
  await fs32.appendFile(gitignorePath, `${suffix}${entry}
8739
9005
  `);
8740
- console.log(chalk11.green(`Added ${entry} to .gitignore`));
9006
+ console.log(chalk12.green(`Added ${entry} to .gitignore`));
8741
9007
  }
8742
9008
  function gitSilent(args, opts) {
8743
9009
  try {
8744
- return execFileSync("git", args, {
9010
+ return execFileSync2("git", args, {
8745
9011
  encoding: "utf-8",
8746
9012
  timeout: opts?.timeout,
8747
9013
  stdio: ["pipe", "pipe", "ignore"]
@@ -8789,30 +9055,30 @@ async function detectAvailableCLIs() {
8789
9055
  for (const adapter of allAdapters) {
8790
9056
  const isAvailable = await adapter.isAvailable();
8791
9057
  if (isAvailable) {
8792
- console.log(chalk11.green(` ✓ ${adapter.name}`));
9058
+ console.log(chalk12.green(` ✓ ${adapter.name}`));
8793
9059
  available.push(adapter);
8794
9060
  } else {
8795
- console.log(chalk11.dim(` ✗ ${adapter.name} (not installed)`));
9061
+ console.log(chalk12.dim(` ✗ ${adapter.name} (not installed)`));
8796
9062
  }
8797
9063
  }
8798
9064
  return available;
8799
9065
  }
8800
9066
  // src/commands/list.ts
8801
- import chalk12 from "chalk";
9067
+ import chalk13 from "chalk";
8802
9068
  function registerListCommand(program) {
8803
9069
  program.command("list").description("List configured gates").action(async () => {
8804
9070
  try {
8805
9071
  const config = await loadConfig();
8806
- console.log(chalk12.bold("Check Gates:"));
9072
+ console.log(chalk13.bold("Check Gates:"));
8807
9073
  for (const c of Object.values(config.checks)) {
8808
9074
  console.log(` - ${c.name}`);
8809
9075
  }
8810
- console.log(chalk12.bold(`
9076
+ console.log(chalk13.bold(`
8811
9077
  Review Gates:`));
8812
9078
  for (const r of Object.values(config.reviews)) {
8813
9079
  console.log(` - ${r.name} (Tools: ${r.cli_preference?.join(", ")})`);
8814
9080
  }
8815
- console.log(chalk12.bold(`
9081
+ console.log(chalk13.bold(`
8816
9082
  Entry Points:`));
8817
9083
  for (const ep of config.project.entry_points) {
8818
9084
  console.log(` - ${ep.path}`);
@@ -8823,7 +9089,7 @@ Entry Points:`));
8823
9089
  }
8824
9090
  } catch (error) {
8825
9091
  const err = error;
8826
- console.error(chalk12.red("Error:"), err.message);
9092
+ console.error(chalk13.red("Error:"), err.message);
8827
9093
  }
8828
9094
  });
8829
9095
  }
@@ -8836,7 +9102,7 @@ function registerReviewCommand(program) {
8836
9102
  }
8837
9103
  // src/scripts/review-audit.ts
8838
9104
  import fs33 from "node:fs";
8839
- import path31 from "node:path";
9105
+ import path32 from "node:path";
8840
9106
  import readline from "node:readline";
8841
9107
  function parseKeyValue2(text) {
8842
9108
  const result = {};
@@ -8863,7 +9129,7 @@ var parseDuration = (d) => {
8863
9129
  };
8864
9130
  function getLogDir3(cwd) {
8865
9131
  try {
8866
- const cfg = path31.join(cwd, ".gauntlet", "config.yml");
9132
+ const cfg = path32.join(cwd, ".gauntlet", "config.yml");
8867
9133
  const m = fs33.readFileSync(cfg, "utf-8").match(/^log_dir:\s*(.+)$/m);
8868
9134
  if (m?.[1])
8869
9135
  return m[1].trim().replace(/^["']|["']$/g, "");
@@ -9170,7 +9436,7 @@ async function main2(date, since) {
9170
9436
  process.exit(1);
9171
9437
  }
9172
9438
  const logDir = getLogDir3(cwd);
9173
- const debugLogPath = path31.join(cwd, logDir, ".debug.log");
9439
+ const debugLogPath = path32.join(cwd, logDir, ".debug.log");
9174
9440
  if (!fs33.existsSync(debugLogPath)) {
9175
9441
  console.log(`No debug log found. (looked in ${logDir}/.debug.log)`);
9176
9442
  process.exit(0);
@@ -9195,9 +9461,149 @@ function registerReviewAuditCommand(program) {
9195
9461
  await main2(opts.date, opts.since);
9196
9462
  });
9197
9463
  }
9198
- // src/core/run-executor-lock.ts
9464
+ // src/core/run-executor.ts
9465
+ import fs37 from "node:fs/promises";
9466
+ import path36 from "node:path";
9467
+
9468
+ // src/output/report.ts
9199
9469
  import fs34 from "node:fs/promises";
9200
- import path32 from "node:path";
9470
+ import path33 from "node:path";
9471
+ function parseReviewJsonFilename(filename) {
9472
+ const m = filename.match(/^(.+)_([^@]+)@(\d+)\.\d+\.json$/);
9473
+ if (!(m?.[1] && m[2] && m[3]))
9474
+ return null;
9475
+ return {
9476
+ jobId: m[1],
9477
+ adapter: m[2],
9478
+ reviewIndex: parseInt(m[3], 10)
9479
+ };
9480
+ }
9481
+ async function collectViolationsFromFile(jsonPath, filename, startId) {
9482
+ const content = await fs34.readFile(jsonPath, "utf-8");
9483
+ const data = JSON.parse(content);
9484
+ if (!(data.violations && Array.isArray(data.violations)))
9485
+ return [];
9486
+ const parsed = parseReviewJsonFilename(filename);
9487
+ if (!parsed)
9488
+ return [];
9489
+ const violations = [];
9490
+ let nextId = startId;
9491
+ for (let i = 0;i < data.violations.length; i++) {
9492
+ const v = data.violations[i];
9493
+ if (!v)
9494
+ continue;
9495
+ const status = v.status || "new";
9496
+ if (status !== "new")
9497
+ continue;
9498
+ violations.push({
9499
+ id: nextId++,
9500
+ gateLabel: parsed.jobId,
9501
+ adapterSuffix: `${data.adapter}@${parsed.reviewIndex}`,
9502
+ file: v.file,
9503
+ line: v.line,
9504
+ issue: v.issue,
9505
+ fix: v.fix,
9506
+ priority: v.priority,
9507
+ jsonPath,
9508
+ violationIndex: i
9509
+ });
9510
+ }
9511
+ return violations;
9512
+ }
9513
+ async function enumerateNewViolations(logDir) {
9514
+ let files;
9515
+ try {
9516
+ files = await fs34.readdir(logDir);
9517
+ } catch (err) {
9518
+ const code = err.code;
9519
+ if (code === "ENOENT")
9520
+ return [];
9521
+ throw new Error(`Failed to read log directory ${logDir}: ${err}`);
9522
+ }
9523
+ const reviewFiles = files.filter((f) => f.endsWith(".json") && parseReviewJsonFilename(f) !== null).sort();
9524
+ const allViolations = [];
9525
+ for (const file of reviewFiles) {
9526
+ try {
9527
+ const fileViolations = await collectViolationsFromFile(path33.join(logDir, file), file, allViolations.length + 1);
9528
+ allViolations.push(...fileViolations);
9529
+ } catch (err) {
9530
+ throw new Error(`Failed to read review JSON ${file}: ${err}`);
9531
+ }
9532
+ }
9533
+ return allViolations;
9534
+ }
9535
+ function statusLineText(status) {
9536
+ switch (status) {
9537
+ case "passed":
9538
+ return "Status: Passed";
9539
+ case "passed_with_warnings":
9540
+ return "Status: Passed with warnings";
9541
+ case "failed":
9542
+ return "Status: Failed";
9543
+ case "retry_limit_exceeded":
9544
+ return "Status: Retry limit exceeded";
9545
+ default:
9546
+ return `Status: ${status}`;
9547
+ }
9548
+ }
9549
+ function formatCheckFailure(result) {
9550
+ const lines = [];
9551
+ lines.push(`### ${result.jobId}`);
9552
+ if (result.command)
9553
+ lines.push(`Command: ${result.command}`);
9554
+ if (result.workingDirectory)
9555
+ lines.push(`Directory: ${result.workingDirectory}`);
9556
+ if (result.fixInstructions)
9557
+ lines.push(`Fix instructions: ${result.fixInstructions}`);
9558
+ if (result.fixWithSkill)
9559
+ lines.push(`Fix skill: ${result.fixWithSkill}`);
9560
+ if (result.logPath)
9561
+ lines.push(`Log: ${result.logPath}`);
9562
+ lines.push("");
9563
+ return lines;
9564
+ }
9565
+ function formatReviewViolation(v) {
9566
+ const lines = [];
9567
+ const priorityStr = v.priority ? ` [${v.priority}]` : "";
9568
+ lines.push(`#${v.id}${priorityStr} ${v.gateLabel} (${v.adapterSuffix})`);
9569
+ lines.push(` ${v.file}:${v.line} - ${v.issue}`);
9570
+ if (v.fix)
9571
+ lines.push(` Fix: ${v.fix}`);
9572
+ lines.push(` JSON: ${v.jsonPath}`);
9573
+ lines.push("");
9574
+ return lines;
9575
+ }
9576
+ async function generateReport(status, gateResults, logDir) {
9577
+ const lines = [statusLineText(status)];
9578
+ if (!gateResults || gateResults.length === 0) {
9579
+ return lines.join(`
9580
+ `);
9581
+ }
9582
+ const checkFailures = gateResults.filter((r) => (r.status === "fail" || r.status === "error") && r.jobId.startsWith("check:"));
9583
+ if (checkFailures.length > 0) {
9584
+ lines.push("", "## CHECK FAILURES", "");
9585
+ for (const result of checkFailures) {
9586
+ lines.push(...formatCheckFailure(result));
9587
+ }
9588
+ }
9589
+ const reviewViolations = await enumerateNewViolations(logDir);
9590
+ if (reviewViolations.length > 0) {
9591
+ lines.push("## REVIEW VIOLATIONS", "");
9592
+ for (const v of reviewViolations) {
9593
+ lines.push(...formatReviewViolation(v));
9594
+ }
9595
+ }
9596
+ return lines.join(`
9597
+ `);
9598
+ }
9599
+
9600
+ // src/core/run-executor-helpers.ts
9601
+ import fs36 from "node:fs/promises";
9602
+ import path35 from "node:path";
9603
+
9604
+ // src/core/run-executor-lock.ts
9605
+ import fs35 from "node:fs/promises";
9606
+ import path34 from "node:path";
9201
9607
  var LOCK_FILENAME2 = ".gauntlet-run.lock";
9202
9608
  var STALE_LOCK_MS = 10 * 60 * 1000;
9203
9609
  function isProcessAlive(pid) {
@@ -9213,9 +9619,9 @@ function isProcessAlive(pid) {
9213
9619
  }
9214
9620
  async function isLockStale(lockPath) {
9215
9621
  try {
9216
- const lockContent = await fs34.readFile(lockPath, "utf-8");
9622
+ const lockContent = await fs35.readFile(lockPath, "utf-8");
9217
9623
  const lockPid = Number.parseInt(lockContent.trim(), 10);
9218
- const lockStat = await fs34.stat(lockPath);
9624
+ const lockStat = await fs35.stat(lockPath);
9219
9625
  const lockAgeMs = Date.now() - lockStat.mtimeMs;
9220
9626
  const pidValid = !Number.isNaN(lockPid);
9221
9627
  if (pidValid && !isProcessAlive(lockPid)) {
@@ -9230,10 +9636,10 @@ async function isLockStale(lockPath) {
9230
9636
  }
9231
9637
  }
9232
9638
  async function tryAcquireLock(logDir) {
9233
- await fs34.mkdir(logDir, { recursive: true });
9234
- const lockPath = path32.resolve(logDir, LOCK_FILENAME2);
9639
+ await fs35.mkdir(logDir, { recursive: true });
9640
+ const lockPath = path34.resolve(logDir, LOCK_FILENAME2);
9235
9641
  try {
9236
- await fs34.writeFile(lockPath, String(process.pid), { flag: "wx" });
9642
+ await fs35.writeFile(lockPath, String(process.pid), { flag: "wx" });
9237
9643
  return true;
9238
9644
  } catch (err) {
9239
9645
  const isExist = typeof err === "object" && err !== null && "code" in err && err.code === "EEXIST";
@@ -9244,9 +9650,9 @@ async function tryAcquireLock(logDir) {
9244
9650
  if (!stale) {
9245
9651
  return false;
9246
9652
  }
9247
- await fs34.rm(lockPath, { force: true });
9653
+ await fs35.rm(lockPath, { force: true });
9248
9654
  try {
9249
- await fs34.writeFile(lockPath, String(process.pid), { flag: "wx" });
9655
+ await fs35.writeFile(lockPath, String(process.pid), { flag: "wx" });
9250
9656
  return true;
9251
9657
  } catch {
9252
9658
  return false;
@@ -9255,7 +9661,7 @@ async function tryAcquireLock(logDir) {
9255
9661
  }
9256
9662
  async function findLatestConsoleLog(logDir) {
9257
9663
  try {
9258
- const files = await fs34.readdir(logDir);
9664
+ const files = await fs35.readdir(logDir);
9259
9665
  let maxNum = -1;
9260
9666
  let latestFile = null;
9261
9667
  for (const file of files) {
@@ -9271,7 +9677,7 @@ async function findLatestConsoleLog(logDir) {
9271
9677
  }
9272
9678
  }
9273
9679
  }
9274
- return latestFile ? path32.join(logDir, latestFile) : null;
9680
+ return latestFile ? path34.join(logDir, latestFile) : null;
9275
9681
  } catch {
9276
9682
  return null;
9277
9683
  }
@@ -9494,6 +9900,16 @@ function determineStatus(outcome) {
9494
9900
  async function buildRunResult(ctx, outcome, jobs) {
9495
9901
  const consoleLogPath = await findLatestConsoleLog(ctx.config.project.log_dir);
9496
9902
  const status = determineStatus(outcome);
9903
+ let reportText;
9904
+ if (ctx.options.report) {
9905
+ reportText = await generateReport(status, outcome.gateResults, ctx.config.project.log_dir);
9906
+ const reportPath = path35.join(ctx.config.project.log_dir, "report.txt");
9907
+ try {
9908
+ await fs36.writeFile(reportPath, reportText, "utf-8");
9909
+ } catch (err) {
9910
+ console.debug(`Failed to write report file ${reportPath}: ${err}`);
9911
+ }
9912
+ }
9497
9913
  if (status === "passed" || status === "retry_limit_exceeded") {
9498
9914
  const reason = status === "passed" ? "all_passed" : "retry_limit_exceeded";
9499
9915
  const debugLogger = getDebugLogger();
@@ -9506,6 +9922,7 @@ async function buildRunResult(ctx, outcome, jobs) {
9506
9922
  gatesRun: jobs.length,
9507
9923
  gatesFailed: outcome.allPassed ? 0 : jobs.length,
9508
9924
  consoleLogPath: consoleLogPath ?? undefined,
9925
+ reportText,
9509
9926
  gateResults: outcome.gateResults
9510
9927
  };
9511
9928
  }
@@ -9578,6 +9995,15 @@ async function runWithLock(ctx, isRerun, logsExist) {
9578
9995
  const { failuresMap, passedSlotsMap, changeOptions } = await processRerunMode2(ctx, isRerun, logsExist);
9579
9996
  const prepared = await detectAndPrepareChanges(ctx, isRerun, failuresMap, changeOptions);
9580
9997
  if ("earlyResult" in prepared) {
9998
+ if (ctx.options.report) {
9999
+ const reportText = await generateReport(prepared.earlyResult.status, prepared.earlyResult.gateResults, ctx.config.project.log_dir);
10000
+ prepared.earlyResult.reportText = reportText;
10001
+ try {
10002
+ const reportPath = path36.join(ctx.config.project.log_dir, "report.txt");
10003
+ await fs37.mkdir(ctx.config.project.log_dir, { recursive: true });
10004
+ await fs37.writeFile(reportPath, reportText, "utf-8");
10005
+ } catch {}
10006
+ }
9581
10007
  return finalizeAndReturn(ctx.loggerInitializedHere, prepared.earlyResult, consoleLogHandle);
9582
10008
  }
9583
10009
  const result = await executeAndReport(ctx, logger, isRerun, failuresMap, passedSlotsMap, changeOptions, prepared.jobs);
@@ -9629,20 +10055,27 @@ async function executeRun(options = {}) {
9629
10055
 
9630
10056
  // src/commands/run.ts
9631
10057
  function registerRunCommand(program) {
9632
- program.command("run").description("Run gates for detected changes").option("-b, --base-branch <branch>", "Override base branch for change detection").option("-g, --gate <name>", "Run specific gate only").option("-c, --commit <sha>", "Use diff for a specific commit").option("-u, --uncommitted", "Use diff for current uncommitted changes (staged and unstaged)").option("-e, --enable-review <name>", "Activate a disabled review for this run (repeatable)", (value, prev = []) => [...prev, value], []).action(async (options) => {
10058
+ program.command("run").description("Run gates for detected changes").option("-b, --base-branch <branch>", "Override base branch for change detection").option("-g, --gate <name>", "Run specific gate only").option("-c, --commit <sha>", "Use diff for a specific commit").option("-u, --uncommitted", "Use diff for current uncommitted changes (staged and unstaged)").option("-e, --enable-review <name>", "Activate a disabled review for this run (repeatable)", (value, prev = []) => [...prev, value], []).option("--report", "Write a structured failure report to stdout").action(async (options) => {
10059
+ const reportEnabled = options.report ?? false;
9633
10060
  const result = await executeRun({
9634
10061
  baseBranch: options.baseBranch,
9635
10062
  gate: options.gate,
9636
10063
  commit: options.commit,
9637
10064
  uncommitted: options.uncommitted,
9638
- enableReviews: new Set(options.enableReview ?? [])
10065
+ enableReviews: new Set(options.enableReview ?? []),
10066
+ report: reportEnabled
9639
10067
  });
10068
+ if (reportEnabled) {
10069
+ const text = result.reportText ?? statusLineText(result.status);
10070
+ process.stdout.write(`${text}
10071
+ `);
10072
+ }
9640
10073
  const code = isSuccessStatus(result.status) ? 0 : 1;
9641
10074
  process.exit(code);
9642
10075
  });
9643
10076
  }
9644
10077
  // src/commands/skip.ts
9645
- import chalk13 from "chalk";
10078
+ import chalk14 from "chalk";
9646
10079
  function registerSkipCommand(program) {
9647
10080
  program.command("skip").description("Advance execution state baseline without running gates").action(async () => {
9648
10081
  let config;
@@ -9661,20 +10094,20 @@ function registerSkipCommand(program) {
9661
10094
  const commit = await getCurrentCommit();
9662
10095
  const shortSha = commit.slice(0, 7);
9663
10096
  await releaseLock(config.project.log_dir);
9664
- console.log(chalk13.green(`Baseline advanced to ${shortSha}. Next run will diff from here.`));
10097
+ console.log(chalk14.green(`Baseline advanced to ${shortSha}. Next run will diff from here.`));
9665
10098
  } catch (error) {
9666
10099
  if (config && lockAcquired) {
9667
10100
  await releaseLock(config.project.log_dir);
9668
10101
  }
9669
10102
  const err = error;
9670
- console.error(chalk13.red("Error:"), err.message);
10103
+ console.error(chalk14.red("Error:"), err.message);
9671
10104
  process.exit(1);
9672
10105
  }
9673
10106
  });
9674
10107
  }
9675
10108
  // src/commands/start-hook.ts
9676
- import fs35 from "node:fs/promises";
9677
- import path33 from "node:path";
10109
+ import fs38 from "node:fs/promises";
10110
+ import path37 from "node:path";
9678
10111
  import YAML8 from "yaml";
9679
10112
  var START_HOOK_MESSAGE = `<IMPORTANT>
9680
10113
  This project uses Agent Gauntlet for automated quality verification.
@@ -9722,9 +10155,9 @@ function isValidConfig(content) {
9722
10155
  }
9723
10156
  function registerStartHookCommand(program) {
9724
10157
  program.command("start-hook").description("Session start hook - primes agent with gauntlet verification instructions").option("--adapter <adapter>", "Output format: claude or cursor", "claude").action(async (options) => {
9725
- const configPath = path33.join(process.cwd(), ".gauntlet", "config.yml");
10158
+ const configPath = path37.join(process.cwd(), ".gauntlet", "config.yml");
9726
10159
  try {
9727
- const content = await fs35.readFile(configPath, "utf-8");
10160
+ const content = await fs38.readFile(configPath, "utf-8");
9728
10161
  if (!isValidConfig(content)) {
9729
10162
  return;
9730
10163
  }
@@ -9734,7 +10167,7 @@ function registerStartHookCommand(program) {
9734
10167
  const adapter = options.adapter;
9735
10168
  try {
9736
10169
  const cwd = process.cwd();
9737
- const logDir = path33.join(cwd, await getLogDir2(cwd));
10170
+ const logDir = path37.join(cwd, await getLogDir2(cwd));
9738
10171
  const globalConfig = await loadGlobalConfig();
9739
10172
  const projectDebugLogConfig = await getDebugLogConfig(cwd);
9740
10173
  const debugLogConfig = mergeDebugLogConfig(projectDebugLogConfig, globalConfig.debug_log);
@@ -9751,17 +10184,119 @@ function registerStatusCommand(program) {
9751
10184
  main();
9752
10185
  });
9753
10186
  }
10187
+ // src/commands/update.ts
10188
+ function registerUpdateCommand(program) {
10189
+ program.command("update").description("Update the agent-gauntlet Claude plugin and refresh skills").action(async () => {
10190
+ await runPluginUpdate();
10191
+ });
10192
+ }
10193
+ // src/commands/update-review.ts
10194
+ import fs39 from "node:fs/promises";
10195
+ async function ensureLogDir(logDir) {
10196
+ try {
10197
+ await fs39.stat(logDir);
10198
+ } catch (error) {
10199
+ const code = error.code;
10200
+ if (code === "ENOENT") {
10201
+ console.error(`Error: Log directory does not exist: ${logDir}`);
10202
+ process.exit(1);
10203
+ }
10204
+ throw new Error(`Failed to access log directory ${logDir}: ${String(error)}`);
10205
+ }
10206
+ }
10207
+ function printViolation(v) {
10208
+ const priorityStr = v.priority ? ` [${v.priority}]` : "";
10209
+ console.log(`#${v.id}${priorityStr} ${v.gateLabel} (${v.adapterSuffix})`);
10210
+ console.log(` ${v.file}:${v.line} - ${v.issue}`);
10211
+ if (v.fix) {
10212
+ console.log(` Fix: ${v.fix}`);
10213
+ }
10214
+ console.log(` JSON: ${v.jsonPath}`);
10215
+ console.log("");
10216
+ }
10217
+ function registerUpdateReviewCommand(program) {
10218
+ const cmd = program.command("update-review").description("Manage review violations");
10219
+ cmd.command("list").description("List pending review violations with numeric IDs").action(async () => {
10220
+ try {
10221
+ const config = await loadConfig();
10222
+ const logDir = config.project.log_dir;
10223
+ await ensureLogDir(logDir);
10224
+ const violations = await enumerateNewViolations(logDir);
10225
+ if (violations.length === 0) {
10226
+ console.log("No pending violations.");
10227
+ process.exit(0);
10228
+ }
10229
+ for (const v of violations) {
10230
+ printViolation(v);
10231
+ }
10232
+ process.exit(0);
10233
+ } catch (error) {
10234
+ const err = error;
10235
+ console.error(`Error: ${err.message || "Unknown error"}`);
10236
+ process.exit(1);
10237
+ }
10238
+ });
10239
+ cmd.command("fix <id> <reason>").description("Mark a violation as fixed").action(async (idStr, reason) => {
10240
+ await updateViolation(idStr, reason, "fixed");
10241
+ });
10242
+ cmd.command("skip <id> <reason>").description("Mark a violation as skipped").action(async (idStr, reason) => {
10243
+ await updateViolation(idStr, reason, "skipped");
10244
+ });
10245
+ }
10246
+ async function updateViolation(idStr, reason, newStatus) {
10247
+ try {
10248
+ if (!reason) {
10249
+ console.error(`Error: Missing reason. Usage: agent-gauntlet update-review ${newStatus} <id> "<reason>"`);
10250
+ process.exit(1);
10251
+ }
10252
+ const id = parseInt(idStr, 10);
10253
+ if (Number.isNaN(id) || id < 1) {
10254
+ console.error(`Error: Invalid ID: ${idStr}`);
10255
+ process.exit(1);
10256
+ }
10257
+ const config = await loadConfig();
10258
+ const logDir = config.project.log_dir;
10259
+ const violations = await enumerateNewViolations(logDir);
10260
+ const target = violations.find((v) => v.id === id);
10261
+ if (!target) {
10262
+ console.error(`Error: Violation #${id} not found. Use 'update-review list' to see available violations.`);
10263
+ process.exit(1);
10264
+ }
10265
+ const content = await fs39.readFile(target.jsonPath, "utf-8");
10266
+ const data = JSON.parse(content);
10267
+ const violation = data.violations[target.violationIndex];
10268
+ if (!violation) {
10269
+ console.error(`Error: Violation at index ${target.violationIndex} not found in ${target.jsonPath}`);
10270
+ process.exit(1);
10271
+ }
10272
+ const currentStatus = violation.status || "new";
10273
+ if (currentStatus !== "new") {
10274
+ console.error(`Error: Violation #${id} is already ${currentStatus} and cannot be updated.`);
10275
+ process.exit(1);
10276
+ }
10277
+ violation.status = newStatus;
10278
+ violation.result = reason;
10279
+ await fs39.writeFile(target.jsonPath, JSON.stringify(data, null, 2), "utf-8");
10280
+ const action = newStatus === "fixed" ? "Fixed" : "Skipped";
10281
+ console.log(`${action} violation #${id}: ${target.file}:${target.line} - ${target.issue}`);
10282
+ process.exit(0);
10283
+ } catch (error) {
10284
+ const err = error;
10285
+ console.error(`Error: ${err.message || "Unknown error"}`);
10286
+ process.exit(1);
10287
+ }
10288
+ }
9754
10289
  // src/commands/validate.ts
9755
- import chalk14 from "chalk";
10290
+ import chalk15 from "chalk";
9756
10291
  function registerValidateCommand(program) {
9757
10292
  program.command("validate").description("Validate .gauntlet/ config files against schemas").action(async () => {
9758
10293
  try {
9759
10294
  await loadConfig();
9760
- console.log(chalk14.green("All config files are valid."));
10295
+ console.log(chalk15.green("All config files are valid."));
9761
10296
  process.exitCode = 0;
9762
10297
  } catch (error) {
9763
10298
  const message = error instanceof Error ? error.message : String(error);
9764
- console.error(chalk14.red("Validation failed:"), message);
10299
+ console.error(chalk15.red("Validation failed:"), message);
9765
10300
  process.exitCode = 1;
9766
10301
  }
9767
10302
  });
@@ -9779,6 +10314,8 @@ registerDetectCommand(program);
9779
10314
  registerListCommand(program);
9780
10315
  registerHealthCommand(program);
9781
10316
  registerInitCommand(program);
10317
+ registerUpdateCommand(program);
10318
+ registerUpdateReviewCommand(program);
9782
10319
  registerValidateCommand(program);
9783
10320
  registerSkipCommand(program);
9784
10321
  registerStartHookCommand(program);
@@ -9790,4 +10327,4 @@ if (process.argv.length < 3) {
9790
10327
  }
9791
10328
  program.parse(process.argv);
9792
10329
 
9793
- //# debugId=BA297B6C5F2D9C1264756E2164756E21
10330
+ //# debugId=9B95D114BC2749E764756E2164756E21