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.
package/dist/mason.js CHANGED
@@ -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) {
@@ -3564,13 +3600,14 @@ __export(launcher_exports, {
3564
3600
  executingVersion: () => executingVersion,
3565
3601
  globalCommand: () => globalCommand,
3566
3602
  hookCommand: () => hookCommand,
3603
+ inactiveSetupMessage: () => inactiveSetupMessage,
3567
3604
  installedCommand: () => installedCommand,
3568
3605
  mcpCommand: () => mcpCommand,
3569
3606
  prepareLaunch: () => prepareLaunch
3570
3607
  });
3571
3608
  import { execFile as execFile10 } from "child_process";
3572
3609
  import { promisify as promisify10 } from "util";
3573
- import fs15 from "fs/promises";
3610
+ import fs16 from "fs/promises";
3574
3611
  function globalCommand(args2, platform = process.platform) {
3575
3612
  return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args2] } : { command: "mason", args: args2 };
3576
3613
  }
@@ -3590,24 +3627,30 @@ async function installedCommand() {
3590
3627
  }
3591
3628
  }
3592
3629
  async function prepareLaunch(host, options = {}) {
3593
- const root = await fs15.realpath((await git3(process.cwd(), "rev-parse", "--show-toplevel")).trim());
3630
+ const root = await fs16.realpath((await git3(options.dir ?? process.cwd(), "rev-parse", "--show-toplevel")).trim());
3594
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;
3595
3635
  if (!entry) {
3596
3636
  if (options.allowInactive) return false;
3597
3637
  throw new Error(`Mason is not configured locally for ${host}. Run mason setup --host ${host} in this checkout.`);
3598
3638
  }
3599
- process.env.MASON_SETUP_ROOT = root;
3600
- process.env.MASON_SETUP_HOST = host;
3601
3639
  process.env.MASON_SETUP_REVISION = entry.revision;
3602
3640
  return true;
3603
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
+ }
3604
3647
  var executingVersion, mcpCommand, hookCommand;
3605
3648
  var init_launcher = __esm({
3606
3649
  "src/setup/launcher.ts"() {
3607
3650
  "use strict";
3608
3651
  init_evidence();
3609
3652
  init_model();
3610
- executingVersion = () => true ? "0.17.0" : "development";
3653
+ executingVersion = () => true ? "0.17.1" : "development";
3611
3654
  mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
3612
3655
  hookCommand = (host) => `mason --setup-host ${host} auto`;
3613
3656
  }
@@ -3620,7 +3663,7 @@ __export(observations_exports, {
3620
3663
  observeActivation: () => observeActivation,
3621
3664
  readObservation: () => readObservation
3622
3665
  });
3623
- import fs16 from "fs/promises";
3666
+ import fs17 from "fs/promises";
3624
3667
  import { z as z12 } from "zod";
3625
3668
  function observationPath(directory, host, revision, version = executingVersion()) {
3626
3669
  return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
@@ -3634,11 +3677,17 @@ async function readObservation(root, directory, host, revision, version = execut
3634
3677
  }
3635
3678
  async function observeActivation(dir, event, options = {}) {
3636
3679
  const host = process.env.MASON_SETUP_HOST, revision = process.env.MASON_SETUP_REVISION;
3637
- 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;
3638
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
+ }
3639
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];
3640
- const ws = capturedDirectory ? { root: await fs16.realpath(dir), directory: capturedDirectory } : await workspace(dir);
3641
- 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;
3642
3691
  const setup = await loadSetup(ws.root);
3643
3692
  if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
3644
3693
  const directory = ws.directory + "/activation";
@@ -3719,7 +3768,7 @@ function normalizeHook(host, raw) {
3719
3768
  stopHookActive: input.stop_hook_active || input.permission_mode === "plan"
3720
3769
  } };
3721
3770
  }
