@codacy/verity-cli 0.28.1-experimental.8cc9c66 → 0.28.1-experimental.902dbd9

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 +466 -94
  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,143 @@ 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
+
13657
+ // src/lib/pending-repeat.ts
13658
+ var STOP = /* @__PURE__ */ new Set([
13659
+ "the",
13660
+ "and",
13661
+ "that",
13662
+ "this",
13663
+ "with",
13664
+ "from",
13665
+ "have",
13666
+ "been",
13667
+ "were",
13668
+ "what",
13669
+ "when",
13670
+ "which",
13671
+ "their",
13672
+ "there",
13673
+ "these",
13674
+ "those",
13675
+ "would",
13676
+ "could",
13677
+ "should",
13678
+ "must",
13679
+ "will",
13680
+ "also",
13681
+ "just",
13682
+ "only",
13683
+ "into",
13684
+ "over",
13685
+ "than",
13686
+ "then",
13687
+ "them",
13688
+ "some",
13689
+ "such",
13690
+ "more",
13691
+ "most",
13692
+ "other",
13693
+ "about",
13694
+ "after",
13695
+ "before",
13696
+ "since",
13697
+ "because",
13698
+ "while",
13699
+ "where",
13700
+ "whether",
13701
+ "ensure",
13702
+ "confirm",
13703
+ "verify",
13704
+ "check"
13705
+ ]);
13706
+ function pendingTokens(text) {
13707
+ if (!text || typeof text !== "string") return [];
13708
+ const out = /* @__PURE__ */ new Set();
13709
+ for (const raw of text.toLowerCase().split(/[^a-z0-9]+/)) {
13710
+ if (raw.length <= 3) continue;
13711
+ if (STOP.has(raw)) continue;
13712
+ out.add(raw);
13713
+ }
13714
+ return [...out].sort();
13715
+ }
13716
+ var REPEAT_THRESHOLD = 0.3;
13717
+ function overlapCoefficient(a, b) {
13718
+ if (a.length === 0 || b.length === 0) return 0;
13719
+ const setB = new Set(b);
13720
+ let shared = 0;
13721
+ for (const t of a) if (setB.has(t)) shared++;
13722
+ return shared / Math.min(a.length, b.length);
13723
+ }
13724
+ function isRepeatOfAny(text, priorFingerprints, threshold = REPEAT_THRESHOLD) {
13725
+ const tokens = pendingTokens(text);
13726
+ if (tokens.length === 0) return false;
13727
+ return priorFingerprints.some((prior) => overlapCoefficient(tokens, prior) >= threshold);
13728
+ }
13729
+
13594
13730
  // src/lib/dossier.ts
13595
13731
  var import_node_fs9 = require("node:fs");
13596
13732
  var import_node_crypto4 = require("node:crypto");
@@ -13599,6 +13735,7 @@ var MAX_LINE_BYTES = 4096;
13599
13735
  var MAX_GOAL_CHARS = 2e3;
13600
13736
  var GOAL_KEEP = 8;
13601
13737
  var GOAL_TOTAL_CAP = 32;
13738
+ var RECENT_PENDING_CAP = 20;
13602
13739
  var HASH_WIDTH = 16;
13603
13740
  var AUTHORED_CAP = 300;
13604
13741
  var NOT_MINE_CAP = 300;
@@ -13967,6 +14104,10 @@ function reduce(state, events, now) {
13967
14104
  consecutiveIdle: ev.idle === false ? 0 : (state.meta.channel?.consecutiveIdle ?? 0) + 1
13968
14105
  };
13969
14106
  state.meta.last_adjudication = ev.intent_verdict ? { verdict: ev.intent_verdict, score: ev.intent_score ?? null, at: ev.at, decision: ev.decision } : void 0;
14107
+ if (Array.isArray(ev.pending_sigs) && ev.pending_sigs.length > 0) {
14108
+ const prior = state.meta.recent_pending_sigs ?? [];
14109
+ state.meta.recent_pending_sigs = [...prior, ...ev.pending_sigs].slice(-RECENT_PENDING_CAP);
14110
+ }
13970
14111
  if (ev.intent_sig) {
13971
14112
  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 };
