mason-context 0.14.2 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/dist/mason-auto.js +807 -643
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-mcp.js +2286 -2352
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason.js +1292 -1056
- package/dist/mason.js.map +1 -1
- package/package.json +1 -1
package/dist/mason-auto.js
CHANGED
|
@@ -2521,7 +2521,7 @@ var init_evidence = __esm({
|
|
|
2521
2521
|
init_dead_command();
|
|
2522
2522
|
init_storage();
|
|
2523
2523
|
exec7 = promisify8(execFile8);
|
|
2524
|
-
engineVersion = true ? "0.
|
|
2524
|
+
engineVersion = true ? "0.16.0" : "development";
|
|
2525
2525
|
hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
|
|
2526
2526
|
content = readAuditInput;
|
|
2527
2527
|
internal = (file) => file === ".mason" || file === ".mason/reports" || file.startsWith(".mason/reports/");
|
|
@@ -2813,9 +2813,9 @@ async function automationStatus(dir) {
|
|
|
2813
2813
|
if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) throw new Error("Automation state belongs to another workspace.");
|
|
2814
2814
|
const inputs = await readInputs(ws.root);
|
|
2815
2815
|
const latest = state.latest ? await readStoreJson(ws.root, state.latest) : null;
|
|
2816
|
-
const
|
|
2816
|
+
const hosts2 = {};
|
|
2817
2817
|
for (const session of Object.values(state.sessions)) {
|
|
2818
|
-
const host =
|
|
2818
|
+
const host = hosts2[session.host] ??= { sessions: 0, observedEvents: [] };
|
|
2819
2819
|
host.sessions++;
|
|
2820
2820
|
host.observedEvents = [.../* @__PURE__ */ new Set([...host.observedEvents, ...Object.keys(session.events)])];
|
|
2821
2821
|
}
|
|
@@ -2828,7 +2828,7 @@ async function automationStatus(dir) {
|
|
|
2828
2828
|
baselinePaths: state.baselines.map((b) => b.path),
|
|
2829
2829
|
reportPath: state.latest,
|
|
2830
2830
|
verificationStatus: unfinished ? "unavailable" : latest?.status ?? "unavailable",
|
|
2831
|
-
hosts,
|
|
2831
|
+
hosts: hosts2,
|
|
2832
2832
|
execution,
|
|
2833
2833
|
note: "Observed events do not prove all tool paths are intercepted. Run check to verify the retained evidence."
|
|
2834
2834
|
};
|
|
@@ -2852,7 +2852,7 @@ var init_runtime = __esm({
|
|
|
2852
2852
|
// src/setup/model.ts
|
|
2853
2853
|
import { z as z7 } from "zod";
|
|
2854
2854
|
async function loadSetup(root) {
|
|
2855
|
-
const raw = await readStoreJson(root,
|
|
2855
|
+
const raw = await readStoreJson(root, SETUP_PATH);
|
|
2856
2856
|
if (raw === null) return null;
|
|
2857
2857
|
return setupSchema.parse(raw);
|
|
2858
2858
|
}
|
|
@@ -2863,28 +2863,23 @@ async function loadSetupReceipt(root, directory, host) {
|
|
|
2863
2863
|
if (receipt.root !== root || receipt.host !== host) throw new Error("Setup receipt belongs to another installation.");
|
|
2864
2864
|
return receipt;
|
|
2865
2865
|
}
|
|
2866
|
-
var
|
|
2866
|
+
var setupHostSchema, setupSchema, SETUP_PATH, receiptSchema;
|
|
2867
2867
|
var init_model = __esm({
|
|
2868
2868
|
"src/setup/model.ts"() {
|
|
2869
2869
|
"use strict";
|
|
2870
2870
|
init_storage();
|
|
2871
|
-
runtimeSchema = z7.object({
|
|
2872
|
-
id: z7.string().regex(/^[a-f0-9]{24}$/),
|
|
2873
|
-
version: z7.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
|
|
2874
|
-
hashes: z7.record(z7.string().regex(/^[a-f0-9]{64}$/)),
|
|
2875
|
-
bundle: z7.object({ target: z7.string(), manifestHash: z7.string().regex(/^[a-f0-9]{64}$/) }).optional()
|
|
2876
|
-
});
|
|
2877
2871
|
setupHostSchema = z7.object({
|
|
2878
|
-
|
|
2872
|
+
configuredVersion: z7.string(),
|
|
2879
2873
|
revision: z7.string().uuid(),
|
|
2880
2874
|
fingerprint: z7.string(),
|
|
2881
2875
|
mcpFingerprint: z7.string(),
|
|
2882
2876
|
instructions: z7.array(z7.string())
|
|
2883
2877
|
});
|
|
2884
|
-
setupSchema = z7.object({ version: z7.literal(
|
|
2878
|
+
setupSchema = z7.object({ version: z7.literal(2), hosts: z7.object({
|
|
2885
2879
|
codex: setupHostSchema.optional(),
|
|
2886
2880
|
claude: setupHostSchema.optional()
|
|
2887
2881
|
}) });
|
|
2882
|
+
SETUP_PATH = ".mason/local/setup.json";
|
|
2888
2883
|
receiptSchema = z7.object({
|
|
2889
2884
|
version: z7.literal(1),
|
|
2890
2885
|
host: z7.enum(["codex", "claude"]),
|
|
@@ -2898,6 +2893,39 @@ var init_model = __esm({
|
|
|
2898
2893
|
}
|
|
2899
2894
|
});
|
|
2900
2895
|
|
|
2896
|
+
// src/setup/launcher.ts
|
|
2897
|
+
import { execFile as execFile9 } from "child_process";
|
|
2898
|
+
import { promisify as promisify9 } from "util";
|
|
2899
|
+
function globalCommand(args, platform = process.platform) {
|
|
2900
|
+
return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args] } : { command: "mason", args };
|
|
2901
|
+
}
|
|
2902
|
+
async function installedCommand() {
|
|
2903
|
+
const invocation = globalCommand(["internal-integration-version"]);
|
|
2904
|
+
try {
|
|
2905
|
+
const { stdout } = await promisify9(execFile9)(
|
|
2906
|
+
invocation.command,
|
|
2907
|
+
invocation.args,
|
|
2908
|
+
{ timeout: 5e3, maxBuffer: 8192, windowsHide: true }
|
|
2909
|
+
);
|
|
2910
|
+
const result = JSON.parse(stdout);
|
|
2911
|
+
if (result.protocol !== 1 || typeof result.version !== "string") throw new Error("Unsupported Mason integration protocol.");
|
|
2912
|
+
return { available: true, version: result.version, message: null };
|
|
2913
|
+
} catch {
|
|
2914
|
+
return { available: false, version: null, message: "An up-to-date mason command is not available on this process's PATH. Install or upgrade Mason, then reopen your terminal or desktop assistant and rerun mason setup." };
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
var executingVersion, mcpCommand, hookCommand;
|
|
2918
|
+
var init_launcher = __esm({
|
|
2919
|
+
"src/setup/launcher.ts"() {
|
|
2920
|
+
"use strict";
|
|
2921
|
+
init_evidence();
|
|
2922
|
+
init_model();
|
|
2923
|
+
executingVersion = () => true ? "0.16.0" : "development";
|
|
2924
|
+
mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
|
|
2925
|
+
hookCommand = (host) => `mason --setup-host ${host} auto`;
|
|
2926
|
+
}
|
|
2927
|
+
});
|
|
2928
|
+
|
|
2901
2929
|
// src/setup/observations.ts
|
|
2902
2930
|
var observations_exports = {};
|
|
2903
2931
|
__export(observations_exports, {
|
|
@@ -2907,14 +2935,14 @@ __export(observations_exports, {
|
|
|
2907
2935
|
});
|
|
2908
2936
|
import fs13 from "fs/promises";
|
|
2909
2937
|
import { z as z8 } from "zod";
|
|
2910
|
-
function observationPath(directory, host, revision) {
|
|
2911
|
-
return `${directory}/activation/${host}-${revision}.json`;
|
|
2938
|
+
function observationPath(directory, host, revision, version = executingVersion()) {
|
|
2939
|
+
return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
|
|
2912
2940
|
}
|
|
2913
|
-
async function readObservation(root, directory, host, revision) {
|
|
2914
|
-
const raw = await readStoreJson(root, observationPath(directory, host, revision));
|
|
2941
|
+
async function readObservation(root, directory, host, revision, version = executingVersion()) {
|
|
2942
|
+
const raw = await readStoreJson(root, observationPath(directory, host, revision, version));
|
|
2915
2943
|
if (raw === null) return null;
|
|
2916
2944
|
const record = observationSchema.parse(raw);
|
|
2917
|
-
if (record.root !== root || record.host !== host || record.revision !== revision) throw new Error("Activation receipt belongs to another installation.");
|
|
2945
|
+
if (record.root !== root || record.host !== host || record.revision !== revision || record.masonVersion !== version) throw new Error("Activation receipt belongs to another installation.");
|
|
2918
2946
|
return record;
|
|
2919
2947
|
}
|
|
2920
2948
|
async function observeActivation(dir, event, options = {}) {
|
|
@@ -2931,6 +2959,7 @@ async function observeActivation(dir, event, options = {}) {
|
|
|
2931
2959
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2932
2960
|
const record = await readObservation(ws.root, ws.directory, host, revision) ?? {
|
|
2933
2961
|
version: 1,
|
|
2962
|
+
masonVersion: executingVersion(),
|
|
2934
2963
|
root: ws.root,
|
|
2935
2964
|
revision,
|
|
2936
2965
|
host,
|
|
@@ -2969,8 +2998,10 @@ var init_observations = __esm({
|
|
|
2969
2998
|
init_store();
|
|
2970
2999
|
init_storage();
|
|
2971
3000
|
init_model();
|
|
3001
|
+
init_launcher();
|
|
2972
3002
|
observationSchema = z8.object({
|
|
2973
3003
|
version: z8.literal(1),
|
|
3004
|
+
masonVersion: z8.string(),
|
|
2974
3005
|
root: z8.string(),
|
|
2975
3006
|
revision: z8.string(),
|
|
2976
3007
|
host: z8.enum(["codex", "claude"]),
|
|
@@ -3060,106 +3091,157 @@ var init_adapters = __esm({
|
|
|
3060
3091
|
}
|
|
3061
3092
|
});
|
|
3062
3093
|
|
|
3063
|
-
// src/
|
|
3064
|
-
var
|
|
3065
|
-
__export(
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
installedAutomation: () => installedAutomation,
|
|
3070
|
-
planAutomationInstall: () => planAutomationInstall
|
|
3094
|
+
// src/setup/files.ts
|
|
3095
|
+
var files_exports = {};
|
|
3096
|
+
__export(files_exports, {
|
|
3097
|
+
applyEdit: () => applyEdit,
|
|
3098
|
+
managedBlock: () => managedBlock,
|
|
3099
|
+
readText: () => readText
|
|
3071
3100
|
});
|
|
3072
|
-
import
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3101
|
+
import fs14 from "fs/promises";
|
|
3102
|
+
import path18 from "path";
|
|
3103
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
3104
|
+
async function readText(root, file) {
|
|
3105
|
+
try {
|
|
3106
|
+
const text2 = await readBoundedFile(await storePath(root, file), 2 * 1024 * 1024);
|
|
3107
|
+
if (text2 === null) throw new Error("Setup input is not a regular file or exceeds 2 MiB: " + file);
|
|
3108
|
+
return text2;
|
|
3109
|
+
} catch (error) {
|
|
3110
|
+
if (error.code === "ENOENT") return null;
|
|
3111
|
+
throw error;
|
|
3112
|
+
}
|
|
3076
3113
|
}
|
|
3077
|
-
|
|
3078
|
-
const
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
const desired = hookConfig(host, command);
|
|
3082
|
-
const newCommand = desired.hooks.SessionStart[0].hooks[0].command;
|
|
3083
|
-
const previous = record.hosts[host]?.command;
|
|
3084
|
-
const hooks = existing.hooks ?? {};
|
|
3085
|
-
for (const event of HOOK_EVENTS) {
|
|
3086
|
-
hooks[event] = (hooks[event] ?? []).map((group) => ({
|
|
3087
|
-
...group,
|
|
3088
|
-
hooks: group.hooks.filter((handler) => !(handler.type === "command" && typeof handler.command === "string" && (handler.command === previous || handler.command === newCommand)))
|
|
3089
|
-
})).filter((group) => group.hooks.length);
|
|
3090
|
-
hooks[event].push(...desired.hooks[event]);
|
|
3114
|
+
function managedBlock(text2, start, end, body) {
|
|
3115
|
+
const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
|
|
3116
|
+
if (starts !== ends || starts > 1 || starts === 1 && text2.indexOf(end) < text2.indexOf(start)) {
|
|
3117
|
+
throw new Error("Ambiguous Mason instruction/configuration markers; repair the marked block before setup.");
|
|
3091
3118
|
}
|
|
3092
|
-
|
|
3093
|
-
|
|
3119
|
+
const eol = text2.includes("\r\n") ? "\r\n" : "\n";
|
|
3120
|
+
const block = [start, body.replace(/\r?\n/g, eol), end].join(eol);
|
|
3121
|
+
if (starts) return text2.slice(0, text2.indexOf(start)) + block + text2.slice(text2.indexOf(end) + end.length);
|
|
3122
|
+
return text2 + (text2.length && !text2.endsWith("\n") ? eol : "") + (text2.length ? eol : "") + block + eol;
|
|
3094
3123
|
}
|
|
3095
|
-
async function
|
|
3096
|
-
const
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3124
|
+
async function applyEdit(root, edit) {
|
|
3125
|
+
const current = await readText(root, edit.path);
|
|
3126
|
+
if (current === edit.after) return false;
|
|
3127
|
+
if (current !== edit.before) throw new Error("Integration input changed during update: " + edit.path + ". Rerun the requested command to resume.");
|
|
3128
|
+
if (edit.after === null) {
|
|
3129
|
+
const file2 = await storePath(root, edit.path);
|
|
3130
|
+
if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
|
|
3131
|
+
await fs14.unlink(file2);
|
|
3132
|
+
return true;
|
|
3133
|
+
}
|
|
3134
|
+
const file = await storePath(root, edit.path, true);
|
|
3135
|
+
const temporary = path18.join(path18.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
|
|
3136
|
+
try {
|
|
3137
|
+
const mode = await fs14.stat(file).then((s) => s.mode & 511, () => 384);
|
|
3138
|
+
const handle = await fs14.open(temporary, "wx", mode);
|
|
3139
|
+
try {
|
|
3140
|
+
await handle.writeFile(edit.after, "utf8");
|
|
3141
|
+
await handle.sync();
|
|
3142
|
+
} finally {
|
|
3143
|
+
await handle.close();
|
|
3144
|
+
}
|
|
3145
|
+
if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
|
|
3146
|
+
await fs14.rename(temporary, file);
|
|
3147
|
+
return true;
|
|
3148
|
+
} finally {
|
|
3149
|
+
await fs14.rm(temporary, { force: true });
|
|
3150
|
+
}
|
|
3109
3151
|
}
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3152
|
+
var init_files2 = __esm({
|
|
3153
|
+
"src/setup/files.ts"() {
|
|
3154
|
+
"use strict";
|
|
3155
|
+
init_files();
|
|
3156
|
+
init_storage();
|
|
3157
|
+
}
|
|
3158
|
+
});
|
|
3159
|
+
|
|
3160
|
+
// src/setup/ownership.ts
|
|
3161
|
+
var ownership_exports = {};
|
|
3162
|
+
__export(ownership_exports, {
|
|
3163
|
+
BLOCKS: () => BLOCKS,
|
|
3164
|
+
OWNERSHIP_PATH: () => OWNERSHIP_PATH,
|
|
3165
|
+
blockSpan: () => blockSpan,
|
|
3166
|
+
completedHookOwnership: () => completedHookOwnership,
|
|
3167
|
+
ownershipEdit: () => ownershipEdit,
|
|
3168
|
+
ownershipSchema: () => ownershipSchema
|
|
3169
|
+
});
|
|
3170
|
+
import { z as z10 } from "zod";
|
|
3171
|
+
import { parse } from "smol-toml";
|
|
3172
|
+
function blockSpan(text2, start, end) {
|
|
3173
|
+
const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
|
|
3174
|
+
if (!starts && !ends) return null;
|
|
3175
|
+
const from = text2.indexOf(start), to = text2.indexOf(end) + end.length;
|
|
3176
|
+
if (starts !== 1 || ends !== 1 || to <= from) throw new Error("Ambiguous Mason markers; inspect the marked block manually.");
|
|
3177
|
+
return { from, to, text: text2.slice(from, to) };
|
|
3178
|
+
}
|
|
3179
|
+
async function ownershipEdit(root, edits, hookCommands = {}) {
|
|
3180
|
+
const before = await readText(root, OWNERSHIP_PATH);
|
|
3181
|
+
const record = before === null ? { version: 1, files: {} } : ownershipSchema.parse(JSON.parse(before));
|
|
3182
|
+
for (const edit of edits) {
|
|
3183
|
+
const previous = record.files[edit.path];
|
|
3184
|
+
const entry = {
|
|
3185
|
+
created: edit.before === null || (previous?.created ?? false),
|
|
3186
|
+
initialHash: edit.before === null || previous?.initialHash === hash(edit.before) ? hash(edit.after) : previous?.initialHash ?? hash(edit.after),
|
|
3187
|
+
blocks: Object.fromEntries(BLOCKS.flatMap(([start, end]) => {
|
|
3188
|
+
const block = blockSpan(edit.after, start, end);
|
|
3189
|
+
return block ? [[start, hash(block.text)]] : [];
|
|
3190
|
+
}))
|
|
3191
|
+
};
|
|
3192
|
+
const mcp = edit.path === ".codex/config.toml" ? parse(edit.after).mcp_servers?.mason : edit.path === ".mcp.json" ? JSON.parse(edit.after).mcpServers?.mason : void 0;
|
|
3193
|
+
const hooks = edit.path === ".codex/hooks.json" || edit.path === ".claude/settings.json" ? JSON.parse(edit.after).hooks : void 0;
|
|
3194
|
+
record.files[edit.path] = {
|
|
3195
|
+
...entry,
|
|
3196
|
+
...mcp === void 0 ? {} : { mcpHash: hash(mcp) },
|
|
3197
|
+
...hookCommands[edit.path] ? { hookCommands: [.../* @__PURE__ */ new Set([...previous?.hookCommands ?? [], hookCommands[edit.path]])] } : {},
|
|
3198
|
+
...hooks === void 0 ? {} : { hooks: Object.fromEntries(Object.entries(hooks).map(([event, groups]) => [event, groups.flatMap((group) => group.hooks.map(hash))])) }
|
|
3126
3199
|
};
|
|
3127
3200
|
}
|
|
3128
|
-
return
|
|
3201
|
+
return { path: OWNERSHIP_PATH, before, after: JSON.stringify(record, null, 2) + "\n" };
|
|
3129
3202
|
}
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3203
|
+
async function completedHookOwnership(root, file, command) {
|
|
3204
|
+
const before = await readText(root, OWNERSHIP_PATH);
|
|
3205
|
+
const record = ownershipSchema.parse(JSON.parse(before ?? "null"));
|
|
3206
|
+
record.files[file].hookCommands = [command];
|
|
3207
|
+
return { path: OWNERSHIP_PATH, before, after: JSON.stringify(record, null, 2) + "\n" };
|
|
3208
|
+
}
|
|
3209
|
+
var OWNERSHIP_PATH, BLOCKS, fileSchema, ownershipSchema;
|
|
3210
|
+
var init_ownership = __esm({
|
|
3211
|
+
"src/setup/ownership.ts"() {
|
|
3133
3212
|
"use strict";
|
|
3134
|
-
init_storage();
|
|
3135
3213
|
init_evidence();
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3214
|
+
init_files2();
|
|
3215
|
+
OWNERSHIP_PATH = ".mason/local/integration.json";
|
|
3216
|
+
BLOCKS = [
|
|
3217
|
+
["<!-- mason:start -->", "<!-- mason:end -->"],
|
|
3218
|
+
["<!-- mason:agents:start -->", "<!-- mason:agents:end -->"],
|
|
3219
|
+
["# mason:mcp:start", "# mason:mcp:end"]
|
|
3220
|
+
];
|
|
3221
|
+
fileSchema = z10.object({
|
|
3222
|
+
created: z10.boolean(),
|
|
3223
|
+
initialHash: z10.string(),
|
|
3224
|
+
blocks: z10.record(z10.string()),
|
|
3225
|
+
mcpHash: z10.string().optional(),
|
|
3226
|
+
hookCommands: z10.array(z10.string()).optional(),
|
|
3227
|
+
hooks: z10.record(z10.array(z10.string())).optional()
|
|
3228
|
+
});
|
|
3229
|
+
ownershipSchema = z10.object({ version: z10.literal(1), files: z10.record(fileSchema) });
|
|
3142
3230
|
}
|
|
3143
3231
|
});
|
|
3144
3232
|
|
|
3145
3233
|
// src/mcp/init.ts
|
|
3146
3234
|
import { z as z11 } from "zod";
|
|
3147
|
-
async function loadProjectMarker(rootDir) {
|
|
3148
|
-
const raw = await readStoreJson(rootDir, ".mason/project.json");
|
|
3149
|
-
return raw === null ? null : markerSchema.parse(raw);
|
|
3150
|
-
}
|
|
3151
|
-
async function saveProjectMarker(rootDir, marker) {
|
|
3152
|
-
await writeStoreJson(rootDir, ".mason/project.json", markerSchema.parse(marker));
|
|
3153
|
-
}
|
|
3154
3235
|
var markerSchema, CLAUDE_MD_SECTION, ASSISTANT_SETUP, QUICKSTART_PLAYBOOK, MAP_PLAYBOOK;
|
|
3155
3236
|
var init_init = __esm({
|
|
3156
3237
|
"src/mcp/init.ts"() {
|
|
3157
3238
|
"use strict";
|
|
3158
3239
|
init_storage();
|
|
3240
|
+
init_files2();
|
|
3159
3241
|
markerSchema = z11.object({
|
|
3160
3242
|
version: z11.literal(1),
|
|
3161
3243
|
initializedAt: z11.string(),
|
|
3162
|
-
features: z11.object({ confluence: z11.boolean().optional() }).optional()
|
|
3244
|
+
features: z11.object({ confluence: z11.boolean().optional() }).passthrough().optional()
|
|
3163
3245
|
}).passthrough();
|
|
3164
3246
|
CLAUDE_MD_SECTION = `<!-- mason:start -->
|
|
3165
3247
|
## Mason project knowledge
|
|
@@ -3181,7 +3263,7 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
|
|
|
3181
3263
|
Inspect source for what the retrieved context does not answer.
|
|
3182
3264
|
<!-- mason:end -->`;
|
|
3183
3265
|
ASSISTANT_SETUP = `UNIFIED SETUP
|
|
3184
|
-
When the user requests Mason setup, call mason_init again with mode: "setup" and host: "codex" or "claude" for the assistant being configured. This uses the same engine as mason-auto setup: retain the initial audit before instruction edits,
|
|
3266
|
+
When the user requests Mason setup, call mason_init again with mode: "setup" and host: "codex" or "claude" for the assistant being configured. This uses the same engine as mason-auto setup: retain the initial audit before instruction edits, use the installed mason command on PATH, merge project MCP configuration and hooks, update marker-delimited instructions and Git ignore rules, and record setup. A concept map is optional.
|
|
3185
3267
|
If the user only requested inspection or review, report findings without running setup. The setup result distinguishes configured from observed activation. Explain the host's native trust step and request a new session; never trust hooks on the user's behalf or claim activation from generated configuration alone. After a normal task finishes, mason_automation(action: "status") or mason-auto status reports observed context use and hook events. Existing findings remain reviewable; setup does not approve advisories or manufacture decisions.
|
|
3186
3268
|
|
|
3187
3269
|
The managed project guidance is:
|
|
@@ -3189,7 +3271,7 @@ ${CLAUDE_MD_SECTION}`;
|
|
|
3189
3271
|
QUICKSTART_PLAYBOOK = `Start with the audit and review results included in this response. No concept map is required.
|
|
3190
3272
|
|
|
3191
3273
|
1. Explain the actionable findings with their source evidence. Separate audit issues, advisories, and skipped checks. The review covers committed changes from the merge base to HEAD; workingTree paths are not included in that review. An unavailable or empty check is not proof that the project is correct. Use the CLI for full output if a summary is truncated.
|
|
3192
|
-
2. Address findings within the user's requested scope. Setup alone authorizes
|
|
3274
|
+
2. Address findings within the user's requested scope. Setup alone authorizes Mason host configuration, hooks, instructions, and ignore rules; rewriting existing project claims requires repair scope. If repair is authorized, call \`mason_repair(dir, action: "prepare")\` before editing; it saves the full original findings even when this summary is truncated. Inspect relevant source, make grounded edits, then call \`mason_repair(dir, action: "verify", baselinePath)\` with that same baseline, including after any final doc commit. Report resolved, unresolved, review-required, unverified, and new findings. Keep suppressed advisories visible even when setup has already dirtied a doc. Do not invent a decision just to populate the store.
|
|
3193
3275
|
3. When the task reveals a real lesson or constraint, call \`save_decision\` with title, body, category, anchors, and known owner/source/actor information. Missing attribution can be added later. The tool writes a local proposal; editing it preserves revision history and requires a new acceptance, while any earlier accepted revision remains operative. An unchanged save never refreshes its evidence. When decision review is requested, \`review_decision\` prepares the record and code evidence before any authorized verdict. Review and commit records through the normal workflow. Retrieve it on the next relevant task with \`get_context(dir, task, files)\`.
|
|
3194
3276
|
|
|
3195
3277
|
${ASSISTANT_SETUP}
|
|
@@ -3231,14 +3313,506 @@ If a build is interrupted, its partials remain in \`.mason/partial-snapshots/\`.
|
|
|
3231
3313
|
}
|
|
3232
3314
|
});
|
|
3233
3315
|
|
|
3316
|
+
// src/utils/git.ts
|
|
3317
|
+
var git_exports = {};
|
|
3318
|
+
__export(git_exports, {
|
|
3319
|
+
isGitRepo: () => isGitRepo
|
|
3320
|
+
});
|
|
3321
|
+
import { execFile as execFile10 } from "child_process";
|
|
3322
|
+
import { promisify as promisify10 } from "util";
|
|
3323
|
+
async function isGitRepo(dir) {
|
|
3324
|
+
try {
|
|
3325
|
+
await exec8("git", ["rev-parse", "--git-dir"], { cwd: dir });
|
|
3326
|
+
return true;
|
|
3327
|
+
} catch {
|
|
3328
|
+
return false;
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
var exec8;
|
|
3332
|
+
var init_git2 = __esm({
|
|
3333
|
+
"src/utils/git.ts"() {
|
|
3334
|
+
"use strict";
|
|
3335
|
+
exec8 = promisify10(execFile10);
|
|
3336
|
+
}
|
|
3337
|
+
});
|
|
3338
|
+
|
|
3339
|
+
// src/setup/config.ts
|
|
3340
|
+
var config_exports = {};
|
|
3341
|
+
__export(config_exports, {
|
|
3342
|
+
ancillaryEdits: () => ancillaryEdits,
|
|
3343
|
+
hookEdits: () => hookEdits,
|
|
3344
|
+
inspectHostConfig: () => inspectHostConfig,
|
|
3345
|
+
instructionEdits: () => instructionEdits,
|
|
3346
|
+
mcpEdit: () => mcpEdit,
|
|
3347
|
+
mcpPath: () => mcpPath
|
|
3348
|
+
});
|
|
3349
|
+
import { isDeepStrictEqual } from "util";
|
|
3350
|
+
import { parse as parse2, stringify } from "smol-toml";
|
|
3351
|
+
import { z as z12 } from "zod";
|
|
3352
|
+
async function instructionEdits(root, host) {
|
|
3353
|
+
const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
|
|
3354
|
+
const existing = found.find((f) => f.text !== null);
|
|
3355
|
+
const primary = host === "codex" && found[0].text === null ? { path: "AGENTS.md", text: null } : existing ?? { path: "AGENTS.md", text: null };
|
|
3356
|
+
const originalGuidance = primary.text === null && existing ? "Follow the existing project conventions in " + existing.path + ".\n" : "";
|
|
3357
|
+
const edits = [{
|
|
3358
|
+
path: primary.path,
|
|
3359
|
+
before: primary.text,
|
|
3360
|
+
after: managedBlock(primary.text ?? originalGuidance, "<!-- mason:start -->", "<!-- mason:end -->", CLAUDE_MD_SECTION.split("\n").slice(1, -1).join("\n"))
|
|
3361
|
+
}];
|
|
3362
|
+
if (primary.path === "AGENTS.md") {
|
|
3363
|
+
const secondary = found.filter((f) => f.path !== "AGENTS.md" && f.text !== null);
|
|
3364
|
+
if (host === "claude" && !secondary.length) secondary.push({ path: "CLAUDE.md", text: null });
|
|
3365
|
+
for (const file of secondary) {
|
|
3366
|
+
const text2 = file.text ?? "";
|
|
3367
|
+
const legacy = text2.includes("<!-- mason:start -->") || text2.includes("<!-- mason:end -->");
|
|
3368
|
+
const imported = "@" + (file.path.startsWith(".claude/") ? "../AGENTS.md" : "AGENTS.md");
|
|
3369
|
+
if (text2.trim() === imported) continue;
|
|
3370
|
+
const pointer = "Mason project knowledge and shared project instructions:\n" + imported;
|
|
3371
|
+
edits.push({
|
|
3372
|
+
path: file.path,
|
|
3373
|
+
before: file.text,
|
|
3374
|
+
after: managedBlock(text2, legacy ? "<!-- mason:start -->" : POINTER_START, legacy ? "<!-- mason:end -->" : POINTER_END, pointer)
|
|
3375
|
+
});
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
return edits;
|
|
3379
|
+
}
|
|
3380
|
+
function managedMcp(previous, host) {
|
|
3381
|
+
const options = previous === void 0 ? {} : object.parse(previous);
|
|
3382
|
+
for (const key of ["command", "args", "url", "type", "headers", "http_headers", "env_http_headers", "bearer_token_env_var"]) delete options[key];
|
|
3383
|
+
return { ...options, ...mcpCommand(host) };
|
|
3384
|
+
}
|
|
3385
|
+
async function mcpEdit(root, host) {
|
|
3386
|
+
const file = mcpPath(host), before = await readText(root, file);
|
|
3387
|
+
if (host === "claude") {
|
|
3388
|
+
const config2 = before === null ? {} : object.parse(JSON.parse(before));
|
|
3389
|
+
const servers2 = object.parse(config2.mcpServers ?? {});
|
|
3390
|
+
return { path: file, before, after: JSON.stringify({ ...config2, mcpServers: { ...servers2, mason: managedMcp(servers2.mason, host) } }, null, 2) + "\n" };
|
|
3391
|
+
}
|
|
3392
|
+
const config = parse2(before ?? "");
|
|
3393
|
+
const managed = (before ?? "").includes(TOML_START);
|
|
3394
|
+
const servers = config.mcp_servers;
|
|
3395
|
+
const desired = managedMcp(servers?.mason, host);
|
|
3396
|
+
let base = before ?? "";
|
|
3397
|
+
if (servers?.mason && !managed) {
|
|
3398
|
+
const headers = [...base.matchAll(/^\s*\[\[?.+?\]\]?[^\S\r\n]*(?:#.*)?$/gm)];
|
|
3399
|
+
const spans = headers.flatMap((header, index2) => /^\s*\[mcp_servers\.mason(?:\.[A-Za-z0-9_-]+)*\]/.test(header[0]) ? [{ start: header.index, end: headers[index2 + 1]?.index ?? base.length }] : []);
|
|
3400
|
+
if (!spans.length) throw new Error("Cannot safely migrate the existing Mason MCP TOML entry. Use an explicit [mcp_servers.mason] table and rerun setup; the file was retained.");
|
|
3401
|
+
for (const span of spans.reverse()) base = base.slice(0, span.start) + base.slice(span.end);
|
|
3402
|
+
}
|
|
3403
|
+
const after = managedBlock(base, TOML_START, TOML_END, stringify({ mcp_servers: { mason: desired } }).trimEnd());
|
|
3404
|
+
const parsed = parse2(after);
|
|
3405
|
+
const expected = { ...config, mcp_servers: { ...servers ?? {}, mason: desired } };
|
|
3406
|
+
if (!isDeepStrictEqual(parsed, expected)) throw new Error("Could not safely configure the Mason MCP entry without changing other settings.");
|
|
3407
|
+
return { path: file, before, after };
|
|
3408
|
+
}
|
|
3409
|
+
async function hookEdits(root, host) {
|
|
3410
|
+
const file = configPath(host), before = await readText(root, file);
|
|
3411
|
+
const { planAutomationInstall: planAutomationInstall2 } = await Promise.resolve().then(() => (init_install(), install_exports));
|
|
3412
|
+
const plan = await planAutomationInstall2(root, host, hookCommand(host));
|
|
3413
|
+
return [
|
|
3414
|
+
{ path: file, before, after: JSON.stringify(plan.config, null, 2) + "\n" },
|
|
3415
|
+
{ path: AUTOMATION_PATH, before: await readText(root, AUTOMATION_PATH), after: JSON.stringify(plan.record, null, 2) + "\n" }
|
|
3416
|
+
];
|
|
3417
|
+
}
|
|
3418
|
+
async function ancillaryEdits(root) {
|
|
3419
|
+
const ignore = await readText(root, ".gitignore");
|
|
3420
|
+
const { git: git3 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
|
|
3421
|
+
const { isGitRepo: isGitRepo2 } = await Promise.resolve().then(() => (init_git2(), git_exports));
|
|
3422
|
+
let parentIgnored = false;
|
|
3423
|
+
try {
|
|
3424
|
+
if (await isGitRepo2(root)) parentIgnored = !!(await git3(root, "check-ignore", "--no-index", ".mason")).trim();
|
|
3425
|
+
} catch (error) {
|
|
3426
|
+
if (error.code !== 1) throw error;
|
|
3427
|
+
}
|
|
3428
|
+
const retainParentRule = parentIgnored || !!ignore?.replace(/\r\n/g, "\n").includes("# mason:ignore:start\n!/.mason/\n/.mason/*");
|
|
3429
|
+
const rules = [
|
|
3430
|
+
...retainParentRule ? ["!/.mason/", "/.mason/*"] : [],
|
|
3431
|
+
"!/.mason/decisions/",
|
|
3432
|
+
"!/.mason/decisions/**",
|
|
3433
|
+
"!/.mason/config.json",
|
|
3434
|
+
"!/.mason/snapshot.json",
|
|
3435
|
+
"/.mason/local/",
|
|
3436
|
+
"/.mason/reports/"
|
|
3437
|
+
].join("\n");
|
|
3438
|
+
return [{ path: ".gitignore", before: ignore, after: managedBlock(ignore ?? "", "# mason:ignore:start", "# mason:ignore:end", rules) }];
|
|
3439
|
+
}
|
|
3440
|
+
async function inspectHostConfig(root, host, plannedMcp) {
|
|
3441
|
+
const text2 = plannedMcp ?? await readText(root, mcpPath(host));
|
|
3442
|
+
const config = host === "codex" ? parse2(text2 ?? "") : object.parse(JSON.parse(text2 ?? "{}"));
|
|
3443
|
+
const servers = host === "codex" ? config.mcp_servers : config.mcpServers;
|
|
3444
|
+
const hooks = automationConfigSchema.parse(JSON.parse(await readText(root, configPath(host)) ?? "{}"));
|
|
3445
|
+
const expected = hookConfig(host, hookCommand(host));
|
|
3446
|
+
const mcp = servers?.mason === void 0 ? null : object.parse(servers.mason);
|
|
3447
|
+
return {
|
|
3448
|
+
mcp,
|
|
3449
|
+
mcpDisabled: mcp?.enabled === false,
|
|
3450
|
+
hooks: Object.fromEntries(Object.keys(expected.hooks).map((event) => [
|
|
3451
|
+
event,
|
|
3452
|
+
(hooks.hooks?.[event] ?? []).flatMap((group) => group.hooks.filter((handler) => handler.command === expected.hooks.SessionStart[0].hooks[0].command).map((handler) => ({ ...group, hooks: [handler] })) ?? [])
|
|
3453
|
+
])),
|
|
3454
|
+
disabled: hooks.disableAllHooks === true || config.features?.hooks === false || config.features?.codex_hooks === false
|
|
3455
|
+
};
|
|
3456
|
+
}
|
|
3457
|
+
var object, mcpPath, TOML_START, TOML_END, POINTER_START, POINTER_END;
|
|
3458
|
+
var init_config = __esm({
|
|
3459
|
+
"src/setup/config.ts"() {
|
|
3460
|
+
"use strict";
|
|
3461
|
+
init_init();
|
|
3462
|
+
init_docs();
|
|
3463
|
+
init_adapters();
|
|
3464
|
+
init_install();
|
|
3465
|
+
init_files2();
|
|
3466
|
+
init_launcher();
|
|
3467
|
+
object = z12.record(z12.unknown());
|
|
3468
|
+
mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
|
|
3469
|
+
TOML_START = "# mason:mcp:start";
|
|
3470
|
+
TOML_END = "# mason:mcp:end";
|
|
3471
|
+
POINTER_START = "<!-- mason:agents:start -->";
|
|
3472
|
+
POINTER_END = "<!-- mason:agents:end -->";
|
|
3473
|
+
}
|
|
3474
|
+
});
|
|
3475
|
+
|
|
3476
|
+
// src/automation/install.ts
|
|
3477
|
+
var install_exports = {};
|
|
3478
|
+
__export(install_exports, {
|
|
3479
|
+
AUTOMATION_PATH: () => AUTOMATION_PATH,
|
|
3480
|
+
automationConfigSchema: () => automationConfigSchema,
|
|
3481
|
+
configPath: () => configPath,
|
|
3482
|
+
installAutomation: () => installAutomation,
|
|
3483
|
+
installedAutomation: () => installedAutomation,
|
|
3484
|
+
planAutomationInstall: () => planAutomationInstall,
|
|
3485
|
+
recordSchema: () => recordSchema
|
|
3486
|
+
});
|
|
3487
|
+
import { z as z13 } from "zod";
|
|
3488
|
+
async function installationRecord(root) {
|
|
3489
|
+
return recordSchema.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
|
|
3490
|
+
}
|
|
3491
|
+
async function installAutomation(dir, host, command) {
|
|
3492
|
+
const ws = await workspace(dir);
|
|
3493
|
+
return withLock(ws.root, ".mason/reports/setup-lock", () => installLocked(ws.root, host, command));
|
|
3494
|
+
}
|
|
3495
|
+
async function planAutomationInstall(root, host, command) {
|
|
3496
|
+
const file = configPath(host);
|
|
3497
|
+
const before = await readText(root, file);
|
|
3498
|
+
const existing = automationConfigSchema.parse(JSON.parse(before ?? "{}"));
|
|
3499
|
+
const record = await installationRecord(root);
|
|
3500
|
+
const desired = hookConfig(host, command);
|
|
3501
|
+
const newCommand = desired.hooks.SessionStart[0].hooks[0].command;
|
|
3502
|
+
const previous = record.hosts[host]?.command;
|
|
3503
|
+
const ownershipText = await readText(root, OWNERSHIP_PATH);
|
|
3504
|
+
const pending = ownershipText === null ? [] : ownershipSchema.parse(JSON.parse(ownershipText)).files[file]?.hookCommands ?? [];
|
|
3505
|
+
const owned = /* @__PURE__ */ new Set([previous, ...pending, newCommand]);
|
|
3506
|
+
const hooks = existing.hooks ?? {};
|
|
3507
|
+
for (const event of HOOK_EVENTS) {
|
|
3508
|
+
hooks[event] = (hooks[event] ?? []).map((group) => ({
|
|
3509
|
+
...group,
|
|
3510
|
+
hooks: group.hooks.filter((handler) => !(handler.type === "command" && typeof handler.command === "string" && owned.has(handler.command)))
|
|
3511
|
+
})).filter((group) => group.hooks.length);
|
|
3512
|
+
hooks[event].push(...desired.hooks[event]);
|
|
3513
|
+
}
|
|
3514
|
+
record.hosts[host] = { command: newCommand };
|
|
3515
|
+
return { file, before, config: { ...existing, hooks }, record, newCommand };
|
|
3516
|
+
}
|
|
3517
|
+
async function installLocked(root, host, command) {
|
|
3518
|
+
const { file, before, config, record, newCommand } = await planAutomationInstall(root, host, command);
|
|
3519
|
+
const { ancillaryEdits: ancillaryEdits2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
3520
|
+
const { applyEdit: applyEdit2 } = await Promise.resolve().then(() => (init_files2(), files_exports));
|
|
3521
|
+
const { ownershipEdit: ownershipEdit2, completedHookOwnership: completedHookOwnership2 } = await Promise.resolve().then(() => (init_ownership(), ownership_exports));
|
|
3522
|
+
const edit = { path: file, before, after: JSON.stringify(config, null, 2) + "\n" };
|
|
3523
|
+
const ancillary = await ancillaryEdits2(root);
|
|
3524
|
+
await applyEdit2(root, await ownershipEdit2(root, [edit], { [file]: newCommand }));
|
|
3525
|
+
for (const edit2 of ancillary) await applyEdit2(root, edit2);
|
|
3526
|
+
await applyEdit2(root, edit);
|
|
3527
|
+
await writeStoreJson(root, AUTOMATION_PATH, record);
|
|
3528
|
+
await applyEdit2(root, await completedHookOwnership2(root, file, newCommand));
|
|
3529
|
+
return {
|
|
3530
|
+
version: 1,
|
|
3531
|
+
host,
|
|
3532
|
+
configPath: file,
|
|
3533
|
+
status: "configured",
|
|
3534
|
+
command: newCommand,
|
|
3535
|
+
events: HOOK_EVENTS,
|
|
3536
|
+
next: host === "codex" ? "Review/trust these hooks using Codex /hooks and start a new session. mason-auto status reports observed events separately from configuration." : "Start a new Claude Code session. mason-auto status reports observed events separately from configuration.",
|
|
3537
|
+
note: "Install mason-context in the project before using the default command. Ignore .mason/reports/ to keep local evidence out of commits. Hooks preserve evidence and suggest scoped repairs; they do not approve edits or decisions."
|
|
3538
|
+
};
|
|
3539
|
+
}
|
|
3540
|
+
async function installedAutomation(dir) {
|
|
3541
|
+
const ws = await workspace(dir);
|
|
3542
|
+
const record = await installationRecord(ws.root);
|
|
3543
|
+
const result = {};
|
|
3544
|
+
for (const host of ["claude", "codex"]) {
|
|
3545
|
+
const expected = record.hosts[host];
|
|
3546
|
+
if (!expected) continue;
|
|
3547
|
+
const current = automationConfigSchema.parse(await readStoreJson(ws.root, configPath(host)) ?? {});
|
|
3548
|
+
const configuredEvents = HOOK_EVENTS.filter((event) => current.hooks?.[event]?.some((group) => group.hooks.some((handler) => handler.type === "command" && handler.command === expected.command)));
|
|
3549
|
+
result[host] = {
|
|
3550
|
+
configPath: configPath(host),
|
|
3551
|
+
configuredEvents,
|
|
3552
|
+
status: current.disableAllHooks === true ? "disabled" : configuredEvents.length === HOOK_EVENTS.length ? "configured" : "incomplete",
|
|
3553
|
+
runtime: "Host version, trust, policy, and tool coverage still determine execution; inspect observed events."
|
|
3554
|
+
};
|
|
3555
|
+
}
|
|
3556
|
+
return result;
|
|
3557
|
+
}
|
|
3558
|
+
var groupSchema, automationConfigSchema, recordSchema, AUTOMATION_PATH, configPath;
|
|
3559
|
+
var init_install = __esm({
|
|
3560
|
+
"src/automation/install.ts"() {
|
|
3561
|
+
"use strict";
|
|
3562
|
+
init_storage();
|
|
3563
|
+
init_evidence();
|
|
3564
|
+
init_adapters();
|
|
3565
|
+
init_store();
|
|
3566
|
+
init_files2();
|
|
3567
|
+
init_ownership();
|
|
3568
|
+
groupSchema = z13.object({ hooks: z13.array(z13.object({ type: z13.string(), command: z13.string().optional() }).passthrough()) }).passthrough();
|
|
3569
|
+
automationConfigSchema = z13.object({ hooks: z13.record(z13.array(groupSchema)).optional() }).passthrough();
|
|
3570
|
+
recordSchema = z13.object({ version: z13.literal(1), hosts: z13.record(z13.object({ command: z13.string() })) });
|
|
3571
|
+
AUTOMATION_PATH = ".mason/local/automation.json";
|
|
3572
|
+
configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
|
|
3573
|
+
}
|
|
3574
|
+
});
|
|
3575
|
+
|
|
3576
|
+
// src/setup/teardown.ts
|
|
3577
|
+
var teardown_exports = {};
|
|
3578
|
+
__export(teardown_exports, {
|
|
3579
|
+
summarizeTeardown: () => summarizeTeardown,
|
|
3580
|
+
teardownProject: () => teardownProject
|
|
3581
|
+
});
|
|
3582
|
+
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
3583
|
+
import { parse as parse3, stringify as stringify2 } from "smol-toml";
|
|
3584
|
+
import fg5 from "fast-glob";
|
|
3585
|
+
import { z as z14 } from "zod";
|
|
3586
|
+
async function planTeardown(root, selected) {
|
|
3587
|
+
const edits = [], diagnostics = [];
|
|
3588
|
+
const inputs = /* @__PURE__ */ new Map();
|
|
3589
|
+
const read = async (file) => {
|
|
3590
|
+
if (!inputs.has(file)) inputs.set(file, await readText(root, file));
|
|
3591
|
+
return inputs.get(file);
|
|
3592
|
+
};
|
|
3593
|
+
const attempt = async (file, action) => {
|
|
3594
|
+
try {
|
|
3595
|
+
await action();
|
|
3596
|
+
} catch (error) {
|
|
3597
|
+
diagnostics.push({ path: file, message: error instanceof Error ? error.message : String(error) });
|
|
3598
|
+
}
|
|
3599
|
+
};
|
|
3600
|
+
const ownershipBefore = await read(OWNERSHIP_PATH);
|
|
3601
|
+
const ownership = ownershipBefore === null ? { version: 1, files: {} } : ownershipSchema.parse(JSON.parse(ownershipBefore));
|
|
3602
|
+
const setupBefore = await read(SETUP_PATH);
|
|
3603
|
+
const setup = setupBefore === null ? null : setupSchema.parse(JSON.parse(setupBefore));
|
|
3604
|
+
const automationBefore = await read(AUTOMATION_PATH);
|
|
3605
|
+
const automation = automationBefore === null ? null : recordSchema.parse(JSON.parse(automationBefore));
|
|
3606
|
+
const commands = (host) => new Set([
|
|
3607
|
+
automation?.hosts[host]?.command,
|
|
3608
|
+
...ownership.files[configPath(host)]?.hookCommands ?? [],
|
|
3609
|
+
hookConfig(host, hookCommand(host)).hooks.SessionStart[0].hooks[0].command,
|
|
3610
|
+
hookConfig(host).hooks.SessionStart[0].hooks[0].command
|
|
3611
|
+
].filter((command) => !!command));
|
|
3612
|
+
const possibleHook = (command, host) => commands(host).has(command) || /\bmason(?:-auto|-hook)?\b/.test(command);
|
|
3613
|
+
const add = (file, before, after, empty) => {
|
|
3614
|
+
edits.push({ path: file, before, after: ownership.files[file]?.created && (empty || ownership.files[file].initialHash === hash(before)) ? null : after });
|
|
3615
|
+
};
|
|
3616
|
+
let remainingHost = hosts.some((host) => !selected.includes(host) && (setup?.hosts[host] || automation?.hosts[host] || ownership.files[mcpPath(host)] || ownership.files[configPath(host)]));
|
|
3617
|
+
for (const host of hosts.filter((host2) => !selected.includes(host2))) {
|
|
3618
|
+
await attempt(mcpPath(host), async () => {
|
|
3619
|
+
const text2 = await read(mcpPath(host));
|
|
3620
|
+
const config = host === "codex" ? parse3(text2 ?? "") : object2.parse(JSON.parse(text2 ?? "{}"));
|
|
3621
|
+
const servers = object2.parse((host === "codex" ? config.mcp_servers : config.mcpServers) ?? {});
|
|
3622
|
+
remainingHost ||= servers.mason !== void 0;
|
|
3623
|
+
});
|
|
3624
|
+
await attempt(configPath(host), async () => {
|
|
3625
|
+
const config = automationConfigSchema.parse(JSON.parse(await read(configPath(host)) ?? "{}"));
|
|
3626
|
+
remainingHost ||= Object.values(config.hooks ?? {}).some((groups) => groups.some((group) => group.hooks.some((handler) => handler.command && possibleHook(handler.command, host))));
|
|
3627
|
+
});
|
|
3628
|
+
}
|
|
3629
|
+
for (const host of selected) {
|
|
3630
|
+
const mcpFile = mcpPath(host);
|
|
3631
|
+
await attempt(mcpFile, async () => {
|
|
3632
|
+
const before = await read(mcpFile);
|
|
3633
|
+
if (before === null) return;
|
|
3634
|
+
const config = host === "codex" ? parse3(before) : object2.parse(JSON.parse(before));
|
|
3635
|
+
const key = host === "codex" ? "mcp_servers" : "mcpServers";
|
|
3636
|
+
const servers = object2.parse(config[key] ?? {});
|
|
3637
|
+
if (servers.mason === void 0) {
|
|
3638
|
+
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.");
|
|
3639
|
+
return;
|
|
3640
|
+
}
|
|
3641
|
+
const expected = ownership.files[mcpFile]?.mcpHash ?? hash(mcpCommand(host));
|
|
3642
|
+
if (hash(servers.mason) !== expected) throw new Error("The Mason MCP entry was edited or its ownership is uncertain; retained for manual cleanup.");
|
|
3643
|
+
delete servers.mason;
|
|
3644
|
+
const desired = { ...config, [key]: servers };
|
|
3645
|
+
if (!Object.keys(servers).length) delete desired[key];
|
|
3646
|
+
if (host === "claude") {
|
|
3647
|
+
add(mcpFile, before, json(desired), !Object.keys(desired).length);
|
|
3648
|
+
return;
|
|
3649
|
+
}
|
|
3650
|
+
const [start, end] = BLOCKS[2], block = blockSpan(before, start, end);
|
|
3651
|
+
if (!block) throw new Error("Mason MCP markers are missing; retained for manual cleanup.");
|
|
3652
|
+
const expectedBlock = ownership.files[mcpFile]?.blocks[start];
|
|
3653
|
+
const canonicalBlock = managedBlock("", start, end, stringify2({ mcp_servers: { mason: mcpCommand(host) } }).trimEnd()).trimEnd();
|
|
3654
|
+
if (expectedBlock ? hash(block.text) !== expectedBlock : block.text.replace(/\r\n/g, "\n") !== canonicalBlock) {
|
|
3655
|
+
throw new Error("The Mason MCP block was edited; retained for manual cleanup.");
|
|
3656
|
+
}
|
|
3657
|
+
const after = before.slice(0, block.from) + before.slice(block.to).replace(/^\r?\n/, "");
|
|
3658
|
+
const parsed = parse3(after);
|
|
3659
|
+
if (!Object.keys(servers).length && parsed[key] !== void 0) desired[key] = {};
|
|
3660
|
+
if (!isDeepStrictEqual2(parsed, desired)) throw new Error("Removing the marked block would change other TOML settings; retained for manual cleanup.");
|
|
3661
|
+
add(mcpFile, before, after, !after.trim());
|
|
3662
|
+
});
|
|
3663
|
+
const hookFile = configPath(host);
|
|
3664
|
+
await attempt(hookFile, async () => {
|
|
3665
|
+
const before = await read(hookFile);
|
|
3666
|
+
if (before === null) return;
|
|
3667
|
+
const config = automationConfigSchema.parse(JSON.parse(before));
|
|
3668
|
+
let changed = false;
|
|
3669
|
+
for (const [event, groups] of Object.entries(config.hooks ?? {})) {
|
|
3670
|
+
const known = ownership.files[hookFile]?.hooks?.[event];
|
|
3671
|
+
const handlers = groups.flatMap((group) => group.hooks);
|
|
3672
|
+
if (known && !handlers.some((handler) => handler.type === "command" && handler.command && commands(host).has(handler.command)) && handlers.some((handler) => !known.includes(hash(handler)))) {
|
|
3673
|
+
diagnostics.push({ path: hookFile, message: "A recorded Mason hook is missing beside a new or edited handler (" + event + "); inspect ownership manually." });
|
|
3674
|
+
}
|
|
3675
|
+
const kept = groups.flatMap((group) => {
|
|
3676
|
+
const handlers2 = group.hooks.filter((handler) => {
|
|
3677
|
+
if (handler.type === "command" && handler.command && commands(host).has(handler.command)) {
|
|
3678
|
+
changed = true;
|
|
3679
|
+
return false;
|
|
3680
|
+
}
|
|
3681
|
+
if (handler.command && possibleHook(handler.command, host)) diagnostics.push({
|
|
3682
|
+
path: hookFile,
|
|
3683
|
+
message: "An unrecognized Mason hook was retained for manual cleanup (" + event + ")."
|
|
3684
|
+
});
|
|
3685
|
+
return true;
|
|
3686
|
+
});
|
|
3687
|
+
return handlers2.length === group.hooks.length ? [group] : handlers2.length ? [{ ...group, hooks: handlers2 }] : [];
|
|
3688
|
+
});
|
|
3689
|
+
if (kept.length || !groups.length) config.hooks[event] = kept;
|
|
3690
|
+
else delete config.hooks[event];
|
|
3691
|
+
}
|
|
3692
|
+
if (!changed) return;
|
|
3693
|
+
if (!Object.keys(config.hooks ?? {}).length) delete config.hooks;
|
|
3694
|
+
add(hookFile, before, json(config), !Object.keys(config).length);
|
|
3695
|
+
});
|
|
3696
|
+
}
|
|
3697
|
+
const instructionEditStart = edits.length;
|
|
3698
|
+
if (!remainingHost && !diagnostics.length) for (const file of DOC_CANDIDATES) await attempt(file, async () => {
|
|
3699
|
+
const before = await read(file);
|
|
3700
|
+
if (before === null) return;
|
|
3701
|
+
let after = before;
|
|
3702
|
+
for (const [start, end] of BLOCKS.slice(0, 2)) {
|
|
3703
|
+
const block = blockSpan(after, start, end);
|
|
3704
|
+
if (!block) continue;
|
|
3705
|
+
const pointer = "Mason project knowledge and shared project instructions:\n@" + (file.startsWith(".claude/") ? "../AGENTS.md" : "AGENTS.md");
|
|
3706
|
+
const canonical = [CLAUDE_MD_SECTION, managedBlock("", start, end, pointer).trimEnd()];
|
|
3707
|
+
const expected = ownership.files[file]?.blocks[start];
|
|
3708
|
+
if (expected ? hash(block.text) !== expected : !canonical.includes(block.text.replace(/\r\n/g, "\n"))) {
|
|
3709
|
+
throw new Error("The Mason instruction block was edited or is unrecognized; retained for manual cleanup.");
|
|
3710
|
+
}
|
|
3711
|
+
after = after.slice(0, block.from) + after.slice(block.to).replace(/^\r?\n/, "");
|
|
3712
|
+
}
|
|
3713
|
+
if (after !== before) add(file, before, after, !after.trim());
|
|
3714
|
+
});
|
|
3715
|
+
if (diagnostics.length) edits.splice(instructionEditStart);
|
|
3716
|
+
if (!diagnostics.length) {
|
|
3717
|
+
const cleanup = [];
|
|
3718
|
+
const receiptFiles = await fg5(
|
|
3719
|
+
selected.map((host) => ".mason/reports/automation/*/setup-" + host + ".json"),
|
|
3720
|
+
{ cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false }
|
|
3721
|
+
);
|
|
3722
|
+
if (receiptFiles.length > 1e3) throw new Error("Too many setup receipts; inspect local setup history before teardown.");
|
|
3723
|
+
for (const file of receiptFiles.sort()) await attempt(file, async () => {
|
|
3724
|
+
const host = file.endsWith("setup-codex.json") ? "codex" : "claude";
|
|
3725
|
+
await loadSetupReceipt(root, file.slice(0, file.lastIndexOf("/")), host);
|
|
3726
|
+
cleanup.push({ path: file, before: await read(file), after: null });
|
|
3727
|
+
});
|
|
3728
|
+
if (!remainingHost) await attempt(".mason/local/project.json", async () => {
|
|
3729
|
+
const before = await read(".mason/local/project.json");
|
|
3730
|
+
if (before === null) return;
|
|
3731
|
+
z14.object({ version: z14.literal(1), initializedAt: z14.string() }).strict().parse(JSON.parse(before));
|
|
3732
|
+
cleanup.push({ path: ".mason/local/project.json", before, after: null });
|
|
3733
|
+
});
|
|
3734
|
+
if (!diagnostics.length) {
|
|
3735
|
+
for (const host of selected) {
|
|
3736
|
+
if (setup) delete setup.hosts[host];
|
|
3737
|
+
if (automation) delete automation.hosts[host];
|
|
3738
|
+
delete ownership.files[mcpPath(host)];
|
|
3739
|
+
delete ownership.files[configPath(host)];
|
|
3740
|
+
}
|
|
3741
|
+
if (!remainingHost) for (const file of DOC_CANDIDATES) delete ownership.files[file];
|
|
3742
|
+
if (setupBefore !== null) cleanup.push({ path: SETUP_PATH, before: setupBefore, after: Object.keys(setup.hosts).length ? json(setup) : null });
|
|
3743
|
+
if (automationBefore !== null) cleanup.push({ path: AUTOMATION_PATH, before: automationBefore, after: Object.keys(automation.hosts).length ? json(automation) : null });
|
|
3744
|
+
if (ownershipBefore !== null) cleanup.push({ path: OWNERSHIP_PATH, before: ownershipBefore, after: Object.keys(ownership.files).length ? json(ownership) : null });
|
|
3745
|
+
edits.push(...cleanup);
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
return { edits: edits.filter((edit) => edit.before !== edit.after), diagnostics, remainingHost, inputs };
|
|
3749
|
+
}
|
|
3750
|
+
async function teardownProject(dir, options = {}) {
|
|
3751
|
+
const { root } = await workspace(dir);
|
|
3752
|
+
const selected = options.host ? [options.host] : [...hosts];
|
|
3753
|
+
const run = async (apply) => {
|
|
3754
|
+
const plan = await planTeardown(root, selected);
|
|
3755
|
+
if (apply) {
|
|
3756
|
+
for (const [file, before] of plan.inputs) if (await readText(root, file) !== before) throw new Error("Input changed during teardown: " + file + ". Rerun teardown.");
|
|
3757
|
+
for (const edit of plan.edits) await applyEdit(root, edit);
|
|
3758
|
+
}
|
|
3759
|
+
return {
|
|
3760
|
+
version: 1,
|
|
3761
|
+
action: "teardown",
|
|
3762
|
+
status: plan.diagnostics.length ? "incomplete" : "complete",
|
|
3763
|
+
root,
|
|
3764
|
+
hosts: selected,
|
|
3765
|
+
dryRun: !!options.dryRun,
|
|
3766
|
+
changes: plan.edits.map((edit) => ({ path: edit.path, action: edit.after === null ? "delete" : "update" })),
|
|
3767
|
+
diagnostics: plan.diagnostics,
|
|
3768
|
+
sharedInstructionsRetained: plan.remainingHost || !!plan.diagnostics.length,
|
|
3769
|
+
next: plan.diagnostics.length ? "Inspect the retained entries, remove or correct them, then rerun teardown." : "Restart running assistants to unload the project integration. Run mason setup --host codex or --host claude to reconnect."
|
|
3770
|
+
};
|
|
3771
|
+
};
|
|
3772
|
+
const preview = await run(false);
|
|
3773
|
+
if (options.dryRun || !preview.changes.length) return preview;
|
|
3774
|
+
return withLock(root, ".mason/reports/setup-lock", () => run(true));
|
|
3775
|
+
}
|
|
3776
|
+
function summarizeTeardown(result) {
|
|
3777
|
+
return [
|
|
3778
|
+
`Mason teardown${result.dryRun ? " preview" : ""}: ${result.status}.`,
|
|
3779
|
+
...result.changes.map((change) => ` ${result.dryRun ? "Would " : ""}${change.action}: ${change.path}`),
|
|
3780
|
+
...result.diagnostics.map((item) => ` Retained ${item.path}: ${item.message}`),
|
|
3781
|
+
...!result.changes.length ? ["No changes."] : [],
|
|
3782
|
+
"Shared knowledge, repair evidence, ignore rules and the installed Mason command are retained.",
|
|
3783
|
+
...result.sharedInstructionsRetained ? ["Shared instruction cleanup is deferred while another host or incomplete removal needs inspection."] : [],
|
|
3784
|
+
...result.dryRun ? ["Preview only; no files were changed."] : [result.next]
|
|
3785
|
+
].join("\n");
|
|
3786
|
+
}
|
|
3787
|
+
var hosts, object2, json;
|
|
3788
|
+
var init_teardown = __esm({
|
|
3789
|
+
"src/setup/teardown.ts"() {
|
|
3790
|
+
"use strict";
|
|
3791
|
+
init_evidence();
|
|
3792
|
+
init_store();
|
|
3793
|
+
init_install();
|
|
3794
|
+
init_adapters();
|
|
3795
|
+
init_docs();
|
|
3796
|
+
init_init();
|
|
3797
|
+
init_files2();
|
|
3798
|
+
init_config();
|
|
3799
|
+
init_launcher();
|
|
3800
|
+
init_model();
|
|
3801
|
+
init_ownership();
|
|
3802
|
+
hosts = ["codex", "claude"];
|
|
3803
|
+
object2 = z14.record(z14.unknown());
|
|
3804
|
+
json = (value) => JSON.stringify(value, null, 2) + "\n";
|
|
3805
|
+
}
|
|
3806
|
+
});
|
|
3807
|
+
|
|
3234
3808
|
// src/review/cochange.ts
|
|
3235
|
-
import { execFile as
|
|
3236
|
-
import { promisify as
|
|
3809
|
+
import { execFile as execFile11 } from "child_process";
|
|
3810
|
+
import { promisify as promisify11 } from "util";
|
|
3237
3811
|
async function buildMatrix(resolvedRoot) {
|
|
3238
3812
|
try {
|
|
3239
|
-
const { stdout: shallow } = await
|
|
3813
|
+
const { stdout: shallow } = await exec9("git", ["rev-parse", "--is-shallow-repository"], { cwd: resolvedRoot });
|
|
3240
3814
|
if (shallow.trim() === "true") return null;
|
|
3241
|
-
const { stdout } = await
|
|
3815
|
+
const { stdout } = await exec9(
|
|
3242
3816
|
"git",
|
|
3243
3817
|
["log", `-n${HISTORY_COMMITS}`, "--format=%x01", "--name-only", "-M"],
|
|
3244
3818
|
{ cwd: resolvedRoot, maxBuffer: 100 * 1024 * 1024 }
|
|
@@ -3294,11 +3868,11 @@ async function findMissingPartners(resolvedRoot, changedFiles, existsOnDisk, all
|
|
|
3294
3868
|
findings.sort((a, b) => b.rate - a.rate || b.sharedCommits - a.sharedCommits);
|
|
3295
3869
|
return findings;
|
|
3296
3870
|
}
|
|
3297
|
-
var
|
|
3871
|
+
var exec9, HISTORY_COMMITS, MIN_FILE_COMMITS, MIN_SHARED_COMMITS, MIN_COCHANGE_RATE, MAX_COMMIT_FILES;
|
|
3298
3872
|
var init_cochange = __esm({
|
|
3299
3873
|
"src/review/cochange.ts"() {
|
|
3300
3874
|
"use strict";
|
|
3301
|
-
|
|
3875
|
+
exec9 = promisify11(execFile11);
|
|
3302
3876
|
HISTORY_COMMITS = 1500;
|
|
3303
3877
|
MIN_FILE_COMMITS = 5;
|
|
3304
3878
|
MIN_SHARED_COMMITS = 4;
|
|
@@ -3353,7 +3927,7 @@ var init_types2 = __esm({
|
|
|
3353
3927
|
});
|
|
3354
3928
|
|
|
3355
3929
|
// src/review/evidence/vitest.ts
|
|
3356
|
-
import { z as
|
|
3930
|
+
import { z as z15 } from "zod";
|
|
3357
3931
|
function parseVitest(raw, sourceRoot) {
|
|
3358
3932
|
const report = schema.parse(raw);
|
|
3359
3933
|
const findings = [], diagnostics = [];
|
|
@@ -3402,24 +3976,24 @@ var init_vitest = __esm({
|
|
|
3402
3976
|
"use strict";
|
|
3403
3977
|
init_paths2();
|
|
3404
3978
|
init_types2();
|
|
3405
|
-
count2 =
|
|
3406
|
-
schema =
|
|
3407
|
-
success:
|
|
3979
|
+
count2 = z15.number().int().nonnegative();
|
|
3980
|
+
schema = z15.object({
|
|
3981
|
+
success: z15.boolean(),
|
|
3408
3982
|
numTotalTests: count2,
|
|
3409
3983
|
numPassedTests: count2,
|
|
3410
3984
|
numFailedTests: count2,
|
|
3411
3985
|
numPendingTests: count2,
|
|
3412
3986
|
numTodoTests: count2,
|
|
3413
3987
|
numFailedTestSuites: count2,
|
|
3414
|
-
testResults:
|
|
3415
|
-
name:
|
|
3416
|
-
status:
|
|
3417
|
-
message:
|
|
3418
|
-
assertionResults:
|
|
3419
|
-
fullName:
|
|
3420
|
-
status:
|
|
3421
|
-
failureMessages:
|
|
3422
|
-
location:
|
|
3988
|
+
testResults: z15.array(z15.object({
|
|
3989
|
+
name: z15.string().min(1),
|
|
3990
|
+
status: z15.enum(["passed", "failed"]),
|
|
3991
|
+
message: z15.string().optional(),
|
|
3992
|
+
assertionResults: z15.array(z15.object({
|
|
3993
|
+
fullName: z15.string(),
|
|
3994
|
+
status: z15.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
|
|
3995
|
+
failureMessages: z15.array(z15.string()).nullable().optional(),
|
|
3996
|
+
location: z15.object({ line: count2, column: count2 }).nullable().optional()
|
|
3423
3997
|
}))
|
|
3424
3998
|
}))
|
|
3425
3999
|
});
|
|
@@ -3427,7 +4001,7 @@ var init_vitest = __esm({
|
|
|
3427
4001
|
});
|
|
3428
4002
|
|
|
3429
4003
|
// src/review/evidence/sarif.ts
|
|
3430
|
-
import { z as
|
|
4004
|
+
import { z as z16 } from "zod";
|
|
3431
4005
|
function parseSarif(raw, sourceRoot) {
|
|
3432
4006
|
const report = schema2.parse(raw), findings = [], diagnostics = [];
|
|
3433
4007
|
let active = 0, suppressed = 0, absent = 0, unresolved = 0;
|
|
@@ -3518,29 +4092,29 @@ var init_sarif = __esm({
|
|
|
3518
4092
|
"use strict";
|
|
3519
4093
|
init_paths2();
|
|
3520
4094
|
init_types2();
|
|
3521
|
-
index =
|
|
3522
|
-
artifact =
|
|
3523
|
-
message =
|
|
3524
|
-
level =
|
|
3525
|
-
location =
|
|
3526
|
-
schema2 =
|
|
3527
|
-
version:
|
|
3528
|
-
runs:
|
|
3529
|
-
tool:
|
|
3530
|
-
invocations:
|
|
3531
|
-
versionControlProvenance:
|
|
3532
|
-
originalUriBaseIds:
|
|
3533
|
-
artifacts:
|
|
3534
|
-
results:
|
|
3535
|
-
ruleId:
|
|
4095
|
+
index = z16.number().int().nonnegative();
|
|
4096
|
+
artifact = z16.object({ uri: z16.string().optional(), uriBaseId: z16.string().optional(), index: index.optional() });
|
|
4097
|
+
message = z16.object({ text: z16.string().optional(), markdown: z16.string().optional(), id: z16.string().optional(), arguments: z16.array(z16.string()).optional() });
|
|
4098
|
+
level = z16.enum(["error", "warning", "note", "none"]);
|
|
4099
|
+
location = z16.object({ physicalLocation: z16.object({ artifactLocation: artifact.optional(), region: z16.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
|
|
4100
|
+
schema2 = z16.object({
|
|
4101
|
+
version: z16.literal("2.1.0"),
|
|
4102
|
+
runs: z16.array(z16.object({
|
|
4103
|
+
tool: z16.object({ driver: z16.object({ name: z16.string().min(1), rules: z16.array(z16.object({ id: z16.string(), defaultConfiguration: z16.object({ level: level.optional() }).optional(), messageStrings: z16.record(message).optional() })).optional(), globalMessageStrings: z16.record(message).optional() }) }),
|
|
4104
|
+
invocations: z16.array(z16.object({ executionSuccessful: z16.boolean(), commandLine: z16.string().optional(), toolExecutionNotifications: z16.array(z16.object({ level: level.optional(), message })).optional() })).optional(),
|
|
4105
|
+
versionControlProvenance: z16.array(z16.object({ revisionId: z16.string().optional() })).optional(),
|
|
4106
|
+
originalUriBaseIds: z16.record(artifact).optional(),
|
|
4107
|
+
artifacts: z16.array(z16.object({ location: artifact.optional() })).optional(),
|
|
4108
|
+
results: z16.array(z16.object({
|
|
4109
|
+
ruleId: z16.string().optional(),
|
|
3536
4110
|
ruleIndex: index.optional(),
|
|
3537
4111
|
message,
|
|
3538
4112
|
level: level.optional(),
|
|
3539
|
-
kind:
|
|
3540
|
-
baselineState:
|
|
3541
|
-
suppressions:
|
|
3542
|
-
locations:
|
|
3543
|
-
relatedLocations:
|
|
4113
|
+
kind: z16.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
|
|
4114
|
+
baselineState: z16.enum(["new", "unchanged", "updated", "absent"]).optional(),
|
|
4115
|
+
suppressions: z16.array(z16.object({ kind: z16.enum(["inSource", "external"]), status: z16.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
|
|
4116
|
+
locations: z16.array(location).optional(),
|
|
4117
|
+
relatedLocations: z16.array(location).optional()
|
|
3544
4118
|
})).optional()
|
|
3545
4119
|
}))
|
|
3546
4120
|
});
|
|
@@ -3548,10 +4122,10 @@ var init_sarif = __esm({
|
|
|
3548
4122
|
});
|
|
3549
4123
|
|
|
3550
4124
|
// src/review/evidence.ts
|
|
3551
|
-
import
|
|
3552
|
-
import { z as
|
|
4125
|
+
import path19 from "path";
|
|
4126
|
+
import { z as z17 } from "zod";
|
|
3553
4127
|
function relativeArtifact(root, file) {
|
|
3554
|
-
const relative =
|
|
4128
|
+
const relative = path19.isAbsolute(file) ? path19.relative(root, file) : file;
|
|
3555
4129
|
const normalized = normalizeRepoPath(relative);
|
|
3556
4130
|
if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
|
|
3557
4131
|
return normalized;
|
|
@@ -3590,7 +4164,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
3590
4164
|
let manifest, raw;
|
|
3591
4165
|
try {
|
|
3592
4166
|
manifest = relativeArtifact(root, supplied);
|
|
3593
|
-
raw =
|
|
4167
|
+
raw = z17.object({ version: z17.literal(1), checks: z17.array(z17.unknown()) }).parse(await readStoreJson(root, manifest));
|
|
3594
4168
|
} catch (error) {
|
|
3595
4169
|
output.diagnostics.push(`${supplied}: ${String(error)}`);
|
|
3596
4170
|
continue;
|
|
@@ -3647,7 +4221,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
3647
4221
|
const report = await readStoreJson(root, file);
|
|
3648
4222
|
if (report === null) throw new Error(`Report artifact is missing: ${file}`);
|
|
3649
4223
|
const sourceRoot = check.sourceRoot ?? root;
|
|
3650
|
-
if (!
|
|
4224
|
+
if (!path19.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
|
|
3651
4225
|
const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : parseSarif(report, sourceRoot);
|
|
3652
4226
|
result.outcome = parsed.outcome;
|
|
3653
4227
|
result.counts = parsed.counts;
|
|
@@ -3743,31 +4317,31 @@ var init_evidence2 = __esm({
|
|
|
3743
4317
|
init_vitest();
|
|
3744
4318
|
init_sarif();
|
|
3745
4319
|
init_types2();
|
|
3746
|
-
checkSchema2 =
|
|
3747
|
-
id:
|
|
3748
|
-
kind:
|
|
3749
|
-
tool:
|
|
3750
|
-
command:
|
|
3751
|
-
commit:
|
|
3752
|
-
workingTreeClean:
|
|
3753
|
-
source:
|
|
3754
|
-
sourceRoot:
|
|
3755
|
-
status:
|
|
3756
|
-
reason:
|
|
3757
|
-
exitCode:
|
|
3758
|
-
report:
|
|
4320
|
+
checkSchema2 = z17.object({
|
|
4321
|
+
id: z17.string().min(1).max(100),
|
|
4322
|
+
kind: z17.enum(["tests", "static-analysis", "security", "complexity", "duplication"]),
|
|
4323
|
+
tool: z17.string().min(1).max(200),
|
|
4324
|
+
command: z17.string().min(1).max(2e3),
|
|
4325
|
+
commit: z17.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
|
|
4326
|
+
workingTreeClean: z17.boolean().optional(),
|
|
4327
|
+
source: z17.string().max(2e3).optional(),
|
|
4328
|
+
sourceRoot: z17.string().min(1).max(4e3).optional(),
|
|
4329
|
+
status: z17.enum(["completed", "skipped", "unavailable"]).default("completed"),
|
|
4330
|
+
reason: z17.string().min(1).max(2e3).optional(),
|
|
4331
|
+
exitCode: z17.number().int().nullable().optional(),
|
|
4332
|
+
report: z17.object({ format: z17.enum(["vitest-json", "sarif"]), path: z17.string().min(1).max(4e3) }).optional()
|
|
3759
4333
|
});
|
|
3760
4334
|
}
|
|
3761
4335
|
});
|
|
3762
4336
|
|
|
3763
4337
|
// src/review/review.ts
|
|
3764
|
-
import
|
|
3765
|
-
import
|
|
3766
|
-
import { execFile as
|
|
3767
|
-
import { promisify as
|
|
4338
|
+
import fs15 from "fs/promises";
|
|
4339
|
+
import path20 from "path";
|
|
4340
|
+
import { execFile as execFile12 } from "child_process";
|
|
4341
|
+
import { promisify as promisify12 } from "util";
|
|
3768
4342
|
async function resolveMergeBase(resolvedRoot, base, head) {
|
|
3769
4343
|
try {
|
|
3770
|
-
const { stdout } = await
|
|
4344
|
+
const { stdout } = await exec10("git", ["merge-base", base, head], {
|
|
3771
4345
|
cwd: resolvedRoot
|
|
3772
4346
|
});
|
|
3773
4347
|
return stdout.trim() || null;
|
|
@@ -3778,7 +4352,7 @@ async function resolveMergeBase(resolvedRoot, base, head) {
|
|
|
3778
4352
|
async function defaultBase(resolvedRoot) {
|
|
3779
4353
|
for (const ref of ["origin/HEAD", "origin/main", "origin/master", "main"]) {
|
|
3780
4354
|
try {
|
|
3781
|
-
await
|
|
4355
|
+
await exec10("git", ["rev-parse", "--verify", "--quiet", ref], {
|
|
3782
4356
|
cwd: resolvedRoot
|
|
3783
4357
|
});
|
|
3784
4358
|
return ref;
|
|
@@ -3791,7 +4365,7 @@ function anchorsTouched(record, changedFiles) {
|
|
|
3791
4365
|
return matchingPaths(record.files, changedFiles);
|
|
3792
4366
|
}
|
|
3793
4367
|
async function computeReview(rootDir, base, options = {}) {
|
|
3794
|
-
const resolvedRoot =
|
|
4368
|
+
const resolvedRoot = path20.resolve(rootDir);
|
|
3795
4369
|
const head = await getCurrentGitHash(resolvedRoot);
|
|
3796
4370
|
if (head === "unknown") return null;
|
|
3797
4371
|
const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
|
|
@@ -3841,7 +4415,7 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
3841
4415
|
analyzed,
|
|
3842
4416
|
async (relPath) => {
|
|
3843
4417
|
try {
|
|
3844
|
-
await
|
|
4418
|
+
await fs15.access(path20.join(resolvedRoot, relPath));
|
|
3845
4419
|
return true;
|
|
3846
4420
|
} catch {
|
|
3847
4421
|
return false;
|
|
@@ -3874,7 +4448,7 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
3874
4448
|
}
|
|
3875
4449
|
return finalize();
|
|
3876
4450
|
}
|
|
3877
|
-
var
|
|
4451
|
+
var exec10, MAX_ANALYZED_FILES;
|
|
3878
4452
|
var init_review = __esm({
|
|
3879
4453
|
"src/review/review.ts"() {
|
|
3880
4454
|
"use strict";
|
|
@@ -3886,7 +4460,7 @@ var init_review = __esm({
|
|
|
3886
4460
|
init_cochange();
|
|
3887
4461
|
init_evidence2();
|
|
3888
4462
|
init_snapshot();
|
|
3889
|
-
|
|
4463
|
+
exec10 = promisify12(execFile12);
|
|
3890
4464
|
MAX_ANALYZED_FILES = 50;
|
|
3891
4465
|
}
|
|
3892
4466
|
});
|
|
@@ -3975,438 +4549,13 @@ var init_onboarding = __esm({
|
|
|
3975
4549
|
}
|
|
3976
4550
|
});
|
|
3977
4551
|
|
|
3978
|
-
// src/setup/files.ts
|
|
3979
|
-
import fs15 from "fs/promises";
|
|
3980
|
-
import path20 from "path";
|
|
3981
|
-
import { randomUUID as randomUUID5 } from "crypto";
|
|
3982
|
-
async function readText(root, file) {
|
|
3983
|
-
try {
|
|
3984
|
-
const text2 = await readBoundedFile(await storePath(root, file), 2 * 1024 * 1024);
|
|
3985
|
-
if (text2 === null) throw new Error("Setup input is not a regular file or exceeds 2 MiB: " + file);
|
|
3986
|
-
return text2;
|
|
3987
|
-
} catch (error) {
|
|
3988
|
-
if (error.code === "ENOENT") return null;
|
|
3989
|
-
throw error;
|
|
3990
|
-
}
|
|
3991
|
-
}
|
|
3992
|
-
function managedBlock(text2, start, end, body) {
|
|
3993
|
-
const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
|
|
3994
|
-
if (starts !== ends || starts > 1 || starts === 1 && text2.indexOf(end) < text2.indexOf(start)) {
|
|
3995
|
-
throw new Error("Ambiguous Mason instruction/configuration markers; repair the marked block before setup.");
|
|
3996
|
-
}
|
|
3997
|
-
const eol = text2.includes("\r\n") ? "\r\n" : "\n";
|
|
3998
|
-
const block = [start, body.replace(/\r?\n/g, eol), end].join(eol);
|
|
3999
|
-
if (starts) return text2.slice(0, text2.indexOf(start)) + block + text2.slice(text2.indexOf(end) + end.length);
|
|
4000
|
-
return text2 + (text2.length && !text2.endsWith("\n") ? eol : "") + (text2.length ? eol : "") + block + eol;
|
|
4001
|
-
}
|
|
4002
|
-
async function applyEdit(root, edit) {
|
|
4003
|
-
const current = await readText(root, edit.path);
|
|
4004
|
-
if (current === edit.after) return false;
|
|
4005
|
-
if (current !== edit.before) throw new Error("Setup input changed during installation: " + edit.path + ". Rerun setup to resume.");
|
|
4006
|
-
const file = await storePath(root, edit.path, true);
|
|
4007
|
-
const temporary = path20.join(path20.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
|
|
4008
|
-
try {
|
|
4009
|
-
const mode = await fs15.stat(file).then((s) => s.mode & 511, () => 384);
|
|
4010
|
-
const handle = await fs15.open(temporary, "wx", mode);
|
|
4011
|
-
try {
|
|
4012
|
-
await handle.writeFile(edit.after, "utf8");
|
|
4013
|
-
await handle.sync();
|
|
4014
|
-
} finally {
|
|
4015
|
-
await handle.close();
|
|
4016
|
-
}
|
|
4017
|
-
if (await readText(root, edit.path) !== edit.before) throw new Error("Setup input changed during installation: " + edit.path);
|
|
4018
|
-
await fs15.rename(temporary, file);
|
|
4019
|
-
return true;
|
|
4020
|
-
} finally {
|
|
4021
|
-
await fs15.rm(temporary, { force: true });
|
|
4022
|
-
}
|
|
4023
|
-
}
|
|
4024
|
-
var init_files2 = __esm({
|
|
4025
|
-
"src/setup/files.ts"() {
|
|
4026
|
-
"use strict";
|
|
4027
|
-
init_files();
|
|
4028
|
-
init_storage();
|
|
4029
|
-
}
|
|
4030
|
-
});
|
|
4031
|
-
|
|
4032
|
-
// src/setup/launcher.ts
|
|
4033
|
-
var BOOTSTRAP, POWERSHELL_BOOTSTRAP, mcpCommand, hookCommand, SHELL_LAUNCHER, POWERSHELL_LAUNCHER, LAUNCHER;
|
|
4034
|
-
var init_launcher = __esm({
|
|
4035
|
-
"src/setup/launcher.ts"() {
|
|
4036
|
-
"use strict";
|
|
4037
|
-
BOOTSTRAP = "require(require('node:path').join(require('node:child_process').execFileSync('git',['rev-parse','--show-toplevel'],{encoding:'utf8'}).trim(),'.mason','run.cjs'))";
|
|
4038
|
-
POWERSHELL_BOOTSTRAP = "& (Join-Path (git rev-parse --show-toplevel) '.mason/run.ps1')";
|
|
4039
|
-
mcpCommand = (host, runtime) => !runtime?.bundle ? { command: "node", args: ["-e", BOOTSTRAP, "--", host, "mcp"] } : process.platform === "win32" ? { command: "powershell.exe", args: ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", POWERSHELL_BOOTSTRAP, host, "mcp"] } : { command: "sh", args: ["-c", 'exec sh "$(git rev-parse --show-toplevel)/.mason/run.sh" "$@"', "mason", host, "mcp"] };
|
|
4040
|
-
hookCommand = (host, runtime) => !runtime?.bundle ? `node -e "${BOOTSTRAP}" -- ${host} auto` : process.platform === "win32" ? `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "${POWERSHELL_BOOTSTRAP} ${host} auto"` : `sh "$(git rev-parse --show-toplevel)/.mason/run.sh" ${host} auto`;
|
|
4041
|
-
SHELL_LAUNCHER = `#!/bin/sh
|
|
4042
|
-
set -eu
|
|
4043
|
-
advisory=0
|
|
4044
|
-
if [ "\${2-}" = auto ] && [ "\${3-}" = hook ]; then advisory=1; fi
|
|
4045
|
-
fail() {
|
|
4046
|
-
if [ "$advisory" -eq 1 ]; then printf '%s\\n' '{"systemMessage":"Mason runtime unavailable. Rerun mason setup for this checkout; verification was not established."}'; exit 0; fi
|
|
4047
|
-
echo 'Mason runtime unavailable. Rerun mason setup for this checkout.' >&2; exit 2
|
|
4048
|
-
}
|
|
4049
|
-
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) || fail
|
|
4050
|
-
case "\${1-}" in codex|claude) ;; *) fail ;; esac
|
|
4051
|
-
pointer="$root/.mason/runtime/$1.txt"
|
|
4052
|
-
[ -f "$pointer" ] || fail
|
|
4053
|
-
runtime=$(cat "$pointer") || fail
|
|
4054
|
-
case "$runtime" in ''|*[!0-9a-f]*) fail ;; esac
|
|
4055
|
-
[ "\${#runtime}" -eq 24 ] || fail
|
|
4056
|
-
[ -x "$root/.mason/runtime/$runtime/node" ] || fail
|
|
4057
|
-
if [ "$advisory" -eq 1 ]; then
|
|
4058
|
-
"$root/.mason/runtime/$runtime/node" "$root/.mason/run.cjs" "$@" || fail
|
|
4059
|
-
else
|
|
4060
|
-
exec "$root/.mason/runtime/$runtime/node" "$root/.mason/run.cjs" "$@"
|
|
4061
|
-
fi
|
|
4062
|
-
`;
|
|
4063
|
-
POWERSHELL_LAUNCHER = `# Managed by mason setup; no system Node installation is used.
|
|
4064
|
-
$ErrorActionPreference = 'Stop'
|
|
4065
|
-
$advisory = $args.Count -ge 3 -and $args[1] -eq 'auto' -and $args[2] -eq 'hook'
|
|
4066
|
-
try {
|
|
4067
|
-
if ($args.Count -lt 2 -or $args[0] -notin @('codex','claude')) { throw 'Invalid Mason host.' }
|
|
4068
|
-
$runtime = (Get-Content -LiteralPath (Join-Path $PSScriptRoot ('runtime/' + $args[0] + '.txt')) -Raw).Trim()
|
|
4069
|
-
if ($runtime -cnotmatch '^[a-f0-9]{24}$') { throw 'Invalid Mason runtime pointer.' }
|
|
4070
|
-
& (Join-Path $PSScriptRoot ('runtime/' + $runtime + '/node.exe')) (Join-Path $PSScriptRoot 'run.cjs') @args
|
|
4071
|
-
if ($LASTEXITCODE -ne 0) { throw 'Mason runtime failed.' }
|
|
4072
|
-
} catch {
|
|
4073
|
-
if ($advisory) { Write-Output '{"systemMessage":"Mason runtime unavailable. Rerun mason setup for this checkout; verification was not established."}'; exit 0 }
|
|
4074
|
-
[Console]::Error.WriteLine($_.Exception.Message); exit 2
|
|
4075
|
-
}
|
|
4076
|
-
`;
|
|
4077
|
-
LAUNCHER = `// Managed by mason-auto setup. Rerun setup to install this checkout's pinned runtime.
|
|
4078
|
-
const fs = require('node:fs');
|
|
4079
|
-
const path = require('node:path');
|
|
4080
|
-
const {pathToFileURL} = require('node:url');
|
|
4081
|
-
const launchArgs = process.argv.slice(process.argv[1] && path.resolve(process.argv[1]) === __filename ? 2 : 1);
|
|
4082
|
-
(async () => {
|
|
4083
|
-
const root = path.dirname(__dirname);
|
|
4084
|
-
const args = launchArgs;
|
|
4085
|
-
const [host, command, ...rest] = args;
|
|
4086
|
-
if (!['codex','claude'].includes(host) || !['auto','mcp'].includes(command)) throw new Error('Invalid Mason launcher arguments.');
|
|
4087
|
-
const setup = JSON.parse(fs.readFileSync(path.join(__dirname, 'setup.json'), 'utf8'));
|
|
4088
|
-
const entry = setup.hosts?.[host];
|
|
4089
|
-
if (setup.version !== 1 || !entry || !/^[a-f0-9]{24}$/.test(entry.runtime?.id)) throw new Error('Mason is not configured for this host. Rerun mason-auto setup.');
|
|
4090
|
-
const base = path.join(__dirname, 'runtime', entry.runtime.id);
|
|
4091
|
-
if (entry.runtime.bundle) {
|
|
4092
|
-
const node = process.platform === 'win32' ? 'node.exe' : 'node';
|
|
4093
|
-
if (fs.realpathSync(process.execPath) !== fs.realpathSync(path.join(base, node))) throw new Error('Mason runtime pointer differs from the configured version. Rerun setup.');
|
|
4094
|
-
}
|
|
4095
|
-
const binary = entry.runtime.bundle ? path.join(base, 'app', 'dist', 'mason-' + command + '.js')
|
|
4096
|
-
: path.join(base, 'node_modules', 'mason-context', 'dist', 'mason-' + command + '.js');
|
|
4097
|
-
if (!fs.existsSync(binary)) throw new Error('The pinned Mason runtime is missing in this checkout. Rerun mason-auto setup --host ' + host + '.');
|
|
4098
|
-
process.env.MASON_SETUP_ROOT = root;
|
|
4099
|
-
process.env.MASON_SETUP_HOST = host;
|
|
4100
|
-
process.env.MASON_SETUP_REVISION = entry.revision;
|
|
4101
|
-
process.argv = [process.execPath, binary, ...rest];
|
|
4102
|
-
await import(pathToFileURL(binary).href);
|
|
4103
|
-
})().catch(error => {
|
|
4104
|
-
const message = 'Mason setup unavailable: ' + error.message;
|
|
4105
|
-
if (launchArgs[1] === 'auto' && launchArgs[2] === 'hook') { console.log(JSON.stringify({systemMessage: message})); process.exitCode = 0; }
|
|
4106
|
-
else { console.error(message); process.exitCode = 2; }
|
|
4107
|
-
});
|
|
4108
|
-
`;
|
|
4109
|
-
}
|
|
4110
|
-
});
|
|
4111
|
-
|
|
4112
|
-
// src/setup/config.ts
|
|
4113
|
-
import { isDeepStrictEqual } from "util";
|
|
4114
|
-
import { parse, stringify } from "smol-toml";
|
|
4115
|
-
import { z as z15 } from "zod";
|
|
4116
|
-
async function instructionEdits(root, host) {
|
|
4117
|
-
const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
|
|
4118
|
-
const existing = found.find((f) => f.text !== null);
|
|
4119
|
-
const primary = host === "codex" && found[0].text === null ? { path: "AGENTS.md", text: null } : existing ?? { path: "AGENTS.md", text: null };
|
|
4120
|
-
const originalGuidance = primary.text === null && existing ? "Follow the existing project conventions in " + existing.path + ".\n" : "";
|
|
4121
|
-
const edits = [{
|
|
4122
|
-
path: primary.path,
|
|
4123
|
-
before: primary.text,
|
|
4124
|
-
after: managedBlock(primary.text ?? originalGuidance, "<!-- mason:start -->", "<!-- mason:end -->", CLAUDE_MD_SECTION.split("\n").slice(1, -1).join("\n"))
|
|
4125
|
-
}];
|
|
4126
|
-
if (primary.path === "AGENTS.md") {
|
|
4127
|
-
const secondary = found.filter((f) => f.path !== "AGENTS.md" && f.text !== null);
|
|
4128
|
-
if (host === "claude" && !secondary.length) secondary.push({ path: "CLAUDE.md", text: null });
|
|
4129
|
-
for (const file of secondary) {
|
|
4130
|
-
const text2 = file.text ?? "";
|
|
4131
|
-
const legacy = text2.includes("<!-- mason:start -->") || text2.includes("<!-- mason:end -->");
|
|
4132
|
-
const imported = "@" + (file.path.startsWith(".claude/") ? "../AGENTS.md" : "AGENTS.md");
|
|
4133
|
-
if (text2.trim() === imported) continue;
|
|
4134
|
-
const pointer = "Mason project knowledge and shared project instructions:\n" + imported;
|
|
4135
|
-
edits.push({
|
|
4136
|
-
path: file.path,
|
|
4137
|
-
before: file.text,
|
|
4138
|
-
after: managedBlock(text2, legacy ? "<!-- mason:start -->" : POINTER_START, legacy ? "<!-- mason:end -->" : POINTER_END, pointer)
|
|
4139
|
-
});
|
|
4140
|
-
}
|
|
4141
|
-
}
|
|
4142
|
-
return edits;
|
|
4143
|
-
}
|
|
4144
|
-
function managedMcp(previous, host, runtime) {
|
|
4145
|
-
const options = previous === void 0 ? {} : object.parse(previous);
|
|
4146
|
-
for (const key of ["command", "args", "url", "type", "headers", "http_headers", "env_http_headers", "bearer_token_env_var"]) delete options[key];
|
|
4147
|
-
return { ...options, ...mcpCommand(host, runtime) };
|
|
4148
|
-
}
|
|
4149
|
-
async function mcpEdit(root, host, runtime) {
|
|
4150
|
-
const file = mcpPath(host), before = await readText(root, file);
|
|
4151
|
-
if (host === "claude") {
|
|
4152
|
-
const config2 = before === null ? {} : object.parse(JSON.parse(before));
|
|
4153
|
-
const servers2 = object.parse(config2.mcpServers ?? {});
|
|
4154
|
-
return { path: file, before, after: JSON.stringify({ ...config2, mcpServers: { ...servers2, mason: managedMcp(servers2.mason, host, runtime) } }, null, 2) + "\n" };
|
|
4155
|
-
}
|
|
4156
|
-
const config = parse(before ?? "");
|
|
4157
|
-
const managed = (before ?? "").includes(TOML_START);
|
|
4158
|
-
const servers = config.mcp_servers;
|
|
4159
|
-
const desired = managedMcp(servers?.mason, host, runtime);
|
|
4160
|
-
let base = before ?? "";
|
|
4161
|
-
if (servers?.mason && !managed) {
|
|
4162
|
-
const headers = [...base.matchAll(/^\s*\[\[?.+?\]\]?[^\S\r\n]*(?:#.*)?$/gm)];
|
|
4163
|
-
const spans = headers.flatMap((header, index2) => /^\s*\[mcp_servers\.mason(?:\.[A-Za-z0-9_-]+)*\]/.test(header[0]) ? [{ start: header.index, end: headers[index2 + 1]?.index ?? base.length }] : []);
|
|
4164
|
-
if (!spans.length) throw new Error("Cannot safely migrate the existing Mason MCP TOML entry. Use an explicit [mcp_servers.mason] table and rerun setup; the file was retained.");
|
|
4165
|
-
for (const span of spans.reverse()) base = base.slice(0, span.start) + base.slice(span.end);
|
|
4166
|
-
}
|
|
4167
|
-
const after = managedBlock(base, TOML_START, TOML_END, stringify({ mcp_servers: { mason: desired } }).trimEnd());
|
|
4168
|
-
const parsed = parse(after);
|
|
4169
|
-
const expected = { ...config, mcp_servers: { ...servers ?? {}, mason: desired } };
|
|
4170
|
-
if (!isDeepStrictEqual(parsed, expected)) throw new Error("Could not safely configure the Mason MCP entry without changing other settings.");
|
|
4171
|
-
return { path: file, before, after };
|
|
4172
|
-
}
|
|
4173
|
-
async function hookEdits(root, host, runtime) {
|
|
4174
|
-
const file = configPath(host), before = await readText(root, file);
|
|
4175
|
-
const { planAutomationInstall: planAutomationInstall2 } = await Promise.resolve().then(() => (init_install(), install_exports));
|
|
4176
|
-
const plan = await planAutomationInstall2(root, host, hookCommand(host, runtime));
|
|
4177
|
-
return [
|
|
4178
|
-
{ path: file, before, after: JSON.stringify(plan.config, null, 2) + "\n" },
|
|
4179
|
-
{ path: ".mason/automation.json", before: await readText(root, ".mason/automation.json"), after: JSON.stringify(plan.record, null, 2) + "\n" }
|
|
4180
|
-
];
|
|
4181
|
-
}
|
|
4182
|
-
async function ancillaryEdits(root) {
|
|
4183
|
-
const ignore = await readText(root, ".gitignore");
|
|
4184
|
-
const { git: git3 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
|
|
4185
|
-
let parentIgnored = false;
|
|
4186
|
-
try {
|
|
4187
|
-
parentIgnored = !!(await git3(root, "check-ignore", "--no-index", ".mason")).trim();
|
|
4188
|
-
} catch (error) {
|
|
4189
|
-
if (error.code !== 1) throw error;
|
|
4190
|
-
}
|
|
4191
|
-
const retainParentRule = parentIgnored || !!ignore?.replace(/\r\n/g, "\n").includes("# mason:ignore:start\n!/.mason/\n/.mason/*");
|
|
4192
|
-
const rules = [...retainParentRule ? ["!/.mason/", "/.mason/*"] : [], "!/.mason/decisions/", "!/.mason/decisions/**", "!/.mason/setup.json", "!/.mason/automation.json", "!/.mason/project.json", "!/.mason/run.cjs", "!/.mason/run.sh", "!/.mason/run.ps1", "/.mason/reports/", "/.mason/runtime/"].join("\n");
|
|
4193
|
-
return [
|
|
4194
|
-
{ path: ".gitignore", before: ignore, after: managedBlock(ignore ?? "", "# mason:ignore:start", "# mason:ignore:end", rules) },
|
|
4195
|
-
{ path: ".mason/run.cjs", before: await readText(root, ".mason/run.cjs"), after: LAUNCHER },
|
|
4196
|
-
{ path: ".mason/run.sh", before: await readText(root, ".mason/run.sh"), after: SHELL_LAUNCHER },
|
|
4197
|
-
{ path: ".mason/run.ps1", before: await readText(root, ".mason/run.ps1"), after: POWERSHELL_LAUNCHER }
|
|
4198
|
-
];
|
|
4199
|
-
}
|
|
4200
|
-
async function inspectHostConfig(root, host, plannedMcp, runtime) {
|
|
4201
|
-
const text2 = plannedMcp ?? await readText(root, mcpPath(host));
|
|
4202
|
-
const config = host === "codex" ? parse(text2 ?? "") : object.parse(JSON.parse(text2 ?? "{}"));
|
|
4203
|
-
const servers = host === "codex" ? config.mcp_servers : config.mcpServers;
|
|
4204
|
-
const hooks = automationConfigSchema.parse(JSON.parse(await readText(root, configPath(host)) ?? "{}"));
|
|
4205
|
-
const expected = hookConfig(host, hookCommand(host, runtime));
|
|
4206
|
-
const mcp = servers?.mason === void 0 ? null : object.parse(servers.mason);
|
|
4207
|
-
return {
|
|
4208
|
-
mcp,
|
|
4209
|
-
mcpDisabled: mcp?.enabled === false,
|
|
4210
|
-
hooks: Object.fromEntries(Object.keys(expected.hooks).map((event) => [
|
|
4211
|
-
event,
|
|
4212
|
-
(hooks.hooks?.[event] ?? []).flatMap((group) => group.hooks.filter((handler) => handler.command === expected.hooks.SessionStart[0].hooks[0].command).map((handler) => ({ ...group, hooks: [handler] })) ?? [])
|
|
4213
|
-
])),
|
|
4214
|
-
disabled: hooks.disableAllHooks === true || config.features?.hooks === false || config.features?.codex_hooks === false
|
|
4215
|
-
};
|
|
4216
|
-
}
|
|
4217
|
-
var object, mcpPath, TOML_START, TOML_END, POINTER_START, POINTER_END;
|
|
4218
|
-
var init_config = __esm({
|
|
4219
|
-
"src/setup/config.ts"() {
|
|
4220
|
-
"use strict";
|
|
4221
|
-
init_init();
|
|
4222
|
-
init_docs();
|
|
4223
|
-
init_adapters();
|
|
4224
|
-
init_install();
|
|
4225
|
-
init_files2();
|
|
4226
|
-
init_launcher();
|
|
4227
|
-
object = z15.record(z15.unknown());
|
|
4228
|
-
mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
|
|
4229
|
-
TOML_START = "# mason:mcp:start";
|
|
4230
|
-
TOML_END = "# mason:mcp:end";
|
|
4231
|
-
POINTER_START = "<!-- mason:agents:start -->";
|
|
4232
|
-
POINTER_END = "<!-- mason:agents:end -->";
|
|
4233
|
-
}
|
|
4234
|
-
});
|
|
4235
|
-
|
|
4236
|
-
// src/distribution/bundle.ts
|
|
4237
|
-
import fs16 from "fs/promises";
|
|
4238
|
-
import path21 from "path";
|
|
4239
|
-
import { createHash as createHash4 } from "crypto";
|
|
4240
|
-
import { z as z16 } from "zod";
|
|
4241
|
-
async function readBundle(root) {
|
|
4242
|
-
const bytes = await fs16.readFile(await storePath(root, "bundle.json"));
|
|
4243
|
-
const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
|
|
4244
|
-
const windows = manifest.target.startsWith("win32-");
|
|
4245
|
-
for (const file of Object.keys(manifest.files)) {
|
|
4246
|
-
if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") || path21.isAbsolute(file)) {
|
|
4247
|
-
throw new Error("Unsafe path in Mason bundle: " + file);
|
|
4248
|
-
}
|
|
4249
|
-
}
|
|
4250
|
-
for (const required of [windows ? "node.exe" : "node", "app/package.json", "app/dist/mason.js", "app/dist/mason-auto.js", "app/dist/mason-mcp.js"]) {
|
|
4251
|
-
if (!manifest.files[required]) throw new Error("Incomplete Mason bundle: " + required);
|
|
4252
|
-
}
|
|
4253
|
-
return { manifest, manifestHash: sha256(bytes) };
|
|
4254
|
-
}
|
|
4255
|
-
async function verifyBundle(root, expectedHash) {
|
|
4256
|
-
const bundle = await readBundle(root);
|
|
4257
|
-
if (expectedHash && bundle.manifestHash !== expectedHash) throw new Error("Mason bundle manifest changed.");
|
|
4258
|
-
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.");
|
|
4259
|
-
const files = Object.keys(bundle.manifest.files);
|
|
4260
|
-
for (let i = 0; i < files.length; i += 8) await Promise.all(files.slice(i, i + 8).map(async (file) => {
|
|
4261
|
-
if (sha256(await fs16.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
|
|
4262
|
-
}));
|
|
4263
|
-
return bundle;
|
|
4264
|
-
}
|
|
4265
|
-
var targets, digest2, bundleSchema, sha256;
|
|
4266
|
-
var init_bundle = __esm({
|
|
4267
|
-
"src/distribution/bundle.ts"() {
|
|
4268
|
-
"use strict";
|
|
4269
|
-
init_storage();
|
|
4270
|
-
targets = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
|
|
4271
|
-
digest2 = z16.string().regex(/^[a-f0-9]{64}$/);
|
|
4272
|
-
bundleSchema = z16.object({
|
|
4273
|
-
format: z16.literal(1),
|
|
4274
|
-
version: z16.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
|
|
4275
|
-
target: z16.enum(targets),
|
|
4276
|
-
nodeVersion: z16.string(),
|
|
4277
|
-
files: z16.record(digest2)
|
|
4278
|
-
}).strict();
|
|
4279
|
-
sha256 = (bytes) => createHash4("sha256").update(bytes).digest("hex");
|
|
4280
|
-
}
|
|
4281
|
-
});
|
|
4282
|
-
|
|
4283
|
-
// src/setup/runtime.ts
|
|
4284
|
-
import fs17 from "fs/promises";
|
|
4285
|
-
import path22 from "path";
|
|
4286
|
-
import { fileURLToPath } from "url";
|
|
4287
|
-
import { randomUUID as randomUUID6, createHash as createHash5 } from "crypto";
|
|
4288
|
-
import { execFile as execFile11 } from "child_process";
|
|
4289
|
-
import { promisify as promisify11 } from "util";
|
|
4290
|
-
async function packageRoot() {
|
|
4291
|
-
let directory = path22.dirname(fileURLToPath(import.meta.url));
|
|
4292
|
-
for (let i = 0; i < 8; i++) {
|
|
4293
|
-
try {
|
|
4294
|
-
const pkg = JSON.parse(await fs17.readFile(path22.join(directory, "package.json"), "utf8"));
|
|
4295
|
-
if (pkg.name === "mason-context") return directory;
|
|
4296
|
-
} catch {
|
|
4297
|
-
}
|
|
4298
|
-
directory = path22.dirname(directory);
|
|
4299
|
-
}
|
|
4300
|
-
throw new Error("Cannot locate the executing Mason distribution. Reinstall Mason and rerun setup.");
|
|
4301
|
-
}
|
|
4302
|
-
async function sourceRuntime() {
|
|
4303
|
-
const source = await packageRoot();
|
|
4304
|
-
const pkg = JSON.parse(await fs17.readFile(path22.join(source, "package.json"), "utf8"));
|
|
4305
|
-
const hashes = Object.fromEntries(await Promise.all(BINARIES.map(async (file) => [file, checksum(await fs17.readFile(path22.join(source, file)))])));
|
|
4306
|
-
const bundleRoot = path22.dirname(source);
|
|
4307
|
-
const bundleExists = await fs17.lstat(path22.join(bundleRoot, "bundle.json")).then(() => true, (error) => {
|
|
4308
|
-
if (error.code === "ENOENT") return false;
|
|
4309
|
-
throw error;
|
|
4310
|
-
});
|
|
4311
|
-
if (bundleExists) {
|
|
4312
|
-
const { manifest, manifestHash } = await verifyBundle(bundleRoot);
|
|
4313
|
-
if (manifest.version !== pkg.version) throw new Error("Mason package and bundle versions differ.");
|
|
4314
|
-
return { source, bundleRoot, runtime: runtimeSchema.parse({
|
|
4315
|
-
id: manifestHash.slice(0, 24),
|
|
4316
|
-
version: pkg.version,
|
|
4317
|
-
hashes,
|
|
4318
|
-
bundle: { target: manifest.target, manifestHash }
|
|
4319
|
-
}) };
|
|
4320
|
-
}
|
|
4321
|
-
return { source, runtime: runtimeSchema.parse({ id: hash([pkg.version, pkg.dependencies, hashes]).slice(0, 24), version: pkg.version, hashes }) };
|
|
4322
|
-
}
|
|
4323
|
-
async function verifyRuntime(root, runtime) {
|
|
4324
|
-
try {
|
|
4325
|
-
const saved = await readStoreJson(root, `.mason/runtime/${runtime.id}/receipt.json`);
|
|
4326
|
-
if (JSON.stringify(saved) !== JSON.stringify(runtime)) return false;
|
|
4327
|
-
if (runtime.bundle) {
|
|
4328
|
-
const base2 = await storePath(root, `.mason/runtime/${runtime.id}`);
|
|
4329
|
-
const { manifest } = await verifyBundle(base2, runtime.bundle.manifestHash);
|
|
4330
|
-
if (manifest.version !== runtime.version || manifest.target !== runtime.bundle.target) return false;
|
|
4331
|
-
for (const file of BINARIES) if (manifest.files["app/" + file] !== runtime.hashes[file]) return false;
|
|
4332
|
-
return true;
|
|
4333
|
-
}
|
|
4334
|
-
const base = `.mason/runtime/${runtime.id}/node_modules/mason-context/`;
|
|
4335
|
-
const pkg = await readStoreJson(root, base + "package.json");
|
|
4336
|
-
if (pkg?.name !== "mason-context" || pkg.version !== runtime.version) return false;
|
|
4337
|
-
for (const file of BINARIES) {
|
|
4338
|
-
if (checksum(await fs17.readFile(await storePath(root, base + file))) !== runtime.hashes[file]) return false;
|
|
4339
|
-
}
|
|
4340
|
-
return true;
|
|
4341
|
-
} catch {
|
|
4342
|
-
return false;
|
|
4343
|
-
}
|
|
4344
|
-
}
|
|
4345
|
-
async function installRuntime(root, selected) {
|
|
4346
|
-
const { runtime, source } = selected;
|
|
4347
|
-
if (await verifyRuntime(root, runtime)) return runtime;
|
|
4348
|
-
const relative = `.mason/runtime/${runtime.id}`;
|
|
4349
|
-
const target = await storePath(root, relative, true);
|
|
4350
|
-
const stage = await storePath(root, ".mason/runtime/.install-" + randomUUID6(), true);
|
|
4351
|
-
await fs17.mkdir(stage);
|
|
4352
|
-
try {
|
|
4353
|
-
if (runtime.bundle) {
|
|
4354
|
-
if (!selected.bundleRoot) throw new Error("Standalone bundle source is missing.");
|
|
4355
|
-
const { manifest } = await verifyBundle(selected.bundleRoot, runtime.bundle.manifestHash);
|
|
4356
|
-
for (const file of ["bundle.json", ...Object.keys(manifest.files)]) {
|
|
4357
|
-
const from = await storePath(selected.bundleRoot, file);
|
|
4358
|
-
const to = await storePath(stage, file, true);
|
|
4359
|
-
await fs17.copyFile(from, to);
|
|
4360
|
-
await fs17.chmod(to, (await fs17.stat(from)).mode & 511);
|
|
4361
|
-
}
|
|
4362
|
-
await verifyBundle(stage, runtime.bundle.manifestHash);
|
|
4363
|
-
} else {
|
|
4364
|
-
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
4365
|
-
const options = { timeout: 12e4, maxBuffer: 2 * 1024 * 1024, windowsHide: true };
|
|
4366
|
-
const packed = JSON.parse((await exec10(npm, ["pack", "--ignore-scripts", "--json", "--pack-destination", stage], { ...options, cwd: source })).stdout);
|
|
4367
|
-
const filename = packed[0]?.filename;
|
|
4368
|
-
if (typeof filename !== "string" || path22.basename(filename) !== filename) throw new Error("npm did not produce a Mason package archive.");
|
|
4369
|
-
await fs17.rename(path22.join(stage, filename), path22.join(stage, "mason.tgz"));
|
|
4370
|
-
await fs17.writeFile(path22.join(stage, "package.json"), JSON.stringify({ name: "mason-project-runtime", private: true, version: "1.0.0" }));
|
|
4371
|
-
await exec10(npm, ["install", "--ignore-scripts", "--omit=dev", "--no-audit", "--no-fund", "--save-exact", "./mason.tgz"], { ...options, cwd: stage });
|
|
4372
|
-
for (const file of BINARIES) {
|
|
4373
|
-
if (checksum(await fs17.readFile(path22.join(stage, "node_modules/mason-context", file))) !== runtime.hashes[file]) throw new Error("Installed Mason binary differs from the selected distribution.");
|
|
4374
|
-
}
|
|
4375
|
-
}
|
|
4376
|
-
await fs17.writeFile(path22.join(stage, "receipt.json"), JSON.stringify(runtime, null, 2) + "\n");
|
|
4377
|
-
try {
|
|
4378
|
-
await fs17.rename(target, target + ".previous-" + randomUUID6());
|
|
4379
|
-
} catch (error) {
|
|
4380
|
-
if (error.code !== "ENOENT") throw error;
|
|
4381
|
-
}
|
|
4382
|
-
await fs17.rename(stage, target);
|
|
4383
|
-
if (!await verifyRuntime(root, runtime)) throw new Error("The installed Mason runtime could not be verified.");
|
|
4384
|
-
return runtime;
|
|
4385
|
-
} finally {
|
|
4386
|
-
await fs17.rm(stage, { recursive: true, force: true });
|
|
4387
|
-
}
|
|
4388
|
-
}
|
|
4389
|
-
var exec10, checksum, BINARIES;
|
|
4390
|
-
var init_runtime2 = __esm({
|
|
4391
|
-
"src/setup/runtime.ts"() {
|
|
4392
|
-
"use strict";
|
|
4393
|
-
init_model();
|
|
4394
|
-
init_storage();
|
|
4395
|
-
init_evidence();
|
|
4396
|
-
init_bundle();
|
|
4397
|
-
exec10 = promisify11(execFile11);
|
|
4398
|
-
checksum = (bytes) => createHash5("sha256").update(bytes).digest("hex");
|
|
4399
|
-
BINARIES = ["dist/mason-auto.js", "dist/mason-mcp.js"];
|
|
4400
|
-
}
|
|
4401
|
-
});
|
|
4402
|
-
|
|
4403
4552
|
// src/setup/status.ts
|
|
4404
4553
|
var status_exports = {};
|
|
4405
4554
|
__export(status_exports, {
|
|
4406
4555
|
setupStatus: () => setupStatus,
|
|
4407
4556
|
summarizeActivation: () => summarizeActivation
|
|
4408
4557
|
});
|
|
4409
|
-
import { isDeepStrictEqual as
|
|
4558
|
+
import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
|
|
4410
4559
|
async function setupStatus(dir) {
|
|
4411
4560
|
const ws = await workspace(dir);
|
|
4412
4561
|
const setup = await loadSetup(ws.root);
|
|
@@ -4423,35 +4572,33 @@ async function setupStatus(dir) {
|
|
|
4423
4572
|
next: pending.length ? "Setup did not finish. Rerun mason-auto setup --host " + pending[0] + " to resume using the retained original evidence." : "Run mason-auto setup --host codex or --host claude."
|
|
4424
4573
|
};
|
|
4425
4574
|
}
|
|
4426
|
-
const
|
|
4427
|
-
const
|
|
4575
|
+
const hosts2 = {};
|
|
4576
|
+
const installed = await installedCommand();
|
|
4428
4577
|
const automation = await automationStatus(ws.root);
|
|
4429
4578
|
for (const host of ["codex", "claude"]) {
|
|
4430
4579
|
const entry = setup.hosts[host];
|
|
4431
4580
|
if (!entry) continue;
|
|
4432
|
-
const launcherCurrent = commonLauncherCurrent && (!entry.runtime.bundle || await readText(ws.root, ".mason/run.sh") === SHELL_LAUNCHER && await readText(ws.root, ".mason/run.ps1") === POWERSHELL_LAUNCHER && await readText(ws.root, `.mason/runtime/${host}.txt`) === entry.runtime.id + "\n");
|
|
4433
4581
|
const instructions = await instructionEdits(ws.root, host);
|
|
4434
4582
|
const instructionsCurrent = instructions.every((edit) => edit.before === edit.after);
|
|
4435
|
-
const
|
|
4436
|
-
const
|
|
4437
|
-
const
|
|
4438
|
-
const hooks = isDeepStrictEqual2(config.hooks, hookConfig(host, hookCommand(host, entry.runtime)).hooks) && !config.disabled;
|
|
4583
|
+
const config = await inspectHostConfig(ws.root, host, void 0);
|
|
4584
|
+
const mcp = !config.mcpDisabled && hash(config.mcp) === entry.mcpFingerprint && isDeepStrictEqual3({ command: config.mcp?.command, args: config.mcp?.args }, mcpCommand(host));
|
|
4585
|
+
const hooks = isDeepStrictEqual3(config.hooks, hookConfig(host, hookCommand(host)).hooks) && !config.disabled;
|
|
4439
4586
|
const local = await loadSetupReceipt(ws.root, ws.directory, host);
|
|
4440
4587
|
const configured = local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
|
|
4441
|
-
const observation = await readObservation(ws.root, ws.directory, host, entry.revision);
|
|
4588
|
+
const observation = await readObservation(ws.root, ws.directory, host, entry.revision, installed.version ?? "unavailable");
|
|
4442
4589
|
const sessions = Object.values(observation?.sessions ?? {}).sort((a, b) => b.at.localeCompare(a.at));
|
|
4443
4590
|
const complete = sessions.find((session) => events.every((event) => session.events.includes(event)));
|
|
4444
4591
|
const observedEvents = complete?.events ?? sessions[0]?.events ?? [];
|
|
4445
4592
|
const pending = [];
|
|
4446
|
-
if (!installed
|
|
4593
|
+
if (!installed.available) pending.push(installed.message);
|
|
4447
4594
|
if (!mcp || !hooks || !instructionsCurrent || !configured) pending.push("Rerun setup to reconcile project configuration; inspect any disabled host settings.");
|
|
4448
4595
|
if (!observation?.contextCalls) pending.push("Start a new assistant session and request task context through Mason's get_context tool.");
|
|
4449
4596
|
if (!complete) pending.push("Review/trust the host configuration, then complete a normal task to observe the full hook lifecycle.");
|
|
4450
4597
|
if (automation.status === "unavailable") pending.push("The latest automation attempt did not establish verification. Run mason-auto check and inspect its diagnostics.");
|
|
4451
|
-
const healthy = installed &&
|
|
4452
|
-
|
|
4598
|
+
const healthy = installed.available && mcp && hooks && instructionsCurrent && configured && automation.status !== "unavailable";
|
|
4599
|
+
hosts2[host] = {
|
|
4453
4600
|
status: healthy ? complete && observation?.contextCalls ? "active" : "pending" : "attention",
|
|
4454
|
-
runtime: installed
|
|
4601
|
+
runtime: installed.available ? "global (" + installed.version + ")" : "unavailable-on-path",
|
|
4455
4602
|
mcp: mcp ? "configured" : "changed",
|
|
4456
4603
|
instructions: instructionsCurrent ? "current" : "changed",
|
|
4457
4604
|
hookConfiguration: hooks ? "configured" : "disabled-or-changed",
|
|
@@ -4461,13 +4608,13 @@ async function setupStatus(dir) {
|
|
|
4461
4608
|
pending
|
|
4462
4609
|
};
|
|
4463
4610
|
}
|
|
4464
|
-
const statuses = Object.values(
|
|
4611
|
+
const statuses = Object.values(hosts2).map((host) => host.status);
|
|
4465
4612
|
const decisions = await loadDecisionStore(ws.root);
|
|
4466
4613
|
return {
|
|
4467
4614
|
version: 1,
|
|
4468
4615
|
status: statuses.length && statuses.every((status) => status === "active") ? "active" : statuses.includes("attention") ? "attention" : "pending",
|
|
4469
4616
|
root: ws.root,
|
|
4470
|
-
hosts,
|
|
4617
|
+
hosts: hosts2,
|
|
4471
4618
|
decisionRecords: decisions.records.length,
|
|
4472
4619
|
diagnostics: decisions.diagnostics,
|
|
4473
4620
|
scope: "Activation receipts record observed use after setup, not complete interception, correct repairs, or measured usefulness. Host trust and higher-priority settings may prevent execution. Receipts are local to this worktree and branch."
|
|
@@ -4497,10 +4644,8 @@ var init_status = __esm({
|
|
|
4497
4644
|
init_decisions();
|
|
4498
4645
|
init_adapters();
|
|
4499
4646
|
init_model();
|
|
4500
|
-
init_runtime2();
|
|
4501
4647
|
init_config();
|
|
4502
4648
|
init_launcher();
|
|
4503
|
-
init_files2();
|
|
4504
4649
|
init_observations();
|
|
4505
4650
|
}
|
|
4506
4651
|
});
|
|
@@ -4512,7 +4657,7 @@ __export(setup_exports, {
|
|
|
4512
4657
|
setupProject: () => setupProject,
|
|
4513
4658
|
summarizeSetup: () => summarizeSetup
|
|
4514
4659
|
});
|
|
4515
|
-
import { randomUUID as
|
|
4660
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
4516
4661
|
async function selectHost(root, explicit) {
|
|
4517
4662
|
if (explicit) return explicit;
|
|
4518
4663
|
if (process.env.MASON_SETUP_HOST === "codex" || process.env.MASON_SETUP_HOST === "claude") return process.env.MASON_SETUP_HOST;
|
|
@@ -4528,28 +4673,29 @@ async function setupProject(dir, options = {}) {
|
|
|
4528
4673
|
return withLock(ws.root, ".mason/reports/setup-lock", async () => {
|
|
4529
4674
|
const existing = await loadSetup(ws.root);
|
|
4530
4675
|
const previousReceipt = await loadSetupReceipt(ws.root, ws.directory, host);
|
|
4531
|
-
const
|
|
4532
|
-
|
|
4676
|
+
const installed = await installedCommand();
|
|
4677
|
+
if (!installed.available) throw new Error(installed.message);
|
|
4533
4678
|
const instructions = await instructionEdits(ws.root, host);
|
|
4534
|
-
const mcp = await mcpEdit(ws.root, host
|
|
4535
|
-
const hooks = await hookEdits(ws.root, host
|
|
4679
|
+
const mcp = await mcpEdit(ws.root, host);
|
|
4680
|
+
const hooks = await hookEdits(ws.root, host);
|
|
4536
4681
|
const ancillary = await ancillaryEdits(ws.root);
|
|
4537
4682
|
const edits = [...ancillary, ...instructions, mcp, ...hooks];
|
|
4538
|
-
const setupBefore = await readText(ws.root,
|
|
4539
|
-
const mcpFingerprint = hash((await inspectHostConfig(ws.root, host, mcp.after
|
|
4540
|
-
const desiredHooks = hookConfig(host, hookCommand(host
|
|
4683
|
+
const setupBefore = await readText(ws.root, SETUP_PATH);
|
|
4684
|
+
const mcpFingerprint = hash((await inspectHostConfig(ws.root, host, mcp.after)).mcp);
|
|
4685
|
+
const desiredHooks = hookConfig(host, hookCommand(host)).hooks;
|
|
4686
|
+
const configuredHook = desiredHooks.SessionStart[0].hooks[0].command;
|
|
4687
|
+
const ownership = await ownershipEdit(ws.root, [...instructions, mcp, hooks[0]], { [hooks[0].path]: configuredHook });
|
|
4541
4688
|
const fingerprint = hash({
|
|
4542
|
-
|
|
4543
|
-
launcher: LAUNCHER,
|
|
4689
|
+
command: "mason",
|
|
4544
4690
|
mcp: mcpFingerprint,
|
|
4545
4691
|
hooks: desiredHooks,
|
|
4546
4692
|
instructions: instructions.map((edit) => edit.path)
|
|
4547
4693
|
});
|
|
4548
4694
|
const previous = existing?.hosts[host];
|
|
4549
4695
|
const changed = edits.some((edit) => edit.before !== edit.after) || previous?.fingerprint !== fingerprint;
|
|
4550
|
-
const revision = !changed && previous ? previous.revision :
|
|
4551
|
-
const setup = existing ?? { version:
|
|
4552
|
-
setup.hosts[host] = {
|
|
4696
|
+
const revision = !changed && previous ? previous.revision : randomUUID6();
|
|
4697
|
+
const setup = existing ?? { version: 2, hosts: {} };
|
|
4698
|
+
setup.hosts[host] = { configuredVersion: installed.version, revision, fingerprint, mcpFingerprint, instructions: instructions.map((e) => e.path) };
|
|
4553
4699
|
const initial = await automate(ws.root, { event: "turn_start" });
|
|
4554
4700
|
const findings = await inspectOnboarding(ws.root, options.base, options.evidence);
|
|
4555
4701
|
const receiptPath = ws.directory + "/setup-" + host + ".json";
|
|
@@ -4564,20 +4710,13 @@ async function setupProject(dir, options = {}) {
|
|
|
4564
4710
|
root: ws.root,
|
|
4565
4711
|
revision
|
|
4566
4712
|
});
|
|
4567
|
-
await installRuntime(ws.root, selected);
|
|
4568
|
-
if (selected.runtime.bundle) {
|
|
4569
|
-
const pointer = `.mason/runtime/${host}.txt`;
|
|
4570
|
-
await applyEdit(ws.root, { path: pointer, before: await readText(ws.root, pointer), after: selected.runtime.id + "\n" });
|
|
4571
|
-
}
|
|
4572
4713
|
const changedFiles = [];
|
|
4714
|
+
if (await applyEdit(ws.root, ownership)) changedFiles.push(ownership.path);
|
|
4573
4715
|
for (const edit of edits) if (await applyEdit(ws.root, edit)) changedFiles.push(edit.path);
|
|
4574
|
-
if (await applyEdit(ws.root, { path:
|
|
4575
|
-
if (!
|
|
4576
|
-
await saveProjectMarker(ws.root, { version: 1, initializedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
4577
|
-
changedFiles.push(".mason/project.json");
|
|
4578
|
-
}
|
|
4716
|
+
if (await applyEdit(ws.root, { path: SETUP_PATH, before: setupBefore, after: JSON.stringify(setup, null, 2) + "\n" })) changedFiles.push(SETUP_PATH);
|
|
4717
|
+
if (await applyEdit(ws.root, await completedHookOwnership(ws.root, hooks[0].path, configuredHook)) && !changedFiles.includes(ownership.path)) changedFiles.push(ownership.path);
|
|
4579
4718
|
const checked = await automate(ws.root, { event: "turn_start" });
|
|
4580
|
-
const configured = await inspectHostConfig(ws.root, host, void 0
|
|
4719
|
+
const configured = await inspectHostConfig(ws.root, host, void 0);
|
|
4581
4720
|
await writeStoreJson(ws.root, receiptPath, {
|
|
4582
4721
|
version: 1,
|
|
4583
4722
|
host,
|
|
@@ -4594,7 +4733,7 @@ async function setupProject(dir, options = {}) {
|
|
|
4594
4733
|
status: "configured",
|
|
4595
4734
|
host,
|
|
4596
4735
|
root: ws.root,
|
|
4597
|
-
runtime:
|
|
4736
|
+
runtime: { kind: "global", command: "mason", version: installed.version },
|
|
4598
4737
|
changedFiles,
|
|
4599
4738
|
initialReportPath,
|
|
4600
4739
|
findings,
|
|
@@ -4608,14 +4747,15 @@ function summarizeSetup(result) {
|
|
|
4608
4747
|
const audit = result.findings.audit;
|
|
4609
4748
|
return [
|
|
4610
4749
|
`Mason configured for ${result.host}.`,
|
|
4611
|
-
"
|
|
4750
|
+
" MCP server and lifecycle hooks use mason from PATH.",
|
|
4612
4751
|
" Project instructions updated; original audit evidence retained.",
|
|
4613
|
-
` ${result.changedFiles.length}
|
|
4752
|
+
` ${result.changedFiles.length} files changed.`,
|
|
4614
4753
|
`Initial audit: ${audit.status}${"counts" in audit ? `; ${audit.counts.issues} issues, ${audit.counts.advisories + audit.counts.suppressedAdvisories} advisories` : ""}.`,
|
|
4615
4754
|
..."issues" in audit ? audit.issues.slice(0, 3).map((f) => " " + f.message) : [],
|
|
4616
4755
|
`Original evidence: ${result.initialReportPath}`,
|
|
4617
4756
|
"Activation: " + result.activation.status + ".",
|
|
4618
4757
|
result.next,
|
|
4758
|
+
"If the assistant cannot find mason, restart the desktop app so it picks up PATH.",
|
|
4619
4759
|
"After the task finishes, run mason-auto status. Configuration alone does not establish activation."
|
|
4620
4760
|
].join("\n");
|
|
4621
4761
|
}
|
|
@@ -4625,7 +4765,6 @@ var init_setup = __esm({
|
|
|
4625
4765
|
init_evidence();
|
|
4626
4766
|
init_store();
|
|
4627
4767
|
init_runtime();
|
|
4628
|
-
init_init();
|
|
4629
4768
|
init_onboarding();
|
|
4630
4769
|
init_storage();
|
|
4631
4770
|
init_files2();
|
|
@@ -4633,8 +4772,8 @@ var init_setup = __esm({
|
|
|
4633
4772
|
init_launcher();
|
|
4634
4773
|
init_adapters();
|
|
4635
4774
|
init_model();
|
|
4636
|
-
init_runtime2();
|
|
4637
4775
|
init_status();
|
|
4776
|
+
init_ownership();
|
|
4638
4777
|
}
|
|
4639
4778
|
});
|
|
4640
4779
|
|
|
@@ -4645,9 +4784,23 @@ init_adapters();
|
|
|
4645
4784
|
init_install();
|
|
4646
4785
|
init_store();
|
|
4647
4786
|
import { parseArgs } from "util";
|
|
4648
|
-
var
|
|
4787
|
+
var TEARDOWN_USAGE = `Usage: mason teardown [options]
|
|
4649
4788
|
|
|
4650
|
-
|
|
4789
|
+
Disconnect Mason from this Git project. Defaults to all project hosts.
|
|
4790
|
+
|
|
4791
|
+
--host codex|claude Disconnect only this host
|
|
4792
|
+
--dir <path> Project directory (defaults to cwd; subdirectories work)
|
|
4793
|
+
--dry-run Preview changes without writing
|
|
4794
|
+
--json Machine-readable changes and diagnostics
|
|
4795
|
+
|
|
4796
|
+
Keeps shared knowledge, repair evidence, ignore rules and the Mason installation.
|
|
4797
|
+
Edited or ambiguous entries are retained for manual cleanup. Restart assistants
|
|
4798
|
+
after teardown. Run mason setup --host codex or --host claude to reconnect.
|
|
4799
|
+
Exit 0: complete/no-op. Exit 2: cleanup needs attention (also in dry runs).`;
|
|
4800
|
+
var USAGE = `Usage: mason-auto <setup|teardown|install|config|status|check|hook> [options]
|
|
4801
|
+
|
|
4802
|
+
setup [--host claude|codex] Connect MCP, instructions and hooks to mason on PATH; retain the initial audit
|
|
4803
|
+
teardown [--host claude|codex] Disconnect one host or all project hosts; retain knowledge and evidence
|
|
4651
4804
|
install --host claude|codex Merge lifecycle hooks into this project's host config
|
|
4652
4805
|
config --host claude|codex Print the host config without writing
|
|
4653
4806
|
status Read configured hooks and observed runtime events
|
|
@@ -4656,9 +4809,11 @@ var USAGE = `Usage: mason-auto <setup|install|config|status|check|hook> [options
|
|
|
4656
4809
|
|
|
4657
4810
|
--dir <path> Project directory (defaults to cwd)
|
|
4658
4811
|
--command <prefix> Installed executable prefix for install/config
|
|
4812
|
+
--dry-run Preview teardown without writing
|
|
4659
4813
|
--json Machine-readable output (status also uses JSON when piped)
|
|
4660
4814
|
|
|
4661
4815
|
check exits 0 for verified checks, 1 for issues, 2 for incomplete/unavailable.
|
|
4816
|
+
teardown exits 0 for complete/no-op, 2 when cleanup needs attention (also in dry runs).
|
|
4662
4817
|
Hooks are advisory and exit 0; a failed capture is reported explicitly.
|
|
4663
4818
|
Local evidence is written under .mason/reports/. No LLM calls or source edits.`;
|
|
4664
4819
|
var parseCli = (argv2) => parseArgs({ args: argv2, allowPositionals: true, options: {
|
|
@@ -4666,7 +4821,8 @@ var parseCli = (argv2) => parseArgs({ args: argv2, allowPositionals: true, optio
|
|
|
4666
4821
|
host: { type: "string" },
|
|
4667
4822
|
command: { type: "string" },
|
|
4668
4823
|
json: { type: "boolean" },
|
|
4669
|
-
help: { type: "boolean", short: "h" }
|
|
4824
|
+
help: { type: "boolean", short: "h" },
|
|
4825
|
+
"dry-run": { type: "boolean" }
|
|
4670
4826
|
} });
|
|
4671
4827
|
function isHookCommand(argv2) {
|
|
4672
4828
|
try {
|
|
@@ -4684,12 +4840,20 @@ async function runAutomationCli(argv2, stdin = "", io = {
|
|
|
4684
4840
|
try {
|
|
4685
4841
|
const { values, positionals } = parseCli(argv2);
|
|
4686
4842
|
if (values.help || !positionals.length) {
|
|
4687
|
-
io.out(USAGE);
|
|
4843
|
+
io.out(positionals[0] === "teardown" ? TEARDOWN_USAGE : USAGE);
|
|
4688
4844
|
return 0;
|
|
4689
4845
|
}
|
|
4690
4846
|
if (positionals.length !== 1) throw new Error("Expected one command.");
|
|
4691
4847
|
[action] = positionals;
|
|
4692
4848
|
const dir = values.dir ?? process.cwd();
|
|
4849
|
+
if (values["dry-run"] !== void 0 && action !== "teardown") throw new Error("--dry-run applies only to teardown.");
|
|
4850
|
+
if (action === "teardown") {
|
|
4851
|
+
if (values.command !== void 0) throw new Error("--command applies only to install/config.");
|
|
4852
|
+
const { teardownProject: teardownProject2, summarizeTeardown: summarizeTeardown2 } = await Promise.resolve().then(() => (init_teardown(), teardown_exports));
|
|
4853
|
+
const result = await teardownProject2(dir, { host: values.host === void 0 ? void 0 : hostSchema.parse(values.host), dryRun: values["dry-run"] });
|
|
4854
|
+
io.out(values.json ? JSON.stringify(result, null, 2) : summarizeTeardown2(result));
|
|
4855
|
+
return result.status === "complete" ? 0 : 2;
|
|
4856
|
+
}
|
|
4693
4857
|
if (action === "hook") {
|
|
4694
4858
|
const output = await runAutomationHook(hostSchema.parse(values.host), stdin);
|
|
4695
4859
|
if (output) io.out(JSON.stringify(output));
|
|
@@ -4720,12 +4884,12 @@ async function runAutomationCli(argv2, stdin = "", io = {
|
|
|
4720
4884
|
io.out(values.json ? JSON.stringify(report, null, 2) : summarize(report));
|
|
4721
4885
|
return report.status === "verified" ? 0 : report.status === "issues-remain" ? 1 : 2;
|
|
4722
4886
|
} catch (error) {
|
|
4723
|
-
const message2 = action === "setup" ? "Mason setup incomplete: " + automationFailure(error).message + ". Rerun the same setup command to resume; retained audit evidence is preserved." : failureMessage(error);
|
|
4887
|
+
const message2 = action === "setup" ? "Mason setup incomplete: " + automationFailure(error).message + ". Rerun the same setup command to resume; retained audit evidence is preserved." : action === "teardown" ? "Mason teardown incomplete: " + automationFailure(error).message + ". Inspect the reported input, then rerun teardown; knowledge and repair evidence are retained." : failureMessage(error);
|
|
4724
4888
|
if (action === "hook" || isHookCommand(argv2)) {
|
|
4725
4889
|
io.out(JSON.stringify({ systemMessage: message2 }));
|
|
4726
4890
|
return 0;
|
|
4727
4891
|
}
|
|
4728
|
-
if (argv2.includes("--json")) io.out(JSON.stringify({ version: 1, ...
|
|
4892
|
+
if (argv2.includes("--json")) io.out(JSON.stringify({ version: 1, ...["setup", "teardown"].includes(action) ? { action, status: "incomplete", next: "Inspect the reported input, then rerun the same command to resume." } : { status: "unavailable" }, failure: automationFailure(error) }));
|
|
4729
4893
|
else io.err(message2);
|
|
4730
4894
|
return 2;
|
|
4731
4895
|
}
|