mason-context 0.14.1 → 0.15.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.
@@ -1105,9 +1105,133 @@ var init_deleted_reference = __esm({
1105
1105
  }
1106
1106
  });
1107
1107
 
1108
- // src/audit/checks/new-module.ts
1109
- import fg2 from "fast-glob";
1108
+ // src/audit/inputs.ts
1109
+ import fs6 from "fs/promises";
1110
+ import * as nativeFs from "fs";
1110
1111
  import path9 from "path";
1112
+ import { execFile as execFile6 } from "child_process";
1113
+ import { promisify as promisify6 } from "util";
1114
+ import fg2 from "fast-glob";
1115
+ async function auditInputPath(root, file) {
1116
+ if (file === ".") return fs6.realpath(root);
1117
+ try {
1118
+ return await storePath(root, file);
1119
+ } catch (error) {
1120
+ if (error instanceof Error && error.message.startsWith("Symlink")) {
1121
+ throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
1122
+ }
1123
+ throw error;
1124
+ }
1125
+ }
1126
+ async function readAuditInput(root, file) {
1127
+ try {
1128
+ const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
1129
+ if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
1130
+ return value;
1131
+ } catch (error) {
1132
+ if (error.code === "ENOENT") return null;
1133
+ throw error;
1134
+ }
1135
+ }
1136
+ async function auditGlob(root, patterns, options) {
1137
+ if (Array.isArray(patterns) && !patterns.length) return [];
1138
+ const directories = /* @__PURE__ */ new Set();
1139
+ const readdir2 = ((directory, opts, callback) => {
1140
+ const relative = path9.relative(root, directory).split(path9.sep).join("/") || ".";
1141
+ directories.add(relative);
1142
+ if (directories.size > MAX_INPUTS) {
1143
+ callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
1144
+ return;
1145
+ }
1146
+ auditInputPath(root, relative).then(() => {
1147
+ nativeFs.readdir(directory, opts, callback);
1148
+ }, callback);
1149
+ });
1150
+ const lstat2 = ((file, callback) => {
1151
+ const relative = path9.relative(root, file).split(path9.sep).join("/") || ".";
1152
+ auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
1153
+ });
1154
+ const stream = fg2.stream(patterns, {
1155
+ cwd: root,
1156
+ ignore: options.ignore,
1157
+ onlyFiles: false,
1158
+ onlyDirectories: options.onlyDirectories,
1159
+ objectMode: true,
1160
+ followSymbolicLinks: options.followSymbolicLinks ?? true,
1161
+ fs: { readdir: readdir2, lstat: lstat2 },
1162
+ concurrency: 16
1163
+ });
1164
+ const matches = [];
1165
+ try {
1166
+ for await (const value of stream) {
1167
+ const entry = value;
1168
+ const file = entry.path;
1169
+ if (options.select && !options.select(file)) continue;
1170
+ await auditInputPath(root, file);
1171
+ if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
1172
+ matches.push(file);
1173
+ if (matches.length > MAX_INPUTS) {
1174
+ const largest = [...matches.reduce((counts, item) => {
1175
+ const dir = item.split("/")[0];
1176
+ counts.set(dir, (counts.get(dir) ?? 0) + 1);
1177
+ return counts;
1178
+ }, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
1179
+ throw new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} relevant paths. Largest directories: ${largest.map(([dir, count3]) => `${dir} (${count3})`).join(", ")}. Narrow this check's inputs.`);
1180
+ }
1181
+ }
1182
+ } finally {
1183
+ stream.destroy();
1184
+ }
1185
+ return matches.sort();
1186
+ }
1187
+ async function gitSourcePaths(root) {
1188
+ const { stdout } = await execute(
1189
+ "git",
1190
+ [
1191
+ "ls-files",
1192
+ "-z",
1193
+ "--cached",
1194
+ "--others",
1195
+ "--exclude-standard",
1196
+ "--",
1197
+ ...SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`)
1198
+ ],
1199
+ { cwd: root, maxBuffer: 32 * 1024 * 1024, timeout: 1e4 }
1200
+ );
1201
+ return [...new Set(stdout.split("\0").filter(Boolean))].sort();
1202
+ }
1203
+ async function gitIgnoredPaths(root, files) {
1204
+ if (!files.length) return /* @__PURE__ */ new Set();
1205
+ const operation = execute(
1206
+ "git",
1207
+ ["check-ignore", "-z", "--stdin"],
1208
+ { cwd: root, maxBuffer: 4 * 1024 * 1024, timeout: 1e4 }
1209
+ );
1210
+ operation.child.stdin.on("error", () => {
1211
+ });
1212
+ operation.child.stdin.end(files.join("\0") + "\0");
1213
+ try {
1214
+ return new Set((await operation).stdout.split("\0").filter(Boolean));
1215
+ } catch (error) {
1216
+ if (error.code === 1) return /* @__PURE__ */ new Set();
1217
+ throw error;
1218
+ }
1219
+ }
1220
+ var execute, MAX_INPUTS;
1221
+ var init_inputs = __esm({
1222
+ "src/audit/inputs.ts"() {
1223
+ "use strict";
1224
+ init_files();
1225
+ init_storage();
1226
+ execute = promisify6(execFile6);
1227
+ MAX_INPUTS = 1e5;
1228
+ }
1229
+ });
1230
+
1231
+ // src/audit/checks/new-module.ts
1232
+ import fg3 from "fast-glob";
1233
+ import fs7 from "fs/promises";
1234
+ import path10 from "path";
1111
1235
  function escapeRegExp(text2) {
1112
1236
  return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1113
1237
  }
@@ -1118,22 +1242,6 @@ function isMentioned(combinedDocs, name) {
1118
1242
  );
1119
1243
  return re.test(combinedDocs);
1120
1244
  }
1121
- async function listSubdirs(absDir) {
1122
- const dirs = await fg2("*", {
1123
- cwd: absDir,
1124
- onlyDirectories: true,
1125
- suppressErrors: true
1126
- });
1127
- return dirs.filter((d) => !DIR_DENYLIST.has(d)).sort();
1128
- }
1129
- async function countSourceFiles(absDir) {
1130
- const files = await fg2(SOURCE_GLOB, {
1131
- cwd: absDir,
1132
- ignore: SOURCE_IGNORE,
1133
- suppressErrors: true
1134
- });
1135
- return files.length;
1136
- }
1137
1245
  async function checkNewModules(ctx) {
1138
1246
  const result = emptyResult();
1139
1247
  if (ctx.docs.length === 0) return result;
@@ -1161,21 +1269,48 @@ async function checkNewModules(ctx) {
1161
1269
  return result;
1162
1270
  }
1163
1271
  async function moduleCandidates(root, combinedDocs) {
1272
+ const [config, sourcePaths] = await Promise.all([loadProjectConfig(root), gitSourcePaths(root)]);
1273
+ const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
1274
+ const sources = sourcePaths.filter((file) => !file.split("/").some((part) => part.startsWith(".")));
1275
+ const listSubdirs = async (dir) => {
1276
+ const entries = await fs7.readdir(await auditInputPath(root, dir), { withFileTypes: true });
1277
+ const dirs = entries.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink()) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)).map((entry) => path10.posix.join(dir, entry.name));
1278
+ const ignored = await gitIgnoredPaths(root, dirs);
1279
+ const visible = new Set(dirs.filter((dir2) => !ignored.has(dir2) || sources.some((file) => file.startsWith(dir2 + "/"))));
1280
+ return (await auditGlob(root, dir === "." ? "*" : `${fg3.escapePath(dir)}/*`, {
1281
+ ignore,
1282
+ onlyDirectories: true,
1283
+ label: "Module directory discovery",
1284
+ select: (file) => visible.has(file)
1285
+ })).map((file) => path10.posix.basename(file));
1286
+ };
1287
+ const countSourceFiles = async (dir) => {
1288
+ const files = sources.filter((file) => file.startsWith(dir + "/"));
1289
+ let count3 = 0;
1290
+ for (let offset = 0; offset < files.length; offset += 256) {
1291
+ count3 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg3.escapePath(file)), {
1292
+ ignore,
1293
+ followSymbolicLinks: false,
1294
+ label: `Source discovery in ${dir}`
1295
+ })).length;
1296
+ if (count3 > 1e5) throw new Error(`Source discovery in ${dir} exceeds 100,000 relevant files. Exclude generated source with .mason/config.json ignore patterns or narrow this audit's checks.`);
1297
+ }
1298
+ return count3;
1299
+ };
1164
1300
  const candidates = [];
1165
- for (const topDir of await listSubdirs(root)) {
1166
- const absTop = path9.join(root, topDir);
1301
+ for (const topDir of await listSubdirs(".")) {
1167
1302
  const topMentioned = isMentioned(combinedDocs, topDir);
1168
1303
  if (!topMentioned) {
1169
- const count3 = await countSourceFiles(absTop);
1304
+ const count3 = await countSourceFiles(topDir);
1170
1305
  if (count3 >= 1) candidates.push({ dir: topDir, sourceFileCount: count3 });
1171
1306
  continue;
1172
1307
  }
1173
- const subdirs = await listSubdirs(absTop);
1308
+ const subdirs = await listSubdirs(topDir);
1174
1309
  const mentioned = subdirs.filter((s) => isMentioned(combinedDocs, s));
1175
1310
  if (mentioned.length < ENUMERATION_THRESHOLD) continue;
1176
1311
  for (const sub of subdirs) {
1177
1312
  if (isMentioned(combinedDocs, sub)) continue;
1178
- const count3 = await countSourceFiles(path9.join(absTop, sub));
1313
+ const count3 = await countSourceFiles(`${topDir}/${sub}`);
1179
1314
  if (count3 >= SECOND_LEVEL_MIN_SOURCE_FILES) {
1180
1315
  candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount: count3 });
1181
1316
  }
@@ -1187,7 +1322,8 @@ var DIR_DENYLIST, SECOND_LEVEL_MIN_SOURCE_FILES, ENUMERATION_THRESHOLD;
1187
1322
  var init_new_module = __esm({
1188
1323
  "src/audit/checks/new-module.ts"() {
1189
1324
  "use strict";
1190
- init_snapshot();
1325
+ init_files();
1326
+ init_inputs();
1191
1327
  init_git();
1192
1328
  init_checks();
1193
1329
  DIR_DENYLIST = /* @__PURE__ */ new Set([
@@ -1215,19 +1351,10 @@ var init_new_module = __esm({
1215
1351
  });
1216
1352
 
1217
1353
  // src/audit/checks/stale-count.ts
1218
- import fs6 from "fs/promises";
1219
- import path10 from "path";
1220
- import fg3 from "fast-glob";
1221
- async function readIfExists(absPath) {
1222
- try {
1223
- return await fs6.readFile(absPath, "utf-8");
1224
- } catch {
1225
- return null;
1226
- }
1227
- }
1354
+ import path11 from "path";
1228
1355
  async function countGradleModules(root) {
1229
1356
  for (const name of ["settings.gradle.kts", "settings.gradle"]) {
1230
- const content2 = await readIfExists(path10.join(root, name));
1357
+ const content2 = await readAuditInput(root, name);
1231
1358
  if (content2 === null) continue;
1232
1359
  const members = [];
1233
1360
  for (const call of content2.matchAll(/include\s*\(([^)]*)\)/g)) {
@@ -1241,26 +1368,28 @@ async function countGradleModules(root) {
1241
1368
  return null;
1242
1369
  }
1243
1370
  async function countNpmWorkspaces(root) {
1244
- const pkgRaw = await readIfExists(path10.join(root, "package.json"));
1371
+ const pkgRaw = await readAuditInput(root, "package.json");
1245
1372
  if (pkgRaw !== null) {
1373
+ let globs = [];
1246
1374
  try {
1247
1375
  const pkg = JSON.parse(pkgRaw);
1248
- const globs = Array.isArray(pkg.workspaces) ? pkg.workspaces : Array.isArray(pkg.workspaces?.packages) ? pkg.workspaces.packages : [];
1249
- if (globs.length > 0) {
1250
- const matched = await fg3(
1251
- globs.map((g) => `${g.replace(/\/+$/, "")}/package.json`),
1252
- { cwd: root, ignore: ["**/node_modules/**"] }
1253
- );
1254
- return {
1255
- actual: matched.length,
1256
- countedFrom: "package.json workspaces",
1257
- members: matched.map((m) => path10.dirname(m)).sort()
1258
- };
1259
- }
1376
+ globs = Array.isArray(pkg.workspaces) ? pkg.workspaces : Array.isArray(pkg.workspaces?.packages) ? pkg.workspaces.packages : [];
1260
1377
  } catch {
1261
1378
  }
1379
+ if (globs.length > 0) {
1380
+ const matched = await auditGlob(
1381
+ root,
1382
+ globs.map((g) => `${g.replace(/\/+$/, "")}/package.json`),
1383
+ { ignore: ["**/node_modules/**"], label: "npm workspace discovery" }
1384
+ );
1385
+ return {
1386
+ actual: matched.length,
1387
+ countedFrom: "package.json workspaces",
1388
+ members: matched.map((m) => path11.dirname(m)).sort()
1389
+ };
1390
+ }
1262
1391
  }
1263
- const pnpmRaw = await readIfExists(path10.join(root, "pnpm-workspace.yaml"));
1392
+ const pnpmRaw = await readAuditInput(root, "pnpm-workspace.yaml");
1264
1393
  if (pnpmRaw !== null) {
1265
1394
  const globs = [];
1266
1395
  let inPackages = false;
@@ -1279,21 +1408,22 @@ async function countNpmWorkspaces(root) {
1279
1408
  }
1280
1409
  }
1281
1410
  if (globs.length > 0) {
1282
- const matched = await fg3(
1411
+ const matched = await auditGlob(
1412
+ root,
1283
1413
  globs.map((g) => `${g.replace(/\/+$/, "")}/package.json`),
1284
- { cwd: root, ignore: ["**/node_modules/**"] }
1414
+ { ignore: ["**/node_modules/**"], label: "pnpm workspace discovery" }
1285
1415
  );
1286
1416
  return {
1287
1417
  actual: matched.length,
1288
1418
  countedFrom: "pnpm-workspace.yaml",
1289
- members: matched.map((m) => path10.dirname(m)).sort()
1419
+ members: matched.map((m) => path11.dirname(m)).sort()
1290
1420
  };
1291
1421
  }
1292
1422
  }
1293
1423
  return null;
1294
1424
  }
1295
1425
  async function countCargoCrates(root) {
1296
- const content2 = await readIfExists(path10.join(root, "Cargo.toml"));
1426
+ const content2 = await readAuditInput(root, "Cargo.toml");
1297
1427
  if (content2 === null) return null;
1298
1428
  const membersBlock = content2.match(/members\s*=\s*\[([\s\S]*?)\]/);
1299
1429
  if (!membersBlock) return null;
@@ -1304,12 +1434,12 @@ async function countCargoCrates(root) {
1304
1434
  const members = /* @__PURE__ */ new Set();
1305
1435
  for (const entry of entries) {
1306
1436
  if (/[*?[\]{}]/.test(entry)) {
1307
- const matched = await fg3(`${entry.replace(/\/+$/, "")}/Cargo.toml`, {
1308
- cwd: root,
1309
- ignore: ["**/target/**"]
1437
+ const matched = await auditGlob(root, `${entry.replace(/\/+$/, "")}/Cargo.toml`, {
1438
+ ignore: ["**/target/**"],
1439
+ label: "Cargo workspace discovery"
1310
1440
  });
1311
- for (const m of matched) members.add(path10.dirname(m));
1312
- } else if (await readIfExists(path10.join(root, entry, "Cargo.toml")) !== null) {
1441
+ for (const m of matched) members.add(path11.dirname(m));
1442
+ } else if (await readAuditInput(root, path11.posix.join(entry, "Cargo.toml")) !== null) {
1313
1443
  members.add(entry);
1314
1444
  }
1315
1445
  }
@@ -1363,18 +1493,18 @@ var MEMBERS_CAP;
1363
1493
  var init_stale_count = __esm({
1364
1494
  "src/audit/checks/stale-count.ts"() {
1365
1495
  "use strict";
1496
+ init_inputs();
1366
1497
  init_checks();
1367
1498
  MEMBERS_CAP = 50;
1368
1499
  }
1369
1500
  });
1370
1501
 
1371
1502
  // src/audit/checks/dead-command.ts
1372
- import fs7 from "fs/promises";
1373
- import path11 from "path";
1374
- import fg4 from "fast-glob";
1375
- async function scriptsOf(absManifest) {
1503
+ async function scriptsOf(root, manifest) {
1504
+ const raw = await readAuditInput(root, manifest);
1505
+ if (raw === null) return null;
1376
1506
  try {
1377
- const pkg = JSON.parse(await fs7.readFile(absManifest, "utf-8"));
1507
+ const pkg = JSON.parse(raw);
1378
1508
  return pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? Object.keys(pkg.scripts) : [];
1379
1509
  } catch {
1380
1510
  return null;
@@ -1386,7 +1516,7 @@ async function checkDeadCommands(ctx) {
1386
1516
  (doc) => doc.claims.commands.map((claim) => ({ doc, claim }))
1387
1517
  );
1388
1518
  if (commandClaims.length === 0) return result;
1389
- const rootScripts = await scriptsOf(path11.join(ctx.root, "package.json"));
1519
+ const rootScripts = await scriptsOf(ctx.root, "package.json");
1390
1520
  if (rootScripts === null) {
1391
1521
  result.skipped.push({
1392
1522
  check: "dead-command",
@@ -1403,7 +1533,7 @@ async function checkDeadCommands(ctx) {
1403
1533
  const manifests = await commandManifests(ctx.root);
1404
1534
  manifestsChecked = ["package.json", ...manifests.sort()];
1405
1535
  for (const manifest of manifests) {
1406
- const scripts = await scriptsOf(path11.join(ctx.root, manifest));
1536
+ const scripts = await scriptsOf(ctx.root, manifest);
1407
1537
  for (const name of scripts ?? []) workspaceScripts.add(name);
1408
1538
  }
1409
1539
  return workspaceScripts;
@@ -1429,23 +1559,24 @@ async function checkDeadCommands(ctx) {
1429
1559
  return result;
1430
1560
  }
1431
1561
  function commandManifests(root) {
1432
- return fg4("**/package.json", {
1433
- cwd: root,
1434
- ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", ".mason/reports/**", "package.json"]
1562
+ return auditGlob(root, "**/package.json", {
1563
+ ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", ".mason/reports/**", "package.json"],
1564
+ label: "Command manifest discovery"
1435
1565
  });
1436
1566
  }
1437
1567
  var AVAILABLE_SCRIPTS_CAP;
1438
1568
  var init_dead_command = __esm({
1439
1569
  "src/audit/checks/dead-command.ts"() {
1440
1570
  "use strict";
1571
+ init_inputs();
1441
1572
  init_checks();
1442
1573
  AVAILABLE_SCRIPTS_CAP = 30;
1443
1574
  }
1444
1575
  });
1445
1576
 
1446
1577
  // src/audit/release-metadata.ts
1447
- import { execFile as execFile6 } from "child_process";
1448
- import { promisify as promisify6 } from "util";
1578
+ import { execFile as execFile7 } from "child_process";
1579
+ import { promisify as promisify7 } from "util";
1449
1580
  async function git(root, args) {
1450
1581
  return (await exec6("git", args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
1451
1582
  }
@@ -1497,7 +1628,7 @@ var exec6;
1497
1628
  var init_release_metadata = __esm({
1498
1629
  "src/audit/release-metadata.ts"() {
1499
1630
  "use strict";
1500
- exec6 = promisify6(execFile6);
1631
+ exec6 = promisify7(execFile7);
1501
1632
  }
1502
1633
  });
1503
1634
 
@@ -2262,9 +2393,9 @@ __export(evidence_exports, {
2262
2393
  import fs11 from "fs/promises";
2263
2394
  import path17 from "path";
2264
2395
  import { createHash as createHash3 } from "crypto";
2265
- import { execFile as execFile7 } from "child_process";
2266
- import { promisify as promisify7 } from "util";
2267
- import fg5 from "fast-glob";
2396
+ import { execFile as execFile8 } from "child_process";
2397
+ import { promisify as promisify8 } from "util";
2398
+ import fg4 from "fast-glob";
2268
2399
  import { z as z5 } from "zod";
2269
2400
  async function git2(root, ...args) {
2270
2401
  return (await exec7("git", args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout;
@@ -2280,34 +2411,13 @@ async function workspace(dir) {
2280
2411
  }
2281
2412
  return { root, gitDir, branch, directory: ".mason/reports/automation/" + hash([root, gitDir, branch]).slice(0, 24) };
2282
2413
  }
2283
- async function content(root, file) {
2284
- try {
2285
- const value = await readBoundedFile(await storePath(root, file), 10 * 1024 * 1024);
2286
- if (value === null) throw new Error("Unreadable or oversized audit input: " + file);
2287
- return value;
2288
- } catch (error) {
2289
- if (error.code === "ENOENT") return null;
2290
- throw error;
2291
- }
2292
- }
2293
2414
  async function readInputs(root) {
2294
- const [headText, docStatus, inventory, shallowPath, replacements] = await Promise.all([
2415
+ const [headText, docStatus, shallowPath, replacements] = await Promise.all([
2295
2416
  git2(root, "rev-parse", "HEAD"),
2296
2417
  git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...DOC_CANDIDATES),
2297
- fg5("**/*", {
2298
- cwd: root,
2299
- dot: true,
2300
- onlyFiles: false,
2301
- followSymbolicLinks: false,
2302
- objectMode: true,
2303
- ignore: SOURCE_IGNORE
2304
- }),
2305
2418
  git2(root, "rev-parse", "--git-path", "shallow"),
2306
2419
  git2(root, "for-each-ref", "--format=%(refname) %(objectname)", "refs/replace")
2307
2420
  ]);
2308
- const entries = inventory.filter((f) => !internal(f.path) && f.path !== ".git").sort((a, b) => a.path.localeCompare(b.path));
2309
- const files = entries.map((f) => f.path);
2310
- if (files.length > 1e5) throw new Error("Automation input inventory exceeds 100,000 paths; use an explicit scoped audit.");
2311
2421
  const head = headText.trim();
2312
2422
  let shallow = null;
2313
2423
  try {
@@ -2328,9 +2438,19 @@ async function readInputs(root) {
2328
2438
  claims.push([claim.path, await exists2(claim.path), await exists2(path17.dirname(claim.path))]);
2329
2439
  }
2330
2440
  }
2331
- if (entries.some((f) => f.dirent.isSymbolicLink())) throw new Error("Automation inventory contains a symbolic link; use an explicit audit to inspect its scope. No cached verification was recorded.");
2332
2441
  const combinedDocs = docContents.map(([, text2]) => text2 ?? "").join("\n");
2333
2442
  const countClaims = docContents.flatMap(([, text2]) => text2 ? extractClaims(text2).counts : []);
2443
+ const commandClaims = docContents.flatMap(([, text2]) => text2 ? extractClaims(text2).commands : []);
2444
+ const rootPackage = await content(root, "package.json");
2445
+ let rootScripts = null;
2446
+ if (rootPackage !== null) {
2447
+ try {
2448
+ const pkg = JSON.parse(rootPackage);
2449
+ rootScripts = pkg?.scripts && typeof pkg.scripts === "object" ? Object.keys(pkg.scripts) : [];
2450
+ } catch {
2451
+ }
2452
+ }
2453
+ const needsWorkspaceScripts = rootScripts !== null && commandClaims.some((claim) => !rootScripts.includes(claim.scriptName));
2334
2454
  const decisionDirectory = await storePath(root, ".mason/decisions");
2335
2455
  const decisionPresence = await fs11.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
2336
2456
  if (error.code === "ENOENT") return "absent";
@@ -2339,16 +2459,19 @@ async function readInputs(root) {
2339
2459
  const [modules, counts, manifests, decisionFiles] = await Promise.all([
2340
2460
  combinedDocs ? moduleCandidates(root, combinedDocs) : [],
2341
2461
  Promise.all(countClaims.map((claim) => resolveCountSource(root, claim))),
2342
- commandManifests(root),
2343
- fg5(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
2462
+ needsWorkspaceScripts ? commandManifests(root) : [],
2463
+ fg4(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
2344
2464
  ]);
2345
- const packages = await Promise.all(["package.json", ...manifests.sort()].map(async (file) => [file, await content(root, file)]));
2346
- const decisions = await Promise.all(decisionFiles.sort().map(async (file) => [file, await content(root, file)]));
2465
+ const packages = [["package.json", rootPackage], ...await Promise.all(manifests.sort().map(async (file) => [file, await content(root, file)]))];
2466
+ const decisions = await Promise.all(decisionFiles.sort().map(async (file) => {
2467
+ await storePath(root, file);
2468
+ return [file, await content(root, file)];
2469
+ }));
2347
2470
  const [status, index2] = decisions.length ? await Promise.all([
2348
2471
  git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
2349
2472
  git2(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
2350
2473
  ]) : ["", ""];
2351
- const common = [2, engineVersion, head, shallow, replacements, docContents];
2474
+ const common = [3, engineVersion, head, shallow, replacements, docContents];
2352
2475
  const keys = {
2353
2476
  "deleted-reference": hash([common, claims, docStatus]),
2354
2477
  "new-module": hash([common, modules]),
@@ -2384,7 +2507,7 @@ function checkCache(raw, inputs) {
2384
2507
  return { version: 1, entries: canonical, digest: hash(canonical) };
2385
2508
  } };
2386
2509
  }
2387
- var exec7, engineVersion, hash, internal, cacheSchema;
2510
+ var exec7, engineVersion, hash, content, internal, cacheSchema;
2388
2511
  var init_evidence = __esm({
2389
2512
  "src/automation/evidence.ts"() {
2390
2513
  "use strict";
@@ -2392,14 +2515,15 @@ var init_evidence = __esm({
2392
2515
  init_claims();
2393
2516
  init_repair();
2394
2517
  init_checks();
2395
- init_files();
2518
+ init_inputs();
2396
2519
  init_new_module();
2397
2520
  init_stale_count();
2398
2521
  init_dead_command();
2399
2522
  init_storage();
2400
- exec7 = promisify7(execFile7);
2401
- engineVersion = true ? "0.14.1" : "development";
2523
+ exec7 = promisify8(execFile8);
2524
+ engineVersion = true ? "0.15.0" : "development";
2402
2525
  hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
2526
+ content = readAuditInput;
2403
2527
  internal = (file) => file === ".mason" || file === ".mason/reports" || file.startsWith(".mason/reports/");
2404
2528
  cacheSchema = z5.object({ version: z5.literal(1), entries: z5.record(z5.object({ key: z5.string(), result: checkResultSchema })), digest: z5.string() });
2405
2529
  }
@@ -2728,7 +2852,7 @@ var init_runtime = __esm({
2728
2852
  // src/setup/model.ts
2729
2853
  import { z as z7 } from "zod";
2730
2854
  async function loadSetup(root) {
2731
- const raw = await readStoreJson(root, ".mason/setup.json");
2855
+ const raw = await readStoreJson(root, SETUP_PATH);
2732
2856
  if (raw === null) return null;
2733
2857
  return setupSchema.parse(raw);
2734
2858
  }
@@ -2739,28 +2863,23 @@ async function loadSetupReceipt(root, directory, host) {
2739
2863
  if (receipt.root !== root || receipt.host !== host) throw new Error("Setup receipt belongs to another installation.");
2740
2864
  return receipt;
2741
2865
  }
2742
- var runtimeSchema, setupHostSchema, setupSchema, receiptSchema;
2866
+ var setupHostSchema, setupSchema, SETUP_PATH, receiptSchema;
2743
2867
  var init_model = __esm({
2744
2868
  "src/setup/model.ts"() {
2745
2869
  "use strict";
2746
2870
  init_storage();
2747
- runtimeSchema = z7.object({
2748
- id: z7.string().regex(/^[a-f0-9]{24}$/),
2749
- version: z7.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
2750
- hashes: z7.record(z7.string().regex(/^[a-f0-9]{64}$/)),
2751
- bundle: z7.object({ target: z7.string(), manifestHash: z7.string().regex(/^[a-f0-9]{64}$/) }).optional()
2752
- });
2753
2871
  setupHostSchema = z7.object({
2754
- runtime: runtimeSchema,
2872
+ configuredVersion: z7.string(),
2755
2873
  revision: z7.string().uuid(),
2756
2874
  fingerprint: z7.string(),
2757
2875
  mcpFingerprint: z7.string(),
2758
2876
  instructions: z7.array(z7.string())
2759
2877
  });
2760
- setupSchema = z7.object({ version: z7.literal(1), hosts: z7.object({
2878
+ setupSchema = z7.object({ version: z7.literal(2), hosts: z7.object({
2761
2879
  codex: setupHostSchema.optional(),
2762
2880
  claude: setupHostSchema.optional()
2763
2881
  }) });
2882
+ SETUP_PATH = ".mason/local/setup.json";
2764
2883
  receiptSchema = z7.object({
2765
2884
  version: z7.literal(1),
2766
2885
  host: z7.enum(["codex", "claude"]),
@@ -2774,6 +2893,39 @@ var init_model = __esm({
2774
2893
  }
2775
2894
  });
2776
2895
 
2896
+ // src/setup/launcher.ts
2897
+ import { execFile as execFile9 } from "child_process";
2898
+ import { promisify as promisify9 } from "util";
2899
+ function globalCommand(args, platform = process.platform) {
2900
+ return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args] } : { command: "mason", args };
2901
+ }
2902
+ async function installedCommand() {
2903
+ const invocation = globalCommand(["internal-integration-version"]);
2904
+ try {
2905
+ const { stdout } = await promisify9(execFile9)(
2906
+ invocation.command,
2907
+ invocation.args,
2908
+ { timeout: 5e3, maxBuffer: 8192, windowsHide: true }
2909
+ );
2910
+ const result = JSON.parse(stdout);
2911
+ if (result.protocol !== 1 || typeof result.version !== "string") throw new Error("Unsupported Mason integration protocol.");
2912
+ return { available: true, version: result.version, message: null };
2913
+ } catch {
2914
+ return { available: false, version: null, message: "An up-to-date mason command is not available on this process's PATH. Install or upgrade Mason, then reopen your terminal or desktop assistant and rerun mason setup." };
2915
+ }
2916
+ }
2917
+ var executingVersion, mcpCommand, hookCommand;
2918
+ var init_launcher = __esm({
2919
+ "src/setup/launcher.ts"() {
2920
+ "use strict";
2921
+ init_evidence();
2922
+ init_model();
2923
+ executingVersion = () => true ? "0.15.0" : "development";
2924
+ mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
2925
+ hookCommand = (host) => `mason --setup-host ${host} auto`;
2926
+ }
2927
+ });
2928
+
2777
2929
  // src/setup/observations.ts
2778
2930
  var observations_exports = {};
2779
2931
  __export(observations_exports, {
@@ -2783,14 +2935,14 @@ __export(observations_exports, {
2783
2935
  });
2784
2936
  import fs13 from "fs/promises";
2785
2937
  import { z as z8 } from "zod";
2786
- function observationPath(directory, host, revision) {
2787
- return `${directory}/activation/${host}-${revision}.json`;
2938
+ function observationPath(directory, host, revision, version = executingVersion()) {
2939
+ return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
2788
2940
  }
2789
- async function readObservation(root, directory, host, revision) {
2790
- const raw = await readStoreJson(root, observationPath(directory, host, revision));
2941
+ async function readObservation(root, directory, host, revision, version = executingVersion()) {
2942
+ const raw = await readStoreJson(root, observationPath(directory, host, revision, version));
2791
2943
  if (raw === null) return null;
2792
2944
  const record = observationSchema.parse(raw);
2793
- if (record.root !== root || record.host !== host || record.revision !== revision) throw new Error("Activation receipt belongs to another installation.");
2945
+ if (record.root !== root || record.host !== host || record.revision !== revision || record.masonVersion !== version) throw new Error("Activation receipt belongs to another installation.");
2794
2946
  return record;
2795
2947
  }
2796
2948
  async function observeActivation(dir, event, options = {}) {
@@ -2807,6 +2959,7 @@ async function observeActivation(dir, event, options = {}) {
2807
2959
  const now = (/* @__PURE__ */ new Date()).toISOString();
2808
2960
  const record = await readObservation(ws.root, ws.directory, host, revision) ?? {
2809
2961
  version: 1,
2962
+ masonVersion: executingVersion(),
2810
2963
  root: ws.root,
2811
2964
  revision,
2812
2965
  host,
@@ -2845,8 +2998,10 @@ var init_observations = __esm({
2845
2998
  init_store();
2846
2999
  init_storage();
2847
3000
  init_model();
3001
+ init_launcher();
2848
3002
  observationSchema = z8.object({
2849
3003
  version: z8.literal(1),
3004
+ masonVersion: z8.string(),
2850
3005
  root: z8.string(),
2851
3006
  revision: z8.string(),
2852
3007
  host: z8.enum(["codex", "claude"]),
@@ -2936,106 +3091,78 @@ var init_adapters = __esm({
2936
3091
  }
2937
3092
  });
2938
3093
 
2939
- // src/automation/install.ts
2940
- var install_exports = {};
2941
- __export(install_exports, {
2942
- automationConfigSchema: () => automationConfigSchema,
2943
- configPath: () => configPath,
2944
- installAutomation: () => installAutomation,
2945
- installedAutomation: () => installedAutomation,
2946
- planAutomationInstall: () => planAutomationInstall
3094
+ // src/setup/files.ts
3095
+ var files_exports = {};
3096
+ __export(files_exports, {
3097
+ applyEdit: () => applyEdit,
3098
+ managedBlock: () => managedBlock,
3099
+ readText: () => readText
2947
3100
  });
2948
- import { z as z10 } from "zod";
2949
- async function installAutomation(dir, host, command) {
2950
- const ws = await workspace(dir);
2951
- return withLock(ws.root, ".mason/reports/automation-install", () => installLocked(ws.root, host, command));
2952
- }
2953
- async function planAutomationInstall(root, host, command) {
2954
- const file = configPath(host);
2955
- const existing = automationConfigSchema.parse(await readStoreJson(root, file) ?? {});
2956
- const record = recordSchema.parse(await readStoreJson(root, ".mason/automation.json") ?? { version: 1, hosts: {} });
2957
- const desired = hookConfig(host, command);
2958
- const newCommand = desired.hooks.SessionStart[0].hooks[0].command;
2959
- const previous = record.hosts[host]?.command;
2960
- const hooks = existing.hooks ?? {};
2961
- for (const event of HOOK_EVENTS) {
2962
- hooks[event] = (hooks[event] ?? []).map((group) => ({
2963
- ...group,
2964
- hooks: group.hooks.filter((handler) => !(handler.type === "command" && typeof handler.command === "string" && (handler.command === previous || handler.command === newCommand)))
2965
- })).filter((group) => group.hooks.length);
2966
- hooks[event].push(...desired.hooks[event]);
3101
+ import fs14 from "fs/promises";
3102
+ import path18 from "path";
3103
+ import { randomUUID as randomUUID5 } from "crypto";
3104
+ async function readText(root, file) {
3105
+ try {
3106
+ const text2 = await readBoundedFile(await storePath(root, file), 2 * 1024 * 1024);
3107
+ if (text2 === null) throw new Error("Setup input is not a regular file or exceeds 2 MiB: " + file);
3108
+ return text2;
3109
+ } catch (error) {
3110
+ if (error.code === "ENOENT") return null;
3111
+ throw error;
2967
3112
  }
2968
- record.hosts[host] = { command: newCommand };
2969
- return { file, config: { ...existing, hooks }, record, newCommand };
2970
3113
  }
2971
- async function installLocked(root, host, command) {
2972
- const { file, config, record, newCommand } = await planAutomationInstall(root, host, command);
2973
- await writeStoreJson(root, file, config);
2974
- await writeStoreJson(root, ".mason/automation.json", record);
2975
- return {
2976
- version: 1,
2977
- host,
2978
- configPath: file,
2979
- status: "configured",
2980
- command: newCommand,
2981
- events: HOOK_EVENTS,
2982
- next: host === "codex" ? "Review/trust these hooks using Codex /hooks and start a new session. mason-auto status reports observed events separately from configuration." : "Start a new Claude Code session. mason-auto status reports observed events separately from configuration.",
2983
- note: "Install mason-context in the project before using the default command. Ignore .mason/reports/ to keep local evidence out of commits. Hooks preserve evidence and suggest scoped repairs; they do not approve edits or decisions."
2984
- };
3114
+ function managedBlock(text2, start, end, body) {
3115
+ const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
3116
+ if (starts !== ends || starts > 1 || starts === 1 && text2.indexOf(end) < text2.indexOf(start)) {
3117
+ throw new Error("Ambiguous Mason instruction/configuration markers; repair the marked block before setup.");
3118
+ }
3119
+ const eol = text2.includes("\r\n") ? "\r\n" : "\n";
3120
+ const block = [start, body.replace(/\r?\n/g, eol), end].join(eol);
3121
+ if (starts) return text2.slice(0, text2.indexOf(start)) + block + text2.slice(text2.indexOf(end) + end.length);
3122
+ return text2 + (text2.length && !text2.endsWith("\n") ? eol : "") + (text2.length ? eol : "") + block + eol;
2985
3123
  }
2986
- async function installedAutomation(dir) {
2987
- const ws = await workspace(dir);
2988
- const raw = await readStoreJson(ws.root, ".mason/automation.json");
2989
- if (raw === null) return {};
2990
- const record = recordSchema.parse(raw);
2991
- const result = {};
2992
- for (const host of ["claude", "codex"]) {
2993
- const expected = record.hosts[host];
2994
- if (!expected) continue;
2995
- const current = automationConfigSchema.parse(await readStoreJson(ws.root, configPath(host)) ?? {});
2996
- const configuredEvents = HOOK_EVENTS.filter((event) => current.hooks?.[event]?.some((group) => group.hooks.some((handler) => handler.type === "command" && handler.command === expected.command)));
2997
- result[host] = {
2998
- configPath: configPath(host),
2999
- configuredEvents,
3000
- status: current.disableAllHooks === true ? "disabled" : configuredEvents.length === HOOK_EVENTS.length ? "configured" : "incomplete",
3001
- runtime: "Host version, trust, policy, and tool coverage still determine execution; inspect observed events."
3002
- };
3124
+ async function applyEdit(root, edit) {
3125
+ const current = await readText(root, edit.path);
3126
+ if (current === edit.after) return false;
3127
+ if (current !== edit.before) throw new Error("Setup input changed during installation: " + edit.path + ". Rerun setup to resume.");
3128
+ const file = await storePath(root, edit.path, true);
3129
+ const temporary = path18.join(path18.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
3130
+ try {
3131
+ const mode = await fs14.stat(file).then((s) => s.mode & 511, () => 384);
3132
+ const handle = await fs14.open(temporary, "wx", mode);
3133
+ try {
3134
+ await handle.writeFile(edit.after, "utf8");
3135
+ await handle.sync();
3136
+ } finally {
3137
+ await handle.close();
3138
+ }
3139
+ if (await readText(root, edit.path) !== edit.before) throw new Error("Setup input changed during installation: " + edit.path);
3140
+ await fs14.rename(temporary, file);
3141
+ return true;
3142
+ } finally {
3143
+ await fs14.rm(temporary, { force: true });
3003
3144
  }
3004
- return result;
3005
3145
  }
3006
- var groupSchema, automationConfigSchema, recordSchema, configPath;
3007
- var init_install = __esm({
3008
- "src/automation/install.ts"() {
3146
+ var init_files2 = __esm({
3147
+ "src/setup/files.ts"() {
3009
3148
  "use strict";
3149
+ init_files();
3010
3150
  init_storage();
3011
- init_evidence();
3012
- init_adapters();
3013
- init_store();
3014
- groupSchema = z10.object({ hooks: z10.array(z10.object({ type: z10.string(), command: z10.string().optional() }).passthrough()) }).passthrough();
3015
- automationConfigSchema = z10.object({ hooks: z10.record(z10.array(groupSchema)).optional() }).passthrough();
3016
- recordSchema = z10.object({ version: z10.literal(1), hosts: z10.record(z10.object({ command: z10.string() })) });
3017
- configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
3018
3151
  }
3019
3152
  });
3020
3153
 
3021
3154
  // src/mcp/init.ts
3022
- import { z as z11 } from "zod";
3023
- async function loadProjectMarker(rootDir) {
3024
- const raw = await readStoreJson(rootDir, ".mason/project.json");
3025
- return raw === null ? null : markerSchema.parse(raw);
3026
- }
3027
- async function saveProjectMarker(rootDir, marker) {
3028
- await writeStoreJson(rootDir, ".mason/project.json", markerSchema.parse(marker));
3029
- }
3155
+ import { z as z10 } from "zod";
3030
3156
  var markerSchema, CLAUDE_MD_SECTION, ASSISTANT_SETUP, QUICKSTART_PLAYBOOK, MAP_PLAYBOOK;
3031
3157
  var init_init = __esm({
3032
3158
  "src/mcp/init.ts"() {
3033
3159
  "use strict";
3034
3160
  init_storage();
3035
- markerSchema = z11.object({
3036
- version: z11.literal(1),
3037
- initializedAt: z11.string(),
3038
- features: z11.object({ confluence: z11.boolean().optional() }).optional()
3161
+ init_files2();
3162
+ markerSchema = z10.object({
3163
+ version: z10.literal(1),
3164
+ initializedAt: z10.string(),
3165
+ features: z10.object({ confluence: z10.boolean().optional() }).passthrough().optional()
3039
3166
  }).passthrough();
3040
3167
  CLAUDE_MD_SECTION = `<!-- mason:start -->
3041
3168
  ## Mason project knowledge
@@ -3057,7 +3184,7 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
3057
3184
  Inspect source for what the retrieved context does not answer.
3058
3185
  <!-- mason:end -->`;
3059
3186
  ASSISTANT_SETUP = `UNIFIED SETUP
3060
- When the user requests Mason setup, call mason_init again with mode: "setup" and host: "codex" or "claude" for the assistant being configured. This uses the same engine as mason-auto setup: retain the initial audit before instruction edits, install a pinned private runtime, merge project MCP configuration and hooks, update marker-delimited instructions and Git ignore rules, and record setup. A concept map is optional.
3187
+ When the user requests Mason setup, call mason_init again with mode: "setup" and host: "codex" or "claude" for the assistant being configured. This uses the same engine as mason-auto setup: retain the initial audit before instruction edits, use the installed mason command on PATH, merge project MCP configuration and hooks, update marker-delimited instructions and Git ignore rules, and record setup. A concept map is optional.
3061
3188
  If the user only requested inspection or review, report findings without running setup. The setup result distinguishes configured from observed activation. Explain the host's native trust step and request a new session; never trust hooks on the user's behalf or claim activation from generated configuration alone. After a normal task finishes, mason_automation(action: "status") or mason-auto status reports observed context use and hook events. Existing findings remain reviewable; setup does not approve advisories or manufacture decisions.
3062
3189
 
3063
3190
  The managed project guidance is:
@@ -3065,7 +3192,7 @@ ${CLAUDE_MD_SECTION}`;
3065
3192
  QUICKSTART_PLAYBOOK = `Start with the audit and review results included in this response. No concept map is required.
3066
3193
 
3067
3194
  1. Explain the actionable findings with their source evidence. Separate audit issues, advisories, and skipped checks. The review covers committed changes from the merge base to HEAD; workingTree paths are not included in that review. An unavailable or empty check is not proof that the project is correct. Use the CLI for full output if a summary is truncated.
3068
- 2. Address findings within the user's requested scope. Setup alone authorizes the Mason runtime, host configuration, hooks, instructions, and ignore rules; rewriting existing project claims requires repair scope. If repair is authorized, call \`mason_repair(dir, action: "prepare")\` before editing; it saves the full original findings even when this summary is truncated. Inspect relevant source, make grounded edits, then call \`mason_repair(dir, action: "verify", baselinePath)\` with that same baseline, including after any final doc commit. Report resolved, unresolved, review-required, unverified, and new findings. Keep suppressed advisories visible even when setup has already dirtied a doc. Do not invent a decision just to populate the store.
3195
+ 2. Address findings within the user's requested scope. Setup alone authorizes Mason host configuration, hooks, instructions, and ignore rules; rewriting existing project claims requires repair scope. If repair is authorized, call \`mason_repair(dir, action: "prepare")\` before editing; it saves the full original findings even when this summary is truncated. Inspect relevant source, make grounded edits, then call \`mason_repair(dir, action: "verify", baselinePath)\` with that same baseline, including after any final doc commit. Report resolved, unresolved, review-required, unverified, and new findings. Keep suppressed advisories visible even when setup has already dirtied a doc. Do not invent a decision just to populate the store.
3069
3196
  3. When the task reveals a real lesson or constraint, call \`save_decision\` with title, body, category, anchors, and known owner/source/actor information. Missing attribution can be added later. The tool writes a local proposal; editing it preserves revision history and requires a new acceptance, while any earlier accepted revision remains operative. An unchanged save never refreshes its evidence. When decision review is requested, \`review_decision\` prepares the record and code evidence before any authorized verdict. Review and commit records through the normal workflow. Retrieve it on the next relevant task with \`get_context(dir, task, files)\`.
3070
3197
 
3071
3198
  ${ASSISTANT_SETUP}
@@ -3107,37 +3234,286 @@ If a build is interrupted, its partials remain in \`.mason/partial-snapshots/\`.
3107
3234
  }
3108
3235
  });
3109
3236
 
3110
- // src/review/cochange.ts
3111
- import { execFile as execFile8 } from "child_process";
3112
- import { promisify as promisify8 } from "util";
3113
- async function buildMatrix(resolvedRoot) {
3237
+ // src/utils/git.ts
3238
+ var git_exports = {};
3239
+ __export(git_exports, {
3240
+ isGitRepo: () => isGitRepo
3241
+ });
3242
+ import { execFile as execFile10 } from "child_process";
3243
+ import { promisify as promisify10 } from "util";
3244
+ async function isGitRepo(dir) {
3114
3245
  try {
3115
- const { stdout: shallow } = await exec8("git", ["rev-parse", "--is-shallow-repository"], { cwd: resolvedRoot });
3116
- if (shallow.trim() === "true") return null;
3117
- const { stdout } = await exec8(
3118
- "git",
3119
- ["log", `-n${HISTORY_COMMITS}`, "--format=%x01", "--name-only", "-M"],
3120
- { cwd: resolvedRoot, maxBuffer: 100 * 1024 * 1024 }
3121
- );
3122
- const commitsByFile = /* @__PURE__ */ new Map();
3123
- const blocks = stdout.split("");
3124
- let index2 = 0;
3125
- for (const block of blocks) {
3126
- const files = block.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith(".mason/"));
3127
- if (files.length === 0 || files.length > MAX_COMMIT_FILES) continue;
3128
- for (const file of files) {
3129
- let set = commitsByFile.get(file);
3130
- if (!set) {
3131
- set = /* @__PURE__ */ new Set();
3132
- commitsByFile.set(file, set);
3133
- }
3134
- set.add(index2);
3135
- }
3136
- index2++;
3137
- }
3138
- return { commitsByFile, totalCommits: index2 };
3246
+ await exec8("git", ["rev-parse", "--git-dir"], { cwd: dir });
3247
+ return true;
3139
3248
  } catch {
3140
- return null;
3249
+ return false;
3250
+ }
3251
+ }
3252
+ var exec8;
3253
+ var init_git2 = __esm({
3254
+ "src/utils/git.ts"() {
3255
+ "use strict";
3256
+ exec8 = promisify10(execFile10);
3257
+ }
3258
+ });
3259
+
3260
+ // src/setup/config.ts
3261
+ var config_exports = {};
3262
+ __export(config_exports, {
3263
+ ancillaryEdits: () => ancillaryEdits,
3264
+ hookEdits: () => hookEdits,
3265
+ inspectHostConfig: () => inspectHostConfig,
3266
+ instructionEdits: () => instructionEdits,
3267
+ mcpEdit: () => mcpEdit,
3268
+ mcpPath: () => mcpPath
3269
+ });
3270
+ import { isDeepStrictEqual } from "util";
3271
+ import { parse, stringify } from "smol-toml";
3272
+ import { z as z11 } from "zod";
3273
+ async function instructionEdits(root, host) {
3274
+ const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
3275
+ const existing = found.find((f) => f.text !== null);
3276
+ const primary = host === "codex" && found[0].text === null ? { path: "AGENTS.md", text: null } : existing ?? { path: "AGENTS.md", text: null };
3277
+ const originalGuidance = primary.text === null && existing ? "Follow the existing project conventions in " + existing.path + ".\n" : "";
3278
+ const edits = [{
3279
+ path: primary.path,
3280
+ before: primary.text,
3281
+ after: managedBlock(primary.text ?? originalGuidance, "<!-- mason:start -->", "<!-- mason:end -->", CLAUDE_MD_SECTION.split("\n").slice(1, -1).join("\n"))
3282
+ }];
3283
+ if (primary.path === "AGENTS.md") {
3284
+ const secondary = found.filter((f) => f.path !== "AGENTS.md" && f.text !== null);
3285
+ if (host === "claude" && !secondary.length) secondary.push({ path: "CLAUDE.md", text: null });
3286
+ for (const file of secondary) {
3287
+ const text2 = file.text ?? "";
3288
+ const legacy = text2.includes("<!-- mason:start -->") || text2.includes("<!-- mason:end -->");
3289
+ const imported = "@" + (file.path.startsWith(".claude/") ? "../AGENTS.md" : "AGENTS.md");
3290
+ if (text2.trim() === imported) continue;
3291
+ const pointer = "Mason project knowledge and shared project instructions:\n" + imported;
3292
+ edits.push({
3293
+ path: file.path,
3294
+ before: file.text,
3295
+ after: managedBlock(text2, legacy ? "<!-- mason:start -->" : POINTER_START, legacy ? "<!-- mason:end -->" : POINTER_END, pointer)
3296
+ });
3297
+ }
3298
+ }
3299
+ return edits;
3300
+ }
3301
+ function managedMcp(previous, host) {
3302
+ const options = previous === void 0 ? {} : object.parse(previous);
3303
+ for (const key of ["command", "args", "url", "type", "headers", "http_headers", "env_http_headers", "bearer_token_env_var"]) delete options[key];
3304
+ return { ...options, ...mcpCommand(host) };
3305
+ }
3306
+ async function mcpEdit(root, host) {
3307
+ const file = mcpPath(host), before = await readText(root, file);
3308
+ if (host === "claude") {
3309
+ const config2 = before === null ? {} : object.parse(JSON.parse(before));
3310
+ const servers2 = object.parse(config2.mcpServers ?? {});
3311
+ return { path: file, before, after: JSON.stringify({ ...config2, mcpServers: { ...servers2, mason: managedMcp(servers2.mason, host) } }, null, 2) + "\n" };
3312
+ }
3313
+ const config = parse(before ?? "");
3314
+ const managed = (before ?? "").includes(TOML_START);
3315
+ const servers = config.mcp_servers;
3316
+ const desired = managedMcp(servers?.mason, host);
3317
+ let base = before ?? "";
3318
+ if (servers?.mason && !managed) {
3319
+ const headers = [...base.matchAll(/^\s*\[\[?.+?\]\]?[^\S\r\n]*(?:#.*)?$/gm)];
3320
+ const spans = headers.flatMap((header, index2) => /^\s*\[mcp_servers\.mason(?:\.[A-Za-z0-9_-]+)*\]/.test(header[0]) ? [{ start: header.index, end: headers[index2 + 1]?.index ?? base.length }] : []);
3321
+ if (!spans.length) throw new Error("Cannot safely migrate the existing Mason MCP TOML entry. Use an explicit [mcp_servers.mason] table and rerun setup; the file was retained.");
3322
+ for (const span of spans.reverse()) base = base.slice(0, span.start) + base.slice(span.end);
3323
+ }
3324
+ const after = managedBlock(base, TOML_START, TOML_END, stringify({ mcp_servers: { mason: desired } }).trimEnd());
3325
+ const parsed = parse(after);
3326
+ const expected = { ...config, mcp_servers: { ...servers ?? {}, mason: desired } };
3327
+ if (!isDeepStrictEqual(parsed, expected)) throw new Error("Could not safely configure the Mason MCP entry without changing other settings.");
3328
+ return { path: file, before, after };
3329
+ }
3330
+ async function hookEdits(root, host) {
3331
+ const file = configPath(host), before = await readText(root, file);
3332
+ const { planAutomationInstall: planAutomationInstall2 } = await Promise.resolve().then(() => (init_install(), install_exports));
3333
+ const plan = await planAutomationInstall2(root, host, hookCommand(host));
3334
+ return [
3335
+ { path: file, before, after: JSON.stringify(plan.config, null, 2) + "\n" },
3336
+ { path: AUTOMATION_PATH, before: await readText(root, AUTOMATION_PATH), after: JSON.stringify(plan.record, null, 2) + "\n" }
3337
+ ];
3338
+ }
3339
+ async function ancillaryEdits(root) {
3340
+ const ignore = await readText(root, ".gitignore");
3341
+ const { git: git3 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
3342
+ const { isGitRepo: isGitRepo2 } = await Promise.resolve().then(() => (init_git2(), git_exports));
3343
+ let parentIgnored = false;
3344
+ try {
3345
+ if (await isGitRepo2(root)) parentIgnored = !!(await git3(root, "check-ignore", "--no-index", ".mason")).trim();
3346
+ } catch (error) {
3347
+ if (error.code !== 1) throw error;
3348
+ }
3349
+ const retainParentRule = parentIgnored || !!ignore?.replace(/\r\n/g, "\n").includes("# mason:ignore:start\n!/.mason/\n/.mason/*");
3350
+ const rules = [
3351
+ ...retainParentRule ? ["!/.mason/", "/.mason/*"] : [],
3352
+ "!/.mason/decisions/",
3353
+ "!/.mason/decisions/**",
3354
+ "!/.mason/config.json",
3355
+ "!/.mason/snapshot.json",
3356
+ "/.mason/local/",
3357
+ "/.mason/reports/"
3358
+ ].join("\n");
3359
+ return [{ path: ".gitignore", before: ignore, after: managedBlock(ignore ?? "", "# mason:ignore:start", "# mason:ignore:end", rules) }];
3360
+ }
3361
+ async function inspectHostConfig(root, host, plannedMcp) {
3362
+ const text2 = plannedMcp ?? await readText(root, mcpPath(host));
3363
+ const config = host === "codex" ? parse(text2 ?? "") : object.parse(JSON.parse(text2 ?? "{}"));
3364
+ const servers = host === "codex" ? config.mcp_servers : config.mcpServers;
3365
+ const hooks = automationConfigSchema.parse(JSON.parse(await readText(root, configPath(host)) ?? "{}"));
3366
+ const expected = hookConfig(host, hookCommand(host));
3367
+ const mcp = servers?.mason === void 0 ? null : object.parse(servers.mason);
3368
+ return {
3369
+ mcp,
3370
+ mcpDisabled: mcp?.enabled === false,
3371
+ hooks: Object.fromEntries(Object.keys(expected.hooks).map((event) => [
3372
+ event,
3373
+ (hooks.hooks?.[event] ?? []).flatMap((group) => group.hooks.filter((handler) => handler.command === expected.hooks.SessionStart[0].hooks[0].command).map((handler) => ({ ...group, hooks: [handler] })) ?? [])
3374
+ ])),
3375
+ disabled: hooks.disableAllHooks === true || config.features?.hooks === false || config.features?.codex_hooks === false
3376
+ };
3377
+ }
3378
+ var object, mcpPath, TOML_START, TOML_END, POINTER_START, POINTER_END;
3379
+ var init_config = __esm({
3380
+ "src/setup/config.ts"() {
3381
+ "use strict";
3382
+ init_init();
3383
+ init_docs();
3384
+ init_adapters();
3385
+ init_install();
3386
+ init_files2();
3387
+ init_launcher();
3388
+ object = z11.record(z11.unknown());
3389
+ mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
3390
+ TOML_START = "# mason:mcp:start";
3391
+ TOML_END = "# mason:mcp:end";
3392
+ POINTER_START = "<!-- mason:agents:start -->";
3393
+ POINTER_END = "<!-- mason:agents:end -->";
3394
+ }
3395
+ });
3396
+
3397
+ // src/automation/install.ts
3398
+ var install_exports = {};
3399
+ __export(install_exports, {
3400
+ AUTOMATION_PATH: () => AUTOMATION_PATH,
3401
+ automationConfigSchema: () => automationConfigSchema,
3402
+ configPath: () => configPath,
3403
+ installAutomation: () => installAutomation,
3404
+ installedAutomation: () => installedAutomation,
3405
+ planAutomationInstall: () => planAutomationInstall
3406
+ });
3407
+ import { z as z12 } from "zod";
3408
+ async function installationRecord(root) {
3409
+ return recordSchema.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
3410
+ }
3411
+ async function installAutomation(dir, host, command) {
3412
+ const ws = await workspace(dir);
3413
+ return withLock(ws.root, ".mason/reports/automation-install", () => installLocked(ws.root, host, command));
3414
+ }
3415
+ async function planAutomationInstall(root, host, command) {
3416
+ const file = configPath(host);
3417
+ const existing = automationConfigSchema.parse(await readStoreJson(root, file) ?? {});
3418
+ const record = await installationRecord(root);
3419
+ const desired = hookConfig(host, command);
3420
+ const newCommand = desired.hooks.SessionStart[0].hooks[0].command;
3421
+ const previous = record.hosts[host]?.command;
3422
+ const owned = /* @__PURE__ */ new Set([previous, newCommand]);
3423
+ const hooks = existing.hooks ?? {};
3424
+ for (const event of HOOK_EVENTS) {
3425
+ hooks[event] = (hooks[event] ?? []).map((group) => ({
3426
+ ...group,
3427
+ hooks: group.hooks.filter((handler) => !(handler.type === "command" && typeof handler.command === "string" && owned.has(handler.command)))
3428
+ })).filter((group) => group.hooks.length);
3429
+ hooks[event].push(...desired.hooks[event]);
3430
+ }
3431
+ record.hosts[host] = { command: newCommand };
3432
+ return { file, config: { ...existing, hooks }, record, newCommand };
3433
+ }
3434
+ async function installLocked(root, host, command) {
3435
+ const { file, config, record, newCommand } = await planAutomationInstall(root, host, command);
3436
+ const { ancillaryEdits: ancillaryEdits2 } = await Promise.resolve().then(() => (init_config(), config_exports));
3437
+ const { applyEdit: applyEdit2 } = await Promise.resolve().then(() => (init_files2(), files_exports));
3438
+ for (const edit of await ancillaryEdits2(root)) await applyEdit2(root, edit);
3439
+ await writeStoreJson(root, file, config);
3440
+ await writeStoreJson(root, AUTOMATION_PATH, record);
3441
+ return {
3442
+ version: 1,
3443
+ host,
3444
+ configPath: file,
3445
+ status: "configured",
3446
+ command: newCommand,
3447
+ events: HOOK_EVENTS,
3448
+ next: host === "codex" ? "Review/trust these hooks using Codex /hooks and start a new session. mason-auto status reports observed events separately from configuration." : "Start a new Claude Code session. mason-auto status reports observed events separately from configuration.",
3449
+ note: "Install mason-context in the project before using the default command. Ignore .mason/reports/ to keep local evidence out of commits. Hooks preserve evidence and suggest scoped repairs; they do not approve edits or decisions."
3450
+ };
3451
+ }
3452
+ async function installedAutomation(dir) {
3453
+ const ws = await workspace(dir);
3454
+ const record = await installationRecord(ws.root);
3455
+ const result = {};
3456
+ for (const host of ["claude", "codex"]) {
3457
+ const expected = record.hosts[host];
3458
+ if (!expected) continue;
3459
+ const current = automationConfigSchema.parse(await readStoreJson(ws.root, configPath(host)) ?? {});
3460
+ const configuredEvents = HOOK_EVENTS.filter((event) => current.hooks?.[event]?.some((group) => group.hooks.some((handler) => handler.type === "command" && handler.command === expected.command)));
3461
+ result[host] = {
3462
+ configPath: configPath(host),
3463
+ configuredEvents,
3464
+ status: current.disableAllHooks === true ? "disabled" : configuredEvents.length === HOOK_EVENTS.length ? "configured" : "incomplete",
3465
+ runtime: "Host version, trust, policy, and tool coverage still determine execution; inspect observed events."
3466
+ };
3467
+ }
3468
+ return result;
3469
+ }
3470
+ var groupSchema, automationConfigSchema, recordSchema, AUTOMATION_PATH, configPath;
3471
+ var init_install = __esm({
3472
+ "src/automation/install.ts"() {
3473
+ "use strict";
3474
+ init_storage();
3475
+ init_evidence();
3476
+ init_adapters();
3477
+ init_store();
3478
+ groupSchema = z12.object({ hooks: z12.array(z12.object({ type: z12.string(), command: z12.string().optional() }).passthrough()) }).passthrough();
3479
+ automationConfigSchema = z12.object({ hooks: z12.record(z12.array(groupSchema)).optional() }).passthrough();
3480
+ recordSchema = z12.object({ version: z12.literal(1), hosts: z12.record(z12.object({ command: z12.string() })) });
3481
+ AUTOMATION_PATH = ".mason/local/automation.json";
3482
+ configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
3483
+ }
3484
+ });
3485
+
3486
+ // src/review/cochange.ts
3487
+ import { execFile as execFile11 } from "child_process";
3488
+ import { promisify as promisify11 } from "util";
3489
+ async function buildMatrix(resolvedRoot) {
3490
+ try {
3491
+ const { stdout: shallow } = await exec9("git", ["rev-parse", "--is-shallow-repository"], { cwd: resolvedRoot });
3492
+ if (shallow.trim() === "true") return null;
3493
+ const { stdout } = await exec9(
3494
+ "git",
3495
+ ["log", `-n${HISTORY_COMMITS}`, "--format=%x01", "--name-only", "-M"],
3496
+ { cwd: resolvedRoot, maxBuffer: 100 * 1024 * 1024 }
3497
+ );
3498
+ const commitsByFile = /* @__PURE__ */ new Map();
3499
+ const blocks = stdout.split("");
3500
+ let index2 = 0;
3501
+ for (const block of blocks) {
3502
+ const files = block.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith(".mason/"));
3503
+ if (files.length === 0 || files.length > MAX_COMMIT_FILES) continue;
3504
+ for (const file of files) {
3505
+ let set = commitsByFile.get(file);
3506
+ if (!set) {
3507
+ set = /* @__PURE__ */ new Set();
3508
+ commitsByFile.set(file, set);
3509
+ }
3510
+ set.add(index2);
3511
+ }
3512
+ index2++;
3513
+ }
3514
+ return { commitsByFile, totalCommits: index2 };
3515
+ } catch {
3516
+ return null;
3141
3517
  }
3142
3518
  }
3143
3519
  async function findMissingPartners(resolvedRoot, changedFiles, existsOnDisk, allChangedFiles = changedFiles) {
@@ -3170,11 +3546,11 @@ async function findMissingPartners(resolvedRoot, changedFiles, existsOnDisk, all
3170
3546
  findings.sort((a, b) => b.rate - a.rate || b.sharedCommits - a.sharedCommits);
3171
3547
  return findings;
3172
3548
  }
3173
- var exec8, HISTORY_COMMITS, MIN_FILE_COMMITS, MIN_SHARED_COMMITS, MIN_COCHANGE_RATE, MAX_COMMIT_FILES;
3549
+ var exec9, HISTORY_COMMITS, MIN_FILE_COMMITS, MIN_SHARED_COMMITS, MIN_COCHANGE_RATE, MAX_COMMIT_FILES;
3174
3550
  var init_cochange = __esm({
3175
3551
  "src/review/cochange.ts"() {
3176
3552
  "use strict";
3177
- exec8 = promisify8(execFile8);
3553
+ exec9 = promisify11(execFile11);
3178
3554
  HISTORY_COMMITS = 1500;
3179
3555
  MIN_FILE_COMMITS = 5;
3180
3556
  MIN_SHARED_COMMITS = 4;
@@ -3229,7 +3605,7 @@ var init_types2 = __esm({
3229
3605
  });
3230
3606
 
3231
3607
  // src/review/evidence/vitest.ts
3232
- import { z as z12 } from "zod";
3608
+ import { z as z13 } from "zod";
3233
3609
  function parseVitest(raw, sourceRoot) {
3234
3610
  const report = schema.parse(raw);
3235
3611
  const findings = [], diagnostics = [];
@@ -3278,24 +3654,24 @@ var init_vitest = __esm({
3278
3654
  "use strict";
3279
3655
  init_paths2();
3280
3656
  init_types2();
3281
- count2 = z12.number().int().nonnegative();
3282
- schema = z12.object({
3283
- success: z12.boolean(),
3657
+ count2 = z13.number().int().nonnegative();
3658
+ schema = z13.object({
3659
+ success: z13.boolean(),
3284
3660
  numTotalTests: count2,
3285
3661
  numPassedTests: count2,
3286
3662
  numFailedTests: count2,
3287
3663
  numPendingTests: count2,
3288
3664
  numTodoTests: count2,
3289
3665
  numFailedTestSuites: count2,
3290
- testResults: z12.array(z12.object({
3291
- name: z12.string().min(1),
3292
- status: z12.enum(["passed", "failed"]),
3293
- message: z12.string().optional(),
3294
- assertionResults: z12.array(z12.object({
3295
- fullName: z12.string(),
3296
- status: z12.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
3297
- failureMessages: z12.array(z12.string()).nullable().optional(),
3298
- location: z12.object({ line: count2, column: count2 }).nullable().optional()
3666
+ testResults: z13.array(z13.object({
3667
+ name: z13.string().min(1),
3668
+ status: z13.enum(["passed", "failed"]),
3669
+ message: z13.string().optional(),
3670
+ assertionResults: z13.array(z13.object({
3671
+ fullName: z13.string(),
3672
+ status: z13.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
3673
+ failureMessages: z13.array(z13.string()).nullable().optional(),
3674
+ location: z13.object({ line: count2, column: count2 }).nullable().optional()
3299
3675
  }))
3300
3676
  }))
3301
3677
  });
@@ -3303,7 +3679,7 @@ var init_vitest = __esm({
3303
3679
  });
3304
3680
 
3305
3681
  // src/review/evidence/sarif.ts
3306
- import { z as z13 } from "zod";
3682
+ import { z as z14 } from "zod";
3307
3683
  function parseSarif(raw, sourceRoot) {
3308
3684
  const report = schema2.parse(raw), findings = [], diagnostics = [];
3309
3685
  let active = 0, suppressed = 0, absent = 0, unresolved = 0;
@@ -3394,29 +3770,29 @@ var init_sarif = __esm({
3394
3770
  "use strict";
3395
3771
  init_paths2();
3396
3772
  init_types2();
3397
- index = z13.number().int().nonnegative();
3398
- artifact = z13.object({ uri: z13.string().optional(), uriBaseId: z13.string().optional(), index: index.optional() });
3399
- message = z13.object({ text: z13.string().optional(), markdown: z13.string().optional(), id: z13.string().optional(), arguments: z13.array(z13.string()).optional() });
3400
- level = z13.enum(["error", "warning", "note", "none"]);
3401
- location = z13.object({ physicalLocation: z13.object({ artifactLocation: artifact.optional(), region: z13.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
3402
- schema2 = z13.object({
3403
- version: z13.literal("2.1.0"),
3404
- runs: z13.array(z13.object({
3405
- tool: z13.object({ driver: z13.object({ name: z13.string().min(1), rules: z13.array(z13.object({ id: z13.string(), defaultConfiguration: z13.object({ level: level.optional() }).optional(), messageStrings: z13.record(message).optional() })).optional(), globalMessageStrings: z13.record(message).optional() }) }),
3406
- invocations: z13.array(z13.object({ executionSuccessful: z13.boolean(), commandLine: z13.string().optional(), toolExecutionNotifications: z13.array(z13.object({ level: level.optional(), message })).optional() })).optional(),
3407
- versionControlProvenance: z13.array(z13.object({ revisionId: z13.string().optional() })).optional(),
3408
- originalUriBaseIds: z13.record(artifact).optional(),
3409
- artifacts: z13.array(z13.object({ location: artifact.optional() })).optional(),
3410
- results: z13.array(z13.object({
3411
- ruleId: z13.string().optional(),
3773
+ index = z14.number().int().nonnegative();
3774
+ artifact = z14.object({ uri: z14.string().optional(), uriBaseId: z14.string().optional(), index: index.optional() });
3775
+ message = z14.object({ text: z14.string().optional(), markdown: z14.string().optional(), id: z14.string().optional(), arguments: z14.array(z14.string()).optional() });
3776
+ level = z14.enum(["error", "warning", "note", "none"]);
3777
+ location = z14.object({ physicalLocation: z14.object({ artifactLocation: artifact.optional(), region: z14.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
3778
+ schema2 = z14.object({
3779
+ version: z14.literal("2.1.0"),
3780
+ runs: z14.array(z14.object({
3781
+ tool: z14.object({ driver: z14.object({ name: z14.string().min(1), rules: z14.array(z14.object({ id: z14.string(), defaultConfiguration: z14.object({ level: level.optional() }).optional(), messageStrings: z14.record(message).optional() })).optional(), globalMessageStrings: z14.record(message).optional() }) }),
3782
+ invocations: z14.array(z14.object({ executionSuccessful: z14.boolean(), commandLine: z14.string().optional(), toolExecutionNotifications: z14.array(z14.object({ level: level.optional(), message })).optional() })).optional(),
3783
+ versionControlProvenance: z14.array(z14.object({ revisionId: z14.string().optional() })).optional(),
3784
+ originalUriBaseIds: z14.record(artifact).optional(),
3785
+ artifacts: z14.array(z14.object({ location: artifact.optional() })).optional(),
3786
+ results: z14.array(z14.object({
3787
+ ruleId: z14.string().optional(),
3412
3788
  ruleIndex: index.optional(),
3413
3789
  message,
3414
3790
  level: level.optional(),
3415
- kind: z13.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
3416
- baselineState: z13.enum(["new", "unchanged", "updated", "absent"]).optional(),
3417
- suppressions: z13.array(z13.object({ kind: z13.enum(["inSource", "external"]), status: z13.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
3418
- locations: z13.array(location).optional(),
3419
- relatedLocations: z13.array(location).optional()
3791
+ kind: z14.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
3792
+ baselineState: z14.enum(["new", "unchanged", "updated", "absent"]).optional(),
3793
+ suppressions: z14.array(z14.object({ kind: z14.enum(["inSource", "external"]), status: z14.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
3794
+ locations: z14.array(location).optional(),
3795
+ relatedLocations: z14.array(location).optional()
3420
3796
  })).optional()
3421
3797
  }))
3422
3798
  });
@@ -3424,10 +3800,10 @@ var init_sarif = __esm({
3424
3800
  });
3425
3801
 
3426
3802
  // src/review/evidence.ts
3427
- import path18 from "path";
3428
- import { z as z14 } from "zod";
3803
+ import path19 from "path";
3804
+ import { z as z15 } from "zod";
3429
3805
  function relativeArtifact(root, file) {
3430
- const relative = path18.isAbsolute(file) ? path18.relative(root, file) : file;
3806
+ const relative = path19.isAbsolute(file) ? path19.relative(root, file) : file;
3431
3807
  const normalized = normalizeRepoPath(relative);
3432
3808
  if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
3433
3809
  return normalized;
@@ -3466,7 +3842,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
3466
3842
  let manifest, raw;
3467
3843
  try {
3468
3844
  manifest = relativeArtifact(root, supplied);
3469
- raw = z14.object({ version: z14.literal(1), checks: z14.array(z14.unknown()) }).parse(await readStoreJson(root, manifest));
3845
+ raw = z15.object({ version: z15.literal(1), checks: z15.array(z15.unknown()) }).parse(await readStoreJson(root, manifest));
3470
3846
  } catch (error) {
3471
3847
  output.diagnostics.push(`${supplied}: ${String(error)}`);
3472
3848
  continue;
@@ -3523,7 +3899,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
3523
3899
  const report = await readStoreJson(root, file);
3524
3900
  if (report === null) throw new Error(`Report artifact is missing: ${file}`);
3525
3901
  const sourceRoot = check.sourceRoot ?? root;
3526
- if (!path18.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
3902
+ if (!path19.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
3527
3903
  const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : parseSarif(report, sourceRoot);
3528
3904
  result.outcome = parsed.outcome;
3529
3905
  result.counts = parsed.counts;
@@ -3619,31 +3995,31 @@ var init_evidence2 = __esm({
3619
3995
  init_vitest();
3620
3996
  init_sarif();
3621
3997
  init_types2();
3622
- checkSchema2 = z14.object({
3623
- id: z14.string().min(1).max(100),
3624
- kind: z14.enum(["tests", "static-analysis", "security", "complexity", "duplication"]),
3625
- tool: z14.string().min(1).max(200),
3626
- command: z14.string().min(1).max(2e3),
3627
- commit: z14.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
3628
- workingTreeClean: z14.boolean().optional(),
3629
- source: z14.string().max(2e3).optional(),
3630
- sourceRoot: z14.string().min(1).max(4e3).optional(),
3631
- status: z14.enum(["completed", "skipped", "unavailable"]).default("completed"),
3632
- reason: z14.string().min(1).max(2e3).optional(),
3633
- exitCode: z14.number().int().nullable().optional(),
3634
- report: z14.object({ format: z14.enum(["vitest-json", "sarif"]), path: z14.string().min(1).max(4e3) }).optional()
3998
+ checkSchema2 = z15.object({
3999
+ id: z15.string().min(1).max(100),
4000
+ kind: z15.enum(["tests", "static-analysis", "security", "complexity", "duplication"]),
4001
+ tool: z15.string().min(1).max(200),
4002
+ command: z15.string().min(1).max(2e3),
4003
+ commit: z15.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
4004
+ workingTreeClean: z15.boolean().optional(),
4005
+ source: z15.string().max(2e3).optional(),
4006
+ sourceRoot: z15.string().min(1).max(4e3).optional(),
4007
+ status: z15.enum(["completed", "skipped", "unavailable"]).default("completed"),
4008
+ reason: z15.string().min(1).max(2e3).optional(),
4009
+ exitCode: z15.number().int().nullable().optional(),
4010
+ report: z15.object({ format: z15.enum(["vitest-json", "sarif"]), path: z15.string().min(1).max(4e3) }).optional()
3635
4011
  });
3636
4012
  }
3637
4013
  });
3638
4014
 
3639
4015
  // src/review/review.ts
3640
- import fs14 from "fs/promises";
3641
- import path19 from "path";
3642
- import { execFile as execFile9 } from "child_process";
3643
- import { promisify as promisify9 } from "util";
4016
+ import fs15 from "fs/promises";
4017
+ import path20 from "path";
4018
+ import { execFile as execFile12 } from "child_process";
4019
+ import { promisify as promisify12 } from "util";
3644
4020
  async function resolveMergeBase(resolvedRoot, base, head) {
3645
4021
  try {
3646
- const { stdout } = await exec9("git", ["merge-base", base, head], {
4022
+ const { stdout } = await exec10("git", ["merge-base", base, head], {
3647
4023
  cwd: resolvedRoot
3648
4024
  });
3649
4025
  return stdout.trim() || null;
@@ -3654,7 +4030,7 @@ async function resolveMergeBase(resolvedRoot, base, head) {
3654
4030
  async function defaultBase(resolvedRoot) {
3655
4031
  for (const ref of ["origin/HEAD", "origin/main", "origin/master", "main"]) {
3656
4032
  try {
3657
- await exec9("git", ["rev-parse", "--verify", "--quiet", ref], {
4033
+ await exec10("git", ["rev-parse", "--verify", "--quiet", ref], {
3658
4034
  cwd: resolvedRoot
3659
4035
  });
3660
4036
  return ref;
@@ -3667,7 +4043,7 @@ function anchorsTouched(record, changedFiles) {
3667
4043
  return matchingPaths(record.files, changedFiles);
3668
4044
  }
3669
4045
  async function computeReview(rootDir, base, options = {}) {
3670
- const resolvedRoot = path19.resolve(rootDir);
4046
+ const resolvedRoot = path20.resolve(rootDir);
3671
4047
  const head = await getCurrentGitHash(resolvedRoot);
3672
4048
  if (head === "unknown") return null;
3673
4049
  const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
@@ -3717,7 +4093,7 @@ async function computeReview(rootDir, base, options = {}) {
3717
4093
  analyzed,
3718
4094
  async (relPath) => {
3719
4095
  try {
3720
- await fs14.access(path19.join(resolvedRoot, relPath));
4096
+ await fs15.access(path20.join(resolvedRoot, relPath));
3721
4097
  return true;
3722
4098
  } catch {
3723
4099
  return false;
@@ -3750,7 +4126,7 @@ async function computeReview(rootDir, base, options = {}) {
3750
4126
  }
3751
4127
  return finalize();
3752
4128
  }
3753
- var exec9, MAX_ANALYZED_FILES;
4129
+ var exec10, MAX_ANALYZED_FILES;
3754
4130
  var init_review = __esm({
3755
4131
  "src/review/review.ts"() {
3756
4132
  "use strict";
@@ -3762,7 +4138,7 @@ var init_review = __esm({
3762
4138
  init_cochange();
3763
4139
  init_evidence2();
3764
4140
  init_snapshot();
3765
- exec9 = promisify9(execFile9);
4141
+ exec10 = promisify12(execFile12);
3766
4142
  MAX_ANALYZED_FILES = 50;
3767
4143
  }
3768
4144
  });
@@ -3851,431 +4227,6 @@ var init_onboarding = __esm({
3851
4227
  }
3852
4228
  });
3853
4229
 
3854
- // src/setup/files.ts
3855
- import fs15 from "fs/promises";
3856
- import path20 from "path";
3857
- import { randomUUID as randomUUID5 } from "crypto";
3858
- async function readText(root, file) {
3859
- try {
3860
- const text2 = await readBoundedFile(await storePath(root, file), 2 * 1024 * 1024);
3861
- if (text2 === null) throw new Error("Setup input is not a regular file or exceeds 2 MiB: " + file);
3862
- return text2;
3863
- } catch (error) {
3864
- if (error.code === "ENOENT") return null;
3865
- throw error;
3866
- }
3867
- }
3868
- function managedBlock(text2, start, end, body) {
3869
- const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
3870
- if (starts !== ends || starts > 1 || starts === 1 && text2.indexOf(end) < text2.indexOf(start)) {
3871
- throw new Error("Ambiguous Mason instruction/configuration markers; repair the marked block before setup.");
3872
- }
3873
- const eol = text2.includes("\r\n") ? "\r\n" : "\n";
3874
- const block = [start, body.replace(/\r?\n/g, eol), end].join(eol);
3875
- if (starts) return text2.slice(0, text2.indexOf(start)) + block + text2.slice(text2.indexOf(end) + end.length);
3876
- return text2 + (text2.length && !text2.endsWith("\n") ? eol : "") + (text2.length ? eol : "") + block + eol;
3877
- }
3878
- async function applyEdit(root, edit) {
3879
- const current = await readText(root, edit.path);
3880
- if (current === edit.after) return false;
3881
- if (current !== edit.before) throw new Error("Setup input changed during installation: " + edit.path + ". Rerun setup to resume.");
3882
- const file = await storePath(root, edit.path, true);
3883
- const temporary = path20.join(path20.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
3884
- try {
3885
- const mode = await fs15.stat(file).then((s) => s.mode & 511, () => 384);
3886
- const handle = await fs15.open(temporary, "wx", mode);
3887
- try {
3888
- await handle.writeFile(edit.after, "utf8");
3889
- await handle.sync();
3890
- } finally {
3891
- await handle.close();
3892
- }
3893
- if (await readText(root, edit.path) !== edit.before) throw new Error("Setup input changed during installation: " + edit.path);
3894
- await fs15.rename(temporary, file);
3895
- return true;
3896
- } finally {
3897
- await fs15.rm(temporary, { force: true });
3898
- }
3899
- }
3900
- var init_files2 = __esm({
3901
- "src/setup/files.ts"() {
3902
- "use strict";
3903
- init_files();
3904
- init_storage();
3905
- }
3906
- });
3907
-
3908
- // src/setup/launcher.ts
3909
- var BOOTSTRAP, POWERSHELL_BOOTSTRAP, mcpCommand, hookCommand, SHELL_LAUNCHER, POWERSHELL_LAUNCHER, LAUNCHER;
3910
- var init_launcher = __esm({
3911
- "src/setup/launcher.ts"() {
3912
- "use strict";
3913
- BOOTSTRAP = "require(require('node:path').join(require('node:child_process').execFileSync('git',['rev-parse','--show-toplevel'],{encoding:'utf8'}).trim(),'.mason','run.cjs'))";
3914
- POWERSHELL_BOOTSTRAP = "& (Join-Path (git rev-parse --show-toplevel) '.mason/run.ps1')";
3915
- 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"] };
3916
- 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`;
3917
- SHELL_LAUNCHER = `#!/bin/sh
3918
- set -eu
3919
- advisory=0
3920
- if [ "\${2-}" = auto ] && [ "\${3-}" = hook ]; then advisory=1; fi
3921
- fail() {
3922
- 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
3923
- echo 'Mason runtime unavailable. Rerun mason setup for this checkout.' >&2; exit 2
3924
- }
3925
- root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) || fail
3926
- case "\${1-}" in codex|claude) ;; *) fail ;; esac
3927
- pointer="$root/.mason/runtime/$1.txt"
3928
- [ -f "$pointer" ] || fail
3929
- runtime=$(cat "$pointer") || fail
3930
- case "$runtime" in ''|*[!0-9a-f]*) fail ;; esac
3931
- [ "\${#runtime}" -eq 24 ] || fail
3932
- [ -x "$root/.mason/runtime/$runtime/node" ] || fail
3933
- if [ "$advisory" -eq 1 ]; then
3934
- "$root/.mason/runtime/$runtime/node" "$root/.mason/run.cjs" "$@" || fail
3935
- else
3936
- exec "$root/.mason/runtime/$runtime/node" "$root/.mason/run.cjs" "$@"
3937
- fi
3938
- `;
3939
- POWERSHELL_LAUNCHER = `# Managed by mason setup; no system Node installation is used.
3940
- $ErrorActionPreference = 'Stop'
3941
- $advisory = $args.Count -ge 3 -and $args[1] -eq 'auto' -and $args[2] -eq 'hook'
3942
- try {
3943
- if ($args.Count -lt 2 -or $args[0] -notin @('codex','claude')) { throw 'Invalid Mason host.' }
3944
- $runtime = (Get-Content -LiteralPath (Join-Path $PSScriptRoot ('runtime/' + $args[0] + '.txt')) -Raw).Trim()
3945
- if ($runtime -cnotmatch '^[a-f0-9]{24}$') { throw 'Invalid Mason runtime pointer.' }
3946
- & (Join-Path $PSScriptRoot ('runtime/' + $runtime + '/node.exe')) (Join-Path $PSScriptRoot 'run.cjs') @args
3947
- if ($LASTEXITCODE -ne 0) { throw 'Mason runtime failed.' }
3948
- } catch {
3949
- if ($advisory) { Write-Output '{"systemMessage":"Mason runtime unavailable. Rerun mason setup for this checkout; verification was not established."}'; exit 0 }
3950
- [Console]::Error.WriteLine($_.Exception.Message); exit 2
3951
- }
3952
- `;
3953
- LAUNCHER = `// Managed by mason-auto setup. Rerun setup to install this checkout's pinned runtime.
3954
- const fs = require('node:fs');
3955
- const path = require('node:path');
3956
- const {pathToFileURL} = require('node:url');
3957
- const launchArgs = process.argv.slice(process.argv[1] && path.resolve(process.argv[1]) === __filename ? 2 : 1);
3958
- (async () => {
3959
- const root = path.dirname(__dirname);
3960
- const args = launchArgs;
3961
- const [host, command, ...rest] = args;
3962
- if (!['codex','claude'].includes(host) || !['auto','mcp'].includes(command)) throw new Error('Invalid Mason launcher arguments.');
3963
- const setup = JSON.parse(fs.readFileSync(path.join(__dirname, 'setup.json'), 'utf8'));
3964
- const entry = setup.hosts?.[host];
3965
- 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.');
3966
- const base = path.join(__dirname, 'runtime', entry.runtime.id);
3967
- if (entry.runtime.bundle) {
3968
- const node = process.platform === 'win32' ? 'node.exe' : 'node';
3969
- if (fs.realpathSync(process.execPath) !== fs.realpathSync(path.join(base, node))) throw new Error('Mason runtime pointer differs from the configured version. Rerun setup.');
3970
- }
3971
- const binary = entry.runtime.bundle ? path.join(base, 'app', 'dist', 'mason-' + command + '.js')
3972
- : path.join(base, 'node_modules', 'mason-context', 'dist', 'mason-' + command + '.js');
3973
- if (!fs.existsSync(binary)) throw new Error('The pinned Mason runtime is missing in this checkout. Rerun mason-auto setup --host ' + host + '.');
3974
- process.env.MASON_SETUP_ROOT = root;
3975
- process.env.MASON_SETUP_HOST = host;
3976
- process.env.MASON_SETUP_REVISION = entry.revision;
3977
- process.argv = [process.execPath, binary, ...rest];
3978
- await import(pathToFileURL(binary).href);
3979
- })().catch(error => {
3980
- const message = 'Mason setup unavailable: ' + error.message;
3981
- if (launchArgs[1] === 'auto' && launchArgs[2] === 'hook') { console.log(JSON.stringify({systemMessage: message})); process.exitCode = 0; }
3982
- else { console.error(message); process.exitCode = 2; }
3983
- });
3984
- `;
3985
- }
3986
- });
3987
-
3988
- // src/setup/config.ts
3989
- import { isDeepStrictEqual } from "util";
3990
- import { parse, stringify } from "smol-toml";
3991
- import { z as z15 } from "zod";
3992
- async function instructionEdits(root, host) {
3993
- const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
3994
- const existing = found.find((f) => f.text !== null);
3995
- const primary = host === "codex" && found[0].text === null ? { path: "AGENTS.md", text: null } : existing ?? { path: "AGENTS.md", text: null };
3996
- const originalGuidance = primary.text === null && existing ? "Follow the existing project conventions in " + existing.path + ".\n" : "";
3997
- const edits = [{
3998
- path: primary.path,
3999
- before: primary.text,
4000
- after: managedBlock(primary.text ?? originalGuidance, "<!-- mason:start -->", "<!-- mason:end -->", CLAUDE_MD_SECTION.split("\n").slice(1, -1).join("\n"))
4001
- }];
4002
- if (primary.path === "AGENTS.md") {
4003
- const secondary = found.filter((f) => f.path !== "AGENTS.md" && f.text !== null);
4004
- if (host === "claude" && !secondary.length) secondary.push({ path: "CLAUDE.md", text: null });
4005
- for (const file of secondary) {
4006
- const text2 = file.text ?? "";
4007
- const legacy = text2.includes("<!-- mason:start -->") || text2.includes("<!-- mason:end -->");
4008
- const imported = "@" + (file.path.startsWith(".claude/") ? "../AGENTS.md" : "AGENTS.md");
4009
- if (text2.trim() === imported) continue;
4010
- const pointer = "Mason project knowledge and shared project instructions:\n" + imported;
4011
- edits.push({
4012
- path: file.path,
4013
- before: file.text,
4014
- after: managedBlock(text2, legacy ? "<!-- mason:start -->" : POINTER_START, legacy ? "<!-- mason:end -->" : POINTER_END, pointer)
4015
- });
4016
- }
4017
- }
4018
- return edits;
4019
- }
4020
- function managedMcp(previous, host, runtime) {
4021
- const options = previous === void 0 ? {} : object.parse(previous);
4022
- for (const key of ["command", "args", "url", "type", "headers", "http_headers", "env_http_headers", "bearer_token_env_var"]) delete options[key];
4023
- return { ...options, ...mcpCommand(host, runtime) };
4024
- }
4025
- async function mcpEdit(root, host, runtime) {
4026
- const file = mcpPath(host), before = await readText(root, file);
4027
- if (host === "claude") {
4028
- const config2 = before === null ? {} : object.parse(JSON.parse(before));
4029
- const servers2 = object.parse(config2.mcpServers ?? {});
4030
- return { path: file, before, after: JSON.stringify({ ...config2, mcpServers: { ...servers2, mason: managedMcp(servers2.mason, host, runtime) } }, null, 2) + "\n" };
4031
- }
4032
- const config = parse(before ?? "");
4033
- const managed = (before ?? "").includes(TOML_START);
4034
- const servers = config.mcp_servers;
4035
- const desired = managedMcp(servers?.mason, host, runtime);
4036
- let base = before ?? "";
4037
- if (servers?.mason && !managed) {
4038
- const headers = [...base.matchAll(/^\s*\[\[?.+?\]\]?[^\S\r\n]*(?:#.*)?$/gm)];
4039
- const spans = headers.flatMap((header, index2) => /^\s*\[mcp_servers\.mason(?:\.[A-Za-z0-9_-]+)*\]/.test(header[0]) ? [{ start: header.index, end: headers[index2 + 1]?.index ?? base.length }] : []);
4040
- if (!spans.length) throw new Error("Cannot safely migrate the existing Mason MCP TOML entry. Use an explicit [mcp_servers.mason] table and rerun setup; the file was retained.");
4041
- for (const span of spans.reverse()) base = base.slice(0, span.start) + base.slice(span.end);
4042
- }
4043
- const after = managedBlock(base, TOML_START, TOML_END, stringify({ mcp_servers: { mason: desired } }).trimEnd());
4044
- const parsed = parse(after);
4045
- const expected = { ...config, mcp_servers: { ...servers ?? {}, mason: desired } };
4046
- if (!isDeepStrictEqual(parsed, expected)) throw new Error("Could not safely configure the Mason MCP entry without changing other settings.");
4047
- return { path: file, before, after };
4048
- }
4049
- async function hookEdits(root, host, runtime) {
4050
- const file = configPath(host), before = await readText(root, file);
4051
- const { planAutomationInstall: planAutomationInstall2 } = await Promise.resolve().then(() => (init_install(), install_exports));
4052
- const plan = await planAutomationInstall2(root, host, hookCommand(host, runtime));
4053
- return [
4054
- { path: file, before, after: JSON.stringify(plan.config, null, 2) + "\n" },
4055
- { path: ".mason/automation.json", before: await readText(root, ".mason/automation.json"), after: JSON.stringify(plan.record, null, 2) + "\n" }
4056
- ];
4057
- }
4058
- async function ancillaryEdits(root) {
4059
- const ignore = await readText(root, ".gitignore");
4060
- const { git: git3 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
4061
- let parentIgnored = false;
4062
- try {
4063
- parentIgnored = !!(await git3(root, "check-ignore", "--no-index", ".mason")).trim();
4064
- } catch (error) {
4065
- if (error.code !== 1) throw error;
4066
- }
4067
- const retainParentRule = parentIgnored || !!ignore?.replace(/\r\n/g, "\n").includes("# mason:ignore:start\n!/.mason/\n/.mason/*");
4068
- 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");
4069
- return [
4070
- { path: ".gitignore", before: ignore, after: managedBlock(ignore ?? "", "# mason:ignore:start", "# mason:ignore:end", rules) },
4071
- { path: ".mason/run.cjs", before: await readText(root, ".mason/run.cjs"), after: LAUNCHER },
4072
- { path: ".mason/run.sh", before: await readText(root, ".mason/run.sh"), after: SHELL_LAUNCHER },
4073
- { path: ".mason/run.ps1", before: await readText(root, ".mason/run.ps1"), after: POWERSHELL_LAUNCHER }
4074
- ];
4075
- }
4076
- async function inspectHostConfig(root, host, plannedMcp, runtime) {
4077
- const text2 = plannedMcp ?? await readText(root, mcpPath(host));
4078
- const config = host === "codex" ? parse(text2 ?? "") : object.parse(JSON.parse(text2 ?? "{}"));
4079
- const servers = host === "codex" ? config.mcp_servers : config.mcpServers;
4080
- const hooks = automationConfigSchema.parse(JSON.parse(await readText(root, configPath(host)) ?? "{}"));
4081
- const expected = hookConfig(host, hookCommand(host, runtime));
4082
- const mcp = servers?.mason === void 0 ? null : object.parse(servers.mason);
4083
- return {
4084
- mcp,
4085
- mcpDisabled: mcp?.enabled === false,
4086
- hooks: Object.fromEntries(Object.keys(expected.hooks).map((event) => [
4087
- event,
4088
- (hooks.hooks?.[event] ?? []).flatMap((group) => group.hooks.filter((handler) => handler.command === expected.hooks.SessionStart[0].hooks[0].command).map((handler) => ({ ...group, hooks: [handler] })) ?? [])
4089
- ])),
4090
- disabled: hooks.disableAllHooks === true || config.features?.hooks === false || config.features?.codex_hooks === false
4091
- };
4092
- }
4093
- var object, mcpPath, TOML_START, TOML_END, POINTER_START, POINTER_END;
4094
- var init_config = __esm({
4095
- "src/setup/config.ts"() {
4096
- "use strict";
4097
- init_init();
4098
- init_docs();
4099
- init_adapters();
4100
- init_install();
4101
- init_files2();
4102
- init_launcher();
4103
- object = z15.record(z15.unknown());
4104
- mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
4105
- TOML_START = "# mason:mcp:start";
4106
- TOML_END = "# mason:mcp:end";
4107
- POINTER_START = "<!-- mason:agents:start -->";
4108
- POINTER_END = "<!-- mason:agents:end -->";
4109
- }
4110
- });
4111
-
4112
- // src/distribution/bundle.ts
4113
- import fs16 from "fs/promises";
4114
- import path21 from "path";
4115
- import { createHash as createHash4 } from "crypto";
4116
- import { z as z16 } from "zod";
4117
- async function readBundle(root) {
4118
- const bytes = await fs16.readFile(await storePath(root, "bundle.json"));
4119
- const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
4120
- const windows = manifest.target.startsWith("win32-");
4121
- for (const file of Object.keys(manifest.files)) {
4122
- if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") || path21.isAbsolute(file)) {
4123
- throw new Error("Unsafe path in Mason bundle: " + file);
4124
- }
4125
- }
4126
- 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"]) {
4127
- if (!manifest.files[required]) throw new Error("Incomplete Mason bundle: " + required);
4128
- }
4129
- return { manifest, manifestHash: sha256(bytes) };
4130
- }
4131
- async function verifyBundle(root, expectedHash) {
4132
- const bundle = await readBundle(root);
4133
- if (expectedHash && bundle.manifestHash !== expectedHash) throw new Error("Mason bundle manifest changed.");
4134
- 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.");
4135
- const files = Object.keys(bundle.manifest.files);
4136
- for (let i = 0; i < files.length; i += 8) await Promise.all(files.slice(i, i + 8).map(async (file) => {
4137
- if (sha256(await fs16.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
4138
- }));
4139
- return bundle;
4140
- }
4141
- var targets, digest2, bundleSchema, sha256;
4142
- var init_bundle = __esm({
4143
- "src/distribution/bundle.ts"() {
4144
- "use strict";
4145
- init_storage();
4146
- targets = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
4147
- digest2 = z16.string().regex(/^[a-f0-9]{64}$/);
4148
- bundleSchema = z16.object({
4149
- format: z16.literal(1),
4150
- version: z16.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
4151
- target: z16.enum(targets),
4152
- nodeVersion: z16.string(),
4153
- files: z16.record(digest2)
4154
- }).strict();
4155
- sha256 = (bytes) => createHash4("sha256").update(bytes).digest("hex");
4156
- }
4157
- });
4158
-
4159
- // src/setup/runtime.ts
4160
- import fs17 from "fs/promises";
4161
- import path22 from "path";
4162
- import { fileURLToPath } from "url";
4163
- import { randomUUID as randomUUID6, createHash as createHash5 } from "crypto";
4164
- import { execFile as execFile10 } from "child_process";
4165
- import { promisify as promisify10 } from "util";
4166
- async function packageRoot() {
4167
- let directory = path22.dirname(fileURLToPath(import.meta.url));
4168
- for (let i = 0; i < 8; i++) {
4169
- try {
4170
- const pkg = JSON.parse(await fs17.readFile(path22.join(directory, "package.json"), "utf8"));
4171
- if (pkg.name === "mason-context") return directory;
4172
- } catch {
4173
- }
4174
- directory = path22.dirname(directory);
4175
- }
4176
- throw new Error("Cannot locate the executing Mason distribution. Reinstall Mason and rerun setup.");
4177
- }
4178
- async function sourceRuntime() {
4179
- const source = await packageRoot();
4180
- const pkg = JSON.parse(await fs17.readFile(path22.join(source, "package.json"), "utf8"));
4181
- const hashes = Object.fromEntries(await Promise.all(BINARIES.map(async (file) => [file, checksum(await fs17.readFile(path22.join(source, file)))])));
4182
- const bundleRoot = path22.dirname(source);
4183
- const bundleExists = await fs17.lstat(path22.join(bundleRoot, "bundle.json")).then(() => true, (error) => {
4184
- if (error.code === "ENOENT") return false;
4185
- throw error;
4186
- });
4187
- if (bundleExists) {
4188
- const { manifest, manifestHash } = await verifyBundle(bundleRoot);
4189
- if (manifest.version !== pkg.version) throw new Error("Mason package and bundle versions differ.");
4190
- return { source, bundleRoot, runtime: runtimeSchema.parse({
4191
- id: manifestHash.slice(0, 24),
4192
- version: pkg.version,
4193
- hashes,
4194
- bundle: { target: manifest.target, manifestHash }
4195
- }) };
4196
- }
4197
- return { source, runtime: runtimeSchema.parse({ id: hash([pkg.version, pkg.dependencies, hashes]).slice(0, 24), version: pkg.version, hashes }) };
4198
- }
4199
- async function verifyRuntime(root, runtime) {
4200
- try {
4201
- const saved = await readStoreJson(root, `.mason/runtime/${runtime.id}/receipt.json`);
4202
- if (JSON.stringify(saved) !== JSON.stringify(runtime)) return false;
4203
- if (runtime.bundle) {
4204
- const base2 = await storePath(root, `.mason/runtime/${runtime.id}`);
4205
- const { manifest } = await verifyBundle(base2, runtime.bundle.manifestHash);
4206
- if (manifest.version !== runtime.version || manifest.target !== runtime.bundle.target) return false;
4207
- for (const file of BINARIES) if (manifest.files["app/" + file] !== runtime.hashes[file]) return false;
4208
- return true;
4209
- }
4210
- const base = `.mason/runtime/${runtime.id}/node_modules/mason-context/`;
4211
- const pkg = await readStoreJson(root, base + "package.json");
4212
- if (pkg?.name !== "mason-context" || pkg.version !== runtime.version) return false;
4213
- for (const file of BINARIES) {
4214
- if (checksum(await fs17.readFile(await storePath(root, base + file))) !== runtime.hashes[file]) return false;
4215
- }
4216
- return true;
4217
- } catch {
4218
- return false;
4219
- }
4220
- }
4221
- async function installRuntime(root, selected) {
4222
- const { runtime, source } = selected;
4223
- if (await verifyRuntime(root, runtime)) return runtime;
4224
- const relative = `.mason/runtime/${runtime.id}`;
4225
- const target = await storePath(root, relative, true);
4226
- const stage = await storePath(root, ".mason/runtime/.install-" + randomUUID6(), true);
4227
- await fs17.mkdir(stage);
4228
- try {
4229
- if (runtime.bundle) {
4230
- if (!selected.bundleRoot) throw new Error("Standalone bundle source is missing.");
4231
- const { manifest } = await verifyBundle(selected.bundleRoot, runtime.bundle.manifestHash);
4232
- for (const file of ["bundle.json", ...Object.keys(manifest.files)]) {
4233
- const from = await storePath(selected.bundleRoot, file);
4234
- const to = await storePath(stage, file, true);
4235
- await fs17.copyFile(from, to);
4236
- await fs17.chmod(to, (await fs17.stat(from)).mode & 511);
4237
- }
4238
- await verifyBundle(stage, runtime.bundle.manifestHash);
4239
- } else {
4240
- const npm = process.platform === "win32" ? "npm.cmd" : "npm";
4241
- const options = { timeout: 12e4, maxBuffer: 2 * 1024 * 1024, windowsHide: true };
4242
- const packed = JSON.parse((await exec10(npm, ["pack", "--ignore-scripts", "--json", "--pack-destination", stage], { ...options, cwd: source })).stdout);
4243
- const filename = packed[0]?.filename;
4244
- if (typeof filename !== "string" || path22.basename(filename) !== filename) throw new Error("npm did not produce a Mason package archive.");
4245
- await fs17.rename(path22.join(stage, filename), path22.join(stage, "mason.tgz"));
4246
- await fs17.writeFile(path22.join(stage, "package.json"), JSON.stringify({ name: "mason-project-runtime", private: true, version: "1.0.0" }));
4247
- await exec10(npm, ["install", "--ignore-scripts", "--omit=dev", "--no-audit", "--no-fund", "--save-exact", "./mason.tgz"], { ...options, cwd: stage });
4248
- for (const file of BINARIES) {
4249
- if (checksum(await fs17.readFile(path22.join(stage, "node_modules/mason-context", file))) !== runtime.hashes[file]) throw new Error("Installed Mason binary differs from the selected distribution.");
4250
- }
4251
- }
4252
- await fs17.writeFile(path22.join(stage, "receipt.json"), JSON.stringify(runtime, null, 2) + "\n");
4253
- try {
4254
- await fs17.rename(target, target + ".previous-" + randomUUID6());
4255
- } catch (error) {
4256
- if (error.code !== "ENOENT") throw error;
4257
- }
4258
- await fs17.rename(stage, target);
4259
- if (!await verifyRuntime(root, runtime)) throw new Error("The installed Mason runtime could not be verified.");
4260
- return runtime;
4261
- } finally {
4262
- await fs17.rm(stage, { recursive: true, force: true });
4263
- }
4264
- }
4265
- var exec10, checksum, BINARIES;
4266
- var init_runtime2 = __esm({
4267
- "src/setup/runtime.ts"() {
4268
- "use strict";
4269
- init_model();
4270
- init_storage();
4271
- init_evidence();
4272
- init_bundle();
4273
- exec10 = promisify10(execFile10);
4274
- checksum = (bytes) => createHash5("sha256").update(bytes).digest("hex");
4275
- BINARIES = ["dist/mason-auto.js", "dist/mason-mcp.js"];
4276
- }
4277
- });
4278
-
4279
4230
  // src/setup/status.ts
4280
4231
  var status_exports = {};
4281
4232
  __export(status_exports, {
@@ -4300,34 +4251,32 @@ async function setupStatus(dir) {
4300
4251
  };
4301
4252
  }
4302
4253
  const hosts = {};
4303
- const commonLauncherCurrent = await readText(ws.root, ".mason/run.cjs") === LAUNCHER;
4254
+ const installed = await installedCommand();
4304
4255
  const automation = await automationStatus(ws.root);
4305
4256
  for (const host of ["codex", "claude"]) {
4306
4257
  const entry = setup.hosts[host];
4307
4258
  if (!entry) continue;
4308
- 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");
4309
4259
  const instructions = await instructionEdits(ws.root, host);
4310
4260
  const instructionsCurrent = instructions.every((edit) => edit.before === edit.after);
4311
- const installed = await verifyRuntime(ws.root, entry.runtime);
4312
- const config = await inspectHostConfig(ws.root, host, void 0, entry.runtime);
4313
- const mcp = !config.mcpDisabled && hash(config.mcp) === entry.mcpFingerprint && isDeepStrictEqual2({ command: config.mcp?.command, args: config.mcp?.args }, mcpCommand(host, entry.runtime));
4314
- const hooks = isDeepStrictEqual2(config.hooks, hookConfig(host, hookCommand(host, entry.runtime)).hooks) && !config.disabled;
4261
+ const config = await inspectHostConfig(ws.root, host, void 0);
4262
+ const mcp = !config.mcpDisabled && hash(config.mcp) === entry.mcpFingerprint && isDeepStrictEqual2({ command: config.mcp?.command, args: config.mcp?.args }, mcpCommand(host));
4263
+ const hooks = isDeepStrictEqual2(config.hooks, hookConfig(host, hookCommand(host)).hooks) && !config.disabled;
4315
4264
  const local = await loadSetupReceipt(ws.root, ws.directory, host);
4316
4265
  const configured = local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
4317
- const observation = await readObservation(ws.root, ws.directory, host, entry.revision);
4266
+ const observation = await readObservation(ws.root, ws.directory, host, entry.revision, installed.version ?? "unavailable");
4318
4267
  const sessions = Object.values(observation?.sessions ?? {}).sort((a, b) => b.at.localeCompare(a.at));
4319
4268
  const complete = sessions.find((session) => events.every((event) => session.events.includes(event)));
4320
4269
  const observedEvents = complete?.events ?? sessions[0]?.events ?? [];
4321
4270
  const pending = [];
4322
- if (!installed || !launcherCurrent) pending.push("Install this checkout's pinned runtime by rerunning setup.");
4271
+ if (!installed.available) pending.push(installed.message);
4323
4272
  if (!mcp || !hooks || !instructionsCurrent || !configured) pending.push("Rerun setup to reconcile project configuration; inspect any disabled host settings.");
4324
4273
  if (!observation?.contextCalls) pending.push("Start a new assistant session and request task context through Mason's get_context tool.");
4325
4274
  if (!complete) pending.push("Review/trust the host configuration, then complete a normal task to observe the full hook lifecycle.");
4326
4275
  if (automation.status === "unavailable") pending.push("The latest automation attempt did not establish verification. Run mason-auto check and inspect its diagnostics.");
4327
- const healthy = installed && launcherCurrent && mcp && hooks && instructionsCurrent && configured && automation.status !== "unavailable";
4276
+ const healthy = installed.available && mcp && hooks && instructionsCurrent && configured && automation.status !== "unavailable";
4328
4277
  hosts[host] = {
4329
4278
  status: healthy ? complete && observation?.contextCalls ? "active" : "pending" : "attention",
4330
- runtime: installed && launcherCurrent ? "installed" : "missing-or-changed",
4279
+ runtime: installed.available ? "global (" + installed.version + ")" : "unavailable-on-path",
4331
4280
  mcp: mcp ? "configured" : "changed",
4332
4281
  instructions: instructionsCurrent ? "current" : "changed",
4333
4282
  hookConfiguration: hooks ? "configured" : "disabled-or-changed",
@@ -4373,10 +4322,8 @@ var init_status = __esm({
4373
4322
  init_decisions();
4374
4323
  init_adapters();
4375
4324
  init_model();
4376
- init_runtime2();
4377
4325
  init_config();
4378
4326
  init_launcher();
4379
- init_files2();
4380
4327
  init_observations();
4381
4328
  }
4382
4329
  });
@@ -4388,7 +4335,7 @@ __export(setup_exports, {
4388
4335
  setupProject: () => setupProject,
4389
4336
  summarizeSetup: () => summarizeSetup
4390
4337
  });
4391
- import { randomUUID as randomUUID7 } from "crypto";
4338
+ import { randomUUID as randomUUID6 } from "crypto";
4392
4339
  async function selectHost(root, explicit) {
4393
4340
  if (explicit) return explicit;
4394
4341
  if (process.env.MASON_SETUP_HOST === "codex" || process.env.MASON_SETUP_HOST === "claude") return process.env.MASON_SETUP_HOST;
@@ -4404,28 +4351,27 @@ async function setupProject(dir, options = {}) {
4404
4351
  return withLock(ws.root, ".mason/reports/setup-lock", async () => {
4405
4352
  const existing = await loadSetup(ws.root);
4406
4353
  const previousReceipt = await loadSetupReceipt(ws.root, ws.directory, host);
4407
- const marker = await loadProjectMarker(ws.root);
4408
- const selected = await sourceRuntime();
4354
+ const installed = await installedCommand();
4355
+ if (!installed.available) throw new Error(installed.message);
4409
4356
  const instructions = await instructionEdits(ws.root, host);
4410
- const mcp = await mcpEdit(ws.root, host, selected.runtime);
4411
- const hooks = await hookEdits(ws.root, host, selected.runtime);
4357
+ const mcp = await mcpEdit(ws.root, host);
4358
+ const hooks = await hookEdits(ws.root, host);
4412
4359
  const ancillary = await ancillaryEdits(ws.root);
4413
4360
  const edits = [...ancillary, ...instructions, mcp, ...hooks];
4414
- const setupBefore = await readText(ws.root, ".mason/setup.json");
4415
- const mcpFingerprint = hash((await inspectHostConfig(ws.root, host, mcp.after, selected.runtime)).mcp);
4416
- const desiredHooks = hookConfig(host, hookCommand(host, selected.runtime)).hooks;
4361
+ const setupBefore = await readText(ws.root, SETUP_PATH);
4362
+ const mcpFingerprint = hash((await inspectHostConfig(ws.root, host, mcp.after)).mcp);
4363
+ const desiredHooks = hookConfig(host, hookCommand(host)).hooks;
4417
4364
  const fingerprint = hash({
4418
- runtime: selected.runtime,
4419
- launcher: LAUNCHER,
4365
+ command: "mason",
4420
4366
  mcp: mcpFingerprint,
4421
4367
  hooks: desiredHooks,
4422
4368
  instructions: instructions.map((edit) => edit.path)
4423
4369
  });
4424
4370
  const previous = existing?.hosts[host];
4425
4371
  const changed = edits.some((edit) => edit.before !== edit.after) || previous?.fingerprint !== fingerprint;
4426
- const revision = !changed && previous ? previous.revision : randomUUID7();
4427
- const setup = existing ?? { version: 1, hosts: {} };
4428
- setup.hosts[host] = { runtime: selected.runtime, revision, fingerprint, mcpFingerprint, instructions: instructions.map((e) => e.path) };
4372
+ const revision = !changed && previous ? previous.revision : randomUUID6();
4373
+ const setup = existing ?? { version: 2, hosts: {} };
4374
+ setup.hosts[host] = { configuredVersion: installed.version, revision, fingerprint, mcpFingerprint, instructions: instructions.map((e) => e.path) };
4429
4375
  const initial = await automate(ws.root, { event: "turn_start" });
4430
4376
  const findings = await inspectOnboarding(ws.root, options.base, options.evidence);
4431
4377
  const receiptPath = ws.directory + "/setup-" + host + ".json";
@@ -4440,20 +4386,11 @@ async function setupProject(dir, options = {}) {
4440
4386
  root: ws.root,
4441
4387
  revision
4442
4388
  });
4443
- await installRuntime(ws.root, selected);
4444
- if (selected.runtime.bundle) {
4445
- const pointer = `.mason/runtime/${host}.txt`;
4446
- await applyEdit(ws.root, { path: pointer, before: await readText(ws.root, pointer), after: selected.runtime.id + "\n" });
4447
- }
4448
4389
  const changedFiles = [];
4449
4390
  for (const edit of edits) if (await applyEdit(ws.root, edit)) changedFiles.push(edit.path);
4450
- if (await applyEdit(ws.root, { path: ".mason/setup.json", before: setupBefore, after: JSON.stringify(setup, null, 2) + "\n" })) changedFiles.push(".mason/setup.json");
4451
- if (!marker) {
4452
- await saveProjectMarker(ws.root, { version: 1, initializedAt: (/* @__PURE__ */ new Date()).toISOString() });
4453
- changedFiles.push(".mason/project.json");
4454
- }
4391
+ if (await applyEdit(ws.root, { path: SETUP_PATH, before: setupBefore, after: JSON.stringify(setup, null, 2) + "\n" })) changedFiles.push(SETUP_PATH);
4455
4392
  const checked = await automate(ws.root, { event: "turn_start" });
4456
- const configured = await inspectHostConfig(ws.root, host, void 0, selected.runtime);
4393
+ const configured = await inspectHostConfig(ws.root, host, void 0);
4457
4394
  await writeStoreJson(ws.root, receiptPath, {
4458
4395
  version: 1,
4459
4396
  host,
@@ -4470,7 +4407,7 @@ async function setupProject(dir, options = {}) {
4470
4407
  status: "configured",
4471
4408
  host,
4472
4409
  root: ws.root,
4473
- runtime: selected.runtime,
4410
+ runtime: { kind: "global", command: "mason", version: installed.version },
4474
4411
  changedFiles,
4475
4412
  initialReportPath,
4476
4413
  findings,
@@ -4484,14 +4421,15 @@ function summarizeSetup(result) {
4484
4421
  const audit = result.findings.audit;
4485
4422
  return [
4486
4423
  `Mason configured for ${result.host}.`,
4487
- " Runtime installed; MCP server and lifecycle hooks configured.",
4424
+ " MCP server and lifecycle hooks use mason from PATH.",
4488
4425
  " Project instructions updated; original audit evidence retained.",
4489
- ` ${result.changedFiles.length} shared files changed.`,
4426
+ ` ${result.changedFiles.length} files changed.`,
4490
4427
  `Initial audit: ${audit.status}${"counts" in audit ? `; ${audit.counts.issues} issues, ${audit.counts.advisories + audit.counts.suppressedAdvisories} advisories` : ""}.`,
4491
4428
  ..."issues" in audit ? audit.issues.slice(0, 3).map((f) => " " + f.message) : [],
4492
4429
  `Original evidence: ${result.initialReportPath}`,
4493
4430
  "Activation: " + result.activation.status + ".",
4494
4431
  result.next,
4432
+ "If the assistant cannot find mason, restart the desktop app so it picks up PATH.",
4495
4433
  "After the task finishes, run mason-auto status. Configuration alone does not establish activation."
4496
4434
  ].join("\n");
4497
4435
  }
@@ -4501,7 +4439,6 @@ var init_setup = __esm({
4501
4439
  init_evidence();
4502
4440
  init_store();
4503
4441
  init_runtime();
4504
- init_init();
4505
4442
  init_onboarding();
4506
4443
  init_storage();
4507
4444
  init_files2();
@@ -4509,7 +4446,6 @@ var init_setup = __esm({
4509
4446
  init_launcher();
4510
4447
  init_adapters();
4511
4448
  init_model();
4512
- init_runtime2();
4513
4449
  init_status();
4514
4450
  }
4515
4451
  });
@@ -4523,7 +4459,7 @@ init_store();
4523
4459
  import { parseArgs } from "util";
4524
4460
  var USAGE = `Usage: mason-auto <setup|install|config|status|check|hook> [options]
4525
4461
 
4526
- setup [--host claude|codex] Install a pinned runtime, MCP, instructions and hooks; retain the initial audit
4462
+ setup [--host claude|codex] Connect MCP, instructions and hooks to mason on PATH; retain the initial audit
4527
4463
  install --host claude|codex Merge lifecycle hooks into this project's host config
4528
4464
  config --host claude|codex Print the host config without writing
4529
4465
  status Read configured hooks and observed runtime events