agent-gauntlet 0.13.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7,9 +7,9 @@ import { Command } from "commander";
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "agent-gauntlet",
10
- version: "0.13.1",
10
+ version: "0.14.0",
11
11
  description: "A CLI tool for testing AI coding agents",
12
- license: "Apache-2.0",
12
+ license: "MIT",
13
13
  author: "Paul Caplan",
14
14
  repository: {
15
15
  type: "git",
@@ -70,6 +70,7 @@ var package_default = {
70
70
  typescript: "^5"
71
71
  },
72
72
  dependencies: {
73
+ "@inquirer/prompts": "^8.2.1",
73
74
  "@logtape/logtape": "^2.0.2",
74
75
  chalk: "^5.6.2",
75
76
  commander: "^14.0.2",
@@ -946,11 +947,14 @@ class ClaudeStopHookAdapter {
946
947
  const stopReason = result.shouldBlock && blockReason ? blockReason : result.message;
947
948
  const response = {
948
949
  decision: result.shouldBlock ? "block" : "approve",
949
- stopReason,
950
- systemMessage: result.message,
951
- status: result.status,
952
- message: result.message
950
+ status: result.status
953
951
  };
952
+ if (stopReason)
953
+ response.stopReason = stopReason;
954
+ if (result.message) {
955
+ response.systemMessage = result.message;
956
+ response.message = result.message;
957
+ }
954
958
  if (result.shouldBlock && blockReason) {
955
959
  response.reason = blockReason;
956
960
  }
@@ -995,7 +999,7 @@ class CursorStopHookAdapter {
995
999
  return JSON.stringify(response2);
996
1000
  }
997
1001
  const response = {
998
- systemMessage: result.message
1002
+ ...result.message ? { systemMessage: result.message } : {}
999
1003
  };
1000
1004
  return JSON.stringify(response);
1001
1005
  }
@@ -1305,6 +1309,9 @@ class DebugLogger {
1305
1309
  async logStopHook(decision, reason) {
1306
1310
  await this.write(`STOP_HOOK decision=${decision} reason=${reason}`);
1307
1311
  }
1312
+ async logStartHook(adapter) {
1313
+ await this.write(`START_HOOK adapter=${adapter}`);
1314
+ }
1308
1315
  async logStopHookDiagnostics(diagnostics) {
1309
1316
  const parts = [
1310
1317
  "STOP_HOOK_DIAG",
@@ -1567,6 +1574,27 @@ async function isCommitInBranch(commit, branch) {
1567
1574
  function getExecutionStateFilename() {
1568
1575
  return EXECUTION_STATE_FILENAME;
1569
1576
  }
1577
+ async function hasWorkingTreeChanges() {
1578
+ return new Promise((resolve) => {
1579
+ const child = spawn("git", ["status", "--porcelain"], {
1580
+ stdio: ["ignore", "pipe", "pipe"]
1581
+ });
1582
+ let stdout = "";
1583
+ child.stdout.on("data", (data) => {
1584
+ stdout += data.toString();
1585
+ });
1586
+ child.on("close", (code) => {
1587
+ if (code === 0) {
1588
+ resolve(stdout.trim().length > 0);
1589
+ } else {
1590
+ resolve(true);
1591
+ }
1592
+ });
1593
+ child.on("error", () => {
1594
+ resolve(true);
1595
+ });
1596
+ });
1597
+ }
1570
1598
  async function gitObjectExists(sha) {
1571
1599
  return new Promise((resolve) => {
1572
1600
  const child = spawn("git", ["cat-file", "-t", sha], {
@@ -1818,7 +1846,7 @@ var STATUS_MESSAGES = {
1818
1846
  no_config: "○ Not a gauntlet project — no .gauntlet/config.yml found.",
1819
1847
  stop_hook_active: "↺ Stop hook cycle detected — allowing stop to prevent infinite loop.",
1820
1848
  loop_detected: "↺ Loop detected — stop hook blocked 3 times within 60s. Allowing stop to prevent infinite loop.",
1821
- stop_hook_disabled: "○ Stop hook is disabled via configuration.",
1849
+ stop_hook_disabled: "",
1822
1850
  invalid_input: "⚠ Invalid hook input — could not parse JSON, allowing stop."
1823
1851
  };
1824
1852
  function getStatusMessage(status, context) {
@@ -2528,6 +2556,9 @@ class ClaudeAdapter {
2528
2556
  transformCommand(markdownContent) {
2529
2557
  return markdownContent;
2530
2558
  }
2559
+ supportsHooks() {
2560
+ return true;
2561
+ }
2531
2562
  async execute(opts) {
2532
2563
  const fullContent = `${opts.prompt}
2533
2564
 
@@ -2727,6 +2758,9 @@ class CodexAdapter {
2727
2758
  transformCommand(markdownContent) {
2728
2759
  return markdownContent;
2729
2760
  }
2761
+ supportsHooks() {
2762
+ return false;
2763
+ }
2730
2764
  buildArgs(allowToolUse, thinkingBudget) {
2731
2765
  const args = [
2732
2766
  "exec",
@@ -2837,6 +2871,9 @@ class CursorAdapter {
2837
2871
  transformCommand(markdownContent) {
2838
2872
  return markdownContent;
2839
2873
  }
2874
+ supportsHooks() {
2875
+ return true;
2876
+ }
2840
2877
  async execute(opts) {
2841
2878
  const fullContent = `${opts.prompt}
2842
2879
 
@@ -3080,6 +3117,9 @@ class GeminiAdapter {
3080
3117
  canUseSymlink() {
3081
3118
  return false;
3082
3119
  }
3120
+ supportsHooks() {
3121
+ return false;
3122
+ }
3083
3123
  transformCommand(markdownContent) {
3084
3124
  const fmMatch = markdownContent.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
3085
3125
  let description = "Run the gauntlet verification suite";
@@ -3279,6 +3319,9 @@ class GitHubCopilotAdapter {
3279
3319
  transformCommand(markdownContent) {
3280
3320
  return markdownContent;
3281
3321
  }
3322
+ supportsHooks() {
3323
+ return false;
3324
+ }
3282
3325
  async execute(opts) {
3283
3326
  const fullContent = `${opts.prompt}
3284
3327
 
@@ -5468,7 +5511,8 @@ async function shouldAutoClean(logDir, baseBranch) {
5468
5511
  } catch {
5469
5512
  return { clean: false };
5470
5513
  }
5471
- if (!state.working_tree_ref || state.working_tree_ref === state.commit) {
5514
+ const hasChanges = await hasWorkingTreeChanges();
5515
+ if (!hasChanges) {
5472
5516
  try {
5473
5517
  const isMerged = await isCommitInBranch(state.commit, baseBranch);
5474
5518
  if (isMerged) {
@@ -6720,13 +6764,141 @@ function registerHelpCommand(program) {
6720
6764
  }
6721
6765
  // src/commands/init.ts
6722
6766
  import { readFileSync } from "node:fs";
6767
+ import fs26 from "node:fs/promises";
6768
+ import path25 from "node:path";
6769
+ import { fileURLToPath } from "node:url";
6770
+ import chalk10 from "chalk";
6771
+
6772
+ // src/commands/init-checksums.ts
6773
+ import { createHash } from "node:crypto";
6723
6774
  import fs25 from "node:fs/promises";
6724
6775
  import path24 from "node:path";
6725
- import { fileURLToPath } from "node:url";
6776
+ async function computeSkillChecksum(skillDir) {
6777
+ const files = await collectFiles(skillDir);
6778
+ files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
6779
+ const hash = createHash("sha256");
6780
+ for (const file of files) {
6781
+ hash.update(file.relativePath);
6782
+ hash.update(file.content);
6783
+ }
6784
+ return hash.digest("hex");
6785
+ }
6786
+ function computeExpectedSkillChecksum(content, references) {
6787
+ const entries = [
6788
+ { relativePath: "SKILL.md", content }
6789
+ ];
6790
+ if (references) {
6791
+ for (const [name, refContent] of Object.entries(references)) {
6792
+ entries.push({
6793
+ relativePath: path24.join("references", name),
6794
+ content: refContent
6795
+ });
6796
+ }
6797
+ }
6798
+ entries.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
6799
+ const hash = createHash("sha256");
6800
+ for (const entry of entries) {
6801
+ hash.update(entry.relativePath);
6802
+ hash.update(entry.content);
6803
+ }
6804
+ return hash.digest("hex");
6805
+ }
6806
+ function computeHookChecksum(entries) {
6807
+ const gauntletEntries = entries.filter((entry) => isGauntletHookEntry(entry));
6808
+ const hash = createHash("sha256");
6809
+ hash.update(JSON.stringify(gauntletEntries));
6810
+ return hash.digest("hex");
6811
+ }
6812
+ function computeExpectedHookChecksum(hookEntries) {
6813
+ return computeHookChecksum(hookEntries);
6814
+ }
6815
+ function isGauntletHookEntry(entry) {
6816
+ if (typeof entry.command === "string" && entry.command.startsWith("agent-gauntlet")) {
6817
+ return true;
6818
+ }
6819
+ const nested = entry.hooks;
6820
+ if (Array.isArray(nested)) {
6821
+ return nested.some((h) => typeof h.command === "string" && h.command.startsWith("agent-gauntlet"));
6822
+ }
6823
+ return false;
6824
+ }
6825
+ async function collectFiles(dir, baseDir) {
6826
+ const base = baseDir ?? dir;
6827
+ const results = [];
6828
+ const entries = await fs25.readdir(dir, { withFileTypes: true });
6829
+ for (const entry of entries) {
6830
+ const fullPath = path24.join(dir, entry.name);
6831
+ if (entry.isDirectory()) {
6832
+ results.push(...await collectFiles(fullPath, base));
6833
+ } else if (entry.isFile()) {
6834
+ const content = await fs25.readFile(fullPath, "utf-8");
6835
+ results.push({ relativePath: path24.relative(base, fullPath), content });
6836
+ }
6837
+ }
6838
+ return results;
6839
+ }
6840
+
6841
+ // src/commands/init-prompts.ts
6842
+ import { checkbox, confirm, number } from "@inquirer/prompts";
6726
6843
  import chalk9 from "chalk";
6727
- var __dirname2 = path24.dirname(fileURLToPath(import.meta.url));
6844
+ async function promptDevCLIs(detectedNames, skipPrompts) {
6845
+ if (skipPrompts)
6846
+ return detectedNames;
6847
+ console.log();
6848
+ console.log(chalk9.bold("Select your development CLI(s). These are the main tools you work in."));
6849
+ const selected = await checkbox({
6850
+ message: "Development CLIs:",
6851
+ choices: detectedNames.map((name) => ({ name, value: name })),
6852
+ required: true
6853
+ });
6854
+ return selected;
6855
+ }
6856
+ async function promptReviewCLIs(detectedNames, skipPrompts) {
6857
+ if (skipPrompts)
6858
+ return detectedNames;
6859
+ console.log();
6860
+ console.log(chalk9.bold("Select your reviewer CLI(s). These are the CLIs that will be used for AI code reviews."));
6861
+ const selected = await checkbox({
6862
+ message: "Review CLIs:",
6863
+ choices: detectedNames.map((name) => ({ name, value: name })),
6864
+ required: true
6865
+ });
6866
+ return selected;
6867
+ }
6868
+ async function promptNumReviews(reviewCliCount, skipPrompts) {
6869
+ if (reviewCliCount === 1)
6870
+ return 1;
6871
+ if (skipPrompts)
6872
+ return reviewCliCount;
6873
+ const result = await number({
6874
+ message: "How many of these CLIs would you like to run on every review?",
6875
+ min: 1,
6876
+ max: reviewCliCount,
6877
+ default: 1
6878
+ });
6879
+ return result ?? 1;
6880
+ }
6881
+ async function promptFileOverwrite(name, skipPrompts) {
6882
+ if (skipPrompts)
6883
+ return true;
6884
+ return confirm({
6885
+ message: `Skill \`${name}\` has changed, update it?`,
6886
+ default: true
6887
+ });
6888
+ }
6889
+ async function promptHookOverwrite(hookFile, skipPrompts) {
6890
+ if (skipPrompts)
6891
+ return true;
6892
+ return confirm({
6893
+ message: `Hook configuration in ${hookFile} has changed, update it?`,
6894
+ default: true
6895
+ });
6896
+ }
6897
+
6898
+ // src/commands/init.ts
6899
+ var __dirname2 = path25.dirname(fileURLToPath(import.meta.url));
6728
6900
  function readSkillTemplate(filename) {
6729
- const templatePath = path24.join(__dirname2, "skill-templates", filename);
6901
+ const templatePath = path25.join(__dirname2, "skill-templates", filename);
6730
6902
  return readFileSync(templatePath, "utf-8");
6731
6903
  }
6732
6904
  var CLI_PREFERENCE_ORDER = [
@@ -6744,13 +6916,15 @@ var ADAPTER_CONFIG = {
6744
6916
  function buildGauntletSkillContent(mode) {
6745
6917
  const isRun = mode === "run";
6746
6918
  const name = isRun ? "run" : "check";
6747
- const description = isRun ? "Run the full verification gauntlet" : "Run checks only (no reviews)";
6919
+ const description = isRun ? "Run the full verification gauntlet. Use this as the final step after completing a coding task — verifies quality, runs checks, and ensures all gates pass. Must be run before committing, pushing, or creating PRs." : "Run checks only (no reviews)";
6748
6920
  const command = isRun ? "agent-gauntlet run" : "agent-gauntlet check";
6749
6921
  const heading = isRun ? "Execute the autonomous verification suite." : "Run the gauntlet checks only — no AI reviews.";
6922
+ const disableModelInvocation = isRun ? "false" : "true";
6750
6923
  const frontmatter = `---
6751
6924
  name: gauntlet-${name}
6752
- description: ${description}
6753
- disable-model-invocation: true
6925
+ description: >-
6926
+ ${description}
6927
+ disable-model-invocation: ${disableModelInvocation}
6754
6928
  allowed-tools: Bash
6755
6929
  ---`;
6756
6930
  const steps = [
@@ -6839,16 +7013,37 @@ var SETUP_SKILL_CONTENT = readSkillTemplate("setup-skill.md");
6839
7013
  var CHECK_CATALOG_REFERENCE = readSkillTemplate("check-catalog.md");
6840
7014
  var PROJECT_STRUCTURE_REFERENCE = readSkillTemplate("setup-ref-project-structure.md");
6841
7015
  var SKILL_DEFINITIONS = [
6842
- { action: "run", content: GAUNTLET_RUN_SKILL_CONTENT },
6843
- { action: "check", content: GAUNTLET_CHECK_SKILL_CONTENT },
6844
- { action: "push-pr", content: PUSH_PR_SKILL_CONTENT },
6845
- { action: "fix-pr", content: FIX_PR_SKILL_CONTENT },
6846
- { action: "status", content: GAUNTLET_STATUS_SKILL_CONTENT },
7016
+ {
7017
+ action: "run",
7018
+ content: GAUNTLET_RUN_SKILL_CONTENT,
7019
+ description: "Run the verification suite"
7020
+ },
7021
+ {
7022
+ action: "check",
7023
+ content: GAUNTLET_CHECK_SKILL_CONTENT,
7024
+ description: "Run a single check gate"
7025
+ },
7026
+ {
7027
+ action: "push-pr",
7028
+ content: PUSH_PR_SKILL_CONTENT,
7029
+ description: "Commit, push, and create a PR"
7030
+ },
7031
+ {
7032
+ action: "fix-pr",
7033
+ content: FIX_PR_SKILL_CONTENT,
7034
+ description: "Fix PR review comments and CI failures"
7035
+ },
7036
+ {
7037
+ action: "status",
7038
+ content: GAUNTLET_STATUS_SKILL_CONTENT,
7039
+ description: "Show gauntlet status"
7040
+ },
6847
7041
  {
6848
7042
  action: "help",
6849
7043
  content: HELP_SKILL_BUNDLE.content,
6850
7044
  references: HELP_SKILL_BUNDLE.references,
6851
- skillsOnly: true
7045
+ skillsOnly: true,
7046
+ description: "Diagnose and explain gauntlet behavior"
6852
7047
  },
6853
7048
  {
6854
7049
  action: "setup",
@@ -6857,82 +7052,192 @@ var SKILL_DEFINITIONS = [
6857
7052
  "check-catalog.md": CHECK_CATALOG_REFERENCE,
6858
7053
  "project-structure.md": PROJECT_STRUCTURE_REFERENCE
6859
7054
  },
6860
- skillsOnly: true
7055
+ skillsOnly: true,
7056
+ description: "Configure checks and reviews interactively"
6861
7057
  }
6862
7058
  ];
7059
+ var NATIVE_CLIS = new Set(["claude", "cursor"]);
6863
7060
  function registerInitCommand(program) {
6864
7061
  program.command("init").description("Initialize .gauntlet configuration").option("-y, --yes", "Skip prompts and use defaults").action(async (options) => {
6865
7062
  const projectRoot = process.cwd();
6866
- const targetDir = path24.join(projectRoot, ".gauntlet");
6867
- if (await exists(targetDir)) {
6868
- console.log(chalk9.yellow(".gauntlet directory already exists."));
6869
- return;
6870
- }
7063
+ const targetDir = path25.join(projectRoot, ".gauntlet");
7064
+ const skipPrompts = options.yes ?? false;
6871
7065
  console.log("Detecting available CLI agents...");
6872
7066
  const availableAdapters = await detectAvailableCLIs();
6873
7067
  if (availableAdapters.length === 0) {
6874
7068
  printNoCLIsMessage();
6875
7069
  return;
6876
7070
  }
6877
- await scaffoldProject({
6878
- projectRoot,
6879
- targetDir,
6880
- availableAdapters,
6881
- skipPrompts: options.yes ?? false
6882
- });
6883
- if (availableAdapters.some((a) => a.name === "claude")) {
6884
- await installStopHook(projectRoot);
6885
- }
6886
- if (availableAdapters.some((a) => a.name === "cursor")) {
6887
- await installCursorStopHook(projectRoot);
7071
+ const detectedNames = availableAdapters.map((a) => a.name);
7072
+ const gauntletExists = await exists(targetDir);
7073
+ let hookAdapters;
7074
+ let instructionCLINames;
7075
+ if (gauntletExists) {
7076
+ console.log(chalk10.dim(".gauntlet/ already exists, skipping scaffolding"));
7077
+ hookAdapters = availableAdapters;
7078
+ instructionCLINames = detectedNames;
7079
+ } else {
7080
+ const devCLINames = await promptDevCLIs(detectedNames, skipPrompts);
7081
+ hookAdapters = availableAdapters.filter((a) => devCLINames.includes(a.name));
7082
+ for (const adapter of hookAdapters) {
7083
+ if (!adapter.supportsHooks()) {
7084
+ console.log(chalk10.yellow(` ${adapter.name} doesn't support hooks yet, skipping hook installation`));
7085
+ }
7086
+ }
7087
+ const reviewCLINames = await promptReviewCLIs(detectedNames, skipPrompts);
7088
+ const numReviews = await promptNumReviews(reviewCLINames.length, skipPrompts);
7089
+ console.log(chalk10.cyan("Agent Gauntlet's built-in code quality reviewer will be installed."));
7090
+ await scaffoldGauntletDir(projectRoot, targetDir, reviewCLINames, numReviews);
7091
+ instructionCLINames = devCLINames;
6888
7092
  }
7093
+ await installExternalFiles(projectRoot, hookAdapters, skipPrompts);
6889
7094
  await addToGitignore(projectRoot, "gauntlet_logs");
6890
- console.log();
6891
- console.log(chalk9.bold("Run /gauntlet-setup to configure your checks and reviews"));
7095
+ printPostInitInstructions(instructionCLINames);
6892
7096
  });
6893
7097
  }
6894
7098
  function printNoCLIsMessage() {
6895
7099
  console.log();
6896
- console.log(chalk9.red("Error: No CLI agents found. Install at least one:"));
7100
+ console.log(chalk10.red("Error: No CLI agents found. Install at least one:"));
6897
7101
  console.log(" - Claude: https://docs.anthropic.com/en/docs/claude-code");
6898
7102
  console.log(" - Gemini: https://github.com/google-gemini/gemini-cli");
6899
7103
  console.log(" - Codex: https://github.com/openai/codex");
6900
7104
  console.log();
6901
7105
  }
6902
- async function scaffoldProject(options) {
6903
- const { projectRoot, targetDir, availableAdapters, skipPrompts } = options;
6904
- await fs25.mkdir(targetDir);
6905
- await fs25.mkdir(path24.join(targetDir, "checks"));
6906
- await fs25.mkdir(path24.join(targetDir, "reviews"));
6907
- const commands = SKILL_DEFINITIONS.map((skill) => ({
6908
- action: skill.action,
6909
- content: skill.content,
6910
- ..."references" in skill ? { references: skill.references } : {},
6911
- ..."skillsOnly" in skill ? { skillsOnly: skill.skillsOnly } : {}
6912
- }));
6913
- if (skipPrompts) {
6914
- await installCommands({
6915
- level: "project",
6916
- agentNames: ["claude"],
6917
- projectRoot,
6918
- commands
6919
- });
6920
- } else {
6921
- await promptAndInstallCommands({ projectRoot, commands });
7106
+ async function scaffoldGauntletDir(_projectRoot, targetDir, reviewCLINames, numReviews) {
7107
+ if (await exists(targetDir)) {
7108
+ console.log(chalk10.dim(".gauntlet/ already exists, skipping scaffolding"));
7109
+ return;
7110
+ }
7111
+ await fs26.mkdir(targetDir);
7112
+ await fs26.mkdir(path25.join(targetDir, "checks"));
7113
+ await fs26.mkdir(path25.join(targetDir, "reviews"));
7114
+ await writeConfigYml(targetDir, reviewCLINames);
7115
+ await fs26.writeFile(path25.join(targetDir, "reviews", "code-quality.yml"), `builtin: code-quality
7116
+ num_reviews: ${numReviews}
7117
+ `);
7118
+ console.log(chalk10.green("Created .gauntlet/reviews/code-quality.yml"));
7119
+ }
7120
+ async function writeSkillFiles(actionDir, content, references) {
7121
+ await fs26.mkdir(actionDir, { recursive: true });
7122
+ await fs26.writeFile(path25.join(actionDir, "SKILL.md"), content);
7123
+ if (references) {
7124
+ const refsDir = path25.join(actionDir, "references");
7125
+ await fs26.mkdir(refsDir, { recursive: true });
7126
+ for (const [fileName, fileContent] of Object.entries(references)) {
7127
+ await fs26.writeFile(path25.join(refsDir, fileName), fileContent);
7128
+ }
7129
+ }
7130
+ }
7131
+ async function installSkillsWithChecksums(projectRoot, skipPrompts) {
7132
+ const skillsDir = path25.join(projectRoot, ".claude", "skills");
7133
+ for (const skill of SKILL_DEFINITIONS) {
7134
+ const actionDir = path25.join(skillsDir, `gauntlet-${skill.action}`);
7135
+ const skillPath = path25.join(actionDir, "SKILL.md");
7136
+ const references = "references" in skill ? skill.references : undefined;
7137
+ if (!await exists(actionDir)) {
7138
+ await writeSkillFiles(actionDir, skill.content, references);
7139
+ console.log(chalk10.green(`Created ${path25.relative(projectRoot, skillPath)}`));
7140
+ continue;
7141
+ }
7142
+ const expectedChecksum = computeExpectedSkillChecksum(skill.content, references);
7143
+ const actualChecksum = await computeSkillChecksum(actionDir);
7144
+ if (expectedChecksum === actualChecksum)
7145
+ continue;
7146
+ const shouldOverwrite = await promptFileOverwrite(`gauntlet-${skill.action}`, skipPrompts);
7147
+ if (!shouldOverwrite)
7148
+ continue;
7149
+ await fs26.rm(actionDir, { recursive: true, force: true });
7150
+ await writeSkillFiles(actionDir, skill.content, references);
7151
+ console.log(chalk10.green(`Updated ${path25.relative(projectRoot, skillPath)}`));
7152
+ }
7153
+ }
7154
+ async function installHookWithChecksums(target, skipPrompts) {
7155
+ const spec = buildHookSpec(target);
7156
+ let existingConfig = {};
7157
+ if (await exists(spec.config.filePath)) {
7158
+ try {
7159
+ existingConfig = JSON.parse(await fs26.readFile(spec.config.filePath, "utf-8"));
7160
+ } catch {
7161
+ existingConfig = {};
7162
+ }
7163
+ }
7164
+ const existingHooks = existingConfig.hooks || {};
7165
+ const existingEntries = Array.isArray(existingHooks[spec.config.hookKey]) ? existingHooks[spec.config.hookKey] : [];
7166
+ const gauntletEntries = existingEntries.filter((e) => isGauntletHookEntry(e));
7167
+ if (gauntletEntries.length === 0) {
7168
+ await installHookWithLog(spec.config, spec.installedMsg, spec.existsMsg);
7169
+ return;
6922
7170
  }
6923
- await writeConfigYml(targetDir, availableAdapters);
6924
- await fs25.writeFile(path24.join(targetDir, "reviews", "code-quality.yml"), `builtin: code-quality
6925
- num_reviews: 1
7171
+ const expectedEntry = spec.config.wrapInHooksArray ? { hooks: [spec.config.hookEntry] } : spec.config.hookEntry;
7172
+ const expectedChecksum = computeExpectedHookChecksum([
7173
+ expectedEntry
7174
+ ]);
7175
+ const actualChecksum = computeHookChecksum(existingEntries);
7176
+ if (expectedChecksum === actualChecksum) {
7177
+ console.log(chalk10.dim(spec.existsMsg));
7178
+ return;
7179
+ }
7180
+ const shouldOverwrite = await promptHookOverwrite(spec.config.filePath, skipPrompts);
7181
+ if (!shouldOverwrite) {
7182
+ console.log(chalk10.dim(spec.existsMsg));
7183
+ return;
7184
+ }
7185
+ const nonGauntletEntries = existingEntries.filter((e) => !isGauntletHookEntry(e));
7186
+ const entryToAdd = spec.config.wrapInHooksArray ? { hooks: [spec.config.hookEntry] } : spec.config.hookEntry;
7187
+ const newEntries = [...nonGauntletEntries, entryToAdd];
7188
+ const merged = {
7189
+ ...spec.config.baseConfig ?? {},
7190
+ ...existingConfig,
7191
+ hooks: {
7192
+ ...existingHooks,
7193
+ [spec.config.hookKey]: newEntries
7194
+ }
7195
+ };
7196
+ await fs26.mkdir(path25.dirname(spec.config.filePath), { recursive: true });
7197
+ await fs26.writeFile(spec.config.filePath, `${JSON.stringify(merged, null, 2)}
6926
7198
  `);
6927
- console.log(chalk9.green("Created .gauntlet/reviews/code-quality.yml"));
6928
- await copyStatusScript(targetDir);
7199
+ console.log(chalk10.green(spec.installedMsg));
7200
+ }
7201
+ async function installExternalFiles(projectRoot, devAdapters, skipPrompts) {
7202
+ await installSkillsWithChecksums(projectRoot, skipPrompts);
7203
+ await copyStatusScript(path25.join(projectRoot, ".gauntlet"));
7204
+ for (const adapter of devAdapters) {
7205
+ if (!adapter.supportsHooks())
7206
+ continue;
7207
+ if (adapter.name !== "claude" && adapter.name !== "cursor")
7208
+ continue;
7209
+ for (const kind of ["stop", "start"]) {
7210
+ const target = {
7211
+ projectRoot,
7212
+ variant: adapter.name,
7213
+ kind
7214
+ };
7215
+ await installHookWithChecksums(target, skipPrompts);
7216
+ }
7217
+ }
7218
+ }
7219
+ function printPostInitInstructions(devCLINames) {
7220
+ const hasNative = devCLINames.some((name) => NATIVE_CLIS.has(name));
7221
+ const nonNativeNames = devCLINames.filter((name) => !NATIVE_CLIS.has(name));
7222
+ const hasNonNative = nonNativeNames.length > 0;
7223
+ console.log();
7224
+ if (hasNative) {
7225
+ console.log(chalk10.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."));
7226
+ }
7227
+ if (hasNonNative) {
7228
+ console.log(chalk10.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."));
7229
+ console.log();
7230
+ console.log("Available skills:");
7231
+ for (const s of SKILL_DEFINITIONS) {
7232
+ console.log(` @.claude/skills/gauntlet-${s.action}/SKILL.md — ${s.description}`);
7233
+ }
7234
+ }
6929
7235
  }
6930
- async function writeConfigYml(targetDir, adapters3) {
7236
+ async function writeConfigYml(targetDir, reviewCLINames) {
6931
7237
  const baseBranch = await detectBaseBranch();
6932
- const sortedAdapters = [...adapters3].sort((a, b) => CLI_PREFERENCE_ORDER.indexOf(a.name) - CLI_PREFERENCE_ORDER.indexOf(b.name));
6933
- const cliList = sortedAdapters.map((a) => ` - ${a.name}`).join(`
7238
+ const cliList = reviewCLINames.map((name) => ` - ${name}`).join(`
6934
7239
  `);
6935
- const adapterSettings = buildAdapterSettingsBlock(adapters3);
7240
+ const adapterSettings = buildAdapterSettingsBlock(reviewCLINames);
6936
7241
  const content = `# Ordered list of CLI agents to try for reviews
6937
7242
  cli:
6938
7243
  default_preference:
@@ -6989,14 +7294,14 @@ entry_points: []
6989
7294
  # enabled: true
6990
7295
  # format: text # Options: text, json
6991
7296
  `;
6992
- await fs25.writeFile(path24.join(targetDir, "config.yml"), content);
6993
- console.log(chalk9.green("Created .gauntlet/config.yml"));
7297
+ await fs26.writeFile(path25.join(targetDir, "config.yml"), content);
7298
+ console.log(chalk10.green("Created .gauntlet/config.yml"));
6994
7299
  }
6995
7300
  async function addToGitignore(projectRoot, entry) {
6996
- const gitignorePath = path24.join(projectRoot, ".gitignore");
7301
+ const gitignorePath = path25.join(projectRoot, ".gitignore");
6997
7302
  let content = "";
6998
7303
  if (await exists(gitignorePath)) {
6999
- content = await fs25.readFile(gitignorePath, "utf-8");
7304
+ content = await fs26.readFile(gitignorePath, "utf-8");
7000
7305
  const lines = content.split(`
7001
7306
  `).map((l) => l.trim());
7002
7307
  if (lines.includes(entry)) {
@@ -7006,9 +7311,9 @@ async function addToGitignore(projectRoot, entry) {
7006
7311
  const suffix = content.length > 0 && !content.endsWith(`
7007
7312
  `) ? `
7008
7313
  ` : "";
7009
- await fs25.appendFile(gitignorePath, `${suffix}${entry}
7314
+ await fs26.appendFile(gitignorePath, `${suffix}${entry}
7010
7315
  `);
7011
- console.log(chalk9.green(`Added ${entry} to .gitignore`));
7316
+ console.log(chalk10.green(`Added ${entry} to .gitignore`));
7012
7317
  }
7013
7318
  function gitSilent(args, opts) {
7014
7319
  const { execFileSync } = __require("node:child_process");
@@ -7035,13 +7340,13 @@ async function detectBaseBranch() {
7035
7340
  }
7036
7341
  return "origin/main";
7037
7342
  }
7038
- function buildAdapterSettingsBlock(adapters3) {
7039
- const items = adapters3.filter((a) => ADAPTER_CONFIG[a.name]);
7343
+ function buildAdapterSettingsBlock(adapterNames) {
7344
+ const items = adapterNames.filter((name) => ADAPTER_CONFIG[name]);
7040
7345
  if (items.length === 0)
7041
7346
  return "";
7042
- const lines = items.map((a) => {
7043
- const c = ADAPTER_CONFIG[a.name];
7044
- return ` ${a.name}:
7347
+ const lines = items.map((name) => {
7348
+ const c = ADAPTER_CONFIG[name];
7349
+ return ` ${name}:
7045
7350
  allow_tool_use: ${c?.allow_tool_use}
7046
7351
  thinking_budget: ${c?.thinking_budget}`;
7047
7352
  });
@@ -7057,233 +7362,171 @@ async function detectAvailableCLIs() {
7057
7362
  for (const adapter of allAdapters) {
7058
7363
  const isAvailable = await adapter.isAvailable();
7059
7364
  if (isAvailable) {
7060
- console.log(chalk9.green(` ✓ ${adapter.name}`));
7365
+ console.log(chalk10.green(` ✓ ${adapter.name}`));
7061
7366
  available.push(adapter);
7062
7367
  } else {
7063
- console.log(chalk9.dim(` ✗ ${adapter.name} (not installed)`));
7368
+ console.log(chalk10.dim(` ✗ ${adapter.name} (not installed)`));
7064
7369
  }
7065
7370
  }
7066
7371
  return available;
7067
7372
  }
7068
7373
  async function copyStatusScript(targetDir) {
7069
- const statusScriptDir = path24.join(targetDir, "skills", "gauntlet", "status", "scripts");
7070
- const statusScriptPath = path24.join(statusScriptDir, "status.ts");
7071
- await fs25.mkdir(statusScriptDir, { recursive: true });
7374
+ const statusScriptDir = path25.join(targetDir, "scripts");
7375
+ const statusScriptPath = path25.join(statusScriptDir, "status.ts");
7376
+ await fs26.mkdir(statusScriptDir, { recursive: true });
7072
7377
  if (await exists(statusScriptPath))
7073
7378
  return;
7074
- const bundledScript = path24.join(path24.dirname(new URL(import.meta.url).pathname), "..", "scripts", "status.ts");
7379
+ const bundledScript = path25.join(path25.dirname(new URL(import.meta.url).pathname), "..", "scripts", "status.ts");
7075
7380
  if (await exists(bundledScript)) {
7076
- await fs25.copyFile(bundledScript, statusScriptPath);
7077
- console.log(chalk9.green("Created .gauntlet/skills/gauntlet/status/scripts/status.ts"));
7381
+ await fs26.copyFile(bundledScript, statusScriptPath);
7382
+ console.log(chalk10.green("Created .gauntlet/scripts/status.ts"));
7078
7383
  } else {
7079
- console.log(chalk9.yellow("Warning: bundled status script not found; /gauntlet-status may fail."));
7384
+ console.log(chalk10.yellow("Warning: bundled status script not found; /gauntlet-status may fail."));
7080
7385
  }
7081
7386
  }
7082
- async function promptAndInstallCommands(options) {
7083
- const { projectRoot, commands } = options;
7084
- await installCommands({
7085
- level: "project",
7086
- agentNames: ["claude"],
7087
- projectRoot,
7088
- commands
7387
+ function hookHasCommand(entries, cmd) {
7388
+ return entries.some((hook) => {
7389
+ if (hook.command === cmd)
7390
+ return true;
7391
+ const nested = hook.hooks;
7392
+ return Array.isArray(nested) && nested.some((h) => h.command === cmd);
7089
7393
  });
7090
7394
  }
7091
- async function installSkill(skillDir, ctx, command) {
7092
- const actionDir = path24.join(skillDir, `gauntlet-${command.action}`);
7093
- const skillPath = path24.join(actionDir, "SKILL.md");
7094
- await fs25.mkdir(actionDir, { recursive: true });
7095
- if (await exists(skillPath)) {
7096
- const relPath2 = ctx.isUserLevel ? skillPath : path24.relative(ctx.projectRoot, skillPath);
7097
- console.log(chalk9.dim(` claude: ${relPath2} already exists, skipping`));
7098
- return;
7099
- }
7100
- await fs25.writeFile(skillPath, command.content);
7101
- const relPath = ctx.isUserLevel ? skillPath : path24.relative(ctx.projectRoot, skillPath);
7102
- console.log(chalk9.green(`Created ${relPath}`));
7103
- if (command.references) {
7104
- const refsDir = path24.join(actionDir, "references");
7105
- await fs25.mkdir(refsDir, { recursive: true });
7106
- for (const [fileName, fileContent] of Object.entries(command.references)) {
7107
- const refPath = path24.join(refsDir, fileName);
7108
- if (await exists(refPath))
7109
- continue;
7110
- await fs25.writeFile(refPath, fileContent);
7111
- const refRelPath = ctx.isUserLevel ? refPath : path24.relative(ctx.projectRoot, refPath);
7112
- console.log(chalk9.green(`Created ${refRelPath}`));
7113
- }
7114
- }
7115
- }
7116
- async function installFlatCommand(adapter, commandDir, ctx, command) {
7117
- const name = command.action === "run" ? "gauntlet" : command.action;
7118
- const fileName = `${name}${adapter.getCommandExtension()}`;
7119
- const filePath = path24.join(commandDir, fileName);
7395
+ async function mergeHookConfig(opts) {
7396
+ const {
7397
+ filePath,
7398
+ hookKey,
7399
+ hookEntry,
7400
+ deduplicateCmd,
7401
+ wrapInHooksArray,
7402
+ baseConfig
7403
+ } = opts;
7404
+ await fs26.mkdir(path25.dirname(filePath), { recursive: true });
7405
+ let existing = {};
7120
7406
  if (await exists(filePath)) {
7121
- const relPath2 = ctx.isUserLevel ? filePath : path24.relative(ctx.projectRoot, filePath);
7122
- console.log(chalk9.dim(` ${adapter.name}: ${relPath2} already exists, skipping`));
7123
- return;
7124
- }
7125
- const transformedContent = adapter.transformCommand(command.content);
7126
- await fs25.writeFile(filePath, transformedContent);
7127
- const relPath = ctx.isUserLevel ? filePath : path24.relative(ctx.projectRoot, filePath);
7128
- console.log(chalk9.green(`Created ${relPath}`));
7129
- }
7130
- async function installSkillsForAdapter(adapter, skillDir, ctx, commands) {
7131
- const resolvedSkillDir = ctx.isUserLevel ? skillDir : path24.join(ctx.projectRoot, skillDir);
7132
- try {
7133
- for (const command of commands) {
7134
- await installSkill(resolvedSkillDir, ctx, command);
7135
- }
7136
- } catch (error) {
7137
- const err = error;
7138
- console.log(chalk9.yellow(` ${adapter.name}: Could not create skill - ${err.message}`));
7139
- }
7140
- }
7141
- async function installFlatCommandsForAdapter(adapter, commandDir, ctx, commands) {
7142
- const resolvedCommandDir = ctx.isUserLevel ? commandDir : path24.join(ctx.projectRoot, commandDir);
7143
- try {
7144
- await fs25.mkdir(resolvedCommandDir, { recursive: true });
7145
- const flatCommands = commands.filter((c) => c.action !== "check" && c.action !== "status" && !c.skillsOnly);
7146
- for (const command of flatCommands) {
7147
- await installFlatCommand(adapter, resolvedCommandDir, ctx, command);
7148
- }
7149
- } catch (error) {
7150
- const err = error;
7151
- console.log(chalk9.yellow(` ${adapter.name}: Could not create command - ${err.message}`));
7152
- }
7153
- }
7154
- async function installCommands(options) {
7155
- const { level, agentNames, projectRoot, commands } = options;
7156
- if (level === "none" || agentNames.length === 0)
7157
- return;
7158
- console.log();
7159
- const allAdapters = getAllAdapters();
7160
- const isUserLevel = level === "user";
7161
- const ctx = { isUserLevel, projectRoot };
7162
- for (const agentName of agentNames) {
7163
- const adapter = allAdapters.find((a) => a.name === agentName);
7164
- if (!adapter)
7165
- continue;
7166
- const skillDir = isUserLevel ? adapter.getUserSkillDir() : adapter.getProjectSkillDir();
7167
- if (skillDir) {
7168
- await installSkillsForAdapter(adapter, skillDir, ctx, commands);
7169
- continue;
7170
- }
7171
- const commandDir = isUserLevel ? adapter.getUserCommandDir() : adapter.getProjectCommandDir();
7172
- if (!commandDir)
7173
- continue;
7174
- await installFlatCommandsForAdapter(adapter, commandDir, ctx, commands);
7175
- }
7176
- }
7177
- var STOP_HOOK_CONFIG = {
7178
- hooks: {
7179
- Stop: [
7180
- {
7181
- hooks: [
7182
- {
7183
- type: "command",
7184
- command: "agent-gauntlet stop-hook",
7185
- timeout: 300
7186
- }
7187
- ]
7188
- }
7189
- ]
7190
- }
7191
- };
7192
- var CURSOR_STOP_HOOK_CONFIG = {
7193
- version: 1,
7194
- hooks: {
7195
- stop: [
7196
- {
7197
- command: "agent-gauntlet stop-hook",
7198
- loop_limit: 10
7199
- }
7200
- ]
7201
- }
7202
- };
7203
- async function installStopHook(projectRoot) {
7204
- const claudeDir = path24.join(projectRoot, ".claude");
7205
- const settingsPath = path24.join(claudeDir, "settings.local.json");
7206
- await fs25.mkdir(claudeDir, { recursive: true });
7207
- let existingSettings = {};
7208
- if (await exists(settingsPath)) {
7209
7407
  try {
7210
- const content = await fs25.readFile(settingsPath, "utf-8");
7211
- existingSettings = JSON.parse(content);
7408
+ existing = JSON.parse(await fs26.readFile(filePath, "utf-8"));
7212
7409
  } catch {
7213
- existingSettings = {};
7410
+ existing = {};
7214
7411
  }
7215
7412
  }
7216
- const existingHooks = existingSettings.hooks || {};
7217
- const existingStopHooks = Array.isArray(existingHooks.Stop) ? existingHooks.Stop : [];
7218
- const hookExists = existingStopHooks.some((hook) => hook?.hooks?.some?.((h) => h?.command === "agent-gauntlet stop-hook"));
7219
- if (hookExists) {
7220
- console.log(chalk9.dim("Stop hook already installed"));
7221
- return;
7413
+ const existingHooks = existing.hooks || {};
7414
+ const existingEntries = Array.isArray(existingHooks[hookKey]) ? existingHooks[hookKey] : [];
7415
+ if (hookHasCommand(existingEntries, deduplicateCmd)) {
7416
+ return false;
7222
7417
  }
7223
- const newStopHooks = [...existingStopHooks, ...STOP_HOOK_CONFIG.hooks.Stop];
7224
- const mergedSettings = {
7225
- ...existingSettings,
7418
+ const entryToAdd = wrapInHooksArray ? { hooks: [hookEntry] } : hookEntry;
7419
+ const newEntries = [...existingEntries, entryToAdd];
7420
+ const merged = {
7421
+ ...baseConfig ?? {},
7422
+ ...existing,
7226
7423
  hooks: {
7227
7424
  ...existingHooks,
7228
- Stop: newStopHooks
7425
+ [hookKey]: newEntries
7229
7426
  }
7230
7427
  };
7231
- await fs25.writeFile(settingsPath, `${JSON.stringify(mergedSettings, null, 2)}
7428
+ await fs26.writeFile(filePath, `${JSON.stringify(merged, null, 2)}
7232
7429
  `);
7233
- console.log(chalk9.green("Stop hook installed - gauntlet will run automatically when agent stops"));
7430
+ return true;
7234
7431
  }
7235
- async function installCursorStopHook(projectRoot) {
7236
- const cursorDir = path24.join(projectRoot, ".cursor");
7237
- const hooksPath = path24.join(cursorDir, "hooks.json");
7238
- await fs25.mkdir(cursorDir, { recursive: true });
7239
- let existingConfig = {};
7240
- if (await exists(hooksPath)) {
7241
- try {
7242
- const content = await fs25.readFile(hooksPath, "utf-8");
7243
- existingConfig = JSON.parse(content);
7244
- } catch {
7245
- existingConfig = {};
7432
+ var START_HOOK_ENTRY = {
7433
+ matcher: "startup|resume|clear|compact",
7434
+ hooks: [
7435
+ {
7436
+ type: "command",
7437
+ command: "agent-gauntlet start-hook",
7438
+ async: false
7246
7439
  }
7247
- }
7248
- const existingHooks = existingConfig.hooks || {};
7249
- const existingStopHooks = Array.isArray(existingHooks.stop) ? existingHooks.stop : [];
7250
- const hookExists = existingStopHooks.some((hook) => hook?.command === "agent-gauntlet stop-hook");
7251
- if (hookExists) {
7252
- console.log(chalk9.dim("Cursor stop hook already installed"));
7253
- return;
7254
- }
7255
- const newStopHooks = [
7256
- ...existingStopHooks,
7257
- ...CURSOR_STOP_HOOK_CONFIG.hooks.stop
7258
- ];
7259
- const mergedConfig = {
7260
- ...existingConfig,
7261
- version: existingConfig.version ?? CURSOR_STOP_HOOK_CONFIG.version,
7262
- hooks: {
7263
- ...existingHooks,
7264
- stop: newStopHooks
7440
+ ]
7441
+ };
7442
+ var CURSOR_START_HOOK_ENTRY = {
7443
+ command: "agent-gauntlet start-hook --adapter cursor"
7444
+ };
7445
+ var STOP_HOOK_ENTRY = {
7446
+ type: "command",
7447
+ command: "agent-gauntlet stop-hook",
7448
+ timeout: 300
7449
+ };
7450
+ var CURSOR_STOP_HOOK_ENTRY = {
7451
+ command: "agent-gauntlet stop-hook",
7452
+ loop_limit: 10
7453
+ };
7454
+ async function installHookWithLog(config, installedMsg, existsMsg) {
7455
+ const added = await mergeHookConfig(config);
7456
+ console.log(added ? chalk10.green(installedMsg) : chalk10.dim(existsMsg));
7457
+ }
7458
+ function buildHookSpec(target) {
7459
+ const { projectRoot, variant, kind } = target;
7460
+ const isCursor = variant === "cursor";
7461
+ const isStop = kind === "stop";
7462
+ const hookConfigs = {
7463
+ "claude-stop": {
7464
+ dir: ".claude",
7465
+ file: "settings.local.json",
7466
+ hookKey: "Stop",
7467
+ entry: STOP_HOOK_ENTRY,
7468
+ cmd: "agent-gauntlet stop-hook",
7469
+ wrap: true
7470
+ },
7471
+ "cursor-stop": {
7472
+ dir: ".cursor",
7473
+ file: "hooks.json",
7474
+ hookKey: "stop",
7475
+ entry: CURSOR_STOP_HOOK_ENTRY,
7476
+ cmd: "agent-gauntlet stop-hook",
7477
+ wrap: false
7478
+ },
7479
+ "claude-start": {
7480
+ dir: ".claude",
7481
+ file: "settings.local.json",
7482
+ hookKey: "SessionStart",
7483
+ entry: START_HOOK_ENTRY,
7484
+ cmd: "agent-gauntlet start-hook",
7485
+ wrap: false
7486
+ },
7487
+ "cursor-start": {
7488
+ dir: ".cursor",
7489
+ file: "hooks.json",
7490
+ hookKey: "sessionStart",
7491
+ entry: CURSOR_START_HOOK_ENTRY,
7492
+ cmd: "agent-gauntlet start-hook --adapter cursor",
7493
+ wrap: false
7265
7494
  }
7266
7495
  };
7267
- await fs25.writeFile(hooksPath, `${JSON.stringify(mergedConfig, null, 2)}
7268
- `);
7269
- console.log(chalk9.green("Cursor stop hook installed - gauntlet will run automatically when agent stops"));
7496
+ const key = `${variant}-${kind}`;
7497
+ const cfg = hookConfigs[key];
7498
+ const prefix = isCursor ? "Cursor " : "";
7499
+ const kindLabel = isCursor ? kind : isStop ? "Stop" : "Start";
7500
+ const purpose = isStop ? "gauntlet will run automatically when agent stops" : "agent will be primed with gauntlet instructions at session start";
7501
+ return {
7502
+ config: {
7503
+ filePath: path25.join(projectRoot, cfg.dir, cfg.file),
7504
+ hookKey: cfg.hookKey,
7505
+ hookEntry: cfg.entry,
7506
+ deduplicateCmd: cfg.cmd,
7507
+ wrapInHooksArray: cfg.wrap,
7508
+ ...isCursor ? { baseConfig: { version: 1 } } : {}
7509
+ },
7510
+ installedMsg: `${prefix}${kindLabel} hook installed - ${purpose}`,
7511
+ existsMsg: `${prefix}${kindLabel} hook already installed`
7512
+ };
7270
7513
  }
7271
7514
  // src/commands/list.ts
7272
- import chalk10 from "chalk";
7515
+ import chalk11 from "chalk";
7273
7516
  function registerListCommand(program) {
7274
7517
  program.command("list").description("List configured gates").action(async () => {
7275
7518
  try {
7276
7519
  const config = await loadConfig();
7277
- console.log(chalk10.bold("Check Gates:"));
7520
+ console.log(chalk11.bold("Check Gates:"));
7278
7521
  Object.values(config.checks).forEach((c) => {
7279
7522
  console.log(` - ${c.name}`);
7280
7523
  });
7281
- console.log(chalk10.bold(`
7524
+ console.log(chalk11.bold(`
7282
7525
  Review Gates:`));
7283
7526
  Object.values(config.reviews).forEach((r) => {
7284
7527
  console.log(` - ${r.name} (Tools: ${r.cli_preference?.join(", ")})`);
7285
7528
  });
7286
- console.log(chalk10.bold(`
7529
+ console.log(chalk11.bold(`
7287
7530
  Entry Points:`));
7288
7531
  config.project.entry_points.forEach((ep) => {
7289
7532
  console.log(` - ${ep.path}`);
@@ -7294,12 +7537,12 @@ Entry Points:`));
7294
7537
  });
7295
7538
  } catch (error) {
7296
7539
  const err = error;
7297
- console.error(chalk10.red("Error:"), err.message);
7540
+ console.error(chalk11.red("Error:"), err.message);
7298
7541
  }
7299
7542
  });
7300
7543
  }
7301
7544
  // src/commands/review.ts
7302
- import chalk11 from "chalk";
7545
+ import chalk12 from "chalk";
7303
7546
  function registerReviewCommand(program) {
7304
7547
  program.command("review").description("Run only applicable reviews for detected changes").option("-b, --base-branch <branch>", "Override base branch for change detection").option("-g, --gate <name>", "Run specific review gate only").option("-c, --commit <sha>", "Use diff for a specific commit").option("-u, --uncommitted", "Use diff for current uncommitted changes (staged and unstaged)").action(async (options) => {
7305
7548
  let config;
@@ -7321,7 +7564,7 @@ function registerReviewCommand(program) {
7321
7564
  const effectiveBaseBranch = options.baseBranch || (process.env.GITHUB_BASE_REF && (process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true") ? process.env.GITHUB_BASE_REF : null) || config.project.base_branch;
7322
7565
  const autoCleanResult = await shouldAutoClean(config.project.log_dir, effectiveBaseBranch);
7323
7566
  if (autoCleanResult.clean) {
7324
- console.log(chalk11.dim(`Auto-cleaning logs (${autoCleanResult.reason})...`));
7567
+ console.log(chalk12.dim(`Auto-cleaning logs (${autoCleanResult.reason})...`));
7325
7568
  await debugLogger?.logClean("auto", autoCleanResult.reason || "unknown");
7326
7569
  await performAutoClean(config.project.log_dir, autoCleanResult);
7327
7570
  }
@@ -7337,7 +7580,7 @@ function registerReviewCommand(program) {
7337
7580
  let changeOptions;
7338
7581
  let passedSlotsMap;
7339
7582
  if (isRerun) {
7340
- console.log(chalk11.dim("Existing logs detected — running in verification mode..."));
7583
+ console.log(chalk12.dim("Existing logs detected — running in verification mode..."));
7341
7584
  const { failures: previousFailures, passedSlots } = await findPreviousFailures(config.project.log_dir, options.gate, true);
7342
7585
  failuresMap = new Map;
7343
7586
  for (const gateFailure of previousFailures) {
@@ -7351,7 +7594,7 @@ function registerReviewCommand(program) {
7351
7594
  passedSlotsMap = passedSlots;
7352
7595
  if (previousFailures.length > 0) {
7353
7596
  const totalViolations = previousFailures.reduce((sum, gf) => sum + gf.adapterFailures.reduce((s, af) => s + af.violations.length, 0), 0);
7354
- console.log(chalk11.yellow(`Found ${previousFailures.length} gate(s) with ${totalViolations} previous violation(s)`));
7597
+ console.log(chalk12.yellow(`Found ${previousFailures.length} gate(s) with ${totalViolations} previous violation(s)`));
7355
7598
  }
7356
7599
  changeOptions = { uncommitted: true };
7357
7600
  const executionState = await readExecutionState(config.project.log_dir);
@@ -7363,7 +7606,7 @@ function registerReviewCommand(program) {
7363
7606
  if (executionState) {
7364
7607
  const resolved = await resolveFixBase(executionState, effectiveBaseBranch);
7365
7608
  if (resolved.warning) {
7366
- console.log(chalk11.yellow(`Warning: ${resolved.warning}`));
7609
+ console.log(chalk12.yellow(`Warning: ${resolved.warning}`));
7367
7610
  }
7368
7611
  if (resolved.fixBase) {
7369
7612
  changeOptions = { fixBase: resolved.fixBase };
@@ -7383,16 +7626,16 @@ function registerReviewCommand(program) {
7383
7626
  });
7384
7627
  const expander = new EntryPointExpander;
7385
7628
  const jobGen = new JobGenerator(config);
7386
- console.log(chalk11.dim("Detecting changes..."));
7629
+ console.log(chalk12.dim("Detecting changes..."));
7387
7630
  const changes = await changeDetector.getChangedFiles();
7388
7631
  if (changes.length === 0) {
7389
- console.log(chalk11.green("No changes detected."));
7632
+ console.log(chalk12.green("No changes detected."));
7390
7633
  await writeExecutionState(config.project.log_dir);
7391
7634
  await releaseLock(config.project.log_dir);
7392
7635
  restoreConsole?.restore();
7393
7636
  process.exit(0);
7394
7637
  }
7395
- console.log(chalk11.dim(`Found ${changes.length} changed files.`));
7638
+ console.log(chalk12.dim(`Found ${changes.length} changed files.`));
7396
7639
  const entryPoints = await expander.expand(config.project.entry_points, changes);
7397
7640
  let jobs = jobGen.generateJobs(entryPoints);
7398
7641
  jobs = jobs.filter((j) => j.type === "review");
@@ -7400,13 +7643,13 @@ function registerReviewCommand(program) {
7400
7643
  jobs = jobs.filter((j) => j.name === options.gate);
7401
7644
  }
7402
7645
  if (jobs.length === 0) {
7403
- console.log(chalk11.yellow("No applicable reviews for these changes."));
7646
+ console.log(chalk12.yellow("No applicable reviews for these changes."));
7404
7647
  await writeExecutionState(config.project.log_dir);
7405
7648
  await releaseLock(config.project.log_dir);
7406
7649
  restoreConsole?.restore();
7407
7650
  process.exit(0);
7408
7651
  }
7409
- console.log(chalk11.dim(`Running ${jobs.length} review(s)...`));
7652
+ console.log(chalk12.dim(`Running ${jobs.length} review(s)...`));
7410
7653
  const runMode = isRerun ? "verification" : "full";
7411
7654
  await debugLogger?.logRunStart(runMode, changes.length, jobs.length);
7412
7655
  const reporter = new ConsoleReporter;
@@ -7429,15 +7672,15 @@ function registerReviewCommand(program) {
7429
7672
  await releaseLock(config.project.log_dir);
7430
7673
  }
7431
7674
  const err = error;
7432
- console.error(chalk11.red("Error:"), err.message);
7675
+ console.error(chalk12.red("Error:"), err.message);
7433
7676
  restoreConsole?.restore();
7434
7677
  process.exit(1);
7435
7678
  }
7436
7679
  });
7437
7680
  }
7438
7681
  // src/core/run-executor.ts
7439
- import fs26 from "node:fs/promises";
7440
- import path25 from "node:path";
7682
+ import fs27 from "node:fs/promises";
7683
+ import path26 from "node:path";
7441
7684
 
7442
7685
  // src/core/diff-stats.ts
7443
7686
  import { execFile as execFile2 } from "node:child_process";
@@ -7746,25 +7989,25 @@ function isProcessAlive(pid) {
7746
7989
  }
7747
7990
  }
7748
7991
  async function tryAcquireLock(logDir) {
7749
- await fs26.mkdir(logDir, { recursive: true });
7750
- const lockPath = path25.resolve(logDir, LOCK_FILENAME2);
7992
+ await fs27.mkdir(logDir, { recursive: true });
7993
+ const lockPath = path26.resolve(logDir, LOCK_FILENAME2);
7751
7994
  try {
7752
- await fs26.writeFile(lockPath, String(process.pid), { flag: "wx" });
7995
+ await fs27.writeFile(lockPath, String(process.pid), { flag: "wx" });
7753
7996
  return true;
7754
7997
  } catch (err) {
7755
7998
  if (typeof err === "object" && err !== null && "code" in err && err.code === "EEXIST") {
7756
7999
  try {
7757
- const lockContent = await fs26.readFile(lockPath, "utf-8");
8000
+ const lockContent = await fs27.readFile(lockPath, "utf-8");
7758
8001
  const lockPid = parseInt(lockContent.trim(), 10);
7759
- const lockStat = await fs26.stat(lockPath);
8002
+ const lockStat = await fs27.stat(lockPath);
7760
8003
  const lockAgeMs = Date.now() - lockStat.mtimeMs;
7761
8004
  const pidValid = !Number.isNaN(lockPid);
7762
8005
  const pidDead = pidValid && !isProcessAlive(lockPid);
7763
8006
  const lockStale = !pidValid && lockAgeMs > STALE_LOCK_MS;
7764
8007
  if (pidDead || lockStale) {
7765
- await fs26.rm(lockPath, { force: true });
8008
+ await fs27.rm(lockPath, { force: true });
7766
8009
  try {
7767
- await fs26.writeFile(lockPath, String(process.pid), {
8010
+ await fs27.writeFile(lockPath, String(process.pid), {
7768
8011
  flag: "wx"
7769
8012
  });
7770
8013
  return true;
@@ -7780,7 +8023,7 @@ async function tryAcquireLock(logDir) {
7780
8023
  }
7781
8024
  async function findLatestConsoleLog(logDir) {
7782
8025
  try {
7783
- const files = await fs26.readdir(logDir);
8026
+ const files = await fs27.readdir(logDir);
7784
8027
  let maxNum = -1;
7785
8028
  let latestFile = null;
7786
8029
  for (const file of files) {
@@ -7796,7 +8039,7 @@ async function findLatestConsoleLog(logDir) {
7796
8039
  }
7797
8040
  }
7798
8041
  }
7799
- return latestFile ? path25.join(logDir, latestFile) : null;
8042
+ return latestFile ? path26.join(logDir, latestFile) : null;
7800
8043
  } catch {
7801
8044
  return null;
7802
8045
  }
@@ -7828,7 +8071,7 @@ var statusMessages = {
7828
8071
  loop_detected: "Loop detected — rapid blocks overridden.",
7829
8072
  interval_not_elapsed: "Run interval not elapsed.",
7830
8073
  invalid_input: "Invalid input.",
7831
- stop_hook_disabled: "Stop hook is disabled via configuration.",
8074
+ stop_hook_disabled: "",
7832
8075
  pr_push_required: "Gates passed — PR needs to be created/updated.",
7833
8076
  ci_pending: "CI checks still running.",
7834
8077
  ci_failed: "CI checks failed or review changes requested.",
@@ -8076,17 +8319,90 @@ function registerRunCommand(program) {
8076
8319
  process.exit(isSuccessStatus(result.status) ? 0 : 1);
8077
8320
  });
8078
8321
  }
8322
+ // src/commands/start-hook.ts
8323
+ import fs28 from "node:fs/promises";
8324
+ import path27 from "node:path";
8325
+ import YAML7 from "yaml";
8326
+ var START_HOOK_MESSAGE = `<IMPORTANT>
8327
+ This project uses Agent Gauntlet for automated quality verification.
8328
+
8329
+ BEFORE reporting any coding task as complete, you MUST run /gauntlet-run
8330
+ to verify your changes pass all quality gates.
8331
+
8332
+ Run /gauntlet-run when ALL of these are true:
8333
+ 1. You completed a coding task (implemented a feature, fixed a bug,
8334
+ refactored code, or made other substantive code changes)
8335
+ 2. You are about to report the work as done, complete, or fixed
8336
+
8337
+ Do NOT run /gauntlet-run when:
8338
+ - You only answered questions, explored code, or ran read-only commands
8339
+ - You are in the middle of a multi-step task (run it at the end, not
8340
+ after every individual change)
8341
+ - The user explicitly asked to skip verification
8342
+
8343
+ If you are unsure whether to run it, run it. False positives (running
8344
+ unnecessarily) are far less costly than false negatives (skipping
8345
+ verification on code that needed it).
8346
+ </IMPORTANT>`;
8347
+ function formatClaudeOutput(message) {
8348
+ return JSON.stringify({
8349
+ hookSpecificOutput: {
8350
+ hookEventName: "SessionStart",
8351
+ additionalContext: message
8352
+ }
8353
+ });
8354
+ }
8355
+ function formatCursorOutput(message) {
8356
+ return message;
8357
+ }
8358
+ function isValidConfig(content) {
8359
+ const trimmed = content.trim();
8360
+ if (!trimmed) {
8361
+ return false;
8362
+ }
8363
+ try {
8364
+ const parsed = YAML7.parse(trimmed);
8365
+ return parsed != null && typeof parsed === "object";
8366
+ } catch {
8367
+ return false;
8368
+ }
8369
+ }
8370
+ function registerStartHookCommand(program) {
8371
+ 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) => {
8372
+ const configPath = path27.join(process.cwd(), ".gauntlet", "config.yml");
8373
+ try {
8374
+ const content = await fs28.readFile(configPath, "utf-8");
8375
+ if (!isValidConfig(content)) {
8376
+ return;
8377
+ }
8378
+ } catch {
8379
+ return;
8380
+ }
8381
+ const adapter = options.adapter;
8382
+ try {
8383
+ const cwd = process.cwd();
8384
+ const logDir = path27.join(cwd, await getLogDir(cwd));
8385
+ const globalConfig = await loadGlobalConfig();
8386
+ const projectDebugLogConfig = await getDebugLogConfig(cwd);
8387
+ const debugLogConfig = mergeDebugLogConfig(projectDebugLogConfig, globalConfig.debug_log);
8388
+ const debugLogger = new DebugLogger(logDir, debugLogConfig);
8389
+ await debugLogger.logStartHook(adapter);
8390
+ } catch {}
8391
+ const output = adapter === "cursor" ? formatCursorOutput(START_HOOK_MESSAGE) : formatClaudeOutput(START_HOOK_MESSAGE);
8392
+ console.log(output);
8393
+ });
8394
+ }
8079
8395
  // src/commands/validate.ts
8080
- import chalk12 from "chalk";
8396
+ import chalk13 from "chalk";
8081
8397
  function registerValidateCommand(program) {
8082
8398
  program.command("validate").description("Validate .gauntlet/ config files against schemas").action(async () => {
8083
8399
  try {
8084
8400
  await loadConfig();
8085
- console.log(chalk12.green("All config files are valid."));
8401
+ console.log(chalk13.green("All config files are valid."));
8086
8402
  process.exitCode = 0;
8087
8403
  } catch (error) {
8088
8404
  const message = error instanceof Error ? error.message : String(error);
8089
- console.error(chalk12.red("Validation failed:"), message);
8405
+ console.error(chalk13.red("Validation failed:"), message);
8090
8406
  process.exitCode = 1;
8091
8407
  }
8092
8408
  });
@@ -8369,6 +8685,7 @@ registerListCommand(program);
8369
8685
  registerHealthCommand(program);
8370
8686
  registerInitCommand(program);
8371
8687
  registerValidateCommand(program);
8688
+ registerStartHookCommand(program);
8372
8689
  registerStopHookCommand(program);
8373
8690
  registerWaitCICommand(program);
8374
8691
  registerHelpCommand(program);
@@ -8377,4 +8694,4 @@ if (process.argv.length < 3) {
8377
8694
  }
8378
8695
  program.parse(process.argv);
8379
8696
 
8380
- //# debugId=61E76D84AB3F58F264756E2164756E21
8697
+ //# debugId=27272999504866CC64756E2164756E21