mason-context 0.17.0 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -844,7 +844,7 @@ async function readAuditInput(root, file) {
844
844
  if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
845
845
  return value;
846
846
  } catch (error) {
847
- if (error.code === "ENOENT") return null;
847
+ if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return null;
848
848
  throw error;
849
849
  }
850
850
  }
@@ -1094,15 +1094,17 @@ function pathClaimScope(doc, claim) {
1094
1094
  };
1095
1095
  }
1096
1096
  async function pathExists(root, file) {
1097
- const absolute = await auditInputPath(root, file);
1098
1097
  try {
1099
- await fs6.access(absolute);
1098
+ await fs6.access(await auditInputPath(root, file));
1100
1099
  return true;
1101
1100
  } catch (error) {
1102
- if (error.code === "ENOENT") return false;
1101
+ if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return false;
1103
1102
  throw error;
1104
1103
  }
1105
1104
  }
1105
+ function gitMetadataPath(file) {
1106
+ return file.split("/").includes(".git");
1107
+ }
1106
1108
  function optionalMasonPath(file) {
1107
1109
  return file.split("/").includes(".mason");
1108
1110
  }
@@ -1341,6 +1343,14 @@ async function checkDeletedReferences(ctx) {
1341
1343
  continue;
1342
1344
  }
1343
1345
  if (scope.candidates.some(optionalMasonPath)) continue;
1346
+ if (scope.candidates.some(gitMetadataPath)) {
1347
+ result.skipped.push({
1348
+ check: "deleted-reference",
1349
+ doc: doc.path,
1350
+ reason: `Reference ${claim.path} names Git metadata, whose layout varies between checkouts; it was not verified as a repository file.`
1351
+ });
1352
+ continue;
1353
+ }
1344
1354
  if ((await Promise.all(scope.candidates.map((file) => pathExists(ctx.root, file)))).some(Boolean)) continue;