3722
- async function runAutomationHook(host, stdin) {
3771
+ async function runAutomationHook(host, stdin, options = {}) {
3723
3772
  let name = "";
3724
3773
  let hook;
3725
3774
  try {
@@ -3733,6 +3782,11 @@ async function runAutomationHook(host, stdin) {
3733
3782
  const input = normalizeHook(host, raw);
3734
3783
  hook = input;
3735
3784
  name = input.name;
3785
+ if (options.managed) {
3786
+ input.cwd = await resolveHookRoot(host, input.cwd);
3787
+ const { prepareLaunch: prepareLaunch2 } = await Promise.resolve().then(() => (init_launcher(), launcher_exports));
3788
+ if (!await prepareLaunch2(host, { dir: input.cwd, allowInactive: true })) return null;
3789
+ }
3736
3790
  if (["before_tool", "after_tool"].includes(input.event.event) && input.readOnly) {
3737
3791
  const observed = await observeReadOnlyTool(input.cwd, input.event);
3738
3792
  const { observeActivation: observeActivation3 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
@@ -3765,6 +3819,9 @@ function managedHookCommand(host, platform = process.platform) {
3765
3819
  function knownManagedHookCommands(host) {
3766
3820
  return [managedHookCommand(host, "linux"), managedHookCommand(host, "win32"), `mason --setup-host ${host} auto hook --host ${host}`];
3767
3821
  }
3822
+ function compatibleManagedHookCommands(host, platform = process.platform) {
3823
+ return [managedHookCommand(host, platform), `mason --setup-host ${host} auto hook --host ${host}`];
3824
+ }
3768
3825
  function hookConfig(host, command2 = "npx --no-install --package mason-context mason-auto") {
3769
3826
  const handler = { type: "command", command: command2 === `mason --setup-host ${host} auto` ? managedHookCommand(host) : command2 + " hook --host " + host, timeout: 30 };
3770
3827
  return { hooks: Object.fromEntries(HOOK_EVENTS.map((name) => [
@@ -3779,6 +3836,7 @@ var init_adapters = __esm({
3779
3836
  init_execution();
3780
3837
  init_runtime();
3781
3838
  init_store();
3839
+ init_hook_root();
3782
3840
  inputSchema = z13.object({
3783
3841
  cwd: z13.string().min(1),
3784
3842
  session_id: z13.string().min(1).max(500),
@@ -3807,8 +3865,8 @@ __export(files_exports, {
3807
3865
  managedBlock: () => managedBlock,
3808
3866
  readText: () => readText
3809
3867
  });
3810
- import fs17 from "fs/promises";
3811
- import path21 from "path";
3868
+ import fs18 from "fs/promises";
3869
+ import path22 from "path";
3812
3870
  import { randomUUID as randomUUID5 } from "crypto";
3813
3871
  async function readText(root, file) {
3814
3872
  try {
@@ -3837,14 +3895,14 @@ async function applyEdit(root, edit) {
3837
3895
  if (edit.after === null) {
3838
3896
  const file2 = await storePath(root, edit.path);
3839
3897
  if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
3840
- await fs17.unlink(file2);
3898
+ await fs18.unlink(file2);
3841
3899
  return true;
3842
3900
  }
3843
3901
  const file = await storePath(root, edit.path, true);
3844
- const temporary = path21.join(path21.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
3902
+ const temporary = path22.join(path22.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
3845
3903
  try {
3846
- const mode = await fs17.stat(file).then((s) => s.mode & 511, () => 384);
3847
- 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);
3848
3906
  try {
3849
3907
  await handle.writeFile(edit.after, "utf8");
3850
3908
  await handle.sync();
@@ -3852,10 +3910,10 @@ async function applyEdit(root, edit) {
3852
3910
  await handle.close();
3853
3911
  }
3854
3912
  if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
3855
- await fs17.rename(temporary, file);
3913
+ await fs18.rename(temporary, file);
3856
3914
  return true;
3857
3915
  } finally {
3858
- await fs17.rm(temporary, { force: true });
3916
+ await fs18.rm(temporary, { force: true });
3859
3917
  }
3860
3918
  }
3861
3919
  var init_files2 = __esm({
@@ -4024,29 +4082,6 @@ If a build is interrupted, its partials remain in \`.mason/partial-snapshots/\`.
4024
4082
  }
4025
4083
  });
4026
4084
 
4027
- // src/utils/git.ts
4028
- var git_exports = {};
4029
- __export(git_exports, {
4030
- isGitRepo: () => isGitRepo
4031
- });
4032
- import { execFile as execFile11 } from "child_process";
4033
- import { promisify as promisify11 } from "util";
4034
- async function isGitRepo(dir) {
4035
- try {
4036
- await exec8("git", ["rev-parse", "--git-dir"], { cwd: dir });
4037
- return true;
4038
- } catch {
4039
- return false;
4040
- }
4041
- }
4042
- var exec8;
4043
- var init_git2 = __esm({
4044
- "src/utils/git.ts"() {
4045
- "use strict";
4046
- exec8 = promisify11(execFile11);
4047
- }
4048
- });
4049
-
4050
4085
  // src/setup/config.ts
4051
4086
  var config_exports = {};
4052
4087
  __export(config_exports, {
@@ -4091,7 +4126,7 @@ async function instructionEdits(root, host) {
4091
4126
  function managedMcp(previous, host) {
4092
4127
  const options = previous === void 0 ? {} : object.parse(previous);
4093
4128
  for (const key of ["command", "args", "url", "type", "headers", "http_headers", "env_http_headers", "bearer_token_env_var"]) delete options[key];
4094
- return { ...options, ...mcpCommand(host) };
4129
+ return { ...options, ...host === "claude" ? { type: "stdio" } : {}, ...mcpCommand(host) };
4095
4130
  }
4096
4131
  async function mcpEdit(root, host) {
4097
4132
  const file = mcpPath(host), before = await readText(root, file);
@@ -4128,17 +4163,9 @@ async function hookEdits(root, host) {
4128
4163
  }
4129
4164
  async function ancillaryEdits(root) {
4130
4165
  const ignore = await readText(root, ".gitignore");
4131
- const { git: git4 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
4132
- const { isGitRepo: isGitRepo2 } = await Promise.resolve().then(() => (init_git2(), git_exports));
4133
- let parentIgnored = false;
4134
- try {
4135
- if (await isGitRepo2(root)) parentIgnored = !!(await git4(root, "check-ignore", "--no-index", ".mason")).trim();
4136
- } catch (error) {
4137
- if (error.code !== 1) throw error;
4138
- }
4139
- const retainParentRule = parentIgnored || !!ignore?.replace(/\r\n/g, "\n").includes("# mason:ignore:start\n!/.mason/\n/.mason/*");
4140
4166
  const rules = [
4141
- ...retainParentRule ? ["!/.mason/", "/.mason/*"] : [],
4167
+ "!/.mason/",
4168
+ "/.mason/*",
4142
4169
  "!/.mason/decisions/",
4143
4170
  "!/.mason/decisions/**",
4144
4171
  "!/.mason/reviews/",
@@ -4156,13 +4183,14 @@ async function inspectHostConfig(root, host, plannedMcp) {
4156
4183
  const servers = host === "codex" ? config.mcp_servers : config.mcpServers;
4157
4184
  const hooks = automationConfigSchema.parse(JSON.parse(await readText(root, configPath(host)) ?? "{}"));
4158
4185
  const expected = hookConfig(host, hookCommand(host));
4186
+ const compatible = compatibleManagedHookCommands(host);
4159
4187
  const mcp = servers?.mason === void 0 ? null : object.parse(servers.mason);
4160
4188
  return {
4161
4189
  mcp,
4162
4190
  mcpDisabled: mcp?.enabled === false,
4163
4191
  hooks: Object.fromEntries(Object.keys(expected.hooks).map((event) => [
4164
4192
  event,
4165
- (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] })) ?? [])
4166
4194
  ])),
4167
4195
  disabled: hooks.disableAllHooks === true || config.features?.hooks === false || config.features?.codex_hooks === false
4168
4196
  };
@@ -4352,8 +4380,12 @@ async function planTeardown(root, selected) {
4352
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.");
4353
4381
  return;
4354
4382
  }
4355
- const expected = ownership.files[mcpFile]?.mcpHash ?? hash(mcpCommand(host));
4356
- 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.");
4357
4389
  delete servers.mason;
4358
4390
  const desired = { ...config, [key]: servers };
4359
4391
  if (!Object.keys(servers).length) delete desired[key];
@@ -4520,13 +4552,13 @@ var init_teardown = __esm({
4520
4552
  });
4521
4553
 
4522
4554
  // src/review/cochange.ts
4523
- import { execFile as execFile12 } from "child_process";
4524
- import { promisify as promisify12 } from "util";
4555
+ import { execFile as execFile11 } from "child_process";
4556
+ import { promisify as promisify11 } from "util";
4525
4557
  async function buildMatrix(resolvedRoot) {
4526
4558
  try {
4527
- 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 });
4528
4560
  if (shallow.trim() === "true") return null;
4529
- const { stdout } = await exec9(
4561
+ const { stdout } = await exec8(
4530
4562
  "git",
4531
4563
  ["log", `-n${HISTORY_COMMITS}`, "--format=%x01", "--name-only", "-M"],
4532
4564
  { cwd: resolvedRoot, maxBuffer: 100 * 1024 * 1024 }
@@ -4582,11 +4614,11 @@ async function findMissingPartners(resolvedRoot, changedFiles, existsOnDisk, all
4582
4614
  findings.sort((a, b) => b.rate - a.rate || b.sharedCommits - a.sharedCommits);
4583
4615
  return findings;
4584
4616
  }
4585
- 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;
4586
4618
  var init_cochange = __esm({
4587
4619
  "src/review/cochange.ts"() {
4588
4620
  "use strict";
4589
- exec9 = promisify12(execFile12);
4621
+ exec8 = promisify11(execFile11);
4590
4622
  HISTORY_COMMITS = 1500;
4591
4623
  MIN_FILE_COMMITS = 5;
4592
4624
  MIN_SHARED_COMMITS = 4;
@@ -4894,10 +4926,10 @@ var init_check = __esm({
4894
4926
  });
4895
4927
 
4896
4928
  // src/review/evidence.ts
4897
- import path22 from "path";
4929
+ import path23 from "path";
4898
4930
  import { z as z22 } from "zod";
4899
4931
  function relativeArtifact(root, file) {
4900
- const relative = path22.isAbsolute(file) ? path22.relative(root, file) : file;
4932
+ const relative = path23.isAbsolute(file) ? path23.relative(root, file) : file;
4901
4933
  const normalized = normalizeRepoPath(relative);
4902
4934
  if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
4903
4935
  return normalized;
@@ -4994,7 +5026,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
4994
5026
  const report = await readStoreJson(root, file);
4995
5027
  if (report === null) throw new Error(`Report artifact is missing: ${file}`);
4996
5028
  const sourceRoot = check.sourceRoot ?? root;
4997
- 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.");
4998
5030
  const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : check.report.format === "mason-check-json" ? parseCheck(report, sourceRoot) : parseSarif(report, sourceRoot);
4999
5031
  result.outcome = parsed.outcome;
5000
5032
  result.counts = parsed.counts;
@@ -5115,13 +5147,13 @@ var init_evidence2 = __esm({
5115
5147
  });
5116
5148
 
5117
5149
  // src/review/review.ts
5118
- import fs18 from "fs/promises";
5119
- import path23 from "path";
5120
- import { execFile as execFile13 } from "child_process";
5121
- 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";
5122
5154
  async function resolveMergeBase(resolvedRoot, base, head) {
5123
5155
  try {
5124
- const { stdout } = await exec10("git", ["merge-base", base, head], {
5156
+ const { stdout } = await exec9("git", ["merge-base", base, head], {
5125
5157
  cwd: resolvedRoot
5126
5158
  });
5127
5159
  return stdout.trim() || null;
@@ -5132,7 +5164,7 @@ async function resolveMergeBase(resolvedRoot, base, head) {
5132
5164
  async function defaultBase(resolvedRoot) {
5133
5165
  for (const ref of ["origin/HEAD", "origin/main", "origin/master", "main"]) {
5134
5166
  try {
5135
- await exec10("git", ["rev-parse", "--verify", "--quiet", ref], {
5167
+ await exec9("git", ["rev-parse", "--verify", "--quiet", ref], {
5136
5168
  cwd: resolvedRoot
5137
5169
  });
5138
5170
  return ref;
@@ -5145,7 +5177,7 @@ function anchorsTouched(record, changedFiles) {
5145
5177
  return matchingPaths(record.files, changedFiles);
5146
5178
  }
5147
5179
  async function computeReview(rootDir, base, options = {}) {
5148
- const resolvedRoot = path23.resolve(rootDir);
5180
+ const resolvedRoot = path24.resolve(rootDir);
5149
5181
  const head = await getCurrentGitHash(resolvedRoot);
5150
5182
  if (head === "unknown") return null;
5151
5183
  const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
@@ -5195,7 +5227,7 @@ async function computeReview(rootDir, base, options = {}) {
5195
5227
  analyzed,
5196
5228
  async (relPath) => {
5197
5229
  try {
5198
- await fs18.access(path23.join(resolvedRoot, relPath));
5230
+ await fs19.access(path24.join(resolvedRoot, relPath));
5199
5231
  return true;
5200
5232
  } catch {
5201
5233
  return false;
@@ -5228,7 +5260,7 @@ async function computeReview(rootDir, base, options = {}) {
5228
5260
  }
5229
5261
  return finalize();
5230
5262
  }
5231
- var exec10, MAX_ANALYZED_FILES;
5263
+ var exec9, MAX_ANALYZED_FILES;
5232
5264
  var init_review = __esm({
5233
5265
  "src/review/review.ts"() {
5234
5266
  "use strict";
@@ -5240,7 +5272,7 @@ var init_review = __esm({
5240
5272
  init_cochange();
5241
5273
  init_evidence2();
5242
5274
  init_snapshot();
5243
- exec10 = promisify13(execFile13);
5275
+ exec9 = promisify12(execFile12);
5244
5276
  MAX_ANALYZED_FILES = 50;
5245
5277
  }
5246
5278
  });
@@ -5362,7 +5394,16 @@ async function setupStatus(dir) {
5362
5394
  const instructionsCurrent = instructions.every((edit) => edit.before === edit.after);
5363
5395
  const config = await inspectHostConfig(ws.root, host, void 0);
5364
5396
  const mcp = !config.mcpDisabled && hash(config.mcp) === entry.mcpFingerprint && isDeepStrictEqual3({ command: config.mcp?.command, args: config.mcp?.args }, mcpCommand(host));
5365
- 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;
5366
5407
  const local = await loadSetupReceipt(ws.root, ws.directory, host);
5367
5408
  const configured = local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
5368
5409
  const observation = await readObservation(ws.root, ws.directory, host, entry.revision, installed.version ?? "unavailable");
@@ -5381,11 +5422,12 @@ async function setupStatus(dir) {
5381
5422
  runtime: installed.available ? "global (" + installed.version + ")" : "unavailable-on-path",
5382
5423
  mcp: mcp ? "configured" : "changed",
5383
5424
  instructions: instructionsCurrent ? "current" : "changed",
5384
- hookConfiguration: hooks ? "configured" : "disabled-or-changed",
5425
+ hookConfiguration: hooks ? currentHooks ? "configured" : "compatible" : "disabled-or-changed",
5385
5426
  observedEvents,
5386
5427
  contextCalls: observation?.contextCalls ?? 0,
5387
5428
  verificationStatus: automation.status === "current" ? automation.verificationStatus ?? "unavailable" : "unavailable",
5388
- 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."] : []
5389
5431
  };
5390
5432
  }
5391
5433
  const statuses = Object.values(hosts2).map((host) => host.status);
@@ -5410,6 +5452,7 @@ function summarizeActivation(status) {
5410
5452
  ` Task context requests: ${state.contextCalls}; verification: ${state.verificationStatus}.`,
5411
5453
  ...state.pending.map((message2) => " Next: " + message2)
5412
5454
  );
5455
+ lines.push(...state.updates.map((message2) => " Optional: " + message2));
5413
5456
  }
5414
5457
  if ("decisionRecords" in status) lines.push(`Decision records: ${status.decisionRecords}.`);
5415
5458
  if (status.next) lines.push(status.next);
@@ -5558,16 +5601,16 @@ var init_setup = __esm({
5558
5601
  });
5559
5602
 
5560
5603
  // src/distribution/bundle.ts
5561
- import fs19 from "fs/promises";
5562
- import path24 from "path";
5604
+ import fs20 from "fs/promises";
5605
+ import path25 from "path";
5563
5606
  import { createHash as createHash4 } from "crypto";
5564
5607
  import { z as z23 } from "zod";
5565
5608
  async function readBundle(root) {
5566
- const bytes = await fs19.readFile(await storePath(root, "bundle.json"));
5609
+ const bytes = await fs20.readFile(await storePath(root, "bundle.json"));
5567
5610
  const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
5568
5611
  const windows = manifest.target.startsWith("win32-");
5569
5612
  for (const file of Object.keys(manifest.files)) {
5570
- if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") || path24.isAbsolute(file)) {
5613
+ if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") || path25.isAbsolute(file)) {
5571
5614
  throw new Error("Unsafe path in Mason bundle: " + file);
5572
5615
  }
5573
5616
  }
@@ -5582,7 +5625,7 @@ async function verifyBundle(root, expectedHash) {
5582
5625
  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.");
5583
5626
  const files = Object.keys(bundle.manifest.files);
5584
5627
  for (let i = 0; i < files.length; i += 8) await Promise.all(files.slice(i, i + 8).map(async (file) => {
5585
- if (sha256(await fs19.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
5628
+ if (sha256(await fs20.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
5586
5629
  }));
5587
5630
  return bundle;
5588
5631
  }
@@ -5605,18 +5648,18 @@ var init_bundle = __esm({
5605
5648
  });
5606
5649
 
5607
5650
  // src/distribution/path.ts
5608
- import fs20 from "fs/promises";
5651
+ import fs21 from "fs/promises";
5609
5652
  import os3 from "os";
5610
- import path25 from "path";
5611
- import { execFile as execFile14 } from "child_process";
5612
- import { promisify as promisify14 } from "util";
5653
+ import path26 from "path";
5654
+ import { execFile as execFile13 } from "child_process";
5655
+ import { promisify as promisify13 } from "util";
5613
5656
  import { createHash as createHash5, randomUUID as randomUUID7 } from "crypto";
5614
5657
  import { z as z24 } from "zod";
5615
5658
  async function configurePath(bin, previous, save, options = {}) {
5616
5659
  const env = options.env ?? process.env, platform = options.platform ?? process.platform;
5617
5660
  const changes = [...previous];
5618
5661
  const currentPath = platform === "win32" ? Object.entries(env).find(([key]) => key.toLowerCase() === "path")?.[1] : env.PATH;
5619
- const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) => path25.posix.isAbsolute(entry) && path25.posix.resolve(entry) === path25.posix.resolve(bin));
5662
+ const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) => path26.posix.isAbsolute(entry) && path26.posix.resolve(entry) === path26.posix.resolve(bin));
5620
5663
  const message2 = alreadyOnPath ? "Run: mason setup --host codex" : "Open a new terminal, then run: mason setup --host codex";
5621
5664
  const manual = (reason2) => ({ status: "manual", changes, message: `${reason2}
5622
5665
  ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --host codex`}` });
@@ -5634,33 +5677,33 @@ ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --h
5634
5677
  }
5635
5678
  return { status: "configured", changes, message: message2 };
5636
5679
  }
5637
- const shell = path25.basename(env.SHELL ?? os3.userInfo().shell ?? "");
5680
+ const shell = path26.basename(env.SHELL ?? os3.userInfo().shell ?? "");
5638
5681
  const userHome = options.userHome ?? os3.homedir();
5639
5682
  let profiles;
5640
5683
  if (!["bash", "zsh", "sh"].includes(shell)) return manual(`Automatic PATH setup is unavailable for ${shell || "an unknown shell"}.`);
5641
- if (env.MASON_PROFILE) profiles = [path25.resolve(env.MASON_PROFILE)];
5642
- else if (shell === "zsh") profiles = [path25.join(env.ZDOTDIR || userHome, ".zshrc")];
5643
- else if (shell === "sh") profiles = [path25.join(userHome, ".profile")];
5684
+ if (env.MASON_PROFILE) profiles = [path26.resolve(env.MASON_PROFILE)];
5685
+ else if (shell === "zsh") profiles = [path26.join(env.ZDOTDIR || userHome, ".zshrc")];
5686
+ else if (shell === "sh") profiles = [path26.join(userHome, ".profile")];
5644
5687
  else {
5645
- const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path25.join(userHome, name));
5688
+ const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path26.join(userHome, name));
5646
5689
  let login = candidates[2];
5647
- for (const file of candidates) if (await fs20.lstat(file).then(() => true, (error) => {
5690
+ for (const file of candidates) if (await fs21.lstat(file).then(() => true, (error) => {
5648
5691
  if (error.code === "ENOENT") return false;
5649
5692
  throw error;
5650
5693
  })) {
5651
5694
  login = file;
5652
5695
  break;
5653
5696
  }
5654
- profiles = [path25.join(userHome, ".bashrc"), login];
5697
+ profiles = [path26.join(userHome, ".bashrc"), login];
5655
5698
  }
5656
5699
  const marker = `# >>> mason PATH ${createHash5("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
5657
5700
  for (const profile of profiles) {
5658
- const stat = await fs20.lstat(profile).catch((error) => {
5701
+ const stat = await fs21.lstat(profile).catch((error) => {
5659
5702
  if (error.code === "ENOENT") return null;
5660
5703
  throw error;
5661
5704
  });
5662
- const file = stat?.isSymbolicLink() ? await fs20.realpath(profile) : path25.resolve(profile);
5663
- const original = await fs20.readFile(file).catch((error) => {
5705
+ const file = stat?.isSymbolicLink() ? await fs21.realpath(profile) : path26.resolve(profile);
5706
+ const original = await fs21.readFile(file).catch((error) => {
5664
5707
  if (error.code === "ENOENT" && !stat) return null;
5665
5708
  throw error;
5666
5709
  });
@@ -5675,8 +5718,8 @@ ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --h
5675
5718
  changes.push({ kind: "profile", file, block, created: original === null });
5676
5719
  await save(changes);
5677
5720
  }
5678
- await fs20.mkdir(path25.dirname(file), { recursive: true });
5679
- const handle = await fs20.open(file, original === null ? "ax+" : "a+", 384);
5721
+ await fs21.mkdir(path26.dirname(file), { recursive: true });
5722
+ const handle = await fs21.open(file, original === null ? "ax+" : "a+", 384);
5680
5723
  try {
5681
5724
  if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0))) throw new Error(`Shell profile changed during installation: ${profile}`);
5682
5725
  await handle.writeFile(block);
@@ -5700,13 +5743,13 @@ async function removePath(changes) {
5700
5743
  await windowsPath("remove", change.entry, current, change.created);
5701
5744
  continue;
5702
5745
  }
5703
- const stat = await fs20.lstat(change.file).catch((error) => {
5746
+ const stat = await fs21.lstat(change.file).catch((error) => {
5704
5747
  if (error.code === "ENOENT") return null;
5705
5748
  throw error;
5706
5749
  });
5707
5750
  if (!stat) continue;
5708
5751
  if (!stat.isFile()) throw new Error("profile type changed");
5709
- const bytes = await fs20.readFile(change.file), text2 = bytes.toString("utf8");
5752
+ const bytes = await fs21.readFile(change.file), text2 = bytes.toString("utf8");
5710
5753
  if (!Buffer.from(text2).equals(bytes)) throw new Error("profile encoding changed");
5711
5754
  const index2 = text2.indexOf(change.block);
5712
5755
  if (index2 < 0 || text2.indexOf(change.block, index2 + change.block.length) >= 0) throw new Error("PATH block was edited or removed");
@@ -5716,7 +5759,7 @@ async function removePath(changes) {
5716
5759
  const temporary = change.file + ".mason-" + randomUUID7();
5717
5760
  try {
5718
5761
  if (stat.nlink > 1) throw new Error("profile has multiple hard links");
5719
- const handle = await fs20.open(temporary, "wx", stat.mode & 511);
5762
+ const handle = await fs21.open(temporary, "wx", stat.mode & 511);
5720
5763
  try {
5721
5764
  await handle.writeFile(next);
5722
5765
  await handle.chmod(stat.mode & 511);
@@ -5725,11 +5768,11 @@ async function removePath(changes) {
5725
5768
  } finally {
5726
5769
  await handle.close();
5727
5770
  }
5728
- const current = await fs20.lstat(change.file);
5729
- if (!current.isFile() || current.ino !== stat.ino || !(await fs20.readFile(change.file)).equals(bytes)) throw new Error("profile changed during uninstall");
5730
- await fs20.rename(temporary, change.file);
5771
+ const current = await fs21.lstat(change.file);
5772
+ if (!current.isFile() || current.ino !== stat.ino || !(await fs21.readFile(change.file)).equals(bytes)) throw new Error("profile changed during uninstall");
5773
+ await fs21.rename(temporary, change.file);
5731
5774
  } finally {
5732
- await fs20.rm(temporary, { force: true });
5775
+ await fs21.rm(temporary, { force: true });
5733
5776
  }
5734
5777
  } catch (error) {
5735
5778
  warnings.push(`Retained PATH setting ${change.kind === "profile" ? change.file : change.entry}: ${error instanceof Error ? error.message : String(error)}`);
@@ -5770,13 +5813,13 @@ var execute3, pathChangeSchema, quote, normalizeWindowsEntry, hasWindowsEntry;
5770
5813
  var init_path = __esm({
5771
5814
  "src/distribution/path.ts"() {
5772
5815
  "use strict";
5773
- execute3 = promisify14(execFile14);
5816
+ execute3 = promisify13(execFile13);
5774
5817
  pathChangeSchema = z24.discriminatedUnion("kind", [
5775
5818
  z24.object({ kind: z24.literal("profile"), file: z24.string(), block: z24.string(), created: z24.boolean() }),
5776
5819
  z24.object({ kind: z24.literal("windows"), entry: z24.string(), created: z24.boolean() })
5777
5820
  ]);
5778
5821
  quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
5779
- normalizeWindowsEntry = (value) => path25.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
5822
+ normalizeWindowsEntry = (value) => path26.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
5780
5823
  hasWindowsEntry = (value, entry, env = process.env) => {
5781
5824
  const variables = new Map(Object.entries(env).map(([key, value2]) => [key.toLowerCase(), value2]));
5782
5825
  const expand = (part) => part.replace(/%([^%]+)%/g, (match, key) => variables.get(key.toLowerCase()) ?? match);
@@ -5794,9 +5837,9 @@ __export(install_exports2, {
5794
5837
  uninstallStandalone: () => uninstallStandalone,
5795
5838
  upgradeStandalone: () => upgradeStandalone
5796
5839
  });
5797
- import fs21 from "fs/promises";
5840
+ import fs22 from "fs/promises";
5798
5841
  import os4 from "os";
5799
- import path26 from "path";
5842
+ import path27 from "path";
5800
5843
  import { fileURLToPath } from "url";
5801
5844
  import { spawn } from "child_process";
5802
5845
  import { randomUUID as randomUUID8 } from "crypto";
@@ -5804,31 +5847,31 @@ import { z as z25 } from "zod";
5804
5847
  async function replace(file, content2, mode = 384) {
5805
5848
  const temp = file + ".tmp-" + randomUUID8();
5806
5849
  try {
5807
- await fs21.writeFile(temp, content2, { mode, flag: "wx" });
5808
- await fs21.rename(temp, file);
5850
+ await fs22.writeFile(temp, content2, { mode, flag: "wx" });
5851
+ await fs22.rename(temp, file);
5809
5852
  } finally {
5810
- await fs21.rm(temp, { force: true });
5853
+ await fs22.rm(temp, { force: true });
5811
5854
  }
5812
5855
  }
5813
5856
  async function installStandalone(source) {
5814
5857
  const bundle = await verifyBundle(source);
5815
- const selectedHome = path26.resolve(defaultHome());
5816
- await fs21.mkdir(selectedHome, { recursive: true });
5817
- const home = await fs21.realpath(selectedHome);
5818
- const selectedBin = path26.resolve(process.env.MASON_BIN_DIR ?? (process.platform === "win32" ? path26.join(home, "bin") : path26.join(os4.homedir(), ".local/bin")));
5858
+ const selectedHome = path27.resolve(defaultHome());
5859
+ await fs22.mkdir(selectedHome, { recursive: true });
5860
+ const home = await fs22.realpath(selectedHome);
5861
+ const selectedBin = path27.resolve(process.env.MASON_BIN_DIR ?? (process.platform === "win32" ? path27.join(home, "bin") : path27.join(os4.homedir(), ".local/bin")));
5819
5862
  return withLock(home, ".install-lock", async () => {
5820
5863
  const raw = await readStoreJson(home, "install.json");
5821
5864
  const previous = raw === null ? null : recordSchema3.parse(raw);
5822
- if (!previous) for (const file of await fs21.readdir(home)) {
5865
+ if (!previous) for (const file of await fs22.readdir(home)) {
5823
5866
  if (file === ".install-lock") continue;
5824
- const directory = path26.join(home, file);
5825
- const stat = await fs21.lstat(directory);
5826
- if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs21.readdir(directory)).length) {
5867
+ const directory = path27.join(home, file);
5868
+ const stat = await fs22.lstat(directory);
5869
+ if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs22.readdir(directory)).length) {
5827
5870
  throw new Error("Mason installation directory is not empty and has no ownership receipt.");
5828
5871
  }
5829
5872
  }
5830
- await fs21.mkdir(selectedBin, { recursive: true });
5831
- const bin = await fs21.realpath(selectedBin);
5873
+ await fs22.mkdir(selectedBin, { recursive: true });
5874
+ const bin = await fs22.realpath(selectedBin);
5832
5875
  if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
5833
5876
  const id = bundle.manifestHash.slice(0, 24);
5834
5877
  const destination = await storePath(home, "versions/" + id);
@@ -5839,10 +5882,10 @@ $previousToken = $env:MASON_UNINSTALL_TOKEN
5839
5882
  $token = [Guid]::NewGuid().ToString()
5840
5883
  $env:MASON_UNINSTALL_TOKEN = $token
5841
5884
  try {
5842
- & ${psQuote(path26.join(destination, "node.exe"))} ${psQuote(path26.join(destination, "app/dist/mason.js"))} @args
5885
+ & ${psQuote(path27.join(destination, "node.exe"))} ${psQuote(path27.join(destination, "app/dist/mason.js"))} @args
5843
5886
  $code = $LASTEXITCODE
5844
5887
  if ($code -eq 0 -and $args.Count -eq 1 -and $args[0] -eq 'uninstall') {
5845
- if ([IO.File]::ReadAllText(${psQuote(path26.join(home, ".uninstall-request.txt"))}) -cne $token) { throw 'Mason did not authorize installation cleanup.' }
5888
+ if ([IO.File]::ReadAllText(${psQuote(path27.join(home, ".uninstall-request.txt"))}) -cne $token) { throw 'Mason did not authorize installation cleanup.' }
5846
5889
  Remove-Item -LiteralPath ${psQuote(home)} -Recurse -Force
5847
5890
  Write-Output ${psQuote("Removed standalone installation: " + home)}
5848
5891
  }
@@ -5851,11 +5894,11 @@ try {
5851
5894
  finally { $env:MASON_UNINSTALL_TOKEN = $previousToken }
5852
5895
  `
5853
5896
  } : { mason: `#!/bin/sh
5854
- exec ${shQuote(path26.join(destination, "node"))} ${shQuote(path26.join(destination, "app/dist/mason.js"))} "$@"
5897
+ exec ${shQuote(path27.join(destination, "node"))} ${shQuote(path27.join(destination, "app/dist/mason.js"))} "$@"
5855
5898
  ` };
5856
5899
  for (const [name, content2] of Object.entries(launchers)) {
5857
5900
  const file = await storePath(bin, name);
5858
- const old = await fs21.readFile(file, "utf8").catch((error) => {
5901
+ const old = await fs22.readFile(file, "utf8").catch((error) => {
5859
5902
  if (error.code === "ENOENT") return null;
5860
5903
  throw error;
5861
5904
  });
@@ -5864,7 +5907,7 @@ exec ${shQuote(path26.join(destination, "node"))} ${shQuote(path26.join(destinat
5864
5907
  const retired = Object.entries({ ...previous?.previousLaunchers, ...previous?.launchers }).filter(([name]) => !(name in launchers));
5865
5908
  const checkRetired = async (name, expected) => {
5866
5909
  if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
5867
- const actual = await fs21.readFile(await storePath(bin, name), "utf8").catch((error) => {
5910
+ const actual = await fs22.readFile(await storePath(bin, name), "utf8").catch((error) => {
5868
5911
  if (error.code === "ENOENT") return null;
5869
5912
  throw error;
5870
5913
  });
@@ -5874,28 +5917,28 @@ exec ${shQuote(path26.join(destination, "node"))} ${shQuote(path26.join(destinat
5874
5917
  const receipt = { format: 1, home, bin, current: id, version: bundle.manifest.version, launchers, pathChanges: previous?.pathChanges ?? [] };
5875
5918
  await writeStoreJson(home, "install.json", { ...receipt, previousLaunchers: previous?.previousLaunchers ?? previous?.launchers });
5876
5919
  const stage = await storePath(home, "versions/.install-" + randomUUID8(), true);
5877
- await fs21.mkdir(stage);
5920
+ await fs22.mkdir(stage);
5878
5921
  try {
5879
5922
  for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
5880
5923
  const from = await storePath(source, file), to = await storePath(stage, file, true);
5881
- await fs21.copyFile(from, to);
5882
- await fs21.chmod(to, (await fs21.stat(from)).mode & 511);
5924
+ await fs22.copyFile(from, to);
5925
+ await fs22.chmod(to, (await fs22.stat(from)).mode & 511);
5883
5926
  }
5884
5927
  await verifyBundle(stage, bundle.manifestHash);
5885
- const exists = await fs21.lstat(destination).then(() => true, (error) => {
5928
+ const exists = await fs22.lstat(destination).then(() => true, (error) => {
5886
5929
  if (error.code === "ENOENT") return false;
5887
5930
  throw error;
5888
5931
  });
5889
5932
  if (exists) await verifyBundle(destination, bundle.manifestHash);
5890
- else await fs21.rename(stage, destination);
5933
+ else await fs22.rename(stage, destination);
5891
5934
  for (const [name, content2] of Object.entries(launchers)) await replace(await storePath(bin, name, true), content2, 493);
5892
5935
  for (const [name, expected] of retired) {
5893
5936
  await checkRetired(name, expected);
5894
- await fs21.rm(await storePath(bin, name), { force: true });
5937
+ await fs22.rm(await storePath(bin, name), { force: true });
5895
5938
  }
5896
5939
  await writeStoreJson(home, "install.json", receipt);
5897
5940
  } finally {
5898
- await fs21.rm(stage, { recursive: true, force: true });
5941
+ await fs22.rm(stage, { recursive: true, force: true });
5899
5942
  }
5900
5943
  const pathResult = await configurePath(bin, receipt.pathChanges, async (changes) => {
5901
5944
  receipt.pathChanges = changes;
@@ -5905,12 +5948,12 @@ exec ${shQuote(path26.join(destination, "node"))} ${shQuote(path26.join(destinat
5905
5948
  });
5906
5949
  }
5907
5950
  async function currentInstallation() {
5908
- const app = path26.dirname(path26.dirname(fileURLToPath(import.meta.url)));
5909
- const bundle = path26.dirname(app), home = path26.dirname(path26.dirname(bundle));
5951
+ const app = path27.dirname(path27.dirname(fileURLToPath(import.meta.url)));
5952
+ const bundle = path27.dirname(app), home = path27.dirname(path27.dirname(bundle));
5910
5953
  const raw = await readStoreJson(home, "install.json");
5911
5954
  if (!raw) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
5912
5955
  const record = recordSchema3.parse(raw);
5913
- if (record.home !== await fs21.realpath(home) || path26.dirname(bundle) !== path26.join(home, "versions")) throw new Error("Invalid standalone installation receipt.");
5956
+ if (record.home !== await fs22.realpath(home) || path27.dirname(bundle) !== path27.join(home, "versions")) throw new Error("Invalid standalone installation receipt.");
5914
5957
  return { home, bundle, record };
5915
5958
  }
5916
5959
  async function upgradeStandalone(version) {
@@ -5918,7 +5961,7 @@ async function upgradeStandalone(version) {
5918
5961
  const { home, bundle, record } = await currentInstallation();
5919
5962
  await verifyBundle(bundle);
5920
5963
  const windows = process.platform === "win32";
5921
- const script = path26.join(bundle, windows ? "install.ps1" : "install.sh");
5964
+ const script = path27.join(bundle, windows ? "install.ps1" : "install.sh");
5922
5965
  return new Promise((resolve, reject) => {
5923
5966
  const child = spawn(
5924
5967
  windows ? "powershell.exe" : "sh",
@@ -5939,7 +5982,7 @@ async function uninstallStandalone() {
5939
5982
  for (const [name, expected] of Object.entries(record.launchers)) {
5940
5983
  if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
5941
5984
  const file = await storePath(record.bin, name);
5942
- const actual = await fs21.readFile(file, "utf8").catch((error) => {
5985
+ const actual = await fs22.readFile(file, "utf8").catch((error) => {
5943
5986
  if (error.code === "ENOENT") return null;
5944
5987
  throw error;
5945
5988
  });
@@ -5947,9 +5990,9 @@ async function uninstallStandalone() {
5947
5990
  }
5948
5991
  for (const warning of await removePath(record.pathChanges ?? [])) console.error(warning);
5949
5992
  if (process.platform === "win32") await replace(await storePath(home, ".uninstall-request.txt", true), token);
5950
- for (const name of Object.keys(record.launchers)) await fs21.rm(await storePath(record.bin, name), { force: true });
5993
+ for (const name of Object.keys(record.launchers)) await fs22.rm(await storePath(record.bin, name), { force: true });
5951
5994
  });
5952
- if (process.platform !== "win32") await fs21.rm(home, { recursive: true, force: true });
5995
+ if (process.platform !== "win32") await fs22.rm(home, { recursive: true, force: true });
5953
5996
  return home;
5954
5997
  }
5955
5998
  var recordSchema3, shQuote, psQuote, WINDOWS_BATCH_LAUNCHER, launcherNames, defaultHome;
@@ -5974,7 +6017,7 @@ var init_install2 = __esm({
5974
6017
  psQuote = (s) => "'" + s.replace(/'/g, "''") + "'";
5975
6018
  WINDOWS_BATCH_LAUNCHER = '@echo off\r\ngoto #_mason_handoff_# 2>NUL || title %COMSPEC% & powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0mason-launcher.ps1" %*\r\n';
5976
6019
  launcherNames = ["mason", "mason.cmd", "mason.ps1", "mason-launcher.ps1"];
5977
- defaultHome = () => process.env.MASON_HOME ?? (process.platform === "win32" ? path26.join(process.env.LOCALAPPDATA ?? os4.homedir(), "Mason") : path26.join(os4.homedir(), ".local/share/mason"));
6020
+ defaultHome = () => process.env.MASON_HOME ?? (process.platform === "win32" ? path27.join(process.env.LOCALAPPDATA ?? os4.homedir(), "Mason") : path27.join(os4.homedir(), ".local/share/mason"));
5978
6021
  }
5979
6022
  });
5980
6023
 
@@ -6039,7 +6082,7 @@ function isHookCommand(argv) {
6039
6082
  async function runAutomationCli(argv, stdin = "", io = {
6040
6083
  out: (s) => process.stdout.write(s + "\n"),
6041
6084
  err: (s) => process.stderr.write(s + "\n")
6042
- }) {
6085
+ }, options = {}) {
6043
6086
  let action = "";
6044
6087
  try {
6045
6088
  const { values, positionals } = parseCli(argv);
@@ -6059,7 +6102,9 @@ async function runAutomationCli(argv, stdin = "", io = {
6059
6102
  return result.status === "complete" ? 0 : 2;
6060
6103
  }
6061
6104
  if (action === "hook") {
6062
- const output = await runAutomationHook(hostSchema.parse(values.host), stdin);
6105
+ const host = hostSchema.parse(values.host);
6106
+ if (options.managedHost && options.managedHost !== host) throw new Error("Managed hook --host does not match its launcher.");
6107
+ const output = await runAutomationHook(host, stdin, { managed: !!options.managedHost });
6063
6108
  if (output) io.out(JSON.stringify(output));
6064
6109
  return 0;
6065
6110
  }
@@ -6104,7 +6149,7 @@ var inputArgs = process.argv.slice(2);
6104
6149
  var managedLaunch = inputArgs[0] === "--setup-host";
6105
6150
  var setupHost = managedLaunch ? inputArgs.splice(0, 2)[1] : void 0;
6106
6151
  var [command, ...args] = inputArgs;
6107
- var usage = `Mason ${"0.17.0"}
6152
+ var usage = `Mason ${"0.17.1"}
6108
6153
 
6109
6154
  Usage: mason <command> [options]
6110
6155
 
@@ -6124,13 +6169,14 @@ mason-audit, mason-review, mason-drift, mason-hook and mason-mcp.`;
6124
6169
  try {
6125
6170
  if (managedLaunch) {
6126
6171
  if (!setupHost || !["codex", "claude"].includes(setupHost) || !["mcp", "auto"].includes(command)) throw new Error("Invalid setup invocation.");
6127
- const { prepareLaunch: prepareLaunch2 } = await Promise.resolve().then(() => (init_launcher(), launcher_exports));
6128
- const active = await prepareLaunch2(setupHost, { allowInactive: command === "auto" && isHookCommand(args) });
6129
- if (!active) process.exit(0);
6172
+ if (!(command === "auto" && isHookCommand(args))) {
6173
+ const { prepareLaunch: prepareLaunch2 } = await Promise.resolve().then(() => (init_launcher(), launcher_exports));
6174
+ await prepareLaunch2(setupHost, { allowInactive: command === "mcp" });
6175
+ }
6130
6176
  }
6131
- if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.17.0" }));
6177
+ if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.17.1" }));
6132
6178
  else if (!command || ["--help", "-h", "help"].includes(command)) console.log(usage);
6133
- else if (["--version", "-v"].includes(command)) console.log("0.17.0");
6179
+ else if (["--version", "-v"].includes(command)) console.log("0.17.1");
6134
6180
  else if (command === "internal-install") {
6135
6181
  if (args.length) throw new Error("internal-install takes no arguments.");
6136
6182
  const { installStandalone: installStandalone2 } = await Promise.resolve().then(() => (init_install2(), install_exports2));
@@ -6153,7 +6199,12 @@ ${installed.path.message}`);
6153
6199
  input += chunk.toString();
6154
6200
  if (Buffer.byteLength(input) > 1024 * 1024) break;
6155
6201
  }
6156
- process.exitCode = await runAutomationCli(forwarded, input);
6202
+ process.exitCode = await runAutomationCli(
6203
+ forwarded,
6204
+ input,
6205
+ void 0,
6206
+ { managedHost: managedLaunch ? setupHost : void 0 }
6207
+ );
6157
6208
  } else if (["audit", "review", "drift", "hook", "mcp"].includes(command)) {
6158
6209
  const binary = new URL(`./mason-${command}.js`, import.meta.url);
6159
6210
  process.argv = [process.execPath, fileURLToPath2(binary), ...args];