@codacy/verity-cli 0.30.1 → 0.31.0-experimental.48d33ea

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
  }
@@ -10477,7 +10478,8 @@ var REVIEWABLE_FILENAMES = /* @__PURE__ */ new Set([
10477
10478
  "Makefile",
10478
10479
  "Dockerfile",
10479
10480
  "Jenkinsfile",
10480
- "Vagrantfile"
10481
+ "Vagrantfile",
10482
+ ".verityignore"
10481
10483
  ]);
10482
10484
  var REVIEWABLE_PATH_PATTERNS = [
10483
10485
  /\.circleci\//,
@@ -10497,7 +10499,7 @@ var SECURITY_PATTERNS = [
10497
10499
  /Dockerfile/
10498
10500
  ];
10499
10501
  var PROD_SERVICE_URL = "https://ofcamwrjwrkazqvdchko.supabase.co/functions/v1";
10500
- var DEFAULT_SERVICE_URL = "".length > 0 ? "" : PROD_SERVICE_URL;
10502
+ var DEFAULT_SERVICE_URL = "https://wukeddyzpijoegyajtnc.supabase.co/functions/v1".length > 0 ? "https://wukeddyzpijoegyajtnc.supabase.co/functions/v1" : PROD_SERVICE_URL;
10501
10503
  var GITHUB_CLIENT_ID = "Iv23li88HxAi3ZrbYzWh";
10502
10504
  var GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
10503
10505
  var GITHUB_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
@@ -10507,6 +10509,7 @@ function githubAppInstallUrl(accountId) {
10507
10509
  return accountId != null ? `https://github.com/apps/${GITHUB_APP_SLUG}/installations/new/permissions?target_id=${accountId}` : GITHUB_APP_INSTALL_URL;
10508
10510
  }
10509
10511
  var ADVISORY_EPISODE_FILE = `${VERITY_DIR}/.advisory-episode`;
10512
+ var IGNORE_DECLARATION_FILE = `${VERITY_DIR}/.ignore-declaration`;
10510
10513
 
10511
10514
  // src/lib/output.ts
10512
10515
  var RED = "\x1B[0;31m";
@@ -13530,6 +13533,42 @@ var LEGACY_MD_END = "<!-- gate-memory:end -->";
13530
13533
  var LEGACY_PRESERVE_START = "<!-- gate-memory:preserve -->";
13531
13534
  var LEGACY_PRESERVE_END = "<!-- /gate-memory:preserve -->";
13532
13535
  var CLAUDE_MD_PROSE = [
13536
+ "## Project Memory",
13537
+ "",
13538
+ "This project has a knowledge graph maintained at `.verity/memory/`. Before starting",
13539
+ "non-trivial work, scan `.verity/memory/index.md` for decisions, gotchas, and patterns",
13540
+ "that may apply to the change you are about to make. Open specific node files via",
13541
+ "the Read tool when the title or scope suggests relevance.",
13542
+ "",
13543
+ "The graph is auto-maintained by Verity. Files at `.verity/memory/_archive/` are",
13544
+ "superseded \u2014 ignore them unless investigating history.",
13545
+ "",
13546
+ "> Durable, hand-curated guidance goes in the preserve region below (it survives",
13547
+ "> regeneration) or anywhere OUTSIDE these markers. Everything else between the",
13548
+ "> markers is tool-owned and overwritten on each run.",
13549
+ "",
13550
+ "## Housekeeping Turns",
13551
+ "",
13552
+ "When a turn will be pure housekeeping \u2014 pulling, installing dependencies,",
13553
+ "rebasing, a formatting sweep you are not authoring \u2014 declare it BEFORE doing it:",
13554
+ "",
13555
+ "```bash",
13556
+ 'verity ignore --turn --agent --reason "pulling latest before starting"',
13557
+ "```",
13558
+ "",
13559
+ "This skips the review for that turn, which saves the turn Verity would",
13560
+ "otherwise spend saying it had nothing to say. Use `--for 30m` instead of",
13561
+ "`--turn` when a single piece of housekeeping spans several turns.",
13562
+ "",
13563
+ "**It is a claim about the turn, not a way to silence review.** The declaration",
13564
+ "is checked against what the turn actually did: if anything is authored \u2014 by you,",
13565
+ "by a subagent, or by a shell command that can write files \u2014 it voids, the review",
13566
+ "runs anyway, and the broken declaration is reported. So declare housekeeping you",
13567
+ "are about to do, never work you have already done, and never as a way to get past",
13568
+ "a finding. Declarations are budgeted per session and every one is recorded with",
13569
+ "its reason."
13570
+ ].join("\n");
13571
+ var CLAUDE_MD_PROSE_PRE_IGNORE = [
13533
13572
  "## Project Memory",
13534
13573
  "",
13535
13574
  "This project has a knowledge graph maintained at `.verity/memory/`. Before starting",
@@ -13654,7 +13693,7 @@ function extractPreserveContent(interior) {
13654
13693
  }
13655
13694
  function stripKnownProse(interior) {
13656
13695
  const trimmed = interior.replace(/^\n+/, "");
13657
- for (const prose of [CLAUDE_MD_PROSE, CLAUDE_MD_PROSE_LEGACY]) {
13696
+ for (const prose of [CLAUDE_MD_PROSE, CLAUDE_MD_PROSE_PRE_IGNORE, CLAUDE_MD_PROSE_LEGACY]) {
13658
13697
  if (trimmed.startsWith(prose)) return trimmed.slice(prose.length);
13659
13698
  }
13660
13699
  return trimmed;
@@ -16270,7 +16309,145 @@ async function readHookStdin() {
16270
16309
 
16271
16310
  // src/commands/standard.ts
16272
16311
  var import_promises9 = require("node:fs/promises");
16312
+ var import_node_fs20 = require("node:fs");
16273
16313
  var import_yaml = __toESM(require_dist());
16314
+
16315
+ // src/lib/verityignore.ts
16316
+ var import_node_fs19 = require("node:fs");
16317
+ var EMPTY = { rules: [], securityOverlap: [], problems: [] };
16318
+ var SECURITY_PROBES = [
16319
+ ".env",
16320
+ ".env.local",
16321
+ ".env.production",
16322
+ "config/.env",
16323
+ "services/api/.env",
16324
+ "package-lock.json",
16325
+ "yarn.lock",
16326
+ "pnpm-lock.yaml",
16327
+ "Cargo.lock",
16328
+ "go.sum",
16329
+ "Gemfile.lock",
16330
+ "Dockerfile",
16331
+ "docker/Dockerfile"
16332
+ ];
16333
+ function isSecuritySensitive(path) {
16334
+ return SECURITY_PATTERNS.some((p) => p.test(path));
16335
+ }
16336
+ function compile(pattern) {
16337
+ let p = pattern;
16338
+ const dirOnly = p.endsWith("/");
16339
+ if (dirOnly) p = p.slice(0, -1);
16340
+ const anchored = p.includes("/");
16341
+ if (p.startsWith("/")) p = p.slice(1);
16342
+ const base = anchored ? p : `**/${p}`;
16343
+ const forms = dirOnly ? [`${base}/**/*`] : [base, `${base}/**`];
16344
+ const regexes = [];
16345
+ for (const f of forms) {
16346
+ try {
16347
+ regexes.push(globToRegex(f));
16348
+ } catch {
16349
+ }
16350
+ }
16351
+ if (regexes.length === 0) return () => false;
16352
+ return (path) => regexes.some((r) => r.test(path));
16353
+ }
16354
+ function parseVerityIgnore(content) {
16355
+ const rules = [];
16356
+ const problems = [];
16357
+ const lines = content.split("\n");
16358
+ for (let i = 0; i < lines.length; i++) {
16359
+ const lineNo = i + 1;
16360
+ let raw = lines[i];
16361
+ raw = raw.replace(/(?<!\\)\s+$/, "");
16362
+ if (raw.length === 0) continue;
16363
+ if (raw.startsWith("#")) continue;
16364
+ let negated = false;
16365
+ if (raw.startsWith("!")) {
16366
+ negated = true;
16367
+ raw = raw.slice(1);
16368
+ } else if (raw.startsWith("\\!") || raw.startsWith("\\#")) {
16369
+ raw = raw.slice(1);
16370
+ }
16371
+ if (raw.length === 0) {
16372
+ problems.push(`line ${lineNo}: "!" with no pattern after it`);
16373
+ continue;
16374
+ }
16375
+ if (raw === "**" || raw === "*" || raw === "/" || raw === "**/*") {
16376
+ problems.push(
16377
+ `line ${lineNo}: "${raw}" would exclude the whole repository from review \u2014 refused. List the directories you mean instead.`
16378
+ );
16379
+ continue;
16380
+ }
16381
+ rules.push({ raw, line: lineNo, negated, test: compile(raw) });
16382
+ }
16383
+ const byRule = /* @__PURE__ */ new Map();
16384
+ for (const probe of SECURITY_PROBES) {
16385
+ let responsible = null;
16386
+ for (const rule of rules) {
16387
+ if (rule.test(probe)) responsible = rule.negated ? null : rule;
16388
+ }
16389
+ if (!responsible) continue;
16390
+ const entry = byRule.get(responsible.line) ?? { raw: responsible.raw, line: responsible.line, hides: [] };
16391
+ entry.hides.push(probe);
16392
+ byRule.set(responsible.line, entry);
16393
+ }
16394
+ const securityOverlap = [...byRule.values()].sort((a, b) => a.line - b.line);
16395
+ return { rules, securityOverlap, problems };
16396
+ }
16397
+ function decide(rules, path) {
16398
+ let excluded = false;
16399
+ for (const rule of rules) {
16400
+ if (rule.test(path)) excluded = !rule.negated;
16401
+ }
16402
+ return excluded;
16403
+ }
16404
+ function isIgnored(ig, path) {
16405
+ return decide(ig.rules, path);
16406
+ }
16407
+ function loadVerityIgnore() {
16408
+ const file = projectPath(VERITYIGNORE_FILE);
16409
+ if (!(0, import_node_fs19.existsSync)(file)) return EMPTY;
16410
+ try {
16411
+ return parseVerityIgnore((0, import_node_fs19.readFileSync)(file, "utf-8"));
16412
+ } catch {
16413
+ return EMPTY;
16414
+ }
16415
+ }
16416
+ function partitionIgnored(paths, ig) {
16417
+ const suspended = paths.some((p) => p === VERITYIGNORE_FILE);
16418
+ if (suspended || ig.rules.length === 0) {
16419
+ return { rules: ig.rules.length, kept: [...paths], ignored: [], securityExcluded: [], suspended };
16420
+ }
16421
+ const kept = [];
16422
+ const ignored = [];
16423
+ for (const p of paths) {
16424
+ if (p !== VERITYIGNORE_FILE && isIgnored(ig, p)) ignored.push(p);
16425
+ else kept.push(p);
16426
+ }
16427
+ return {
16428
+ rules: ig.rules.length,
16429
+ kept,
16430
+ ignored,
16431
+ securityExcluded: ignored.filter(isSecuritySensitive),
16432
+ suspended: false
16433
+ };
16434
+ }
16435
+ function ignoreShare(kept, ignored) {
16436
+ const total = kept + ignored;
16437
+ if (total === 0) return 0;
16438
+ return Math.round(ignored / total * 1e3) / 1e3;
16439
+ }
16440
+ function describeSecurityOverlap(ig) {
16441
+ if (ig.securityOverlap.length === 0) return null;
16442
+ const lines = ig.securityOverlap.map(
16443
+ (o) => ` line ${o.line}: "${o.raw}" would hide ${o.hides.slice(0, 3).join(", ")}` + (o.hides.length > 3 ? ` (+${o.hides.length - 3} more)` : "")
16444
+ );
16445
+ return `.verityignore: ${ig.securityOverlap.length} pattern(s) can hide security-sensitive files from review:
16446
+ ${lines.join("\n")}
16447
+ These are still excluded \u2014 this is a warning, not a refusal. Add a \`!\` rule to keep one in scope, e.g. \`!.env*\`.`;
16448
+ }
16449
+
16450
+ // src/commands/standard.ts
16274
16451
  function registerStandardCommands(program2) {
16275
16452
  const standard = program2.command("standard").description("Manage the project Standard");
16276
16453
  standard.command("push").description("Upload the project Standard to the service").option("--file <path>", "Path to standard YAML file", STANDARD_FILE).option("--created-by <name>", "Who created this version", "claude-code").action(async (opts) => {
@@ -16299,12 +16476,25 @@ function registerStandardCommands(program2) {
16299
16476
  printError(`Invalid YAML in ${opts.file}: ${err.message}`);
16300
16477
  process.exit(1);
16301
16478
  }
16479
+ let ignoreRaw = null;
16480
+ const ignorePath = projectPath(VERITYIGNORE_FILE);
16481
+ if ((0, import_node_fs20.existsSync)(ignorePath)) {
16482
+ try {
16483
+ ignoreRaw = (0, import_node_fs20.readFileSync)(ignorePath, "utf-8");
16484
+ const overlap = describeSecurityOverlap(parseVerityIgnore(ignoreRaw));
16485
+ if (overlap) printWarn(overlap);
16486
+ } catch {
16487
+ }
16488
+ }
16302
16489
  const result = await apiRequest({
16303
16490
  method: "POST",
16304
16491
  path: "/standards",
16305
16492
  serviceUrl: urlResult.data,
16306
16493
  token: tokenResult.data.token,
16307
- body: { content, created_by: opts.createdBy },
16494
+ body: {
16495
+ content: ignoreRaw === null ? content : { ...content, verityignore: ignoreRaw },
16496
+ created_by: opts.createdBy
16497
+ },
16308
16498
  verbose: globals.verbose
16309
16499
  });
16310
16500
  if (!result.ok) {
@@ -16522,6 +16712,309 @@ function formatRunDetail(run) {
16522
16712
  return lines;
16523
16713
  }
16524
16714
 
16715
+ // src/lib/ignore-declaration.ts
16716
+ var import_node_fs22 = require("node:fs");
16717
+
16718
+ // src/lib/debounce.ts
16719
+ var import_node_fs21 = require("node:fs");
16720
+ var import_node_crypto10 = require("node:crypto");
16721
+ function scopedFile(base, sessionId) {
16722
+ if (!sessionId) return base;
16723
+ return `${base}.${(0, import_node_crypto10.createHash)("sha1").update(sessionId).digest("hex").slice(0, 12)}`;
16724
+ }
16725
+ function checkDebounce(debounceSeconds = DEBOUNCE_SECONDS, sessionId) {
16726
+ const file = scopedFile(DEBOUNCE_FILE, sessionId);
16727
+ if (!(0, import_node_fs21.existsSync)(file)) return null;
16728
+ try {
16729
+ const lastTs = parseInt((0, import_node_fs21.readFileSync)(file, "utf-8").trim(), 10);
16730
+ const nowTs = Math.floor(Date.now() / 1e3);
16731
+ const elapsed = nowTs - lastTs;
16732
+ if (elapsed < debounceSeconds) {
16733
+ return `Debounced \u2014 last analysis was ${elapsed}s ago`;
16734
+ }
16735
+ } catch {
16736
+ }
16737
+ return null;
16738
+ }
16739
+ function checkMtime(files, bypassForRecentCommits, sessionId) {
16740
+ if (bypassForRecentCommits) return null;
16741
+ const file = scopedFile(DEBOUNCE_FILE, sessionId);
16742
+ if (!(0, import_node_fs21.existsSync)(file)) return null;
16743
+ let debounceTime;
16744
+ try {
16745
+ debounceTime = (0, import_node_fs21.statSync)(file).mtimeMs;
16746
+ } catch {
16747
+ return null;
16748
+ }
16749
+ for (const f of files) {
16750
+ const resolved = resolveFile(f);
16751
+ if (!resolved) continue;
16752
+ try {
16753
+ const stat3 = (0, import_node_fs21.statSync)(resolved);
16754
+ if (stat3.mtimeMs > debounceTime) {
16755
+ return null;
16756
+ }
16757
+ } catch {
16758
+ continue;
16759
+ }
16760
+ }
16761
+ return "No files modified since last analysis";
16762
+ }
16763
+ function computeContentHash(files) {
16764
+ const hash = (0, import_node_crypto10.createHash)("sha1");
16765
+ const sorted = [...files].sort();
16766
+ for (const f of sorted) {
16767
+ const resolved = resolveFile(f) ?? f;
16768
+ try {
16769
+ if ((0, import_node_fs21.existsSync)(resolved)) {
16770
+ hash.update((0, import_node_fs21.readFileSync)(resolved));
16771
+ }
16772
+ } catch {
16773
+ }
16774
+ }
16775
+ return hash.digest("hex");
16776
+ }
16777
+ function checkContentHash(files, sessionId) {
16778
+ const hash = computeContentHash(files);
16779
+ const file = scopedFile(HASH_FILE, sessionId);
16780
+ if ((0, import_node_fs21.existsSync)(file)) {
16781
+ try {
16782
+ const storedHash = (0, import_node_fs21.readFileSync)(file, "utf-8").trim();
16783
+ if (hash === storedHash) {
16784
+ return { skip: "No source changes since last analysis", hash };
16785
+ }
16786
+ } catch {
16787
+ }
16788
+ }
16789
+ return { skip: null, hash };
16790
+ }
16791
+ function recordAnalysisStart(sessionId) {
16792
+ (0, import_node_fs21.mkdirSync)(VERITY_DIR, { recursive: true });
16793
+ (0, import_node_fs21.writeFileSync)(scopedFile(DEBOUNCE_FILE, sessionId), String(Math.floor(Date.now() / 1e3)));
16794
+ }
16795
+ function recordPassHash(hash, sessionId) {
16796
+ (0, import_node_fs21.writeFileSync)(scopedFile(HASH_FILE, sessionId), hash);
16797
+ }
16798
+ function narrowToRecent(files, sessionId) {
16799
+ const file = scopedFile(DEBOUNCE_FILE, sessionId);
16800
+ if (!(0, import_node_fs21.existsSync)(file)) return files;
16801
+ let debounceTime;
16802
+ try {
16803
+ debounceTime = (0, import_node_fs21.statSync)(file).mtimeMs;
16804
+ } catch {
16805
+ return files;
16806
+ }
16807
+ const recent = files.filter((f) => {
16808
+ try {
16809
+ return (0, import_node_fs21.existsSync)(f) && (0, import_node_fs21.statSync)(f).mtimeMs > debounceTime;
16810
+ } catch {
16811
+ return false;
16812
+ }
16813
+ });
16814
+ return recent.length > 0 ? recent : files;
16815
+ }
16816
+ function readIteration(currentCommit, _contentHash) {
16817
+ return Math.max(1, readBlockState(currentCommit).attempts);
16818
+ }
16819
+ var NO_BLOCKS = { attempts: 0, blocks: 0, fingerprint: null };
16820
+ function readBlockState(currentCommit, opts) {
16821
+ if (opts?.newUserPrompt) return NO_BLOCKS;
16822
+ if (!(0, import_node_fs21.existsSync)(ITERATION_FILE)) return NO_BLOCKS;
16823
+ try {
16824
+ const stored = (0, import_node_fs21.readFileSync)(ITERATION_FILE, "utf-8").trim();
16825
+ const parsed = stored.startsWith("{") ? parseJsonState(stored) : parseLegacyState(stored);
16826
+ if (!parsed) return NO_BLOCKS;
16827
+ if (parsed.commit !== currentCommit) return NO_BLOCKS;
16828
+ if (parsed.ts > 0 && Math.floor(Date.now() / 1e3) - parsed.ts > 600) return NO_BLOCKS;
16829
+ return { attempts: parsed.attempts, blocks: parsed.blocks, fingerprint: parsed.fingerprint };
16830
+ } catch {
16831
+ return NO_BLOCKS;
16832
+ }
16833
+ }
16834
+ function parseJsonState(raw) {
16835
+ const o = JSON.parse(raw);
16836
+ const attempts = typeof o.attempts === "number" ? o.attempts : NaN;
16837
+ if (isNaN(attempts)) return null;
16838
+ return {
16839
+ attempts,
16840
+ blocks: typeof o.blocks === "number" ? o.blocks : attempts,
16841
+ fingerprint: typeof o.fingerprint === "string" && o.fingerprint ? o.fingerprint : null,
16842
+ commit: typeof o.commit === "string" ? o.commit : "",
16843
+ ts: typeof o.ts === "number" ? o.ts : 0
16844
+ };
16845
+ }
16846
+ function parseLegacyState(raw) {
16847
+ const parts = raw.split(":");
16848
+ const n = parseInt(parts[0], 10);
16849
+ if (isNaN(n)) return null;
16850
+ return {
16851
+ attempts: n,
16852
+ // The old file has no separate block count; the old counter is the closest
16853
+ // honest answer, and it errs toward releasing sooner rather than later.
16854
+ blocks: n,
16855
+ fingerprint: parts.slice(3).join(":") || null,
16856
+ commit: parts[1] ?? "",
16857
+ ts: parseInt(parts[2] ?? "0", 10)
16858
+ };
16859
+ }
16860
+ function findingsFingerprint(findings) {
16861
+ const keys = findings.map((f) => `${String(f.pattern_id ?? "?")}|${String(f.file ?? "?")}`).filter((k) => k !== "?|?");
16862
+ return [...new Set(keys)].sort().join(",");
16863
+ }
16864
+ function isSameProblem(previous, current) {
16865
+ if (!previous || !current) return false;
16866
+ const prev = new Set(previous.split(","));
16867
+ return current.split(",").some((k) => prev.has(k));
16868
+ }
16869
+ function writeBlockState(commit, state) {
16870
+ (0, import_node_fs21.mkdirSync)(VERITY_DIR, { recursive: true });
16871
+ (0, import_node_fs21.writeFileSync)(
16872
+ ITERATION_FILE,
16873
+ JSON.stringify({
16874
+ v: 2,
16875
+ attempts: state.attempts,
16876
+ blocks: state.blocks,
16877
+ commit,
16878
+ ts: Math.floor(Date.now() / 1e3),
16879
+ fingerprint: state.fingerprint ?? void 0
16880
+ })
16881
+ );
16882
+ }
16883
+ function resetBlockState(commit) {
16884
+ writeBlockState(commit, { attempts: 0, blocks: 0, fingerprint: null });
16885
+ }
16886
+
16887
+ // src/lib/ignore-declaration.ts
16888
+ var IGNORE_BUDGET = 3;
16889
+ var MAX_WINDOW_SECONDS = 60 * 60;
16890
+ var TURN_FUSE_SECONDS = 10 * 60;
16891
+ function parseDuration(input) {
16892
+ const trimmed = input.trim().toLowerCase();
16893
+ const m = /^(\d+)(s|m|h)?$/.exec(trimmed);
16894
+ if (!m) {
16895
+ return { ok: false, error: `Could not read "${input}" as a duration. Use 30m, 45s, or 1h.` };
16896
+ }
16897
+ const n = parseInt(m[1], 10);
16898
+ if (!Number.isFinite(n) || n <= 0) {
16899
+ return { ok: false, error: `A duration must be a positive number of seconds, minutes or hours \u2014 got "${input}".` };
16900
+ }
16901
+ const unit = m[2] ?? "m";
16902
+ const seconds = unit === "s" ? n : unit === "h" ? n * 3600 : n * 60;
16903
+ if (seconds > MAX_WINDOW_SECONDS) {
16904
+ return {
16905
+ ok: false,
16906
+ error: `${input} is longer than the ${MAX_WINDOW_SECONDS / 60}-minute maximum. An ignore is meant to cover one piece of housekeeping, not a sitting \u2014 declare it again when you need it.`
16907
+ };
16908
+ }
16909
+ return { ok: true, seconds };
16910
+ }
16911
+ function resolveActive(state, now) {
16912
+ if (!state?.active) return null;
16913
+ if (state.active.expires <= now) return null;
16914
+ return state.active;
16915
+ }
16916
+ function verifyDeclaration(input) {
16917
+ const d = input.declaration;
16918
+ if (!d) return { honoured: false, void: false };
16919
+ if (!input.authorshipIsObservable) {
16920
+ return {
16921
+ honoured: false,
16922
+ void: true,
16923
+ why: "the transcript window could not show what this turn did, so the declaration could not be checked"
16924
+ };
16925
+ }
16926
+ if (input.commandRecordTruncated) {
16927
+ return {
16928
+ honoured: false,
16929
+ void: true,
16930
+ why: "the record of commands run this turn was incomplete, so the declaration could not be checked"
16931
+ };
16932
+ }
16933
+ if (input.agentAuthoredFiles > 0) {
16934
+ return {
16935
+ honoured: false,
16936
+ void: true,
16937
+ why: `${input.agentAuthoredFiles} file${input.agentAuthoredFiles === 1 ? " was" : "s were"} authored during it`
16938
+ };
16939
+ }
16940
+ if (input.subagents > 0) {
16941
+ return {
16942
+ honoured: false,
16943
+ void: true,
16944
+ why: "work was delegated to a subagent during it, whose authorship this turn cannot account for"
16945
+ };
16946
+ }
16947
+ const authoring = [...input.agentCommands ?? [], ...input.userCommands ?? []].filter(
16948
+ (c) => !isNonAuthoringCommand(c)
16949
+ );
16950
+ if (authoring.length > 0) {
16951
+ return {
16952
+ honoured: false,
16953
+ void: true,
16954
+ why: `a command that can write files ran during it (${authoring[0]})`
16955
+ };
16956
+ }
16957
+ return { honoured: true };
16958
+ }
16959
+ function stateFile(sessionId) {
16960
+ return projectPath(scopedFile(IGNORE_DECLARATION_FILE, sessionId));
16961
+ }
16962
+ function ignoreStateKeys(token, sessionId) {
16963
+ const scoped = sessionScopeKey(token, sessionId);
16964
+ const userOnly = sessionScopeKey(token, void 0);
16965
+ return scoped === userOnly ? [scoped] : [scoped, userOnly];
16966
+ }
16967
+ function resolveIgnoreState(keys) {
16968
+ for (const key of keys) {
16969
+ const state = readIgnoreState(key);
16970
+ if (state) return { state, key };
16971
+ }
16972
+ return null;
16973
+ }
16974
+ function readIgnoreState(sessionId) {
16975
+ const file = stateFile(sessionId);
16976
+ if (!(0, import_node_fs22.existsSync)(file)) return null;
16977
+ try {
16978
+ const o = JSON.parse((0, import_node_fs22.readFileSync)(file, "utf-8")) ?? {};
16979
+ const spent = typeof o.spent === "number" ? o.spent : 0;
16980
+ const raw = o.active;
16981
+ let active = null;
16982
+ if (raw && typeof raw === "object") {
16983
+ const scope2 = raw.scope === "window" ? "window" : raw.scope === "turn" ? "turn" : null;
16984
+ const expires = typeof raw.expires === "number" ? raw.expires : null;
16985
+ if (scope2 && expires != null) {
16986
+ active = {
16987
+ scope: scope2,
16988
+ origin: raw.origin === "agent" ? "agent" : "user",
16989
+ reason: typeof raw.reason === "string" ? raw.reason : "",
16990
+ at: typeof raw.at === "number" ? raw.at : 0,
16991
+ expires
16992
+ };
16993
+ }
16994
+ }
16995
+ return { v: 1, active, spent };
16996
+ } catch {
16997
+ return null;
16998
+ }
16999
+ }
17000
+ function writeIgnoreState(state, sessionId) {
17001
+ try {
17002
+ (0, import_node_fs22.mkdirSync)(projectPath(VERITY_DIR), { recursive: true });
17003
+ (0, import_node_fs22.writeFileSync)(stateFile(sessionId), JSON.stringify({ v: 1, active: state.active, spent: state.spent }));
17004
+ } catch {
17005
+ }
17006
+ }
17007
+ function clearActiveDeclaration(sessionId) {
17008
+ const prev = readIgnoreState(sessionId);
17009
+ if (!prev) return;
17010
+ writeIgnoreState({ v: 1, active: null, spent: prev.spent }, sessionId);
17011
+ }
17012
+ function describeRemaining(d, now) {
17013
+ const left = Math.max(0, d.expires - now);
17014
+ if (left >= 90) return `${Math.round(left / 60)}m left`;
17015
+ return `${left}s left`;
17016
+ }
17017
+
16525
17018
  // src/commands/status.ts
16526
17019
  function timeAgo(isoDate) {
16527
17020
  const ms = Date.now() - new Date(isoDate).getTime();
@@ -16630,6 +17123,39 @@ function registerStatusCommand(program2) {
16630
17123
  printInfo(`Trend: ${r.trend}`);
16631
17124
  printInfo(`Runs: ${r.count} recorded`);
16632
17125
  }
17126
+ {
17127
+ const tokenForScope = tokenResult.data.token;
17128
+ const found = resolveIgnoreState(
17129
+ ignoreStateKeys(tokenForScope, process.env.CLAUDE_SESSION_ID || void 0)
17130
+ );
17131
+ const now = Math.floor(Date.now() / 1e3);
17132
+ const active = resolveActive(found?.state ?? null, now);
17133
+ const spent = found?.state.spent ?? 0;
17134
+ if (active) {
17135
+ printInfo("");
17136
+ printInfo("--- Ignore Declared ---");
17137
+ printInfo(`Scope: ${active.scope === "turn" ? "the next turn" : `a window, ${describeRemaining(active, now)}`}`);
17138
+ printInfo(`Reason: ${active.reason}`);
17139
+ printInfo(`Declared by: ${active.origin === "agent" ? "the agent" : "you"}`);
17140
+ printInfo(`Budget: ${spent}/${IGNORE_BUDGET} declarations this session`);
17141
+ printInfo('Turns that author anything are still reviewed \u2014 the declaration voids. "verity ignore clear" cancels it.');
17142
+ } else if (spent > 0) {
17143
+ printInfo("");
17144
+ printInfo(`Ignore budget: ${spent}/${IGNORE_BUDGET} declarations used this session.`);
17145
+ }
17146
+ }
17147
+ {
17148
+ const ig = loadVerityIgnore();
17149
+ if (ig.rules.length > 0 || ig.problems.length > 0) {
17150
+ printInfo("");
17151
+ printInfo("--- .verityignore ---");
17152
+ printInfo(`Rules: ${ig.rules.length}`);
17153
+ for (const problem of ig.problems) printWarn(` ${problem}`);
17154
+ const overlap = describeSecurityOverlap(ig);
17155
+ if (overlap) printWarn(overlap);
17156
+ printInfo("An edit to .verityignore suspends every rule for that turn \u2014 the edit is always reviewed.");
17157
+ }
17158
+ }
16633
17159
  if (mem.pending_items && mem.pending_items.length > 0) {
16634
17160
  printInfo("");
16635
17161
  printInfo("--- Pending Items ---");
@@ -16793,6 +17319,7 @@ function createRun(opts, globals) {
16793
17319
  allChanged: [],
16794
17320
  hasRecentCommitFiles: false,
16795
17321
  changedUniverse: [],
17322
+ verityIgnored: { rules: 0, kept: [], ignored: [], securityExcluded: [], suspended: false },
16796
17323
  analyzable: [],
16797
17324
  reviewable: [],
16798
17325
  securityFiles: [],
@@ -16826,6 +17353,7 @@ function createRun(opts, globals) {
16826
17353
  deletedNodePaths: [],
16827
17354
  editedUploads: [],
16828
17355
  autoSeedNotice: null,
17356
+ voidedIgnoreNotice: null,
16829
17357
  foldResult: null,
16830
17358
  foldConservation: null,
16831
17359
  memorySession: null,
@@ -16846,7 +17374,7 @@ function createRun(opts, globals) {
16846
17374
  }
16847
17375
 
16848
17376
  // src/lib/stderr-log.ts
16849
- var import_node_fs19 = require("node:fs");
17377
+ var import_node_fs23 = require("node:fs");
16850
17378
  var TOKEN_RE2 = /verity_[0-9a-f]{16,}/g;
16851
17379
  var ANSI_RE = /\u001b\[[0-?]*[ -/]*[@-~]/g;
16852
17380
  function scrub(s) {
@@ -16859,9 +17387,9 @@ function append(text) {
16859
17387
  try {
16860
17388
  const dir = projectPath(DEBUG_LOG_DIR);
16861
17389
  const file = projectPath(STDERR_LOG_FILE);
16862
- (0, import_node_fs19.mkdirSync)(dir, { recursive: true });
17390
+ (0, import_node_fs23.mkdirSync)(dir, { recursive: true });
16863
17391
  rotateIfNeeded(file);
16864
- (0, import_node_fs19.appendFileSync)(file, text);
17392
+ (0, import_node_fs23.appendFileSync)(file, text);
16865
17393
  } catch {
16866
17394
  }
16867
17395
  }
@@ -17019,7 +17547,7 @@ function installRunEvidence(run) {
17019
17547
 
17020
17548
  // src/lib/git-frame.ts
17021
17549
  var import_node_child_process7 = require("node:child_process");
17022
- var import_node_fs20 = require("node:fs");
17550
+ var import_node_fs24 = require("node:fs");
17023
17551
  var import_node_os3 = require("node:os");
17024
17552
  var import_node_path18 = require("node:path");
17025
17553
  var import_node_path19 = require("node:path");
@@ -17158,14 +17686,14 @@ function gitAt(dir, args) {
17158
17686
  }
17159
17687
  function realpathOr(p) {
17160
17688
  try {
17161
- return import_node_fs20.realpathSync.native(p);
17689
+ return import_node_fs24.realpathSync.native(p);
17162
17690
  } catch {
17163
17691
  return (0, import_node_path18.resolve)(p);
17164
17692
  }
17165
17693
  }
17166
17694
  function resolveFrame(input) {
17167
17695
  const found = findMomentSegment(input.command, input.on);
17168
- const hookDirUsable = !!input.hookCwd && (0, import_node_fs20.existsSync)(input.hookCwd);
17696
+ const hookDirUsable = !!input.hookCwd && (0, import_node_fs24.existsSync)(input.hookCwd);
17169
17697
  const baseDir = hookDirUsable ? input.hookCwd : process.cwd();
17170
17698
  let anchor = hookDirUsable ? "hook-cwd" : "process-cwd";
17171
17699
  const refuse = (refusal) => ({
@@ -17188,7 +17716,7 @@ function resolveFrame(input) {
17188
17716
  if (dirs.size > 1) return refuse(`target:multiple ${found.moment} targets in one command`);
17189
17717
  const targetDir = dirs.size === 1 ? [...dirs][0] : baseDir;
17190
17718
  if (targetDir !== baseDir) {
17191
- if (!(0, import_node_fs20.existsSync)(targetDir)) return refuse(`target:directory does not exist: ${targetDir}`);
17719
+ if (!(0, import_node_fs24.existsSync)(targetDir)) return refuse(`target:directory does not exist: ${targetDir}`);
17192
17720
  dir = targetDir;
17193
17721
  }
17194
17722
  }
@@ -17227,7 +17755,7 @@ var SHA_RE2 = /^[0-9a-f]{40}$/;
17227
17755
  function baselineShaAt(frame) {
17228
17756
  if (!frame.worktreeRoot) return null;
17229
17757
  try {
17230
- const sha = (0, import_node_fs20.readFileSync)((0, import_node_path19.join)(frame.worktreeRoot, BASELINE_SHA_FILE), "utf-8").trim();
17758
+ const sha = (0, import_node_fs24.readFileSync)((0, import_node_path19.join)(frame.worktreeRoot, BASELINE_SHA_FILE), "utf-8").trim();
17231
17759
  if (!SHA_RE2.test(sha)) return null;
17232
17760
  return refResolves(frame, sha) ? sha : null;
17233
17761
  } catch {
@@ -17340,7 +17868,7 @@ function truthy(v) {
17340
17868
  }
17341
17869
 
17342
17870
  // src/lib/transcript.ts
17343
- var import_node_fs21 = require("node:fs");
17871
+ var import_node_fs25 = require("node:fs");
17344
17872
  var MAX_READ_BYTES = 256 * 1024;
17345
17873
  var SMALL_FILE_BYTES = 64 * 1024;
17346
17874
  var MAX_FILES_LIST = 20;
@@ -17366,7 +17894,7 @@ async function extractActionSummary(transcriptPath) {
17366
17894
  function readTurnLines(transcriptPath) {
17367
17895
  let size;
17368
17896
  try {
17369
- size = (0, import_node_fs21.statSync)(transcriptPath).size;
17897
+ size = (0, import_node_fs25.statSync)(transcriptPath).size;
17370
17898
  } catch {
17371
17899
  return null;
17372
17900
  }
@@ -17374,7 +17902,7 @@ function readTurnLines(transcriptPath) {
17374
17902
  let raw;
17375
17903
  let windowed = false;
17376
17904
  if (size <= SMALL_FILE_BYTES) {
17377
- raw = (0, import_node_fs21.readFileSync)(transcriptPath, "utf-8");
17905
+ raw = (0, import_node_fs25.readFileSync)(transcriptPath, "utf-8");
17378
17906
  } else {
17379
17907
  windowed = true;
17380
17908
  const buf = Buffer.alloc(Math.min(MAX_READ_BYTES, size));
@@ -17432,6 +17960,7 @@ function buildSummary(lines) {
17432
17960
  const commands = [];
17433
17961
  const userCommands = [];
17434
17962
  let userCommandsTruncated = false;
17963
+ let commandsTruncated = false;
17435
17964
  let searches = 0;
17436
17965
  let subagents = 0;
17437
17966
  let webFetches = 0;
@@ -17472,7 +18001,10 @@ function buildSummary(lines) {
17472
18001
  totalToolCalls++;
17473
18002
  const toolName = block.name ?? "unknown";
17474
18003
  toolCounts[toolName] = (toolCounts[toolName] ?? 0) + 1;
17475
- if (totalToolCalls > MAX_TOOL_BLOCKS) continue;
18004
+ if (totalToolCalls > MAX_TOOL_BLOCKS) {
18005
+ if (toolName === "Bash") commandsTruncated = true;
18006
+ continue;
18007
+ }
17476
18008
  const input = block.input ?? {};
17477
18009
  switch (toolName) {
17478
18010
  case "Read":
@@ -17492,9 +18024,11 @@ function buildSummary(lines) {
17492
18024
  addPath(filesEdited, input.file_path);
17493
18025
  break;
17494
18026
  case "Bash": {
17495
- const cmd = sanitizeCommand(input.command);
17496
- if (cmd && commands.length < MAX_COMMANDS) {
17497
- commands.push(cmd);
18027
+ const { cmd, lost } = sanitizeCommandWithLoss(input.command);
18028
+ if (lost) commandsTruncated = true;
18029
+ if (cmd) {
18030
+ if (commands.length < MAX_COMMANDS) commands.push(cmd);
18031
+ else commandsTruncated = true;
17498
18032
  }
17499
18033
  break;
17500
18034
  }
@@ -17537,6 +18071,7 @@ function buildSummary(lines) {
17537
18071
  ],
17538
18072
  searches,
17539
18073
  commands,
18074
+ ...commandsTruncated ? { commands_truncated: true } : {},
17540
18075
  user_commands: userCommands,
17541
18076
  ...userCommandsTruncated ? { user_commands_truncated: true } : {},
17542
18077
  subagents,
@@ -17547,6 +18082,7 @@ function buildSummary(lines) {
17547
18082
  };
17548
18083
  if (JSON.stringify(summary).length > MAX_SUMMARY_BYTES) {
17549
18084
  summary.commands = [];
18085
+ summary.commands_truncated = true;
17550
18086
  summary.user_commands = [];
17551
18087
  summary.user_commands_truncated = true;
17552
18088
  if (JSON.stringify(summary).length > MAX_SUMMARY_BYTES) {
@@ -17582,12 +18118,22 @@ function userTypedCommands(entry) {
17582
18118
  }
17583
18119
  return { commands, truncated };
17584
18120
  }
18121
+ function sanitizeCommandWithLoss(rawCmd) {
18122
+ if (typeof rawCmd !== "string" || !rawCmd) return { cmd: null, lost: false };
18123
+ const lines = rawCmd.split("\n");
18124
+ const lost = lines.slice(1).some((l) => l.trim().length > 0);
18125
+ const first = lines[0];
18126
+ const cmd = sanitizeCommand(first);
18127
+ const hadSeparator = SEPARATORS.some((sep2) => first.indexOf(sep2) > 0);
18128
+ return { cmd, lost: lost || !hadSeparator && first.length > MAX_COMMAND_CHARS };
18129
+ }
18130
+ var SEPARATORS = [" | ", " > ", " >> ", " 2>", " && ", " ; "];
17585
18131
  function sanitizeCommand(rawCmd) {
17586
18132
  if (typeof rawCmd !== "string" || !rawCmd) return null;
17587
18133
  let cmd = rawCmd.split("\n")[0];
17588
18134
  let cut = -1;
17589
18135
  let marker = "";
17590
- for (const sep2 of [" | ", " > ", " >> ", " 2>", " && ", " ; "]) {
18136
+ for (const sep2 of SEPARATORS) {
17591
18137
  const idx = cmd.indexOf(sep2);
17592
18138
  if (idx > 0 && (cut === -1 || idx < cut)) {
17593
18139
  cut = idx;
@@ -17837,7 +18383,7 @@ function channelSilence(input) {
17837
18383
  // src/lib/cli-version.ts
17838
18384
  function cliVersion() {
17839
18385
  try {
17840
- return true ? "0.30.1" : "dev";
18386
+ return true ? "0.31.0-experimental.48d33ea" : "dev";
17841
18387
  } catch {
17842
18388
  return "dev";
17843
18389
  }
@@ -17878,7 +18424,7 @@ async function sendSkipBeacon(ctx, reason) {
17878
18424
 
17879
18425
  // src/lib/static-analysis.ts
17880
18426
  var import_node_child_process8 = require("node:child_process");
17881
- var import_node_fs22 = require("node:fs");
18427
+ var import_node_fs26 = require("node:fs");
17882
18428
  var SEVERITY_ORDER = {
17883
18429
  Error: 0,
17884
18430
  Critical: 0,
@@ -17926,7 +18472,7 @@ function runCodacyAnalysis(files) {
17926
18472
  if (files.length === 0) return empty;
17927
18473
  const existingFiles = files.filter((f) => {
17928
18474
  try {
17929
- return (0, import_node_fs22.existsSync)(f);
18475
+ return (0, import_node_fs26.existsSync)(f);
17930
18476
  } catch {
17931
18477
  return false;
17932
18478
  }
@@ -18124,6 +18670,7 @@ async function passAndExit(run, reason, skip, kindOverride) {
18124
18670
  "bare-acknowledgment",
18125
18671
  "reflection-prompt",
18126
18672
  "command-only-turn",
18673
+ "declared-ignore",
18127
18674
  "skip-mode",
18128
18675
  "zero-increment",
18129
18676
  "debounce",
@@ -18163,19 +18710,37 @@ async function scope(run) {
18163
18710
  const { files: allChanged, hasRecentCommitFiles } = getChangedFiles();
18164
18711
  run.changedUniverse = allChanged;
18165
18712
  const { kept: external } = partitionVerityOwned(allChanged);
18166
- const analyzable = filterAnalyzable(external);
18167
- const reviewable = filterReviewable(external);
18168
- const securityFiles = filterSecurity(external);
18713
+ const verityIgnore = loadVerityIgnore();
18714
+ const ignored = partitionIgnored(external, verityIgnore);
18715
+ for (const problem of verityIgnore.problems) {
18716
+ process.stderr.write(`Verity: .verityignore ${problem}
18717
+ `);
18718
+ }
18719
+ if (ignored.securityExcluded.length > 0) {
18720
+ process.stderr.write(
18721
+ `Verity: .verityignore excluded ${ignored.securityExcluded.length} security-sensitive file(s) from review this run: ${ignored.securityExcluded.slice(0, 5).join(", ")}. Add a \`!\` rule to keep them in scope.
18722
+ `
18723
+ );
18724
+ }
18725
+ if (ignored.suspended) {
18726
+ process.stderr.write(
18727
+ "Verity: .verityignore changed this turn \u2014 its rules are suspended for this run, so nothing is excluded by them. They take effect once this turn has been reviewed.\n"
18728
+ );
18729
+ }
18730
+ const inScope = ignored.kept;
18731
+ const analyzable = filterAnalyzable(inScope);
18732
+ const reviewable = filterReviewable(inScope);
18733
+ const securityFiles = filterSecurity(inScope);
18169
18734
  const noFilesChanged = analyzable.length === 0 && reviewable.length === 0 && securityFiles.length === 0;
18170
18735
  if (noFilesChanged && !assistantResponse) {
18171
18736
  await passAndExit(run, "No analyzable files changed", "no-analyzable-files");
18172
18737
  }
18173
18738
  const allForReview = Array.from(/* @__PURE__ */ new Set([...analyzable, ...reviewable]));
18174
- Object.assign(run, { allChanged, allForReview, analyzable, hasRecentCommitFiles, noFilesChanged, reviewable, securityFiles });
18739
+ Object.assign(run, { allChanged, allForReview, analyzable, hasRecentCommitFiles, noFilesChanged, reviewable, securityFiles, verityIgnored: ignored });
18175
18740
  }
18176
18741
 
18177
18742
  // src/lib/specs.ts
18178
- var import_node_fs23 = require("node:fs");
18743
+ var import_node_fs27 = require("node:fs");
18179
18744
  var import_node_path20 = require("node:path");
18180
18745
  var SPEC_CANDIDATES = [
18181
18746
  "CLAUDE.md",
@@ -18207,16 +18772,16 @@ function discoverSpecs(consulted = []) {
18207
18772
  const totalCap = relevant ? MAX_TOTAL_SPEC_BYTES : UNCONSULTED_TOTAL_BYTES;
18208
18773
  if (totalBytes >= totalCap) return false;
18209
18774
  if (seen.has(specPath)) return true;
18210
- if (!(0, import_node_fs23.existsSync)(specPath)) return true;
18775
+ if (!(0, import_node_fs27.existsSync)(specPath)) return true;
18211
18776
  seen.add(specPath);
18212
18777
  const remaining = totalCap - totalBytes;
18213
18778
  const fileCap = relevant ? MAX_SPEC_FILE_BYTES : UNCONSULTED_FILE_BYTES;
18214
18779
  const readBytes = Math.min(fileCap, remaining);
18215
18780
  try {
18216
18781
  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);
18782
+ const fd = (0, import_node_fs27.openSync)(specPath, "r");
18783
+ const bytesRead = (0, import_node_fs27.readSync)(fd, buf, 0, readBytes, 0);
18784
+ (0, import_node_fs27.closeSync)(fd);
18220
18785
  const content = buf.slice(0, bytesRead).toString("utf-8");
18221
18786
  if (!content) return true;
18222
18787
  result.push({ path: specPath, content });
@@ -18232,7 +18797,7 @@ function discoverSpecs(consulted = []) {
18232
18797
  if (!addSpec(candidate)) break;
18233
18798
  }
18234
18799
  for (const dir of ["spec", "docs"]) {
18235
- if (!(0, import_node_fs23.existsSync)(dir)) continue;
18800
+ if (!(0, import_node_fs27.existsSync)(dir)) continue;
18236
18801
  try {
18237
18802
  const mdFiles = findMdFiles(dir, 2).sort();
18238
18803
  for (const mdFile of mdFiles) {
@@ -18247,7 +18812,7 @@ function findMdFiles(dir, maxDepth, depth = 0) {
18247
18812
  if (depth >= maxDepth) return [];
18248
18813
  const result = [];
18249
18814
  try {
18250
- const entries = (0, import_node_fs23.readdirSync)(dir, { withFileTypes: true });
18815
+ const entries = (0, import_node_fs27.readdirSync)(dir, { withFileTypes: true });
18251
18816
  for (const entry of entries) {
18252
18817
  const fullPath = (0, import_node_path20.join)(dir, entry.name);
18253
18818
  if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -18266,14 +18831,14 @@ function discoverPlans() {
18266
18831
  const candidates = [];
18267
18832
  const seen = /* @__PURE__ */ new Set();
18268
18833
  for (const plansDir of [localPlansDir, homePlansDir]) {
18269
- if (!(0, import_node_fs23.existsSync)(plansDir)) continue;
18834
+ if (!(0, import_node_fs27.existsSync)(plansDir)) continue;
18270
18835
  try {
18271
- for (const f of (0, import_node_fs23.readdirSync)(plansDir)) {
18836
+ for (const f of (0, import_node_fs27.readdirSync)(plansDir)) {
18272
18837
  if (!f.endsWith(".md") || seen.has(f)) continue;
18273
18838
  seen.add(f);
18274
18839
  const fullPath = (0, import_node_path20.join)(plansDir, f);
18275
18840
  try {
18276
- const stat3 = (0, import_node_fs23.statSync)(fullPath);
18841
+ const stat3 = (0, import_node_fs27.statSync)(fullPath);
18277
18842
  candidates.push({ name: f, path: fullPath, mtime: stat3.mtimeMs, size: stat3.size });
18278
18843
  } catch {
18279
18844
  }
@@ -18286,7 +18851,7 @@ function discoverPlans() {
18286
18851
  for (const entry of candidates.slice(0, MAX_PLAN_FILES)) {
18287
18852
  if (entry.size > MAX_PLAN_FILE_BYTES) continue;
18288
18853
  try {
18289
- const content = (0, import_node_fs23.readFileSync)(entry.path, "utf-8");
18854
+ const content = (0, import_node_fs27.readFileSync)(entry.path, "utf-8");
18290
18855
  result.push({ name: entry.name, content });
18291
18856
  } catch {
18292
18857
  }
@@ -18306,6 +18871,50 @@ async function intentInputs(run) {
18306
18871
  })) {
18307
18872
  await passAndExit(run, "User command only \u2014 skipping analysis", "command-only-turn");
18308
18873
  }
18874
+ {
18875
+ const ignoreKeys = ignoreStateKeys(
18876
+ run.tokenResult.ok ? run.tokenResult.data.token : void 0,
18877
+ null
18878
+ );
18879
+ const found = resolveIgnoreState([baselineSessionId, ...ignoreKeys]);
18880
+ const declaration = resolveActive(found?.state ?? null, Math.floor(Date.now() / 1e3));
18881
+ if (declaration) {
18882
+ const commandRecordTruncated = actionSummary?.commands_truncated === true || actionSummary?.user_commands_truncated === true || // The derived signal, and the only one an older client's summary can
18883
+ // give: `tool_counts` keeps counting Bash calls past every cap that
18884
+ // stops the list growing, so a mismatch IS a loss. See
18885
+ // `ActionSummary.commands_truncated`.
18886
+ (actionSummary?.tool_counts?.Bash ?? 0) > (actionSummary?.commands?.length ?? 0);
18887
+ const outcome = verifyDeclaration({
18888
+ declaration,
18889
+ agentAuthoredFiles: (actionSummary?.files_edited.length ?? 0) + (actionSummary?.files_created.length ?? 0),
18890
+ subagents: actionSummary?.subagents ?? 0,
18891
+ agentCommands: actionSummary?.commands,
18892
+ userCommands: actionSummary?.user_commands,
18893
+ commandRecordTruncated,
18894
+ authorshipIsObservable: !!actionSummary && actionSummary.transcript_windowed !== "orphaned"
18895
+ });
18896
+ if (outcome.honoured) {
18897
+ if (declaration.scope === "turn" && found) clearActiveDeclaration(found.key);
18898
+ logEvent("ignore_honoured", { scope: declaration.scope, origin: declaration.origin });
18899
+ await passAndExit(
18900
+ run,
18901
+ `skipping this turn \u2014 declared housekeeping ("${declaration.reason}")`,
18902
+ "declared-ignore"
18903
+ );
18904
+ } else if (outcome.void) {
18905
+ if (found) clearActiveDeclaration(found.key);
18906
+ logEvent("ignore_voided", {
18907
+ scope: declaration.scope,
18908
+ origin: declaration.origin,
18909
+ why: outcome.why
18910
+ });
18911
+ const notice = `Verity: the ignore declared for this window ("${declaration.reason}") was voided \u2014 ${outcome.why}. Reviewing normally.`;
18912
+ process.stderr.write(`${notice}
18913
+ `);
18914
+ run.voidedIgnoreNotice = notice;
18915
+ }
18916
+ }
18917
+ }
18309
18918
  const conversation = await readAndClearConversationBuffer(baselineSessionId);
18310
18919
  const specs = discoverSpecs(actionSummary?.files_read ?? []);
18311
18920
  const plans = discoverPlans();
@@ -18419,177 +19028,8 @@ async function mode(run) {
18419
19028
  Object.assign(run, { analysisMode, contextFilePaths, sessionAuthoredCode, sessionIdForMemory });
18420
19029
  }
18421
19030
 
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
19031
  // src/lib/fold.ts
18592
- var import_node_fs25 = require("node:fs");
19032
+ var import_node_fs28 = require("node:fs");
18593
19033
  var import_node_path21 = require("node:path");
18594
19034
  var KNOWN_RECORD_TYPES = /* @__PURE__ */ new Set([
18595
19035
  "user",
@@ -18727,7 +19167,7 @@ function candidateRoots(repoRoot2) {
18727
19167
  const norm = repoRoot2.replace(/\\/g, "/").replace(/\/+$/, "");
18728
19168
  const out = [norm];
18729
19169
  try {
18730
- const real = import_node_fs25.realpathSync.native(norm).replace(/\\/g, "/").replace(/\/+$/, "");
19170
+ const real = import_node_fs28.realpathSync.native(norm).replace(/\\/g, "/").replace(/\/+$/, "");
18731
19171
  if (real !== norm) out.push(real);
18732
19172
  } catch {
18733
19173
  }
@@ -18815,8 +19255,8 @@ function fold(transcriptPath, opts = {}) {
18815
19255
  }
18816
19256
  };
18817
19257
  try {
18818
- if (!(0, import_node_fs25.existsSync)(transcriptPath)) return result;
18819
- ingest((0, import_node_fs25.readFileSync)(transcriptPath, "utf8"), "agent");
19258
+ if (!(0, import_node_fs28.existsSync)(transcriptPath)) return result;
19259
+ ingest((0, import_node_fs28.readFileSync)(transcriptPath, "utf8"), "agent");
18820
19260
  result.coverage.complete = true;
18821
19261
  } catch {
18822
19262
  return result;
@@ -18827,19 +19267,19 @@ function fold(transcriptPath, opts = {}) {
18827
19267
  (0, import_node_path21.basename)(transcriptPath).replace(/\.jsonl$/, ""),
18828
19268
  "subagents"
18829
19269
  );
18830
- if ((0, import_node_fs25.existsSync)(sidecarDir)) {
19270
+ if ((0, import_node_fs28.existsSync)(sidecarDir)) {
18831
19271
  const maxFiles = opts.maxSidecars ?? 200;
18832
19272
  const maxBytes = opts.maxSidecarBytes ?? 16 * 1024 * 1024;
18833
19273
  const found = [];
18834
19274
  const walk = (d, depth) => {
18835
19275
  if (depth > 4) return;
18836
- for (const e of (0, import_node_fs25.readdirSync)(d, { withFileTypes: true })) {
19276
+ for (const e of (0, import_node_fs28.readdirSync)(d, { withFileTypes: true })) {
18837
19277
  const p = (0, import_node_path21.join)(d, e.name);
18838
19278
  if (e.isDirectory()) {
18839
19279
  walk(p, depth + 1);
18840
19280
  } else if (e.name.startsWith("agent-") && e.name.endsWith(".jsonl")) {
18841
19281
  try {
18842
- const st = (0, import_node_fs25.statSync)(p);
19282
+ const st = (0, import_node_fs28.statSync)(p);
18843
19283
  found.push({ path: p, size: st.size, mtimeMs: st.mtimeMs });
18844
19284
  } catch {
18845
19285
  result.coverage.malformed++;
@@ -18856,7 +19296,7 @@ function fold(transcriptPath, opts = {}) {
18856
19296
  continue;
18857
19297
  }
18858
19298
  try {
18859
- ingest((0, import_node_fs25.readFileSync)(f.path, "utf8"), "subagent");
19299
+ ingest((0, import_node_fs28.readFileSync)(f.path, "utf8"), "subagent");
18860
19300
  bytes += f.size;
18861
19301
  result.coverage.subagentFiles++;
18862
19302
  } catch {
@@ -18891,7 +19331,7 @@ function fold(transcriptPath, opts = {}) {
18891
19331
  }
18892
19332
  function classifyUnobserved(path) {
18893
19333
  try {
18894
- const st = (0, import_node_fs25.statSync)(path);
19334
+ const st = (0, import_node_fs28.statSync)(path);
18895
19335
  if (!st.isFile()) return "unreadable";
18896
19336
  } catch {
18897
19337
  return "unreadable";
@@ -19195,20 +19635,20 @@ async function evidence(run) {
19195
19635
  }
19196
19636
 
19197
19637
  // src/lib/cache-cleanup.ts
19198
- var import_node_fs26 = require("node:fs");
19638
+ var import_node_fs29 = require("node:fs");
19199
19639
  var import_node_path22 = require("node:path");
19200
19640
  var CACHE_TTL_DAYS = 7;
19201
19641
  function pruneStaleCache() {
19202
19642
  try {
19203
19643
  const dir = projectPath(CACHE_DIR);
19204
19644
  const cutoff = Date.now() - CACHE_TTL_DAYS * 24 * 3600 * 1e3;
19205
- for (const entry of (0, import_node_fs26.readdirSync)(dir)) {
19645
+ for (const entry of (0, import_node_fs29.readdirSync)(dir)) {
19206
19646
  if (!entry.startsWith("pending-")) continue;
19207
19647
  const path = (0, import_node_path22.join)(dir, entry);
19208
19648
  try {
19209
- const stat3 = (0, import_node_fs26.statSync)(path);
19649
+ const stat3 = (0, import_node_fs29.statSync)(path);
19210
19650
  if (stat3.mtimeMs < cutoff) {
19211
- (0, import_node_fs26.unlinkSync)(path);
19651
+ (0, import_node_fs29.unlinkSync)(path);
19212
19652
  logEvent("cache_entry_pruned", {
19213
19653
  path: entry,
19214
19654
  age_days: Math.round((Date.now() - stat3.mtimeMs) / 864e5)
@@ -19222,7 +19662,7 @@ function pruneStaleCache() {
19222
19662
  }
19223
19663
 
19224
19664
  // src/lib/context-files.ts
19225
- var import_node_fs27 = require("node:fs");
19665
+ var import_node_fs30 = require("node:fs");
19226
19666
  var MAX_CONTEXT_FILES = 10;
19227
19667
  var MAX_CONTEXT_FILE_BYTES = 10240;
19228
19668
  var MAX_CONTEXT_TOTAL_BYTES = 51200;
@@ -19243,7 +19683,7 @@ function gatherContextFiles(contextPaths, deltaFiles) {
19243
19683
  continue;
19244
19684
  }
19245
19685
  try {
19246
- const content = (0, import_node_fs27.readFileSync)(safePath, "utf8");
19686
+ const content = (0, import_node_fs30.readFileSync)(safePath, "utf8");
19247
19687
  const bytes = Buffer.byteLength(content);
19248
19688
  if (bytes > MAX_CONTEXT_FILE_BYTES) {
19249
19689
  logEvent("context_file_skipped", { path: filePath, reason: "too_large", bytes });
@@ -19305,7 +19745,7 @@ async function contextFiles(run) {
19305
19745
 
19306
19746
  // src/lib/seed-runner.ts
19307
19747
  var import_promises11 = require("node:fs/promises");
19308
- var import_node_fs28 = require("node:fs");
19748
+ var import_node_fs31 = require("node:fs");
19309
19749
  var import_node_path23 = require("node:path");
19310
19750
  var import_yaml2 = __toESM(require_dist());
19311
19751
 
@@ -19545,7 +19985,7 @@ function renderNodeMarkdown(candidate, nodeId, createdAt) {
19545
19985
  return fm;
19546
19986
  }
19547
19987
  async function runSeed(opts) {
19548
- if (!(0, import_node_fs28.existsSync)(STANDARD_FILE)) {
19988
+ if (!(0, import_node_fs31.existsSync)(STANDARD_FILE)) {
19549
19989
  return { created: 0, failed: 0, skipped: "no_standard", candidates: [] };
19550
19990
  }
19551
19991
  let standardDoc;
@@ -19557,7 +19997,7 @@ async function runSeed(opts) {
19557
19997
  }
19558
19998
  const knowledgeSpec = standardDoc.knowledge_spec ?? {};
19559
19999
  let readmeContent;
19560
- if ((0, import_node_fs28.existsSync)("README.md")) {
20000
+ if ((0, import_node_fs31.existsSync)("README.md")) {
19561
20001
  try {
19562
20002
  readmeContent = await (0, import_promises11.readFile)("README.md", "utf-8");
19563
20003
  } catch {
@@ -19565,7 +20005,7 @@ async function runSeed(opts) {
19565
20005
  }
19566
20006
  let claudeMdContent;
19567
20007
  for (const p of ["CLAUDE.md", ".claude/CLAUDE.md"]) {
19568
- if ((0, import_node_fs28.existsSync)(p)) {
20008
+ if ((0, import_node_fs31.existsSync)(p)) {
19569
20009
  try {
19570
20010
  claudeMdContent = await (0, import_promises11.readFile)(p, "utf-8");
19571
20011
  break;
@@ -19589,7 +20029,7 @@ async function runSeed(opts) {
19589
20029
  return { created: 0, failed: 0, skipped: "no_candidates", candidates: [] };
19590
20030
  }
19591
20031
  const overviewPath = (0, import_node_path23.join)(MEMORY_DIR, "domain", "project-overview.md");
19592
- if ((0, import_node_fs28.existsSync)(overviewPath) && !opts.force) {
20032
+ if ((0, import_node_fs31.existsSync)(overviewPath) && !opts.force) {
19593
20033
  return { created: 0, failed: 0, skipped: "already_seeded", candidates };
19594
20034
  }
19595
20035
  if (opts.dryRun) {
@@ -19644,7 +20084,7 @@ async function runSeed(opts) {
19644
20084
  }
19645
20085
 
19646
20086
  // src/commands/analyze/phases/08-memory-manifest.ts
19647
- var import_node_fs29 = require("node:fs");
20087
+ var import_node_fs32 = require("node:fs");
19648
20088
  var import_node_path24 = require("node:path");
19649
20089
  async function memoryManifest(run) {
19650
20090
  const { globals } = run;
@@ -19656,8 +20096,8 @@ async function memoryManifest(run) {
19656
20096
  try {
19657
20097
  await ensureMemoryDir();
19658
20098
  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);
20099
+ const hasStandard = (0, import_node_fs32.existsSync)(STANDARD_FILE);
20100
+ const alreadyTried = (0, import_node_fs32.existsSync)(seedMarker);
19661
20101
  if (hasStandard && !alreadyTried) {
19662
20102
  const preManifest = await buildManifest();
19663
20103
  if (preManifest.nodes.length === 0) {
@@ -19670,7 +20110,7 @@ async function memoryManifest(run) {
19670
20110
  dryRun: false
19671
20111
  });
19672
20112
  if (seedResult.created > 0) {
19673
- (0, import_node_fs29.writeFileSync)(seedMarker, `${(/* @__PURE__ */ new Date()).toISOString()} created=${seedResult.created}
20113
+ (0, import_node_fs32.writeFileSync)(seedMarker, `${(/* @__PURE__ */ new Date()).toISOString()} created=${seedResult.created}
19674
20114
  `);
19675
20115
  autoSeedNotice = `Seeded ${seedResult.created} knowledge node(s) from your existing Standard (one-time).`;
19676
20116
  logEvent("auto_seed_ran", {
@@ -19678,7 +20118,7 @@ async function memoryManifest(run) {
19678
20118
  failed: seedResult.failed
19679
20119
  });
19680
20120
  } else if (seedResult.skipped === "already_seeded") {
19681
- (0, import_node_fs29.writeFileSync)(seedMarker, `${(/* @__PURE__ */ new Date()).toISOString()} skipped=already_seeded
20121
+ (0, import_node_fs32.writeFileSync)(seedMarker, `${(/* @__PURE__ */ new Date()).toISOString()} skipped=already_seeded
19682
20122
  `);
19683
20123
  } else {
19684
20124
  logEvent("auto_seed_noop", {
@@ -19867,7 +20307,7 @@ async function workingMemory(run) {
19867
20307
  }
19868
20308
 
19869
20309
  // src/lib/note-budget.ts
19870
- var import_node_fs30 = require("node:fs");
20310
+ var import_node_fs33 = require("node:fs");
19871
20311
  var ADVISORY_BUDGET = { PASS: 1, WARN: 2 };
19872
20312
  var EPISODE_STALE_SECONDS = 30 * 60;
19873
20313
  var FRESH = { delivered: 0, tasksCompleted: 0, ts: 0 };
@@ -19889,9 +20329,9 @@ function advisoryBudgetSpent(episode, rawDecision) {
19889
20329
  }
19890
20330
  function readAdvisoryEpisode(sessionId) {
19891
20331
  const file = scopedFile(ADVISORY_EPISODE_FILE, sessionId);
19892
- if (!(0, import_node_fs30.existsSync)(file)) return null;
20332
+ if (!(0, import_node_fs33.existsSync)(file)) return null;
19893
20333
  try {
19894
- const o = JSON.parse((0, import_node_fs30.readFileSync)(file, "utf-8")) ?? {};
20334
+ const o = JSON.parse((0, import_node_fs33.readFileSync)(file, "utf-8")) ?? {};
19895
20335
  const delivered = typeof o.delivered === "number" ? o.delivered : NaN;
19896
20336
  if (isNaN(delivered)) return null;
19897
20337
  return {
@@ -19905,8 +20345,8 @@ function readAdvisoryEpisode(sessionId) {
19905
20345
  }
19906
20346
  function writeAdvisoryEpisode(episode, sessionId) {
19907
20347
  try {
19908
- (0, import_node_fs30.mkdirSync)(VERITY_DIR, { recursive: true });
19909
- (0, import_node_fs30.writeFileSync)(
20348
+ (0, import_node_fs33.mkdirSync)(VERITY_DIR, { recursive: true });
20349
+ (0, import_node_fs33.writeFileSync)(
19910
20350
  scopedFile(ADVISORY_EPISODE_FILE, sessionId),
19911
20351
  JSON.stringify({ v: 1, ...episode })
19912
20352
  );
@@ -20013,7 +20453,16 @@ async function buildRequest(run) {
20013
20453
  // the state, so the number is one turn lagged by construction. The
20014
20454
  // degenerate win for the budget is a dead channel that looks like clean
20015
20455
  // code; this is what makes "did delivery rate collapse" a query.
20016
- advisory_delivered_prior: readAdvisoryEpisode(run.baselineSessionId)?.delivered ?? 0
20456
+ advisory_delivered_prior: readAdvisoryEpisode(run.baselineSessionId)?.delivered ?? 0,
20457
+ // `.verityignore` — see CoverageTelemetry.verityignore for why the SHARE is
20458
+ // the number that matters and why no paths travel with it.
20459
+ verityignore: {
20460
+ rules: run.verityIgnored.rules,
20461
+ excluded: run.verityIgnored.ignored.length,
20462
+ share: ignoreShare(run.verityIgnored.kept.length, run.verityIgnored.ignored.length),
20463
+ security_excluded: run.verityIgnored.securityExcluded.length,
20464
+ suspended: run.verityIgnored.suspended
20465
+ }
20017
20466
  };
20018
20467
  const requestBody = {
20019
20468
  coverage_telemetry: coverageTelemetry,
@@ -20211,14 +20660,14 @@ async function buildRequest(run) {
20211
20660
  }
20212
20661
 
20213
20662
  // src/lib/offline.ts
20214
- var import_node_fs31 = require("node:fs");
20663
+ var import_node_fs34 = require("node:fs");
20215
20664
  var import_node_crypto11 = require("node:crypto");
20216
20665
  function cacheRequest(body) {
20217
20666
  try {
20218
- (0, import_node_fs31.mkdirSync)(CACHE_DIR, { recursive: true });
20667
+ (0, import_node_fs34.mkdirSync)(CACHE_DIR, { recursive: true });
20219
20668
  const suffix = (0, import_node_crypto11.randomBytes)(4).toString("hex");
20220
20669
  const filename = `pending-${Math.floor(Date.now() / 1e3)}-${suffix}.json`;
20221
- (0, import_node_fs31.writeFileSync)(`${CACHE_DIR}/${filename}`, JSON.stringify(redactRequest(body)));
20670
+ (0, import_node_fs34.writeFileSync)(`${CACHE_DIR}/${filename}`, JSON.stringify(redactRequest(body)));
20222
20671
  } catch {
20223
20672
  }
20224
20673
  }
@@ -20337,7 +20786,7 @@ async function transmit(run) {
20337
20786
  }
20338
20787
 
20339
20788
  // src/commands/analyze/phases/13-reconcile.ts
20340
- var import_node_fs32 = require("node:fs");
20789
+ var import_node_fs35 = require("node:fs");
20341
20790
  var import_node_path26 = require("node:path");
20342
20791
  async function reconcile(run) {
20343
20792
  const { actionSummary, allChanged, analyzable, baseline, baselineSessionId, codeDelta, contentHash, conversation, decision, foldResult, memory, memorySession, response, reviewable, securityFiles, turnId } = run;
@@ -20348,7 +20797,7 @@ async function reconcile(run) {
20348
20797
  const st = foldDossier(memorySession.d);
20349
20798
  openElsewhere = openBlockingElsewhere(st.statements, sentPaths, (file, line) => {
20350
20799
  try {
20351
- const src = (0, import_node_fs32.readFileSync)((0, import_node_path26.join)(repoRoot(), file), "utf8").split("\n");
20800
+ const src = (0, import_node_fs35.readFileSync)((0, import_node_path26.join)(repoRoot(), file), "utf8").split("\n");
20352
20801
  const at = src[line - 1];
20353
20802
  return at === void 0 ? null : lineSha(at);
20354
20803
  } catch {
@@ -20409,6 +20858,25 @@ async function reconcile(run) {
20409
20858
  stage: "self-scope",
20410
20859
  kind: "policy"
20411
20860
  })),
20861
+ // ⚠ WHAT THE TEAM'S OWN `.verityignore` REMOVED — every path, named
20862
+ // (VRT-135 · 3/3). The 0.30 principle: an exclusion the ledger cannot see
20863
+ // is a scoping decision nobody can audit, and this is the one exclusion
20864
+ // stage a user can edit, so it is the one that most needs to be visible.
20865
+ //
20866
+ // `policy`, by D2's bar — no version of this product would have reviewed a
20867
+ // file the project declared out of scope, so it must not downgrade PASS to
20868
+ // WARN. That is also why it is not narrated per-turn by `describeCoverage`
20869
+ // (policy exclusions are recorded, not announced): repeating "your dist/
20870
+ // went unreviewed" on every turn is how a channel gets muted. The SHARE
20871
+ // below is the signal that replaces the noise.
20872
+ //
20873
+ // Taken from the run, not recomputed — see context.ts `verityIgnored`.
20874
+ ...run.verityIgnored.ignored.map((path) => ({
20875
+ path,
20876
+ reason: "verityignore",
20877
+ stage: "verityignore",
20878
+ kind: "policy"
20879
+ })),
20412
20880
  // The extension allowlist. POLICY: a changed README was never going to be
20413
20881
  // reviewed, and calling that a coverage gap would downgrade nearly every
20414
20882
  // PASS to WARN until WARN meant nothing. Recorded so the ledger balances and
@@ -20638,7 +21106,7 @@ function agentContextFor(response, intentRepeat = 0, priorPendingFingerprints =
20638
21106
  }
20639
21107
  async function render(run) {
20640
21108
  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;
21109
+ const { actionSummary, assistantResponse, autoSeedNotice, voidedIgnoreNotice, baselineSessionId, codeDelta, contentHash, conversation, currentCommit, decision, intentRepeatCount, memory, openElsewhere, priorPendingFingerprints, response, reviewCoverage, serviceUrl, sessionIdForMemory, silenced, token, watermarkHash, watermarkIsPartial } = run;
20642
21110
  let { iteration } = run;
20643
21111
  const metadata = response.metadata ?? {};
20644
21112
  const intentAmbiguity = metadata.intent_ambiguity;
@@ -20890,6 +21358,7 @@ ${YELLOW}${grantNudge.trim()}${NC}
20890
21358
  const viewUrl = response.view_url ?? "";
20891
21359
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
20892
21360
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
21361
+ if (voidedIgnoreNotice) userSummary = `${voidedIgnoreNotice} ${userSummary}`;
20893
21362
  userSummary += loginNudge + grantNudge;
20894
21363
  emitVerdict({
20895
21364
  proposed: "PASS",
@@ -20911,6 +21380,7 @@ ${YELLOW}${grantNudge.trim()}${NC}
20911
21380
  const viewUrl = response.view_url ?? "";
20912
21381
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
20913
21382
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
21383
+ if (voidedIgnoreNotice) userSummary = `${voidedIgnoreNotice} ${userSummary}`;
20914
21384
  userSummary += loginNudge + grantNudge;
20915
21385
  emitVerdict({
20916
21386
  proposed: "WARN",
@@ -20926,7 +21396,11 @@ ${YELLOW}${grantNudge.trim()}${NC}
20926
21396
  }
20927
21397
  default: {
20928
21398
  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;
21399
+ const msg = [
21400
+ voidedIgnoreNotice,
21401
+ autoSeedNotice,
21402
+ "Verity: unrecognised verdict \u2014 treating as WARN"
21403
+ ].filter(Boolean).join(" ") + loginNudge + grantNudge;
20930
21404
  process.stderr.write(
20931
21405
  `Verity: server returned an unrecognised gate_decision (${raw}). Rendering WARN rather than PASS. Update the CLI: npm i -g @codacy/verity-cli
20932
21406
  `
@@ -21003,7 +21477,7 @@ async function runAnalyze(opts, globals) {
21003
21477
  }
21004
21478
 
21005
21479
  // src/commands/baseline.ts
21006
- var import_node_fs33 = require("node:fs");
21480
+ var import_node_fs36 = require("node:fs");
21007
21481
  function registerBaselineCommands(program2) {
21008
21482
  const baseline = program2.command("baseline").description("Manage the task-start working-tree baseline");
21009
21483
  baseline.command("capture").description("Snapshot the working tree at task start (used by SessionStart hook)").option("--session-id <id>", "Session id (overrides any value from stdin)").option("--source <source>", "Lifecycle hint: startup|resume|clear|compact").action(async (opts) => {
@@ -21012,7 +21486,7 @@ function registerBaselineCommands(program2) {
21012
21486
  process.chdir(repoRoot());
21013
21487
  } catch {
21014
21488
  }
21015
- if (!(0, import_node_fs33.existsSync)(VERITY_DIR)) {
21489
+ if (!(0, import_node_fs36.existsSync)(VERITY_DIR)) {
21016
21490
  process.exit(0);
21017
21491
  }
21018
21492
  let sessionId = opts.sessionId;
@@ -21052,7 +21526,7 @@ async function readStdin() {
21052
21526
  }
21053
21527
 
21054
21528
  // src/commands/review.ts
21055
- var import_node_fs34 = require("node:fs");
21529
+ var import_node_fs37 = require("node:fs");
21056
21530
  function registerReviewCommand(program2) {
21057
21531
  program2.command("review").description("Run on-demand Verity analysis (advisory, never blocks)").requiredOption("--files <paths>", "Comma-separated file list").option("--changed <paths>", "Subset of --files that were modified").option("--intent <text>", "User intent description (max 2000 chars)").option("--specs <paths>", "Comma-separated spec file paths").option("--json", "Output raw JSON response").action(async (opts) => {
21058
21532
  const globals = program2.opts();
@@ -21071,7 +21545,7 @@ async function runReview(opts, globals) {
21071
21545
  const securityFiles = filterSecurity(allFiles);
21072
21546
  let staticResults;
21073
21547
  if (isCodacyAvailable()) {
21074
- const scannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles])).filter((f) => (0, import_node_fs34.existsSync)(f) || resolveFile(f) !== null);
21548
+ const scannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles])).filter((f) => (0, import_node_fs37.existsSync)(f) || resolveFile(f) !== null);
21075
21549
  staticResults = runCodacyAnalysis(scannable);
21076
21550
  } else {
21077
21551
  staticResults = {
@@ -21097,10 +21571,10 @@ async function runReview(opts, globals) {
21097
21571
  const specPaths = opts.specs.split(",").map((f) => f.trim()).filter(Boolean);
21098
21572
  specs = [];
21099
21573
  for (const p of specPaths) {
21100
- if (!(0, import_node_fs34.existsSync)(p)) continue;
21574
+ if (!(0, import_node_fs37.existsSync)(p)) continue;
21101
21575
  try {
21102
- const { readFileSync: readFileSync20 } = await import("node:fs");
21103
- const content = readFileSync20(p, "utf-8");
21576
+ const { readFileSync: readFileSync23 } = await import("node:fs");
21577
+ const content = readFileSync23(p, "utf-8");
21104
21578
  specs.push({ path: p, content: content.slice(0, 10240) });
21105
21579
  } catch {
21106
21580
  }
@@ -21157,7 +21631,7 @@ async function runReview(opts, globals) {
21157
21631
  }
21158
21632
 
21159
21633
  // src/commands/guard.ts
21160
- var import_node_fs35 = require("node:fs");
21634
+ var import_node_fs38 = require("node:fs");
21161
21635
  var import_node_path27 = require("node:path");
21162
21636
  var GUARD_BLOCK_CAP = 2;
21163
21637
  var GUARD_ITER_FILE = (0, import_node_path27.join)(VERITY_DIR, ".guard-iteration");
@@ -21205,7 +21679,7 @@ function readPreToolUseStdin() {
21205
21679
  }
21206
21680
  function readIterMap() {
21207
21681
  try {
21208
- const raw = JSON.parse((0, import_node_fs35.readFileSync)(GUARD_ITER_FILE, "utf-8"));
21682
+ const raw = JSON.parse((0, import_node_fs38.readFileSync)(GUARD_ITER_FILE, "utf-8"));
21209
21683
  if (raw && typeof raw === "object") {
21210
21684
  if (typeof raw.moment === "string" && typeof raw.count === "number") {
21211
21685
  return { [raw.moment]: raw.count };
@@ -21225,10 +21699,10 @@ function readIter(moment) {
21225
21699
  }
21226
21700
  function writeIter(moment, count) {
21227
21701
  try {
21228
- (0, import_node_fs35.mkdirSync)(VERITY_DIR, { recursive: true });
21702
+ (0, import_node_fs38.mkdirSync)(VERITY_DIR, { recursive: true });
21229
21703
  const map = readIterMap();
21230
21704
  map[moment] = count;
21231
- (0, import_node_fs35.writeFileSync)(GUARD_ITER_FILE, JSON.stringify(map));
21705
+ (0, import_node_fs38.writeFileSync)(GUARD_ITER_FILE, JSON.stringify(map));
21232
21706
  } catch {
21233
21707
  }
21234
21708
  }
@@ -21238,10 +21712,10 @@ function resetIter(moment) {
21238
21712
  if (!(moment in map)) return;
21239
21713
  delete map[moment];
21240
21714
  if (Object.keys(map).length === 0) {
21241
- if ((0, import_node_fs35.existsSync)(GUARD_ITER_FILE)) (0, import_node_fs35.unlinkSync)(GUARD_ITER_FILE);
21715
+ if ((0, import_node_fs38.existsSync)(GUARD_ITER_FILE)) (0, import_node_fs38.unlinkSync)(GUARD_ITER_FILE);
21242
21716
  } else {
21243
- (0, import_node_fs35.mkdirSync)(VERITY_DIR, { recursive: true });
21244
- (0, import_node_fs35.writeFileSync)(GUARD_ITER_FILE, JSON.stringify(map));
21717
+ (0, import_node_fs38.mkdirSync)(VERITY_DIR, { recursive: true });
21718
+ (0, import_node_fs38.writeFileSync)(GUARD_ITER_FILE, JSON.stringify(map));
21245
21719
  }
21246
21720
  } catch {
21247
21721
  }
@@ -21302,16 +21776,17 @@ function extractStatedIntent(moment, command, pushedMessages = null) {
21302
21776
  const text = moment === "pre-commit" ? parseCommitMessage(command) : parsePrIntent(command) ?? pushedMessages;
21303
21777
  return isSubstantiveIntent(text) ? text : null;
21304
21778
  }
21305
- function hasBlockingFinding(response) {
21779
+ function hasBlockingFinding(response, sentFiles) {
21306
21780
  const findings = response.findings ?? [];
21307
- return findings.some((f) => f.scope !== "pre-existing" && ["critical", "high"].includes((f.severity ?? "").toLowerCase()));
21781
+ const sent = sentFiles ? new Set(sentFiles.map((p) => p.replace(/\\/g, "/"))) : null;
21782
+ return findings.some((f) => f.scope !== "pre-existing" && ["critical", "high"].includes((f.severity ?? "").toLowerCase()) && (sent === null || typeof f.file === "string" && sent.has(f.file.replace(/\\/g, "/"))));
21308
21783
  }
21309
21784
  function buildGuardRequest(moment, files, codeDelta, iter, sessionId, statedIntent, coverageTelemetry) {
21310
21785
  const analyzable = filterAnalyzable(files);
21311
21786
  const securityFiles = filterSecurity(files);
21312
21787
  let staticResults;
21313
21788
  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);
21789
+ const scannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles])).map((f) => (0, import_node_fs38.existsSync)(f) ? f : resolveFile(f)).filter((f) => f !== null);
21315
21790
  staticResults = runCodacyAnalysis(scannable);
21316
21791
  } else {
21317
21792
  staticResults = { tool: "@codacy/analysis-cli", findings: [], summary: { total_findings: 0, by_severity: {}, tools_run: [] } };
@@ -21468,7 +21943,7 @@ async function runGuard(opts, globals) {
21468
21943
  const link = viewUrl ? ` \u2014 ${viewUrl}` : "";
21469
21944
  const covLine = coverageSummary(coverage);
21470
21945
  const covDetail = coverageBlock(coverage);
21471
- if (decision === "FAIL" && hasBlockingFinding(response)) {
21946
+ if (decision === "FAIL" && hasBlockingFinding(response, codeDelta.files.map((f) => f.path))) {
21472
21947
  writeIter(moment, iter + 1);
21473
21948
  writeBlockMessage(moment, response, covDetail);
21474
21949
  process.exit(2);
@@ -21538,15 +22013,107 @@ function writeBlockMessage(moment, response, covDetail) {
21538
22013
  `);
21539
22014
  }
21540
22015
 
22016
+ // src/commands/ignore.ts
22017
+ function registerIgnoreCommand(program2) {
22018
+ const ignore = program2.command("ignore").description("Declare the next turn (or a short window) as housekeeping \u2014 no review needed").option("--turn", "Cover the next turn only (the default)").option("--for <duration>", "Cover a window: 30m, 45s, 1h (max 60m)").option("--reason <reason>", "Why this window needs no review \u2014 required, and recorded").option("--agent", "Mark the declaration as agent-invoked (a ledger label, not a permission)").option("--session-id <id>", "Session id (defaults to $CLAUDE_SESSION_ID)").option("--json", "Output raw JSON").action(async (opts) => {
22019
+ const globals = program2.opts();
22020
+ const now = Math.floor(Date.now() / 1e3);
22021
+ const reason = opts.reason?.trim();
22022
+ if (!reason) {
22023
+ printError(
22024
+ 'A reason is required: verity ignore --turn --reason "pulling latest before starting".\nIt is recorded with the declaration and is the only trace a skipped turn leaves.'
22025
+ );
22026
+ process.exit(1);
22027
+ }
22028
+ if (opts.for && opts.turn) {
22029
+ printError("Use either --turn or --for, not both \u2014 they are two different windows.");
22030
+ process.exit(1);
22031
+ }
22032
+ let scope2 = "turn";
22033
+ let ttl = TURN_FUSE_SECONDS;
22034
+ if (opts.for) {
22035
+ const parsed = parseDuration(opts.for);
22036
+ if (!parsed.ok) {
22037
+ printError(parsed.error);
22038
+ process.exit(1);
22039
+ }
22040
+ scope2 = "window";
22041
+ ttl = parsed.seconds;
22042
+ }
22043
+ const tokenResult = await resolveToken(globals.token);
22044
+ const token = tokenResult.ok ? tokenResult.data.token : void 0;
22045
+ const sessionId = opts.sessionId || process.env.CLAUDE_SESSION_ID || void 0;
22046
+ const keys = ignoreStateKeys(token, sessionId);
22047
+ const existing = resolveIgnoreState(keys);
22048
+ const spent = existing?.state.spent ?? 0;
22049
+ const writeKey = existing?.key ?? keys[0];
22050
+ if (spent >= IGNORE_BUDGET) {
22051
+ const msg = `Ignore budget spent for this session (${spent}/${IGNORE_BUDGET} declarations). The next turn will be reviewed normally. The budget is per session \u2014 it is what stops an ignore from becoming a standing mute.`;
22052
+ if (opts.json) {
22053
+ printJson({ declared: false, reason_refused: "budget-spent", spent, budget: IGNORE_BUDGET });
22054
+ } else {
22055
+ printWarn(msg);
22056
+ }
22057
+ logEvent("ignore_refused", { why: "budget-spent", spent, budget: IGNORE_BUDGET });
22058
+ process.exit(0);
22059
+ }
22060
+ const declaration = {
22061
+ scope: scope2,
22062
+ origin: opts.agent ? "agent" : "user",
22063
+ reason,
22064
+ at: now,
22065
+ expires: now + ttl
22066
+ };
22067
+ writeIgnoreState({ v: 1, active: declaration, spent: spent + 1 }, writeKey);
22068
+ logEvent("ignore_declared", {
22069
+ scope: scope2,
22070
+ origin: declaration.origin,
22071
+ ttl_seconds: ttl,
22072
+ spent: spent + 1,
22073
+ budget: IGNORE_BUDGET
22074
+ });
22075
+ if (opts.json) {
22076
+ printJson({
22077
+ declared: true,
22078
+ scope: scope2,
22079
+ origin: declaration.origin,
22080
+ reason,
22081
+ expires_at: new Date(declaration.expires * 1e3).toISOString(),
22082
+ spent: spent + 1,
22083
+ budget: IGNORE_BUDGET
22084
+ });
22085
+ return;
22086
+ }
22087
+ const window = scope2 === "turn" ? "the next turn" : `the next ${describeRemaining(declaration, now).replace(" left", "")}`;
22088
+ printInfo(`Verity will skip ${window} \u2014 "${reason}" (${spent + 1}/${IGNORE_BUDGET} this session).`);
22089
+ printInfo("It covers turns that author nothing. If anything is written, the declaration voids and the review runs.");
22090
+ });
22091
+ ignore.command("clear").description("Cancel the active declaration (the spent budget is not refunded)").option("--session-id <id>", "Session id (defaults to $CLAUDE_SESSION_ID)").action(async (opts) => {
22092
+ const globals = program2.opts();
22093
+ const tokenResult = await resolveToken(globals.token);
22094
+ const token = tokenResult.ok ? tokenResult.data.token : void 0;
22095
+ const sessionId = opts.sessionId || process.env.CLAUDE_SESSION_ID || void 0;
22096
+ const found = resolveIgnoreState(ignoreStateKeys(token, sessionId));
22097
+ const active = resolveActive(found?.state ?? null, Math.floor(Date.now() / 1e3));
22098
+ if (!found || !active) {
22099
+ printInfo("No active ignore declaration.");
22100
+ return;
22101
+ }
22102
+ clearActiveDeclaration(found.key);
22103
+ logEvent("ignore_cleared", { scope: active.scope, origin: active.origin });
22104
+ printInfo(`Cleared: "${active.reason}". The next turn will be reviewed normally.`);
22105
+ });
22106
+ }
22107
+
21541
22108
  // src/commands/init.ts
21542
- var import_node_fs37 = require("node:fs");
22109
+ var import_node_fs40 = require("node:fs");
21543
22110
  var import_promises13 = require("node:fs/promises");
21544
22111
  var import_node_path29 = require("node:path");
21545
22112
  var import_node_child_process11 = require("node:child_process");
21546
22113
  var readline2 = __toESM(require("node:readline/promises"));
21547
22114
 
21548
22115
  // src/commands/migrate.ts
21549
- var import_node_fs36 = require("node:fs");
22116
+ var import_node_fs39 = require("node:fs");
21550
22117
  var import_node_path28 = require("node:path");
21551
22118
  var import_node_child_process10 = require("node:child_process");
21552
22119
 
@@ -21678,10 +22245,10 @@ async function runMigration(opts = {}) {
21678
22245
  function migrateProjectDir(root, actions) {
21679
22246
  const gateDir = (0, import_node_path28.join)(root, ".gate");
21680
22247
  const verityDir = (0, import_node_path28.join)(root, ".verity");
21681
- if ((0, import_node_fs36.existsSync)(gateDir) && !(0, import_node_fs36.existsSync)(verityDir)) {
22248
+ if ((0, import_node_fs39.existsSync)(gateDir) && !(0, import_node_fs39.existsSync)(verityDir)) {
21682
22249
  return migrateProjectDirRename(root, gateDir, verityDir, actions);
21683
22250
  }
21684
- if ((0, import_node_fs36.existsSync)(gateDir) && (0, import_node_fs36.existsSync)(verityDir)) {
22251
+ if ((0, import_node_fs39.existsSync)(gateDir) && (0, import_node_fs39.existsSync)(verityDir)) {
21685
22252
  return migrateProjectDirCarry(gateDir, verityDir, actions);
21686
22253
  }
21687
22254
  return false;
@@ -21702,13 +22269,13 @@ function migrateProjectDirRename(root, gateDir, verityDir, actions) {
21702
22269
  }
21703
22270
  }
21704
22271
  if (moved) {
21705
- if ((0, import_node_fs36.existsSync)(gateDir)) {
22272
+ if ((0, import_node_fs39.existsSync)(gateDir)) {
21706
22273
  const carried = carryLegacyContents(gateDir, verityDir);
21707
22274
  if (carried > 0) {
21708
22275
  actions.push(`Carried ${carried} untracked legacy file(s) from .gate/ into .verity/`);
21709
22276
  }
21710
22277
  try {
21711
- (0, import_node_fs36.rmSync)(gateDir, { recursive: true, force: true });
22278
+ (0, import_node_fs39.rmSync)(gateDir, { recursive: true, force: true });
21712
22279
  } catch {
21713
22280
  }
21714
22281
  }
@@ -21724,7 +22291,7 @@ function migrateProjectDirCarry(gateDir, verityDir, actions) {
21724
22291
  actions.push(`Carried ${carried} legacy file(s) from .gate/ into .verity/`);
21725
22292
  }
21726
22293
  try {
21727
- (0, import_node_fs36.rmSync)(gateDir, { recursive: true, force: true });
22294
+ (0, import_node_fs39.rmSync)(gateDir, { recursive: true, force: true });
21728
22295
  } catch {
21729
22296
  }
21730
22297
  return carried > 0;
@@ -21733,9 +22300,9 @@ function migrateGlobalCredentials(home, actions) {
21733
22300
  if (!home) return;
21734
22301
  const gateCreds = (0, import_node_path28.join)(home, ".gate", "credentials");
21735
22302
  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 });
22303
+ if (!(0, import_node_fs39.existsSync)(gateCreds)) return;
22304
+ if (!(0, import_node_fs39.existsSync)(verityCreds)) {
22305
+ (0, import_node_fs39.mkdirSync)((0, import_node_path28.join)(home, ".verity"), { recursive: true });
21739
22306
  moveFile(gateCreds, verityCreds);
21740
22307
  actions.push("Moved ~/.gate/credentials \u2192 ~/.verity/credentials");
21741
22308
  return;
@@ -21758,7 +22325,7 @@ async function migrateLegacyHooks(root, actions) {
21758
22325
  }
21759
22326
  async function migrateClaudeMd(root, actions) {
21760
22327
  const claudeMd = (0, import_node_path28.join)(root, "CLAUDE.md");
21761
- const hadLegacyBlock = (0, import_node_fs36.existsSync)(claudeMd) && hasLegacyMemoryBlock(readFileSyncSafe(claudeMd));
22328
+ const hadLegacyBlock = (0, import_node_fs39.existsSync)(claudeMd) && hasLegacyMemoryBlock(readFileSyncSafe(claudeMd));
21762
22329
  if (!hadLegacyBlock) return;
21763
22330
  try {
21764
22331
  await ensureClaudeMdPointer(root);
@@ -21770,7 +22337,7 @@ async function migrateClaudeMd(root, actions) {
21770
22337
  function migrateStandardFile(root, actions) {
21771
22338
  const gateMd = (0, import_node_path28.join)(root, "GATE.md");
21772
22339
  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;
22340
+ if (!(0, import_node_fs39.existsSync)(gateMd) || (0, import_node_fs39.existsSync)(verityMd)) return;
21774
22341
  let moved = false;
21775
22342
  if (isGitRepo(root) && isGitTracked(root, "GATE.md")) {
21776
22343
  try {
@@ -21782,12 +22349,12 @@ function migrateStandardFile(root, actions) {
21782
22349
  if (!moved) moveFile(gateMd, verityMd);
21783
22350
  const content = readFileSyncSafe(verityMd);
21784
22351
  const refreshed = content.split("GATE.md").join("VERITY.md");
21785
- if (refreshed !== content) (0, import_node_fs36.writeFileSync)(verityMd, refreshed);
22352
+ if (refreshed !== content) (0, import_node_fs39.writeFileSync)(verityMd, refreshed);
21786
22353
  actions.push("Renamed GATE.md \u2192 VERITY.md");
21787
22354
  }
21788
22355
  async function migrateTelemetryHeaders(root, actions) {
21789
22356
  const file = (0, import_node_path28.join)(root, ".claude", "settings.local.json");
21790
- if (!(0, import_node_fs36.existsSync)(file)) return;
22357
+ if (!(0, import_node_fs39.existsSync)(file)) return;
21791
22358
  let settings;
21792
22359
  try {
21793
22360
  settings = JSON.parse(readFileSyncSafe(file) || "{}");
@@ -21835,14 +22402,14 @@ function mergeGlobalCredentials(gateCreds, verityCreds) {
21835
22402
  }
21836
22403
  if (toAppend.length > 0) {
21837
22404
  const sep2 = verityContent.endsWith("\n") || verityContent === "" ? "" : "\n";
21838
- (0, import_node_fs36.writeFileSync)(verityCreds, verityContent + sep2 + toAppend.join("\n") + "\n");
22405
+ (0, import_node_fs39.writeFileSync)(verityCreds, verityContent + sep2 + toAppend.join("\n") + "\n");
21839
22406
  }
21840
- (0, import_node_fs36.rmSync)(gateCreds, { force: true });
22407
+ (0, import_node_fs39.rmSync)(gateCreds, { force: true });
21841
22408
  return toAppend.length;
21842
22409
  }
21843
22410
  function readFileSyncSafe(path) {
21844
22411
  try {
21845
- return (0, import_node_fs36.readFileSync)(path, "utf-8");
22412
+ return (0, import_node_fs39.readFileSync)(path, "utf-8");
21846
22413
  } catch {
21847
22414
  return "";
21848
22415
  }
@@ -21857,35 +22424,35 @@ function hasStagedChanges(root) {
21857
22424
  }
21858
22425
  function moveDir(from, to) {
21859
22426
  try {
21860
- (0, import_node_fs36.renameSync)(from, to);
22427
+ (0, import_node_fs39.renameSync)(from, to);
21861
22428
  } catch (err) {
21862
22429
  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 });
22430
+ (0, import_node_fs39.cpSync)(from, to, { recursive: true });
22431
+ (0, import_node_fs39.rmSync)(from, { recursive: true, force: true });
21865
22432
  }
21866
22433
  }
21867
22434
  function moveFile(from, to) {
21868
22435
  try {
21869
- (0, import_node_fs36.renameSync)(from, to);
22436
+ (0, import_node_fs39.renameSync)(from, to);
21870
22437
  } catch (err) {
21871
22438
  if (err.code !== "EXDEV") throw err;
21872
- (0, import_node_fs36.cpSync)(from, to);
21873
- (0, import_node_fs36.rmSync)(from, { force: true });
22439
+ (0, import_node_fs39.cpSync)(from, to);
22440
+ (0, import_node_fs39.rmSync)(from, { force: true });
21874
22441
  }
21875
22442
  }
21876
22443
  function carryLegacyContents(gateDir, verityDir) {
21877
22444
  let copied = 0;
21878
22445
  const walk = (relDir) => {
21879
22446
  const srcDir = (0, import_node_path28.join)(gateDir, relDir);
21880
- for (const entry of (0, import_node_fs36.readdirSync)(srcDir)) {
22447
+ for (const entry of (0, import_node_fs39.readdirSync)(srcDir)) {
21881
22448
  const rel = relDir ? (0, import_node_path28.join)(relDir, entry) : entry;
21882
22449
  const src = (0, import_node_path28.join)(gateDir, rel);
21883
22450
  const dest = (0, import_node_path28.join)(verityDir, rel);
21884
- if ((0, import_node_fs36.statSync)(src).isDirectory()) {
22451
+ if ((0, import_node_fs39.statSync)(src).isDirectory()) {
21885
22452
  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);
22453
+ } else if (!(0, import_node_fs39.existsSync)(dest)) {
22454
+ (0, import_node_fs39.mkdirSync)((0, import_node_path28.dirname)(dest), { recursive: true });
22455
+ (0, import_node_fs39.cpSync)(src, dest);
21889
22456
  copied++;
21890
22457
  }
21891
22458
  }
@@ -21896,20 +22463,20 @@ function carryLegacyContents(gateDir, verityDir) {
21896
22463
  async function needsMigration(root = repoRoot()) {
21897
22464
  const gateDir = (0, import_node_path28.join)(root, ".gate");
21898
22465
  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"))) {
22466
+ if ((0, import_node_fs39.existsSync)(gateDir) && !(0, import_node_fs39.existsSync)(verityDir)) return true;
22467
+ if ((0, import_node_fs39.existsSync)(gateDir) && (0, import_node_fs39.existsSync)(verityDir)) {
22468
+ if ((0, import_node_fs39.existsSync)((0, import_node_path28.join)(gateDir, "credentials")) && !(0, import_node_fs39.existsSync)((0, import_node_path28.join)(verityDir, "credentials"))) {
21902
22469
  return true;
21903
22470
  }
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"))) {
22471
+ if ((0, import_node_fs39.existsSync)((0, import_node_path28.join)(gateDir, "memory")) && !(0, import_node_fs39.existsSync)((0, import_node_path28.join)(verityDir, "memory"))) {
21905
22472
  return true;
21906
22473
  }
21907
22474
  }
21908
22475
  const claudeMd = (0, import_node_path28.join)(root, "CLAUDE.md");
21909
- if ((0, import_node_fs36.existsSync)(claudeMd) && hasLegacyMemoryBlock(readFileSyncSafe(claudeMd))) {
22476
+ if ((0, import_node_fs39.existsSync)(claudeMd) && hasLegacyMemoryBlock(readFileSyncSafe(claudeMd))) {
21910
22477
  return true;
21911
22478
  }
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"))) {
22479
+ if ((0, import_node_fs39.existsSync)((0, import_node_path28.join)(root, "GATE.md")) && !(0, import_node_fs39.existsSync)((0, import_node_path28.join)(root, "VERITY.md"))) {
21913
22480
  return true;
21914
22481
  }
21915
22482
  if (await hasLegacyHooksAt(root)) return true;
@@ -22053,7 +22620,7 @@ function resolveDataDir() {
22053
22620
  // local dev: running from repo root
22054
22621
  ];
22055
22622
  for (const candidate of candidates) {
22056
- if ((0, import_node_fs37.existsSync)((0, import_node_path29.join)(candidate, "skills"))) {
22623
+ if ((0, import_node_fs40.existsSync)((0, import_node_path29.join)(candidate, "skills"))) {
22057
22624
  return candidate;
22058
22625
  }
22059
22626
  }
@@ -22069,7 +22636,7 @@ function registerInitCommand(program2) {
22069
22636
  program2.command("init").description("Initialize Verity in the current project").option("--force", "Overwrite existing skills and hooks").action(async (opts) => {
22070
22637
  const force = opts.force ?? false;
22071
22638
  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));
22639
+ const isProject = projectMarkers.some((m) => (0, import_node_fs40.existsSync)(m));
22073
22640
  if (!isProject) {
22074
22641
  printError("No project detected in the current directory.");
22075
22642
  printInfo('Run "verity init" from your project root.');
@@ -22139,14 +22706,14 @@ function registerInitCommand(program2) {
22139
22706
  for (const skill of skills) {
22140
22707
  const src = (0, import_node_path29.join)(skillsSource, skill);
22141
22708
  const dest = (0, import_node_path29.join)(skillsDest, skill);
22142
- if (!(0, import_node_fs37.existsSync)(src)) {
22709
+ if (!(0, import_node_fs40.existsSync)(src)) {
22143
22710
  printWarn(` Skill data not found: ${skill}`);
22144
22711
  continue;
22145
22712
  }
22146
- if ((0, import_node_fs37.existsSync)(dest) && !force) {
22713
+ if ((0, import_node_fs40.existsSync)(dest) && !force) {
22147
22714
  const srcSkill = (0, import_node_path29.join)(src, "SKILL.md");
22148
22715
  const destSkill = (0, import_node_path29.join)(dest, "SKILL.md");
22149
- if ((0, import_node_fs37.existsSync)(destSkill)) {
22716
+ if ((0, import_node_fs40.existsSync)(destSkill)) {
22150
22717
  try {
22151
22718
  const srcContent = await (0, import_promises13.readFile)(srcSkill, "utf-8");
22152
22719
  const destContent = await (0, import_promises13.readFile)(destSkill, "utf-8");
@@ -22224,7 +22791,7 @@ function registerInitCommand(program2) {
22224
22791
  }
22225
22792
 
22226
22793
  // src/commands/uninstall.ts
22227
- var import_node_fs38 = require("node:fs");
22794
+ var import_node_fs41 = require("node:fs");
22228
22795
  var import_node_path30 = require("node:path");
22229
22796
  var SKILL_NAMES = [
22230
22797
  "verity-setup",
@@ -22245,10 +22812,10 @@ function registerUninstallCommand(program2) {
22245
22812
  const skillsRoot = projectPath(".claude/skills");
22246
22813
  for (const name of SKILL_NAMES) {
22247
22814
  const dir = (0, import_node_path30.join)(skillsRoot, name);
22248
- if ((0, import_node_fs38.existsSync)(dir)) {
22815
+ if ((0, import_node_fs41.existsSync)(dir)) {
22249
22816
  actions.push({
22250
22817
  label: `Remove .claude/skills/${name}/`,
22251
- apply: () => (0, import_node_fs38.rmSync)(dir, { recursive: true, force: true })
22818
+ apply: () => (0, import_node_fs41.rmSync)(dir, { recursive: true, force: true })
22252
22819
  });
22253
22820
  }
22254
22821
  }
@@ -22262,24 +22829,24 @@ function registerUninstallCommand(program2) {
22262
22829
  });
22263
22830
  }
22264
22831
  const verityDir = projectPath(VERITY_DIR);
22265
- if ((0, import_node_fs38.existsSync)(verityDir)) {
22832
+ if ((0, import_node_fs41.existsSync)(verityDir)) {
22266
22833
  actions.push({
22267
22834
  label: `Remove ${VERITY_DIR}/`,
22268
- apply: () => (0, import_node_fs38.rmSync)(verityDir, { recursive: true, force: true })
22835
+ apply: () => (0, import_node_fs41.rmSync)(verityDir, { recursive: true, force: true })
22269
22836
  });
22270
22837
  }
22271
22838
  if (!keepVerityMd) {
22272
22839
  const verityMd = projectPath(VERITY_MD_FILE);
22273
- if ((0, import_node_fs38.existsSync)(verityMd)) {
22840
+ if ((0, import_node_fs41.existsSync)(verityMd)) {
22274
22841
  actions.push({
22275
22842
  label: `Remove ${VERITY_MD_FILE}`,
22276
- apply: () => (0, import_node_fs38.rmSync)(verityMd, { force: true })
22843
+ apply: () => (0, import_node_fs41.rmSync)(verityMd, { force: true })
22277
22844
  });
22278
22845
  }
22279
22846
  }
22280
22847
  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);
22848
+ if ((0, import_node_fs41.existsSync)(path) && (0, import_node_fs41.statSync)(path).isDirectory() && (0, import_node_fs41.readdirSync)(path).length === 0) {
22849
+ (0, import_node_fs41.rmdirSync)(path);
22283
22850
  }
22284
22851
  };
22285
22852
  actions.push({
@@ -22291,10 +22858,10 @@ function registerUninstallCommand(program2) {
22291
22858
  });
22292
22859
  const home = process.env.HOME ?? "";
22293
22860
  const globalVerityDir = (0, import_node_path30.join)(home, ".verity");
22294
- if (purgeGlobal && (0, import_node_fs38.existsSync)(globalVerityDir)) {
22861
+ if (purgeGlobal && (0, import_node_fs41.existsSync)(globalVerityDir)) {
22295
22862
  actions.push({
22296
22863
  label: `Remove ~/.verity/ (global credentials \u2014 reconnect requires re-registration)`,
22297
- apply: () => (0, import_node_fs38.rmSync)(globalVerityDir, { recursive: true, force: true })
22864
+ apply: () => (0, import_node_fs41.rmSync)(globalVerityDir, { recursive: true, force: true })
22298
22865
  });
22299
22866
  }
22300
22867
  if (actions.length === 0) {
@@ -22488,7 +23055,7 @@ function registerTaskCommands(program2) {
22488
23055
  }
22489
23056
 
22490
23057
  // src/commands/reset.ts
22491
- var import_node_fs39 = require("node:fs");
23058
+ var import_node_fs42 = require("node:fs");
22492
23059
  var import_node_path31 = require("node:path");
22493
23060
  function registerResetCommand(program2) {
22494
23061
  program2.command("reset").description("Close the current task and clear transient state").option("--keep-task", "Only purge caches; leave the current task open").option("--all", "Also purge diagnostic logs (.verity/.logs/)").action(async (opts) => {
@@ -22526,11 +23093,11 @@ function registerResetCommand(program2) {
22526
23093
  }
22527
23094
  const cacheDir = projectPath(CACHE_DIR);
22528
23095
  let purged = 0;
22529
- if ((0, import_node_fs39.existsSync)(cacheDir)) {
22530
- for (const entry of (0, import_node_fs39.readdirSync)(cacheDir)) {
23096
+ if ((0, import_node_fs42.existsSync)(cacheDir)) {
23097
+ for (const entry of (0, import_node_fs42.readdirSync)(cacheDir)) {
22531
23098
  if (entry.startsWith("pending-")) {
22532
23099
  try {
22533
- (0, import_node_fs39.unlinkSync)((0, import_node_path31.join)(cacheDir, entry));
23100
+ (0, import_node_fs42.unlinkSync)((0, import_node_path31.join)(cacheDir, entry));
22534
23101
  purged++;
22535
23102
  } catch {
22536
23103
  }
@@ -22545,19 +23112,19 @@ function registerResetCommand(program2) {
22545
23112
  projectPath(`${VERITY_DIR}/.last-analysis`)
22546
23113
  ];
22547
23114
  for (const file of filesToClear) {
22548
- if ((0, import_node_fs39.existsSync)(file)) {
23115
+ if ((0, import_node_fs42.existsSync)(file)) {
22549
23116
  try {
22550
- (0, import_node_fs39.writeFileSync)(file, "");
23117
+ (0, import_node_fs42.writeFileSync)(file, "");
22551
23118
  } catch {
22552
23119
  }
22553
23120
  }
22554
23121
  }
22555
23122
  if (opts.all) {
22556
23123
  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)) {
23124
+ if ((0, import_node_fs42.existsSync)(logsDir)) {
23125
+ for (const entry of (0, import_node_fs42.readdirSync)(logsDir)) {
22559
23126
  try {
22560
- (0, import_node_fs39.unlinkSync)((0, import_node_path31.join)(logsDir, entry));
23127
+ (0, import_node_fs42.unlinkSync)((0, import_node_path31.join)(logsDir, entry));
22561
23128
  } catch {
22562
23129
  }
22563
23130
  }
@@ -22865,8 +23432,8 @@ function registerTelemetryCommands(program2) {
22865
23432
  }
22866
23433
 
22867
23434
  // 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");
23435
+ program.name("verity").description("CLI for Verity quality gate service").version("0.31.0-experimental.48d33ea").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async (_thisCommand, actionCommand) => {
23436
+ installStderrLog(actionCommand.name(), process.argv.slice(2), "0.31.0-experimental.48d33ea");
22870
23437
  setUserNamedServiceUrl(program.opts().serviceUrl);
22871
23438
  try {
22872
23439
  await foldLegacyLocalCredential();
@@ -22889,6 +23456,7 @@ registerAnalyzeCommand(program);
22889
23456
  registerBaselineCommands(program);
22890
23457
  registerReviewCommand(program);
22891
23458
  registerGuardCommand(program);
23459
+ registerIgnoreCommand(program);
22892
23460
  registerInitCommand(program);
22893
23461
  registerUninstallCommand(program);
22894
23462
  registerTaskCommands(program);