mason-context 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mason-mcp.js CHANGED
@@ -3504,10 +3504,10 @@ async function analyzeImpact(rootDir, targetFiles) {
3504
3504
  tests
3505
3505
  };
3506
3506
  }
3507
- async function resolveTargetFiles(rootDir, targets) {
3507
+ async function resolveTargetFiles(rootDir, targets2) {
3508
3508
  const resolved = [];
3509
3509
  const access = await createFileAccess(rootDir);
3510
- for (const target of targets) {
3510
+ for (const target of targets2) {
3511
3511
  if (target.includes("/")) {
3512
3512
  const normalized = normalizeRepoPath(target);
3513
3513
  if (normalized) resolved.push(normalized);
@@ -3667,23 +3667,23 @@ __export(assemble_exports, {
3667
3667
  import path22 from "path";
3668
3668
  import fs14 from "fs/promises";
3669
3669
  async function collectImpact(root, candidates) {
3670
- const targets = /* @__PURE__ */ new Set();
3670
+ const targets2 = /* @__PURE__ */ new Set();
3671
3671
  let sourceFiles;
3672
3672
  for (const candidate of sanitizeRepoPaths(candidates)) {
3673
3673
  const stat = await fs14.stat(path22.join(root, candidate)).catch(() => null);
3674
3674
  if (stat?.isDirectory()) {
3675
3675
  sourceFiles ??= await (await createFileAccess(root)).list();
3676
3676
  for (const file of sourceFiles) {
3677
- if (anchorMatches(candidate, file)) targets.add(file);
3678
- if (targets.size >= MAX_IMPACT_TARGETS) break;
3677
+ if (anchorMatches(candidate, file)) targets2.add(file);
3678
+ if (targets2.size >= MAX_IMPACT_TARGETS) break;
3679
3679
  }
3680
3680
  } else {
3681
- targets.add(candidate);
3681
+ targets2.add(candidate);
3682
3682
  }
3683
- if (targets.size >= MAX_IMPACT_TARGETS) break;
3683
+ if (targets2.size >= MAX_IMPACT_TARGETS) break;
3684
3684
  }
3685
- if (!targets.size) return { impact: null, relatedTests: [] };
3686
- const result = await analyzeImpact(root, [...targets]);
3685
+ if (!targets2.size) return { impact: null, relatedTests: [] };
3686
+ const result = await analyzeImpact(root, [...targets2]);
3687
3687
  return {
3688
3688
  impact: { targets: result.targetFiles, cochange: result.cochange, references: result.references.slice(0, 10) },
3689
3689
  relatedTests: [...new Set(result.tests.map((t) => t.file))]
@@ -4163,7 +4163,7 @@ var init_evidence2 = __esm({
4163
4163
  init_dead_command();
4164
4164
  init_storage();
4165
4165
  exec14 = promisify14(execFile14);
4166
- engineVersion = true ? "0.13.0" : "development";
4166
+ engineVersion = true ? "0.14.0" : "development";
4167
4167
  hash = (value) => createHash4("sha256").update(JSON.stringify(value)).digest("hex");
4168
4168
  internal = (file) => file === ".mason" || file === ".mason/reports" || file.startsWith(".mason/reports/");
4169
4169
  cacheSchema = z11.object({ version: z11.literal(1), entries: z11.record(z11.object({ key: z11.string(), result: checkResultSchema })), digest: z11.string() });
@@ -4630,7 +4630,8 @@ var init_model = __esm({
4630
4630
  runtimeSchema = z15.object({
4631
4631
  id: z15.string().regex(/^[a-f0-9]{24}$/),
4632
4632
  version: z15.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
4633
- hashes: z15.record(z15.string().regex(/^[a-f0-9]{64}$/))
4633
+ hashes: z15.record(z15.string().regex(/^[a-f0-9]{64}$/)),
4634
+ bundle: z15.object({ target: z15.string(), manifestHash: z15.string().regex(/^[a-f0-9]{64}$/) }).optional()
4634
4635
  });
4635
4636
  setupHostSchema = z15.object({
4636
4637
  runtime: runtimeSchema,
@@ -4656,40 +4657,109 @@ var init_model = __esm({
4656
4657
  }
4657
4658
  });
4658
4659
 
4659
- // src/setup/runtime.ts
4660
+ // src/distribution/bundle.ts
4660
4661
  import fs17 from "fs/promises";
4661
4662
  import path24 from "path";
4663
+ import { createHash as createHash5 } from "crypto";
4664
+ import { z as z16 } from "zod";
4665
+ async function readBundle(root) {
4666
+ const bytes = await fs17.readFile(await storePath(root, "bundle.json"));
4667
+ const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
4668
+ const windows = manifest.target.startsWith("win32-");
4669
+ for (const file of Object.keys(manifest.files)) {
4670
+ if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") || path24.isAbsolute(file)) {
4671
+ throw new Error("Unsafe path in Mason bundle: " + file);
4672
+ }
4673
+ }
4674
+ 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"]) {
4675
+ if (!manifest.files[required]) throw new Error("Incomplete Mason bundle: " + required);
4676
+ }
4677
+ return { manifest, manifestHash: sha256(bytes) };
4678
+ }
4679
+ async function verifyBundle(root, expectedHash) {
4680
+ const bundle = await readBundle(root);
4681
+ if (expectedHash && bundle.manifestHash !== expectedHash) throw new Error("Mason bundle manifest changed.");
4682
+ 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.");
4683
+ const files = Object.keys(bundle.manifest.files);
4684
+ for (let i = 0; i < files.length; i += 8) await Promise.all(files.slice(i, i + 8).map(async (file) => {
4685
+ if (sha256(await fs17.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
4686
+ }));
4687
+ return bundle;
4688
+ }
4689
+ var targets, digest2, bundleSchema, sha256;
4690
+ var init_bundle = __esm({
4691
+ "src/distribution/bundle.ts"() {
4692
+ "use strict";
4693
+ init_storage();
4694
+ targets = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
4695
+ digest2 = z16.string().regex(/^[a-f0-9]{64}$/);
4696
+ bundleSchema = z16.object({
4697
+ format: z16.literal(1),
4698
+ version: z16.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
4699
+ target: z16.enum(targets),
4700
+ nodeVersion: z16.string(),
4701
+ files: z16.record(digest2)
4702
+ }).strict();
4703
+ sha256 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
4704
+ }
4705
+ });
4706
+
4707
+ // src/setup/runtime.ts
4708
+ import fs18 from "fs/promises";
4709
+ import path25 from "path";
4662
4710
  import { fileURLToPath } from "url";
4663
- import { randomUUID as randomUUID5, createHash as createHash5 } from "crypto";
4711
+ import { randomUUID as randomUUID5, createHash as createHash6 } from "crypto";
4664
4712
  import { execFile as execFile15 } from "child_process";
4665
4713
  import { promisify as promisify15 } from "util";
4666
4714
  async function packageRoot() {
4667
- let directory = path24.dirname(fileURLToPath(import.meta.url));
4715
+ let directory = path25.dirname(fileURLToPath(import.meta.url));
4668
4716
  for (let i = 0; i < 8; i++) {
4669
4717
  try {
4670
- const pkg = JSON.parse(await fs17.readFile(path24.join(directory, "package.json"), "utf8"));
4718
+ const pkg = JSON.parse(await fs18.readFile(path25.join(directory, "package.json"), "utf8"));
4671
4719
  if (pkg.name === "mason-context") return directory;
4672
4720
  } catch {
4673
4721
  }
4674
- directory = path24.dirname(directory);
4722
+ directory = path25.dirname(directory);
4675
4723
  }
4676
4724
  throw new Error("Cannot locate the executing Mason distribution. Reinstall Mason and rerun setup.");
4677
4725
  }
4678
4726
  async function sourceRuntime() {
4679
4727
  const source = await packageRoot();
4680
- const pkg = JSON.parse(await fs17.readFile(path24.join(source, "package.json"), "utf8"));
4681
- const hashes = Object.fromEntries(await Promise.all(BINARIES.map(async (file) => [file, checksum(await fs17.readFile(path24.join(source, file)))])));
4728
+ const pkg = JSON.parse(await fs18.readFile(path25.join(source, "package.json"), "utf8"));
4729
+ const hashes = Object.fromEntries(await Promise.all(BINARIES.map(async (file) => [file, checksum(await fs18.readFile(path25.join(source, file)))])));
4730
+ const bundleRoot = path25.dirname(source);
4731
+ const bundleExists = await fs18.lstat(path25.join(bundleRoot, "bundle.json")).then(() => true, (error) => {
4732
+ if (error.code === "ENOENT") return false;
4733
+ throw error;
4734
+ });
4735
+ if (bundleExists) {
4736
+ const { manifest, manifestHash } = await verifyBundle(bundleRoot);
4737
+ if (manifest.version !== pkg.version) throw new Error("Mason package and bundle versions differ.");
4738
+ return { source, bundleRoot, runtime: runtimeSchema.parse({
4739
+ id: manifestHash.slice(0, 24),
4740
+ version: pkg.version,
4741
+ hashes,
4742
+ bundle: { target: manifest.target, manifestHash }
4743
+ }) };
4744
+ }
4682
4745
  return { source, runtime: runtimeSchema.parse({ id: hash([pkg.version, pkg.dependencies, hashes]).slice(0, 24), version: pkg.version, hashes }) };
4683
4746
  }
4684
4747
  async function verifyRuntime(root, runtime) {
4685
4748
  try {
4686
4749
  const saved = await readStoreJson(root, `.mason/runtime/${runtime.id}/receipt.json`);
4687
4750
  if (JSON.stringify(saved) !== JSON.stringify(runtime)) return false;
4751
+ if (runtime.bundle) {
4752
+ const base2 = await storePath(root, `.mason/runtime/${runtime.id}`);
4753
+ const { manifest } = await verifyBundle(base2, runtime.bundle.manifestHash);
4754
+ if (manifest.version !== runtime.version || manifest.target !== runtime.bundle.target) return false;
4755
+ for (const file of BINARIES) if (manifest.files["app/" + file] !== runtime.hashes[file]) return false;
4756
+ return true;
4757
+ }
4688
4758
  const base = `.mason/runtime/${runtime.id}/node_modules/mason-context/`;
4689
4759
  const pkg = await readStoreJson(root, base + "package.json");
4690
4760
  if (pkg?.name !== "mason-context" || pkg.version !== runtime.version) return false;
4691
4761
  for (const file of BINARIES) {
4692
- if (checksum(await fs17.readFile(await storePath(root, base + file))) !== runtime.hashes[file]) return false;
4762
+ if (checksum(await fs18.readFile(await storePath(root, base + file))) !== runtime.hashes[file]) return false;
4693
4763
  }
4694
4764
  return true;
4695
4765
  } catch {
@@ -4702,30 +4772,42 @@ async function installRuntime(root, selected) {
4702
4772
  const relative = `.mason/runtime/${runtime.id}`;
4703
4773
  const target = await storePath(root, relative, true);
4704
4774
  const stage = await storePath(root, ".mason/runtime/.install-" + randomUUID5(), true);
4705
- await fs17.mkdir(stage);
4775
+ await fs18.mkdir(stage);
4706
4776
  try {
4707
- const npm = process.platform === "win32" ? "npm.cmd" : "npm";
4708
- const options = { timeout: 12e4, maxBuffer: 2 * 1024 * 1024, windowsHide: true };
4709
- const packed = JSON.parse((await exec15(npm, ["pack", "--ignore-scripts", "--json", "--pack-destination", stage], { ...options, cwd: source })).stdout);
4710
- const filename = packed[0]?.filename;
4711
- if (typeof filename !== "string" || path24.basename(filename) !== filename) throw new Error("npm did not produce a Mason package archive.");
4712
- await fs17.rename(path24.join(stage, filename), path24.join(stage, "mason.tgz"));
4713
- await fs17.writeFile(path24.join(stage, "package.json"), JSON.stringify({ name: "mason-project-runtime", private: true, version: "1.0.0" }));
4714
- await exec15(npm, ["install", "--ignore-scripts", "--omit=dev", "--no-audit", "--no-fund", "--save-exact", "./mason.tgz"], { ...options, cwd: stage });
4715
- for (const file of BINARIES) {
4716
- if (checksum(await fs17.readFile(path24.join(stage, "node_modules/mason-context", file))) !== runtime.hashes[file]) throw new Error("Installed Mason binary differs from the selected distribution.");
4777
+ if (runtime.bundle) {
4778
+ if (!selected.bundleRoot) throw new Error("Standalone bundle source is missing.");
4779
+ const { manifest } = await verifyBundle(selected.bundleRoot, runtime.bundle.manifestHash);
4780
+ for (const file of ["bundle.json", ...Object.keys(manifest.files)]) {
4781
+ const from = await storePath(selected.bundleRoot, file);
4782
+ const to = await storePath(stage, file, true);
4783
+ await fs18.copyFile(from, to);
4784
+ await fs18.chmod(to, (await fs18.stat(from)).mode & 511);
4785
+ }
4786
+ await verifyBundle(stage, runtime.bundle.manifestHash);
4787
+ } else {
4788
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
4789
+ const options = { timeout: 12e4, maxBuffer: 2 * 1024 * 1024, windowsHide: true };
4790
+ const packed = JSON.parse((await exec15(npm, ["pack", "--ignore-scripts", "--json", "--pack-destination", stage], { ...options, cwd: source })).stdout);
4791
+ const filename = packed[0]?.filename;
4792
+ if (typeof filename !== "string" || path25.basename(filename) !== filename) throw new Error("npm did not produce a Mason package archive.");
4793
+ await fs18.rename(path25.join(stage, filename), path25.join(stage, "mason.tgz"));
4794
+ await fs18.writeFile(path25.join(stage, "package.json"), JSON.stringify({ name: "mason-project-runtime", private: true, version: "1.0.0" }));
4795
+ await exec15(npm, ["install", "--ignore-scripts", "--omit=dev", "--no-audit", "--no-fund", "--save-exact", "./mason.tgz"], { ...options, cwd: stage });
4796
+ for (const file of BINARIES) {
4797
+ if (checksum(await fs18.readFile(path25.join(stage, "node_modules/mason-context", file))) !== runtime.hashes[file]) throw new Error("Installed Mason binary differs from the selected distribution.");
4798
+ }
4717
4799
  }
4718
- await fs17.writeFile(path24.join(stage, "receipt.json"), JSON.stringify(runtime, null, 2) + "\n");
4800
+ await fs18.writeFile(path25.join(stage, "receipt.json"), JSON.stringify(runtime, null, 2) + "\n");
4719
4801
  try {
4720
- await fs17.rename(target, target + ".previous-" + randomUUID5());
4802
+ await fs18.rename(target, target + ".previous-" + randomUUID5());
4721
4803
  } catch (error) {
4722
4804
  if (error.code !== "ENOENT") throw error;
4723
4805
  }
4724
- await fs17.rename(stage, target);
4806
+ await fs18.rename(stage, target);
4725
4807
  if (!await verifyRuntime(root, runtime)) throw new Error("The installed Mason runtime could not be verified.");
4726
4808
  return runtime;
4727
4809
  } finally {
4728
- await fs17.rm(stage, { recursive: true, force: true });
4810
+ await fs18.rm(stage, { recursive: true, force: true });
4729
4811
  }
4730
4812
  }
4731
4813
  var exec15, checksum, BINARIES;
@@ -4735,15 +4817,16 @@ var init_runtime2 = __esm({
4735
4817
  init_model();
4736
4818
  init_storage();
4737
4819
  init_evidence2();
4820
+ init_bundle();
4738
4821
  exec15 = promisify15(execFile15);
4739
- checksum = (bytes) => createHash5("sha256").update(bytes).digest("hex");
4822
+ checksum = (bytes) => createHash6("sha256").update(bytes).digest("hex");
4740
4823
  BINARIES = ["dist/mason-auto.js", "dist/mason-mcp.js"];
4741
4824
  }
4742
4825
  });
4743
4826
 
4744
4827
  // src/setup/files.ts
4745
- import fs18 from "fs/promises";
4746
- import path25 from "path";
4828
+ import fs19 from "fs/promises";
4829
+ import path26 from "path";
4747
4830
  import { randomUUID as randomUUID6 } from "crypto";
4748
4831
  async function readText(root, file) {
4749
4832
  try {
@@ -4770,10 +4853,10 @@ async function applyEdit(root, edit) {
4770
4853
  if (current === edit.after) return false;
4771
4854
  if (current !== edit.before) throw new Error("Setup input changed during installation: " + edit.path + ". Rerun setup to resume.");
4772
4855
  const file = await storePath(root, edit.path, true);
4773
- const temporary = path25.join(path25.dirname(file), ".mason-setup-" + randomUUID6() + ".tmp");
4856
+ const temporary = path26.join(path26.dirname(file), ".mason-setup-" + randomUUID6() + ".tmp");
4774
4857
  try {
4775
- const mode = await fs18.stat(file).then((s) => s.mode & 511, () => 384);
4776
- const handle = await fs18.open(temporary, "wx", mode);
4858
+ const mode = await fs19.stat(file).then((s) => s.mode & 511, () => 384);
4859
+ const handle = await fs19.open(temporary, "wx", mode);
4777
4860
  try {
4778
4861
  await handle.writeFile(edit.after, "utf8");
4779
4862
  await handle.sync();
@@ -4781,10 +4864,10 @@ async function applyEdit(root, edit) {
4781
4864
  await handle.close();
4782
4865
  }
4783
4866
  if (await readText(root, edit.path) !== edit.before) throw new Error("Setup input changed during installation: " + edit.path);
4784
- await fs18.rename(temporary, file);
4867
+ await fs19.rename(temporary, file);
4785
4868
  return true;
4786
4869
  } finally {
4787
- await fs18.rm(temporary, { force: true });
4870
+ await fs19.rm(temporary, { force: true });
4788
4871
  }
4789
4872
  }
4790
4873
  var init_files2 = __esm({
@@ -4796,18 +4879,55 @@ var init_files2 = __esm({
4796
4879
  });
4797
4880
 
4798
4881
  // src/setup/launcher.ts
4799
- var BOOTSTRAP, mcpCommand, hookCommand, LAUNCHER;
4882
+ var BOOTSTRAP, POWERSHELL_BOOTSTRAP, mcpCommand, hookCommand, SHELL_LAUNCHER, POWERSHELL_LAUNCHER, LAUNCHER;
4800
4883
  var init_launcher = __esm({
4801
4884
  "src/setup/launcher.ts"() {
4802
4885
  "use strict";
4803
4886
  BOOTSTRAP = "require(require('node:path').join(require('node:child_process').execFileSync('git',['rev-parse','--show-toplevel'],{encoding:'utf8'}).trim(),'.mason','run.cjs'))";
4804
- mcpCommand = (host) => ({ command: "node", args: ["-e", BOOTSTRAP, "--", host, "mcp"] });
4805
- hookCommand = (host) => `node -e "${BOOTSTRAP}" -- ${host} auto`;
4887
+ POWERSHELL_BOOTSTRAP = "& (Join-Path (git rev-parse --show-toplevel) '.mason/run.ps1')";
4888
+ mcpCommand = (host, runtime) => !runtime?.bundle ? { command: "node", args: ["-e", BOOTSTRAP, "--", host, "mcp"] } : process.platform === "win32" ? { command: "powershell.exe", args: ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", POWERSHELL_BOOTSTRAP, host, "mcp"] } : { command: "sh", args: ["-c", 'exec sh "$(git rev-parse --show-toplevel)/.mason/run.sh" "$@"', "mason", host, "mcp"] };
4889
+ hookCommand = (host, runtime) => !runtime?.bundle ? `node -e "${BOOTSTRAP}" -- ${host} auto` : process.platform === "win32" ? `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "${POWERSHELL_BOOTSTRAP} ${host} auto"` : `sh "$(git rev-parse --show-toplevel)/.mason/run.sh" ${host} auto`;
4890
+ SHELL_LAUNCHER = `#!/bin/sh
4891
+ set -eu
4892
+ advisory=0
4893
+ if [ "\${2-}" = auto ] && [ "\${3-}" = hook ]; then advisory=1; fi
4894
+ fail() {
4895
+ if [ "$advisory" -eq 1 ]; then printf '%s\\n' '{"systemMessage":"Mason runtime unavailable. Rerun mason setup for this checkout; verification was not established."}'; exit 0; fi
4896
+ echo 'Mason runtime unavailable. Rerun mason setup for this checkout.' >&2; exit 2
4897
+ }
4898
+ root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) || fail
4899
+ case "\${1-}" in codex|claude) ;; *) fail ;; esac
4900
+ pointer="$root/.mason/runtime/$1.txt"
4901
+ [ -f "$pointer" ] || fail
4902
+ runtime=$(cat "$pointer") || fail
4903
+ case "$runtime" in ''|*[!0-9a-f]*) fail ;; esac
4904
+ [ "\${#runtime}" -eq 24 ] || fail
4905
+ [ -x "$root/.mason/runtime/$runtime/node" ] || fail
4906
+ if [ "$advisory" -eq 1 ]; then
4907
+ "$root/.mason/runtime/$runtime/node" "$root/.mason/run.cjs" "$@" || fail
4908
+ else
4909
+ exec "$root/.mason/runtime/$runtime/node" "$root/.mason/run.cjs" "$@"
4910
+ fi
4911
+ `;
4912
+ POWERSHELL_LAUNCHER = `# Managed by mason setup; no system Node installation is used.
4913
+ $ErrorActionPreference = 'Stop'
4914
+ $advisory = $args.Count -ge 3 -and $args[1] -eq 'auto' -and $args[2] -eq 'hook'
4915
+ try {
4916
+ if ($args.Count -lt 2 -or $args[0] -notin @('codex','claude')) { throw 'Invalid Mason host.' }
4917
+ $runtime = (Get-Content -LiteralPath (Join-Path $PSScriptRoot ('runtime/' + $args[0] + '.txt')) -Raw).Trim()
4918
+ if ($runtime -cnotmatch '^[a-f0-9]{24}$') { throw 'Invalid Mason runtime pointer.' }
4919
+ & (Join-Path $PSScriptRoot ('runtime/' + $runtime + '/node.exe')) (Join-Path $PSScriptRoot 'run.cjs') @args
4920
+ if ($LASTEXITCODE -ne 0) { throw 'Mason runtime failed.' }
4921
+ } catch {
4922
+ if ($advisory) { Write-Output '{"systemMessage":"Mason runtime unavailable. Rerun mason setup for this checkout; verification was not established."}'; exit 0 }
4923
+ [Console]::Error.WriteLine($_.Exception.Message); exit 2
4924
+ }
4925
+ `;
4806
4926
  LAUNCHER = `// Managed by mason-auto setup. Rerun setup to install this checkout's pinned runtime.
4807
4927
  const fs = require('node:fs');
4808
4928
  const path = require('node:path');
4809
4929
  const {pathToFileURL} = require('node:url');
4810
- const launchArgs = process.argv.slice(1);
4930
+ const launchArgs = process.argv.slice(process.argv[1] && path.resolve(process.argv[1]) === __filename ? 2 : 1);
4811
4931
  (async () => {
4812
4932
  const root = path.dirname(__dirname);
4813
4933
  const args = launchArgs;
@@ -4816,7 +4936,13 @@ const launchArgs = process.argv.slice(1);
4816
4936
  const setup = JSON.parse(fs.readFileSync(path.join(__dirname, 'setup.json'), 'utf8'));
4817
4937
  const entry = setup.hosts?.[host];
4818
4938
  if (setup.version !== 1 || !entry || !/^[a-f0-9]{24}$/.test(entry.runtime?.id)) throw new Error('Mason is not configured for this host. Rerun mason-auto setup.');
4819
- const binary = path.join(__dirname, 'runtime', entry.runtime.id, 'node_modules', 'mason-context', 'dist', 'mason-' + command + '.js');
4939
+ const base = path.join(__dirname, 'runtime', entry.runtime.id);
4940
+ if (entry.runtime.bundle) {
4941
+ const node = process.platform === 'win32' ? 'node.exe' : 'node';
4942
+ if (fs.realpathSync(process.execPath) !== fs.realpathSync(path.join(base, node))) throw new Error('Mason runtime pointer differs from the configured version. Rerun setup.');
4943
+ }
4944
+ const binary = entry.runtime.bundle ? path.join(base, 'app', 'dist', 'mason-' + command + '.js')
4945
+ : path.join(base, 'node_modules', 'mason-context', 'dist', 'mason-' + command + '.js');
4820
4946
  if (!fs.existsSync(binary)) throw new Error('The pinned Mason runtime is missing in this checkout. Rerun mason-auto setup --host ' + host + '.');
4821
4947
  process.env.MASON_SETUP_ROOT = root;
4822
4948
  process.env.MASON_SETUP_HOST = host;
@@ -4835,7 +4961,7 @@ const launchArgs = process.argv.slice(1);
4835
4961
  // src/setup/config.ts
4836
4962
  import { isDeepStrictEqual } from "util";
4837
4963
  import { parse, stringify } from "smol-toml";
4838
- import { z as z16 } from "zod";
4964
+ import { z as z17 } from "zod";
4839
4965
  async function instructionEdits(root, host) {
4840
4966
  const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
4841
4967
  const existing = found.find((f) => f.text !== null);
@@ -4864,22 +4990,22 @@ async function instructionEdits(root, host) {
4864
4990
  }
4865
4991
  return edits;
4866
4992
  }
4867
- function managedMcp(previous, host) {
4993
+ function managedMcp(previous, host, runtime) {
4868
4994
  const options = previous === void 0 ? {} : object.parse(previous);
4869
4995
  for (const key of ["command", "args", "url", "type", "headers", "http_headers", "env_http_headers", "bearer_token_env_var"]) delete options[key];
4870
- return { ...options, ...mcpCommand(host) };
4996
+ return { ...options, ...mcpCommand(host, runtime) };
4871
4997
  }
4872
- async function mcpEdit(root, host) {
4998
+ async function mcpEdit(root, host, runtime) {
4873
4999
  const file = mcpPath(host), before = await readText(root, file);
4874
5000
  if (host === "claude") {
4875
5001
  const config2 = before === null ? {} : object.parse(JSON.parse(before));
4876
5002
  const servers2 = object.parse(config2.mcpServers ?? {});
4877
- return { path: file, before, after: JSON.stringify({ ...config2, mcpServers: { ...servers2, mason: managedMcp(servers2.mason, host) } }, null, 2) + "\n" };
5003
+ return { path: file, before, after: JSON.stringify({ ...config2, mcpServers: { ...servers2, mason: managedMcp(servers2.mason, host, runtime) } }, null, 2) + "\n" };
4878
5004
  }
4879
5005
  const config = parse(before ?? "");
4880
5006
  const managed = (before ?? "").includes(TOML_START);
4881
5007
  const servers = config.mcp_servers;
4882
- const desired = managedMcp(servers?.mason, host);
5008
+ const desired = managedMcp(servers?.mason, host, runtime);
4883
5009
  let base = before ?? "";
4884
5010
  if (servers?.mason && !managed) {
4885
5011
  const headers = [...base.matchAll(/^\s*\[\[?.+?\]\]?[^\S\r\n]*(?:#.*)?$/gm)];
@@ -4893,10 +5019,10 @@ async function mcpEdit(root, host) {
4893
5019
  if (!isDeepStrictEqual(parsed, expected)) throw new Error("Could not safely configure the Mason MCP entry without changing other settings.");
4894
5020
  return { path: file, before, after };
4895
5021
  }
4896
- async function hookEdits(root, host) {
5022
+ async function hookEdits(root, host, runtime) {
4897
5023
  const file = configPath(host), before = await readText(root, file);
4898
5024
  const { planAutomationInstall: planAutomationInstall2 } = await Promise.resolve().then(() => (init_install(), install_exports));
4899
- const plan = await planAutomationInstall2(root, host, hookCommand(host));
5025
+ const plan = await planAutomationInstall2(root, host, hookCommand(host, runtime));
4900
5026
  return [
4901
5027
  { path: file, before, after: JSON.stringify(plan.config, null, 2) + "\n" },
4902
5028
  { path: ".mason/automation.json", before: await readText(root, ".mason/automation.json"), after: JSON.stringify(plan.record, null, 2) + "\n" }
@@ -4912,18 +5038,20 @@ async function ancillaryEdits(root) {
4912
5038
  if (error.code !== 1) throw error;
4913
5039
  }
4914
5040
  const retainParentRule = parentIgnored || !!ignore?.replace(/\r\n/g, "\n").includes("# mason:ignore:start\n!/.mason/\n/.mason/*");
4915
- const rules = [...retainParentRule ? ["!/.mason/", "/.mason/*"] : [], "!/.mason/decisions/", "!/.mason/decisions/**", "!/.mason/setup.json", "!/.mason/automation.json", "!/.mason/project.json", "!/.mason/run.cjs", "/.mason/reports/", "/.mason/runtime/"].join("\n");
5041
+ const rules = [...retainParentRule ? ["!/.mason/", "/.mason/*"] : [], "!/.mason/decisions/", "!/.mason/decisions/**", "!/.mason/setup.json", "!/.mason/automation.json", "!/.mason/project.json", "!/.mason/run.cjs", "!/.mason/run.sh", "!/.mason/run.ps1", "/.mason/reports/", "/.mason/runtime/"].join("\n");
4916
5042
  return [
4917
5043
  { path: ".gitignore", before: ignore, after: managedBlock(ignore ?? "", "# mason:ignore:start", "# mason:ignore:end", rules) },
4918
- { path: ".mason/run.cjs", before: await readText(root, ".mason/run.cjs"), after: LAUNCHER }
5044
+ { path: ".mason/run.cjs", before: await readText(root, ".mason/run.cjs"), after: LAUNCHER },
5045
+ { path: ".mason/run.sh", before: await readText(root, ".mason/run.sh"), after: SHELL_LAUNCHER },
5046
+ { path: ".mason/run.ps1", before: await readText(root, ".mason/run.ps1"), after: POWERSHELL_LAUNCHER }
4919
5047
  ];
4920
5048
  }
4921
- async function inspectHostConfig(root, host, plannedMcp) {
5049
+ async function inspectHostConfig(root, host, plannedMcp, runtime) {
4922
5050
  const text2 = plannedMcp ?? await readText(root, mcpPath(host));
4923
5051
  const config = host === "codex" ? parse(text2 ?? "") : object.parse(JSON.parse(text2 ?? "{}"));
4924
5052
  const servers = host === "codex" ? config.mcp_servers : config.mcpServers;
4925
5053
  const hooks = automationConfigSchema.parse(JSON.parse(await readText(root, configPath(host)) ?? "{}"));
4926
- const expected = hookConfig(host, hookCommand(host));
5054
+ const expected = hookConfig(host, hookCommand(host, runtime));
4927
5055
  const mcp = servers?.mason === void 0 ? null : object.parse(servers.mason);
4928
5056
  return {
4929
5057
  mcp,
@@ -4945,7 +5073,7 @@ var init_config = __esm({
4945
5073
  init_install();
4946
5074
  init_files2();
4947
5075
  init_launcher();
4948
- object = z16.record(z16.unknown());
5076
+ object = z17.record(z17.unknown());
4949
5077
  mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
4950
5078
  TOML_START = "# mason:mcp:start";
4951
5079
  TOML_END = "# mason:mcp:end";
@@ -4961,8 +5089,8 @@ __export(observations_exports, {
4961
5089
  observeActivation: () => observeActivation,
4962
5090
  readObservation: () => readObservation
4963
5091
  });
4964
- import fs19 from "fs/promises";
4965
- import { z as z17 } from "zod";
5092
+ import fs20 from "fs/promises";
5093
+ import { z as z18 } from "zod";
4966
5094
  function observationPath(directory, host, revision) {
4967
5095
  return `${directory}/activation/${host}-${revision}.json`;
4968
5096
  }
@@ -4978,8 +5106,8 @@ async function observeActivation(dir, event, options = {}) {
4978
5106
  if (host !== "codex" && host !== "claude" || !revision || !process.env.MASON_SETUP_ROOT) return null;
4979
5107
  try {
4980
5108
  const capturedDirectory = options.reportPath?.match(/^(\.mason\/reports\/automation\/[a-f0-9]{24})\/checks\//)?.[1];
4981
- const ws = capturedDirectory ? { root: await fs19.realpath(dir), directory: capturedDirectory } : await workspace(dir);
4982
- if (ws.root !== await fs19.realpath(process.env.MASON_SETUP_ROOT)) return null;
5109
+ const ws = capturedDirectory ? { root: await fs20.realpath(dir), directory: capturedDirectory } : await workspace(dir);
5110
+ if (ws.root !== await fs20.realpath(process.env.MASON_SETUP_ROOT)) return null;
4983
5111
  const setup = await loadSetup(ws.root);
4984
5112
  if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
4985
5113
  const directory = ws.directory + "/activation";
@@ -5025,18 +5153,18 @@ var init_observations = __esm({
5025
5153
  init_store();
5026
5154
  init_storage();
5027
5155
  init_model();
5028
- observationSchema = z17.object({
5029
- version: z17.literal(1),
5030
- root: z17.string(),
5031
- revision: z17.string(),
5032
- host: z17.enum(["codex", "claude"]),
5033
- contextCalls: z17.number().int().nonnegative(),
5034
- lastContextAt: z17.string().optional(),
5035
- sessions: z17.record(z17.object({
5036
- events: z17.array(z17.enum(events)),
5037
- at: z17.string(),
5038
- verificationStatus: z17.string().optional(),
5039
- reportPath: z17.string().optional()
5156
+ observationSchema = z18.object({
5157
+ version: z18.literal(1),
5158
+ root: z18.string(),
5159
+ revision: z18.string(),
5160
+ host: z18.enum(["codex", "claude"]),
5161
+ contextCalls: z18.number().int().nonnegative(),
5162
+ lastContextAt: z18.string().optional(),
5163
+ sessions: z18.record(z18.object({
5164
+ events: z18.array(z18.enum(events)),
5165
+ at: z18.string(),
5166
+ verificationStatus: z18.string().optional(),
5167
+ reportPath: z18.string().optional()
5040
5168
  }))
5041
5169
  });
5042
5170
  }
@@ -5066,17 +5194,18 @@ async function setupStatus(dir) {
5066
5194
  };
5067
5195
  }
5068
5196
  const hosts = {};
5069
- const launcherCurrent = await readText(ws.root, ".mason/run.cjs") === LAUNCHER;
5197
+ const commonLauncherCurrent = await readText(ws.root, ".mason/run.cjs") === LAUNCHER;
5070
5198
  const automation = await automationStatus(ws.root);
5071
5199
  for (const host of ["codex", "claude"]) {
5072
5200
  const entry = setup.hosts[host];
5073
5201
  if (!entry) continue;
5202
+ const launcherCurrent = commonLauncherCurrent && (!entry.runtime.bundle || await readText(ws.root, ".mason/run.sh") === SHELL_LAUNCHER && await readText(ws.root, ".mason/run.ps1") === POWERSHELL_LAUNCHER && await readText(ws.root, `.mason/runtime/${host}.txt`) === entry.runtime.id + "\n");
5074
5203
  const instructions = await instructionEdits(ws.root, host);
5075
5204
  const instructionsCurrent = instructions.every((edit) => edit.before === edit.after);
5076
5205
  const installed = await verifyRuntime(ws.root, entry.runtime);
5077
- const config = await inspectHostConfig(ws.root, host);
5078
- const mcp = !config.mcpDisabled && hash(config.mcp) === entry.mcpFingerprint && isDeepStrictEqual2({ command: config.mcp?.command, args: config.mcp?.args }, mcpCommand(host));
5079
- const hooks = isDeepStrictEqual2(config.hooks, hookConfig(host, hookCommand(host)).hooks) && !config.disabled;
5206
+ const config = await inspectHostConfig(ws.root, host, void 0, entry.runtime);
5207
+ const mcp = !config.mcpDisabled && hash(config.mcp) === entry.mcpFingerprint && isDeepStrictEqual2({ command: config.mcp?.command, args: config.mcp?.args }, mcpCommand(host, entry.runtime));
5208
+ const hooks = isDeepStrictEqual2(config.hooks, hookConfig(host, hookCommand(host, entry.runtime)).hooks) && !config.disabled;
5080
5209
  const local = await loadSetupReceipt(ws.root, ws.directory, host);
5081
5210
  const configured = local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
5082
5211
  const observation = await readObservation(ws.root, ws.directory, host, entry.revision);
@@ -5170,15 +5299,15 @@ async function setupProject(dir, options = {}) {
5170
5299
  const existing = await loadSetup(ws.root);
5171
5300
  const previousReceipt = await loadSetupReceipt(ws.root, ws.directory, host);
5172
5301
  const marker = await loadProjectMarker(ws.root);
5302
+ const selected = await sourceRuntime();
5173
5303
  const instructions = await instructionEdits(ws.root, host);
5174
- const mcp = await mcpEdit(ws.root, host);
5175
- const hooks = await hookEdits(ws.root, host);
5304
+ const mcp = await mcpEdit(ws.root, host, selected.runtime);
5305
+ const hooks = await hookEdits(ws.root, host, selected.runtime);
5176
5306
  const ancillary = await ancillaryEdits(ws.root);
5177
5307
  const edits = [...ancillary, ...instructions, mcp, ...hooks];
5178
5308
  const setupBefore = await readText(ws.root, ".mason/setup.json");
5179
- const selected = await sourceRuntime();
5180
- const mcpFingerprint = hash((await inspectHostConfig(ws.root, host, mcp.after)).mcp);
5181
- const desiredHooks = hookConfig(host, hookCommand(host)).hooks;
5309
+ const mcpFingerprint = hash((await inspectHostConfig(ws.root, host, mcp.after, selected.runtime)).mcp);
5310
+ const desiredHooks = hookConfig(host, hookCommand(host, selected.runtime)).hooks;
5182
5311
  const fingerprint = hash({
5183
5312
  runtime: selected.runtime,
5184
5313
  launcher: LAUNCHER,
@@ -5206,6 +5335,10 @@ async function setupProject(dir, options = {}) {
5206
5335
  revision
5207
5336
  });
5208
5337
  await installRuntime(ws.root, selected);
5338
+ if (selected.runtime.bundle) {
5339
+ const pointer = `.mason/runtime/${host}.txt`;
5340
+ await applyEdit(ws.root, { path: pointer, before: await readText(ws.root, pointer), after: selected.runtime.id + "\n" });
5341
+ }
5209
5342
  const changedFiles = [];
5210
5343
  for (const edit of edits) if (await applyEdit(ws.root, edit)) changedFiles.push(edit.path);
5211
5344
  if (await applyEdit(ws.root, { path: ".mason/setup.json", before: setupBefore, after: JSON.stringify(setup, null, 2) + "\n" })) changedFiles.push(".mason/setup.json");
@@ -5214,7 +5347,7 @@ async function setupProject(dir, options = {}) {
5214
5347
  changedFiles.push(".mason/project.json");
5215
5348
  }
5216
5349
  const checked = await automate(ws.root, { event: "turn_start" });
5217
- const configured = await inspectHostConfig(ws.root, host);
5350
+ const configured = await inspectHostConfig(ws.root, host, void 0, selected.runtime);
5218
5351
  await writeStoreJson(ws.root, receiptPath, {
5219
5352
  version: 1,
5220
5353
  host,
@@ -5283,8 +5416,8 @@ __export(client_exports, {
5283
5416
  function createConfluenceClient(config, fetchFn = fetch) {
5284
5417
  const baseUrl = config.baseUrl.replace(/\/+$/, "");
5285
5418
  const auth = "Basic " + Buffer.from(`${config.email}:${config.apiToken}`).toString("base64");
5286
- async function call(method, path29, body) {
5287
- const res = await fetchFn(`${baseUrl}${path29}`, {
5419
+ async function call(method, path30, body) {
5420
+ const res = await fetchFn(`${baseUrl}${path30}`, {
5288
5421
  method,
5289
5422
  headers: {
5290
5423
  Authorization: auth,
@@ -5296,7 +5429,7 @@ function createConfluenceClient(config, fetchFn = fetch) {
5296
5429
  if (!res.ok) {
5297
5430
  const text2 = await res.text();
5298
5431
  throw new Error(
5299
- `Confluence ${method} ${path29} failed: ${res.status} ${res.statusText} \u2014 ${text2}`
5432
+ `Confluence ${method} ${path30} failed: ${res.status} ${res.statusText} \u2014 ${text2}`
5300
5433
  );
5301
5434
  }
5302
5435
  if (res.status === 204) return null;
@@ -5397,28 +5530,28 @@ __export(config_exports, {
5397
5530
  saveConfluenceConfig: () => saveConfluenceConfig,
5398
5531
  validateProvider: () => validateProvider
5399
5532
  });
5400
- import fs20 from "fs/promises";
5401
- import path26 from "path";
5533
+ import fs21 from "fs/promises";
5534
+ import path27 from "path";
5402
5535
  import os3 from "os";
5403
5536
  import { execFile as execFile16 } from "child_process";
5404
5537
  import { promisify as promisify16 } from "util";
5405
5538
  function configDir() {
5406
- return path26.join(os3.homedir(), ".mason");
5539
+ return path27.join(os3.homedir(), ".mason");
5407
5540
  }
5408
5541
  function configFile() {
5409
- return path26.join(configDir(), "config.json");
5542
+ return path27.join(configDir(), "config.json");
5410
5543
  }
5411
5544
  async function loadConfig() {
5412
5545
  try {
5413
- const raw = await fs20.readFile(configFile(), "utf-8");
5546
+ const raw = await fs21.readFile(configFile(), "utf-8");
5414
5547
  return JSON.parse(raw);
5415
5548
  } catch {
5416
5549
  return null;
5417
5550
  }
5418
5551
  }
5419
5552
  async function saveConfig(config) {
5420
- await fs20.mkdir(configDir(), { recursive: true });
5421
- await fs20.writeFile(configFile(), JSON.stringify(config, null, 2), "utf-8");
5553
+ await fs21.mkdir(configDir(), { recursive: true });
5554
+ await fs21.writeFile(configFile(), JSON.stringify(config, null, 2), "utf-8");
5422
5555
  }
5423
5556
  function getDefaultModel(provider) {
5424
5557
  return DEFAULT_MODELS[provider];
@@ -5578,21 +5711,21 @@ var init_renderer = __esm({
5578
5711
  });
5579
5712
 
5580
5713
  // src/confluence/diff.ts
5581
- import fs21 from "fs/promises";
5582
- import path27 from "path";
5583
- import { createHash as createHash6 } from "crypto";
5714
+ import fs22 from "fs/promises";
5715
+ import path28 from "path";
5716
+ import { createHash as createHash7 } from "crypto";
5584
5717
  function hashDescription(description) {
5585
- return createHash6("sha256").update(description, "utf8").digest("hex");
5718
+ return createHash7("sha256").update(description, "utf8").digest("hex");
5586
5719
  }
5587
5720
  function syncStateDir(rootDir) {
5588
- return path27.join(rootDir, ".mason");
5721
+ return path28.join(rootDir, ".mason");
5589
5722
  }
5590
5723
  function syncStatePath(rootDir) {
5591
- return path27.join(syncStateDir(rootDir), "confluence-sync.json");
5724
+ return path28.join(syncStateDir(rootDir), "confluence-sync.json");
5592
5725
  }
5593
5726
  async function loadSyncState(rootDir) {
5594
5727
  try {
5595
- const raw = await fs21.readFile(syncStatePath(rootDir), "utf-8");
5728
+ const raw = await fs22.readFile(syncStatePath(rootDir), "utf-8");
5596
5729
  const parsed = JSON.parse(raw);
5597
5730
  if (parsed.version !== 2) return null;
5598
5731
  return parsed;
@@ -5601,8 +5734,8 @@ async function loadSyncState(rootDir) {
5601
5734
  }
5602
5735
  }
5603
5736
  async function saveSyncState(rootDir, state) {
5604
- await fs21.mkdir(syncStateDir(rootDir), { recursive: true });
5605
- await fs21.writeFile(
5737
+ await fs22.mkdir(syncStateDir(rootDir), { recursive: true });
5738
+ await fs22.writeFile(
5606
5739
  syncStatePath(rootDir),
5607
5740
  JSON.stringify(state, null, 2),
5608
5741
  "utf-8"
@@ -6152,12 +6285,12 @@ var init_sync = __esm({
6152
6285
  init_provenance();
6153
6286
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6154
6287
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6155
- import { z as z18 } from "zod";
6288
+ import { z as z19 } from "zod";
6156
6289
 
6157
6290
  // src/mcp/tools.ts
6158
6291
  init_repair();
6159
- import fs22 from "fs/promises";
6160
- import path28 from "path";
6292
+ import fs23 from "fs/promises";
6293
+ import path29 from "path";
6161
6294
  import { execFile as execFile18 } from "child_process";
6162
6295
  import { promisify as promisify18 } from "util";
6163
6296
 
@@ -7062,7 +7195,7 @@ async function buildContext(dir) {
7062
7195
  };
7063
7196
  }
7064
7197
  async function analyzeProject(dir) {
7065
- const rootDir = path28.resolve(dir);
7198
+ const rootDir = path29.resolve(dir);
7066
7199
  const context = await buildContext(rootDir);
7067
7200
  const results = await runAll(context);
7068
7201
  const projectSnapshot = await detectProjectSnapshot(rootDir);
@@ -7117,7 +7250,7 @@ async function detectProjectSnapshot(rootDir) {
7117
7250
  const present = [];
7118
7251
  for (const file of buildFiles) {
7119
7252
  try {
7120
- await fs22.access(path28.join(rootDir, file));
7253
+ await fs23.access(path29.join(rootDir, file));
7121
7254
  present.push(file);
7122
7255
  } catch {
7123
7256
  }
@@ -7159,7 +7292,7 @@ async function detectProjectSnapshot(rootDir) {
7159
7292
  const sourceFiles = await access.list();
7160
7293
  const fileCounts = {};
7161
7294
  for (const file of sourceFiles) {
7162
- const ext = path28.extname(file).slice(1);
7295
+ const ext = path29.extname(file).slice(1);
7163
7296
  fileCounts[ext] = (fileCounts[ext] ?? 0) + 1;
7164
7297
  }
7165
7298
  return {
@@ -7170,7 +7303,7 @@ async function detectProjectSnapshot(rootDir) {
7170
7303
  };
7171
7304
  }
7172
7305
  async function getCodeSamples(dir, count3 = 15) {
7173
- const rootDir = path28.resolve(dir);
7306
+ const rootDir = path29.resolve(dir);
7174
7307
  const samples = await sampleFiles(rootDir, count3);
7175
7308
  const output = {
7176
7309
  note: "These are previews (first ~60 lines). Read the file directly with your own tools to see it in full.",
@@ -7213,7 +7346,7 @@ async function unmappedContextResponse(rootDir) {
7213
7346
  });
7214
7347
  }
7215
7348
  async function getProjectStructure(dir) {
7216
- const rootDir = path28.resolve(dir);
7349
+ const rootDir = path29.resolve(dir);
7217
7350
  const allFiles = await (await createFileAccess(rootDir)).list("**/*");
7218
7351
  const dirInfo = /* @__PURE__ */ new Map();
7219
7352
  for (const file of allFiles) {
@@ -7225,7 +7358,7 @@ async function getProjectStructure(dir) {
7225
7358
  }
7226
7359
  const info = dirInfo.get(dirPath);
7227
7360
  info.fileCount++;
7228
- const ext = path28.extname(file).slice(1);
7361
+ const ext = path29.extname(file).slice(1);
7229
7362
  if (ext) {
7230
7363
  info.extensions.set(ext, (info.extensions.get(ext) ?? 0) + 1);
7231
7364
  }
@@ -7270,7 +7403,7 @@ async function buildChangedFilePreviews(rootDir, changedFiles) {
7270
7403
  return previews2;
7271
7404
  }
7272
7405
  async function getSnapshot(dir) {
7273
- const rootDir = path28.resolve(dir);
7406
+ const rootDir = path29.resolve(dir);
7274
7407
  const snapshot = await loadSnapshot(rootDir);
7275
7408
  if (!snapshot) {
7276
7409
  return unmappedContextResponse(rootDir);
@@ -7371,7 +7504,7 @@ function driftHint(report) {
7371
7504
  return "Read the changed files under staleFeatures/staleFlows, update those entries (fold unmappedFiles into the right features), and call save_snapshot with only the affected entries \u2014 unchanged entries are preserved. Drop ghostFiles from any entries that reference them, and delete features/flows that no longer exist via save_snapshot's removeFeatures/removeFlows.";
7372
7505
  }
7373
7506
  async function checkDrift(dir) {
7374
- const rootDir = path28.resolve(dir);
7507
+ const rootDir = path29.resolve(dir);
7375
7508
  const report = await computeDrift(rootDir);
7376
7509
  if (!report) {
7377
7510
  return JSON.stringify({
@@ -7402,7 +7535,7 @@ async function checkDrift(dir) {
7402
7535
  return JSON.stringify({ exists: true, ...report, verification, hint });
7403
7536
  }
7404
7537
  async function generateSnapshotBatch(dir, offset = 0, batchSize = DEFAULT_BATCH_SIZE, files) {
7405
- const rootDir = path28.resolve(dir);
7538
+ const rootDir = path29.resolve(dir);
7406
7539
  const scoped = files !== void 0 && files.length > 0;
7407
7540
  const scopeFiles = scoped ? sanitizePaths(rootDir, files) : void 0;
7408
7541
  const batch = await prepareSnapshotBatch(dir, offset, batchSize, scopeFiles);
@@ -7449,7 +7582,7 @@ async function generateSnapshotBatch(dir, offset = 0, batchSize = DEFAULT_BATCH_
7449
7582
  );
7450
7583
  }
7451
7584
  async function saveSnapshotPartial(dir, batchId, offset, features, flows) {
7452
- const rootDir = path28.resolve(dir);
7585
+ const rootDir = path29.resolve(dir);
7453
7586
  for (const feat of Object.values(features)) {
7454
7587
  feat.files = sanitizePaths(rootDir, feat.files);
7455
7588
  if (feat.tests) feat.tests = sanitizePaths(rootDir, feat.tests);
@@ -7478,7 +7611,7 @@ async function saveSnapshotPartial(dir, batchId, offset, features, flows) {
7478
7611
  );
7479
7612
  }
7480
7613
  async function reduceSnapshot(dir) {
7481
- const rootDir = path28.resolve(dir);
7614
+ const rootDir = path29.resolve(dir);
7482
7615
  const partials = await loadAllPartials(rootDir);
7483
7616
  if (partials.length === 0) {
7484
7617
  return JSON.stringify(
@@ -7540,7 +7673,7 @@ async function reduceSnapshot(dir) {
7540
7673
  );
7541
7674
  }
7542
7675
  async function fullAnalysis(dir) {
7543
- const rootDir = path28.resolve(dir);
7676
+ const rootDir = path29.resolve(dir);
7544
7677
  const [analysis, structure, samples, testMap, snapshot] = await Promise.all([
7545
7678
  analyzeProject(dir),
7546
7679
  getProjectStructure(dir),
@@ -7569,7 +7702,7 @@ function sanitizePaths(rootDir, files) {
7569
7702
  return sanitizeRepoPaths(files);
7570
7703
  }
7571
7704
  async function saveSnapshotData(dir, features, flows, removeFeatures = [], removeFlows = []) {
7572
- const rootDir = path28.resolve(dir);
7705
+ const rootDir = path29.resolve(dir);
7573
7706
  const gitHash = await getCurrentGitHash(rootDir);
7574
7707
  const now = (/* @__PURE__ */ new Date()).toISOString();
7575
7708
  for (const feat of Object.values(features)) {
@@ -7653,7 +7786,7 @@ async function saveSnapshotData(dir, features, flows, removeFeatures = [], remov
7653
7786
  });
7654
7787
  }
7655
7788
  async function getImpact(dir, files) {
7656
- const rootDir = path28.resolve(dir);
7789
+ const rootDir = path29.resolve(dir);
7657
7790
  const { analyzeImpact: analyzeImpact2 } = await Promise.resolve().then(() => (init_impact(), impact_exports));
7658
7791
  const result = await analyzeImpact2(rootDir, files);
7659
7792
  return JSON.stringify(result, null, 2);
@@ -7662,7 +7795,7 @@ var VERIFY_DEFAULT_SAMPLE = 5;
7662
7795
  var VERIFY_MAX_FILES_PER_ENTRY = 8;
7663
7796
  var VERIFY_SKELETON_CHARS = 500;
7664
7797
  async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
7665
- const rootDir = path28.resolve(dir);
7798
+ const rootDir = path29.resolve(dir);
7666
7799
  const snapshot = await loadSnapshot(rootDir);
7667
7800
  if (!snapshot) {
7668
7801
  return JSON.stringify({
@@ -7727,7 +7860,7 @@ async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
7727
7860
  });
7728
7861
  }
7729
7862
  async function saveVerification(dir, verdicts) {
7730
- const rootDir = path28.resolve(dir);
7863
+ const rootDir = path29.resolve(dir);
7731
7864
  const snapshot = await loadSnapshot(rootDir);
7732
7865
  if (!snapshot) {
7733
7866
  return JSON.stringify({ exists: false, hint: "No concept map exists." });
@@ -7765,16 +7898,16 @@ async function saveVerification(dir, verdicts) {
7765
7898
  });
7766
7899
  }
7767
7900
  async function saveDecision(dir, input) {
7768
- const rootDir = path28.resolve(dir);
7901
+ const rootDir = path29.resolve(dir);
7769
7902
  const { upsertDecision: upsertDecision2 } = await Promise.resolve().then(() => (init_decisions(), decisions_exports));
7770
7903
  const result = await upsertDecision2(rootDir, input);
7771
7904
  return JSON.stringify(result);
7772
7905
  }
7773
7906
  async function reviewDecision2(dir, input) {
7774
- return JSON.stringify(await reviewDecision(path28.resolve(dir), input));
7907
+ return JSON.stringify(await reviewDecision(path29.resolve(dir), input));
7775
7908
  }
7776
7909
  async function getContext(dir, task, files) {
7777
- const rootDir = path28.resolve(dir);
7910
+ const rootDir = path29.resolve(dir);
7778
7911
  const { assembleContext: assembleContext2 } = await Promise.resolve().then(() => (init_assemble(), assemble_exports));
7779
7912
  const bundle = await assembleContext2(rootDir, task, files);
7780
7913
  return JSON.stringify(bundle);
@@ -7816,7 +7949,7 @@ async function masonInit(dir, options = {}) {
7816
7949
  return JSON.stringify(await setupProject2(dir, options), null, 2);
7817
7950
  }
7818
7951
  if (options.host) throw new Error("host applies only to mode: setup.");
7819
- const rootDir = path28.resolve(dir);
7952
+ const rootDir = path29.resolve(dir);
7820
7953
  const marker = await loadProjectMarker(rootDir);
7821
7954
  const mode = options.mode ?? "quickstart";
7822
7955
  const findings = await inspectOnboarding(rootDir, options.base, options.evidence);
@@ -7834,7 +7967,7 @@ async function masonInit(dir, options = {}) {
7834
7967
  );
7835
7968
  }
7836
7969
  async function masonCompleteInit(dir, options = {}) {
7837
- const rootDir = path28.resolve(dir);
7970
+ const rootDir = path29.resolve(dir);
7838
7971
  const existing = await loadProjectMarker(rootDir);
7839
7972
  const marker = {
7840
7973
  version: 1,
@@ -7942,7 +8075,7 @@ async function masonSetConfluence(input) {
7942
8075
  );
7943
8076
  }
7944
8077
  async function exportToConfluenceTool(dir, overrides) {
7945
- const rootDir = path28.resolve(dir);
8078
+ const rootDir = path29.resolve(dir);
7946
8079
  const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
7947
8080
  const { exportToConfluence: exportToConfluence2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
7948
8081
  const config = await loadConfig2();
@@ -7980,7 +8113,7 @@ function createMcpServer() {
7980
8113
  const server = new McpServer(
7981
8114
  {
7982
8115
  name: "mason",
7983
- version: "0.13.0"
8116
+ version: "0.14.0"
7984
8117
  },
7985
8118
  {
7986
8119
  instructions: 'Mason retrieves recorded decisions, file impact, and optional feature/flow maps. Use get_context with the task and known files, and get_impact before editing. Save learned rationale and constraints with save_decision; review and commit the local records through the project workflow. These tools work without initialization or a concept map. Consult trust and diagnostics: changed or unknown freshness needs source inspection, failed verification needs correction, and proposals are suggestions and legacy records are unreviewed. Accepted decisions are recorded team constraints, still subject to freshness checks. Use review_decision to prepare code evidence and record only authorized acceptance, reaffirmation, or retirement. Never invent a reviewer or treat these recorded identities as authenticated approval. mason_init returns documentation audit and committed-diff review findings with a quickstart guide. Use mode: "map" only when a full architecture map is requested. A missing map is not a setup failure; use decisions and source evidence. get_snapshot provides architecture navigation when a map is available. The mason-audit and mason-review CLIs also work without setup.'
@@ -7990,11 +8123,11 @@ function createMcpServer() {
7990
8123
  "mason_init",
7991
8124
  "Inspect this project now: returns documentation audit findings, committed-diff review findings, decision/map status, and a quickstart playbook. Quickstart and map modes are read-only and deterministic. Explicit mode: setup installs a pinned project runtime, MCP configuration, instructions and lifecycle hooks while retaining original audit evidence; use it only when the user requests setup. Optional host selects codex or claude. Optional base selects the review comparison; evidence imports CI manifests with check outcomes, commit freshness, and links to changed files and accepted decisions. mode: map returns the full Map-Reduce build workflow. Repeat calls refresh findings even after setup.",
7992
8125
  {
7993
- dir: z18.string().describe("Absolute path to the project root directory"),
7994
- mode: z18.enum(["quickstart", "map", "setup"]).optional().default("quickstart").describe("Quickstart inspects without edits; map requests an architecture build; setup installs the shared onboarding flow."),
7995
- host: z18.enum(["codex", "claude"]).optional().describe("Assistant to configure in setup mode; inferred only when unambiguous."),
7996
- base: z18.string().optional().describe("Git ref for committed-diff review. Defaults to the first available main branch ref."),
7997
- evidence: z18.array(z18.string()).max(10).optional().describe("Repository-local CI evidence manifests to include in the review. Imports Vitest JSON and SARIF without executing check commands.")
8126
+ dir: z19.string().describe("Absolute path to the project root directory"),
8127
+ mode: z19.enum(["quickstart", "map", "setup"]).optional().default("quickstart").describe("Quickstart inspects without edits; map requests an architecture build; setup installs the shared onboarding flow."),
8128
+ host: z19.enum(["codex", "claude"]).optional().describe("Assistant to configure in setup mode; inferred only when unambiguous."),
8129
+ base: z19.string().optional().describe("Git ref for committed-diff review. Defaults to the first available main branch ref."),
8130
+ evidence: z19.array(z19.string()).max(10).optional().describe("Repository-local CI evidence manifests to include in the review. Imports Vitest JSON and SARIF without executing check commands.")
7998
8131
  },
7999
8132
  async ({ dir, mode, host, base, evidence }) => {
8000
8133
  const result = await masonInit(dir, { mode, host, base, evidence });
@@ -8005,8 +8138,8 @@ function createMcpServer() {
8005
8138
  "mason_automation",
8006
8139
  "Inspect installed automation and observed host events, or resume/check retained documentation repair evidence across sessions. status is read-only; check saves local baselines and verification reports without editing source or approving advisories. Returns concise results with a full report path. Works without a map.",
8007
8140
  {
8008
- dir: z18.string().describe("Absolute path to the project directory"),
8009
- action: z18.enum(["status", "check"]).describe("Inspect configuration and receipts, or capture/resume and verify original audit evidence.")
8141
+ dir: z19.string().describe("Absolute path to the project directory"),
8142
+ action: z19.enum(["status", "check"]).describe("Inspect configuration and receipts, or capture/resume and verify original audit evidence.")
8010
8143
  },
8011
8144
  async ({ dir, action }) => ({ content: [{ type: "text", text: await masonAutomation(dir, action) }] })
8012
8145
  );
@@ -8014,10 +8147,10 @@ function createMcpServer() {
8014
8147
  "mason_repair",
8015
8148
  "Track documentation repairs against original audit evidence. prepare saves a local baseline and returns a scoped work order; verify reads that baseline and reports resolved, unresolved, review-required, unverified, and new findings. Suppressed advisories remain unresolved. Does not edit documentation or approve decisions. No map required.",
8016
8149
  {
8017
- dir: z18.string().describe("Absolute path to the project root directory"),
8018
- action: z18.enum(["prepare", "verify"]),
8019
- baselinePath: z18.string().optional().describe("Original baseline path returned by prepare; required for verify."),
8020
- checks: z18.array(z18.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"])).min(1).optional().describe("Optional audit check subset for prepare. Verification always uses the original checks.")
8150
+ dir: z19.string().describe("Absolute path to the project root directory"),
8151
+ action: z19.enum(["prepare", "verify"]),
8152
+ baselinePath: z19.string().optional().describe("Original baseline path returned by prepare; required for verify."),
8153
+ checks: z19.array(z19.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"])).min(1).optional().describe("Optional audit check subset for prepare. Verification always uses the original checks.")
8021
8154
  },
8022
8155
  async ({ dir, action, baselinePath, checks }) => {
8023
8156
  const result = await masonRepair(dir, { action, baselinePath, checks });
@@ -8028,8 +8161,8 @@ function createMcpServer() {
8028
8161
  "mason_complete_init",
8029
8162
  "Record completion of assistant instruction setup in .mason/project.json. Other tools work without this marker. Repeated calls preserve the original setup time and existing settings; pass confluenceConfigured only to change that setting.",
8030
8163
  {
8031
- dir: z18.string().describe("Absolute path to the project root directory"),
8032
- confluenceConfigured: z18.boolean().optional().describe("Set the Confluence setup status; omit to preserve the existing value")
8164
+ dir: z19.string().describe("Absolute path to the project root directory"),
8165
+ confluenceConfigured: z19.boolean().optional().describe("Set the Confluence setup status; omit to preserve the existing value")
8033
8166
  },
8034
8167
  async ({ dir, confluenceConfigured }) => {
8035
8168
  const result = await masonCompleteInit(dir, { confluenceConfigured });
@@ -8040,11 +8173,11 @@ function createMcpServer() {
8040
8173
  "mason_set_confluence",
8041
8174
  "Configure Confluence credentials. Two-step flow: (1) call without `spaceKey` to validate the credentials and receive a list of available spaces \u2014 relay them to the user. (2) call again with the same `baseUrl`/`email`/`apiToken` plus the chosen `spaceKey` to persist. Credentials are stored in `~/.mason/config.json`. Warn the user that the API token will be visible in chat history before they paste it.",
8042
8175
  {
8043
- baseUrl: z18.string().describe("Confluence base URL. Accepts `acme`, `acme.atlassian.net`, or `https://acme.atlassian.net` (normalized automatically)."),
8044
- email: z18.string().describe("User's Atlassian account email"),
8045
- apiToken: z18.string().describe("API token from id.atlassian.com/manage-profile/security/api-tokens"),
8046
- spaceKey: z18.string().optional().describe("Confluence space key. Omit on the first call to list available spaces."),
8047
- parentPageId: z18.string().optional().describe("Optional parent page ID under which Mason's index page is created")
8176
+ baseUrl: z19.string().describe("Confluence base URL. Accepts `acme`, `acme.atlassian.net`, or `https://acme.atlassian.net` (normalized automatically)."),
8177
+ email: z19.string().describe("User's Atlassian account email"),
8178
+ apiToken: z19.string().describe("API token from id.atlassian.com/manage-profile/security/api-tokens"),
8179
+ spaceKey: z19.string().optional().describe("Confluence space key. Omit on the first call to list available spaces."),
8180
+ parentPageId: z19.string().optional().describe("Optional parent page ID under which Mason's index page is created")
8048
8181
  },
8049
8182
  async ({ baseUrl, email, apiToken, spaceKey, parentPageId }) => {
8050
8183
  const result = await masonSetConfluence({
@@ -8061,7 +8194,7 @@ function createMcpServer() {
8061
8194
  "full_analysis",
8062
8195
  "One-shot orientation for a project WITHOUT a concept map (get_snapshot returned exists:false). Returns git history stats, project structure with file counts, curated code sample previews (~60 lines each), and test-to-source mapping. On a mapped project, prefer get_snapshot \u2014 it is cheaper and answers feature/architecture questions directly.",
8063
8196
  {
8064
- dir: z18.string().describe("Absolute path to the project root directory")
8197
+ dir: z19.string().describe("Absolute path to the project root directory")
8065
8198
  },
8066
8199
  async ({ dir }) => {
8067
8200
  const result = await fullAnalysis(dir);
@@ -8074,7 +8207,7 @@ function createMcpServer() {
8074
8207
  "analyze_project",
8075
8208
  "Run git history analysis on a codebase. Returns commit convention patterns, stale directories, and frequently changed files. These are aggregate stats across hundreds of commits that would be expensive to compute manually.",
8076
8209
  {
8077
- dir: z18.string().describe("Absolute path to the project root directory")
8210
+ dir: z19.string().describe("Absolute path to the project root directory")
8078
8211
  },
8079
8212
  async ({ dir }) => {
8080
8213
  const result = await analyzeProject(dir);
@@ -8087,8 +8220,8 @@ function createMcpServer() {
8087
8220
  "get_code_samples",
8088
8221
  "Get previews (first ~60 lines) of representative source files from the codebase. Includes entry points, config files, hot files (frequently changed), test examples, and one file per directory for breadth. Read files natively for full content.",
8089
8222
  {
8090
- dir: z18.string().describe("Absolute path to the project root directory"),
8091
- count: z18.number().optional().default(15).describe("Maximum number of files to sample (default: 15)")
8223
+ dir: z19.string().describe("Absolute path to the project root directory"),
8224
+ count: z19.number().optional().default(15).describe("Maximum number of files to sample (default: 15)")
8092
8225
  },
8093
8226
  async ({ dir, count: count3 }) => {
8094
8227
  const result = await getCodeSamples(dir, count3);
@@ -8101,7 +8234,7 @@ function createMcpServer() {
8101
8234
  "get_snapshot",
8102
8235
  "Return the optional feature-to-file architecture map with drift and trust evidence. If no map exists, returns exists:false plus project structure, Git signals, and test pairs. Decision capture, get_context, and get_impact still work. No initialization required.",
8103
8236
  {
8104
- dir: z18.string().describe("Absolute path to the project root directory")
8237
+ dir: z19.string().describe("Absolute path to the project root directory")
8105
8238
  },
8106
8239
  async ({ dir }) => {
8107
8240
  const result = await getSnapshot(dir);
@@ -8114,9 +8247,9 @@ function createMcpServer() {
8114
8247
  "get_context",
8115
8248
  "Assemble task context: matching decisions with rationale, approval, owner, sources, last review, and freshness, plus related tests, file impact, and any available map entries. Proposals are suggestions; legacy records are unreviewed; accepted decisions are constraints subject to freshness. No initialization or map required. Pass task and optional files. map.status and diagnostics preserve missing or invalid knowledge. Impact covers up to three unique files, expanding directory anchors.",
8116
8249
  {
8117
- dir: z18.string().describe("Absolute path to the project root directory"),
8118
- task: z18.string().describe("The task, bug, or change request in natural language \u2014 e.g. 'add rate limiting to the API client' or a ticket description"),
8119
- files: z18.array(z18.string()).optional().describe("Optional file paths already known to be involved (e.g. from a diff or stack trace). Entries containing them are boosted above pure text matches.")
8250
+ dir: z19.string().describe("Absolute path to the project root directory"),
8251
+ task: z19.string().describe("The task, bug, or change request in natural language \u2014 e.g. 'add rate limiting to the API client' or a ticket description"),
8252
+ files: z19.array(z19.string()).optional().describe("Optional file paths already known to be involved (e.g. from a diff or stack trace). Entries containing them are boosted above pure text matches.")
8120
8253
  },
8121
8254
  async ({ dir, task, files }) => {
8122
8255
  const result = await getContext(dir, task, files);
@@ -8132,10 +8265,10 @@ function createMcpServer() {
8132
8265
  "generate_snapshot_batch",
8133
8266
  "Map step of the concept-map build. Returns one batch of source files (skeletons of every file in the batch plus a few deeper-read bodies for grounding), along with a system prompt instructing you to derive features and flows for ONLY this batch. Call repeatedly with the returned `nextOffset` until it is null, calling `save_partial_snapshot` between each call. Use product-natural feature names so partials merge cleanly in the reduce step.",
8134
8267
  {
8135
- dir: z18.string().describe("Absolute path to the project root directory"),
8136
- offset: z18.number().int().optional().describe("0-indexed file offset to start the batch at. Omit on the first call; pass the `nextOffset` from the previous response for subsequent calls."),
8137
- batchSize: z18.number().int().optional().describe("Files per batch. Defaults to 50."),
8138
- files: z18.array(z18.string()).optional().describe("Scope the batch walk to this explicit file list \u2014 e.g. the drift set from mason_check_drift (changedFiles + unmappedFiles). Pass the SAME list on every batch call of one refresh run. Triggers refresh mode: reduce_snapshot will merge the partials into the existing map instead of rebuilding it.")
8268
+ dir: z19.string().describe("Absolute path to the project root directory"),
8269
+ offset: z19.number().int().optional().describe("0-indexed file offset to start the batch at. Omit on the first call; pass the `nextOffset` from the previous response for subsequent calls."),
8270
+ batchSize: z19.number().int().optional().describe("Files per batch. Defaults to 50."),
8271
+ files: z19.array(z19.string()).optional().describe("Scope the batch walk to this explicit file list \u2014 e.g. the drift set from mason_check_drift (changedFiles + unmappedFiles). Pass the SAME list on every batch call of one refresh run. Triggers refresh mode: reduce_snapshot will merge the partials into the existing map instead of rebuilding it.")
8139
8272
  },
8140
8273
  async ({ dir, offset, batchSize, files }) => {
8141
8274
  const result = await generateSnapshotBatch(dir, offset, batchSize, files);
@@ -8148,23 +8281,23 @@ function createMcpServer() {
8148
8281
  "save_partial_snapshot",
8149
8282
  "Persist the partial concept map you derived for one batch. Call this once per batch, with the `batchId` from the `generate_snapshot_batch` response. Partials accumulate in `.mason/partial-snapshots/` and are merged in the reduce step.",
8150
8283
  {
8151
- dir: z18.string().describe("Absolute path to the project root directory"),
8152
- batchId: z18.string().describe("The `batchId` returned by `generate_snapshot_batch`."),
8153
- offset: z18.number().int().describe("The `offset` returned by `generate_snapshot_batch`. Used to order partials in the reduce step."),
8154
- features: z18.record(
8155
- z18.object({
8156
- description: z18.string(),
8157
- files: z18.array(z18.string()),
8158
- tests: z18.array(z18.string()).optional(),
8159
- type: z18.enum(["capability", "infrastructure"]).optional().describe(
8284
+ dir: z19.string().describe("Absolute path to the project root directory"),
8285
+ batchId: z19.string().describe("The `batchId` returned by `generate_snapshot_batch`."),
8286
+ offset: z19.number().int().describe("The `offset` returned by `generate_snapshot_batch`. Used to order partials in the reduce step."),
8287
+ features: z19.record(
8288
+ z19.object({
8289
+ description: z19.string(),
8290
+ files: z19.array(z19.string()),
8291
+ tests: z19.array(z19.string()).optional(),
8292
+ type: z19.enum(["capability", "infrastructure"]).optional().describe(
8160
8293
  '"capability" (user-facing functionality) or "infrastructure" (internal plumbing with no end user \u2014 DI/service wiring, config, logging, adapters). Defaults to "capability".'
8161
8294
  )
8162
8295
  })
8163
8296
  ).describe("Partial features for this batch only \u2014 files outside the batch will be added by other partials."),
8164
- flows: z18.record(
8165
- z18.object({
8166
- description: z18.string(),
8167
- chain: z18.array(z18.string())
8297
+ flows: z19.record(
8298
+ z19.object({
8299
+ description: z19.string(),
8300
+ chain: z19.array(z19.string())
8168
8301
  })
8169
8302
  ).describe("Partial flows whose entire chain is in this batch. Cross-batch flows are reconstructed in reduce.")
8170
8303
  },
@@ -8179,7 +8312,7 @@ function createMcpServer() {
8179
8312
  "reduce_snapshot",
8180
8313
  "Reduce step of the concept-map build. Returns every partial snapshot plus a system prompt asking you to merge them into one coherent project-wide map. Resolve platform variants into single product features, dedupe near-duplicates, and ensure no file is dropped. After producing the unified map, call `save_snapshot` to persist it (this also clears the partials).",
8181
8314
  {
8182
- dir: z18.string().describe("Absolute path to the project root directory")
8315
+ dir: z19.string().describe("Absolute path to the project root directory")
8183
8316
  },
8184
8317
  async ({ dir }) => {
8185
8318
  const result = await reduceSnapshot(dir);
@@ -8192,25 +8325,25 @@ function createMcpServer() {
8192
8325
  "save_snapshot",
8193
8326
  "Save a concept-to-files map as a persistent project snapshot. Maps feature names and data flows to the files that implement them. Persists across conversations \u2014 future sessions can call get_snapshot to instantly find relevant files. No API key needed \u2014 you are the LLM generating the map.",
8194
8327
  {
8195
- dir: z18.string().describe("Absolute path to the project root directory"),
8196
- features: z18.record(
8197
- z18.object({
8198
- description: z18.string().describe("One-line description of the feature"),
8199
- files: z18.array(z18.string()).describe("File paths that implement this feature"),
8200
- tests: z18.array(z18.string()).optional().describe("Test file paths for this feature"),
8201
- type: z18.enum(["capability", "infrastructure"]).optional().describe(
8328
+ dir: z19.string().describe("Absolute path to the project root directory"),
8329
+ features: z19.record(
8330
+ z19.object({
8331
+ description: z19.string().describe("One-line description of the feature"),
8332
+ files: z19.array(z19.string()).describe("File paths that implement this feature"),
8333
+ tests: z19.array(z19.string()).optional().describe("Test file paths for this feature"),
8334
+ type: z19.enum(["capability", "infrastructure"]).optional().describe(
8202
8335
  'Classification: "capability" for user-facing functionality, "infrastructure" for internal plumbing with no end user (DI/service wiring, config, logging, adapters). Capabilities are published to Confluence; infrastructure stays in the AI concept map only. Defaults to "capability".'
8203
8336
  )
8204
8337
  })
8205
8338
  ).describe("Map of feature names to their implementing files"),
8206
- flows: z18.record(
8207
- z18.object({
8208
- description: z18.string().describe("One-line description of the flow"),
8209
- chain: z18.array(z18.string()).describe("Ordered list of file paths showing data/call flow")
8339
+ flows: z19.record(
8340
+ z19.object({
8341
+ description: z19.string().describe("One-line description of the flow"),
8342
+ chain: z19.array(z19.string()).describe("Ordered list of file paths showing data/call flow")
8210
8343
  })
8211
8344
  ).describe("Map of flow names to ordered file chains"),
8212
- removeFeatures: z18.array(z18.string()).optional().describe("Feature names to delete from the existing map \u2014 for features that were renamed or no longer exist. Applied before merging; only meaningful on incremental saves."),
8213
- removeFlows: z18.array(z18.string()).optional().describe("Flow names to delete from the existing map. Applied before merging; only meaningful on incremental saves.")
8345
+ removeFeatures: z19.array(z19.string()).optional().describe("Feature names to delete from the existing map \u2014 for features that were renamed or no longer exist. Applied before merging; only meaningful on incremental saves."),
8346
+ removeFlows: z19.array(z19.string()).optional().describe("Flow names to delete from the existing map. Applied before merging; only meaningful on incremental saves.")
8214
8347
  },
8215
8348
  async ({ dir, features, flows, removeFeatures, removeFlows }) => {
8216
8349
  const result = await saveSnapshotData(
@@ -8229,17 +8362,17 @@ function createMcpServer() {
8229
8362
  "save_decision",
8230
8363
  "Capture or revise a decision proposal with rationale, anchors, optional owner, sources, and a known actor. No setup or map required. Writes a local record and preserves content history. Changes create a pending proposal while the last accepted revision remains operative; unchanged content does not re-verify or refresh it. Use review_decision for authorized acceptance or reaffirmation. A proposal cannot supersede a record with an operative accepted revision; review its replacement and retire the original separately.",
8231
8364
  {
8232
- dir: z18.string().describe("Absolute path to the project root directory"),
8233
- title: z18.string().max(80).describe("Short, specific headline \u2014 becomes the stable record id"),
8234
- body: z18.string().max(1500).describe("The knowledge itself: what was tried/decided, why, and what to avoid. Must contain information NOT derivable by reading the code."),
8235
- category: z18.enum(["decision", "gotcha", "deprecation", "convention"]),
8236
- files: z18.array(z18.string()).optional().describe("Repo-relative files or directory prefixes this applies to. Matching is shared by retrieval, hooks, review, and drift checking; changes flag the decision for re-verification."),
8237
- id: z18.string().optional().describe("Existing id to revise. Changed content becomes a proposal; unchanged content leaves the review and freshness untouched."),
8238
- supersedes: z18.string().optional().describe("Id of an unreviewed record or proposal with no accepted revision to replace. Operative accepted decisions require separate review and retirement."),
8365
+ dir: z19.string().describe("Absolute path to the project root directory"),
8366
+ title: z19.string().max(80).describe("Short, specific headline \u2014 becomes the stable record id"),
8367
+ body: z19.string().max(1500).describe("The knowledge itself: what was tried/decided, why, and what to avoid. Must contain information NOT derivable by reading the code."),
8368
+ category: z19.enum(["decision", "gotcha", "deprecation", "convention"]),
8369
+ files: z19.array(z19.string()).optional().describe("Repo-relative files or directory prefixes this applies to. Matching is shared by retrieval, hooks, review, and drift checking; changes flag the decision for re-verification."),
8370
+ id: z19.string().optional().describe("Existing id to revise. Changed content becomes a proposal; unchanged content leaves the review and freshness untouched."),
8371
+ supersedes: z19.string().optional().describe("Id of an unreviewed record or proposal with no accepted revision to replace. Operative accepted decisions require separate review and retirement."),
8239
8372
  owner: attributionSchema.shape.owner.describe("Responsible person or team, when known. Null clears it. Required for acceptance."),
8240
8373
  sources: attributionSchema.shape.sources.describe("Known PR, issue, incident, discussion, or document references. Omit to preserve; [] clears. At least one is required for acceptance."),
8241
8374
  actor: attributionSchema.shape.actor.describe("Known person or agent recording this revision. Omit if unknown; do not infer from Git identity."),
8242
- force: z18.boolean().optional().describe("Save even when a near-duplicate was detected")
8375
+ force: z19.boolean().optional().describe("Save even when a near-duplicate was detected")
8243
8376
  },
8244
8377
  async ({ dir, title, body, category, files, id, supersedes, force, owner, sources, actor }) => {
8245
8378
  const result = await saveDecision(dir, {
@@ -8261,12 +8394,12 @@ function createMcpServer() {
8261
8394
  "review_decision",
8262
8395
  "Prepare a decision review: returns the full record and history, any operative accepted revision, provenance, changes and previews for both sets of anchors, and a reviewToken. Then record accept, reaffirm, or retire with that token, the authorized reviewer, and a reason. Acceptance replaces the operative revision; retirement withdraws the entire record including its proposal. Acceptance requires owner, source, readable Git HEAD, and committed anchor changes. Changed records or code invalidate the token. Identities and approvals are recorded assertions for normal PR review, not authenticated proof.",
8263
8396
  {
8264
- dir: z18.string().describe("Absolute path to the project root directory"),
8265
- id: z18.string().regex(/^[a-zA-Z0-9_-]+$/).describe("Decision id from get_context or save_decision"),
8266
- action: z18.enum(["prepare", "accept", "reaffirm", "retire"]).optional().default("prepare").describe("Prepare is read-only. Other actions record an explicitly authorized review."),
8267
- reviewer: z18.string().trim().min(1).max(200).optional().describe("Identity of the actual reviewer; required for a verdict. Never invent one."),
8268
- note: z18.string().trim().min(1).max(1500).optional().describe("Review rationale; required for a verdict. Cite evidence for the decision."),
8269
- reviewToken: z18.string().regex(/^[a-f0-9]{64}$/).optional().describe("Token from the prepared review; rejects stale record or code revisions")
8397
+ dir: z19.string().describe("Absolute path to the project root directory"),
8398
+ id: z19.string().regex(/^[a-zA-Z0-9_-]+$/).describe("Decision id from get_context or save_decision"),
8399
+ action: z19.enum(["prepare", "accept", "reaffirm", "retire"]).optional().default("prepare").describe("Prepare is read-only. Other actions record an explicitly authorized review."),
8400
+ reviewer: z19.string().trim().min(1).max(200).optional().describe("Identity of the actual reviewer; required for a verdict. Never invent one."),
8401
+ note: z19.string().trim().min(1).max(1500).optional().describe("Review rationale; required for a verdict. Cite evidence for the decision."),
8402
+ reviewToken: z19.string().regex(/^[a-f0-9]{64}$/).optional().describe("Token from the prepared review; rejects stale record or code revisions")
8270
8403
  },
8271
8404
  async ({ dir, ...input }) => {
8272
8405
  const result = await reviewDecision2(dir, input);
@@ -8277,7 +8410,7 @@ function createMcpServer() {
8277
8410
  "mason_check_drift",
8278
8411
  "Check how far the concept map has drifted from HEAD. Deterministic (git + filesystem, no LLM). Returns which features/flows are stale and the changed files behind them, new source files not yet mapped, ghost files (mapped but deleted), renames, and a `recommendation`: `up-to-date` (nothing to do), `incremental` (update just the stale entries via save_snapshot), or `full-rebuild` (re-run the Map-Reduce build). Call this before trusting the map in a long session, or periodically to keep the map and any synced wikis fresh.",
8279
8412
  {
8280
- dir: z18.string().describe("Absolute path to the project root directory")
8413
+ dir: z19.string().describe("Absolute path to the project root directory")
8281
8414
  },
8282
8415
  async ({ dir }) => {
8283
8416
  const result = await checkDrift(dir);
@@ -8290,8 +8423,8 @@ function createMcpServer() {
8290
8423
  "verify_snapshot",
8291
8424
  "Spot-check the concept map's CORRECTNESS (drift checks freshness; this checks entries were right to begin with). Returns a sample of entries \u2014 always the never-verified and least-recently-verified first \u2014 with skeletons of their claimed files, for you to judge whether the files actually implement what the entry claims. Report verdicts back via save_verification. Run periodically, or after an automated refresh wrote entries no human reviewed.",
8292
8425
  {
8293
- dir: z18.string().describe("Absolute path to the project root directory"),
8294
- sample: z18.number().int().optional().describe("Entries to sample (default 5)")
8426
+ dir: z19.string().describe("Absolute path to the project root directory"),
8427
+ sample: z19.number().int().optional().describe("Entries to sample (default 5)")
8295
8428
  },
8296
8429
  async ({ dir, sample }) => {
8297
8430
  const result = await verifySnapshot(dir, sample);
@@ -8302,11 +8435,11 @@ function createMcpServer() {
8302
8435
  "save_verification",
8303
8436
  "Record verify_snapshot verdicts. Entries judged ok are stamped verifiedAt; failures are flagged verificationFailed with your note and surface in get_context, get_snapshot, and mason_check_drift until corrected. Verdict notes are required for failures.",
8304
8437
  {
8305
- dir: z18.string().describe("Absolute path to the project root directory"),
8306
- verdicts: z18.record(
8307
- z18.object({
8308
- ok: z18.boolean(),
8309
- note: z18.string().optional().describe("One line on what's wrong \u2014 required when ok is false")
8438
+ dir: z19.string().describe("Absolute path to the project root directory"),
8439
+ verdicts: z19.record(
8440
+ z19.object({
8441
+ ok: z19.boolean(),
8442
+ note: z19.string().optional().describe("One line on what's wrong \u2014 required when ok is false")
8310
8443
  })
8311
8444
  ).describe("Entry name \u2192 verdict, exactly as returned by verify_snapshot")
8312
8445
  },
@@ -8319,8 +8452,8 @@ function createMcpServer() {
8319
8452
  "get_impact",
8320
8453
  "Trace the impact of changing files: historical co-change partners, references, and related tests. Deterministic, read-only, and usable without initialization, saved decisions, or a concept map.",
8321
8454
  {
8322
- dir: z18.string().describe("Absolute path to the project root directory"),
8323
- files: z18.array(z18.string()).describe("File paths or names to analyze (e.g., ['WeatherRepository.kt'] or ['src/services/auth.ts'])")
8455
+ dir: z19.string().describe("Absolute path to the project root directory"),
8456
+ files: z19.array(z19.string()).describe("File paths or names to analyze (e.g., ['WeatherRepository.kt'] or ['src/services/auth.ts'])")
8324
8457
  },
8325
8458
  async ({ dir, files }) => {
8326
8459
  const result = await getImpact(dir, files);
@@ -8333,12 +8466,12 @@ function createMcpServer() {
8333
8466
  "export_to_confluence",
8334
8467
  "Sync the project's concept map to Confluence as product-readable wiki pages: an index page, one page per feature (PM-language descriptions, no file paths), and a changelog page. Mason replaces managed page bodies; manual edits to those bodies are overwritten. Requires `mason_set_confluence` to have been called first.",
8335
8468
  {
8336
- dir: z18.string().describe("Absolute path to the project root directory"),
8337
- spaceKey: z18.string().optional().describe("Override the configured space key"),
8338
- parentPageId: z18.string().optional().describe("Override the configured parent page ID"),
8339
- indexPageTitle: z18.string().optional().describe("Title of the index page (default: 'Mason \u2014 System Map')"),
8340
- changelogPageTitle: z18.string().optional().describe("Title of the changelog page (default: 'Mason \u2014 Changelog')"),
8341
- featurePagePrefix: z18.string().optional().describe("Prefix for each feature page title (default: 'Feature: ')")
8469
+ dir: z19.string().describe("Absolute path to the project root directory"),
8470
+ spaceKey: z19.string().optional().describe("Override the configured space key"),
8471
+ parentPageId: z19.string().optional().describe("Override the configured parent page ID"),
8472
+ indexPageTitle: z19.string().optional().describe("Title of the index page (default: 'Mason \u2014 System Map')"),
8473
+ changelogPageTitle: z19.string().optional().describe("Title of the changelog page (default: 'Mason \u2014 Changelog')"),
8474
+ featurePagePrefix: z19.string().optional().describe("Prefix for each feature page title (default: 'Feature: ')")
8342
8475
  },
8343
8476
  async ({ dir, spaceKey, parentPageId, indexPageTitle, changelogPageTitle, featurePagePrefix }) => {
8344
8477
  const result = await exportToConfluenceTool(dir, {