@codacy/verity-cli 0.28.1-experimental.c2dc717 → 0.28.1-experimental.db64e52

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 +603 -171
  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);
@@ -13649,6 +13754,84 @@ function isMetaTaskLabel(label2) {
13649
13754
  ];
13650
13755
  return metaPatterns.some((p) => p.test(trimmed));
13651
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
+ }
13652
13835
 
13653
13836
  // src/lib/dossier.ts
13654
13837
  var import_node_fs9 = require("node:fs");
@@ -13658,6 +13841,7 @@ var MAX_LINE_BYTES = 4096;
13658
13841
  var MAX_GOAL_CHARS = 2e3;
13659
13842
  var GOAL_KEEP = 8;
13660
13843
  var GOAL_TOTAL_CAP = 32;
13844
+ var RECENT_PENDING_CAP = 20;
13661
13845
  var HASH_WIDTH = 16;
13662
13846
  var AUTHORED_CAP = 300;
13663
13847
  var NOT_MINE_CAP = 300;
@@ -14026,6 +14210,10 @@ function reduce(state, events, now) {
14026
14210
  consecutiveIdle: ev.idle === false ? 0 : (state.meta.channel?.consecutiveIdle ?? 0) + 1
14027
14211
  };
14028
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
+ }
14029
14217
  if (ev.intent_sig) {
14030
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 };
14031
14219
  } else {
@@ -14250,12 +14438,12 @@ function readFoldCache(d) {
14250
14438
  if (!(0, import_node_fs9.existsSync)(d.foldPath)) return null;
14251
14439
  const raw = JSON.parse((0, import_node_fs9.readFileSync)(d.foldPath, "utf8"));
14252
14440
  if (raw?.v !== 1) return null;
14253
- const cached = expandState(raw);
14254
- if (!cached?.meta) return null;
14441
+ const cached2 = expandState(raw);
14442
+ if (!cached2?.meta) return null;
14255
14443
  const size = (0, import_node_fs9.existsSync)(d.eventsPath) ? (0, import_node_fs9.statSync)(d.eventsPath).size : 0;
14256
14444
  const rotations = (0, import_node_fs9.existsSync)(d.rotatedDir) ? (0, import_node_fs9.readdirSync)(d.rotatedDir).filter((f) => f.endsWith(".jsonl")).length : 0;
14257
- if (cached.meta.upto_offset !== size || cached.meta.rotations !== rotations) return null;
14258
- return cached;
14445
+ if (cached2.meta.upto_offset !== size || cached2.meta.rotations !== rotations) return null;
14446
+ return cached2;
14259
14447
  } catch {
14260
14448
  return null;
14261
14449
  }
@@ -14882,6 +15070,13 @@ function recordVerdict(d, v) {
14882
15070
  branch: v.branch,
14883
15071
  decision: v.decision,
14884
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
+ },
14885
15080
  emitted: v.emitted === true,
14886
15081
  idle: v.idle !== false,
14887
15082
  ...v.intent?.verdict && { intent_verdict: v.intent.verdict },
@@ -15493,8 +15688,8 @@ function preImage(repoRelPath, baseline) {
15493
15688
  perBaseline = /* @__PURE__ */ new Map();
15494
15689
  preImageCache.set(baseline, perBaseline);
15495
15690
  }
15496
- const cached = perBaseline.get(repoRelPath);
15497
- if (cached) return cached;
15691
+ const cached2 = perBaseline.get(repoRelPath);
15692
+ if (cached2) return cached2;
15498
15693
  const resolved = resolvePreImage(repoRelPath, baseline);
15499
15694
  perBaseline.set(repoRelPath, resolved);
15500
15695
  return resolved;
@@ -15538,6 +15733,34 @@ ${addedLines}`,
15538
15733
  }
15539
15734
  return { diffs, has_baseline: true };
15540
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
+ }
15541
15764
  function changedSinceBaseline(repoRelPath, baseline) {
15542
15765
  const pre = preImage(repoRelPath, baseline);
15543
15766
  let current;
@@ -16414,40 +16637,40 @@ function narrowToRecent(files, sessionId) {
16414
16637
  });
16415
16638
  return recent.length > 0 ? recent : files;
16416
16639
  }
16417
- function readIteration(currentCommit, _contentHash) {
16418
- 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 };
16419
16642
  try {
16420
16643
  const stored = (0, import_node_fs15.readFileSync)(ITERATION_FILE, "utf-8").trim();
16421
16644
  const parts = stored.split(":");
16422
16645
  const iter = parseInt(parts[0], 10);
16423
16646
  const storedCommit = parts[1] ?? "";
16424
16647
  const storedTimestamp = parseInt(parts[2] ?? "0", 10);
16425
- if (isNaN(iter)) return 1;
16426
- 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 };
16427
16651
  if (storedTimestamp > 0) {
16428
16652
  const elapsed = Math.floor(Date.now() / 1e3) - storedTimestamp;
16429
- if (elapsed > 600) return 1;
16653
+ if (elapsed > 600) return { iteration: 1, fingerprint: null };
16430
16654
  }
16431
- return iter;
16655
+ return { iteration: iter, fingerprint };
16432
16656
  } catch {
16433
- return 1;
16657
+ return { iteration: 1, fingerprint: null };
16434
16658
  }
16435
16659
  }
16436
- function checkMaxIterations(currentCommit, maxIterations = MAX_ITERATIONS, contentHash) {
16437
- const iteration = readIteration(currentCommit, contentHash);
16438
- if (iteration > maxIterations) {
16439
- writeIteration(1, currentCommit, contentHash);
16440
- return {
16441
- skip: `Max Verity iterations (${maxIterations}) reached \u2014 accepting to prevent infinite loop. Human review required before deploying.`,
16442
- iteration
16443
- };
16444
- }
16445
- 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(",");
16446
16663
  }
16447
- function writeIteration(iteration, commit, _contentHash) {
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));
16668
+ }
16669
+ function writeIteration(iteration, commit, _contentHash, fingerprint) {
16448
16670
  (0, import_node_fs15.mkdirSync)(VERITY_DIR, { recursive: true });
16449
16671
  const ts = Math.floor(Date.now() / 1e3);
16450
- (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}`);
16451
16674
  }
