@codacy/verity-cli 0.30.1 → 0.31.0-experimental.6bc3b1b

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
@@ -10387,6 +10387,7 @@ var CLAUDE_SETTINGS_FILE = ".claude/settings.json";
10387
10387
  var STANDARD_FILE = `${VERITY_DIR}/standard.yaml`;
10388
10388
  var MEMORY_DIR = `${VERITY_DIR}/memory`;
10389
10389
  var CODACY_CONFIG_FILE = ".codacy/codacy.config.json";
10390
+ var VERITYIGNORE_FILE = ".verityignore";
10390
10391
  function projectPath(relativePath) {
10391
10392
  return (0, import_node_path.join)(repoRoot(), relativePath);
10392
10393
  }
@@ -10406,6 +10407,13 @@ var CONVERSATION_BUFFER_FILE = `${VERITY_DIR}/.conversation-buffer`;
10406
10407
  var CONVERSATION_MAX_ENTRIES = 10;
10407
10408
  var CONVERSATION_WINDOW_MINUTES = 15;
10408
10409
  var MAX_FINDINGS = 25;
10410
+ var GIT_MOMENT_BUDGET = {
10411
+ maxFiles: 120,
10412
+ maxFileBytes: 98304,
10413
+ // 96KB, under the server's 100k chars
10414
+ maxTotalBytes: 583680
10415
+ // 570KB, under the server's 600k chars
10416
+ };
10409
10417
  var ANALYZABLE_EXTENSIONS = /* @__PURE__ */ new Set([
10410
10418
  "ts",
10411
10419
  "tsx",
@@ -10477,7 +10485,8 @@ var REVIEWABLE_FILENAMES = /* @__PURE__ */ new Set([
10477
10485
  "Makefile",
10478
10486
  "Dockerfile",
10479
10487
  "Jenkinsfile",
10480
- "Vagrantfile"
10488
+ "Vagrantfile",
10489
+ ".verityignore"
10481
10490
  ]);
10482
10491
  var REVIEWABLE_PATH_PATTERNS = [
10483
10492
  /\.circleci\//,
@@ -10497,7 +10506,7 @@ var SECURITY_PATTERNS = [
10497
10506
  /Dockerfile/
10498
10507
  ];
10499
10508
  var PROD_SERVICE_URL = "https://ofcamwrjwrkazqvdchko.supabase.co/functions/v1";
10500
- var DEFAULT_SERVICE_URL = "".length > 0 ? "" : PROD_SERVICE_URL;
10509
+ var DEFAULT_SERVICE_URL = "https://wukeddyzpijoegyajtnc.supabase.co/functions/v1".length > 0 ? "https://wukeddyzpijoegyajtnc.supabase.co/functions/v1" : PROD_SERVICE_URL;
10501
10510
  var GITHUB_CLIENT_ID = "Iv23li88HxAi3ZrbYzWh";
10502
10511
  var GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
10503
10512
  var GITHUB_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
@@ -10507,6 +10516,7 @@ function githubAppInstallUrl(accountId) {
10507
10516
  return accountId != null ? `https://github.com/apps/${GITHUB_APP_SLUG}/installations/new/permissions?target_id=${accountId}` : GITHUB_APP_INSTALL_URL;
10508
10517
  }
10509
10518
  var ADVISORY_EPISODE_FILE = `${VERITY_DIR}/.advisory-episode`;
10519
+ var IGNORE_DECLARATION_FILE = `${VERITY_DIR}/.ignore-declaration`;
10510
10520
 
10511
10521
  // src/lib/output.ts
10512
10522
  var RED = "\x1B[0;31m";
@@ -13530,6 +13540,90 @@ var LEGACY_MD_END = "<!-- gate-memory:end -->";
13530
13540
  var LEGACY_PRESERVE_START = "<!-- gate-memory:preserve -->";
13531
13541
  var LEGACY_PRESERVE_END = "<!-- /gate-memory:preserve -->";
13532
13542
  var CLAUDE_MD_PROSE = [
13543
+ "## Project Memory",
13544
+ "",
13545
+ "This project has a knowledge graph maintained at `.verity/memory/`. Before starting",
13546
+ "non-trivial work, scan `.verity/memory/index.md` for decisions, gotchas, and patterns",
13547
+ "that may apply to the change you are about to make. Open specific node files via",
13548
+ "the Read tool when the title or scope suggests relevance.",
13549
+ "",
13550
+ "The graph is auto-maintained by Verity. Files at `.verity/memory/_archive/` are",
13551
+ "superseded \u2014 ignore them unless investigating history.",
13552
+ "",
13553
+ "## Quality gate: accepted risks",
13554
+ "",
13555
+ "When the Verity pre-commit/pre-push gate FAILs, fix the findings \u2014 that is the",
13556
+ 'default. Use `verity waive <pattern-id> --file <path> --reason "\u2026"` ONLY to relay',
13557
+ "a risk a human has explicitly accepted: a named code-review finding, an ADR, or",
13558
+ "the user saying so in this conversation. The --reason must cite that source.",
13559
+ "",
13560
+ "Never waive on your own judgment, to get past a block, or pre-emptively. A waive",
13561
+ "binds to the file's current bytes and voids automatically when the file changes,",
13562
+ "and every waive is recorded in the run ledger. For a pattern-level false positive",
13563
+ "use `verity feedback finding <run-id> <pattern-id> false_positive` instead.",
13564
+ "",
13565
+ "> Durable, hand-curated guidance goes in the preserve region below (it survives",
13566
+ "> regeneration) or anywhere OUTSIDE these markers. Everything else between the",
13567
+ "> markers is tool-owned and overwritten on each run.",
13568
+ "",
13569
+ "## Housekeeping Turns",
13570
+ "",
13571
+ "When a turn will be pure housekeeping \u2014 pulling, installing dependencies,",
13572
+ "rebasing, a formatting sweep you are not authoring \u2014 declare it BEFORE doing it:",
13573
+ "",
13574
+ "```bash",
13575
+ 'verity ignore --turn --agent --reason "pulling latest before starting"',
13576
+ "```",
13577
+ "",
13578
+ "This skips the review for that turn, which saves the turn Verity would",
13579
+ "otherwise spend saying it had nothing to say. Use `--for 30m` instead of",
13580
+ "`--turn` when a single piece of housekeeping spans several turns.",
13581
+ "",
13582
+ "**It is a claim about the turn, not a way to silence review.** The declaration",
13583
+ "is checked against what the turn actually did: if anything is authored \u2014 by you,",
13584
+ "by a subagent, or by a shell command that can write files \u2014 it voids, the review",
13585
+ "runs anyway, and the broken declaration is reported. So declare housekeeping you",
13586
+ "are about to do, never work you have already done, and never as a way to get past",
13587
+ "a finding. Declarations are budgeted per session and every one is recorded with",
13588
+ "its reason."
13589
+ ].join("\n");
13590
+ var CLAUDE_MD_PROSE_PRE_WAIVE = [
13591
+ "## Project Memory",
13592
+ "",
13593
+ "This project has a knowledge graph maintained at `.verity/memory/`. Before starting",
13594
+ "non-trivial work, scan `.verity/memory/index.md` for decisions, gotchas, and patterns",
13595
+ "that may apply to the change you are about to make. Open specific node files via",
13596
+ "the Read tool when the title or scope suggests relevance.",
13597
+ "",
13598
+ "The graph is auto-maintained by Verity. Files at `.verity/memory/_archive/` are",
13599
+ "superseded \u2014 ignore them unless investigating history.",
13600
+ "",
13601
+ "> Durable, hand-curated guidance goes in the preserve region below (it survives",
13602
+ "> regeneration) or anywhere OUTSIDE these markers. Everything else between the",
13603
+ "> markers is tool-owned and overwritten on each run.",
13604
+ "",
13605
+ "## Housekeeping Turns",
13606
+ "",
13607
+ "When a turn will be pure housekeeping \u2014 pulling, installing dependencies,",
13608
+ "rebasing, a formatting sweep you are not authoring \u2014 declare it BEFORE doing it:",
13609
+ "",
13610
+ "```bash",
13611
+ 'verity ignore --turn --agent --reason "pulling latest before starting"',
13612
+ "```",
13613
+ "",
13614
+ "This skips the review for that turn, which saves the turn Verity would",
13615
+ "otherwise spend saying it had nothing to say. Use `--for 30m` instead of",
13616
+ "`--turn` when a single piece of housekeeping spans several turns.",
13617
+ "",
13618
+ "**It is a claim about the turn, not a way to silence review.** The declaration",
13619
+ "is checked against what the turn actually did: if anything is authored \u2014 by you,",
13620
+ "by a subagent, or by a shell command that can write files \u2014 it voids, the review",
13621
+ "runs anyway, and the broken declaration is reported. So declare housekeeping you",
13622
+ "are about to do, never work you have already done, and never as a way to get past",
13623
+ "a finding. Declarations are budgeted per session and every one is recorded with",
13624
+ "its reason."
13625
+ ].join("\n");
13626
+ var CLAUDE_MD_PROSE_PRE_IGNORE = [
13533
13627
  "## Project Memory",
13534
13628
  "",
13535
13629
  "This project has a knowledge graph maintained at `.verity/memory/`. Before starting",
@@ -13654,7 +13748,12 @@ function extractPreserveContent(interior) {
13654
13748
  }
13655
13749
  function stripKnownProse(interior) {
13656
13750
  const trimmed = interior.replace(/^\n+/, "");
13657
- for (const prose of [CLAUDE_MD_PROSE, CLAUDE_MD_PROSE_LEGACY]) {
13751
+ for (const prose of [
13752
+ CLAUDE_MD_PROSE,
13753
+ CLAUDE_MD_PROSE_PRE_WAIVE,
13754
+ CLAUDE_MD_PROSE_PRE_IGNORE,
13755
+ CLAUDE_MD_PROSE_LEGACY
13756
+ ]) {
13658
13757
  if (trimmed.startsWith(prose)) return trimmed.slice(prose.length);
13659
13758
  }
13660
13759
  return trimmed;
@@ -16270,7 +16369,145 @@ async function readHookStdin() {
16270
16369
 
16271
16370
  // src/commands/standard.ts
16272
16371
  var import_promises9 = require("node:fs/promises");
16372
+ var import_node_fs20 = require("node:fs");
16273
16373
  var import_yaml = __toESM(require_dist());
16374
+
16375
+ // src/lib/verityignore.ts
16376
+ var import_node_fs19 = require("node:fs");
16377
+ var EMPTY = { rules: [], securityOverlap: [], problems: [] };
16378
+ var SECURITY_PROBES = [
16379
+ ".env",
16380
+ ".env.local",
16381
+ ".env.production",
16382
+ "config/.env",
16383
+ "services/api/.env",
16384
+ "package-lock.json",
16385
+ "yarn.lock",
16386
+ "pnpm-lock.yaml",
16387
+ "Cargo.lock",
16388
+ "go.sum",
16389
+ "Gemfile.lock",
16390
+ "Dockerfile",
16391
+ "docker/Dockerfile"
16392
+ ];
16393
+ function isSecuritySensitive(path) {
16394
+ return SECURITY_PATTERNS.some((p) => p.test(path));
16395
+ }
16396
+ function compile(pattern) {
16397
+ let p = pattern;
16398
+ const dirOnly = p.endsWith("/");
16399
+ if (dirOnly) p = p.slice(0, -1);
16400
+ const anchored = p.includes("/");
16401
+ if (p.startsWith("/")) p = p.slice(1);
16402
+ const base = anchored ? p : `**/${p}`;
16403
+ const forms = dirOnly ? [`${base}/**/*`] : [base, `${base}/**`];
16404
+ const regexes = [];
16405
+ for (const f of forms) {
16406
+ try {
16407
+ regexes.push(globToRegex(f));
16408
+ } catch {
16409
+ }
16410
+ }
16411
+ if (regexes.length === 0) return () => false;
16412
+ return (path) => regexes.some((r) => r.test(path));
16413
+ }
16414
+ function parseVerityIgnore(content) {
16415
+ const rules = [];
16416
+ const problems = [];
16417
+ const lines = content.split("\n");
16418
+ for (let i = 0; i < lines.length; i++) {
16419
+ const lineNo = i + 1;
16420
+ let raw = lines[i];
16421
+ raw = raw.replace(/(?<!\\)\s+$/, "");
16422
+ if (raw.length === 0) continue;
16423
+ if (raw.startsWith("#")) continue;
16424
+ let negated = false;
16425
+ if (raw.startsWith("!")) {
16426
+ negated = true;
16427
+ raw = raw.slice(1);
16428
+ } else if (raw.startsWith("\\!") || raw.startsWith("\\#")) {
16429
+ raw = raw.slice(1);
16430
+ }
16431
+ if (raw.length === 0) {
16432
+ problems.push(`line ${lineNo}: "!" with no pattern after it`);
16433
+ continue;
16434
+ }
16435
+ if (raw === "**" || raw === "*" || raw === "/" || raw === "**/*") {
16436
+ problems.push(
16437
+ `line ${lineNo}: "${raw}" would exclude the whole repository from review \u2014 refused. List the directories you mean instead.`
16438
+ );
16439
+ continue;
16440
+ }
16441
+ rules.push({ raw, line: lineNo, negated, test: compile(raw) });
16442
+ }
16443
+ const byRule = /* @__PURE__ */ new Map();
16444
+ for (const probe of SECURITY_PROBES) {
16445
+ let responsible = null;
16446
+ for (const rule of rules) {
16447
+ if (rule.test(probe)) responsible = rule.negated ? null : rule;
16448
+ }
16449
+ if (!responsible) continue;
16450
+ const entry = byRule.get(responsible.line) ?? { raw: responsible.raw, line: responsible.line, hides: [] };
16451
+ entry.hides.push(probe);
16452
+ byRule.set(responsible.line, entry);
16453
+ }
16454
+ const securityOverlap = [...byRule.values()].sort((a, b) => a.line - b.line);
16455
+ return { rules, securityOverlap, problems };
16456
+ }
16457
+ function decide(rules, path) {
16458
+ let excluded = false;
16459
+ for (const rule of rules) {
16460
+ if (rule.test(path)) excluded = !rule.negated;
16461
+ }
16462
+ return excluded;
16463
+ }
16464
+ function isIgnored(ig, path) {
16465
+ return decide(ig.rules, path);
16466
+ }
16467
+ function loadVerityIgnore() {
16468
+ const file = projectPath(VERITYIGNORE_FILE);
16469
+ if (!(0, import_node_fs19.existsSync)(file)) return EMPTY;
16470
+ try {
16471
+ return parseVerityIgnore((0, import_node_fs19.readFileSync)(file, "utf-8"));
16472
+ } catch {
16473
+ return EMPTY;
16474
+ }
16475
+ }
16476
+ function partitionIgnored(paths, ig) {
16477
+ const suspended = paths.some((p) => p === VERITYIGNORE_FILE);
16478
+ if (suspended || ig.rules.length === 0) {
16479
+ return { rules: ig.rules.length, kept: [...paths], ignored: [], securityExcluded: [], suspended };
16480
+ }
16481
+ const kept = [];
16482
+ const ignored = [];
16483
+ for (const p of paths) {
16484
+ if (p !== VERITYIGNORE_FILE && isIgnored(ig, p)) ignored.push(p);
16485
+ else kept.push(p);
16486
+ }
16487
+ return {
16488
+ rules: ig.rules.length,
16489
+ kept,
16490
+ ignored,
16491
+ securityExcluded: ignored.filter(isSecuritySensitive),
16492
+ suspended: false
16493
+ };
16494
+ }
16495
+ function ignoreShare(kept, ignored) {
16496
+ const total = kept + ignored;
16497
+ if (total === 0) return 0;
16498
+ return Math.round(ignored / total * 1e3) / 1e3;
16499
+ }
16500
+ function describeSecurityOverlap(ig) {
16501
+ if (ig.securityOverlap.length === 0) return null;
16502
+ const lines = ig.securityOverlap.map(
16503
+ (o) => ` line ${o.line}: "${o.raw}" would hide ${o.hides.slice(0, 3).join(", ")}` + (o.hides.length > 3 ? ` (+${o.hides.length - 3} more)` : "")
16504
+ );
16505
+ return `.verityignore: ${ig.securityOverlap.length} pattern(s) can hide security-sensitive files from review:
16506
+ ${lines.join("\n")}
16507
+ These are still excluded \u2014 this is a warning, not a refusal. Add a \`!\` rule to keep one in scope, e.g. \`!.env*\`.`;
16508
+ }
16509
+
16510
+ // src/commands/standard.ts
16274
16511
  function registerStandardCommands(program2) {
16275
16512
  const standard = program2.command("standard").description("Manage the project Standard");
16276
16513
  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) => {
@@ -16299,12 +16536,25 @@ function registerStandardCommands(program2) {
16299
16536
  printError(`Invalid YAML in ${opts.file}: ${err.message}`);
16300
16537
  process.exit(1);
16301
16538
  }
16539
+ let ignoreRaw = null;
16540
+ const ignorePath = projectPath(VERITYIGNORE_FILE);
16541
+ if ((0, import_node_fs20.existsSync)(ignorePath)) {
16542
+ try {
16543
+ ignoreRaw = (0, import_node_fs20.readFileSync)(ignorePath, "utf-8");
16544
+ const overlap = describeSecurityOverlap(parseVerityIgnore(ignoreRaw));
16545
+ if (overlap) printWarn(overlap);
16546
+ } catch {
16547
+ }
16548
+ }
16302
16549
  const result = await apiRequest({
16303
16550
  method: "POST",
16304
16551
  path: "/standards",
16305
16552
  serviceUrl: urlResult.data,
16306
16553
  token: tokenResult.data.token,
16307
- body: { content, created_by: opts.createdBy },
16554
+ body: {
16555
+ content: ignoreRaw === null ? content : { ...content, verityignore: ignoreRaw },
16556
+ created_by: opts.createdBy
16557
+ },
16308
16558
  verbose: globals.verbose
16309
16559
  });
16310
16560
  if (!result.ok) {
@@ -16381,6 +16631,15 @@ function registerStandardCommands(program2) {
16381
16631
  var import_promises10 = require("node:fs/promises");
16382
16632
  function registerConfigCommands(program2) {
16383
16633
  const config = program2.command("config").description("Manage analysis configuration");
16634
+ config.command("service-url").description("Print the resolved service URL (used by the shipped skills)").action(async () => {
16635
+ const globals = program2.opts();
16636
+ const urlResult = await resolveServiceUrl(globals.serviceUrl);
16637
+ if (!urlResult.ok) {
16638
+ printError(urlResult.error);
16639
+ process.exit(1);
16640
+ }
16641
+ process.stdout.write(urlResult.data + "\n");
16642
+ });
16384
16643
  config.command("push").description("Upload the analysis config to the service").option("--file <path>", "Path to config file", CODACY_CONFIG_FILE).action(async (opts) => {
16385
16644
  const globals = program2.opts();
16386
16645
  const tokenResult = await resolveToken(globals.token);
@@ -16522,99 +16781,402 @@ function formatRunDetail(run) {
16522
16781
  return lines;
16523
16782
  }
16524
16783
 
16525
- // src/commands/status.ts
16526
- function timeAgo(isoDate) {
16527
- const ms = Date.now() - new Date(isoDate).getTime();
16528
- const mins = Math.floor(ms / 6e4);
16529
- if (mins < 1) return "just now";
16530
- if (mins < 60) return `${mins}m ago`;
16531
- const hrs = Math.floor(mins / 60);
16532
- if (hrs < 24) return `${hrs}h ago`;
16533
- const days = Math.floor(hrs / 24);
16534
- return `${days}d ago`;
16784
+ // src/lib/ignore-declaration.ts
16785
+ var import_node_fs22 = require("node:fs");
16786
+
16787
+ // src/lib/debounce.ts
16788
+ var import_node_fs21 = require("node:fs");
16789
+ var import_node_crypto10 = require("node:crypto");
16790
+ function scopedFile(base, sessionId) {
16791
+ if (!sessionId) return base;
16792
+ return `${base}.${(0, import_node_crypto10.createHash)("sha1").update(sessionId).digest("hex").slice(0, 12)}`;
16535
16793
  }
16536
- function registerStatusCommand(program2) {
16537
- 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) => {
16538
- const globals = program2.opts();
16539
- const tokenResult = await resolveToken(globals.token);
16540
- if (!tokenResult.ok) {
16541
- printError(tokenResult.error);
16542
- process.exit(1);
16543
- }
16544
- const urlResult = await resolveServiceUrl(globals.serviceUrl);
16545
- if (!urlResult.ok) {
16546
- printError(urlResult.error);
16547
- process.exit(1);
16548
- }
16549
- const token = tokenResult.data.token;
16550
- const serviceUrl = urlResult.data;
16551
- const who = await whoami(token, serviceUrl, globals.verbose);
16552
- const memResult = await apiRequest({
16553
- method: "GET",
16554
- path: "/memory",
16555
- serviceUrl,
16556
- token,
16557
- verbose: globals.verbose
16558
- });
16559
- const denial = memResult.ok ? null : authDenialRemedy(memResult.error);
16560
- if (!memResult.ok && !denial) {
16561
- printError(memResult.error);
16562
- process.exit(1);
16794
+ function checkDebounce(debounceSeconds = DEBOUNCE_SECONDS, sessionId) {
16795
+ const file = scopedFile(DEBOUNCE_FILE, sessionId);
16796
+ if (!(0, import_node_fs21.existsSync)(file)) return null;
16797
+ try {
16798
+ const lastTs = parseInt((0, import_node_fs21.readFileSync)(file, "utf-8").trim(), 10);
16799
+ const nowTs = Math.floor(Date.now() / 1e3);
16800
+ const elapsed = nowTs - lastTs;
16801
+ if (elapsed < debounceSeconds) {
16802
+ return `Debounced \u2014 last analysis was ${elapsed}s ago`;
16563
16803
  }
16564
- const mem = memResult.ok ? memResult.data : null;
16565
- if (opts.json) {
16566
- const output = {
16567
- auth: who.ok ? who.data : { error: who.error },
16568
- memory: mem
16569
- };
16570
- if (denial && !memResult.ok) {
16571
- output.error = { code: denial.code, message: memResult.error, remedy: denial.remedy };
16572
- }
16573
- if (opts.history && !denial) {
16574
- const runsResult = await apiRequest({
16575
- method: "GET",
16576
- path: `/runs?limit=${opts.limit}`,
16577
- serviceUrl,
16578
- token,
16579
- verbose: globals.verbose
16580
- });
16581
- if (runsResult.ok) {
16582
- output.runs = runsResult.data.runs;
16583
- }
16804
+ } catch {
16805
+ }
16806
+ return null;
16807
+ }
16808
+ function checkMtime(files, bypassForRecentCommits, sessionId) {
16809
+ if (bypassForRecentCommits) return null;
16810
+ const file = scopedFile(DEBOUNCE_FILE, sessionId);
16811
+ if (!(0, import_node_fs21.existsSync)(file)) return null;
16812
+ let debounceTime;
16813
+ try {
16814
+ debounceTime = (0, import_node_fs21.statSync)(file).mtimeMs;
16815
+ } catch {
16816
+ return null;
16817
+ }
16818
+ for (const f of files) {
16819
+ const resolved = resolveFile(f);
16820
+ if (!resolved) continue;
16821
+ try {
16822
+ const stat3 = (0, import_node_fs21.statSync)(resolved);
16823
+ if (stat3.mtimeMs > debounceTime) {
16824
+ return null;
16584
16825
  }
16585
- printJson(output);
16586
- return;
16587
- }
16588
- if (mem?.configured === false) {
16589
- printInfo("Verity is not configured for this project. Run /verity-setup.");
16590
- return;
16591
- }
16592
- printInfo("=== Verity Status ===");
16593
- if (who.ok && who.data.logged_in) {
16594
- printInfo(`Account: Logged in as ${who.data.email ?? `user #${who.data.user_id}`} \u2713`);
16595
- } else if (who.ok && who.data.anonymous) {
16596
- printInfo('Account: Anonymous \u2014 runs not saved, no cloud memory. Run "verity login".');
16597
- } else if (tokenResult.data.userId != null) {
16598
- printInfo(`Account: Logged in as ${tokenResult.data.email ?? `user #${tokenResult.data.userId}`} (cached \u2014 could not reach the Verity service) \u2713`);
16599
- } else if (!who.ok) {
16600
- printInfo(`Account: Unknown \u2014 could not reach the Verity service (${who.error})`);
16826
+ } catch {
16827
+ continue;
16601
16828
  }
16602
- if (who.ok) {
16603
- const nudge = reverifyNudge(who.data);
16604
- if (nudge) printWarn(` ${nudge}`);
16829
+ }
16830
+ return "No files modified since last analysis";
16831
+ }
16832
+ function computeContentHash(files) {
16833
+ const hash = (0, import_node_crypto10.createHash)("sha1");
16834
+ const sorted = [...files].sort();
16835
+ for (const f of sorted) {
16836
+ const resolved = resolveFile(f) ?? f;
16837
+ try {
16838
+ if ((0, import_node_fs21.existsSync)(resolved)) {
16839
+ hash.update((0, import_node_fs21.readFileSync)(resolved));
16840
+ }
16841
+ } catch {
16605
16842
  }
16606
- if (denial) {
16607
- printWarn(`Access: ${denial.remedy}`);
16608
- printInfo(" Project status, history, and cloud memory stay unavailable until then.");
16843
+ }
16844
+ return hash.digest("hex");
16845
+ }
16846
+ function checkContentHash(files, sessionId) {
16847
+ const hash = computeContentHash(files);
16848
+ const file = scopedFile(HASH_FILE, sessionId);
16849
+ if ((0, import_node_fs21.existsSync)(file)) {
16850
+ try {
16851
+ const storedHash = (0, import_node_fs21.readFileSync)(file, "utf-8").trim();
16852
+ if (hash === storedHash) {
16853
+ return { skip: "No source changes since last analysis", hash };
16854
+ }
16855
+ } catch {
16609
16856
  }
16610
- if (mem?.project_name) printInfo(`Project: ${mem.project_name}`);
16611
- if (mem?.standard) {
16612
- const s = mem.standard;
16613
- printInfo(`Standard: v${s.version} (${s.quality_dimensions} quality, ${s.security_patterns} security, ${s.custom_patterns} custom)`);
16614
- printInfo(`Languages: ${s.languages.join(", ")}`);
16857
+ }
16858
+ return { skip: null, hash };
16859
+ }
16860
+ function recordAnalysisStart(sessionId) {
16861
+ (0, import_node_fs21.mkdirSync)(VERITY_DIR, { recursive: true });
16862
+ (0, import_node_fs21.writeFileSync)(scopedFile(DEBOUNCE_FILE, sessionId), String(Math.floor(Date.now() / 1e3)));
16863
+ }
16864
+ function recordPassHash(hash, sessionId) {
16865
+ (0, import_node_fs21.writeFileSync)(scopedFile(HASH_FILE, sessionId), hash);
16866
+ }
16867
+ function narrowToRecent(files, sessionId) {
16868
+ const file = scopedFile(DEBOUNCE_FILE, sessionId);
16869
+ if (!(0, import_node_fs21.existsSync)(file)) return files;
16870
+ let debounceTime;
16871
+ try {
16872
+ debounceTime = (0, import_node_fs21.statSync)(file).mtimeMs;
16873
+ } catch {
16874
+ return files;
16875
+ }
16876
+ const recent = files.filter((f) => {
16877
+ try {
16878
+ return (0, import_node_fs21.existsSync)(f) && (0, import_node_fs21.statSync)(f).mtimeMs > debounceTime;
16879
+ } catch {
16880
+ return false;
16615
16881
  }
16616
- const hookStatus = await checkAllVerityHooks();
16617
- const moments = [];
16882
+ });
16883
+ return recent.length > 0 ? recent : files;
16884
+ }
16885
+ function readIteration(currentCommit, _contentHash) {
16886
+ return Math.max(1, readBlockState(currentCommit).attempts);
16887
+ }
16888
+ var NO_BLOCKS = { attempts: 0, blocks: 0, fingerprint: null };
16889
+ function readBlockState(currentCommit, opts) {
16890
+ if (opts?.newUserPrompt) return NO_BLOCKS;
16891
+ if (!(0, import_node_fs21.existsSync)(ITERATION_FILE)) return NO_BLOCKS;
16892
+ try {
16893
+ const stored = (0, import_node_fs21.readFileSync)(ITERATION_FILE, "utf-8").trim();
16894
+ const parsed = stored.startsWith("{") ? parseJsonState(stored) : parseLegacyState(stored);
16895
+ if (!parsed) return NO_BLOCKS;
16896
+ if (parsed.commit !== currentCommit) return NO_BLOCKS;
16897
+ if (parsed.ts > 0 && Math.floor(Date.now() / 1e3) - parsed.ts > 600) return NO_BLOCKS;
16898
+ return { attempts: parsed.attempts, blocks: parsed.blocks, fingerprint: parsed.fingerprint };
16899
+ } catch {
16900
+ return NO_BLOCKS;
16901
+ }
16902
+ }
16903
+ function parseJsonState(raw) {
16904
+ const o = JSON.parse(raw);
16905
+ const attempts = typeof o.attempts === "number" ? o.attempts : NaN;
16906
+ if (isNaN(attempts)) return null;
16907
+ return {
16908
+ attempts,
16909
+ blocks: typeof o.blocks === "number" ? o.blocks : attempts,
16910
+ fingerprint: typeof o.fingerprint === "string" && o.fingerprint ? o.fingerprint : null,
16911
+ commit: typeof o.commit === "string" ? o.commit : "",
16912
+ ts: typeof o.ts === "number" ? o.ts : 0
16913
+ };
16914
+ }
16915
+ function parseLegacyState(raw) {
16916
+ const parts = raw.split(":");
16917
+ const n = parseInt(parts[0], 10);
16918
+ if (isNaN(n)) return null;
16919
+ return {
16920
+ attempts: n,
16921
+ // The old file has no separate block count; the old counter is the closest
16922
+ // honest answer, and it errs toward releasing sooner rather than later.
16923
+ blocks: n,
16924
+ fingerprint: parts.slice(3).join(":") || null,
16925
+ commit: parts[1] ?? "",
16926
+ ts: parseInt(parts[2] ?? "0", 10)
16927
+ };
16928
+ }
16929
+ function findingsFingerprint(findings) {
16930
+ const keys = findings.map((f) => `${String(f.pattern_id ?? "?")}|${String(f.file ?? "?")}`).filter((k) => k !== "?|?");
16931
+ return [...new Set(keys)].sort().join(",");
16932
+ }
16933
+ function isSameProblem(previous, current) {
16934
+ if (!previous || !current) return false;
16935
+ const prev = new Set(previous.split(","));
16936
+ return current.split(",").some((k) => prev.has(k));
16937
+ }
16938
+ function writeBlockState(commit, state) {
16939
+ (0, import_node_fs21.mkdirSync)(VERITY_DIR, { recursive: true });
16940
+ (0, import_node_fs21.writeFileSync)(
16941
+ ITERATION_FILE,
16942
+ JSON.stringify({
16943
+ v: 2,
16944
+ attempts: state.attempts,
16945
+ blocks: state.blocks,
16946
+ commit,
16947
+ ts: Math.floor(Date.now() / 1e3),
16948
+ fingerprint: state.fingerprint ?? void 0
16949
+ })
16950
+ );
16951
+ }
16952
+ function resetBlockState(commit) {
16953
+ writeBlockState(commit, { attempts: 0, blocks: 0, fingerprint: null });
16954
+ }
16955
+
16956
+ // src/lib/ignore-declaration.ts
16957
+ var IGNORE_BUDGET = 3;
16958
+ var MAX_WINDOW_SECONDS = 60 * 60;
16959
+ var TURN_FUSE_SECONDS = 10 * 60;
16960
+ function parseDuration(input) {
16961
+ const trimmed = input.trim().toLowerCase();
16962
+ const m = /^(\d+)(s|m|h)?$/.exec(trimmed);
16963
+ if (!m) {
16964
+ return { ok: false, error: `Could not read "${input}" as a duration. Use 30m, 45s, or 1h.` };
16965
+ }
16966
+ const n = parseInt(m[1], 10);
16967
+ if (!Number.isFinite(n) || n <= 0) {
16968
+ return { ok: false, error: `A duration must be a positive number of seconds, minutes or hours \u2014 got "${input}".` };
16969
+ }
16970
+ const unit = m[2] ?? "m";
16971
+ const seconds = unit === "s" ? n : unit === "h" ? n * 3600 : n * 60;
16972
+ if (seconds > MAX_WINDOW_SECONDS) {
16973
+ return {
16974
+ ok: false,
16975
+ 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.`
16976
+ };
16977
+ }
16978
+ return { ok: true, seconds };
16979
+ }
16980
+ function resolveActive(state, now) {
16981
+ if (!state?.active) return null;
16982
+ if (state.active.expires <= now) return null;
16983
+ return state.active;
16984
+ }
16985
+ function verifyDeclaration(input) {
16986
+ const d = input.declaration;
16987
+ if (!d) return { honoured: false, void: false };
16988
+ if (!input.authorshipIsObservable) {
16989
+ return {
16990
+ honoured: false,
16991
+ void: true,
16992
+ why: "the transcript window could not show what this turn did, so the declaration could not be checked"
16993
+ };
16994
+ }
16995
+ if (input.commandRecordTruncated) {
16996
+ return {
16997
+ honoured: false,
16998
+ void: true,
16999
+ why: "the record of commands run this turn was incomplete, so the declaration could not be checked"
17000
+ };
17001
+ }
17002
+ if (input.agentAuthoredFiles > 0) {
17003
+ return {
17004
+ honoured: false,
17005
+ void: true,
17006
+ why: `${input.agentAuthoredFiles} file${input.agentAuthoredFiles === 1 ? " was" : "s were"} authored during it`
17007
+ };
17008
+ }
17009
+ if (input.subagents > 0) {
17010
+ return {
17011
+ honoured: false,
17012
+ void: true,
17013
+ why: "work was delegated to a subagent during it, whose authorship this turn cannot account for"
17014
+ };
17015
+ }
17016
+ const authoring = [...input.agentCommands ?? [], ...input.userCommands ?? []].filter(
17017
+ (c) => !isNonAuthoringCommand(c)
17018
+ );
17019
+ if (authoring.length > 0) {
17020
+ return {
17021
+ honoured: false,
17022
+ void: true,
17023
+ why: `a command that can write files ran during it (${authoring[0]})`
17024
+ };
17025
+ }
17026
+ return { honoured: true };
17027
+ }
17028
+ function stateFile(sessionId) {
17029
+ return projectPath(scopedFile(IGNORE_DECLARATION_FILE, sessionId));
17030
+ }
17031
+ function ignoreStateKeys(token, sessionId) {
17032
+ const scoped = sessionScopeKey(token, sessionId);
17033
+ const userOnly = sessionScopeKey(token, void 0);
17034
+ return scoped === userOnly ? [scoped] : [scoped, userOnly];
17035
+ }
17036
+ function resolveIgnoreState(keys) {
17037
+ for (const key of keys) {
17038
+ const state = readIgnoreState(key);
17039
+ if (state) return { state, key };
17040
+ }
17041
+ return null;
17042
+ }
17043
+ function readIgnoreState(sessionId) {
17044
+ const file = stateFile(sessionId);
17045
+ if (!(0, import_node_fs22.existsSync)(file)) return null;
17046
+ try {
17047
+ const o = JSON.parse((0, import_node_fs22.readFileSync)(file, "utf-8")) ?? {};
17048
+ const spent = typeof o.spent === "number" ? o.spent : 0;
17049
+ const raw = o.active;
17050
+ let active = null;
17051
+ if (raw && typeof raw === "object") {
17052
+ const scope2 = raw.scope === "window" ? "window" : raw.scope === "turn" ? "turn" : null;
17053
+ const expires = typeof raw.expires === "number" ? raw.expires : null;
17054
+ if (scope2 && expires != null) {
17055
+ active = {
17056
+ scope: scope2,
17057
+ origin: raw.origin === "agent" ? "agent" : "user",
17058
+ reason: typeof raw.reason === "string" ? raw.reason : "",
17059
+ at: typeof raw.at === "number" ? raw.at : 0,
17060
+ expires
17061
+ };
17062
+ }
17063
+ }
17064
+ return { v: 1, active, spent };
17065
+ } catch {
17066
+ return null;
17067
+ }
17068
+ }
17069
+ function writeIgnoreState(state, sessionId) {
17070
+ try {
17071
+ (0, import_node_fs22.mkdirSync)(projectPath(VERITY_DIR), { recursive: true });
17072
+ (0, import_node_fs22.writeFileSync)(stateFile(sessionId), JSON.stringify({ v: 1, active: state.active, spent: state.spent }));
17073
+ } catch {
17074
+ }
17075
+ }
17076
+ function clearActiveDeclaration(sessionId) {
17077
+ const prev = readIgnoreState(sessionId);
17078
+ if (!prev) return;
17079
+ writeIgnoreState({ v: 1, active: null, spent: prev.spent }, sessionId);
17080
+ }
17081
+ function describeRemaining(d, now) {
17082
+ const left = Math.max(0, d.expires - now);
17083
+ if (left >= 90) return `${Math.round(left / 60)}m left`;
17084
+ return `${left}s left`;
17085
+ }
17086
+
17087
+ // src/commands/status.ts
17088
+ function timeAgo(isoDate) {
17089
+ const ms = Date.now() - new Date(isoDate).getTime();
17090
+ const mins = Math.floor(ms / 6e4);
17091
+ if (mins < 1) return "just now";
17092
+ if (mins < 60) return `${mins}m ago`;
17093
+ const hrs = Math.floor(mins / 60);
17094
+ if (hrs < 24) return `${hrs}h ago`;
17095
+ const days = Math.floor(hrs / 24);
17096
+ return `${days}d ago`;
17097
+ }
17098
+ function registerStatusCommand(program2) {
17099
+ 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) => {
17100
+ const globals = program2.opts();
17101
+ const tokenResult = await resolveToken(globals.token);
17102
+ if (!tokenResult.ok) {
17103
+ printError(tokenResult.error);
17104
+ process.exit(1);
17105
+ }
17106
+ const urlResult = await resolveServiceUrl(globals.serviceUrl);
17107
+ if (!urlResult.ok) {
17108
+ printError(urlResult.error);
17109
+ process.exit(1);
17110
+ }
17111
+ const token = tokenResult.data.token;
17112
+ const serviceUrl = urlResult.data;
17113
+ const who = await whoami(token, serviceUrl, globals.verbose);
17114
+ const memResult = await apiRequest({
17115
+ method: "GET",
17116
+ path: "/memory",
17117
+ serviceUrl,
17118
+ token,
17119
+ verbose: globals.verbose
17120
+ });
17121
+ const denial = memResult.ok ? null : authDenialRemedy(memResult.error);
17122
+ if (!memResult.ok && !denial) {
17123
+ printError(memResult.error);
17124
+ process.exit(1);
17125
+ }
17126
+ const mem = memResult.ok ? memResult.data : null;
17127
+ if (opts.json) {
17128
+ const output = {
17129
+ auth: who.ok ? who.data : { error: who.error },
17130
+ memory: mem
17131
+ };
17132
+ if (denial && !memResult.ok) {
17133
+ output.error = { code: denial.code, message: memResult.error, remedy: denial.remedy };
17134
+ }
17135
+ if (opts.history && !denial) {
17136
+ const runsResult = await apiRequest({
17137
+ method: "GET",
17138
+ path: `/runs?limit=${opts.limit}`,
17139
+ serviceUrl,
17140
+ token,
17141
+ verbose: globals.verbose
17142
+ });
17143
+ if (runsResult.ok) {
17144
+ output.runs = runsResult.data.runs;
17145
+ }
17146
+ }
17147
+ printJson(output);
17148
+ return;
17149
+ }
17150
+ if (mem?.configured === false) {
17151
+ printInfo("Verity is not configured for this project. Run /verity-setup.");
17152
+ return;
17153
+ }
17154
+ printInfo("=== Verity Status ===");
17155
+ if (who.ok && who.data.logged_in) {
17156
+ printInfo(`Account: Logged in as ${who.data.email ?? `user #${who.data.user_id}`} \u2713`);
17157
+ } else if (who.ok && who.data.anonymous) {
17158
+ printInfo('Account: Anonymous \u2014 runs not saved, no cloud memory. Run "verity login".');
17159
+ } else if (tokenResult.data.userId != null) {
17160
+ printInfo(`Account: Logged in as ${tokenResult.data.email ?? `user #${tokenResult.data.userId}`} (cached \u2014 could not reach the Verity service) \u2713`);
17161
+ } else if (!who.ok) {
17162
+ printInfo(`Account: Unknown \u2014 could not reach the Verity service (${who.error})`);
17163
+ }
17164
+ if (who.ok) {
17165
+ const nudge = reverifyNudge(who.data);
17166
+ if (nudge) printWarn(` ${nudge}`);
17167
+ }
17168
+ if (denial) {
17169
+ printWarn(`Access: ${denial.remedy}`);
17170
+ printInfo(" Project status, history, and cloud memory stay unavailable until then.");
17171
+ }
17172
+ if (mem?.project_name) printInfo(`Project: ${mem.project_name}`);
17173
+ if (mem?.standard) {
17174
+ const s = mem.standard;
17175
+ printInfo(`Standard: v${s.version} (${s.quality_dimensions} quality, ${s.security_patterns} security, ${s.custom_patterns} custom)`);
17176
+ printInfo(`Languages: ${s.languages.join(", ")}`);
17177
+ }
17178
+ const hookStatus = await checkAllVerityHooks();
17179
+ const moments = [];
16618
17180
  if (hookStatus.stop) moments.push("stop");
16619
17181
  if (hookStatus.guardOn.includes("commit")) moments.push("pre-commit");
16620
17182
  if (hookStatus.guardOn.includes("push")) moments.push("pre-push/PR");
@@ -16630,6 +17192,39 @@ function registerStatusCommand(program2) {
16630
17192
  printInfo(`Trend: ${r.trend}`);
16631
17193
  printInfo(`Runs: ${r.count} recorded`);
16632
17194
  }
17195
+ {
17196
+ const tokenForScope = tokenResult.data.token;
17197
+ const found = resolveIgnoreState(
17198
+ ignoreStateKeys(tokenForScope, process.env.CLAUDE_SESSION_ID || void 0)
17199
+ );
17200
+ const now = Math.floor(Date.now() / 1e3);
17201
+ const active = resolveActive(found?.state ?? null, now);
17202
+ const spent = found?.state.spent ?? 0;
17203
+ if (active) {
17204
+ printInfo("");
17205
+ printInfo("--- Ignore Declared ---");
17206
+ printInfo(`Scope: ${active.scope === "turn" ? "the next turn" : `a window, ${describeRemaining(active, now)}`}`);
17207
+ printInfo(`Reason: ${active.reason}`);
17208
+ printInfo(`Declared by: ${active.origin === "agent" ? "the agent" : "you"}`);
17209
+ printInfo(`Budget: ${spent}/${IGNORE_BUDGET} declarations this session`);
17210
+ printInfo('Turns that author anything are still reviewed \u2014 the declaration voids. "verity ignore clear" cancels it.');
17211
+ } else if (spent > 0) {
17212
+ printInfo("");
17213
+ printInfo(`Ignore budget: ${spent}/${IGNORE_BUDGET} declarations used this session.`);
17214
+ }
17215
+ }
17216
+ {
17217
+ const ig = loadVerityIgnore();
17218
+ if (ig.rules.length > 0 || ig.problems.length > 0) {
17219
+ printInfo("");
17220
+ printInfo("--- .verityignore ---");
17221
+ printInfo(`Rules: ${ig.rules.length}`);
17222
+ for (const problem of ig.problems) printWarn(` ${problem}`);
17223
+ const overlap = describeSecurityOverlap(ig);
17224
+ if (overlap) printWarn(overlap);
17225
+ printInfo("An edit to .verityignore suspends every rule for that turn \u2014 the edit is always reviewed.");
17226
+ }
17227
+ }
16633
17228
  if (mem.pending_items && mem.pending_items.length > 0) {
16634
17229
  printInfo("");
16635
17230
  printInfo("--- Pending Items ---");
@@ -16793,6 +17388,7 @@ function createRun(opts, globals) {
16793
17388
  allChanged: [],
16794
17389
  hasRecentCommitFiles: false,
16795
17390
  changedUniverse: [],
17391
+ verityIgnored: { rules: 0, kept: [], ignored: [], securityExcluded: [], suspended: false },
16796
17392
  analyzable: [],
16797
17393
  reviewable: [],
16798
17394
  securityFiles: [],
@@ -16826,6 +17422,7 @@ function createRun(opts, globals) {
16826
17422
  deletedNodePaths: [],
16827
17423
  editedUploads: [],
16828
17424
  autoSeedNotice: null,
17425
+ voidedIgnoreNotice: null,
16829
17426
  foldResult: null,
16830
17427
  foldConservation: null,
16831
17428
  memorySession: null,
@@ -16846,7 +17443,7 @@ function createRun(opts, globals) {
16846
17443
  }
16847
17444
 
16848
17445
  // src/lib/stderr-log.ts
16849
- var import_node_fs19 = require("node:fs");
17446
+ var import_node_fs23 = require("node:fs");
16850
17447
  var TOKEN_RE2 = /verity_[0-9a-f]{16,}/g;
16851
17448
  var ANSI_RE = /\u001b\[[0-?]*[ -/]*[@-~]/g;
16852
17449
  function scrub(s) {
@@ -16859,9 +17456,9 @@ function append(text) {
16859
17456
  try {
16860
17457
  const dir = projectPath(DEBUG_LOG_DIR);
16861
17458
  const file = projectPath(STDERR_LOG_FILE);
16862
- (0, import_node_fs19.mkdirSync)(dir, { recursive: true });
17459
+ (0, import_node_fs23.mkdirSync)(dir, { recursive: true });
16863
17460
  rotateIfNeeded(file);
16864
- (0, import_node_fs19.appendFileSync)(file, text);
17461
+ (0, import_node_fs23.appendFileSync)(file, text);
16865
17462
  } catch {
16866
17463
  }
16867
17464
  }
@@ -17019,7 +17616,7 @@ function installRunEvidence(run) {
17019
17616
 
17020
17617
  // src/lib/git-frame.ts
17021
17618
  var import_node_child_process7 = require("node:child_process");
17022
- var import_node_fs20 = require("node:fs");
17619
+ var import_node_fs24 = require("node:fs");
17023
17620
  var import_node_os3 = require("node:os");
17024
17621
  var import_node_path18 = require("node:path");
17025
17622
  var import_node_path19 = require("node:path");
@@ -17158,14 +17755,14 @@ function gitAt(dir, args) {
17158
17755
  }
17159
17756
  function realpathOr(p) {
17160
17757
  try {
17161
- return import_node_fs20.realpathSync.native(p);
17758
+ return import_node_fs24.realpathSync.native(p);
17162
17759
  } catch {
17163
17760
  return (0, import_node_path18.resolve)(p);
17164
17761
  }
17165
17762
  }
17166
17763
  function resolveFrame(input) {
17167
17764
  const found = findMomentSegment(input.command, input.on);
17168
- const hookDirUsable = !!input.hookCwd && (0, import_node_fs20.existsSync)(input.hookCwd);
17765
+ const hookDirUsable = !!input.hookCwd && (0, import_node_fs24.existsSync)(input.hookCwd);
17169
17766
  const baseDir = hookDirUsable ? input.hookCwd : process.cwd();
17170
17767
  let anchor = hookDirUsable ? "hook-cwd" : "process-cwd";
17171
17768
  const refuse = (refusal) => ({
@@ -17188,7 +17785,7 @@ function resolveFrame(input) {
17188
17785
  if (dirs.size > 1) return refuse(`target:multiple ${found.moment} targets in one command`);
17189
17786
  const targetDir = dirs.size === 1 ? [...dirs][0] : baseDir;
17190
17787
  if (targetDir !== baseDir) {
17191
- if (!(0, import_node_fs20.existsSync)(targetDir)) return refuse(`target:directory does not exist: ${targetDir}`);
17788
+ if (!(0, import_node_fs24.existsSync)(targetDir)) return refuse(`target:directory does not exist: ${targetDir}`);
17192
17789
  dir = targetDir;
17193
17790
  }
17194
17791
  }
@@ -17227,7 +17824,7 @@ var SHA_RE2 = /^[0-9a-f]{40}$/;
17227
17824
  function baselineShaAt(frame) {
17228
17825
  if (!frame.worktreeRoot) return null;
17229
17826
  try {
17230
- const sha = (0, import_node_fs20.readFileSync)((0, import_node_path19.join)(frame.worktreeRoot, BASELINE_SHA_FILE), "utf-8").trim();
17827
+ const sha = (0, import_node_fs24.readFileSync)((0, import_node_path19.join)(frame.worktreeRoot, BASELINE_SHA_FILE), "utf-8").trim();
17231
17828
  if (!SHA_RE2.test(sha)) return null;
17232
17829
  return refResolves(frame, sha) ? sha : null;
17233
17830
  } catch {
@@ -17340,7 +17937,7 @@ function truthy(v) {
17340
17937
  }
17341
17938
 
17342
17939
  // src/lib/transcript.ts
17343
- var import_node_fs21 = require("node:fs");
17940
+ var import_node_fs25 = require("node:fs");
17344
17941
  var MAX_READ_BYTES = 256 * 1024;
17345
17942
  var SMALL_FILE_BYTES = 64 * 1024;
17346
17943
  var MAX_FILES_LIST = 20;
@@ -17366,7 +17963,7 @@ async function extractActionSummary(transcriptPath) {
17366
17963
  function readTurnLines(transcriptPath) {
17367
17964
  let size;
17368
17965
  try {
17369
- size = (0, import_node_fs21.statSync)(transcriptPath).size;
17966
+ size = (0, import_node_fs25.statSync)(transcriptPath).size;
17370
17967
  } catch {
17371
17968
  return null;
17372
17969
  }
@@ -17374,7 +17971,7 @@ function readTurnLines(transcriptPath) {
17374
17971
  let raw;
17375
17972
  let windowed = false;
17376
17973
  if (size <= SMALL_FILE_BYTES) {
17377
- raw = (0, import_node_fs21.readFileSync)(transcriptPath, "utf-8");
17974
+ raw = (0, import_node_fs25.readFileSync)(transcriptPath, "utf-8");
17378
17975
  } else {
17379
17976
  windowed = true;
17380
17977
  const buf = Buffer.alloc(Math.min(MAX_READ_BYTES, size));
@@ -17432,6 +18029,7 @@ function buildSummary(lines) {
17432
18029
  const commands = [];
17433
18030
  const userCommands = [];
17434
18031
  let userCommandsTruncated = false;
18032
+ let commandsTruncated = false;
17435
18033
  let searches = 0;
17436
18034
  let subagents = 0;
17437
18035
  let webFetches = 0;
@@ -17472,7 +18070,10 @@ function buildSummary(lines) {
17472
18070
  totalToolCalls++;
17473
18071
  const toolName = block.name ?? "unknown";
17474
18072
  toolCounts[toolName] = (toolCounts[toolName] ?? 0) + 1;
17475
- if (totalToolCalls > MAX_TOOL_BLOCKS) continue;
18073
+ if (totalToolCalls > MAX_TOOL_BLOCKS) {
18074
+ if (toolName === "Bash") commandsTruncated = true;
18075
+ continue;
18076
+ }
17476
18077
  const input = block.input ?? {};
17477
18078
  switch (toolName) {
17478
18079
  case "Read":
@@ -17492,9 +18093,11 @@ function buildSummary(lines) {
17492
18093
  addPath(filesEdited, input.file_path);
17493
18094
  break;
17494
18095
  case "Bash": {
17495
- const cmd = sanitizeCommand(input.command);
17496
- if (cmd && commands.length < MAX_COMMANDS) {
17497
- commands.push(cmd);
18096
+ const { cmd, lost } = sanitizeCommandWithLoss(input.command);
18097
+ if (lost) commandsTruncated = true;
18098
+ if (cmd) {
18099
+ if (commands.length < MAX_COMMANDS) commands.push(cmd);
18100
+ else commandsTruncated = true;
17498
18101
  }
17499
18102
  break;
17500
18103
  }
@@ -17537,6 +18140,7 @@ function buildSummary(lines) {
17537
18140
  ],
17538
18141
  searches,
17539
18142
  commands,
18143
+ ...commandsTruncated ? { commands_truncated: true } : {},
17540
18144
  user_commands: userCommands,
17541
18145
  ...userCommandsTruncated ? { user_commands_truncated: true } : {},
17542
18146
  subagents,
@@ -17547,6 +18151,7 @@ function buildSummary(lines) {
17547
18151
  };
17548
18152
  if (JSON.stringify(summary).length > MAX_SUMMARY_BYTES) {
17549
18153
  summary.commands = [];
18154
+ summary.commands_truncated = true;
17550
18155
  summary.user_commands = [];
17551
18156
  summary.user_commands_truncated = true;
17552
18157
  if (JSON.stringify(summary).length > MAX_SUMMARY_BYTES) {
@@ -17582,12 +18187,22 @@ function userTypedCommands(entry) {
17582
18187
  }
17583
18188
  return { commands, truncated };
17584
18189
  }
18190
+ function sanitizeCommandWithLoss(rawCmd) {
18191
+ if (typeof rawCmd !== "string" || !rawCmd) return { cmd: null, lost: false };
18192
+ const lines = rawCmd.split("\n");
18193
+ const lost = lines.slice(1).some((l) => l.trim().length > 0);
18194
+ const first = lines[0];
18195
+ const cmd = sanitizeCommand(first);
18196
+ const hadSeparator = SEPARATORS.some((sep2) => first.indexOf(sep2) > 0);
18197
+ return { cmd, lost: lost || !hadSeparator && first.length > MAX_COMMAND_CHARS };
18198
+ }
18199
+ var SEPARATORS = [" | ", " > ", " >> ", " 2>", " && ", " ; "];
17585
18200
  function sanitizeCommand(rawCmd) {
17586
18201
  if (typeof rawCmd !== "string" || !rawCmd) return null;
17587
18202
  let cmd = rawCmd.split("\n")[0];
17588
18203
  let cut = -1;
17589
18204
  let marker = "";
17590
- for (const sep2 of [" | ", " > ", " >> ", " 2>", " && ", " ; "]) {
18205
+ for (const sep2 of SEPARATORS) {
17591
18206
  const idx = cmd.indexOf(sep2);
17592
18207
  if (idx > 0 && (cut === -1 || idx < cut)) {
17593
18208
  cut = idx;
@@ -17837,7 +18452,7 @@ function channelSilence(input) {
17837
18452
  // src/lib/cli-version.ts
17838
18453
  function cliVersion() {
17839
18454
  try {
17840
- return true ? "0.30.1" : "dev";
18455
+ return true ? "0.31.0-experimental.6bc3b1b" : "dev";
17841
18456
  } catch {
17842
18457
  return "dev";
17843
18458
  }
@@ -17878,7 +18493,7 @@ async function sendSkipBeacon(ctx, reason) {
17878
18493
 
17879
18494
  // src/lib/static-analysis.ts
17880
18495
  var import_node_child_process8 = require("node:child_process");
17881
- var import_node_fs22 = require("node:fs");
18496
+ var import_node_fs26 = require("node:fs");
17882
18497
  var SEVERITY_ORDER = {
17883
18498
  Error: 0,
17884
18499
  Critical: 0,
@@ -17926,7 +18541,7 @@ function runCodacyAnalysis(files) {
17926
18541
  if (files.length === 0) return empty;
17927
18542
  const existingFiles = files.filter((f) => {
17928
18543
  try {
17929
- return (0, import_node_fs22.existsSync)(f);
18544
+ return (0, import_node_fs26.existsSync)(f);
17930
18545
  } catch {
17931
18546
  return false;
17932
18547
  }
@@ -18124,6 +18739,7 @@ async function passAndExit(run, reason, skip, kindOverride) {
18124
18739
  "bare-acknowledgment",
18125
18740
  "reflection-prompt",
18126
18741
  "command-only-turn",
18742
+ "declared-ignore",
18127
18743
  "skip-mode",
18128
18744
  "zero-increment",
18129
18745
  "debounce",
@@ -18163,19 +18779,37 @@ async function scope(run) {
18163
18779
  const { files: allChanged, hasRecentCommitFiles } = getChangedFiles();
18164
18780
  run.changedUniverse = allChanged;
18165
18781
  const { kept: external } = partitionVerityOwned(allChanged);
18166
- const analyzable = filterAnalyzable(external);
18167
- const reviewable = filterReviewable(external);
18168
- const securityFiles = filterSecurity(external);
18782
+ const verityIgnore = loadVerityIgnore();
18783
+ const ignored = partitionIgnored(external, verityIgnore);
18784
+ for (const problem of verityIgnore.problems) {
18785
+ process.stderr.write(`Verity: .verityignore ${problem}
18786
+ `);
18787
+ }
18788
+ if (ignored.securityExcluded.length > 0) {
18789
+ process.stderr.write(
18790
+ `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.
18791
+ `
18792
+ );
18793
+ }
18794
+ if (ignored.suspended) {
18795
+ process.stderr.write(
18796
+ "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"
18797
+ );
18798
+ }
18799
+ const inScope = ignored.kept;
18800
+ const analyzable = filterAnalyzable(inScope);
18801
+ const reviewable = filterReviewable(inScope);
18802
+ const securityFiles = filterSecurity(inScope);
18169
18803
  const noFilesChanged = analyzable.length === 0 && reviewable.length === 0 && securityFiles.length === 0;
18170
18804
  if (noFilesChanged && !assistantResponse) {
18171
18805
  await passAndExit(run, "No analyzable files changed", "no-analyzable-files");
18172
18806
  }
18173
18807
  const allForReview = Array.from(/* @__PURE__ */ new Set([...analyzable, ...reviewable]));
18174
- Object.assign(run, { allChanged, allForReview, analyzable, hasRecentCommitFiles, noFilesChanged, reviewable, securityFiles });
18808
+ Object.assign(run, { allChanged, allForReview, analyzable, hasRecentCommitFiles, noFilesChanged, reviewable, securityFiles, verityIgnored: ignored });
18175
18809
  }
18176
18810
 
18177
18811
  // src/lib/specs.ts
18178
- var import_node_fs23 = require("node:fs");
18812
+ var import_node_fs27 = require("node:fs");
18179
18813
  var import_node_path20 = require("node:path");
18180
18814
  var SPEC_CANDIDATES = [
18181
18815
  "CLAUDE.md",
@@ -18207,16 +18841,16 @@ function discoverSpecs(consulted = []) {
18207
18841
  const totalCap = relevant ? MAX_TOTAL_SPEC_BYTES : UNCONSULTED_TOTAL_BYTES;
18208
18842
  if (totalBytes >= totalCap) return false;
18209
18843
  if (seen.has(specPath)) return true;
18210
- if (!(0, import_node_fs23.existsSync)(specPath)) return true;
18844
+ if (!(0, import_node_fs27.existsSync)(specPath)) return true;
18211
18845
  seen.add(specPath);
18212
18846
  const remaining = totalCap - totalBytes;
18213
18847
  const fileCap = relevant ? MAX_SPEC_FILE_BYTES : UNCONSULTED_FILE_BYTES;
18214
18848
  const readBytes = Math.min(fileCap, remaining);
18215
18849
  try {
18216
18850
  const buf = Buffer.alloc(readBytes);
18217
- const fd = (0, import_node_fs23.openSync)(specPath, "r");
18218
- const bytesRead = (0, import_node_fs23.readSync)(fd, buf, 0, readBytes, 0);
18219
- (0, import_node_fs23.closeSync)(fd);
18851
+ const fd = (0, import_node_fs27.openSync)(specPath, "r");
18852
+ const bytesRead = (0, import_node_fs27.readSync)(fd, buf, 0, readBytes, 0);
18853
+ (0, import_node_fs27.closeSync)(fd);
18220
18854
  const content = buf.slice(0, bytesRead).toString("utf-8");
18221
18855
  if (!content) return true;
18222
18856
  result.push({ path: specPath, content });
@@ -18232,7 +18866,7 @@ function discoverSpecs(consulted = []) {
18232
18866
  if (!addSpec(candidate)) break;
18233
18867
  }
18234
18868
  for (const dir of ["spec", "docs"]) {
18235
- if (!(0, import_node_fs23.existsSync)(dir)) continue;
18869
+ if (!(0, import_node_fs27.existsSync)(dir)) continue;
18236
18870
  try {
18237
18871
  const mdFiles = findMdFiles(dir, 2).sort();
18238
18872
  for (const mdFile of mdFiles) {
@@ -18247,7 +18881,7 @@ function findMdFiles(dir, maxDepth, depth = 0) {
18247
18881
  if (depth >= maxDepth) return [];
18248
18882
  const result = [];
18249
18883
  try {
18250
- const entries = (0, import_node_fs23.readdirSync)(dir, { withFileTypes: true });
18884
+ const entries = (0, import_node_fs27.readdirSync)(dir, { withFileTypes: true });
18251
18885
  for (const entry of entries) {
18252
18886
  const fullPath = (0, import_node_path20.join)(dir, entry.name);
18253
18887
  if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -18266,14 +18900,14 @@ function discoverPlans() {
18266
18900
  const candidates = [];
18267
18901
  const seen = /* @__PURE__ */ new Set();
18268
18902
  for (const plansDir of [localPlansDir, homePlansDir]) {
18269
- if (!(0, import_node_fs23.existsSync)(plansDir)) continue;
18903
+ if (!(0, import_node_fs27.existsSync)(plansDir)) continue;
18270
18904
  try {
18271
- for (const f of (0, import_node_fs23.readdirSync)(plansDir)) {
18905
+ for (const f of (0, import_node_fs27.readdirSync)(plansDir)) {
18272
18906
  if (!f.endsWith(".md") || seen.has(f)) continue;
18273
18907
  seen.add(f);
18274
18908
  const fullPath = (0, import_node_path20.join)(plansDir, f);
18275
18909
  try {
18276
- const stat3 = (0, import_node_fs23.statSync)(fullPath);
18910
+ const stat3 = (0, import_node_fs27.statSync)(fullPath);
18277
18911
  candidates.push({ name: f, path: fullPath, mtime: stat3.mtimeMs, size: stat3.size });
18278
18912
  } catch {
18279
18913
  }
@@ -18286,13 +18920,35 @@ function discoverPlans() {
18286
18920
  for (const entry of candidates.slice(0, MAX_PLAN_FILES)) {
18287
18921
  if (entry.size > MAX_PLAN_FILE_BYTES) continue;
18288
18922
  try {
18289
- const content = (0, import_node_fs23.readFileSync)(entry.path, "utf-8");
18923
+ const content = (0, import_node_fs27.readFileSync)(entry.path, "utf-8");
18290
18924
  result.push({ name: entry.name, content });
18291
18925
  } catch {
18292
18926
  }
18293
18927
  }
18294
18928
  return result;
18295
18929
  }
18930
+ var GUARD_DOC_EXT = /\.(md|mdx|txt|rst|adoc)$/i;
18931
+ function discoverGuardDocs(rangeFiles2) {
18932
+ const result = [];
18933
+ let totalBytes = 0;
18934
+ for (const path of rangeFiles2) {
18935
+ if (result.length >= MAX_SPEC_FILES) break;
18936
+ if (!GUARD_DOC_EXT.test(path)) continue;
18937
+ if (path.startsWith("/") || path.includes("..")) continue;
18938
+ if (!(0, import_node_fs27.existsSync)(path)) continue;
18939
+ try {
18940
+ const stat3 = (0, import_node_fs27.statSync)(path);
18941
+ if (stat3.size > MAX_PLAN_FILE_BYTES) continue;
18942
+ if (totalBytes + stat3.size > MAX_TOTAL_SPEC_BYTES) continue;
18943
+ const content = (0, import_node_fs27.readFileSync)(path, "utf-8");
18944
+ if (!content) continue;
18945
+ result.push({ name: path, content });
18946
+ totalBytes += content.length;
18947
+ } catch {
18948
+ }
18949
+ }
18950
+ return result;
18951
+ }
18296
18952
 
18297
18953
  // src/commands/analyze/phases/03-intent-inputs.ts
18298
18954
  async function intentInputs(run) {
@@ -18306,6 +18962,50 @@ async function intentInputs(run) {
18306
18962
  })) {
18307
18963
  await passAndExit(run, "User command only \u2014 skipping analysis", "command-only-turn");
18308
18964
  }
18965
+ {
18966
+ const ignoreKeys = ignoreStateKeys(
18967
+ run.tokenResult.ok ? run.tokenResult.data.token : void 0,
18968
+ null
18969
+ );
18970
+ const found = resolveIgnoreState([baselineSessionId, ...ignoreKeys]);
18971
+ const declaration = resolveActive(found?.state ?? null, Math.floor(Date.now() / 1e3));
18972
+ if (declaration) {
18973
+ const commandRecordTruncated = actionSummary?.commands_truncated === true || actionSummary?.user_commands_truncated === true || // The derived signal, and the only one an older client's summary can
18974
+ // give: `tool_counts` keeps counting Bash calls past every cap that
18975
+ // stops the list growing, so a mismatch IS a loss. See
18976
+ // `ActionSummary.commands_truncated`.
18977
+ (actionSummary?.tool_counts?.Bash ?? 0) > (actionSummary?.commands?.length ?? 0);
18978
+ const outcome = verifyDeclaration({
18979
+ declaration,
18980
+ agentAuthoredFiles: (actionSummary?.files_edited.length ?? 0) + (actionSummary?.files_created.length ?? 0),
18981
+ subagents: actionSummary?.subagents ?? 0,
18982
+ agentCommands: actionSummary?.commands,
18983
+ userCommands: actionSummary?.user_commands,
18984
+ commandRecordTruncated,
18985
+ authorshipIsObservable: !!actionSummary && actionSummary.transcript_windowed !== "orphaned"
18986
+ });
18987
+ if (outcome.honoured) {
18988
+ if (declaration.scope === "turn" && found) clearActiveDeclaration(found.key);
18989
+ logEvent("ignore_honoured", { scope: declaration.scope, origin: declaration.origin });
18990
+ await passAndExit(
18991
+ run,
18992
+ `skipping this turn \u2014 declared housekeeping ("${declaration.reason}")`,
18993
+ "declared-ignore"
18994
+ );
18995
+ } else if (outcome.void) {
18996
+ if (found) clearActiveDeclaration(found.key);
18997
+ logEvent("ignore_voided", {
18998
+ scope: declaration.scope,
18999
+ origin: declaration.origin,
19000
+ why: outcome.why
19001
+ });
19002
+ const notice = `Verity: the ignore declared for this window ("${declaration.reason}") was voided \u2014 ${outcome.why}. Reviewing normally.`;
19003
+ process.stderr.write(`${notice}
19004
+ `);
19005
+ run.voidedIgnoreNotice = notice;
19006
+ }
19007
+ }
19008
+ }
18309
19009
  const conversation = await readAndClearConversationBuffer(baselineSessionId);
18310
19010
  const specs = discoverSpecs(actionSummary?.files_read ?? []);
18311
19011
  const plans = discoverPlans();
@@ -18419,177 +19119,8 @@ async function mode(run) {
18419
19119
  Object.assign(run, { analysisMode, contextFilePaths, sessionAuthoredCode, sessionIdForMemory });
18420
19120
  }
18421
19121
 
18422
- // src/lib/debounce.ts
18423
- var import_node_fs24 = require("node:fs");
18424
- var import_node_crypto10 = require("node:crypto");
18425
- function scopedFile(base, sessionId) {
18426
- if (!sessionId) return base;
18427
- return `${base}.${(0, import_node_crypto10.createHash)("sha1").update(sessionId).digest("hex").slice(0, 12)}`;
18428
- }
18429
- function checkDebounce(debounceSeconds = DEBOUNCE_SECONDS, sessionId) {
18430
- const file = scopedFile(DEBOUNCE_FILE, sessionId);
18431
- if (!(0, import_node_fs24.existsSync)(file)) return null;
18432
- try {
18433
- const lastTs = parseInt((0, import_node_fs24.readFileSync)(file, "utf-8").trim(), 10);
18434
- const nowTs = Math.floor(Date.now() / 1e3);
18435
- const elapsed = nowTs - lastTs;
18436
- if (elapsed < debounceSeconds) {
18437
- return `Debounced \u2014 last analysis was ${elapsed}s ago`;
18438
- }
18439
- } catch {
18440
- }
18441
- return null;
18442
- }
18443
- function checkMtime(files, bypassForRecentCommits, sessionId) {
18444
- if (bypassForRecentCommits) return null;
18445
- const file = scopedFile(DEBOUNCE_FILE, sessionId);
18446
- if (!(0, import_node_fs24.existsSync)(file)) return null;
18447
- let debounceTime;
18448
- try {
18449
- debounceTime = (0, import_node_fs24.statSync)(file).mtimeMs;
18450
- } catch {
18451
- return null;
18452
- }
18453
- for (const f of files) {
18454
- const resolved = resolveFile(f);
18455
- if (!resolved) continue;
18456
- try {
18457
- const stat3 = (0, import_node_fs24.statSync)(resolved);
18458
- if (stat3.mtimeMs > debounceTime) {
18459
- return null;
18460
- }
18461
- } catch {
18462
- continue;
18463
- }
18464
- }
18465
- return "No files modified since last analysis";
18466
- }
18467
- function computeContentHash(files) {
18468
- const hash = (0, import_node_crypto10.createHash)("sha1");
18469
- const sorted = [...files].sort();
18470
- for (const f of sorted) {
18471
- const resolved = resolveFile(f) ?? f;
18472
- try {
18473
- if ((0, import_node_fs24.existsSync)(resolved)) {
18474
- hash.update((0, import_node_fs24.readFileSync)(resolved));
18475
- }
18476
- } catch {
18477
- }
18478
- }
18479
- return hash.digest("hex");
18480
- }
18481
- function checkContentHash(files, sessionId) {
18482
- const hash = computeContentHash(files);
18483
- const file = scopedFile(HASH_FILE, sessionId);
18484
- if ((0, import_node_fs24.existsSync)(file)) {
18485
- try {
18486
- const storedHash = (0, import_node_fs24.readFileSync)(file, "utf-8").trim();
18487
- if (hash === storedHash) {
18488
- return { skip: "No source changes since last analysis", hash };
18489
- }
18490
- } catch {
18491
- }
18492
- }
18493
- return { skip: null, hash };
18494
- }
18495
- function recordAnalysisStart(sessionId) {
18496
- (0, import_node_fs24.mkdirSync)(VERITY_DIR, { recursive: true });
18497
- (0, import_node_fs24.writeFileSync)(scopedFile(DEBOUNCE_FILE, sessionId), String(Math.floor(Date.now() / 1e3)));
18498
- }
18499
- function recordPassHash(hash, sessionId) {
18500
- (0, import_node_fs24.writeFileSync)(scopedFile(HASH_FILE, sessionId), hash);
18501
- }
18502
- function narrowToRecent(files, sessionId) {
18503
- const file = scopedFile(DEBOUNCE_FILE, sessionId);
18504
- if (!(0, import_node_fs24.existsSync)(file)) return files;
18505
- let debounceTime;
18506
- try {
18507
- debounceTime = (0, import_node_fs24.statSync)(file).mtimeMs;
18508
- } catch {
18509
- return files;
18510
- }
18511
- const recent = files.filter((f) => {
18512
- try {
18513
- return (0, import_node_fs24.existsSync)(f) && (0, import_node_fs24.statSync)(f).mtimeMs > debounceTime;
18514
- } catch {
18515
- return false;
18516
- }
18517
- });
18518
- return recent.length > 0 ? recent : files;
18519
- }
18520
- function readIteration(currentCommit, _contentHash) {
18521
- return Math.max(1, readBlockState(currentCommit).attempts);
18522
- }
18523
- var NO_BLOCKS = { attempts: 0, blocks: 0, fingerprint: null };
18524
- function readBlockState(currentCommit, opts) {
18525
- if (opts?.newUserPrompt) return NO_BLOCKS;
18526
- if (!(0, import_node_fs24.existsSync)(ITERATION_FILE)) return NO_BLOCKS;
18527
- try {
18528
- const stored = (0, import_node_fs24.readFileSync)(ITERATION_FILE, "utf-8").trim();
18529
- const parsed = stored.startsWith("{") ? parseJsonState(stored) : parseLegacyState(stored);
18530
- if (!parsed) return NO_BLOCKS;
18531
- if (parsed.commit !== currentCommit) return NO_BLOCKS;
18532
- if (parsed.ts > 0 && Math.floor(Date.now() / 1e3) - parsed.ts > 600) return NO_BLOCKS;
18533
- return { attempts: parsed.attempts, blocks: parsed.blocks, fingerprint: parsed.fingerprint };
18534
- } catch {
18535
- return NO_BLOCKS;
18536
- }
18537
- }
18538
- function parseJsonState(raw) {
18539
- const o = JSON.parse(raw);
18540
- const attempts = typeof o.attempts === "number" ? o.attempts : NaN;
18541
- if (isNaN(attempts)) return null;
18542
- return {
18543
- attempts,
18544
- blocks: typeof o.blocks === "number" ? o.blocks : attempts,
18545
- fingerprint: typeof o.fingerprint === "string" && o.fingerprint ? o.fingerprint : null,
18546
- commit: typeof o.commit === "string" ? o.commit : "",
18547
- ts: typeof o.ts === "number" ? o.ts : 0
18548
- };
18549
- }
18550
- function parseLegacyState(raw) {
18551
- const parts = raw.split(":");
18552
- const n = parseInt(parts[0], 10);
18553
- if (isNaN(n)) return null;
18554
- return {
18555
- attempts: n,
18556
- // The old file has no separate block count; the old counter is the closest
18557
- // honest answer, and it errs toward releasing sooner rather than later.
18558
- blocks: n,
18559
- fingerprint: parts.slice(3).join(":") || null,
18560
- commit: parts[1] ?? "",
18561
- ts: parseInt(parts[2] ?? "0", 10)
18562
- };
18563
- }
18564
- function findingsFingerprint(findings) {
18565
- const keys = findings.map((f) => `${String(f.pattern_id ?? "?")}|${String(f.file ?? "?")}`).filter((k) => k !== "?|?");
18566
- return [...new Set(keys)].sort().join(",");
18567
- }
18568
- function isSameProblem(previous, current) {
18569
- if (!previous || !current) return false;
18570
- const prev = new Set(previous.split(","));
18571
- return current.split(",").some((k) => prev.has(k));
18572
- }
18573
- function writeBlockState(commit, state) {
18574
- (0, import_node_fs24.mkdirSync)(VERITY_DIR, { recursive: true });
18575
- (0, import_node_fs24.writeFileSync)(
18576
- ITERATION_FILE,
18577
- JSON.stringify({
18578
- v: 2,
18579
- attempts: state.attempts,
18580
- blocks: state.blocks,
18581
- commit,
18582
- ts: Math.floor(Date.now() / 1e3),
18583
- fingerprint: state.fingerprint ?? void 0
18584
- })
18585
- );
18586
- }
18587
- function resetBlockState(commit) {
18588
- writeBlockState(commit, { attempts: 0, blocks: 0, fingerprint: null });
18589
- }
18590
-
18591
19122
  // src/lib/fold.ts
18592
- var import_node_fs25 = require("node:fs");
19123
+ var import_node_fs28 = require("node:fs");
18593
19124
  var import_node_path21 = require("node:path");
18594
19125
  var KNOWN_RECORD_TYPES = /* @__PURE__ */ new Set([
18595
19126
  "user",
@@ -18727,7 +19258,7 @@ function candidateRoots(repoRoot2) {
18727
19258
  const norm = repoRoot2.replace(/\\/g, "/").replace(/\/+$/, "");
18728
19259
  const out = [norm];
18729
19260
  try {
18730
- const real = import_node_fs25.realpathSync.native(norm).replace(/\\/g, "/").replace(/\/+$/, "");
19261
+ const real = import_node_fs28.realpathSync.native(norm).replace(/\\/g, "/").replace(/\/+$/, "");
18731
19262
  if (real !== norm) out.push(real);
18732
19263
  } catch {
18733
19264
  }
@@ -18815,8 +19346,8 @@ function fold(transcriptPath, opts = {}) {
18815
19346
  }
18816
19347
  };
18817
19348
  try {
18818
- if (!(0, import_node_fs25.existsSync)(transcriptPath)) return result;
18819
- ingest((0, import_node_fs25.readFileSync)(transcriptPath, "utf8"), "agent");
19349
+ if (!(0, import_node_fs28.existsSync)(transcriptPath)) return result;
19350
+ ingest((0, import_node_fs28.readFileSync)(transcriptPath, "utf8"), "agent");
18820
19351
  result.coverage.complete = true;
18821
19352
  } catch {
18822
19353
  return result;
@@ -18827,19 +19358,19 @@ function fold(transcriptPath, opts = {}) {
18827
19358
  (0, import_node_path21.basename)(transcriptPath).replace(/\.jsonl$/, ""),
18828
19359
  "subagents"
18829
19360
  );
18830
- if ((0, import_node_fs25.existsSync)(sidecarDir)) {
19361
+ if ((0, import_node_fs28.existsSync)(sidecarDir)) {
18831
19362
  const maxFiles = opts.maxSidecars ?? 200;
18832
19363
  const maxBytes = opts.maxSidecarBytes ?? 16 * 1024 * 1024;
18833
19364
  const found = [];
18834
19365
  const walk = (d, depth) => {
18835
19366
  if (depth > 4) return;
18836
- for (const e of (0, import_node_fs25.readdirSync)(d, { withFileTypes: true })) {
19367
+ for (const e of (0, import_node_fs28.readdirSync)(d, { withFileTypes: true })) {
18837
19368
  const p = (0, import_node_path21.join)(d, e.name);
18838
19369
  if (e.isDirectory()) {
18839
19370
  walk(p, depth + 1);
18840
19371
  } else if (e.name.startsWith("agent-") && e.name.endsWith(".jsonl")) {
18841
19372
  try {
18842
- const st = (0, import_node_fs25.statSync)(p);
19373
+ const st = (0, import_node_fs28.statSync)(p);
18843
19374
  found.push({ path: p, size: st.size, mtimeMs: st.mtimeMs });
18844
19375
  } catch {
18845
19376
  result.coverage.malformed++;
@@ -18856,7 +19387,7 @@ function fold(transcriptPath, opts = {}) {
18856
19387
  continue;
18857
19388
  }
18858
19389
  try {
18859
- ingest((0, import_node_fs25.readFileSync)(f.path, "utf8"), "subagent");
19390
+ ingest((0, import_node_fs28.readFileSync)(f.path, "utf8"), "subagent");
18860
19391
  bytes += f.size;
18861
19392
  result.coverage.subagentFiles++;
18862
19393
  } catch {
@@ -18891,7 +19422,7 @@ function fold(transcriptPath, opts = {}) {
18891
19422
  }
18892
19423
  function classifyUnobserved(path) {
18893
19424
  try {
18894
- const st = (0, import_node_fs25.statSync)(path);
19425
+ const st = (0, import_node_fs28.statSync)(path);
18895
19426
  if (!st.isFile()) return "unreadable";
18896
19427
  } catch {
18897
19428
  return "unreadable";
@@ -19195,20 +19726,20 @@ async function evidence(run) {
19195
19726
  }
19196
19727
 
19197
19728
  // src/lib/cache-cleanup.ts
19198
- var import_node_fs26 = require("node:fs");
19729
+ var import_node_fs29 = require("node:fs");
19199
19730
  var import_node_path22 = require("node:path");
19200
19731
  var CACHE_TTL_DAYS = 7;
19201
19732
  function pruneStaleCache() {
19202
19733
  try {
19203
19734
  const dir = projectPath(CACHE_DIR);
19204
19735
  const cutoff = Date.now() - CACHE_TTL_DAYS * 24 * 3600 * 1e3;
19205
- for (const entry of (0, import_node_fs26.readdirSync)(dir)) {
19736
+ for (const entry of (0, import_node_fs29.readdirSync)(dir)) {
19206
19737
  if (!entry.startsWith("pending-")) continue;
19207
19738
  const path = (0, import_node_path22.join)(dir, entry);
19208
19739
  try {
19209
- const stat3 = (0, import_node_fs26.statSync)(path);
19740
+ const stat3 = (0, import_node_fs29.statSync)(path);
19210
19741
  if (stat3.mtimeMs < cutoff) {
19211
- (0, import_node_fs26.unlinkSync)(path);
19742
+ (0, import_node_fs29.unlinkSync)(path);
19212
19743
  logEvent("cache_entry_pruned", {
19213
19744
  path: entry,
19214
19745
  age_days: Math.round((Date.now() - stat3.mtimeMs) / 864e5)
@@ -19222,7 +19753,7 @@ function pruneStaleCache() {
19222
19753
  }
19223
19754
 
19224
19755
  // src/lib/context-files.ts
19225
- var import_node_fs27 = require("node:fs");
19756
+ var import_node_fs30 = require("node:fs");
19226
19757
  var MAX_CONTEXT_FILES = 10;
19227
19758
  var MAX_CONTEXT_FILE_BYTES = 10240;
19228
19759
  var MAX_CONTEXT_TOTAL_BYTES = 51200;
@@ -19243,7 +19774,7 @@ function gatherContextFiles(contextPaths, deltaFiles) {
19243
19774
  continue;
19244
19775
  }
19245
19776
  try {
19246
- const content = (0, import_node_fs27.readFileSync)(safePath, "utf8");
19777
+ const content = (0, import_node_fs30.readFileSync)(safePath, "utf8");
19247
19778
  const bytes = Buffer.byteLength(content);
19248
19779
  if (bytes > MAX_CONTEXT_FILE_BYTES) {
19249
19780
  logEvent("context_file_skipped", { path: filePath, reason: "too_large", bytes });
@@ -19305,7 +19836,7 @@ async function contextFiles(run) {
19305
19836
 
19306
19837
  // src/lib/seed-runner.ts
19307
19838
  var import_promises11 = require("node:fs/promises");
19308
- var import_node_fs28 = require("node:fs");
19839
+ var import_node_fs31 = require("node:fs");
19309
19840
  var import_node_path23 = require("node:path");
19310
19841
  var import_yaml2 = __toESM(require_dist());
19311
19842
 
@@ -19545,7 +20076,7 @@ function renderNodeMarkdown(candidate, nodeId, createdAt) {
19545
20076
  return fm;
19546
20077
  }
19547
20078
  async function runSeed(opts) {
19548
- if (!(0, import_node_fs28.existsSync)(STANDARD_FILE)) {
20079
+ if (!(0, import_node_fs31.existsSync)(STANDARD_FILE)) {
19549
20080
  return { created: 0, failed: 0, skipped: "no_standard", candidates: [] };
19550
20081
  }
19551
20082
  let standardDoc;
@@ -19557,7 +20088,7 @@ async function runSeed(opts) {
19557
20088
  }
19558
20089
  const knowledgeSpec = standardDoc.knowledge_spec ?? {};
19559
20090
  let readmeContent;
19560
- if ((0, import_node_fs28.existsSync)("README.md")) {
20091
+ if ((0, import_node_fs31.existsSync)("README.md")) {
19561
20092
  try {
19562
20093
  readmeContent = await (0, import_promises11.readFile)("README.md", "utf-8");
19563
20094
  } catch {
@@ -19565,7 +20096,7 @@ async function runSeed(opts) {
19565
20096
  }
19566
20097
  let claudeMdContent;
19567
20098
  for (const p of ["CLAUDE.md", ".claude/CLAUDE.md"]) {
19568
- if ((0, import_node_fs28.existsSync)(p)) {
20099
+ if ((0, import_node_fs31.existsSync)(p)) {
19569
20100
  try {
19570
20101
  claudeMdContent = await (0, import_promises11.readFile)(p, "utf-8");
19571
20102
  break;
@@ -19589,7 +20120,7 @@ async function runSeed(opts) {
19589
20120
  return { created: 0, failed: 0, skipped: "no_candidates", candidates: [] };
19590
20121
  }
19591
20122
  const overviewPath = (0, import_node_path23.join)(MEMORY_DIR, "domain", "project-overview.md");
19592
- if ((0, import_node_fs28.existsSync)(overviewPath) && !opts.force) {
20123
+ if ((0, import_node_fs31.existsSync)(overviewPath) && !opts.force) {
19593
20124
  return { created: 0, failed: 0, skipped: "already_seeded", candidates };
19594
20125
  }
19595
20126
  if (opts.dryRun) {
@@ -19644,7 +20175,7 @@ async function runSeed(opts) {
19644
20175
  }
19645
20176
 
19646
20177
  // src/commands/analyze/phases/08-memory-manifest.ts
19647
- var import_node_fs29 = require("node:fs");
20178
+ var import_node_fs32 = require("node:fs");
19648
20179
  var import_node_path24 = require("node:path");
19649
20180
  async function memoryManifest(run) {
19650
20181
  const { globals } = run;
@@ -19656,8 +20187,8 @@ async function memoryManifest(run) {
19656
20187
  try {
19657
20188
  await ensureMemoryDir();
19658
20189
  const seedMarker = (0, import_node_path24.join)(VERITY_DIR, ".seeded");
19659
- const hasStandard = (0, import_node_fs29.existsSync)(STANDARD_FILE);
19660
- const alreadyTried = (0, import_node_fs29.existsSync)(seedMarker);
20190
+ const hasStandard = (0, import_node_fs32.existsSync)(STANDARD_FILE);
20191
+ const alreadyTried = (0, import_node_fs32.existsSync)(seedMarker);
19661
20192
  if (hasStandard && !alreadyTried) {
19662
20193
  const preManifest = await buildManifest();
19663
20194
  if (preManifest.nodes.length === 0) {
@@ -19670,7 +20201,7 @@ async function memoryManifest(run) {
19670
20201
  dryRun: false
19671
20202
  });
19672
20203
  if (seedResult.created > 0) {
19673
- (0, import_node_fs29.writeFileSync)(seedMarker, `${(/* @__PURE__ */ new Date()).toISOString()} created=${seedResult.created}
20204
+ (0, import_node_fs32.writeFileSync)(seedMarker, `${(/* @__PURE__ */ new Date()).toISOString()} created=${seedResult.created}
19674
20205
  `);
19675
20206
  autoSeedNotice = `Seeded ${seedResult.created} knowledge node(s) from your existing Standard (one-time).`;
19676
20207
  logEvent("auto_seed_ran", {
@@ -19678,7 +20209,7 @@ async function memoryManifest(run) {
19678
20209
  failed: seedResult.failed
19679
20210
  });
19680
20211
  } else if (seedResult.skipped === "already_seeded") {
19681
- (0, import_node_fs29.writeFileSync)(seedMarker, `${(/* @__PURE__ */ new Date()).toISOString()} skipped=already_seeded
20212
+ (0, import_node_fs32.writeFileSync)(seedMarker, `${(/* @__PURE__ */ new Date()).toISOString()} skipped=already_seeded
19682
20213
  `);
19683
20214
  } else {
19684
20215
  logEvent("auto_seed_noop", {
@@ -19867,7 +20398,7 @@ async function workingMemory(run) {
19867
20398
  }
19868
20399
 
19869
20400
  // src/lib/note-budget.ts
19870
- var import_node_fs30 = require("node:fs");
20401
+ var import_node_fs33 = require("node:fs");
19871
20402
  var ADVISORY_BUDGET = { PASS: 1, WARN: 2 };
19872
20403
  var EPISODE_STALE_SECONDS = 30 * 60;
19873
20404
  var FRESH = { delivered: 0, tasksCompleted: 0, ts: 0 };
@@ -19889,9 +20420,9 @@ function advisoryBudgetSpent(episode, rawDecision) {
19889
20420
  }
19890
20421
  function readAdvisoryEpisode(sessionId) {
19891
20422
  const file = scopedFile(ADVISORY_EPISODE_FILE, sessionId);
19892
- if (!(0, import_node_fs30.existsSync)(file)) return null;
20423
+ if (!(0, import_node_fs33.existsSync)(file)) return null;
19893
20424
  try {
19894
- const o = JSON.parse((0, import_node_fs30.readFileSync)(file, "utf-8")) ?? {};
20425
+ const o = JSON.parse((0, import_node_fs33.readFileSync)(file, "utf-8")) ?? {};
19895
20426
  const delivered = typeof o.delivered === "number" ? o.delivered : NaN;
19896
20427
  if (isNaN(delivered)) return null;
19897
20428
  return {
@@ -19905,8 +20436,8 @@ function readAdvisoryEpisode(sessionId) {
19905
20436
  }
19906
20437
  function writeAdvisoryEpisode(episode, sessionId) {
19907
20438
  try {
19908
- (0, import_node_fs30.mkdirSync)(VERITY_DIR, { recursive: true });
19909
- (0, import_node_fs30.writeFileSync)(
20439
+ (0, import_node_fs33.mkdirSync)(VERITY_DIR, { recursive: true });
20440
+ (0, import_node_fs33.writeFileSync)(
19910
20441
  scopedFile(ADVISORY_EPISODE_FILE, sessionId),
19911
20442
  JSON.stringify({ v: 1, ...episode })
19912
20443
  );
@@ -20013,7 +20544,16 @@ async function buildRequest(run) {
20013
20544
  // the state, so the number is one turn lagged by construction. The
20014
20545
  // degenerate win for the budget is a dead channel that looks like clean
20015
20546
  // code; this is what makes "did delivery rate collapse" a query.
20016
- advisory_delivered_prior: readAdvisoryEpisode(run.baselineSessionId)?.delivered ?? 0
20547
+ advisory_delivered_prior: readAdvisoryEpisode(run.baselineSessionId)?.delivered ?? 0,
20548
+ // `.verityignore` — see CoverageTelemetry.verityignore for why the SHARE is
20549
+ // the number that matters and why no paths travel with it.
20550
+ verityignore: {
20551
+ rules: run.verityIgnored.rules,
20552
+ excluded: run.verityIgnored.ignored.length,
20553
+ share: ignoreShare(run.verityIgnored.kept.length, run.verityIgnored.ignored.length),
20554
+ security_excluded: run.verityIgnored.securityExcluded.length,
20555
+ suspended: run.verityIgnored.suspended
20556
+ }
20017
20557
  };
20018
20558
  const requestBody = {
20019
20559
  coverage_telemetry: coverageTelemetry,
@@ -20211,14 +20751,14 @@ async function buildRequest(run) {
20211
20751
  }
20212
20752
 
20213
20753
  // src/lib/offline.ts
20214
- var import_node_fs31 = require("node:fs");
20754
+ var import_node_fs34 = require("node:fs");
20215
20755
  var import_node_crypto11 = require("node:crypto");
20216
20756
  function cacheRequest(body) {
20217
20757
  try {
20218
- (0, import_node_fs31.mkdirSync)(CACHE_DIR, { recursive: true });
20758
+ (0, import_node_fs34.mkdirSync)(CACHE_DIR, { recursive: true });
20219
20759
  const suffix = (0, import_node_crypto11.randomBytes)(4).toString("hex");
20220
20760
  const filename = `pending-${Math.floor(Date.now() / 1e3)}-${suffix}.json`;
20221
- (0, import_node_fs31.writeFileSync)(`${CACHE_DIR}/${filename}`, JSON.stringify(redactRequest(body)));
20761
+ (0, import_node_fs34.writeFileSync)(`${CACHE_DIR}/${filename}`, JSON.stringify(redactRequest(body)));
20222
20762
  } catch {
20223
20763
  }
20224
20764
  }
@@ -20337,7 +20877,7 @@ async function transmit(run) {
20337
20877
  }
20338
20878
 
20339
20879
  // src/commands/analyze/phases/13-reconcile.ts
20340
- var import_node_fs32 = require("node:fs");
20880
+ var import_node_fs35 = require("node:fs");
20341
20881
  var import_node_path26 = require("node:path");
20342
20882
  async function reconcile(run) {
20343
20883
  const { actionSummary, allChanged, analyzable, baseline, baselineSessionId, codeDelta, contentHash, conversation, decision, foldResult, memory, memorySession, response, reviewable, securityFiles, turnId } = run;
@@ -20348,7 +20888,7 @@ async function reconcile(run) {
20348
20888
  const st = foldDossier(memorySession.d);
20349
20889
  openElsewhere = openBlockingElsewhere(st.statements, sentPaths, (file, line) => {
20350
20890
  try {
20351
- const src = (0, import_node_fs32.readFileSync)((0, import_node_path26.join)(repoRoot(), file), "utf8").split("\n");
20891
+ const src = (0, import_node_fs35.readFileSync)((0, import_node_path26.join)(repoRoot(), file), "utf8").split("\n");
20352
20892
  const at = src[line - 1];
20353
20893
  return at === void 0 ? null : lineSha(at);
20354
20894
  } catch {
@@ -20409,6 +20949,25 @@ async function reconcile(run) {
20409
20949
  stage: "self-scope",
20410
20950
  kind: "policy"
20411
20951
  })),
20952
+ // ⚠ WHAT THE TEAM'S OWN `.verityignore` REMOVED — every path, named
20953
+ // (VRT-135 · 3/3). The 0.30 principle: an exclusion the ledger cannot see
20954
+ // is a scoping decision nobody can audit, and this is the one exclusion
20955
+ // stage a user can edit, so it is the one that most needs to be visible.
20956
+ //
20957
+ // `policy`, by D2's bar — no version of this product would have reviewed a
20958
+ // file the project declared out of scope, so it must not downgrade PASS to
20959
+ // WARN. That is also why it is not narrated per-turn by `describeCoverage`
20960
+ // (policy exclusions are recorded, not announced): repeating "your dist/
20961
+ // went unreviewed" on every turn is how a channel gets muted. The SHARE
20962
+ // below is the signal that replaces the noise.
20963
+ //
20964
+ // Taken from the run, not recomputed — see context.ts `verityIgnored`.
20965
+ ...run.verityIgnored.ignored.map((path) => ({
20966
+ path,
20967
+ reason: "verityignore",
20968
+ stage: "verityignore",
20969
+ kind: "policy"
20970
+ })),
20412
20971
  // The extension allowlist. POLICY: a changed README was never going to be
20413
20972
  // reviewed, and calling that a coverage gap would downgrade nearly every
20414
20973
  // PASS to WARN until WARN meant nothing. Recorded so the ledger balances and
@@ -20638,7 +21197,7 @@ function agentContextFor(response, intentRepeat = 0, priorPendingFingerprints =
20638
21197
  }
20639
21198
  async function render(run) {
20640
21199
  const { opts, globals } = run;
20641
- const { actionSummary, assistantResponse, autoSeedNotice, baselineSessionId, codeDelta, contentHash, conversation, currentCommit, decision, intentRepeatCount, memory, openElsewhere, priorPendingFingerprints, response, reviewCoverage, serviceUrl, sessionIdForMemory, silenced, token, watermarkHash, watermarkIsPartial } = run;
21200
+ const { actionSummary, assistantResponse, autoSeedNotice, voidedIgnoreNotice, baselineSessionId, codeDelta, contentHash, conversation, currentCommit, decision, intentRepeatCount, memory, openElsewhere, priorPendingFingerprints, response, reviewCoverage, serviceUrl, sessionIdForMemory, silenced, token, watermarkHash, watermarkIsPartial } = run;
20642
21201
  let { iteration } = run;
20643
21202
  const metadata = response.metadata ?? {};
20644
21203
  const intentAmbiguity = metadata.intent_ambiguity;
@@ -20890,6 +21449,7 @@ ${YELLOW}${grantNudge.trim()}${NC}
20890
21449
  const viewUrl = response.view_url ?? "";
20891
21450
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
20892
21451
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
21452
+ if (voidedIgnoreNotice) userSummary = `${voidedIgnoreNotice} ${userSummary}`;
20893
21453
  userSummary += loginNudge + grantNudge;
20894
21454
  emitVerdict({
20895
21455
  proposed: "PASS",
@@ -20911,6 +21471,7 @@ ${YELLOW}${grantNudge.trim()}${NC}
20911
21471
  const viewUrl = response.view_url ?? "";
20912
21472
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
20913
21473
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
21474
+ if (voidedIgnoreNotice) userSummary = `${voidedIgnoreNotice} ${userSummary}`;
20914
21475
  userSummary += loginNudge + grantNudge;
20915
21476
  emitVerdict({
20916
21477
  proposed: "WARN",
@@ -20926,7 +21487,11 @@ ${YELLOW}${grantNudge.trim()}${NC}
20926
21487
  }
20927
21488
  default: {
20928
21489
  const raw = String(decision ?? "(missing)");
20929
- const msg = (autoSeedNotice ? `${autoSeedNotice} Verity: unrecognised verdict \u2014 treating as WARN` : "Verity: unrecognised verdict \u2014 treating as WARN") + loginNudge + grantNudge;
21490
+ const msg = [
21491
+ voidedIgnoreNotice,
21492
+ autoSeedNotice,
21493
+ "Verity: unrecognised verdict \u2014 treating as WARN"
21494
+ ].filter(Boolean).join(" ") + loginNudge + grantNudge;
20930
21495
  process.stderr.write(
20931
21496
  `Verity: server returned an unrecognised gate_decision (${raw}). Rendering WARN rather than PASS. Update the CLI: npm i -g @codacy/verity-cli
20932
21497
  `
@@ -21003,7 +21568,7 @@ async function runAnalyze(opts, globals) {
21003
21568
  }
21004
21569
 
21005
21570
  // src/commands/baseline.ts
21006
- var import_node_fs33 = require("node:fs");
21571
+ var import_node_fs36 = require("node:fs");
21007
21572
  function registerBaselineCommands(program2) {
21008
21573
  const baseline = program2.command("baseline").description("Manage the task-start working-tree baseline");
21009
21574
  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) => {
@@ -21012,7 +21577,7 @@ function registerBaselineCommands(program2) {
21012
21577
  process.chdir(repoRoot());
21013
21578
  } catch {
21014
21579
  }
21015
- if (!(0, import_node_fs33.existsSync)(VERITY_DIR)) {
21580
+ if (!(0, import_node_fs36.existsSync)(VERITY_DIR)) {
21016
21581
  process.exit(0);
21017
21582
  }
21018
21583
  let sessionId = opts.sessionId;
@@ -21052,7 +21617,7 @@ async function readStdin() {
21052
21617
  }
21053
21618
 
21054
21619
  // src/commands/review.ts
21055
- var import_node_fs34 = require("node:fs");
21620
+ var import_node_fs37 = require("node:fs");
21056
21621
  function registerReviewCommand(program2) {
21057
21622
  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) => {
21058
21623
  const globals = program2.opts();
@@ -21071,7 +21636,7 @@ async function runReview(opts, globals) {
21071
21636
  const securityFiles = filterSecurity(allFiles);
21072
21637
  let staticResults;
21073
21638
  if (isCodacyAvailable()) {
21074
- const scannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles])).filter((f) => (0, import_node_fs34.existsSync)(f) || resolveFile(f) !== null);
21639
+ const scannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles])).filter((f) => (0, import_node_fs37.existsSync)(f) || resolveFile(f) !== null);
21075
21640
  staticResults = runCodacyAnalysis(scannable);
21076
21641
  } else {
21077
21642
  staticResults = {
@@ -21097,10 +21662,10 @@ async function runReview(opts, globals) {
21097
21662
  const specPaths = opts.specs.split(",").map((f) => f.trim()).filter(Boolean);
21098
21663
  specs = [];
21099
21664
  for (const p of specPaths) {
21100
- if (!(0, import_node_fs34.existsSync)(p)) continue;
21665
+ if (!(0, import_node_fs37.existsSync)(p)) continue;
21101
21666
  try {
21102
- const { readFileSync: readFileSync20 } = await import("node:fs");
21103
- const content = readFileSync20(p, "utf-8");
21667
+ const { readFileSync: readFileSync24 } = await import("node:fs");
21668
+ const content = readFileSync24(p, "utf-8");
21104
21669
  specs.push({ path: p, content: content.slice(0, 10240) });
21105
21670
  } catch {
21106
21671
  }
@@ -21157,7 +21722,7 @@ async function runReview(opts, globals) {
21157
21722
  }
21158
21723
 
21159
21724
  // src/commands/guard.ts
21160
- var import_node_fs35 = require("node:fs");
21725
+ var import_node_fs38 = require("node:fs");
21161
21726
  var import_node_path27 = require("node:path");
21162
21727
  var GUARD_BLOCK_CAP = 2;
21163
21728
  var GUARD_ITER_FILE = (0, import_node_path27.join)(VERITY_DIR, ".guard-iteration");
@@ -21205,7 +21770,7 @@ function readPreToolUseStdin() {
21205
21770
  }
21206
21771
  function readIterMap() {
21207
21772
  try {
21208
- const raw = JSON.parse((0, import_node_fs35.readFileSync)(GUARD_ITER_FILE, "utf-8"));
21773
+ const raw = JSON.parse((0, import_node_fs38.readFileSync)(GUARD_ITER_FILE, "utf-8"));
21209
21774
  if (raw && typeof raw === "object") {
21210
21775
  if (typeof raw.moment === "string" && typeof raw.count === "number") {
21211
21776
  return { [raw.moment]: raw.count };
@@ -21225,10 +21790,10 @@ function readIter(moment) {
21225
21790
  }
21226
21791
  function writeIter(moment, count) {
21227
21792
  try {
21228
- (0, import_node_fs35.mkdirSync)(VERITY_DIR, { recursive: true });
21793
+ (0, import_node_fs38.mkdirSync)(VERITY_DIR, { recursive: true });
21229
21794
  const map = readIterMap();
21230
21795
  map[moment] = count;
21231
- (0, import_node_fs35.writeFileSync)(GUARD_ITER_FILE, JSON.stringify(map));
21796
+ (0, import_node_fs38.writeFileSync)(GUARD_ITER_FILE, JSON.stringify(map));
21232
21797
  } catch {
21233
21798
  }
21234
21799
  }
@@ -21238,10 +21803,10 @@ function resetIter(moment) {
21238
21803
  if (!(moment in map)) return;
21239
21804
  delete map[moment];
21240
21805
  if (Object.keys(map).length === 0) {
21241
- if ((0, import_node_fs35.existsSync)(GUARD_ITER_FILE)) (0, import_node_fs35.unlinkSync)(GUARD_ITER_FILE);
21806
+ if ((0, import_node_fs38.existsSync)(GUARD_ITER_FILE)) (0, import_node_fs38.unlinkSync)(GUARD_ITER_FILE);
21242
21807
  } else {
21243
- (0, import_node_fs35.mkdirSync)(VERITY_DIR, { recursive: true });
21244
- (0, import_node_fs35.writeFileSync)(GUARD_ITER_FILE, JSON.stringify(map));
21808
+ (0, import_node_fs38.mkdirSync)(VERITY_DIR, { recursive: true });
21809
+ (0, import_node_fs38.writeFileSync)(GUARD_ITER_FILE, JSON.stringify(map));
21245
21810
  }
21246
21811
  } catch {
21247
21812
  }
@@ -21302,16 +21867,17 @@ function extractStatedIntent(moment, command, pushedMessages = null) {
21302
21867
  const text = moment === "pre-commit" ? parseCommitMessage(command) : parsePrIntent(command) ?? pushedMessages;
21303
21868
  return isSubstantiveIntent(text) ? text : null;
21304
21869
  }
21305
- function hasBlockingFinding(response) {
21870
+ function hasBlockingFinding(response, sentFiles) {
21306
21871
  const findings = response.findings ?? [];
21307
- return findings.some((f) => f.scope !== "pre-existing" && ["critical", "high"].includes((f.severity ?? "").toLowerCase()));
21872
+ const sent = sentFiles ? new Set(sentFiles.map((p) => p.replace(/\\/g, "/"))) : null;
21873
+ 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, "/"))));
21308
21874
  }
21309
21875
  function buildGuardRequest(moment, files, codeDelta, iter, sessionId, statedIntent, coverageTelemetry) {
21310
21876
  const analyzable = filterAnalyzable(files);
21311
21877
  const securityFiles = filterSecurity(files);
21312
21878
  let staticResults;
21313
21879
  if (isCodacyAvailable()) {
21314
- const scannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles])).map((f) => (0, import_node_fs35.existsSync)(f) ? f : resolveFile(f)).filter((f) => f !== null);
21880
+ const scannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles])).map((f) => (0, import_node_fs38.existsSync)(f) ? f : resolveFile(f)).filter((f) => f !== null);
21315
21881
  staticResults = runCodacyAnalysis(scannable);
21316
21882
  } else {
21317
21883
  staticResults = { tool: "@codacy/analysis-cli", findings: [], summary: { total_findings: 0, by_severity: {}, tools_run: [] } };
@@ -21333,13 +21899,11 @@ function buildGuardRequest(moment, files, codeDelta, iter, sessionId, statedInte
21333
21899
  }
21334
21900
  };
21335
21901
  if (coverageTelemetry) requestBody.coverage_telemetry = coverageTelemetry;
21336
- const specs = discoverSpecs();
21337
- const plans = discoverPlans();
21338
- if (specs.length > 0 || plans.length > 0 || statedIntent) {
21902
+ const docs = discoverGuardDocs(files);
21903
+ if (docs.length > 0 || statedIntent) {
21339
21904
  const intentContext = {};
21340
21905
  if (statedIntent) intentContext.user_prompt = statedIntent;
21341
- if (specs.length > 0) intentContext.specs = specs;
21342
- if (plans.length > 0) intentContext.plans = plans;
21906
+ if (docs.length > 0) intentContext.plans = docs;
21343
21907
  requestBody.intent_context = intentContext;
21344
21908
  }
21345
21909
  return requestBody;
@@ -21420,7 +21984,7 @@ async function runGuard(opts, globals) {
21420
21984
  const urlResult = await resolveServiceUrl(globals.serviceUrl);
21421
21985
  if (!tokenResult.ok || !urlResult.ok) process.exit(0);
21422
21986
  logEvent("guard_frame", { moment, ...frameTelemetry(frame, range) });
21423
- const codeDelta = collectCodeDelta(files);
21987
+ const codeDelta = collectCodeDelta(files, GIT_MOMENT_BUDGET);
21424
21988
  if (codeDelta.total_files === 0) process.exit(0);
21425
21989
  const statedIntent = extractStatedIntent(
21426
21990
  moment,
@@ -21446,6 +22010,7 @@ async function runGuard(opts, globals) {
21446
22010
  excluded: codeDelta.excluded.map((e) => ({ path: e.path, reason: e.reason }))
21447
22011
  };
21448
22012
  logToFileOnly(coverageBlock(coverage));
22013
+ const reviewStart = Date.now();
21449
22014
  const result = await analyzeRequest({
21450
22015
  serviceUrl: urlResult.data,
21451
22016
  token: tokenResult.data.token,
@@ -21454,6 +22019,7 @@ async function runGuard(opts, globals) {
21454
22019
  timeout: 29e4,
21455
22020
  cmd: "guard"
21456
22021
  });
22022
+ const reviewSecs = Math.max(1, Math.round((Date.now() - reviewStart) / 1e3));
21457
22023
  if (!result.ok) {
21458
22024
  const authRemedy = result.error.startsWith("STALE_VERIFICATION") ? " Your GitHub verification expired \u2014 run `verity login` to re-verify." : result.error.startsWith("FORBIDDEN") ? " No access grant for this repository \u2014 run `verity login` to refresh your grants." : result.error.startsWith("INVALID_TOKEN") ? " Your Verity login expired or was revoked \u2014 run `verity login` to sign in again." : "";
21459
22025
  emitAllowNotice(
@@ -21466,9 +22032,9 @@ async function runGuard(opts, globals) {
21466
22032
  const decision = response.gate_decision ?? "(unrecognised)";
21467
22033
  const viewUrl = response.view_url ?? "";
21468
22034
  const link = viewUrl ? ` \u2014 ${viewUrl}` : "";
21469
- const covLine = coverageSummary(coverage);
22035
+ const covLine = `${coverageSummary(coverage)} in ${reviewSecs}s`;
21470
22036
  const covDetail = coverageBlock(coverage);
21471
- if (decision === "FAIL" && hasBlockingFinding(response)) {
22037
+ if (decision === "FAIL" && hasBlockingFinding(response, codeDelta.files.map((f) => f.path))) {
21472
22038
  writeIter(moment, iter + 1);
21473
22039
  writeBlockMessage(moment, response, covDetail);
21474
22040
  process.exit(2);
@@ -21536,17 +22102,166 @@ function writeBlockMessage(moment, response, covDetail) {
21536
22102
  `);
21537
22103
  process.stderr.write(`${BOLD}Fix these, then re-run the ${verb}.${NC}
21538
22104
  `);
22105
+ process.stderr.write(`${DIM}Accepted risk? Record it: verity waive <pattern-id> --file <path> --reason "\u2026" (voids automatically when the file changes).${NC}
22106
+ `);
22107
+ }
22108
+
22109
+ // src/commands/ignore.ts
22110
+ function registerIgnoreCommand(program2) {
22111
+ 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) => {
22112
+ const globals = program2.opts();
22113
+ const now = Math.floor(Date.now() / 1e3);
22114
+ const reason = opts.reason?.trim();
22115
+ if (!reason) {
22116
+ printError(
22117
+ '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.'
22118
+ );
22119
+ process.exit(1);
22120
+ }
22121
+ if (opts.for && opts.turn) {
22122
+ printError("Use either --turn or --for, not both \u2014 they are two different windows.");
22123
+ process.exit(1);
22124
+ }
22125
+ let scope2 = "turn";
22126
+ let ttl = TURN_FUSE_SECONDS;
22127
+ if (opts.for) {
22128
+ const parsed = parseDuration(opts.for);
22129
+ if (!parsed.ok) {
22130
+ printError(parsed.error);
22131
+ process.exit(1);
22132
+ }
22133
+ scope2 = "window";
22134
+ ttl = parsed.seconds;
22135
+ }
22136
+ const tokenResult = await resolveToken(globals.token);
22137
+ const token = tokenResult.ok ? tokenResult.data.token : void 0;
22138
+ const sessionId = opts.sessionId || process.env.CLAUDE_SESSION_ID || void 0;
22139
+ const keys = ignoreStateKeys(token, sessionId);
22140
+ const existing = resolveIgnoreState(keys);
22141
+ const spent = existing?.state.spent ?? 0;
22142
+ const writeKey = existing?.key ?? keys[0];
22143
+ if (spent >= IGNORE_BUDGET) {
22144
+ 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.`;
22145
+ if (opts.json) {
22146
+ printJson({ declared: false, reason_refused: "budget-spent", spent, budget: IGNORE_BUDGET });
22147
+ } else {
22148
+ printWarn(msg);
22149
+ }
22150
+ logEvent("ignore_refused", { why: "budget-spent", spent, budget: IGNORE_BUDGET });
22151
+ process.exit(0);
22152
+ }
22153
+ const declaration = {
22154
+ scope: scope2,
22155
+ origin: opts.agent ? "agent" : "user",
22156
+ reason,
22157
+ at: now,
22158
+ expires: now + ttl
22159
+ };
22160
+ writeIgnoreState({ v: 1, active: declaration, spent: spent + 1 }, writeKey);
22161
+ logEvent("ignore_declared", {
22162
+ scope: scope2,
22163
+ origin: declaration.origin,
22164
+ ttl_seconds: ttl,
22165
+ spent: spent + 1,
22166
+ budget: IGNORE_BUDGET
22167
+ });
22168
+ if (opts.json) {
22169
+ printJson({
22170
+ declared: true,
22171
+ scope: scope2,
22172
+ origin: declaration.origin,
22173
+ reason,
22174
+ expires_at: new Date(declaration.expires * 1e3).toISOString(),
22175
+ spent: spent + 1,
22176
+ budget: IGNORE_BUDGET
22177
+ });
22178
+ return;
22179
+ }
22180
+ const window = scope2 === "turn" ? "the next turn" : `the next ${describeRemaining(declaration, now).replace(" left", "")}`;
22181
+ printInfo(`Verity will skip ${window} \u2014 "${reason}" (${spent + 1}/${IGNORE_BUDGET} this session).`);
22182
+ printInfo("It covers turns that author nothing. If anything is written, the declaration voids and the review runs.");
22183
+ });
22184
+ 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) => {
22185
+ const globals = program2.opts();
22186
+ const tokenResult = await resolveToken(globals.token);
22187
+ const token = tokenResult.ok ? tokenResult.data.token : void 0;
22188
+ const sessionId = opts.sessionId || process.env.CLAUDE_SESSION_ID || void 0;
22189
+ const found = resolveIgnoreState(ignoreStateKeys(token, sessionId));
22190
+ const active = resolveActive(found?.state ?? null, Math.floor(Date.now() / 1e3));
22191
+ if (!found || !active) {
22192
+ printInfo("No active ignore declaration.");
22193
+ return;
22194
+ }
22195
+ clearActiveDeclaration(found.key);
22196
+ logEvent("ignore_cleared", { scope: active.scope, origin: active.origin });
22197
+ printInfo(`Cleared: "${active.reason}". The next turn will be reviewed normally.`);
22198
+ });
22199
+ }
22200
+
22201
+ // src/commands/waive.ts
22202
+ var import_node_crypto12 = require("node:crypto");
22203
+ var import_node_fs39 = require("node:fs");
22204
+ function registerWaiveCommand(program2) {
22205
+ program2.command("waive <pattern-id>").description("Record an accepted-risk disposition for an open finding (voids when the file changes)").option("--file <path>", "File the finding is anchored to, REPO-RELATIVE (recommended \u2014 narrows the waive)").requiredOption("--reason <text>", "The human disposition this records (reviewer finding, ADR, \u2026)").action(async (patternId, opts) => {
22206
+ const globals = program2.opts();
22207
+ try {
22208
+ process.chdir(repoRoot());
22209
+ } catch {
22210
+ }
22211
+ if (opts.reason.trim().length < 20) {
22212
+ printError("A waive needs a real reason (>= 20 chars) \u2014 name the human disposition it records.");
22213
+ process.exit(1);
22214
+ }
22215
+ const tokenResult = await resolveToken(globals.token);
22216
+ if (!tokenResult.ok) {
22217
+ printError(tokenResult.error);
22218
+ process.exit(1);
22219
+ }
22220
+ const urlResult = await resolveServiceUrl(globals.serviceUrl);
22221
+ if (!urlResult.ok) {
22222
+ printError(urlResult.error);
22223
+ process.exit(1);
22224
+ }
22225
+ const body = {
22226
+ pattern_id: patternId,
22227
+ reason: opts.reason.trim()
22228
+ };
22229
+ if (opts.file) {
22230
+ body.file = opts.file;
22231
+ try {
22232
+ body.file_sha256 = (0, import_node_crypto12.createHash)("sha256").update((0, import_node_fs39.readFileSync)(opts.file)).digest("hex");
22233
+ } catch {
22234
+ printError(`Cannot read ${opts.file} \u2014 run from the repo root, or omit --file to waive by pattern.`);
22235
+ process.exit(1);
22236
+ }
22237
+ }
22238
+ const result = await apiRequest({
22239
+ method: "POST",
22240
+ path: "/feedback/waive",
22241
+ serviceUrl: urlResult.data,
22242
+ token: tokenResult.data.token,
22243
+ body,
22244
+ verbose: globals.verbose
22245
+ });
22246
+ if (!result.ok) {
22247
+ printError(`Couldn't record the waive: ${result.error}`);
22248
+ process.exit(1);
22249
+ }
22250
+ logEvent("waive_recorded", { pattern: patternId, file: opts.file ?? null, waived: result.data.waived });
22251
+ printInfo(`Waived ${result.data.waived} open statement(s) for '${patternId}'${opts.file ? ` on ${opts.file}` : ""}.`);
22252
+ printInfo(result.data.note);
22253
+ });
21539
22254
  }
21540
22255
 
21541
22256
  // src/commands/init.ts
21542
- var import_node_fs37 = require("node:fs");
22257
+ var import_node_fs41 = require("node:fs");
21543
22258
  var import_promises13 = require("node:fs/promises");
21544
22259
  var import_node_path29 = require("node:path");
21545
22260
  var import_node_child_process11 = require("node:child_process");
21546
22261
  var readline2 = __toESM(require("node:readline/promises"));
21547
22262
 
21548
22263
  // src/commands/migrate.ts
21549
- var import_node_fs36 = require("node:fs");
22264
+ var import_node_fs40 = require("node:fs");
21550
22265
  var import_node_path28 = require("node:path");
21551
22266
  var import_node_child_process10 = require("node:child_process");
21552
22267
 
@@ -21678,10 +22393,10 @@ async function runMigration(opts = {}) {
21678
22393
  function migrateProjectDir(root, actions) {
21679
22394
  const gateDir = (0, import_node_path28.join)(root, ".gate");
21680
22395
  const verityDir = (0, import_node_path28.join)(root, ".verity");
21681
- if ((0, import_node_fs36.existsSync)(gateDir) && !(0, import_node_fs36.existsSync)(verityDir)) {
22396
+ if ((0, import_node_fs40.existsSync)(gateDir) && !(0, import_node_fs40.existsSync)(verityDir)) {
21682
22397
  return migrateProjectDirRename(root, gateDir, verityDir, actions);
21683
22398
  }
21684
- if ((0, import_node_fs36.existsSync)(gateDir) && (0, import_node_fs36.existsSync)(verityDir)) {
22399
+ if ((0, import_node_fs40.existsSync)(gateDir) && (0, import_node_fs40.existsSync)(verityDir)) {
21685
22400
  return migrateProjectDirCarry(gateDir, verityDir, actions);
21686
22401
  }
21687
22402
  return false;
@@ -21702,13 +22417,13 @@ function migrateProjectDirRename(root, gateDir, verityDir, actions) {
21702
22417
  }
21703
22418
  }
21704
22419
  if (moved) {
21705
- if ((0, import_node_fs36.existsSync)(gateDir)) {
22420
+ if ((0, import_node_fs40.existsSync)(gateDir)) {
21706
22421
  const carried = carryLegacyContents(gateDir, verityDir);
21707
22422
  if (carried > 0) {
21708
22423
  actions.push(`Carried ${carried} untracked legacy file(s) from .gate/ into .verity/`);
21709
22424
  }
21710
22425
  try {
21711
- (0, import_node_fs36.rmSync)(gateDir, { recursive: true, force: true });
22426
+ (0, import_node_fs40.rmSync)(gateDir, { recursive: true, force: true });
21712
22427
  } catch {
21713
22428
  }
21714
22429
  }
@@ -21724,7 +22439,7 @@ function migrateProjectDirCarry(gateDir, verityDir, actions) {
21724
22439
  actions.push(`Carried ${carried} legacy file(s) from .gate/ into .verity/`);
21725
22440
  }
21726
22441
  try {
21727
- (0, import_node_fs36.rmSync)(gateDir, { recursive: true, force: true });
22442
+ (0, import_node_fs40.rmSync)(gateDir, { recursive: true, force: true });
21728
22443
  } catch {
21729
22444
  }
21730
22445
  return carried > 0;
@@ -21733,9 +22448,9 @@ function migrateGlobalCredentials(home, actions) {
21733
22448
  if (!home) return;
21734
22449
  const gateCreds = (0, import_node_path28.join)(home, ".gate", "credentials");
21735
22450
  const verityCreds = (0, import_node_path28.join)(home, ".verity", "credentials");
21736
- if (!(0, import_node_fs36.existsSync)(gateCreds)) return;
21737
- if (!(0, import_node_fs36.existsSync)(verityCreds)) {
21738
- (0, import_node_fs36.mkdirSync)((0, import_node_path28.join)(home, ".verity"), { recursive: true });
22451
+ if (!(0, import_node_fs40.existsSync)(gateCreds)) return;
22452
+ if (!(0, import_node_fs40.existsSync)(verityCreds)) {
22453
+ (0, import_node_fs40.mkdirSync)((0, import_node_path28.join)(home, ".verity"), { recursive: true });
21739
22454
  moveFile(gateCreds, verityCreds);
21740
22455
  actions.push("Moved ~/.gate/credentials \u2192 ~/.verity/credentials");
21741
22456
  return;
@@ -21758,7 +22473,7 @@ async function migrateLegacyHooks(root, actions) {
21758
22473
  }
21759
22474
  async function migrateClaudeMd(root, actions) {
21760
22475
  const claudeMd = (0, import_node_path28.join)(root, "CLAUDE.md");
21761
- const hadLegacyBlock = (0, import_node_fs36.existsSync)(claudeMd) && hasLegacyMemoryBlock(readFileSyncSafe(claudeMd));
22476
+ const hadLegacyBlock = (0, import_node_fs40.existsSync)(claudeMd) && hasLegacyMemoryBlock(readFileSyncSafe(claudeMd));
21762
22477
  if (!hadLegacyBlock) return;
21763
22478
  try {
21764
22479
  await ensureClaudeMdPointer(root);
@@ -21770,7 +22485,7 @@ async function migrateClaudeMd(root, actions) {
21770
22485
  function migrateStandardFile(root, actions) {
21771
22486
  const gateMd = (0, import_node_path28.join)(root, "GATE.md");
21772
22487
  const verityMd = (0, import_node_path28.join)(root, "VERITY.md");
21773
- if (!(0, import_node_fs36.existsSync)(gateMd) || (0, import_node_fs36.existsSync)(verityMd)) return;
22488
+ if (!(0, import_node_fs40.existsSync)(gateMd) || (0, import_node_fs40.existsSync)(verityMd)) return;
21774
22489
  let moved = false;
21775
22490
  if (isGitRepo(root) && isGitTracked(root, "GATE.md")) {
21776
22491
  try {
@@ -21782,12 +22497,12 @@ function migrateStandardFile(root, actions) {
21782
22497
  if (!moved) moveFile(gateMd, verityMd);
21783
22498
  const content = readFileSyncSafe(verityMd);
21784
22499
  const refreshed = content.split("GATE.md").join("VERITY.md");
21785
- if (refreshed !== content) (0, import_node_fs36.writeFileSync)(verityMd, refreshed);
22500
+ if (refreshed !== content) (0, import_node_fs40.writeFileSync)(verityMd, refreshed);
21786
22501
  actions.push("Renamed GATE.md \u2192 VERITY.md");
21787
22502
  }
21788
22503
  async function migrateTelemetryHeaders(root, actions) {
21789
22504
  const file = (0, import_node_path28.join)(root, ".claude", "settings.local.json");
21790
- if (!(0, import_node_fs36.existsSync)(file)) return;
22505
+ if (!(0, import_node_fs40.existsSync)(file)) return;
21791
22506
  let settings;
21792
22507
  try {
21793
22508
  settings = JSON.parse(readFileSyncSafe(file) || "{}");
@@ -21835,14 +22550,14 @@ function mergeGlobalCredentials(gateCreds, verityCreds) {
21835
22550
  }
21836
22551
  if (toAppend.length > 0) {
21837
22552
  const sep2 = verityContent.endsWith("\n") || verityContent === "" ? "" : "\n";
21838
- (0, import_node_fs36.writeFileSync)(verityCreds, verityContent + sep2 + toAppend.join("\n") + "\n");
22553
+ (0, import_node_fs40.writeFileSync)(verityCreds, verityContent + sep2 + toAppend.join("\n") + "\n");
21839
22554
  }
21840
- (0, import_node_fs36.rmSync)(gateCreds, { force: true });
22555
+ (0, import_node_fs40.rmSync)(gateCreds, { force: true });
21841
22556
  return toAppend.length;
21842
22557
  }
21843
22558
  function readFileSyncSafe(path) {
21844
22559
  try {
21845
- return (0, import_node_fs36.readFileSync)(path, "utf-8");
22560
+ return (0, import_node_fs40.readFileSync)(path, "utf-8");
21846
22561
  } catch {
21847
22562
  return "";
21848
22563
  }
@@ -21857,35 +22572,35 @@ function hasStagedChanges(root) {
21857
22572
  }
21858
22573
  function moveDir(from, to) {
21859
22574
  try {
21860
- (0, import_node_fs36.renameSync)(from, to);
22575
+ (0, import_node_fs40.renameSync)(from, to);
21861
22576
  } catch (err) {
21862
22577
  if (err.code !== "EXDEV") throw err;
21863
- (0, import_node_fs36.cpSync)(from, to, { recursive: true });
21864
- (0, import_node_fs36.rmSync)(from, { recursive: true, force: true });
22578
+ (0, import_node_fs40.cpSync)(from, to, { recursive: true });
22579
+ (0, import_node_fs40.rmSync)(from, { recursive: true, force: true });
21865
22580
  }
21866
22581
  }
21867
22582
  function moveFile(from, to) {
21868
22583
  try {
21869
- (0, import_node_fs36.renameSync)(from, to);
22584
+ (0, import_node_fs40.renameSync)(from, to);
21870
22585
  } catch (err) {
21871
22586
  if (err.code !== "EXDEV") throw err;
21872
- (0, import_node_fs36.cpSync)(from, to);
21873
- (0, import_node_fs36.rmSync)(from, { force: true });
22587
+ (0, import_node_fs40.cpSync)(from, to);
22588
+ (0, import_node_fs40.rmSync)(from, { force: true });
21874
22589
  }
21875
22590
  }
21876
22591
  function carryLegacyContents(gateDir, verityDir) {
21877
22592
  let copied = 0;
21878
22593
  const walk = (relDir) => {
21879
22594
  const srcDir = (0, import_node_path28.join)(gateDir, relDir);
21880
- for (const entry of (0, import_node_fs36.readdirSync)(srcDir)) {
22595
+ for (const entry of (0, import_node_fs40.readdirSync)(srcDir)) {
21881
22596
  const rel = relDir ? (0, import_node_path28.join)(relDir, entry) : entry;
21882
22597
  const src = (0, import_node_path28.join)(gateDir, rel);
21883
22598
  const dest = (0, import_node_path28.join)(verityDir, rel);
21884
- if ((0, import_node_fs36.statSync)(src).isDirectory()) {
22599
+ if ((0, import_node_fs40.statSync)(src).isDirectory()) {
21885
22600
  walk(rel);
21886
- } else if (!(0, import_node_fs36.existsSync)(dest)) {
21887
- (0, import_node_fs36.mkdirSync)((0, import_node_path28.dirname)(dest), { recursive: true });
21888
- (0, import_node_fs36.cpSync)(src, dest);
22601
+ } else if (!(0, import_node_fs40.existsSync)(dest)) {
22602
+ (0, import_node_fs40.mkdirSync)((0, import_node_path28.dirname)(dest), { recursive: true });
22603
+ (0, import_node_fs40.cpSync)(src, dest);
21889
22604
  copied++;
21890
22605
  }
21891
22606
  }
@@ -21896,20 +22611,20 @@ function carryLegacyContents(gateDir, verityDir) {
21896
22611
  async function needsMigration(root = repoRoot()) {
21897
22612
  const gateDir = (0, import_node_path28.join)(root, ".gate");
21898
22613
  const verityDir = (0, import_node_path28.join)(root, ".verity");
21899
- if ((0, import_node_fs36.existsSync)(gateDir) && !(0, import_node_fs36.existsSync)(verityDir)) return true;
21900
- if ((0, import_node_fs36.existsSync)(gateDir) && (0, import_node_fs36.existsSync)(verityDir)) {
21901
- if ((0, import_node_fs36.existsSync)((0, import_node_path28.join)(gateDir, "credentials")) && !(0, import_node_fs36.existsSync)((0, import_node_path28.join)(verityDir, "credentials"))) {
22614
+ if ((0, import_node_fs40.existsSync)(gateDir) && !(0, import_node_fs40.existsSync)(verityDir)) return true;
22615
+ if ((0, import_node_fs40.existsSync)(gateDir) && (0, import_node_fs40.existsSync)(verityDir)) {
22616
+ if ((0, import_node_fs40.existsSync)((0, import_node_path28.join)(gateDir, "credentials")) && !(0, import_node_fs40.existsSync)((0, import_node_path28.join)(verityDir, "credentials"))) {
21902
22617
  return true;
21903
22618
  }
21904
- if ((0, import_node_fs36.existsSync)((0, import_node_path28.join)(gateDir, "memory")) && !(0, import_node_fs36.existsSync)((0, import_node_path28.join)(verityDir, "memory"))) {
22619
+ if ((0, import_node_fs40.existsSync)((0, import_node_path28.join)(gateDir, "memory")) && !(0, import_node_fs40.existsSync)((0, import_node_path28.join)(verityDir, "memory"))) {
21905
22620
  return true;
21906
22621
  }
21907
22622
  }
21908
22623
  const claudeMd = (0, import_node_path28.join)(root, "CLAUDE.md");
21909
- if ((0, import_node_fs36.existsSync)(claudeMd) && hasLegacyMemoryBlock(readFileSyncSafe(claudeMd))) {
22624
+ if ((0, import_node_fs40.existsSync)(claudeMd) && hasLegacyMemoryBlock(readFileSyncSafe(claudeMd))) {
21910
22625
  return true;
21911
22626
  }
21912
- if ((0, import_node_fs36.existsSync)((0, import_node_path28.join)(root, "GATE.md")) && !(0, import_node_fs36.existsSync)((0, import_node_path28.join)(root, "VERITY.md"))) {
22627
+ if ((0, import_node_fs40.existsSync)((0, import_node_path28.join)(root, "GATE.md")) && !(0, import_node_fs40.existsSync)((0, import_node_path28.join)(root, "VERITY.md"))) {
21913
22628
  return true;
21914
22629
  }
21915
22630
  if (await hasLegacyHooksAt(root)) return true;
@@ -22053,7 +22768,7 @@ function resolveDataDir() {
22053
22768
  // local dev: running from repo root
22054
22769
  ];
22055
22770
  for (const candidate of candidates) {
22056
- if ((0, import_node_fs37.existsSync)((0, import_node_path29.join)(candidate, "skills"))) {
22771
+ if ((0, import_node_fs41.existsSync)((0, import_node_path29.join)(candidate, "skills"))) {
22057
22772
  return candidate;
22058
22773
  }
22059
22774
  }
@@ -22069,7 +22784,7 @@ function registerInitCommand(program2) {
22069
22784
  program2.command("init").description("Initialize Verity in the current project").option("--force", "Overwrite existing skills and hooks").action(async (opts) => {
22070
22785
  const force = opts.force ?? false;
22071
22786
  const projectMarkers = [".git", "package.json", "pyproject.toml", "go.mod", "Cargo.toml", "Gemfile", "pom.xml", "build.gradle"];
22072
- const isProject = projectMarkers.some((m) => (0, import_node_fs37.existsSync)(m));
22787
+ const isProject = projectMarkers.some((m) => (0, import_node_fs41.existsSync)(m));
22073
22788
  if (!isProject) {
22074
22789
  printError("No project detected in the current directory.");
22075
22790
  printInfo('Run "verity init" from your project root.');
@@ -22139,14 +22854,14 @@ function registerInitCommand(program2) {
22139
22854
  for (const skill of skills) {
22140
22855
  const src = (0, import_node_path29.join)(skillsSource, skill);
22141
22856
  const dest = (0, import_node_path29.join)(skillsDest, skill);
22142
- if (!(0, import_node_fs37.existsSync)(src)) {
22857
+ if (!(0, import_node_fs41.existsSync)(src)) {
22143
22858
  printWarn(` Skill data not found: ${skill}`);
22144
22859
  continue;
22145
22860
  }
22146
- if ((0, import_node_fs37.existsSync)(dest) && !force) {
22861
+ if ((0, import_node_fs41.existsSync)(dest) && !force) {
22147
22862
  const srcSkill = (0, import_node_path29.join)(src, "SKILL.md");
22148
22863
  const destSkill = (0, import_node_path29.join)(dest, "SKILL.md");
22149
- if ((0, import_node_fs37.existsSync)(destSkill)) {
22864
+ if ((0, import_node_fs41.existsSync)(destSkill)) {
22150
22865
  try {
22151
22866
  const srcContent = await (0, import_promises13.readFile)(srcSkill, "utf-8");
22152
22867
  const destContent = await (0, import_promises13.readFile)(destSkill, "utf-8");
@@ -22224,7 +22939,7 @@ function registerInitCommand(program2) {
22224
22939
  }
22225
22940
 
22226
22941
  // src/commands/uninstall.ts
22227
- var import_node_fs38 = require("node:fs");
22942
+ var import_node_fs42 = require("node:fs");
22228
22943
  var import_node_path30 = require("node:path");
22229
22944
  var SKILL_NAMES = [
22230
22945
  "verity-setup",
@@ -22245,10 +22960,10 @@ function registerUninstallCommand(program2) {
22245
22960
  const skillsRoot = projectPath(".claude/skills");
22246
22961
  for (const name of SKILL_NAMES) {
22247
22962
  const dir = (0, import_node_path30.join)(skillsRoot, name);
22248
- if ((0, import_node_fs38.existsSync)(dir)) {
22963
+ if ((0, import_node_fs42.existsSync)(dir)) {
22249
22964
  actions.push({
22250
22965
  label: `Remove .claude/skills/${name}/`,
22251
- apply: () => (0, import_node_fs38.rmSync)(dir, { recursive: true, force: true })
22966
+ apply: () => (0, import_node_fs42.rmSync)(dir, { recursive: true, force: true })
22252
22967
  });
22253
22968
  }
22254
22969
  }
@@ -22262,24 +22977,24 @@ function registerUninstallCommand(program2) {
22262
22977
  });
22263
22978
  }
22264
22979
  const verityDir = projectPath(VERITY_DIR);
22265
- if ((0, import_node_fs38.existsSync)(verityDir)) {
22980
+ if ((0, import_node_fs42.existsSync)(verityDir)) {
22266
22981
  actions.push({
22267
22982
  label: `Remove ${VERITY_DIR}/`,
22268
- apply: () => (0, import_node_fs38.rmSync)(verityDir, { recursive: true, force: true })
22983
+ apply: () => (0, import_node_fs42.rmSync)(verityDir, { recursive: true, force: true })
22269
22984
  });
22270
22985
  }
22271
22986
  if (!keepVerityMd) {
22272
22987
  const verityMd = projectPath(VERITY_MD_FILE);
22273
- if ((0, import_node_fs38.existsSync)(verityMd)) {
22988
+ if ((0, import_node_fs42.existsSync)(verityMd)) {
22274
22989
  actions.push({
22275
22990
  label: `Remove ${VERITY_MD_FILE}`,
22276
- apply: () => (0, import_node_fs38.rmSync)(verityMd, { force: true })
22991
+ apply: () => (0, import_node_fs42.rmSync)(verityMd, { force: true })
22277
22992
  });
22278
22993
  }
22279
22994
  }
22280
22995
  const cleanupEmptyDir = (path) => {
22281
- if ((0, import_node_fs38.existsSync)(path) && (0, import_node_fs38.statSync)(path).isDirectory() && (0, import_node_fs38.readdirSync)(path).length === 0) {
22282
- (0, import_node_fs38.rmdirSync)(path);
22996
+ if ((0, import_node_fs42.existsSync)(path) && (0, import_node_fs42.statSync)(path).isDirectory() && (0, import_node_fs42.readdirSync)(path).length === 0) {
22997
+ (0, import_node_fs42.rmdirSync)(path);
22283
22998
  }
22284
22999
  };
22285
23000
  actions.push({
@@ -22291,10 +23006,10 @@ function registerUninstallCommand(program2) {
22291
23006
  });
22292
23007
  const home = process.env.HOME ?? "";
22293
23008
  const globalVerityDir = (0, import_node_path30.join)(home, ".verity");
22294
- if (purgeGlobal && (0, import_node_fs38.existsSync)(globalVerityDir)) {
23009
+ if (purgeGlobal && (0, import_node_fs42.existsSync)(globalVerityDir)) {
22295
23010
  actions.push({
22296
23011
  label: `Remove ~/.verity/ (global credentials \u2014 reconnect requires re-registration)`,
22297
- apply: () => (0, import_node_fs38.rmSync)(globalVerityDir, { recursive: true, force: true })
23012
+ apply: () => (0, import_node_fs42.rmSync)(globalVerityDir, { recursive: true, force: true })
22298
23013
  });
22299
23014
  }
22300
23015
  if (actions.length === 0) {
@@ -22488,7 +23203,7 @@ function registerTaskCommands(program2) {
22488
23203
  }
22489
23204
 
22490
23205
  // src/commands/reset.ts
22491
- var import_node_fs39 = require("node:fs");
23206
+ var import_node_fs43 = require("node:fs");
22492
23207
  var import_node_path31 = require("node:path");
22493
23208
  function registerResetCommand(program2) {
22494
23209
  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) => {
@@ -22526,11 +23241,11 @@ function registerResetCommand(program2) {
22526
23241
  }
22527
23242
  const cacheDir = projectPath(CACHE_DIR);
22528
23243
  let purged = 0;
22529
- if ((0, import_node_fs39.existsSync)(cacheDir)) {
22530
- for (const entry of (0, import_node_fs39.readdirSync)(cacheDir)) {
23244
+ if ((0, import_node_fs43.existsSync)(cacheDir)) {
23245
+ for (const entry of (0, import_node_fs43.readdirSync)(cacheDir)) {
22531
23246
  if (entry.startsWith("pending-")) {
22532
23247
  try {
22533
- (0, import_node_fs39.unlinkSync)((0, import_node_path31.join)(cacheDir, entry));
23248
+ (0, import_node_fs43.unlinkSync)((0, import_node_path31.join)(cacheDir, entry));
22534
23249
  purged++;
22535
23250
  } catch {
22536
23251
  }
@@ -22545,19 +23260,19 @@ function registerResetCommand(program2) {
22545
23260
  projectPath(`${VERITY_DIR}/.last-analysis`)
22546
23261
  ];
22547
23262
  for (const file of filesToClear) {
22548
- if ((0, import_node_fs39.existsSync)(file)) {
23263
+ if ((0, import_node_fs43.existsSync)(file)) {
22549
23264
  try {
22550
- (0, import_node_fs39.writeFileSync)(file, "");
23265
+ (0, import_node_fs43.writeFileSync)(file, "");
22551
23266
  } catch {
22552
23267
  }
22553
23268
  }
22554
23269
  }
22555
23270
  if (opts.all) {
22556
23271
  const logsDir = projectPath(`${VERITY_DIR}/.logs`);
22557
- if ((0, import_node_fs39.existsSync)(logsDir)) {
22558
- for (const entry of (0, import_node_fs39.readdirSync)(logsDir)) {
23272
+ if ((0, import_node_fs43.existsSync)(logsDir)) {
23273
+ for (const entry of (0, import_node_fs43.readdirSync)(logsDir)) {
22559
23274
  try {
22560
- (0, import_node_fs39.unlinkSync)((0, import_node_path31.join)(logsDir, entry));
23275
+ (0, import_node_fs43.unlinkSync)((0, import_node_path31.join)(logsDir, entry));
22561
23276
  } catch {
22562
23277
  }
22563
23278
  }
@@ -22865,8 +23580,8 @@ function registerTelemetryCommands(program2) {
22865
23580
  }
22866
23581
 
22867
23582
  // src/cli.ts
22868
- program.name("verity").description("CLI for Verity quality gate service").version("0.30.1").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) => {
22869
- installStderrLog(actionCommand.name(), process.argv.slice(2), "0.30.1");
23583
+ program.name("verity").description("CLI for Verity quality gate service").version("0.31.0-experimental.6bc3b1b").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) => {
23584
+ installStderrLog(actionCommand.name(), process.argv.slice(2), "0.31.0-experimental.6bc3b1b");
22870
23585
  setUserNamedServiceUrl(program.opts().serviceUrl);
22871
23586
  try {
22872
23587
  await foldLegacyLocalCredential();
@@ -22889,6 +23604,8 @@ registerAnalyzeCommand(program);
22889
23604
  registerBaselineCommands(program);
22890
23605
  registerReviewCommand(program);
22891
23606
  registerGuardCommand(program);
23607
+ registerIgnoreCommand(program);
23608
+ registerWaiveCommand(program);
22892
23609
  registerInitCommand(program);
22893
23610
  registerUninstallCommand(program);
22894
23611
  registerTaskCommands(program);