@codacy/verity-cli 0.24.0-experimental.fe39839 → 0.25.0-experimental.15d1b58

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
@@ -10326,8 +10326,8 @@ var {
10326
10326
 
10327
10327
  // src/commands/auth.ts
10328
10328
  var import_promises3 = require("node:fs/promises");
10329
- var import_node_child_process3 = require("node:child_process");
10330
- var import_node_path2 = require("node:path");
10329
+ var import_node_child_process4 = require("node:child_process");
10330
+ var import_node_path3 = require("node:path");
10331
10331
 
10332
10332
  // src/lib/auth.ts
10333
10333
  var import_promises = require("node:fs/promises");
@@ -10476,6 +10476,10 @@ var SECURITY_PATTERNS = [
10476
10476
  ];
10477
10477
  var PROD_SERVICE_URL = "https://ofcamwrjwrkazqvdchko.supabase.co/functions/v1";
10478
10478
  var DEFAULT_SERVICE_URL = "https://yyfaqvcgslcrzvrbvqik.supabase.co/functions/v1".length > 0 ? "https://yyfaqvcgslcrzvrbvqik.supabase.co/functions/v1" : PROD_SERVICE_URL;
10479
+ var GITHUB_CLIENT_ID = "Ov23liBpj42KMTtUtN10";
10480
+ var GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
10481
+ var GITHUB_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
10482
+ var GITHUB_OAUTH_SCOPES = "repo user:email";
10479
10483
 
10480
10484
  // src/lib/auth.ts
10481
10485
  async function resolveToken(flagToken) {
@@ -10641,7 +10645,8 @@ async function apiRequest(options) {
10641
10645
  timeout = 9e4,
10642
10646
  cmd = "unknown",
10643
10647
  retry = false,
10644
- encodeBody = false
10648
+ encodeBody = false,
10649
+ extraHeaders
10645
10650
  } = options;
10646
10651
  const url = `${serviceUrl}${path}`;
10647
10652
  const headers = {
@@ -10650,6 +10655,9 @@ async function apiRequest(options) {
10650
10655
  if (token) {
10651
10656
  headers["Authorization"] = `Bearer ${token}`;
10652
10657
  }
10658
+ if (extraHeaders) {
10659
+ Object.assign(headers, extraHeaders);
10660
+ }
10653
10661
  const testMockScenario = process.env.VERITY_TEST_MOCK_SCENARIO;
10654
10662
  if (testMockScenario) {
10655
10663
  headers["X-Verity-Mock-Scenario"] = testMockScenario;
@@ -10740,6 +10748,324 @@ function analyzeRequest(options) {
10740
10748
  });
10741
10749
  }
10742
10750
 
10751
+ // src/lib/provider-auth.ts
10752
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
10753
+ var form = (fields) => new URLSearchParams(fields).toString();
10754
+ async function githubDeviceFlow() {
10755
+ const override = process.env.VERITY_PROVIDER_TOKEN;
10756
+ if (override) return { ok: true, data: override };
10757
+ let dc;
10758
+ try {
10759
+ const res = await fetch(GITHUB_DEVICE_CODE_URL, {
10760
+ method: "POST",
10761
+ headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" },
10762
+ body: form({ client_id: GITHUB_CLIENT_ID, scope: GITHUB_OAUTH_SCOPES })
10763
+ });
10764
+ if (!res.ok) {
10765
+ return { ok: false, error: `GitHub device-code request failed (HTTP ${res.status})` };
10766
+ }
10767
+ dc = await res.json();
10768
+ } catch (err) {
10769
+ return { ok: false, error: `Network error contacting GitHub: ${err.message}` };
10770
+ }
10771
+ if (!dc.device_code || !dc.user_code) {
10772
+ return {
10773
+ ok: false,
10774
+ error: "GitHub did not return a device code (is Device Flow enabled on the OAuth app?)"
10775
+ };
10776
+ }
10777
+ printInfo("");
10778
+ printInfo(`To authorize Verity, open: ${dc.verification_uri}`);
10779
+ printInfo(`And enter the code: ${dc.user_code}`);
10780
+ printInfo("Waiting for authorization\u2026");
10781
+ const deadline = Date.now() + (dc.expires_in || 900) * 1e3;
10782
+ let interval = dc.interval || 5;
10783
+ while (Date.now() < deadline) {
10784
+ await sleep(interval * 1e3);
10785
+ let data;
10786
+ try {
10787
+ const res = await fetch(GITHUB_ACCESS_TOKEN_URL, {
10788
+ method: "POST",
10789
+ headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" },
10790
+ body: form({
10791
+ client_id: GITHUB_CLIENT_ID,
10792
+ device_code: dc.device_code,
10793
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
10794
+ })
10795
+ });
10796
+ data = await res.json().catch(() => ({}));
10797
+ } catch {
10798
+ continue;
10799
+ }
10800
+ if (data.access_token) return { ok: true, data: data.access_token };
10801
+ switch (data.error) {
10802
+ case "authorization_pending":
10803
+ break;
10804
+ case "slow_down":
10805
+ interval += 5;
10806
+ break;
10807
+ case "access_denied":
10808
+ return { ok: false, error: "Authorization was denied on GitHub." };
10809
+ case "expired_token":
10810
+ return { ok: false, error: "The authorization code expired. Re-run register." };
10811
+ default:
10812
+ if (data.error) return { ok: false, error: `GitHub auth error: ${data.error}` };
10813
+ }
10814
+ }
10815
+ return { ok: false, error: "Timed out waiting for GitHub authorization." };
10816
+ }
10817
+
10818
+ // src/lib/git.ts
10819
+ var import_node_child_process3 = require("node:child_process");
10820
+ var import_node_fs2 = require("node:fs");
10821
+ var import_node_path2 = require("node:path");
10822
+ function resolveFile(relpath) {
10823
+ if ((0, import_node_fs2.existsSync)(relpath)) return relpath;
10824
+ if ((0, import_node_fs2.existsSync)(".claude/worktrees")) {
10825
+ try {
10826
+ const entries = (0, import_node_fs2.readdirSync)(".claude/worktrees", { withFileTypes: true });
10827
+ for (const entry of entries) {
10828
+ if (!entry.isDirectory()) continue;
10829
+ const candidate = (0, import_node_path2.join)(".claude/worktrees", entry.name, relpath);
10830
+ if ((0, import_node_fs2.existsSync)(candidate)) return candidate;
10831
+ }
10832
+ } catch {
10833
+ }
10834
+ }
10835
+ return null;
10836
+ }
10837
+ function execGit(cmd) {
10838
+ try {
10839
+ return (0, import_node_child_process3.execSync)(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
10840
+ } catch {
10841
+ return "";
10842
+ }
10843
+ }
10844
+ function splitLines(s) {
10845
+ return s.split("\n").filter((l) => l.length > 0);
10846
+ }
10847
+ var SHA_RE = /^[0-9a-f]{40}$/;
10848
+ function readBaselineSha() {
10849
+ if (!(0, import_node_fs2.existsSync)(BASELINE_SHA_FILE)) return null;
10850
+ let sha;
10851
+ try {
10852
+ sha = (0, import_node_fs2.readFileSync)(BASELINE_SHA_FILE, "utf-8").trim();
10853
+ } catch {
10854
+ return null;
10855
+ }
10856
+ if (!SHA_RE.test(sha)) return null;
10857
+ const reachable = execGit(`git cat-file -e ${sha}^{commit} 2>/dev/null && echo ok`) === "ok";
10858
+ if (!reachable) {
10859
+ try {
10860
+ (0, import_node_fs2.unlinkSync)(BASELINE_SHA_FILE);
10861
+ } catch {
10862
+ }
10863
+ return null;
10864
+ }
10865
+ return sha;
10866
+ }
10867
+ function writeBaselineSha(sha) {
10868
+ if (!SHA_RE.test(sha)) return;
10869
+ try {
10870
+ (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(BASELINE_SHA_FILE), { recursive: true });
10871
+ (0, import_node_fs2.writeFileSync)(BASELINE_SHA_FILE, sha);
10872
+ } catch {
10873
+ }
10874
+ }
10875
+ function getChangedFiles() {
10876
+ const sets = /* @__PURE__ */ new Set();
10877
+ let hasRecentCommitFiles = false;
10878
+ for (const f of splitLines(execGit("git diff --name-only HEAD"))) sets.add(f);
10879
+ for (const f of splitLines(execGit("git diff --name-only --cached"))) sets.add(f);
10880
+ for (const f of splitLines(execGit("git ls-files --others --exclude-standard"))) sets.add(f);
10881
+ const baseline = readBaselineSha();
10882
+ if (baseline) {
10883
+ const committed = splitLines(execGit(`git diff --name-only ${baseline}..HEAD`));
10884
+ if (committed.length > 0) {
10885
+ hasRecentCommitFiles = true;
10886
+ for (const f of committed) sets.add(f);
10887
+ }
10888
+ } else {
10889
+ const headTimestamp = parseInt(execGit("git log -1 --format=%ct HEAD"), 10) || 0;
10890
+ const commitAge = Math.floor(Date.now() / 1e3) - headTimestamp;
10891
+ const hasUnstaged = splitLines(execGit("git diff --name-only HEAD")).length > 0;
10892
+ const hasStaged = splitLines(execGit("git diff --name-only --cached")).length > 0;
10893
+ if (commitAge < 120 && !hasUnstaged && !hasStaged) {
10894
+ const recentFiles = splitLines(execGit("git diff --name-only HEAD~1..HEAD"));
10895
+ if (recentFiles.length > 0) {
10896
+ hasRecentCommitFiles = true;
10897
+ for (const f of recentFiles) sets.add(f);
10898
+ }
10899
+ }
10900
+ }
10901
+ for (const f of getWorktreeFiles()) sets.add(f);
10902
+ const filtered = Array.from(sets).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
10903
+ return { files: filtered, hasRecentCommitFiles };
10904
+ }
10905
+ function getStagedFiles() {
10906
+ return splitLines(execGit("git diff --cached --name-only")).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
10907
+ }
10908
+ function getDirtyFiles() {
10909
+ const set = /* @__PURE__ */ new Set();
10910
+ for (const f of splitLines(execGit("git diff --name-only HEAD"))) set.add(f);
10911
+ for (const f of splitLines(execGit("git diff --name-only --cached"))) set.add(f);
10912
+ for (const f of splitLines(execGit("git ls-files --others --exclude-standard"))) set.add(f);
10913
+ return Array.from(set).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
10914
+ }
10915
+ function showContentAtRef(ref, repoRelPath) {
10916
+ if (!ref || ref === "no-git") return null;
10917
+ const normalizedPath = repoRelPath.replace(/\\/g, "/");
10918
+ try {
10919
+ return (0, import_node_child_process3.execFileSync)("git", ["show", `${ref}:${normalizedPath}`], {
10920
+ encoding: "utf-8",
10921
+ maxBuffer: 64 * 1024 * 1024,
10922
+ stdio: ["pipe", "pipe", "pipe"]
10923
+ });
10924
+ } catch {
10925
+ return null;
10926
+ }
10927
+ }
10928
+ function getPushRangeFiles() {
10929
+ const diff = (range) => splitLines(execGit(`git diff --name-only ${range}`)).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
10930
+ const resolvers = [
10931
+ () => execGit("git rev-parse --abbrev-ref --symbolic-full-name @{push}") ? "@{push}..HEAD" : null,
10932
+ () => execGit("git rev-parse --abbrev-ref --symbolic-full-name @{upstream}") ? "@{upstream}..HEAD" : null,
10933
+ () => {
10934
+ const branch = execGit("git rev-parse --abbrev-ref HEAD");
10935
+ return branch && branch !== "HEAD" && execGit(`git rev-parse --verify -q origin/${branch}`) ? `origin/${branch}..HEAD` : null;
10936
+ }
10937
+ ];
10938
+ for (const resolve of resolvers) {
10939
+ const range = resolve();
10940
+ if (range) return { files: diff(range), range };
10941
+ }
10942
+ const baseline = readBaselineSha();
10943
+ if (baseline) {
10944
+ const files = diff(`${baseline}..HEAD`);
10945
+ if (files.length > 0) return { files, range: `${baseline}..HEAD` };
10946
+ }
10947
+ const last = diff("HEAD~1..HEAD");
10948
+ return { files: last, range: last.length > 0 ? "HEAD~1..HEAD" : null };
10949
+ }
10950
+ function getPushRangeMessages() {
10951
+ const { range } = getPushRangeFiles();
10952
+ if (!range) return "";
10953
+ return execGit(`git log ${range} --format=%B%x00`).split("\0").map((s) => s.trim()).filter(Boolean).join("\n\n");
10954
+ }
10955
+ function getWorktreeFiles() {
10956
+ const result = [];
10957
+ const worktreeDir = ".claude/worktrees";
10958
+ if (!(0, import_node_fs2.existsSync)(worktreeDir)) return result;
10959
+ try {
10960
+ const fiveMinAgo = Date.now() - 5 * 60 * 1e3;
10961
+ const entries = (0, import_node_fs2.readdirSync)(worktreeDir, { withFileTypes: true });
10962
+ for (const entry of entries) {
10963
+ if (!entry.isDirectory()) continue;
10964
+ const wtDir = (0, import_node_path2.join)(worktreeDir, entry.name);
10965
+ scanDir(wtDir, wtDir, fiveMinAgo, result);
10966
+ }
10967
+ } catch {
10968
+ }
10969
+ return result;
10970
+ }
10971
+ function scanDir(baseDir, dir, minMtime, result) {
10972
+ try {
10973
+ const entries = (0, import_node_fs2.readdirSync)(dir, { withFileTypes: true });
10974
+ for (const entry of entries) {
10975
+ const fullPath = (0, import_node_path2.join)(dir, entry.name);
10976
+ if (entry.isDirectory()) {
10977
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
10978
+ scanDir(baseDir, fullPath, minMtime, result);
10979
+ } else if (entry.isFile()) {
10980
+ const ext = (0, import_node_path2.extname)(entry.name).slice(1);
10981
+ if (!ANALYZABLE_EXTENSIONS.has(ext)) continue;
10982
+ try {
10983
+ const stat3 = (0, import_node_fs2.statSync)(fullPath);
10984
+ if (stat3.mtimeMs >= minMtime) {
10985
+ const relPath = fullPath.slice(baseDir.length + 1);
10986
+ result.push(relPath);
10987
+ }
10988
+ } catch {
10989
+ }
10990
+ }
10991
+ }
10992
+ } catch {
10993
+ }
10994
+ }
10995
+ function filterAnalyzable(files) {
10996
+ return files.filter((f) => {
10997
+ const ext = (0, import_node_path2.extname)(f).slice(1);
10998
+ return ANALYZABLE_EXTENSIONS.has(ext);
10999
+ });
11000
+ }
11001
+ function filterReviewable(files) {
11002
+ return files.filter((f) => {
11003
+ const ext = (0, import_node_path2.extname)(f).slice(1);
11004
+ if (ANALYZABLE_EXTENSIONS.has(ext)) return false;
11005
+ if (REVIEWABLE_EXTENSIONS.has(ext)) return true;
11006
+ const basename2 = f.split("/").pop() ?? "";
11007
+ if (REVIEWABLE_FILENAMES.has(basename2)) return true;
11008
+ if (REVIEWABLE_PATH_PATTERNS.some((p) => p.test(f))) return true;
11009
+ return false;
11010
+ });
11011
+ }
11012
+ function filterSecurity(files) {
11013
+ return files.filter(
11014
+ (f) => SECURITY_PATTERNS.some((p) => p.test(f))
11015
+ );
11016
+ }
11017
+ function getCurrentCommit() {
11018
+ return execGit("git rev-parse HEAD") || "no-git";
11019
+ }
11020
+ function detectProvider(host) {
11021
+ const h = host.toLowerCase();
11022
+ if (h.includes("github")) return "github";
11023
+ if (h.includes("gitlab")) return "gitlab";
11024
+ if (h.includes("bitbucket")) return "bitbucket";
11025
+ return "unknown";
11026
+ }
11027
+ function parseRemote(raw) {
11028
+ if (!raw || typeof raw !== "string") return null;
11029
+ let s = raw.trim();
11030
+ if (!s) return null;
11031
+ let host = "";
11032
+ let path = "";
11033
+ const scp = s.match(/^[^/@]+@([^:/]+):(.+)$/);
11034
+ if (scp) {
11035
+ host = scp[1];
11036
+ path = scp[2];
11037
+ } else {
11038
+ s = s.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, "");
11039
+ s = s.replace(/^[^/@]+@/, "");
11040
+ const slash = s.indexOf("/");
11041
+ if (slash === -1) return null;
11042
+ host = s.slice(0, slash);
11043
+ path = s.slice(slash + 1);
11044
+ }
11045
+ host = host.toLowerCase().trim();
11046
+ path = path.replace(/\/+$/, "").replace(/\.git$/, "");
11047
+ if (!host || !path) return null;
11048
+ const segments = path.split("/").filter(Boolean);
11049
+ if (segments.length < 2) return null;
11050
+ const owner = segments[0];
11051
+ const repo = segments[segments.length - 1];
11052
+ if (!owner || !repo) return null;
11053
+ return {
11054
+ host,
11055
+ owner,
11056
+ repo,
11057
+ provider: detectProvider(host),
11058
+ orgUrl: `https://${host}/${owner}`,
11059
+ orgName: owner
11060
+ };
11061
+ }
11062
+ function listTrackedFiles() {
11063
+ const set = /* @__PURE__ */ new Set();
11064
+ for (const f of splitLines(execGit("git ls-files"))) set.add(f);
11065
+ for (const f of splitLines(execGit("git ls-files --others --exclude-standard"))) set.add(f);
11066
+ return Array.from(set);
11067
+ }
11068
+
10743
11069
  // src/commands/auth.ts
10744
11070
  function registerAuthCommands(program2) {
10745
11071
  const auth = program2.command("auth").description("Manage project authentication");
@@ -10749,35 +11075,65 @@ function registerAuthCommands(program2) {
10749
11075
  let remote = opts.remote;
10750
11076
  if (!remote) {
10751
11077
  try {
10752
- remote = (0, import_node_child_process3.execSync)("git remote get-url origin", { encoding: "utf-8" }).trim();
11078
+ remote = (0, import_node_child_process4.execSync)("git remote get-url origin", { encoding: "utf-8" }).trim();
10753
11079
  } catch {
10754
11080
  printError("No git remote found. Use --remote to specify one.");
10755
11081
  process.exit(1);
10756
11082
  }
10757
11083
  }
11084
+ const parsed = parseRemote(remote);
11085
+ if (!parsed) {
11086
+ printError(`Could not parse git remote: ${remote}`);
11087
+ process.exit(1);
11088
+ }
11089
+ if (parsed.provider !== "github") {
11090
+ printError(`Provider '${parsed.provider}' is not supported yet \u2014 GitHub only for now.`);
11091
+ process.exit(1);
11092
+ }
11093
+ const providerAuth = await githubDeviceFlow();
11094
+ if (!providerAuth.ok) {
11095
+ printError(providerAuth.error);
11096
+ process.exit(1);
11097
+ }
11098
+ const providerToken = providerAuth.data;
10758
11099
  const result = await apiRequest({
10759
11100
  method: "POST",
10760
11101
  path: "/auth/register",
10761
11102
  serviceUrl,
10762
11103
  body: { project_name: opts.project, git_remote_url: remote },
11104
+ extraHeaders: { "X-Provider-Token": providerToken },
10763
11105
  verbose: globals.verbose
10764
11106
  });
10765
11107
  if (!result.ok) {
10766
11108
  printError(result.error);
10767
11109
  process.exit(1);
10768
11110
  }
10769
- const { project_id, token, service_url } = result.data;
11111
+ const { project_id, token, service_url, user } = result.data;
10770
11112
  await (0, import_promises3.mkdir)(VERITY_DIR, { recursive: true });
10771
- await (0, import_promises3.writeFile)(CREDENTIALS_FILE, `token: ${token}
11113
+ await (0, import_promises3.writeFile)(
11114
+ CREDENTIALS_FILE,
11115
+ `token: ${token}
10772
11116
  service_url: ${service_url}
10773
- `);
11117
+ provider_token: ${providerToken}
11118
+ `,
11119
+ { mode: 384 }
11120
+ );
11121
+ await (0, import_promises3.chmod)(CREDENTIALS_FILE, 384).catch(() => {
11122
+ });
10774
11123
  try {
10775
- await (0, import_promises3.mkdir)((0, import_node_path2.dirname)(GLOBAL_CREDENTIALS_FILE), { recursive: true });
10776
- await (0, import_promises3.appendFile)(GLOBAL_CREDENTIALS_FILE, `${remote} token: ${token}
10777
- `);
11124
+ await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(GLOBAL_CREDENTIALS_FILE), { recursive: true });
11125
+ await (0, import_promises3.appendFile)(
11126
+ GLOBAL_CREDENTIALS_FILE,
11127
+ `${remote} token: ${token}
11128
+ ${remote} provider_token: ${providerToken}
11129
+ `
11130
+ );
11131
+ await (0, import_promises3.chmod)(GLOBAL_CREDENTIALS_FILE, 384).catch(() => {
11132
+ });
10778
11133
  } catch {
10779
11134
  }
10780
11135
  printInfo(`Project registered: ${project_id}`);
11136
+ if (user?.email) printInfo(`Authenticated as: ${user.email}`);
10781
11137
  printJson({ project_id, service_url });
10782
11138
  });
10783
11139
  auth.command("verify").description("Verify the current token is valid").action(async () => {
@@ -10811,7 +11167,7 @@ service_url: ${service_url}
10811
11167
  let remote = opts.remote;
10812
11168
  if (!remote) {
10813
11169
  try {
10814
- remote = (0, import_node_child_process3.execSync)("git remote get-url origin", { encoding: "utf-8" }).trim();
11170
+ remote = (0, import_node_child_process4.execSync)("git remote get-url origin", { encoding: "utf-8" }).trim();
10815
11171
  } catch {
10816
11172
  printError("No git remote found. Use --remote to specify one.");
10817
11173
  process.exit(1);
@@ -10839,7 +11195,7 @@ service_url: ${service_url}
10839
11195
 
10840
11196
  // src/lib/hooks.ts
10841
11197
  var import_promises4 = require("node:fs/promises");
10842
- var import_node_path3 = require("node:path");
11198
+ var import_node_path4 = require("node:path");
10843
11199
  var VERITY_STOP_HOOK = {
10844
11200
  type: "command",
10845
11201
  command: "verity analyze",
@@ -10991,19 +11347,19 @@ async function checkAllVerityHooksDetailed() {
10991
11347
  return { stop, intent, baseline, current: hasCurrent, legacyOnly: hasLegacy && !hasCurrent };
10992
11348
  }
10993
11349
  async function writeSettings(settings) {
10994
- await (0, import_promises4.mkdir)((0, import_node_path3.dirname)(CLAUDE_SETTINGS_FILE), { recursive: true });
11350
+ await (0, import_promises4.mkdir)((0, import_node_path4.dirname)(CLAUDE_SETTINGS_FILE), { recursive: true });
10995
11351
  await (0, import_promises4.writeFile)(CLAUDE_SETTINGS_FILE, JSON.stringify(settings, null, 2) + "\n");
10996
11352
  }
10997
11353
  async function readSettingsAt(root) {
10998
11354
  try {
10999
- return JSON.parse(await (0, import_promises4.readFile)((0, import_node_path3.join)(root, CLAUDE_SETTINGS_FILE), "utf-8"));
11355
+ return JSON.parse(await (0, import_promises4.readFile)((0, import_node_path4.join)(root, CLAUDE_SETTINGS_FILE), "utf-8"));
11000
11356
  } catch {
11001
11357
  return {};
11002
11358
  }
11003
11359
  }
11004
11360
  async function writeSettingsAt(root, settings) {
11005
- const file = (0, import_node_path3.join)(root, CLAUDE_SETTINGS_FILE);
11006
- await (0, import_promises4.mkdir)((0, import_node_path3.dirname)(file), { recursive: true });
11361
+ const file = (0, import_node_path4.join)(root, CLAUDE_SETTINGS_FILE);
11362
+ await (0, import_promises4.mkdir)((0, import_node_path4.dirname)(file), { recursive: true });
11007
11363
  await (0, import_promises4.writeFile)(file, JSON.stringify(settings, null, 2) + "\n");
11008
11364
  }
11009
11365
  async function hasLegacyHooksAt(root) {
@@ -11240,15 +11596,19 @@ function registerHooksCommands(program2) {
11240
11596
  }
11241
11597
 
11242
11598
  // src/commands/intent.ts
11243
- var import_node_crypto2 = require("node:crypto");
11599
+ var import_node_crypto3 = require("node:crypto");
11244
11600
 
11245
11601
  // src/lib/conversation-buffer.ts
11246
11602
  var import_promises5 = require("node:fs/promises");
11247
- var import_node_fs2 = require("node:fs");
11248
- var import_node_child_process4 = require("node:child_process");
11603
+ var import_node_fs3 = require("node:fs");
11604
+ var import_node_child_process5 = require("node:child_process");
11605
+ var import_node_crypto = require("node:crypto");
11249
11606
  function stripImageReferences(text) {
11250
11607
  return text.replace(/\[Image #\d+\]/g, "[screenshot \u2014 not available for review]");
11251
11608
  }
11609
+ function bufferTmpPath() {
11610
+ return `${CONVERSATION_BUFFER_FILE}.${process.pid}.${(0, import_node_crypto.randomBytes)(4).toString("hex")}.tmp`;
11611
+ }
11252
11612
  async function appendToConversationBuffer(prompt, sessionId) {
11253
11613
  try {
11254
11614
  await (0, import_promises5.mkdir)(VERITY_DIR, { recursive: true });
@@ -11268,7 +11628,7 @@ async function appendToConversationBuffer(prompt, sessionId) {
11268
11628
  recent.push(entry);
11269
11629
  const capped = recent.slice(-CONVERSATION_MAX_ENTRIES);
11270
11630
  const content = capped.map((e) => JSON.stringify(e)).join("\n") + "\n";
11271
- const tmpFile = `${CONVERSATION_BUFFER_FILE}.tmp`;
11631
+ const tmpFile = bufferTmpPath();
11272
11632
  await (0, import_promises5.writeFile)(tmpFile, content);
11273
11633
  await (0, import_promises5.rename)(tmpFile, CONVERSATION_BUFFER_FILE);
11274
11634
  } catch {
@@ -11276,7 +11636,7 @@ async function appendToConversationBuffer(prompt, sessionId) {
11276
11636
  }
11277
11637
  async function readAndClearConversationBuffer(currentSessionId) {
11278
11638
  try {
11279
- if ((0, import_node_fs2.existsSync)(CONVERSATION_BUFFER_FILE)) {
11639
+ if ((0, import_node_fs3.existsSync)(CONVERSATION_BUFFER_FILE)) {
11280
11640
  const entries = await readBufferEntries();
11281
11641
  let mine = entries;
11282
11642
  let others = [];
@@ -11286,7 +11646,7 @@ async function readAndClearConversationBuffer(currentSessionId) {
11286
11646
  }
11287
11647
  if (others.length > 0) {
11288
11648
  const remaining = others.map((e) => JSON.stringify(e)).join("\n") + "\n";
11289
- const tmpFile = `${CONVERSATION_BUFFER_FILE}.tmp`;
11649
+ const tmpFile = bufferTmpPath();
11290
11650
  await (0, import_promises5.writeFile)(tmpFile, remaining);
11291
11651
  await (0, import_promises5.rename)(tmpFile, CONVERSATION_BUFFER_FILE);
11292
11652
  } else {
@@ -11300,7 +11660,7 @@ async function readAndClearConversationBuffer(currentSessionId) {
11300
11660
  };
11301
11661
  }
11302
11662
  }
11303
- if ((0, import_node_fs2.existsSync)(INTENT_FILE)) {
11663
+ if ((0, import_node_fs3.existsSync)(INTENT_FILE)) {
11304
11664
  try {
11305
11665
  const content = await (0, import_promises5.readFile)(INTENT_FILE, "utf-8");
11306
11666
  await (0, import_promises5.unlink)(INTENT_FILE).catch(() => {
@@ -11344,7 +11704,7 @@ async function readBufferEntries() {
11344
11704
  }
11345
11705
  function getRecentCommitMessages() {
11346
11706
  try {
11347
- const output = (0, import_node_child_process4.execSync)(
11707
+ const output = (0, import_node_child_process5.execSync)(
11348
11708
  'git log --since="30 minutes ago" --format="%s" -5',
11349
11709
  { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
11350
11710
  ).trim();
@@ -11357,8 +11717,8 @@ function getRecentCommitMessages() {
11357
11717
 
11358
11718
  // src/lib/task-context-buffer.ts
11359
11719
  var import_promises6 = require("node:fs/promises");
11360
- var import_node_fs3 = require("node:fs");
11361
- var import_node_path4 = require("node:path");
11720
+ var import_node_fs4 = require("node:fs");
11721
+ var import_node_path5 = require("node:path");
11362
11722
  var TASK_CONTEXT_DIR = `${VERITY_DIR}/.task-context`;
11363
11723
  var MAX_BUFFER_BYTES = 500 * 1024;
11364
11724
  var MAX_PROMPT_CHARS = 2e3;
@@ -11397,7 +11757,7 @@ async function appendResponseToTaskBuffer(taskId, assistantResponse, actionSumma
11397
11757
  }
11398
11758
  async function readTaskContextBuffer(taskId) {
11399
11759
  const filePath = bufferPath(taskId);
11400
- if (!(0, import_node_fs3.existsSync)(filePath)) return null;
11760
+ if (!(0, import_node_fs4.existsSync)(filePath)) return null;
11401
11761
  try {
11402
11762
  const content = await (0, import_promises6.readFile)(filePath, "utf-8");
11403
11763
  if (!content.trim()) return null;
@@ -11431,12 +11791,12 @@ async function readTaskContextBuffer(taskId) {
11431
11791
  }
11432
11792
  async function cleanupTaskContextBuffers() {
11433
11793
  try {
11434
- if (!(0, import_node_fs3.existsSync)(TASK_CONTEXT_DIR)) return;
11794
+ if (!(0, import_node_fs4.existsSync)(TASK_CONTEXT_DIR)) return;
11435
11795
  const files = await (0, import_promises6.readdir)(TASK_CONTEXT_DIR);
11436
11796
  const cutoffMs = Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1e3;
11437
11797
  for (const file of files) {
11438
11798
  if (!file.endsWith(".jsonl")) continue;
11439
- const filePath = (0, import_node_path4.join)(TASK_CONTEXT_DIR, file);
11799
+ const filePath = (0, import_node_path5.join)(TASK_CONTEXT_DIR, file);
11440
11800
  try {
11441
11801
  const stats = await (0, import_promises6.stat)(filePath);
11442
11802
  if (stats.mtimeMs < cutoffMs) {
@@ -11450,13 +11810,13 @@ async function cleanupTaskContextBuffers() {
11450
11810
  }
11451
11811
  function bufferPath(taskId) {
11452
11812
  const safe = taskId.replace(/[^a-zA-Z0-9_-]/g, "");
11453
- return (0, import_node_path4.join)(TASK_CONTEXT_DIR, `${safe}.jsonl`);
11813
+ return (0, import_node_path5.join)(TASK_CONTEXT_DIR, `${safe}.jsonl`);
11454
11814
  }
11455
11815
  async function appendEntry(taskId, entry) {
11456
11816
  try {
11457
11817
  await (0, import_promises6.mkdir)(TASK_CONTEXT_DIR, { recursive: true });
11458
11818
  const filePath = bufferPath(taskId);
11459
- if ((0, import_node_fs3.existsSync)(filePath)) {
11819
+ if ((0, import_node_fs4.existsSync)(filePath)) {
11460
11820
  const stats = await (0, import_promises6.stat)(filePath);
11461
11821
  if (stats.size >= MAX_BUFFER_BYTES) {
11462
11822
  const content = await (0, import_promises6.readFile)(filePath, "utf-8");
@@ -11467,7 +11827,7 @@ async function appendEntry(taskId, entry) {
11467
11827
  }
11468
11828
  }
11469
11829
  const line = JSON.stringify(entry) + "\n";
11470
- const existing = (0, import_node_fs3.existsSync)(filePath) ? await (0, import_promises6.readFile)(filePath, "utf-8") : "";
11830
+ const existing = (0, import_node_fs4.existsSync)(filePath) ? await (0, import_promises6.readFile)(filePath, "utf-8") : "";
11471
11831
  await (0, import_promises6.writeFile)(filePath, existing + line);
11472
11832
  } catch {
11473
11833
  }
@@ -11475,8 +11835,8 @@ async function appendEntry(taskId, entry) {
11475
11835
 
11476
11836
  // src/lib/memory-retrieval.ts
11477
11837
  var import_promises7 = require("node:fs/promises");
11478
- var import_node_fs4 = require("node:fs");
11479
- var import_node_path5 = require("node:path");
11838
+ var import_node_fs5 = require("node:fs");
11839
+ var import_node_path6 = require("node:path");
11480
11840
  var memoryDir = () => projectPath(`${VERITY_DIR}/memory`);
11481
11841
  var DOMAINS = ["decisions", "quality", "security", "intent", "gotchas", "patterns", "domain", "integrations"];
11482
11842
  var DEFAULT_BUDGET_TOKENS = 2e3;
@@ -11569,19 +11929,19 @@ function parseFrontmatter(content) {
11569
11929
  return { fm, body: match[2].trim() };
11570
11930
  }
11571
11931
  async function retrieveForInjection(promptText, taskFiles = [], budgetTokens = DEFAULT_BUDGET_TOKENS) {
11572
- if (!(0, import_node_fs4.existsSync)(memoryDir())) return null;
11932
+ if (!(0, import_node_fs5.existsSync)(memoryDir())) return null;
11573
11933
  const budget = Math.min(budgetTokens, MAX_BUDGET_TOKENS);
11574
11934
  const promptTokens = tokenize(promptText);
11575
11935
  const nodes = [];
11576
11936
  for (const domain of DOMAINS) {
11577
- const domainDir = (0, import_node_path5.join)(memoryDir(), domain);
11578
- if (!(0, import_node_fs4.existsSync)(domainDir)) continue;
11937
+ const domainDir = (0, import_node_path6.join)(memoryDir(), domain);
11938
+ if (!(0, import_node_fs5.existsSync)(domainDir)) continue;
11579
11939
  try {
11580
11940
  const files = await (0, import_promises7.readdir)(domainDir);
11581
11941
  for (const file of files) {
11582
11942
  if (!file.endsWith(".md")) continue;
11583
11943
  try {
11584
- const content = await (0, import_promises7.readFile)((0, import_node_path5.join)(domainDir, file), "utf-8");
11944
+ const content = await (0, import_promises7.readFile)((0, import_node_path6.join)(domainDir, file), "utf-8");
11585
11945
  const { fm, body } = parseFrontmatter(content);
11586
11946
  if (fm.status && fm.status !== "active") continue;
11587
11947
  nodes.push({
@@ -11640,9 +12000,9 @@ async function retrieveForInjection(promptText, taskFiles = [], budgetTokens = D
11640
12000
 
11641
12001
  // src/lib/memory-sync.ts
11642
12002
  var import_promises8 = require("node:fs/promises");
11643
- var import_node_fs5 = require("node:fs");
11644
- var import_node_path6 = require("node:path");
11645
- var import_node_crypto = require("node:crypto");
12003
+ var import_node_fs6 = require("node:fs");
12004
+ var import_node_path7 = require("node:path");
12005
+ var import_node_crypto2 = require("node:crypto");
11646
12006
 
11647
12007
  // src/lib/glob-match.ts
11648
12008
  function globToRegex(glob) {
@@ -11712,35 +12072,35 @@ var syncStateFile = () => projectPath(`${VERITY_DIR}/.memory-sync-state.json`);
11712
12072
  async function ensureMemoryDir() {
11713
12073
  await (0, import_promises8.mkdir)(memoryDir2(), { recursive: true });
11714
12074
  for (const domain of DOMAINS2) {
11715
- await (0, import_promises8.mkdir)((0, import_node_path6.join)(memoryDir2(), domain), { recursive: true });
12075
+ await (0, import_promises8.mkdir)((0, import_node_path7.join)(memoryDir2(), domain), { recursive: true });
11716
12076
  }
11717
- if (!(0, import_node_fs5.existsSync)((0, import_node_path6.join)(memoryDir2(), "SCHEMA.md"))) {
11718
- await (0, import_promises8.writeFile)((0, import_node_path6.join)(memoryDir2(), "SCHEMA.md"), SCHEMA_TEMPLATE);
12077
+ if (!(0, import_node_fs6.existsSync)((0, import_node_path7.join)(memoryDir2(), "SCHEMA.md"))) {
12078
+ await (0, import_promises8.writeFile)((0, import_node_path7.join)(memoryDir2(), "SCHEMA.md"), SCHEMA_TEMPLATE);
11719
12079
  }
11720
- if (!(0, import_node_fs5.existsSync)((0, import_node_path6.join)(memoryDir2(), "index.md"))) {
11721
- await (0, import_promises8.writeFile)((0, import_node_path6.join)(memoryDir2(), "index.md"), "# Project Memory Index\n\nNo nodes yet. Run an analysis to start building the knowledge graph.\n");
12080
+ if (!(0, import_node_fs6.existsSync)((0, import_node_path7.join)(memoryDir2(), "index.md"))) {
12081
+ await (0, import_promises8.writeFile)((0, import_node_path7.join)(memoryDir2(), "index.md"), "# Project Memory Index\n\nNo nodes yet. Run an analysis to start building the knowledge graph.\n");
11722
12082
  }
11723
- if (!(0, import_node_fs5.existsSync)((0, import_node_path6.join)(memoryDir2(), "log.md"))) {
11724
- await (0, import_promises8.writeFile)((0, import_node_path6.join)(memoryDir2(), "log.md"), "# Memory Log\n\n");
12083
+ if (!(0, import_node_fs6.existsSync)((0, import_node_path7.join)(memoryDir2(), "log.md"))) {
12084
+ await (0, import_promises8.writeFile)((0, import_node_path7.join)(memoryDir2(), "log.md"), "# Memory Log\n\n");
11725
12085
  }
11726
12086
  }
11727
12087
  async function buildManifest() {
11728
- if (!(0, import_node_fs5.existsSync)(memoryDir2())) {
12088
+ if (!(0, import_node_fs6.existsSync)(memoryDir2())) {
11729
12089
  return { schema_version: 1, nodes: [], index_hash: null, log_length: 0 };
11730
12090
  }
11731
12091
  const nodes = [];
11732
12092
  for (const domain of DOMAINS2) {
11733
- const domainDir = (0, import_node_path6.join)(memoryDir2(), domain);
11734
- if (!(0, import_node_fs5.existsSync)(domainDir)) continue;
12093
+ const domainDir = (0, import_node_path7.join)(memoryDir2(), domain);
12094
+ if (!(0, import_node_fs6.existsSync)(domainDir)) continue;
11735
12095
  try {
11736
12096
  const files = await (0, import_promises8.readdir)(domainDir);
11737
12097
  for (const file of files) {
11738
12098
  if (!file.endsWith(".md")) continue;
11739
12099
  const filePath = `${domain}/${file}`;
11740
- const fullPath = (0, import_node_path6.join)(memoryDir2(), filePath);
12100
+ const fullPath = (0, import_node_path7.join)(memoryDir2(), filePath);
11741
12101
  try {
11742
12102
  const content = await (0, import_promises8.readFile)(fullPath, "utf-8");
11743
- const hash = (0, import_node_crypto.createHash)("sha256").update(content).digest("hex").slice(0, 16);
12103
+ const hash = (0, import_node_crypto2.createHash)("sha256").update(content).digest("hex").slice(0, 16);
11744
12104
  nodes.push({ path: filePath, content_hash: `sha256:${hash}` });
11745
12105
  } catch {
11746
12106
  }
@@ -11750,32 +12110,32 @@ async function buildManifest() {
11750
12110
  }
11751
12111
  let indexHash = null;
11752
12112
  try {
11753
- const indexContent = await (0, import_promises8.readFile)((0, import_node_path6.join)(memoryDir2(), "index.md"), "utf-8");
11754
- indexHash = `sha256:${(0, import_node_crypto.createHash)("sha256").update(indexContent).digest("hex").slice(0, 16)}`;
12113
+ const indexContent = await (0, import_promises8.readFile)((0, import_node_path7.join)(memoryDir2(), "index.md"), "utf-8");
12114
+ indexHash = `sha256:${(0, import_node_crypto2.createHash)("sha256").update(indexContent).digest("hex").slice(0, 16)}`;
11755
12115
  } catch {
11756
12116
  }
11757
12117
  let logLength = 0;
11758
12118
  try {
11759
- const logContent = await (0, import_promises8.readFile)((0, import_node_path6.join)(memoryDir2(), "log.md"), "utf-8");
12119
+ const logContent = await (0, import_promises8.readFile)((0, import_node_path7.join)(memoryDir2(), "log.md"), "utf-8");
11760
12120
  logLength = logContent.split("\n").length;
11761
12121
  } catch {
11762
12122
  }
11763
12123
  return { schema_version: 1, nodes, index_hash: indexHash, log_length: logLength };
11764
12124
  }
11765
12125
  function hashContent(content) {
11766
- return `sha256:${(0, import_node_crypto.createHash)("sha256").update(content).digest("hex").slice(0, 16)}`;
12126
+ return `sha256:${(0, import_node_crypto2.createHash)("sha256").update(content).digest("hex").slice(0, 16)}`;
11767
12127
  }
11768
12128
  async function readOnDiskNodes() {
11769
12129
  const out = /* @__PURE__ */ new Map();
11770
- if (!(0, import_node_fs5.existsSync)(memoryDir2())) return out;
12130
+ if (!(0, import_node_fs6.existsSync)(memoryDir2())) return out;
11771
12131
  for (const domain of DOMAINS2) {
11772
- const domainDir = (0, import_node_path6.join)(memoryDir2(), domain);
11773
- if (!(0, import_node_fs5.existsSync)(domainDir)) continue;
12132
+ const domainDir = (0, import_node_path7.join)(memoryDir2(), domain);
12133
+ if (!(0, import_node_fs6.existsSync)(domainDir)) continue;
11774
12134
  try {
11775
12135
  for (const file of await (0, import_promises8.readdir)(domainDir)) {
11776
12136
  if (!file.endsWith(".md")) continue;
11777
12137
  try {
11778
- out.set(`${domain}/${file}`, hashContent(await (0, import_promises8.readFile)((0, import_node_path6.join)(domainDir, file), "utf-8")));
12138
+ out.set(`${domain}/${file}`, hashContent(await (0, import_promises8.readFile)((0, import_node_path7.join)(domainDir, file), "utf-8")));
11779
12139
  } catch {
11780
12140
  }
11781
12141
  }
@@ -11821,8 +12181,8 @@ async function computeEditedNodeUploads() {
11821
12181
  const uploads = [];
11822
12182
  for (const [path, prevHash] of prev) {
11823
12183
  if (prevHash == null) continue;
11824
- const full = (0, import_node_path6.join)(memoryDir2(), path);
11825
- if (!(0, import_node_fs5.existsSync)(full)) continue;
12184
+ const full = (0, import_node_path7.join)(memoryDir2(), path);
12185
+ if (!(0, import_node_fs6.existsSync)(full)) continue;
11826
12186
  let content;
11827
12187
  try {
11828
12188
  content = await (0, import_promises8.readFile)(full, "utf-8");
@@ -11858,15 +12218,15 @@ async function applyMemoryWrites(writes, opts = {}) {
11858
12218
  const logLines = [`- ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)} \u2014 Applied ${count} write(s) from server`];
11859
12219
  for (const n of notes) logLines.push(` - ${n}`);
11860
12220
  try {
11861
- const existing = (0, import_node_fs5.existsSync)((0, import_node_path6.join)(memoryDir2(), "log.md")) ? await (0, import_promises8.readFile)((0, import_node_path6.join)(memoryDir2(), "log.md"), "utf-8") : "# Memory Log\n\n";
11862
- await (0, import_promises8.writeFile)((0, import_node_path6.join)(memoryDir2(), "log.md"), existing + logLines.join("\n") + "\n");
12221
+ const existing = (0, import_node_fs6.existsSync)((0, import_node_path7.join)(memoryDir2(), "log.md")) ? await (0, import_promises8.readFile)((0, import_node_path7.join)(memoryDir2(), "log.md"), "utf-8") : "# Memory Log\n\n";
12222
+ await (0, import_promises8.writeFile)((0, import_node_path7.join)(memoryDir2(), "log.md"), existing + logLines.join("\n") + "\n");
11863
12223
  } catch {
11864
12224
  }
11865
12225
  await recordSyncedNodePaths();
11866
12226
  return count;
11867
12227
  }
11868
12228
  async function applyOneWrite(write, treePaths) {
11869
- const fullPath = (0, import_node_path6.join)(memoryDir2(), write.path);
12229
+ const fullPath = (0, import_node_path7.join)(memoryDir2(), write.path);
11870
12230
  const notes = [];
11871
12231
  let content = write.content;
11872
12232
  if (treePaths && treePaths.length > 0) {
@@ -11876,7 +12236,7 @@ async function applyOneWrite(write, treePaths) {
11876
12236
  notes.push(`${write.path}: dropped unmatched file_globs [${grounded.dropped.join(", ")}]`);
11877
12237
  }
11878
12238
  }
11879
- if ((0, import_node_fs5.existsSync)(fullPath)) {
12239
+ if ((0, import_node_fs6.existsSync)(fullPath)) {
11880
12240
  let existing = "";
11881
12241
  try {
11882
12242
  existing = await (0, import_promises8.readFile)(fullPath, "utf-8");
@@ -11888,7 +12248,7 @@ async function applyOneWrite(write, treePaths) {
11888
12248
  return { written: false, notes };
11889
12249
  }
11890
12250
  }
11891
- await (0, import_promises8.mkdir)((0, import_node_path6.dirname)(fullPath), { recursive: true });
12251
+ await (0, import_promises8.mkdir)((0, import_node_path7.dirname)(fullPath), { recursive: true });
11892
12252
  await (0, import_promises8.writeFile)(fullPath, content);
11893
12253
  return { written: true, notes };
11894
12254
  }
@@ -11929,8 +12289,8 @@ async function regenerateIndex() {
11929
12289
  ];
11930
12290
  let totalNodes = 0;
11931
12291
  for (const domain of DOMAINS2.filter((d) => d !== "_archive")) {
11932
- const domainDir = (0, import_node_path6.join)(memoryDir2(), domain);
11933
- if (!(0, import_node_fs5.existsSync)(domainDir)) continue;
12292
+ const domainDir = (0, import_node_path7.join)(memoryDir2(), domain);
12293
+ if (!(0, import_node_fs6.existsSync)(domainDir)) continue;
11934
12294
  try {
11935
12295
  const files = await (0, import_promises8.readdir)(domainDir);
11936
12296
  const mdFiles = files.filter((f) => f.endsWith(".md"));
@@ -11940,7 +12300,7 @@ async function regenerateIndex() {
11940
12300
  for (const file of mdFiles.sort()) {
11941
12301
  const slug = file.replace(/\.md$/, "");
11942
12302
  try {
11943
- const content = await (0, import_promises8.readFile)((0, import_node_path6.join)(domainDir, file), "utf-8");
12303
+ const content = await (0, import_promises8.readFile)((0, import_node_path7.join)(domainDir, file), "utf-8");
11944
12304
  const title = pickFrontmatter(content, "title") ?? slug;
11945
12305
  const kind = pickFrontmatter(content, "kind") ?? "-";
11946
12306
  const confidence = pickFrontmatter(content, "confidence");
@@ -11964,7 +12324,7 @@ async function regenerateIndex() {
11964
12324
  lines.push("No nodes yet. Run an analysis to start building the knowledge graph.");
11965
12325
  }
11966
12326
  const next = lines.join("\n") + "\n";
11967
- const indexPath = (0, import_node_path6.join)(memoryDir2(), "index.md");
12327
+ const indexPath = (0, import_node_path7.join)(memoryDir2(), "index.md");
11968
12328
  let existing = null;
11969
12329
  try {
11970
12330
  existing = await (0, import_promises8.readFile)(indexPath, "utf-8");
@@ -12046,9 +12406,9 @@ function hasLegacyMemoryBlock(text) {
12046
12406
  return findMarker(text, LEGACY_MD_START) !== -1;
12047
12407
  }
12048
12408
  async function ensureClaudeMdPointer(cwd = repoRoot()) {
12049
- const claudeMdPath = (0, import_node_path6.join)(cwd, "CLAUDE.md");
12409
+ const claudeMdPath = (0, import_node_path7.join)(cwd, "CLAUDE.md");
12050
12410
  let existing = "";
12051
- if ((0, import_node_fs5.existsSync)(claudeMdPath)) {
12411
+ if ((0, import_node_fs6.existsSync)(claudeMdPath)) {
12052
12412
  existing = await (0, import_promises8.readFile)(claudeMdPath, "utf-8");
12053
12413
  }
12054
12414
  let startTag = CLAUDE_MD_START;
@@ -12178,7 +12538,7 @@ Body content (\u22648KB). Use [[node-id]] wikilinks for cross-references.
12178
12538
  `;
12179
12539
 
12180
12540
  // src/commands/intent.ts
12181
- var import_node_fs6 = require("node:fs");
12541
+ var import_node_fs7 = require("node:fs");
12182
12542
  function registerIntentCommands(program2) {
12183
12543
  const intent = program2.command("intent").description("Manage intent capture");
12184
12544
  intent.command("capture").description("Capture user intent from stdin (used by UserPromptSubmit hook)").action(async () => {
@@ -12187,7 +12547,7 @@ function registerIntentCommands(program2) {
12187
12547
  process.chdir(repoRoot());
12188
12548
  } catch {
12189
12549
  }
12190
- if (!(0, import_node_fs6.existsSync)(VERITY_DIR)) {
12550
+ if (!(0, import_node_fs7.existsSync)(VERITY_DIR)) {
12191
12551
  process.exit(0);
12192
12552
  }
12193
12553
  const chunks = [];
@@ -12250,7 +12610,7 @@ async function fireClassify(prompt, sessionId) {
12250
12610
  logEvent("classify_skipped", { reason: "no_service_url", detail: urlResult.error });
12251
12611
  return;
12252
12612
  }
12253
- const promptHash = (0, import_node_crypto2.createHash)("sha256").update(prompt).digest("hex");
12613
+ const promptHash = (0, import_node_crypto3.createHash)("sha256").update(prompt).digest("hex");
12254
12614
  const result = await apiRequest({
12255
12615
  method: "POST",
12256
12616
  path: "/classify-task",
@@ -12766,215 +13126,6 @@ async function sendGeneralFeedback(message, opts, globals) {
12766
13126
  var import_node_fs19 = require("node:fs");
12767
13127
  var import_node_path14 = require("node:path");
12768
13128
 
12769
- // src/lib/git.ts
12770
- var import_node_child_process5 = require("node:child_process");
12771
- var import_node_fs7 = require("node:fs");
12772
- var import_node_path7 = require("node:path");
12773
- function resolveFile(relpath) {
12774
- if ((0, import_node_fs7.existsSync)(relpath)) return relpath;
12775
- if ((0, import_node_fs7.existsSync)(".claude/worktrees")) {
12776
- try {
12777
- const entries = (0, import_node_fs7.readdirSync)(".claude/worktrees", { withFileTypes: true });
12778
- for (const entry of entries) {
12779
- if (!entry.isDirectory()) continue;
12780
- const candidate = (0, import_node_path7.join)(".claude/worktrees", entry.name, relpath);
12781
- if ((0, import_node_fs7.existsSync)(candidate)) return candidate;
12782
- }
12783
- } catch {
12784
- }
12785
- }
12786
- return null;
12787
- }
12788
- function execGit(cmd) {
12789
- try {
12790
- return (0, import_node_child_process5.execSync)(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
12791
- } catch {
12792
- return "";
12793
- }
12794
- }
12795
- function splitLines(s) {
12796
- return s.split("\n").filter((l) => l.length > 0);
12797
- }
12798
- var SHA_RE = /^[0-9a-f]{40}$/;
12799
- function readBaselineSha() {
12800
- if (!(0, import_node_fs7.existsSync)(BASELINE_SHA_FILE)) return null;
12801
- let sha;
12802
- try {
12803
- sha = (0, import_node_fs7.readFileSync)(BASELINE_SHA_FILE, "utf-8").trim();
12804
- } catch {
12805
- return null;
12806
- }
12807
- if (!SHA_RE.test(sha)) return null;
12808
- const reachable = execGit(`git cat-file -e ${sha}^{commit} 2>/dev/null && echo ok`) === "ok";
12809
- if (!reachable) {
12810
- try {
12811
- (0, import_node_fs7.unlinkSync)(BASELINE_SHA_FILE);
12812
- } catch {
12813
- }
12814
- return null;
12815
- }
12816
- return sha;
12817
- }
12818
- function writeBaselineSha(sha) {
12819
- if (!SHA_RE.test(sha)) return;
12820
- try {
12821
- (0, import_node_fs7.mkdirSync)((0, import_node_path7.dirname)(BASELINE_SHA_FILE), { recursive: true });
12822
- (0, import_node_fs7.writeFileSync)(BASELINE_SHA_FILE, sha);
12823
- } catch {
12824
- }
12825
- }
12826
- function getChangedFiles() {
12827
- const sets = /* @__PURE__ */ new Set();
12828
- let hasRecentCommitFiles = false;
12829
- for (const f of splitLines(execGit("git diff --name-only HEAD"))) sets.add(f);
12830
- for (const f of splitLines(execGit("git diff --name-only --cached"))) sets.add(f);
12831
- for (const f of splitLines(execGit("git ls-files --others --exclude-standard"))) sets.add(f);
12832
- const baseline = readBaselineSha();
12833
- if (baseline) {
12834
- const committed = splitLines(execGit(`git diff --name-only ${baseline}..HEAD`));
12835
- if (committed.length > 0) {
12836
- hasRecentCommitFiles = true;
12837
- for (const f of committed) sets.add(f);
12838
- }
12839
- } else {
12840
- const headTimestamp = parseInt(execGit("git log -1 --format=%ct HEAD"), 10) || 0;
12841
- const commitAge = Math.floor(Date.now() / 1e3) - headTimestamp;
12842
- const hasUnstaged = splitLines(execGit("git diff --name-only HEAD")).length > 0;
12843
- const hasStaged = splitLines(execGit("git diff --name-only --cached")).length > 0;
12844
- if (commitAge < 120 && !hasUnstaged && !hasStaged) {
12845
- const recentFiles = splitLines(execGit("git diff --name-only HEAD~1..HEAD"));
12846
- if (recentFiles.length > 0) {
12847
- hasRecentCommitFiles = true;
12848
- for (const f of recentFiles) sets.add(f);
12849
- }
12850
- }
12851
- }
12852
- for (const f of getWorktreeFiles()) sets.add(f);
12853
- const filtered = Array.from(sets).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
12854
- return { files: filtered, hasRecentCommitFiles };
12855
- }
12856
- function getStagedFiles() {
12857
- return splitLines(execGit("git diff --cached --name-only")).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
12858
- }
12859
- function getDirtyFiles() {
12860
- const set = /* @__PURE__ */ new Set();
12861
- for (const f of splitLines(execGit("git diff --name-only HEAD"))) set.add(f);
12862
- for (const f of splitLines(execGit("git diff --name-only --cached"))) set.add(f);
12863
- for (const f of splitLines(execGit("git ls-files --others --exclude-standard"))) set.add(f);
12864
- return Array.from(set).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
12865
- }
12866
- function showContentAtRef(ref, repoRelPath) {
12867
- if (!ref || ref === "no-git") return null;
12868
- const normalizedPath = repoRelPath.replace(/\\/g, "/");
12869
- try {
12870
- return (0, import_node_child_process5.execFileSync)("git", ["show", `${ref}:${normalizedPath}`], {
12871
- encoding: "utf-8",
12872
- maxBuffer: 64 * 1024 * 1024,
12873
- stdio: ["pipe", "pipe", "pipe"]
12874
- });
12875
- } catch {
12876
- return null;
12877
- }
12878
- }
12879
- function getPushRangeFiles() {
12880
- const diff = (range) => splitLines(execGit(`git diff --name-only ${range}`)).filter((f) => !f.startsWith(".verity/") && !f.startsWith(".verity\\"));
12881
- const resolvers = [
12882
- () => execGit("git rev-parse --abbrev-ref --symbolic-full-name @{push}") ? "@{push}..HEAD" : null,
12883
- () => execGit("git rev-parse --abbrev-ref --symbolic-full-name @{upstream}") ? "@{upstream}..HEAD" : null,
12884
- () => {
12885
- const branch = execGit("git rev-parse --abbrev-ref HEAD");
12886
- return branch && branch !== "HEAD" && execGit(`git rev-parse --verify -q origin/${branch}`) ? `origin/${branch}..HEAD` : null;
12887
- }
12888
- ];
12889
- for (const resolve of resolvers) {
12890
- const range = resolve();
12891
- if (range) return { files: diff(range), range };
12892
- }
12893
- const baseline = readBaselineSha();
12894
- if (baseline) {
12895
- const files = diff(`${baseline}..HEAD`);
12896
- if (files.length > 0) return { files, range: `${baseline}..HEAD` };
12897
- }
12898
- const last = diff("HEAD~1..HEAD");
12899
- return { files: last, range: last.length > 0 ? "HEAD~1..HEAD" : null };
12900
- }
12901
- function getPushRangeMessages() {
12902
- const { range } = getPushRangeFiles();
12903
- if (!range) return "";
12904
- return execGit(`git log ${range} --format=%B%x00`).split("\0").map((s) => s.trim()).filter(Boolean).join("\n\n");
12905
- }
12906
- function getWorktreeFiles() {
12907
- const result = [];
12908
- const worktreeDir = ".claude/worktrees";
12909
- if (!(0, import_node_fs7.existsSync)(worktreeDir)) return result;
12910
- try {
12911
- const fiveMinAgo = Date.now() - 5 * 60 * 1e3;
12912
- const entries = (0, import_node_fs7.readdirSync)(worktreeDir, { withFileTypes: true });
12913
- for (const entry of entries) {
12914
- if (!entry.isDirectory()) continue;
12915
- const wtDir = (0, import_node_path7.join)(worktreeDir, entry.name);
12916
- scanDir(wtDir, wtDir, fiveMinAgo, result);
12917
- }
12918
- } catch {
12919
- }
12920
- return result;
12921
- }
12922
- function scanDir(baseDir, dir, minMtime, result) {
12923
- try {
12924
- const entries = (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true });
12925
- for (const entry of entries) {
12926
- const fullPath = (0, import_node_path7.join)(dir, entry.name);
12927
- if (entry.isDirectory()) {
12928
- if (entry.name === "node_modules" || entry.name === ".git") continue;
12929
- scanDir(baseDir, fullPath, minMtime, result);
12930
- } else if (entry.isFile()) {
12931
- const ext = (0, import_node_path7.extname)(entry.name).slice(1);
12932
- if (!ANALYZABLE_EXTENSIONS.has(ext)) continue;
12933
- try {
12934
- const stat3 = (0, import_node_fs7.statSync)(fullPath);
12935
- if (stat3.mtimeMs >= minMtime) {
12936
- const relPath = fullPath.slice(baseDir.length + 1);
12937
- result.push(relPath);
12938
- }
12939
- } catch {
12940
- }
12941
- }
12942
- }
12943
- } catch {
12944
- }
12945
- }
12946
- function filterAnalyzable(files) {
12947
- return files.filter((f) => {
12948
- const ext = (0, import_node_path7.extname)(f).slice(1);
12949
- return ANALYZABLE_EXTENSIONS.has(ext);
12950
- });
12951
- }
12952
- function filterReviewable(files) {
12953
- return files.filter((f) => {
12954
- const ext = (0, import_node_path7.extname)(f).slice(1);
12955
- if (ANALYZABLE_EXTENSIONS.has(ext)) return false;
12956
- if (REVIEWABLE_EXTENSIONS.has(ext)) return true;
12957
- const basename2 = f.split("/").pop() ?? "";
12958
- if (REVIEWABLE_FILENAMES.has(basename2)) return true;
12959
- if (REVIEWABLE_PATH_PATTERNS.some((p) => p.test(f))) return true;
12960
- return false;
12961
- });
12962
- }
12963
- function filterSecurity(files) {
12964
- return files.filter(
12965
- (f) => SECURITY_PATTERNS.some((p) => p.test(f))
12966
- );
12967
- }
12968
- function getCurrentCommit() {
12969
- return execGit("git rev-parse HEAD") || "no-git";
12970
- }
12971
- function listTrackedFiles() {
12972
- const set = /* @__PURE__ */ new Set();
12973
- for (const f of splitLines(execGit("git ls-files"))) set.add(f);
12974
- for (const f of splitLines(execGit("git ls-files --others --exclude-standard"))) set.add(f);
12975
- return Array.from(set);
12976
- }
12977
-
12978
13129
  // src/lib/files.ts
12979
13130
  var import_node_fs8 = require("node:fs");
12980
13131
  var import_node_path8 = require("node:path");
@@ -13107,11 +13258,16 @@ function collectCodeDelta(files, opts) {
13107
13258
 
13108
13259
  // src/lib/debounce.ts
13109
13260
  var import_node_fs9 = require("node:fs");
13110
- var import_node_crypto3 = require("node:crypto");
13111
- function checkDebounce(debounceSeconds = DEBOUNCE_SECONDS) {
13112
- if (!(0, import_node_fs9.existsSync)(DEBOUNCE_FILE)) return null;
13261
+ var import_node_crypto4 = require("node:crypto");
13262
+ function scopedFile(base, sessionId) {
13263
+ if (!sessionId) return base;
13264
+ return `${base}.${(0, import_node_crypto4.createHash)("sha1").update(sessionId).digest("hex").slice(0, 12)}`;
13265
+ }
13266
+ function checkDebounce(debounceSeconds = DEBOUNCE_SECONDS, sessionId) {
13267
+ const file = scopedFile(DEBOUNCE_FILE, sessionId);
13268
+ if (!(0, import_node_fs9.existsSync)(file)) return null;
13113
13269
  try {
13114
- const lastTs = parseInt((0, import_node_fs9.readFileSync)(DEBOUNCE_FILE, "utf-8").trim(), 10);
13270
+ const lastTs = parseInt((0, import_node_fs9.readFileSync)(file, "utf-8").trim(), 10);
13115
13271
  const nowTs = Math.floor(Date.now() / 1e3);
13116
13272
  const elapsed = nowTs - lastTs;
13117
13273
  if (elapsed < debounceSeconds) {
@@ -13121,12 +13277,13 @@ function checkDebounce(debounceSeconds = DEBOUNCE_SECONDS) {
13121
13277
  }
13122
13278
  return null;
13123
13279
  }
13124
- function checkMtime(files, bypassForRecentCommits) {
13280
+ function checkMtime(files, bypassForRecentCommits, sessionId) {
13125
13281
  if (bypassForRecentCommits) return null;
13126
- if (!(0, import_node_fs9.existsSync)(DEBOUNCE_FILE)) return null;
13282
+ const file = scopedFile(DEBOUNCE_FILE, sessionId);
13283
+ if (!(0, import_node_fs9.existsSync)(file)) return null;
13127
13284
  let debounceTime;
13128
13285
  try {
13129
- debounceTime = (0, import_node_fs9.statSync)(DEBOUNCE_FILE).mtimeMs;
13286
+ debounceTime = (0, import_node_fs9.statSync)(file).mtimeMs;
13130
13287
  } catch {
13131
13288
  return null;
13132
13289
  }
@@ -13145,7 +13302,7 @@ function checkMtime(files, bypassForRecentCommits) {
13145
13302
  return "No files modified since last analysis";
13146
13303
  }
13147
13304
  function computeContentHash(files) {
13148
- const hash = (0, import_node_crypto3.createHash)("sha1");
13305
+ const hash = (0, import_node_crypto4.createHash)("sha1");
13149
13306
  const sorted = [...files].sort();
13150
13307
  for (const f of sorted) {
13151
13308
  const resolved = resolveFile(f) ?? f;
@@ -13158,11 +13315,12 @@ function computeContentHash(files) {
13158
13315
  }
13159
13316
  return hash.digest("hex");
13160
13317
  }
13161
- function checkContentHash(files) {
13318
+ function checkContentHash(files, sessionId) {
13162
13319
  const hash = computeContentHash(files);
13163
- if ((0, import_node_fs9.existsSync)(HASH_FILE)) {
13320
+ const file = scopedFile(HASH_FILE, sessionId);
13321
+ if ((0, import_node_fs9.existsSync)(file)) {
13164
13322
  try {
13165
- const storedHash = (0, import_node_fs9.readFileSync)(HASH_FILE, "utf-8").trim();
13323
+ const storedHash = (0, import_node_fs9.readFileSync)(file, "utf-8").trim();
13166
13324
  if (hash === storedHash) {
13167
13325
  return { skip: "No source changes since last analysis", hash };
13168
13326
  }
@@ -13171,18 +13329,19 @@ function checkContentHash(files) {
13171
13329
  }
13172
13330
  return { skip: null, hash };
13173
13331
  }
13174
- function recordAnalysisStart() {
13332
+ function recordAnalysisStart(sessionId) {
13175
13333
  (0, import_node_fs9.mkdirSync)(VERITY_DIR, { recursive: true });
13176
- (0, import_node_fs9.writeFileSync)(DEBOUNCE_FILE, String(Math.floor(Date.now() / 1e3)));
13334
+ (0, import_node_fs9.writeFileSync)(scopedFile(DEBOUNCE_FILE, sessionId), String(Math.floor(Date.now() / 1e3)));
13177
13335
  }
13178
- function recordPassHash(hash) {
13179
- (0, import_node_fs9.writeFileSync)(HASH_FILE, hash);
13336
+ function recordPassHash(hash, sessionId) {
13337
+ (0, import_node_fs9.writeFileSync)(scopedFile(HASH_FILE, sessionId), hash);
13180
13338
  }
13181
- function narrowToRecent(files) {
13182
- if (!(0, import_node_fs9.existsSync)(DEBOUNCE_FILE)) return files;
13339
+ function narrowToRecent(files, sessionId) {
13340
+ const file = scopedFile(DEBOUNCE_FILE, sessionId);
13341
+ if (!(0, import_node_fs9.existsSync)(file)) return files;
13183
13342
  let debounceTime;
13184
13343
  try {
13185
- debounceTime = (0, import_node_fs9.statSync)(DEBOUNCE_FILE).mtimeMs;
13344
+ debounceTime = (0, import_node_fs9.statSync)(file).mtimeMs;
13186
13345
  } catch {
13187
13346
  return files;
13188
13347
  }
@@ -13531,14 +13690,14 @@ function cleanStaleSnapshots(dir, keepSet) {
13531
13690
  // src/lib/baseline.ts
13532
13691
  var import_node_fs13 = require("node:fs");
13533
13692
  var import_node_path11 = require("node:path");
13534
- var import_node_crypto4 = require("node:crypto");
13693
+ var import_node_crypto5 = require("node:crypto");
13535
13694
  var BASELINE_VERSION = 1;
13536
13695
  var BASELINE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
13537
13696
  var MIRROR_MAX_BYTES = 2 * 1024 * 1024;
13538
13697
  var DEFAULT_SESSION_KEY = "_default";
13539
13698
  function sessionKey(sessionId) {
13540
13699
  if (!sessionId) return DEFAULT_SESSION_KEY;
13541
- return (0, import_node_crypto4.createHash)("sha256").update(sessionId).digest("hex").slice(0, 16);
13700
+ return (0, import_node_crypto5.createHash)("sha256").update(sessionId).digest("hex").slice(0, 16);
13542
13701
  }
13543
13702
  function sessionDir(key) {
13544
13703
  return (0, import_node_path11.join)(projectPath(BASELINE_DIR), key);
@@ -13722,11 +13881,11 @@ function pruneOldBaselines() {
13722
13881
 
13723
13882
  // src/lib/offline.ts
13724
13883
  var import_node_fs14 = require("node:fs");
13725
- var import_node_crypto5 = require("node:crypto");
13884
+ var import_node_crypto6 = require("node:crypto");
13726
13885
  function cacheRequest(body) {
13727
13886
  try {
13728
13887
  (0, import_node_fs14.mkdirSync)(CACHE_DIR, { recursive: true });
13729
- const suffix = (0, import_node_crypto5.randomBytes)(4).toString("hex");
13888
+ const suffix = (0, import_node_crypto6.randomBytes)(4).toString("hex");
13730
13889
  const filename = `pending-${Math.floor(Date.now() / 1e3)}-${suffix}.json`;
13731
13890
  (0, import_node_fs14.writeFileSync)(`${CACHE_DIR}/${filename}`, JSON.stringify(body));
13732
13891
  } catch {
@@ -14688,7 +14847,7 @@ async function runAnalyze(opts, globals) {
14688
14847
  let currentCommit = "";
14689
14848
  if (analysisMode !== "plan") {
14690
14849
  const debounceSeconds = parseInt(opts.debounce, 10);
14691
- const debounceSkip = checkDebounce(debounceSeconds);
14850
+ const debounceSkip = checkDebounce(debounceSeconds, baselineSessionId);
14692
14851
  if (debounceSkip) {
14693
14852
  if (assistantResponse) {
14694
14853
  analysisMode = "plan";
@@ -14698,7 +14857,7 @@ async function runAnalyze(opts, globals) {
14698
14857
  }
14699
14858
  if (analysisMode !== "plan") {
14700
14859
  const allCheckable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...reviewable, ...securityFiles]));
14701
- const mtimeSkip = checkMtime(allCheckable, hasRecentCommitFiles);
14860
+ const mtimeSkip = checkMtime(allCheckable, hasRecentCommitFiles, baselineSessionId);
14702
14861
  if (mtimeSkip) {
14703
14862
  if (assistantResponse) {
14704
14863
  analysisMode = "plan";
@@ -14708,9 +14867,9 @@ async function runAnalyze(opts, globals) {
14708
14867
  }
14709
14868
  }
14710
14869
  if (analysisMode !== "plan") {
14711
- recordAnalysisStart();
14870
+ recordAnalysisStart(baselineSessionId);
14712
14871
  const allCheckable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...reviewable, ...securityFiles]));
14713
- const hashResult = checkContentHash(allCheckable);
14872
+ const hashResult = checkContentHash(allCheckable, baselineSessionId);
14714
14873
  if (hashResult.skip) {
14715
14874
  if (assistantResponse) {
14716
14875
  analysisMode = "plan";
@@ -14722,7 +14881,7 @@ async function runAnalyze(opts, globals) {
14722
14881
  if (analysisMode !== "plan") {
14723
14882
  const agentNarrowed = narrowToAgentAuthored(allForReview, actionSummary);
14724
14883
  const baseForReview = agentNarrowed.length > 0 ? agentNarrowed : allForReview;
14725
- const recentForReview = narrowToRecent(baseForReview);
14884
+ const recentForReview = narrowToRecent(baseForReview, baselineSessionId);
14726
14885
  if (!opts.skipStatic && isCodacyAvailable()) {
14727
14886
  let allScannable = Array.from(/* @__PURE__ */ new Set([...analyzable, ...securityFiles]));
14728
14887
  if (baseline) {
@@ -15097,7 +15256,7 @@ async function runAnalyze(opts, globals) {
15097
15256
  }
15098
15257
  case "PASS": {
15099
15258
  writeIteration(1, currentCommit, contentHash ?? void 0);
15100
- if (contentHash) recordPassHash(contentHash);
15259
+ if (contentHash) recordPassHash(contentHash, baselineSessionId);
15101
15260
  if (currentCommit && currentCommit !== "no-git") writeBaselineSha(currentCommit);
15102
15261
  let userSummary = response.user_summary ?? "Verity: PASS";
15103
15262
  const viewUrl = response.view_url ?? "";
@@ -15108,7 +15267,7 @@ async function runAnalyze(opts, globals) {
15108
15267
  break;
15109
15268
  }
15110
15269
  case "WARN": {
15111
- if (contentHash) recordPassHash(contentHash);
15270
+ if (contentHash) recordPassHash(contentHash, baselineSessionId);
15112
15271
  if (currentCommit && currentCommit !== "no-git") writeBaselineSha(currentCommit);
15113
15272
  let userSummary = response.user_summary ?? "Verity: WARN";
15114
15273
  const viewUrl = response.view_url ?? "";
@@ -16766,7 +16925,7 @@ function registerTelemetryCommands(program2) {
16766
16925
  }
16767
16926
 
16768
16927
  // src/cli.ts
16769
- program.name("verity").description("CLI for Verity quality gate service").version("0.24.0-experimental.fe39839").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr");
16928
+ program.name("verity").description("CLI for Verity quality gate service").version("0.25.0-experimental.15d1b58").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr");
16770
16929
  registerAuthCommands(program);
16771
16930
  registerHooksCommands(program);
16772
16931
  registerIntentCommands(program);
@@ -312,18 +312,25 @@ Expected: single-digit findings per file, not hundreds. If you see 50+ issues fr
312
312
 
313
313
  ## Step 6: Register with Verity service
314
314
 
315
- Use the `verity` CLI to register. It handles credential storage and service URL automatically.
315
+ Use the `verity` CLI to register. It handles provider auth, credential storage, and the service URL automatically.
316
316
 
317
317
  ```bash
318
318
  verity auth register --project "PROJECT_NAME" --remote "GIT_REMOTE_URL"
319
319
  ```
320
320
 
321
+ Registration is **provider-gated** (GitHub today): the CLI runs a GitHub OAuth
322
+ **device flow** and prints something like *"open https://github.com/login/device
323
+ and enter code WXYZ-1234"*. The user approves in the browser; the CLI then proves
324
+ the user has **write access** to the repo before the service issues a token. Tell
325
+ the user to expect this prompt and to complete it in their browser.
326
+
321
327
  This command:
322
- - **New project**: Registers, stores token + service URL in `.verity/credentials`, prints `project_id`. Continue.
323
- - **Already registered**: Automatically discovers the project, ensures `.verity/credentials` has the service URL. If a token already exists in credentials, it updates the file and succeeds. If no token exists, it asks you to paste one.
328
+ - **Write access confirmed**: Registers, stores the verity token + service URL + provider token in `.verity/credentials` (perms 600), prints `project_id` and the authenticated email. Continue.
329
+ - **No write access** (`403 NO_WRITE_ACCESS`): The user lacks push rights on the repo they can't register it. Verity still runs locally as an anonymous gate, but no history/org/repo data is stored. Stop.
330
+ - **Non-GitHub remote**: Only GitHub is supported for now; show the error and stop.
324
331
  - **Other errors**: Show the error and stop.
325
332
 
326
- After this step, `.verity/credentials` will contain both `token` and `service_url` — all subsequent `verity` commands will work.
333
+ After this step, `.verity/credentials` will contain `token`, `service_url`, and `provider_token` — all subsequent `verity` commands will work.
327
334
 
328
335
  ---
329
336
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codacy/verity-cli",
3
- "version": "0.24.0-experimental.fe39839",
3
+ "version": "0.25.0-experimental.15d1b58",
4
4
  "description": "CLI for Verity quality gate service",
5
5
  "homepage": "https://verity.md",
6
6
  "repository": {