@codacy/verity-cli 0.22.0 → 0.23.1

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/bin/verity.js CHANGED
@@ -10648,6 +10648,10 @@ async function apiRequest(options) {
10648
10648
  if (token) {
10649
10649
  headers["Authorization"] = `Bearer ${token}`;
10650
10650
  }
10651
+ const testMockScenario = process.env.VERITY_TEST_MOCK_SCENARIO;
10652
+ if (testMockScenario) {
10653
+ headers["X-Verity-Mock-Scenario"] = testMockScenario;
10654
+ }
10651
10655
  printVerbose(`${method} ${url}`, verbose);
10652
10656
  let serializedBody;
10653
10657
  if (body !== void 0 && body !== null) {
@@ -10719,6 +10723,20 @@ async function apiRequest(options) {
10719
10723
  });
10720
10724
  return { ok: true, data };
10721
10725
  }
10726
+ function analyzeRequest(options) {
10727
+ return apiRequest({
10728
+ method: "POST",
10729
+ path: "/analyze",
10730
+ serviceUrl: options.serviceUrl,
10731
+ token: options.token,
10732
+ body: options.body,
10733
+ timeout: options.timeout,
10734
+ cmd: options.cmd,
10735
+ verbose: options.verbose,
10736
+ retry: options.retry,
10737
+ encodeBody: true
10738
+ });
10739
+ }
10722
10740
 
10723
10741
  // src/commands/auth.ts
