@codacy/verity-cli 0.28.1-experimental.5ec7373 → 0.28.1-experimental.8cc9c66

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 +592 -41
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -63,6 +63,11 @@ No re-setup needed: your token, Standard, and run history all carry over.
63
63
  | `verity token create --name <name>` | Mint a service token for CI (`--expires <days>` optional) |
64
64
  | `verity token list` | List this repository's tokens (ids and metadata only) |
65
65
  | `verity token revoke <id>` | Revoke a token by id |
66
+ | `verity sessions list` | List your logins — device, last use, expiry (logins expire after 90 days) |
67
+ | `verity sessions revoke <id>` | Revoke one login by session id |
68
+ | `verity logout` | Sign out on this machine |
69
+ | `verity logout --others` | Sign out every OTHER machine (e.g. a lost laptop) |
70
+ | `verity logout --all` | Sign out everywhere, including here |
66
71
  | `verity hooks install` | Wire Claude Code hooks |
67
72
  | `verity standard push` | Upload project Standard |
68
73
  | **Knowledge** | |
package/bin/verity.js CHANGED
@@ -10636,14 +10636,14 @@ async function readGlobalCredential(remote) {
10636
10636
  const parsed = parseCredentialLine(line);
10637
10637
  if (parsed && parsed.remote === key) last = parsed.rec;
10638
10638
  }
10639
- if (last) return last;
10639
+ if (last) return { ...last, keyed: true };
10640
10640
  }
10641
10641
  let plain = null;
10642
10642
  for (const line of lines) {
10643
10643
  const parsed = parseCredentialLine(line);
10644
10644
  if (parsed && parsed.remote === "") plain = parsed.rec;
10645
10645
  }
10646
- return plain;
10646
+ return plain ? { ...plain, keyed: false } : null;
10647
10647
  }
10648
10648
  async function upsertGlobalCredential(remote, rec) {
10649
10649
  const path = globalCredentialsPath();
@@ -10707,6 +10707,36 @@ async function removeSupersededUserCredentials(loginServiceUrl, loginUserId) {
10707
10707
  }
10708
10708
  return removed;
10709
10709
  }
