@codacy/verity-cli 0.28.1-experimental.5ec7373 → 0.28.1-experimental.644501f

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/README.md +5 -0
  2. package/bin/verity.js +1317 -222
  3. package/package.json +1 -1
package/bin/verity.js CHANGED
@@ -10386,10 +10386,9 @@ function projectPath(relativePath) {
10386
10386
  return (0, import_node_path.join)(repoRoot(), relativePath);
10387
10387
  }
10388
10388
  var MAX_DELTA_BYTES = 194560;
10389
- var MAX_FILES = 20;
10389
+ var MAX_FILES = 40;
10390
10390
  var MAX_FILE_BYTES = 51200;
10391
10391
  var DEBOUNCE_SECONDS = 30;
10392
- var MAX_ITERATIONS = 2;
10393
10392
  var MAX_SPEC_FILES = 10;
10394
10393
  var MAX_SPEC_FILE_BYTES = 10240;
10395
10394
  var MAX_TOTAL_SPEC_BYTES = 30720;
@@ -10636,14 +10635,14 @@ async function readGlobalCredential(remote) {
10636
10635
  const parsed = parseCredentialLine(line);
10637
10636
  if (parsed && parsed.remote === key) last = parsed.rec;
10638
10637
  }
10639
- if (last) return last;
10638
+ if (last) return { ...last, keyed: true };
10640
10639
  }
10641
10640
  let plain = null;
10642
10641
  for (const line of lines) {
10643
10642
  const parsed = parseCredentialLine(line);
10644
10643
  if (parsed && parsed.remote === "") plain = parsed.rec;
10645
10644
  }
10646
- return plain;
10645
+ return plain ? { ...plain, keyed: false } : null;
10647
10646
  }
10648
10647
  async function upsertGlobalCredential(remote, rec) {
10649
10648
  const path = globalCredentialsPath();
@@ -10707,6 +10706,36 @@ async function removeSupersededUserCredentials(loginServiceUrl, loginUserId) {
10707
10706
  }
10708
10707
  return removed;
10709
10708
  }
10709
+ async function removeGlobalCredential(remote) {
10710
+ const path = globalCredentialsPath();
10711
+ let content;
10712
+ try {
10713
+ content = await (0, import_promises.readFile)(path, "utf-8");
10714
+ } catch {
10715
+ return false;
10716
+ }
10717
+ const key = encodeRemoteKey(remote);
10718
+ const kept = [];
10719
+ let removed = false;
10720
+ for (const line of content.split("\n")) {
10721
+ const parsed = parseCredentialLine(line);
10722
+ if (parsed && parsed.remote === key) {
10723
+ removed = true;
10724
+ continue;
10725
+ }
10726
+ kept.push(line);
10727
+ }
10728
+ if (!removed) return false;
10729
+ while (kept.length && kept[kept.length - 1].trim() === "") kept.pop();
10730
+ try {
10731
+ await (0, import_promises.writeFile)(path, kept.length ? kept.join("\n") + "\n" : "", { mode: 384 });
10732
+ await (0, import_promises.chmod)(path, 384).catch(() => {
10733
+ });
10734
+ } catch {
10735
+ return false;
10736
+ }
10737
+ return true;
10738
+ }
10710
10739
  function parseLocalCredentialFile(content) {
10711
10740
  const tokenMatch = content.match(/token:\s*((?:gate_|verity_)[a-f0-9]+)/);
10712
10741
  if (!tokenMatch) return null;
@@ -10894,8 +10923,8 @@ function filterReviewable(files) {
10894
10923
  const ext = (0, import_node_path3.extname)(f).slice(1);
10895
10924
  if (ANALYZABLE_EXTENSIONS.has(ext)) return false;
10896
10925
  if (REVIEWABLE_EXTENSIONS.has(ext)) return true;
10897
- const basename4 = f.split("/").pop() ?? "";
10898
- if (REVIEWABLE_FILENAMES.has(basename4)) return true;
10926
+ const basename3 = f.split("/").pop() ?? "";
10927
+ if (REVIEWABLE_FILENAMES.has(basename3)) return true;
10899
10928
  if (REVIEWABLE_PATH_PATTERNS.some((p) => p.test(f))) return true;
10900
10929
  return false;
10901
10930
  });
@@ -10986,7 +11015,54 @@ function listTrackedFiles() {
10986
11015
  return Array.from(set);
10987
11016
  }
