@codacy/verity-cli 0.28.1-experimental.dfb3ce6 → 0.28.1-experimental.e79117b

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 (3) hide show
  1. package/LICENSE +156 -0
  2. package/bin/verity.js +996 -228
  3. package/package.json +4 -2
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
  });
@@ -11016,7 +11015,54 @@ function listTrackedFiles() {
11016
11015
  return Array.from(set);
11017
11016
  }
11018
11017
  function sanitizeRemote(remote) {
11019
- return remote.replace(/^([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^/]*@/, "$1");
11018
+ const withoutUserinfo = remote.replace(/^([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^/]*@/, "$1");
11019
+ return /[\u0000-\u001f\u007f]/.test(withoutUserinfo) ? "" : withoutUserinfo;
11020
+ }
11021
+
11022
+ // src/lib/token-pin.ts
11023
+ var cached;
11024
+ var warned = false;
11025
+ function warnOnce(message, warn) {
11026
+ if (!warned) {
11027
+ warned = true;
11028
+ warn(message);
11029
+ }
11030
+ return message;
11031
+ }
11032
+ async function storedCredential() {
11033
+ if (cached !== void 0) return cached;
11034
+ const rec = await readGlobalCredential(currentRemote());
11035
+ cached = rec ? { token: rec.token, serviceUrl: rec.serviceUrl } : null;
11036
+ return cached;
11037
+ }
11038
+ var userNamedUrl = null;
11039
+ function setUserNamedServiceUrl(url) {
11040
+ userNamedUrl = url?.trim() || null;
11041
+ }
11042
+ async function checkTokenPin(token, targetUrl) {
11043
+ if (process.env.VERITY_TOKEN && token === process.env.VERITY_TOKEN) return { attach: true };
11044
+ const envUrl = process.env.VERITY_SERVICE_URL?.trim();
11045
+ if (envUrl && normalizeUrl(envUrl) === normalizeUrl(targetUrl)) return { attach: true };
11046
+ if (userNamedUrl && normalizeUrl(userNamedUrl) === normalizeUrl(targetUrl)) return { attach: true };
11047
+ const stored = await storedCredential();
11048
+ if (!stored || stored.token !== token) return { attach: true };
11049
+ if (!stored.serviceUrl) return { attach: false, reason: "unpinnable" };
11050
+ return normalizeUrl(stored.serviceUrl) === normalizeUrl(targetUrl) ? { attach: true } : { attach: false, reason: "mismatch", mintedFor: stored.serviceUrl };
11051
+ }
11052
+ function normalizeUrl(raw) {
11053
+ const trimmed = raw.trim().replace(/\/+$/, "");
11054
+ try {
11055
+ const u = new URL(trimmed);
11056
+ return `${u.protocol}//${u.host.toLowerCase()}${u.pathname.replace(/\/+$/, "")}`;
11057
+ } catch {
11058
+ return trimmed.toLowerCase();
11059
+ }
11060
+ }
11061
+ function pinRefusalMessage(verdict, targetUrl) {
11062
+ if (verdict.reason === "unpinnable") {
11063
+ return `Refusing to send your Verity login to ${targetUrl}: this machine's credential does not record which service issued it, so it cannot be verified. Run "verity login" to re-issue it.`;
11064
+ }
11065
+ return `Refusing to send your Verity login to ${targetUrl}: it was issued by ${verdict.mintedFor}. A repository cannot redirect your credential to another service. If this service is genuinely yours, log in against it explicitly: VERITY_SERVICE_URL=${targetUrl} verity login`;
11020
11066
  }
11021
11067
 
11022
11068
  // src/lib/api-client.ts
@@ -11053,6 +11099,24 @@ async function apiRequest(options) {
11053
11099
  "Content-Type": "application/json"
11054
11100
  };
11055
11101
  if (token) {
11102
+ const pin = await checkTokenPin(token, serviceUrl);
11103
+ if (!pin.attach) {
11104
+ const message = warnOnce(pinRefusalMessage(pin, serviceUrl), printWarn);
11105
+ logHttpCall({
11106
+ cmd,
11107
+ method,
11108
+ url,
11109
+ duration_ms: 0,
11110
+ http_status: null,
11111
+ // 'network' rather than a new category on purpose: no request left the
11112
+ // machine, and every caller's offline path is exactly the handling a
11113
+ // refusal wants (fall back to local, never fabricate a verdict).
11114
+ category: "network",
11115
+ error: "token_pin_refused",
11116
+ retry
11117
+ });
11118
+ return { ok: false, error: `TOKEN_PIN_REFUSED: ${message}`, category: "network" };
11119
+ }
11056
11120
  headers["Authorization"] = `Bearer ${token}`;
11057
11121
  }
11058
11122
  const remote = requestRemote();
@@ -11165,14 +11229,9 @@ async function serviceUrlFromCredentials() {
11165
11229
  async function serviceUrlFromVerityMd() {
11166
11230
  try {
11167
11231
  const content = await (0, import_promises2.readFile)(projectPath(VERITY_MD_FILE), "utf-8");
11168
- const boldLine = content.split("\n").find((l) => /\*\*url\*\*/i.test(l));
11169
- if (boldLine) {
11170
- const urlMatch = boldLine.match(/https:\/\/[^\s]+/);
11171
- if (urlMatch) return urlMatch[0];
11172
- }
11173
- const plainLine = content.split("\n").find((l) => /(?:url|service)\s*:/i.test(l));
11174
- if (plainLine) {
11175
- const urlMatch = plainLine.match(/https:\/\/[^\s]+/);
11232
+ const line = content.split("\n").find((l) => /\*{0,2}(?:url|service)\*{0,2}\s*:/i.test(l));
11233
+ if (line) {
11234
+ const urlMatch = line.match(/https:\/\/[^\s]+/);
11176
11235
  if (urlMatch) return urlMatch[0];
11177
11236
  }
11178
11237
  } catch {
@@ -11195,7 +11254,15 @@ async function resolveServiceUrlDetailed(flagUrl) {
11195
11254
  if (mdUrl) {
11196
11255
  return { ok: true, data: { url: mdUrl, source: "verity_md" } };
11197
11256
  }
11198
- return { ok: false, error: "No Verity service URL found. Run /verity-setup to configure." };
11257
+ return {
11258
+ ok: false,
11259
+ error: 'No Verity service URL found. Run "verity login" to get started, or /verity-setup to configure this project.'
11260
+ };
11261
+ }
11262
+ async function resolveServiceUrlForAuth(flagUrl) {
11263
+ const strict = await resolveServiceUrlDetailed(flagUrl);
11264
+ if (strict.ok) return strict.data;
11265
+ return { url: DEFAULT_SERVICE_URL, source: "default" };
11199
11266
  }
11200
11267
  async function resolveServiceUrl(flagUrl) {
11201
11268
  const result = await resolveServiceUrlDetailed(flagUrl);
@@ -11234,7 +11301,10 @@ async function resolveToken(flagToken) {
11234
11301
  data: { token: local.token, source: "local", userId: local.userId, email: local.email }
11235
11302
  };
11236
11303
  }
11237
- return { ok: false, error: "No Verity token found. Run /verity-setup to configure." };
11304
+ return {
11305
+ ok: false,
11306
+ error: 'No Verity token found. Run "verity login" to sign in, or /verity-setup to set up this project.'
11307
+ };
11238
11308
  }
11239
11309
  async function whoami(token, serviceUrl, verbose) {
11240
11310
  return apiRequest({
@@ -11671,16 +11741,81 @@ function registerAuthCommands(program2) {
11671
11741
  });
11672
11742
  }
11673
11743
 
11744
+ // src/lib/login-report.ts
11745
+ async function reportLoginOutcome(out, opts = {}) {
11746
+ const identity = out.email ?? (out.userId != null ? `user #${out.userId}` : "your account");
11747
+ printInfo(`Logged in as ${identity}. \u2713`);
11748
+ printInfo(` Access granted to ${out.repoCount} ${out.repoCount === 1 ? "repository" : "repositories"}.`);
11749
+ if (out.expiresAt) {
11750
+ printInfo(` This login expires on ${out.expiresAt.slice(0, 10)} \u2014 "verity login" again to renew.`);
11751
+ printInfo(' See your machines with "verity sessions list"; sign out with "verity logout".');
11752
+ }
11753
+ if (out.repoCount > 0) {
11754
+ printInfo(" Runs, history, and cloud memory now sync to Verity everywhere you have write access.");
11755
+ }
11756
+ if (out.prunedCredentials > 0) {
11757
+ printVerbose(`Pruned ${out.prunedCredentials} superseded per-repository credential line(s).`, opts.verbose);
11758
+ } else if (out.prunedCredentials < 0) {
11759
+ printWarn(" Could not rewrite ~/.verity/credentials: superseded per-repository tokens remain and");
11760
+ printWarn(" will keep taking precedence over this login in their own repositories.");
11761
+ printInfo(` Check the file's permissions; the next successful "verity login" retries the cleanup.`);
11762
+ }
11763
+ if (out.repoCount === 0) {
11764
+ printWarn("The Verity GitHub App is not installed on any account you can access.");
11765
+ printInfo(" Install it (and grant your repositories), then re-run verity login:");
11766
+ printInfo(` ${githubAppInstallUrl(null)}`);
11767
+ return;
11768
+ }
11769
+ const remote = opts.remote;
11770
+ if (!remote) return;
11771
+ const who = await whoami(out.token, out.serviceUrl, opts.verbose);
11772
+ if (who.ok && who.data.grant_status != null) {
11773
+ printInfo(" \u2713 This repository is covered.");
11774
+ } else if (!who.ok) {
11775
+ printWarn(` Could not confirm this repository's coverage (${who.error}) \u2014 verity status will show it.`);
11776
+ } else {
11777
+ const parsed = parseRemote(remote);
11778
+ const installUrl = githubAppInstallUrl(parsed ? await githubAccountId(parsed.owner) : null);
11779
+ printWarn(` This repository (${parsed ? `${parsed.owner}/${parsed.repo}` : remote}) is NOT covered by your grants.`);
11780
+ printInfo(" Grant the Verity GitHub App access to it, then re-run verity login:");
11781
+ printInfo(` ${installUrl}`);
11782
+ }
11783
+ const rec = await readGlobalCredential(remote);
11784
+ if (rec && rec.token !== out.token) {
11785
+ const otherBackend = rec.serviceUrl != null && rec.serviceUrl !== out.serviceUrl;
11786
+ const otherIdentity = rec.userId != null && out.userId != null && rec.userId !== out.userId;
11787
+ if (otherBackend) {
11788
+ printWarn(` Note: this repository is pinned to a different Verity service (${rec.serviceUrl})`);
11789
+ printWarn(" by its own credential line, which takes precedence here \u2014 this login does not");
11790
+ printWarn(" change that. To move the repository, remove its line from ~/.verity/credentials.");
11791
+ printWarn(' Until that line is removed, "verity login" here cannot fast-path and will run');
11792
+ printWarn(" the full GitHub flow every time.");
11793
+ } else if (otherIdentity) {
11794
+ printWarn(" Note: this repository uses a different account's credential, which takes");
11795
+ printWarn(' precedence here \u2014 this login leaves it in place, and "verity login" in this');
11796
+ printWarn(" repository will report that account. Remove its line from ~/.verity/credentials");
11797
+ printWarn(" only if you want this repository on the login you just completed.");
11798
+ } else {
11799
+ const kind = rec.userId != null ? "superseded per-repository" : "anonymous project-specific";
11800
+ printWarn(` Note: this repository has a ${kind} credential that takes`);
11801
+ printWarn(" precedence here. Remove its line from ~/.verity/credentials to use your login.");
11802
+ printWarn(' Until then, "verity login" in this repository re-runs the full GitHub flow');
11803
+ printWarn(" every time.");
11804
+ }
11805
+ }
11806
+ }
11807
+
11674
11808
  // src/commands/login.ts
11675
11809
  function registerLoginCommand(program2) {
11676
11810
  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
11811
  const globals = program2.opts();
11678
- const urlResult = await resolveServiceUrlDetailed(globals.serviceUrl);
11679
- if (!urlResult.ok) {
11680
- printError(urlResult.error);
11681
- process.exit(1);
11812
+ const resolution = await resolveServiceUrlForAuth(globals.serviceUrl);
11813
+ if (resolution.source === "default") {
11814
+ printInfo(`No Verity service configured on this machine \u2014 using the default: ${resolution.url}`);
11815
+ } else {
11816
+ printVerbose(`Service URL from ${resolution.source}: ${resolution.url}`, globals.verbose);
11682
11817
  }
11683
- const heal = await maybeHealServiceUrl(urlResult.data, globals.verbose);
11818
+ const heal = await maybeHealServiceUrl(resolution, globals.verbose);
11684
11819
  const serviceUrl = heal.serviceUrl;
11685
11820
  if (heal.healed) {
11686
11821
  printInfo(" Completing login updates ~/.verity/credentials against the live service.");
@@ -11721,65 +11856,7 @@ function registerLoginCommand(program2) {
11721
11856
  printError(`Login failed: ${result.error}`);
11722
11857
  process.exit(1);
11723
11858
  }
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
- }
11859
+ await reportLoginOutcome(result.data, { remote: remote || void 0, verbose: globals.verbose });
11783
11860
  });
11784
11861
  }
11785
11862
 
@@ -11914,12 +11991,12 @@ async function auth(globals) {
11914
11991
  printError(tokenResult.error);
11915
11992
  process.exit(1);
11916
11993
  }
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 };
11994
+ const resolution = await resolveServiceUrlForAuth(globals.serviceUrl);
11995
+ return {
11996
+ token: tokenResult.data.token,
11997
+ serviceUrl: resolution.url,
11998
+ keyed: tokenResult.data.keyed === true
11999
+ };
11923
12000
  }
11924
12001
  function explain(error) {
11925
12002
  if (error.startsWith("FORBIDDEN")) {
@@ -11928,6 +12005,20 @@ function explain(error) {
11928
12005
  printInfo(' This login has expired or was revoked \u2014 run "verity login" to sign in again.');
11929
12006
  }
11930
12007
  }
12008
+ async function logoutLocallyOnly(keyed) {
12009
+ printWarn("Could not reach the Verity service to revoke this login (see above).");
12010
+ const cleared = await removeGlobalCredential(keyed ? currentRemote() : "");
12011
+ if (cleared) {
12012
+ printInfo(keyed ? "This repository\u2019s Verity credential cleared \u2014 it is signed out. \u2713" : "Local login credential cleared \u2014 this machine is signed out. \u2713");
12013
+ printInfo(" The session may still exist server-side; revoke it from another machine with");
12014
+ printInfo(' "verity sessions revoke <session-id>" if that matters.');
12015
+ } else {
12016
+ printInfo("No local credential to clear \u2014 already signed out here.");
12017
+ }
12018
+ }
12019
+ function isPinRefusal(error) {
12020
+ return error.startsWith("TOKEN_PIN_REFUSED");
12021
+ }
11931
12022
  function registerSessionsCommands(program2) {
11932
12023
  const sessions = program2.command("sessions").description("List and revoke your Verity logins (one per machine)");
11933
12024
  sessions.command("list").description("List your active logins \u2014 device, last use, and expiry").option("--json", "Output raw JSON").action(async (opts) => {
@@ -11972,7 +12063,7 @@ function registerSessionsCommands(program2) {
11972
12063
  });
11973
12064
  sessions.command("revoke <session-id>").description("Revoke one login. Revoking this machine's also clears the local credential").action(async (sessionId) => {
11974
12065
  const globals = program2.opts();
11975
- const { token, serviceUrl } = await auth(globals);
12066
+ const { token, serviceUrl, keyed } = await auth(globals);
11976
12067
  const result = await apiRequest({
11977
12068
  method: "DELETE",
11978
12069
  path: `/auth/sessions/${encodeURIComponent(sessionId)}`,
@@ -11991,7 +12082,7 @@ function registerSessionsCommands(program2) {
11991
12082
  }
11992
12083
  printInfo(`Session ${sessionId} revoked. \u2713`);
11993
12084
  if (result.data.was_current) {
11994
- const cleared = await removeGlobalCredential("");
12085
+ const cleared = await removeGlobalCredential(keyed ? currentRemote() : "");
11995
12086
  printInfo(cleared ? ' That was this machine \u2014 local credential cleared. Run "verity login" to sign back in.' : ' That was this machine. Run "verity login" to sign back in.');
11996
12087
  }
11997
12088
  });
@@ -12003,7 +12094,7 @@ function registerLogoutCommand(program2) {
12003
12094
  printError("Use either --all or --others, not both.");
12004
12095
  process.exit(1);
12005
12096
  }
12006
- const { token, serviceUrl } = await auth(globals);
12097
+ const { token, serviceUrl, keyed } = await auth(globals);
12007
12098
  if (opts.all || opts.others) {
12008
12099
  const result = await apiRequest({
12009
12100
  method: "DELETE",
@@ -12014,6 +12105,10 @@ function registerLogoutCommand(program2) {
12014
12105
  cmd: "logout"
12015
12106
  });
12016
12107
  if (!result.ok) {
12108
+ if (isPinRefusal(result.error) && !opts.others) {
12109
+ await logoutLocallyOnly(keyed);
12110
+ return;
12111
+ }
12017
12112
  printError(result.error);
12018
12113
  explain(result.error);
12019
12114
  process.exit(1);
@@ -12038,15 +12133,21 @@ function registerLogoutCommand(program2) {
12038
12133
  cmd: "logout"
12039
12134
  });
12040
12135
  if (!list.ok) {
12136
+ if (isPinRefusal(list.error)) {
12137
+ await logoutLocallyOnly(keyed);
12138
+ return;
12139
+ }
12041
12140
  printError(list.error);
12042
12141
  explain(list.error);
12043
12142
  process.exit(1);
12044
12143
  }
12045
12144
  const current = list.data.sessions.find((s) => s.current);
12046
12145
  if (!current) {
12047
- printWarn("This machine is not signed in with a Verity login.");
12048
- const cleared2 = await removeGlobalCredential("");
12049
- if (cleared2) printInfo(" Cleared the local login credential anyway.");
12146
+ printWarn(keyed ? "This repository uses its own Verity credential, not a machine login." : "This machine is not signed in with a Verity login.");
12147
+ const cleared2 = await removeGlobalCredential(keyed ? currentRemote() : "");
12148
+ if (cleared2) {
12149
+ printInfo(keyed ? " Cleared this repository\u2019s credential; your machine login is untouched." : " Cleared the local login credential anyway.");
12150
+ }
12050
12151
  return;
12051
12152
  }
12052
12153
  const revoked = await apiRequest({
@@ -12058,6 +12159,10 @@ function registerLogoutCommand(program2) {
12058
12159
  cmd: "logout"
12059
12160
  });
12060
12161
  if (!revoked.ok) {
12162
+ if (isPinRefusal(revoked.error)) {
12163
+ await logoutLocallyOnly(keyed);
12164
+ return;
12165
+ }
12061
12166
  printError(revoked.error);
12062
12167
  explain(revoked.error);
12063
12168
  process.exit(1);
@@ -13591,6 +13696,143 @@ var import_node_fs10 = require("node:fs");
13591
13696
  var import_node_crypto5 = require("node:crypto");
13592
13697
  var import_node_path11 = require("node:path");
13593
13698
 
13699
+ // src/lib/skip-detection.ts
13700
+ function isBareAckPrompt(prompt) {
13701
+ if (typeof prompt !== "string") return false;
13702
+ const trimmed = prompt.trim();
13703
+ if (trimmed.length === 0) return false;
13704
+ if (trimmed.length > 20) return false;
13705
+ 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;
13706
+ return bareAckPattern.test(trimmed);
13707
+ }
13708
+ function isContinuationPrompt(prompt) {
13709
+ if (typeof prompt !== "string") return false;
13710
+ const trimmed = prompt.trim();
13711
+ if (trimmed.length === 0) return false;
13712
+ if (trimmed.length > 24) return false;
13713
+ 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;
13714
+ return continuation.test(trimmed) || isBareAckPrompt(trimmed);
13715
+ }
13716
+ function resolveGoalPrompt(prompts) {
13717
+ if (prompts.length === 0) return null;
13718
+ const latest = prompts[prompts.length - 1];
13719
+ if (!isContinuationPrompt(latest.prompt)) return { entry: latest, turnsBack: 0 };
13720
+ for (let i = prompts.length - 2; i >= 0; i--) {
13721
+ if (!isContinuationPrompt(prompts[i].prompt)) {
13722
+ return { entry: prompts[i], turnsBack: prompts.length - 1 - i };
13723
+ }
13724
+ }
13725
+ return { entry: latest, turnsBack: 0 };
13726
+ }
13727
+ function isReflectionQuestion(response) {
13728
+ if (!response || typeof response !== "string") return false;
13729
+ const markers = [
13730
+ /reflection\s+for\s+future\s+agents/i,
13731
+ /what(?:'s|\s+is)\s+one\s+thing\s+you\s+learned/i,
13732
+ /say\s+['"]?skip['"]?\s+to\s+skip/i,
13733
+ /quick\s+reflection\s+question/i,
13734
+ // Post-flip (VRT-21): the agent drafts the reflection itself and, when
13735
+ // interactive, asks the user to confirm/correct before recording. That
13736
+ // turn authors no code either, so it's still a reflection turn.
13737
+ /reflection\s+draft/i,
13738
+ /confirm,?\s+correct,?\s+or\s+add/i
13739
+ ];
13740
+ return markers.some((m) => m.test(response));
13741
+ }
13742
+ function isMetaTaskLabel(label2) {
13743
+ if (label2 === null || label2 === void 0) return false;
13744
+ if (typeof label2 !== "string") return false;
13745
+ const trimmed = label2.trim();
13746
+ if (trimmed.length === 0) return true;
13747
+ const metaPatterns = [
13748
+ /^verity\s+[\w-]+\s+response$/i,
13749
+ // "Verity reflect response"
13750
+ /^simple user response$/i,
13751
+ /^verity\s+command$/i,
13752
+ // "Verity command"
13753
+ /^user\s+(question|reply|response|ack)$/i
13754
+ ];
13755
+ return metaPatterns.some((p) => p.test(trimmed));
13756
+ }
13757
+ function shouldSkipForBareAck(input) {
13758
+ if (!isBareAckPrompt(input.prompt)) return false;
13759
+ if (input.turnAuthoredCode) return false;
13760
+ return input.canSeeTurnAuthorship;
13761
+ }
13762
+
13763
+ // src/lib/pending-repeat.ts
13764
+ var STOP = /* @__PURE__ */ new Set([
13765
+ "the",
13766
+ "and",
13767
+ "that",
13768
+ "this",
13769
+ "with",
13770
+ "from",
13771
+ "have",
13772
+ "been",
13773
+ "were",
13774
+ "what",
13775
+ "when",
13776
+ "which",
13777
+ "their",
13778
+ "there",
13779
+ "these",
13780
+ "those",
13781
+ "would",
13782
+ "could",
13783
+ "should",
13784
+ "must",
13785
+ "will",
13786
+ "also",
13787
+ "just",
13788
+ "only",
13789
+ "into",
13790
+ "over",
13791
+ "than",
13792
+ "then",
13793
+ "them",
13794
+ "some",
13795
+ "such",
13796
+ "more",
13797
+ "most",
13798
+ "other",
13799
+ "about",
13800
+ "after",
13801
+ "before",
13802
+ "since",
13803
+ "because",
13804
+ "while",
13805
+ "where",
13806
+ "whether",
13807
+ "ensure",
13808
+ "confirm",
13809
+ "verify",
13810
+ "check"
13811
+ ]);
13812
+ function pendingTokens(text) {
13813
+ if (!text || typeof text !== "string") return [];
13814
+ const out = /* @__PURE__ */ new Set();
13815
+ for (const raw of text.toLowerCase().split(/[^a-z0-9]+/)) {
13816
+ if (raw.length <= 3) continue;
13817
+ if (STOP.has(raw)) continue;
13818
+ out.add(raw);
13819
+ }
13820
+ return [...out].sort();
13821
+ }
13822
+ var REPEAT_THRESHOLD = 0.3;
13823
+ function overlapCoefficient(a, b) {
13824
+ if (a.length === 0 || b.length === 0) return 0;
13825
+ const setB = new Set(b);
13826
+ let shared = 0;
13827
+ for (const t of a) if (setB.has(t)) shared++;
13828
+ return shared / Math.min(a.length, b.length);
13829
+ }
13830
+ function isRepeatOfAny(text, priorFingerprints, threshold = REPEAT_THRESHOLD) {
13831
+ const tokens = pendingTokens(text);
13832
+ if (tokens.length === 0) return false;
13833
+ return priorFingerprints.some((prior) => overlapCoefficient(tokens, prior) >= threshold);
13834
+ }
13835
+
13594
13836
  // src/lib/dossier.ts
13595
13837
  var import_node_fs9 = require("node:fs");
13596
13838
  var import_node_crypto4 = require("node:crypto");
@@ -13599,6 +13841,7 @@ var MAX_LINE_BYTES = 4096;
13599
13841
  var MAX_GOAL_CHARS = 2e3;
13600
13842
  var GOAL_KEEP = 8;
13601
13843
  var GOAL_TOTAL_CAP = 32;
13844
+ var RECENT_PENDING_CAP = 20;
13602
13845
  var HASH_WIDTH = 16;
13603
13846
  var AUTHORED_CAP = 300;
13604
13847
  var NOT_MINE_CAP = 300;
@@ -13967,6 +14210,10 @@ function reduce(state, events, now) {
13967
14210
  consecutiveIdle: ev.idle === false ? 0 : (state.meta.channel?.consecutiveIdle ?? 0) + 1
13968
14211
  };
13969
14212
  state.meta.last_adjudication = ev.intent_verdict ? { verdict: ev.intent_verdict, score: ev.intent_score ?? null, at: ev.at, decision: ev.decision } : void 0;
14213
+ if (Array.isArray(ev.pending_sigs) && ev.pending_sigs.length > 0) {
14214
+ const prior = state.meta.recent_pending_sigs ?? [];
14215
+ state.meta.recent_pending_sigs = [...prior, ...ev.pending_sigs].slice(-RECENT_PENDING_CAP);
14216
+ }
13970
14217
  if (ev.intent_sig) {
13971
14218
  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
14219
  } else {
@@ -14191,12 +14438,12 @@ function readFoldCache(d) {
14191
14438
  if (!(0, import_node_fs9.existsSync)(d.foldPath)) return null;
14192
14439
  const raw = JSON.parse((0, import_node_fs9.readFileSync)(d.foldPath, "utf8"));
14193
14440
  if (raw?.v !== 1) return null;
14194
- const cached = expandState(raw);
14195
- if (!cached?.meta) return null;
14441
+ const cached2 = expandState(raw);
14442
+ if (!cached2?.meta) return null;
14196
14443
  const size = (0, import_node_fs9.existsSync)(d.eventsPath) ? (0, import_node_fs9.statSync)(d.eventsPath).size : 0;
14197
14444
  const rotations = (0, import_node_fs9.existsSync)(d.rotatedDir) ? (0, import_node_fs9.readdirSync)(d.rotatedDir).filter((f) => f.endsWith(".jsonl")).length : 0;
14198
- if (cached.meta.upto_offset !== size || cached.meta.rotations !== rotations) return null;
14199
- return cached;
14445
+ if (cached2.meta.upto_offset !== size || cached2.meta.rotations !== rotations) return null;
14446
+ return cached2;
14200
14447
  } catch {
14201
14448
  return null;
14202
14449
  }
@@ -14333,6 +14580,10 @@ function projectMemory(state, opts) {
14333
14580
  ...active.delivered && { delivered: active.delivered }
14334
14581
  };
14335
14582
  }
14583
+ if (opts.capture) {
14584
+ const missed = Math.max(0, opts.capture.seen - opts.capture.captured);
14585
+ p.capture = { seen: opts.capture.seen, captured: opts.capture.captured, missed };
14586
+ }
14336
14587
  if (state.meta.last_adjudication) {
14337
14588
  const a = state.meta.last_adjudication;
14338
14589
  p.last_adjudication = { verdict: a.verdict, score: a.score, at: a.at };
@@ -14579,7 +14830,8 @@ function recall(d, input) {
14579
14830
  continuity,
14580
14831
  spoken: reanchored.spoken,
14581
14832
  refused: reanchored.dropped.length,
14582
- lastVerdictSeq
14833
+ lastVerdictSeq,
14834
+ ...input.capture && { capture: input.capture }
14583
14835
  });
14584
14836
  return {
14585
14837
  state: effective,
@@ -14690,7 +14942,19 @@ function sessionDossier(token, sessionId) {
14690
14942
  const d = openDossier(identity);
14691
14943
  return d ? { d, identity } : null;
14692
14944
  }
14945
+ function hasActiveGoal(d) {
14946
+ try {
14947
+ if (!(0, import_node_fs10.existsSync)(d.eventsPath)) return false;
14948
+ return (0, import_node_fs10.readFileSync)(d.eventsPath, "utf8").includes('"k":"goal"');
14949
+ } catch {
14950
+ return false;
14951
+ }
14952
+ }
14693
14953
  function recordGoal(d, prompt, source = "prompt") {
14954
+ if (source === "prompt" && isContinuationPrompt(prompt) && hasActiveGoal(d)) {
14955
+ appendEvent(d, { k: "goal_continue", text: prompt.slice(0, 64) });
14956
+ return;
14957
+ }
14694
14958
  const text = prompt.slice(0, MAX_GOAL_CHARS);
14695
14959
  appendEvent(d, {
14696
14960
  k: "goal",
@@ -14806,6 +15070,13 @@ function recordVerdict(d, v) {
14806
15070
  branch: v.branch,
14807
15071
  decision: v.decision,
14808
15072
  ...sig && { intent_sig: sig },
15073
+ // Fingerprints of the pending items this verdict delivered, so the NEXT turn
15074
+ // can tell a repeat from a new requirement. Only recorded when the channel
15075
+ // actually spoke — a silenced turn delivered nothing, so nothing was "said
15076
+ // before" and labelling the next turn's items as repeats would be a lie.
15077
+ ...v.emitted === true && v.pendingTexts && v.pendingTexts.length > 0 && {
15078
+ pending_sigs: v.pendingTexts.slice(0, 8).map((t) => pendingTokens(t).slice(0, 16))
15079
+ },
14809
15080
  emitted: v.emitted === true,
14810
15081
  idle: v.idle !== false,
14811
15082
  ...v.intent?.verdict && { intent_verdict: v.intent.verdict },
@@ -14854,7 +15125,9 @@ function recallMemory(d, identity, opts) {
14854
15125
  const state = foldDossier(d);
14855
15126
  const watermark = state.meta.watermark?.sha ?? null;
14856
15127
  const watermarkPaths = (state.authored ?? []).map((a) => a.path);
15128
+ const captureCmp = typeof opts.userMessagesSeen === "number" ? { seen: opts.userMessagesSeen, captured: state.meta.goal_chain } : void 0;
14857
15129
  const r = recall(d, {
15130
+ ...captureCmp && { capture: captureCmp },
14858
15131
  identity,
14859
15132
  currentSessionKey: opts.currentSessionKey,
14860
15133
  branchNow: getCurrentBranch(),
@@ -15111,6 +15384,8 @@ function collectCodeDelta(files, opts) {
15111
15384
  let totalSize = 0;
15112
15385
  let truncationReason = null;
15113
15386
  const droppedPaths = [];
15387
+ const excluded = [];
15388
+ const exclude = (path, reason) => excluded.push({ path, reason, stage: "collectCodeDelta", kind: "capacity" });
15114
15389
  for (const filepath of sorted) {
15115
15390
  if (result.length >= maxFiles) {
15116
15391
  truncationReason ??= "max_files";
@@ -15118,14 +15393,21 @@ function collectCodeDelta(files, opts) {
15118
15393
  continue;
15119
15394
  }
15120
15395
  const resolved = resolveFile(filepath);
15121
- if (!resolved) continue;
15396
+ if (!resolved) {
15397
+ exclude(filepath, "path-not-resolvable");
15398
+ continue;
15399
+ }
15122
15400
  let size;
15123
15401
  try {
15124
15402
  size = (0, import_node_fs11.statSync)(resolved).size;
15125
15403
  } catch {
15404
+ exclude(filepath, "not-stattable");
15405
+ continue;
15406
+ }
15407
+ if (size > maxFileBytes) {
15408
+ exclude(filepath, `over-file-size-limit-${maxFileBytes}b`);
15126
15409
  continue;
15127
15410
  }
15128
- if (size > maxFileBytes) continue;
15129
15411
  if (totalSize + size > maxTotalBytes) {
15130
15412
  truncationReason ??= "max_total_bytes";
15131
15413
  const idx = sorted.indexOf(filepath);
@@ -15136,6 +15418,7 @@ function collectCodeDelta(files, opts) {
15136
15418
  try {
15137
15419
  content = (0, import_node_fs11.readFileSync)(resolved, "utf-8");
15138
15420
  } catch {
15421
+ exclude(filepath, "not-readable");
15139
15422
  continue;
15140
15423
  }
15141
15424
  totalSize += size;
@@ -15149,10 +15432,14 @@ function collectCodeDelta(files, opts) {
15149
15432
  (sum, f) => sum + f.content.split("\n").length,
15150
15433
  0
15151
15434
  );
15435
+ for (const path of droppedPaths) {
15436
+ exclude(path, truncationReason === "max_files" ? "max-files-cap" : "max-total-bytes-cap");
15437
+ }
15152
15438
  return {
15153
15439
  files: result,
15154
15440
  total_lines: totalLines,
15155
15441
  total_files: result.length,
15442
+ excluded,
15156
15443
  ...truncationReason && {
15157
15444
  truncated: {
15158
15445
  reason: truncationReason,
@@ -15401,8 +15688,8 @@ function preImage(repoRelPath, baseline) {
15401
15688
  perBaseline = /* @__PURE__ */ new Map();
15402
15689
  preImageCache.set(baseline, perBaseline);
15403
15690
  }
15404
- const cached = perBaseline.get(repoRelPath);
15405
- if (cached) return cached;
15691
+ const cached2 = perBaseline.get(repoRelPath);
15692
+ if (cached2) return cached2;
15406
15693
  const resolved = resolvePreImage(repoRelPath, baseline);
15407
15694
  perBaseline.set(repoRelPath, resolved);
15408
15695
  return resolved;
@@ -15446,6 +15733,34 @@ ${addedLines}`,
15446
15733
  }
15447
15734
  return { diffs, has_baseline: true };
15448
15735
  }
15736
+ function absorbIntoBaseline(paths, sessionId) {
15737
+ const baseline = readBaseline(sessionId);
15738
+ if (!baseline || paths.length === 0) return 0;
15739
+ const dir = sessionDir(sessionKey(baseline.session_id));
15740
+ let adopted = 0;
15741
+ const dirty = new Set(baseline.dirty_paths);
15742
+ for (const p of paths) {
15743
+ try {
15744
+ const content = safeReadForMirror(projectPath(p));
15745
+ if (content === null) continue;
15746
+ const dest = mirrorPath(dir, p);
15747
+ (0, import_node_fs13.mkdirSync)((0, import_node_path14.dirname)(dest), { recursive: true });
15748
+ (0, import_node_fs13.writeFileSync)(dest, content);
15749
+ dirty.add(p);
15750
+ adopted++;
15751
+ } catch {
15752
+ }
15753
+ }
15754
+ if (adopted === 0) return 0;
15755
+ try {
15756
+ const updated = { ...baseline, dirty_paths: [...dirty] };
15757
+ (0, import_node_fs13.writeFileSync)(manifestPath(dir), JSON.stringify(updated));
15758
+ preImageCache.delete(baseline);
15759
+ } catch {
15760
+ return 0;
15761
+ }
15762
+ return adopted;
15763
+ }
15449
15764
  function changedSinceBaseline(repoRelPath, baseline) {
15450
15765
  const pre = preImage(repoRelPath, baseline);
15451
15766
  let current;
@@ -16322,40 +16637,40 @@ function narrowToRecent(files, sessionId) {
16322
16637
  });
16323
16638
  return recent.length > 0 ? recent : files;
16324
16639
  }
16325
- function readIteration(currentCommit, _contentHash) {
16326
- if (!(0, import_node_fs15.existsSync)(ITERATION_FILE)) return 1;
16640
+ function readIterationState(currentCommit) {
16641
+ if (!(0, import_node_fs15.existsSync)(ITERATION_FILE)) return { iteration: 1, fingerprint: null };
16327
16642
  try {
16328
16643
  const stored = (0, import_node_fs15.readFileSync)(ITERATION_FILE, "utf-8").trim();
16329
16644
  const parts = stored.split(":");
16330
16645
  const iter = parseInt(parts[0], 10);
16331
16646
  const storedCommit = parts[1] ?? "";
16332
16647
  const storedTimestamp = parseInt(parts[2] ?? "0", 10);
16333
- if (isNaN(iter)) return 1;
16334
- if (storedCommit !== currentCommit) return 1;
16648
+ const fingerprint = parts.slice(3).join(":") || null;
16649
+ if (isNaN(iter)) return { iteration: 1, fingerprint: null };
16650
+ if (storedCommit !== currentCommit) return { iteration: 1, fingerprint: null };
16335
16651
  if (storedTimestamp > 0) {
16336
16652
  const elapsed = Math.floor(Date.now() / 1e3) - storedTimestamp;
16337
- if (elapsed > 600) return 1;
16653
+ if (elapsed > 600) return { iteration: 1, fingerprint: null };
16338
16654
  }
16339
- return iter;
16655
+ return { iteration: iter, fingerprint };
16340
16656
  } catch {
16341
- return 1;
16657
+ return { iteration: 1, fingerprint: null };
16342
16658
  }
16343
16659
  }
16344
- function checkMaxIterations(currentCommit, maxIterations = MAX_ITERATIONS, contentHash) {
16345
- const iteration = readIteration(currentCommit, contentHash);
16346
- if (iteration > maxIterations) {
16347
- writeIteration(1, currentCommit, contentHash);
16348
- return {
16349
- skip: `Max Verity iterations (${maxIterations}) reached \u2014 accepting to prevent infinite loop. Human review required before deploying.`,
16350
- iteration
16351
- };
16352
- }
16353
- return { skip: null, iteration };
16660
+ function findingsFingerprint(findings) {
16661
+ const keys = findings.map((f) => `${String(f.pattern_id ?? "?")}|${String(f.file ?? "?")}`).filter((k) => k !== "?|?");
16662
+ return [...new Set(keys)].sort().join(",");
16663
+ }
16664
+ function isSameProblem(previous, current) {
16665
+ if (!previous || !current) return false;
16666
+ const prev = new Set(previous.split(","));
16667
+ return current.split(",").some((k) => prev.has(k));
16354
16668
  }
16355
- function writeIteration(iteration, commit, _contentHash) {
16669
+ function writeIteration(iteration, commit, _contentHash, fingerprint) {
16356
16670
  (0, import_node_fs15.mkdirSync)(VERITY_DIR, { recursive: true });
16357
16671
  const ts = Math.floor(Date.now() / 1e3);
16358
- (0, import_node_fs15.writeFileSync)(ITERATION_FILE, `${iteration}:${commit}:${ts}`);
16672
+ const fp = fingerprint ? `:${fingerprint}` : "";
16673
+ (0, import_node_fs15.writeFileSync)(ITERATION_FILE, `${iteration}:${commit}:${ts}${fp}`);
16359
16674
  }
16360
16675
 
16361
16676
  // src/lib/static-analysis.ts
@@ -16604,7 +16919,7 @@ function resolveTaskContext(opts) {
16604
16919
  // src/lib/cli-version.ts
16605
16920
  function cliVersion() {
16606
16921
  try {
16607
- return true ? "0.28.1-experimental.dfb3ce6" : "dev";
16922
+ return true ? "0.28.1-experimental.e79117b" : "dev";
16608
16923
  } catch {
16609
16924
  return "dev";
16610
16925
  }
@@ -16698,10 +17013,26 @@ function cacheRequest(body) {
16698
17013
  (0, import_node_fs18.mkdirSync)(CACHE_DIR, { recursive: true });
16699
17014
  const suffix = (0, import_node_crypto9.randomBytes)(4).toString("hex");
16700
17015
  const filename = `pending-${Math.floor(Date.now() / 1e3)}-${suffix}.json`;
16701
- (0, import_node_fs18.writeFileSync)(`${CACHE_DIR}/${filename}`, JSON.stringify(body));
17016
+ (0, import_node_fs18.writeFileSync)(`${CACHE_DIR}/${filename}`, JSON.stringify(redactRequest(body)));
16702
17017
  } catch {
16703
17018
  }
16704
17019
  }
17020
+ function redactRequest(body) {
17021
+ const b = body ?? {};
17022
+ const delta = b.code_delta ?? {};
17023
+ const files = Array.isArray(delta.files) ? delta.files : [];
17024
+ return {
17025
+ captured_at: (/* @__PURE__ */ new Date()).toISOString(),
17026
+ undelivered: true,
17027
+ total_files: files.length,
17028
+ total_bytes: files.reduce(
17029
+ (n, f) => n + (typeof f.content === "string" ? f.content.length : 0),
17030
+ 0
17031
+ ),
17032
+ // Paths only. A path is already visible in the repository; the content is not.
17033
+ files: files.map((f) => typeof f.path === "string" ? f.path : "<unknown>").slice(0, 100)
17034
+ };
17035
+ }
16705
17036
  function buildOfflineFallback(reason, staticResults) {
16706
17037
  return {
16707
17038
  // G12 / INV-18 — WARN, not PASS.
@@ -16830,6 +17161,15 @@ var KNOWN_RECORD_TYPES = /* @__PURE__ */ new Set([
16830
17161
  ]);
16831
17162
  var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "NotebookEdit", "MultiEdit"]);
16832
17163
  var DISPATCH_TOOLS = /* @__PURE__ */ new Set(["Task", "Workflow"]);
17164
+ function hasUserText(record) {
17165
+ const message = record.message;
17166
+ const content = message?.content ?? record.content;
17167
+ if (typeof content === "string") return content.trim().length > 0;
17168
+ if (!Array.isArray(content)) return false;
17169
+ return content.some(
17170
+ (b) => b?.type === "text" && typeof b.text === "string" && b.text.trim().length > 0
17171
+ );
17172
+ }
16833
17173
  var COMMAND_CLASSES = [
16834
17174
  [/\b(npm|yarn|pnpm|bun)\s+(run\s+)?test\b|\bvitest\b|\bjest\b|\bpytest\b|\bgo test\b/, "test"],
16835
17175
  [/\b(npm|yarn|pnpm|bun)\s+(run\s+)?build\b|\btsc\b|\bwebpack\b|\bcargo build\b/, "build"],
@@ -16919,8 +17259,8 @@ function commandShape(cmd) {
16919
17259
  var COMMAND_HEAD_CHARS = 80;
16920
17260
  var rootCandidateCache = /* @__PURE__ */ new Map();
16921
17261
  function candidateRoots(repoRoot2) {
16922
- const cached = rootCandidateCache.get(repoRoot2);
16923
- if (cached) return cached;
17262
+ const cached2 = rootCandidateCache.get(repoRoot2);
17263
+ if (cached2) return cached2;
16924
17264
  const norm = repoRoot2.replace(/\\/g, "/").replace(/\/+$/, "");
16925
17265
  const out = [norm];
16926
17266
  try {
@@ -16957,6 +17297,7 @@ function fold(transcriptPath, opts = {}) {
16957
17297
  malformed: 0,
16958
17298
  subagentFiles: 0,
16959
17299
  dispatched: 0,
17300
+ userMessages: 0,
16960
17301
  subagentSkipped: 0,
16961
17302
  compactions: 0,
16962
17303
  complete: false
@@ -16983,6 +17324,7 @@ function fold(transcriptPath, opts = {}) {
16983
17324
  if (type === "system" && record.subtype === "compact_boundary") {
16984
17325
  result.coverage.compactions++;
16985
17326
  }
17327
+ if (type === "user" && hasUserText(record)) result.coverage.userMessages++;
16986
17328
  collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, opts.repoRoot, result.coverage);
16987
17329
  }
16988
17330
  };
@@ -17138,6 +17480,86 @@ function checkConservation(changedFiles, result, repoRoot2) {
17138
17480
  };
17139
17481
  }
17140
17482
 
17483
+ // src/lib/verdict.ts
17484
+ function reconcileCoverage(changed, coverage) {
17485
+ const changedSet = new Set(changed);
17486
+ const reviewed = coverage.reviewed.filter((p) => changedSet.has(p));
17487
+ const claimed = /* @__PURE__ */ new Set([...reviewed, ...coverage.notReviewed.map((n) => n.path)]);
17488
+ const unaccounted = [...changedSet].filter((p) => !claimed.has(p)).sort();
17489
+ const notReviewed = [
17490
+ ...coverage.notReviewed.filter((n) => changedSet.has(n.path)),
17491
+ ...unaccounted.map((path) => ({
17492
+ path,
17493
+ reason: "unaccounted",
17494
+ // Named so the eventual bug report writes itself: some stage removed this
17495
+ // path and did not say so.
17496
+ stage: "unknown-stage",
17497
+ // An undeclared drop is CAPACITY by default. A stage that cannot be
17498
+ // bothered to say why it dropped a file does not get the benefit of the
17499
+ // doubt — that default is what makes forgetting expensive.
17500
+ kind: "capacity"
17501
+ }))
17502
+ ];
17503
+ return {
17504
+ coverage: { reviewed: [...new Set(reviewed)].sort(), notReviewed },
17505
+ unaccounted,
17506
+ balances: unaccounted.length === 0
17507
+ };
17508
+ }
17509
+ function resolveVerdict(proposed, coverage) {
17510
+ if (proposed === "FAIL") return "FAIL";
17511
+ const blocking = coverage.notReviewed.filter((n) => (n.kind ?? "capacity") !== "policy");
17512
+ if (blocking.length === 0) return proposed;
17513
+ return "WARN";
17514
+ }
17515
+ function describeCoverage(coverage, maxPaths = 5) {
17516
+ const relevant = coverage.notReviewed.filter((n) => (n.kind ?? "capacity") !== "policy");
17517
+ if (relevant.length === 0) return null;
17518
+ const byReason = /* @__PURE__ */ new Map();
17519
+ for (const n of relevant) {
17520
+ const key = `${n.reason}`;
17521
+ const list = byReason.get(key) ?? [];
17522
+ list.push(n.path);
17523
+ byReason.set(key, list);
17524
+ }
17525
+ const lines = [];
17526
+ for (const [reason, paths] of [...byReason.entries()].sort()) {
17527
+ const shown = paths.slice(0, maxPaths).join(", ");
17528
+ const more = paths.length > maxPaths ? ` (+${paths.length - maxPaths} more)` : "";
17529
+ lines.push(` ${paths.length} not reviewed \u2014 ${reason}: ${shown}${more}`);
17530
+ }
17531
+ return `NOT A CLEAN REVIEW. ${relevant.length} changed file(s) never reached the reviewer, so this verdict does not cover them:
17532
+ ${lines.join("\n")}
17533
+ Treat those files as UNCHECKED, not as approved.`;
17534
+ }
17535
+ function openBlockingElsewhere(statements, reviewedNow, lineShaAt) {
17536
+ const reviewed = new Set(reviewedNow);
17537
+ const out = [];
17538
+ const seen = /* @__PURE__ */ new Set();
17539
+ for (const s of statements) {
17540
+ if (s.outcome !== "open") continue;
17541
+ if (s.register !== "BLOCK") continue;
17542
+ if (s.carried) continue;
17543
+ if (reviewed.has(s.file)) continue;
17544
+ if (!s.line_sha) continue;
17545
+ if (lineShaAt(s.file, s.line) !== s.line_sha) continue;
17546
+ const key = `${s.file}::${s.pattern_id}`;
17547
+ if (seen.has(key)) continue;
17548
+ seen.add(key);
17549
+ out.push({ file: s.file, line: s.line, pattern_id: s.pattern_id });
17550
+ }
17551
+ return out;
17552
+ }
17553
+ function describeOpenElsewhere(open) {
17554
+ if (open.length === 0) return null;
17555
+ const lines = open.slice(0, 5).map((o) => ` ${o.file}:${o.line} [${o.pattern_id}]`);
17556
+ const more = open.length > 5 ? `
17557
+ (+${open.length - 5} more)` : "";
17558
+ return `STILL OPEN ELSEWHERE. ${open.length} blocking finding(s) Verity raised earlier are still present in files this run did not review:
17559
+ ${lines.join("\n")}${more}
17560
+ This verdict covers the current change only. The tree is not clean.`;
17561
+ }
17562
+
17141
17563
  // src/lib/channel.ts
17142
17564
  var MAX_AGENT_CONTEXT_CHARS = 1500;
17143
17565
  var MAX_AGENT_ITEMS = 5;
@@ -17187,9 +17609,13 @@ function buildAgentContext(input) {
17187
17609
  }
17188
17610
  for (const p of input.pendingItems ?? []) {
17189
17611
  if (lines.length >= MAX_AGENT_ITEMS) break;
17612
+ if (p.pattern_id === "intent-misalignment") continue;
17190
17613
  const text = p.description ?? p.title ?? p.reason;
17191
17614
  if (!text) continue;
17192
- lines.push(renderItem("", text, p.pattern_id, p.file, p.line));
17615
+ const seenBefore = isRepeatOfAny(text, input.priorPendingFingerprints ?? []);
17616
+ lines.push(
17617
+ 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)" : "")
17618
+ );
17193
17619
  }
17194
17620
  if (lines.length === 0) return null;
17195
17621
  const body = `${REPORT_PREFIX}
@@ -17225,6 +17651,39 @@ function channelSilence(input) {
17225
17651
  return null;
17226
17652
  }
17227
17653
 
17654
+ // src/lib/emit.ts
17655
+ var YELLOW2 = "\x1B[33m";
17656
+ var NC2 = "\x1B[0m";
17657
+ function emitVerdict(input) {
17658
+ const exit = input.exit ?? ((code) => process.exit(code));
17659
+ const { coverage, unaccounted } = reconcileCoverage(input.changed, input.coverage);
17660
+ let verdict = resolveVerdict(input.proposed, coverage);
17661
+ const openElsewhere = input.openElsewhere ?? [];
17662
+ if (verdict === "PASS" && openElsewhere.length > 0) verdict = "WARN";
17663
+ const note = [describeCoverage(coverage), describeOpenElsewhere(openElsewhere)].filter(Boolean).join("\n\n") || null;
17664
+ if (unaccounted.length > 0) {
17665
+ process.stderr.write(
17666
+ `${YELLOW2}Verity: ${unaccounted.length} changed file(s) could not be attributed to any review stage \u2014 counted as unreviewed.${NC2}
17667
+ `
17668
+ );
17669
+ }
17670
+ if (verdict === "FAIL") {
17671
+ input.renderBlocking?.();
17672
+ if (input.agentContext) {
17673
+ process.stderr.write(`
17674
+ ${input.agentContext}
17675
+ `);
17676
+ }
17677
+ if (note && !input.silenced) process.stderr.write(`
17678
+ ${YELLOW2}${note}${NC2}
17679
+ `);
17680
+ return exit(2);
17681
+ }
17682
+ const agentBlock = input.silenced ? null : [input.agentContext, note].filter(Boolean).join("\n\n") || null;
17683
+ printJsonCompact(buildHookOutput(verdict, input.userSummary, agentBlock));
17684
+ return exit(0);
17685
+ }
17686
+
17228
17687
  // src/lib/cache-cleanup.ts
17229
17688
  var import_node_fs21 = require("node:fs");
17230
17689
  var import_node_path18 = require("node:path");
@@ -17306,6 +17765,13 @@ function isGitOnlyPrompt(prompt) {
17306
17765
  return true;
17307
17766
  }
17308
17767
  function reconcileAnalysisMode(predictedMode, signals) {
17768
+ const mode = resolveAnalysisMode(predictedMode, signals);
17769
+ if (mode !== "skip") return mode;
17770
+ const windowIsOrphaned = signals.actionSummary?.transcript_windowed === "orphaned";
17771
+ if (windowIsOrphaned && !signals.sessionAuthoredCode) return "standard";
17772
+ return mode;
17773
+ }
17774
+ function resolveAnalysisMode(predictedMode, signals) {
17309
17775
  if (!predictedMode || !isValidMode(predictedMode)) {
17310
17776
  return detectAnalysisMode(
17311
17777
  signals.noFilesChanged,
@@ -17404,46 +17870,6 @@ function shouldWarmRetryAnalyze(result) {
17404
17870
  return false;
17405
17871
  }
17406
17872
 
17407
- // src/lib/skip-detection.ts
17408
- function isBareAckPrompt(prompt) {
17409
- if (typeof prompt !== "string") return false;
17410
- const trimmed = prompt.trim();
17411
- if (trimmed.length === 0) return false;
17412
- if (trimmed.length > 20) return false;
17413
- 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;
17414
- return bareAckPattern.test(trimmed);
17415
- }
17416
- function isReflectionQuestion(response) {
17417
- if (!response || typeof response !== "string") return false;
17418
- const markers = [
17419
- /reflection\s+for\s+future\s+agents/i,
17420
- /what(?:'s|\s+is)\s+one\s+thing\s+you\s+learned/i,
17421
- /say\s+['"]?skip['"]?\s+to\s+skip/i,
17422
- /quick\s+reflection\s+question/i,
17423
- // Post-flip (VRT-21): the agent drafts the reflection itself and, when
17424
- // interactive, asks the user to confirm/correct before recording. That
17425
- // turn authors no code either, so it's still a reflection turn.
17426
- /reflection\s+draft/i,
17427
- /confirm,?\s+correct,?\s+or\s+add/i
17428
- ];
17429
- return markers.some((m) => m.test(response));
17430
- }
17431
- function isMetaTaskLabel(label2) {
17432
- if (label2 === null || label2 === void 0) return false;
17433
- if (typeof label2 !== "string") return false;
17434
- const trimmed = label2.trim();
17435
- if (trimmed.length === 0) return true;
17436
- const metaPatterns = [
17437
- /^verity\s+[\w-]+\s+response$/i,
17438
- // "Verity reflect response"
17439
- /^simple user response$/i,
17440
- /^verity\s+command$/i,
17441
- // "Verity command"
17442
- /^user\s+(question|reply|response|ack)$/i
17443
- ];
17444
- return metaPatterns.some((p) => p.test(trimmed));
17445
- }
17446
-
17447
17873
  // src/lib/transcript.ts
17448
17874
  var import_node_fs22 = require("node:fs");
17449
17875
  var MAX_READ_BYTES = 256 * 1024;
@@ -17457,9 +17883,11 @@ var MAX_SUMMARY_BYTES = 4096;
17457
17883
  var HOME = process.env.HOME ?? "";
17458
17884
  async function extractActionSummary(transcriptPath) {
17459
17885
  try {
17460
- const lines = readTurnLines(transcriptPath);
17461
- if (!lines || lines.length === 0) return null;
17462
- return buildSummary(lines);
17886
+ const read = readTurnLines(transcriptPath);
17887
+ if (!read || read.lines.length === 0) return null;
17888
+ const summary = buildSummary(read.lines);
17889
+ if (summary) summary.transcript_windowed = read.window;
17890
+ return summary;
17463
17891
  } catch {
17464
17892
  return null;
17465
17893
  }
@@ -17473,9 +17901,11 @@ function readTurnLines(transcriptPath) {
17473
17901
  }
17474
17902
  if (size === 0) return null;
17475
17903
  let raw;
17904
+ let windowed = false;
17476
17905
  if (size <= SMALL_FILE_BYTES) {
17477
17906
  raw = (0, import_node_fs22.readFileSync)(transcriptPath, "utf-8");
17478
17907
  } else {
17908
+ windowed = true;
17479
17909
  const buf = Buffer.alloc(Math.min(MAX_READ_BYTES, size));
17480
17910
  const fd = require("node:fs").openSync(transcriptPath, "r");
17481
17911
  try {
@@ -17493,17 +17923,22 @@ function readTurnLines(transcriptPath) {
17493
17923
  const allLines = raw.split("\n").filter((l) => l.trim().length > 0);
17494
17924
  if (allLines.length === 0) return null;
17495
17925
  let turnStart = 0;
17926
+ let boundaryFound = false;
17496
17927
  for (let i = allLines.length - 1; i >= 0; i--) {
17497
17928
  try {
17498
17929
  const parsed = JSON.parse(allLines[i]);
17499
17930
  if (parsed.type === "user" && isRealUserMessage(parsed)) {
17500
17931
  turnStart = i;
17932
+ boundaryFound = true;
17501
17933
  break;
17502
17934
  }
17503
17935
  } catch {
17504
17936
  }
17505
17937
  }
17506
- return allLines.slice(turnStart);
17938
+ return {
17939
+ lines: allLines.slice(turnStart),
17940
+ window: !windowed ? "whole" : boundaryFound ? "windowed" : "orphaned"
17941
+ };
17507
17942
  }
17508
17943
  function isRealUserMessage(parsed) {
17509
17944
  const message = parsed.message;
@@ -17607,6 +18042,13 @@ function buildSummary(lines) {
17607
18042
  files_read: capArray(filesRead, MAX_FILES_LIST),
17608
18043
  files_edited: capArray(filesEdited, MAX_FILES_LIST),
17609
18044
  files_created: capArray(filesCreated, MAX_CREATED_LIST),
18045
+ // The complement of the two caps that affect SCOPE. `files_read` is excluded
18046
+ // deliberately: reading a file is not authoring it, so a capped read list
18047
+ // narrows nothing.
18048
+ capped_out: [
18049
+ ...cappedOut(filesEdited, MAX_FILES_LIST),
18050
+ ...cappedOut(filesCreated, MAX_CREATED_LIST)
18051
+ ],
17610
18052
  searches,
17611
18053
  commands,
17612
18054
  subagents,
@@ -17657,6 +18099,9 @@ function sanitizeCommand(rawCmd) {
17657
18099
  function capArray(set, max) {
17658
18100
  return Array.from(set).slice(0, max);
17659
18101
  }
18102
+ function cappedOut(set, max) {
18103
+ return Array.from(set).slice(max);
18104
+ }
17660
18105
 
17661
18106
  // src/lib/run-mode.ts
17662
18107
  function parseAutonomousEnv(raw) {
@@ -18051,11 +18496,12 @@ async function readStopHookStdin() {
18051
18496
  return empty;
18052
18497
  }
18053
18498
  }
18054
- function agentContextFor(response, intentRepeat = 0) {
18499
+ function agentContextFor(response, intentRepeat = 0, priorPendingFingerprints = []) {
18055
18500
  const metadata = response.metadata ?? {};
18056
18501
  const intent = response.intent_alignment ?? {};
18057
18502
  return buildAgentContext({
18058
18503
  intentRepeat,
18504
+ priorPendingFingerprints,
18059
18505
  gateDecision: String(response.gate_decision ?? ""),
18060
18506
  findings: response.findings ?? [],
18061
18507
  pendingItems: response.pending_items ?? [],
@@ -18066,12 +18512,45 @@ function agentContextFor(response, intentRepeat = 0) {
18066
18512
  });
18067
18513
  }
18068
18514
  var beaconCtx = null;
18069
- async function passAndExit(reason, skip) {
18515
+ async function passAndExit(reason, skip, kindOverride) {
18070
18516
  const sent = await sendSkipBeacon(beaconCtx, skip);
18071
18517
  logEvent("skip", { reason: skip, beacon: sent });
18072
- printJsonCompact({ gate_decision: "PASS", systemMessage: `Verity: ${reason}` });
18518
+ const POLICY_SKIPS = /* @__PURE__ */ new Set([
18519
+ "no-analyzable-files",
18520
+ "verity-command",
18521
+ "bare-acknowledgment",
18522
+ "reflection-prompt",
18523
+ "skip-mode",
18524
+ "zero-increment",
18525
+ "debounce",
18526
+ "no-delta-since-last-review"
18527
+ ]);
18528
+ const skipKind = kindOverride ?? (POLICY_SKIPS.has(skip) ? "policy" : "capacity");
18529
+ const changed = skipCoverageChanged;
18530
+ const { coverage, unaccounted } = reconcileCoverage(changed, {
18531
+ reviewed: [],
18532
+ notReviewed: changed.map((path) => ({ path, reason: skip, stage: "pre-flight", kind: skipKind }))
18533
+ });
18534
+ const verdict = resolveVerdict("PASS", coverage);
18535
+ const note = describeCoverage(coverage);
18536
+ if (unaccounted.length > 0) {
18537
+ logEvent("coverage_unaccounted", { where: "passAndExit", skip, count: unaccounted.length });
18538
+ }
18539
+ const AGENT_SILENT_SKIPS = /* @__PURE__ */ new Set([]);
18540
+ const agentNote = AGENT_SILENT_SKIPS.has(skip) ? null : note;
18541
+ printJsonCompact(
18542
+ buildHookOutput(
18543
+ verdict,
18544
+ `Verity: ${reason}`,
18545
+ // The agent's ONLY input is additionalContext. Sixteen of the nineteen
18546
+ // terminating paths wrote `systemMessage` — the human's field — and told
18547
+ // the agent nothing at all.
18548
+ agentNote
18549
+ )
18550
+ );
18073
18551
  process.exit(0);
18074
18552
  }
18553
+ var skipCoverageChanged = [];
18075
18554
  var EMPTY_STATIC = {
18076
18555
  tool: "@codacy/analysis-cli",
18077
18556
  findings: [],
@@ -18086,7 +18565,7 @@ function runLocalStatic(analyzable, securityFiles, baseline, skipStatic) {
18086
18565
  }
18087
18566
  function localOnlyAndExit(staticResults) {
18088
18567
  printJsonCompact({
18089
- gate_decision: "PASS",
18568
+ gate_decision: "WARN",
18090
18569
  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.",
18091
18570
  unauthenticated: true,
18092
18571
  static_results: staticResults
@@ -18146,6 +18625,7 @@ async function runAnalyze(opts, globals) {
18146
18625
  });
18147
18626
  }
18148
18627
  const { files: allChanged, hasRecentCommitFiles } = getChangedFiles();
18628
+ skipCoverageChanged = allChanged;
18149
18629
  const analyzable = filterAnalyzable(allChanged);
18150
18630
  const reviewable = filterReviewable(allChanged);
18151
18631
  const securityFiles = filterSecurity(allChanged);
@@ -18157,15 +18637,27 @@ async function runAnalyze(opts, globals) {
18157
18637
  const conversation = await readAndClearConversationBuffer(baselineSessionId);
18158
18638
  const specs = discoverSpecs();
18159
18639
  const plans = discoverPlans();
18640
+ const agentAuthoredCodeThisTurn = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0));
18641
+ const turnAuthoredCode = agentAuthoredCodeThisTurn || !!baseline && allForReview.some((f) => changedSinceBaseline(f, baseline));
18642
+ const authorshipIsObservable = !!actionSummary && actionSummary.transcript_windowed !== "orphaned" || !!baseline;
18643
+ const canSeeTurnAuthorship = authorshipIsObservable;
18644
+ let earlyFold = null;
18160
18645
  const latestPrompt = conversation?.prompts?.[conversation.prompts.length - 1]?.prompt ?? "";
18161
18646
  if (/^\s*\/verity-/i.test(latestPrompt)) {
18647
+ const setupAuthored = [
18648
+ ...actionSummary?.files_edited ?? [],
18649
+ ...actionSummary?.files_created ?? []
18650
+ ];
18651
+ if (setupAuthored.length > 0) {
18652
+ const adopted = absorbIntoBaseline(setupAuthored, baselineSessionId);
18653
+ logEvent("baseline_absorbed", { skip: "verity-command", offered: setupAuthored.length, adopted });
18654
+ }
18162
18655
  await passAndExit("Verity command \u2014 skipping analysis", "verity-command");
18163
18656
  }
18164
- if (isBareAckPrompt(latestPrompt)) {
18657
+ if (shouldSkipForBareAck({ prompt: latestPrompt, turnAuthoredCode, canSeeTurnAuthorship })) {
18165
18658
  await passAndExit("Bare acknowledgment \u2014 skipping analysis", "bare-acknowledgment");
18166
18659
  }
18167
- const agentAuthoredCodeThisTurn = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0));
18168
- if (isReflectionQuestion(assistantResponse) && !agentAuthoredCodeThisTurn) {
18660
+ if (isReflectionQuestion(assistantResponse) && !turnAuthoredCode && canSeeTurnAuthorship) {
18169
18661
  await passAndExit("Reflection-prompt turn \u2014 skipping analysis", "reflection-prompt");
18170
18662
  }
18171
18663
  const urlResult = await resolveServiceUrl(globals.serviceUrl);
@@ -18211,7 +18703,11 @@ async function runAnalyze(opts, globals) {
18211
18703
  );
18212
18704
  }
18213
18705
  if (analysisMode === "skip") {
18214
- await passAndExit("Skip mode \u2014 no code work to analyze", "skip-mode");
18706
+ await passAndExit(
18707
+ "Skip mode \u2014 no code work to analyze",
18708
+ "skip-mode",
18709
+ turnAuthoredCode ? "capacity" : void 0
18710
+ );
18215
18711
  }
18216
18712
  let staticResults = {
18217
18713
  tool: "@codacy/analysis-cli",
@@ -18221,7 +18717,8 @@ async function runAnalyze(opts, globals) {
18221
18717
  let codeDelta = {
18222
18718
  files: [],
18223
18719
  total_lines: 0,
18224
- total_files: 0
18720
+ total_files: 0,
18721
+ excluded: []
18225
18722
  };
18226
18723
  let snapshotResult = { has_snapshots: false, diffs: [] };
18227
18724
  let contentHash = null;
@@ -18262,10 +18759,43 @@ async function runAnalyze(opts, globals) {
18262
18759
  contentHash = hashResult.hash;
18263
18760
  if (analysisMode !== "plan") {
18264
18761
  const scoped = scopeToAuthored(allForReview, actionSummary);
18265
- if (scoped.signal === "none-authored" && !hasNonEditAuthorship(actionSummary, sessionAuthoredCode)) {
18762
+ const canTrustNoneAuthored = scoped.signal === "none-authored" && authorshipIsObservable;
18763
+ if (canTrustNoneAuthored && !hasNonEditAuthorship(actionSummary, sessionAuthoredCode)) {
18266
18764
  await passAndExit("No agent-authored code this turn \u2014 working-tree changes were not authored by this session", "zero-increment");
18267
18765
  }
18268
- const baseForReview = scoped.signal === "authored" && scoped.files.length > 0 ? scoped.files : allForReview;
18766
+ if (scoped.signal === "none-authored" && !authorshipIsObservable) {
18767
+ logEvent("none_authored_unverifiable", {
18768
+ reason: "orphaned_window_no_baseline",
18769
+ would_have_skipped: allForReview.length
18770
+ });
18771
+ }
18772
+ const narrowingIsTrustworthy = scoped.signal === "authored" && scoped.files.length > 0 && actionSummary?.transcript_windowed !== "orphaned";
18773
+ let recoveredScope = [];
18774
+ if (!narrowingIsTrustworthy && transcriptPath) {
18775
+ try {
18776
+ earlyFold = fold(transcriptPath, { changedFiles: allForReview, repoRoot: repoRoot() });
18777
+ const authoredWhole = new Set(earlyFold.authored.map((a) => a.p));
18778
+ if (authoredWhole.size > 0) {
18779
+ const root = repoRoot();
18780
+ recoveredScope = allForReview.filter((f) => authoredWhole.has(toRepoRelative(f, root)));
18781
+ }
18782
+ logEvent("scope_recovered_from_fold", {
18783
+ window_saw: scoped.files.length,
18784
+ fold_saw: authoredWhole.size,
18785
+ recovered: recoveredScope.length,
18786
+ would_have_widened_to: allForReview.length
18787
+ });
18788
+ } catch {
18789
+ earlyFold = null;
18790
+ }
18791
+ }
18792
+ if (!narrowingIsTrustworthy && scoped.signal === "authored" && recoveredScope.length === 0) {
18793
+ logEvent("scope_widened_orphaned_window", {
18794
+ would_have_sent: scoped.files.length,
18795
+ widened_to: allForReview.length
18796
+ });
18797
+ }
18798
+ const baseForReview = narrowingIsTrustworthy ? scoped.files : recoveredScope.length > 0 ? recoveredScope : allForReview;
18269
18799
  const recentForReview = narrowToRecent(baseForReview, baselineSessionId);
18270
18800
  if (!opts.skipStatic && isCodacyAvailable()) {
18271
18801
  let allScannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles]));
@@ -18286,7 +18816,11 @@ async function runAnalyze(opts, globals) {
18286
18816
  if (assistantResponse) {
18287
18817
  analysisMode = "plan";
18288
18818
  } else {
18289
- await passAndExit("No files within size limits to analyze", "size-limit");
18819
+ await passAndExit(
18820
+ "No files within size limits to analyze",
18821
+ "size-limit",
18822
+ codeDelta.excluded.length > 0 ? "capacity" : "policy"
18823
+ );
18290
18824
  }
18291
18825
  }
18292
18826
  }
@@ -18299,19 +18833,13 @@ async function runAnalyze(opts, globals) {
18299
18833
  snapshotResult = generateSnapshotDiffs(codeDelta.files);
18300
18834
  }
18301
18835
  currentCommit = getCurrentCommit();
18302
- const maxIterations = parseInt(opts.maxIterations, 10);
18303
- const iterResult = checkMaxIterations(currentCommit, maxIterations, contentHash ?? void 0);
18304
- if (iterResult.skip) await passAndExit(iterResult.skip, "iteration-cap");
18305
- iteration = iterResult.iteration;
18836
+ iteration = readIterationState(currentCommit).iteration;
18306
18837
  }
18307
18838
  }
18308
18839
  if (analysisMode === "plan") {
18309
18840
  recordAnalysisStart();
18310
18841
  currentCommit = getCurrentCommit();
18311
- const maxIterations = parseInt(opts.maxIterations, 10);
18312
- const iterResult = checkMaxIterations(currentCommit, maxIterations);
18313
- if (iterResult.skip) await passAndExit(iterResult.skip, "iteration-cap");
18314
- iteration = iterResult.iteration;
18842
+ iteration = readIterationState(currentCommit).iteration;
18315
18843
  }
18316
18844
  const contextFiles = gatherContextFiles(contextFilePaths, codeDelta.files);
18317
18845
  for (const f of codeDelta.files) {
@@ -18378,7 +18906,7 @@ async function runAnalyze(opts, globals) {
18378
18906
  let foldConservation = null;
18379
18907
  if (transcriptPath) {
18380
18908
  try {
18381
- foldResult = fold(transcriptPath, { changedFiles: allForReview, repoRoot: repoRoot() });
18909
+ foldResult = earlyFold ?? fold(transcriptPath, { changedFiles: allForReview, repoRoot: repoRoot() });
18382
18910
  foldConservation = checkConservation(allForReview, foldResult, repoRoot());
18383
18911
  if (!foldConservation.holds) {
18384
18912
  process.stderr.write(
@@ -18460,7 +18988,10 @@ async function runAnalyze(opts, globals) {
18460
18988
  priorState.capabilities
18461
18989
  );
18462
18990
  memory = recallMemory(memorySession.d, memorySession.identity, {
18463
- currentSessionKey: memorySession.identity.sessionKey
18991
+ currentSessionKey: memorySession.identity.sessionKey,
18992
+ // The independent witness. Only meaningful when a transcript was folded —
18993
+ // otherwise it stays undefined and capture coverage reads as UNKNOWN.
18994
+ ...foldResult && { userMessagesSeen: foldResult.coverage.userMessages }
18464
18995
  });
18465
18996
  if (memory && !memory.provenanceHolds) {
18466
18997
  process.stderr.write("Verity: working-memory provenance check failed \u2014 recall suppressed.\n");
@@ -18481,7 +19012,30 @@ async function runAnalyze(opts, globals) {
18481
19012
  hasUserPrompt: (conversation?.prompts?.length ?? 0) > 0,
18482
19013
  isTTY: process.stdout.isTTY === true
18483
19014
  });
19015
+ const excludedByReason = {};
19016
+ for (const e of codeDelta.excluded ?? []) {
19017
+ excludedByReason[e.reason] = (excludedByReason[e.reason] ?? 0) + 1;
19018
+ }
19019
+ const coverageTelemetry = {
19020
+ // git's whole answer, before ANY narrowing. The number that has never been sent.
19021
+ changed_all: allChanged.length,
19022
+ analyzable: analyzable.length,
19023
+ reviewable: reviewable.length,
19024
+ security: securityFiles.length,
19025
+ // after the allowlist, before authorship scoping and the caps
19026
+ for_review: allForReview.length,
19027
+ // what actually reaches the reviewer
19028
+ sent: codeDelta.files.length,
19029
+ // the two silent narrowings, counted separately so they can be told apart
19030
+ capped_out: actionSummary?.capped_out?.length ?? 0,
19031
+ excluded: (codeDelta.excluded ?? []).length,
19032
+ excluded_by_reason: excludedByReason,
19033
+ // was the transcript itself truncated? The 256 KB window means "this turn"
19034
+ // can quietly mean "the last 256 KB of it".
19035
+ transcript_windowed: actionSummary?.transcript_windowed ?? null
19036
+ };
18484
19037
  const requestBody = {
19038
+ coverage_telemetry: coverageTelemetry,
18485
19039
  static_results: staticResults,
18486
19040
  code_delta: codeDelta,
18487
19041
  changed_files: allForReview,
@@ -18567,6 +19121,54 @@ async function runAnalyze(opts, globals) {
18567
19121
  // replaced a population floor with ≈35% power that was sub-integer for
18568
19122
  // three-quarters of the fleet.
18569
19123
  conservation: foldConservation,
19124
+ // ⚠ VRT-52 — RECORDED, NOT APPLIED. The number nobody has.
19125
+ //
19126
+ // The whole "task-scoped delta" design space rests on an assumption that
19127
+ // has been observed exactly ONCE: that delta files routinely belong to
19128
+ // earlier work. Three designs were built on it and all three were killed
19129
+ // adversarially — two by measurement — so before another is attempted,
19130
+ // measure the base rate.
19131
+ //
19132
+ // `authored_under_earlier_goal` counts delta paths whose LAST authorship
19133
+ // event precedes the seq of the goal now in force. Both numbers come from
19134
+ // the same append-only counter (`nextSeq`), so the comparison is exact.
19135
+ //
19136
+ // Keyed on the GOAL, deliberately, not on the task id. The task classifier
19137
+ // reported `is_new_task` on two consecutive turns of one task 25 seconds
19138
+ // apart, so a task-keyed number would measure its unreliability rather
19139
+ // than the phenomenon. And this only became meaningful once `recordGoal`
19140
+ // stopped letting a bare "ok" supersede the goal — before that the seq
19141
+ // advanced every turn and this would have degenerated to "not edited this
19142
+ // turn", which is the exact mistake that sank one of the three designs.
19143
+ //
19144
+ // Changes no payload the reviewer sees, no narrowing, no verdict.
19145
+ vrt52: (() => {
19146
+ const goalSeq = memory?.projection.goal?.seq;
19147
+ if (goalSeq === void 0 || !memorySession) return { known: false };
19148
+ const lastSeq2 = new Map(
19149
+ foldDossier(memorySession.d).authored_all.map((a) => [a.path, a.last_seq])
19150
+ );
19151
+ let earlier = 0;
19152
+ let unknown = 0;
19153
+ for (const f of codeDelta.files) {
19154
+ const seen = lastSeq2.get(f.path);
19155
+ if (seen === void 0) unknown++;
19156
+ else if (seen < goalSeq) earlier++;
19157
+ }
19158
+ return {
19159
+ known: true,
19160
+ goal_seq: goalSeq,
19161
+ delta: codeDelta.files.length,
19162
+ // Files this delta carries that were last written under an EARLIER
19163
+ // instruction. If this stays near zero, VRT-52's code half is
19164
+ // unnecessary and should be closed saying so.
19165
+ authored_under_earlier_goal: earlier,
19166
+ // Delta files the dossier has no authorship record for at all —
19167
+ // pre-existing tree state, or an authorship channel the fold cannot
19168
+ // see. Reported separately so a blind spot is never counted as a zero.
19169
+ no_authorship_record: unknown
19170
+ };
19171
+ })(),
18570
19172
  // P2 — RECORDED, NOT APPLIED. What this run WOULD have reviewed if it
18571
19173
  // narrowed to the within-session increment: what changed since the last
18572
19174
  // VERDICT rather than since task start.
@@ -18599,7 +19201,20 @@ async function runAnalyze(opts, globals) {
18599
19201
  const intentContext = {};
18600
19202
  if (conversation && conversation.prompts.length > 0) {
18601
19203
  const latest = conversation.prompts[conversation.prompts.length - 1];
18602
- intentContext.user_prompt = latest.prompt;
19204
+ const goalPrompt = resolveGoalPrompt(conversation.prompts) ?? { entry: latest, turnsBack: 0 };
19205
+ intentContext.user_prompt = goalPrompt.entry.prompt;
19206
+ if (isContinuationPrompt(intentContext.user_prompt)) {
19207
+ const carried = memory?.projection.goal?.text;
19208
+ if (carried && !isContinuationPrompt(carried)) {
19209
+ intentContext.continuation_prompt = latest.prompt;
19210
+ intentContext.user_prompt = carried;
19211
+ logEvent("goal_from_dossier", { chars: carried.length });
19212
+ }
19213
+ }
19214
+ if (goalPrompt.turnsBack > 0) {
19215
+ intentContext.continuation_prompt = latest.prompt;
19216
+ logEvent("goal_walked_back", { turns_back: goalPrompt.turnsBack });
19217
+ }
18603
19218
  intentContext.session_id = latest.session_id || void 0;
18604
19219
  intentContext.prompt_captured_at = latest.captured_at || void 0;
18605
19220
  if (conversation.prompts.length > 1) {
@@ -18691,6 +19306,85 @@ async function runAnalyze(opts, globals) {
18691
19306
  const response = result.data;
18692
19307
  const decision = response.gate_decision ?? "(unrecognised)";
18693
19308
  const sentPaths = codeDelta.files.map((f) => f.path);
19309
+ let openElsewhere = [];
19310
+ if (memorySession) {
19311
+ try {
19312
+ const st = foldDossier(memorySession.d);
19313
+ openElsewhere = openBlockingElsewhere(st.statements, sentPaths, (file, line) => {
19314
+ try {
19315
+ const src = (0, import_node_fs24.readFileSync)((0, import_node_path20.join)(repoRoot(), file), "utf8").split("\n");
19316
+ const at = src[line - 1];
19317
+ return at === void 0 ? null : lineSha(at);
19318
+ } catch {
19319
+ return null;
19320
+ }
19321
+ });
19322
+ } catch {
19323
+ }
19324
+ }
19325
+ const reviewCoverage = {
19326
+ reviewed: sentPaths,
19327
+ // Declared drops from the stages that DO report themselves today. The other
19328
+ // stages surface via `unaccounted`, which is the tripwire, not the design.
19329
+ notReviewed: [
19330
+ // Every exit from the collection loop, each named. Six reasons where there
19331
+ // used to be two recorded and four silent — the silent ones including the
19332
+ // per-file size cap, which could drop a whole source file without leaving a
19333
+ // trace anywhere in the payload or the run row.
19334
+ ...codeDelta.excluded,
19335
+ // The server-side 300-line middle-out truncation. It only bites on the
19336
+ // full-file branch (a first analysis, before snapshots exist) because
19337
+ // analyze normally sends diffs — but on that branch the reviewer sees the
19338
+ // first and last 100 lines and nothing between, and until now said so to
19339
+ // nobody. CAPACITY: a partial look is not a look.
19340
+ ...(response.metadata?.truncated_files ?? []).map((path) => ({
19341
+ path,
19342
+ reason: "file-middle-truncated-300-lines",
19343
+ stage: "prompt-builder",
19344
+ kind: "capacity"
19345
+ })),
19346
+ // The 20-entry edit cap. CAPACITY, and the sharpest of the lot: it narrows
19347
+ // what is REVIEWED, not merely what is summarised — a session editing 25
19348
+ // files had five silently excluded from the reviewed set.
19349
+ ...(actionSummary?.capped_out ?? []).map((path) => ({
19350
+ path,
19351
+ reason: "edit-list-cap-20",
19352
+ stage: "extractActionSummary",
19353
+ kind: "capacity"
19354
+ })),
19355
+ // ⚠ BASELINE SCOPING — the biggest source of false NOT A CLEAN REVIEW.
19356
+ //
19357
+ // The universe is `allChanged`, git's whole dirty tree. The reviewed set is
19358
+ // scoped to what THIS SESSION authored (the VRT-26 contamination cure), so
19359
+ // every pre-existing dirty file is in the universe, absent from `reviewed`,
19360
+ // and — until now — declared by nobody. It fell through to `unaccounted`,
19361
+ // became capacity, and produced "NOT A CLEAN REVIEW: admin.js" over a file
19362
+ // that was never this session's to review.
19363
+ //
19364
+ // Measured 2026-08-04: three consecutive runs over an untouched tree gave
19365
+ // three different answers — .claude/settings.json, then admin.js, then six
19366
+ // files — because each run took a different path and each path had a
19367
+ // different idea of the universe. POLICY: not this session's work is not a
19368
+ // coverage gap, it is the cure working.
19369
+ ...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) => ({
19370
+ path,
19371
+ reason: "not-authored-this-session",
19372
+ stage: "baseline-scoping",
19373
+ kind: "policy"
19374
+ })),
19375
+ // The extension allowlist, and it is POLICY rather than capacity: a changed
19376
+ // README was never going to be reviewed, and treating that as a coverage
19377
+ // gap would downgrade nearly every PASS to WARN until WARN meant nothing.
19378
+ // Recorded so the ledger balances and so "what did Verity ignore entirely"
19379
+ // is answerable — but it never touches the verdict.
19380
+ ...allChanged.filter((p) => !analyzable.includes(p) && !reviewable.includes(p) && !securityFiles.includes(p)).map((path) => ({
19381
+ path,
19382
+ reason: "not-a-reviewed-file-type",
19383
+ stage: "extension-allowlist",
19384
+ kind: "policy"
19385
+ }))
19386
+ ]
19387
+ };
18694
19388
  const watermarkHash = sentPaths.length > 0 ? computeContentHash(sentPaths) : contentHash;
18695
19389
  const watermarkIsPartial = !!codeDelta.truncated;
18696
19390
  let silenced = null;
@@ -18721,6 +19415,13 @@ async function runAnalyze(opts, globals) {
18721
19415
  });
18722
19416
  }
18723
19417
  let intentRepeatCount = 0;
19418
+ const priorPendingFingerprints = memorySession ? (() => {
19419
+ try {
19420
+ return foldDossier(memorySession.d).meta.recent_pending_sigs ?? [];
19421
+ } catch {
19422
+ return [];
19423
+ }
19424
+ })() : [];
18724
19425
  if (memorySession) {
18725
19426
  try {
18726
19427
  recordVerdict(memorySession.d, {
@@ -18743,7 +19444,10 @@ async function runAnalyze(opts, globals) {
18743
19444
  // What next turn reads as `emittedLast`. A suppressed turn did not
18744
19445
  // speak, so it cannot be the cause of the turn after it — which is what
18745
19446
  // keeps this from becoming a permanent gag.
18746
- emitted: !silenced
19447
+ emitted: !silenced,
19448
+ // Fingerprinted for the NEXT turn's repeat check. Reviewer pending items
19449
+ // carry no `pattern_id`, so their content is the only available key.
19450
+ pendingTexts: (response.pending_items ?? []).map((p) => String(p.description ?? p.title ?? p.reason ?? "")).filter(Boolean)
18747
19451
  });
18748
19452
  intentRepeatCount = Math.max(0, (foldDossier(memorySession.d).meta.intent_repeat?.consecutive ?? 1) - 1);
18749
19453
  } catch {
@@ -18843,9 +19547,41 @@ async function runAnalyze(opts, globals) {
18843
19547
  reverify_by: response.reverify_by
18844
19548
  });
18845
19549
  const grantNudge = grantWarning ? ` \u26A0\uFE0F ${grantWarning}` : "";
18846
- switch (decision) {
19550
+ let capReleased = false;
19551
+ let effectiveDecision = decision;
19552
+ if (decision === "FAIL") {
19553
+ const blocking = (response.findings ?? []).filter((f) => {
19554
+ const sev = String(f.severity ?? "").toLowerCase();
19555
+ return sev === "critical" || sev === "high";
19556
+ });
19557
+ const fingerprint = findingsFingerprint(blocking);
19558
+ const prior = readIterationState(currentCommit);
19559
+ const sameProblem = isSameProblem(prior.fingerprint, fingerprint);
19560
+ const nextIteration = sameProblem ? prior.iteration + 1 : 1;
19561
+ const maxIterations = parseInt(opts.maxIterations, 10);
19562
+ writeIteration(nextIteration, currentCommit, contentHash ?? void 0, fingerprint);
19563
+ iteration = nextIteration;
19564
+ if (nextIteration > maxIterations) {
19565
+ capReleased = true;
19566
+ effectiveDecision = "WARN";
19567
+ logEvent("iteration_cap_released", { iteration: nextIteration, fingerprint });
19568
+ }
19569
+ }
19570
+ if (capReleased) {
19571
+ const findings = response.findings ?? [];
19572
+ const lines = findings.slice(0, 5).map((f) => ` [${String(f.severity ?? "?").toUpperCase()}] ${String(f.title ?? f.message ?? "")} (${String(f.file ?? "?")}:${String(f.line ?? "?")})`);
19573
+ emitVerdict({
19574
+ proposed: "WARN",
19575
+ changed: skipCoverageChanged,
19576
+ coverage: reviewCoverage,
19577
+ 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.
19578
+ ${lines.join("\n")}`,
19579
+ agentContext: null,
19580
+ silenced: true
19581
+ });
19582
+ }
19583
+ switch (effectiveDecision) {
18847
19584
  case "FAIL": {
18848
- writeIteration(iteration + 1, currentCommit, contentHash ?? void 0);
18849
19585
  const assessment = response.assessment;
18850
19586
  const narrative = assessment?.narrative ?? "";
18851
19587
  const findings = response.findings ?? [];
@@ -18922,7 +19658,19 @@ ${YELLOW}${loginNudge.trim()}${NC}
18922
19658
  if (grantNudge) process.stderr.write(`
18923
19659
  ${YELLOW}${grantNudge.trim()}${NC}
18924
19660
  `);
18925
- process.exit(2);
19661
+ emitVerdict({
19662
+ proposed: "FAIL",
19663
+ changed: skipCoverageChanged,
19664
+ coverage: reviewCoverage,
19665
+ userSummary: "",
19666
+ // Subject to the SAME cycle cut as PASS/WARN. Suppressing here is safe:
19667
+ // the findings themselves are rendered above by the blocking renderer,
19668
+ // so what the cut removes is the repeated commentary, never the defect.
19669
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount, priorPendingFingerprints),
19670
+ // The coverage note is silenced with it — half a channel is still a channel.
19671
+ silenced: !!silenced,
19672
+ openElsewhere
19673
+ });
18926
19674
  break;
18927
19675
  }
18928
19676
  case "PASS": {
@@ -18934,10 +19682,16 @@ ${YELLOW}${grantNudge.trim()}${NC}
18934
19682
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
18935
19683
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
18936
19684
  userSummary += loginNudge + grantNudge;
18937
- printJsonCompact(
18938
- buildHookOutput("PASS", userSummary, silenced ? null : agentContextFor(response, intentRepeatCount))
18939
- );
18940
- process.exit(0);
19685
+ emitVerdict({
19686
+ proposed: "PASS",
19687
+ changed: skipCoverageChanged,
19688
+ coverage: reviewCoverage,
19689
+ userSummary,
19690
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount, priorPendingFingerprints),
19691
+ // The coverage note is silenced with it — half a channel is still a channel.
19692
+ silenced: !!silenced,
19693
+ openElsewhere
19694
+ });
18941
19695
  break;
18942
19696
  }
18943
19697
  case "WARN": {
@@ -18948,10 +19702,16 @@ ${YELLOW}${grantNudge.trim()}${NC}
18948
19702
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
18949
19703
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
18950
19704
  userSummary += loginNudge + grantNudge;
18951
- printJsonCompact(
18952
- buildHookOutput("WARN", userSummary, silenced ? null : agentContextFor(response, intentRepeatCount))
18953
- );
18954
- process.exit(0);
19705
+ emitVerdict({
19706
+ proposed: "WARN",
19707
+ changed: skipCoverageChanged,
19708
+ coverage: reviewCoverage,
19709
+ userSummary,
19710
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount, priorPendingFingerprints),
19711
+ // The coverage note is silenced with it — half a channel is still a channel.
19712
+ silenced: !!silenced,
19713
+ openElsewhere
19714
+ });
18955
19715
  break;
18956
19716
  }
18957
19717
  default: {
@@ -19064,8 +19824,8 @@ async function runReview(opts, globals) {
19064
19824
  for (const p of specPaths) {
19065
19825
  if (!(0, import_node_fs26.existsSync)(p)) continue;
19066
19826
  try {
19067
- const { readFileSync: readFileSync15 } = await import("node:fs");
19068
- const content = readFileSync15(p, "utf-8");
19827
+ const { readFileSync: readFileSync16 } = await import("node:fs");
19828
+ const content = readFileSync16(p, "utf-8");
19069
19829
  specs.push({ path: p, content: content.slice(0, 10240) });
19070
19830
  } catch {
19071
19831
  }
@@ -19868,6 +20628,13 @@ async function confirmExistingLogin(serviceUrl, remote, opts) {
19868
20628
  return "handled";
19869
20629
  }
19870
20630
  if (!who.ok) {
20631
+ const denial = authDenialRemedy(who.error);
20632
+ if (denial) {
20633
+ console.log("");
20634
+ printWarn(`Your existing Verity credential was rejected (${denial.code}).`);
20635
+ printInfo(` ${denial.remedy}`);
20636
+ return "drive-login";
20637
+ }
19871
20638
  if (existing.data.userId != null) {
19872
20639
  printInfo(`Logged in as ${existing.data.email ?? `user #${existing.data.userId}`} (cached \u2014 could not reach the Verity service). \u2713`);
19873
20640
  } else {
@@ -19883,15 +20650,14 @@ async function confirmExistingLogin(serviceUrl, remote, opts) {
19883
20650
  return "drive-login";
19884
20651
  }
19885
20652
  async function runOptionalAuth(resolution, opts = {}) {
19886
- let serviceUrl = resolution?.url ?? DEFAULT_SERVICE_URL;
19887
- let healed = false;
19888
- if (resolution) {
19889
- const heal = await maybeHealServiceUrl(resolution, opts.verbose);
19890
- serviceUrl = heal.serviceUrl;
19891
- healed = heal.healed;
19892
- if (healed) {
19893
- printInfo(" Log in below to re-register this project and repair ~/.verity/credentials.");
19894
- }
20653
+ if (resolution.source === "default") {
20654
+ printInfo(`No Verity service configured on this machine \u2014 using the default: ${resolution.url}`);
20655
+ }
20656
+ const heal = await maybeHealServiceUrl(resolution, opts.verbose);
20657
+ const serviceUrl = heal.serviceUrl;
20658
+ const healed = heal.healed;
20659
+ if (healed) {
20660
+ printInfo(" Log in below to re-register this project and repair ~/.verity/credentials.");
19895
20661
  }
19896
20662
  let remote = "";
19897
20663
  try {
@@ -19909,8 +20675,10 @@ async function runOptionalAuth(resolution, opts = {}) {
19909
20675
  if (process.stdin.isTTY && process.stdout.isTTY) {
19910
20676
  console.log("");
19911
20677
  console.log(" Signing in is optional. What it does:");
19912
- console.log(" - Confirms you have write access to this repository. The GitHub token");
19913
- console.log(" is used once to verify that, then discarded \u2014 Verity never stores it.");
20678
+ console.log(" - Confirms which repositories you can write to. The GitHub token is");
20679
+ console.log(" used once for that check, then discarded \u2014 Verity never stores it.");
20680
+ console.log(" - One login covers every repository you can write to \u2014 other repos");
20681
+ console.log(" need no further sign-in on this machine.");
19914
20682
  console.log(" - It does NOT give Verity access to your code. Code checked by the gate");
19915
20683
  console.log(" is analyzed in memory and discarded \u2014 we never store your code.");
19916
20684
  console.log(" - It is required to store and access run history for this repo");
@@ -19925,17 +20693,10 @@ async function runOptionalAuth(resolution, opts = {}) {
19925
20693
  localOnlyNote();
19926
20694
  return;
19927
20695
  }
19928
- if (!remote) {
19929
- printWarn("No git remote found \u2014 cannot authenticate yet.");
19930
- localOnlyNote();
19931
- return;
19932
- }
19933
- const projectName = parseRemote(remote)?.repo ?? (0, import_node_path23.basename)(process.cwd());
19934
20696
  printInfo("Authenticating with GitHub\u2026");
19935
- const result = await registerProject({ projectName, remote, serviceUrl, verbose: opts.verbose });
20697
+ const result = await loginOnce({ serviceUrl, remote: remote || void 0, verbose: opts.verbose });
19936
20698
  if (result.ok) {
19937
- const who = result.data.email ?? (result.data.userId != null ? `user #${result.data.userId}` : null);
19938
- printInfo(`Logged in${who ? ` as ${who}` : ""} \u2713 \u2014 runs, history, and cloud memory now sync to Verity.`);
20699
+ await reportLoginOutcome(result.data, { remote: remote || void 0, verbose: opts.verbose });
19939
20700
  } else {
19940
20701
  printWarn(`Authentication did not complete: ${result.error}`);
19941
20702
  localOnlyNote();
@@ -20086,8 +20847,8 @@ function registerInitCommand(program2) {
20086
20847
  console.log("");
20087
20848
  try {
20088
20849
  const globals = program2.opts();
20089
- const urlResult = await resolveServiceUrlDetailed(globals.serviceUrl);
20090
- await runOptionalAuth(urlResult.ok ? urlResult.data : null, {
20850
+ const resolution = await resolveServiceUrlForAuth(globals.serviceUrl);
20851
+ await runOptionalAuth(resolution, {
20091
20852
  token: globals.token,
20092
20853
  verbose: globals.verbose
20093
20854
  });
@@ -20703,6 +21464,12 @@ function registerTelemetryCommands(program2) {
20703
21464
  printError(urlResult.error);
20704
21465
  process.exit(1);
20705
21466
  }
21467
+ const pin = await checkTokenPin(tokenResult.data.token, urlResult.data);
21468
+ if (!pin.attach && pin.reason === "unpinnable") {
21469
+ printWarn("Your Verity credential does not record which service issued it, so this endpoint");
21470
+ printWarn(" cannot be verified \u2014 a repository could point your telemetry elsewhere.");
21471
+ printWarn(' Run "verity login" to re-issue the credential; Verity can check it after that.');
21472
+ }
20706
21473
  const result = await installTelemetry(urlResult.data);
20707
21474
  if (!result.ok) {
20708
21475
  printError(result.error);
@@ -20751,7 +21518,8 @@ function registerTelemetryCommands(program2) {
20751
21518
  }
20752
21519
 
20753
21520
  // src/cli.ts
20754
- program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.dfb3ce6").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async () => {
21521
+ program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.e79117b").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async () => {
21522
+ setUserNamedServiceUrl(program.opts().serviceUrl);
20755
21523
  try {
20756
21524
  await foldLegacyLocalCredential();
20757
21525
  } catch {