1345
1355
  const candidates = await Promise.all(scope.candidates.map(async (file) => ({
1346
1356
  file,
@@ -2860,14 +2870,6 @@ var init_repair = __esm({
2860
2870
  });
2861
2871
 
2862
2872
  // src/automation/evidence.ts
2863
- var evidence_exports = {};
2864
- __export(evidence_exports, {
2865
- checkCache: () => checkCache,
2866
- git: () => git3,
2867
- hash: () => hash,
2868
- readInputs: () => readInputs,
2869
- workspace: () => workspace
2870
- });
2871
2873
  import fs13 from "fs/promises";
2872
2874
  import path20 from "path";
2873
2875
  import { createHash as createHash3 } from "crypto";
@@ -2913,6 +2915,7 @@ async function readInputs(root) {
2913
2915
  docContents.push([file, text2]);
2914
2916
  for (const claim of text2 ? extractClaims(text2).paths : []) {
2915
2917
  const scope = pathClaimScope(file, claim);
2918
+ if (scope?.candidates.some(gitMetadataPath)) continue;
2916
2919
  for (const candidate of scope?.candidates ?? []) {
2917
2920
  if (optionalMasonPath(candidate)) continue;
2918
2921
  claims.push([candidate, await pathExists(root, candidate), await pathExists(root, path20.posix.dirname(candidate))]);
@@ -2993,7 +2996,7 @@ var init_evidence = __esm({
2993
2996
  init_storage();
2994
2997
  init_advisory_review();
2995
2998
  exec7 = promisify9(execFile9);
2996
- engineVersion = true ? "0.17.0" : "development";
2999
+ engineVersion = true ? "0.17.1" : "development";
2997
3000
  hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
2998
3001
  content = readAuditInput;
2999
3002
  cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
@@ -3514,6 +3517,39 @@ var init_runtime = __esm({
3514
3517
  }
3515
3518
  });
3516
3519
 
3520
+ // src/automation/hook-root.ts
3521
+ import fs15 from "fs/promises";
3522
+ import path21 from "path";
3523
+ async function repositoryRoot(dir) {
3524
+ if (!path21.isAbsolute(dir)) throw new Error("Hook input working directory must be absolute.");
3525
+ try {
3526
+ return await fs15.realpath((await git3(dir, "rev-parse", "--show-toplevel")).trim());
3527
+ } catch (error) {
3528
+ if (error.code === "ENOENT" || /not a git repository/.test(String(error.stderr))) return null;
3529
+ throw error;
3530
+ }
3531
+ }
3532
+ async function resolveHookRoot(host, cwd, options = {}) {
3533
+ const current = await repositoryRoot(cwd);
3534
+ if (current) return current;
3535
+ const projectDir = options.projectDir ?? (host === "claude" ? process.env.CLAUDE_PROJECT_DIR : void 0);
3536
+ const projectRoot = projectDir ? await repositoryRoot(projectDir) : null;
3537
+ if (projectDir && !projectRoot) throw new Error("Hook input is outside Git and the host project is no longer a Git repository. Restore its history before running mason check.");
3538
+ const launchRoot = await repositoryRoot(options.launchCwd ?? process.cwd());
3539
+ const roots = [...new Set([launchRoot, projectRoot].filter((root) => root !== null))];
3540
+ if (!roots.length) throw new Error("Hook input is outside a Git repository and no project root is available. Return to the project and run mason check.");
3541
+ if (roots.length > 1) throw new Error("Hook input is outside a Git repository and project roots disagree. Return to the intended checkout and run mason check; verification was not established.");
3542
+ const worktrees = (await git3(roots[0], "worktree", "list", "--porcelain", "-z")).split("\0").filter((field) => field.startsWith("worktree "));
3543
+ if (worktrees.length !== 1) throw new Error("Hook input is outside a Git repository with multiple worktrees. Return to the intended worktree and run mason check; the active checkout is unknown.");
3544
+ return roots[0];
3545
+ }
3546
+ var init_hook_root = __esm({
3547
+ "src/automation/hook-root.ts"() {
3548
+ "use strict";
3549
+ init_evidence();
3550
+ }
3551
+ });
3552
+
3517
3553
  // src/setup/model.ts
3518
3554
  import { z as z11 } from "zod";
3519
3555
  async function loadSetup(root) {
@@ -3559,9 +3595,19 @@ var init_model = __esm({
3559
3595
  });
3560
3596
 
3561
3597
  // src/setup/launcher.ts
3598
+ var launcher_exports = {};
3599
+ __export(launcher_exports, {
3600
+ executingVersion: () => executingVersion,
3601
+ globalCommand: () => globalCommand,
3602
+ hookCommand: () => hookCommand,
3603
+ inactiveSetupMessage: () => inactiveSetupMessage,
3604
+ installedCommand: () => installedCommand,
3605
+ mcpCommand: () => mcpCommand,
3606
+ prepareLaunch: () => prepareLaunch
3607
+ });
3562
3608
  import { execFile as execFile10 } from "child_process";
3563
3609
  import { promisify as promisify10 } from "util";
3564
- import fs15 from "fs/promises";
3610
+ import fs16 from "fs/promises";
3565
3611
  function globalCommand(args, platform = process.platform) {
3566
3612
  return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args] } : { command: "mason", args };
3567
3613
  }
@@ -3580,13 +3626,31 @@ async function installedCommand() {
3580
3626
  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." };
3581
3627
  }
3582
3628
  }
3629
+ async function prepareLaunch(host, options = {}) {
3630
+ const root = await fs16.realpath((await git3(options.dir ?? process.cwd(), "rev-parse", "--show-toplevel")).trim());
3631
+ const entry = (await loadSetup(root))?.hosts[host];
3632
+ process.env.MASON_SETUP_ROOT = root;
3633
+ process.env.MASON_SETUP_HOST = host;
3634
+ delete process.env.MASON_SETUP_REVISION;
3635
+ if (!entry) {
3636
+ if (options.allowInactive) return false;
3637
+ throw new Error(`Mason is not configured locally for ${host}. Run mason setup --host ${host} in this checkout.`);
3638
+ }
3639
+ process.env.MASON_SETUP_REVISION = entry.revision;
3640
+ return true;
3641
+ }
3642
+ function inactiveSetupMessage() {
3643
+ const host = process.env.MASON_SETUP_HOST;
3644
+ if (!process.env.MASON_SETUP_ROOT || process.env.MASON_SETUP_REVISION || host !== "claude" && host !== "codex") return null;
3645
+ return `Mason MCP is available, but automatic hooks are inactive in ${process.env.MASON_SETUP_ROOT}. Run mason setup --host ${host} in that checkout, review the host configuration, then restart the assistant. Setup has not been performed automatically.`;
3646
+ }
3583
3647
  var executingVersion, mcpCommand, hookCommand;
3584
3648
  var init_launcher = __esm({
3585
3649
  "src/setup/launcher.ts"() {
3586
3650
  "use strict";
3587
3651
  init_evidence();
3588
3652
  init_model();
3589
- executingVersion = () => true ? "0.17.0" : "development";
3653
+ executingVersion = () => true ? "0.17.1" : "development";
3590
3654
  mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
3591
3655
  hookCommand = (host) => `mason --setup-host ${host} auto`;
3592
3656
  }
@@ -3599,7 +3663,7 @@ __export(observations_exports, {
3599
3663
  observeActivation: () => observeActivation,
3600
3664
  readObservation: () => readObservation
3601
3665
  });
3602
- import fs16 from "fs/promises";
3666
+ import fs17 from "fs/promises";
3603
3667
  import { z as z12 } from "zod";
3604
3668
  function observationPath(directory, host, revision, version = executingVersion()) {
3605
3669
  return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
@@ -3613,11 +3677,17 @@ async function readObservation(root, directory, host, revision, version = execut
3613
3677
  }
3614
3678
  async function observeActivation(dir, event, options = {}) {
3615
3679
  const host = process.env.MASON_SETUP_HOST, revision = process.env.MASON_SETUP_REVISION;
3616
- if (host !== "codex" && host !== "claude" || !revision || !process.env.MASON_SETUP_ROOT) return null;
3680
+ if (host !== "codex" && host !== "claude" || !process.env.MASON_SETUP_ROOT) return null;
3617
3681
  try {
3682
+ if (!revision) {
3683
+ if ((await loadSetup(process.env.MASON_SETUP_ROOT))?.hosts[host]) {
3684
+ return "Mason setup is now present. Restart the assistant to connect this MCP session to the configured integration; activation has not been established by this session.";
3685
+ }
3686
+ return inactiveSetupMessage();
3687
+ }
3618
3688
  const capturedDirectory = options.directory?.match(/^\.mason\/reports\/automation\/[a-f0-9]{24}$/)?.[0] ?? options.reportPath?.match(/^(\.mason\/reports\/automation\/[a-f0-9]{24})\/checks\//)?.[1];
3619
- const ws = capturedDirectory ? { root: await fs16.realpath(dir), directory: capturedDirectory } : await workspace(dir);
3620
- if (ws.root !== await fs16.realpath(process.env.MASON_SETUP_ROOT)) return null;
3689
+ const ws = capturedDirectory ? { root: await fs17.realpath(dir), directory: capturedDirectory } : await workspace(dir);
3690
+ if (ws.root !== await fs17.realpath(process.env.MASON_SETUP_ROOT)) return null;
3621
3691
  const setup = await loadSetup(ws.root);
3622
3692
  if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
3623
3693
  const directory = ws.directory + "/activation";
@@ -3698,7 +3768,7 @@ function normalizeHook(host, raw) {
3698
3768
  stopHookActive: input2.stop_hook_active || input2.permission_mode === "plan"
3699
3769
  } };
3700
3770
  }
3701
- async function runAutomationHook(host, stdin) {
3771
+ async function runAutomationHook(host, stdin, options = {}) {
3702
3772
  let name = "";
3703
3773
  let hook;
3704
3774
  try {
@@ -3712,6 +3782,11 @@ async function runAutomationHook(host, stdin) {
3712
3782
  const input2 = normalizeHook(host, raw);
3713
3783
  hook = input2;
3714
3784
  name = input2.name;
3785
+ if (options.managed) {
3786
+ input2.cwd = await resolveHookRoot(host, input2.cwd);
3787
+ const { prepareLaunch: prepareLaunch2 } = await Promise.resolve().then(() => (init_launcher(), launcher_exports));
3788
+ if (!await prepareLaunch2(host, { dir: input2.cwd, allowInactive: true })) return null;
3789
+ }
3715
3790
  if (["before_tool", "after_tool"].includes(input2.event.event) && input2.readOnly) {
3716
3791
  const observed = await observeReadOnlyTool(input2.cwd, input2.event);
3717
3792
  const { observeActivation: observeActivation3 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
@@ -3744,6 +3819,9 @@ function managedHookCommand(host, platform = process.platform) {
3744
3819
  function knownManagedHookCommands(host) {
3745
3820
  return [managedHookCommand(host, "linux"), managedHookCommand(host, "win32"), `mason --setup-host ${host} auto hook --host ${host}`];
3746
3821
  }
3822
+ function compatibleManagedHookCommands(host, platform = process.platform) {
3823
+ return [managedHookCommand(host, platform), `mason --setup-host ${host} auto hook --host ${host}`];
3824
+ }
3747
3825
  function hookConfig(host, command = "npx --no-install --package mason-context mason-auto") {
3748
3826
  const handler = { type: "command", command: command === `mason --setup-host ${host} auto` ? managedHookCommand(host) : command + " hook --host " + host, timeout: 30 };
3749
3827
  return { hooks: Object.fromEntries(HOOK_EVENTS.map((name) => [
@@ -3758,6 +3836,7 @@ var init_adapters = __esm({
3758
3836
  init_execution();
3759
3837
  init_runtime();
3760
3838
  init_store();
3839
+ init_hook_root();
3761
3840
  inputSchema = z13.object({
3762
3841
  cwd: z13.string().min(1),
3763
3842
  session_id: z13.string().min(1).max(500),
@@ -3786,8 +3865,8 @@ __export(files_exports, {
3786
3865
  managedBlock: () => managedBlock,
3787
3866
  readText: () => readText
3788
3867
  });
3789
- import fs17 from "fs/promises";
3790
- import path21 from "path";
3868
+ import fs18 from "fs/promises";
3869
+ import path22 from "path";
3791
3870
  import { randomUUID as randomUUID5 } from "crypto";
3792
3871
  async function readText(root, file) {
3793
3872
  try {
@@ -3816,14 +3895,14 @@ async function applyEdit(root, edit) {
3816
3895
  if (edit.after === null) {
3817
3896
  const file2 = await storePath(root, edit.path);
3818
3897
  if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
3819
- await fs17.unlink(file2);
3898
+ await fs18.unlink(file2);
3820
3899
  return true;
3821
3900
  }
3822
3901
  const file = await storePath(root, edit.path, true);
3823
- const temporary = path21.join(path21.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
3902
+ const temporary = path22.join(path22.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
3824
3903
  try {
3825
- const mode = await fs17.stat(file).then((s) => s.mode & 511, () => 384);
3826
- const handle = await fs17.open(temporary, "wx", mode);
3904
+ const mode = await fs18.stat(file).then((s) => s.mode & 511, () => 384);
3905
+ const handle = await fs18.open(temporary, "wx", mode);
3827
3906
  try {
3828
3907
  await handle.writeFile(edit.after, "utf8");
3829
3908
  await handle.sync();
@@ -3831,10 +3910,10 @@ async function applyEdit(root, edit) {
3831
3910
  await handle.close();
3832
3911
  }
3833
3912
  if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
3834
- await fs17.rename(temporary, file);
3913
+ await fs18.rename(temporary, file);
3835
3914
  return true;
3836
3915
  } finally {
3837
- await fs17.rm(temporary, { force: true });
3916
+ await fs18.rm(temporary, { force: true });
3838
3917
  }
3839
3918
  }
3840
3919
  var init_files2 = __esm({
@@ -4003,29 +4082,6 @@ If a build is interrupted, its partials remain in \`.mason/partial-snapshots/\`.
4003
4082
  }
4004
4083
  });
4005
4084
 
4006
- // src/utils/git.ts
4007
- var git_exports = {};
4008
- __export(git_exports, {
4009
- isGitRepo: () => isGitRepo
4010
- });
4011
- import { execFile as execFile11 } from "child_process";
4012
- import { promisify as promisify11 } from "util";
4013
- async function isGitRepo(dir) {
4014
- try {
4015
- await exec8("git", ["rev-parse", "--git-dir"], { cwd: dir });
4016
- return true;
4017
- } catch {
4018
- return false;
4019
- }
4020
- }
4021
- var exec8;
4022
- var init_git2 = __esm({
4023
- "src/utils/git.ts"() {
4024
- "use strict";
4025
- exec8 = promisify11(execFile11);
4026
- }
4027
- });
4028
-
4029
4085
  // src/setup/config.ts
4030
4086
  var config_exports = {};
4031
4087
  __export(config_exports, {
@@ -4070,7 +4126,7 @@ async function instructionEdits(root, host) {
4070
4126
  function managedMcp(previous, host) {
4071
4127
  const options = previous === void 0 ? {} : object.parse(previous);
4072
4128
  for (const key of ["command", "args", "url", "type", "headers", "http_headers", "env_http_headers", "bearer_token_env_var"]) delete options[key];
4073
- return { ...options, ...mcpCommand(host) };
4129
+ return { ...options, ...host === "claude" ? { type: "stdio" } : {}, ...mcpCommand(host) };
4074
4130
  }
4075
4131
  async function mcpEdit(root, host) {
4076
4132
  const file = mcpPath(host), before = await readText(root, file);
@@ -4107,17 +4163,9 @@ async function hookEdits(root, host) {
4107
4163
  }
4108
4164
  async function ancillaryEdits(root) {
4109
4165
  const ignore = await readText(root, ".gitignore");
4110
- const { git: git4 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
4111
- const { isGitRepo: isGitRepo2 } = await Promise.resolve().then(() => (init_git2(), git_exports));
4112
- let parentIgnored = false;
4113
- try {
4114
- if (await isGitRepo2(root)) parentIgnored = !!(await git4(root, "check-ignore", "--no-index", ".mason")).trim();
4115
- } catch (error) {
4116
- if (error.code !== 1) throw error;
4117
- }
4118
- const retainParentRule = parentIgnored || !!ignore?.replace(/\r\n/g, "\n").includes("# mason:ignore:start\n!/.mason/\n/.mason/*");
4119
4166
  const rules = [
4120
- ...retainParentRule ? ["!/.mason/", "/.mason/*"] : [],
4167
+ "!/.mason/",
4168
+ "/.mason/*",
4121
4169
  "!/.mason/decisions/",
4122
4170
  "!/.mason/decisions/**",
4123
4171
  "!/.mason/reviews/",
@@ -4135,13 +4183,14 @@ async function inspectHostConfig(root, host, plannedMcp) {
4135
4183
  const servers = host === "codex" ? config.mcp_servers : config.mcpServers;
4136
4184
  const hooks = automationConfigSchema.parse(JSON.parse(await readText(root, configPath(host)) ?? "{}"));
4137
4185
  const expected = hookConfig(host, hookCommand(host));
4186
+ const compatible = compatibleManagedHookCommands(host);
4138
4187
  const mcp = servers?.mason === void 0 ? null : object.parse(servers.mason);
4139
4188
  return {
4140
4189
  mcp,
4141
4190
  mcpDisabled: mcp?.enabled === false,
4142
4191
  hooks: Object.fromEntries(Object.keys(expected.hooks).map((event) => [
4143
4192
  event,
4144
- (hooks.hooks?.[event] ?? []).flatMap((group) => group.hooks.filter((handler) => handler.command === expected.hooks.SessionStart[0].hooks[0].command).map((handler) => ({ ...group, hooks: [handler] })) ?? [])
4193
+ (hooks.hooks?.[event] ?? []).flatMap((group) => group.hooks.filter((handler) => compatible.includes(handler.command ?? "")).map((handler) => ({ ...group, hooks: [handler] })) ?? [])
4145
4194
  ])),
4146
4195
  disabled: hooks.disableAllHooks === true || config.features?.hooks === false || config.features?.codex_hooks === false
4147
4196
  };
@@ -4331,8 +4380,12 @@ async function planTeardown(root, selected) {
4331
4380
  if (host === "codex" && BLOCKS[2].some((marker) => before.includes(marker))) throw new Error("The marked Mason MCP entry was removed or renamed; inspect the retained block manually.");
4332
4381
  return;
4333
4382
  }
4334
- const expected = ownership.files[mcpFile]?.mcpHash ?? hash(mcpCommand(host));
4335
- if (hash(servers.mason) !== expected) throw new Error("The Mason MCP entry was edited or its ownership is uncertain; retained for manual cleanup.");
4383
+ const ownedHash = ownership.files[mcpFile]?.mcpHash;
4384
+ const expected = ownedHash ? [ownedHash] : [
4385
+ hash(mcpCommand(host)),
4386
+ ...host === "claude" ? [hash({ type: "stdio", ...mcpCommand(host) })] : []
4387
+ ];
4388
+ if (!expected.includes(hash(servers.mason))) throw new Error("The Mason MCP entry was edited or its ownership is uncertain; retained for manual cleanup.");
4336
4389
  delete servers.mason;
4337
4390
  const desired = { ...config, [key]: servers };
4338
4391
  if (!Object.keys(servers).length) delete desired[key];
@@ -4499,13 +4552,13 @@ var init_teardown = __esm({
4499
4552
  });
4500
4553
 
4501
4554
  // src/review/cochange.ts
4502
- import { execFile as execFile12 } from "child_process";
4503
- import { promisify as promisify12 } from "util";
4555
+ import { execFile as execFile11 } from "child_process";
4556
+ import { promisify as promisify11 } from "util";
4504
4557
  async function buildMatrix(resolvedRoot) {
4505
4558
  try {
4506
- const { stdout: shallow } = await exec9("git", ["rev-parse", "--is-shallow-repository"], { cwd: resolvedRoot });
4559
+ const { stdout: shallow } = await exec8("git", ["rev-parse", "--is-shallow-repository"], { cwd: resolvedRoot });
4507
4560
  if (shallow.trim() === "true") return null;
4508
- const { stdout } = await exec9(
4561
+ const { stdout } = await exec8(
4509
4562
  "git",
4510
4563
  ["log", `-n${HISTORY_COMMITS}`, "--format=%x01", "--name-only", "-M"],
4511
4564
  { cwd: resolvedRoot, maxBuffer: 100 * 1024 * 1024 }
@@ -4561,11 +4614,11 @@ async function findMissingPartners(resolvedRoot, changedFiles, existsOnDisk, all
4561
4614
  findings.sort((a, b) => b.rate - a.rate || b.sharedCommits - a.sharedCommits);
4562
4615
  return findings;
4563
4616
  }
4564
- var exec9, HISTORY_COMMITS, MIN_FILE_COMMITS, MIN_SHARED_COMMITS, MIN_COCHANGE_RATE, MAX_COMMIT_FILES;
4617
+ var exec8, HISTORY_COMMITS, MIN_FILE_COMMITS, MIN_SHARED_COMMITS, MIN_COCHANGE_RATE, MAX_COMMIT_FILES;
4565
4618
  var init_cochange = __esm({
4566
4619
  "src/review/cochange.ts"() {
4567
4620
  "use strict";
4568
- exec9 = promisify12(execFile12);
4621
+ exec8 = promisify11(execFile11);
4569
4622
  HISTORY_COMMITS = 1500;
4570
4623
  MIN_FILE_COMMITS = 5;
4571
4624
  MIN_SHARED_COMMITS = 4;
@@ -4873,10 +4926,10 @@ var init_check = __esm({
4873
4926
  });
4874
4927
 
4875
4928
  // src/review/evidence.ts
4876
- import path22 from "path";
4929
+ import path23 from "path";
4877
4930
  import { z as z22 } from "zod";
4878
4931
  function relativeArtifact(root, file) {
4879
- const relative = path22.isAbsolute(file) ? path22.relative(root, file) : file;
4932
+ const relative = path23.isAbsolute(file) ? path23.relative(root, file) : file;
4880
4933
  const normalized = normalizeRepoPath(relative);
4881
4934
  if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
4882
4935
  return normalized;
@@ -4973,7 +5026,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
4973
5026
  const report = await readStoreJson(root, file);
4974
5027
  if (report === null) throw new Error(`Report artifact is missing: ${file}`);
4975
5028
  const sourceRoot = check.sourceRoot ?? root;
4976
- if (!path22.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
5029
+ if (!path23.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
4977
5030
  const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : check.report.format === "mason-check-json" ? parseCheck(report, sourceRoot) : parseSarif(report, sourceRoot);
4978
5031
  result.outcome = parsed.outcome;
4979
5032
  result.counts = parsed.counts;
@@ -5094,13 +5147,13 @@ var init_evidence2 = __esm({
5094
5147
  });
5095
5148
 
5096
5149
  // src/review/review.ts
5097
- import fs18 from "fs/promises";
5098
- import path23 from "path";
5099
- import { execFile as execFile13 } from "child_process";
5100
- import { promisify as promisify13 } from "util";
5150
+ import fs19 from "fs/promises";
5151
+ import path24 from "path";
5152
+ import { execFile as execFile12 } from "child_process";
5153
+ import { promisify as promisify12 } from "util";
5101
5154
  async function resolveMergeBase(resolvedRoot, base, head) {
5102
5155
  try {
5103
- const { stdout } = await exec10("git", ["merge-base", base, head], {
5156
+ const { stdout } = await exec9("git", ["merge-base", base, head], {
5104
5157
  cwd: resolvedRoot
5105
5158
  });
5106
5159
  return stdout.trim() || null;
@@ -5111,7 +5164,7 @@ async function resolveMergeBase(resolvedRoot, base, head) {
5111
5164
  async function defaultBase(resolvedRoot) {
5112
5165
  for (const ref of ["origin/HEAD", "origin/main", "origin/master", "main"]) {
5113
5166
  try {
5114
- await exec10("git", ["rev-parse", "--verify", "--quiet", ref], {
5167
+ await exec9("git", ["rev-parse", "--verify", "--quiet", ref], {
5115
5168
  cwd: resolvedRoot
5116
5169
  });
5117
5170
  return ref;
@@ -5124,7 +5177,7 @@ function anchorsTouched(record, changedFiles) {
5124
5177
  return matchingPaths(record.files, changedFiles);
5125
5178
  }
5126
5179
  async function computeReview(rootDir, base, options = {}) {
5127
- const resolvedRoot = path23.resolve(rootDir);
5180
+ const resolvedRoot = path24.resolve(rootDir);
5128
5181
  const head = await getCurrentGitHash(resolvedRoot);
5129
5182
  if (head === "unknown") return null;
5130
5183
  const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
@@ -5174,7 +5227,7 @@ async function computeReview(rootDir, base, options = {}) {
5174
5227
  analyzed,
5175
5228
  async (relPath) => {
5176
5229
  try {
5177
- await fs18.access(path23.join(resolvedRoot, relPath));
5230
+ await fs19.access(path24.join(resolvedRoot, relPath));
5178
5231
  return true;
5179
5232
  } catch {
5180
5233
  return false;
@@ -5207,7 +5260,7 @@ async function computeReview(rootDir, base, options = {}) {
5207
5260
  }
5208
5261
  return finalize();
5209
5262
  }
5210
- var exec10, MAX_ANALYZED_FILES;
5263
+ var exec9, MAX_ANALYZED_FILES;
5211
5264
  var init_review = __esm({
5212
5265
  "src/review/review.ts"() {
5213
5266
  "use strict";
@@ -5219,7 +5272,7 @@ var init_review = __esm({
5219
5272
  init_cochange();
5220
5273
  init_evidence2();
5221
5274
  init_snapshot();
5222
- exec10 = promisify13(execFile13);
5275
+ exec9 = promisify12(execFile12);
5223
5276
  MAX_ANALYZED_FILES = 50;
5224
5277
  }
5225
5278
  });
@@ -5341,7 +5394,16 @@ async function setupStatus(dir) {
5341
5394
  const instructionsCurrent = instructions.every((edit) => edit.before === edit.after);
5342
5395
  const config = await inspectHostConfig(ws.root, host, void 0);
5343
5396
  const mcp = !config.mcpDisabled && hash(config.mcp) === entry.mcpFingerprint && isDeepStrictEqual3({ command: config.mcp?.command, args: config.mcp?.args }, mcpCommand(host));
5344
- const hooks = isDeepStrictEqual3(config.hooks, hookConfig(host, hookCommand(host)).hooks) && !config.disabled;
5397
+ const expectedHooks = hookConfig(host, hookCommand(host)).hooks;
5398
+ const currentHooks = isDeepStrictEqual3(config.hooks, expectedHooks);
5399
+ const compatibleHooks = Object.fromEntries(Object.entries(config.hooks).map(([event, groups]) => [
5400
+ event,
5401
+ groups.map((group) => ({ ...group, hooks: group.hooks.map((handler) => ({
5402
+ ...handler,
5403
+ command: expectedHooks.SessionStart[0].hooks[0].command
5404
+ })) }))
5405
+ ]));
5406
+ const hooks = isDeepStrictEqual3(compatibleHooks, expectedHooks) && !config.disabled;
5345
5407
  const local = await loadSetupReceipt(ws.root, ws.directory, host);
5346
5408
  const configured = local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
5347
5409
  const observation = await readObservation(ws.root, ws.directory, host, entry.revision, installed.version ?? "unavailable");
@@ -5360,11 +5422,12 @@ async function setupStatus(dir) {
5360
5422
  runtime: installed.available ? "global (" + installed.version + ")" : "unavailable-on-path",
5361
5423
  mcp: mcp ? "configured" : "changed",
5362
5424
  instructions: instructionsCurrent ? "current" : "changed",
5363
- hookConfiguration: hooks ? "configured" : "disabled-or-changed",
5425
+ hookConfiguration: hooks ? currentHooks ? "configured" : "compatible" : "disabled-or-changed",
5364
5426
  observedEvents,
5365
5427
  contextCalls: observation?.contextCalls ?? 0,
5366
5428
  verificationStatus: automation.status === "current" ? automation.verificationStatus ?? "unavailable" : "unavailable",
5367
- pending
5429
+ pending,
5430
+ updates: hooks && !currentHooks ? ["A newer hook template is available. Run mason setup --host " + host + " to update it when convenient; review and commit the configuration changes."] : []
5368
5431
  };
5369
5432
  }
5370
5433
  const statuses = Object.values(hosts2).map((host) => host.status);
@@ -5389,6 +5452,7 @@ function summarizeActivation(status) {
5389
5452
  ` Task context requests: ${state.contextCalls}; verification: ${state.verificationStatus}.`,
5390
5453
  ...state.pending.map((message2) => " Next: " + message2)
5391
5454
  );
5455
+ lines.push(...state.updates.map((message2) => " Optional: " + message2));
5392
5456
  }
5393
5457
  if ("decisionRecords" in status) lines.push(`Decision records: ${status.decisionRecords}.`);
5394
5458
  if (status.next) lines.push(status.next);
@@ -5594,7 +5658,7 @@ function isHookCommand(argv2) {
5594
5658
  async function runAutomationCli(argv2, stdin = "", io = {
5595
5659
  out: (s) => process.stdout.write(s + "\n"),
5596
5660
  err: (s) => process.stderr.write(s + "\n")
5597
- }) {
5661
+ }, options = {}) {
5598
5662
  let action = "";
5599
5663
  try {
5600
5664
  const { values, positionals } = parseCli(argv2);
@@ -5614,7 +5678,9 @@ async function runAutomationCli(argv2, stdin = "", io = {
5614
5678
  return result.status === "complete" ? 0 : 2;
5615
5679
  }
5616
5680
  if (action === "hook") {
5617
- const output = await runAutomationHook(hostSchema.parse(values.host), stdin);
5681
+ const host = hostSchema.parse(values.host);
5682
+ if (options.managedHost && options.managedHost !== host) throw new Error("Managed hook --host does not match its launcher.");
5683
+ const output = await runAutomationHook(host, stdin, { managed: !!options.managedHost });
5618
5684
  if (output) io.out(JSON.stringify(output));
5619
5685
  return 0;
5620
5686
  }