@codacy/verity-cli 0.28.1-experimental.055cd97 → 0.28.1-experimental.10cb8ce

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 +834 -202
  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;
@@ -10924,8 +10923,8 @@ function filterReviewable(files) {
10924
10923
  const ext = (0, import_node_path3.extname)(f).slice(1);
10925
10924
  if (ANALYZABLE_EXTENSIONS.has(ext)) return false;
10926
10925
  if (REVIEWABLE_EXTENSIONS.has(ext)) return true;
10927
- const basename4 = f.split("/").pop() ?? "";
10928
- if (REVIEWABLE_FILENAMES.has(basename4)) return true;
10926
+ const basename3 = f.split("/").pop() ?? "";
10927
+ if (REVIEWABLE_FILENAMES.has(basename3)) return true;
10929
10928
  if (REVIEWABLE_PATH_PATTERNS.some((p) => p.test(f))) return true;
10930
10929
  return false;
10931
10930
  });
@@ -11195,7 +11194,15 @@ async function resolveServiceUrlDetailed(flagUrl) {
11195
11194
  if (mdUrl) {
11196
11195
  return { ok: true, data: { url: mdUrl, source: "verity_md" } };
11197
11196
  }
11198
- return { ok: false, error: "No Verity service URL found. Run /verity-setup to configure." };
11197
+ return {
11198
+ ok: false,
11199
+ error: 'No Verity service URL found. Run "verity login" to get started, or /verity-setup to configure this project.'
11200
+ };
11201
+ }
11202
+ async function resolveServiceUrlForAuth(flagUrl) {
11203
+ const strict = await resolveServiceUrlDetailed(flagUrl);
11204
+ if (strict.ok) return strict.data;
11205
+ return { url: DEFAULT_SERVICE_URL, source: "default" };
11199
11206
  }
11200
11207
  async function resolveServiceUrl(flagUrl) {
11201
11208
  const result = await resolveServiceUrlDetailed(flagUrl);
@@ -11234,7 +11241,10 @@ async function resolveToken(flagToken) {
11234
11241
  data: { token: local.token, source: "local", userId: local.userId, email: local.email }
11235
11242
  };
11236
11243
  }
11237
- return { ok: false, error: "No Verity token found. Run /verity-setup to configure." };
11244
+ return {
11245
+ ok: false,
11246
+ error: 'No Verity token found. Run "verity login" to sign in, or /verity-setup to set up this project.'
11247
+ };
11238
11248
  }
