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/CHANGELOG.md +10 -0
- package/dist/mason-audit.js +14 -4
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-auto.js +155 -89
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-mcp.js +2656 -2635
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason.js +216 -165
- package/dist/mason.js.map +1 -1
- package/package.json +1 -1
package/dist/mason-auto.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
|
|
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(
|
|
1098
|
+
await fs6.access(await auditInputPath(root, file));
|
|
1100
1099
|
return true;
|
|
1101
1100
|
} catch (error) {
|
|
1102
|
-
if (error.code
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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" || !
|
|
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
|
|
3620
|
-
if (ws.root !== await
|
|
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
|
|
3790
|
-
import
|
|
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
|
|
3898
|
+
await fs18.unlink(file2);
|
|
3820
3899
|
return true;
|
|
3821
3900
|
}
|
|
3822
3901
|
const file = await storePath(root, edit.path, true);
|
|
3823
|
-
const temporary =
|
|
3902
|
+
const temporary = path22.join(path22.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
|
|
3824
3903
|
try {
|
|
3825
|
-
const mode = await
|
|
3826
|
-
const handle = await
|
|
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
|
|
3913
|
+
await fs18.rename(temporary, file);
|
|
3835
3914
|
return true;
|
|
3836
3915
|
} finally {
|
|
3837
|
-
await
|
|
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
|
-
|
|
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
|
|
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
|
|
4335
|
-
|
|
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
|
|
4503
|
-
import { promisify as
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
4929
|
+
import path23 from "path";
|
|
4877
4930
|
import { z as z22 } from "zod";
|
|
4878
4931
|
function relativeArtifact(root, file) {
|
|
4879
|
-
const relative =
|
|
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 (!
|
|
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
|
|
5098
|
-
import
|
|
5099
|
-
import { execFile as
|
|
5100
|
-
import { promisify as
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
}
|