10988
11017
  function sanitizeRemote(remote) {
10989
- 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`;
10990
11066
  }
10991
11067
 
10992
11068
  // src/lib/api-client.ts
@@ -11023,6 +11099,24 @@ async function apiRequest(options) {
11023
11099
  "Content-Type": "application/json"
11024
11100
  };
11025
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
+ }
11026
11120
  headers["Authorization"] = `Bearer ${token}`;
11027
11121
  }
11028
11122
  const remote = requestRemote();
@@ -11135,14 +11229,9 @@ async function serviceUrlFromCredentials() {
11135
11229
  async function serviceUrlFromVerityMd() {
11136
11230
  try {
11137
11231
  const content = await (0, import_promises2.readFile)(projectPath(VERITY_MD_FILE), "utf-8");
11138
- const boldLine = content.split("\n").find((l) => /\*\*url\*\*/i.test(l));
11139
- if (boldLine) {
11140
- const urlMatch = boldLine.match(/https:\/\/[^\s]+/);
11141
- if (urlMatch) return urlMatch[0];
11142
- }
11143
- const plainLine = content.split("\n").find((l) => /(?:url|service)\s*:/i.test(l));
11144
- if (plainLine) {
11145
- 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]+/);
11146
11235
  if (urlMatch) return urlMatch[0];
11147
11236
  }
11148
11237
  } catch {
@@ -11165,7 +11254,15 @@ async function resolveServiceUrlDetailed(flagUrl) {
11165
11254
  if (mdUrl) {
11166
11255
  return { ok: true, data: { url: mdUrl, source: "verity_md" } };
11167
11256
  }
11168
- 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" };
11169
11266
  }
11170
11267
  async function resolveServiceUrl(flagUrl) {
11171
11268
  const result = await resolveServiceUrlDetailed(flagUrl);
@@ -11188,7 +11285,13 @@ async function resolveToken(flagToken) {
11188
11285
  if (rec) {
11189
11286
  return {
11190
11287
  ok: true,
11191
- data: { token: rec.token, source: "global", userId: rec.userId, email: rec.email }
11288
+ data: {
11289
+ token: rec.token,
11290
+ source: "global",
11291
+ userId: rec.userId,
11292
+ email: rec.email,
11293
+ keyed: rec.keyed
11294
+ }
11192
11295
  };
11193
11296
  }
11194
11297
  const local = await readLegacyLocalCredential();
@@ -11198,7 +11301,10 @@ async function resolveToken(flagToken) {
11198
11301
  data: { token: local.token, source: "local", userId: local.userId, email: local.email }
11199
11302
  };
11200
11303
  }
11201
- 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
+ };
11202
11308
  }
11203
11309
  async function whoami(token, serviceUrl, verbose) {
11204
11310
  return apiRequest({
@@ -11220,6 +11326,16 @@ function reverifyNudge(who) {
11220
11326
  }
11221
11327
  return null;
11222
11328
  }
11329
+ function isLegacyPerRepoCredential(auth2) {
11330
+ return auth2.source === "local" || auth2.source === "global" && auth2.keyed === true;
11331
+ }
11332
+ async function shouldUpgradeOnLogin(auth2) {
11333
+ if (!isLegacyPerRepoCredential(auth2)) return false;
11334
+ if (auth2.userId == null) return true;
11335
+ const bare = await readGlobalCredential("");
11336
+ if (bare?.userId == null) return true;
11337
+ return bare.userId === auth2.userId;
11338
+ }
11223
11339
  function authDenialRemedy(error) {
11224
11340
  if (error.startsWith("STALE_VERIFICATION")) {
11225
11341
  return {
@@ -11233,6 +11349,12 @@ function authDenialRemedy(error) {
11233
11349
  remedy: 'No access grant for this repository \u2014 run "verity login" to refresh your grants (a repo granted after your last login needs one), or get write access to it.'
11234
11350
  };
11235
11351
  }
11352
+ if (error.startsWith("INVALID_TOKEN")) {
11353
+ return {
11354
+ code: "INVALID_TOKEN",
11355
+ remedy: 'Your Verity login has expired or was revoked \u2014 run "verity login" to sign in again.'
11356
+ };
11357
+ }
11236
11358
  return null;
11237
11359
  }
11238
11360
  async function probeService(serviceUrl, verbose) {
@@ -11273,6 +11395,7 @@ async function maybeHealServiceUrl(resolution, verbose) {
11273
11395
 
11274
11396
  // src/lib/register.ts
11275
11397
  var readline = __toESM(require("node:readline/promises"));
11398
+ var import_node_os = require("node:os");
11276
11399
 
11277
11400
  // src/lib/provider-auth.ts
11278
11401
  var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
@@ -11460,6 +11583,15 @@ async function registerProject(opts) {
11460
11583
  }
11461
11584
  return { ok: true, data: { projectId: project_id, serviceUrl: service_url, email, userId } };
11462
11585
  }
11586
+ function deviceLabel() {
11587
+ const override = process.env.VERITY_DEVICE_LABEL?.trim();
11588
+ if (override) return override;
11589
+ try {
11590
+ return (0, import_node_os.hostname)() || void 0;
11591
+ } catch {
11592
+ return void 0;
11593
+ }
11594
+ }
11463
11595
  async function loginOnce(opts) {
11464
11596
  const usingTokenOverride = Boolean(process.env.VERITY_PROVIDER_TOKEN);
11465
11597
  const providerAuth = await githubDeviceFlow();
@@ -11480,13 +11612,19 @@ async function loginOnce(opts) {
11480
11612
  path: "/auth/login",
11481
11613
  serviceUrl: opts.serviceUrl,
11482
11614
  extraHeaders: { "X-Provider-Token": providerToken },
11615
+ // Label the session so its owner can tell their machines apart in
11616
+ // `verity sessions list` — a list of identical "login" rows is unusable when
11617
+ // the question is "which of these is the laptop I lost?". The hostname is the
11618
+ // useful default; VERITY_DEVICE_LABEL overrides it for anyone who would
11619
+ // rather not send it. Server-side it is sanitized and capped.
11620
+ body: { device: deviceLabel() },
11483
11621
  verbose: opts.verbose,
11484
11622
  cmd: "login"
11485
11623
  });
11486
11624
  if (!result.ok) {
11487
11625
  return { ok: false, error: result.error };
11488
11626
  }
11489
- const { token, service_url, user_id, user, repo_count } = result.data;
11627
+ const { token, service_url, user_id, user, repo_count, expires_at } = result.data;
11490
11628
  const loginUserId = user_id ?? user?.id ?? void 0;
11491
11629
  try {
11492
11630
  await upsertGlobalCredential("", {
@@ -11510,15 +11648,16 @@ async function loginOnce(opts) {
11510
11648
  email: user?.email,
11511
11649
  userId: loginUserId,
11512
11650
  repoCount: repo_count ?? 0,
11513
- prunedCredentials: pruned
11651
+ prunedCredentials: pruned,
11652
+ expiresAt: expires_at
11514
11653
  }
11515
11654
  };
11516
11655
  }
11517
11656
 
11518
11657
  // src/commands/auth.ts
11519
11658
  function registerAuthCommands(program2) {
11520
- const auth = program2.command("auth").description("Manage project authentication");
11521
- auth.command("register").description("Register a project with Verity").requiredOption("--project <name>", "Project name").option("--remote <url>", "Git remote URL (auto-detected if omitted)").action(async (opts) => {
11659
+ const auth2 = program2.command("auth").description("Manage project authentication");
11660
+ auth2.command("register").description("Register a project with Verity").requiredOption("--project <name>", "Project name").option("--remote <url>", "Git remote URL (auto-detected if omitted)").action(async (opts) => {
11522
11661
  const globals = program2.opts();
11523
11662
  const serviceUrl = globals.serviceUrl ?? DEFAULT_SERVICE_URL;
11524
11663
  let remote = opts.remote;
@@ -11545,7 +11684,7 @@ function registerAuthCommands(program2) {
11545
11684
  if (email) printInfo(`Authenticated as: ${email}`);
11546
11685
  printJson({ project_id: projectId, service_url: resolvedUrl });
11547
11686
  });
11548
- auth.command("verify").description("Verify the current token is valid").action(async () => {
11687
+ auth2.command("verify").description("Verify the current token is valid").action(async () => {
11549
11688
  const globals = program2.opts();
11550
11689
  const tokenResult = await resolveToken(globals.token);
11551
11690
  if (!tokenResult.ok) {
@@ -11571,7 +11710,7 @@ function registerAuthCommands(program2) {
11571
11710
  printInfo(`Token valid. Project: ${result.data.project_name}`);
11572
11711
  printJson(result.data);
11573
11712
  });
11574
- auth.command("discover").description("Check if a project is registered").option("--remote <url>", "Git remote URL (auto-detected if omitted)").action(async (opts) => {
11713
+ auth2.command("discover").description("Check if a project is registered").option("--remote <url>", "Git remote URL (auto-detected if omitted)").action(async (opts) => {
11575
11714
  const globals = program2.opts();
11576
11715
  let remote = opts.remote;
11577
11716
  if (!remote) {
@@ -11602,16 +11741,81 @@ function registerAuthCommands(program2) {
11602
11741
  });
11603
11742
  }
11604
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
+
11605
11808
  // src/commands/login.ts
11606
11809
  function registerLoginCommand(program2) {
11607
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) => {
11608
11811
  const globals = program2.opts();
11609
- const urlResult = await resolveServiceUrlDetailed(globals.serviceUrl);
11610
- if (!urlResult.ok) {
11611
- printError(urlResult.error);
11612
- 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);
11613
11817
  }
11614
- const heal = await maybeHealServiceUrl(urlResult.data, globals.verbose);
11818
+ const heal = await maybeHealServiceUrl(resolution, globals.verbose);
11615
11819
  const serviceUrl = heal.serviceUrl;
11616
11820
  if (heal.healed) {
11617
11821
  printInfo(" Completing login updates ~/.verity/credentials against the live service.");
@@ -11627,13 +11831,19 @@ function registerLoginCommand(program2) {
11627
11831
  const who = await whoami(existing.data.token, serviceUrl, globals.verbose);
11628
11832
  if (who.ok && who.data.logged_in) {
11629
11833
  const nudge = reverifyNudge(who.data);
11630
- if (!nudge) {
11834
+ const upgrade = await shouldUpgradeOnLogin(existing.data);
11835
+ if (!nudge && !upgrade) {
11631
11836
  printInfo(`Already logged in as ${who.data.email ?? `user #${who.data.user_id}`}. \u2713`);
11632
11837
  printInfo(" Re-authenticate with: verity login --force");
11633
11838
  return;
11634
11839
  }
11635
- printWarn(nudge);
11636
- printInfo("Re-verifying your repository access\u2026");
11840
+ if (nudge) {
11841
+ printWarn(nudge);
11842
+ printInfo("Re-verifying your repository access\u2026");
11843
+ } else {
11844
+ printInfo("You are signed in with a per-repository token (the old format).");
11845
+ printInfo(" Upgrading to a single login that covers every repository you can write to\u2026");
11846
+ }
11637
11847
  } else if (who.ok && who.data.anonymous) {
11638
11848
  printInfo("You have an anonymous token (the gate runs, but nothing is saved). Logging you in\u2026");
11639
11849
  } else if (!who.ok) {
@@ -11646,51 +11856,7 @@ function registerLoginCommand(program2) {
11646
11856
  printError(`Login failed: ${result.error}`);
11647
11857
  process.exit(1);
11648
11858
  }
11649
- const out = result.data;
11650
- const identity = out.email ?? (out.userId != null ? `user #${out.userId}` : "your account");
11651
- printInfo(`Logged in as ${identity}. \u2713`);
11652
- printInfo(` Access granted to ${out.repoCount} ${out.repoCount === 1 ? "repository" : "repositories"}.`);
11653
- printInfo(" Runs, history, and cloud memory now sync to Verity everywhere you have write access.");
11654
- if (out.prunedCredentials > 0) {
11655
- printVerbose(`Pruned ${out.prunedCredentials} superseded per-repository credential line(s).`, globals.verbose);
11656
- } else if (out.prunedCredentials < 0) {
11657
- printWarn(" Could not rewrite ~/.verity/credentials: superseded per-repository tokens remain and");
11658
- printWarn(" will keep taking precedence over this login in their own repositories.");
11659
- printInfo(` Check the file's permissions; the next successful "verity login" retries the cleanup.`);
11660
- }
11661
- if (out.repoCount === 0) {
11662
- printWarn("The Verity GitHub App is not installed on any account you can access.");
11663
- printInfo(` Install it (and grant your repositories), then re-run verity login:`);
11664
- printInfo(` ${githubAppInstallUrl(null)}`);
11665
- return;
11666
- }
11667
- if (remote) {
11668
- const who = await whoami(out.token, out.serviceUrl, globals.verbose);
11669
- if (who.ok && who.data.grant_status != null) {
11670
- printInfo(" \u2713 This repository is covered.");
11671
- } else if (!who.ok) {
11672
- printWarn(` Could not confirm this repository's coverage (${who.error}) \u2014 verity status will show it.`);
11673
- } else {
11674
- const parsed = parseRemote(remote);
11675
- const installUrl = githubAppInstallUrl(parsed ? await githubAccountId(parsed.owner) : null);
11676
- printWarn(` This repository (${parsed ? `${parsed.owner}/${parsed.repo}` : remote}) is NOT covered by your grants.`);
11677
- printInfo(" Grant the Verity GitHub App access to it, then re-run verity login:");
11678
- printInfo(` ${installUrl}`);
11679
- }
11680
- const rec = await readGlobalCredential(remote);
11681
- if (rec && rec.token !== out.token) {
11682
- const otherBackend = rec.serviceUrl != null && rec.serviceUrl !== out.serviceUrl;
11683
- if (otherBackend) {
11684
- printWarn(` Note: this repository is pinned to a different Verity service (${rec.serviceUrl})`);
11685
- printWarn(" by its own credential line, which takes precedence here \u2014 this login does not");
11686
- printWarn(" change that. To move the repository, remove its line from ~/.verity/credentials.");
11687
- } else {
11688
- const kind = rec.userId != null ? "superseded per-repository" : "anonymous project-specific";
11689
- printWarn(` Note: this repository has a ${kind} credential that takes`);
11690
- printWarn(" precedence here. Remove its line from ~/.verity/credentials to use your login.");
11691
- }
11692
- }
11693
- }
11859
+ await reportLoginOutcome(result.data, { remote: remote || void 0, verbose: globals.verbose });
11694
11860
  });