10710
+ async function removeGlobalCredential(remote) {
10711
+ const path = globalCredentialsPath();
10712
+ let content;
10713
+ try {
10714
+ content = await (0, import_promises.readFile)(path, "utf-8");
10715
+ } catch {
10716
+ return false;
10717
+ }
10718
+ const key = encodeRemoteKey(remote);
10719
+ const kept = [];
10720
+ let removed = false;
10721
+ for (const line of content.split("\n")) {
10722
+ const parsed = parseCredentialLine(line);
10723
+ if (parsed && parsed.remote === key) {
10724
+ removed = true;
10725
+ continue;
10726
+ }
10727
+ kept.push(line);
10728
+ }
10729
+ if (!removed) return false;
10730
+ while (kept.length && kept[kept.length - 1].trim() === "") kept.pop();
10731
+ try {
10732
+ await (0, import_promises.writeFile)(path, kept.length ? kept.join("\n") + "\n" : "", { mode: 384 });
10733
+ await (0, import_promises.chmod)(path, 384).catch(() => {
10734
+ });
10735
+ } catch {
10736
+ return false;
10737
+ }
10738
+ return true;
10739
+ }
10710
10740
  function parseLocalCredentialFile(content) {
10711
10741
  const tokenMatch = content.match(/token:\s*((?:gate_|verity_)[a-f0-9]+)/);
10712
10742
  if (!tokenMatch) return null;
@@ -11188,7 +11218,13 @@ async function resolveToken(flagToken) {
11188
11218
  if (rec) {
11189
11219
  return {
11190
11220
  ok: true,
11191
- data: { token: rec.token, source: "global", userId: rec.userId, email: rec.email }
11221
+ data: {
11222
+ token: rec.token,
11223
+ source: "global",
11224
+ userId: rec.userId,
11225
+ email: rec.email,
11226
+ keyed: rec.keyed
11227
+ }
11192
11228
  };
11193
11229
  }
11194
11230
  const local = await readLegacyLocalCredential();
@@ -11220,6 +11256,16 @@ function reverifyNudge(who) {
11220
11256
  }
11221
11257
  return null;
11222
11258
  }
11259
+ function isLegacyPerRepoCredential(auth2) {
11260
+ return auth2.source === "local" || auth2.source === "global" && auth2.keyed === true;
11261
+ }
11262
+ async function shouldUpgradeOnLogin(auth2) {
11263
+ if (!isLegacyPerRepoCredential(auth2)) return false;
11264
+ if (auth2.userId == null) return true;
11265
+ const bare = await readGlobalCredential("");
11266
+ if (bare?.userId == null) return true;
11267
+ return bare.userId === auth2.userId;
11268
+ }
11223
11269
  function authDenialRemedy(error) {
11224
11270
  if (error.startsWith("STALE_VERIFICATION")) {
11225
11271
  return {
@@ -11233,6 +11279,12 @@ function authDenialRemedy(error) {
11233
11279
  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
11280
  };
11235
11281
  }
11282
+ if (error.startsWith("INVALID_TOKEN")) {
11283
+ return {
11284
+ code: "INVALID_TOKEN",
11285
+ remedy: 'Your Verity login has expired or was revoked \u2014 run "verity login" to sign in again.'
11286
+ };
11287
+ }
11236
11288
  return null;
11237
11289
  }
11238
11290
  async function probeService(serviceUrl, verbose) {
@@ -11273,6 +11325,7 @@ async function maybeHealServiceUrl(resolution, verbose) {
11273
11325
 
11274
11326
  // src/lib/register.ts
11275
11327
  var readline = __toESM(require("node:readline/promises"));
11328
+ var import_node_os = require("node:os");
11276
11329
 
11277
11330
  // src/lib/provider-auth.ts
11278
11331
  var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
@@ -11460,6 +11513,15 @@ async function registerProject(opts) {
11460
11513
  }
11461
11514
  return { ok: true, data: { projectId: project_id, serviceUrl: service_url, email, userId } };
11462
11515
  }
11516
+ function deviceLabel() {
11517
+ const override = process.env.VERITY_DEVICE_LABEL?.trim();
11518
+ if (override) return override;
11519
+ try {
11520
+ return (0, import_node_os.hostname)() || void 0;
11521
+ } catch {
11522
+ return void 0;
11523
+ }
11524
+ }
11463
11525
  async function loginOnce(opts) {
11464
11526
  const usingTokenOverride = Boolean(process.env.VERITY_PROVIDER_TOKEN);
11465
11527
  const providerAuth = await githubDeviceFlow();
@@ -11480,13 +11542,19 @@ async function loginOnce(opts) {
11480
11542
  path: "/auth/login",
11481
11543
  serviceUrl: opts.serviceUrl,
11482
11544
  extraHeaders: { "X-Provider-Token": providerToken },
11545
+ // Label the session so its owner can tell their machines apart in
11546
+ // `verity sessions list` — a list of identical "login" rows is unusable when
11547
+ // the question is "which of these is the laptop I lost?". The hostname is the
11548
+ // useful default; VERITY_DEVICE_LABEL overrides it for anyone who would
11549
+ // rather not send it. Server-side it is sanitized and capped.
11550
+ body: { device: deviceLabel() },
11483
11551
  verbose: opts.verbose,
11484
11552
  cmd: "login"
11485
11553
  });
11486
11554
  if (!result.ok) {
11487
11555
  return { ok: false, error: result.error };
11488
11556
  }
11489
- const { token, service_url, user_id, user, repo_count } = result.data;
11557
+ const { token, service_url, user_id, user, repo_count, expires_at } = result.data;
11490
11558
  const loginUserId = user_id ?? user?.id ?? void 0;
11491
11559
  try {
11492
11560
  await upsertGlobalCredential("", {
@@ -11510,15 +11578,16 @@ async function loginOnce(opts) {
11510
11578
  email: user?.email,
11511
11579
  userId: loginUserId,
11512
11580
  repoCount: repo_count ?? 0,
11513
- prunedCredentials: pruned
11581
+ prunedCredentials: pruned,
11582
+ expiresAt: expires_at
11514
11583
  }
11515
11584
  };
11516
11585
  }
11517
11586
 
11518
11587
  // src/commands/auth.ts
11519
11588
  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) => {
11589
+ const auth2 = program2.command("auth").description("Manage project authentication");
11590
+ 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
11591
  const globals = program2.opts();
11523
11592
  const serviceUrl = globals.serviceUrl ?? DEFAULT_SERVICE_URL;
11524
11593
  let remote = opts.remote;
@@ -11545,7 +11614,7 @@ function registerAuthCommands(program2) {
11545
11614
  if (email) printInfo(`Authenticated as: ${email}`);
11546
11615
  printJson({ project_id: projectId, service_url: resolvedUrl });
11547
11616
  });
11548
- auth.command("verify").description("Verify the current token is valid").action(async () => {
11617
+ auth2.command("verify").description("Verify the current token is valid").action(async () => {
11549
11618
  const globals = program2.opts();
11550
11619
  const tokenResult = await resolveToken(globals.token);
11551
11620
  if (!tokenResult.ok) {
@@ -11571,7 +11640,7 @@ function registerAuthCommands(program2) {
11571
11640
  printInfo(`Token valid. Project: ${result.data.project_name}`);
11572
11641
  printJson(result.data);
11573
11642
  });
11574
- auth.command("discover").description("Check if a project is registered").option("--remote <url>", "Git remote URL (auto-detected if omitted)").action(async (opts) => {
11643
+ auth2.command("discover").description("Check if a project is registered").option("--remote <url>", "Git remote URL (auto-detected if omitted)").action(async (opts) => {
11575
11644
  const globals = program2.opts();
11576
11645
  let remote = opts.remote;
11577
11646
  if (!remote) {
@@ -11627,13 +11696,19 @@ function registerLoginCommand(program2) {
11627
11696
  const who = await whoami(existing.data.token, serviceUrl, globals.verbose);
11628
11697
  if (who.ok && who.data.logged_in) {
11629
11698
  const nudge = reverifyNudge(who.data);
11630
- if (!nudge) {
11699
+ const upgrade = await shouldUpgradeOnLogin(existing.data);
11700
+ if (!nudge && !upgrade) {
11631
11701
  printInfo(`Already logged in as ${who.data.email ?? `user #${who.data.user_id}`}. \u2713`);
11632
11702
  printInfo(" Re-authenticate with: verity login --force");
11633
11703
  return;
11634
11704
  }
11635
- printWarn(nudge);
11636
- printInfo("Re-verifying your repository access\u2026");
11705
+ if (nudge) {
11706
+ printWarn(nudge);
11707
+ printInfo("Re-verifying your repository access\u2026");
11708
+ } else {
11709
+ printInfo("You are signed in with a per-repository token (the old format).");
11710
+ printInfo(" Upgrading to a single login that covers every repository you can write to\u2026");
11711
+ }
11637
11712
  } else if (who.ok && who.data.anonymous) {
11638
11713
  printInfo("You have an anonymous token (the gate runs, but nothing is saved). Logging you in\u2026");
11639
11714
  } else if (!who.ok) {
@@ -11650,6 +11725,10 @@ function registerLoginCommand(program2) {
11650
11725
  const identity = out.email ?? (out.userId != null ? `user #${out.userId}` : "your account");
11651
11726
  printInfo(`Logged in as ${identity}. \u2713`);
11652
11727
  printInfo(` Access granted to ${out.repoCount} ${out.repoCount === 1 ? "repository" : "repositories"}.`);
11728
+ if (out.expiresAt) {
11729
+ printInfo(` This login expires on ${out.expiresAt.slice(0, 10)} \u2014 "verity login" again to renew.`);
11730
+ printInfo(' See your machines with "verity sessions list"; sign out with "verity logout".');
11731
+ }
11653
11732
  printInfo(" Runs, history, and cloud memory now sync to Verity everywhere you have write access.");
11654
11733
  if (out.prunedCredentials > 0) {
11655
11734
  printVerbose(`Pruned ${out.prunedCredentials} superseded per-repository credential line(s).`, globals.verbose);
@@ -11680,14 +11759,24 @@ function registerLoginCommand(program2) {
11680
11759
  const rec = await readGlobalCredential(remote);
11681
11760
  if (rec && rec.token !== out.token) {
11682
11761
  const otherBackend = rec.serviceUrl != null && rec.serviceUrl !== out.serviceUrl;
11762
+ const otherIdentity = rec.userId != null && out.userId != null && rec.userId !== out.userId;
11683
11763
  if (otherBackend) {
11684
11764
  printWarn(` Note: this repository is pinned to a different Verity service (${rec.serviceUrl})`);
11685
11765
  printWarn(" by its own credential line, which takes precedence here \u2014 this login does not");
11686
11766
  printWarn(" change that. To move the repository, remove its line from ~/.verity/credentials.");
11767
+ printWarn(' Until that line is removed, "verity login" here cannot fast-path and will run');
11768
+ printWarn(" the full GitHub flow every time.");
11769
+ } else if (otherIdentity) {
11770
+ printWarn(" Note: this repository uses a different account's credential, which takes");
11771
+ printWarn(' precedence here \u2014 this login leaves it in place, and "verity login" in this');
11772
+ printWarn(" repository will report that account. Remove its line from ~/.verity/credentials");
11773
+ printWarn(" only if you want this repository on the login you just completed.");
11687
11774
  } else {
11688
11775
  const kind = rec.userId != null ? "superseded per-repository" : "anonymous project-specific";
11689
11776
  printWarn(` Note: this repository has a ${kind} credential that takes`);
11690
11777
  printWarn(" precedence here. Remove its line from ~/.verity/credentials to use your login.");
11778
+ printWarn(' Until then, "verity login" in this repository re-runs the full GitHub flow');
11779
+ printWarn(" every time.");
11691
11780
  }
11692
11781
  }
11693
11782
  }
@@ -11809,6 +11898,177 @@ function registerTokenCommand(program2) {
11809
11898
  });
11810
11899
  }
11811
11900
 
11901
+ // src/commands/sessions.ts
11902
+ function shortDate(iso) {
11903
+ return iso ? iso.slice(0, 10) : "\u2014";
11904
+ }
11905
+ function daysUntil(iso) {
11906
+ if (!iso) return null;
11907
+ const ms = Date.parse(iso);
11908
+ if (Number.isNaN(ms)) return null;
11909
+ return Math.round((ms - Date.now()) / 864e5);
11910
+ }
11911
+ async function auth(globals) {
11912
+ const tokenResult = await resolveToken(globals.token);
11913
+ if (!tokenResult.ok) {
11914
+ printError(tokenResult.error);
11915
+ process.exit(1);
11916
+ }
11917
+ const urlResult = await resolveServiceUrl(globals.serviceUrl);
11918
+ if (!urlResult.ok) {
11919
+ printError(urlResult.error);
11920
+ process.exit(1);
11921
+ }
11922
+ return { token: tokenResult.data.token, serviceUrl: urlResult.data };
11923
+ }
11924
+ function explain(error) {
11925
+ if (error.startsWith("FORBIDDEN")) {
11926
+ printInfo(' Sessions belong to a logged-in account \u2014 run "verity login" first.');
11927
+ } else if (error.startsWith("INVALID_TOKEN")) {
11928
+ printInfo(' This login has expired or was revoked \u2014 run "verity login" to sign in again.');
11929
+ }
11930
+ }
11931
+ function registerSessionsCommands(program2) {
11932
+ const sessions = program2.command("sessions").description("List and revoke your Verity logins (one per machine)");
11933
+ sessions.command("list").description("List your active logins \u2014 device, last use, and expiry").option("--json", "Output raw JSON").action(async (opts) => {
11934
+ const globals = program2.opts();
11935
+ const { token, serviceUrl } = await auth(globals);
11936
+ const result = await apiRequest({
11937
+ method: "GET",
11938
+ path: "/auth/sessions",
11939
+ serviceUrl,
11940
+ token,
11941
+ verbose: globals.verbose,
11942
+ cmd: "sessions"
11943
+ });
11944
+ if (!result.ok) {
11945
+ printError(result.error);
11946
+ explain(result.error);
11947
+ process.exit(1);
11948
+ }
11949
+ if (opts.json) {
11950
+ printJson(result.data);
11951
+ return;
11952
+ }
11953
+ const list = result.data.sessions;
11954
+ if (list.length === 0) {
11955
+ printInfo('No active logins. (Run "verity login".)');
11956
+ return;
11957
+ }
11958
+ printInfo(`${list.length} active login${list.length === 1 ? "" : "s"}:`);
11959
+ printInfo("");
11960
+ printInfo(`${"SESSION ID".padEnd(38)}${"DEVICE".padEnd(24)}${"CREATED".padEnd(12)}${"LAST USED".padEnd(12)}EXPIRES`);
11961
+ for (const s of list) {
11962
+ const days = daysUntil(s.expires_at);
11963
+ const expiry = s.expires_at ? `${shortDate(s.expires_at)}${days != null ? ` (${days}d)` : ""}` : "never";
11964
+ const device = (s.device ?? "login").slice(0, 22);
11965
+ printInfo(
11966
+ `${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" : ""}`
11967
+ );
11968
+ }
11969
+ printInfo("");
11970
+ printInfo("Revoke one: verity sessions revoke <session-id>");
11971
+ printInfo("Sign out everywhere else: verity logout --others");
11972
+ });
11973
+ sessions.command("revoke <session-id>").description("Revoke one login. Revoking this machine's also clears the local credential").action(async (sessionId) => {
11974
+ const globals = program2.opts();
11975
+ const { token, serviceUrl } = await auth(globals);
11976
+ const result = await apiRequest({
11977
+ method: "DELETE",
11978
+ path: `/auth/sessions/${encodeURIComponent(sessionId)}`,
11979
+ serviceUrl,
11980
+ token,
11981
+ verbose: globals.verbose,
11982
+ cmd: "sessions-revoke"
11983
+ });
11984
+ if (!result.ok) {
11985
+ printError(result.error);
11986
+ if (result.http_status === 404) {
11987
+ printInfo(' No session with that id on your account \u2014 check "verity sessions list".');
11988
+ }
11989
+ explain(result.error);
11990
+ process.exit(1);
11991
+ }
11992
+ printInfo(`Session ${sessionId} revoked. \u2713`);
11993
+ if (result.data.was_current) {
11994
+ const cleared = await removeGlobalCredential("");
11995
+ printInfo(cleared ? ' That was this machine \u2014 local credential cleared. Run "verity login" to sign back in.' : ' That was this machine. Run "verity login" to sign back in.');
11996
+ }
11997
+ });
11998
+ }
11999
+ function registerLogoutCommand(program2) {
12000
+ 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) => {
12001
+ const globals = program2.opts();
12002
+ if (opts.all && opts.others) {
12003
+ printError("Use either --all or --others, not both.");
12004
+ process.exit(1);
12005
+ }
12006
+ const { token, serviceUrl } = await auth(globals);
12007
+ if (opts.all || opts.others) {
12008
+ const result = await apiRequest({
12009
+ method: "DELETE",
12010
+ path: opts.others ? "/auth/sessions?others=true" : "/auth/sessions",
12011
+ serviceUrl,
12012
+ token,
12013
+ verbose: globals.verbose,
12014
+ cmd: "logout"
12015
+ });
12016
+ if (!result.ok) {
12017
+ printError(result.error);
12018
+ explain(result.error);
12019
+ process.exit(1);
12020
+ }
12021
+ const n = result.data.revoked;
12022
+ printInfo(`Revoked ${n} login${n === 1 ? "" : "s"}. \u2713`);
12023
+ if (opts.others) {
12024
+ printInfo(" This machine is still signed in.");
12025
+ return;
12026
+ }
12027
+ const cleared2 = await removeGlobalCredential("");
12028
+ if (cleared2) printInfo(" Local credential cleared.");
12029
+ printInfo(' Run "verity login" to sign back in.');
12030
+ return;
12031
+ }
12032
+ const list = await apiRequest({
12033
+ method: "GET",
12034
+ path: "/auth/sessions",
12035
+ serviceUrl,
12036
+ token,
12037
+ verbose: globals.verbose,
12038
+ cmd: "logout"
12039
+ });
12040
+ if (!list.ok) {
12041
+ printError(list.error);
12042
+ explain(list.error);
12043
+ process.exit(1);
12044
+ }
12045
+ const current = list.data.sessions.find((s) => s.current);
12046
+ if (!current) {
12047
+ printWarn("This machine is not signed in with a Verity login.");
12048
+ const cleared2 = await removeGlobalCredential("");
12049
+ if (cleared2) printInfo(" Cleared the local login credential anyway.");
12050
+ return;
12051
+ }
12052
+ const revoked = await apiRequest({
12053
+ method: "DELETE",
12054
+ path: `/auth/sessions/${current.id}`,
12055
+ serviceUrl,
12056
+ token,
12057
+ verbose: globals.verbose,
12058
+ cmd: "logout"
12059
+ });
12060
+ if (!revoked.ok) {
12061
+ printError(revoked.error);
12062
+ explain(revoked.error);
12063
+ process.exit(1);
12064
+ }
12065
+ const cleared = await removeGlobalCredential("");
12066
+ printInfo("Signed out on this machine. \u2713");
12067
+ if (cleared) printInfo(" Local credential cleared.");
12068
+ printInfo(' Your other machines are unaffected \u2014 use "verity logout --all" for all of them.');
12069
+ });
12070
+ }
12071
+
11812
12072
  // src/lib/hooks.ts
11813
12073
  var import_promises4 = require("node:fs/promises");
11814
12074
  var import_node_path5 = require("node:path");
@@ -12430,7 +12690,7 @@ function getRecentCommitMessages() {
12430
12690
  // src/lib/context-identity.ts
12431
12691
  var import_node_crypto2 = require("node:crypto");
12432
12692
  var import_node_fs5 = require("node:fs");
12433
- var import_node_os = require("node:os");
12693
+ var import_node_os2 = require("node:os");
12434
12694
  var import_node_path6 = require("node:path");
12435
12695
  var SHARED_SENTINELS = /* @__PURE__ */ new Set([
12436
12696
  "",
@@ -12485,7 +12745,7 @@ function contextIdentity(input) {
12485
12745
  }
12486
12746
  function verityHome() {
12487
12747
  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");
12748
+ return override && override.trim() ? (0, import_node_path6.resolve)(override) : (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".verity");
12489
12749
  }
12490
12750
  function dossierDir(identity) {
12491
12751
  return (0, import_node_path6.join)(verityHome(), "sessions", identity.userKey, identity.treeKey, identity.sessionKey);
@@ -13700,6 +13960,12 @@ function reduce(state, events, now) {
13700
13960
  }
13701
13961
  case "verdict": {
13702
13962
  state.meta.last_verdict_seq = ev.seq;
13963
+ state.meta.channel = {
13964
+ emittedLast: ev.emitted === true,
13965
+ // Reset by ANY movement, so the counter measures a standstill rather
13966
+ // than session length.
13967
+ consecutiveIdle: ev.idle === false ? 0 : (state.meta.channel?.consecutiveIdle ?? 0) + 1
13968
+ };
13703
13969
  state.meta.last_adjudication = ev.intent_verdict ? { verdict: ev.intent_verdict, score: ev.intent_score ?? null, at: ev.at, decision: ev.decision } : void 0;
13704
13970
  if (ev.intent_sig) {
13705
13971
  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 };
@@ -14067,6 +14333,10 @@ function projectMemory(state, opts) {
14067
14333
  ...active.delivered && { delivered: active.delivered }
14068
14334
  };
14069
14335
  }
14336
+ if (opts.capture) {
14337
+ const missed = Math.max(0, opts.capture.seen - opts.capture.captured);
14338
+ p.capture = { seen: opts.capture.seen, captured: opts.capture.captured, missed };
14339
+ }
14070
14340
  if (state.meta.last_adjudication) {
14071
14341
  const a = state.meta.last_adjudication;
14072
14342
  p.last_adjudication = { verdict: a.verdict, score: a.score, at: a.at };
@@ -14313,7 +14583,8 @@ function recall(d, input) {
14313
14583
  continuity,
14314
14584
  spoken: reanchored.spoken,
14315
14585
  refused: reanchored.dropped.length,
14316
- lastVerdictSeq
14586
+ lastVerdictSeq,
14587
+ ...input.capture && { capture: input.capture }
14317
14588
  });
14318
14589
  return {
14319
14590
  state: effective,
@@ -14527,6 +14798,11 @@ function recordVerdict(d, v) {
14527
14798
  line_sha: at !== void 0 ? lineSha(at) : null
14528
14799
  });
14529
14800
  }
14801
+ const foldedNow = foldDossier(d);
14802
+ const sig = intentSignature(v.intent, {
14803
+ goalSeq: foldedNow.goal.find((g) => g.status === "active")?.seq ?? 0,
14804
+ idle: v.idle !== false
14805
+ });
14530
14806
  appendEvent(d, {
14531
14807
  k: "verdict",
14532
14808
  run_id: v.runId,
@@ -14534,15 +14810,19 @@ function recordVerdict(d, v) {
14534
14810
  watermark_sha: v.watermarkSha,
14535
14811
  branch: v.branch,
14536
14812
  decision: v.decision,
14537
- ...intentSignature(v.intent) && { intent_sig: intentSignature(v.intent) },
14813
+ ...sig && { intent_sig: sig },
14814
+ emitted: v.emitted === true,
14815
+ idle: v.idle !== false,
14538
14816
  ...v.intent?.verdict && { intent_verdict: v.intent.verdict },
14539
14817
  ...typeof v.intent?.score === "number" && { intent_score: v.intent.score }
14540
14818
  });
14541
14819
  }
14542
- function intentSignature(intent) {
14820
+ function intentSignature(intent, ctx) {
14543
14821
  if (!intent?.verdict) return null;
14544
14822
  if (intent.verdict !== "misaligned" && intent.verdict !== "partial") return null;
14545
- return `${intent.verdict}:${lineSha(intent.gaps?.[0] ?? "")}`;
14823
+ const goal = ctx ? `g${ctx.goalSeq}` : "g?";
14824
+ const moved = ctx?.idle === false ? "active" : "idle";
14825
+ return `${intent.verdict}:${goal}:${moved}`;
14546
14826
  }
14547
14827
  function toRegister(severity) {
14548
14828
  switch (severity) {
@@ -14579,7 +14859,9 @@ function recallMemory(d, identity, opts) {
14579
14859
  const state = foldDossier(d);
14580
14860
  const watermark = state.meta.watermark?.sha ?? null;
14581
14861
  const watermarkPaths = (state.authored ?? []).map((a) => a.path);
14862
+ const captureCmp = typeof opts.userMessagesSeen === "number" ? { seen: opts.userMessagesSeen, captured: state.meta.goal_chain } : void 0;
14582
14863
  const r = recall(d, {
14864
+ ...captureCmp && { capture: captureCmp },
14583
14865
  identity,
14584
14866
  currentSessionKey: opts.currentSessionKey,
14585
14867
  branchNow: getCurrentBranch(),
@@ -14836,6 +15118,8 @@ function collectCodeDelta(files, opts) {
14836
15118
  let totalSize = 0;
14837
15119
  let truncationReason = null;
14838
15120
  const droppedPaths = [];
15121
+ const excluded = [];
15122
+ const exclude = (path, reason) => excluded.push({ path, reason, stage: "collectCodeDelta", kind: "capacity" });
14839
15123
  for (const filepath of sorted) {
14840
15124
  if (result.length >= maxFiles) {
14841
15125
  truncationReason ??= "max_files";
@@ -14843,14 +15127,21 @@ function collectCodeDelta(files, opts) {
14843
15127
  continue;
14844
15128
  }
14845
15129
  const resolved = resolveFile(filepath);
14846
- if (!resolved) continue;
15130
+ if (!resolved) {
15131
+ exclude(filepath, "path-not-resolvable");
15132
+ continue;
15133
+ }
14847
15134
  let size;
14848
15135
  try {
14849
15136
  size = (0, import_node_fs11.statSync)(resolved).size;
14850
15137
  } catch {
15138
+ exclude(filepath, "not-stattable");
15139
+ continue;
15140
+ }
15141
+ if (size > maxFileBytes) {
15142
+ exclude(filepath, `over-file-size-limit-${maxFileBytes}b`);
14851
15143
  continue;
14852
15144
  }
14853
- if (size > maxFileBytes) continue;
14854
15145
  if (totalSize + size > maxTotalBytes) {
14855
15146
  truncationReason ??= "max_total_bytes";
14856
15147
  const idx = sorted.indexOf(filepath);
@@ -14861,6 +15152,7 @@ function collectCodeDelta(files, opts) {
14861
15152
  try {
14862
15153
  content = (0, import_node_fs11.readFileSync)(resolved, "utf-8");
14863
15154
  } catch {
15155
+ exclude(filepath, "not-readable");
14864
15156
  continue;
14865
15157
  }
14866
15158
  totalSize += size;
@@ -14874,10 +15166,14 @@ function collectCodeDelta(files, opts) {
14874
15166
  (sum, f) => sum + f.content.split("\n").length,
14875
15167
  0
14876
15168
  );
15169
+ for (const path of droppedPaths) {
15170
+ exclude(path, truncationReason === "max_files" ? "max-files-cap" : "max-total-bytes-cap");
15171
+ }
14877
15172
  return {
14878
15173
  files: result,
14879
15174
  total_lines: totalLines,
14880
15175
  total_files: result.length,
15176
+ excluded,
14881
15177
  ...truncationReason && {
14882
15178
  truncated: {
14883
15179
  reason: truncationReason,
@@ -16329,7 +16625,7 @@ function resolveTaskContext(opts) {
16329
16625
  // src/lib/cli-version.ts
16330
16626
  function cliVersion() {
16331
16627
  try {
16332
- return true ? "0.28.1-experimental.5ec7373" : "dev";
16628
+ return true ? "0.28.1-experimental.8cc9c66" : "dev";
16333
16629
  } catch {
16334
16630
  return "dev";
16335
16631
  }
@@ -16554,6 +16850,16 @@ var KNOWN_RECORD_TYPES = /* @__PURE__ */ new Set([
16554
16850
  "subagent"
16555
16851
  ]);
16556
16852
  var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "NotebookEdit", "MultiEdit"]);
16853
+ var DISPATCH_TOOLS = /* @__PURE__ */ new Set(["Task", "Workflow"]);
16854
+ function hasUserText(record) {
16855
+ const message = record.message;
16856
+ const content = message?.content ?? record.content;
16857
+ if (typeof content === "string") return content.trim().length > 0;
16858
+ if (!Array.isArray(content)) return false;
16859
+ return content.some(
16860
+ (b) => b?.type === "text" && typeof b.text === "string" && b.text.trim().length > 0
16861
+ );
16862
+ }
16557
16863
  var COMMAND_CLASSES = [
16558
16864
  [/\b(npm|yarn|pnpm|bun)\s+(run\s+)?test\b|\bvitest\b|\bjest\b|\bpytest\b|\bgo test\b/, "test"],
16559
16865
  [/\b(npm|yarn|pnpm|bun)\s+(run\s+)?build\b|\btsc\b|\bwebpack\b|\bcargo build\b/, "build"],
@@ -16680,6 +16986,8 @@ function fold(transcriptPath, opts = {}) {
16680
16986
  totalRecords: 0,
16681
16987
  malformed: 0,
16682
16988
  subagentFiles: 0,
16989
+ dispatched: 0,
16990
+ userMessages: 0,
16683
16991
  subagentSkipped: 0,
16684
16992
  compactions: 0,
16685
16993
  complete: false
@@ -16706,7 +17014,8 @@ function fold(transcriptPath, opts = {}) {
16706
17014
  if (type === "system" && record.subtype === "compact_boundary") {
16707
17015
  result.coverage.compactions++;
16708
17016
  }
16709
- collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, opts.repoRoot);
17017
+ if (type === "user" && hasUserText(record)) result.coverage.userMessages++;
17018
+ collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, opts.repoRoot, result.coverage);
16710
17019
  }
16711
17020
  };
16712
17021
  try {
@@ -16778,7 +17087,7 @@ function classifyUnobserved(path) {
16778
17087
  if (/\.(png|jpg|jpeg|gif|pdf|zip|woff2?|ico|mp4)$/i.test(path)) return "binary";
16779
17088
  return "no_edit_record";
16780
17089
  }
16781
- function collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, repoRoot2) {
17090
+ function collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, repoRoot2, tally) {
16782
17091
  const message = record.message;
16783
17092
  const content = message?.content ?? record.content;
16784
17093
  const blocks = Array.isArray(content) ? content : content && typeof content === "object" ? [content] : [];
@@ -16800,6 +17109,9 @@ function collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse
16800
17109
  byPath.set(path, entry);
16801
17110
  }
16802
17111
  }
17112
+ if (DISPATCH_TOOLS.has(name) && tally) {
17113
+ tally.dispatched += 1;
17114
+ }
16803
17115
  if (name === "Bash") {
16804
17116
  const cmd = typeof input.command === "string" ? input.command : "";
16805
17117
  if (cmd) {
@@ -16858,6 +17170,59 @@ function checkConservation(changedFiles, result, repoRoot2) {
16858
17170
  };
16859
17171
  }
16860
17172
 
17173
+ // src/lib/verdict.ts
17174
+ function reconcileCoverage(changed, coverage) {
17175
+ const changedSet = new Set(changed);
17176
+ const reviewed = coverage.reviewed.filter((p) => changedSet.has(p));
17177
+ const claimed = /* @__PURE__ */ new Set([...reviewed, ...coverage.notReviewed.map((n) => n.path)]);
17178
+ const unaccounted = [...changedSet].filter((p) => !claimed.has(p)).sort();
17179
+ const notReviewed = [
17180
+ ...coverage.notReviewed.filter((n) => changedSet.has(n.path)),
17181
+ ...unaccounted.map((path) => ({
17182
+ path,
17183
+ reason: "unaccounted",
17184
+ // Named so the eventual bug report writes itself: some stage removed this
17185
+ // path and did not say so.
17186
+ stage: "unknown-stage",
17187
+ // An undeclared drop is CAPACITY by default. A stage that cannot be
17188
+ // bothered to say why it dropped a file does not get the benefit of the
17189
+ // doubt — that default is what makes forgetting expensive.
17190
+ kind: "capacity"
17191
+ }))
17192
+ ];
17193
+ return {
17194
+ coverage: { reviewed: [...new Set(reviewed)].sort(), notReviewed },
17195
+ unaccounted,
17196
+ balances: unaccounted.length === 0
17197
+ };
17198
+ }
17199
+ function resolveVerdict(proposed, coverage) {
17200
+ if (proposed === "FAIL") return "FAIL";
17201
+ const blocking = coverage.notReviewed.filter((n) => (n.kind ?? "capacity") !== "policy");
17202
+ if (blocking.length === 0) return proposed;
17203
+ return "WARN";
17204
+ }
17205
+ function describeCoverage(coverage, maxPaths = 5) {
17206
+ const relevant = coverage.notReviewed.filter((n) => (n.kind ?? "capacity") !== "policy");
17207
+ if (relevant.length === 0) return null;
17208
+ const byReason = /* @__PURE__ */ new Map();
17209
+ for (const n of relevant) {
17210
+ const key = `${n.reason}`;
17211
+ const list = byReason.get(key) ?? [];
17212
+ list.push(n.path);
17213
+ byReason.set(key, list);
17214
+ }
17215
+ const lines = [];
17216
+ for (const [reason, paths] of [...byReason.entries()].sort()) {
17217
+ const shown = paths.slice(0, maxPaths).join(", ");
17218
+ const more = paths.length > maxPaths ? ` (+${paths.length - maxPaths} more)` : "";
17219
+ lines.push(` ${paths.length} not reviewed \u2014 ${reason}: ${shown}${more}`);
17220
+ }
17221
+ return `NOT A CLEAN REVIEW. ${relevant.length} changed file(s) never reached the reviewer, so this verdict does not cover them:
17222
+ ${lines.join("\n")}
17223
+ Treat those files as UNCHECKED, not as approved.`;
17224
+ }
17225
+
16861
17226
  // src/lib/channel.ts
16862
17227
  var MAX_AGENT_CONTEXT_CHARS = 1500;
16863
17228
  var MAX_AGENT_ITEMS = 5;
@@ -16936,6 +17301,45 @@ function buildHookOutput(gateDecision, systemMessage, agentContext) {
16936
17301
  } : {}
16937
17302
  };
16938
17303
  }
17304
+ var IDLE_EPISODE_CAP = 3;
17305
+ function channelSilence(input) {
17306
+ const movedSomething = input.newUserPrompt || input.newAuthorship;
17307
+ if (movedSomething) return null;
17308
+ if (input.consecutiveIdle >= IDLE_EPISODE_CAP) return "idle-episode-cap";
17309
+ if (input.emittedLast) return "caused-by-our-own-emission";
17310
+ return null;
17311
+ }
17312
+
17313
+ // src/lib/emit.ts
17314
+ var YELLOW2 = "\x1B[33m";
17315
+ var NC2 = "\x1B[0m";
17316
+ function emitVerdict(input) {
17317
+ const exit = input.exit ?? ((code) => process.exit(code));
17318
+ const { coverage, unaccounted } = reconcileCoverage(input.changed, input.coverage);
17319
+ const verdict = resolveVerdict(input.proposed, coverage);
17320
+ const note = describeCoverage(coverage);
17321
+ if (unaccounted.length > 0) {
17322
+ process.stderr.write(
17323
+ `${YELLOW2}Verity: ${unaccounted.length} changed file(s) could not be attributed to any review stage \u2014 counted as unreviewed.${NC2}
17324
+ `
17325
+ );
17326
+ }
17327
+ if (verdict === "FAIL") {
17328
+ input.renderBlocking?.();
17329
+ if (input.agentContext) {
17330
+ process.stderr.write(`
17331
+ ${input.agentContext}
17332
+ `);
17333
+ }
17334
+ if (note) process.stderr.write(`
17335
+ ${YELLOW2}${note}${NC2}
17336
+ `);
17337
+ return exit(2);
17338
+ }
17339
+ const agentBlock = [input.agentContext, note].filter(Boolean).join("\n\n") || null;
17340
+ printJsonCompact(buildHookOutput(verdict, input.userSummary, agentBlock));
17341
+ return exit(0);
17342
+ }
16939
17343
 
16940
17344
  // src/lib/cache-cleanup.ts
16941
17345
  var import_node_fs21 = require("node:fs");
@@ -17319,6 +17723,13 @@ function buildSummary(lines) {
17319
17723
  files_read: capArray(filesRead, MAX_FILES_LIST),
17320
17724
  files_edited: capArray(filesEdited, MAX_FILES_LIST),
17321
17725
  files_created: capArray(filesCreated, MAX_CREATED_LIST),
17726
+ // The complement of the two caps that affect SCOPE. `files_read` is excluded
17727
+ // deliberately: reading a file is not authoring it, so a capped read list
17728
+ // narrows nothing.
17729
+ capped_out: [
17730
+ ...cappedOut(filesEdited, MAX_FILES_LIST),
17731
+ ...cappedOut(filesCreated, MAX_CREATED_LIST)
17732
+ ],
17322
17733
  searches,
17323
17734
  commands,
17324
17735
  subagents,
@@ -17369,6 +17780,9 @@ function sanitizeCommand(rawCmd) {
17369
17780
  function capArray(set, max) {
17370
17781
  return Array.from(set).slice(0, max);
17371
17782
  }
17783
+ function cappedOut(set, max) {
17784
+ return Array.from(set).slice(max);
17785
+ }
17372
17786
 
17373
17787
  // src/lib/run-mode.ts
17374
17788
  function parseAutonomousEnv(raw) {
@@ -17778,12 +18192,43 @@ function agentContextFor(response, intentRepeat = 0) {
17778
18192
  });
17779
18193
  }
17780
18194
  var beaconCtx = null;
17781
- async function passAndExit(reason, skip) {
18195
+ async function passAndExit(reason, skip, kindOverride) {
17782
18196
  const sent = await sendSkipBeacon(beaconCtx, skip);
17783
18197
  logEvent("skip", { reason: skip, beacon: sent });
17784
- printJsonCompact({ gate_decision: "PASS", systemMessage: `Verity: ${reason}` });
18198
+ const POLICY_SKIPS = /* @__PURE__ */ new Set([
18199
+ "no-analyzable-files",
18200
+ "verity-command",
18201
+ "bare-acknowledgment",
18202
+ "reflection-prompt",
18203
+ "skip-mode",
18204
+ "zero-increment",
18205
+ "debounce",
18206
+ "no-delta-since-last-review"
18207
+ ]);
18208
+ const skipKind = kindOverride ?? (POLICY_SKIPS.has(skip) ? "policy" : "capacity");
18209
+ const changed = skipCoverageChanged;
18210
+ const { coverage, unaccounted } = reconcileCoverage(changed, {
18211
+ reviewed: [],
18212
+ notReviewed: changed.map((path) => ({ path, reason: skip, stage: "pre-flight", kind: skipKind }))
18213
+ });
18214
+ const verdict = resolveVerdict("PASS", coverage);
18215
+ const note = describeCoverage(coverage);
18216
+ if (unaccounted.length > 0) {
18217
+ logEvent("coverage_unaccounted", { where: "passAndExit", skip, count: unaccounted.length });
18218
+ }
18219
+ printJsonCompact(
18220
+ buildHookOutput(
18221
+ verdict,
18222
+ `Verity: ${reason}`,
18223
+ // The agent's ONLY input is additionalContext. Sixteen of the nineteen
18224
+ // terminating paths wrote `systemMessage` — the human's field — and told
18225
+ // the agent nothing at all.
18226
+ note
18227
+ )
18228
+ );
17785
18229
  process.exit(0);
17786
18230
  }
18231
+ var skipCoverageChanged = [];
17787
18232
  var EMPTY_STATIC = {
17788
18233
  tool: "@codacy/analysis-cli",
17789
18234
  findings: [],
@@ -17798,7 +18243,7 @@ function runLocalStatic(analyzable, securityFiles, baseline, skipStatic) {
17798
18243
  }
17799
18244
  function localOnlyAndExit(staticResults) {
17800
18245
  printJsonCompact({
17801
- gate_decision: "PASS",
18246
+ gate_decision: "WARN",
17802
18247
  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
18248
  unauthenticated: true,
17804
18249
  static_results: staticResults
@@ -17858,6 +18303,7 @@ async function runAnalyze(opts, globals) {
17858
18303
  });
17859
18304
  }
17860
18305
  const { files: allChanged, hasRecentCommitFiles } = getChangedFiles();
18306
+ skipCoverageChanged = allChanged;
17861
18307
  const analyzable = filterAnalyzable(allChanged);
17862
18308
  const reviewable = filterReviewable(allChanged);
17863
18309
  const securityFiles = filterSecurity(allChanged);
@@ -17933,7 +18379,8 @@ async function runAnalyze(opts, globals) {
17933
18379
  let codeDelta = {
17934
18380
  files: [],
17935
18381
  total_lines: 0,
17936
- total_files: 0
18382
+ total_files: 0,
18383
+ excluded: []
17937
18384
  };
17938
18385
  let snapshotResult = { has_snapshots: false, diffs: [] };
17939
18386
  let contentHash = null;
@@ -17998,7 +18445,11 @@ async function runAnalyze(opts, globals) {
17998
18445
  if (assistantResponse) {
17999
18446
  analysisMode = "plan";
18000
18447
  } else {
18001
- await passAndExit("No files within size limits to analyze", "size-limit");
18448
+ await passAndExit(
18449
+ "No files within size limits to analyze",
18450
+ "size-limit",
18451
+ codeDelta.excluded.length > 0 ? "capacity" : "policy"
18452
+ );
18002
18453
  }
18003
18454
  }
18004
18455
  }
@@ -18172,7 +18623,10 @@ async function runAnalyze(opts, globals) {
18172
18623
  priorState.capabilities
18173
18624
  );
18174
18625
  memory = recallMemory(memorySession.d, memorySession.identity, {
18175
- currentSessionKey: memorySession.identity.sessionKey
18626
+ currentSessionKey: memorySession.identity.sessionKey,
18627
+ // The independent witness. Only meaningful when a transcript was folded —
18628
+ // otherwise it stays undefined and capture coverage reads as UNKNOWN.
18629
+ ...foldResult && { userMessagesSeen: foldResult.coverage.userMessages }
18176
18630
  });
18177
18631
  if (memory && !memory.provenanceHolds) {
18178
18632
  process.stderr.write("Verity: working-memory provenance check failed \u2014 recall suppressed.\n");
@@ -18387,6 +18841,8 @@ async function runAnalyze(opts, globals) {
18387
18841
  message = "Verity: your GitHub verification expired \u2014 run `verity login` to re-verify (local analysis this run)";
18388
18842
  } else if (result.error.startsWith("FORBIDDEN")) {
18389
18843
  message = "Verity: no access grant for this repository \u2014 run `verity login` to refresh your grants (local analysis this run)";
18844
+ } else if (result.error.startsWith("INVALID_TOKEN")) {
18845
+ message = "Verity: your login expired or was revoked \u2014 run `verity login` to sign in again (local analysis this run)";
18390
18846
  } else if (result.http_status && result.http_status >= 400 && result.http_status < 500) {
18391
18847
  message = `Verity: request rejected (HTTP ${result.http_status}) \u2014 ${result.error}`;
18392
18848
  } else if (result.http_status && result.http_status >= 500) {
@@ -18401,8 +18857,78 @@ async function runAnalyze(opts, globals) {
18401
18857
  const response = result.data;
18402
18858
  const decision = response.gate_decision ?? "(unrecognised)";
18403
18859
  const sentPaths = codeDelta.files.map((f) => f.path);
18860
+ const reviewCoverage = {
18861
+ reviewed: sentPaths,
18862
+ // Declared drops from the stages that DO report themselves today. The other
18863
+ // stages surface via `unaccounted`, which is the tripwire, not the design.
18864
+ notReviewed: [
18865
+ // Every exit from the collection loop, each named. Six reasons where there
18866
+ // used to be two recorded and four silent — the silent ones including the
18867
+ // per-file size cap, which could drop a whole source file without leaving a
18868
+ // trace anywhere in the payload or the run row.
18869
+ ...codeDelta.excluded,
18870
+ // The server-side 300-line middle-out truncation. It only bites on the
18871
+ // full-file branch (a first analysis, before snapshots exist) because
18872
+ // analyze normally sends diffs — but on that branch the reviewer sees the
18873
+ // first and last 100 lines and nothing between, and until now said so to
18874
+ // nobody. CAPACITY: a partial look is not a look.
18875
+ ...(response.metadata?.truncated_files ?? []).map((path) => ({
18876
+ path,
18877
+ reason: "file-middle-truncated-300-lines",
18878
+ stage: "prompt-builder",
18879
+ kind: "capacity"
18880
+ })),
18881
+ // The 20-entry edit cap. CAPACITY, and the sharpest of the lot: it narrows
18882
+ // what is REVIEWED, not merely what is summarised — a session editing 25
18883
+ // files had five silently excluded from the reviewed set.
18884
+ ...(actionSummary?.capped_out ?? []).map((path) => ({
18885
+ path,
18886
+ reason: "edit-list-cap-20",
18887
+ stage: "extractActionSummary",
18888
+ kind: "capacity"
18889
+ })),
18890
+ // The extension allowlist, and it is POLICY rather than capacity: a changed
18891
+ // README was never going to be reviewed, and treating that as a coverage
18892
+ // gap would downgrade nearly every PASS to WARN until WARN meant nothing.
18893
+ // Recorded so the ledger balances and so "what did Verity ignore entirely"
18894
+ // is answerable — but it never touches the verdict.
18895
+ ...allChanged.filter((p) => !analyzable.includes(p) && !reviewable.includes(p) && !securityFiles.includes(p)).map((path) => ({
18896
+ path,
18897
+ reason: "not-a-reviewed-file-type",
18898
+ stage: "extension-allowlist",
18899
+ kind: "policy"
18900
+ }))
18901
+ ]
18902
+ };
18404
18903
  const watermarkHash = sentPaths.length > 0 ? computeContentHash(sentPaths) : contentHash;
18405
18904
  const watermarkIsPartial = !!codeDelta.truncated;
18905
+ let silenced = null;
18906
+ let turnIsIdleForChannel = true;
18907
+ if (memorySession) {
18908
+ try {
18909
+ const st = foldDossier(memorySession.d);
18910
+ turnIsIdleForChannel = st.authored_all.every((a) => a.hash_now === a.hash_at_last_verdict);
18911
+ silenced = channelSilence({
18912
+ // The BUFFER, not intentContext.user_prompt: the latter falls back to a
18913
+ // linked issue (VRT-53 W4) when no human spoke, and a fallback goal is
18914
+ // not a user utterance. Treating it as one would keep the loop alive on
18915
+ // exactly the autonomous cohort.
18916
+ newUserPrompt: (conversation?.prompts?.length ?? 0) > 0,
18917
+ newAuthorship: !turnIsIdleForChannel,
18918
+ emittedLast: st.meta.channel?.emittedLast === true,
18919
+ consecutiveIdle: st.meta.channel?.consecutiveIdle ?? 0
18920
+ });
18921
+ } catch {
18922
+ silenced = null;
18923
+ }
18924
+ }
18925
+ if (silenced) {
18926
+ logEvent("channel_silenced", {
18927
+ reason: silenced,
18928
+ run_id: response.run_id ?? turnId,
18929
+ decision
18930
+ });
18931
+ }
18406
18932
  let intentRepeatCount = 0;
18407
18933
  if (memorySession) {
18408
18934
  try {
@@ -18418,7 +18944,15 @@ async function runAnalyze(opts, globals) {
18418
18944
  title: f.title,
18419
18945
  severity: f.severity
18420
18946
  })) ?? [],
18421
- intent: response.intent_alignment ?? null
18947
+ intent: response.intent_alignment ?? null,
18948
+ // The same signal F1 introduced: bytes differing from the hash frozen at
18949
+ // the last verdict. A turn that moved nothing is the only kind that can
18950
+ // accumulate a repeat.
18951
+ idle: turnIsIdleForChannel,
18952
+ // What next turn reads as `emittedLast`. A suppressed turn did not
18953
+ // speak, so it cannot be the cause of the turn after it — which is what
18954
+ // keeps this from becoming a permanent gag.
18955
+ emitted: !silenced
18422
18956
  });
18423
18957
  intentRepeatCount = Math.max(0, (foldDossier(memorySession.d).meta.intent_repeat?.consecutive ?? 1) - 1);
18424
18958
  } catch {
@@ -18597,7 +19131,16 @@ ${YELLOW}${loginNudge.trim()}${NC}
18597
19131
  if (grantNudge) process.stderr.write(`
18598
19132
  ${YELLOW}${grantNudge.trim()}${NC}
18599
19133
  `);
18600
- process.exit(2);
19134
+ emitVerdict({
19135
+ proposed: "FAIL",
19136
+ changed: skipCoverageChanged,
19137
+ coverage: reviewCoverage,
19138
+ userSummary: "",
19139
+ // Subject to the SAME cycle cut as PASS/WARN. Suppressing here is safe:
19140
+ // the findings themselves are rendered above by the blocking renderer,
19141
+ // so what the cut removes is the repeated commentary, never the defect.
19142
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount)
19143
+ });
18601
19144
  break;
18602
19145
  }
18603
19146
  case "PASS": {
@@ -18609,10 +19152,13 @@ ${YELLOW}${grantNudge.trim()}${NC}
18609
19152
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
18610
19153
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
18611
19154
  userSummary += loginNudge + grantNudge;
18612
- printJsonCompact(
18613
- buildHookOutput("PASS", userSummary, agentContextFor(response, intentRepeatCount))
18614
- );
18615
- process.exit(0);
19155
+ emitVerdict({
19156
+ proposed: "PASS",
19157
+ changed: skipCoverageChanged,
19158
+ coverage: reviewCoverage,
19159
+ userSummary,
19160
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount)
19161
+ });
18616
19162
  break;
18617
19163
  }
18618
19164
  case "WARN": {
@@ -18623,10 +19169,13 @@ ${YELLOW}${grantNudge.trim()}${NC}
18623
19169
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
18624
19170
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
18625
19171
  userSummary += loginNudge + grantNudge;
18626
- printJsonCompact(
18627
- buildHookOutput("WARN", userSummary, agentContextFor(response, intentRepeatCount))
18628
- );
18629
- process.exit(0);
19172
+ emitVerdict({
19173
+ proposed: "WARN",
19174
+ changed: skipCoverageChanged,
19175
+ coverage: reviewCoverage,
19176
+ userSummary,
19177
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount)
19178
+ });
18630
19179
  break;
18631
19180
  }
18632
19181
  default: {
@@ -19042,7 +19591,7 @@ async function runGuard(opts, globals) {
19042
19591
  cmd: "guard"
19043
19592
  });
19044
19593
  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." : "";
19594
+ 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
19595
  emitAllowNotice(
19047
19596
  `\u26A0 Verity ${moment}: ${authRemedy ? "not authorized" : "service offline"} \u2014 ${verb}ed WITHOUT review${authRemedy}`,
19048
19597
  `Verity ${moment}: ${authRemedy ? "not authorized" : "service unavailable"} (${result.error}); the ${verb} was allowed WITHOUT a Verity review.${authRemedy}`
@@ -20426,7 +20975,7 @@ function registerTelemetryCommands(program2) {
20426
20975
  }
20427
20976
 
20428
20977
  // 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 () => {
20978
+ program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.8cc9c66").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async () => {
20430
20979
  try {
20431
20980
  await foldLegacyLocalCredential();
20432
20981
  } catch {
@@ -20435,6 +20984,8 @@ program.name("verity").description("CLI for Verity quality gate service").versio
20435
20984
  registerAuthCommands(program);
20436
20985
  registerLoginCommand(program);
20437
20986
  registerTokenCommand(program);
20987
+ registerSessionsCommands(program);
20988
+ registerLogoutCommand(program);
20438
20989
  registerHooksCommands(program);
20439
20990
  registerIntentCommands(program);
20440
20991
  registerLifecycleCommands(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codacy/verity-cli",
3
- "version": "0.28.1-experimental.5ec7373",
3
+ "version": "0.28.1-experimental.8cc9c66",
4
4
  "description": "CLI for Verity quality gate service",
5
5
  "homepage": "https://verity.md",
6
6
  "bugs": {