@codacy/verity-cli 0.28.1-experimental.a57c8d9 → 0.28.1-experimental.a72d2d9

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.
package/bin/verity.js CHANGED
@@ -10570,135 +10570,6 @@ function rotateIfNeeded(file) {
10570
10570
  }
10571
10571
  }
10572
10572
 
10573
- // src/lib/api-client.ts
10574
- function describeFetchError(err, url) {
10575
- const message = err instanceof Error ? err.message : String(err);
10576
- const cause = err?.cause;
10577
- const causeBits = [cause?.code, cause?.hostname].filter(Boolean).join(" ");
10578
- const detail = causeBits || (cause?.message && cause.message !== message ? cause.message : "");
10579
- return `${message}${detail ? ` (${detail})` : ""} \u2014 could not reach ${url}`;
10580
- }
10581
- async function apiRequest(options) {
10582
- const {
10583
- method,
10584
- path,
10585
- serviceUrl,
10586
- token,
10587
- body,
10588
- verbose,
10589
- timeout = 9e4,
10590
- cmd = "unknown",
10591
- retry = false,
10592
- encodeBody = false,
10593
- extraHeaders
10594
- } = options;
10595
- const url = `${serviceUrl}${path}`;
10596
- const headers = {
10597
- "Content-Type": "application/json"
10598
- };
10599
- if (token) {
10600
- headers["Authorization"] = `Bearer ${token}`;
10601
- }
10602
- if (extraHeaders) {
10603
- Object.assign(headers, extraHeaders);
10604
- }
10605
- const testMockScenario = process.env.VERITY_TEST_MOCK_SCENARIO;
10606
- if (testMockScenario) {
10607
- headers["X-Verity-Mock-Scenario"] = testMockScenario;
10608
- }
10609
- const testMockFailure = process.env.VERITY_TEST_MOCK_FAILURE;
10610
- if (testMockFailure) {
10611
- headers["X-Verity-Mock-Failure"] = testMockFailure;
10612
- }
10613
- printVerbose(`${method} ${url}`, verbose);
10614
- let serializedBody;
10615
- if (body !== void 0 && body !== null) {
10616
- const innerJson = JSON.stringify(body);
10617
- if (encodeBody) {
10618
- const payload = Buffer.from(innerJson, "utf8").toString("base64");
10619
- serializedBody = JSON.stringify({ encoding: "base64", payload });
10620
- } else {
10621
- serializedBody = innerJson;
10622
- }
10623
- printVerbose(`Body: ${serializedBody.slice(0, 500)}`, verbose);
10624
- }
10625
- const startedAt = Date.now();
10626
- const bodyBytes = serializedBody ? Buffer.byteLength(serializedBody) : 0;
10627
- const logBase = { cmd, method, url, body_bytes: bodyBytes, retry, encoded: encodeBody };
10628
- let response;
10629
- try {
10630
- response = await fetch(url, {
10631
- method,
10632
- headers,
10633
- body: serializedBody,
10634
- signal: AbortSignal.timeout(timeout)
10635
- });
10636
- } catch (err) {
10637
- const duration2 = Date.now() - startedAt;
10638
- const isTimeout = err instanceof DOMException && err.name === "TimeoutError";
10639
- const category = isTimeout ? "timeout" : "network";
10640
- const error = isTimeout ? `Request timed out after ${timeout}ms (${url})` : `Network error: ${describeFetchError(err, url)}`;
10641
- logHttpCall({ ...logBase, duration_ms: duration2, http_status: null, category, error });
10642
- return { ok: false, error, category, http_status: null };
10643
- }
10644
- let data;
10645
- try {
10646
- data = response.status === 204 || response.headers.get("content-length") === "0" ? {} : await response.json();
10647
- } catch {
10648
- const duration2 = Date.now() - startedAt;
10649
- const error = `Invalid JSON response (HTTP ${response.status})`;
10650
- logHttpCall({
10651
- ...logBase,
10652
- duration_ms: duration2,
10653
- http_status: response.status,
10654
- category: "invalid_json",
10655
- error
10656
- });
10657
- return { ok: false, error, category: "invalid_json", http_status: response.status };
10658
- }
10659
- printVerbose(`Response ${response.status}: ${JSON.stringify(data).slice(0, 500)}`, verbose);
10660
- const duration = Date.now() - startedAt;
10661
- if (!response.ok) {
10662
- const apiErr = data;
10663
- const code = apiErr?.error?.code ?? "UNKNOWN";
10664
- const message = apiErr?.error?.message ?? `HTTP ${response.status}`;
10665
- const category = response.status >= 500 ? "http_5xx" : "http_4xx";
10666
- const error = `${code}: ${message}`;
10667
- logHttpCall({
10668
- ...logBase,
10669
- duration_ms: duration,
10670
- http_status: response.status,
10671
- category,
10672
- error
10673
- });
10674
- return { ok: false, error, category, http_status: response.status };
10675
- }
10676
- logHttpCall({
10677
- ...logBase,
10678
- duration_ms: duration,
10679
- http_status: response.status,
10680
- category: "ok"
10681
- });
10682
- return { ok: true, data };
10683
- }
10684
- function analyzeRequest(options) {
10685
- return apiRequest({
10686
- method: "POST",
10687
- path: "/analyze",
10688
- serviceUrl: options.serviceUrl,
10689
- token: options.token,
10690
- body: options.body,
10691
- timeout: options.timeout,
10692
- cmd: options.cmd,
10693
- verbose: options.verbose,
10694
- retry: options.retry,
10695
- encodeBody: true
10696
- });
10697
- }
10698
-
10699
- // src/lib/service-url.ts
10700
- var import_promises2 = require("node:fs/promises");
10701
-
10702
10573
  // src/lib/credentials.ts
10703
10574
  var import_promises = require("node:fs/promises");
10704
10575
  var import_node_path2 = require("node:path");
@@ -10765,14 +10636,14 @@ async function readGlobalCredential(remote) {
10765
10636
  const parsed = parseCredentialLine(line);
10766
10637
  if (parsed && parsed.remote === key) last = parsed.rec;
10767
10638
  }
10768
- if (last) return last;
10639
+ if (last) return { ...last, keyed: true };
10769
10640
  }
10770
10641
  let plain = null;
10771
10642
  for (const line of lines) {
10772
10643
  const parsed = parseCredentialLine(line);
10773
10644
  if (parsed && parsed.remote === "") plain = parsed.rec;
10774
10645
  }
10775
- return plain;
10646
+ return plain ? { ...plain, keyed: false } : null;
10776
10647
  }
10777
10648
  async function upsertGlobalCredential(remote, rec) {
10778
10649
  const path = globalCredentialsPath();
@@ -10805,6 +10676,67 @@ async function upsertGlobalCredential(remote, rec) {
10805
10676
  await (0, import_promises.chmod)(path, 384).catch(() => {
10806
10677
  });
10807
10678
  }
10679
+ async function removeSupersededUserCredentials(loginServiceUrl, loginUserId) {
10680
+ if (loginUserId == null) return 0;
10681
+ const path = globalCredentialsPath();
10682
+ let content;
10683
+ try {
10684
+ content = await (0, import_promises.readFile)(path, "utf-8");
10685
+ } catch {
10686
+ return 0;
10687
+ }
10688
+ const kept = [];
10689
+ let removed = 0;
10690
+ for (const line of content.split("\n")) {
10691
+ const parsed = parseCredentialLine(line);
10692
+ const superseded = parsed !== null && parsed.remote !== "" && parsed.rec.userId === loginUserId && (!parsed.rec.serviceUrl || parsed.rec.serviceUrl === loginServiceUrl);
10693
+ if (superseded) {
10694
+ removed++;
10695
+ continue;
10696
+ }
10697
+ kept.push(line);
10698
+ }
10699
+ if (removed === 0) return 0;
10700
+ while (kept.length && kept[kept.length - 1].trim() === "") kept.pop();
10701
+ try {
10702
+ await (0, import_promises.writeFile)(path, kept.join("\n") + "\n", { mode: 384 });
10703
+ await (0, import_promises.chmod)(path, 384).catch(() => {
10704
+ });
10705
+ } catch {
10706
+ return -1;
10707
+ }
10708
+ return removed;
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
+ }
10808
10740
  function parseLocalCredentialFile(content) {
10809
10741
  const tokenMatch = content.match(/token:\s*((?:gate_|verity_)[a-f0-9]+)/);
10810
10742
  if (!tokenMatch) return null;
@@ -10854,10 +10786,378 @@ async function foldLegacyLocalCredential(remoteArg) {
10854
10786
  }
10855
10787
  await (0, import_promises.unlink)(projectPath(CREDENTIALS_FILE)).catch(() => {
10856
10788
  });
10857
- return true;
10789
+ return true;
10790
+ }
10791
+
10792
+ // src/lib/git.ts
10793
+ var import_node_child_process3 = require("node:child_process");
10794
+ var import_node_fs3 = require("node:fs");
10795
+ var import_node_path3 = require("node:path");
10796
+ function resolveFile(relpath) {
10797
+ return (0, import_node_fs3.existsSync)(relpath) ? relpath : null;
10798
+ }
10799
+ function execGit(cmd) {
10800
+ try {
10801
+ return (0, import_node_child_process3.execSync)(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
10802
+ } catch {
10803
+ return "";
10804
+ }
10805
+ }
10806
+ function splitLines(s) {
10807
+ return s.split("\n").filter((l) => l.length > 0);
10808
+ }
10809
+ var SHA_RE = /^[0-9a-f]{40}$/;
10810
+ function readBaselineSha() {
10811
+ if (!(0, import_node_fs3.existsSync)(BASELINE_SHA_FILE)) return null;
10812
+ let sha;
10813
+ try {
10814
+ sha = (0, import_node_fs3.readFileSync)(BASELINE_SHA_FILE, "utf-8").trim();
10815
+ } catch {
10816
+ return null;
10817
+ }
10818
+ if (!SHA_RE.test(sha)) return null;
10819
+ const reachable = execGit(`git cat-file -e ${sha}^{commit} 2>/dev/null && echo ok`) === "ok";
10820
+ if (!reachable) {
10821
+ try {
10822
+ (0, import_node_fs3.unlinkSync)(BASELINE_SHA_FILE);
10823
+ } catch {
10824
+ }
10825
+ return null;
10826
+ }
10827
+ return sha;
10828
+ }
10829
+ function writeBaselineSha(sha) {
10830
+ if (!SHA_RE.test(sha)) return;
10831
+ try {
10832
+ (0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(BASELINE_SHA_FILE), { recursive: true });
10833
+ (0, import_node_fs3.writeFileSync)(BASELINE_SHA_FILE, sha);
10834
+ } catch {
10835
+ }
10836
+ }
10837
+ function getChangedFiles() {
10838
+ const sets = /* @__PURE__ */ new Set();
10839
+ let hasRecentCommitFiles = false;
10840
+ for (const f of splitLines(execGit("git diff --name-only HEAD"))) sets.add(f);
10841
+ for (const f of splitLines(execGit("git diff --name-only --cached"))) sets.add(f);
10842
+ for (const f of splitLines(execGit("git ls-files --others --exclude-standard"))) sets.add(f);
10843
+ const baseline = readBaselineSha();
10844
+ if (baseline) {
10845
+ const committed = splitLines(execGit(`git diff --name-only ${baseline}..HEAD`));
10846
+ if (committed.length > 0) {
10847
+ hasRecentCommitFiles = true;
10848
+ for (const f of committed) sets.add(f);
10849
+ }
10850
+ } else {
10851
+ const headTimestamp = parseInt(execGit("git log -1 --format=%ct HEAD"), 10) || 0;
10852
+ const commitAge = Math.floor(Date.now() / 1e3) - headTimestamp;
10853
+ const hasUnstaged = splitLines(execGit("git diff --name-only HEAD")).length > 0;
10854
+ const hasStaged = splitLines(execGit("git diff --name-only --cached")).length > 0;
10855
+ if (commitAge < 120 && !hasUnstaged && !hasStaged) {
10856
+ const recentFiles = splitLines(execGit("git diff --name-only HEAD~1..HEAD"));
10857
+ if (recentFiles.length > 0) {
10858
+ hasRecentCommitFiles = true;
10859
+ for (const f of recentFiles) sets.add(f);
10860
+ }
10861
+ }
10862
+ }
10863
+ const filtered = Array.from(sets).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
10864
+ return { files: filtered, hasRecentCommitFiles };
10865
+ }
10866
+ function getStagedFiles() {
10867
+ return splitLines(execGit("git diff --cached --name-only")).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
10868
+ }
10869
+ function getDirtyFiles() {
10870
+ const set = /* @__PURE__ */ new Set();
10871
+ for (const f of splitLines(execGit("git diff --name-only HEAD"))) set.add(f);
10872
+ for (const f of splitLines(execGit("git diff --name-only --cached"))) set.add(f);
10873
+ for (const f of splitLines(execGit("git ls-files --others --exclude-standard"))) set.add(f);
10874
+ return Array.from(set).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
10875
+ }
10876
+ function showContentAtRef(ref, repoRelPath) {
10877
+ if (!ref || ref === "no-git") return null;
10878
+ const normalizedPath = repoRelPath.replace(/\\/g, "/");
10879
+ try {
10880
+ return (0, import_node_child_process3.execFileSync)("git", ["show", `${ref}:${normalizedPath}`], {
10881
+ encoding: "utf-8",
10882
+ maxBuffer: 64 * 1024 * 1024,
10883
+ stdio: ["pipe", "pipe", "pipe"]
10884
+ });
10885
+ } catch {
10886
+ return null;
10887
+ }
10888
+ }
10889
+ function getPushRangeFiles() {
10890
+ const diff = (range) => splitLines(execGit(`git diff --name-only ${range}`)).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
10891
+ const resolvers = [
10892
+ () => execGit("git rev-parse --abbrev-ref --symbolic-full-name @{push}") ? "@{push}..HEAD" : null,
10893
+ () => execGit("git rev-parse --abbrev-ref --symbolic-full-name @{upstream}") ? "@{upstream}..HEAD" : null,
10894
+ () => {
10895
+ const branch = execGit("git rev-parse --abbrev-ref HEAD");
10896
+ return branch && branch !== "HEAD" && execGit(`git rev-parse --verify -q origin/${branch}`) ? `origin/${branch}..HEAD` : null;
10897
+ }
10898
+ ];
10899
+ for (const resolve2 of resolvers) {
10900
+ const range = resolve2();
10901
+ if (range) return { files: diff(range), range };
10902
+ }
10903
+ const baseline = readBaselineSha();
10904
+ if (baseline) {
10905
+ const files = diff(`${baseline}..HEAD`);
10906
+ if (files.length > 0) return { files, range: `${baseline}..HEAD` };
10907
+ }
10908
+ const last = diff("HEAD~1..HEAD");
10909
+ return { files: last, range: last.length > 0 ? "HEAD~1..HEAD" : null };
10910
+ }
10911
+ function getPushRangeMessages() {
10912
+ const { range } = getPushRangeFiles();
10913
+ if (!range) return "";
10914
+ return execGit(`git log ${range} --format=%B%x00`).split("\0").map((s) => s.trim()).filter(Boolean).join("\n\n");
10915
+ }
10916
+ function filterAnalyzable(files) {
10917
+ return files.filter((f) => {
10918
+ const ext = (0, import_node_path3.extname)(f).slice(1);
10919
+ return ANALYZABLE_EXTENSIONS.has(ext);
10920
+ });
10921
+ }
10922
+ function filterReviewable(files) {
10923
+ return files.filter((f) => {
10924
+ const ext = (0, import_node_path3.extname)(f).slice(1);
10925
+ if (ANALYZABLE_EXTENSIONS.has(ext)) return false;
10926
+ if (REVIEWABLE_EXTENSIONS.has(ext)) return true;
10927
+ const basename4 = f.split("/").pop() ?? "";
10928
+ if (REVIEWABLE_FILENAMES.has(basename4)) return true;
10929
+ if (REVIEWABLE_PATH_PATTERNS.some((p) => p.test(f))) return true;
10930
+ return false;
10931
+ });
10932
+ }
10933
+ function filterSecurity(files) {
10934
+ return files.filter(
10935
+ (f) => SECURITY_PATTERNS.some((p) => p.test(f))
10936
+ );
10937
+ }
10938
+ function getCurrentCommit() {
10939
+ return execGit("git rev-parse HEAD") || "no-git";
10940
+ }
10941
+ function getCurrentBranch() {
10942
+ const b = execGit("git rev-parse --abbrev-ref HEAD");
10943
+ return !b || b === "HEAD" ? null : b;
10944
+ }
10945
+ function commitResolves(sha) {
10946
+ if (!sha) return false;
10947
+ try {
10948
+ (0, import_node_child_process3.execSync)(`git merge-base --is-ancestor ${JSON.stringify(sha)} HEAD`, {
10949
+ stdio: ["pipe", "pipe", "pipe"]
10950
+ });
10951
+ return true;
10952
+ } catch {
10953
+ return false;
10954
+ }
10955
+ }
10956
+ function commitsSincePaths(sha, paths) {
10957
+ if (!sha) return null;
10958
+ try {
10959
+ const scope = paths.length > 0 ? ` -- ${paths.slice(0, 50).map((p) => JSON.stringify(p)).join(" ")}` : "";
10960
+ const out = (0, import_node_child_process3.execSync)(`git rev-list --count ${JSON.stringify(sha)}..HEAD${scope}`, {
10961
+ encoding: "utf-8",
10962
+ stdio: ["pipe", "pipe", "pipe"]
10963
+ }).trim();
10964
+ const n = Number.parseInt(out, 10);
10965
+ return Number.isFinite(n) ? n : null;
10966
+ } catch {
10967
+ return null;
10968
+ }
10969
+ }
10970
+ function detectProvider(host) {
10971
+ const h = host.toLowerCase();
10972
+ if (h.includes("github")) return "github";
10973
+ if (h.includes("gitlab")) return "gitlab";
10974
+ if (h.includes("bitbucket")) return "bitbucket";
10975
+ return "unknown";
10976
+ }
10977
+ function parseRemote(raw) {
10978
+ if (!raw || typeof raw !== "string") return null;
10979
+ let s = raw.trim();
10980
+ if (!s) return null;
10981
+ let host = "";
10982
+ let path = "";
10983
+ const scp = s.match(/^[^/@]+@([^:/]+):(.+)$/);
10984
+ if (scp) {
10985
+ host = scp[1];
10986
+ path = scp[2];
10987
+ } else {
10988
+ s = s.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, "");
10989
+ s = s.replace(/^[^/@]+@/, "");
10990
+ const slash = s.indexOf("/");
10991
+ if (slash === -1) return null;
10992
+ host = s.slice(0, slash);
10993
+ path = s.slice(slash + 1);
10994
+ }
10995
+ host = host.toLowerCase().trim();
10996
+ path = path.replace(/\/+$/, "").replace(/\.git$/, "");
10997
+ if (!host || !path) return null;
10998
+ const segments = path.split("/").filter(Boolean);
10999
+ if (segments.length < 2) return null;
11000
+ const owner = segments[0];
11001
+ const repo = segments[segments.length - 1];
11002
+ if (!owner || !repo) return null;
11003
+ return {
11004
+ host,
11005
+ owner,
11006
+ repo,
11007
+ provider: detectProvider(host),
11008
+ orgUrl: `https://${host}/${owner}`,
11009
+ orgName: owner
11010
+ };
11011
+ }
11012
+ function listTrackedFiles() {
11013
+ const set = /* @__PURE__ */ new Set();
11014
+ for (const f of splitLines(execGit("git ls-files"))) set.add(f);
11015
+ for (const f of splitLines(execGit("git ls-files --others --exclude-standard"))) set.add(f);
11016
+ return Array.from(set);
11017
+ }
11018
+ function sanitizeRemote(remote) {
11019
+ return remote.replace(/^([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^/]*@/, "$1");
11020
+ }
11021
+
11022
+ // src/lib/api-client.ts
11023
+ var cachedRemote = null;
11024
+ function requestRemote() {
11025
+ const override = process.env.VERITY_REMOTE_OVERRIDE;
11026
+ if (override) return sanitizeRemote(override.trim());
11027
+ if (cachedRemote === null) cachedRemote = sanitizeRemote(currentRemote());
11028
+ return cachedRemote;
11029
+ }
11030
+ function describeFetchError(err, url) {
11031
+ const message = err instanceof Error ? err.message : String(err);
11032
+ const cause = err?.cause;
11033
+ const causeBits = [cause?.code, cause?.hostname].filter(Boolean).join(" ");
11034
+ const detail = causeBits || (cause?.message && cause.message !== message ? cause.message : "");
11035
+ return `${message}${detail ? ` (${detail})` : ""} \u2014 could not reach ${url}`;
11036
+ }
11037
+ async function apiRequest(options) {
11038
+ const {
11039
+ method,
11040
+ path,
11041
+ serviceUrl,
11042
+ token,
11043
+ body,
11044
+ verbose,
11045
+ timeout = 9e4,
11046
+ cmd = "unknown",
11047
+ retry = false,
11048
+ encodeBody = false,
11049
+ extraHeaders
11050
+ } = options;
11051
+ const url = `${serviceUrl}${path}`;
11052
+ const headers = {
11053
+ "Content-Type": "application/json"
11054
+ };
11055
+ if (token) {
11056
+ headers["Authorization"] = `Bearer ${token}`;
11057
+ }
11058
+ const remote = requestRemote();
11059
+ if (remote) {
11060
+ headers["X-Verity-Remote"] = remote;
11061
+ }
11062
+ if (extraHeaders) {
11063
+ Object.assign(headers, extraHeaders);
11064
+ }
11065
+ const testMockScenario = process.env.VERITY_TEST_MOCK_SCENARIO;
11066
+ if (testMockScenario) {
11067
+ headers["X-Verity-Mock-Scenario"] = testMockScenario;
11068
+ }
11069
+ const testMockFailure = process.env.VERITY_TEST_MOCK_FAILURE;
11070
+ if (testMockFailure) {
11071
+ headers["X-Verity-Mock-Failure"] = testMockFailure;
11072
+ }
11073
+ printVerbose(`${method} ${url}`, verbose);
11074
+ let serializedBody;
11075
+ if (body !== void 0 && body !== null) {
11076
+ const innerJson = JSON.stringify(body);
11077
+ if (encodeBody) {
11078
+ const payload = Buffer.from(innerJson, "utf8").toString("base64");
11079
+ serializedBody = JSON.stringify({ encoding: "base64", payload });
11080
+ } else {
11081
+ serializedBody = innerJson;
11082
+ }
11083
+ printVerbose(`Body: ${serializedBody.slice(0, 500)}`, verbose);
11084
+ }
11085
+ const startedAt = Date.now();
11086
+ const bodyBytes = serializedBody ? Buffer.byteLength(serializedBody) : 0;
11087
+ const logBase = { cmd, method, url, body_bytes: bodyBytes, retry, encoded: encodeBody };
11088
+ let response;
11089
+ try {
11090
+ response = await fetch(url, {
11091
+ method,
11092
+ headers,
11093
+ body: serializedBody,
11094
+ signal: AbortSignal.timeout(timeout)
11095
+ });
11096
+ } catch (err) {
11097
+ const duration2 = Date.now() - startedAt;
11098
+ const isTimeout = err instanceof DOMException && err.name === "TimeoutError";
11099
+ const category = isTimeout ? "timeout" : "network";
11100
+ const error = isTimeout ? `Request timed out after ${timeout}ms (${url})` : `Network error: ${describeFetchError(err, url)}`;
11101
+ logHttpCall({ ...logBase, duration_ms: duration2, http_status: null, category, error });
11102
+ return { ok: false, error, category, http_status: null };
11103
+ }
11104
+ let data;
11105
+ try {
11106
+ data = response.status === 204 || response.headers.get("content-length") === "0" ? {} : await response.json();
11107
+ } catch {
11108
+ const duration2 = Date.now() - startedAt;
11109
+ const error = `Invalid JSON response (HTTP ${response.status})`;
11110
+ logHttpCall({
11111
+ ...logBase,
11112
+ duration_ms: duration2,
11113
+ http_status: response.status,
11114
+ category: "invalid_json",
11115
+ error
11116
+ });
11117
+ return { ok: false, error, category: "invalid_json", http_status: response.status };
11118
+ }
11119
+ printVerbose(`Response ${response.status}: ${JSON.stringify(data).slice(0, 500)}`, verbose);
11120
+ const duration = Date.now() - startedAt;
11121
+ if (!response.ok) {
11122
+ const apiErr = data;
11123
+ const code = apiErr?.error?.code ?? "UNKNOWN";
11124
+ const message = apiErr?.error?.message ?? `HTTP ${response.status}`;
11125
+ const category = response.status >= 500 ? "http_5xx" : "http_4xx";
11126
+ const error = `${code}: ${message}`;
11127
+ logHttpCall({
11128
+ ...logBase,
11129
+ duration_ms: duration,
11130
+ http_status: response.status,
11131
+ category,
11132
+ error
11133
+ });
11134
+ return { ok: false, error, category, http_status: response.status };
11135
+ }
11136
+ logHttpCall({
11137
+ ...logBase,
11138
+ duration_ms: duration,
11139
+ http_status: response.status,
11140
+ category: "ok"
11141
+ });
11142
+ return { ok: true, data };
11143
+ }
11144
+ function analyzeRequest(options) {
11145
+ return apiRequest({
11146
+ method: "POST",
11147
+ path: "/analyze",
11148
+ serviceUrl: options.serviceUrl,
11149
+ token: options.token,
11150
+ body: options.body,
11151
+ timeout: options.timeout,
11152
+ cmd: options.cmd,
11153
+ verbose: options.verbose,
11154
+ retry: options.retry,
11155
+ encodeBody: true
11156
+ });
10858
11157
  }
10859
11158
 
10860
11159
  // src/lib/service-url.ts
11160
+ var import_promises2 = require("node:fs/promises");
10861
11161
  async function serviceUrlFromCredentials() {
10862
11162
  const rec = await readGlobalCredential(currentRemote());
10863
11163
  return rec?.serviceUrl ?? null;
@@ -10918,7 +11218,13 @@ async function resolveToken(flagToken) {
10918
11218
  if (rec) {
10919
11219
  return {
10920
11220
  ok: true,
10921
- 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
+ }
10922
11228
  };
10923
11229
  }
10924
11230
  const local = await readLegacyLocalCredential();
@@ -10940,6 +11246,47 @@ async function whoami(token, serviceUrl, verbose) {
10940
11246
  cmd: "whoami"
10941
11247
  });
10942
11248
  }
11249
+ function reverifyNudge(who) {
11250
+ if (who.grant_status === "hard_stale") {
11251
+ return 'Your GitHub verification has expired \u2014 run "verity login" to restore saved runs and memory.';
11252
+ }
11253
+ if (who.grant_status === "soft_stale") {
11254
+ const by = who.reverify_by ? ` by ${who.reverify_by.slice(0, 10)}` : " soon";
11255
+ return `Your GitHub verification needs a refresh${by} \u2014 run "verity login" to re-verify.`;
11256
+ }
11257
+ return null;
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
+ }
11269
+ function authDenialRemedy(error) {
11270
+ if (error.startsWith("STALE_VERIFICATION")) {
11271
+ return {
11272
+ code: "STALE_VERIFICATION",
11273
+ remedy: 'Your GitHub verification has expired \u2014 run "verity login" to re-verify your repository access.'
11274
+ };
11275
+ }
11276
+ if (error.startsWith("FORBIDDEN")) {
11277
+ return {
11278
+ code: "FORBIDDEN",
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.'
11280
+ };
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
+ }
11288
+ return null;
11289
+ }
10943
11290
  async function probeService(serviceUrl, verbose) {
10944
11291
  const res = await apiRequest({
10945
11292
  method: "GET",
@@ -10978,6 +11325,7 @@ async function maybeHealServiceUrl(resolution, verbose) {
10978
11325
 
10979
11326
  // src/lib/register.ts
10980
11327
  var readline = __toESM(require("node:readline/promises"));
11328
+ var import_node_os = require("node:os");
10981
11329
 
10982
11330
  // src/lib/provider-auth.ts
10983
11331
  var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
@@ -11064,250 +11412,23 @@ async function githubDeviceFlow() {
11064
11412
  data = await res.json().catch(() => ({}));
11065
11413
  } catch {
11066
11414
  continue;
11067
- }
11068
- if (data.access_token) return { ok: true, data: data.access_token };
11069
- switch (data.error) {
11070
- case "authorization_pending":
11071
- break;
11072
- case "slow_down":
11073
- interval += 5;
11074
- break;
11075
- case "access_denied":
11076
- return { ok: false, error: "Authorization was denied on GitHub." };
11077
- case "expired_token":
11078
- return { ok: false, error: "The authorization code expired. Re-run register." };
11079
- default:
11080
- if (data.error) return { ok: false, error: `GitHub auth error: ${data.error}` };
11081
- }
11082
- }
11083
- return { ok: false, error: "Timed out waiting for GitHub authorization." };
11084
- }
11085
-
11086
- // src/lib/git.ts
11087
- var import_node_child_process3 = require("node:child_process");
11088
- var import_node_fs3 = require("node:fs");
11089
- var import_node_path3 = require("node:path");
11090
- function resolveFile(relpath) {
11091
- return (0, import_node_fs3.existsSync)(relpath) ? relpath : null;
11092
- }
11093
- function execGit(cmd) {
11094
- try {
11095
- return (0, import_node_child_process3.execSync)(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
11096
- } catch {
11097
- return "";
11098
- }
11099
- }
11100
- function splitLines(s) {
11101
- return s.split("\n").filter((l) => l.length > 0);
11102
- }
11103
- var SHA_RE = /^[0-9a-f]{40}$/;
11104
- function readBaselineSha() {
11105
- if (!(0, import_node_fs3.existsSync)(BASELINE_SHA_FILE)) return null;
11106
- let sha;
11107
- try {
11108
- sha = (0, import_node_fs3.readFileSync)(BASELINE_SHA_FILE, "utf-8").trim();
11109
- } catch {
11110
- return null;
11111
- }
11112
- if (!SHA_RE.test(sha)) return null;
11113
- const reachable = execGit(`git cat-file -e ${sha}^{commit} 2>/dev/null && echo ok`) === "ok";
11114
- if (!reachable) {
11115
- try {
11116
- (0, import_node_fs3.unlinkSync)(BASELINE_SHA_FILE);
11117
- } catch {
11118
- }
11119
- return null;
11120
- }
11121
- return sha;
11122
- }
11123
- function writeBaselineSha(sha) {
11124
- if (!SHA_RE.test(sha)) return;
11125
- try {
11126
- (0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(BASELINE_SHA_FILE), { recursive: true });
11127
- (0, import_node_fs3.writeFileSync)(BASELINE_SHA_FILE, sha);
11128
- } catch {
11129
- }
11130
- }
11131
- function getChangedFiles() {
11132
- const sets = /* @__PURE__ */ new Set();
11133
- let hasRecentCommitFiles = false;
11134
- for (const f of splitLines(execGit("git diff --name-only HEAD"))) sets.add(f);
11135
- for (const f of splitLines(execGit("git diff --name-only --cached"))) sets.add(f);
11136
- for (const f of splitLines(execGit("git ls-files --others --exclude-standard"))) sets.add(f);
11137
- const baseline = readBaselineSha();
11138
- if (baseline) {
11139
- const committed = splitLines(execGit(`git diff --name-only ${baseline}..HEAD`));
11140
- if (committed.length > 0) {
11141
- hasRecentCommitFiles = true;
11142
- for (const f of committed) sets.add(f);
11143
- }
11144
- } else {
11145
- const headTimestamp = parseInt(execGit("git log -1 --format=%ct HEAD"), 10) || 0;
11146
- const commitAge = Math.floor(Date.now() / 1e3) - headTimestamp;
11147
- const hasUnstaged = splitLines(execGit("git diff --name-only HEAD")).length > 0;
11148
- const hasStaged = splitLines(execGit("git diff --name-only --cached")).length > 0;
11149
- if (commitAge < 120 && !hasUnstaged && !hasStaged) {
11150
- const recentFiles = splitLines(execGit("git diff --name-only HEAD~1..HEAD"));
11151
- if (recentFiles.length > 0) {
11152
- hasRecentCommitFiles = true;
11153
- for (const f of recentFiles) sets.add(f);
11154
- }
11155
- }
11156
- }
11157
- const filtered = Array.from(sets).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
11158
- return { files: filtered, hasRecentCommitFiles };
11159
- }
11160
- function getStagedFiles() {
11161
- return splitLines(execGit("git diff --cached --name-only")).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
11162
- }
11163
- function getDirtyFiles() {
11164
- const set = /* @__PURE__ */ new Set();
11165
- for (const f of splitLines(execGit("git diff --name-only HEAD"))) set.add(f);
11166
- for (const f of splitLines(execGit("git diff --name-only --cached"))) set.add(f);
11167
- for (const f of splitLines(execGit("git ls-files --others --exclude-standard"))) set.add(f);
11168
- return Array.from(set).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
11169
- }
11170
- function showContentAtRef(ref, repoRelPath) {
11171
- if (!ref || ref === "no-git") return null;
11172
- const normalizedPath = repoRelPath.replace(/\\/g, "/");
11173
- try {
11174
- return (0, import_node_child_process3.execFileSync)("git", ["show", `${ref}:${normalizedPath}`], {
11175
- encoding: "utf-8",
11176
- maxBuffer: 64 * 1024 * 1024,
11177
- stdio: ["pipe", "pipe", "pipe"]
11178
- });
11179
- } catch {
11180
- return null;
11181
- }
11182
- }
11183
- function getPushRangeFiles() {
11184
- const diff = (range) => splitLines(execGit(`git diff --name-only ${range}`)).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
11185
- const resolvers = [
11186
- () => execGit("git rev-parse --abbrev-ref --symbolic-full-name @{push}") ? "@{push}..HEAD" : null,
11187
- () => execGit("git rev-parse --abbrev-ref --symbolic-full-name @{upstream}") ? "@{upstream}..HEAD" : null,
11188
- () => {
11189
- const branch = execGit("git rev-parse --abbrev-ref HEAD");
11190
- return branch && branch !== "HEAD" && execGit(`git rev-parse --verify -q origin/${branch}`) ? `origin/${branch}..HEAD` : null;
11191
- }
11192
- ];
11193
- for (const resolve2 of resolvers) {
11194
- const range = resolve2();
11195
- if (range) return { files: diff(range), range };
11196
- }
11197
- const baseline = readBaselineSha();
11198
- if (baseline) {
11199
- const files = diff(`${baseline}..HEAD`);
11200
- if (files.length > 0) return { files, range: `${baseline}..HEAD` };
11201
- }
11202
- const last = diff("HEAD~1..HEAD");
11203
- return { files: last, range: last.length > 0 ? "HEAD~1..HEAD" : null };
11204
- }
11205
- function getPushRangeMessages() {
11206
- const { range } = getPushRangeFiles();
11207
- if (!range) return "";
11208
- return execGit(`git log ${range} --format=%B%x00`).split("\0").map((s) => s.trim()).filter(Boolean).join("\n\n");
11209
- }
11210
- function filterAnalyzable(files) {
11211
- return files.filter((f) => {
11212
- const ext = (0, import_node_path3.extname)(f).slice(1);
11213
- return ANALYZABLE_EXTENSIONS.has(ext);
11214
- });
11215
- }
11216
- function filterReviewable(files) {
11217
- return files.filter((f) => {
11218
- const ext = (0, import_node_path3.extname)(f).slice(1);
11219
- if (ANALYZABLE_EXTENSIONS.has(ext)) return false;
11220
- if (REVIEWABLE_EXTENSIONS.has(ext)) return true;
11221
- const basename5 = f.split("/").pop() ?? "";
11222
- if (REVIEWABLE_FILENAMES.has(basename5)) return true;
11223
- if (REVIEWABLE_PATH_PATTERNS.some((p) => p.test(f))) return true;
11224
- return false;
11225
- });
11226
- }
11227
- function filterSecurity(files) {
11228
- return files.filter(
11229
- (f) => SECURITY_PATTERNS.some((p) => p.test(f))
11230
- );
11231
- }
11232
- function getCurrentCommit() {
11233
- return execGit("git rev-parse HEAD") || "no-git";
11234
- }
11235
- function getCurrentBranch() {
11236
- const b = execGit("git rev-parse --abbrev-ref HEAD");
11237
- return !b || b === "HEAD" ? null : b;
11238
- }
11239
- function commitResolves(sha) {
11240
- if (!sha) return false;
11241
- try {
11242
- (0, import_node_child_process3.execSync)(`git merge-base --is-ancestor ${JSON.stringify(sha)} HEAD`, {
11243
- stdio: ["pipe", "pipe", "pipe"]
11244
- });
11245
- return true;
11246
- } catch {
11247
- return false;
11248
- }
11249
- }
11250
- function commitsSincePaths(sha, paths) {
11251
- if (!sha) return null;
11252
- try {
11253
- const scope = paths.length > 0 ? ` -- ${paths.slice(0, 50).map((p) => JSON.stringify(p)).join(" ")}` : "";
11254
- const out = (0, import_node_child_process3.execSync)(`git rev-list --count ${JSON.stringify(sha)}..HEAD${scope}`, {
11255
- encoding: "utf-8",
11256
- stdio: ["pipe", "pipe", "pipe"]
11257
- }).trim();
11258
- const n = Number.parseInt(out, 10);
11259
- return Number.isFinite(n) ? n : null;
11260
- } catch {
11261
- return null;
11262
- }
11263
- }
11264
- function detectProvider(host) {
11265
- const h = host.toLowerCase();
11266
- if (h.includes("github")) return "github";
11267
- if (h.includes("gitlab")) return "gitlab";
11268
- if (h.includes("bitbucket")) return "bitbucket";
11269
- return "unknown";
11270
- }
11271
- function parseRemote(raw) {
11272
- if (!raw || typeof raw !== "string") return null;
11273
- let s = raw.trim();
11274
- if (!s) return null;
11275
- let host = "";
11276
- let path = "";
11277
- const scp = s.match(/^[^/@]+@([^:/]+):(.+)$/);
11278
- if (scp) {
11279
- host = scp[1];
11280
- path = scp[2];
11281
- } else {
11282
- s = s.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, "");
11283
- s = s.replace(/^[^/@]+@/, "");
11284
- const slash = s.indexOf("/");
11285
- if (slash === -1) return null;
11286
- host = s.slice(0, slash);
11287
- path = s.slice(slash + 1);
11415
+ }
11416
+ if (data.access_token) return { ok: true, data: data.access_token };
11417
+ switch (data.error) {
11418
+ case "authorization_pending":
11419
+ break;
11420
+ case "slow_down":
11421
+ interval += 5;
11422
+ break;
11423
+ case "access_denied":
11424
+ return { ok: false, error: "Authorization was denied on GitHub." };
11425
+ case "expired_token":
11426
+ return { ok: false, error: "The authorization code expired. Re-run register." };
11427
+ default:
11428
+ if (data.error) return { ok: false, error: `GitHub auth error: ${data.error}` };
11429
+ }
11288
11430
  }
11289
- host = host.toLowerCase().trim();
11290
- path = path.replace(/\/+$/, "").replace(/\.git$/, "");
11291
- if (!host || !path) return null;
11292
- const segments = path.split("/").filter(Boolean);
11293
- if (segments.length < 2) return null;
11294
- const owner = segments[0];
11295
- const repo = segments[segments.length - 1];
11296
- if (!owner || !repo) return null;
11297
- return {
11298
- host,
11299
- owner,
11300
- repo,
11301
- provider: detectProvider(host),
11302
- orgUrl: `https://${host}/${owner}`,
11303
- orgName: owner
11304
- };
11305
- }
11306
- function listTrackedFiles() {
11307
- const set = /* @__PURE__ */ new Set();
11308
- for (const f of splitLines(execGit("git ls-files"))) set.add(f);
11309
- for (const f of splitLines(execGit("git ls-files --others --exclude-standard"))) set.add(f);
11310
- return Array.from(set);
11431
+ return { ok: false, error: "Timed out waiting for GitHub authorization." };
11311
11432
  }
11312
11433
 
11313
11434
  // src/lib/register.ts
@@ -11392,11 +11513,81 @@ async function registerProject(opts) {
11392
11513
  }
11393
11514
  return { ok: true, data: { projectId: project_id, serviceUrl: service_url, email, userId } };
11394
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
+ }
11525
+ async function loginOnce(opts) {
11526
+ const usingTokenOverride = Boolean(process.env.VERITY_PROVIDER_TOKEN);
11527
+ const providerAuth = await githubDeviceFlow();
11528
+ if (!providerAuth.ok) {
11529
+ return { ok: false, error: providerAuth.error };
11530
+ }
11531
+ const providerToken = providerAuth.data;
11532
+ const parsed = opts.remote ? parseRemote(opts.remote) : null;
11533
+ if (!usingTokenOverride && parsed && parsed.provider === "github") {
11534
+ const installed = await ensureAppInstalled(parsed.owner, parsed.repo, providerToken);
11535
+ if (!installed.ok) {
11536
+ printWarn(installed.error);
11537
+ printInfo("Continuing login \u2014 repositories on other accounts are still granted.");
11538
+ }
11539
+ }
11540
+ const result = await apiRequest({
11541
+ method: "POST",
11542
+ path: "/auth/login",
11543
+ serviceUrl: opts.serviceUrl,
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() },
11551
+ verbose: opts.verbose,
11552
+ cmd: "login"
11553
+ });
11554
+ if (!result.ok) {
11555
+ return { ok: false, error: result.error };
11556
+ }
11557
+ const { token, service_url, user_id, user, repo_count, expires_at } = result.data;
11558
+ const loginUserId = user_id ?? user?.id ?? void 0;
11559
+ try {
11560
+ await upsertGlobalCredential("", {
11561
+ token,
11562
+ serviceUrl: service_url,
11563
+ userId: loginUserId,
11564
+ email: user?.email
11565
+ });
11566
+ } catch (err) {
11567
+ return {
11568
+ ok: false,
11569
+ error: `Logged in with the Verity service, but could not save credentials to ~/.verity/credentials: ${err.message}. Check filesystem permissions and re-run "verity login".`
11570
+ };
11571
+ }
11572
+ const pruned = await removeSupersededUserCredentials(service_url, loginUserId);
11573
+ return {
11574
+ ok: true,
11575
+ data: {
11576
+ token,
11577
+ serviceUrl: service_url,
11578
+ email: user?.email,
11579
+ userId: loginUserId,
11580
+ repoCount: repo_count ?? 0,
11581
+ prunedCredentials: pruned,
11582
+ expiresAt: expires_at
11583
+ }
11584
+ };
11585
+ }
11395
11586
 
