@codacy/verity-cli 0.28.1-experimental.055cd97 → 0.28.1-experimental.39013ce

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 +385 -63
  2. package/package.json +1 -1
package/bin/verity.js CHANGED
@@ -13591,6 +13591,70 @@ var import_node_fs10 = require("node:fs");
13591
13591
  var import_node_crypto5 = require("node:crypto");
13592
13592
  var import_node_path11 = require("node:path");
13593
13593
 
13594
+ // src/lib/skip-detection.ts
13595
+ function isBareAckPrompt(prompt) {
13596
+ if (typeof prompt !== "string") return false;
13597
+ const trimmed = prompt.trim();
13598
+ if (trimmed.length === 0) return false;
13599
+ if (trimmed.length > 20) return false;
13600
+ 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;
13601
+ return bareAckPattern.test(trimmed);
13602
+ }
13603
+ function isContinuationPrompt(prompt) {
13604
+ if (typeof prompt !== "string") return false;
13605
+ const trimmed = prompt.trim();
13606
+ if (trimmed.length === 0) return false;
13607
+ if (trimmed.length > 24) return false;
13608
+ 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;
13609
+ return continuation.test(trimmed) || isBareAckPrompt(trimmed);
13610
+ }
13611
+ function resolveGoalPrompt(prompts) {
13612
+ if (prompts.length === 0) return null;
13613
+ const latest = prompts[prompts.length - 1];
13614
+ if (!isContinuationPrompt(latest.prompt)) return { entry: latest, turnsBack: 0 };
13615
+ for (let i = prompts.length - 2; i >= 0; i--) {
13616
+ if (!isContinuationPrompt(prompts[i].prompt)) {
13617
+ return { entry: prompts[i], turnsBack: prompts.length - 1 - i };
13618
+ }
13619
+ }
13620
+ return { entry: latest, turnsBack: 0 };
13621
+ }
13622
+ function isReflectionQuestion(response) {
13623
+ if (!response || typeof response !== "string") return false;
13624
+ const markers = [
13625
+ /reflection\s+for\s+future\s+agents/i,
13626
+ /what(?:'s|\s+is)\s+one\s+thing\s+you\s+learned/i,
13627
+ /say\s+['"]?skip['"]?\s+to\s+skip/i,
13628
+ /quick\s+reflection\s+question/i,
13629
+ // Post-flip (VRT-21): the agent drafts the reflection itself and, when
13630
+ // interactive, asks the user to confirm/correct before recording. That
13631
+ // turn authors no code either, so it's still a reflection turn.
13632
+ /reflection\s+draft/i,
13633
+ /confirm,?\s+correct,?\s+or\s+add/i
13634
+ ];
13635
+ return markers.some((m) => m.test(response));
13636
+ }
13637
+ function isMetaTaskLabel(label2) {
13638
+ if (label2 === null || label2 === void 0) return false;
13639
+ if (typeof label2 !== "string") return false;
13640
+ const trimmed = label2.trim();
13641
+ if (trimmed.length === 0) return true;
13642
+ const metaPatterns = [
13643
+ /^verity\s+[\w-]+\s+response$/i,
13644
+ // "Verity reflect response"
13645
+ /^simple user response$/i,
13646
+ /^verity\s+command$/i,
13647
+ // "Verity command"
13648
+ /^user\s+(question|reply|response|ack)$/i
13649
+ ];
13650
+ return metaPatterns.some((p) => p.test(trimmed));
13651
+ }
13652
+ function shouldSkipForBareAck(input) {
13653
+ if (!isBareAckPrompt(input.prompt)) return false;
13654
+ if (input.turnAuthoredCode) return false;
13655
+ return input.canSeeTurnAuthorship;
13656
+ }
13657
+
13594
13658
  // src/lib/dossier.ts
13595
13659
  var import_node_fs9 = require("node:fs");
13596
13660
  var import_node_crypto4 = require("node:crypto");
@@ -14695,7 +14759,19 @@ function sessionDossier(token, sessionId) {
14695
14759
  const d = openDossier(identity);
14696
14760
  return d ? { d, identity } : null;
14697
14761
  }
14762
+ function hasActiveGoal(d) {
14763
+ try {
14764
+ if (!(0, import_node_fs10.existsSync)(d.eventsPath)) return false;
14765
+ return (0, import_node_fs10.readFileSync)(d.eventsPath, "utf8").includes('"k":"goal"');
14766
+ } catch {
14767
+ return false;
14768
+ }
14769
+ }
14698
14770
  function recordGoal(d, prompt, source = "prompt") {
14771
+ if (source === "prompt" && isContinuationPrompt(prompt) && hasActiveGoal(d)) {
14772
+ appendEvent(d, { k: "goal_continue", text: prompt.slice(0, 64) });
14773
+ return;
14774
+ }
14699
14775
  const text = prompt.slice(0, MAX_GOAL_CHARS);
14700
14776
  appendEvent(d, {
14701
14777
  k: "goal",
@@ -15118,6 +15194,8 @@ function collectCodeDelta(files, opts) {
15118
15194
  let totalSize = 0;
15119
15195
  let truncationReason = null;
15120
15196
  const droppedPaths = [];
15197
+ const excluded = [];
15198
+ const exclude = (path, reason) => excluded.push({ path, reason, stage: "collectCodeDelta", kind: "capacity" });
15121
15199
  for (const filepath of sorted) {
15122
15200
  if (result.length >= maxFiles) {
15123
15201
  truncationReason ??= "max_files";
@@ -15125,14 +15203,21 @@ function collectCodeDelta(files, opts) {
15125
15203
  continue;
15126
15204
  }
15127
15205
  const resolved = resolveFile(filepath);
15128
- if (!resolved) continue;
15206
+ if (!resolved) {
15207
+ exclude(filepath, "path-not-resolvable");
15208
+ continue;
15209
+ }
15129
15210
  let size;
15130
15211
  try {
15131
15212
  size = (0, import_node_fs11.statSync)(resolved).size;
15132
15213
  } catch {
15214
+ exclude(filepath, "not-stattable");
15215
+ continue;
15216
+ }
15217
+ if (size > maxFileBytes) {
15218
+ exclude(filepath, `over-file-size-limit-${maxFileBytes}b`);
15133
15219
  continue;
15134
15220
  }
15135
- if (size > maxFileBytes) continue;
15136
15221
  if (totalSize + size > maxTotalBytes) {
15137
15222
  truncationReason ??= "max_total_bytes";
15138
15223
  const idx = sorted.indexOf(filepath);
@@ -15143,6 +15228,7 @@ function collectCodeDelta(files, opts) {
15143
15228
  try {
15144
15229
  content = (0, import_node_fs11.readFileSync)(resolved, "utf-8");
15145
15230
  } catch {
15231
+ exclude(filepath, "not-readable");
15146
15232
  continue;
15147
15233
  }
15148
15234
  totalSize += size;
@@ -15156,10 +15242,14 @@ function collectCodeDelta(files, opts) {
15156
15242
  (sum, f) => sum + f.content.split("\n").length,
15157
15243
  0
15158
15244
  );
15245
+ for (const path of droppedPaths) {
15246
+ exclude(path, truncationReason === "max_files" ? "max-files-cap" : "max-total-bytes-cap");
15247
+ }
15159
15248
  return {
15160
15249
  files: result,
15161
15250
  total_lines: totalLines,
15162
15251
  total_files: result.length,
15252
+ excluded,
15163
15253
  ...truncationReason && {
15164
15254
  truncated: {
15165
15255
  reason: truncationReason,
@@ -16611,7 +16701,7 @@ function resolveTaskContext(opts) {
16611
16701
  // src/lib/cli-version.ts
16612
16702
  function cliVersion() {
16613
16703
  try {
16614
- return true ? "0.28.1-experimental.055cd97" : "dev";
16704
+ return true ? "0.28.1-experimental.39013ce" : "dev";
16615
16705
  } catch {
16616
16706
  return "dev";
16617
16707
  }
@@ -17156,6 +17246,86 @@ function checkConservation(changedFiles, result, repoRoot2) {
17156
17246
  };
17157
17247
  }
17158
17248
 
17249
+ // src/lib/verdict.ts
17250
+ function reconcileCoverage(changed, coverage) {
17251
+ const changedSet = new Set(changed);
17252
+ const reviewed = coverage.reviewed.filter((p) => changedSet.has(p));
17253
+ const claimed = /* @__PURE__ */ new Set([...reviewed, ...coverage.notReviewed.map((n) => n.path)]);
17254
+ const unaccounted = [...changedSet].filter((p) => !claimed.has(p)).sort();
17255
+ const notReviewed = [
17256
+ ...coverage.notReviewed.filter((n) => changedSet.has(n.path)),
17257
+ ...unaccounted.map((path) => ({
17258
+ path,
17259
+ reason: "unaccounted",
17260
+ // Named so the eventual bug report writes itself: some stage removed this
17261
+ // path and did not say so.
17262
+ stage: "unknown-stage",
17263
+ // An undeclared drop is CAPACITY by default. A stage that cannot be
17264
+ // bothered to say why it dropped a file does not get the benefit of the
17265
+ // doubt — that default is what makes forgetting expensive.
17266
+ kind: "capacity"
17267
+ }))
17268
+ ];
17269
+ return {
17270
+ coverage: { reviewed: [...new Set(reviewed)].sort(), notReviewed },
17271
+ unaccounted,
17272
+ balances: unaccounted.length === 0
17273
+ };
17274
+ }
17275
+ function resolveVerdict(proposed, coverage) {
17276
+ if (proposed === "FAIL") return "FAIL";
17277
+ const blocking = coverage.notReviewed.filter((n) => (n.kind ?? "capacity") !== "policy");
17278
+ if (blocking.length === 0) return proposed;
17279
+ return "WARN";
17280
+ }
17281
+ function describeCoverage(coverage, maxPaths = 5) {
17282
+ const relevant = coverage.notReviewed.filter((n) => (n.kind ?? "capacity") !== "policy");
17283
+ if (relevant.length === 0) return null;
17284
+ const byReason = /* @__PURE__ */ new Map();
17285
+ for (const n of relevant) {
17286
+ const key = `${n.reason}`;
17287
+ const list = byReason.get(key) ?? [];
17288
+ list.push(n.path);
17289
+ byReason.set(key, list);
17290
+ }
17291
+ const lines = [];
17292
+ for (const [reason, paths] of [...byReason.entries()].sort()) {
17293
+ const shown = paths.slice(0, maxPaths).join(", ");
17294
+ const more = paths.length > maxPaths ? ` (+${paths.length - maxPaths} more)` : "";
17295
+ lines.push(` ${paths.length} not reviewed \u2014 ${reason}: ${shown}${more}`);
17296
+ }
17297
+ return `NOT A CLEAN REVIEW. ${relevant.length} changed file(s) never reached the reviewer, so this verdict does not cover them:
17298
+ ${lines.join("\n")}
17299
+ Treat those files as UNCHECKED, not as approved.`;
17300
+ }
17301
+ function openBlockingElsewhere(statements, reviewedNow, lineShaAt) {
17302
+ const reviewed = new Set(reviewedNow);
17303
+ const out = [];
17304
+ const seen = /* @__PURE__ */ new Set();
17305
+ for (const s of statements) {
17306
+ if (s.outcome !== "open") continue;
17307
+ if (s.register !== "BLOCK") continue;
17308
+ if (s.carried) continue;
17309
+ if (reviewed.has(s.file)) continue;
17310
+ if (!s.line_sha) continue;
17311
+ if (lineShaAt(s.file, s.line) !== s.line_sha) continue;
17312
+ const key = `${s.file}::${s.pattern_id}`;
17313
+ if (seen.has(key)) continue;
17314
+ seen.add(key);
17315
+ out.push({ file: s.file, line: s.line, pattern_id: s.pattern_id });
17316
+ }
17317
+ return out;
17318
+ }
17319
+ function describeOpenElsewhere(open) {
17320
+ if (open.length === 0) return null;
17321
+ const lines = open.slice(0, 5).map((o) => ` ${o.file}:${o.line} [${o.pattern_id}]`);
17322
+ const more = open.length > 5 ? `
17323
+ (+${open.length - 5} more)` : "";
17324
+ return `STILL OPEN ELSEWHERE. ${open.length} blocking finding(s) Verity raised earlier are still present in files this run did not review:
17325
+ ${lines.join("\n")}${more}
17326
+ This verdict covers the current change only. The tree is not clean.`;
17327
+ }
17328
+
17159
17329
  // src/lib/channel.ts
17160
17330
  var MAX_AGENT_CONTEXT_CHARS = 1500;
17161
17331
  var MAX_AGENT_ITEMS = 5;
@@ -17243,6 +17413,39 @@ function channelSilence(input) {
17243
17413
  return null;
17244
17414
  }
17245
17415
 
17416
+ // src/lib/emit.ts
17417
+ var YELLOW2 = "\x1B[33m";
17418
+ var NC2 = "\x1B[0m";
17419
+ function emitVerdict(input) {
17420
+ const exit = input.exit ?? ((code) => process.exit(code));
17421
+ const { coverage, unaccounted } = reconcileCoverage(input.changed, input.coverage);
17422
+ let verdict = resolveVerdict(input.proposed, coverage);
17423
+ const openElsewhere = input.openElsewhere ?? [];
17424
+ if (verdict === "PASS" && openElsewhere.length > 0) verdict = "WARN";
17425
+ const note = [describeCoverage(coverage), describeOpenElsewhere(openElsewhere)].filter(Boolean).join("\n\n") || null;
17426
+ if (unaccounted.length > 0) {
17427
+ process.stderr.write(
17428
+ `${YELLOW2}Verity: ${unaccounted.length} changed file(s) could not be attributed to any review stage \u2014 counted as unreviewed.${NC2}
17429
+ `
17430
+ );
17431
+ }
17432
+ if (verdict === "FAIL") {
17433
+ input.renderBlocking?.();
17434
+ if (input.agentContext) {
17435
+ process.stderr.write(`
17436
+ ${input.agentContext}
17437
+ `);
17438
+ }
17439
+ if (note && !input.silenced) process.stderr.write(`
17440
+ ${YELLOW2}${note}${NC2}
17441
+ `);
17442
+ return exit(2);
17443
+ }
17444
+ const agentBlock = input.silenced ? null : [input.agentContext, note].filter(Boolean).join("\n\n") || null;
17445
+ printJsonCompact(buildHookOutput(verdict, input.userSummary, agentBlock));
17446
+ return exit(0);
17447
+ }
17448
+
17246
17449
  // src/lib/cache-cleanup.ts
17247
17450
  var import_node_fs21 = require("node:fs");
17248
17451
  var import_node_path18 = require("node:path");
@@ -17422,46 +17625,6 @@ function shouldWarmRetryAnalyze(result) {
17422
17625
  return false;
17423
17626
  }
17424
17627
 
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
17628
  // src/lib/transcript.ts
17466
17629
  var import_node_fs22 = require("node:fs");
17467
17630
  var MAX_READ_BYTES = 256 * 1024;
@@ -17625,6 +17788,13 @@ function buildSummary(lines) {
17625
17788
  files_read: capArray(filesRead, MAX_FILES_LIST),
17626
17789
  files_edited: capArray(filesEdited, MAX_FILES_LIST),
17627
17790
  files_created: capArray(filesCreated, MAX_CREATED_LIST),
17791
+ // The complement of the two caps that affect SCOPE. `files_read` is excluded
17792
+ // deliberately: reading a file is not authoring it, so a capped read list
17793
+ // narrows nothing.
17794
+ capped_out: [
17795
+ ...cappedOut(filesEdited, MAX_FILES_LIST),
17796
+ ...cappedOut(filesCreated, MAX_CREATED_LIST)
17797
+ ],
17628
17798
  searches,
17629
17799
  commands,
17630
17800
  subagents,
@@ -17675,6 +17845,9 @@ function sanitizeCommand(rawCmd) {
17675
17845
  function capArray(set, max) {
17676
17846
  return Array.from(set).slice(0, max);
17677
17847
  }
17848
+ function cappedOut(set, max) {
17849
+ return Array.from(set).slice(max);
17850
+ }
17678
17851
 
17679
17852
  // src/lib/run-mode.ts
17680
17853
  function parseAutonomousEnv(raw) {
@@ -18084,12 +18257,45 @@ function agentContextFor(response, intentRepeat = 0) {
18084
18257
  });
18085
18258
  }
18086
18259
  var beaconCtx = null;
18087
- async function passAndExit(reason, skip) {
18260
+ async function passAndExit(reason, skip, kindOverride) {
18088
18261
  const sent = await sendSkipBeacon(beaconCtx, skip);
18089
18262
  logEvent("skip", { reason: skip, beacon: sent });
18090
- printJsonCompact({ gate_decision: "PASS", systemMessage: `Verity: ${reason}` });
18263
+ const POLICY_SKIPS = /* @__PURE__ */ new Set([
18264
+ "no-analyzable-files",
18265
+ "verity-command",
18266
+ "bare-acknowledgment",
18267
+ "reflection-prompt",
18268
+ "skip-mode",
18269
+ "zero-increment",
18270
+ "debounce",
18271
+ "no-delta-since-last-review"
18272
+ ]);
18273
+ const skipKind = kindOverride ?? (POLICY_SKIPS.has(skip) ? "policy" : "capacity");
18274
+ const changed = skipCoverageChanged;
18275
+ const { coverage, unaccounted } = reconcileCoverage(changed, {
18276
+ reviewed: [],
18277
+ notReviewed: changed.map((path) => ({ path, reason: skip, stage: "pre-flight", kind: skipKind }))
18278
+ });
18279
+ const verdict = resolveVerdict("PASS", coverage);
18280
+ const note = describeCoverage(coverage);
18281
+ if (unaccounted.length > 0) {
18282
+ logEvent("coverage_unaccounted", { where: "passAndExit", skip, count: unaccounted.length });
18283
+ }
18284
+ const AGENT_SILENT_SKIPS = /* @__PURE__ */ new Set(["iteration-cap"]);
18285
+ const agentNote = AGENT_SILENT_SKIPS.has(skip) ? null : note;
18286
+ printJsonCompact(
18287
+ buildHookOutput(
18288
+ verdict,
18289
+ `Verity: ${reason}`,
18290
+ // The agent's ONLY input is additionalContext. Sixteen of the nineteen
18291
+ // terminating paths wrote `systemMessage` — the human's field — and told
18292
+ // the agent nothing at all.
18293
+ agentNote
18294
+ )
18295
+ );
18091
18296
  process.exit(0);
18092
18297
  }
18298
+ var skipCoverageChanged = [];
18093
18299
  var EMPTY_STATIC = {
18094
18300
  tool: "@codacy/analysis-cli",
18095
18301
  findings: [],
@@ -18104,7 +18310,7 @@ function runLocalStatic(analyzable, securityFiles, baseline, skipStatic) {
18104
18310
  }
18105
18311
  function localOnlyAndExit(staticResults) {
18106
18312
  printJsonCompact({
18107
- gate_decision: "PASS",
18313
+ gate_decision: "WARN",
18108
18314
  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
18315
  unauthenticated: true,
18110
18316
  static_results: staticResults
@@ -18164,6 +18370,7 @@ async function runAnalyze(opts, globals) {
18164
18370
  });
18165
18371
  }
18166
18372
  const { files: allChanged, hasRecentCommitFiles } = getChangedFiles();
18373
+ skipCoverageChanged = allChanged;
18167
18374
  const analyzable = filterAnalyzable(allChanged);
18168
18375
  const reviewable = filterReviewable(allChanged);
18169
18376
  const securityFiles = filterSecurity(allChanged);
@@ -18179,10 +18386,12 @@ async function runAnalyze(opts, globals) {
18179
18386
  if (/^\s*\/verity-/i.test(latestPrompt)) {
18180
18387
  await passAndExit("Verity command \u2014 skipping analysis", "verity-command");
18181
18388
  }
18182
- if (isBareAckPrompt(latestPrompt)) {
18389
+ const agentAuthoredCodeThisTurn = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0));
18390
+ const turnAuthoredCode = agentAuthoredCodeThisTurn || !!baseline && allForReview.some((f) => changedSinceBaseline(f, baseline));
18391
+ const canSeeTurnAuthorship = !!actionSummary || !!baseline;
18392
+ if (shouldSkipForBareAck({ prompt: latestPrompt, turnAuthoredCode, canSeeTurnAuthorship })) {
18183
18393
  await passAndExit("Bare acknowledgment \u2014 skipping analysis", "bare-acknowledgment");
18184
18394
  }
18185
- const agentAuthoredCodeThisTurn = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0));
18186
18395
  if (isReflectionQuestion(assistantResponse) && !agentAuthoredCodeThisTurn) {
18187
18396
  await passAndExit("Reflection-prompt turn \u2014 skipping analysis", "reflection-prompt");
18188
18397
  }
@@ -18239,7 +18448,8 @@ async function runAnalyze(opts, globals) {
18239
18448
  let codeDelta = {
18240
18449
  files: [],
18241
18450
  total_lines: 0,
18242
- total_files: 0
18451
+ total_files: 0,
18452
+ excluded: []
18243
18453
  };
18244
18454
  let snapshotResult = { has_snapshots: false, diffs: [] };
18245
18455
  let contentHash = null;
@@ -18304,7 +18514,11 @@ async function runAnalyze(opts, globals) {
18304
18514
  if (assistantResponse) {
18305
18515
  analysisMode = "plan";
18306
18516
  } else {
18307
- await passAndExit("No files within size limits to analyze", "size-limit");
18517
+ await passAndExit(
18518
+ "No files within size limits to analyze",
18519
+ "size-limit",
18520
+ codeDelta.excluded.length > 0 ? "capacity" : "policy"
18521
+ );
18308
18522
  }
18309
18523
  }
18310
18524
  }
@@ -18620,7 +18834,12 @@ async function runAnalyze(opts, globals) {
18620
18834
  const intentContext = {};
18621
18835
  if (conversation && conversation.prompts.length > 0) {
18622
18836
  const latest = conversation.prompts[conversation.prompts.length - 1];
18623
- intentContext.user_prompt = latest.prompt;
18837
+ const goalPrompt = resolveGoalPrompt(conversation.prompts) ?? { entry: latest, turnsBack: 0 };
18838
+ intentContext.user_prompt = goalPrompt.entry.prompt;
18839
+ if (goalPrompt.turnsBack > 0) {
18840
+ intentContext.continuation_prompt = latest.prompt;
18841
+ logEvent("goal_walked_back", { turns_back: goalPrompt.turnsBack });
18842
+ }
18624
18843
  intentContext.session_id = latest.session_id || void 0;
18625
18844
  intentContext.prompt_captured_at = latest.captured_at || void 0;
18626
18845
  if (conversation.prompts.length > 1) {
@@ -18712,6 +18931,85 @@ async function runAnalyze(opts, globals) {
18712
18931
  const response = result.data;
18713
18932
  const decision = response.gate_decision ?? "(unrecognised)";
18714
18933
  const sentPaths = codeDelta.files.map((f) => f.path);
18934
+ let openElsewhere = [];
18935
+ if (memorySession) {
18936
+ try {
18937
+ const st = foldDossier(memorySession.d);
18938
+ openElsewhere = openBlockingElsewhere(st.statements, sentPaths, (file, line) => {
18939
+ try {
18940
+ const src = (0, import_node_fs24.readFileSync)((0, import_node_path20.join)(repoRoot(), file), "utf8").split("\n");
18941
+ const at = src[line - 1];
18942
+ return at === void 0 ? null : lineSha(at);
18943
+ } catch {
18944
+ return null;
18945
+ }
18946
+ });
18947
+ } catch {
18948
+ }
18949
+ }
18950
+ const reviewCoverage = {
18951
+ reviewed: sentPaths,
18952
+ // Declared drops from the stages that DO report themselves today. The other
18953
+ // stages surface via `unaccounted`, which is the tripwire, not the design.
18954
+ notReviewed: [
18955
+ // Every exit from the collection loop, each named. Six reasons where there
18956
+ // used to be two recorded and four silent — the silent ones including the
18957
+ // per-file size cap, which could drop a whole source file without leaving a
18958
+ // trace anywhere in the payload or the run row.
18959
+ ...codeDelta.excluded,
18960
+ // The server-side 300-line middle-out truncation. It only bites on the
18961
+ // full-file branch (a first analysis, before snapshots exist) because
18962
+ // analyze normally sends diffs — but on that branch the reviewer sees the
18963
+ // first and last 100 lines and nothing between, and until now said so to
18964
+ // nobody. CAPACITY: a partial look is not a look.
18965
+ ...(response.metadata?.truncated_files ?? []).map((path) => ({
18966
+ path,
18967
+ reason: "file-middle-truncated-300-lines",
18968
+ stage: "prompt-builder",
18969
+ kind: "capacity"
18970
+ })),
18971
+ // The 20-entry edit cap. CAPACITY, and the sharpest of the lot: it narrows
18972
+ // what is REVIEWED, not merely what is summarised — a session editing 25
18973
+ // files had five silently excluded from the reviewed set.
18974
+ ...(actionSummary?.capped_out ?? []).map((path) => ({
18975
+ path,
18976
+ reason: "edit-list-cap-20",
18977
+ stage: "extractActionSummary",
18978
+ kind: "capacity"
18979
+ })),
18980
+ // ⚠ BASELINE SCOPING — the biggest source of false NOT A CLEAN REVIEW.
18981
+ //
18982
+ // The universe is `allChanged`, git's whole dirty tree. The reviewed set is
18983
+ // scoped to what THIS SESSION authored (the VRT-26 contamination cure), so
18984
+ // every pre-existing dirty file is in the universe, absent from `reviewed`,
18985
+ // and — until now — declared by nobody. It fell through to `unaccounted`,
18986
+ // became capacity, and produced "NOT A CLEAN REVIEW: admin.js" over a file
18987
+ // that was never this session's to review.
18988
+ //
18989
+ // Measured 2026-08-04: three consecutive runs over an untouched tree gave
18990
+ // three different answers — .claude/settings.json, then admin.js, then six
18991
+ // files — because each run took a different path and each path had a
18992
+ // different idea of the universe. POLICY: not this session's work is not a
18993
+ // coverage gap, it is the cure working.
18994
+ ...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) => ({
18995
+ path,
18996
+ reason: "not-authored-this-session",
18997
+ stage: "baseline-scoping",
18998
+ kind: "policy"
18999
+ })),
19000
+ // The extension allowlist, and it is POLICY rather than capacity: a changed
19001
+ // README was never going to be reviewed, and treating that as a coverage
19002
+ // gap would downgrade nearly every PASS to WARN until WARN meant nothing.
19003
+ // Recorded so the ledger balances and so "what did Verity ignore entirely"
19004
+ // is answerable — but it never touches the verdict.
19005
+ ...allChanged.filter((p) => !analyzable.includes(p) && !reviewable.includes(p) && !securityFiles.includes(p)).map((path) => ({
19006
+ path,
19007
+ reason: "not-a-reviewed-file-type",
19008
+ stage: "extension-allowlist",
19009
+ kind: "policy"
19010
+ }))
19011
+ ]
19012
+ };
18715
19013
  const watermarkHash = sentPaths.length > 0 ? computeContentHash(sentPaths) : contentHash;
18716
19014
  const watermarkIsPartial = !!codeDelta.truncated;
18717
19015
  let silenced = null;
@@ -18943,7 +19241,19 @@ ${YELLOW}${loginNudge.trim()}${NC}
18943
19241
  if (grantNudge) process.stderr.write(`
18944
19242
  ${YELLOW}${grantNudge.trim()}${NC}
18945
19243
  `);
18946
- process.exit(2);
19244
+ emitVerdict({
19245
+ proposed: "FAIL",
19246
+ changed: skipCoverageChanged,
19247
+ coverage: reviewCoverage,
19248
+ userSummary: "",
19249
+ // Subject to the SAME cycle cut as PASS/WARN. Suppressing here is safe:
19250
+ // the findings themselves are rendered above by the blocking renderer,
19251
+ // so what the cut removes is the repeated commentary, never the defect.
19252
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount),
19253
+ // The coverage note is silenced with it — half a channel is still a channel.
19254
+ silenced: !!silenced,
19255
+ openElsewhere
19256
+ });
18947
19257
  break;
18948
19258
  }
18949
19259
  case "PASS": {
@@ -18955,10 +19265,16 @@ ${YELLOW}${grantNudge.trim()}${NC}
18955
19265
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
18956
19266
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
18957
19267
  userSummary += loginNudge + grantNudge;
18958
- printJsonCompact(
18959
- buildHookOutput("PASS", userSummary, silenced ? null : agentContextFor(response, intentRepeatCount))
18960
- );
18961
- process.exit(0);
19268
+ emitVerdict({
19269
+ proposed: "PASS",
19270
+ changed: skipCoverageChanged,
19271
+ coverage: reviewCoverage,
19272
+ userSummary,
19273
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount),
19274
+ // The coverage note is silenced with it — half a channel is still a channel.
19275
+ silenced: !!silenced,
19276
+ openElsewhere
19277
+ });
18962
19278
  break;
18963
19279
  }
18964
19280
  case "WARN": {
@@ -18969,10 +19285,16 @@ ${YELLOW}${grantNudge.trim()}${NC}
18969
19285
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
18970
19286
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
18971
19287
  userSummary += loginNudge + grantNudge;
18972
- printJsonCompact(
18973
- buildHookOutput("WARN", userSummary, silenced ? null : agentContextFor(response, intentRepeatCount))
18974
- );
18975
- process.exit(0);
19288
+ emitVerdict({
19289
+ proposed: "WARN",
19290
+ changed: skipCoverageChanged,
19291
+ coverage: reviewCoverage,
19292
+ userSummary,
19293
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount),
19294
+ // The coverage note is silenced with it — half a channel is still a channel.
19295
+ silenced: !!silenced,
19296
+ openElsewhere
19297
+ });
18976
19298
  break;
18977
19299
  }
18978
19300
  default: {
@@ -19085,8 +19407,8 @@ async function runReview(opts, globals) {
19085
19407
  for (const p of specPaths) {
19086
19408
  if (!(0, import_node_fs26.existsSync)(p)) continue;
19087
19409
  try {
19088
- const { readFileSync: readFileSync15 } = await import("node:fs");
19089
- const content = readFileSync15(p, "utf-8");
19410
+ const { readFileSync: readFileSync16 } = await import("node:fs");
19411
+ const content = readFileSync16(p, "utf-8");
19090
19412
  specs.push({ path: p, content: content.slice(0, 10240) });
19091
19413
  } catch {
19092
19414
  }
@@ -20772,7 +21094,7 @@ function registerTelemetryCommands(program2) {
20772
21094
  }
20773
21095
 
20774
21096
  // 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 () => {
21097
+ program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.39013ce").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
21098
  try {
20777
21099
  await foldLegacyLocalCredential();
20778
21100
  } 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.39013ce",
4
4
  "description": "CLI for Verity quality gate service",
5
5
  "homepage": "https://verity.md",
6
6
  "bugs": {