11695
11861
  }
11696
11862
 
@@ -11809,6 +11975,205 @@ function registerTokenCommand(program2) {
11809
11975
  });
11810
11976
  }
11811
11977
 
11978
+ // src/commands/sessions.ts
11979
+ function shortDate(iso) {
11980
+ return iso ? iso.slice(0, 10) : "\u2014";
11981
+ }
11982
+ function daysUntil(iso) {
11983
+ if (!iso) return null;
11984
+ const ms = Date.parse(iso);
11985
+ if (Number.isNaN(ms)) return null;
11986
+ return Math.round((ms - Date.now()) / 864e5);
11987
+ }
11988
+ async function auth(globals) {
11989
+ const tokenResult = await resolveToken(globals.token);
11990
+ if (!tokenResult.ok) {
11991
+ printError(tokenResult.error);
11992
+ process.exit(1);
11993
+ }
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
+ };
12000
+ }
12001
+ function explain(error) {
12002
+ if (error.startsWith("FORBIDDEN")) {
12003
+ printInfo(' Sessions belong to a logged-in account \u2014 run "verity login" first.');
12004
+ } else if (error.startsWith("INVALID_TOKEN")) {
12005
+ printInfo(' This login has expired or was revoked \u2014 run "verity login" to sign in again.');
12006
+ }
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
+ }
12022
+ function registerSessionsCommands(program2) {
12023
+ const sessions = program2.command("sessions").description("List and revoke your Verity logins (one per machine)");
12024
+ sessions.command("list").description("List your active logins \u2014 device, last use, and expiry").option("--json", "Output raw JSON").action(async (opts) => {
12025
+ const globals = program2.opts();
12026
+ const { token, serviceUrl } = await auth(globals);
12027
+ const result = await apiRequest({
12028
+ method: "GET",
12029
+ path: "/auth/sessions",
12030
+ serviceUrl,
12031
+ token,
12032
+ verbose: globals.verbose,
12033
+ cmd: "sessions"
12034
+ });
12035
+ if (!result.ok) {
12036
+ printError(result.error);
12037
+ explain(result.error);
12038
+ process.exit(1);
12039
+ }
12040
+ if (opts.json) {
12041
+ printJson(result.data);
12042
+ return;
12043
+ }
12044
+ const list = result.data.sessions;
12045
+ if (list.length === 0) {
12046
+ printInfo('No active logins. (Run "verity login".)');
12047
+ return;
12048
+ }
12049
+ printInfo(`${list.length} active login${list.length === 1 ? "" : "s"}:`);
12050
+ printInfo("");
12051
+ printInfo(`${"SESSION ID".padEnd(38)}${"DEVICE".padEnd(24)}${"CREATED".padEnd(12)}${"LAST USED".padEnd(12)}EXPIRES`);
12052
+ for (const s of list) {
12053
+ const days = daysUntil(s.expires_at);
12054
+ const expiry = s.expires_at ? `${shortDate(s.expires_at)}${days != null ? ` (${days}d)` : ""}` : "never";
12055
+ const device = (s.device ?? "login").slice(0, 22);
12056
+ printInfo(
12057
+ `${s.id.padEnd(38)}${device.padEnd(24)}${shortDate(s.created_at).padEnd(12)}${shortDate(s.last_used_at).padEnd(12)}${expiry}${s.current ? " \u2190 this machine" : ""}`
12058
+ );
12059
+ }
12060
+ printInfo("");
12061
+ printInfo("Revoke one: verity sessions revoke <session-id>");
12062
+ printInfo("Sign out everywhere else: verity logout --others");
12063
+ });
12064
+ sessions.command("revoke <session-id>").description("Revoke one login. Revoking this machine's also clears the local credential").action(async (sessionId) => {
12065
+ const globals = program2.opts();
12066
+ const { token, serviceUrl, keyed } = await auth(globals);
12067
+ const result = await apiRequest({
12068
+ method: "DELETE",
12069
+ path: `/auth/sessions/${encodeURIComponent(sessionId)}`,
12070
+ serviceUrl,
12071
+ token,
12072
+ verbose: globals.verbose,
12073
+ cmd: "sessions-revoke"
12074
+ });
12075
+ if (!result.ok) {
12076
+ printError(result.error);
12077
+ if (result.http_status === 404) {
12078
+ printInfo(' No session with that id on your account \u2014 check "verity sessions list".');
12079
+ }
12080
+ explain(result.error);
12081
+ process.exit(1);
12082
+ }
12083
+ printInfo(`Session ${sessionId} revoked. \u2713`);
12084
+ if (result.data.was_current) {
12085
+ const cleared = await removeGlobalCredential(keyed ? currentRemote() : "");
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.');
12087
+ }
12088
+ });
12089
+ }
12090
+ function registerLogoutCommand(program2) {
12091
+ program2.command("logout").description("Sign out of Verity on this machine (--all / --others for every machine)").option("--all", "Revoke every login on every machine, including this one").option("--others", "Revoke every login EXCEPT this machine (e.g. a lost laptop)").action(async (opts) => {
12092
+ const globals = program2.opts();
12093
+ if (opts.all && opts.others) {
12094
+ printError("Use either --all or --others, not both.");
12095
+ process.exit(1);
12096
+ }
12097
+ const { token, serviceUrl, keyed } = await auth(globals);
12098
+ if (opts.all || opts.others) {
12099
+ const result = await apiRequest({
12100
+ method: "DELETE",
12101
+ path: opts.others ? "/auth/sessions?others=true" : "/auth/sessions",
12102
+ serviceUrl,
12103
+ token,
12104
+ verbose: globals.verbose,
12105
+ cmd: "logout"
12106
+ });
12107
+ if (!result.ok) {
12108
+ if (isPinRefusal(result.error) && !opts.others) {
12109
+ await logoutLocallyOnly(keyed);
12110
+ return;
12111
+ }
12112
+ printError(result.error);
12113
+ explain(result.error);
12114
+ process.exit(1);
12115
+ }
12116
+ const n = result.data.revoked;
12117
+ printInfo(`Revoked ${n} login${n === 1 ? "" : "s"}. \u2713`);
12118
+ if (opts.others) {
12119
+ printInfo(" This machine is still signed in.");
12120
+ return;
12121
+ }
12122
+ const cleared2 = await removeGlobalCredential("");
12123
+ if (cleared2) printInfo(" Local credential cleared.");
12124
+ printInfo(' Run "verity login" to sign back in.');
12125
+ return;
12126
+ }
12127
+ const list = await apiRequest({
12128
+ method: "GET",
12129
+ path: "/auth/sessions",
12130
+ serviceUrl,
12131
+ token,
12132
+ verbose: globals.verbose,
12133
+ cmd: "logout"
12134
+ });
12135
+ if (!list.ok) {
12136
+ if (isPinRefusal(list.error)) {
12137
+ await logoutLocallyOnly(keyed);
12138
+ return;
12139
+ }
12140
+ printError(list.error);
12141
+ explain(list.error);
12142
+ process.exit(1);
12143
+ }
12144
+ const current = list.data.sessions.find((s) => s.current);
12145
+ if (!current) {
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
+ }
12151
+ return;
12152
+ }
12153
+ const revoked = await apiRequest({
12154
+ method: "DELETE",
12155
+ path: `/auth/sessions/${current.id}`,
12156
+ serviceUrl,
12157
+ token,
12158
+ verbose: globals.verbose,
12159
+ cmd: "logout"
12160
+ });
12161
+ if (!revoked.ok) {
12162
+ if (isPinRefusal(revoked.error)) {
12163
+ await logoutLocallyOnly(keyed);
12164
+ return;
12165
+ }
12166
+ printError(revoked.error);
12167
+ explain(revoked.error);
12168
+ process.exit(1);
12169
+ }
12170
+ const cleared = await removeGlobalCredential("");
12171
+ printInfo("Signed out on this machine. \u2713");
12172
+ if (cleared) printInfo(" Local credential cleared.");
12173
+ printInfo(' Your other machines are unaffected \u2014 use "verity logout --all" for all of them.');
12174
+ });
12175
+ }
12176
+
11812
12177
  // src/lib/hooks.ts
11813
12178
  var import_promises4 = require("node:fs/promises");
11814
12179
  var import_node_path5 = require("node:path");
