@codacy/verity-cli 0.35.5 → 0.36.0

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
@@ -926,8 +926,8 @@ var require_suggestSimilar = __commonJS2({
926
926
  if (candidate.length <= 1) return;
927
927
  const distance = editDistance(word, candidate);
928
928
  const length = Math.max(word.length, candidate.length);
929
- const similarity = (length - distance) / length;
930
- if (similarity > minSimilarity) {
929
+ const similarity2 = (length - distance) / length;
930
+ if (similarity2 > minSimilarity) {
931
931
  if (distance < bestDistance) {
932
932
  bestDistance = distance;
933
933
  similar = [candidate];
@@ -14442,11 +14442,61 @@ var {
14442
14442
  Help
14443
14443
  } = import_index.default;
14444
14444
 
14445
- // src/commands/auth.ts
14446
- var import_node_child_process5 = require("node:child_process");
14445
+ // src/lib/child-process.ts
14446
+ var cp = __toESM2(require("node:child_process"));
14447
+ var import_node_util = require("node:util");
14448
+ function hiddenOptions(options) {
14449
+ return { ...options ?? {}, windowsHide: true };
14450
+ }
14451
+ function argvCall(rest) {
14452
+ let [args2, options, callback] = rest;
14453
+ if (!Array.isArray(args2)) {
14454
+ if (typeof args2 === "function") {
14455
+ callback = args2;
14456
+ options = void 0;
14457
+ } else if (args2 != null && typeof args2 === "object") {
14458
+ callback = options;
14459
+ options = args2;
14460
+ }
14461
+ args2 = [];
14462
+ }
14463
+ if (typeof options === "function") {
14464
+ callback = options;
14465
+ options = void 0;
14466
+ }
14467
+ return { args: args2, options: hiddenOptions(options), callback };
14468
+ }
14469
+ var execSync2 = ((command, options) => cp.execSync(command, hiddenOptions(options)));
14470
+ var execFileSync2 = ((file, ...rest) => {
14471
+ const { args: args2, options } = argvCall(rest);
14472
+ return cp.execFileSync(file, args2, options);
14473
+ });
14474
+ var spawnSync2 = ((command, ...rest) => {
14475
+ const { args: args2, options } = argvCall(rest);
14476
+ return cp.spawnSync(command, args2, options);
14477
+ });
14478
+ var spawn2 = ((command, ...rest) => {
14479
+ const { args: args2, options } = argvCall(rest);
14480
+ return cp.spawn(command, args2, options);
14481
+ });
14482
+ var spawnShown = ((command, ...rest) => {
14483
+ const { args: args2, options } = argvCall(rest);
14484
+ const { windowsHide: _hidden, ...shown } = options;
14485
+ return cp.spawn(command, args2, shown);
14486
+ });
14487
+ var execFile2 = ((file, ...rest) => {
14488
+ const { args: args2, options, callback } = argvCall(rest);
14489
+ return cp.execFile(file, args2, options, callback);
14490
+ });
14491
+ var execFileAsync = (0, import_node_util.promisify)(cp.execFile);
14492
+ Object.defineProperty(execFile2, import_node_util.promisify.custom, {
14493
+ value: (file, ...rest) => {
14494
+ const { args: args2, options } = argvCall(rest);
14495
+ return execFileAsync(file, args2, options);
14496
+ }
14497
+ });
14447
14498
 
14448
14499
  // src/constants.ts
14449
- var import_node_child_process = require("node:child_process");
14450
14500
  var import_node_fs2 = require("node:fs");
14451
14501
  var import_node_path2 = require("node:path");
14452
14502
 
@@ -14532,7 +14582,7 @@ var _repoRoot = null;
14532
14582
  function repoRoot() {
14533
14583
  if (_repoRoot === null) {
14534
14584
  try {
14535
- _repoRoot = (0, import_node_child_process.execSync)("git rev-parse --show-toplevel", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
14585
+ _repoRoot = execFileSync2("git", ["rev-parse", "--show-toplevel"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
14536
14586
  } catch {
14537
14587
  _repoRoot = process.cwd();
14538
14588
  }
@@ -14547,7 +14597,7 @@ var _mainRoot;
14547
14597
  function mainWorktreeRoot() {
14548
14598
  if (_mainRoot !== void 0) return _mainRoot;
14549
14599
  try {
14550
- const common = (0, import_node_child_process.execSync)("git rev-parse --git-common-dir", {
14600
+ const common = execFileSync2("git", ["rev-parse", "--git-common-dir"], {
14551
14601
  encoding: "utf-8",
14552
14602
  stdio: ["pipe", "pipe", "pipe"]
14553
14603
  }).trim();
@@ -14824,7 +14874,6 @@ function rotateIfNeeded(file) {
14824
14874
  // src/lib/credentials.ts
14825
14875
  var import_promises = require("node:fs/promises");
14826
14876
  var import_node_path3 = require("node:path");
14827
- var import_node_child_process2 = require("node:child_process");
14828
14877
 
14829
14878
  // src/lib/service-origin.ts
14830
14879
  var LEGACY = new URL(LEGACY_PROD_SERVICE_ORIGIN);
@@ -14860,7 +14909,7 @@ function globalCredentialsPath() {
14860
14909
  }
14861
14910
  function currentRemote() {
14862
14911
  try {
14863
- return (0, import_node_child_process2.execSync)("git remote get-url origin", {
14912
+ return execFileSync2("git", ["remote", "get-url", "origin"], {
14864
14913
  encoding: "utf-8",
14865
14914
  stdio: ["pipe", "pipe", "pipe"]
14866
14915
  }).trim();
@@ -15073,15 +15122,64 @@ async function foldLegacyLocalCredential(remoteArg) {
15073
15122
  }
15074
15123
 
15075
15124
  // src/lib/git.ts
15076
- var import_node_child_process3 = require("node:child_process");
15077
15125
  var import_node_fs4 = require("node:fs");
15078
15126
  var import_node_path4 = require("node:path");
15127
+
15128
+ // src/lib/secret-paths.ts
15129
+ var SECRET_SHAPED_PATHS = [
15130
+ // dotenv and direnv
15131
+ /(^|\/)\.env(\.[^/]*)?$/i,
15132
+ /(^|\/)\.envrc$/i,
15133
+ // private keys, keystores, encrypted vaults
15134
+ /\.(pem|key|p8|p12|pfx|jks|keystore|asc|gpg|kdbx|keytab|ovpn)$/i,
15135
+ /(^|\/)id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/i,
15136
+ // tools that keep a credential in a dotfile
15137
+ /(^|\/)\.(npmrc|netrc|pypirc|pgpass|git-credentials|htpasswd|s3cfg|boto|dockercfg|terraformrc)$/i,
15138
+ /(^|\/)terraform\.rc$/i,
15139
+ // Terraform state and variables carry provider secrets in plain text
15140
+ /\.tfstate(\.backup)?$/i,
15141
+ /\.tfvars(\.json)?$/i,
15142
+ // cloud service-account keys and kubeconfig
15143
+ /(^|\/)[^/]*service[-_]?account[^/]*\.json$/i,
15144
+ /(^|\/)application_default_credentials\.json$/i,
15145
+ /(^|\/)kubeconfig(\.(ya?ml|json|conf))?$/i,
15146
+ // A file NAMED credentials or secret(s), with ANY extension. ⚠ This includes
15147
+ // source modules such as `src/config/secrets.ts` and `credentials.py`: such a
15148
+ // module often holds the literal key, so it is withheld like any other secret.
15149
+ // The cost: the AI reviewer never reads it. Trivy still scans it on the
15150
+ // machine (git.ts filterSecurity), and its path is sent. The agent tools
15151
+ // refused these names before this list was shared (agent-code-tools.test.ts).
15152
+ /(^|\/)(credentials|secrets?)(\.[^/]*)?$/i,
15153
+ // directories that hold nothing but credentials
15154
+ /(^|\/)\.(ssh|aws|gnupg|docker|kube)(\/|$)/i,
15155
+ /(^|\/)\.git(\/|$)/
15156
+ ];
15157
+ var SECRET_WITHHELD_REASON = "secret-shaped-path";
15158
+ var SECRET_REFUSED_READ = "shaped like a secret: Verity never sends this file";
15159
+ function isSecretShapedPath(path) {
15160
+ const normalized = path.replace(/\\/g, "/");
15161
+ return SECRET_SHAPED_PATHS.some((pattern) => pattern.test(normalized));
15162
+ }
15163
+ function scannerWitnessedWithheld(excluded, staticFindings) {
15164
+ const norm3 = (p) => p.replace(/\\/g, "/").replace(/^(\.\/)+/, "");
15165
+ const withheld = new Set((excluded ?? []).filter((e) => e.reason === SECRET_WITHHELD_REASON).map((e) => norm3(e.path)));
15166
+ if (withheld.size === 0) return [];
15167
+ const out2 = /* @__PURE__ */ new Set();
15168
+ for (const s of staticFindings ?? []) {
15169
+ if (typeof s.file !== "string" || typeof s.tool !== "string" || s.tool.length === 0) continue;
15170
+ const file = norm3(s.file);
15171
+ if (withheld.has(file)) out2.add(file);
15172
+ }
15173
+ return [...out2];
15174
+ }
15175
+
15176
+ // src/lib/git.ts
15079
15177
  function resolveFile(relpath) {
15080
15178
  return (0, import_node_fs4.existsSync)(relpath) ? relpath : null;
15081
15179
  }
15082
- function execGit(cmd) {
15180
+ function execGit(...args2) {
15083
15181
  try {
15084
- return (0, import_node_child_process3.execSync)(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
15182
+ return execFileSync2("git", args2, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
15085
15183
  } catch {
15086
15184
  return "";
15087
15185
  }
@@ -15122,20 +15220,25 @@ function writeBaselineSha(sha) {
15122
15220
  }
15123
15221
  function commitObjectExists(sha) {
15124
15222
  if (!SHA_RE.test(sha)) return false;
15125
- return execGit(`git cat-file -e ${sha}^{commit} 2>/dev/null && echo ok`) === "ok";
15223
+ try {
15224
+ execFileSync2("git", ["cat-file", "-e", `${sha}^{commit}`], { stdio: "ignore" });
15225
+ return true;
15226
+ } catch {
15227
+ return false;
15228
+ }
15126
15229
  }
15127
15230
  function committedSinceRewrite(oldSha) {
15128
15231
  if (!SHA_RE.test(oldSha)) return [];
15129
- const newLocal = new Set(splitLines(execGit(`git rev-list HEAD --not ${oldSha} --remotes`)));
15232
+ const newLocal = new Set(splitLines(execGit("rev-list", "HEAD", "--not", oldSha, "--remotes")));
15130
15233
  if (newLocal.size === 0) return [];
15131
15234
  const files = /* @__PURE__ */ new Set();
15132
- for (const line2 of splitLines(execGit(`git cherry ${oldSha} HEAD`))) {
15235
+ for (const line2 of splitLines(execGit("cherry", oldSha, "HEAD"))) {
15133
15236
  const sp = line2.indexOf(" ");
15134
15237
  if (sp < 0) continue;
15135
15238
  const mark = line2.slice(0, sp);
15136
15239
  const sha = line2.slice(sp + 1).trim();
15137
15240
  if (mark !== "+" || !newLocal.has(sha)) continue;
15138
- for (const f of splitLines(execGit(`git diff-tree --no-commit-id --name-only -r ${sha}`))) files.add(f);
15241
+ for (const f of splitLines(execGit("diff-tree", "--no-commit-id", "--name-only", "-r", sha))) files.add(f);
15139
15242
  }
15140
15243
  return [...files].filter((f) => !isVerityOwnedPath(f));
15141
15244
  }
@@ -15148,13 +15251,13 @@ function isVerityOwnedPath(file) {
15148
15251
  function getChangedFiles() {
15149
15252
  const sets = /* @__PURE__ */ new Set();
15150
15253
  let hasRecentCommitFiles = false;
15151
- for (const f of splitLines(execGit("git diff --name-only HEAD"))) sets.add(f);
15152
- for (const f of splitLines(execGit("git diff --name-only --cached"))) sets.add(f);
15153
- for (const f of splitLines(execGit("git ls-files --others --exclude-standard"))) sets.add(f);
15254
+ for (const f of splitLines(execGit("diff", "--name-only", "HEAD"))) sets.add(f);
15255
+ for (const f of splitLines(execGit("diff", "--name-only", "--cached"))) sets.add(f);
15256
+ for (const f of splitLines(execGit("ls-files", "--others", "--exclude-standard"))) sets.add(f);
15154
15257
  const rawBaseline = readRawBaselineSha();
15155
15258
  const baseline = readBaselineSha();
15156
15259
  if (baseline) {
15157
- const committed = splitLines(execGit(`git diff --name-only ${baseline}..HEAD`));
15260
+ const committed = splitLines(execGit("diff", "--name-only", `${baseline}..HEAD`));
15158
15261
  if (committed.length > 0) {
15159
15262
  hasRecentCommitFiles = true;
15160
15263
  for (const f of committed) sets.add(f);
@@ -15167,12 +15270,12 @@ function getChangedFiles() {
15167
15270
  for (const f of committed) sets.add(f);
15168
15271
  }
15169
15272
  } else {
15170
- const headTimestamp = parseInt(execGit("git log -1 --format=%ct HEAD"), 10) || 0;
15273
+ const headTimestamp = parseInt(execGit("log", "-1", "--format=%ct", "HEAD"), 10) || 0;
15171
15274
  const commitAge = Math.floor(Date.now() / 1e3) - headTimestamp;
15172
- const hasUnstaged = splitLines(execGit("git diff --name-only HEAD")).length > 0;
15173
- const hasStaged = splitLines(execGit("git diff --name-only --cached")).length > 0;
15275
+ const hasUnstaged = splitLines(execGit("diff", "--name-only", "HEAD")).length > 0;
15276
+ const hasStaged = splitLines(execGit("diff", "--name-only", "--cached")).length > 0;
15174
15277
  if (commitAge < 120 && !hasUnstaged && !hasStaged) {
15175
- const recentFiles = splitLines(execGit("git diff --name-only HEAD~1..HEAD"));
15278
+ const recentFiles = splitLines(execGit("diff", "--name-only", "HEAD~1..HEAD"));
15176
15279
  if (recentFiles.length > 0) {
15177
15280
  hasRecentCommitFiles = true;
15178
15281
  for (const f of recentFiles) sets.add(f);
@@ -15184,16 +15287,16 @@ function getChangedFiles() {
15184
15287
  }
15185
15288
  function getDirtyFiles() {
15186
15289
  const set = /* @__PURE__ */ new Set();
15187
- for (const f of splitLines(execGit("git diff --name-only HEAD"))) set.add(f);
15188
- for (const f of splitLines(execGit("git diff --name-only --cached"))) set.add(f);
15189
- for (const f of splitLines(execGit("git ls-files --others --exclude-standard"))) set.add(f);
15290
+ for (const f of splitLines(execGit("diff", "--name-only", "HEAD"))) set.add(f);
15291
+ for (const f of splitLines(execGit("diff", "--name-only", "--cached"))) set.add(f);
15292
+ for (const f of splitLines(execGit("ls-files", "--others", "--exclude-standard"))) set.add(f);
15190
15293
  return Array.from(set).filter((f) => !isVerityOwnedPath(f));
15191
15294
  }
15192
15295
  function showContentAtRef(ref, repoRelPath) {
15193
15296
  if (!ref || ref === "no-git") return null;
15194
15297
  const normalizedPath = repoRelPath.replace(/\\/g, "/");
15195
15298
  try {
15196
- return (0, import_node_child_process3.execFileSync)("git", ["show", `${ref}:${normalizedPath}`], {
15299
+ return execFileSync2("git", ["show", `${ref}:${normalizedPath}`], {
15197
15300
  encoding: "utf-8",
15198
15301
  maxBuffer: 64 * 1024 * 1024,
15199
15302
  stdio: ["pipe", "pipe", "pipe"]
@@ -15221,20 +15324,20 @@ function filterReviewable(files) {
15221
15324
  }
15222
15325
  function filterSecurity(files) {
15223
15326
  return files.filter(
15224
- (f) => SECURITY_PATTERNS.some((p) => p.test(f))
15327
+ (f) => SECURITY_PATTERNS.some((p) => p.test(f)) || isSecretShapedPath(f)
15225
15328
  );
15226
15329
  }
15227
15330
  function getCurrentCommit() {
15228
- return execGit("git rev-parse HEAD") || "no-git";
15331
+ return execGit("rev-parse", "HEAD") || "no-git";
15229
15332
  }
15230
15333
  function getCurrentBranch() {
15231
- const b = execGit("git rev-parse --abbrev-ref HEAD");
15334
+ const b = execGit("rev-parse", "--abbrev-ref", "HEAD");
15232
15335
  return !b || b === "HEAD" ? null : b;
15233
15336
  }
15234
15337
  function commitResolves(sha) {
15235
15338
  if (!sha) return false;
15236
15339
  try {
15237
- (0, import_node_child_process3.execFileSync)("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
15340
+ execFileSync2("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
15238
15341
  stdio: ["pipe", "pipe", "pipe"]
15239
15342
  });
15240
15343
  return true;
@@ -15247,7 +15350,7 @@ function commitsSincePaths(sha, paths) {
15247
15350
  try {
15248
15351
  const args2 = ["rev-list", "--count", `${sha}..HEAD`];
15249
15352
  if (paths.length > 0) args2.push("--", ...paths.slice(0, 50));
15250
- const out2 = (0, import_node_child_process3.execFileSync)("git", args2, {
15353
+ const out2 = execFileSync2("git", args2, {
15251
15354
  encoding: "utf-8",
15252
15355
  stdio: ["pipe", "pipe", "pipe"]
15253
15356
  }).trim();
@@ -15301,8 +15404,8 @@ function parseRemote(raw) {
15301
15404
  }
15302
15405
  function listTrackedFiles() {
15303
15406
  const set = /* @__PURE__ */ new Set();
15304
- for (const f of splitLines(execGit("git ls-files"))) set.add(f);
15305
- for (const f of splitLines(execGit("git ls-files --others --exclude-standard"))) set.add(f);
15407
+ for (const f of splitLines(execGit("ls-files"))) set.add(f);
15408
+ for (const f of splitLines(execGit("ls-files", "--others", "--exclude-standard"))) set.add(f);
15306
15409
  return Array.from(set);
15307
15410
  }
15308
15411
  function sanitizeRemote(remote) {
@@ -15351,7 +15454,7 @@ function pinRefusalMessage(verdict, targetUrl) {
15351
15454
  // src/lib/cli-version.ts
15352
15455
  function cliVersion() {
15353
15456
  try {
15354
- return true ? "0.35.5" : "dev";
15457
+ return true ? "0.36.0" : "dev";
15355
15458
  } catch {
15356
15459
  return "dev";
15357
15460
  }
@@ -15730,7 +15833,6 @@ async function maybeHealServiceUrl(resolution, verbose) {
15730
15833
  var import_node_os2 = require("node:os");
15731
15834
 
15732
15835
  // src/lib/handoff.ts
15733
- var import_node_child_process4 = require("node:child_process");
15734
15836
  function isHttpsPage(url) {
15735
15837
  try {
15736
15838
  const u = new URL(url);
@@ -15767,7 +15869,7 @@ function openInBrowser(url, opts = {}) {
15767
15869
  const cmd = launcher(platform4, url);
15768
15870
  if (!cmd) return false;
15769
15871
  try {
15770
- const child = (opts.run ?? import_node_child_process4.spawn)(cmd[0], cmd[1], { stdio: "ignore", detached: true });
15872
+ const child = (opts.run ?? spawnShown)(cmd[0], cmd[1], { stdio: "ignore", detached: true });
15771
15873
  child.on("error", () => {
15772
15874
  });
15773
15875
  child.unref();
@@ -15791,7 +15893,7 @@ function copyToClipboard(text3, opts = {}) {
15791
15893
  if (env.SSH_CONNECTION || env.SSH_TTY || env.CI) return false;
15792
15894
  for (const [cmd, args2] of clipboards(platform4, env)) {
15793
15895
  try {
15794
- const r = (opts.run ?? import_node_child_process4.spawnSync)(cmd, args2, { input: text3, stdio: ["pipe", "ignore", "ignore"], timeout: 2e3 });
15896
+ const r = (opts.run ?? spawnSync2)(cmd, args2, { input: text3, stdio: ["pipe", "ignore", "ignore"], timeout: 2e3 });
15795
15897
  if (!r.error && r.status === 0) return true;
15796
15898
  } catch {
15797
15899
  }
@@ -16245,7 +16347,7 @@ function registerAuthCommands(program2) {
16245
16347
  let remote = opts.remote;
16246
16348
  if (!remote) {
16247
16349
  try {
16248
- remote = (0, import_node_child_process5.execSync)("git remote get-url origin", { encoding: "utf-8" }).trim();
16350
+ remote = execFileSync2("git", ["remote", "get-url", "origin"], { encoding: "utf-8" }).trim();
16249
16351
  } catch {
16250
16352
  printError("No git remote found. Use --remote to specify one.");
16251
16353
  process.exit(1);
@@ -16297,7 +16399,7 @@ function registerAuthCommands(program2) {
16297
16399
  let remote = opts.remote;
16298
16400
  if (!remote) {
16299
16401
  try {
16300
- remote = (0, import_node_child_process5.execSync)("git remote get-url origin", { encoding: "utf-8" }).trim();
16402
+ remote = execFileSync2("git", ["remote", "get-url", "origin"], { encoding: "utf-8" }).trim();
16301
16403
  } catch {
16302
16404
  printError("No git remote found. Use --remote to specify one.");
16303
16405
  process.exit(1);
@@ -16963,7 +17065,6 @@ var import_yaml = __toESM2(require_dist());
16963
17065
  var import_node_path11 = require("node:path");
16964
17066
 
16965
17067
  // src/lib/detect.ts
16966
- var import_node_child_process6 = require("node:child_process");
16967
17068
  var import_node_fs10 = require("node:fs");
16968
17069
  var import_node_path10 = require("node:path");
16969
17070
 
@@ -17058,8 +17159,12 @@ function collectCodeDelta(files, opts) {
17058
17159
  let truncationReason = null;
17059
17160
  const droppedPaths = [];
17060
17161
  const excluded = [];
17061
- const exclude = (path, reason) => excluded.push({ path, reason, stage: "collectCodeDelta", kind: "capacity" });
17162
+ const exclude = (path, reason, kind2 = "capacity") => excluded.push({ path, reason, stage: "collectCodeDelta", kind: kind2 });
17062
17163
  for (const filepath of sorted) {
17164
+ if (isSecretShapedPath(filepath)) {
17165
+ exclude(filepath, SECRET_WITHHELD_REASON, "policy");
17166
+ continue;
17167
+ }
17063
17168
  if (result.length >= maxFiles) {
17064
17169
  truncationReason ??= "max_files";
17065
17170
  droppedPaths.push(filepath);
@@ -17229,7 +17334,7 @@ function isProjectFile(path) {
17229
17334
  }
17230
17335
  function listProjectFiles(root) {
17231
17336
  try {
17232
- const out2 = (0, import_node_child_process6.execSync)("git ls-files -z --cached --others --exclude-standard", {
17337
+ const out2 = execFileSync2("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
17233
17338
  cwd: root,
17234
17339
  encoding: "utf-8",
17235
17340
  maxBuffer: 32 * 1024 * 1024,
@@ -17717,8 +17822,8 @@ var CLI_DECLARATION = {
17717
17822
  // src/lib/verity-md.ts
17718
17823
  var UNPRINTABLE = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]+/g;
17719
17824
  function markdownPlain(text3, max = 200) {
17720
- const oneLine2 = text3.replace(UNPRINTABLE, " ").replace(/\s+/g, " ").trim();
17721
- const bounded = oneLine2.length > max ? `${oneLine2.slice(0, max - 1)}\u2026` : oneLine2;
17825
+ const oneLine3 = text3.replace(UNPRINTABLE, " ").replace(/\s+/g, " ").trim();
17826
+ const bounded = oneLine3.length > max ? `${oneLine3.slice(0, max - 1)}\u2026` : oneLine3;
17722
17827
  return bounded.replace(/[\\`*_{}[\]()<>#+!|~]/g, (c) => `\\${c}`);
17723
17828
  }
17724
17829
  function serviceLink(url) {
@@ -19183,7 +19288,6 @@ var import_node_fs13 = require("node:fs");
19183
19288
  var import_node_path14 = require("node:path");
19184
19289
 
19185
19290
  // src/lib/hardened-git.ts
19186
- var import_node_child_process7 = require("node:child_process");
19187
19291
  var HARDENED_CONFIG_ARGS = [
19188
19292
  "-c",
19189
19293
  "core.fsmonitor=false",
@@ -19217,7 +19321,7 @@ function classifyError(err2) {
19217
19321
  }
19218
19322
  function runGitSync(cwd, args2, opts = {}) {
19219
19323
  try {
19220
- const stdout = (0, import_node_child_process7.execFileSync)("git", hardenedGitArgv(args2), {
19324
+ const stdout = execFileSync2("git", hardenedGitArgv(args2), {
19221
19325
  cwd,
19222
19326
  env: hardenedGitEnv(),
19223
19327
  encoding: "utf-8",
@@ -19232,7 +19336,7 @@ function runGitSync(cwd, args2, opts = {}) {
19232
19336
  }
19233
19337
  function runGit(cwd, args2, opts = {}) {
19234
19338
  return new Promise((resolvePromise) => {
19235
- (0, import_node_child_process7.execFile)(
19339
+ execFile2(
19236
19340
  "git",
19237
19341
  hardenedGitArgv(args2),
19238
19342
  {
@@ -20094,7 +20198,6 @@ var import_node_crypto10 = require("node:crypto");
20094
20198
  // src/lib/conversation-buffer.ts
20095
20199
  var import_promises5 = require("node:fs/promises");
20096
20200
  var import_node_fs19 = require("node:fs");
20097
- var import_node_child_process8 = require("node:child_process");
20098
20201
  var import_node_crypto2 = require("node:crypto");
20099
20202
  function stripImageReferences(text3) {
20100
20203
  return text3.replace(/\[Image #\d+\]/g, "[screenshot \u2014 not available for review]");
@@ -20197,8 +20300,9 @@ async function readBufferEntries() {
20197
20300
  }
20198
20301
  function getRecentCommitMessages() {
20199
20302
  try {
20200
- const output = (0, import_node_child_process8.execSync)(
20201
- 'git log --since="30 minutes ago" --format="%s" -5',
20303
+ const output = execFileSync2(
20304
+ "git",
20305
+ ["log", "--since=30 minutes ago", "--format=%s", "-5"],
20202
20306
  { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
20203
20307
  ).trim();
20204
20308
  if (!output) return [];
@@ -21206,7 +21310,6 @@ var import_node_path23 = require("node:path");
21206
21310
  var import_node_crypto4 = require("node:crypto");
21207
21311
 
21208
21312
  // src/lib/gitignore.ts
21209
- var import_node_child_process9 = require("node:child_process");
21210
21313
  var import_node_fs24 = require("node:fs");
21211
21314
  var VERITY_GITIGNORE_MARKER = "# Verity \u2014 machine-local state.";
21212
21315
  var SETTINGS_LOCAL_IGNORE_ENTRY = ".claude/settings.local.json";
@@ -21247,7 +21350,7 @@ var SUPERSEDED_STANDARD_LINE = "!.verity/standard.yaml";
21247
21350
  var OLD_COMMENT_LINES = /* @__PURE__ */ new Set([SUPERSEDED_COMMENT, ...MEMORY_COMMENT]);
21248
21351
  function isIgnored3(path, opts = {}) {
21249
21352
  try {
21250
- (0, import_node_child_process9.execSync)(`git check-ignore -q ${opts.noIndex ? "--no-index " : ""}-- "${path}"`, { stdio: "pipe" });
21353
+ execFileSync2("git", ["check-ignore", "-q", ...opts.noIndex ? ["--no-index"] : [], "--", path], { stdio: "pipe" });
21251
21354
  return true;
21252
21355
  } catch (err2) {
21253
21356
  return err2.status === 1 ? false : null;
@@ -21266,7 +21369,7 @@ function memoryPosture() {
21266
21369
  }
21267
21370
  function committedMemoryFiles() {
21268
21371
  try {
21269
- return (0, import_node_child_process9.execSync)("git ls-files -z -- .verity/memory", {
21372
+ return execFileSync2("git", ["ls-files", "-z", "--", ".verity/memory"], {
21270
21373
  encoding: "utf-8",
21271
21374
  stdio: ["pipe", "pipe", "pipe"]
21272
21375
  }).split("\0").filter(Boolean);
@@ -21282,7 +21385,7 @@ function standardPosture() {
21282
21385
  }
21283
21386
  function committedStandardFile() {
21284
21387
  try {
21285
- return (0, import_node_child_process9.execSync)(`git ls-files -z -- ${STANDARD_PATH}`, {
21388
+ return execFileSync2("git", ["ls-files", "-z", "--", STANDARD_PATH], {
21286
21389
  encoding: "utf-8",
21287
21390
  stdio: ["pipe", "pipe", "pipe"]
21288
21391
  }).split("\0").filter(Boolean).length > 0;
@@ -21366,7 +21469,7 @@ function ensureVerityGitignore() {
21366
21469
  function untrackMemory() {
21367
21470
  if (committedMemoryFiles().length === 0) return "none";
21368
21471
  try {
21369
- (0, import_node_child_process9.execSync)("git rm -r --cached --quiet -- .verity/memory", { stdio: "pipe" });
21472
+ execFileSync2("git", ["rm", "-r", "--cached", "--quiet", "--", ".verity/memory"], { stdio: "pipe" });
21370
21473
  return "untracked";
21371
21474
  } catch {
21372
21475
  return "failed";
@@ -21441,7 +21544,7 @@ function untrackStandard() {
21441
21544
  }
21442
21545
  if (!committedStandardFile()) return { ok: true, untracked: false };
21443
21546
  try {
21444
- (0, import_node_child_process9.execSync)(`git rm --cached --quiet -- ${STANDARD_PATH}`, { stdio: "pipe" });
21547
+ execFileSync2("git", ["rm", "--cached", "--quiet", "--", STANDARD_PATH], { stdio: "pipe" });
21445
21548
  return { ok: true, untracked: true };
21446
21549
  } catch {
21447
21550
  restore();
@@ -21473,7 +21576,7 @@ function keepMemoryTracked() {
21473
21576
  function committedVerityState() {
21474
21577
  let out2 = "";
21475
21578
  try {
21476
- out2 = (0, import_node_child_process9.execSync)("git ls-files -z -- .verity", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
21579
+ out2 = execFileSync2("git", ["ls-files", "-z", "--", ".verity"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
21477
21580
  } catch {
21478
21581
  return [];
21479
21582
  }
@@ -21484,16 +21587,16 @@ function untrackVerityState() {
21484
21587
  if (tracked.length === 0) return "none";
21485
21588
  const standardWasTracked = committedStandardFile();
21486
21589
  try {
21487
- (0, import_node_child_process9.execSync)("git rm -r --cached --quiet -- .verity", { stdio: "pipe" });
21590
+ execFileSync2("git", ["rm", "-r", "--cached", "--quiet", "--", ".verity"], { stdio: "pipe" });
21488
21591
  if (standardWasTracked) {
21489
21592
  try {
21490
- (0, import_node_child_process9.execSync)(`git add -f -- ${STANDARD_PATH}`, { stdio: "pipe" });
21593
+ execFileSync2("git", ["add", "-f", "--", STANDARD_PATH], { stdio: "pipe" });
21491
21594
  } catch {
21492
21595
  }
21493
21596
  }
21494
21597
  if (memoryPosture() === "committable") {
21495
21598
  try {
21496
- (0, import_node_child_process9.execSync)("git add -- .verity/memory", { stdio: "pipe" });
21599
+ execFileSync2("git", ["add", "--", ".verity/memory"], { stdio: "pipe" });
21497
21600
  } catch {
21498
21601
  }
21499
21602
  }
@@ -21637,6 +21740,152 @@ async function buildManifest() {
21637
21740
  function hashContent(content) {
21638
21741
  return `sha256:${(0, import_node_crypto4.createHash)("sha256").update(content).digest("hex").slice(0, 16)}`;
21639
21742
  }
21743
+ var hashNodeContent = hashContent;
21744
+ async function writeNodeFile(relPath, content) {
21745
+ const fullPath = resolveInside(memoryDir2(), relPath);
21746
+ if (!fullPath) return false;
21747
+ if (!await ensureDirInRepo((0, import_node_path23.dirname)(fullPath))) return false;
21748
+ return writeInTree(fullPath, content);
21749
+ }
21750
+ async function writeContainedNodeFile(fullPath, content) {
21751
+ if (!await ensureDirInRepo((0, import_node_path23.dirname)(fullPath))) return false;
21752
+ return writeInTree(fullPath, content);
21753
+ }
21754
+ async function removeNodeFile(relPath) {
21755
+ const fullPath = resolveInside(memoryDir2(), relPath);
21756
+ if (!fullPath || !dirInRepo((0, import_node_path23.dirname)(fullPath))) return false;
21757
+ try {
21758
+ await (0, import_promises9.rm)(fullPath, { force: true });
21759
+ return true;
21760
+ } catch {
21761
+ return false;
21762
+ }
21763
+ }
21764
+ function dirInRepo(dir) {
21765
+ try {
21766
+ return isInsideRoot((0, import_node_fs26.realpathSync)(repoRoot()), (0, import_node_fs26.realpathSync)(dir));
21767
+ } catch {
21768
+ return false;
21769
+ }
21770
+ }
21771
+ function memoryDirEscapesRepo() {
21772
+ return (0, import_node_fs26.existsSync)(memoryDir2()) && !dirInRepo(memoryDir2());
21773
+ }
21774
+ async function ensureDirInRepo(dir) {
21775
+ let probe = dir;
21776
+ while (!(0, import_node_fs26.existsSync)(probe) && (0, import_node_path23.dirname)(probe) !== probe) probe = (0, import_node_path23.dirname)(probe);
21777
+ if (!dirInRepo(probe)) return false;
21778
+ await (0, import_promises9.mkdir)(dir, { recursive: true });
21779
+ return dirInRepo(dir);
21780
+ }
21781
+ async function writeFileAtomicInTree(path, content) {
21782
+ const dir = (0, import_node_path23.dirname)(path);
21783
+ if (!await ensureDirInRepo(dir)) return false;
21784
+ const real = (0, import_node_fs26.realpathSync)(dir);
21785
+ const tmp = `.${(0, import_node_path23.basename)(path)}.${process.pid}.${Date.now()}.tmp`;
21786
+ if (!createInRoot(real, tmp, content)) return false;
21787
+ try {
21788
+ await (0, import_promises9.rename)((0, import_node_path23.join)(real, tmp), (0, import_node_path23.join)(real, (0, import_node_path23.basename)(path)));
21789
+ return true;
21790
+ } catch {
21791
+ try {
21792
+ await (0, import_promises9.rm)((0, import_node_path23.join)(real, tmp), { force: true });
21793
+ } catch {
21794
+ }
21795
+ return false;
21796
+ }
21797
+ }
21798
+ async function markNodesSynced(entries) {
21799
+ await mergeSyncBaseline(new Map(entries));
21800
+ }
21801
+ async function forgetSyncedNodes(paths) {
21802
+ if (paths.length === 0 || !(0, import_node_fs26.existsSync)(syncStateFile())) return;
21803
+ try {
21804
+ try {
21805
+ JSON.parse(await (0, import_promises9.readFile)(syncStateFile(), "utf-8"));
21806
+ } catch {
21807
+ return;
21808
+ }
21809
+ const baseline = await readSyncBaseline();
21810
+ let changed = false;
21811
+ for (const p of paths) changed = baseline.delete(p) || changed;
21812
+ if (!changed) return;
21813
+ const nodes = [...baseline].map(([path, hash]) => ({ path, hash }));
21814
+ await writeFileAtomic(syncStateFile(), JSON.stringify({ schema: 2, nodes }) + "\n");
21815
+ } catch {
21816
+ }
21817
+ }
21818
+ async function refreshMemoryIndex() {
21819
+ await regenerateIndex();
21820
+ }
21821
+ async function appendMemoryLog(lines) {
21822
+ try {
21823
+ const logPath = (0, import_node_path23.join)(memoryDir2(), "log.md");
21824
+ const existing = (0, import_node_fs26.existsSync)(logPath) ? await (0, import_promises9.readFile)(logPath, "utf-8") : "# Memory Log\n\n";
21825
+ writeInTree(logPath, existing + lines.join("\n") + "\n");
21826
+ } catch {
21827
+ }
21828
+ }
21829
+ function uploadableNodeContent(content) {
21830
+ const m = content.match(/^(---\n[\s\S]*?\n---\n)([\s\S]*)$/);
21831
+ if (!m) return content;
21832
+ const lines = m[2].split("\n");
21833
+ let start2 = 0;
21834
+ while (start2 < lines.length && lines[start2].trim() === "") start2++;
21835
+ if (start2 < lines.length && /^# \S/.test(lines[start2])) start2++;
21836
+ let end = lines.length;
21837
+ const related = lines.lastIndexOf("## Related");
21838
+ if (related >= start2) {
21839
+ const tail = lines.slice(related + 1);
21840
+ if (tail.every((l) => l.trim() === "" || /^\*\*[^*]+:\*\*$/.test(l.trim()) || /^- \[\[[^\]]+\]\]$/.test(l.trim()))) {
21841
+ end = related;
21842
+ }
21843
+ }
21844
+ const body2 = lines.slice(start2, end).join("\n").trim();
21845
+ return `${storableFrontmatter(m[1])}
21846
+ ${body2}
21847
+ `;
21848
+ }
21849
+ function bareStorableValue(value) {
21850
+ if (value === "" || value !== value.trim() || /[\r\n]/.test(value)) return null;
21851
+ if (/^(null|true|false)$/.test(value) || /^\d+(\.\d+)?$/.test(value)) return null;
21852
+ if (value.startsWith('"') || value.startsWith("[") && value.endsWith("]")) return null;
21853
+ return value;
21854
+ }
21855
+ function storableFrontmatter(block) {
21856
+ const lines = block.split("\n");
21857
+ let inGlobs = false;
21858
+ for (let i2 = 0; i2 < lines.length; i2++) {
21859
+ const line2 = lines[i2];
21860
+ if (/^file_globs:\s*$/.test(line2)) {
21861
+ inGlobs = true;
21862
+ continue;
21863
+ }
21864
+ if (inGlobs && /^\s+-\s/.test(line2)) {
21865
+ const q = line2.match(/^(\s+-\s+)("(?:[^"\\]|\\.)*")\s*$/);
21866
+ if (q && q[2].includes("\\")) {
21867
+ const bare = unquote(q[2]);
21868
+ if (bare !== null && !bare.startsWith('"') && bare === bare.trim() && !/[\r\n]/.test(bare)) lines[i2] = `${q[1]}${bare}`;
21869
+ }
21870
+ continue;
21871
+ }
21872
+ inGlobs = false;
21873
+ const t2 = line2.match(/^title:\s*("(?:[^"\\]|\\.)*")\s*$/);
21874
+ if (t2 && t2[1].includes("\\")) {
21875
+ const bare = unquote(t2[1]);
21876
+ const storable = bare === null ? null : bareStorableValue(bare);
21877
+ if (storable !== null) lines[i2] = `title: ${storable}`;
21878
+ }
21879
+ }
21880
+ return lines.join("\n");
21881
+ }
21882
+ function unquote(quoted) {
21883
+ try {
21884
+ return String(JSON.parse(quoted));
21885
+ } catch {
21886
+ return null;
21887
+ }
21888
+ }
21640
21889
  async function readOnDiskNodes() {
21641
21890
  const out2 = /* @__PURE__ */ new Map();
21642
21891
  if (!(0, import_node_fs26.existsSync)(memoryDir2())) return out2;
@@ -21706,7 +21955,7 @@ async function computeEditedNodeUploads() {
21706
21955
  } catch {
21707
21956
  continue;
21708
21957
  }
21709
- if (hashContent(content) !== prevHash) uploads.push({ path, content });
21958
+ if (hashContent(content) !== prevHash) uploads.push({ path, content: uploadableNodeContent(content) });
21710
21959
  }
21711
21960
  return uploads;
21712
21961
  }
@@ -21792,10 +22041,14 @@ async function applyOneWrite(write, treePaths, lastServed) {
21792
22041
  if (existing !== "") {
21793
22042
  const served = lastServed?.get(write.path);
21794
22043
  if (served == null || served !== hashContent(existing)) {
21795
- notes.push(`${write.path}: kept local edit (differs from server)`);
21796
- return { written: false, syncedHash: null, notes };
22044
+ if (nodeSubstance(existing) !== nodeSubstance(content)) {
22045
+ notes.push(`${write.path}: kept local edit (differs from server)`);
22046
+ return { written: false, syncedHash: null, notes };
22047
+ }
22048
+ notes.push(`${write.path}: updated from server (the local edit is already on the server)`);
22049
+ } else {
22050
+ notes.push(`${write.path}: updated from server (untouched here since the server delivered it)`);
21797
22051
  }
21798
- notes.push(`${write.path}: updated from server (untouched here since the server delivered it)`);
21799
22052
  }
21800
22053
  }
21801
22054
  await (0, import_promises9.mkdir)((0, import_node_path23.dirname)(fullPath), { recursive: true });
@@ -21805,6 +22058,15 @@ async function applyOneWrite(write, treePaths, lastServed) {
21805
22058
  }
21806
22059
  return { written: true, syncedHash: hashContent(content), notes };
21807
22060
  }
22061
+ var SERVER_OWNED_LINE = /^(schema|created_at|updated_at|session|task|run|related|supersedes|superseded_by|contradicts|caused_by|example_of|evidence_run_ids|pattern_ref|source|created_by|confidence):/;
22062
+ function nodeSubstance(content) {
22063
+ const m = uploadableNodeContent(content).match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
22064
+ if (!m) return content;
22065
+ const fm = m[1].split("\n").filter((l) => !SERVER_OWNED_LINE.test(l)).join("\n");
22066
+ return `${fm}
22067
+ ---
22068
+ ${m[2].replace(/\s+$/, "")}`;
22069
+ }
21808
22070
  function groundFileGlobs(content, treePaths) {
21809
22071
  const globs = pickFrontmatterArray(content, "file_globs");
21810
22072
  if (globs.length === 0) return { content, dropped: [] };
@@ -24339,7 +24601,6 @@ var import_node_crypto11 = require("node:crypto");
24339
24601
  // src/lib/snapshot.ts
24340
24602
  var import_node_fs32 = require("node:fs");
24341
24603
  var import_node_path27 = require("node:path");
24342
- var import_node_child_process10 = require("node:child_process");
24343
24604
  function generateSnapshotDiffs(files) {
24344
24605
  if (!(0, import_node_fs32.existsSync)(SNAPSHOT_DIR)) {
24345
24606
  return { diffs: [], has_snapshots: false };
@@ -24389,8 +24650,9 @@ function computeDiff(oldContent, newContent, filePath) {
24389
24650
  (0, import_node_fs32.mkdirSync)(SNAPSHOT_DIR, { recursive: true });
24390
24651
  (0, import_node_fs32.writeFileSync)(tmpOld, oldContent);
24391
24652
  (0, import_node_fs32.writeFileSync)(tmpNew, newContent);
24392
- const result = (0, import_node_child_process10.execSync)(
24393
- `git diff --no-index --unified=10 -- "${tmpOld}" "${tmpNew}"`,
24653
+ const result = execFileSync2(
24654
+ "git",
24655
+ ["diff", "--no-index", "--unified=10", "--", tmpOld, tmpNew],
24394
24656
  { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
24395
24657
  );
24396
24658
  return result || null;
@@ -25142,7 +25404,7 @@ var EXCLUDED_DIR = /(^|\/)(node_modules|dist|build|bin|out|vendor|coverage|\.ver
25142
25404
  var TEST_PATH = /(^|\/)(tests?|__tests__|spec|specs|fixtures?|e2e|testdata)\/|\.(test|spec|stories)\.[cm]?[jt]sx?$|_test\.(go|py|rb)$|test_[^/]*\.py$/;
25143
25405
  var GENERATED2 = /\.d\.ts$|\.min\.[jt]s$|\.generated\.|\.pb\.|lock\.json$|\.lock$|(^|\/)(integrations\/supabase\/types|database\.types|supabase\.types)\.ts$|(^|\/)__generated__\//;
25144
25406
  function isCandidateSource(file) {
25145
- return SOURCE_EXT.test(file) && !EXCLUDED_DIR.test(file) && !TEST_PATH.test(file) && !GENERATED2.test(file);
25407
+ return SOURCE_EXT.test(file) && !EXCLUDED_DIR.test(file) && !TEST_PATH.test(file) && !GENERATED2.test(file) && !isSecretShapedPath(file);
25146
25408
  }
25147
25409
  function collectHistory(root, opts = {}) {
25148
25410
  const args2 = [
@@ -27947,7 +28209,6 @@ var import_node_fs58 = require("node:fs");
27947
28209
  var import_node_path46 = require("node:path");
27948
28210
 
27949
28211
  // src/lib/repo-context.ts
27950
- var import_node_child_process11 = require("node:child_process");
27951
28212
  var import_node_os5 = require("node:os");
27952
28213
  function rgInvocations(env = process.env) {
27953
28214
  const out2 = [{ cmd: "rg" }];
@@ -28507,7 +28768,7 @@ function partitionImporters(rgLines, changedPaths, opts) {
28507
28768
  function runRg(args2, cwd, timeoutMs) {
28508
28769
  let res = null;
28509
28770
  for (const inv of rgInvocations()) {
28510
- res = (0, import_node_child_process11.spawnSync)(inv.cmd, args2, {
28771
+ res = spawnSync2(inv.cmd, args2, {
28511
28772
  ...inv.argv0 ? { argv0: inv.argv0 } : {},
28512
28773
  cwd,
28513
28774
  env: ripgrepEnv(),
@@ -28974,7 +29235,7 @@ function findMomentSegment(command, on) {
28974
29235
  function classifyCommand(command, on) {
28975
29236
  return findMomentSegment(command, on)?.moment ?? null;
28976
29237
  }
28977
- function unquote(token) {
29238
+ function unquote2(token) {
28978
29239
  if (token.length >= 2) {
28979
29240
  const first = token[0];
28980
29241
  if ((first === "'" || first === '"') && token.endsWith(first)) return token.slice(1, -1);
@@ -28994,7 +29255,7 @@ function extractCommandTarget(command, segmentIndex, baseDir) {
28994
29255
  dir = (0, import_node_os6.homedir)();
28995
29256
  continue;
28996
29257
  }
28997
- const raw = unquote(m[1]);
29258
+ const raw = unquote2(m[1]);
28998
29259
  if (SHELL_DYNAMIC.test(raw) || raw === "-") {
28999
29260
  return { dir: null, named: true, unresolvable: `cd target not statically resolvable: ${raw}` };
29000
29261
  }
@@ -29013,7 +29274,7 @@ function extractCommandTarget(command, segmentIndex, baseDir) {
29013
29274
  let cm;
29014
29275
  while ((cm = cRe.exec(optsRegion)) !== null) {
29015
29276
  named = true;
29016
- const raw = unquote(cm[1]);
29277
+ const raw = unquote2(cm[1]);
29017
29278
  if (SHELL_DYNAMIC.test(raw)) {
29018
29279
  return { dir: null, named: true, unresolvable: `-C target not statically resolvable: ${raw}` };
29019
29280
  }
@@ -29029,7 +29290,7 @@ function parsePushTarget(segment) {
29029
29290
  const m = PUSH_RE.exec(segment);
29030
29291
  if (!m) return none;
29031
29292
  const rest = segment.slice(m.index + m[0].length);
29032
- const tokens = (rest.match(new RegExp(`'[^']*'|"[^"]*"|\\S+`, "g")) ?? []).map(unquote);
29293
+ const tokens = (rest.match(new RegExp(`'[^']*'|"[^"]*"|\\S+`, "g")) ?? []).map(unquote2);
29033
29294
  let remote = null;
29034
29295
  let refspec = null;
29035
29296
  let isDelete = false;
@@ -29189,7 +29450,7 @@ var COMMIT_VALUE_OPTS = /* @__PURE__ */ new Set([
29189
29450
  var ADD_HEAD = `git${GIT_GLOBAL_OPTS}\\s+add(?![\\w-])`;
29190
29451
  var ADD_RE = new RegExp(`(?:^|[\\s;&|(])${ADD_HEAD}|(?:^|[;&|(])\\s*[^\\s;&|()'"]*\\/${ADD_HEAD}`);
29191
29452
  function pathspecOf(word) {
29192
- const raw = unquote(word);
29453
+ const raw = unquote2(word);
29193
29454
  if (!raw) return { reason: "an empty pathspec" };
29194
29455
  if (PATHSPEC_UNRESOLVABLE.test(raw)) {
29195
29456
  return { reason: `the shell rewrites the pathspec "${raw}" before git sees it` };
@@ -29306,12 +29567,57 @@ function stagedRange(frame, command) {
29306
29567
  if (wtVsHead.has(f) && wtVsMerge.has(f) && !isVerityOwnedPath(f)) resolutions.add(f);
29307
29568
  }
29308
29569
  }
29309
- return { kind: "merge", base: "HEAD", head: "INDEX", via: "merge-resolutions", files: [...resolutions] };
29570
+ const parents = [objectId(frame, "HEAD"), objectId(frame, "MERGE_HEAD")];
29571
+ const bases2 = parents.every((p) => p !== null) ? parents : void 0;
29572
+ return { kind: "merge", base: "HEAD", head: "INDEX", via: "merge-resolutions", files: [...resolutions], ...bases2 ? { bases: bases2 } : {} };
29310
29573
  }
29574
+ const bases = stagedBases(frame, command);
29311
29575
  if (plan.kind === "files") {
29312
- return { kind: "staged", base: "HEAD", head: "INDEX", via: plan.via, files: plan.files };
29576
+ return { kind: "staged", base: "HEAD", head: "INDEX", via: plan.via, files: plan.files, ...bases ? { bases } : {} };
29313
29577
  }
29314
- return { kind: "staged", base: "HEAD", head: "INDEX", via: "index" };
29578
+ return { kind: "staged", base: "HEAD", head: "INDEX", via: "index", ...bases ? { bases } : {} };
29579
+ }
29580
+ var OBJECT_ID_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
29581
+ function objectId(frame, ref) {
29582
+ const id = frameGit(frame, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]);
29583
+ return OBJECT_ID_RE.test(id) ? id : null;
29584
+ }
29585
+ function stagedBases(frame, command) {
29586
+ const head = objectId(frame, "HEAD");
29587
+ if (head === null) return void 0;
29588
+ if (!command || !commitAmends(command)) return [head];
29589
+ const parents = frameGit(frame, ["rev-list", "--parents", "-n", "1", head]).split(" ").slice(1);
29590
+ return parents.length > 0 && parents.every((p) => OBJECT_ID_RE.test(p)) ? parents : void 0;
29591
+ }
29592
+ function commitAmends(command) {
29593
+ const found = findMomentSegment(command, ["commit"]);
29594
+ if (!found) return false;
29595
+ const words = shellWords(splitSegments(command)[found.segmentIndex]);
29596
+ const at = words.indexOf("commit");
29597
+ if (at === -1) return false;
29598
+ let skipValue = false;
29599
+ for (const word of words.slice(at + 1)) {
29600
+ if (skipValue) {
29601
+ skipValue = false;
29602
+ continue;
29603
+ }
29604
+ if (word === "--") break;
29605
+ if (!word.startsWith("--")) {
29606
+ if (word.startsWith("-") && word.length > 1) {
29607
+ for (let i2 = 1; i2 < word.length; i2++) {
29608
+ if (COMMIT_VALUE_OPTS.has(`-${word[i2]}`)) {
29609
+ if (i2 === word.length - 1) skipValue = true;
29610
+ break;
29611
+ }
29612
+ }
29613
+ }
29614
+ continue;
29615
+ }
29616
+ const name2 = word.split("=")[0];
29617
+ if (name2.length >= 4 && "--amend".startsWith(name2)) return true;
29618
+ if (COMMIT_VALUE_OPTS.has(name2) && !word.includes("=")) skipValue = true;
29619
+ }
29620
+ return false;
29315
29621
  }
29316
29622
  function resolvePushRange(frame, command, on) {
29317
29623
  const nothing = (via2) => ({ kind: "nothing", base: null, head: "HEAD", via: via2 });
@@ -29328,13 +29634,15 @@ function resolvePushRange(frame, command, on) {
29328
29634
  if (commits.length === 0) return { kind: "nothing", base: null, head, via: "already-published" };
29329
29635
  const boundary = frameGit(frame, ["rev-list", head, "--not", remotePattern, "--boundary"]).split("\n").filter((l) => l.startsWith("-")).map((l) => l.slice(1));
29330
29636
  const base = boundary.find((s) => SHA_RE2.test(s)) ?? null;
29637
+ const published = boundary.filter((s) => OBJECT_ID_RE.test(s));
29638
+ const bases = published.length > 0 && published.length <= MAX_CHANGE_BASES ? published : void 0;
29331
29639
  const files = /* @__PURE__ */ new Set();
29332
29640
  for (const sha of commits) {
29333
29641
  for (const f of frameGit(frame, ["diff-tree", "--no-commit-id", "--name-only", "-r", sha]).split("\n")) {
29334
29642
  if (f && !isVerityOwnedPath(f)) files.add(f);
29335
29643
  }
29336
29644
  }
29337
- return { kind: "push", base, head, via, files: [...files], commits };
29645
+ return { kind: "push", base, head, via, files: [...files], commits, ...bases ? { bases } : {} };
29338
29646
  }
29339
29647
  function rangeFiles(frame, range) {
29340
29648
  if (range.files) return range.files.filter((f) => !isVerityOwnedPath(f));
@@ -29378,6 +29686,226 @@ function rangeChangeSignals(frame, range, paths) {
29378
29686
  flush();
29379
29687
  return out2;
29380
29688
  }
29689
+ var MAX_CHANGE_BASES = 8;
29690
+ var CHANGED_LINES_BUDGET_MS = 1500;
29691
+ var MAX_RANGES_PER_FILE = 2e3;
29692
+ var HUNK_RE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
29693
+ function withoutCr(line2) {
29694
+ return line2 !== void 0 && line2.endsWith("\r") ? line2.slice(0, -1) : line2;
29695
+ }
29696
+ function bodyLines(lines) {
29697
+ return lines.length > 0 && lines[lines.length - 1] === "" ? lines.slice(0, -1) : lines;
29698
+ }
29699
+ function parseChangedHunks(diff, sent, base) {
29700
+ const lines = /* @__PURE__ */ new Set();
29701
+ const removedAfter = /* @__PURE__ */ new Set();
29702
+ const text3 = diff.split("\n");
29703
+ const rebuilt = [];
29704
+ let oldPos = 0;
29705
+ let hunks = 0;
29706
+ for (let i2 = 0; i2 < text3.length; i2++) {
29707
+ if (text3[i2].startsWith("Binary files ")) return { reason: "binary" };
29708
+ const h = HUNK_RE.exec(text3[i2]);
29709
+ if (!h) continue;
29710
+ hunks++;
29711
+ const oldStart = Number(h[1]);
29712
+ const oldCount = h[2] === void 0 ? 1 : Number(h[2]);
29713
+ const newStart = Number(h[3]);
29714
+ const newCount = h[4] === void 0 ? 1 : Number(h[4]);
29715
+ if (base) {
29716
+ const copyUntil = oldCount === 0 ? oldStart : oldStart - 1;
29717
+ if (copyUntil < oldPos || copyUntil > base.length) return { reason: "snapshot-differs" };
29718
+ for (; oldPos < copyUntil; oldPos++) rebuilt.push(base[oldPos]);
29719
+ }
29720
+ let seenOld = 0;
29721
+ let seenNew = 0;
29722
+ while (seenOld < oldCount || seenNew < newCount) {
29723
+ i2++;
29724
+ if (i2 >= text3.length) return { reason: "diff-truncated" };
29725
+ const l = text3[i2];
29726
+ if (l.startsWith("\\")) continue;
29727
+ if (l.startsWith("-") && seenOld < oldCount) {
29728
+ if (base) {
29729
+ if (oldPos >= base.length || withoutCr(base[oldPos]) !== withoutCr(l.slice(1))) return { reason: "snapshot-differs" };
29730
+ oldPos++;
29731
+ }
29732
+ seenOld++;
29733
+ continue;
29734
+ }
29735
+ if (l.startsWith("+") && seenNew < newCount) {
29736
+ const at = newStart + seenNew;
29737
+ if (withoutCr(sent[at - 1]) !== withoutCr(l.slice(1))) return { reason: "bytes-differ" };
29738
+ if (base) {
29739
+ rebuilt.push(l.slice(1));
29740
+ if (rebuilt.length !== at) return { reason: "snapshot-differs" };
29741
+ }
29742
+ lines.add(at);
29743
+ seenNew++;
29744
+ continue;
29745
+ }
29746
+ return { reason: "diff-unparsed" };
29747
+ }
29748
+ if (newCount === 0) {
29749
+ if (base && rebuilt.length !== newStart) return { reason: "snapshot-differs" };
29750
+ removedAfter.add(newStart);
29751
+ }
29752
+ }
29753
+ if (hunks === 0) return { reason: "no-hunks" };
29754
+ if (base) {
29755
+ for (; oldPos < base.length; oldPos++) rebuilt.push(base[oldPos]);
29756
+ const want = bodyLines(sent);
29757
+ const got = bodyLines(rebuilt);
29758
+ if (got.length !== want.length || got.some((l, k) => withoutCr(l) !== withoutCr(want[k]))) {
29759
+ return { reason: "snapshot-differs" };
29760
+ }
29761
+ }
29762
+ return { lines, removedAfter };
29763
+ }
29764
+ function intersect(a, b) {
29765
+ return new Set([...a].filter((x) => b.has(x)));
29766
+ }
29767
+ function toRanges(lines) {
29768
+ const sorted = [...lines].sort((x, y) => x - y);
29769
+ const out2 = [];
29770
+ for (const n2 of sorted) {
29771
+ const last = out2[out2.length - 1];
29772
+ if (last && n2 === last[1] + 1) last[1] = n2;
29773
+ else out2.push([n2, n2]);
29774
+ }
29775
+ return out2;
29776
+ }
29777
+ function rangeChangedLines(frame, range, files, budgetMs = CHANGED_LINES_BUDGET_MS) {
29778
+ const out2 = { files: [], omitted: {} };
29779
+ const omit = (why) => {
29780
+ out2.omitted[why] = (out2.omitted[why] ?? 0) + 1;
29781
+ };
29782
+ const bases = range.bases ?? [];
29783
+ if (!frame.worktreeRoot || range.kind === "nothing" || bases.length === 0) {
29784
+ for (const _ of files) omit("no-base");
29785
+ return out2;
29786
+ }
29787
+ const deadline = Date.now() + budgetMs;
29788
+ const blobAt = (rev, path) => {
29789
+ const r = runGitSync(frame.worktreeRoot, ["cat-file", "blob", `${rev}:${path}`], {
29790
+ timeoutMs: Math.max(1, deadline - Date.now())
29791
+ });
29792
+ if (!r.ok && r.error === "timeout") return "deadline";
29793
+ return { text: r.ok ? r.stdout : null };
29794
+ };
29795
+ const merges = [];
29796
+ {
29797
+ const parentSets = [];
29798
+ if (range.kind === "push") {
29799
+ const commits = range.commits ?? [];
29800
+ let readAll = commits.length > 0 && commits.length <= 100;
29801
+ if (readAll) {
29802
+ const r = runGitSync(frame.worktreeRoot, ["show", "-s", "--format=%P", ...commits], {
29803
+ timeoutMs: Math.max(1, deadline - Date.now())
29804
+ });
29805
+ if (r.ok) {
29806
+ for (const line2 of r.stdout.split("\n")) {
29807
+ const ps = line2.trim().split(/\s+/).filter(Boolean);
29808
+ if (ps.length >= 2) parentSets.push(ps);
29809
+ }
29810
+ } else readAll = false;
29811
+ }
29812
+ if (!readAll && bases.length > 1) parentSets.push(bases);
29813
+ } else if (bases.length > 1) {
29814
+ parentSets.push(bases);
29815
+ }
29816
+ for (const parents of parentSets) {
29817
+ const mb = runGitSync(frame.worktreeRoot, ["merge-base", "--octopus", ...parents], {
29818
+ timeoutMs: Math.max(1, deadline - Date.now())
29819
+ });
29820
+ merges.push({ parents, split: mb.ok ? mb.stdout.trim().split("\n")[0] || null : null });
29821
+ }
29822
+ }
29823
+ const mergedByAMerge = (path) => {
29824
+ for (const m of merges) {
29825
+ const split = m.split === null ? void 0 : blobAt(m.split, path);
29826
+ if (split === "deadline") return "deadline";
29827
+ let first;
29828
+ let changed = 0;
29829
+ for (const parent of m.parents) {
29830
+ const b = blobAt(parent, path);
29831
+ if (b === "deadline") return "deadline";
29832
+ if (split === void 0) {
29833
+ if (first === void 0) first = b.text;
29834
+ else if (b.text !== first) return true;
29835
+ } else if (b.text !== split.text && ++changed >= 2) {
29836
+ return true;
29837
+ }
29838
+ }
29839
+ }
29840
+ return false;
29841
+ };
29842
+ for (const file of files) {
29843
+ if (typeof file.content !== "string") {
29844
+ omit("no-content");
29845
+ continue;
29846
+ }
29847
+ const sent = file.content.split("\n");
29848
+ let lines = null;
29849
+ let removedAfter = null;
29850
+ let why = null;
29851
+ const merged = mergedByAMerge(file.path);
29852
+ if (merged !== false) {
29853
+ omit(merged === "deadline" ? "deadline" : "merged-file");
29854
+ continue;
29855
+ }
29856
+ for (const base of bases) {
29857
+ const left = deadline - Date.now();
29858
+ if (left <= 0) {
29859
+ why = "deadline";
29860
+ break;
29861
+ }
29862
+ const res = runGitSync(frame.worktreeRoot, [
29863
+ "--literal-pathspecs",
29864
+ "diff",
29865
+ ...DIFF_READ_FLAGS,
29866
+ "--no-color",
29867
+ "--no-renames",
29868
+ "-U0",
29869
+ "--src-prefix=a/",
29870
+ "--dst-prefix=b/",
29871
+ base,
29872
+ "--",
29873
+ file.path
29874
+ ], { timeoutMs: left });
29875
+ if (!res.ok) {
29876
+ why = res.error === "timeout" ? "deadline" : "git-failed";
29877
+ break;
29878
+ }
29879
+ const blob = blobAt(base, file.path);
29880
+ if (blob === "deadline") {
29881
+ why = "deadline";
29882
+ break;
29883
+ }
29884
+ const parsed = parseChangedHunks(res.stdout, sent, blob.text === null ? [] : blob.text.split("\n"));
29885
+ if ("reason" in parsed) {
29886
+ why = parsed.reason;
29887
+ break;
29888
+ }
29889
+ lines = lines ? intersect(lines, parsed.lines) : parsed.lines;
29890
+ removedAfter = removedAfter ? intersect(removedAfter, parsed.removedAfter) : parsed.removedAfter;
29891
+ }
29892
+ if (why !== null) {
29893
+ omit(why);
29894
+ continue;
29895
+ }
29896
+ if (!lines || !removedAfter || lines.size === 0 && removedAfter.size === 0) {
29897
+ omit("no-change");
29898
+ continue;
29899
+ }
29900
+ const ranges = toRanges(lines);
29901
+ if (ranges.length + removedAfter.size > MAX_RANGES_PER_FILE) {
29902
+ omit("too-many-ranges");
29903
+ continue;
29904
+ }
29905
+ out2.files.push({ path: file.path, ranges, removed_after: [...removedAfter].sort((x, y) => x - y) });
29906
+ }
29907
+ return out2;
29908
+ }
29381
29909
  function rangeMessages(frame, range) {
29382
29910
  if (range.kind !== "push" || !range.commits || range.commits.length === 0) return "";
29383
29911
  const commits = range.commits.slice(0, 100);
@@ -30120,7 +30648,6 @@ async function sendSkipBeacon(ctx, reason) {
30120
30648
  }
30121
30649
 
30122
30650
  // src/lib/static-analysis.ts
30123
- var import_node_child_process12 = require("node:child_process");
30124
30651
  var import_node_module = require("node:module");
30125
30652
  var import_node_path37 = require("node:path");
30126
30653
  var import_node_fs47 = require("node:fs");
@@ -30130,9 +30657,9 @@ var import_node_fs46 = require("node:fs");
30130
30657
  var import_node_path36 = require("node:path");
30131
30658
  var TRIVY_PATTERNS = [
30132
30659
  "Trivy_secret",
30133
- "Trivy_malicious_packages",
30134
30660
  "Trivy_vulnerability_critical",
30135
- "Trivy_vulnerability_high"
30661
+ "Trivy_vulnerability_high",
30662
+ "Trivy_malicious_packages"
30136
30663
  ];
30137
30664
  var TRIVY_TOOL = "trivy";
30138
30665
  function trivyConfig() {
@@ -30171,6 +30698,14 @@ var SEVERITY_ORDER = {
30171
30698
  function isCodacyAvailable() {
30172
30699
  return resolveAnalyzer() !== null;
30173
30700
  }
30701
+ var ANALYZER_PRIVACY_ENV = {
30702
+ TRIVY_DISABLE_TELEMETRY: "true",
30703
+ TRIVY_OFFLINE_SCAN: "true",
30704
+ CODACY_DISABLE_UPDATE_CHECK: "1"
30705
+ };
30706
+ function analyzerEnv(env = process.env) {
30707
+ return { ...env, ...ANALYZER_PRIVACY_ENV };
30708
+ }
30174
30709
  var NO_BUNDLED_ANALYZER_ENV = "VERITY_NO_BUNDLED_ANALYZER";
30175
30710
  function bundledAnalyzer(from = __filename) {
30176
30711
  try {
@@ -30199,7 +30734,7 @@ function analyzerCommand(analyzer, argv, execPath = process.execPath) {
30199
30734
  return { command: analyzer.path, args: argv };
30200
30735
  }
30201
30736
  function prewarmFile(root = repoRoot()) {
30202
- const listed = (0, import_node_child_process12.spawnSync)("git", ["ls-files", "-z"], { cwd: root, encoding: "utf-8", maxBuffer: 64 * 1024 * 1024 });
30737
+ const listed = spawnSync2("git", ["ls-files", "-z"], { cwd: root, encoding: "utf-8", maxBuffer: 64 * 1024 * 1024 });
30203
30738
  if (listed.status !== 0 || typeof listed.stdout !== "string") return null;
30204
30739
  for (const file of listed.stdout.split("\0")) {
30205
30740
  if (!file) continue;
@@ -30232,10 +30767,11 @@ function startTrivyPrewarm(opts = {}) {
30232
30767
  const file = prewarmFile();
30233
30768
  if (!file) return false;
30234
30769
  const { command, args: args2 } = analyzerCommand(analyzer, buildAnalyzerArgv([file], config));
30235
- const child = (opts.run ?? import_node_child_process12.spawn)(command, args2, {
30770
+ const child = (opts.run ?? spawn2)(command, args2, {
30236
30771
  cwd: repoRoot(),
30237
30772
  detached: true,
30238
- stdio: "ignore"
30773
+ stdio: "ignore",
30774
+ env: analyzerEnv(opts.env ?? process.env)
30239
30775
  });
30240
30776
  child.on("error", () => {
30241
30777
  });
@@ -30312,10 +30848,11 @@ function runCodacyAnalysis(files, opts = {}) {
30312
30848
  const resolved = resolveAnalyzer();
30313
30849
  const { command, args: args2 } = resolved ? analyzerCommand(resolved, argv) : { command: "codacy-analysis", args: argv };
30314
30850
  const timeoutMs = opts.timeoutMs ?? ANALYZER_TIMEOUT_MS;
30315
- const proc = (0, import_node_child_process12.spawnSync)(command, args2, {
30851
+ const proc = spawnSync2(command, args2, {
30316
30852
  encoding: "utf-8",
30317
30853
  maxBuffer: 10 * 1024 * 1024,
30318
- timeout: timeoutMs
30854
+ timeout: timeoutMs,
30855
+ env: analyzerEnv()
30319
30856
  });
30320
30857
  if (proc.error?.code === "ETIMEDOUT") {
30321
30858
  const background = (opts.prewarm ?? startTrivyPrewarm)();
@@ -30682,6 +31219,7 @@ function discoverSpecs(consulted = []) {
30682
31219
  const totalCap = relevant ? MAX_TOTAL_SPEC_BYTES : UNCONSULTED_TOTAL_BYTES;
30683
31220
  if (totalBytes >= totalCap) return false;
30684
31221
  if (seen.has(specPath)) return true;
31222
+ if (isSecretShapedPath(specPath)) return true;
30685
31223
  if (!(0, import_node_fs48.existsSync)(specPath)) return true;
30686
31224
  seen.add(specPath);
30687
31225
  const remaining = totalCap - totalBytes;
@@ -30751,7 +31289,7 @@ function discoverPlans() {
30751
31289
  continue;
30752
31290
  }
30753
31291
  for (const f of names2) {
30754
- if (!f.endsWith(".md") || seen.has(f)) continue;
31292
+ if (!f.endsWith(".md") || seen.has(f) || isSecretShapedPath(f)) continue;
30755
31293
  seen.add(f);
30756
31294
  const relPath = source2.prefix ? (0, import_node_path38.join)(source2.prefix, f) : f;
30757
31295
  const stat3 = statRegularInRoot(source2.root, relPath);
@@ -30787,6 +31325,7 @@ function discoverGuardDocs(rangeFiles2) {
30787
31325
  for (const path of rangeFiles2) {
30788
31326
  if (result.length >= MAX_SPEC_FILES) break;
30789
31327
  if (!GUARD_DOC_EXT.test(path)) continue;
31328
+ if (isSecretShapedPath(path)) continue;
30790
31329
  if (path.startsWith("/") || path.includes("..")) continue;
30791
31330
  if (!(0, import_node_fs48.existsSync)(path)) continue;
30792
31331
  const opened = openRegularInRoot(process.cwd(), path);
@@ -31566,7 +32105,7 @@ async function evidence(run4) {
31566
32105
  run4,
31567
32106
  "No files within size limits to analyze",
31568
32107
  "size-limit",
31569
- codeDelta.excluded.length > 0 ? "capacity" : "policy"
32108
+ codeDelta.excluded.some((e) => e.kind === "capacity") ? "capacity" : "policy"
31570
32109
  );
31571
32110
  }
31572
32111
  }
@@ -31671,6 +32210,10 @@ function gatherContextFiles(contextPaths, deltaFiles, opts) {
31671
32210
  logEvent("context_file_skipped", { path: filePath, reason: "verity_owned" });
31672
32211
  continue;
31673
32212
  }
32213
+ if (isSecretShapedPath(filePath)) {
32214
+ logEvent("context_file_skipped", { path: filePath, reason: "secret_shaped" });
32215
+ continue;
32216
+ }
31674
32217
  const safePath2 = resolveInside(process.cwd(), filePath);
31675
32218
  if (!safePath2) {
31676
32219
  logEvent("context_file_skipped", { path: filePath, reason: "outside_repo" });
@@ -31994,7 +32537,7 @@ function definitionsIn(root, spec) {
31994
32537
  } else if (p.type === "arguments" && moduleCall(p.parent)) {
31995
32538
  const call = p.parent;
31996
32539
  const route = p.namedChildren[0];
31997
- const quoted = route && route.id !== n2.id && route.type === "string" ? unquote2(route.text) : null;
32540
+ const quoted = route && route.id !== n2.id && route.type === "string" ? unquote3(route.text) : null;
31998
32541
  const path = quoted && /^[\w/:.{}*-]{1,60}$/.test(quoted) ? quoted : null;
31999
32542
  const callee = call.childForFieldName("function").text;
32000
32543
  add(path ? `${callee} ${path}` : callee, kind3, call.parent, n2, call);
@@ -32009,7 +32552,7 @@ function moduleCall(call) {
32009
32552
  const f = call?.type === "call_expression" ? call.childForFieldName("function") : null;
32010
32553
  return !!f && (f.type === "identifier" || f.type === "member_expression") && f.text.length <= 40 && !f.text.includes("\n") && call.parent?.type === "expression_statement" && call.parent.parent?.type === "program";
32011
32554
  }
32012
- function unquote2(s) {
32555
+ function unquote3(s) {
32013
32556
  const body2 = s.replace(/^[rRbBuUfF]{0,2}(?=["'`])/, "");
32014
32557
  const q3 = body2.startsWith('"""') || body2.startsWith("'''");
32015
32558
  return q3 ? body2.slice(3, -3) : body2.slice(1, -1);
@@ -32032,8 +32575,8 @@ function argsOf(args2, spec) {
32032
32575
  return list2.slice(0, MAX_ARGS).map((a) => {
32033
32576
  let literal = null;
32034
32577
  const left = a.type === "binary_expression" ? a.childForFieldName("left") : null;
32035
- if (spec.strings.has(a.type)) literal = unquote2(a.text);
32036
- else if (left && spec.strings.has(left.type)) literal = `${unquote2(left.text)}${PLACEHOLDER}`;
32578
+ if (spec.strings.has(a.type)) literal = unquote3(a.text);
32579
+ else if (left && spec.strings.has(left.type)) literal = `${unquote3(left.text)}${PLACEHOLDER}`;
32037
32580
  return { literal: literal === null ? null : cap(literal, LITERAL_CAP), text: cap(a.text, TEXT_CAP) };
32038
32581
  });
32039
32582
  }
@@ -32648,7 +33191,7 @@ function accessMap(input) {
32648
33191
  }
32649
33192
  };
32650
33193
  consider(fileFns, path, true, state);
32651
- const terms = [];
33194
+ const terms2 = [];
32652
33195
  let files = 0;
32653
33196
  let truncated = false;
32654
33197
  if (input.source) {
@@ -32659,7 +33202,7 @@ function accessMap(input) {
32659
33202
  truncated = true;
32660
33203
  break;
32661
33204
  }
32662
- terms.push(term);
33205
+ terms2.push(term);
32663
33206
  const found = input.source.search(term, { maxHits: 200, timeoutMs: Math.max(100, Math.min(800, input.deadlineAt - Date.now())), sourceOnly: true });
32664
33207
  if (!found.ok) {
32665
33208
  truncated = true;
@@ -32706,7 +33249,7 @@ function accessMap(input) {
32706
33249
  swallowedWrites: [...new Set(effects.filter((e) => e.op === "write" && e.caught === "swallow").map((e) => e.line))]
32707
33250
  },
32708
33251
  unknownResources: effects.filter((e) => e.resource.name === null || e.resource.confidence === "unknown").length,
32709
- searched: { terms, files, truncated }
33252
+ searched: { terms: terms2, files, truncated }
32710
33253
  };
32711
33254
  }
32712
33255
  function renderEffect(e) {
@@ -33203,9 +33746,9 @@ function tsCandidates(spec, fromPath, cfg) {
33203
33746
  }
33204
33747
  const out2 = [];
33205
33748
  for (const b of bases) {
33206
- const stem = b.replace(/\.(?:js|jsx|mjs|cjs)$/, "");
33749
+ const stem2 = b.replace(/\.(?:js|jsx|mjs|cjs)$/, "");
33207
33750
  if (languageOf2(b)) out2.push(b);
33208
- for (const e of TS_EXTS) out2.push(`${stem}${e}`);
33751
+ for (const e of TS_EXTS) out2.push(`${stem2}${e}`);
33209
33752
  for (const e of TS_EXTS) out2.push(`${b}/index${e}`);
33210
33753
  }
33211
33754
  return [...new Set(out2)];
@@ -33802,10 +34345,10 @@ var writersOfTool = async (ctx, args2) => {
33802
34345
  if (name2.length < 2) return { ok: false, error: "resource name too short to search for" };
33803
34346
  const deadline = Date.now() + TOOL_MS;
33804
34347
  const src = source(ctx);
33805
- const terms = [.../* @__PURE__ */ new Set([name2, ...searchTerms({ kind: "table", name: name2, confidence: "inferred" })])];
34348
+ const terms2 = [.../* @__PURE__ */ new Set([name2, ...searchTerms({ kind: "table", name: name2, confidence: "inferred" })])];
33806
34349
  const files = /* @__PURE__ */ new Set();
33807
34350
  let truncated = false;
33808
- for (const t2 of terms) {
34351
+ for (const t2 of terms2) {
33809
34352
  const found = src.search(t2, { maxHits: 300, timeoutMs: Math.max(100, deadline - Date.now()) });
33810
34353
  if (!found.ok) {
33811
34354
  truncated = true;
@@ -33867,17 +34410,38 @@ var TOOL_LIMITS = {
33867
34410
  diffTimeoutMs: 1e4,
33868
34411
  concurrency: 4
33869
34412
  };
33870
- var DENIED_PATHS = [
33871
- /(^|\/)\.env(\.[^/]*)?$/i,
33872
- /\.(pem|key|p12|pfx|jks|keystore|asc|gpg|kdbx)$/i,
33873
- /(^|\/)id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/i,
33874
- /(^|\/)\.(npmrc|netrc|pypirc|pgpass|git-credentials)$/i,
33875
- /(^|\/)(credentials|secrets?)(\.[^/]*)?$/i,
33876
- /(^|\/)\.(ssh|aws|gnupg|docker)(\/|$)/i,
33877
- /(^|\/)\.git(\/|$)/
33878
- ];
33879
34413
  function isDeniedPath(path) {
33880
- return DENIED_PATHS.some((pattern) => pattern.test(path)) || isVerityOwnedPath(path);
34414
+ return isSecretShapedPath(path) || isVerityOwnedPath(path);
34415
+ }
34416
+ function diffChunkPaths(chunk) {
34417
+ const out2 = /* @__PURE__ */ new Set();
34418
+ const header = /^diff --git a\/(.+?) b\/(.+)$/m.exec(chunk);
34419
+ if (header) {
34420
+ out2.add(header[1]);
34421
+ out2.add(header[2]);
34422
+ }
34423
+ for (const re of [/^--- a\/(.+)$/m, /^\+\+\+ b\/(.+)$/m, /^rename from (.+)$/m, /^rename to (.+)$/m, /^copy from (.+)$/m, /^copy to (.+)$/m]) {
34424
+ const m = re.exec(chunk);
34425
+ if (m) out2.add(m[1]);
34426
+ }
34427
+ return [...out2];
34428
+ }
34429
+ function withoutDeniedDiffs(diff) {
34430
+ const kept = [];
34431
+ const withheld = /* @__PURE__ */ new Set();
34432
+ for (const chunk of diff.split(/^(?=diff --git )/m)) {
34433
+ if (!chunk.startsWith("diff --git ")) {
34434
+ if (chunk.trim()) kept.push(chunk);
34435
+ continue;
34436
+ }
34437
+ const paths = diffChunkPaths(chunk);
34438
+ if (paths.length === 0 || paths.some((p) => isDeniedPath(p))) {
34439
+ withheld.add(paths[paths.length - 1] ?? "(a path that could not be read)");
34440
+ continue;
34441
+ }
34442
+ kept.push(chunk);
34443
+ }
34444
+ return { text: kept.join(""), withheld: [...withheld] };
33881
34445
  }
33882
34446
  function validateRepoPath(value, { allowRoot = false } = {}) {
33883
34447
  if (typeof value !== "string") return { ok: false, error: "path must be a string" };
@@ -33984,7 +34548,12 @@ var gitDiffTool = async (ctx, args2) => {
33984
34548
  }
33985
34549
  const res = await git(ctx, ["--literal-pathspecs", ...diffArgs, "--", path.value]);
33986
34550
  if (!res.ok) return { ok: false, error: gitFailure(res, "diff") };
33987
- return { ok: true, value: res.stdout.trim() ? capOutput(res.stdout) : `no changes to ${path.value} in the change under review` };
34551
+ if (!res.stdout.trim()) return { ok: true, value: `no changes to ${path.value} in the change under review` };
34552
+ const { text: text3, withheld } = withoutDeniedDiffs(res.stdout);
34553
+ const note = withheld.length > 0 ? `
34554
+ [not shown: ${withheld.length} file(s) shaped like secrets or owned by Verity: ${withheld.slice(0, 20).join(", ")}]` : "";
34555
+ if (!text3.trim()) return { ok: true, value: `no reviewable changes to ${path.value} in the change under review${note}` };
34556
+ return { ok: true, value: capOutput(text3) + note };
33988
34557
  };
33989
34558
  var gitLogTool = async (ctx, args2) => {
33990
34559
  const path = validateRepoPath(args2.path);
@@ -34361,23 +34930,23 @@ function deriveSeedNodes(inputs) {
34361
34930
  const out2 = [];
34362
34931
  const ks = inputs.knowledgeSpec;
34363
34932
  if (ks.project_name) {
34364
- const bodyLines = [];
34933
+ const bodyLines2 = [];
34365
34934
  const arch3 = ks.architecture ? `${ks.architecture} ` : "";
34366
34935
  const langs = (ks.languages ?? []).filter(Boolean);
34367
34936
  const frameworks2 = (ks.frameworks ?? []).filter(Boolean);
34368
34937
  const primaryLang = langs[0] ?? "software";
34369
- bodyLines.push(`**${ks.project_name}** is a ${arch3}${primaryLang} project.`);
34370
- bodyLines.push("");
34371
- if (langs.length > 0) bodyLines.push(`- **Languages:** ${langs.join(", ")}`);
34372
- if (frameworks2.length > 0) bodyLines.push(`- **Frameworks:** ${frameworks2.join(", ")}`);
34373
- if (ks.build_system) bodyLines.push(`- **Build system:** ${ks.build_system}`);
34374
- if (ks.test_framework) bodyLines.push(`- **Test framework:** ${ks.test_framework}`);
34375
- bodyLines.push("");
34376
- bodyLines.push("_Generated from the Standard synthesized by `/verity-setup`. Edit this file to add project context._");
34938
+ bodyLines2.push(`**${ks.project_name}** is a ${arch3}${primaryLang} project.`);
34939
+ bodyLines2.push("");
34940
+ if (langs.length > 0) bodyLines2.push(`- **Languages:** ${langs.join(", ")}`);
34941
+ if (frameworks2.length > 0) bodyLines2.push(`- **Frameworks:** ${frameworks2.join(", ")}`);
34942
+ if (ks.build_system) bodyLines2.push(`- **Build system:** ${ks.build_system}`);
34943
+ if (ks.test_framework) bodyLines2.push(`- **Test framework:** ${ks.test_framework}`);
34944
+ bodyLines2.push("");
34945
+ bodyLines2.push("_Generated from the Standard synthesized by `/verity-setup`. Edit this file to add project context._");
34377
34946
  out2.push({
34378
34947
  kind: "domain",
34379
34948
  title: "Project overview",
34380
- body: bodyLines.join("\n"),
34949
+ body: bodyLines2.join("\n"),
34381
34950
  domains: [],
34382
34951
  // Broad globs so the overview surfaces on any source file change.
34383
34952
  // It's the project's anchor node — cheap to inject, high signal.
@@ -34457,10 +35026,10 @@ var NODE_DIRS = ["decisions", "quality", "security", "intent", "gotchas", "patte
34457
35026
  function normalizeTitle(title) {
34458
35027
  return title.trim().replace(/\s+/g, " ").toLowerCase();
34459
35028
  }
34460
- async function localSeedTitles(memoryDir3 = MEMORY_DIR) {
35029
+ async function localSeedTitles(memoryDir4 = MEMORY_DIR) {
34461
35030
  const titles = /* @__PURE__ */ new Set();
34462
35031
  for (const dir of NODE_DIRS) {
34463
- const full = (0, import_node_path42.join)(memoryDir3, dir);
35032
+ const full = (0, import_node_path42.join)(memoryDir4, dir);
34464
35033
  if (!(0, import_node_fs53.existsSync)(full)) continue;
34465
35034
  let files;
34466
35035
  try {
@@ -34506,18 +35075,20 @@ function pickFrontmatterTitle(content) {
34506
35075
  }
34507
35076
  return raw;
34508
35077
  }
34509
- async function serverSeedTitles(opts) {
34510
- const res = await apiRequest({
34511
- method: "GET",
34512
- path: "/compound/memory/nodes?created_by=seed&status=active&limit=100",
34513
- serviceUrl: opts.serviceUrl,
34514
- token: opts.token,
34515
- verbose: opts.verbose,
34516
- cmd: "memory_seed"
34517
- });
34518
- if (!res.ok || !Array.isArray(res.data.nodes)) return null;
35078
+ async function serverSeedTitles(opts, statuses = ["active"]) {
34519
35079
  const titles = /* @__PURE__ */ new Set();
34520
- for (const n2 of res.data.nodes) if (typeof n2?.title === "string") titles.add(normalizeTitle(n2.title));
35080
+ for (const status of statuses) {
35081
+ const res = await apiRequest({
35082
+ method: "GET",
35083
+ path: `/compound/memory/nodes?created_by=seed&status=${encodeURIComponent(status)}&limit=100`,
35084
+ serviceUrl: opts.serviceUrl,
35085
+ token: opts.token,
35086
+ verbose: opts.verbose,
35087
+ cmd: "memory_seed"
35088
+ });
35089
+ if (!res.ok || !Array.isArray(res.data.nodes)) return null;
35090
+ for (const n2 of res.data.nodes) if (typeof n2?.title === "string") titles.add(normalizeTitle(n2.title));
35091
+ }
34521
35092
  return titles;
34522
35093
  }
34523
35094
  function renderNodeMarkdown(candidate, nodeId, createdAt) {
@@ -34608,6 +35179,13 @@ async function runSeed(opts) {
34608
35179
  if (toPost.length === 0) {
34609
35180
  return { created: 0, existing: 0, failed: 0, skipped: "already_seeded", candidates: candidates2 };
34610
35181
  }
35182
+ const retired = await serverSeedTitles(opts, ["archived", "superseded"]);
35183
+ if (retired) {
35184
+ toPost = toPost.filter((c) => !retired.has(normalizeTitle(c.title)));
35185
+ if (toPost.length === 0) {
35186
+ return { created: 0, existing: 0, failed: 0, skipped: "already_seeded", candidates: candidates2 };
35187
+ }
35188
+ }
34611
35189
  }
34612
35190
  }
34613
35191
  }
@@ -34657,8 +35235,9 @@ async function runSeed(opts) {
34657
35235
  }
34658
35236
  const content = typeof res.data.content === "string" && res.data.content.length > 0 ? res.data.content : renderNodeMarkdown(c, nodeId, createdAt);
34659
35237
  try {
34660
- await (0, import_promises11.mkdir)((0, import_node_path42.dirname)(targetPath), { recursive: true });
34661
- await (0, import_promises11.writeFile)(targetPath, content);
35238
+ if (!await writeContainedNodeFile(targetPath, content)) {
35239
+ throw new Error("refused: a link, or a folder outside the repository");
35240
+ }
34662
35241
  if (deduplicated) existing++;
34663
35242
  else {
34664
35243
  created++;
@@ -34997,7 +35576,6 @@ function isExplicitlyAutonomous(env = process.env) {
34997
35576
  }
34998
35577
 
34999
35578
  // src/lib/task-context.ts
35000
- var import_node_child_process13 = require("node:child_process");
35001
35579
  var CLOSING_RE = /\b(close[sd]?|fix(?:e[sd])?|resolve[sd]?)\b[\s:]*#(\d+)/i;
35002
35580
  var BRANCH_RE = /(?:^|[/_-])(?:issue|gh|fix)[-_/]?(\d+)\b/i;
35003
35581
  function parseLinkedIssue(sources) {
@@ -35013,7 +35591,7 @@ function parseLinkedIssue(sources) {
35013
35591
  }
35014
35592
  function safeExec(cmd, timeout) {
35015
35593
  try {
35016
- return (0, import_node_child_process13.execSync)(cmd, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout }).trim();
35594
+ return execSync2(cmd, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout }).trim();
35017
35595
  } catch {
35018
35596
  return "";
35019
35597
  }
@@ -35791,7 +36369,6 @@ function describeRelease(release, input) {
35791
36369
  }
35792
36370
 
35793
36371
  // src/lib/memory-pull.ts
35794
- var import_node_child_process14 = require("node:child_process");
35795
36372
  var import_promises12 = require("node:fs/promises");
35796
36373
  var pullStateFile = () => projectPath(`${VERITY_DIR}/.memory-pull-state.json`);
35797
36374
  var PAGE_SIZE = 200;
@@ -35882,6 +36459,13 @@ async function recordServedFiles(served) {
35882
36459
  for (const [path, hash] of served) state.served.set(path, hash);
35883
36460
  await writePullState(state);
35884
36461
  }
36462
+ async function forgetServedFiles(paths) {
36463
+ if (paths.length === 0) return;
36464
+ const state = await readPullState();
36465
+ let changed = false;
36466
+ for (const p of paths) changed = state.served.delete(p) || changed;
36467
+ if (changed) await writePullState(state);
36468
+ }
35885
36469
  async function readPullState() {
35886
36470
  try {
35887
36471
  const parsed = JSON.parse(await (0, import_promises12.readFile)(pullStateFile(), "utf-8"));
@@ -35914,7 +36498,7 @@ function pullsAtSessionStart(auth3, source2) {
35914
36498
  }
35915
36499
  function spawnBackgroundPull(deps = {}) {
35916
36500
  const d = {
35917
- spawn: import_node_child_process14.spawn,
36501
+ spawn: spawn2,
35918
36502
  execPath: process.execPath,
35919
36503
  script: process.argv[1],
35920
36504
  cwd: process.cwd(),
@@ -36371,12 +36955,10 @@ async function runAnalyze(opts, globals) {
36371
36955
 
36372
36956
  // src/lib/prove/lean-background.ts
36373
36957
  var import_node_fs63 = require("node:fs");
36374
- var import_node_child_process17 = require("node:child_process");
36375
36958
  var import_node_crypto18 = require("node:crypto");
36376
36959
  var import_node_path51 = require("node:path");
36377
36960
 
36378
36961
  // src/lib/prove/runner.ts
36379
- var import_node_child_process15 = require("node:child_process");
36380
36962
  var import_node_os9 = require("node:os");
36381
36963
  var import_node_fs61 = require("node:fs");
36382
36964
  var import_node_os10 = require("node:os");
@@ -37114,7 +37696,7 @@ function findToolchain(env = process.env) {
37114
37696
  const candidates2 = [(0, import_node_path49.join)(managedBin, exe), ...elanHas ? [(0, import_node_path49.join)(elanBin, exe)] : [], exe];
37115
37697
  for (const candidate of candidates2) {
37116
37698
  try {
37117
- (0, import_node_child_process15.execFileSync)(candidate, ["--version"], { env: merged, stdio: ["ignore", "pipe", "pipe"], timeout: 1e4 });
37699
+ execFileSync2(candidate, ["--version"], { env: merged, stdio: ["ignore", "pipe", "pipe"], timeout: 1e4 });
37118
37700
  return { lake: candidate, env: merged };
37119
37701
  } catch {
37120
37702
  }
@@ -37125,7 +37707,7 @@ var INSTALL_HINT = "Lean 4 is not installed. Run `verity lean install` (official
37125
37707
  function run2(tc, cwd, args2, timeoutMs) {
37126
37708
  const started = Date.now();
37127
37709
  try {
37128
- const stdout = (0, import_node_child_process15.execFileSync)(tc.lake, args2, {
37710
+ const stdout = execFileSync2(tc.lake, args2, {
37129
37711
  cwd,
37130
37712
  env: tc.env,
37131
37713
  encoding: "utf-8",
@@ -37151,7 +37733,7 @@ function runAsync(tc, cwd, args2, timeoutMs) {
37151
37733
  const MAX = 16 * 1024 * 1024;
37152
37734
  return new Promise((resolve10) => {
37153
37735
  const posix = (0, import_node_os9.platform)() !== "win32";
37154
- const child = (0, import_node_child_process15.spawn)(tc.lake, args2, { cwd, env: tc.env, stdio: ["ignore", "pipe", "pipe"], detached: posix });
37736
+ const child = spawn2(tc.lake, args2, { cwd, env: tc.env, stdio: ["ignore", "pipe", "pipe"], detached: posix });
37155
37737
  let stdout = "";
37156
37738
  let stderr = "";
37157
37739
  let timedOut = false;
@@ -37238,18 +37820,21 @@ function listModelFiles(root) {
37238
37820
  function trimDiagnostics(text3) {
37239
37821
  return text3.split("\n").filter((l) => !l.startsWith("trace:")).join("\n").trim().slice(0, 4e3);
37240
37822
  }
37823
+ function leanFileArg(rel) {
37824
+ return rel.startsWith("-") ? `./${rel}` : rel;
37825
+ }
37241
37826
  function checkModel(tc, root, modelFile, opts = {}) {
37242
37827
  const proofsAbs = (0, import_node_path49.join)(root, PROOFS_DIR);
37243
37828
  const rel = (0, import_node_path49.relative)(proofsAbs, modelFile);
37244
37829
  const problem = proofsWorkspaceProblem(root);
37245
- const res = problem ? refused(problem) : run2(tc, proofsAbs, ["env", "lean", rel], opts.timeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS);
37830
+ const res = problem ? refused(problem) : run2(tc, proofsAbs, ["env", "lean", leanFileArg(rel)], opts.timeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS);
37246
37831
  return toReport(res, root, modelFile, opts.timeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS);
37247
37832
  }
37248
37833
  async function checkModelAsync(tc, root, modelFile, opts = {}) {
37249
37834
  const proofsAbs = (0, import_node_path49.join)(root, PROOFS_DIR);
37250
37835
  const rel = (0, import_node_path49.relative)(proofsAbs, modelFile);
37251
37836
  const problem = proofsWorkspaceProblem(root);
37252
- const res = problem ? refused(problem) : await runAsync(tc, proofsAbs, ["env", "lean", rel], opts.timeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS);
37837
+ const res = problem ? refused(problem) : await runAsync(tc, proofsAbs, ["env", "lean", leanFileArg(rel)], opts.timeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS);
37253
37838
  return toReport(res, root, modelFile, opts.timeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS);
37254
37839
  }
37255
37840
  function toReport(res, root, modelFile, timeoutMs) {
@@ -37303,7 +37888,6 @@ function writeResult(root, report) {
37303
37888
  // src/lib/prove/lean-install.ts
37304
37889
  var import_node_fs62 = require("node:fs");
37305
37890
  var import_node_crypto17 = require("node:crypto");
37306
- var import_node_child_process16 = require("node:child_process");
37307
37891
  var import_node_os11 = require("node:os");
37308
37892
  var import_node_path50 = require("node:path");
37309
37893
  var import_promises13 = require("node:stream/promises");
@@ -37374,7 +37958,7 @@ function unzip(zip, dest) {
37374
37958
  const errors = [];
37375
37959
  for (const [cmd, args2] of attempts) {
37376
37960
  try {
37377
- (0, import_node_child_process16.execFileSync)(cmd, args2, { stdio: ["ignore", "ignore", "pipe"], timeout: 15 * 6e4 });
37961
+ execFileSync2(cmd, args2, { stdio: ["ignore", "ignore", "pipe"], timeout: 15 * 6e4 });
37378
37962
  if ((0, import_node_fs62.readdirSync)(dest).length > 0) return { ok: true, via: cmd };
37379
37963
  errors.push(`${cmd}: produced nothing`);
37380
37964
  } catch (err2) {
@@ -37577,7 +38161,7 @@ function startBackgroundInstall(opts) {
37577
38161
  (0, import_node_fs63.mkdirSync)(home, { recursive: true });
37578
38162
  const log = (0, import_node_fs63.openSync)((0, import_node_path51.join)(home, LOG_FILE), import_node_fs63.constants.O_WRONLY | import_node_fs63.constants.O_APPEND | import_node_fs63.constants.O_CREAT | (import_node_fs63.constants.O_NOFOLLOW ?? 0), 384);
37579
38163
  try {
37580
- const child = (opts.run ?? import_node_child_process17.spawn)(process.execPath, [opts.cli, "lean", "install", "--yes", "--background"], {
38164
+ const child = (opts.run ?? spawn2)(process.execPath, [opts.cli, "lean", "install", "--yes", "--background"], {
37581
38165
  detached: true,
37582
38166
  stdio: ["ignore", log, log],
37583
38167
  env: { ...env, DO_NOT_TRACK: env.DO_NOT_TRACK ?? "1" }
@@ -38424,6 +39008,7 @@ var deterministicRanker = {
38424
39008
  }
38425
39009
  };
38426
39010
  function excerpt(root, file, bytes, readSource, changed, codeMap, codeSource) {
39011
+ if (isSecretShapedPath(file)) return "";
38427
39012
  const read2 = readSource ? readSource(file, EXCERPT_SOURCE_CAP_BYTES) : readRegularFileInRoot(root, file, EXCERPT_SOURCE_CAP_BYTES);
38428
39013
  if (!read2.ok) return "";
38429
39014
  if (changed && changed.length > 0) {
@@ -39554,7 +40139,7 @@ function isTrackedInRoot(root, file) {
39554
40139
  }
39555
40140
  function buildReplayBrief(root, report, property, modelSource, readSource) {
39556
40141
  const sources = (report.header?.abstracts ?? []).map((a) => {
39557
- const read2 = readSource ? readSource(a.file, MAX_SOURCE_BYTES) : isTrackedInRoot(root, a.file) ? readRegularFileInRoot(root, a.file, MAX_SOURCE_BYTES) : { ok: false, reason: "not tracked by git \u2014 models may only abstract tracked source" };
40142
+ const read2 = isSecretShapedPath(a.file) ? { ok: false, reason: SECRET_REFUSED_READ } : readSource ? readSource(a.file, MAX_SOURCE_BYTES) : isTrackedInRoot(root, a.file) ? readRegularFileInRoot(root, a.file, MAX_SOURCE_BYTES) : { ok: false, reason: "not tracked by git \u2014 models may only abstract tracked source" };
39558
40143
  const text3 = read2.ok ? read2.content : "";
39559
40144
  return {
39560
40145
  file: a.file,
@@ -40307,10 +40892,20 @@ function isNetworkError(message) {
40307
40892
  var GENERATE_RESERVE_MS = 4e4;
40308
40893
  function workingTreeReader(root) {
40309
40894
  return (file, maxBytes) => {
40895
+ if (isSecretShapedPath(file)) return { ok: false, reason: SECRET_REFUSED_READ };
40310
40896
  const r = readRegularFileInRoot(root, file, maxBytes);
40311
40897
  return r.ok ? { ok: true, content: r.content } : { ok: false, reason: r.reason };
40312
40898
  };
40313
40899
  }
40900
+ function pushedTipReader(tip) {
40901
+ return (file, maxBytes) => {
40902
+ if (isSecretShapedPath(file)) return { ok: false, reason: SECRET_REFUSED_READ };
40903
+ const content = showContentAtRef(tip, file);
40904
+ if (content === null) return { ok: false, reason: `not at the pushed tip (${tip.slice(0, 12)})` };
40905
+ if (Buffer.byteLength(content) > maxBytes) return { ok: false, reason: "too-large" };
40906
+ return { ok: true, content };
40907
+ };
40908
+ }
40314
40909
  function sha256Of(content) {
40315
40910
  return (0, import_node_crypto21.createHash)("sha256").update(content).digest("hex");
40316
40911
  }
@@ -41560,12 +42155,7 @@ function startFormalPhase(input) {
41560
42155
  changedLines = void 0;
41561
42156
  }
41562
42157
  const tip = input.range.kind === "push" && input.range.head !== "INDEX" ? input.range.head : null;
41563
- const readSource = tip ? (file, maxBytes) => {
41564
- const content = showContentAtRef(tip, file);
41565
- if (content === null) return { ok: false, reason: `not at the pushed tip (${tip.slice(0, 12)})` };
41566
- if (Buffer.byteLength(content) > maxBytes) return { ok: false, reason: "too-large" };
41567
- return { ok: true, content };
41568
- } : void 0;
42158
+ const readSource = tip ? pushedTipReader(tip) : void 0;
41569
42159
  const run4 = async () => {
41570
42160
  const prep = await prepareFormalPhase(input.root, process.env, input.auth, readSource, withoutPaths(tip ? revisionSource(input.root, tip) : worktreeSource(input.root), isDeniedPath), { tier: profile.formal.tier });
41571
42161
  if (!prep.ok) return skippedOutcome(prep.skip, prep.detail);
@@ -41636,7 +42226,8 @@ async function runGuard(opts, globals) {
41636
42226
  if (!tokenResult.ok || !urlResult.ok) process.exit(0);
41637
42227
  logEvent("guard_frame", { moment, ...frameTelemetry(frame, range) });
41638
42228
  const codeDelta = collectCodeDelta(files, GIT_MOMENT_BUDGET);
41639
- if (codeDelta.total_files === 0) process.exit(0);
42229
+ const withheld = codeDelta.excluded.some((e) => e.kind === "policy");
42230
+ if (codeDelta.total_files === 0 && !withheld) process.exit(0);
41640
42231
  const statedIntent = extractStatedIntent(
41641
42232
  moment,
41642
42233
  command,
@@ -41651,6 +42242,14 @@ async function runGuard(opts, globals) {
41651
42242
  statedIntent,
41652
42243
  buildGuardCoverage(files, codeDelta, frame, range)
41653
42244
  );
42245
+ const changedLines = rangeChangedLines(frame, range, codeDelta.files);
42246
+ if (changedLines.files.length > 0) requestBody2.changed_lines = changedLines.files;
42247
+ logEvent("guard_changed_lines", {
42248
+ moment,
42249
+ sent: codeDelta.files.length,
42250
+ with_ranges: changedLines.files.length,
42251
+ omitted: changedLines.omitted
42252
+ });
41654
42253
  try {
41655
42254
  const ig = loadVerityIgnore();
41656
42255
  const reviewedRev = range.kind === "push" && range.head !== "INDEX" ? range.head : null;
@@ -41733,7 +42332,7 @@ async function runGuard(opts, globals) {
41733
42332
  timeout: 29e4,
41734
42333
  cmd: "guard"
41735
42334
  });
41736
- const agent = startAgentReview({
42335
+ const agent = codeDelta.total_files === 0 ? null : startAgentReview({
41737
42336
  serviceUrl: urlResult.data,
41738
42337
  token: tokenResult.data.token,
41739
42338
  verbose: globals.verbose,
@@ -41804,7 +42403,11 @@ async function runGuard(opts, globals) {
41804
42403
  const agentLine = describeAgentOutcome(agentOutcome);
41805
42404
  const formalLine = describeFormalOutcome(formalOutcome) || (projectConfig ? formalOffUnverifiedLine(moment, projectConfig) : "");
41806
42405
  const covDetail = [coverageBlock(coverage), agentLine, formalLine].filter(Boolean).join("\n ");
41807
- const witnessed = [...codeDelta.files.map((f) => f.path), ...agentVerdict?.witnessed_files ?? []];
42406
+ const witnessed = [
42407
+ ...codeDelta.files.map((f) => f.path),
42408
+ ...agentVerdict?.witnessed_files ?? [],
42409
+ ...scannerWitnessedWithheld(codeDelta.excluded, requestBody2.static_results?.findings)
42410
+ ];
41808
42411
  mergeFormalFindings(response, formalOutcome, files);
41809
42412
  decision = response.gate_decision ?? "(unrecognised)";
41810
42413
  if (formalOutcome) {
@@ -43014,7 +43617,6 @@ var Rail = class {
43014
43617
  var import_node_fs74 = require("node:fs");
43015
43618
  var import_node_path62 = require("node:path");
43016
43619
  var import_node_os13 = require("node:os");
43017
- var import_node_child_process18 = require("node:child_process");
43018
43620
  function createInitUi(opts) {
43019
43621
  const term = opts.term ?? detectTerm();
43020
43622
  const screen = new Screen(term);
@@ -43040,7 +43642,7 @@ function markInitSeen(home = process.env.HOME || (0, import_node_os13.homedir)()
43040
43642
  }
43041
43643
  function repoLabel() {
43042
43644
  try {
43043
- const url = (0, import_node_child_process18.execSync)("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
43645
+ const url = execFileSync2("git", ["remote", "get-url", "origin"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
43044
43646
  const parsed = parseRemote(url);
43045
43647
  if (parsed) return terminalText(`${parsed.owner}/${parsed.repo}`, 120);
43046
43648
  } catch {
@@ -43049,7 +43651,6 @@ function repoLabel() {
43049
43651
  }
43050
43652
 
43051
43653
  // src/lib/prereqs.ts
43052
- var import_node_child_process19 = require("node:child_process");
43053
43654
  var import_node_fs75 = require("node:fs");
43054
43655
  var MIN_NODE_MAJOR = 20;
43055
43656
  function which(bin) {
@@ -43071,7 +43672,7 @@ function checkNode() {
43071
43672
  function checkGit() {
43072
43673
  let detail2 = "";
43073
43674
  try {
43074
- detail2 = (0, import_node_child_process19.execSync)("git --version", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
43675
+ detail2 = execFileSync2("git", ["--version"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
43075
43676
  } catch {
43076
43677
  return {
43077
43678
  id: "git",
@@ -43126,7 +43727,7 @@ function sameFile(a, b) {
43126
43727
  var INSTALL_TIMEOUT_MS = 12e4;
43127
43728
  function run3(command, args2, opts = {}) {
43128
43729
  return new Promise((resolve10) => {
43129
- const child = (0, import_node_child_process19.spawn)(command, args2, {
43730
+ const child = spawn2(command, args2, {
43130
43731
  stdio: opts.inherit ? "inherit" : "pipe",
43131
43732
  timeout: INSTALL_TIMEOUT_MS
43132
43733
  });
@@ -43325,12 +43926,11 @@ async function healLocalTelemetry(root = repoRoot()) {
43325
43926
  }
43326
43927
 
43327
43928
  // src/lib/telemetry-gate.ts
43328
- var import_node_child_process20 = require("node:child_process");
43329
43929
  var import_node_fs76 = require("node:fs");
43330
43930
  var import_node_path65 = require("node:path");
43331
43931
  var PROJECT_MARKERS = ["setup.json", "config.json"];
43332
43932
  function writtenOnThisMachine(root, rel) {
43333
- const r = (0, import_node_child_process20.spawnSync)("git", hardenedGitArgv(["ls-files", "--error-unmatch", "--", rel]), {
43933
+ const r = spawnSync2("git", hardenedGitArgv(["ls-files", "--error-unmatch", "--", rel]), {
43334
43934
  cwd: root,
43335
43935
  env: hardenedGitEnv(),
43336
43936
  encoding: "utf-8",
@@ -43660,7 +44260,6 @@ function backgroundStep(ui, opts) {
43660
44260
  var import_promises15 = require("node:fs/promises");
43661
44261
 
43662
44262
  // src/lib/codacy-legacy.ts
43663
- var import_node_child_process21 = require("node:child_process");
43664
44263
  var VERITY_ESLINT_CONFIG_FILE = ".codacy/tools-configs/eslint.config.mjs";
43665
44264
  var VERITY_ESLINT_LOCAL_PATH = "./.codacy/tools-configs/eslint.config.mjs";
43666
44265
  var VERITY_ESLINT_FIRST_LINE = "// Verity-owned ESLint config for the quality gate.";
@@ -43682,7 +44281,7 @@ function classifyCodacyConfig(root = repoRoot()) {
43682
44281
  }
43683
44282
  function isTracked(root, path) {
43684
44283
  try {
43685
- (0, import_node_child_process21.execFileSync)("git", ["ls-files", "--error-unmatch", "--", path], { cwd: root, stdio: "pipe" });
44284
+ execFileSync2("git", ["ls-files", "--error-unmatch", "--", path], { cwd: root, stdio: "pipe" });
43686
44285
  return true;
43687
44286
  } catch {
43688
44287
  return false;
@@ -43697,7 +44296,7 @@ function removeVerityCodacyConfig(root = repoRoot()) {
43697
44296
  untracked.push(f);
43698
44297
  continue;
43699
44298
  }
43700
- (0, import_node_child_process21.execFileSync)("git", ["rm", "--quiet", "--", f], { cwd: root, stdio: "pipe" });
44299
+ execFileSync2("git", ["rm", "--quiet", "--", f], { cwd: root, stdio: "pipe" });
43701
44300
  removed.push(f);
43702
44301
  }
43703
44302
  } catch (err2) {
@@ -44433,19 +45032,18 @@ function sayChanged(ui, before) {
44433
45032
  // src/commands/migrate.ts
44434
45033
  var import_node_fs79 = require("node:fs");
44435
45034
  var import_node_path68 = require("node:path");
44436
- var import_node_child_process22 = require("node:child_process");
44437
45035
  var LEGACY_NPM_PACKAGE = "@codacy/gate-cli";
44438
45036
  function defaultNpmRemover(pkg) {
44439
45037
  let listing = "";
44440
45038
  try {
44441
- listing = (0, import_node_child_process22.execSync)(`npm ls -g --depth=0 --json ${pkg}`, { stdio: "pipe", timeout: 6e4 }).toString();
45039
+ listing = execSync2(`npm ls -g --depth=0 --json ${pkg}`, { stdio: "pipe", timeout: 6e4 }).toString();
44442
45040
  } catch (err2) {
44443
45041
  const stdout = err2.stdout;
44444
45042
  listing = stdout ? stdout.toString() : "";
44445
45043
  }
44446
45044
  const installed2 = parseGlobalListing(listing, pkg);
44447
45045
  if (installed2 === false) return "absent";
44448
- (0, import_node_child_process22.execSync)(`npm rm -g ${pkg}`, { stdio: "pipe", timeout: 12e4 });
45046
+ execSync2(`npm rm -g ${pkg}`, { stdio: "pipe", timeout: 12e4 });
44449
45047
  return installed2 ? "removed" : "unknown";
44450
45048
  }
44451
45049
  function parseGlobalListing(listing, pkg) {
@@ -44463,7 +45061,7 @@ function parseGlobalListing(listing, pkg) {
44463
45061
  return Object.prototype.hasOwnProperty.call(deps, pkg);
44464
45062
  }
44465
45063
  function git2(cwd, args2) {
44466
- (0, import_node_child_process22.execFileSync)("git", hardenedGitArgv(args2), { cwd, stdio: "pipe", env: hardenedGitEnv() });
45064
+ execFileSync2("git", hardenedGitArgv(args2), { cwd, stdio: "pipe", env: hardenedGitEnv() });
44467
45065
  }
44468
45066
  function isGitTracked(cwd, relPath) {
44469
45067
  try {
@@ -44797,7 +45395,6 @@ function registerMigrateCommand(program2) {
44797
45395
  }
44798
45396
 
44799
45397
  // src/commands/init/sign-in.ts
44800
- var import_node_child_process23 = require("node:child_process");
44801
45398
  async function classifyExistingLogin(serviceUrl, remote, opts) {
44802
45399
  const existing = await resolveToken(opts.token);
44803
45400
  if (!existing.ok) return { kind: "none" };
@@ -44823,7 +45420,7 @@ async function classifyExistingLogin(serviceUrl, remote, opts) {
44823
45420
  }
44824
45421
  function gitRemote() {
44825
45422
  try {
44826
- return (0, import_node_child_process23.execSync)("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
45423
+ return execFileSync2("git", ["remote", "get-url", "origin"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
44827
45424
  } catch {
44828
45425
  return "";
44829
45426
  }
@@ -45011,7 +45608,7 @@ async function recordAnswers(ui, momentIds, telemetry, completed) {
45011
45608
  moments: momentIds,
45012
45609
  ...telemetry ? { telemetry } : {},
45013
45610
  // A failed run also clears an earlier stamp: "done" describes this checkout now.
45014
- init: completed ? { completed_at: (/* @__PURE__ */ new Date()).toISOString(), cli_version: true ? "0.35.5" : "dev" } : void 0
45611
+ init: completed ? { completed_at: (/* @__PURE__ */ new Date()).toISOString(), cli_version: true ? "0.36.0" : "dev" } : void 0
45015
45612
  });
45016
45613
  } catch (err2) {
45017
45614
  ui.rail.line(`Could not record the setup answers: ${terminalText(err2.message, 200)}`, "warn");
@@ -45714,6 +46311,9 @@ var import_node_fs84 = require("node:fs");
45714
46311
  // src/lib/reflection-globs.ts
45715
46312
  var MAX_GLOBS = 6;
45716
46313
  var PATH_TOKEN = /[A-Za-z0-9_./-]*[A-Za-z0-9_-]\.[A-Za-z0-9]{1,8}(?::\d+(?::\d+)?)?/g;
46314
+ var GLOB_TOKEN = /[A-Za-z0-9_.-][A-Za-z0-9_./-]*\/[A-Za-z0-9_./*?-]*[*?][A-Za-z0-9_./*?-]*/g;
46315
+ var DIR_TOKEN = /[A-Za-z0-9_.-][A-Za-z0-9_./-]*\/(?![A-Za-z0-9_.*?/-])/g;
46316
+ var BARE_TOKEN = /`([A-Za-z0-9_][A-Za-z0-9_./-]*)`/g;
45717
46317
  function clean(token) {
45718
46318
  return token.replace(/:\d+(?::\d+)?$/, "").replace(/^[`'"(\[]+/, "").replace(/[`'"),\].;!?]+$/, "").replace(/^\.\//, "");
45719
46319
  }
@@ -45727,25 +46327,91 @@ function deriveFileGlobs(text3, treePaths) {
45727
46327
  if (!text3 || treePaths.length === 0) return [];
45728
46328
  const out2 = [];
45729
46329
  const seen = /* @__PURE__ */ new Set();
45730
- for (const raw of text3.match(PATH_TOKEN) ?? []) {
45731
- const token = clean(raw);
45732
- if (!token) continue;
45733
- const path = resolve8(token, treePaths);
45734
- if (!path || seen.has(path)) continue;
45735
- seen.add(path);
45736
- out2.push(path);
46330
+ const candidates2 = [];
46331
+ for (const m of text3.matchAll(PATH_TOKEN)) {
46332
+ const token = clean(m[0]);
46333
+ candidates2.push({ at: m.index ?? 0, glob: token ? resolve8(token, treePaths) : null });
46334
+ }
46335
+ for (const m of text3.matchAll(GLOB_TOKEN)) {
46336
+ candidates2.push({ at: m.index ?? 0, glob: resolveGlob(clean(m[0]), treePaths) });
46337
+ }
46338
+ for (const m of text3.matchAll(DIR_TOKEN)) {
46339
+ candidates2.push({ at: m.index ?? 0, glob: resolveDir(m[0], treePaths) });
46340
+ }
46341
+ for (const m of text3.matchAll(BARE_TOKEN)) {
46342
+ candidates2.push({ at: m.index ?? 0, glob: resolveBare(clean(m[1]), treePaths) });
46343
+ }
46344
+ candidates2.sort((a, b) => a.at - b.at);
46345
+ for (const { glob } of candidates2) {
46346
+ if (!glob || seen.has(glob)) continue;
46347
+ seen.add(glob);
46348
+ out2.push(glob);
45737
46349
  if (out2.length >= MAX_GLOBS) break;
45738
46350
  }
45739
46351
  return out2;
45740
46352
  }
46353
+ function resolveBare(token, treePaths) {
46354
+ if (!token) return null;
46355
+ if (token.includes("/")) return treePaths.includes(token) ? token : null;
46356
+ if (!/^[A-Z]/.test(token)) return null;
46357
+ return resolve8(token, treePaths);
46358
+ }
46359
+ function resolveGlob(token, treePaths) {
46360
+ if (!token || !/[*?]/.test(token)) return null;
46361
+ return anyPathMatches(token, treePaths) ? token : null;
46362
+ }
46363
+ function resolveDir(token, treePaths) {
46364
+ const dir = token.replace(/^\.\//, "").replace(/\/+$/, "");
46365
+ if (!dir || dir === "." || dir.startsWith("../")) return null;
46366
+ const prefix = `${dir}/`;
46367
+ return treePaths.some((p) => p.startsWith(prefix)) ? `${dir}/**` : null;
46368
+ }
46369
+
46370
+ // src/lib/reflection-title.ts
46371
+ var DERIVED_TITLE_MAX = 120;
46372
+ var TITLE_MAX = 200;
46373
+ function deriveReflectionTitle(text3, max = DERIVED_TITLE_MAX) {
46374
+ const firstLine = text3.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0) ?? "";
46375
+ const line2 = stripMarkup(firstLine);
46376
+ const sentence = firstSentence(line2);
46377
+ return cutAtWord(sentence, max);
46378
+ }
46379
+ function checkExplicitTitle(raw) {
46380
+ const title = raw.replace(/\s+/g, " ").trim();
46381
+ if (!title) return { ok: false, error: "--title is empty." };
46382
+ if (title.length > TITLE_MAX) {
46383
+ return { ok: false, error: `--title is ${title.length} characters; the limit is ${TITLE_MAX}.` };
46384
+ }
46385
+ return { ok: true, title };
46386
+ }
46387
+ function stripMarkup(line2) {
46388
+ let s = line2.replace(/^#{1,6}\s+/, "").replace(/^>\s*/, "").replace(/^(?:[-*+]|\d+[.)])\s+/, "").trim();
46389
+ const wrapped = s.match(/^(\*\*|__|\*|_)(.+)\1$/);
46390
+ if (wrapped) s = wrapped[2].trim();
46391
+ return s.replace(/\s+/g, " ");
46392
+ }
46393
+ function firstSentence(line2) {
46394
+ const m = line2.match(/^(.+?[.!?])(?=\s+[A-Z0-9`"'(\[])/);
46395
+ const s = (m ? m[1] : line2).trim();
46396
+ return s.replace(/[.:;,]+$/, "").trim() || s;
46397
+ }
46398
+ function cutAtWord(s, max) {
46399
+ if (s.length <= max) return s;
46400
+ const room = s.slice(0, max - 1);
46401
+ const lastSpace = room.lastIndexOf(" ");
46402
+ const head = lastSpace > max / 2 ? room.slice(0, lastSpace) : room;
46403
+ return `${head.replace(/[\s,.;:—-]+$/, "")}\u2026`;
46404
+ }
45741
46405
 
45742
46406
  // src/commands/reflect.ts
45743
46407
  async function writeNodeToDisk(args2) {
45744
46408
  try {
46409
+ const served = /* @__PURE__ */ new Map();
45745
46410
  const written = await applyMemoryWrites(
45746
46411
  [{ path: args2.filePath, content: args2.content, node_id: args2.nodeId, op: "create" }],
45747
- { treePaths: listTrackedFiles(), recordBaseline: false }
46412
+ { treePaths: listTrackedFiles(), baseline: "written", lastServed: await readServedLedger(), served }
45748
46413
  );
46414
+ await recordServedFiles(served);
45749
46415
  if (written === 0) return false;
45750
46416
  return (0, import_node_fs84.existsSync)(projectPath(`${VERITY_DIR}/memory/${args2.filePath}`));
45751
46417
  } catch {
@@ -45753,13 +46419,26 @@ async function writeNodeToDisk(args2) {
45753
46419
  }
45754
46420
  }
45755
46421
  function registerReflectCommand(program2) {
45756
- program2.command("reflect").description("Capture learnings \u2014 auto-extract or record a reflection").option("--user-input <text>", "The reflection to record (the agent-drafted or user-dictated text); - reads it from stdin, so the words never pass through shell quoting").option("--kind <kind>", "Node kind (decision, gotcha, pattern, security, quality, intent, domain, integration)", "gotcha").option("--task-id <id>", "Task to reflect on (defaults to current task)").option("--confirmed", "The USER authored or confirmed these exact words \u2014 records as human-authored (source: user, confidence 1.0)").option("--file-globs <glob...>", "Files this reflection is about. Without any, the paths cited in the text are used; a reflection matching no file never surfaces in a review").option("--autonomous", "(deprecated, ignored) reflections are always recorded without a confirm step").action(async (opts) => {
46422
+ program2.command("reflect").description("Capture learnings \u2014 auto-extract or record a reflection").option("--user-input <text>", "The reflection to record (the agent-drafted or user-dictated text); - reads it from stdin, so the words never pass through shell quoting").option("--kind <kind>", "Node kind (decision, gotcha, pattern, security, quality, intent, domain, integration)", "gotcha").option("--title <text>", "The node title (up to 200 characters). Without it, the first line or sentence of the text is used").option("--task-id <id>", "Task to reflect on (defaults to current task)").option("--confirmed", "The USER authored or confirmed these exact words \u2014 records as human-authored (source: user, confidence 1.0)").option("--file-globs <glob...>", "Files this reflection is about. Without any, the paths cited in the text are used; a reflection matching no file never surfaces in a review").option("--autonomous", "(deprecated, ignored) reflections are always recorded without a confirm step").action(async (opts) => {
45757
46423
  const globals = program2.opts();
45758
46424
  const input = readTextInput(opts.userInput, "--user-input");
45759
46425
  if (!input.ok) {
45760
46426
  printError(input.error);
45761
46427
  process.exit(1);
45762
46428
  }
46429
+ let explicitTitle = null;
46430
+ if (typeof opts.title === "string") {
46431
+ const checked = checkExplicitTitle(opts.title);
46432
+ if (!checked.ok) {
46433
+ printError(checked.error);
46434
+ process.exit(1);
46435
+ }
46436
+ explicitTitle = checked.title;
46437
+ }
46438
+ try {
46439
+ process.chdir(repoRoot());
46440
+ } catch {
46441
+ }
45763
46442
  await ensureMemoryDir();
45764
46443
  const tokenResult = await resolveToken(globals.token);
45765
46444
  if (!tokenResult.ok) {
@@ -45829,6 +46508,7 @@ function registerReflectCommand(program2) {
45829
46508
  const explicitGlobs = Array.isArray(opts.fileGlobs) ? opts.fileGlobs.map((g) => g.trim()).filter(Boolean) : [];
45830
46509
  const derivedGlobs = explicitGlobs.length > 0 ? [] : deriveFileGlobs(userInput, listTrackedFiles());
45831
46510
  const fileGlobs = explicitGlobs.length > 0 ? explicitGlobs : derivedGlobs.length > 0 ? derivedGlobs : inheritedGlobs;
46511
+ const title = explicitTitle ?? deriveReflectionTitle(userInput);
45832
46512
  const result = await apiRequest({
45833
46513
  method: "POST",
45834
46514
  path: "/compound/reflect",
@@ -45836,7 +46516,7 @@ function registerReflectCommand(program2) {
45836
46516
  token,
45837
46517
  body: {
45838
46518
  kind: kind2,
45839
- title: userInput.slice(0, 200),
46519
+ title,
45840
46520
  body: userInput,
45841
46521
  domains: [],
45842
46522
  file_globs: fileGlobs,
@@ -45871,9 +46551,12 @@ function registerReflectCommand(program2) {
45871
46551
  printInfo(
45872
46552
  synced ? `Recorded ${nodeId} \u2192 .verity/memory/${filePath}` : `Recorded ${nodeId} (syncs to .verity/memory/${filePath} on the next analysis).`
45873
46553
  );
46554
+ const storedNode = result.data.node;
46555
+ const storedTitle = typeof storedNode?.title === "string" ? storedNode.title : title;
46556
+ printInfo(` Title: ${terminalText(storedTitle, 200)}`);
45874
46557
  if (viewUrl) printInfo(` ${viewUrl}`);
45875
46558
  printInfo(
45876
- recordedAsUser ? " Recorded as yours (source: user)." : " Auto-recorded from this task \u2014 nobody reviewed it. Edit or delete the file if it is wrong."
46559
+ recordedAsUser ? " Recorded as yours (source: user)." : ` Auto-recorded from this task \u2014 nobody reviewed it. If it is wrong: verity memory edit ${nodeId} \u2026, or verity memory delete ${nodeId}.`
45877
46560
  );
45878
46561
  if (recordedSource !== source2) {
45879
46562
  printWarn(
@@ -45917,9 +46600,1353 @@ function registerReflectCommand(program2) {
45917
46600
  });
45918
46601
  }
45919
46602
 
46603
+ // src/lib/memory-curate.ts
46604
+ var import_promises18 = require("node:fs/promises");
46605
+ var import_node_fs85 = require("node:fs");
46606
+ var import_node_path72 = require("node:path");
46607
+ var memoryDir3 = () => projectPath(`${VERITY_DIR}/memory`);
46608
+ var trashDir = () => projectPath(`${VERITY_DIR}/.memory-trash`);
46609
+ var NODE_DOMAINS = ["decisions", "quality", "security", "intent", "gotchas", "patterns", "domain", "integrations"];
46610
+ var MAX_NODE_FILE_BYTES2 = 64 * 1024;
46611
+ var NODE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,199}$/;
46612
+ var SETTABLE_STATUSES = ["active", "archived", "superseded"];
46613
+ function readInRepo(fullPath, max) {
46614
+ const read2 = readRegularFileInRoot(repoRoot(), fullPath, max);
46615
+ return read2.ok ? read2.content : null;
46616
+ }
46617
+ async function listLocalNodes() {
46618
+ const out2 = [];
46619
+ if (!(0, import_node_fs85.existsSync)(memoryDir3()) || memoryDirEscapesRepo()) return out2;
46620
+ for (const domain of NODE_DOMAINS) {
46621
+ const dir = (0, import_node_path72.join)(memoryDir3(), domain);
46622
+ if (!(0, import_node_fs85.existsSync)(dir)) continue;
46623
+ let files;
46624
+ try {
46625
+ files = (await (0, import_promises18.readdir)(dir)).filter((f) => f.endsWith(".md")).sort();
46626
+ } catch {
46627
+ continue;
46628
+ }
46629
+ for (const file of files) {
46630
+ const content = readInRepo((0, import_node_path72.join)(dir, file), MAX_NODE_FILE_BYTES2);
46631
+ if (content === null) continue;
46632
+ const id = readId(content) ?? file.replace(/\.md$/, "");
46633
+ out2.push({ nodeId: id, path: `${domain}/${file}`, content });
46634
+ }
46635
+ }
46636
+ return out2;
46637
+ }
46638
+ async function findLocalNode(nodeId) {
46639
+ if (!NODE_ID_PATTERN.test(nodeId) || memoryDirEscapesRepo()) return null;
46640
+ for (const domain of NODE_DOMAINS) {
46641
+ const rel = `${domain}/${nodeId}.md`;
46642
+ const content = readInRepo((0, import_node_path72.join)(memoryDir3(), rel), MAX_NODE_FILE_BYTES2);
46643
+ if (content !== null && (readId(content) ?? nodeId) === nodeId) return { nodeId, path: rel, content };
46644
+ }
46645
+ return (await listLocalNodes()).find((n2) => n2.nodeId === nodeId) ?? null;
46646
+ }
46647
+ function readId(content) {
46648
+ const id = pickFrontmatter(frontmatterOf2(content) ?? "", "id");
46649
+ return id && NODE_ID_PATTERN.test(id) ? id : null;
46650
+ }
46651
+ function readTitle(content) {
46652
+ const fm = frontmatterOf2(content);
46653
+ const line2 = fm?.split("\n").find((l) => l.startsWith("title:"));
46654
+ if (!line2) return null;
46655
+ const raw = line2.slice("title:".length).trim();
46656
+ if (raw.startsWith('"')) {
46657
+ try {
46658
+ return String(JSON.parse(raw));
46659
+ } catch {
46660
+ return raw.replace(/^"|"$/g, "");
46661
+ }
46662
+ }
46663
+ return raw || null;
46664
+ }
46665
+ function readStatus(content) {
46666
+ return pickFrontmatter(frontmatterOf2(content) ?? "", "status") ?? "active";
46667
+ }
46668
+ function readGlobs(content) {
46669
+ const fm = frontmatterOf2(content) ?? "";
46670
+ const lines = fm.split("\n");
46671
+ const at = lines.findIndex((l) => /^file_globs:/.test(l));
46672
+ if (at < 0) return [];
46673
+ const inline = lines[at].slice("file_globs:".length).trim();
46674
+ if (inline.startsWith("[")) {
46675
+ try {
46676
+ const parsed = JSON.parse(inline);
46677
+ return Array.isArray(parsed) ? parsed.map(String) : [];
46678
+ } catch {
46679
+ return pickFrontmatterArray(fm, "file_globs");
46680
+ }
46681
+ }
46682
+ const out2 = [];
46683
+ for (let j = at + 1; j < lines.length && /^\s+-\s/.test(lines[j]); j++) {
46684
+ const item = lines[j].replace(/^\s+-\s+/, "").trim();
46685
+ let value = item;
46686
+ if (item.startsWith('"')) {
46687
+ try {
46688
+ value = String(JSON.parse(item));
46689
+ } catch {
46690
+ value = item.replace(/^"|"$/g, "");
46691
+ }
46692
+ }
46693
+ if (value) out2.push(value);
46694
+ }
46695
+ return out2;
46696
+ }
46697
+ function frontmatterOf2(content) {
46698
+ const m = content.match(/^---\n([\s\S]*?)\n---\n/);
46699
+ return m ? `${m[1]}
46700
+ ` : null;
46701
+ }
46702
+ function hasChanges(c) {
46703
+ return c.title !== void 0 || c.fileGlobs !== void 0 || c.status !== void 0 || c.body !== void 0;
46704
+ }
46705
+ function applyNodeChanges(content, changes) {
46706
+ const m = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
46707
+ if (!m) return null;
46708
+ let fm = m[1];
46709
+ let rest = m[2];
46710
+ if (changes.title !== void 0) {
46711
+ fm = setFrontmatterLine(fm, "title", JSON.stringify(changes.title));
46712
+ const restLines = rest.split("\n");
46713
+ const first = restLines.findIndex((l) => l.trim() !== "");
46714
+ if (first >= 0 && /^# /.test(restLines[first])) {
46715
+ restLines[first] = `# ${changes.title}`;
46716
+ rest = restLines.join("\n");
46717
+ }
46718
+ }
46719
+ if (changes.status !== void 0) fm = setFrontmatterLine(fm, "status", changes.status);
46720
+ if (changes.fileGlobs !== void 0) {
46721
+ const block = changes.fileGlobs.length > 0 ? "file_globs:\n" + changes.fileGlobs.map((g) => ` - ${JSON.stringify(g)}`).join("\n") : "file_globs: []";
46722
+ fm = /^file_globs:/m.test(fm) ? replaceFileGlobsBlock(fm, block) : `${fm}
46723
+ ${block}`;
46724
+ }
46725
+ if (changes.body !== void 0) rest = replaceBody(rest, changes.body);
46726
+ return `---
46727
+ ${fm}
46728
+ ---
46729
+ ${rest}`;
46730
+ }
46731
+ function setFrontmatterLine(fm, key, value) {
46732
+ const re = new RegExp(`^${key}:.*$`, "m");
46733
+ return re.test(fm) ? fm.replace(re, `${key}: ${value}`) : `${fm}
46734
+ ${key}: ${value}`;
46735
+ }
46736
+ function replaceBody(rest, body2) {
46737
+ const lines = rest.split("\n");
46738
+ let i2 = 0;
46739
+ while (i2 < lines.length && lines[i2].trim() === "") i2++;
46740
+ const heading = i2 < lines.length && /^# \S/.test(lines[i2]) ? lines[i2] : null;
46741
+ const related = lines.lastIndexOf("## Related");
46742
+ const relatedBlock = related > i2 ? lines.slice(related).join("\n").trimEnd() : null;
46743
+ const parts2 = [""];
46744
+ if (heading) parts2.push(heading, "");
46745
+ parts2.push(body2.trim());
46746
+ if (relatedBlock) parts2.push("", relatedBlock);
46747
+ return parts2.join("\n") + "\n";
46748
+ }
46749
+ async function fetchServiceNode(api, nodeId) {
46750
+ const res = await apiRequest({
46751
+ method: "GET",
46752
+ path: `/compound/memory/nodes/${encodeURIComponent(nodeId)}`,
46753
+ serviceUrl: api.serviceUrl,
46754
+ token: api.token,
46755
+ verbose: api.verbose,
46756
+ cmd: "memory-curate-read"
46757
+ });
46758
+ if (!res.ok) return { ok: false, error: res.error, notFound: res.http_status === 404 };
46759
+ return { ok: true, node: res.data };
46760
+ }
46761
+ async function setServiceStatus(api, nodeId, status) {
46762
+ const res = await apiRequest({
46763
+ method: "PATCH",
46764
+ path: `/compound/memory/nodes/${encodeURIComponent(nodeId)}`,
46765
+ serviceUrl: api.serviceUrl,
46766
+ token: api.token,
46767
+ body: { status },
46768
+ verbose: api.verbose,
46769
+ cmd: "memory-curate-status"
46770
+ });
46771
+ if (!res.ok) return { ok: false, error: res.error, notFound: res.http_status === 404 };
46772
+ if (res.data.status !== status) {
46773
+ return { ok: false, error: `the service answered with status "${String(res.data.status)}", not "${status}"` };
46774
+ }
46775
+ return { ok: true };
46776
+ }
46777
+ function uploadRefusal(content) {
46778
+ if (!readId(content) || !pickFrontmatter(frontmatterOf2(content) ?? "", "kind") || !readTitle(content)) {
46779
+ return "the file has no id, kind or title in its frontmatter, so the service would skip it";
46780
+ }
46781
+ const bodyLength = uploadBodyLength(content);
46782
+ if (bodyLength > SERVICE_BODY_MAX) {
46783
+ return `the body is ${bodyLength} characters; the service keeps ${SERVICE_BODY_MAX}, so it would be cut. Shorten it`;
46784
+ }
46785
+ const unstorable = unstorableField(content);
46786
+ if (unstorable) {
46787
+ return `the ${unstorable} would not be stored as written (it starts with a quote, is a bare number, true, false or null, or holds a line break). Change it`;
46788
+ }
46789
+ return null;
46790
+ }
46791
+ async function uploadNode(api, path, content, opts = {}) {
46792
+ const refusal = uploadRefusal(content);
46793
+ if (refusal) return { ok: false, error: refusal, unchanged: true };
46794
+ const nodeId = readId(content);
46795
+ const prior = await fetchServiceNode(api, nodeId);
46796
+ if (!prior.ok && !prior.notFound) return { ok: false, error: `could not read the node first: ${prior.error}` };
46797
+ const row2 = prior.ok ? prior.node : null;
46798
+ if (opts.expect !== void 0 && (!row2 || !sameEditableFields(row2, opts.expect))) {
46799
+ return { ok: false, stale: true, error: `the service copy of ${nodeId} is not the one expected, so it was not overwritten` };
46800
+ }
46801
+ const base = opts.base ?? content;
46802
+ if (row2 && !opts.force && serviceIsNewer(row2, base) && !sameEditableFields(row2, base)) {
46803
+ return {
46804
+ ok: false,
46805
+ stale: true,
46806
+ error: `the service has a newer, different version of ${nodeId} (updated ${String(row2.updated_at)}) than the file here. Run \`verity memory pull\` and edit again, or pass --force to overwrite it`
46807
+ };
46808
+ }
46809
+ const notes = [];
46810
+ if (row2) {
46811
+ const changed = ["source", "created_by", "confidence"].filter((k) => {
46812
+ const v = pickFrontmatter(frontmatterOf2(content) ?? "", k);
46813
+ return v !== null && v !== String(row2[k]);
46814
+ });
46815
+ if (changed.length > 0) notes.push(`${changed.join(", ")} ${changed.length > 1 ? "are" : "is"} kept from the service; the file's change to ${changed.length > 1 ? "them" : "it"} was not uploaded`);
46816
+ }
46817
+ const upload = row2 ? withRowFields(uploadableNodeContent(content), row2) : uploadableNodeContent(content);
46818
+ const res = await apiRequest({
46819
+ method: "POST",
46820
+ path: "/compound/memory/sync",
46821
+ serviceUrl: api.serviceUrl,
46822
+ token: api.token,
46823
+ body: { files: [{ path, content: upload }] },
46824
+ verbose: api.verbose,
46825
+ cmd: "memory-curate-upload"
46826
+ });
46827
+ if (!res.ok) return { ok: false, error: res.error };
46828
+ const back = await fetchServiceNode(api, nodeId);
46829
+ if (!back.ok) return { ok: false, error: `uploaded, but could not read it back: ${back.error}` };
46830
+ const mismatched = [];
46831
+ if (back.node.title !== readTitle(content)) mismatched.push("title");
46832
+ if ((back.node.status ?? "active") !== readStatus(content)) mismatched.push("status");
46833
+ const stored = Array.isArray(back.node.file_globs) ? back.node.file_globs.map(String) : [];
46834
+ if (!sameSet(stored, readGlobs(content))) mismatched.push("file_globs");
46835
+ if (back.node.body !== nodeBody(upload)) mismatched.push("body");
46836
+ return { ok: true, mismatched, final: adoptRowFields(content, back.node), notes };
46837
+ }
46838
+ function unstorableField(content) {
46839
+ const title = readTitle(content) ?? "";
46840
+ if (/["\\]/.test(title) && bareStorableValue(title) === null) return "title";
46841
+ if (/[\r\n]/.test(title)) return "title";
46842
+ for (const g of readGlobs(content)) {
46843
+ if (/["\\]/.test(g) && (g.startsWith('"') || g !== g.trim())) return "file_globs";
46844
+ if (/[\r\n]/.test(g)) return "file_globs";
46845
+ }
46846
+ return null;
46847
+ }
46848
+ function timeOf(value) {
46849
+ const t2 = typeof value === "string" ? Date.parse(value) : NaN;
46850
+ return Number.isFinite(t2) ? t2 : null;
46851
+ }
46852
+ function serviceIsNewer(row2, content) {
46853
+ const server = timeOf(row2.updated_at);
46854
+ const local = timeOf(pickFrontmatter(frontmatterOf2(content) ?? "", "updated_at"));
46855
+ return server !== null && local !== null && server - local > 1e3;
46856
+ }
46857
+ function sameEditableFields(row2, content) {
46858
+ const globs = Array.isArray(row2.file_globs) ? row2.file_globs.map(String) : [];
46859
+ return row2.title === readTitle(content) && (row2.status ?? "active") === readStatus(content) && sameSet(globs, readGlobs(content)) && row2.body === nodeBody(uploadableNodeContent(content));
46860
+ }
46861
+ function storableScalar(value) {
46862
+ if (/[\r\n]/.test(value)) return null;
46863
+ return /["\\]/.test(value) ? bareStorableValue(value) : JSON.stringify(value);
46864
+ }
46865
+ function withRowFields(content, row2) {
46866
+ const m = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
46867
+ if (!m) return content;
46868
+ const lines = [];
46869
+ const evidence2 = Array.isArray(row2.evidence_run_ids) ? row2.evidence_run_ids.filter((x) => typeof x === "string") : [];
46870
+ if (evidence2.length > 0) lines.push(`evidence_run_ids: ${JSON.stringify(evidence2)}`);
46871
+ if (typeof row2.pattern_ref === "string" && row2.pattern_ref) {
46872
+ const v = storableScalar(row2.pattern_ref);
46873
+ if (v !== null) lines.push(`pattern_ref: ${v}`);
46874
+ }
46875
+ if (typeof row2.source === "string") {
46876
+ const v = storableScalar(row2.source);
46877
+ if (v !== null) lines.push(`source: ${v}`);
46878
+ }
46879
+ if (typeof row2.created_by === "string") {
46880
+ const v = storableScalar(row2.created_by);
46881
+ if (v !== null) lines.push(`created_by: ${v}`);
46882
+ }
46883
+ if (typeof row2.confidence === "number" && Number.isFinite(row2.confidence)) lines.push(`confidence: ${row2.confidence}`);
46884
+ const replaced = new Set(lines.map((l) => l.slice(0, l.indexOf(":"))));
46885
+ const fm = m[1].split("\n").filter((l) => !replaced.has(l.slice(0, l.indexOf(":"))));
46886
+ return `---
46887
+ ${[...fm, ...lines].join("\n")}
46888
+ ---
46889
+ ${m[2]}`;
46890
+ }
46891
+ function adoptRowFields(content, row2) {
46892
+ const m = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
46893
+ if (!m) return content;
46894
+ let fm = m[1];
46895
+ const set = (key, value) => {
46896
+ const re = new RegExp(`^${key}:.*$`, "m");
46897
+ fm = re.test(fm) ? fm.replace(re, () => `${key}: ${value}`) : `${fm}
46898
+ ${key}: ${value}`;
46899
+ };
46900
+ if (typeof row2.updated_at === "string" && timeOf(row2.updated_at) !== null) set("updated_at", row2.updated_at);
46901
+ if (typeof row2.source === "string" && /^[a-z_]+$/.test(row2.source)) set("source", row2.source);
46902
+ if (typeof row2.created_by === "string" && !/[\r\n]/.test(row2.created_by)) set("created_by", /^[A-Za-z0-9@._/-]+$/.test(row2.created_by) ? row2.created_by : JSON.stringify(row2.created_by));
46903
+ if (typeof row2.confidence === "number" && Number.isFinite(row2.confidence)) set("confidence", String(row2.confidence));
46904
+ return `---
46905
+ ${fm}
46906
+ ---
46907
+ ${m[2]}`;
46908
+ }
46909
+ var SERVICE_BODY_MAX = 8192;
46910
+ function nodeBody(content) {
46911
+ const m = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
46912
+ return (m ? m[1] : content).trim();
46913
+ }
46914
+ function uploadBodyLength(content) {
46915
+ return nodeBody(uploadableNodeContent(content)).length;
46916
+ }
46917
+ function sameSet(a, b) {
46918
+ return a.length === b.length && a.every((x) => b.includes(x));
46919
+ }
46920
+ async function recordSynced(path, content) {
46921
+ const entry = /* @__PURE__ */ new Map([[path, hashNodeContent(content)]]);
46922
+ await markNodesSynced(entry);
46923
+ await recordServedFiles(entry);
46924
+ }
46925
+ var MAX_TRASH_BYTES = 4 * MAX_NODE_FILE_BYTES2;
46926
+ async function readTrash(nodeId) {
46927
+ if (!NODE_ID_PATTERN.test(nodeId)) return null;
46928
+ const text3 = readInRepo((0, import_node_path72.join)(trashDir(), `${nodeId}.json`), MAX_TRASH_BYTES);
46929
+ if (text3 === null) return null;
46930
+ try {
46931
+ const parsed = JSON.parse(text3);
46932
+ return parsed?.node_id === nodeId ? parsed : null;
46933
+ } catch {
46934
+ return null;
46935
+ }
46936
+ }
46937
+ async function writeTrash(record2) {
46938
+ const ok = await writeFileAtomicInTree((0, import_node_path72.join)(trashDir(), `${record2.node_id}.json`), JSON.stringify(record2, null, 2) + "\n");
46939
+ if (!ok) throw new Error(".verity/.memory-trash/ is a link, or a folder outside the repository");
46940
+ }
46941
+ async function removeTrash(nodeId) {
46942
+ if (!(0, import_node_fs85.existsSync)(trashDir()) || !dirInRepo(trashDir())) return;
46943
+ try {
46944
+ await (0, import_promises18.rm)((0, import_node_path72.join)(trashDir(), `${nodeId}.json`), { force: true });
46945
+ } catch {
46946
+ }
46947
+ }
46948
+ async function editNode(api, node, next, opts = {}) {
46949
+ const refusal = uploadRefusal(next);
46950
+ if (refusal) return { ok: false, error: refusal, unchanged: true };
46951
+ if (next !== node.content && !await writeNodeFile(node.path, next)) {
46952
+ return { ok: false, error: `could not write .verity/memory/${node.path} (a link, or a folder outside the repository)`, unchanged: true };
46953
+ }
46954
+ const up = await uploadNode(api, node.path, next, { force: opts.force, base: opts.base ?? node.content, expect: opts.expect });
46955
+ if (!up.ok) {
46956
+ if (up.stale && next !== node.content) await writeNodeFile(node.path, node.content);
46957
+ return up;
46958
+ }
46959
+ if (up.mismatched.length > 0) return up;
46960
+ const final = up.final !== next && await writeNodeFile(node.path, up.final) ? up.final : next;
46961
+ await recordSynced(node.path, final);
46962
+ await refreshMemoryIndex();
46963
+ await appendMemoryLog([`- ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)} \u2014 Uploaded ${node.nodeId} by hand (${node.path})`]);
46964
+ return { ...up, final };
46965
+ }
46966
+ async function deleteNode(api, nodeId, opts = {}) {
46967
+ const local = await findLocalNode(nodeId);
46968
+ const status = opts.status ?? "archived";
46969
+ const previous = await readTrash(nodeId);
46970
+ try {
46971
+ await writeTrash({
46972
+ node_id: nodeId,
46973
+ path: local?.path ?? null,
46974
+ content: local?.content ?? null,
46975
+ status_before: local ? readStatus(local.content) : "active",
46976
+ reason: opts.reason ?? null,
46977
+ deleted_at: (/* @__PURE__ */ new Date()).toISOString(),
46978
+ via: opts.via ?? "delete"
46979
+ });
46980
+ } catch (err2) {
46981
+ return { ok: false, error: `could not save the local record in .verity/.memory-trash/ first (${err2.message}); nothing was changed` };
46982
+ }
46983
+ const set = await setServiceStatus(api, nodeId, status);
46984
+ if (!set.ok && !(set.notFound && local)) {
46985
+ if (previous) await writeTrash(previous).catch(() => {
46986
+ });
46987
+ else await removeTrash(nodeId);
46988
+ return set;
46989
+ }
46990
+ const localOnly = !set.ok;
46991
+ let fileKept = false;
46992
+ if (local) {
46993
+ if (await removeNodeFile(local.path)) {
46994
+ await forgetSyncedNodes([local.path]);
46995
+ await forgetServedFiles([local.path]);
46996
+ await refreshMemoryIndex();
46997
+ } else {
46998
+ fileKept = true;
46999
+ }
47000
+ }
47001
+ const why = opts.reason ? `: ${opts.reason}` : "";
47002
+ await appendMemoryLog([`- ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)} \u2014 ${localOnly ? "Removed local-only" : `Set ${status}`} ${nodeId}${local ? ` (${local.path})` : ""} via ${opts.via ?? "delete"}${why}`]);
47003
+ return { ok: true, path: local?.path ?? null, localOnly, fileKept };
47004
+ }
47005
+ async function restoreNode(api, nodeId, fallback) {
47006
+ const trash = await readTrash(nodeId);
47007
+ const path = trash?.path ?? fallback?.path ?? null;
47008
+ const content = trash?.content ?? fallback?.content ?? null;
47009
+ const set = await setServiceStatus(api, nodeId, "active");
47010
+ const localOnly = !set.ok && !!set.notFound && path !== null && content !== null;
47011
+ if (!set.ok && !localOnly) return set;
47012
+ let restored = false;
47013
+ if (path && content !== null) {
47014
+ const next = readStatus(content) === "active" ? content : applyNodeChanges(content, { status: "active" }) ?? content;
47015
+ if (!await writeNodeFile(path, next)) {
47016
+ return { ok: false, error: `${localOnly ? "the service has no such node, and" : "active on the service again, but"} .verity/memory/${path} could not be written (a link, or a folder outside the repository). The text is kept in .verity/.memory-trash/${nodeId}.json` };
47017
+ }
47018
+ if (!localOnly) await recordSynced(path, next);
47019
+ restored = true;
47020
+ }
47021
+ await removeTrash(nodeId);
47022
+ await refreshMemoryIndex();
47023
+ await appendMemoryLog([`- ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)} \u2014 Restored ${nodeId}${path ? ` (${path})` : ""}${localOnly ? " (local only)" : ""}`]);
47024
+ return { ok: true, path: restored ? path : null, fromTrash: restored, localOnly };
47025
+ }
47026
+
47027
+ // src/commands/memory-curate.ts
47028
+ function cleanReason(raw) {
47029
+ if (raw === void 0) return { ok: true, reason: null };
47030
+ const reason = String(raw).replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim();
47031
+ if (reason.length === 0 || reason.length > 500) return { ok: false, error: "--reason is 1 to 500 characters." };
47032
+ return { ok: true, reason };
47033
+ }
47034
+ async function resolveApi(program2) {
47035
+ const globals = program2.opts();
47036
+ const tokenResult = await resolveToken(globals.token);
47037
+ if (!tokenResult.ok) {
47038
+ printError(tokenResult.error);
47039
+ process.exit(1);
47040
+ }
47041
+ const urlResult = await resolveServiceUrl(globals.serviceUrl);
47042
+ if (!urlResult.ok) {
47043
+ printError(urlResult.error);
47044
+ process.exit(1);
47045
+ }
47046
+ return { serviceUrl: urlResult.data, token: tokenResult.data.token, verbose: globals.verbose };
47047
+ }
47048
+ function refuseLinkedMemoryDir() {
47049
+ if (memoryDirEscapesRepo()) {
47050
+ printError(".verity/memory is a link (or under one) that leads out of this repository. Refusing to read or change it.");
47051
+ process.exit(1);
47052
+ }
47053
+ }
47054
+ function checkNodeId(nodeId) {
47055
+ if (!NODE_ID_PATTERN.test(nodeId)) {
47056
+ printError('A node id looks like n123-some-slug. `verity memory ask "<topic>"` and .verity/memory/index.md list them.');
47057
+ process.exit(1);
47058
+ }
47059
+ }
47060
+ function registerMemoryCurationCommands(memory2, program2) {
47061
+ memory2.command("edit <node-id>").description("Change a knowledge node and upload it now. With no change option, uploads the file as it is on disk").option("--title <text>", "New title (up to 200 characters)").option("--file-globs <glob...>", "Replace the files this node is about. Each glob must match a file in the repository").option("--status <status>", `New status: ${SETTABLE_STATUSES.join(", ")}`).option("--body <text>", "Replace the body; - reads it from stdin").option("--force", "Upload even when the service has a newer, different version of the node (it is overwritten)").action(async (nodeId, opts) => {
47062
+ checkNodeId(nodeId);
47063
+ const body2 = opts.body === void 0 ? null : readTextInput(opts.body, "--body");
47064
+ if (body2 && !body2.ok) {
47065
+ printError(body2.error);
47066
+ process.exit(1);
47067
+ }
47068
+ try {
47069
+ process.chdir(repoRoot());
47070
+ } catch {
47071
+ }
47072
+ refuseLinkedMemoryDir();
47073
+ const changes = {};
47074
+ if (opts.title !== void 0) {
47075
+ const t2 = checkExplicitTitle(opts.title);
47076
+ if (!t2.ok) {
47077
+ printError(t2.error);
47078
+ process.exit(1);
47079
+ }
47080
+ changes.title = t2.title;
47081
+ }
47082
+ if (opts.status !== void 0) {
47083
+ if (!SETTABLE_STATUSES.includes(opts.status)) {
47084
+ printError(`--status is one of: ${SETTABLE_STATUSES.join(", ")}.`);
47085
+ process.exit(1);
47086
+ }
47087
+ changes.status = opts.status;
47088
+ }
47089
+ if (opts.fileGlobs !== void 0) {
47090
+ const globs = [...new Set(opts.fileGlobs.map((g) => g.trim()).filter(Boolean))];
47091
+ const tree2 = listTrackedFiles();
47092
+ const unmatched = globs.filter((g) => !anyPathMatches(g, tree2));
47093
+ if (globs.length === 0 || unmatched.length > 0) {
47094
+ printError(globs.length === 0 ? "--file-globs needs at least one glob. A node with none never surfaces in a review." : `No file in this repository matches: ${unmatched.join(", ")}. Paths are relative to the repository root.`);
47095
+ process.exit(1);
47096
+ }
47097
+ changes.fileGlobs = globs;
47098
+ }
47099
+ if (body2) changes.body = body2.text;
47100
+ const api = await resolveApi(program2);
47101
+ let node = await findLocalNode(nodeId);
47102
+ if (!node) {
47103
+ try {
47104
+ await pullRepoMemory({ serviceUrl: api.serviceUrl, token: api.token, verbose: api.verbose });
47105
+ } catch {
47106
+ }
47107
+ node = await findLocalNode(nodeId);
47108
+ }
47109
+ if (!node) {
47110
+ printError(`No node ${nodeId} in .verity/memory/. \`verity memory pull\` fetches the graph; \`verity memory ask "<topic>"\` finds node ids.`);
47111
+ process.exit(1);
47112
+ }
47113
+ const next = hasChanges(changes) ? applyNodeChanges(node.content, changes) : node.content;
47114
+ if (next === null) {
47115
+ printError(`.verity/memory/${terminalText(node.path)} has no frontmatter to change.`);
47116
+ process.exit(1);
47117
+ }
47118
+ const bodyLength = uploadBodyLength(next);
47119
+ if (bodyLength > SERVICE_BODY_MAX) {
47120
+ printError(`The body is ${bodyLength} characters; the service keeps ${SERVICE_BODY_MAX}, so it would be cut. Shorten it.`);
47121
+ process.exit(1);
47122
+ }
47123
+ if (changes.status !== void 0 && Object.keys(changes).length === 1) {
47124
+ const set = await setServiceStatus(api, nodeId, changes.status);
47125
+ if (!set.ok) {
47126
+ printError(`Could not change the status of ${nodeId}: ${set.error}`);
47127
+ process.exit(1);
47128
+ }
47129
+ const written = await writeNodeFile(node.path, next);
47130
+ if (written) {
47131
+ const entry = /* @__PURE__ */ new Map([[node.path, hashNodeContent(next)]]);
47132
+ await markNodesSynced(entry);
47133
+ await recordServedFiles(entry);
47134
+ await refreshMemoryIndex();
47135
+ }
47136
+ printInfo(`${nodeId} is ${changes.status} on the service (.verity/memory/${terminalText(node.path)}).`);
47137
+ if (!written) {
47138
+ printError(` But .verity/memory/${terminalText(node.path)} could not be written (a link, or a folder outside the repository). Its status line still says ${readStatus(node.content)}.`);
47139
+ process.exit(1);
47140
+ }
47141
+ if (changes.status !== "active") printInfo(" It no longer surfaces in reviews. `verity memory delete` also removes the file here.");
47142
+ process.exit(0);
47143
+ }
47144
+ const result = await editNode(api, node, next, { force: !!opts.force });
47145
+ if (!result.ok) {
47146
+ printError(`Could not upload ${nodeId}: ${result.error}`);
47147
+ if (result.stale || result.unchanged) printWarn(" Nothing was changed, here or on the service.");
47148
+ else if (next !== node.content) printWarn(` The change is saved in .verity/memory/${terminalText(node.path)}. Run the same command again to upload it.`);
47149
+ process.exit(1);
47150
+ }
47151
+ if (result.mismatched.length > 0) {
47152
+ printError(`The service did not take the change to ${nodeId}: ${result.mismatched.join(", ")} differ from the file.`);
47153
+ process.exit(1);
47154
+ }
47155
+ const changed = Object.keys(changes);
47156
+ printInfo(changed.length > 0 ? `Updated ${nodeId} (${changed.map((k) => k === "fileGlobs" ? "file_globs" : k).join(", ")}) and uploaded it \u2192 .verity/memory/${terminalText(node.path)}` : `Uploaded ${nodeId} as it is on disk \u2192 .verity/memory/${terminalText(node.path)}`);
47157
+ printInfo(" The service holds this version now. The next analysis will not send it again.");
47158
+ for (const note of result.notes) printWarn(` Note: ${note}.`);
47159
+ process.exit(0);
47160
+ });
47161
+ memory2.command("delete <node-id>").description("Archive a knowledge node on the service and remove its file here. `verity memory restore` undoes it").option("--reason <text>", "Why the node is wrong or no longer applies (1\u2013500 characters, kept on this machine); - reads it from stdin").action(async (nodeId, opts) => {
47162
+ checkNodeId(nodeId);
47163
+ const rawReason = opts.reason === void 0 ? null : readTextInput(opts.reason, "--reason");
47164
+ if (rawReason && !rawReason.ok) {
47165
+ printError(rawReason.error);
47166
+ process.exit(1);
47167
+ }
47168
+ const reason = cleanReason(rawReason ? rawReason.text : void 0);
47169
+ if (!reason.ok) {
47170
+ printError(reason.error);
47171
+ process.exit(1);
47172
+ }
47173
+ try {
47174
+ process.chdir(repoRoot());
47175
+ } catch {
47176
+ }
47177
+ refuseLinkedMemoryDir();
47178
+ const api = await resolveApi(program2);
47179
+ const result = await deleteNode(api, nodeId, { reason: reason.reason, via: "delete" });
47180
+ if (!result.ok) {
47181
+ printError(result.notFound ? `No node ${nodeId} on the service, and no file for it here.` : `Could not archive ${nodeId}: ${result.error}`);
47182
+ process.exit(1);
47183
+ }
47184
+ if (result.localOnly) {
47185
+ printInfo(`The service has no node ${nodeId}. Removed the file that existed only here (.verity/memory/${terminalText(result.path)}).`);
47186
+ } else {
47187
+ printInfo(`Archived ${nodeId} on the service${result.path ? ` and removed .verity/memory/${terminalText(result.path)}` : ""}.`);
47188
+ printInfo(" It no longer surfaces in reviews, here or for your team.");
47189
+ }
47190
+ if (result.fileKept) {
47191
+ printWarn(` But .verity/memory/${terminalText(result.path)} could not be removed here. Delete it by hand; the service no longer serves it.`);
47192
+ }
47193
+ if (reason.reason) printInfo(" The reason is kept in .verity/.memory-trash/ and .verity/memory/log.md on this machine.");
47194
+ printInfo(` Undo: verity memory restore ${nodeId}`);
47195
+ process.exit(0);
47196
+ });
47197
+ memory2.command("restore <node-id>").description("Undo `verity memory delete`: make the node active on the service again and put its file back").action(async (nodeId) => {
47198
+ checkNodeId(nodeId);
47199
+ try {
47200
+ process.chdir(repoRoot());
47201
+ } catch {
47202
+ }
47203
+ refuseLinkedMemoryDir();
47204
+ const api = await resolveApi(program2);
47205
+ const trash = await readTrash(nodeId);
47206
+ const result = await restoreNode(api, nodeId);
47207
+ if (!result.ok) {
47208
+ printError(result.notFound ? `No node ${nodeId} on the service.` : `Could not restore ${nodeId}: ${result.error}`);
47209
+ process.exit(1);
47210
+ }
47211
+ if (result.fromTrash) {
47212
+ printInfo(result.localOnly ? `Restored ${nodeId} here: back in .verity/memory/${terminalText(result.path)}. The service never had it, so it is still only on this machine.` : `Restored ${nodeId}: active on the service, and back in .verity/memory/${terminalText(result.path)}.`);
47213
+ process.exit(0);
47214
+ }
47215
+ let fetched = false;
47216
+ try {
47217
+ const pull = await pullRepoMemory({ serviceUrl: api.serviceUrl, token: api.token, verbose: api.verbose, force: true });
47218
+ fetched = pull.ok && !!await findLocalNode(nodeId);
47219
+ } catch {
47220
+ }
47221
+ printInfo(fetched ? `Restored ${nodeId}: active on the service, and pulled back into .verity/memory/.` : `Restored ${nodeId} on the service. Its file arrives with the next \`verity memory pull\`.`);
47222
+ if (trash?.reason) printInfo(` It was deleted with the reason: ${terminalText(trash.reason, 500)}`);
47223
+ process.exit(0);
47224
+ });
47225
+ }
47226
+
47227
+ // src/commands/memory-ask.ts
47228
+ var import_node_fs86 = require("node:fs");
47229
+
47230
+ // src/lib/memory-ask.ts
47231
+ var K1 = 1.2;
47232
+ var B = 0.75;
47233
+ var TITLE_WEIGHT = 3;
47234
+ var PATH_BONUS = 2;
47235
+ var RELATIVE_FLOOR = 0.25;
47236
+ var ANSWER_LINES = 3;
47237
+ var MIN_SENTENCE = 20;
47238
+ function terms(text3) {
47239
+ return text3.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOPWORDS.has(w)).map(stem);
47240
+ }
47241
+ function stem(w) {
47242
+ if (w.length > 5 && w.endsWith("ies")) return `${w.slice(0, -3)}y`;
47243
+ if (w.length > 5 && w.endsWith("ing")) return w.slice(0, -3);
47244
+ if (w.length > 4 && w.endsWith("ed")) return w.slice(0, -2);
47245
+ if (w.length > 3 && w.endsWith("s") && !w.endsWith("ss")) return w.slice(0, -1);
47246
+ return w;
47247
+ }
47248
+ function globTerms(globs) {
47249
+ return terms(globs.join(" ").replace(/[/*._-]+/g, " "));
47250
+ }
47251
+ function askMemory(question, nodes, opts = {}) {
47252
+ const limit = Math.max(1, opts.limit ?? 5);
47253
+ const qTerms = [...new Set(terms(question))];
47254
+ const empty = { question, terms: qTerms, hits: [], answer: [] };
47255
+ if (nodes.length === 0 || qTerms.length === 0 && !opts.citedPaths?.length) return empty;
47256
+ const docs = nodes.map((n2) => {
47257
+ const words = [
47258
+ ...Array.from({ length: TITLE_WEIGHT }, () => terms(n2.title)).flat(),
47259
+ ...terms(n2.body),
47260
+ ...globTerms(n2.fileGlobs)
47261
+ ];
47262
+ const tf = /* @__PURE__ */ new Map();
47263
+ for (const w of words) tf.set(w, (tf.get(w) ?? 0) + 1);
47264
+ return { node: n2, tf, len: words.length };
47265
+ });
47266
+ const avgLen = docs.reduce((s, d) => s + d.len, 0) / docs.length || 1;
47267
+ const df = /* @__PURE__ */ new Map();
47268
+ for (const t2 of qTerms) df.set(t2, docs.filter((d) => d.tf.has(t2)).length);
47269
+ const idf = (t2) => {
47270
+ const n2 = df.get(t2) ?? 0;
47271
+ return Math.log(1 + (docs.length - n2 + 0.5) / (n2 + 0.5));
47272
+ };
47273
+ const cited = (opts.citedPaths ?? []).filter(Boolean);
47274
+ const scored = docs.map((d) => {
47275
+ let score = 0;
47276
+ for (const t2 of qTerms) {
47277
+ const f = d.tf.get(t2) ?? 0;
47278
+ if (f === 0) continue;
47279
+ score += idf(t2) * (f * (K1 + 1)) / (f + K1 * (1 - B + B * d.len / avgLen));
47280
+ }
47281
+ if (cited.length > 0 && d.node.fileGlobs.some((g) => matchesAny(g, cited))) score += PATH_BONUS;
47282
+ return { node: d.node, score };
47283
+ }).filter((s) => s.score > 0);
47284
+ scored.sort((a, b) => b.score - a.score || a.node.nodeId.localeCompare(b.node.nodeId));
47285
+ const top = scored[0]?.score ?? 0;
47286
+ const hits = scored.filter((s) => s.score >= top * RELATIVE_FLOOR).slice(0, limit).map((s) => ({
47287
+ ...s.node,
47288
+ score: Math.round(s.score * 100) / 100,
47289
+ excerpt: bestSentences(s.node.body, qTerms, idf, 2)
47290
+ }));
47291
+ return { question, terms: qTerms, hits, answer: composeAnswer(hits, qTerms, idf) };
47292
+ }
47293
+ function matchesAny(glob, paths) {
47294
+ let re;
47295
+ try {
47296
+ re = globToRegex(glob);
47297
+ } catch {
47298
+ return false;
47299
+ }
47300
+ return paths.some((p) => re.test(p) || p === glob || glob.startsWith(`${p}/`));
47301
+ }
47302
+ function sentences(body2) {
47303
+ const prose = body2.replace(/```[\s\S]*?```/g, " ").split("\n").filter((l) => !/^\s*(#|- \[\[|\*\*[^*]+:\*\*\s*$|>)/.test(l)).join("\n");
47304
+ return prose.split(/(?<=[.!?])\s+(?=[A-Z0-9`"'(\[])|\n{2,}/).map((s) => s.replace(/\s+/g, " ").trim()).filter((s) => s.length >= MIN_SENTENCE);
47305
+ }
47306
+ function sentenceScore(sentence, qTerms, idf) {
47307
+ const own = new Set(terms(sentence));
47308
+ let s = 0;
47309
+ for (const t2 of qTerms) if (own.has(t2)) s += idf(t2);
47310
+ return s;
47311
+ }
47312
+ function bestSentences(body2, qTerms, idf, n2) {
47313
+ return sentences(body2).map((text3, i2) => ({ text: text3, i: i2, s: sentenceScore(text3, qTerms, idf) })).filter((x) => x.s > 0).sort((a, b) => b.s - a.s || a.i - b.i).slice(0, n2).map((x) => x.text);
47314
+ }
47315
+ function composeAnswer(hits, qTerms, idf) {
47316
+ const out2 = [];
47317
+ const seen = /* @__PURE__ */ new Set();
47318
+ const pool = hits.slice(0, 3);
47319
+ for (const round of [0, 1]) {
47320
+ for (const h of pool) {
47321
+ const text3 = h.excerpt[round];
47322
+ if (!text3 || seen.has(text3) || sentenceScore(text3, qTerms, idf) === 0) continue;
47323
+ seen.add(text3);
47324
+ out2.push({ text: text3, nodeId: h.nodeId });
47325
+ if (out2.length >= ANSWER_LINES) return out2;
47326
+ }
47327
+ }
47328
+ return out2;
47329
+ }
47330
+
47331
+ // src/commands/memory-ask.ts
47332
+ var BODY_JSON_MAX = 4e3;
47333
+ async function loadAskNodes(opts = {}) {
47334
+ const out2 = [];
47335
+ for (const n2 of await listLocalNodes()) {
47336
+ const status = readStatus(n2.content);
47337
+ if (!opts.includeInactive && status !== "active") continue;
47338
+ const fm = n2.content.match(/^---\n([\s\S]*?)\n---\n/);
47339
+ out2.push({
47340
+ nodeId: n2.nodeId,
47341
+ path: n2.path,
47342
+ kind: pickFrontmatter(fm?.[1] ?? "", "kind") ?? n2.path.split("/")[0],
47343
+ title: readTitle(n2.content) ?? n2.nodeId,
47344
+ body: n2.content.slice(fm ? fm[0].length : 0).trim(),
47345
+ fileGlobs: readGlobs(n2.content),
47346
+ status,
47347
+ scope: "repo"
47348
+ });
47349
+ }
47350
+ try {
47351
+ for (const c of await readOrgMirror()) {
47352
+ out2.push({
47353
+ nodeId: c.tag_id,
47354
+ path: null,
47355
+ kind: c.kind,
47356
+ title: c.title,
47357
+ body: c.body,
47358
+ fileGlobs: [],
47359
+ status: "active",
47360
+ scope: "org",
47361
+ origin: c.origin
47362
+ });
47363
+ }
47364
+ } catch {
47365
+ }
47366
+ return out2;
47367
+ }
47368
+ function printText(result, full) {
47369
+ const lines = [];
47370
+ if (result.answer.length > 0) {
47371
+ lines.push("Answer, quoted from the nodes (read the nodes before you rely on it):");
47372
+ for (const a of result.answer) lines.push(` - ${a.text} [${a.nodeId}]`);
47373
+ lines.push("");
47374
+ }
47375
+ lines.push(`${result.hits.length} node(s), best first:`);
47376
+ result.hits.forEach((h, i2) => {
47377
+ const where2 = h.scope === "org" ? `org claim ${h.origin ?? ""}`.trim() : `.verity/memory/${h.path}`;
47378
+ const status = h.status !== "active" ? ` \xB7 ${h.status}` : "";
47379
+ lines.push("");
47380
+ lines.push(`${i2 + 1}. ${h.nodeId} \xB7 ${h.kind}${status} \xB7 score ${h.score}`);
47381
+ lines.push(` ${h.title}`);
47382
+ lines.push(` ${where2}${h.fileGlobs.length > 0 ? ` \xB7 scope: ${h.fileGlobs.slice(0, 3).join(", ")}` : ""}`);
47383
+ if (full) {
47384
+ for (const l of h.body.split("\n")) lines.push(` ${l}`);
47385
+ } else {
47386
+ for (const e of h.excerpt) lines.push(` "${e}"`);
47387
+ }
47388
+ });
47389
+ console.log(terminalText(lines.join("\n"), Number.POSITIVE_INFINITY));
47390
+ }
47391
+ function registerMemoryAskCommand(memory2) {
47392
+ memory2.command("ask <question...>").alias("recall").description("Ask the knowledge graph a question. Prints the matching nodes with their ids, and the sentences that answer it. Local: nothing is sent").option("--limit <n>", "At most this many nodes (default 5)", "5").option("--json", "Print JSON: the quoted answer, and each node with its id, path, score and body").option("--full", "Print each node's whole body instead of the matching sentences").option("--all", "Include archived and superseded nodes still on disk").action(async (words, opts) => {
47393
+ const fromStdin = words.length === 1 && words[0] === "-";
47394
+ const stdin = fromStdin ? readTextInput("-", "the question") : null;
47395
+ if (stdin && !stdin.ok) {
47396
+ printError(stdin.error);
47397
+ process.exit(1);
47398
+ }
47399
+ const question = (stdin ? stdin.text ?? "" : words.join(" ")).replace(/\s+/g, " ").trim();
47400
+ const limit = Number.parseInt(opts.limit, 10);
47401
+ if (!question) {
47402
+ printError('Ask a question: verity memory ask "why do we \u2026"');
47403
+ process.exit(1);
47404
+ }
47405
+ if (!Number.isInteger(limit) || limit < 1 || limit > 50) {
47406
+ printError("--limit is 1 to 50.");
47407
+ process.exit(1);
47408
+ }
47409
+ try {
47410
+ process.chdir(repoRoot());
47411
+ } catch {
47412
+ }
47413
+ refuseLinkedMemoryDir();
47414
+ const nodes = await loadAskNodes({ includeInactive: !!opts.all });
47415
+ let tree2 = [];
47416
+ try {
47417
+ tree2 = listTrackedFiles();
47418
+ } catch {
47419
+ }
47420
+ const result = askMemory(question, nodes, { limit, citedPaths: deriveFileGlobs(question, tree2) });
47421
+ if (opts.json) {
47422
+ console.log(JSON.stringify({
47423
+ question: result.question,
47424
+ answer: result.answer.map((a) => ({ text: a.text, node_id: a.nodeId })),
47425
+ nodes: result.hits.map((h) => ({
47426
+ node_id: h.nodeId,
47427
+ kind: h.kind,
47428
+ title: h.title,
47429
+ status: h.status,
47430
+ scope: h.scope,
47431
+ ...h.scope === "org" ? { origin: h.origin ?? null } : { path: `.verity/memory/${h.path}` },
47432
+ file_globs: h.fileGlobs,
47433
+ score: h.score,
47434
+ excerpt: h.excerpt,
47435
+ body: h.body.length > BODY_JSON_MAX ? `${h.body.slice(0, BODY_JSON_MAX)}\u2026` : h.body
47436
+ }))
47437
+ }, null, 2));
47438
+ process.exit(0);
47439
+ }
47440
+ if (nodes.length === 0) {
47441
+ const hasGraph = (0, import_node_fs86.existsSync)(projectPath(`${VERITY_DIR}/memory`));
47442
+ console.log(hasGraph ? "The knowledge graph here has no nodes yet." : "No knowledge graph in this repository yet. `verity memory pull` fetches it.");
47443
+ process.exit(0);
47444
+ }
47445
+ if (result.hits.length === 0) {
47446
+ console.log(`No node matches "${terminalText(question, 200)}". Try other words, or \`verity memory pull\` if the graph here is old.`);
47447
+ process.exit(0);
47448
+ }
47449
+ printText(result, !!opts.full);
47450
+ process.exit(0);
47451
+ });
47452
+ }
47453
+
47454
+ // src/commands/memory-dream.ts
47455
+ var import_promises19 = require("node:fs/promises");
47456
+ var import_node_fs87 = require("node:fs");
47457
+ var import_node_path73 = require("node:path");
47458
+
47459
+ // src/lib/memory-dream.ts
47460
+ var MERGE_AT = 0.6;
47461
+ var REVIEW_AT = 0.4;
47462
+ var REVIEW_MAX = 40;
47463
+ function toDreamNode(file) {
47464
+ if (readStatus(file.content) !== "active") return null;
47465
+ const m = file.content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
47466
+ const fm = m?.[1] ?? "";
47467
+ const supersededBy = pickFrontmatter(fm, "superseded_by");
47468
+ const confidence = Number.parseFloat(pickFrontmatter(fm, "confidence") ?? "");
47469
+ return {
47470
+ nodeId: file.nodeId,
47471
+ path: file.path,
47472
+ kind: pickFrontmatter(fm, "kind") ?? file.path.split("/")[0],
47473
+ title: readTitle(file.content) ?? file.nodeId,
47474
+ body: ownText(m?.[2] ?? ""),
47475
+ fileGlobs: readGlobs(file.content),
47476
+ source: pickFrontmatter(fm, "source") ?? "extractor",
47477
+ confidence: Number.isFinite(confidence) ? confidence : 0.5,
47478
+ createdAt: pickFrontmatter(fm, "created_at") ?? "",
47479
+ createdBy: pickFrontmatter(fm, "created_by") ?? "",
47480
+ supersededBy: supersededBy && supersededBy !== "null" ? supersededBy : null,
47481
+ contradicts: pickFrontmatterArray(fm, "contradicts")
47482
+ };
47483
+ }
47484
+ function ownText(rest) {
47485
+ return rest.replace(/^\s*# .*\n/, "").replace(/\n## Related\n[\s\S]*$/, "").trim();
47486
+ }
47487
+ var STUB_SENTENCES = [
47488
+ /This project uses \*\*[^*\n]+\*\*\.?/g,
47489
+ /Capture gotchas and integration-specific decisions here via `\/verity-reflect` as they come up during work\.?/g
47490
+ ];
47491
+ function isPlaceholder(n2) {
47492
+ let own = n2.body;
47493
+ let template = false;
47494
+ for (const re of STUB_SENTENCES) {
47495
+ const stripped = own.replace(re, " ");
47496
+ if (stripped !== own) template = true;
47497
+ own = stripped;
47498
+ }
47499
+ const ownTerms = terms(own);
47500
+ if (template) return ownTerms.length === 0;
47501
+ return ownTerms.length === 0 && terms(n2.title).length === 0;
47502
+ }
47503
+ function similarity(a, b) {
47504
+ const all = jaccard(new Set(terms(`${a.title} ${a.body}`)), new Set(terms(`${b.title} ${b.body}`)));
47505
+ const title = jaccard(new Set(terms(a.title)), new Set(terms(b.title)));
47506
+ return Math.max(all, (all + title) / 2);
47507
+ }
47508
+ function jaccard(a, b) {
47509
+ if (a.size === 0 || b.size === 0) return 0;
47510
+ let inter = 0;
47511
+ for (const x of a) if (b.has(x)) inter++;
47512
+ return inter / (a.size + b.size - inter);
47513
+ }
47514
+ var SOURCE_RANK = { user: 3, agent: 2, extractor: 1 };
47515
+ function compareStrength(a, b) {
47516
+ return (SOURCE_RANK[b.source] ?? 0) - (SOURCE_RANK[a.source] ?? 0) || b.confidence - a.confidence || b.fileGlobs.length - a.fileGlobs.length || a.createdAt.localeCompare(b.createdAt) || a.nodeId.localeCompare(b.nodeId);
47517
+ }
47518
+ var isConfirmed = (n2) => n2.source === "user";
47519
+ var pairKey = (a, b) => [a, b].sort().join("|");
47520
+ var NEGATION = /\b(?:not|no|never|none|nor|neither|without|avoid|stop|forbid(?:den)?|disallow(?:ed)?|cannot|can't|cant|don't|dont|doesn't|doesnt|didn't|isn't|aren't|wasn't|won't|wont|shouldn't|mustn't|needn't)\b/g;
47521
+ function negations(text3) {
47522
+ return (text3.toLowerCase().replace(/[’`]/g, "'").match(NEGATION) ?? []).length;
47523
+ }
47524
+ function negatedTerms(text3) {
47525
+ const lower = text3.toLowerCase().replace(/[’`]/g, "'");
47526
+ const out2 = [];
47527
+ for (const m of lower.matchAll(NEGATION)) {
47528
+ const after = lower.slice((m.index ?? 0) + m[0].length, (m.index ?? 0) + m[0].length + 60);
47529
+ out2.push(terms(after)[0] ?? "\u2205");
47530
+ }
47531
+ return out2.sort();
47532
+ }
47533
+ function samePolarity(a, b) {
47534
+ if (negations(a.title) > 0 !== negations(b.title) > 0) return false;
47535
+ const na = negatedTerms(`${a.title}. ${a.body}`);
47536
+ const nb = negatedTerms(`${b.title}. ${b.body}`);
47537
+ return na.length === nb.length && na.every((t2, i2) => t2 === nb[i2]);
47538
+ }
47539
+ var round2 = (x) => Math.round(x * 100) / 100;
47540
+ function planDream(input, opts = {}) {
47541
+ const mergeAt = opts.mergeAt ?? MERGE_AT;
47542
+ const reviewAt = Math.min(opts.reviewAt ?? REVIEW_AT, mergeAt);
47543
+ const byId = new Map(input.map((n2) => [n2.nodeId, n2]));
47544
+ const changes = [];
47545
+ const review = [];
47546
+ const touched = /* @__PURE__ */ new Set();
47547
+ const reviewed = /* @__PURE__ */ new Set();
47548
+ const addReview = (a, b, why, sim) => {
47549
+ const key = pairKey(a.nodeId, b.nodeId);
47550
+ if (reviewed.has(key)) return;
47551
+ reviewed.add(key);
47552
+ review.push({ nodes: [a.nodeId, b.nodeId], titles: [a.title, b.title], why, ...sim !== void 0 && { similarity: round2(sim) } });
47553
+ };
47554
+ for (const n2 of input) {
47555
+ if (isConfirmed(n2) || !isPlaceholder(n2)) continue;
47556
+ changes.push({ type: "prune", nodeId: n2.nodeId, title: n2.title, why: "a placeholder with no content of its own" });
47557
+ touched.add(n2.nodeId);
47558
+ }
47559
+ for (const n2 of input) {
47560
+ if (touched.has(n2.nodeId) || !n2.supersededBy) continue;
47561
+ const by = byId.get(n2.supersededBy);
47562
+ if (!by || touched.has(by.nodeId)) continue;
47563
+ if (isConfirmed(n2)) {
47564
+ addReview(n2, by, "marked superseded, but a person confirmed it");
47565
+ continue;
47566
+ }
47567
+ changes.push({ type: "supersede", nodeId: n2.nodeId, title: n2.title, by: by.nodeId, why: `its superseded_by names ${by.nodeId}` });
47568
+ touched.add(n2.nodeId);
47569
+ }
47570
+ for (const a of input) {
47571
+ for (const id of a.contradicts) {
47572
+ const b = byId.get(id);
47573
+ if (!b || touched.has(a.nodeId) || touched.has(b.nodeId)) continue;
47574
+ if (isConfirmed(a) !== isConfirmed(b)) {
47575
+ const [keep, drop] = isConfirmed(a) ? [a, b] : [b, a];
47576
+ changes.push({ type: "supersede", nodeId: drop.nodeId, title: drop.title, by: keep.nodeId, why: `contradicts ${keep.nodeId}, which a person confirmed` });
47577
+ touched.add(drop.nodeId);
47578
+ } else {
47579
+ addReview(a, b, "they contradict each other, and nothing in the graph says which is right");
47580
+ }
47581
+ }
47582
+ }
47583
+ const contradicting = /* @__PURE__ */ new Set();
47584
+ for (const a of input) for (const id of a.contradicts) contradicting.add(pairKey(a.nodeId, id));
47585
+ const pool = input.filter((n2) => !touched.has(n2.nodeId)).sort(compareStrength);
47586
+ const merged = /* @__PURE__ */ new Set();
47587
+ for (let i2 = 0; i2 < pool.length; i2++) {
47588
+ const keep = pool[i2];
47589
+ if (merged.has(keep.nodeId)) continue;
47590
+ for (let j = i2 + 1; j < pool.length; j++) {
47591
+ const other = pool[j];
47592
+ if (merged.has(other.nodeId) || other.kind !== keep.kind) continue;
47593
+ if (contradicting.has(pairKey(keep.nodeId, other.nodeId))) continue;
47594
+ const sim = similarity(keep, other);
47595
+ if (sim < reviewAt) continue;
47596
+ if (sim < mergeAt) {
47597
+ addReview(keep, other, "they may say the same thing", sim);
47598
+ continue;
47599
+ }
47600
+ if (!samePolarity(keep, other)) {
47601
+ addReview(keep, other, "one says not or never where the other does not: they may say opposite things", sim);
47602
+ continue;
47603
+ }
47604
+ if (isConfirmed(other)) {
47605
+ addReview(keep, other, "a near-duplicate, but a person confirmed both or the weaker one", sim);
47606
+ continue;
47607
+ }
47608
+ const addGlobs = other.fileGlobs.filter((g) => !keep.fileGlobs.includes(g));
47609
+ if (isConfirmed(keep) && addGlobs.length > 0) {
47610
+ addReview(keep, other, `a near-duplicate, but merging would widen the scope of ${keep.nodeId}, which a person confirmed`, sim);
47611
+ continue;
47612
+ }
47613
+ changes.push({
47614
+ type: "merge",
47615
+ nodeId: other.nodeId,
47616
+ title: other.title,
47617
+ into: keep.nodeId,
47618
+ similarity: round2(sim),
47619
+ addGlobs,
47620
+ why: `a near-duplicate of ${keep.nodeId}`
47621
+ });
47622
+ merged.add(other.nodeId);
47623
+ }
47624
+ }
47625
+ review.sort((a, b) => (b.similarity ?? 1) - (a.similarity ?? 1));
47626
+ return {
47627
+ scanned: input.length,
47628
+ changes,
47629
+ review: review.slice(0, REVIEW_MAX),
47630
+ reviewOverflow: Math.max(0, review.length - REVIEW_MAX)
47631
+ };
47632
+ }
47633
+ function oneLine2(title, max = 90) {
47634
+ const flat = title.replace(/\s+/g, " ").trim();
47635
+ return flat.length <= max ? flat : `${flat.slice(0, max - 1).replace(/\s+\S*$/, "")}\u2026`;
47636
+ }
47637
+ function renderPlan(plan, heading) {
47638
+ const lines = [`# ${heading}`, "", `Scanned ${plan.scanned} active node(s).`, ""];
47639
+ const of = (t2) => plan.changes.filter((c) => c.type === t2);
47640
+ const section = (title, items, line2) => {
47641
+ if (items.length === 0) return;
47642
+ lines.push(`## ${title} (${items.length})`, "");
47643
+ for (const c of items) lines.push(line2(c));
47644
+ lines.push("");
47645
+ };
47646
+ section("Merge near-duplicates", of("merge"), (c) => {
47647
+ const m = c;
47648
+ return `- ${m.nodeId} \u2192 into ${m.into} (overlap ${m.similarity})${m.addGlobs.length ? `, scope +${m.addGlobs.join(", ")}` : ""} \u2014 ${oneLine2(m.title)}`;
47649
+ });
47650
+ section("Retire superseded or contradicted", of("supersede"), (c) => `- ${c.nodeId} \u2014 ${c.why} \u2014 ${oneLine2(c.title)}`);
47651
+ section("Prune placeholders", of("prune"), (c) => `- ${c.nodeId} \u2014 ${oneLine2(c.title)}`);
47652
+ if (plan.changes.length === 0) lines.push("Nothing to change.", "");
47653
+ if (plan.review.length > 0) {
47654
+ lines.push(`## For a person to decide (${plan.review.length + plan.reviewOverflow}, not applied)`, "");
47655
+ for (const r of plan.review) {
47656
+ lines.push(`- ${r.nodes[0]} / ${r.nodes[1]}${r.similarity !== void 0 ? ` (overlap ${r.similarity})` : ""} \u2014 ${r.why}`);
47657
+ lines.push(` "${oneLine2(r.titles[0], 70)}" / "${oneLine2(r.titles[1], 70)}"`);
47658
+ }
47659
+ if (plan.reviewOverflow > 0) lines.push(`- \u2026 and ${plan.reviewOverflow} more`);
47660
+ lines.push("", 'To act on one: verity memory delete <id> --reason "duplicate of <id>"', "");
47661
+ }
47662
+ return terminalText(lines.join("\n"), Number.POSITIVE_INFINITY);
47663
+ }
47664
+
47665
+ // src/commands/memory-dream.ts
47666
+ var dreamsDir = () => projectPath(`${VERITY_DIR}/dreams`);
47667
+ var DREAM_ID = /^dream-\d{8}-\d{6}(?:-\d{3})?$/;
47668
+ var MAX_RECORD_BYTES2 = 8 * 1024 * 1024;
47669
+ function newDreamId(now = Date.now()) {
47670
+ for (let t2 = now; ; t2++) {
47671
+ const iso = new Date(t2).toISOString();
47672
+ const id = `dream-${iso.slice(0, 10).replace(/-/g, "")}-${iso.slice(11, 19).replace(/:/g, "")}-${iso.slice(20, 23)}`;
47673
+ if (!(0, import_node_fs87.existsSync)((0, import_node_path73.join)(dreamsDir(), `${id}.json`))) return id;
47674
+ }
47675
+ }
47676
+ async function saveRecord(record2) {
47677
+ const outcome = record2.results.length > 0 ? ["## Outcome", "", ...record2.results.map((r) => `- ${r.ok ? "done" : "FAILED"}: ${r.type} ${r.nodeId}${r.error ? ` \u2014 ${r.error}` : ""}`), ""] : [];
47678
+ const footer = [`Status: ${record2.status}.`, "", `Undo: verity memory dream --revert ${record2.id}`, ""];
47679
+ const saved = await writeFileAtomicInTree((0, import_node_path73.join)(dreamsDir(), `${record2.id}.json`), JSON.stringify(record2, null, 2) + "\n") && await writeFileAtomicInTree((0, import_node_path73.join)(dreamsDir(), `${record2.id}.md`), [renderPlan(record2.plan, record2.id), ...outcome, ...footer].join("\n"));
47680
+ if (!saved) throw new Error(".verity/dreams/ is a link, or a folder outside the repository");
47681
+ }
47682
+ async function readRecord(id) {
47683
+ if (!DREAM_ID.test(id)) return null;
47684
+ const read2 = readRegularFileInRoot(repoRoot(), (0, import_node_path73.join)(dreamsDir(), `${id}.json`), MAX_RECORD_BYTES2);
47685
+ if (!read2.ok) return null;
47686
+ try {
47687
+ const parsed = JSON.parse(read2.content);
47688
+ return isDreamRecord(parsed) && parsed.id === id ? parsed : null;
47689
+ } catch {
47690
+ return null;
47691
+ }
47692
+ }
47693
+ function isDreamRecord(v) {
47694
+ const r = v;
47695
+ const str4 = (x) => typeof x === "string";
47696
+ if (!r || !str4(r.id) || !["applying", "applied", "partial", "reverted"].includes(r.status)) return false;
47697
+ if (!r.plan || !Array.isArray(r.plan.changes) || !r.plan.changes.every((c) => !!c && str4(c.nodeId) && str4(c.type))) return false;
47698
+ if (!Array.isArray(r.results) || !r.results.every((x) => !!x && str4(x.nodeId) && str4(x.type) && typeof x.ok === "boolean")) return false;
47699
+ if (!r.before || typeof r.before !== "object" || Array.isArray(r.before)) return false;
47700
+ const before = r.before;
47701
+ if (!Object.values(before).every((b) => !!b && str4(b.path) && str4(b.content))) return false;
47702
+ if (r.after !== void 0) {
47703
+ if (!r.after || typeof r.after !== "object" || Array.isArray(r.after)) return false;
47704
+ if (!Object.values(r.after).every((b) => !!b && str4(b.path) && str4(b.content))) return false;
47705
+ }
47706
+ return r.results.every((x) => !x.ok || Object.prototype.hasOwnProperty.call(before, x.nodeId));
47707
+ }
47708
+ async function resolveApi2(program2) {
47709
+ const globals = program2.opts();
47710
+ const tokenResult = await resolveToken(globals.token);
47711
+ if (!tokenResult.ok) {
47712
+ printError(tokenResult.error);
47713
+ process.exit(1);
47714
+ }
47715
+ const urlResult = await resolveServiceUrl(globals.serviceUrl);
47716
+ if (!urlResult.ok) {
47717
+ printError(urlResult.error);
47718
+ process.exit(1);
47719
+ }
47720
+ return { serviceUrl: urlResult.data, token: tokenResult.data.token, verbose: globals.verbose };
47721
+ }
47722
+ async function currentNodes() {
47723
+ return (await listLocalNodes()).map(toDreamNode).filter((n2) => n2 !== null);
47724
+ }
47725
+ async function applyDreamPlan(api, plan) {
47726
+ const record2 = { id: newDreamId(), created_at: (/* @__PURE__ */ new Date()).toISOString(), status: "applying", plan, before: {}, results: [] };
47727
+ const keepers = new Set(plan.changes.filter((c) => c.type === "merge" && c.addGlobs.length > 0).map((c) => c.into));
47728
+ for (const id of [...plan.changes.map((c) => c.nodeId), ...keepers]) {
47729
+ const local = await findLocalNode(id);
47730
+ if (local) record2.before[id] = { path: local.path, content: local.content };
47731
+ }
47732
+ await saveRecord(record2);
47733
+ const addGlobs = /* @__PURE__ */ new Map();
47734
+ for (const c of plan.changes) {
47735
+ if (c.type === "merge" && c.addGlobs.length > 0 && record2.before[c.nodeId]) {
47736
+ addGlobs.set(c.into, [...addGlobs.get(c.into) ?? [], ...c.addGlobs]);
47737
+ }
47738
+ }
47739
+ const keeperFailed = /* @__PURE__ */ new Set();
47740
+ for (const [id, globs] of addGlobs) {
47741
+ const keeper = await findLocalNode(id);
47742
+ let error;
47743
+ if (!keeper || !record2.before[id]) error = "the file is not here";
47744
+ else {
47745
+ const before = record2.before[id];
47746
+ const next = applyNodeChanges(keeper.content, { fileGlobs: [.../* @__PURE__ */ new Set([...readGlobs(keeper.content), ...globs])] });
47747
+ const result = next === null ? { ok: false, error: "no frontmatter" } : await editNode(api, keeper, next);
47748
+ if (result.ok && result.mismatched.length === 0) {
47749
+ record2.after = { ...record2.after ?? {}, [id]: { path: before.path, content: result.final } };
47750
+ } else if (result.ok && next !== null) {
47751
+ error = `the service did not take: ${result.mismatched.join(", ")}`;
47752
+ const back = await editNode(api, { ...keeper, content: next }, before.content, { force: true });
47753
+ if (back.ok && back.mismatched.length === 0) error = `${error}; put back as it was`;
47754
+ else {
47755
+ const held = back.ok ? back.final : result.final;
47756
+ record2.after = { ...record2.after ?? {}, [id]: { path: before.path, content: held } };
47757
+ error = `${error}; and putting it back failed (${back.ok ? back.mismatched.join(", ") : back.error}): --revert retries`;
47758
+ }
47759
+ } else {
47760
+ error = result.ok ? void 0 : result.error;
47761
+ if (!await writeNodeFile(before.path, before.content)) {
47762
+ error = `${error}; and the file here could not be put back, so it still has the wider scope (--revert puts it back)`;
47763
+ }
47764
+ }
47765
+ }
47766
+ if (error) keeperFailed.add(id);
47767
+ record2.results.push({ nodeId: id, type: "scope", ok: !error, ...error && { error } });
47768
+ await saveRecord(record2);
47769
+ }
47770
+ for (const change of plan.changes) {
47771
+ if (!record2.before[change.nodeId]) {
47772
+ record2.results.push({ nodeId: change.nodeId, type: change.type, ok: false, error: "its file is gone, so its text could not be saved; not changed" });
47773
+ await saveRecord(record2);
47774
+ continue;
47775
+ }
47776
+ if (change.type === "merge" && keeperFailed.has(change.into)) {
47777
+ record2.results.push({ nodeId: change.nodeId, type: change.type, ok: false, error: `${change.into} could not take its scope; not merged` });
47778
+ await saveRecord(record2);
47779
+ continue;
47780
+ }
47781
+ const result = await deleteNode(api, change.nodeId, {
47782
+ status: change.type === "prune" ? "archived" : "superseded",
47783
+ via: "dream",
47784
+ reason: `${record2.id}: ${change.why}`
47785
+ });
47786
+ record2.results.push({
47787
+ nodeId: change.nodeId,
47788
+ type: change.type,
47789
+ ok: result.ok,
47790
+ ...!result.ok && { error: result.error },
47791
+ ...result.ok && result.fileKept && { error: "done on the service, but the file here could not be removed" }
47792
+ });
47793
+ await saveRecord(record2);
47794
+ }
47795
+ record2.status = record2.results.every((r) => r.ok) ? "applied" : "partial";
47796
+ await saveRecord(record2);
47797
+ return record2;
47798
+ }
47799
+ async function revert(api, record2) {
47800
+ const out2 = [];
47801
+ if (record2.status === "applying") {
47802
+ const seen = new Set(record2.results.map((r) => r.nodeId));
47803
+ for (const c of record2.plan.changes) {
47804
+ if (!seen.has(c.nodeId) && record2.before[c.nodeId]) record2.results.push({ nodeId: c.nodeId, type: c.type, ok: true });
47805
+ }
47806
+ const planned = new Set(record2.plan.changes.map((c) => c.nodeId));
47807
+ for (const id of Object.keys(record2.before)) {
47808
+ if (!planned.has(id) && !seen.has(id)) record2.results.push({ nodeId: id, type: "scope", ok: true });
47809
+ }
47810
+ }
47811
+ const changedOnService = record2.results.filter((x) => x.type === "scope" && (x.ok || record2.after?.[x.nodeId]));
47812
+ for (const r of changedOnService) {
47813
+ const before = record2.before[r.nodeId];
47814
+ if (!before) {
47815
+ out2.push({ ...r, ok: false, error: "no saved text" });
47816
+ continue;
47817
+ }
47818
+ const current = await findLocalNode(r.nodeId) ?? { nodeId: r.nodeId, path: before.path, content: "" };
47819
+ const after = record2.after?.[r.nodeId]?.content;
47820
+ let result = await editNode(api, current, before.content, after !== void 0 ? { base: after } : {});
47821
+ if (!result.ok && result.stale && after !== void 0) {
47822
+ result = await editNode(api, current, before.content, { force: true, expect: before.content });
47823
+ }
47824
+ const error = !result.ok ? result.stale ? "changed on the service since this dream; not reverted. Fix it with `verity memory edit`" : result.error : result.mismatched.length > 0 ? `the service did not take: ${result.mismatched.join(", ")}` : void 0;
47825
+ out2.push({ nodeId: r.nodeId, type: "scope", ok: !error, ...error && { error } });
47826
+ }
47827
+ for (const r of record2.results.filter((x) => !x.ok && x.type === "scope" && !record2.after?.[x.nodeId])) {
47828
+ const before = record2.before[r.nodeId];
47829
+ const current = await findLocalNode(r.nodeId);
47830
+ if (!before || current?.content === before.content) continue;
47831
+ const ok = await writeNodeFile(before.path, before.content);
47832
+ out2.push({ nodeId: r.nodeId, type: "scope", ok, ...!ok && { error: "the file here could not be written back" } });
47833
+ }
47834
+ for (const r of [...record2.results].reverse().filter((x) => x.ok && x.type !== "scope")) {
47835
+ const result = await restoreNode(api, r.nodeId, record2.before[r.nodeId]);
47836
+ out2.push({ nodeId: r.nodeId, type: r.type, ok: result.ok, ...!result.ok && { error: result.error } });
47837
+ }
47838
+ return out2;
47839
+ }
47840
+ function registerMemoryDreamCommand(memory2, program2) {
47841
+ memory2.command("dream").description("Consolidate the knowledge graph: merge near-duplicates, retire superseded nodes, prune empty placeholders. Prints the plan; --apply makes the changes and saves a report that --revert undoes").option("--apply", "Make the changes, on the service and here, and save a report in .verity/dreams/").option("--revert <dream-id>", "Undo an applied dream").option("--list", "List the dreams applied on this machine").option("--json", "Print the plan, or the outcome, as JSON").action(async (opts) => {
47842
+ try {
47843
+ process.chdir(repoRoot());
47844
+ } catch {
47845
+ }
47846
+ refuseLinkedMemoryDir();
47847
+ if ([opts.apply, opts.revert !== void 0, opts.list].filter(Boolean).length > 1) {
47848
+ printError("Use one of --apply, --revert and --list.");
47849
+ process.exit(1);
47850
+ }
47851
+ if (opts.list) {
47852
+ const files = (0, import_node_fs87.existsSync)(dreamsDir()) ? (await (0, import_promises19.readdir)(dreamsDir())).filter((f) => f.endsWith(".json") && DREAM_ID.test(f.slice(0, -5))).sort() : [];
47853
+ if (files.length === 0) {
47854
+ console.log("No dream has been applied on this machine.");
47855
+ process.exit(0);
47856
+ }
47857
+ for (const f of files) {
47858
+ const r = await readRecord(f.slice(0, -5));
47859
+ if (r) console.log(terminalText(`${r.id} ${r.status.padEnd(8)} ${r.results.filter((x) => x.ok).length} change(s) .verity/dreams/${r.id}.md`));
47860
+ }
47861
+ process.exit(0);
47862
+ }
47863
+ if (opts.revert !== void 0) {
47864
+ if (!DREAM_ID.test(opts.revert)) {
47865
+ printError("A dream id looks like dream-20261112-101500. `verity memory dream --list` lists them.");
47866
+ process.exit(1);
47867
+ }
47868
+ const record3 = await readRecord(opts.revert);
47869
+ if (!record3) {
47870
+ printError(`No valid record .verity/dreams/${opts.revert}.json on this machine (missing, or not a record this command wrote).`);
47871
+ process.exit(1);
47872
+ }
47873
+ if (record3.status === "reverted") {
47874
+ printInfo(`${record3.id} is already reverted.`);
47875
+ process.exit(0);
47876
+ }
47877
+ const api2 = await resolveApi2(program2);
47878
+ const results = await revert(api2, record3);
47879
+ const failed2 = results.filter((r) => !r.ok);
47880
+ if (failed2.length === 0) {
47881
+ record3.status = "reverted";
47882
+ record3.reverted_at = (/* @__PURE__ */ new Date()).toISOString();
47883
+ try {
47884
+ await saveRecord(record3);
47885
+ } catch (err2) {
47886
+ printWarn(`Reverted, but the record could not be marked as reverted: ${err2.message}.`);
47887
+ }
47888
+ }
47889
+ if (opts.json) {
47890
+ console.log(JSON.stringify({ id: record3.id, status: record3.status, results }, null, 2));
47891
+ process.exit(failed2.length ? 1 : 0);
47892
+ }
47893
+ if (failed2.length === 0) printInfo(`Reverted ${record3.id}: ${results.length} node(s) back as they were.`);
47894
+ else {
47895
+ printError(`Reverted ${results.length - failed2.length} of ${results.length}. Not reverted:`);
47896
+ for (const f of failed2) printWarn(` ${f.nodeId}: ${f.error}`);
47897
+ printWarn(` Run the same command again to retry.`);
47898
+ }
47899
+ process.exit(failed2.length ? 1 : 0);
47900
+ }
47901
+ const nodes = await currentNodes();
47902
+ if (nodes.length === 0) {
47903
+ console.log("No active nodes in .verity/memory/. `verity memory pull` fetches the graph.");
47904
+ process.exit(0);
47905
+ }
47906
+ const plan = planDream(nodes);
47907
+ if (!opts.apply) {
47908
+ if (opts.json) console.log(JSON.stringify({ applied: false, ...plan }, null, 2));
47909
+ else {
47910
+ console.log(renderPlan(plan, "Dream plan (nothing changed yet)"));
47911
+ if (plan.changes.length > 0) console.log("Apply it: verity memory dream --apply");
47912
+ }
47913
+ process.exit(0);
47914
+ }
47915
+ if (plan.changes.length === 0) {
47916
+ if (opts.json) console.log(JSON.stringify({ applied: false, ...plan }, null, 2));
47917
+ else console.log(renderPlan(plan, "Dream plan: nothing to change"));
47918
+ process.exit(0);
47919
+ }
47920
+ const api = await resolveApi2(program2);
47921
+ let record2;
47922
+ try {
47923
+ record2 = await applyDreamPlan(api, plan);
47924
+ } catch (err2) {
47925
+ printError(`Could not save the dream record: ${err2.message}. Changes stop here; the record on disk says what was done.`);
47926
+ process.exit(1);
47927
+ }
47928
+ const failed = record2.results.filter((r) => !r.ok);
47929
+ if (opts.json) {
47930
+ console.log(JSON.stringify({ applied: true, id: record2.id, status: record2.status, report: `.verity/dreams/${record2.id}.md`, results: record2.results, review: plan.review }, null, 2));
47931
+ process.exit(failed.length ? 1 : 0);
47932
+ }
47933
+ console.log(renderPlan(plan, `${record2.id} (applied)`));
47934
+ if (failed.length > 0) {
47935
+ printWarn(`${failed.length} change(s) failed:`);
47936
+ for (const f of failed) printWarn(` ${f.type} ${f.nodeId}: ${f.error}`);
47937
+ }
47938
+ printInfo(`Report: .verity/dreams/${record2.id}.md`);
47939
+ printInfo(`Undo: verity memory dream --revert ${record2.id}`);
47940
+ process.exit(failed.length ? 1 : 0);
47941
+ });
47942
+ }
47943
+
45920
47944
  // src/commands/memory.ts
45921
47945
  function registerMemoryCommand(program2) {
45922
47946
  const memory2 = program2.command("memory").description("Manage the project knowledge base");
47947
+ registerMemoryCurationCommands(memory2, program2);
47948
+ registerMemoryAskCommand(memory2);
47949
+ registerMemoryDreamCommand(memory2, program2);
45923
47950
  memory2.command("seed").description("Seed .verity/memory/ from what /verity-setup already extracted (knowledge_spec, README, CLAUDE.md)").option("--force", "Re-seed even if the memory dir already has seeded nodes").option("--dry-run", "Print the candidate nodes without writing or syncing").action(async (opts) => {
45924
47951
  const globals = program2.opts();
45925
47952
  const tokenResult = await resolveToken(globals.token);
@@ -46184,7 +48211,7 @@ function registerRunCommand(program2) {
46184
48211
  }
46185
48212
 
46186
48213
  // src/commands/telemetry.ts
46187
- var import_node_fs85 = require("node:fs");
48214
+ var import_node_fs88 = require("node:fs");
46188
48215
  var SCOPE = "every Claude Code session on this machine";
46189
48216
  function declineHeaders(why) {
46190
48217
  process.stderr.write(`Verity telemetry is off here: ${why} Nothing is exported from this session.
@@ -46192,7 +48219,7 @@ function declineHeaders(why) {
46192
48219
  process.exitCode = 1;
46193
48220
  }
46194
48221
  async function recordTelemetryChoice(choice) {
46195
- if (!(0, import_node_fs85.existsSync)(projectPath(SETUP_STATE_FILE))) return;
48222
+ if (!(0, import_node_fs88.existsSync)(projectPath(SETUP_STATE_FILE))) return;
46196
48223
  try {
46197
48224
  if ((await readSetupState())?.telemetry === choice) return;
46198
48225
  await writeSetupState({ telemetry: choice });
@@ -46300,12 +48327,12 @@ function registerTelemetryCommands(program2) {
46300
48327
  }
46301
48328
 
46302
48329
  // src/commands/prove.ts
46303
- var import_node_fs87 = require("node:fs");
46304
- var import_node_path73 = require("node:path");
48330
+ var import_node_fs90 = require("node:fs");
48331
+ var import_node_path75 = require("node:path");
46305
48332
 
46306
48333
  // src/lib/prove/on-demand-run.ts
46307
- var import_node_fs86 = require("node:fs");
46308
- var import_node_path72 = require("node:path");
48334
+ var import_node_fs89 = require("node:fs");
48335
+ var import_node_path74 = require("node:path");
46309
48336
  var ON_DEMAND_RUN = `${PROOFS_DIR}/.results/.formal-run.json`;
46310
48337
  var ON_DEMAND_RUN_REUSE_MS = 18 * 6e4;
46311
48338
  function isLocalState2(root) {
@@ -46327,7 +48354,7 @@ function forgetOnDemandRun(root) {
46327
48354
  setFormalRun(null);
46328
48355
  if (!isLocalState2(root)) return;
46329
48356
  try {
46330
- (0, import_node_fs86.unlinkSync)((0, import_node_path72.join)(root, ON_DEMAND_RUN));
48357
+ (0, import_node_fs89.unlinkSync)((0, import_node_path74.join)(root, ON_DEMAND_RUN));
46331
48358
  } catch {
46332
48359
  }
46333
48360
  }
@@ -46504,7 +48531,7 @@ function registerProveCommand(program2) {
46504
48531
  process.exit(2);
46505
48532
  }
46506
48533
  const limit = Math.max(1, Math.min(50, Number(opts.limit) || 10));
46507
- let signals = collectRiskSignals(root, { since: opts.since, limit, paths: paths.map((p) => (0, import_node_path73.relative)(root, (0, import_node_path73.resolve)(p)) || ".") });
48534
+ let signals = collectRiskSignals(root, { since: opts.since, limit, paths: paths.map((p) => (0, import_node_path75.relative)(root, (0, import_node_path75.resolve)(p)) || ".") });
46508
48535
  if (opts.hint && paths.length === 0) {
46509
48536
  const hit = hintMatches(root, opts.hint);
46510
48537
  if (hit.files.length > 0) {
@@ -46558,7 +48585,7 @@ function registerProveCommand(program2) {
46558
48585
  });
46559
48586
  prove.command("check [model]").description("Elaborate a model (or every model) with the local Lean toolchain and report each property").option("--timeout <ms>", "Per-model timeout in ms", "120000").option("--json", "Machine-readable output").action((model, opts) => {
46560
48587
  const root = repoRoot();
46561
- if (!(0, import_node_fs87.existsSync)((0, import_node_path73.join)(root, PROOFS_DIR, "lakefile.toml"))) {
48588
+ if (!(0, import_node_fs90.existsSync)((0, import_node_path75.join)(root, PROOFS_DIR, "lakefile.toml"))) {
46562
48589
  printError(`No ${PROOFS_DIR}/ project here. Run \`verity prove init\` first.`);
46563
48590
  process.exit(2);
46564
48591
  }
@@ -46582,7 +48609,7 @@ ${build.stderr.split("\n").filter((l) => !l.startsWith("trace:")).slice(0, 20).j
46582
48609
  const reports = [];
46583
48610
  for (const f of files) {
46584
48611
  const report = checkModel(tc, root, f, { timeoutMs: Math.max(1e3, Number(opts.timeout) || 12e4) });
46585
- report.file = (0, import_node_path73.relative)(root, f);
48612
+ report.file = (0, import_node_path75.relative)(root, f);
46586
48613
  reports.push(report);
46587
48614
  try {
46588
48615
  writeResult(root, report);
@@ -46620,7 +48647,7 @@ ${build.stderr.split("\n").filter((l) => !l.startsWith("trace:")).slice(0, 20).j
46620
48647
  printError("Not logged in \u2014 replays run on the Verity service. Run `verity login` first.");
46621
48648
  process.exit(2);
46622
48649
  }
46623
- if (!(0, import_node_fs87.existsSync)((0, import_node_path73.join)(root, PROOFS_DIR, "lakefile.toml"))) {
48650
+ if (!(0, import_node_fs90.existsSync)((0, import_node_path75.join)(root, PROOFS_DIR, "lakefile.toml"))) {
46624
48651
  printError(`No ${PROOFS_DIR}/ project here. Run \`verity prove init\` first.`);
46625
48652
  process.exit(2);
46626
48653
  }
@@ -46659,7 +48686,7 @@ ${report.error ?? ""}`);
46659
48686
  process.exit(2);
46660
48687
  }
46661
48688
  }
46662
- const modelSource = readRegularFileInRoot(root, (0, import_node_path73.relative)(root, file), 512 * 1024);
48689
+ const modelSource = readRegularFileInRoot(root, (0, import_node_path75.relative)(root, file), 512 * 1024);
46663
48690
  if (!opts.json) printInfo(`replaying ${targets.length} counterexample(s) of ${model} in network-less containers (${Math.min(targets.length, REPLAY_CONCURRENCY)} at a time)\u2026`);
46664
48691
  const outcomes = await mapPool(targets, REPLAY_CONCURRENCY, async (p) => {
46665
48692
  const brief = buildReplayBrief(root, report, p, modelSource.ok ? modelSource.content : "");
@@ -46680,7 +48707,7 @@ ${report.error ?? ""}`);
46680
48707
  });
46681
48708
  prove.command("auto [files...]").description("The formal phase the pre-push guard runs: select one risky symbol among changed files, generate a model, check it, replay counterexamples (default files: the commits ahead of upstream)").option("--deadline <ms>", "Budget for the whole phase", "170000").option("--json", "Machine-readable output").action(async (paths, opts) => {
46682
48709
  const root = repoRoot();
46683
- let files = paths.map((p) => (0, import_node_path73.relative)(root, (0, import_node_path73.resolve)(p)));
48710
+ let files = paths.map((p) => (0, import_node_path75.relative)(root, (0, import_node_path75.resolve)(p)));
46684
48711
  if (files.length === 0) {
46685
48712
  const ahead = runGitSync(root, ["diff", "--name-only", "@{upstream}...HEAD"]);
46686
48713
  const fallback = ahead.ok ? ahead : runGitSync(root, ["diff", "--name-only", "HEAD~1..HEAD"]);
@@ -46754,7 +48781,7 @@ ${f.reason ? ` ${terminalText(f.reason, 2e3)}
46754
48781
  let revise;
46755
48782
  if (opts.revise) {
46756
48783
  const existing = resolveModelFile(root, opts.revise);
46757
- const src = existing ? readRegularFileInRoot(root, (0, import_node_path73.relative)(root, existing), 64 * 1024) : null;
48784
+ const src = existing ? readRegularFileInRoot(root, (0, import_node_path75.relative)(root, existing), 64 * 1024) : null;
46758
48785
  if (!src || !src.ok || !opts.note) {
46759
48786
  printError("--revise needs an existing model under Models/ and --note - with what the replay showed on stdin");
46760
48787
  process.exit(2);
@@ -46803,8 +48830,8 @@ ${f.reason ? ` ${terminalText(f.reason, 2e3)}
46803
48830
  });
46804
48831
  prove.command("waive <file>").description("Dismiss the remembered formal finding(s) on a file for its current bytes \u2014 the accepted-risk path for a formal block").requiredOption("--reason <text>", "The human disposition this records (reviewer finding, ADR, \u2026)").action((file, opts) => {
46805
48832
  const root = repoRoot();
46806
- const rel0 = (0, import_node_path73.relative)(root, (0, import_node_path73.isAbsolute)(file) ? file : (0, import_node_path73.join)(root, file)).replace(/\\/g, "/");
46807
- const rel = rel0.startsWith("..") || (0, import_node_path73.isAbsolute)(rel0) ? null : rel0;
48833
+ const rel0 = (0, import_node_path75.relative)(root, (0, import_node_path75.isAbsolute)(file) ? file : (0, import_node_path75.join)(root, file)).replace(/\\/g, "/");
48834
+ const rel = rel0.startsWith("..") || (0, import_node_path75.isAbsolute)(rel0) ? null : rel0;
46808
48835
  if (!rel) {
46809
48836
  printError(`${file} is not inside this repository`);
46810
48837
  process.exit(2);
@@ -46820,8 +48847,8 @@ ${f.reason ? ` ${terminalText(f.reason, 2e3)}
46820
48847
  });
46821
48848
  prove.command("hash <file>").description("Print the sha256 of a file, for a model header (Verity.model \u2026 sha256). Path is REPO-relative, as in the header").action((file) => {
46822
48849
  const root = repoRoot();
46823
- const rel = (0, import_node_path73.relative)(root, (0, import_node_path73.isAbsolute)(file) ? file : (0, import_node_path73.join)(root, file));
46824
- const digest = rel.startsWith("..") || (0, import_node_path73.isAbsolute)(rel) ? null : sha256InRoot(root, rel);
48850
+ const rel = (0, import_node_path75.relative)(root, (0, import_node_path75.isAbsolute)(file) ? file : (0, import_node_path75.join)(root, file));
48851
+ const digest = rel.startsWith("..") || (0, import_node_path75.isAbsolute)(rel) ? null : sha256InRoot(root, rel);
46825
48852
  if (!digest) {
46826
48853
  printError(`Cannot hash ${file}: it must be a regular file inside this repository (not a symlink)`);
46827
48854
  process.exit(2);
@@ -46855,10 +48882,9 @@ function refreshScaffold(root) {
46855
48882
  }
46856
48883
 
46857
48884
  // src/commands/lean.ts
46858
- var import_node_fs88 = require("node:fs");
46859
- var import_node_child_process24 = require("node:child_process");
48885
+ var import_node_fs91 = require("node:fs");
46860
48886
  var import_node_os16 = require("node:os");
46861
- var import_node_path74 = require("node:path");
48887
+ var import_node_path76 = require("node:path");
46862
48888
  function registerLeanCommand(program2) {
46863
48889
  const lean = program2.command("lean").description("The Lean 4 toolchain formal verification needs (managed download, pinned by this CLI)");
46864
48890
  lean.command("status").option("--json", "Machine-readable output").description("Which toolchain would run, and whether it matches the pinned version").action((opts) => {
@@ -46866,7 +48892,7 @@ function registerLeanCommand(program2) {
46866
48892
  let version = null;
46867
48893
  if (tc) {
46868
48894
  try {
46869
- version = (0, import_node_child_process24.execFileSync)(tc.lake.replace(/lake(\.exe)?$/, "lean$1"), ["--version"], { env: tc.env, encoding: "utf-8", timeout: 1e4 }).trim();
48895
+ version = execFileSync2(tc.lake.replace(/lake(\.exe)?$/, "lean$1"), ["--version"], { env: tc.env, encoding: "utf-8", timeout: 1e4 }).trim();
46870
48896
  } catch {
46871
48897
  version = null;
46872
48898
  }
@@ -46877,7 +48903,7 @@ function registerLeanCommand(program2) {
46877
48903
  const bg = readInstallState();
46878
48904
  const background = bg ? { ...bg, running: bg.state === "running" && installRunning() } : null;
46879
48905
  if (opts.json) {
46880
- printJson({ pinned: LEAN_TOOLCHAIN, found: tc?.lake ?? null, version, matches_pin: matches, managed_dir: managed, managed_installed: (0, import_node_fs88.existsSync)((0, import_node_path74.join)(managed, "bin")), asset: leanAssetName(), background });
48906
+ printJson({ pinned: LEAN_TOOLCHAIN, found: tc?.lake ?? null, version, matches_pin: matches, managed_dir: managed, managed_installed: (0, import_node_fs91.existsSync)((0, import_node_path76.join)(managed, "bin")), asset: leanAssetName(), background });
46881
48907
  return;
46882
48908
  }
46883
48909
  printInfo(`pinned: ${LEAN_TOOLCHAIN}`);
@@ -46898,7 +48924,7 @@ function registerLeanCommand(program2) {
46898
48924
  process.exit(2);
46899
48925
  }
46900
48926
  const dir = pinnedVersionDir();
46901
- if ((0, import_node_fs88.existsSync)((0, import_node_path74.join)(dir, "bin", "lake")) && !opts.force) {
48927
+ if ((0, import_node_fs91.existsSync)((0, import_node_path76.join)(dir, "bin", "lake")) && !opts.force) {
46902
48928
  printInfo(`already installed at ${dir}`);
46903
48929
  return;
46904
48930
  }
@@ -46934,20 +48960,20 @@ function registerLeanCommand(program2) {
46934
48960
  });
46935
48961
  lean.command("remove").description("Delete the Verity-managed toolchain for the pinned version").action(() => {
46936
48962
  const dir = pinnedVersionDir();
46937
- if (!(0, import_node_fs88.existsSync)(dir)) {
48963
+ if (!(0, import_node_fs91.existsSync)(dir)) {
46938
48964
  printInfo("nothing to remove");
46939
48965
  return;
46940
48966
  }
46941
- (0, import_node_fs88.rmSync)(dir, { recursive: true, force: true });
48967
+ (0, import_node_fs91.rmSync)(dir, { recursive: true, force: true });
46942
48968
  printInfo(`removed ${dir}`);
46943
48969
  });
46944
48970
  }
46945
48971
 
46946
48972
  // src/cli.ts
46947
- program.name("verity").description("CLI for Verity quality gate service").version("0.35.5").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async (_thisCommand, actionCommand) => {
48973
+ program.name("verity").description("CLI for Verity quality gate service").version("0.36.0").option("--token <token>", "Override authentication token").option("--service-url <url>", "Override service URL").option("--verbose", "Log HTTP requests/responses to stderr").hook("preAction", async (_thisCommand, actionCommand) => {
46948
48974
  const isTelemetryHelper = actionCommand.name() === "headers" && actionCommand.parent?.name() === "telemetry";
46949
48975
  if (!isTelemetryHelper) {
46950
- installStderrLog(actionCommand.name(), process.argv.slice(2), "0.35.5");
48976
+ installStderrLog(actionCommand.name(), process.argv.slice(2), "0.36.0");
46951
48977
  }
46952
48978
  setUserNamedServiceUrl(program.opts().serviceUrl);
46953
48979
  try {