@codacy/verity-cli 0.28.1-experimental.4da2503 → 0.28.1-experimental.6eac6aa

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.
Files changed (2) hide show
  1. package/bin/verity.js +646 -102
  2. package/package.json +1 -1
package/bin/verity.js CHANGED
@@ -10386,10 +10386,9 @@ function projectPath(relativePath) {
10386
10386
  return (0, import_node_path.join)(repoRoot(), relativePath);
10387
10387
  }
10388
10388
  var MAX_DELTA_BYTES = 194560;
10389
- var MAX_FILES = 20;
10389
+ var MAX_FILES = 40;
10390
10390
  var MAX_FILE_BYTES = 51200;
10391
10391
  var DEBOUNCE_SECONDS = 30;
10392
- var MAX_ITERATIONS = 2;
10393
10392
  var MAX_SPEC_FILES = 10;
10394
10393
  var MAX_SPEC_FILE_BYTES = 10240;
10395
10394
  var MAX_TOTAL_SPEC_BYTES = 30720;
@@ -13591,6 +13590,70 @@ var import_node_fs10 = require("node:fs");
13591
13590
  var import_node_crypto5 = require("node:crypto");
13592
13591
  var import_node_path11 = require("node:path");
13593
13592
 
13593
+ // src/lib/skip-detection.ts
13594
+ function isBareAckPrompt(prompt) {
13595
+ if (typeof prompt !== "string") return false;
13596
+ const trimmed = prompt.trim();
13597
+ if (trimmed.length === 0) return false;
13598
+ if (trimmed.length > 20) return false;
13599
+ const bareAckPattern = /^(\d{1,2}|y|n|yes|no|yep|nope|ok(ay)?|sure|skip|cancel|stop|done|noted|got\s+it|sounds\s+good|thanks|thank\s+you|thx)[.!?]*$/i;
13600
+ return bareAckPattern.test(trimmed);
13601
+ }
13602
+ function isContinuationPrompt(prompt) {
13603
+ if (typeof prompt !== "string") return false;
13604
+ const trimmed = prompt.trim();
13605
+ if (trimmed.length === 0) return false;
13606
+ if (trimmed.length > 24) return false;
13607
+ const continuation = /^(let['’]?s\s+(go|do\s+it|start|continue)|go|go\s+ahead|go\s+on|proceed|continue|carry\s+on|keep\s+going|do\s+it|make\s+it\s+so|next|start|begin|ship\s+it|yes\s+please|please\s+continue|perfect|great|nice|excellent|agreed)[.!]*$/i;
13608
+ return continuation.test(trimmed) || isBareAckPrompt(trimmed);
13609
+ }
13610
+ function resolveGoalPrompt(prompts) {
13611
+ if (prompts.length === 0) return null;
13612
+ const latest = prompts[prompts.length - 1];
13613
+ if (!isContinuationPrompt(latest.prompt)) return { entry: latest, turnsBack: 0 };
13614
+ for (let i = prompts.length - 2; i >= 0; i--) {
13615
+ if (!isContinuationPrompt(prompts[i].prompt)) {
13616
+ return { entry: prompts[i], turnsBack: prompts.length - 1 - i };
13617
+ }
13618
+ }
13619
+ return { entry: latest, turnsBack: 0 };
13620
+ }
13621
+ function isReflectionQuestion(response) {
13622
+ if (!response || typeof response !== "string") return false;
13623
+ const markers = [
13624
+ /reflection\s+for\s+future\s+agents/i,
13625
+ /what(?:'s|\s+is)\s+one\s+thing\s+you\s+learned/i,
13626
+ /say\s+['"]?skip['"]?\s+to\s+skip/i,
13627
+ /quick\s+reflection\s+question/i,
13628
+ // Post-flip (VRT-21): the agent drafts the reflection itself and, when
13629
+ // interactive, asks the user to confirm/correct before recording. That
13630
+ // turn authors no code either, so it's still a reflection turn.
13631
+ /reflection\s+draft/i,
13632
+ /confirm,?\s+correct,?\s+or\s+add/i
13633
+ ];
13634
+ return markers.some((m) => m.test(response));
13635
+ }
13636
+ function isMetaTaskLabel(label2) {
13637
+ if (label2 === null || label2 === void 0) return false;
13638
+ if (typeof label2 !== "string") return false;
13639
+ const trimmed = label2.trim();
13640
+ if (trimmed.length === 0) return true;
13641
+ const metaPatterns = [
13642
+ /^verity\s+[\w-]+\s+response$/i,
13643
+ // "Verity reflect response"
13644
+ /^simple user response$/i,
13645
+ /^verity\s+command$/i,
13646
+ // "Verity command"
13647
+ /^user\s+(question|reply|response|ack)$/i
13648
+ ];
13649
+ return metaPatterns.some((p) => p.test(trimmed));
13650
+ }
13651
+ function shouldSkipForBareAck(input) {
13652
+ if (!isBareAckPrompt(input.prompt)) return false;
13653
+ if (input.turnAuthoredCode) return false;
13654
+ return input.canSeeTurnAuthorship;
13655
+ }
13656
+
13594
13657
  // src/lib/dossier.ts
13595
13658
  var import_node_fs9 = require("node:fs");
13596
13659
  var import_node_crypto4 = require("node:crypto");
@@ -13960,6 +14023,12 @@ function reduce(state, events, now) {
13960
14023
  }
13961
14024
  case "verdict": {
13962
14025
  state.meta.last_verdict_seq = ev.seq;
14026
+ state.meta.channel = {
14027
+ emittedLast: ev.emitted === true,
14028
+ // Reset by ANY movement, so the counter measures a standstill rather
14029
+ // than session length.
14030
+ consecutiveIdle: ev.idle === false ? 0 : (state.meta.channel?.consecutiveIdle ?? 0) + 1
14031
+ };
13963
14032
  state.meta.last_adjudication = ev.intent_verdict ? { verdict: ev.intent_verdict, score: ev.intent_score ?? null, at: ev.at, decision: ev.decision } : void 0;
13964
14033
  if (ev.intent_sig) {
13965
14034
  state.meta.intent_repeat = state.meta.intent_repeat && state.meta.intent_repeat.sig === ev.intent_sig ? { sig: ev.intent_sig, consecutive: state.meta.intent_repeat.consecutive + 1 } : { sig: ev.intent_sig, consecutive: 1 };
@@ -14327,6 +14396,10 @@ function projectMemory(state, opts) {
14327
14396
  ...active.delivered && { delivered: active.delivered }
14328
14397
  };
14329
14398
  }
14399
+ if (opts.capture) {
14400
+ const missed = Math.max(0, opts.capture.seen - opts.capture.captured);
14401
+ p.capture = { seen: opts.capture.seen, captured: opts.capture.captured, missed };
14402
+ }
14330
14403
  if (state.meta.last_adjudication) {
14331
14404
  const a = state.meta.last_adjudication;
14332
14405
  p.last_adjudication = { verdict: a.verdict, score: a.score, at: a.at };
@@ -14573,7 +14646,8 @@ function recall(d, input) {
14573
14646
  continuity,
14574
14647
  spoken: reanchored.spoken,
14575
14648
  refused: reanchored.dropped.length,
14576
- lastVerdictSeq
14649
+ lastVerdictSeq,
14650
+ ...input.capture && { capture: input.capture }
14577
14651
  });
14578
14652
  return {
14579
14653
  state: effective,
@@ -14684,7 +14758,19 @@ function sessionDossier(token, sessionId) {
14684
14758
  const d = openDossier(identity);
14685
14759
  return d ? { d, identity } : null;
14686
14760
  }
14761
+ function hasActiveGoal(d) {
14762
+ try {
14763
+ if (!(0, import_node_fs10.existsSync)(d.eventsPath)) return false;
14764
+ return (0, import_node_fs10.readFileSync)(d.eventsPath, "utf8").includes('"k":"goal"');
14765
+ } catch {
14766
+ return false;
14767
+ }
14768
+ }
14687
14769
  function recordGoal(d, prompt, source = "prompt") {
14770
+ if (source === "prompt" && isContinuationPrompt(prompt) && hasActiveGoal(d)) {
14771
+ appendEvent(d, { k: "goal_continue", text: prompt.slice(0, 64) });
14772
+ return;
14773
+ }
14688
14774
  const text = prompt.slice(0, MAX_GOAL_CHARS);
14689
14775
  appendEvent(d, {
14690
14776
  k: "goal",
@@ -14800,6 +14886,8 @@ function recordVerdict(d, v) {
14800
14886
  branch: v.branch,
14801
14887
  decision: v.decision,
14802
14888
  ...sig && { intent_sig: sig },
14889
+ emitted: v.emitted === true,
14890
+ idle: v.idle !== false,
14803
14891
  ...v.intent?.verdict && { intent_verdict: v.intent.verdict },
14804
14892
  ...typeof v.intent?.score === "number" && { intent_score: v.intent.score }
14805
14893
  });
@@ -14846,7 +14934,9 @@ function recallMemory(d, identity, opts) {
14846
14934
  const state = foldDossier(d);
14847
14935
  const watermark = state.meta.watermark?.sha ?? null;
14848
14936
  const watermarkPaths = (state.authored ?? []).map((a) => a.path);
14937
+ const captureCmp = typeof opts.userMessagesSeen === "number" ? { seen: opts.userMessagesSeen, captured: state.meta.goal_chain } : void 0;
14849
14938
  const r = recall(d, {
14939
+ ...captureCmp && { capture: captureCmp },
14850
14940
  identity,
14851
14941
  currentSessionKey: opts.currentSessionKey,
14852
14942
  branchNow: getCurrentBranch(),
@@ -15103,6 +15193,8 @@ function collectCodeDelta(files, opts) {
15103
15193
  let totalSize = 0;
15104
15194
  let truncationReason = null;
15105
15195
  const droppedPaths = [];
15196
+ const excluded = [];
15197
+ const exclude = (path, reason) => excluded.push({ path, reason, stage: "collectCodeDelta", kind: "capacity" });
15106
15198
  for (const filepath of sorted) {
15107
15199
  if (result.length >= maxFiles) {
15108
15200
  truncationReason ??= "max_files";
@@ -15110,14 +15202,21 @@ function collectCodeDelta(files, opts) {
15110
15202
  continue;
15111
15203
  }
15112
15204
  const resolved = resolveFile(filepath);
15113
- if (!resolved) continue;
15205
+ if (!resolved) {
15206
+ exclude(filepath, "path-not-resolvable");
15207
+ continue;
15208
+ }
15114
15209
  let size;
15115
15210
  try {
15116
15211
  size = (0, import_node_fs11.statSync)(resolved).size;
15117
15212
  } catch {
15213
+ exclude(filepath, "not-stattable");
15214
+ continue;
15215
+ }
15216
+ if (size > maxFileBytes) {
15217
+ exclude(filepath, `over-file-size-limit-${maxFileBytes}b`);
15118
15218
  continue;
15119
15219
  }
15120
- if (size > maxFileBytes) continue;
15121
15220
  if (totalSize + size > maxTotalBytes) {
15122
15221
  truncationReason ??= "max_total_bytes";
15123
15222
  const idx = sorted.indexOf(filepath);
@@ -15128,6 +15227,7 @@ function collectCodeDelta(files, opts) {
15128
15227
  try {
15129
15228
  content = (0, import_node_fs11.readFileSync)(resolved, "utf-8");
15130
15229
  } catch {
15230
+ exclude(filepath, "not-readable");
15131
15231
  continue;
15132
15232
  }
15133
15233
  totalSize += size;
@@ -15141,10 +15241,14 @@ function collectCodeDelta(files, opts) {
15141
15241
  (sum, f) => sum + f.content.split("\n").length,
15142
15242
  0
15143
15243
  );
15244
+ for (const path of droppedPaths) {
15245
+ exclude(path, truncationReason === "max_files" ? "max-files-cap" : "max-total-bytes-cap");
15246
+ }
15144
15247
  return {
15145
15248
  files: result,
15146
15249
  total_lines: totalLines,
15147
15250
  total_files: result.length,
15251
+ excluded,
15148
15252
  ...truncationReason && {
15149
15253
  truncated: {
15150
15254
  reason: truncationReason,
@@ -15438,6 +15542,34 @@ ${addedLines}`,
15438
15542
  }
15439
15543
  return { diffs, has_baseline: true };
15440
15544
  }
15545
+ function absorbIntoBaseline(paths, sessionId) {
15546
+ const baseline = readBaseline(sessionId);
15547
+ if (!baseline || paths.length === 0) return 0;
15548
+ const dir = sessionDir(sessionKey(baseline.session_id));
15549
+ let adopted = 0;
15550
+ const dirty = new Set(baseline.dirty_paths);
15551
+ for (const p of paths) {
15552
+ try {
15553
+ const content = safeReadForMirror(projectPath(p));
15554
+ if (content === null) continue;
15555
+ const dest = mirrorPath(dir, p);
15556
+ (0, import_node_fs13.mkdirSync)((0, import_node_path14.dirname)(dest), { recursive: true });
15557
+ (0, import_node_fs13.writeFileSync)(dest, content);
15558
+ dirty.add(p);
15559
+ adopted++;
15560
+ } catch {
15561
+ }
15562
+ }
15563
+ if (adopted === 0) return 0;
15564
+ try {
15565
+ const updated = { ...baseline, dirty_paths: [...dirty] };
15566
+ (0, import_node_fs13.writeFileSync)(manifestPath(dir), JSON.stringify(updated));
15567
+ preImageCache.delete(baseline);
15568
+ } catch {
15569
+ return 0;
15570
+ }
15571
+ return adopted;
15572
+ }
15441
15573
  function changedSinceBaseline(repoRelPath, baseline) {
15442
15574
  const pre = preImage(repoRelPath, baseline);
15443
15575
  let current;
@@ -16314,40 +16446,40 @@ function narrowToRecent(files, sessionId) {
16314
16446
  });
16315
16447
  return recent.length > 0 ? recent : files;
16316
16448
  }
16317
- function readIteration(currentCommit, _contentHash) {
16318
- if (!(0, import_node_fs15.existsSync)(ITERATION_FILE)) return 1;
16449
+ function readIterationState(currentCommit) {
16450
+ if (!(0, import_node_fs15.existsSync)(ITERATION_FILE)) return { iteration: 1, fingerprint: null };
16319
16451
  try {
16320
16452
  const stored = (0, import_node_fs15.readFileSync)(ITERATION_FILE, "utf-8").trim();
16321
16453
  const parts = stored.split(":");
16322
16454
  const iter = parseInt(parts[0], 10);
16323
16455
  const storedCommit = parts[1] ?? "";
16324
16456
  const storedTimestamp = parseInt(parts[2] ?? "0", 10);
16325
- if (isNaN(iter)) return 1;
16326
- if (storedCommit !== currentCommit) return 1;
16457
+ const fingerprint = parts.slice(3).join(":") || null;
16458
+ if (isNaN(iter)) return { iteration: 1, fingerprint: null };
16459
+ if (storedCommit !== currentCommit) return { iteration: 1, fingerprint: null };
16327
16460
  if (storedTimestamp > 0) {
16328
16461
  const elapsed = Math.floor(Date.now() / 1e3) - storedTimestamp;
16329
- if (elapsed > 600) return 1;
16462
+ if (elapsed > 600) return { iteration: 1, fingerprint: null };
16330
16463
  }
16331
- return iter;
16464
+ return { iteration: iter, fingerprint };
16332
16465
  } catch {
16333
- return 1;
16466
+ return { iteration: 1, fingerprint: null };
16334
16467
  }
16335
16468
  }
16336
- function checkMaxIterations(currentCommit, maxIterations = MAX_ITERATIONS, contentHash) {
16337
- const iteration = readIteration(currentCommit, contentHash);
16338
- if (iteration > maxIterations) {
16339
- writeIteration(1, currentCommit, contentHash);
16340
- return {
16341
- skip: `Max Verity iterations (${maxIterations}) reached \u2014 accepting to prevent infinite loop. Human review required before deploying.`,
16342
- iteration
16343
- };
16344
- }
16345
- return { skip: null, iteration };
16469
+ function findingsFingerprint(findings) {
16470
+ const keys = findings.map((f) => `${String(f.pattern_id ?? "?")}|${String(f.file ?? "?")}`).filter((k) => k !== "?|?");
16471
+ return [...new Set(keys)].sort().join(",");
16346
16472
  }
16347
- function writeIteration(iteration, commit, _contentHash) {
16473
+ function isSameProblem(previous, current) {
16474
+ if (!previous || !current) return false;
16475
+ const prev = new Set(previous.split(","));
16476
+ return current.split(",").some((k) => prev.has(k));
16477
+ }
16478
+ function writeIteration(iteration, commit, _contentHash, fingerprint) {
16348
16479
  (0, import_node_fs15.mkdirSync)(VERITY_DIR, { recursive: true });
16349
16480
  const ts = Math.floor(Date.now() / 1e3);
16350
- (0, import_node_fs15.writeFileSync)(ITERATION_FILE, `${iteration}:${commit}:${ts}`);
16481
+ const fp = fingerprint ? `:${fingerprint}` : "";
16482
+ (0, import_node_fs15.writeFileSync)(ITERATION_FILE, `${iteration}:${commit}:${ts}${fp}`);
16351
16483
  }
16352
16484
 
16353
16485
  // src/lib/static-analysis.ts
@@ -16596,7 +16728,7 @@ function resolveTaskContext(opts) {
16596
16728
  // src/lib/cli-version.ts
16597
16729
  function cliVersion() {
16598
16730
  try {
16599
- return true ? "0.28.1-experimental.4da2503" : "dev";
16731
+ return true ? "0.28.1-experimental.6eac6aa" : "dev";
16600
16732
  } catch {
16601
16733
  return "dev";
16602
16734
  }
@@ -16822,6 +16954,15 @@ var KNOWN_RECORD_TYPES = /* @__PURE__ */ new Set([
16822
16954
  ]);
16823
16955
  var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "NotebookEdit", "MultiEdit"]);
16824
16956
  var DISPATCH_TOOLS = /* @__PURE__ */ new Set(["Task", "Workflow"]);
16957
+ function hasUserText(record) {
16958
+ const message = record.message;
16959
+ const content = message?.content ?? record.content;
16960
+ if (typeof content === "string") return content.trim().length > 0;
16961
+ if (!Array.isArray(content)) return false;
16962
+ return content.some(
16963
+ (b) => b?.type === "text" && typeof b.text === "string" && b.text.trim().length > 0
16964
+ );
16965
+ }
16825
16966
  var COMMAND_CLASSES = [
16826
16967
  [/\b(npm|yarn|pnpm|bun)\s+(run\s+)?test\b|\bvitest\b|\bjest\b|\bpytest\b|\bgo test\b/, "test"],
16827
16968
  [/\b(npm|yarn|pnpm|bun)\s+(run\s+)?build\b|\btsc\b|\bwebpack\b|\bcargo build\b/, "build"],
@@ -16949,6 +17090,7 @@ function fold(transcriptPath, opts = {}) {
16949
17090
  malformed: 0,
16950
17091
  subagentFiles: 0,
16951
17092
  dispatched: 0,
17093
+ userMessages: 0,
16952
17094
  subagentSkipped: 0,
16953
17095
  compactions: 0,
16954
17096
  complete: false
@@ -16975,6 +17117,7 @@ function fold(transcriptPath, opts = {}) {
16975
17117
  if (type === "system" && record.subtype === "compact_boundary") {
16976
17118
  result.coverage.compactions++;
16977
17119
  }
17120
+ if (type === "user" && hasUserText(record)) result.coverage.userMessages++;
16978
17121
  collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, opts.repoRoot, result.coverage);
16979
17122
  }
16980
17123
  };
@@ -17130,6 +17273,86 @@ function checkConservation(changedFiles, result, repoRoot2) {
17130
17273
  };
17131
17274
  }
17132
17275
 
17276
+ // src/lib/verdict.ts
17277
+ function reconcileCoverage(changed, coverage) {
17278
+ const changedSet = new Set(changed);
17279
+ const reviewed = coverage.reviewed.filter((p) => changedSet.has(p));
17280
+ const claimed = /* @__PURE__ */ new Set([...reviewed, ...coverage.notReviewed.map((n) => n.path)]);
17281
+ const unaccounted = [...changedSet].filter((p) => !claimed.has(p)).sort();
17282
+ const notReviewed = [
17283
+ ...coverage.notReviewed.filter((n) => changedSet.has(n.path)),
17284
+ ...unaccounted.map((path) => ({
17285
+ path,
17286
+ reason: "unaccounted",
17287
+ // Named so the eventual bug report writes itself: some stage removed this
17288
+ // path and did not say so.
17289
+ stage: "unknown-stage",
17290
+ // An undeclared drop is CAPACITY by default. A stage that cannot be
17291
+ // bothered to say why it dropped a file does not get the benefit of the
17292
+ // doubt — that default is what makes forgetting expensive.
17293
+ kind: "capacity"
17294
+ }))
17295
+ ];
17296
+ return {
17297
+ coverage: { reviewed: [...new Set(reviewed)].sort(), notReviewed },
17298
+ unaccounted,
17299
+ balances: unaccounted.length === 0
17300
+ };
17301
+ }
17302
+ function resolveVerdict(proposed, coverage) {
17303
+ if (proposed === "FAIL") return "FAIL";
17304
+ const blocking = coverage.notReviewed.filter((n) => (n.kind ?? "capacity") !== "policy");
17305
+ if (blocking.length === 0) return proposed;
17306
+ return "WARN";
17307
+ }
17308
+ function describeCoverage(coverage, maxPaths = 5) {
17309
+ const relevant = coverage.notReviewed.filter((n) => (n.kind ?? "capacity") !== "policy");
17310
+ if (relevant.length === 0) return null;
17311
+ const byReason = /* @__PURE__ */ new Map();
17312
+ for (const n of relevant) {
17313
+ const key = `${n.reason}`;
17314
+ const list = byReason.get(key) ?? [];
17315
+ list.push(n.path);
17316
+ byReason.set(key, list);
17317
+ }
17318
+ const lines = [];
17319
+ for (const [reason, paths] of [...byReason.entries()].sort()) {
17320
+ const shown = paths.slice(0, maxPaths).join(", ");
17321
+ const more = paths.length > maxPaths ? ` (+${paths.length - maxPaths} more)` : "";
17322
+ lines.push(` ${paths.length} not reviewed \u2014 ${reason}: ${shown}${more}`);
17323
+ }
17324
+ return `NOT A CLEAN REVIEW. ${relevant.length} changed file(s) never reached the reviewer, so this verdict does not cover them:
17325
+ ${lines.join("\n")}
17326
+ Treat those files as UNCHECKED, not as approved.`;
17327
+ }
17328
+ function openBlockingElsewhere(statements, reviewedNow, lineShaAt) {
17329
+ const reviewed = new Set(reviewedNow);
17330
+ const out = [];
17331
+ const seen = /* @__PURE__ */ new Set();
17332
+ for (const s of statements) {
17333
+ if (s.outcome !== "open") continue;
17334
+ if (s.register !== "BLOCK") continue;
17335
+ if (s.carried) continue;
17336
+ if (reviewed.has(s.file)) continue;
17337
+ if (!s.line_sha) continue;
17338
+ if (lineShaAt(s.file, s.line) !== s.line_sha) continue;
17339
+ const key = `${s.file}::${s.pattern_id}`;
17340
+ if (seen.has(key)) continue;
17341
+ seen.add(key);
17342
+ out.push({ file: s.file, line: s.line, pattern_id: s.pattern_id });
17343
+ }
17344
+ return out;
17345
+ }
17346
+ function describeOpenElsewhere(open) {
17347
+ if (open.length === 0) return null;
17348
+ const lines = open.slice(0, 5).map((o) => ` ${o.file}:${o.line} [${o.pattern_id}]`);
17349
+ const more = open.length > 5 ? `
17350
+ (+${open.length - 5} more)` : "";
17351
+ return `STILL OPEN ELSEWHERE. ${open.length} blocking finding(s) Verity raised earlier are still present in files this run did not review:
17352
+ ${lines.join("\n")}${more}
17353
+ This verdict covers the current change only. The tree is not clean.`;
17354
+ }
17355
+
17133
17356
  // src/lib/channel.ts
17134
17357
  var MAX_AGENT_CONTEXT_CHARS = 1500;
17135
17358
  var MAX_AGENT_ITEMS = 5;
@@ -17179,6 +17402,7 @@ function buildAgentContext(input) {
17179
17402
  }
17180
17403
  for (const p of input.pendingItems ?? []) {
17181
17404
  if (lines.length >= MAX_AGENT_ITEMS) break;
17405
+ if (p.pattern_id === "intent-misalignment") continue;
17182
17406
  const text = p.description ?? p.title ?? p.reason;
17183
17407
  if (!text) continue;
17184
17408
  lines.push(renderItem("", text, p.pattern_id, p.file, p.line));
@@ -17208,6 +17432,47 @@ function buildHookOutput(gateDecision, systemMessage, agentContext) {
17208
17432
  } : {}
17209
17433
  };
17210
17434
  }
17435
+ var IDLE_EPISODE_CAP = 3;
17436
+ function channelSilence(input) {
17437
+ const movedSomething = input.newUserPrompt || input.newAuthorship;
17438
+ if (movedSomething) return null;
17439
+ if (input.consecutiveIdle >= IDLE_EPISODE_CAP) return "idle-episode-cap";
17440
+ if (input.emittedLast) return "caused-by-our-own-emission";
17441
+ return null;
17442
+ }
17443
+
17444
+ // src/lib/emit.ts
17445
+ var YELLOW2 = "\x1B[33m";
17446
+ var NC2 = "\x1B[0m";
17447
+ function emitVerdict(input) {
17448
+ const exit = input.exit ?? ((code) => process.exit(code));
17449
+ const { coverage, unaccounted } = reconcileCoverage(input.changed, input.coverage);
17450
+ let verdict = resolveVerdict(input.proposed, coverage);
17451
+ const openElsewhere = input.openElsewhere ?? [];
17452
+ if (verdict === "PASS" && openElsewhere.length > 0) verdict = "WARN";
17453
+ const note = [describeCoverage(coverage), describeOpenElsewhere(openElsewhere)].filter(Boolean).join("\n\n") || null;
17454
+ if (unaccounted.length > 0) {
17455
+ process.stderr.write(
17456
+ `${YELLOW2}Verity: ${unaccounted.length} changed file(s) could not be attributed to any review stage \u2014 counted as unreviewed.${NC2}
17457
+ `
17458
+ );
17459
+ }
17460
+ if (verdict === "FAIL") {
17461
+ input.renderBlocking?.();
17462
+ if (input.agentContext) {
17463
+ process.stderr.write(`
17464
+ ${input.agentContext}
17465
+ `);
17466
+ }
17467
+ if (note && !input.silenced) process.stderr.write(`
17468
+ ${YELLOW2}${note}${NC2}
17469
+ `);
17470
+ return exit(2);
17471
+ }
17472
+ const agentBlock = input.silenced ? null : [input.agentContext, note].filter(Boolean).join("\n\n") || null;
17473
+ printJsonCompact(buildHookOutput(verdict, input.userSummary, agentBlock));
17474
+ return exit(0);
17475
+ }
17211
17476
 
17212
17477
  // src/lib/cache-cleanup.ts
17213
17478
  var import_node_fs21 = require("node:fs");
@@ -17388,46 +17653,6 @@ function shouldWarmRetryAnalyze(result) {
17388
17653
  return false;
17389
17654
  }
17390
17655
 
17391
- // src/lib/skip-detection.ts
17392
- function isBareAckPrompt(prompt) {
17393
- if (typeof prompt !== "string") return false;
17394
- const trimmed = prompt.trim();
17395
- if (trimmed.length === 0) return false;
17396
- if (trimmed.length > 20) return false;
17397
- const bareAckPattern = /^(\d{1,2}|y|n|yes|no|yep|nope|ok(ay)?|sure|skip|cancel|stop|done|noted|got\s+it|sounds\s+good|thanks|thank\s+you|thx)[.!?]*$/i;
17398
- return bareAckPattern.test(trimmed);
17399
- }
17400
- function isReflectionQuestion(response) {
17401
- if (!response || typeof response !== "string") return false;
17402
- const markers = [
17403
- /reflection\s+for\s+future\s+agents/i,
17404
- /what(?:'s|\s+is)\s+one\s+thing\s+you\s+learned/i,
17405
- /say\s+['"]?skip['"]?\s+to\s+skip/i,
17406
- /quick\s+reflection\s+question/i,
17407
- // Post-flip (VRT-21): the agent drafts the reflection itself and, when
17408
- // interactive, asks the user to confirm/correct before recording. That
17409
- // turn authors no code either, so it's still a reflection turn.
17410
- /reflection\s+draft/i,
17411
- /confirm,?\s+correct,?\s+or\s+add/i
17412
- ];
17413
- return markers.some((m) => m.test(response));
17414
- }
17415
- function isMetaTaskLabel(label2) {
17416
- if (label2 === null || label2 === void 0) return false;
17417
- if (typeof label2 !== "string") return false;
17418
- const trimmed = label2.trim();
17419
- if (trimmed.length === 0) return true;
17420
- const metaPatterns = [
17421
- /^verity\s+[\w-]+\s+response$/i,
17422
- // "Verity reflect response"
17423
- /^simple user response$/i,
17424
- /^verity\s+command$/i,
17425
- // "Verity command"
17426
- /^user\s+(question|reply|response|ack)$/i
17427
- ];
17428
- return metaPatterns.some((p) => p.test(trimmed));
17429
- }
17430
-
17431
17656
  // src/lib/transcript.ts
17432
17657
  var import_node_fs22 = require("node:fs");
17433
17658
  var MAX_READ_BYTES = 256 * 1024;
@@ -17441,9 +17666,11 @@ var MAX_SUMMARY_BYTES = 4096;
17441
17666
  var HOME = process.env.HOME ?? "";
17442
17667
  async function extractActionSummary(transcriptPath) {
17443
17668
  try {
17444
- const lines = readTurnLines(transcriptPath);
17445
- if (!lines || lines.length === 0) return null;
17446
- return buildSummary(lines);
17669
+ const read = readTurnLines(transcriptPath);
17670
+ if (!read || read.lines.length === 0) return null;
17671
+ const summary = buildSummary(read.lines);
17672
+ if (summary) summary.transcript_windowed = read.window;
17673
+ return summary;
17447
17674
  } catch {
17448
17675
  return null;
17449
17676
  }
@@ -17457,9 +17684,11 @@ function readTurnLines(transcriptPath) {
17457
17684
  }
17458
17685
  if (size === 0) return null;
17459
17686
  let raw;
17687
+ let windowed = false;
17460
17688
  if (size <= SMALL_FILE_BYTES) {
17461
17689
  raw = (0, import_node_fs22.readFileSync)(transcriptPath, "utf-8");
17462
17690
  } else {
17691
+ windowed = true;
17463
17692
  const buf = Buffer.alloc(Math.min(MAX_READ_BYTES, size));
17464
17693
  const fd = require("node:fs").openSync(transcriptPath, "r");
17465
17694
  try {
@@ -17477,17 +17706,22 @@ function readTurnLines(transcriptPath) {
17477
17706
  const allLines = raw.split("\n").filter((l) => l.trim().length > 0);
17478
17707
  if (allLines.length === 0) return null;
17479
17708
  let turnStart = 0;
17709
+ let boundaryFound = false;
17480
17710
  for (let i = allLines.length - 1; i >= 0; i--) {
17481
17711
  try {
17482
17712
  const parsed = JSON.parse(allLines[i]);
17483
17713
  if (parsed.type === "user" && isRealUserMessage(parsed)) {
17484
17714
  turnStart = i;
17715
+ boundaryFound = true;
17485
17716
  break;
17486
17717
  }
17487
17718
  } catch {
17488
17719
  }
17489
17720
  }
17490
- return allLines.slice(turnStart);
17721
+ return {
17722
+ lines: allLines.slice(turnStart),
17723
+ window: !windowed ? "whole" : boundaryFound ? "windowed" : "orphaned"
17724
+ };
17491
17725
  }
17492
17726
  function isRealUserMessage(parsed) {
17493
17727
  const message = parsed.message;
@@ -17591,6 +17825,13 @@ function buildSummary(lines) {
17591
17825
  files_read: capArray(filesRead, MAX_FILES_LIST),
17592
17826
  files_edited: capArray(filesEdited, MAX_FILES_LIST),
17593
17827
  files_created: capArray(filesCreated, MAX_CREATED_LIST),
17828
+ // The complement of the two caps that affect SCOPE. `files_read` is excluded
17829
+ // deliberately: reading a file is not authoring it, so a capped read list
17830
+ // narrows nothing.
17831
+ capped_out: [
17832
+ ...cappedOut(filesEdited, MAX_FILES_LIST),
17833
+ ...cappedOut(filesCreated, MAX_CREATED_LIST)
17834
+ ],
17594
17835
  searches,
17595
17836
  commands,
17596
17837
  subagents,
@@ -17641,6 +17882,9 @@ function sanitizeCommand(rawCmd) {
17641
17882
  function capArray(set, max) {
17642
17883
  return Array.from(set).slice(0, max);
17643
17884
  }
17885
+ function cappedOut(set, max) {
17886
+ return Array.from(set).slice(max);
17887
+ }
17644
17888
 
17645
17889
  // src/lib/run-mode.ts
17646
17890
  function parseAutonomousEnv(raw) {
@@ -18050,12 +18294,45 @@ function agentContextFor(response, intentRepeat = 0) {
18050
18294
  });
18051
18295
  }
18052
18296
  var beaconCtx = null;
18053
- async function passAndExit(reason, skip) {
18297
+ async function passAndExit(reason, skip, kindOverride) {
18054
18298
  const sent = await sendSkipBeacon(beaconCtx, skip);
18055
18299
  logEvent("skip", { reason: skip, beacon: sent });
18056
- printJsonCompact({ gate_decision: "PASS", systemMessage: `Verity: ${reason}` });
18300
+ const POLICY_SKIPS = /* @__PURE__ */ new Set([
18301
+ "no-analyzable-files",
18302
+ "verity-command",
18303
+ "bare-acknowledgment",
18304
+ "reflection-prompt",
18305
+ "skip-mode",
18306
+ "zero-increment",
18307
+ "debounce",
18308
+ "no-delta-since-last-review"
18309
+ ]);
18310
+ const skipKind = kindOverride ?? (POLICY_SKIPS.has(skip) ? "policy" : "capacity");
18311
+ const changed = skipCoverageChanged;
18312
+ const { coverage, unaccounted } = reconcileCoverage(changed, {
18313
+ reviewed: [],
18314
+ notReviewed: changed.map((path) => ({ path, reason: skip, stage: "pre-flight", kind: skipKind }))
18315
+ });
18316
+ const verdict = resolveVerdict("PASS", coverage);
18317
+ const note = describeCoverage(coverage);
18318
+ if (unaccounted.length > 0) {
18319
+ logEvent("coverage_unaccounted", { where: "passAndExit", skip, count: unaccounted.length });
18320
+ }
18321
+ const AGENT_SILENT_SKIPS = /* @__PURE__ */ new Set([]);
18322
+ const agentNote = AGENT_SILENT_SKIPS.has(skip) ? null : note;
18323
+ printJsonCompact(
18324
+ buildHookOutput(
18325
+ verdict,
18326
+ `Verity: ${reason}`,
18327
+ // The agent's ONLY input is additionalContext. Sixteen of the nineteen
18328
+ // terminating paths wrote `systemMessage` — the human's field — and told
18329
+ // the agent nothing at all.
18330
+ agentNote
18331
+ )
18332
+ );
18057
18333
  process.exit(0);
18058
18334
  }
18335
+ var skipCoverageChanged = [];
18059
18336
  var EMPTY_STATIC = {
18060
18337
  tool: "@codacy/analysis-cli",
18061
18338
  findings: [],
@@ -18070,7 +18347,7 @@ function runLocalStatic(analyzable, securityFiles, baseline, skipStatic) {
18070
18347
  }
18071
18348
  function localOnlyAndExit(staticResults) {
18072
18349
  printJsonCompact({
18073
- gate_decision: "PASS",
18350
+ gate_decision: "WARN",
18074
18351
  systemMessage: "Verity: not authenticated \u2014 ran a local static-only check (no deep review, no upload). Run `verity init` to authenticate and enable the full quality gate.",
18075
18352
  unauthenticated: true,
18076
18353
  static_results: staticResults
@@ -18130,6 +18407,7 @@ async function runAnalyze(opts, globals) {
18130
18407
  });
18131
18408
  }
18132
18409
  const { files: allChanged, hasRecentCommitFiles } = getChangedFiles();
18410
+ skipCoverageChanged = allChanged;
18133
18411
  const analyzable = filterAnalyzable(allChanged);
18134
18412
  const reviewable = filterReviewable(allChanged);
18135
18413
  const securityFiles = filterSecurity(allChanged);
@@ -18141,14 +18419,24 @@ async function runAnalyze(opts, globals) {
18141
18419
  const conversation = await readAndClearConversationBuffer(baselineSessionId);
18142
18420
  const specs = discoverSpecs();
18143
18421
  const plans = discoverPlans();
18422
+ const agentAuthoredCodeThisTurn = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0));
18423
+ const turnAuthoredCode = agentAuthoredCodeThisTurn || !!baseline && allForReview.some((f) => changedSinceBaseline(f, baseline));
18424
+ const canSeeTurnAuthorship = !!actionSummary || !!baseline;
18144
18425
  const latestPrompt = conversation?.prompts?.[conversation.prompts.length - 1]?.prompt ?? "";
18145
18426
  if (/^\s*\/verity-/i.test(latestPrompt)) {
18427
+ const setupAuthored = [
18428
+ ...actionSummary?.files_edited ?? [],
18429
+ ...actionSummary?.files_created ?? []
18430
+ ];
18431
+ if (setupAuthored.length > 0) {
18432
+ const adopted = absorbIntoBaseline(setupAuthored, baselineSessionId);
18433
+ logEvent("baseline_absorbed", { skip: "verity-command", offered: setupAuthored.length, adopted });
18434
+ }
18146
18435
  await passAndExit("Verity command \u2014 skipping analysis", "verity-command");
18147
18436
  }
18148
- if (isBareAckPrompt(latestPrompt)) {
18437
+ if (shouldSkipForBareAck({ prompt: latestPrompt, turnAuthoredCode, canSeeTurnAuthorship })) {
18149
18438
  await passAndExit("Bare acknowledgment \u2014 skipping analysis", "bare-acknowledgment");
18150
18439
  }
18151
- const agentAuthoredCodeThisTurn = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0));
18152
18440
  if (isReflectionQuestion(assistantResponse) && !agentAuthoredCodeThisTurn) {
18153
18441
  await passAndExit("Reflection-prompt turn \u2014 skipping analysis", "reflection-prompt");
18154
18442
  }
@@ -18195,7 +18483,11 @@ async function runAnalyze(opts, globals) {
18195
18483
  );
18196
18484
  }
18197
18485
  if (analysisMode === "skip") {
18198
- await passAndExit("Skip mode \u2014 no code work to analyze", "skip-mode");
18486
+ await passAndExit(
18487
+ "Skip mode \u2014 no code work to analyze",
18488
+ "skip-mode",
18489
+ turnAuthoredCode ? "capacity" : void 0
18490
+ );
18199
18491
  }
18200
18492
  let staticResults = {
18201
18493
  tool: "@codacy/analysis-cli",
@@ -18205,7 +18497,8 @@ async function runAnalyze(opts, globals) {
18205
18497
  let codeDelta = {
18206
18498
  files: [],
18207
18499
  total_lines: 0,
18208
- total_files: 0
18500
+ total_files: 0,
18501
+ excluded: []
18209
18502
  };
18210
18503
  let snapshotResult = { has_snapshots: false, diffs: [] };
18211
18504
  let contentHash = null;
@@ -18270,7 +18563,11 @@ async function runAnalyze(opts, globals) {
18270
18563
  if (assistantResponse) {
18271
18564
  analysisMode = "plan";
18272
18565
  } else {
18273
- await passAndExit("No files within size limits to analyze", "size-limit");
18566
+ await passAndExit(
18567
+ "No files within size limits to analyze",
18568
+ "size-limit",
18569
+ codeDelta.excluded.length > 0 ? "capacity" : "policy"
18570
+ );
18274
18571
  }
18275
18572
  }
18276
18573
  }
@@ -18283,19 +18580,13 @@ async function runAnalyze(opts, globals) {
18283
18580
  snapshotResult = generateSnapshotDiffs(codeDelta.files);
18284
18581
  }
18285
18582
  currentCommit = getCurrentCommit();
18286
- const maxIterations = parseInt(opts.maxIterations, 10);
18287
- const iterResult = checkMaxIterations(currentCommit, maxIterations, contentHash ?? void 0);
18288
- if (iterResult.skip) await passAndExit(iterResult.skip, "iteration-cap");
18289
- iteration = iterResult.iteration;
18583
+ iteration = readIterationState(currentCommit).iteration;
18290
18584
  }
18291
18585
  }
18292
18586
  if (analysisMode === "plan") {
18293
18587
  recordAnalysisStart();
18294
18588
  currentCommit = getCurrentCommit();
18295
- const maxIterations = parseInt(opts.maxIterations, 10);
18296
- const iterResult = checkMaxIterations(currentCommit, maxIterations);
18297
- if (iterResult.skip) await passAndExit(iterResult.skip, "iteration-cap");
18298
- iteration = iterResult.iteration;
18589
+ iteration = readIterationState(currentCommit).iteration;
18299
18590
  }
18300
18591
  const contextFiles = gatherContextFiles(contextFilePaths, codeDelta.files);
18301
18592
  for (const f of codeDelta.files) {
@@ -18444,7 +18735,10 @@ async function runAnalyze(opts, globals) {
18444
18735
  priorState.capabilities
18445
18736
  );
18446
18737
  memory = recallMemory(memorySession.d, memorySession.identity, {
18447
- currentSessionKey: memorySession.identity.sessionKey
18738
+ currentSessionKey: memorySession.identity.sessionKey,
18739
+ // The independent witness. Only meaningful when a transcript was folded —
18740
+ // otherwise it stays undefined and capture coverage reads as UNKNOWN.
18741
+ ...foldResult && { userMessagesSeen: foldResult.coverage.userMessages }
18448
18742
  });
18449
18743
  if (memory && !memory.provenanceHolds) {
18450
18744
  process.stderr.write("Verity: working-memory provenance check failed \u2014 recall suppressed.\n");
@@ -18465,7 +18759,30 @@ async function runAnalyze(opts, globals) {
18465
18759
  hasUserPrompt: (conversation?.prompts?.length ?? 0) > 0,
18466
18760
  isTTY: process.stdout.isTTY === true
18467
18761
  });
18762
+ const excludedByReason = {};
18763
+ for (const e of codeDelta.excluded ?? []) {
18764
+ excludedByReason[e.reason] = (excludedByReason[e.reason] ?? 0) + 1;
18765
+ }
18766
+ const coverageTelemetry = {
18767
+ // git's whole answer, before ANY narrowing. The number that has never been sent.
18768
+ changed_all: allChanged.length,
18769
+ analyzable: analyzable.length,
18770
+ reviewable: reviewable.length,
18771
+ security: securityFiles.length,
18772
+ // after the allowlist, before authorship scoping and the caps
18773
+ for_review: allForReview.length,
18774
+ // what actually reaches the reviewer
18775
+ sent: codeDelta.files.length,
18776
+ // the two silent narrowings, counted separately so they can be told apart
18777
+ capped_out: actionSummary?.capped_out?.length ?? 0,
18778
+ excluded: (codeDelta.excluded ?? []).length,
18779
+ excluded_by_reason: excludedByReason,
18780
+ // was the transcript itself truncated? The 256 KB window means "this turn"
18781
+ // can quietly mean "the last 256 KB of it".
18782
+ transcript_windowed: actionSummary?.transcript_windowed ?? null
18783
+ };
18468
18784
  const requestBody = {
18785
+ coverage_telemetry: coverageTelemetry,
18469
18786
  static_results: staticResults,
18470
18787
  code_delta: codeDelta,
18471
18788
  changed_files: allForReview,
@@ -18551,6 +18868,54 @@ async function runAnalyze(opts, globals) {
18551
18868
  // replaced a population floor with ≈35% power that was sub-integer for
18552
18869
  // three-quarters of the fleet.
18553
18870
  conservation: foldConservation,
18871
+ // ⚠ VRT-52 — RECORDED, NOT APPLIED. The number nobody has.
18872
+ //
18873
+ // The whole "task-scoped delta" design space rests on an assumption that
18874
+ // has been observed exactly ONCE: that delta files routinely belong to
18875
+ // earlier work. Three designs were built on it and all three were killed
18876
+ // adversarially — two by measurement — so before another is attempted,
18877
+ // measure the base rate.
18878
+ //
18879
+ // `authored_under_earlier_goal` counts delta paths whose LAST authorship
18880
+ // event precedes the seq of the goal now in force. Both numbers come from
18881
+ // the same append-only counter (`nextSeq`), so the comparison is exact.
18882
+ //
18883
+ // Keyed on the GOAL, deliberately, not on the task id. The task classifier
18884
+ // reported `is_new_task` on two consecutive turns of one task 25 seconds
18885
+ // apart, so a task-keyed number would measure its unreliability rather
18886
+ // than the phenomenon. And this only became meaningful once `recordGoal`
18887
+ // stopped letting a bare "ok" supersede the goal — before that the seq
18888
+ // advanced every turn and this would have degenerated to "not edited this
18889
+ // turn", which is the exact mistake that sank one of the three designs.
18890
+ //
18891
+ // Changes no payload the reviewer sees, no narrowing, no verdict.
18892
+ vrt52: (() => {
18893
+ const goalSeq = memory?.projection.goal?.seq;
18894
+ if (goalSeq === void 0 || !memorySession) return { known: false };
18895
+ const lastSeq2 = new Map(
18896
+ foldDossier(memorySession.d).authored_all.map((a) => [a.path, a.last_seq])
18897
+ );
18898
+ let earlier = 0;
18899
+ let unknown = 0;
18900
+ for (const f of codeDelta.files) {
18901
+ const seen = lastSeq2.get(f.path);
18902
+ if (seen === void 0) unknown++;
18903
+ else if (seen < goalSeq) earlier++;
18904
+ }
18905
+ return {
18906
+ known: true,
18907
+ goal_seq: goalSeq,
18908
+ delta: codeDelta.files.length,
18909
+ // Files this delta carries that were last written under an EARLIER
18910
+ // instruction. If this stays near zero, VRT-52's code half is
18911
+ // unnecessary and should be closed saying so.
18912
+ authored_under_earlier_goal: earlier,
18913
+ // Delta files the dossier has no authorship record for at all —
18914
+ // pre-existing tree state, or an authorship channel the fold cannot
18915
+ // see. Reported separately so a blind spot is never counted as a zero.
18916
+ no_authorship_record: unknown
18917
+ };
18918
+ })(),
18554
18919
  // P2 — RECORDED, NOT APPLIED. What this run WOULD have reviewed if it
18555
18920
  // narrowed to the within-session increment: what changed since the last
18556
18921
  // VERDICT rather than since task start.
@@ -18583,7 +18948,20 @@ async function runAnalyze(opts, globals) {
18583
18948
  const intentContext = {};
18584
18949
  if (conversation && conversation.prompts.length > 0) {
18585
18950
  const latest = conversation.prompts[conversation.prompts.length - 1];
18586
- intentContext.user_prompt = latest.prompt;
18951
+ const goalPrompt = resolveGoalPrompt(conversation.prompts) ?? { entry: latest, turnsBack: 0 };
18952
+ intentContext.user_prompt = goalPrompt.entry.prompt;
18953
+ if (isContinuationPrompt(intentContext.user_prompt)) {
18954
+ const carried = memory?.projection.goal?.text;
18955
+ if (carried && !isContinuationPrompt(carried)) {
18956
+ intentContext.continuation_prompt = latest.prompt;
18957
+ intentContext.user_prompt = carried;
18958
+ logEvent("goal_from_dossier", { chars: carried.length });
18959
+ }
18960
+ }
18961
+ if (goalPrompt.turnsBack > 0) {
18962
+ intentContext.continuation_prompt = latest.prompt;
18963
+ logEvent("goal_walked_back", { turns_back: goalPrompt.turnsBack });
18964
+ }
18587
18965
  intentContext.session_id = latest.session_id || void 0;
18588
18966
  intentContext.prompt_captured_at = latest.captured_at || void 0;
18589
18967
  if (conversation.prompts.length > 1) {
@@ -18675,8 +19053,114 @@ async function runAnalyze(opts, globals) {
18675
19053
  const response = result.data;
18676
19054
  const decision = response.gate_decision ?? "(unrecognised)";
18677
19055
  const sentPaths = codeDelta.files.map((f) => f.path);
19056
+ let openElsewhere = [];
19057
+ if (memorySession) {
19058
+ try {
19059
+ const st = foldDossier(memorySession.d);
19060
+ openElsewhere = openBlockingElsewhere(st.statements, sentPaths, (file, line) => {
19061
+ try {
19062
+ const src = (0, import_node_fs24.readFileSync)((0, import_node_path20.join)(repoRoot(), file), "utf8").split("\n");
19063
+ const at = src[line - 1];
19064
+ return at === void 0 ? null : lineSha(at);
19065
+ } catch {
19066
+ return null;
19067
+ }
19068
+ });
19069
+ } catch {
19070
+ }
19071
+ }
19072
+ const reviewCoverage = {
19073
+ reviewed: sentPaths,
19074
+ // Declared drops from the stages that DO report themselves today. The other
19075
+ // stages surface via `unaccounted`, which is the tripwire, not the design.
19076
+ notReviewed: [
19077
+ // Every exit from the collection loop, each named. Six reasons where there
19078
+ // used to be two recorded and four silent — the silent ones including the
19079
+ // per-file size cap, which could drop a whole source file without leaving a
19080
+ // trace anywhere in the payload or the run row.
19081
+ ...codeDelta.excluded,
19082
+ // The server-side 300-line middle-out truncation. It only bites on the
19083
+ // full-file branch (a first analysis, before snapshots exist) because
19084
+ // analyze normally sends diffs — but on that branch the reviewer sees the
19085
+ // first and last 100 lines and nothing between, and until now said so to
19086
+ // nobody. CAPACITY: a partial look is not a look.
19087
+ ...(response.metadata?.truncated_files ?? []).map((path) => ({
19088
+ path,
19089
+ reason: "file-middle-truncated-300-lines",
19090
+ stage: "prompt-builder",
19091
+ kind: "capacity"
19092
+ })),
19093
+ // The 20-entry edit cap. CAPACITY, and the sharpest of the lot: it narrows
19094
+ // what is REVIEWED, not merely what is summarised — a session editing 25
19095
+ // files had five silently excluded from the reviewed set.
19096
+ ...(actionSummary?.capped_out ?? []).map((path) => ({
19097
+ path,
19098
+ reason: "edit-list-cap-20",
19099
+ stage: "extractActionSummary",
19100
+ kind: "capacity"
19101
+ })),
19102
+ // ⚠ BASELINE SCOPING — the biggest source of false NOT A CLEAN REVIEW.
19103
+ //
19104
+ // The universe is `allChanged`, git's whole dirty tree. The reviewed set is
19105
+ // scoped to what THIS SESSION authored (the VRT-26 contamination cure), so
19106
+ // every pre-existing dirty file is in the universe, absent from `reviewed`,
19107
+ // and — until now — declared by nobody. It fell through to `unaccounted`,
19108
+ // became capacity, and produced "NOT A CLEAN REVIEW: admin.js" over a file
19109
+ // that was never this session's to review.
19110
+ //
19111
+ // Measured 2026-08-04: three consecutive runs over an untouched tree gave
19112
+ // three different answers — .claude/settings.json, then admin.js, then six
19113
+ // files — because each run took a different path and each path had a
19114
+ // different idea of the universe. POLICY: not this session's work is not a
19115
+ // coverage gap, it is the cure working.
19116
+ ...allChanged.filter((p) => !sentPaths.includes(p) && !codeDelta.excluded.some((e) => e.path === p)).filter((p) => analyzable.includes(p) || reviewable.includes(p) || securityFiles.includes(p)).map((path) => ({
19117
+ path,
19118
+ reason: "not-authored-this-session",
19119
+ stage: "baseline-scoping",
19120
+ kind: "policy"
19121
+ })),
19122
+ // The extension allowlist, and it is POLICY rather than capacity: a changed
19123
+ // README was never going to be reviewed, and treating that as a coverage
19124
+ // gap would downgrade nearly every PASS to WARN until WARN meant nothing.
19125
+ // Recorded so the ledger balances and so "what did Verity ignore entirely"
19126
+ // is answerable — but it never touches the verdict.
19127
+ ...allChanged.filter((p) => !analyzable.includes(p) && !reviewable.includes(p) && !securityFiles.includes(p)).map((path) => ({
19128
+ path,
19129
+ reason: "not-a-reviewed-file-type",
19130
+ stage: "extension-allowlist",
19131
+ kind: "policy"
19132
+ }))
19133
+ ]
19134
+ };
18678
19135
  const watermarkHash = sentPaths.length > 0 ? computeContentHash(sentPaths) : contentHash;
18679
19136
  const watermarkIsPartial = !!codeDelta.truncated;
19137
+ let silenced = null;
19138
+ let turnIsIdleForChannel = true;
19139
+ if (memorySession) {
19140
+ try {
19141
+ const st = foldDossier(memorySession.d);
19142
+ turnIsIdleForChannel = st.authored_all.every((a) => a.hash_now === a.hash_at_last_verdict);
19143
+ silenced = channelSilence({
19144
+ // The BUFFER, not intentContext.user_prompt: the latter falls back to a
19145
+ // linked issue (VRT-53 W4) when no human spoke, and a fallback goal is
19146
+ // not a user utterance. Treating it as one would keep the loop alive on
19147
+ // exactly the autonomous cohort.
19148
+ newUserPrompt: (conversation?.prompts?.length ?? 0) > 0,
19149
+ newAuthorship: !turnIsIdleForChannel,
19150
+ emittedLast: st.meta.channel?.emittedLast === true,
19151
+ consecutiveIdle: st.meta.channel?.consecutiveIdle ?? 0
19152
+ });
19153
+ } catch {
19154
+ silenced = null;
19155
+ }
19156
+ }
19157
+ if (silenced) {
19158
+ logEvent("channel_silenced", {
19159
+ reason: silenced,
19160
+ run_id: response.run_id ?? turnId,
19161
+ decision
19162
+ });
19163
+ }
18680
19164
  let intentRepeatCount = 0;
18681
19165
  if (memorySession) {
18682
19166
  try {
@@ -18696,7 +19180,11 @@ async function runAnalyze(opts, globals) {
18696
19180
  // The same signal F1 introduced: bytes differing from the hash frozen at
18697
19181
  // the last verdict. A turn that moved nothing is the only kind that can
18698
19182
  // accumulate a repeat.
18699
- idle: foldDossier(memorySession.d).authored_all.every((a) => a.hash_now === a.hash_at_last_verdict)
19183
+ idle: turnIsIdleForChannel,
19184
+ // What next turn reads as `emittedLast`. A suppressed turn did not
19185
+ // speak, so it cannot be the cause of the turn after it — which is what
19186
+ // keeps this from becoming a permanent gag.
19187
+ emitted: !silenced
18700
19188
  });
18701
19189
  intentRepeatCount = Math.max(0, (foldDossier(memorySession.d).meta.intent_repeat?.consecutive ?? 1) - 1);
18702
19190
  } catch {
@@ -18796,9 +19284,41 @@ async function runAnalyze(opts, globals) {
18796
19284
  reverify_by: response.reverify_by
18797
19285
  });
18798
19286
  const grantNudge = grantWarning ? ` \u26A0\uFE0F ${grantWarning}` : "";
18799
- switch (decision) {
19287
+ let capReleased = false;
19288
+ let effectiveDecision = decision;
19289
+ if (decision === "FAIL") {
19290
+ const blocking = (response.findings ?? []).filter((f) => {
19291
+ const sev = String(f.severity ?? "").toLowerCase();
19292
+ return sev === "critical" || sev === "high";
19293
+ });
19294
+ const fingerprint = findingsFingerprint(blocking);
19295
+ const prior = readIterationState(currentCommit);
19296
+ const sameProblem = isSameProblem(prior.fingerprint, fingerprint);
19297
+ const nextIteration = sameProblem ? prior.iteration + 1 : 1;
19298
+ const maxIterations = parseInt(opts.maxIterations, 10);
19299
+ writeIteration(nextIteration, currentCommit, contentHash ?? void 0, fingerprint);
19300
+ iteration = nextIteration;
19301
+ if (nextIteration > maxIterations) {
19302
+ capReleased = true;
19303
+ effectiveDecision = "WARN";
19304
+ logEvent("iteration_cap_released", { iteration: nextIteration, fingerprint });
19305
+ }
19306
+ }
19307
+ if (capReleased) {
19308
+ const findings = response.findings ?? [];
19309
+ const lines = findings.slice(0, 5).map((f) => ` [${String(f.severity ?? "?").toUpperCase()}] ${String(f.title ?? f.message ?? "")} (${String(f.file ?? "?")}:${String(f.line ?? "?")})`);
19310
+ emitVerdict({
19311
+ proposed: "WARN",
19312
+ changed: skipCoverageChanged,
19313
+ coverage: reviewCoverage,
19314
+ userSummary: `Verity: WARN \u2014 self-healing limit (${opts.maxIterations}) reached on the same finding. NO LONGER BLOCKING, but ${findings.length} finding(s) remain OPEN and were NOT fixed. Human review required before deploying.
19315
+ ${lines.join("\n")}`,
19316
+ agentContext: null,
19317
+ silenced: true
19318
+ });
19319
+ }
19320
+ switch (effectiveDecision) {
18800
19321
  case "FAIL": {
18801
- writeIteration(iteration + 1, currentCommit, contentHash ?? void 0);
18802
19322
  const assessment = response.assessment;
18803
19323
  const narrative = assessment?.narrative ?? "";
18804
19324
  const findings = response.findings ?? [];
@@ -18875,7 +19395,19 @@ ${YELLOW}${loginNudge.trim()}${NC}
18875
19395
  if (grantNudge) process.stderr.write(`
18876
19396
  ${YELLOW}${grantNudge.trim()}${NC}
18877
19397
  `);
18878
- process.exit(2);
19398
+ emitVerdict({
19399
+ proposed: "FAIL",
19400
+ changed: skipCoverageChanged,
19401
+ coverage: reviewCoverage,
19402
+ userSummary: "",
19403
+ // Subject to the SAME cycle cut as PASS/WARN. Suppressing here is safe:
19404
+ // the findings themselves are rendered above by the blocking renderer,
19405
+ // so what the cut removes is the repeated commentary, never the defect.
19406
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount),
19407
+ // The coverage note is silenced with it — half a channel is still a channel.
19408
+ silenced: !!silenced,
19409
+ openElsewhere
19410
+ });
18879
19411
  break;
18880
19412
  }
18881
19413
  case "PASS": {
@@ -18887,10 +19419,16 @@ ${YELLOW}${grantNudge.trim()}${NC}
18887
19419
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
18888
19420
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
18889
19421
  userSummary += loginNudge + grantNudge;
18890
- printJsonCompact(
18891
- buildHookOutput("PASS", userSummary, agentContextFor(response, intentRepeatCount))
18892
- );
18893
- process.exit(0);
19422
+ emitVerdict({
19423
+ proposed: "PASS",
19424
+ changed: skipCoverageChanged,
19425
+ coverage: reviewCoverage,
19426
+ userSummary,
19427
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount),
19428
+ // The coverage note is silenced with it — half a channel is still a channel.
19429
+ silenced: !!silenced,
19430
+ openElsewhere
19431
+ });
18894
19432
  break;
18895
19433
  }
18896
19434
  case "WARN": {
@@ -18901,10 +19439,16 @@ ${YELLOW}${grantNudge.trim()}${NC}
18901
19439
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
18902
19440
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
18903
19441
  userSummary += loginNudge + grantNudge;
18904
- printJsonCompact(
18905
- buildHookOutput("WARN", userSummary, agentContextFor(response, intentRepeatCount))
18906
- );
18907
- process.exit(0);
19442
+ emitVerdict({
19443
+ proposed: "WARN",
19444
+ changed: skipCoverageChanged,
19445
+ coverage: reviewCoverage,
19446
+ userSummary,
19447
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount),
19448
+ // The coverage note is silenced with it — half a channel is still a channel.
19449
+ silenced: !!silenced,
19450
+ openElsewhere
19451
+ });
18908
19452
  break;
18909
19453
  }
18910
19454
  default: {
@@ -19017,8 +19561,8 @@ async function runReview(opts, globals) {
19017
19561
  for (const p of specPaths) {
19018
19562
  if (!(0, import_node_fs26.existsSync)(p)) continue;
19019
19563
  try {
19020
- const { readFileSync: readFileSync15 } = await import("node:fs");
19021
- const content = readFileSync15(p, "utf-8");
19564
+ const { readFileSync: readFileSync16 } = await import("node:fs");
19565
+ const content = readFileSync16(p, "utf-8");
19022
19566
  specs.push({ path: p, content: content.slice(0, 10240) });
19023
19567
  } catch {
19024
19568
  }
@@ -20704,7 +21248,7 @@ function registerTelemetryCommands(program2) {
20704
21248
  }
20705
21249
 
20706
21250
  // src/cli.ts
20707
- program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.4da2503").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async () => {
21251
+ program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.6eac6aa").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async () => {
20708
21252
  try {
20709
21253
  await foldLegacyLocalCredential();
20710
21254
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codacy/verity-cli",
3
- "version": "0.28.1-experimental.4da2503",
3
+ "version": "0.28.1-experimental.6eac6aa",
4
4
  "description": "CLI for Verity quality gate service",
5
5
  "homepage": "https://verity.md",
6
6
  "bugs": {