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.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() {
@@ -95,6 +96,26 @@ var init_git_read = __esm({
95
96
  }
96
97
  });
97
98
 
99
+ // src/audit/types.ts
100
+ var types_exports = {};
101
+ __export(types_exports, {
102
+ ALL_CHECKS: () => ALL_CHECKS
103
+ });
104
+ var ALL_CHECKS;
105
+ var init_types = __esm({
106
+ "src/audit/types.ts"() {
107
+ "use strict";
108
+ ALL_CHECKS = [
109
+ "deleted-reference",
110
+ "new-module",
111
+ "stale-count",
112
+ "dead-command",
113
+ "deps-changed",
114
+ "decision-anchor-drift"
115
+ ];
116
+ }
117
+ });
118
+
98
119
  // src/utils/files.ts
99
120
  import fs from "fs/promises";
100
121
  import { constants } from "fs";
@@ -139,6 +160,27 @@ async function loadProjectConfig(root) {
139
160
  }
140
161
  config[key] = value[key];
141
162
  }
163
+ if (value.audit !== void 0) {
164
+ const audit = value.audit;
165
+ if (!audit || typeof audit !== "object" || Array.isArray(audit)) throw new Error("Configuration audit must be an object");
166
+ const patterns = (raw2) => {
167
+ if (!Array.isArray(raw2) || raw2.length > 100 || !raw2.every((s) => typeof s === "string" && normalizeRepoPath(s) === s && !s.startsWith("!") && !/[\x00-\x1f]/.test(s))) {
168
+ throw new Error("Audit patterns must be at most 100 repository-relative globs without traversal or negation");
169
+ }
170
+ return raw2;
171
+ };
172
+ config.audit = {};
173
+ if (audit.include !== void 0) config.audit.include = patterns(audit.include);
174
+ if (audit.exclude !== void 0) {
175
+ if (!audit.exclude || typeof audit.exclude !== "object" || Array.isArray(audit.exclude)) throw new Error("audit.exclude must map check names to patterns");
176
+ const { ALL_CHECKS: ALL_CHECKS2 } = await Promise.resolve().then(() => (init_types(), types_exports));
177
+ config.audit.exclude = {};
178
+ for (const [check, globs] of Object.entries(audit.exclude)) {
179
+ if (!ALL_CHECKS2.includes(check)) throw new Error("Unknown audit check: " + check);
180
+ config.audit.exclude[check] = patterns(globs);
181
+ }
182
+ }
183
+ }
142
184
  return config;
143
185
  } catch (error) {
144
186
  if (error.code === "ENOENT") return {};
@@ -438,8 +480,190 @@ var init_store = __esm({
438
480
  }
439
481
  });
440
482
 
441
- // src/audit/commands.ts
483
+ // src/audit/inspection.ts
484
+ import fg2 from "fast-glob";
485
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
442
486
  import path4 from "path";