11239
11249
  async function whoami(token, serviceUrl, verbose) {
11240
11250
  return apiRequest({
@@ -11671,16 +11681,81 @@ function registerAuthCommands(program2) {
11671
11681
  });
11672
11682
  }
11673
11683
 
11684
+ // src/lib/login-report.ts
11685
+ async function reportLoginOutcome(out, opts = {}) {
11686
+ const identity = out.email ?? (out.userId != null ? `user #${out.userId}` : "your account");
11687
+ printInfo(`Logged in as ${identity}. \u2713`);
11688
+ printInfo(` Access granted to ${out.repoCount} ${out.repoCount === 1 ? "repository" : "repositories"}.`);
11689
+ if (out.expiresAt) {
11690
+ printInfo(` This login expires on ${out.expiresAt.slice(0, 10)} \u2014 "verity login" again to renew.`);
11691
+ printInfo(' See your machines with "verity sessions list"; sign out with "verity logout".');
11692
+ }
11693
+ if (out.repoCount > 0) {
11694
+ printInfo(" Runs, history, and cloud memory now sync to Verity everywhere you have write access.");
11695
+ }
11696
+ if (out.prunedCredentials > 0) {
11697
+ printVerbose(`Pruned ${out.prunedCredentials} superseded per-repository credential line(s).`, opts.verbose);
11698
+ } else if (out.prunedCredentials < 0) {
11699
+ printWarn(" Could not rewrite ~/.verity/credentials: superseded per-repository tokens remain and");
11700
+ printWarn(" will keep taking precedence over this login in their own repositories.");
11701
+ printInfo(` Check the file's permissions; the next successful "verity login" retries the cleanup.`);
11702
+ }
11703
+ if (out.repoCount === 0) {
11704
+ printWarn("The Verity GitHub App is not installed on any account you can access.");
11705
+ printInfo(" Install it (and grant your repositories), then re-run verity login:");
11706
+ printInfo(` ${githubAppInstallUrl(null)}`);
11707
+ return;
11708
+ }
11709
+ const remote = opts.remote;
11710
+ if (!remote) return;
11711
+ const who = await whoami(out.token, out.serviceUrl, opts.verbose);
11712
+ if (who.ok && who.data.grant_status != null) {
11713
+ printInfo(" \u2713 This repository is covered.");
11714
+ } else if (!who.ok) {
11715
+ printWarn(` Could not confirm this repository's coverage (${who.error}) \u2014 verity status will show it.`);
11716
+ } else {
11717
+ const parsed = parseRemote(remote);
11718
+ const installUrl = githubAppInstallUrl(parsed ? await githubAccountId(parsed.owner) : null);
11719
+ printWarn(` This repository (${parsed ? `${parsed.owner}/${parsed.repo}` : remote}) is NOT covered by your grants.`);
11720
+ printInfo(" Grant the Verity GitHub App access to it, then re-run verity login:");
11721
+ printInfo(` ${installUrl}`);
11722
+ }
11723
+ const rec = await readGlobalCredential(remote);
11724
+ if (rec && rec.token !== out.token) {
11725
+ const otherBackend = rec.serviceUrl != null && rec.serviceUrl !== out.serviceUrl;
11726
+ const otherIdentity = rec.userId != null && out.userId != null && rec.userId !== out.userId;
11727
+ if (otherBackend) {
11728
+ printWarn(` Note: this repository is pinned to a different Verity service (${rec.serviceUrl})`);
11729
+ printWarn(" by its own credential line, which takes precedence here \u2014 this login does not");
11730
+ printWarn(" change that. To move the repository, remove its line from ~/.verity/credentials.");
11731
+ printWarn(' Until that line is removed, "verity login" here cannot fast-path and will run');
11732
+ printWarn(" the full GitHub flow every time.");
11733
+ } else if (otherIdentity) {
11734
+ printWarn(" Note: this repository uses a different account's credential, which takes");
11735
+ printWarn(' precedence here \u2014 this login leaves it in place, and "verity login" in this');
11736
+ printWarn(" repository will report that account. Remove its line from ~/.verity/credentials");
11737
+ printWarn(" only if you want this repository on the login you just completed.");
11738
+ } else {
11739
+ const kind = rec.userId != null ? "superseded per-repository" : "anonymous project-specific";
11740
+ printWarn(` Note: this repository has a ${kind} credential that takes`);
11741
+ printWarn(" precedence here. Remove its line from ~/.verity/credentials to use your login.");
11742
+ printWarn(' Until then, "verity login" in this repository re-runs the full GitHub flow');
11743
+ printWarn(" every time.");
11744
+ }
11745
+ }
11746
+ }
11747
+
11674
11748
  // src/commands/login.ts
11675
11749
  function registerLoginCommand(program2) {
11676
11750
  program2.command("login").description("Log in to Verity (one GitHub login grants access to all your repositories)").option("--force", "Re-authenticate even if already logged in").action(async (opts) => {
11677
11751
  const globals = program2.opts();
11678
- const urlResult = await resolveServiceUrlDetailed(globals.serviceUrl);
11679
- if (!urlResult.ok) {
11680
- printError(urlResult.error);
11681
- process.exit(1);
11752
+ const resolution = await resolveServiceUrlForAuth(globals.serviceUrl);
11753
+ if (resolution.source === "default") {
11754
+ printInfo(`No Verity service configured on this machine \u2014 using the default: ${resolution.url}`);
11755
+ } else {
11756
+ printVerbose(`Service URL from ${resolution.source}: ${resolution.url}`, globals.verbose);
11682
11757
  }
11683
- const heal = await maybeHealServiceUrl(urlResult.data, globals.verbose);
11758
+ const heal = await maybeHealServiceUrl(resolution, globals.verbose);
11684
11759
  const serviceUrl = heal.serviceUrl;
11685
11760
  if (heal.healed) {
11686
11761
  printInfo(" Completing login updates ~/.verity/credentials against the live service.");
@@ -11721,65 +11796,7 @@ function registerLoginCommand(program2) {
11721
11796
  printError(`Login failed: ${result.error}`);
11722
11797
  process.exit(1);
11723
11798
  }
11724
- const out = result.data;
11725
- const identity = out.email ?? (out.userId != null ? `user #${out.userId}` : "your account");
11726
- printInfo(`Logged in as ${identity}. \u2713`);
11727
- printInfo(` Access granted to ${out.repoCount} ${out.repoCount === 1 ? "repository" : "repositories"}.`);
11728
- if (out.expiresAt) {
11729
- printInfo(` This login expires on ${out.expiresAt.slice(0, 10)} \u2014 "verity login" again to renew.`);
11730
- printInfo(' See your machines with "verity sessions list"; sign out with "verity logout".');
11731
- }
11732
- printInfo(" Runs, history, and cloud memory now sync to Verity everywhere you have write access.");
11733
- if (out.prunedCredentials > 0) {
11734
- printVerbose(`Pruned ${out.prunedCredentials} superseded per-repository credential line(s).`, globals.verbose);
11735
- } else if (out.prunedCredentials < 0) {
11736
- printWarn(" Could not rewrite ~/.verity/credentials: superseded per-repository tokens remain and");
11737
- printWarn(" will keep taking precedence over this login in their own repositories.");
11738
- printInfo(` Check the file's permissions; the next successful "verity login" retries the cleanup.`);
11739
- }
11740
- if (out.repoCount === 0) {
11741
- printWarn("The Verity GitHub App is not installed on any account you can access.");
11742
- printInfo(` Install it (and grant your repositories), then re-run verity login:`);
11743
- printInfo(` ${githubAppInstallUrl(null)}`);
11744
- return;
11745
- }
11746
- if (remote) {
11747
- const who = await whoami(out.token, out.serviceUrl, globals.verbose);
11748
- if (who.ok && who.data.grant_status != null) {
11749
- printInfo(" \u2713 This repository is covered.");
11750
- } else if (!who.ok) {
11751
- printWarn(` Could not confirm this repository's coverage (${who.error}) \u2014 verity status will show it.`);
11752
- } else {
11753
- const parsed = parseRemote(remote);
11754
- const installUrl = githubAppInstallUrl(parsed ? await githubAccountId(parsed.owner) : null);
11755
- printWarn(` This repository (${parsed ? `${parsed.owner}/${parsed.repo}` : remote}) is NOT covered by your grants.`);
11756
- printInfo(" Grant the Verity GitHub App access to it, then re-run verity login:");
11757
- printInfo(` ${installUrl}`);
11758
- }
11759
- const rec = await readGlobalCredential(remote);
11760
- if (rec && rec.token !== out.token) {
11761
- const otherBackend = rec.serviceUrl != null && rec.serviceUrl !== out.serviceUrl;
11762
- const otherIdentity = rec.userId != null && out.userId != null && rec.userId !== out.userId;
11763
- if (otherBackend) {
11764
- printWarn(` Note: this repository is pinned to a different Verity service (${rec.serviceUrl})`);
11765
- printWarn(" by its own credential line, which takes precedence here \u2014 this login does not");
11766
- printWarn(" change that. To move the repository, remove its line from ~/.verity/credentials.");
11767
- printWarn(' Until that line is removed, "verity login" here cannot fast-path and will run');
11768
- printWarn(" the full GitHub flow every time.");
11769
- } else if (otherIdentity) {
11770
- printWarn(" Note: this repository uses a different account's credential, which takes");
11771
- printWarn(' precedence here \u2014 this login leaves it in place, and "verity login" in this');
11772
- printWarn(" repository will report that account. Remove its line from ~/.verity/credentials");
11773
- printWarn(" only if you want this repository on the login you just completed.");
11774
- } else {
11775
- const kind = rec.userId != null ? "superseded per-repository" : "anonymous project-specific";
11776
- printWarn(` Note: this repository has a ${kind} credential that takes`);
11777
- printWarn(" precedence here. Remove its line from ~/.verity/credentials to use your login.");
11778
- printWarn(' Until then, "verity login" in this repository re-runs the full GitHub flow');
11779
- printWarn(" every time.");
11780
- }
11781
- }
11782
- }
11799
+ await reportLoginOutcome(result.data, { remote: remote || void 0, verbose: globals.verbose });
11783
11800
  });
11784
11801
  }
11785
11802
 
@@ -11914,12 +11931,8 @@ async function auth(globals) {
11914
11931
  printError(tokenResult.error);
11915
11932
  process.exit(1);
11916
11933
  }
11917
- const urlResult = await resolveServiceUrl(globals.serviceUrl);
11918
- if (!urlResult.ok) {
11919
- printError(urlResult.error);
11920
- process.exit(1);
11921
- }
11922
- return { token: tokenResult.data.token, serviceUrl: urlResult.data };
11934
+ const resolution = await resolveServiceUrlForAuth(globals.serviceUrl);
11935
+ return { token: tokenResult.data.token, serviceUrl: resolution.url };
11923
11936
  }
11924
11937
  function explain(error) {
11925
11938
  if (error.startsWith("FORBIDDEN")) {
@@ -13591,6 +13604,143 @@ var import_node_fs10 = require("node:fs");
13591
13604
  var import_node_crypto5 = require("node:crypto");
13592
13605
  var import_node_path11 = require("node:path");
13593
13606
 
13607
+ // src/lib/skip-detection.ts
13608
+ function isBareAckPrompt(prompt) {
13609
+ if (typeof prompt !== "string") return false;
13610
+ const trimmed = prompt.trim();
13611
+ if (trimmed.length === 0) return false;
13612
+ if (trimmed.length > 20) return false;
13613
+ 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;
13614
+ return bareAckPattern.test(trimmed);
13615
+ }
13616
+ function isContinuationPrompt(prompt) {
13617
+ if (typeof prompt !== "string") return false;
13618
+ const trimmed = prompt.trim();
13619
+ if (trimmed.length === 0) return false;
13620
+ if (trimmed.length > 24) return false;
13621
+ 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;
13622
+ return continuation.test(trimmed) || isBareAckPrompt(trimmed);
13623
+ }
13624
+ function resolveGoalPrompt(prompts) {
13625
+ if (prompts.length === 0) return null;
13626
+ const latest = prompts[prompts.length - 1];
13627
+ if (!isContinuationPrompt(latest.prompt)) return { entry: latest, turnsBack: 0 };
13628
+ for (let i = prompts.length - 2; i >= 0; i--) {
13629
+ if (!isContinuationPrompt(prompts[i].prompt)) {
13630
+ return { entry: prompts[i], turnsBack: prompts.length - 1 - i };
13631
+ }
13632
+ }
13633
+ return { entry: latest, turnsBack: 0 };
13634
+ }
13635
+ function isReflectionQuestion(response) {
13636
+ if (!response || typeof response !== "string") return false;
13637
+ const markers = [
13638
+ /reflection\s+for\s+future\s+agents/i,
13639
+ /what(?:'s|\s+is)\s+one\s+thing\s+you\s+learned/i,
13640
+ /say\s+['"]?skip['"]?\s+to\s+skip/i,
13641
+ /quick\s+reflection\s+question/i,
13642
+ // Post-flip (VRT-21): the agent drafts the reflection itself and, when
13643
+ // interactive, asks the user to confirm/correct before recording. That
13644
+ // turn authors no code either, so it's still a reflection turn.
13645
+ /reflection\s+draft/i,
13646
+ /confirm,?\s+correct,?\s+or\s+add/i
13647
+ ];
13648
+ return markers.some((m) => m.test(response));
13649
+ }
13650
+ function isMetaTaskLabel(label2) {
13651
+ if (label2 === null || label2 === void 0) return false;
13652
+ if (typeof label2 !== "string") return false;
13653
+ const trimmed = label2.trim();
13654
+ if (trimmed.length === 0) return true;
13655
+ const metaPatterns = [
13656
+ /^verity\s+[\w-]+\s+response$/i,
13657
+ // "Verity reflect response"
13658
+ /^simple user response$/i,
13659
+ /^verity\s+command$/i,
13660
+ // "Verity command"
13661
+ /^user\s+(question|reply|response|ack)$/i
13662
+ ];
13663
+ return metaPatterns.some((p) => p.test(trimmed));
13664
+ }
13665
+ function shouldSkipForBareAck(input) {
13666
+ if (!isBareAckPrompt(input.prompt)) return false;
13667
+ if (input.turnAuthoredCode) return false;
13668
+ return input.canSeeTurnAuthorship;
13669
+ }
13670
+
13671
+ // src/lib/pending-repeat.ts
13672
+ var STOP = /* @__PURE__ */ new Set([
13673
+ "the",
13674
+ "and",
13675
+ "that",
13676
+ "this",
13677
+ "with",
13678
+ "from",
13679
+ "have",
13680
+ "been",
13681
+ "were",
13682
+ "what",
13683
+ "when",
13684
+ "which",
13685
+ "their",
13686
+ "there",
13687
+ "these",
13688
+ "those",
13689
+ "would",
13690
+ "could",
13691
+ "should",
13692
+ "must",
13693
+ "will",
13694
+ "also",
13695
+ "just",
13696
+ "only",
13697
+ "into",
13698
+ "over",
13699
+ "than",
13700
+ "then",
13701
+ "them",
13702
+ "some",
13703
+ "such",
13704
+ "more",
13705
+ "most",
13706
+ "other",
13707
+ "about",
13708
+ "after",
13709
+ "before",
13710
+ "since",
13711
+ "because",
13712
+ "while",
13713
+ "where",
13714
+ "whether",
13715
+ "ensure",
13716
+ "confirm",
13717
+ "verify",
13718
+ "check"
13719
+ ]);
13720
+ function pendingTokens(text) {
13721
+ if (!text || typeof text !== "string") return [];
13722
+ const out = /* @__PURE__ */ new Set();
13723
+ for (const raw of text.toLowerCase().split(/[^a-z0-9]+/)) {
13724
+ if (raw.length <= 3) continue;
13725
+ if (STOP.has(raw)) continue;
13726
+ out.add(raw);
13727
+ }
13728
+ return [...out].sort();
13729
+ }
13730
+ var REPEAT_THRESHOLD = 0.3;
13731
+ function overlapCoefficient(a, b) {
13732
+ if (a.length === 0 || b.length === 0) return 0;
13733
+ const setB = new Set(b);
13734
+ let shared = 0;
13735
+ for (const t of a) if (setB.has(t)) shared++;
13736
+ return shared / Math.min(a.length, b.length);
13737
+ }
13738
+ function isRepeatOfAny(text, priorFingerprints, threshold = REPEAT_THRESHOLD) {
13739
+ const tokens = pendingTokens(text);
13740
+ if (tokens.length === 0) return false;
13741
+ return priorFingerprints.some((prior) => overlapCoefficient(tokens, prior) >= threshold);
13742
+ }
13743
+
13594
13744
  // src/lib/dossier.ts
13595
13745
  var import_node_fs9 = require("node:fs");
13596
13746
  var import_node_crypto4 = require("node:crypto");
@@ -13599,6 +13749,7 @@ var MAX_LINE_BYTES = 4096;
13599
13749
  var MAX_GOAL_CHARS = 2e3;
13600
13750
  var GOAL_KEEP = 8;
13601
13751
  var GOAL_TOTAL_CAP = 32;
13752
+ var RECENT_PENDING_CAP = 20;
13602
13753
  var HASH_WIDTH = 16;
13603
13754
  var AUTHORED_CAP = 300;
13604
13755
  var NOT_MINE_CAP = 300;
@@ -13967,6 +14118,10 @@ function reduce(state, events, now) {
13967
14118
  consecutiveIdle: ev.idle === false ? 0 : (state.meta.channel?.consecutiveIdle ?? 0) + 1
13968
14119
  };
13969
14120
  state.meta.last_adjudication = ev.intent_verdict ? { verdict: ev.intent_verdict, score: ev.intent_score ?? null, at: ev.at, decision: ev.decision } : void 0;
14121
+ if (Array.isArray(ev.pending_sigs) && ev.pending_sigs.length > 0) {
14122
+ const prior = state.meta.recent_pending_sigs ?? [];
14123
+ state.meta.recent_pending_sigs = [...prior, ...ev.pending_sigs].slice(-RECENT_PENDING_CAP);
14124
+ }
13970
14125
  if (ev.intent_sig) {
13971
14126
  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
14127
  } else {
@@ -14695,7 +14850,19 @@ function sessionDossier(token, sessionId) {
14695
14850
  const d = openDossier(identity);
14696
14851
  return d ? { d, identity } : null;
14697
14852
  }
14853
+ function hasActiveGoal(d) {
14854
+ try {
14855
+ if (!(0, import_node_fs10.existsSync)(d.eventsPath)) return false;
14856
+ return (0, import_node_fs10.readFileSync)(d.eventsPath, "utf8").includes('"k":"goal"');
14857
+ } catch {
14858
+ return false;
14859
+ }
14860
+ }
14698
14861
  function recordGoal(d, prompt, source = "prompt") {
14862
+ if (source === "prompt" && isContinuationPrompt(prompt) && hasActiveGoal(d)) {
14863
+ appendEvent(d, { k: "goal_continue", text: prompt.slice(0, 64) });
14864
+ return;
14865
+ }
14699
14866
  const text = prompt.slice(0, MAX_GOAL_CHARS);
14700
14867
  appendEvent(d, {
14701
14868
  k: "goal",
@@ -14811,6 +14978,13 @@ function recordVerdict(d, v) {
14811
14978
  branch: v.branch,
14812
14979
  decision: v.decision,
14813
14980
  ...sig && { intent_sig: sig },
14981
+ // Fingerprints of the pending items this verdict delivered, so the NEXT turn
14982
+ // can tell a repeat from a new requirement. Only recorded when the channel
14983
+ // actually spoke — a silenced turn delivered nothing, so nothing was "said
14984
+ // before" and labelling the next turn's items as repeats would be a lie.
14985
+ ...v.emitted === true && v.pendingTexts && v.pendingTexts.length > 0 && {
14986
+ pending_sigs: v.pendingTexts.slice(0, 8).map((t) => pendingTokens(t).slice(0, 16))
14987
+ },
14814
14988
  emitted: v.emitted === true,
14815
14989
  idle: v.idle !== false,
14816
14990
  ...v.intent?.verdict && { intent_verdict: v.intent.verdict },
@@ -15118,6 +15292,8 @@ function collectCodeDelta(files, opts) {
15118
15292
  let totalSize = 0;
15119
15293
  let truncationReason = null;
15120
15294
  const droppedPaths = [];
15295
+ const excluded = [];
15296
+ const exclude = (path, reason) => excluded.push({ path, reason, stage: "collectCodeDelta", kind: "capacity" });
15121
15297
  for (const filepath of sorted) {
15122
15298
  if (result.length >= maxFiles) {
15123
15299
  truncationReason ??= "max_files";
@@ -15125,14 +15301,21 @@ function collectCodeDelta(files, opts) {
15125
15301
  continue;
15126
15302
  }
15127
15303
  const resolved = resolveFile(filepath);
15128
- if (!resolved) continue;
15304
+ if (!resolved) {
15305
+ exclude(filepath, "path-not-resolvable");
15306
+ continue;
15307
+ }
15129
15308
  let size;
15130
15309
  try {
15131
15310
  size = (0, import_node_fs11.statSync)(resolved).size;
15132
15311
  } catch {
15312
+ exclude(filepath, "not-stattable");
15313
+ continue;
15314
+ }
15315
+ if (size > maxFileBytes) {
15316
+ exclude(filepath, `over-file-size-limit-${maxFileBytes}b`);
15133
15317
  continue;
15134
15318
  }
15135
- if (size > maxFileBytes) continue;
15136
15319
  if (totalSize + size > maxTotalBytes) {
15137
15320
  truncationReason ??= "max_total_bytes";
15138
15321
  const idx = sorted.indexOf(filepath);
@@ -15143,6 +15326,7 @@ function collectCodeDelta(files, opts) {
15143
15326
  try {
15144
15327
  content = (0, import_node_fs11.readFileSync)(resolved, "utf-8");
15145
15328
  } catch {
15329
+ exclude(filepath, "not-readable");
15146
15330
  continue;
15147
15331
  }
15148
15332
  totalSize += size;
@@ -15156,10 +15340,14 @@ function collectCodeDelta(files, opts) {
15156
15340
  (sum, f) => sum + f.content.split("\n").length,
15157
15341
  0
15158
15342
  );
15343
+ for (const path of droppedPaths) {
15344
+ exclude(path, truncationReason === "max_files" ? "max-files-cap" : "max-total-bytes-cap");
15345
+ }
15159
15346
  return {
15160
15347
  files: result,
15161
15348
  total_lines: totalLines,
15162
15349
  total_files: result.length,
15350
+ excluded,
15163
15351
  ...truncationReason && {
15164
15352
  truncated: {
15165
15353
  reason: truncationReason,
@@ -15453,6 +15641,34 @@ ${addedLines}`,
15453
15641
  }
15454
15642
  return { diffs, has_baseline: true };
15455
15643
  }
15644
+ function absorbIntoBaseline(paths, sessionId) {
15645
+ const baseline = readBaseline(sessionId);
15646
+ if (!baseline || paths.length === 0) return 0;
15647
+ const dir = sessionDir(sessionKey(baseline.session_id));
15648
+ let adopted = 0;
15649
+ const dirty = new Set(baseline.dirty_paths);
15650
+ for (const p of paths) {
15651
+ try {
15652
+ const content = safeReadForMirror(projectPath(p));
15653
+ if (content === null) continue;
15654
+ const dest = mirrorPath(dir, p);
15655
+ (0, import_node_fs13.mkdirSync)((0, import_node_path14.dirname)(dest), { recursive: true });
15656
+ (0, import_node_fs13.writeFileSync)(dest, content);
15657
+ dirty.add(p);
15658
+ adopted++;
15659
+ } catch {
15660
+ }
15661
+ }
15662
+ if (adopted === 0) return 0;
15663
+ try {
15664
+ const updated = { ...baseline, dirty_paths: [...dirty] };
15665
+ (0, import_node_fs13.writeFileSync)(manifestPath(dir), JSON.stringify(updated));
15666
+ preImageCache.delete(baseline);
15667
+ } catch {
15668
+ return 0;
15669
+ }
15670
+ return adopted;
15671
+ }
15456
15672
  function changedSinceBaseline(repoRelPath, baseline) {
15457
15673
  const pre = preImage(repoRelPath, baseline);
15458
15674
  let current;
@@ -16329,40 +16545,40 @@ function narrowToRecent(files, sessionId) {
16329
16545
  });
16330
16546
  return recent.length > 0 ? recent : files;
16331
16547
  }
16332
- function readIteration(currentCommit, _contentHash) {
16333
- if (!(0, import_node_fs15.existsSync)(ITERATION_FILE)) return 1;
16548
+ function readIterationState(currentCommit) {
16549
+ if (!(0, import_node_fs15.existsSync)(ITERATION_FILE)) return { iteration: 1, fingerprint: null };
16334
16550
  try {
16335
16551
  const stored = (0, import_node_fs15.readFileSync)(ITERATION_FILE, "utf-8").trim();
16336
16552
  const parts = stored.split(":");
16337
16553
  const iter = parseInt(parts[0], 10);
16338
16554
  const storedCommit = parts[1] ?? "";
16339
16555
  const storedTimestamp = parseInt(parts[2] ?? "0", 10);
16340
- if (isNaN(iter)) return 1;
16341
- if (storedCommit !== currentCommit) return 1;
16556
+ const fingerprint = parts.slice(3).join(":") || null;
16557
+ if (isNaN(iter)) return { iteration: 1, fingerprint: null };
16558
+ if (storedCommit !== currentCommit) return { iteration: 1, fingerprint: null };
16342
16559
  if (storedTimestamp > 0) {
16343
16560
  const elapsed = Math.floor(Date.now() / 1e3) - storedTimestamp;
16344
- if (elapsed > 600) return 1;
16561
+ if (elapsed > 600) return { iteration: 1, fingerprint: null };
16345
16562
  }
16346
- return iter;
16563
+ return { iteration: iter, fingerprint };
16347
16564
  } catch {
16348
- return 1;
16565
+ return { iteration: 1, fingerprint: null };
16349
16566
  }
16350
16567
  }
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 };
16568
+ function findingsFingerprint(findings) {
16569
+ const keys = findings.map((f) => `${String(f.pattern_id ?? "?")}|${String(f.file ?? "?")}`).filter((k) => k !== "?|?");
16570
+ return [...new Set(keys)].sort().join(",");
16361
16571
  }
16362
- function writeIteration(iteration, commit, _contentHash) {
16572
+ function isSameProblem(previous, current) {
16573
+ if (!previous || !current) return false;
16574
+ const prev = new Set(previous.split(","));
16575
+ return current.split(",").some((k) => prev.has(k));
16576
+ }
16577
+ function writeIteration(iteration, commit, _contentHash, fingerprint) {
16363
16578
  (0, import_node_fs15.mkdirSync)(VERITY_DIR, { recursive: true });
16364
16579
  const ts = Math.floor(Date.now() / 1e3);
16365
- (0, import_node_fs15.writeFileSync)(ITERATION_FILE, `${iteration}:${commit}:${ts}`);
16580
+ const fp = fingerprint ? `:${fingerprint}` : "";
16581
+ (0, import_node_fs15.writeFileSync)(ITERATION_FILE, `${iteration}:${commit}:${ts}${fp}`);
16366
16582
  }
16367
16583
 
16368
16584
  // src/lib/static-analysis.ts
@@ -16611,7 +16827,7 @@ function resolveTaskContext(opts) {
16611
16827
  // src/lib/cli-version.ts
16612
16828
  function cliVersion() {
16613
16829
  try {
16614
- return true ? "0.28.1-experimental.055cd97" : "dev";
16830
+ return true ? "0.28.1-experimental.10cb8ce" : "dev";
16615
16831
  } catch {
16616
16832
  return "dev";
16617
16833
  }
@@ -17156,6 +17372,86 @@ function checkConservation(changedFiles, result, repoRoot2) {
17156
17372
  };
17157
17373
  }
17158
17374
 
17375
+ // src/lib/verdict.ts
17376
+ function reconcileCoverage(changed, coverage) {
17377
+ const changedSet = new Set(changed);
17378
+ const reviewed = coverage.reviewed.filter((p) => changedSet.has(p));
17379
+ const claimed = /* @__PURE__ */ new Set([...reviewed, ...coverage.notReviewed.map((n) => n.path)]);
17380
+ const unaccounted = [...changedSet].filter((p) => !claimed.has(p)).sort();
17381
+ const notReviewed = [
17382
+ ...coverage.notReviewed.filter((n) => changedSet.has(n.path)),
17383
+ ...unaccounted.map((path) => ({
17384
+ path,
17385
+ reason: "unaccounted",
17386
+ // Named so the eventual bug report writes itself: some stage removed this
17387
+ // path and did not say so.
17388
+ stage: "unknown-stage",
17389
+ // An undeclared drop is CAPACITY by default. A stage that cannot be
17390
+ // bothered to say why it dropped a file does not get the benefit of the
17391
+ // doubt — that default is what makes forgetting expensive.
17392
+ kind: "capacity"
17393
+ }))
17394
+ ];
17395
+ return {
17396
+ coverage: { reviewed: [...new Set(reviewed)].sort(), notReviewed },
17397
+ unaccounted,
17398
+ balances: unaccounted.length === 0
17399
+ };
17400
+ }
17401
+ function resolveVerdict(proposed, coverage) {
17402
+ if (proposed === "FAIL") return "FAIL";
17403
+ const blocking = coverage.notReviewed.filter((n) => (n.kind ?? "capacity") !== "policy");
17404
+ if (blocking.length === 0) return proposed;
17405
+ return "WARN";
17406
+ }
17407
+ function describeCoverage(coverage, maxPaths = 5) {
17408
+ const relevant = coverage.notReviewed.filter((n) => (n.kind ?? "capacity") !== "policy");
17409
+ if (relevant.length === 0) return null;
17410
+ const byReason = /* @__PURE__ */ new Map();
17411
+ for (const n of relevant) {
17412
+ const key = `${n.reason}`;
17413
+ const list = byReason.get(key) ?? [];
17414
+ list.push(n.path);
17415
+ byReason.set(key, list);
17416
+ }
17417
+ const lines = [];
17418
+ for (const [reason, paths] of [...byReason.entries()].sort()) {
17419
+ const shown = paths.slice(0, maxPaths).join(", ");
17420
+ const more = paths.length > maxPaths ? ` (+${paths.length - maxPaths} more)` : "";
17421
+ lines.push(` ${paths.length} not reviewed \u2014 ${reason}: ${shown}${more}`);
17422
+ }
17423
+ return `NOT A CLEAN REVIEW. ${relevant.length} changed file(s) never reached the reviewer, so this verdict does not cover them:
17424
+ ${lines.join("\n")}
17425
+ Treat those files as UNCHECKED, not as approved.`;
17426
+ }
17427
+ function openBlockingElsewhere(statements, reviewedNow, lineShaAt) {
17428
+ const reviewed = new Set(reviewedNow);
17429
+ const out = [];
17430
+ const seen = /* @__PURE__ */ new Set();
17431
+ for (const s of statements) {
17432
+ if (s.outcome !== "open") continue;
17433
+ if (s.register !== "BLOCK") continue;
17434
+ if (s.carried) continue;
17435
+ if (reviewed.has(s.file)) continue;
17436
+ if (!s.line_sha) continue;
17437
+ if (lineShaAt(s.file, s.line) !== s.line_sha) continue;
17438
+ const key = `${s.file}::${s.pattern_id}`;
17439
+ if (seen.has(key)) continue;
17440
+ seen.add(key);
17441
+ out.push({ file: s.file, line: s.line, pattern_id: s.pattern_id });
17442
+ }
17443
+ return out;
17444
+ }
17445
+ function describeOpenElsewhere(open) {
17446
+ if (open.length === 0) return null;
17447
+ const lines = open.slice(0, 5).map((o) => ` ${o.file}:${o.line} [${o.pattern_id}]`);
17448
+ const more = open.length > 5 ? `
17449
+ (+${open.length - 5} more)` : "";
17450
+ return `STILL OPEN ELSEWHERE. ${open.length} blocking finding(s) Verity raised earlier are still present in files this run did not review:
17451
+ ${lines.join("\n")}${more}
17452
+ This verdict covers the current change only. The tree is not clean.`;
17453
+ }
17454
+
17159
17455
  // src/lib/channel.ts
17160
17456
  var MAX_AGENT_CONTEXT_CHARS = 1500;
17161
17457
  var MAX_AGENT_ITEMS = 5;
@@ -17205,9 +17501,13 @@ function buildAgentContext(input) {
17205
17501
  }
17206
17502
  for (const p of input.pendingItems ?? []) {
17207
17503
  if (lines.length >= MAX_AGENT_ITEMS) break;
17504
+ if (p.pattern_id === "intent-misalignment") continue;
17208
17505
  const text = p.description ?? p.title ?? p.reason;
17209
17506
  if (!text) continue;
17210
- lines.push(renderItem("", text, p.pattern_id, p.file, p.line));
17507
+ const seenBefore = isRepeatOfAny(text, input.priorPendingFingerprints ?? []);
17508
+ lines.push(
17509
+ 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)" : "")
17510
+ );
17211
17511
  }
17212
17512
  if (lines.length === 0) return null;
17213
17513
  const body = `${REPORT_PREFIX}
@@ -17243,6 +17543,39 @@ function channelSilence(input) {
17243
17543
  return null;
17244
17544
  }
17245
17545
 
17546
+ // src/lib/emit.ts
17547
+ var YELLOW2 = "\x1B[33m";
17548
+ var NC2 = "\x1B[0m";
17549
+ function emitVerdict(input) {
17550
+ const exit = input.exit ?? ((code) => process.exit(code));
17551
+ const { coverage, unaccounted } = reconcileCoverage(input.changed, input.coverage);
17552
+ let verdict = resolveVerdict(input.proposed, coverage);
17553
+ const openElsewhere = input.openElsewhere ?? [];
17554
+ if (verdict === "PASS" && openElsewhere.length > 0) verdict = "WARN";
17555
+ const note = [describeCoverage(coverage), describeOpenElsewhere(openElsewhere)].filter(Boolean).join("\n\n") || null;
17556
+ if (unaccounted.length > 0) {
17557
+ process.stderr.write(
17558
+ `${YELLOW2}Verity: ${unaccounted.length} changed file(s) could not be attributed to any review stage \u2014 counted as unreviewed.${NC2}
17559
+ `
17560
+ );
17561
+ }
17562
+ if (verdict === "FAIL") {
17563
+ input.renderBlocking?.();
17564
+ if (input.agentContext) {
17565
+ process.stderr.write(`
17566
+ ${input.agentContext}
17567
+ `);
17568
+ }
17569
+ if (note && !input.silenced) process.stderr.write(`
17570
+ ${YELLOW2}${note}${NC2}
17571
+ `);
17572
+ return exit(2);
17573
+ }
17574
+ const agentBlock = input.silenced ? null : [input.agentContext, note].filter(Boolean).join("\n\n") || null;
17575
+ printJsonCompact(buildHookOutput(verdict, input.userSummary, agentBlock));
17576
+ return exit(0);
17577
+ }
17578
+
17246
17579
  // src/lib/cache-cleanup.ts
17247
17580
  var import_node_fs21 = require("node:fs");
17248
17581
  var import_node_path18 = require("node:path");
@@ -17324,6 +17657,13 @@ function isGitOnlyPrompt(prompt) {
17324
17657
  return true;
17325
17658
  }
17326
17659
  function reconcileAnalysisMode(predictedMode, signals) {
17660
+ const mode = resolveAnalysisMode(predictedMode, signals);
17661
+ if (mode !== "skip") return mode;
17662
+ const windowIsOrphaned = signals.actionSummary?.transcript_windowed === "orphaned";
17663
+ if (windowIsOrphaned && !signals.sessionAuthoredCode) return "standard";
17664
+ return mode;
17665
+ }
17666
+ function resolveAnalysisMode(predictedMode, signals) {
17327
17667
  if (!predictedMode || !isValidMode(predictedMode)) {
17328
17668
  return detectAnalysisMode(
17329
17669
  signals.noFilesChanged,
@@ -17422,46 +17762,6 @@ function shouldWarmRetryAnalyze(result) {
17422
17762
  return false;
17423
17763
  }
17424
17764
 
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
17765
  // src/lib/transcript.ts
17466
17766
  var import_node_fs22 = require("node:fs");
17467
17767
  var MAX_READ_BYTES = 256 * 1024;
@@ -17475,9 +17775,11 @@ var MAX_SUMMARY_BYTES = 4096;
17475
17775
  var HOME = process.env.HOME ?? "";
17476
17776
  async function extractActionSummary(transcriptPath) {
17477
17777
  try {
17478
- const lines = readTurnLines(transcriptPath);
17479
- if (!lines || lines.length === 0) return null;
17480
- return buildSummary(lines);
17778
+ const read = readTurnLines(transcriptPath);
17779
+ if (!read || read.lines.length === 0) return null;
17780
+ const summary = buildSummary(read.lines);
17781
+ if (summary) summary.transcript_windowed = read.window;
17782
+ return summary;
17481
17783
  } catch {
17482
17784
  return null;
17483
17785
  }
@@ -17491,9 +17793,11 @@ function readTurnLines(transcriptPath) {
17491
17793
  }
17492
17794
  if (size === 0) return null;
17493
17795
  let raw;
17796
+ let windowed = false;
17494
17797
  if (size <= SMALL_FILE_BYTES) {
17495
17798
  raw = (0, import_node_fs22.readFileSync)(transcriptPath, "utf-8");
17496
17799
  } else {
17800
+ windowed = true;
17497
17801
  const buf = Buffer.alloc(Math.min(MAX_READ_BYTES, size));
17498
17802
  const fd = require("node:fs").openSync(transcriptPath, "r");
17499
17803
  try {
@@ -17511,17 +17815,22 @@ function readTurnLines(transcriptPath) {
17511
17815
  const allLines = raw.split("\n").filter((l) => l.trim().length > 0);
17512
17816
  if (allLines.length === 0) return null;
17513
17817
  let turnStart = 0;
17818
+ let boundaryFound = false;
17514
17819
  for (let i = allLines.length - 1; i >= 0; i--) {
17515
17820
  try {
17516
17821
  const parsed = JSON.parse(allLines[i]);
17517
17822
  if (parsed.type === "user" && isRealUserMessage(parsed)) {
17518
17823
  turnStart = i;
17824
+ boundaryFound = true;
17519
17825
  break;
17520
17826
  }
17521
17827
  } catch {
17522
17828
  }
17523
17829
  }
17524
- return allLines.slice(turnStart);
17830
+ return {
17831
+ lines: allLines.slice(turnStart),
17832
+ window: !windowed ? "whole" : boundaryFound ? "windowed" : "orphaned"
17833
+ };
17525
17834
  }
17526
17835
  function isRealUserMessage(parsed) {
17527
17836
  const message = parsed.message;
@@ -17625,6 +17934,13 @@ function buildSummary(lines) {
17625
17934
  files_read: capArray(filesRead, MAX_FILES_LIST),
17626
17935
  files_edited: capArray(filesEdited, MAX_FILES_LIST),
17627
17936
  files_created: capArray(filesCreated, MAX_CREATED_LIST),
17937
+ // The complement of the two caps that affect SCOPE. `files_read` is excluded
17938
+ // deliberately: reading a file is not authoring it, so a capped read list
17939
+ // narrows nothing.
17940
+ capped_out: [
17941
+ ...cappedOut(filesEdited, MAX_FILES_LIST),
17942
+ ...cappedOut(filesCreated, MAX_CREATED_LIST)
17943
+ ],
17628
17944
  searches,
17629
17945
  commands,
17630
17946
  subagents,
@@ -17675,6 +17991,9 @@ function sanitizeCommand(rawCmd) {
17675
17991
  function capArray(set, max) {
17676
17992
  return Array.from(set).slice(0, max);
17677
17993
  }
17994
+ function cappedOut(set, max) {
17995
+ return Array.from(set).slice(max);
17996
+ }
17678
17997
 
17679
17998
  // src/lib/run-mode.ts
17680
17999
  function parseAutonomousEnv(raw) {
@@ -18069,11 +18388,12 @@ async function readStopHookStdin() {
18069
18388
  return empty;
18070
18389
  }
18071
18390
  }
18072
- function agentContextFor(response, intentRepeat = 0) {
18391
+ function agentContextFor(response, intentRepeat = 0, priorPendingFingerprints = []) {
18073
18392
  const metadata = response.metadata ?? {};
18074
18393
  const intent = response.intent_alignment ?? {};
18075
18394
  return buildAgentContext({
18076
18395
  intentRepeat,
18396
+ priorPendingFingerprints,
18077
18397
  gateDecision: String(response.gate_decision ?? ""),
18078
18398
  findings: response.findings ?? [],
18079
18399
  pendingItems: response.pending_items ?? [],
@@ -18084,12 +18404,45 @@ function agentContextFor(response, intentRepeat = 0) {
18084
18404
  });
18085
18405
  }
18086
18406
  var beaconCtx = null;
18087
- async function passAndExit(reason, skip) {
18407
+ async function passAndExit(reason, skip, kindOverride) {
18088
18408
  const sent = await sendSkipBeacon(beaconCtx, skip);
18089
18409
  logEvent("skip", { reason: skip, beacon: sent });
18090
- printJsonCompact({ gate_decision: "PASS", systemMessage: `Verity: ${reason}` });
18410
+ const POLICY_SKIPS = /* @__PURE__ */ new Set([
18411
+ "no-analyzable-files",
18412
+ "verity-command",
18413
+ "bare-acknowledgment",
18414
+ "reflection-prompt",
18415
+ "skip-mode",
18416
+ "zero-increment",
18417
+ "debounce",
18418
+ "no-delta-since-last-review"
18419
+ ]);
18420
+ const skipKind = kindOverride ?? (POLICY_SKIPS.has(skip) ? "policy" : "capacity");
18421
+ const changed = skipCoverageChanged;
18422
+ const { coverage, unaccounted } = reconcileCoverage(changed, {
18423
+ reviewed: [],
18424
+ notReviewed: changed.map((path) => ({ path, reason: skip, stage: "pre-flight", kind: skipKind }))
18425
+ });
18426
+ const verdict = resolveVerdict("PASS", coverage);
18427
+ const note = describeCoverage(coverage);
18428
+ if (unaccounted.length > 0) {
18429
+ logEvent("coverage_unaccounted", { where: "passAndExit", skip, count: unaccounted.length });
18430
+ }
18431
+ const AGENT_SILENT_SKIPS = /* @__PURE__ */ new Set([]);
18432
+ const agentNote = AGENT_SILENT_SKIPS.has(skip) ? null : note;
18433
+ printJsonCompact(
18434
+ buildHookOutput(
18435
+ verdict,
18436
+ `Verity: ${reason}`,
18437
+ // The agent's ONLY input is additionalContext. Sixteen of the nineteen
18438
+ // terminating paths wrote `systemMessage` — the human's field — and told
18439
+ // the agent nothing at all.
18440
+ agentNote
18441
+ )
18442
+ );
18091
18443
  process.exit(0);
18092
18444
  }
18445
+ var skipCoverageChanged = [];
18093
18446
  var EMPTY_STATIC = {
18094
18447
  tool: "@codacy/analysis-cli",
18095
18448
  findings: [],
@@ -18104,7 +18457,7 @@ function runLocalStatic(analyzable, securityFiles, baseline, skipStatic) {
18104
18457
  }
18105
18458
  function localOnlyAndExit(staticResults) {
18106
18459
  printJsonCompact({
18107
- gate_decision: "PASS",
18460
+ gate_decision: "WARN",
18108
18461
  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
18462
  unauthenticated: true,
18110
18463
  static_results: staticResults
@@ -18164,6 +18517,7 @@ async function runAnalyze(opts, globals) {
18164
18517
  });
18165
18518
  }
18166
18519
  const { files: allChanged, hasRecentCommitFiles } = getChangedFiles();
18520
+ skipCoverageChanged = allChanged;
18167
18521
  const analyzable = filterAnalyzable(allChanged);
18168
18522
  const reviewable = filterReviewable(allChanged);
18169
18523
  const securityFiles = filterSecurity(allChanged);
@@ -18175,15 +18529,27 @@ async function runAnalyze(opts, globals) {
18175
18529
  const conversation = await readAndClearConversationBuffer(baselineSessionId);
18176
18530
  const specs = discoverSpecs();
18177
18531
  const plans = discoverPlans();
18532
+ const agentAuthoredCodeThisTurn = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0));
18533
+ const turnAuthoredCode = agentAuthoredCodeThisTurn || !!baseline && allForReview.some((f) => changedSinceBaseline(f, baseline));
18534
+ const authorshipIsObservable = !!actionSummary && actionSummary.transcript_windowed !== "orphaned" || !!baseline;
18535
+ const canSeeTurnAuthorship = authorshipIsObservable;
18536
+ let earlyFold = null;
18178
18537
  const latestPrompt = conversation?.prompts?.[conversation.prompts.length - 1]?.prompt ?? "";
18179
18538
  if (/^\s*\/verity-/i.test(latestPrompt)) {
18539
+ const setupAuthored = [
18540
+ ...actionSummary?.files_edited ?? [],
18541
+ ...actionSummary?.files_created ?? []
18542
+ ];
18543
+ if (setupAuthored.length > 0) {
18544
+ const adopted = absorbIntoBaseline(setupAuthored, baselineSessionId);
18545
+ logEvent("baseline_absorbed", { skip: "verity-command", offered: setupAuthored.length, adopted });
18546
+ }
18180
18547
  await passAndExit("Verity command \u2014 skipping analysis", "verity-command");
18181
18548
  }
18182
- if (isBareAckPrompt(latestPrompt)) {
18549
+ if (shouldSkipForBareAck({ prompt: latestPrompt, turnAuthoredCode, canSeeTurnAuthorship })) {
18183
18550
  await passAndExit("Bare acknowledgment \u2014 skipping analysis", "bare-acknowledgment");
18184
18551
  }
18185
- const agentAuthoredCodeThisTurn = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0));
18186
- if (isReflectionQuestion(assistantResponse) && !agentAuthoredCodeThisTurn) {
18552
+ if (isReflectionQuestion(assistantResponse) && !turnAuthoredCode && canSeeTurnAuthorship) {
18187
18553
  await passAndExit("Reflection-prompt turn \u2014 skipping analysis", "reflection-prompt");
18188
18554
  }
18189
18555
  const urlResult = await resolveServiceUrl(globals.serviceUrl);
@@ -18229,7 +18595,11 @@ async function runAnalyze(opts, globals) {
18229
18595
  );
18230
18596
  }
18231
18597
  if (analysisMode === "skip") {
18232
- await passAndExit("Skip mode \u2014 no code work to analyze", "skip-mode");
18598
+ await passAndExit(
18599
+ "Skip mode \u2014 no code work to analyze",
18600
+ "skip-mode",
18601
+ turnAuthoredCode ? "capacity" : void 0
18602
+ );
18233
18603
  }
18234
18604
  let staticResults = {
18235
18605
  tool: "@codacy/analysis-cli",
@@ -18239,7 +18609,8 @@ async function runAnalyze(opts, globals) {
18239
18609
  let codeDelta = {
18240
18610
  files: [],
18241
18611
  total_lines: 0,
18242
- total_files: 0
18612
+ total_files: 0,
18613
+ excluded: []
18243
18614
  };
18244
18615
  let snapshotResult = { has_snapshots: false, diffs: [] };
18245
18616
  let contentHash = null;
@@ -18280,10 +18651,43 @@ async function runAnalyze(opts, globals) {
18280
18651
  contentHash = hashResult.hash;
18281
18652
  if (analysisMode !== "plan") {
18282
18653
  const scoped = scopeToAuthored(allForReview, actionSummary);
18283
- if (scoped.signal === "none-authored" && !hasNonEditAuthorship(actionSummary, sessionAuthoredCode)) {
18654
+ const canTrustNoneAuthored = scoped.signal === "none-authored" && authorshipIsObservable;
18655
+ if (canTrustNoneAuthored && !hasNonEditAuthorship(actionSummary, sessionAuthoredCode)) {
18284
18656
  await passAndExit("No agent-authored code this turn \u2014 working-tree changes were not authored by this session", "zero-increment");
18285
18657
  }
18286
- const baseForReview = scoped.signal === "authored" && scoped.files.length > 0 ? scoped.files : allForReview;
18658
+ if (scoped.signal === "none-authored" && !authorshipIsObservable) {
18659
+ logEvent("none_authored_unverifiable", {
18660
+ reason: "orphaned_window_no_baseline",
18661
+ would_have_skipped: allForReview.length
18662
+ });
18663
+ }
18664
+ const narrowingIsTrustworthy = scoped.signal === "authored" && scoped.files.length > 0 && actionSummary?.transcript_windowed !== "orphaned";
18665
+ let recoveredScope = [];
18666
+ if (!narrowingIsTrustworthy && transcriptPath) {
18667
+ try {
18668
+ earlyFold = fold(transcriptPath, { changedFiles: allForReview, repoRoot: repoRoot() });
18669
+ const authoredWhole = new Set(earlyFold.authored.map((a) => a.p));
18670
+ if (authoredWhole.size > 0) {
18671
+ const root = repoRoot();
18672
+ recoveredScope = allForReview.filter((f) => authoredWhole.has(toRepoRelative(f, root)));
18673
+ }
18674
+ logEvent("scope_recovered_from_fold", {
18675
+ window_saw: scoped.files.length,
18676
+ fold_saw: authoredWhole.size,
18677
+ recovered: recoveredScope.length,
18678
+ would_have_widened_to: allForReview.length
18679
+ });
18680
+ } catch {
18681
+ earlyFold = null;
18682
+ }
18683
+ }
18684
+ if (!narrowingIsTrustworthy && scoped.signal === "authored" && recoveredScope.length === 0) {
18685
+ logEvent("scope_widened_orphaned_window", {
18686
+ would_have_sent: scoped.files.length,
18687
+ widened_to: allForReview.length
18688
+ });
18689
+ }
18690
+ const baseForReview = narrowingIsTrustworthy ? scoped.files : recoveredScope.length > 0 ? recoveredScope : allForReview;
18287
18691
  const recentForReview = narrowToRecent(baseForReview, baselineSessionId);
18288
18692
  if (!opts.skipStatic && isCodacyAvailable()) {
18289
18693
  let allScannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles]));
@@ -18304,7 +18708,11 @@ async function runAnalyze(opts, globals) {
18304
18708
  if (assistantResponse) {
18305
18709
  analysisMode = "plan";
18306
18710
  } else {
18307
- await passAndExit("No files within size limits to analyze", "size-limit");
18711
+ await passAndExit(
18712
+ "No files within size limits to analyze",
18713
+ "size-limit",
18714
+ codeDelta.excluded.length > 0 ? "capacity" : "policy"
18715
+ );
18308
18716
  }
18309
18717
  }
18310
18718
  }
@@ -18317,19 +18725,13 @@ async function runAnalyze(opts, globals) {
18317
18725
  snapshotResult = generateSnapshotDiffs(codeDelta.files);
18318
18726
  }
18319
18727
  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;
18728
+ iteration = readIterationState(currentCommit).iteration;
18324
18729
  }
18325
18730
  }
18326
18731
  if (analysisMode === "plan") {
18327
18732
  recordAnalysisStart();
18328
18733
  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;
18734
+ iteration = readIterationState(currentCommit).iteration;
18333
18735
  }
18334
18736
  const contextFiles = gatherContextFiles(contextFilePaths, codeDelta.files);
18335
18737
  for (const f of codeDelta.files) {
@@ -18396,7 +18798,7 @@ async function runAnalyze(opts, globals) {
18396
18798
  let foldConservation = null;
18397
18799
  if (transcriptPath) {
18398
18800
  try {
18399
- foldResult = fold(transcriptPath, { changedFiles: allForReview, repoRoot: repoRoot() });
18801
+ foldResult = earlyFold ?? fold(transcriptPath, { changedFiles: allForReview, repoRoot: repoRoot() });
18400
18802
  foldConservation = checkConservation(allForReview, foldResult, repoRoot());
18401
18803
  if (!foldConservation.holds) {
18402
18804
  process.stderr.write(
@@ -18502,7 +18904,30 @@ async function runAnalyze(opts, globals) {
18502
18904
  hasUserPrompt: (conversation?.prompts?.length ?? 0) > 0,
18503
18905
  isTTY: process.stdout.isTTY === true
18504
18906
  });
18907
+ const excludedByReason = {};
18908
+ for (const e of codeDelta.excluded ?? []) {
18909
+ excludedByReason[e.reason] = (excludedByReason[e.reason] ?? 0) + 1;
18910
+ }
18911
+ const coverageTelemetry = {
18912
+ // git's whole answer, before ANY narrowing. The number that has never been sent.
18913
+ changed_all: allChanged.length,
18914
+ analyzable: analyzable.length,
18915
+ reviewable: reviewable.length,
18916
+ security: securityFiles.length,
18917
+ // after the allowlist, before authorship scoping and the caps
18918
+ for_review: allForReview.length,
18919
+ // what actually reaches the reviewer
18920
+ sent: codeDelta.files.length,
18921
+ // the two silent narrowings, counted separately so they can be told apart
18922
+ capped_out: actionSummary?.capped_out?.length ?? 0,
18923
+ excluded: (codeDelta.excluded ?? []).length,
18924
+ excluded_by_reason: excludedByReason,
18925
+ // was the transcript itself truncated? The 256 KB window means "this turn"
18926
+ // can quietly mean "the last 256 KB of it".
18927
+ transcript_windowed: actionSummary?.transcript_windowed ?? null
18928
+ };
18505
18929
  const requestBody = {
18930
+ coverage_telemetry: coverageTelemetry,
18506
18931
  static_results: staticResults,
18507
18932
  code_delta: codeDelta,
18508
18933
  changed_files: allForReview,
@@ -18588,6 +19013,54 @@ async function runAnalyze(opts, globals) {
18588
19013
  // replaced a population floor with ≈35% power that was sub-integer for
18589
19014
  // three-quarters of the fleet.
18590
19015
  conservation: foldConservation,
19016
+ // ⚠ VRT-52 — RECORDED, NOT APPLIED. The number nobody has.
19017
+ //
19018
+ // The whole "task-scoped delta" design space rests on an assumption that
19019
+ // has been observed exactly ONCE: that delta files routinely belong to
19020
+ // earlier work. Three designs were built on it and all three were killed
19021
+ // adversarially — two by measurement — so before another is attempted,
19022
+ // measure the base rate.
19023
+ //
19024
+ // `authored_under_earlier_goal` counts delta paths whose LAST authorship
19025
+ // event precedes the seq of the goal now in force. Both numbers come from
19026
+ // the same append-only counter (`nextSeq`), so the comparison is exact.
19027
+ //
19028
+ // Keyed on the GOAL, deliberately, not on the task id. The task classifier
19029
+ // reported `is_new_task` on two consecutive turns of one task 25 seconds
19030
+ // apart, so a task-keyed number would measure its unreliability rather
19031
+ // than the phenomenon. And this only became meaningful once `recordGoal`
19032
+ // stopped letting a bare "ok" supersede the goal — before that the seq
19033
+ // advanced every turn and this would have degenerated to "not edited this
19034
+ // turn", which is the exact mistake that sank one of the three designs.
19035
+ //
19036
+ // Changes no payload the reviewer sees, no narrowing, no verdict.
19037
+ vrt52: (() => {
19038
+ const goalSeq = memory?.projection.goal?.seq;
19039
+ if (goalSeq === void 0 || !memorySession) return { known: false };
19040
+ const lastSeq2 = new Map(
19041
+ foldDossier(memorySession.d).authored_all.map((a) => [a.path, a.last_seq])
19042
+ );
19043
+ let earlier = 0;
19044
+ let unknown = 0;
19045
+ for (const f of codeDelta.files) {
19046
+ const seen = lastSeq2.get(f.path);
19047
+ if (seen === void 0) unknown++;
19048
+ else if (seen < goalSeq) earlier++;
19049
+ }
19050
+ return {
19051
+ known: true,
19052
+ goal_seq: goalSeq,
19053
+ delta: codeDelta.files.length,
19054
+ // Files this delta carries that were last written under an EARLIER
19055
+ // instruction. If this stays near zero, VRT-52's code half is
19056
+ // unnecessary and should be closed saying so.
19057
+ authored_under_earlier_goal: earlier,
19058
+ // Delta files the dossier has no authorship record for at all —
19059
+ // pre-existing tree state, or an authorship channel the fold cannot
19060
+ // see. Reported separately so a blind spot is never counted as a zero.
19061
+ no_authorship_record: unknown
19062
+ };
19063
+ })(),
18591
19064
  // P2 — RECORDED, NOT APPLIED. What this run WOULD have reviewed if it
18592
19065
  // narrowed to the within-session increment: what changed since the last
18593
19066
  // VERDICT rather than since task start.
@@ -18620,7 +19093,20 @@ async function runAnalyze(opts, globals) {
18620
19093
  const intentContext = {};
18621
19094
  if (conversation && conversation.prompts.length > 0) {
18622
19095
  const latest = conversation.prompts[conversation.prompts.length - 1];
18623
- intentContext.user_prompt = latest.prompt;
19096
+ const goalPrompt = resolveGoalPrompt(conversation.prompts) ?? { entry: latest, turnsBack: 0 };
19097
+ intentContext.user_prompt = goalPrompt.entry.prompt;
19098
+ if (isContinuationPrompt(intentContext.user_prompt)) {
19099
+ const carried = memory?.projection.goal?.text;
19100
+ if (carried && !isContinuationPrompt(carried)) {
19101
+ intentContext.continuation_prompt = latest.prompt;
19102
+ intentContext.user_prompt = carried;
19103
+ logEvent("goal_from_dossier", { chars: carried.length });
19104
+ }
19105
+ }
19106
+ if (goalPrompt.turnsBack > 0) {
19107
+ intentContext.continuation_prompt = latest.prompt;
19108
+ logEvent("goal_walked_back", { turns_back: goalPrompt.turnsBack });
19109
+ }
18624
19110
  intentContext.session_id = latest.session_id || void 0;
18625
19111
  intentContext.prompt_captured_at = latest.captured_at || void 0;
18626
19112
  if (conversation.prompts.length > 1) {
@@ -18712,6 +19198,85 @@ async function runAnalyze(opts, globals) {
18712
19198
  const response = result.data;
18713
19199
  const decision = response.gate_decision ?? "(unrecognised)";
18714
19200
  const sentPaths = codeDelta.files.map((f) => f.path);
19201
+ let openElsewhere = [];
19202
+ if (memorySession) {
19203
+ try {
19204
+ const st = foldDossier(memorySession.d);
19205
+ openElsewhere = openBlockingElsewhere(st.statements, sentPaths, (file, line) => {
19206
+ try {
19207
+ const src = (0, import_node_fs24.readFileSync)((0, import_node_path20.join)(repoRoot(), file), "utf8").split("\n");
19208
+ const at = src[line - 1];
19209
+ return at === void 0 ? null : lineSha(at);
19210
+ } catch {
19211
+ return null;
19212
+ }
19213
+ });
19214
+ } catch {
19215
+ }
19216
+ }
19217
+ const reviewCoverage = {
19218
+ reviewed: sentPaths,
19219
+ // Declared drops from the stages that DO report themselves today. The other
19220
+ // stages surface via `unaccounted`, which is the tripwire, not the design.
19221
+ notReviewed: [
19222
+ // Every exit from the collection loop, each named. Six reasons where there
19223
+ // used to be two recorded and four silent — the silent ones including the
19224
+ // per-file size cap, which could drop a whole source file without leaving a
19225
+ // trace anywhere in the payload or the run row.
19226
+ ...codeDelta.excluded,
19227
+ // The server-side 300-line middle-out truncation. It only bites on the
19228
+ // full-file branch (a first analysis, before snapshots exist) because
19229
+ // analyze normally sends diffs — but on that branch the reviewer sees the
19230
+ // first and last 100 lines and nothing between, and until now said so to
19231
+ // nobody. CAPACITY: a partial look is not a look.
19232
+ ...(response.metadata?.truncated_files ?? []).map((path) => ({
19233
+ path,
19234
+ reason: "file-middle-truncated-300-lines",
19235
+ stage: "prompt-builder",
19236
+ kind: "capacity"
19237
+ })),
19238
+ // The 20-entry edit cap. CAPACITY, and the sharpest of the lot: it narrows
19239
+ // what is REVIEWED, not merely what is summarised — a session editing 25
19240
+ // files had five silently excluded from the reviewed set.
19241
+ ...(actionSummary?.capped_out ?? []).map((path) => ({
19242
+ path,
19243
+ reason: "edit-list-cap-20",
19244
+ stage: "extractActionSummary",
19245
+ kind: "capacity"
19246
+ })),
19247
+ // ⚠ BASELINE SCOPING — the biggest source of false NOT A CLEAN REVIEW.
19248
+ //
19249
+ // The universe is `allChanged`, git's whole dirty tree. The reviewed set is
19250
+ // scoped to what THIS SESSION authored (the VRT-26 contamination cure), so
19251
+ // every pre-existing dirty file is in the universe, absent from `reviewed`,
19252
+ // and — until now — declared by nobody. It fell through to `unaccounted`,
19253
+ // became capacity, and produced "NOT A CLEAN REVIEW: admin.js" over a file
19254
+ // that was never this session's to review.
19255
+ //
19256
+ // Measured 2026-08-04: three consecutive runs over an untouched tree gave
19257
+ // three different answers — .claude/settings.json, then admin.js, then six
19258
+ // files — because each run took a different path and each path had a
19259
+ // different idea of the universe. POLICY: not this session's work is not a
19260
+ // coverage gap, it is the cure working.
19261
+ ...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) => ({
19262
+ path,
19263
+ reason: "not-authored-this-session",
19264
+ stage: "baseline-scoping",
19265
+ kind: "policy"
19266
+ })),
19267
+ // The extension allowlist, and it is POLICY rather than capacity: a changed
19268
+ // README was never going to be reviewed, and treating that as a coverage
19269
+ // gap would downgrade nearly every PASS to WARN until WARN meant nothing.
19270
+ // Recorded so the ledger balances and so "what did Verity ignore entirely"
19271
+ // is answerable — but it never touches the verdict.
19272
+ ...allChanged.filter((p) => !analyzable.includes(p) && !reviewable.includes(p) && !securityFiles.includes(p)).map((path) => ({
19273
+ path,
19274
+ reason: "not-a-reviewed-file-type",
19275
+ stage: "extension-allowlist",
19276
+ kind: "policy"
19277
+ }))
19278
+ ]
19279
+ };
18715
19280
  const watermarkHash = sentPaths.length > 0 ? computeContentHash(sentPaths) : contentHash;
18716
19281
  const watermarkIsPartial = !!codeDelta.truncated;
18717
19282
  let silenced = null;
@@ -18742,6 +19307,13 @@ async function runAnalyze(opts, globals) {
18742
19307
  });
18743
19308
  }
18744
19309
  let intentRepeatCount = 0;
19310
+ const priorPendingFingerprints = memorySession ? (() => {
19311
+ try {
19312
+ return foldDossier(memorySession.d).meta.recent_pending_sigs ?? [];
19313
+ } catch {
19314
+ return [];
19315
+ }
19316
+ })() : [];
18745
19317
  if (memorySession) {
18746
19318
  try {
18747
19319
  recordVerdict(memorySession.d, {
@@ -18764,7 +19336,10 @@ async function runAnalyze(opts, globals) {
18764
19336
  // What next turn reads as `emittedLast`. A suppressed turn did not
18765
19337
  // speak, so it cannot be the cause of the turn after it — which is what
18766
19338
  // keeps this from becoming a permanent gag.
18767
- emitted: !silenced
19339
+ emitted: !silenced,
19340
+ // Fingerprinted for the NEXT turn's repeat check. Reviewer pending items
19341
+ // carry no `pattern_id`, so their content is the only available key.
19342
+ pendingTexts: (response.pending_items ?? []).map((p) => String(p.description ?? p.title ?? p.reason ?? "")).filter(Boolean)
18768
19343
  });
18769
19344
  intentRepeatCount = Math.max(0, (foldDossier(memorySession.d).meta.intent_repeat?.consecutive ?? 1) - 1);
18770
19345
  } catch {
@@ -18864,9 +19439,41 @@ async function runAnalyze(opts, globals) {
18864
19439
  reverify_by: response.reverify_by
18865
19440
  });
18866
19441
  const grantNudge = grantWarning ? ` \u26A0\uFE0F ${grantWarning}` : "";
18867
- switch (decision) {
19442
+ let capReleased = false;
19443
+ let effectiveDecision = decision;
19444
+ if (decision === "FAIL") {
19445
+ const blocking = (response.findings ?? []).filter((f) => {
19446
+ const sev = String(f.severity ?? "").toLowerCase();
19447
+ return sev === "critical" || sev === "high";
19448
+ });
19449
+ const fingerprint = findingsFingerprint(blocking);
19450
+ const prior = readIterationState(currentCommit);
19451
+ const sameProblem = isSameProblem(prior.fingerprint, fingerprint);
19452
+ const nextIteration = sameProblem ? prior.iteration + 1 : 1;
19453
+ const maxIterations = parseInt(opts.maxIterations, 10);
19454
+ writeIteration(nextIteration, currentCommit, contentHash ?? void 0, fingerprint);
19455
+ iteration = nextIteration;
19456
+ if (nextIteration > maxIterations) {
19457
+ capReleased = true;
19458
+ effectiveDecision = "WARN";
19459
+ logEvent("iteration_cap_released", { iteration: nextIteration, fingerprint });
19460
+ }
19461
+ }
19462
+ if (capReleased) {
19463
+ const findings = response.findings ?? [];
19464
+ const lines = findings.slice(0, 5).map((f) => ` [${String(f.severity ?? "?").toUpperCase()}] ${String(f.title ?? f.message ?? "")} (${String(f.file ?? "?")}:${String(f.line ?? "?")})`);
19465
+ emitVerdict({
19466
+ proposed: "WARN",
19467
+ changed: skipCoverageChanged,
19468
+ coverage: reviewCoverage,
19469
+ 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.
19470
+ ${lines.join("\n")}`,
19471
+ agentContext: null,
19472
+ silenced: true
19473
+ });
19474
+ }
19475
+ switch (effectiveDecision) {
18868
19476
  case "FAIL": {
18869
- writeIteration(iteration + 1, currentCommit, contentHash ?? void 0);
18870
19477
  const assessment = response.assessment;
18871
19478
  const narrative = assessment?.narrative ?? "";
18872
19479
  const findings = response.findings ?? [];
@@ -18943,7 +19550,19 @@ ${YELLOW}${loginNudge.trim()}${NC}
18943
19550
  if (grantNudge) process.stderr.write(`
18944
19551
  ${YELLOW}${grantNudge.trim()}${NC}
18945
19552
  `);
18946
- process.exit(2);
19553
+ emitVerdict({
19554
+ proposed: "FAIL",
19555
+ changed: skipCoverageChanged,
19556
+ coverage: reviewCoverage,
19557
+ userSummary: "",
19558
+ // Subject to the SAME cycle cut as PASS/WARN. Suppressing here is safe:
19559
+ // the findings themselves are rendered above by the blocking renderer,
19560
+ // so what the cut removes is the repeated commentary, never the defect.
19561
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount, priorPendingFingerprints),
19562
+ // The coverage note is silenced with it — half a channel is still a channel.
19563
+ silenced: !!silenced,
19564
+ openElsewhere
19565
+ });
18947
19566
  break;
18948
19567
  }
18949
19568
  case "PASS": {
@@ -18955,10 +19574,16 @@ ${YELLOW}${grantNudge.trim()}${NC}
18955
19574
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
18956
19575
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
18957
19576
  userSummary += loginNudge + grantNudge;
18958
- printJsonCompact(
18959
- buildHookOutput("PASS", userSummary, silenced ? null : agentContextFor(response, intentRepeatCount))
18960
- );
18961
- process.exit(0);
19577
+ emitVerdict({
19578
+ proposed: "PASS",
19579
+ changed: skipCoverageChanged,
19580
+ coverage: reviewCoverage,
19581
+ userSummary,
19582
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount, priorPendingFingerprints),
19583
+ // The coverage note is silenced with it — half a channel is still a channel.
19584
+ silenced: !!silenced,
19585
+ openElsewhere
19586
+ });
18962
19587
  break;
18963
19588
  }
18964
19589
  case "WARN": {
@@ -18969,10 +19594,16 @@ ${YELLOW}${grantNudge.trim()}${NC}
18969
19594
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
18970
19595
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
18971
19596
  userSummary += loginNudge + grantNudge;
18972
- printJsonCompact(
18973
- buildHookOutput("WARN", userSummary, silenced ? null : agentContextFor(response, intentRepeatCount))
18974
- );
18975
- process.exit(0);
19597
+ emitVerdict({
19598
+ proposed: "WARN",
19599
+ changed: skipCoverageChanged,
19600
+ coverage: reviewCoverage,
19601
+ userSummary,
19602
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount, priorPendingFingerprints),
19603
+ // The coverage note is silenced with it — half a channel is still a channel.
19604
+ silenced: !!silenced,
19605
+ openElsewhere
19606
+ });
18976
19607
  break;
18977
19608
  }
18978
19609
  default: {
@@ -19085,8 +19716,8 @@ async function runReview(opts, globals) {
19085
19716
  for (const p of specPaths) {
19086
19717
  if (!(0, import_node_fs26.existsSync)(p)) continue;
19087
19718
  try {
19088
- const { readFileSync: readFileSync15 } = await import("node:fs");
19089
- const content = readFileSync15(p, "utf-8");
19719
+ const { readFileSync: readFileSync16 } = await import("node:fs");
19720
+ const content = readFileSync16(p, "utf-8");
19090
19721
  specs.push({ path: p, content: content.slice(0, 10240) });
19091
19722
  } catch {
19092
19723
  }
@@ -19889,6 +20520,13 @@ async function confirmExistingLogin(serviceUrl, remote, opts) {
19889
20520
  return "handled";
19890
20521
  }
19891
20522
  if (!who.ok) {
20523
+ const denial = authDenialRemedy(who.error);
20524
+ if (denial) {
20525
+ console.log("");
20526
+ printWarn(`Your existing Verity credential was rejected (${denial.code}).`);
20527
+ printInfo(` ${denial.remedy}`);
20528
+ return "drive-login";
20529
+ }
19892
20530
  if (existing.data.userId != null) {
19893
20531
  printInfo(`Logged in as ${existing.data.email ?? `user #${existing.data.userId}`} (cached \u2014 could not reach the Verity service). \u2713`);
19894
20532
  } else {
@@ -19904,15 +20542,14 @@ async function confirmExistingLogin(serviceUrl, remote, opts) {
19904
20542
  return "drive-login";
19905
20543
  }
19906
20544
  async function runOptionalAuth(resolution, opts = {}) {
19907
- let serviceUrl = resolution?.url ?? DEFAULT_SERVICE_URL;
19908
- let healed = false;
19909
- if (resolution) {
19910
- const heal = await maybeHealServiceUrl(resolution, opts.verbose);
19911
- serviceUrl = heal.serviceUrl;
19912
- healed = heal.healed;
19913
- if (healed) {
19914
- printInfo(" Log in below to re-register this project and repair ~/.verity/credentials.");
19915
- }
20545
+ if (resolution.source === "default") {
20546
+ printInfo(`No Verity service configured on this machine \u2014 using the default: ${resolution.url}`);
20547
+ }
20548
+ const heal = await maybeHealServiceUrl(resolution, opts.verbose);
20549
+ const serviceUrl = heal.serviceUrl;
20550
+ const healed = heal.healed;
20551
+ if (healed) {
20552
+ printInfo(" Log in below to re-register this project and repair ~/.verity/credentials.");
19916
20553
  }
19917
20554
  let remote = "";
19918
20555
  try {
@@ -19930,8 +20567,10 @@ async function runOptionalAuth(resolution, opts = {}) {
19930
20567
  if (process.stdin.isTTY && process.stdout.isTTY) {
19931
20568
  console.log("");
19932
20569
  console.log(" Signing in is optional. What it does:");
19933
- console.log(" - Confirms you have write access to this repository. The GitHub token");
19934
- console.log(" is used once to verify that, then discarded \u2014 Verity never stores it.");
20570
+ console.log(" - Confirms which repositories you can write to. The GitHub token is");
20571
+ console.log(" used once for that check, then discarded \u2014 Verity never stores it.");
20572
+ console.log(" - One login covers every repository you can write to \u2014 other repos");
20573
+ console.log(" need no further sign-in on this machine.");
19935
20574
  console.log(" - It does NOT give Verity access to your code. Code checked by the gate");
19936
20575
  console.log(" is analyzed in memory and discarded \u2014 we never store your code.");
19937
20576
  console.log(" - It is required to store and access run history for this repo");
@@ -19946,17 +20585,10 @@ async function runOptionalAuth(resolution, opts = {}) {
19946
20585
  localOnlyNote();
19947
20586
  return;
19948
20587
  }
19949
- if (!remote) {
19950
- printWarn("No git remote found \u2014 cannot authenticate yet.");
19951
- localOnlyNote();
19952
- return;
19953
- }
19954
- const projectName = parseRemote(remote)?.repo ?? (0, import_node_path23.basename)(process.cwd());
19955
20588
  printInfo("Authenticating with GitHub\u2026");
19956
- const result = await registerProject({ projectName, remote, serviceUrl, verbose: opts.verbose });
20589
+ const result = await loginOnce({ serviceUrl, remote: remote || void 0, verbose: opts.verbose });
19957
20590
  if (result.ok) {
19958
- const who = result.data.email ?? (result.data.userId != null ? `user #${result.data.userId}` : null);
19959
- printInfo(`Logged in${who ? ` as ${who}` : ""} \u2713 \u2014 runs, history, and cloud memory now sync to Verity.`);
20591
+ await reportLoginOutcome(result.data, { remote: remote || void 0, verbose: opts.verbose });
19960
20592
  } else {
19961
20593
  printWarn(`Authentication did not complete: ${result.error}`);
19962
20594
  localOnlyNote();
@@ -20107,8 +20739,8 @@ function registerInitCommand(program2) {
20107
20739
  console.log("");
20108
20740
  try {
20109
20741
  const globals = program2.opts();
20110
- const urlResult = await resolveServiceUrlDetailed(globals.serviceUrl);
20111
- await runOptionalAuth(urlResult.ok ? urlResult.data : null, {
20742
+ const resolution = await resolveServiceUrlForAuth(globals.serviceUrl);
20743
+ await runOptionalAuth(resolution, {
20112
20744
  token: globals.token,
20113
20745
  verbose: globals.verbose
20114
20746
  });
@@ -20772,7 +21404,7 @@ function registerTelemetryCommands(program2) {
20772
21404
  }
20773
21405
 
20774
21406
  // 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 () => {
21407
+ program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.10cb8ce").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
21408
  try {
20777
21409
  await foldLegacyLocalCredential();
20778
21410
  } catch {