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

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 +429 -30
  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);
@@ -13579,6 +13839,13 @@ function reduce(state, events, now) {
13579
13839
  });
13580
13840
  break;
13581
13841
  }
13842
+ case "goal_delivered": {
13843
+ const g = state.goal.find((x) => x.status === "active");
13844
+ if (!g) break;
13845
+ if (g.delivered) break;
13846
+ g.delivered = { at: ev.at, seq: ev.seq, summary: ev.summary };
13847
+ break;
13848
+ }
13582
13849
  case "authored": {
13583
13850
  authoredEvents++;
13584
13851
  const e = byPath.get(ev.path) ?? {
@@ -13693,6 +13960,18 @@ function reduce(state, events, now) {
13693
13960
  }
13694
13961
  case "verdict": {
13695
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
+ };
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;
13970
+ if (ev.intent_sig) {
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 };
13972
+ } else {
13973
+ state.meta.intent_repeat = void 0;
13974
+ }
13696
13975
  state.meta.watermark = {
13697
13976
  sha: ev.head_sha,
13698
13977
  reviewed_hash: ev.watermark_sha,
@@ -13778,7 +14057,12 @@ function compactState(s) {
13778
14057
  const ms = (iso) => Date.parse(iso) || 0;
13779
14058
  return {
13780
14059
  v: 1,
13781
- g: s.goal.map((g) => [g.seq, ms(g.at), g.hash, g.superseded_by, g.text ?? 0, g.text_len ?? 0, g.source ?? 0, g.status ?? 0, g.repeats ?? 0, g.truncated ? 1 : 0, g.collapsed ? 1 : 0]),
14060
+ // APPEND-ONLY POSITIONALLY. Indices 11-13 carry `delivered`; a cache row
14061
+ // written before it existed has length 11 and expands with `delivered`
14062
+ // absent, which is the correct reading of "nothing had been delivered yet".
14063
+ // Inserting rather than appending would silently re-interpret every existing
14064
+ // cached row.
14065
+ g: s.goal.map((g) => [g.seq, ms(g.at), g.hash, g.superseded_by, g.text ?? 0, g.text_len ?? 0, g.source ?? 0, g.status ?? 0, g.repeats ?? 0, g.truncated ? 1 : 0, g.collapsed ? 1 : 0, g.delivered ? ms(g.delivered.at) : 0, g.delivered?.seq ?? 0, g.delivered?.summary ?? 0]),
13782
14066
  a: s.authored?.map((a) => [a.path, a.origin, a.edits, a.hunks, a.adds, a.dels, a.hash_now, a.hash_at_last_verdict, a.first_seq, a.last_seq]) ?? null,
13783
14067
  n: s.not_mine?.map((n) => [n.path, n.reason, ms(n.at), n.head_sha]) ?? null,
13784
14068
  t: s.statements.map((x) => [x.anchor_key, x.file, x.line, x.pattern_id, x.title_hash, x.register, x.line_sha, x.said_at_seq, ms(x.said_at), x.outcome, x.outcome_at ? ms(x.outcome_at) : 0, x.repeats, x.carried ? 1 : 0]),
@@ -13824,7 +14108,14 @@ function expandState(raw) {
13824
14108
  ...x[7] ? { status: x[7] } : {},
13825
14109
  ...x[8] ? { repeats: x[8] } : {},
13826
14110
  ...x[9] ? { truncated: true } : {},
13827
- ...x[10] ? { collapsed: true } : {}
14111
+ ...x[10] ? { collapsed: true } : {},
14112
+ ...x[11] ? {
14113
+ delivered: {
14114
+ at: iso(x[11]),
14115
+ seq: x[12] ?? 0,
14116
+ summary: x[13] || ""
14117
+ }
14118
+ } : {}
13828
14119
  })),
13829
14120
  authored: decodedAuthored,
13830
14121
  // The cache stores the BOUNDED list, so this is the bounded list too. That
@@ -14038,9 +14329,14 @@ function projectMemory(state, opts) {
14038
14329
  seq: active.seq,
14039
14330
  superseded,
14040
14331
  truncated: active.truncated === true,
14041
- collapsed: state.meta.collapsed.goal ?? 0
14332
+ collapsed: state.meta.collapsed.goal ?? 0,
14333
+ ...active.delivered && { delivered: active.delivered }
14042
14334
  };
14043
14335
  }
14336
+ if (state.meta.last_adjudication) {
14337
+ const a = state.meta.last_adjudication;
14338
+ p.last_adjudication = { verdict: a.verdict, score: a.score, at: a.at };
14339
+ }
14044
14340
  const ageOf = (seq, carried) => carried ? "carried" : seq > lastVerdictSeq ? "this_turn" : "this_session";
14045
14341
  if (opts.spoken.length > 0) {
14046
14342
  p.statements = opts.spoken.map((s) => ({
@@ -14466,6 +14762,10 @@ function toStatus(n) {
14466
14762
  }
14467
14763
  function recordVerdict(d, v) {
14468
14764
  const root = repoRoot();
14765
+ if (v.intent?.verdict === "aligned") {
14766
+ const summary = (v.intent.implemented ?? "").trim().slice(0, 400);
14767
+ if (summary) appendEvent(d, { k: "goal_delivered", summary });
14768
+ }
14469
14769
  const lines = /* @__PURE__ */ new Map();
14470
14770
  for (const f of v.findings) {
14471
14771
  if (!f.file || typeof f.line !== "number" || !f.pattern_id) continue;
@@ -14493,15 +14793,32 @@ function recordVerdict(d, v) {
14493
14793
  line_sha: at !== void 0 ? lineSha(at) : null
14494
14794
  });
14495
14795
  }
14796
+ const foldedNow = foldDossier(d);
14797
+ const sig = intentSignature(v.intent, {
14798
+ goalSeq: foldedNow.goal.find((g) => g.status === "active")?.seq ?? 0,
14799
+ idle: v.idle !== false
14800
+ });
14496
14801
  appendEvent(d, {
14497
14802
  k: "verdict",
14498
14803
  run_id: v.runId,
14499
14804
  head_sha: getCurrentCommit(),
14500
14805
  watermark_sha: v.watermarkSha,
14501
14806
  branch: v.branch,
14502
- decision: v.decision
14807
+ decision: v.decision,
14808
+ ...sig && { intent_sig: sig },
14809
+ emitted: v.emitted === true,
14810
+ idle: v.idle !== false,
14811
+ ...v.intent?.verdict && { intent_verdict: v.intent.verdict },
14812
+ ...typeof v.intent?.score === "number" && { intent_score: v.intent.score }
14503
14813
  });
14504
14814
  }
14815
+ function intentSignature(intent, ctx) {
14816
+ if (!intent?.verdict) return null;
14817
+ if (intent.verdict !== "misaligned" && intent.verdict !== "partial") return null;
14818
+ const goal = ctx ? `g${ctx.goalSeq}` : "g?";
14819
+ const moved = ctx?.idle === false ? "active" : "idle";
14820
+ return `${intent.verdict}:${goal}:${moved}`;
14821
+ }
14505
14822
  function toRegister(severity) {
14506
14823
  switch (severity) {
14507
14824
  case "critical":
@@ -16287,7 +16604,7 @@ function resolveTaskContext(opts) {
16287
16604
  // src/lib/cli-version.ts
16288
16605
  function cliVersion() {
16289
16606
  try {
16290
- return true ? "0.28.1-experimental.dbd87b1" : "dev";
16607
+ return true ? "0.28.1-experimental.dfb3ce6" : "dev";
16291
16608
  } catch {
16292
16609
  return "dev";
16293
16610
  }
@@ -16512,6 +16829,7 @@ var KNOWN_RECORD_TYPES = /* @__PURE__ */ new Set([
16512
16829
  "subagent"
16513
16830
  ]);
16514
16831
  var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "NotebookEdit", "MultiEdit"]);
16832
+ var DISPATCH_TOOLS = /* @__PURE__ */ new Set(["Task", "Workflow"]);
16515
16833
  var COMMAND_CLASSES = [
16516
16834
  [/\b(npm|yarn|pnpm|bun)\s+(run\s+)?test\b|\bvitest\b|\bjest\b|\bpytest\b|\bgo test\b/, "test"],
16517
16835
  [/\b(npm|yarn|pnpm|bun)\s+(run\s+)?build\b|\btsc\b|\bwebpack\b|\bcargo build\b/, "build"],
@@ -16638,6 +16956,7 @@ function fold(transcriptPath, opts = {}) {
16638
16956
  totalRecords: 0,
16639
16957
  malformed: 0,
16640
16958
  subagentFiles: 0,
16959
+ dispatched: 0,
16641
16960
  subagentSkipped: 0,
16642
16961
  compactions: 0,
16643
16962
  complete: false
@@ -16664,7 +16983,7 @@ function fold(transcriptPath, opts = {}) {
16664
16983
  if (type === "system" && record.subtype === "compact_boundary") {
16665
16984
  result.coverage.compactions++;
16666
16985
  }
16667
- collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, opts.repoRoot);
16986
+ collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, opts.repoRoot, result.coverage);
16668
16987
  }
16669
16988
  };
16670
16989
  try {
@@ -16736,7 +17055,7 @@ function classifyUnobserved(path) {
16736
17055
  if (/\.(png|jpg|jpeg|gif|pdf|zip|woff2?|ico|mp4)$/i.test(path)) return "binary";
16737
17056
  return "no_edit_record";
16738
17057
  }
16739
- function collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, repoRoot2) {
17058
+ function collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, repoRoot2, tally) {
16740
17059
  const message = record.message;
16741
17060
  const content = message?.content ?? record.content;
16742
17061
  const blocks = Array.isArray(content) ? content : content && typeof content === "object" ? [content] : [];
@@ -16758,6 +17077,9 @@ function collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse
16758
17077
  byPath.set(path, entry);
16759
17078
  }
16760
17079
  }
17080
+ if (DISPATCH_TOOLS.has(name) && tally) {
17081
+ tally.dispatched += 1;
17082
+ }
16761
17083
  if (name === "Bash") {
16762
17084
  const cmd = typeof input.command === "string" ? input.command : "";
16763
17085
  if (cmd) {
@@ -16838,9 +17160,16 @@ function buildAgentContext(input) {
16838
17160
  }
16839
17161
  if (input.intentVerdict === "misaligned" || input.intentVerdict === "partial") {
16840
17162
  const gap = input.intentGaps?.[0];
16841
- lines.push(
16842
- `- This does not look like the change that was asked for` + (gap ? `: ${gap}` : ".") + " Not blocking \u2014 but check it against what you were asked to do."
16843
- );
17163
+ const repeat = input.intentRepeat ?? 0;
17164
+ if (repeat > 0) {
17165
+ lines.push(
17166
+ `- Same intent flag as the last ${repeat === 1 ? "turn" : `${repeat} turns`}, on a goal that has not changed. Nothing new here \u2014 do not relay or re-explain it again; either act on it or carry on.`
17167
+ );
17168
+ } else {
17169
+ lines.push(
17170
+ `- This does not look like the change that was asked for` + (gap ? `: ${gap}` : ".") + " Not blocking \u2014 but check it against what you were asked to do."
17171
+ );
17172
+ }
16844
17173
  }
16845
17174
  const agentFindings = (input.findings ?? []).filter((f) => f.scope !== "pre-existing");
16846
17175
  for (const f of agentFindings) {
@@ -16887,6 +17216,14 @@ function buildHookOutput(gateDecision, systemMessage, agentContext) {
16887
17216
  } : {}
16888
17217
  };
16889
17218
  }
17219
+ var IDLE_EPISODE_CAP = 3;
17220
+ function channelSilence(input) {
17221
+ const movedSomething = input.newUserPrompt || input.newAuthorship;
17222
+ if (movedSomething) return null;
17223
+ if (input.consecutiveIdle >= IDLE_EPISODE_CAP) return "idle-episode-cap";
17224
+ if (input.emittedLast) return "caused-by-our-own-emission";
17225
+ return null;
17226
+ }
16890
17227
 
16891
17228
  // src/lib/cache-cleanup.ts
16892
17229
  var import_node_fs21 = require("node:fs");
@@ -17714,10 +18051,11 @@ async function readStopHookStdin() {
17714
18051
  return empty;
17715
18052
  }
17716
18053
  }
17717
- function agentContextFor(response) {
18054
+ function agentContextFor(response, intentRepeat = 0) {
17718
18055
  const metadata = response.metadata ?? {};
17719
18056
  const intent = response.intent_alignment ?? {};
17720
18057
  return buildAgentContext({
18058
+ intentRepeat,
17721
18059
  gateDecision: String(response.gate_decision ?? ""),
17722
18060
  findings: response.findings ?? [],
17723
18061
  pendingItems: response.pending_items ?? [],
@@ -18201,6 +18539,25 @@ async function runAnalyze(opts, globals) {
18201
18539
  projection: {
18202
18540
  v: 1,
18203
18541
  authored: foldResult?.authored ?? [],
18542
+ // ⚠ AUTHORED *SINCE THE LAST VERDICT* — a different question from `authored`.
18543
+ //
18544
+ // `authored` above is the fold of the session TRANSCRIPT, which is
18545
+ // cumulative: on turn 3 it still lists the files turn 2 edited. The
18546
+ // account's close-out reads it as "was this file edited since the
18547
+ // statement was raised", and those are not the same set.
18548
+ //
18549
+ // Measured 2026-08-03 (shirt-seller): four real vulnerabilities were
18550
+ // raised on the turn that wrote them, then marked `fixed` 36 seconds
18551
+ // later by a SUMMARY turn that edited nothing — `authored` still said 2,
18552
+ // the turn carried 0 findings, so "gone + file authored" resolved to
18553
+ // fixed. The vulnerabilities were still on disk. A silent false `fixed`
18554
+ // is worse than a false `open`: it retires the statement the Account
18555
+ // exists to keep.
18556
+ //
18557
+ // `hash_at_last_verdict` is frozen at each verdict and `hash_now` tracks
18558
+ // disk, so their inequality IS "changed since we last spoke" — already
18559
+ // computed, already maintained by the divergence machinery.
18560
+ authored_since_verdict: memorySession ? foldDossier(memorySession.d).authored_all.filter((a) => a.hash_now !== a.hash_at_last_verdict).map((a) => a.path) : [],
18204
18561
  unobserved: foldResult?.unobserved ?? [],
18205
18562
  commands: foldResult?.commands ?? [],
18206
18563
  unknown_types: foldResult?.unknownTypes ?? [],
@@ -18318,6 +18675,8 @@ async function runAnalyze(opts, globals) {
18318
18675
  message = "Verity: your GitHub verification expired \u2014 run `verity login` to re-verify (local analysis this run)";
18319
18676
  } else if (result.error.startsWith("FORBIDDEN")) {
18320
18677
  message = "Verity: no access grant for this repository \u2014 run `verity login` to refresh your grants (local analysis this run)";
18678
+ } else if (result.error.startsWith("INVALID_TOKEN")) {
18679
+ message = "Verity: your login expired or was revoked \u2014 run `verity login` to sign in again (local analysis this run)";
18321
18680
  } else if (result.http_status && result.http_status >= 400 && result.http_status < 500) {
18322
18681
  message = `Verity: request rejected (HTTP ${result.http_status}) \u2014 ${result.error}`;
18323
18682
  } else if (result.http_status && result.http_status >= 500) {
@@ -18334,6 +18693,34 @@ async function runAnalyze(opts, globals) {
18334
18693
  const sentPaths = codeDelta.files.map((f) => f.path);
18335
18694
  const watermarkHash = sentPaths.length > 0 ? computeContentHash(sentPaths) : contentHash;
18336
18695
  const watermarkIsPartial = !!codeDelta.truncated;
18696
+ let silenced = null;
18697
+ let turnIsIdleForChannel = true;
18698
+ if (memorySession) {
18699
+ try {
18700
+ const st = foldDossier(memorySession.d);
18701
+ turnIsIdleForChannel = st.authored_all.every((a) => a.hash_now === a.hash_at_last_verdict);
18702
+ silenced = channelSilence({
18703
+ // The BUFFER, not intentContext.user_prompt: the latter falls back to a
18704
+ // linked issue (VRT-53 W4) when no human spoke, and a fallback goal is
18705
+ // not a user utterance. Treating it as one would keep the loop alive on
18706
+ // exactly the autonomous cohort.
18707
+ newUserPrompt: (conversation?.prompts?.length ?? 0) > 0,
18708
+ newAuthorship: !turnIsIdleForChannel,
18709
+ emittedLast: st.meta.channel?.emittedLast === true,
18710
+ consecutiveIdle: st.meta.channel?.consecutiveIdle ?? 0
18711
+ });
18712
+ } catch {
18713
+ silenced = null;
18714
+ }
18715
+ }
18716
+ if (silenced) {
18717
+ logEvent("channel_silenced", {
18718
+ reason: silenced,
18719
+ run_id: response.run_id ?? turnId,
18720
+ decision
18721
+ });
18722
+ }
18723
+ let intentRepeatCount = 0;
18337
18724
  if (memorySession) {
18338
18725
  try {
18339
18726
  recordVerdict(memorySession.d, {
@@ -18347,8 +18734,18 @@ async function runAnalyze(opts, globals) {
18347
18734
  pattern_id: f.pattern_id ?? f.rule_id,
18348
18735
  title: f.title,
18349
18736
  severity: f.severity
18350
- })) ?? []
18737
+ })) ?? [],
18738
+ intent: response.intent_alignment ?? null,
18739
+ // The same signal F1 introduced: bytes differing from the hash frozen at
18740
+ // the last verdict. A turn that moved nothing is the only kind that can
18741
+ // accumulate a repeat.
18742
+ idle: turnIsIdleForChannel,
18743
+ // What next turn reads as `emittedLast`. A suppressed turn did not
18744
+ // speak, so it cannot be the cause of the turn after it — which is what
18745
+ // keeps this from becoming a permanent gag.
18746
+ emitted: !silenced
18351
18747
  });
18748
+ intentRepeatCount = Math.max(0, (foldDossier(memorySession.d).meta.intent_repeat?.consecutive ?? 1) - 1);
18352
18749
  } catch {
18353
18750
  }
18354
18751
  }
@@ -18538,7 +18935,7 @@ ${YELLOW}${grantNudge.trim()}${NC}
18538
18935
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
18539
18936
  userSummary += loginNudge + grantNudge;
18540
18937
  printJsonCompact(
18541
- buildHookOutput("PASS", userSummary, agentContextFor(response))
18938
+ buildHookOutput("PASS", userSummary, silenced ? null : agentContextFor(response, intentRepeatCount))
18542
18939
  );
18543
18940
  process.exit(0);
18544
18941
  break;
@@ -18552,7 +18949,7 @@ ${YELLOW}${grantNudge.trim()}${NC}
18552
18949
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
18553
18950
  userSummary += loginNudge + grantNudge;
18554
18951
  printJsonCompact(
18555
- buildHookOutput("WARN", userSummary, agentContextFor(response))
18952
+ buildHookOutput("WARN", userSummary, silenced ? null : agentContextFor(response, intentRepeatCount))
18556
18953
  );
18557
18954
  process.exit(0);
18558
18955
  break;
@@ -18970,7 +19367,7 @@ async function runGuard(opts, globals) {
18970
19367
  cmd: "guard"
18971
19368
  });
18972
19369
  if (!result.ok) {
18973
- 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." : "";
19370
+ 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." : "";
18974
19371
  emitAllowNotice(
18975
19372
  `\u26A0 Verity ${moment}: ${authRemedy ? "not authorized" : "service offline"} \u2014 ${verb}ed WITHOUT review${authRemedy}`,
18976
19373
  `Verity ${moment}: ${authRemedy ? "not authorized" : "service unavailable"} (${result.error}); the ${verb} was allowed WITHOUT a Verity review.${authRemedy}`
@@ -20354,7 +20751,7 @@ function registerTelemetryCommands(program2) {
20354
20751
  }
20355
20752
 
20356
20753
  // src/cli.ts
20357
- program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.dbd87b1").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async () => {
20754
+ program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.dfb3ce6").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async () => {
20358
20755
  try {
20359
20756
  await foldLegacyLocalCredential();
20360
20757
  } catch {
@@ -20363,6 +20760,8 @@ program.name("verity").description("CLI for Verity quality gate service").versio
20363
20760
  registerAuthCommands(program);
20364
20761
  registerLoginCommand(program);
20365
20762
  registerTokenCommand(program);
20763
+ registerSessionsCommands(program);
20764
+ registerLogoutCommand(program);
20366
20765
  registerHooksCommands(program);
20367
20766
  registerIntentCommands(program);
20368
20767
  registerLifecycleCommands(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codacy/verity-cli",
3
- "version": "0.28.1-experimental.dbd87b1",
3
+ "version": "0.28.1-experimental.dfb3ce6",
4
4
  "description": "CLI for Verity quality gate service",
5
5
  "homepage": "https://verity.md",
6
6
  "bugs": {