@codacy/verity-cli 0.30.0 → 0.30.1-experimental.cbeb697

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
@@ -10342,6 +10342,10 @@ function repoRoot() {
10342
10342
  }
10343
10343
  return _repoRoot;
10344
10344
  }
10345
+ function _resetRepoRoot() {
10346
+ _repoRoot = null;
10347
+ _mainRoot = void 0;
10348
+ }
10345
10349
  var _mainRoot;
10346
10350
  function mainWorktreeRoot() {
10347
10351
  if (_mainRoot !== void 0) return _mainRoot;
@@ -10383,6 +10387,7 @@ var CLAUDE_SETTINGS_FILE = ".claude/settings.json";
10383
10387
  var STANDARD_FILE = `${VERITY_DIR}/standard.yaml`;
10384
10388
  var MEMORY_DIR = `${VERITY_DIR}/memory`;
10385
10389
  var CODACY_CONFIG_FILE = ".codacy/codacy.config.json";
10390
+ var VERITYIGNORE_FILE = ".verityignore";
10386
10391
  function projectPath(relativePath) {
10387
10392
  return (0, import_node_path.join)(repoRoot(), relativePath);
10388
10393
  }
@@ -10473,7 +10478,8 @@ var REVIEWABLE_FILENAMES = /* @__PURE__ */ new Set([
10473
10478
  "Makefile",
10474
10479
  "Dockerfile",
10475
10480
  "Jenkinsfile",
10476
- "Vagrantfile"
10481
+ "Vagrantfile",
10482
+ ".verityignore"
10477
10483
  ]);
10478
10484
  var REVIEWABLE_PATH_PATTERNS = [
10479
10485
  /\.circleci\//,
@@ -10493,7 +10499,7 @@ var SECURITY_PATTERNS = [
10493
10499
  /Dockerfile/
10494
10500
  ];
10495
10501
  var PROD_SERVICE_URL = "https://ofcamwrjwrkazqvdchko.supabase.co/functions/v1";
10496
- var DEFAULT_SERVICE_URL = "".length > 0 ? "" : PROD_SERVICE_URL;
10502
+ var DEFAULT_SERVICE_URL = "https://wukeddyzpijoegyajtnc.supabase.co/functions/v1".length > 0 ? "https://wukeddyzpijoegyajtnc.supabase.co/functions/v1" : PROD_SERVICE_URL;
10497
10503
  var GITHUB_CLIENT_ID = "Iv23li88HxAi3ZrbYzWh";
10498
10504
  var GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
10499
10505
  var GITHUB_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
@@ -10503,6 +10509,7 @@ function githubAppInstallUrl(accountId) {
10503
10509
  return accountId != null ? `https://github.com/apps/${GITHUB_APP_SLUG}/installations/new/permissions?target_id=${accountId}` : GITHUB_APP_INSTALL_URL;
10504
10510
  }
10505
10511
  var ADVISORY_EPISODE_FILE = `${VERITY_DIR}/.advisory-episode`;
10512
+ var IGNORE_DECLARATION_FILE = `${VERITY_DIR}/.ignore-declaration`;
10506
10513
 
10507
10514
  // src/lib/output.ts
10508
10515
  var RED = "\x1B[0;31m";
@@ -10804,13 +10811,6 @@ function execGit(cmd) {
10804
10811
  return "";
10805
10812
  }
10806
10813
  }
10807
- function execGitArgs(args) {
10808
- try {
10809
- return (0, import_node_child_process3.execFileSync)("git", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
10810
- } catch {
10811
- return "";
10812
- }
10813
- }
10814
10814
  function splitLines(s) {
10815
10815
  return s.split("\n").filter((l) => l.length > 0);
10816
10816
  }
@@ -10877,9 +10877,6 @@ function getChangedFiles() {
10877
10877
  const filtered = Array.from(sets).filter((f) => !isVerityOwnedPath(f));
10878
10878
  return { files: filtered, hasRecentCommitFiles };
10879
10879
  }
10880
- function getStagedFiles() {
10881
- return splitLines(execGit("git diff --cached --name-only")).filter((f) => !isVerityOwnedPath(f));
10882
- }
10883
10880
  function getDirtyFiles() {
10884
10881
  const set = /* @__PURE__ */ new Set();
10885
10882
  for (const f of splitLines(execGit("git diff --name-only HEAD"))) set.add(f);
@@ -10900,33 +10897,6 @@ function showContentAtRef(ref, repoRelPath) {
10900
10897
  return null;
10901
10898
  }
10902
10899
  }
10903
- function getPushRangeFiles() {
10904
- const diff = (range) => splitLines(execGitArgs(["diff", "--name-only", range])).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
10905
- const resolvers = [
10906
- () => execGitArgs(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{push}"]) ? "@{push}..HEAD" : null,
10907
- () => execGitArgs(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]) ? "@{upstream}..HEAD" : null,
10908
- () => {
10909
- const branch = execGitArgs(["rev-parse", "--abbrev-ref", "HEAD"]);
10910
- return branch && branch !== "HEAD" && execGitArgs(["rev-parse", "--verify", "-q", `origin/${branch}`]) ? `origin/${branch}..HEAD` : null;
10911
- }
10912
- ];
10913
- for (const resolve3 of resolvers) {
10914
- const range = resolve3();
10915
- if (range) return { files: diff(range), range };
10916
- }
10917
- const baseline = readBaselineSha();
10918
- if (baseline) {
10919
- const files = diff(`${baseline}..HEAD`);
10920
- if (files.length > 0) return { files, range: `${baseline}..HEAD` };
10921
- }
10922
- const last = diff("HEAD~1..HEAD");
10923
- return { files: last, range: last.length > 0 ? "HEAD~1..HEAD" : null };
10924
- }
10925
- function getPushRangeMessages() {
10926
- const { range } = getPushRangeFiles();
10927
- if (!range) return "";
10928
- return execGitArgs(["log", range, "--format=%B%x00"]).split("\0").map((s) => s.trim()).filter(Boolean).join("\n\n");
10929
- }
10930
10900
  function filterAnalyzable(files) {
10931
10901
  return files.filter((f) => {
10932
10902
  const ext = (0, import_node_path3.extname)(f).slice(1);
@@ -11417,7 +11387,7 @@ var readline = __toESM(require("node:readline/promises"));
11417
11387
  var import_node_os = require("node:os");
11418
11388
 
11419
11389
  // src/lib/provider-auth.ts
11420
- var sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
11390
+ var sleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
11421
11391
  var form = (fields) => new URLSearchParams(fields).toString();
11422
11392
  async function githubAccountId(owner) {
11423
11393
  try {
@@ -13563,6 +13533,42 @@ var LEGACY_MD_END = "<!-- gate-memory:end -->";
13563
13533
  var LEGACY_PRESERVE_START = "<!-- gate-memory:preserve -->";
13564
13534
  var LEGACY_PRESERVE_END = "<!-- /gate-memory:preserve -->";
13565
13535
  var CLAUDE_MD_PROSE = [
13536
+ "## Project Memory",
13537
+ "",
13538
+ "This project has a knowledge graph maintained at `.verity/memory/`. Before starting",
13539
+ "non-trivial work, scan `.verity/memory/index.md` for decisions, gotchas, and patterns",
13540
+ "that may apply to the change you are about to make. Open specific node files via",
13541
+ "the Read tool when the title or scope suggests relevance.",
13542
+ "",
13543
+ "The graph is auto-maintained by Verity. Files at `.verity/memory/_archive/` are",
13544
+ "superseded \u2014 ignore them unless investigating history.",
13545
+ "",
13546
+ "> Durable, hand-curated guidance goes in the preserve region below (it survives",
13547
+ "> regeneration) or anywhere OUTSIDE these markers. Everything else between the",
13548
+ "> markers is tool-owned and overwritten on each run.",
13549
+ "",
13550
+ "## Housekeeping Turns",
13551
+ "",
13552
+ "When a turn will be pure housekeeping \u2014 pulling, installing dependencies,",
13553
+ "rebasing, a formatting sweep you are not authoring \u2014 declare it BEFORE doing it:",
13554
+ "",
13555
+ "```bash",
13556
+ 'verity ignore --turn --agent --reason "pulling latest before starting"',
13557
+ "```",
13558
+ "",
13559
+ "This skips the review for that turn, which saves the turn Verity would",
13560
+ "otherwise spend saying it had nothing to say. Use `--for 30m` instead of",
13561
+ "`--turn` when a single piece of housekeeping spans several turns.",
13562
+ "",
13563
+ "**It is a claim about the turn, not a way to silence review.** The declaration",
13564
+ "is checked against what the turn actually did: if anything is authored \u2014 by you,",
13565
+ "by a subagent, or by a shell command that can write files \u2014 it voids, the review",
13566
+ "runs anyway, and the broken declaration is reported. So declare housekeeping you",
13567
+ "are about to do, never work you have already done, and never as a way to get past",
13568
+ "a finding. Declarations are budgeted per session and every one is recorded with",
13569
+ "its reason."
13570
+ ].join("\n");
13571
+ var CLAUDE_MD_PROSE_PRE_IGNORE = [
13566
13572
  "## Project Memory",
13567
13573
  "",
13568
13574
  "This project has a knowledge graph maintained at `.verity/memory/`. Before starting",
@@ -13687,7 +13693,7 @@ function extractPreserveContent(interior) {
13687
13693
  }
13688
13694
  function stripKnownProse(interior) {
13689
13695
  const trimmed = interior.replace(/^\n+/, "");
13690
- for (const prose of [CLAUDE_MD_PROSE, CLAUDE_MD_PROSE_LEGACY]) {
13696
+ for (const prose of [CLAUDE_MD_PROSE, CLAUDE_MD_PROSE_PRE_IGNORE, CLAUDE_MD_PROSE_LEGACY]) {
13691
13697
  if (trimmed.startsWith(prose)) return trimmed.slice(prose.length);
13692
13698
  }
13693
13699
  return trimmed;
@@ -13747,6 +13753,160 @@ var import_node_fs14 = require("node:fs");
13747
13753
  var import_node_crypto7 = require("node:crypto");
13748
13754
  var import_node_path13 = require("node:path");
13749
13755
 
13756
+ // src/lib/analysis-mode.ts
13757
+ var DEBUG_PHRASES = [
13758
+ "not working",
13759
+ "doesn't work",
13760
+ "doesn't work",
13761
+ "does not work",
13762
+ "isn't working",
13763
+ "is not working",
13764
+ "can't figure out",
13765
+ "stack trace"
13766
+ ];
13767
+ var DEBUG_WORDS = [
13768
+ "fix",
13769
+ "bug",
13770
+ "broken",
13771
+ "crash",
13772
+ "crashing",
13773
+ "failing",
13774
+ "debug",
13775
+ "debugging",
13776
+ "investigate",
13777
+ "troubleshoot",
13778
+ "regression",
13779
+ "wrong"
13780
+ ];
13781
+ var DEBUG_PATTERN = new RegExp(
13782
+ [
13783
+ ...DEBUG_PHRASES.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
13784
+ ...DEBUG_WORDS.map((w) => `\\b${w}\\b`)
13785
+ ].join("|"),
13786
+ "i"
13787
+ );
13788
+ var FALSE_POSITIVE_PATTERNS = [
13789
+ /\b(?:add|create|implement|write|build|design|set\s*up)\b.{0,20}\berror\b/i,
13790
+ /\berror\s+handling\b/i,
13791
+ /\berror\s+boundar(?:y|ies)\b/i,
13792
+ /\berror\s+(?:type|class|page|component|message|code|enum)\b/i,
13793
+ /\b(?:add|create|implement|write|build)\b.{0,20}\b(?:fix|debug|issue)\b/i
13794
+ ];
13795
+ function hasDebugIntent(prompt) {
13796
+ if (!DEBUG_PATTERN.test(prompt)) return false;
13797
+ for (const fp of FALSE_POSITIVE_PATTERNS) {
13798
+ if (fp.test(prompt)) return false;
13799
+ }
13800
+ return true;
13801
+ }
13802
+ var GIT_ONLY_PATTERN = /\b(commit|push|deploy|merge|rebase|tag|release|publish|ship)\b/i;
13803
+ var CODE_AUTHORING_PATTERN = /\b(add|create|implement|build|write|fix|update|change|refactor|modify|remove|delete|move|rename)\b.*\b(function|component|feature|endpoint|test|file|module|class|type|interface|hook|page|route|style|migration|code|bug|error|issue)\b/i;
13804
+ function isGitOnlyPrompt(prompt) {
13805
+ if (!GIT_ONLY_PATTERN.test(prompt)) return false;
13806
+ if (CODE_AUTHORING_PATTERN.test(prompt)) return false;
13807
+ return true;
13808
+ }
13809
+ function reconcileAnalysisMode(predictedMode, signals) {
13810
+ const mode2 = resolveAnalysisMode(predictedMode, signals);
13811
+ if (mode2 !== "skip") return mode2;
13812
+ const windowIsOrphaned = signals.actionSummary?.transcript_windowed === "orphaned";
13813
+ if (windowIsOrphaned && !signals.sessionAuthoredCode) return "standard";
13814
+ return mode2;
13815
+ }
13816
+ function resolveAnalysisMode(predictedMode, signals) {
13817
+ if (!predictedMode || !isValidMode(predictedMode)) {
13818
+ return detectAnalysisMode(
13819
+ signals.noFilesChanged,
13820
+ signals.assistantResponse,
13821
+ signals.conversationPrompts,
13822
+ signals.actionSummary,
13823
+ signals.sessionAuthoredCode
13824
+ );
13825
+ }
13826
+ const agentAuthoredCode = !!(signals.actionSummary && (signals.actionSummary.files_edited.length > 0 || signals.actionSummary.files_created.length > 0)) || !!signals.sessionAuthoredCode;
13827
+ const agentInvestigated = didAgentInvestigate(signals.actionSummary);
13828
+ switch (predictedMode) {
13829
+ case "skip":
13830
+ if (agentAuthoredCode) return "standard";
13831
+ return "skip";
13832
+ case "plan":
13833
+ if (agentAuthoredCode) return "standard";
13834
+ return "plan";
13835
+ case "debug":
13836
+ return "debug";
13837
+ case "standard":
13838
+ if (!!signals.actionSummary && !agentAuthoredCode && !!signals.assistantResponse) {
13839
+ return agentInvestigated ? "plan" : "skip";
13840
+ }
13841
+ return "standard";
13842
+ }
13843
+ }
13844
+ function didAgentInvestigate(summary) {
13845
+ if (!summary) return false;
13846
+ return summary.files_read.length > 0 || summary.searches > 0 || summary.commands.length > 0 || summary.subagents > 0 || summary.web_fetches > 0;
13847
+ }
13848
+ function isValidMode(mode2) {
13849
+ return mode2 === "standard" || mode2 === "plan" || mode2 === "debug" || mode2 === "skip";
13850
+ }
13851
+ function detectAnalysisMode(noFilesChanged, assistantResponse, conversationPrompts, actionSummary, sessionAuthoredCode) {
13852
+ const agentAuthoredCode = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0)) || !!sessionAuthoredCode;
13853
+ if (conversationPrompts.length > 0 && conversationPrompts.every(isGitOnlyPrompt)) {
13854
+ if (!agentAuthoredCode) return "skip";
13855
+ }
13856
+ if (noFilesChanged && !!assistantResponse && !agentAuthoredCode) {
13857
+ return "plan";
13858
+ }
13859
+ if (!!actionSummary && !agentAuthoredCode && !!assistantResponse) {
13860
+ return didAgentInvestigate(actionSummary) ? "plan" : "skip";
13861
+ }
13862
+ for (const prompt of conversationPrompts) {
13863
+ if (hasDebugIntent(prompt)) {
13864
+ return "debug";
13865
+ }
13866
+ }
13867
+ return "standard";
13868
+ }
13869
+ var FILE_MUTATE_RE = /(?:^|[\s|&;(`])(?:sed\s+-i|perl\s+-i|awk\b|tee\b|dd\b|cp\b|mv\b|ln\b|install\b|touch\b|patch\b|git\s+(?:apply|am)\b|cargo\s+build|go\s+generate|make\b|--write\b|--fix\b|--in-place\b)|>>?(?![&>])/i;
13870
+ var GIT_PLUMBING_RE = /^\s*git\s+(?:merge|rebase|stash|cherry-pick|revert|pull|fetch|checkout|switch|reset|restore|clean)\b/i;
13871
+ var READ_ONLY_RE = /^\s*(?:git\s+(?:status|diff|log|show|branch|remote|config|rev-parse|ls-files|blame|describe)|ls|cat|head|tail|less|grep|rg|find|pwd|echo|printf|wc|which|type|tree|stat|file|env|printenv|date|whoami)\b/i;
13872
+ var CHAIN_RE = /&&|\||;|\$\(|\x60/;
13873
+ function isNonAuthoringCommand(cmd) {
13874
+ if (typeof cmd !== "string" || cmd.trim().length === 0) return false;
13875
+ if (FILE_MUTATE_RE.test(cmd)) return false;
13876
+ if (CHAIN_RE.test(cmd)) return false;
13877
+ return GIT_PLUMBING_RE.test(cmd) || READ_ONLY_RE.test(cmd);
13878
+ }
13879
+ function hasNonEditAuthorship(actionSummary, sessionAuthoredCode) {
13880
+ if (!actionSummary) return sessionAuthoredCode;
13881
+ if ((actionSummary.subagents ?? 0) > 0) return true;
13882
+ if (Object.keys(actionSummary.tool_counts ?? {}).some((t) => t.startsWith("mcp__"))) return true;
13883
+ const commands = actionSummary.commands ?? [];
13884
+ if (commands.some((c) => FILE_MUTATE_RE.test(c))) return true;
13885
+ if (sessionAuthoredCode) {
13886
+ const allSafe = commands.length > 0 && commands.every(isNonAuthoringCommand);
13887
+ if (!allSafe) return true;
13888
+ }
13889
+ return false;
13890
+ }
13891
+ function scopeToAuthored(files, actionSummary) {
13892
+ if (!actionSummary) return { files, signal: "no-transcript" };
13893
+ const touched = [...actionSummary.files_edited ?? [], ...actionSummary.files_created ?? []];
13894
+ if (touched.length === 0) return { files: [], signal: "none-authored" };
13895
+ return { files: narrowToAgentAuthored(files, actionSummary), signal: "authored" };
13896
+ }
13897
+ function narrowToAgentAuthored(files, actionSummary) {
13898
+ if (!actionSummary) return files;
13899
+ const touched = [
13900
+ ...actionSummary.files_edited,
13901
+ ...actionSummary.files_created
13902
+ ];
13903
+ if (touched.length === 0) return files;
13904
+ return files.filter((f) => {
13905
+ const suffix = "/" + f;
13906
+ return touched.some((t) => t === f || t.endsWith(suffix));
13907
+ });
13908
+ }
13909
+
13750
13910
  // src/lib/skip-detection.ts
13751
13911
  function isBareAckPrompt(prompt) {
13752
13912
  if (typeof prompt !== "string") return false;
@@ -13810,6 +13970,15 @@ function shouldSkipForBareAck(input) {
13810
13970
  if (input.turnAuthoredCode) return false;
13811
13971
  return input.canSeeTurnAuthorship;
13812
13972
  }
13973
+ function isCommandOnlyTurn(input) {
13974
+ if (!input.authorshipIsObservable) return false;
13975
+ if (input.userCommandsTruncated) return false;
13976
+ const commands = input.userCommands ?? [];
13977
+ if (commands.length === 0) return false;
13978
+ if (input.agentAuthoredFiles > 0) return false;
13979
+ if (input.agentToolCalls > 0) return false;
13980
+ return commands.every(isNonAuthoringCommand);
13981
+ }
13813
13982
 
13814
13983
  // src/lib/pending-repeat.ts
13815
13984
  var STOP = /* @__PURE__ */ new Set([
@@ -16119,17 +16288,17 @@ async function readHookStdin() {
16119
16288
  try {
16120
16289
  if (process.stdin.isTTY) return {};
16121
16290
  const chunks = [];
16122
- const timeout = new Promise((resolve3) => setTimeout(() => resolve3({}), 500));
16123
- const read = new Promise((resolve3) => {
16291
+ const timeout = new Promise((resolve4) => setTimeout(() => resolve4({}), 500));
16292
+ const read = new Promise((resolve4) => {
16124
16293
  process.stdin.on("data", (c) => chunks.push(c));
16125
16294
  process.stdin.on("end", () => {
16126
16295
  try {
16127
- resolve3(JSON.parse(Buffer.concat(chunks).toString("utf-8").trim() || "{}"));
16296
+ resolve4(JSON.parse(Buffer.concat(chunks).toString("utf-8").trim() || "{}"));
16128
16297
  } catch {
16129
- resolve3({});
16298
+ resolve4({});
16130
16299
  }
16131
16300
  });
16132
- process.stdin.on("error", () => resolve3({}));
16301
+ process.stdin.on("error", () => resolve4({}));
16133
16302
  process.stdin.resume();
16134
16303
  });
16135
16304
  return await Promise.race([read, timeout]);
@@ -16140,7 +16309,145 @@ async function readHookStdin() {
16140
16309
 
16141
16310
  // src/commands/standard.ts
16142
16311
  var import_promises9 = require("node:fs/promises");
16312
+ var import_node_fs20 = require("node:fs");
16143
16313
  var import_yaml = __toESM(require_dist());
16314
+
16315
+ // src/lib/verityignore.ts
16316
+ var import_node_fs19 = require("node:fs");
16317
+ var EMPTY = { rules: [], securityOverlap: [], problems: [] };
16318
+ var SECURITY_PROBES = [
16319
+ ".env",
16320
+ ".env.local",
16321
+ ".env.production",
16322
+ "config/.env",
16323
+ "services/api/.env",
16324
+ "package-lock.json",
16325
+ "yarn.lock",
16326
+ "pnpm-lock.yaml",
16327
+ "Cargo.lock",
16328
+ "go.sum",
16329
+ "Gemfile.lock",
16330
+ "Dockerfile",
16331
+ "docker/Dockerfile"
16332
+ ];
16333
+ function isSecuritySensitive(path) {
16334
+ return SECURITY_PATTERNS.some((p) => p.test(path));
16335
+ }
16336
+ function compile(pattern) {
16337
+ let p = pattern;
16338
+ const dirOnly = p.endsWith("/");
16339
+ if (dirOnly) p = p.slice(0, -1);
16340
+ const anchored = p.includes("/");
16341
+ if (p.startsWith("/")) p = p.slice(1);
16342
+ const base = anchored ? p : `**/${p}`;
16343
+ const forms = dirOnly ? [`${base}/**/*`] : [base, `${base}/**`];
16344
+ const regexes = [];
16345
+ for (const f of forms) {
16346
+ try {
16347
+ regexes.push(globToRegex(f));
16348
+ } catch {
16349
+ }
16350
+ }
16351
+ if (regexes.length === 0) return () => false;
16352
+ return (path) => regexes.some((r) => r.test(path));
16353
+ }
16354
+ function parseVerityIgnore(content) {
16355
+ const rules = [];
16356
+ const problems = [];
16357
+ const lines = content.split("\n");
16358
+ for (let i = 0; i < lines.length; i++) {
16359
+ const lineNo = i + 1;
16360
+ let raw = lines[i];
16361
+ raw = raw.replace(/(?<!\\)\s+$/, "");
16362
+ if (raw.length === 0) continue;
16363
+ if (raw.startsWith("#")) continue;
16364
+ let negated = false;
16365
+ if (raw.startsWith("!")) {
16366
+ negated = true;
16367
+ raw = raw.slice(1);
16368
+ } else if (raw.startsWith("\\!") || raw.startsWith("\\#")) {
16369
+ raw = raw.slice(1);
16370
+ }
16371
+ if (raw.length === 0) {
16372
+ problems.push(`line ${lineNo}: "!" with no pattern after it`);
16373
+ continue;
16374
+ }
16375
+ if (raw === "**" || raw === "*" || raw === "/" || raw === "**/*") {
16376
+ problems.push(
16377
+ `line ${lineNo}: "${raw}" would exclude the whole repository from review \u2014 refused. List the directories you mean instead.`
16378
+ );
16379
+ continue;
16380
+ }
16381
+ rules.push({ raw, line: lineNo, negated, test: compile(raw) });
16382
+ }
16383
+ const byRule = /* @__PURE__ */ new Map();
16384
+ for (const probe of SECURITY_PROBES) {
16385
+ let responsible = null;
16386
+ for (const rule of rules) {
16387
+ if (rule.test(probe)) responsible = rule.negated ? null : rule;
16388
+ }
16389
+ if (!responsible) continue;
16390
+ const entry = byRule.get(responsible.line) ?? { raw: responsible.raw, line: responsible.line, hides: [] };
16391
+ entry.hides.push(probe);
16392
+ byRule.set(responsible.line, entry);
16393
+ }
16394
+ const securityOverlap = [...byRule.values()].sort((a, b) => a.line - b.line);
16395
+ return { rules, securityOverlap, problems };
16396
+ }
16397
+ function decide(rules, path) {
16398
+ let excluded = false;
16399
+ for (const rule of rules) {
16400
+ if (rule.test(path)) excluded = !rule.negated;
16401
+ }
16402
+ return excluded;
16403
+ }
16404
+ function isIgnored(ig, path) {
16405
+ return decide(ig.rules, path);
16406
+ }
16407
+ function loadVerityIgnore() {
16408
+ const file = projectPath(VERITYIGNORE_FILE);
16409
+ if (!(0, import_node_fs19.existsSync)(file)) return EMPTY;
16410
+ try {
16411
+ return parseVerityIgnore((0, import_node_fs19.readFileSync)(file, "utf-8"));
16412
+ } catch {
16413
+ return EMPTY;
16414
+ }
16415
+ }
16416
+ function partitionIgnored(paths, ig) {
16417
+ const suspended = paths.some((p) => p === VERITYIGNORE_FILE);
16418
+ if (suspended || ig.rules.length === 0) {
16419
+ return { rules: ig.rules.length, kept: [...paths], ignored: [], securityExcluded: [], suspended };
16420
+ }
16421
+ const kept = [];
16422
+ const ignored = [];
16423
+ for (const p of paths) {
16424
+ if (p !== VERITYIGNORE_FILE && isIgnored(ig, p)) ignored.push(p);
16425
+ else kept.push(p);
16426
+ }
16427
+ return {
16428
+ rules: ig.rules.length,
16429
+ kept,
16430
+ ignored,
16431
+ securityExcluded: ignored.filter(isSecuritySensitive),
16432
+ suspended: false
16433
+ };
16434
+ }
16435
+ function ignoreShare(kept, ignored) {
16436
+ const total = kept + ignored;
16437
+ if (total === 0) return 0;
16438
+ return Math.round(ignored / total * 1e3) / 1e3;
16439
+ }
16440
+ function describeSecurityOverlap(ig) {
16441
+ if (ig.securityOverlap.length === 0) return null;
16442
+ const lines = ig.securityOverlap.map(
16443
+ (o) => ` line ${o.line}: "${o.raw}" would hide ${o.hides.slice(0, 3).join(", ")}` + (o.hides.length > 3 ? ` (+${o.hides.length - 3} more)` : "")
16444
+ );
16445
+ return `.verityignore: ${ig.securityOverlap.length} pattern(s) can hide security-sensitive files from review:
16446
+ ${lines.join("\n")}
16447
+ These are still excluded \u2014 this is a warning, not a refusal. Add a \`!\` rule to keep one in scope, e.g. \`!.env*\`.`;
16448
+ }
16449
+
16450
+ // src/commands/standard.ts
16144
16451
  function registerStandardCommands(program2) {
16145
16452
  const standard = program2.command("standard").description("Manage the project Standard");
16146
16453
  standard.command("push").description("Upload the project Standard to the service").option("--file <path>", "Path to standard YAML file", STANDARD_FILE).option("--created-by <name>", "Who created this version", "claude-code").action(async (opts) => {
@@ -16169,12 +16476,25 @@ function registerStandardCommands(program2) {
16169
16476
  printError(`Invalid YAML in ${opts.file}: ${err.message}`);
16170
16477
  process.exit(1);
16171
16478
  }
16479
+ let ignoreRaw = null;
16480
+ const ignorePath = projectPath(VERITYIGNORE_FILE);
16481
+ if ((0, import_node_fs20.existsSync)(ignorePath)) {
16482
+ try {
16483
+ ignoreRaw = (0, import_node_fs20.readFileSync)(ignorePath, "utf-8");
16484
+ const overlap = describeSecurityOverlap(parseVerityIgnore(ignoreRaw));
16485
+ if (overlap) printWarn(overlap);
16486
+ } catch {
16487
+ }
16488
+ }
16172
16489
  const result = await apiRequest({
16173
16490
  method: "POST",
16174
16491
  path: "/standards",
16175
16492
  serviceUrl: urlResult.data,
16176
16493
  token: tokenResult.data.token,
16177
- body: { content, created_by: opts.createdBy },
16494
+ body: {
16495
+ content: ignoreRaw === null ? content : { ...content, verityignore: ignoreRaw },
16496
+ created_by: opts.createdBy
16497
+ },
16178
16498
  verbose: globals.verbose
16179
16499
  });
16180
16500
  if (!result.ok) {
@@ -16392,27 +16712,330 @@ function formatRunDetail(run) {
16392
16712
  return lines;
16393
16713
  }
16394
16714
 
16395
- // src/commands/status.ts
16396
- function timeAgo(isoDate) {
16397
- const ms = Date.now() - new Date(isoDate).getTime();
16398
- const mins = Math.floor(ms / 6e4);
16399
- if (mins < 1) return "just now";
16400
- if (mins < 60) return `${mins}m ago`;
16401
- const hrs = Math.floor(mins / 60);
16402
- if (hrs < 24) return `${hrs}h ago`;
16403
- const days = Math.floor(hrs / 24);
16404
- return `${days}d ago`;
16715
+ // src/lib/ignore-declaration.ts
16716
+ var import_node_fs22 = require("node:fs");
16717
+
16718
+ // src/lib/debounce.ts
16719
+ var import_node_fs21 = require("node:fs");
16720
+ var import_node_crypto10 = require("node:crypto");
16721
+ function scopedFile(base, sessionId) {
16722
+ if (!sessionId) return base;
16723
+ return `${base}.${(0, import_node_crypto10.createHash)("sha1").update(sessionId).digest("hex").slice(0, 12)}`;
16405
16724
  }
16406
- function registerStatusCommand(program2) {
16407
- program2.command("status").description("Show project quality status").option("--history", "Include recent run history").option("--limit <n>", "Number of history entries", "5").option("--json", "Output raw JSON").action(async (opts) => {
16408
- const globals = program2.opts();
16409
- const tokenResult = await resolveToken(globals.token);
16410
- if (!tokenResult.ok) {
16411
- printError(tokenResult.error);
16412
- process.exit(1);
16413
- }
16414
- const urlResult = await resolveServiceUrl(globals.serviceUrl);
16415
- if (!urlResult.ok) {
16725
+ function checkDebounce(debounceSeconds = DEBOUNCE_SECONDS, sessionId) {
16726
+ const file = scopedFile(DEBOUNCE_FILE, sessionId);
16727
+ if (!(0, import_node_fs21.existsSync)(file)) return null;
16728
+ try {
16729
+ const lastTs = parseInt((0, import_node_fs21.readFileSync)(file, "utf-8").trim(), 10);
16730
+ const nowTs = Math.floor(Date.now() / 1e3);
16731
+ const elapsed = nowTs - lastTs;
16732
+ if (elapsed < debounceSeconds) {
16733
+ return `Debounced \u2014 last analysis was ${elapsed}s ago`;
16734
+ }
16735
+ } catch {
16736
+ }
16737
+ return null;
16738
+ }
16739
+ function checkMtime(files, bypassForRecentCommits, sessionId) {
16740
+ if (bypassForRecentCommits) return null;
16741
+ const file = scopedFile(DEBOUNCE_FILE, sessionId);
16742
+ if (!(0, import_node_fs21.existsSync)(file)) return null;
16743
+ let debounceTime;
16744
+ try {
16745
+ debounceTime = (0, import_node_fs21.statSync)(file).mtimeMs;
16746
+ } catch {
16747
+ return null;
16748
+ }
16749
+ for (const f of files) {
16750
+ const resolved = resolveFile(f);
16751
+ if (!resolved) continue;
16752
+ try {
16753
+ const stat3 = (0, import_node_fs21.statSync)(resolved);
16754
+ if (stat3.mtimeMs > debounceTime) {
16755
+ return null;
16756
+ }
16757
+ } catch {
16758
+ continue;
16759
+ }
16760
+ }
16761
+ return "No files modified since last analysis";
16762
+ }
16763
+ function computeContentHash(files) {
16764
+ const hash = (0, import_node_crypto10.createHash)("sha1");
16765
+ const sorted = [...files].sort();
16766
+ for (const f of sorted) {
16767
+ const resolved = resolveFile(f) ?? f;
16768
+ try {
16769
+ if ((0, import_node_fs21.existsSync)(resolved)) {
16770
+ hash.update((0, import_node_fs21.readFileSync)(resolved));
16771
+ }
16772
+ } catch {
16773
+ }
16774
+ }
16775
+ return hash.digest("hex");
16776
+ }
16777
+ function checkContentHash(files, sessionId) {
16778
+ const hash = computeContentHash(files);
16779
+ const file = scopedFile(HASH_FILE, sessionId);
16780
+ if ((0, import_node_fs21.existsSync)(file)) {
16781
+ try {
16782
+ const storedHash = (0, import_node_fs21.readFileSync)(file, "utf-8").trim();
16783
+ if (hash === storedHash) {
16784
+ return { skip: "No source changes since last analysis", hash };
16785
+ }
16786
+ } catch {
16787
+ }
16788
+ }
16789
+ return { skip: null, hash };
16790
+ }
16791
+ function recordAnalysisStart(sessionId) {
16792
+ (0, import_node_fs21.mkdirSync)(VERITY_DIR, { recursive: true });
16793
+ (0, import_node_fs21.writeFileSync)(scopedFile(DEBOUNCE_FILE, sessionId), String(Math.floor(Date.now() / 1e3)));
16794
+ }
16795
+ function recordPassHash(hash, sessionId) {
16796
+ (0, import_node_fs21.writeFileSync)(scopedFile(HASH_FILE, sessionId), hash);
16797
+ }
16798
+ function narrowToRecent(files, sessionId) {
16799
+ const file = scopedFile(DEBOUNCE_FILE, sessionId);
16800
+ if (!(0, import_node_fs21.existsSync)(file)) return files;
16801
+ let debounceTime;
16802
+ try {
16803
+ debounceTime = (0, import_node_fs21.statSync)(file).mtimeMs;
16804
+ } catch {
16805
+ return files;
16806
+ }
16807
+ const recent = files.filter((f) => {
16808
+ try {
16809
+ return (0, import_node_fs21.existsSync)(f) && (0, import_node_fs21.statSync)(f).mtimeMs > debounceTime;
16810
+ } catch {
16811
+ return false;
16812
+ }
16813
+ });
16814
+ return recent.length > 0 ? recent : files;
16815
+ }
16816
+ function readIteration(currentCommit, _contentHash) {
16817
+ return Math.max(1, readBlockState(currentCommit).attempts);
16818
+ }
16819
+ var NO_BLOCKS = { attempts: 0, blocks: 0, fingerprint: null };
16820
+ function readBlockState(currentCommit, opts) {
16821
+ if (opts?.newUserPrompt) return NO_BLOCKS;
16822
+ if (!(0, import_node_fs21.existsSync)(ITERATION_FILE)) return NO_BLOCKS;
16823
+ try {
16824
+ const stored = (0, import_node_fs21.readFileSync)(ITERATION_FILE, "utf-8").trim();
16825
+ const parsed = stored.startsWith("{") ? parseJsonState(stored) : parseLegacyState(stored);
16826
+ if (!parsed) return NO_BLOCKS;
16827
+ if (parsed.commit !== currentCommit) return NO_BLOCKS;
16828
+ if (parsed.ts > 0 && Math.floor(Date.now() / 1e3) - parsed.ts > 600) return NO_BLOCKS;
16829
+ return { attempts: parsed.attempts, blocks: parsed.blocks, fingerprint: parsed.fingerprint };
16830
+ } catch {
16831
+ return NO_BLOCKS;
16832
+ }
16833
+ }
16834
+ function parseJsonState(raw) {
16835
+ const o = JSON.parse(raw);
16836
+ const attempts = typeof o.attempts === "number" ? o.attempts : NaN;
16837
+ if (isNaN(attempts)) return null;
16838
+ return {
16839
+ attempts,
16840
+ blocks: typeof o.blocks === "number" ? o.blocks : attempts,
16841
+ fingerprint: typeof o.fingerprint === "string" && o.fingerprint ? o.fingerprint : null,
16842
+ commit: typeof o.commit === "string" ? o.commit : "",
16843
+ ts: typeof o.ts === "number" ? o.ts : 0
16844
+ };
16845
+ }
16846
+ function parseLegacyState(raw) {
16847
+ const parts = raw.split(":");
16848
+ const n = parseInt(parts[0], 10);
16849
+ if (isNaN(n)) return null;
16850
+ return {
16851
+ attempts: n,
16852
+ // The old file has no separate block count; the old counter is the closest
16853
+ // honest answer, and it errs toward releasing sooner rather than later.
16854
+ blocks: n,
16855
+ fingerprint: parts.slice(3).join(":") || null,
16856
+ commit: parts[1] ?? "",
16857
+ ts: parseInt(parts[2] ?? "0", 10)
16858
+ };
16859
+ }
16860
+ function findingsFingerprint(findings) {
16861
+ const keys = findings.map((f) => `${String(f.pattern_id ?? "?")}|${String(f.file ?? "?")}`).filter((k) => k !== "?|?");
16862
+ return [...new Set(keys)].sort().join(",");
16863
+ }
16864
+ function isSameProblem(previous, current) {
16865
+ if (!previous || !current) return false;
16866
+ const prev = new Set(previous.split(","));
16867
+ return current.split(",").some((k) => prev.has(k));
16868
+ }
16869
+ function writeBlockState(commit, state) {
16870
+ (0, import_node_fs21.mkdirSync)(VERITY_DIR, { recursive: true });
16871
+ (0, import_node_fs21.writeFileSync)(
16872
+ ITERATION_FILE,
16873
+ JSON.stringify({
16874
+ v: 2,
16875
+ attempts: state.attempts,
16876
+ blocks: state.blocks,
16877
+ commit,
16878
+ ts: Math.floor(Date.now() / 1e3),
16879
+ fingerprint: state.fingerprint ?? void 0
16880
+ })
16881
+ );
16882
+ }
16883
+ function resetBlockState(commit) {
16884
+ writeBlockState(commit, { attempts: 0, blocks: 0, fingerprint: null });
16885
+ }
16886
+
16887
+ // src/lib/ignore-declaration.ts
16888
+ var IGNORE_BUDGET = 3;
16889
+ var MAX_WINDOW_SECONDS = 60 * 60;
16890
+ var TURN_FUSE_SECONDS = 10 * 60;
16891
+ function parseDuration(input) {
16892
+ const trimmed = input.trim().toLowerCase();
16893
+ const m = /^(\d+)(s|m|h)?$/.exec(trimmed);
16894
+ if (!m) {
16895
+ return { ok: false, error: `Could not read "${input}" as a duration. Use 30m, 45s, or 1h.` };
16896
+ }
16897
+ const n = parseInt(m[1], 10);
16898
+ if (!Number.isFinite(n) || n <= 0) {
16899
+ return { ok: false, error: `A duration must be a positive number of seconds, minutes or hours \u2014 got "${input}".` };
16900
+ }
16901
+ const unit = m[2] ?? "m";
16902
+ const seconds = unit === "s" ? n : unit === "h" ? n * 3600 : n * 60;
16903
+ if (seconds > MAX_WINDOW_SECONDS) {
16904
+ return {
16905
+ ok: false,
16906
+ error: `${input} is longer than the ${MAX_WINDOW_SECONDS / 60}-minute maximum. An ignore is meant to cover one piece of housekeeping, not a sitting \u2014 declare it again when you need it.`
16907
+ };
16908
+ }
16909
+ return { ok: true, seconds };
16910
+ }
16911
+ function resolveActive(state, now) {
16912
+ if (!state?.active) return null;
16913
+ if (state.active.expires <= now) return null;
16914
+ return state.active;
16915
+ }
16916
+ function verifyDeclaration(input) {
16917
+ const d = input.declaration;
16918
+ if (!d) return { honoured: false, void: false };
16919
+ if (!input.authorshipIsObservable) {
16920
+ return {
16921
+ honoured: false,
16922
+ void: true,
16923
+ why: "the transcript window could not show what this turn did, so the declaration could not be checked"
16924
+ };
16925
+ }
16926
+ if (input.commandRecordTruncated) {
16927
+ return {
16928
+ honoured: false,
16929
+ void: true,
16930
+ why: "the record of commands run this turn was incomplete, so the declaration could not be checked"
16931
+ };
16932
+ }
16933
+ if (input.agentAuthoredFiles > 0) {
16934
+ return {
16935
+ honoured: false,
16936
+ void: true,
16937
+ why: `${input.agentAuthoredFiles} file${input.agentAuthoredFiles === 1 ? " was" : "s were"} authored during it`
16938
+ };
16939
+ }
16940
+ if (input.subagents > 0) {
16941
+ return {
16942
+ honoured: false,
16943
+ void: true,
16944
+ why: "work was delegated to a subagent during it, whose authorship this turn cannot account for"
16945
+ };
16946
+ }
16947
+ const authoring = [...input.agentCommands ?? [], ...input.userCommands ?? []].filter(
16948
+ (c) => !isNonAuthoringCommand(c)
16949
+ );
16950
+ if (authoring.length > 0) {
16951
+ return {
16952
+ honoured: false,
16953
+ void: true,
16954
+ why: `a command that can write files ran during it (${authoring[0]})`
16955
+ };
16956
+ }
16957
+ return { honoured: true };
16958
+ }
16959
+ function stateFile(sessionId) {
16960
+ return projectPath(scopedFile(IGNORE_DECLARATION_FILE, sessionId));
16961
+ }
16962
+ function ignoreStateKeys(token, sessionId) {
16963
+ const scoped = sessionScopeKey(token, sessionId);
16964
+ const userOnly = sessionScopeKey(token, void 0);
16965
+ return scoped === userOnly ? [scoped] : [scoped, userOnly];
16966
+ }
16967
+ function resolveIgnoreState(keys) {
16968
+ for (const key of keys) {
16969
+ const state = readIgnoreState(key);
16970
+ if (state) return { state, key };
16971
+ }
16972
+ return null;
16973
+ }
16974
+ function readIgnoreState(sessionId) {
16975
+ const file = stateFile(sessionId);
16976
+ if (!(0, import_node_fs22.existsSync)(file)) return null;
16977
+ try {
16978
+ const o = JSON.parse((0, import_node_fs22.readFileSync)(file, "utf-8")) ?? {};
16979
+ const spent = typeof o.spent === "number" ? o.spent : 0;
16980
+ const raw = o.active;
16981
+ let active = null;
16982
+ if (raw && typeof raw === "object") {
16983
+ const scope2 = raw.scope === "window" ? "window" : raw.scope === "turn" ? "turn" : null;
16984
+ const expires = typeof raw.expires === "number" ? raw.expires : null;
16985
+ if (scope2 && expires != null) {
16986
+ active = {
16987
+ scope: scope2,
16988
+ origin: raw.origin === "agent" ? "agent" : "user",
16989
+ reason: typeof raw.reason === "string" ? raw.reason : "",
16990
+ at: typeof raw.at === "number" ? raw.at : 0,
16991
+ expires
16992
+ };
16993
+ }
16994
+ }
16995
+ return { v: 1, active, spent };
16996
+ } catch {
16997
+ return null;
16998
+ }
16999
+ }
17000
+ function writeIgnoreState(state, sessionId) {
17001
+ try {
17002
+ (0, import_node_fs22.mkdirSync)(projectPath(VERITY_DIR), { recursive: true });
17003
+ (0, import_node_fs22.writeFileSync)(stateFile(sessionId), JSON.stringify({ v: 1, active: state.active, spent: state.spent }));
17004
+ } catch {
17005
+ }
17006
+ }
17007
+ function clearActiveDeclaration(sessionId) {
17008
+ const prev = readIgnoreState(sessionId);
17009
+ if (!prev) return;
17010
+ writeIgnoreState({ v: 1, active: null, spent: prev.spent }, sessionId);
17011
+ }
17012
+ function describeRemaining(d, now) {
17013
+ const left = Math.max(0, d.expires - now);
17014
+ if (left >= 90) return `${Math.round(left / 60)}m left`;
17015
+ return `${left}s left`;
17016
+ }
17017
+
17018
+ // src/commands/status.ts
17019
+ function timeAgo(isoDate) {
17020
+ const ms = Date.now() - new Date(isoDate).getTime();
17021
+ const mins = Math.floor(ms / 6e4);
17022
+ if (mins < 1) return "just now";
17023
+ if (mins < 60) return `${mins}m ago`;
17024
+ const hrs = Math.floor(mins / 60);
17025
+ if (hrs < 24) return `${hrs}h ago`;
17026
+ const days = Math.floor(hrs / 24);
17027
+ return `${days}d ago`;
17028
+ }
17029
+ function registerStatusCommand(program2) {
17030
+ program2.command("status").description("Show project quality status").option("--history", "Include recent run history").option("--limit <n>", "Number of history entries", "5").option("--json", "Output raw JSON").action(async (opts) => {
17031
+ const globals = program2.opts();
17032
+ const tokenResult = await resolveToken(globals.token);
17033
+ if (!tokenResult.ok) {
17034
+ printError(tokenResult.error);
17035
+ process.exit(1);
17036
+ }
17037
+ const urlResult = await resolveServiceUrl(globals.serviceUrl);
17038
+ if (!urlResult.ok) {
16416
17039
  printError(urlResult.error);
16417
17040
  process.exit(1);
16418
17041
  }
@@ -16500,6 +17123,39 @@ function registerStatusCommand(program2) {
16500
17123
  printInfo(`Trend: ${r.trend}`);
16501
17124
  printInfo(`Runs: ${r.count} recorded`);
16502
17125
  }
17126
+ {
17127
+ const tokenForScope = tokenResult.data.token;
17128
+ const found = resolveIgnoreState(
17129
+ ignoreStateKeys(tokenForScope, process.env.CLAUDE_SESSION_ID || void 0)
17130
+ );
17131
+ const now = Math.floor(Date.now() / 1e3);
17132
+ const active = resolveActive(found?.state ?? null, now);
17133
+ const spent = found?.state.spent ?? 0;
17134
+ if (active) {
17135
+ printInfo("");
17136
+ printInfo("--- Ignore Declared ---");
17137
+ printInfo(`Scope: ${active.scope === "turn" ? "the next turn" : `a window, ${describeRemaining(active, now)}`}`);
17138
+ printInfo(`Reason: ${active.reason}`);
17139
+ printInfo(`Declared by: ${active.origin === "agent" ? "the agent" : "you"}`);
17140
+ printInfo(`Budget: ${spent}/${IGNORE_BUDGET} declarations this session`);
17141
+ printInfo('Turns that author anything are still reviewed \u2014 the declaration voids. "verity ignore clear" cancels it.');
17142
+ } else if (spent > 0) {
17143
+ printInfo("");
17144
+ printInfo(`Ignore budget: ${spent}/${IGNORE_BUDGET} declarations used this session.`);
17145
+ }
17146
+ }
17147
+ {
17148
+ const ig = loadVerityIgnore();
17149
+ if (ig.rules.length > 0 || ig.problems.length > 0) {
17150
+ printInfo("");
17151
+ printInfo("--- .verityignore ---");
17152
+ printInfo(`Rules: ${ig.rules.length}`);
17153
+ for (const problem of ig.problems) printWarn(` ${problem}`);
17154
+ const overlap = describeSecurityOverlap(ig);
17155
+ if (overlap) printWarn(overlap);
17156
+ printInfo("An edit to .verityignore suspends every rule for that turn \u2014 the edit is always reviewed.");
17157
+ }
17158
+ }
16503
17159
  if (mem.pending_items && mem.pending_items.length > 0) {
16504
17160
  printInfo("");
16505
17161
  printInfo("--- Pending Items ---");
@@ -16647,6 +17303,7 @@ function createRun(opts, globals) {
16647
17303
  phaseReached: "",
16648
17304
  phasesCompleted: [],
16649
17305
  skipReason: null,
17306
+ treeFrame: null,
16650
17307
  turnId: "",
16651
17308
  // The value `resolveReachability` itself returns when every rung declines.
16652
17309
  reachability: { human_reachable: "unknown", human_reachable_source: "inferred", rung: "none" },
@@ -16662,6 +17319,7 @@ function createRun(opts, globals) {
16662
17319
  allChanged: [],
16663
17320
  hasRecentCommitFiles: false,
16664
17321
  changedUniverse: [],
17322
+ verityIgnored: { rules: 0, kept: [], ignored: [], securityExcluded: [], suspended: false },
16665
17323
  analyzable: [],
16666
17324
  reviewable: [],
16667
17325
  securityFiles: [],
@@ -16695,6 +17353,7 @@ function createRun(opts, globals) {
16695
17353
  deletedNodePaths: [],
16696
17354
  editedUploads: [],
16697
17355
  autoSeedNotice: null,
17356
+ voidedIgnoreNotice: null,
16698
17357
  foldResult: null,
16699
17358
  foldConservation: null,
16700
17359
  memorySession: null,
@@ -16715,7 +17374,7 @@ function createRun(opts, globals) {
16715
17374
  }
16716
17375
 
16717
17376
  // src/lib/stderr-log.ts
16718
- var import_node_fs19 = require("node:fs");
17377
+ var import_node_fs23 = require("node:fs");
16719
17378
  var TOKEN_RE2 = /verity_[0-9a-f]{16,}/g;
16720
17379
  var ANSI_RE = /\u001b\[[0-?]*[ -/]*[@-~]/g;
16721
17380
  function scrub(s) {
@@ -16728,9 +17387,9 @@ function append(text) {
16728
17387
  try {
16729
17388
  const dir = projectPath(DEBUG_LOG_DIR);
16730
17389
  const file = projectPath(STDERR_LOG_FILE);
16731
- (0, import_node_fs19.mkdirSync)(dir, { recursive: true });
17390
+ (0, import_node_fs23.mkdirSync)(dir, { recursive: true });
16732
17391
  rotateIfNeeded(file);
16733
- (0, import_node_fs19.appendFileSync)(file, text);
17392
+ (0, import_node_fs23.appendFileSync)(file, text);
16734
17393
  } catch {
16735
17394
  }
16736
17395
  }
@@ -16786,6 +17445,10 @@ function formatRunEvidence(run, startedAt) {
16786
17445
  `;
16787
17446
  out += row("turn", `${run.turnId || "(unminted)"}${run.sessionId ? ` \xB7 session ${run.sessionId}` : ""}`);
16788
17447
  out += row("reached", `${run.phaseReached || "(none)"}${run.skipReason ? ` \xB7 SKIPPED: ${run.skipReason}` : ""} \xB7 ${ms}ms`);
17448
+ if (run.treeFrame) {
17449
+ const f = run.treeFrame;
17450
+ out += row("tree", f.worktreeRoot ? `${f.worktreeRoot}${f.isLinkedWorktree ? " \xB7 linked worktree" : ""}${f.branch ? ` \xB7 branch ${f.branch}` : " \xB7 detached"}` : `(unresolved: ${f.refusal ?? "unknown"})`);
17451
+ }
16789
17452
  out += row("changed", `${run.changedUniverse.length} from git \xB7 analyzable ${run.analyzable.length} \xB7 reviewable ${run.reviewable.length} \xB7 security ${run.securityFiles.length} \xB7 forReview ${run.allForReview.length}`);
16790
17453
  const done = (phase) => run.phasesCompleted.includes(phase);
16791
17454
  const ifDone = (phase, value) => done(phase) ? value : "?";
@@ -16882,17 +17545,312 @@ function installRunEvidence(run) {
16882
17545
  });
16883
17546
  }
16884
17547
 
16885
- // src/lib/reachability.ts
16886
- function resolveReachability(input = {}) {
16887
- const env = input.env ?? {};
16888
- if (input.autonomousFlag === true) {
16889
- return { human_reachable: "no", human_reachable_source: "declared", rung: "flag" };
17548
+ // src/lib/git-frame.ts
17549
+ var import_node_child_process7 = require("node:child_process");
17550
+ var import_node_fs24 = require("node:fs");
17551
+ var import_node_os3 = require("node:os");
17552
+ var import_node_path18 = require("node:path");
17553
+ var import_node_path19 = require("node:path");
17554
+ var VALUE_TOKEN = `(?:'[^']*'|"[^"]*"|\\S+)`;
17555
+ var GIT_GLOBAL_OPTS = `(?:\\s+(?:-[Cc]\\s+${VALUE_TOKEN}|--?[\\w-]+(?:=\\S+)?))*`;
17556
+ var COMMIT_HEAD = `git${GIT_GLOBAL_OPTS}\\s+commit(?![\\w-])`;
17557
+ var PUSH_HEAD = `git${GIT_GLOBAL_OPTS}\\s+push\\b`;
17558
+ var GH_PR_HEAD = `gh${GIT_GLOBAL_OPTS}\\s+pr\\s+create\\b`;
17559
+ var COMMIT_RE = new RegExp(`(?:^|[\\s;&|(])${COMMIT_HEAD}|(?:^|[;&|(])\\s*[^\\s;&|()'"]*\\/${COMMIT_HEAD}`);
17560
+ var PUSH_RE = new RegExp(`(?:^|[\\s;&|(])${PUSH_HEAD}|(?:^|[;&|(])\\s*[^\\s;&|()'"]*\\/${PUSH_HEAD}`);
17561
+ var GH_PR_RE = new RegExp(`(?:^|[\\s;&|(])${GH_PR_HEAD}|(?:^|[;&|(])\\s*[^\\s;&|()'"]*\\/${GH_PR_HEAD}`);
17562
+ function splitSegments(command) {
17563
+ return (command ?? "").split(/&&|\|\||;|\n/);
17564
+ }
17565
+ function findMomentSegment(command, on) {
17566
+ const segments = splitSegments(command);
17567
+ let commitIdx = -1;
17568
+ let pushIdx = -1;
17569
+ for (let i = 0; i < segments.length; i++) {
17570
+ const seg = segments[i];
17571
+ if (/--dry-run\b/.test(seg)) continue;
17572
+ if (commitIdx === -1 && COMMIT_RE.test(seg)) commitIdx = i;
17573
+ if (pushIdx === -1 && (PUSH_RE.test(seg) || GH_PR_RE.test(seg))) pushIdx = i;
16890
17574
  }
16891
- if (truthy(env.VERITY_AUTONOMOUS)) {
16892
- return { human_reachable: "no", human_reachable_source: "declared", rung: "env" };
17575
+ if (commitIdx !== -1 && on.includes("commit")) return { moment: "pre-commit", segmentIndex: commitIdx };
17576
+ if (pushIdx !== -1 && on.includes("push")) return { moment: "pre-push", segmentIndex: pushIdx };
17577
+ return null;
17578
+ }
17579
+ function classifyCommand(command, on) {
17580
+ return findMomentSegment(command, on)?.moment ?? null;
17581
+ }
17582
+ function unquote(token) {
17583
+ if (token.length >= 2) {
17584
+ const first = token[0];
17585
+ if ((first === "'" || first === '"') && token.endsWith(first)) return token.slice(1, -1);
17586
+ }
17587
+ return token;
17588
+ }
17589
+ var SHELL_DYNAMIC = /[$`\\]/;
17590
+ function extractCommandTarget(command, segmentIndex, baseDir) {
17591
+ const segments = splitSegments(command);
17592
+ let dir = baseDir;
17593
+ let named = false;
17594
+ for (let i = 0; i < segmentIndex; i++) {
17595
+ const m = segments[i].match(new RegExp(`^\\s*cd(?:\\s+(${VALUE_TOKEN}))?\\s*$`));
17596
+ if (!m) continue;
17597
+ named = true;
17598
+ if (m[1] === void 0) {
17599
+ dir = (0, import_node_os3.homedir)();
17600
+ continue;
17601
+ }
17602
+ const raw = unquote(m[1]);
17603
+ if (SHELL_DYNAMIC.test(raw) || raw === "-") {
17604
+ return { dir: null, named: true, unresolvable: `cd target not statically resolvable: ${raw}` };
17605
+ }
17606
+ const expanded = raw === "~" ? (0, import_node_os3.homedir)() : raw.startsWith("~/") ? (0, import_node_path19.join)((0, import_node_os3.homedir)(), raw.slice(2)) : raw;
17607
+ dir = (0, import_node_path18.isAbsolute)(expanded) ? expanded : (0, import_node_path18.resolve)(dir, expanded);
17608
+ }
17609
+ const seg = segments[segmentIndex];
17610
+ const overrideMatch = /--(?:git-dir|work-tree)(?:=|\s)|\bGIT_(?:DIR|WORK_TREE|INDEX_FILE)=/.exec(seg);
17611
+ if (overrideMatch) {
17612
+ return { dir: null, named: true, unresolvable: `git-dir/work-tree/index override in command: ${overrideMatch[0].trim()}` };
17613
+ }
17614
+ const gitMatch = seg.match(new RegExp(`(?:^|[\\s;&|(])(?:[^\\s;&|()'"]*\\/)?git(${GIT_GLOBAL_OPTS})\\s`));
17615
+ if (gitMatch) {
17616
+ const optsRegion = gitMatch[1] ?? "";
17617
+ const cRe = new RegExp(`-C\\s+(${VALUE_TOKEN})`, "g");
17618
+ let cm;
17619
+ while ((cm = cRe.exec(optsRegion)) !== null) {
17620
+ named = true;
17621
+ const raw = unquote(cm[1]);
17622
+ if (SHELL_DYNAMIC.test(raw)) {
17623
+ return { dir: null, named: true, unresolvable: `-C target not statically resolvable: ${raw}` };
17624
+ }
17625
+ const expanded = raw === "~" ? (0, import_node_os3.homedir)() : raw.startsWith("~/") ? (0, import_node_path19.join)((0, import_node_os3.homedir)(), raw.slice(2)) : raw;
17626
+ dir = (0, import_node_path18.isAbsolute)(expanded) ? expanded : (0, import_node_path18.resolve)(dir, expanded);
17627
+ }
17628
+ }
17629
+ return { dir, named, unresolvable: null };
17630
+ }
17631
+ var PUSH_VALUE_FLAGS = /* @__PURE__ */ new Set(["-o", "--push-option", "--receive-pack", "--exec"]);
17632
+ function parsePushTarget(segment) {
17633
+ const none = { remote: null, srcRef: null, dstRef: null, isDelete: false };
17634
+ const m = PUSH_RE.exec(segment);
17635
+ if (!m) return none;
17636
+ const rest = segment.slice(m.index + m[0].length);
17637
+ const tokens = (rest.match(new RegExp(`'[^']*'|"[^"]*"|\\S+`, "g")) ?? []).map(unquote);
17638
+ let remote = null;
17639
+ let refspec = null;
17640
+ let isDelete = false;
17641
+ for (let i = 0; i < tokens.length; i++) {
17642
+ const t = tokens[i];
17643
+ if (t === "-d" || t === "--delete") {
17644
+ isDelete = true;
17645
+ continue;
17646
+ }
17647
+ if (t.startsWith("--repo=")) {
17648
+ remote = t.slice("--repo=".length);
17649
+ continue;
17650
+ }
17651
+ if (t === "--repo") {
17652
+ remote = tokens[++i] ?? null;
17653
+ continue;
17654
+ }
17655
+ if (PUSH_VALUE_FLAGS.has(t)) {
17656
+ i++;
17657
+ continue;
17658
+ }
17659
+ if (t.startsWith("-")) continue;
17660
+ if (SHELL_DYNAMIC.test(t)) return none;
17661
+ if (remote === null) {
17662
+ remote = t;
17663
+ continue;
17664
+ }
17665
+ if (refspec === null) {
17666
+ refspec = t;
17667
+ continue;
17668
+ }
17669
+ break;
16893
17670
  }
16894
- if (truthy(env.CI) || truthy(env.GITHUB_ACTIONS) || truthy(env.BUILDKITE) || truthy(env.JENKINS_URL)) {
16895
- return { human_reachable: "no", human_reachable_source: "declared", rung: "env" };
17671
+ if (refspec === null) return { remote, srcRef: null, dstRef: null, isDelete };
17672
+ const spec = refspec.startsWith("+") ? refspec.slice(1) : refspec;
17673
+ const colon = spec.indexOf(":");
17674
+ if (colon === -1) return { remote, srcRef: spec, dstRef: null, isDelete };
17675
+ const src = spec.slice(0, colon);
17676
+ const dst = spec.slice(colon + 1);
17677
+ if (src === "") return { remote, srcRef: null, dstRef: dst || null, isDelete: true };
17678
+ return { remote, srcRef: src, dstRef: dst || null, isDelete };
17679
+ }
17680
+ function gitAt(dir, args) {
17681
+ try {
17682
+ return (0, import_node_child_process7.execFileSync)("git", args, { cwd: dir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
17683
+ } catch {
17684
+ return "";
17685
+ }
17686
+ }
17687
+ function realpathOr(p) {
17688
+ try {
17689
+ return import_node_fs24.realpathSync.native(p);
17690
+ } catch {
17691
+ return (0, import_node_path18.resolve)(p);
17692
+ }
17693
+ }
17694
+ function resolveFrame(input) {
17695
+ const found = findMomentSegment(input.command, input.on);
17696
+ const hookDirUsable = !!input.hookCwd && (0, import_node_fs24.existsSync)(input.hookCwd);
17697
+ const baseDir = hookDirUsable ? input.hookCwd : process.cwd();
17698
+ let anchor = hookDirUsable ? "hook-cwd" : "process-cwd";
17699
+ const refuse = (refusal) => ({
17700
+ moment: found?.moment ?? null,
17701
+ frame: { worktreeRoot: null, gitDir: null, commonDir: null, isLinkedWorktree: false, branch: null, anchor, refusal }
17702
+ });
17703
+ let dir = baseDir;
17704
+ if (found) {
17705
+ const segments = splitSegments(input.command);
17706
+ const kindRes = found.moment === "pre-commit" ? [COMMIT_RE] : [PUSH_RE, GH_PR_RE];
17707
+ const dirs = /* @__PURE__ */ new Set();
17708
+ for (let i = 0; i < segments.length; i++) {
17709
+ if (/--dry-run\b/.test(segments[i])) continue;
17710
+ if (!kindRes.some((re) => re.test(segments[i]))) continue;
17711
+ const target = extractCommandTarget(input.command, i, baseDir);
17712
+ if (target.named) anchor = "command-target";
17713
+ if (target.unresolvable) return refuse(`target:${target.unresolvable}`);
17714
+ dirs.add(target.dir ?? baseDir);
17715
+ }
17716
+ if (dirs.size > 1) return refuse(`target:multiple ${found.moment} targets in one command`);
17717
+ const targetDir = dirs.size === 1 ? [...dirs][0] : baseDir;
17718
+ if (targetDir !== baseDir) {
17719
+ if (!(0, import_node_fs24.existsSync)(targetDir)) return refuse(`target:directory does not exist: ${targetDir}`);
17720
+ dir = targetDir;
17721
+ }
17722
+ }
17723
+ const toplevel = gitAt(dir, ["rev-parse", "--show-toplevel"]);
17724
+ if (!toplevel) {
17725
+ return refuse(anchor === "command-target" ? `target:not a git repository: ${dir}` : `anchor:not a git repository: ${dir}`);
17726
+ }
17727
+ const gitDirRaw = gitAt(dir, ["rev-parse", "--absolute-git-dir"]);
17728
+ const commonRaw = gitAt(dir, ["rev-parse", "--git-common-dir"]);
17729
+ const gitDir = gitDirRaw ? realpathOr(gitDirRaw) : null;
17730
+ const commonDir = commonRaw ? realpathOr((0, import_node_path18.isAbsolute)(commonRaw) ? commonRaw : (0, import_node_path18.resolve)(dir, commonRaw)) : null;
17731
+ const branchRaw = gitAt(dir, ["rev-parse", "--abbrev-ref", "HEAD"]);
17732
+ return {
17733
+ moment: found?.moment ?? null,
17734
+ frame: {
17735
+ worktreeRoot: realpathOr(toplevel),
17736
+ gitDir,
17737
+ commonDir,
17738
+ // The one honest definition: a linked worktree's own git dir differs from
17739
+ // the shared one. No path convention involved — see the header.
17740
+ isLinkedWorktree: !!gitDir && !!commonDir && gitDir !== commonDir,
17741
+ branch: !branchRaw || branchRaw === "HEAD" ? null : branchRaw,
17742
+ anchor,
17743
+ refusal: null
17744
+ }
17745
+ };
17746
+ }
17747
+ function frameGit(frame, args) {
17748
+ if (!frame.worktreeRoot) return "";
17749
+ return gitAt(frame.worktreeRoot, args);
17750
+ }
17751
+ function refResolves(frame, ref) {
17752
+ return frameGit(frame, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]) !== "";
17753
+ }
17754
+ var SHA_RE2 = /^[0-9a-f]{40}$/;
17755
+ function baselineShaAt(frame) {
17756
+ if (!frame.worktreeRoot) return null;
17757
+ try {
17758
+ const sha = (0, import_node_fs24.readFileSync)((0, import_node_path19.join)(frame.worktreeRoot, BASELINE_SHA_FILE), "utf-8").trim();
17759
+ if (!SHA_RE2.test(sha)) return null;
17760
+ return refResolves(frame, sha) ? sha : null;
17761
+ } catch {
17762
+ return null;
17763
+ }
17764
+ }
17765
+ function stagedRange() {
17766
+ return { kind: "staged", base: "HEAD", head: "INDEX", via: "index" };
17767
+ }
17768
+ function resolvePushRange(frame, command, on) {
17769
+ const nothing = (via) => ({ kind: "nothing", base: null, head: "HEAD", via });
17770
+ if (!frame.worktreeRoot) return nothing("refused");
17771
+ const found = findMomentSegment(command, on);
17772
+ const segment = found ? splitSegments(command)[found.segmentIndex] : "";
17773
+ const target = parsePushTarget(segment);
17774
+ if (target.isDelete) return nothing("deletion");
17775
+ const head = target.srcRef ?? "HEAD";
17776
+ if (!refResolves(frame, head)) return nothing(`src-unresolvable:${head}`);
17777
+ const srcName = target.srcRef;
17778
+ const branchForRemote = srcName ?? frame.branch;
17779
+ const candidates = [];
17780
+ if (target.remote && (target.dstRef ?? srcName)) {
17781
+ const dstName = (target.dstRef ?? srcName).replace(/^refs\/heads\//, "");
17782
+ candidates.push({ ref: `refs/remotes/${target.remote}/${dstName}`, via: `refspec:${target.remote}/${dstName}` });
17783
+ }
17784
+ if (target.remote && !srcName && !target.dstRef && frame.branch) {
17785
+ candidates.push({ ref: `refs/remotes/${target.remote}/${frame.branch}`, via: `remote:${target.remote}/${frame.branch}` });
17786
+ }
17787
+ candidates.push({ ref: srcName ? `${srcName}@{push}` : "@{push}", via: "@{push}" });
17788
+ candidates.push({ ref: srcName ? `${srcName}@{upstream}` : "@{upstream}", via: "@{upstream}" });
17789
+ if (branchForRemote) {
17790
+ candidates.push({
17791
+ ref: `refs/remotes/origin/${branchForRemote.replace(/^refs\/heads\//, "")}`,
17792
+ via: `origin/${branchForRemote.replace(/^refs\/heads\//, "")}`
17793
+ });
17794
+ }
17795
+ for (const c of candidates) {
17796
+ if (!refResolves(frame, c.ref)) continue;
17797
+ const mergeBase = frameGit(frame, ["merge-base", c.ref, head]);
17798
+ if (SHA_RE2.test(mergeBase)) return { kind: "push", base: mergeBase, head, via: c.via };
17799
+ }
17800
+ const baseline = baselineShaAt(frame);
17801
+ if (baseline) return { kind: "baseline", base: baseline, head, via: "review-baseline" };
17802
+ if (refResolves(frame, `${head}~1`)) return { kind: "last-commit", base: `${head}~1`, head, via: `${head}~1` };
17803
+ return nothing("no-parent");
17804
+ }
17805
+ function rangeFiles(frame, range) {
17806
+ let out;
17807
+ switch (range.kind) {
17808
+ case "staged":
17809
+ out = frameGit(frame, ["diff", "--cached", "--name-only"]);
17810
+ break;
17811
+ case "push":
17812
+ case "baseline":
17813
+ case "last-commit":
17814
+ out = frameGit(frame, ["diff", "--name-only", range.base, range.head === "INDEX" ? "HEAD" : range.head]);
17815
+ break;
17816
+ case "nothing":
17817
+ return [];
17818
+ }
17819
+ return out.split("\n").filter((l) => l.length > 0).filter((f) => !isVerityOwnedPath(f));
17820
+ }
17821
+ function rangeMessages(frame, range) {
17822
+ if (range.kind === "staged" || range.kind === "nothing" || !range.base) return "";
17823
+ return frameGit(frame, ["log", `${range.base}..${range.head === "INDEX" ? "HEAD" : range.head}`, "--format=%B%x00"]).split("\0").map((s) => s.trim()).filter(Boolean).join("\n\n");
17824
+ }
17825
+ function frameTelemetry(frame, range, divergence) {
17826
+ const t = {
17827
+ anchor: frame.anchor,
17828
+ linked_worktree: frame.isLinkedWorktree,
17829
+ range_via: range?.via ?? null,
17830
+ refusal: frame.refusal
17831
+ };
17832
+ if (divergence) {
17833
+ t.root_differs = !!frame.worktreeRoot && !!divergence.actualRoot && realpathOr(frame.worktreeRoot) !== realpathOr(divergence.actualRoot);
17834
+ const a = [...divergence.actualFiles].sort().join("\n");
17835
+ const b = [...divergence.frameFiles].sort().join("\n");
17836
+ t.files_differ = a !== b;
17837
+ t.frame_file_count = divergence.frameFiles.length;
17838
+ t.actual_file_count = divergence.actualFiles.length;
17839
+ }
17840
+ return t;
17841
+ }
17842
+
17843
+ // src/lib/reachability.ts
17844
+ function resolveReachability(input = {}) {
17845
+ const env = input.env ?? {};
17846
+ if (input.autonomousFlag === true) {
17847
+ return { human_reachable: "no", human_reachable_source: "declared", rung: "flag" };
17848
+ }
17849
+ if (truthy(env.VERITY_AUTONOMOUS)) {
17850
+ return { human_reachable: "no", human_reachable_source: "declared", rung: "env" };
17851
+ }
17852
+ if (truthy(env.CI) || truthy(env.GITHUB_ACTIONS) || truthy(env.BUILDKITE) || truthy(env.JENKINS_URL)) {
17853
+ return { human_reachable: "no", human_reachable_source: "declared", rung: "env" };
16896
17854
  }
16897
17855
  if (truthy(env.VERITY_INTERACTIVE)) {
16898
17856
  return { human_reachable: "yes", human_reachable_source: "declared", rung: "env" };
@@ -16910,7 +17868,7 @@ function truthy(v) {
16910
17868
  }
16911
17869
 
16912
17870
  // src/lib/transcript.ts
16913
- var import_node_fs20 = require("node:fs");
17871
+ var import_node_fs25 = require("node:fs");
16914
17872
  var MAX_READ_BYTES = 256 * 1024;
16915
17873
  var SMALL_FILE_BYTES = 64 * 1024;
16916
17874
  var MAX_FILES_LIST = 20;
@@ -16920,6 +17878,8 @@ var MAX_COMMAND_CHARS = 80;
16920
17878
  var MAX_TOOL_BLOCKS = 200;
16921
17879
  var MAX_SUMMARY_BYTES = 4096;
16922
17880
  var HOME = process.env.HOME ?? "";
17881
+ var BASH_INPUT_RE = /^\s*<bash-input>([\s\S]*?)<\/bash-input>/;
17882
+ var BASH_ECHO_RE = /^\s*<bash-(?:stdout|stderr)>/;
16923
17883
  async function extractActionSummary(transcriptPath) {
16924
17884
  try {
16925
17885
  const read = readTurnLines(transcriptPath);
@@ -16934,7 +17894,7 @@ async function extractActionSummary(transcriptPath) {
16934
17894
  function readTurnLines(transcriptPath) {
16935
17895
  let size;
16936
17896
  try {
16937
- size = (0, import_node_fs20.statSync)(transcriptPath).size;
17897
+ size = (0, import_node_fs25.statSync)(transcriptPath).size;
16938
17898
  } catch {
16939
17899
  return null;
16940
17900
  }
@@ -16942,7 +17902,7 @@ function readTurnLines(transcriptPath) {
16942
17902
  let raw;
16943
17903
  let windowed = false;
16944
17904
  if (size <= SMALL_FILE_BYTES) {
16945
- raw = (0, import_node_fs20.readFileSync)(transcriptPath, "utf-8");
17905
+ raw = (0, import_node_fs25.readFileSync)(transcriptPath, "utf-8");
16946
17906
  } else {
16947
17907
  windowed = true;
16948
17908
  const buf = Buffer.alloc(Math.min(MAX_READ_BYTES, size));
@@ -16983,7 +17943,7 @@ function isRealUserMessage(parsed) {
16983
17943
  const message = parsed.message;
16984
17944
  if (!message) return false;
16985
17945
  const content = message.content;
16986
- if (typeof content === "string") return true;
17946
+ if (typeof content === "string") return !BASH_ECHO_RE.test(content);
16987
17947
  if (Array.isArray(content)) {
16988
17948
  return content.some((b) => {
16989
17949
  if (typeof b !== "object" || b === null) return false;
@@ -16998,6 +17958,9 @@ function buildSummary(lines) {
16998
17958
  const filesEdited = /* @__PURE__ */ new Set();
16999
17959
  const filesCreated = /* @__PURE__ */ new Set();
17000
17960
  const commands = [];
17961
+ const userCommands = [];
17962
+ let userCommandsTruncated = false;
17963
+ let commandsTruncated = false;
17001
17964
  let searches = 0;
17002
17965
  let subagents = 0;
17003
17966
  let webFetches = 0;
@@ -17015,6 +17978,17 @@ function buildSummary(lines) {
17015
17978
  if (entry.type === "user" && !firstTimestamp) {
17016
17979
  firstTimestamp = entry.timestamp ?? null;
17017
17980
  }
17981
+ if (entry.type === "user") {
17982
+ const typed = userTypedCommands(entry);
17983
+ if (typed.truncated) userCommandsTruncated = true;
17984
+ for (const cmd of typed.commands) {
17985
+ if (userCommands.length >= MAX_COMMANDS) {
17986
+ userCommandsTruncated = true;
17987
+ break;
17988
+ }
17989
+ userCommands.push(cmd);
17990
+ }
17991
+ }
17018
17992
  if (entry.type !== "assistant") continue;
17019
17993
  turnMessages++;
17020
17994
  lastTimestamp = entry.timestamp ?? lastTimestamp;
@@ -17027,7 +18001,10 @@ function buildSummary(lines) {
17027
18001
  totalToolCalls++;
17028
18002
  const toolName = block.name ?? "unknown";
17029
18003
  toolCounts[toolName] = (toolCounts[toolName] ?? 0) + 1;
17030
- if (totalToolCalls > MAX_TOOL_BLOCKS) continue;
18004
+ if (totalToolCalls > MAX_TOOL_BLOCKS) {
18005
+ if (toolName === "Bash") commandsTruncated = true;
18006
+ continue;
18007
+ }
17031
18008
  const input = block.input ?? {};
17032
18009
  switch (toolName) {
17033
18010
  case "Read":
@@ -17047,9 +18024,11 @@ function buildSummary(lines) {
17047
18024
  addPath(filesEdited, input.file_path);
17048
18025
  break;
17049
18026
  case "Bash": {
17050
- const cmd = sanitizeCommand(input.command);
17051
- if (cmd && commands.length < MAX_COMMANDS) {
17052
- commands.push(cmd);
18027
+ const { cmd, lost } = sanitizeCommandWithLoss(input.command);
18028
+ if (lost) commandsTruncated = true;
18029
+ if (cmd) {
18030
+ if (commands.length < MAX_COMMANDS) commands.push(cmd);
18031
+ else commandsTruncated = true;
17053
18032
  }
17054
18033
  break;
17055
18034
  }
@@ -17092,6 +18071,9 @@ function buildSummary(lines) {
17092
18071
  ],
17093
18072
  searches,
17094
18073
  commands,
18074
+ ...commandsTruncated ? { commands_truncated: true } : {},
18075
+ user_commands: userCommands,
18076
+ ...userCommandsTruncated ? { user_commands_truncated: true } : {},
17095
18077
  subagents,
17096
18078
  web_fetches: webFetches,
17097
18079
  total_tool_calls: totalToolCalls,
@@ -17100,6 +18082,9 @@ function buildSummary(lines) {
17100
18082
  };
17101
18083
  if (JSON.stringify(summary).length > MAX_SUMMARY_BYTES) {
17102
18084
  summary.commands = [];
18085
+ summary.commands_truncated = true;
18086
+ summary.user_commands = [];
18087
+ summary.user_commands_truncated = true;
17103
18088
  if (JSON.stringify(summary).length > MAX_SUMMARY_BYTES) {
17104
18089
  summary.files_read = summary.files_read.slice(0, 10);
17105
18090
  summary.files_edited = summary.files_edited.slice(0, 10);
@@ -17117,12 +18102,38 @@ function addPath(set, rawPath) {
17117
18102
  if (p.length > 200) p = p.slice(0, 200);
17118
18103
  set.add(p);
17119
18104
  }
18105
+ function userTypedCommands(entry) {
18106
+ const none = { commands: [], truncated: false };
18107
+ const message = entry.message;
18108
+ const content = message?.content;
18109
+ if (typeof content !== "string") return none;
18110
+ const m = BASH_INPUT_RE.exec(content);
18111
+ if (!m) return none;
18112
+ const commands = [];
18113
+ let truncated = false;
18114
+ for (const line of m[1].split("\n")) {
18115
+ if (line.length > MAX_COMMAND_CHARS) truncated = true;
18116
+ const cmd = sanitizeCommand(line);
18117
+ if (cmd) commands.push(cmd);
18118
+ }
18119
+ return { commands, truncated };
18120
+ }
18121
+ function sanitizeCommandWithLoss(rawCmd) {
18122
+ if (typeof rawCmd !== "string" || !rawCmd) return { cmd: null, lost: false };
18123
+ const lines = rawCmd.split("\n");
18124
+ const lost = lines.slice(1).some((l) => l.trim().length > 0);
18125
+ const first = lines[0];
18126
+ const cmd = sanitizeCommand(first);
18127
+ const hadSeparator = SEPARATORS.some((sep2) => first.indexOf(sep2) > 0);
18128
+ return { cmd, lost: lost || !hadSeparator && first.length > MAX_COMMAND_CHARS };
18129
+ }
18130
+ var SEPARATORS = [" | ", " > ", " >> ", " 2>", " && ", " ; "];
17120
18131
  function sanitizeCommand(rawCmd) {
17121
18132
  if (typeof rawCmd !== "string" || !rawCmd) return null;
17122
18133
  let cmd = rawCmd.split("\n")[0];
17123
18134
  let cut = -1;
17124
18135
  let marker = "";
17125
- for (const sep2 of [" | ", " > ", " >> ", " 2>", " && ", " ; "]) {
18136
+ for (const sep2 of SEPARATORS) {
17126
18137
  const idx = cmd.indexOf(sep2);
17127
18138
  if (idx > 0 && (cut === -1 || idx < cut)) {
17128
18139
  cut = idx;
@@ -17150,28 +18161,28 @@ async function readStopHookStdin() {
17150
18161
  try {
17151
18162
  if (process.stdin.isTTY) return empty;
17152
18163
  const chunks = [];
17153
- const timeout = new Promise((resolve3) => setTimeout(() => resolve3(empty), 500));
17154
- const read = new Promise((resolve3) => {
18164
+ const timeout = new Promise((resolve4) => setTimeout(() => resolve4(empty), 500));
18165
+ const read = new Promise((resolve4) => {
17155
18166
  process.stdin.on("data", (chunk) => chunks.push(chunk));
17156
18167
  process.stdin.on("end", () => {
17157
18168
  const raw = Buffer.concat(chunks).toString("utf-8").trim();
17158
18169
  if (!raw) {
17159
- resolve3(empty);
18170
+ resolve4(empty);
17160
18171
  return;
17161
18172
  }
17162
18173
  try {
17163
18174
  const data = JSON.parse(raw);
17164
- resolve3({
18175
+ resolve4({
17165
18176
  assistantMessage: typeof data.last_assistant_message === "string" ? data.last_assistant_message : null,
17166
18177
  stopReason: typeof data.stop_reason === "string" ? data.stop_reason : null,
17167
18178
  transcriptPath: typeof data.transcript_path === "string" ? data.transcript_path : null,
17168
18179
  sessionId: typeof data.session_id === "string" ? data.session_id : null
17169
18180
  });
17170
18181
  } catch {
17171
- resolve3(empty);
18182
+ resolve4(empty);
17172
18183
  }
17173
18184
  });
17174
- process.stdin.on("error", () => resolve3(empty));
18185
+ process.stdin.on("error", () => resolve4(empty));
17175
18186
  process.stdin.resume();
17176
18187
  });
17177
18188
  return await Promise.race([read, timeout]);
@@ -17185,6 +18196,7 @@ async function bootstrap(run) {
17185
18196
  process.chdir(repoRoot());
17186
18197
  } catch {
17187
18198
  }
18199
+ run.treeFrame = resolveFrame({ command: "", on: [], hookCwd: null }).frame;
17188
18200
  const turnId = `t-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
17189
18201
  let reachability = resolveReachability({
17190
18202
  autonomousFlag: process.env.VERITY_AUTONOMOUS === "1" || opts.mode === "autonomous",
@@ -17350,7 +18362,7 @@ function truncateToCap(text) {
17350
18362
  function buildHookOutput(gateDecision, systemMessage, agentContext) {
17351
18363
  return {
17352
18364
  gate_decision: gateDecision,
17353
- systemMessage,
18365
+ ...systemMessage === null ? {} : { systemMessage },
17354
18366
  ...agentContext ? {
17355
18367
  hookSpecificOutput: {
17356
18368
  hookEventName: "Stop",
@@ -17371,7 +18383,7 @@ function channelSilence(input) {
17371
18383
  // src/lib/cli-version.ts
17372
18384
  function cliVersion() {
17373
18385
  try {
17374
- return true ? "0.30.0" : "dev";
18386
+ return true ? "0.30.1-experimental.cbeb697" : "dev";
17375
18387
  } catch {
17376
18388
  return "dev";
17377
18389
  }
@@ -17411,8 +18423,8 @@ async function sendSkipBeacon(ctx, reason) {
17411
18423
  }
17412
18424
 
17413
18425
  // src/lib/static-analysis.ts
17414
- var import_node_child_process7 = require("node:child_process");
17415
- var import_node_fs21 = require("node:fs");
18426
+ var import_node_child_process8 = require("node:child_process");
18427
+ var import_node_fs26 = require("node:fs");
17416
18428
  var SEVERITY_ORDER = {
17417
18429
  Error: 0,
17418
18430
  Critical: 0,
@@ -17424,7 +18436,7 @@ var SEVERITY_ORDER = {
17424
18436
  };
17425
18437
  function isCodacyAvailable() {
17426
18438
  try {
17427
- (0, import_node_child_process7.execSync)("which codacy-analysis", { stdio: "pipe" });
18439
+ (0, import_node_child_process8.execSync)("which codacy-analysis", { stdio: "pipe" });
17428
18440
  return true;
17429
18441
  } catch {
17430
18442
  return false;
@@ -17460,13 +18472,13 @@ function runCodacyAnalysis(files) {
17460
18472
  if (files.length === 0) return empty;
17461
18473
  const existingFiles = files.filter((f) => {
17462
18474
  try {
17463
- return (0, import_node_fs21.existsSync)(f);
18475
+ return (0, import_node_fs26.existsSync)(f);
17464
18476
  } catch {
17465
18477
  return false;
17466
18478
  }
17467
18479
  });
17468
18480
  if (existingFiles.length === 0) return empty;
17469
- const proc = (0, import_node_child_process7.spawnSync)("codacy-analysis", buildAnalyzerArgv(existingFiles), {
18481
+ const proc = (0, import_node_child_process8.spawnSync)("codacy-analysis", buildAnalyzerArgv(existingFiles), {
17470
18482
  encoding: "utf-8",
17471
18483
  maxBuffer: 10 * 1024 * 1024
17472
18484
  });
@@ -17657,6 +18669,8 @@ async function passAndExit(run, reason, skip, kindOverride) {
17657
18669
  "verity-command",
17658
18670
  "bare-acknowledgment",
17659
18671
  "reflection-prompt",
18672
+ "command-only-turn",
18673
+ "declared-ignore",
17660
18674
  "skip-mode",
17661
18675
  "zero-increment",
17662
18676
  "debounce",
@@ -17675,10 +18689,12 @@ async function passAndExit(run, reason, skip, kindOverride) {
17675
18689
  }
17676
18690
  const AGENT_SILENT_SKIPS = /* @__PURE__ */ new Set([]);
17677
18691
  const agentNote = AGENT_SILENT_SKIPS.has(skip) ? null : note;
18692
+ const HUMAN_SILENT_SKIPS = /* @__PURE__ */ new Set(["command-only-turn"]);
18693
+ const humanNote = HUMAN_SILENT_SKIPS.has(skip) ? null : `Verity: ${reason}`;
17678
18694
  printJsonCompact(
17679
18695
  buildHookOutput(
17680
18696
  verdict,
17681
- `Verity: ${reason}`,
18697
+ humanNote,
17682
18698
  // `additionalContext` is the agent's ONLY input. Writing only
17683
18699
  // `systemMessage` — the human's field — tells the agent nothing at all,
17684
18700
  // which is what sixteen of the nineteen terminating paths used to do.
@@ -17694,20 +18710,38 @@ async function scope(run) {
17694
18710
  const { files: allChanged, hasRecentCommitFiles } = getChangedFiles();
17695
18711
  run.changedUniverse = allChanged;
17696
18712
  const { kept: external } = partitionVerityOwned(allChanged);
17697
- const analyzable = filterAnalyzable(external);
17698
- const reviewable = filterReviewable(external);
17699
- const securityFiles = filterSecurity(external);
18713
+ const verityIgnore = loadVerityIgnore();
18714
+ const ignored = partitionIgnored(external, verityIgnore);
18715
+ for (const problem of verityIgnore.problems) {
18716
+ process.stderr.write(`Verity: .verityignore ${problem}
18717
+ `);
18718
+ }
18719
+ if (ignored.securityExcluded.length > 0) {
18720
+ process.stderr.write(
18721
+ `Verity: .verityignore excluded ${ignored.securityExcluded.length} security-sensitive file(s) from review this run: ${ignored.securityExcluded.slice(0, 5).join(", ")}. Add a \`!\` rule to keep them in scope.
18722
+ `
18723
+ );
18724
+ }
18725
+ if (ignored.suspended) {
18726
+ process.stderr.write(
18727
+ "Verity: .verityignore changed this turn \u2014 its rules are suspended for this run, so nothing is excluded by them. They take effect once this turn has been reviewed.\n"
18728
+ );
18729
+ }
18730
+ const inScope = ignored.kept;
18731
+ const analyzable = filterAnalyzable(inScope);
18732
+ const reviewable = filterReviewable(inScope);
18733
+ const securityFiles = filterSecurity(inScope);
17700
18734
  const noFilesChanged = analyzable.length === 0 && reviewable.length === 0 && securityFiles.length === 0;
17701
18735
  if (noFilesChanged && !assistantResponse) {
17702
18736
  await passAndExit(run, "No analyzable files changed", "no-analyzable-files");
17703
18737
  }
17704
18738
  const allForReview = Array.from(/* @__PURE__ */ new Set([...analyzable, ...reviewable]));
17705
- Object.assign(run, { allChanged, allForReview, analyzable, hasRecentCommitFiles, noFilesChanged, reviewable, securityFiles });
18739
+ Object.assign(run, { allChanged, allForReview, analyzable, hasRecentCommitFiles, noFilesChanged, reviewable, securityFiles, verityIgnored: ignored });
17706
18740
  }
17707
18741
 
17708
18742
  // src/lib/specs.ts
17709
- var import_node_fs22 = require("node:fs");
17710
- var import_node_path18 = require("node:path");
18743
+ var import_node_fs27 = require("node:fs");
18744
+ var import_node_path20 = require("node:path");
17711
18745
  var SPEC_CANDIDATES = [
17712
18746
  "CLAUDE.md",
17713
18747
  "AGENTS.md",
@@ -17738,16 +18772,16 @@ function discoverSpecs(consulted = []) {
17738
18772
  const totalCap = relevant ? MAX_TOTAL_SPEC_BYTES : UNCONSULTED_TOTAL_BYTES;
17739
18773
  if (totalBytes >= totalCap) return false;
17740
18774
  if (seen.has(specPath)) return true;
17741
- if (!(0, import_node_fs22.existsSync)(specPath)) return true;
18775
+ if (!(0, import_node_fs27.existsSync)(specPath)) return true;
17742
18776
  seen.add(specPath);
17743
18777
  const remaining = totalCap - totalBytes;
17744
18778
  const fileCap = relevant ? MAX_SPEC_FILE_BYTES : UNCONSULTED_FILE_BYTES;
17745
18779
  const readBytes = Math.min(fileCap, remaining);
17746
18780
  try {
17747
18781
  const buf = Buffer.alloc(readBytes);
17748
- const fd = (0, import_node_fs22.openSync)(specPath, "r");
17749
- const bytesRead = (0, import_node_fs22.readSync)(fd, buf, 0, readBytes, 0);
17750
- (0, import_node_fs22.closeSync)(fd);
18782
+ const fd = (0, import_node_fs27.openSync)(specPath, "r");
18783
+ const bytesRead = (0, import_node_fs27.readSync)(fd, buf, 0, readBytes, 0);
18784
+ (0, import_node_fs27.closeSync)(fd);
17751
18785
  const content = buf.slice(0, bytesRead).toString("utf-8");
17752
18786
  if (!content) return true;
17753
18787
  result.push({ path: specPath, content });
@@ -17763,7 +18797,7 @@ function discoverSpecs(consulted = []) {
17763
18797
  if (!addSpec(candidate)) break;
17764
18798
  }
17765
18799
  for (const dir of ["spec", "docs"]) {
17766
- if (!(0, import_node_fs22.existsSync)(dir)) continue;
18800
+ if (!(0, import_node_fs27.existsSync)(dir)) continue;
17767
18801
  try {
17768
18802
  const mdFiles = findMdFiles(dir, 2).sort();
17769
18803
  for (const mdFile of mdFiles) {
@@ -17778,9 +18812,9 @@ function findMdFiles(dir, maxDepth, depth = 0) {
17778
18812
  if (depth >= maxDepth) return [];
17779
18813
  const result = [];
17780
18814
  try {
17781
- const entries = (0, import_node_fs22.readdirSync)(dir, { withFileTypes: true });
18815
+ const entries = (0, import_node_fs27.readdirSync)(dir, { withFileTypes: true });
17782
18816
  for (const entry of entries) {
17783
- const fullPath = (0, import_node_path18.join)(dir, entry.name);
18817
+ const fullPath = (0, import_node_path20.join)(dir, entry.name);
17784
18818
  if (entry.isFile() && entry.name.endsWith(".md")) {
17785
18819
  result.push(fullPath);
17786
18820
  } else if (entry.isDirectory() && depth < maxDepth - 1) {
@@ -17792,19 +18826,19 @@ function findMdFiles(dir, maxDepth, depth = 0) {
17792
18826
  return result;
17793
18827
  }
17794
18828
  function discoverPlans() {
17795
- const homePlansDir = (0, import_node_path18.join)(process.env.HOME ?? "", ".claude", "plans");
18829
+ const homePlansDir = (0, import_node_path20.join)(process.env.HOME ?? "", ".claude", "plans");
17796
18830
  const localPlansDir = ".claude/plans";
17797
18831
  const candidates = [];
17798
18832
  const seen = /* @__PURE__ */ new Set();
17799
18833
  for (const plansDir of [localPlansDir, homePlansDir]) {
17800
- if (!(0, import_node_fs22.existsSync)(plansDir)) continue;
18834
+ if (!(0, import_node_fs27.existsSync)(plansDir)) continue;
17801
18835
  try {
17802
- for (const f of (0, import_node_fs22.readdirSync)(plansDir)) {
18836
+ for (const f of (0, import_node_fs27.readdirSync)(plansDir)) {
17803
18837
  if (!f.endsWith(".md") || seen.has(f)) continue;
17804
18838
  seen.add(f);
17805
- const fullPath = (0, import_node_path18.join)(plansDir, f);
18839
+ const fullPath = (0, import_node_path20.join)(plansDir, f);
17806
18840
  try {
17807
- const stat3 = (0, import_node_fs22.statSync)(fullPath);
18841
+ const stat3 = (0, import_node_fs27.statSync)(fullPath);
17808
18842
  candidates.push({ name: f, path: fullPath, mtime: stat3.mtimeMs, size: stat3.size });
17809
18843
  } catch {
17810
18844
  }
@@ -17817,7 +18851,7 @@ function discoverPlans() {
17817
18851
  for (const entry of candidates.slice(0, MAX_PLAN_FILES)) {
17818
18852
  if (entry.size > MAX_PLAN_FILE_BYTES) continue;
17819
18853
  try {
17820
- const content = (0, import_node_fs22.readFileSync)(entry.path, "utf-8");
18854
+ const content = (0, import_node_fs27.readFileSync)(entry.path, "utf-8");
17821
18855
  result.push({ name: entry.name, content });
17822
18856
  } catch {
17823
18857
  }
@@ -17828,6 +18862,59 @@ function discoverPlans() {
17828
18862
  // src/commands/analyze/phases/03-intent-inputs.ts
17829
18863
  async function intentInputs(run) {
17830
18864
  const { actionSummary, allForReview, assistantResponse, baseline, baselineSessionId } = run;
18865
+ if (isCommandOnlyTurn({
18866
+ userCommands: actionSummary?.user_commands,
18867
+ userCommandsTruncated: actionSummary?.user_commands_truncated,
18868
+ agentAuthoredFiles: (actionSummary?.files_edited.length ?? 0) + (actionSummary?.files_created.length ?? 0),
18869
+ agentToolCalls: actionSummary?.total_tool_calls ?? 0,
18870
+ authorshipIsObservable: !!actionSummary && actionSummary.transcript_windowed !== "orphaned"
18871
+ })) {
18872
+ await passAndExit(run, "User command only \u2014 skipping analysis", "command-only-turn");
18873
+ }
18874
+ {
18875
+ const ignoreKeys = ignoreStateKeys(
18876
+ run.tokenResult.ok ? run.tokenResult.data.token : void 0,
18877
+ null
18878
+ );
18879
+ const found = resolveIgnoreState([baselineSessionId, ...ignoreKeys]);
18880
+ const declaration = resolveActive(found?.state ?? null, Math.floor(Date.now() / 1e3));
18881
+ if (declaration) {
18882
+ const commandRecordTruncated = actionSummary?.commands_truncated === true || actionSummary?.user_commands_truncated === true || // The derived signal, and the only one an older client's summary can
18883
+ // give: `tool_counts` keeps counting Bash calls past every cap that
18884
+ // stops the list growing, so a mismatch IS a loss. See
18885
+ // `ActionSummary.commands_truncated`.
18886
+ (actionSummary?.tool_counts?.Bash ?? 0) > (actionSummary?.commands?.length ?? 0);
18887
+ const outcome = verifyDeclaration({
18888
+ declaration,
18889
+ agentAuthoredFiles: (actionSummary?.files_edited.length ?? 0) + (actionSummary?.files_created.length ?? 0),
18890
+ subagents: actionSummary?.subagents ?? 0,
18891
+ agentCommands: actionSummary?.commands,
18892
+ userCommands: actionSummary?.user_commands,
18893
+ commandRecordTruncated,
18894
+ authorshipIsObservable: !!actionSummary && actionSummary.transcript_windowed !== "orphaned"
18895
+ });
18896
+ if (outcome.honoured) {
18897
+ if (declaration.scope === "turn" && found) clearActiveDeclaration(found.key);
18898
+ logEvent("ignore_honoured", { scope: declaration.scope, origin: declaration.origin });
18899
+ await passAndExit(
18900
+ run,
18901
+ `skipping this turn \u2014 declared housekeeping ("${declaration.reason}")`,
18902
+ "declared-ignore"
18903
+ );
18904
+ } else if (outcome.void) {
18905
+ if (found) clearActiveDeclaration(found.key);
18906
+ logEvent("ignore_voided", {
18907
+ scope: declaration.scope,
18908
+ origin: declaration.origin,
18909
+ why: outcome.why
18910
+ });
18911
+ const notice = `Verity: the ignore declared for this window ("${declaration.reason}") was voided \u2014 ${outcome.why}. Reviewing normally.`;
18912
+ process.stderr.write(`${notice}
18913
+ `);
18914
+ run.voidedIgnoreNotice = notice;
18915
+ }
18916
+ }
18917
+ }
17831
18918
  const conversation = await readAndClearConversationBuffer(baselineSessionId);
17832
18919
  const specs = discoverSpecs(actionSummary?.files_read ?? []);
17833
18920
  const plans = discoverPlans();
@@ -17868,156 +18955,6 @@ async function connect(run) {
17868
18955
  Object.assign(run, { urlResult, serviceUrl: urlResult.data, token: tokenResult.data.token });
17869
18956
  }
17870
18957
 
17871
- // src/lib/analysis-mode.ts
17872
- var DEBUG_PHRASES = [
17873
- "not working",
17874
- "doesn't work",
17875
- "doesn't work",
17876
- "does not work",
17877
- "isn't working",
17878
- "is not working",
17879
- "can't figure out",
17880
- "stack trace"
17881
- ];
17882
- var DEBUG_WORDS = [
17883
- "fix",
17884
- "bug",
17885
- "broken",
17886
- "crash",
17887
- "crashing",
17888
- "failing",
17889
- "debug",
17890
- "debugging",
17891
- "investigate",
17892
- "troubleshoot",
17893
- "regression",
17894
- "wrong"
17895
- ];
17896
- var DEBUG_PATTERN = new RegExp(
17897
- [
17898
- ...DEBUG_PHRASES.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
17899
- ...DEBUG_WORDS.map((w) => `\\b${w}\\b`)
17900
- ].join("|"),
17901
- "i"
17902
- );
17903
- var FALSE_POSITIVE_PATTERNS = [
17904
- /\b(?:add|create|implement|write|build|design|set\s*up)\b.{0,20}\berror\b/i,
17905
- /\berror\s+handling\b/i,
17906
- /\berror\s+boundar(?:y|ies)\b/i,
17907
- /\berror\s+(?:type|class|page|component|message|code|enum)\b/i,
17908
- /\b(?:add|create|implement|write|build)\b.{0,20}\b(?:fix|debug|issue)\b/i
17909
- ];
17910
- function hasDebugIntent(prompt) {
17911
- if (!DEBUG_PATTERN.test(prompt)) return false;
17912
- for (const fp of FALSE_POSITIVE_PATTERNS) {
17913
- if (fp.test(prompt)) return false;
17914
- }
17915
- return true;
17916
- }
17917
- var GIT_ONLY_PATTERN = /\b(commit|push|deploy|merge|rebase|tag|release|publish|ship)\b/i;
17918
- var CODE_AUTHORING_PATTERN = /\b(add|create|implement|build|write|fix|update|change|refactor|modify|remove|delete|move|rename)\b.*\b(function|component|feature|endpoint|test|file|module|class|type|interface|hook|page|route|style|migration|code|bug|error|issue)\b/i;
17919
- function isGitOnlyPrompt(prompt) {
17920
- if (!GIT_ONLY_PATTERN.test(prompt)) return false;
17921
- if (CODE_AUTHORING_PATTERN.test(prompt)) return false;
17922
- return true;
17923
- }
17924
- function reconcileAnalysisMode(predictedMode, signals) {
17925
- const mode2 = resolveAnalysisMode(predictedMode, signals);
17926
- if (mode2 !== "skip") return mode2;
17927
- const windowIsOrphaned = signals.actionSummary?.transcript_windowed === "orphaned";
17928
- if (windowIsOrphaned && !signals.sessionAuthoredCode) return "standard";
17929
- return mode2;
17930
- }
17931
- function resolveAnalysisMode(predictedMode, signals) {
17932
- if (!predictedMode || !isValidMode(predictedMode)) {
17933
- return detectAnalysisMode(
17934
- signals.noFilesChanged,
17935
- signals.assistantResponse,
17936
- signals.conversationPrompts,
17937
- signals.actionSummary,
17938
- signals.sessionAuthoredCode
17939
- );
17940
- }
17941
- const agentAuthoredCode = !!(signals.actionSummary && (signals.actionSummary.files_edited.length > 0 || signals.actionSummary.files_created.length > 0)) || !!signals.sessionAuthoredCode;
17942
- const agentInvestigated = didAgentInvestigate(signals.actionSummary);
17943
- switch (predictedMode) {
17944
- case "skip":
17945
- if (agentAuthoredCode) return "standard";
17946
- return "skip";
17947
- case "plan":
17948
- if (agentAuthoredCode) return "standard";
17949
- return "plan";
17950
- case "debug":
17951
- return "debug";
17952
- case "standard":
17953
- if (!!signals.actionSummary && !agentAuthoredCode && !!signals.assistantResponse) {
17954
- return agentInvestigated ? "plan" : "skip";
17955
- }
17956
- return "standard";
17957
- }
17958
- }
17959
- function didAgentInvestigate(summary) {
17960
- if (!summary) return false;
17961
- return summary.files_read.length > 0 || summary.searches > 0 || summary.commands.length > 0 || summary.subagents > 0 || summary.web_fetches > 0;
17962
- }
17963
- function isValidMode(mode2) {
17964
- return mode2 === "standard" || mode2 === "plan" || mode2 === "debug" || mode2 === "skip";
17965
- }
17966
- function detectAnalysisMode(noFilesChanged, assistantResponse, conversationPrompts, actionSummary, sessionAuthoredCode) {
17967
- const agentAuthoredCode = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0)) || !!sessionAuthoredCode;
17968
- if (conversationPrompts.length > 0 && conversationPrompts.every(isGitOnlyPrompt)) {
17969
- if (!agentAuthoredCode) return "skip";
17970
- }
17971
- if (noFilesChanged && !!assistantResponse && !agentAuthoredCode) {
17972
- return "plan";
17973
- }
17974
- if (!!actionSummary && !agentAuthoredCode && !!assistantResponse) {
17975
- return didAgentInvestigate(actionSummary) ? "plan" : "skip";
17976
- }
17977
- for (const prompt of conversationPrompts) {
17978
- if (hasDebugIntent(prompt)) {
17979
- return "debug";
17980
- }
17981
- }
17982
- return "standard";
17983
- }
17984
- var FILE_MUTATE_RE = /(?:^|[\s|&;(`])(?:sed\s+-i|perl\s+-i|awk\b|tee\b|dd\b|cp\b|mv\b|ln\b|install\b|touch\b|patch\b|git\s+(?:apply|am)\b|cargo\s+build|go\s+generate|make\b|--write\b|--fix\b|--in-place\b)|>>?(?![&>])/i;
17985
- var GIT_PLUMBING_RE = /^\s*git\s+(?:merge|rebase|stash|cherry-pick|revert|pull|fetch|checkout|switch|reset|restore|clean)\b/i;
17986
- var READ_ONLY_RE = /^\s*(?:git\s+(?:status|diff|log|show|branch|remote|config|rev-parse|ls-files|blame|describe)|ls|cat|head|tail|less|grep|rg|find|pwd|echo|printf|wc|which|type|tree|stat|file|env|printenv|date|whoami)\b/i;
17987
- var CHAIN_RE = /&&|\||;|\$\(|\x60/;
17988
- function hasNonEditAuthorship(actionSummary, sessionAuthoredCode) {
17989
- if (!actionSummary) return sessionAuthoredCode;
17990
- if ((actionSummary.subagents ?? 0) > 0) return true;
17991
- if (Object.keys(actionSummary.tool_counts ?? {}).some((t) => t.startsWith("mcp__"))) return true;
17992
- const commands = actionSummary.commands ?? [];
17993
- if (commands.some((c) => FILE_MUTATE_RE.test(c))) return true;
17994
- if (sessionAuthoredCode) {
17995
- const allSafe = commands.length > 0 && commands.every(
17996
- (c) => !CHAIN_RE.test(c) && (GIT_PLUMBING_RE.test(c) || READ_ONLY_RE.test(c))
17997
- );
17998
- if (!allSafe) return true;
17999
- }
18000
- return false;
18001
- }
18002
- function scopeToAuthored(files, actionSummary) {
18003
- if (!actionSummary) return { files, signal: "no-transcript" };
18004
- const touched = [...actionSummary.files_edited ?? [], ...actionSummary.files_created ?? []];
18005
- if (touched.length === 0) return { files: [], signal: "none-authored" };
18006
- return { files: narrowToAgentAuthored(files, actionSummary), signal: "authored" };
18007
- }
18008
- function narrowToAgentAuthored(files, actionSummary) {
18009
- if (!actionSummary) return files;
18010
- const touched = [
18011
- ...actionSummary.files_edited,
18012
- ...actionSummary.files_created
18013
- ];
18014
- if (touched.length === 0) return files;
18015
- return files.filter((f) => {
18016
- const suffix = "/" + f;
18017
- return touched.some((t) => t === f || t.endsWith(suffix));
18018
- });
18019
- }
18020
-
18021
18958
  // src/commands/analyze/phases/05-mode.ts
18022
18959
  async function mode(run) {
18023
18960
  const { opts, globals } = run;
@@ -18038,231 +18975,62 @@ async function mode(run) {
18038
18975
  });
18039
18976
  if (memoryResult.ok) {
18040
18977
  if (Array.isArray(memoryResult.data.context_files)) {
18041
- contextFilePaths = memoryResult.data.context_files;
18042
- }
18043
- const rawMode = memoryResult.data.predicted_mode;
18044
- if (rawMode && ["standard", "plan", "debug", "skip"].includes(rawMode)) {
18045
- predictedMode = rawMode;
18046
- }
18047
- }
18048
- } catch {
18049
- logEvent("memory_fetch_failed", { reason: "exception" });
18050
- }
18051
- const conversationPrompts = (conversation?.prompts ?? []).map((p) => p.prompt);
18052
- let analysisMode;
18053
- const sessionAuthoredCode = !!baseline && allForReview.some((f) => changedSinceBaseline(f, baseline));
18054
- const modeOverride = opts.mode;
18055
- const forced = !!modeOverride && ["standard", "plan", "debug", "skip"].includes(modeOverride);
18056
- if (forced) {
18057
- analysisMode = modeOverride;
18058
- } else {
18059
- analysisMode = reconcileAnalysisMode(
18060
- predictedMode,
18061
- { noFilesChanged, assistantResponse, actionSummary, conversationPrompts, sessionAuthoredCode }
18062
- );
18063
- }
18064
- const investigated = didAgentInvestigate(actionSummary);
18065
- run.modeDecision = {
18066
- predicted: predictedMode ?? null,
18067
- resolved: analysisMode,
18068
- authored: turnAuthoredCode,
18069
- investigated,
18070
- forced
18071
- };
18072
- logEvent("mode_resolved", {
18073
- predicted: predictedMode ?? null,
18074
- resolved: analysisMode,
18075
- forced,
18076
- authored: turnAuthoredCode,
18077
- investigated,
18078
- // The two counters that decide `investigated`, so a false reading is
18079
- // traceable to the tool that was not recognised.
18080
- subagents: actionSummary?.subagents ?? null,
18081
- files_read: actionSummary?.files_read.length ?? null
18082
- });
18083
- if (analysisMode === "skip") {
18084
- await passAndExit(
18085
- run,
18086
- "Skip mode \u2014 no code work to analyze",
18087
- "skip-mode",
18088
- turnAuthoredCode ? "capacity" : void 0
18089
- );
18090
- }
18091
- Object.assign(run, { analysisMode, contextFilePaths, sessionAuthoredCode, sessionIdForMemory });
18092
- }
18093
-
18094
- // src/lib/debounce.ts
18095
- var import_node_fs23 = require("node:fs");
18096
- var import_node_crypto10 = require("node:crypto");
18097
- function scopedFile(base, sessionId) {
18098
- if (!sessionId) return base;
18099
- return `${base}.${(0, import_node_crypto10.createHash)("sha1").update(sessionId).digest("hex").slice(0, 12)}`;
18100
- }
18101
- function checkDebounce(debounceSeconds = DEBOUNCE_SECONDS, sessionId) {
18102
- const file = scopedFile(DEBOUNCE_FILE, sessionId);
18103
- if (!(0, import_node_fs23.existsSync)(file)) return null;
18104
- try {
18105
- const lastTs = parseInt((0, import_node_fs23.readFileSync)(file, "utf-8").trim(), 10);
18106
- const nowTs = Math.floor(Date.now() / 1e3);
18107
- const elapsed = nowTs - lastTs;
18108
- if (elapsed < debounceSeconds) {
18109
- return `Debounced \u2014 last analysis was ${elapsed}s ago`;
18110
- }
18111
- } catch {
18112
- }
18113
- return null;
18114
- }
18115
- function checkMtime(files, bypassForRecentCommits, sessionId) {
18116
- if (bypassForRecentCommits) return null;
18117
- const file = scopedFile(DEBOUNCE_FILE, sessionId);
18118
- if (!(0, import_node_fs23.existsSync)(file)) return null;
18119
- let debounceTime;
18120
- try {
18121
- debounceTime = (0, import_node_fs23.statSync)(file).mtimeMs;
18122
- } catch {
18123
- return null;
18124
- }
18125
- for (const f of files) {
18126
- const resolved = resolveFile(f);
18127
- if (!resolved) continue;
18128
- try {
18129
- const stat3 = (0, import_node_fs23.statSync)(resolved);
18130
- if (stat3.mtimeMs > debounceTime) {
18131
- return null;
18132
- }
18133
- } catch {
18134
- continue;
18135
- }
18136
- }
18137
- return "No files modified since last analysis";
18138
- }
18139
- function computeContentHash(files) {
18140
- const hash = (0, import_node_crypto10.createHash)("sha1");
18141
- const sorted = [...files].sort();
18142
- for (const f of sorted) {
18143
- const resolved = resolveFile(f) ?? f;
18144
- try {
18145
- if ((0, import_node_fs23.existsSync)(resolved)) {
18146
- hash.update((0, import_node_fs23.readFileSync)(resolved));
18978
+ contextFilePaths = memoryResult.data.context_files;
18147
18979
  }
18148
- } catch {
18149
- }
18150
- }
18151
- return hash.digest("hex");
18152
- }
18153
- function checkContentHash(files, sessionId) {
18154
- const hash = computeContentHash(files);
18155
- const file = scopedFile(HASH_FILE, sessionId);
18156
- if ((0, import_node_fs23.existsSync)(file)) {
18157
- try {
18158
- const storedHash = (0, import_node_fs23.readFileSync)(file, "utf-8").trim();
18159
- if (hash === storedHash) {
18160
- return { skip: "No source changes since last analysis", hash };
18980
+ const rawMode = memoryResult.data.predicted_mode;
18981
+ if (rawMode && ["standard", "plan", "debug", "skip"].includes(rawMode)) {
18982
+ predictedMode = rawMode;
18161
18983
  }
18162
- } catch {
18163
18984
  }
18164
- }
18165
- return { skip: null, hash };
18166
- }
18167
- function recordAnalysisStart(sessionId) {
18168
- (0, import_node_fs23.mkdirSync)(VERITY_DIR, { recursive: true });
18169
- (0, import_node_fs23.writeFileSync)(scopedFile(DEBOUNCE_FILE, sessionId), String(Math.floor(Date.now() / 1e3)));
18170
- }
18171
- function recordPassHash(hash, sessionId) {
18172
- (0, import_node_fs23.writeFileSync)(scopedFile(HASH_FILE, sessionId), hash);
18173
- }
18174
- function narrowToRecent(files, sessionId) {
18175
- const file = scopedFile(DEBOUNCE_FILE, sessionId);
18176
- if (!(0, import_node_fs23.existsSync)(file)) return files;
18177
- let debounceTime;
18178
- try {
18179
- debounceTime = (0, import_node_fs23.statSync)(file).mtimeMs;
18180
18985
  } catch {
18181
- return files;
18986
+ logEvent("memory_fetch_failed", { reason: "exception" });
18182
18987
  }
18183
- const recent = files.filter((f) => {
18184
- try {
18185
- return (0, import_node_fs23.existsSync)(f) && (0, import_node_fs23.statSync)(f).mtimeMs > debounceTime;
18186
- } catch {
18187
- return false;
18188
- }
18189
- });
18190
- return recent.length > 0 ? recent : files;
18191
- }
18192
- function readIteration(currentCommit, _contentHash) {
18193
- return Math.max(1, readBlockState(currentCommit).attempts);
18194
- }
18195
- var NO_BLOCKS = { attempts: 0, blocks: 0, fingerprint: null };
18196
- function readBlockState(currentCommit, opts) {
18197
- if (opts?.newUserPrompt) return NO_BLOCKS;
18198
- if (!(0, import_node_fs23.existsSync)(ITERATION_FILE)) return NO_BLOCKS;
18199
- try {
18200
- const stored = (0, import_node_fs23.readFileSync)(ITERATION_FILE, "utf-8").trim();
18201
- const parsed = stored.startsWith("{") ? parseJsonState(stored) : parseLegacyState(stored);
18202
- if (!parsed) return NO_BLOCKS;
18203
- if (parsed.commit !== currentCommit) return NO_BLOCKS;
18204
- if (parsed.ts > 0 && Math.floor(Date.now() / 1e3) - parsed.ts > 600) return NO_BLOCKS;
18205
- return { attempts: parsed.attempts, blocks: parsed.blocks, fingerprint: parsed.fingerprint };
18206
- } catch {
18207
- return NO_BLOCKS;
18988
+ const conversationPrompts = (conversation?.prompts ?? []).map((p) => p.prompt);
18989
+ let analysisMode;
18990
+ const sessionAuthoredCode = !!baseline && allForReview.some((f) => changedSinceBaseline(f, baseline));
18991
+ const modeOverride = opts.mode;
18992
+ const forced = !!modeOverride && ["standard", "plan", "debug", "skip"].includes(modeOverride);
18993
+ if (forced) {
18994
+ analysisMode = modeOverride;
18995
+ } else {
18996
+ analysisMode = reconcileAnalysisMode(
18997
+ predictedMode,
18998
+ { noFilesChanged, assistantResponse, actionSummary, conversationPrompts, sessionAuthoredCode }
18999
+ );
18208
19000
  }
18209
- }
18210
- function parseJsonState(raw) {
18211
- const o = JSON.parse(raw);
18212
- const attempts = typeof o.attempts === "number" ? o.attempts : NaN;
18213
- if (isNaN(attempts)) return null;
18214
- return {
18215
- attempts,
18216
- blocks: typeof o.blocks === "number" ? o.blocks : attempts,
18217
- fingerprint: typeof o.fingerprint === "string" && o.fingerprint ? o.fingerprint : null,
18218
- commit: typeof o.commit === "string" ? o.commit : "",
18219
- ts: typeof o.ts === "number" ? o.ts : 0
18220
- };
18221
- }
18222
- function parseLegacyState(raw) {
18223
- const parts = raw.split(":");
18224
- const n = parseInt(parts[0], 10);
18225
- if (isNaN(n)) return null;
18226
- return {
18227
- attempts: n,
18228
- // The old file has no separate block count; the old counter is the closest
18229
- // honest answer, and it errs toward releasing sooner rather than later.
18230
- blocks: n,
18231
- fingerprint: parts.slice(3).join(":") || null,
18232
- commit: parts[1] ?? "",
18233
- ts: parseInt(parts[2] ?? "0", 10)
19001
+ const investigated = didAgentInvestigate(actionSummary);
19002
+ run.modeDecision = {
19003
+ predicted: predictedMode ?? null,
19004
+ resolved: analysisMode,
19005
+ authored: turnAuthoredCode,
19006
+ investigated,
19007
+ forced
18234
19008
  };
18235
- }
18236
- function findingsFingerprint(findings) {
18237
- const keys = findings.map((f) => `${String(f.pattern_id ?? "?")}|${String(f.file ?? "?")}`).filter((k) => k !== "?|?");
18238
- return [...new Set(keys)].sort().join(",");
18239
- }
18240
- function isSameProblem(previous, current) {
18241
- if (!previous || !current) return false;
18242
- const prev = new Set(previous.split(","));
18243
- return current.split(",").some((k) => prev.has(k));
18244
- }
18245
- function writeBlockState(commit, state) {
18246
- (0, import_node_fs23.mkdirSync)(VERITY_DIR, { recursive: true });
18247
- (0, import_node_fs23.writeFileSync)(
18248
- ITERATION_FILE,
18249
- JSON.stringify({
18250
- v: 2,
18251
- attempts: state.attempts,
18252
- blocks: state.blocks,
18253
- commit,
18254
- ts: Math.floor(Date.now() / 1e3),
18255
- fingerprint: state.fingerprint ?? void 0
18256
- })
18257
- );
18258
- }
18259
- function resetBlockState(commit) {
18260
- writeBlockState(commit, { attempts: 0, blocks: 0, fingerprint: null });
19009
+ logEvent("mode_resolved", {
19010
+ predicted: predictedMode ?? null,
19011
+ resolved: analysisMode,
19012
+ forced,
19013
+ authored: turnAuthoredCode,
19014
+ investigated,
19015
+ // The two counters that decide `investigated`, so a false reading is
19016
+ // traceable to the tool that was not recognised.
19017
+ subagents: actionSummary?.subagents ?? null,
19018
+ files_read: actionSummary?.files_read.length ?? null
19019
+ });
19020
+ if (analysisMode === "skip") {
19021
+ await passAndExit(
19022
+ run,
19023
+ "Skip mode \u2014 no code work to analyze",
19024
+ "skip-mode",
19025
+ turnAuthoredCode ? "capacity" : void 0
19026
+ );
19027
+ }
19028
+ Object.assign(run, { analysisMode, contextFilePaths, sessionAuthoredCode, sessionIdForMemory });
18261
19029
  }
18262
19030
 
18263
19031
  // src/lib/fold.ts
18264
- var import_node_fs24 = require("node:fs");
18265
- var import_node_path19 = require("node:path");
19032
+ var import_node_fs28 = require("node:fs");
19033
+ var import_node_path21 = require("node:path");
18266
19034
  var KNOWN_RECORD_TYPES = /* @__PURE__ */ new Set([
18267
19035
  "user",
18268
19036
  "assistant",
@@ -18300,7 +19068,7 @@ var COMMAND_CLASSES = [
18300
19068
  [/\btsc\b|\bmypy\b|\btypecheck\b/, "typecheck"],
18301
19069
  [/^git\s/, "git"]
18302
19070
  ];
18303
- function classifyCommand(cmd) {
19071
+ function classifyCommand2(cmd) {
18304
19072
  for (const [re, cls] of COMMAND_CLASSES) if (re.test(cmd)) return cls;
18305
19073
  return "other";
18306
19074
  }
@@ -18399,7 +19167,7 @@ function candidateRoots(repoRoot2) {
18399
19167
  const norm = repoRoot2.replace(/\\/g, "/").replace(/\/+$/, "");
18400
19168
  const out = [norm];
18401
19169
  try {
18402
- const real = import_node_fs24.realpathSync.native(norm).replace(/\\/g, "/").replace(/\/+$/, "");
19170
+ const real = import_node_fs28.realpathSync.native(norm).replace(/\\/g, "/").replace(/\/+$/, "");
18403
19171
  if (real !== norm) out.push(real);
18404
19172
  } catch {
18405
19173
  }
@@ -18487,31 +19255,31 @@ function fold(transcriptPath, opts = {}) {
18487
19255
  }
18488
19256
  };
18489
19257
  try {
18490
- if (!(0, import_node_fs24.existsSync)(transcriptPath)) return result;
18491
- ingest((0, import_node_fs24.readFileSync)(transcriptPath, "utf8"), "agent");
19258
+ if (!(0, import_node_fs28.existsSync)(transcriptPath)) return result;
19259
+ ingest((0, import_node_fs28.readFileSync)(transcriptPath, "utf8"), "agent");
18492
19260
  result.coverage.complete = true;
18493
19261
  } catch {
18494
19262
  return result;
18495
19263
  }
18496
19264
  try {
18497
- const sidecarDir = (0, import_node_path19.join)(
18498
- (0, import_node_path19.dirname)(transcriptPath),
18499
- (0, import_node_path19.basename)(transcriptPath).replace(/\.jsonl$/, ""),
19265
+ const sidecarDir = (0, import_node_path21.join)(
19266
+ (0, import_node_path21.dirname)(transcriptPath),
19267
+ (0, import_node_path21.basename)(transcriptPath).replace(/\.jsonl$/, ""),
18500
19268
  "subagents"
18501
19269
  );
18502
- if ((0, import_node_fs24.existsSync)(sidecarDir)) {
19270
+ if ((0, import_node_fs28.existsSync)(sidecarDir)) {
18503
19271
  const maxFiles = opts.maxSidecars ?? 200;
18504
19272
  const maxBytes = opts.maxSidecarBytes ?? 16 * 1024 * 1024;
18505
19273
  const found = [];
18506
19274
  const walk = (d, depth) => {
18507
19275
  if (depth > 4) return;
18508
- for (const e of (0, import_node_fs24.readdirSync)(d, { withFileTypes: true })) {
18509
- const p = (0, import_node_path19.join)(d, e.name);
19276
+ for (const e of (0, import_node_fs28.readdirSync)(d, { withFileTypes: true })) {
19277
+ const p = (0, import_node_path21.join)(d, e.name);
18510
19278
  if (e.isDirectory()) {
18511
19279
  walk(p, depth + 1);
18512
19280
  } else if (e.name.startsWith("agent-") && e.name.endsWith(".jsonl")) {
18513
19281
  try {
18514
- const st = (0, import_node_fs24.statSync)(p);
19282
+ const st = (0, import_node_fs28.statSync)(p);
18515
19283
  found.push({ path: p, size: st.size, mtimeMs: st.mtimeMs });
18516
19284
  } catch {
18517
19285
  result.coverage.malformed++;
@@ -18528,7 +19296,7 @@ function fold(transcriptPath, opts = {}) {
18528
19296
  continue;
18529
19297
  }
18530
19298
  try {
18531
- ingest((0, import_node_fs24.readFileSync)(f.path, "utf8"), "subagent");
19299
+ ingest((0, import_node_fs28.readFileSync)(f.path, "utf8"), "subagent");
18532
19300
  bytes += f.size;
18533
19301
  result.coverage.subagentFiles++;
18534
19302
  } catch {
@@ -18563,7 +19331,7 @@ function fold(transcriptPath, opts = {}) {
18563
19331
  }
18564
19332
  function classifyUnobserved(path) {
18565
19333
  try {
18566
- const st = (0, import_node_fs24.statSync)(path);
19334
+ const st = (0, import_node_fs28.statSync)(path);
18567
19335
  if (!st.isFile()) return "unreadable";
18568
19336
  } catch {
18569
19337
  return "unreadable";
@@ -18629,7 +19397,7 @@ function collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse
18629
19397
  if (name === "Bash") {
18630
19398
  const cmd = typeof input.command === "string" ? input.command : "";
18631
19399
  if (cmd) {
18632
- const cls = classifyCommand(cmd);
19400
+ const cls = classifyCommand2(cmd);
18633
19401
  const prev = commandStats.get(cls) ?? { last_status: null, runs: 0, head: "" };
18634
19402
  commandStats.set(cls, {
18635
19403
  // UNKNOWN until this command's OWN result arrives. Inheriting the
@@ -18867,20 +19635,20 @@ async function evidence(run) {
18867
19635
  }
18868
19636
 
18869
19637
  // src/lib/cache-cleanup.ts
18870
- var import_node_fs25 = require("node:fs");
18871
- var import_node_path20 = require("node:path");
19638
+ var import_node_fs29 = require("node:fs");
19639
+ var import_node_path22 = require("node:path");
18872
19640
  var CACHE_TTL_DAYS = 7;
18873
19641
  function pruneStaleCache() {
18874
19642
  try {
18875
19643
  const dir = projectPath(CACHE_DIR);
18876
19644
  const cutoff = Date.now() - CACHE_TTL_DAYS * 24 * 3600 * 1e3;
18877
- for (const entry of (0, import_node_fs25.readdirSync)(dir)) {
19645
+ for (const entry of (0, import_node_fs29.readdirSync)(dir)) {
18878
19646
  if (!entry.startsWith("pending-")) continue;
18879
- const path = (0, import_node_path20.join)(dir, entry);
19647
+ const path = (0, import_node_path22.join)(dir, entry);
18880
19648
  try {
18881
- const stat3 = (0, import_node_fs25.statSync)(path);
19649
+ const stat3 = (0, import_node_fs29.statSync)(path);
18882
19650
  if (stat3.mtimeMs < cutoff) {
18883
- (0, import_node_fs25.unlinkSync)(path);
19651
+ (0, import_node_fs29.unlinkSync)(path);
18884
19652
  logEvent("cache_entry_pruned", {
18885
19653
  path: entry,
18886
19654
  age_days: Math.round((Date.now() - stat3.mtimeMs) / 864e5)
@@ -18894,7 +19662,7 @@ function pruneStaleCache() {
18894
19662
  }
18895
19663
 
18896
19664
  // src/lib/context-files.ts
18897
- var import_node_fs26 = require("node:fs");
19665
+ var import_node_fs30 = require("node:fs");
18898
19666
  var MAX_CONTEXT_FILES = 10;
18899
19667
  var MAX_CONTEXT_FILE_BYTES = 10240;
18900
19668
  var MAX_CONTEXT_TOTAL_BYTES = 51200;
@@ -18915,7 +19683,7 @@ function gatherContextFiles(contextPaths, deltaFiles) {
18915
19683
  continue;
18916
19684
  }
18917
19685
  try {
18918
- const content = (0, import_node_fs26.readFileSync)(safePath, "utf8");
19686
+ const content = (0, import_node_fs30.readFileSync)(safePath, "utf8");
18919
19687
  const bytes = Buffer.byteLength(content);
18920
19688
  if (bytes > MAX_CONTEXT_FILE_BYTES) {
18921
19689
  logEvent("context_file_skipped", { path: filePath, reason: "too_large", bytes });
@@ -18977,8 +19745,8 @@ async function contextFiles(run) {
18977
19745
 
18978
19746
  // src/lib/seed-runner.ts
18979
19747
  var import_promises11 = require("node:fs/promises");
18980
- var import_node_fs27 = require("node:fs");
18981
- var import_node_path21 = require("node:path");
19748
+ var import_node_fs31 = require("node:fs");
19749
+ var import_node_path23 = require("node:path");
18982
19750
  var import_yaml2 = __toESM(require_dist());
18983
19751
 
18984
19752
  // src/lib/seed.ts
@@ -19217,7 +19985,7 @@ function renderNodeMarkdown(candidate, nodeId, createdAt) {
19217
19985
  return fm;
19218
19986
  }
19219
19987
  async function runSeed(opts) {
19220
- if (!(0, import_node_fs27.existsSync)(STANDARD_FILE)) {
19988
+ if (!(0, import_node_fs31.existsSync)(STANDARD_FILE)) {
19221
19989
  return { created: 0, failed: 0, skipped: "no_standard", candidates: [] };
19222
19990
  }
19223
19991
  let standardDoc;
@@ -19229,7 +19997,7 @@ async function runSeed(opts) {
19229
19997
  }
19230
19998
  const knowledgeSpec = standardDoc.knowledge_spec ?? {};
19231
19999
  let readmeContent;
19232
- if ((0, import_node_fs27.existsSync)("README.md")) {
20000
+ if ((0, import_node_fs31.existsSync)("README.md")) {
19233
20001
  try {
19234
20002
  readmeContent = await (0, import_promises11.readFile)("README.md", "utf-8");
19235
20003
  } catch {
@@ -19237,7 +20005,7 @@ async function runSeed(opts) {
19237
20005
  }
19238
20006
  let claudeMdContent;
19239
20007
  for (const p of ["CLAUDE.md", ".claude/CLAUDE.md"]) {
19240
- if ((0, import_node_fs27.existsSync)(p)) {
20008
+ if ((0, import_node_fs31.existsSync)(p)) {
19241
20009
  try {
19242
20010
  claudeMdContent = await (0, import_promises11.readFile)(p, "utf-8");
19243
20011
  break;
@@ -19260,8 +20028,8 @@ async function runSeed(opts) {
19260
20028
  if (candidates.length === 0) {
19261
20029
  return { created: 0, failed: 0, skipped: "no_candidates", candidates: [] };
19262
20030
  }
19263
- const overviewPath = (0, import_node_path21.join)(MEMORY_DIR, "domain", "project-overview.md");
19264
- if ((0, import_node_fs27.existsSync)(overviewPath) && !opts.force) {
20031
+ const overviewPath = (0, import_node_path23.join)(MEMORY_DIR, "domain", "project-overview.md");
20032
+ if ((0, import_node_fs31.existsSync)(overviewPath) && !opts.force) {
19265
20033
  return { created: 0, failed: 0, skipped: "already_seeded", candidates };
19266
20034
  }
19267
20035
  if (opts.dryRun) {
@@ -19303,7 +20071,7 @@ async function runSeed(opts) {
19303
20071
  continue;
19304
20072
  }
19305
20073
  try {
19306
- await (0, import_promises11.mkdir)((0, import_node_path21.dirname)(targetPath), { recursive: true });
20074
+ await (0, import_promises11.mkdir)((0, import_node_path23.dirname)(targetPath), { recursive: true });
19307
20075
  await (0, import_promises11.writeFile)(targetPath, renderNodeMarkdown(c, nodeId, createdAt));
19308
20076
  created++;
19309
20077
  opts.onCreated?.(nodeId, filePathRel, c);
@@ -19316,8 +20084,8 @@ async function runSeed(opts) {
19316
20084
  }
19317
20085
 
19318
20086
  // src/commands/analyze/phases/08-memory-manifest.ts
19319
- var import_node_fs28 = require("node:fs");
19320
- var import_node_path22 = require("node:path");
20087
+ var import_node_fs32 = require("node:fs");
20088
+ var import_node_path24 = require("node:path");
19321
20089
  async function memoryManifest(run) {
19322
20090
  const { globals } = run;
19323
20091
  const { serviceUrl, token } = run;
@@ -19327,9 +20095,9 @@ async function memoryManifest(run) {
19327
20095
  let autoSeedNotice = null;
19328
20096
  try {
19329
20097
  await ensureMemoryDir();
19330
- const seedMarker = (0, import_node_path22.join)(VERITY_DIR, ".seeded");
19331
- const hasStandard = (0, import_node_fs28.existsSync)(STANDARD_FILE);
19332
- const alreadyTried = (0, import_node_fs28.existsSync)(seedMarker);
20098
+ const seedMarker = (0, import_node_path24.join)(VERITY_DIR, ".seeded");
20099
+ const hasStandard = (0, import_node_fs32.existsSync)(STANDARD_FILE);
20100
+ const alreadyTried = (0, import_node_fs32.existsSync)(seedMarker);
19333
20101
  if (hasStandard && !alreadyTried) {
19334
20102
  const preManifest = await buildManifest();
19335
20103
  if (preManifest.nodes.length === 0) {
@@ -19342,7 +20110,7 @@ async function memoryManifest(run) {
19342
20110
  dryRun: false
19343
20111
  });
19344
20112
  if (seedResult.created > 0) {
19345
- (0, import_node_fs28.writeFileSync)(seedMarker, `${(/* @__PURE__ */ new Date()).toISOString()} created=${seedResult.created}
20113
+ (0, import_node_fs32.writeFileSync)(seedMarker, `${(/* @__PURE__ */ new Date()).toISOString()} created=${seedResult.created}
19346
20114
  `);
19347
20115
  autoSeedNotice = `Seeded ${seedResult.created} knowledge node(s) from your existing Standard (one-time).`;
19348
20116
  logEvent("auto_seed_ran", {
@@ -19350,7 +20118,7 @@ async function memoryManifest(run) {
19350
20118
  failed: seedResult.failed
19351
20119
  });
19352
20120
  } else if (seedResult.skipped === "already_seeded") {
19353
- (0, import_node_fs28.writeFileSync)(seedMarker, `${(/* @__PURE__ */ new Date()).toISOString()} skipped=already_seeded
20121
+ (0, import_node_fs32.writeFileSync)(seedMarker, `${(/* @__PURE__ */ new Date()).toISOString()} skipped=already_seeded
19354
20122
  `);
19355
20123
  } else {
19356
20124
  logEvent("auto_seed_noop", {
@@ -19445,7 +20213,7 @@ function computeIncrement(reviewedPaths, hashOf, priorAuthored) {
19445
20213
  }
19446
20214
 
19447
20215
  // src/commands/analyze/phases/10-working-memory.ts
19448
- var import_node_path23 = require("node:path");
20216
+ var import_node_path25 = require("node:path");
19449
20217
  async function workingMemory(run) {
19450
20218
  const { opts } = run;
19451
20219
  const { allForReview, baseline, conversation, foldResult, sessionId, token, transcriptPath } = run;
@@ -19457,7 +20225,7 @@ async function workingMemory(run) {
19457
20225
  const priorState = foldForMarks(memorySession.d);
19458
20226
  incrementReport = computeIncrement(
19459
20227
  allForReview,
19460
- (p) => fileHash((0, import_node_path23.join)(repoRoot(), p)),
20228
+ (p) => fileHash((0, import_node_path25.join)(repoRoot(), p)),
19461
20229
  priorState.authored_all.map((a) => ({
19462
20230
  path: a.path,
19463
20231
  hash_at_last_verdict: a.hash_at_last_verdict
@@ -19539,7 +20307,7 @@ async function workingMemory(run) {
19539
20307
  }
19540
20308
 
19541
20309
  // src/lib/note-budget.ts
19542
- var import_node_fs29 = require("node:fs");
20310
+ var import_node_fs33 = require("node:fs");
19543
20311
  var ADVISORY_BUDGET = { PASS: 1, WARN: 2 };
19544
20312
  var EPISODE_STALE_SECONDS = 30 * 60;
19545
20313
  var FRESH = { delivered: 0, tasksCompleted: 0, ts: 0 };
@@ -19561,9 +20329,9 @@ function advisoryBudgetSpent(episode, rawDecision) {
19561
20329
  }
19562
20330
  function readAdvisoryEpisode(sessionId) {
19563
20331
  const file = scopedFile(ADVISORY_EPISODE_FILE, sessionId);
19564
- if (!(0, import_node_fs29.existsSync)(file)) return null;
20332
+ if (!(0, import_node_fs33.existsSync)(file)) return null;
19565
20333
  try {
19566
- const o = JSON.parse((0, import_node_fs29.readFileSync)(file, "utf-8")) ?? {};
20334
+ const o = JSON.parse((0, import_node_fs33.readFileSync)(file, "utf-8")) ?? {};
19567
20335
  const delivered = typeof o.delivered === "number" ? o.delivered : NaN;
19568
20336
  if (isNaN(delivered)) return null;
19569
20337
  return {
@@ -19577,8 +20345,8 @@ function readAdvisoryEpisode(sessionId) {
19577
20345
  }
19578
20346
  function writeAdvisoryEpisode(episode, sessionId) {
19579
20347
  try {
19580
- (0, import_node_fs29.mkdirSync)(VERITY_DIR, { recursive: true });
19581
- (0, import_node_fs29.writeFileSync)(
20348
+ (0, import_node_fs33.mkdirSync)(VERITY_DIR, { recursive: true });
20349
+ (0, import_node_fs33.writeFileSync)(
19582
20350
  scopedFile(ADVISORY_EPISODE_FILE, sessionId),
19583
20351
  JSON.stringify({ v: 1, ...episode })
19584
20352
  );
@@ -19608,7 +20376,7 @@ function isExplicitlyAutonomous(env = process.env) {
19608
20376
  }
19609
20377
 
19610
20378
  // src/lib/task-context.ts
19611
- var import_node_child_process8 = require("node:child_process");
20379
+ var import_node_child_process9 = require("node:child_process");
19612
20380
  var CLOSING_RE = /\b(close[sd]?|fix(?:e[sd])?|resolve[sd]?)\b[\s:]*#(\d+)/i;
19613
20381
  var BRANCH_RE = /(?:^|[/_-])(?:issue|gh|fix)[-_/]?(\d+)\b/i;
19614
20382
  function parseLinkedIssue(sources) {
@@ -19624,7 +20392,7 @@ function parseLinkedIssue(sources) {
19624
20392
  }
19625
20393
  function safeExec(cmd, timeout) {
19626
20394
  try {
19627
- return (0, import_node_child_process8.execSync)(cmd, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout }).trim();
20395
+ return (0, import_node_child_process9.execSync)(cmd, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout }).trim();
19628
20396
  } catch {
19629
20397
  return "";
19630
20398
  }
@@ -19685,7 +20453,16 @@ async function buildRequest(run) {
19685
20453
  // the state, so the number is one turn lagged by construction. The
19686
20454
  // degenerate win for the budget is a dead channel that looks like clean
19687
20455
  // code; this is what makes "did delivery rate collapse" a query.
19688
- advisory_delivered_prior: readAdvisoryEpisode(run.baselineSessionId)?.delivered ?? 0
20456
+ advisory_delivered_prior: readAdvisoryEpisode(run.baselineSessionId)?.delivered ?? 0,
20457
+ // `.verityignore` — see CoverageTelemetry.verityignore for why the SHARE is
20458
+ // the number that matters and why no paths travel with it.
20459
+ verityignore: {
20460
+ rules: run.verityIgnored.rules,
20461
+ excluded: run.verityIgnored.ignored.length,
20462
+ share: ignoreShare(run.verityIgnored.kept.length, run.verityIgnored.ignored.length),
20463
+ security_excluded: run.verityIgnored.securityExcluded.length,
20464
+ suspended: run.verityIgnored.suspended
20465
+ }
19689
20466
  };
19690
20467
  const requestBody = {
19691
20468
  coverage_telemetry: coverageTelemetry,
@@ -19870,7 +20647,10 @@ async function buildRequest(run) {
19870
20647
  }
19871
20648
  if (specs.length > 0) intentContext.specs = specs;
19872
20649
  if (plans.length > 0) intentContext.plans = plans;
19873
- if (actionSummary) intentContext.action_summary = actionSummary;
20650
+ if (actionSummary) {
20651
+ const { user_commands: _uc, user_commands_truncated: _uct, ...onTheWire } = actionSummary;
20652
+ intentContext.action_summary = onTheWire;
20653
+ }
19874
20654
  for (const key of Object.keys(intentContext)) {
19875
20655
  if (intentContext[key] == null) delete intentContext[key];
19876
20656
  }
@@ -19880,14 +20660,14 @@ async function buildRequest(run) {
19880
20660
  }
19881
20661
 
19882
20662
  // src/lib/offline.ts
19883
- var import_node_fs30 = require("node:fs");
20663
+ var import_node_fs34 = require("node:fs");
19884
20664
  var import_node_crypto11 = require("node:crypto");
19885
20665
  function cacheRequest(body) {
19886
20666
  try {
19887
- (0, import_node_fs30.mkdirSync)(CACHE_DIR, { recursive: true });
20667
+ (0, import_node_fs34.mkdirSync)(CACHE_DIR, { recursive: true });
19888
20668
  const suffix = (0, import_node_crypto11.randomBytes)(4).toString("hex");
19889
20669
  const filename = `pending-${Math.floor(Date.now() / 1e3)}-${suffix}.json`;
19890
- (0, import_node_fs30.writeFileSync)(`${CACHE_DIR}/${filename}`, JSON.stringify(redactRequest(body)));
20670
+ (0, import_node_fs34.writeFileSync)(`${CACHE_DIR}/${filename}`, JSON.stringify(redactRequest(body)));
19891
20671
  } catch {
19892
20672
  }
19893
20673
  }
@@ -20006,8 +20786,8 @@ async function transmit(run) {
20006
20786
  }
20007
20787
 
20008
20788
  // src/commands/analyze/phases/13-reconcile.ts
20009
- var import_node_fs31 = require("node:fs");
20010
- var import_node_path24 = require("node:path");
20789
+ var import_node_fs35 = require("node:fs");
20790
+ var import_node_path26 = require("node:path");
20011
20791
  async function reconcile(run) {
20012
20792
  const { actionSummary, allChanged, analyzable, baseline, baselineSessionId, codeDelta, contentHash, conversation, decision, foldResult, memory, memorySession, response, reviewable, securityFiles, turnId } = run;
20013
20793
  const sentPaths = codeDelta.files.map((f) => f.path);
@@ -20017,7 +20797,7 @@ async function reconcile(run) {
20017
20797
  const st = foldDossier(memorySession.d);
20018
20798
  openElsewhere = openBlockingElsewhere(st.statements, sentPaths, (file, line) => {
20019
20799
  try {
20020
- const src = (0, import_node_fs31.readFileSync)((0, import_node_path24.join)(repoRoot(), file), "utf8").split("\n");
20800
+ const src = (0, import_node_fs35.readFileSync)((0, import_node_path26.join)(repoRoot(), file), "utf8").split("\n");
20021
20801
  const at = src[line - 1];
20022
20802
  return at === void 0 ? null : lineSha(at);
20023
20803
  } catch {
@@ -20078,6 +20858,25 @@ async function reconcile(run) {
20078
20858
  stage: "self-scope",
20079
20859
  kind: "policy"
20080
20860
  })),
20861
+ // ⚠ WHAT THE TEAM'S OWN `.verityignore` REMOVED — every path, named
20862
+ // (VRT-135 · 3/3). The 0.30 principle: an exclusion the ledger cannot see
20863
+ // is a scoping decision nobody can audit, and this is the one exclusion
20864
+ // stage a user can edit, so it is the one that most needs to be visible.
20865
+ //
20866
+ // `policy`, by D2's bar — no version of this product would have reviewed a
20867
+ // file the project declared out of scope, so it must not downgrade PASS to
20868
+ // WARN. That is also why it is not narrated per-turn by `describeCoverage`
20869
+ // (policy exclusions are recorded, not announced): repeating "your dist/
20870
+ // went unreviewed" on every turn is how a channel gets muted. The SHARE
20871
+ // below is the signal that replaces the noise.
20872
+ //
20873
+ // Taken from the run, not recomputed — see context.ts `verityIgnored`.
20874
+ ...run.verityIgnored.ignored.map((path) => ({
20875
+ path,
20876
+ reason: "verityignore",
20877
+ stage: "verityignore",
20878
+ kind: "policy"
20879
+ })),
20081
20880
  // The extension allowlist. POLICY: a changed README was never going to be
20082
20881
  // reviewed, and calling that a coverage gap would downgrade nearly every
20083
20882
  // PASS to WARN until WARN meant nothing. Recorded so the ledger balances and
@@ -20307,7 +21106,7 @@ function agentContextFor(response, intentRepeat = 0, priorPendingFingerprints =
20307
21106
  }
20308
21107
  async function render(run) {
20309
21108
  const { opts, globals } = run;
20310
- const { actionSummary, assistantResponse, autoSeedNotice, baselineSessionId, codeDelta, contentHash, conversation, currentCommit, decision, intentRepeatCount, memory, openElsewhere, priorPendingFingerprints, response, reviewCoverage, serviceUrl, sessionIdForMemory, silenced, token, watermarkHash, watermarkIsPartial } = run;
21109
+ const { actionSummary, assistantResponse, autoSeedNotice, voidedIgnoreNotice, baselineSessionId, codeDelta, contentHash, conversation, currentCommit, decision, intentRepeatCount, memory, openElsewhere, priorPendingFingerprints, response, reviewCoverage, serviceUrl, sessionIdForMemory, silenced, token, watermarkHash, watermarkIsPartial } = run;
20311
21110
  let { iteration } = run;
20312
21111
  const metadata = response.metadata ?? {};
20313
21112
  const intentAmbiguity = metadata.intent_ambiguity;
@@ -20559,6 +21358,7 @@ ${YELLOW}${grantNudge.trim()}${NC}
20559
21358
  const viewUrl = response.view_url ?? "";
20560
21359
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
20561
21360
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
21361
+ if (voidedIgnoreNotice) userSummary = `${voidedIgnoreNotice} ${userSummary}`;
20562
21362
  userSummary += loginNudge + grantNudge;
20563
21363
  emitVerdict({
20564
21364
  proposed: "PASS",
@@ -20580,6 +21380,7 @@ ${YELLOW}${grantNudge.trim()}${NC}
20580
21380
  const viewUrl = response.view_url ?? "";
20581
21381
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
20582
21382
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
21383
+ if (voidedIgnoreNotice) userSummary = `${voidedIgnoreNotice} ${userSummary}`;
20583
21384
  userSummary += loginNudge + grantNudge;
20584
21385
  emitVerdict({
20585
21386
  proposed: "WARN",
@@ -20595,7 +21396,11 @@ ${YELLOW}${grantNudge.trim()}${NC}
20595
21396
  }
20596
21397
  default: {
20597
21398
  const raw = String(decision ?? "(missing)");
20598
- const msg = (autoSeedNotice ? `${autoSeedNotice} Verity: unrecognised verdict \u2014 treating as WARN` : "Verity: unrecognised verdict \u2014 treating as WARN") + loginNudge + grantNudge;
21399
+ const msg = [
21400
+ voidedIgnoreNotice,
21401
+ autoSeedNotice,
21402
+ "Verity: unrecognised verdict \u2014 treating as WARN"
21403
+ ].filter(Boolean).join(" ") + loginNudge + grantNudge;
20599
21404
  process.stderr.write(
20600
21405
  `Verity: server returned an unrecognised gate_decision (${raw}). Rendering WARN rather than PASS. Update the CLI: npm i -g @codacy/verity-cli
20601
21406
  `
@@ -20672,7 +21477,7 @@ async function runAnalyze(opts, globals) {
20672
21477
  }
20673
21478
 
20674
21479
  // src/commands/baseline.ts
20675
- var import_node_fs32 = require("node:fs");
21480
+ var import_node_fs36 = require("node:fs");
20676
21481
  function registerBaselineCommands(program2) {
20677
21482
  const baseline = program2.command("baseline").description("Manage the task-start working-tree baseline");
20678
21483
  baseline.command("capture").description("Snapshot the working tree at task start (used by SessionStart hook)").option("--session-id <id>", "Session id (overrides any value from stdin)").option("--source <source>", "Lifecycle hint: startup|resume|clear|compact").action(async (opts) => {
@@ -20681,7 +21486,7 @@ function registerBaselineCommands(program2) {
20681
21486
  process.chdir(repoRoot());
20682
21487
  } catch {
20683
21488
  }
20684
- if (!(0, import_node_fs32.existsSync)(VERITY_DIR)) {
21489
+ if (!(0, import_node_fs36.existsSync)(VERITY_DIR)) {
20685
21490
  process.exit(0);
20686
21491
  }
20687
21492
  let sessionId = opts.sessionId;
@@ -20721,7 +21526,7 @@ async function readStdin() {
20721
21526
  }
20722
21527
 
20723
21528
  // src/commands/review.ts
20724
- var import_node_fs33 = require("node:fs");
21529
+ var import_node_fs37 = require("node:fs");
20725
21530
  function registerReviewCommand(program2) {
20726
21531
  program2.command("review").description("Run on-demand Verity analysis (advisory, never blocks)").requiredOption("--files <paths>", "Comma-separated file list").option("--changed <paths>", "Subset of --files that were modified").option("--intent <text>", "User intent description (max 2000 chars)").option("--specs <paths>", "Comma-separated spec file paths").option("--json", "Output raw JSON response").action(async (opts) => {
20727
21532
  const globals = program2.opts();
@@ -20740,7 +21545,7 @@ async function runReview(opts, globals) {
20740
21545
  const securityFiles = filterSecurity(allFiles);
20741
21546
  let staticResults;
20742
21547
  if (isCodacyAvailable()) {
20743
- const scannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles])).filter((f) => (0, import_node_fs33.existsSync)(f) || resolveFile(f) !== null);
21548
+ const scannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles])).filter((f) => (0, import_node_fs37.existsSync)(f) || resolveFile(f) !== null);
20744
21549
  staticResults = runCodacyAnalysis(scannable);
20745
21550
  } else {
20746
21551
  staticResults = {
@@ -20766,10 +21571,10 @@ async function runReview(opts, globals) {
20766
21571
  const specPaths = opts.specs.split(",").map((f) => f.trim()).filter(Boolean);
20767
21572
  specs = [];
20768
21573
  for (const p of specPaths) {
20769
- if (!(0, import_node_fs33.existsSync)(p)) continue;
21574
+ if (!(0, import_node_fs37.existsSync)(p)) continue;
20770
21575
  try {
20771
- const { readFileSync: readFileSync19 } = await import("node:fs");
20772
- const content = readFileSync19(p, "utf-8");
21576
+ const { readFileSync: readFileSync23 } = await import("node:fs");
21577
+ const content = readFileSync23(p, "utf-8");
20773
21578
  specs.push({ path: p, content: content.slice(0, 10240) });
20774
21579
  } catch {
20775
21580
  }
@@ -20826,15 +21631,15 @@ async function runReview(opts, globals) {
20826
21631
  }
20827
21632
 
20828
21633
  // src/commands/guard.ts
20829
- var import_node_fs34 = require("node:fs");
20830
- var import_node_path25 = require("node:path");
21634
+ var import_node_fs38 = require("node:fs");
21635
+ var import_node_path27 = require("node:path");
20831
21636
  var GUARD_BLOCK_CAP = 2;
20832
- var GUARD_ITER_FILE = (0, import_node_path25.join)(VERITY_DIR, ".guard-iteration");
21637
+ var GUARD_ITER_FILE = (0, import_node_path27.join)(VERITY_DIR, ".guard-iteration");
20833
21638
  function readPreToolUseStdin() {
20834
21639
  const empty = { command: "", cwd: null, sessionId: null };
20835
- return new Promise((resolve3) => {
21640
+ return new Promise((resolve4) => {
20836
21641
  try {
20837
- if (process.stdin.isTTY) return resolve3(empty);
21642
+ if (process.stdin.isTTY) return resolve4(empty);
20838
21643
  const chunks = [];
20839
21644
  let timer;
20840
21645
  let settled = false;
@@ -20847,7 +21652,7 @@ function readPreToolUseStdin() {
20847
21652
  process.stdin.removeListener("end", onEnd);
20848
21653
  process.stdin.removeListener("error", onError);
20849
21654
  process.stdin.pause();
20850
- resolve3(value);
21655
+ resolve4(value);
20851
21656
  };
20852
21657
  const onEnd = () => {
20853
21658
  try {
@@ -20868,32 +21673,13 @@ function readPreToolUseStdin() {
20868
21673
  process.stdin.on("error", onError);
20869
21674
  process.stdin.resume();
20870
21675
  } catch {
20871
- resolve3(empty);
21676
+ resolve4(empty);
20872
21677
  }
20873
21678
  });
20874
21679
  }
20875
- function buildCommandRe(head) {
20876
- return new RegExp(`(?:^|[\\s;&|(])${head}|(?:^|[;&|(])\\s*[^\\s;&|()'"]*\\/${head}`);
20877
- }
20878
- var GIT_GLOBAL_OPTS = "(?:\\s+(?:-[Cc]\\s+\\S+|--?[\\w-]+(?:=\\S+)?))*";
20879
- var COMMIT_RE = buildCommandRe(`git${GIT_GLOBAL_OPTS}\\s+commit(?![\\w-])`);
20880
- var PUSH_RE = buildCommandRe(`git${GIT_GLOBAL_OPTS}\\s+push\\b`);
20881
- var GH_PR_RE = buildCommandRe(`gh${GIT_GLOBAL_OPTS}\\s+pr\\s+create\\b`);
20882
- function classifyCommand2(command, on) {
20883
- let commit = false;
20884
- let push = false;
20885
- for (const seg of (command ?? "").split(/&&|\|\||;|\n/)) {
20886
- if (/--dry-run\b/.test(seg)) continue;
20887
- if (COMMIT_RE.test(seg)) commit = true;
20888
- if (PUSH_RE.test(seg) || GH_PR_RE.test(seg)) push = true;
20889
- }
20890
- if (commit && on.includes("commit")) return "pre-commit";
20891
- if (push && on.includes("push")) return "pre-push";
20892
- return null;
20893
- }
20894
21680
  function readIterMap() {
20895
21681
  try {
20896
- const raw = JSON.parse((0, import_node_fs34.readFileSync)(GUARD_ITER_FILE, "utf-8"));
21682
+ const raw = JSON.parse((0, import_node_fs38.readFileSync)(GUARD_ITER_FILE, "utf-8"));
20897
21683
  if (raw && typeof raw === "object") {
20898
21684
  if (typeof raw.moment === "string" && typeof raw.count === "number") {
20899
21685
  return { [raw.moment]: raw.count };
@@ -20913,10 +21699,10 @@ function readIter(moment) {
20913
21699
  }
20914
21700
  function writeIter(moment, count) {
20915
21701
  try {
20916
- (0, import_node_fs34.mkdirSync)(VERITY_DIR, { recursive: true });
21702
+ (0, import_node_fs38.mkdirSync)(VERITY_DIR, { recursive: true });
20917
21703
  const map = readIterMap();
20918
21704
  map[moment] = count;
20919
- (0, import_node_fs34.writeFileSync)(GUARD_ITER_FILE, JSON.stringify(map));
21705
+ (0, import_node_fs38.writeFileSync)(GUARD_ITER_FILE, JSON.stringify(map));
20920
21706
  } catch {
20921
21707
  }
20922
21708
  }
@@ -20926,10 +21712,10 @@ function resetIter(moment) {
20926
21712
  if (!(moment in map)) return;
20927
21713
  delete map[moment];
20928
21714
  if (Object.keys(map).length === 0) {
20929
- if ((0, import_node_fs34.existsSync)(GUARD_ITER_FILE)) (0, import_node_fs34.unlinkSync)(GUARD_ITER_FILE);
21715
+ if ((0, import_node_fs38.existsSync)(GUARD_ITER_FILE)) (0, import_node_fs38.unlinkSync)(GUARD_ITER_FILE);
20930
21716
  } else {
20931
- (0, import_node_fs34.mkdirSync)(VERITY_DIR, { recursive: true });
20932
- (0, import_node_fs34.writeFileSync)(GUARD_ITER_FILE, JSON.stringify(map));
21717
+ (0, import_node_fs38.mkdirSync)(VERITY_DIR, { recursive: true });
21718
+ (0, import_node_fs38.writeFileSync)(GUARD_ITER_FILE, JSON.stringify(map));
20933
21719
  }
20934
21720
  } catch {
20935
21721
  }
@@ -20944,8 +21730,14 @@ function registerGuardCommand(program2) {
20944
21730
  }
20945
21731
  });
20946
21732
  }
20947
- function getMomentFiles(moment) {
20948
- return moment === "pre-commit" ? getStagedFiles() : getPushRangeFiles().files;
21733
+ function resolveMomentRange(moment, frame, command, on) {
21734
+ return moment === "pre-commit" ? stagedRange() : resolvePushRange(frame, command, on);
21735
+ }
21736
+ function describeRange(range) {
21737
+ if (range.kind === "staged") return "staged";
21738
+ if (range.kind === "nothing" || !range.base) return null;
21739
+ const base = /^[0-9a-f]{40}$/.test(range.base) ? range.base.slice(0, 7) : range.base;
21740
+ return `${base}..${range.head} via ${range.via}`;
20949
21741
  }
20950
21742
  function matchFlagValue(command, flags) {
20951
21743
  const re = new RegExp(`(?<![\\w-])(?:${flags})(?:=|\\s+)('((?:[^'\\\\]|\\\\.)*)'|"((?:[^"\\\\]|\\\\.)*)"|([^\\s'"-][^\\s]*))`);
@@ -20980,26 +21772,25 @@ function isSubstantiveIntent(text) {
20980
21772
  if (SHELL_PLUMBING.test(t)) return false;
20981
21773
  return true;
20982
21774
  }
20983
- function extractStatedIntent(moment, command) {
20984
- const text = moment === "pre-commit" ? parseCommitMessage(command) : parsePrIntent(command) ?? (getPushRangeMessages() || null);
21775
+ function extractStatedIntent(moment, command, pushedMessages = null) {
21776
+ const text = moment === "pre-commit" ? parseCommitMessage(command) : parsePrIntent(command) ?? pushedMessages;
20985
21777
  return isSubstantiveIntent(text) ? text : null;
20986
21778
  }
20987
- function hasBlockingFinding(response) {
21779
+ function hasBlockingFinding(response, sentFiles) {
20988
21780
  const findings = response.findings ?? [];
20989
- return findings.some((f) => f.scope !== "pre-existing" && ["critical", "high"].includes((f.severity ?? "").toLowerCase()));
21781
+ const sent = sentFiles ? new Set(sentFiles.map((p) => p.replace(/\\/g, "/"))) : null;
21782
+ return findings.some((f) => f.scope !== "pre-existing" && ["critical", "high"].includes((f.severity ?? "").toLowerCase()) && (sent === null || typeof f.file === "string" && sent.has(f.file.replace(/\\/g, "/"))));
20990
21783
  }
20991
- function buildGuardRequest(moment, files, iter, sessionId, command) {
21784
+ function buildGuardRequest(moment, files, codeDelta, iter, sessionId, statedIntent, coverageTelemetry) {
20992
21785
  const analyzable = filterAnalyzable(files);
20993
21786
  const securityFiles = filterSecurity(files);
20994
21787
  let staticResults;
20995
21788
  if (isCodacyAvailable()) {
20996
- const scannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles])).map((f) => (0, import_node_fs34.existsSync)(f) ? f : resolveFile(f)).filter((f) => f !== null);
21789
+ const scannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles])).map((f) => (0, import_node_fs38.existsSync)(f) ? f : resolveFile(f)).filter((f) => f !== null);
20997
21790
  staticResults = runCodacyAnalysis(scannable);
20998
21791
  } else {
20999
21792
  staticResults = { tool: "@codacy/analysis-cli", findings: [], summary: { total_findings: 0, by_severity: {}, tools_run: [] } };
21000
21793
  }
21001
- const codeDelta = collectCodeDelta(files);
21002
- if (codeDelta.total_files === 0) return null;
21003
21794
  const trigger = moment === "pre-commit" ? "hook:pre-commit" : "hook:pre-push";
21004
21795
  const requestBody = {
21005
21796
  static_results: staticResults,
@@ -21016,9 +21807,9 @@ function buildGuardRequest(moment, files, iter, sessionId, command) {
21016
21807
  iteration: iter + 1
21017
21808
  }
21018
21809
  };
21810
+ if (coverageTelemetry) requestBody.coverage_telemetry = coverageTelemetry;
21019
21811
  const specs = discoverSpecs();
21020
21812
  const plans = discoverPlans();
21021
- const statedIntent = extractStatedIntent(moment, command);
21022
21813
  if (specs.length > 0 || plans.length > 0 || statedIntent) {
21023
21814
  const intentContext = {};
21024
21815
  if (statedIntent) intentContext.user_prompt = statedIntent;
@@ -21028,6 +21819,39 @@ function buildGuardRequest(moment, files, iter, sessionId, command) {
21028
21819
  }
21029
21820
  return requestBody;
21030
21821
  }
21822
+ function buildGuardCoverage(files, codeDelta, frame, frameRange) {
21823
+ const byReason = {};
21824
+ for (const e of codeDelta.excluded) byReason[e.reason] = (byReason[e.reason] ?? 0) + 1;
21825
+ return {
21826
+ changed_all: files.length,
21827
+ analyzable: filterAnalyzable(files).length,
21828
+ reviewable: filterReviewable(files).length,
21829
+ security: filterSecurity(files).length,
21830
+ for_review: files.length,
21831
+ sent: codeDelta.total_files,
21832
+ capped_out: codeDelta.truncated?.dropped ?? 0,
21833
+ excluded: codeDelta.excluded.length,
21834
+ excluded_by_reason: byReason,
21835
+ transcript_windowed: null,
21836
+ guard_frame: frameTelemetry(frame, frameRange)
21837
+ };
21838
+ }
21839
+ function coverageSummary(c) {
21840
+ const range = c.range ? ` @ ${c.range}` : "";
21841
+ return `reviewed ${c.sent.length} file(s)${range}`;
21842
+ }
21843
+ function coverageBlock(c) {
21844
+ const lines = [];
21845
+ const tree = c.root ? `${c.root}${c.linked ? " (linked worktree)" : ""}${c.branch ? ` \xB7 branch ${c.branch}` : ""}` : "(no tree resolved)";
21846
+ lines.push(`Reviewed (${c.moment}): ${c.sent.length} file(s)${c.range ? ` @ ${c.range}` : ""}`);
21847
+ lines.push(` Tree: ${tree}`);
21848
+ for (const f of c.sent) lines.push(` - ${f}`);
21849
+ if (c.excluded.length > 0) {
21850
+ lines.push(` Excluded (${c.excluded.length}):`);
21851
+ for (const e of c.excluded) lines.push(` - ${e.path} (${e.reason})`);
21852
+ }
21853
+ return lines.join("\n");
21854
+ }
21031
21855
  function emitAllowNotice(userMsg, agentMsg) {
21032
21856
  process.stdout.write(JSON.stringify({
21033
21857
  systemMessage: userMsg,
@@ -21038,15 +21862,24 @@ function emitAllowNotice(userMsg, agentMsg) {
21038
21862
  async function runGuard(opts, globals) {
21039
21863
  const on = opts.on.split(",").map((s) => s.trim()).filter((s) => s === "commit" || s === "push");
21040
21864
  const { command, cwd, sessionId } = await readPreToolUseStdin();
21041
- if (cwd && (0, import_node_fs34.existsSync)(cwd)) {
21042
- try {
21043
- process.chdir(cwd);
21044
- } catch {
21045
- }
21046
- }
21047
- const moment = classifyCommand2(command, on);
21865
+ const moment = classifyCommand(command, on);
21048
21866
  if (!moment) process.exit(0);
21049
21867
  const verb = moment === "pre-commit" ? "commit" : "push";
21868
+ const { frame } = resolveFrame({ command, on, hookCwd: cwd });
21869
+ if (frame.refusal || !frame.worktreeRoot) {
21870
+ logEvent("guard_frame", { moment, ...frameTelemetry(frame, null) });
21871
+ if ((frame.refusal ?? "").startsWith("anchor:")) process.exit(0);
21872
+ emitAllowNotice(
21873
+ `\u26A0 Verity ${moment}: could not resolve the tree this ${verb} targets \u2014 ${verb}ed WITHOUT review`,
21874
+ `Verity ${moment}: the target tree could not be resolved (${frame.refusal}); the ${verb} was allowed WITHOUT a Verity review.`
21875
+ );
21876
+ }
21877
+ try {
21878
+ process.chdir(frame.worktreeRoot);
21879
+ } catch {
21880
+ process.exit(0);
21881
+ }
21882
+ _resetRepoRoot();
21050
21883
  const iter = readIter(moment);
21051
21884
  if (iter >= GUARD_BLOCK_CAP) {
21052
21885
  resetIter(moment);
@@ -21055,13 +21888,39 @@ async function runGuard(opts, globals) {
21055
21888
  `Verity ${moment}: review-cycle cap (${GUARD_BLOCK_CAP}) reached; the ${verb} was allowed without a further block.`
21056
21889
  );
21057
21890
  }
21058
- const files = getMomentFiles(moment);
21891
+ const range = resolveMomentRange(moment, frame, command, on);
21892
+ const files = rangeFiles(frame, range);
21059
21893
  if (files.length === 0) process.exit(0);
21060
21894
  const tokenResult = await resolveToken(globals.token);
21061
21895
  const urlResult = await resolveServiceUrl(globals.serviceUrl);
21062
21896
  if (!tokenResult.ok || !urlResult.ok) process.exit(0);
21063
- const requestBody = buildGuardRequest(moment, files, iter, sessionId, command);
21064
- if (!requestBody) process.exit(0);
21897
+ logEvent("guard_frame", { moment, ...frameTelemetry(frame, range) });
21898
+ const codeDelta = collectCodeDelta(files);
21899
+ if (codeDelta.total_files === 0) process.exit(0);
21900
+ const statedIntent = extractStatedIntent(
21901
+ moment,
21902
+ command,
21903
+ moment === "pre-push" ? rangeMessages(frame, range) || null : null
21904
+ );
21905
+ const requestBody = buildGuardRequest(
21906
+ moment,
21907
+ files,
21908
+ codeDelta,
21909
+ iter,
21910
+ sessionId,
21911
+ statedIntent,
21912
+ buildGuardCoverage(files, codeDelta, frame, range)
21913
+ );
21914
+ const coverage = {
21915
+ moment,
21916
+ root: frame.worktreeRoot,
21917
+ branch: frame.branch,
21918
+ linked: frame.isLinkedWorktree,
21919
+ range: describeRange(range),
21920
+ sent: codeDelta.files.map((f) => f.path),
21921
+ excluded: codeDelta.excluded.map((e) => ({ path: e.path, reason: e.reason }))
21922
+ };
21923
+ logToFileOnly(coverageBlock(coverage));
21065
21924
  const result = await analyzeRequest({
21066
21925
  serviceUrl: urlResult.data,
21067
21926
  token: tokenResult.data.token,
@@ -21082,31 +21941,39 @@ async function runGuard(opts, globals) {
21082
21941
  const decision = response.gate_decision ?? "(unrecognised)";
21083
21942
  const viewUrl = response.view_url ?? "";
21084
21943
  const link = viewUrl ? ` \u2014 ${viewUrl}` : "";
21085
- if (decision === "FAIL" && hasBlockingFinding(response)) {
21944
+ const covLine = coverageSummary(coverage);
21945
+ const covDetail = coverageBlock(coverage);
21946
+ if (decision === "FAIL" && hasBlockingFinding(response, codeDelta.files.map((f) => f.path))) {
21086
21947
  writeIter(moment, iter + 1);
21087
- writeBlockMessage(moment, response);
21948
+ writeBlockMessage(moment, response, covDetail);
21088
21949
  process.exit(2);
21089
21950
  }
21090
21951
  resetIter(moment);
21091
21952
  if (decision === "FAIL") {
21092
21953
  const narrative = response.assessment?.narrative ?? "";
21093
21954
  emitAllowNotice(
21094
- `\u26A0 Verity ${moment}: the ${verb} may not match its stated purpose \u2014 proceeding${link}`,
21095
- `Verity ${moment}: intent-alignment WARNING (not blocked).${narrative ? " " + narrative : ""}${viewUrl ? ` Report: ${viewUrl}` : ""}`
21955
+ `\u26A0 Verity ${moment}: the ${verb} may not match its stated purpose \u2014 proceeding (${covLine})${link}`,
21956
+ `Verity ${moment}: intent-alignment WARNING (not blocked).${narrative ? " " + narrative : ""}
21957
+ ${covDetail}${viewUrl ? `
21958
+ Report: ${viewUrl}` : ""}`
21096
21959
  );
21097
21960
  }
21098
21961
  if (decision === "WARN") {
21099
21962
  emitAllowNotice(
21100
- `\u26A0 Verity ${moment}: WARN \u2014 proceeding${link}`,
21101
- `Verity ${moment} review: WARN (proceeding).${viewUrl ? ` Report: ${viewUrl}` : ""}`
21963
+ `\u26A0 Verity ${moment}: WARN \u2014 proceeding (${covLine})${link}`,
21964
+ `Verity ${moment} review: WARN (proceeding).
21965
+ ${covDetail}${viewUrl ? `
21966
+ Report: ${viewUrl}` : ""}`
21102
21967
  );
21103
21968
  }
21104
21969
  emitAllowNotice(
21105
- `\u2713 Verity ${moment}: PASS${link}`,
21106
- `Verity ${moment} review: PASS.${viewUrl ? ` Report: ${viewUrl}` : ""}`
21970
+ `\u2713 Verity ${moment}: PASS (${covLine})${link}`,
21971
+ `Verity ${moment} review: PASS.
21972
+ ${covDetail}${viewUrl ? `
21973
+ Report: ${viewUrl}` : ""}`
21107
21974
  );
21108
21975
  }
21109
- function writeBlockMessage(moment, response) {
21976
+ function writeBlockMessage(moment, response, covDetail) {
21110
21977
  const label2 = moment === "pre-commit" ? "pre-commit" : "pre-push";
21111
21978
  const verb = moment === "pre-commit" ? "commit" : "push";
21112
21979
  const assessment = response.assessment;
@@ -21136,6 +22003,9 @@ function writeBlockMessage(moment, response) {
21136
22003
  `);
21137
22004
  }
21138
22005
  }
22006
+ process.stderr.write(`${DIM}${covDetail}${NC}
22007
+
22008
+ `);
21139
22009
  if (viewUrl) process.stderr.write(`${CYAN}Full report: ${viewUrl}${NC}
21140
22010
 
21141
22011
  `);
@@ -21143,17 +22013,109 @@ function writeBlockMessage(moment, response) {
21143
22013
  `);
21144
22014
  }
21145
22015
 
22016
+ // src/commands/ignore.ts
22017
+ function registerIgnoreCommand(program2) {
22018
+ const ignore = program2.command("ignore").description("Declare the next turn (or a short window) as housekeeping \u2014 no review needed").option("--turn", "Cover the next turn only (the default)").option("--for <duration>", "Cover a window: 30m, 45s, 1h (max 60m)").option("--reason <reason>", "Why this window needs no review \u2014 required, and recorded").option("--agent", "Mark the declaration as agent-invoked (a ledger label, not a permission)").option("--session-id <id>", "Session id (defaults to $CLAUDE_SESSION_ID)").option("--json", "Output raw JSON").action(async (opts) => {
22019
+ const globals = program2.opts();
22020
+ const now = Math.floor(Date.now() / 1e3);
22021
+ const reason = opts.reason?.trim();
22022
+ if (!reason) {
22023
+ printError(
22024
+ 'A reason is required: verity ignore --turn --reason "pulling latest before starting".\nIt is recorded with the declaration and is the only trace a skipped turn leaves.'
22025
+ );
22026
+ process.exit(1);
22027
+ }
22028
+ if (opts.for && opts.turn) {
22029
+ printError("Use either --turn or --for, not both \u2014 they are two different windows.");
22030
+ process.exit(1);
22031
+ }
22032
+ let scope2 = "turn";
22033
+ let ttl = TURN_FUSE_SECONDS;
22034
+ if (opts.for) {
22035
+ const parsed = parseDuration(opts.for);
22036
+ if (!parsed.ok) {
22037
+ printError(parsed.error);
22038
+ process.exit(1);
22039
+ }
22040
+ scope2 = "window";
22041
+ ttl = parsed.seconds;
22042
+ }
22043
+ const tokenResult = await resolveToken(globals.token);
22044
+ const token = tokenResult.ok ? tokenResult.data.token : void 0;
22045
+ const sessionId = opts.sessionId || process.env.CLAUDE_SESSION_ID || void 0;
22046
+ const keys = ignoreStateKeys(token, sessionId);
22047
+ const existing = resolveIgnoreState(keys);
22048
+ const spent = existing?.state.spent ?? 0;
22049
+ const writeKey = existing?.key ?? keys[0];
22050
+ if (spent >= IGNORE_BUDGET) {
22051
+ const msg = `Ignore budget spent for this session (${spent}/${IGNORE_BUDGET} declarations). The next turn will be reviewed normally. The budget is per session \u2014 it is what stops an ignore from becoming a standing mute.`;
22052
+ if (opts.json) {
22053
+ printJson({ declared: false, reason_refused: "budget-spent", spent, budget: IGNORE_BUDGET });
22054
+ } else {
22055
+ printWarn(msg);
22056
+ }
22057
+ logEvent("ignore_refused", { why: "budget-spent", spent, budget: IGNORE_BUDGET });
22058
+ process.exit(0);
22059
+ }
22060
+ const declaration = {
22061
+ scope: scope2,
22062
+ origin: opts.agent ? "agent" : "user",
22063
+ reason,
22064
+ at: now,
22065
+ expires: now + ttl
22066
+ };
22067
+ writeIgnoreState({ v: 1, active: declaration, spent: spent + 1 }, writeKey);
22068
+ logEvent("ignore_declared", {
22069
+ scope: scope2,
22070
+ origin: declaration.origin,
22071
+ ttl_seconds: ttl,
22072
+ spent: spent + 1,
22073
+ budget: IGNORE_BUDGET
22074
+ });
22075
+ if (opts.json) {
22076
+ printJson({
22077
+ declared: true,
22078
+ scope: scope2,
22079
+ origin: declaration.origin,
22080
+ reason,
22081
+ expires_at: new Date(declaration.expires * 1e3).toISOString(),
22082
+ spent: spent + 1,
22083
+ budget: IGNORE_BUDGET
22084
+ });
22085
+ return;
22086
+ }
22087
+ const window = scope2 === "turn" ? "the next turn" : `the next ${describeRemaining(declaration, now).replace(" left", "")}`;
22088
+ printInfo(`Verity will skip ${window} \u2014 "${reason}" (${spent + 1}/${IGNORE_BUDGET} this session).`);
22089
+ printInfo("It covers turns that author nothing. If anything is written, the declaration voids and the review runs.");
22090
+ });
22091
+ ignore.command("clear").description("Cancel the active declaration (the spent budget is not refunded)").option("--session-id <id>", "Session id (defaults to $CLAUDE_SESSION_ID)").action(async (opts) => {
22092
+ const globals = program2.opts();
22093
+ const tokenResult = await resolveToken(globals.token);
22094
+ const token = tokenResult.ok ? tokenResult.data.token : void 0;
22095
+ const sessionId = opts.sessionId || process.env.CLAUDE_SESSION_ID || void 0;
22096
+ const found = resolveIgnoreState(ignoreStateKeys(token, sessionId));
22097
+ const active = resolveActive(found?.state ?? null, Math.floor(Date.now() / 1e3));
22098
+ if (!found || !active) {
22099
+ printInfo("No active ignore declaration.");
22100
+ return;
22101
+ }
22102
+ clearActiveDeclaration(found.key);
22103
+ logEvent("ignore_cleared", { scope: active.scope, origin: active.origin });
22104
+ printInfo(`Cleared: "${active.reason}". The next turn will be reviewed normally.`);
22105
+ });
22106
+ }
22107
+
21146
22108
  // src/commands/init.ts
21147
- var import_node_fs36 = require("node:fs");
22109
+ var import_node_fs40 = require("node:fs");
21148
22110
  var import_promises13 = require("node:fs/promises");
21149
- var import_node_path27 = require("node:path");
21150
- var import_node_child_process10 = require("node:child_process");
22111
+ var import_node_path29 = require("node:path");
22112
+ var import_node_child_process11 = require("node:child_process");
21151
22113
  var readline2 = __toESM(require("node:readline/promises"));
21152
22114
 
21153
22115
  // src/commands/migrate.ts
21154
- var import_node_fs35 = require("node:fs");
21155
- var import_node_path26 = require("node:path");
21156
- var import_node_child_process9 = require("node:child_process");
22116
+ var import_node_fs39 = require("node:fs");
22117
+ var import_node_path28 = require("node:path");
22118
+ var import_node_child_process10 = require("node:child_process");
21157
22119
 
21158
22120
  // src/lib/telemetry.ts
21159
22121
  var import_promises12 = require("node:fs/promises");
@@ -21248,11 +22210,11 @@ async function uninstallTelemetry() {
21248
22210
  // src/commands/migrate.ts
21249
22211
  var LEGACY_NPM_PACKAGE = "@codacy/gate-cli";
21250
22212
  function defaultNpmRemover(pkg) {
21251
- (0, import_node_child_process9.execSync)(`npm rm -g ${pkg}`, { stdio: "pipe", timeout: 12e4 });
22213
+ (0, import_node_child_process10.execSync)(`npm rm -g ${pkg}`, { stdio: "pipe", timeout: 12e4 });
21252
22214
  }
21253
22215
  function isGitTracked(cwd, relPath) {
21254
22216
  try {
21255
- (0, import_node_child_process9.execSync)(`git ls-files --error-unmatch ${relPath}`, { cwd, stdio: "pipe" });
22217
+ (0, import_node_child_process10.execSync)(`git ls-files --error-unmatch ${relPath}`, { cwd, stdio: "pipe" });
21256
22218
  return true;
21257
22219
  } catch {
21258
22220
  return false;
@@ -21260,7 +22222,7 @@ function isGitTracked(cwd, relPath) {
21260
22222
  }
21261
22223
  function isGitRepo(cwd) {
21262
22224
  try {
21263
- (0, import_node_child_process9.execSync)("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
22225
+ (0, import_node_child_process10.execSync)("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
21264
22226
  return true;
21265
22227
  } catch {
21266
22228
  return false;
@@ -21281,12 +22243,12 @@ async function runMigration(opts = {}) {
21281
22243
  return { actions, migrated: actions.length > 0 };
21282
22244
  }
21283
22245
  function migrateProjectDir(root, actions) {
21284
- const gateDir = (0, import_node_path26.join)(root, ".gate");
21285
- const verityDir = (0, import_node_path26.join)(root, ".verity");
21286
- if ((0, import_node_fs35.existsSync)(gateDir) && !(0, import_node_fs35.existsSync)(verityDir)) {
22246
+ const gateDir = (0, import_node_path28.join)(root, ".gate");
22247
+ const verityDir = (0, import_node_path28.join)(root, ".verity");
22248
+ if ((0, import_node_fs39.existsSync)(gateDir) && !(0, import_node_fs39.existsSync)(verityDir)) {
21287
22249
  return migrateProjectDirRename(root, gateDir, verityDir, actions);
21288
22250
  }
21289
- if ((0, import_node_fs35.existsSync)(gateDir) && (0, import_node_fs35.existsSync)(verityDir)) {
22251
+ if ((0, import_node_fs39.existsSync)(gateDir) && (0, import_node_fs39.existsSync)(verityDir)) {
21290
22252
  return migrateProjectDirCarry(gateDir, verityDir, actions);
21291
22253
  }
21292
22254
  return false;
@@ -21300,20 +22262,20 @@ function migrateProjectDirRename(root, gateDir, verityDir, actions) {
21300
22262
  );
21301
22263
  }
21302
22264
  try {
21303
- (0, import_node_child_process9.execSync)("git mv .gate .verity", { cwd: root, stdio: "pipe" });
22265
+ (0, import_node_child_process10.execSync)("git mv .gate .verity", { cwd: root, stdio: "pipe" });
21304
22266
  actions.push("Moved .gate/ \u2192 .verity/ (git mv, staged)");
21305
22267
  moved = true;
21306
22268
  } catch {
21307
22269
  }
21308
22270
  }
21309
22271
  if (moved) {
21310
- if ((0, import_node_fs35.existsSync)(gateDir)) {
22272
+ if ((0, import_node_fs39.existsSync)(gateDir)) {
21311
22273
  const carried = carryLegacyContents(gateDir, verityDir);
21312
22274
  if (carried > 0) {
21313
22275
  actions.push(`Carried ${carried} untracked legacy file(s) from .gate/ into .verity/`);
21314
22276
  }
21315
22277
  try {
21316
- (0, import_node_fs35.rmSync)(gateDir, { recursive: true, force: true });
22278
+ (0, import_node_fs39.rmSync)(gateDir, { recursive: true, force: true });
21317
22279
  } catch {
21318
22280
  }
21319
22281
  }
@@ -21329,18 +22291,18 @@ function migrateProjectDirCarry(gateDir, verityDir, actions) {
21329
22291
  actions.push(`Carried ${carried} legacy file(s) from .gate/ into .verity/`);
21330
22292
  }
21331
22293
  try {
21332
- (0, import_node_fs35.rmSync)(gateDir, { recursive: true, force: true });
22294
+ (0, import_node_fs39.rmSync)(gateDir, { recursive: true, force: true });
21333
22295
  } catch {
21334
22296
  }
21335
22297
  return carried > 0;
21336
22298
  }
21337
22299
  function migrateGlobalCredentials(home, actions) {
21338
22300
  if (!home) return;
21339
- const gateCreds = (0, import_node_path26.join)(home, ".gate", "credentials");
21340
- const verityCreds = (0, import_node_path26.join)(home, ".verity", "credentials");
21341
- if (!(0, import_node_fs35.existsSync)(gateCreds)) return;
21342
- if (!(0, import_node_fs35.existsSync)(verityCreds)) {
21343
- (0, import_node_fs35.mkdirSync)((0, import_node_path26.join)(home, ".verity"), { recursive: true });
22301
+ const gateCreds = (0, import_node_path28.join)(home, ".gate", "credentials");
22302
+ const verityCreds = (0, import_node_path28.join)(home, ".verity", "credentials");
22303
+ if (!(0, import_node_fs39.existsSync)(gateCreds)) return;
22304
+ if (!(0, import_node_fs39.existsSync)(verityCreds)) {
22305
+ (0, import_node_fs39.mkdirSync)((0, import_node_path28.join)(home, ".verity"), { recursive: true });
21344
22306
  moveFile(gateCreds, verityCreds);
21345
22307
  actions.push("Moved ~/.gate/credentials \u2192 ~/.verity/credentials");
21346
22308
  return;
@@ -21362,8 +22324,8 @@ async function migrateLegacyHooks(root, actions) {
21362
22324
  }
21363
22325
  }
21364
22326
  async function migrateClaudeMd(root, actions) {
21365
- const claudeMd = (0, import_node_path26.join)(root, "CLAUDE.md");
21366
- const hadLegacyBlock = (0, import_node_fs35.existsSync)(claudeMd) && hasLegacyMemoryBlock(readFileSyncSafe(claudeMd));
22327
+ const claudeMd = (0, import_node_path28.join)(root, "CLAUDE.md");
22328
+ const hadLegacyBlock = (0, import_node_fs39.existsSync)(claudeMd) && hasLegacyMemoryBlock(readFileSyncSafe(claudeMd));
21367
22329
  if (!hadLegacyBlock) return;
21368
22330
  try {
21369
22331
  await ensureClaudeMdPointer(root);
@@ -21373,13 +22335,13 @@ async function migrateClaudeMd(root, actions) {
21373
22335
  }
21374
22336
  }
21375
22337
  function migrateStandardFile(root, actions) {
21376
- const gateMd = (0, import_node_path26.join)(root, "GATE.md");
21377
- const verityMd = (0, import_node_path26.join)(root, "VERITY.md");
21378
- if (!(0, import_node_fs35.existsSync)(gateMd) || (0, import_node_fs35.existsSync)(verityMd)) return;
22338
+ const gateMd = (0, import_node_path28.join)(root, "GATE.md");
22339
+ const verityMd = (0, import_node_path28.join)(root, "VERITY.md");
22340
+ if (!(0, import_node_fs39.existsSync)(gateMd) || (0, import_node_fs39.existsSync)(verityMd)) return;
21379
22341
  let moved = false;
21380
22342
  if (isGitRepo(root) && isGitTracked(root, "GATE.md")) {
21381
22343
  try {
21382
- (0, import_node_child_process9.execSync)("git mv GATE.md VERITY.md", { cwd: root, stdio: "pipe" });
22344
+ (0, import_node_child_process10.execSync)("git mv GATE.md VERITY.md", { cwd: root, stdio: "pipe" });
21383
22345
  moved = true;
21384
22346
  } catch {
21385
22347
  }
@@ -21387,12 +22349,12 @@ function migrateStandardFile(root, actions) {
21387
22349
  if (!moved) moveFile(gateMd, verityMd);
21388
22350
  const content = readFileSyncSafe(verityMd);
21389
22351
  const refreshed = content.split("GATE.md").join("VERITY.md");
21390
- if (refreshed !== content) (0, import_node_fs35.writeFileSync)(verityMd, refreshed);
22352
+ if (refreshed !== content) (0, import_node_fs39.writeFileSync)(verityMd, refreshed);
21391
22353
  actions.push("Renamed GATE.md \u2192 VERITY.md");
21392
22354
  }
21393
22355
  async function migrateTelemetryHeaders(root, actions) {
21394
- const file = (0, import_node_path26.join)(root, ".claude", "settings.local.json");
21395
- if (!(0, import_node_fs35.existsSync)(file)) return;
22356
+ const file = (0, import_node_path28.join)(root, ".claude", "settings.local.json");
22357
+ if (!(0, import_node_fs39.existsSync)(file)) return;
21396
22358
  let settings;
21397
22359
  try {
21398
22360
  settings = JSON.parse(readFileSyncSafe(file) || "{}");
@@ -21440,21 +22402,21 @@ function mergeGlobalCredentials(gateCreds, verityCreds) {
21440
22402
  }
21441
22403
  if (toAppend.length > 0) {
21442
22404
  const sep2 = verityContent.endsWith("\n") || verityContent === "" ? "" : "\n";
21443
- (0, import_node_fs35.writeFileSync)(verityCreds, verityContent + sep2 + toAppend.join("\n") + "\n");
22405
+ (0, import_node_fs39.writeFileSync)(verityCreds, verityContent + sep2 + toAppend.join("\n") + "\n");
21444
22406
  }
21445
- (0, import_node_fs35.rmSync)(gateCreds, { force: true });
22407
+ (0, import_node_fs39.rmSync)(gateCreds, { force: true });
21446
22408
  return toAppend.length;
21447
22409
  }
21448
22410
  function readFileSyncSafe(path) {
21449
22411
  try {
21450
- return (0, import_node_fs35.readFileSync)(path, "utf-8");
22412
+ return (0, import_node_fs39.readFileSync)(path, "utf-8");
21451
22413
  } catch {
21452
22414
  return "";
21453
22415
  }
21454
22416
  }
21455
22417
  function hasStagedChanges(root) {
21456
22418
  try {
21457
- (0, import_node_child_process9.execSync)("git diff --cached --quiet", { cwd: root, stdio: "pipe" });
22419
+ (0, import_node_child_process10.execSync)("git diff --cached --quiet", { cwd: root, stdio: "pipe" });
21458
22420
  return false;
21459
22421
  } catch {
21460
22422
  return true;
@@ -21462,35 +22424,35 @@ function hasStagedChanges(root) {
21462
22424
  }
21463
22425
  function moveDir(from, to) {
21464
22426
  try {
21465
- (0, import_node_fs35.renameSync)(from, to);
22427
+ (0, import_node_fs39.renameSync)(from, to);
21466
22428
  } catch (err) {
21467
22429
  if (err.code !== "EXDEV") throw err;
21468
- (0, import_node_fs35.cpSync)(from, to, { recursive: true });
21469
- (0, import_node_fs35.rmSync)(from, { recursive: true, force: true });
22430
+ (0, import_node_fs39.cpSync)(from, to, { recursive: true });
22431
+ (0, import_node_fs39.rmSync)(from, { recursive: true, force: true });
21470
22432
  }
21471
22433
  }
21472
22434
  function moveFile(from, to) {
21473
22435
  try {
21474
- (0, import_node_fs35.renameSync)(from, to);
22436
+ (0, import_node_fs39.renameSync)(from, to);
21475
22437
  } catch (err) {
21476
22438
  if (err.code !== "EXDEV") throw err;
21477
- (0, import_node_fs35.cpSync)(from, to);
21478
- (0, import_node_fs35.rmSync)(from, { force: true });
22439
+ (0, import_node_fs39.cpSync)(from, to);
22440
+ (0, import_node_fs39.rmSync)(from, { force: true });
21479
22441
  }
21480
22442
  }
21481
22443
  function carryLegacyContents(gateDir, verityDir) {
21482
22444
  let copied = 0;
21483
22445
  const walk = (relDir) => {
21484
- const srcDir = (0, import_node_path26.join)(gateDir, relDir);
21485
- for (const entry of (0, import_node_fs35.readdirSync)(srcDir)) {
21486
- const rel = relDir ? (0, import_node_path26.join)(relDir, entry) : entry;
21487
- const src = (0, import_node_path26.join)(gateDir, rel);
21488
- const dest = (0, import_node_path26.join)(verityDir, rel);
21489
- if ((0, import_node_fs35.statSync)(src).isDirectory()) {
22446
+ const srcDir = (0, import_node_path28.join)(gateDir, relDir);
22447
+ for (const entry of (0, import_node_fs39.readdirSync)(srcDir)) {
22448
+ const rel = relDir ? (0, import_node_path28.join)(relDir, entry) : entry;
22449
+ const src = (0, import_node_path28.join)(gateDir, rel);
22450
+ const dest = (0, import_node_path28.join)(verityDir, rel);
22451
+ if ((0, import_node_fs39.statSync)(src).isDirectory()) {
21490
22452
  walk(rel);
21491
- } else if (!(0, import_node_fs35.existsSync)(dest)) {
21492
- (0, import_node_fs35.mkdirSync)((0, import_node_path26.dirname)(dest), { recursive: true });
21493
- (0, import_node_fs35.cpSync)(src, dest);
22453
+ } else if (!(0, import_node_fs39.existsSync)(dest)) {
22454
+ (0, import_node_fs39.mkdirSync)((0, import_node_path28.dirname)(dest), { recursive: true });
22455
+ (0, import_node_fs39.cpSync)(src, dest);
21494
22456
  copied++;
21495
22457
  }
21496
22458
  }
@@ -21499,22 +22461,22 @@ function carryLegacyContents(gateDir, verityDir) {
21499
22461
  return copied;
21500
22462
  }
21501
22463
  async function needsMigration(root = repoRoot()) {
21502
- const gateDir = (0, import_node_path26.join)(root, ".gate");
21503
- const verityDir = (0, import_node_path26.join)(root, ".verity");
21504
- if ((0, import_node_fs35.existsSync)(gateDir) && !(0, import_node_fs35.existsSync)(verityDir)) return true;
21505
- if ((0, import_node_fs35.existsSync)(gateDir) && (0, import_node_fs35.existsSync)(verityDir)) {
21506
- if ((0, import_node_fs35.existsSync)((0, import_node_path26.join)(gateDir, "credentials")) && !(0, import_node_fs35.existsSync)((0, import_node_path26.join)(verityDir, "credentials"))) {
22464
+ const gateDir = (0, import_node_path28.join)(root, ".gate");
22465
+ const verityDir = (0, import_node_path28.join)(root, ".verity");
22466
+ if ((0, import_node_fs39.existsSync)(gateDir) && !(0, import_node_fs39.existsSync)(verityDir)) return true;
22467
+ if ((0, import_node_fs39.existsSync)(gateDir) && (0, import_node_fs39.existsSync)(verityDir)) {
22468
+ if ((0, import_node_fs39.existsSync)((0, import_node_path28.join)(gateDir, "credentials")) && !(0, import_node_fs39.existsSync)((0, import_node_path28.join)(verityDir, "credentials"))) {
21507
22469
  return true;
21508
22470
  }
21509
- if ((0, import_node_fs35.existsSync)((0, import_node_path26.join)(gateDir, "memory")) && !(0, import_node_fs35.existsSync)((0, import_node_path26.join)(verityDir, "memory"))) {
22471
+ if ((0, import_node_fs39.existsSync)((0, import_node_path28.join)(gateDir, "memory")) && !(0, import_node_fs39.existsSync)((0, import_node_path28.join)(verityDir, "memory"))) {
21510
22472
  return true;
21511
22473
  }
21512
22474
  }
21513
- const claudeMd = (0, import_node_path26.join)(root, "CLAUDE.md");
21514
- if ((0, import_node_fs35.existsSync)(claudeMd) && hasLegacyMemoryBlock(readFileSyncSafe(claudeMd))) {
22475
+ const claudeMd = (0, import_node_path28.join)(root, "CLAUDE.md");
22476
+ if ((0, import_node_fs39.existsSync)(claudeMd) && hasLegacyMemoryBlock(readFileSyncSafe(claudeMd))) {
21515
22477
  return true;
21516
22478
  }
21517
- if ((0, import_node_fs35.existsSync)((0, import_node_path26.join)(root, "GATE.md")) && !(0, import_node_fs35.existsSync)((0, import_node_path26.join)(root, "VERITY.md"))) {
22479
+ if ((0, import_node_fs39.existsSync)((0, import_node_path28.join)(root, "GATE.md")) && !(0, import_node_fs39.existsSync)((0, import_node_path28.join)(root, "VERITY.md"))) {
21518
22480
  return true;
21519
22481
  }
21520
22482
  if (await hasLegacyHooksAt(root)) return true;
@@ -21607,7 +22569,7 @@ async function runOptionalAuth(resolution, opts = {}) {
21607
22569
  }
21608
22570
  let remote = "";
21609
22571
  try {
21610
- remote = (0, import_node_child_process10.execSync)("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
22572
+ remote = (0, import_node_child_process11.execSync)("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
21611
22573
  } catch {
21612
22574
  }
21613
22575
  if (!healed) {
@@ -21650,15 +22612,15 @@ async function runOptionalAuth(resolution, opts = {}) {
21650
22612
  }
21651
22613
  function resolveDataDir() {
21652
22614
  const candidates = [
21653
- (0, import_node_path27.join)(__dirname, "..", "data"),
22615
+ (0, import_node_path29.join)(__dirname, "..", "data"),
21654
22616
  // installed: node_modules/@codacy/verity-cli/data
21655
- (0, import_node_path27.join)(__dirname, "..", "..", "data"),
22617
+ (0, import_node_path29.join)(__dirname, "..", "..", "data"),
21656
22618
  // edge case: nested resolution
21657
- (0, import_node_path27.join)(process.cwd(), "cli", "data")
22619
+ (0, import_node_path29.join)(process.cwd(), "cli", "data")
21658
22620
  // local dev: running from repo root
21659
22621
  ];
21660
22622
  for (const candidate of candidates) {
21661
- if ((0, import_node_fs36.existsSync)((0, import_node_path27.join)(candidate, "skills"))) {
22623
+ if ((0, import_node_fs40.existsSync)((0, import_node_path29.join)(candidate, "skills"))) {
21662
22624
  return candidate;
21663
22625
  }
21664
22626
  }
@@ -21674,7 +22636,7 @@ function registerInitCommand(program2) {
21674
22636
  program2.command("init").description("Initialize Verity in the current project").option("--force", "Overwrite existing skills and hooks").action(async (opts) => {
21675
22637
  const force = opts.force ?? false;
21676
22638
  const projectMarkers = [".git", "package.json", "pyproject.toml", "go.mod", "Cargo.toml", "Gemfile", "pom.xml", "build.gradle"];
21677
- const isProject = projectMarkers.some((m) => (0, import_node_fs36.existsSync)(m));
22639
+ const isProject = projectMarkers.some((m) => (0, import_node_fs40.existsSync)(m));
21678
22640
  if (!isProject) {
21679
22641
  printError("No project detected in the current directory.");
21680
22642
  printInfo('Run "verity init" from your project root.');
@@ -21702,30 +22664,30 @@ function registerInitCommand(program2) {
21702
22664
  }
21703
22665
  printInfo(` Node.js ${nodeVersion} \u2713`);
21704
22666
  try {
21705
- const gitVersion = (0, import_node_child_process10.execSync)("git --version", { encoding: "utf-8" }).trim();
22667
+ const gitVersion = (0, import_node_child_process11.execSync)("git --version", { encoding: "utf-8" }).trim();
21706
22668
  printInfo(` ${gitVersion} \u2713`);
21707
22669
  } catch {
21708
22670
  printError("git is required but not installed. Install from https://git-scm.com");
21709
22671
  process.exit(1);
21710
22672
  }
21711
22673
  try {
21712
- (0, import_node_child_process10.execSync)("which claude", { encoding: "utf-8" });
22674
+ (0, import_node_child_process11.execSync)("which claude", { encoding: "utf-8" });
21713
22675
  printInfo(" Claude Code \u2713");
21714
22676
  } catch {
21715
22677
  printWarn(" Claude Code not found \u2014 hooks will be configured but need Claude Code to run.");
21716
22678
  }
21717
22679
  try {
21718
- (0, import_node_child_process10.execSync)("which codacy-analysis", { encoding: "utf-8", stdio: "pipe" });
22680
+ (0, import_node_child_process11.execSync)("which codacy-analysis", { encoding: "utf-8", stdio: "pipe" });
21719
22681
  printInfo(" @codacy/analysis-cli \u2713");
21720
22682
  } catch {
21721
22683
  printInfo(" Installing @codacy/analysis-cli...");
21722
22684
  try {
21723
- (0, import_node_child_process10.execSync)("npm install -g @codacy/analysis-cli", { encoding: "utf-8", stdio: "pipe", timeout: 12e4 });
22685
+ (0, import_node_child_process11.execSync)("npm install -g @codacy/analysis-cli", { encoding: "utf-8", stdio: "pipe", timeout: 12e4 });
21724
22686
  printInfo(" @codacy/analysis-cli installed \u2713");
21725
22687
  } catch {
21726
22688
  try {
21727
22689
  printWarn(" Retrying with sudo...");
21728
- (0, import_node_child_process10.execSync)("sudo npm install -g @codacy/analysis-cli", { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
22690
+ (0, import_node_child_process11.execSync)("sudo npm install -g @codacy/analysis-cli", { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
21729
22691
  printInfo(" @codacy/analysis-cli installed \u2713");
21730
22692
  } catch {
21731
22693
  printWarn(" Could not install @codacy/analysis-cli automatically.");
@@ -21737,21 +22699,21 @@ function registerInitCommand(program2) {
21737
22699
  console.log("");
21738
22700
  printInfo("Installing skills...");
21739
22701
  const dataDir = resolveDataDir();
21740
- const skillsSource = (0, import_node_path27.join)(dataDir, "skills");
22702
+ const skillsSource = (0, import_node_path29.join)(dataDir, "skills");
21741
22703
  const skillsDest = ".claude/skills";
21742
22704
  const skills = ["verity-setup", "verity-analyze", "verity-status", "verity-feedback", "verity-learn", "verity-memory", "verity-insights", "verity-reflect"];
21743
22705
  let skillsInstalled = 0;
21744
22706
  for (const skill of skills) {
21745
- const src = (0, import_node_path27.join)(skillsSource, skill);
21746
- const dest = (0, import_node_path27.join)(skillsDest, skill);
21747
- if (!(0, import_node_fs36.existsSync)(src)) {
22707
+ const src = (0, import_node_path29.join)(skillsSource, skill);
22708
+ const dest = (0, import_node_path29.join)(skillsDest, skill);
22709
+ if (!(0, import_node_fs40.existsSync)(src)) {
21748
22710
  printWarn(` Skill data not found: ${skill}`);
21749
22711
  continue;
21750
22712
  }
21751
- if ((0, import_node_fs36.existsSync)(dest) && !force) {
21752
- const srcSkill = (0, import_node_path27.join)(src, "SKILL.md");
21753
- const destSkill = (0, import_node_path27.join)(dest, "SKILL.md");
21754
- if ((0, import_node_fs36.existsSync)(destSkill)) {
22713
+ if ((0, import_node_fs40.existsSync)(dest) && !force) {
22714
+ const srcSkill = (0, import_node_path29.join)(src, "SKILL.md");
22715
+ const destSkill = (0, import_node_path29.join)(dest, "SKILL.md");
22716
+ if ((0, import_node_fs40.existsSync)(destSkill)) {
21755
22717
  try {
21756
22718
  const srcContent = await (0, import_promises13.readFile)(srcSkill, "utf-8");
21757
22719
  const destContent = await (0, import_promises13.readFile)(destSkill, "utf-8");
@@ -21794,7 +22756,7 @@ function registerInitCommand(program2) {
21794
22756
  } catch (err) {
21795
22757
  printWarn(` Could not update CLAUDE.md: ${err.message}`);
21796
22758
  }
21797
- const globalVerityDir = (0, import_node_path27.join)(process.env.HOME ?? "", ".verity");
22759
+ const globalVerityDir = (0, import_node_path29.join)(process.env.HOME ?? "", ".verity");
21798
22760
  await (0, import_promises13.mkdir)(globalVerityDir, { recursive: true });
21799
22761
  console.log("");
21800
22762
  try {
@@ -21829,8 +22791,8 @@ function registerInitCommand(program2) {
21829
22791
  }
21830
22792
 
21831
22793
  // src/commands/uninstall.ts
21832
- var import_node_fs37 = require("node:fs");
21833
- var import_node_path28 = require("node:path");
22794
+ var import_node_fs41 = require("node:fs");
22795
+ var import_node_path30 = require("node:path");
21834
22796
  var SKILL_NAMES = [
21835
22797
  "verity-setup",
21836
22798
  "verity-analyze",
@@ -21849,11 +22811,11 @@ function registerUninstallCommand(program2) {
21849
22811
  const actions = [];
21850
22812
  const skillsRoot = projectPath(".claude/skills");
21851
22813
  for (const name of SKILL_NAMES) {
21852
- const dir = (0, import_node_path28.join)(skillsRoot, name);
21853
- if ((0, import_node_fs37.existsSync)(dir)) {
22814
+ const dir = (0, import_node_path30.join)(skillsRoot, name);
22815
+ if ((0, import_node_fs41.existsSync)(dir)) {
21854
22816
  actions.push({
21855
22817
  label: `Remove .claude/skills/${name}/`,
21856
- apply: () => (0, import_node_fs37.rmSync)(dir, { recursive: true, force: true })
22818
+ apply: () => (0, import_node_fs41.rmSync)(dir, { recursive: true, force: true })
21857
22819
  });
21858
22820
  }
21859
22821
  }
@@ -21867,24 +22829,24 @@ function registerUninstallCommand(program2) {
21867
22829
  });
21868
22830
  }
21869
22831
  const verityDir = projectPath(VERITY_DIR);
21870
- if ((0, import_node_fs37.existsSync)(verityDir)) {
22832
+ if ((0, import_node_fs41.existsSync)(verityDir)) {
21871
22833
  actions.push({
21872
22834
  label: `Remove ${VERITY_DIR}/`,
21873
- apply: () => (0, import_node_fs37.rmSync)(verityDir, { recursive: true, force: true })
22835
+ apply: () => (0, import_node_fs41.rmSync)(verityDir, { recursive: true, force: true })
21874
22836
  });
21875
22837
  }
21876
22838
  if (!keepVerityMd) {
21877
22839
  const verityMd = projectPath(VERITY_MD_FILE);
21878
- if ((0, import_node_fs37.existsSync)(verityMd)) {
22840
+ if ((0, import_node_fs41.existsSync)(verityMd)) {
21879
22841
  actions.push({
21880
22842
  label: `Remove ${VERITY_MD_FILE}`,
21881
- apply: () => (0, import_node_fs37.rmSync)(verityMd, { force: true })
22843
+ apply: () => (0, import_node_fs41.rmSync)(verityMd, { force: true })
21882
22844
  });
21883
22845
  }
21884
22846
  }
21885
22847
  const cleanupEmptyDir = (path) => {
21886
- if ((0, import_node_fs37.existsSync)(path) && (0, import_node_fs37.statSync)(path).isDirectory() && (0, import_node_fs37.readdirSync)(path).length === 0) {
21887
- (0, import_node_fs37.rmdirSync)(path);
22848
+ if ((0, import_node_fs41.existsSync)(path) && (0, import_node_fs41.statSync)(path).isDirectory() && (0, import_node_fs41.readdirSync)(path).length === 0) {
22849
+ (0, import_node_fs41.rmdirSync)(path);
21888
22850
  }
21889
22851
  };
21890
22852
  actions.push({
@@ -21895,11 +22857,11 @@ function registerUninstallCommand(program2) {
21895
22857
  }
21896
22858
  });
21897
22859
  const home = process.env.HOME ?? "";
21898
- const globalVerityDir = (0, import_node_path28.join)(home, ".verity");
21899
- if (purgeGlobal && (0, import_node_fs37.existsSync)(globalVerityDir)) {
22860
+ const globalVerityDir = (0, import_node_path30.join)(home, ".verity");
22861
+ if (purgeGlobal && (0, import_node_fs41.existsSync)(globalVerityDir)) {
21900
22862
  actions.push({
21901
22863
  label: `Remove ~/.verity/ (global credentials \u2014 reconnect requires re-registration)`,
21902
- apply: () => (0, import_node_fs37.rmSync)(globalVerityDir, { recursive: true, force: true })
22864
+ apply: () => (0, import_node_fs41.rmSync)(globalVerityDir, { recursive: true, force: true })
21903
22865
  });
21904
22866
  }
21905
22867
  if (actions.length === 0) {
@@ -22093,8 +23055,8 @@ function registerTaskCommands(program2) {
22093
23055
  }
22094
23056
 
22095
23057
  // src/commands/reset.ts
22096
- var import_node_fs38 = require("node:fs");
22097
- var import_node_path29 = require("node:path");
23058
+ var import_node_fs42 = require("node:fs");
23059
+ var import_node_path31 = require("node:path");
22098
23060
  function registerResetCommand(program2) {
22099
23061
  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) => {
22100
23062
  const globals = program2.opts();
@@ -22131,11 +23093,11 @@ function registerResetCommand(program2) {
22131
23093
  }
22132
23094
  const cacheDir = projectPath(CACHE_DIR);
22133
23095
  let purged = 0;
22134
- if ((0, import_node_fs38.existsSync)(cacheDir)) {
22135
- for (const entry of (0, import_node_fs38.readdirSync)(cacheDir)) {
23096
+ if ((0, import_node_fs42.existsSync)(cacheDir)) {
23097
+ for (const entry of (0, import_node_fs42.readdirSync)(cacheDir)) {
22136
23098
  if (entry.startsWith("pending-")) {
22137
23099
  try {
22138
- (0, import_node_fs38.unlinkSync)((0, import_node_path29.join)(cacheDir, entry));
23100
+ (0, import_node_fs42.unlinkSync)((0, import_node_path31.join)(cacheDir, entry));
22139
23101
  purged++;
22140
23102
  } catch {
22141
23103
  }
@@ -22150,19 +23112,19 @@ function registerResetCommand(program2) {
22150
23112
  projectPath(`${VERITY_DIR}/.last-analysis`)
22151
23113
  ];
22152
23114
  for (const file of filesToClear) {
22153
- if ((0, import_node_fs38.existsSync)(file)) {
23115
+ if ((0, import_node_fs42.existsSync)(file)) {
22154
23116
  try {
22155
- (0, import_node_fs38.writeFileSync)(file, "");
23117
+ (0, import_node_fs42.writeFileSync)(file, "");
22156
23118
  } catch {
22157
23119
  }
22158
23120
  }
22159
23121
  }
22160
23122
  if (opts.all) {
22161
23123
  const logsDir = projectPath(`${VERITY_DIR}/.logs`);
22162
- if ((0, import_node_fs38.existsSync)(logsDir)) {
22163
- for (const entry of (0, import_node_fs38.readdirSync)(logsDir)) {
23124
+ if ((0, import_node_fs42.existsSync)(logsDir)) {
23125
+ for (const entry of (0, import_node_fs42.readdirSync)(logsDir)) {
22164
23126
  try {
22165
- (0, import_node_fs38.unlinkSync)((0, import_node_path29.join)(logsDir, entry));
23127
+ (0, import_node_fs42.unlinkSync)((0, import_node_path31.join)(logsDir, entry));
22166
23128
  } catch {
22167
23129
  }
22168
23130
  }
@@ -22470,8 +23432,8 @@ function registerTelemetryCommands(program2) {
22470
23432
  }
22471
23433
 
22472
23434
  // src/cli.ts
22473
- program.name("verity").description("CLI for Verity quality gate service").version("0.30.0").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async (_thisCommand, actionCommand) => {
22474
- installStderrLog(actionCommand.name(), process.argv.slice(2), "0.30.0");
23435
+ program.name("verity").description("CLI for Verity quality gate service").version("0.30.1-experimental.cbeb697").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async (_thisCommand, actionCommand) => {
23436
+ installStderrLog(actionCommand.name(), process.argv.slice(2), "0.30.1-experimental.cbeb697");
22475
23437
  setUserNamedServiceUrl(program.opts().serviceUrl);
22476
23438
  try {
22477
23439
  await foldLegacyLocalCredential();
@@ -22494,6 +23456,7 @@ registerAnalyzeCommand(program);
22494
23456
  registerBaselineCommands(program);
22495
23457
  registerReviewCommand(program);
22496
23458
  registerGuardCommand(program);
23459
+ registerIgnoreCommand(program);
22497
23460
  registerInitCommand(program);
22498
23461
  registerUninstallCommand(program);
22499
23462
  registerTaskCommands(program);