mason-context 0.18.0 → 0.19.1

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/dist/mason-mcp.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ if (!import.meta.url.endsWith("/mason-runtime.js")) await import("./mason-runtime.js").then(m => m.registerRuntime());
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
5
  var __esm = (fn, res) => function __init() {
@@ -57,23 +58,486 @@ var init_git_read = __esm({
57
58
  }
58
59
  });
59
60
 
61
+ // src/utils/paths.ts
62
+ import path from "path";
63
+ function normalizeRepoPath(value) {
64
+ const slash = value.replace(/\\/g, "/");
65
+ if (!slash || slash.includes("\0") || path.posix.isAbsolute(slash) || /^[A-Za-z]:/.test(slash)) return null;
66
+ if (slash.split("/").includes("..")) return null;
67
+ const normalized = path.posix.normalize(slash).replace(/\/$/, "");
68
+ return normalized === "." ? null : normalized;
69
+ }
70
+ function sanitizeRepoPaths(files) {
71
+ return [...new Set(files.map(normalizeRepoPath).filter((p) => p !== null))];
72
+ }
73
+ function isWithinRoot(root, candidate) {
74
+ const relative = path.relative(root, candidate);
75
+ return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
76
+ }
77
+ function anchorMatches(anchor, file) {
78
+ const a = normalizeRepoPath(anchor);
79
+ const f = normalizeRepoPath(file);
80
+ return a !== null && f !== null && (a === f || f.startsWith(`${a}/`));
81
+ }
82
+ function matchingPaths(anchors, files) {
83
+ return [...new Set(files)].filter((file) => anchors.some((anchor) => anchorMatches(anchor, file)));
84
+ }
85
+ var init_paths = __esm({
86
+ "src/utils/paths.ts"() {
87
+ "use strict";
88
+ }
89
+ });
90
+
91
+ // src/audit/types.ts
92
+ var types_exports = {};
93
+ __export(types_exports, {
94
+ ALL_CHECKS: () => ALL_CHECKS
95
+ });
96
+ var ALL_CHECKS;
97
+ var init_types = __esm({
98
+ "src/audit/types.ts"() {
99
+ "use strict";
100
+ ALL_CHECKS = [
101
+ "deleted-reference",
102
+ "new-module",
103
+ "stale-count",
104
+ "dead-command",
105
+ "deps-changed",
106
+ "decision-anchor-drift"
107
+ ];
108
+ }
109
+ });
110
+
111
+ // src/utils/files.ts
112
+ import fs from "fs/promises";
113
+ import { constants } from "fs";
114
+ import path2 from "path";
115
+ import fg from "fast-glob";
116
+ function isSensitiveFile(file) {
117
+ return file.split(/[\\/]/).some(
118
+ (part) => /^(?:\.env(?:\..*)?|id_rsa.*|id_ed25519.*)$|\.(?:pem|key|p12|pfx|jks|keystore)$|credentials\.|secret|^local\.properties$/i.test(part)
119
+ );
120
+ }
121
+ async function readBoundedFile(file, maxBytes) {
122
+ const handle = await fs.open(file, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
123
+ try {
124
+ const stat = await handle.stat();
125
+ if (!stat.isFile() || stat.size > maxBytes) return null;
126
+ const buffer = Buffer.alloc(Math.min(maxBytes + 1, stat.size + 1));
127
+ let bytes = 0;
128
+ while (bytes < buffer.length) {
129
+ const result = await handle.read(buffer, bytes, buffer.length - bytes, null);
130
+ if (result.bytesRead === 0) break;
131
+ bytes += result.bytesRead;
132
+ }
133
+ return bytes === buffer.length ? null : buffer.subarray(0, bytes).toString("utf8");
134
+ } finally {
135
+ await handle.close();
136
+ }
137
+ }
138
+ async function loadProjectConfig(root) {
139
+ try {
140
+ const canonicalRoot = await fs.realpath(root);
141
+ const configPath2 = await fs.realpath(path2.join(root, ".mason/config.json"));
142
+ if (!isWithinRoot(canonicalRoot, configPath2)) throw new Error("Project configuration resolves outside the repository");
143
+ const raw = await readBoundedFile(configPath2, 64 * 1024);
144
+ if (raw === null) throw new Error("Project configuration is not a regular file or exceeds 64 KiB");
145
+ const value = JSON.parse(raw);
146
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected a configuration object");
147
+ const config = {};
148
+ for (const key of ["patterns", "alwaysInclude", "ignore"]) {
149
+ if (value[key] === void 0) continue;
150
+ if (!Array.isArray(value[key]) || !value[key].every((s) => typeof s === "string")) {
151
+ throw new Error(`Configuration ${key} must be an array of strings`);
152
+ }
153
+ config[key] = value[key];
154
+ }
155
+ if (value.audit !== void 0) {
156
+ const audit = value.audit;
157
+ if (!audit || typeof audit !== "object" || Array.isArray(audit)) throw new Error("Configuration audit must be an object");
158
+ const patterns = (raw2) => {
159
+ if (!Array.isArray(raw2) || raw2.length > 100 || !raw2.every((s) => typeof s === "string" && normalizeRepoPath(s) === s && !s.startsWith("!") && !/[\x00-\x1f]/.test(s))) {
160
+ throw new Error("Audit patterns must be at most 100 repository-relative globs without traversal or negation");
161
+ }
162
+ return raw2;
163
+ };
164
+ config.audit = {};
165
+ if (audit.include !== void 0) config.audit.include = patterns(audit.include);
166
+ if (audit.exclude !== void 0) {
167
+ if (!audit.exclude || typeof audit.exclude !== "object" || Array.isArray(audit.exclude)) throw new Error("audit.exclude must map check names to patterns");
168
+ const { ALL_CHECKS: ALL_CHECKS2 } = await Promise.resolve().then(() => (init_types(), types_exports));
169
+ config.audit.exclude = {};
170
+ for (const [check, globs] of Object.entries(audit.exclude)) {
171
+ if (!ALL_CHECKS2.includes(check)) throw new Error("Unknown audit check: " + check);
172
+ config.audit.exclude[check] = patterns(globs);
173
+ }
174
+ }
175
+ }
176
+ return config;
177
+ } catch (error) {
178
+ if (error.code === "ENOENT") return {};
179
+ throw new Error(`Cannot apply project file policy: ${error instanceof Error ? error.message : String(error)}`);
180
+ }
181
+ }
182
+ async function createFileAccess(rootDir) {
183
+ const root = path2.resolve(rootDir);
184
+ const canonicalRoot = await fs.realpath(root).catch(() => root);
185
+ const config = await loadProjectConfig(root);
186
+ const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
187
+ let gitFiles = null;
188
+ try {
189
+ const { stdout } = await execGit(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], { cwd: root, maxBuffer: 50 * 1024 * 1024 });
190
+ gitFiles = new Set(stdout.split("\0").filter(Boolean));
191
+ } catch {
192
+ let inGit = false;
193
+ try {
194
+ await execGit(["rev-parse", "--git-dir"], { cwd: root });
195
+ inGit = true;
196
+ } catch {
197
+ }
198
+ if (inGit) throw new Error("Cannot enumerate Git files safely");
199
+ }
200
+ async function resolve2(file) {
201
+ const relative = normalizeRepoPath(file);
202
+ if (!relative || isSensitiveFile(relative) || gitFiles && !gitFiles.has(relative)) return null;
203
+ const candidate = path2.join(root, relative);
204
+ try {
205
+ const real = await fs.realpath(candidate);
206
+ if (!isWithinRoot(canonicalRoot, real) || isSensitiveFile(path2.relative(canonicalRoot, real))) return null;
207
+ const stat = await fs.stat(real);
208
+ if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES) return null;
209
+ if (gitFiles && !gitFiles.has(path2.relative(canonicalRoot, real).split(path2.sep).join("/"))) return null;
210
+ return real;
211
+ } catch {
212
+ return null;
213
+ }
214
+ }
215
+ async function list(patterns = SOURCE_GLOB, options = {}) {
216
+ const found = await fg(patterns, { cwd: root, ignore, followSymbolicLinks: false, ...options });
217
+ const safe = await Promise.all(found.map(async (f) => await resolve2(f) ? f : null));
218
+ return safe.filter((f) => f !== null).sort();
219
+ }
220
+ async function read(file) {
221
+ const relative = normalizeRepoPath(file);
222
+ if (!relative) return null;
223
+ const real = await resolve2(relative);
224
+ if (!real) return null;
225
+ for (const rel of /* @__PURE__ */ new Set([relative, path2.relative(canonicalRoot, real).split(path2.sep).join("/")])) {
226
+ if (!(await fg(fg.escapePath(rel), { cwd: root, ignore, dot: true })).length) return null;
227
+ }
228
+ try {
229
+ const content2 = await readBoundedFile(real, MAX_SOURCE_BYTES);
230
+ return content2 === null ? null : { path: relative, content: content2, totalLines: content2.split("\n").length };
231
+ } catch {
232
+ return null;
233
+ }
234
+ }
235
+ return { root, config, list, read };
236
+ }
237
+ var SOURCE_EXTENSIONS, SOURCE_GLOB, SOURCE_IGNORE, MAX_SOURCE_BYTES;
238
+ var init_files = __esm({
239
+ "src/utils/files.ts"() {
240
+ "use strict";
241
+ init_git_read();
242
+ init_paths();
243
+ SOURCE_EXTENSIONS = ["ts", "tsx", "js", "jsx", "mts", "cts", "mjs", "cjs", "vue", "svelte", "kt", "kts", "java", "py", "go", "rs", "swift", "rb", "cs", "cpp", "c", "h", "hpp", "dart", "php"];
244
+ SOURCE_GLOB = `**/*.{${SOURCE_EXTENSIONS.join(",")}}`;
245
+ SOURCE_IGNORE = [
246
+ "**/node_modules/**",
247
+ "**/dist/**",
248
+ "**/build/**",
249
+ "**/.gradle/**",
250
+ "**/target/**",
251
+ "**/.git/**",
252
+ "**/.mason/**",
253
+ "**/vendor/**",
254
+ "**/__pycache__/**",
255
+ "**/venv/**",
256
+ "**/.venv/**",
257
+ "**/*.min.*",
258
+ "**/*.map",
259
+ "**/*.lock",
260
+ "**/generated/**",
261
+ "**/*.generated.*",
262
+ "**/R.java",
263
+ "**/BuildConfig.java",
264
+ "**/package-lock.json",
265
+ "**/yarn.lock",
266
+ "**/pnpm-lock.yaml"
267
+ ];
268
+ MAX_SOURCE_BYTES = 1024 * 1024;
269
+ }
270
+ });
271
+
272
+ // src/utils/storage.ts
273
+ import fs2 from "fs/promises";
274
+ import path3 from "path";
275
+ import { randomUUID } from "crypto";
276
+ async function storePath(root, relative, createParents = false) {
277
+ const normalized = normalizeRepoPath(relative);
278
+ if (!normalized) throw new Error(`Invalid store path: ${relative}`);
279
+ let current = await fs2.realpath(root);
280
+ const parts = normalized.split("/");
281
+ for (let i = 0; i < parts.length; i++) {
282
+ current = path3.join(current, parts[i]);
283
+ let stat;
284
+ try {
285
+ stat = await fs2.lstat(current);
286
+ } catch (error) {
287
+ if (error.code !== "ENOENT") throw error;
288
+ if (createParents && i < parts.length - 1) {
289
+ try {
290
+ await fs2.mkdir(current);
291
+ } catch (mkdirError) {
292
+ if (mkdirError.code !== "EEXIST") throw mkdirError;
293
+ }
294
+ stat = await fs2.lstat(current);
295
+ }
296
+ }
297
+ if (stat?.isSymbolicLink()) throw new Error(`Symlink in store path: ${relative}`);
298
+ }
299
+ return current;
300
+ }
301
+ async function readStoreJson(root, relative) {
302
+ try {
303
+ const file = await storePath(root, relative);
304
+ const raw = await readBoundedFile(file, 10 * 1024 * 1024);
305
+ if (raw === null) throw new Error("file is not regular or exceeds 10 MiB");
306
+ const parsed = JSON.parse(raw);
307
+ if (parsed === null) throw new Error("expected a JSON object, received null");
308
+ return parsed;
309
+ } catch (error) {
310
+ if (error.code === "ENOENT") return null;
311
+ throw new Error(`Invalid Mason store ${relative}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
312
+ }
313
+ }
314
+ async function writeStoreJson(root, relative, value) {
315
+ const payload = JSON.stringify(value, null, 2) + "\n";
316
+ if (Buffer.byteLength(payload) > 10 * 1024 * 1024) {
317
+ throw new Error(`Mason store ${relative} exceeds 10 MiB`);
318
+ }
319
+ const file = await storePath(root, relative, true);
320
+ const temporary = path3.join(path3.dirname(file), `.${path3.basename(file)}.${randomUUID()}.tmp`);
321
+ try {
322
+ const handle = await fs2.open(temporary, "wx", 384);
323
+ try {
324
+ await handle.writeFile(payload, "utf8");
325
+ await handle.sync();
326
+ } finally {
327
+ await handle.close();
328
+ }
329
+ await fs2.rename(temporary, file);
330
+ } finally {
331
+ await fs2.rm(temporary, { force: true });
332
+ }
333
+ }
334
+ var init_storage = __esm({
335
+ "src/utils/storage.ts"() {
336
+ "use strict";
337
+ init_paths();
338
+ init_files();
339
+ }
340
+ });
341
+
342
+ // src/audit/inspection.ts
343
+ import fg2 from "fast-glob";
344
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
345
+ import path4 from "path";
346
+ function withRepositoryInspection(root, run) {
347
+ return inspections.run({ root: path4.resolve(root), reads: /* @__PURE__ */ new Map() }, run);
348
+ }
349
+ function inspectionRead(root, key, read) {
350
+ const inspection = inspections.getStore();
351
+ if (!inspection || inspection.root !== path4.resolve(root)) return read();
352
+ if (!inspection.reads.has(key)) {
353
+ const pending = read();
354
+ inspection.reads.set(key, pending);
355
+ void pending.catch(() => {
356
+ if (inspection.reads.get(key) === pending) inspection.reads.delete(key);
357
+ });
358
+ }
359
+ return inspection.reads.get(key);
360
+ }
361
+ function inspectionGit(args, options) {
362
+ return inspectionRead(options.cwd, "git:" + JSON.stringify([args, options]), () => execGit(args, options));
363
+ }
364
+ async function auditGitPaths(root, kind) {
365
+ const custom = (await loadProjectConfig(root)).audit?.include ?? [];
366
+ const shared = inspections.getStore()?.root === path4.resolve(root) && !custom.length;
367
+ const expanded = fg2.generateTasks(custom).flatMap((task) => task.positive);
368
+ if (expanded.length > 1e3) throw new Error("Audit include patterns expand to more than 1,000 globs");
369
+ const documentSpecs = [...docSpecs, ...expanded.map((pattern) => `:(glob)${pattern}`)];
370
+ const specs = shared ? [...documentSpecs, ...sourceSpecs] : kind === "documents" ? documentSpecs : sourceSpecs;
371
+ const limit = kind === "documents" ? 16 * 1024 * 1024 : 32 * 1024 * 1024;
372
+ const { stdout } = await inspectionGit(
373
+ ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...specs],
374
+ { cwd: root, maxBuffer: shared ? 48 * 1024 * 1024 : limit, timeout: 1e4 }
375
+ );
376
+ const paths = [...new Set(stdout.split("\0").filter(Boolean))].filter((file) => kind === "documents" ? custom.length > 0 || /^(readme|agents|claude)\.md$/i.test(path4.posix.basename(file)) : sourceSuffixes.has(file.slice(file.lastIndexOf("."))));
377
+ if (Buffer.byteLength(paths.join("\0")) > limit) throw new Error("Git " + kind + " inventory exceeds its bounded read limit.");
378
+ return paths.sort();
379
+ }
380
+ var inspections, sourceSpecs, docSpecs, sourceSuffixes;
381
+ var init_inspection = __esm({
382
+ "src/audit/inspection.ts"() {
383
+ "use strict";
384
+ init_git_read();
385
+ init_files();
386
+ inspections = new AsyncLocalStorage2();
387
+ sourceSpecs = SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`);
388
+ docSpecs = [":(icase,glob)**/readme.md", ":(icase,glob)**/agents.md", ":(icase,glob)**/claude.md"];
389
+ sourceSuffixes = new Set(SOURCE_EXTENSIONS.map((extension) => "." + extension));
390
+ }
391
+ });
392
+
393
+ // src/audit/inputs.ts
394
+ import fs3 from "fs/promises";
395
+ import * as nativeFs from "fs";
396
+ import path5 from "path";
397
+ import fg3 from "fast-glob";
398
+ async function auditInputPath(root, file) {
399
+ if (file === ".") return fs3.realpath(root);
400
+ try {
401
+ return await storePath(root, file);
402
+ } catch (error) {
403
+ if (error instanceof Error && error.message.startsWith("Symlink")) {
404
+ throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
405
+ }
406
+ throw error;
407
+ }
408
+ }
409
+ async function readAuditInput(root, file) {
410
+ try {
411
+ const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
412
+ if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
413
+ return value;
414
+ } catch (error) {
415
+ if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return null;
416
+ throw error;
417
+ }
418
+ }
419
+ async function auditGlob(root, patterns, options) {
420
+ if (Array.isArray(patterns) && !patterns.length) return [];
421
+ const directories = /* @__PURE__ */ new Set();
422
+ const readdir2 = ((directory, opts, callback) => {
423
+ const relative = path5.relative(root, directory).split(path5.sep).join("/") || ".";
424
+ directories.add(relative);
425
+ if (directories.size > MAX_INPUTS) {
426
+ callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
427
+ return;
428
+ }
429
+ auditInputPath(root, relative).then(() => {
430
+ nativeFs.readdir(directory, opts, callback);
431
+ }, callback);
432
+ });
433
+ const lstat2 = ((file, callback) => {
434
+ const relative = path5.relative(root, file).split(path5.sep).join("/") || ".";
435
+ auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
436
+ });
437
+ const stream = fg3.stream(patterns, {
438
+ cwd: root,
439
+ ignore: options.ignore,
440
+ onlyFiles: false,
441
+ onlyDirectories: options.onlyDirectories,
442
+ objectMode: true,
443
+ followSymbolicLinks: options.followSymbolicLinks ?? true,
444
+ fs: { readdir: readdir2, lstat: lstat2 },
445
+ concurrency: 16
446
+ });
447
+ const matches = [];
448
+ try {
449
+ for await (const value of stream) {
450
+ const entry = value;
451
+ const file = entry.path;
452
+ if (options.select && !options.select(file)) continue;
453
+ await auditInputPath(root, file);
454
+ if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
455
+ matches.push(file);
456
+ if (matches.length > MAX_INPUTS) {
457
+ const largest = [...matches.reduce((counts, item) => {
458
+ const dir = item.split("/")[0];
459
+ counts.set(dir, (counts.get(dir) ?? 0) + 1);
460
+ return counts;
461
+ }, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
462
+ throw new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} relevant paths. Largest directories: ${largest.map(([dir, count4]) => `${dir} (${count4})`).join(", ")}. Narrow this check's inputs.`);
463
+ }
464
+ }
465
+ } finally {
466
+ stream.destroy();
467
+ }
468
+ return matches.sort();
469
+ }
470
+ async function gitSourcePaths(root) {
471
+ return auditGitPaths(root, "sources");
472
+ }
473
+ async function gitIgnoredPaths(root, files) {
474
+ if (!files.length) return /* @__PURE__ */ new Set();
475
+ const ignored = /* @__PURE__ */ new Set();
476
+ for (let offset = 0; offset < files.length; offset += 1e3) {
477
+ try {
478
+ const { stdout } = await execGit(["check-ignore", "-z", "--stdin"], {
479
+ cwd: root,
480
+ maxBuffer: 4 * 1024 * 1024,
481
+ timeout: 1e4,
482
+ input: files.slice(offset, offset + 1e3).join("\0") + "\0"
483
+ });
484
+ for (const file of stdout.split("\0").filter(Boolean)) ignored.add(file);
485
+ } catch (error) {
486
+ if (error.code !== 1) throw error;
487
+ }
488
+ }
489
+ return ignored;
490
+ }
491
+ var MAX_INPUTS;
492
+ var init_inputs = __esm({
493
+ "src/audit/inputs.ts"() {
494
+ "use strict";
495
+ init_files();
496
+ init_storage();
497
+ init_git_read();
498
+ init_inspection();
499
+ MAX_INPUTS = 1e5;
500
+ }
501
+ });
502
+
503
+ // src/audit/policy.ts
504
+ import fg4 from "fast-glob";
505
+ async function includedCheckPaths(root, paths, exclude) {
506
+ if (!exclude.length) return new Set(paths);
507
+ const included = /* @__PURE__ */ new Set();
508
+ for (let offset = 0; offset < paths.length; offset += 256) {
509
+ for (const file of await auditGlob(root, paths.slice(offset, offset + 256).map((file2) => fg4.escapePath(file2)), {
510
+ ignore: exclude,
511
+ followSymbolicLinks: false,
512
+ label: "Audit check exclusions"
513
+ })) included.add(file);
514
+ }
515
+ return included;
516
+ }
517
+ var init_policy = __esm({
518
+ "src/audit/policy.ts"() {
519
+ "use strict";
520
+ init_inputs();
521
+ }
522
+ });
523
+
60
524
  // src/audit/commands.ts
61
- import path from "path";
525
+ import path6 from "path";
62
526
  function splitCommands(line) {
63
527
  const parts = [];
64
- let quote = "", start = 0;
528
+ let quote2 = "", start = 0;
65
529
  for (let i = 0; i < line.length; i++) {
66
530
  const char = line[i];
67
531
  if (char === "\\") {
68
532
  i++;
69
533
  continue;
70
534
  }
71
- if (quote) {
72
- if (char === quote) quote = "";
535
+ if (quote2) {
536
+ if (char === quote2) quote2 = "";
73
537
  continue;
74
538
  }
75
539
  if (char === "'" || char === '"') {
76
- quote = char;
540
+ quote2 = char;
77
541
  continue;
78
542
  }
79
543
  if (char === ";" || line.slice(i, i + 2) === "&&") {
@@ -94,7 +558,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
94
558
  const cd = text2.match(/^cd\s+(.+)$/);
95
559
  if (cd) {
96
560
  const directory = literal(cd[1]);
97
- cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null : path.posix.join(cwd ?? ".", directory);
561
+ cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null : path6.posix.join(cwd ?? ".", directory);
98
562
  continue;
99
563
  }
100
564
  for (const match of text2.matchAll(COMMAND)) {
@@ -105,7 +569,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
105
569
  if (shell && match.index !== 0 || match[3].startsWith("-")) scopeUnknown = "Unsupported shell wrapper or options before the script.";
106
570
  if (selector) {
107
571
  const selected = literal(selector[1]);
108
- directory = selected === null || cwd === null ? null : path.posix.join(cwd ?? ".", selected);
572
+ directory = selected === null || cwd === null ? null : path6.posix.join(cwd ?? ".", selected);
109
573
  } else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
110
574
  const tail = text2.slice(match.index + match[0].length).split(/\s--(?:\s|$)/)[0];
111
575
  if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(tail)) {
@@ -135,7 +599,7 @@ var init_commands = __esm({
135
599
  COMMAND = new RegExp(`\\b(npm|pnpm|yarn)((?:[ \\t]+${TOKEN})*?)[ \\t]+run[ \\t]+([A-Za-z0-9:_.-]+)`, "g");
136
600
  literal = (value) => {
137
601
  const unquoted = value.replace(/^(["'])(.*)\1$/, "$2");
138
- return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path.posix.isAbsolute(unquoted) ? null : unquoted;
602
+ return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path6.posix.isAbsolute(unquoted) ? null : unquoted;
139
603
  };
140
604
  }
141
605
  });
@@ -391,268 +855,52 @@ function extractClaims(content2) {
391
855
  paths: [...paths.values()],
392
856
  counts,
393
857
  commands: [...commands2.values()]
394
- };
395
- }
396
- var ROOT_FILE_NAMES, SHELL_FENCE_INFOS, markdown, COUNT_RE, COUNT_DENYLIST_RE, IGNORE_LINE, IGNORE_START, IGNORE_END;
397
- var init_claims = __esm({
398
- "src/audit/claims.ts"() {
399
- "use strict";
400
- init_commands();
401
- init_tree();
402
- ROOT_FILE_NAMES = /* @__PURE__ */ new Set([
403
- "package.json",
404
- "package-lock.json",
405
- "pnpm-workspace.yaml",
406
- "tsconfig.json",
407
- "tsup.config.ts",
408
- "vitest.config.ts",
409
- "Makefile",
410
- "Dockerfile",
411
- "docker-compose.yml",
412
- "Cargo.toml",
413
- "go.mod",
414
- "go.sum",
415
- "pyproject.toml",
416
- "requirements.txt",
417
- "Gemfile",
418
- "composer.json",
419
- "settings.gradle.kts",
420
- "settings.gradle",
421
- "build.gradle.kts",
422
- "build.gradle",
423
- "manifest.json",
424
- "server.json",
425
- "README.md",
426
- "CHANGELOG.md",
427
- "LICENSE",
428
- "CLAUDE.md",
429
- "AGENTS.md",
430
- ".gitignore",
431
- ".env.example"
432
- ]);
433
- SHELL_FENCE_INFOS = /* @__PURE__ */ new Set(["", "bash", "sh", "shell", "console", "zsh"]);
434
- markdown = new MarkdownIt("commonmark", { html: false });
435
- COUNT_RE = /(\d+)\s+(modules?|packages?|workspaces?|crates?)\b/gi;
436
- COUNT_DENYLIST_RE = /^\s*(manager|registr|lock|json)/i;
437
- IGNORE_LINE = "<!-- mason:ignore -->";
438
- IGNORE_START = "<!-- mason:ignore-start -->";
439
- IGNORE_END = "<!-- mason:ignore-end -->";
440
- }
441
- });
442
-
443
- // src/utils/paths.ts
444
- import path2 from "path";
445
- function normalizeRepoPath(value) {
446
- const slash = value.replace(/\\/g, "/");
447
- if (!slash || slash.includes("\0") || path2.posix.isAbsolute(slash) || /^[A-Za-z]:/.test(slash)) return null;
448
- if (slash.split("/").includes("..")) return null;
449
- const normalized = path2.posix.normalize(slash).replace(/\/$/, "");
450
- return normalized === "." ? null : normalized;
451
- }
452
- function sanitizeRepoPaths(files) {
453
- return [...new Set(files.map(normalizeRepoPath).filter((p) => p !== null))];
454
- }
455
- function isWithinRoot(root, candidate) {
456
- const relative = path2.relative(root, candidate);
457
- return relative !== ".." && !relative.startsWith(`..${path2.sep}`) && !path2.isAbsolute(relative);
458
- }
459
- function anchorMatches(anchor, file) {
460
- const a = normalizeRepoPath(anchor);
461
- const f = normalizeRepoPath(file);
462
- return a !== null && f !== null && (a === f || f.startsWith(`${a}/`));
463
- }
464
- function matchingPaths(anchors, files) {
465
- return [...new Set(files)].filter((file) => anchors.some((anchor) => anchorMatches(anchor, file)));
466
- }
467
- var init_paths = __esm({
468
- "src/utils/paths.ts"() {
469
- "use strict";
470
- }
471
- });
472
-
473
- // src/utils/files.ts
474
- import fs from "fs/promises";
475
- import { constants } from "fs";
476
- import path3 from "path";
477
- import fg from "fast-glob";
478
- function isSensitiveFile(file) {
479
- return file.split(/[\\/]/).some(
480
- (part) => /^(?:\.env(?:\..*)?|id_rsa.*|id_ed25519.*)$|\.(?:pem|key|p12|pfx|jks|keystore)$|credentials\.|secret|^local\.properties$/i.test(part)
481
- );
482
- }
483
- async function readBoundedFile(file, maxBytes) {
484
- const handle = await fs.open(file, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
485
- try {
486
- const stat = await handle.stat();
487
- if (!stat.isFile() || stat.size > maxBytes) return null;
488
- const buffer = Buffer.alloc(Math.min(maxBytes + 1, stat.size + 1));
489
- let bytes = 0;
490
- while (bytes < buffer.length) {
491
- const result = await handle.read(buffer, bytes, buffer.length - bytes, null);
492
- if (result.bytesRead === 0) break;
493
- bytes += result.bytesRead;
494
- }
495
- return bytes === buffer.length ? null : buffer.subarray(0, bytes).toString("utf8");
496
- } finally {
497
- await handle.close();
498
- }
499
- }
500
- async function loadProjectConfig(root) {
501
- try {
502
- const canonicalRoot = await fs.realpath(root);
503
- const configPath2 = await fs.realpath(path3.join(root, ".mason/config.json"));
504
- if (!isWithinRoot(canonicalRoot, configPath2)) throw new Error("Project configuration resolves outside the repository");
505
- const raw = await readBoundedFile(configPath2, 64 * 1024);
506
- if (raw === null) throw new Error("Project configuration is not a regular file or exceeds 64 KiB");
507
- const value = JSON.parse(raw);
508
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected a configuration object");
509
- const config = {};
510
- for (const key of ["patterns", "alwaysInclude", "ignore"]) {
511
- if (value[key] === void 0) continue;
512
- if (!Array.isArray(value[key]) || !value[key].every((s) => typeof s === "string")) {
513
- throw new Error(`Configuration ${key} must be an array of strings`);
514
- }
515
- config[key] = value[key];
516
- }
517
- return config;
518
- } catch (error) {
519
- if (error.code === "ENOENT") return {};
520
- throw new Error(`Cannot apply project file policy: ${error instanceof Error ? error.message : String(error)}`);
521
- }
522
- }
523
- async function createFileAccess(rootDir) {
524
- const root = path3.resolve(rootDir);
525
- const canonicalRoot = await fs.realpath(root).catch(() => root);
526
- const config = await loadProjectConfig(root);
527
- const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
528
- let gitFiles = null;
529
- try {
530
- const { stdout } = await execGit(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], { cwd: root, maxBuffer: 50 * 1024 * 1024 });
531
- gitFiles = new Set(stdout.split("\0").filter(Boolean));
532
- } catch {
533
- let inGit = false;
534
- try {
535
- await execGit(["rev-parse", "--git-dir"], { cwd: root });
536
- inGit = true;
537
- } catch {
538
- }
539
- if (inGit) throw new Error("Cannot enumerate Git files safely");
540
- }
541
- async function resolve2(file) {
542
- const relative = normalizeRepoPath(file);
543
- if (!relative || isSensitiveFile(relative) || gitFiles && !gitFiles.has(relative)) return null;
544
- const candidate = path3.join(root, relative);
545
- try {
546
- const real = await fs.realpath(candidate);
547
- if (!isWithinRoot(canonicalRoot, real) || isSensitiveFile(path3.relative(canonicalRoot, real))) return null;
548
- const stat = await fs.stat(real);
549
- if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES) return null;
550
- if (gitFiles && !gitFiles.has(path3.relative(canonicalRoot, real).split(path3.sep).join("/"))) return null;
551
- return real;
552
- } catch {
553
- return null;
554
- }
555
- }
556
- async function list(patterns = SOURCE_GLOB, options = {}) {
557
- const found = await fg(patterns, { cwd: root, ignore, followSymbolicLinks: false, ...options });
558
- const safe = await Promise.all(found.map(async (f) => await resolve2(f) ? f : null));
559
- return safe.filter((f) => f !== null).sort();
560
- }
561
- async function read(file) {
562
- const relative = normalizeRepoPath(file);
563
- if (!relative) return null;
564
- const real = await resolve2(relative);
565
- if (!real) return null;
566
- for (const rel of /* @__PURE__ */ new Set([relative, path3.relative(canonicalRoot, real).split(path3.sep).join("/")])) {
567
- if (!(await fg(fg.escapePath(rel), { cwd: root, ignore, dot: true })).length) return null;
568
- }
569
- try {
570
- const content2 = await readBoundedFile(real, MAX_SOURCE_BYTES);
571
- return content2 === null ? null : { path: relative, content: content2, totalLines: content2.split("\n").length };
572
- } catch {
573
- return null;
574
- }
575
- }
576
- return { root, config, list, read };
577
- }
578
- var SOURCE_EXTENSIONS, SOURCE_GLOB, SOURCE_IGNORE, MAX_SOURCE_BYTES;
579
- var init_files = __esm({
580
- "src/utils/files.ts"() {
581
- "use strict";
582
- init_git_read();
583
- init_paths();
584
- SOURCE_EXTENSIONS = ["ts", "tsx", "js", "jsx", "mts", "cts", "mjs", "cjs", "vue", "svelte", "kt", "kts", "java", "py", "go", "rs", "swift", "rb", "cs", "cpp", "c", "h", "hpp", "dart", "php"];
585
- SOURCE_GLOB = `**/*.{${SOURCE_EXTENSIONS.join(",")}}`;
586
- SOURCE_IGNORE = [
587
- "**/node_modules/**",
588
- "**/dist/**",
589
- "**/build/**",
590
- "**/.gradle/**",
591
- "**/target/**",
592
- "**/.git/**",
593
- "**/.mason/**",
594
- "**/vendor/**",
595
- "**/__pycache__/**",
596
- "**/venv/**",
597
- "**/.venv/**",
598
- "**/*.min.*",
599
- "**/*.map",
600
- "**/*.lock",
601
- "**/generated/**",
602
- "**/*.generated.*",
603
- "**/R.java",
604
- "**/BuildConfig.java",
605
- "**/package-lock.json",
606
- "**/yarn.lock",
607
- "**/pnpm-lock.yaml"
608
- ];
609
- MAX_SOURCE_BYTES = 1024 * 1024;
610
- }
611
- });
612
-
613
- // src/audit/inspection.ts
614
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
615
- import path4 from "path";
616
- function withRepositoryInspection(root, run) {
617
- return inspections.run({ root: path4.resolve(root), reads: /* @__PURE__ */ new Map() }, run);
618
- }
619
- function inspectionRead(root, key, read) {
620
- const inspection = inspections.getStore();
621
- if (!inspection || inspection.root !== path4.resolve(root)) return read();
622
- if (!inspection.reads.has(key)) {
623
- const pending = read();
624
- inspection.reads.set(key, pending);
625
- void pending.catch(() => {
626
- if (inspection.reads.get(key) === pending) inspection.reads.delete(key);
627
- });
628
- }
629
- return inspection.reads.get(key);
630
- }
631
- function inspectionGit(args, options) {
632
- return inspectionRead(options.cwd, "git:" + JSON.stringify([args, options]), () => execGit(args, options));
633
- }
634
- async function auditGitPaths(root, kind) {
635
- const shared = inspections.getStore()?.root === path4.resolve(root);
636
- const specs = shared ? [...docSpecs, ...sourceSpecs] : kind === "documents" ? docSpecs : sourceSpecs;
637
- const limit = kind === "documents" ? 16 * 1024 * 1024 : 32 * 1024 * 1024;
638
- const { stdout } = await inspectionGit(
639
- ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...specs],
640
- { cwd: root, maxBuffer: shared ? 48 * 1024 * 1024 : limit, timeout: 1e4 }
641
- );
642
- const paths = [...new Set(stdout.split("\0").filter(Boolean))].filter((file) => kind === "documents" ? /^(readme|agents|claude)\.md$/i.test(path4.posix.basename(file)) : sourceSuffixes.has(file.slice(file.lastIndexOf("."))));
643
- if (Buffer.byteLength(paths.join("\0")) > limit) throw new Error("Git " + kind + " inventory exceeds its bounded read limit.");
644
- return paths.sort();
645
- }
646
- var inspections, sourceSpecs, docSpecs, sourceSuffixes;
647
- var init_inspection = __esm({
648
- "src/audit/inspection.ts"() {
858
+ };
859
+ }
860
+ var ROOT_FILE_NAMES, SHELL_FENCE_INFOS, markdown, COUNT_RE, COUNT_DENYLIST_RE, IGNORE_LINE, IGNORE_START, IGNORE_END;
861
+ var init_claims = __esm({
862
+ "src/audit/claims.ts"() {
649
863
  "use strict";
650
- init_git_read();
651
- init_files();
652
- inspections = new AsyncLocalStorage2();
653
- sourceSpecs = SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`);
654
- docSpecs = [":(icase,glob)**/readme.md", ":(icase,glob)**/agents.md", ":(icase,glob)**/claude.md"];
655
- sourceSuffixes = new Set(SOURCE_EXTENSIONS.map((extension) => "." + extension));
864
+ init_commands();
865
+ init_tree();
866
+ ROOT_FILE_NAMES = /* @__PURE__ */ new Set([
867
+ "package.json",
868
+ "package-lock.json",
869
+ "pnpm-workspace.yaml",
870
+ "tsconfig.json",
871
+ "tsup.config.ts",
872
+ "vitest.config.ts",
873
+ "Makefile",
874
+ "Dockerfile",
875
+ "docker-compose.yml",
876
+ "Cargo.toml",
877
+ "go.mod",
878
+ "go.sum",
879
+ "pyproject.toml",
880
+ "requirements.txt",
881
+ "Gemfile",
882
+ "composer.json",
883
+ "settings.gradle.kts",
884
+ "settings.gradle",
885
+ "build.gradle.kts",
886
+ "build.gradle",
887
+ "manifest.json",
888
+ "server.json",
889
+ "README.md",
890
+ "CHANGELOG.md",
891
+ "LICENSE",
892
+ "CLAUDE.md",
893
+ "AGENTS.md",
894
+ ".gitignore",
895
+ ".env.example"
896
+ ]);
897
+ SHELL_FENCE_INFOS = /* @__PURE__ */ new Set(["", "bash", "sh", "shell", "console", "zsh"]);
898
+ markdown = new MarkdownIt("commonmark", { html: false });
899
+ COUNT_RE = /(\d+)\s+(modules?|packages?|workspaces?|crates?)\b/gi;
900
+ COUNT_DENYLIST_RE = /^\s*(manager|registr|lock|json)/i;
901
+ IGNORE_LINE = "<!-- mason:ignore -->";
902
+ IGNORE_START = "<!-- mason:ignore-start -->";
903
+ IGNORE_END = "<!-- mason:ignore-end -->";
656
904
  }
657
905
  });
658
906
 
@@ -741,190 +989,10 @@ var init_git = __esm({
741
989
  }
742
990
  });
743
991
 
744
- // src/utils/storage.ts
745
- import fs2 from "fs/promises";
746
- import path5 from "path";
747
- import { randomUUID } from "crypto";
748
- async function storePath(root, relative, createParents = false) {
749
- const normalized = normalizeRepoPath(relative);
750
- if (!normalized) throw new Error(`Invalid store path: ${relative}`);
751
- let current = await fs2.realpath(root);
752
- const parts = normalized.split("/");
753
- for (let i = 0; i < parts.length; i++) {
754
- current = path5.join(current, parts[i]);
755
- let stat;
756
- try {
757
- stat = await fs2.lstat(current);
758
- } catch (error) {
759
- if (error.code !== "ENOENT") throw error;
760
- if (createParents && i < parts.length - 1) {
761
- try {
762
- await fs2.mkdir(current);
763
- } catch (mkdirError) {
764
- if (mkdirError.code !== "EEXIST") throw mkdirError;
765
- }
766
- stat = await fs2.lstat(current);
767
- }
768
- }
769
- if (stat?.isSymbolicLink()) throw new Error(`Symlink in store path: ${relative}`);
770
- }
771
- return current;
772
- }
773
- async function readStoreJson(root, relative) {
774
- try {
775
- const file = await storePath(root, relative);
776
- const raw = await readBoundedFile(file, 10 * 1024 * 1024);
777
- if (raw === null) throw new Error("file is not regular or exceeds 10 MiB");
778
- const parsed = JSON.parse(raw);
779
- if (parsed === null) throw new Error("expected a JSON object, received null");
780
- return parsed;
781
- } catch (error) {
782
- if (error.code === "ENOENT") return null;
783
- throw new Error(`Invalid Mason store ${relative}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
784
- }
785
- }
786
- async function writeStoreJson(root, relative, value) {
787
- const payload = JSON.stringify(value, null, 2) + "\n";
788
- if (Buffer.byteLength(payload) > 10 * 1024 * 1024) {
789
- throw new Error(`Mason store ${relative} exceeds 10 MiB`);
790
- }
791
- const file = await storePath(root, relative, true);
792
- const temporary = path5.join(path5.dirname(file), `.${path5.basename(file)}.${randomUUID()}.tmp`);
793
- try {
794
- const handle = await fs2.open(temporary, "wx", 384);
795
- try {
796
- await handle.writeFile(payload, "utf8");
797
- await handle.sync();
798
- } finally {
799
- await handle.close();
800
- }
801
- await fs2.rename(temporary, file);
802
- } finally {
803
- await fs2.rm(temporary, { force: true });
804
- }
805
- }
806
- var init_storage = __esm({
807
- "src/utils/storage.ts"() {
808
- "use strict";
809
- init_paths();
810
- init_files();
811
- }
812
- });
813
-
814
- // src/audit/inputs.ts
815
- import fs3 from "fs/promises";
816
- import * as nativeFs from "fs";
817
- import path6 from "path";
818
- import fg2 from "fast-glob";
819
- async function auditInputPath(root, file) {
820
- if (file === ".") return fs3.realpath(root);
821
- try {
822
- return await storePath(root, file);
823
- } catch (error) {
824
- if (error instanceof Error && error.message.startsWith("Symlink")) {
825
- throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
826
- }
827
- throw error;
828
- }
829
- }
830
- async function readAuditInput(root, file) {
831
- try {
832
- const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
833
- if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
834
- return value;
835
- } catch (error) {
836
- if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return null;
837
- throw error;
838
- }
839
- }
840
- async function auditGlob(root, patterns, options) {
841
- if (Array.isArray(patterns) && !patterns.length) return [];
842
- const directories = /* @__PURE__ */ new Set();
843
- const readdir2 = ((directory, opts, callback) => {
844
- const relative = path6.relative(root, directory).split(path6.sep).join("/") || ".";
845
- directories.add(relative);
846
- if (directories.size > MAX_INPUTS) {
847
- callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
848
- return;
849
- }
850
- auditInputPath(root, relative).then(() => {
851
- nativeFs.readdir(directory, opts, callback);
852
- }, callback);
853
- });
854
- const lstat2 = ((file, callback) => {
855
- const relative = path6.relative(root, file).split(path6.sep).join("/") || ".";
856
- auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
857
- });
858
- const stream = fg2.stream(patterns, {
859
- cwd: root,
860
- ignore: options.ignore,
861
- onlyFiles: false,
862
- onlyDirectories: options.onlyDirectories,
863
- objectMode: true,
864
- followSymbolicLinks: options.followSymbolicLinks ?? true,
865
- fs: { readdir: readdir2, lstat: lstat2 },
866
- concurrency: 16
867
- });
868
- const matches = [];
869
- try {
870
- for await (const value of stream) {
871
- const entry = value;
872
- const file = entry.path;
873
- if (options.select && !options.select(file)) continue;
874
- await auditInputPath(root, file);
875
- if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
876
- matches.push(file);
877
- if (matches.length > MAX_INPUTS) {
878
- const largest = [...matches.reduce((counts, item) => {
879
- const dir = item.split("/")[0];
880
- counts.set(dir, (counts.get(dir) ?? 0) + 1);
881
- return counts;
882
- }, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
883
- throw new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} relevant paths. Largest directories: ${largest.map(([dir, count4]) => `${dir} (${count4})`).join(", ")}. Narrow this check's inputs.`);
884
- }
885
- }
886
- } finally {
887
- stream.destroy();
888
- }
889
- return matches.sort();
890
- }
891
- async function gitSourcePaths(root) {
892
- return auditGitPaths(root, "sources");
893
- }
894
- async function gitIgnoredPaths(root, files) {
895
- if (!files.length) return /* @__PURE__ */ new Set();
896
- const ignored = /* @__PURE__ */ new Set();
897
- for (let offset = 0; offset < files.length; offset += 1e3) {
898
- try {
899
- const { stdout } = await execGit(["check-ignore", "-z", "--stdin"], {
900
- cwd: root,
901
- maxBuffer: 4 * 1024 * 1024,
902
- timeout: 1e4,
903
- input: files.slice(offset, offset + 1e3).join("\0") + "\0"
904
- });
905
- for (const file of stdout.split("\0").filter(Boolean)) ignored.add(file);
906
- } catch (error) {
907
- if (error.code !== 1) throw error;
908
- }
909
- }
910
- return ignored;
911
- }
912
- var MAX_INPUTS;
913
- var init_inputs = __esm({
914
- "src/audit/inputs.ts"() {
915
- "use strict";
916
- init_files();
917
- init_storage();
918
- init_git_read();
919
- init_inspection();
920
- MAX_INPUTS = 1e5;
921
- }
922
- });
923
-
924
992
  // src/audit/docs.ts
925
993
  import fs4 from "fs/promises";
926
994
  import path7 from "path";
927
- import fg3 from "fast-glob";
995
+ import fg5 from "fast-glob";
928
996
  async function dirtyDocs(resolvedRoot, files) {
929
997
  if (!files.length) return /* @__PURE__ */ new Set();
930
998
  try {
@@ -976,7 +1044,7 @@ async function discoverDocPaths(root) {
976
1044
  const paths = [];
977
1045
  const unique = [...new Set(candidates)].sort();
978
1046
  for (let offset = 0; offset < unique.length; offset += 256) {
979
- paths.push(...await auditGlob(root, unique.slice(offset, offset + 256).map((p) => fg3.escapePath(p)), {
1047
+ paths.push(...await auditGlob(root, unique.slice(offset, offset + 256).map((p) => fg5.escapePath(p)), {
980
1048
  ignore: excluded,
981
1049
  followSymbolicLinks: false,
982
1050
  label: "Documentation discovery"
@@ -1006,6 +1074,7 @@ async function localDocPaths(root) {
1006
1074
  return paths;
1007
1075
  }
1008
1076
  function documentScope(file) {
1077
+ if (!/^(readme|agents|claude)\.md$/i.test(path7.posix.basename(file))) return ".";
1009
1078
  const parent = path7.posix.dirname(file);
1010
1079
  return path7.posix.basename(parent).toLowerCase() === ".claude" ? path7.posix.dirname(parent) : parent;
1011
1080
  }
@@ -1027,7 +1096,7 @@ async function readDocs(resolvedRoot) {
1027
1096
  path: candidate,
1028
1097
  content: content2,
1029
1098
  scope: documentScope(candidate),
1030
- kind: /^readme\.md$/i.test(path7.posix.basename(candidate)) ? "readme" : "instructions",
1099
+ kind: /^readme\.md$/i.test(path7.posix.basename(candidate)) ? "readme" : /^(agents|claude)\.md$/i.test(path7.posix.basename(candidate)) ? "instructions" : "guide",
1031
1100
  lineCount: content2.split("\n").length,
1032
1101
  lastCommit,
1033
1102
  dirty: dirty.has(candidate),
@@ -1569,34 +1638,18 @@ async function computeDrift(rootDir) {
1569
1638
  report.stale ||= changedMapped.size > 0 || report.unmappedFiles.length > 0 || committedGhosts.length > 0;
1570
1639
  if (!report.historyAvailable) report.recommendation = "full-rebuild";
1571
1640
  else if (!report.stale) report.recommendation = "up-to-date";
1572
- else report.recommendation = changedMapped.size >= FULL_REBUILD_MIN_CHANGED_MAPPED_FILES && changedMapped.size / Math.max(1, mappedFiles.size) > FULL_REBUILD_FRACTION ? "full-rebuild" : "incremental";
1573
- return report;
1574
- }
1575
- var FULL_REBUILD_FRACTION, FULL_REBUILD_MIN_CHANGED_MAPPED_FILES;
1576
- var init_drift = __esm({
1577
- "src/drift/drift.ts"() {
1578
- "use strict";
1579
- init_git_read();
1580
- init_snapshot();
1581
- init_paths();
1582
- FULL_REBUILD_FRACTION = 0.4;
1583
- FULL_REBUILD_MIN_CHANGED_MAPPED_FILES = 10;
1584
- }
1585
- });
1586
-
1587
- // src/audit/types.ts
1588
- var ALL_CHECKS;
1589
- var init_types = __esm({
1590
- "src/audit/types.ts"() {
1591
- "use strict";
1592
- ALL_CHECKS = [
1593
- "deleted-reference",
1594
- "new-module",
1595
- "stale-count",
1596
- "dead-command",
1597
- "deps-changed",
1598
- "decision-anchor-drift"
1599
- ];
1641
+ else report.recommendation = changedMapped.size >= FULL_REBUILD_MIN_CHANGED_MAPPED_FILES && changedMapped.size / Math.max(1, mappedFiles.size) > FULL_REBUILD_FRACTION ? "full-rebuild" : "incremental";
1642
+ return report;
1643
+ }
1644
+ var FULL_REBUILD_FRACTION, FULL_REBUILD_MIN_CHANGED_MAPPED_FILES;
1645
+ var init_drift = __esm({
1646
+ "src/drift/drift.ts"() {
1647
+ "use strict";
1648
+ init_git_read();
1649
+ init_snapshot();
1650
+ init_paths();
1651
+ FULL_REBUILD_FRACTION = 0.4;
1652
+ FULL_REBUILD_MIN_CHANGED_MAPPED_FILES = 10;
1600
1653
  }
1601
1654
  });
1602
1655
 
@@ -1668,7 +1721,7 @@ var init_deleted_reference = __esm({
1668
1721
  });
1669
1722
 
1670
1723
  // src/audit/checks/new-module.ts
1671
- import fg4 from "fast-glob";
1724
+ import fg6 from "fast-glob";
1672
1725
  import fs8 from "fs/promises";
1673
1726
  import path13 from "path";
1674
1727
  function escapeRegExp(text2) {
@@ -1730,7 +1783,7 @@ async function collectModuleCandidates(root, combinedDocs) {
1730
1783
  const result = /* @__PURE__ */ new Map();
1731
1784
  for (const dir of parents) {
1732
1785
  const visible = new Set(byParent.get(dir).filter((candidate) => !ignored.has(candidate) || sources.some((file) => file.startsWith(candidate + "/"))));
1733
- result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${fg4.escapePath(dir)}/*`, {
1786
+ result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${fg6.escapePath(dir)}/*`, {
1734
1787
  ignore,
1735
1788
  onlyDirectories: true,
1736
1789
  label: "Module directory discovery",
@@ -1743,7 +1796,7 @@ async function collectModuleCandidates(root, combinedDocs) {
1743
1796
  const files = sources.filter((file) => file.startsWith(dir + "/"));
1744
1797
  let count4 = 0;
1745
1798
  for (let offset = 0; offset < files.length; offset += 256) {
1746
- count4 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg4.escapePath(file)), {
1799
+ count4 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg6.escapePath(file)), {
1747
1800
  ignore,
1748
1801
  followSymbolicLinks: false,
1749
1802
  label: `Source discovery in ${dir}`
@@ -2078,6 +2131,87 @@ var init_dead_command = __esm({
2078
2131
  }
2079
2132
  });
2080
2133
 
2134
+ // src/audit/dependency-claims.ts
2135
+ import path16 from "path";
2136
+ import { parse as parseToml } from "smol-toml";
2137
+ function dependencyText(content2) {
2138
+ return content2.replace(/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g, (text2) => text2.replace(/[^\n]/g, " ")).replace(/<!--[\s\S]*?-->/g, (text2) => text2.replace(/[^\n]/g, " "));
2139
+ }
2140
+ function declarations(file, source) {
2141
+ const result = /* @__PURE__ */ new Map();
2142
+ if (!source) return result;
2143
+ if (path16.posix.basename(file) === "package.json") {
2144
+ const json = JSON.parse(source);
2145
+ for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies", "engines"]) {
2146
+ for (const [name, value] of Object.entries(json[section] ?? {})) {
2147
+ if (typeof value === "string") result.set(name, [...result.has(name) ? [result.get(name)] : [], value].join("; "));
2148
+ }
2149
+ }
2150
+ } else if (file.endsWith("libs.versions.toml")) {
2151
+ const catalog = parseToml(source);
2152
+ for (const section of ["libraries", "plugins"]) for (const [alias, entry] of Object.entries(catalog[section] ?? {})) {
2153
+ if (typeof entry === "string") {
2154
+ const parts = entry.split(":");
2155
+ if (parts.length >= 3) result.set(parts.slice(0, -1).join(":"), parts.at(-1));
2156
+ } else if (entry && typeof entry === "object") {
2157
+ const name = entry.module ?? (entry.group && entry.name ? `${entry.group}:${entry.name}` : entry.id);
2158
+ const version = typeof entry.version === "string" ? entry.version : catalog.versions?.[entry.version?.ref];
2159
+ if (typeof name === "string") result.set(name, typeof version === "string" ? version : "unspecified");
2160
+ }
2161
+ }
2162
+ } else if (/build\.gradle(?:\.kts)?$/.test(file)) {
2163
+ for (const match of source.matchAll(/["']([\w.-]+:[\w.-]+):([^"'\s]+)["']/g)) result.set(match[1], match[2]);
2164
+ for (const match of source.matchAll(/\bid\s*\(?\s*["']([\w.-]+)["']\s*\)?\s*version\s*["']([^"']+)["']/g)) result.set(match[1], match[2]);
2165
+ }
2166
+ return result;
2167
+ }
2168
+ async function matchDependencyChanges(root, from, head, files, content2) {
2169
+ const matches = [];
2170
+ const lines = dependencyText(content2).split("\n");
2171
+ let incomplete = files.length > 64;
2172
+ for (const file of files.slice(0, 64)) {
2173
+ if (!/(?:^|\/)(?:package\.json|libs\.versions\.toml|build\.gradle(?:\.kts)?)$/.test(file)) {
2174
+ incomplete = true;
2175
+ continue;
2176
+ }
2177
+ try {
2178
+ const read = async (revision) => {
2179
+ const { stdout: entry } = await inspectionGit(["ls-tree", revision, "--", `:(literal)${file}`], { cwd: root, timeout: 1e4 });
2180
+ if (!entry) return "";
2181
+ if (!entry.startsWith("100644 ") && !entry.startsWith("100755 ")) throw new Error("Unsupported manifest mode");
2182
+ return (await inspectionGit(["show", `${revision}:${file}`], { cwd: root, maxBuffer: 1024 * 1024, timeout: 1e4 })).stdout;
2183
+ };
2184
+ const [before, after] = await Promise.all([read(from), read(head)]);
2185
+ const old = declarations(file, before), next = declarations(file, after);
2186
+ for (const dependency of /* @__PURE__ */ new Set([...old.keys(), ...next.keys()])) {
2187
+ if (old.get(dependency) === next.get(dependency)) continue;
2188
+ const escaped = dependency.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2189
+ const mention = new RegExp(`(^|[^a-zA-Z0-9_@./-])${escaped}(?![a-zA-Z0-9_/-]|\\.[a-zA-Z0-9_])`, "i");
2190
+ const line = lines.findIndex((text2) => mention.test(text2));
2191
+ if (line < 0) continue;
2192
+ matches.push({
2193
+ dependency,
2194
+ manifest: file,
2195
+ before: old.get(dependency) ?? null,
2196
+ after: next.get(dependency) ?? null,
2197
+ line: line + 1,
2198
+ excerpt: lines[line].trim().slice(0, 500)
2199
+ });
2200
+ if (matches.length >= 100) return { matches, incomplete: true };
2201
+ }
2202
+ } catch {
2203
+ incomplete = true;
2204
+ }
2205
+ }
2206
+ return { matches, incomplete };
2207
+ }
2208
+ var init_dependency_claims = __esm({
2209
+ "src/audit/dependency-claims.ts"() {
2210
+ "use strict";
2211
+ init_inspection();
2212
+ }
2213
+ });
2214
+
2081
2215
  // src/audit/release-metadata.ts
2082
2216
  async function git(root, args) {
2083
2217
  return (await execGit(args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
@@ -2134,6 +2268,10 @@ var init_release_metadata = __esm({
2134
2268
  });
2135
2269
 
2136
2270
  // src/audit/checks/deps-changed.ts
2271
+ function hasDependencyContent(content2) {
2272
+ const text2 = content2.replace(/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g, "").replace(/<!--[\s\S]*?-->/g, "");
2273
+ return /\b(?:dependenc(?:y|ies)|librar(?:y|ies)|frameworks?|tech(?:nology)? stack|prerequisites|requirements)\b/i.test(text2) || /\b(?:package\.json|build\.gradle(?:\.kts)?|settings\.gradle(?:\.kts)?|libs\.versions\.toml|Cargo\.toml|go\.mod|pyproject\.toml|requirements\.txt|Gemfile|composer\.json)\b/i.test(text2) || /\b(?:node(?:\.js)?|npm|pnpm|yarn|bun|deno|python|ruby|rust|go|java|jdk|kotlin|gradle|swift|php|react|vue|angular|compose|ktor|room)\s*(?:version\s*)?[`*:=>~^v\s-]*\d+(?:\.\d+)*\b/i.test(text2) || /\b(?:npm|pnpm|yarn|pip3?|cargo|gem|composer)\s+(?:install|add|require)\b/i.test(text2);
2274
+ }
2137
2275
  function manifestPathspecs(doc) {
2138
2276
  const scope = documentScope(doc);
2139
2277
  if (scope === ".") return MANIFEST_PATHSPECS;
@@ -2153,12 +2291,17 @@ async function checkDepsChanged(ctx) {
2153
2291
  });
2154
2292
  continue;
2155
2293
  }
2294
+ let content2 = doc.content;
2156
2295
  if (doc.dirty) {
2157
- result.skipped.push({
2158
- check: "deps-changed",
2159
- doc: doc.path,
2160
- reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
2161
- });
2296
+ try {
2297
+ content2 = (await inspectionGit(
2298
+ ["show", `${doc.lastCommit.hash}:${doc.path}`],
2299
+ { cwd: ctx.root, maxBuffer: 10 * 1024 * 1024, timeout: 1e4 }
2300
+ )).stdout;
2301
+ } catch {
2302
+ result.skipped.push({ check: "deps-changed", doc: doc.path, reason: `${doc.path}: committed dependency content is unavailable` });
2303
+ continue;
2304
+ }
2162
2305
  }
2163
2306
  const range = await commitsTouchingSince(
2164
2307
  ctx.root,
@@ -2183,16 +2326,32 @@ async function checkDepsChanged(ctx) {
2183
2326
  range.commits = relevant;
2184
2327
  range.total = relevant.length;
2185
2328
  if (range.total === 0) continue;
2186
- const latest = range.commits[0];
2329
+ const relevance = await matchDependencyChanges(
2330
+ ctx.root,
2331
+ doc.lastCommit.hash,
2332
+ ctx.headHash,
2333
+ [...new Set(range.commits.flatMap((commit) => commit.files))],
2334
+ content2
2335
+ );
2336
+ if (!relevance.matches.length && !hasDependencyContent(content2)) continue;
2337
+ const match = relevance.matches[0];
2338
+ if (doc.dirty && match) result.skipped.push({
2339
+ check: "deps-changed",
2340
+ doc: doc.path,
2341
+ reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
2342
+ });
2187
2343
  (doc.dirty ? result.suppressedAdvisories : result.advisories).push({
2188
2344
  type: "deps-changed",
2189
- message: `dependency manifests touched by ${range.total} commit${range.total === 1 ? "" : "s"} since ${doc.path} was last committed (latest: ${latest.hash.slice(0, 7)} "${latest.subject}")`,
2190
- anchor: { doc: doc.path, line: null, excerpt: null },
2345
+ ...match ? {} : { resolution: "informational" },
2346
+ message: match ? `${match.dependency} changed from ${match.before ?? "absent"} to ${match.after ?? "removed"} in ${match.manifest}; review ${doc.path}:${match.line}: ${match.excerpt}${relevance.matches.length > 1 ? ` (${relevance.matches.length - 1} more matches in evidence)` : ""}` : `Dependency manifests changed after the document baseline ${doc.lastCommit.hash.slice(0, 7)} (${doc.lastCommit.date.slice(0, 10)}); no specific affected passage was established. Informational only.${relevance.incomplete ? " Some manifest declarations could not be matched." : ""}`,
2347
+ anchor: { doc: doc.path, line: match?.line ?? null, excerpt: match?.excerpt ?? null },
2191
2348
  evidence: {
2192
2349
  kind: "doc-behind-manifests",
2193
2350
  docLastCommit: doc.lastCommit,
2194
2351
  manifestCommits: range.commits.slice(0, MANIFEST_COMMITS_CAP),
2195
- totalCommits: range.total
2352
+ totalCommits: range.total,
2353
+ matches: relevance.matches,
2354
+ matchingIncomplete: relevance.incomplete
2196
2355
  }
2197
2356
  });
2198
2357
  }
@@ -2202,9 +2361,11 @@ var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
2202
2361
  var init_deps_changed = __esm({
2203
2362
  "src/audit/checks/deps-changed.ts"() {
2204
2363
  "use strict";
2364
+ init_dependency_claims();
2205
2365
  init_docs();
2206
2366
  init_release_metadata();
2207
2367
  init_git();
2368
+ init_inspection();
2208
2369
  init_checks();
2209
2370
  MANIFEST_COMMITS_CAP = 10;
2210
2371
  MANIFEST_PATHSPECS = [
@@ -2225,7 +2386,7 @@ var init_deps_changed = __esm({
2225
2386
  });
2226
2387
 
2227
2388
  // src/context/lexical.ts
2228
- import path16 from "path";
2389
+ import path17 from "path";
2229
2390
  function tokenize(text2) {
2230
2391
  return text2.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2 && !STOPWORDS.has(t));
2231
2392
  }
@@ -2238,7 +2399,7 @@ function tokenSet(text2) {
2238
2399
  function scoreEntry(taskTokens, entry) {
2239
2400
  const nameTokens = tokenSet(entry.name);
2240
2401
  const descTokens = tokenSet(entry.description);
2241
- const fileTokens = tokenSet(entry.files.map((f) => path16.basename(f)).join(" "));
2402
+ const fileTokens = tokenSet(entry.files.map((f) => path17.basename(f)).join(" "));
2242
2403
  let score = 0;
2243
2404
  for (const token of taskTokens) {
2244
2405
  if (nameTokens.has(token)) score += 3;
@@ -2572,7 +2733,7 @@ __export(decisions_exports, {
2572
2733
  withDecisionWrite: () => withDecisionWrite
2573
2734
  });
2574
2735
  import fs9 from "fs/promises";
2575
- import path17 from "path";
2736
+ import path18 from "path";
2576
2737
  import { createHash } from "crypto";
2577
2738
  import { z as z3 } from "zod";
2578
2739
  async function loadDecisionStore(rootDir) {
@@ -2670,7 +2831,7 @@ async function upsertDecision(rootDir, input) {
2670
2831
  if (input.files && files.length < input.files.length) warnings.push("some anchor paths were outside the repo or duplicated and were dropped");
2671
2832
  for (const file of files) {
2672
2833
  try {
2673
- await fs9.access(path17.join(rootDir, file));
2834
+ await fs9.access(path18.join(rootDir, file));
2674
2835
  } catch {
2675
2836
  warnings.push(`anchor file does not exist on disk: ${file}`);
2676
2837
  }
@@ -2795,9 +2956,9 @@ var init_decisions = __esm({
2795
2956
  });
2796
2957
 
2797
2958
  // src/decisions/drift.ts
2798
- import path18 from "path";
2959
+ import path19 from "path";
2799
2960
  async function computeDecisionDrift(rootDir, decisions) {
2800
- const resolvedRoot = path18.resolve(rootDir);
2961
+ const resolvedRoot = path19.resolve(rootDir);
2801
2962
  const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
2802
2963
  const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
2803
2964
  const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
@@ -2993,7 +3154,9 @@ var init_findings = __esm({
2993
3154
  kind: z4.literal("doc-behind-manifests"),
2994
3155
  docLastCommit: commitSchema,
2995
3156
  manifestCommits: z4.array(commitSchema.extend({ files: z4.array(z4.string()) })),
2996
- totalCommits: count
3157
+ totalCommits: count,
3158
+ matches: z4.array(z4.object({ dependency: z4.string(), manifest: z4.string(), before: z4.string().nullable(), after: z4.string().nullable(), line: z4.number().int().positive(), excerpt: z4.string() })).optional(),
3159
+ matchingIncomplete: z4.boolean().optional()
2997
3160
  }),
2998
3161
  z4.object({
2999
3162
  kind: z4.literal("decision-anchor"),
@@ -3009,7 +3172,7 @@ var init_findings = __esm({
3009
3172
  type: z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
3010
3173
  confidence: z4.enum(["certain", "likely"])
3011
3174
  });
3012
- advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z4.literal("recheck").optional() });
3175
+ advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z4.enum(["recheck", "informational"]).optional() });
3013
3176
  checkResultSchema = z4.object({
3014
3177
  issues: z4.array(issueSchema),
3015
3178
  advisories: z4.array(advisorySchema),
@@ -3072,14 +3235,18 @@ async function readRecord(root, id) {
3072
3235
  if (raw === null) return null;
3073
3236
  const record = recordSchema.parse(raw);
3074
3237
  const { digest: stored, ...payload } = record;
3075
- if (record.id !== id || digest(payload) !== stored || record.events.some((event) => findingId(event.finding) !== id || event.evidence.fingerprint !== digest([event.evidence.scope, event.evidence.tree, event.evidence.lastScopeCommit, event.evidence.paths]))) {
3238
+ if (record.id !== id || digest(payload) !== stored || record.events.some((event) => findingId(event.finding) !== id || (event.outcome === "dismissed" ? !event.reasonCode || event.finding.type !== "deps-changed" : !event.reviewer || !!event.reasonCode) || event.evidence.fingerprint !== evidenceFingerprint(event.evidence, event.reasonCode))) {
3076
3239
  throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
3077
3240
  }
3078
3241
  return record;
3079
3242
  }
3080
- function scopes(finding) {
3243
+ function evidenceFingerprint(evidence, reason2) {
3244
+ return digest([evidence.scope, evidence.tree, ...reason2 === "no-dependency-claims" ? [] : [evidence.lastScopeCommit], evidence.paths]);
3245
+ }
3246
+ function scopes(finding, reason2) {
3081
3247
  const files = [finding.anchor.doc], e = finding.evidence;
3082
3248
  if (e.kind === "decision-anchor") throw new Error("Use review_decision for decision acceptance, reaffirmation or retirement.");
3249
+ if (reason2 === "no-dependency-claims" && e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`];
3083
3250
  if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
3084
3251
  if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
3085
3252
  else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
@@ -3090,9 +3257,9 @@ function scopes(finding) {
3090
3257
  return `:(literal)${file}`;
3091
3258
  }).sort();
3092
3259
  }
3093
- async function reviewEvidence(root, finding) {
3260
+ async function reviewEvidence(root, finding, reason2) {
3094
3261
  if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc) throw new Error("Invalid advisory document path.");
3095
- const scope = scopes(finding);
3262
+ const scope = scopes(finding, reason2);
3096
3263
  const head = (await git2(root, "rev-parse", "HEAD")).trim();
3097
3264
  const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
3098
3265
  if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
@@ -3118,7 +3285,7 @@ async function reviewEvidence(root, finding) {
3118
3285
  if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
3119
3286
  }
3120
3287
  }
3121
- const fingerprint = digest([scope, tree, lastScopeCommit, paths]);
3288
+ const fingerprint = evidenceFingerprint({ head, scope, tree, lastScopeCommit, paths, fingerprint: "" }, reason2);
3122
3289
  if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
3123
3290
  return {
3124
3291
  ...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
@@ -3165,7 +3332,7 @@ async function assessAdvisory(root, finding, baselineHead) {
3165
3332
  const record = await readRecord(root, id);
3166
3333
  if (!record) return null;
3167
3334
  const event = record.events.at(-1);
3168
- const current = await reviewEvidence(root, event.finding);
3335
+ const current = await reviewEvidence(root, event.finding, event.reasonCode);
3169
3336
  await git2(root, "merge-base", "--is-ancestor", event.evidence.head, current.head);
3170
3337
  const base = {
3171
3338
  id,
@@ -3175,7 +3342,7 @@ async function assessAdvisory(root, finding, baselineHead) {
3175
3342
  outcome: event.outcome,
3176
3343
  reviewedHead: event.evidence.head
3177
3344
  };
3178
- if (current.dirty || current.fingerprint !== event.evidence.fingerprint || digest(scopes(finding)) !== digest(event.evidence.scope)) return { ...base, status: "reopened", reason: "Relevant document or repository evidence changed after the assessment; prepare a new review." };
3345
+ if (current.dirty || current.fingerprint !== event.evidence.fingerprint || digest(scopes(finding, event.reasonCode)) !== digest(event.evidence.scope)) return { ...base, status: "reopened", reason: "Relevant document or repository evidence changed after the assessment; prepare a new review." };
3179
3346
  return { ...base, status: event.outcome === "deferred" ? "deferred" : "current", reason: event.outcome === "deferred" ? "The review was deferred and remains outstanding." : "An explicit assessment covers the current committed scope. This does not approve an engineering decision or certify correctness." };
3180
3347
  } catch (error) {
3181
3348
  return { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
@@ -3203,9 +3370,13 @@ async function reviewAdvisory(rootDir, input) {
3203
3370
  decisionId: finding.evidence.decisionId,
3204
3371
  hint: "Use review_decision to prepare evidence and record authorized acceptance, reaffirmation or retirement, then verify this original repair baseline. An advisory assessment cannot approve a decision."
3205
3372
  };
3373
+ if (request.action === "dismiss") {
3374
+ if (finding.type !== "deps-changed" || finding.evidence.kind !== "doc-behind-manifests") throw new Error("One-step dismissal is only available for dependency advisories.");
3375
+ if (!request.reasonCode || !request.note) throw new Error("Dismissal requires reasonCode and note; it does not require a reviewer or prepared token.");
3376
+ } else if (request.reasonCode) throw new Error("reasonCode is only used with dismiss.");
3206
3377
  const prepare = async () => {
3207
3378
  const record = await readRecord(root, request.findingId);
3208
- const evidence = await reviewEvidence(root, finding);
3379
+ const evidence = await reviewEvidence(root, finding, request.reasonCode);
3209
3380
  await git2(root, "merge-base", "--is-ancestor", baseline.original.headHash, evidence.head);
3210
3381
  const token = digest({ baseline: baseline.original, finding, evidence, record });
3211
3382
  return { record, evidence, token };
@@ -3234,10 +3405,10 @@ async function reviewAdvisory(rootDir, input) {
3234
3405
  hint: "Inspect the original finding, scoped document and relevant code changes. Record addressed, inapplicable or deferred only when the user or cited project review authorizes that assessment. Supply the actual reviewer, reason in note and reviewToken. Relevant edits must be committed first. Deferral remains outstanding. Review identities are recorded assertions, not authenticated approvals."
3235
3406
  };
3236
3407
  }
3237
- if (!request.reviewer || !request.note || !request.reviewToken) throw new Error("Prepare first; recording an assessment requires reviewToken, reviewer and note.");
3408
+ if (request.action !== "dismiss" && (!request.reviewer || !request.note || !request.reviewToken)) throw new Error("Prepare first; recording an assessment requires reviewToken, reviewer and note.");
3238
3409
  return withLock(root, ".mason/reports/advisory-review-locks/" + request.findingId, async () => {
3239
3410
  const state = await prepare();
3240
- if (state.token !== request.reviewToken) throw new Error("Review conflict: evidence or review history changed since preparation. Prepare and inspect a new review.");
3411
+ if (request.action !== "dismiss" && state.token !== request.reviewToken) throw new Error("Review conflict: evidence or review history changed since preparation. Prepare and inspect a new review.");
3241
3412
  if (state.evidence.dirty) throw new Error("Commit relevant document and repository edits before recording the assessment. Unrelated local work may remain.");
3242
3413
  if ((state.record?.events.length ?? 0) >= 200) throw new Error("Review history limit reached; existing events were retained.");
3243
3414
  const { dirty, documentDigest, ...evidence } = state.evidence;
@@ -3245,7 +3416,8 @@ async function reviewAdvisory(rootDir, input) {
3245
3416
  at: (/* @__PURE__ */ new Date()).toISOString(),
3246
3417
  reviewer: request.reviewer,
3247
3418
  note: request.note,
3248
- outcome: request.action,
3419
+ outcome: request.action === "dismiss" ? "dismissed" : request.action,
3420
+ reasonCode: request.reasonCode,
3249
3421
  finding,
3250
3422
  evidence
3251
3423
  });
@@ -3256,11 +3428,11 @@ async function reviewAdvisory(rootDir, input) {
3256
3428
  status: "recorded",
3257
3429
  recordPath: recordPath(request.findingId),
3258
3430
  event,
3259
- hint: "Review and commit this assessment record through the normal project workflow, then verify the original repair baseline. Relevant changes reopen the assessment; deferral remains outstanding."
3431
+ hint: request.action === "dismiss" ? "Dismissal saved and effective locally. Commit the record to share it. No-dependency-claims dismissals reopen on document content changes; unrelated-manifest-change dismissals reopen on scoped document or manifest changes. This does not approve a decision." : "Review and commit this assessment record through the normal project workflow, then verify the original repair baseline. Relevant changes reopen the assessment; deferral remains outstanding."
3260
3432
  };
3261
3433
  });
3262
3434
  }
3263
- var git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
3435
+ var git2, hashSchema, commitSchema2, evidenceSchema2, dismissalReasonSchema, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
3264
3436
  var init_advisory_review = __esm({
3265
3437
  "src/audit/advisory-review.ts"() {
3266
3438
  "use strict";
@@ -3286,11 +3458,13 @@ var init_advisory_review = __esm({
3286
3458
  lastScopeCommit: commitSchema2,
3287
3459
  paths: z6.array(z6.object({ path: z6.string(), exists: z6.boolean() })).max(1e3)
3288
3460
  });
3461
+ dismissalReasonSchema = z6.enum(["no-dependency-claims", "unrelated-manifest-change"]);
3289
3462
  eventSchema2 = z6.object({
3290
3463
  at: z6.string().datetime(),
3291
- reviewer: z6.string().trim().min(1).max(200),
3464
+ reviewer: z6.string().trim().min(1).max(200).optional(),
3292
3465
  note: z6.string().trim().min(1).max(2500),
3293
- outcome: z6.enum(["addressed", "inapplicable", "deferred"]),
3466
+ outcome: z6.enum(["addressed", "inapplicable", "deferred", "dismissed"]),
3467
+ reasonCode: dismissalReasonSchema.optional(),
3294
3468
  finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
3295
3469
  evidence: evidenceSchema2
3296
3470
  });
@@ -3302,13 +3476,14 @@ var init_advisory_review = __esm({
3302
3476
  recordPath: z6.string().optional(),
3303
3477
  reviewer: z6.string().optional(),
3304
3478
  note: z6.string().optional(),
3305
- outcome: z6.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired"]).optional(),
3479
+ outcome: z6.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired", "dismissed"]).optional(),
3306
3480
  reviewedHead: z6.string().optional()
3307
3481
  });
3308
3482
  advisoryReviewRequest = z6.object({
3309
3483
  baselinePath: z6.string().min(1),
3310
3484
  findingId: hashSchema,
3311
- action: z6.enum(["prepare", "addressed", "inapplicable", "deferred"]).default("prepare"),
3485
+ action: z6.enum(["prepare", "addressed", "inapplicable", "deferred", "dismiss"]).default("prepare"),
3486
+ reasonCode: dismissalReasonSchema.optional(),
3312
3487
  reviewer: eventSchema2.shape.reviewer.optional(),
3313
3488
  note: eventSchema2.shape.note.optional(),
3314
3489
  reviewToken: hashSchema.optional()
@@ -3319,12 +3494,12 @@ var init_advisory_review = __esm({
3319
3494
 
3320
3495
  // src/audit/audit.ts
3321
3496
  import fs11 from "fs/promises";
3322
- import path19 from "path";
3497
+ import path20 from "path";
3323
3498
  async function computeAudit(rootDir, options = {}) {
3324
3499
  return profilePhase("audit.current", () => auditCurrent(rootDir, options));
3325
3500
  }
3326
3501
  async function auditCurrent(rootDir, options) {
3327
- const resolvedRoot = path19.resolve(rootDir);
3502
+ const resolvedRoot = path20.resolve(rootDir);
3328
3503
  const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
3329
3504
  if (docs.length === 0) return null;
3330
3505
  const report = {
@@ -3358,7 +3533,7 @@ async function auditCurrent(rootDir, options) {
3358
3533
  }
3359
3534
  let decisionsPresent = false;
3360
3535
  try {
3361
- await fs11.access(path19.join(resolvedRoot, ".mason", "decisions"));
3536
+ await fs11.access(path20.join(resolvedRoot, ".mason", "decisions"));
3362
3537
  decisionsPresent = true;
3363
3538
  } catch {
3364
3539
  }
@@ -3370,13 +3545,21 @@ async function auditCurrent(rootDir, options) {
3370
3545
  changesSinceDoc,
3371
3546
  decisionsPresent
3372
3547
  };
3548
+ const policy = (await loadProjectConfig(resolvedRoot)).audit;
3373
3549
  const selected = options.checks ?? ALL_CHECKS;
3374
3550
  for (const name of ALL_CHECKS) {
3375
3551
  if (!selected.includes(name)) continue;
3376
- const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, ctx) : CHECKS[name](ctx));
3552
+ const exclusions = policy?.exclude?.[name] ?? [];
3553
+ const included = await includedCheckPaths(resolvedRoot, docs.map((doc) => doc.path), exclusions);
3554
+ const excluded = docs.filter((doc) => !included.has(doc.path)).map((doc) => ({ check: name, doc: doc.path }));
3555
+ if (excluded.length) (report.excludedChecks ??= []).push(...excluded);
3556
+ const scoped = { ...ctx, docs: docs.filter((doc) => included.has(doc.path)) };
3557
+ const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, scoped) : CHECKS[name](scoped));
3377
3558
  report.checksRun.push(name);
3378
3559
  report.issues.push(...issues);
3379
- report.advisories.push(...advisories);
3560
+ const advisoryPaths = await includedCheckPaths(resolvedRoot, [...new Set(advisories.map((f) => f.anchor.doc))], exclusions);
3561
+ report.advisories.push(...advisories.filter((f) => advisoryPaths.has(f.anchor.doc)));
3562
+ for (const f of advisories) if (!advisoryPaths.has(f.anchor.doc)) (report.excludedChecks ??= []).push({ check: name, doc: f.anchor.doc });
3380
3563
  report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
3381
3564
  report.skippedChecks.push(...skipped);
3382
3565
  }
@@ -3388,6 +3571,8 @@ async function auditCurrent(rootDir, options) {
3388
3571
  var init_audit = __esm({
3389
3572
  "src/audit/audit.ts"() {
3390
3573
  "use strict";
3574
+ init_files();
3575
+ init_policy();
3391
3576
  init_profile();
3392
3577
  init_drift();
3393
3578
  init_snapshot();
@@ -3413,7 +3598,7 @@ __export(repair_exports, {
3413
3598
  withRepairSession: () => withRepairSession
3414
3599
  });
3415
3600
  import fs12 from "fs/promises";
3416
- import path20 from "path";
3601
+ import path21 from "path";
3417
3602
  import { randomUUID as randomUUID2 } from "crypto";
3418
3603
  import { z as z7 } from "zod";
3419
3604
  function recheckable(finding) {
@@ -3434,7 +3619,7 @@ async function docState(root) {
3434
3619
  docs.push([doc, null]);
3435
3620
  }
3436
3621
  }
3437
- return digest(docs);
3622
+ return digest([docs, (await loadProjectConfig(root)).audit]);
3438
3623
  }
3439
3624
  async function stableAudit(root, checks, options = {}) {
3440
3625
  return profilePhase("repair.audit", () => readStableAudit(root, checks, options));
@@ -3503,8 +3688,8 @@ async function prepareOriginal(rootDir, checks, options, session) {
3503
3688
  }
3504
3689
  async function loadRepairBaseline(rootDir, baselinePath) {
3505
3690
  const root = await fs12.realpath(rootDir);
3506
- const declaredRoot = path20.resolve(rootDir);
3507
- const relative = path20.isAbsolute(baselinePath) ? path20.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
3691
+ const declaredRoot = path21.resolve(rootDir);
3692
+ const relative = path21.isAbsolute(baselinePath) ? path21.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
3508
3693
  const stored = baselineSchema.parse(await readStoreJson(root, relative));
3509
3694
  const { digest: savedDigest, ...payload } = stored;
3510
3695
  if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
@@ -3553,6 +3738,9 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
3553
3738
  if (now && ("confidence" in finding || "confidence" in now)) {
3554
3739
  return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
3555
3740
  }
3741
+ if (current.excludedChecks?.some((excluded) => excluded.check === finding.type && excluded.doc === finding.anchor.doc)) {
3742
+ return { ...base, status: "resolved", reason: "Excluded by project audit policy; this is not a correctness assessment. Original evidence is retained." };
3743
+ }
3556
3744
  const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
3557
3745
  if (!current.checksRun?.includes(finding.type) || skipped.length) {
3558
3746
  return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
@@ -3563,11 +3751,14 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
3563
3751
  if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
3564
3752
  return { ...base, review, status: "review-required", reason: review.reason };
3565
3753
  }
3754
+ if (!("confidence" in finding) && finding.evidence.kind === "doc-behind-manifests" && !finding.evidence.matches?.length && (!now || !("confidence" in now) && now.resolution === "informational")) {
3755
+ return { ...base, status: "resolved", reason: "Manifest recency alone establishes no affected passage. Retained as information; no assessment is required." };
3756
+ }
3566
3757
  if (!("confidence" in finding) && (!recheckable(finding) || now)) {
3567
3758
  return {
3568
3759
  ...base,
3569
3760
  status: "review-required",
3570
- reason: "An audit cannot establish that this advisory was reviewed. Retain its original evidence and report a separate assessment; editing or committing the doc is not approval."
3761
+ reason: (now ? "The current check still reports this advisory. " : finding.type === "deps-changed" ? "The current check no longer reports this condition; the historical advisory remains awaiting assessment. " : "The current check no longer reports the original finding; retained evidence still requires assessment. ") + "An audit cannot establish that this advisory was reviewed. Retain its original evidence and report a separate assessment; editing or committing the doc is not approval."
3571
3762
  };
3572
3763
  }
3573
3764
  return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
@@ -3591,7 +3782,7 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
3591
3782
  const reviewedIds = new Set(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
3592
3783
  const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
3593
3784
  for (const f of findings) counts[f.status]++;
3594
- const incomplete = diagnostics.length > 0 || counts.unverified > 0 || counts["review-required"] > 0 || (current?.skippedChecks.length ?? 0) > 0 || newFindings.some((f) => !("confidence" in f) && !reviewedIds.has(findingId(f)));
3785
+ const incomplete = diagnostics.length > 0 || counts.unverified > 0 || counts["review-required"] > 0 || (current?.skippedChecks.length ?? 0) > 0 || newFindings.some((f) => !("confidence" in f) && f.resolution !== "informational" && !reviewedIds.has(findingId(f)));
3595
3786
  const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
3596
3787
  return {
3597
3788
  version: 1,
@@ -3614,7 +3805,7 @@ function repairExitCode(report) {
3614
3805
  function formatRepairSummary(report) {
3615
3806
  return [
3616
3807
  "Repair verification: " + report.status + ". Baseline: " + report.baselinePath,
3617
- ...report.findings.map((f) => " [" + f.status + "] " + f.original.type + " " + f.original.anchor.doc + ": " + f.original.message + "\n " + f.reason + "\n findingId: " + f.id),
3808
+ ...report.findings.map((f) => " [" + f.status + "] " + f.original.type + " " + f.original.anchor.doc + ": " + (f.current?.message ?? "Historical evidence at " + report.baselineHead.slice(0, 7) + ": " + f.original.message) + "\n " + f.reason + "\n findingId: " + f.id),
3618
3809
  ...report.newFindings.map((f) => " [new] " + f.type + " " + f.anchor.doc + ": " + f.message),
3619
3810
  ...report.diagnostics.map((d) => " [unverified] " + d),
3620
3811
  ...(report.currentAudit?.skippedChecks ?? []).map((s) => " [skipped] " + s.check + ": " + s.reason),
@@ -3647,13 +3838,14 @@ var init_repair = __esm({
3647
3838
  docs: z7.array(z7.object({
3648
3839
  path: z7.string().refine((value) => normalizeRepoPath(value) === value),
3649
3840
  scope: z7.string().optional(),
3650
- kind: z7.enum(["instructions", "readme"]).optional(),
3841
+ kind: z7.enum(["instructions", "readme", "guide"]).optional(),
3651
3842
  lastCommit: commitSchema.nullable(),
3652
3843
  dirty: z7.boolean(),
3653
3844
  lineCount: count2
3654
3845
  })).nonempty(),
3655
3846
  decisionsChecked: z7.boolean(),
3656
3847
  clean: z7.boolean(),
3848
+ excludedChecks: z7.array(z7.object({ check: checkSchema, doc: z7.string() })).optional(),
3657
3849
  issues: z7.array(issueSchema),
3658
3850
  advisories: z7.array(advisorySchema),
3659
3851
  suppressedAdvisories: z7.array(advisorySchema).optional(),
@@ -3679,9 +3871,9 @@ var init_repair = __esm({
3679
3871
 
3680
3872
  // src/automation/evidence.ts
3681
3873
  import fs13 from "fs/promises";
3682
- import path21 from "path";
3874
+ import path22 from "path";
3683
3875
  import { createHash as createHash3 } from "crypto";
3684
- import fg5 from "fast-glob";
3876
+ import fg7 from "fast-glob";
3685
3877
  import { z as z8 } from "zod";
3686
3878
  async function git3(root, ...args) {
3687
3879
  return profilePhase("automation.git", async () => (await execGit(args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout);
@@ -3699,6 +3891,11 @@ async function readInputs(root) {
3699
3891
  }
3700
3892
  async function collectInputs(root) {
3701
3893
  const docPaths = await discoverDocPaths(root);
3894
+ const auditPolicy = (await loadProjectConfig(root)).audit;
3895
+ const allowed = /* @__PURE__ */ new Map();
3896
+ for (const name of ["deleted-reference", "new-module", "stale-count", "dead-command"]) {
3897
+ allowed.set(name, await includedCheckPaths(root, docPaths, auditPolicy?.exclude?.[name] ?? []));
3898
+ }
3702
3899
  const [headText, docStatus, shallowPath, replacements] = await Promise.all([
3703
3900
  git3(root, "rev-parse", "HEAD"),
3704
3901
  docPaths.length ? git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...docPaths.map((p) => `:(literal)${p}`)) : "",
@@ -3708,7 +3905,7 @@ async function collectInputs(root) {
3708
3905
  const head = headText.trim();
3709
3906
  let shallow = null;
3710
3907
  try {
3711
- shallow = await fs13.readFile(path21.resolve(root, shallowPath.trim()), "utf8");
3908
+ shallow = await fs13.readFile(path22.resolve(root, shallowPath.trim()), "utf8");
3712
3909
  } catch (error) {
3713
3910
  if (error.code !== "ENOENT") throw error;
3714
3911
  }
@@ -3727,18 +3924,18 @@ async function collectInputs(root) {
3727
3924
  docContents.push([file, text2]);
3728
3925
  const parsed = { path: file, content: text2 ?? "", claims: extractClaims(text2 ?? "") };
3729
3926
  parsedDocs.push(parsed);
3730
- for (const claim of parsed.claims.paths) {
3927
+ for (const claim of allowed.get("deleted-reference").has(file) ? parsed.claims.paths : []) {
3731
3928
  const scope = pathClaimScope(file, claim);
3732
3929
  if (scope?.candidates.some(gitMetadataPath)) continue;
3733
3930
  for (const candidate of scope?.candidates ?? []) {
3734
3931
  if (optionalMasonPath(candidate)) continue;
3735
- const [filePresent, parentPresent] = await Promise.all([exists(candidate), exists(path21.posix.dirname(candidate))]);
3932
+ const [filePresent, parentPresent] = await Promise.all([exists(candidate), exists(path22.posix.dirname(candidate))]);
3736
3933
  claims.push([candidate, filePresent, parentPresent]);
3737
3934
  }
3738
3935
  }
3739
3936
  }
3740
- const combinedDocs = moduleDocumentation(parsedDocs);
3741
- const countClaims = parsedDocs.flatMap((doc) => doc.claims.counts.map((claim) => ({ doc: doc.path, claim })));
3937
+ const combinedDocs = moduleDocumentation(parsedDocs.filter((doc) => allowed.get("new-module").has(doc.path)));
3938
+ const countClaims = parsedDocs.filter((doc) => allowed.get("stale-count").has(doc.path)).flatMap((doc) => doc.claims.counts.map((claim) => ({ doc: doc.path, claim })));
3742
3939
  const decisionDirectory = await storePath(root, ".mason/decisions");
3743
3940
  const decisionPresence = await fs13.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
3744
3941
  if (error.code === "ENOENT") return "absent";
@@ -3747,8 +3944,8 @@ async function collectInputs(root) {
3747
3944
  const [modules, counts, commands2, decisionFiles] = await Promise.all([
3748
3945
  combinedDocs ? moduleCandidates(root, combinedDocs) : [],
3749
3946
  Promise.all(countClaims.map(({ doc, claim }) => resolveDocCountSource(root, doc, claim))),
3750
- commandInputs(root, parsedDocs),
3751
- fg5(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
3947
+ commandInputs(root, parsedDocs.filter((doc) => allowed.get("dead-command").has(doc.path))),
3948
+ fg7(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
3752
3949
  ]);
3753
3950
  const decisions = await Promise.all(decisionFiles.sort().map(async (file) => {
3754
3951
  await storePath(root, file);
@@ -3758,7 +3955,7 @@ async function collectInputs(root) {
3758
3955
  git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
3759
3956
  git3(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
3760
3957
  ]) : ["", ""];
3761
- const common = [5, engineVersion, head, shallow, replacements, docContents];
3958
+ const common = [7, engineVersion, head, shallow, replacements, docContents, auditPolicy];
3762
3959
  const keys = {
3763
3960
  "deleted-reference": hash([common, claims, docStatus]),
3764
3961
  "new-module": hash([common, modules]),
@@ -3805,6 +4002,8 @@ var engineVersion, hash, content, cacheSchema;
3805
4002
  var init_evidence = __esm({
3806
4003
  "src/automation/evidence.ts"() {
3807
4004
  "use strict";
4005
+ init_policy();
4006
+ init_files();
3808
4007
  init_git_read();
3809
4008
  init_profile();
3810
4009
  init_docs();
@@ -3818,7 +4017,7 @@ var init_evidence = __esm({
3818
4017
  init_dead_command();
3819
4018
  init_storage();
3820
4019
  init_advisory_review();
3821
- engineVersion = true ? "0.18.0" : "development";
4020
+ engineVersion = true ? "0.19.1" : "development";
3822
4021
  hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
3823
4022
  content = readAuditInput;
3824
4023
  cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
@@ -3828,18 +4027,49 @@ var init_evidence = __esm({
3828
4027
  // src/setup/model.ts
3829
4028
  var model_exports = {};
3830
4029
  __export(model_exports, {
4030
+ INACTIVE_HOSTS_PATH: () => INACTIVE_HOSTS_PATH,
3831
4031
  SETUP_PATH: () => SETUP_PATH,
4032
+ effectiveSetup: () => effectiveSetup,
4033
+ inactiveHostsSchema: () => inactiveHostsSchema,
3832
4034
  loadSetup: () => loadSetup,
3833
4035
  loadSetupReceipt: () => loadSetupReceipt,
3834
4036
  setupHostSchema: () => setupHostSchema,
3835
4037
  setupSchema: () => setupSchema
3836
4038
  });
3837
4039
  import { z as z9 } from "zod";
4040
+ import fs14 from "fs/promises";
4041
+ import path23 from "path";
3838
4042
  async function loadSetup(root) {
3839
4043
  const raw = await readStoreJson(root, SETUP_PATH);
3840
4044
  if (raw === null) return null;
3841
4045
  return setupSchema.parse(raw);
3842
4046
  }
4047
+ async function effectiveSetup(root) {
4048
+ const local = await loadSetup(root);
4049
+ if (local) return { root, setup: local };
4050
+ const common = await fs14.realpath(path23.resolve(root, (await git3(root, "rev-parse", "--git-common-dir")).trim()));
4051
+ const own = await fs14.realpath((await git3(root, "rev-parse", "--absolute-git-dir")).trim());
4052
+ if (common === own) return { root, setup: null };
4053
+ const primary = (await git3(root, "worktree", "list", "--porcelain", "-z")).split("\0\0")[0].split("\0");
4054
+ const candidate = primary.find((field) => field.startsWith("worktree "))?.slice(9);
4055
+ if (!candidate || primary.includes("bare")) return { root, setup: null };
4056
+ let inheritedRoot;
4057
+ try {
4058
+ inheritedRoot = await fs14.realpath((await git3(candidate, "rev-parse", "--show-toplevel")).trim());
4059
+ } catch (error) {
4060
+ if (/must be run in a work tree/.test(String(error.stderr))) return { root, setup: null };
4061
+ throw error;
4062
+ }
4063
+ const inheritedCommon = await fs14.realpath(path23.resolve(
4064
+ inheritedRoot,
4065
+ (await git3(inheritedRoot, "rev-parse", "--git-common-dir")).trim()
4066
+ ));
4067
+ if (inheritedCommon !== common) throw new Error("Linked worktree setup does not belong to the same Git repository.");
4068
+ const setup = await loadSetup(inheritedRoot);
4069
+ const inactive = await readStoreJson(root, INACTIVE_HOSTS_PATH);
4070
+ if (inactive && setup) for (const host of inactiveHostsSchema.parse(inactive).hosts) delete setup.hosts[host];
4071
+ return { root: inheritedRoot, setup };
4072
+ }
3843
4073
  async function loadSetupReceipt(root, directory, host) {
3844
4074
  const raw = await readStoreJson(root, directory + "/setup-" + host + ".json");
3845
4075
  if (raw === null) return null;
@@ -3847,10 +4077,11 @@ async function loadSetupReceipt(root, directory, host) {
3847
4077
  if (receipt.root !== root || receipt.host !== host) throw new Error("Setup receipt belongs to another installation.");
3848
4078
  return receipt;
3849
4079
  }
3850
- var setupHostSchema, setupSchema, SETUP_PATH, receiptSchema;
4080
+ var setupHostSchema, setupSchema, SETUP_PATH, INACTIVE_HOSTS_PATH, inactiveHostsSchema, receiptSchema;
3851
4081
  var init_model = __esm({
3852
4082
  "src/setup/model.ts"() {
3853
4083
  "use strict";
4084
+ init_evidence();
3854
4085
  init_storage();
3855
4086
  setupHostSchema = z9.object({
3856
4087
  configuredVersion: z9.string(),
@@ -3864,6 +4095,8 @@ var init_model = __esm({
3864
4095
  claude: setupHostSchema.optional()
3865
4096
  }) });
3866
4097
  SETUP_PATH = ".mason/local/setup.json";
4098
+ INACTIVE_HOSTS_PATH = ".mason/local/inactive-hosts.json";
4099
+ inactiveHostsSchema = z9.object({ version: z9.literal(1), hosts: z9.array(z9.enum(["claude", "codex"])) });
3867
4100
  receiptSchema = z9.object({
3868
4101
  version: z9.literal(1),
3869
4102
  host: z9.enum(["codex", "claude"]),
@@ -3880,7 +4113,7 @@ var init_model = __esm({
3880
4113
  // src/setup/launcher.ts
3881
4114
  import { execFile as execFile2 } from "child_process";
3882
4115
  import { promisify as promisify2 } from "util";
3883
- import fs14 from "fs/promises";
4116
+ import fs15 from "fs/promises";
3884
4117
  function globalCommand(args, platform = process.platform) {
3885
4118
  return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args] } : { command: "mason", args };
3886
4119
  }
@@ -3910,7 +4143,7 @@ var init_launcher = __esm({
3910
4143
  "use strict";
3911
4144
  init_evidence();
3912
4145
  init_model();
3913
- executingVersion = () => true ? "0.18.0" : "development";
4146
+ executingVersion = () => true ? "0.19.1" : "development";
3914
4147
  mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
3915
4148
  hookCommand = (host) => `mason --setup-host ${host} auto`;
3916
4149
  }
@@ -4035,8 +4268,8 @@ __export(files_exports, {
4035
4268
  managedBlock: () => managedBlock,
4036
4269
  readText: () => readText
4037
4270
  });
4038
- import fs17 from "fs/promises";
4039
- import path25 from "path";
4271
+ import fs18 from "fs/promises";
4272
+ import path27 from "path";
4040
4273
  import { randomUUID as randomUUID3 } from "crypto";
4041
4274
  async function readText(root, file) {
4042
4275
  try {
@@ -4065,14 +4298,14 @@ async function applyEdit(root, edit) {
4065
4298
  if (edit.after === null) {
4066
4299
  const file2 = await storePath(root, edit.path);
4067
4300
  if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
4068
- await fs17.unlink(file2);
4301
+ await fs18.unlink(file2);
4069
4302
  return true;
4070
4303
  }
4071
4304
  const file = await storePath(root, edit.path, true);
4072
- const temporary = path25.join(path25.dirname(file), ".mason-setup-" + randomUUID3() + ".tmp");
4305
+ const temporary = path27.join(path27.dirname(file), ".mason-setup-" + randomUUID3() + ".tmp");
4073
4306
  try {
4074
- const mode = await fs17.stat(file).then((s) => s.mode & 511, () => 384);
4075
- const handle = await fs17.open(temporary, "wx", mode);
4307
+ const mode = await fs18.stat(file).then((s) => s.mode & 511, () => 384);
4308
+ const handle = await fs18.open(temporary, "wx", mode);
4076
4309
  try {
4077
4310
  await handle.writeFile(edit.after, "utf8");
4078
4311
  await handle.sync();
@@ -4080,10 +4313,10 @@ async function applyEdit(root, edit) {
4080
4313
  await handle.close();
4081
4314
  }
4082
4315
  if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
4083
- await fs17.rename(temporary, file);
4316
+ await fs18.rename(temporary, file);
4084
4317
  return true;
4085
4318
  } finally {
4086
- await fs17.rm(temporary, { force: true });
4319
+ await fs18.rm(temporary, { force: true });
4087
4320
  }
4088
4321
  }
4089
4322
  var init_files2 = __esm({
@@ -4105,7 +4338,7 @@ __export(execution_exports, {
4105
4338
  recordExecution: () => recordExecution
4106
4339
  });
4107
4340
  import os2 from "os";
4108
- import fs18 from "fs/promises";
4341
+ import fs19 from "fs/promises";
4109
4342
  import { randomUUID as randomUUID4 } from "crypto";
4110
4343
  import { z as z12 } from "zod";
4111
4344
  function parseExecution(raw) {
@@ -4233,7 +4466,7 @@ async function recordExecution(root, directory, event, run) {
4233
4466
  throw Object.assign(new Error(failure.message, { cause: error }), { failure });
4234
4467
  }
4235
4468
  } finally {
4236
- await fs18.rm(await storePath(root, owner), { force: true }).catch(() => {
4469
+ await fs19.rm(await storePath(root, owner), { force: true }).catch(() => {
4237
4470
  });
4238
4471
  }
4239
4472
  }
@@ -4317,13 +4550,17 @@ async function observeReadOnlyTool(dir, event) {
4317
4550
  }
4318
4551
  function summarize(report) {
4319
4552
  const open = report.findings.filter((f) => f.status !== "resolved");
4553
+ const onlyReview = report.status === "incomplete" && !report.diagnostics.length && !report.counts.unresolved && !report.counts.unverified && report.counts["review-required"] > 0;
4320
4554
  return [
4321
- `Mason: ${report.status}; ${report.counts.unresolved} unresolved, ${report.counts["review-required"]} need review, ${report.counts.unverified} unverified.`,
4322
- ...open.slice(0, 4).map((f) => `[${f.status}] ${cleanText(f.original.anchor.doc)}: ${cleanText(f.original.message)}`),
4555
+ onlyReview ? `Mason: ${report.counts["review-required"]} advisor${report.counts["review-required"] === 1 ? "y" : "ies"} awaiting review; no unresolved issues.` : `Mason: ${report.status}; ${report.counts.unresolved} unresolved, ${report.counts["review-required"]} need review, ${report.counts.unverified} unverified.`,
4556
+ ...open.slice(0, 4).map((f) => {
4557
+ const detail = f.current ? f.current.message : f.status === "review-required" ? `${f.original.type}: current check no longer reports ${f.original.type === "deps-changed" ? "this condition" : "the original finding"}; historical evidence awaits assessment.` : `Original evidence: ${f.original.message}. ${f.reason}`;
4558
+ return `[${f.status}] ${cleanText(f.original.anchor.doc)}: ${cleanText(detail)}`;
4559
+ }),
4323
4560
  ...open.length > 4 ? [`${open.length - 4} more findings in the report.`] : [],
4324
4561
  ...report.diagnostics.slice(0, 2).map(cleanText),
4325
4562
  `Evidence: ${report.reportPath}. Resume/check with mason_automation(action: "check") or mason-auto check.`,
4326
- "Keep original evidence. Address findings relevant to the authorized task; report unrelated findings and unresolved advisories without approving them."
4563
+ "Keep original evidence. Address findings relevant to the authorized task; report unrelated findings with a brief scope reason and suggested follow-up. Use review_advisory for authorized assessments; otherwise leave advisories open without approving them."
4327
4564
  ].join("\n");
4328
4565
  }
4329
4566
  async function loadState(ws) {
@@ -4547,7 +4784,7 @@ async function automate(dir, event) {
4547
4784
  if (event.event === "after_tool" && event.mutating) {
4548
4785
  session.mutationObserved = true;
4549
4786
  if (!event.toolId || !session.pending[event.toolId]) {
4550
- const gap = "A tool completed without an observed matching pre-tool capture; pre-edit coverage is unknown.";
4787
+ const gap = "A tool completed without an observed matching pre-tool capture in this checkout; pre-edit coverage is unknown. The tool may have changed checkouts; captures are kept separately for each worktree.";
4551
4788
  if (!session.coverageGaps.includes(gap)) session.coverageGaps.push(gap);
4552
4789
  }
4553
4790
  if (event.toolId) delete session.pending[event.toolId];
@@ -4656,8 +4893,8 @@ var init_runtime = __esm({
4656
4893
  });
4657
4894
 
4658
4895
  // src/automation/hook-root.ts
4659
- import fs19 from "fs/promises";
4660
- import path26 from "path";
4896
+ import fs20 from "fs/promises";
4897
+ import path28 from "path";
4661
4898
  var init_hook_root = __esm({
4662
4899
  "src/automation/hook-root.ts"() {
4663
4900
  "use strict";
@@ -5067,9 +5304,11 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
5067
5304
  - For an architectural overview, use \`get_snapshot\` if a map is available. If \`map.status\` is missing or invalid, use available decisions and source evidence; do not start building a map unless requested.
5068
5305
  - \`mason_init\` returns documentation audit and committed-diff review results, plus a short setup guide. Pass \`evidence\` with local CI manifest paths to include test and analysis results; the CLI equivalent is \`mason-review --evidence <manifest>\`. State skipped, unavailable, stale, or unknown checks explicitly. Related accepted decisions identify review context, not proven violations.
5069
5306
 
5070
- - When documentation repair is authorized, use \`mason_repair(action: "prepare")\` before edits, keep its baselinePath, and use \`mason_repair(action: "verify", baselinePath)\` after edits and any final doc commit. Report every original finding's outcome and any new findings. Suppressed advisories remain unresolved; editing a doc does not approve it.
5307
+ - When documentation repair is authorized, use \`mason_repair(action: "prepare")\` before edits, keep its baselinePath, and use \`mason_repair(action: "verify", baselinePath)\` after edits and any final doc commit. Report every original finding's outcome and any new findings. Specific suppressed advisories remain unresolved; editing a doc does not approve it. Informational dependency notices require no review.
5071
5308
  - When advisory assessment is authorized, use \`review_advisory(action: "prepare", baselinePath, findingId)\` to inspect original evidence and history. Record \`addressed\`, \`inapplicable\`, or \`deferred\` with the actual reviewer, reason in note, and returned reviewToken only after relevant edits are committed. Commit the assessment and verify the same baseline again. Relevant evidence changes reopen it; deferral stays outstanding. Decision findings use \`review_decision\`; advisory closure never approves a decision.
5072
5309
 
5310
+ - For a dependency advisory, \`review_advisory(action: "dismiss", baselinePath, findingId, reasonCode, note)\` records a one-step dismissal without reviewer or token. Use \`no-dependency-claims\` for a document with no dependency claims (reopens on document content changes), or \`unrelated-manifest-change\` for reviewed changes that do not affect its guidance (reopens on document or manifest changes). Keep the relevant scope committed; commit the dismissal to share it. This never approves a decision.
5311
+
5073
5312
  - When Mason automation is installed, use \`mason_automation(action: "status")\` to inspect configured hooks and observed events, and \`mason_automation(action: "check")\` to resume its retained repair evidence. CLI fallback: \`mason-auto status\` / \`mason-auto check\`. Preserve existing baselines across sessions. Automatic checks do not authorize unrelated repairs or approve advisories.
5074
5313
 
5075
5314
  Inspect source for what the retrieved context does not answer.
@@ -5500,10 +5739,10 @@ var init_check = __esm({
5500
5739
  });
5501
5740
 
5502
5741
  // src/review/evidence.ts
5503
- import path27 from "path";
5742
+ import path29 from "path";
5504
5743
  import { z as z22 } from "zod";
5505
5744
  function relativeArtifact(root, file) {
5506
- const relative = path27.isAbsolute(file) ? path27.relative(root, file) : file;
5745
+ const relative = path29.isAbsolute(file) ? path29.relative(root, file) : file;
5507
5746
  const normalized = normalizeRepoPath(relative);
5508
5747
  if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
5509
5748
  return normalized;
@@ -5600,7 +5839,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
5600
5839
  const report = await readStoreJson(root, file);
5601
5840
  if (report === null) throw new Error(`Report artifact is missing: ${file}`);
5602
5841
  const sourceRoot = check.sourceRoot ?? root;
5603
- if (!path27.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
5842
+ if (!path29.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
5604
5843
  const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : check.report.format === "mason-check-json" ? parseCheck(report, sourceRoot) : parseSarif(report, sourceRoot);
5605
5844
  result.outcome = parsed.outcome;
5606
5845
  result.counts = parsed.counts;
@@ -5721,8 +5960,8 @@ var init_evidence2 = __esm({
5721
5960
  });
5722
5961
 
5723
5962
  // src/review/review.ts
5724
- import fs20 from "fs/promises";
5725
- import path28 from "path";
5963
+ import fs21 from "fs/promises";
5964
+ import path30 from "path";
5726
5965
  import { execFile as execFile7 } from "child_process";
5727
5966
  import { promisify as promisify7 } from "util";
5728
5967
  async function resolveMergeBase(resolvedRoot, base, head) {
@@ -5751,7 +5990,7 @@ function anchorsTouched(record, changedFiles) {
5751
5990
  return matchingPaths(record.files, changedFiles);
5752
5991
  }
5753
5992
  async function computeReview(rootDir, base, options = {}) {
5754
- const resolvedRoot = path28.resolve(rootDir);
5993
+ const resolvedRoot = path30.resolve(rootDir);
5755
5994
  const head = await getCurrentGitHash(resolvedRoot);
5756
5995
  if (head === "unknown") return null;
5757
5996
  const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
@@ -5801,7 +6040,7 @@ async function computeReview(rootDir, base, options = {}) {
5801
6040
  analyzed,
5802
6041
  async (relPath) => {
5803
6042
  try {
5804
- await fs20.access(path28.join(resolvedRoot, relPath));
6043
+ await fs21.access(path30.join(resolvedRoot, relPath));
5805
6044
  return true;
5806
6045
  } catch {
5807
6046
  return false;
@@ -5936,7 +6175,7 @@ var init_onboarding = __esm({
5936
6175
  });
5937
6176
 
5938
6177
  // src/impact/references.ts
5939
- import path29 from "path";
6178
+ import path31 from "path";
5940
6179
  function templateEnd(content2, start) {
5941
6180
  let i = start + 1, interpolation = 0;
5942
6181
  while (i < content2.length) {
@@ -5958,10 +6197,10 @@ function templateEnd(content2, start) {
5958
6197
  continue;
5959
6198
  }
5960
6199
  if (c === '"' || c === "'") {
5961
- const quote = c;
6200
+ const quote2 = c;
5962
6201
  for (i++; i < content2.length; i++) {
5963
6202
  if (content2[i] === "\\") i++;
5964
- else if (content2[i] === quote) {
6203
+ else if (content2[i] === quote2) {
5965
6204
  i++;
5966
6205
  break;
5967
6206
  }
@@ -6062,10 +6301,10 @@ function jvmImports(content2) {
6062
6301
  }
6063
6302
  function resolveImport(file, specifier, available) {
6064
6303
  if (!specifier.startsWith("./") && !specifier.startsWith("../")) return null;
6065
- const relative = path29.posix.normalize(path29.posix.join(path29.posix.dirname(file), specifier));
6304
+ const relative = path31.posix.normalize(path31.posix.join(path31.posix.dirname(file), specifier));
6066
6305
  if (relative.startsWith("../")) return null;
6067
6306
  if (available.has(relative)) return relative;
6068
- const ext = path29.posix.extname(relative);
6307
+ const ext = path31.posix.extname(relative);
6069
6308
  const replacements = { ".js": [".ts", ".tsx"], ".mjs": [".mts"], ".cjs": [".cts"] };
6070
6309
  const candidates = ext ? (replacements[ext] ?? []).map((e) => relative.slice(0, -ext.length) + e) : jsExtensions.flatMap((e) => [relative + e, relative + "/index" + e]);
6071
6310
  const found = candidates.filter((f) => available.has(f));
@@ -6074,23 +6313,23 @@ function resolveImport(file, specifier, available) {
6074
6313
  function mentionsPath(content2, target) {
6075
6314
  return new RegExp(`(?<![\\w./\\\\-])(?:\\./)?${escape(target)}(?![\\w./\\\\-])`).test(content2);
6076
6315
  }
6077
- function createReferenceMatcher(targets, available, targetContents = /* @__PURE__ */ new Map()) {
6316
+ function createReferenceMatcher(targets2, available, targetContents = /* @__PURE__ */ new Map()) {
6078
6317
  const counts = /* @__PURE__ */ new Map();
6079
6318
  for (const file of available) {
6080
- const name = path29.posix.basename(file, path29.posix.extname(file));
6319
+ const name = path31.posix.basename(file, path31.posix.extname(file));
6081
6320
  counts.set(name, (counts.get(name) ?? 0) + 1);
6082
6321
  }
6083
6322
  const jvmTargets = new Map([...targetContents].filter(([file]) => /\.(?:kt|kts|java)$/.test(file)).map(([file, content2]) => [file, declaredSymbols(content2)]));
6084
6323
  return (source) => {
6085
- if (targets.includes(source.path)) return [];
6086
- const resolved = new Set(jsExtensions.includes(path29.posix.extname(source.path)) ? imports(source.content).map((s) => resolveImport(source.path, s, available)).filter(Boolean) : []);
6324
+ if (targets2.includes(source.path)) return [];
6325
+ const resolved = new Set(jsExtensions.includes(path31.posix.extname(source.path)) ? imports(source.content).map((s) => resolveImport(source.path, s, available)).filter(Boolean) : []);
6087
6326
  if (/\.(?:kt|kts|java)$/.test(source.path)) {
6088
6327
  const imported = jvmImports(source.content);
6089
6328
  for (const [target, symbols] of jvmTargets) if (symbols.some((symbol) => imported.some((name) => name === symbol || name.startsWith(symbol + ".")))) resolved.add(target);
6090
6329
  }
6091
6330
  const groups = /* @__PURE__ */ new Map();
6092
6331
  const add = (evidence, match) => groups.set(evidence, [...groups.get(evidence) ?? [], match]);
6093
- for (const target of targets) {
6332
+ for (const target of targets2) {
6094
6333
  if (resolved.has(target)) {
6095
6334
  add("resolved-import", target);
6096
6335
  continue;
@@ -6099,7 +6338,7 @@ function createReferenceMatcher(targets, available, targetContents = /* @__PURE_
6099
6338
  add("explicit-path", target);
6100
6339
  continue;
6101
6340
  }
6102
- const ext = path29.posix.extname(target), name = path29.posix.basename(target, ext);
6341
+ const ext = path31.posix.extname(target), name = path31.posix.basename(target, ext);
6103
6342
  if (!/\.(?:[cm]?[jt]sx?|py|kt|java|go|rs|swift|rb|cs|cpp|c|h|hpp|dart|php)$/.test(ext) || name.startsWith(".") || name.length < 3 || genericNames.has(name.toLowerCase()) || counts.get(name) !== 1) continue;
6104
6343
  if (word(name).test(source.content)) {
6105
6344
  add("name-candidate", name);
@@ -6128,11 +6367,11 @@ var impact_exports = {};
6128
6367
  __export(impact_exports, {
6129
6368
  analyzeImpact: () => analyzeImpact
6130
6369
  });
6131
- import path30 from "path";
6370
+ import path32 from "path";
6132
6371
  import { execFile as execFile8 } from "child_process";
6133
6372
  import { promisify as promisify8 } from "util";
6134
6373
  async function analyzeImpact(rootDir, targetFiles) {
6135
- const resolvedRoot = path30.resolve(rootDir);
6374
+ const resolvedRoot = path32.resolve(rootDir);
6136
6375
  const resolvedTargets = await resolveTargetFiles(resolvedRoot, targetFiles);
6137
6376
  const [cochange, references, tests] = await Promise.all([
6138
6377
  getCochangeFiles(resolvedRoot, resolvedTargets),
@@ -6146,10 +6385,10 @@ async function analyzeImpact(rootDir, targetFiles) {
6146
6385
  tests
6147
6386
  };
6148
6387
  }
6149
- async function resolveTargetFiles(rootDir, targets) {
6388
+ async function resolveTargetFiles(rootDir, targets2) {
6150
6389
  const resolved = [];
6151
6390
  const access = await createFileAccess(rootDir);
6152
- for (const target of targets) {
6391
+ for (const target of targets2) {
6153
6392
  if (target.includes("/")) {
6154
6393
  const normalized = normalizeRepoPath(target);
6155
6394
  if (normalized) resolved.push(normalized);
@@ -6246,9 +6485,9 @@ async function getRelatedTests(rootDir, targetFiles) {
6246
6485
  const testFiles = await (await createFileAccess(rootDir)).list(testPatterns);
6247
6486
  const results = [];
6248
6487
  for (const target of targetFiles) {
6249
- const targetBaseName = path30.basename(target).replace(/\.[^.]+$/, "");
6488
+ const targetBaseName = path32.basename(target).replace(/\.[^.]+$/, "");
6250
6489
  for (const testFile of testFiles) {
6251
- const testBaseName = path30.basename(testFile).replace(/\.[^.]+$/, "");
6490
+ const testBaseName = path32.basename(testFile).replace(/\.[^.]+$/, "");
6252
6491
  const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
6253
6492
  if (sourceName === targetBaseName) {
6254
6493
  results.push({
@@ -6276,26 +6515,26 @@ var assemble_exports = {};
6276
6515
  __export(assemble_exports, {
6277
6516
  assembleContext: () => assembleContext
6278
6517
  });
6279
- import path31 from "path";
6280
- import fs21 from "fs/promises";
6518
+ import path33 from "path";
6519
+ import fs22 from "fs/promises";
6281
6520
  async function collectImpact(root, candidates) {
6282
- const targets = /* @__PURE__ */ new Set();
6521
+ const targets2 = /* @__PURE__ */ new Set();
6283
6522
  let sourceFiles;
6284
6523
  for (const candidate of sanitizeRepoPaths(candidates)) {
6285
- const stat = await fs21.stat(path31.join(root, candidate)).catch(() => null);
6524
+ const stat = await fs22.stat(path33.join(root, candidate)).catch(() => null);
6286
6525
  if (stat?.isDirectory()) {
6287
6526
  sourceFiles ??= await (await createFileAccess(root)).list();
6288
6527
  for (const file of sourceFiles) {
6289
- if (anchorMatches(candidate, file)) targets.add(file);
6290
- if (targets.size >= MAX_IMPACT_TARGETS) break;
6528
+ if (anchorMatches(candidate, file)) targets2.add(file);
6529
+ if (targets2.size >= MAX_IMPACT_TARGETS) break;
6291
6530
  }
6292
6531
  } else {
6293
- targets.add(candidate);
6532
+ targets2.add(candidate);
6294
6533
  }
6295
- if (targets.size >= MAX_IMPACT_TARGETS) break;
6534
+ if (targets2.size >= MAX_IMPACT_TARGETS) break;
6296
6535
  }
6297
- if (!targets.size) return { impact: null, relatedTests: [] };
6298
- const result = await analyzeImpact(root, [...targets]);
6536
+ if (!targets2.size) return { impact: null, relatedTests: [] };
6537
+ const result = await analyzeImpact(root, [...targets2]);
6299
6538
  const referenceCandidatesOmitted = result.references.filter((r) => r.evidence === "name-candidate").length;
6300
6539
  return {
6301
6540
  impact: {
@@ -6308,7 +6547,7 @@ async function collectImpact(root, candidates) {
6308
6547
  };
6309
6548
  }
6310
6549
  async function assembleContext(rootDir, task, files) {
6311
- const resolvedRoot = path31.resolve(rootDir);
6550
+ const resolvedRoot = path33.resolve(rootDir);
6312
6551
  const mapState = await inspectSnapshot(resolvedRoot);
6313
6552
  const snapshot = mapState.snapshot;
6314
6553
  const store = await loadDecisionStore(resolvedRoot);
@@ -6504,7 +6743,7 @@ __export(observations_exports, {
6504
6743
  observeActivation: () => observeActivation,
6505
6744
  readObservation: () => readObservation
6506
6745
  });
6507
- import fs22 from "fs/promises";
6746
+ import fs23 from "fs/promises";
6508
6747
  import { z as z23 } from "zod";
6509
6748
  function observationPath(directory, host, revision, version = executingVersion()) {
6510
6749
  return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
@@ -6521,15 +6760,15 @@ async function observeActivation(dir, event, options = {}) {
6521
6760
  if (host !== "codex" && host !== "claude" || !process.env.MASON_SETUP_ROOT) return null;
6522
6761
  try {
6523
6762
  if (!revision) {
6524
- if ((await loadSetup(process.env.MASON_SETUP_ROOT))?.hosts[host]) {
6763
+ if ((await effectiveSetup(process.env.MASON_SETUP_ROOT)).setup?.hosts[host]) {
6525
6764
  return "Mason setup is now present. Restart the assistant to connect this MCP session to the configured integration; activation has not been established by this session.";
6526
6765
  }
6527
6766
  return inactiveSetupMessage();
6528
6767
  }
6529
6768
  const capturedDirectory = options.directory?.match(/^\.mason\/reports\/automation\/[a-f0-9]{24}$/)?.[0] ?? options.reportPath?.match(/^(\.mason\/reports\/automation\/[a-f0-9]{24})\/checks\//)?.[1];
6530
- const ws = capturedDirectory ? { root: await fs22.realpath(dir), directory: capturedDirectory } : await workspace(dir);
6531
- if (ws.root !== await fs22.realpath(process.env.MASON_SETUP_ROOT)) return null;
6532
- const setup = await loadSetup(ws.root);
6769
+ const ws = capturedDirectory ? { root: await fs23.realpath(dir), directory: capturedDirectory } : await workspace(dir);
6770
+ if (ws.root !== await fs23.realpath(process.env.MASON_SETUP_ROOT)) return null;
6771
+ const { setup } = await effectiveSetup(ws.root);
6533
6772
  if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
6534
6773
  const directory = ws.directory + "/activation";
6535
6774
  await withLock(ws.root, directory, async () => {
@@ -6603,8 +6842,9 @@ __export(status_exports, {
6603
6842
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
6604
6843
  async function setupStatus(dir) {
6605
6844
  const ws = await workspace(dir);
6606
- const setup = await loadSetup(ws.root);
6607
- if (!setup) {
6845
+ const activation = await effectiveSetup(ws.root);
6846
+ const setup = activation.setup;
6847
+ if (!setup || !Object.keys(setup.hosts).length) {
6608
6848
  const partial = await Promise.all(["codex", "claude"].map(async (host) => ({
6609
6849
  host,
6610
6850
  receipt: await loadSetupReceipt(ws.root, ws.directory, host)
@@ -6638,7 +6878,7 @@ async function setupStatus(dir) {
6638
6878
  ]));
6639
6879
  const hooks = isDeepStrictEqual2(compatibleHooks, expectedHooks) && !config.disabled;
6640
6880
  const local = await loadSetupReceipt(ws.root, ws.directory, host);
6641
- const configured = local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
6881
+ const configured = activation.root !== ws.root || local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
6642
6882
  const observation = await readObservation(ws.root, ws.directory, host, entry.revision, installed.version ?? "unavailable");
6643
6883
  const sessions = Object.values(observation?.sessions ?? {}).sort((a, b) => b.at.localeCompare(a.at));
6644
6884
  const complete = sessions.find((session) => events.every((event) => session.events.includes(event)));
@@ -6670,6 +6910,7 @@ async function setupStatus(dir) {
6670
6910
  status: statuses.length && statuses.every((status) => status === "active") ? "active" : statuses.includes("attention") ? "attention" : "pending",
6671
6911
  root: ws.root,
6672
6912
  hosts,
6913
+ activationRoot: activation.root,
6673
6914
  decisionRecords: decisions.records.length,
6674
6915
  diagnostics: decisions.diagnostics,
6675
6916
  scope: "Activation receipts record observed use after setup, not complete interception, correct repairs, or measured usefulness. Host trust and higher-priority settings may prevent execution. Receipts are local to this worktree and branch."
@@ -6677,6 +6918,9 @@ async function setupStatus(dir) {
6677
6918
  }
6678
6919
  function summarizeActivation(status) {
6679
6920
  const lines = ["Mason setup: " + status.status + "."];
6921
+ if (status.activationRoot && status.activationRoot !== status.root) {
6922
+ lines.push("Activation inherited from " + status.activationRoot + "; evidence belongs to this worktree.");
6923
+ }
6680
6924
  for (const [host, state] of Object.entries(status.hosts)) {
6681
6925
  lines.push(
6682
6926
  `${host}: ${state.status}`,
@@ -6846,8 +7090,8 @@ __export(client_exports, {
6846
7090
  function createConfluenceClient(config, fetchFn = fetch) {
6847
7091
  const baseUrl = config.baseUrl.replace(/\/+$/, "");
6848
7092
  const auth = "Basic " + Buffer.from(`${config.email}:${config.apiToken}`).toString("base64");
6849
- async function call(method, path35, body) {
6850
- const res = await fetchFn(`${baseUrl}${path35}`, {
7093
+ async function call(method, path43, body) {
7094
+ const res = await fetchFn(`${baseUrl}${path43}`, {
6851
7095
  method,
6852
7096
  headers: {
6853
7097
  Authorization: auth,
@@ -6859,7 +7103,7 @@ function createConfluenceClient(config, fetchFn = fetch) {
6859
7103
  if (!res.ok) {
6860
7104
  const text2 = await res.text();
6861
7105
  throw new Error(
6862
- `Confluence ${method} ${path35} failed: ${res.status} ${res.statusText} \u2014 ${text2}`
7106
+ `Confluence ${method} ${path43} failed: ${res.status} ${res.statusText} \u2014 ${text2}`
6863
7107
  );
6864
7108
  }
6865
7109
  if (res.status === 204) return null;
@@ -6960,28 +7204,28 @@ __export(config_exports2, {
6960
7204
  saveConfluenceConfig: () => saveConfluenceConfig,
6961
7205
  validateProvider: () => validateProvider
6962
7206
  });
6963
- import fs23 from "fs/promises";
6964
- import path32 from "path";
7207
+ import fs24 from "fs/promises";
7208
+ import path34 from "path";
6965
7209
  import os3 from "os";
6966
7210
  import { execFile as execFile9 } from "child_process";
6967
7211
  import { promisify as promisify9 } from "util";
6968
7212
  function configDir() {
6969
- return path32.join(os3.homedir(), ".mason");
7213
+ return path34.join(os3.homedir(), ".mason");
6970
7214
  }
6971
7215
  function configFile() {
6972
- return path32.join(configDir(), "config.json");
7216
+ return path34.join(configDir(), "config.json");
6973
7217
  }
6974
7218
  async function loadConfig() {
6975
7219
  try {
6976
- const raw = await fs23.readFile(configFile(), "utf-8");
7220
+ const raw = await fs24.readFile(configFile(), "utf-8");
6977
7221
  return JSON.parse(raw);
6978
7222
  } catch {
6979
7223
  return null;
6980
7224
  }
6981
7225
  }
6982
7226
  async function saveConfig(config) {
6983
- await fs23.mkdir(configDir(), { recursive: true });
6984
- await fs23.writeFile(configFile(), JSON.stringify(config, null, 2), "utf-8");
7227
+ await fs24.mkdir(configDir(), { recursive: true });
7228
+ await fs24.writeFile(configFile(), JSON.stringify(config, null, 2), "utf-8");
6985
7229
  }
6986
7230
  function getDefaultModel(provider) {
6987
7231
  return DEFAULT_MODELS[provider];
@@ -7141,21 +7385,21 @@ var init_renderer = __esm({
7141
7385
  });
7142
7386
 
7143
7387
  // src/confluence/diff.ts
7144
- import fs24 from "fs/promises";
7145
- import path33 from "path";
7388
+ import fs25 from "fs/promises";
7389
+ import path35 from "path";
7146
7390
  import { createHash as createHash6 } from "crypto";
7147
7391
  function hashDescription(description) {
7148
7392
  return createHash6("sha256").update(description, "utf8").digest("hex");
7149
7393
  }
7150
7394
  function syncStateDir(rootDir) {
7151
- return path33.join(rootDir, ".mason");
7395
+ return path35.join(rootDir, ".mason");
7152
7396
  }
7153
7397
  function syncStatePath(rootDir) {
7154
- return path33.join(syncStateDir(rootDir), "confluence-sync.json");
7398
+ return path35.join(syncStateDir(rootDir), "confluence-sync.json");
7155
7399
  }
7156
7400
  async function loadSyncState(rootDir) {
7157
7401
  try {
7158
- const raw = await fs24.readFile(syncStatePath(rootDir), "utf-8");
7402
+ const raw = await fs25.readFile(syncStatePath(rootDir), "utf-8");
7159
7403
  const parsed = JSON.parse(raw);
7160
7404
  if (parsed.version !== 2) return null;
7161
7405
  return parsed;
@@ -7164,8 +7408,8 @@ async function loadSyncState(rootDir) {
7164
7408
  }
7165
7409
  }
7166
7410
  async function saveSyncState(rootDir, state) {
7167
- await fs24.mkdir(syncStateDir(rootDir), { recursive: true });
7168
- await fs24.writeFile(
7411
+ await fs25.mkdir(syncStateDir(rootDir), { recursive: true });
7412
+ await fs25.writeFile(
7169
7413
  syncStatePath(rootDir),
7170
7414
  JSON.stringify(state, null, 2),
7171
7415
  "utf-8"
@@ -7723,8 +7967,8 @@ import { z as z24 } from "zod";
7723
7967
  // src/mcp/tools.ts
7724
7968
  init_repair();
7725
7969
  init_advisory_review();
7726
- import fs25 from "fs/promises";
7727
- import path34 from "path";
7970
+ import fs26 from "fs/promises";
7971
+ import path36 from "path";
7728
7972
  import { execFile as execFile11 } from "child_process";
7729
7973
  import { promisify as promisify11 } from "util";
7730
7974
 
@@ -7733,11 +7977,11 @@ init_audit();
7733
7977
  init_types();
7734
7978
  init_repair();
7735
7979
  init_findings();
7736
- import path23 from "path";
7980
+ import path25 from "path";
7737
7981
 
7738
7982
  // src/audit/advisory-cli.ts
7739
7983
  init_advisory_review();
7740
- import path22 from "path";
7984
+ import path24 from "path";
7741
7985
  import { stripVTControlCharacters } from "util";
7742
7986
 
7743
7987
  // src/audit/cli.ts
@@ -7804,7 +8048,7 @@ function formatFixPrompt(report, baselinePath) {
7804
8048
  "- new-module: review whether the directory needs documenting; an omission alone does not establish a defect. Preserve scope and describe only what you verified."
7805
8049
  );
7806
8050
  lines.push(
7807
- "- Historical ADVISORIES require a separate authorized assessment using review_advisory with the baselinePath and findingId from verification. Prepare and inspect evidence first; record addressed, inapplicable or deferred with the actual reviewer, note and reviewToken. Decision findings use review_decision. Candidates marked resolution: recheck can be rerun to see whether the condition remains; this does not establish semantic approval. Report what remains unknown."
8051
+ "- Specific historical ADVISORIES require a separate authorized assessment using review_advisory with the baselinePath and findingId from verification. Prepare and inspect evidence first; record addressed, inapplicable or deferred with the actual reviewer, note and reviewToken. Dependency notices marked resolution: informational require no review. Dependency advisories may be dismissed with action: dismiss, reasonCode (no-dependency-claims or unrelated-manifest-change), and note, without a reviewer or token. Decision findings use review_decision. Candidates marked resolution: recheck can be rerun to see whether the condition remains; this does not establish semantic approval. Report what remains unknown."
7808
8052
  );
7809
8053
  lines.push("");
7810
8054
  lines.push("AUDIT REPORT (current context files and repository evidence, including local edits):");
@@ -7834,18 +8078,18 @@ import { execFile as execFile3 } from "child_process";
7834
8078
  import { promisify as promisify3 } from "util";
7835
8079
 
7836
8080
  // src/analyzers/base.ts
7837
- import fs15 from "fs/promises";
7838
- import fg6 from "fast-glob";
8081
+ import fs16 from "fs/promises";
8082
+ import fg8 from "fast-glob";
7839
8083
  var BaseAnalyzer = class {
7840
8084
  async findFiles(patterns, root) {
7841
- return fg6(patterns, {
8085
+ return fg8(patterns, {
7842
8086
  cwd: root,
7843
8087
  ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"],
7844
8088
  absolute: true
7845
8089
  });
7846
8090
  }
7847
8091
  async readFile(filePath) {
7848
- return fs15.readFile(filePath, "utf-8");
8092
+ return fs16.readFile(filePath, "utf-8");
7849
8093
  }
7850
8094
  createFinding(partial) {
7851
8095
  return {
@@ -8050,7 +8294,7 @@ async function isGitRepo(dir) {
8050
8294
 
8051
8295
  // src/mcp/sampler.ts
8052
8296
  init_files();
8053
- import path24 from "path";
8297
+ import path26 from "path";
8054
8298
  import { execFile as execFile4 } from "child_process";
8055
8299
  import { promisify as promisify4 } from "util";
8056
8300
  var exec2 = promisify4(execFile4);
@@ -8158,8 +8402,8 @@ async function sampleFiles(rootDir, maxFiles = 25) {
8158
8402
  const projectConfig = access.config;
8159
8403
  for (const filePath of projectConfig.alwaysInclude ?? []) {
8160
8404
  if (selected.size >= maxFiles) break;
8161
- const resolvedPath = path24.resolve(rootDir, filePath);
8162
- if (!resolvedPath.startsWith(path24.resolve(rootDir))) continue;
8405
+ const resolvedPath = path26.resolve(rootDir, filePath);
8406
+ if (!resolvedPath.startsWith(path26.resolve(rootDir))) continue;
8163
8407
  selected.set(filePath, "always-include (project config)");
8164
8408
  }
8165
8409
  let configCount = 0;
@@ -8225,7 +8469,7 @@ async function sampleFiles(rootDir, maxFiles = 25) {
8225
8469
  if (!line) continue;
8226
8470
  if (line.includes("node_modules") || line.includes("/build/") || line.includes(".gradle") || line.includes("/generated/"))
8227
8471
  continue;
8228
- const ext = path24.extname(line).slice(1);
8472
+ const ext = path26.extname(line).slice(1);
8229
8473
  if (!SOURCE_EXTENSIONS.includes(ext)) continue;
8230
8474
  fileCounts.set(line, (fileCounts.get(line) ?? 0) + 1);
8231
8475
  }
@@ -8580,7 +8824,7 @@ ${JSON.stringify({ partials }, null, 2)}`;
8580
8824
  init_lock();
8581
8825
  init_storage();
8582
8826
  init_snapshot();
8583
- import fs16 from "fs/promises";
8827
+ import fs17 from "fs/promises";
8584
8828
  import { z as z11 } from "zod";
8585
8829
  var DIRECTORY = ".mason/partial-snapshots";
8586
8830
  var partialSchema = z11.object({
@@ -8598,7 +8842,7 @@ async function savePartial(rootDir, partial) {
8598
8842
  async function loadAllPartials(rootDir) {
8599
8843
  let entries;
8600
8844
  try {
8601
- entries = await fs16.readdir(await storePath(rootDir, DIRECTORY));
8845
+ entries = await fs17.readdir(await storePath(rootDir, DIRECTORY));
8602
8846
  } catch (error) {
8603
8847
  if (error.code === "ENOENT") return [];
8604
8848
  throw error;
@@ -8621,11 +8865,11 @@ async function loadScope(rootDir) {
8621
8865
  }
8622
8866
  async function clearScope(rootDir) {
8623
8867
  await withSnapshotWrite(rootDir, async () => {
8624
- await fs16.rm(await storePath(rootDir, `${DIRECTORY}/scope.json`), { force: true });
8868
+ await fs17.rm(await storePath(rootDir, `${DIRECTORY}/scope.json`), { force: true });
8625
8869
  });
8626
8870
  }
8627
8871
  async function clearAllPartials(rootDir) {
8628
- await fs16.rm(await storePath(rootDir, DIRECTORY), { recursive: true, force: true });
8872
+ await fs17.rm(await storePath(rootDir, DIRECTORY), { recursive: true, force: true });
8629
8873
  }
8630
8874
  function batchIdFor(offset) {
8631
8875
  return `batch-${String(offset).padStart(6, "0")}`;
@@ -8642,7 +8886,7 @@ async function buildContext(dir) {
8642
8886
  };
8643
8887
  }
8644
8888
  async function analyzeProject(dir) {
8645
- const rootDir = path34.resolve(dir);
8889
+ const rootDir = path36.resolve(dir);
8646
8890
  const context = await buildContext(rootDir);
8647
8891
  const results = await runAll(context);
8648
8892
  const projectSnapshot = await detectProjectSnapshot(rootDir);
@@ -8697,7 +8941,7 @@ async function detectProjectSnapshot(rootDir) {
8697
8941
  const present = [];
8698
8942
  for (const file of buildFiles) {
8699
8943
  try {
8700
- await fs25.access(path34.join(rootDir, file));
8944
+ await fs26.access(path36.join(rootDir, file));
8701
8945
  present.push(file);
8702
8946
  } catch {
8703
8947
  }
@@ -8739,7 +8983,7 @@ async function detectProjectSnapshot(rootDir) {
8739
8983
  const sourceFiles = await access.list();
8740
8984
  const fileCounts = {};
8741
8985
  for (const file of sourceFiles) {
8742
- const ext = path34.extname(file).slice(1);
8986
+ const ext = path36.extname(file).slice(1);
8743
8987
  fileCounts[ext] = (fileCounts[ext] ?? 0) + 1;
8744
8988
  }
8745
8989
  return {
@@ -8750,7 +8994,7 @@ async function detectProjectSnapshot(rootDir) {
8750
8994
  };
8751
8995
  }
8752
8996
  async function getCodeSamples(dir, count4 = 15) {
8753
- const rootDir = path34.resolve(dir);
8997
+ const rootDir = path36.resolve(dir);
8754
8998
  const samples = await sampleFiles(rootDir, count4);
8755
8999
  const output = {
8756
9000
  note: "These are previews (first ~60 lines). Read the file directly with your own tools to see it in full.",
@@ -8793,7 +9037,7 @@ async function unmappedContextResponse(rootDir) {
8793
9037
  });
8794
9038
  }
8795
9039
  async function getProjectStructure(dir) {
8796
- const rootDir = path34.resolve(dir);
9040
+ const rootDir = path36.resolve(dir);
8797
9041
  const allFiles = await (await createFileAccess(rootDir)).list("**/*");
8798
9042
  const dirInfo = /* @__PURE__ */ new Map();
8799
9043
  for (const file of allFiles) {
@@ -8805,7 +9049,7 @@ async function getProjectStructure(dir) {
8805
9049
  }
8806
9050
  const info = dirInfo.get(dirPath);
8807
9051
  info.fileCount++;
8808
- const ext = path34.extname(file).slice(1);
9052
+ const ext = path36.extname(file).slice(1);
8809
9053
  if (ext) {
8810
9054
  info.extensions.set(ext, (info.extensions.get(ext) ?? 0) + 1);
8811
9055
  }
@@ -8850,7 +9094,7 @@ async function buildChangedFilePreviews(rootDir, changedFiles) {
8850
9094
  return previews2;
8851
9095
  }
8852
9096
  async function getSnapshot(dir) {
8853
- const rootDir = path34.resolve(dir);
9097
+ const rootDir = path36.resolve(dir);
8854
9098
  const snapshot = await loadSnapshot(rootDir);
8855
9099
  if (!snapshot) {
8856
9100
  return unmappedContextResponse(rootDir);
@@ -8951,7 +9195,7 @@ function driftHint(report) {
8951
9195
  return "Read the changed files under staleFeatures/staleFlows, update those entries (fold unmappedFiles into the right features), and call save_snapshot with only the affected entries \u2014 unchanged entries are preserved. Drop ghostFiles from any entries that reference them, and delete features/flows that no longer exist via save_snapshot's removeFeatures/removeFlows.";
8952
9196
  }
8953
9197
  async function checkDrift(dir) {
8954
- const rootDir = path34.resolve(dir);
9198
+ const rootDir = path36.resolve(dir);
8955
9199
  const report = await computeDrift(rootDir);
8956
9200
  if (!report) {
8957
9201
  return JSON.stringify({
@@ -8982,7 +9226,7 @@ async function checkDrift(dir) {
8982
9226
  return JSON.stringify({ exists: true, ...report, verification, hint });
8983
9227
  }
8984
9228
  async function generateSnapshotBatch(dir, offset = 0, batchSize = DEFAULT_BATCH_SIZE, files) {
8985
- const rootDir = path34.resolve(dir);
9229
+ const rootDir = path36.resolve(dir);
8986
9230
  const scoped = files !== void 0 && files.length > 0;
8987
9231
  const scopeFiles = scoped ? sanitizePaths(rootDir, files) : void 0;
8988
9232
  const batch = await prepareSnapshotBatch(dir, offset, batchSize, scopeFiles);
@@ -9029,7 +9273,7 @@ async function generateSnapshotBatch(dir, offset = 0, batchSize = DEFAULT_BATCH_
9029
9273
  );
9030
9274
  }
9031
9275
  async function saveSnapshotPartial(dir, batchId, offset, features, flows) {
9032
- const rootDir = path34.resolve(dir);
9276
+ const rootDir = path36.resolve(dir);
9033
9277
  for (const feat of Object.values(features)) {
9034
9278
  feat.files = sanitizePaths(rootDir, feat.files);
9035
9279
  if (feat.tests) feat.tests = sanitizePaths(rootDir, feat.tests);
@@ -9058,7 +9302,7 @@ async function saveSnapshotPartial(dir, batchId, offset, features, flows) {
9058
9302
  );
9059
9303
  }
9060
9304
  async function reduceSnapshot(dir) {
9061
- const rootDir = path34.resolve(dir);
9305
+ const rootDir = path36.resolve(dir);
9062
9306
  const partials = await loadAllPartials(rootDir);
9063
9307
  if (partials.length === 0) {
9064
9308
  return JSON.stringify(
@@ -9120,7 +9364,7 @@ async function reduceSnapshot(dir) {
9120
9364
  );
9121
9365
  }
9122
9366
  async function fullAnalysis(dir) {
9123
- const rootDir = path34.resolve(dir);
9367
+ const rootDir = path36.resolve(dir);
9124
9368
  const [analysis, structure, samples, testMap, snapshot] = await Promise.all([
9125
9369
  analyzeProject(dir),
9126
9370
  getProjectStructure(dir),
@@ -9149,7 +9393,7 @@ function sanitizePaths(rootDir, files) {
9149
9393
  return sanitizeRepoPaths(files);
9150
9394
  }
9151
9395
  async function saveSnapshotData(dir, features, flows, removeFeatures = [], removeFlows = []) {
9152
- const rootDir = path34.resolve(dir);
9396
+ const rootDir = path36.resolve(dir);
9153
9397
  return updateSnapshot(rootDir, async (previous) => {
9154
9398
  const gitHash = await getCurrentGitHash(rootDir);
9155
9399
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -9229,14 +9473,14 @@ async function saveSnapshotData(dir, features, flows, removeFeatures = [], remov
9229
9473
  });
9230
9474
  }
9231
9475
  async function getImpact(dir, files) {
9232
- const rootDir = path34.resolve(dir);
9476
+ const rootDir = path36.resolve(dir);
9233
9477
  const { analyzeImpact: analyzeImpact2 } = await Promise.resolve().then(() => (init_impact(), impact_exports));
9234
9478
  const result = await analyzeImpact2(rootDir, files);
9235
9479
  return JSON.stringify(result, null, 2);
9236
9480
  }
9237
9481
  var VERIFY_DEFAULT_SAMPLE = 5;
9238
9482
  async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
9239
- const rootDir = path34.resolve(dir);
9483
+ const rootDir = path36.resolve(dir);
9240
9484
  const snapshot = await loadSnapshot(rootDir);
9241
9485
  if (!snapshot) {
9242
9486
  return JSON.stringify({
@@ -9289,7 +9533,7 @@ async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
9289
9533
  });
9290
9534
  }
9291
9535
  async function saveVerification(dir, verdicts) {
9292
- const rootDir = path34.resolve(dir);
9536
+ const rootDir = path36.resolve(dir);
9293
9537
  return updateSnapshot(rootDir, async (snapshot) => {
9294
9538
  if (!snapshot) {
9295
9539
  return { snapshot: null, result: JSON.stringify({ exists: false, stamped: [], hint: "No concept map exists. Build it and run verify_snapshot before recording verdicts." }) };
@@ -9363,16 +9607,16 @@ async function saveVerification(dir, verdicts) {
9363
9607
  });
9364
9608
  }
9365
9609
  async function saveDecision(dir, input) {
9366
- const rootDir = path34.resolve(dir);
9610
+ const rootDir = path36.resolve(dir);
9367
9611
  const { upsertDecision: upsertDecision2 } = await Promise.resolve().then(() => (init_decisions(), decisions_exports));
9368
9612
  const result = await upsertDecision2(rootDir, input);
9369
9613
  return JSON.stringify(result);
9370
9614
  }
9371
9615
  async function reviewDecision2(dir, input) {
9372
- return JSON.stringify(await reviewDecision(path34.resolve(dir), input));
9616
+ return JSON.stringify(await reviewDecision(path36.resolve(dir), input));
9373
9617
  }
9374
9618
  async function getContext(dir, task, files) {
9375
- const rootDir = path34.resolve(dir);
9619
+ const rootDir = path36.resolve(dir);
9376
9620
  const { assembleContext: assembleContext2 } = await Promise.resolve().then(() => (init_assemble(), assemble_exports));
9377
9621
  const bundle = await assembleContext2(rootDir, task, files);
9378
9622
  return JSON.stringify(bundle);
@@ -9421,7 +9665,7 @@ async function masonInit(dir, options = {}) {
9421
9665
  return JSON.stringify(await setupProject2(dir, options), null, 2);
9422
9666
  }
9423
9667
  if (options.host) throw new Error("host applies only to mode: setup.");
9424
- const rootDir = path34.resolve(dir);
9668
+ const rootDir = path36.resolve(dir);
9425
9669
  const marker = await loadProjectMarker(rootDir);
9426
9670
  const { loadSetup: loadSetup2 } = await Promise.resolve().then(() => (init_model(), model_exports));
9427
9671
  const setup = await loadSetup2(rootDir);
@@ -9441,7 +9685,7 @@ async function masonInit(dir, options = {}) {
9441
9685
  );
9442
9686
  }
9443
9687
  async function masonCompleteInit(dir, options = {}) {
9444
- const rootDir = path34.resolve(dir);
9688
+ const rootDir = path36.resolve(dir);
9445
9689
  const existing = await loadProjectMarker(rootDir);
9446
9690
  const features = await loadProjectFeatures(rootDir);
9447
9691
  const marker = {
@@ -9550,7 +9794,7 @@ async function masonSetConfluence(input) {
9550
9794
  );
9551
9795
  }
9552
9796
  async function exportToConfluenceTool(dir, overrides) {
9553
- const rootDir = path34.resolve(dir);
9797
+ const rootDir = path36.resolve(dir);
9554
9798
  const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports2));
9555
9799
  const { exportToConfluence: exportToConfluence2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
9556
9800
  const config = await loadConfig2();
@@ -9589,7 +9833,7 @@ function createMcpServer() {
9589
9833
  const server = new McpServer(
9590
9834
  {
9591
9835
  name: "mason",
9592
- version: "0.18.0"
9836
+ version: "0.19.1"
9593
9837
  },
9594
9838
  {
9595
9839
  instructions: (setupNotice ? setupNotice + "\n\n" : "") + 'Mason retrieves recorded decisions, file impact, and optional feature/flow maps. Use get_context with the task and known files, and get_impact before editing. Save learned rationale and constraints with save_decision; review and commit the local records through the project workflow. These tools work without initialization or a concept map. Consult trust and diagnostics: changed or unknown freshness needs source inspection, stale or unknown map verification needs a fresh review, recorded failed verdicts need correction, and proposals are suggestions and legacy records are unreviewed. Accepted decisions are recorded team constraints, still subject to freshness checks. Use review_decision to prepare code evidence and record only authorized acceptance, reaffirmation, or retirement. Never invent a reviewer or treat these recorded identities as authenticated approval. mason_init returns documentation audit and committed-diff review findings with a quickstart guide. Use mode: "map" only when a full architecture map is requested. A missing map is not a setup failure; use decisions and source evidence. get_snapshot provides architecture navigation when a map is available. The mason-audit and mason-review CLIs also work without setup.'
@@ -9635,7 +9879,7 @@ function createMcpServer() {
9635
9879
  );
9636
9880
  server.tool(
9637
9881
  "review_advisory",
9638
- "Prepare an assessment of an original repair advisory and its exact repository scope, then record authorized addressed, inapplicable or deferred outcomes with reviewToken, reviewer and note. Relevant changes reopen reviews; unrelated commits preserve them. Records belong in Git. Decision findings route to review_decision; this tool never approves decisions. Recorded identities are assertions, not authenticated approvals.",
9882
+ "Prepare an assessment of an original repair advisory and its exact repository scope, then record authorized addressed, inapplicable or deferred outcomes with reviewToken, reviewer and note. Relevant changes reopen reviews; unrelated commits preserve them. Records belong in Git. Decision findings route to review_decision; this tool never approves decisions. Recorded identities are assertions, not authenticated approvals. For dependency advisories, action dismiss with reasonCode and note records a one-step dismissal without a token or reviewer. no-dependency-claims reopens only on document content changes; unrelated-manifest-change also tracks scoped manifests.",
9639
9883
  { dir: z24.string().describe("Absolute path to the project root"), ...advisoryReviewRequest.shape },
9640
9884
  async ({ dir, ...request }) => ({ content: [{ type: "text", text: await masonReviewAdvisory(dir, request) }] })
9641
9885
  );
@@ -9970,14 +10214,592 @@ function createMcpServer() {
9970
10214
  );
9971
10215
  return server;
9972
10216
  }
9973
- async function startMcpServer() {
10217
+ async function startMcpServer(onClose) {
9974
10218
  const server = createMcpServer();
10219
+ if (onClose) server.server.onclose = onClose;
9975
10220
  const transport = new StdioServerTransport();
9976
10221
  await server.connect(transport);
9977
10222
  }
9978
10223
 
10224
+ // src/distribution/updates.ts
10225
+ init_store();
10226
+ init_storage();
10227
+ import fs33 from "fs/promises";
10228
+ import path42 from "path";
10229
+ import { spawn as spawn3 } from "child_process";
10230
+ import { randomUUID as randomUUID11 } from "crypto";
10231
+ import { z as z30 } from "zod";
10232
+
10233
+ // src/distribution/state.ts
10234
+ init_storage();
10235
+ import os5 from "os";
10236
+ import path38 from "path";
10237
+ import { fileURLToPath } from "url";
10238
+ import fs28 from "fs/promises";
10239
+ import { z as z26 } from "zod";
10240
+
10241
+ // src/distribution/path.ts
10242
+ import fs27 from "fs/promises";
10243
+ import os4 from "os";
10244
+ import path37 from "path";
10245
+ import { execFile as execFile12 } from "child_process";
10246
+ import { promisify as promisify12 } from "util";
10247
+ import { createHash as createHash7, randomUUID as randomUUID7 } from "crypto";
10248
+ import { z as z25 } from "zod";
10249
+ var execute2 = promisify12(execFile12);
10250
+ var pathChangeSchema = z25.discriminatedUnion("kind", [
10251
+ z25.object({ kind: z25.literal("profile"), file: z25.string(), block: z25.string(), created: z25.boolean() }),
10252
+ z25.object({ kind: z25.literal("windows"), entry: z25.string(), created: z25.boolean() })
10253
+ ]);
10254
+ var quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
10255
+ async function configurePath(bin, previous, save, options = {}) {
10256
+ const env = options.env ?? process.env, platform = options.platform ?? process.platform;
10257
+ const changes = [...previous];
10258
+ const currentPath = platform === "win32" ? Object.entries(env).find(([key]) => key.toLowerCase() === "path")?.[1] : env.PATH;
10259
+ const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) => path37.posix.isAbsolute(entry) && path37.posix.resolve(entry) === path37.posix.resolve(bin));
10260
+ const setupHint = "To connect a project, run one of these inside its directory:\n Codex: mason setup --host codex\n Claude Code: mason setup --host claude";
10261
+ const message2 = (alreadyOnPath ? "" : "Open a new terminal to use mason.\n") + setupHint;
10262
+ const manual = (reason2) => ({ status: "manual", changes, message: `${reason2}
10263
+ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
10264
+ `}${setupHint}` });
10265
+ if (env.MASON_NO_MODIFY_PATH === "1") return manual("Automatic PATH setup is disabled (MASON_NO_MODIFY_PATH=1).");
10266
+ if (/[\r\n\0]/.test(bin) || bin.includes(platform === "win32" ? ";" : ":")) return manual("This installation path cannot be represented safely in PATH.");
10267
+ try {
10268
+ if (platform === "win32") {
10269
+ const current = await windowsPath("read");
10270
+ if (!hasWindowsEntry(current.value, bin)) {
10271
+ if (!changes.some((c) => c.kind === "windows" && c.entry === bin)) {
10272
+ changes.push({ kind: "windows", entry: bin, created: current.value === null });
10273
+ await save(changes);
10274
+ }
10275
+ await windowsPath("add", bin, current);
10276
+ }
10277
+ return { status: "configured", changes, message: message2 };
10278
+ }
10279
+ const shell = path37.basename(env.SHELL ?? os4.userInfo().shell ?? "");
10280
+ const userHome = options.userHome ?? os4.homedir();
10281
+ let profiles2;
10282
+ if (!["bash", "zsh", "sh"].includes(shell)) return manual(`Automatic PATH setup is unavailable for ${shell || "an unknown shell"}.`);
10283
+ if (env.MASON_PROFILE) profiles2 = [path37.resolve(env.MASON_PROFILE)];
10284
+ else if (shell === "zsh") profiles2 = [path37.join(env.ZDOTDIR || userHome, ".zshrc")];
10285
+ else if (shell === "sh") profiles2 = [path37.join(userHome, ".profile")];
10286
+ else {
10287
+ const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path37.join(userHome, name));
10288
+ let login = candidates[2];
10289
+ for (const file of candidates) if (await fs27.lstat(file).then(() => true, (error) => {
10290
+ if (error.code === "ENOENT") return false;
10291
+ throw error;
10292
+ })) {
10293
+ login = file;
10294
+ break;
10295
+ }
10296
+ profiles2 = [path37.join(userHome, ".bashrc"), login];
10297
+ }
10298
+ const marker = `# >>> mason PATH ${createHash7("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
10299
+ for (const profile of profiles2) {
10300
+ const stat = await fs27.lstat(profile).catch((error) => {
10301
+ if (error.code === "ENOENT") return null;
10302
+ throw error;
10303
+ });
10304
+ const file = stat?.isSymbolicLink() ? await fs27.realpath(profile) : path37.resolve(profile);
10305
+ const original = await fs27.readFile(file).catch((error) => {
10306
+ if (error.code === "ENOENT" && !stat) return null;
10307
+ throw error;
10308
+ });
10309
+ if (original && !Buffer.from(original.toString("utf8")).equals(original)) return manual(`Shell profile is not UTF-8: ${profile}.`);
10310
+ const text2 = original?.toString("utf8") ?? "";
10311
+ const owned = changes.find((c) => c.kind === "profile" && c.file === file);
10312
+ if (owned && text2.includes(owned.block)) continue;
10313
+ if (text2.includes(marker) || owned && text2.includes("# <<< mason PATH")) return manual(`Mason's PATH block was edited; retained ${profile}.`);
10314
+ const newline = text2.includes("\r\n") ? "\r\n" : "\n";
10315
+ const block = owned?.block ?? ("\n" + marker + '\ncase ":${PATH-}:" in\n *:' + quote(bin) + ":*) ;;\n *) export PATH=" + quote(bin) + ':"${PATH-}" ;;\nesac\n# <<< mason PATH <<<\n').replaceAll("\n", newline);
10316
+ if (!owned) {
10317
+ changes.push({ kind: "profile", file, block, created: original === null });
10318
+ await save(changes);
10319
+ }
10320
+ await fs27.mkdir(path37.dirname(file), { recursive: true });
10321
+ const handle = await fs27.open(file, original === null ? "ax+" : "a+", 384);
10322
+ try {
10323
+ if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0))) throw new Error(`Shell profile changed during installation: ${profile}`);
10324
+ await handle.writeFile(block);
10325
+ await handle.sync();
10326
+ } finally {
10327
+ await handle.close();
10328
+ }
10329
+ }
10330
+ return { status: "configured", changes, message: message2 };
10331
+ } catch (error) {
10332
+ return manual(`Could not configure PATH: ${error instanceof Error ? error.message : String(error)}`);
10333
+ }
10334
+ }
10335
+ var normalizeWindowsEntry = (value) => path37.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
10336
+ var hasWindowsEntry = (value, entry, env = process.env) => {
10337
+ const variables = new Map(Object.entries(env).map(([key, value2]) => [key.toLowerCase(), value2]));
10338
+ const expand = (part) => part.replace(/%([^%]+)%/g, (match, key) => variables.get(key.toLowerCase()) ?? match);
10339
+ return (value ?? "").split(";").some((part) => normalizeWindowsEntry(expand(part)) === normalizeWindowsEntry(entry));
10340
+ };
10341
+ async function windowsPath(action, entry = "", expected, removeEmpty = false) {
10342
+ const payload = Buffer.from(JSON.stringify({ action, entry, expected, removeEmpty })).toString("base64");
10343
+ const script = `$ErrorActionPreference = 'Stop'
10344
+ [Console]::OutputEncoding = New-Object Text.UTF8Encoding($false)
10345
+ $p = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${payload}')) | ConvertFrom-Json
10346
+ $key = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment')
10347
+ try {
10348
+ $value = $key.GetValue('Path', $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
10349
+ $kind = if ($null -eq $value) { $null } else { $key.GetValueKind('Path').ToString() }
10350
+ if ($null -ne $kind -and $kind -notin @('String','ExpandString')) { throw 'User PATH is not a string.' }
10351
+ if ($p.action -eq 'read') { @{ value = $value; kind = $kind } | ConvertTo-Json -Compress; exit 0 }
10352
+ if ($p.expected -and ($value -cne $p.expected.value -or $kind -cne $p.expected.kind)) { throw 'User PATH changed concurrently; retry the operation.' }
10353
+ $parts = if ($null -eq $value -or $value -eq '') { @() } else { @($value.Split(';')) }
10354
+ if ($p.action -eq 'add') { $next = (@($p.entry) + $parts) -join ';' }
10355
+ else { $next = (@($parts | Where-Object { $_ -cne $p.entry })) -join ';' }
10356
+ if ($next -cne $value) {
10357
+ $writeKind = if ($kind) { [Microsoft.Win32.RegistryValueKind]$kind } else { [Microsoft.Win32.RegistryValueKind]::ExpandString }
10358
+ if ($p.action -eq 'remove' -and $p.removeEmpty -and $next -eq '') { $key.DeleteValue('Path', $false) }
10359
+ else { $key.SetValue('Path', $next, $writeKind) }
10360
+ Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public class MasonEnvironment { [DllImport("user32.dll", CharSet=CharSet.Unicode, SetLastError=true)] public static extern IntPtr SendMessageTimeout(IntPtr h, uint m, UIntPtr w, string l, uint f, uint t, out UIntPtr r); }'
10361
+ $result = [UIntPtr]::Zero
10362
+ [void][MasonEnvironment]::SendMessageTimeout([IntPtr]0xffff, 0x1a, [UIntPtr]::Zero, 'Environment', 2, 1000, [ref]$result)
10363
+ }
10364
+ @{ value = $next; kind = $kind } | ConvertTo-Json -Compress
10365
+ } finally { $key.Dispose() }
10366
+ `;
10367
+ const { stdout } = await execute2("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { windowsHide: true, timeout: 15e3, maxBuffer: 1024 * 1024 });
10368
+ return JSON.parse(stdout.replace(/^\uFEFF/, "").trim());
10369
+ }
10370
+
10371
+ // src/distribution/state.ts
10372
+ var releaseVersion = z26.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/);
10373
+ var bundleId = z26.string().regex(/^[a-f0-9]{24}$/);
10374
+ var updatePolicySchema = z26.object({
10375
+ enabled: z26.boolean(),
10376
+ revision: z26.string(),
10377
+ pinnedVersion: releaseVersion.optional(),
10378
+ mirror: z26.string().optional()
10379
+ });
10380
+ var recordSchema3 = z26.object({
10381
+ format: z26.literal(1),
10382
+ home: z26.string(),
10383
+ bin: z26.string(),
10384
+ current: bundleId,
10385
+ version: releaseVersion,
10386
+ launchers: z26.record(z26.string()),
10387
+ previousLaunchers: z26.record(z26.string()).optional(),
10388
+ pathChanges: z26.array(pathChangeSchema).optional(),
10389
+ updates: updatePolicySchema.optional(),
10390
+ previous: z26.object({ id: bundleId, version: releaseVersion }).optional(),
10391
+ pending: z26.object({ id: bundleId, version: releaseVersion, policyRevision: z26.string() }).optional()
10392
+ });
10393
+ async function readInstallation(home) {
10394
+ const raw = await readStoreJson(home, "install.json");
10395
+ if (!raw) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
10396
+ const record = recordSchema3.parse(raw);
10397
+ if (record.home !== await fs28.realpath(home)) throw new Error("Invalid standalone installation receipt.");
10398
+ return record;
10399
+ }
10400
+ function standaloneLocation() {
10401
+ const bundle = path38.resolve(path38.dirname(fileURLToPath(import.meta.url)), "../..");
10402
+ const home = path38.dirname(path38.dirname(bundle));
10403
+ if (path38.dirname(bundle) !== path38.join(home, "versions") || !/^[a-f0-9]{24}$/.test(path38.basename(bundle))) {
10404
+ return null;
10405
+ }
10406
+ return { home, bundle };
10407
+ }
10408
+ async function currentInstallation() {
10409
+ const location2 = standaloneLocation();
10410
+ if (!location2) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
10411
+ const { home, bundle } = location2;
10412
+ const record = await readInstallation(home);
10413
+ return { home, bundle, record };
10414
+ }
10415
+ function automaticUpdatesAllowed(record, env = process.env) {
10416
+ return record.updates?.enabled === true && !record.updates.pinnedVersion && !record.updates.mirror && !env.CI && env.MASON_NO_AUTO_UPDATE !== "1" && !env.MASON_RELEASE_BASE && !env.MASON_VERSION;
10417
+ }
10418
+ function newerStableVersion(candidate, installed) {
10419
+ if (!/^\d+\.\d+\.\d+$/.test(candidate)) return false;
10420
+ const a = candidate.split(".").map(BigInt), b = installed.split("-")[0].split(".").map(BigInt);
10421
+ for (let i = 0; i < 3; i++) {
10422
+ if (a[i] !== b[i]) return a[i] > b[i];
10423
+ }
10424
+ return installed.includes("-");
10425
+ }
10426
+
10427
+ // src/distribution/bundle.ts
10428
+ init_storage();
10429
+ import fs29 from "fs/promises";
10430
+ import path39 from "path";
10431
+ import { createHash as createHash8 } from "crypto";
10432
+ import { z as z27 } from "zod";
10433
+ var targets = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
10434
+ var digest2 = z27.string().regex(/^[a-f0-9]{64}$/);
10435
+ var bundleSchema = z27.object({
10436
+ format: z27.literal(1),
10437
+ version: z27.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
10438
+ target: z27.enum(targets),
10439
+ nodeVersion: z27.string(),
10440
+ files: z27.record(digest2)
10441
+ }).strict();
10442
+ var sha256 = (bytes) => createHash8("sha256").update(bytes).digest("hex");
10443
+ async function readBundle(root) {
10444
+ const bytes = await fs29.readFile(await storePath(root, "bundle.json"));
10445
+ const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
10446
+ const windows = manifest.target.startsWith("win32-");
10447
+ for (const file of Object.keys(manifest.files)) {
10448
+ if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") || path39.isAbsolute(file)) {
10449
+ throw new Error("Unsafe path in Mason bundle: " + file);
10450
+ }
10451
+ }
10452
+ for (const required of [windows ? "node.exe" : "node", "app/package.json", "app/dist/mason.js", "app/dist/mason-auto.js", "app/dist/mason-mcp.js"]) {
10453
+ if (!manifest.files[required]) throw new Error("Incomplete Mason bundle: " + required);
10454
+ }
10455
+ return { manifest, manifestHash: sha256(bytes) };
10456
+ }
10457
+ async function verifyBundle(root, expectedHash) {
10458
+ const bundle = await readBundle(root);
10459
+ if (expectedHash && bundle.manifestHash !== expectedHash) throw new Error("Mason bundle manifest changed.");
10460
+ if (bundle.manifest.target !== `${process.platform}-${process.arch}`) throw new Error("Mason bundle is for another platform. Install this platform's bundle and rerun setup.");
10461
+ const files = Object.keys(bundle.manifest.files);
10462
+ for (let i = 0; i < files.length; i += 8) await Promise.all(files.slice(i, i + 8).map(async (file) => {
10463
+ if (sha256(await fs29.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
10464
+ }));
10465
+ return bundle;
10466
+ }
10467
+
10468
+ // src/distribution/runtime.ts
10469
+ init_store();
10470
+ init_storage();
10471
+ import fs30 from "fs/promises";
10472
+ import { unlinkSync } from "fs";
10473
+ import os6 from "os";
10474
+ import path40 from "path";
10475
+ import { randomUUID as randomUUID8 } from "crypto";
10476
+ import { z as z28 } from "zod";
10477
+ var leaseSchema = z28.object({ pid: z28.number().int().positive(), host: z28.string(), bundle: z28.string().regex(/^[a-f0-9]{24}$/) });
10478
+
10479
+ // src/distribution/install.ts
10480
+ import fs32 from "fs/promises";
10481
+ import os8 from "os";
10482
+ import path41 from "path";
10483
+ import { execFile as execFile13, spawn as spawn2 } from "child_process";
10484
+ import { promisify as promisify13 } from "util";
10485
+ import { randomUUID as randomUUID10 } from "crypto";
10486
+ init_storage();
10487
+ init_store();
10488
+
10489
+ // src/distribution/sessions.ts
10490
+ init_storage();
10491
+ import fs31 from "fs/promises";
10492
+ import os7 from "os";
10493
+ import { randomUUID as randomUUID9 } from "crypto";
10494
+ import { z as z29 } from "zod";
10495
+ var leaseSchema2 = z29.object({ pid: z29.number().int().positive(), host: z29.string() });
10496
+ async function hasRunningMcp(home) {
10497
+ const directory = await storePath(home, "sessions");
10498
+ const files = await fs31.readdir(directory).catch((error) => {
10499
+ if (error.code === "ENOENT") return [];
10500
+ throw error;
10501
+ });
10502
+ for (const name of files) {
10503
+ if (!/^[a-f0-9-]+\.json$/.test(name)) return true;
10504
+ const relative = "sessions/" + name;
10505
+ try {
10506
+ const raw = await readStoreJson(home, relative);
10507
+ if (raw === null) continue;
10508
+ const lease = leaseSchema2.parse(raw);
10509
+ if (lease.host !== os7.hostname()) return true;
10510
+ try {
10511
+ process.kill(lease.pid, 0);
10512
+ return true;
10513
+ } catch (error) {
10514
+ if (error.code !== "ESRCH") return true;
10515
+ }
10516
+ await fs31.rm(await storePath(home, relative), { force: true });
10517
+ } catch {
10518
+ return true;
10519
+ }
10520
+ }
10521
+ return false;
10522
+ }
10523
+ async function registerMcp(home) {
10524
+ const name = `sessions/${randomUUID9()}.json`;
10525
+ await writeStoreJson(home, name, { pid: process.pid, host: os7.hostname() });
10526
+ return async () => {
10527
+ await fs31.rm(await storePath(home, name), { force: true }).catch(() => {
10528
+ });
10529
+ };
10530
+ }
10531
+
10532
+ // src/distribution/install.ts
10533
+ var shQuote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
10534
+ var psQuote = (s) => "'" + s.replace(/'/g, "''") + "'";
10535
+ var WINDOWS_BATCH_LAUNCHER = '@echo off\r\ngoto #_mason_handoff_# 2>NUL || title %COMSPEC% & powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0mason-launcher.ps1" %*\r\n';
10536
+ var launcherNames = ["mason", "mason.cmd", "mason.ps1", "mason-launcher.ps1"];
10537
+ async function replace(file, content2, mode = 384) {
10538
+ const temp = file + ".tmp-" + randomUUID10();
10539
+ try {
10540
+ await fs32.writeFile(temp, content2, { mode, flag: "wx" });
10541
+ await fs32.rename(temp, file);
10542
+ } finally {
10543
+ await fs32.rm(temp, { force: true });
10544
+ }
10545
+ }
10546
+ function assertUpdatePolicy(record, version, revision) {
10547
+ if (!record || !automaticUpdatesAllowed(record, { ...process.env, MASON_VERSION: "" }) || record.updates?.revision !== revision || !newerStableVersion(version, record.version)) {
10548
+ throw new Error("Update policy or installed version changed; download was not activated.");
10549
+ }
10550
+ }
10551
+ async function installLocked2(home, source, bundle, progress, options, alreadyStaged = false) {
10552
+ const raw = await readStoreJson(home, "install.json");
10553
+ const previous = raw === null ? null : recordSchema3.parse(raw);
10554
+ if (options.stageOnly) assertUpdatePolicy(previous, bundle.manifest.version, options.policyRevision);
10555
+ if (!previous) for (const file of await fs32.readdir(home)) {
10556
+ if (file === ".install-lock") continue;
10557
+ const directory = path41.join(home, file);
10558
+ const stat = await fs32.lstat(directory);
10559
+ if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs32.readdir(directory)).length) {
10560
+ throw new Error("Mason installation directory is not empty and has no ownership receipt.");
10561
+ }
10562
+ }
10563
+ const selectedBin = path41.resolve(process.env.MASON_BIN_DIR ?? previous?.bin ?? (process.platform === "win32" ? path41.join(home, "bin") : path41.join(os8.homedir(), ".local/bin")));
10564
+ await fs32.mkdir(selectedBin, { recursive: true });
10565
+ const bin = await fs32.realpath(selectedBin);
10566
+ if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
10567
+ const id = bundle.manifestHash.slice(0, 24);
10568
+ const destination = await storePath(home, "versions/" + id);
10569
+ if (options.stageOnly) {
10570
+ const receipt2 = { ...previous, pending: { id, version: bundle.manifest.version, policyRevision: options.policyRevision } };
10571
+ await writeStoreJson(home, "install.json", receipt2);
10572
+ return { home, bin, version: bundle.manifest.version, previousVersion: previous.version, path: { message: "Update staged for a future MCP launch." } };
10573
+ }
10574
+ const launchers = process.platform === "win32" ? {
10575
+ "mason.cmd": WINDOWS_BATCH_LAUNCHER,
10576
+ "mason-launcher.ps1": `$ErrorActionPreference = 'Stop'
10577
+ $previousToken = $env:MASON_UNINSTALL_TOKEN
10578
+ $token = [Guid]::NewGuid().ToString()
10579
+ $env:MASON_UNINSTALL_TOKEN = $token
10580
+ try {
10581
+ & ${psQuote(path41.join(destination, "node.exe"))} ${psQuote(path41.join(destination, "app/dist/mason.js"))} @args
10582
+ $code = $LASTEXITCODE
10583
+ if ($code -eq 0 -and $args.Count -eq 1 -and $args[0] -eq 'uninstall') {
10584
+ if ([IO.File]::ReadAllText(${psQuote(path41.join(home, ".uninstall-request.txt"))}) -cne $token) { throw 'Mason did not authorize installation cleanup.' }
10585
+ Remove-Item -LiteralPath ${psQuote(home)} -Recurse -Force
10586
+ Write-Output ${psQuote("Removed standalone installation: " + home)}
10587
+ }
10588
+ exit $code
10589
+ } catch { [Console]::Error.WriteLine($_.Exception.Message); exit 2 }
10590
+ finally { $env:MASON_UNINSTALL_TOKEN = $previousToken }
10591
+ `
10592
+ } : { mason: `#!/bin/sh
10593
+ exec ${shQuote(path41.join(destination, "node"))} ${shQuote(path41.join(destination, "app/dist/mason.js"))} "$@"
10594
+ ` };
10595
+ for (const [name, content2] of Object.entries(launchers)) {
10596
+ const file = await storePath(bin, name);
10597
+ const old = await fs32.readFile(file, "utf8").catch((error) => {
10598
+ if (error.code === "ENOENT") return null;
10599
+ throw error;
10600
+ });
10601
+ if (old !== null && old !== previous?.launchers[name] && old !== previous?.previousLaunchers?.[name] && old !== content2) throw new Error("Refusing to replace an unrelated or edited launcher: " + file);
10602
+ }
10603
+ const retired = Object.entries({ ...previous?.previousLaunchers, ...previous?.launchers }).filter(([name]) => !(name in launchers));
10604
+ const checkRetired = async (name, expected) => {
10605
+ if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
10606
+ const actual = await fs32.readFile(await storePath(bin, name), "utf8").catch((error) => {
10607
+ if (error.code === "ENOENT") return null;
10608
+ throw error;
10609
+ });
10610
+ if (actual !== null && actual !== expected) throw new Error("Retired launcher was edited; retained: " + name);
10611
+ };
10612
+ for (const [name, expected] of retired) await checkRetired(name, expected);
10613
+ progress?.step(`Installing Mason ${bundle.manifest.version}`);
10614
+ const updates = options.preservePolicy && previous?.updates ? { ...previous.updates } : {
10615
+ ...previous?.updates ?? { enabled: !previous && !process.env.CI && process.env.MASON_NO_AUTO_UPDATE !== "1" },
10616
+ revision: randomUUID10(),
10617
+ pinnedVersion: process.env.MASON_VERSION?.replace(/^v/, "") || void 0,
10618
+ mirror: process.env.MASON_RELEASE_BASE || previous?.updates?.mirror
10619
+ };
10620
+ if (options.rollback) {
10621
+ updates.pinnedVersion = bundle.manifest.version;
10622
+ updates.revision = randomUUID10();
10623
+ }
10624
+ const receipt = {
10625
+ format: 1,
10626
+ home,
10627
+ bin,
10628
+ current: id,
10629
+ version: bundle.manifest.version,
10630
+ launchers,
10631
+ pathChanges: previous?.pathChanges ?? [],
10632
+ updates,
10633
+ previous: previous && previous.current !== id ? { id: previous.current, version: previous.version } : previous?.previous
10634
+ };
10635
+ await writeStoreJson(home, "install.json", previous ? { ...previous, previousLaunchers: launchers } : receipt);
10636
+ try {
10637
+ if (!alreadyStaged) await copyBundle(home, source, destination, bundle);
10638
+ for (const [name, content2] of Object.entries(launchers)) await replace(await storePath(bin, name, true), content2, 493);
10639
+ for (const [name, expected] of retired) {
10640
+ await checkRetired(name, expected);
10641
+ await fs32.rm(await storePath(bin, name), { force: true });
10642
+ }
10643
+ await writeStoreJson(home, "install.json", receipt);
10644
+ } catch (error) {
10645
+ if (previous) {
10646
+ for (const [name, content2] of Object.entries(previous.launchers)) await replace(await storePath(bin, name, true), content2, 493);
10647
+ await writeStoreJson(home, "install.json", previous);
10648
+ }
10649
+ throw error;
10650
+ }
10651
+ if (options.preservePolicy) return { home, bin, version: receipt.version, previousVersion: previous?.version, path: { message: "Installation selected." } };
10652
+ progress?.step("Checking terminal command availability");
10653
+ const pathResult = await configurePath(bin, receipt.pathChanges ?? [], async (changes) => {
10654
+ receipt.pathChanges = changes;
10655
+ await writeStoreJson(home, "install.json", receipt);
10656
+ });
10657
+ return { home, bin, version: bundle.manifest.version, previousVersion: previous?.version, path: pathResult };
10658
+ }
10659
+ async function copyBundle(home, source, destination, bundle) {
10660
+ const exists = await fs32.lstat(destination).then(() => true, (error) => {
10661
+ if (error.code === "ENOENT") return false;
10662
+ throw error;
10663
+ });
10664
+ if (exists) {
10665
+ await verifyBundle(destination, bundle.manifestHash);
10666
+ return;
10667
+ }
10668
+ const stage = await storePath(home, "versions/.install-" + randomUUID10(), true);
10669
+ await fs32.mkdir(stage);
10670
+ try {
10671
+ for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
10672
+ const from = await storePath(source, file), to = await storePath(stage, file, true);
10673
+ await fs32.copyFile(from, to);
10674
+ await fs32.chmod(to, (await fs32.stat(from)).mode & 511);
10675
+ }
10676
+ await verifyBundle(stage, bundle.manifestHash);
10677
+ try {
10678
+ await fs32.rename(stage, destination);
10679
+ } catch (error) {
10680
+ try {
10681
+ await verifyBundle(destination, bundle.manifestHash);
10682
+ } catch {
10683
+ throw error;
10684
+ }
10685
+ }
10686
+ } finally {
10687
+ await fs32.rm(stage, { recursive: true, force: true });
10688
+ }
10689
+ }
10690
+ async function prepareStandaloneMcp(home) {
10691
+ const before = await readInstallation(home), candidate = before.pending;
10692
+ let verified, verificationError;
10693
+ if (candidate && automaticUpdatesAllowed(before) && !await hasRunningMcp(home)) {
10694
+ try {
10695
+ verified = await verifyBundle(await storePath(home, "versions/" + candidate.id));
10696
+ if (verified.manifestHash.slice(0, 24) !== candidate.id || verified.manifest.version !== candidate.version) throw new Error("Staged update changed.");
10697
+ } catch (error) {
10698
+ verificationError = error;
10699
+ }
10700
+ }
10701
+ return withLock(home, ".install-lock", async () => {
10702
+ let record = await readInstallation(home);
10703
+ const pending = record.pending;
10704
+ if (pending && candidate?.id === pending.id && (verified || verificationError) && automaticUpdatesAllowed(record) && pending.policyRevision === record.updates?.revision && newerStableVersion(pending.version, record.version) && !await hasRunningMcp(home)) {
10705
+ try {
10706
+ if (verificationError) throw verificationError;
10707
+ const source = await storePath(home, "versions/" + pending.id);
10708
+ await installLocked2(home, source, verified, void 0, { preservePolicy: true }, true);
10709
+ record = await readInstallation(home);
10710
+ } catch (error) {
10711
+ record = await readInstallation(home);
10712
+ delete record.pending;
10713
+ await writeStoreJson(home, "install.json", record);
10714
+ await writeStoreJson(home, "update-status.json", { lastAttempt: Date.now(), error: String(error) });
10715
+ }
10716
+ }
10717
+ const release = await registerMcp(home);
10718
+ return { bundle: await storePath(home, "versions/" + record.current), release };
10719
+ });
10720
+ }
10721
+
10722
+ // src/distribution/updates.ts
10723
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
10724
+ var RELEASE_AGE_MS = 24 * 60 * 60 * 1e3;
10725
+ var releaseListSchema = z30.array(z30.object({ tag_name: z30.string(), draft: z30.boolean(), prerelease: z30.boolean(), published_at: z30.string().nullable() }));
10726
+ var updateManifestSchema = z30.object({
10727
+ format: z30.literal(1),
10728
+ version: z30.string().regex(/^\d+\.\d+\.\d+$/),
10729
+ publishedAt: z30.string().datetime(),
10730
+ assets: z30.record(z30.string().regex(/^[a-f0-9]{64}$/))
10731
+ }).strict();
10732
+ var statusSchema2 = z30.object({ lastAttempt: z30.number(), availableVersion: z30.string().optional(), error: z30.string().optional() });
10733
+ async function scheduleAutomaticUpdate() {
10734
+ try {
10735
+ const { home, record } = await currentInstallation();
10736
+ if (!automaticUpdatesAllowed(record)) return;
10737
+ const status = statusSchema2.safeParse(await readStoreJson(home, "update-status.json"));
10738
+ const now = Date.now();
10739
+ if (status.success && status.data.lastAttempt <= now && now - status.data.lastAttempt < CHECK_INTERVAL_MS) return;
10740
+ const bundle = await storePath(home, "versions/" + record.current);
10741
+ const child = spawn3(
10742
+ path42.join(bundle, process.platform === "win32" ? "node.exe" : "node"),
10743
+ [path42.join(bundle, "app/dist/mason.js"), "internal-update"],
10744
+ { cwd: home, env: process.env, stdio: "ignore", detached: true, windowsHide: true }
10745
+ );
10746
+ child.on("error", () => {
10747
+ });
10748
+ child.unref();
10749
+ } catch {
10750
+ }
10751
+ }
10752
+ async function startMcpWithUpdates(start) {
10753
+ const current = standaloneLocation();
10754
+ if (!current) {
10755
+ await start();
10756
+ return;
10757
+ }
10758
+ const session = await prepareStandaloneMcp(current.home);
10759
+ if (session.bundle !== current.bundle) {
10760
+ const child = spawn3(
10761
+ path42.join(session.bundle, process.platform === "win32" ? "node.exe" : "node"),
10762
+ [path42.join(session.bundle, "app/dist/mason-mcp.js"), ...process.argv.slice(2)],
10763
+ { stdio: "inherit", windowsHide: true }
10764
+ );
10765
+ const interrupt = () => {
10766
+ child.kill("SIGINT");
10767
+ }, terminate = () => {
10768
+ child.kill("SIGTERM");
10769
+ };
10770
+ process.on("SIGINT", interrupt);
10771
+ process.on("SIGTERM", terminate);
10772
+ try {
10773
+ process.exitCode = await new Promise((resolve2, reject) => {
10774
+ child.on("error", reject);
10775
+ child.on("exit", (code) => resolve2(code ?? 1));
10776
+ });
10777
+ } finally {
10778
+ process.off("SIGINT", interrupt);
10779
+ process.off("SIGTERM", terminate);
10780
+ await session.release();
10781
+ }
10782
+ return;
10783
+ }
10784
+ const timer = setInterval(() => {
10785
+ void scheduleAutomaticUpdate();
10786
+ }, 60 * 60 * 1e3);
10787
+ timer.unref();
10788
+ const stop = () => {
10789
+ clearInterval(timer);
10790
+ void session?.release();
10791
+ };
10792
+ try {
10793
+ await start(stop);
10794
+ void scheduleAutomaticUpdate();
10795
+ } catch (error) {
10796
+ stop();
10797
+ throw error;
10798
+ }
10799
+ }
10800
+
9979
10801
  // bin/mason-mcp.ts
9980
- startMcpServer().catch((err) => {
10802
+ startMcpWithUpdates(startMcpServer).catch((err) => {
9981
10803
  process.stderr.write(`Mason MCP server error: ${err}
9982
10804
  `);
9983
10805
  process.exit(1);