@codacy/verity-cli 0.30.1 → 0.31.0-experimental.2221878

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;
@@ -14320,6 +14419,7 @@ function reduce(state, events, now) {
14320
14419
  existing.line_sha = ev.line_sha ?? existing.line_sha;
14321
14420
  existing.register = ev.register;
14322
14421
  if (existing.outcome === "fixed") existing.outcome = "recurred";
14422
+ else if (existing.outcome !== "open") existing.outcome = "open";
14323
14423
  break;
14324
14424
  }
14325
14425
  byAnchor.set(ev.anchor_key, {
@@ -16270,7 +16370,145 @@ async function readHookStdin() {
16270
16370
 
16271
16371
  // src/commands/standard.ts
16272
16372
  var import_promises9 = require("node:fs/promises");
16373
+ var import_node_fs20 = require("node:fs");
16273
16374
  var import_yaml = __toESM(require_dist());
16375
+
16376
+ // src/lib/verityignore.ts
16377
+ var import_node_fs19 = require("node:fs");
16378
+ var EMPTY = { rules: [], securityOverlap: [], problems: [] };
16379
+ var SECURITY_PROBES = [
16380
+ ".env",
16381
+ ".env.local",
16382
+ ".env.production",
16383
+ "config/.env",
16384
+ "services/api/.env",
16385
+ "package-lock.json",
16386
+ "yarn.lock",
16387
+ "pnpm-lock.yaml",
16388
+ "Cargo.lock",
16389
+ "go.sum",
16390
+ "Gemfile.lock",
16391
+ "Dockerfile",
16392
+ "docker/Dockerfile"
16393
+ ];
16394
+ function isSecuritySensitive(path) {
16395
+ return SECURITY_PATTERNS.some((p) => p.test(path));
16396
+ }
16397
+ function compile(pattern) {
16398
+ let p = pattern;
16399
+ const dirOnly = p.endsWith("/");
16400
+ if (dirOnly) p = p.slice(0, -1);
16401
+ const anchored = p.includes("/");
16402
+ if (p.startsWith("/")) p = p.slice(1);
16403
+ const base = anchored ? p : `**/${p}`;
16404
+ const forms = dirOnly ? [`${base}/**/*`] : [base, `${base}/**`];
16405
+ const regexes = [];
16406
+ for (const f of forms) {
16407
+ try {
16408
+ regexes.push(globToRegex(f));
16409
+ } catch {
16410
+ }
16411
+ }
16412
+ if (regexes.length === 0) return () => false;
16413
+ return (path) => regexes.some((r) => r.test(path));
16414
+ }
16415
+ function parseVerityIgnore(content) {
16416
+ const rules = [];
16417
+ const problems = [];
16418
+ const lines = content.split("\n");
16419
+ for (let i = 0; i < lines.length; i++) {
16420
+ const lineNo = i + 1;
16421
+ let raw = lines[i];
16422
+ raw = raw.replace(/(?<!\\)\s+$/, "");
16423
+ if (raw.length === 0) continue;
16424
+ if (raw.startsWith("#")) continue;
16425
+ let negated = false;
16426
+ if (raw.startsWith("!")) {
16427
+ negated = true;
16428
+ raw = raw.slice(1);
16429
+ } else if (raw.startsWith("\\!") || raw.startsWith("\\#")) {
16430
+ raw = raw.slice(1);
16431
+ }
16432
+ if (raw.length === 0) {
16433
+ problems.push(`line ${lineNo}: "!" with no pattern after it`);
16434
+ continue;
16435
+ }
16436
+ if (raw === "**" || raw === "*" || raw === "/" || raw === "**/*") {
16437
+ problems.push(
16438
+ `line ${lineNo}: "${raw}" would exclude the whole repository from review \u2014 refused. List the directories you mean instead.`
16439
+ );
16440
+ continue;
16441
+ }
16442
+ rules.push({ raw, line: lineNo, negated, test: compile(raw) });
16443
+ }
16444
+ const byRule = /* @__PURE__ */ new Map();
16445
+ for (const probe of SECURITY_PROBES) {
16446
+ let responsible = null;
16447
+ for (const rule of rules) {
16448
+ if (rule.test(probe)) responsible = rule.negated ? null : rule;
16449
+ }
16450
+ if (!responsible) continue;
16451
+ const entry = byRule.get(responsible.line) ?? { raw: responsible.raw, line: responsible.line, hides: [] };
16452
+ entry.hides.push(probe);
16453
+ byRule.set(responsible.line, entry);
16454
+ }
16455
+ const securityOverlap = [...byRule.values()].sort((a, b) => a.line - b.line);
16456
+ return { rules, securityOverlap, problems };
16457
+ }
16458
+ function decide(rules, path) {
16459
+ let excluded = false;
16460
+ for (const rule of rules) {
16461
+ if (rule.test(path)) excluded = !rule.negated;
16462
+ }
16463
+ return excluded;
16464
+ }
16465
+ function isIgnored(ig, path) {
16466
+ return decide(ig.rules, path);
16467
+ }
16468
+ function loadVerityIgnore() {
16469
+ const file = projectPath(VERITYIGNORE_FILE);
16470
+ if (!(0, import_node_fs19.existsSync)(file)) return EMPTY;
16471
+ try {
16472
+ return parseVerityIgnore((0, import_node_fs19.readFileSync)(file, "utf-8"));
16473
+ } catch {
16474
+ return EMPTY;
16475
+ }
16476
+ }
16477
+ function partitionIgnored(paths, ig) {
16478
+ const suspended = paths.some((p) => p === VERITYIGNORE_FILE);
16479
+ if (suspended || ig.rules.length === 0) {
16480
+ return { rules: ig.rules.length, kept: [...paths], ignored: [], securityExcluded: [], suspended };
16481
+ }
16482
+ const kept = [];
16483
+ const ignored = [];
16484
+ for (const p of paths) {
16485
+ if (p !== VERITYIGNORE_FILE && isIgnored(ig, p)) ignored.push(p);
16486
+ else kept.push(p);
16487
+ }
16488
+ return {
16489
+ rules: ig.rules.length,
16490
+ kept,
16491
+ ignored,
16492
+ securityExcluded: ignored.filter(isSecuritySensitive),
16493
+ suspended: false
16494
+ };
16495
+ }
16496
+ function ignoreShare(kept, ignored) {
16497
+ const total = kept + ignored;
16498
+ if (total === 0) return 0;
16499
+ return Math.round(ignored / total * 1e3) / 1e3;
16500
+ }
16501
+ function describeSecurityOverlap(ig) {
16502
+ if (ig.securityOverlap.length === 0) return null;
16503
+ const lines = ig.securityOverlap.map(
16504
+ (o) => ` line ${o.line}: "${o.raw}" would hide ${o.hides.slice(0, 3).join(", ")}` + (o.hides.length > 3 ? ` (+${o.hides.length - 3} more)` : "")
16505
+ );
16506
+ return `.verityignore: ${ig.securityOverlap.length} pattern(s) can hide security-sensitive files from review:
16507
+ ${lines.join("\n")}
16508
+ These are still excluded \u2014 this is a warning, not a refusal. Add a \`!\` rule to keep one in scope, e.g. \`!.env*\`.`;
16509
+ }
16510
+
16511
+ // src/commands/standard.ts
16274
16512
  function registerStandardCommands(program2) {
16275
16513
  const standard = program2.command("standard").description("Manage the project Standard");
16276
16514
  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 +16537,25 @@ function registerStandardCommands(program2) {
16299
16537
  printError(`Invalid YAML in ${opts.file}: ${err.message}`);
16300
16538
  process.exit(1);
16301
16539
  }
16540
+ let ignoreRaw = null;
16541
+ const ignorePath = projectPath(VERITYIGNORE_FILE);
16542
+ if ((0, import_node_fs20.existsSync)(ignorePath)) {
16543
+ try {
16544
+ ignoreRaw = (0, import_node_fs20.readFileSync)(ignorePath, "utf-8");
16545
+ const overlap = describeSecurityOverlap(parseVerityIgnore(ignoreRaw));
16546
+ if (overlap) printWarn(overlap);
16547
+ } catch {
16548
+ }
16549
+ }
16302
16550
  const result = await apiRequest({
16303
16551
  method: "POST",
16304
16552
  path: "/standards",
16305
16553
  serviceUrl: urlResult.data,
16306
16554
  token: tokenResult.data.token,
16307
- body: { content, created_by: opts.createdBy },
16555
+ body: {
16556
+ content: ignoreRaw === null ? content : { ...content, verityignore: ignoreRaw },
16557
+ created_by: opts.createdBy
16558
+ },
16308
16559
  verbose: globals.verbose
16309
16560
  });
16310
16561
  if (!result.ok) {
@@ -16381,6 +16632,15 @@ function registerStandardCommands(program2) {
16381
16632
  var import_promises10 = require("node:fs/promises");
16382
16633
  function registerConfigCommands(program2) {
16383
16634
  const config = program2.command("config").description("Manage analysis configuration");
16635
+ config.command("service-url").description("Print the resolved service URL (used by the shipped skills)").action(async () => {
16636
+ const globals = program2.opts();
16637
+ const urlResult = await resolveServiceUrl(globals.serviceUrl);
16638
+ if (!urlResult.ok) {
16639
+ printError(urlResult.error);
16640
+ process.exit(1);
16641
+ }
16642
+ process.stdout.write(urlResult.data + "\n");
16643
+ });
16384
16644
  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
16645
  const globals = program2.opts();
16386
16646
  const tokenResult = await resolveToken(globals.token);
@@ -16522,99 +16782,402 @@ function formatRunDetail(run) {
16522
16782
  return lines;
16523
16783
  }
16524
16784
 
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`;
16785
+ // src/lib/ignore-declaration.ts
16786
+ var import_node_fs22 = require("node:fs");
16787
+
16788
+ // src/lib/debounce.ts
16789
+ var import_node_fs21 = require("node:fs");
16790
+ var import_node_crypto10 = require("node:crypto");
16791
+ function scopedFile(base, sessionId) {
16792
+ if (!sessionId) return base;
16793
+ return `${base}.${(0, import_node_crypto10.createHash)("sha1").update(sessionId).digest("hex").slice(0, 12)}`;
16535
16794
  }
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);
16795
+ function checkDebounce(debounceSeconds = DEBOUNCE_SECONDS, sessionId) {
16796
+ const file = scopedFile(DEBOUNCE_FILE, sessionId);
16797
+ if (!(0, import_node_fs21.existsSync)(file)) return null;
16798
+ try {
16799
+ const lastTs = parseInt((0, import_node_fs21.readFileSync)(file, "utf-8").trim(), 10);
16800
+ const nowTs = Math.floor(Date.now() / 1e3);
16801
+ const elapsed = nowTs - lastTs;
16802
+ if (elapsed < debounceSeconds) {
16803
+ return `Debounced \u2014 last analysis was ${elapsed}s ago`;
16563
16804
  }
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
- }
16805
+ } catch {
16806
+ }
16807
+ return null;
16808
+ }
16809
+ function checkMtime(files, bypassForRecentCommits, sessionId) {
16810
+ if (bypassForRecentCommits) return null;
16811
+ const file = scopedFile(DEBOUNCE_FILE, sessionId);
16812
+ if (!(0, import_node_fs21.existsSync)(file)) return null;
16813
+ let debounceTime;
16814
+ try {
16815
+ debounceTime = (0, import_node_fs21.statSync)(file).mtimeMs;
16816
+ } catch {
16817
+ return null;
16818
+ }
16819
+ for (const f of files) {
16820
+ const resolved = resolveFile(f);
16821
+ if (!resolved) continue;
16822
+ try {
16823
+ const stat3 = (0, import_node_fs21.statSync)(resolved);
16824
+ if (stat3.mtimeMs > debounceTime) {
16825
+ return null;
16584
16826
  }
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})`);
16827
+ } catch {
16828
+ continue;
16601
16829
  }
16602
- if (who.ok) {
16603
- const nudge = reverifyNudge(who.data);
16604
- if (nudge) printWarn(` ${nudge}`);
16830
+ }
16831
+ return "No files modified since last analysis";
16832
+ }
16833
+ function computeContentHash(files) {
16834
+ const hash = (0, import_node_crypto10.createHash)("sha1");
16835
+ const sorted = [...files].sort();
16836
+ for (const f of sorted) {
16837
+ const resolved = resolveFile(f) ?? f;
16838
+ try {
16839
+ if ((0, import_node_fs21.existsSync)(resolved)) {
16840
+ hash.update((0, import_node_fs21.readFileSync)(resolved));
16841
+ }
16842
+ } catch {
16605
16843
  }
16606
- if (denial) {
16607
- printWarn(`Access: ${denial.remedy}`);
16608
- printInfo(" Project status, history, and cloud memory stay unavailable until then.");
16844
+ }
16845
+ return hash.digest("hex");
16846
+ }
16847
+ function checkContentHash(files, sessionId) {
16848
+ const hash = computeContentHash(files);
16849
+ const file = scopedFile(HASH_FILE, sessionId);
16850
+ if ((0, import_node_fs21.existsSync)(file)) {
16851
+ try {
16852
+ const storedHash = (0, import_node_fs21.readFileSync)(file, "utf-8").trim();
16853
+ if (hash === storedHash) {
16854
+ return { skip: "No source changes since last analysis", hash };
16855
+ }
16856
+ } catch {
16609
16857
  }
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(", ")}`);
16858
+ }
16859
+ return { skip: null, hash };
16860
+ }
16861
+ function recordAnalysisStart(sessionId) {
16862
+ (0, import_node_fs21.mkdirSync)(VERITY_DIR, { recursive: true });
16863
+ (0, import_node_fs21.writeFileSync)(scopedFile(DEBOUNCE_FILE, sessionId), String(Math.floor(Date.now() / 1e3)));
16864
+ }
16865
+ function recordPassHash(hash, sessionId) {
16866
+ (0, import_node_fs21.writeFileSync)(scopedFile(HASH_FILE, sessionId), hash);
16867
+ }
16868
+ function narrowToRecent(files, sessionId) {
16869
+ const file = scopedFile(DEBOUNCE_FILE, sessionId);
16870
+ if (!(0, import_node_fs21.existsSync)(file)) return files;
16871
+ let debounceTime;
16872
+ try {
16873
+ debounceTime = (0, import_node_fs21.statSync)(file).mtimeMs;
16874
+ } catch {
16875
+ return files;
16876
+ }
16877
+ const recent = files.filter((f) => {
16878
+ try {
16879
+ return (0, import_node_fs21.existsSync)(f) && (0, import_node_fs21.statSync)(f).mtimeMs > debounceTime;
16880
+ } catch {
16881
+ return false;
16615
16882
  }
16616
- const hookStatus = await checkAllVerityHooks();
16617
- const moments = [];
16883
+ });
16884
+ return recent.length > 0 ? recent : files;
16885
+ }
16886
+ function readIteration(currentCommit, _contentHash) {
16887
+ return Math.max(1, readBlockState(currentCommit).attempts);
16888
+ }
16889
+ var NO_BLOCKS = { attempts: 0, blocks: 0, fingerprint: null };
16890
+ function readBlockState(currentCommit, opts) {
16891
+ if (opts?.newUserPrompt) return NO_BLOCKS;
16892
+ if (!(0, import_node_fs21.existsSync)(ITERATION_FILE)) return NO_BLOCKS;
16893
+ try {
16894
+ const stored = (0, import_node_fs21.readFileSync)(ITERATION_FILE, "utf-8").trim();
16895
+ const parsed = stored.startsWith("{") ? parseJsonState(stored) : parseLegacyState(stored);
16896
+ if (!parsed) return NO_BLOCKS;
16897
+ if (parsed.commit !== currentCommit) return NO_BLOCKS;
16898
+ if (parsed.ts > 0 && Math.floor(Date.now() / 1e3) - parsed.ts > 600) return NO_BLOCKS;
16899
+ return { attempts: parsed.attempts, blocks: parsed.blocks, fingerprint: parsed.fingerprint };
16900
+ } catch {
16901
+ return NO_BLOCKS;
16902
+ }
16903
+ }
16904
+ function parseJsonState(raw) {
16905
+ const o = JSON.parse(raw);
16906
+ const attempts = typeof o.attempts === "number" ? o.attempts : NaN;
16907
+ if (isNaN(attempts)) return null;
16908
+ return {
16909
+ attempts,
16910
+ blocks: typeof o.blocks === "number" ? o.blocks : attempts,
16911
+ fingerprint: typeof o.fingerprint === "string" && o.fingerprint ? o.fingerprint : null,
16912
+ commit: typeof o.commit === "string" ? o.commit : "",
16913
+ ts: typeof o.ts === "number" ? o.ts : 0
16914
+ };
16915
+ }
16916
+ function parseLegacyState(raw) {
16917
+ const parts = raw.split(":");
16918
+ const n = parseInt(parts[0], 10);
16919
+ if (isNaN(n)) return null;
16920
+ return {
16921
+ attempts: n,
16922
+ // The old file has no separate block count; the old counter is the closest
16923
+ // honest answer, and it errs toward releasing sooner rather than later.
16924
+ blocks: n,
16925
+ fingerprint: parts.slice(3).join(":") || null,
16926
+ commit: parts[1] ?? "",
16927
+ ts: parseInt(parts[2] ?? "0", 10)
16928
+ };
16929
+ }
16930
+ function findingsFingerprint(findings) {
16931
+ const keys = findings.map((f) => `${String(f.pattern_id ?? "?")}|${String(f.file ?? "?")}`).filter((k) => k !== "?|?");
16932
+ return [...new Set(keys)].sort().join(",");
16933
+ }
16934
+ function isSameProblem(previous, current) {
16935
+ if (!previous || !current) return false;
16936
+ const prev = new Set(previous.split(","));
16937
+ return current.split(",").some((k) => prev.has(k));
16938
+ }
16939
+ function writeBlockState(commit, state) {
16940
+ (0, import_node_fs21.mkdirSync)(VERITY_DIR, { recursive: true });
16941
+ (0, import_node_fs21.writeFileSync)(
16942
+ ITERATION_FILE,
16943
+ JSON.stringify({
16944
+ v: 2,
16945
+ attempts: state.attempts,
16946
+ blocks: state.blocks,
16947
+ commit,
16948
+ ts: Math.floor(Date.now() / 1e3),
16949
+ fingerprint: state.fingerprint ?? void 0
16950
+ })
16951
+ );
16952
+ }
16953
+ function resetBlockState(commit) {
16954
+ writeBlockState(commit, { attempts: 0, blocks: 0, fingerprint: null });
16955
+ }
16956
+
16957
+ // src/lib/ignore-declaration.ts
16958
+ var IGNORE_BUDGET = 3;
16959
+ var MAX_WINDOW_SECONDS = 60 * 60;
16960
+ var TURN_FUSE_SECONDS = 10 * 60;
16961
+ function parseDuration(input) {
16962
+ const trimmed = input.trim().toLowerCase();
16963
+ const m = /^(\d+)(s|m|h)?$/.exec(trimmed);
16964
+ if (!m) {
16965
+ return { ok: false, error: `Could not read "${input}" as a duration. Use 30m, 45s, or 1h.` };
16966
+ }
16967
+ const n = parseInt(m[1], 10);
16968
+ if (!Number.isFinite(n) || n <= 0) {
16969
+ return { ok: false, error: `A duration must be a positive number of seconds, minutes or hours \u2014 got "${input}".` };
16970
+ }
16971
+ const unit = m[2] ?? "m";
16972
+ const seconds = unit === "s" ? n : unit === "h" ? n * 3600 : n * 60;
16973
+ if (seconds > MAX_WINDOW_SECONDS) {
16974
+ return {
16975
+ ok: false,
16976
+ 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.`
16977
+ };
16978
+ }
16979
+ return { ok: true, seconds };
16980
+ }
16981
+ function resolveActive(state, now) {
16982
+ if (!state?.active) return null;
16983
+ if (state.active.expires <= now) return null;
16984
+ return state.active;
16985
+ }
16986
+ function verifyDeclaration(input) {
16987
+ const d = input.declaration;
16988
+ if (!d) return { honoured: false, void: false };
16989
+ if (!input.authorshipIsObservable) {
16990
+ return {
16991
+ honoured: false,
16992
+ void: true,
16993
+ why: "the transcript window could not show what this turn did, so the declaration could not be checked"
16994
+ };
16995
+ }
16996
+ if (input.commandRecordTruncated) {
16997
+ return {
16998
+ honoured: false,
16999
+ void: true,
17000
+ why: "the record of commands run this turn was incomplete, so the declaration could not be checked"
17001
+ };
17002
+ }
17003
+ if (input.agentAuthoredFiles > 0) {
17004
+ return {
17005
+ honoured: false,
17006
+ void: true,
17007
+ why: `${input.agentAuthoredFiles} file${input.agentAuthoredFiles === 1 ? " was" : "s were"} authored during it`
17008
+ };
17009
+ }
17010
+ if (input.subagents > 0) {
17011
+ return {
17012
+ honoured: false,
17013
+ void: true,
17014
+ why: "work was delegated to a subagent during it, whose authorship this turn cannot account for"
17015
+ };
17016
+ }
17017
+ const authoring = [...input.agentCommands ?? [], ...input.userCommands ?? []].filter(
17018
+ (c) => !isNonAuthoringCommand(c)
17019
+ );
17020
+ if (authoring.length > 0) {
17021
+ return {
17022
+ honoured: false,
17023
+ void: true,
17024
+ why: `a command that can write files ran during it (${authoring[0]})`
17025
+ };
17026
+ }
17027
+ return { honoured: true };
17028
+ }
17029
+ function stateFile(sessionId) {
17030
+ return projectPath(scopedFile(IGNORE_DECLARATION_FILE, sessionId));
17031
+ }
17032
+ function ignoreStateKeys(token, sessionId) {
17033
+ const scoped = sessionScopeKey(token, sessionId);
17034
+ const userOnly = sessionScopeKey(token, void 0);
17035
+ return scoped === userOnly ? [scoped] : [scoped, userOnly];
17036
+ }
17037
+ function resolveIgnoreState(keys) {
17038
+ for (const key of keys) {
17039
+ const state = readIgnoreState(key);
17040
+ if (state) return { state, key };
17041
+ }
17042
+ return null;
17043
+ }
17044
+ function readIgnoreState(sessionId) {
17045
+ const file = stateFile(sessionId);
17046
+ if (!(0, import_node_fs22.existsSync)(file)) return null;
17047
+ try {
17048
+ const o = JSON.parse((0, import_node_fs22.readFileSync)(file, "utf-8")) ?? {};
17049
+ const spent = typeof o.spent === "number" ? o.spent : 0;
17050
+ const raw = o.active;
17051
+ let active = null;
17052
+ if (raw && typeof raw === "object") {
17053
+ const scope2 = raw.scope === "window" ? "window" : raw.scope === "turn" ? "turn" : null;
17054
+ const expires = typeof raw.expires === "number" ? raw.expires : null;
17055
+ if (scope2 && expires != null) {
17056
+ active = {
17057
+ scope: scope2,
17058
+ origin: raw.origin === "agent" ? "agent" : "user",
17059
+ reason: typeof raw.reason === "string" ? raw.reason : "",
17060
+ at: typeof raw.at === "number" ? raw.at : 0,
17061
+ expires
17062
+ };
17063
+ }
17064
+ }
17065
+ return { v: 1, active, spent };
17066
+ } catch {
17067
+ return null;
17068
+ }
17069
+ }
17070
+ function writeIgnoreState(state, sessionId) {
17071
+ try {
17072
+ (0, import_node_fs22.mkdirSync)(projectPath(VERITY_DIR), { recursive: true });
17073
+ (0, import_node_fs22.writeFileSync)(stateFile(sessionId), JSON.stringify({ v: 1, active: state.active, spent: state.spent }));
17074
+ } catch {
17075
+ }
17076
+ }
17077
+ function clearActiveDeclaration(sessionId) {
17078
+ const prev = readIgnoreState(sessionId);
17079
+ if (!prev) return;
17080
+ writeIgnoreState({ v: 1, active: null, spent: prev.spent }, sessionId);
17081
+ }
17082
+ function describeRemaining(d, now) {
17083
+ const left = Math.max(0, d.expires - now);
17084
+ if (left >= 90) return `${Math.round(left / 60)}m left`;
17085
+ return `${left}s left`;
17086
+ }
17087
+
17088
+ // src/commands/status.ts
17089
+ function timeAgo(isoDate) {
17090
+ const ms = Date.now() - new Date(isoDate).getTime();
17091
+ const mins = Math.floor(ms / 6e4);
17092
+ if (mins < 1) return "just now";
17093
+ if (mins < 60) return `${mins}m ago`;
17094
+ const hrs = Math.floor(mins / 60);
17095
+ if (hrs < 24) return `${hrs}h ago`;
17096
+ const days = Math.floor(hrs / 24);
17097
+ return `${days}d ago`;
17098
+ }
17099
+ function registerStatusCommand(program2) {
17100
+ 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) => {
17101
+ const globals = program2.opts();
17102
+ const tokenResult = await resolveToken(globals.token);
17103
+ if (!tokenResult.ok) {
17104
+ printError(tokenResult.error);
17105
+ process.exit(1);
17106
+ }
17107
+ const urlResult = await resolveServiceUrl(globals.serviceUrl);
17108
+ if (!urlResult.ok) {
17109
+ printError(urlResult.error);
17110
+ process.exit(1);
17111
+ }
17112
+ const token = tokenResult.data.token;
17113
+ const serviceUrl = urlResult.data;
17114
+ const who = await whoami(token, serviceUrl, globals.verbose);
17115
+ const memResult = await apiRequest({
17116
+ method: "GET",
17117
+ path: "/memory",
17118
+ serviceUrl,
17119
+ token,
17120
+ verbose: globals.verbose
17121
+ });
17122
+ const denial = memResult.ok ? null : authDenialRemedy(memResult.error);
17123
+ if (!memResult.ok && !denial) {
17124
+ printError(memResult.error);
17125
+ process.exit(1);
17126
+ }
17127
+ const mem = memResult.ok ? memResult.data : null;
17128
+ if (opts.json) {
17129
+ const output = {
17130
+ auth: who.ok ? who.data : { error: who.error },
17131
+ memory: mem
17132
+ };
17133
+ if (denial && !memResult.ok) {
17134
+ output.error = { code: denial.code, message: memResult.error, remedy: denial.remedy };
17135
+ }
17136
+ if (opts.history && !denial) {
17137
+ const runsResult = await apiRequest({
17138
+ method: "GET",
17139
+ path: `/runs?limit=${opts.limit}`,
17140
+ serviceUrl,
17141
+ token,
17142
+ verbose: globals.verbose
17143
+ });
17144
+ if (runsResult.ok) {
17145
+ output.runs = runsResult.data.runs;
17146
+ }
17147
+ }
17148
+ printJson(output);
17149
+ return;
17150
+ }
17151
+ if (mem?.configured === false) {
17152
+ printInfo("Verity is not configured for this project. Run /verity-setup.");
17153
+ return;
17154
+ }
17155
+ printInfo("=== Verity Status ===");
17156
+ if (who.ok && who.data.logged_in) {
17157
+ printInfo(`Account: Logged in as ${who.data.email ?? `user #${who.data.user_id}`} \u2713`);
17158
+ } else if (who.ok && who.data.anonymous) {
17159
+ printInfo('Account: Anonymous \u2014 runs not saved, no cloud memory. Run "verity login".');
17160
+ } else if (tokenResult.data.userId != null) {
17161
+ printInfo(`Account: Logged in as ${tokenResult.data.email ?? `user #${tokenResult.data.userId}`} (cached \u2014 could not reach the Verity service) \u2713`);
17162
+ } else if (!who.ok) {
17163
+ printInfo(`Account: Unknown \u2014 could not reach the Verity service (${who.error})`);
17164
+ }
17165
+ if (who.ok) {
17166
+ const nudge = reverifyNudge(who.data);
17167
+ if (nudge) printWarn(` ${nudge}`);
17168
+ }
17169
+ if (denial) {
17170
+ printWarn(`Access: ${denial.remedy}`);
17171
+ printInfo(" Project status, history, and cloud memory stay unavailable until then.");
17172
+ }
17173
+ if (mem?.project_name) printInfo(`Project: ${mem.project_name}`);
17174
+ if (mem?.standard) {
17175
+ const s = mem.standard;
17176
+ printInfo(`Standard: v${s.version} (${s.quality_dimensions} quality, ${s.security_patterns} security, ${s.custom_patterns} custom)`);
17177
+ printInfo(`Languages: ${s.languages.join(", ")}`);
17178
+ }
17179
+ const hookStatus = await checkAllVerityHooks();
17180
+ const moments = [];
16618
17181
  if (hookStatus.stop) moments.push("stop");
16619
17182
  if (hookStatus.guardOn.includes("commit")) moments.push("pre-commit");
16620
17183
  if (hookStatus.guardOn.includes("push")) moments.push("pre-push/PR");
@@ -16630,11 +17193,45 @@ function registerStatusCommand(program2) {
16630
17193
  printInfo(`Trend: ${r.trend}`);
16631
17194
  printInfo(`Runs: ${r.count} recorded`);
16632
17195
  }
17196
+ {
17197
+ const tokenForScope = tokenResult.data.token;
17198
+ const found = resolveIgnoreState(
17199
+ ignoreStateKeys(tokenForScope, process.env.CLAUDE_SESSION_ID || void 0)
17200
+ );
17201
+ const now = Math.floor(Date.now() / 1e3);
17202
+ const active = resolveActive(found?.state ?? null, now);
17203
+ const spent = found?.state.spent ?? 0;
17204
+ if (active) {
17205
+ printInfo("");
17206
+ printInfo("--- Ignore Declared ---");
17207
+ printInfo(`Scope: ${active.scope === "turn" ? "the next turn" : `a window, ${describeRemaining(active, now)}`}`);
17208
+ printInfo(`Reason: ${active.reason}`);
17209
+ printInfo(`Declared by: ${active.origin === "agent" ? "the agent" : "you"}`);
17210
+ printInfo(`Budget: ${spent}/${IGNORE_BUDGET} declarations this session`);
17211
+ printInfo('Turns that author anything are still reviewed \u2014 the declaration voids. "verity ignore clear" cancels it.');
17212
+ } else if (spent > 0) {
17213
+ printInfo("");
17214
+ printInfo(`Ignore budget: ${spent}/${IGNORE_BUDGET} declarations used this session.`);
17215
+ }
17216
+ }
17217
+ {
17218
+ const ig = loadVerityIgnore();
17219
+ if (ig.rules.length > 0 || ig.problems.length > 0) {
17220
+ printInfo("");
17221
+ printInfo("--- .verityignore ---");
17222
+ printInfo(`Rules: ${ig.rules.length}`);
17223
+ for (const problem of ig.problems) printWarn(` ${problem}`);
17224
+ const overlap = describeSecurityOverlap(ig);
17225
+ if (overlap) printWarn(overlap);
17226
+ printInfo("An edit to .verityignore suspends every rule for that turn \u2014 the edit is always reviewed.");
17227
+ }
17228
+ }
16633
17229
  if (mem.pending_items && mem.pending_items.length > 0) {
16634
17230
  printInfo("");
16635
17231
  printInfo("--- Pending Items ---");
16636
17232
  for (const item of mem.pending_items) {
16637
- printInfo(` [${item.priority.toUpperCase()}] ${item.description}`);
17233
+ const priority = typeof item.priority === "string" && item.priority.length > 0 ? item.priority.toUpperCase() : "UNSPECIFIED";
17234
+ printInfo(` [${priority}] ${item.description ?? "(no description)"}`);
16638
17235
  }
16639
17236
  }
16640
17237
  const recentTasks = mem.recent_tasks;
@@ -16793,6 +17390,7 @@ function createRun(opts, globals) {
16793
17390
  allChanged: [],
16794
17391
  hasRecentCommitFiles: false,
16795
17392
  changedUniverse: [],
17393
+ verityIgnored: { rules: 0, kept: [], ignored: [], securityExcluded: [], suspended: false },
16796
17394
  analyzable: [],
16797
17395
  reviewable: [],
16798
17396
  securityFiles: [],
@@ -16826,6 +17424,7 @@ function createRun(opts, globals) {
16826
17424
  deletedNodePaths: [],
16827
17425
  editedUploads: [],
16828
17426
  autoSeedNotice: null,
17427
+ voidedIgnoreNotice: null,
16829
17428
  foldResult: null,
16830
17429
  foldConservation: null,
16831
17430
  memorySession: null,
@@ -16846,7 +17445,7 @@ function createRun(opts, globals) {
16846
17445
  }
16847
17446
 
16848
17447
  // src/lib/stderr-log.ts
16849
- var import_node_fs19 = require("node:fs");
17448
+ var import_node_fs23 = require("node:fs");
16850
17449
  var TOKEN_RE2 = /verity_[0-9a-f]{16,}/g;
16851
17450
  var ANSI_RE = /\u001b\[[0-?]*[ -/]*[@-~]/g;
16852
17451
  function scrub(s) {
@@ -16859,9 +17458,9 @@ function append(text) {
16859
17458
  try {
16860
17459
  const dir = projectPath(DEBUG_LOG_DIR);
16861
17460
  const file = projectPath(STDERR_LOG_FILE);
16862
- (0, import_node_fs19.mkdirSync)(dir, { recursive: true });
17461
+ (0, import_node_fs23.mkdirSync)(dir, { recursive: true });
16863
17462
  rotateIfNeeded(file);
16864
- (0, import_node_fs19.appendFileSync)(file, text);
17463
+ (0, import_node_fs23.appendFileSync)(file, text);
16865
17464
  } catch {
16866
17465
  }
16867
17466
  }
@@ -17019,7 +17618,7 @@ function installRunEvidence(run) {
17019
17618
 
17020
17619
  // src/lib/git-frame.ts
17021
17620
  var import_node_child_process7 = require("node:child_process");
17022
- var import_node_fs20 = require("node:fs");
17621
+ var import_node_fs24 = require("node:fs");
17023
17622
  var import_node_os3 = require("node:os");
17024
17623
  var import_node_path18 = require("node:path");
17025
17624
  var import_node_path19 = require("node:path");
@@ -17158,14 +17757,14 @@ function gitAt(dir, args) {
17158
17757
  }
17159
17758
  function realpathOr(p) {
17160
17759
  try {
17161
- return import_node_fs20.realpathSync.native(p);
17760
+ return import_node_fs24.realpathSync.native(p);
17162
17761
  } catch {
17163
17762
  return (0, import_node_path18.resolve)(p);
17164
17763
  }
17165
17764
  }
17166
17765
  function resolveFrame(input) {
17167
17766
  const found = findMomentSegment(input.command, input.on);
17168
- const hookDirUsable = !!input.hookCwd && (0, import_node_fs20.existsSync)(input.hookCwd);
17767
+ const hookDirUsable = !!input.hookCwd && (0, import_node_fs24.existsSync)(input.hookCwd);
17169
17768
  const baseDir = hookDirUsable ? input.hookCwd : process.cwd();
17170
17769
  let anchor = hookDirUsable ? "hook-cwd" : "process-cwd";
17171
17770
  const refuse = (refusal) => ({
@@ -17188,7 +17787,7 @@ function resolveFrame(input) {
17188
17787
  if (dirs.size > 1) return refuse(`target:multiple ${found.moment} targets in one command`);
17189
17788
  const targetDir = dirs.size === 1 ? [...dirs][0] : baseDir;
17190
17789
  if (targetDir !== baseDir) {
17191
- if (!(0, import_node_fs20.existsSync)(targetDir)) return refuse(`target:directory does not exist: ${targetDir}`);
17790
+ if (!(0, import_node_fs24.existsSync)(targetDir)) return refuse(`target:directory does not exist: ${targetDir}`);
17192
17791
  dir = targetDir;
17193
17792
  }
17194
17793
  }
@@ -17227,7 +17826,7 @@ var SHA_RE2 = /^[0-9a-f]{40}$/;
17227
17826
  function baselineShaAt(frame) {
17228
17827
  if (!frame.worktreeRoot) return null;
17229
17828
  try {
17230
- const sha = (0, import_node_fs20.readFileSync)((0, import_node_path19.join)(frame.worktreeRoot, BASELINE_SHA_FILE), "utf-8").trim();
17829
+ const sha = (0, import_node_fs24.readFileSync)((0, import_node_path19.join)(frame.worktreeRoot, BASELINE_SHA_FILE), "utf-8").trim();
17231
17830
  if (!SHA_RE2.test(sha)) return null;
17232
17831
  return refResolves(frame, sha) ? sha : null;
17233
17832
  } catch {
@@ -17340,7 +17939,7 @@ function truthy(v) {
17340
17939
  }
17341
17940
 
17342
17941
  // src/lib/transcript.ts
17343
- var import_node_fs21 = require("node:fs");
17942
+ var import_node_fs25 = require("node:fs");
17344
17943
  var MAX_READ_BYTES = 256 * 1024;
17345
17944
  var SMALL_FILE_BYTES = 64 * 1024;
17346
17945
  var MAX_FILES_LIST = 20;
@@ -17366,7 +17965,7 @@ async function extractActionSummary(transcriptPath) {
17366
17965
  function readTurnLines(transcriptPath) {
17367
17966
  let size;
17368
17967
  try {
17369
- size = (0, import_node_fs21.statSync)(transcriptPath).size;
17968
+ size = (0, import_node_fs25.statSync)(transcriptPath).size;
17370
17969
  } catch {
17371
17970
  return null;
17372
17971
  }
@@ -17374,7 +17973,7 @@ function readTurnLines(transcriptPath) {
17374
17973
  let raw;
17375
17974
  let windowed = false;
17376
17975
  if (size <= SMALL_FILE_BYTES) {
17377
- raw = (0, import_node_fs21.readFileSync)(transcriptPath, "utf-8");
17976
+ raw = (0, import_node_fs25.readFileSync)(transcriptPath, "utf-8");
17378
17977
  } else {
17379
17978
  windowed = true;
17380
17979
  const buf = Buffer.alloc(Math.min(MAX_READ_BYTES, size));
@@ -17432,6 +18031,7 @@ function buildSummary(lines) {
17432
18031
  const commands = [];
17433
18032
  const userCommands = [];
17434
18033
  let userCommandsTruncated = false;
18034
+ let commandsTruncated = false;
17435
18035
  let searches = 0;
17436
18036
  let subagents = 0;
17437
18037
  let webFetches = 0;
@@ -17472,7 +18072,10 @@ function buildSummary(lines) {
17472
18072
  totalToolCalls++;
17473
18073
  const toolName = block.name ?? "unknown";
17474
18074
  toolCounts[toolName] = (toolCounts[toolName] ?? 0) + 1;
17475
- if (totalToolCalls > MAX_TOOL_BLOCKS) continue;
18075
+ if (totalToolCalls > MAX_TOOL_BLOCKS) {
18076
+ if (toolName === "Bash") commandsTruncated = true;
18077
+ continue;
18078
+ }
17476
18079
  const input = block.input ?? {};
17477
18080
  switch (toolName) {
17478
18081
  case "Read":
@@ -17492,9 +18095,11 @@ function buildSummary(lines) {
17492
18095
  addPath(filesEdited, input.file_path);
17493
18096
  break;
17494
18097
  case "Bash": {
17495
- const cmd = sanitizeCommand(input.command);
17496
- if (cmd && commands.length < MAX_COMMANDS) {
17497
- commands.push(cmd);
18098
+ const { cmd, lost } = sanitizeCommandWithLoss(input.command);
18099
+ if (lost) commandsTruncated = true;
18100
+ if (cmd) {
18101
+ if (commands.length < MAX_COMMANDS) commands.push(cmd);
18102
+ else commandsTruncated = true;
17498
18103
  }
17499
18104
  break;
17500
18105
  }
@@ -17537,6 +18142,7 @@ function buildSummary(lines) {
17537
18142
  ],
17538
18143
  searches,
17539
18144
  commands,
18145
+ ...commandsTruncated ? { commands_truncated: true } : {},
17540
18146
  user_commands: userCommands,
17541
18147
  ...userCommandsTruncated ? { user_commands_truncated: true } : {},
17542
18148
  subagents,
@@ -17547,6 +18153,7 @@ function buildSummary(lines) {
17547
18153
  };
17548
18154
  if (JSON.stringify(summary).length > MAX_SUMMARY_BYTES) {
17549
18155
  summary.commands = [];
18156
+ summary.commands_truncated = true;
17550
18157
  summary.user_commands = [];
17551
18158
  summary.user_commands_truncated = true;
17552
18159
  if (JSON.stringify(summary).length > MAX_SUMMARY_BYTES) {
@@ -17582,12 +18189,22 @@ function userTypedCommands(entry) {
17582
18189
  }
17583
18190
  return { commands, truncated };
17584
18191
  }
18192
+ function sanitizeCommandWithLoss(rawCmd) {
18193
+ if (typeof rawCmd !== "string" || !rawCmd) return { cmd: null, lost: false };
18194
+ const lines = rawCmd.split("\n");
18195
+ const lost = lines.slice(1).some((l) => l.trim().length > 0);
18196
+ const first = lines[0];
18197
+ const cmd = sanitizeCommand(first);
18198
+ const hadSeparator = SEPARATORS.some((sep2) => first.indexOf(sep2) > 0);
18199
+ return { cmd, lost: lost || !hadSeparator && first.length > MAX_COMMAND_CHARS };
18200
+ }
18201
+ var SEPARATORS = [" | ", " > ", " >> ", " 2>", " && ", " ; "];
17585
18202
  function sanitizeCommand(rawCmd) {
17586
18203
  if (typeof rawCmd !== "string" || !rawCmd) return null;
17587
18204
  let cmd = rawCmd.split("\n")[0];
17588
18205
  let cut = -1;
17589
18206
  let marker = "";
17590
- for (const sep2 of [" | ", " > ", " >> ", " 2>", " && ", " ; "]) {
18207
+ for (const sep2 of SEPARATORS) {
17591
18208
  const idx = cmd.indexOf(sep2);
17592
18209
  if (idx > 0 && (cut === -1 || idx < cut)) {
17593
18210
  cut = idx;
@@ -17837,7 +18454,7 @@ function channelSilence(input) {
17837
18454
  // src/lib/cli-version.ts
17838
18455
  function cliVersion() {
17839
18456
  try {
17840
- return true ? "0.30.1" : "dev";
18457
+ return true ? "0.31.0-experimental.2221878" : "dev";
17841
18458
  } catch {
17842
18459
  return "dev";
17843
18460
  }
@@ -17878,7 +18495,7 @@ async function sendSkipBeacon(ctx, reason) {
17878
18495
 
17879
18496
  // src/lib/static-analysis.ts
17880
18497
  var import_node_child_process8 = require("node:child_process");
17881
- var import_node_fs22 = require("node:fs");
18498
+ var import_node_fs26 = require("node:fs");
17882
18499
  var SEVERITY_ORDER = {
17883
18500
  Error: 0,
17884
18501
  Critical: 0,
@@ -17926,7 +18543,7 @@ function runCodacyAnalysis(files) {
17926
18543
  if (files.length === 0) return empty;
17927
18544
  const existingFiles = files.filter((f) => {
17928
18545
  try {
17929
- return (0, import_node_fs22.existsSync)(f);
18546
+ return (0, import_node_fs26.existsSync)(f);
17930
18547
  } catch {
17931
18548
  return false;
17932
18549
  }
@@ -18087,9 +18704,10 @@ function describeOpenElsewhere(open) {
18087
18704
  const lines = open.slice(0, 5).map((o) => ` ${o.file}:${o.line} [${o.pattern_id}]`);
18088
18705
  const more = open.length > 5 ? `
18089
18706
  (+${open.length - 5} more)` : "";
18090
- return `STILL OPEN ELSEWHERE. ${open.length} blocking finding(s) Verity raised earlier are still present in files this run did not review:
18707
+ return `STILL OPEN ELSEWHERE. ${open.length} finding(s) Verity raised earlier are still on disk in files this run did not review:
18091
18708
  ${lines.join("\n")}${more}
18092
- This verdict covers the current change only. The tree is not clean.`;
18709
+ They did not gate this run \u2014 this verdict covers the current change only. The tree is not clean.
18710
+ Fix them, or record a disposition: verity waive <pattern-id> --file <path> --reason "\u2026"`;
18093
18711
  }
18094
18712
 
18095
18713
  // src/commands/analyze/exit.ts
@@ -18124,6 +18742,7 @@ async function passAndExit(run, reason, skip, kindOverride) {
18124
18742
  "bare-acknowledgment",
18125
18743
  "reflection-prompt",
18126
18744
  "command-only-turn",
18745
+ "declared-ignore",
18127
18746
  "skip-mode",
18128
18747
  "zero-increment",
18129
18748
  "debounce",
@@ -18163,19 +18782,37 @@ async function scope(run) {
18163
18782
  const { files: allChanged, hasRecentCommitFiles } = getChangedFiles();
18164
18783
  run.changedUniverse = allChanged;
18165
18784
  const { kept: external } = partitionVerityOwned(allChanged);
18166
- const analyzable = filterAnalyzable(external);
18167
- const reviewable = filterReviewable(external);
18168
- const securityFiles = filterSecurity(external);
18785
+ const verityIgnore = loadVerityIgnore();
18786
+ const ignored = partitionIgnored(external, verityIgnore);
18787
+ for (const problem of verityIgnore.problems) {
18788
+ process.stderr.write(`Verity: .verityignore ${problem}
18789
+ `);
18790
+ }
18791
+ if (ignored.securityExcluded.length > 0) {
18792
+ process.stderr.write(
18793
+ `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.
18794
+ `
18795
+ );
18796
+ }
18797
+ if (ignored.suspended) {
18798
+ process.stderr.write(
18799
+ "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"
18800
+ );
18801
+ }
18802
+ const inScope = ignored.kept;
18803
+ const analyzable = filterAnalyzable(inScope);
18804
+ const reviewable = filterReviewable(inScope);
18805
+ const securityFiles = filterSecurity(inScope);
18169
18806
  const noFilesChanged = analyzable.length === 0 && reviewable.length === 0 && securityFiles.length === 0;
18170
18807
  if (noFilesChanged && !assistantResponse) {
18171
18808
  await passAndExit(run, "No analyzable files changed", "no-analyzable-files");
18172
18809
  }
18173
18810
  const allForReview = Array.from(/* @__PURE__ */ new Set([...analyzable, ...reviewable]));
18174
- Object.assign(run, { allChanged, allForReview, analyzable, hasRecentCommitFiles, noFilesChanged, reviewable, securityFiles });
18811
+ Object.assign(run, { allChanged, allForReview, analyzable, hasRecentCommitFiles, noFilesChanged, reviewable, securityFiles, verityIgnored: ignored });
18175
18812
  }
18176
18813
 
18177
18814
  // src/lib/specs.ts
18178
- var import_node_fs23 = require("node:fs");
18815
+ var import_node_fs27 = require("node:fs");
18179
18816
  var import_node_path20 = require("node:path");
18180
18817
  var SPEC_CANDIDATES = [
18181
18818
  "CLAUDE.md",
@@ -18207,16 +18844,16 @@ function discoverSpecs(consulted = []) {
18207
18844
  const totalCap = relevant ? MAX_TOTAL_SPEC_BYTES : UNCONSULTED_TOTAL_BYTES;
18208
18845
  if (totalBytes >= totalCap) return false;
18209
18846
  if (seen.has(specPath)) return true;
18210
- if (!(0, import_node_fs23.existsSync)(specPath)) return true;
18847
+ if (!(0, import_node_fs27.existsSync)(specPath)) return true;
18211
18848
  seen.add(specPath);
18212
18849
  const remaining = totalCap - totalBytes;
18213
18850
  const fileCap = relevant ? MAX_SPEC_FILE_BYTES : UNCONSULTED_FILE_BYTES;
18214
18851
  const readBytes = Math.min(fileCap, remaining);
18215
18852
  try {
18216
18853
  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);
18854
+ const fd = (0, import_node_fs27.openSync)(specPath, "r");
18855
+ const bytesRead = (0, import_node_fs27.readSync)(fd, buf, 0, readBytes, 0);
18856
+ (0, import_node_fs27.closeSync)(fd);
18220
18857
  const content = buf.slice(0, bytesRead).toString("utf-8");
18221
18858
  if (!content) return true;
18222
18859
  result.push({ path: specPath, content });
@@ -18232,7 +18869,7 @@ function discoverSpecs(consulted = []) {
18232
18869
  if (!addSpec(candidate)) break;
18233
18870
  }
18234
18871
  for (const dir of ["spec", "docs"]) {
18235
- if (!(0, import_node_fs23.existsSync)(dir)) continue;
18872
+ if (!(0, import_node_fs27.existsSync)(dir)) continue;
18236
18873
  try {
18237
18874
  const mdFiles = findMdFiles(dir, 2).sort();
18238
18875
  for (const mdFile of mdFiles) {
@@ -18247,7 +18884,7 @@ function findMdFiles(dir, maxDepth, depth = 0) {
18247
18884
  if (depth >= maxDepth) return [];
18248
18885
  const result = [];
18249
18886
  try {
18250
- const entries = (0, import_node_fs23.readdirSync)(dir, { withFileTypes: true });
18887
+ const entries = (0, import_node_fs27.readdirSync)(dir, { withFileTypes: true });
18251
18888
  for (const entry of entries) {
18252
18889
  const fullPath = (0, import_node_path20.join)(dir, entry.name);
18253
18890
  if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -18266,14 +18903,14 @@ function discoverPlans() {
18266
18903
  const candidates = [];
18267
18904
  const seen = /* @__PURE__ */ new Set();
18268
18905
  for (const plansDir of [localPlansDir, homePlansDir]) {
18269
- if (!(0, import_node_fs23.existsSync)(plansDir)) continue;
18906
+ if (!(0, import_node_fs27.existsSync)(plansDir)) continue;
18270
18907
  try {
18271
- for (const f of (0, import_node_fs23.readdirSync)(plansDir)) {
18908
+ for (const f of (0, import_node_fs27.readdirSync)(plansDir)) {
18272
18909
  if (!f.endsWith(".md") || seen.has(f)) continue;
18273
18910
  seen.add(f);
18274
18911
  const fullPath = (0, import_node_path20.join)(plansDir, f);
18275
18912
  try {
18276
- const stat3 = (0, import_node_fs23.statSync)(fullPath);
18913
+ const stat3 = (0, import_node_fs27.statSync)(fullPath);
18277
18914
  candidates.push({ name: f, path: fullPath, mtime: stat3.mtimeMs, size: stat3.size });
18278
18915
  } catch {
18279
18916
  }
@@ -18286,13 +18923,35 @@ function discoverPlans() {
18286
18923
  for (const entry of candidates.slice(0, MAX_PLAN_FILES)) {
18287
18924
  if (entry.size > MAX_PLAN_FILE_BYTES) continue;
18288
18925
  try {
18289
- const content = (0, import_node_fs23.readFileSync)(entry.path, "utf-8");
18926
+ const content = (0, import_node_fs27.readFileSync)(entry.path, "utf-8");
18290
18927
  result.push({ name: entry.name, content });
18291
18928
  } catch {
18292
18929
  }
18293
18930
  }
18294
18931
  return result;
18295
18932
  }
18933
+ var GUARD_DOC_EXT = /\.(md|mdx|txt|rst|adoc)$/i;
18934
+ function discoverGuardDocs(rangeFiles2) {
18935
+ const result = [];
18936
+ let totalBytes = 0;
18937
+ for (const path of rangeFiles2) {
18938
+ if (result.length >= MAX_SPEC_FILES) break;
18939
+ if (!GUARD_DOC_EXT.test(path)) continue;
18940
+ if (path.startsWith("/") || path.includes("..")) continue;
18941
+ if (!(0, import_node_fs27.existsSync)(path)) continue;
18942
+ try {
18943
+ const stat3 = (0, import_node_fs27.statSync)(path);
18944
+ if (stat3.size > MAX_PLAN_FILE_BYTES) continue;
18945
+ if (totalBytes + stat3.size > MAX_TOTAL_SPEC_BYTES) continue;
18946
+ const content = (0, import_node_fs27.readFileSync)(path, "utf-8");
18947
+ if (!content) continue;
18948
+ result.push({ name: path, content });
18949
+ totalBytes += content.length;
18950
+ } catch {
18951
+ }
18952
+ }
18953
+ return result;
18954
+ }
18296
18955
 
18297
18956
  // src/commands/analyze/phases/03-intent-inputs.ts
18298
18957
  async function intentInputs(run) {
@@ -18306,6 +18965,50 @@ async function intentInputs(run) {
18306
18965
  })) {
18307
18966
  await passAndExit(run, "User command only \u2014 skipping analysis", "command-only-turn");
18308
18967
  }
18968
+ {
18969
+ const ignoreKeys = ignoreStateKeys(
18970
+ run.tokenResult.ok ? run.tokenResult.data.token : void 0,
18971
+ null
18972
+ );
18973
+ const found = resolveIgnoreState([baselineSessionId, ...ignoreKeys]);
18974
+ const declaration = resolveActive(found?.state ?? null, Math.floor(Date.now() / 1e3));
18975
+ if (declaration) {
18976
+ const commandRecordTruncated = actionSummary?.commands_truncated === true || actionSummary?.user_commands_truncated === true || // The derived signal, and the only one an older client's summary can
18977
+ // give: `tool_counts` keeps counting Bash calls past every cap that
18978
+ // stops the list growing, so a mismatch IS a loss. See
18979
+ // `ActionSummary.commands_truncated`.
18980
+ (actionSummary?.tool_counts?.Bash ?? 0) > (actionSummary?.commands?.length ?? 0);
18981
+ const outcome = verifyDeclaration({
18982
+ declaration,
18983
+ agentAuthoredFiles: (actionSummary?.files_edited.length ?? 0) + (actionSummary?.files_created.length ?? 0),
18984
+ subagents: actionSummary?.subagents ?? 0,
18985
+ agentCommands: actionSummary?.commands,
18986
+ userCommands: actionSummary?.user_commands,
18987
+ commandRecordTruncated,
18988
+ authorshipIsObservable: !!actionSummary && actionSummary.transcript_windowed !== "orphaned"
18989
+ });
18990
+ if (outcome.honoured) {
18991
+ if (declaration.scope === "turn" && found) clearActiveDeclaration(found.key);
18992
+ logEvent("ignore_honoured", { scope: declaration.scope, origin: declaration.origin });
18993
+ await passAndExit(
18994
+ run,
18995
+ `skipping this turn \u2014 declared housekeeping ("${declaration.reason}")`,
18996
+ "declared-ignore"
18997
+ );
18998
+ } else if (outcome.void) {
18999
+ if (found) clearActiveDeclaration(found.key);
19000
+ logEvent("ignore_voided", {
19001
+ scope: declaration.scope,
19002
+ origin: declaration.origin,
19003
+ why: outcome.why
19004
+ });
19005
+ const notice = `Verity: the ignore declared for this window ("${declaration.reason}") was voided \u2014 ${outcome.why}. Reviewing normally.`;
19006
+ process.stderr.write(`${notice}
19007
+ `);
19008
+ run.voidedIgnoreNotice = notice;
19009
+ }
19010
+ }
19011
+ }
18309
19012
  const conversation = await readAndClearConversationBuffer(baselineSessionId);
18310
19013
  const specs = discoverSpecs(actionSummary?.files_read ?? []);
18311
19014
  const plans = discoverPlans();
@@ -18419,177 +19122,8 @@ async function mode(run) {
18419
19122
  Object.assign(run, { analysisMode, contextFilePaths, sessionAuthoredCode, sessionIdForMemory });
18420
19123
  }
18421
19124
 
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
19125
  // src/lib/fold.ts
18592
- var import_node_fs25 = require("node:fs");
19126
+ var import_node_fs28 = require("node:fs");
18593
19127
  var import_node_path21 = require("node:path");
18594
19128
  var KNOWN_RECORD_TYPES = /* @__PURE__ */ new Set([
18595
19129
  "user",
@@ -18727,7 +19261,7 @@ function candidateRoots(repoRoot2) {
18727
19261
  const norm = repoRoot2.replace(/\\/g, "/").replace(/\/+$/, "");
18728
19262
  const out = [norm];
18729
19263
  try {
18730
- const real = import_node_fs25.realpathSync.native(norm).replace(/\\/g, "/").replace(/\/+$/, "");
19264
+ const real = import_node_fs28.realpathSync.native(norm).replace(/\\/g, "/").replace(/\/+$/, "");
18731
19265
  if (real !== norm) out.push(real);
18732
19266
  } catch {
18733
19267
  }
@@ -18815,8 +19349,8 @@ function fold(transcriptPath, opts = {}) {
18815
19349
  }
18816
19350
  };
18817
19351
  try {
18818
- if (!(0, import_node_fs25.existsSync)(transcriptPath)) return result;
18819
- ingest((0, import_node_fs25.readFileSync)(transcriptPath, "utf8"), "agent");
19352
+ if (!(0, import_node_fs28.existsSync)(transcriptPath)) return result;
19353
+ ingest((0, import_node_fs28.readFileSync)(transcriptPath, "utf8"), "agent");
18820
19354
  result.coverage.complete = true;
18821
19355
  } catch {
18822
19356
  return result;
@@ -18827,19 +19361,19 @@ function fold(transcriptPath, opts = {}) {
18827
19361
  (0, import_node_path21.basename)(transcriptPath).replace(/\.jsonl$/, ""),
18828
19362
  "subagents"
18829
19363
  );
18830
- if ((0, import_node_fs25.existsSync)(sidecarDir)) {
19364
+ if ((0, import_node_fs28.existsSync)(sidecarDir)) {
18831
19365
  const maxFiles = opts.maxSidecars ?? 200;
18832
19366
  const maxBytes = opts.maxSidecarBytes ?? 16 * 1024 * 1024;
18833
19367
  const found = [];
18834
19368
  const walk = (d, depth) => {
18835
19369
  if (depth > 4) return;
18836
- for (const e of (0, import_node_fs25.readdirSync)(d, { withFileTypes: true })) {
19370
+ for (const e of (0, import_node_fs28.readdirSync)(d, { withFileTypes: true })) {
18837
19371
  const p = (0, import_node_path21.join)(d, e.name);
18838
19372
  if (e.isDirectory()) {
18839
19373
  walk(p, depth + 1);
18840
19374
  } else if (e.name.startsWith("agent-") && e.name.endsWith(".jsonl")) {
18841
19375
  try {
18842
- const st = (0, import_node_fs25.statSync)(p);
19376
+ const st = (0, import_node_fs28.statSync)(p);
18843
19377
  found.push({ path: p, size: st.size, mtimeMs: st.mtimeMs });
18844
19378
  } catch {
18845
19379
  result.coverage.malformed++;
@@ -18856,7 +19390,7 @@ function fold(transcriptPath, opts = {}) {
18856
19390
  continue;
18857
19391
  }
18858
19392
  try {
18859
- ingest((0, import_node_fs25.readFileSync)(f.path, "utf8"), "subagent");
19393
+ ingest((0, import_node_fs28.readFileSync)(f.path, "utf8"), "subagent");
18860
19394
  bytes += f.size;
18861
19395
  result.coverage.subagentFiles++;
18862
19396
  } catch {
@@ -18891,7 +19425,7 @@ function fold(transcriptPath, opts = {}) {
18891
19425
  }
18892
19426
  function classifyUnobserved(path) {
18893
19427
  try {
18894
- const st = (0, import_node_fs25.statSync)(path);
19428
+ const st = (0, import_node_fs28.statSync)(path);
18895
19429
  if (!st.isFile()) return "unreadable";
18896
19430
  } catch {
18897
19431
  return "unreadable";
@@ -19195,20 +19729,20 @@ async function evidence(run) {
19195
19729
  }
19196
19730
 
19197
19731
  // src/lib/cache-cleanup.ts
19198
- var import_node_fs26 = require("node:fs");
19732
+ var import_node_fs29 = require("node:fs");
19199
19733
  var import_node_path22 = require("node:path");
19200
19734
  var CACHE_TTL_DAYS = 7;
19201
19735
  function pruneStaleCache() {
19202
19736
  try {
19203
19737
  const dir = projectPath(CACHE_DIR);
19204
19738
  const cutoff = Date.now() - CACHE_TTL_DAYS * 24 * 3600 * 1e3;
19205
- for (const entry of (0, import_node_fs26.readdirSync)(dir)) {
19739
+ for (const entry of (0, import_node_fs29.readdirSync)(dir)) {
19206
19740
  if (!entry.startsWith("pending-")) continue;
19207
19741
  const path = (0, import_node_path22.join)(dir, entry);
19208
19742
  try {
19209
- const stat3 = (0, import_node_fs26.statSync)(path);
19743
+ const stat3 = (0, import_node_fs29.statSync)(path);
19210
19744
  if (stat3.mtimeMs < cutoff) {
19211
- (0, import_node_fs26.unlinkSync)(path);
19745
+ (0, import_node_fs29.unlinkSync)(path);
19212
19746
  logEvent("cache_entry_pruned", {
19213
19747
  path: entry,
19214
19748
  age_days: Math.round((Date.now() - stat3.mtimeMs) / 864e5)
@@ -19222,7 +19756,7 @@ function pruneStaleCache() {
19222
19756
  }
19223
19757
 
19224
19758
  // src/lib/context-files.ts
19225
- var import_node_fs27 = require("node:fs");
19759
+ var import_node_fs30 = require("node:fs");
19226
19760
  var MAX_CONTEXT_FILES = 10;
19227
19761
  var MAX_CONTEXT_FILE_BYTES = 10240;
19228
19762
  var MAX_CONTEXT_TOTAL_BYTES = 51200;
@@ -19243,7 +19777,7 @@ function gatherContextFiles(contextPaths, deltaFiles) {
19243
19777
  continue;
19244
19778
  }
19245
19779
  try {
19246
- const content = (0, import_node_fs27.readFileSync)(safePath, "utf8");
19780
+ const content = (0, import_node_fs30.readFileSync)(safePath, "utf8");
19247
19781
  const bytes = Buffer.byteLength(content);
19248
19782
  if (bytes > MAX_CONTEXT_FILE_BYTES) {
19249
19783
  logEvent("context_file_skipped", { path: filePath, reason: "too_large", bytes });
@@ -19305,7 +19839,7 @@ async function contextFiles(run) {
19305
19839
 
19306
19840
  // src/lib/seed-runner.ts
19307
19841
  var import_promises11 = require("node:fs/promises");
19308
- var import_node_fs28 = require("node:fs");
19842
+ var import_node_fs31 = require("node:fs");
19309
19843
  var import_node_path23 = require("node:path");
19310
19844
  var import_yaml2 = __toESM(require_dist());
19311
19845
 
@@ -19545,7 +20079,7 @@ function renderNodeMarkdown(candidate, nodeId, createdAt) {
19545
20079
  return fm;
19546
20080
  }
19547
20081
  async function runSeed(opts) {
19548
- if (!(0, import_node_fs28.existsSync)(STANDARD_FILE)) {
20082
+ if (!(0, import_node_fs31.existsSync)(STANDARD_FILE)) {
19549
20083
  return { created: 0, failed: 0, skipped: "no_standard", candidates: [] };
19550
20084
  }
19551
20085
  let standardDoc;
@@ -19557,7 +20091,7 @@ async function runSeed(opts) {
19557
20091
  }
19558
20092
  const knowledgeSpec = standardDoc.knowledge_spec ?? {};
19559
20093
  let readmeContent;
19560
- if ((0, import_node_fs28.existsSync)("README.md")) {
20094
+ if ((0, import_node_fs31.existsSync)("README.md")) {
19561
20095
  try {
19562
20096
  readmeContent = await (0, import_promises11.readFile)("README.md", "utf-8");
19563
20097
  } catch {
@@ -19565,7 +20099,7 @@ async function runSeed(opts) {
19565
20099
  }
19566
20100
  let claudeMdContent;
19567
20101
  for (const p of ["CLAUDE.md", ".claude/CLAUDE.md"]) {
19568
- if ((0, import_node_fs28.existsSync)(p)) {
20102
+ if ((0, import_node_fs31.existsSync)(p)) {
19569
20103
  try {
19570
20104
  claudeMdContent = await (0, import_promises11.readFile)(p, "utf-8");
19571
20105
  break;
@@ -19589,7 +20123,7 @@ async function runSeed(opts) {
19589
20123
  return { created: 0, failed: 0, skipped: "no_candidates", candidates: [] };
19590
20124
  }
19591
20125
  const overviewPath = (0, import_node_path23.join)(MEMORY_DIR, "domain", "project-overview.md");
19592
- if ((0, import_node_fs28.existsSync)(overviewPath) && !opts.force) {
20126
+ if ((0, import_node_fs31.existsSync)(overviewPath) && !opts.force) {
19593
20127
  return { created: 0, failed: 0, skipped: "already_seeded", candidates };
19594
20128
  }
19595
20129
  if (opts.dryRun) {
@@ -19644,7 +20178,7 @@ async function runSeed(opts) {
19644
20178
  }
19645
20179
 
19646
20180
  // src/commands/analyze/phases/08-memory-manifest.ts
19647
- var import_node_fs29 = require("node:fs");
20181
+ var import_node_fs32 = require("node:fs");
19648
20182
  var import_node_path24 = require("node:path");
19649
20183
  async function memoryManifest(run) {
19650
20184
  const { globals } = run;
@@ -19656,8 +20190,8 @@ async function memoryManifest(run) {
19656
20190
  try {
19657
20191
  await ensureMemoryDir();
19658
20192
  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);
20193
+ const hasStandard = (0, import_node_fs32.existsSync)(STANDARD_FILE);
20194
+ const alreadyTried = (0, import_node_fs32.existsSync)(seedMarker);
19661
20195
  if (hasStandard && !alreadyTried) {
19662
20196
  const preManifest = await buildManifest();
19663
20197
  if (preManifest.nodes.length === 0) {
@@ -19670,7 +20204,7 @@ async function memoryManifest(run) {
19670
20204
  dryRun: false
19671
20205
  });
19672
20206
  if (seedResult.created > 0) {
19673
- (0, import_node_fs29.writeFileSync)(seedMarker, `${(/* @__PURE__ */ new Date()).toISOString()} created=${seedResult.created}
20207
+ (0, import_node_fs32.writeFileSync)(seedMarker, `${(/* @__PURE__ */ new Date()).toISOString()} created=${seedResult.created}
19674
20208
  `);
19675
20209
  autoSeedNotice = `Seeded ${seedResult.created} knowledge node(s) from your existing Standard (one-time).`;
19676
20210
  logEvent("auto_seed_ran", {
@@ -19678,7 +20212,7 @@ async function memoryManifest(run) {
19678
20212
  failed: seedResult.failed
19679
20213
  });
19680
20214
  } else if (seedResult.skipped === "already_seeded") {
19681
- (0, import_node_fs29.writeFileSync)(seedMarker, `${(/* @__PURE__ */ new Date()).toISOString()} skipped=already_seeded
20215
+ (0, import_node_fs32.writeFileSync)(seedMarker, `${(/* @__PURE__ */ new Date()).toISOString()} skipped=already_seeded
19682
20216
  `);
19683
20217
  } else {
19684
20218
  logEvent("auto_seed_noop", {
@@ -19867,7 +20401,7 @@ async function workingMemory(run) {
19867
20401
  }
19868
20402
 
19869
20403
  // src/lib/note-budget.ts
19870
- var import_node_fs30 = require("node:fs");
20404
+ var import_node_fs33 = require("node:fs");
19871
20405
  var ADVISORY_BUDGET = { PASS: 1, WARN: 2 };
19872
20406
  var EPISODE_STALE_SECONDS = 30 * 60;
19873
20407
  var FRESH = { delivered: 0, tasksCompleted: 0, ts: 0 };
@@ -19889,9 +20423,9 @@ function advisoryBudgetSpent(episode, rawDecision) {
19889
20423
  }
19890
20424
  function readAdvisoryEpisode(sessionId) {
19891
20425
  const file = scopedFile(ADVISORY_EPISODE_FILE, sessionId);
19892
- if (!(0, import_node_fs30.existsSync)(file)) return null;
20426
+ if (!(0, import_node_fs33.existsSync)(file)) return null;
19893
20427
  try {
19894
- const o = JSON.parse((0, import_node_fs30.readFileSync)(file, "utf-8")) ?? {};
20428
+ const o = JSON.parse((0, import_node_fs33.readFileSync)(file, "utf-8")) ?? {};
19895
20429
  const delivered = typeof o.delivered === "number" ? o.delivered : NaN;
19896
20430
  if (isNaN(delivered)) return null;
19897
20431
  return {
@@ -19905,8 +20439,8 @@ function readAdvisoryEpisode(sessionId) {
19905
20439
  }
19906
20440
  function writeAdvisoryEpisode(episode, sessionId) {
19907
20441
  try {
19908
- (0, import_node_fs30.mkdirSync)(VERITY_DIR, { recursive: true });
19909
- (0, import_node_fs30.writeFileSync)(
20442
+ (0, import_node_fs33.mkdirSync)(VERITY_DIR, { recursive: true });
20443
+ (0, import_node_fs33.writeFileSync)(
19910
20444
  scopedFile(ADVISORY_EPISODE_FILE, sessionId),
19911
20445
  JSON.stringify({ v: 1, ...episode })
19912
20446
  );
@@ -20013,7 +20547,16 @@ async function buildRequest(run) {
20013
20547
  // the state, so the number is one turn lagged by construction. The
20014
20548
  // degenerate win for the budget is a dead channel that looks like clean
20015
20549
  // code; this is what makes "did delivery rate collapse" a query.
20016
- advisory_delivered_prior: readAdvisoryEpisode(run.baselineSessionId)?.delivered ?? 0
20550
+ advisory_delivered_prior: readAdvisoryEpisode(run.baselineSessionId)?.delivered ?? 0,
20551
+ // `.verityignore` — see CoverageTelemetry.verityignore for why the SHARE is
20552
+ // the number that matters and why no paths travel with it.
20553
+ verityignore: {
20554
+ rules: run.verityIgnored.rules,
20555
+ excluded: run.verityIgnored.ignored.length,
20556
+ share: ignoreShare(run.verityIgnored.kept.length, run.verityIgnored.ignored.length),
20557
+ security_excluded: run.verityIgnored.securityExcluded.length,
20558
+ suspended: run.verityIgnored.suspended
20559
+ }
20017
20560
  };
20018
20561
  const requestBody = {
20019
20562
  coverage_telemetry: coverageTelemetry,
@@ -20211,14 +20754,14 @@ async function buildRequest(run) {
20211
20754
  }
20212
20755
 
20213
20756
  // src/lib/offline.ts
20214
- var import_node_fs31 = require("node:fs");
20757
+ var import_node_fs34 = require("node:fs");
20215
20758
  var import_node_crypto11 = require("node:crypto");
20216
20759
  function cacheRequest(body) {
20217
20760
  try {
20218
- (0, import_node_fs31.mkdirSync)(CACHE_DIR, { recursive: true });
20761
+ (0, import_node_fs34.mkdirSync)(CACHE_DIR, { recursive: true });
20219
20762
  const suffix = (0, import_node_crypto11.randomBytes)(4).toString("hex");
20220
20763
  const filename = `pending-${Math.floor(Date.now() / 1e3)}-${suffix}.json`;
20221
- (0, import_node_fs31.writeFileSync)(`${CACHE_DIR}/${filename}`, JSON.stringify(redactRequest(body)));
20764
+ (0, import_node_fs34.writeFileSync)(`${CACHE_DIR}/${filename}`, JSON.stringify(redactRequest(body)));
20222
20765
  } catch {
20223
20766
  }
20224
20767
  }
@@ -20337,7 +20880,7 @@ async function transmit(run) {
20337
20880
  }
20338
20881
 
20339
20882
  // src/commands/analyze/phases/13-reconcile.ts
20340
- var import_node_fs32 = require("node:fs");
20883
+ var import_node_fs35 = require("node:fs");
20341
20884
  var import_node_path26 = require("node:path");
20342
20885
  async function reconcile(run) {
20343
20886
  const { actionSummary, allChanged, analyzable, baseline, baselineSessionId, codeDelta, contentHash, conversation, decision, foldResult, memory, memorySession, response, reviewable, securityFiles, turnId } = run;
@@ -20348,7 +20891,7 @@ async function reconcile(run) {
20348
20891
  const st = foldDossier(memorySession.d);
20349
20892
  openElsewhere = openBlockingElsewhere(st.statements, sentPaths, (file, line) => {
20350
20893
  try {
20351
- const src = (0, import_node_fs32.readFileSync)((0, import_node_path26.join)(repoRoot(), file), "utf8").split("\n");
20894
+ const src = (0, import_node_fs35.readFileSync)((0, import_node_path26.join)(repoRoot(), file), "utf8").split("\n");
20352
20895
  const at = src[line - 1];
20353
20896
  return at === void 0 ? null : lineSha(at);
20354
20897
  } catch {
@@ -20409,6 +20952,25 @@ async function reconcile(run) {
20409
20952
  stage: "self-scope",
20410
20953
  kind: "policy"
20411
20954
  })),
20955
+ // ⚠ WHAT THE TEAM'S OWN `.verityignore` REMOVED — every path, named
20956
+ // (VRT-135 · 3/3). The 0.30 principle: an exclusion the ledger cannot see
20957
+ // is a scoping decision nobody can audit, and this is the one exclusion
20958
+ // stage a user can edit, so it is the one that most needs to be visible.
20959
+ //
20960
+ // `policy`, by D2's bar — no version of this product would have reviewed a
20961
+ // file the project declared out of scope, so it must not downgrade PASS to
20962
+ // WARN. That is also why it is not narrated per-turn by `describeCoverage`
20963
+ // (policy exclusions are recorded, not announced): repeating "your dist/
20964
+ // went unreviewed" on every turn is how a channel gets muted. The SHARE
20965
+ // below is the signal that replaces the noise.
20966
+ //
20967
+ // Taken from the run, not recomputed — see context.ts `verityIgnored`.
20968
+ ...run.verityIgnored.ignored.map((path) => ({
20969
+ path,
20970
+ reason: "verityignore",
20971
+ stage: "verityignore",
20972
+ kind: "policy"
20973
+ })),
20412
20974
  // The extension allowlist. POLICY: a changed README was never going to be
20413
20975
  // reviewed, and calling that a coverage gap would downgrade nearly every
20414
20976
  // PASS to WARN until WARN meant nothing. Recorded so the ledger balances and
@@ -20483,6 +21045,14 @@ async function reconcile(run) {
20483
21045
  })() : [];
20484
21046
  if (memorySession) {
20485
21047
  try {
21048
+ const settledAnchors = response.metadata?.settled_anchors;
21049
+ if (Array.isArray(settledAnchors)) {
21050
+ for (const anchorKey of settledAnchors) {
21051
+ if (typeof anchorKey === "string" && anchorKey.length > 0) {
21052
+ appendEvent(memorySession.d, { k: "outcome", anchor_key: anchorKey, outcome: "answered" });
21053
+ }
21054
+ }
21055
+ }
20486
21056
  recordVerdict(memorySession.d, {
20487
21057
  runId: response.run_id ?? turnId,
20488
21058
  decision,
@@ -20638,7 +21208,7 @@ function agentContextFor(response, intentRepeat = 0, priorPendingFingerprints =
20638
21208
  }
20639
21209
  async function render(run) {
20640
21210
  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;
21211
+ 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
21212
  let { iteration } = run;
20643
21213
  const metadata = response.metadata ?? {};
20644
21214
  const intentAmbiguity = metadata.intent_ambiguity;
@@ -20890,6 +21460,7 @@ ${YELLOW}${grantNudge.trim()}${NC}
20890
21460
  const viewUrl = response.view_url ?? "";
20891
21461
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
20892
21462
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
21463
+ if (voidedIgnoreNotice) userSummary = `${voidedIgnoreNotice} ${userSummary}`;
20893
21464
  userSummary += loginNudge + grantNudge;
20894
21465
  emitVerdict({
20895
21466
  proposed: "PASS",
@@ -20911,6 +21482,7 @@ ${YELLOW}${grantNudge.trim()}${NC}
20911
21482
  const viewUrl = response.view_url ?? "";
20912
21483
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
20913
21484
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
21485
+ if (voidedIgnoreNotice) userSummary = `${voidedIgnoreNotice} ${userSummary}`;
20914
21486
  userSummary += loginNudge + grantNudge;
20915
21487
  emitVerdict({
20916
21488
  proposed: "WARN",
@@ -20926,7 +21498,11 @@ ${YELLOW}${grantNudge.trim()}${NC}
20926
21498
  }
20927
21499
  default: {
20928
21500
  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;
21501
+ const msg = [
21502
+ voidedIgnoreNotice,
21503
+ autoSeedNotice,
21504
+ "Verity: unrecognised verdict \u2014 treating as WARN"
21505
+ ].filter(Boolean).join(" ") + loginNudge + grantNudge;
20930
21506
  process.stderr.write(
20931
21507
  `Verity: server returned an unrecognised gate_decision (${raw}). Rendering WARN rather than PASS. Update the CLI: npm i -g @codacy/verity-cli
20932
21508
  `
@@ -21003,7 +21579,7 @@ async function runAnalyze(opts, globals) {
21003
21579
  }
21004
21580
 
21005
21581
  // src/commands/baseline.ts
21006
- var import_node_fs33 = require("node:fs");
21582
+ var import_node_fs36 = require("node:fs");
21007
21583
  function registerBaselineCommands(program2) {
21008
21584
  const baseline = program2.command("baseline").description("Manage the task-start working-tree baseline");
21009
21585
  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 +21588,7 @@ function registerBaselineCommands(program2) {
21012
21588
  process.chdir(repoRoot());
21013
21589
  } catch {
21014
21590
  }
21015
- if (!(0, import_node_fs33.existsSync)(VERITY_DIR)) {
21591
+ if (!(0, import_node_fs36.existsSync)(VERITY_DIR)) {
21016
21592
  process.exit(0);
21017
21593
  }
21018
21594
  let sessionId = opts.sessionId;
@@ -21052,7 +21628,7 @@ async function readStdin() {
21052
21628
  }
21053
21629
 
21054
21630
  // src/commands/review.ts
21055
- var import_node_fs34 = require("node:fs");
21631
+ var import_node_fs37 = require("node:fs");
21056
21632
  function registerReviewCommand(program2) {
21057
21633
  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
21634
  const globals = program2.opts();
@@ -21071,7 +21647,7 @@ async function runReview(opts, globals) {
21071
21647
  const securityFiles = filterSecurity(allFiles);
21072
21648
  let staticResults;
21073
21649
  if (isCodacyAvailable()) {
21074
- const scannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles])).filter((f) => (0, import_node_fs34.existsSync)(f) || resolveFile(f) !== null);
21650
+ const scannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles])).filter((f) => (0, import_node_fs37.existsSync)(f) || resolveFile(f) !== null);
21075
21651
  staticResults = runCodacyAnalysis(scannable);
21076
21652
  } else {
21077
21653
  staticResults = {
@@ -21097,10 +21673,10 @@ async function runReview(opts, globals) {
21097
21673
  const specPaths = opts.specs.split(",").map((f) => f.trim()).filter(Boolean);
21098
21674
  specs = [];
21099
21675
  for (const p of specPaths) {
21100
- if (!(0, import_node_fs34.existsSync)(p)) continue;
21676
+ if (!(0, import_node_fs37.existsSync)(p)) continue;
21101
21677
  try {
21102
- const { readFileSync: readFileSync20 } = await import("node:fs");
21103
- const content = readFileSync20(p, "utf-8");
21678
+ const { readFileSync: readFileSync24 } = await import("node:fs");
21679
+ const content = readFileSync24(p, "utf-8");
21104
21680
  specs.push({ path: p, content: content.slice(0, 10240) });
21105
21681
  } catch {
21106
21682
  }
@@ -21157,7 +21733,7 @@ async function runReview(opts, globals) {
21157
21733
  }
21158
21734
 
21159
21735
  // src/commands/guard.ts
21160
- var import_node_fs35 = require("node:fs");
21736
+ var import_node_fs38 = require("node:fs");
21161
21737
  var import_node_path27 = require("node:path");
21162
21738
  var GUARD_BLOCK_CAP = 2;
21163
21739
  var GUARD_ITER_FILE = (0, import_node_path27.join)(VERITY_DIR, ".guard-iteration");
@@ -21205,7 +21781,7 @@ function readPreToolUseStdin() {
21205
21781
  }
21206
21782
  function readIterMap() {
21207
21783
  try {
21208
- const raw = JSON.parse((0, import_node_fs35.readFileSync)(GUARD_ITER_FILE, "utf-8"));
21784
+ const raw = JSON.parse((0, import_node_fs38.readFileSync)(GUARD_ITER_FILE, "utf-8"));
21209
21785
  if (raw && typeof raw === "object") {
21210
21786
  if (typeof raw.moment === "string" && typeof raw.count === "number") {
21211
21787
  return { [raw.moment]: raw.count };
@@ -21225,10 +21801,10 @@ function readIter(moment) {
21225
21801
  }
21226
21802
  function writeIter(moment, count) {
21227
21803
  try {
21228
- (0, import_node_fs35.mkdirSync)(VERITY_DIR, { recursive: true });
21804
+ (0, import_node_fs38.mkdirSync)(VERITY_DIR, { recursive: true });
21229
21805
  const map = readIterMap();
21230
21806
  map[moment] = count;
21231
- (0, import_node_fs35.writeFileSync)(GUARD_ITER_FILE, JSON.stringify(map));
21807
+ (0, import_node_fs38.writeFileSync)(GUARD_ITER_FILE, JSON.stringify(map));
21232
21808
  } catch {
21233
21809
  }
21234
21810
  }
@@ -21238,10 +21814,10 @@ function resetIter(moment) {
21238
21814
  if (!(moment in map)) return;
21239
21815
  delete map[moment];
21240
21816
  if (Object.keys(map).length === 0) {
21241
- if ((0, import_node_fs35.existsSync)(GUARD_ITER_FILE)) (0, import_node_fs35.unlinkSync)(GUARD_ITER_FILE);
21817
+ if ((0, import_node_fs38.existsSync)(GUARD_ITER_FILE)) (0, import_node_fs38.unlinkSync)(GUARD_ITER_FILE);
21242
21818
  } else {
21243
- (0, import_node_fs35.mkdirSync)(VERITY_DIR, { recursive: true });
21244
- (0, import_node_fs35.writeFileSync)(GUARD_ITER_FILE, JSON.stringify(map));
21819
+ (0, import_node_fs38.mkdirSync)(VERITY_DIR, { recursive: true });
21820
+ (0, import_node_fs38.writeFileSync)(GUARD_ITER_FILE, JSON.stringify(map));
21245
21821
  }
21246
21822
  } catch {
21247
21823
  }
@@ -21302,16 +21878,17 @@ function extractStatedIntent(moment, command, pushedMessages = null) {
21302
21878
  const text = moment === "pre-commit" ? parseCommitMessage(command) : parsePrIntent(command) ?? pushedMessages;
21303
21879
  return isSubstantiveIntent(text) ? text : null;
21304
21880
  }
21305
- function hasBlockingFinding(response) {
21881
+ function hasBlockingFinding(response, sentFiles) {
21306
21882
  const findings = response.findings ?? [];
21307
- return findings.some((f) => f.scope !== "pre-existing" && ["critical", "high"].includes((f.severity ?? "").toLowerCase()));
21883
+ const sent = sentFiles ? new Set(sentFiles.map((p) => p.replace(/\\/g, "/"))) : null;
21884
+ 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
21885
  }
21309
21886
  function buildGuardRequest(moment, files, codeDelta, iter, sessionId, statedIntent, coverageTelemetry) {
21310
21887
  const analyzable = filterAnalyzable(files);
21311
21888
  const securityFiles = filterSecurity(files);
21312
21889
  let staticResults;
21313
21890
  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);
21891
+ 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
21892
  staticResults = runCodacyAnalysis(scannable);
21316
21893
  } else {
21317
21894
  staticResults = { tool: "@codacy/analysis-cli", findings: [], summary: { total_findings: 0, by_severity: {}, tools_run: [] } };
@@ -21333,13 +21910,11 @@ function buildGuardRequest(moment, files, codeDelta, iter, sessionId, statedInte
21333
21910
  }
21334
21911
  };
21335
21912
  if (coverageTelemetry) requestBody.coverage_telemetry = coverageTelemetry;
21336
- const specs = discoverSpecs();
21337
- const plans = discoverPlans();
21338
- if (specs.length > 0 || plans.length > 0 || statedIntent) {
21913
+ const docs = discoverGuardDocs(files);
21914
+ if (docs.length > 0 || statedIntent) {
21339
21915
  const intentContext = {};
21340
21916
  if (statedIntent) intentContext.user_prompt = statedIntent;
21341
- if (specs.length > 0) intentContext.specs = specs;
21342
- if (plans.length > 0) intentContext.plans = plans;
21917
+ if (docs.length > 0) intentContext.plans = docs;
21343
21918
  requestBody.intent_context = intentContext;
21344
21919
  }
21345
21920
  return requestBody;
@@ -21420,7 +21995,7 @@ async function runGuard(opts, globals) {
21420
21995
  const urlResult = await resolveServiceUrl(globals.serviceUrl);
21421
21996
  if (!tokenResult.ok || !urlResult.ok) process.exit(0);
21422
21997
  logEvent("guard_frame", { moment, ...frameTelemetry(frame, range) });
21423
- const codeDelta = collectCodeDelta(files);
21998
+ const codeDelta = collectCodeDelta(files, GIT_MOMENT_BUDGET);
21424
21999
  if (codeDelta.total_files === 0) process.exit(0);
21425
22000
  const statedIntent = extractStatedIntent(
21426
22001
  moment,
@@ -21446,6 +22021,7 @@ async function runGuard(opts, globals) {
21446
22021
  excluded: codeDelta.excluded.map((e) => ({ path: e.path, reason: e.reason }))
21447
22022
  };
21448
22023
  logToFileOnly(coverageBlock(coverage));
22024
+ const reviewStart = Date.now();
21449
22025
  const result = await analyzeRequest({
21450
22026
  serviceUrl: urlResult.data,
21451
22027
  token: tokenResult.data.token,
@@ -21454,6 +22030,7 @@ async function runGuard(opts, globals) {
21454
22030
  timeout: 29e4,
21455
22031
  cmd: "guard"
21456
22032
  });
22033
+ const reviewSecs = Math.max(1, Math.round((Date.now() - reviewStart) / 1e3));
21457
22034
  if (!result.ok) {
21458
22035
  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
22036
  emitAllowNotice(
@@ -21466,9 +22043,9 @@ async function runGuard(opts, globals) {
21466
22043
  const decision = response.gate_decision ?? "(unrecognised)";
21467
22044
  const viewUrl = response.view_url ?? "";
21468
22045
  const link = viewUrl ? ` \u2014 ${viewUrl}` : "";
21469
- const covLine = coverageSummary(coverage);
22046
+ const covLine = `${coverageSummary(coverage)} in ${reviewSecs}s`;
21470
22047
  const covDetail = coverageBlock(coverage);
21471
- if (decision === "FAIL" && hasBlockingFinding(response)) {
22048
+ if (decision === "FAIL" && hasBlockingFinding(response, codeDelta.files.map((f) => f.path))) {
21472
22049
  writeIter(moment, iter + 1);
21473
22050
  writeBlockMessage(moment, response, covDetail);
21474
22051
  process.exit(2);
@@ -21535,18 +22112,167 @@ function writeBlockMessage(moment, response, covDetail) {
21535
22112
 
21536
22113
  `);
21537
22114
  process.stderr.write(`${BOLD}Fix these, then re-run the ${verb}.${NC}
22115
+ `);
22116
+ process.stderr.write(`${DIM}Accepted risk? Record it: verity waive <pattern-id> --file <path> --reason "\u2026" (voids automatically when the file changes).${NC}
21538
22117
  `);
21539
22118
  }
21540
22119
 
22120
+ // src/commands/ignore.ts
22121
+ function registerIgnoreCommand(program2) {
22122
+ 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) => {
22123
+ const globals = program2.opts();
22124
+ const now = Math.floor(Date.now() / 1e3);
22125
+ const reason = opts.reason?.trim();
22126
+ if (!reason) {
22127
+ printError(
22128
+ '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.'
22129
+ );
22130
+ process.exit(1);
22131
+ }
22132
+ if (opts.for && opts.turn) {
22133
+ printError("Use either --turn or --for, not both \u2014 they are two different windows.");
22134
+ process.exit(1);
22135
+ }
22136
+ let scope2 = "turn";
22137
+ let ttl = TURN_FUSE_SECONDS;
22138
+ if (opts.for) {
22139
+ const parsed = parseDuration(opts.for);
22140
+ if (!parsed.ok) {
22141
+ printError(parsed.error);
22142
+ process.exit(1);
22143
+ }
22144
+ scope2 = "window";
22145
+ ttl = parsed.seconds;
22146
+ }
22147
+ const tokenResult = await resolveToken(globals.token);
22148
+ const token = tokenResult.ok ? tokenResult.data.token : void 0;
22149
+ const sessionId = opts.sessionId || process.env.CLAUDE_SESSION_ID || void 0;
22150
+ const keys = ignoreStateKeys(token, sessionId);
22151
+ const existing = resolveIgnoreState(keys);
22152
+ const spent = existing?.state.spent ?? 0;
22153
+ const writeKey = existing?.key ?? keys[0];
22154
+ if (spent >= IGNORE_BUDGET) {
22155
+ 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.`;
22156
+ if (opts.json) {
22157
+ printJson({ declared: false, reason_refused: "budget-spent", spent, budget: IGNORE_BUDGET });
22158
+ } else {
22159
+ printWarn(msg);
22160
+ }
22161
+ logEvent("ignore_refused", { why: "budget-spent", spent, budget: IGNORE_BUDGET });
22162
+ process.exit(0);
22163
+ }
22164
+ const declaration = {
22165
+ scope: scope2,
22166
+ origin: opts.agent ? "agent" : "user",
22167
+ reason,
22168
+ at: now,
22169
+ expires: now + ttl
22170
+ };
22171
+ writeIgnoreState({ v: 1, active: declaration, spent: spent + 1 }, writeKey);
22172
+ logEvent("ignore_declared", {
22173
+ scope: scope2,
22174
+ origin: declaration.origin,
22175
+ ttl_seconds: ttl,
22176
+ spent: spent + 1,
22177
+ budget: IGNORE_BUDGET
22178
+ });
22179
+ if (opts.json) {
22180
+ printJson({
22181
+ declared: true,
22182
+ scope: scope2,
22183
+ origin: declaration.origin,
22184
+ reason,
22185
+ expires_at: new Date(declaration.expires * 1e3).toISOString(),
22186
+ spent: spent + 1,
22187
+ budget: IGNORE_BUDGET
22188
+ });
22189
+ return;
22190
+ }
22191
+ const window = scope2 === "turn" ? "the next turn" : `the next ${describeRemaining(declaration, now).replace(" left", "")}`;
22192
+ printInfo(`Verity will skip ${window} \u2014 "${reason}" (${spent + 1}/${IGNORE_BUDGET} this session).`);
22193
+ printInfo("It covers turns that author nothing. If anything is written, the declaration voids and the review runs.");
22194
+ });
22195
+ 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) => {
22196
+ const globals = program2.opts();
22197
+ const tokenResult = await resolveToken(globals.token);
22198
+ const token = tokenResult.ok ? tokenResult.data.token : void 0;
22199
+ const sessionId = opts.sessionId || process.env.CLAUDE_SESSION_ID || void 0;
22200
+ const found = resolveIgnoreState(ignoreStateKeys(token, sessionId));
22201
+ const active = resolveActive(found?.state ?? null, Math.floor(Date.now() / 1e3));
22202
+ if (!found || !active) {
22203
+ printInfo("No active ignore declaration.");
22204
+ return;
22205
+ }
22206
+ clearActiveDeclaration(found.key);
22207
+ logEvent("ignore_cleared", { scope: active.scope, origin: active.origin });
22208
+ printInfo(`Cleared: "${active.reason}". The next turn will be reviewed normally.`);
22209
+ });
22210
+ }
22211
+
22212
+ // src/commands/waive.ts
22213
+ var import_node_crypto12 = require("node:crypto");
22214
+ var import_node_fs39 = require("node:fs");
22215
+ function registerWaiveCommand(program2) {
22216
+ 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) => {
22217
+ const globals = program2.opts();
22218
+ try {
22219
+ process.chdir(repoRoot());
22220
+ } catch {
22221
+ }
22222
+ if (opts.reason.trim().length < 20) {
22223
+ printError("A waive needs a real reason (>= 20 chars) \u2014 name the human disposition it records.");
22224
+ process.exit(1);
22225
+ }
22226
+ const tokenResult = await resolveToken(globals.token);
22227
+ if (!tokenResult.ok) {
22228
+ printError(tokenResult.error);
22229
+ process.exit(1);
22230
+ }
22231
+ const urlResult = await resolveServiceUrl(globals.serviceUrl);
22232
+ if (!urlResult.ok) {
22233
+ printError(urlResult.error);
22234
+ process.exit(1);
22235
+ }
22236
+ const body = {
22237
+ pattern_id: patternId,
22238
+ reason: opts.reason.trim()
22239
+ };
22240
+ if (opts.file) {
22241
+ body.file = opts.file;
22242
+ try {
22243
+ body.file_sha256 = (0, import_node_crypto12.createHash)("sha256").update((0, import_node_fs39.readFileSync)(opts.file)).digest("hex");
22244
+ } catch {
22245
+ printError(`Cannot read ${opts.file} \u2014 run from the repo root, or omit --file to waive by pattern.`);
22246
+ process.exit(1);
22247
+ }
22248
+ }
22249
+ const result = await apiRequest({
22250
+ method: "POST",
22251
+ path: "/feedback/waive",
22252
+ serviceUrl: urlResult.data,
22253
+ token: tokenResult.data.token,
22254
+ body,
22255
+ verbose: globals.verbose
22256
+ });
22257
+ if (!result.ok) {
22258
+ printError(`Couldn't record the waive: ${result.error}`);
22259
+ process.exit(1);
22260
+ }
22261
+ logEvent("waive_recorded", { pattern: patternId, file: opts.file ?? null, waived: result.data.waived });
22262
+ printInfo(`Waived ${result.data.waived} open statement(s) for '${patternId}'${opts.file ? ` on ${opts.file}` : ""}.`);
22263
+ printInfo(result.data.note);
22264
+ });
22265
+ }
22266
+
21541
22267
  // src/commands/init.ts
21542
- var import_node_fs37 = require("node:fs");
22268
+ var import_node_fs41 = require("node:fs");
21543
22269
  var import_promises13 = require("node:fs/promises");
21544
22270
  var import_node_path29 = require("node:path");
21545
22271
  var import_node_child_process11 = require("node:child_process");
21546
22272
  var readline2 = __toESM(require("node:readline/promises"));
21547
22273
 
21548
22274
  // src/commands/migrate.ts
21549
- var import_node_fs36 = require("node:fs");
22275
+ var import_node_fs40 = require("node:fs");
21550
22276
  var import_node_path28 = require("node:path");
21551
22277
  var import_node_child_process10 = require("node:child_process");
21552
22278
 
@@ -21678,10 +22404,10 @@ async function runMigration(opts = {}) {
21678
22404
  function migrateProjectDir(root, actions) {
21679
22405
  const gateDir = (0, import_node_path28.join)(root, ".gate");
21680
22406
  const verityDir = (0, import_node_path28.join)(root, ".verity");
21681
- if ((0, import_node_fs36.existsSync)(gateDir) && !(0, import_node_fs36.existsSync)(verityDir)) {
22407
+ if ((0, import_node_fs40.existsSync)(gateDir) && !(0, import_node_fs40.existsSync)(verityDir)) {
21682
22408
  return migrateProjectDirRename(root, gateDir, verityDir, actions);
21683
22409
  }
21684
- if ((0, import_node_fs36.existsSync)(gateDir) && (0, import_node_fs36.existsSync)(verityDir)) {
22410
+ if ((0, import_node_fs40.existsSync)(gateDir) && (0, import_node_fs40.existsSync)(verityDir)) {
21685
22411
  return migrateProjectDirCarry(gateDir, verityDir, actions);
21686
22412
  }
21687
22413
  return false;
@@ -21702,13 +22428,13 @@ function migrateProjectDirRename(root, gateDir, verityDir, actions) {
21702
22428
  }
21703
22429
  }
21704
22430
  if (moved) {
21705
- if ((0, import_node_fs36.existsSync)(gateDir)) {
22431
+ if ((0, import_node_fs40.existsSync)(gateDir)) {
21706
22432
  const carried = carryLegacyContents(gateDir, verityDir);
21707
22433
  if (carried > 0) {
21708
22434
  actions.push(`Carried ${carried} untracked legacy file(s) from .gate/ into .verity/`);
21709
22435
  }
21710
22436
  try {
21711
- (0, import_node_fs36.rmSync)(gateDir, { recursive: true, force: true });
22437
+ (0, import_node_fs40.rmSync)(gateDir, { recursive: true, force: true });
21712
22438
  } catch {
21713
22439
  }
21714
22440
  }
@@ -21724,7 +22450,7 @@ function migrateProjectDirCarry(gateDir, verityDir, actions) {
21724
22450
  actions.push(`Carried ${carried} legacy file(s) from .gate/ into .verity/`);
21725
22451
  }
21726
22452
  try {
21727
- (0, import_node_fs36.rmSync)(gateDir, { recursive: true, force: true });
22453
+ (0, import_node_fs40.rmSync)(gateDir, { recursive: true, force: true });
21728
22454
  } catch {
21729
22455
  }
21730
22456
  return carried > 0;
@@ -21733,9 +22459,9 @@ function migrateGlobalCredentials(home, actions) {
21733
22459
  if (!home) return;
21734
22460
  const gateCreds = (0, import_node_path28.join)(home, ".gate", "credentials");
21735
22461
  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 });
22462
+ if (!(0, import_node_fs40.existsSync)(gateCreds)) return;
22463
+ if (!(0, import_node_fs40.existsSync)(verityCreds)) {
22464
+ (0, import_node_fs40.mkdirSync)((0, import_node_path28.join)(home, ".verity"), { recursive: true });
21739
22465
  moveFile(gateCreds, verityCreds);
21740
22466
  actions.push("Moved ~/.gate/credentials \u2192 ~/.verity/credentials");
21741
22467
  return;
@@ -21758,7 +22484,7 @@ async function migrateLegacyHooks(root, actions) {
21758
22484
  }
21759
22485
  async function migrateClaudeMd(root, actions) {
21760
22486
  const claudeMd = (0, import_node_path28.join)(root, "CLAUDE.md");
21761
- const hadLegacyBlock = (0, import_node_fs36.existsSync)(claudeMd) && hasLegacyMemoryBlock(readFileSyncSafe(claudeMd));
22487
+ const hadLegacyBlock = (0, import_node_fs40.existsSync)(claudeMd) && hasLegacyMemoryBlock(readFileSyncSafe(claudeMd));
21762
22488
  if (!hadLegacyBlock) return;
21763
22489
  try {
21764
22490
  await ensureClaudeMdPointer(root);
@@ -21770,7 +22496,7 @@ async function migrateClaudeMd(root, actions) {
21770
22496
  function migrateStandardFile(root, actions) {
21771
22497
  const gateMd = (0, import_node_path28.join)(root, "GATE.md");
21772
22498
  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;
22499
+ if (!(0, import_node_fs40.existsSync)(gateMd) || (0, import_node_fs40.existsSync)(verityMd)) return;
21774
22500
  let moved = false;
21775
22501
  if (isGitRepo(root) && isGitTracked(root, "GATE.md")) {
21776
22502
  try {
@@ -21782,12 +22508,12 @@ function migrateStandardFile(root, actions) {
21782
22508
  if (!moved) moveFile(gateMd, verityMd);
21783
22509
  const content = readFileSyncSafe(verityMd);
21784
22510
  const refreshed = content.split("GATE.md").join("VERITY.md");
21785
- if (refreshed !== content) (0, import_node_fs36.writeFileSync)(verityMd, refreshed);
22511
+ if (refreshed !== content) (0, import_node_fs40.writeFileSync)(verityMd, refreshed);
21786
22512
  actions.push("Renamed GATE.md \u2192 VERITY.md");
21787
22513
  }
21788
22514
  async function migrateTelemetryHeaders(root, actions) {
21789
22515
  const file = (0, import_node_path28.join)(root, ".claude", "settings.local.json");
21790
- if (!(0, import_node_fs36.existsSync)(file)) return;
22516
+ if (!(0, import_node_fs40.existsSync)(file)) return;
21791
22517
  let settings;
21792
22518
  try {
21793
22519
  settings = JSON.parse(readFileSyncSafe(file) || "{}");
@@ -21835,14 +22561,14 @@ function mergeGlobalCredentials(gateCreds, verityCreds) {
21835
22561
  }
21836
22562
  if (toAppend.length > 0) {
21837
22563
  const sep2 = verityContent.endsWith("\n") || verityContent === "" ? "" : "\n";
21838
- (0, import_node_fs36.writeFileSync)(verityCreds, verityContent + sep2 + toAppend.join("\n") + "\n");
22564
+ (0, import_node_fs40.writeFileSync)(verityCreds, verityContent + sep2 + toAppend.join("\n") + "\n");
21839
22565
  }
21840
- (0, import_node_fs36.rmSync)(gateCreds, { force: true });
22566
+ (0, import_node_fs40.rmSync)(gateCreds, { force: true });
21841
22567
  return toAppend.length;
21842
22568
  }
21843
22569
  function readFileSyncSafe(path) {
21844
22570
  try {
21845
- return (0, import_node_fs36.readFileSync)(path, "utf-8");
22571
+ return (0, import_node_fs40.readFileSync)(path, "utf-8");
21846
22572
  } catch {
21847
22573
  return "";
21848
22574
  }
@@ -21857,35 +22583,35 @@ function hasStagedChanges(root) {
21857
22583
  }
21858
22584
  function moveDir(from, to) {
21859
22585
  try {
21860
- (0, import_node_fs36.renameSync)(from, to);
22586
+ (0, import_node_fs40.renameSync)(from, to);
21861
22587
  } catch (err) {
21862
22588
  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 });
22589
+ (0, import_node_fs40.cpSync)(from, to, { recursive: true });
22590
+ (0, import_node_fs40.rmSync)(from, { recursive: true, force: true });
21865
22591
  }
21866
22592
  }
21867
22593
  function moveFile(from, to) {
21868
22594
  try {
21869
- (0, import_node_fs36.renameSync)(from, to);
22595
+ (0, import_node_fs40.renameSync)(from, to);
21870
22596
  } catch (err) {
21871
22597
  if (err.code !== "EXDEV") throw err;
21872
- (0, import_node_fs36.cpSync)(from, to);
21873
- (0, import_node_fs36.rmSync)(from, { force: true });
22598
+ (0, import_node_fs40.cpSync)(from, to);
22599
+ (0, import_node_fs40.rmSync)(from, { force: true });
21874
22600
  }
21875
22601
  }
21876
22602
  function carryLegacyContents(gateDir, verityDir) {
21877
22603
  let copied = 0;
21878
22604
  const walk = (relDir) => {
21879
22605
  const srcDir = (0, import_node_path28.join)(gateDir, relDir);
21880
- for (const entry of (0, import_node_fs36.readdirSync)(srcDir)) {
22606
+ for (const entry of (0, import_node_fs40.readdirSync)(srcDir)) {
21881
22607
  const rel = relDir ? (0, import_node_path28.join)(relDir, entry) : entry;
21882
22608
  const src = (0, import_node_path28.join)(gateDir, rel);
21883
22609
  const dest = (0, import_node_path28.join)(verityDir, rel);
21884
- if ((0, import_node_fs36.statSync)(src).isDirectory()) {
22610
+ if ((0, import_node_fs40.statSync)(src).isDirectory()) {
21885
22611
  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);
22612
+ } else if (!(0, import_node_fs40.existsSync)(dest)) {
22613
+ (0, import_node_fs40.mkdirSync)((0, import_node_path28.dirname)(dest), { recursive: true });
22614
+ (0, import_node_fs40.cpSync)(src, dest);
21889
22615
  copied++;
21890
22616
  }
21891
22617
  }
@@ -21896,20 +22622,20 @@ function carryLegacyContents(gateDir, verityDir) {
21896
22622
  async function needsMigration(root = repoRoot()) {
21897
22623
  const gateDir = (0, import_node_path28.join)(root, ".gate");
21898
22624
  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"))) {
22625
+ if ((0, import_node_fs40.existsSync)(gateDir) && !(0, import_node_fs40.existsSync)(verityDir)) return true;
22626
+ if ((0, import_node_fs40.existsSync)(gateDir) && (0, import_node_fs40.existsSync)(verityDir)) {
22627
+ 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
22628
  return true;
21903
22629
  }
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"))) {
22630
+ 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
22631
  return true;
21906
22632
  }
21907
22633
  }
21908
22634
  const claudeMd = (0, import_node_path28.join)(root, "CLAUDE.md");
21909
- if ((0, import_node_fs36.existsSync)(claudeMd) && hasLegacyMemoryBlock(readFileSyncSafe(claudeMd))) {
22635
+ if ((0, import_node_fs40.existsSync)(claudeMd) && hasLegacyMemoryBlock(readFileSyncSafe(claudeMd))) {
21910
22636
  return true;
21911
22637
  }
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"))) {
22638
+ 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
22639
  return true;
21914
22640
  }
21915
22641
  if (await hasLegacyHooksAt(root)) return true;
@@ -22053,7 +22779,7 @@ function resolveDataDir() {
22053
22779
  // local dev: running from repo root
22054
22780
  ];
22055
22781
  for (const candidate of candidates) {
22056
- if ((0, import_node_fs37.existsSync)((0, import_node_path29.join)(candidate, "skills"))) {
22782
+ if ((0, import_node_fs41.existsSync)((0, import_node_path29.join)(candidate, "skills"))) {
22057
22783
  return candidate;
22058
22784
  }
22059
22785
  }
@@ -22069,7 +22795,7 @@ function registerInitCommand(program2) {
22069
22795
  program2.command("init").description("Initialize Verity in the current project").option("--force", "Overwrite existing skills and hooks").action(async (opts) => {
22070
22796
  const force = opts.force ?? false;
22071
22797
  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));
22798
+ const isProject = projectMarkers.some((m) => (0, import_node_fs41.existsSync)(m));
22073
22799
  if (!isProject) {
22074
22800
  printError("No project detected in the current directory.");
22075
22801
  printInfo('Run "verity init" from your project root.');
@@ -22139,14 +22865,14 @@ function registerInitCommand(program2) {
22139
22865
  for (const skill of skills) {
22140
22866
  const src = (0, import_node_path29.join)(skillsSource, skill);
22141
22867
  const dest = (0, import_node_path29.join)(skillsDest, skill);
22142
- if (!(0, import_node_fs37.existsSync)(src)) {
22868
+ if (!(0, import_node_fs41.existsSync)(src)) {
22143
22869
  printWarn(` Skill data not found: ${skill}`);
22144
22870
  continue;
22145
22871
  }
22146
- if ((0, import_node_fs37.existsSync)(dest) && !force) {
22872
+ if ((0, import_node_fs41.existsSync)(dest) && !force) {
22147
22873
  const srcSkill = (0, import_node_path29.join)(src, "SKILL.md");
22148
22874
  const destSkill = (0, import_node_path29.join)(dest, "SKILL.md");
22149
- if ((0, import_node_fs37.existsSync)(destSkill)) {
22875
+ if ((0, import_node_fs41.existsSync)(destSkill)) {
22150
22876
  try {
22151
22877
  const srcContent = await (0, import_promises13.readFile)(srcSkill, "utf-8");
22152
22878
  const destContent = await (0, import_promises13.readFile)(destSkill, "utf-8");
@@ -22224,7 +22950,7 @@ function registerInitCommand(program2) {
22224
22950
  }
22225
22951
 
22226
22952
  // src/commands/uninstall.ts
22227
- var import_node_fs38 = require("node:fs");
22953
+ var import_node_fs42 = require("node:fs");
22228
22954
  var import_node_path30 = require("node:path");
22229
22955
  var SKILL_NAMES = [
22230
22956
  "verity-setup",
@@ -22245,10 +22971,10 @@ function registerUninstallCommand(program2) {
22245
22971
  const skillsRoot = projectPath(".claude/skills");
22246
22972
  for (const name of SKILL_NAMES) {
22247
22973
  const dir = (0, import_node_path30.join)(skillsRoot, name);
22248
- if ((0, import_node_fs38.existsSync)(dir)) {
22974
+ if ((0, import_node_fs42.existsSync)(dir)) {
22249
22975
  actions.push({
22250
22976
  label: `Remove .claude/skills/${name}/`,
22251
- apply: () => (0, import_node_fs38.rmSync)(dir, { recursive: true, force: true })
22977
+ apply: () => (0, import_node_fs42.rmSync)(dir, { recursive: true, force: true })
22252
22978
  });
22253
22979
  }
22254
22980
  }
@@ -22262,24 +22988,24 @@ function registerUninstallCommand(program2) {
22262
22988
  });
22263
22989
  }
22264
22990
  const verityDir = projectPath(VERITY_DIR);
22265
- if ((0, import_node_fs38.existsSync)(verityDir)) {
22991
+ if ((0, import_node_fs42.existsSync)(verityDir)) {
22266
22992
  actions.push({
22267
22993
  label: `Remove ${VERITY_DIR}/`,
22268
- apply: () => (0, import_node_fs38.rmSync)(verityDir, { recursive: true, force: true })
22994
+ apply: () => (0, import_node_fs42.rmSync)(verityDir, { recursive: true, force: true })
22269
22995
  });
22270
22996
  }
22271
22997
  if (!keepVerityMd) {
22272
22998
  const verityMd = projectPath(VERITY_MD_FILE);
22273
- if ((0, import_node_fs38.existsSync)(verityMd)) {
22999
+ if ((0, import_node_fs42.existsSync)(verityMd)) {
22274
23000
  actions.push({
22275
23001
  label: `Remove ${VERITY_MD_FILE}`,
22276
- apply: () => (0, import_node_fs38.rmSync)(verityMd, { force: true })
23002
+ apply: () => (0, import_node_fs42.rmSync)(verityMd, { force: true })
22277
23003
  });
22278
23004
  }
22279
23005
  }
22280
23006
  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);
23007
+ if ((0, import_node_fs42.existsSync)(path) && (0, import_node_fs42.statSync)(path).isDirectory() && (0, import_node_fs42.readdirSync)(path).length === 0) {
23008
+ (0, import_node_fs42.rmdirSync)(path);
22283
23009
  }
22284
23010
  };
22285
23011
  actions.push({
@@ -22291,10 +23017,10 @@ function registerUninstallCommand(program2) {
22291
23017
  });
22292
23018
  const home = process.env.HOME ?? "";
22293
23019
  const globalVerityDir = (0, import_node_path30.join)(home, ".verity");
22294
- if (purgeGlobal && (0, import_node_fs38.existsSync)(globalVerityDir)) {
23020
+ if (purgeGlobal && (0, import_node_fs42.existsSync)(globalVerityDir)) {
22295
23021
  actions.push({
22296
23022
  label: `Remove ~/.verity/ (global credentials \u2014 reconnect requires re-registration)`,
22297
- apply: () => (0, import_node_fs38.rmSync)(globalVerityDir, { recursive: true, force: true })
23023
+ apply: () => (0, import_node_fs42.rmSync)(globalVerityDir, { recursive: true, force: true })
22298
23024
  });
22299
23025
  }
22300
23026
  if (actions.length === 0) {
@@ -22488,7 +23214,7 @@ function registerTaskCommands(program2) {
22488
23214
  }
22489
23215
 
22490
23216
  // src/commands/reset.ts
22491
- var import_node_fs39 = require("node:fs");
23217
+ var import_node_fs43 = require("node:fs");
22492
23218
  var import_node_path31 = require("node:path");
22493
23219
  function registerResetCommand(program2) {
22494
23220
  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 +23252,11 @@ function registerResetCommand(program2) {
22526
23252
  }
22527
23253
  const cacheDir = projectPath(CACHE_DIR);
22528
23254
  let purged = 0;
22529
- if ((0, import_node_fs39.existsSync)(cacheDir)) {
22530
- for (const entry of (0, import_node_fs39.readdirSync)(cacheDir)) {
23255
+ if ((0, import_node_fs43.existsSync)(cacheDir)) {
23256
+ for (const entry of (0, import_node_fs43.readdirSync)(cacheDir)) {
22531
23257
  if (entry.startsWith("pending-")) {
22532
23258
  try {
22533
- (0, import_node_fs39.unlinkSync)((0, import_node_path31.join)(cacheDir, entry));
23259
+ (0, import_node_fs43.unlinkSync)((0, import_node_path31.join)(cacheDir, entry));
22534
23260
  purged++;
22535
23261
  } catch {
22536
23262
  }
@@ -22545,19 +23271,19 @@ function registerResetCommand(program2) {
22545
23271
  projectPath(`${VERITY_DIR}/.last-analysis`)
22546
23272
  ];
22547
23273
  for (const file of filesToClear) {
22548
- if ((0, import_node_fs39.existsSync)(file)) {
23274
+ if ((0, import_node_fs43.existsSync)(file)) {
22549
23275
  try {
22550
- (0, import_node_fs39.writeFileSync)(file, "");
23276
+ (0, import_node_fs43.writeFileSync)(file, "");
22551
23277
  } catch {
22552
23278
  }
22553
23279
  }
22554
23280
  }
22555
23281
  if (opts.all) {
22556
23282
  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)) {
23283
+ if ((0, import_node_fs43.existsSync)(logsDir)) {
23284
+ for (const entry of (0, import_node_fs43.readdirSync)(logsDir)) {
22559
23285
  try {
22560
- (0, import_node_fs39.unlinkSync)((0, import_node_path31.join)(logsDir, entry));
23286
+ (0, import_node_fs43.unlinkSync)((0, import_node_path31.join)(logsDir, entry));
22561
23287
  } catch {
22562
23288
  }
22563
23289
  }
@@ -22865,8 +23591,8 @@ function registerTelemetryCommands(program2) {
22865
23591
  }
22866
23592
 
22867
23593
  // 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");
23594
+ program.name("verity").description("CLI for Verity quality gate service").version("0.31.0-experimental.2221878").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) => {
23595
+ installStderrLog(actionCommand.name(), process.argv.slice(2), "0.31.0-experimental.2221878");
22870
23596
  setUserNamedServiceUrl(program.opts().serviceUrl);
22871
23597
  try {
22872
23598
  await foldLegacyLocalCredential();
@@ -22889,6 +23615,8 @@ registerAnalyzeCommand(program);
22889
23615
  registerBaselineCommands(program);
22890
23616
  registerReviewCommand(program);
22891
23617
  registerGuardCommand(program);
23618
+ registerIgnoreCommand(program);
23619
+ registerWaiveCommand(program);
22892
23620
  registerInitCommand(program);
22893
23621
  registerUninstallCommand(program);
22894
23622
  registerTaskCommands(program);