mason-context 0.14.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/dist/mason-auto.js +448 -636
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-mcp.js +2292 -2453
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason.js +832 -949
- package/dist/mason.js.map +1 -1
- package/package.json +1 -1
package/dist/mason.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.15.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/");
|
|
@@ -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,56 @@ var init_model = __esm({
|
|
|
2898
2893
|
}
|
|
2899
2894
|
});
|
|
2900
2895
|
|
|
2896
|
+
// src/setup/launcher.ts
|
|
2897
|
+
var launcher_exports = {};
|
|
2898
|
+
__export(launcher_exports, {
|
|
2899
|
+
executingVersion: () => executingVersion,
|
|
2900
|
+
globalCommand: () => globalCommand,
|
|
2901
|
+
hookCommand: () => hookCommand,
|
|
2902
|
+
installedCommand: () => installedCommand,
|
|
2903
|
+
mcpCommand: () => mcpCommand,
|
|
2904
|
+
prepareLaunch: () => prepareLaunch
|
|
2905
|
+
});
|
|
2906
|
+
import { execFile as execFile9 } from "child_process";
|
|
2907
|
+
import { promisify as promisify9 } from "util";
|
|
2908
|
+
function globalCommand(args2, platform = process.platform) {
|
|
2909
|
+
return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args2] } : { command: "mason", args: args2 };
|
|
2910
|
+
}
|
|
2911
|
+
async function installedCommand() {
|
|
2912
|
+
const invocation = globalCommand(["internal-integration-version"]);
|
|
2913
|
+
try {
|
|
2914
|
+
const { stdout } = await promisify9(execFile9)(
|
|
2915
|
+
invocation.command,
|
|
2916
|
+
invocation.args,
|
|
2917
|
+
{ timeout: 5e3, maxBuffer: 8192, windowsHide: true }
|
|
2918
|
+
);
|
|
2919
|
+
const result = JSON.parse(stdout);
|
|
2920
|
+
if (result.protocol !== 1 || typeof result.version !== "string") throw new Error("Unsupported Mason integration protocol.");
|
|
2921
|
+
return { available: true, version: result.version, message: null };
|
|
2922
|
+
} catch {
|
|
2923
|
+
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." };
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
async function prepareLaunch(host) {
|
|
2927
|
+
const ws = await workspace(process.cwd());
|
|
2928
|
+
const entry = (await loadSetup(ws.root))?.hosts[host];
|
|
2929
|
+
if (!entry) throw new Error(`Mason is not configured locally for ${host}. Run mason setup --host ${host} in this checkout.`);
|
|
2930
|
+
process.env.MASON_SETUP_ROOT = ws.root;
|
|
2931
|
+
process.env.MASON_SETUP_HOST = host;
|
|
2932
|
+
process.env.MASON_SETUP_REVISION = entry.revision;
|
|
2933
|
+
}
|
|
2934
|
+
var executingVersion, mcpCommand, hookCommand;
|
|
2935
|
+
var init_launcher = __esm({
|
|
2936
|
+
"src/setup/launcher.ts"() {
|
|
2937
|
+
"use strict";
|
|
2938
|
+
init_evidence();
|
|
2939
|
+
init_model();
|
|
2940
|
+
executingVersion = () => true ? "0.15.0" : "development";
|
|
2941
|
+
mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
|
|
2942
|
+
hookCommand = (host) => `mason --setup-host ${host} auto`;
|
|
2943
|
+
}
|
|
2944
|
+
});
|
|
2945
|
+
|
|
2901
2946
|
// src/setup/observations.ts
|
|
2902
2947
|
var observations_exports = {};
|
|
2903
2948
|
__export(observations_exports, {
|
|
@@ -2907,14 +2952,14 @@ __export(observations_exports, {
|
|
|
2907
2952
|
});
|
|
2908
2953
|
import fs13 from "fs/promises";
|
|
2909
2954
|
import { z as z8 } from "zod";
|
|
2910
|
-
function observationPath(directory, host, revision) {
|
|
2911
|
-
return `${directory}/activation/${host}-${revision}.json`;
|
|
2955
|
+
function observationPath(directory, host, revision, version = executingVersion()) {
|
|
2956
|
+
return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
|
|
2912
2957
|
}
|
|
2913
|
-
async function readObservation(root, directory, host, revision) {
|
|
2914
|
-
const raw = await readStoreJson(root, observationPath(directory, host, revision));
|
|
2958
|
+
async function readObservation(root, directory, host, revision, version = executingVersion()) {
|
|
2959
|
+
const raw = await readStoreJson(root, observationPath(directory, host, revision, version));
|
|
2915
2960
|
if (raw === null) return null;
|
|
2916
2961
|
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.");
|
|
2962
|
+
if (record.root !== root || record.host !== host || record.revision !== revision || record.masonVersion !== version) throw new Error("Activation receipt belongs to another installation.");
|
|
2918
2963
|
return record;
|
|
2919
2964
|
}
|
|
2920
2965
|
async function observeActivation(dir, event, options = {}) {
|
|
@@ -2931,6 +2976,7 @@ async function observeActivation(dir, event, options = {}) {
|
|
|
2931
2976
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2932
2977
|
const record = await readObservation(ws.root, ws.directory, host, revision) ?? {
|
|
2933
2978
|
version: 1,
|
|
2979
|
+
masonVersion: executingVersion(),
|
|
2934
2980
|
root: ws.root,
|
|
2935
2981
|
revision,
|
|
2936
2982
|
host,
|
|
@@ -2969,8 +3015,10 @@ var init_observations = __esm({
|
|
|
2969
3015
|
init_store();
|
|
2970
3016
|
init_storage();
|
|
2971
3017
|
init_model();
|
|
3018
|
+
init_launcher();
|
|
2972
3019
|
observationSchema = z8.object({
|
|
2973
3020
|
version: z8.literal(1),
|
|
3021
|
+
masonVersion: z8.string(),
|
|
2974
3022
|
root: z8.string(),
|
|
2975
3023
|
revision: z8.string(),
|
|
2976
3024
|
host: z8.enum(["codex", "claude"]),
|
|
@@ -3060,106 +3108,78 @@ var init_adapters = __esm({
|
|
|
3060
3108
|
}
|
|
3061
3109
|
});
|
|
3062
3110
|
|
|
3063
|
-
// src/
|
|
3064
|
-
var
|
|
3065
|
-
__export(
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
installedAutomation: () => installedAutomation,
|
|
3070
|
-
planAutomationInstall: () => planAutomationInstall
|
|
3111
|
+
// src/setup/files.ts
|
|
3112
|
+
var files_exports = {};
|
|
3113
|
+
__export(files_exports, {
|
|
3114
|
+
applyEdit: () => applyEdit,
|
|
3115
|
+
managedBlock: () => managedBlock,
|
|
3116
|
+
readText: () => readText
|
|
3071
3117
|
});
|
|
3072
|
-
import
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
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]);
|
|
3118
|
+
import fs14 from "fs/promises";
|
|
3119
|
+
import path18 from "path";
|
|
3120
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
3121
|
+
async function readText(root, file) {
|
|
3122
|
+
try {
|
|
3123
|
+
const text2 = await readBoundedFile(await storePath(root, file), 2 * 1024 * 1024);
|
|
3124
|
+
if (text2 === null) throw new Error("Setup input is not a regular file or exceeds 2 MiB: " + file);
|
|
3125
|
+
return text2;
|
|
3126
|
+
} catch (error) {
|
|
3127
|
+
if (error.code === "ENOENT") return null;
|
|
3128
|
+
throw error;
|
|
3091
3129
|
}
|
|
3092
|
-
record.hosts[host] = { command: newCommand };
|
|
3093
|
-
return { file, config: { ...existing, hooks }, record, newCommand };
|
|
3094
3130
|
}
|
|
3095
|
-
|
|
3096
|
-
const
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
command: newCommand,
|
|
3105
|
-
events: HOOK_EVENTS,
|
|
3106
|
-
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.",
|
|
3107
|
-
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."
|
|
3108
|
-
};
|
|
3131
|
+
function managedBlock(text2, start, end, body) {
|
|
3132
|
+
const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
|
|
3133
|
+
if (starts !== ends || starts > 1 || starts === 1 && text2.indexOf(end) < text2.indexOf(start)) {
|
|
3134
|
+
throw new Error("Ambiguous Mason instruction/configuration markers; repair the marked block before setup.");
|
|
3135
|
+
}
|
|
3136
|
+
const eol = text2.includes("\r\n") ? "\r\n" : "\n";
|
|
3137
|
+
const block = [start, body.replace(/\r?\n/g, eol), end].join(eol);
|
|
3138
|
+
if (starts) return text2.slice(0, text2.indexOf(start)) + block + text2.slice(text2.indexOf(end) + end.length);
|
|
3139
|
+
return text2 + (text2.length && !text2.endsWith("\n") ? eol : "") + (text2.length ? eol : "") + block + eol;
|
|
3109
3140
|
}
|
|
3110
|
-
async function
|
|
3111
|
-
const
|
|
3112
|
-
|
|
3113
|
-
if (
|
|
3114
|
-
const
|
|
3115
|
-
const
|
|
3116
|
-
|
|
3117
|
-
const
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3141
|
+
async function applyEdit(root, edit) {
|
|
3142
|
+
const current = await readText(root, edit.path);
|
|
3143
|
+
if (current === edit.after) return false;
|
|
3144
|
+
if (current !== edit.before) throw new Error("Setup input changed during installation: " + edit.path + ". Rerun setup to resume.");
|
|
3145
|
+
const file = await storePath(root, edit.path, true);
|
|
3146
|
+
const temporary = path18.join(path18.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
|
|
3147
|
+
try {
|
|
3148
|
+
const mode = await fs14.stat(file).then((s) => s.mode & 511, () => 384);
|
|
3149
|
+
const handle = await fs14.open(temporary, "wx", mode);
|
|
3150
|
+
try {
|
|
3151
|
+
await handle.writeFile(edit.after, "utf8");
|
|
3152
|
+
await handle.sync();
|
|
3153
|
+
} finally {
|
|
3154
|
+
await handle.close();
|
|
3155
|
+
}
|
|
3156
|
+
if (await readText(root, edit.path) !== edit.before) throw new Error("Setup input changed during installation: " + edit.path);
|
|
3157
|
+
await fs14.rename(temporary, file);
|
|
3158
|
+
return true;
|
|
3159
|
+
} finally {
|
|
3160
|
+
await fs14.rm(temporary, { force: true });
|
|
3127
3161
|
}
|
|
3128
|
-
return result;
|
|
3129
3162
|
}
|
|
3130
|
-
var
|
|
3131
|
-
|
|
3132
|
-
"src/automation/install.ts"() {
|
|
3163
|
+
var init_files2 = __esm({
|
|
3164
|
+
"src/setup/files.ts"() {
|
|
3133
3165
|
"use strict";
|
|
3166
|
+
init_files();
|
|
3134
3167
|
init_storage();
|
|
3135
|
-
init_evidence();
|
|
3136
|
-
init_adapters();
|
|
3137
|
-
init_store();
|
|
3138
|
-
groupSchema = z10.object({ hooks: z10.array(z10.object({ type: z10.string(), command: z10.string().optional() }).passthrough()) }).passthrough();
|
|
3139
|
-
automationConfigSchema = z10.object({ hooks: z10.record(z10.array(groupSchema)).optional() }).passthrough();
|
|
3140
|
-
recordSchema = z10.object({ version: z10.literal(1), hosts: z10.record(z10.object({ command: z10.string() })) });
|
|
3141
|
-
configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
|
|
3142
3168
|
}
|
|
3143
3169
|
});
|
|
3144
3170
|
|
|
3145
3171
|
// src/mcp/init.ts
|
|
3146
|
-
import { z as
|
|
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
|
-
}
|
|
3172
|
+
import { z as z10 } from "zod";
|
|
3154
3173
|
var markerSchema, CLAUDE_MD_SECTION, ASSISTANT_SETUP, QUICKSTART_PLAYBOOK, MAP_PLAYBOOK;
|
|
3155
3174
|
var init_init = __esm({
|
|
3156
3175
|
"src/mcp/init.ts"() {
|
|
3157
3176
|
"use strict";
|
|
3158
3177
|
init_storage();
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3178
|
+
init_files2();
|
|
3179
|
+
markerSchema = z10.object({
|
|
3180
|
+
version: z10.literal(1),
|
|
3181
|
+
initializedAt: z10.string(),
|
|
3182
|
+
features: z10.object({ confluence: z10.boolean().optional() }).passthrough().optional()
|
|
3163
3183
|
}).passthrough();
|
|
3164
3184
|
CLAUDE_MD_SECTION = `<!-- mason:start -->
|
|
3165
3185
|
## Mason project knowledge
|
|
@@ -3181,7 +3201,7 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
|
|
|
3181
3201
|
Inspect source for what the retrieved context does not answer.
|
|
3182
3202
|
<!-- mason:end -->`;
|
|
3183
3203
|
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,
|
|
3204
|
+
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
3205
|
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
3206
|
|
|
3187
3207
|
The managed project guidance is:
|
|
@@ -3189,7 +3209,7 @@ ${CLAUDE_MD_SECTION}`;
|
|
|
3189
3209
|
QUICKSTART_PLAYBOOK = `Start with the audit and review results included in this response. No concept map is required.
|
|
3190
3210
|
|
|
3191
3211
|
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
|
|
3212
|
+
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
3213
|
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
3214
|
|
|
3195
3215
|
${ASSISTANT_SETUP}
|
|
@@ -3231,114 +3251,363 @@ If a build is interrupted, its partials remain in \`.mason/partial-snapshots/\`.
|
|
|
3231
3251
|
}
|
|
3232
3252
|
});
|
|
3233
3253
|
|
|
3234
|
-
// src/
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3254
|
+
// src/utils/git.ts
|
|
3255
|
+
var git_exports = {};
|
|
3256
|
+
__export(git_exports, {
|
|
3257
|
+
isGitRepo: () => isGitRepo
|
|
3258
|
+
});
|
|
3259
|
+
import { execFile as execFile10 } from "child_process";
|
|
3260
|
+
import { promisify as promisify10 } from "util";
|
|
3261
|
+
async function isGitRepo(dir) {
|
|
3238
3262
|
try {
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
const { stdout } = await exec8(
|
|
3242
|
-
"git",
|
|
3243
|
-
["log", `-n${HISTORY_COMMITS}`, "--format=%x01", "--name-only", "-M"],
|
|
3244
|
-
{ cwd: resolvedRoot, maxBuffer: 100 * 1024 * 1024 }
|
|
3245
|
-
);
|
|
3246
|
-
const commitsByFile = /* @__PURE__ */ new Map();
|
|
3247
|
-
const blocks = stdout.split("");
|
|
3248
|
-
let index2 = 0;
|
|
3249
|
-
for (const block of blocks) {
|
|
3250
|
-
const files = block.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith(".mason/"));
|
|
3251
|
-
if (files.length === 0 || files.length > MAX_COMMIT_FILES) continue;
|
|
3252
|
-
for (const file of files) {
|
|
3253
|
-
let set = commitsByFile.get(file);
|
|
3254
|
-
if (!set) {
|
|
3255
|
-
set = /* @__PURE__ */ new Set();
|
|
3256
|
-
commitsByFile.set(file, set);
|
|
3257
|
-
}
|
|
3258
|
-
set.add(index2);
|
|
3259
|
-
}
|
|
3260
|
-
index2++;
|
|
3261
|
-
}
|
|
3262
|
-
return { commitsByFile, totalCommits: index2 };
|
|
3263
|
+
await exec8("git", ["rev-parse", "--git-dir"], { cwd: dir });
|
|
3264
|
+
return true;
|
|
3263
3265
|
} catch {
|
|
3264
|
-
return
|
|
3265
|
-
}
|
|
3266
|
-
}
|
|
3267
|
-
async function findMissingPartners(resolvedRoot, changedFiles, existsOnDisk, allChangedFiles = changedFiles) {
|
|
3268
|
-
const matrix = await buildMatrix(resolvedRoot);
|
|
3269
|
-
if (!matrix) return null;
|
|
3270
|
-
const changedSet = new Set(allChangedFiles);
|
|
3271
|
-
const findings = [];
|
|
3272
|
-
for (const changedFile of changedFiles) {
|
|
3273
|
-
const fileCommits = matrix.commitsByFile.get(changedFile);
|
|
3274
|
-
if (!fileCommits || fileCommits.size < MIN_FILE_COMMITS) continue;
|
|
3275
|
-
for (const [partner, partnerCommits] of matrix.commitsByFile) {
|
|
3276
|
-
if (partner === changedFile || changedSet.has(partner)) continue;
|
|
3277
|
-
let shared = 0;
|
|
3278
|
-
for (const c of fileCommits) {
|
|
3279
|
-
if (partnerCommits.has(c)) shared++;
|
|
3280
|
-
}
|
|
3281
|
-
if (shared < MIN_SHARED_COMMITS) continue;
|
|
3282
|
-
const rate = shared / fileCommits.size;
|
|
3283
|
-
if (rate < MIN_COCHANGE_RATE) continue;
|
|
3284
|
-
if (!await existsOnDisk(partner)) continue;
|
|
3285
|
-
findings.push({
|
|
3286
|
-
changedFile,
|
|
3287
|
-
missingPartner: partner,
|
|
3288
|
-
sharedCommits: shared,
|
|
3289
|
-
fileCommits: fileCommits.size,
|
|
3290
|
-
rate: Math.round(rate * 100) / 100
|
|
3291
|
-
});
|
|
3292
|
-
}
|
|
3266
|
+
return false;
|
|
3293
3267
|
}
|
|
3294
|
-
findings.sort((a, b) => b.rate - a.rate || b.sharedCommits - a.sharedCommits);
|
|
3295
|
-
return findings;
|
|
3296
3268
|
}
|
|
3297
|
-
var exec8
|
|
3298
|
-
var
|
|
3299
|
-
"src/
|
|
3269
|
+
var exec8;
|
|
3270
|
+
var init_git2 = __esm({
|
|
3271
|
+
"src/utils/git.ts"() {
|
|
3300
3272
|
"use strict";
|
|
3301
|
-
exec8 =
|
|
3302
|
-
HISTORY_COMMITS = 1500;
|
|
3303
|
-
MIN_FILE_COMMITS = 5;
|
|
3304
|
-
MIN_SHARED_COMMITS = 4;
|
|
3305
|
-
MIN_COCHANGE_RATE = 0.6;
|
|
3306
|
-
MAX_COMMIT_FILES = 30;
|
|
3273
|
+
exec8 = promisify10(execFile10);
|
|
3307
3274
|
}
|
|
3308
3275
|
});
|
|
3309
3276
|
|
|
3310
|
-
// src/
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3277
|
+
// src/setup/config.ts
|
|
3278
|
+
var config_exports = {};
|
|
3279
|
+
__export(config_exports, {
|
|
3280
|
+
ancillaryEdits: () => ancillaryEdits,
|
|
3281
|
+
hookEdits: () => hookEdits,
|
|
3282
|
+
inspectHostConfig: () => inspectHostConfig,
|
|
3283
|
+
instructionEdits: () => instructionEdits,
|
|
3284
|
+
mcpEdit: () => mcpEdit,
|
|
3285
|
+
mcpPath: () => mcpPath
|
|
3286
|
+
});
|
|
3287
|
+
import { isDeepStrictEqual } from "util";
|
|
3288
|
+
import { parse, stringify } from "smol-toml";
|
|
3289
|
+
import { z as z11 } from "zod";
|
|
3290
|
+
async function instructionEdits(root, host) {
|
|
3291
|
+
const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
|
|
3292
|
+
const existing = found.find((f) => f.text !== null);
|
|
3293
|
+
const primary = host === "codex" && found[0].text === null ? { path: "AGENTS.md", text: null } : existing ?? { path: "AGENTS.md", text: null };
|
|
3294
|
+
const originalGuidance = primary.text === null && existing ? "Follow the existing project conventions in " + existing.path + ".\n" : "";
|
|
3295
|
+
const edits = [{
|
|
3296
|
+
path: primary.path,
|
|
3297
|
+
before: primary.text,
|
|
3298
|
+
after: managedBlock(primary.text ?? originalGuidance, "<!-- mason:start -->", "<!-- mason:end -->", CLAUDE_MD_SECTION.split("\n").slice(1, -1).join("\n"))
|
|
3299
|
+
}];
|
|
3300
|
+
if (primary.path === "AGENTS.md") {
|
|
3301
|
+
const secondary = found.filter((f) => f.path !== "AGENTS.md" && f.text !== null);
|
|
3302
|
+
if (host === "claude" && !secondary.length) secondary.push({ path: "CLAUDE.md", text: null });
|
|
3303
|
+
for (const file of secondary) {
|
|
3304
|
+
const text2 = file.text ?? "";
|
|
3305
|
+
const legacy = text2.includes("<!-- mason:start -->") || text2.includes("<!-- mason:end -->");
|
|
3306
|
+
const imported = "@" + (file.path.startsWith(".claude/") ? "../AGENTS.md" : "AGENTS.md");
|
|
3307
|
+
if (text2.trim() === imported) continue;
|
|
3308
|
+
const pointer = "Mason project knowledge and shared project instructions:\n" + imported;
|
|
3309
|
+
edits.push({
|
|
3310
|
+
path: file.path,
|
|
3311
|
+
before: file.text,
|
|
3312
|
+
after: managedBlock(text2, legacy ? "<!-- mason:start -->" : POINTER_START, legacy ? "<!-- mason:end -->" : POINTER_END, pointer)
|
|
3313
|
+
});
|
|
3324
3314
|
}
|
|
3325
|
-
} catch {
|
|
3326
|
-
return null;
|
|
3327
|
-
}
|
|
3328
|
-
file = file.replace(/\\/g, "/");
|
|
3329
|
-
const root = sourceRoot.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
3330
|
-
if (file.startsWith("/") || /^[A-Za-z]:/.test(file)) {
|
|
3331
|
-
if (!file.startsWith(root + "/")) return null;
|
|
3332
|
-
file = file.slice(root.length + 1);
|
|
3333
3315
|
}
|
|
3334
|
-
return
|
|
3316
|
+
return edits;
|
|
3335
3317
|
}
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3318
|
+
function managedMcp(previous, host) {
|
|
3319
|
+
const options = previous === void 0 ? {} : object.parse(previous);
|
|
3320
|
+
for (const key of ["command", "args", "url", "type", "headers", "http_headers", "env_http_headers", "bearer_token_env_var"]) delete options[key];
|
|
3321
|
+
return { ...options, ...mcpCommand(host) };
|
|
3322
|
+
}
|
|
3323
|
+
async function mcpEdit(root, host) {
|
|
3324
|
+
const file = mcpPath(host), before = await readText(root, file);
|
|
3325
|
+
if (host === "claude") {
|
|
3326
|
+
const config2 = before === null ? {} : object.parse(JSON.parse(before));
|
|
3327
|
+
const servers2 = object.parse(config2.mcpServers ?? {});
|
|
3328
|
+
return { path: file, before, after: JSON.stringify({ ...config2, mcpServers: { ...servers2, mason: managedMcp(servers2.mason, host) } }, null, 2) + "\n" };
|
|
3329
|
+
}
|
|
3330
|
+
const config = parse(before ?? "");
|
|
3331
|
+
const managed = (before ?? "").includes(TOML_START);
|
|
3332
|
+
const servers = config.mcp_servers;
|
|
3333
|
+
const desired = managedMcp(servers?.mason, host);
|
|
3334
|
+
let base = before ?? "";
|
|
3335
|
+
if (servers?.mason && !managed) {
|
|
3336
|
+
const headers = [...base.matchAll(/^\s*\[\[?.+?\]\]?[^\S\r\n]*(?:#.*)?$/gm)];
|
|
3337
|
+
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 }] : []);
|
|
3338
|
+
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.");
|
|
3339
|
+
for (const span of spans.reverse()) base = base.slice(0, span.start) + base.slice(span.end);
|
|
3340
|
+
}
|
|
3341
|
+
const after = managedBlock(base, TOML_START, TOML_END, stringify({ mcp_servers: { mason: desired } }).trimEnd());
|
|
3342
|
+
const parsed = parse(after);
|
|
3343
|
+
const expected = { ...config, mcp_servers: { ...servers ?? {}, mason: desired } };
|
|
3344
|
+
if (!isDeepStrictEqual(parsed, expected)) throw new Error("Could not safely configure the Mason MCP entry without changing other settings.");
|
|
3345
|
+
return { path: file, before, after };
|
|
3346
|
+
}
|
|
3347
|
+
async function hookEdits(root, host) {
|
|
3348
|
+
const file = configPath(host), before = await readText(root, file);
|
|
3349
|
+
const { planAutomationInstall: planAutomationInstall2 } = await Promise.resolve().then(() => (init_install(), install_exports));
|
|
3350
|
+
const plan = await planAutomationInstall2(root, host, hookCommand(host));
|
|
3351
|
+
return [
|
|
3352
|
+
{ path: file, before, after: JSON.stringify(plan.config, null, 2) + "\n" },
|
|
3353
|
+
{ path: AUTOMATION_PATH, before: await readText(root, AUTOMATION_PATH), after: JSON.stringify(plan.record, null, 2) + "\n" }
|
|
3354
|
+
];
|
|
3355
|
+
}
|
|
3356
|
+
async function ancillaryEdits(root) {
|
|
3357
|
+
const ignore = await readText(root, ".gitignore");
|
|
3358
|
+
const { git: git3 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
|
|
3359
|
+
const { isGitRepo: isGitRepo2 } = await Promise.resolve().then(() => (init_git2(), git_exports));
|
|
3360
|
+
let parentIgnored = false;
|
|
3361
|
+
try {
|
|
3362
|
+
if (await isGitRepo2(root)) parentIgnored = !!(await git3(root, "check-ignore", "--no-index", ".mason")).trim();
|
|
3363
|
+
} catch (error) {
|
|
3364
|
+
if (error.code !== 1) throw error;
|
|
3365
|
+
}
|
|
3366
|
+
const retainParentRule = parentIgnored || !!ignore?.replace(/\r\n/g, "\n").includes("# mason:ignore:start\n!/.mason/\n/.mason/*");
|
|
3367
|
+
const rules = [
|
|
3368
|
+
...retainParentRule ? ["!/.mason/", "/.mason/*"] : [],
|
|
3369
|
+
"!/.mason/decisions/",
|
|
3370
|
+
"!/.mason/decisions/**",
|
|
3371
|
+
"!/.mason/config.json",
|
|
3372
|
+
"!/.mason/snapshot.json",
|
|
3373
|
+
"/.mason/local/",
|
|
3374
|
+
"/.mason/reports/"
|
|
3375
|
+
].join("\n");
|
|
3376
|
+
return [{ path: ".gitignore", before: ignore, after: managedBlock(ignore ?? "", "# mason:ignore:start", "# mason:ignore:end", rules) }];
|
|
3377
|
+
}
|
|
3378
|
+
async function inspectHostConfig(root, host, plannedMcp) {
|
|
3379
|
+
const text2 = plannedMcp ?? await readText(root, mcpPath(host));
|
|
3380
|
+
const config = host === "codex" ? parse(text2 ?? "") : object.parse(JSON.parse(text2 ?? "{}"));
|
|
3381
|
+
const servers = host === "codex" ? config.mcp_servers : config.mcpServers;
|
|
3382
|
+
const hooks = automationConfigSchema.parse(JSON.parse(await readText(root, configPath(host)) ?? "{}"));
|
|
3383
|
+
const expected = hookConfig(host, hookCommand(host));
|
|
3384
|
+
const mcp = servers?.mason === void 0 ? null : object.parse(servers.mason);
|
|
3385
|
+
return {
|
|
3386
|
+
mcp,
|
|
3387
|
+
mcpDisabled: mcp?.enabled === false,
|
|
3388
|
+
hooks: Object.fromEntries(Object.keys(expected.hooks).map((event) => [
|
|
3389
|
+
event,
|
|
3390
|
+
(hooks.hooks?.[event] ?? []).flatMap((group) => group.hooks.filter((handler) => handler.command === expected.hooks.SessionStart[0].hooks[0].command).map((handler) => ({ ...group, hooks: [handler] })) ?? [])
|
|
3391
|
+
])),
|
|
3392
|
+
disabled: hooks.disableAllHooks === true || config.features?.hooks === false || config.features?.codex_hooks === false
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3395
|
+
var object, mcpPath, TOML_START, TOML_END, POINTER_START, POINTER_END;
|
|
3396
|
+
var init_config = __esm({
|
|
3397
|
+
"src/setup/config.ts"() {
|
|
3398
|
+
"use strict";
|
|
3399
|
+
init_init();
|
|
3400
|
+
init_docs();
|
|
3401
|
+
init_adapters();
|
|
3402
|
+
init_install();
|
|
3403
|
+
init_files2();
|
|
3404
|
+
init_launcher();
|
|
3405
|
+
object = z11.record(z11.unknown());
|
|
3406
|
+
mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
|
|
3407
|
+
TOML_START = "# mason:mcp:start";
|
|
3408
|
+
TOML_END = "# mason:mcp:end";
|
|
3409
|
+
POINTER_START = "<!-- mason:agents:start -->";
|
|
3410
|
+
POINTER_END = "<!-- mason:agents:end -->";
|
|
3411
|
+
}
|
|
3412
|
+
});
|
|
3413
|
+
|
|
3414
|
+
// src/automation/install.ts
|
|
3415
|
+
var install_exports = {};
|
|
3416
|
+
__export(install_exports, {
|
|
3417
|
+
AUTOMATION_PATH: () => AUTOMATION_PATH,
|
|
3418
|
+
automationConfigSchema: () => automationConfigSchema,
|
|
3419
|
+
configPath: () => configPath,
|
|
3420
|
+
installAutomation: () => installAutomation,
|
|
3421
|
+
installedAutomation: () => installedAutomation,
|
|
3422
|
+
planAutomationInstall: () => planAutomationInstall
|
|
3423
|
+
});
|
|
3424
|
+
import { z as z12 } from "zod";
|
|
3425
|
+
async function installationRecord(root) {
|
|
3426
|
+
return recordSchema.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
|
|
3427
|
+
}
|
|
3428
|
+
async function installAutomation(dir, host, command2) {
|
|
3429
|
+
const ws = await workspace(dir);
|
|
3430
|
+
return withLock(ws.root, ".mason/reports/automation-install", () => installLocked(ws.root, host, command2));
|
|
3431
|
+
}
|
|
3432
|
+
async function planAutomationInstall(root, host, command2) {
|
|
3433
|
+
const file = configPath(host);
|
|
3434
|
+
const existing = automationConfigSchema.parse(await readStoreJson(root, file) ?? {});
|
|
3435
|
+
const record = await installationRecord(root);
|
|
3436
|
+
const desired = hookConfig(host, command2);
|
|
3437
|
+
const newCommand = desired.hooks.SessionStart[0].hooks[0].command;
|
|
3438
|
+
const previous = record.hosts[host]?.command;
|
|
3439
|
+
const owned = /* @__PURE__ */ new Set([previous, newCommand]);
|
|
3440
|
+
const hooks = existing.hooks ?? {};
|
|
3441
|
+
for (const event of HOOK_EVENTS) {
|
|
3442
|
+
hooks[event] = (hooks[event] ?? []).map((group) => ({
|
|
3443
|
+
...group,
|
|
3444
|
+
hooks: group.hooks.filter((handler) => !(handler.type === "command" && typeof handler.command === "string" && owned.has(handler.command)))
|
|
3445
|
+
})).filter((group) => group.hooks.length);
|
|
3446
|
+
hooks[event].push(...desired.hooks[event]);
|
|
3447
|
+
}
|
|
3448
|
+
record.hosts[host] = { command: newCommand };
|
|
3449
|
+
return { file, config: { ...existing, hooks }, record, newCommand };
|
|
3450
|
+
}
|
|
3451
|
+
async function installLocked(root, host, command2) {
|
|
3452
|
+
const { file, config, record, newCommand } = await planAutomationInstall(root, host, command2);
|
|
3453
|
+
const { ancillaryEdits: ancillaryEdits2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
3454
|
+
const { applyEdit: applyEdit2 } = await Promise.resolve().then(() => (init_files2(), files_exports));
|
|
3455
|
+
for (const edit of await ancillaryEdits2(root)) await applyEdit2(root, edit);
|
|
3456
|
+
await writeStoreJson(root, file, config);
|
|
3457
|
+
await writeStoreJson(root, AUTOMATION_PATH, record);
|
|
3458
|
+
return {
|
|
3459
|
+
version: 1,
|
|
3460
|
+
host,
|
|
3461
|
+
configPath: file,
|
|
3462
|
+
status: "configured",
|
|
3463
|
+
command: newCommand,
|
|
3464
|
+
events: HOOK_EVENTS,
|
|
3465
|
+
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.",
|
|
3466
|
+
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."
|
|
3467
|
+
};
|
|
3468
|
+
}
|
|
3469
|
+
async function installedAutomation(dir) {
|
|
3470
|
+
const ws = await workspace(dir);
|
|
3471
|
+
const record = await installationRecord(ws.root);
|
|
3472
|
+
const result = {};
|
|
3473
|
+
for (const host of ["claude", "codex"]) {
|
|
3474
|
+
const expected = record.hosts[host];
|
|
3475
|
+
if (!expected) continue;
|
|
3476
|
+
const current = automationConfigSchema.parse(await readStoreJson(ws.root, configPath(host)) ?? {});
|
|
3477
|
+
const configuredEvents = HOOK_EVENTS.filter((event) => current.hooks?.[event]?.some((group) => group.hooks.some((handler) => handler.type === "command" && handler.command === expected.command)));
|
|
3478
|
+
result[host] = {
|
|
3479
|
+
configPath: configPath(host),
|
|
3480
|
+
configuredEvents,
|
|
3481
|
+
status: current.disableAllHooks === true ? "disabled" : configuredEvents.length === HOOK_EVENTS.length ? "configured" : "incomplete",
|
|
3482
|
+
runtime: "Host version, trust, policy, and tool coverage still determine execution; inspect observed events."
|
|
3483
|
+
};
|
|
3484
|
+
}
|
|
3485
|
+
return result;
|
|
3486
|
+
}
|
|
3487
|
+
var groupSchema, automationConfigSchema, recordSchema, AUTOMATION_PATH, configPath;
|
|
3488
|
+
var init_install = __esm({
|
|
3489
|
+
"src/automation/install.ts"() {
|
|
3490
|
+
"use strict";
|
|
3491
|
+
init_storage();
|
|
3492
|
+
init_evidence();
|
|
3493
|
+
init_adapters();
|
|
3494
|
+
init_store();
|
|
3495
|
+
groupSchema = z12.object({ hooks: z12.array(z12.object({ type: z12.string(), command: z12.string().optional() }).passthrough()) }).passthrough();
|
|
3496
|
+
automationConfigSchema = z12.object({ hooks: z12.record(z12.array(groupSchema)).optional() }).passthrough();
|
|
3497
|
+
recordSchema = z12.object({ version: z12.literal(1), hosts: z12.record(z12.object({ command: z12.string() })) });
|
|
3498
|
+
AUTOMATION_PATH = ".mason/local/automation.json";
|
|
3499
|
+
configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
|
|
3500
|
+
}
|
|
3501
|
+
});
|
|
3502
|
+
|
|
3503
|
+
// src/review/cochange.ts
|
|
3504
|
+
import { execFile as execFile11 } from "child_process";
|
|
3505
|
+
import { promisify as promisify11 } from "util";
|
|
3506
|
+
async function buildMatrix(resolvedRoot) {
|
|
3507
|
+
try {
|
|
3508
|
+
const { stdout: shallow } = await exec9("git", ["rev-parse", "--is-shallow-repository"], { cwd: resolvedRoot });
|
|
3509
|
+
if (shallow.trim() === "true") return null;
|
|
3510
|
+
const { stdout } = await exec9(
|
|
3511
|
+
"git",
|
|
3512
|
+
["log", `-n${HISTORY_COMMITS}`, "--format=%x01", "--name-only", "-M"],
|
|
3513
|
+
{ cwd: resolvedRoot, maxBuffer: 100 * 1024 * 1024 }
|
|
3514
|
+
);
|
|
3515
|
+
const commitsByFile = /* @__PURE__ */ new Map();
|
|
3516
|
+
const blocks = stdout.split("");
|
|
3517
|
+
let index2 = 0;
|
|
3518
|
+
for (const block of blocks) {
|
|
3519
|
+
const files = block.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith(".mason/"));
|
|
3520
|
+
if (files.length === 0 || files.length > MAX_COMMIT_FILES) continue;
|
|
3521
|
+
for (const file of files) {
|
|
3522
|
+
let set = commitsByFile.get(file);
|
|
3523
|
+
if (!set) {
|
|
3524
|
+
set = /* @__PURE__ */ new Set();
|
|
3525
|
+
commitsByFile.set(file, set);
|
|
3526
|
+
}
|
|
3527
|
+
set.add(index2);
|
|
3528
|
+
}
|
|
3529
|
+
index2++;
|
|
3530
|
+
}
|
|
3531
|
+
return { commitsByFile, totalCommits: index2 };
|
|
3532
|
+
} catch {
|
|
3533
|
+
return null;
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
async function findMissingPartners(resolvedRoot, changedFiles, existsOnDisk, allChangedFiles = changedFiles) {
|
|
3537
|
+
const matrix = await buildMatrix(resolvedRoot);
|
|
3538
|
+
if (!matrix) return null;
|
|
3539
|
+
const changedSet = new Set(allChangedFiles);
|
|
3540
|
+
const findings = [];
|
|
3541
|
+
for (const changedFile of changedFiles) {
|
|
3542
|
+
const fileCommits = matrix.commitsByFile.get(changedFile);
|
|
3543
|
+
if (!fileCommits || fileCommits.size < MIN_FILE_COMMITS) continue;
|
|
3544
|
+
for (const [partner, partnerCommits] of matrix.commitsByFile) {
|
|
3545
|
+
if (partner === changedFile || changedSet.has(partner)) continue;
|
|
3546
|
+
let shared = 0;
|
|
3547
|
+
for (const c of fileCommits) {
|
|
3548
|
+
if (partnerCommits.has(c)) shared++;
|
|
3549
|
+
}
|
|
3550
|
+
if (shared < MIN_SHARED_COMMITS) continue;
|
|
3551
|
+
const rate = shared / fileCommits.size;
|
|
3552
|
+
if (rate < MIN_COCHANGE_RATE) continue;
|
|
3553
|
+
if (!await existsOnDisk(partner)) continue;
|
|
3554
|
+
findings.push({
|
|
3555
|
+
changedFile,
|
|
3556
|
+
missingPartner: partner,
|
|
3557
|
+
sharedCommits: shared,
|
|
3558
|
+
fileCommits: fileCommits.size,
|
|
3559
|
+
rate: Math.round(rate * 100) / 100
|
|
3560
|
+
});
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
findings.sort((a, b) => b.rate - a.rate || b.sharedCommits - a.sharedCommits);
|
|
3564
|
+
return findings;
|
|
3565
|
+
}
|
|
3566
|
+
var exec9, HISTORY_COMMITS, MIN_FILE_COMMITS, MIN_SHARED_COMMITS, MIN_COCHANGE_RATE, MAX_COMMIT_FILES;
|
|
3567
|
+
var init_cochange = __esm({
|
|
3568
|
+
"src/review/cochange.ts"() {
|
|
3569
|
+
"use strict";
|
|
3570
|
+
exec9 = promisify11(execFile11);
|
|
3571
|
+
HISTORY_COMMITS = 1500;
|
|
3572
|
+
MIN_FILE_COMMITS = 5;
|
|
3573
|
+
MIN_SHARED_COMMITS = 4;
|
|
3574
|
+
MIN_COCHANGE_RATE = 0.6;
|
|
3575
|
+
MAX_COMMIT_FILES = 30;
|
|
3576
|
+
}
|
|
3577
|
+
});
|
|
3578
|
+
|
|
3579
|
+
// src/review/evidence/paths.ts
|
|
3580
|
+
function evidencePath(value, sourceRoot, uri = false) {
|
|
3581
|
+
if (value.length > 4e3) return null;
|
|
3582
|
+
let file = value;
|
|
3583
|
+
try {
|
|
3584
|
+
if (uri) {
|
|
3585
|
+
if (file.startsWith("file:")) {
|
|
3586
|
+
const url = new URL(file);
|
|
3587
|
+
if (url.hostname && url.hostname !== "localhost") return null;
|
|
3588
|
+
file = decodeURIComponent(url.pathname).replace(/^\/([A-Za-z]:\/)/, "$1");
|
|
3589
|
+
} else {
|
|
3590
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(file)) return null;
|
|
3591
|
+
file = decodeURIComponent(file);
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
} catch {
|
|
3595
|
+
return null;
|
|
3596
|
+
}
|
|
3597
|
+
file = file.replace(/\\/g, "/");
|
|
3598
|
+
const root = sourceRoot.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
3599
|
+
if (file.startsWith("/") || /^[A-Za-z]:/.test(file)) {
|
|
3600
|
+
if (!file.startsWith(root + "/")) return null;
|
|
3601
|
+
file = file.slice(root.length + 1);
|
|
3602
|
+
}
|
|
3603
|
+
return normalizeRepoPath(file);
|
|
3604
|
+
}
|
|
3605
|
+
var init_paths2 = __esm({
|
|
3606
|
+
"src/review/evidence/paths.ts"() {
|
|
3607
|
+
"use strict";
|
|
3608
|
+
init_paths();
|
|
3609
|
+
}
|
|
3610
|
+
});
|
|
3342
3611
|
|
|
3343
3612
|
// src/review/evidence/types.ts
|
|
3344
3613
|
function messagePreview(message2) {
|
|
@@ -3353,7 +3622,7 @@ var init_types2 = __esm({
|
|
|
3353
3622
|
});
|
|
3354
3623
|
|
|
3355
3624
|
// src/review/evidence/vitest.ts
|
|
3356
|
-
import { z as
|
|
3625
|
+
import { z as z13 } from "zod";
|
|
3357
3626
|
function parseVitest(raw, sourceRoot) {
|
|
3358
3627
|
const report = schema.parse(raw);
|
|
3359
3628
|
const findings = [], diagnostics = [];
|
|
@@ -3402,24 +3671,24 @@ var init_vitest = __esm({
|
|
|
3402
3671
|
"use strict";
|
|
3403
3672
|
init_paths2();
|
|
3404
3673
|
init_types2();
|
|
3405
|
-
count2 =
|
|
3406
|
-
schema =
|
|
3407
|
-
success:
|
|
3674
|
+
count2 = z13.number().int().nonnegative();
|
|
3675
|
+
schema = z13.object({
|
|
3676
|
+
success: z13.boolean(),
|
|
3408
3677
|
numTotalTests: count2,
|
|
3409
3678
|
numPassedTests: count2,
|
|
3410
3679
|
numFailedTests: count2,
|
|
3411
3680
|
numPendingTests: count2,
|
|
3412
3681
|
numTodoTests: count2,
|
|
3413
3682
|
numFailedTestSuites: count2,
|
|
3414
|
-
testResults:
|
|
3415
|
-
name:
|
|
3416
|
-
status:
|
|
3417
|
-
message:
|
|
3418
|
-
assertionResults:
|
|
3419
|
-
fullName:
|
|
3420
|
-
status:
|
|
3421
|
-
failureMessages:
|
|
3422
|
-
location:
|
|
3683
|
+
testResults: z13.array(z13.object({
|
|
3684
|
+
name: z13.string().min(1),
|
|
3685
|
+
status: z13.enum(["passed", "failed"]),
|
|
3686
|
+
message: z13.string().optional(),
|
|
3687
|
+
assertionResults: z13.array(z13.object({
|
|
3688
|
+
fullName: z13.string(),
|
|
3689
|
+
status: z13.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
|
|
3690
|
+
failureMessages: z13.array(z13.string()).nullable().optional(),
|
|
3691
|
+
location: z13.object({ line: count2, column: count2 }).nullable().optional()
|
|
3423
3692
|
}))
|
|
3424
3693
|
}))
|
|
3425
3694
|
});
|
|
@@ -3427,7 +3696,7 @@ var init_vitest = __esm({
|
|
|
3427
3696
|
});
|
|
3428
3697
|
|
|
3429
3698
|
// src/review/evidence/sarif.ts
|
|
3430
|
-
import { z as
|
|
3699
|
+
import { z as z14 } from "zod";
|
|
3431
3700
|
function parseSarif(raw, sourceRoot) {
|
|
3432
3701
|
const report = schema2.parse(raw), findings = [], diagnostics = [];
|
|
3433
3702
|
let active = 0, suppressed = 0, absent = 0, unresolved = 0;
|
|
@@ -3518,29 +3787,29 @@ var init_sarif = __esm({
|
|
|
3518
3787
|
"use strict";
|
|
3519
3788
|
init_paths2();
|
|
3520
3789
|
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:
|
|
3790
|
+
index = z14.number().int().nonnegative();
|
|
3791
|
+
artifact = z14.object({ uri: z14.string().optional(), uriBaseId: z14.string().optional(), index: index.optional() });
|
|
3792
|
+
message = z14.object({ text: z14.string().optional(), markdown: z14.string().optional(), id: z14.string().optional(), arguments: z14.array(z14.string()).optional() });
|
|
3793
|
+
level = z14.enum(["error", "warning", "note", "none"]);
|
|
3794
|
+
location = z14.object({ physicalLocation: z14.object({ artifactLocation: artifact.optional(), region: z14.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
|
|
3795
|
+
schema2 = z14.object({
|
|
3796
|
+
version: z14.literal("2.1.0"),
|
|
3797
|
+
runs: z14.array(z14.object({
|
|
3798
|
+
tool: z14.object({ driver: z14.object({ name: z14.string().min(1), rules: z14.array(z14.object({ id: z14.string(), defaultConfiguration: z14.object({ level: level.optional() }).optional(), messageStrings: z14.record(message).optional() })).optional(), globalMessageStrings: z14.record(message).optional() }) }),
|
|
3799
|
+
invocations: z14.array(z14.object({ executionSuccessful: z14.boolean(), commandLine: z14.string().optional(), toolExecutionNotifications: z14.array(z14.object({ level: level.optional(), message })).optional() })).optional(),
|
|
3800
|
+
versionControlProvenance: z14.array(z14.object({ revisionId: z14.string().optional() })).optional(),
|
|
3801
|
+
originalUriBaseIds: z14.record(artifact).optional(),
|
|
3802
|
+
artifacts: z14.array(z14.object({ location: artifact.optional() })).optional(),
|
|
3803
|
+
results: z14.array(z14.object({
|
|
3804
|
+
ruleId: z14.string().optional(),
|
|
3536
3805
|
ruleIndex: index.optional(),
|
|
3537
3806
|
message,
|
|
3538
3807
|
level: level.optional(),
|
|
3539
|
-
kind:
|
|
3540
|
-
baselineState:
|
|
3541
|
-
suppressions:
|
|
3542
|
-
locations:
|
|
3543
|
-
relatedLocations:
|
|
3808
|
+
kind: z14.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
|
|
3809
|
+
baselineState: z14.enum(["new", "unchanged", "updated", "absent"]).optional(),
|
|
3810
|
+
suppressions: z14.array(z14.object({ kind: z14.enum(["inSource", "external"]), status: z14.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
|
|
3811
|
+
locations: z14.array(location).optional(),
|
|
3812
|
+
relatedLocations: z14.array(location).optional()
|
|
3544
3813
|
})).optional()
|
|
3545
3814
|
}))
|
|
3546
3815
|
});
|
|
@@ -3548,10 +3817,10 @@ var init_sarif = __esm({
|
|
|
3548
3817
|
});
|
|
3549
3818
|
|
|
3550
3819
|
// src/review/evidence.ts
|
|
3551
|
-
import
|
|
3552
|
-
import { z as
|
|
3820
|
+
import path19 from "path";
|
|
3821
|
+
import { z as z15 } from "zod";
|
|
3553
3822
|
function relativeArtifact(root, file) {
|
|
3554
|
-
const relative =
|
|
3823
|
+
const relative = path19.isAbsolute(file) ? path19.relative(root, file) : file;
|
|
3555
3824
|
const normalized = normalizeRepoPath(relative);
|
|
3556
3825
|
if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
|
|
3557
3826
|
return normalized;
|
|
@@ -3590,7 +3859,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
3590
3859
|
let manifest, raw;
|
|
3591
3860
|
try {
|
|
3592
3861
|
manifest = relativeArtifact(root, supplied);
|
|
3593
|
-
raw =
|
|
3862
|
+
raw = z15.object({ version: z15.literal(1), checks: z15.array(z15.unknown()) }).parse(await readStoreJson(root, manifest));
|
|
3594
3863
|
} catch (error) {
|
|
3595
3864
|
output.diagnostics.push(`${supplied}: ${String(error)}`);
|
|
3596
3865
|
continue;
|
|
@@ -3647,7 +3916,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
3647
3916
|
const report = await readStoreJson(root, file);
|
|
3648
3917
|
if (report === null) throw new Error(`Report artifact is missing: ${file}`);
|
|
3649
3918
|
const sourceRoot = check.sourceRoot ?? root;
|
|
3650
|
-
if (!
|
|
3919
|
+
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
3920
|
const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : parseSarif(report, sourceRoot);
|
|
3652
3921
|
result.outcome = parsed.outcome;
|
|
3653
3922
|
result.counts = parsed.counts;
|
|
@@ -3743,31 +4012,31 @@ var init_evidence2 = __esm({
|
|
|
3743
4012
|
init_vitest();
|
|
3744
4013
|
init_sarif();
|
|
3745
4014
|
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:
|
|
4015
|
+
checkSchema2 = z15.object({
|
|
4016
|
+
id: z15.string().min(1).max(100),
|
|
4017
|
+
kind: z15.enum(["tests", "static-analysis", "security", "complexity", "duplication"]),
|
|
4018
|
+
tool: z15.string().min(1).max(200),
|
|
4019
|
+
command: z15.string().min(1).max(2e3),
|
|
4020
|
+
commit: z15.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
|
|
4021
|
+
workingTreeClean: z15.boolean().optional(),
|
|
4022
|
+
source: z15.string().max(2e3).optional(),
|
|
4023
|
+
sourceRoot: z15.string().min(1).max(4e3).optional(),
|
|
4024
|
+
status: z15.enum(["completed", "skipped", "unavailable"]).default("completed"),
|
|
4025
|
+
reason: z15.string().min(1).max(2e3).optional(),
|
|
4026
|
+
exitCode: z15.number().int().nullable().optional(),
|
|
4027
|
+
report: z15.object({ format: z15.enum(["vitest-json", "sarif"]), path: z15.string().min(1).max(4e3) }).optional()
|
|
3759
4028
|
});
|
|
3760
4029
|
}
|
|
3761
4030
|
});
|
|
3762
4031
|
|
|
3763
4032
|
// src/review/review.ts
|
|
3764
|
-
import
|
|
3765
|
-
import
|
|
3766
|
-
import { execFile as
|
|
3767
|
-
import { promisify as
|
|
4033
|
+
import fs15 from "fs/promises";
|
|
4034
|
+
import path20 from "path";
|
|
4035
|
+
import { execFile as execFile12 } from "child_process";
|
|
4036
|
+
import { promisify as promisify12 } from "util";
|
|
3768
4037
|
async function resolveMergeBase(resolvedRoot, base, head) {
|
|
3769
4038
|
try {
|
|
3770
|
-
const { stdout } = await
|
|
4039
|
+
const { stdout } = await exec10("git", ["merge-base", base, head], {
|
|
3771
4040
|
cwd: resolvedRoot
|
|
3772
4041
|
});
|
|
3773
4042
|
return stdout.trim() || null;
|
|
@@ -3778,7 +4047,7 @@ async function resolveMergeBase(resolvedRoot, base, head) {
|
|
|
3778
4047
|
async function defaultBase(resolvedRoot) {
|
|
3779
4048
|
for (const ref of ["origin/HEAD", "origin/main", "origin/master", "main"]) {
|
|
3780
4049
|
try {
|
|
3781
|
-
await
|
|
4050
|
+
await exec10("git", ["rev-parse", "--verify", "--quiet", ref], {
|
|
3782
4051
|
cwd: resolvedRoot
|
|
3783
4052
|
});
|
|
3784
4053
|
return ref;
|
|
@@ -3791,7 +4060,7 @@ function anchorsTouched(record, changedFiles) {
|
|
|
3791
4060
|
return matchingPaths(record.files, changedFiles);
|
|
3792
4061
|
}
|
|
3793
4062
|
async function computeReview(rootDir, base, options = {}) {
|
|
3794
|
-
const resolvedRoot =
|
|
4063
|
+
const resolvedRoot = path20.resolve(rootDir);
|
|
3795
4064
|
const head = await getCurrentGitHash(resolvedRoot);
|
|
3796
4065
|
if (head === "unknown") return null;
|
|
3797
4066
|
const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
|
|
@@ -3820,589 +4089,158 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
3820
4089
|
if (report.evidence.status === "passed") report.evidence.status = "incomplete";
|
|
3821
4090
|
}
|
|
3822
4091
|
}
|
|
3823
|
-
const finalize = async () => {
|
|
3824
|
-
if (report.evidence && await getCurrentGitHash(resolvedRoot) !== head) {
|
|
3825
|
-
report.evidence.diagnostics.push("HEAD changed during the review; rerun to obtain consistent change and knowledge associations.");
|
|
3826
|
-
for (const check of report.evidence.checks) check.freshness = "unknown";
|
|
3827
|
-
report.evidence.summary.stale = 0;
|
|
3828
|
-
report.evidence.summary.unknown = report.evidence.checks.length;
|
|
3829
|
-
if (report.evidence.status !== "unavailable") report.evidence.status = "incomplete";
|
|
3830
|
-
}
|
|
3831
|
-
return report;
|
|
3832
|
-
};
|
|
3833
|
-
if (changedFiles.length === 0) return finalize();
|
|
3834
|
-
let analyzed = changedFiles;
|
|
3835
|
-
if (changedFiles.length > MAX_ANALYZED_FILES) {
|
|
3836
|
-
analyzed = changedFiles.slice(0, MAX_ANALYZED_FILES);
|
|
3837
|
-
report.truncated = true;
|
|
3838
|
-
}
|
|
3839
|
-
const partners = await findMissingPartners(
|
|
3840
|
-
resolvedRoot,
|
|
3841
|
-
analyzed,
|
|
3842
|
-
async (relPath) => {
|
|
3843
|
-
try {
|
|
3844
|
-
await fs14.access(path19.join(resolvedRoot, relPath));
|
|
3845
|
-
return true;
|
|
3846
|
-
} catch {
|
|
3847
|
-
return false;
|
|
3848
|
-
}
|
|
3849
|
-
},
|
|
3850
|
-
changedFiles
|
|
3851
|
-
);
|
|
3852
|
-
if (partners === null) {
|
|
3853
|
-
report.historyAvailable = false;
|
|
3854
|
-
} else {
|
|
3855
|
-
report.missingPartners = partners;
|
|
3856
|
-
}
|
|
3857
|
-
const decisions = store.records;
|
|
3858
|
-
for (const record of decisions) {
|
|
3859
|
-
if (record.status !== "active") continue;
|
|
3860
|
-
const effective = effectiveDecision(record);
|
|
3861
|
-
const touched = anchorsTouched(effective, changedFiles);
|
|
3862
|
-
const proposalTouched = effective !== record ? anchorsTouched(record, changedFiles) : [];
|
|
3863
|
-
if (touched.length > 0 || proposalTouched.length > 0) {
|
|
3864
|
-
const { pendingProposal, ...knowledge } = decisionKnowledge(record, decisionDrift.freshness?.[record.id] ?? "unknown", decisionDrift.pendingProposals?.[record.id]?.freshness ?? "unknown");
|
|
3865
|
-
report.touchedDecisions.push({
|
|
3866
|
-
...knowledge,
|
|
3867
|
-
...pendingProposal ? { pendingProposal: { ...pendingProposal, touchedFiles: proposalTouched } } : {},
|
|
3868
|
-
id: record.id,
|
|
3869
|
-
anchors: effective.files,
|
|
3870
|
-
freshness: decisionDrift.freshness?.[record.id] ?? "unknown",
|
|
3871
|
-
touchedFiles: touched
|
|
3872
|
-
});
|
|
3873
|
-
}
|
|
3874
|
-
}
|
|
3875
|
-
return finalize();
|
|
3876
|
-
}
|
|
3877
|
-
var exec9, MAX_ANALYZED_FILES;
|
|
3878
|
-
var init_review = __esm({
|
|
3879
|
-
"src/review/review.ts"() {
|
|
3880
|
-
"use strict";
|
|
3881
|
-
init_drift();
|
|
3882
|
-
init_provenance();
|
|
3883
|
-
init_decisions();
|
|
3884
|
-
init_paths();
|
|
3885
|
-
init_drift2();
|
|
3886
|
-
init_cochange();
|
|
3887
|
-
init_evidence2();
|
|
3888
|
-
init_snapshot();
|
|
3889
|
-
exec9 = promisify10(execFile10);
|
|
3890
|
-
MAX_ANALYZED_FILES = 50;
|
|
3891
|
-
}
|
|
3892
|
-
});
|
|
3893
|
-
|
|
3894
|
-
// src/mcp/onboarding.ts
|
|
3895
|
-
async function auditSummary(root) {
|
|
3896
|
-
try {
|
|
3897
|
-
const report = await computeAudit(root);
|
|
3898
|
-
if (!report) return { status: "no-context-files", reason: "No AGENTS.md, CLAUDE.md, or .claude/CLAUDE.md found to audit." };
|
|
3899
|
-
if (!report.gitAvailable) return { status: "unavailable", reason: "Audit needs readable Git history to verify documentation claims.", docs: report.docs };
|
|
3900
|
-
return {
|
|
3901
|
-
...report,
|
|
3902
|
-
status: "complete",
|
|
3903
|
-
issues: report.issues.slice(0, MAX_FINDINGS2),
|
|
3904
|
-
advisories: report.advisories.slice(0, MAX_FINDINGS2),
|
|
3905
|
-
suppressedAdvisories: (report.suppressedAdvisories ?? []).slice(0, MAX_FINDINGS2),
|
|
3906
|
-
counts: {
|
|
3907
|
-
issues: report.issues.length,
|
|
3908
|
-
advisories: report.advisories.length,
|
|
3909
|
-
suppressedAdvisories: report.suppressedAdvisories?.length ?? 0
|
|
3910
|
-
},
|
|
3911
|
-
truncated: report.issues.length > MAX_FINDINGS2 || report.advisories.length > MAX_FINDINGS2 || (report.suppressedAdvisories?.length ?? 0) > MAX_FINDINGS2
|
|
3912
|
-
};
|
|
3913
|
-
} catch (error) {
|
|
3914
|
-
return { status: "unavailable", reason: reason(error) };
|
|
3915
|
-
}
|
|
3916
|
-
}
|
|
3917
|
-
async function reviewSummary(root, requestedBase, evidence) {
|
|
3918
|
-
const workingTree = await getWorkingTree(root);
|
|
3919
|
-
const scope = "committed";
|
|
3920
|
-
try {
|
|
3921
|
-
const base = requestedBase ?? await defaultBase(root);
|
|
3922
|
-
if (!base) return { status: "unavailable", scope, workingTree, reason: "No default review base resolves. Pass base to mason_init, or run mason-review --base <ref>." };
|
|
3923
|
-
const report = await computeReview(root, base, { evidence });
|
|
3924
|
-
if (!report) return { status: "unavailable", scope, base, workingTree, reason: "The review base, merge base, or committed diff could not be read." };
|
|
3925
|
-
return {
|
|
3926
|
-
...report,
|
|
3927
|
-
scope,
|
|
3928
|
-
workingTree,
|
|
3929
|
-
...report.evidence ? { evidence: summarizeEvidence(report.evidence) } : {},
|
|
3930
|
-
status: !report.historyAvailable ? "unavailable" : report.changedFiles.length ? "complete" : "no-changes",
|
|
3931
|
-
...!report.historyAvailable ? { reason: "Co-change history could not be read; review findings are incomplete." } : {},
|
|
3932
|
-
changedFiles: report.changedFiles.slice(0, MAX_FINDINGS2),
|
|
3933
|
-
missingPartners: report.missingPartners.slice(0, MAX_FINDINGS2),
|
|
3934
|
-
touchedDecisions: report.touchedDecisions.slice(0, MAX_FINDINGS2),
|
|
3935
|
-
counts: { changedFiles: report.changedFiles.length, missingPartners: report.missingPartners.length, touchedDecisions: report.touchedDecisions.length },
|
|
3936
|
-
truncated: report.truncated || [report.changedFiles, report.missingPartners, report.touchedDecisions].some((list) => list.length > MAX_FINDINGS2),
|
|
3937
|
-
hint: "Reviews cover merge-base..HEAD. Uncommitted paths are reported separately in workingTree; they have not been reviewed."
|
|
3938
|
-
};
|
|
3939
|
-
} catch (error) {
|
|
3940
|
-
return { status: "unavailable", scope, workingTree, reason: reason(error) };
|
|
3941
|
-
}
|
|
3942
|
-
}
|
|
3943
|
-
async function inspectOnboarding(root, base, evidence) {
|
|
3944
|
-
const [audit, review, map, decisions] = await Promise.all([
|
|
3945
|
-
auditSummary(root),
|
|
3946
|
-
reviewSummary(root, base, evidence),
|
|
3947
|
-
inspectSnapshot(root),
|
|
3948
|
-
loadDecisionStore(root)
|
|
3949
|
-
]);
|
|
3950
|
-
return {
|
|
3951
|
-
audit,
|
|
3952
|
-
review,
|
|
3953
|
-
map: { status: map.status },
|
|
3954
|
-
decisions: {
|
|
3955
|
-
active: decisions.records.filter((record) => record.status === "active").length,
|
|
3956
|
-
...Object.fromEntries(["accepted", "proposed", "unreviewed"].map((approval) => [approval, decisions.records.filter((record) => record.status === "active" && decisionApproval(effectiveDecision(record)) === approval).length])),
|
|
3957
|
-
pendingProposals: decisions.records.filter((record) => effectiveDecision(record) !== record).length
|
|
3958
|
-
},
|
|
3959
|
-
diagnostics: [...map.diagnostics, ...decisions.diagnostics]
|
|
3960
|
-
};
|
|
3961
|
-
}
|
|
3962
|
-
var MAX_FINDINGS2, reason;
|
|
3963
|
-
var init_onboarding = __esm({
|
|
3964
|
-
"src/mcp/onboarding.ts"() {
|
|
3965
|
-
"use strict";
|
|
3966
|
-
init_audit();
|
|
3967
|
-
init_review();
|
|
3968
|
-
init_drift();
|
|
3969
|
-
init_snapshot();
|
|
3970
|
-
init_provenance();
|
|
3971
|
-
init_evidence2();
|
|
3972
|
-
init_decisions();
|
|
3973
|
-
MAX_FINDINGS2 = 20;
|
|
3974
|
-
reason = (error) => error instanceof Error ? error.message : String(error);
|
|
3975
|
-
}
|
|
3976
|
-
});
|
|
3977
|
-
|
|
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);
|
|
4092
|
+
const finalize = async () => {
|
|
4093
|
+
if (report.evidence && await getCurrentGitHash(resolvedRoot) !== head) {
|
|
4094
|
+
report.evidence.diagnostics.push("HEAD changed during the review; rerun to obtain consistent change and knowledge associations.");
|
|
4095
|
+
for (const check of report.evidence.checks) check.freshness = "unknown";
|
|
4096
|
+
report.evidence.summary.stale = 0;
|
|
4097
|
+
report.evidence.summary.unknown = report.evidence.checks.length;
|
|
4098
|
+
if (report.evidence.status !== "unavailable") report.evidence.status = "incomplete";
|
|
4248
4099
|
}
|
|
4100
|
+
return report;
|
|
4101
|
+
};
|
|
4102
|
+
if (changedFiles.length === 0) return finalize();
|
|
4103
|
+
let analyzed = changedFiles;
|
|
4104
|
+
if (changedFiles.length > MAX_ANALYZED_FILES) {
|
|
4105
|
+
analyzed = changedFiles.slice(0, MAX_ANALYZED_FILES);
|
|
4106
|
+
report.truncated = true;
|
|
4249
4107
|
}
|
|
4250
|
-
|
|
4251
|
-
|
|
4108
|
+
const partners = await findMissingPartners(
|
|
4109
|
+
resolvedRoot,
|
|
4110
|
+
analyzed,
|
|
4111
|
+
async (relPath) => {
|
|
4112
|
+
try {
|
|
4113
|
+
await fs15.access(path20.join(resolvedRoot, relPath));
|
|
4114
|
+
return true;
|
|
4115
|
+
} catch {
|
|
4116
|
+
return false;
|
|
4117
|
+
}
|
|
4118
|
+
},
|
|
4119
|
+
changedFiles
|
|
4120
|
+
);
|
|
4121
|
+
if (partners === null) {
|
|
4122
|
+
report.historyAvailable = false;
|
|
4123
|
+
} else {
|
|
4124
|
+
report.missingPartners = partners;
|
|
4252
4125
|
}
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4126
|
+
const decisions = store.records;
|
|
4127
|
+
for (const record of decisions) {
|
|
4128
|
+
if (record.status !== "active") continue;
|
|
4129
|
+
const effective = effectiveDecision(record);
|
|
4130
|
+
const touched = anchorsTouched(effective, changedFiles);
|
|
4131
|
+
const proposalTouched = effective !== record ? anchorsTouched(record, changedFiles) : [];
|
|
4132
|
+
if (touched.length > 0 || proposalTouched.length > 0) {
|
|
4133
|
+
const { pendingProposal, ...knowledge } = decisionKnowledge(record, decisionDrift.freshness?.[record.id] ?? "unknown", decisionDrift.pendingProposals?.[record.id]?.freshness ?? "unknown");
|
|
4134
|
+
report.touchedDecisions.push({
|
|
4135
|
+
...knowledge,
|
|
4136
|
+
...pendingProposal ? { pendingProposal: { ...pendingProposal, touchedFiles: proposalTouched } } : {},
|
|
4137
|
+
id: record.id,
|
|
4138
|
+
anchors: effective.files,
|
|
4139
|
+
freshness: decisionDrift.freshness?.[record.id] ?? "unknown",
|
|
4140
|
+
touchedFiles: touched
|
|
4141
|
+
});
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
return finalize();
|
|
4264
4145
|
}
|
|
4265
|
-
var
|
|
4266
|
-
var
|
|
4267
|
-
"src/
|
|
4146
|
+
var exec10, MAX_ANALYZED_FILES;
|
|
4147
|
+
var init_review = __esm({
|
|
4148
|
+
"src/review/review.ts"() {
|
|
4268
4149
|
"use strict";
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
sha256 = (bytes) => createHash4("sha256").update(bytes).digest("hex");
|
|
4150
|
+
init_drift();
|
|
4151
|
+
init_provenance();
|
|
4152
|
+
init_decisions();
|
|
4153
|
+
init_paths();
|
|
4154
|
+
init_drift2();
|
|
4155
|
+
init_cochange();
|
|
4156
|
+
init_evidence2();
|
|
4157
|
+
init_snapshot();
|
|
4158
|
+
exec10 = promisify12(execFile12);
|
|
4159
|
+
MAX_ANALYZED_FILES = 50;
|
|
4280
4160
|
}
|
|
4281
4161
|
});
|
|
4282
4162
|
|
|
4283
|
-
// src/
|
|
4284
|
-
|
|
4285
|
-
__export(runtime_exports, {
|
|
4286
|
-
installRuntime: () => installRuntime,
|
|
4287
|
-
sourceRuntime: () => sourceRuntime,
|
|
4288
|
-
verifyRuntime: () => verifyRuntime
|
|
4289
|
-
});
|
|
4290
|
-
import fs17 from "fs/promises";
|
|
4291
|
-
import path22 from "path";
|
|
4292
|
-
import { fileURLToPath } from "url";
|
|
4293
|
-
import { randomUUID as randomUUID6, createHash as createHash5 } from "crypto";
|
|
4294
|
-
import { execFile as execFile11 } from "child_process";
|
|
4295
|
-
import { promisify as promisify11 } from "util";
|
|
4296
|
-
async function packageRoot() {
|
|
4297
|
-
let directory = path22.dirname(fileURLToPath(import.meta.url));
|
|
4298
|
-
for (let i = 0; i < 8; i++) {
|
|
4299
|
-
try {
|
|
4300
|
-
const pkg = JSON.parse(await fs17.readFile(path22.join(directory, "package.json"), "utf8"));
|
|
4301
|
-
if (pkg.name === "mason-context") return directory;
|
|
4302
|
-
} catch {
|
|
4303
|
-
}
|
|
4304
|
-
directory = path22.dirname(directory);
|
|
4305
|
-
}
|
|
4306
|
-
throw new Error("Cannot locate the executing Mason distribution. Reinstall Mason and rerun setup.");
|
|
4307
|
-
}
|
|
4308
|
-
async function sourceRuntime() {
|
|
4309
|
-
const source = await packageRoot();
|
|
4310
|
-
const pkg = JSON.parse(await fs17.readFile(path22.join(source, "package.json"), "utf8"));
|
|
4311
|
-
const hashes = Object.fromEntries(await Promise.all(BINARIES.map(async (file) => [file, checksum(await fs17.readFile(path22.join(source, file)))])));
|
|
4312
|
-
const bundleRoot = path22.dirname(source);
|
|
4313
|
-
const bundleExists = await fs17.lstat(path22.join(bundleRoot, "bundle.json")).then(() => true, (error) => {
|
|
4314
|
-
if (error.code === "ENOENT") return false;
|
|
4315
|
-
throw error;
|
|
4316
|
-
});
|
|
4317
|
-
if (bundleExists) {
|
|
4318
|
-
const { manifest, manifestHash } = await verifyBundle(bundleRoot);
|
|
4319
|
-
if (manifest.version !== pkg.version) throw new Error("Mason package and bundle versions differ.");
|
|
4320
|
-
return { source, bundleRoot, runtime: runtimeSchema.parse({
|
|
4321
|
-
id: manifestHash.slice(0, 24),
|
|
4322
|
-
version: pkg.version,
|
|
4323
|
-
hashes,
|
|
4324
|
-
bundle: { target: manifest.target, manifestHash }
|
|
4325
|
-
}) };
|
|
4326
|
-
}
|
|
4327
|
-
return { source, runtime: runtimeSchema.parse({ id: hash([pkg.version, pkg.dependencies, hashes]).slice(0, 24), version: pkg.version, hashes }) };
|
|
4328
|
-
}
|
|
4329
|
-
async function verifyRuntime(root, runtime) {
|
|
4163
|
+
// src/mcp/onboarding.ts
|
|
4164
|
+
async function auditSummary(root) {
|
|
4330
4165
|
try {
|
|
4331
|
-
const
|
|
4332
|
-
if (
|
|
4333
|
-
if (
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
} catch {
|
|
4348
|
-
return
|
|
4166
|
+
const report = await computeAudit(root);
|
|
4167
|
+
if (!report) return { status: "no-context-files", reason: "No AGENTS.md, CLAUDE.md, or .claude/CLAUDE.md found to audit." };
|
|
4168
|
+
if (!report.gitAvailable) return { status: "unavailable", reason: "Audit needs readable Git history to verify documentation claims.", docs: report.docs };
|
|
4169
|
+
return {
|
|
4170
|
+
...report,
|
|
4171
|
+
status: "complete",
|
|
4172
|
+
issues: report.issues.slice(0, MAX_FINDINGS2),
|
|
4173
|
+
advisories: report.advisories.slice(0, MAX_FINDINGS2),
|
|
4174
|
+
suppressedAdvisories: (report.suppressedAdvisories ?? []).slice(0, MAX_FINDINGS2),
|
|
4175
|
+
counts: {
|
|
4176
|
+
issues: report.issues.length,
|
|
4177
|
+
advisories: report.advisories.length,
|
|
4178
|
+
suppressedAdvisories: report.suppressedAdvisories?.length ?? 0
|
|
4179
|
+
},
|
|
4180
|
+
truncated: report.issues.length > MAX_FINDINGS2 || report.advisories.length > MAX_FINDINGS2 || (report.suppressedAdvisories?.length ?? 0) > MAX_FINDINGS2
|
|
4181
|
+
};
|
|
4182
|
+
} catch (error) {
|
|
4183
|
+
return { status: "unavailable", reason: reason(error) };
|
|
4349
4184
|
}
|
|
4350
4185
|
}
|
|
4351
|
-
async function
|
|
4352
|
-
const
|
|
4353
|
-
|
|
4354
|
-
const relative = `.mason/runtime/${runtime.id}`;
|
|
4355
|
-
const target = await storePath(root, relative, true);
|
|
4356
|
-
const stage = await storePath(root, ".mason/runtime/.install-" + randomUUID6(), true);
|
|
4357
|
-
await fs17.mkdir(stage);
|
|
4186
|
+
async function reviewSummary(root, requestedBase, evidence) {
|
|
4187
|
+
const workingTree = await getWorkingTree(root);
|
|
4188
|
+
const scope = "committed";
|
|
4358
4189
|
try {
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
}
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
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.");
|
|
4380
|
-
}
|
|
4381
|
-
}
|
|
4382
|
-
await fs17.writeFile(path22.join(stage, "receipt.json"), JSON.stringify(runtime, null, 2) + "\n");
|
|
4383
|
-
try {
|
|
4384
|
-
await fs17.rename(target, target + ".previous-" + randomUUID6());
|
|
4385
|
-
} catch (error) {
|
|
4386
|
-
if (error.code !== "ENOENT") throw error;
|
|
4387
|
-
}
|
|
4388
|
-
await fs17.rename(stage, target);
|
|
4389
|
-
if (!await verifyRuntime(root, runtime)) throw new Error("The installed Mason runtime could not be verified.");
|
|
4390
|
-
return runtime;
|
|
4391
|
-
} finally {
|
|
4392
|
-
await fs17.rm(stage, { recursive: true, force: true });
|
|
4190
|
+
const base = requestedBase ?? await defaultBase(root);
|
|
4191
|
+
if (!base) return { status: "unavailable", scope, workingTree, reason: "No default review base resolves. Pass base to mason_init, or run mason-review --base <ref>." };
|
|
4192
|
+
const report = await computeReview(root, base, { evidence });
|
|
4193
|
+
if (!report) return { status: "unavailable", scope, base, workingTree, reason: "The review base, merge base, or committed diff could not be read." };
|
|
4194
|
+
return {
|
|
4195
|
+
...report,
|
|
4196
|
+
scope,
|
|
4197
|
+
workingTree,
|
|
4198
|
+
...report.evidence ? { evidence: summarizeEvidence(report.evidence) } : {},
|
|
4199
|
+
status: !report.historyAvailable ? "unavailable" : report.changedFiles.length ? "complete" : "no-changes",
|
|
4200
|
+
...!report.historyAvailable ? { reason: "Co-change history could not be read; review findings are incomplete." } : {},
|
|
4201
|
+
changedFiles: report.changedFiles.slice(0, MAX_FINDINGS2),
|
|
4202
|
+
missingPartners: report.missingPartners.slice(0, MAX_FINDINGS2),
|
|
4203
|
+
touchedDecisions: report.touchedDecisions.slice(0, MAX_FINDINGS2),
|
|
4204
|
+
counts: { changedFiles: report.changedFiles.length, missingPartners: report.missingPartners.length, touchedDecisions: report.touchedDecisions.length },
|
|
4205
|
+
truncated: report.truncated || [report.changedFiles, report.missingPartners, report.touchedDecisions].some((list) => list.length > MAX_FINDINGS2),
|
|
4206
|
+
hint: "Reviews cover merge-base..HEAD. Uncommitted paths are reported separately in workingTree; they have not been reviewed."
|
|
4207
|
+
};
|
|
4208
|
+
} catch (error) {
|
|
4209
|
+
return { status: "unavailable", scope, workingTree, reason: reason(error) };
|
|
4393
4210
|
}
|
|
4394
4211
|
}
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4212
|
+
async function inspectOnboarding(root, base, evidence) {
|
|
4213
|
+
const [audit, review, map, decisions] = await Promise.all([
|
|
4214
|
+
auditSummary(root),
|
|
4215
|
+
reviewSummary(root, base, evidence),
|
|
4216
|
+
inspectSnapshot(root),
|
|
4217
|
+
loadDecisionStore(root)
|
|
4218
|
+
]);
|
|
4219
|
+
return {
|
|
4220
|
+
audit,
|
|
4221
|
+
review,
|
|
4222
|
+
map: { status: map.status },
|
|
4223
|
+
decisions: {
|
|
4224
|
+
active: decisions.records.filter((record) => record.status === "active").length,
|
|
4225
|
+
...Object.fromEntries(["accepted", "proposed", "unreviewed"].map((approval) => [approval, decisions.records.filter((record) => record.status === "active" && decisionApproval(effectiveDecision(record)) === approval).length])),
|
|
4226
|
+
pendingProposals: decisions.records.filter((record) => effectiveDecision(record) !== record).length
|
|
4227
|
+
},
|
|
4228
|
+
diagnostics: [...map.diagnostics, ...decisions.diagnostics]
|
|
4229
|
+
};
|
|
4230
|
+
}
|
|
4231
|
+
var MAX_FINDINGS2, reason;
|
|
4232
|
+
var init_onboarding = __esm({
|
|
4233
|
+
"src/mcp/onboarding.ts"() {
|
|
4398
4234
|
"use strict";
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4235
|
+
init_audit();
|
|
4236
|
+
init_review();
|
|
4237
|
+
init_drift();
|
|
4238
|
+
init_snapshot();
|
|
4239
|
+
init_provenance();
|
|
4240
|
+
init_evidence2();
|
|
4241
|
+
init_decisions();
|
|
4242
|
+
MAX_FINDINGS2 = 20;
|
|
4243
|
+
reason = (error) => error instanceof Error ? error.message : String(error);
|
|
4406
4244
|
}
|
|
4407
4245
|
});
|
|
4408
4246
|
|
|
@@ -4430,34 +4268,32 @@ async function setupStatus(dir) {
|
|
|
4430
4268
|
};
|
|
4431
4269
|
}
|
|
4432
4270
|
const hosts = {};
|
|
4433
|
-
const
|
|
4271
|
+
const installed = await installedCommand();
|
|
4434
4272
|
const automation = await automationStatus(ws.root);
|
|
4435
4273
|
for (const host of ["codex", "claude"]) {
|
|
4436
4274
|
const entry = setup.hosts[host];
|
|
4437
4275
|
if (!entry) continue;
|
|
4438
|
-
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");
|
|
4439
4276
|
const instructions = await instructionEdits(ws.root, host);
|
|
4440
4277
|
const instructionsCurrent = instructions.every((edit) => edit.before === edit.after);
|
|
4441
|
-
const
|
|
4442
|
-
const
|
|
4443
|
-
const
|
|
4444
|
-
const hooks = isDeepStrictEqual2(config.hooks, hookConfig(host, hookCommand(host, entry.runtime)).hooks) && !config.disabled;
|
|
4278
|
+
const config = await inspectHostConfig(ws.root, host, void 0);
|
|
4279
|
+
const mcp = !config.mcpDisabled && hash(config.mcp) === entry.mcpFingerprint && isDeepStrictEqual2({ command: config.mcp?.command, args: config.mcp?.args }, mcpCommand(host));
|
|
4280
|
+
const hooks = isDeepStrictEqual2(config.hooks, hookConfig(host, hookCommand(host)).hooks) && !config.disabled;
|
|
4445
4281
|
const local = await loadSetupReceipt(ws.root, ws.directory, host);
|
|
4446
4282
|
const configured = local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
|
|
4447
|
-
const observation = await readObservation(ws.root, ws.directory, host, entry.revision);
|
|
4283
|
+
const observation = await readObservation(ws.root, ws.directory, host, entry.revision, installed.version ?? "unavailable");
|
|
4448
4284
|
const sessions = Object.values(observation?.sessions ?? {}).sort((a, b) => b.at.localeCompare(a.at));
|
|
4449
4285
|
const complete = sessions.find((session) => events.every((event) => session.events.includes(event)));
|
|
4450
4286
|
const observedEvents = complete?.events ?? sessions[0]?.events ?? [];
|
|
4451
4287
|
const pending = [];
|
|
4452
|
-
if (!installed
|
|
4288
|
+
if (!installed.available) pending.push(installed.message);
|
|
4453
4289
|
if (!mcp || !hooks || !instructionsCurrent || !configured) pending.push("Rerun setup to reconcile project configuration; inspect any disabled host settings.");
|
|
4454
4290
|
if (!observation?.contextCalls) pending.push("Start a new assistant session and request task context through Mason's get_context tool.");
|
|
4455
4291
|
if (!complete) pending.push("Review/trust the host configuration, then complete a normal task to observe the full hook lifecycle.");
|
|
4456
4292
|
if (automation.status === "unavailable") pending.push("The latest automation attempt did not establish verification. Run mason-auto check and inspect its diagnostics.");
|
|
4457
|
-
const healthy = installed &&
|
|
4293
|
+
const healthy = installed.available && mcp && hooks && instructionsCurrent && configured && automation.status !== "unavailable";
|
|
4458
4294
|
hosts[host] = {
|
|
4459
4295
|
status: healthy ? complete && observation?.contextCalls ? "active" : "pending" : "attention",
|
|
4460
|
-
runtime: installed
|
|
4296
|
+
runtime: installed.available ? "global (" + installed.version + ")" : "unavailable-on-path",
|
|
4461
4297
|
mcp: mcp ? "configured" : "changed",
|
|
4462
4298
|
instructions: instructionsCurrent ? "current" : "changed",
|
|
4463
4299
|
hookConfiguration: hooks ? "configured" : "disabled-or-changed",
|
|
@@ -4503,10 +4339,8 @@ var init_status = __esm({
|
|
|
4503
4339
|
init_decisions();
|
|
4504
4340
|
init_adapters();
|
|
4505
4341
|
init_model();
|
|
4506
|
-
init_runtime2();
|
|
4507
4342
|
init_config();
|
|
4508
4343
|
init_launcher();
|
|
4509
|
-
init_files2();
|
|
4510
4344
|
init_observations();
|
|
4511
4345
|
}
|
|
4512
4346
|
});
|
|
@@ -4518,7 +4352,7 @@ __export(setup_exports, {
|
|
|
4518
4352
|
setupProject: () => setupProject,
|
|
4519
4353
|
summarizeSetup: () => summarizeSetup
|
|
4520
4354
|
});
|
|
4521
|
-
import { randomUUID as
|
|
4355
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
4522
4356
|
async function selectHost(root, explicit) {
|
|
4523
4357
|
if (explicit) return explicit;
|
|
4524
4358
|
if (process.env.MASON_SETUP_HOST === "codex" || process.env.MASON_SETUP_HOST === "claude") return process.env.MASON_SETUP_HOST;
|
|
@@ -4534,28 +4368,27 @@ async function setupProject(dir, options = {}) {
|
|
|
4534
4368
|
return withLock(ws.root, ".mason/reports/setup-lock", async () => {
|
|
4535
4369
|
const existing = await loadSetup(ws.root);
|
|
4536
4370
|
const previousReceipt = await loadSetupReceipt(ws.root, ws.directory, host);
|
|
4537
|
-
const
|
|
4538
|
-
|
|
4371
|
+
const installed = await installedCommand();
|
|
4372
|
+
if (!installed.available) throw new Error(installed.message);
|
|
4539
4373
|
const instructions = await instructionEdits(ws.root, host);
|
|
4540
|
-
const mcp = await mcpEdit(ws.root, host
|
|
4541
|
-
const hooks = await hookEdits(ws.root, host
|
|
4374
|
+
const mcp = await mcpEdit(ws.root, host);
|
|
4375
|
+
const hooks = await hookEdits(ws.root, host);
|
|
4542
4376
|
const ancillary = await ancillaryEdits(ws.root);
|
|
4543
4377
|
const edits = [...ancillary, ...instructions, mcp, ...hooks];
|
|
4544
|
-
const setupBefore = await readText(ws.root,
|
|
4545
|
-
const mcpFingerprint = hash((await inspectHostConfig(ws.root, host, mcp.after
|
|
4546
|
-
const desiredHooks = hookConfig(host, hookCommand(host
|
|
4378
|
+
const setupBefore = await readText(ws.root, SETUP_PATH);
|
|
4379
|
+
const mcpFingerprint = hash((await inspectHostConfig(ws.root, host, mcp.after)).mcp);
|
|
4380
|
+
const desiredHooks = hookConfig(host, hookCommand(host)).hooks;
|
|
4547
4381
|
const fingerprint = hash({
|
|
4548
|
-
|
|
4549
|
-
launcher: LAUNCHER,
|
|
4382
|
+
command: "mason",
|
|
4550
4383
|
mcp: mcpFingerprint,
|
|
4551
4384
|
hooks: desiredHooks,
|
|
4552
4385
|
instructions: instructions.map((edit) => edit.path)
|
|
4553
4386
|
});
|
|
4554
4387
|
const previous = existing?.hosts[host];
|
|
4555
4388
|
const changed = edits.some((edit) => edit.before !== edit.after) || previous?.fingerprint !== fingerprint;
|
|
4556
|
-
const revision = !changed && previous ? previous.revision :
|
|
4557
|
-
const setup = existing ?? { version:
|
|
4558
|
-
setup.hosts[host] = {
|
|
4389
|
+
const revision = !changed && previous ? previous.revision : randomUUID6();
|
|
4390
|
+
const setup = existing ?? { version: 2, hosts: {} };
|
|
4391
|
+
setup.hosts[host] = { configuredVersion: installed.version, revision, fingerprint, mcpFingerprint, instructions: instructions.map((e) => e.path) };
|
|
4559
4392
|
const initial = await automate(ws.root, { event: "turn_start" });
|
|
4560
4393
|
const findings = await inspectOnboarding(ws.root, options.base, options.evidence);
|
|
4561
4394
|
const receiptPath = ws.directory + "/setup-" + host + ".json";
|
|
@@ -4570,20 +4403,11 @@ async function setupProject(dir, options = {}) {
|
|
|
4570
4403
|
root: ws.root,
|
|
4571
4404
|
revision
|
|
4572
4405
|
});
|
|
4573
|
-
await installRuntime(ws.root, selected);
|
|
4574
|
-
if (selected.runtime.bundle) {
|
|
4575
|
-
const pointer = `.mason/runtime/${host}.txt`;
|
|
4576
|
-
await applyEdit(ws.root, { path: pointer, before: await readText(ws.root, pointer), after: selected.runtime.id + "\n" });
|
|
4577
|
-
}
|
|
4578
4406
|
const changedFiles = [];
|
|
4579
4407
|
for (const edit of edits) if (await applyEdit(ws.root, edit)) changedFiles.push(edit.path);
|
|
4580
|
-
if (await applyEdit(ws.root, { path:
|
|
4581
|
-
if (!marker) {
|
|
4582
|
-
await saveProjectMarker(ws.root, { version: 1, initializedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
4583
|
-
changedFiles.push(".mason/project.json");
|
|
4584
|
-
}
|
|
4408
|
+
if (await applyEdit(ws.root, { path: SETUP_PATH, before: setupBefore, after: JSON.stringify(setup, null, 2) + "\n" })) changedFiles.push(SETUP_PATH);
|
|
4585
4409
|
const checked = await automate(ws.root, { event: "turn_start" });
|
|
4586
|
-
const configured = await inspectHostConfig(ws.root, host, void 0
|
|
4410
|
+
const configured = await inspectHostConfig(ws.root, host, void 0);
|
|
4587
4411
|
await writeStoreJson(ws.root, receiptPath, {
|
|
4588
4412
|
version: 1,
|
|
4589
4413
|
host,
|
|
@@ -4600,7 +4424,7 @@ async function setupProject(dir, options = {}) {
|
|
|
4600
4424
|
status: "configured",
|
|
4601
4425
|
host,
|
|
4602
4426
|
root: ws.root,
|
|
4603
|
-
runtime:
|
|
4427
|
+
runtime: { kind: "global", command: "mason", version: installed.version },
|
|
4604
4428
|
changedFiles,
|
|
4605
4429
|
initialReportPath,
|
|
4606
4430
|
findings,
|
|
@@ -4614,14 +4438,15 @@ function summarizeSetup(result) {
|
|
|
4614
4438
|
const audit = result.findings.audit;
|
|
4615
4439
|
return [
|
|
4616
4440
|
`Mason configured for ${result.host}.`,
|
|
4617
|
-
"
|
|
4441
|
+
" MCP server and lifecycle hooks use mason from PATH.",
|
|
4618
4442
|
" Project instructions updated; original audit evidence retained.",
|
|
4619
|
-
` ${result.changedFiles.length}
|
|
4443
|
+
` ${result.changedFiles.length} files changed.`,
|
|
4620
4444
|
`Initial audit: ${audit.status}${"counts" in audit ? `; ${audit.counts.issues} issues, ${audit.counts.advisories + audit.counts.suppressedAdvisories} advisories` : ""}.`,
|
|
4621
4445
|
..."issues" in audit ? audit.issues.slice(0, 3).map((f) => " " + f.message) : [],
|
|
4622
4446
|
`Original evidence: ${result.initialReportPath}`,
|
|
4623
4447
|
"Activation: " + result.activation.status + ".",
|
|
4624
4448
|
result.next,
|
|
4449
|
+
"If the assistant cannot find mason, restart the desktop app so it picks up PATH.",
|
|
4625
4450
|
"After the task finishes, run mason-auto status. Configuration alone does not establish activation."
|
|
4626
4451
|
].join("\n");
|
|
4627
4452
|
}
|
|
@@ -4631,7 +4456,6 @@ var init_setup = __esm({
|
|
|
4631
4456
|
init_evidence();
|
|
4632
4457
|
init_store();
|
|
4633
4458
|
init_runtime();
|
|
4634
|
-
init_init();
|
|
4635
4459
|
init_onboarding();
|
|
4636
4460
|
init_storage();
|
|
4637
4461
|
init_files2();
|
|
@@ -4639,24 +4463,70 @@ var init_setup = __esm({
|
|
|
4639
4463
|
init_launcher();
|
|
4640
4464
|
init_adapters();
|
|
4641
4465
|
init_model();
|
|
4642
|
-
init_runtime2();
|
|
4643
4466
|
init_status();
|
|
4644
4467
|
}
|
|
4645
4468
|
});
|
|
4646
4469
|
|
|
4470
|
+
// src/distribution/bundle.ts
|
|
4471
|
+
import fs16 from "fs/promises";
|
|
4472
|
+
import path21 from "path";
|
|
4473
|
+
import { createHash as createHash4 } from "crypto";
|
|
4474
|
+
import { z as z16 } from "zod";
|
|
4475
|
+
async function readBundle(root) {
|
|
4476
|
+
const bytes = await fs16.readFile(await storePath(root, "bundle.json"));
|
|
4477
|
+
const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
|
|
4478
|
+
const windows = manifest.target.startsWith("win32-");
|
|
4479
|
+
for (const file of Object.keys(manifest.files)) {
|
|
4480
|
+
if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") || path21.isAbsolute(file)) {
|
|
4481
|
+
throw new Error("Unsafe path in Mason bundle: " + file);
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
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"]) {
|
|
4485
|
+
if (!manifest.files[required]) throw new Error("Incomplete Mason bundle: " + required);
|
|
4486
|
+
}
|
|
4487
|
+
return { manifest, manifestHash: sha256(bytes) };
|
|
4488
|
+
}
|
|
4489
|
+
async function verifyBundle(root, expectedHash) {
|
|
4490
|
+
const bundle = await readBundle(root);
|
|
4491
|
+
if (expectedHash && bundle.manifestHash !== expectedHash) throw new Error("Mason bundle manifest changed.");
|
|
4492
|
+
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.");
|
|
4493
|
+
const files = Object.keys(bundle.manifest.files);
|
|
4494
|
+
for (let i = 0; i < files.length; i += 8) await Promise.all(files.slice(i, i + 8).map(async (file) => {
|
|
4495
|
+
if (sha256(await fs16.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
|
|
4496
|
+
}));
|
|
4497
|
+
return bundle;
|
|
4498
|
+
}
|
|
4499
|
+
var targets, digest2, bundleSchema, sha256;
|
|
4500
|
+
var init_bundle = __esm({
|
|
4501
|
+
"src/distribution/bundle.ts"() {
|
|
4502
|
+
"use strict";
|
|
4503
|
+
init_storage();
|
|
4504
|
+
targets = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
|
|
4505
|
+
digest2 = z16.string().regex(/^[a-f0-9]{64}$/);
|
|
4506
|
+
bundleSchema = z16.object({
|
|
4507
|
+
format: z16.literal(1),
|
|
4508
|
+
version: z16.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
|
|
4509
|
+
target: z16.enum(targets),
|
|
4510
|
+
nodeVersion: z16.string(),
|
|
4511
|
+
files: z16.record(digest2)
|
|
4512
|
+
}).strict();
|
|
4513
|
+
sha256 = (bytes) => createHash4("sha256").update(bytes).digest("hex");
|
|
4514
|
+
}
|
|
4515
|
+
});
|
|
4516
|
+
|
|
4647
4517
|
// src/distribution/path.ts
|
|
4648
|
-
import
|
|
4518
|
+
import fs17 from "fs/promises";
|
|
4649
4519
|
import os3 from "os";
|
|
4650
|
-
import
|
|
4651
|
-
import { execFile as
|
|
4652
|
-
import { promisify as
|
|
4653
|
-
import { createHash as
|
|
4520
|
+
import path22 from "path";
|
|
4521
|
+
import { execFile as execFile13 } from "child_process";
|
|
4522
|
+
import { promisify as promisify13 } from "util";
|
|
4523
|
+
import { createHash as createHash5, randomUUID as randomUUID7 } from "crypto";
|
|
4654
4524
|
import { z as z17 } from "zod";
|
|
4655
4525
|
async function configurePath(bin, previous, save, options = {}) {
|
|
4656
4526
|
const env = options.env ?? process.env, platform = options.platform ?? process.platform;
|
|
4657
4527
|
const changes = [...previous];
|
|
4658
4528
|
const currentPath = platform === "win32" ? Object.entries(env).find(([key]) => key.toLowerCase() === "path")?.[1] : env.PATH;
|
|
4659
|
-
const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) =>
|
|
4529
|
+
const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) => path22.posix.isAbsolute(entry) && path22.posix.resolve(entry) === path22.posix.resolve(bin));
|
|
4660
4530
|
const message2 = alreadyOnPath ? "Run: mason setup --host codex" : "Open a new terminal, then run: mason setup --host codex";
|
|
4661
4531
|
const manual = (reason2) => ({ status: "manual", changes, message: `${reason2}
|
|
4662
4532
|
${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --host codex`}` });
|
|
@@ -4674,33 +4544,33 @@ ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --h
|
|
|
4674
4544
|
}
|
|
4675
4545
|
return { status: "configured", changes, message: message2 };
|
|
4676
4546
|
}
|
|
4677
|
-
const shell =
|
|
4547
|
+
const shell = path22.basename(env.SHELL ?? os3.userInfo().shell ?? "");
|
|
4678
4548
|
const userHome = options.userHome ?? os3.homedir();
|
|
4679
4549
|
let profiles;
|
|
4680
4550
|
if (!["bash", "zsh", "sh"].includes(shell)) return manual(`Automatic PATH setup is unavailable for ${shell || "an unknown shell"}.`);
|
|
4681
|
-
if (env.MASON_PROFILE) profiles = [
|
|
4682
|
-
else if (shell === "zsh") profiles = [
|
|
4683
|
-
else if (shell === "sh") profiles = [
|
|
4551
|
+
if (env.MASON_PROFILE) profiles = [path22.resolve(env.MASON_PROFILE)];
|
|
4552
|
+
else if (shell === "zsh") profiles = [path22.join(env.ZDOTDIR || userHome, ".zshrc")];
|
|
4553
|
+
else if (shell === "sh") profiles = [path22.join(userHome, ".profile")];
|
|
4684
4554
|
else {
|
|
4685
|
-
const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) =>
|
|
4555
|
+
const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path22.join(userHome, name));
|
|
4686
4556
|
let login = candidates[2];
|
|
4687
|
-
for (const file of candidates) if (await
|
|
4557
|
+
for (const file of candidates) if (await fs17.lstat(file).then(() => true, (error) => {
|
|
4688
4558
|
if (error.code === "ENOENT") return false;
|
|
4689
4559
|
throw error;
|
|
4690
4560
|
})) {
|
|
4691
4561
|
login = file;
|
|
4692
4562
|
break;
|
|
4693
4563
|
}
|
|
4694
|
-
profiles = [
|
|
4564
|
+
profiles = [path22.join(userHome, ".bashrc"), login];
|
|
4695
4565
|
}
|
|
4696
|
-
const marker = `# >>> mason PATH ${
|
|
4566
|
+
const marker = `# >>> mason PATH ${createHash5("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
|
|
4697
4567
|
for (const profile of profiles) {
|
|
4698
|
-
const stat = await
|
|
4568
|
+
const stat = await fs17.lstat(profile).catch((error) => {
|
|
4699
4569
|
if (error.code === "ENOENT") return null;
|
|
4700
4570
|
throw error;
|
|
4701
4571
|
});
|
|
4702
|
-
const file = stat?.isSymbolicLink() ? await
|
|
4703
|
-
const original = await
|
|
4572
|
+
const file = stat?.isSymbolicLink() ? await fs17.realpath(profile) : path22.resolve(profile);
|
|
4573
|
+
const original = await fs17.readFile(file).catch((error) => {
|
|
4704
4574
|
if (error.code === "ENOENT" && !stat) return null;
|
|
4705
4575
|
throw error;
|
|
4706
4576
|
});
|
|
@@ -4715,8 +4585,8 @@ ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --h
|
|
|
4715
4585
|
changes.push({ kind: "profile", file, block, created: original === null });
|
|
4716
4586
|
await save(changes);
|
|
4717
4587
|
}
|
|
4718
|
-
await
|
|
4719
|
-
const handle = await
|
|
4588
|
+
await fs17.mkdir(path22.dirname(file), { recursive: true });
|
|
4589
|
+
const handle = await fs17.open(file, original === null ? "ax+" : "a+", 384);
|
|
4720
4590
|
try {
|
|
4721
4591
|
if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0))) throw new Error(`Shell profile changed during installation: ${profile}`);
|
|
4722
4592
|
await handle.writeFile(block);
|
|
@@ -4740,23 +4610,23 @@ async function removePath(changes) {
|
|
|
4740
4610
|
await windowsPath("remove", change.entry, current, change.created);
|
|
4741
4611
|
continue;
|
|
4742
4612
|
}
|
|
4743
|
-
const stat = await
|
|
4613
|
+
const stat = await fs17.lstat(change.file).catch((error) => {
|
|
4744
4614
|
if (error.code === "ENOENT") return null;
|
|
4745
4615
|
throw error;
|
|
4746
4616
|
});
|
|
4747
4617
|
if (!stat) continue;
|
|
4748
4618
|
if (!stat.isFile()) throw new Error("profile type changed");
|
|
4749
|
-
const bytes = await
|
|
4619
|
+
const bytes = await fs17.readFile(change.file), text2 = bytes.toString("utf8");
|
|
4750
4620
|
if (!Buffer.from(text2).equals(bytes)) throw new Error("profile encoding changed");
|
|
4751
4621
|
const index2 = text2.indexOf(change.block);
|
|
4752
4622
|
if (index2 < 0 || text2.indexOf(change.block, index2 + change.block.length) >= 0) throw new Error("PATH block was edited or removed");
|
|
4753
4623
|
const prefix = text2.slice(0, index2), suffix = text2.slice(index2 + change.block.length);
|
|
4754
4624
|
const separator = prefix && suffix && !prefix.endsWith("\n") ? change.block.startsWith("\r\n") ? "\r\n" : "\n" : "";
|
|
4755
4625
|
const next = Buffer.from(prefix + separator + suffix);
|
|
4756
|
-
const temporary = change.file + ".mason-" +
|
|
4626
|
+
const temporary = change.file + ".mason-" + randomUUID7();
|
|
4757
4627
|
try {
|
|
4758
4628
|
if (stat.nlink > 1) throw new Error("profile has multiple hard links");
|
|
4759
|
-
const handle = await
|
|
4629
|
+
const handle = await fs17.open(temporary, "wx", stat.mode & 511);
|
|
4760
4630
|
try {
|
|
4761
4631
|
await handle.writeFile(next);
|
|
4762
4632
|
await handle.chmod(stat.mode & 511);
|
|
@@ -4765,11 +4635,11 @@ async function removePath(changes) {
|
|
|
4765
4635
|
} finally {
|
|
4766
4636
|
await handle.close();
|
|
4767
4637
|
}
|
|
4768
|
-
const current = await
|
|
4769
|
-
if (!current.isFile() || current.ino !== stat.ino || !(await
|
|
4770
|
-
await
|
|
4638
|
+
const current = await fs17.lstat(change.file);
|
|
4639
|
+
if (!current.isFile() || current.ino !== stat.ino || !(await fs17.readFile(change.file)).equals(bytes)) throw new Error("profile changed during uninstall");
|
|
4640
|
+
await fs17.rename(temporary, change.file);
|
|
4771
4641
|
} finally {
|
|
4772
|
-
await
|
|
4642
|
+
await fs17.rm(temporary, { force: true });
|
|
4773
4643
|
}
|
|
4774
4644
|
} catch (error) {
|
|
4775
4645
|
warnings.push(`Retained PATH setting ${change.kind === "profile" ? change.file : change.entry}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -4810,13 +4680,13 @@ var execute2, pathChangeSchema, quote, normalizeWindowsEntry, hasWindowsEntry;
|
|
|
4810
4680
|
var init_path = __esm({
|
|
4811
4681
|
"src/distribution/path.ts"() {
|
|
4812
4682
|
"use strict";
|
|
4813
|
-
execute2 =
|
|
4683
|
+
execute2 = promisify13(execFile13);
|
|
4814
4684
|
pathChangeSchema = z17.discriminatedUnion("kind", [
|
|
4815
4685
|
z17.object({ kind: z17.literal("profile"), file: z17.string(), block: z17.string(), created: z17.boolean() }),
|
|
4816
4686
|
z17.object({ kind: z17.literal("windows"), entry: z17.string(), created: z17.boolean() })
|
|
4817
4687
|
]);
|
|
4818
4688
|
quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
|
4819
|
-
normalizeWindowsEntry = (value) =>
|
|
4689
|
+
normalizeWindowsEntry = (value) => path22.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
|
|
4820
4690
|
hasWindowsEntry = (value, entry, env = process.env) => {
|
|
4821
4691
|
const variables = new Map(Object.entries(env).map(([key, value2]) => [key.toLowerCase(), value2]));
|
|
4822
4692
|
const expand = (part) => part.replace(/%([^%]+)%/g, (match, key) => variables.get(key.toLowerCase()) ?? match);
|
|
@@ -4834,41 +4704,41 @@ __export(install_exports2, {
|
|
|
4834
4704
|
uninstallStandalone: () => uninstallStandalone,
|
|
4835
4705
|
upgradeStandalone: () => upgradeStandalone
|
|
4836
4706
|
});
|
|
4837
|
-
import
|
|
4707
|
+
import fs18 from "fs/promises";
|
|
4838
4708
|
import os4 from "os";
|
|
4839
|
-
import
|
|
4840
|
-
import { fileURLToPath
|
|
4709
|
+
import path23 from "path";
|
|
4710
|
+
import { fileURLToPath } from "url";
|
|
4841
4711
|
import { spawn } from "child_process";
|
|
4842
|
-
import { randomUUID as
|
|
4712
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
4843
4713
|
import { z as z18 } from "zod";
|
|
4844
4714
|
async function replace(file, content2, mode = 384) {
|
|
4845
|
-
const temp = file + ".tmp-" +
|
|
4715
|
+
const temp = file + ".tmp-" + randomUUID8();
|
|
4846
4716
|
try {
|
|
4847
|
-
await
|
|
4848
|
-
await
|
|
4717
|
+
await fs18.writeFile(temp, content2, { mode, flag: "wx" });
|
|
4718
|
+
await fs18.rename(temp, file);
|
|
4849
4719
|
} finally {
|
|
4850
|
-
await
|
|
4720
|
+
await fs18.rm(temp, { force: true });
|
|
4851
4721
|
}
|
|
4852
4722
|
}
|
|
4853
4723
|
async function installStandalone(source) {
|
|
4854
4724
|
const bundle = await verifyBundle(source);
|
|
4855
|
-
const selectedHome =
|
|
4856
|
-
await
|
|
4857
|
-
const home = await
|
|
4858
|
-
const selectedBin =
|
|
4725
|
+
const selectedHome = path23.resolve(defaultHome());
|
|
4726
|
+
await fs18.mkdir(selectedHome, { recursive: true });
|
|
4727
|
+
const home = await fs18.realpath(selectedHome);
|
|
4728
|
+
const selectedBin = path23.resolve(process.env.MASON_BIN_DIR ?? (process.platform === "win32" ? path23.join(home, "bin") : path23.join(os4.homedir(), ".local/bin")));
|
|
4859
4729
|
return withLock(home, ".install-lock", async () => {
|
|
4860
4730
|
const raw = await readStoreJson(home, "install.json");
|
|
4861
4731
|
const previous = raw === null ? null : recordSchema2.parse(raw);
|
|
4862
|
-
if (!previous) for (const file of await
|
|
4732
|
+
if (!previous) for (const file of await fs18.readdir(home)) {
|
|
4863
4733
|
if (file === ".install-lock") continue;
|
|
4864
|
-
const directory =
|
|
4865
|
-
const stat = await
|
|
4866
|
-
if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await
|
|
4734
|
+
const directory = path23.join(home, file);
|
|
4735
|
+
const stat = await fs18.lstat(directory);
|
|
4736
|
+
if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs18.readdir(directory)).length) {
|
|
4867
4737
|
throw new Error("Mason installation directory is not empty and has no ownership receipt.");
|
|
4868
4738
|
}
|
|
4869
4739
|
}
|
|
4870
|
-
await
|
|
4871
|
-
const bin = await
|
|
4740
|
+
await fs18.mkdir(selectedBin, { recursive: true });
|
|
4741
|
+
const bin = await fs18.realpath(selectedBin);
|
|
4872
4742
|
if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
|
|
4873
4743
|
const id = bundle.manifestHash.slice(0, 24);
|
|
4874
4744
|
const destination = await storePath(home, "versions/" + id);
|
|
@@ -4879,10 +4749,10 @@ $previousToken = $env:MASON_UNINSTALL_TOKEN
|
|
|
4879
4749
|
$token = [Guid]::NewGuid().ToString()
|
|
4880
4750
|
$env:MASON_UNINSTALL_TOKEN = $token
|
|
4881
4751
|
try {
|
|
4882
|
-
& ${psQuote(
|
|
4752
|
+
& ${psQuote(path23.join(destination, "node.exe"))} ${psQuote(path23.join(destination, "app/dist/mason.js"))} @args
|
|
4883
4753
|
$code = $LASTEXITCODE
|
|
4884
4754
|
if ($code -eq 0 -and $args.Count -eq 1 -and $args[0] -eq 'uninstall') {
|
|
4885
|
-
if ([IO.File]::ReadAllText(${psQuote(
|
|
4755
|
+
if ([IO.File]::ReadAllText(${psQuote(path23.join(home, ".uninstall-request.txt"))}) -cne $token) { throw 'Mason did not authorize installation cleanup.' }
|
|
4886
4756
|
Remove-Item -LiteralPath ${psQuote(home)} -Recurse -Force
|
|
4887
4757
|
Write-Output ${psQuote("Removed standalone installation: " + home)}
|
|
4888
4758
|
}
|
|
@@ -4891,11 +4761,11 @@ try {
|
|
|
4891
4761
|
finally { $env:MASON_UNINSTALL_TOKEN = $previousToken }
|
|
4892
4762
|
`
|
|
4893
4763
|
} : { mason: `#!/bin/sh
|
|
4894
|
-
exec ${shQuote(
|
|
4764
|
+
exec ${shQuote(path23.join(destination, "node"))} ${shQuote(path23.join(destination, "app/dist/mason.js"))} "$@"
|
|
4895
4765
|
` };
|
|
4896
4766
|
for (const [name, content2] of Object.entries(launchers)) {
|
|
4897
4767
|
const file = await storePath(bin, name);
|
|
4898
|
-
const old = await
|
|
4768
|
+
const old = await fs18.readFile(file, "utf8").catch((error) => {
|
|
4899
4769
|
if (error.code === "ENOENT") return null;
|
|
4900
4770
|
throw error;
|
|
4901
4771
|
});
|
|
@@ -4904,7 +4774,7 @@ exec ${shQuote(path24.join(destination, "node"))} ${shQuote(path24.join(destinat
|
|
|
4904
4774
|
const retired = Object.entries({ ...previous?.previousLaunchers, ...previous?.launchers }).filter(([name]) => !(name in launchers));
|
|
4905
4775
|
const checkRetired = async (name, expected) => {
|
|
4906
4776
|
if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
|
|
4907
|
-
const actual = await
|
|
4777
|
+
const actual = await fs18.readFile(await storePath(bin, name), "utf8").catch((error) => {
|
|
4908
4778
|
if (error.code === "ENOENT") return null;
|
|
4909
4779
|
throw error;
|
|
4910
4780
|
});
|
|
@@ -4913,29 +4783,29 @@ exec ${shQuote(path24.join(destination, "node"))} ${shQuote(path24.join(destinat
|
|
|
4913
4783
|
for (const [name, expected] of retired) await checkRetired(name, expected);
|
|
4914
4784
|
const receipt = { format: 1, home, bin, current: id, version: bundle.manifest.version, launchers, pathChanges: previous?.pathChanges ?? [] };
|
|
4915
4785
|
await writeStoreJson(home, "install.json", { ...receipt, previousLaunchers: previous?.previousLaunchers ?? previous?.launchers });
|
|
4916
|
-
const stage = await storePath(home, "versions/.install-" +
|
|
4917
|
-
await
|
|
4786
|
+
const stage = await storePath(home, "versions/.install-" + randomUUID8(), true);
|
|
4787
|
+
await fs18.mkdir(stage);
|
|
4918
4788
|
try {
|
|
4919
4789
|
for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
|
|
4920
4790
|
const from = await storePath(source, file), to = await storePath(stage, file, true);
|
|
4921
|
-
await
|
|
4922
|
-
await
|
|
4791
|
+
await fs18.copyFile(from, to);
|
|
4792
|
+
await fs18.chmod(to, (await fs18.stat(from)).mode & 511);
|
|
4923
4793
|
}
|
|
4924
4794
|
await verifyBundle(stage, bundle.manifestHash);
|
|
4925
|
-
const exists2 = await
|
|
4795
|
+
const exists2 = await fs18.lstat(destination).then(() => true, (error) => {
|
|
4926
4796
|
if (error.code === "ENOENT") return false;
|
|
4927
4797
|
throw error;
|
|
4928
4798
|
});
|
|
4929
4799
|
if (exists2) await verifyBundle(destination, bundle.manifestHash);
|
|
4930
|
-
else await
|
|
4800
|
+
else await fs18.rename(stage, destination);
|
|
4931
4801
|
for (const [name, content2] of Object.entries(launchers)) await replace(await storePath(bin, name, true), content2, 493);
|
|
4932
4802
|
for (const [name, expected] of retired) {
|
|
4933
4803
|
await checkRetired(name, expected);
|
|
4934
|
-
await
|
|
4804
|
+
await fs18.rm(await storePath(bin, name), { force: true });
|
|
4935
4805
|
}
|
|
4936
4806
|
await writeStoreJson(home, "install.json", receipt);
|
|
4937
4807
|
} finally {
|
|
4938
|
-
await
|
|
4808
|
+
await fs18.rm(stage, { recursive: true, force: true });
|
|
4939
4809
|
}
|
|
4940
4810
|
const pathResult = await configurePath(bin, receipt.pathChanges, async (changes) => {
|
|
4941
4811
|
receipt.pathChanges = changes;
|
|
@@ -4945,12 +4815,12 @@ exec ${shQuote(path24.join(destination, "node"))} ${shQuote(path24.join(destinat
|
|
|
4945
4815
|
});
|
|
4946
4816
|
}
|
|
4947
4817
|
async function currentInstallation() {
|
|
4948
|
-
const app =
|
|
4949
|
-
const bundle =
|
|
4818
|
+
const app = path23.dirname(path23.dirname(fileURLToPath(import.meta.url)));
|
|
4819
|
+
const bundle = path23.dirname(app), home = path23.dirname(path23.dirname(bundle));
|
|
4950
4820
|
const raw = await readStoreJson(home, "install.json");
|
|
4951
4821
|
if (!raw) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
|
|
4952
4822
|
const record = recordSchema2.parse(raw);
|
|
4953
|
-
if (record.home !== await
|
|
4823
|
+
if (record.home !== await fs18.realpath(home) || path23.dirname(bundle) !== path23.join(home, "versions")) throw new Error("Invalid standalone installation receipt.");
|
|
4954
4824
|
return { home, bundle, record };
|
|
4955
4825
|
}
|
|
4956
4826
|
async function upgradeStandalone(version) {
|
|
@@ -4958,7 +4828,7 @@ async function upgradeStandalone(version) {
|
|
|
4958
4828
|
const { home, bundle, record } = await currentInstallation();
|
|
4959
4829
|
await verifyBundle(bundle);
|
|
4960
4830
|
const windows = process.platform === "win32";
|
|
4961
|
-
const script =
|
|
4831
|
+
const script = path23.join(bundle, windows ? "install.ps1" : "install.sh");
|
|
4962
4832
|
return new Promise((resolve, reject) => {
|
|
4963
4833
|
const child = spawn(
|
|
4964
4834
|
windows ? "powershell.exe" : "sh",
|
|
@@ -4979,7 +4849,7 @@ async function uninstallStandalone() {
|
|
|
4979
4849
|
for (const [name, expected] of Object.entries(record.launchers)) {
|
|
4980
4850
|
if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
|
|
4981
4851
|
const file = await storePath(record.bin, name);
|
|
4982
|
-
const actual = await
|
|
4852
|
+
const actual = await fs18.readFile(file, "utf8").catch((error) => {
|
|
4983
4853
|
if (error.code === "ENOENT") return null;
|
|
4984
4854
|
throw error;
|
|
4985
4855
|
});
|
|
@@ -4987,9 +4857,9 @@ async function uninstallStandalone() {
|
|
|
4987
4857
|
}
|
|
4988
4858
|
for (const warning of await removePath(record.pathChanges ?? [])) console.error(warning);
|
|
4989
4859
|
if (process.platform === "win32") await replace(await storePath(home, ".uninstall-request.txt", true), token);
|
|
4990
|
-
for (const name of Object.keys(record.launchers)) await
|
|
4860
|
+
for (const name of Object.keys(record.launchers)) await fs18.rm(await storePath(record.bin, name), { force: true });
|
|
4991
4861
|
});
|
|
4992
|
-
if (process.platform !== "win32") await
|
|
4862
|
+
if (process.platform !== "win32") await fs18.rm(home, { recursive: true, force: true });
|
|
4993
4863
|
return home;
|
|
4994
4864
|
}
|
|
4995
4865
|
var recordSchema2, shQuote, psQuote, WINDOWS_BATCH_LAUNCHER, launcherNames, defaultHome;
|
|
@@ -5014,12 +4884,12 @@ var init_install2 = __esm({
|
|
|
5014
4884
|
psQuote = (s) => "'" + s.replace(/'/g, "''") + "'";
|
|
5015
4885
|
WINDOWS_BATCH_LAUNCHER = '@echo off\r\ngoto #_mason_handoff_# 2>NUL || title %COMSPEC% & powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0mason-launcher.ps1" %*\r\n';
|
|
5016
4886
|
launcherNames = ["mason", "mason.cmd", "mason.ps1", "mason-launcher.ps1"];
|
|
5017
|
-
defaultHome = () => process.env.MASON_HOME ?? (process.platform === "win32" ?
|
|
4887
|
+
defaultHome = () => process.env.MASON_HOME ?? (process.platform === "win32" ? path23.join(process.env.LOCALAPPDATA ?? os4.homedir(), "Mason") : path23.join(os4.homedir(), ".local/share/mason"));
|
|
5018
4888
|
}
|
|
5019
4889
|
});
|
|
5020
4890
|
|
|
5021
4891
|
// bin/mason.ts
|
|
5022
|
-
import { fileURLToPath as
|
|
4892
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5023
4893
|
|
|
5024
4894
|
// src/automation/cli.ts
|
|
5025
4895
|
init_execution();
|
|
@@ -5030,7 +4900,7 @@ init_store();
|
|
|
5030
4900
|
import { parseArgs } from "util";
|
|
5031
4901
|
var USAGE = `Usage: mason-auto <setup|install|config|status|check|hook> [options]
|
|
5032
4902
|
|
|
5033
|
-
setup [--host claude|codex]
|
|
4903
|
+
setup [--host claude|codex] Connect MCP, instructions and hooks to mason on PATH; retain the initial audit
|
|
5034
4904
|
install --host claude|codex Merge lifecycle hooks into this project's host config
|
|
5035
4905
|
config --host claude|codex Print the host config without writing
|
|
5036
4906
|
status Read configured hooks and observed runtime events
|
|
@@ -5115,8 +4985,11 @@ async function runAutomationCli(argv, stdin = "", io = {
|
|
|
5115
4985
|
}
|
|
5116
4986
|
|
|
5117
4987
|
// bin/mason.ts
|
|
5118
|
-
var
|
|
5119
|
-
var
|
|
4988
|
+
var inputArgs = process.argv.slice(2);
|
|
4989
|
+
var managedLaunch = inputArgs[0] === "--setup-host";
|
|
4990
|
+
var setupHost = managedLaunch ? inputArgs.splice(0, 2)[1] : void 0;
|
|
4991
|
+
var [command, ...args] = inputArgs;
|
|
4992
|
+
var usage = `Mason ${"0.15.0"}
|
|
5120
4993
|
|
|
5121
4994
|
Usage: mason <command> [options]
|
|
5122
4995
|
|
|
@@ -5133,24 +5006,28 @@ Usage: mason <command> [options]
|
|
|
5133
5006
|
Use mason <command> --help for options. npm users can keep using mason-auto,
|
|
5134
5007
|
mason-audit, mason-review, mason-drift, mason-hook and mason-mcp.`;
|
|
5135
5008
|
try {
|
|
5136
|
-
if (
|
|
5137
|
-
|
|
5009
|
+
if (managedLaunch) {
|
|
5010
|
+
if (!setupHost || !["codex", "claude"].includes(setupHost) || !["mcp", "auto"].includes(command)) throw new Error("Invalid setup invocation.");
|
|
5011
|
+
const { prepareLaunch: prepareLaunch2 } = await Promise.resolve().then(() => (init_launcher(), launcher_exports));
|
|
5012
|
+
await prepareLaunch2(setupHost);
|
|
5013
|
+
}
|
|
5014
|
+
if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.15.0" }));
|
|
5015
|
+
else if (!command || ["--help", "-h", "help"].includes(command)) console.log(usage);
|
|
5016
|
+
else if (["--version", "-v"].includes(command)) console.log("0.15.0");
|
|
5138
5017
|
else if (command === "internal-install") {
|
|
5139
5018
|
if (args.length) throw new Error("internal-install takes no arguments.");
|
|
5140
|
-
const { sourceRuntime: sourceRuntime2 } = await Promise.resolve().then(() => (init_runtime2(), runtime_exports));
|
|
5141
|
-
const selected = await sourceRuntime2();
|
|
5142
|
-
if (!selected.bundleRoot) throw new Error("This command requires a standalone bundle.");
|
|
5143
5019
|
const { installStandalone: installStandalone2 } = await Promise.resolve().then(() => (init_install2(), install_exports2));
|
|
5144
|
-
const
|
|
5020
|
+
const bundleRoot = fileURLToPath2(new URL("../..", import.meta.url));
|
|
5021
|
+
const installed = await installStandalone2(bundleRoot);
|
|
5145
5022
|
console.log(`Installed Mason ${installed.version} in ${installed.home}.
|
|
5146
5023
|
${installed.path.message}`);
|
|
5147
5024
|
} else if (command === "upgrade" || command === "uninstall") {
|
|
5148
|
-
if (args.includes("--help")) console.log(command === "upgrade" ? "Usage: mason upgrade [version].
|
|
5025
|
+
if (args.includes("--help")) console.log(command === "upgrade" ? "Usage: mason upgrade [version]. Configured projects use the upgraded command on their next launch. Restart running assistants." : "Usage: mason uninstall. Removes the standalone user installation; project configuration and knowledge are retained; integrations need Mason reinstalled.");
|
|
5149
5026
|
else {
|
|
5150
5027
|
const { upgradeStandalone: upgradeStandalone2, uninstallStandalone: uninstallStandalone2 } = await Promise.resolve().then(() => (init_install2(), install_exports2));
|
|
5151
5028
|
if (args.length > (command === "upgrade" ? 1 : 0)) throw new Error("Unexpected arguments.");
|
|
5152
5029
|
if (command === "upgrade") process.exitCode = await upgradeStandalone2(args[0]);
|
|
5153
|
-
else console.log((process.platform === "win32" ? "Finishing installation cleanup after runtime exit: " : "Removed standalone installation: ") + await uninstallStandalone2() + ". Project
|
|
5030
|
+
else console.log((process.platform === "win32" ? "Finishing installation cleanup after runtime exit: " : "Removed standalone installation: ") + await uninstallStandalone2() + ". Project configuration and knowledge were retained. Reinstall Mason to use its integrations.");
|
|
5154
5031
|
}
|
|
5155
5032
|
} else if (["setup", "status", "check", "auto"].includes(command)) {
|
|
5156
5033
|
const forwarded = command === "auto" ? args : [command, ...args];
|
|
@@ -5162,11 +5039,17 @@ ${installed.path.message}`);
|
|
|
5162
5039
|
process.exitCode = await runAutomationCli(forwarded, input);
|
|
5163
5040
|
} else if (["audit", "review", "drift", "hook", "mcp"].includes(command)) {
|
|
5164
5041
|
const binary = new URL(`./mason-${command}.js`, import.meta.url);
|
|
5165
|
-
process.argv = [process.execPath,
|
|
5042
|
+
process.argv = [process.execPath, fileURLToPath2(binary), ...args];
|
|
5166
5043
|
await import(binary.href);
|
|
5167
5044
|
} else throw new Error("Unknown command: " + command + ". Run mason --help.");
|
|
5168
5045
|
} catch (error) {
|
|
5169
|
-
|
|
5170
|
-
|
|
5046
|
+
const message2 = "Mason: " + (error instanceof Error ? error.message : String(error));
|
|
5047
|
+
if (command === "auto" && isHookCommand(args)) {
|
|
5048
|
+
console.log(JSON.stringify({ systemMessage: message2 + " Verification was not established." }));
|
|
5049
|
+
process.exitCode = 0;
|
|
5050
|
+
} else {
|
|
5051
|
+
console.error(message2);
|
|
5052
|
+
process.exitCode = 2;
|
|
5053
|
+
}
|
|
5171
5054
|
}
|
|
5172
5055
|
//# sourceMappingURL=mason.js.map
|