10724
10742
  function registerAuthCommands(program2) {
@@ -10830,8 +10848,21 @@ var VERITY_INTENT_HOOK = {
10830
10848
  type: "command",
10831
10849
  command: "verity intent capture"
10832
10850
  };
10851
+ var GUARD_TIMEOUT = 300;
10852
+ function buildGuardHook(on) {
10853
+ return {
10854
+ type: "command",
10855
+ command: `verity guard --on ${on.join(",")}`,
10856
+ timeout: GUARD_TIMEOUT,
10857
+ statusMessage: "Running Verity git-moment review"
10858
+ };
10859
+ }
10833
10860
  var VERITY_STOP_RE = /(?:^|[\/\s"'])verity\s+analyze\b/;
10834
10861
  var VERITY_INTENT_RE = /(?:^|[\/\s"'])verity\s+intent\s+capture\b/;
10862
+ var VERITY_GUARD_RE = /(?:^|[\/\s"'])verity\s+guard\b/;
10863
+ function isVerityGuardHook(entry) {
10864
+ return VERITY_GUARD_RE.test(entry.command ?? "");
10865
+ }
10835
10866
  var LEGACY_STOP_RE = /(?:^|[\/\s"'])gate\s+analyze\b/;
10836
10867
  var LEGACY_INTENT_RE = /(?:^|[\/\s"'])gate\s+intent\s+capture\b/;
10837
10868
  function isVerityStopHook(entry) {
@@ -10843,7 +10874,7 @@ function isVerityIntentHook(entry) {
10843
10874
  return VERITY_INTENT_RE.test(c) || LEGACY_INTENT_RE.test(c) || c.includes(".verity/hooks/capture-intent.sh") || c.includes(".gate/hooks/capture-intent.sh");
10844
10875
  }
10845
10876
  function isVerityHook(entry) {
10846
- return isVerityStopHook(entry) || isVerityIntentHook(entry);
10877
+ return isVerityStopHook(entry) || isVerityIntentHook(entry) || isVerityGuardHook(entry);
10847
10878
  }
10848
10879
  function isCurrentVerityStopHook(entry) {
10849
10880
  const c = entry.command ?? "";
@@ -10854,7 +10885,7 @@ function isCurrentVerityIntentHook(entry) {
10854
10885
  return VERITY_INTENT_RE.test(c) || c.includes(".verity/hooks/capture-intent.sh");
10855
10886
  }
10856
10887
  function isCurrentVerityHook(entry) {
10857
- return isCurrentVerityStopHook(entry) || isCurrentVerityIntentHook(entry);
10888
+ return isCurrentVerityStopHook(entry) || isCurrentVerityIntentHook(entry) || isVerityGuardHook(entry);
10858
10889
  }
10859
10890
  function settingsHasLegacyHook(settings) {
10860
10891
  for (const groups of Object.values(settings.hooks ?? {})) {
@@ -10894,12 +10925,16 @@ async function readAllSettings() {
10894
10925
  async function checkAllVerityHooks() {
10895
10926
  let stop = false;
10896
10927
  let intent = false;
10928
+ let guard = false;
10929
+ let guardOn = [];
10897
10930
  for (const settings of await readAllSettings()) {
10898
10931
  const r = checkVerityHooks(settings);
10899
10932
  stop = stop || r.stop;
10900
10933
  intent = intent || r.intent;
10934
+ guard = guard || r.guard;
10935
+ if (r.guardOn.length > guardOn.length) guardOn = r.guardOn;
10901
10936
  }
10902
- return { stop, intent };
10937
+ return { stop, intent, guard, guardOn };
10903
10938
  }
10904
10939
  async function checkAllVerityHooksDetailed() {
10905
10940
  let stop = false;
@@ -10962,7 +10997,19 @@ function checkVerityHooks(settings) {
10962
10997
  const hasIntent = intentGroups.some(
10963
10998
  (g) => g.hooks?.some((h) => isCurrentVerityIntentHook(h))
10964
10999
  );
10965
- return { stop: hasStop, intent: hasIntent };
11000
+ let guard = false;
11001
+ let guardOn = [];
11002
+ for (const g of hooks["PreToolUse"] ?? []) {
11003
+ for (const h of g.hooks ?? []) {
11004
+ if (!isVerityGuardHook(h)) continue;
11005
+ guard = true;
11006
+ const m = (h.command ?? "").match(/--on[=\s]+([a-z,]+)/);
11007
+ if (m) {
11008
+ guardOn = m[1].split(",").filter((x) => x === "commit" || x === "push");
11009
+ }
11010
+ }
11011
+ }
11012
+ return { stop: hasStop, intent: hasIntent, guard, guardOn };
10966
11013
  }
10967
11014
  function installVerityHooks(settings, force, externalPresent = { stop: false, intent: false }) {
10968
11015
  const local = checkVerityHooks(settings);
@@ -11011,6 +11058,7 @@ function installVerityHooks(settings, force, externalPresent = { stop: false, in
11011
11058
  function removeVerityHooks(settings) {
11012
11059
  const newSettings = { ...settings };
11013
11060
  if (!newSettings.hooks) return newSettings;
11061
+ newSettings.hooks = { ...newSettings.hooks };
11014
11062
  for (const key of Object.keys(newSettings.hooks)) {
11015
11063
  const groups = newSettings.hooks[key];
11016
11064
  if (!groups) continue;
@@ -11027,12 +11075,60 @@ function removeVerityHooks(settings) {
11027
11075
  }
11028
11076
  return newSettings;
11029
11077
  }
11078
+ function reconcileMomentHooks(settings, moments, externalPresent = {
11079
+ stop: false,
11080
+ intent: false,
11081
+ guardOn: []
11082
+ }) {
11083
+ const s = removeVerityHooks(settings);
11084
+ if (!s.hooks) s.hooks = {};
11085
+ const push = (event, group) => {
11086
+ (s.hooks[event] ??= []).push(group);
11087
+ };
11088
+ if (!externalPresent.intent) push("UserPromptSubmit", { hooks: [VERITY_INTENT_HOOK] });
11089
+ if (moments.includes("stop") && !externalPresent.stop) {
11090
+ push("Stop", { hooks: [VERITY_STOP_HOOK] });
11091
+ }
11092
+ const on = [];
11093
+ if (moments.includes("pre-commit")) on.push("commit");
11094
+ if (moments.includes("pre-push")) on.push("push");
11095
+ const remaining = on.filter((m) => !externalPresent.guardOn.includes(m));
11096
+ if (remaining.length > 0) {
11097
+ push("PreToolUse", { matcher: "Bash", hooks: [buildGuardHook(remaining)] });
11098
+ }
11099
+ for (const k of Object.keys(s.hooks)) {
11100
+ if (s.hooks[k].length === 0) delete s.hooks[k];
11101
+ }
11102
+ if (Object.keys(s.hooks).length === 0) delete s.hooks;
11103
+ return s;
11104
+ }
11030
11105
 
11031
11106
  // src/commands/hooks.ts
11107
+ var ALL_MOMENTS = ["stop", "pre-commit", "pre-push"];
11108
+ function parseMoments(raw) {
11109
+ const seen = /* @__PURE__ */ new Set();
11110
+ for (const part of raw.split(",").map((s) => s.trim()).filter(Boolean)) {
11111
+ if (ALL_MOMENTS.includes(part)) seen.add(part);
11112
+ }
11113
+ return ALL_MOMENTS.filter((m) => seen.has(m));
11114
+ }
11032
11115
  function registerHooksCommands(program2) {
11033
11116
  const hooks = program2.command("hooks").description("Manage Claude Code hook wiring");
11034
- hooks.command("install").description("Install Verity hooks into Claude Code settings").option("--force", "Overwrite existing Verity hooks").action(async (opts) => {
11117
+ hooks.command("install").description("Install Verity hooks into Claude Code settings").option("--force", "Overwrite existing Verity hooks").option("--moments <list>", "Reconcile to exactly these moments: stop,pre-commit,pre-push").action(async (opts) => {
11035
11118
  const force = opts.force ?? false;
11119
+ if (opts.moments != null) {
11120
+ const moments = parseMoments(opts.moments);
11121
+ const external = await checkAllVerityHooks();
11122
+ const settings2 = reconcileMomentHooks(await readSettings(), moments, external);
11123
+ await writeSettings(settings2);
11124
+ const status = await checkAllVerityHooks();
11125
+ printInfo("Verity hooks reconciled in .claude/settings.json:");
11126
+ printInfo(` Stop (verity analyze): ${moments.includes("stop") ? "on" : "off"}`);
11127
+ printInfo(` Pre-commit gate: ${status.guardOn.includes("commit") ? "on" : "off"}`);
11128
+ printInfo(` Pre-push/PR gate: ${status.guardOn.includes("push") ? "on" : "off"}`);
11129
+ printInfo(" UserPromptSubmit (verity intent capture): on");
11130
+ return;
11131
+ }
11036
11132
  const detail = await checkAllVerityHooksDetailed();
11037
11133
  const present = { stop: detail.stop, intent: detail.intent };
11038
11134
  if (detail.legacyOnly) {
@@ -11070,7 +11166,12 @@ function registerHooksCommands(program2) {
11070
11166
  const status = await checkAllVerityHooks();
11071
11167
  printInfo(`Stop hook (verity analyze): ${status.stop ? "installed" : "not installed"}`);
11072
11168
  printInfo(`Intent hook (verity intent capture): ${status.intent ? "installed" : "not installed"}`);
11073
- if (!status.stop || !status.intent) {
11169
+ const gates = status.guard ? status.guardOn.join(", ") : "none";
11170
+ printInfo(`Git-moment gate (verity guard): ${status.guard ? `installed [${gates}]` : "not installed"}`);
11171
+ if (!status.stop && !status.guard) {
11172
+ printWarn('No analysis moment is active (neither Stop nor a git-moment gate) \u2014 code changes will not be reviewed. Run "verity hooks install --moments stop" (or /verity-setup) to enable one.');
11173
+ }
11174
+ if (!status.intent) {
11074
11175
  printWarn('Run "verity hooks install" to install missing hooks.');
11075
11176
  }
11076
11177
  });
@@ -12437,6 +12538,12 @@ function registerStatusCommand(program2) {
12437
12538
  printInfo(`Standard: v${s.version} (${s.quality_dimensions} quality, ${s.security_patterns} security, ${s.custom_patterns} custom)`);
12438
12539
  printInfo(`Languages: ${s.languages.join(", ")}`);
12439
12540
  }
12541
+ const hookStatus = await checkAllVerityHooks();
12542
+ const moments = [];
12543
+ if (hookStatus.stop) moments.push("stop");
12544
+ if (hookStatus.guardOn.includes("commit")) moments.push("pre-commit");
12545
+ if (hookStatus.guardOn.includes("push")) moments.push("pre-push/PR");
12546
+ printInfo(`Moments: ${moments.length > 0 ? moments.join(", ") : "none (run /verity-setup)"}`);
12440
12547
  if (mem.recent_runs) {
12441
12548
  const r = mem.recent_runs;
12442
12549
  printInfo("");
@@ -12677,6 +12784,31 @@ function getChangedFiles() {
12677
12784
  const filtered = Array.from(sets).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
12678
12785
  return { files: filtered, hasRecentCommitFiles };
12679
12786
  }
12787
+ function getStagedFiles() {
12788
+ return splitLines(execGit("git diff --cached --name-only")).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
12789
+ }
12790
+ function getPushRangeFiles() {
12791
+ const diff = (range) => splitLines(execGit(`git diff --name-only ${range}`)).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
12792
+ const resolvers = [
12793
+ () => execGit("git rev-parse --abbrev-ref --symbolic-full-name @{push}") ? "@{push}..HEAD" : null,
12794
+ () => execGit("git rev-parse --abbrev-ref --symbolic-full-name @{upstream}") ? "@{upstream}..HEAD" : null,
12795
+ () => {
12796
+ const branch = execGit("git rev-parse --abbrev-ref HEAD");
12797
+ return branch && branch !== "HEAD" && execGit(`git rev-parse --verify -q origin/${branch}`) ? `origin/${branch}..HEAD` : null;
12798
+ }
12799
+ ];
12800
+ for (const resolve of resolvers) {
12801
+ const range = resolve();
12802
+ if (range) return { files: diff(range), range };
12803
+ }
12804
+ const baseline = readBaselineSha();
12805
+ if (baseline) {
12806
+ const files = diff(`${baseline}..HEAD`);
12807
+ if (files.length > 0) return { files, range: `${baseline}..HEAD` };
12808
+ }
12809
+ const last = diff("HEAD~1..HEAD");
12810
+ return { files: last, range: last.length > 0 ? "HEAD~1..HEAD" : null };
12811
+ }
12680
12812
  function getWorktreeFiles() {
12681
12813
  const result = [];
12682
12814
  const worktreeDir = ".claude/worktrees";
@@ -14431,16 +14563,13 @@ async function runAnalyze(opts, globals) {
14431
14563
  requestBody.intent_context = intentContext;
14432
14564
  }
14433
14565
  const ANALYZE_TIMEOUT_MS = 1e5;
14434
- let result = await apiRequest({
14435
- method: "POST",
14436
- path: "/analyze",
14566
+ let result = await analyzeRequest({
14437
14567
  serviceUrl: urlResult.data,
14438
14568
  token: tokenResult.data.token,
14439
14569
  body: requestBody,
14440
14570
  verbose: globals.verbose,
14441
14571
  timeout: ANALYZE_TIMEOUT_MS,
14442
- cmd: "analyze",
14443
- encodeBody: true
14572
+ cmd: "analyze"
14444
14573
  });
14445
14574
  if (!result.ok && shouldWarmRetryAnalyze(result)) {
14446
14575
  logEvent("analyze_warm_retry", {
@@ -14457,17 +14586,14 @@ async function runAnalyze(opts, globals) {
14457
14586
  timeout: 15e3,
14458
14587
  cmd: "analyze_warmup"
14459
14588
  });
14460
- result = await apiRequest({
14461
- method: "POST",
14462
- path: "/analyze",
14589
+ result = await analyzeRequest({
14463
14590
  serviceUrl: urlResult.data,
14464
14591
  token: tokenResult.data.token,
14465
14592
  body: requestBody,
14466
14593
  verbose: globals.verbose,
14467
14594
  timeout: ANALYZE_TIMEOUT_MS,
14468
14595
  cmd: "analyze",
14469
- retry: true,
14470
- encodeBody: true
14596
+ retry: true
14471
14597
  });
14472
14598
  }
14473
14599
  if (!result.ok) {
@@ -14728,8 +14854,8 @@ async function runReview(opts, globals) {
14728
14854
  for (const p of specPaths) {
14729
14855
  if (!(0, import_node_fs19.existsSync)(p)) continue;
14730
14856
  try {
14731
- const { readFileSync: readFileSync10 } = await import("node:fs");
14732
- const content = readFileSync10(p, "utf-8");
14857
+ const { readFileSync: readFileSync11 } = await import("node:fs");
14858
+ const content = readFileSync11(p, "utf-8");
14733
14859
  specs.push({ path: p, content: content.slice(0, 10240) });
14734
14860
  } catch {
14735
14861
  }
@@ -14759,14 +14885,13 @@ async function runReview(opts, globals) {
14759
14885
  if (plans.length > 0) intentContext.plans = plans;
14760
14886
  requestBody.intent_context = intentContext;
14761
14887
  }
14762
- const result = await apiRequest({
14763
- method: "POST",
14764
- path: "/analyze",
14888
+ const result = await analyzeRequest({
14765
14889
  serviceUrl: urlResult.data,
14766
14890
  token: tokenResult.data.token,
14767
14891
  body: requestBody,
14768
14892
  verbose: globals.verbose,
14769
- timeout: 12e4
14893
+ timeout: 12e4,
14894
+ cmd: "review"
14770
14895
  });
14771
14896
  if (!result.ok) {
14772
14897
  printError(`Service error: ${result.error}`);
@@ -14786,15 +14911,270 @@ async function runReview(opts, globals) {
14786
14911
  process.exit(0);
14787
14912
  }
14788
14913
 
14914
+ // src/commands/guard.ts
14915
+ var import_node_fs20 = require("node:fs");
14916
+ var import_node_path14 = require("node:path");
14917
+ var GUARD_BLOCK_CAP = 2;
14918
+ var GUARD_ITER_FILE = (0, import_node_path14.join)(VERITY_DIR, ".guard-iteration");
14919
+ function readPreToolUseStdin() {
14920
+ const empty = { command: "", cwd: null, sessionId: null };
14921
+ return new Promise((resolve) => {
14922
+ try {
14923
+ if (process.stdin.isTTY) return resolve(empty);
14924
+ const chunks = [];
14925
+ let timer;
14926
+ let settled = false;
14927
+ const onData = (c) => chunks.push(c);
14928
+ const settle = (value) => {
14929
+ if (settled) return;
14930
+ settled = true;
14931
+ if (timer) clearTimeout(timer);
14932
+ process.stdin.removeListener("data", onData);
14933
+ process.stdin.removeListener("end", onEnd);
14934
+ process.stdin.removeListener("error", onError);
14935
+ process.stdin.pause();
14936
+ resolve(value);
14937
+ };
14938
+ const onEnd = () => {
14939
+ try {
14940
+ const data = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
14941
+ settle({
14942
+ command: typeof data?.tool_input?.command === "string" ? data.tool_input.command : "",
14943
+ cwd: typeof data?.cwd === "string" ? data.cwd : null,
14944
+ sessionId: typeof data?.session_id === "string" ? data.session_id : null
14945
+ });
14946
+ } catch {
14947
+ settle(empty);
14948
+ }
14949
+ };
14950
+ const onError = () => settle(empty);
14951
+ timer = setTimeout(() => settle(empty), 2e3);
14952
+ process.stdin.on("data", onData);
14953
+ process.stdin.on("end", onEnd);
14954
+ process.stdin.on("error", onError);
14955
+ process.stdin.resume();
14956
+ } catch {
14957
+ resolve(empty);
14958
+ }
14959
+ });
14960
+ }
14961
+ function buildCommandRe(head) {
14962
+ return new RegExp(`(?:^|[\\s;&|(])${head}|(?:^|[;&|(])\\s*[^\\s;&|()'"]*\\/${head}`);
14963
+ }
14964
+ var GIT_GLOBAL_OPTS = "(?:\\s+(?:-[Cc]\\s+\\S+|--?[\\w-]+(?:=\\S+)?))*";
14965
+ var COMMIT_RE = buildCommandRe(`git${GIT_GLOBAL_OPTS}\\s+commit(?![\\w-])`);
14966
+ var PUSH_RE = buildCommandRe(`git${GIT_GLOBAL_OPTS}\\s+push\\b`);
14967
+ var GH_PR_RE = buildCommandRe(`gh${GIT_GLOBAL_OPTS}\\s+pr\\s+create\\b`);
14968
+ function classifyCommand(command, on) {
14969
+ let commit = false;
14970
+ let push = false;
14971
+ for (const seg of (command ?? "").split(/&&|\|\||;|\n/)) {
14972
+ if (/--dry-run\b/.test(seg)) continue;
14973
+ if (COMMIT_RE.test(seg)) commit = true;
14974
+ if (PUSH_RE.test(seg) || GH_PR_RE.test(seg)) push = true;
14975
+ }
14976
+ if (commit && on.includes("commit")) return "pre-commit";
14977
+ if (push && on.includes("push")) return "pre-push";
14978
+ return null;
14979
+ }
14980
+ function readIterMap() {
14981
+ try {
14982
+ const raw = JSON.parse((0, import_node_fs20.readFileSync)(GUARD_ITER_FILE, "utf-8"));
14983
+ if (raw && typeof raw === "object") {
14984
+ if (typeof raw.moment === "string" && typeof raw.count === "number") {
14985
+ return { [raw.moment]: raw.count };
14986
+ }
14987
+ const out = {};
14988
+ for (const m of ["pre-commit", "pre-push"]) {
14989
+ if (typeof raw[m] === "number") out[m] = raw[m];
14990
+ }
14991
+ return out;
14992
+ }
14993
+ } catch {
14994
+ }
14995
+ return {};
14996
+ }
14997
+ function readIter(moment) {
14998
+ return readIterMap()[moment] ?? 0;
14999
+ }
15000
+ function writeIter(moment, count) {
15001
+ try {
15002
+ (0, import_node_fs20.mkdirSync)(VERITY_DIR, { recursive: true });
15003
+ const map = readIterMap();
15004
+ map[moment] = count;
15005
+ (0, import_node_fs20.writeFileSync)(GUARD_ITER_FILE, JSON.stringify(map));
15006
+ } catch {
15007
+ }
15008
+ }
15009
+ function resetIter(moment) {
15010
+ try {
15011
+ const map = readIterMap();
15012
+ if (!(moment in map)) return;
15013
+ delete map[moment];
15014
+ if (Object.keys(map).length === 0) {
15015
+ if ((0, import_node_fs20.existsSync)(GUARD_ITER_FILE)) (0, import_node_fs20.unlinkSync)(GUARD_ITER_FILE);
15016
+ } else {
15017
+ (0, import_node_fs20.mkdirSync)(VERITY_DIR, { recursive: true });
15018
+ (0, import_node_fs20.writeFileSync)(GUARD_ITER_FILE, JSON.stringify(map));
15019
+ }
15020
+ } catch {
15021
+ }
15022
+ }
15023
+ function registerGuardCommand(program2) {
15024
+ program2.command("guard").description("Git-moment gate: review staged/to-push changes before a commit/push (PreToolUse hook)").option("--on <moments>", "Which git moments to gate: commit,push", "commit,push").option("--json", "Output raw JSON response (debug)").action(async (opts) => {
15025
+ const globals = program2.opts();
15026
+ try {
15027
+ await runGuard(opts, globals);
15028
+ } catch {
15029
+ process.exit(0);
15030
+ }
15031
+ });
15032
+ }
15033
+ function getMomentFiles(moment) {
15034
+ return moment === "pre-commit" ? getStagedFiles() : getPushRangeFiles().files;
15035
+ }
15036
+ function buildGuardRequest(moment, files, iter, sessionId) {
15037
+ const analyzable = filterAnalyzable(files);
15038
+ const securityFiles = filterSecurity(files);
15039
+ let staticResults;
15040
+ if (isCodacyAvailable()) {
15041
+ const scannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles])).map((f) => (0, import_node_fs20.existsSync)(f) ? f : resolveFile(f)).filter((f) => f !== null);
15042
+ staticResults = runCodacyAnalysis(scannable);
15043
+ } else {
15044
+ staticResults = { tool: "@codacy/analysis-cli", findings: [], summary: { total_findings: 0, by_severity: {}, tools_run: [] } };
15045
+ }
15046
+ const codeDelta = collectCodeDelta(files);
15047
+ if (codeDelta.total_files === 0) return null;
15048
+ const trigger = moment === "pre-commit" ? "hook:pre-commit" : "hook:pre-push";
15049
+ const requestBody = {
15050
+ static_results: staticResults,
15051
+ code_delta: codeDelta,
15052
+ changed_files: files,
15053
+ standard_version: "latest",
15054
+ context: {
15055
+ agent_model: process.env.CLAUDE_MODEL ?? "unknown",
15056
+ agent_harness: "claude-code",
15057
+ // Prefer the session id Claude Code passes on the hook payload; fall back
15058
+ // to the env var, then 'unknown'.
15059
+ session_id: sessionId ?? process.env.CLAUDE_SESSION_ID ?? "unknown",
15060
+ trigger,
15061
+ iteration: iter + 1
15062
+ }
15063
+ };
15064
+ const specs = discoverSpecs();
15065
+ const plans = discoverPlans();
15066
+ if (specs.length > 0 || plans.length > 0) {
15067
+ const intentContext = {};
15068
+ if (specs.length > 0) intentContext.specs = specs;
15069
+ if (plans.length > 0) intentContext.plans = plans;
15070
+ requestBody.intent_context = intentContext;
15071
+ }
15072
+ return requestBody;
15073
+ }
15074
+ function allowWithNotice(moment, decision) {
15075
+ resetIter(moment);
15076
+ if (decision === "WARN") {
15077
+ process.stderr.write(`${YELLOW}Verity ${moment}: WARN \u2014 proceeding. See findings in the report.${NC}
15078
+ `);
15079
+ }
15080
+ process.exit(0);
15081
+ }
15082
+ async function runGuard(opts, globals) {
15083
+ const on = opts.on.split(",").map((s) => s.trim()).filter((s) => s === "commit" || s === "push");
15084
+ const { command, cwd, sessionId } = await readPreToolUseStdin();
15085
+ if (cwd && (0, import_node_fs20.existsSync)(cwd)) {
15086
+ try {
15087
+ process.chdir(cwd);
15088
+ } catch {
15089
+ }
15090
+ }
15091
+ const moment = classifyCommand(command, on);
15092
+ if (!moment) process.exit(0);
15093
+ const iter = readIter(moment);
15094
+ if (iter >= GUARD_BLOCK_CAP) {
15095
+ process.stderr.write(`${DIM}Verity ${moment}: ${GUARD_BLOCK_CAP} review cycles reached \u2014 allowing through.${NC}
15096
+ `);
15097
+ resetIter(moment);
15098
+ process.exit(0);
15099
+ }
15100
+ const files = getMomentFiles(moment);
15101
+ if (files.length === 0) process.exit(0);
15102
+ const tokenResult = await resolveToken(globals.token);
15103
+ const urlResult = await resolveServiceUrl(globals.serviceUrl);
15104
+ if (!tokenResult.ok || !urlResult.ok) process.exit(0);
15105
+ const requestBody = buildGuardRequest(moment, files, iter, sessionId);
15106
+ if (!requestBody) process.exit(0);
15107
+ const result = await analyzeRequest({
15108
+ serviceUrl: urlResult.data,
15109
+ token: tokenResult.data.token,
15110
+ body: requestBody,
15111
+ verbose: globals.verbose,
15112
+ timeout: 29e4,
15113
+ cmd: "guard"
15114
+ });
15115
+ if (!result.ok) {
15116
+ process.stderr.write(`${DIM}Verity ${moment}: service unavailable \u2014 allowing commit/push (${result.error}).${NC}
15117
+ `);
15118
+ process.exit(0);
15119
+ }
15120
+ if (opts.json) {
15121
+ process.stdout.write(JSON.stringify(result.data) + "\n");
15122
+ }
15123
+ const response = result.data;
15124
+ const decision = response.gate_decision ?? "PASS";
15125
+ if (decision === "FAIL") {
15126
+ writeIter(moment, iter + 1);
15127
+ writeBlockMessage(moment, response);
15128
+ process.exit(2);
15129
+ }
15130
+ allowWithNotice(moment, decision);
15131
+ }
15132
+ function writeBlockMessage(moment, response) {
15133
+ const label2 = moment === "pre-commit" ? "pre-commit" : "pre-push";
15134
+ const verb = moment === "pre-commit" ? "commit" : "push";
15135
+ const assessment = response.assessment;
15136
+ const narrative = assessment?.narrative ?? "";
15137
+ const findings = response.findings ?? [];
15138
+ const viewUrl = response.view_url ?? "";
15139
+ process.stderr.write(`${RED}${BOLD}\u2501\u2501\u2501 Verity ${label2} gate: FAIL \u2501\u2501\u2501${NC}
15140
+
15141
+ `);
15142
+ if (narrative) process.stderr.write(`${DIM}${narrative}${NC}
15143
+
15144
+ `);
15145
+ const agentFindings = findings.filter((f) => f.scope !== "pre-existing");
15146
+ if (agentFindings.length > 0) {
15147
+ process.stderr.write(`${BOLD}${agentFindings.length} issue(s) in the ${verb} \u2014 fix critical/high severity before ${verb === "commit" ? "committing" : "pushing"}:${NC}
15148
+
15149
+ `);
15150
+ for (const f of agentFindings) {
15151
+ const severity = (f.severity ?? "").toUpperCase();
15152
+ const fix = f.fix;
15153
+ process.stderr.write(`[${severity}] ${f.title ?? ""}
15154
+ `);
15155
+ process.stderr.write(` File: ${f.file ?? ""}:${f.line ?? ""}
15156
+ `);
15157
+ process.stderr.write(` Fix: ${fix?.description ?? "See description"}
15158
+
15159
+ `);
15160
+ }
15161
+ }
15162
+ if (viewUrl) process.stderr.write(`${CYAN}Full report: ${viewUrl}${NC}
15163
+
15164
+ `);
15165
+ process.stderr.write(`${BOLD}Fix these, then re-run the ${verb}.${NC}
15166
+ `);
15167
+ }
15168
+
14789
15169
  // src/commands/init.ts
14790
- var import_node_fs21 = require("node:fs");
15170
+ var import_node_fs22 = require("node:fs");
14791
15171
  var import_promises12 = require("node:fs/promises");
14792
- var import_node_path15 = require("node:path");
15172
+ var import_node_path16 = require("node:path");
14793
15173
  var import_node_child_process9 = require("node:child_process");
14794
15174
 
14795
15175
  // src/commands/migrate.ts
14796
- var import_node_fs20 = require("node:fs");
14797
- var import_node_path14 = require("node:path");
15176
+ var import_node_fs21 = require("node:fs");
15177
+ var import_node_path15 = require("node:path");
14798
15178
  var import_node_child_process8 = require("node:child_process");
14799
15179
  var LEGACY_NPM_PACKAGE = "@codacy/gate-cli";
14800
15180
  function defaultNpmRemover(pkg) {
@@ -14830,12 +15210,12 @@ async function runMigration(opts = {}) {
14830
15210
  return { actions, migrated: actions.length > 0 };
14831
15211
  }
14832
15212
  function migrateProjectDir(root, actions) {
14833
- const gateDir = (0, import_node_path14.join)(root, ".gate");
14834
- const verityDir = (0, import_node_path14.join)(root, ".verity");
14835
- if ((0, import_node_fs20.existsSync)(gateDir) && !(0, import_node_fs20.existsSync)(verityDir)) {
15213
+ const gateDir = (0, import_node_path15.join)(root, ".gate");
15214
+ const verityDir = (0, import_node_path15.join)(root, ".verity");
15215
+ if ((0, import_node_fs21.existsSync)(gateDir) && !(0, import_node_fs21.existsSync)(verityDir)) {
14836
15216
  return migrateProjectDirRename(root, gateDir, verityDir, actions);
14837
15217
  }
14838
- if ((0, import_node_fs20.existsSync)(gateDir) && (0, import_node_fs20.existsSync)(verityDir)) {
15218
+ if ((0, import_node_fs21.existsSync)(gateDir) && (0, import_node_fs21.existsSync)(verityDir)) {
14839
15219
  return migrateProjectDirCarry(gateDir, verityDir, actions);
14840
15220
  }
14841
15221
  return false;
@@ -14856,13 +15236,13 @@ function migrateProjectDirRename(root, gateDir, verityDir, actions) {
14856
15236
  }
14857
15237
  }
14858
15238
  if (moved) {
14859
- if ((0, import_node_fs20.existsSync)(gateDir)) {
15239
+ if ((0, import_node_fs21.existsSync)(gateDir)) {
14860
15240
  const carried = carryLegacyContents(gateDir, verityDir);
14861
15241
  if (carried > 0) {
14862
15242
  actions.push(`Carried ${carried} untracked legacy file(s) from .gate/ into .verity/`);
14863
15243
  }
14864
15244
  try {
14865
- (0, import_node_fs20.rmSync)(gateDir, { recursive: true, force: true });
15245
+ (0, import_node_fs21.rmSync)(gateDir, { recursive: true, force: true });
14866
15246
  } catch {
14867
15247
  }
14868
15248
  }
@@ -14878,18 +15258,18 @@ function migrateProjectDirCarry(gateDir, verityDir, actions) {
14878
15258
  actions.push(`Carried ${carried} legacy file(s) from .gate/ into .verity/`);
14879
15259
  }
14880
15260
  try {
14881
- (0, import_node_fs20.rmSync)(gateDir, { recursive: true, force: true });
15261
+ (0, import_node_fs21.rmSync)(gateDir, { recursive: true, force: true });
14882
15262
  } catch {
14883
15263
  }
14884
15264
  return carried > 0;
14885
15265
  }
14886
15266
  function migrateGlobalCredentials(home, actions) {
14887
15267
  if (!home) return;
14888
- const gateCreds = (0, import_node_path14.join)(home, ".gate", "credentials");
14889
- const verityCreds = (0, import_node_path14.join)(home, ".verity", "credentials");
14890
- if (!(0, import_node_fs20.existsSync)(gateCreds)) return;
14891
- if (!(0, import_node_fs20.existsSync)(verityCreds)) {
14892
- (0, import_node_fs20.mkdirSync)((0, import_node_path14.join)(home, ".verity"), { recursive: true });
15268
+ const gateCreds = (0, import_node_path15.join)(home, ".gate", "credentials");
15269
+ const verityCreds = (0, import_node_path15.join)(home, ".verity", "credentials");
15270
+ if (!(0, import_node_fs21.existsSync)(gateCreds)) return;
15271
+ if (!(0, import_node_fs21.existsSync)(verityCreds)) {
15272
+ (0, import_node_fs21.mkdirSync)((0, import_node_path15.join)(home, ".verity"), { recursive: true });
14893
15273
  moveFile(gateCreds, verityCreds);
14894
15274
  actions.push("Moved ~/.gate/credentials \u2192 ~/.verity/credentials");
14895
15275
  return;
@@ -14911,8 +15291,8 @@ async function migrateLegacyHooks(root, actions) {
14911
15291
  }
14912
15292
  }
14913
15293
  async function migrateClaudeMd(root, actions) {
14914
- const claudeMd = (0, import_node_path14.join)(root, "CLAUDE.md");
14915
- const hadLegacyBlock = (0, import_node_fs20.existsSync)(claudeMd) && hasLegacyMemoryBlock(readFileSyncSafe(claudeMd));
15294
+ const claudeMd = (0, import_node_path15.join)(root, "CLAUDE.md");
15295
+ const hadLegacyBlock = (0, import_node_fs21.existsSync)(claudeMd) && hasLegacyMemoryBlock(readFileSyncSafe(claudeMd));
14916
15296
  if (!hadLegacyBlock) return;
14917
15297
  try {
14918
15298
  await ensureClaudeMdPointer(root);
@@ -14922,9 +15302,9 @@ async function migrateClaudeMd(root, actions) {
14922
15302
  }
14923
15303
  }
14924
15304
  function migrateStandardFile(root, actions) {
14925
- const gateMd = (0, import_node_path14.join)(root, "GATE.md");
14926
- const verityMd = (0, import_node_path14.join)(root, "VERITY.md");
14927
- if (!(0, import_node_fs20.existsSync)(gateMd) || (0, import_node_fs20.existsSync)(verityMd)) return;
15305
+ const gateMd = (0, import_node_path15.join)(root, "GATE.md");
15306
+ const verityMd = (0, import_node_path15.join)(root, "VERITY.md");
15307
+ if (!(0, import_node_fs21.existsSync)(gateMd) || (0, import_node_fs21.existsSync)(verityMd)) return;
14928
15308
  let moved = false;
14929
15309
  if (isGitRepo(root) && isGitTracked(root, "GATE.md")) {
14930
15310
  try {
@@ -14936,7 +15316,7 @@ function migrateStandardFile(root, actions) {
14936
15316
  if (!moved) moveFile(gateMd, verityMd);
14937
15317
  const content = readFileSyncSafe(verityMd);
14938
15318
  const refreshed = content.split("GATE.md").join("VERITY.md");
14939
- if (refreshed !== content) (0, import_node_fs20.writeFileSync)(verityMd, refreshed);
15319
+ if (refreshed !== content) (0, import_node_fs21.writeFileSync)(verityMd, refreshed);
14940
15320
  actions.push("Renamed GATE.md \u2192 VERITY.md");
14941
15321
  }
14942
15322
  function removeLegacyPackage(movedProjectDir, npmRemover, actions) {
@@ -14970,14 +15350,14 @@ function mergeGlobalCredentials(gateCreds, verityCreds) {
14970
15350
  }
14971
15351
  if (toAppend.length > 0) {
14972
15352
  const sep = verityContent.endsWith("\n") || verityContent === "" ? "" : "\n";
14973
- (0, import_node_fs20.writeFileSync)(verityCreds, verityContent + sep + toAppend.join("\n") + "\n");
15353
+ (0, import_node_fs21.writeFileSync)(verityCreds, verityContent + sep + toAppend.join("\n") + "\n");
14974
15354
  }
14975
- (0, import_node_fs20.rmSync)(gateCreds, { force: true });
15355
+ (0, import_node_fs21.rmSync)(gateCreds, { force: true });
14976
15356
  return toAppend.length;
14977
15357
  }
14978
15358
  function readFileSyncSafe(path) {
14979
15359
  try {
14980
- return (0, import_node_fs20.readFileSync)(path, "utf-8");
15360
+ return (0, import_node_fs21.readFileSync)(path, "utf-8");
14981
15361
  } catch {
14982
15362
  return "";
14983
15363
  }
@@ -14992,35 +15372,35 @@ function hasStagedChanges(root) {
14992
15372
  }
14993
15373
  function moveDir(from, to) {
14994
15374
  try {
14995
- (0, import_node_fs20.renameSync)(from, to);
15375
+ (0, import_node_fs21.renameSync)(from, to);
14996
15376
  } catch (err) {
14997
15377
  if (err.code !== "EXDEV") throw err;
14998
- (0, import_node_fs20.cpSync)(from, to, { recursive: true });
14999
- (0, import_node_fs20.rmSync)(from, { recursive: true, force: true });
15378
+ (0, import_node_fs21.cpSync)(from, to, { recursive: true });
15379
+ (0, import_node_fs21.rmSync)(from, { recursive: true, force: true });
15000
15380
  }
15001
15381
  }
15002
15382
  function moveFile(from, to) {
15003
15383
  try {
15004
- (0, import_node_fs20.renameSync)(from, to);
15384
+ (0, import_node_fs21.renameSync)(from, to);
15005
15385
  } catch (err) {
15006
15386
  if (err.code !== "EXDEV") throw err;
15007
- (0, import_node_fs20.cpSync)(from, to);
15008
- (0, import_node_fs20.rmSync)(from, { force: true });
15387
+ (0, import_node_fs21.cpSync)(from, to);
15388
+ (0, import_node_fs21.rmSync)(from, { force: true });
15009
15389
  }
15010
15390
  }
15011
15391
  function carryLegacyContents(gateDir, verityDir) {
15012
15392
  let copied = 0;
15013
15393
  const walk = (relDir) => {
15014
- const srcDir = (0, import_node_path14.join)(gateDir, relDir);
15015
- for (const entry of (0, import_node_fs20.readdirSync)(srcDir)) {
15016
- const rel = relDir ? (0, import_node_path14.join)(relDir, entry) : entry;
15017
- const src = (0, import_node_path14.join)(gateDir, rel);
15018
- const dest = (0, import_node_path14.join)(verityDir, rel);
15019
- if ((0, import_node_fs20.statSync)(src).isDirectory()) {
15394
+ const srcDir = (0, import_node_path15.join)(gateDir, relDir);
15395
+ for (const entry of (0, import_node_fs21.readdirSync)(srcDir)) {
15396
+ const rel = relDir ? (0, import_node_path15.join)(relDir, entry) : entry;
15397
+ const src = (0, import_node_path15.join)(gateDir, rel);
15398
+ const dest = (0, import_node_path15.join)(verityDir, rel);
15399
+ if ((0, import_node_fs21.statSync)(src).isDirectory()) {
15020
15400
  walk(rel);
15021
- } else if (!(0, import_node_fs20.existsSync)(dest)) {
15022
- (0, import_node_fs20.mkdirSync)((0, import_node_path14.dirname)(dest), { recursive: true });
15023
- (0, import_node_fs20.cpSync)(src, dest);
15401
+ } else if (!(0, import_node_fs21.existsSync)(dest)) {
15402
+ (0, import_node_fs21.mkdirSync)((0, import_node_path15.dirname)(dest), { recursive: true });
15403
+ (0, import_node_fs21.cpSync)(src, dest);
15024
15404
  copied++;
15025
15405
  }
15026
15406
  }
@@ -15029,22 +15409,22 @@ function carryLegacyContents(gateDir, verityDir) {
15029
15409
  return copied;
15030
15410
  }
15031
15411
  async function needsMigration(root = repoRoot()) {
15032
- const gateDir = (0, import_node_path14.join)(root, ".gate");
15033
- const verityDir = (0, import_node_path14.join)(root, ".verity");
15034
- if ((0, import_node_fs20.existsSync)(gateDir) && !(0, import_node_fs20.existsSync)(verityDir)) return true;
15035
- if ((0, import_node_fs20.existsSync)(gateDir) && (0, import_node_fs20.existsSync)(verityDir)) {
15036
- if ((0, import_node_fs20.existsSync)((0, import_node_path14.join)(gateDir, "credentials")) && !(0, import_node_fs20.existsSync)((0, import_node_path14.join)(verityDir, "credentials"))) {
15412
+ const gateDir = (0, import_node_path15.join)(root, ".gate");
15413
+ const verityDir = (0, import_node_path15.join)(root, ".verity");
15414
+ if ((0, import_node_fs21.existsSync)(gateDir) && !(0, import_node_fs21.existsSync)(verityDir)) return true;
15415
+ if ((0, import_node_fs21.existsSync)(gateDir) && (0, import_node_fs21.existsSync)(verityDir)) {
15416
+ if ((0, import_node_fs21.existsSync)((0, import_node_path15.join)(gateDir, "credentials")) && !(0, import_node_fs21.existsSync)((0, import_node_path15.join)(verityDir, "credentials"))) {
15037
15417
  return true;
15038
15418
  }
15039
- if ((0, import_node_fs20.existsSync)((0, import_node_path14.join)(gateDir, "memory")) && !(0, import_node_fs20.existsSync)((0, import_node_path14.join)(verityDir, "memory"))) {
15419
+ if ((0, import_node_fs21.existsSync)((0, import_node_path15.join)(gateDir, "memory")) && !(0, import_node_fs21.existsSync)((0, import_node_path15.join)(verityDir, "memory"))) {
15040
15420
  return true;
15041
15421
  }
15042
15422
  }
15043
- const claudeMd = (0, import_node_path14.join)(root, "CLAUDE.md");
15044
- if ((0, import_node_fs20.existsSync)(claudeMd) && hasLegacyMemoryBlock(readFileSyncSafe(claudeMd))) {
15423
+ const claudeMd = (0, import_node_path15.join)(root, "CLAUDE.md");
15424
+ if ((0, import_node_fs21.existsSync)(claudeMd) && hasLegacyMemoryBlock(readFileSyncSafe(claudeMd))) {
15045
15425
  return true;
15046
15426
  }
15047
- if ((0, import_node_fs20.existsSync)((0, import_node_path14.join)(root, "GATE.md")) && !(0, import_node_fs20.existsSync)((0, import_node_path14.join)(root, "VERITY.md"))) {
15427
+ if ((0, import_node_fs21.existsSync)((0, import_node_path15.join)(root, "GATE.md")) && !(0, import_node_fs21.existsSync)((0, import_node_path15.join)(root, "VERITY.md"))) {
15048
15428
  return true;
15049
15429
  }
15050
15430
  if (await hasLegacyHooksAt(root)) return true;
@@ -15072,15 +15452,15 @@ function registerMigrateCommand(program2) {
15072
15452
  // src/commands/init.ts
15073
15453
  function resolveDataDir() {
15074
15454
  const candidates = [
15075
- (0, import_node_path15.join)(__dirname, "..", "data"),
15455
+ (0, import_node_path16.join)(__dirname, "..", "data"),
15076
15456
  // installed: node_modules/@codacy/verity-cli/data
15077
- (0, import_node_path15.join)(__dirname, "..", "..", "data"),
15457
+ (0, import_node_path16.join)(__dirname, "..", "..", "data"),
15078
15458
  // edge case: nested resolution
15079
- (0, import_node_path15.join)(process.cwd(), "cli", "data")
15459
+ (0, import_node_path16.join)(process.cwd(), "cli", "data")
15080
15460
  // local dev: running from repo root
15081
15461
  ];
15082
15462
  for (const candidate of candidates) {
15083
- if ((0, import_node_fs21.existsSync)((0, import_node_path15.join)(candidate, "skills"))) {
15463
+ if ((0, import_node_fs22.existsSync)((0, import_node_path16.join)(candidate, "skills"))) {
15084
15464
  return candidate;
15085
15465
  }
15086
15466
  }
@@ -15096,7 +15476,7 @@ function registerInitCommand(program2) {
15096
15476
  program2.command("init").description("Initialize Verity in the current project").option("--force", "Overwrite existing skills and hooks").action(async (opts) => {
15097
15477
  const force = opts.force ?? false;
15098
15478
  const projectMarkers = [".git", "package.json", "pyproject.toml", "go.mod", "Cargo.toml", "Gemfile", "pom.xml", "build.gradle"];
15099
- const isProject = projectMarkers.some((m) => (0, import_node_fs21.existsSync)(m));
15479
+ const isProject = projectMarkers.some((m) => (0, import_node_fs22.existsSync)(m));
15100
15480
  if (!isProject) {
15101
15481
  printError("No project detected in the current directory.");
15102
15482
  printInfo('Run "verity init" from your project root.');
@@ -15159,21 +15539,21 @@ function registerInitCommand(program2) {
15159
15539
  console.log("");
15160
15540
  printInfo("Installing skills...");
15161
15541
  const dataDir = resolveDataDir();
15162
- const skillsSource = (0, import_node_path15.join)(dataDir, "skills");
15542
+ const skillsSource = (0, import_node_path16.join)(dataDir, "skills");
15163
15543
  const skillsDest = ".claude/skills";
15164
15544
  const skills = ["verity-setup", "verity-analyze", "verity-status", "verity-feedback", "verity-learn", "verity-memory", "verity-insights", "verity-reflect"];
15165
15545
  let skillsInstalled = 0;
15166
15546
  for (const skill of skills) {
15167
- const src = (0, import_node_path15.join)(skillsSource, skill);
15168
- const dest = (0, import_node_path15.join)(skillsDest, skill);
15169
- if (!(0, import_node_fs21.existsSync)(src)) {
15547
+ const src = (0, import_node_path16.join)(skillsSource, skill);
15548
+ const dest = (0, import_node_path16.join)(skillsDest, skill);
15549
+ if (!(0, import_node_fs22.existsSync)(src)) {
15170
15550
  printWarn(` Skill data not found: ${skill}`);
15171
15551
  continue;
15172
15552
  }
15173
- if ((0, import_node_fs21.existsSync)(dest) && !force) {
15174
- const srcSkill = (0, import_node_path15.join)(src, "SKILL.md");
15175
- const destSkill = (0, import_node_path15.join)(dest, "SKILL.md");
15176
- if ((0, import_node_fs21.existsSync)(destSkill)) {
15553
+ if ((0, import_node_fs22.existsSync)(dest) && !force) {
15554
+ const srcSkill = (0, import_node_path16.join)(src, "SKILL.md");
15555
+ const destSkill = (0, import_node_path16.join)(dest, "SKILL.md");
15556
+ if ((0, import_node_fs22.existsSync)(destSkill)) {
15177
15557
  try {
15178
15558
  const srcContent = await (0, import_promises12.readFile)(srcSkill, "utf-8");
15179
15559
  const destContent = await (0, import_promises12.readFile)(destSkill, "utf-8");
@@ -15209,7 +15589,7 @@ function registerInitCommand(program2) {
15209
15589
  } catch (err) {
15210
15590
  printWarn(` Could not update CLAUDE.md: ${err.message}`);
15211
15591
  }
15212
- const globalVerityDir = (0, import_node_path15.join)(process.env.HOME ?? "", ".verity");
15592
+ const globalVerityDir = (0, import_node_path16.join)(process.env.HOME ?? "", ".verity");
15213
15593
  await (0, import_promises12.mkdir)(globalVerityDir, { recursive: true });
15214
15594
  console.log("");
15215
15595
  printInfo("Verity initialized!");
@@ -15232,8 +15612,8 @@ function registerInitCommand(program2) {
15232
15612
  }
15233
15613
 
15234
15614
  // src/commands/uninstall.ts
15235
- var import_node_fs22 = require("node:fs");
15236
- var import_node_path16 = require("node:path");
15615
+ var import_node_fs23 = require("node:fs");
15616
+ var import_node_path17 = require("node:path");
15237
15617
  var SKILL_NAMES = [
15238
15618
  "verity-setup",
15239
15619
  "verity-analyze",
@@ -15252,11 +15632,11 @@ function registerUninstallCommand(program2) {
15252
15632
  const actions = [];
15253
15633
  const skillsRoot = projectPath(".claude/skills");
15254
15634
  for (const name of SKILL_NAMES) {
15255
- const dir = (0, import_node_path16.join)(skillsRoot, name);
15256
- if ((0, import_node_fs22.existsSync)(dir)) {
15635
+ const dir = (0, import_node_path17.join)(skillsRoot, name);
15636
+ if ((0, import_node_fs23.existsSync)(dir)) {
15257
15637
  actions.push({
15258
15638
  label: `Remove .claude/skills/${name}/`,
15259
- apply: () => (0, import_node_fs22.rmSync)(dir, { recursive: true, force: true })
15639
+ apply: () => (0, import_node_fs23.rmSync)(dir, { recursive: true, force: true })
15260
15640
  });
15261
15641
  }
15262
15642
  }
@@ -15270,24 +15650,24 @@ function registerUninstallCommand(program2) {
15270
15650
  });
15271
15651
  }
15272
15652
  const verityDir = projectPath(VERITY_DIR);
15273
- if ((0, import_node_fs22.existsSync)(verityDir)) {
15653
+ if ((0, import_node_fs23.existsSync)(verityDir)) {
15274
15654
  actions.push({
15275
15655
  label: `Remove ${VERITY_DIR}/`,
15276
- apply: () => (0, import_node_fs22.rmSync)(verityDir, { recursive: true, force: true })
15656
+ apply: () => (0, import_node_fs23.rmSync)(verityDir, { recursive: true, force: true })
15277
15657
  });
15278
15658
  }
15279
15659
  if (!keepVerityMd) {
15280
15660
  const verityMd = projectPath(VERITY_MD_FILE);
15281
- if ((0, import_node_fs22.existsSync)(verityMd)) {
15661
+ if ((0, import_node_fs23.existsSync)(verityMd)) {
15282
15662
  actions.push({
15283
15663
  label: `Remove ${VERITY_MD_FILE}`,
15284
- apply: () => (0, import_node_fs22.rmSync)(verityMd, { force: true })
15664
+ apply: () => (0, import_node_fs23.rmSync)(verityMd, { force: true })
15285
15665
  });
15286
15666
  }
15287
15667
  }
15288
15668
  const cleanupEmptyDir = (path) => {
15289
- if ((0, import_node_fs22.existsSync)(path) && (0, import_node_fs22.statSync)(path).isDirectory() && (0, import_node_fs22.readdirSync)(path).length === 0) {
15290
- (0, import_node_fs22.rmdirSync)(path);
15669
+ if ((0, import_node_fs23.existsSync)(path) && (0, import_node_fs23.statSync)(path).isDirectory() && (0, import_node_fs23.readdirSync)(path).length === 0) {
15670
+ (0, import_node_fs23.rmdirSync)(path);
15291
15671
  }
15292
15672
  };
15293
15673
  actions.push({
@@ -15298,11 +15678,11 @@ function registerUninstallCommand(program2) {
15298
15678
  }
15299
15679
  });
15300
15680
  const home = process.env.HOME ?? "";
15301
- const globalVerityDir = (0, import_node_path16.join)(home, ".verity");
15302
- if (purgeGlobal && (0, import_node_fs22.existsSync)(globalVerityDir)) {
15681
+ const globalVerityDir = (0, import_node_path17.join)(home, ".verity");
15682
+ if (purgeGlobal && (0, import_node_fs23.existsSync)(globalVerityDir)) {
15303
15683
  actions.push({
15304
15684
  label: `Remove ~/.verity/ (global credentials \u2014 reconnect requires re-registration)`,
15305
- apply: () => (0, import_node_fs22.rmSync)(globalVerityDir, { recursive: true, force: true })
15685
+ apply: () => (0, import_node_fs23.rmSync)(globalVerityDir, { recursive: true, force: true })
15306
15686
  });
15307
15687
  }
15308
15688
  if (actions.length === 0) {
@@ -15496,8 +15876,8 @@ function registerTaskCommands(program2) {
15496
15876
  }
15497
15877
 
15498
15878
  // src/commands/reset.ts
15499
- var import_node_fs23 = require("node:fs");
15500
- var import_node_path17 = require("node:path");
15879
+ var import_node_fs24 = require("node:fs");
15880
+ var import_node_path18 = require("node:path");
15501
15881
  function registerResetCommand(program2) {
15502
15882
  program2.command("reset").description("Close the current task and clear transient state").option("--keep-task", "Only purge caches; leave the current task open").option("--all", "Also purge diagnostic logs (.verity/.logs/)").action(async (opts) => {
15503
15883
  const globals = program2.opts();
@@ -15534,11 +15914,11 @@ function registerResetCommand(program2) {
15534
15914
  }
15535
15915
  const cacheDir = projectPath(CACHE_DIR);
15536
15916
  let purged = 0;
15537
- if ((0, import_node_fs23.existsSync)(cacheDir)) {
15538
- for (const entry of (0, import_node_fs23.readdirSync)(cacheDir)) {
15917
+ if ((0, import_node_fs24.existsSync)(cacheDir)) {
15918
+ for (const entry of (0, import_node_fs24.readdirSync)(cacheDir)) {
15539
15919
  if (entry.startsWith("pending-")) {
15540
15920
  try {
15541
- (0, import_node_fs23.unlinkSync)((0, import_node_path17.join)(cacheDir, entry));
15921
+ (0, import_node_fs24.unlinkSync)((0, import_node_path18.join)(cacheDir, entry));
15542
15922
  purged++;
15543
15923
  } catch {
15544
15924
  }
@@ -15553,19 +15933,19 @@ function registerResetCommand(program2) {
15553
15933
  projectPath(`${VERITY_DIR}/.last-analysis`)
15554
15934
  ];
15555
15935
  for (const file of filesToClear) {
15556
- if ((0, import_node_fs23.existsSync)(file)) {
15936
+ if ((0, import_node_fs24.existsSync)(file)) {
15557
15937
  try {
15558
- (0, import_node_fs23.writeFileSync)(file, "");
15938
+ (0, import_node_fs24.writeFileSync)(file, "");
15559
15939
  } catch {
15560
15940
  }
15561
15941
  }
15562
15942
  }
15563
15943
  if (opts.all) {
15564
15944
  const logsDir = projectPath(`${VERITY_DIR}/.logs`);
15565
- if ((0, import_node_fs23.existsSync)(logsDir)) {
15566
- for (const entry of (0, import_node_fs23.readdirSync)(logsDir)) {
15945
+ if ((0, import_node_fs24.existsSync)(logsDir)) {
15946
+ for (const entry of (0, import_node_fs24.readdirSync)(logsDir)) {
15567
15947
  try {
15568
- (0, import_node_fs23.unlinkSync)((0, import_node_path17.join)(logsDir, entry));
15948
+ (0, import_node_fs24.unlinkSync)((0, import_node_path18.join)(logsDir, entry));
15569
15949
  } catch {
15570
15950
  }
15571
15951
  }
@@ -15803,7 +16183,7 @@ function registerRunCommand(program2) {
15803
16183
 
15804
16184
  // src/lib/telemetry.ts
15805
16185
  var import_promises13 = require("node:fs/promises");
15806
- var import_node_path18 = require("node:path");
16186
+ var import_node_path19 = require("node:path");
15807
16187
  var SETTINGS_LOCAL_FILE2 = ".claude/settings.local.json";
15808
16188
  var GITIGNORE_FILE = ".gitignore";
15809
16189
  var GITIGNORE_ENTRY = ".claude/settings.local.json";
@@ -15841,7 +16221,7 @@ async function readSettingsLocal() {
15841
16221
  }
15842
16222
  async function writeSettingsLocal(settings) {
15843
16223
  const file = projectPath(SETTINGS_LOCAL_FILE2);
15844
- await (0, import_promises13.mkdir)((0, import_node_path18.dirname)(file), { recursive: true });
16224
+ await (0, import_promises13.mkdir)((0, import_node_path19.dirname)(file), { recursive: true });
15845
16225
  await (0, import_promises13.writeFile)(file, JSON.stringify(settings, null, 2) + "\n");
15846
16226
  }
15847
16227
  async function ensureGitignore() {
@@ -15937,7 +16317,7 @@ function registerTelemetryCommands(program2) {
15937
16317
  }
15938
16318
 
15939
16319
  // src/cli.ts
15940
- program.name("verity").description("CLI for Verity quality gate service").version("0.22.0").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr");
16320
+ program.name("verity").description("CLI for Verity quality gate service").version("0.23.1").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr");
15941
16321
  registerAuthCommands(program);
15942
16322
  registerHooksCommands(program);
15943
16323
  registerIntentCommands(program);
@@ -15947,6 +16327,7 @@ registerStatusCommand(program);
15947
16327
  registerFeedbackCommand(program);
15948
16328
  registerAnalyzeCommand(program);
15949
16329
  registerReviewCommand(program);
16330
+ registerGuardCommand(program);
15950
16331
  registerInitCommand(program);
15951
16332
  registerUninstallCommand(program);
15952
16333
  registerTaskCommands(program);
@@ -75,6 +75,32 @@ Default to `balanced` if the user says "default" or doesn't specify a preference
75
75
 
76
76
  ---
77
77
 
78
+ ## Step 3a: Choose analysis moments
79
+
80
+ This decides **when** Verity reviews code. Use the **AskUserQuestion** tool with
81
+ `multiSelect: true`:
82
+
83
+ > **When should Verity review your code? (select all that apply)**
84
+ > - **On stop** (recommended) — after every agent turn. Fast feedback while you work.
85
+ > - **Before commit** — gates `git commit`; reviews the **staged** diff and blocks the commit on FAIL.
86
+ > - **Before push / PR** — gates `git push` and `gh pr create`; reviews the **to-be-pushed** commits and blocks on FAIL.
87
+ >
88
+ > Default: **On stop**
89
+
90
+ Notes to convey if asked:
91
+ - The git-moment gates run an independent reviewer at the boundary the customer cares
92
+ about; a FAIL blocks the commit/push so the agent fixes issues before they land
93
+ (capped at 2 review cycles so you're never stuck).
94
+ - These are **Claude Code** hooks (they fire when the agent runs `git commit`/`git push`),
95
+ not native git hooks — no `.git/hooks` files, no GitHub App.
96
+ - "On stop" alone matches today's behavior. Many teams pick **Before commit + Before push**
97
+ and turn **On stop** off to review only at real boundaries.
98
+
99
+ Map the selection to moment ids: On stop → `stop`, Before commit → `pre-commit`,
100
+ Before push/PR → `pre-push`. Remember this list; you apply it in **Step 9**.
101
+
102
+ ---
103
+
78
104
  ## Step 3b: Ask about cost & usage telemetry (opt-in)
79
105
 
80
106
  Cost & usage observability is **opt-in** and **required** for the `/usage` dashboard — all
@@ -414,27 +440,37 @@ These blocks enable two capabilities:
414
440
 
415
441
  ---
416
442
 
417
- ## Step 9: Verify hooks
443
+ ## Step 9: Wire the selected moments
418
444
 
419
- The installer already wired the Claude Code hooks via `verity hooks install`. Verify they're in place:
445
+ Apply the moments the user chose in **Step 3a**. Pass the comma-separated moment ids
446
+ (always include the always-on intent hook automatically):
447
+
448
+ ```bash
449
+ # Example: user chose On stop + Before commit + Before push
450
+ verity hooks install --moments stop,pre-commit,pre-push
451
+ ```
452
+
453
+ `--moments` reconciles Verity's own hooks in `.claude/settings.json` to **exactly**
454
+ the selection (adding the git-moment gate, removing the Stop hook if unselected),
455
+ while preserving every non-Verity hook. The `verity intent capture` hook is always
456
+ installed. If the user picked only `stop` (the default), pass `--moments stop`.
457
+
458
+ Then verify:
420
459
 
421
460
  ```bash
422
461
  verity hooks check
423
462
  ```
424
463
 
425
- Expected output:
464
+ Expected output (for `stop,pre-commit,pre-push`):
426
465
  ```
427
466
  [verity] Stop hook (verity analyze): installed
428
467
  [verity] Intent hook (verity intent capture): installed
468
+ [verity] Git-moment gate (verity guard): installed [commit, push]
429
469
  ```
430
470
 
431
- If hooks are missing (e.g., user installed manually without the installer), install them:
432
-
433
- ```bash
434
- verity hooks install
435
- ```
436
-
437
- This wires both the Stop hook (`verity analyze`) and UserPromptSubmit hook (`verity intent capture`) into `.claude/settings.json`, preserving any existing hooks.
471
+ The git-moment gate is a single `PreToolUse(Bash)` hook (`verity guard`) that
472
+ intercepts `git commit` / `git push` / `gh pr create` and reviews the staged or
473
+ to-be-pushed diff before it lands.
438
474
 
439
475
  ---
440
476
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codacy/verity-cli",
3
- "version": "0.22.0",
3
+ "version": "0.23.1",
4
4
  "description": "CLI for Verity quality gate service",
5
5
  "homepage": "https://verity.md",
6
6
  "repository": {
@@ -24,7 +24,8 @@
24
24
  },
25
25
  "scripts": {
26
26
  "build": "node scripts/build.js",
27
- "test": "node --import tsx --test $(find tests -name '*.test.ts' | sort)",
27
+ "test": "node --import tsx --test $(find tests -name '*.test.ts' -not -path 'tests/e2e/*' | sort)",
28
+ "test:e2e": "node --import tsx --test --test-concurrency=1 tests/e2e/*.test.ts",
28
29
  "typecheck": "tsc --noEmit",
29
30
  "prepublishOnly": "npm run typecheck && npm run build"
30
31
  },