13972
14113
  } else {
@@ -14695,7 +14836,19 @@ function sessionDossier(token, sessionId) {
14695
14836
  const d = openDossier(identity);
14696
14837
  return d ? { d, identity } : null;
14697
14838
  }
14839
+ function hasActiveGoal(d) {
14840
+ try {
14841
+ if (!(0, import_node_fs10.existsSync)(d.eventsPath)) return false;
14842
+ return (0, import_node_fs10.readFileSync)(d.eventsPath, "utf8").includes('"k":"goal"');
14843
+ } catch {
14844
+ return false;
14845
+ }
14846
+ }
14698
14847
  function recordGoal(d, prompt, source = "prompt") {
14848
+ if (source === "prompt" && isContinuationPrompt(prompt) && hasActiveGoal(d)) {
14849
+ appendEvent(d, { k: "goal_continue", text: prompt.slice(0, 64) });
14850
+ return;
14851
+ }
14699
14852
  const text = prompt.slice(0, MAX_GOAL_CHARS);
14700
14853
  appendEvent(d, {
14701
14854
  k: "goal",
@@ -14811,6 +14964,13 @@ function recordVerdict(d, v) {
14811
14964
  branch: v.branch,
14812
14965
  decision: v.decision,
14813
14966
  ...sig && { intent_sig: sig },
14967
+ // Fingerprints of the pending items this verdict delivered, so the NEXT turn
14968
+ // can tell a repeat from a new requirement. Only recorded when the channel
14969
+ // actually spoke — a silenced turn delivered nothing, so nothing was "said
14970
+ // before" and labelling the next turn's items as repeats would be a lie.
14971
+ ...v.emitted === true && v.pendingTexts && v.pendingTexts.length > 0 && {
14972
+ pending_sigs: v.pendingTexts.slice(0, 8).map((t) => pendingTokens(t).slice(0, 16))
14973
+ },
14814
14974
  emitted: v.emitted === true,
14815
14975
  idle: v.idle !== false,
14816
14976
  ...v.intent?.verdict && { intent_verdict: v.intent.verdict },
@@ -15467,6 +15627,34 @@ ${addedLines}`,
15467
15627
  }
15468
15628
  return { diffs, has_baseline: true };
15469
15629
  }
15630
+ function absorbIntoBaseline(paths, sessionId) {
15631
+ const baseline = readBaseline(sessionId);
15632
+ if (!baseline || paths.length === 0) return 0;
15633
+ const dir = sessionDir(sessionKey(baseline.session_id));
15634
+ let adopted = 0;
15635
+ const dirty = new Set(baseline.dirty_paths);
15636
+ for (const p of paths) {
15637
+ try {
15638
+ const content = safeReadForMirror(projectPath(p));
15639
+ if (content === null) continue;
15640
+ const dest = mirrorPath(dir, p);
15641
+ (0, import_node_fs13.mkdirSync)((0, import_node_path14.dirname)(dest), { recursive: true });
15642
+ (0, import_node_fs13.writeFileSync)(dest, content);
15643
+ dirty.add(p);
15644
+ adopted++;
15645
+ } catch {
15646
+ }
15647
+ }
15648
+ if (adopted === 0) return 0;
15649
+ try {
15650
+ const updated = { ...baseline, dirty_paths: [...dirty] };
15651
+ (0, import_node_fs13.writeFileSync)(manifestPath(dir), JSON.stringify(updated));
15652
+ preImageCache.delete(baseline);
15653
+ } catch {
15654
+ return 0;
15655
+ }
15656
+ return adopted;
15657
+ }
15470
15658
  function changedSinceBaseline(repoRelPath, baseline) {
15471
15659
  const pre = preImage(repoRelPath, baseline);
15472
15660
  let current;
@@ -16343,40 +16531,40 @@ function narrowToRecent(files, sessionId) {
16343
16531
  });
16344
16532
  return recent.length > 0 ? recent : files;
16345
16533
  }
16346
- function readIteration(currentCommit, _contentHash) {
16347
- if (!(0, import_node_fs15.existsSync)(ITERATION_FILE)) return 1;
16534
+ function readIterationState(currentCommit) {
16535
+ if (!(0, import_node_fs15.existsSync)(ITERATION_FILE)) return { iteration: 1, fingerprint: null };
16348
16536
  try {
16349
16537
  const stored = (0, import_node_fs15.readFileSync)(ITERATION_FILE, "utf-8").trim();
16350
16538
  const parts = stored.split(":");
16351
16539
  const iter = parseInt(parts[0], 10);
16352
16540
  const storedCommit = parts[1] ?? "";
16353
16541
  const storedTimestamp = parseInt(parts[2] ?? "0", 10);
16354
- if (isNaN(iter)) return 1;
16355
- if (storedCommit !== currentCommit) return 1;
16542
+ const fingerprint = parts.slice(3).join(":") || null;
16543
+ if (isNaN(iter)) return { iteration: 1, fingerprint: null };
16544
+ if (storedCommit !== currentCommit) return { iteration: 1, fingerprint: null };
16356
16545
  if (storedTimestamp > 0) {
16357
16546
  const elapsed = Math.floor(Date.now() / 1e3) - storedTimestamp;
16358
- if (elapsed > 600) return 1;
16547
+ if (elapsed > 600) return { iteration: 1, fingerprint: null };
16359
16548
  }
16360
- return iter;
16549
+ return { iteration: iter, fingerprint };
16361
16550
  } catch {
16362
- return 1;
16551
+ return { iteration: 1, fingerprint: null };
16363
16552
  }
16364
16553
  }
16365
- function checkMaxIterations(currentCommit, maxIterations = MAX_ITERATIONS, contentHash) {
16366
- const iteration = readIteration(currentCommit, contentHash);
16367
- if (iteration > maxIterations) {
16368
- writeIteration(1, currentCommit, contentHash);
16369
- return {
16370
- skip: `Max Verity iterations (${maxIterations}) reached \u2014 accepting to prevent infinite loop. Human review required before deploying.`,
16371
- iteration
16372
- };
16373
- }
16374
- return { skip: null, iteration };
16554
+ function findingsFingerprint(findings) {
16555
+ const keys = findings.map((f) => `${String(f.pattern_id ?? "?")}|${String(f.file ?? "?")}`).filter((k) => k !== "?|?");
16556
+ return [...new Set(keys)].sort().join(",");
16557
+ }
16558
+ function isSameProblem(previous, current) {
16559
+ if (!previous || !current) return false;
16560
+ const prev = new Set(previous.split(","));
16561
+ return current.split(",").some((k) => prev.has(k));
16375
16562
  }
16376
- function writeIteration(iteration, commit, _contentHash) {
16563
+ function writeIteration(iteration, commit, _contentHash, fingerprint) {
16377
16564
  (0, import_node_fs15.mkdirSync)(VERITY_DIR, { recursive: true });
16378
16565
  const ts = Math.floor(Date.now() / 1e3);
16379
- (0, import_node_fs15.writeFileSync)(ITERATION_FILE, `${iteration}:${commit}:${ts}`);
16566
+ const fp = fingerprint ? `:${fingerprint}` : "";
16567
+ (0, import_node_fs15.writeFileSync)(ITERATION_FILE, `${iteration}:${commit}:${ts}${fp}`);
16380
16568
  }
16381
16569
 
16382
16570
  // src/lib/static-analysis.ts
@@ -16625,7 +16813,7 @@ function resolveTaskContext(opts) {
16625
16813
  // src/lib/cli-version.ts
16626
16814
  function cliVersion() {
16627
16815
  try {
16628
- return true ? "0.28.1-experimental.8cc9c66" : "dev";
16816
+ return true ? "0.28.1-experimental.902dbd9" : "dev";
16629
16817
  } catch {
16630
16818
  return "dev";
16631
16819
  }
@@ -17222,6 +17410,33 @@ function describeCoverage(coverage, maxPaths = 5) {
17222
17410
  ${lines.join("\n")}
17223
17411
  Treat those files as UNCHECKED, not as approved.`;
17224
17412
  }
17413
+ function openBlockingElsewhere(statements, reviewedNow, lineShaAt) {
17414
+ const reviewed = new Set(reviewedNow);
17415
+ const out = [];
17416
+ const seen = /* @__PURE__ */ new Set();
17417
+ for (const s of statements) {
17418
+ if (s.outcome !== "open") continue;
17419
+ if (s.register !== "BLOCK") continue;
17420
+ if (s.carried) continue;
17421
+ if (reviewed.has(s.file)) continue;
17422
+ if (!s.line_sha) continue;
17423
+ if (lineShaAt(s.file, s.line) !== s.line_sha) continue;
17424
+ const key = `${s.file}::${s.pattern_id}`;
17425
+ if (seen.has(key)) continue;
17426
+ seen.add(key);
17427
+ out.push({ file: s.file, line: s.line, pattern_id: s.pattern_id });
17428
+ }
17429
+ return out;
17430
+ }
17431
+ function describeOpenElsewhere(open) {
17432
+ if (open.length === 0) return null;
17433
+ const lines = open.slice(0, 5).map((o) => ` ${o.file}:${o.line} [${o.pattern_id}]`);
17434
+ const more = open.length > 5 ? `
17435
+ (+${open.length - 5} more)` : "";
17436
+ return `STILL OPEN ELSEWHERE. ${open.length} blocking finding(s) Verity raised earlier are still present in files this run did not review:
17437
+ ${lines.join("\n")}${more}
17438
+ This verdict covers the current change only. The tree is not clean.`;
17439
+ }
17225
17440
 
17226
17441
  // src/lib/channel.ts
17227
17442
  var MAX_AGENT_CONTEXT_CHARS = 1500;
@@ -17272,9 +17487,13 @@ function buildAgentContext(input) {
17272
17487
  }
17273
17488
  for (const p of input.pendingItems ?? []) {
17274
17489
  if (lines.length >= MAX_AGENT_ITEMS) break;
17490
+ if (p.pattern_id === "intent-misalignment") continue;
17275
17491
  const text = p.description ?? p.title ?? p.reason;
17276
17492
  if (!text) continue;
17277
- lines.push(renderItem("", text, p.pattern_id, p.file, p.line));
17493
+ const seenBefore = isRepeatOfAny(text, input.priorPendingFingerprints ?? []);
17494
+ lines.push(
17495
+ renderItem("", text, p.pattern_id, p.file, p.line) + (seenBefore ? "\n (raised earlier this session and still open \u2014 do not re-explain it; act on it or carry on)" : "")
17496
+ );
17278
17497
  }
17279
17498
  if (lines.length === 0) return null;
17280
17499
  const body = `${REPORT_PREFIX}
@@ -17316,8 +17535,10 @@ var NC2 = "\x1B[0m";
17316
17535
  function emitVerdict(input) {
17317
17536
  const exit = input.exit ?? ((code) => process.exit(code));
17318
17537
  const { coverage, unaccounted } = reconcileCoverage(input.changed, input.coverage);
17319
- const verdict = resolveVerdict(input.proposed, coverage);
17320
- const note = describeCoverage(coverage);
17538
+ let verdict = resolveVerdict(input.proposed, coverage);
17539
+ const openElsewhere = input.openElsewhere ?? [];
17540
+ if (verdict === "PASS" && openElsewhere.length > 0) verdict = "WARN";
17541
+ const note = [describeCoverage(coverage), describeOpenElsewhere(openElsewhere)].filter(Boolean).join("\n\n") || null;
17321
17542
  if (unaccounted.length > 0) {
17322
17543
  process.stderr.write(
17323
17544
  `${YELLOW2}Verity: ${unaccounted.length} changed file(s) could not be attributed to any review stage \u2014 counted as unreviewed.${NC2}
@@ -17331,12 +17552,12 @@ function emitVerdict(input) {
17331
17552
  ${input.agentContext}
17332
17553
  `);
17333
17554
  }
17334
- if (note) process.stderr.write(`
17555
+ if (note && !input.silenced) process.stderr.write(`
17335
17556
  ${YELLOW2}${note}${NC2}
17336
17557
  `);
17337
17558
  return exit(2);
17338
17559
  }
17339
- const agentBlock = [input.agentContext, note].filter(Boolean).join("\n\n") || null;
17560
+ const agentBlock = input.silenced ? null : [input.agentContext, note].filter(Boolean).join("\n\n") || null;
17340
17561
  printJsonCompact(buildHookOutput(verdict, input.userSummary, agentBlock));
17341
17562
  return exit(0);
17342
17563
  }
@@ -17520,46 +17741,6 @@ function shouldWarmRetryAnalyze(result) {
17520
17741
  return false;
17521
17742
  }
17522
17743
 
17523
- // src/lib/skip-detection.ts
17524
- function isBareAckPrompt(prompt) {
17525
- if (typeof prompt !== "string") return false;
17526
- const trimmed = prompt.trim();
17527
- if (trimmed.length === 0) return false;
17528
- if (trimmed.length > 20) return false;
17529
- 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;
17530
- return bareAckPattern.test(trimmed);
17531
- }
17532
- function isReflectionQuestion(response) {
17533
- if (!response || typeof response !== "string") return false;
17534
- const markers = [
17535
- /reflection\s+for\s+future\s+agents/i,
17536
- /what(?:'s|\s+is)\s+one\s+thing\s+you\s+learned/i,
17537
- /say\s+['"]?skip['"]?\s+to\s+skip/i,
17538
- /quick\s+reflection\s+question/i,
17539
- // Post-flip (VRT-21): the agent drafts the reflection itself and, when
17540
- // interactive, asks the user to confirm/correct before recording. That
17541
- // turn authors no code either, so it's still a reflection turn.
17542
- /reflection\s+draft/i,
17543
- /confirm,?\s+correct,?\s+or\s+add/i
17544
- ];
17545
- return markers.some((m) => m.test(response));
17546
- }
17547
- function isMetaTaskLabel(label2) {
17548
- if (label2 === null || label2 === void 0) return false;
17549
- if (typeof label2 !== "string") return false;
17550
- const trimmed = label2.trim();
17551
- if (trimmed.length === 0) return true;
17552
- const metaPatterns = [
17553
- /^verity\s+[\w-]+\s+response$/i,
17554
- // "Verity reflect response"
17555
- /^simple user response$/i,
17556
- /^verity\s+command$/i,
17557
- // "Verity command"
17558
- /^user\s+(question|reply|response|ack)$/i
17559
- ];
17560
- return metaPatterns.some((p) => p.test(trimmed));
17561
- }
17562
-
17563
17744
  // src/lib/transcript.ts
17564
17745
  var import_node_fs22 = require("node:fs");
17565
17746
  var MAX_READ_BYTES = 256 * 1024;
@@ -17573,9 +17754,11 @@ var MAX_SUMMARY_BYTES = 4096;
17573
17754
  var HOME = process.env.HOME ?? "";
17574
17755
  async function extractActionSummary(transcriptPath) {
17575
17756
  try {
17576
- const lines = readTurnLines(transcriptPath);
17577
- if (!lines || lines.length === 0) return null;
17578
- return buildSummary(lines);
17757
+ const read = readTurnLines(transcriptPath);
17758
+ if (!read || read.lines.length === 0) return null;
17759
+ const summary = buildSummary(read.lines);
17760
+ if (summary) summary.transcript_windowed = read.window;
17761
+ return summary;
17579
17762
  } catch {
17580
17763
  return null;
17581
17764
  }
@@ -17589,9 +17772,11 @@ function readTurnLines(transcriptPath) {
17589
17772
  }
17590
17773
  if (size === 0) return null;
17591
17774
  let raw;
17775
+ let windowed = false;
17592
17776
  if (size <= SMALL_FILE_BYTES) {
17593
17777
  raw = (0, import_node_fs22.readFileSync)(transcriptPath, "utf-8");
17594
17778
  } else {
17779
+ windowed = true;
17595
17780
  const buf = Buffer.alloc(Math.min(MAX_READ_BYTES, size));
17596
17781
  const fd = require("node:fs").openSync(transcriptPath, "r");
17597
17782
  try {
@@ -17609,17 +17794,22 @@ function readTurnLines(transcriptPath) {
17609
17794
  const allLines = raw.split("\n").filter((l) => l.trim().length > 0);
17610
17795
  if (allLines.length === 0) return null;
17611
17796
  let turnStart = 0;
17797
+ let boundaryFound = false;
17612
17798
  for (let i = allLines.length - 1; i >= 0; i--) {
17613
17799
  try {
17614
17800
  const parsed = JSON.parse(allLines[i]);
17615
17801
  if (parsed.type === "user" && isRealUserMessage(parsed)) {
17616
17802
  turnStart = i;
17803
+ boundaryFound = true;
17617
17804
  break;
17618
17805
  }
17619
17806
  } catch {
17620
17807
  }
17621
17808
  }
17622
- return allLines.slice(turnStart);
17809
+ return {
17810
+ lines: allLines.slice(turnStart),
17811
+ window: !windowed ? "whole" : boundaryFound ? "windowed" : "orphaned"
17812
+ };
17623
17813
  }
17624
17814
  function isRealUserMessage(parsed) {
17625
17815
  const message = parsed.message;
@@ -18177,11 +18367,12 @@ async function readStopHookStdin() {
18177
18367
  return empty;
18178
18368
  }
18179
18369
  }
18180
- function agentContextFor(response, intentRepeat = 0) {
18370
+ function agentContextFor(response, intentRepeat = 0, priorPendingFingerprints = []) {
18181
18371
  const metadata = response.metadata ?? {};
18182
18372
  const intent = response.intent_alignment ?? {};
18183
18373
  return buildAgentContext({
18184
18374
  intentRepeat,
18375
+ priorPendingFingerprints,
18185
18376
  gateDecision: String(response.gate_decision ?? ""),
18186
18377
  findings: response.findings ?? [],
18187
18378
  pendingItems: response.pending_items ?? [],
@@ -18216,6 +18407,8 @@ async function passAndExit(reason, skip, kindOverride) {
18216
18407
  if (unaccounted.length > 0) {
18217
18408
  logEvent("coverage_unaccounted", { where: "passAndExit", skip, count: unaccounted.length });
18218
18409
  }
18410
+ const AGENT_SILENT_SKIPS = /* @__PURE__ */ new Set([]);
18411
+ const agentNote = AGENT_SILENT_SKIPS.has(skip) ? null : note;
18219
18412
  printJsonCompact(
18220
18413
  buildHookOutput(
18221
18414
  verdict,
@@ -18223,7 +18416,7 @@ async function passAndExit(reason, skip, kindOverride) {
18223
18416
  // The agent's ONLY input is additionalContext. Sixteen of the nineteen
18224
18417
  // terminating paths wrote `systemMessage` — the human's field — and told
18225
18418
  // the agent nothing at all.
18226
- note
18419
+ agentNote
18227
18420
  )
18228
18421
  );
18229
18422
  process.exit(0);
@@ -18315,14 +18508,24 @@ async function runAnalyze(opts, globals) {
18315
18508
  const conversation = await readAndClearConversationBuffer(baselineSessionId);
18316
18509
  const specs = discoverSpecs();
18317
18510
  const plans = discoverPlans();
18511
+ const agentAuthoredCodeThisTurn = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0));
18512
+ const turnAuthoredCode = agentAuthoredCodeThisTurn || !!baseline && allForReview.some((f) => changedSinceBaseline(f, baseline));
18513
+ const canSeeTurnAuthorship = !!actionSummary || !!baseline;
18318
18514
  const latestPrompt = conversation?.prompts?.[conversation.prompts.length - 1]?.prompt ?? "";
18319
18515
  if (/^\s*\/verity-/i.test(latestPrompt)) {
18516
+ const setupAuthored = [
18517
+ ...actionSummary?.files_edited ?? [],
18518
+ ...actionSummary?.files_created ?? []
18519
+ ];
18520
+ if (setupAuthored.length > 0) {
18521
+ const adopted = absorbIntoBaseline(setupAuthored, baselineSessionId);
18522
+ logEvent("baseline_absorbed", { skip: "verity-command", offered: setupAuthored.length, adopted });
18523
+ }
18320
18524
  await passAndExit("Verity command \u2014 skipping analysis", "verity-command");
18321
18525
  }
18322
- if (isBareAckPrompt(latestPrompt)) {
18526
+ if (shouldSkipForBareAck({ prompt: latestPrompt, turnAuthoredCode, canSeeTurnAuthorship })) {
18323
18527
  await passAndExit("Bare acknowledgment \u2014 skipping analysis", "bare-acknowledgment");
18324
18528
  }
18325
- const agentAuthoredCodeThisTurn = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0));
18326
18529
  if (isReflectionQuestion(assistantResponse) && !agentAuthoredCodeThisTurn) {
18327
18530
  await passAndExit("Reflection-prompt turn \u2014 skipping analysis", "reflection-prompt");
18328
18531
  }
@@ -18369,7 +18572,11 @@ async function runAnalyze(opts, globals) {
18369
18572
  );
18370
18573
  }
18371
18574
  if (analysisMode === "skip") {
18372
- await passAndExit("Skip mode \u2014 no code work to analyze", "skip-mode");
18575
+ await passAndExit(
18576
+ "Skip mode \u2014 no code work to analyze",
18577
+ "skip-mode",
18578
+ turnAuthoredCode ? "capacity" : void 0
18579
+ );
18373
18580
  }
18374
18581
  let staticResults = {
18375
18582
  tool: "@codacy/analysis-cli",
@@ -18462,19 +18669,13 @@ async function runAnalyze(opts, globals) {
18462
18669
  snapshotResult = generateSnapshotDiffs(codeDelta.files);
18463
18670
  }
18464
18671
  currentCommit = getCurrentCommit();
18465
- const maxIterations = parseInt(opts.maxIterations, 10);
18466
- const iterResult = checkMaxIterations(currentCommit, maxIterations, contentHash ?? void 0);
18467
- if (iterResult.skip) await passAndExit(iterResult.skip, "iteration-cap");
18468
- iteration = iterResult.iteration;
18672
+ iteration = readIterationState(currentCommit).iteration;
18469
18673
  }
18470
18674
  }
18471
18675
  if (analysisMode === "plan") {
18472
18676
  recordAnalysisStart();
18473
18677
  currentCommit = getCurrentCommit();
18474
- const maxIterations = parseInt(opts.maxIterations, 10);
18475
- const iterResult = checkMaxIterations(currentCommit, maxIterations);
18476
- if (iterResult.skip) await passAndExit(iterResult.skip, "iteration-cap");
18477
- iteration = iterResult.iteration;
18678
+ iteration = readIterationState(currentCommit).iteration;
18478
18679
  }
18479
18680
  const contextFiles = gatherContextFiles(contextFilePaths, codeDelta.files);
18480
18681
  for (const f of codeDelta.files) {
@@ -18647,7 +18848,30 @@ async function runAnalyze(opts, globals) {
18647
18848
  hasUserPrompt: (conversation?.prompts?.length ?? 0) > 0,
18648
18849
  isTTY: process.stdout.isTTY === true
18649
18850
  });
18851
+ const excludedByReason = {};
18852
+ for (const e of codeDelta.excluded ?? []) {
18853
+ excludedByReason[e.reason] = (excludedByReason[e.reason] ?? 0) + 1;
18854
+ }
18855
+ const coverageTelemetry = {
18856
+ // git's whole answer, before ANY narrowing. The number that has never been sent.
18857
+ changed_all: allChanged.length,
18858
+ analyzable: analyzable.length,
18859
+ reviewable: reviewable.length,
18860
+ security: securityFiles.length,
18861
+ // after the allowlist, before authorship scoping and the caps
18862
+ for_review: allForReview.length,
18863
+ // what actually reaches the reviewer
18864
+ sent: codeDelta.files.length,
18865
+ // the two silent narrowings, counted separately so they can be told apart
18866
+ capped_out: actionSummary?.capped_out?.length ?? 0,
18867
+ excluded: (codeDelta.excluded ?? []).length,
18868
+ excluded_by_reason: excludedByReason,
18869
+ // was the transcript itself truncated? The 256 KB window means "this turn"
18870
+ // can quietly mean "the last 256 KB of it".
18871
+ transcript_windowed: actionSummary?.transcript_windowed ?? null
18872
+ };
18650
18873
  const requestBody = {
18874
+ coverage_telemetry: coverageTelemetry,
18651
18875
  static_results: staticResults,
18652
18876
  code_delta: codeDelta,
18653
18877
  changed_files: allForReview,
@@ -18733,6 +18957,54 @@ async function runAnalyze(opts, globals) {
18733
18957
  // replaced a population floor with ≈35% power that was sub-integer for
18734
18958
  // three-quarters of the fleet.
18735
18959
  conservation: foldConservation,
18960
+ // ⚠ VRT-52 — RECORDED, NOT APPLIED. The number nobody has.
18961
+ //
18962
+ // The whole "task-scoped delta" design space rests on an assumption that
18963
+ // has been observed exactly ONCE: that delta files routinely belong to
18964
+ // earlier work. Three designs were built on it and all three were killed
18965
+ // adversarially — two by measurement — so before another is attempted,
18966
+ // measure the base rate.
18967
+ //
18968
+ // `authored_under_earlier_goal` counts delta paths whose LAST authorship
18969
+ // event precedes the seq of the goal now in force. Both numbers come from
18970
+ // the same append-only counter (`nextSeq`), so the comparison is exact.
18971
+ //
18972
+ // Keyed on the GOAL, deliberately, not on the task id. The task classifier
18973
+ // reported `is_new_task` on two consecutive turns of one task 25 seconds
18974
+ // apart, so a task-keyed number would measure its unreliability rather
18975
+ // than the phenomenon. And this only became meaningful once `recordGoal`
18976
+ // stopped letting a bare "ok" supersede the goal — before that the seq
18977
+ // advanced every turn and this would have degenerated to "not edited this
18978
+ // turn", which is the exact mistake that sank one of the three designs.
18979
+ //
18980
+ // Changes no payload the reviewer sees, no narrowing, no verdict.
18981
+ vrt52: (() => {
18982
+ const goalSeq = memory?.projection.goal?.seq;
18983
+ if (goalSeq === void 0 || !memorySession) return { known: false };
18984
+ const lastSeq2 = new Map(
18985
+ foldDossier(memorySession.d).authored_all.map((a) => [a.path, a.last_seq])
18986
+ );
18987
+ let earlier = 0;
18988
+ let unknown = 0;
18989
+ for (const f of codeDelta.files) {
18990
+ const seen = lastSeq2.get(f.path);
18991
+ if (seen === void 0) unknown++;
18992
+ else if (seen < goalSeq) earlier++;
18993
+ }
18994
+ return {
18995
+ known: true,
18996
+ goal_seq: goalSeq,
18997
+ delta: codeDelta.files.length,
18998
+ // Files this delta carries that were last written under an EARLIER
18999
+ // instruction. If this stays near zero, VRT-52's code half is
19000
+ // unnecessary and should be closed saying so.
19001
+ authored_under_earlier_goal: earlier,
19002
+ // Delta files the dossier has no authorship record for at all —
19003
+ // pre-existing tree state, or an authorship channel the fold cannot
19004
+ // see. Reported separately so a blind spot is never counted as a zero.
19005
+ no_authorship_record: unknown
19006
+ };
19007
+ })(),
18736
19008
  // P2 — RECORDED, NOT APPLIED. What this run WOULD have reviewed if it
18737
19009
  // narrowed to the within-session increment: what changed since the last
18738
19010
  // VERDICT rather than since task start.
@@ -18765,7 +19037,20 @@ async function runAnalyze(opts, globals) {
18765
19037
  const intentContext = {};
18766
19038
  if (conversation && conversation.prompts.length > 0) {
18767
19039
  const latest = conversation.prompts[conversation.prompts.length - 1];
18768
- intentContext.user_prompt = latest.prompt;
19040
+ const goalPrompt = resolveGoalPrompt(conversation.prompts) ?? { entry: latest, turnsBack: 0 };
19041
+ intentContext.user_prompt = goalPrompt.entry.prompt;
19042
+ if (isContinuationPrompt(intentContext.user_prompt)) {
19043
+ const carried = memory?.projection.goal?.text;
19044
+ if (carried && !isContinuationPrompt(carried)) {
19045
+ intentContext.continuation_prompt = latest.prompt;
19046
+ intentContext.user_prompt = carried;
19047
+ logEvent("goal_from_dossier", { chars: carried.length });
19048
+ }
19049
+ }
19050
+ if (goalPrompt.turnsBack > 0) {
19051
+ intentContext.continuation_prompt = latest.prompt;
19052
+ logEvent("goal_walked_back", { turns_back: goalPrompt.turnsBack });
19053
+ }
18769
19054
  intentContext.session_id = latest.session_id || void 0;
18770
19055
  intentContext.prompt_captured_at = latest.captured_at || void 0;
18771
19056
  if (conversation.prompts.length > 1) {
@@ -18857,6 +19142,22 @@ async function runAnalyze(opts, globals) {
18857
19142
  const response = result.data;
18858
19143
  const decision = response.gate_decision ?? "(unrecognised)";
18859
19144
  const sentPaths = codeDelta.files.map((f) => f.path);
19145
+ let openElsewhere = [];
19146
+ if (memorySession) {
19147
+ try {
19148
+ const st = foldDossier(memorySession.d);
19149
+ openElsewhere = openBlockingElsewhere(st.statements, sentPaths, (file, line) => {
19150
+ try {
19151
+ const src = (0, import_node_fs24.readFileSync)((0, import_node_path20.join)(repoRoot(), file), "utf8").split("\n");
19152
+ const at = src[line - 1];
19153
+ return at === void 0 ? null : lineSha(at);
19154
+ } catch {
19155
+ return null;
19156
+ }
19157
+ });
19158
+ } catch {
19159
+ }
19160
+ }
18860
19161
  const reviewCoverage = {
18861
19162
  reviewed: sentPaths,
18862
19163
  // Declared drops from the stages that DO report themselves today. The other
@@ -18887,6 +19188,26 @@ async function runAnalyze(opts, globals) {
18887
19188
  stage: "extractActionSummary",
18888
19189
  kind: "capacity"
18889
19190
  })),
19191
+ // ⚠ BASELINE SCOPING — the biggest source of false NOT A CLEAN REVIEW.
19192
+ //
19193
+ // The universe is `allChanged`, git's whole dirty tree. The reviewed set is
19194
+ // scoped to what THIS SESSION authored (the VRT-26 contamination cure), so
19195
+ // every pre-existing dirty file is in the universe, absent from `reviewed`,
19196
+ // and — until now — declared by nobody. It fell through to `unaccounted`,
19197
+ // became capacity, and produced "NOT A CLEAN REVIEW: admin.js" over a file
19198
+ // that was never this session's to review.
19199
+ //
19200
+ // Measured 2026-08-04: three consecutive runs over an untouched tree gave
19201
+ // three different answers — .claude/settings.json, then admin.js, then six
19202
+ // files — because each run took a different path and each path had a
19203
+ // different idea of the universe. POLICY: not this session's work is not a
19204
+ // coverage gap, it is the cure working.
19205
+ ...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) => ({
19206
+ path,
19207
+ reason: "not-authored-this-session",
19208
+ stage: "baseline-scoping",
19209
+ kind: "policy"
19210
+ })),
18890
19211
  // The extension allowlist, and it is POLICY rather than capacity: a changed
18891
19212
  // README was never going to be reviewed, and treating that as a coverage
18892
19213
  // gap would downgrade nearly every PASS to WARN until WARN meant nothing.
@@ -18930,6 +19251,13 @@ async function runAnalyze(opts, globals) {
18930
19251
  });
18931
19252
  }
18932
19253
  let intentRepeatCount = 0;
19254
+ const priorPendingFingerprints = memorySession ? (() => {
19255
+ try {
19256
+ return foldDossier(memorySession.d).meta.recent_pending_sigs ?? [];
19257
+ } catch {
19258
+ return [];
19259
+ }
19260
+ })() : [];
18933
19261
  if (memorySession) {
18934
19262
  try {
18935
19263
  recordVerdict(memorySession.d, {
@@ -18952,7 +19280,10 @@ async function runAnalyze(opts, globals) {
18952
19280
  // What next turn reads as `emittedLast`. A suppressed turn did not
18953
19281
  // speak, so it cannot be the cause of the turn after it — which is what
18954
19282
  // keeps this from becoming a permanent gag.
18955
- emitted: !silenced
19283
+ emitted: !silenced,
19284
+ // Fingerprinted for the NEXT turn's repeat check. Reviewer pending items
19285
+ // carry no `pattern_id`, so their content is the only available key.
19286
+ pendingTexts: (response.pending_items ?? []).map((p) => String(p.description ?? p.title ?? p.reason ?? "")).filter(Boolean)
18956
19287
  });
18957
19288
  intentRepeatCount = Math.max(0, (foldDossier(memorySession.d).meta.intent_repeat?.consecutive ?? 1) - 1);
18958
19289
  } catch {
@@ -19052,9 +19383,41 @@ async function runAnalyze(opts, globals) {
19052
19383
  reverify_by: response.reverify_by
19053
19384
  });
19054
19385
  const grantNudge = grantWarning ? ` \u26A0\uFE0F ${grantWarning}` : "";
19055
- switch (decision) {
19386
+ let capReleased = false;
19387
+ let effectiveDecision = decision;
19388
+ if (decision === "FAIL") {
19389
+ const blocking = (response.findings ?? []).filter((f) => {
19390
+ const sev = String(f.severity ?? "").toLowerCase();
19391
+ return sev === "critical" || sev === "high";
19392
+ });
19393
+ const fingerprint = findingsFingerprint(blocking);
19394
+ const prior = readIterationState(currentCommit);
19395
+ const sameProblem = isSameProblem(prior.fingerprint, fingerprint);
19396
+ const nextIteration = sameProblem ? prior.iteration + 1 : 1;
19397
+ const maxIterations = parseInt(opts.maxIterations, 10);
19398
+ writeIteration(nextIteration, currentCommit, contentHash ?? void 0, fingerprint);
19399
+ iteration = nextIteration;
19400
+ if (nextIteration > maxIterations) {
19401
+ capReleased = true;
19402
+ effectiveDecision = "WARN";
19403
+ logEvent("iteration_cap_released", { iteration: nextIteration, fingerprint });
19404
+ }
19405
+ }
19406
+ if (capReleased) {
19407
+ const findings = response.findings ?? [];
19408
+ const lines = findings.slice(0, 5).map((f) => ` [${String(f.severity ?? "?").toUpperCase()}] ${String(f.title ?? f.message ?? "")} (${String(f.file ?? "?")}:${String(f.line ?? "?")})`);
19409
+ emitVerdict({
19410
+ proposed: "WARN",
19411
+ changed: skipCoverageChanged,
19412
+ coverage: reviewCoverage,
19413
+ 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.
19414
+ ${lines.join("\n")}`,
19415
+ agentContext: null,
19416
+ silenced: true
19417
+ });
19418
+ }
19419
+ switch (effectiveDecision) {
19056
19420
  case "FAIL": {
19057
- writeIteration(iteration + 1, currentCommit, contentHash ?? void 0);
19058
19421
  const assessment = response.assessment;
19059
19422
  const narrative = assessment?.narrative ?? "";
19060
19423
  const findings = response.findings ?? [];
@@ -19139,7 +19502,10 @@ ${YELLOW}${grantNudge.trim()}${NC}
19139
19502
  // Subject to the SAME cycle cut as PASS/WARN. Suppressing here is safe:
19140
19503
  // the findings themselves are rendered above by the blocking renderer,
19141
19504
  // so what the cut removes is the repeated commentary, never the defect.
19142
- agentContext: silenced ? null : agentContextFor(response, intentRepeatCount)
19505
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount, priorPendingFingerprints),
19506
+ // The coverage note is silenced with it — half a channel is still a channel.
19507
+ silenced: !!silenced,
19508
+ openElsewhere
19143
19509
  });
19144
19510
  break;
19145
19511
  }
@@ -19157,7 +19523,10 @@ ${YELLOW}${grantNudge.trim()}${NC}
19157
19523
  changed: skipCoverageChanged,
19158
19524
  coverage: reviewCoverage,
19159
19525
  userSummary,
19160
- agentContext: silenced ? null : agentContextFor(response, intentRepeatCount)
19526
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount, priorPendingFingerprints),
19527
+ // The coverage note is silenced with it — half a channel is still a channel.
19528
+ silenced: !!silenced,
19529
+ openElsewhere
19161
19530
  });
19162
19531
  break;
19163
19532
  }
@@ -19174,7 +19543,10 @@ ${YELLOW}${grantNudge.trim()}${NC}
19174
19543
  changed: skipCoverageChanged,
19175
19544
  coverage: reviewCoverage,
19176
19545
  userSummary,
19177
- agentContext: silenced ? null : agentContextFor(response, intentRepeatCount)
19546
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount, priorPendingFingerprints),
19547
+ // The coverage note is silenced with it — half a channel is still a channel.
19548
+ silenced: !!silenced,
19549
+ openElsewhere
19178
19550
  });
19179
19551
  break;
19180
19552
  }
@@ -19288,8 +19660,8 @@ async function runReview(opts, globals) {
19288
19660
  for (const p of specPaths) {
19289
19661
  if (!(0, import_node_fs26.existsSync)(p)) continue;
19290
19662
  try {
19291
- const { readFileSync: readFileSync15 } = await import("node:fs");
19292
- const content = readFileSync15(p, "utf-8");
19663
+ const { readFileSync: readFileSync16 } = await import("node:fs");
19664
+ const content = readFileSync16(p, "utf-8");
19293
19665
  specs.push({ path: p, content: content.slice(0, 10240) });
19294
19666
  } catch {
19295
19667
  }
@@ -20975,7 +21347,7 @@ function registerTelemetryCommands(program2) {
20975
21347
  }
20976
21348
 
20977
21349
  // src/cli.ts
20978
- program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.8cc9c66").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async () => {
21350
+ program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.902dbd9").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async () => {
20979
21351
  try {
20980
21352
  await foldLegacyLocalCredential();
20981
21353
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codacy/verity-cli",
3
- "version": "0.28.1-experimental.8cc9c66",
3
+ "version": "0.28.1-experimental.902dbd9",
4
4
  "description": "CLI for Verity quality gate service",
5
5
  "homepage": "https://verity.md",
6
6
  "bugs": {