11396
11587
  // src/commands/auth.ts
11397
11588
  function registerAuthCommands(program2) {
11398
- const auth = program2.command("auth").description("Manage project authentication");
11399
- 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) => {
11400
11591
  const globals = program2.opts();
11401
11592
  const serviceUrl = globals.serviceUrl ?? DEFAULT_SERVICE_URL;
11402
11593
  let remote = opts.remote;
@@ -11423,7 +11614,7 @@ function registerAuthCommands(program2) {
11423
11614
  if (email) printInfo(`Authenticated as: ${email}`);
11424
11615
  printJson({ project_id: projectId, service_url: resolvedUrl });
11425
11616
  });
11426
- 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 () => {
11427
11618
  const globals = program2.opts();
11428
11619
  const tokenResult = await resolveToken(globals.token);
11429
11620
  if (!tokenResult.ok) {
@@ -11449,7 +11640,7 @@ function registerAuthCommands(program2) {
11449
11640
  printInfo(`Token valid. Project: ${result.data.project_name}`);
11450
11641
  printJson(result.data);
11451
11642
  });
11452
- 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) => {
11453
11644
  const globals = program2.opts();
11454
11645
  let remote = opts.remote;
11455
11646
  if (!remote) {
@@ -11481,10 +11672,8 @@ function registerAuthCommands(program2) {
11481
11672
  }
11482
11673
 
11483
11674
  // src/commands/login.ts
11484
- var import_node_child_process5 = require("node:child_process");
11485
- var import_node_path4 = require("node:path");
11486
11675
  function registerLoginCommand(program2) {
11487
- program2.command("login").description("Log in to Verity (link your GitHub identity so runs and memory are saved)").option("--force", "Re-authenticate even if already logged in").action(async (opts) => {
11676
+ program2.command("login").description("Log in to Verity (one GitHub login grants access to all your repositories)").option("--force", "Re-authenticate even if already logged in").action(async (opts) => {
11488
11677
  const globals = program2.opts();
11489
11678
  const urlResult = await resolveServiceUrlDetailed(globals.serviceUrl);
11490
11679
  if (!urlResult.ok) {
@@ -11494,60 +11683,399 @@ function registerLoginCommand(program2) {
11494
11683
  const heal = await maybeHealServiceUrl(urlResult.data, globals.verbose);
11495
11684
  const serviceUrl = heal.serviceUrl;
11496
11685
  if (heal.healed) {
11497
- printInfo(" Completing login re-registers this project and updates ~/.verity/credentials.");
11686
+ printInfo(" Completing login updates ~/.verity/credentials against the live service.");
11687
+ }
11688
+ const remote = currentRemote();
11689
+ if (remote && !parseRemote(remote)) {
11690
+ printWarn(`This repository's origin remote is not a recognizable git URL: ${remote}`);
11691
+ printInfo(" Verity cannot identify the repository from it, so runs here will not be saved.");
11692
+ printInfo(" Point origin at the full URL (e.g. git@github.com:owner/repo.git) to fix it.");
11498
11693
  }
11499
11694
  const existing = await resolveToken(globals.token);
11500
11695
  if (existing.ok && !opts.force && !heal.healed) {
11501
- if (existing.data.userId != null) {
11502
- printInfo(`Already logged in as ${existing.data.email ?? `user #${existing.data.userId}`}. \u2713`);
11503
- printInfo(" Re-authenticate with: verity login --force");
11504
- return;
11505
- }
11506
- const who2 = await whoami(existing.data.token, serviceUrl, globals.verbose);
11507
- if (who2.ok && who2.data.logged_in) {
11508
- printInfo(`Already logged in as ${who2.data.email ?? `user #${who2.data.user_id}`}. \u2713`);
11509
- printInfo(" Re-authenticate with: verity login --force");
11510
- return;
11511
- }
11512
- if (who2.ok && who2.data.anonymous) {
11696
+ const who = await whoami(existing.data.token, serviceUrl, globals.verbose);
11697
+ if (who.ok && who.data.logged_in) {
11698
+ const nudge = reverifyNudge(who.data);
11699
+ const upgrade = await shouldUpgradeOnLogin(existing.data);
11700
+ if (!nudge && !upgrade) {
11701
+ printInfo(`Already logged in as ${who.data.email ?? `user #${who.data.user_id}`}. \u2713`);
11702
+ printInfo(" Re-authenticate with: verity login --force");
11703
+ return;
11704
+ }
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
+ }
11712
+ } else if (who.ok && who.data.anonymous) {
11513
11713
  printInfo("You have an anonymous token (the gate runs, but nothing is saved). Logging you in\u2026");
11514
- } else if (!who2.ok) {
11714
+ } else if (!who.ok) {
11515
11715
  printWarn("Could not confirm your current login state with the service \u2014 proceeding to log in.");
11516
11716
  }
11517
11717
  }
11518
- let remote = "";
11519
- try {
11520
- remote = (0, import_node_child_process5.execSync)("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
11521
- } catch {
11718
+ printInfo("Authenticating with GitHub\u2026");
11719
+ const result = await loginOnce({ serviceUrl, remote: remote || void 0, verbose: globals.verbose });
11720
+ if (!result.ok) {
11721
+ printError(`Login failed: ${result.error}`);
11722
+ process.exit(1);
11522
11723
  }
11523
- if (!remote) {
11524
- printError("verity login needs a git remote (origin) to link this repository.");
11525
- printInfo(" Add one with: git remote add origin <url>, then re-run verity login.");
11724
+ const out = result.data;
11725
+ const identity = out.email ?? (out.userId != null ? `user #${out.userId}` : "your account");
11726
+ printInfo(`Logged in as ${identity}. \u2713`);
11727
+ printInfo(` Access granted to ${out.repoCount} ${out.repoCount === 1 ? "repository" : "repositories"}.`);
11728
+ if (out.expiresAt) {
11729
+ printInfo(` This login expires on ${out.expiresAt.slice(0, 10)} \u2014 "verity login" again to renew.`);
11730
+ printInfo(' See your machines with "verity sessions list"; sign out with "verity logout".');
11731
+ }
11732
+ printInfo(" Runs, history, and cloud memory now sync to Verity everywhere you have write access.");
11733
+ if (out.prunedCredentials > 0) {
11734
+ printVerbose(`Pruned ${out.prunedCredentials} superseded per-repository credential line(s).`, globals.verbose);
11735
+ } else if (out.prunedCredentials < 0) {
11736
+ printWarn(" Could not rewrite ~/.verity/credentials: superseded per-repository tokens remain and");
11737
+ printWarn(" will keep taking precedence over this login in their own repositories.");
11738
+ printInfo(` Check the file's permissions; the next successful "verity login" retries the cleanup.`);
11739
+ }
11740
+ if (out.repoCount === 0) {
11741
+ printWarn("The Verity GitHub App is not installed on any account you can access.");
11742
+ printInfo(` Install it (and grant your repositories), then re-run verity login:`);
11743
+ printInfo(` ${githubAppInstallUrl(null)}`);
11744
+ return;
11745
+ }
11746
+ if (remote) {
11747
+ const who = await whoami(out.token, out.serviceUrl, globals.verbose);
11748
+ if (who.ok && who.data.grant_status != null) {
11749
+ printInfo(" \u2713 This repository is covered.");
11750
+ } else if (!who.ok) {
11751
+ printWarn(` Could not confirm this repository's coverage (${who.error}) \u2014 verity status will show it.`);
11752
+ } else {
11753
+ const parsed = parseRemote(remote);
11754
+ const installUrl = githubAppInstallUrl(parsed ? await githubAccountId(parsed.owner) : null);
11755
+ printWarn(` This repository (${parsed ? `${parsed.owner}/${parsed.repo}` : remote}) is NOT covered by your grants.`);
11756
+ printInfo(" Grant the Verity GitHub App access to it, then re-run verity login:");
11757
+ printInfo(` ${installUrl}`);
11758
+ }
11759
+ const rec = await readGlobalCredential(remote);
11760
+ if (rec && rec.token !== out.token) {
11761
+ const otherBackend = rec.serviceUrl != null && rec.serviceUrl !== out.serviceUrl;
11762
+ const otherIdentity = rec.userId != null && out.userId != null && rec.userId !== out.userId;
11763
+ if (otherBackend) {
11764
+ printWarn(` Note: this repository is pinned to a different Verity service (${rec.serviceUrl})`);
11765
+ printWarn(" by its own credential line, which takes precedence here \u2014 this login does not");
11766
+ printWarn(" change that. To move the repository, remove its line from ~/.verity/credentials.");
11767
+ printWarn(' Until that line is removed, "verity login" here cannot fast-path and will run');
11768
+ printWarn(" the full GitHub flow every time.");
11769
+ } else if (otherIdentity) {
11770
+ printWarn(" Note: this repository uses a different account's credential, which takes");
11771
+ printWarn(' precedence here \u2014 this login leaves it in place, and "verity login" in this');
11772
+ printWarn(" repository will report that account. Remove its line from ~/.verity/credentials");
11773
+ printWarn(" only if you want this repository on the login you just completed.");
11774
+ } else {
11775
+ const kind = rec.userId != null ? "superseded per-repository" : "anonymous project-specific";
11776
+ printWarn(` Note: this repository has a ${kind} credential that takes`);
11777
+ printWarn(" precedence here. Remove its line from ~/.verity/credentials to use your login.");
11778
+ printWarn(' Until then, "verity login" in this repository re-runs the full GitHub flow');
11779
+ printWarn(" every time.");
11780
+ }
11781
+ }
11782
+ }
11783
+ });
11784
+ }
11785
+
11786
+ // src/commands/token.ts
11787
+ function registerTokenCommand(program2) {
11788
+ const token = program2.command("token").description("Manage service (CI/machine) tokens for this repository");
11789
+ async function requireAuth(globals) {
11790
+ const tokenResult = await resolveToken(globals.token);
11791
+ if (!tokenResult.ok) {
11792
+ printError(tokenResult.error);
11526
11793
  process.exit(1);
11527
11794
  }
11528
- const projectName = parseRemote(remote)?.repo ?? (0, import_node_path4.basename)(process.cwd());
11529
- printInfo("Authenticating with GitHub\u2026");
11530
- const result = await registerProject({ projectName, remote, serviceUrl, verbose: globals.verbose });
11795
+ const urlResult = await resolveServiceUrl(globals.serviceUrl);
11796
+ if (!urlResult.ok) {
11797
+ printError(urlResult.error);
11798
+ process.exit(1);
11799
+ }
11800
+ return { bearer: tokenResult.data.token, serviceUrl: urlResult.data };
11801
+ }
11802
+ function explainDenial(error) {
11803
+ if (error.startsWith("STALE_VERIFICATION")) {
11804
+ printInfo(' Your GitHub verification has expired \u2014 run "verity login", then retry.');
11805
+ } else if (error.startsWith("FORBIDDEN")) {
11806
+ printInfo(" You need write access to this repository. If it was added recently,");
11807
+ printInfo(' run "verity login" to refresh your grants.');
11808
+ } else if (error.startsWith("INVALID_REQUEST")) {
11809
+ printInfo(" Tokens are managed per repository \u2014 run this inside a repository with a");
11810
+ printInfo(" git remote (origin), so Verity knows which project the token belongs to.");
11811
+ }
11812
+ }
11813
+ token.command("create").description("Mint a service token for CI (store it in your CI secret store as VERITY_TOKEN)").requiredOption("--name <name>", "Label for the token (e.g. github-actions)").option("--expires <days>", "Optional expiry in days (default: never)").action(async (opts) => {
11814
+ const globals = program2.opts();
11815
+ const { bearer, serviceUrl } = await requireAuth(globals);
11816
+ let expiresInDays;
11817
+ if (opts.expires != null) {
11818
+ expiresInDays = Number(opts.expires);
11819
+ if (!Number.isInteger(expiresInDays) || expiresInDays <= 0 || expiresInDays > 3650) {
11820
+ printError("--expires must be a whole number of days between 1 and 3650");
11821
+ process.exit(1);
11822
+ }
11823
+ }
11824
+ const result = await apiRequest({
11825
+ method: "POST",
11826
+ path: "/auth/tokens",
11827
+ serviceUrl,
11828
+ token: bearer,
11829
+ body: {
11830
+ agent_name: String(opts.name).trim(),
11831
+ token_type: "service",
11832
+ ...expiresInDays != null ? { expires_in_days: expiresInDays } : {}
11833
+ },
11834
+ verbose: globals.verbose,
11835
+ cmd: "token-create"
11836
+ });
11531
11837
  if (!result.ok) {
11532
- printError(`Login failed: ${result.error}`);
11838
+ printError(`Could not create the service token: ${result.error}`);
11839
+ explainDenial(result.error);
11840
+ process.exit(1);
11841
+ }
11842
+ printInfo(`Service token "${result.data.agent_name}" created. \u2713`);
11843
+ printInfo("");
11844
+ printInfo(` ${result.data.token}`);
11845
+ printInfo("");
11846
+ printWarn("This token is shown ONCE \u2014 store it now (e.g. as a VERITY_TOKEN CI secret).");
11847
+ if (result.data.expires_at) {
11848
+ printInfo(` Expires: ${result.data.expires_at.slice(0, 10)}`);
11849
+ }
11850
+ printInfo(" Any current writer of this repository can revoke it: verity token revoke <id>");
11851
+ printInfo(` Token id: ${result.data.token_id}`);
11852
+ });
11853
+ token.command("list").description("List this repository's tokens (ids and metadata only \u2014 never secrets)").action(async () => {
11854
+ const globals = program2.opts();
11855
+ const { bearer, serviceUrl } = await requireAuth(globals);
11856
+ const result = await apiRequest({
11857
+ method: "GET",
11858
+ path: "/auth/tokens",
11859
+ serviceUrl,
11860
+ token: bearer,
11861
+ verbose: globals.verbose,
11862
+ cmd: "token-list"
11863
+ });
11864
+ if (!result.ok) {
11865
+ printError(`Could not list tokens: ${result.error}`);
11866
+ explainDenial(result.error);
11867
+ process.exit(1);
11868
+ }
11869
+ const tokens = result.data.tokens ?? [];
11870
+ if (tokens.length === 0) {
11871
+ printInfo("No tokens found for this repository.");
11872
+ return;
11873
+ }
11874
+ for (const t of tokens) {
11875
+ const type = t.token_type ?? "user";
11876
+ const expires = t.expires_at ? `expires ${t.expires_at.slice(0, 10)}` : "no expiry";
11877
+ const lastUsed = t.last_used_at ? `last used ${t.last_used_at.slice(0, 10)}` : "never used";
11878
+ printInfo(`${t.id} [${type}] ${t.agent_name ?? "unnamed"} (${expires}, ${lastUsed})`);
11879
+ }
11880
+ });
11881
+ token.command("revoke <id>").description("Revoke a token by id (service tokens: any current writer may revoke)").action(async (id) => {
11882
+ const globals = program2.opts();
11883
+ const { bearer, serviceUrl } = await requireAuth(globals);
11884
+ const result = await apiRequest({
11885
+ method: "DELETE",
11886
+ path: `/auth/tokens/${encodeURIComponent(id)}`,
11887
+ serviceUrl,
11888
+ token: bearer,
11889
+ verbose: globals.verbose,
11890
+ cmd: "token-revoke"
11891
+ });
11892
+ if (!result.ok) {
11893
+ printError(`Could not revoke the token: ${result.error}`);
11894
+ explainDenial(result.error);
11895
+ process.exit(1);
11896
+ }
11897
+ printInfo(`Token ${result.data.token_id} revoked. \u2713`);
11898
+ });
11899
+ }
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);
11533
12043
  process.exit(1);
11534
12044
  }
11535
- const who = result.data.email ?? (result.data.userId != null ? `user #${result.data.userId}` : "your account");
11536
- printInfo(`Logged in as ${who}. \u2713`);
11537
- printInfo(" Runs, history, and cloud memory now sync to Verity for this project.");
11538
- if (result.data.userId == null) {
11539
- printWarn(" (Server did not return a user id \u2014 update the CLI if this persists.)");
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);
11540
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.');
11541
12069
  });
11542
12070
  }
11543
12071
 
11544
12072
  // src/lib/hooks.ts
11545
12073
  var import_promises4 = require("node:fs/promises");
11546
- var import_node_path6 = require("node:path");
12074
+ var import_node_path5 = require("node:path");
11547
12075
 
11548
12076
  // src/lib/json-file.ts
11549
12077
  var import_promises3 = require("node:fs/promises");
11550
- var import_node_path5 = require("node:path");
12078
+ var import_node_path4 = require("node:path");
11551
12079
  function jsonSemanticEqual(a, b) {
11552
12080
  if (a === b) return true;
11553
12081
  if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
@@ -11593,7 +12121,7 @@ async function writeJsonFilePreservingStyle(file, value) {
11593
12121
  const indent = currentRaw !== null ? detectJsonIndent(currentRaw) : 2;
11594
12122
  const next = JSON.stringify(value, null, indent) + "\n";
11595
12123
  if (next === currentRaw) return false;
11596
- await (0, import_promises3.mkdir)((0, import_node_path5.dirname)(file), { recursive: true });
12124
+ await (0, import_promises3.mkdir)((0, import_node_path4.dirname)(file), { recursive: true });
11597
12125
  await (0, import_promises3.writeFile)(file, next);
11598
12126
  return true;
11599
12127
  }
@@ -11766,13 +12294,13 @@ async function writeSettings(settings) {
11766
12294
  }
11767
12295
  async function readSettingsAt(root) {
11768
12296
  try {
11769
- return JSON.parse(await (0, import_promises4.readFile)((0, import_node_path6.join)(root, CLAUDE_SETTINGS_FILE), "utf-8"));
12297
+ return JSON.parse(await (0, import_promises4.readFile)((0, import_node_path5.join)(root, CLAUDE_SETTINGS_FILE), "utf-8"));
11770
12298
  } catch {
11771
12299
  return {};
11772
12300
  }
11773
12301
  }
11774
12302
  async function writeSettingsAt(root, settings) {
11775
- await writeJsonFilePreservingStyle((0, import_node_path6.join)(root, CLAUDE_SETTINGS_FILE), settings);
12303
+ await writeJsonFilePreservingStyle((0, import_node_path5.join)(root, CLAUDE_SETTINGS_FILE), settings);
11776
12304
  }
11777
12305
  async function hasLegacyHooksAt(root) {
11778
12306
  const settings = await readSettingsAt(root);
@@ -12045,7 +12573,7 @@ var import_node_crypto6 = require("node:crypto");
12045
12573
  // src/lib/conversation-buffer.ts
12046
12574
  var import_promises5 = require("node:fs/promises");
12047
12575
  var import_node_fs4 = require("node:fs");
12048
- var import_node_child_process6 = require("node:child_process");
12576
+ var import_node_child_process5 = require("node:child_process");
12049
12577
  var import_node_crypto = require("node:crypto");
12050
12578
  function stripImageReferences(text) {
12051
12579
  return text.replace(/\[Image #\d+\]/g, "[screenshot \u2014 not available for review]");
@@ -12148,7 +12676,7 @@ async function readBufferEntries() {
12148
12676
  }
12149
12677
  function getRecentCommitMessages() {
12150
12678
  try {
12151
- const output = (0, import_node_child_process6.execSync)(
12679
+ const output = (0, import_node_child_process5.execSync)(
12152
12680
  'git log --since="30 minutes ago" --format="%s" -5',
12153
12681
  { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
12154
12682
  ).trim();
@@ -12162,8 +12690,8 @@ function getRecentCommitMessages() {
12162
12690
  // src/lib/context-identity.ts
12163
12691
  var import_node_crypto2 = require("node:crypto");
12164
12692
  var import_node_fs5 = require("node:fs");
12165
- var import_node_os = require("node:os");
12166
- var import_node_path7 = require("node:path");
12693
+ var import_node_os2 = require("node:os");
12694
+ var import_node_path6 = require("node:path");
12167
12695
  var SHARED_SENTINELS = /* @__PURE__ */ new Set([
12168
12696
  "",
12169
12697
  "-",
@@ -12217,13 +12745,13 @@ function contextIdentity(input) {
12217
12745
  }
12218
12746
  function verityHome() {
12219
12747
  const override = process.env.VERITY_HOME;
12220
- return override && override.trim() ? (0, import_node_path7.resolve)(override) : (0, import_node_path7.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");
12221
12749
  }
12222
12750
  function dossierDir(identity) {
12223
- return (0, import_node_path7.join)(verityHome(), "sessions", identity.userKey, identity.treeKey, identity.sessionKey);
12751
+ return (0, import_node_path6.join)(verityHome(), "sessions", identity.userKey, identity.treeKey, identity.sessionKey);
12224
12752
  }
12225
12753
  function treeDir(identity) {
12226
- return (0, import_node_path7.join)(verityHome(), "sessions", identity.userKey, identity.treeKey);
12754
+ return (0, import_node_path6.join)(verityHome(), "sessions", identity.userKey, identity.treeKey);
12227
12755
  }
12228
12756
  function scopeIdentity(token, sessionId) {
12229
12757
  const t = (token ?? "").trim();
@@ -12239,7 +12767,7 @@ function sessionScopeKey(token, sessionId) {
12239
12767
  // src/lib/task-context-buffer.ts
12240
12768
  var import_promises6 = require("node:fs/promises");
12241
12769
  var import_node_fs6 = require("node:fs");
12242
- var import_node_path8 = require("node:path");
12770
+ var import_node_path7 = require("node:path");
12243
12771
  var TASK_CONTEXT_DIR = `${VERITY_DIR}/.task-context`;
12244
12772
  var MAX_BUFFER_BYTES = 500 * 1024;
12245
12773
  var MAX_PROMPT_CHARS = 2e3;
@@ -12317,7 +12845,7 @@ async function cleanupTaskContextBuffers() {
12317
12845
  const cutoffMs = Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1e3;
12318
12846
  for (const file of files) {
12319
12847
  if (!file.endsWith(".jsonl")) continue;
12320
- const filePath = (0, import_node_path8.join)(TASK_CONTEXT_DIR, file);
12848
+ const filePath = (0, import_node_path7.join)(TASK_CONTEXT_DIR, file);
12321
12849
  try {
12322
12850
  const stats = await (0, import_promises6.stat)(filePath);
12323
12851
  if (stats.mtimeMs < cutoffMs) {
@@ -12331,7 +12859,7 @@ async function cleanupTaskContextBuffers() {
12331
12859
  }
12332
12860
  function bufferPath(taskId) {
12333
12861
  const safe = taskId.replace(/[^a-zA-Z0-9_-]/g, "");
12334
- return (0, import_node_path8.join)(TASK_CONTEXT_DIR, `${safe}.jsonl`);
12862
+ return (0, import_node_path7.join)(TASK_CONTEXT_DIR, `${safe}.jsonl`);
12335
12863
  }
12336
12864
  async function appendEntry(taskId, entry) {
12337
12865
  try {
@@ -12357,7 +12885,7 @@ async function appendEntry(taskId, entry) {
12357
12885
  // src/lib/memory-retrieval.ts
12358
12886
  var import_promises7 = require("node:fs/promises");
12359
12887
  var import_node_fs7 = require("node:fs");
12360
- var import_node_path9 = require("node:path");
12888
+ var import_node_path8 = require("node:path");
12361
12889
  var memoryDir = () => projectPath(`${VERITY_DIR}/memory`);
12362
12890
  var DOMAINS = ["decisions", "quality", "security", "intent", "gotchas", "patterns", "domain", "integrations"];
12363
12891
  var DEFAULT_BUDGET_TOKENS = 2e3;
@@ -12455,14 +12983,14 @@ async function retrieveForInjection(promptText, taskFiles = [], budgetTokens = D
12455
12983
  const promptTokens = tokenize(promptText);
12456
12984
  const nodes = [];
12457
12985
  for (const domain of DOMAINS) {
12458
- const domainDir = (0, import_node_path9.join)(memoryDir(), domain);
12986
+ const domainDir = (0, import_node_path8.join)(memoryDir(), domain);
12459
12987
  if (!(0, import_node_fs7.existsSync)(domainDir)) continue;
12460
12988
  try {
12461
12989
  const files = await (0, import_promises7.readdir)(domainDir);
12462
12990
  for (const file of files) {
12463
12991
  if (!file.endsWith(".md")) continue;
12464
12992
  try {
12465
- const content = await (0, import_promises7.readFile)((0, import_node_path9.join)(domainDir, file), "utf-8");
12993
+ const content = await (0, import_promises7.readFile)((0, import_node_path8.join)(domainDir, file), "utf-8");
12466
12994
  const { fm, body } = parseFrontmatter(content);
12467
12995
  if (fm.status && fm.status !== "active") continue;
12468
12996
  nodes.push({
@@ -12522,7 +13050,7 @@ async function retrieveForInjection(promptText, taskFiles = [], budgetTokens = D
12522
13050
  // src/lib/memory-sync.ts
12523
13051
  var import_promises8 = require("node:fs/promises");
12524
13052
  var import_node_fs8 = require("node:fs");
12525
- var import_node_path10 = require("node:path");
13053
+ var import_node_path9 = require("node:path");
12526
13054
  var import_node_crypto3 = require("node:crypto");
12527
13055
 
12528
13056
  // src/lib/glob-match.ts
@@ -12593,16 +13121,16 @@ var syncStateFile = () => projectPath(`${VERITY_DIR}/.memory-sync-state.json`);
12593
13121
  async function ensureMemoryDir() {
12594
13122
  await (0, import_promises8.mkdir)(memoryDir2(), { recursive: true });
12595
13123
  for (const domain of DOMAINS2) {
12596
- await (0, import_promises8.mkdir)((0, import_node_path10.join)(memoryDir2(), domain), { recursive: true });
13124
+ await (0, import_promises8.mkdir)((0, import_node_path9.join)(memoryDir2(), domain), { recursive: true });
12597
13125
  }
12598
- if (!(0, import_node_fs8.existsSync)((0, import_node_path10.join)(memoryDir2(), "SCHEMA.md"))) {
12599
- await (0, import_promises8.writeFile)((0, import_node_path10.join)(memoryDir2(), "SCHEMA.md"), SCHEMA_TEMPLATE);
13126
+ if (!(0, import_node_fs8.existsSync)((0, import_node_path9.join)(memoryDir2(), "SCHEMA.md"))) {
13127
+ await (0, import_promises8.writeFile)((0, import_node_path9.join)(memoryDir2(), "SCHEMA.md"), SCHEMA_TEMPLATE);
12600
13128
  }
12601
- if (!(0, import_node_fs8.existsSync)((0, import_node_path10.join)(memoryDir2(), "index.md"))) {
12602
- await (0, import_promises8.writeFile)((0, import_node_path10.join)(memoryDir2(), "index.md"), "# Project Memory Index\n\nNo nodes yet. Run an analysis to start building the knowledge graph.\n");
13129
+ if (!(0, import_node_fs8.existsSync)((0, import_node_path9.join)(memoryDir2(), "index.md"))) {
13130
+ await (0, import_promises8.writeFile)((0, import_node_path9.join)(memoryDir2(), "index.md"), "# Project Memory Index\n\nNo nodes yet. Run an analysis to start building the knowledge graph.\n");
12603
13131
  }
12604
- if (!(0, import_node_fs8.existsSync)((0, import_node_path10.join)(memoryDir2(), "log.md"))) {
12605
- await (0, import_promises8.writeFile)((0, import_node_path10.join)(memoryDir2(), "log.md"), "# Memory Log\n\n");
13132
+ if (!(0, import_node_fs8.existsSync)((0, import_node_path9.join)(memoryDir2(), "log.md"))) {
13133
+ await (0, import_promises8.writeFile)((0, import_node_path9.join)(memoryDir2(), "log.md"), "# Memory Log\n\n");
12606
13134
  }
12607
13135
  }
12608
13136
  async function buildManifest() {
@@ -12611,14 +13139,14 @@ async function buildManifest() {
12611
13139
  }
12612
13140
  const nodes = [];
12613
13141
  for (const domain of DOMAINS2) {
12614
- const domainDir = (0, import_node_path10.join)(memoryDir2(), domain);
13142
+ const domainDir = (0, import_node_path9.join)(memoryDir2(), domain);
12615
13143
  if (!(0, import_node_fs8.existsSync)(domainDir)) continue;
12616
13144
  try {
12617
13145
  const files = await (0, import_promises8.readdir)(domainDir);
12618
13146
  for (const file of files) {
12619
13147
  if (!file.endsWith(".md")) continue;
12620
13148
  const filePath = `${domain}/${file}`;
12621
- const fullPath = (0, import_node_path10.join)(memoryDir2(), filePath);
13149
+ const fullPath = (0, import_node_path9.join)(memoryDir2(), filePath);
12622
13150
  try {
12623
13151
  const content = await (0, import_promises8.readFile)(fullPath, "utf-8");
12624
13152
  const hash = (0, import_node_crypto3.createHash)("sha256").update(content).digest("hex").slice(0, 16);
@@ -12631,13 +13159,13 @@ async function buildManifest() {
12631
13159
  }
12632
13160
  let indexHash = null;
12633
13161
  try {
12634
- const indexContent = await (0, import_promises8.readFile)((0, import_node_path10.join)(memoryDir2(), "index.md"), "utf-8");
13162
+ const indexContent = await (0, import_promises8.readFile)((0, import_node_path9.join)(memoryDir2(), "index.md"), "utf-8");
12635
13163
  indexHash = `sha256:${(0, import_node_crypto3.createHash)("sha256").update(indexContent).digest("hex").slice(0, 16)}`;
12636
13164
  } catch {
12637
13165
  }
12638
13166
  let logLength = 0;
12639
13167
  try {
12640
- const logContent = await (0, import_promises8.readFile)((0, import_node_path10.join)(memoryDir2(), "log.md"), "utf-8");
13168
+ const logContent = await (0, import_promises8.readFile)((0, import_node_path9.join)(memoryDir2(), "log.md"), "utf-8");
12641
13169
  logLength = logContent.split("\n").length;
12642
13170
  } catch {
12643
13171
  }
@@ -12650,13 +13178,13 @@ async function readOnDiskNodes() {
12650
13178
  const out = /* @__PURE__ */ new Map();
12651
13179
  if (!(0, import_node_fs8.existsSync)(memoryDir2())) return out;
12652
13180
  for (const domain of DOMAINS2) {
12653
- const domainDir = (0, import_node_path10.join)(memoryDir2(), domain);
13181
+ const domainDir = (0, import_node_path9.join)(memoryDir2(), domain);
12654
13182
  if (!(0, import_node_fs8.existsSync)(domainDir)) continue;
12655
13183
  try {
12656
13184
  for (const file of await (0, import_promises8.readdir)(domainDir)) {
12657
13185
  if (!file.endsWith(".md")) continue;
12658
13186
  try {
12659
- out.set(`${domain}/${file}`, hashContent(await (0, import_promises8.readFile)((0, import_node_path10.join)(domainDir, file), "utf-8")));
13187
+ out.set(`${domain}/${file}`, hashContent(await (0, import_promises8.readFile)((0, import_node_path9.join)(domainDir, file), "utf-8")));
12660
13188
  } catch {
12661
13189
  }
12662
13190
  }
@@ -12702,7 +13230,7 @@ async function computeEditedNodeUploads() {
12702
13230
  const uploads = [];
12703
13231
  for (const [path, prevHash] of prev) {
12704
13232
  if (prevHash == null) continue;
12705
- const full = (0, import_node_path10.join)(memoryDir2(), path);
13233
+ const full = (0, import_node_path9.join)(memoryDir2(), path);
12706
13234
  if (!(0, import_node_fs8.existsSync)(full)) continue;
12707
13235
  let content;
12708
13236
  try {
@@ -12739,15 +13267,15 @@ async function applyMemoryWrites(writes, opts = {}) {
12739
13267
  const logLines = [`- ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)} \u2014 Applied ${count} write(s) from server`];
12740
13268
  for (const n of notes) logLines.push(` - ${n}`);
12741
13269
  try {
12742
- const existing = (0, import_node_fs8.existsSync)((0, import_node_path10.join)(memoryDir2(), "log.md")) ? await (0, import_promises8.readFile)((0, import_node_path10.join)(memoryDir2(), "log.md"), "utf-8") : "# Memory Log\n\n";
12743
- await (0, import_promises8.writeFile)((0, import_node_path10.join)(memoryDir2(), "log.md"), existing + logLines.join("\n") + "\n");
13270
+ const existing = (0, import_node_fs8.existsSync)((0, import_node_path9.join)(memoryDir2(), "log.md")) ? await (0, import_promises8.readFile)((0, import_node_path9.join)(memoryDir2(), "log.md"), "utf-8") : "# Memory Log\n\n";
13271
+ await (0, import_promises8.writeFile)((0, import_node_path9.join)(memoryDir2(), "log.md"), existing + logLines.join("\n") + "\n");
12744
13272
  } catch {
12745
13273
  }
12746
13274
  await recordSyncedNodePaths();
12747
13275
  return count;
12748
13276
  }
12749
13277
  async function applyOneWrite(write, treePaths) {
12750
- const fullPath = (0, import_node_path10.join)(memoryDir2(), write.path);
13278
+ const fullPath = (0, import_node_path9.join)(memoryDir2(), write.path);
12751
13279
  const notes = [];
12752
13280
  let content = write.content;
12753
13281
  if (treePaths && treePaths.length > 0) {
@@ -12769,7 +13297,7 @@ async function applyOneWrite(write, treePaths) {
12769
13297
  return { written: false, notes };
12770
13298
  }
12771
13299
  }
12772
- await (0, import_promises8.mkdir)((0, import_node_path10.dirname)(fullPath), { recursive: true });
13300
+ await (0, import_promises8.mkdir)((0, import_node_path9.dirname)(fullPath), { recursive: true });
12773
13301
  await (0, import_promises8.writeFile)(fullPath, content);
12774
13302
  return { written: true, notes };
12775
13303
  }
@@ -12810,7 +13338,7 @@ async function regenerateIndex() {
12810
13338
  ];
12811
13339
  let totalNodes = 0;
12812
13340
  for (const domain of DOMAINS2.filter((d) => d !== "_archive")) {
12813
- const domainDir = (0, import_node_path10.join)(memoryDir2(), domain);
13341
+ const domainDir = (0, import_node_path9.join)(memoryDir2(), domain);
12814
13342
  if (!(0, import_node_fs8.existsSync)(domainDir)) continue;
12815
13343
  try {
12816
13344
  const files = await (0, import_promises8.readdir)(domainDir);
@@ -12821,7 +13349,7 @@ async function regenerateIndex() {
12821
13349
  for (const file of mdFiles.sort()) {
12822
13350
  const slug = file.replace(/\.md$/, "");
12823
13351
  try {
12824
- const content = await (0, import_promises8.readFile)((0, import_node_path10.join)(domainDir, file), "utf-8");
13352
+ const content = await (0, import_promises8.readFile)((0, import_node_path9.join)(domainDir, file), "utf-8");
12825
13353
  const title = pickFrontmatter(content, "title") ?? slug;
12826
13354
  const kind = pickFrontmatter(content, "kind") ?? "-";
12827
13355
  const confidence = pickFrontmatter(content, "confidence");
@@ -12845,7 +13373,7 @@ async function regenerateIndex() {
12845
13373
  lines.push("No nodes yet. Run an analysis to start building the knowledge graph.");
12846
13374
  }
12847
13375
  const next = lines.join("\n") + "\n";
12848
- const indexPath = (0, import_node_path10.join)(memoryDir2(), "index.md");
13376
+ const indexPath = (0, import_node_path9.join)(memoryDir2(), "index.md");
12849
13377
  let existing = null;
12850
13378
  try {
12851
13379
  existing = await (0, import_promises8.readFile)(indexPath, "utf-8");
@@ -12927,7 +13455,7 @@ function hasLegacyMemoryBlock(text) {
12927
13455
  return findMarker(text, LEGACY_MD_START) !== -1;
12928
13456
  }
12929
13457
  async function ensureClaudeMdPointer(cwd = repoRoot()) {
12930
- const claudeMdPath = (0, import_node_path10.join)(cwd, "CLAUDE.md");
13458
+ const claudeMdPath = (0, import_node_path9.join)(cwd, "CLAUDE.md");
12931
13459
  let existing = "";
12932
13460
  if ((0, import_node_fs8.existsSync)(claudeMdPath)) {
12933
13461
  existing = await (0, import_promises8.readFile)(claudeMdPath, "utf-8");
@@ -13061,12 +13589,76 @@ Body content (\u22648KB). Use [[node-id]] wikilinks for cross-references.
13061
13589
  // src/lib/dossier-session.ts
13062
13590
  var import_node_fs10 = require("node:fs");
13063
13591
  var import_node_crypto5 = require("node:crypto");
13064
- var import_node_path12 = require("node:path");
13592
+ var import_node_path11 = require("node:path");
13593
+
13594
+ // src/lib/skip-detection.ts
13595
+ function isBareAckPrompt(prompt) {
13596
+ if (typeof prompt !== "string") return false;
13597
+ const trimmed = prompt.trim();
13598
+ if (trimmed.length === 0) return false;
13599
+ if (trimmed.length > 20) return false;
13600
+ const bareAckPattern = /^(\d{1,2}|y|n|yes|no|yep|nope|ok(ay)?|sure|skip|cancel|stop|done|noted|got\s+it|sounds\s+good|thanks|thank\s+you|thx)[.!?]*$/i;
13601
+ return bareAckPattern.test(trimmed);
13602
+ }
13603
+ function isContinuationPrompt(prompt) {
13604
+ if (typeof prompt !== "string") return false;
13605
+ const trimmed = prompt.trim();
13606
+ if (trimmed.length === 0) return false;
13607
+ if (trimmed.length > 24) return false;
13608
+ const continuation = /^(let['’]?s\s+(go|do\s+it|start|continue)|go|go\s+ahead|go\s+on|proceed|continue|carry\s+on|keep\s+going|do\s+it|make\s+it\s+so|next|start|begin|ship\s+it|yes\s+please|please\s+continue|perfect|great|nice|excellent|agreed)[.!]*$/i;
13609
+ return continuation.test(trimmed) || isBareAckPrompt(trimmed);
13610
+ }
13611
+ function resolveGoalPrompt(prompts) {
13612
+ if (prompts.length === 0) return null;
13613
+ const latest = prompts[prompts.length - 1];
13614
+ if (!isContinuationPrompt(latest.prompt)) return { entry: latest, turnsBack: 0 };
13615
+ for (let i = prompts.length - 2; i >= 0; i--) {
13616
+ if (!isContinuationPrompt(prompts[i].prompt)) {
13617
+ return { entry: prompts[i], turnsBack: prompts.length - 1 - i };
13618
+ }
13619
+ }
13620
+ return { entry: latest, turnsBack: 0 };
13621
+ }
13622
+ function isReflectionQuestion(response) {
13623
+ if (!response || typeof response !== "string") return false;
13624
+ const markers = [
13625
+ /reflection\s+for\s+future\s+agents/i,
13626
+ /what(?:'s|\s+is)\s+one\s+thing\s+you\s+learned/i,
13627
+ /say\s+['"]?skip['"]?\s+to\s+skip/i,
13628
+ /quick\s+reflection\s+question/i,
13629
+ // Post-flip (VRT-21): the agent drafts the reflection itself and, when
13630
+ // interactive, asks the user to confirm/correct before recording. That
13631
+ // turn authors no code either, so it's still a reflection turn.
13632
+ /reflection\s+draft/i,
13633
+ /confirm,?\s+correct,?\s+or\s+add/i
13634
+ ];
13635
+ return markers.some((m) => m.test(response));
13636
+ }
13637
+ function isMetaTaskLabel(label2) {
13638
+ if (label2 === null || label2 === void 0) return false;
13639
+ if (typeof label2 !== "string") return false;
13640
+ const trimmed = label2.trim();
13641
+ if (trimmed.length === 0) return true;
13642
+ const metaPatterns = [
13643
+ /^verity\s+[\w-]+\s+response$/i,
13644
+ // "Verity reflect response"
13645
+ /^simple user response$/i,
13646
+ /^verity\s+command$/i,
13647
+ // "Verity command"
13648
+ /^user\s+(question|reply|response|ack)$/i
13649
+ ];
13650
+ return metaPatterns.some((p) => p.test(trimmed));
13651
+ }
13652
+ function shouldSkipForBareAck(input) {
13653
+ if (!isBareAckPrompt(input.prompt)) return false;
13654
+ if (input.turnAuthoredCode) return false;
13655
+ return input.canSeeTurnAuthorship;
13656
+ }
13065
13657
 
13066
13658
  // src/lib/dossier.ts
13067
13659
  var import_node_fs9 = require("node:fs");
13068
13660
  var import_node_crypto4 = require("node:crypto");
13069
- var import_node_path11 = require("node:path");
13661
+ var import_node_path10 = require("node:path");
13070
13662
  var MAX_LINE_BYTES = 4096;
13071
13663
  var MAX_GOAL_CHARS = 2e3;
13072
13664
  var GOAL_KEEP = 8;
@@ -13109,9 +13701,9 @@ function openDossier(identity) {
13109
13701
  return {
13110
13702
  dir,
13111
13703
  identity,
13112
- eventsPath: (0, import_node_path11.join)(dir, "events.jsonl"),
13113
- foldPath: (0, import_node_path11.join)(dir, "fold.json"),
13114
- rotatedDir: (0, import_node_path11.join)(dir, "rotated")
13704
+ eventsPath: (0, import_node_path10.join)(dir, "events.jsonl"),
13705
+ foldPath: (0, import_node_path10.join)(dir, "fold.json"),
13706
+ rotatedDir: (0, import_node_path10.join)(dir, "rotated")
13115
13707
  };
13116
13708
  } catch {
13117
13709
  return null;
@@ -13184,11 +13776,11 @@ function rotateIfNeeded2(d) {
13184
13776
  if (!(0, import_node_fs9.existsSync)(d.eventsPath)) return;
13185
13777
  if ((0, import_node_fs9.statSync)(d.eventsPath).size < ROTATE_BYTES) return;
13186
13778
  (0, import_node_fs9.mkdirSync)(d.rotatedDir, { recursive: true, mode: 448 });
13187
- (0, import_node_fs9.renameSync)(d.eventsPath, (0, import_node_path11.join)(d.rotatedDir, `events.${Date.now()}.jsonl`));
13779
+ (0, import_node_fs9.renameSync)(d.eventsPath, (0, import_node_path10.join)(d.rotatedDir, `events.${Date.now()}.jsonl`));
13188
13780
  const kept = (0, import_node_fs9.readdirSync)(d.rotatedDir).filter((f) => f.endsWith(".jsonl")).sort();
13189
13781
  for (const stale of kept.slice(0, Math.max(0, kept.length - ROTATE_KEEP))) {
13190
13782
  try {
13191
- (0, import_node_fs9.renameSync)((0, import_node_path11.join)(d.rotatedDir, stale), (0, import_node_path11.join)(d.rotatedDir, `${stale}.pruned`));
13783
+ (0, import_node_fs9.renameSync)((0, import_node_path10.join)(d.rotatedDir, stale), (0, import_node_path10.join)(d.rotatedDir, `${stale}.pruned`));
13192
13784
  } catch {
13193
13785
  }
13194
13786
  }
@@ -13250,7 +13842,7 @@ function foldDossier(d, opts = {}) {
13250
13842
  state.meta.rotations = files.length;
13251
13843
  for (const f of files) {
13252
13844
  try {
13253
- ingest((0, import_node_fs9.readFileSync)((0, import_node_path11.join)(d.rotatedDir, f), "utf8"));
13845
+ ingest((0, import_node_fs9.readFileSync)((0, import_node_path10.join)(d.rotatedDir, f), "utf8"));
13254
13846
  } catch {
13255
13847
  state.meta.dropped_lines++;
13256
13848
  }
@@ -13311,6 +13903,13 @@ function reduce(state, events, now) {
13311
13903
  });
13312
13904
  break;
13313
13905
  }
13906
+ case "goal_delivered": {
13907
+ const g = state.goal.find((x) => x.status === "active");
13908
+ if (!g) break;
13909
+ if (g.delivered) break;
13910
+ g.delivered = { at: ev.at, seq: ev.seq, summary: ev.summary };
13911
+ break;
13912
+ }
13314
13913
  case "authored": {
13315
13914
  authoredEvents++;
13316
13915
  const e = byPath.get(ev.path) ?? {
@@ -13425,6 +14024,18 @@ function reduce(state, events, now) {
13425
14024
  }
13426
14025
  case "verdict": {
13427
14026
  state.meta.last_verdict_seq = ev.seq;
14027
+ state.meta.channel = {
14028
+ emittedLast: ev.emitted === true,
14029
+ // Reset by ANY movement, so the counter measures a standstill rather
14030
+ // than session length.
14031
+ consecutiveIdle: ev.idle === false ? 0 : (state.meta.channel?.consecutiveIdle ?? 0) + 1
14032
+ };
14033
+ state.meta.last_adjudication = ev.intent_verdict ? { verdict: ev.intent_verdict, score: ev.intent_score ?? null, at: ev.at, decision: ev.decision } : void 0;
14034
+ if (ev.intent_sig) {
14035
+ 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 };
14036
+ } else {
14037
+ state.meta.intent_repeat = void 0;
14038
+ }
13428
14039
  state.meta.watermark = {
13429
14040
  sha: ev.head_sha,
13430
14041
  reviewed_hash: ev.watermark_sha,
@@ -13510,7 +14121,12 @@ function compactState(s) {
13510
14121
  const ms = (iso) => Date.parse(iso) || 0;
13511
14122
  return {
13512
14123
  v: 1,
13513
- 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]),
14124
+ // APPEND-ONLY POSITIONALLY. Indices 11-13 carry `delivered`; a cache row
14125
+ // written before it existed has length 11 and expands with `delivered`
14126
+ // absent, which is the correct reading of "nothing had been delivered yet".
14127
+ // Inserting rather than appending would silently re-interpret every existing
14128
+ // cached row.
14129
+ 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]),
13514
14130
  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,
13515
14131
  n: s.not_mine?.map((n) => [n.path, n.reason, ms(n.at), n.head_sha]) ?? null,
13516
14132
  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]),
@@ -13556,7 +14172,14 @@ function expandState(raw) {
13556
14172
  ...x[7] ? { status: x[7] } : {},
13557
14173
  ...x[8] ? { repeats: x[8] } : {},
13558
14174
  ...x[9] ? { truncated: true } : {},
13559
- ...x[10] ? { collapsed: true } : {}
14175
+ ...x[10] ? { collapsed: true } : {},
14176
+ ...x[11] ? {
14177
+ delivered: {
14178
+ at: iso(x[11]),
14179
+ seq: x[12] ?? 0,
14180
+ summary: x[13] || ""
14181
+ }
14182
+ } : {}
13560
14183
  })),
13561
14184
  authored: decodedAuthored,
13562
14185
  // The cache stores the BOUNDED list, so this is the bounded list too. That
@@ -13770,9 +14393,18 @@ function projectMemory(state, opts) {
13770
14393
  seq: active.seq,
13771
14394
  superseded,
13772
14395
  truncated: active.truncated === true,
13773
- collapsed: state.meta.collapsed.goal ?? 0
14396
+ collapsed: state.meta.collapsed.goal ?? 0,
14397
+ ...active.delivered && { delivered: active.delivered }
13774
14398
  };
13775
14399
  }
14400
+ if (opts.capture) {
14401
+ const missed = Math.max(0, opts.capture.seen - opts.capture.captured);
14402
+ p.capture = { seen: opts.capture.seen, captured: opts.capture.captured, missed };
14403
+ }
14404
+ if (state.meta.last_adjudication) {
14405
+ const a = state.meta.last_adjudication;
14406
+ p.last_adjudication = { verdict: a.verdict, score: a.score, at: a.at };
14407
+ }
13776
14408
  const ageOf = (seq, carried) => carried ? "carried" : seq > lastVerdictSeq ? "this_turn" : "this_session";
13777
14409
  if (opts.spoken.length > 0) {
13778
14410
  p.statements = opts.spoken.map((s) => ({
@@ -14015,7 +14647,8 @@ function recall(d, input) {
14015
14647
  continuity,
14016
14648
  spoken: reanchored.spoken,
14017
14649
  refused: reanchored.dropped.length,
14018
- lastVerdictSeq
14650
+ lastVerdictSeq,
14651
+ ...input.capture && { capture: input.capture }
14019
14652
  });
14020
14653
  return {
14021
14654
  state: effective,
@@ -14046,16 +14679,16 @@ function foreignAuthoredPaths(identity, opts = {}) {
14046
14679
  for (const entry of (0, import_node_fs10.readdirSync)(dir, { withFileTypes: true })) {
14047
14680
  if (!entry.isDirectory()) continue;
14048
14681
  if (entry.name === identity.sessionKey) continue;
14049
- const log = (0, import_node_path12.join)(dir, entry.name, "events.jsonl");
14682
+ const log = (0, import_node_path11.join)(dir, entry.name, "events.jsonl");
14050
14683
  try {
14051
14684
  if (!(0, import_node_fs10.existsSync)(log)) continue;
14052
14685
  if (now - (0, import_node_fs10.statSync)(log).mtimeMs > windowMs) continue;
14053
14686
  const sib = {
14054
- dir: (0, import_node_path12.join)(dir, entry.name),
14687
+ dir: (0, import_node_path11.join)(dir, entry.name),
14055
14688
  identity,
14056
14689
  eventsPath: log,
14057
- foldPath: (0, import_node_path12.join)(dir, entry.name, "fold.json"),
14058
- rotatedDir: (0, import_node_path12.join)(dir, entry.name, "rotated")
14690
+ foldPath: (0, import_node_path11.join)(dir, entry.name, "fold.json"),
14691
+ rotatedDir: (0, import_node_path11.join)(dir, entry.name, "rotated")
14059
14692
  };
14060
14693
  const state = readFoldCache(sib) ?? foldDossier(sib);
14061
14694
  sessions++;
@@ -14083,22 +14716,22 @@ function pruneOldDossiers(identity, maxAgeMs = 7 * 24 * 60 * 60 * 1e3) {
14083
14716
  let removed = 0;
14084
14717
  try {
14085
14718
  const mine = dossierDir(identity);
14086
- const userDir = (0, import_node_path12.dirname)((0, import_node_path12.dirname)(mine));
14719
+ const userDir = (0, import_node_path11.dirname)((0, import_node_path11.dirname)(mine));
14087
14720
  if (!(0, import_node_fs10.existsSync)(userDir)) return 0;
14088
14721
  const cutoff = Date.now() - maxAgeMs;
14089
14722
  for (const tree of (0, import_node_fs10.readdirSync)(userDir, { withFileTypes: true })) {
14090
14723
  if (!tree.isDirectory()) continue;
14091
- const treePath = (0, import_node_path12.join)(userDir, tree.name);
14724
+ const treePath = (0, import_node_path11.join)(userDir, tree.name);
14092
14725
  let live = 0;
14093
14726
  for (const entry of (0, import_node_fs10.readdirSync)(treePath, { withFileTypes: true })) {
14094
14727
  if (!entry.isDirectory()) continue;
14095
- const dir = (0, import_node_path12.join)(treePath, entry.name);
14728
+ const dir = (0, import_node_path11.join)(treePath, entry.name);
14096
14729
  if (dir === mine) {
14097
14730
  live++;
14098
14731
  continue;
14099
14732
  }
14100
14733
  try {
14101
- const log = (0, import_node_path12.join)(dir, "events.jsonl");
14734
+ const log = (0, import_node_path11.join)(dir, "events.jsonl");
14102
14735
  const at = (0, import_node_fs10.existsSync)(log) ? (0, import_node_fs10.statSync)(log).mtimeMs : (0, import_node_fs10.statSync)(dir).mtimeMs;
14103
14736
  if (at < cutoff) {
14104
14737
  (0, import_node_fs10.rmSync)(dir, { recursive: true, force: true });
@@ -14126,7 +14759,19 @@ function sessionDossier(token, sessionId) {
14126
14759
  const d = openDossier(identity);
14127
14760
  return d ? { d, identity } : null;
14128
14761
  }
14762
+ function hasActiveGoal(d) {
14763
+ try {
14764
+ if (!(0, import_node_fs10.existsSync)(d.eventsPath)) return false;
14765
+ return (0, import_node_fs10.readFileSync)(d.eventsPath, "utf8").includes('"k":"goal"');
14766
+ } catch {
14767
+ return false;
14768
+ }
14769
+ }
14129
14770
  function recordGoal(d, prompt, source = "prompt") {
14771
+ if (source === "prompt" && isContinuationPrompt(prompt) && hasActiveGoal(d)) {
14772
+ appendEvent(d, { k: "goal_continue", text: prompt.slice(0, 64) });
14773
+ return;
14774
+ }
14130
14775
  const text = prompt.slice(0, MAX_GOAL_CHARS);
14131
14776
  appendEvent(d, {
14132
14777
  k: "goal",
@@ -14141,7 +14786,7 @@ function recordTurn(d, t) {
14141
14786
  for (const a of t.authored) {
14142
14787
  const origin = a.owner === "subagent" ? "subagent" : "edit_tool";
14143
14788
  const prior = t.known?.authored?.get(a.p);
14144
- const hash = fileHash((0, import_node_path12.join)(root, a.p));
14789
+ const hash = fileHash((0, import_node_path11.join)(root, a.p));
14145
14790
  const hunks = Math.max(0, a.h - (prior?.hunks ?? 0));
14146
14791
  const adds = Math.max(0, a.a - (prior?.adds ?? 0));
14147
14792
  const dels = Math.max(0, a.d - (prior?.dels ?? 0));
@@ -14174,7 +14819,7 @@ function recordTurn(d, t) {
14174
14819
  }
14175
14820
  const seenDivergence = t.known?.divergence ?? /* @__PURE__ */ new Set();
14176
14821
  for (const u of t.unobserved) {
14177
- const hash = fileHash((0, import_node_path12.join)(root, u.p));
14822
+ const hash = fileHash((0, import_node_path11.join)(root, u.p));
14178
14823
  if (seenDivergence.has(divergenceKey(u.p, hash))) continue;
14179
14824
  appendEvent(d, { k: "divergence", kind: "external_mutation", path: u.p, hash });
14180
14825
  }
@@ -14198,12 +14843,16 @@ function toStatus(n) {
14198
14843
  }
14199
14844
  function recordVerdict(d, v) {
14200
14845
  const root = repoRoot();
14846
+ if (v.intent?.verdict === "aligned") {
14847
+ const summary = (v.intent.implemented ?? "").trim().slice(0, 400);
14848
+ if (summary) appendEvent(d, { k: "goal_delivered", summary });
14849
+ }
14201
14850
  const lines = /* @__PURE__ */ new Map();
14202
14851
  for (const f of v.findings) {
14203
14852
  if (!f.file || typeof f.line !== "number" || !f.pattern_id) continue;
14204
14853
  if (!lines.has(f.file)) {
14205
14854
  try {
14206
- const abs = (0, import_node_path12.join)(root, f.file);
14855
+ const abs = (0, import_node_path11.join)(root, f.file);
14207
14856
  lines.set(f.file, (0, import_node_fs10.existsSync)(abs) ? (0, import_node_fs10.readFileSync)(abs, "utf8").split("\n") : null);
14208
14857
  } catch {
14209
14858
  lines.set(f.file, null);
@@ -14225,15 +14874,32 @@ function recordVerdict(d, v) {
14225
14874
  line_sha: at !== void 0 ? lineSha(at) : null
14226
14875
  });
14227
14876
  }
14877
+ const foldedNow = foldDossier(d);
14878
+ const sig = intentSignature(v.intent, {
14879
+ goalSeq: foldedNow.goal.find((g) => g.status === "active")?.seq ?? 0,
14880
+ idle: v.idle !== false
14881
+ });
14228
14882
  appendEvent(d, {
14229
14883
  k: "verdict",
14230
14884
  run_id: v.runId,
14231
14885
  head_sha: getCurrentCommit(),
14232
14886
  watermark_sha: v.watermarkSha,
14233
14887
  branch: v.branch,
14234
- decision: v.decision
14888
+ decision: v.decision,
14889
+ ...sig && { intent_sig: sig },
14890
+ emitted: v.emitted === true,
14891
+ idle: v.idle !== false,
14892
+ ...v.intent?.verdict && { intent_verdict: v.intent.verdict },
14893
+ ...typeof v.intent?.score === "number" && { intent_score: v.intent.score }
14235
14894
  });
14236
14895
  }
14896
+ function intentSignature(intent, ctx) {
14897
+ if (!intent?.verdict) return null;
14898
+ if (intent.verdict !== "misaligned" && intent.verdict !== "partial") return null;
14899
+ const goal = ctx ? `g${ctx.goalSeq}` : "g?";
14900
+ const moved = ctx?.idle === false ? "active" : "idle";
14901
+ return `${intent.verdict}:${goal}:${moved}`;
14902
+ }
14237
14903
  function toRegister(severity) {
14238
14904
  switch (severity) {
14239
14905
  case "critical":
@@ -14269,7 +14935,9 @@ function recallMemory(d, identity, opts) {
14269
14935
  const state = foldDossier(d);
14270
14936
  const watermark = state.meta.watermark?.sha ?? null;
14271
14937
  const watermarkPaths = (state.authored ?? []).map((a) => a.path);
14938
+ const captureCmp = typeof opts.userMessagesSeen === "number" ? { seen: opts.userMessagesSeen, captured: state.meta.goal_chain } : void 0;
14272
14939
  const r = recall(d, {
14940
+ ...captureCmp && { capture: captureCmp },
14273
14941
  identity,
14274
14942
  currentSessionKey: opts.currentSessionKey,
14275
14943
  branchNow: getCurrentBranch(),
@@ -14278,7 +14946,7 @@ function recallMemory(d, identity, opts) {
14278
14946
  budgetBytes: opts.budgetBytes,
14279
14947
  readFileLines: (file) => {
14280
14948
  try {
14281
- const abs = (0, import_node_path12.join)(root, file);
14949
+ const abs = (0, import_node_path11.join)(root, file);
14282
14950
  return (0, import_node_fs10.existsSync)(abs) ? (0, import_node_fs10.readFileSync)(abs, "utf8").split("\n") : null;
14283
14951
  } catch {
14284
14952
  return null;
@@ -14418,21 +15086,21 @@ async function fireClassify(prompt, sessionId, globals) {
14418
15086
 
14419
15087
  // src/commands/lifecycle.ts
14420
15088
  var import_node_fs14 = require("node:fs");
14421
- var import_node_path16 = require("node:path");
15089
+ var import_node_path15 = require("node:path");
14422
15090
 
14423
15091
  // src/lib/baseline.ts
14424
15092
  var import_node_fs13 = require("node:fs");
14425
- var import_node_path15 = require("node:path");
15093
+ var import_node_path14 = require("node:path");
14426
15094
  var import_node_crypto7 = require("node:crypto");
14427
15095
 
14428
15096
  // src/lib/snapshot.ts
14429
15097
  var import_node_fs12 = require("node:fs");
14430
- var import_node_path14 = require("node:path");
14431
- var import_node_child_process7 = require("node:child_process");
15098
+ var import_node_path13 = require("node:path");
15099
+ var import_node_child_process6 = require("node:child_process");
14432
15100
 
14433
15101
  // src/lib/files.ts
14434
15102
  var import_node_fs11 = require("node:fs");
14435
- var import_node_path13 = require("node:path");
15103
+ var import_node_path12 = require("node:path");
14436
15104
  var LANG_MAP = {
14437
15105
  // Analyzable (static analysis + Gemini)
14438
15106
  ts: "typescript",
@@ -14500,7 +15168,7 @@ var LANG_MAP = {
14500
15168
  mk: "make"
14501
15169
  };
14502
15170
  function detectLanguage(filepath) {
14503
- const ext = (0, import_node_path13.extname)(filepath).slice(1);
15171
+ const ext = (0, import_node_path12.extname)(filepath).slice(1);
14504
15172
  return LANG_MAP[ext] ?? ext;
14505
15173
  }
14506
15174
  function sortByMtime(files) {
@@ -14526,6 +15194,8 @@ function collectCodeDelta(files, opts) {
14526
15194
  let totalSize = 0;
14527
15195
  let truncationReason = null;
14528
15196
  const droppedPaths = [];
15197
+ const excluded = [];
15198
+ const exclude = (path, reason) => excluded.push({ path, reason, stage: "collectCodeDelta", kind: "capacity" });
14529
15199
  for (const filepath of sorted) {
14530
15200
  if (result.length >= maxFiles) {
14531
15201
  truncationReason ??= "max_files";
@@ -14533,14 +15203,21 @@ function collectCodeDelta(files, opts) {
14533
15203
  continue;
14534
15204
  }
14535
15205
  const resolved = resolveFile(filepath);
14536
- if (!resolved) continue;
15206
+ if (!resolved) {
15207
+ exclude(filepath, "path-not-resolvable");
15208
+ continue;
15209
+ }
14537
15210
  let size;
14538
15211
  try {
14539
15212
  size = (0, import_node_fs11.statSync)(resolved).size;
14540
15213
  } catch {
15214
+ exclude(filepath, "not-stattable");
15215
+ continue;
15216
+ }
15217
+ if (size > maxFileBytes) {
15218
+ exclude(filepath, `over-file-size-limit-${maxFileBytes}b`);
14541
15219
  continue;
14542
15220
  }
14543
- if (size > maxFileBytes) continue;
14544
15221
  if (totalSize + size > maxTotalBytes) {
14545
15222
  truncationReason ??= "max_total_bytes";
14546
15223
  const idx = sorted.indexOf(filepath);
@@ -14551,6 +15228,7 @@ function collectCodeDelta(files, opts) {
14551
15228
  try {
14552
15229
  content = (0, import_node_fs11.readFileSync)(resolved, "utf-8");
14553
15230
  } catch {
15231
+ exclude(filepath, "not-readable");
14554
15232
  continue;
14555
15233
  }
14556
15234
  totalSize += size;
@@ -14564,10 +15242,14 @@ function collectCodeDelta(files, opts) {
14564
15242
  (sum, f) => sum + f.content.split("\n").length,
14565
15243
  0
14566
15244
  );
15245
+ for (const path of droppedPaths) {
15246
+ exclude(path, truncationReason === "max_files" ? "max-files-cap" : "max-total-bytes-cap");
15247
+ }
14567
15248
  return {
14568
15249
  files: result,
14569
15250
  total_lines: totalLines,
14570
15251
  total_files: result.length,
15252
+ excluded,
14571
15253
  ...truncationReason && {
14572
15254
  truncated: {
14573
15255
  reason: truncationReason,
@@ -14588,7 +15270,7 @@ function generateSnapshotDiffs(files) {
14588
15270
  }
14589
15271
  const diffs = [];
14590
15272
  for (const file of files) {
14591
- const snapshotPath = (0, import_node_path14.join)(SNAPSHOT_DIR, file.path);
15273
+ const snapshotPath = (0, import_node_path13.join)(SNAPSHOT_DIR, file.path);
14592
15274
  const language = file.language ?? detectLanguage(file.path);
14593
15275
  if ((0, import_node_fs12.existsSync)(snapshotPath)) {
14594
15276
  const oldContent = (0, import_node_fs12.readFileSync)(snapshotPath, "utf-8");
@@ -14615,21 +15297,21 @@ ${addedLines}`,
14615
15297
  function saveSnapshots(files) {
14616
15298
  const snapshotPaths = /* @__PURE__ */ new Set();
14617
15299
  for (const file of files) {
14618
- const snapshotPath = (0, import_node_path14.join)(SNAPSHOT_DIR, file.path);
15300
+ const snapshotPath = (0, import_node_path13.join)(SNAPSHOT_DIR, file.path);
14619
15301
  snapshotPaths.add(snapshotPath);
14620
- (0, import_node_fs12.mkdirSync)((0, import_node_path14.dirname)(snapshotPath), { recursive: true });
15302
+ (0, import_node_fs12.mkdirSync)((0, import_node_path13.dirname)(snapshotPath), { recursive: true });
14621
15303
  (0, import_node_fs12.writeFileSync)(snapshotPath, file.content);
14622
15304
  }
14623
15305
  cleanStaleSnapshots(SNAPSHOT_DIR, snapshotPaths);
14624
15306
  }
14625
15307
  function computeDiff(oldContent, newContent, filePath) {
14626
- const tmpOld = (0, import_node_path14.join)(SNAPSHOT_DIR, ".diff-old.tmp");
14627
- const tmpNew = (0, import_node_path14.join)(SNAPSHOT_DIR, ".diff-new.tmp");
15308
+ const tmpOld = (0, import_node_path13.join)(SNAPSHOT_DIR, ".diff-old.tmp");
15309
+ const tmpNew = (0, import_node_path13.join)(SNAPSHOT_DIR, ".diff-new.tmp");
14628
15310
  try {
14629
15311
  (0, import_node_fs12.mkdirSync)(SNAPSHOT_DIR, { recursive: true });
14630
15312
  (0, import_node_fs12.writeFileSync)(tmpOld, oldContent);
14631
15313
  (0, import_node_fs12.writeFileSync)(tmpNew, newContent);
14632
- const result = (0, import_node_child_process7.execSync)(
15314
+ const result = (0, import_node_child_process6.execSync)(
14633
15315
  `git diff --no-index --unified=10 -- "${tmpOld}" "${tmpNew}"`,
14634
15316
  { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
14635
15317
  );
@@ -14657,7 +15339,7 @@ function cleanStaleSnapshots(dir, keepSet) {
14657
15339
  const entries = (0, import_node_fs12.readdirSync)(dir, { withFileTypes: true });
14658
15340
  for (const entry of entries) {
14659
15341
  if (entry.name.startsWith(".")) continue;
14660
- const fullPath = (0, import_node_path14.join)(dir, entry.name);
15342
+ const fullPath = (0, import_node_path13.join)(dir, entry.name);
14661
15343
  if (entry.isDirectory()) {
14662
15344
  cleanStaleSnapshots(fullPath, keepSet);
14663
15345
  try {
@@ -14686,13 +15368,13 @@ function sessionKey(sessionId) {
14686
15368
  return (0, import_node_crypto7.createHash)("sha256").update(sessionId).digest("hex").slice(0, 16);
14687
15369
  }
14688
15370
  function sessionDir(key) {
14689
- return (0, import_node_path15.join)(projectPath(BASELINE_DIR), key);
15371
+ return (0, import_node_path14.join)(projectPath(BASELINE_DIR), key);
14690
15372
  }
14691
15373
  function manifestPath(dir) {
14692
- return (0, import_node_path15.join)(dir, "manifest.json");
15374
+ return (0, import_node_path14.join)(dir, "manifest.json");
14693
15375
  }
14694
15376
  function mirrorPath(dir, repoRelPath) {
14695
- return (0, import_node_path15.join)(dir, "files", repoRelPath);
15377
+ return (0, import_node_path14.join)(dir, "files", repoRelPath);
14696
15378
  }
14697
15379
  var CARRY_FILE = `${BASELINE_DIR}/.carry`;
14698
15380
  var CARRY_WINDOW_MS = 12e4;
@@ -14754,7 +15436,7 @@ function captureBaseline(opts = {}) {
14754
15436
  (0, import_node_fs13.rmSync)(dir, { recursive: true, force: true });
14755
15437
  } catch {
14756
15438
  }
14757
- const filesDir = (0, import_node_path15.join)(dir, "files");
15439
+ const filesDir = (0, import_node_path14.join)(dir, "files");
14758
15440
  const mirrored = [];
14759
15441
  try {
14760
15442
  (0, import_node_fs13.mkdirSync)(filesDir, { recursive: true });
@@ -14764,7 +15446,7 @@ function captureBaseline(opts = {}) {
14764
15446
  if (content === null) continue;
14765
15447
  const dest = mirrorPath(dir, p);
14766
15448
  try {
14767
- (0, import_node_fs13.mkdirSync)((0, import_node_path15.dirname)(dest), { recursive: true });
15449
+ (0, import_node_fs13.mkdirSync)((0, import_node_path14.dirname)(dest), { recursive: true });
14768
15450
  (0, import_node_fs13.writeFileSync)(dest, content);
14769
15451
  mirrored.push(p);
14770
15452
  } catch {
@@ -14892,7 +15574,7 @@ function pruneOldBaselines() {
14892
15574
  }
14893
15575
  const now = Date.now();
14894
15576
  for (const name of entries) {
14895
- const dir = (0, import_node_path15.join)(root, name);
15577
+ const dir = (0, import_node_path14.join)(root, name);
14896
15578
  const manifest = readManifest(dir);
14897
15579
  if (!manifest) {
14898
15580
  try {
@@ -15078,7 +15760,7 @@ function buildCompactionContext(session) {
15078
15760
  commitsSince: commitsSincePaths(watermark, (state.authored ?? []).map((a) => a.path)),
15079
15761
  readFileLines: (file) => {
15080
15762
  try {
15081
- const abs = (0, import_node_path16.join)(root, file);
15763
+ const abs = (0, import_node_path15.join)(root, file);
15082
15764
  return (0, import_node_fs14.existsSync)(abs) ? (0, import_node_fs14.readFileSync)(abs, "utf8").split("\n") : null;
15083
15765
  } catch {
15084
15766
  return null;
@@ -15414,6 +16096,7 @@ function registerStatusCommand(program2) {
15414
16096
  }
15415
16097
  const token = tokenResult.data.token;
15416
16098
  const serviceUrl = urlResult.data;
16099
+ const who = await whoami(token, serviceUrl, globals.verbose);
15417
16100
  const memResult = await apiRequest({
15418
16101
  method: "GET",
15419
16102
  path: "/memory",
@@ -15421,14 +16104,21 @@ function registerStatusCommand(program2) {
15421
16104
  token,
15422
16105
  verbose: globals.verbose
15423
16106
  });
15424
- if (!memResult.ok) {
16107
+ const denial = memResult.ok ? null : authDenialRemedy(memResult.error);
16108
+ if (!memResult.ok && !denial) {
15425
16109
  printError(memResult.error);
15426
16110
  process.exit(1);
15427
16111
  }
15428
- const mem = memResult.data;
16112
+ const mem = memResult.ok ? memResult.data : null;
15429
16113
  if (opts.json) {
15430
- const output = { memory: mem };
15431
- if (opts.history) {
16114
+ const output = {
16115
+ auth: who.ok ? who.data : { error: who.error },
16116
+ memory: mem
16117
+ };
16118
+ if (denial && !memResult.ok) {
16119
+ output.error = { code: denial.code, message: memResult.error, remedy: denial.remedy };
16120
+ }
16121
+ if (opts.history && !denial) {
15432
16122
  const runsResult = await apiRequest({
15433
16123
  method: "GET",
15434
16124
  path: `/runs?limit=${opts.limit}`,
@@ -15443,23 +16133,30 @@ function registerStatusCommand(program2) {
15443
16133
  printJson(output);
15444
16134
  return;
15445
16135
  }
15446
- if (mem.configured === false) {
16136
+ if (mem?.configured === false) {
15447
16137
  printInfo("Verity is not configured for this project. Run /verity-setup.");
15448
16138
  return;
15449
16139
  }
15450
16140
  printInfo("=== Verity Status ===");
15451
- if (tokenResult.data.userId != null) {
15452
- printInfo(`Account: Logged in as ${tokenResult.data.email ?? `user #${tokenResult.data.userId}`} \u2713`);
15453
- } else {
15454
- const who = await whoami(token, serviceUrl, globals.verbose);
15455
- if (who.ok && who.data.logged_in) {
15456
- printInfo(`Account: Logged in as ${who.data.email ?? `user #${who.data.user_id}`} \u2713`);
15457
- } else if (who.ok && who.data.anonymous) {
15458
- printInfo('Account: Anonymous \u2014 runs not saved, no cloud memory. Run "verity login".');
15459
- }
15460
- }
15461
- if (mem.project_name) printInfo(`Project: ${mem.project_name}`);
15462
- if (mem.standard) {
16141
+ if (who.ok && who.data.logged_in) {
16142
+ printInfo(`Account: Logged in as ${who.data.email ?? `user #${who.data.user_id}`} \u2713`);
16143
+ } else if (who.ok && who.data.anonymous) {
16144
+ printInfo('Account: Anonymous \u2014 runs not saved, no cloud memory. Run "verity login".');
16145
+ } else if (tokenResult.data.userId != null) {
16146
+ printInfo(`Account: Logged in as ${tokenResult.data.email ?? `user #${tokenResult.data.userId}`} (cached \u2014 could not reach the Verity service) \u2713`);
16147
+ } else if (!who.ok) {
16148
+ printInfo(`Account: Unknown \u2014 could not reach the Verity service (${who.error})`);
16149
+ }
16150
+ if (who.ok) {
16151
+ const nudge = reverifyNudge(who.data);
16152
+ if (nudge) printWarn(` ${nudge}`);
16153
+ }
16154
+ if (denial) {
16155
+ printWarn(`Access: ${denial.remedy}`);
16156
+ printInfo(" Project status, history, and cloud memory stay unavailable until then.");
16157
+ }
16158
+ if (mem?.project_name) printInfo(`Project: ${mem.project_name}`);
16159
+ if (mem?.standard) {
15463
16160
  const s = mem.standard;
15464
16161
  printInfo(`Standard: v${s.version} (${s.quality_dimensions} quality, ${s.security_patterns} security, ${s.custom_patterns} custom)`);
15465
16162
  printInfo(`Languages: ${s.languages.join(", ")}`);
@@ -15470,6 +16167,7 @@ function registerStatusCommand(program2) {
15470
16167
  if (hookStatus.guardOn.includes("commit")) moments.push("pre-commit");
15471
16168
  if (hookStatus.guardOn.includes("push")) moments.push("pre-push/PR");
15472
16169
  printInfo(`Moments: ${moments.length > 0 ? moments.join(", ") : "none (run /verity-setup)"}`);
16170
+ if (!mem) return;
15473
16171
  if (mem.recent_runs) {
15474
16172
  const r = mem.recent_runs;
15475
16173
  printInfo("");
@@ -15621,7 +16319,7 @@ async function sendGeneralFeedback(message, opts, globals) {
15621
16319
 
15622
16320
  // src/commands/analyze.ts
15623
16321
  var import_node_fs24 = require("node:fs");
15624
- var import_node_path21 = require("node:path");
16322
+ var import_node_path20 = require("node:path");
15625
16323
 
15626
16324
  // src/lib/debounce.ts
15627
16325
  var import_node_fs15 = require("node:fs");
@@ -15758,7 +16456,7 @@ function writeIteration(iteration, commit, _contentHash) {
15758
16456
  }
15759
16457
 
15760
16458
  // src/lib/static-analysis.ts
15761
- var import_node_child_process8 = require("node:child_process");
16459
+ var import_node_child_process7 = require("node:child_process");
15762
16460
  var import_node_fs16 = require("node:fs");
15763
16461
  var SEVERITY_ORDER = {
15764
16462
  Error: 0,
@@ -15771,7 +16469,7 @@ var SEVERITY_ORDER = {
15771
16469
  };
15772
16470
  function isCodacyAvailable() {
15773
16471
  try {
15774
- (0, import_node_child_process8.execSync)("which codacy-analysis", { stdio: "pipe" });
16472
+ (0, import_node_child_process7.execSync)("which codacy-analysis", { stdio: "pipe" });
15775
16473
  return true;
15776
16474
  } catch {
15777
16475
  return false;
@@ -15795,7 +16493,7 @@ function runCodacyAnalysis(files) {
15795
16493
  const fileArgs = existingFiles.join(" ");
15796
16494
  let output;
15797
16495
  try {
15798
- output = (0, import_node_child_process8.execSync)(
16496
+ output = (0, import_node_child_process7.execSync)(
15799
16497
  `codacy-analysis analyze --install-dependencies --files ${fileArgs} --output-format json --log-level error --parallel-tools 3`,
15800
16498
  { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 10 * 1024 * 1024 }
15801
16499
  );
@@ -15849,7 +16547,7 @@ function runCodacyAnalysis(files) {
15849
16547
 
15850
16548
  // src/lib/specs.ts
15851
16549
  var import_node_fs17 = require("node:fs");
15852
- var import_node_path17 = require("node:path");
16550
+ var import_node_path16 = require("node:path");
15853
16551
  var SPEC_CANDIDATES = [
15854
16552
  "CLAUDE.md",
15855
16553
  "AGENTS.md",
@@ -15911,7 +16609,7 @@ function findMdFiles(dir, maxDepth, depth = 0) {
15911
16609
  try {
15912
16610
  const entries = (0, import_node_fs17.readdirSync)(dir, { withFileTypes: true });
15913
16611
  for (const entry of entries) {
15914
- const fullPath = (0, import_node_path17.join)(dir, entry.name);
16612
+ const fullPath = (0, import_node_path16.join)(dir, entry.name);
15915
16613
  if (entry.isFile() && entry.name.endsWith(".md")) {
15916
16614
  result.push(fullPath);
15917
16615
  } else if (entry.isDirectory() && depth < maxDepth - 1) {
@@ -15923,7 +16621,7 @@ function findMdFiles(dir, maxDepth, depth = 0) {
15923
16621
  return result;
15924
16622
  }
15925
16623
  function discoverPlans() {
15926
- const homePlansDir = (0, import_node_path17.join)(process.env.HOME ?? "", ".claude", "plans");
16624
+ const homePlansDir = (0, import_node_path16.join)(process.env.HOME ?? "", ".claude", "plans");
15927
16625
  const localPlansDir = ".claude/plans";
15928
16626
  const candidates = [];
15929
16627
  const seen = /* @__PURE__ */ new Set();
@@ -15933,7 +16631,7 @@ function discoverPlans() {
15933
16631
  for (const f of (0, import_node_fs17.readdirSync)(plansDir)) {
15934
16632
  if (!f.endsWith(".md") || seen.has(f)) continue;
15935
16633
  seen.add(f);
15936
- const fullPath = (0, import_node_path17.join)(plansDir, f);
16634
+ const fullPath = (0, import_node_path16.join)(plansDir, f);
15937
16635
  try {
15938
16636
  const stat3 = (0, import_node_fs17.statSync)(fullPath);
15939
16637
  candidates.push({ name: f, path: fullPath, mtime: stat3.mtimeMs, size: stat3.size });
@@ -15957,7 +16655,7 @@ function discoverPlans() {
15957
16655
  }
15958
16656
 
15959
16657
  // src/lib/task-context.ts
15960
- var import_node_child_process9 = require("node:child_process");
16658
+ var import_node_child_process8 = require("node:child_process");
15961
16659
  var CLOSING_RE = /\b(close[sd]?|fix(?:e[sd])?|resolve[sd]?)\b[\s:]*#(\d+)/i;
15962
16660
  var BRANCH_RE = /(?:^|[/_-])(?:issue|gh|fix)[-_/]?(\d+)\b/i;
15963
16661
  function parseLinkedIssue(sources) {
@@ -15973,7 +16671,7 @@ function parseLinkedIssue(sources) {
15973
16671
  }
15974
16672
  function safeExec(cmd, timeout) {
15975
16673
  try {
15976
- return (0, import_node_child_process9.execSync)(cmd, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout }).trim();
16674
+ return (0, import_node_child_process8.execSync)(cmd, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout }).trim();
15977
16675
  } catch {
15978
16676
  return "";
15979
16677
  }
@@ -16003,7 +16701,7 @@ function resolveTaskContext(opts) {
16003
16701
  // src/lib/cli-version.ts
16004
16702
  function cliVersion() {
16005
16703
  try {
16006
- return true ? "0.28.1-experimental.a57c8d9" : "dev";
16704
+ return true ? "0.28.1-experimental.a72d2d9" : "dev";
16007
16705
  } catch {
16008
16706
  return "dev";
16009
16707
  }
@@ -16207,7 +16905,7 @@ function truthy(v) {
16207
16905
 
16208
16906
  // src/lib/fold.ts
16209
16907
  var import_node_fs20 = require("node:fs");
16210
- var import_node_path18 = require("node:path");
16908
+ var import_node_path17 = require("node:path");
16211
16909
  var KNOWN_RECORD_TYPES = /* @__PURE__ */ new Set([
16212
16910
  "user",
16213
16911
  "assistant",
@@ -16228,6 +16926,16 @@ var KNOWN_RECORD_TYPES = /* @__PURE__ */ new Set([
16228
16926
  "subagent"
16229
16927
  ]);
16230
16928
  var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "NotebookEdit", "MultiEdit"]);
16929
+ var DISPATCH_TOOLS = /* @__PURE__ */ new Set(["Task", "Workflow"]);
16930
+ function hasUserText(record) {
16931
+ const message = record.message;
16932
+ const content = message?.content ?? record.content;
16933
+ if (typeof content === "string") return content.trim().length > 0;
16934
+ if (!Array.isArray(content)) return false;
16935
+ return content.some(
16936
+ (b) => b?.type === "text" && typeof b.text === "string" && b.text.trim().length > 0
16937
+ );
16938
+ }
16231
16939
  var COMMAND_CLASSES = [
16232
16940
  [/\b(npm|yarn|pnpm|bun)\s+(run\s+)?test\b|\bvitest\b|\bjest\b|\bpytest\b|\bgo test\b/, "test"],
16233
16941
  [/\b(npm|yarn|pnpm|bun)\s+(run\s+)?build\b|\btsc\b|\bwebpack\b|\bcargo build\b/, "build"],
@@ -16354,6 +17062,8 @@ function fold(transcriptPath, opts = {}) {
16354
17062
  totalRecords: 0,
16355
17063
  malformed: 0,
16356
17064
  subagentFiles: 0,
17065
+ dispatched: 0,
17066
+ userMessages: 0,
16357
17067
  subagentSkipped: 0,
16358
17068
  compactions: 0,
16359
17069
  complete: false
@@ -16380,7 +17090,8 @@ function fold(transcriptPath, opts = {}) {
16380
17090
  if (type === "system" && record.subtype === "compact_boundary") {
16381
17091
  result.coverage.compactions++;
16382
17092
  }
16383
- collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, opts.repoRoot);
17093
+ if (type === "user" && hasUserText(record)) result.coverage.userMessages++;
17094
+ collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, opts.repoRoot, result.coverage);
16384
17095
  }
16385
17096
  };
16386
17097
  try {
@@ -16391,7 +17102,7 @@ function fold(transcriptPath, opts = {}) {
16391
17102
  return result;
16392
17103
  }
16393
17104
  try {
16394
- const sidecarDir = (0, import_node_path18.join)((0, import_node_path18.dirname)(transcriptPath), "subagents");
17105
+ const sidecarDir = (0, import_node_path17.join)((0, import_node_path17.dirname)(transcriptPath), "subagents");
16395
17106
  if ((0, import_node_fs20.existsSync)(sidecarDir)) {
16396
17107
  const maxFiles = opts.maxSidecars ?? 200;
16397
17108
  const maxBytes = opts.maxSidecarBytes ?? 16 * 1024 * 1024;
@@ -16399,7 +17110,7 @@ function fold(transcriptPath, opts = {}) {
16399
17110
  const walk = (d, depth) => {
16400
17111
  if (depth > 4) return;
16401
17112
  for (const e of (0, import_node_fs20.readdirSync)(d, { withFileTypes: true })) {
16402
- const p = (0, import_node_path18.join)(d, e.name);
17113
+ const p = (0, import_node_path17.join)(d, e.name);
16403
17114
  if (e.isDirectory()) {
16404
17115
  walk(p, depth + 1);
16405
17116
  } else if (e.name.startsWith("agent-") && e.name.endsWith(".jsonl")) {
@@ -16452,7 +17163,7 @@ function classifyUnobserved(path) {
16452
17163
  if (/\.(png|jpg|jpeg|gif|pdf|zip|woff2?|ico|mp4)$/i.test(path)) return "binary";
16453
17164
  return "no_edit_record";
16454
17165
  }
16455
- function collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, repoRoot2) {
17166
+ function collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse, repoRoot2, tally) {
16456
17167
  const message = record.message;
16457
17168
  const content = message?.content ?? record.content;
16458
17169
  const blocks = Array.isArray(content) ? content : content && typeof content === "object" ? [content] : [];
@@ -16474,6 +17185,9 @@ function collectFromRecord(record, owner, byPath, commandStats, pendingByToolUse
16474
17185
  byPath.set(path, entry);
16475
17186
  }
16476
17187
  }
17188
+ if (DISPATCH_TOOLS.has(name) && tally) {
17189
+ tally.dispatched += 1;
17190
+ }
16477
17191
  if (name === "Bash") {
16478
17192
  const cmd = typeof input.command === "string" ? input.command : "";
16479
17193
  if (cmd) {
@@ -16532,6 +17246,86 @@ function checkConservation(changedFiles, result, repoRoot2) {
16532
17246
  };
16533
17247
  }
16534
17248
 
17249
+ // src/lib/verdict.ts
17250
+ function reconcileCoverage(changed, coverage) {
17251
+ const changedSet = new Set(changed);
17252
+ const reviewed = coverage.reviewed.filter((p) => changedSet.has(p));
17253
+ const claimed = /* @__PURE__ */ new Set([...reviewed, ...coverage.notReviewed.map((n) => n.path)]);
17254
+ const unaccounted = [...changedSet].filter((p) => !claimed.has(p)).sort();
17255
+ const notReviewed = [
17256
+ ...coverage.notReviewed.filter((n) => changedSet.has(n.path)),
17257
+ ...unaccounted.map((path) => ({
17258
+ path,
17259
+ reason: "unaccounted",
17260
+ // Named so the eventual bug report writes itself: some stage removed this
17261
+ // path and did not say so.
17262
+ stage: "unknown-stage",
17263
+ // An undeclared drop is CAPACITY by default. A stage that cannot be
17264
+ // bothered to say why it dropped a file does not get the benefit of the
17265
+ // doubt — that default is what makes forgetting expensive.
17266
+ kind: "capacity"
17267
+ }))
17268
+ ];
17269
+ return {
17270
+ coverage: { reviewed: [...new Set(reviewed)].sort(), notReviewed },
17271
+ unaccounted,
17272
+ balances: unaccounted.length === 0
17273
+ };
17274
+ }
17275
+ function resolveVerdict(proposed, coverage) {
17276
+ if (proposed === "FAIL") return "FAIL";
17277
+ const blocking = coverage.notReviewed.filter((n) => (n.kind ?? "capacity") !== "policy");
17278
+ if (blocking.length === 0) return proposed;
17279
+ return "WARN";
17280
+ }
17281
+ function describeCoverage(coverage, maxPaths = 5) {
17282
+ const relevant = coverage.notReviewed.filter((n) => (n.kind ?? "capacity") !== "policy");
17283
+ if (relevant.length === 0) return null;
17284
+ const byReason = /* @__PURE__ */ new Map();
17285
+ for (const n of relevant) {
17286
+ const key = `${n.reason}`;
17287
+ const list = byReason.get(key) ?? [];
17288
+ list.push(n.path);
17289
+ byReason.set(key, list);
17290
+ }
17291
+ const lines = [];
17292
+ for (const [reason, paths] of [...byReason.entries()].sort()) {
17293
+ const shown = paths.slice(0, maxPaths).join(", ");
17294
+ const more = paths.length > maxPaths ? ` (+${paths.length - maxPaths} more)` : "";
17295
+ lines.push(` ${paths.length} not reviewed \u2014 ${reason}: ${shown}${more}`);
17296
+ }
17297
+ return `NOT A CLEAN REVIEW. ${relevant.length} changed file(s) never reached the reviewer, so this verdict does not cover them:
17298
+ ${lines.join("\n")}
17299
+ Treat those files as UNCHECKED, not as approved.`;
17300
+ }
17301
+ function openBlockingElsewhere(statements, reviewedNow, lineShaAt) {
17302
+ const reviewed = new Set(reviewedNow);
17303
+ const out = [];
17304
+ const seen = /* @__PURE__ */ new Set();
17305
+ for (const s of statements) {
17306
+ if (s.outcome !== "open") continue;
17307
+ if (s.register !== "BLOCK") continue;
17308
+ if (s.carried) continue;
17309
+ if (reviewed.has(s.file)) continue;
17310
+ if (!s.line_sha) continue;
17311
+ if (lineShaAt(s.file, s.line) !== s.line_sha) continue;
17312
+ const key = `${s.file}::${s.pattern_id}`;
17313
+ if (seen.has(key)) continue;
17314
+ seen.add(key);
17315
+ out.push({ file: s.file, line: s.line, pattern_id: s.pattern_id });
17316
+ }
17317
+ return out;
17318
+ }
17319
+ function describeOpenElsewhere(open) {
17320
+ if (open.length === 0) return null;
17321
+ const lines = open.slice(0, 5).map((o) => ` ${o.file}:${o.line} [${o.pattern_id}]`);
17322
+ const more = open.length > 5 ? `
17323
+ (+${open.length - 5} more)` : "";
17324
+ return `STILL OPEN ELSEWHERE. ${open.length} blocking finding(s) Verity raised earlier are still present in files this run did not review:
17325
+ ${lines.join("\n")}${more}
17326
+ This verdict covers the current change only. The tree is not clean.`;
17327
+ }
17328
+
16535
17329
  // src/lib/channel.ts
16536
17330
  var MAX_AGENT_CONTEXT_CHARS = 1500;
16537
17331
  var MAX_AGENT_ITEMS = 5;
@@ -16554,9 +17348,16 @@ function buildAgentContext(input) {
16554
17348
  }
16555
17349
  if (input.intentVerdict === "misaligned" || input.intentVerdict === "partial") {
16556
17350
  const gap = input.intentGaps?.[0];
16557
- lines.push(
16558
- `- 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."
16559
- );
17351
+ const repeat = input.intentRepeat ?? 0;
17352
+ if (repeat > 0) {
17353
+ lines.push(
17354
+ `- 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.`
17355
+ );
17356
+ } else {
17357
+ lines.push(
17358
+ `- 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."
17359
+ );
17360
+ }
16560
17361
  }
16561
17362
  const agentFindings = (input.findings ?? []).filter((f) => f.scope !== "pre-existing");
16562
17363
  for (const f of agentFindings) {
@@ -16603,10 +17404,51 @@ function buildHookOutput(gateDecision, systemMessage, agentContext) {
16603
17404
  } : {}
16604
17405
  };
16605
17406
  }
17407
+ var IDLE_EPISODE_CAP = 3;
17408
+ function channelSilence(input) {
17409
+ const movedSomething = input.newUserPrompt || input.newAuthorship;
17410
+ if (movedSomething) return null;
17411
+ if (input.consecutiveIdle >= IDLE_EPISODE_CAP) return "idle-episode-cap";
17412
+ if (input.emittedLast) return "caused-by-our-own-emission";
17413
+ return null;
17414
+ }
17415
+
17416
+ // src/lib/emit.ts
17417
+ var YELLOW2 = "\x1B[33m";
17418
+ var NC2 = "\x1B[0m";
17419
+ function emitVerdict(input) {
17420
+ const exit = input.exit ?? ((code) => process.exit(code));
17421
+ const { coverage, unaccounted } = reconcileCoverage(input.changed, input.coverage);
17422
+ let verdict = resolveVerdict(input.proposed, coverage);
17423
+ const openElsewhere = input.openElsewhere ?? [];
17424
+ if (verdict === "PASS" && openElsewhere.length > 0) verdict = "WARN";
17425
+ const note = [describeCoverage(coverage), describeOpenElsewhere(openElsewhere)].filter(Boolean).join("\n\n") || null;
17426
+ if (unaccounted.length > 0) {
17427
+ process.stderr.write(
17428
+ `${YELLOW2}Verity: ${unaccounted.length} changed file(s) could not be attributed to any review stage \u2014 counted as unreviewed.${NC2}
17429
+ `
17430
+ );
17431
+ }
17432
+ if (verdict === "FAIL") {
17433
+ input.renderBlocking?.();
17434
+ if (input.agentContext) {
17435
+ process.stderr.write(`
17436
+ ${input.agentContext}
17437
+ `);
17438
+ }
17439
+ if (note && !input.silenced) process.stderr.write(`
17440
+ ${YELLOW2}${note}${NC2}
17441
+ `);
17442
+ return exit(2);
17443
+ }
17444
+ const agentBlock = input.silenced ? null : [input.agentContext, note].filter(Boolean).join("\n\n") || null;
17445
+ printJsonCompact(buildHookOutput(verdict, input.userSummary, agentBlock));
17446
+ return exit(0);
17447
+ }
16606
17448
 
16607
17449
  // src/lib/cache-cleanup.ts
16608
17450
  var import_node_fs21 = require("node:fs");
16609
- var import_node_path19 = require("node:path");
17451
+ var import_node_path18 = require("node:path");
16610
17452
  var CACHE_TTL_DAYS = 7;
16611
17453
  function pruneStaleCache() {
16612
17454
  try {
@@ -16614,7 +17456,7 @@ function pruneStaleCache() {
16614
17456
  const cutoff = Date.now() - CACHE_TTL_DAYS * 24 * 3600 * 1e3;
16615
17457
  for (const entry of (0, import_node_fs21.readdirSync)(dir)) {
16616
17458
  if (!entry.startsWith("pending-")) continue;
16617
- const path = (0, import_node_path19.join)(dir, entry);
17459
+ const path = (0, import_node_path18.join)(dir, entry);
16618
17460
  try {
16619
17461
  const stat3 = (0, import_node_fs21.statSync)(path);
16620
17462
  if (stat3.mtimeMs < cutoff) {
@@ -16783,46 +17625,6 @@ function shouldWarmRetryAnalyze(result) {
16783
17625
  return false;
16784
17626
  }
16785
17627
 
16786
- // src/lib/skip-detection.ts
16787
- function isBareAckPrompt(prompt) {
16788
- if (typeof prompt !== "string") return false;
16789
- const trimmed = prompt.trim();
16790
- if (trimmed.length === 0) return false;
16791
- if (trimmed.length > 20) return false;
16792
- const bareAckPattern = /^(\d{1,2}|y|n|yes|no|yep|nope|ok(ay)?|sure|skip|cancel|stop|done|noted|got\s+it|sounds\s+good|thanks|thank\s+you|thx)[.!?]*$/i;
16793
- return bareAckPattern.test(trimmed);
16794
- }
16795
- function isReflectionQuestion(response) {
16796
- if (!response || typeof response !== "string") return false;
16797
- const markers = [
16798
- /reflection\s+for\s+future\s+agents/i,
16799
- /what(?:'s|\s+is)\s+one\s+thing\s+you\s+learned/i,
16800
- /say\s+['"]?skip['"]?\s+to\s+skip/i,
16801
- /quick\s+reflection\s+question/i,
16802
- // Post-flip (VRT-21): the agent drafts the reflection itself and, when
16803
- // interactive, asks the user to confirm/correct before recording. That
16804
- // turn authors no code either, so it's still a reflection turn.
16805
- /reflection\s+draft/i,
16806
- /confirm,?\s+correct,?\s+or\s+add/i
16807
- ];
16808
- return markers.some((m) => m.test(response));
16809
- }
16810
- function isMetaTaskLabel(label2) {
16811
- if (label2 === null || label2 === void 0) return false;
16812
- if (typeof label2 !== "string") return false;
16813
- const trimmed = label2.trim();
16814
- if (trimmed.length === 0) return true;
16815
- const metaPatterns = [
16816
- /^verity\s+[\w-]+\s+response$/i,
16817
- // "Verity reflect response"
16818
- /^simple user response$/i,
16819
- /^verity\s+command$/i,
16820
- // "Verity command"
16821
- /^user\s+(question|reply|response|ack)$/i
16822
- ];
16823
- return metaPatterns.some((p) => p.test(trimmed));
16824
- }
16825
-
16826
17628
  // src/lib/transcript.ts
16827
17629
  var import_node_fs22 = require("node:fs");
16828
17630
  var MAX_READ_BYTES = 256 * 1024;
@@ -16986,6 +17788,13 @@ function buildSummary(lines) {
16986
17788
  files_read: capArray(filesRead, MAX_FILES_LIST),
16987
17789
  files_edited: capArray(filesEdited, MAX_FILES_LIST),
16988
17790
  files_created: capArray(filesCreated, MAX_CREATED_LIST),
17791
+ // The complement of the two caps that affect SCOPE. `files_read` is excluded
17792
+ // deliberately: reading a file is not authoring it, so a capped read list
17793
+ // narrows nothing.
17794
+ capped_out: [
17795
+ ...cappedOut(filesEdited, MAX_FILES_LIST),
17796
+ ...cappedOut(filesCreated, MAX_CREATED_LIST)
17797
+ ],
16989
17798
  searches,
16990
17799
  commands,
16991
17800
  subagents,
@@ -17036,6 +17845,9 @@ function sanitizeCommand(rawCmd) {
17036
17845
  function capArray(set, max) {
17037
17846
  return Array.from(set).slice(0, max);
17038
17847
  }
17848
+ function cappedOut(set, max) {
17849
+ return Array.from(set).slice(max);
17850
+ }
17039
17851
 
17040
17852
  // src/lib/run-mode.ts
17041
17853
  function parseAutonomousEnv(raw) {
@@ -17061,7 +17873,7 @@ function isExplicitlyAutonomous(env = process.env) {
17061
17873
  // src/lib/seed-runner.ts
17062
17874
  var import_promises11 = require("node:fs/promises");
17063
17875
  var import_node_fs23 = require("node:fs");
17064
- var import_node_path20 = require("node:path");
17876
+ var import_node_path19 = require("node:path");
17065
17877
  var import_yaml2 = __toESM(require_dist());
17066
17878
 
17067
17879
  // src/lib/seed.ts
@@ -17343,7 +18155,7 @@ async function runSeed(opts) {
17343
18155
  if (candidates.length === 0) {
17344
18156
  return { created: 0, failed: 0, skipped: "no_candidates", candidates: [] };
17345
18157
  }
17346
- const overviewPath = (0, import_node_path20.join)(MEMORY_DIR, "domain", "project-overview.md");
18158
+ const overviewPath = (0, import_node_path19.join)(MEMORY_DIR, "domain", "project-overview.md");
17347
18159
  if ((0, import_node_fs23.existsSync)(overviewPath) && !opts.force) {
17348
18160
  return { created: 0, failed: 0, skipped: "already_seeded", candidates };
17349
18161
  }
@@ -17379,9 +18191,9 @@ async function runSeed(opts) {
17379
18191
  }
17380
18192
  const nodeId = res.data.node_id;
17381
18193
  const filePathRel = res.data.file_path;
17382
- const targetPath = (0, import_node_path20.join)(MEMORY_DIR, filePathRel);
18194
+ const targetPath = (0, import_node_path19.join)(MEMORY_DIR, filePathRel);
17383
18195
  try {
17384
- await (0, import_promises11.mkdir)((0, import_node_path20.dirname)(targetPath), { recursive: true });
18196
+ await (0, import_promises11.mkdir)((0, import_node_path19.dirname)(targetPath), { recursive: true });
17385
18197
  await (0, import_promises11.writeFile)(targetPath, renderNodeMarkdown(c, nodeId, createdAt));
17386
18198
  created++;
17387
18199
  opts.onCreated?.(nodeId, filePathRel, c);
@@ -17430,10 +18242,11 @@ async function readStopHookStdin() {
17430
18242
  return empty;
17431
18243
  }
17432
18244
  }
17433
- function agentContextFor(response) {
18245
+ function agentContextFor(response, intentRepeat = 0) {
17434
18246
  const metadata = response.metadata ?? {};
17435
18247
  const intent = response.intent_alignment ?? {};
17436
18248
  return buildAgentContext({
18249
+ intentRepeat,
17437
18250
  gateDecision: String(response.gate_decision ?? ""),
17438
18251
  findings: response.findings ?? [],
17439
18252
  pendingItems: response.pending_items ?? [],
@@ -17444,12 +18257,45 @@ function agentContextFor(response) {
17444
18257
  });
17445
18258
  }
17446
18259
  var beaconCtx = null;
17447
- async function passAndExit(reason, skip) {
18260
+ async function passAndExit(reason, skip, kindOverride) {
17448
18261
  const sent = await sendSkipBeacon(beaconCtx, skip);
17449
18262
  logEvent("skip", { reason: skip, beacon: sent });
17450
- printJsonCompact({ gate_decision: "PASS", systemMessage: `Verity: ${reason}` });
18263
+ const POLICY_SKIPS = /* @__PURE__ */ new Set([
18264
+ "no-analyzable-files",
18265
+ "verity-command",
18266
+ "bare-acknowledgment",
18267
+ "reflection-prompt",
18268
+ "skip-mode",
18269
+ "zero-increment",
18270
+ "debounce",
18271
+ "no-delta-since-last-review"
18272
+ ]);
18273
+ const skipKind = kindOverride ?? (POLICY_SKIPS.has(skip) ? "policy" : "capacity");
18274
+ const changed = skipCoverageChanged;
18275
+ const { coverage, unaccounted } = reconcileCoverage(changed, {
18276
+ reviewed: [],
18277
+ notReviewed: changed.map((path) => ({ path, reason: skip, stage: "pre-flight", kind: skipKind }))
18278
+ });
18279
+ const verdict = resolveVerdict("PASS", coverage);
18280
+ const note = describeCoverage(coverage);
18281
+ if (unaccounted.length > 0) {
18282
+ logEvent("coverage_unaccounted", { where: "passAndExit", skip, count: unaccounted.length });
18283
+ }
18284
+ const AGENT_SILENT_SKIPS = /* @__PURE__ */ new Set(["iteration-cap"]);
18285
+ const agentNote = AGENT_SILENT_SKIPS.has(skip) ? null : note;
18286
+ printJsonCompact(
18287
+ buildHookOutput(
18288
+ verdict,
18289
+ `Verity: ${reason}`,
18290
+ // The agent's ONLY input is additionalContext. Sixteen of the nineteen
18291
+ // terminating paths wrote `systemMessage` — the human's field — and told
18292
+ // the agent nothing at all.
18293
+ agentNote
18294
+ )
18295
+ );
17451
18296
  process.exit(0);
17452
18297
  }
18298
+ var skipCoverageChanged = [];
17453
18299
  var EMPTY_STATIC = {
17454
18300
  tool: "@codacy/analysis-cli",
17455
18301
  findings: [],
@@ -17464,7 +18310,7 @@ function runLocalStatic(analyzable, securityFiles, baseline, skipStatic) {
17464
18310
  }
17465
18311
  function localOnlyAndExit(staticResults) {
17466
18312
  printJsonCompact({
17467
- gate_decision: "PASS",
18313
+ gate_decision: "WARN",
17468
18314
  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.",
17469
18315
  unauthenticated: true,
17470
18316
  static_results: staticResults
@@ -17524,6 +18370,7 @@ async function runAnalyze(opts, globals) {
17524
18370
  });
17525
18371
  }
17526
18372
  const { files: allChanged, hasRecentCommitFiles } = getChangedFiles();
18373
+ skipCoverageChanged = allChanged;
17527
18374
  const analyzable = filterAnalyzable(allChanged);
17528
18375
  const reviewable = filterReviewable(allChanged);
17529
18376
  const securityFiles = filterSecurity(allChanged);
@@ -17539,10 +18386,12 @@ async function runAnalyze(opts, globals) {
17539
18386
  if (/^\s*\/verity-/i.test(latestPrompt)) {
17540
18387
  await passAndExit("Verity command \u2014 skipping analysis", "verity-command");
17541
18388
  }
17542
- if (isBareAckPrompt(latestPrompt)) {
18389
+ const agentAuthoredCodeThisTurn = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0));
18390
+ const turnAuthoredCode = agentAuthoredCodeThisTurn || !!baseline && allForReview.some((f) => changedSinceBaseline(f, baseline));
18391
+ const canSeeTurnAuthorship = !!actionSummary || !!baseline;
18392
+ if (shouldSkipForBareAck({ prompt: latestPrompt, turnAuthoredCode, canSeeTurnAuthorship })) {
17543
18393
  await passAndExit("Bare acknowledgment \u2014 skipping analysis", "bare-acknowledgment");
17544
18394
  }
17545
- const agentAuthoredCodeThisTurn = !!(actionSummary && (actionSummary.files_edited.length > 0 || actionSummary.files_created.length > 0));
17546
18395
  if (isReflectionQuestion(assistantResponse) && !agentAuthoredCodeThisTurn) {
17547
18396
  await passAndExit("Reflection-prompt turn \u2014 skipping analysis", "reflection-prompt");
17548
18397
  }
@@ -17599,7 +18448,8 @@ async function runAnalyze(opts, globals) {
17599
18448
  let codeDelta = {
17600
18449
  files: [],
17601
18450
  total_lines: 0,
17602
- total_files: 0
18451
+ total_files: 0,
18452
+ excluded: []
17603
18453
  };
17604
18454
  let snapshotResult = { has_snapshots: false, diffs: [] };
17605
18455
  let contentHash = null;
@@ -17664,7 +18514,11 @@ async function runAnalyze(opts, globals) {
17664
18514
  if (assistantResponse) {
17665
18515
  analysisMode = "plan";
17666
18516
  } else {
17667
- await passAndExit("No files within size limits to analyze", "size-limit");
18517
+ await passAndExit(
18518
+ "No files within size limits to analyze",
18519
+ "size-limit",
18520
+ codeDelta.excluded.length > 0 ? "capacity" : "policy"
18521
+ );
17668
18522
  }
17669
18523
  }
17670
18524
  }
@@ -17707,7 +18561,7 @@ async function runAnalyze(opts, globals) {
17707
18561
  let autoSeedNotice = null;
17708
18562
  try {
17709
18563
  await ensureMemoryDir();
17710
- const seedMarker = (0, import_node_path21.join)(VERITY_DIR, ".seeded");
18564
+ const seedMarker = (0, import_node_path20.join)(VERITY_DIR, ".seeded");
17711
18565
  const hasStandard = (0, import_node_fs24.existsSync)(STANDARD_FILE);
17712
18566
  const alreadyTried = (0, import_node_fs24.existsSync)(seedMarker);
17713
18567
  if (hasStandard && !alreadyTried) {
@@ -17775,7 +18629,7 @@ async function runAnalyze(opts, globals) {
17775
18629
  const priorState = foldForMarks(memorySession.d);
17776
18630
  incrementReport = computeIncrement(
17777
18631
  allForReview,
17778
- (p) => fileHash((0, import_node_path21.join)(repoRoot(), p)),
18632
+ (p) => fileHash((0, import_node_path20.join)(repoRoot(), p)),
17779
18633
  priorState.authored_all.map((a) => ({
17780
18634
  path: a.path,
17781
18635
  hash_at_last_verdict: a.hash_at_last_verdict
@@ -17838,7 +18692,10 @@ async function runAnalyze(opts, globals) {
17838
18692
  priorState.capabilities
17839
18693
  );
17840
18694
  memory = recallMemory(memorySession.d, memorySession.identity, {
17841
- currentSessionKey: memorySession.identity.sessionKey
18695
+ currentSessionKey: memorySession.identity.sessionKey,
18696
+ // The independent witness. Only meaningful when a transcript was folded —
18697
+ // otherwise it stays undefined and capture coverage reads as UNKNOWN.
18698
+ ...foldResult && { userMessagesSeen: foldResult.coverage.userMessages }
17842
18699
  });
17843
18700
  if (memory && !memory.provenanceHolds) {
17844
18701
  process.stderr.write("Verity: working-memory provenance check failed \u2014 recall suppressed.\n");
@@ -17917,6 +18774,25 @@ async function runAnalyze(opts, globals) {
17917
18774
  projection: {
17918
18775
  v: 1,
17919
18776
  authored: foldResult?.authored ?? [],
18777
+ // ⚠ AUTHORED *SINCE THE LAST VERDICT* — a different question from `authored`.
18778
+ //
18779
+ // `authored` above is the fold of the session TRANSCRIPT, which is
18780
+ // cumulative: on turn 3 it still lists the files turn 2 edited. The
18781
+ // account's close-out reads it as "was this file edited since the
18782
+ // statement was raised", and those are not the same set.
18783
+ //
18784
+ // Measured 2026-08-03 (shirt-seller): four real vulnerabilities were
18785
+ // raised on the turn that wrote them, then marked `fixed` 36 seconds
18786
+ // later by a SUMMARY turn that edited nothing — `authored` still said 2,
18787
+ // the turn carried 0 findings, so "gone + file authored" resolved to
18788
+ // fixed. The vulnerabilities were still on disk. A silent false `fixed`
18789
+ // is worse than a false `open`: it retires the statement the Account
18790
+ // exists to keep.
18791
+ //
18792
+ // `hash_at_last_verdict` is frozen at each verdict and `hash_now` tracks
18793
+ // disk, so their inequality IS "changed since we last spoke" — already
18794
+ // computed, already maintained by the divergence machinery.
18795
+ authored_since_verdict: memorySession ? foldDossier(memorySession.d).authored_all.filter((a) => a.hash_now !== a.hash_at_last_verdict).map((a) => a.path) : [],
17920
18796
  unobserved: foldResult?.unobserved ?? [],
17921
18797
  commands: foldResult?.commands ?? [],
17922
18798
  unknown_types: foldResult?.unknownTypes ?? [],
@@ -17958,7 +18834,20 @@ async function runAnalyze(opts, globals) {
17958
18834
  const intentContext = {};
17959
18835
  if (conversation && conversation.prompts.length > 0) {
17960
18836
  const latest = conversation.prompts[conversation.prompts.length - 1];
17961
- intentContext.user_prompt = latest.prompt;
18837
+ const goalPrompt = resolveGoalPrompt(conversation.prompts) ?? { entry: latest, turnsBack: 0 };
18838
+ intentContext.user_prompt = goalPrompt.entry.prompt;
18839
+ if (isContinuationPrompt(intentContext.user_prompt)) {
18840
+ const carried = memory?.projection.goal?.text;
18841
+ if (carried && !isContinuationPrompt(carried)) {
18842
+ intentContext.continuation_prompt = latest.prompt;
18843
+ intentContext.user_prompt = carried;
18844
+ logEvent("goal_from_dossier", { chars: carried.length });
18845
+ }
18846
+ }
18847
+ if (goalPrompt.turnsBack > 0) {
18848
+ intentContext.continuation_prompt = latest.prompt;
18849
+ logEvent("goal_walked_back", { turns_back: goalPrompt.turnsBack });
18850
+ }
17962
18851
  intentContext.session_id = latest.session_id || void 0;
17963
18852
  intentContext.prompt_captured_at = latest.captured_at || void 0;
17964
18853
  if (conversation.prompts.length > 1) {
@@ -18030,6 +18919,12 @@ async function runAnalyze(opts, globals) {
18030
18919
  message = `Verity: request rejected \u2014 payload too large (${kb}KB over the 190KB cap). Commit or stash pre-existing changes, or run with --mode skip if you didn't intend to submit code this turn.`;
18031
18920
  } else if (result.category === "network" || result.category === "timeout") {
18032
18921
  message = `Verity offline \u2014 ${result.error}`;
18922
+ } else if (result.error.startsWith("STALE_VERIFICATION")) {
18923
+ message = "Verity: your GitHub verification expired \u2014 run `verity login` to re-verify (local analysis this run)";
18924
+ } else if (result.error.startsWith("FORBIDDEN")) {
18925
+ message = "Verity: no access grant for this repository \u2014 run `verity login` to refresh your grants (local analysis this run)";
18926
+ } else if (result.error.startsWith("INVALID_TOKEN")) {
18927
+ message = "Verity: your login expired or was revoked \u2014 run `verity login` to sign in again (local analysis this run)";
18033
18928
  } else if (result.http_status && result.http_status >= 400 && result.http_status < 500) {
18034
18929
  message = `Verity: request rejected (HTTP ${result.http_status}) \u2014 ${result.error}`;
18035
18930
  } else if (result.http_status && result.http_status >= 500) {
@@ -18044,8 +18939,115 @@ async function runAnalyze(opts, globals) {
18044
18939
  const response = result.data;
18045
18940
  const decision = response.gate_decision ?? "(unrecognised)";
18046
18941
  const sentPaths = codeDelta.files.map((f) => f.path);
18942
+ let openElsewhere = [];
18943
+ if (memorySession) {
18944
+ try {
18945
+ const st = foldDossier(memorySession.d);
18946
+ openElsewhere = openBlockingElsewhere(st.statements, sentPaths, (file, line) => {
18947
+ try {
18948
+ const src = (0, import_node_fs24.readFileSync)((0, import_node_path20.join)(repoRoot(), file), "utf8").split("\n");
18949
+ const at = src[line - 1];
18950
+ return at === void 0 ? null : lineSha(at);
18951
+ } catch {
18952
+ return null;
18953
+ }
18954
+ });
18955
+ } catch {
18956
+ }
18957
+ }
18958
+ const reviewCoverage = {
18959
+ reviewed: sentPaths,
18960
+ // Declared drops from the stages that DO report themselves today. The other
18961
+ // stages surface via `unaccounted`, which is the tripwire, not the design.
18962
+ notReviewed: [
18963
+ // Every exit from the collection loop, each named. Six reasons where there
18964
+ // used to be two recorded and four silent — the silent ones including the
18965
+ // per-file size cap, which could drop a whole source file without leaving a
18966
+ // trace anywhere in the payload or the run row.
18967
+ ...codeDelta.excluded,
18968
+ // The server-side 300-line middle-out truncation. It only bites on the
18969
+ // full-file branch (a first analysis, before snapshots exist) because
18970
+ // analyze normally sends diffs — but on that branch the reviewer sees the
18971
+ // first and last 100 lines and nothing between, and until now said so to
18972
+ // nobody. CAPACITY: a partial look is not a look.
18973
+ ...(response.metadata?.truncated_files ?? []).map((path) => ({
18974
+ path,
18975
+ reason: "file-middle-truncated-300-lines",
18976
+ stage: "prompt-builder",
18977
+ kind: "capacity"
18978
+ })),
18979
+ // The 20-entry edit cap. CAPACITY, and the sharpest of the lot: it narrows
18980
+ // what is REVIEWED, not merely what is summarised — a session editing 25
18981
+ // files had five silently excluded from the reviewed set.
18982
+ ...(actionSummary?.capped_out ?? []).map((path) => ({
18983
+ path,
18984
+ reason: "edit-list-cap-20",
18985
+ stage: "extractActionSummary",
18986
+ kind: "capacity"
18987
+ })),
18988
+ // ⚠ BASELINE SCOPING — the biggest source of false NOT A CLEAN REVIEW.
18989
+ //
18990
+ // The universe is `allChanged`, git's whole dirty tree. The reviewed set is
18991
+ // scoped to what THIS SESSION authored (the VRT-26 contamination cure), so
18992
+ // every pre-existing dirty file is in the universe, absent from `reviewed`,
18993
+ // and — until now — declared by nobody. It fell through to `unaccounted`,
18994
+ // became capacity, and produced "NOT A CLEAN REVIEW: admin.js" over a file
18995
+ // that was never this session's to review.
18996
+ //
18997
+ // Measured 2026-08-04: three consecutive runs over an untouched tree gave
18998
+ // three different answers — .claude/settings.json, then admin.js, then six
18999
+ // files — because each run took a different path and each path had a
19000
+ // different idea of the universe. POLICY: not this session's work is not a
19001
+ // coverage gap, it is the cure working.
19002
+ ...allChanged.filter((p) => !sentPaths.includes(p) && !codeDelta.excluded.some((e) => e.path === p)).filter((p) => analyzable.includes(p) || reviewable.includes(p) || securityFiles.includes(p)).map((path) => ({
19003
+ path,
19004
+ reason: "not-authored-this-session",
19005
+ stage: "baseline-scoping",
19006
+ kind: "policy"
19007
+ })),
19008
+ // The extension allowlist, and it is POLICY rather than capacity: a changed
19009
+ // README was never going to be reviewed, and treating that as a coverage
19010
+ // gap would downgrade nearly every PASS to WARN until WARN meant nothing.
19011
+ // Recorded so the ledger balances and so "what did Verity ignore entirely"
19012
+ // is answerable — but it never touches the verdict.
19013
+ ...allChanged.filter((p) => !analyzable.includes(p) && !reviewable.includes(p) && !securityFiles.includes(p)).map((path) => ({
19014
+ path,
19015
+ reason: "not-a-reviewed-file-type",
19016
+ stage: "extension-allowlist",
19017
+ kind: "policy"
19018
+ }))
19019
+ ]
19020
+ };
18047
19021
  const watermarkHash = sentPaths.length > 0 ? computeContentHash(sentPaths) : contentHash;
18048
19022
  const watermarkIsPartial = !!codeDelta.truncated;
19023
+ let silenced = null;
19024
+ let turnIsIdleForChannel = true;
19025
+ if (memorySession) {
19026
+ try {
19027
+ const st = foldDossier(memorySession.d);
19028
+ turnIsIdleForChannel = st.authored_all.every((a) => a.hash_now === a.hash_at_last_verdict);
19029
+ silenced = channelSilence({
19030
+ // The BUFFER, not intentContext.user_prompt: the latter falls back to a
19031
+ // linked issue (VRT-53 W4) when no human spoke, and a fallback goal is
19032
+ // not a user utterance. Treating it as one would keep the loop alive on
19033
+ // exactly the autonomous cohort.
19034
+ newUserPrompt: (conversation?.prompts?.length ?? 0) > 0,
19035
+ newAuthorship: !turnIsIdleForChannel,
19036
+ emittedLast: st.meta.channel?.emittedLast === true,
19037
+ consecutiveIdle: st.meta.channel?.consecutiveIdle ?? 0
19038
+ });
19039
+ } catch {
19040
+ silenced = null;
19041
+ }
19042
+ }
19043
+ if (silenced) {
19044
+ logEvent("channel_silenced", {
19045
+ reason: silenced,
19046
+ run_id: response.run_id ?? turnId,
19047
+ decision
19048
+ });
19049
+ }
19050
+ let intentRepeatCount = 0;
18049
19051
  if (memorySession) {
18050
19052
  try {
18051
19053
  recordVerdict(memorySession.d, {
@@ -18059,8 +19061,18 @@ async function runAnalyze(opts, globals) {
18059
19061
  pattern_id: f.pattern_id ?? f.rule_id,
18060
19062
  title: f.title,
18061
19063
  severity: f.severity
18062
- })) ?? []
19064
+ })) ?? [],
19065
+ intent: response.intent_alignment ?? null,
19066
+ // The same signal F1 introduced: bytes differing from the hash frozen at
19067
+ // the last verdict. A turn that moved nothing is the only kind that can
19068
+ // accumulate a repeat.
19069
+ idle: turnIsIdleForChannel,
19070
+ // What next turn reads as `emittedLast`. A suppressed turn did not
19071
+ // speak, so it cannot be the cause of the turn after it — which is what
19072
+ // keeps this from becoming a permanent gag.
19073
+ emitted: !silenced
18063
19074
  });
19075
+ intentRepeatCount = Math.max(0, (foldDossier(memorySession.d).meta.intent_repeat?.consecutive ?? 1) - 1);
18064
19076
  } catch {
18065
19077
  }
18066
19078
  }
@@ -18153,6 +19165,11 @@ async function runAnalyze(opts, globals) {
18153
19165
  }
18154
19166
  saveSnapshots(codeDelta.files.map((f) => ({ path: f.path, content: f.content })));
18155
19167
  const loginNudge = response.persisted === false ? " \u26A0\uFE0F Not logged in \u2014 this run was NOT saved and Verity has no memory of your project. Run `verity login` to unlock run history, trends, and cloud memory." : "";
19168
+ const grantWarning = reverifyNudge({
19169
+ grant_status: response.grant_status,
19170
+ reverify_by: response.reverify_by
19171
+ });
19172
+ const grantNudge = grantWarning ? ` \u26A0\uFE0F ${grantWarning}` : "";
18156
19173
  switch (decision) {
18157
19174
  case "FAIL": {
18158
19175
  writeIteration(iteration + 1, currentCommit, contentHash ?? void 0);
@@ -18229,7 +19246,22 @@ async function runAnalyze(opts, globals) {
18229
19246
  if (loginNudge) process.stderr.write(`
18230
19247
  ${YELLOW}${loginNudge.trim()}${NC}
18231
19248
  `);
18232
- process.exit(2);
19249
+ if (grantNudge) process.stderr.write(`
19250
+ ${YELLOW}${grantNudge.trim()}${NC}
19251
+ `);
19252
+ emitVerdict({
19253
+ proposed: "FAIL",
19254
+ changed: skipCoverageChanged,
19255
+ coverage: reviewCoverage,
19256
+ userSummary: "",
19257
+ // Subject to the SAME cycle cut as PASS/WARN. Suppressing here is safe:
19258
+ // the findings themselves are rendered above by the blocking renderer,
19259
+ // so what the cut removes is the repeated commentary, never the defect.
19260
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount),
19261
+ // The coverage note is silenced with it — half a channel is still a channel.
19262
+ silenced: !!silenced,
19263
+ openElsewhere
19264
+ });
18233
19265
  break;
18234
19266
  }
18235
19267
  case "PASS": {
@@ -18240,11 +19272,17 @@ ${YELLOW}${loginNudge.trim()}${NC}
18240
19272
  const viewUrl = response.view_url ?? "";
18241
19273
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
18242
19274
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
18243
- userSummary += loginNudge;
18244
- printJsonCompact(
18245
- buildHookOutput("PASS", userSummary, agentContextFor(response))
18246
- );
18247
- process.exit(0);
19275
+ userSummary += loginNudge + grantNudge;
19276
+ emitVerdict({
19277
+ proposed: "PASS",
19278
+ changed: skipCoverageChanged,
19279
+ coverage: reviewCoverage,
19280
+ userSummary,
19281
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount),
19282
+ // The coverage note is silenced with it — half a channel is still a channel.
19283
+ silenced: !!silenced,
19284
+ openElsewhere
19285
+ });
18248
19286
  break;
18249
19287
  }
18250
19288
  case "WARN": {
@@ -18254,16 +19292,22 @@ ${YELLOW}${loginNudge.trim()}${NC}
18254
19292
  const viewUrl = response.view_url ?? "";
18255
19293
  if (viewUrl) userSummary += ` Report: ${viewUrl}`;
18256
19294
  if (autoSeedNotice) userSummary = `${autoSeedNotice} ${userSummary}`;
18257
- userSummary += loginNudge;
18258
- printJsonCompact(
18259
- buildHookOutput("WARN", userSummary, agentContextFor(response))
18260
- );
18261
- process.exit(0);
19295
+ userSummary += loginNudge + grantNudge;
19296
+ emitVerdict({
19297
+ proposed: "WARN",
19298
+ changed: skipCoverageChanged,
19299
+ coverage: reviewCoverage,
19300
+ userSummary,
19301
+ agentContext: silenced ? null : agentContextFor(response, intentRepeatCount),
19302
+ // The coverage note is silenced with it — half a channel is still a channel.
19303
+ silenced: !!silenced,
19304
+ openElsewhere
19305
+ });
18262
19306
  break;
18263
19307
  }
18264
19308
  default: {
18265
19309
  const raw = String(decision ?? "(missing)");
18266
- const msg = (autoSeedNotice ? `${autoSeedNotice} Verity: unrecognised verdict \u2014 treating as WARN` : "Verity: unrecognised verdict \u2014 treating as WARN") + loginNudge;
19310
+ const msg = (autoSeedNotice ? `${autoSeedNotice} Verity: unrecognised verdict \u2014 treating as WARN` : "Verity: unrecognised verdict \u2014 treating as WARN") + loginNudge + grantNudge;
18267
19311
  process.stderr.write(
18268
19312
  `Verity: server returned an unrecognised gate_decision (${raw}). Rendering WARN rather than PASS. Update the CLI: npm i -g @codacy/verity-cli
18269
19313
  `
@@ -18371,8 +19415,8 @@ async function runReview(opts, globals) {
18371
19415
  for (const p of specPaths) {
18372
19416
  if (!(0, import_node_fs26.existsSync)(p)) continue;
18373
19417
  try {
18374
- const { readFileSync: readFileSync15 } = await import("node:fs");
18375
- const content = readFileSync15(p, "utf-8");
19418
+ const { readFileSync: readFileSync16 } = await import("node:fs");
19419
+ const content = readFileSync16(p, "utf-8");
18376
19420
  specs.push({ path: p, content: content.slice(0, 10240) });
18377
19421
  } catch {
18378
19422
  }
@@ -18430,9 +19474,9 @@ async function runReview(opts, globals) {
18430
19474
 
18431
19475
  // src/commands/guard.ts
18432
19476
  var import_node_fs27 = require("node:fs");
18433
- var import_node_path22 = require("node:path");
19477
+ var import_node_path21 = require("node:path");
18434
19478
  var GUARD_BLOCK_CAP = 2;
18435
- var GUARD_ITER_FILE = (0, import_node_path22.join)(VERITY_DIR, ".guard-iteration");
19479
+ var GUARD_ITER_FILE = (0, import_node_path21.join)(VERITY_DIR, ".guard-iteration");
18436
19480
  function readPreToolUseStdin() {
18437
19481
  const empty = { command: "", cwd: null, sessionId: null };
18438
19482
  return new Promise((resolve2) => {
@@ -18674,9 +19718,10 @@ async function runGuard(opts, globals) {
18674
19718
  cmd: "guard"
18675
19719
  });
18676
19720
  if (!result.ok) {
19721
+ 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." : "";
18677
19722
  emitAllowNotice(
18678
- `\u26A0 Verity ${moment}: service offline \u2014 ${verb}ed WITHOUT review`,
18679
- `Verity ${moment}: service unavailable (${result.error}); the ${verb} was allowed WITHOUT a Verity review.`
19723
+ `\u26A0 Verity ${moment}: ${authRemedy ? "not authorized" : "service offline"} \u2014 ${verb}ed WITHOUT review${authRemedy}`,
19724
+ `Verity ${moment}: ${authRemedy ? "not authorized" : "service unavailable"} (${result.error}); the ${verb} was allowed WITHOUT a Verity review.${authRemedy}`
18680
19725
  );
18681
19726
  }
18682
19727
  if (opts.json) process.stderr.write(JSON.stringify(result.data) + "\n");
@@ -18748,14 +19793,14 @@ function writeBlockMessage(moment, response) {
18748
19793
  // src/commands/init.ts
18749
19794
  var import_node_fs29 = require("node:fs");
18750
19795
  var import_promises13 = require("node:fs/promises");
18751
- var import_node_path24 = require("node:path");
18752
- var import_node_child_process11 = require("node:child_process");
19796
+ var import_node_path23 = require("node:path");
19797
+ var import_node_child_process10 = require("node:child_process");
18753
19798
  var readline2 = __toESM(require("node:readline/promises"));
18754
19799
 
18755
19800
  // src/commands/migrate.ts
18756
19801
  var import_node_fs28 = require("node:fs");
18757
- var import_node_path23 = require("node:path");
18758
- var import_node_child_process10 = require("node:child_process");
19802
+ var import_node_path22 = require("node:path");
19803
+ var import_node_child_process9 = require("node:child_process");
18759
19804
 
18760
19805
  // src/lib/telemetry.ts
18761
19806
  var import_promises12 = require("node:fs/promises");
@@ -18850,11 +19895,11 @@ async function uninstallTelemetry() {
18850
19895
  // src/commands/migrate.ts
18851
19896
  var LEGACY_NPM_PACKAGE = "@codacy/gate-cli";
18852
19897
  function defaultNpmRemover(pkg) {
18853
- (0, import_node_child_process10.execSync)(`npm rm -g ${pkg}`, { stdio: "pipe", timeout: 12e4 });
19898
+ (0, import_node_child_process9.execSync)(`npm rm -g ${pkg}`, { stdio: "pipe", timeout: 12e4 });
18854
19899
  }
18855
19900
  function isGitTracked(cwd, relPath) {
18856
19901
  try {
18857
- (0, import_node_child_process10.execSync)(`git ls-files --error-unmatch ${relPath}`, { cwd, stdio: "pipe" });
19902
+ (0, import_node_child_process9.execSync)(`git ls-files --error-unmatch ${relPath}`, { cwd, stdio: "pipe" });
18858
19903
  return true;
18859
19904
  } catch {
18860
19905
  return false;
@@ -18862,7 +19907,7 @@ function isGitTracked(cwd, relPath) {
18862
19907
  }
18863
19908
  function isGitRepo(cwd) {
18864
19909
  try {
18865
- (0, import_node_child_process10.execSync)("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
19910
+ (0, import_node_child_process9.execSync)("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
18866
19911
  return true;
18867
19912
  } catch {
18868
19913
  return false;
@@ -18883,8 +19928,8 @@ async function runMigration(opts = {}) {
18883
19928
  return { actions, migrated: actions.length > 0 };
18884
19929
  }
18885
19930
  function migrateProjectDir(root, actions) {
18886
- const gateDir = (0, import_node_path23.join)(root, ".gate");
18887
- const verityDir = (0, import_node_path23.join)(root, ".verity");
19931
+ const gateDir = (0, import_node_path22.join)(root, ".gate");
19932
+ const verityDir = (0, import_node_path22.join)(root, ".verity");
18888
19933
  if ((0, import_node_fs28.existsSync)(gateDir) && !(0, import_node_fs28.existsSync)(verityDir)) {
18889
19934
  return migrateProjectDirRename(root, gateDir, verityDir, actions);
18890
19935
  }
@@ -18902,7 +19947,7 @@ function migrateProjectDirRename(root, gateDir, verityDir, actions) {
18902
19947
  );
18903
19948
  }
18904
19949
  try {
18905
- (0, import_node_child_process10.execSync)("git mv .gate .verity", { cwd: root, stdio: "pipe" });
19950
+ (0, import_node_child_process9.execSync)("git mv .gate .verity", { cwd: root, stdio: "pipe" });
18906
19951
  actions.push("Moved .gate/ \u2192 .verity/ (git mv, staged)");
18907
19952
  moved = true;
18908
19953
  } catch {
@@ -18938,11 +19983,11 @@ function migrateProjectDirCarry(gateDir, verityDir, actions) {
18938
19983
  }
18939
19984
  function migrateGlobalCredentials(home, actions) {
18940
19985
  if (!home) return;
18941
- const gateCreds = (0, import_node_path23.join)(home, ".gate", "credentials");
18942
- const verityCreds = (0, import_node_path23.join)(home, ".verity", "credentials");
19986
+ const gateCreds = (0, import_node_path22.join)(home, ".gate", "credentials");
19987
+ const verityCreds = (0, import_node_path22.join)(home, ".verity", "credentials");
18943
19988
  if (!(0, import_node_fs28.existsSync)(gateCreds)) return;
18944
19989
  if (!(0, import_node_fs28.existsSync)(verityCreds)) {
18945
- (0, import_node_fs28.mkdirSync)((0, import_node_path23.join)(home, ".verity"), { recursive: true });
19990
+ (0, import_node_fs28.mkdirSync)((0, import_node_path22.join)(home, ".verity"), { recursive: true });
18946
19991
  moveFile(gateCreds, verityCreds);
18947
19992
  actions.push("Moved ~/.gate/credentials \u2192 ~/.verity/credentials");
18948
19993
  return;
@@ -18964,7 +20009,7 @@ async function migrateLegacyHooks(root, actions) {
18964
20009
  }
18965
20010
  }
18966
20011
  async function migrateClaudeMd(root, actions) {
18967
- const claudeMd = (0, import_node_path23.join)(root, "CLAUDE.md");
20012
+ const claudeMd = (0, import_node_path22.join)(root, "CLAUDE.md");
18968
20013
  const hadLegacyBlock = (0, import_node_fs28.existsSync)(claudeMd) && hasLegacyMemoryBlock(readFileSyncSafe(claudeMd));
18969
20014
  if (!hadLegacyBlock) return;
18970
20015
  try {
@@ -18975,13 +20020,13 @@ async function migrateClaudeMd(root, actions) {
18975
20020
  }
18976
20021
  }
18977
20022
  function migrateStandardFile(root, actions) {
18978
- const gateMd = (0, import_node_path23.join)(root, "GATE.md");
18979
- const verityMd = (0, import_node_path23.join)(root, "VERITY.md");
20023
+ const gateMd = (0, import_node_path22.join)(root, "GATE.md");
20024
+ const verityMd = (0, import_node_path22.join)(root, "VERITY.md");
18980
20025
  if (!(0, import_node_fs28.existsSync)(gateMd) || (0, import_node_fs28.existsSync)(verityMd)) return;
18981
20026
  let moved = false;
18982
20027
  if (isGitRepo(root) && isGitTracked(root, "GATE.md")) {
18983
20028
  try {
18984
- (0, import_node_child_process10.execSync)("git mv GATE.md VERITY.md", { cwd: root, stdio: "pipe" });
20029
+ (0, import_node_child_process9.execSync)("git mv GATE.md VERITY.md", { cwd: root, stdio: "pipe" });
18985
20030
  moved = true;
18986
20031
  } catch {
18987
20032
  }
@@ -18993,7 +20038,7 @@ function migrateStandardFile(root, actions) {
18993
20038
  actions.push("Renamed GATE.md \u2192 VERITY.md");
18994
20039
  }
18995
20040
  async function migrateTelemetryHeaders(root, actions) {
18996
- const file = (0, import_node_path23.join)(root, ".claude", "settings.local.json");
20041
+ const file = (0, import_node_path22.join)(root, ".claude", "settings.local.json");
18997
20042
  if (!(0, import_node_fs28.existsSync)(file)) return;
18998
20043
  let settings;
18999
20044
  try {
@@ -19056,7 +20101,7 @@ function readFileSyncSafe(path) {
19056
20101
  }
19057
20102
  function hasStagedChanges(root) {
19058
20103
  try {
19059
- (0, import_node_child_process10.execSync)("git diff --cached --quiet", { cwd: root, stdio: "pipe" });
20104
+ (0, import_node_child_process9.execSync)("git diff --cached --quiet", { cwd: root, stdio: "pipe" });
19060
20105
  return false;
19061
20106
  } catch {
19062
20107
  return true;
@@ -19083,15 +20128,15 @@ function moveFile(from, to) {
19083
20128
  function carryLegacyContents(gateDir, verityDir) {
19084
20129
  let copied = 0;
19085
20130
  const walk = (relDir) => {
19086
- const srcDir = (0, import_node_path23.join)(gateDir, relDir);
20131
+ const srcDir = (0, import_node_path22.join)(gateDir, relDir);
19087
20132
  for (const entry of (0, import_node_fs28.readdirSync)(srcDir)) {
19088
- const rel = relDir ? (0, import_node_path23.join)(relDir, entry) : entry;
19089
- const src = (0, import_node_path23.join)(gateDir, rel);
19090
- const dest = (0, import_node_path23.join)(verityDir, rel);
20133
+ const rel = relDir ? (0, import_node_path22.join)(relDir, entry) : entry;
20134
+ const src = (0, import_node_path22.join)(gateDir, rel);
20135
+ const dest = (0, import_node_path22.join)(verityDir, rel);
19091
20136
  if ((0, import_node_fs28.statSync)(src).isDirectory()) {
19092
20137
  walk(rel);
19093
20138
  } else if (!(0, import_node_fs28.existsSync)(dest)) {
19094
- (0, import_node_fs28.mkdirSync)((0, import_node_path23.dirname)(dest), { recursive: true });
20139
+ (0, import_node_fs28.mkdirSync)((0, import_node_path22.dirname)(dest), { recursive: true });
19095
20140
  (0, import_node_fs28.cpSync)(src, dest);
19096
20141
  copied++;
19097
20142
  }
@@ -19101,22 +20146,22 @@ function carryLegacyContents(gateDir, verityDir) {
19101
20146
  return copied;
19102
20147
  }
19103
20148
  async function needsMigration(root = repoRoot()) {
19104
- const gateDir = (0, import_node_path23.join)(root, ".gate");
19105
- const verityDir = (0, import_node_path23.join)(root, ".verity");
20149
+ const gateDir = (0, import_node_path22.join)(root, ".gate");
20150
+ const verityDir = (0, import_node_path22.join)(root, ".verity");
19106
20151
  if ((0, import_node_fs28.existsSync)(gateDir) && !(0, import_node_fs28.existsSync)(verityDir)) return true;
19107
20152
  if ((0, import_node_fs28.existsSync)(gateDir) && (0, import_node_fs28.existsSync)(verityDir)) {
19108
- if ((0, import_node_fs28.existsSync)((0, import_node_path23.join)(gateDir, "credentials")) && !(0, import_node_fs28.existsSync)((0, import_node_path23.join)(verityDir, "credentials"))) {
20153
+ if ((0, import_node_fs28.existsSync)((0, import_node_path22.join)(gateDir, "credentials")) && !(0, import_node_fs28.existsSync)((0, import_node_path22.join)(verityDir, "credentials"))) {
19109
20154
  return true;
19110
20155
  }
19111
- if ((0, import_node_fs28.existsSync)((0, import_node_path23.join)(gateDir, "memory")) && !(0, import_node_fs28.existsSync)((0, import_node_path23.join)(verityDir, "memory"))) {
20156
+ if ((0, import_node_fs28.existsSync)((0, import_node_path22.join)(gateDir, "memory")) && !(0, import_node_fs28.existsSync)((0, import_node_path22.join)(verityDir, "memory"))) {
19112
20157
  return true;
19113
20158
  }
19114
20159
  }
19115
- const claudeMd = (0, import_node_path23.join)(root, "CLAUDE.md");
20160
+ const claudeMd = (0, import_node_path22.join)(root, "CLAUDE.md");
19116
20161
  if ((0, import_node_fs28.existsSync)(claudeMd) && hasLegacyMemoryBlock(readFileSyncSafe(claudeMd))) {
19117
20162
  return true;
19118
20163
  }
19119
- if ((0, import_node_fs28.existsSync)((0, import_node_path23.join)(root, "GATE.md")) && !(0, import_node_fs28.existsSync)((0, import_node_path23.join)(root, "VERITY.md"))) {
20164
+ if ((0, import_node_fs28.existsSync)((0, import_node_path22.join)(root, "GATE.md")) && !(0, import_node_fs28.existsSync)((0, import_node_path22.join)(root, "VERITY.md"))) {
19120
20165
  return true;
19121
20166
  }
19122
20167
  if (await hasLegacyHooksAt(root)) return true;
@@ -19152,12 +20197,25 @@ async function promptYes(question) {
19152
20197
  rl.close();
19153
20198
  }
19154
20199
  }
19155
- async function confirmExistingLogin(serviceUrl, opts) {
20200
+ async function confirmExistingLogin(serviceUrl, remote, opts) {
19156
20201
  const existing = await resolveToken(opts.token);
19157
20202
  if (!existing.ok) return "drive-login";
19158
20203
  const who = await whoami(existing.data.token, serviceUrl, opts.verbose);
19159
20204
  if (who.ok && who.data.logged_in) {
19160
- printInfo(`Logged in as ${who.data.email ?? `user #${who.data.user_id}`} \u2713 \u2014 runs & memory sync to Verity.`);
20205
+ const identity = who.data.email ?? `user #${who.data.user_id}`;
20206
+ const covered = who.data.grant_status != null;
20207
+ if (!remote) {
20208
+ printInfo(`Logged in as ${identity} \u2713`);
20209
+ printInfo(" This directory has no git remote, so there is no project here to sync to.");
20210
+ } else if (covered) {
20211
+ printInfo(`Logged in as ${identity} \u2713 \u2014 runs & memory sync to Verity.`);
20212
+ } else {
20213
+ printInfo(`Logged in as ${identity} \u2713`);
20214
+ printWarn(" This repository is NOT covered by your Verity access grants \u2014 nothing will sync.");
20215
+ printInfo(' Grant the Verity GitHub App access to it, then run "verity login" to refresh your grants.');
20216
+ }
20217
+ const nudge = reverifyNudge(who.data);
20218
+ if (nudge) printWarn(` ${nudge}`);
19161
20219
  return "handled";
19162
20220
  }
19163
20221
  if (!who.ok) {
@@ -19186,18 +20244,18 @@ async function runOptionalAuth(resolution, opts = {}) {
19186
20244
  printInfo(" Log in below to re-register this project and repair ~/.verity/credentials.");
19187
20245
  }
19188
20246
  }
19189
- if (!healed) {
19190
- const state = await confirmExistingLogin(serviceUrl, opts);
19191
- if (state === "handled") return;
19192
- }
19193
20247
  let remote = "";
19194
20248
  try {
19195
- remote = (0, import_node_child_process11.execSync)("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
20249
+ remote = (0, import_node_child_process10.execSync)("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
19196
20250
  } catch {
19197
20251
  }
20252
+ if (!healed) {
20253
+ const state = await confirmExistingLogin(serviceUrl, remote, opts);
20254
+ if (state === "handled") return;
20255
+ }
19198
20256
  const localOnlyNote = () => {
19199
20257
  printInfo("Verity runs in local-only mode: the gate still runs and shows static findings, but nothing uploads.");
19200
- printInfo(' Authenticate anytime: run "verity init" again, or "verity auth register".');
20258
+ printInfo(' Authenticate anytime: run "verity login" (one login covers every repo you can write to).');
19201
20259
  };
19202
20260
  if (process.stdin.isTTY && process.stdout.isTTY) {
19203
20261
  console.log("");
@@ -19223,7 +20281,7 @@ async function runOptionalAuth(resolution, opts = {}) {
19223
20281
  localOnlyNote();
19224
20282
  return;
19225
20283
  }
19226
- const projectName = parseRemote(remote)?.repo ?? (0, import_node_path24.basename)(process.cwd());
20284
+ const projectName = parseRemote(remote)?.repo ?? (0, import_node_path23.basename)(process.cwd());
19227
20285
  printInfo("Authenticating with GitHub\u2026");
19228
20286
  const result = await registerProject({ projectName, remote, serviceUrl, verbose: opts.verbose });
19229
20287
  if (result.ok) {
@@ -19236,15 +20294,15 @@ async function runOptionalAuth(resolution, opts = {}) {
19236
20294
  }
19237
20295
  function resolveDataDir() {
19238
20296
  const candidates = [
19239
- (0, import_node_path24.join)(__dirname, "..", "data"),
20297
+ (0, import_node_path23.join)(__dirname, "..", "data"),
19240
20298
  // installed: node_modules/@codacy/verity-cli/data
19241
- (0, import_node_path24.join)(__dirname, "..", "..", "data"),
20299
+ (0, import_node_path23.join)(__dirname, "..", "..", "data"),
19242
20300
  // edge case: nested resolution
19243
- (0, import_node_path24.join)(process.cwd(), "cli", "data")
20301
+ (0, import_node_path23.join)(process.cwd(), "cli", "data")
19244
20302
  // local dev: running from repo root
19245
20303
  ];
19246
20304
  for (const candidate of candidates) {
19247
- if ((0, import_node_fs29.existsSync)((0, import_node_path24.join)(candidate, "skills"))) {
20305
+ if ((0, import_node_fs29.existsSync)((0, import_node_path23.join)(candidate, "skills"))) {
19248
20306
  return candidate;
19249
20307
  }
19250
20308
  }
@@ -19288,30 +20346,30 @@ function registerInitCommand(program2) {
19288
20346
  }
19289
20347
  printInfo(` Node.js ${nodeVersion} \u2713`);
19290
20348
  try {
19291
- const gitVersion = (0, import_node_child_process11.execSync)("git --version", { encoding: "utf-8" }).trim();
20349
+ const gitVersion = (0, import_node_child_process10.execSync)("git --version", { encoding: "utf-8" }).trim();
19292
20350
  printInfo(` ${gitVersion} \u2713`);
19293
20351
  } catch {
19294
20352
  printError("git is required but not installed. Install from https://git-scm.com");
19295
20353
  process.exit(1);
19296
20354
  }
19297
20355
  try {
19298
- (0, import_node_child_process11.execSync)("which claude", { encoding: "utf-8" });
20356
+ (0, import_node_child_process10.execSync)("which claude", { encoding: "utf-8" });
19299
20357
  printInfo(" Claude Code \u2713");
19300
20358
  } catch {
19301
20359
  printWarn(" Claude Code not found \u2014 hooks will be configured but need Claude Code to run.");
19302
20360
  }
19303
20361
  try {
19304
- (0, import_node_child_process11.execSync)("which codacy-analysis", { encoding: "utf-8", stdio: "pipe" });
20362
+ (0, import_node_child_process10.execSync)("which codacy-analysis", { encoding: "utf-8", stdio: "pipe" });
19305
20363
  printInfo(" @codacy/analysis-cli \u2713");
19306
20364
  } catch {
19307
20365
  printInfo(" Installing @codacy/analysis-cli...");
19308
20366
  try {
19309
- (0, import_node_child_process11.execSync)("npm install -g @codacy/analysis-cli", { encoding: "utf-8", stdio: "pipe", timeout: 12e4 });
20367
+ (0, import_node_child_process10.execSync)("npm install -g @codacy/analysis-cli", { encoding: "utf-8", stdio: "pipe", timeout: 12e4 });
19310
20368
  printInfo(" @codacy/analysis-cli installed \u2713");
19311
20369
  } catch {
19312
20370
  try {
19313
20371
  printWarn(" Retrying with sudo...");
19314
- (0, import_node_child_process11.execSync)("sudo npm install -g @codacy/analysis-cli", { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
20372
+ (0, import_node_child_process10.execSync)("sudo npm install -g @codacy/analysis-cli", { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
19315
20373
  printInfo(" @codacy/analysis-cli installed \u2713");
19316
20374
  } catch {
19317
20375
  printWarn(" Could not install @codacy/analysis-cli automatically.");
@@ -19323,20 +20381,20 @@ function registerInitCommand(program2) {
19323
20381
  console.log("");
19324
20382
  printInfo("Installing skills...");
19325
20383
  const dataDir = resolveDataDir();
19326
- const skillsSource = (0, import_node_path24.join)(dataDir, "skills");
20384
+ const skillsSource = (0, import_node_path23.join)(dataDir, "skills");
19327
20385
  const skillsDest = ".claude/skills";
19328
20386
  const skills = ["verity-setup", "verity-analyze", "verity-status", "verity-feedback", "verity-learn", "verity-memory", "verity-insights", "verity-reflect"];
19329
20387
  let skillsInstalled = 0;
19330
20388
  for (const skill of skills) {
19331
- const src = (0, import_node_path24.join)(skillsSource, skill);
19332
- const dest = (0, import_node_path24.join)(skillsDest, skill);
20389
+ const src = (0, import_node_path23.join)(skillsSource, skill);
20390
+ const dest = (0, import_node_path23.join)(skillsDest, skill);
19333
20391
  if (!(0, import_node_fs29.existsSync)(src)) {
19334
20392
  printWarn(` Skill data not found: ${skill}`);
19335
20393
  continue;
19336
20394
  }
19337
20395
  if ((0, import_node_fs29.existsSync)(dest) && !force) {
19338
- const srcSkill = (0, import_node_path24.join)(src, "SKILL.md");
19339
- const destSkill = (0, import_node_path24.join)(dest, "SKILL.md");
20396
+ const srcSkill = (0, import_node_path23.join)(src, "SKILL.md");
20397
+ const destSkill = (0, import_node_path23.join)(dest, "SKILL.md");
19340
20398
  if ((0, import_node_fs29.existsSync)(destSkill)) {
19341
20399
  try {
19342
20400
  const srcContent = await (0, import_promises13.readFile)(srcSkill, "utf-8");
@@ -19374,7 +20432,7 @@ function registerInitCommand(program2) {
19374
20432
  } catch (err) {
19375
20433
  printWarn(` Could not update CLAUDE.md: ${err.message}`);
19376
20434
  }
19377
- const globalVerityDir = (0, import_node_path24.join)(process.env.HOME ?? "", ".verity");
20435
+ const globalVerityDir = (0, import_node_path23.join)(process.env.HOME ?? "", ".verity");
19378
20436
  await (0, import_promises13.mkdir)(globalVerityDir, { recursive: true });
19379
20437
  console.log("");
19380
20438
  try {
@@ -19403,14 +20461,14 @@ function registerInitCommand(program2) {
19403
20461
  console.log(" .verity/memory/ \u2014 knowledge base (8 domains, commit to git)");
19404
20462
  console.log("");
19405
20463
  console.log(" Next step: open this project in Claude Code and run /verity-setup");
19406
- console.log(' (Not authenticated? Verity runs in local-only mode until you run "verity auth register".)');
20464
+ console.log(' (Not authenticated? Verity runs in local-only mode until you run "verity login".)');
19407
20465
  console.log("");
19408
20466
  });
19409
20467
  }
19410
20468
 
19411
20469
  // src/commands/uninstall.ts
19412
20470
  var import_node_fs30 = require("node:fs");
19413
- var import_node_path25 = require("node:path");
20471
+ var import_node_path24 = require("node:path");
19414
20472
  var SKILL_NAMES = [
19415
20473
  "verity-setup",
19416
20474
  "verity-analyze",
@@ -19429,7 +20487,7 @@ function registerUninstallCommand(program2) {
19429
20487
  const actions = [];
19430
20488
  const skillsRoot = projectPath(".claude/skills");
19431
20489
  for (const name of SKILL_NAMES) {
19432
- const dir = (0, import_node_path25.join)(skillsRoot, name);
20490
+ const dir = (0, import_node_path24.join)(skillsRoot, name);
19433
20491
  if ((0, import_node_fs30.existsSync)(dir)) {
19434
20492
  actions.push({
19435
20493
  label: `Remove .claude/skills/${name}/`,
@@ -19475,7 +20533,7 @@ function registerUninstallCommand(program2) {
19475
20533
  }
19476
20534
  });
19477
20535
  const home = process.env.HOME ?? "";
19478
- const globalVerityDir = (0, import_node_path25.join)(home, ".verity");
20536
+ const globalVerityDir = (0, import_node_path24.join)(home, ".verity");
19479
20537
  if (purgeGlobal && (0, import_node_fs30.existsSync)(globalVerityDir)) {
19480
20538
  actions.push({
19481
20539
  label: `Remove ~/.verity/ (global credentials \u2014 reconnect requires re-registration)`,
@@ -19674,7 +20732,7 @@ function registerTaskCommands(program2) {
19674
20732
 
19675
20733
  // src/commands/reset.ts
19676
20734
  var import_node_fs31 = require("node:fs");
19677
- var import_node_path26 = require("node:path");
20735
+ var import_node_path25 = require("node:path");
19678
20736
  function registerResetCommand(program2) {
19679
20737
  program2.command("reset").description("Close the current task and clear transient state").option("--keep-task", "Only purge caches; leave the current task open").option("--all", "Also purge diagnostic logs (.verity/.logs/)").action(async (opts) => {
19680
20738
  const globals = program2.opts();
@@ -19715,7 +20773,7 @@ function registerResetCommand(program2) {
19715
20773
  for (const entry of (0, import_node_fs31.readdirSync)(cacheDir)) {
19716
20774
  if (entry.startsWith("pending-")) {
19717
20775
  try {
19718
- (0, import_node_fs31.unlinkSync)((0, import_node_path26.join)(cacheDir, entry));
20776
+ (0, import_node_fs31.unlinkSync)((0, import_node_path25.join)(cacheDir, entry));
19719
20777
  purged++;
19720
20778
  } catch {
19721
20779
  }
@@ -19742,7 +20800,7 @@ function registerResetCommand(program2) {
19742
20800
  if ((0, import_node_fs31.existsSync)(logsDir)) {
19743
20801
  for (const entry of (0, import_node_fs31.readdirSync)(logsDir)) {
19744
20802
  try {
19745
- (0, import_node_fs31.unlinkSync)((0, import_node_path26.join)(logsDir, entry));
20803
+ (0, import_node_fs31.unlinkSync)((0, import_node_path25.join)(logsDir, entry));
19746
20804
  } catch {
19747
20805
  }
19748
20806
  }
@@ -20010,7 +21068,11 @@ function registerTelemetryCommands(program2) {
20010
21068
  const globals = program2.opts();
20011
21069
  const tokenResult = await resolveToken(globals.token);
20012
21070
  if (tokenResult.ok) {
20013
- printJsonCompact({ Authorization: `Bearer ${tokenResult.data.token}` });
21071
+ const remote = requestRemote();
21072
+ printJsonCompact({
21073
+ Authorization: `Bearer ${tokenResult.data.token}`,
21074
+ ...remote ? { "X-Verity-Remote": remote } : {}
21075
+ });
20014
21076
  } else {
20015
21077
  printJsonCompact({});
20016
21078
  }
@@ -20040,7 +21102,7 @@ function registerTelemetryCommands(program2) {
20040
21102
  }
20041
21103
 
20042
21104
  // src/cli.ts
20043
- program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.a57c8d9").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async () => {
21105
+ program.name("verity").description("CLI for Verity quality gate service").version("0.28.1-experimental.a72d2d9").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async () => {
20044
21106
  try {
20045
21107
  await foldLegacyLocalCredential();
20046
21108
  } catch {
@@ -20048,6 +21110,9 @@ program.name("verity").description("CLI for Verity quality gate service").versio
20048
21110
  });
20049
21111
  registerAuthCommands(program);
20050
21112
  registerLoginCommand(program);
21113
+ registerTokenCommand(program);
21114
+ registerSessionsCommands(program);
21115
+ registerLogoutCommand(program);
20051
21116
  registerHooksCommands(program);
20052
21117
  registerIntentCommands(program);
20053
21118
  registerLifecycleCommands(program);