@@ -12430,7 +12795,7 @@ function getRecentCommitMessages() {
12430
12795
  // src/lib/context-identity.ts
12431
12796
  var import_node_crypto2 = require("node:crypto");
12432
12797
  var import_node_fs5 = require("node:fs");
12433
- var import_node_os = require("node:os");
12798
+ var import_node_os2 = require("node:os");
12434
12799
  var import_node_path6 = require("node:path");
12435
12800
  var SHARED_SENTINELS = /* @__PURE__ */ new Set([
12436
12801
  "",
@@ -12485,7 +12850,7 @@ function contextIdentity(input) {
12485
12850
  }
12486
12851
  function verityHome() {
12487
12852
  const override = process.env.VERITY_HOME;
12488
- return override && override.trim() ? (0, import_node_path6.resolve)(override) : (0, import_node_path6.join)((0, import_node_os.homedir)(), ".verity");
12853
+ return override && override.trim() ? (0, import_node_path6.resolve)(override) : (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".verity");
12489
12854
  }
12490
12855
  function dossierDir(identity) {
12491
12856
  return (0, import_node_path6.join)(verityHome(), "sessions", identity.userKey, identity.treeKey, identity.sessionKey);
@@ -13331,6 +13696,143 @@ var import_node_fs10 = require("node:fs");
13331
13696
  var import_node_crypto5 = require("node:crypto");
13332
13697
  var import_node_path11 = require("node:path");
13333
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
+
13334
13836
  // src/lib/dossier.ts
13335
13837
  var import_node_fs9 = require("node:fs");
13336
13838
  var import_node_crypto4 = require("node:crypto");
@@ -13339,6 +13841,7 @@ var MAX_LINE_BYTES = 4096;
13339
13841
  var MAX_GOAL_CHARS = 2e3;
13340
13842
  var GOAL_KEEP = 8;
13341
13843
  var GOAL_TOTAL_CAP = 32;
13844
+ var RECENT_PENDING_CAP = 20;
13342
13845
  var HASH_WIDTH = 16;
13343
13846
  var AUTHORED_CAP = 300;
13344
13847
  var NOT_MINE_CAP = 300;
@@ -13700,7 +14203,17 @@ function reduce(state, events, now) {
13700
14203
  }
13701
14204
  case "verdict": {
13702
14205
  state.meta.last_verdict_seq = ev.seq;
14206
+ state.meta.channel = {
14207
+ emittedLast: ev.emitted === true,
14208
+ // Reset by ANY movement, so the counter measures a standstill rather
14209
+ // than session length.
14210
+ consecutiveIdle: ev.idle === false ? 0 : (state.meta.channel?.consecutiveIdle ?? 0) + 1
14211
+ };
13703
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
+ }
13704
14217
  if (ev.intent_sig) {
13705
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 };
13706
14219
  } else {
@@ -13925,12 +14438,12 @@ function readFoldCache(d) {
13925
14438
  if (!(0, import_node_fs9.existsSync)(d.foldPath)) return null;
13926
14439
  const raw = JSON.parse((0, import_node_fs9.readFileSync)(d.foldPath, "utf8"));
13927
14440
  if (raw?.v !== 1) return null;
13928
- const cached = expandState(raw);
13929
- if (!cached?.meta) return null;
14441
+ const cached2 = expandState(raw);
14442
+ if (!cached2?.meta) return null;
13930
14443
  const size = (0, import_node_fs9.existsSync)(d.eventsPath) ? (0, import_node_fs9.statSync)(d.eventsPath).size : 0;
13931
14444
  const rotations = (0, import_node_fs9.existsSync)(d.rotatedDir) ? (0, import_node_fs9.readdirSync)(d.rotatedDir).filter((f) => f.endsWith(".jsonl")).length : 0;
13932
- if (cached.meta.upto_offset !== size || cached.meta.rotations !== rotations) return null;
13933
- return cached;
14445
+ if (cached2.meta.upto_offset !== size || cached2.meta.rotations !== rotations) return null;
14446
+ return cached2;
13934
14447
  } catch {
13935
14448
  return null;
13936
14449
  }
@@ -14067,6 +14580,10 @@ function projectMemory(state, opts) {
14067
14580
  ...active.delivered && { delivered: active.delivered }
14068
14581
  };
14069
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
+ }
14070
14587
  if (state.meta.last_adjudication) {
14071
14588
  const a = state.meta.last_adjudication;
14072
14589
  p.last_adjudication = { verdict: a.verdict, score: a.score, at: a.at };
@@ -14313,7 +14830,8 @@ function recall(d, input) {
14313
14830
  continuity,
14314
14831
  spoken: reanchored.spoken,
14315
14832
  refused: reanchored.dropped.length,
14316
- lastVerdictSeq
14833
+ lastVerdictSeq,
14834
+ ...input.capture && { capture: input.capture }
14317
14835
  });
14318
14836
  return {
14319
14837
  state: effective,
@@ -14424,7 +14942,19 @@ function sessionDossier(token, sessionId) {
14424
14942
  const d = openDossier(identity);
14425
14943
  return d ? { d, identity } : null;
14426
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
+ }
14427
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
+ }
14428
14958
  const text = prompt.slice(0, MAX_GOAL_CHARS);
14429
14959
  appendEvent(d, {
14430
14960
  k: "goal",
@@ -14527,6 +15057,11 @@ function recordVerdict(d, v) {
14527
15057
  line_sha: at !== void 0 ? lineSha(at) : null
14528
15058
  });
14529
15059
  }
15060
+ const foldedNow = foldDossier(d);
15061
+ const sig = intentSignature(v.intent, {
15062
+ goalSeq: foldedNow.goal.find((g) => g.status === "active")?.seq ?? 0,
15063
+ idle: v.idle !== false
15064
+ });
14530
15065
  appendEvent(d, {
14531
15066
  k: "verdict",
14532
15067
  run_id: v.runId,
@@ -14534,15 +15069,26 @@ function recordVerdict(d, v) {
14534
15069
  watermark_sha: v.watermarkSha,
14535
15070
  branch: v.branch,
14536
15071
  decision: v.decision,
14537
- ...intentSignature(v.intent) && { intent_sig: intentSignature(v.intent) },
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
+ },
15080
+ emitted: v.emitted === true,
15081
+ idle: v.idle !== false,
14538
15082
  ...v.intent?.verdict && { intent_verdict: v.intent.verdict },
14539
15083
  ...typeof v.intent?.score === "number" && { intent_score: v.intent.score }
14540
15084
  });
14541
15085
  }
14542
- function intentSignature(intent) {
15086
+ function intentSignature(intent, ctx) {
14543
15087
  if (!intent?.verdict) return null;
14544
15088
  if (intent.verdict !== "misaligned" && intent.verdict !== "partial") return null;
14545
- return `${intent.verdict}:${lineSha(intent.gaps?.[0] ?? "")}`;
15089
+ const goal = ctx ? `g${ctx.goalSeq}` : "g?";
15090
+ const moved = ctx?.idle === false ? "active" : "idle";
15091
+ return `${intent.verdict}:${goal}:${moved}`;
14546
15092
  }
14547
15093
  function toRegister(severity) {
14548
15094
  switch (severity) {
@@ -14579,7 +15125,9 @@ function recallMemory(d, identity, opts) {
14579
15125
  const state = foldDossier(d);
14580
15126
  const watermark = state.meta.watermark?.sha ?? null;
14581
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;
14582
15129
  const r = recall(d, {
15130
+ ...captureCmp && { capture: captureCmp },
14583
15131
  identity,
14584
15132
  currentSessionKey: opts.currentSessionKey,
14585
15133
  branchNow: getCurrentBranch(),
@@ -14836,6 +15384,8 @@ function collectCodeDelta(files, opts) {
14836
15384
  let totalSize = 0;
14837
15385
  let truncationReason = null;
14838
15386
  const droppedPaths = [];
15387
+ const excluded = [];
15388
+ const exclude = (path, reason) => excluded.push({ path, reason, stage: "collectCodeDelta", kind: "capacity" });
14839
15389
  for (const filepath of sorted) {
14840
15390
  if (result.length >= maxFiles) {
14841
15391
  truncationReason ??= "max_files";
@@ -14843,14 +15393,21 @@ function collectCodeDelta(files, opts) {
14843
15393
  continue;
14844
15394
  }
14845
15395
  const resolved = resolveFile(filepath);
14846
- if (!resolved) continue;
15396
+ if (!resolved) {
15397
+ exclude(filepath, "path-not-resolvable");
15398
+ continue;
15399
+ }
14847
15400
  let size;
14848
15401
  try {
14849
15402
  size = (0, import_node_fs11.statSync)(resolved).size;
14850
15403
  } catch {
15404
+ exclude(filepath, "not-stattable");
15405
+ continue;
15406
+ }
15407
+ if (size > maxFileBytes) {
15408
+ exclude(filepath, `over-file-size-limit-${maxFileBytes}b`);
14851
15409
  continue;
14852
15410
  }
14853
- if (size > maxFileBytes) continue;
14854
15411
  if (totalSize + size > maxTotalBytes) {
14855
15412
  truncationReason ??= "max_total_bytes";
14856
15413
  const idx = sorted.indexOf(filepath);
@@ -14861,6 +15418,7 @@ function collectCodeDelta(files, opts) {
14861
15418
  try {
14862
15419
  content = (0, import_node_fs11.readFileSync)(resolved, "utf-8");
14863
15420
  } catch {
15421
+ exclude(filepath, "not-readable");
14864
15422
  continue;
14865
15423
  }
14866
15424
  totalSize += size;
@@ -14874,10 +15432,14 @@ function collectCodeDelta(files, opts) {
14874
15432
  (sum, f) => sum + f.content.split("\n").length,
14875
15433
  0
14876
15434
  );
15435
+ for (const path of droppedPaths) {
15436
+ exclude(path, truncationReason === "max_files" ? "max-files-cap" : "max-total-bytes-cap");
15437
+ }
14877
15438
  return {
14878
15439
  files: result,
14879
15440
  total_lines: totalLines,
14880
15441
  total_files: result.length,
15442
+ excluded,
14881
15443
  ...truncationReason && {
14882
15444
  truncated: {
14883
15445
  reason: truncationReason,
@@ -15126,8 +15688,8 @@ function preImage(repoRelPath, baseline) {
15126
15688
  perBaseline = /* @__PURE__ */ new Map();
15127
15689
  preImageCache.set(baseline, perBaseline);
15128
15690
  }
15129
- const cached = perBaseline.get(repoRelPath);
15130
- if (cached) return cached;
15691
+ const cached2 = perBaseline.get(repoRelPath);
15692
+ if (cached2) return cached2;
15131
15693
  const resolved = resolvePreImage(repoRelPath, baseline);
15132
15694
  perBaseline.set(repoRelPath, resolved);
15133
15695
  return resolved;
@@ -15171,6 +15733,34 @@ ${addedLines}`,
15171
15733
  }
15172
15734
  return { diffs, has_baseline: true };
15173
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
+ }
15174
15764
  function changedSinceBaseline(repoRelPath, baseline) {
15175
15765
  const pre = preImage(repoRelPath, baseline);
15176
15766
  let current;
@@ -16047,40 +16637,40 @@ function narrowToRecent(files, sessionId) {
16047
16637
  });
16048
16638
  return recent.length > 0 ? recent : files;
16049
16639
  }
16050
- function readIteration(currentCommit, _contentHash) {
16051
- 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 };
16052
16642
  try {
16053
16643
  const stored = (0, import_node_fs15.readFileSync)(ITERATION_FILE, "utf-8").trim();
16054
16644
  const parts = stored.split(":");
16055
16645
  const iter = parseInt(parts[0], 10);
16056
16646
  const storedCommit = parts[1] ?? "";
16057
16647
  const storedTimestamp = parseInt(parts[2] ?? "0", 10);
16058
- if (isNaN(iter)) return 1;
16059
- 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 };
16060
16651
  if (storedTimestamp > 0) {
16061
16652
  const elapsed = Math.floor(Date.now() / 1e3) - storedTimestamp;
16062
- if (elapsed > 600) return 1;
16653
+ if (elapsed > 600) return { iteration: 1, fingerprint: null };
16063
16654
  }
16064
- return iter;
16655
+ return { iteration: iter, fingerprint };
16065
16656
  } catch {
16066
- return 1;
16657
+ return { iteration: 1, fingerprint: null };
16067
16658
  }
16068
16659
  }
16069
- function checkMaxIterations(currentCommit, maxIterations = MAX_ITERATIONS, contentHash) {
16070
- const iteration = readIteration(currentCommit, contentHash);
16071
- if (iteration > maxIterations) {
16072
- writeIteration(1, currentCommit, contentHash);
16073
- return {
16074
- skip: `Max Verity iterations (${maxIterations}) reached \u2014 accepting to prevent infinite loop. Human review required before deploying.`,
16075
- iteration
16076
- };
16077
- }
16078
- 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));
16079
16668
  }
16080
- function writeIteration(iteration, commit, _contentHash) {
16669
+ function writeIteration(iteration, commit, _contentHash, fingerprint) {
16081
16670
  (0, import_node_fs15.mkdirSync)(VERITY_DIR, { recursive: true });
16082
16671
  const ts = Math.floor(Date.now() / 1e3);
16083
- (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}`);
16084
16674
  }
16085
16675
 
16086
16676
  // src/lib/static-analysis.ts
@@ -16329,7 +16919,7 @@ function resolveTaskContext(opts) {
16329
16919
  // src/lib/cli-version.ts
16330
16920
  function cliVersion() {
16331
16921
  try {
16332
- return true ? "0.28.1-experimental.5ec7373" : "dev";
16922
+ return true ? "0.28.1-experimental.644501f" : "dev";
16333
16923
  } catch {
16334
16924
  return "dev";
16335
16925
  }
@@ -16423,10 +17013,26 @@ function cacheRequest(body) {
16423
17013
  (0, import_node_fs18.mkdirSync)(CACHE_DIR, { recursive: true });
16424
17014
  const suffix = (0, import_node_crypto9.randomBytes)(4).toString("hex");
16425
17015
  const filename = `pending-${Math.floor(Date.now() / 1e3)}-${suffix}.json`;
16426
- (0, import_node_fs18.writeFileSync)(`${CACHE_DIR}/${filename}`, JSON.stringify(body));
17016
+ (0, import_node_fs18.writeFileSync)(`${CACHE_DIR}/${filename}`, JSON.stringify(redactRequest(body)));
16427
17017
  } catch {
16428
17018
  }
16429
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
+ }
16430
17036
  function buildOfflineFallback(reason, staticResults) {
16431
17037
  return {
16432
17038
  // G12 / INV-18 — WARN, not PASS.
@@ -16554,6 +17160,16 @@ var KNOWN_RECORD_TYPES = /* @__PURE__ */ new Set([
16554
17160
  "subagent"
16555
17161
  ]);
16556
17162
  var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "NotebookEdit", "MultiEdit"]);
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
+ }
16557
17173
  var COMMAND_CLASSES = [
16558
17174
  [/\b(npm|yarn|pnpm|bun)\s+(run\s+)?test\b|\bvitest\b|\bjest\b|\bpytest\b|\bgo test\b/, "test"],
16559
17175
  [/\b(npm|yarn|pnpm|bun)\s+(run\s+)?build\b|\btsc\b|\bwebpack\b|\bcargo build\b/, "build"],
@@ -16643,8 +17259,8 @@ function commandShape(cmd) {
16643
17259
  var COMMAND_HEAD_CHARS = 80;
16644
17260
  var rootCandidateCache = /* @__PURE__ */ new Map();
16645
17261
  function candidateRoots(repoRoot2) {
16646
- const cached = rootCandidateCache.get(repoRoot2);
16647
- if (cached) return cached;
17262
+ const cached2 = rootCandidateCache.get(repoRoot2);
17263
+ if (cached2) return cached2;
16648
17264
  const norm = repoRoot2.replace(/\\/g, "/").replace(/\/+$/, "");
16649
17265
  const out = [norm];
16650
17266
  try {
@@ -16680,6 +17296,8 @@ function fold(transcriptPath, opts = {}) {
16680
17296
  totalRecords: 0,
16681
17297
  malformed: 0,
16682
17298
  subagentFiles: 0,
17299
+ dispatched: 0,
17300
+ userMessages: 0,
16683
17301
  subagentSkipped: 0,
16684
17302
  compactions: 0,
16685
17303
  complete: false
@@ -16706,7 +17324,8 @@ function fold(transcriptPath, opts = {}) {
16706
17324
  if (type === "system" && record.subtype === "compact_boundary") {
16707
17325
  result.coverage.compactions++;
16708
17326
  }
16709
- collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, opts.repoRoot);
17327
+ if (type === "user" && hasUserText(record)) result.coverage.userMessages++;
17328
+ collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, opts.repoRoot, result.coverage);
16710
17329
  }
16711
17330
  };
16712
17331
  try {
@@ -16778,7 +17397,7 @@ function classifyUnobserved(path) {
16778
17397
  if (/\.(png|jpg|jpeg|gif|pdf|zip|woff2?|ico|mp4)$/i.test(path)) return "binary";
16779
17398
  return "no_edit_record";
16780
17399
  }
16781
- function collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, repoRoot2) {
17400
+ function collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, repoRoot2, tally) {
16782
17401
  const message = record.message;
16783
17402
  const content = message?.content ?? record.content;
16784
17403
  const blocks = Array.isArray(content) ? content : content && typeof content === "object" ? [content] : [];
@@ -16800,6 +17419,9 @@ function collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse
16800
17419
  byPath.set(path, entry);
16801
17420
  }
16802
17421
  }
17422
+ if (DISPATCH_TOOLS.has(name) && tally) {
17423
+ tally.dispatched += 1;
17424
+ }
16803
17425
  if (name === "Bash") {
16804
17426
  const cmd = typeof input.command === "string" ? input.command : "";
16805
17427
  if (cmd) {
@@ -16858,6 +17480,86 @@ function checkConservation(changedFiles, result, repoRoot2) {
16858
17480
  };
16859
17481
  }
16860
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
+
16861
17563
  // src/lib/channel.ts
16862
17564
  var MAX_AGENT_CONTEXT_CHARS = 1500;
16863
17565
  var MAX_AGENT_ITEMS = 5;
@@ -16907,9 +17609,13 @@ function buildAgentContext(input) {
16907
17609
  }
16908
17610
  for (const p of input.pendingItems ?? []) {
16909
17611
  if (lines.length >= MAX_AGENT_ITEMS) break;
17612
+ if (p.pattern_id === "intent-misalignment") continue;
16910
17613
  const text = p.description ?? p.title ?? p.reason;
16911
17614
  if (!text) continue;
16912
- 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
+ );
16913
17619
  }
16914
17620
  if (lines.length === 0) return null;
16915
17621
  const body = `${REPORT_PREFIX}
@@ -16936,6 +17642,47 @@ function buildHookOutput(gateDecision, systemMessage, agentContext) {
16936
17642
  } : {}
16937
17643
  };
16938
17644
  }
17645
+ var IDLE_EPISODE_CAP = 3;
17646
+ function channelSilence(input) {
17647
+ const movedSomething = input.newUserPrompt || input.newAuthorship;
17648
+ if (movedSomething) return null;
17649
+ if (input.consecutiveIdle >= IDLE_EPISODE_CAP) return "idle-episode-cap";
17650
+ if (input.emittedLast) return "caused-by-our-own-emission";
17651
+ return null;
17652
+ }
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
+ }
16939
17686
 
16940
17687
  // src/lib/cache-cleanup.ts
16941
17688
  var import_node_fs21 = require("node:fs");
@@ -17018,6 +17765,13 @@ function isGitOnlyPrompt(prompt) {
17018
17765
  return true;
17019
17766
  }
17020
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) {
17021
17775
  if (!predictedMode || !isValidMode(predictedMode)) {
17022
17776
  return detectAnalysisMode(
17023
17777
  signals.noFilesChanged,
@@ -17116,46 +17870,6 @@ function shouldWarmRetryAnalyze(result) {
17116
17870
  return false;
17117
17871
  }
17118
17872
 
17119
- // src/lib/skip-detection.ts
17120
- function isBareAckPrompt(prompt) {
17121
- if (typeof prompt !== "string") return false;
17122
- const trimmed = prompt.trim();
17123
- if (trimmed.length === 0) return false;
17124
- if (trimmed.length > 20) return false;
17125
- 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;
17126
- return bareAckPattern.test(trimmed);
17127
- }
17128
- function isReflectionQuestion(response) {
17129
- if (!response || typeof response !== "string") return false;
17130
- const markers = [
17131
- /reflection\s+for\s+future\s+agents/i,
17132
- /what(?:'s|\s+is)\s+one\s+thing\s+you\s+learned/i,
17133
- /say\s+['"]?skip['"]?\s+to\s+skip/i,
17134
- /quick\s+reflection\s+question/i,
17135
- // Post-flip (VRT-21): the agent drafts the reflection itself and, when
17136
- // interactive, asks the user to confirm/correct before recording. That
17137
- // turn authors no code either, so it's still a reflection turn.
17138
- /reflection\s+draft/i,
17139
- /confirm,?\s+correct,?\s+or\s+add/i
17140
- ];
17141
- return markers.some((m) => m.test(response));
17142
- }
17143
- function isMetaTaskLabel(label2) {
17144
- if (label2 === null || label2 === void 0) return false;
17145
- if (typeof label2 !== "string") return false;
17146
- const trimmed = label2.trim();
17147
- if (trimmed.length === 0) return true;
17148
- const metaPatterns = [
17149
- /^verity\s+[\w-]+\s+response$/i,
17150
- // "Verity reflect response"
17151
- /^simple user response$/i,
17152
- /^verity\s+command$/i,
17153
- // "Verity command"
17154
- /^user\s+(question|reply|response|ack)$/i
17155
- ];
17156
- return metaPatterns.some((p) => p.test(trimmed));
17157
- }
17158
-
17159
17873
  // src/lib/transcript.ts
17160
17874
  var import_node_fs22 = require("node:fs");
17161
17875
  var MAX_READ_BYTES = 256 * 1024;
@@ -17169,9 +17883,11 @@ var MAX_SUMMARY_BYTES = 4096;
17169
17883
  var HOME = process.env.HOME ?? "";
17170
17884
  async function extractActionSummary(transcriptPath) {
17171
17885
  try {
17172
- const lines = readTurnLines(transcriptPath);
17173
- if (!lines || lines.length === 0) return null;
17174
- 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;
17175
17891
  } catch {
17176
17892
  return null;
17177
17893
  }
@@ -17185,9 +17901,11 @@ function readTurnLines(transcriptPath) {
17185
17901
  }
17186
17902
  if (size === 0) return null;
17187
17903
  let raw;
17904
+ let windowed = false;
17188
17905
  if (size <= SMALL_FILE_BYTES) {
17189
17906
  raw = (0, import_node_fs22.readFileSync)(transcriptPath, "utf-8");
17190
17907
  } else {
17908
+ windowed = true;
17191
17909
  const buf = Buffer.alloc(Math.min(MAX_READ_BYTES, size));
17192
17910
  const fd = require("node:fs").openSync(transcriptPath, "r");
17193
17911
  try {
@@ -17205,17 +17923,22 @@ function readTurnLines(transcriptPath) {
17205
17923
  const allLines = raw.split("\n").filter((l) => l.trim().length > 0);
17206
17924
  if (allLines.length === 0) return null;
17207
17925
  let turnStart = 0;
17926
+ let boundaryFound = false;
17208
17927
  for (let i = allLines.length - 1; i >= 0; i--) {
17209
17928
  try {
17210
17929
  const parsed = JSON.parse(allLines[i]);
17211
17930
  if (parsed.type === "user" && isRealUserMessage(parsed)) {
17212
17931
  turnStart = i;
17932
+ boundaryFound = true;
17213
17933
  break;
17214
17934
  }
17215
17935
  } catch {
17216
17936
  }
17217
17937
  }
17218
- return allLines.slice(turnStart);
17938
+ return {
17939
+ lines: allLines.slice(turnStart),
17940
+ window: !windowed ? "whole" : boundaryFound ? "windowed" : "orphaned"
17941
+ };
17219
17942
  }
17220
17943
  function isRealUserMessage(parsed) {
17221
17944
  const message = parsed.message;
@@ -17319,6 +18042,13 @@ function buildSummary(lines) {
17319
18042
  files_read: capArray(filesRead, MAX_FILES_LIST),
17320
18043
  files_edited: capArray(filesEdited, MAX_FILES_LIST),
17321
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
+ ],
17322
18052
  searches,
17323
18053
  commands,
17324
18054
  subagents,
@@ -17369,6 +18099,9 @@ function sanitizeCommand(rawCmd) {
17369
18099
  function capArray(set, max) {
17370
18100
  return Array.from(set).slice(0, max);
17371
18101
  }
18102
+ function cappedOut(set, max) {
18103
+ return Array.from(set).slice(max);
18104
+ }
17372
18105
 
17373
18106
  // src/lib/run-mode.ts
17374
18107
  function parseAutonomousEnv(raw) {
@@ -17763,11 +18496,12 @@ async function readStopHookStdin() {
17763
18496
  return empty;
17764
18497
  }
17765
18498
  }
17766
- function agentContextFor(response, intentRepeat = 0) {
18499
+ function agentContextFor(response, intentRepeat = 0, priorPendingFingerprints = []) {
17767
18500
  const metadata = response.metadata ?? {};
17768
18501
  const intent = response.intent_alignment ?? {};
17769
18502
  return buildAgentContext({
17770
18503
  intentRepeat,
18504
+ priorPendingFingerprints,
17771
18505
  gateDecision: String(response.gate_decision ?? ""),
17772
18506
  findings: response.findings ?? [],
17773
18507
  pendingItems: response.pending_items ?? [],
@@ -17778,12 +18512,45 @@ function agentContextFor(response, intentRepeat = 0) {
17778
18512
  });
17779
18513
  }
17780
18514
  var beaconCtx = null;
17781
- async function passAndExit(reason, skip) {
18515
+ async function passAndExit(reason, skip, kindOverride) {
17782
18516
  const sent = await sendSkipBeacon(beaconCtx, skip);
17783
18517
  logEvent("skip", { reason: skip, beacon: sent });
17784
- 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
+ );
17785
18551
  process.exit(0);
17786
18552
  }
18553
+ var skipCoverageChanged = [];
17787
18554
  var EMPTY_STATIC = {
17788
18555
  tool: "@codacy/analysis-cli",
17789
18556
  findings: [],
@@ -17798,7 +18565,7 @@ function runLocalStatic(analyzable, securityFiles, baseline, skipStatic) {
17798
18565
  }
17799
18566
  function localOnlyAndExit(staticResults) {
17800
18567
  printJsonCompact({
17801
- gate_decision: "PASS",
18568
+ gate_decision: "WARN",
17802
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.",
17803
18570
  unauthenticated: true,
17804
18571
  static_results: staticResults
@@ -17858,6 +18625,7 @@ async function runAnalyze(opts, globals) {
17858
18625
  });
17859
18626
  }
17860
18627
  const { files: allChanged, hasRecentCommitFiles } = getChangedFiles();
18628
+ skipCoverageChanged = allChanged;
17861
18629
  const analyzable = filterAnalyzable(allChanged);
17862
18630
  const reviewable = filterReviewable(allChanged);
17863
18631
  const securityFiles = filterSecurity(allChanged);
@@ -17869,15 +18637,27 @@ async function runAnalyze(opts, globals) {
17869
18637
  const conversation = await readAndClearConversationBuffer(baselineSessionId);
17870
18638
  const specs = discoverSpecs();
17871
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;
17872
18645
  const latestPrompt = conversation?.prompts?.[conversation.prompts.length - 1]?.prompt ?? "";
17873
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
+ }
17874
18655
  await passAndExit("Verity command \u2014 skipping analysis", "verity-command");
17875
18656
  }
17876
- if (isBareAckPrompt(latestPrompt)) {
18657
+ if (shouldSkipForBareAck({ prompt: latestPrompt, turnAuthoredCode, canSeeTurnAuthorship })) {
17877
18658
  await passAndExit("Bare acknowledgment \u2014 skipping analysis", "bare-acknowledgment");
17878
18659
  }
17879
- const agentAuthoredCodeThisTurn = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0));
17880
- if (isReflectionQuestion(assistantResponse) && !agentAuthoredCodeThisTurn) {
18660
+ if (isReflectionQuestion(assistantResponse) && !turnAuthoredCode && canSeeTurnAuthorship) {
17881
18661
  await passAndExit("Reflection-prompt turn \u2014 skipping analysis", "reflection-prompt");
17882
18662
  }
17883
18663
  const urlResult = await resolveServiceUrl(globals.serviceUrl);
@@ -17923,7 +18703,11 @@ async function runAnalyze(opts, globals) {
17923
18703
  );
17924
18704
  }
17925
18705
  if (analysisMode === "skip") {
17926
- 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
+ );
17927
18711
  }
17928
18712
  let staticResults = {
17929
18713
  tool: "@codacy/analysis-cli",
@@ -17933,7 +18717,8 @@ async function runAnalyze(opts, globals) {
17933
18717
  let codeDelta = {
17934
18718
  files: [],
17935
18719
  total_lines: 0,
17936
- total_files: 0
18720
+ total_files: 0,
18721
+ excluded: []
17937
18722
  };
17938
18723
  let snapshotResult = { has_snapshots: false, diffs: [] };
17939
18724
  let contentHash = null;
@@ -17974,10 +18759,43 @@ async function runAnalyze(opts, globals) {
17974
18759
  contentHash = hashResult.hash;
17975
18760
  if (analysisMode !== "plan") {
17976
18761
  const scoped = scopeToAuthored(allForReview, actionSummary);
17977
- if (scoped.signal === "none-authored" && !hasNonEditAuthorship(actionSummary, sessionAuthoredCode)) {
18762
+ const canTrustNoneAuthored = scoped.signal === "none-authored" && authorshipIsObservable;
18763
+ if (canTrustNoneAuthored && !hasNonEditAuthorship(actionSummary, sessionAuthoredCode)) {
17978
18764
  await passAndExit("No agent-authored code this turn \u2014 working-tree changes were not authored by this session", "zero-increment");
17979
18765
  }
17980
- 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;
17981
18799
  const recentForReview = narrowToRecent(baseForReview, baselineSessionId);
17982
18800
  if (!opts.skipStatic && isCodacyAvailable()) {
17983
18801
  let allScannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles]));
@@ -17998,7 +18816,11 @@ async function runAnalyze(opts, globals) {
17998
18816
  if (assistantResponse) {
17999
18817
  analysisMode = "plan";
18000
18818
  } else {
18001
- 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
+ );
18002
18824
  }
18003
18825
  }
18004
18826
  }
@@ -18011,19 +18833,13 @@ async function runAnalyze(opts, globals) {
18011
18833
  snapshotResult = generateSnapshotDiffs(codeDelta.files);
18012
18834
  }
18013
18835
  currentCommit = getCurrentCommit();
18014
- const maxIterations = parseInt(opts.maxIterations, 10);
18015
- const iterResult = checkMaxIterations(currentCommit, maxIterations, contentHash ?? void 0);
18016
- if (iterResult.skip) await passAndExit(iterResult.skip, "iteration-cap");
18017
- iteration = iterResult.iteration;
18836
+ iteration = readIterationState(currentCommit).iteration;
18018
18837
  }
18019
18838
  }
18020
18839
  if (analysisMode === "plan") {
18021
18840
  recordAnalysisStart();
18022
18841
  currentCommit = getCurrentCommit();
18023
- const maxIterations = parseInt(opts.maxIterations, 10);
18024
- const iterResult = checkMaxIterations(currentCommit, maxIterations);
18025
- if (iterResult.skip) await passAndExit(iterResult.skip, "iteration-cap");
18026
- iteration = iterResult.iteration;
18842
+ iteration = readIterationState(currentCommit).iteration;
18027
18843
  }
18028
18844
  const contextFiles = gatherContextFiles(contextFilePaths, codeDelta.files);
18029
18845
  for (const f of codeDelta.files) {
@@ -18090,7 +18906,7 @@ async function runAnalyze(opts, globals) {
18090
18906
  let foldConservation = null;
18091
18907
  if (transcriptPath) {
18092
18908
  try {
18093
- foldResult = fold(transcriptPath, { changedFiles: allForReview, repoRoot: repoRoot() });
18909
+ foldResult = earlyFold ?? fold(transcriptPath, { changedFiles: allForReview, repoRoot: repoRoot() });
18094
18910
  foldConservation = checkConservation(allForReview, foldResult, repoRoot());
18095
18911
  if (!foldConservation.holds) {
18096
18912
  process.stderr.write(
@@ -18172,7 +18988,10 @@ async function runAnalyze(opts, globals) {
18172
18988
  priorState.capabilities
18173
18989
  );
18174
18990
  memory = recallMemory(memorySession.d, memorySession.identity, {
18175
- 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 }
18176
18995
  });
18177
18996
  if (memory && !memory.provenanceHolds) {
18178
18997
  process.stderr.write("Verity: working-memory provenance check failed \u2014 recall suppressed.\n");
@@ -18193,7 +19012,30 @@ async function runAnalyze(opts, globals) {
18193
19012
  hasUserPrompt: (conversation?.prompts?.length ?? 0) > 0,
18194
19013
  isTTY: process.stdout.isTTY === true
18195
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
+ };
18196
19037
  const requestBody = {
19038
+ coverage_telemetry: coverageTelemetry,
18197
19039
  static_results: staticResults,
18198
19040
  code_delta: codeDelta,
18199
19041
  changed_files: allForReview,
@@ -18279,6 +19121,54 @@ async function runAnalyze(opts, globals) {
18279
19121
  // replaced a population floor with ≈35% power that was sub-integer for
18280
19122
  // three-quarters of the fleet.
18281
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
+ })(),
18282
19172
  // P2 — RECORDED, NOT APPLIED. What this run WOULD have reviewed if it
18283
19173
  // narrowed to the within-session increment: what changed since the last
18284
19174
  // VERDICT rather than since task start.
@@ -18311,7 +19201,20 @@ async function runAnalyze(opts, globals) {
18311
19201
  const intentContext = {};
18312
19202
  if (conversation && conversation.prompts.length > 0) {
18313
19203
  const latest = conversation.prompts[conversation.prompts.length - 1];
18314
- 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
+ }
18315
19218
  intentContext.session_id = latest.session_id || void 0;
18316
19219
  intentContext.prompt_captured_at = latest.captured_at || void 0;
18317
19220
  if (conversation.prompts.length > 1) {
@@ -18387,6 +19290,8 @@ async function runAnalyze(opts, globals) {
18387
19290
  message = "Verity: your GitHub verification expired \u2014 run `verity login` to re-verify (local analysis this run)";
18388
19291
  } else if (result.error.startsWith("FORBIDDEN")) {
18389
19292
  message = "Verity: no access grant for this repository \u2014 run `verity login` to refresh your grants (local analysis this run)";
19293
+ } else if (result.error.startsWith("INVALID_TOKEN")) {
19294
+ message = "Verity: your login expired or was revoked \u2014 run `verity login` to sign in again (local analysis this run)";
18390
19295
  } else if (result.http_status && result.http_status >= 400 && result.http_status < 500) {
18391
19296
  message = `Verity: request rejected (HTTP ${result.http_status}) \u2014 ${result.error}`;
18392
19297
  } else if (result.http_status && result.http_status >= 500) {
@@ -18401,9 +19306,122 @@ async function runAnalyze(opts, globals) {
18401
19306
  const response = result.data;
18402
19307
  const decision = response.gate_decision ?? "(unrecognised)";
18403
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
+ };
18404
19388
  const watermarkHash = sentPaths.length > 0 ? computeContentHash(sentPaths) : contentHash;
18405
19389
  const watermarkIsPartial = !!codeDelta.truncated;
19390
+ let silenced = null;
19391
+ let turnIsIdleForChannel = true;
19392
+ if (memorySession) {
19393
+ try {
19394
+ const st = foldDossier(memorySession.d);
19395
+ turnIsIdleForChannel = st.authored_all.every((a) => a.hash_now === a.hash_at_last_verdict);
19396
+ silenced = channelSilence({
19397
+ // The BUFFER, not intentContext.user_prompt: the latter falls back to a
19398
+ // linked issue (VRT-53 W4) when no human spoke, and a fallback goal is
19399
+ // not a user utterance. Treating it as one would keep the loop alive on
19400
+ // exactly the autonomous cohort.
19401
+ newUserPrompt: (conversation?.prompts?.length ?? 0) > 0,
19402
+ newAuthorship: !turnIsIdleForChannel,
19403
+ emittedLast: st.meta.channel?.emittedLast === true,
19404
+ consecutiveIdle: st.meta.channel?.consecutiveIdle ?? 0
19405
+ });
19406
+ } catch {
19407
+ silenced = null;
19408
+ }
19409
+ }
19410
+ if (silenced) {
19411
+ logEvent("channel_silenced", {
19412
+ reason: silenced,
19413
+ run_id: response.run_id ?? turnId,
19414
+ decision
19415
+ });
19416
+ }
18406
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
+ })() : [];
18407
19425
  if (memorySession) {
18408
19426
  try {
18409
19427
  recordVerdict(memorySession.d, {
@@ -18418,7 +19436,18 @@ async function runAnalyze(opts, globals) {
18418
19436
  title: f.title,
18419
19437
  severity: f.severity
18420
19438
  })) ?? [],
18421
- intent: response.intent_alignment ?? null
19439
+ intent: response.intent_alignment ?? null,
19440
+ // The same signal F1 introduced: bytes differing from the hash frozen at
19441
+ // the last verdict. A turn that moved nothing is the only kind that can
19442
+ // accumulate a repeat.
19443
+ idle: turnIsIdleForChannel,
19444
+ // What next turn reads as `emittedLast`. A suppressed turn did not
19445
+ // speak, so it cannot be the cause of the turn after it — which is what
19446
+ // keeps this from becoming a permanent gag.
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)
18422
19451
  });
18423
19452
  intentRepeatCount = Math.max(0, (foldDossier(memorySession.d).meta.intent_repeat?.consecutive ?? 1) - 1);
18424
19453
  } catch {
@@ -18518,9 +19547,41 @@ async function runAnalyze(opts, globals) {
18518
19547
  reverify_by: response.reverify_by
18519
19548
  });
18520
19549
  const grantNudge = grantWarning ? ` \u26A0\uFE0F ${grantWarning}` : "";
18521
- 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) {
18522
19584
  case "FAIL": {
18523
- writeIteration(iteration + 1, currentCommit, contentHash ?? void 0);
18524
19585
  const assessment = response.assessment;
18525
19586
  const narrative = assessment?.narrative ?? "";
18526
19587
  const findings = response.findings ?? [];
@@ -18597,7 +19658,19 @@ ${YELLOW}${loginNudge.trim()}${NC}
18597
19658
  if (grantNudge) process.stderr.write(`
18598
19659
  ${YELLOW}${grantNudge.trim()}${NC}
18599
19660
  `);
18600
- 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
+ });
18601
19674
  break;
18602
19675
  }
18603
19676
  case "PASS": {
@@ -18609,10 +19682,16 @@ ${YELLOW}${grantNudge.trim()}${NC}
18609
19682
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
18610
19683
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
18611
19684
  userSummary += loginNudge + grantNudge;
18612
- printJsonCompact(
18613
- buildHookOutput("PASS", userSummary, agentContextFor(response, intentRepeatCount))
18614
- );
18615
- 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
+ });
18616
19695
  break;
18617
19696
  }
18618
19697
  case "WARN": {
@@ -18623,10 +19702,16 @@ ${YELLOW}${grantNudge.trim()}${NC}
18623
19702
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
18624
19703
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
18625
19704
  userSummary += loginNudge + grantNudge;
18626
- printJsonCompact(
18627
- buildHookOutput("WARN", userSummary, agentContextFor(response, intentRepeatCount))
18628
- );
18629
- 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
+ });
18630
19715
  break;
18631
19716
  }
18632
19717
  default: {
@@ -18739,8 +19824,8 @@ async function runReview(opts, globals) {
18739
19824
  for (const p of specPaths) {
18740
19825
  if (!(0, import_node_fs26.existsSync)(p)) continue;
18741
19826
  try {
18742
- const { readFileSync: readFileSync15 } = await import("node:fs");
18743
- const content = readFileSync15(p, "utf-8");
19827
+ const { readFileSync: readFileSync16 } = await import("node:fs");
19828
+ const content = readFileSync16(p, "utf-8");
18744
19829
  specs.push({ path: p, content: content.slice(0, 10240) });
18745
19830
  } catch {
18746
19831
  }
@@ -19042,7 +20127,7 @@ async function runGuard(opts, globals) {
19042
20127
  cmd: "guard"
19043
20128
  });
19044
20129
  if (!result.ok) {
19045
- const authRemedy = result.error.startsWith("STALE_VERIFICATION") ? " Your GitHub verification expired \u2014 run `verity login` to re-verify." : result.error.startsWith("FORBIDDEN") ? " No access grant for this repository \u2014 run `verity login` to refresh your grants." : "";
20130
+ const authRemedy = result.error.startsWith("STALE_VERIFICATION") ? " Your GitHub verification expired \u2014 run `verity login` to re-verify." : result.error.startsWith("FORBIDDEN") ? " No access grant for this repository \u2014 run `verity login` to refresh your grants." : result.error.startsWith("INVALID_TOKEN") ? " Your Verity login expired or was revoked \u2014 run `verity login` to sign in again." : "";
19046
20131
  emitAllowNotice(
19047
20132
  `\u26A0 Verity ${moment}: ${authRemedy ? "not authorized" : "service offline"} \u2014 ${verb}ed WITHOUT review${authRemedy}`,
19048
20133
  `Verity ${moment}: ${authRemedy ? "not authorized" : "service unavailable"} (${result.error}); the ${verb} was allowed WITHOUT a Verity review.${authRemedy}`
@@ -19543,6 +20628,13 @@ async function confirmExistingLogin(serviceUrl, remote, opts) {
19543
20628
  return "handled";
19544
20629
  }
19545
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
+ }
19546
20638
  if (existing.data.userId != null) {
19547
20639
  printInfo(`Logged in as ${existing.data.email ?? `user #${existing.data.userId}`} (cached \u2014 could not reach the Verity service). \u2713`);
19548
20640
  } else {
@@ -19558,15 +20650,14 @@ async function confirmExistingLogin(serviceUrl, remote, opts) {
19558
20650
  return "drive-login";
19559
20651
  }
19560
20652
  async function runOptionalAuth(resolution, opts = {}) {
19561
- let serviceUrl = resolution?.url ?? DEFAULT_SERVICE_URL;
19562
- let healed = false;
19563
- if (resolution) {
19564
- const heal = await maybeHealServiceUrl(resolution, opts.verbose);
19565
- serviceUrl = heal.serviceUrl;
19566
- healed = heal.healed;
19567
- if (healed) {
19568
- printInfo(" Log in below to re-register this project and repair ~/.verity/credentials.");
19569
- }
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.");
19570
20661
  }
19571
20662
  let remote = "";
19572
20663
  try {
@@ -19584,8 +20675,10 @@ async function runOptionalAuth(resolution, opts = {}) {
19584
20675
  if (process.stdin.isTTY && process.stdout.isTTY) {
19585
20676
  console.log("");
19586
20677
  console.log(" Signing in is optional. What it does:");
19587
- console.log(" - Confirms you have write access to this repository. The GitHub token");
19588
- 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.");
19589
20682
  console.log(" - It does NOT give Verity access to your code. Code checked by the gate");
19590
20683
  console.log(" is analyzed in memory and discarded \u2014 we never store your code.");
19591
20684
  console.log(" - It is required to store and access run history for this repo");
@@ -19600,17 +20693,10 @@ async function runOptionalAuth(resolution, opts = {}) {
19600
20693
  localOnlyNote();
19601
20694
  return;
19602
20695
  }
19603
- if (!remote) {
19604
- printWarn("No git remote found \u2014 cannot authenticate yet.");
19605
- localOnlyNote();
19606
- return;
19607
- }
19608
- const projectName = parseRemote(remote)?.repo ?? (0, import_node_path23.basename)(process.cwd());
19609
20696
  printInfo("Authenticating with GitHub\u2026");
19610
- const result = await registerProject({ projectName, remote, serviceUrl, verbose: opts.verbose });
20697
+ const result = await loginOnce({ serviceUrl, remote: remote || void 0, verbose: opts.verbose });
19611
20698
  if (result.ok) {
19612
- const who = result.data.email ?? (result.data.userId != null ? `user #${result.data.userId}` : null);
19613
- 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 });
19614
20700
  } else {
19615
20701
  printWarn(`Authentication did not complete: ${result.error}`);
19616
20702
  localOnlyNote();
@@ -19761,8 +20847,8 @@ function registerInitCommand(program2) {
19761
20847
  console.log("");
19762
20848
  try {
19763
20849
  const globals = program2.opts();
19764
- const urlResult = await resolveServiceUrlDetailed(globals.serviceUrl);
19765
- await runOptionalAuth(urlResult.ok ? urlResult.data : null, {
20850
+ const resolution = await resolveServiceUrlForAuth(globals.serviceUrl);
20851
+ await runOptionalAuth(resolution, {
19766
20852
  token: globals.token,
19767
20853
  verbose: globals.verbose
19768
20854
  });
@@ -20378,6 +21464,12 @@ function registerTelemetryCommands(program2) {
20378
21464
  printError(urlResult.error);
20379
21465
  process.exit(1);
20380
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
+ }
20381
21473
  const result = await installTelemetry(urlResult.data);
20382
21474
  if (!result.ok) {
20383
21475
  printError(result.error);
@@ -20426,7 +21518,8 @@ function registerTelemetryCommands(program2) {
20426
21518
  }
20427
21519
 
20428
21520
  // src/cli.ts
20429
- program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.5ec7373").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.644501f").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);
20430
21523
  try {
20431
21524
  await foldLegacyLocalCredential();
20432
21525
  } catch {
@@ -20435,6 +21528,8 @@ program.name("verity").description("CLI for Verity quality gate service").versio
20435
21528
  registerAuthCommands(program);
20436
21529
  registerLoginCommand(program);
20437
21530
  registerTokenCommand(program);
21531
+ registerSessionsCommands(program);
21532
+ registerLogoutCommand(program);
20438
21533
  registerHooksCommands(program);
20439
21534
  registerIntentCommands(program);
20440
21535
  registerLifecycleCommands(program);