@codacy/verity-cli 0.28.1-experimental.055cd97 → 0.28.1-experimental.4410c26

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 +575 -99
  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");
@@ -14695,7 +14758,19 @@ function sessionDossier(token, sessionId) {
14695
14758
  const d = openDossier(identity);
14696
14759
  return d ? { d, identity } : null;
14697
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
+ }
14698
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
+ }
14699
14774
  const text = prompt.slice(0, MAX_GOAL_CHARS);
14700
14775
  appendEvent(d, {
14701
14776
  k: "goal",
@@ -15118,6 +15193,8 @@ function collectCodeDelta(files, opts) {
15118
15193
  let totalSize = 0;
15119
15194
  let truncationReason = null;
15120
15195
  const droppedPaths = [];
15196
+ const excluded = [];
15197
+ const exclude = (path, reason) => excluded.push({ path, reason, stage: "collectCodeDelta", kind: "capacity" });
15121
15198
  for (const filepath of sorted) {
15122
15199
  if (result.length >= maxFiles) {
15123
15200
  truncationReason ??= "max_files";
@@ -15125,14 +15202,21 @@ function collectCodeDelta(files, opts) {
15125
15202
  continue;
15126
15203
  }
15127
15204
  const resolved = resolveFile(filepath);
15128
- if (!resolved) continue;
15205
+ if (!resolved) {
15206
+ exclude(filepath, "path-not-resolvable");
15207
+ continue;
15208
+ }
15129
15209
  let size;
15130
15210
  try {
15131
15211
  size = (0, import_node_fs11.statSync)(resolved).size;
15132
15212
  } catch {
15213
+ exclude(filepath, "not-stattable");
15214
+ continue;
15215
+ }
15216
+ if (size > maxFileBytes) {
15217
+ exclude(filepath, `over-file-size-limit-${maxFileBytes}b`);
15133
15218
  continue;
15134
15219
  }
15135
- if (size > maxFileBytes) continue;
15136
15220
  if (totalSize + size > maxTotalBytes) {
15137
15221
  truncationReason ??= "max_total_bytes";
15138
15222
  const idx = sorted.indexOf(filepath);
@@ -15143,6 +15227,7 @@ function collectCodeDelta(files, opts) {
15143
15227
  try {
15144
15228
  content = (0, import_node_fs11.readFileSync)(resolved, "utf-8");
15145
15229
  } catch {
15230
+ exclude(filepath, "not-readable");
15146
15231
  continue;
15147
15232
  }
15148
15233
  totalSize += size;
@@ -15156,10 +15241,14 @@ function collectCodeDelta(files, opts) {
15156
15241
  (sum, f) => sum + f.content.split("\n").length,
15157
15242
  0
15158
15243
  );
15244
+ for (const path of droppedPaths) {
15245
+ exclude(path, truncationReason === "max_files" ? "max-files-cap" : "max-total-bytes-cap");
15246
+ }
15159
15247
  return {
15160
15248
  files: result,
15161
15249
  total_lines: totalLines,
15162
15250
  total_files: result.length,
15251
+ excluded,
15163
15252
  ...truncationReason && {
15164
15253
  truncated: {
15165
15254
  reason: truncationReason,
@@ -15453,6 +15542,34 @@ ${addedLines}`,
15453
15542
  }
15454
15543
  return { diffs, has_baseline: true };
15455
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
+ }
15456
15573
  function changedSinceBaseline(repoRelPath, baseline) {
15457
15574
  const pre = preImage(repoRelPath, baseline);
15458
15575
  let current;
@@ -16329,40 +16446,40 @@ function narrowToRecent(files, sessionId) {
16329
16446
  });
16330
16447
  return recent.length > 0 ? recent : files;
16331
16448
  }
16332
- function readIteration(currentCommit, _contentHash) {
16333
- 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 };
16334
16451
  try {
16335
16452
  const stored = (0, import_node_fs15.readFileSync)(ITERATION_FILE, "utf-8").trim();
16336
16453
  const parts = stored.split(":");
16337
16454
  const iter = parseInt(parts[0], 10);
16338
16455
  const storedCommit = parts[1] ?? "";
16339
16456
  const storedTimestamp = parseInt(parts[2] ?? "0", 10);
16340
- if (isNaN(iter)) return 1;
16341
- 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 };
16342
16460
  if (storedTimestamp > 0) {
16343
16461
  const elapsed = Math.floor(Date.now() / 1e3) - storedTimestamp;
16344
- if (elapsed > 600) return 1;
16462
+ if (elapsed > 600) return { iteration: 1, fingerprint: null };
16345
16463
  }
16346
- return iter;
16464
+ return { iteration: iter, fingerprint };
16347
16465
  } catch {
16348
- return 1;
16466
+ return { iteration: 1, fingerprint: null };
16349
16467
  }
16350
16468
  }
16351
- function checkMaxIterations(currentCommit, maxIterations = MAX_ITERATIONS, contentHash) {
16352
- const iteration = readIteration(currentCommit, contentHash);
16353
- if (iteration > maxIterations) {
16354
- writeIteration(1, currentCommit, contentHash);
16355
- return {
16356
- skip: `Max Verity iterations (${maxIterations}) reached \u2014 accepting to prevent infinite loop. Human review required before deploying.`,
16357
- iteration
16358
- };
16359
- }
16360
- 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(",");
16361
16472
  }
16362
- 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) {
16363
16479
  (0, import_node_fs15.mkdirSync)(VERITY_DIR, { recursive: true });
16364
16480
  const ts = Math.floor(Date.now() / 1e3);
16365
- (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}`);
16366
16483
  }
16367
16484
 
16368
16485
  // src/lib/static-analysis.ts
@@ -16611,7 +16728,7 @@ function resolveTaskContext(opts) {
16611
16728
  // src/lib/cli-version.ts
16612
16729
  function cliVersion() {
16613
16730
  try {
16614
- return true ? "0.28.1-experimental.055cd97" : "dev";
16731
+ return true ? "0.28.1-experimental.4410c26" : "dev";
16615
16732
  } catch {
16616
16733
  return "dev";
16617
16734
  }
@@ -17156,6 +17273,86 @@ function checkConservation(changedFiles, result, repoRoot2) {
17156
17273
  };
17157
17274
  }
17158
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
+
17159
17356
  // src/lib/channel.ts
17160
17357
  var MAX_AGENT_CONTEXT_CHARS = 1500;
17161
17358
  var MAX_AGENT_ITEMS = 5;
@@ -17205,6 +17402,7 @@ function buildAgentContext(input) {
17205
17402
  }
17206
17403
  for (const p of input.pendingItems ?? []) {
17207
17404
  if (lines.length >= MAX_AGENT_ITEMS) break;
17405
+ if (p.pattern_id === "intent-misalignment") continue;
17208
17406
  const text = p.description ?? p.title ?? p.reason;
17209
17407
  if (!text) continue;
17210
17408
  lines.push(renderItem("", text, p.pattern_id, p.file, p.line));
@@ -17243,6 +17441,39 @@ function channelSilence(input) {
17243
17441
  return null;
17244
17442
  }
17245
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
+ }
17476
+
17246
17477
  // src/lib/cache-cleanup.ts
17247
17478
  var import_node_fs21 = require("node:fs");
17248
17479
  var import_node_path18 = require("node:path");
@@ -17422,46 +17653,6 @@ function shouldWarmRetryAnalyze(result) {
17422
17653
  return false;
17423
17654
  }
17424
17655
 
17425
- // src/lib/skip-detection.ts
17426
- function isBareAckPrompt(prompt) {
17427
- if (typeof prompt !== "string") return false;
17428
- const trimmed = prompt.trim();
17429
- if (trimmed.length === 0) return false;
17430
- if (trimmed.length > 20) return false;
17431
- 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;
17432
- return bareAckPattern.test(trimmed);
17433
- }
17434
- function isReflectionQuestion(response) {
17435
- if (!response || typeof response !== "string") return false;
17436
- const markers = [
17437
- /reflection\s+for\s+future\s+agents/i,
17438
- /what(?:'s|\s+is)\s+one\s+thing\s+you\s+learned/i,
17439
- /say\s+['"]?skip['"]?\s+to\s+skip/i,
17440
- /quick\s+reflection\s+question/i,
17441
- // Post-flip (VRT-21): the agent drafts the reflection itself and, when
17442
- // interactive, asks the user to confirm/correct before recording. That
17443
- // turn authors no code either, so it's still a reflection turn.
17444
- /reflection\s+draft/i,
17445
- /confirm,?\s+correct,?\s+or\s+add/i
17446
- ];
17447
- return markers.some((m) => m.test(response));
17448
- }
17449
- function isMetaTaskLabel(label2) {
17450
- if (label2 === null || label2 === void 0) return false;
17451
- if (typeof label2 !== "string") return false;
17452
- const trimmed = label2.trim();
17453
- if (trimmed.length === 0) return true;
17454
- const metaPatterns = [
17455
- /^verity\s+[\w-]+\s+response$/i,
17456
- // "Verity reflect response"
17457
- /^simple user response$/i,
17458
- /^verity\s+command$/i,
17459
- // "Verity command"
17460
- /^user\s+(question|reply|response|ack)$/i
17461
- ];
17462
- return metaPatterns.some((p) => p.test(trimmed));
17463
- }
17464
-
17465
17656
  // src/lib/transcript.ts
17466
17657
  var import_node_fs22 = require("node:fs");
17467
17658
  var MAX_READ_BYTES = 256 * 1024;
@@ -17475,9 +17666,11 @@ var MAX_SUMMARY_BYTES = 4096;
17475
17666
  var HOME = process.env.HOME ?? "";
17476
17667
  async function extractActionSummary(transcriptPath) {
17477
17668
  try {
17478
- const lines = readTurnLines(transcriptPath);
17479
- if (!lines || lines.length === 0) return null;
17480
- 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;
17481
17674
  } catch {
17482
17675
  return null;
17483
17676
  }
@@ -17491,9 +17684,11 @@ function readTurnLines(transcriptPath) {
17491
17684
  }
17492
17685
  if (size === 0) return null;
17493
17686
  let raw;
17687
+ let windowed = false;
17494
17688
  if (size <= SMALL_FILE_BYTES) {
17495
17689
  raw = (0, import_node_fs22.readFileSync)(transcriptPath, "utf-8");
17496
17690
  } else {
17691
+ windowed = true;
17497
17692
  const buf = Buffer.alloc(Math.min(MAX_READ_BYTES, size));
17498
17693
  const fd = require("node:fs").openSync(transcriptPath, "r");
17499
17694
  try {
@@ -17511,17 +17706,22 @@ function readTurnLines(transcriptPath) {
17511
17706
  const allLines = raw.split("\n").filter((l) => l.trim().length > 0);
17512
17707
  if (allLines.length === 0) return null;
17513
17708
  let turnStart = 0;
17709
+ let boundaryFound = false;
17514
17710
  for (let i = allLines.length - 1; i >= 0; i--) {
17515
17711
  try {
17516
17712
  const parsed = JSON.parse(allLines[i]);
17517
17713
  if (parsed.type === "user" && isRealUserMessage(parsed)) {
17518
17714
  turnStart = i;
17715
+ boundaryFound = true;
17519
17716
  break;
17520
17717
  }
17521
17718
  } catch {
17522
17719
  }
17523
17720
  }
17524
- return allLines.slice(turnStart);
17721
+ return {
17722
+ lines: allLines.slice(turnStart),
17723
+ window: !windowed ? "whole" : boundaryFound ? "windowed" : "orphaned"
17724
+ };
17525
17725
  }
17526
17726
  function isRealUserMessage(parsed) {
17527
17727
  const message = parsed.message;
@@ -17625,6 +17825,13 @@ function buildSummary(lines) {
17625
17825
  files_read: capArray(filesRead, MAX_FILES_LIST),
17626
17826
  files_edited: capArray(filesEdited, MAX_FILES_LIST),
17627
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
+ ],
17628
17835
  searches,
17629
17836
  commands,
17630
17837
  subagents,
@@ -17675,6 +17882,9 @@ function sanitizeCommand(rawCmd) {
17675
17882
  function capArray(set, max) {
17676
17883
  return Array.from(set).slice(0, max);
17677
17884
  }
17885
+ function cappedOut(set, max) {
17886
+ return Array.from(set).slice(max);
17887
+ }
17678
17888
 
17679
17889
  // src/lib/run-mode.ts
17680
17890
  function parseAutonomousEnv(raw) {
@@ -18084,12 +18294,45 @@ function agentContextFor(response, intentRepeat = 0) {
18084
18294
  });
18085
18295
  }
18086
18296
  var beaconCtx = null;
18087
- async function passAndExit(reason, skip) {
18297
+ async function passAndExit(reason, skip, kindOverride) {
18088
18298
  const sent = await sendSkipBeacon(beaconCtx, skip);
18089
18299
  logEvent("skip", { reason: skip, beacon: sent });
18090
- 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
+ );
18091
18333
  process.exit(0);
18092
18334
  }
18335
+ var skipCoverageChanged = [];
18093
18336
  var EMPTY_STATIC = {
18094
18337
  tool: "@codacy/analysis-cli",
18095
18338
  findings: [],
@@ -18104,7 +18347,7 @@ function runLocalStatic(analyzable, securityFiles, baseline, skipStatic) {
18104
18347
  }
18105
18348
  function localOnlyAndExit(staticResults) {
18106
18349
  printJsonCompact({
18107
- gate_decision: "PASS",
18350
+ gate_decision: "WARN",
18108
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.",
18109
18352
  unauthenticated: true,
18110
18353
  static_results: staticResults
@@ -18164,6 +18407,7 @@ async function runAnalyze(opts, globals) {
18164
18407
  });
18165
18408
  }
18166
18409
  const { files: allChanged, hasRecentCommitFiles } = getChangedFiles();
18410
+ skipCoverageChanged = allChanged;
18167
18411
  const analyzable = filterAnalyzable(allChanged);
18168
18412
  const reviewable = filterReviewable(allChanged);
18169
18413
  const securityFiles = filterSecurity(allChanged);
@@ -18175,14 +18419,24 @@ async function runAnalyze(opts, globals) {
18175
18419
  const conversation = await readAndClearConversationBuffer(baselineSessionId);
18176
18420
  const specs = discoverSpecs();
18177
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;
18178
18425
  const latestPrompt = conversation?.prompts?.[conversation.prompts.length - 1]?.prompt ?? "";
18179
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
+ }
18180
18435
  await passAndExit("Verity command \u2014 skipping analysis", "verity-command");
18181
18436
  }
18182
- if (isBareAckPrompt(latestPrompt)) {
18437
+ if (shouldSkipForBareAck({ prompt: latestPrompt, turnAuthoredCode, canSeeTurnAuthorship })) {
18183
18438
  await passAndExit("Bare acknowledgment \u2014 skipping analysis", "bare-acknowledgment");
18184
18439
  }
18185
- const agentAuthoredCodeThisTurn = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0));
18186
18440
  if (isReflectionQuestion(assistantResponse) && !agentAuthoredCodeThisTurn) {
18187
18441
  await passAndExit("Reflection-prompt turn \u2014 skipping analysis", "reflection-prompt");
18188
18442
  }
@@ -18229,7 +18483,11 @@ async function runAnalyze(opts, globals) {
18229
18483
  );
18230
18484
  }
18231
18485
  if (analysisMode === "skip") {
18232
- 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
+ );
18233
18491
  }
18234
18492
  let staticResults = {
18235
18493
  tool: "@codacy/analysis-cli",
@@ -18239,7 +18497,8 @@ async function runAnalyze(opts, globals) {
18239
18497
  let codeDelta = {
18240
18498
  files: [],
18241
18499
  total_lines: 0,
18242
- total_files: 0
18500
+ total_files: 0,
18501
+ excluded: []
18243
18502
  };
18244
18503
  let snapshotResult = { has_snapshots: false, diffs: [] };
18245
18504
  let contentHash = null;
@@ -18304,7 +18563,11 @@ async function runAnalyze(opts, globals) {
18304
18563
  if (assistantResponse) {
18305
18564
  analysisMode = "plan";
18306
18565
  } else {
18307
- 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
+ );
18308
18571
  }
18309
18572
  }
18310
18573
  }
@@ -18317,19 +18580,13 @@ async function runAnalyze(opts, globals) {
18317
18580
  snapshotResult = generateSnapshotDiffs(codeDelta.files);
18318
18581
  }
18319
18582
  currentCommit = getCurrentCommit();
18320
- const maxIterations = parseInt(opts.maxIterations, 10);
18321
- const iterResult = checkMaxIterations(currentCommit, maxIterations, contentHash ?? void 0);
18322
- if (iterResult.skip) await passAndExit(iterResult.skip, "iteration-cap");
18323
- iteration = iterResult.iteration;
18583
+ iteration = readIterationState(currentCommit).iteration;
18324
18584
  }
18325
18585
  }
18326
18586
  if (analysisMode === "plan") {
18327
18587
  recordAnalysisStart();
18328
18588
  currentCommit = getCurrentCommit();
18329
- const maxIterations = parseInt(opts.maxIterations, 10);
18330
- const iterResult = checkMaxIterations(currentCommit, maxIterations);
18331
- if (iterResult.skip) await passAndExit(iterResult.skip, "iteration-cap");
18332
- iteration = iterResult.iteration;
18589
+ iteration = readIterationState(currentCommit).iteration;
18333
18590
  }
18334
18591
  const contextFiles = gatherContextFiles(contextFilePaths, codeDelta.files);
18335
18592
  for (const f of codeDelta.files) {
@@ -18502,7 +18759,30 @@ async function runAnalyze(opts, globals) {
18502
18759
  hasUserPrompt: (conversation?.prompts?.length ?? 0) > 0,
18503
18760
  isTTY: process.stdout.isTTY === true
18504
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
+ };
18505
18784
  const requestBody = {
18785
+ coverage_telemetry: coverageTelemetry,
18506
18786
  static_results: staticResults,
18507
18787
  code_delta: codeDelta,
18508
18788
  changed_files: allForReview,
@@ -18588,6 +18868,54 @@ async function runAnalyze(opts, globals) {
18588
18868
  // replaced a population floor with ≈35% power that was sub-integer for
18589
18869
  // three-quarters of the fleet.
18590
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
+ })(),
18591
18919
  // P2 — RECORDED, NOT APPLIED. What this run WOULD have reviewed if it
18592
18920
  // narrowed to the within-session increment: what changed since the last
18593
18921
  // VERDICT rather than since task start.
@@ -18620,7 +18948,20 @@ async function runAnalyze(opts, globals) {
18620
18948
  const intentContext = {};
18621
18949
  if (conversation && conversation.prompts.length > 0) {
18622
18950
  const latest = conversation.prompts[conversation.prompts.length - 1];
18623
- 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
+ }
18624
18965
  intentContext.session_id = latest.session_id || void 0;
18625
18966
  intentContext.prompt_captured_at = latest.captured_at || void 0;
18626
18967
  if (conversation.prompts.length > 1) {
@@ -18712,6 +19053,85 @@ async function runAnalyze(opts, globals) {
18712
19053
  const response = result.data;
18713
19054
  const decision = response.gate_decision ?? "(unrecognised)";
18714
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
+ };
18715
19135
  const watermarkHash = sentPaths.length > 0 ? computeContentHash(sentPaths) : contentHash;
18716
19136
  const watermarkIsPartial = !!codeDelta.truncated;
18717
19137
  let silenced = null;
@@ -18864,9 +19284,41 @@ async function runAnalyze(opts, globals) {
18864
19284
  reverify_by: response.reverify_by
18865
19285
  });
18866
19286
  const grantNudge = grantWarning ? ` \u26A0\uFE0F ${grantWarning}` : "";
18867
- 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) {
18868
19321
  case "FAIL": {
18869
- writeIteration(iteration + 1, currentCommit, contentHash ?? void 0);
18870
19322
  const assessment = response.assessment;
18871
19323
  const narrative = assessment?.narrative ?? "";
18872
19324
  const findings = response.findings ?? [];
@@ -18943,7 +19395,19 @@ ${YELLOW}${loginNudge.trim()}${NC}
18943
19395
  if (grantNudge) process.stderr.write(`
18944
19396
  ${YELLOW}${grantNudge.trim()}${NC}
18945
19397
  `);
18946
- 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
+ });
18947
19411
  break;
18948
19412
  }
18949
19413
  case "PASS": {
@@ -18955,10 +19419,16 @@ ${YELLOW}${grantNudge.trim()}${NC}
18955
19419
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
18956
19420
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
18957
19421
  userSummary += loginNudge + grantNudge;
18958
- printJsonCompact(
18959
- buildHookOutput("PASS", userSummary, silenced ? null : agentContextFor(response, intentRepeatCount))
18960
- );
18961
- 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
+ });
18962
19432
  break;
18963
19433
  }
18964
19434
  case "WARN": {
@@ -18969,10 +19439,16 @@ ${YELLOW}${grantNudge.trim()}${NC}
18969
19439
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
18970
19440
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
18971
19441
  userSummary += loginNudge + grantNudge;
18972
- printJsonCompact(
18973
- buildHookOutput("WARN", userSummary, silenced ? null : agentContextFor(response, intentRepeatCount))
18974
- );
18975
- 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
+ });
18976
19452
  break;
18977
19453
  }
18978
19454
  default: {
@@ -19085,8 +19561,8 @@ async function runReview(opts, globals) {
19085
19561
  for (const p of specPaths) {
19086
19562
  if (!(0, import_node_fs26.existsSync)(p)) continue;
19087
19563
  try {
19088
- const { readFileSync: readFileSync15 } = await import("node:fs");
19089
- const content = readFileSync15(p, "utf-8");
19564
+ const { readFileSync: readFileSync16 } = await import("node:fs");
19565
+ const content = readFileSync16(p, "utf-8");
19090
19566
  specs.push({ path: p, content: content.slice(0, 10240) });
19091
19567
  } catch {
19092
19568
  }
@@ -20772,7 +21248,7 @@ function registerTelemetryCommands(program2) {
20772
21248
  }
20773
21249
 
20774
21250
  // src/cli.ts
20775
- program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.055cd97").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.4410c26").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async () => {
20776
21252
  try {
20777
21253
  await foldLegacyLocalCredential();
20778
21254
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codacy/verity-cli",
3
- "version": "0.28.1-experimental.055cd97",
3
+ "version": "0.28.1-experimental.4410c26",
4
4
  "description": "CLI for Verity quality gate service",
5
5
  "homepage": "https://verity.md",
6
6
  "bugs": {