487
+ function withRepositoryInspection(root, run) {
488
+ return inspections.run({ root: path4.resolve(root), reads: /* @__PURE__ */ new Map() }, run);
489
+ }
490
+ function inspectionRead(root, key, read) {
491
+ const inspection = inspections.getStore();
492
+ if (!inspection || inspection.root !== path4.resolve(root)) return read();
493
+ if (!inspection.reads.has(key)) {
494
+ const pending = read();
495
+ inspection.reads.set(key, pending);
496
+ void pending.catch(() => {
497
+ if (inspection.reads.get(key) === pending) inspection.reads.delete(key);
498
+ });
499
+ }
500
+ return inspection.reads.get(key);
501
+ }
502
+ function inspectionGit(args2, options) {
503
+ return inspectionRead(options.cwd, "git:" + JSON.stringify([args2, options]), () => execGit(args2, options));
504
+ }
505
+ async function auditGitPaths(root, kind) {
506
+ const custom = (await loadProjectConfig(root)).audit?.include ?? [];
507
+ const shared = inspections.getStore()?.root === path4.resolve(root) && !custom.length;
508
+ const expanded = fg2.generateTasks(custom).flatMap((task) => task.positive);
509
+ if (expanded.length > 1e3) throw new Error("Audit include patterns expand to more than 1,000 globs");
510
+ const documentSpecs = [...docSpecs, ...expanded.map((pattern) => `:(glob)${pattern}`)];
511
+ const specs = shared ? [...documentSpecs, ...sourceSpecs] : kind === "documents" ? documentSpecs : sourceSpecs;
512
+ const limit = kind === "documents" ? 16 * 1024 * 1024 : 32 * 1024 * 1024;
513
+ const { stdout } = await inspectionGit(
514
+ ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...specs],
515
+ { cwd: root, maxBuffer: shared ? 48 * 1024 * 1024 : limit, timeout: 1e4 }
516
+ );
517
+ 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("."))));
518
+ if (Buffer.byteLength(paths.join("\0")) > limit) throw new Error("Git " + kind + " inventory exceeds its bounded read limit.");
519
+ return paths.sort();
520
+ }
521
+ var inspections, sourceSpecs, docSpecs, sourceSuffixes;
522
+ var init_inspection = __esm({
523
+ "src/audit/inspection.ts"() {
524
+ "use strict";
525
+ init_git_read();
526
+ init_files();
527
+ inspections = new AsyncLocalStorage2();
528
+ sourceSpecs = SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`);
529
+ docSpecs = [":(icase,glob)**/readme.md", ":(icase,glob)**/agents.md", ":(icase,glob)**/claude.md"];
530
+ sourceSuffixes = new Set(SOURCE_EXTENSIONS.map((extension) => "." + extension));
531
+ }
532
+ });
533
+
534
+ // src/audit/inputs.ts
535
+ import fs4 from "fs/promises";
536
+ import * as nativeFs from "fs";
537
+ import path5 from "path";
538
+ import fg3 from "fast-glob";
539
+ async function auditInputPath(root, file) {
540
+ if (file === ".") return fs4.realpath(root);
541
+ try {
542
+ return await storePath(root, file);
543
+ } catch (error) {
544
+ if (error instanceof Error && error.message.startsWith("Symlink")) {
545
+ throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
546
+ }
547
+ throw error;
548
+ }
549
+ }
550
+ async function readAuditInput(root, file) {
551
+ try {
552
+ const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
553
+ if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
554
+ return value;
555
+ } catch (error) {
556
+ if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return null;
557
+ throw error;
558
+ }
559
+ }
560
+ async function auditGlob(root, patterns, options) {
561
+ if (Array.isArray(patterns) && !patterns.length) return [];
562
+ const directories = /* @__PURE__ */ new Set();
563
+ const readdir2 = ((directory, opts, callback) => {
564
+ const relative = path5.relative(root, directory).split(path5.sep).join("/") || ".";
565
+ directories.add(relative);
566
+ if (directories.size > MAX_INPUTS) {
567
+ callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
568
+ return;
569
+ }
570
+ auditInputPath(root, relative).then(() => {
571
+ nativeFs.readdir(directory, opts, callback);
572
+ }, callback);
573
+ });
574
+ const lstat2 = ((file, callback) => {
575
+ const relative = path5.relative(root, file).split(path5.sep).join("/") || ".";
576
+ auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
577
+ });
578
+ const stream = fg3.stream(patterns, {
579
+ cwd: root,
580
+ ignore: options.ignore,
581
+ onlyFiles: false,
582
+ onlyDirectories: options.onlyDirectories,
583
+ objectMode: true,
584
+ followSymbolicLinks: options.followSymbolicLinks ?? true,
585
+ fs: { readdir: readdir2, lstat: lstat2 },
586
+ concurrency: 16
587
+ });
588
+ const matches = [];
589
+ try {
590
+ for await (const value of stream) {
591
+ const entry = value;
592
+ const file = entry.path;
593
+ if (options.select && !options.select(file)) continue;
594
+ await auditInputPath(root, file);
595
+ if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
596
+ matches.push(file);
597
+ if (matches.length > MAX_INPUTS) {
598
+ const largest = [...matches.reduce((counts, item) => {
599
+ const dir = item.split("/")[0];
600
+ counts.set(dir, (counts.get(dir) ?? 0) + 1);
601
+ return counts;
602
+ }, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
603
+ 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.`);
604
+ }
605
+ }
606
+ } finally {
607
+ stream.destroy();
608
+ }
609
+ return matches.sort();
610
+ }
611
+ async function gitSourcePaths(root) {
612
+ return auditGitPaths(root, "sources");
613
+ }
614
+ async function gitIgnoredPaths(root, files) {
615
+ if (!files.length) return /* @__PURE__ */ new Set();
616
+ const ignored = /* @__PURE__ */ new Set();
617
+ for (let offset = 0; offset < files.length; offset += 1e3) {
618
+ try {
619
+ const { stdout } = await execGit(["check-ignore", "-z", "--stdin"], {
620
+ cwd: root,
621
+ maxBuffer: 4 * 1024 * 1024,
622
+ timeout: 1e4,
623
+ input: files.slice(offset, offset + 1e3).join("\0") + "\0"
624
+ });
625
+ for (const file of stdout.split("\0").filter(Boolean)) ignored.add(file);
626
+ } catch (error) {
627
+ if (error.code !== 1) throw error;
628
+ }
629
+ }
630
+ return ignored;
631
+ }
632
+ var MAX_INPUTS;
633
+ var init_inputs = __esm({
634
+ "src/audit/inputs.ts"() {
635
+ "use strict";
636
+ init_files();
637
+ init_storage();
638
+ init_git_read();
639
+ init_inspection();
640
+ MAX_INPUTS = 1e5;
641
+ }
642
+ });
643
+
644
+ // src/audit/policy.ts
645
+ import fg4 from "fast-glob";
646
+ async function includedCheckPaths(root, paths, exclude) {
647
+ if (!exclude.length) return new Set(paths);
648
+ const included = /* @__PURE__ */ new Set();
649
+ for (let offset = 0; offset < paths.length; offset += 256) {
650
+ for (const file of await auditGlob(root, paths.slice(offset, offset + 256).map((file2) => fg4.escapePath(file2)), {
651
+ ignore: exclude,
652
+ followSymbolicLinks: false,
653
+ label: "Audit check exclusions"
654
+ })) included.add(file);
655
+ }
656
+ return included;
657
+ }
658
+ var init_policy = __esm({
659
+ "src/audit/policy.ts"() {
660
+ "use strict";
661
+ init_inputs();
662
+ }
663
+ });
664
+
665
+ // src/audit/commands.ts
666
+ import path6 from "path";
443
667
  function splitCommands(line) {
444
668
  const parts = [];
445
669
  let quote2 = "", start = 0;
@@ -475,7 +699,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
475
699
  const cd = text2.match(/^cd\s+(.+)$/);
476
700
  if (cd) {
477
701
  const directory = literal(cd[1]);
478
- cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null : path4.posix.join(cwd ?? ".", directory);
702
+ cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null : path6.posix.join(cwd ?? ".", directory);
479
703
  continue;
480
704
  }
481
705
  for (const match of text2.matchAll(COMMAND)) {
@@ -486,7 +710,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
486
710
  if (shell && match.index !== 0 || match[3].startsWith("-")) scopeUnknown = "Unsupported shell wrapper or options before the script.";
487
711
  if (selector) {
488
712
  const selected = literal(selector[1]);
489
- directory = selected === null || cwd === null ? null : path4.posix.join(cwd ?? ".", selected);
713
+ directory = selected === null || cwd === null ? null : path6.posix.join(cwd ?? ".", selected);
490
714
  } else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
491
715
  const tail = text2.slice(match.index + match[0].length).split(/\s--(?:\s|$)/)[0];
492
716
  if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(tail)) {
@@ -516,7 +740,7 @@ var init_commands = __esm({
516
740
  COMMAND = new RegExp(`\\b(npm|pnpm|yarn)((?:[ \\t]+${TOKEN})*?)[ \\t]+run[ \\t]+([A-Za-z0-9:_.-]+)`, "g");
517
741
  literal = (value) => {
518
742
  const unquoted = value.replace(/^(["'])(.*)\1$/, "$2");
519
- return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path4.posix.isAbsolute(unquoted) ? null : unquoted;
743
+ return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path6.posix.isAbsolute(unquoted) ? null : unquoted;
520
744
  };
521
745
  }
522
746
  });
@@ -821,52 +1045,6 @@ var init_claims = __esm({
821
1045
  }
822
1046
  });
823
1047
 
824
- // src/audit/inspection.ts
825
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
826
- import path5 from "path";
827
- function withRepositoryInspection(root, run) {
828
- return inspections.run({ root: path5.resolve(root), reads: /* @__PURE__ */ new Map() }, run);
829
- }
830
- function inspectionRead(root, key, read) {
831
- const inspection = inspections.getStore();
832
- if (!inspection || inspection.root !== path5.resolve(root)) return read();
833
- if (!inspection.reads.has(key)) {
834
- const pending = read();
835
- inspection.reads.set(key, pending);
836
- void pending.catch(() => {
837
- if (inspection.reads.get(key) === pending) inspection.reads.delete(key);
838
- });
839
- }
840
- return inspection.reads.get(key);
841
- }
842
- function inspectionGit(args2, options) {
843
- return inspectionRead(options.cwd, "git:" + JSON.stringify([args2, options]), () => execGit(args2, options));
844
- }
845
- async function auditGitPaths(root, kind) {
846
- const shared = inspections.getStore()?.root === path5.resolve(root);
847
- const specs = shared ? [...docSpecs, ...sourceSpecs] : kind === "documents" ? docSpecs : sourceSpecs;
848
- const limit = kind === "documents" ? 16 * 1024 * 1024 : 32 * 1024 * 1024;
849
- const { stdout } = await inspectionGit(
850
- ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...specs],
851
- { cwd: root, maxBuffer: shared ? 48 * 1024 * 1024 : limit, timeout: 1e4 }
852
- );
853
- const paths = [...new Set(stdout.split("\0").filter(Boolean))].filter((file) => kind === "documents" ? /^(readme|agents|claude)\.md$/i.test(path5.posix.basename(file)) : sourceSuffixes.has(file.slice(file.lastIndexOf("."))));
854
- if (Buffer.byteLength(paths.join("\0")) > limit) throw new Error("Git " + kind + " inventory exceeds its bounded read limit.");
855
- return paths.sort();
856
- }
857
- var inspections, sourceSpecs, docSpecs, sourceSuffixes;
858
- var init_inspection = __esm({
859
- "src/audit/inspection.ts"() {
860
- "use strict";
861
- init_git_read();
862
- init_files();
863
- inspections = new AsyncLocalStorage2();
864
- sourceSpecs = SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`);
865
- docSpecs = [":(icase,glob)**/readme.md", ":(icase,glob)**/agents.md", ":(icase,glob)**/claude.md"];
866
- sourceSuffixes = new Set(SOURCE_EXTENSIONS.map((extension) => "." + extension));
867
- }
868
- });
869
-
870
1048
  // src/audit/git.ts
871
1049
  function parseCommitLine(line) {
872
1050
  const parts = line.split(" ");
@@ -952,136 +1130,26 @@ var init_git = __esm({
952
1130
  }
953
1131
  });
954
1132
 
955
- // src/audit/inputs.ts
956
- import fs4 from "fs/promises";
957
- import * as nativeFs from "fs";
958
- import path6 from "path";
959
- import fg2 from "fast-glob";
960
- async function auditInputPath(root, file) {
961
- if (file === ".") return fs4.realpath(root);
1133
+ // src/audit/docs.ts
1134
+ import fs5 from "fs/promises";
1135
+ import path7 from "path";
1136
+ import fg5 from "fast-glob";
1137
+ async function dirtyDocs(resolvedRoot, files) {
1138
+ if (!files.length) return /* @__PURE__ */ new Set();
962
1139
  try {
963
- return await storePath(root, file);
964
- } catch (error) {
965
- if (error instanceof Error && error.message.startsWith("Symlink")) {
966
- throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
967
- }
968
- throw error;
969
- }
970
- }
971
- async function readAuditInput(root, file) {
972
- try {
973
- const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
974
- if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
975
- return value;
976
- } catch (error) {
977
- if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return null;
978
- throw error;
979
- }
980
- }
981
- async function auditGlob(root, patterns, options) {
982
- if (Array.isArray(patterns) && !patterns.length) return [];
983
- const directories = /* @__PURE__ */ new Set();
984
- const readdir2 = ((directory, opts, callback) => {
985
- const relative = path6.relative(root, directory).split(path6.sep).join("/") || ".";
986
- directories.add(relative);
987
- if (directories.size > MAX_INPUTS) {
988
- callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
989
- return;
990
- }
991
- auditInputPath(root, relative).then(() => {
992
- nativeFs.readdir(directory, opts, callback);
993
- }, callback);
994
- });
995
- const lstat2 = ((file, callback) => {
996
- const relative = path6.relative(root, file).split(path6.sep).join("/") || ".";
997
- auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
998
- });
999
- const stream = fg2.stream(patterns, {
1000
- cwd: root,
1001
- ignore: options.ignore,
1002
- onlyFiles: false,
1003
- onlyDirectories: options.onlyDirectories,
1004
- objectMode: true,
1005
- followSymbolicLinks: options.followSymbolicLinks ?? true,
1006
- fs: { readdir: readdir2, lstat: lstat2 },
1007
- concurrency: 16
1008
- });
1009
- const matches = [];
1010
- try {
1011
- for await (const value of stream) {
1012
- const entry = value;
1013
- const file = entry.path;
1014
- if (options.select && !options.select(file)) continue;
1015
- await auditInputPath(root, file);
1016
- if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
1017
- matches.push(file);
1018
- if (matches.length > MAX_INPUTS) {
1019
- const largest = [...matches.reduce((counts, item) => {
1020
- const dir = item.split("/")[0];
1021
- counts.set(dir, (counts.get(dir) ?? 0) + 1);
1022
- return counts;
1023
- }, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
1024
- 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.`);
1025
- }
1026
- }
1027
- } finally {
1028
- stream.destroy();
1029
- }
1030
- return matches.sort();
1031
- }
1032
- async function gitSourcePaths(root) {
1033
- return auditGitPaths(root, "sources");
1034
- }
1035
- async function gitIgnoredPaths(root, files) {
1036
- if (!files.length) return /* @__PURE__ */ new Set();
1037
- const ignored = /* @__PURE__ */ new Set();
1038
- for (let offset = 0; offset < files.length; offset += 1e3) {
1039
- try {
1040
- const { stdout } = await execGit(["check-ignore", "-z", "--stdin"], {
1041
- cwd: root,
1042
- maxBuffer: 4 * 1024 * 1024,
1043
- timeout: 1e4,
1044
- input: files.slice(offset, offset + 1e3).join("\0") + "\0"
1045
- });
1046
- for (const file of stdout.split("\0").filter(Boolean)) ignored.add(file);
1047
- } catch (error) {
1048
- if (error.code !== 1) throw error;
1049
- }
1050
- }
1051
- return ignored;
1052
- }
1053
- var MAX_INPUTS;
1054
- var init_inputs = __esm({
1055
- "src/audit/inputs.ts"() {
1056
- "use strict";
1057
- init_files();
1058
- init_storage();
1059
- init_git_read();
1060
- init_inspection();
1061
- MAX_INPUTS = 1e5;
1062
- }
1063
- });
1064
-
1065
- // src/audit/docs.ts
1066
- import fs5 from "fs/promises";
1067
- import path7 from "path";
1068
- import fg3 from "fast-glob";
1069
- async function dirtyDocs(resolvedRoot, files) {
1070
- if (!files.length) return /* @__PURE__ */ new Set();
1071
- try {
1072
- const dirty = /* @__PURE__ */ new Set();
1073
- for (let offset = 0; offset < files.length; offset += 256) {
1074
- const { stdout } = await execGit(
1075
- ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...files.slice(offset, offset + 256).map((file) => `:(literal)${file}`)],
1076
- { cwd: resolvedRoot, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 }
1077
- );
1078
- const entries = stdout.split("\0");
1079
- for (let i = 0; i < entries.length; i++) {
1080
- if (!entries[i]) continue;
1081
- const status = entries[i].slice(0, 2);
1082
- dirty.add(entries[i].slice(3));
1083
- if (/[RC]/.test(status) && entries[i + 1]) dirty.add(entries[++i]);
1084
- }
1140
+ const dirty = /* @__PURE__ */ new Set();
1141
+ for (let offset = 0; offset < files.length; offset += 256) {
1142
+ const { stdout } = await execGit(
1143
+ ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...files.slice(offset, offset + 256).map((file) => `:(literal)${file}`)],
1144
+ { cwd: resolvedRoot, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 }
1145
+ );
1146
+ const entries = stdout.split("\0");
1147
+ for (let i = 0; i < entries.length; i++) {
1148
+ if (!entries[i]) continue;
1149
+ const status = entries[i].slice(0, 2);
1150
+ dirty.add(entries[i].slice(3));
1151
+ if (/[RC]/.test(status) && entries[i + 1]) dirty.add(entries[++i]);
1152
+ }
1085
1153
  }
1086
1154
  return dirty;
1087
1155
  } catch {
@@ -1117,7 +1185,7 @@ async function discoverDocPaths(root) {
1117
1185
  const paths = [];
1118
1186
  const unique = [...new Set(candidates)].sort();
1119
1187
  for (let offset = 0; offset < unique.length; offset += 256) {
1120
- paths.push(...await auditGlob(root, unique.slice(offset, offset + 256).map((p) => fg3.escapePath(p)), {
1188
+ paths.push(...await auditGlob(root, unique.slice(offset, offset + 256).map((p) => fg5.escapePath(p)), {
1121
1189
  ignore: excluded,
1122
1190
  followSymbolicLinks: false,
1123
1191
  label: "Documentation discovery"
@@ -1147,6 +1215,7 @@ async function localDocPaths(root) {
1147
1215
  return paths;
1148
1216
  }
1149
1217
  function documentScope(file) {
1218
+ if (!/^(readme|agents|claude)\.md$/i.test(path7.posix.basename(file))) return ".";
1150
1219
  const parent = path7.posix.dirname(file);
1151
1220
  return path7.posix.basename(parent).toLowerCase() === ".claude" ? path7.posix.dirname(parent) : parent;
1152
1221
  }
@@ -1168,7 +1237,7 @@ async function readDocs(resolvedRoot) {
1168
1237
  path: candidate,
1169
1238
  content: content2,
1170
1239
  scope: documentScope(candidate),
1171
- kind: /^readme\.md$/i.test(path7.posix.basename(candidate)) ? "readme" : "instructions",
1240
+ kind: /^readme\.md$/i.test(path7.posix.basename(candidate)) ? "readme" : /^(agents|claude)\.md$/i.test(path7.posix.basename(candidate)) ? "instructions" : "guide",
1172
1241
  lineCount: content2.split("\n").length,
1173
1242
  lastCommit,
1174
1243
  dirty: dirty.has(candidate),
@@ -1437,22 +1506,6 @@ var init_drift = __esm({
1437
1506
  }
1438
1507
  });
1439
1508
 
1440
- // src/audit/types.ts
1441
- var ALL_CHECKS;
1442
- var init_types = __esm({
1443
- "src/audit/types.ts"() {
1444
- "use strict";
1445
- ALL_CHECKS = [
1446
- "deleted-reference",
1447
- "new-module",
1448
- "stale-count",
1449
- "dead-command",
1450
- "deps-changed",
1451
- "decision-anchor-drift"
1452
- ];
1453
- }
1454
- });
1455
-
1456
1509
  // src/audit/checks/deleted-reference.ts
1457
1510
  import path12 from "path";
1458
1511
  async function checkDeletedReferences(ctx) {
@@ -1521,7 +1574,7 @@ var init_deleted_reference = __esm({
1521
1574
  });
1522
1575
 
1523
1576
  // src/audit/checks/new-module.ts
1524
- import fg4 from "fast-glob";
1577
+ import fg6 from "fast-glob";
1525
1578
  import fs8 from "fs/promises";
1526
1579
  import path13 from "path";
1527
1580
  function escapeRegExp(text2) {
@@ -1583,7 +1636,7 @@ async function collectModuleCandidates(root, combinedDocs) {
1583
1636
  const result = /* @__PURE__ */ new Map();
1584
1637
  for (const dir of parents) {
1585
1638
  const visible = new Set(byParent.get(dir).filter((candidate) => !ignored.has(candidate) || sources.some((file) => file.startsWith(candidate + "/"))));
1586
- result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${fg4.escapePath(dir)}/*`, {
1639
+ result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${fg6.escapePath(dir)}/*`, {
1587
1640
  ignore,
1588
1641
  onlyDirectories: true,
1589
1642
  label: "Module directory discovery",
@@ -1596,7 +1649,7 @@ async function collectModuleCandidates(root, combinedDocs) {
1596
1649
  const files = sources.filter((file) => file.startsWith(dir + "/"));
1597
1650
  let count4 = 0;
1598
1651
  for (let offset = 0; offset < files.length; offset += 256) {
1599
- count4 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg4.escapePath(file)), {
1652
+ count4 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg6.escapePath(file)), {
1600
1653
  ignore,
1601
1654
  followSymbolicLinks: false,
1602
1655
  label: `Source discovery in ${dir}`
@@ -1931,6 +1984,87 @@ var init_dead_command = __esm({
1931
1984
  }
1932
1985
  });
1933
1986
 
1987
+ // src/audit/dependency-claims.ts
1988
+ import path16 from "path";
1989
+ import { parse as parseToml } from "smol-toml";
1990
+ function dependencyText(content2) {
1991
+ return content2.replace(/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g, (text2) => text2.replace(/[^\n]/g, " ")).replace(/<!--[\s\S]*?-->/g, (text2) => text2.replace(/[^\n]/g, " "));
1992
+ }
1993
+ function declarations(file, source) {
1994
+ const result = /* @__PURE__ */ new Map();
1995
+ if (!source) return result;
1996
+ if (path16.posix.basename(file) === "package.json") {
1997
+ const json2 = JSON.parse(source);
1998
+ for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies", "engines"]) {
1999
+ for (const [name, value] of Object.entries(json2[section] ?? {})) {
2000
+ if (typeof value === "string") result.set(name, [...result.has(name) ? [result.get(name)] : [], value].join("; "));
2001
+ }
2002
+ }
2003
+ } else if (file.endsWith("libs.versions.toml")) {
2004
+ const catalog = parseToml(source);
2005
+ for (const section of ["libraries", "plugins"]) for (const [alias, entry] of Object.entries(catalog[section] ?? {})) {
2006
+ if (typeof entry === "string") {
2007
+ const parts = entry.split(":");
2008
+ if (parts.length >= 3) result.set(parts.slice(0, -1).join(":"), parts.at(-1));
2009
+ } else if (entry && typeof entry === "object") {
2010
+ const name = entry.module ?? (entry.group && entry.name ? `${entry.group}:${entry.name}` : entry.id);
2011
+ const version = typeof entry.version === "string" ? entry.version : catalog.versions?.[entry.version?.ref];
2012
+ if (typeof name === "string") result.set(name, typeof version === "string" ? version : "unspecified");
2013
+ }
2014
+ }
2015
+ } else if (/build\.gradle(?:\.kts)?$/.test(file)) {
2016
+ for (const match of source.matchAll(/["']([\w.-]+:[\w.-]+):([^"'\s]+)["']/g)) result.set(match[1], match[2]);
2017
+ for (const match of source.matchAll(/\bid\s*\(?\s*["']([\w.-]+)["']\s*\)?\s*version\s*["']([^"']+)["']/g)) result.set(match[1], match[2]);
2018
+ }
2019
+ return result;
2020
+ }
2021
+ async function matchDependencyChanges(root, from, head, files, content2) {
2022
+ const matches = [];
2023
+ const lines = dependencyText(content2).split("\n");
2024
+ let incomplete = files.length > 64;
2025
+ for (const file of files.slice(0, 64)) {
2026
+ if (!/(?:^|\/)(?:package\.json|libs\.versions\.toml|build\.gradle(?:\.kts)?)$/.test(file)) {
2027
+ incomplete = true;
2028
+ continue;
2029
+ }
2030
+ try {
2031
+ const read = async (revision) => {
2032
+ const { stdout: entry } = await inspectionGit(["ls-tree", revision, "--", `:(literal)${file}`], { cwd: root, timeout: 1e4 });
2033
+ if (!entry) return "";
2034
+ if (!entry.startsWith("100644 ") && !entry.startsWith("100755 ")) throw new Error("Unsupported manifest mode");
2035
+ return (await inspectionGit(["show", `${revision}:${file}`], { cwd: root, maxBuffer: 1024 * 1024, timeout: 1e4 })).stdout;
2036
+ };
2037
+ const [before, after] = await Promise.all([read(from), read(head)]);
2038
+ const old = declarations(file, before), next = declarations(file, after);
2039
+ for (const dependency of /* @__PURE__ */ new Set([...old.keys(), ...next.keys()])) {
2040
+ if (old.get(dependency) === next.get(dependency)) continue;
2041
+ const escaped = dependency.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2042
+ const mention = new RegExp(`(^|[^a-zA-Z0-9_@./-])${escaped}(?![a-zA-Z0-9_/-]|\\.[a-zA-Z0-9_])`, "i");
2043
+ const line = lines.findIndex((text2) => mention.test(text2));
2044
+ if (line < 0) continue;
2045
+ matches.push({
2046
+ dependency,
2047
+ manifest: file,
2048
+ before: old.get(dependency) ?? null,
2049
+ after: next.get(dependency) ?? null,
2050
+ line: line + 1,
2051
+ excerpt: lines[line].trim().slice(0, 500)
2052
+ });
2053
+ if (matches.length >= 100) return { matches, incomplete: true };
2054
+ }
2055
+ } catch {
2056
+ incomplete = true;
2057
+ }
2058
+ }
2059
+ return { matches, incomplete };
2060
+ }
2061
+ var init_dependency_claims = __esm({
2062
+ "src/audit/dependency-claims.ts"() {
2063
+ "use strict";
2064
+ init_inspection();
2065
+ }
2066
+ });
2067
+
1934
2068
  // src/audit/release-metadata.ts
1935
2069
  async function git(root, args2) {
1936
2070
  return (await execGit(args2, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
@@ -1987,6 +2121,10 @@ var init_release_metadata = __esm({
1987
2121
  });
1988
2122
 
1989
2123
  // src/audit/checks/deps-changed.ts
2124
+ function hasDependencyContent(content2) {
2125
+ const text2 = content2.replace(/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g, "").replace(/<!--[\s\S]*?-->/g, "");
2126
+ 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);
2127
+ }
1990
2128
  function manifestPathspecs(doc) {
1991
2129
  const scope = documentScope(doc);
1992
2130
  if (scope === ".") return MANIFEST_PATHSPECS;
@@ -2006,12 +2144,17 @@ async function checkDepsChanged(ctx) {
2006
2144
  });
2007
2145
  continue;
2008
2146
  }
2147
+ let content2 = doc.content;
2009
2148
  if (doc.dirty) {
2010
- result.skipped.push({
2011
- check: "deps-changed",
2012
- doc: doc.path,
2013
- reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
2014
- });
2149
+ try {
2150
+ content2 = (await inspectionGit(
2151
+ ["show", `${doc.lastCommit.hash}:${doc.path}`],
2152
+ { cwd: ctx.root, maxBuffer: 10 * 1024 * 1024, timeout: 1e4 }
2153
+ )).stdout;
2154
+ } catch {
2155
+ result.skipped.push({ check: "deps-changed", doc: doc.path, reason: `${doc.path}: committed dependency content is unavailable` });
2156
+ continue;
2157
+ }
2015
2158
  }
2016
2159
  const range = await commitsTouchingSince(
2017
2160
  ctx.root,
@@ -2036,16 +2179,32 @@ async function checkDepsChanged(ctx) {
2036
2179
  range.commits = relevant;
2037
2180
  range.total = relevant.length;
2038
2181
  if (range.total === 0) continue;
2039
- const latest = range.commits[0];
2182
+ const relevance = await matchDependencyChanges(
2183
+ ctx.root,
2184
+ doc.lastCommit.hash,
2185
+ ctx.headHash,
2186
+ [...new Set(range.commits.flatMap((commit) => commit.files))],
2187
+ content2
2188
+ );
2189
+ if (!relevance.matches.length && !hasDependencyContent(content2)) continue;
2190
+ const match = relevance.matches[0];
2191
+ if (doc.dirty && match) result.skipped.push({
2192
+ check: "deps-changed",
2193
+ doc: doc.path,
2194
+ reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
2195
+ });
2040
2196
  (doc.dirty ? result.suppressedAdvisories : result.advisories).push({
2041
2197
  type: "deps-changed",
2042
- 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}")`,
2043
- anchor: { doc: doc.path, line: null, excerpt: null },
2198
+ ...match ? {} : { resolution: "informational" },
2199
+ 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." : ""}`,
2200
+ anchor: { doc: doc.path, line: match?.line ?? null, excerpt: match?.excerpt ?? null },
2044
2201
  evidence: {
2045
2202
  kind: "doc-behind-manifests",
2046
2203
  docLastCommit: doc.lastCommit,
2047
2204
  manifestCommits: range.commits.slice(0, MANIFEST_COMMITS_CAP),
2048
- totalCommits: range.total
2205
+ totalCommits: range.total,
2206
+ matches: relevance.matches,
2207
+ matchingIncomplete: relevance.incomplete
2049
2208
  }
2050
2209
  });
2051
2210
  }
@@ -2055,9 +2214,11 @@ var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
2055
2214
  var init_deps_changed = __esm({
2056
2215
  "src/audit/checks/deps-changed.ts"() {
2057
2216
  "use strict";
2217
+ init_dependency_claims();
2058
2218
  init_docs();
2059
2219
  init_release_metadata();
2060
2220
  init_git();
2221
+ init_inspection();
2061
2222
  init_checks();
2062
2223
  MANIFEST_COMMITS_CAP = 10;
2063
2224
  MANIFEST_PATHSPECS = [
@@ -2078,7 +2239,7 @@ var init_deps_changed = __esm({
2078
2239
  });
2079
2240
 
2080
2241
  // src/context/lexical.ts
2081
- import path16 from "path";
2242
+ import path17 from "path";
2082
2243
  var init_lexical = __esm({
2083
2244
  "src/context/lexical.ts"() {
2084
2245
  "use strict";
@@ -2262,7 +2423,7 @@ var init_provenance = __esm({
2262
2423
 
2263
2424
  // src/decisions/decisions.ts
2264
2425
  import fs9 from "fs/promises";
2265
- import path17 from "path";
2426
+ import path18 from "path";
2266
2427
  import { createHash } from "crypto";
2267
2428
  import { z as z4 } from "zod";
2268
2429
  async function loadDecisionStore(rootDir) {
@@ -2313,9 +2474,9 @@ var init_decisions = __esm({
2313
2474
  });
2314
2475
 
2315
2476
  // src/decisions/drift.ts
2316
- import path18 from "path";
2477
+ import path19 from "path";
2317
2478
  async function computeDecisionDrift(rootDir, decisions) {
2318
- const resolvedRoot = path18.resolve(rootDir);
2479
+ const resolvedRoot = path19.resolve(rootDir);
2319
2480
  const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
2320
2481
  const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
2321
2482
  const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
@@ -2511,7 +2672,9 @@ var init_findings = __esm({
2511
2672
  kind: z5.literal("doc-behind-manifests"),
2512
2673
  docLastCommit: commitSchema,
2513
2674
  manifestCommits: z5.array(commitSchema.extend({ files: z5.array(z5.string()) })),
2514
- totalCommits: count
2675
+ totalCommits: count,
2676
+ matches: z5.array(z5.object({ dependency: z5.string(), manifest: z5.string(), before: z5.string().nullable(), after: z5.string().nullable(), line: z5.number().int().positive(), excerpt: z5.string() })).optional(),
2677
+ matchingIncomplete: z5.boolean().optional()
2515
2678
  }),
2516
2679
  z5.object({
2517
2680
  kind: z5.literal("decision-anchor"),
@@ -2527,7 +2690,7 @@ var init_findings = __esm({
2527
2690
  type: z5.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
2528
2691
  confidence: z5.enum(["certain", "likely"])
2529
2692
  });
2530
- advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z5.literal("recheck").optional() });
2693
+ advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z5.enum(["recheck", "informational"]).optional() });
2531
2694
  checkResultSchema = z5.object({
2532
2695
  issues: z5.array(issueSchema),
2533
2696
  advisories: z5.array(advisorySchema),
@@ -2545,14 +2708,18 @@ async function readRecord(root, id) {
2545
2708
  if (raw === null) return null;
2546
2709
  const record = recordSchema.parse(raw);
2547
2710
  const { digest: stored, ...payload } = record;
2548
- 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]))) {
2711
+ 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))) {
2549
2712
  throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
2550
2713
  }
2551
2714
  return record;
2552
2715
  }
2553
- function scopes(finding) {
2716
+ function evidenceFingerprint(evidence, reason2) {
2717
+ return digest([evidence.scope, evidence.tree, ...reason2 === "no-dependency-claims" ? [] : [evidence.lastScopeCommit], evidence.paths]);
2718
+ }
2719
+ function scopes(finding, reason2) {
2554
2720
  const files = [finding.anchor.doc], e = finding.evidence;
2555
2721
  if (e.kind === "decision-anchor") throw new Error("Use review_decision for decision acceptance, reaffirmation or retirement.");
2722
+ if (reason2 === "no-dependency-claims" && e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`];
2556
2723
  if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
2557
2724
  if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
2558
2725
  else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
@@ -2563,9 +2730,9 @@ function scopes(finding) {
2563
2730
  return `:(literal)${file}`;
2564
2731
  }).sort();
2565
2732
  }
2566
- async function reviewEvidence(root, finding) {
2733
+ async function reviewEvidence(root, finding, reason2) {
2567
2734
  if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc) throw new Error("Invalid advisory document path.");
2568
- const scope = scopes(finding);
2735
+ const scope = scopes(finding, reason2);
2569
2736
  const head = (await git2(root, "rev-parse", "HEAD")).trim();
2570
2737
  const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
2571
2738
  if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
@@ -2591,7 +2758,7 @@ async function reviewEvidence(root, finding) {
2591
2758
  if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
2592
2759
  }
2593
2760
  }
2594
- const fingerprint = digest([scope, tree, lastScopeCommit, paths]);
2761
+ const fingerprint = evidenceFingerprint({ head, scope, tree, lastScopeCommit, paths, fingerprint: "" }, reason2);
2595
2762
  if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
2596
2763
  return {
2597
2764
  ...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
@@ -2638,7 +2805,7 @@ async function assessAdvisory(root, finding, baselineHead) {
2638
2805
  const record = await readRecord(root, id);
2639
2806
  if (!record) return null;
2640
2807
  const event = record.events.at(-1);
2641
- const current = await reviewEvidence(root, event.finding);
2808
+ const current = await reviewEvidence(root, event.finding, event.reasonCode);
2642
2809
  await git2(root, "merge-base", "--is-ancestor", event.evidence.head, current.head);
2643
2810
  const base = {
2644
2811
  id,
@@ -2648,7 +2815,7 @@ async function assessAdvisory(root, finding, baselineHead) {
2648
2815
  outcome: event.outcome,
2649
2816
  reviewedHead: event.evidence.head
2650
2817
  };
2651
- 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." };
2818
+ 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." };
2652
2819
  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." };
2653
2820
  } catch (error) {
2654
2821
  return { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
@@ -2665,7 +2832,7 @@ async function advisoryReviewInventory(root) {
2665
2832
  if (names.length > 5e3) throw new Error("Advisory review inventory exceeds 5,000 records.");
2666
2833
  return Promise.all(names.filter((name) => name.endsWith(".json")).sort().map(async (name) => [name, await readAuditInput(root, ".mason/reviews/advisories/" + name)]));
2667
2834
  }
2668
- var git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
2835
+ var git2, hashSchema, commitSchema2, evidenceSchema2, dismissalReasonSchema, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
2669
2836
  var init_advisory_review = __esm({
2670
2837
  "src/audit/advisory-review.ts"() {
2671
2838
  "use strict";
@@ -2691,11 +2858,13 @@ var init_advisory_review = __esm({
2691
2858
  lastScopeCommit: commitSchema2,
2692
2859
  paths: z6.array(z6.object({ path: z6.string(), exists: z6.boolean() })).max(1e3)
2693
2860
  });
2861
+ dismissalReasonSchema = z6.enum(["no-dependency-claims", "unrelated-manifest-change"]);
2694
2862
  eventSchema2 = z6.object({
2695
2863
  at: z6.string().datetime(),
2696
- reviewer: z6.string().trim().min(1).max(200),
2864
+ reviewer: z6.string().trim().min(1).max(200).optional(),
2697
2865
  note: z6.string().trim().min(1).max(2500),
2698
- outcome: z6.enum(["addressed", "inapplicable", "deferred"]),
2866
+ outcome: z6.enum(["addressed", "inapplicable", "deferred", "dismissed"]),
2867
+ reasonCode: dismissalReasonSchema.optional(),
2699
2868
  finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
2700
2869
  evidence: evidenceSchema2
2701
2870
  });
@@ -2707,13 +2876,14 @@ var init_advisory_review = __esm({
2707
2876
  recordPath: z6.string().optional(),
2708
2877
  reviewer: z6.string().optional(),
2709
2878
  note: z6.string().optional(),
2710
- outcome: z6.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired"]).optional(),
2879
+ outcome: z6.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired", "dismissed"]).optional(),
2711
2880
  reviewedHead: z6.string().optional()
2712
2881
  });
2713
2882
  advisoryReviewRequest = z6.object({
2714
2883
  baselinePath: z6.string().min(1),
2715
2884
  findingId: hashSchema,
2716
- action: z6.enum(["prepare", "addressed", "inapplicable", "deferred"]).default("prepare"),
2885
+ action: z6.enum(["prepare", "addressed", "inapplicable", "deferred", "dismiss"]).default("prepare"),
2886
+ reasonCode: dismissalReasonSchema.optional(),
2717
2887
  reviewer: eventSchema2.shape.reviewer.optional(),
2718
2888
  note: eventSchema2.shape.note.optional(),
2719
2889
  reviewToken: hashSchema.optional()
@@ -2724,12 +2894,12 @@ var init_advisory_review = __esm({
2724
2894
 
2725
2895
  // src/audit/audit.ts
2726
2896
  import fs11 from "fs/promises";
2727
- import path19 from "path";
2897
+ import path20 from "path";
2728
2898
  async function computeAudit(rootDir, options = {}) {
2729
2899
  return profilePhase("audit.current", () => auditCurrent(rootDir, options));
2730
2900
  }
2731
2901
  async function auditCurrent(rootDir, options) {
2732
- const resolvedRoot = path19.resolve(rootDir);
2902
+ const resolvedRoot = path20.resolve(rootDir);
2733
2903
  const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
2734
2904
  if (docs.length === 0) return null;
2735
2905
  const report = {
@@ -2763,7 +2933,7 @@ async function auditCurrent(rootDir, options) {
2763
2933
  }
2764
2934
  let decisionsPresent = false;
2765
2935
  try {
2766
- await fs11.access(path19.join(resolvedRoot, ".mason", "decisions"));
2936
+ await fs11.access(path20.join(resolvedRoot, ".mason", "decisions"));
2767
2937
  decisionsPresent = true;
2768
2938
  } catch {
2769
2939
  }
@@ -2775,13 +2945,21 @@ async function auditCurrent(rootDir, options) {
2775
2945
  changesSinceDoc,
2776
2946
  decisionsPresent
2777
2947
  };
2948
+ const policy = (await loadProjectConfig(resolvedRoot)).audit;
2778
2949
  const selected = options.checks ?? ALL_CHECKS;
2779
2950
  for (const name of ALL_CHECKS) {
2780
2951
  if (!selected.includes(name)) continue;
2781
- const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, ctx) : CHECKS[name](ctx));
2952
+ const exclusions = policy?.exclude?.[name] ?? [];
2953
+ const included = await includedCheckPaths(resolvedRoot, docs.map((doc) => doc.path), exclusions);
2954
+ const excluded = docs.filter((doc) => !included.has(doc.path)).map((doc) => ({ check: name, doc: doc.path }));
2955
+ if (excluded.length) (report.excludedChecks ??= []).push(...excluded);
2956
+ const scoped = { ...ctx, docs: docs.filter((doc) => included.has(doc.path)) };
2957
+ const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, scoped) : CHECKS[name](scoped));
2782
2958
  report.checksRun.push(name);
2783
2959
  report.issues.push(...issues);
2784
- report.advisories.push(...advisories);
2960
+ const advisoryPaths = await includedCheckPaths(resolvedRoot, [...new Set(advisories.map((f) => f.anchor.doc))], exclusions);
2961
+ report.advisories.push(...advisories.filter((f) => advisoryPaths.has(f.anchor.doc)));
2962
+ for (const f of advisories) if (!advisoryPaths.has(f.anchor.doc)) (report.excludedChecks ??= []).push({ check: name, doc: f.anchor.doc });
2785
2963
  report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
2786
2964
  report.skippedChecks.push(...skipped);
2787
2965
  }
@@ -2793,6 +2971,8 @@ async function auditCurrent(rootDir, options) {
2793
2971
  var init_audit = __esm({
2794
2972
  "src/audit/audit.ts"() {
2795
2973
  "use strict";
2974
+ init_files();
2975
+ init_policy();
2796
2976
  init_profile();
2797
2977
  init_drift();
2798
2978
  init_snapshot();
@@ -2805,7 +2985,7 @@ var init_audit = __esm({
2805
2985
 
2806
2986
  // src/audit/repair.ts
2807
2987
  import fs12 from "fs/promises";
2808
- import path20 from "path";
2988
+ import path21 from "path";
2809
2989
  import { randomUUID as randomUUID2 } from "crypto";
2810
2990
  import { z as z7 } from "zod";
2811
2991
  function recheckable(finding) {
@@ -2826,7 +3006,7 @@ async function docState(root) {
2826
3006
  docs.push([doc, null]);
2827
3007
  }
2828
3008
  }
2829
- return digest(docs);
3009
+ return digest([docs, (await loadProjectConfig(root)).audit]);
2830
3010
  }
2831
3011
  async function stableAudit(root, checks, options = {}) {
2832
3012
  return profilePhase("repair.audit", () => readStableAudit(root, checks, options));
@@ -2892,8 +3072,8 @@ async function prepareOriginal(rootDir, checks, options, session) {
2892
3072
  }
2893
3073
  async function loadRepairBaseline(rootDir, baselinePath) {
2894
3074
  const root = await fs12.realpath(rootDir);
2895
- const declaredRoot = path20.resolve(rootDir);
2896
- const relative = path20.isAbsolute(baselinePath) ? path20.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
3075
+ const declaredRoot = path21.resolve(rootDir);
3076
+ const relative = path21.isAbsolute(baselinePath) ? path21.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
2897
3077
  const stored = baselineSchema.parse(await readStoreJson(root, relative));
2898
3078
  const { digest: savedDigest, ...payload } = stored;
2899
3079
  if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
@@ -2939,6 +3119,9 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
2939
3119
  if (now && ("confidence" in finding || "confidence" in now)) {
2940
3120
  return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
2941
3121
  }
3122
+ if (current.excludedChecks?.some((excluded) => excluded.check === finding.type && excluded.doc === finding.anchor.doc)) {
3123
+ return { ...base, status: "resolved", reason: "Excluded by project audit policy; this is not a correctness assessment. Original evidence is retained." };
3124
+ }
2942
3125
  const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
2943
3126
  if (!current.checksRun?.includes(finding.type) || skipped.length) {
2944
3127
  return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
@@ -2949,11 +3132,14 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
2949
3132
  if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
2950
3133
  return { ...base, review, status: "review-required", reason: review.reason };
2951
3134
  }
3135
+ if (!("confidence" in finding) && finding.evidence.kind === "doc-behind-manifests" && !finding.evidence.matches?.length && (!now || !("confidence" in now) && now.resolution === "informational")) {
3136
+ return { ...base, status: "resolved", reason: "Manifest recency alone establishes no affected passage. Retained as information; no assessment is required." };
3137
+ }
2952
3138
  if (!("confidence" in finding) && (!recheckable(finding) || now)) {
2953
3139
  return {
2954
3140
  ...base,
2955
3141
  status: "review-required",
2956
- 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."
3142
+ 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."
2957
3143
  };
2958
3144
  }
2959
3145
  return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
@@ -2977,7 +3163,7 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
2977
3163
  const reviewedIds = new Set(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
2978
3164
  const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
2979
3165
  for (const f of findings) counts[f.status]++;
2980
- 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)));
3166
+ 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)));
2981
3167
  const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
2982
3168
  return {
2983
3169
  version: 1,
@@ -3020,13 +3206,14 @@ var init_repair = __esm({
3020
3206
  docs: z7.array(z7.object({
3021
3207
  path: z7.string().refine((value) => normalizeRepoPath(value) === value),
3022
3208
  scope: z7.string().optional(),
3023
- kind: z7.enum(["instructions", "readme"]).optional(),
3209
+ kind: z7.enum(["instructions", "readme", "guide"]).optional(),
3024
3210
  lastCommit: commitSchema.nullable(),
3025
3211
  dirty: z7.boolean(),
3026
3212
  lineCount: count2
3027
3213
  })).nonempty(),
3028
3214
  decisionsChecked: z7.boolean(),
3029
3215
  clean: z7.boolean(),
3216
+ excludedChecks: z7.array(z7.object({ check: checkSchema, doc: z7.string() })).optional(),
3030
3217
  issues: z7.array(issueSchema),
3031
3218
  advisories: z7.array(advisorySchema),
3032
3219
  suppressedAdvisories: z7.array(advisorySchema).optional(),
@@ -3052,9 +3239,9 @@ var init_repair = __esm({
3052
3239
 
3053
3240
  // src/automation/evidence.ts
3054
3241
  import fs13 from "fs/promises";
3055
- import path21 from "path";
3242
+ import path22 from "path";
3056
3243
  import { createHash as createHash3 } from "crypto";
3057
- import fg5 from "fast-glob";
3244
+ import fg7 from "fast-glob";
3058
3245
  import { z as z8 } from "zod";
3059
3246
  async function git3(root, ...args2) {
3060
3247
  return profilePhase("automation.git", async () => (await execGit(args2, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout);
@@ -3072,6 +3259,11 @@ async function readInputs(root) {
3072
3259
  }
3073
3260
  async function collectInputs(root) {
3074
3261
  const docPaths = await discoverDocPaths(root);
3262
+ const auditPolicy = (await loadProjectConfig(root)).audit;
3263
+ const allowed = /* @__PURE__ */ new Map();
3264
+ for (const name of ["deleted-reference", "new-module", "stale-count", "dead-command"]) {
3265
+ allowed.set(name, await includedCheckPaths(root, docPaths, auditPolicy?.exclude?.[name] ?? []));
3266
+ }
3075
3267
  const [headText, docStatus, shallowPath, replacements] = await Promise.all([
3076
3268
  git3(root, "rev-parse", "HEAD"),
3077
3269
  docPaths.length ? git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...docPaths.map((p) => `:(literal)${p}`)) : "",
@@ -3081,7 +3273,7 @@ async function collectInputs(root) {
3081
3273
  const head = headText.trim();
3082
3274
  let shallow = null;
3083
3275
  try {
3084
- shallow = await fs13.readFile(path21.resolve(root, shallowPath.trim()), "utf8");
3276
+ shallow = await fs13.readFile(path22.resolve(root, shallowPath.trim()), "utf8");
3085
3277
  } catch (error) {
3086
3278
  if (error.code !== "ENOENT") throw error;
3087
3279
  }
@@ -3100,18 +3292,18 @@ async function collectInputs(root) {
3100
3292
  docContents.push([file, text2]);
3101
3293
  const parsed = { path: file, content: text2 ?? "", claims: extractClaims(text2 ?? "") };
3102
3294
  parsedDocs.push(parsed);
3103
- for (const claim of parsed.claims.paths) {
3295
+ for (const claim of allowed.get("deleted-reference").has(file) ? parsed.claims.paths : []) {
3104
3296
  const scope = pathClaimScope(file, claim);
3105
3297
  if (scope?.candidates.some(gitMetadataPath)) continue;
3106
3298
  for (const candidate of scope?.candidates ?? []) {
3107
3299
  if (optionalMasonPath(candidate)) continue;
3108
- const [filePresent, parentPresent] = await Promise.all([exists(candidate), exists(path21.posix.dirname(candidate))]);
3300
+ const [filePresent, parentPresent] = await Promise.all([exists(candidate), exists(path22.posix.dirname(candidate))]);
3109
3301
  claims.push([candidate, filePresent, parentPresent]);
3110
3302
  }
3111
3303
  }
3112
3304
  }
3113
- const combinedDocs = moduleDocumentation(parsedDocs);
3114
- const countClaims = parsedDocs.flatMap((doc) => doc.claims.counts.map((claim) => ({ doc: doc.path, claim })));
3305
+ const combinedDocs = moduleDocumentation(parsedDocs.filter((doc) => allowed.get("new-module").has(doc.path)));
3306
+ const countClaims = parsedDocs.filter((doc) => allowed.get("stale-count").has(doc.path)).flatMap((doc) => doc.claims.counts.map((claim) => ({ doc: doc.path, claim })));
3115
3307
  const decisionDirectory = await storePath(root, ".mason/decisions");
3116
3308
  const decisionPresence = await fs13.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
3117
3309
  if (error.code === "ENOENT") return "absent";
@@ -3120,8 +3312,8 @@ async function collectInputs(root) {
3120
3312
  const [modules, counts, commands2, decisionFiles] = await Promise.all([
3121
3313
  combinedDocs ? moduleCandidates(root, combinedDocs) : [],
3122
3314
  Promise.all(countClaims.map(({ doc, claim }) => resolveDocCountSource(root, doc, claim))),
3123
- commandInputs(root, parsedDocs),
3124
- fg5(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
3315
+ commandInputs(root, parsedDocs.filter((doc) => allowed.get("dead-command").has(doc.path))),
3316
+ fg7(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
3125
3317
  ]);
3126
3318
  const decisions = await Promise.all(decisionFiles.sort().map(async (file) => {
3127
3319
  await storePath(root, file);
@@ -3131,7 +3323,7 @@ async function collectInputs(root) {
3131
3323
  git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
3132
3324
  git3(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
3133
3325
  ]) : ["", ""];
3134
- const common = [5, engineVersion, head, shallow, replacements, docContents];
3326
+ const common = [7, engineVersion, head, shallow, replacements, docContents, auditPolicy];
3135
3327
  const keys = {
3136
3328
  "deleted-reference": hash([common, claims, docStatus]),
3137
3329
  "new-module": hash([common, modules]),
@@ -3178,6 +3370,8 @@ var engineVersion, hash, content, cacheSchema;
3178
3370
  var init_evidence = __esm({
3179
3371
  "src/automation/evidence.ts"() {
3180
3372
  "use strict";
3373
+ init_policy();
3374
+ init_files();
3181
3375
  init_git_read();
3182
3376
  init_profile();
3183
3377
  init_docs();
@@ -3191,7 +3385,7 @@ var init_evidence = __esm({
3191
3385
  init_dead_command();
3192
3386
  init_storage();
3193
3387
  init_advisory_review();
3194
- engineVersion = true ? "0.18.0" : "development";
3388
+ engineVersion = true ? "0.19.1" : "development";
3195
3389
  hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
3196
3390
  content = readAuditInput;
3197
3391
  cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
@@ -3405,13 +3599,17 @@ async function observeReadOnlyTool(dir, event) {
3405
3599
  }
3406
3600
  function summarize(report) {
3407
3601
  const open = report.findings.filter((f) => f.status !== "resolved");
3602
+ const onlyReview = report.status === "incomplete" && !report.diagnostics.length && !report.counts.unresolved && !report.counts.unverified && report.counts["review-required"] > 0;
3408
3603
  return [
3409
- `Mason: ${report.status}; ${report.counts.unresolved} unresolved, ${report.counts["review-required"]} need review, ${report.counts.unverified} unverified.`,
3410
- ...open.slice(0, 4).map((f) => `[${f.status}] ${cleanText(f.original.anchor.doc)}: ${cleanText(f.original.message)}`),
3604
+ 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.`,
3605
+ ...open.slice(0, 4).map((f) => {
3606
+ 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}`;
3607
+ return `[${f.status}] ${cleanText(f.original.anchor.doc)}: ${cleanText(detail)}`;
3608
+ }),
3411
3609
  ...open.length > 4 ? [`${open.length - 4} more findings in the report.`] : [],
3412
3610
  ...report.diagnostics.slice(0, 2).map(cleanText),
3413
3611
  `Evidence: ${report.reportPath}. Resume/check with mason_automation(action: "check") or mason-auto check.`,
3414
- "Keep original evidence. Address findings relevant to the authorized task; report unrelated findings and unresolved advisories without approving them."
3612
+ "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."
3415
3613
  ].join("\n");
3416
3614
  }
3417
3615
  async function loadState(ws) {
@@ -3635,7 +3833,7 @@ async function automate(dir, event) {
3635
3833
  if (event.event === "after_tool" && event.mutating) {
3636
3834
  session.mutationObserved = true;
3637
3835
  if (!event.toolId || !session.pending[event.toolId]) {
3638
- const gap = "A tool completed without an observed matching pre-tool capture; pre-edit coverage is unknown.";
3836
+ 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.";
3639
3837
  if (!session.coverageGaps.includes(gap)) session.coverageGaps.push(gap);
3640
3838
  }
3641
3839
  if (event.toolId) delete session.pending[event.toolId];
@@ -3745,9 +3943,9 @@ var init_runtime = __esm({
3745
3943
 
3746
3944
  // src/automation/hook-root.ts
3747
3945
  import fs15 from "fs/promises";
3748
- import path22 from "path";
3946
+ import path23 from "path";
3749
3947
  async function repositoryRoot(dir) {
3750
- if (!path22.isAbsolute(dir)) throw new Error("Hook input working directory must be absolute.");
3948
+ if (!path23.isAbsolute(dir)) throw new Error("Hook input working directory must be absolute.");
3751
3949
  try {
3752
3950
  return await fs15.realpath((await git3(dir, "rev-parse", "--show-toplevel")).trim());
3753
3951
  } catch (error) {
@@ -3778,11 +3976,39 @@ var init_hook_root = __esm({
3778
3976
 
3779
3977
  // src/setup/model.ts
3780
3978
  import { z as z11 } from "zod";
3979
+ import fs16 from "fs/promises";
3980
+ import path24 from "path";
3781
3981
  async function loadSetup(root) {
3782
3982
  const raw = await readStoreJson(root, SETUP_PATH);
3783
3983
  if (raw === null) return null;
3784
3984
  return setupSchema.parse(raw);
3785
3985
  }
3986
+ async function effectiveSetup(root) {
3987
+ const local = await loadSetup(root);
3988
+ if (local) return { root, setup: local };
3989
+ const common = await fs16.realpath(path24.resolve(root, (await git3(root, "rev-parse", "--git-common-dir")).trim()));
3990
+ const own = await fs16.realpath((await git3(root, "rev-parse", "--absolute-git-dir")).trim());
3991
+ if (common === own) return { root, setup: null };
3992
+ const primary = (await git3(root, "worktree", "list", "--porcelain", "-z")).split("\0\0")[0].split("\0");
3993
+ const candidate = primary.find((field) => field.startsWith("worktree "))?.slice(9);
3994
+ if (!candidate || primary.includes("bare")) return { root, setup: null };
3995
+ let inheritedRoot;
3996
+ try {
3997
+ inheritedRoot = await fs16.realpath((await git3(candidate, "rev-parse", "--show-toplevel")).trim());
3998
+ } catch (error) {
3999
+ if (/must be run in a work tree/.test(String(error.stderr))) return { root, setup: null };
4000
+ throw error;
4001
+ }
4002
+ const inheritedCommon = await fs16.realpath(path24.resolve(
4003
+ inheritedRoot,
4004
+ (await git3(inheritedRoot, "rev-parse", "--git-common-dir")).trim()
4005
+ ));
4006
+ if (inheritedCommon !== common) throw new Error("Linked worktree setup does not belong to the same Git repository.");
4007
+ const setup = await loadSetup(inheritedRoot);
4008
+ const inactive = await readStoreJson(root, INACTIVE_HOSTS_PATH);
4009
+ if (inactive && setup) for (const host of inactiveHostsSchema.parse(inactive).hosts) delete setup.hosts[host];
4010
+ return { root: inheritedRoot, setup };
4011
+ }
3786
4012
  async function loadSetupReceipt(root, directory, host) {
3787
4013
  const raw = await readStoreJson(root, directory + "/setup-" + host + ".json");
3788
4014
  if (raw === null) return null;
@@ -3790,10 +4016,11 @@ async function loadSetupReceipt(root, directory, host) {
3790
4016
  if (receipt.root !== root || receipt.host !== host) throw new Error("Setup receipt belongs to another installation.");
3791
4017
  return receipt;
3792
4018
  }
3793
- var setupHostSchema, setupSchema, SETUP_PATH, receiptSchema;
4019
+ var setupHostSchema, setupSchema, SETUP_PATH, INACTIVE_HOSTS_PATH, inactiveHostsSchema, receiptSchema;
3794
4020
  var init_model = __esm({
3795
4021
  "src/setup/model.ts"() {
3796
4022
  "use strict";
4023
+ init_evidence();
3797
4024
  init_storage();
3798
4025
  setupHostSchema = z11.object({
3799
4026
  configuredVersion: z11.string(),
@@ -3807,6 +4034,8 @@ var init_model = __esm({
3807
4034
  claude: setupHostSchema.optional()
3808
4035
  }) });
3809
4036
  SETUP_PATH = ".mason/local/setup.json";
4037
+ INACTIVE_HOSTS_PATH = ".mason/local/inactive-hosts.json";
4038
+ inactiveHostsSchema = z11.object({ version: z11.literal(1), hosts: z11.array(z11.enum(["claude", "codex"])) });
3810
4039
  receiptSchema = z11.object({
3811
4040
  version: z11.literal(1),
3812
4041
  host: z11.enum(["codex", "claude"]),
@@ -3833,7 +4062,7 @@ __export(launcher_exports, {
3833
4062
  });
3834
4063
  import { execFile as execFile2 } from "child_process";
3835
4064
  import { promisify as promisify2 } from "util";
3836
- import fs16 from "fs/promises";
4065
+ import fs17 from "fs/promises";
3837
4066
  function globalCommand(args2, platform = process.platform) {
3838
4067
  return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args2] } : { command: "mason", args: args2 };
3839
4068
  }
@@ -3853,8 +4082,8 @@ async function installedCommand() {
3853
4082
  }
3854
4083
  }
3855
4084
  async function prepareLaunch(host, options = {}) {
3856
- const root = await fs16.realpath((await git3(options.dir ?? process.cwd(), "rev-parse", "--show-toplevel")).trim());
3857
- const entry = (await loadSetup(root))?.hosts[host];
4085
+ const root = await fs17.realpath((await git3(options.dir ?? process.cwd(), "rev-parse", "--show-toplevel")).trim());
4086
+ const entry = (await effectiveSetup(root)).setup?.hosts[host];
3858
4087
  process.env.MASON_SETUP_ROOT = root;
3859
4088
  process.env.MASON_SETUP_HOST = host;
3860
4089
  delete process.env.MASON_SETUP_REVISION;
@@ -3876,7 +4105,7 @@ var init_launcher = __esm({
3876
4105
  "use strict";
3877
4106
  init_evidence();
3878
4107
  init_model();
3879
- executingVersion = () => true ? "0.18.0" : "development";
4108
+ executingVersion = () => true ? "0.19.1" : "development";
3880
4109
  mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
3881
4110
  hookCommand = (host) => `mason --setup-host ${host} auto`;
3882
4111
  }
@@ -3889,7 +4118,7 @@ __export(observations_exports, {
3889
4118
  observeActivation: () => observeActivation,
3890
4119
  readObservation: () => readObservation
3891
4120
  });
3892
- import fs17 from "fs/promises";
4121
+ import fs18 from "fs/promises";
3893
4122
  import { z as z12 } from "zod";
3894
4123
  function observationPath(directory, host, revision, version = executingVersion()) {
3895
4124
  return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
@@ -3906,15 +4135,15 @@ async function observeActivation(dir, event, options = {}) {
3906
4135
  if (host !== "codex" && host !== "claude" || !process.env.MASON_SETUP_ROOT) return null;
3907
4136
  try {
3908
4137
  if (!revision) {
3909
- if ((await loadSetup(process.env.MASON_SETUP_ROOT))?.hosts[host]) {
4138
+ if ((await effectiveSetup(process.env.MASON_SETUP_ROOT)).setup?.hosts[host]) {
3910
4139
  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.";
3911
4140
  }
3912
4141
  return inactiveSetupMessage();
3913
4142
  }
3914
4143
  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];
3915
- const ws = capturedDirectory ? { root: await fs17.realpath(dir), directory: capturedDirectory } : await workspace(dir);
3916
- if (ws.root !== await fs17.realpath(process.env.MASON_SETUP_ROOT)) return null;
3917
- const setup = await loadSetup(ws.root);
4144
+ const ws = capturedDirectory ? { root: await fs18.realpath(dir), directory: capturedDirectory } : await workspace(dir);
4145
+ if (ws.root !== await fs18.realpath(process.env.MASON_SETUP_ROOT)) return null;
4146
+ const { setup } = await effectiveSetup(ws.root);
3918
4147
  if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
3919
4148
  const directory = ws.directory + "/activation";
3920
4149
  await withLock(ws.root, directory, async () => {
@@ -4091,8 +4320,8 @@ __export(files_exports, {
4091
4320
  managedBlock: () => managedBlock,
4092
4321
  readText: () => readText
4093
4322
  });
4094
- import fs18 from "fs/promises";
4095
- import path23 from "path";
4323
+ import fs19 from "fs/promises";
4324
+ import path25 from "path";
4096
4325
  import { randomUUID as randomUUID5 } from "crypto";
4097
4326
  async function readText(root, file) {
4098
4327
  try {
@@ -4121,14 +4350,14 @@ async function applyEdit(root, edit) {
4121
4350
  if (edit.after === null) {
4122
4351
  const file2 = await storePath(root, edit.path);
4123
4352
  if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
4124
- await fs18.unlink(file2);
4353
+ await fs19.unlink(file2);
4125
4354
  return true;
4126
4355
  }
4127
4356
  const file = await storePath(root, edit.path, true);
4128
- const temporary = path23.join(path23.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
4357
+ const temporary = path25.join(path25.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
4129
4358
  try {
4130
- const mode = await fs18.stat(file).then((s) => s.mode & 511, () => 384);
4131
- const handle = await fs18.open(temporary, "wx", mode);
4359
+ const mode = await fs19.stat(file).then((s) => s.mode & 511, () => 384);
4360
+ const handle = await fs19.open(temporary, "wx", mode);
4132
4361
  try {
4133
4362
  await handle.writeFile(edit.after, "utf8");
4134
4363
  await handle.sync();
@@ -4136,10 +4365,10 @@ async function applyEdit(root, edit) {
4136
4365
  await handle.close();
4137
4366
  }
4138
4367
  if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
4139
- await fs18.rename(temporary, file);
4368
+ await fs19.rename(temporary, file);
4140
4369
  return true;
4141
4370
  } finally {
4142
- await fs18.rm(temporary, { force: true });
4371
+ await fs19.rm(temporary, { force: true });
4143
4372
  }
4144
4373
  }
4145
4374
  var init_files2 = __esm({
@@ -4250,9 +4479,11 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
4250
4479
  - 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.
4251
4480
  - \`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.
4252
4481
 
4253
- - 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.
4482
+ - 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.
4254
4483
  - 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.
4255
4484
 
4485
+ - 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.
4486
+
4256
4487
  - 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.
4257
4488
 
4258
4489
  Inspect source for what the retrieved context does not answer.
@@ -4547,8 +4778,10 @@ __export(teardown_exports, {
4547
4778
  teardownProject: () => teardownProject
4548
4779
  });
4549
4780
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
4781
+ import fs20 from "fs/promises";
4782
+ import path26 from "path";
4550
4783
  import { parse as parse3, stringify as stringify2 } from "smol-toml";
4551
- import fg6 from "fast-glob";
4784
+ import fg8 from "fast-glob";
4552
4785
  import { z as z18 } from "zod";
4553
4786
  async function planTeardown(root, selected) {
4554
4787
  const edits = [], diagnostics = [];
@@ -4568,6 +4801,11 @@ async function planTeardown(root, selected) {
4568
4801
  const ownership = ownershipBefore === null ? { version: 1, files: {} } : ownershipSchema.parse(JSON.parse(ownershipBefore));
4569
4802
  const setupBefore = await read(SETUP_PATH);
4570
4803
  const setup = setupBefore === null ? null : setupSchema.parse(JSON.parse(setupBefore));
4804
+ const linked = (await workspace(root)).gitDir !== await fs20.realpath(
4805
+ path26.resolve(root, (await git3(root, "rev-parse", "--git-common-dir")).trim())
4806
+ );
4807
+ const inactiveBefore = linked && !setup ? await read(INACTIVE_HOSTS_PATH) : null;
4808
+ const inactive = inactiveBefore === null ? { version: 1, hosts: [] } : inactiveHostsSchema.parse(JSON.parse(inactiveBefore));
4571
4809
  const automationBefore = await read(AUTOMATION_PATH);
4572
4810
  const automation = automationBefore === null ? null : recordSchema2.parse(JSON.parse(automationBefore));
4573
4811
  const commands2 = (host) => new Set([
@@ -4687,7 +4925,7 @@ async function planTeardown(root, selected) {
4687
4925
  if (diagnostics.length) edits.splice(instructionEditStart);
4688
4926
  if (!diagnostics.length) {
4689
4927
  const cleanup = [];
4690
- const receiptFiles = await fg6(
4928
+ const receiptFiles = await fg8(
4691
4929
  selected.map((host) => ".mason/reports/automation/*/setup-" + host + ".json"),
4692
4930
  { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false }
4693
4931
  );
@@ -4711,7 +4949,16 @@ async function planTeardown(root, selected) {
4711
4949
  delete ownership.files[configPath(host)];
4712
4950
  }
4713
4951
  if (!remainingHost) for (const file of DOC_CANDIDATES) delete ownership.files[file];
4714
- if (setupBefore !== null) cleanup.push({ path: SETUP_PATH, before: setupBefore, after: Object.keys(setup.hosts).length ? json(setup) : null });
4952
+ if (setupBefore !== null) cleanup.push({
4953
+ path: SETUP_PATH,
4954
+ before: setupBefore,
4955
+ after: linked || Object.keys(setup.hosts).length ? json(setup) : null
4956
+ });
4957
+ else if (linked) cleanup.push({
4958
+ path: INACTIVE_HOSTS_PATH,
4959
+ before: inactiveBefore,
4960
+ after: json({ version: 1, hosts: [.../* @__PURE__ */ new Set([...inactive.hosts, ...selected])].sort() })
4961
+ });
4715
4962
  if (automationBefore !== null) cleanup.push({ path: AUTOMATION_PATH, before: automationBefore, after: Object.keys(automation.hosts).length ? json(automation) : null });
4716
4963
  if (ownershipBefore !== null) cleanup.push({ path: OWNERSHIP_PATH, before: ownershipBefore, after: Object.keys(ownership.files).length ? json(ownership) : null });
4717
4964
  edits.push(...cleanup);
@@ -5152,10 +5399,10 @@ var init_check = __esm({
5152
5399
  });
5153
5400
 
5154
5401
  // src/review/evidence.ts
5155
- import path24 from "path";
5402
+ import path27 from "path";
5156
5403
  import { z as z22 } from "zod";
5157
5404
  function relativeArtifact(root, file) {
5158
- const relative = path24.isAbsolute(file) ? path24.relative(root, file) : file;
5405
+ const relative = path27.isAbsolute(file) ? path27.relative(root, file) : file;
5159
5406
  const normalized = normalizeRepoPath(relative);
5160
5407
  if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
5161
5408
  return normalized;
@@ -5252,7 +5499,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
5252
5499
  const report = await readStoreJson(root, file);
5253
5500
  if (report === null) throw new Error(`Report artifact is missing: ${file}`);
5254
5501
  const sourceRoot = check.sourceRoot ?? root;
5255
- if (!path24.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
5502
+ if (!path27.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
5256
5503
  const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : check.report.format === "mason-check-json" ? parseCheck(report, sourceRoot) : parseSarif(report, sourceRoot);
5257
5504
  result.outcome = parsed.outcome;
5258
5505
  result.counts = parsed.counts;
@@ -5373,8 +5620,8 @@ var init_evidence2 = __esm({
5373
5620
  });
5374
5621
 
5375
5622
  // src/review/review.ts
5376
- import fs19 from "fs/promises";
5377
- import path25 from "path";
5623
+ import fs21 from "fs/promises";
5624
+ import path28 from "path";
5378
5625
  import { execFile as execFile4 } from "child_process";
5379
5626
  import { promisify as promisify4 } from "util";
5380
5627
  async function resolveMergeBase(resolvedRoot, base, head) {
@@ -5403,7 +5650,7 @@ function anchorsTouched(record, changedFiles) {
5403
5650
  return matchingPaths(record.files, changedFiles);
5404
5651
  }
5405
5652
  async function computeReview(rootDir, base, options = {}) {
5406
- const resolvedRoot = path25.resolve(rootDir);
5653
+ const resolvedRoot = path28.resolve(rootDir);
5407
5654
  const head = await getCurrentGitHash(resolvedRoot);
5408
5655
  if (head === "unknown") return null;
5409
5656
  const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
@@ -5453,7 +5700,7 @@ async function computeReview(rootDir, base, options = {}) {
5453
5700
  analyzed,
5454
5701
  async (relPath) => {
5455
5702
  try {
5456
- await fs19.access(path25.join(resolvedRoot, relPath));
5703
+ await fs21.access(path28.join(resolvedRoot, relPath));
5457
5704
  return true;
5458
5705
  } catch {
5459
5706
  return false;
@@ -5596,8 +5843,9 @@ __export(status_exports, {
5596
5843
  import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
5597
5844
  async function setupStatus(dir) {
5598
5845
  const ws = await workspace(dir);
5599
- const setup = await loadSetup(ws.root);
5600
- if (!setup) {
5846
+ const activation = await effectiveSetup(ws.root);
5847
+ const setup = activation.setup;
5848
+ if (!setup || !Object.keys(setup.hosts).length) {
5601
5849
  const partial = await Promise.all(["codex", "claude"].map(async (host) => ({
5602
5850
  host,
5603
5851
  receipt: await loadSetupReceipt(ws.root, ws.directory, host)
@@ -5631,7 +5879,7 @@ async function setupStatus(dir) {
5631
5879
  ]));
5632
5880
  const hooks = isDeepStrictEqual3(compatibleHooks, expectedHooks) && !config.disabled;
5633
5881
  const local = await loadSetupReceipt(ws.root, ws.directory, host);
5634
- const configured = local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
5882
+ const configured = activation.root !== ws.root || local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
5635
5883
  const observation = await readObservation(ws.root, ws.directory, host, entry.revision, installed.version ?? "unavailable");
5636
5884
  const sessions = Object.values(observation?.sessions ?? {}).sort((a, b) => b.at.localeCompare(a.at));
5637
5885
  const complete = sessions.find((session) => events.every((event) => session.events.includes(event)));
@@ -5663,6 +5911,7 @@ async function setupStatus(dir) {
5663
5911
  status: statuses.length && statuses.every((status) => status === "active") ? "active" : statuses.includes("attention") ? "attention" : "pending",
5664
5912
  root: ws.root,
5665
5913
  hosts: hosts2,
5914
+ activationRoot: activation.root,
5666
5915
  decisionRecords: decisions.records.length,
5667
5916
  diagnostics: decisions.diagnostics,
5668
5917
  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."
@@ -5670,6 +5919,9 @@ async function setupStatus(dir) {
5670
5919
  }
5671
5920
  function summarizeActivation(status) {
5672
5921
  const lines = ["Mason setup: " + status.status + "."];
5922
+ if (status.activationRoot && status.activationRoot !== status.root) {
5923
+ lines.push("Activation inherited from " + status.activationRoot + "; evidence belongs to this worktree.");
5924
+ }
5673
5925
  for (const [host, state] of Object.entries(status.hosts)) {
5674
5926
  lines.push(
5675
5927
  `${host}: ${state.status}`,
@@ -5831,66 +6083,19 @@ var init_setup = __esm({
5831
6083
  }
5832
6084
  });
5833
6085
 
5834
- // src/distribution/bundle.ts
5835
- import fs20 from "fs/promises";
5836
- import path26 from "path";
5837
- import { createHash as createHash4 } from "crypto";
5838
- import { z as z23 } from "zod";
5839
- async function readBundle(root) {
5840
- const bytes = await fs20.readFile(await storePath(root, "bundle.json"));
5841
- const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
5842
- const windows = manifest.target.startsWith("win32-");
5843
- for (const file of Object.keys(manifest.files)) {
5844
- if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") || path26.isAbsolute(file)) {
5845
- throw new Error("Unsafe path in Mason bundle: " + file);
5846
- }
5847
- }
5848
- 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"]) {
5849
- if (!manifest.files[required]) throw new Error("Incomplete Mason bundle: " + required);
5850
- }
5851
- return { manifest, manifestHash: sha256(bytes) };
5852
- }
5853
- async function verifyBundle(root, expectedHash) {
5854
- const bundle = await readBundle(root);
5855
- if (expectedHash && bundle.manifestHash !== expectedHash) throw new Error("Mason bundle manifest changed.");
5856
- 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.");
5857
- const files = Object.keys(bundle.manifest.files);
5858
- for (let i = 0; i < files.length; i += 8) await Promise.all(files.slice(i, i + 8).map(async (file) => {
5859
- if (sha256(await fs20.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
5860
- }));
5861
- return bundle;
5862
- }
5863
- var targets, digest2, bundleSchema, sha256;
5864
- var init_bundle = __esm({
5865
- "src/distribution/bundle.ts"() {
5866
- "use strict";
5867
- init_storage();
5868
- targets = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
5869
- digest2 = z23.string().regex(/^[a-f0-9]{64}$/);
5870
- bundleSchema = z23.object({
5871
- format: z23.literal(1),
5872
- version: z23.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
5873
- target: z23.enum(targets),
5874
- nodeVersion: z23.string(),
5875
- files: z23.record(digest2)
5876
- }).strict();
5877
- sha256 = (bytes) => createHash4("sha256").update(bytes).digest("hex");
5878
- }
5879
- });
5880
-
5881
6086
  // src/distribution/path.ts
5882
- import fs21 from "fs/promises";
6087
+ import fs22 from "fs/promises";
5883
6088
  import os3 from "os";
5884
- import path27 from "path";
6089
+ import path29 from "path";
5885
6090
  import { execFile as execFile5 } from "child_process";
5886
6091
  import { promisify as promisify5 } from "util";
5887
- import { createHash as createHash5, randomUUID as randomUUID7 } from "crypto";
5888
- import { z as z24 } from "zod";
6092
+ import { createHash as createHash4, randomUUID as randomUUID7 } from "crypto";
6093
+ import { z as z23 } from "zod";
5889
6094
  async function configurePath(bin, previous, save, options = {}) {
5890
6095
  const env = options.env ?? process.env, platform = options.platform ?? process.platform;
5891
6096
  const changes = [...previous];
5892
6097
  const currentPath = platform === "win32" ? Object.entries(env).find(([key]) => key.toLowerCase() === "path")?.[1] : env.PATH;
5893
- const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) => path27.posix.isAbsolute(entry) && path27.posix.resolve(entry) === path27.posix.resolve(bin));
6098
+ const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) => path29.posix.isAbsolute(entry) && path29.posix.resolve(entry) === path29.posix.resolve(bin));
5894
6099
  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";
5895
6100
  const message2 = (alreadyOnPath ? "" : "Open a new terminal to use mason.\n") + setupHint;
5896
6101
  const manual = (reason2) => ({ status: "manual", changes, message: `${reason2}
@@ -5910,33 +6115,33 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
5910
6115
  }
5911
6116
  return { status: "configured", changes, message: message2 };
5912
6117
  }
5913
- const shell = path27.basename(env.SHELL ?? os3.userInfo().shell ?? "");
6118
+ const shell = path29.basename(env.SHELL ?? os3.userInfo().shell ?? "");
5914
6119
  const userHome = options.userHome ?? os3.homedir();
5915
6120
  let profiles2;
5916
6121
  if (!["bash", "zsh", "sh"].includes(shell)) return manual(`Automatic PATH setup is unavailable for ${shell || "an unknown shell"}.`);
5917
- if (env.MASON_PROFILE) profiles2 = [path27.resolve(env.MASON_PROFILE)];
5918
- else if (shell === "zsh") profiles2 = [path27.join(env.ZDOTDIR || userHome, ".zshrc")];
5919
- else if (shell === "sh") profiles2 = [path27.join(userHome, ".profile")];
6122
+ if (env.MASON_PROFILE) profiles2 = [path29.resolve(env.MASON_PROFILE)];
6123
+ else if (shell === "zsh") profiles2 = [path29.join(env.ZDOTDIR || userHome, ".zshrc")];
6124
+ else if (shell === "sh") profiles2 = [path29.join(userHome, ".profile")];
5920
6125
  else {
5921
- const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path27.join(userHome, name));
6126
+ const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path29.join(userHome, name));
5922
6127
  let login = candidates[2];
5923
- for (const file of candidates) if (await fs21.lstat(file).then(() => true, (error) => {
6128
+ for (const file of candidates) if (await fs22.lstat(file).then(() => true, (error) => {
5924
6129
  if (error.code === "ENOENT") return false;
5925
6130
  throw error;
5926
6131
  })) {
5927
6132
  login = file;
5928
6133
  break;
5929
6134
  }
5930
- profiles2 = [path27.join(userHome, ".bashrc"), login];
6135
+ profiles2 = [path29.join(userHome, ".bashrc"), login];
5931
6136
  }
5932
- const marker = `# >>> mason PATH ${createHash5("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
6137
+ const marker = `# >>> mason PATH ${createHash4("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
5933
6138
  for (const profile of profiles2) {
5934
- const stat = await fs21.lstat(profile).catch((error) => {
6139
+ const stat = await fs22.lstat(profile).catch((error) => {
5935
6140
  if (error.code === "ENOENT") return null;
5936
6141
  throw error;
5937
6142
  });
5938
- const file = stat?.isSymbolicLink() ? await fs21.realpath(profile) : path27.resolve(profile);
5939
- const original = await fs21.readFile(file).catch((error) => {
6143
+ const file = stat?.isSymbolicLink() ? await fs22.realpath(profile) : path29.resolve(profile);
6144
+ const original = await fs22.readFile(file).catch((error) => {
5940
6145
  if (error.code === "ENOENT" && !stat) return null;
5941
6146
  throw error;
5942
6147
  });
@@ -5951,8 +6156,8 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
5951
6156
  changes.push({ kind: "profile", file, block, created: original === null });
5952
6157
  await save(changes);
5953
6158
  }
5954
- await fs21.mkdir(path27.dirname(file), { recursive: true });
5955
- const handle = await fs21.open(file, original === null ? "ax+" : "a+", 384);
6159
+ await fs22.mkdir(path29.dirname(file), { recursive: true });
6160
+ const handle = await fs22.open(file, original === null ? "ax+" : "a+", 384);
5956
6161
  try {
5957
6162
  if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0))) throw new Error(`Shell profile changed during installation: ${profile}`);
5958
6163
  await handle.writeFile(block);
@@ -5976,13 +6181,13 @@ async function removePath(changes) {
5976
6181
  await windowsPath("remove", change.entry, current, change.created);
5977
6182
  continue;
5978
6183
  }
5979
- const stat = await fs21.lstat(change.file).catch((error) => {
6184
+ const stat = await fs22.lstat(change.file).catch((error) => {
5980
6185
  if (error.code === "ENOENT") return null;
5981
6186
  throw error;
5982
6187
  });
5983
6188
  if (!stat) continue;
5984
6189
  if (!stat.isFile()) throw new Error("profile type changed");
5985
- const bytes = await fs21.readFile(change.file), text2 = bytes.toString("utf8");
6190
+ const bytes = await fs22.readFile(change.file), text2 = bytes.toString("utf8");
5986
6191
  if (!Buffer.from(text2).equals(bytes)) throw new Error("profile encoding changed");
5987
6192
  const index2 = text2.indexOf(change.block);
5988
6193
  if (index2 < 0 || text2.indexOf(change.block, index2 + change.block.length) >= 0) throw new Error("PATH block was edited or removed");
@@ -5992,7 +6197,7 @@ async function removePath(changes) {
5992
6197
  const temporary = change.file + ".mason-" + randomUUID7();
5993
6198
  try {
5994
6199
  if (stat.nlink > 1) throw new Error("profile has multiple hard links");
5995
- const handle = await fs21.open(temporary, "wx", stat.mode & 511);
6200
+ const handle = await fs22.open(temporary, "wx", stat.mode & 511);
5996
6201
  try {
5997
6202
  await handle.writeFile(next);
5998
6203
  await handle.chmod(stat.mode & 511);
@@ -6001,11 +6206,11 @@ async function removePath(changes) {
6001
6206
  } finally {
6002
6207
  await handle.close();
6003
6208
  }
6004
- const current = await fs21.lstat(change.file);
6005
- if (!current.isFile() || current.ino !== stat.ino || !(await fs21.readFile(change.file)).equals(bytes)) throw new Error("profile changed during uninstall");
6006
- await fs21.rename(temporary, change.file);
6209
+ const current = await fs22.lstat(change.file);
6210
+ if (!current.isFile() || current.ino !== stat.ino || !(await fs22.readFile(change.file)).equals(bytes)) throw new Error("profile changed during uninstall");
6211
+ await fs22.rename(temporary, change.file);
6007
6212
  } finally {
6008
- await fs21.rm(temporary, { force: true });
6213
+ await fs22.rm(temporary, { force: true });
6009
6214
  }
6010
6215
  } catch (error) {
6011
6216
  warnings.push(`Retained PATH setting ${change.kind === "profile" ? change.file : change.entry}: ${error instanceof Error ? error.message : String(error)}`);
@@ -6047,12 +6252,12 @@ var init_path = __esm({
6047
6252
  "src/distribution/path.ts"() {
6048
6253
  "use strict";
6049
6254
  execute2 = promisify5(execFile5);
6050
- pathChangeSchema = z24.discriminatedUnion("kind", [
6051
- z24.object({ kind: z24.literal("profile"), file: z24.string(), block: z24.string(), created: z24.boolean() }),
6052
- z24.object({ kind: z24.literal("windows"), entry: z24.string(), created: z24.boolean() })
6255
+ pathChangeSchema = z23.discriminatedUnion("kind", [
6256
+ z23.object({ kind: z23.literal("profile"), file: z23.string(), block: z23.string(), created: z23.boolean() }),
6257
+ z23.object({ kind: z23.literal("windows"), entry: z23.string(), created: z23.boolean() })
6053
6258
  ]);
6054
6259
  quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
6055
- normalizeWindowsEntry = (value) => path27.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
6260
+ normalizeWindowsEntry = (value) => path29.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
6056
6261
  hasWindowsEntry = (value, entry, env = process.env) => {
6057
6262
  const variables = new Map(Object.entries(env).map(([key, value2]) => [key.toLowerCase(), value2]));
6058
6263
  const expand = (part) => part.replace(/%([^%]+)%/g, (match, key) => variables.get(key.toLowerCase()) ?? match);
@@ -6061,65 +6266,346 @@ var init_path = __esm({
6061
6266
  }
6062
6267
  });
6063
6268
 
6269
+ // src/distribution/state.ts
6270
+ var state_exports = {};
6271
+ __export(state_exports, {
6272
+ automaticUpdatesAllowed: () => automaticUpdatesAllowed,
6273
+ currentInstallation: () => currentInstallation,
6274
+ defaultHome: () => defaultHome,
6275
+ newerStableVersion: () => newerStableVersion,
6276
+ readInstallation: () => readInstallation,
6277
+ recordSchema: () => recordSchema3,
6278
+ releaseVersion: () => releaseVersion,
6279
+ standaloneLocation: () => standaloneLocation,
6280
+ updatePolicySchema: () => updatePolicySchema
6281
+ });
6282
+ import os4 from "os";
6283
+ import path30 from "path";
6284
+ import { fileURLToPath } from "url";
6285
+ import fs23 from "fs/promises";
6286
+ import { z as z24 } from "zod";
6287
+ async function readInstallation(home) {
6288
+ const raw = await readStoreJson(home, "install.json");
6289
+ if (!raw) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
6290
+ const record = recordSchema3.parse(raw);
6291
+ if (record.home !== await fs23.realpath(home)) throw new Error("Invalid standalone installation receipt.");
6292
+ return record;
6293
+ }
6294
+ function standaloneLocation() {
6295
+ const bundle = path30.resolve(path30.dirname(fileURLToPath(import.meta.url)), "../..");
6296
+ const home = path30.dirname(path30.dirname(bundle));
6297
+ if (path30.dirname(bundle) !== path30.join(home, "versions") || !/^[a-f0-9]{24}$/.test(path30.basename(bundle))) {
6298
+ return null;
6299
+ }
6300
+ return { home, bundle };
6301
+ }
6302
+ async function currentInstallation() {
6303
+ const location2 = standaloneLocation();
6304
+ if (!location2) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
6305
+ const { home, bundle } = location2;
6306
+ const record = await readInstallation(home);
6307
+ return { home, bundle, record };
6308
+ }
6309
+ function automaticUpdatesAllowed(record, env = process.env) {
6310
+ 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;
6311
+ }
6312
+ function newerStableVersion(candidate, installed) {
6313
+ if (!/^\d+\.\d+\.\d+$/.test(candidate)) return false;
6314
+ const a = candidate.split(".").map(BigInt), b = installed.split("-")[0].split(".").map(BigInt);
6315
+ for (let i = 0; i < 3; i++) {
6316
+ if (a[i] !== b[i]) return a[i] > b[i];
6317
+ }
6318
+ return installed.includes("-");
6319
+ }
6320
+ var releaseVersion, bundleId, updatePolicySchema, recordSchema3, defaultHome;
6321
+ var init_state = __esm({
6322
+ "src/distribution/state.ts"() {
6323
+ "use strict";
6324
+ init_storage();
6325
+ init_path();
6326
+ releaseVersion = z24.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/);
6327
+ bundleId = z24.string().regex(/^[a-f0-9]{24}$/);
6328
+ updatePolicySchema = z24.object({
6329
+ enabled: z24.boolean(),
6330
+ revision: z24.string(),
6331
+ pinnedVersion: releaseVersion.optional(),
6332
+ mirror: z24.string().optional()
6333
+ });
6334
+ recordSchema3 = z24.object({
6335
+ format: z24.literal(1),
6336
+ home: z24.string(),
6337
+ bin: z24.string(),
6338
+ current: bundleId,
6339
+ version: releaseVersion,
6340
+ launchers: z24.record(z24.string()),
6341
+ previousLaunchers: z24.record(z24.string()).optional(),
6342
+ pathChanges: z24.array(pathChangeSchema).optional(),
6343
+ updates: updatePolicySchema.optional(),
6344
+ previous: z24.object({ id: bundleId, version: releaseVersion }).optional(),
6345
+ pending: z24.object({ id: bundleId, version: releaseVersion, policyRevision: z24.string() }).optional()
6346
+ });
6347
+ defaultHome = () => process.env.MASON_HOME ?? (process.platform === "win32" ? path30.join(process.env.LOCALAPPDATA ?? os4.homedir(), "Mason") : path30.join(os4.homedir(), ".local/share/mason"));
6348
+ }
6349
+ });
6350
+
6351
+ // src/distribution/bundle.ts
6352
+ import fs24 from "fs/promises";
6353
+ import path31 from "path";
6354
+ import { createHash as createHash5 } from "crypto";
6355
+ import { z as z25 } from "zod";
6356
+ async function readBundle(root) {
6357
+ const bytes = await fs24.readFile(await storePath(root, "bundle.json"));
6358
+ const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
6359
+ const windows = manifest.target.startsWith("win32-");
6360
+ for (const file of Object.keys(manifest.files)) {
6361
+ if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") || path31.isAbsolute(file)) {
6362
+ throw new Error("Unsafe path in Mason bundle: " + file);
6363
+ }
6364
+ }
6365
+ 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"]) {
6366
+ if (!manifest.files[required]) throw new Error("Incomplete Mason bundle: " + required);
6367
+ }
6368
+ return { manifest, manifestHash: sha256(bytes) };
6369
+ }
6370
+ async function verifyBundle(root, expectedHash) {
6371
+ const bundle = await readBundle(root);
6372
+ if (expectedHash && bundle.manifestHash !== expectedHash) throw new Error("Mason bundle manifest changed.");
6373
+ 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.");
6374
+ const files = Object.keys(bundle.manifest.files);
6375
+ for (let i = 0; i < files.length; i += 8) await Promise.all(files.slice(i, i + 8).map(async (file) => {
6376
+ if (sha256(await fs24.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
6377
+ }));
6378
+ return bundle;
6379
+ }
6380
+ var targets, digest2, bundleSchema, sha256;
6381
+ var init_bundle = __esm({
6382
+ "src/distribution/bundle.ts"() {
6383
+ "use strict";
6384
+ init_storage();
6385
+ targets = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
6386
+ digest2 = z25.string().regex(/^[a-f0-9]{64}$/);
6387
+ bundleSchema = z25.object({
6388
+ format: z25.literal(1),
6389
+ version: z25.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
6390
+ target: z25.enum(targets),
6391
+ nodeVersion: z25.string(),
6392
+ files: z25.record(digest2)
6393
+ }).strict();
6394
+ sha256 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
6395
+ }
6396
+ });
6397
+
6398
+ // src/distribution/runtime.ts
6399
+ import fs25 from "fs/promises";
6400
+ import { unlinkSync } from "fs";
6401
+ import os5 from "os";
6402
+ import path32 from "path";
6403
+ import { randomUUID as randomUUID8 } from "crypto";
6404
+ import { z as z26 } from "zod";
6405
+ async function pruneUnusedVersions(home) {
6406
+ const versions = await storePath(home, "versions");
6407
+ const candidates = [];
6408
+ const initial = await readInstallation(home);
6409
+ const initialKeep = /* @__PURE__ */ new Set([initial.current, initial.previous?.id, initial.pending?.id]);
6410
+ for (const name of await fs25.readdir(versions)) {
6411
+ const retired = /^\.retired-([a-f0-9]{24})-[a-f0-9-]+$/.exec(name);
6412
+ const id = retired?.[1] ?? name;
6413
+ if (!/^[a-f0-9]{24}$/.test(id) || initialKeep.has(id)) continue;
6414
+ try {
6415
+ const directory = await storePath(home, `versions/${name}`);
6416
+ await fs25.access(await storePath(directory, "app/dist/mason-runtime.js"));
6417
+ const bundle = await verifyBundle(directory);
6418
+ if (bundle.manifestHash.slice(0, 24) === id && bundle.manifest.files["app/dist/mason-runtime.js"])
6419
+ candidates.push({ id, directory, retired: !!retired });
6420
+ } catch {
6421
+ }
6422
+ }
6423
+ if (!candidates.length) return;
6424
+ const garbage = [];
6425
+ await withLock(home, ".install-lock", async () => {
6426
+ const record = await readInstallation(home);
6427
+ const keep = /* @__PURE__ */ new Set([record.current, record.previous?.id, record.pending?.id]);
6428
+ const directory = await storePath(home, "runtimes");
6429
+ const leases = await fs25.readdir(directory).catch((error) => {
6430
+ if (error.code === "ENOENT") return [];
6431
+ throw error;
6432
+ });
6433
+ for (const name of leases) {
6434
+ if (!/^[a-f0-9-]+\.json$/.test(name)) return;
6435
+ try {
6436
+ const raw = await readStoreJson(home, `runtimes/${name}`);
6437
+ if (raw === null) continue;
6438
+ const lease = leaseSchema.parse(raw);
6439
+ if (lease.host !== os5.hostname()) return;
6440
+ try {
6441
+ process.kill(lease.pid, 0);
6442
+ keep.add(lease.bundle);
6443
+ } catch (error) {
6444
+ if (error.code !== "ESRCH") return;
6445
+ await fs25.rm(await storePath(home, `runtimes/${name}`), { force: true });
6446
+ }
6447
+ } catch {
6448
+ return;
6449
+ }
6450
+ }
6451
+ for (const candidate of candidates) {
6452
+ if (keep.has(candidate.id)) continue;
6453
+ try {
6454
+ const retired = candidate.retired ? candidate.directory : await storePath(home, `versions/.retired-${candidate.id}-${randomUUID8()}`);
6455
+ if (!candidate.retired) await fs25.rename(candidate.directory, retired);
6456
+ garbage.push(retired);
6457
+ } catch {
6458
+ }
6459
+ }
6460
+ });
6461
+ for (const directory of garbage) await fs25.rm(directory, { recursive: true }).catch(() => {
6462
+ });
6463
+ }
6464
+ var leaseSchema;
6465
+ var init_runtime2 = __esm({
6466
+ "src/distribution/runtime.ts"() {
6467
+ "use strict";
6468
+ init_store();
6469
+ init_storage();
6470
+ init_state();
6471
+ init_bundle();
6472
+ leaseSchema = z26.object({ pid: z26.number().int().positive(), host: z26.string(), bundle: z26.string().regex(/^[a-f0-9]{24}$/) });
6473
+ }
6474
+ });
6475
+
6476
+ // src/distribution/sessions.ts
6477
+ import fs26 from "fs/promises";
6478
+ import os6 from "os";
6479
+ import { randomUUID as randomUUID9 } from "crypto";
6480
+ import { z as z27 } from "zod";
6481
+ async function hasRunningMcp(home) {
6482
+ const directory = await storePath(home, "sessions");
6483
+ const files = await fs26.readdir(directory).catch((error) => {
6484
+ if (error.code === "ENOENT") return [];
6485
+ throw error;
6486
+ });
6487
+ for (const name of files) {
6488
+ if (!/^[a-f0-9-]+\.json$/.test(name)) return true;
6489
+ const relative = "sessions/" + name;
6490
+ try {
6491
+ const raw = await readStoreJson(home, relative);
6492
+ if (raw === null) continue;
6493
+ const lease = leaseSchema2.parse(raw);
6494
+ if (lease.host !== os6.hostname()) return true;
6495
+ try {
6496
+ process.kill(lease.pid, 0);
6497
+ return true;
6498
+ } catch (error) {
6499
+ if (error.code !== "ESRCH") return true;
6500
+ }
6501
+ await fs26.rm(await storePath(home, relative), { force: true });
6502
+ } catch {
6503
+ return true;
6504
+ }
6505
+ }
6506
+ return false;
6507
+ }
6508
+ async function registerMcp(home) {
6509
+ const name = `sessions/${randomUUID9()}.json`;
6510
+ await writeStoreJson(home, name, { pid: process.pid, host: os6.hostname() });
6511
+ return async () => {
6512
+ await fs26.rm(await storePath(home, name), { force: true }).catch(() => {
6513
+ });
6514
+ };
6515
+ }
6516
+ var leaseSchema2;
6517
+ var init_sessions = __esm({
6518
+ "src/distribution/sessions.ts"() {
6519
+ "use strict";
6520
+ init_storage();
6521
+ leaseSchema2 = z27.object({ pid: z27.number().int().positive(), host: z27.string() });
6522
+ }
6523
+ });
6524
+
6064
6525
  // src/distribution/install.ts
6065
6526
  var install_exports2 = {};
6066
6527
  __export(install_exports2, {
6067
6528
  WINDOWS_BATCH_LAUNCHER: () => WINDOWS_BATCH_LAUNCHER,
6068
6529
  defaultHome: () => defaultHome,
6069
6530
  installStandalone: () => installStandalone,
6531
+ prepareStandaloneMcp: () => prepareStandaloneMcp,
6532
+ rollbackStandalone: () => rollbackStandalone,
6070
6533
  uninstallStandalone: () => uninstallStandalone,
6071
6534
  upgradeStandalone: () => upgradeStandalone
6072
6535
  });
6073
- import fs22 from "fs/promises";
6074
- import os4 from "os";
6075
- import path28 from "path";
6076
- import { fileURLToPath } from "url";
6077
- import { spawn } from "child_process";
6078
- import { randomUUID as randomUUID8 } from "crypto";
6079
- import { z as z25 } from "zod";
6536
+ import fs27 from "fs/promises";
6537
+ import os7 from "os";
6538
+ import path33 from "path";
6539
+ import { execFile as execFile6, spawn } from "child_process";
6540
+ import { promisify as promisify6 } from "util";
6541
+ import { randomUUID as randomUUID10 } from "crypto";
6080
6542
  async function replace(file, content2, mode = 384) {
6081
- const temp = file + ".tmp-" + randomUUID8();
6543
+ const temp = file + ".tmp-" + randomUUID10();
6082
6544
  try {
6083
- await fs22.writeFile(temp, content2, { mode, flag: "wx" });
6084
- await fs22.rename(temp, file);
6545
+ await fs27.writeFile(temp, content2, { mode, flag: "wx" });
6546
+ await fs27.rename(temp, file);
6085
6547
  } finally {
6086
- await fs22.rm(temp, { force: true });
6548
+ await fs27.rm(temp, { force: true });
6087
6549
  }
6088
6550
  }
6089
- async function installStandalone(source, progress2) {
6551
+ async function installStandalone(source, progress2, options = {}) {
6090
6552
  progress2?.step("Checking installation files and location");
6091
6553
  const bundle = await verifyBundle(source);
6092
- const selectedHome = path28.resolve(defaultHome());
6093
- await fs22.mkdir(selectedHome, { recursive: true });
6094
- const home = await fs22.realpath(selectedHome);
6095
- const selectedBin = path28.resolve(process.env.MASON_BIN_DIR ?? (process.platform === "win32" ? path28.join(home, "bin") : path28.join(os4.homedir(), ".local/bin")));
6096
- return withLock(home, ".install-lock", async () => {
6097
- const raw = await readStoreJson(home, "install.json");
6098
- const previous = raw === null ? null : recordSchema3.parse(raw);
6099
- if (!previous) for (const file of await fs22.readdir(home)) {
6100
- if (file === ".install-lock") continue;
6101
- const directory = path28.join(home, file);
6102
- const stat = await fs22.lstat(directory);
6103
- if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs22.readdir(directory)).length) {
6104
- throw new Error("Mason installation directory is not empty and has no ownership receipt.");
6105
- }
6106
- }
6107
- await fs22.mkdir(selectedBin, { recursive: true });
6108
- const bin = await fs22.realpath(selectedBin);
6109
- if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
6110
- const id = bundle.manifestHash.slice(0, 24);
6111
- const destination = await storePath(home, "versions/" + id);
6112
- const launchers = process.platform === "win32" ? {
6113
- "mason.cmd": WINDOWS_BATCH_LAUNCHER,
6114
- "mason-launcher.ps1": `$ErrorActionPreference = 'Stop'
6554
+ const selectedHome = path33.resolve(defaultHome());
6555
+ if (!options.stageOnly) await fs27.mkdir(selectedHome, { recursive: true });
6556
+ const home = await fs27.realpath(selectedHome);
6557
+ if (options.stageOnly) {
6558
+ assertUpdatePolicy(await readInstallation(home), bundle.manifest.version, options.policyRevision);
6559
+ const destination = await storePath(home, "versions/" + bundle.manifestHash.slice(0, 24));
6560
+ await copyBundle(home, source, destination, bundle);
6561
+ const { stdout } = await promisify6(execFile6)(
6562
+ path33.join(destination, process.platform === "win32" ? "node.exe" : "node"),
6563
+ [path33.join(destination, "app/dist/mason.js"), "--version"],
6564
+ { timeout: 3e4, windowsHide: true, env: { ...process.env, MASON_NO_AUTO_UPDATE: "1" } }
6565
+ );
6566
+ if (stdout.trim() !== bundle.manifest.version) throw new Error("Downloaded Mason failed its version health check.");
6567
+ }
6568
+ return withLock(home, ".install-lock", () => installLocked2(home, source, bundle, progress2, options));
6569
+ }
6570
+ function assertUpdatePolicy(record, version, revision) {
6571
+ if (!record || !automaticUpdatesAllowed(record, { ...process.env, MASON_VERSION: "" }) || record.updates?.revision !== revision || !newerStableVersion(version, record.version)) {
6572
+ throw new Error("Update policy or installed version changed; download was not activated.");
6573
+ }
6574
+ }
6575
+ async function installLocked2(home, source, bundle, progress2, options, alreadyStaged = false) {
6576
+ const raw = await readStoreJson(home, "install.json");
6577
+ const previous = raw === null ? null : recordSchema3.parse(raw);
6578
+ if (options.stageOnly) assertUpdatePolicy(previous, bundle.manifest.version, options.policyRevision);
6579
+ if (!previous) for (const file of await fs27.readdir(home)) {
6580
+ if (file === ".install-lock") continue;
6581
+ const directory = path33.join(home, file);
6582
+ const stat = await fs27.lstat(directory);
6583
+ if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs27.readdir(directory)).length) {
6584
+ throw new Error("Mason installation directory is not empty and has no ownership receipt.");
6585
+ }
6586
+ }
6587
+ const selectedBin = path33.resolve(process.env.MASON_BIN_DIR ?? previous?.bin ?? (process.platform === "win32" ? path33.join(home, "bin") : path33.join(os7.homedir(), ".local/bin")));
6588
+ await fs27.mkdir(selectedBin, { recursive: true });
6589
+ const bin = await fs27.realpath(selectedBin);
6590
+ if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
6591
+ const id = bundle.manifestHash.slice(0, 24);
6592
+ const destination = await storePath(home, "versions/" + id);
6593
+ if (options.stageOnly) {
6594
+ const receipt2 = { ...previous, pending: { id, version: bundle.manifest.version, policyRevision: options.policyRevision } };
6595
+ await writeStoreJson(home, "install.json", receipt2);
6596
+ return { home, bin, version: bundle.manifest.version, previousVersion: previous.version, path: { message: "Update staged for a future MCP launch." } };
6597
+ }
6598
+ const launchers = process.platform === "win32" ? {
6599
+ "mason.cmd": WINDOWS_BATCH_LAUNCHER,
6600
+ "mason-launcher.ps1": `$ErrorActionPreference = 'Stop'
6115
6601
  $previousToken = $env:MASON_UNINSTALL_TOKEN
6116
6602
  $token = [Guid]::NewGuid().ToString()
6117
6603
  $env:MASON_UNINSTALL_TOKEN = $token
6118
6604
  try {
6119
- & ${psQuote(path28.join(destination, "node.exe"))} ${psQuote(path28.join(destination, "app/dist/mason.js"))} @args
6605
+ & ${psQuote(path33.join(destination, "node.exe"))} ${psQuote(path33.join(destination, "app/dist/mason.js"))} @args
6120
6606
  $code = $LASTEXITCODE
6121
6607
  if ($code -eq 0 -and $args.Count -eq 1 -and $args[0] -eq 'uninstall') {
6122
- if ([IO.File]::ReadAllText(${psQuote(path28.join(home, ".uninstall-request.txt"))}) -cne $token) { throw 'Mason did not authorize installation cleanup.' }
6608
+ if ([IO.File]::ReadAllText(${psQuote(path33.join(home, ".uninstall-request.txt"))}) -cne $token) { throw 'Mason did not authorize installation cleanup.' }
6123
6609
  Remove-Item -LiteralPath ${psQuote(home)} -Recurse -Force
6124
6610
  Write-Output ${psQuote("Removed standalone installation: " + home)}
6125
6611
  }
@@ -6127,88 +6613,207 @@ try {
6127
6613
  } catch { [Console]::Error.WriteLine($_.Exception.Message); exit 2 }
6128
6614
  finally { $env:MASON_UNINSTALL_TOKEN = $previousToken }
6129
6615
  `
6130
- } : { mason: `#!/bin/sh
6131
- exec ${shQuote(path28.join(destination, "node"))} ${shQuote(path28.join(destination, "app/dist/mason.js"))} "$@"
6616
+ } : { mason: `#!/bin/sh
6617
+ exec ${shQuote(path33.join(destination, "node"))} ${shQuote(path33.join(destination, "app/dist/mason.js"))} "$@"
6132
6618
  ` };
6133
- for (const [name, content2] of Object.entries(launchers)) {
6134
- const file = await storePath(bin, name);
6135
- const old = await fs22.readFile(file, "utf8").catch((error) => {
6136
- if (error.code === "ENOENT") return null;
6137
- throw error;
6138
- });
6139
- 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);
6619
+ for (const [name, content2] of Object.entries(launchers)) {
6620
+ const file = await storePath(bin, name);
6621
+ const old = await fs27.readFile(file, "utf8").catch((error) => {
6622
+ if (error.code === "ENOENT") return null;
6623
+ throw error;
6624
+ });
6625
+ 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);
6626
+ }
6627
+ const retired = Object.entries({ ...previous?.previousLaunchers, ...previous?.launchers }).filter(([name]) => !(name in launchers));
6628
+ const checkRetired = async (name, expected) => {
6629
+ if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
6630
+ const actual = await fs27.readFile(await storePath(bin, name), "utf8").catch((error) => {
6631
+ if (error.code === "ENOENT") return null;
6632
+ throw error;
6633
+ });
6634
+ if (actual !== null && actual !== expected) throw new Error("Retired launcher was edited; retained: " + name);
6635
+ };
6636
+ for (const [name, expected] of retired) await checkRetired(name, expected);
6637
+ progress2?.step(`Installing Mason ${bundle.manifest.version}`);
6638
+ const updates = options.preservePolicy && previous?.updates ? { ...previous.updates } : {
6639
+ ...previous?.updates ?? { enabled: !previous && !process.env.CI && process.env.MASON_NO_AUTO_UPDATE !== "1" },
6640
+ revision: randomUUID10(),
6641
+ pinnedVersion: process.env.MASON_VERSION?.replace(/^v/, "") || void 0,
6642
+ mirror: process.env.MASON_RELEASE_BASE || previous?.updates?.mirror
6643
+ };
6644
+ if (options.rollback) {
6645
+ updates.pinnedVersion = bundle.manifest.version;
6646
+ updates.revision = randomUUID10();
6647
+ }
6648
+ const receipt = {
6649
+ format: 1,
6650
+ home,
6651
+ bin,
6652
+ current: id,
6653
+ version: bundle.manifest.version,
6654
+ launchers,
6655
+ pathChanges: previous?.pathChanges ?? [],
6656
+ updates,
6657
+ previous: previous && previous.current !== id ? { id: previous.current, version: previous.version } : previous?.previous
6658
+ };
6659
+ await writeStoreJson(home, "install.json", previous ? { ...previous, previousLaunchers: launchers } : receipt);
6660
+ try {
6661
+ if (!alreadyStaged) await copyBundle(home, source, destination, bundle);
6662
+ for (const [name, content2] of Object.entries(launchers)) await replace(await storePath(bin, name, true), content2, 493);
6663
+ for (const [name, expected] of retired) {
6664
+ await checkRetired(name, expected);
6665
+ await fs27.rm(await storePath(bin, name), { force: true });
6140
6666
  }
6141
- const retired = Object.entries({ ...previous?.previousLaunchers, ...previous?.launchers }).filter(([name]) => !(name in launchers));
6142
- const checkRetired = async (name, expected) => {
6143
- if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
6144
- const actual = await fs22.readFile(await storePath(bin, name), "utf8").catch((error) => {
6145
- if (error.code === "ENOENT") return null;
6146
- throw error;
6147
- });
6148
- if (actual !== null && actual !== expected) throw new Error("Retired launcher was edited; retained: " + name);
6149
- };
6150
- for (const [name, expected] of retired) await checkRetired(name, expected);
6151
- progress2?.step(`Installing Mason ${bundle.manifest.version}`);
6152
- const receipt = { format: 1, home, bin, current: id, version: bundle.manifest.version, launchers, pathChanges: previous?.pathChanges ?? [] };
6153
- await writeStoreJson(home, "install.json", { ...receipt, previousLaunchers: previous?.previousLaunchers ?? previous?.launchers });
6154
- const stage = await storePath(home, "versions/.install-" + randomUUID8(), true);
6155
- await fs22.mkdir(stage);
6667
+ await writeStoreJson(home, "install.json", receipt);
6668
+ } catch (error) {
6669
+ if (previous) {
6670
+ for (const [name, content2] of Object.entries(previous.launchers)) await replace(await storePath(bin, name, true), content2, 493);
6671
+ await writeStoreJson(home, "install.json", previous);
6672
+ }
6673
+ throw error;
6674
+ }
6675
+ if (options.preservePolicy) return { home, bin, version: receipt.version, previousVersion: previous?.version, path: { message: "Installation selected." } };
6676
+ progress2?.step("Checking terminal command availability");
6677
+ const pathResult = await configurePath(bin, receipt.pathChanges ?? [], async (changes) => {
6678
+ receipt.pathChanges = changes;
6679
+ await writeStoreJson(home, "install.json", receipt);
6680
+ });
6681
+ return { home, bin, version: bundle.manifest.version, previousVersion: previous?.version, path: pathResult };
6682
+ }
6683
+ async function copyBundle(home, source, destination, bundle) {
6684
+ const exists = await fs27.lstat(destination).then(() => true, (error) => {
6685
+ if (error.code === "ENOENT") return false;
6686
+ throw error;
6687
+ });
6688
+ if (exists) {
6689
+ await verifyBundle(destination, bundle.manifestHash);
6690
+ return;
6691
+ }
6692
+ const stage = await storePath(home, "versions/.install-" + randomUUID10(), true);
6693
+ await fs27.mkdir(stage);
6694
+ try {
6695
+ for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
6696
+ const from = await storePath(source, file), to = await storePath(stage, file, true);
6697
+ await fs27.copyFile(from, to);
6698
+ await fs27.chmod(to, (await fs27.stat(from)).mode & 511);
6699
+ }
6700
+ await verifyBundle(stage, bundle.manifestHash);
6156
6701
  try {
6157
- for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
6158
- const from = await storePath(source, file), to = await storePath(stage, file, true);
6159
- await fs22.copyFile(from, to);
6160
- await fs22.chmod(to, (await fs22.stat(from)).mode & 511);
6161
- }
6162
- await verifyBundle(stage, bundle.manifestHash);
6163
- const exists = await fs22.lstat(destination).then(() => true, (error) => {
6164
- if (error.code === "ENOENT") return false;
6702
+ await fs27.rename(stage, destination);
6703
+ } catch (error) {
6704
+ try {
6705
+ await verifyBundle(destination, bundle.manifestHash);
6706
+ } catch {
6165
6707
  throw error;
6166
- });
6167
- if (exists) await verifyBundle(destination, bundle.manifestHash);
6168
- else await fs22.rename(stage, destination);
6169
- for (const [name, content2] of Object.entries(launchers)) await replace(await storePath(bin, name, true), content2, 493);
6170
- for (const [name, expected] of retired) {
6171
- await checkRetired(name, expected);
6172
- await fs22.rm(await storePath(bin, name), { force: true });
6173
6708
  }
6174
- await writeStoreJson(home, "install.json", receipt);
6175
- } finally {
6176
- await fs22.rm(stage, { recursive: true, force: true });
6177
6709
  }
6178
- progress2?.step("Checking terminal command availability");
6179
- const pathResult = await configurePath(bin, receipt.pathChanges, async (changes) => {
6180
- receipt.pathChanges = changes;
6181
- await writeStoreJson(home, "install.json", receipt);
6182
- });
6183
- return { home, bin, version: bundle.manifest.version, previousVersion: previous?.version, path: pathResult };
6710
+ } finally {
6711
+ await fs27.rm(stage, { recursive: true, force: true });
6712
+ }
6713
+ }
6714
+ async function prepareStandaloneMcp(home) {
6715
+ const before = await readInstallation(home), candidate = before.pending;
6716
+ let verified, verificationError;
6717
+ if (candidate && automaticUpdatesAllowed(before) && !await hasRunningMcp(home)) {
6718
+ try {
6719
+ verified = await verifyBundle(await storePath(home, "versions/" + candidate.id));
6720
+ if (verified.manifestHash.slice(0, 24) !== candidate.id || verified.manifest.version !== candidate.version) throw new Error("Staged update changed.");
6721
+ } catch (error) {
6722
+ verificationError = error;
6723
+ }
6724
+ }
6725
+ return withLock(home, ".install-lock", async () => {
6726
+ let record = await readInstallation(home);
6727
+ const pending = record.pending;
6728
+ if (pending && candidate?.id === pending.id && (verified || verificationError) && automaticUpdatesAllowed(record) && pending.policyRevision === record.updates?.revision && newerStableVersion(pending.version, record.version) && !await hasRunningMcp(home)) {
6729
+ try {
6730
+ if (verificationError) throw verificationError;
6731
+ const source = await storePath(home, "versions/" + pending.id);
6732
+ await installLocked2(home, source, verified, void 0, { preservePolicy: true }, true);
6733
+ record = await readInstallation(home);
6734
+ } catch (error) {
6735
+ record = await readInstallation(home);
6736
+ delete record.pending;
6737
+ await writeStoreJson(home, "install.json", record);
6738
+ await writeStoreJson(home, "update-status.json", { lastAttempt: Date.now(), error: String(error) });
6739
+ }
6740
+ }
6741
+ const release = await registerMcp(home);
6742
+ return { bundle: await storePath(home, "versions/" + record.current), release };
6184
6743
  });
6185
6744
  }
6186
- async function currentInstallation() {
6187
- const app = path28.dirname(path28.dirname(fileURLToPath(import.meta.url)));
6188
- const bundle = path28.dirname(app), home = path28.dirname(path28.dirname(bundle));
6189
- const raw = await readStoreJson(home, "install.json");
6190
- if (!raw) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
6191
- const record = recordSchema3.parse(raw);
6192
- if (record.home !== await fs22.realpath(home) || path28.dirname(bundle) !== path28.join(home, "versions")) throw new Error("Invalid standalone installation receipt.");
6193
- return { home, bundle, record };
6745
+ async function rollbackStandalone() {
6746
+ const { home } = await currentInstallation();
6747
+ return withLock(home, ".install-lock", async () => {
6748
+ const record = await readInstallation(home);
6749
+ if (!record.previous) throw new Error("No previous installation is available.");
6750
+ if (await hasRunningMcp(home)) throw new Error("Close running Mason MCP sessions before rolling back.");
6751
+ const source = await storePath(home, "versions/" + record.previous.id);
6752
+ const bundle = await verifyBundle(source);
6753
+ if (bundle.manifestHash.slice(0, 24) !== record.previous.id || bundle.manifest.version !== record.previous.version) throw new Error("Previous installation changed.");
6754
+ return installLocked2(home, source, bundle, void 0, { preservePolicy: true, rollback: true });
6755
+ });
6194
6756
  }
6195
6757
  async function upgradeStandalone(version, progress2) {
6196
6758
  if (version && !/^v?\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(version)) throw new Error("Expected a release version such as 0.14.0.");
6197
6759
  progress2?.step("Checking current installation");
6198
6760
  const { home, bundle, record } = await currentInstallation();
6199
6761
  await verifyBundle(bundle);
6762
+ const mirror = process.env.MASON_RELEASE_BASE || record.updates?.mirror || "";
6763
+ if (mirror && !version) throw new Error("Specify a version when upgrading from a company mirror; no public latest-version lookup was made.");
6764
+ let selected = version?.replace(/^v/, "");
6765
+ if (!selected) {
6766
+ progress2?.step("Checking the latest release");
6767
+ const response = await fetch("https://github.com/adrianczuczka/mason/releases/latest", {
6768
+ method: "HEAD",
6769
+ redirect: "follow",
6770
+ signal: AbortSignal.timeout(3e4)
6771
+ });
6772
+ if (!response.ok) throw new Error(`Latest release request failed (HTTP ${response.status}).`);
6773
+ selected = new URL(response.url).pathname.split("/").pop()?.replace(/^v/, "");
6774
+ if (!selected || !/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(selected)) throw new Error("Invalid Mason release version.");
6775
+ }
6776
+ const alreadyInstalled = await withLock(home, ".install-lock", async () => {
6777
+ const current = await readInstallation(home);
6778
+ if (current.version !== selected) return false;
6779
+ await verifyBundle(await storePath(home, "versions/" + current.current));
6780
+ current.updates = {
6781
+ ...current.updates ?? { enabled: false },
6782
+ revision: randomUUID10(),
6783
+ pinnedVersion: version ? selected : void 0,
6784
+ mirror: mirror || void 0
6785
+ };
6786
+ delete current.pending;
6787
+ await writeStoreJson(home, "install.json", current);
6788
+ return true;
6789
+ });
6790
+ if (alreadyInstalled) {
6791
+ progress2?.stop();
6792
+ console.log(`Mason ${selected} is already installed.`);
6793
+ return 0;
6794
+ }
6200
6795
  const windows = process.platform === "win32";
6201
- const script = path28.join(bundle, windows ? "install.ps1" : "install.sh");
6796
+ const script = path33.join(bundle, windows ? "install.ps1" : "install.sh");
6202
6797
  progress2?.stop();
6203
- return new Promise((resolve, reject) => {
6798
+ const code = await new Promise((resolve, reject) => {
6204
6799
  const child = spawn(
6205
6800
  windows ? "powershell.exe" : "sh",
6206
- windows ? ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", script] : [script],
6207
- { stdio: "inherit", windowsHide: true, env: { ...process.env, MASON_HOME: home, MASON_BIN_DIR: record.bin, MASON_VERSION: version ?? "" } }
6801
+ windows ? ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", script, selected] : [script, selected],
6802
+ { stdio: "inherit", windowsHide: true, env: {
6803
+ ...process.env,
6804
+ MASON_HOME: home,
6805
+ MASON_BIN_DIR: record.bin,
6806
+ MASON_RELEASE_BASE: process.env.MASON_RELEASE_BASE || record.updates?.mirror || "",
6807
+ MASON_VERSION: version ?? "",
6808
+ MASON_UPDATE_POLICY_REVISION: "",
6809
+ MASON_EXPECTED_SHA256: ""
6810
+ } }
6208
6811
  );
6209
6812
  child.on("error", reject);
6210
- child.on("exit", (code) => resolve(code ?? 2));
6813
+ child.on("exit", (code2) => resolve(code2 ?? 2));
6211
6814
  });
6815
+ if (code === 0) console.log("Restart running assistants to use this installation. Existing project integrations use mason from PATH.");
6816
+ return code;
6212
6817
  }
6213
6818
  async function uninstallStandalone() {
6214
6819
  const { home, record } = await currentInstallation();
@@ -6220,7 +6825,7 @@ async function uninstallStandalone() {
6220
6825
  for (const [name, expected] of Object.entries(record.launchers)) {
6221
6826
  if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
6222
6827
  const file = await storePath(record.bin, name);
6223
- const actual = await fs22.readFile(file, "utf8").catch((error) => {
6828
+ const actual = await fs27.readFile(file, "utf8").catch((error) => {
6224
6829
  if (error.code === "ENOENT") return null;
6225
6830
  throw error;
6226
6831
  });
@@ -6228,12 +6833,12 @@ async function uninstallStandalone() {
6228
6833
  }
6229
6834
  for (const warning of await removePath(record.pathChanges ?? [])) console.error(warning);
6230
6835
  if (process.platform === "win32") await replace(await storePath(home, ".uninstall-request.txt", true), token);
6231
- for (const name of Object.keys(record.launchers)) await fs22.rm(await storePath(record.bin, name), { force: true });
6836
+ for (const name of Object.keys(record.launchers)) await fs27.rm(await storePath(record.bin, name), { force: true });
6232
6837
  });
6233
- if (process.platform !== "win32") await fs22.rm(home, { recursive: true, force: true });
6838
+ if (process.platform !== "win32") await fs27.rm(home, { recursive: true, force: true });
6234
6839
  return home;
6235
6840
  }
6236
- var recordSchema3, shQuote, psQuote, WINDOWS_BATCH_LAUNCHER, launcherNames, defaultHome;
6841
+ var shQuote, psQuote, WINDOWS_BATCH_LAUNCHER, launcherNames;
6237
6842
  var init_install2 = __esm({
6238
6843
  "src/distribution/install.ts"() {
6239
6844
  "use strict";
@@ -6241,21 +6846,254 @@ var init_install2 = __esm({
6241
6846
  init_storage();
6242
6847
  init_store();
6243
6848
  init_path();
6244
- recordSchema3 = z25.object({
6245
- format: z25.literal(1),
6246
- home: z25.string(),
6247
- bin: z25.string(),
6248
- current: z25.string().regex(/^[a-f0-9]{24}$/),
6249
- version: z25.string(),
6250
- launchers: z25.record(z25.string()),
6251
- previousLaunchers: z25.record(z25.string()).optional(),
6252
- pathChanges: z25.array(pathChangeSchema).optional()
6253
- });
6849
+ init_state();
6850
+ init_sessions();
6851
+ init_state();
6254
6852
  shQuote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
6255
6853
  psQuote = (s) => "'" + s.replace(/'/g, "''") + "'";
6256
6854
  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';
6257
6855
  launcherNames = ["mason", "mason.cmd", "mason.ps1", "mason-launcher.ps1"];
6258
- defaultHome = () => process.env.MASON_HOME ?? (process.platform === "win32" ? path28.join(process.env.LOCALAPPDATA ?? os4.homedir(), "Mason") : path28.join(os4.homedir(), ".local/share/mason"));
6856
+ }
6857
+ });
6858
+
6859
+ // src/distribution/updates.ts
6860
+ var updates_exports = {};
6861
+ __export(updates_exports, {
6862
+ CHECK_INTERVAL_MS: () => CHECK_INTERVAL_MS,
6863
+ RELEASE_AGE_MS: () => RELEASE_AGE_MS,
6864
+ fetchVerifiedRelease: () => fetchVerifiedRelease,
6865
+ runAutomaticUpdate: () => runAutomaticUpdate,
6866
+ scheduleAutomaticUpdate: () => scheduleAutomaticUpdate,
6867
+ startMcpWithUpdates: () => startMcpWithUpdates,
6868
+ updateManifestSchema: () => updateManifestSchema,
6869
+ updateSettings: () => updateSettings,
6870
+ updateStatus: () => updateStatus
6871
+ });
6872
+ import fs28 from "fs/promises";
6873
+ import path34 from "path";
6874
+ import { spawn as spawn2 } from "child_process";
6875
+ import { randomUUID as randomUUID11 } from "crypto";
6876
+ import { z as z28 } from "zod";
6877
+ async function boundedDownload(url) {
6878
+ const response = await fetch(url, { signal: AbortSignal.timeout(15e3), redirect: "follow" });
6879
+ if (!response.ok || !response.body) throw new Error(`Update metadata request failed (HTTP ${response.status}).`);
6880
+ if (Number(response.headers.get("content-length")) > 1024 * 1024) throw new Error("Update metadata is too large.");
6881
+ const reader = response.body.getReader(), chunks = [];
6882
+ let size = 0;
6883
+ try {
6884
+ for (; ; ) {
6885
+ const { value, done } = await reader.read();
6886
+ if (done) break;
6887
+ size += value.length;
6888
+ if (size > 1024 * 1024) throw new Error("Update metadata is too large.");
6889
+ chunks.push(value);
6890
+ }
6891
+ } finally {
6892
+ await reader.cancel();
6893
+ }
6894
+ return Buffer.concat(chunks);
6895
+ }
6896
+ async function fetchVerifiedRelease(home, now = Date.now()) {
6897
+ const releases = releaseListSchema.parse(JSON.parse((await boundedDownload("https://api.github.com/repos/adrianczuczka/mason/releases?per_page=30")).toString("utf8")));
6898
+ const installed = (await readInstallation(home)).version;
6899
+ let selected;
6900
+ for (const release of releases) {
6901
+ const version = release.tag_name.replace(/^v/, "");
6902
+ if (release.tag_name !== "v" + version || release.draft || release.prerelease || !release.published_at || !(now - Date.parse(release.published_at) >= RELEASE_AGE_MS) || !newerStableVersion(version, installed)) continue;
6903
+ if (!selected || newerStableVersion(version, selected)) selected = version;
6904
+ }
6905
+ if (!selected) return null;
6906
+ const bytes = await boundedDownload(`${releaseBase}/download/v${selected}/update.json`);
6907
+ const manifest = updateManifestSchema.parse(JSON.parse(bytes.toString("utf8")));
6908
+ if (manifest.version !== selected) throw new Error("Signed manifest does not match the selected release.");
6909
+ const signature = JSON.parse((await boundedDownload(`${releaseBase}/download/v${manifest.version}/update.sigstore.json`)).toString("utf8"));
6910
+ if (!signature?.messageSignature || signature.dsseEnvelope) throw new Error("Update metadata requires a detached message signature.");
6911
+ const { verify } = await import("sigstore");
6912
+ await verify(signature, bytes, {
6913
+ certificateIdentityURI: `https://github.com/adrianczuczka/mason/.github/workflows/publish.yml@refs/tags/v${manifest.version}`,
6914
+ certificateIssuer: "https://token.actions.githubusercontent.com",
6915
+ tufCachePath: await storePath(home, "sigstore-cache"),
6916
+ timeout: 1e4,
6917
+ retry: 0,
6918
+ ctLogThreshold: 1,
6919
+ tlogThreshold: 1
6920
+ });
6921
+ return manifest;
6922
+ }
6923
+ async function runInstaller(home, version, digest3, revision) {
6924
+ const record = await readInstallation(home);
6925
+ const bundle = await storePath(home, "versions/" + record.current);
6926
+ await verifyBundle(bundle);
6927
+ const windows = process.platform === "win32", script = path34.join(bundle, windows ? "install.ps1" : "install.sh");
6928
+ await new Promise((resolve, reject) => {
6929
+ const child = spawn2(windows ? "powershell.exe" : "sh", windows ? ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", script] : [script], {
6930
+ cwd: home,
6931
+ windowsHide: true,
6932
+ stdio: ["ignore", "ignore", "pipe"],
6933
+ timeout: 10 * 60 * 1e3,
6934
+ env: {
6935
+ ...process.env,
6936
+ MASON_HOME: home,
6937
+ MASON_BIN_DIR: record.bin,
6938
+ MASON_VERSION: version,
6939
+ MASON_RELEASE_BASE: "",
6940
+ MASON_EXPECTED_SHA256: digest3,
6941
+ MASON_UPDATE_POLICY_REVISION: revision
6942
+ }
6943
+ });
6944
+ let diagnostic = "";
6945
+ child.stderr.on("data", (bytes) => {
6946
+ diagnostic = (diagnostic + bytes).slice(-4e3);
6947
+ });
6948
+ child.on("error", reject);
6949
+ child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`Update download failed (${code}): ${diagnostic.trim()}`)));
6950
+ });
6951
+ }
6952
+ async function runAutomaticUpdate(home, dependencies = { fetchRelease: fetchVerifiedRelease, install: runInstaller }, now = Date.now()) {
6953
+ await withLock(home, ".update-lock", async () => {
6954
+ const record = await readInstallation(home);
6955
+ if (!automaticUpdatesAllowed(record)) return;
6956
+ const old = statusSchema2.safeParse(await readStoreJson(home, "update-status.json"));
6957
+ if (old.success && now - old.data.lastAttempt < CHECK_INTERVAL_MS && old.data.lastAttempt <= now) return;
6958
+ const status = { lastAttempt: now };
6959
+ await writeStoreJson(home, "update-status.json", status);
6960
+ try {
6961
+ await pruneUnusedVersions(home).catch(() => {
6962
+ });
6963
+ const release = await dependencies.fetchRelease(home);
6964
+ if (!release) return;
6965
+ status.availableVersion = release.version;
6966
+ const age = now - Date.parse(release.publishedAt);
6967
+ if (age >= RELEASE_AGE_MS && newerStableVersion(release.version, record.version) && (!record.pending || newerStableVersion(release.version, record.pending.version))) {
6968
+ const asset = `mason-${process.platform}-${process.arch}.${process.platform === "win32" ? "zip" : "tar.gz"}`;
6969
+ const digest3 = release.assets[asset];
6970
+ if (!digest3) throw new Error("Signed release has no archive for this platform.");
6971
+ const fresh = await readInstallation(home);
6972
+ if (automaticUpdatesAllowed(fresh) && fresh.updates?.revision === record.updates.revision && newerStableVersion(release.version, fresh.version)) {
6973
+ await dependencies.install(home, release.version, digest3, record.updates.revision);
6974
+ }
6975
+ }
6976
+ } catch (error) {
6977
+ status.error = error instanceof Error ? error.message : String(error);
6978
+ }
6979
+ if (await fs28.access(path34.join(home, "install.json")).then(() => true, () => false)) await writeStoreJson(home, "update-status.json", status);
6980
+ }, 0);
6981
+ }
6982
+ async function scheduleAutomaticUpdate() {
6983
+ try {
6984
+ const { home, record } = await currentInstallation();
6985
+ if (!automaticUpdatesAllowed(record)) return;
6986
+ const status = statusSchema2.safeParse(await readStoreJson(home, "update-status.json"));
6987
+ const now = Date.now();
6988
+ if (status.success && status.data.lastAttempt <= now && now - status.data.lastAttempt < CHECK_INTERVAL_MS) return;
6989
+ const bundle = await storePath(home, "versions/" + record.current);
6990
+ const child = spawn2(
6991
+ path34.join(bundle, process.platform === "win32" ? "node.exe" : "node"),
6992
+ [path34.join(bundle, "app/dist/mason.js"), "internal-update"],
6993
+ { cwd: home, env: process.env, stdio: "ignore", detached: true, windowsHide: true }
6994
+ );
6995
+ child.on("error", () => {
6996
+ });
6997
+ child.unref();
6998
+ } catch {
6999
+ }
7000
+ }
7001
+ async function updateSettings(home, action) {
7002
+ return withLock(home, ".install-lock", async () => {
7003
+ const record = await readInstallation(home);
7004
+ const updates = { ...record.updates ?? { enabled: false }, revision: randomUUID11() };
7005
+ if (action === "enable") updates.enabled = true;
7006
+ if (action === "disable") updates.enabled = false;
7007
+ if (action === "pin") updates.pinnedVersion = record.version;
7008
+ if (action === "unpin") delete updates.pinnedVersion;
7009
+ record.updates = updates;
7010
+ delete record.pending;
7011
+ await writeStoreJson(home, "install.json", record);
7012
+ return record;
7013
+ });
7014
+ }
7015
+ async function updateStatus(home) {
7016
+ const record = await readInstallation(home);
7017
+ const lastCheck = statusSchema2.parse(await readStoreJson(home, "update-status.json") ?? { lastAttempt: 0 });
7018
+ return {
7019
+ version: record.version,
7020
+ enabled: record.updates?.enabled ?? false,
7021
+ effective: automaticUpdatesAllowed(record),
7022
+ pinnedVersion: record.updates?.pinnedVersion ?? null,
7023
+ mirror: record.updates?.mirror ?? null,
7024
+ pendingVersion: record.pending?.version ?? null,
7025
+ lastCheck: lastCheck.lastAttempt ? lastCheck : null
7026
+ };
7027
+ }
7028
+ async function startMcpWithUpdates(start) {
7029
+ const current = standaloneLocation();
7030
+ if (!current) {
7031
+ await start();
7032
+ return;
7033
+ }
7034
+ const session = await prepareStandaloneMcp(current.home);
7035
+ if (session.bundle !== current.bundle) {
7036
+ const child = spawn2(
7037
+ path34.join(session.bundle, process.platform === "win32" ? "node.exe" : "node"),
7038
+ [path34.join(session.bundle, "app/dist/mason-mcp.js"), ...process.argv.slice(2)],
7039
+ { stdio: "inherit", windowsHide: true }
7040
+ );
7041
+ const interrupt = () => {
7042
+ child.kill("SIGINT");
7043
+ }, terminate = () => {
7044
+ child.kill("SIGTERM");
7045
+ };
7046
+ process.on("SIGINT", interrupt);
7047
+ process.on("SIGTERM", terminate);
7048
+ try {
7049
+ process.exitCode = await new Promise((resolve, reject) => {
7050
+ child.on("error", reject);
7051
+ child.on("exit", (code) => resolve(code ?? 1));
7052
+ });
7053
+ } finally {
7054
+ process.off("SIGINT", interrupt);
7055
+ process.off("SIGTERM", terminate);
7056
+ await session.release();
7057
+ }
7058
+ return;
7059
+ }
7060
+ const timer = setInterval(() => {
7061
+ void scheduleAutomaticUpdate();
7062
+ }, 60 * 60 * 1e3);
7063
+ timer.unref();
7064
+ const stop = () => {
7065
+ clearInterval(timer);
7066
+ void session?.release();
7067
+ };
7068
+ try {
7069
+ await start(stop);
7070
+ void scheduleAutomaticUpdate();
7071
+ } catch (error) {
7072
+ stop();
7073
+ throw error;
7074
+ }
7075
+ }
7076
+ var CHECK_INTERVAL_MS, RELEASE_AGE_MS, releaseBase, releaseListSchema, updateManifestSchema, statusSchema2;
7077
+ var init_updates = __esm({
7078
+ "src/distribution/updates.ts"() {
7079
+ "use strict";
7080
+ init_store();
7081
+ init_storage();
7082
+ init_state();
7083
+ init_bundle();
7084
+ init_runtime2();
7085
+ init_install2();
7086
+ CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
7087
+ RELEASE_AGE_MS = 24 * 60 * 60 * 1e3;
7088
+ releaseBase = "https://github.com/adrianczuczka/mason/releases";
7089
+ releaseListSchema = z28.array(z28.object({ tag_name: z28.string(), draft: z28.boolean(), prerelease: z28.boolean(), published_at: z28.string().nullable() }));
7090
+ updateManifestSchema = z28.object({
7091
+ format: z28.literal(1),
7092
+ version: z28.string().regex(/^\d+\.\d+\.\d+$/),
7093
+ publishedAt: z28.string().datetime(),
7094
+ assets: z28.record(z28.string().regex(/^[a-f0-9]{64}$/))
7095
+ }).strict();
7096
+ statusSchema2 = z28.object({ lastAttempt: z28.number(), availableVersion: z28.string().optional(), error: z28.string().optional() });
6259
7097
  }
6260
7098
  });
6261
7099
 
@@ -6452,7 +7290,7 @@ var inputArgs = process.argv.slice(2);
6452
7290
  var managedLaunch = inputArgs[0] === "--setup-host";
6453
7291
  var setupHost = managedLaunch ? inputArgs.splice(0, 2)[1] : void 0;
6454
7292
  var [command, ...args] = inputArgs;
6455
- var usage = `Mason ${"0.18.0"}
7293
+ var usage = `Mason ${"0.19.1"}
6456
7294
 
6457
7295
  Usage: mason <command> [options]
6458
7296
 
@@ -6465,6 +7303,9 @@ Usage: mason <command> [options]
6465
7303
  drift Check an optional architecture map
6466
7304
  mcp Run the MCP server (stdio)
6467
7305
  upgrade [version] Upgrade the standalone user installation
7306
+ updates [status|enable|disable|pin|unpin]
7307
+ Manage automatic standalone updates
7308
+ rollback Restore and pin the previous standalone version
6468
7309
  uninstall Remove that installation; retain project data
6469
7310
 
6470
7311
  Use mason <command> --help for options. npm users can keep using mason-auto,
@@ -6478,19 +7319,67 @@ try {
6478
7319
  await prepareLaunch2(setupHost, { allowInactive: command === "mcp" });
6479
7320
  }
6480
7321
  }
6481
- if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.18.0" }));
7322
+ if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.19.1" }));
6482
7323
  else if (!command || ["--help", "-h", "help"].includes(command)) console.log(usage);
6483
- else if (["--version", "-v"].includes(command)) console.log("0.18.0");
6484
- else if (command === "internal-install") {
7324
+ else if (["--version", "-v"].includes(command)) console.log("0.19.1");
7325
+ else if (command === "internal-update") {
7326
+ if (args.length) throw new Error("internal-update takes no arguments.");
7327
+ const { currentInstallation: currentInstallation2 } = await Promise.resolve().then(() => (init_state(), state_exports));
7328
+ const { runAutomaticUpdate: runAutomaticUpdate2 } = await Promise.resolve().then(() => (init_updates(), updates_exports));
7329
+ await runAutomaticUpdate2((await currentInstallation2()).home);
7330
+ } else if (command === "internal-install") {
6485
7331
  if (args.length) throw new Error("internal-install takes no arguments.");
6486
7332
  const { installStandalone: installStandalone2 } = await Promise.resolve().then(() => (init_install2(), install_exports2));
6487
7333
  const bundleRoot = fileURLToPath2(new URL("../..", import.meta.url));
6488
7334
  progress = createProgress();
6489
- const installed = await installStandalone2(bundleRoot, progress);
7335
+ const policyRevision = process.env.MASON_UPDATE_POLICY_REVISION;
7336
+ if (policyRevision && !/^[a-f0-9]{64}$/.test(process.env.MASON_EXPECTED_SHA256 ?? "")) throw new Error("Automatic staging requires a verified release checksum.");
7337
+ const installed = await installStandalone2(bundleRoot, progress, { stageOnly: !!policyRevision, policyRevision });
6490
7338
  progress.stop();
6491
7339
  const outcome = installed.previousVersion && installed.previousVersion !== installed.version ? `Updated Mason ${installed.previousVersion} \u2192 ${installed.version}` : `Installed Mason ${installed.version}`;
6492
- console.log(`${outcome} in ${installed.home}.
6493
- ${installed.path.message}`);
7340
+ if (policyRevision) console.log(`Staged Mason ${installed.version}. It will activate at a future MCP launch after existing servers exit.`);
7341
+ else {
7342
+ const { updateStatus: updateStatus2 } = await Promise.resolve().then(() => (init_updates(), updates_exports));
7343
+ const status = await updateStatus2(installed.home);
7344
+ const policy = status.pinnedVersion ? `pinned to ${status.pinnedVersion}` : status.mirror ? "managed by your mirror" : status.enabled ? "enabled" : "disabled";
7345
+ console.log(`${outcome} in ${installed.home}.
7346
+ ${installed.path.message}
7347
+ Automatic updates: ${policy}. Manage with mason updates.`);
7348
+ }
7349
+ } else if (command === "updates") {
7350
+ if (args.includes("--help")) console.log("Usage: mason updates [status|enable|disable|pin|unpin] [--json]. Pin holds the installed version; mason upgrade <version> installs and pins a specific release. Environment and mirror restrictions still apply.");
7351
+ else {
7352
+ const action = args.filter((arg) => arg !== "--json")[0] ?? "status";
7353
+ if (args.filter((arg) => arg !== "--json").length > 1 || !["status", "enable", "disable", "pin", "unpin"].includes(action)) throw new Error("Usage: mason updates [status|enable|disable|pin|unpin] [--json]");
7354
+ const { currentInstallation: currentInstallation2 } = await Promise.resolve().then(() => (init_state(), state_exports));
7355
+ const { updateSettings: updateSettings2, updateStatus: updateStatus2, scheduleAutomaticUpdate: scheduleAutomaticUpdate2 } = await Promise.resolve().then(() => (init_updates(), updates_exports));
7356
+ const { home } = await currentInstallation2();
7357
+ if (action !== "status") await updateSettings2(home, action);
7358
+ const status = await updateStatus2(home);
7359
+ if (args.includes("--json")) console.log(JSON.stringify(status));
7360
+ else {
7361
+ console.log(`Mason ${status.version}
7362
+ Automatic updates: ${status.effective ? "enabled" : "inactive"} (preference: ${status.enabled ? "enabled" : "disabled"}).`);
7363
+ if (status.pinnedVersion) console.log(`Pinned to ${status.pinnedVersion}. Run mason updates unpin to follow stable releases.`);
7364
+ if (status.mirror) console.log("Company mirror installation: automatic public updates are disabled.");
7365
+ if (!status.effective && status.enabled && !status.pinnedVersion && !status.mirror) console.log("Suppressed by CI, MASON_NO_AUTO_UPDATE, MASON_VERSION, or MASON_RELEASE_BASE.");
7366
+ if (status.pendingVersion) console.log(`Downloaded ${status.pendingVersion}; activates on a future MCP launch after existing servers exit.`);
7367
+ if (status.lastCheck) {
7368
+ console.log(`Last check: ${new Date(status.lastCheck.lastAttempt).toISOString()}`);
7369
+ if (status.lastCheck.availableVersion) console.log(`Available stable release: ${status.lastCheck.availableVersion}`);
7370
+ if (status.lastCheck.error) console.log(`Last update problem: ${status.lastCheck.error}`);
7371
+ }
7372
+ }
7373
+ if (action === "enable" || action === "unpin") void scheduleAutomaticUpdate2();
7374
+ }
7375
+ } else if (command === "rollback") {
7376
+ if (args.includes("--help")) console.log("Usage: mason rollback. Close running Mason MCP servers, then restore and pin the previous installed version.");
7377
+ else {
7378
+ if (args.length) throw new Error("rollback takes no arguments.");
7379
+ const { rollbackStandalone: rollbackStandalone2 } = await Promise.resolve().then(() => (init_install2(), install_exports2));
7380
+ const restored = await rollbackStandalone2();
7381
+ console.log(`Restored Mason ${restored.version} and pinned it. Run mason updates unpin to resume automatic updates.`);
7382
+ }
6494
7383
  } else if (command === "upgrade" || command === "uninstall") {
6495
7384
  if (args.includes("--help")) console.log(command === "upgrade" ? "Usage: mason upgrade [version]. Configured projects use the upgraded command on their next launch. Restart running assistants." : "Usage: mason uninstall. Removes the standalone user installation; project configuration and knowledge are retained. Run mason teardown in each project first to disconnect its integrations.");
6496
7385
  else {
@@ -6499,7 +7388,6 @@ ${installed.path.message}`);
6499
7388
  if (command === "upgrade") {
6500
7389
  progress = createProgress();
6501
7390
  process.exitCode = await upgradeStandalone2(args[0], progress);
6502
- if (process.exitCode === 0) console.log("Restart running assistants to use this installation. Existing project integrations use mason from PATH.");
6503
7391
  } else console.log((process.platform === "win32" ? "Finishing installation cleanup after runtime exit: " : "Removed standalone installation: ") + await uninstallStandalone2() + ". Project configuration and knowledge were retained. Reinstall Mason to use its integrations.");
6504
7392
  }
6505
7393
  } else if (["setup", "teardown", "status", "check", "auto"].includes(command)) {
@@ -6515,6 +7403,10 @@ ${installed.path.message}`);
6515
7403
  void 0,
6516
7404
  { managedHost: managedLaunch ? setupHost : void 0 }
6517
7405
  );
7406
+ if (command === "setup" && process.exitCode === 0 && !args.includes("--help")) {
7407
+ const { scheduleAutomaticUpdate: scheduleAutomaticUpdate2 } = await Promise.resolve().then(() => (init_updates(), updates_exports));
7408
+ void scheduleAutomaticUpdate2();
7409
+ }
6518
7410
  } else if (["audit", "review", "drift", "hook", "mcp"].includes(command)) {
6519
7411
  const binary = new URL(`./mason-${command}.js`, import.meta.url);
6520
7412
  process.argv = [process.execPath, fileURLToPath2(binary), ...args];