mason-context 0.22.0 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/mason-audit.js +74 -26
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-drift.js +42 -19
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js +41 -19
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +699 -173
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +54 -29
- package/dist/mason-review.js.map +1 -1
- package/dist/mason-runtime.js +12 -6
- package/dist/mason-runtime.js.map +1 -1
- package/dist/mason.js +722 -148
- package/dist/mason.js.map +1 -1
- package/package.json +1 -1
package/dist/mason.js
CHANGED
|
@@ -426,11 +426,12 @@ async function storePath(root, relative, createParents = false) {
|
|
|
426
426
|
}
|
|
427
427
|
return current;
|
|
428
428
|
}
|
|
429
|
-
async function readStoreJson(root, relative) {
|
|
429
|
+
async function readStoreJson(root, relative, maxBytes = STORE_MAX_BYTES) {
|
|
430
430
|
try {
|
|
431
431
|
const file = await storePath(root, relative);
|
|
432
|
-
const raw = await readBoundedFile(file,
|
|
433
|
-
if (raw === null)
|
|
432
|
+
const raw = await readBoundedFile(file, maxBytes);
|
|
433
|
+
if (raw === null)
|
|
434
|
+
throw new Error(`file is not regular or exceeds ${maxBytes / 1024 / 1024} MiB`);
|
|
434
435
|
const parsed = JSON.parse(raw);
|
|
435
436
|
if (parsed === null) throw new Error("expected a JSON object, received null");
|
|
436
437
|
return parsed;
|
|
@@ -442,9 +443,9 @@ async function readStoreJson(root, relative) {
|
|
|
442
443
|
);
|
|
443
444
|
}
|
|
444
445
|
}
|
|
445
|
-
async function writeStoreJson(root, relative, value) {
|
|
446
|
-
const payload = JSON.stringify(value, null, 2) + "\n";
|
|
447
|
-
if (Buffer.byteLength(payload) >
|
|
446
|
+
async function writeStoreJson(root, relative, value, options = {}) {
|
|
447
|
+
const payload = JSON.stringify(value, null, options.compact ? void 0 : 2) + "\n";
|
|
448
|
+
if (Buffer.byteLength(payload) > STORE_MAX_BYTES) {
|
|
448
449
|
throw new Error(`Mason store ${relative} exceeds 10 MiB`);
|
|
449
450
|
}
|
|
450
451
|
const file = await storePath(root, relative, true);
|
|
@@ -462,11 +463,13 @@ async function writeStoreJson(root, relative, value) {
|
|
|
462
463
|
await fs2.rm(temporary, { force: true });
|
|
463
464
|
}
|
|
464
465
|
}
|
|
466
|
+
var STORE_MAX_BYTES;
|
|
465
467
|
var init_storage = __esm({
|
|
466
468
|
"src/utils/storage.ts"() {
|
|
467
469
|
"use strict";
|
|
468
470
|
init_paths();
|
|
469
471
|
init_files();
|
|
472
|
+
STORE_MAX_BYTES = 10 * 1024 * 1024;
|
|
470
473
|
}
|
|
471
474
|
});
|
|
472
475
|
|
|
@@ -553,7 +556,12 @@ async function captureAnchorScopes(root, scopes2) {
|
|
|
553
556
|
);
|
|
554
557
|
const inventory = access.inventory();
|
|
555
558
|
if (!inventory) throw new Error("Git anchor inventory unavailable");
|
|
556
|
-
const
|
|
559
|
+
const visible = indexMatchingPaths(
|
|
560
|
+
union.map((anchor) => [anchor]),
|
|
561
|
+
inventory
|
|
562
|
+
);
|
|
563
|
+
const hidden = new Set(union.filter((_, index) => !visible[index].length));
|
|
564
|
+
const ignored = hidden.size ? (await inspectionGit(
|
|
557
565
|
[
|
|
558
566
|
"ls-files",
|
|
559
567
|
"-z",
|
|
@@ -561,13 +569,11 @@ async function captureAnchorScopes(root, scopes2) {
|
|
|
561
569
|
"--ignored",
|
|
562
570
|
"--exclude-standard",
|
|
563
571
|
"--",
|
|
564
|
-
...
|
|
572
|
+
...[...hidden].map((anchor) => `:(literal)${anchor}`)
|
|
565
573
|
],
|
|
566
574
|
{ cwd: root, maxBuffer: 8 * 1024 * 1024 }
|
|
567
|
-
);
|
|
568
|
-
const files = [
|
|
569
|
-
.../* @__PURE__ */ new Set([...inventory, ...ignored.stdout.split("\0").filter(Boolean)])
|
|
570
|
-
].sort();
|
|
575
|
+
)).stdout.split("\0").filter(Boolean) : [];
|
|
576
|
+
const files = [.../* @__PURE__ */ new Set([...inventory, ...ignored])].sort();
|
|
571
577
|
const patterns = await Promise.all(
|
|
572
578
|
union.map(async (anchor) => {
|
|
573
579
|
const literal2 = fg3.escapePath(anchor);
|
|
@@ -583,7 +589,7 @@ async function captureAnchorScopes(root, scopes2) {
|
|
|
583
589
|
const filesByScope = new Map(
|
|
584
590
|
scopes2.map((scope, index) => [JSON.stringify([...new Set(scope)].sort()), indexed[index]])
|
|
585
591
|
);
|
|
586
|
-
return { filesByScope, eligible };
|
|
592
|
+
return { filesByScope, eligible, hidden, ignored: new Set(ignored) };
|
|
587
593
|
};
|
|
588
594
|
return Promise.all(
|
|
589
595
|
scopes2.map((scope) => {
|
|
@@ -595,8 +601,10 @@ async function captureAnchorScopes(root, scopes2) {
|
|
|
595
601
|
const capture = { anchors, files: {}, complete: true };
|
|
596
602
|
if (!anchors.length) return capture;
|
|
597
603
|
try {
|
|
598
|
-
const { eligible, filesByScope } = await (context ??= prepareContext());
|
|
599
|
-
const files = filesByScope.get(JSON.stringify(anchors))
|
|
604
|
+
const { eligible, filesByScope, hidden, ignored } = await (context ??= prepareContext());
|
|
605
|
+
const files = filesByScope.get(JSON.stringify(anchors)).filter(
|
|
606
|
+
(file) => !ignored.has(file) || anchors.some((anchor) => hidden.has(anchor) && anchorMatches(anchor, file))
|
|
607
|
+
);
|
|
600
608
|
if (files.length > 2e3) return { ...capture, complete: false };
|
|
601
609
|
for (const file of files) {
|
|
602
610
|
const evidence = await inspectionRead(
|
|
@@ -3003,6 +3011,195 @@ var init_deps_changed = __esm({
|
|
|
3003
3011
|
}
|
|
3004
3012
|
});
|
|
3005
3013
|
|
|
3014
|
+
// src/decisions/excerpts.ts
|
|
3015
|
+
import { createHash as createHash4 } from "crypto";
|
|
3016
|
+
import { execFile as execFile2 } from "child_process";
|
|
3017
|
+
import { promisify as promisify2 } from "util";
|
|
3018
|
+
function driftBaseline(record) {
|
|
3019
|
+
const effective = effectiveDecision(record);
|
|
3020
|
+
const inspection = latestDecisionInspection(
|
|
3021
|
+
record,
|
|
3022
|
+
decisionApproval(effective) === "accepted" ? "operative" : "all"
|
|
3023
|
+
);
|
|
3024
|
+
return {
|
|
3025
|
+
record: effective,
|
|
3026
|
+
capture: inspection?.capture ?? (effective.version === 2 ? effective.capture : void 0),
|
|
3027
|
+
commits: [inspection?.headHash, effective.refreshedHash].filter(
|
|
3028
|
+
(hash2) => !!hash2 && /^[a-f0-9]{40,64}$/.test(hash2)
|
|
3029
|
+
)
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
async function capturedCommit(root, file, hash2, preferred) {
|
|
3033
|
+
if (/[\n\r]/.test(file)) return null;
|
|
3034
|
+
const { stdout } = await exec(
|
|
3035
|
+
"git",
|
|
3036
|
+
["--literal-pathspecs", "log", "-n", String(HISTORY_DEPTH), "--format=%H", "--", file],
|
|
3037
|
+
{ cwd: root, maxBuffer: 1024 * 1024 }
|
|
3038
|
+
);
|
|
3039
|
+
const commits = [.../* @__PURE__ */ new Set([...preferred, ...stdout.split("\n").filter(Boolean)])];
|
|
3040
|
+
const batch = exec("git", ["cat-file", "--batch"], {
|
|
3041
|
+
cwd: root,
|
|
3042
|
+
encoding: "buffer",
|
|
3043
|
+
maxBuffer: 64 * 1024 * 1024
|
|
3044
|
+
});
|
|
3045
|
+
batch.child.stdin.end(commits.map((commit) => `${commit}:${file}
|
|
3046
|
+
`).join(""));
|
|
3047
|
+
const output = (await batch).stdout;
|
|
3048
|
+
let position = 0;
|
|
3049
|
+
for (const commit of commits) {
|
|
3050
|
+
const end = output.indexOf(10, position);
|
|
3051
|
+
if (end < 0) break;
|
|
3052
|
+
const header = output.subarray(position, end).toString("utf8").split(" ");
|
|
3053
|
+
position = end + 1;
|
|
3054
|
+
if (header[1] !== "blob") continue;
|
|
3055
|
+
const size = Number(header[2]);
|
|
3056
|
+
const bytes = output.subarray(position, position + size);
|
|
3057
|
+
position += size + 1;
|
|
3058
|
+
if (createHash4("sha256").update(bytes).digest("hex") === hash2) return commit;
|
|
3059
|
+
}
|
|
3060
|
+
return null;
|
|
3061
|
+
}
|
|
3062
|
+
function namedIdentifiers(text2) {
|
|
3063
|
+
const names = /* @__PURE__ */ new Set();
|
|
3064
|
+
const add = (name) => {
|
|
3065
|
+
if (name.length >= 3) names.add(name);
|
|
3066
|
+
};
|
|
3067
|
+
for (const [, span] of text2.matchAll(/`([^`]+)`/g))
|
|
3068
|
+
for (const [name] of span.matchAll(/[A-Za-z_$][\w$]*(?:-[A-Za-z0-9_$]+)*/g)) add(name);
|
|
3069
|
+
for (const [, name] of text2.matchAll(/([A-Za-z_$][\w$]*)\(/g)) add(name);
|
|
3070
|
+
for (const [name] of text2.matchAll(/[A-Za-z_$][\w$]*/g))
|
|
3071
|
+
if (/[a-z][A-Z]|[_$]/.test(name)) add(name);
|
|
3072
|
+
return [...names];
|
|
3073
|
+
}
|
|
3074
|
+
function codeTokens(code) {
|
|
3075
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
3076
|
+
for (const [name] of code.matchAll(/[A-Za-z_$][\w$]*/g)) {
|
|
3077
|
+
tokens.add(name.toLowerCase());
|
|
3078
|
+
for (const part of name.split(/_+|(?<=[a-z0-9])(?=[A-Z])/))
|
|
3079
|
+
if (part.length >= 3) tokens.add(part.toLowerCase());
|
|
3080
|
+
}
|
|
3081
|
+
return tokens;
|
|
3082
|
+
}
|
|
3083
|
+
function parseHunks(diff) {
|
|
3084
|
+
const hunks = [];
|
|
3085
|
+
for (const line of diff.split("\n")) {
|
|
3086
|
+
if (line.startsWith("@@")) hunks.push({ header: line, lines: [] });
|
|
3087
|
+
else if (hunks.length && /^[+-]/.test(line)) hunks.at(-1).lines.push(line);
|
|
3088
|
+
}
|
|
3089
|
+
return hunks;
|
|
3090
|
+
}
|
|
3091
|
+
async function driftExcerpt(root, record, changedFiles) {
|
|
3092
|
+
const baseline = driftBaseline(record);
|
|
3093
|
+
const excerpt = { mentions: [], relevance: "unknown", files: [], omittedFiles: [] };
|
|
3094
|
+
const named = namedIdentifiers(`${baseline.record.title}
|
|
3095
|
+
${baseline.record.body}`);
|
|
3096
|
+
const found = /* @__PURE__ */ new Set();
|
|
3097
|
+
let complete = !!baseline.capture && changedFiles.length <= MAX_DIFFED_FILES;
|
|
3098
|
+
const access = await createFileAccess(root);
|
|
3099
|
+
for (const [index, file] of changedFiles.slice(0, MAX_DIFFED_FILES).entries()) {
|
|
3100
|
+
let hunks = null;
|
|
3101
|
+
let unavailable;
|
|
3102
|
+
const captured = baseline.capture?.files[file];
|
|
3103
|
+
try {
|
|
3104
|
+
if (!baseline.capture?.anchors.some((anchor) => anchorMatches(anchor, file))) {
|
|
3105
|
+
unavailable = "not in the captured anchor scope";
|
|
3106
|
+
} else if (captured === void 0) {
|
|
3107
|
+
const source = await access.read(file);
|
|
3108
|
+
if (source)
|
|
3109
|
+
hunks = [
|
|
3110
|
+
{
|
|
3111
|
+
header: "(added since capture)",
|
|
3112
|
+
lines: source.content.replace(/\r?\n$/, "").split("\n").map((line) => "+" + line)
|
|
3113
|
+
}
|
|
3114
|
+
];
|
|
3115
|
+
else unavailable = "added since capture but unreadable";
|
|
3116
|
+
} else {
|
|
3117
|
+
const commit = await capturedCommit(root, file, captured, baseline.commits);
|
|
3118
|
+
if (!commit) unavailable = "captured content is not in recent Git history";
|
|
3119
|
+
else {
|
|
3120
|
+
const { stdout } = await exec(
|
|
3121
|
+
"git",
|
|
3122
|
+
[
|
|
3123
|
+
"--literal-pathspecs",
|
|
3124
|
+
"diff",
|
|
3125
|
+
"--no-ext-diff",
|
|
3126
|
+
"--no-textconv",
|
|
3127
|
+
"--no-color",
|
|
3128
|
+
"--no-renames",
|
|
3129
|
+
"-U0",
|
|
3130
|
+
commit,
|
|
3131
|
+
"--",
|
|
3132
|
+
file
|
|
3133
|
+
],
|
|
3134
|
+
{ cwd: root, maxBuffer: 1024 * 1024 }
|
|
3135
|
+
);
|
|
3136
|
+
hunks = parseHunks(stdout);
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
} catch {
|
|
3140
|
+
unavailable = "diff could not be read within its bounds";
|
|
3141
|
+
}
|
|
3142
|
+
if (!hunks?.length) complete = false;
|
|
3143
|
+
const code = (hunks ?? []).map((hunk) => [hunk.header, ...hunk.lines.map((line) => line.slice(1))].join("\n")).join("\n");
|
|
3144
|
+
const tokens = codeTokens(code);
|
|
3145
|
+
for (const name of named)
|
|
3146
|
+
if (name.includes("-") ? code.toLowerCase().includes(name.toLowerCase()) : tokens.has(name.toLowerCase()))
|
|
3147
|
+
found.add(name);
|
|
3148
|
+
if (index >= MAX_SHOWN_FILES) excerpt.omittedFiles.push(file);
|
|
3149
|
+
else
|
|
3150
|
+
excerpt.files.push({
|
|
3151
|
+
path: file,
|
|
3152
|
+
hunks: (hunks ?? []).slice(0, MAX_SHOWN_HUNKS).map((hunk) => ({ ...hunk, lines: hunk.lines.slice(0, MAX_HUNK_LINES) })),
|
|
3153
|
+
omittedHunks: Math.max(0, (hunks?.length ?? 0) - MAX_SHOWN_HUNKS),
|
|
3154
|
+
...unavailable ? { unavailable } : {}
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
excerpt.omittedFiles.push(...changedFiles.slice(MAX_DIFFED_FILES));
|
|
3158
|
+
excerpt.mentions = [...found].sort();
|
|
3159
|
+
excerpt.relevance = found.size ? "mentioned" : complete && named.length ? "low" : "unknown";
|
|
3160
|
+
if (excerpt.relevance === "unknown")
|
|
3161
|
+
excerpt.unknownReason = named.length ? "incomplete" : "no-identifiers";
|
|
3162
|
+
return excerpt;
|
|
3163
|
+
}
|
|
3164
|
+
function formatDriftExcerpt(title, id, excerpt) {
|
|
3165
|
+
const safe = (value, max = 160) => value.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, max);
|
|
3166
|
+
const lines = [`Changed lines for decision ${JSON.stringify(safe(title, 250))} (${safe(id)}):`];
|
|
3167
|
+
lines.push(
|
|
3168
|
+
excerpt.relevance === "mentioned" ? ` Changed lines mention identifiers named by the decision: ${excerpt.mentions.map((name) => safe(name, 60)).join(", ")}.` : excerpt.relevance === "low" ? " Low relevance: no changed line or enclosing declaration mentions an identifier named by the decision. This is a name match, not evidence of consistency; inspect before recording." : excerpt.unknownReason === "no-identifiers" ? " Relevance not assessed: the decision names no code identifiers (such as `refresh()` or onRefresh) to match against the changes." : " Relevance unknown: some changed content could not be compared with its captured version."
|
|
3169
|
+
);
|
|
3170
|
+
for (const file of excerpt.files) {
|
|
3171
|
+
if (file.unavailable) {
|
|
3172
|
+
lines.push(` ${safe(file.path)}: ${file.unavailable}.`);
|
|
3173
|
+
continue;
|
|
3174
|
+
}
|
|
3175
|
+
for (const hunk of file.hunks)
|
|
3176
|
+
lines.push(
|
|
3177
|
+
` ${safe(file.path)} ${safe(hunk.header, 120)}`,
|
|
3178
|
+
...hunk.lines.map((line) => " " + safe(line))
|
|
3179
|
+
);
|
|
3180
|
+
if (!file.hunks.length) lines.push(` ${safe(file.path)}: changed without text hunks.`);
|
|
3181
|
+
if (file.omittedHunks) lines.push(` ${safe(file.path)}: ${file.omittedHunks} more hunk(s).`);
|
|
3182
|
+
}
|
|
3183
|
+
if (excerpt.omittedFiles.length)
|
|
3184
|
+
lines.push(` ${excerpt.omittedFiles.length} more changed file(s) not shown.`);
|
|
3185
|
+
return lines.join("\n");
|
|
3186
|
+
}
|
|
3187
|
+
var exec, MAX_DIFFED_FILES, MAX_SHOWN_FILES, MAX_SHOWN_HUNKS, MAX_HUNK_LINES, HISTORY_DEPTH;
|
|
3188
|
+
var init_excerpts = __esm({
|
|
3189
|
+
"src/decisions/excerpts.ts"() {
|
|
3190
|
+
"use strict";
|
|
3191
|
+
init_files();
|
|
3192
|
+
init_paths();
|
|
3193
|
+
init_provenance();
|
|
3194
|
+
exec = promisify2(execFile2);
|
|
3195
|
+
MAX_DIFFED_FILES = 8;
|
|
3196
|
+
MAX_SHOWN_FILES = 3;
|
|
3197
|
+
MAX_SHOWN_HUNKS = 2;
|
|
3198
|
+
MAX_HUNK_LINES = 8;
|
|
3199
|
+
HISTORY_DEPTH = 20;
|
|
3200
|
+
}
|
|
3201
|
+
});
|
|
3202
|
+
|
|
3006
3203
|
// src/decisions/drift.ts
|
|
3007
3204
|
import path17 from "path";
|
|
3008
3205
|
async function computeDecisionDrift(rootDir, decisions) {
|
|
@@ -3067,15 +3264,11 @@ async function computeDecisionDrift(rootDir, decisions) {
|
|
|
3067
3264
|
};
|
|
3068
3265
|
for (const record of store.records) {
|
|
3069
3266
|
if (record.status !== "active") continue;
|
|
3070
|
-
const
|
|
3071
|
-
const
|
|
3072
|
-
record,
|
|
3073
|
-
decisionApproval(effective) === "accepted" ? "operative" : "all"
|
|
3074
|
-
);
|
|
3075
|
-
const state = await inspect(effective, inspection?.capture);
|
|
3267
|
+
const baseline = driftBaseline(record);
|
|
3268
|
+
const state = await inspect(baseline.record, baseline.capture);
|
|
3076
3269
|
report.freshness[record.id] = state.freshness;
|
|
3077
3270
|
if (state.changedFiles.length) report.staleDecisions[record.id] = state.changedFiles;
|
|
3078
|
-
if (
|
|
3271
|
+
if (baseline.record !== record)
|
|
3079
3272
|
(report.pendingProposals ??= {})[record.id] = await inspect(
|
|
3080
3273
|
record,
|
|
3081
3274
|
latestDecisionInspection(record)?.capture
|
|
@@ -3091,13 +3284,14 @@ var init_drift2 = __esm({
|
|
|
3091
3284
|
init_paths();
|
|
3092
3285
|
init_snapshot();
|
|
3093
3286
|
init_decisions();
|
|
3287
|
+
init_excerpts();
|
|
3094
3288
|
init_provenance();
|
|
3095
3289
|
}
|
|
3096
3290
|
});
|
|
3097
3291
|
|
|
3098
3292
|
// src/audit/checks/decision-anchor.ts
|
|
3099
3293
|
async function checkDecisionAnchors(ctx) {
|
|
3100
|
-
const result = emptyResult();
|
|
3294
|
+
const result = { ...emptyResult(), incompleteDecisions: [] };
|
|
3101
3295
|
if (!ctx.decisionsPresent) return result;
|
|
3102
3296
|
const store = await loadDecisionStore(ctx.root);
|
|
3103
3297
|
const records = store.records;
|
|
@@ -3129,6 +3323,7 @@ async function checkDecisionAnchors(ctx) {
|
|
|
3129
3323
|
check: "decision-anchor-drift",
|
|
3130
3324
|
reason: `Anchor evidence for decision ${record.id} is incomplete or unreadable`
|
|
3131
3325
|
});
|
|
3326
|
+
result.incompleteDecisions.push(record.id);
|
|
3132
3327
|
continue;
|
|
3133
3328
|
}
|
|
3134
3329
|
if (!changedFiles.length) continue;
|
|
@@ -3190,7 +3385,7 @@ var init_checks = __esm({
|
|
|
3190
3385
|
});
|
|
3191
3386
|
|
|
3192
3387
|
// src/audit/findings.ts
|
|
3193
|
-
import { createHash as
|
|
3388
|
+
import { createHash as createHash5 } from "crypto";
|
|
3194
3389
|
import { z as z6 } from "zod";
|
|
3195
3390
|
function findingId(finding) {
|
|
3196
3391
|
const e = finding.evidence;
|
|
@@ -3221,7 +3416,7 @@ var digest, checkSchema, commitSchema, anchorSchema, count, scopeSchema, evidenc
|
|
|
3221
3416
|
var init_findings = __esm({
|
|
3222
3417
|
"src/audit/findings.ts"() {
|
|
3223
3418
|
"use strict";
|
|
3224
|
-
digest = (value) =>
|
|
3419
|
+
digest = (value) => createHash5("sha256").update(JSON.stringify(value)).digest("hex");
|
|
3225
3420
|
checkSchema = z6.enum([
|
|
3226
3421
|
"deleted-reference",
|
|
3227
3422
|
"new-module",
|
|
@@ -3328,12 +3523,25 @@ var init_findings = __esm({
|
|
|
3328
3523
|
}
|
|
3329
3524
|
});
|
|
3330
3525
|
|
|
3526
|
+
// src/automation/pending-tools.ts
|
|
3527
|
+
function boundPending(pending) {
|
|
3528
|
+
for (const key of Object.keys(pending).slice(0, -PENDING_LIMIT + 1)) delete pending[key];
|
|
3529
|
+
}
|
|
3530
|
+
var PENDING_LIMIT;
|
|
3531
|
+
var init_pending_tools = __esm({
|
|
3532
|
+
"src/automation/pending-tools.ts"() {
|
|
3533
|
+
"use strict";
|
|
3534
|
+
PENDING_LIMIT = 32;
|
|
3535
|
+
}
|
|
3536
|
+
});
|
|
3537
|
+
|
|
3331
3538
|
// src/automation/stop-review.ts
|
|
3332
3539
|
import { z as z7 } from "zod";
|
|
3333
3540
|
function observeDecisionEdits(session, inputs, event) {
|
|
3334
3541
|
if (event.host !== "claude" || !event.mutating || !event.toolId) return;
|
|
3335
3542
|
const state = session.stopReview ??= empty();
|
|
3336
3543
|
if (event.event === "before_tool") {
|
|
3544
|
+
boundPending(state.pending);
|
|
3337
3545
|
state.pending[event.toolId] = { head: inputs.head, evidence: inputs.decisionEvidence };
|
|
3338
3546
|
} else if (event.event === "after_tool") {
|
|
3339
3547
|
const before = state.pending[event.toolId];
|
|
@@ -3390,7 +3598,7 @@ function decisionStopReview(session, inputs, report, reentry) {
|
|
|
3390
3598
|
}
|
|
3391
3599
|
decisions.set(evidence.decisionId, value);
|
|
3392
3600
|
}
|
|
3393
|
-
if (!decisions.size) return { reason: null, block: false };
|
|
3601
|
+
if (!decisions.size) return { reason: null, block: false, decisions: [] };
|
|
3394
3602
|
let fresh = false;
|
|
3395
3603
|
for (const [id, value] of decisions) {
|
|
3396
3604
|
const previous = state.delivered[id];
|
|
@@ -3413,7 +3621,7 @@ function decisionStopReview(session, inputs, report, reentry) {
|
|
|
3413
3621
|
"If contradicted or uncertain, explain the mismatch to the user and request direction. Do not accept, reaffirm, or retire a decision without authorized human review. A revised proposal leaves the accepted revision operative until reviewed.",
|
|
3414
3622
|
`Evidence: ${report.reportPath}. Concurrent changes can contribute; captures do not prove authorship. This reminder does not resolve the finding.`
|
|
3415
3623
|
].join("\n");
|
|
3416
|
-
return { reason, block: !reentry && fresh };
|
|
3624
|
+
return { reason, block: !reentry && fresh, decisions: [...decisions.keys()] };
|
|
3417
3625
|
}
|
|
3418
3626
|
var evidenceSchema2, stopReviewSchema, empty;
|
|
3419
3627
|
var init_stop_review = __esm({
|
|
@@ -3421,6 +3629,7 @@ var init_stop_review = __esm({
|
|
|
3421
3629
|
"use strict";
|
|
3422
3630
|
init_anchors();
|
|
3423
3631
|
init_findings();
|
|
3632
|
+
init_pending_tools();
|
|
3424
3633
|
evidenceSchema2 = z7.record(z7.object({ digest: z7.string(), capture: anchorCaptureSchema }));
|
|
3425
3634
|
stopReviewSchema = z7.object({
|
|
3426
3635
|
pending: z7.record(z7.object({ head: z7.string(), evidence: evidenceSchema2 })),
|
|
@@ -3577,16 +3786,41 @@ function parseState(raw) {
|
|
|
3577
3786
|
throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
|
|
3578
3787
|
}
|
|
3579
3788
|
}
|
|
3789
|
+
function clearPendingTools(session) {
|
|
3790
|
+
session.pending = {};
|
|
3791
|
+
if (session.stopReview) session.stopReview.pending = {};
|
|
3792
|
+
}
|
|
3793
|
+
function pruneSessions(state, now, active, limit = SESSION_LIMIT) {
|
|
3794
|
+
for (const session of Object.values(state.sessions))
|
|
3795
|
+
if (now.getTime() - Date.parse(session.lastUsed) > IDLE_PENDING_MS) clearPendingTools(session);
|
|
3796
|
+
const evictable = Object.keys(state.sessions).filter((key) => key !== active).sort((a, b) => state.sessions[a].lastUsed.localeCompare(state.sessions[b].lastUsed));
|
|
3797
|
+
const excess = Object.keys(state.sessions).length - limit;
|
|
3798
|
+
for (const key of evictable.slice(0, Math.max(0, excess))) delete state.sessions[key];
|
|
3799
|
+
}
|
|
3800
|
+
function serializeState(state, active) {
|
|
3801
|
+
let payload = JSON.stringify(state) + "\n";
|
|
3802
|
+
while (Buffer.byteLength(payload) > STORE_MAX_BYTES) {
|
|
3803
|
+
const before = Object.keys(state.sessions).length;
|
|
3804
|
+
pruneSessions(state, /* @__PURE__ */ new Date(), active, before - 1);
|
|
3805
|
+
if (Object.keys(state.sessions).length === before)
|
|
3806
|
+
throw new Error(
|
|
3807
|
+
"Mason automation state exceeds 10 MiB after evicting every other session; inspect the active session in .mason/reports/automation/<id>/state.json. Original evidence was retained."
|
|
3808
|
+
);
|
|
3809
|
+
payload = JSON.stringify(state) + "\n";
|
|
3810
|
+
}
|
|
3811
|
+
return state;
|
|
3812
|
+
}
|
|
3580
3813
|
function withLock(root, directory3, run, waitMs = 5e3) {
|
|
3581
3814
|
return withStoreLock(root, directory3, run, waitMs, "Automation");
|
|
3582
3815
|
}
|
|
3583
|
-
var hostSchema, events, stateSchema;
|
|
3816
|
+
var hostSchema, events, stateSchema, SESSION_LIMIT, IDLE_PENDING_MS, STATE_READ_MAX_BYTES;
|
|
3584
3817
|
var init_store = __esm({
|
|
3585
3818
|
"src/automation/store.ts"() {
|
|
3586
3819
|
"use strict";
|
|
3587
3820
|
init_store_lock();
|
|
3588
3821
|
init_stop_review();
|
|
3589
3822
|
init_notifications();
|
|
3823
|
+
init_storage();
|
|
3590
3824
|
hostSchema = z9.enum(["claude", "codex"]);
|
|
3591
3825
|
events = [
|
|
3592
3826
|
"session_start",
|
|
@@ -3608,6 +3842,8 @@ var init_store = __esm({
|
|
|
3608
3842
|
host: hostSchema,
|
|
3609
3843
|
lastUsed: z9.string(),
|
|
3610
3844
|
notifiedDiagnostics: z9.array(z9.string()).default([]),
|
|
3845
|
+
/** Digest of the upstream drift notice already shown to this session. */
|
|
3846
|
+
upstreamNotice: z9.string().optional(),
|
|
3611
3847
|
pending: z9.record(z9.string()),
|
|
3612
3848
|
stopReview: stopReviewSchema.optional(),
|
|
3613
3849
|
coverageGaps: z9.array(z9.string()),
|
|
@@ -3622,6 +3858,9 @@ var init_store = __esm({
|
|
|
3622
3858
|
agentNotifications: notificationStateSchema.optional(),
|
|
3623
3859
|
analysis: z9.object({ id: z9.string(), fingerprint: z9.string(), completedAt: z9.number(), path: z9.string() }).optional()
|
|
3624
3860
|
});
|
|
3861
|
+
SESSION_LIMIT = 32;
|
|
3862
|
+
IDLE_PENDING_MS = 24 * 60 * 60 * 1e3;
|
|
3863
|
+
STATE_READ_MAX_BYTES = 64 * 1024 * 1024;
|
|
3625
3864
|
}
|
|
3626
3865
|
});
|
|
3627
3866
|
|
|
@@ -4354,9 +4593,18 @@ var init_repair = __esm({
|
|
|
4354
4593
|
});
|
|
4355
4594
|
|
|
4356
4595
|
// src/automation/evidence.ts
|
|
4596
|
+
var evidence_exports = {};
|
|
4597
|
+
__export(evidence_exports, {
|
|
4598
|
+
checkCache: () => checkCache,
|
|
4599
|
+
git: () => git3,
|
|
4600
|
+
hash: () => hash,
|
|
4601
|
+
readGuidanceInputs: () => readGuidanceInputs,
|
|
4602
|
+
readInputs: () => readInputs,
|
|
4603
|
+
workspace: () => workspace
|
|
4604
|
+
});
|
|
4357
4605
|
import fs14 from "fs/promises";
|
|
4358
4606
|
import path20 from "path";
|
|
4359
|
-
import { createHash as
|
|
4607
|
+
import { createHash as createHash6 } from "crypto";
|
|
4360
4608
|
import { z as z12 } from "zod";
|
|
4361
4609
|
async function git3(root, ...args2) {
|
|
4362
4610
|
return profilePhase(
|
|
@@ -4595,8 +4843,8 @@ function checkCache(raw, inputs) {
|
|
|
4595
4843
|
diagnostic,
|
|
4596
4844
|
has: (name) => entries[name]?.key === inputs.keys[name] && !entries[name].result.skipped.length,
|
|
4597
4845
|
serialize: () => {
|
|
4598
|
-
const
|
|
4599
|
-
return { version: 1, entries:
|
|
4846
|
+
const canonical2 = cacheSchema.shape.entries.parse(entries);
|
|
4847
|
+
return { version: 1, entries: canonical2, digest: hash(canonical2) };
|
|
4600
4848
|
}
|
|
4601
4849
|
};
|
|
4602
4850
|
}
|
|
@@ -4623,8 +4871,8 @@ var init_evidence = __esm({
|
|
|
4623
4871
|
init_dead_command();
|
|
4624
4872
|
init_storage();
|
|
4625
4873
|
init_advisory_review();
|
|
4626
|
-
engineVersion = true ? "0.
|
|
4627
|
-
hash = (value) =>
|
|
4874
|
+
engineVersion = true ? "0.23.1" : "development";
|
|
4875
|
+
hash = (value) => createHash6("sha256").update(JSON.stringify(value)).digest("hex");
|
|
4628
4876
|
content = readAuditInput;
|
|
4629
4877
|
cacheSchema = z12.object({
|
|
4630
4878
|
version: z12.literal(1),
|
|
@@ -5099,8 +5347,9 @@ async function executionStatus(root, directory3) {
|
|
|
5099
5347
|
const outstanding = [...log.attempts].reverse().find(
|
|
5100
5348
|
(a) => ["failed", "unknown"].includes(a.status) && (!completed || (a.finishedAt ?? a.startedAt) >= completed.startedAt)
|
|
5101
5349
|
);
|
|
5102
|
-
const
|
|
5103
|
-
|
|
5350
|
+
const inFlight = log.attempts.some((a) => a.status === "running");
|
|
5351
|
+
const status = outstanding?.status ?? (completed ? "completed" : inFlight ? "running" : log.attempts.at(-1)?.status ?? "not-observed");
|
|
5352
|
+
return { status, inFlight, attempts: log.attempts, discardedAttempts: log.discardedAttempts };
|
|
5104
5353
|
}
|
|
5105
5354
|
var failureSchema, attemptSchema, executionSchema, notificationsSchema, leasePath;
|
|
5106
5355
|
var init_execution = __esm({
|
|
@@ -5159,6 +5408,58 @@ var init_execution = __esm({
|
|
|
5159
5408
|
}
|
|
5160
5409
|
});
|
|
5161
5410
|
|
|
5411
|
+
// src/decisions/attribution.ts
|
|
5412
|
+
async function branchAttribution(root) {
|
|
5413
|
+
const git4 = async (...args2) => (await execGit(args2, { cwd: root, maxBuffer: 16 * 1024 * 1024 })).stdout.trim();
|
|
5414
|
+
let target = null;
|
|
5415
|
+
for (const ref of TARGETS) {
|
|
5416
|
+
try {
|
|
5417
|
+
await git4("rev-parse", "--verify", "--quiet", `${ref}^{commit}`);
|
|
5418
|
+
target = ref === "refs/remotes/origin/HEAD" ? await git4("symbolic-ref", "--short", ref) : ref.replace(/^refs\/(remotes|heads)\//, "");
|
|
5419
|
+
break;
|
|
5420
|
+
} catch {
|
|
5421
|
+
}
|
|
5422
|
+
}
|
|
5423
|
+
if (!target) return null;
|
|
5424
|
+
const current = await git4("symbolic-ref", "--quiet", "--short", "HEAD").catch(() => "");
|
|
5425
|
+
if (current && (target === current || target.endsWith("/" + current))) return null;
|
|
5426
|
+
let mergeBase, head;
|
|
5427
|
+
try {
|
|
5428
|
+
[mergeBase, head] = await Promise.all([
|
|
5429
|
+
git4("merge-base", "HEAD", target),
|
|
5430
|
+
git4("rev-parse", "HEAD")
|
|
5431
|
+
]);
|
|
5432
|
+
} catch {
|
|
5433
|
+
return null;
|
|
5434
|
+
}
|
|
5435
|
+
if (!mergeBase || mergeBase === head) return null;
|
|
5436
|
+
const [committed, workingTree] = await Promise.all([
|
|
5437
|
+
git4("diff", "--name-only", "--no-renames", "-z", mergeBase, "HEAD"),
|
|
5438
|
+
getWorkingTree(root)
|
|
5439
|
+
]);
|
|
5440
|
+
if (!workingTree.available) return null;
|
|
5441
|
+
return {
|
|
5442
|
+
target,
|
|
5443
|
+
mergeBase,
|
|
5444
|
+
branchFiles: /* @__PURE__ */ new Set([...committed.split("\0").filter(Boolean), ...workingTree.changedFiles])
|
|
5445
|
+
};
|
|
5446
|
+
}
|
|
5447
|
+
var TARGETS;
|
|
5448
|
+
var init_attribution = __esm({
|
|
5449
|
+
"src/decisions/attribution.ts"() {
|
|
5450
|
+
"use strict";
|
|
5451
|
+
init_drift();
|
|
5452
|
+
init_git_read();
|
|
5453
|
+
TARGETS = [
|
|
5454
|
+
"refs/remotes/origin/HEAD",
|
|
5455
|
+
"refs/remotes/origin/main",
|
|
5456
|
+
"refs/remotes/origin/master",
|
|
5457
|
+
"refs/heads/main",
|
|
5458
|
+
"refs/heads/master"
|
|
5459
|
+
];
|
|
5460
|
+
}
|
|
5461
|
+
});
|
|
5462
|
+
|
|
5162
5463
|
// src/decisions/pending.ts
|
|
5163
5464
|
var pending_exports = {};
|
|
5164
5465
|
__export(pending_exports, {
|
|
@@ -5245,7 +5546,7 @@ async function observeReadOnlyTool(dir, event) {
|
|
|
5245
5546
|
const ws = await profilePhase("automation.workspace", () => workspace(dir));
|
|
5246
5547
|
await withLock(ws.root, ws.directory, async () => {
|
|
5247
5548
|
const statePath2 = ws.directory + "/state.json";
|
|
5248
|
-
const raw = await readStoreJson(ws.root, statePath2);
|
|
5549
|
+
const raw = await readStoreJson(ws.root, statePath2, STATE_READ_MAX_BYTES);
|
|
5249
5550
|
if (raw === null) return;
|
|
5250
5551
|
const state = parseState(raw);
|
|
5251
5552
|
if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
|
|
@@ -5258,7 +5559,14 @@ async function observeReadOnlyTool(dir, event) {
|
|
|
5258
5559
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5259
5560
|
session.lastUsed = now;
|
|
5260
5561
|
session.events[event.event] = { at: now, count: (session.events[event.event]?.count ?? 0) + 1 };
|
|
5261
|
-
await writeStoreJson(
|
|
5562
|
+
await writeStoreJson(
|
|
5563
|
+
ws.root,
|
|
5564
|
+
statePath2,
|
|
5565
|
+
serializeState(state, hash([event.host, event.sessionId])),
|
|
5566
|
+
{
|
|
5567
|
+
compact: true
|
|
5568
|
+
}
|
|
5569
|
+
);
|
|
5262
5570
|
});
|
|
5263
5571
|
return { root: ws.root, directory: ws.directory };
|
|
5264
5572
|
}
|
|
@@ -5278,7 +5586,7 @@ function summarize(report) {
|
|
|
5278
5586
|
].join("\n");
|
|
5279
5587
|
}
|
|
5280
5588
|
async function loadState(ws) {
|
|
5281
|
-
const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
|
|
5589
|
+
const raw = await readStoreJson(ws.root, ws.directory + "/state.json", STATE_READ_MAX_BYTES);
|
|
5282
5590
|
const state = raw === null ? {
|
|
5283
5591
|
version: 1,
|
|
5284
5592
|
root: ws.root,
|
|
@@ -5510,13 +5818,47 @@ async function analyze(ws, inputs, event, startedAt) {
|
|
|
5510
5818
|
completedAt: analysis.completedAt,
|
|
5511
5819
|
path: analysisPath
|
|
5512
5820
|
};
|
|
5513
|
-
await writeStoreJson(ws.root, ws.directory + "/state.json", state)
|
|
5821
|
+
await writeStoreJson(ws.root, ws.directory + "/state.json", serializeState(state, null), {
|
|
5822
|
+
compact: true
|
|
5823
|
+
});
|
|
5514
5824
|
});
|
|
5515
5825
|
return { analysis, shared: false };
|
|
5516
5826
|
},
|
|
5517
5827
|
Math.max(0, 25e3 - (Date.now() - startedAt))
|
|
5518
5828
|
);
|
|
5519
5829
|
}
|
|
5830
|
+
function upstreamDriftNotice(drift) {
|
|
5831
|
+
const listed = drift.decisions.slice(0, 5).map((decision) => `${JSON.stringify(cleanText(decision.title))} (${cleanText(decision.id)})`);
|
|
5832
|
+
const more = drift.decisions.length - listed.length;
|
|
5833
|
+
return [
|
|
5834
|
+
`Mason: ${drift.decisions.length} decision(s) have anchors changed only by ${cleanText(drift.target)} since this branch's merge-base, not by this branch: ${listed.join(", ")}${more ? ` and ${more} more` : ""}.`,
|
|
5835
|
+
`They are not this checkout's work and do not block. Inspect them on ${cleanText(drift.target)}, where they are reported as usual; inspections recorded here would ride along with this branch.`
|
|
5836
|
+
].join("\n");
|
|
5837
|
+
}
|
|
5838
|
+
async function driftExcerpts(root, report, ids) {
|
|
5839
|
+
const unique = [...new Set(ids)].slice(0, 4);
|
|
5840
|
+
if (!unique.length) return null;
|
|
5841
|
+
const records = (await loadDecisionStore(root)).records;
|
|
5842
|
+
const sections = [];
|
|
5843
|
+
for (const id of unique) {
|
|
5844
|
+
const record = records.find((candidate) => candidate.id === id);
|
|
5845
|
+
const changed = /* @__PURE__ */ new Set();
|
|
5846
|
+
let title = record?.title ?? id;
|
|
5847
|
+
for (const finding of report.findings) {
|
|
5848
|
+
const evidence = finding.current?.evidence;
|
|
5849
|
+
if (finding.status === "resolved" || evidence?.kind !== "decision-anchor") continue;
|
|
5850
|
+
if (evidence.decisionId !== id) continue;
|
|
5851
|
+
title = evidence.title;
|
|
5852
|
+
for (const file of evidence.changedFiles) changed.add(file);
|
|
5853
|
+
}
|
|
5854
|
+
if (!record || !changed.size) continue;
|
|
5855
|
+
try {
|
|
5856
|
+
sections.push(formatDriftExcerpt(title, id, await driftExcerpt(root, record, [...changed])));
|
|
5857
|
+
} catch {
|
|
5858
|
+
}
|
|
5859
|
+
}
|
|
5860
|
+
return sections.join("\n") || null;
|
|
5861
|
+
}
|
|
5520
5862
|
async function guidanceAnalysis(ws, inputs) {
|
|
5521
5863
|
const checked = await checkDecisionAnchors({
|
|
5522
5864
|
root: ws.root,
|
|
@@ -5525,7 +5867,27 @@ async function guidanceAnalysis(ws, inputs) {
|
|
|
5525
5867
|
changesSinceDoc: /* @__PURE__ */ new Map(),
|
|
5526
5868
|
decisionsPresent: true
|
|
5527
5869
|
});
|
|
5528
|
-
const
|
|
5870
|
+
const attribution = checked.advisories.length ? await branchAttribution(ws.root).catch(() => null) : null;
|
|
5871
|
+
const own = new Set(
|
|
5872
|
+
checked.advisories.flatMap(
|
|
5873
|
+
(advisory) => advisory.evidence.kind === "decision-anchor" && advisory.evidence.changedFiles.some((file) => attribution?.branchFiles.has(file)) ? [advisory.evidence.decisionId] : []
|
|
5874
|
+
)
|
|
5875
|
+
);
|
|
5876
|
+
const upstream = /* @__PURE__ */ new Map();
|
|
5877
|
+
const advisories = checked.advisories.filter((advisory) => {
|
|
5878
|
+
const evidence = advisory.evidence;
|
|
5879
|
+
if (!attribution || evidence.kind !== "decision-anchor" || own.has(evidence.decisionId))
|
|
5880
|
+
return true;
|
|
5881
|
+
const entry = upstream.get(evidence.decisionId) ?? {
|
|
5882
|
+
id: evidence.decisionId,
|
|
5883
|
+
title: evidence.title,
|
|
5884
|
+
changedFiles: []
|
|
5885
|
+
};
|
|
5886
|
+
entry.changedFiles = [.../* @__PURE__ */ new Set([...entry.changedFiles, ...evidence.changedFiles])].sort();
|
|
5887
|
+
upstream.set(evidence.decisionId, entry);
|
|
5888
|
+
return false;
|
|
5889
|
+
});
|
|
5890
|
+
const findings = advisories.map((current) => ({
|
|
5529
5891
|
id: findingId(current),
|
|
5530
5892
|
original: current,
|
|
5531
5893
|
current,
|
|
@@ -5537,13 +5899,16 @@ async function guidanceAnalysis(ws, inputs) {
|
|
|
5537
5899
|
const prior = z16.object({ findings: z16.array(repairFindingSchema) }).parse(await readStoreJson(ws.root, before.guidance.path));
|
|
5538
5900
|
const currentIds = new Set(findings.map((finding) => finding.id));
|
|
5539
5901
|
const records = (await loadDecisionStore(ws.root)).records;
|
|
5902
|
+
const incomplete = new Set(checked.incompleteDecisions);
|
|
5903
|
+
const sharedSkip = checked.skipped.length > checked.incompleteDecisions.length;
|
|
5540
5904
|
for (const previous of prior.findings) {
|
|
5541
5905
|
if (currentIds.has(previous.id) || previous.original.evidence.kind !== "decision-anchor")
|
|
5542
5906
|
continue;
|
|
5543
5907
|
const evidence = previous.original.evidence;
|
|
5908
|
+
if (upstream.has(evidence.decisionId)) continue;
|
|
5544
5909
|
const record = records.find((record2) => record2.id === evidence.decisionId);
|
|
5545
5910
|
const capture = record && inputs.decisionEvidence[record.id]?.capture;
|
|
5546
|
-
const resolved = record?.status === "retired" || !!record && !!capture?.complete && capture.anchors.length > 0 && !
|
|
5911
|
+
const resolved = record?.status === "retired" || !!record && !!capture?.complete && capture.anchors.length > 0 && !sharedSkip && !incomplete.has(record.id);
|
|
5547
5912
|
findings.push({
|
|
5548
5913
|
id: previous.id,
|
|
5549
5914
|
original: previous.original,
|
|
@@ -5568,6 +5933,13 @@ async function guidanceAnalysis(ws, inputs) {
|
|
|
5568
5933
|
baselinePaths: [],
|
|
5569
5934
|
reportPath: "",
|
|
5570
5935
|
findings,
|
|
5936
|
+
...attribution && upstream.size ? {
|
|
5937
|
+
upstreamDrift: {
|
|
5938
|
+
target: attribution.target,
|
|
5939
|
+
mergeBase: attribution.mergeBase,
|
|
5940
|
+
decisions: [...upstream.values()]
|
|
5941
|
+
}
|
|
5942
|
+
} : {},
|
|
5571
5943
|
diagnostics: [
|
|
5572
5944
|
...checked.skipped.map((item) => item.reason),
|
|
5573
5945
|
...findings.filter((finding) => finding.status === "unverified").map((finding) => `Decision ${finding.id}: ${finding.reason}`)
|
|
@@ -5645,17 +6017,13 @@ async function runAutomation(dir, event, guidance) {
|
|
|
5645
6017
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5646
6018
|
const key = event.host && event.sessionId ? hash([event.host, event.sessionId]) : null;
|
|
5647
6019
|
const newSession = key !== null && !state.sessions[key];
|
|
6020
|
+
pruneSessions(
|
|
6021
|
+
state,
|
|
6022
|
+
new Date(now),
|
|
6023
|
+
newSession ? null : key,
|
|
6024
|
+
newSession ? SESSION_LIMIT - 1 : SESSION_LIMIT
|
|
6025
|
+
);
|
|
5648
6026
|
if (key && !state.sessions[key]) {
|
|
5649
|
-
const keys = Object.keys(state.sessions).sort(
|
|
5650
|
-
(a, b) => state.sessions[a].lastUsed.localeCompare(state.sessions[b].lastUsed)
|
|
5651
|
-
);
|
|
5652
|
-
const removable = keys.filter((k) => !Object.keys(state.sessions[k].pending).length);
|
|
5653
|
-
for (const expired of removable.slice(0, Math.max(0, keys.length - 31)))
|
|
5654
|
-
delete state.sessions[expired];
|
|
5655
|
-
if (Object.keys(state.sessions).length >= 128)
|
|
5656
|
-
throw new Error(
|
|
5657
|
-
"Too many sessions with unfinished tool calls; original evidence was retained."
|
|
5658
|
-
);
|
|
5659
6027
|
state.sessions[key] = {
|
|
5660
6028
|
host: event.host,
|
|
5661
6029
|
notifiedDiagnostics: [],
|
|
@@ -5675,8 +6043,7 @@ async function runAutomation(dir, event, guidance) {
|
|
|
5675
6043
|
count: (session.events[event.event]?.count ?? 0) + 1
|
|
5676
6044
|
};
|
|
5677
6045
|
if (event.event === "before_tool" && event.mutating && event.toolId) {
|
|
5678
|
-
|
|
5679
|
-
throw new Error("Too many unfinished tool calls to track pre-edit evidence.");
|
|
6046
|
+
boundPending(session.pending);
|
|
5680
6047
|
session.pending[event.toolId] = inputs.fingerprint;
|
|
5681
6048
|
}
|
|
5682
6049
|
if (event.event === "after_tool" && event.mutating) {
|
|
@@ -5702,11 +6069,23 @@ async function runAutomation(dir, event, guidance) {
|
|
|
5702
6069
|
...diagnostics
|
|
5703
6070
|
])
|
|
5704
6071
|
];
|
|
5705
|
-
const stopReview = session && event.host === "claude" && event.event === "task_end" && config.stopBlock === "drift" ? decisionStopReview(session, inputs, report, !!event.stopHookActive) : { reason: null, block: false };
|
|
6072
|
+
const stopReview = session && event.host === "claude" && event.event === "task_end" && config.stopBlock === "drift" ? decisionStopReview(session, inputs, report, !!event.stopHookActive) : { reason: null, block: false, decisions: [] };
|
|
6073
|
+
const excerpts = session ? await driftExcerpts(ws.root, report, [
|
|
6074
|
+
...stopReview.decisions,
|
|
6075
|
+
...[...selected, ...feedback].flatMap((finding) => {
|
|
6076
|
+
const evidence = (finding.current ?? finding.original).evidence;
|
|
6077
|
+
return evidence.kind === "decision-anchor" ? [evidence.decisionId] : [];
|
|
6078
|
+
})
|
|
6079
|
+
]) : null;
|
|
6080
|
+
const upstream = report.upstreamDrift ? upstreamDriftNotice(report.upstreamDrift) : null;
|
|
6081
|
+
const upstreamNotice = session && upstream && session.upstreamNotice !== hash(upstream) ? upstream : null;
|
|
6082
|
+
if (session) session.upstreamNotice = upstream ? hash(upstream) : void 0;
|
|
5706
6083
|
const message = !session ? summarize(report) : [
|
|
5707
6084
|
stopReview.reason,
|
|
5708
6085
|
completionSummary(selected, report.reportPath),
|
|
5709
6086
|
completionSummary(feedback, report.reportPath),
|
|
6087
|
+
excerpts,
|
|
6088
|
+
upstreamNotice,
|
|
5710
6089
|
...diagnostics.length ? [
|
|
5711
6090
|
`Mason: verification limited. ${diagnostics.slice(0, 2).map(cleanText).join(" ")} Evidence: ${report.reportPath}.`
|
|
5712
6091
|
] : []
|
|
@@ -5724,7 +6103,10 @@ async function runAutomation(dir, event, guidance) {
|
|
|
5724
6103
|
state.latest = report.reportPath;
|
|
5725
6104
|
}
|
|
5726
6105
|
if (persistReport) await writeStoreJson(ws.root, report.reportPath, report);
|
|
5727
|
-
|
|
6106
|
+
if (session && event.event === "task_end") clearPendingTools(session);
|
|
6107
|
+
await writeStoreJson(ws.root, ws.directory + "/state.json", serializeState(state, key), {
|
|
6108
|
+
compact: true
|
|
6109
|
+
});
|
|
5728
6110
|
return {
|
|
5729
6111
|
report,
|
|
5730
6112
|
message,
|
|
@@ -5762,7 +6144,7 @@ async function automationStatus(dir) {
|
|
|
5762
6144
|
const knowledge = await pendingKnowledge2(ws.root);
|
|
5763
6145
|
const execution = await executionStatus(ws.root, ws.directory);
|
|
5764
6146
|
const notifications = await failureNotifications(ws.root, ws.directory);
|
|
5765
|
-
const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
|
|
6147
|
+
const raw = await readStoreJson(ws.root, ws.directory + "/state.json", STATE_READ_MAX_BYTES);
|
|
5766
6148
|
if (raw === null)
|
|
5767
6149
|
return {
|
|
5768
6150
|
version: 1,
|
|
@@ -5790,7 +6172,10 @@ async function automationStatus(dir) {
|
|
|
5790
6172
|
host.sessions++;
|
|
5791
6173
|
host.observedEvents = [.../* @__PURE__ */ new Set([...host.observedEvents, ...Object.keys(session.events)])];
|
|
5792
6174
|
}
|
|
5793
|
-
const unfinished = ["failed", "unknown"
|
|
6175
|
+
const unfinished = ["failed", "unknown"].includes(execution.status);
|
|
6176
|
+
const pendingSessions = Object.values(state.sessions).filter(
|
|
6177
|
+
(session) => Object.keys(session.pending).length
|
|
6178
|
+
);
|
|
5794
6179
|
return {
|
|
5795
6180
|
version: 1,
|
|
5796
6181
|
stopBlock: config.stopBlock,
|
|
@@ -5803,6 +6188,12 @@ async function automationStatus(dir) {
|
|
|
5803
6188
|
auditReportPath: state.latest,
|
|
5804
6189
|
verificationStatus: unfinished ? "unavailable" : latest?.status ?? "unavailable",
|
|
5805
6190
|
hosts: hosts2,
|
|
6191
|
+
sessions: Object.keys(state.sessions).length,
|
|
6192
|
+
unfinishedToolCalls: pendingSessions.reduce(
|
|
6193
|
+
(total, session) => total + Object.keys(session.pending).length,
|
|
6194
|
+
0
|
|
6195
|
+
),
|
|
6196
|
+
oldestUnfinishedSessionUsedAt: pendingSessions.map((session) => session.lastUsed).sort()[0] ?? null,
|
|
5806
6197
|
execution,
|
|
5807
6198
|
notifications,
|
|
5808
6199
|
knowledge,
|
|
@@ -5815,6 +6206,8 @@ var init_runtime = __esm({
|
|
|
5815
6206
|
"src/automation/runtime.ts"() {
|
|
5816
6207
|
"use strict";
|
|
5817
6208
|
init_decisions();
|
|
6209
|
+
init_excerpts();
|
|
6210
|
+
init_attribution();
|
|
5818
6211
|
init_decision_anchor();
|
|
5819
6212
|
init_config();
|
|
5820
6213
|
init_stop_review();
|
|
@@ -5827,6 +6220,7 @@ var init_runtime = __esm({
|
|
|
5827
6220
|
init_types();
|
|
5828
6221
|
init_storage();
|
|
5829
6222
|
init_evidence();
|
|
6223
|
+
init_pending_tools();
|
|
5830
6224
|
init_store();
|
|
5831
6225
|
SCOPE = "Documentation audit evidence only. Hook receipts show observed events, not complete interception. Resolved findings no longer fail their checks or have a recorded assessment covering current evidence. Repair and review only within the user's task authorization.";
|
|
5832
6226
|
priority = { resolved: 0, "review-required": 1, unresolved: 2, unverified: 3 };
|
|
@@ -5930,7 +6324,7 @@ import { z as z17 } from "zod";
|
|
|
5930
6324
|
import fs18 from "fs/promises";
|
|
5931
6325
|
import path22 from "path";
|
|
5932
6326
|
import os3 from "os";
|
|
5933
|
-
import { createHash as
|
|
6327
|
+
import { createHash as createHash7 } from "crypto";
|
|
5934
6328
|
async function exists(p) {
|
|
5935
6329
|
try {
|
|
5936
6330
|
await fs18.access(p);
|
|
@@ -6022,10 +6416,10 @@ async function runHook(stdinText, env = {}) {
|
|
|
6022
6416
|
const stateDir = env.stateDir ?? os3.tmpdir();
|
|
6023
6417
|
const stateFile = path22.join(
|
|
6024
6418
|
stateDir,
|
|
6025
|
-
`mason-hook-${
|
|
6419
|
+
`mason-hook-${createHash7("sha256").update(root).digest("hex").slice(0, 12)}-${stateKey(input)}.json`
|
|
6026
6420
|
);
|
|
6027
6421
|
const injected = await loadInjected(stateFile);
|
|
6028
|
-
const recordKey = (record) => `${record.id}:${
|
|
6422
|
+
const recordKey = (record) => `${record.id}:${createHash7("sha256").update(JSON.stringify(record)).digest("hex")}`;
|
|
6029
6423
|
const previouslyInjected = (record) => [...injected].some((key) => key === record.id || key.startsWith(`${record.id}:`));
|
|
6030
6424
|
const fresh = records.filter(
|
|
6031
6425
|
(r) => !injected.has(recordKey(r)) && (previouslyInjected(r) || r.status === "active" && anchorsCover(r, relPath))
|
|
@@ -6175,8 +6569,8 @@ __export(launcher_exports, {
|
|
|
6175
6569
|
prepareLaunch: () => prepareLaunch
|
|
6176
6570
|
});
|
|
6177
6571
|
import { z as z19 } from "zod";
|
|
6178
|
-
import { execFile as
|
|
6179
|
-
import { promisify as
|
|
6572
|
+
import { execFile as execFile3 } from "child_process";
|
|
6573
|
+
import { promisify as promisify3 } from "util";
|
|
6180
6574
|
import fs20 from "fs/promises";
|
|
6181
6575
|
function globalCommand(args2, platform = process.platform) {
|
|
6182
6576
|
return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args2] } : { command: "mason", args: args2 };
|
|
@@ -6184,7 +6578,7 @@ function globalCommand(args2, platform = process.platform) {
|
|
|
6184
6578
|
async function installedCommand() {
|
|
6185
6579
|
const invocation = globalCommand(["internal-integration-version"]);
|
|
6186
6580
|
try {
|
|
6187
|
-
const { stdout } = await
|
|
6581
|
+
const { stdout } = await promisify3(execFile3)(invocation.command, invocation.args, {
|
|
6188
6582
|
timeout: 5e3,
|
|
6189
6583
|
maxBuffer: 8192,
|
|
6190
6584
|
windowsHide: true
|
|
@@ -6230,7 +6624,7 @@ var init_launcher = __esm({
|
|
|
6230
6624
|
"use strict";
|
|
6231
6625
|
init_evidence();
|
|
6232
6626
|
init_model();
|
|
6233
|
-
executingVersion = () => true ? "0.
|
|
6627
|
+
executingVersion = () => true ? "0.23.1" : "development";
|
|
6234
6628
|
mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
|
|
6235
6629
|
hookCommand = (host) => `mason --setup-host ${host}`;
|
|
6236
6630
|
}
|
|
@@ -6431,10 +6825,19 @@ async function runAutomationHook(host, stdin, options = {}) {
|
|
|
6431
6825
|
if (!message) return null;
|
|
6432
6826
|
return {
|
|
6433
6827
|
systemMessage: message,
|
|
6434
|
-
...[
|
|
6828
|
+
...[
|
|
6829
|
+
"SessionStart",
|
|
6830
|
+
"UserPromptSubmit",
|
|
6831
|
+
"PreToolUse",
|
|
6832
|
+
"PostToolUse",
|
|
6833
|
+
"PostToolUseFailure"
|
|
6834
|
+
].includes(name) ? { hookSpecificOutput: { hookEventName: name, additionalContext: message } } : {}
|
|
6435
6835
|
};
|
|
6436
6836
|
}
|
|
6437
6837
|
}
|
|
6838
|
+
function hookEvents(host) {
|
|
6839
|
+
return [...HOOK_EVENTS, ...OPTIONAL_HOOK_EVENTS[host]];
|
|
6840
|
+
}
|
|
6438
6841
|
function managedHookCommand(host, platform = process.platform, legacy = false) {
|
|
6439
6842
|
const command2 = `mason --setup-host ${host}${legacy ? " auto" : ""} hook --host ${host}`;
|
|
6440
6843
|
return platform === "win32" ? `cmd.exe /d /s /c "where mason >nul 2>nul & if errorlevel 1 (exit /b 0) else (${command2})"` : `if command -v mason >/dev/null 2>&1; then ${command2}; fi`;
|
|
@@ -6467,11 +6870,11 @@ function hookConfig(host, command2 = "npx --no-install --package mason-context m
|
|
|
6467
6870
|
};
|
|
6468
6871
|
return {
|
|
6469
6872
|
hooks: Object.fromEntries(
|
|
6470
|
-
|
|
6873
|
+
hookEvents(host).map((name) => [
|
|
6471
6874
|
name,
|
|
6472
6875
|
[
|
|
6473
6876
|
{
|
|
6474
|
-
...["PreToolUse", "PostToolUse"].includes(name) ? { matcher: ".*" } : {},
|
|
6877
|
+
...["PreToolUse", "PostToolUse", "PostToolUseFailure"].includes(name) ? { matcher: ".*" } : {},
|
|
6475
6878
|
hooks: [{ ...handler }]
|
|
6476
6879
|
}
|
|
6477
6880
|
]
|
|
@@ -6479,7 +6882,7 @@ function hookConfig(host, command2 = "npx --no-install --package mason-context m
|
|
|
6479
6882
|
)
|
|
6480
6883
|
};
|
|
6481
6884
|
}
|
|
6482
|
-
var inputSchema, lifecycle, HOOK_EVENTS;
|
|
6885
|
+
var inputSchema, lifecycle, HOOK_EVENTS, OPTIONAL_HOOK_EVENTS;
|
|
6483
6886
|
var init_adapters = __esm({
|
|
6484
6887
|
"src/automation/adapters.ts"() {
|
|
6485
6888
|
"use strict";
|
|
@@ -6497,6 +6900,7 @@ var init_adapters = __esm({
|
|
|
6497
6900
|
"UserPromptSubmit",
|
|
6498
6901
|
"PreToolUse",
|
|
6499
6902
|
"PostToolUse",
|
|
6903
|
+
"PostToolUseFailure",
|
|
6500
6904
|
"Stop"
|
|
6501
6905
|
]),
|
|
6502
6906
|
tool_name: z21.string().optional(),
|
|
@@ -6511,6 +6915,8 @@ var init_adapters = __esm({
|
|
|
6511
6915
|
UserPromptSubmit: "turn_start",
|
|
6512
6916
|
PreToolUse: "before_tool",
|
|
6513
6917
|
PostToolUse: "after_tool",
|
|
6918
|
+
// Claude reports failed calls separately; they still end the paired tool capture.
|
|
6919
|
+
PostToolUseFailure: "after_tool",
|
|
6514
6920
|
Stop: "task_end"
|
|
6515
6921
|
};
|
|
6516
6922
|
HOOK_EVENTS = [
|
|
@@ -6520,6 +6926,10 @@ var init_adapters = __esm({
|
|
|
6520
6926
|
"PostToolUse",
|
|
6521
6927
|
"Stop"
|
|
6522
6928
|
];
|
|
6929
|
+
OPTIONAL_HOOK_EVENTS = {
|
|
6930
|
+
claude: ["PostToolUseFailure"],
|
|
6931
|
+
codex: []
|
|
6932
|
+
};
|
|
6523
6933
|
}
|
|
6524
6934
|
});
|
|
6525
6935
|
|
|
@@ -6742,16 +7152,126 @@ Existing maps remain readable for legacy projects. Managed map creation has been
|
|
|
6742
7152
|
}
|
|
6743
7153
|
});
|
|
6744
7154
|
|
|
7155
|
+
// src/decisions/merge.ts
|
|
7156
|
+
var merge_exports = {};
|
|
7157
|
+
__export(merge_exports, {
|
|
7158
|
+
MERGE_ATTRIBUTE: () => MERGE_ATTRIBUTE,
|
|
7159
|
+
MERGE_DRIVER_COMMAND: () => MERGE_DRIVER_COMMAND,
|
|
7160
|
+
MERGE_DRIVER_NAME: () => MERGE_DRIVER_NAME,
|
|
7161
|
+
mergeDecisionRecords: () => mergeDecisionRecords,
|
|
7162
|
+
runDecisionMerge: () => runDecisionMerge
|
|
7163
|
+
});
|
|
7164
|
+
import fs23 from "fs/promises";
|
|
7165
|
+
function appendOnly(base, ours, theirs) {
|
|
7166
|
+
if (base === void 0 && ours === void 0 && theirs === void 0) return void 0;
|
|
7167
|
+
const lists = [base ?? [], ours ?? [], theirs ?? []];
|
|
7168
|
+
if (!lists.every(Array.isArray)) return null;
|
|
7169
|
+
const [common, current, incoming] = lists;
|
|
7170
|
+
const extendsCommon = (list) => list.length >= common.length && common.every((entry, i) => canonical(entry) === canonical(list[i]));
|
|
7171
|
+
if (!extendsCommon(current) || !extendsCommon(incoming)) return null;
|
|
7172
|
+
const added = /* @__PURE__ */ new Map();
|
|
7173
|
+
for (const entry of [...current.slice(common.length), ...incoming.slice(common.length)])
|
|
7174
|
+
added.set(canonical(entry), entry);
|
|
7175
|
+
const at = (entry) => String(entry.at ?? "");
|
|
7176
|
+
const appended = [...added].sort(
|
|
7177
|
+
([a, x], [b, y]) => at(x) < at(y) ? -1 : at(x) > at(y) ? 1 : a < b ? -1 : a > b ? 1 : 0
|
|
7178
|
+
);
|
|
7179
|
+
return [...common, ...appended.map(([, entry]) => entry)];
|
|
7180
|
+
}
|
|
7181
|
+
function mergeDecisionRecords(baseText, oursText, theirsText, expectedId) {
|
|
7182
|
+
const conflict = (reason) => ({ status: "conflict", reason });
|
|
7183
|
+
let parsed;
|
|
7184
|
+
try {
|
|
7185
|
+
parsed = [baseText, oursText, theirsText].map((text2) => JSON.parse(text2));
|
|
7186
|
+
} catch {
|
|
7187
|
+
return conflict("a version is not valid JSON");
|
|
7188
|
+
}
|
|
7189
|
+
if (!parsed.every((value) => value && typeof value === "object" && !Array.isArray(value)))
|
|
7190
|
+
return conflict("a version is not a JSON object");
|
|
7191
|
+
const [base, ours, theirs] = parsed;
|
|
7192
|
+
for (const [label, side] of [
|
|
7193
|
+
["current", ours],
|
|
7194
|
+
["incoming", theirs]
|
|
7195
|
+
])
|
|
7196
|
+
if (!decisionSchema.safeParse(side).success)
|
|
7197
|
+
return conflict(`the ${label} version is not a valid decision record`);
|
|
7198
|
+
const merged = {};
|
|
7199
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(ours), ...Object.keys(theirs), ...Object.keys(base)])) {
|
|
7200
|
+
if (APPEND_ONLY.has(key)) {
|
|
7201
|
+
const list = appendOnly(base[key], ours[key], theirs[key]);
|
|
7202
|
+
if (list === null) return conflict(`${key} on one side does not extend the common version`);
|
|
7203
|
+
if (list !== void 0) merged[key] = list;
|
|
7204
|
+
continue;
|
|
7205
|
+
}
|
|
7206
|
+
const [b, o, t] = [base[key], ours[key], theirs[key]].map(canonical);
|
|
7207
|
+
if (o !== t && o !== b && t !== b) return conflict(`both sides changed ${key}`);
|
|
7208
|
+
const value = o === b ? theirs[key] : ours[key];
|
|
7209
|
+
if (value !== void 0) merged[key] = value;
|
|
7210
|
+
}
|
|
7211
|
+
const result = decisionSchema.safeParse(merged);
|
|
7212
|
+
if (!result.success)
|
|
7213
|
+
return conflict(`the combined record is invalid: ${result.error.issues[0].message}`);
|
|
7214
|
+
if (expectedId !== void 0 && result.data.id !== expectedId)
|
|
7215
|
+
return conflict("the record id does not match its file name");
|
|
7216
|
+
return { status: "merged", text: JSON.stringify(result.data, null, 2) + "\n" };
|
|
7217
|
+
}
|
|
7218
|
+
async function runDecisionMerge(args2) {
|
|
7219
|
+
const [basePath, oursPath, theirsPath, , pathname] = args2;
|
|
7220
|
+
if (!basePath || !oursPath || !theirsPath || args2.length > 5)
|
|
7221
|
+
throw new Error(
|
|
7222
|
+
"Usage: mason merge-decision <base> <current> <incoming> [marker-size] [path]. Git runs this through the merge driver that mason setup configures."
|
|
7223
|
+
);
|
|
7224
|
+
const read = async (file) => {
|
|
7225
|
+
const text2 = await readBoundedFile(file, 10 * 1024 * 1024);
|
|
7226
|
+
if (text2 === null) throw new Error(`${file} is not a regular file or exceeds 10 MiB`);
|
|
7227
|
+
return text2;
|
|
7228
|
+
};
|
|
7229
|
+
const id = pathname && /(?:^|\/)\.mason\/decisions\/([^/]+)\.json$/.exec(pathname)?.[1];
|
|
7230
|
+
const result = mergeDecisionRecords(
|
|
7231
|
+
await read(basePath),
|
|
7232
|
+
await read(oursPath),
|
|
7233
|
+
await read(theirsPath),
|
|
7234
|
+
id || void 0
|
|
7235
|
+
);
|
|
7236
|
+
if (result.status === "conflict") {
|
|
7237
|
+
console.error(
|
|
7238
|
+
`Mason: ${pathname || "decision record"}: ${result.reason}; leaving it to Git's text merge.`
|
|
7239
|
+
);
|
|
7240
|
+
return 1;
|
|
7241
|
+
}
|
|
7242
|
+
await fs23.writeFile(oursPath, result.text, "utf8");
|
|
7243
|
+
return 0;
|
|
7244
|
+
}
|
|
7245
|
+
var MERGE_DRIVER_NAME, MERGE_ATTRIBUTE, MERGE_DRIVER_COMMAND, APPEND_ONLY, canonical;
|
|
7246
|
+
var init_merge = __esm({
|
|
7247
|
+
"src/decisions/merge.ts"() {
|
|
7248
|
+
"use strict";
|
|
7249
|
+
init_files();
|
|
7250
|
+
init_provenance();
|
|
7251
|
+
MERGE_DRIVER_NAME = "mason-decision";
|
|
7252
|
+
MERGE_ATTRIBUTE = `.mason/decisions/*.json merge=${MERGE_DRIVER_NAME}`;
|
|
7253
|
+
MERGE_DRIVER_COMMAND = "mason merge-decision %O %A %B %L %P || git merge-file --marker-size=%L -L ours -L base -L theirs %A %O %B";
|
|
7254
|
+
APPEND_ONLY = /* @__PURE__ */ new Set(["history", "inspections"]);
|
|
7255
|
+
canonical = (value) => JSON.stringify(
|
|
7256
|
+
value,
|
|
7257
|
+
(_, item) => item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) : item
|
|
7258
|
+
);
|
|
7259
|
+
}
|
|
7260
|
+
});
|
|
7261
|
+
|
|
6745
7262
|
// src/setup/config.ts
|
|
6746
7263
|
var config_exports = {};
|
|
6747
7264
|
__export(config_exports, {
|
|
6748
7265
|
ancillaryEdits: () => ancillaryEdits,
|
|
7266
|
+
configureDecisionMerge: () => configureDecisionMerge,
|
|
7267
|
+
decisionMergeStatus: () => decisionMergeStatus,
|
|
6749
7268
|
hookEdits: () => hookEdits,
|
|
6750
7269
|
inspectHostConfig: () => inspectHostConfig,
|
|
6751
7270
|
inspectIntegration: () => inspectIntegration,
|
|
6752
7271
|
instructionEdits: () => instructionEdits,
|
|
6753
7272
|
mcpEdit: () => mcpEdit,
|
|
6754
|
-
mcpPath: () => mcpPath
|
|
7273
|
+
mcpPath: () => mcpPath,
|
|
7274
|
+
mergeDriverSummary: () => mergeDriverSummary
|
|
6755
7275
|
});
|
|
6756
7276
|
import { isDeepStrictEqual } from "util";
|
|
6757
7277
|
import { parse as parse2, stringify } from "smol-toml";
|
|
@@ -6873,6 +7393,7 @@ async function hookEdits(root, host) {
|
|
|
6873
7393
|
}
|
|
6874
7394
|
async function ancillaryEdits(root) {
|
|
6875
7395
|
const ignore = await readText(root, ".gitignore");
|
|
7396
|
+
const attributes = await readText(root, ".gitattributes");
|
|
6876
7397
|
const rules = [
|
|
6877
7398
|
"!/.mason/",
|
|
6878
7399
|
"/.mason/*",
|
|
@@ -6890,9 +7411,45 @@ async function ancillaryEdits(root) {
|
|
|
6890
7411
|
path: ".gitignore",
|
|
6891
7412
|
before: ignore,
|
|
6892
7413
|
after: managedBlock(ignore ?? "", "# mason:ignore:start", "# mason:ignore:end", rules)
|
|
7414
|
+
},
|
|
7415
|
+
// Unconfigured checkouts keep Git's text merge; mason setup enables the driver locally.
|
|
7416
|
+
{
|
|
7417
|
+
path: ".gitattributes",
|
|
7418
|
+
before: attributes,
|
|
7419
|
+
after: managedBlock(
|
|
7420
|
+
attributes ?? "",
|
|
7421
|
+
"# mason:merge:start",
|
|
7422
|
+
"# mason:merge:end",
|
|
7423
|
+
MERGE_ATTRIBUTE
|
|
7424
|
+
)
|
|
6893
7425
|
}
|
|
6894
7426
|
];
|
|
6895
7427
|
}
|
|
7428
|
+
async function decisionMergeStatus(root) {
|
|
7429
|
+
const { git: git4 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
|
|
7430
|
+
const attribute = (await git4(root, "check-attr", "merge", "--", ".mason/decisions/record.json")).trim();
|
|
7431
|
+
const driver = await git4(root, "config", "--get", `merge.${MERGE_DRIVER_NAME}.driver`).then(
|
|
7432
|
+
(value) => value.trim(),
|
|
7433
|
+
() => null
|
|
7434
|
+
);
|
|
7435
|
+
return {
|
|
7436
|
+
attribute: attribute.endsWith(`: merge: ${MERGE_DRIVER_NAME}`),
|
|
7437
|
+
driver: driver === null ? "missing" : driver === MERGE_DRIVER_COMMAND ? "configured" : "custom"
|
|
7438
|
+
};
|
|
7439
|
+
}
|
|
7440
|
+
function mergeDriverSummary(state) {
|
|
7441
|
+
const driver = state.driver === "configured" ? "Decision record merge driver: configured." : state.driver === "custom" ? "Decision record merge driver: customized in Git config; Mason left it unchanged." : "Decision record merge driver: not configured; Git text-merges decision records. Run mason setup to enable it.";
|
|
7442
|
+
return state.attribute ? driver : driver + " The .gitattributes entry is missing; a maintainer can add it with mason init.";
|
|
7443
|
+
}
|
|
7444
|
+
async function configureDecisionMerge(root) {
|
|
7445
|
+
const status = await decisionMergeStatus(root);
|
|
7446
|
+
if (status.driver !== "missing") return status;
|
|
7447
|
+
const { git: git4 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
|
|
7448
|
+
const section = `merge.${MERGE_DRIVER_NAME}`;
|
|
7449
|
+
await git4(root, "config", "--local", `${section}.name`, "Mason decision record merge");
|
|
7450
|
+
await git4(root, "config", "--local", `${section}.driver`, MERGE_DRIVER_COMMAND);
|
|
7451
|
+
return { ...status, driver: "configured" };
|
|
7452
|
+
}
|
|
6896
7453
|
async function inspectHostConfig(root, host, plannedMcp) {
|
|
6897
7454
|
const text2 = plannedMcp ?? await readText(root, mcpPath(host));
|
|
6898
7455
|
const config = host === "codex" ? parse2(text2 ?? "") : object.parse(JSON.parse(text2 ?? "{}"));
|
|
@@ -6943,7 +7500,10 @@ async function inspectIntegration(root, host) {
|
|
|
6943
7500
|
])
|
|
6944
7501
|
);
|
|
6945
7502
|
const mcpCompatible = isDeepStrictEqual({ command: config.mcp?.command, args: config.mcp?.args }, mcpCommand(host)) && (config.mcp?.type === void 0 || config.mcp.type === "stdio");
|
|
6946
|
-
const
|
|
7503
|
+
const required = new Set(HOOK_EVENTS);
|
|
7504
|
+
const hooksCompatible = Object.entries(expectedHooks).every(
|
|
7505
|
+
([event, groups]) => isDeepStrictEqual(normalizedHooks[event], groups) || !required.has(event) && !normalizedHooks[event]?.length
|
|
7506
|
+
);
|
|
6947
7507
|
return {
|
|
6948
7508
|
...config,
|
|
6949
7509
|
instructions,
|
|
@@ -6965,6 +7525,7 @@ var init_config2 = __esm({
|
|
|
6965
7525
|
init_install();
|
|
6966
7526
|
init_files2();
|
|
6967
7527
|
init_launcher();
|
|
7528
|
+
init_merge();
|
|
6968
7529
|
object = z24.record(z24.unknown());
|
|
6969
7530
|
mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
|
|
6970
7531
|
TOML_START = "# mason:mcp:start";
|
|
@@ -7011,7 +7572,7 @@ async function planAutomationInstall(root, host, command2) {
|
|
|
7011
7572
|
const pending = ownershipText === null ? [] : ownershipSchema.parse(JSON.parse(ownershipText)).files[file]?.hookCommands ?? [];
|
|
7012
7573
|
const owned = /* @__PURE__ */ new Set([previous, ...pending, newCommand, ...knownManagedHookCommands(host)]);
|
|
7013
7574
|
const hooks = existing.hooks ?? {};
|
|
7014
|
-
for (const event of
|
|
7575
|
+
for (const event of hookEvents(host)) {
|
|
7015
7576
|
hooks[event] = (hooks[event] ?? []).map((group) => ({
|
|
7016
7577
|
...group,
|
|
7017
7578
|
hooks: group.hooks.filter(
|
|
@@ -7045,7 +7606,7 @@ async function installLocked(root, host, command2) {
|
|
|
7045
7606
|
configPath: file,
|
|
7046
7607
|
status: "configured",
|
|
7047
7608
|
command: newCommand,
|
|
7048
|
-
events:
|
|
7609
|
+
events: hookEvents(host),
|
|
7049
7610
|
next: host === "codex" ? "Review/trust these hooks using Codex /hooks and start a new session. mason status reports observed events separately from configuration." : "Start a new Claude Code session. mason status reports observed events separately from configuration.",
|
|
7050
7611
|
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."
|
|
7051
7612
|
};
|
|
@@ -7060,7 +7621,7 @@ async function installedAutomation(dir) {
|
|
|
7060
7621
|
const current = automationConfigSchema.parse(
|
|
7061
7622
|
await readStoreJson(ws.root, configPath3(host)) ?? {}
|
|
7062
7623
|
);
|
|
7063
|
-
const configuredEvents =
|
|
7624
|
+
const configuredEvents = hookEvents(host).filter(
|
|
7064
7625
|
(event) => current.hooks?.[event]?.some(
|
|
7065
7626
|
(group) => group.hooks.some(
|
|
7066
7627
|
(handler) => handler.type === "command" && handler.command === expected.command
|
|
@@ -7070,7 +7631,7 @@ async function installedAutomation(dir) {
|
|
|
7070
7631
|
result[host] = {
|
|
7071
7632
|
configPath: configPath3(host),
|
|
7072
7633
|
configuredEvents,
|
|
7073
|
-
status: current.disableAllHooks === true ? "disabled" :
|
|
7634
|
+
status: current.disableAllHooks === true ? "disabled" : HOOK_EVENTS.every((event) => configuredEvents.includes(event)) ? "configured" : "incomplete",
|
|
7074
7635
|
runtime: "Host version, trust, policy, and tool coverage still determine execution; inspect observed events."
|
|
7075
7636
|
};
|
|
7076
7637
|
}
|
|
@@ -7106,7 +7667,7 @@ __export(teardown_exports, {
|
|
|
7106
7667
|
teardownProject: () => teardownProject
|
|
7107
7668
|
});
|
|
7108
7669
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
7109
|
-
import
|
|
7670
|
+
import fs24 from "fs/promises";
|
|
7110
7671
|
import path25 from "path";
|
|
7111
7672
|
import { parse as parse3, stringify as stringify2 } from "smol-toml";
|
|
7112
7673
|
import fg8 from "fast-glob";
|
|
@@ -7132,7 +7693,7 @@ async function planProjectTeardown(root, selected) {
|
|
|
7132
7693
|
const ownership = ownershipBefore === null ? { version: 1, files: {} } : ownershipSchema.parse(JSON.parse(ownershipBefore));
|
|
7133
7694
|
const setupBefore = await read(SETUP_PATH);
|
|
7134
7695
|
const setup = setupBefore === null ? null : setupSchema.parse(JSON.parse(setupBefore));
|
|
7135
|
-
const linked = (await workspace(root)).gitDir !== await
|
|
7696
|
+
const linked = (await workspace(root)).gitDir !== await fs24.realpath(
|
|
7136
7697
|
path25.resolve(root, (await git3(root, "rev-parse", "--git-common-dir")).trim())
|
|
7137
7698
|
);
|
|
7138
7699
|
const inactiveBefore = linked && !setup ? await read(INACTIVE_HOSTS_PATH) : null;
|
|
@@ -7282,9 +7843,9 @@ async function planProjectTeardown(root, selected) {
|
|
|
7282
7843
|
const block = blockSpan(after, start, end);
|
|
7283
7844
|
if (!block) continue;
|
|
7284
7845
|
const pointer = "Mason project knowledge and shared project instructions:\n@" + (file.startsWith(".claude/") ? "../AGENTS.md" : "AGENTS.md");
|
|
7285
|
-
const
|
|
7846
|
+
const canonical2 = [CLAUDE_MD_SECTION, managedBlock("", start, end, pointer).trimEnd()];
|
|
7286
7847
|
const expected = ownership.files[file]?.blocks[start];
|
|
7287
|
-
if (expected ? hash(block.text) !== expected : !
|
|
7848
|
+
if (expected ? hash(block.text) !== expected : !canonical2.includes(block.text.replace(/\r\n/g, "\n"))) {
|
|
7288
7849
|
throw new Error(
|
|
7289
7850
|
"The Mason instruction block was edited or is unrecognized; retained for manual cleanup."
|
|
7290
7851
|
);
|
|
@@ -7376,7 +7937,7 @@ async function planLocalTeardown(root, selected) {
|
|
|
7376
7937
|
if (setup) edits.push({ path: SETUP_PATH, before: setupBefore, after: json(setup) });
|
|
7377
7938
|
if (automation)
|
|
7378
7939
|
edits.push({ path: AUTOMATION_PATH, before: automationBefore, after: json(automation) });
|
|
7379
|
-
const linked = (await workspace(root)).gitDir !== await
|
|
7940
|
+
const linked = (await workspace(root)).gitDir !== await fs24.realpath(
|
|
7380
7941
|
path25.resolve(root, (await git3(root, "rev-parse", "--git-common-dir")).trim())
|
|
7381
7942
|
);
|
|
7382
7943
|
if (linked && !setup) {
|
|
@@ -7455,7 +8016,7 @@ function summarizeTeardown(result) {
|
|
|
7455
8016
|
),
|
|
7456
8017
|
...result.diagnostics.map((item) => ` Retained ${item.path}: ${item.message}`),
|
|
7457
8018
|
...!result.changes.length ? ["No changes."] : [],
|
|
7458
|
-
"Shared knowledge, repair evidence, ignore rules and the installed Mason command are retained.",
|
|
8019
|
+
"Shared knowledge, repair evidence, ignore rules, the decision merge driver and the installed Mason command are retained.",
|
|
7459
8020
|
...result.scope === "project" && result.sharedInstructionsRetained ? [
|
|
7460
8021
|
"Shared instruction cleanup is deferred while another host or incomplete removal needs inspection."
|
|
7461
8022
|
] : [],
|
|
@@ -7493,6 +8054,7 @@ async function setupStatus(dir) {
|
|
|
7493
8054
|
const ws = await workspace(dir);
|
|
7494
8055
|
const activation = await effectiveSetup(ws.root);
|
|
7495
8056
|
const setup = activation.setup;
|
|
8057
|
+
const mergeDriver = await decisionMergeStatus(ws.root);
|
|
7496
8058
|
if (!setup || !Object.keys(setup.hosts).length) {
|
|
7497
8059
|
const partial = await Promise.all(
|
|
7498
8060
|
["codex", "claude"].map(async (host) => ({
|
|
@@ -7523,6 +8085,7 @@ async function setupStatus(dir) {
|
|
|
7523
8085
|
status: pending.length ? "incomplete" : "not-configured",
|
|
7524
8086
|
hosts: {},
|
|
7525
8087
|
configuration,
|
|
8088
|
+
mergeDriver,
|
|
7526
8089
|
next: pending.length ? "Setup did not finish. Rerun mason setup --host " + pending[0] + " to resume local activation; use mason init if shared configuration needs updating." : "Project hooks are inactive. Run mason setup --host codex or --host claude for compatible shared configuration; a maintainer uses mason init to prepare missing or incompatible configuration."
|
|
7527
8090
|
};
|
|
7528
8091
|
}
|
|
@@ -7597,6 +8160,7 @@ async function setupStatus(dir) {
|
|
|
7597
8160
|
hosts: hosts2,
|
|
7598
8161
|
activationRoot: activation.root,
|
|
7599
8162
|
decisionRecords: decisions.records.length,
|
|
8163
|
+
mergeDriver,
|
|
7600
8164
|
diagnostics: decisions.diagnostics,
|
|
7601
8165
|
scope: "Activation receipts record observed use after setup, not complete interception, correct repairs, or measured usefulness. Host trust and higher-priority settings may prevent execution. Receipts are local to this worktree and branch."
|
|
7602
8166
|
};
|
|
@@ -7619,6 +8183,8 @@ function summarizeActivation(status) {
|
|
|
7619
8183
|
lines.push(...state.updates.map((message) => " Optional: " + message));
|
|
7620
8184
|
}
|
|
7621
8185
|
if ("decisionRecords" in status) lines.push(`Decision records: ${status.decisionRecords}.`);
|
|
8186
|
+
if (status.mergeDriver.attribute || status.mergeDriver.driver !== "missing")
|
|
8187
|
+
lines.push(mergeDriverSummary(status.mergeDriver));
|
|
7622
8188
|
if (status.next) lines.push(status.next);
|
|
7623
8189
|
return lines.join("\n");
|
|
7624
8190
|
}
|
|
@@ -7736,6 +8302,7 @@ async function setupProject(dir, options = {}) {
|
|
|
7736
8302
|
after: JSON.stringify(setup, null, 2) + "\n"
|
|
7737
8303
|
}))
|
|
7738
8304
|
changedFiles.push(SETUP_PATH);
|
|
8305
|
+
const mergeDriver = await configureDecisionMerge(ws.root);
|
|
7739
8306
|
options.progress?.step("Checking agent configuration");
|
|
7740
8307
|
for (const input of sharedInputs)
|
|
7741
8308
|
if (await readText(ws.root, input.path) !== input.before)
|
|
@@ -7759,6 +8326,7 @@ async function setupProject(dir, options = {}) {
|
|
|
7759
8326
|
runtime: { kind: "global", command: "mason", version: installed.version },
|
|
7760
8327
|
changedFiles,
|
|
7761
8328
|
receiptPath,
|
|
8329
|
+
mergeDriver,
|
|
7762
8330
|
activation: await setupStatus(ws.root),
|
|
7763
8331
|
next: configured.disabled || configured.mcpDisabled ? "Mason hooks or MCP are disabled in project configuration. Review that setting before activation." : host === "codex" ? "Review/trust this project's MCP configuration and hooks in Codex (/hooks in the CLI), then start a new session and give it a normal task." : "Approve the project MCP server in Claude Code, then start a new session and give it a normal task."
|
|
7764
8332
|
};
|
|
@@ -7770,6 +8338,7 @@ function summarizeSetup(result) {
|
|
|
7770
8338
|
" MCP server and lifecycle hooks use mason from PATH.",
|
|
7771
8339
|
" Existing shared configuration validated; only local activation state was written.",
|
|
7772
8340
|
` ${result.changedFiles.length} files changed.`,
|
|
8341
|
+
" " + mergeDriverSummary(result.mergeDriver),
|
|
7773
8342
|
"Activation: " + result.activation.status + ".",
|
|
7774
8343
|
result.next,
|
|
7775
8344
|
"If the assistant cannot find mason, restart the desktop app so it picks up PATH.",
|
|
@@ -7844,12 +8413,12 @@ var init_setup = __esm({
|
|
|
7844
8413
|
});
|
|
7845
8414
|
|
|
7846
8415
|
// src/distribution/path.ts
|
|
7847
|
-
import
|
|
8416
|
+
import fs25 from "fs/promises";
|
|
7848
8417
|
import os4 from "os";
|
|
7849
8418
|
import path26 from "path";
|
|
7850
|
-
import { execFile as
|
|
7851
|
-
import { promisify as
|
|
7852
|
-
import { createHash as
|
|
8419
|
+
import { execFile as execFile4 } from "child_process";
|
|
8420
|
+
import { promisify as promisify4 } from "util";
|
|
8421
|
+
import { createHash as createHash8, randomUUID as randomUUID7 } from "crypto";
|
|
7853
8422
|
import { z as z27 } from "zod";
|
|
7854
8423
|
async function configurePath(bin, previous, save, options = {}) {
|
|
7855
8424
|
const env = options.env ?? process.env, platform = options.platform ?? process.platform;
|
|
@@ -7897,7 +8466,7 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
|
|
|
7897
8466
|
);
|
|
7898
8467
|
let login = candidates[2];
|
|
7899
8468
|
for (const file of candidates)
|
|
7900
|
-
if (await
|
|
8469
|
+
if (await fs25.lstat(file).then(
|
|
7901
8470
|
() => true,
|
|
7902
8471
|
(error) => {
|
|
7903
8472
|
if (hasErrorCode(error, "ENOENT")) return false;
|
|
@@ -7909,14 +8478,14 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
|
|
|
7909
8478
|
}
|
|
7910
8479
|
profiles2 = [path26.join(userHome, ".bashrc"), login];
|
|
7911
8480
|
}
|
|
7912
|
-
const marker = `# >>> mason PATH ${
|
|
8481
|
+
const marker = `# >>> mason PATH ${createHash8("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
|
|
7913
8482
|
for (const profile of profiles2) {
|
|
7914
|
-
const stat = await
|
|
8483
|
+
const stat = await fs25.lstat(profile).catch((error) => {
|
|
7915
8484
|
if (hasErrorCode(error, "ENOENT")) return null;
|
|
7916
8485
|
throw error;
|
|
7917
8486
|
});
|
|
7918
|
-
const file = stat?.isSymbolicLink() ? await
|
|
7919
|
-
const original = await
|
|
8487
|
+
const file = stat?.isSymbolicLink() ? await fs25.realpath(profile) : path26.resolve(profile);
|
|
8488
|
+
const original = await fs25.readFile(file).catch((error) => {
|
|
7920
8489
|
if (hasErrorCode(error, "ENOENT") && !stat) return null;
|
|
7921
8490
|
throw error;
|
|
7922
8491
|
});
|
|
@@ -7935,8 +8504,8 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
|
|
|
7935
8504
|
changes.push({ kind: "profile", file, block, created: original === null });
|
|
7936
8505
|
await save(changes);
|
|
7937
8506
|
}
|
|
7938
|
-
await
|
|
7939
|
-
const handle = await
|
|
8507
|
+
await fs25.mkdir(path26.dirname(file), { recursive: true });
|
|
8508
|
+
const handle = await fs25.open(file, original === null ? "ax+" : "a+", 384);
|
|
7940
8509
|
try {
|
|
7941
8510
|
if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0)))
|
|
7942
8511
|
throw new Error(`Shell profile changed during installation: ${profile}`);
|
|
@@ -7964,13 +8533,13 @@ async function removePath(changes) {
|
|
|
7964
8533
|
await windowsPath("remove", change.entry, current, change.created);
|
|
7965
8534
|
continue;
|
|
7966
8535
|
}
|
|
7967
|
-
const stat = await
|
|
8536
|
+
const stat = await fs25.lstat(change.file).catch((error) => {
|
|
7968
8537
|
if (hasErrorCode(error, "ENOENT")) return null;
|
|
7969
8538
|
throw error;
|
|
7970
8539
|
});
|
|
7971
8540
|
if (!stat) continue;
|
|
7972
8541
|
if (!stat.isFile()) throw new Error("profile type changed");
|
|
7973
|
-
const bytes = await
|
|
8542
|
+
const bytes = await fs25.readFile(change.file), text2 = bytes.toString("utf8");
|
|
7974
8543
|
if (!Buffer.from(text2).equals(bytes)) throw new Error("profile encoding changed");
|
|
7975
8544
|
const index = text2.indexOf(change.block);
|
|
7976
8545
|
if (index < 0 || text2.indexOf(change.block, index + change.block.length) >= 0)
|
|
@@ -7981,7 +8550,7 @@ async function removePath(changes) {
|
|
|
7981
8550
|
const temporary = change.file + ".mason-" + randomUUID7();
|
|
7982
8551
|
try {
|
|
7983
8552
|
if (stat.nlink > 1) throw new Error("profile has multiple hard links");
|
|
7984
|
-
const handle = await
|
|
8553
|
+
const handle = await fs25.open(temporary, "wx", stat.mode & 511);
|
|
7985
8554
|
try {
|
|
7986
8555
|
await handle.writeFile(next);
|
|
7987
8556
|
await handle.chmod(stat.mode & 511);
|
|
@@ -7990,12 +8559,12 @@ async function removePath(changes) {
|
|
|
7990
8559
|
} finally {
|
|
7991
8560
|
await handle.close();
|
|
7992
8561
|
}
|
|
7993
|
-
const current = await
|
|
7994
|
-
if (!current.isFile() || current.ino !== stat.ino || !(await
|
|
8562
|
+
const current = await fs25.lstat(change.file);
|
|
8563
|
+
if (!current.isFile() || current.ino !== stat.ino || !(await fs25.readFile(change.file)).equals(bytes))
|
|
7995
8564
|
throw new Error("profile changed during uninstall");
|
|
7996
|
-
await
|
|
8565
|
+
await fs25.rename(temporary, change.file);
|
|
7997
8566
|
} finally {
|
|
7998
|
-
await
|
|
8567
|
+
await fs25.rm(temporary, { force: true });
|
|
7999
8568
|
}
|
|
8000
8569
|
} catch (error) {
|
|
8001
8570
|
warnings.push(
|
|
@@ -8052,7 +8621,7 @@ var init_path = __esm({
|
|
|
8052
8621
|
"src/distribution/path.ts"() {
|
|
8053
8622
|
"use strict";
|
|
8054
8623
|
init_validation();
|
|
8055
|
-
execute2 =
|
|
8624
|
+
execute2 = promisify4(execFile4);
|
|
8056
8625
|
pathChangeSchema = z27.discriminatedUnion("kind", [
|
|
8057
8626
|
z27.object({
|
|
8058
8627
|
kind: z27.literal("profile"),
|
|
@@ -8088,7 +8657,7 @@ __export(state_exports, {
|
|
|
8088
8657
|
import os5 from "os";
|
|
8089
8658
|
import path27 from "path";
|
|
8090
8659
|
import { fileURLToPath } from "url";
|
|
8091
|
-
import
|
|
8660
|
+
import fs26 from "fs/promises";
|
|
8092
8661
|
import { z as z28 } from "zod";
|
|
8093
8662
|
async function readInstallation(home) {
|
|
8094
8663
|
const raw = await readStoreJson(home, "install.json");
|
|
@@ -8097,7 +8666,7 @@ async function readInstallation(home) {
|
|
|
8097
8666
|
"Run this command from a standalone installation. npm users can upgrade with npm."
|
|
8098
8667
|
);
|
|
8099
8668
|
const record = recordSchema3.parse(raw);
|
|
8100
|
-
if (record.home !== await
|
|
8669
|
+
if (record.home !== await fs26.realpath(home))
|
|
8101
8670
|
throw new Error("Invalid standalone installation receipt.");
|
|
8102
8671
|
return record;
|
|
8103
8672
|
}
|
|
@@ -8162,12 +8731,12 @@ var init_state = __esm({
|
|
|
8162
8731
|
});
|
|
8163
8732
|
|
|
8164
8733
|
// src/distribution/bundle.ts
|
|
8165
|
-
import
|
|
8734
|
+
import fs27 from "fs/promises";
|
|
8166
8735
|
import path28 from "path";
|
|
8167
|
-
import { createHash as
|
|
8736
|
+
import { createHash as createHash9 } from "crypto";
|
|
8168
8737
|
import { z as z29 } from "zod";
|
|
8169
8738
|
async function readBundle(root) {
|
|
8170
|
-
const bytes = await
|
|
8739
|
+
const bytes = await fs27.readFile(await storePath(root, "bundle.json"));
|
|
8171
8740
|
const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
|
|
8172
8741
|
const windows = manifest.target.startsWith("win32-");
|
|
8173
8742
|
for (const file of Object.keys(manifest.files)) {
|
|
@@ -8197,7 +8766,7 @@ async function verifyBundle(root, expectedHash) {
|
|
|
8197
8766
|
for (let i = 0; i < files.length; i += 8)
|
|
8198
8767
|
await Promise.all(
|
|
8199
8768
|
files.slice(i, i + 8).map(async (file) => {
|
|
8200
|
-
if (sha256(await
|
|
8769
|
+
if (sha256(await fs27.readFile(await storePath(root, file))) !== bundle.manifest.files[file])
|
|
8201
8770
|
throw new Error("Mason bundle checksum mismatch: " + file);
|
|
8202
8771
|
})
|
|
8203
8772
|
);
|
|
@@ -8224,7 +8793,7 @@ var init_bundle = __esm({
|
|
|
8224
8793
|
nodeVersion: z29.string(),
|
|
8225
8794
|
files: z29.record(digest2)
|
|
8226
8795
|
}).strict();
|
|
8227
|
-
sha256 = (bytes) =>
|
|
8796
|
+
sha256 = (bytes) => createHash9("sha256").update(bytes).digest("hex");
|
|
8228
8797
|
}
|
|
8229
8798
|
});
|
|
8230
8799
|
|
|
@@ -8247,7 +8816,7 @@ var init_trust2 = __esm({
|
|
|
8247
8816
|
});
|
|
8248
8817
|
|
|
8249
8818
|
// src/distribution/runtime.ts
|
|
8250
|
-
import
|
|
8819
|
+
import fs28 from "fs/promises";
|
|
8251
8820
|
import { unlinkSync } from "fs";
|
|
8252
8821
|
import os6 from "os";
|
|
8253
8822
|
import path29 from "path";
|
|
@@ -8258,13 +8827,13 @@ async function pruneUnusedVersions(home) {
|
|
|
8258
8827
|
const candidates = [];
|
|
8259
8828
|
const initial = await readInstallation(home);
|
|
8260
8829
|
const initialKeep = /* @__PURE__ */ new Set([initial.current, initial.previous?.id, initial.pending?.id]);
|
|
8261
|
-
for (const name of await
|
|
8830
|
+
for (const name of await fs28.readdir(versions)) {
|
|
8262
8831
|
const retired = /^\.retired-([a-f0-9]{24})-[a-f0-9-]+$/.exec(name);
|
|
8263
8832
|
const id = retired?.[1] ?? name;
|
|
8264
8833
|
if (!/^[a-f0-9]{24}$/.test(id) || initialKeep.has(id)) continue;
|
|
8265
8834
|
try {
|
|
8266
8835
|
const directory3 = await storePath(home, `versions/${name}`);
|
|
8267
|
-
await
|
|
8836
|
+
await fs28.access(await storePath(directory3, "app/dist/mason-runtime.js"));
|
|
8268
8837
|
const bundle = await verifyBundle(directory3);
|
|
8269
8838
|
if (bundle.manifestHash.slice(0, 24) === id && bundle.manifest.files["app/dist/mason-runtime.js"])
|
|
8270
8839
|
candidates.push({ id, directory: directory3, retired: !!retired });
|
|
@@ -8277,7 +8846,7 @@ async function pruneUnusedVersions(home) {
|
|
|
8277
8846
|
const record = await readInstallation(home);
|
|
8278
8847
|
const keep = /* @__PURE__ */ new Set([record.current, record.previous?.id, record.pending?.id]);
|
|
8279
8848
|
const directory3 = await storePath(home, "runtimes");
|
|
8280
|
-
const leases = await
|
|
8849
|
+
const leases = await fs28.readdir(directory3).catch((error) => {
|
|
8281
8850
|
if (hasErrorCode(error, "ENOENT")) return [];
|
|
8282
8851
|
throw error;
|
|
8283
8852
|
});
|
|
@@ -8293,7 +8862,7 @@ async function pruneUnusedVersions(home) {
|
|
|
8293
8862
|
keep.add(lease.bundle);
|
|
8294
8863
|
} catch (error) {
|
|
8295
8864
|
if (error.code !== "ESRCH") return;
|
|
8296
|
-
await
|
|
8865
|
+
await fs28.rm(await storePath(home, `runtimes/${name}`), { force: true });
|
|
8297
8866
|
}
|
|
8298
8867
|
} catch {
|
|
8299
8868
|
return;
|
|
@@ -8303,13 +8872,13 @@ async function pruneUnusedVersions(home) {
|
|
|
8303
8872
|
if (keep.has(candidate.id)) continue;
|
|
8304
8873
|
try {
|
|
8305
8874
|
const retired = candidate.retired ? candidate.directory : await storePath(home, `versions/.retired-${candidate.id}-${randomUUID8()}`);
|
|
8306
|
-
if (!candidate.retired) await
|
|
8875
|
+
if (!candidate.retired) await fs28.rename(candidate.directory, retired);
|
|
8307
8876
|
garbage.push(retired);
|
|
8308
8877
|
} catch {
|
|
8309
8878
|
}
|
|
8310
8879
|
}
|
|
8311
8880
|
});
|
|
8312
|
-
for (const directory3 of garbage) await
|
|
8881
|
+
for (const directory3 of garbage) await fs28.rm(directory3, { recursive: true }).catch(() => {
|
|
8313
8882
|
});
|
|
8314
8883
|
}
|
|
8315
8884
|
var leaseSchema;
|
|
@@ -8331,13 +8900,13 @@ var init_runtime2 = __esm({
|
|
|
8331
8900
|
});
|
|
8332
8901
|
|
|
8333
8902
|
// src/distribution/sessions.ts
|
|
8334
|
-
import
|
|
8903
|
+
import fs29 from "fs/promises";
|
|
8335
8904
|
import os7 from "os";
|
|
8336
8905
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
8337
8906
|
import { z as z31 } from "zod";
|
|
8338
8907
|
async function hasRunningMcp(home) {
|
|
8339
8908
|
const directory3 = await storePath(home, "sessions");
|
|
8340
|
-
const files = await
|
|
8909
|
+
const files = await fs29.readdir(directory3).catch((error) => {
|
|
8341
8910
|
if (hasErrorCode(error, "ENOENT")) return [];
|
|
8342
8911
|
throw error;
|
|
8343
8912
|
});
|
|
@@ -8355,7 +8924,7 @@ async function hasRunningMcp(home) {
|
|
|
8355
8924
|
} catch (error) {
|
|
8356
8925
|
if (error.code !== "ESRCH") return true;
|
|
8357
8926
|
}
|
|
8358
|
-
await
|
|
8927
|
+
await fs29.rm(await storePath(home, relative), { force: true });
|
|
8359
8928
|
} catch {
|
|
8360
8929
|
return true;
|
|
8361
8930
|
}
|
|
@@ -8366,7 +8935,7 @@ async function registerMcp(home) {
|
|
|
8366
8935
|
const name = `sessions/${randomUUID9()}.json`;
|
|
8367
8936
|
await writeStoreJson(home, name, { pid: process.pid, host: os7.hostname() });
|
|
8368
8937
|
return async () => {
|
|
8369
|
-
await
|
|
8938
|
+
await fs29.rm(await storePath(home, name), { force: true }).catch(() => {
|
|
8370
8939
|
});
|
|
8371
8940
|
};
|
|
8372
8941
|
}
|
|
@@ -8391,27 +8960,27 @@ __export(install_exports2, {
|
|
|
8391
8960
|
uninstallStandalone: () => uninstallStandalone,
|
|
8392
8961
|
upgradeStandalone: () => upgradeStandalone
|
|
8393
8962
|
});
|
|
8394
|
-
import
|
|
8963
|
+
import fs30 from "fs/promises";
|
|
8395
8964
|
import os8 from "os";
|
|
8396
8965
|
import path30 from "path";
|
|
8397
|
-
import { execFile as
|
|
8398
|
-
import { promisify as
|
|
8966
|
+
import { execFile as execFile5, spawn } from "child_process";
|
|
8967
|
+
import { promisify as promisify5 } from "util";
|
|
8399
8968
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
8400
8969
|
async function replace(file, content2, mode = 384) {
|
|
8401
8970
|
const temp = file + ".tmp-" + randomUUID10();
|
|
8402
8971
|
try {
|
|
8403
|
-
await
|
|
8404
|
-
await
|
|
8972
|
+
await fs30.writeFile(temp, content2, { mode, flag: "wx" });
|
|
8973
|
+
await fs30.rename(temp, file);
|
|
8405
8974
|
} finally {
|
|
8406
|
-
await
|
|
8975
|
+
await fs30.rm(temp, { force: true });
|
|
8407
8976
|
}
|
|
8408
8977
|
}
|
|
8409
8978
|
async function installStandalone(source, progress2, options = {}) {
|
|
8410
8979
|
progress2?.step("Checking installation files and location");
|
|
8411
8980
|
const bundle = await verifyBundle(source);
|
|
8412
8981
|
const selectedHome = path30.resolve(defaultHome());
|
|
8413
|
-
if (!options.stageOnly) await
|
|
8414
|
-
const home = await
|
|
8982
|
+
if (!options.stageOnly) await fs30.mkdir(selectedHome, { recursive: true });
|
|
8983
|
+
const home = await fs30.realpath(selectedHome);
|
|
8415
8984
|
if (options.stageOnly) {
|
|
8416
8985
|
assertUpdatePolicy(
|
|
8417
8986
|
await readInstallation(home),
|
|
@@ -8420,7 +8989,7 @@ async function installStandalone(source, progress2, options = {}) {
|
|
|
8420
8989
|
);
|
|
8421
8990
|
const destination = await storePath(home, "versions/" + bundle.manifestHash.slice(0, 24));
|
|
8422
8991
|
await copyBundle(home, source, destination, bundle);
|
|
8423
|
-
const { stdout } = await
|
|
8992
|
+
const { stdout } = await promisify5(execFile5)(
|
|
8424
8993
|
path30.join(destination, process.platform === "win32" ? "node.exe" : "node"),
|
|
8425
8994
|
[path30.join(destination, "app/dist/mason.js"), "--version"],
|
|
8426
8995
|
{ timeout: 3e4, windowsHide: true, env: { ...process.env, MASON_NO_AUTO_UPDATE: "1" } }
|
|
@@ -8445,19 +9014,19 @@ async function installLocked2(home, source, bundle, progress2, options, alreadyS
|
|
|
8445
9014
|
if (options.stageOnly)
|
|
8446
9015
|
assertUpdatePolicy(previous, bundle.manifest.version, options.policyRevision);
|
|
8447
9016
|
if (!previous)
|
|
8448
|
-
for (const file of await
|
|
9017
|
+
for (const file of await fs30.readdir(home)) {
|
|
8449
9018
|
if (file === ".install-lock") continue;
|
|
8450
9019
|
const directory3 = path30.join(home, file);
|
|
8451
|
-
const stat = await
|
|
8452
|
-
if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await
|
|
9020
|
+
const stat = await fs30.lstat(directory3);
|
|
9021
|
+
if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs30.readdir(directory3)).length) {
|
|
8453
9022
|
throw new Error("Mason installation directory is not empty and has no ownership receipt.");
|
|
8454
9023
|
}
|
|
8455
9024
|
}
|
|
8456
9025
|
const selectedBin = path30.resolve(
|
|
8457
9026
|
process.env.MASON_BIN_DIR ?? previous?.bin ?? (process.platform === "win32" ? path30.join(home, "bin") : path30.join(os8.homedir(), ".local/bin"))
|
|
8458
9027
|
);
|
|
8459
|
-
await
|
|
8460
|
-
const bin = await
|
|
9028
|
+
await fs30.mkdir(selectedBin, { recursive: true });
|
|
9029
|
+
const bin = await fs30.realpath(selectedBin);
|
|
8461
9030
|
if (previous && (previous.home !== home || previous.bin !== bin))
|
|
8462
9031
|
throw new Error("Installation paths differ from the existing Mason receipt.");
|
|
8463
9032
|
const id = bundle.manifestHash.slice(0, 24);
|
|
@@ -8501,7 +9070,7 @@ exec ${shQuote(path30.join(destination, "node"))} ${shQuote(path30.join(destinat
|
|
|
8501
9070
|
};
|
|
8502
9071
|
for (const [name, content2] of Object.entries(launchers)) {
|
|
8503
9072
|
const file = await storePath(bin, name);
|
|
8504
|
-
const old = await
|
|
9073
|
+
const old = await fs30.readFile(file, "utf8").catch((error) => {
|
|
8505
9074
|
if (hasErrorCode(error, "ENOENT")) return null;
|
|
8506
9075
|
throw error;
|
|
8507
9076
|
});
|
|
@@ -8513,7 +9082,7 @@ exec ${shQuote(path30.join(destination, "node"))} ${shQuote(path30.join(destinat
|
|
|
8513
9082
|
);
|
|
8514
9083
|
const checkRetired = async (name, expected) => {
|
|
8515
9084
|
if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
|
|
8516
|
-
const actual = await
|
|
9085
|
+
const actual = await fs30.readFile(await storePath(bin, name), "utf8").catch((error) => {
|
|
8517
9086
|
if (hasErrorCode(error, "ENOENT")) return null;
|
|
8518
9087
|
throw error;
|
|
8519
9088
|
});
|
|
@@ -8556,7 +9125,7 @@ exec ${shQuote(path30.join(destination, "node"))} ${shQuote(path30.join(destinat
|
|
|
8556
9125
|
await replace(await storePath(bin, name, true), content2, 493);
|
|
8557
9126
|
for (const [name, expected] of retired) {
|
|
8558
9127
|
await checkRetired(name, expected);
|
|
8559
|
-
await
|
|
9128
|
+
await fs30.rm(await storePath(bin, name), { force: true });
|
|
8560
9129
|
}
|
|
8561
9130
|
await writeStoreJson(home, "install.json", receipt);
|
|
8562
9131
|
} catch (error) {
|
|
@@ -8589,7 +9158,7 @@ exec ${shQuote(path30.join(destination, "node"))} ${shQuote(path30.join(destinat
|
|
|
8589
9158
|
};
|
|
8590
9159
|
}
|
|
8591
9160
|
async function copyBundle(home, source, destination, bundle) {
|
|
8592
|
-
const exists2 = await
|
|
9161
|
+
const exists2 = await fs30.lstat(destination).then(
|
|
8593
9162
|
() => true,
|
|
8594
9163
|
(error) => {
|
|
8595
9164
|
if (hasErrorCode(error, "ENOENT")) return false;
|
|
@@ -8601,16 +9170,16 @@ async function copyBundle(home, source, destination, bundle) {
|
|
|
8601
9170
|
return;
|
|
8602
9171
|
}
|
|
8603
9172
|
const stage = await storePath(home, "versions/.install-" + randomUUID10(), true);
|
|
8604
|
-
await
|
|
9173
|
+
await fs30.mkdir(stage);
|
|
8605
9174
|
try {
|
|
8606
9175
|
for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
|
|
8607
9176
|
const from = await storePath(source, file), to = await storePath(stage, file, true);
|
|
8608
|
-
await
|
|
8609
|
-
await
|
|
9177
|
+
await fs30.copyFile(from, to);
|
|
9178
|
+
await fs30.chmod(to, (await fs30.stat(from)).mode & 511);
|
|
8610
9179
|
}
|
|
8611
9180
|
await verifyBundle(stage, bundle.manifestHash);
|
|
8612
9181
|
try {
|
|
8613
|
-
await
|
|
9182
|
+
await fs30.rename(stage, destination);
|
|
8614
9183
|
} catch (error) {
|
|
8615
9184
|
try {
|
|
8616
9185
|
await verifyBundle(destination, bundle.manifestHash);
|
|
@@ -8619,7 +9188,7 @@ async function copyBundle(home, source, destination, bundle) {
|
|
|
8619
9188
|
}
|
|
8620
9189
|
}
|
|
8621
9190
|
} finally {
|
|
8622
|
-
await
|
|
9191
|
+
await fs30.rm(stage, { recursive: true, force: true });
|
|
8623
9192
|
}
|
|
8624
9193
|
}
|
|
8625
9194
|
async function prepareStandaloneMcp(home) {
|
|
@@ -8766,7 +9335,7 @@ async function uninstallStandalone() {
|
|
|
8766
9335
|
for (const [name, expected] of Object.entries(record.launchers)) {
|
|
8767
9336
|
if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
|
|
8768
9337
|
const file = await storePath(record.bin, name);
|
|
8769
|
-
const actual = await
|
|
9338
|
+
const actual = await fs30.readFile(file, "utf8").catch((error) => {
|
|
8770
9339
|
if (hasErrorCode(error, "ENOENT")) return null;
|
|
8771
9340
|
throw error;
|
|
8772
9341
|
});
|
|
@@ -8777,9 +9346,9 @@ async function uninstallStandalone() {
|
|
|
8777
9346
|
if (process.platform === "win32")
|
|
8778
9347
|
await replace(await storePath(home, ".uninstall-request.txt", true), token);
|
|
8779
9348
|
for (const name of Object.keys(record.launchers))
|
|
8780
|
-
await
|
|
9349
|
+
await fs30.rm(await storePath(record.bin, name), { force: true });
|
|
8781
9350
|
});
|
|
8782
|
-
if (process.platform !== "win32") await
|
|
9351
|
+
if (process.platform !== "win32") await fs30.rm(home, { recursive: true, force: true });
|
|
8783
9352
|
return home;
|
|
8784
9353
|
}
|
|
8785
9354
|
var shQuote, psQuote, WINDOWS_BATCH_LAUNCHER, launcherNames;
|
|
@@ -8815,7 +9384,7 @@ __export(updates_exports, {
|
|
|
8815
9384
|
updateSettings: () => updateSettings,
|
|
8816
9385
|
updateStatus: () => updateStatus
|
|
8817
9386
|
});
|
|
8818
|
-
import
|
|
9387
|
+
import fs31 from "fs/promises";
|
|
8819
9388
|
import path31 from "path";
|
|
8820
9389
|
import { spawn as spawn2 } from "child_process";
|
|
8821
9390
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
@@ -8952,7 +9521,7 @@ async function runAutomaticUpdate(home, dependencies = { fetchRelease: fetchVeri
|
|
|
8952
9521
|
} catch (error) {
|
|
8953
9522
|
status.error = error instanceof Error ? error.message : String(error);
|
|
8954
9523
|
}
|
|
8955
|
-
if (await
|
|
9524
|
+
if (await fs31.access(path31.join(home, "install.json")).then(
|
|
8956
9525
|
() => true,
|
|
8957
9526
|
() => false
|
|
8958
9527
|
))
|
|
@@ -9419,7 +9988,7 @@ var inputArgs = process.argv.slice(2);
|
|
|
9419
9988
|
var managedLaunch = inputArgs[0] === "--setup-host";
|
|
9420
9989
|
var setupHost = managedLaunch ? inputArgs.splice(0, 2)[1] : void 0;
|
|
9421
9990
|
var [command, ...args] = inputArgs;
|
|
9422
|
-
var usage = `Mason ${"0.
|
|
9991
|
+
var usage = `Mason ${"0.23.1"}
|
|
9423
9992
|
|
|
9424
9993
|
Usage: mason <command> [options]
|
|
9425
9994
|
|
|
@@ -9436,6 +10005,8 @@ Usage: mason <command> [options]
|
|
|
9436
10005
|
review --base <ref> Review committed changes
|
|
9437
10006
|
drift Check an optional architecture map
|
|
9438
10007
|
mcp Run the MCP server (stdio)
|
|
10008
|
+
merge-decision <base> <current> <incoming>
|
|
10009
|
+
Git merge driver for decision records (set by setup)
|
|
9439
10010
|
upgrade [version] Upgrade the standalone user installation
|
|
9440
10011
|
updates [status|enable|disable|pin|unpin]
|
|
9441
10012
|
Manage automatic standalone updates
|
|
@@ -9456,9 +10027,9 @@ try {
|
|
|
9456
10027
|
}
|
|
9457
10028
|
}
|
|
9458
10029
|
if (command === "internal-integration-version")
|
|
9459
|
-
console.log(JSON.stringify({ protocol: 1, version: "0.
|
|
10030
|
+
console.log(JSON.stringify({ protocol: 1, version: "0.23.1" }));
|
|
9460
10031
|
else if (!command || ["--help", "-h", "help"].includes(command)) console.log(usage);
|
|
9461
|
-
else if (["--version", "-v"].includes(command)) console.log("0.
|
|
10032
|
+
else if (["--version", "-v"].includes(command)) console.log("0.23.1");
|
|
9462
10033
|
else if (command === "internal-update") {
|
|
9463
10034
|
if (args.length) throw new Error("internal-update takes no arguments.");
|
|
9464
10035
|
const { currentInstallation: currentInstallation2 } = await Promise.resolve().then(() => (init_state(), state_exports));
|
|
@@ -9492,6 +10063,9 @@ ${installed.path.message}
|
|
|
9492
10063
|
Automatic updates: ${policy}. Manage with mason updates.`
|
|
9493
10064
|
);
|
|
9494
10065
|
}
|
|
10066
|
+
} else if (command === "merge-decision") {
|
|
10067
|
+
const { runDecisionMerge: runDecisionMerge2 } = await Promise.resolve().then(() => (init_merge(), merge_exports));
|
|
10068
|
+
process.exitCode = await runDecisionMerge2(args);
|
|
9495
10069
|
} else if (command === "updates") {
|
|
9496
10070
|
if (args.includes("--help"))
|
|
9497
10071
|
console.log(
|