16452
16675
 
16453
16676
  // src/lib/static-analysis.ts
@@ -16696,7 +16919,7 @@ function resolveTaskContext(opts) {
16696
16919
  // src/lib/cli-version.ts
16697
16920
  function cliVersion() {
16698
16921
  try {
16699
- return true ? "0.28.1-experimental.c2dc717" : "dev";
16922
+ return true ? "0.28.1-experimental.db64e52" : "dev";
16700
16923
  } catch {
16701
16924
  return "dev";
16702
16925
  }
@@ -16790,10 +17013,26 @@ function cacheRequest(body) {
16790
17013
  (0, import_node_fs18.mkdirSync)(CACHE_DIR, { recursive: true });
16791
17014
  const suffix = (0, import_node_crypto9.randomBytes)(4).toString("hex");
16792
17015
  const filename = `pending-${Math.floor(Date.now() / 1e3)}-${suffix}.json`;
16793
- (0, import_node_fs18.writeFileSync)(`${CACHE_DIR}/${filename}`, JSON.stringify(body));
17016
+ (0, import_node_fs18.writeFileSync)(`${CACHE_DIR}/${filename}`, JSON.stringify(redactRequest(body)));
16794
17017
  } catch {
16795
17018
  }
16796
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
+ }
16797
17036
  function buildOfflineFallback(reason, staticResults) {
16798
17037
  return {
16799
17038
  // G12 / INV-18 — WARN, not PASS.
@@ -17020,8 +17259,8 @@ function commandShape(cmd) {
17020
17259
  var COMMAND_HEAD_CHARS = 80;
17021
17260
  var rootCandidateCache = /* @__PURE__ */ new Map();
17022
17261
  function candidateRoots(repoRoot2) {
17023
- const cached = rootCandidateCache.get(repoRoot2);
17024
- if (cached) return cached;
17262
+ const cached2 = rootCandidateCache.get(repoRoot2);
17263
+ if (cached2) return cached2;
17025
17264
  const norm = repoRoot2.replace(/\\/g, "/").replace(/\/+$/, "");
17026
17265
  const out = [norm];
17027
17266
  try {
@@ -17370,9 +17609,13 @@ function buildAgentContext(input) {
17370
17609
  }
17371
17610
  for (const p of input.pendingItems ?? []) {
17372
17611
  if (lines.length >= MAX_AGENT_ITEMS) break;
17612
+ if (p.pattern_id === "intent-misalignment") continue;
17373
17613
  const text = p.description ?? p.title ?? p.reason;
17374
17614
  if (!text) continue;
17375
- 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
+ );
17376
17619
  }
17377
17620
  if (lines.length === 0) return null;
17378
17621
  const body = `${REPORT_PREFIX}
@@ -17522,6 +17765,13 @@ function isGitOnlyPrompt(prompt) {
17522
17765
  return true;
17523
17766
  }
17524
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) {
17525
17775
  if (!predictedMode || !isValidMode(predictedMode)) {
17526
17776
  return detectAnalysisMode(
17527
17777
  signals.noFilesChanged,
@@ -17633,9 +17883,11 @@ var MAX_SUMMARY_BYTES = 4096;
17633
17883
  var HOME = process.env.HOME ?? "";
17634
17884
  async function extractActionSummary(transcriptPath) {
17635
17885
  try {
17636
- const lines = readTurnLines(transcriptPath);
17637
- if (!lines || lines.length === 0) return null;
17638
- 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;
17639
17891
  } catch {
17640
17892
  return null;
17641
17893
  }
@@ -17649,9 +17901,11 @@ function readTurnLines(transcriptPath) {
17649
17901
  }
17650
17902
  if (size === 0) return null;
17651
17903
  let raw;
17904
+ let windowed = false;
17652
17905
  if (size <= SMALL_FILE_BYTES) {
17653
17906
  raw = (0, import_node_fs22.readFileSync)(transcriptPath, "utf-8");
17654
17907
  } else {
17908
+ windowed = true;
17655
17909
  const buf = Buffer.alloc(Math.min(MAX_READ_BYTES, size));
17656
17910
  const fd = require("node:fs").openSync(transcriptPath, "r");
17657
17911
  try {
@@ -17669,17 +17923,22 @@ function readTurnLines(transcriptPath) {
17669
17923
  const allLines = raw.split("\n").filter((l) => l.trim().length > 0);
17670
17924
  if (allLines.length === 0) return null;
17671
17925
  let turnStart = 0;
17926
+ let boundaryFound = false;
17672
17927
  for (let i = allLines.length - 1; i >= 0; i--) {
17673
17928
  try {
17674
17929
  const parsed = JSON.parse(allLines[i]);
17675
17930
  if (parsed.type === "user" && isRealUserMessage(parsed)) {
17676
17931
  turnStart = i;
17932
+ boundaryFound = true;
17677
17933
  break;
17678
17934
  }
17679
17935
  } catch {
17680
17936
  }
17681
17937
  }
17682
- return allLines.slice(turnStart);
17938
+ return {
17939
+ lines: allLines.slice(turnStart),
17940
+ window: !windowed ? "whole" : boundaryFound ? "windowed" : "orphaned"
17941
+ };
17683
17942
  }
17684
17943
  function isRealUserMessage(parsed) {
17685
17944
  const message = parsed.message;
@@ -18237,11 +18496,12 @@ async function readStopHookStdin() {
18237
18496
  return empty;
18238
18497
  }
18239
18498
  }
18240
- function agentContextFor(response, intentRepeat = 0) {
18499
+ function agentContextFor(response, intentRepeat = 0, priorPendingFingerprints = []) {
18241
18500
  const metadata = response.metadata ?? {};
18242
18501
  const intent = response.intent_alignment ?? {};
18243
18502
  return buildAgentContext({
18244
18503
  intentRepeat,
18504
+ priorPendingFingerprints,
18245
18505
  gateDecision: String(response.gate_decision ?? ""),
18246
18506
  findings: response.findings ?? [],
18247
18507
  pendingItems: response.pending_items ?? [],
@@ -18276,7 +18536,7 @@ async function passAndExit(reason, skip, kindOverride) {
18276
18536
  if (unaccounted.length > 0) {
18277
18537
  logEvent("coverage_unaccounted", { where: "passAndExit", skip, count: unaccounted.length });
18278
18538
  }
18279
- const AGENT_SILENT_SKIPS = /* @__PURE__ */ new Set(["iteration-cap"]);
18539
+ const AGENT_SILENT_SKIPS = /* @__PURE__ */ new Set([]);
18280
18540
  const agentNote = AGENT_SILENT_SKIPS.has(skip) ? null : note;
18281
18541
  printJsonCompact(
18282
18542
  buildHookOutput(
@@ -18377,15 +18637,27 @@ async function runAnalyze(opts, globals) {
18377
18637
  const conversation = await readAndClearConversationBuffer(baselineSessionId);
18378
18638
  const specs = discoverSpecs();
18379
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;
18380
18645
  const latestPrompt = conversation?.prompts?.[conversation.prompts.length - 1]?.prompt ?? "";
18381
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
+ }
18382
18655
  await passAndExit("Verity command \u2014 skipping analysis", "verity-command");
18383
18656
  }
18384
- if (isBareAckPrompt(latestPrompt)) {
18657
+ if (shouldSkipForBareAck({ prompt: latestPrompt, turnAuthoredCode, canSeeTurnAuthorship })) {
18385
18658
  await passAndExit("Bare acknowledgment \u2014 skipping analysis", "bare-acknowledgment");
18386
18659
  }
18387
- const agentAuthoredCodeThisTurn = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0));
18388
- if (isReflectionQuestion(assistantResponse) && !agentAuthoredCodeThisTurn) {
18660
+ if (isReflectionQuestion(assistantResponse) && !turnAuthoredCode && canSeeTurnAuthorship) {
18389
18661
  await passAndExit("Reflection-prompt turn \u2014 skipping analysis", "reflection-prompt");
18390
18662
  }
18391
18663
  const urlResult = await resolveServiceUrl(globals.serviceUrl);
@@ -18431,7 +18703,11 @@ async function runAnalyze(opts, globals) {
18431
18703
  );
18432
18704
  }
18433
18705
  if (analysisMode === "skip") {
18434
- 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
+ );
18435
18711
  }
18436
18712
  let staticResults = {
18437
18713
  tool: "@codacy/analysis-cli",
@@ -18483,10 +18759,43 @@ async function runAnalyze(opts, globals) {
18483
18759
  contentHash = hashResult.hash;
18484
18760
  if (analysisMode !== "plan") {
18485
18761
  const scoped = scopeToAuthored(allForReview, actionSummary);
18486
- if (scoped.signal === "none-authored" && !hasNonEditAuthorship(actionSummary, sessionAuthoredCode)) {
18762
+ const canTrustNoneAuthored = scoped.signal === "none-authored" && authorshipIsObservable;
18763
+ if (canTrustNoneAuthored && !hasNonEditAuthorship(actionSummary, sessionAuthoredCode)) {
18487
18764
  await passAndExit("No agent-authored code this turn \u2014 working-tree changes were not authored by this session", "zero-increment");
18488
18765
  }
18489
- 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;
18490
18799
  const recentForReview = narrowToRecent(baseForReview, baselineSessionId);
18491
18800
  if (!opts.skipStatic && isCodacyAvailable()) {
18492
18801
  let allScannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles]));
@@ -18524,19 +18833,13 @@ async function runAnalyze(opts, globals) {
18524
18833
  snapshotResult = generateSnapshotDiffs(codeDelta.files);
18525
18834
  }
18526
18835
  currentCommit = getCurrentCommit();
18527
- const maxIterations = parseInt(opts.maxIterations, 10);
18528
- const iterResult = checkMaxIterations(currentCommit, maxIterations, contentHash ?? void 0);
18529
- if (iterResult.skip) await passAndExit(iterResult.skip, "iteration-cap");
18530
- iteration = iterResult.iteration;
18836
+ iteration = readIterationState(currentCommit).iteration;
18531
18837
  }
18532
18838
  }
18533
18839
  if (analysisMode === "plan") {
18534
18840
  recordAnalysisStart();
18535
18841
  currentCommit = getCurrentCommit();
18536
- const maxIterations = parseInt(opts.maxIterations, 10);
18537
- const iterResult = checkMaxIterations(currentCommit, maxIterations);
18538
- if (iterResult.skip) await passAndExit(iterResult.skip, "iteration-cap");
18539
- iteration = iterResult.iteration;
18842
+ iteration = readIterationState(currentCommit).iteration;
18540
18843
  }
18541
18844
  const contextFiles = gatherContextFiles(contextFilePaths, codeDelta.files);
18542
18845
  for (const f of codeDelta.files) {
@@ -18603,7 +18906,7 @@ async function runAnalyze(opts, globals) {
18603
18906
  let foldConservation = null;
18604
18907
  if (transcriptPath) {
18605
18908
  try {
18606
- foldResult = fold(transcriptPath, { changedFiles: allForReview, repoRoot: repoRoot() });
18909
+ foldResult = earlyFold ?? fold(transcriptPath, { changedFiles: allForReview, repoRoot: repoRoot() });
18607
18910
  foldConservation = checkConservation(allForReview, foldResult, repoRoot());
18608
18911
  if (!foldConservation.holds) {
18609
18912
  process.stderr.write(
@@ -18709,7 +19012,30 @@ async function runAnalyze(opts, globals) {
18709
19012
  hasUserPrompt: (conversation?.prompts?.length ?? 0) > 0,
18710
19013
  isTTY: process.stdout.isTTY === true
18711
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
+ };
18712
19037
  const requestBody = {
19038
+ coverage_telemetry: coverageTelemetry,
18713
19039
  static_results: staticResults,
18714
19040
  code_delta: codeDelta,
18715
19041
  changed_files: allForReview,
@@ -18795,6 +19121,54 @@ async function runAnalyze(opts, globals) {
18795
19121
  // replaced a population floor with ≈35% power that was sub-integer for
18796
19122
  // three-quarters of the fleet.
18797
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
+ })(),
18798
19172
  // P2 — RECORDED, NOT APPLIED. What this run WOULD have reviewed if it
18799
19173
  // narrowed to the within-session increment: what changed since the last
18800
19174
  // VERDICT rather than since task start.
@@ -18829,6 +19203,14 @@ async function runAnalyze(opts, globals) {
18829
19203
  const latest = conversation.prompts[conversation.prompts.length - 1];
18830
19204
  const goalPrompt = resolveGoalPrompt(conversation.prompts) ?? { entry: latest, turnsBack: 0 };
18831
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
+ }
18832
19214
  if (goalPrompt.turnsBack > 0) {
18833
19215
  intentContext.continuation_prompt = latest.prompt;
18834
19216
  logEvent("goal_walked_back", { turns_back: goalPrompt.turnsBack });
@@ -19033,6 +19415,13 @@ async function runAnalyze(opts, globals) {
19033
19415
  });
19034
19416
  }
19035
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
+ })() : [];
19036
19425
  if (memorySession) {
19037
19426
  try {
19038
19427
  recordVerdict(memorySession.d, {
@@ -19055,7 +19444,10 @@ async function runAnalyze(opts, globals) {
19055
19444
  // What next turn reads as `emittedLast`. A suppressed turn did not
19056
19445
  // speak, so it cannot be the cause of the turn after it — which is what
19057
19446
  // keeps this from becoming a permanent gag.
19058
- 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)
19059
19451
  });
19060
19452
  intentRepeatCount = Math.max(0, (foldDossier(memorySession.d).meta.intent_repeat?.consecutive ?? 1) - 1);
19061
19453
  } catch {
@@ -19155,9 +19547,41 @@ async function runAnalyze(opts, globals) {
19155
19547
  reverify_by: response.reverify_by
19156
19548
  });
19157
19549
  const grantNudge = grantWarning ? ` \u26A0\uFE0F ${grantWarning}` : "";
19158
- 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) {
19159
19584
  case "FAIL": {
19160
- writeIteration(iteration + 1, currentCommit, contentHash ?? void 0);
19161
19585
  const assessment = response.assessment;
19162
19586
  const narrative = assessment?.narrative ?? "";
19163
19587
  const findings = response.findings ?? [];
@@ -19242,7 +19666,7 @@ ${YELLOW}${grantNudge.trim()}${NC}
19242
19666
  // Subject to the SAME cycle cut as PASS/WARN. Suppressing here is safe:
19243
19667
  // the findings themselves are rendered above by the blocking renderer,
19244
19668
  // so what the cut removes is the repeated commentary, never the defect.
19245
- agentContext: silenced ? null : agentContextFor(response, intentRepeatCount),
19669
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount, priorPendingFingerprints),
19246
19670
  // The coverage note is silenced with it — half a channel is still a channel.
19247
19671
  silenced: !!silenced,
19248
19672
  openElsewhere
@@ -19263,7 +19687,7 @@ ${YELLOW}${grantNudge.trim()}${NC}
19263
19687
  changed: skipCoverageChanged,
19264
19688
  coverage: reviewCoverage,
19265
19689
  userSummary,
19266
- agentContext: silenced ? null : agentContextFor(response, intentRepeatCount),
19690
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount, priorPendingFingerprints),
19267
19691
  // The coverage note is silenced with it — half a channel is still a channel.
19268
19692
  silenced: !!silenced,
19269
19693
  openElsewhere
@@ -19283,7 +19707,7 @@ ${YELLOW}${grantNudge.trim()}${NC}
19283
19707
  changed: skipCoverageChanged,
19284
19708
  coverage: reviewCoverage,
19285
19709
  userSummary,
19286
- agentContext: silenced ? null : agentContextFor(response, intentRepeatCount),
19710
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount, priorPendingFingerprints),
19287
19711
  // The coverage note is silenced with it — half a channel is still a channel.
19288
19712
  silenced: !!silenced,
19289
19713
  openElsewhere
@@ -20204,6 +20628,13 @@ async function confirmExistingLogin(serviceUrl, remote, opts) {
20204
20628
  return "handled";
20205
20629
  }
20206
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
+ }
20207
20638
  if (existing.data.userId != null) {
20208
20639
  printInfo(`Logged in as ${existing.data.email ?? `user #${existing.data.userId}`} (cached \u2014 could not reach the Verity service). \u2713`);
20209
20640
  } else {
@@ -20219,15 +20650,14 @@ async function confirmExistingLogin(serviceUrl, remote, opts) {
20219
20650
  return "drive-login";
20220
20651
  }
20221
20652
  async function runOptionalAuth(resolution, opts = {}) {
20222
- let serviceUrl = resolution?.url ?? DEFAULT_SERVICE_URL;
20223
- let healed = false;
20224
- if (resolution) {
20225
- const heal = await maybeHealServiceUrl(resolution, opts.verbose);
20226
- serviceUrl = heal.serviceUrl;
20227
- healed = heal.healed;
20228
- if (healed) {
20229
- printInfo(" Log in below to re-register this project and repair ~/.verity/credentials.");
20230
- }
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.");
20231
20661
  }
20232
20662
  let remote = "";
20233
20663
  try {
@@ -20245,8 +20675,10 @@ async function runOptionalAuth(resolution, opts = {}) {
20245
20675
  if (process.stdin.isTTY && process.stdout.isTTY) {
20246
20676
  console.log("");
20247
20677
  console.log(" Signing in is optional. What it does:");
20248
- console.log(" - Confirms you have write access to this repository. The GitHub token");
20249
- 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.");
20250
20682
  console.log(" - It does NOT give Verity access to your code. Code checked by the gate");
20251
20683
  console.log(" is analyzed in memory and discarded \u2014 we never store your code.");
20252
20684
  console.log(" - It is required to store and access run history for this repo");
@@ -20261,17 +20693,10 @@ async function runOptionalAuth(resolution, opts = {}) {
20261
20693
  localOnlyNote();
20262
20694
  return;
20263
20695
  }
20264
- if (!remote) {
20265
- printWarn("No git remote found \u2014 cannot authenticate yet.");
20266
- localOnlyNote();
20267
- return;
20268
- }
20269
- const projectName = parseRemote(remote)?.repo ?? (0, import_node_path23.basename)(process.cwd());
20270
20696
  printInfo("Authenticating with GitHub\u2026");
20271
- const result = await registerProject({ projectName, remote, serviceUrl, verbose: opts.verbose });
20697
+ const result = await loginOnce({ serviceUrl, remote: remote || void 0, verbose: opts.verbose });
20272
20698
  if (result.ok) {
20273
- const who = result.data.email ?? (result.data.userId != null ? `user #${result.data.userId}` : null);
20274
- 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 });
20275
20700
  } else {
20276
20701
  printWarn(`Authentication did not complete: ${result.error}`);
20277
20702
  localOnlyNote();
@@ -20422,8 +20847,8 @@ function registerInitCommand(program2) {
20422
20847
  console.log("");
20423
20848
  try {
20424
20849
  const globals = program2.opts();
20425
- const urlResult = await resolveServiceUrlDetailed(globals.serviceUrl);
20426
- await runOptionalAuth(urlResult.ok ? urlResult.data : null, {
20850
+ const resolution = await resolveServiceUrlForAuth(globals.serviceUrl);
20851
+ await runOptionalAuth(resolution, {
20427
20852
  token: globals.token,
20428
20853
  verbose: globals.verbose
20429
20854
  });
@@ -21039,6 +21464,12 @@ function registerTelemetryCommands(program2) {
21039
21464
  printError(urlResult.error);
21040
21465
  process.exit(1);
21041
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
+ }
21042
21473
  const result = await installTelemetry(urlResult.data);
21043
21474
  if (!result.ok) {
21044
21475
  printError(result.error);
@@ -21087,7 +21518,8 @@ function registerTelemetryCommands(program2) {
21087
21518
  }
21088
21519
 
21089
21520
  // src/cli.ts
21090
- program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.c2dc717").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.db64e52").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);
21091
21523
  try {
21092
21524
  await foldLegacyLocalCredential();
21093
21525
  } catch {