mason-context 0.17.4 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +2 -0
- package/dist/mason-audit.js +132 -68
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-auto.js +165 -45
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-drift.js +10 -4
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js +15 -9
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +1177 -348
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +11 -5
- package/dist/mason-review.js.map +1 -1
- package/dist/mason-runtime.js +337 -0
- package/dist/mason-runtime.js.map +1 -0
- package/dist/mason.js +964 -238
- package/dist/mason.js.map +1 -1
- package/package.json +2 -1
package/dist/mason-mcp.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
if (!import.meta.url.endsWith("/mason-runtime.js")) await import("./mason-runtime.js").then(m => m.registerRuntime());
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
5
|
var __esm = (fn, res) => function __init() {
|
|
@@ -61,19 +62,19 @@ var init_git_read = __esm({
|
|
|
61
62
|
import path from "path";
|
|
62
63
|
function splitCommands(line) {
|
|
63
64
|
const parts = [];
|
|
64
|
-
let
|
|
65
|
+
let quote2 = "", start = 0;
|
|
65
66
|
for (let i = 0; i < line.length; i++) {
|
|
66
67
|
const char = line[i];
|
|
67
68
|
if (char === "\\") {
|
|
68
69
|
i++;
|
|
69
70
|
continue;
|
|
70
71
|
}
|
|
71
|
-
if (
|
|
72
|
-
if (char ===
|
|
72
|
+
if (quote2) {
|
|
73
|
+
if (char === quote2) quote2 = "";
|
|
73
74
|
continue;
|
|
74
75
|
}
|
|
75
76
|
if (char === "'" || char === '"') {
|
|
76
|
-
|
|
77
|
+
quote2 = char;
|
|
77
78
|
continue;
|
|
78
79
|
}
|
|
79
80
|
if (char === ";" || line.slice(i, i + 2) === "&&") {
|
|
@@ -1098,6 +1099,105 @@ var init_scope = __esm({
|
|
|
1098
1099
|
}
|
|
1099
1100
|
});
|
|
1100
1101
|
|
|
1102
|
+
// src/utils/store-lock.ts
|
|
1103
|
+
import fs6 from "fs/promises";
|
|
1104
|
+
import os from "os";
|
|
1105
|
+
async function lockDiagnostic(file) {
|
|
1106
|
+
let reason2;
|
|
1107
|
+
try {
|
|
1108
|
+
const owner = JSON.parse(await fs6.readFile(file, "utf8"));
|
|
1109
|
+
if (!owner || typeof owner.host !== "string" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
|
|
1110
|
+
reason2 = "Lock owner data is incomplete or malformed.";
|
|
1111
|
+
} else if (owner.host !== os.hostname()) {
|
|
1112
|
+
reason2 = "The lock belongs to another host; its process cannot be checked locally.";
|
|
1113
|
+
} else {
|
|
1114
|
+
try {
|
|
1115
|
+
process.kill(owner.pid, 0);
|
|
1116
|
+
reason2 = `Local owner PID ${owner.pid} is still running.`;
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
reason2 = error.code === "ESRCH" ? `Local owner PID ${owner.pid} has exited, but lock recovery has not completed.` : `Local owner PID ${owner.pid} could not be checked.`;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
reason2 = error instanceof SyntaxError ? "Lock owner data is incomplete or malformed." : "Lock owner data could not be read; the lock may have changed.";
|
|
1123
|
+
}
|
|
1124
|
+
if (await fs6.lstat(file + ".reclaim").then(() => true, () => false)) {
|
|
1125
|
+
reason2 += " A recovery guard also needs inspection: " + file + ".reclaim.";
|
|
1126
|
+
}
|
|
1127
|
+
return reason2 + " Retry if a writer is active. For manual recovery, stop all Mason writers for this checkout, confirm they have exited (including other hosts), then remove only confirmed abandoned lock and lock.reclaim files. See docs/reference.md#snapshot-repairs-and-verification.";
|
|
1128
|
+
}
|
|
1129
|
+
async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason store") {
|
|
1130
|
+
const file = await storePath(root, directory + "/lock", true);
|
|
1131
|
+
const deadline = Date.now() + waitMs;
|
|
1132
|
+
let handle;
|
|
1133
|
+
while (!handle) {
|
|
1134
|
+
try {
|
|
1135
|
+
handle = await fs6.open(file, "wx", 384);
|
|
1136
|
+
try {
|
|
1137
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
await handle.close().catch(() => {
|
|
1140
|
+
});
|
|
1141
|
+
handle = void 0;
|
|
1142
|
+
await fs6.rm(file, { force: true }).catch(() => {
|
|
1143
|
+
});
|
|
1144
|
+
throw error;
|
|
1145
|
+
}
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
if (error.code !== "EEXIST") throw error;
|
|
1148
|
+
try {
|
|
1149
|
+
const owner = JSON.parse(await fs6.readFile(file, "utf8"));
|
|
1150
|
+
if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
1151
|
+
try {
|
|
1152
|
+
process.kill(owner.pid, 0);
|
|
1153
|
+
} catch (probe) {
|
|
1154
|
+
if (probe.code === "ESRCH") {
|
|
1155
|
+
const reclaim = file + ".reclaim";
|
|
1156
|
+
let guard;
|
|
1157
|
+
try {
|
|
1158
|
+
guard = await fs6.open(reclaim, "wx", 384);
|
|
1159
|
+
const current = JSON.parse(await fs6.readFile(file, "utf8"));
|
|
1160
|
+
if (current.pid === owner.pid && current.host === owner.host) await fs6.unlink(file);
|
|
1161
|
+
} finally {
|
|
1162
|
+
if (guard) {
|
|
1163
|
+
await guard.close();
|
|
1164
|
+
await fs6.rm(reclaim, { force: true });
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
} catch {
|
|
1171
|
+
}
|
|
1172
|
+
if (Date.now() >= deadline) throw new Error(label + " is busy or its lock needs inspection: " + file + ". " + await lockDiagnostic(file));
|
|
1173
|
+
await new Promise((resolve2) => setTimeout(resolve2, 40));
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
try {
|
|
1177
|
+
return await run();
|
|
1178
|
+
} finally {
|
|
1179
|
+
await handle.close();
|
|
1180
|
+
await fs6.unlink(file);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
var init_store_lock = __esm({
|
|
1184
|
+
"src/utils/store-lock.ts"() {
|
|
1185
|
+
"use strict";
|
|
1186
|
+
init_storage();
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
// src/snapshot/lock.ts
|
|
1191
|
+
function withSnapshotWrite(root, run, waitMs = 5e3) {
|
|
1192
|
+
return withStoreLock(root, ".mason/local/snapshot-write", run, waitMs, "Snapshot store");
|
|
1193
|
+
}
|
|
1194
|
+
var init_lock = __esm({
|
|
1195
|
+
"src/snapshot/lock.ts"() {
|
|
1196
|
+
"use strict";
|
|
1197
|
+
init_store_lock();
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1101
1201
|
// src/test-map.ts
|
|
1102
1202
|
var test_map_exports = {};
|
|
1103
1203
|
__export(test_map_exports, {
|
|
@@ -1203,8 +1303,15 @@ async function inspectSnapshot(rootDir) {
|
|
|
1203
1303
|
}] };
|
|
1204
1304
|
}
|
|
1205
1305
|
}
|
|
1206
|
-
async function
|
|
1207
|
-
|
|
1306
|
+
async function updateSnapshot(rootDir, update) {
|
|
1307
|
+
return withSnapshotWrite(rootDir, async () => {
|
|
1308
|
+
const change = await update(await loadSnapshot(rootDir));
|
|
1309
|
+
if (change.snapshot) {
|
|
1310
|
+
await writeStoreJson(rootDir, ".mason/snapshot.json", snapshotSchema.parse(change.snapshot));
|
|
1311
|
+
await change.afterSave?.();
|
|
1312
|
+
}
|
|
1313
|
+
return change.result;
|
|
1314
|
+
});
|
|
1208
1315
|
}
|
|
1209
1316
|
async function getCurrentGitHash(rootDir) {
|
|
1210
1317
|
try {
|
|
@@ -1277,6 +1384,7 @@ var init_snapshot = __esm({
|
|
|
1277
1384
|
init_files();
|
|
1278
1385
|
init_files();
|
|
1279
1386
|
init_storage();
|
|
1387
|
+
init_lock();
|
|
1280
1388
|
init_paths();
|
|
1281
1389
|
init_test_map();
|
|
1282
1390
|
repoPath = z.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
|
|
@@ -1284,6 +1392,7 @@ var init_snapshot = __esm({
|
|
|
1284
1392
|
refreshedHash: z.string().optional(),
|
|
1285
1393
|
verifiedAt: z.string().optional(),
|
|
1286
1394
|
verifiedHash: z.string().optional(),
|
|
1395
|
+
verificationToken: z.string().optional(),
|
|
1287
1396
|
verificationFailed: z.boolean().optional(),
|
|
1288
1397
|
verificationNote: z.string().optional()
|
|
1289
1398
|
};
|
|
@@ -1311,7 +1420,7 @@ var init_snapshot = __esm({
|
|
|
1311
1420
|
});
|
|
1312
1421
|
|
|
1313
1422
|
// src/drift/drift.ts
|
|
1314
|
-
import
|
|
1423
|
+
import fs7 from "fs/promises";
|
|
1315
1424
|
import path11 from "path";
|
|
1316
1425
|
function parseChanges(output) {
|
|
1317
1426
|
const fields = output.split("\0");
|
|
@@ -1379,7 +1488,7 @@ async function findGhostFiles(resolvedRoot, mappedFiles) {
|
|
|
1379
1488
|
const ghosts = [];
|
|
1380
1489
|
for (const file of mappedFiles) {
|
|
1381
1490
|
try {
|
|
1382
|
-
await
|
|
1491
|
+
await fs7.access(path11.join(resolvedRoot, file));
|
|
1383
1492
|
} catch {
|
|
1384
1493
|
ghosts.push(file);
|
|
1385
1494
|
}
|
|
@@ -1561,7 +1670,7 @@ var init_deleted_reference = __esm({
|
|
|
1561
1670
|
|
|
1562
1671
|
// src/audit/checks/new-module.ts
|
|
1563
1672
|
import fg4 from "fast-glob";
|
|
1564
|
-
import
|
|
1673
|
+
import fs8 from "fs/promises";
|
|
1565
1674
|
import path13 from "path";
|
|
1566
1675
|
function escapeRegExp(text2) {
|
|
1567
1676
|
return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -1613,7 +1722,7 @@ async function collectModuleCandidates(root, combinedDocs) {
|
|
|
1613
1722
|
const byParent = /* @__PURE__ */ new Map();
|
|
1614
1723
|
for (let offset = 0; offset < parents.length; offset += 16) {
|
|
1615
1724
|
await Promise.all(parents.slice(offset, offset + 16).map(async (dir) => {
|
|
1616
|
-
const entries = await
|
|
1725
|
+
const entries = await fs8.readdir(await auditInputPath(root, dir), { withFileTypes: true });
|
|
1617
1726
|
const dirs = entries.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink() && sources.some((file) => file.startsWith(path13.posix.join(dir, entry.name) + "/"))) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)).map((entry) => path13.posix.join(dir, entry.name));
|
|
1618
1727
|
byParent.set(dir, dirs);
|
|
1619
1728
|
}));
|
|
@@ -2026,6 +2135,10 @@ var init_release_metadata = __esm({
|
|
|
2026
2135
|
});
|
|
2027
2136
|
|
|
2028
2137
|
// src/audit/checks/deps-changed.ts
|
|
2138
|
+
function hasDependencyContent(content2) {
|
|
2139
|
+
const text2 = content2.replace(/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g, "").replace(/<!--[\s\S]*?-->/g, "");
|
|
2140
|
+
return /\b(?:dependenc(?:y|ies)|librar(?:y|ies)|frameworks?|tech(?:nology)? stack|prerequisites|requirements)\b/i.test(text2) || /\b(?:package\.json|build\.gradle(?:\.kts)?|settings\.gradle(?:\.kts)?|libs\.versions\.toml|Cargo\.toml|go\.mod|pyproject\.toml|requirements\.txt|Gemfile|composer\.json)\b/i.test(text2) || /\b(?:node(?:\.js)?|npm|pnpm|yarn|bun|deno|python|ruby|rust|go|java|jdk|kotlin|gradle|swift|php|react|vue|angular|compose|ktor|room)\s*(?:version\s*)?[`*:=>~^v\s-]*\d+(?:\.\d+)*\b/i.test(text2) || /\b(?:npm|pnpm|yarn|pip3?|cargo|gem|composer)\s+(?:install|add|require)\b/i.test(text2);
|
|
2141
|
+
}
|
|
2029
2142
|
function manifestPathspecs(doc) {
|
|
2030
2143
|
const scope = documentScope(doc);
|
|
2031
2144
|
if (scope === ".") return MANIFEST_PATHSPECS;
|
|
@@ -2052,6 +2165,19 @@ async function checkDepsChanged(ctx) {
|
|
|
2052
2165
|
reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
|
|
2053
2166
|
});
|
|
2054
2167
|
}
|
|
2168
|
+
let content2 = doc.content;
|
|
2169
|
+
if (doc.dirty) {
|
|
2170
|
+
try {
|
|
2171
|
+
content2 = (await inspectionGit(
|
|
2172
|
+
["show", `${doc.lastCommit.hash}:${doc.path}`],
|
|
2173
|
+
{ cwd: ctx.root, maxBuffer: 10 * 1024 * 1024, timeout: 1e4 }
|
|
2174
|
+
)).stdout;
|
|
2175
|
+
} catch {
|
|
2176
|
+
result.skipped.push({ check: "deps-changed", doc: doc.path, reason: `${doc.path}: committed dependency content is unavailable` });
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
if (!hasDependencyContent(content2)) continue;
|
|
2055
2181
|
const range = await commitsTouchingSince(
|
|
2056
2182
|
ctx.root,
|
|
2057
2183
|
doc.lastCommit.hash,
|
|
@@ -2097,6 +2223,7 @@ var init_deps_changed = __esm({
|
|
|
2097
2223
|
init_docs();
|
|
2098
2224
|
init_release_metadata();
|
|
2099
2225
|
init_git();
|
|
2226
|
+
init_inspection();
|
|
2100
2227
|
init_checks();
|
|
2101
2228
|
MANIFEST_COMMITS_CAP = 10;
|
|
2102
2229
|
MANIFEST_PATHSPECS = [
|
|
@@ -2236,7 +2363,9 @@ function assessTrust(entry, freshness) {
|
|
|
2236
2363
|
}
|
|
2237
2364
|
function trustHint(states) {
|
|
2238
2365
|
const parts = [];
|
|
2239
|
-
if (states.some((s) => s.verification === "failed")) parts.push("Verification failed for returned entries; do not rely on those descriptions until corrected.");
|
|
2366
|
+
if (states.some((s) => s.verification === "failed" || s.recordedVerdict === "failed")) parts.push("Verification failed for returned entries; do not rely on those descriptions until corrected.");
|
|
2367
|
+
if (states.some((s) => s.verification === "stale")) parts.push("Recorded verification evidence changed; use verify_snapshot and review the current evidence before reusing those verdicts.");
|
|
2368
|
+
if (states.some((s) => s.verification === "unknown")) parts.push("Recorded verification evidence cannot be confirmed; inspect the files and obtain a fresh review before relying on those verdicts.");
|
|
2240
2369
|
if (states.some((s) => s.freshness === "unknown")) parts.push("Freshness is unknown for some returned entries; inspect their files before relying on them.");
|
|
2241
2370
|
if (states.some((s) => s.freshness === "changed")) parts.push("Some returned entries have changed files, including possible local edits; verify against the current code.");
|
|
2242
2371
|
if (!parts.length) parts.push("No changes detected in the returned anchors. This does not prove the descriptions are correct.");
|
|
@@ -2461,7 +2590,7 @@ __export(decisions_exports, {
|
|
|
2461
2590
|
upsertDecision: () => upsertDecision,
|
|
2462
2591
|
withDecisionWrite: () => withDecisionWrite
|
|
2463
2592
|
});
|
|
2464
|
-
import
|
|
2593
|
+
import fs9 from "fs/promises";
|
|
2465
2594
|
import path17 from "path";
|
|
2466
2595
|
import { createHash } from "crypto";
|
|
2467
2596
|
import { z as z3 } from "zod";
|
|
@@ -2470,7 +2599,7 @@ async function loadDecisionStore(rootDir) {
|
|
|
2470
2599
|
const diagnostics = [];
|
|
2471
2600
|
let entries;
|
|
2472
2601
|
try {
|
|
2473
|
-
entries = await
|
|
2602
|
+
entries = await fs9.readdir(await storePath(rootDir, ".mason/decisions"));
|
|
2474
2603
|
} catch (error) {
|
|
2475
2604
|
if (error.code !== "ENOENT") diagnostics.push({ path: ".mason/decisions", message: String(error) });
|
|
2476
2605
|
return { records, diagnostics };
|
|
@@ -2529,7 +2658,7 @@ async function withDecisionWrite(root, operation) {
|
|
|
2529
2658
|
const lockPath = await storePath(root, ".mason/decisions/.write-lock", true);
|
|
2530
2659
|
let lock;
|
|
2531
2660
|
try {
|
|
2532
|
-
lock = await
|
|
2661
|
+
lock = await fs9.open(lockPath, "wx", 384);
|
|
2533
2662
|
} catch (error) {
|
|
2534
2663
|
if (error.code === "EEXIST") return { status: "error", error: "Decision store is locked by another write. Retry after it finishes; an abandoned .mason/decisions/.write-lock must be removed only after confirming no writer is running." };
|
|
2535
2664
|
throw error;
|
|
@@ -2538,7 +2667,7 @@ async function withDecisionWrite(root, operation) {
|
|
|
2538
2667
|
return await operation();
|
|
2539
2668
|
} finally {
|
|
2540
2669
|
await lock.close();
|
|
2541
|
-
await
|
|
2670
|
+
await fs9.unlink(lockPath);
|
|
2542
2671
|
}
|
|
2543
2672
|
}
|
|
2544
2673
|
async function upsertDecision(rootDir, input) {
|
|
@@ -2560,7 +2689,7 @@ async function upsertDecision(rootDir, input) {
|
|
|
2560
2689
|
if (input.files && files.length < input.files.length) warnings.push("some anchor paths were outside the repo or duplicated and were dropped");
|
|
2561
2690
|
for (const file of files) {
|
|
2562
2691
|
try {
|
|
2563
|
-
await
|
|
2692
|
+
await fs9.access(path17.join(rootDir, file));
|
|
2564
2693
|
} catch {
|
|
2565
2694
|
warnings.push(`anchor file does not exist on disk: ${file}`);
|
|
2566
2695
|
}
|
|
@@ -2910,8 +3039,6 @@ var init_findings = __esm({
|
|
|
2910
3039
|
});
|
|
2911
3040
|
|
|
2912
3041
|
// src/automation/store.ts
|
|
2913
|
-
import fs9 from "fs/promises";
|
|
2914
|
-
import os from "os";
|
|
2915
3042
|
import { z as z5 } from "zod";
|
|
2916
3043
|
function parseState(raw) {
|
|
2917
3044
|
try {
|
|
@@ -2920,65 +3047,14 @@ function parseState(raw) {
|
|
|
2920
3047
|
throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
|
|
2921
3048
|
}
|
|
2922
3049
|
}
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
const deadline = Date.now() + waitMs;
|
|
2926
|
-
let handle;
|
|
2927
|
-
while (!handle) {
|
|
2928
|
-
try {
|
|
2929
|
-
handle = await fs9.open(file, "wx", 384);
|
|
2930
|
-
try {
|
|
2931
|
-
await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
|
|
2932
|
-
} catch (error) {
|
|
2933
|
-
await handle.close().catch(() => {
|
|
2934
|
-
});
|
|
2935
|
-
handle = void 0;
|
|
2936
|
-
await fs9.rm(file, { force: true }).catch(() => {
|
|
2937
|
-
});
|
|
2938
|
-
throw error;
|
|
2939
|
-
}
|
|
2940
|
-
} catch (error) {
|
|
2941
|
-
if (error.code !== "EEXIST") throw error;
|
|
2942
|
-
try {
|
|
2943
|
-
const owner = JSON.parse(await fs9.readFile(file, "utf8"));
|
|
2944
|
-
if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
2945
|
-
try {
|
|
2946
|
-
process.kill(owner.pid, 0);
|
|
2947
|
-
} catch (probe) {
|
|
2948
|
-
if (probe.code === "ESRCH") {
|
|
2949
|
-
const reclaim = file + ".reclaim";
|
|
2950
|
-
let guard;
|
|
2951
|
-
try {
|
|
2952
|
-
guard = await fs9.open(reclaim, "wx", 384);
|
|
2953
|
-
const current = JSON.parse(await fs9.readFile(file, "utf8"));
|
|
2954
|
-
if (current.pid === owner.pid && current.host === owner.host) await fs9.unlink(file);
|
|
2955
|
-
} finally {
|
|
2956
|
-
if (guard) {
|
|
2957
|
-
await guard.close();
|
|
2958
|
-
await fs9.rm(reclaim, { force: true });
|
|
2959
|
-
}
|
|
2960
|
-
}
|
|
2961
|
-
}
|
|
2962
|
-
}
|
|
2963
|
-
}
|
|
2964
|
-
} catch {
|
|
2965
|
-
}
|
|
2966
|
-
if (Date.now() >= deadline) throw new Error("Automation is busy or its lock needs inspection: " + file);
|
|
2967
|
-
await new Promise((resolve2) => setTimeout(resolve2, 40));
|
|
2968
|
-
}
|
|
2969
|
-
}
|
|
2970
|
-
try {
|
|
2971
|
-
return await run();
|
|
2972
|
-
} finally {
|
|
2973
|
-
await handle.close();
|
|
2974
|
-
await fs9.unlink(file);
|
|
2975
|
-
}
|
|
3050
|
+
function withLock(root, directory, run, waitMs = 5e3) {
|
|
3051
|
+
return withStoreLock(root, directory, run, waitMs, "Automation");
|
|
2976
3052
|
}
|
|
2977
3053
|
var hostSchema, events, stateSchema;
|
|
2978
3054
|
var init_store = __esm({
|
|
2979
3055
|
"src/automation/store.ts"() {
|
|
2980
3056
|
"use strict";
|
|
2981
|
-
|
|
3057
|
+
init_store_lock();
|
|
2982
3058
|
hostSchema = z5.enum(["claude", "codex"]);
|
|
2983
3059
|
events = ["session_start", "turn_start", "before_tool", "after_tool", "task_end"];
|
|
2984
3060
|
stateSchema = z5.object({
|
|
@@ -3510,7 +3586,7 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
3510
3586
|
return {
|
|
3511
3587
|
...base,
|
|
3512
3588
|
status: "review-required",
|
|
3513
|
-
reason: "An audit cannot establish that this advisory was reviewed. Retain its original evidence and report a separate assessment; editing or committing the doc is not approval."
|
|
3589
|
+
reason: (now ? "The current check still reports this advisory. " : finding.type === "deps-changed" ? "The current check no longer reports this condition; the historical advisory remains awaiting assessment. " : "The current check no longer reports the original finding; retained evidence still requires assessment. ") + "An audit cannot establish that this advisory was reviewed. Retain its original evidence and report a separate assessment; editing or committing the doc is not approval."
|
|
3514
3590
|
};
|
|
3515
3591
|
}
|
|
3516
3592
|
return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
|
|
@@ -3701,7 +3777,7 @@ async function collectInputs(root) {
|
|
|
3701
3777
|
git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
|
|
3702
3778
|
git3(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
|
|
3703
3779
|
]) : ["", ""];
|
|
3704
|
-
const common = [
|
|
3780
|
+
const common = [6, engineVersion, head, shallow, replacements, docContents];
|
|
3705
3781
|
const keys = {
|
|
3706
3782
|
"deleted-reference": hash([common, claims, docStatus]),
|
|
3707
3783
|
"new-module": hash([common, modules]),
|
|
@@ -3761,7 +3837,7 @@ var init_evidence = __esm({
|
|
|
3761
3837
|
init_dead_command();
|
|
3762
3838
|
init_storage();
|
|
3763
3839
|
init_advisory_review();
|
|
3764
|
-
engineVersion = true ? "0.
|
|
3840
|
+
engineVersion = true ? "0.19.0" : "development";
|
|
3765
3841
|
hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
|
|
3766
3842
|
content = readAuditInput;
|
|
3767
3843
|
cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
|
|
@@ -3771,18 +3847,49 @@ var init_evidence = __esm({
|
|
|
3771
3847
|
// src/setup/model.ts
|
|
3772
3848
|
var model_exports = {};
|
|
3773
3849
|
__export(model_exports, {
|
|
3850
|
+
INACTIVE_HOSTS_PATH: () => INACTIVE_HOSTS_PATH,
|
|
3774
3851
|
SETUP_PATH: () => SETUP_PATH,
|
|
3852
|
+
effectiveSetup: () => effectiveSetup,
|
|
3853
|
+
inactiveHostsSchema: () => inactiveHostsSchema,
|
|
3775
3854
|
loadSetup: () => loadSetup,
|
|
3776
3855
|
loadSetupReceipt: () => loadSetupReceipt,
|
|
3777
3856
|
setupHostSchema: () => setupHostSchema,
|
|
3778
3857
|
setupSchema: () => setupSchema
|
|
3779
3858
|
});
|
|
3780
3859
|
import { z as z9 } from "zod";
|
|
3860
|
+
import fs14 from "fs/promises";
|
|
3861
|
+
import path22 from "path";
|
|
3781
3862
|
async function loadSetup(root) {
|
|
3782
3863
|
const raw = await readStoreJson(root, SETUP_PATH);
|
|
3783
3864
|
if (raw === null) return null;
|
|
3784
3865
|
return setupSchema.parse(raw);
|
|
3785
3866
|
}
|
|
3867
|
+
async function effectiveSetup(root) {
|
|
3868
|
+
const local = await loadSetup(root);
|
|
3869
|
+
if (local) return { root, setup: local };
|
|
3870
|
+
const common = await fs14.realpath(path22.resolve(root, (await git3(root, "rev-parse", "--git-common-dir")).trim()));
|
|
3871
|
+
const own = await fs14.realpath((await git3(root, "rev-parse", "--absolute-git-dir")).trim());
|
|
3872
|
+
if (common === own) return { root, setup: null };
|
|
3873
|
+
const primary = (await git3(root, "worktree", "list", "--porcelain", "-z")).split("\0\0")[0].split("\0");
|
|
3874
|
+
const candidate = primary.find((field) => field.startsWith("worktree "))?.slice(9);
|
|
3875
|
+
if (!candidate || primary.includes("bare")) return { root, setup: null };
|
|
3876
|
+
let inheritedRoot;
|
|
3877
|
+
try {
|
|
3878
|
+
inheritedRoot = await fs14.realpath((await git3(candidate, "rev-parse", "--show-toplevel")).trim());
|
|
3879
|
+
} catch (error) {
|
|
3880
|
+
if (/must be run in a work tree/.test(String(error.stderr))) return { root, setup: null };
|
|
3881
|
+
throw error;
|
|
3882
|
+
}
|
|
3883
|
+
const inheritedCommon = await fs14.realpath(path22.resolve(
|
|
3884
|
+
inheritedRoot,
|
|
3885
|
+
(await git3(inheritedRoot, "rev-parse", "--git-common-dir")).trim()
|
|
3886
|
+
));
|
|
3887
|
+
if (inheritedCommon !== common) throw new Error("Linked worktree setup does not belong to the same Git repository.");
|
|
3888
|
+
const setup = await loadSetup(inheritedRoot);
|
|
3889
|
+
const inactive = await readStoreJson(root, INACTIVE_HOSTS_PATH);
|
|
3890
|
+
if (inactive && setup) for (const host of inactiveHostsSchema.parse(inactive).hosts) delete setup.hosts[host];
|
|
3891
|
+
return { root: inheritedRoot, setup };
|
|
3892
|
+
}
|
|
3786
3893
|
async function loadSetupReceipt(root, directory, host) {
|
|
3787
3894
|
const raw = await readStoreJson(root, directory + "/setup-" + host + ".json");
|
|
3788
3895
|
if (raw === null) return null;
|
|
@@ -3790,10 +3897,11 @@ async function loadSetupReceipt(root, directory, host) {
|
|
|
3790
3897
|
if (receipt.root !== root || receipt.host !== host) throw new Error("Setup receipt belongs to another installation.");
|
|
3791
3898
|
return receipt;
|
|
3792
3899
|
}
|
|
3793
|
-
var setupHostSchema, setupSchema, SETUP_PATH, receiptSchema;
|
|
3900
|
+
var setupHostSchema, setupSchema, SETUP_PATH, INACTIVE_HOSTS_PATH, inactiveHostsSchema, receiptSchema;
|
|
3794
3901
|
var init_model = __esm({
|
|
3795
3902
|
"src/setup/model.ts"() {
|
|
3796
3903
|
"use strict";
|
|
3904
|
+
init_evidence();
|
|
3797
3905
|
init_storage();
|
|
3798
3906
|
setupHostSchema = z9.object({
|
|
3799
3907
|
configuredVersion: z9.string(),
|
|
@@ -3807,6 +3915,8 @@ var init_model = __esm({
|
|
|
3807
3915
|
claude: setupHostSchema.optional()
|
|
3808
3916
|
}) });
|
|
3809
3917
|
SETUP_PATH = ".mason/local/setup.json";
|
|
3918
|
+
INACTIVE_HOSTS_PATH = ".mason/local/inactive-hosts.json";
|
|
3919
|
+
inactiveHostsSchema = z9.object({ version: z9.literal(1), hosts: z9.array(z9.enum(["claude", "codex"])) });
|
|
3810
3920
|
receiptSchema = z9.object({
|
|
3811
3921
|
version: z9.literal(1),
|
|
3812
3922
|
host: z9.enum(["codex", "claude"]),
|
|
@@ -3823,7 +3933,7 @@ var init_model = __esm({
|
|
|
3823
3933
|
// src/setup/launcher.ts
|
|
3824
3934
|
import { execFile as execFile2 } from "child_process";
|
|
3825
3935
|
import { promisify as promisify2 } from "util";
|
|
3826
|
-
import
|
|
3936
|
+
import fs15 from "fs/promises";
|
|
3827
3937
|
function globalCommand(args, platform = process.platform) {
|
|
3828
3938
|
return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args] } : { command: "mason", args };
|
|
3829
3939
|
}
|
|
@@ -3853,12 +3963,124 @@ var init_launcher = __esm({
|
|
|
3853
3963
|
"use strict";
|
|
3854
3964
|
init_evidence();
|
|
3855
3965
|
init_model();
|
|
3856
|
-
executingVersion = () => true ? "0.
|
|
3966
|
+
executingVersion = () => true ? "0.19.0" : "development";
|
|
3857
3967
|
mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
|
|
3858
3968
|
hookCommand = (host) => `mason --setup-host ${host} auto`;
|
|
3859
3969
|
}
|
|
3860
3970
|
});
|
|
3861
3971
|
|
|
3972
|
+
// src/snapshot/review.ts
|
|
3973
|
+
import { createHash as createHash5 } from "crypto";
|
|
3974
|
+
function snapshotEntryContent(entry) {
|
|
3975
|
+
return {
|
|
3976
|
+
description: entry.description,
|
|
3977
|
+
files: "files" in entry ? entry.files : void 0,
|
|
3978
|
+
chain: "chain" in entry ? entry.chain : void 0,
|
|
3979
|
+
tests: "files" in entry ? entry.tests : void 0,
|
|
3980
|
+
type: "files" in entry ? normalizeFeatureType(entry.type) : void 0
|
|
3981
|
+
};
|
|
3982
|
+
}
|
|
3983
|
+
function reviewToken(kind, name, entry, evidence) {
|
|
3984
|
+
return createHash5("sha256").update(JSON.stringify({
|
|
3985
|
+
version: 1,
|
|
3986
|
+
kind,
|
|
3987
|
+
name,
|
|
3988
|
+
entry: snapshotEntryContent(entry),
|
|
3989
|
+
evidence
|
|
3990
|
+
})).digest("hex");
|
|
3991
|
+
}
|
|
3992
|
+
async function prepareSnapshotReview(access, kind, name, entry) {
|
|
3993
|
+
const files = "files" in entry ? entry.files : entry.chain;
|
|
3994
|
+
const evidence = [];
|
|
3995
|
+
const skeletons = [];
|
|
3996
|
+
for (const file of files.slice(0, VERIFY_MAX_FILES_PER_ENTRY)) {
|
|
3997
|
+
const full = await access.read(file);
|
|
3998
|
+
evidence.push({ path: file, hash: full ? sourceHash(full.content) : null });
|
|
3999
|
+
skeletons.push(full ? { path: full.path, content: full.content.slice(0, VERIFY_SKELETON_CHARS) } : { path: file, missing: true });
|
|
4000
|
+
}
|
|
4001
|
+
return { reviewToken: reviewToken(kind, name, entry, evidence), skeletons, truncated: files.length > VERIFY_MAX_FILES_PER_ENTRY };
|
|
4002
|
+
}
|
|
4003
|
+
function createSnapshotEvidenceReader(access) {
|
|
4004
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
4005
|
+
return async (kind, name, entry) => {
|
|
4006
|
+
const files = "files" in entry ? entry.files : entry.chain;
|
|
4007
|
+
const evidence = [];
|
|
4008
|
+
for (const file of files.slice(0, VERIFY_MAX_FILES_PER_ENTRY)) {
|
|
4009
|
+
let hash2 = hashes.get(file);
|
|
4010
|
+
if (!hash2) {
|
|
4011
|
+
hash2 = access.read(file).then((full) => full ? sourceHash(full.content) : null);
|
|
4012
|
+
hashes.set(file, hash2);
|
|
4013
|
+
}
|
|
4014
|
+
evidence.push({ path: file, hash: await hash2 });
|
|
4015
|
+
}
|
|
4016
|
+
return {
|
|
4017
|
+
reviewToken: reviewToken(kind, name, entry, evidence),
|
|
4018
|
+
evidenceAvailable: evidence.length > 0 && evidence.every((file) => file.hash !== null)
|
|
4019
|
+
};
|
|
4020
|
+
};
|
|
4021
|
+
}
|
|
4022
|
+
var VERIFY_MAX_FILES_PER_ENTRY, VERIFY_SKELETON_CHARS, sourceHash;
|
|
4023
|
+
var init_review = __esm({
|
|
4024
|
+
"src/snapshot/review.ts"() {
|
|
4025
|
+
"use strict";
|
|
4026
|
+
init_snapshot();
|
|
4027
|
+
VERIFY_MAX_FILES_PER_ENTRY = 8;
|
|
4028
|
+
VERIFY_SKELETON_CHARS = 500;
|
|
4029
|
+
sourceHash = (content2) => createHash5("sha256").update(content2).digest("hex");
|
|
4030
|
+
}
|
|
4031
|
+
});
|
|
4032
|
+
|
|
4033
|
+
// src/snapshot/trust.ts
|
|
4034
|
+
function createSnapshotTrustReader(root) {
|
|
4035
|
+
let reader;
|
|
4036
|
+
return async (kind, name, entry, freshness) => {
|
|
4037
|
+
const trust = assessTrust(entry, freshness);
|
|
4038
|
+
const verdict = entry.verificationFailed ? "failed" : entry.verifiedAt ? "passed" : void 0;
|
|
4039
|
+
if (!verdict) return trust;
|
|
4040
|
+
trust.recordedVerdict = verdict;
|
|
4041
|
+
const unknown = (reason2) => {
|
|
4042
|
+
trust.verification = "unknown";
|
|
4043
|
+
trust.reasons.push(reason2);
|
|
4044
|
+
return trust;
|
|
4045
|
+
};
|
|
4046
|
+
if (!entry.verificationToken || !/^[a-f0-9]{64}$/.test(entry.verificationToken)) return unknown("The recorded verdict has no supported evidence token; obtain a fresh verify_snapshot review.");
|
|
4047
|
+
try {
|
|
4048
|
+
reader ??= createFileAccess(root).then(createSnapshotEvidenceReader).catch(() => null);
|
|
4049
|
+
const readReview = await reader;
|
|
4050
|
+
if (!readReview) return unknown("Verification source evidence could not be read under the current file policy.");
|
|
4051
|
+
const review = await readReview(kind, name, entry);
|
|
4052
|
+
if (!review.evidenceAvailable) {
|
|
4053
|
+
return unknown("Verification source evidence is missing, excluded, unreadable, or empty; the recorded verdict cannot be confirmed.");
|
|
4054
|
+
}
|
|
4055
|
+
if (review.reviewToken !== entry.verificationToken) {
|
|
4056
|
+
trust.verification = "stale";
|
|
4057
|
+
trust.reasons.push("The entry or sampled source contents differ from the recorded verification evidence; obtain a fresh review.");
|
|
4058
|
+
}
|
|
4059
|
+
return trust;
|
|
4060
|
+
} catch {
|
|
4061
|
+
return unknown("Verification source evidence could not be read under the current file policy.");
|
|
4062
|
+
}
|
|
4063
|
+
};
|
|
4064
|
+
}
|
|
4065
|
+
async function readSnapshotTrustIndex(readTrust, snapshot, drift) {
|
|
4066
|
+
const features = /* @__PURE__ */ Object.create(null), flows = /* @__PURE__ */ Object.create(null);
|
|
4067
|
+
for (const [name, entry] of Object.entries(snapshot.features)) {
|
|
4068
|
+
features[name] = await readTrust("feature", name, entry, drift?.featureFreshness?.[name] ?? "unknown");
|
|
4069
|
+
}
|
|
4070
|
+
for (const [name, entry] of Object.entries(snapshot.flows)) {
|
|
4071
|
+
flows[name] = await readTrust("flow", name, entry, drift?.flowFreshness?.[name] ?? "unknown");
|
|
4072
|
+
}
|
|
4073
|
+
return { features, flows };
|
|
4074
|
+
}
|
|
4075
|
+
var init_trust2 = __esm({
|
|
4076
|
+
"src/snapshot/trust.ts"() {
|
|
4077
|
+
"use strict";
|
|
4078
|
+
init_trust();
|
|
4079
|
+
init_files();
|
|
4080
|
+
init_review();
|
|
4081
|
+
}
|
|
4082
|
+
});
|
|
4083
|
+
|
|
3862
4084
|
// src/setup/files.ts
|
|
3863
4085
|
var files_exports = {};
|
|
3864
4086
|
__export(files_exports, {
|
|
@@ -3866,8 +4088,8 @@ __export(files_exports, {
|
|
|
3866
4088
|
managedBlock: () => managedBlock,
|
|
3867
4089
|
readText: () => readText
|
|
3868
4090
|
});
|
|
3869
|
-
import
|
|
3870
|
-
import
|
|
4091
|
+
import fs18 from "fs/promises";
|
|
4092
|
+
import path26 from "path";
|
|
3871
4093
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
3872
4094
|
async function readText(root, file) {
|
|
3873
4095
|
try {
|
|
@@ -3896,14 +4118,14 @@ async function applyEdit(root, edit) {
|
|
|
3896
4118
|
if (edit.after === null) {
|
|
3897
4119
|
const file2 = await storePath(root, edit.path);
|
|
3898
4120
|
if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
|
|
3899
|
-
await
|
|
4121
|
+
await fs18.unlink(file2);
|
|
3900
4122
|
return true;
|
|
3901
4123
|
}
|
|
3902
4124
|
const file = await storePath(root, edit.path, true);
|
|
3903
|
-
const temporary =
|
|
4125
|
+
const temporary = path26.join(path26.dirname(file), ".mason-setup-" + randomUUID3() + ".tmp");
|
|
3904
4126
|
try {
|
|
3905
|
-
const mode = await
|
|
3906
|
-
const handle = await
|
|
4127
|
+
const mode = await fs18.stat(file).then((s) => s.mode & 511, () => 384);
|
|
4128
|
+
const handle = await fs18.open(temporary, "wx", mode);
|
|
3907
4129
|
try {
|
|
3908
4130
|
await handle.writeFile(edit.after, "utf8");
|
|
3909
4131
|
await handle.sync();
|
|
@@ -3911,10 +4133,10 @@ async function applyEdit(root, edit) {
|
|
|
3911
4133
|
await handle.close();
|
|
3912
4134
|
}
|
|
3913
4135
|
if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
|
|
3914
|
-
await
|
|
4136
|
+
await fs18.rename(temporary, file);
|
|
3915
4137
|
return true;
|
|
3916
4138
|
} finally {
|
|
3917
|
-
await
|
|
4139
|
+
await fs18.rm(temporary, { force: true });
|
|
3918
4140
|
}
|
|
3919
4141
|
}
|
|
3920
4142
|
var init_files2 = __esm({
|
|
@@ -3936,7 +4158,7 @@ __export(execution_exports, {
|
|
|
3936
4158
|
recordExecution: () => recordExecution
|
|
3937
4159
|
});
|
|
3938
4160
|
import os2 from "os";
|
|
3939
|
-
import
|
|
4161
|
+
import fs19 from "fs/promises";
|
|
3940
4162
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
3941
4163
|
import { z as z12 } from "zod";
|
|
3942
4164
|
function parseExecution(raw) {
|
|
@@ -4064,7 +4286,7 @@ async function recordExecution(root, directory, event, run) {
|
|
|
4064
4286
|
throw Object.assign(new Error(failure.message, { cause: error }), { failure });
|
|
4065
4287
|
}
|
|
4066
4288
|
} finally {
|
|
4067
|
-
await
|
|
4289
|
+
await fs19.rm(await storePath(root, owner), { force: true }).catch(() => {
|
|
4068
4290
|
});
|
|
4069
4291
|
}
|
|
4070
4292
|
}
|
|
@@ -4148,13 +4370,17 @@ async function observeReadOnlyTool(dir, event) {
|
|
|
4148
4370
|
}
|
|
4149
4371
|
function summarize(report) {
|
|
4150
4372
|
const open = report.findings.filter((f) => f.status !== "resolved");
|
|
4373
|
+
const onlyReview = report.status === "incomplete" && !report.diagnostics.length && !report.counts.unresolved && !report.counts.unverified && report.counts["review-required"] > 0;
|
|
4151
4374
|
return [
|
|
4152
|
-
`Mason: ${report.status}; ${report.counts.unresolved} unresolved, ${report.counts["review-required"]} need review, ${report.counts.unverified} unverified.`,
|
|
4153
|
-
...open.slice(0, 4).map((f) =>
|
|
4375
|
+
onlyReview ? `Mason: ${report.counts["review-required"]} advisor${report.counts["review-required"] === 1 ? "y" : "ies"} awaiting review; no unresolved issues.` : `Mason: ${report.status}; ${report.counts.unresolved} unresolved, ${report.counts["review-required"]} need review, ${report.counts.unverified} unverified.`,
|
|
4376
|
+
...open.slice(0, 4).map((f) => {
|
|
4377
|
+
const detail = f.current ? f.current.message : f.status === "review-required" ? `${f.original.type}: current check no longer reports ${f.original.type === "deps-changed" ? "this condition" : "the original finding"}; historical evidence awaits assessment.` : `Original evidence: ${f.original.message}. ${f.reason}`;
|
|
4378
|
+
return `[${f.status}] ${cleanText(f.original.anchor.doc)}: ${cleanText(detail)}`;
|
|
4379
|
+
}),
|
|
4154
4380
|
...open.length > 4 ? [`${open.length - 4} more findings in the report.`] : [],
|
|
4155
4381
|
...report.diagnostics.slice(0, 2).map(cleanText),
|
|
4156
4382
|
`Evidence: ${report.reportPath}. Resume/check with mason_automation(action: "check") or mason-auto check.`,
|
|
4157
|
-
"Keep original evidence. Address findings relevant to the authorized task; report unrelated findings and
|
|
4383
|
+
"Keep original evidence. Address findings relevant to the authorized task; report unrelated findings with a brief scope reason and suggested follow-up. Use review_advisory for authorized assessments; otherwise leave advisories open without approving them."
|
|
4158
4384
|
].join("\n");
|
|
4159
4385
|
}
|
|
4160
4386
|
async function loadState(ws) {
|
|
@@ -4378,7 +4604,7 @@ async function automate(dir, event) {
|
|
|
4378
4604
|
if (event.event === "after_tool" && event.mutating) {
|
|
4379
4605
|
session.mutationObserved = true;
|
|
4380
4606
|
if (!event.toolId || !session.pending[event.toolId]) {
|
|
4381
|
-
const gap = "A tool completed without an observed matching pre-tool capture; pre-edit coverage is unknown.";
|
|
4607
|
+
const gap = "A tool completed without an observed matching pre-tool capture in this checkout; pre-edit coverage is unknown. The tool may have changed checkouts; captures are kept separately for each worktree.";
|
|
4382
4608
|
if (!session.coverageGaps.includes(gap)) session.coverageGaps.push(gap);
|
|
4383
4609
|
}
|
|
4384
4610
|
if (event.toolId) delete session.pending[event.toolId];
|
|
@@ -4487,8 +4713,8 @@ var init_runtime = __esm({
|
|
|
4487
4713
|
});
|
|
4488
4714
|
|
|
4489
4715
|
// src/automation/hook-root.ts
|
|
4490
|
-
import
|
|
4491
|
-
import
|
|
4716
|
+
import fs20 from "fs/promises";
|
|
4717
|
+
import path27 from "path";
|
|
4492
4718
|
var init_hook_root = __esm({
|
|
4493
4719
|
"src/automation/hook-root.ts"() {
|
|
4494
4720
|
"use strict";
|
|
@@ -4893,7 +5119,7 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
|
|
|
4893
5119
|
- Before editing a file \u2192 \`get_impact\` to check references, tests, and historical change partners.
|
|
4894
5120
|
- Investigation or correction reveals a reusable project lesson \u2192 \`save_decision\` before finishing. Capture only the lasting constraint and reason, with anchors and known sources, owner, and recorder. This creates a proposal; never infer approval. Preserve uncertain causes and unknown attribution. Route project lessons here; keep personal preferences in agent memory. Skip code summaries, current checkout status, temporary task permissions, session trivia, and secrets. No qualifying lesson means no record.
|
|
4895
5121
|
- Before saving, compare the lesson with matching \`get_context\` records and any pending proposal. If new evidence changes the same lesson's assumptions, scope, or recommended action, revise it with \`save_decision(id: existingId)\`, including when it is accepted. Preserve supported rationale and sources, replace obsolete instructions, and add the new evidence. Create a separate record for a genuinely distinct lesson; skip unchanged restatements. Revision preserves history and leaves the accepted version operative until authorized review.
|
|
4896
|
-
- Consult trust metadata before relying on entries: unknown or changed freshness requires inspection, and failed
|
|
5122
|
+
- Consult trust metadata before relying on entries: unknown or changed freshness requires inspection, stale or unknown map verification requires a fresh evidence review, and a recorded failed verdict means the description must be corrected. Check approval too: proposals are suggestions, legacy records are unreviewed, and accepted decisions are recorded constraints subject to freshness checks. An accepted revision remains operative while a pending proposal is reviewed; keep both versions and their freshness distinct.
|
|
4897
5123
|
- Asked to review or re-verify a decision \u2192 \`review_decision\` first to inspect content, sources, history, and code changes. Record acceptance, reaffirmation, or retirement only when authorized by the user or a cited team review, with the actual reviewer and reason. Never infer approval from unchanged code. Review and commit the local record through the normal project workflow.
|
|
4898
5124
|
- For an architectural overview, use \`get_snapshot\` if a map is available. If \`map.status\` is missing or invalid, use available decisions and source evidence; do not start building a map unless requested.
|
|
4899
5125
|
- \`mason_init\` returns documentation audit and committed-diff review results, plus a short setup guide. Pass \`evidence\` with local CI manifest paths to include test and analysis results; the CLI equivalent is \`mason-review --evidence <manifest>\`. State skipped, unavailable, stale, or unknown checks explicitly. Related accepted decisions identify review context, not proven violations.
|
|
@@ -5331,10 +5557,10 @@ var init_check = __esm({
|
|
|
5331
5557
|
});
|
|
5332
5558
|
|
|
5333
5559
|
// src/review/evidence.ts
|
|
5334
|
-
import
|
|
5560
|
+
import path28 from "path";
|
|
5335
5561
|
import { z as z22 } from "zod";
|
|
5336
5562
|
function relativeArtifact(root, file) {
|
|
5337
|
-
const relative =
|
|
5563
|
+
const relative = path28.isAbsolute(file) ? path28.relative(root, file) : file;
|
|
5338
5564
|
const normalized = normalizeRepoPath(relative);
|
|
5339
5565
|
if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
|
|
5340
5566
|
return normalized;
|
|
@@ -5431,7 +5657,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
5431
5657
|
const report = await readStoreJson(root, file);
|
|
5432
5658
|
if (report === null) throw new Error(`Report artifact is missing: ${file}`);
|
|
5433
5659
|
const sourceRoot = check.sourceRoot ?? root;
|
|
5434
|
-
if (!
|
|
5660
|
+
if (!path28.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
|
|
5435
5661
|
const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : check.report.format === "mason-check-json" ? parseCheck(report, sourceRoot) : parseSarif(report, sourceRoot);
|
|
5436
5662
|
result.outcome = parsed.outcome;
|
|
5437
5663
|
result.counts = parsed.counts;
|
|
@@ -5552,8 +5778,8 @@ var init_evidence2 = __esm({
|
|
|
5552
5778
|
});
|
|
5553
5779
|
|
|
5554
5780
|
// src/review/review.ts
|
|
5555
|
-
import
|
|
5556
|
-
import
|
|
5781
|
+
import fs21 from "fs/promises";
|
|
5782
|
+
import path29 from "path";
|
|
5557
5783
|
import { execFile as execFile7 } from "child_process";
|
|
5558
5784
|
import { promisify as promisify7 } from "util";
|
|
5559
5785
|
async function resolveMergeBase(resolvedRoot, base, head) {
|
|
@@ -5582,7 +5808,7 @@ function anchorsTouched(record, changedFiles) {
|
|
|
5582
5808
|
return matchingPaths(record.files, changedFiles);
|
|
5583
5809
|
}
|
|
5584
5810
|
async function computeReview(rootDir, base, options = {}) {
|
|
5585
|
-
const resolvedRoot =
|
|
5811
|
+
const resolvedRoot = path29.resolve(rootDir);
|
|
5586
5812
|
const head = await getCurrentGitHash(resolvedRoot);
|
|
5587
5813
|
if (head === "unknown") return null;
|
|
5588
5814
|
const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
|
|
@@ -5632,7 +5858,7 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
5632
5858
|
analyzed,
|
|
5633
5859
|
async (relPath) => {
|
|
5634
5860
|
try {
|
|
5635
|
-
await
|
|
5861
|
+
await fs21.access(path29.join(resolvedRoot, relPath));
|
|
5636
5862
|
return true;
|
|
5637
5863
|
} catch {
|
|
5638
5864
|
return false;
|
|
@@ -5666,7 +5892,7 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
5666
5892
|
return finalize();
|
|
5667
5893
|
}
|
|
5668
5894
|
var exec5, MAX_ANALYZED_FILES;
|
|
5669
|
-
var
|
|
5895
|
+
var init_review2 = __esm({
|
|
5670
5896
|
"src/review/review.ts"() {
|
|
5671
5897
|
"use strict";
|
|
5672
5898
|
init_drift();
|
|
@@ -5755,7 +5981,7 @@ var init_onboarding = __esm({
|
|
|
5755
5981
|
"src/mcp/onboarding.ts"() {
|
|
5756
5982
|
"use strict";
|
|
5757
5983
|
init_audit();
|
|
5758
|
-
|
|
5984
|
+
init_review2();
|
|
5759
5985
|
init_drift();
|
|
5760
5986
|
init_snapshot();
|
|
5761
5987
|
init_provenance();
|
|
@@ -5767,7 +5993,7 @@ var init_onboarding = __esm({
|
|
|
5767
5993
|
});
|
|
5768
5994
|
|
|
5769
5995
|
// src/impact/references.ts
|
|
5770
|
-
import
|
|
5996
|
+
import path30 from "path";
|
|
5771
5997
|
function templateEnd(content2, start) {
|
|
5772
5998
|
let i = start + 1, interpolation = 0;
|
|
5773
5999
|
while (i < content2.length) {
|
|
@@ -5789,10 +6015,10 @@ function templateEnd(content2, start) {
|
|
|
5789
6015
|
continue;
|
|
5790
6016
|
}
|
|
5791
6017
|
if (c === '"' || c === "'") {
|
|
5792
|
-
const
|
|
6018
|
+
const quote2 = c;
|
|
5793
6019
|
for (i++; i < content2.length; i++) {
|
|
5794
6020
|
if (content2[i] === "\\") i++;
|
|
5795
|
-
else if (content2[i] ===
|
|
6021
|
+
else if (content2[i] === quote2) {
|
|
5796
6022
|
i++;
|
|
5797
6023
|
break;
|
|
5798
6024
|
}
|
|
@@ -5893,10 +6119,10 @@ function jvmImports(content2) {
|
|
|
5893
6119
|
}
|
|
5894
6120
|
function resolveImport(file, specifier, available) {
|
|
5895
6121
|
if (!specifier.startsWith("./") && !specifier.startsWith("../")) return null;
|
|
5896
|
-
const relative =
|
|
6122
|
+
const relative = path30.posix.normalize(path30.posix.join(path30.posix.dirname(file), specifier));
|
|
5897
6123
|
if (relative.startsWith("../")) return null;
|
|
5898
6124
|
if (available.has(relative)) return relative;
|
|
5899
|
-
const ext =
|
|
6125
|
+
const ext = path30.posix.extname(relative);
|
|
5900
6126
|
const replacements = { ".js": [".ts", ".tsx"], ".mjs": [".mts"], ".cjs": [".cts"] };
|
|
5901
6127
|
const candidates = ext ? (replacements[ext] ?? []).map((e) => relative.slice(0, -ext.length) + e) : jsExtensions.flatMap((e) => [relative + e, relative + "/index" + e]);
|
|
5902
6128
|
const found = candidates.filter((f) => available.has(f));
|
|
@@ -5905,23 +6131,23 @@ function resolveImport(file, specifier, available) {
|
|
|
5905
6131
|
function mentionsPath(content2, target) {
|
|
5906
6132
|
return new RegExp(`(?<![\\w./\\\\-])(?:\\./)?${escape(target)}(?![\\w./\\\\-])`).test(content2);
|
|
5907
6133
|
}
|
|
5908
|
-
function createReferenceMatcher(
|
|
6134
|
+
function createReferenceMatcher(targets2, available, targetContents = /* @__PURE__ */ new Map()) {
|
|
5909
6135
|
const counts = /* @__PURE__ */ new Map();
|
|
5910
6136
|
for (const file of available) {
|
|
5911
|
-
const name =
|
|
6137
|
+
const name = path30.posix.basename(file, path30.posix.extname(file));
|
|
5912
6138
|
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
5913
6139
|
}
|
|
5914
6140
|
const jvmTargets = new Map([...targetContents].filter(([file]) => /\.(?:kt|kts|java)$/.test(file)).map(([file, content2]) => [file, declaredSymbols(content2)]));
|
|
5915
6141
|
return (source) => {
|
|
5916
|
-
if (
|
|
5917
|
-
const resolved = new Set(jsExtensions.includes(
|
|
6142
|
+
if (targets2.includes(source.path)) return [];
|
|
6143
|
+
const resolved = new Set(jsExtensions.includes(path30.posix.extname(source.path)) ? imports(source.content).map((s) => resolveImport(source.path, s, available)).filter(Boolean) : []);
|
|
5918
6144
|
if (/\.(?:kt|kts|java)$/.test(source.path)) {
|
|
5919
6145
|
const imported = jvmImports(source.content);
|
|
5920
6146
|
for (const [target, symbols] of jvmTargets) if (symbols.some((symbol) => imported.some((name) => name === symbol || name.startsWith(symbol + ".")))) resolved.add(target);
|
|
5921
6147
|
}
|
|
5922
6148
|
const groups = /* @__PURE__ */ new Map();
|
|
5923
6149
|
const add = (evidence, match) => groups.set(evidence, [...groups.get(evidence) ?? [], match]);
|
|
5924
|
-
for (const target of
|
|
6150
|
+
for (const target of targets2) {
|
|
5925
6151
|
if (resolved.has(target)) {
|
|
5926
6152
|
add("resolved-import", target);
|
|
5927
6153
|
continue;
|
|
@@ -5930,7 +6156,7 @@ function createReferenceMatcher(targets, available, targetContents = /* @__PURE_
|
|
|
5930
6156
|
add("explicit-path", target);
|
|
5931
6157
|
continue;
|
|
5932
6158
|
}
|
|
5933
|
-
const ext =
|
|
6159
|
+
const ext = path30.posix.extname(target), name = path30.posix.basename(target, ext);
|
|
5934
6160
|
if (!/\.(?:[cm]?[jt]sx?|py|kt|java|go|rs|swift|rb|cs|cpp|c|h|hpp|dart|php)$/.test(ext) || name.startsWith(".") || name.length < 3 || genericNames.has(name.toLowerCase()) || counts.get(name) !== 1) continue;
|
|
5935
6161
|
if (word(name).test(source.content)) {
|
|
5936
6162
|
add("name-candidate", name);
|
|
@@ -5959,11 +6185,11 @@ var impact_exports = {};
|
|
|
5959
6185
|
__export(impact_exports, {
|
|
5960
6186
|
analyzeImpact: () => analyzeImpact
|
|
5961
6187
|
});
|
|
5962
|
-
import
|
|
6188
|
+
import path31 from "path";
|
|
5963
6189
|
import { execFile as execFile8 } from "child_process";
|
|
5964
6190
|
import { promisify as promisify8 } from "util";
|
|
5965
6191
|
async function analyzeImpact(rootDir, targetFiles) {
|
|
5966
|
-
const resolvedRoot =
|
|
6192
|
+
const resolvedRoot = path31.resolve(rootDir);
|
|
5967
6193
|
const resolvedTargets = await resolveTargetFiles(resolvedRoot, targetFiles);
|
|
5968
6194
|
const [cochange, references, tests] = await Promise.all([
|
|
5969
6195
|
getCochangeFiles(resolvedRoot, resolvedTargets),
|
|
@@ -5977,10 +6203,10 @@ async function analyzeImpact(rootDir, targetFiles) {
|
|
|
5977
6203
|
tests
|
|
5978
6204
|
};
|
|
5979
6205
|
}
|
|
5980
|
-
async function resolveTargetFiles(rootDir,
|
|
6206
|
+
async function resolveTargetFiles(rootDir, targets2) {
|
|
5981
6207
|
const resolved = [];
|
|
5982
6208
|
const access = await createFileAccess(rootDir);
|
|
5983
|
-
for (const target of
|
|
6209
|
+
for (const target of targets2) {
|
|
5984
6210
|
if (target.includes("/")) {
|
|
5985
6211
|
const normalized = normalizeRepoPath(target);
|
|
5986
6212
|
if (normalized) resolved.push(normalized);
|
|
@@ -6077,9 +6303,9 @@ async function getRelatedTests(rootDir, targetFiles) {
|
|
|
6077
6303
|
const testFiles = await (await createFileAccess(rootDir)).list(testPatterns);
|
|
6078
6304
|
const results = [];
|
|
6079
6305
|
for (const target of targetFiles) {
|
|
6080
|
-
const targetBaseName =
|
|
6306
|
+
const targetBaseName = path31.basename(target).replace(/\.[^.]+$/, "");
|
|
6081
6307
|
for (const testFile of testFiles) {
|
|
6082
|
-
const testBaseName =
|
|
6308
|
+
const testBaseName = path31.basename(testFile).replace(/\.[^.]+$/, "");
|
|
6083
6309
|
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
6084
6310
|
if (sourceName === targetBaseName) {
|
|
6085
6311
|
results.push({
|
|
@@ -6107,26 +6333,26 @@ var assemble_exports = {};
|
|
|
6107
6333
|
__export(assemble_exports, {
|
|
6108
6334
|
assembleContext: () => assembleContext
|
|
6109
6335
|
});
|
|
6110
|
-
import
|
|
6111
|
-
import
|
|
6336
|
+
import path32 from "path";
|
|
6337
|
+
import fs22 from "fs/promises";
|
|
6112
6338
|
async function collectImpact(root, candidates) {
|
|
6113
|
-
const
|
|
6339
|
+
const targets2 = /* @__PURE__ */ new Set();
|
|
6114
6340
|
let sourceFiles;
|
|
6115
6341
|
for (const candidate of sanitizeRepoPaths(candidates)) {
|
|
6116
|
-
const stat = await
|
|
6342
|
+
const stat = await fs22.stat(path32.join(root, candidate)).catch(() => null);
|
|
6117
6343
|
if (stat?.isDirectory()) {
|
|
6118
6344
|
sourceFiles ??= await (await createFileAccess(root)).list();
|
|
6119
6345
|
for (const file of sourceFiles) {
|
|
6120
|
-
if (anchorMatches(candidate, file))
|
|
6121
|
-
if (
|
|
6346
|
+
if (anchorMatches(candidate, file)) targets2.add(file);
|
|
6347
|
+
if (targets2.size >= MAX_IMPACT_TARGETS) break;
|
|
6122
6348
|
}
|
|
6123
6349
|
} else {
|
|
6124
|
-
|
|
6350
|
+
targets2.add(candidate);
|
|
6125
6351
|
}
|
|
6126
|
-
if (
|
|
6352
|
+
if (targets2.size >= MAX_IMPACT_TARGETS) break;
|
|
6127
6353
|
}
|
|
6128
|
-
if (!
|
|
6129
|
-
const result = await analyzeImpact(root, [...
|
|
6354
|
+
if (!targets2.size) return { impact: null, relatedTests: [] };
|
|
6355
|
+
const result = await analyzeImpact(root, [...targets2]);
|
|
6130
6356
|
const referenceCandidatesOmitted = result.references.filter((r) => r.evidence === "name-candidate").length;
|
|
6131
6357
|
return {
|
|
6132
6358
|
impact: {
|
|
@@ -6139,7 +6365,7 @@ async function collectImpact(root, candidates) {
|
|
|
6139
6365
|
};
|
|
6140
6366
|
}
|
|
6141
6367
|
async function assembleContext(rootDir, task, files) {
|
|
6142
|
-
const resolvedRoot =
|
|
6368
|
+
const resolvedRoot = path32.resolve(rootDir);
|
|
6143
6369
|
const mapState = await inspectSnapshot(resolvedRoot);
|
|
6144
6370
|
const snapshot = mapState.snapshot;
|
|
6145
6371
|
const store = await loadDecisionStore(resolvedRoot);
|
|
@@ -6171,6 +6397,7 @@ async function assembleContext(rootDir, task, files) {
|
|
|
6171
6397
|
};
|
|
6172
6398
|
}
|
|
6173
6399
|
const drift = await computeDrift(resolvedRoot);
|
|
6400
|
+
const readTrust = createSnapshotTrustReader(resolvedRoot);
|
|
6174
6401
|
const featureScores = Object.entries(snapshot.features).map(([name, feat]) => ({
|
|
6175
6402
|
name,
|
|
6176
6403
|
feat,
|
|
@@ -6197,14 +6424,7 @@ async function assembleContext(rootDir, task, files) {
|
|
|
6197
6424
|
Object.assign(bundle, await collectImpact(resolvedRoot, [...anchorFiles, ...Object.values(decisions).flatMap((d) => [...d.files, ...d.pendingProposal?.files ?? []])]));
|
|
6198
6425
|
bundle.diagnostics = store.diagnostics;
|
|
6199
6426
|
bundle.freshness = drift;
|
|
6200
|
-
bundle.trust =
|
|
6201
|
-
features: Object.fromEntries(Object.entries(snapshot.features).map(
|
|
6202
|
-
([name, entry]) => [name, assessTrust(entry, drift?.featureFreshness?.[name] ?? "unknown")]
|
|
6203
|
-
)),
|
|
6204
|
-
flows: Object.fromEntries(Object.entries(snapshot.flows).map(
|
|
6205
|
-
([name, entry]) => [name, assessTrust(entry, drift?.flowFreshness?.[name] ?? "unknown")]
|
|
6206
|
-
))
|
|
6207
|
-
};
|
|
6427
|
+
bundle.trust = await readSnapshotTrustIndex(readTrust, snapshot, drift);
|
|
6208
6428
|
bundle.hint += " " + trustHint([
|
|
6209
6429
|
...Object.values(bundle.trust.features),
|
|
6210
6430
|
...Object.values(bundle.trust.flows),
|
|
@@ -6217,7 +6437,7 @@ async function assembleContext(rootDir, task, files) {
|
|
|
6217
6437
|
const features = {};
|
|
6218
6438
|
const staleMatches = [];
|
|
6219
6439
|
for (const { name, feat, score } of featureScores) {
|
|
6220
|
-
const trust =
|
|
6440
|
+
const trust = await readTrust("feature", name, feat, drift?.featureFreshness?.[name] ?? "unknown");
|
|
6221
6441
|
const stale2 = trust.freshness !== "current";
|
|
6222
6442
|
if (stale2) staleMatches.push(name);
|
|
6223
6443
|
features[name] = {
|
|
@@ -6232,7 +6452,7 @@ async function assembleContext(rootDir, task, files) {
|
|
|
6232
6452
|
}
|
|
6233
6453
|
const flows = {};
|
|
6234
6454
|
for (const { name, flow, score } of flowScores) {
|
|
6235
|
-
const trust =
|
|
6455
|
+
const trust = await readTrust("flow", name, flow, drift?.flowFreshness?.[name] ?? "unknown");
|
|
6236
6456
|
const stale2 = trust.freshness !== "current";
|
|
6237
6457
|
if (stale2) staleMatches.push(name);
|
|
6238
6458
|
flows[name] = {
|
|
@@ -6317,6 +6537,7 @@ var init_assemble = __esm({
|
|
|
6317
6537
|
"use strict";
|
|
6318
6538
|
init_snapshot();
|
|
6319
6539
|
init_files();
|
|
6540
|
+
init_trust2();
|
|
6320
6541
|
init_drift();
|
|
6321
6542
|
init_impact();
|
|
6322
6543
|
init_lexical();
|
|
@@ -6340,7 +6561,7 @@ __export(observations_exports, {
|
|
|
6340
6561
|
observeActivation: () => observeActivation,
|
|
6341
6562
|
readObservation: () => readObservation
|
|
6342
6563
|
});
|
|
6343
|
-
import
|
|
6564
|
+
import fs23 from "fs/promises";
|
|
6344
6565
|
import { z as z23 } from "zod";
|
|
6345
6566
|
function observationPath(directory, host, revision, version = executingVersion()) {
|
|
6346
6567
|
return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
|
|
@@ -6357,15 +6578,15 @@ async function observeActivation(dir, event, options = {}) {
|
|
|
6357
6578
|
if (host !== "codex" && host !== "claude" || !process.env.MASON_SETUP_ROOT) return null;
|
|
6358
6579
|
try {
|
|
6359
6580
|
if (!revision) {
|
|
6360
|
-
if ((await
|
|
6581
|
+
if ((await effectiveSetup(process.env.MASON_SETUP_ROOT)).setup?.hosts[host]) {
|
|
6361
6582
|
return "Mason setup is now present. Restart the assistant to connect this MCP session to the configured integration; activation has not been established by this session.";
|
|
6362
6583
|
}
|
|
6363
6584
|
return inactiveSetupMessage();
|
|
6364
6585
|
}
|
|
6365
6586
|
const capturedDirectory = options.directory?.match(/^\.mason\/reports\/automation\/[a-f0-9]{24}$/)?.[0] ?? options.reportPath?.match(/^(\.mason\/reports\/automation\/[a-f0-9]{24})\/checks\//)?.[1];
|
|
6366
|
-
const ws = capturedDirectory ? { root: await
|
|
6367
|
-
if (ws.root !== await
|
|
6368
|
-
const setup = await
|
|
6587
|
+
const ws = capturedDirectory ? { root: await fs23.realpath(dir), directory: capturedDirectory } : await workspace(dir);
|
|
6588
|
+
if (ws.root !== await fs23.realpath(process.env.MASON_SETUP_ROOT)) return null;
|
|
6589
|
+
const { setup } = await effectiveSetup(ws.root);
|
|
6369
6590
|
if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
|
|
6370
6591
|
const directory = ws.directory + "/activation";
|
|
6371
6592
|
await withLock(ws.root, directory, async () => {
|
|
@@ -6439,8 +6660,9 @@ __export(status_exports, {
|
|
|
6439
6660
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
6440
6661
|
async function setupStatus(dir) {
|
|
6441
6662
|
const ws = await workspace(dir);
|
|
6442
|
-
const
|
|
6443
|
-
|
|
6663
|
+
const activation = await effectiveSetup(ws.root);
|
|
6664
|
+
const setup = activation.setup;
|
|
6665
|
+
if (!setup || !Object.keys(setup.hosts).length) {
|
|
6444
6666
|
const partial = await Promise.all(["codex", "claude"].map(async (host) => ({
|
|
6445
6667
|
host,
|
|
6446
6668
|
receipt: await loadSetupReceipt(ws.root, ws.directory, host)
|
|
@@ -6474,7 +6696,7 @@ async function setupStatus(dir) {
|
|
|
6474
6696
|
]));
|
|
6475
6697
|
const hooks = isDeepStrictEqual2(compatibleHooks, expectedHooks) && !config.disabled;
|
|
6476
6698
|
const local = await loadSetupReceipt(ws.root, ws.directory, host);
|
|
6477
|
-
const configured = local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
|
|
6699
|
+
const configured = activation.root !== ws.root || local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
|
|
6478
6700
|
const observation = await readObservation(ws.root, ws.directory, host, entry.revision, installed.version ?? "unavailable");
|
|
6479
6701
|
const sessions = Object.values(observation?.sessions ?? {}).sort((a, b) => b.at.localeCompare(a.at));
|
|
6480
6702
|
const complete = sessions.find((session) => events.every((event) => session.events.includes(event)));
|
|
@@ -6506,6 +6728,7 @@ async function setupStatus(dir) {
|
|
|
6506
6728
|
status: statuses.length && statuses.every((status) => status === "active") ? "active" : statuses.includes("attention") ? "attention" : "pending",
|
|
6507
6729
|
root: ws.root,
|
|
6508
6730
|
hosts,
|
|
6731
|
+
activationRoot: activation.root,
|
|
6509
6732
|
decisionRecords: decisions.records.length,
|
|
6510
6733
|
diagnostics: decisions.diagnostics,
|
|
6511
6734
|
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."
|
|
@@ -6513,6 +6736,9 @@ async function setupStatus(dir) {
|
|
|
6513
6736
|
}
|
|
6514
6737
|
function summarizeActivation(status) {
|
|
6515
6738
|
const lines = ["Mason setup: " + status.status + "."];
|
|
6739
|
+
if (status.activationRoot && status.activationRoot !== status.root) {
|
|
6740
|
+
lines.push("Activation inherited from " + status.activationRoot + "; evidence belongs to this worktree.");
|
|
6741
|
+
}
|
|
6516
6742
|
for (const [host, state] of Object.entries(status.hosts)) {
|
|
6517
6743
|
lines.push(
|
|
6518
6744
|
`${host}: ${state.status}`,
|
|
@@ -6682,8 +6908,8 @@ __export(client_exports, {
|
|
|
6682
6908
|
function createConfluenceClient(config, fetchFn = fetch) {
|
|
6683
6909
|
const baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
6684
6910
|
const auth = "Basic " + Buffer.from(`${config.email}:${config.apiToken}`).toString("base64");
|
|
6685
|
-
async function call(method,
|
|
6686
|
-
const res = await fetchFn(`${baseUrl}${
|
|
6911
|
+
async function call(method, path42, body) {
|
|
6912
|
+
const res = await fetchFn(`${baseUrl}${path42}`, {
|
|
6687
6913
|
method,
|
|
6688
6914
|
headers: {
|
|
6689
6915
|
Authorization: auth,
|
|
@@ -6695,7 +6921,7 @@ function createConfluenceClient(config, fetchFn = fetch) {
|
|
|
6695
6921
|
if (!res.ok) {
|
|
6696
6922
|
const text2 = await res.text();
|
|
6697
6923
|
throw new Error(
|
|
6698
|
-
`Confluence ${method} ${
|
|
6924
|
+
`Confluence ${method} ${path42} failed: ${res.status} ${res.statusText} \u2014 ${text2}`
|
|
6699
6925
|
);
|
|
6700
6926
|
}
|
|
6701
6927
|
if (res.status === 204) return null;
|
|
@@ -6796,28 +7022,28 @@ __export(config_exports2, {
|
|
|
6796
7022
|
saveConfluenceConfig: () => saveConfluenceConfig,
|
|
6797
7023
|
validateProvider: () => validateProvider
|
|
6798
7024
|
});
|
|
6799
|
-
import
|
|
6800
|
-
import
|
|
7025
|
+
import fs24 from "fs/promises";
|
|
7026
|
+
import path33 from "path";
|
|
6801
7027
|
import os3 from "os";
|
|
6802
7028
|
import { execFile as execFile9 } from "child_process";
|
|
6803
7029
|
import { promisify as promisify9 } from "util";
|
|
6804
7030
|
function configDir() {
|
|
6805
|
-
return
|
|
7031
|
+
return path33.join(os3.homedir(), ".mason");
|
|
6806
7032
|
}
|
|
6807
7033
|
function configFile() {
|
|
6808
|
-
return
|
|
7034
|
+
return path33.join(configDir(), "config.json");
|
|
6809
7035
|
}
|
|
6810
7036
|
async function loadConfig() {
|
|
6811
7037
|
try {
|
|
6812
|
-
const raw = await
|
|
7038
|
+
const raw = await fs24.readFile(configFile(), "utf-8");
|
|
6813
7039
|
return JSON.parse(raw);
|
|
6814
7040
|
} catch {
|
|
6815
7041
|
return null;
|
|
6816
7042
|
}
|
|
6817
7043
|
}
|
|
6818
7044
|
async function saveConfig(config) {
|
|
6819
|
-
await
|
|
6820
|
-
await
|
|
7045
|
+
await fs24.mkdir(configDir(), { recursive: true });
|
|
7046
|
+
await fs24.writeFile(configFile(), JSON.stringify(config, null, 2), "utf-8");
|
|
6821
7047
|
}
|
|
6822
7048
|
function getDefaultModel(provider) {
|
|
6823
7049
|
return DEFAULT_MODELS[provider];
|
|
@@ -6977,21 +7203,21 @@ var init_renderer = __esm({
|
|
|
6977
7203
|
});
|
|
6978
7204
|
|
|
6979
7205
|
// src/confluence/diff.ts
|
|
6980
|
-
import
|
|
6981
|
-
import
|
|
6982
|
-
import { createHash as
|
|
7206
|
+
import fs25 from "fs/promises";
|
|
7207
|
+
import path34 from "path";
|
|
7208
|
+
import { createHash as createHash6 } from "crypto";
|
|
6983
7209
|
function hashDescription(description) {
|
|
6984
|
-
return
|
|
7210
|
+
return createHash6("sha256").update(description, "utf8").digest("hex");
|
|
6985
7211
|
}
|
|
6986
7212
|
function syncStateDir(rootDir) {
|
|
6987
|
-
return
|
|
7213
|
+
return path34.join(rootDir, ".mason");
|
|
6988
7214
|
}
|
|
6989
7215
|
function syncStatePath(rootDir) {
|
|
6990
|
-
return
|
|
7216
|
+
return path34.join(syncStateDir(rootDir), "confluence-sync.json");
|
|
6991
7217
|
}
|
|
6992
7218
|
async function loadSyncState(rootDir) {
|
|
6993
7219
|
try {
|
|
6994
|
-
const raw = await
|
|
7220
|
+
const raw = await fs25.readFile(syncStatePath(rootDir), "utf-8");
|
|
6995
7221
|
const parsed = JSON.parse(raw);
|
|
6996
7222
|
if (parsed.version !== 2) return null;
|
|
6997
7223
|
return parsed;
|
|
@@ -7000,8 +7226,8 @@ async function loadSyncState(rootDir) {
|
|
|
7000
7226
|
}
|
|
7001
7227
|
}
|
|
7002
7228
|
async function saveSyncState(rootDir, state) {
|
|
7003
|
-
await
|
|
7004
|
-
await
|
|
7229
|
+
await fs25.mkdir(syncStateDir(rootDir), { recursive: true });
|
|
7230
|
+
await fs25.writeFile(
|
|
7005
7231
|
syncStatePath(rootDir),
|
|
7006
7232
|
JSON.stringify(state, null, 2),
|
|
7007
7233
|
"utf-8"
|
|
@@ -7559,8 +7785,8 @@ import { z as z24 } from "zod";
|
|
|
7559
7785
|
// src/mcp/tools.ts
|
|
7560
7786
|
init_repair();
|
|
7561
7787
|
init_advisory_review();
|
|
7562
|
-
import
|
|
7563
|
-
import
|
|
7788
|
+
import fs26 from "fs/promises";
|
|
7789
|
+
import path35 from "path";
|
|
7564
7790
|
import { execFile as execFile11 } from "child_process";
|
|
7565
7791
|
import { promisify as promisify11 } from "util";
|
|
7566
7792
|
|
|
@@ -7569,11 +7795,11 @@ init_audit();
|
|
|
7569
7795
|
init_types();
|
|
7570
7796
|
init_repair();
|
|
7571
7797
|
init_findings();
|
|
7572
|
-
import
|
|
7798
|
+
import path24 from "path";
|
|
7573
7799
|
|
|
7574
7800
|
// src/audit/advisory-cli.ts
|
|
7575
7801
|
init_advisory_review();
|
|
7576
|
-
import
|
|
7802
|
+
import path23 from "path";
|
|
7577
7803
|
import { stripVTControlCharacters } from "util";
|
|
7578
7804
|
|
|
7579
7805
|
// src/audit/cli.ts
|
|
@@ -7670,7 +7896,7 @@ import { execFile as execFile3 } from "child_process";
|
|
|
7670
7896
|
import { promisify as promisify3 } from "util";
|
|
7671
7897
|
|
|
7672
7898
|
// src/analyzers/base.ts
|
|
7673
|
-
import
|
|
7899
|
+
import fs16 from "fs/promises";
|
|
7674
7900
|
import fg6 from "fast-glob";
|
|
7675
7901
|
var BaseAnalyzer = class {
|
|
7676
7902
|
async findFiles(patterns, root) {
|
|
@@ -7681,7 +7907,7 @@ var BaseAnalyzer = class {
|
|
|
7681
7907
|
});
|
|
7682
7908
|
}
|
|
7683
7909
|
async readFile(filePath) {
|
|
7684
|
-
return
|
|
7910
|
+
return fs16.readFile(filePath, "utf-8");
|
|
7685
7911
|
}
|
|
7686
7912
|
createFinding(partial) {
|
|
7687
7913
|
return {
|
|
@@ -7886,7 +8112,7 @@ async function isGitRepo(dir) {
|
|
|
7886
8112
|
|
|
7887
8113
|
// src/mcp/sampler.ts
|
|
7888
8114
|
init_files();
|
|
7889
|
-
import
|
|
8115
|
+
import path25 from "path";
|
|
7890
8116
|
import { execFile as execFile4 } from "child_process";
|
|
7891
8117
|
import { promisify as promisify4 } from "util";
|
|
7892
8118
|
var exec2 = promisify4(execFile4);
|
|
@@ -7994,8 +8220,8 @@ async function sampleFiles(rootDir, maxFiles = 25) {
|
|
|
7994
8220
|
const projectConfig = access.config;
|
|
7995
8221
|
for (const filePath of projectConfig.alwaysInclude ?? []) {
|
|
7996
8222
|
if (selected.size >= maxFiles) break;
|
|
7997
|
-
const resolvedPath =
|
|
7998
|
-
if (!resolvedPath.startsWith(
|
|
8223
|
+
const resolvedPath = path25.resolve(rootDir, filePath);
|
|
8224
|
+
if (!resolvedPath.startsWith(path25.resolve(rootDir))) continue;
|
|
7999
8225
|
selected.set(filePath, "always-include (project config)");
|
|
8000
8226
|
}
|
|
8001
8227
|
let configCount = 0;
|
|
@@ -8061,7 +8287,7 @@ async function sampleFiles(rootDir, maxFiles = 25) {
|
|
|
8061
8287
|
if (!line) continue;
|
|
8062
8288
|
if (line.includes("node_modules") || line.includes("/build/") || line.includes(".gradle") || line.includes("/generated/"))
|
|
8063
8289
|
continue;
|
|
8064
|
-
const ext =
|
|
8290
|
+
const ext = path25.extname(line).slice(1);
|
|
8065
8291
|
if (!SOURCE_EXTENSIONS.includes(ext)) continue;
|
|
8066
8292
|
fileCounts.set(line, (fileCounts.get(line) ?? 0) + 1);
|
|
8067
8293
|
}
|
|
@@ -8207,8 +8433,8 @@ async function reviewState(root, record) {
|
|
|
8207
8433
|
changedFiles: touchedPaths(committed),
|
|
8208
8434
|
localChanges: matchingPaths(anchors, workingTree.changedFiles)
|
|
8209
8435
|
};
|
|
8210
|
-
const
|
|
8211
|
-
return { reviewToken, evidence, committed, workingTreeAvailable: workingTree.available };
|
|
8436
|
+
const reviewToken2 = createHash4("sha256").update(JSON.stringify({ record, evidence, workingTreeAvailable: workingTree.available })).digest("hex");
|
|
8437
|
+
return { reviewToken: reviewToken2, evidence, committed, workingTreeAvailable: workingTree.available };
|
|
8212
8438
|
}
|
|
8213
8439
|
async function previews(root, record, state) {
|
|
8214
8440
|
const access = await createFileAccess(root);
|
|
@@ -8314,7 +8540,9 @@ async function reviewDecision(root, input) {
|
|
|
8314
8540
|
// src/mcp/tools.ts
|
|
8315
8541
|
init_drift2();
|
|
8316
8542
|
init_snapshot();
|
|
8543
|
+
init_trust2();
|
|
8317
8544
|
init_drift();
|
|
8545
|
+
init_review();
|
|
8318
8546
|
|
|
8319
8547
|
// src/snapshot/prompt.ts
|
|
8320
8548
|
var BATCH_SYSTEM_PROMPT = `You are Mason, building one piece of a larger concept-to-files map via a Map-Reduce pattern.
|
|
@@ -8411,9 +8639,10 @@ ${JSON.stringify({ partials }, null, 2)}`;
|
|
|
8411
8639
|
}
|
|
8412
8640
|
|
|
8413
8641
|
// src/snapshot/partials.ts
|
|
8642
|
+
init_lock();
|
|
8414
8643
|
init_storage();
|
|
8415
8644
|
init_snapshot();
|
|
8416
|
-
import
|
|
8645
|
+
import fs17 from "fs/promises";
|
|
8417
8646
|
import { z as z11 } from "zod";
|
|
8418
8647
|
var DIRECTORY = ".mason/partial-snapshots";
|
|
8419
8648
|
var partialSchema = z11.object({
|
|
@@ -8426,12 +8655,12 @@ var partialSchema = z11.object({
|
|
|
8426
8655
|
var scopeSchema2 = z11.object({ files: z11.array(z11.string()), savedAt: z11.string() });
|
|
8427
8656
|
async function savePartial(rootDir, partial) {
|
|
8428
8657
|
if (!/^[a-zA-Z0-9_-]+$/.test(partial.batchId)) throw new Error(`Invalid batchId: ${partial.batchId}`);
|
|
8429
|
-
await writeStoreJson(rootDir, `${DIRECTORY}/${partial.batchId}.json`, partialSchema.parse(partial));
|
|
8658
|
+
await withSnapshotWrite(rootDir, () => writeStoreJson(rootDir, `${DIRECTORY}/${partial.batchId}.json`, partialSchema.parse(partial)));
|
|
8430
8659
|
}
|
|
8431
8660
|
async function loadAllPartials(rootDir) {
|
|
8432
8661
|
let entries;
|
|
8433
8662
|
try {
|
|
8434
|
-
entries = await
|
|
8663
|
+
entries = await fs17.readdir(await storePath(rootDir, DIRECTORY));
|
|
8435
8664
|
} catch (error) {
|
|
8436
8665
|
if (error.code === "ENOENT") return [];
|
|
8437
8666
|
throw error;
|
|
@@ -8446,17 +8675,19 @@ async function loadAllPartials(rootDir) {
|
|
|
8446
8675
|
return partials.sort((a, b) => a.offset - b.offset);
|
|
8447
8676
|
}
|
|
8448
8677
|
async function saveScope(rootDir, files) {
|
|
8449
|
-
await writeStoreJson(rootDir, `${DIRECTORY}/scope.json`, { files, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
8678
|
+
await withSnapshotWrite(rootDir, () => writeStoreJson(rootDir, `${DIRECTORY}/scope.json`, { files, savedAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
8450
8679
|
}
|
|
8451
8680
|
async function loadScope(rootDir) {
|
|
8452
8681
|
const raw = await readStoreJson(rootDir, `${DIRECTORY}/scope.json`);
|
|
8453
8682
|
return raw === null ? null : scopeSchema2.parse(raw).files;
|
|
8454
8683
|
}
|
|
8455
8684
|
async function clearScope(rootDir) {
|
|
8456
|
-
await
|
|
8685
|
+
await withSnapshotWrite(rootDir, async () => {
|
|
8686
|
+
await fs17.rm(await storePath(rootDir, `${DIRECTORY}/scope.json`), { force: true });
|
|
8687
|
+
});
|
|
8457
8688
|
}
|
|
8458
8689
|
async function clearAllPartials(rootDir) {
|
|
8459
|
-
await
|
|
8690
|
+
await fs17.rm(await storePath(rootDir, DIRECTORY), { recursive: true, force: true });
|
|
8460
8691
|
}
|
|
8461
8692
|
function batchIdFor(offset) {
|
|
8462
8693
|
return `batch-${String(offset).padStart(6, "0")}`;
|
|
@@ -8473,7 +8704,7 @@ async function buildContext(dir) {
|
|
|
8473
8704
|
};
|
|
8474
8705
|
}
|
|
8475
8706
|
async function analyzeProject(dir) {
|
|
8476
|
-
const rootDir =
|
|
8707
|
+
const rootDir = path35.resolve(dir);
|
|
8477
8708
|
const context = await buildContext(rootDir);
|
|
8478
8709
|
const results = await runAll(context);
|
|
8479
8710
|
const projectSnapshot = await detectProjectSnapshot(rootDir);
|
|
@@ -8528,7 +8759,7 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
8528
8759
|
const present = [];
|
|
8529
8760
|
for (const file of buildFiles) {
|
|
8530
8761
|
try {
|
|
8531
|
-
await
|
|
8762
|
+
await fs26.access(path35.join(rootDir, file));
|
|
8532
8763
|
present.push(file);
|
|
8533
8764
|
} catch {
|
|
8534
8765
|
}
|
|
@@ -8570,7 +8801,7 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
8570
8801
|
const sourceFiles = await access.list();
|
|
8571
8802
|
const fileCounts = {};
|
|
8572
8803
|
for (const file of sourceFiles) {
|
|
8573
|
-
const ext =
|
|
8804
|
+
const ext = path35.extname(file).slice(1);
|
|
8574
8805
|
fileCounts[ext] = (fileCounts[ext] ?? 0) + 1;
|
|
8575
8806
|
}
|
|
8576
8807
|
return {
|
|
@@ -8581,7 +8812,7 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
8581
8812
|
};
|
|
8582
8813
|
}
|
|
8583
8814
|
async function getCodeSamples(dir, count4 = 15) {
|
|
8584
|
-
const rootDir =
|
|
8815
|
+
const rootDir = path35.resolve(dir);
|
|
8585
8816
|
const samples = await sampleFiles(rootDir, count4);
|
|
8586
8817
|
const output = {
|
|
8587
8818
|
note: "These are previews (first ~60 lines). Read the file directly with your own tools to see it in full.",
|
|
@@ -8624,7 +8855,7 @@ async function unmappedContextResponse(rootDir) {
|
|
|
8624
8855
|
});
|
|
8625
8856
|
}
|
|
8626
8857
|
async function getProjectStructure(dir) {
|
|
8627
|
-
const rootDir =
|
|
8858
|
+
const rootDir = path35.resolve(dir);
|
|
8628
8859
|
const allFiles = await (await createFileAccess(rootDir)).list("**/*");
|
|
8629
8860
|
const dirInfo = /* @__PURE__ */ new Map();
|
|
8630
8861
|
for (const file of allFiles) {
|
|
@@ -8636,7 +8867,7 @@ async function getProjectStructure(dir) {
|
|
|
8636
8867
|
}
|
|
8637
8868
|
const info = dirInfo.get(dirPath);
|
|
8638
8869
|
info.fileCount++;
|
|
8639
|
-
const ext =
|
|
8870
|
+
const ext = path35.extname(file).slice(1);
|
|
8640
8871
|
if (ext) {
|
|
8641
8872
|
info.extensions.set(ext, (info.extensions.get(ext) ?? 0) + 1);
|
|
8642
8873
|
}
|
|
@@ -8681,7 +8912,7 @@ async function buildChangedFilePreviews(rootDir, changedFiles) {
|
|
|
8681
8912
|
return previews2;
|
|
8682
8913
|
}
|
|
8683
8914
|
async function getSnapshot(dir) {
|
|
8684
|
-
const rootDir =
|
|
8915
|
+
const rootDir = path35.resolve(dir);
|
|
8685
8916
|
const snapshot = await loadSnapshot(rootDir);
|
|
8686
8917
|
if (!snapshot) {
|
|
8687
8918
|
return unmappedContextResponse(rootDir);
|
|
@@ -8719,9 +8950,9 @@ async function getSnapshot(dir) {
|
|
|
8719
8950
|
const store = await loadDecisionStore2(rootDir);
|
|
8720
8951
|
const decisionRecords = store.records;
|
|
8721
8952
|
const decisionDrift = await computeDecisionDrift(rootDir, decisionRecords);
|
|
8953
|
+
const readTrust = createSnapshotTrustReader(rootDir);
|
|
8722
8954
|
const trust = {
|
|
8723
|
-
|
|
8724
|
-
flows: Object.fromEntries(Object.entries(snapshot.flows).map(([name, entry]) => [name, assessTrust(entry, drift?.flowFreshness?.[name] ?? "unknown")])),
|
|
8955
|
+
...await readSnapshotTrustIndex(readTrust, snapshot, drift),
|
|
8725
8956
|
decisions: Object.fromEntries(decisionRecords.filter((d) => d.status === "active").map((d) => [d.id, decisionTrust(effectiveDecision(d), decisionDrift.freshness?.[d.id] ?? "unknown")]))
|
|
8726
8957
|
};
|
|
8727
8958
|
output.trust = trust;
|
|
@@ -8782,7 +9013,7 @@ function driftHint(report) {
|
|
|
8782
9013
|
return "Read the changed files under staleFeatures/staleFlows, update those entries (fold unmappedFiles into the right features), and call save_snapshot with only the affected entries \u2014 unchanged entries are preserved. Drop ghostFiles from any entries that reference them, and delete features/flows that no longer exist via save_snapshot's removeFeatures/removeFlows.";
|
|
8783
9014
|
}
|
|
8784
9015
|
async function checkDrift(dir) {
|
|
8785
|
-
const rootDir =
|
|
9016
|
+
const rootDir = path35.resolve(dir);
|
|
8786
9017
|
const report = await computeDrift(rootDir);
|
|
8787
9018
|
if (!report) {
|
|
8788
9019
|
return JSON.stringify({
|
|
@@ -8813,7 +9044,7 @@ async function checkDrift(dir) {
|
|
|
8813
9044
|
return JSON.stringify({ exists: true, ...report, verification, hint });
|
|
8814
9045
|
}
|
|
8815
9046
|
async function generateSnapshotBatch(dir, offset = 0, batchSize = DEFAULT_BATCH_SIZE, files) {
|
|
8816
|
-
const rootDir =
|
|
9047
|
+
const rootDir = path35.resolve(dir);
|
|
8817
9048
|
const scoped = files !== void 0 && files.length > 0;
|
|
8818
9049
|
const scopeFiles = scoped ? sanitizePaths(rootDir, files) : void 0;
|
|
8819
9050
|
const batch = await prepareSnapshotBatch(dir, offset, batchSize, scopeFiles);
|
|
@@ -8860,7 +9091,7 @@ async function generateSnapshotBatch(dir, offset = 0, batchSize = DEFAULT_BATCH_
|
|
|
8860
9091
|
);
|
|
8861
9092
|
}
|
|
8862
9093
|
async function saveSnapshotPartial(dir, batchId, offset, features, flows) {
|
|
8863
|
-
const rootDir =
|
|
9094
|
+
const rootDir = path35.resolve(dir);
|
|
8864
9095
|
for (const feat of Object.values(features)) {
|
|
8865
9096
|
feat.files = sanitizePaths(rootDir, feat.files);
|
|
8866
9097
|
if (feat.tests) feat.tests = sanitizePaths(rootDir, feat.tests);
|
|
@@ -8889,7 +9120,7 @@ async function saveSnapshotPartial(dir, batchId, offset, features, flows) {
|
|
|
8889
9120
|
);
|
|
8890
9121
|
}
|
|
8891
9122
|
async function reduceSnapshot(dir) {
|
|
8892
|
-
const rootDir =
|
|
9123
|
+
const rootDir = path35.resolve(dir);
|
|
8893
9124
|
const partials = await loadAllPartials(rootDir);
|
|
8894
9125
|
if (partials.length === 0) {
|
|
8895
9126
|
return JSON.stringify(
|
|
@@ -8951,7 +9182,7 @@ async function reduceSnapshot(dir) {
|
|
|
8951
9182
|
);
|
|
8952
9183
|
}
|
|
8953
9184
|
async function fullAnalysis(dir) {
|
|
8954
|
-
const rootDir =
|
|
9185
|
+
const rootDir = path35.resolve(dir);
|
|
8955
9186
|
const [analysis, structure, samples, testMap, snapshot] = await Promise.all([
|
|
8956
9187
|
analyzeProject(dir),
|
|
8957
9188
|
getProjectStructure(dir),
|
|
@@ -8980,100 +9211,94 @@ function sanitizePaths(rootDir, files) {
|
|
|
8980
9211
|
return sanitizeRepoPaths(files);
|
|
8981
9212
|
}
|
|
8982
9213
|
async function saveSnapshotData(dir, features, flows, removeFeatures = [], removeFlows = []) {
|
|
8983
|
-
const rootDir =
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9214
|
+
const rootDir = path35.resolve(dir);
|
|
9215
|
+
return updateSnapshot(rootDir, async (previous) => {
|
|
9216
|
+
const gitHash = await getCurrentGitHash(rootDir);
|
|
9217
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9218
|
+
for (const feat of Object.values(features)) {
|
|
9219
|
+
feat.files = sanitizePaths(rootDir, feat.files);
|
|
9220
|
+
if (feat.tests) feat.tests = sanitizePaths(rootDir, feat.tests);
|
|
9221
|
+
feat.type = normalizeFeatureType(feat.type);
|
|
9222
|
+
}
|
|
9223
|
+
for (const flow of Object.values(flows)) {
|
|
9224
|
+
flow.chain = sanitizePaths(rootDir, flow.chain);
|
|
9225
|
+
}
|
|
9226
|
+
const partials = await loadAllPartials(rootDir);
|
|
9227
|
+
const replaceMode = partials.length > 0;
|
|
9228
|
+
const existing = replaceMode ? null : previous;
|
|
9229
|
+
const preserveVerification = (next, old) => {
|
|
9230
|
+
delete next.verifiedAt;
|
|
9231
|
+
delete next.verifiedHash;
|
|
9232
|
+
delete next.verificationToken;
|
|
9233
|
+
delete next.verificationFailed;
|
|
9234
|
+
delete next.verificationNote;
|
|
9235
|
+
if (!old) return;
|
|
9236
|
+
if (JSON.stringify(snapshotEntryContent(next)) !== JSON.stringify(snapshotEntryContent(old))) return;
|
|
9237
|
+
next.verificationToken = old.verificationToken;
|
|
9238
|
+
next.verifiedAt = old.verifiedAt;
|
|
9239
|
+
next.verifiedHash = old.verifiedHash;
|
|
9240
|
+
next.verificationFailed = old.verificationFailed;
|
|
9241
|
+
next.verificationNote = old.verificationNote;
|
|
9242
|
+
};
|
|
9243
|
+
for (const [name, entry] of Object.entries(features)) preserveVerification(entry, previous?.features[name]);
|
|
9244
|
+
for (const [name, entry] of Object.entries(flows)) preserveVerification(entry, previous?.flows[name]);
|
|
9245
|
+
if (existing) {
|
|
9246
|
+
if (existing.gitHash !== "unknown") {
|
|
9247
|
+
for (const feat of Object.values(existing.features)) {
|
|
9248
|
+
feat.refreshedHash ??= existing.gitHash;
|
|
9249
|
+
}
|
|
9250
|
+
for (const flow of Object.values(existing.flows)) {
|
|
9251
|
+
flow.refreshedHash ??= existing.gitHash;
|
|
9252
|
+
}
|
|
9019
9253
|
}
|
|
9020
|
-
|
|
9021
|
-
|
|
9254
|
+
const removedFeatures = removeFeatures.filter(
|
|
9255
|
+
(name) => name in existing.features
|
|
9256
|
+
);
|
|
9257
|
+
const removedFlows = removeFlows.filter((name) => name in existing.flows);
|
|
9258
|
+
for (const name of removedFeatures) delete existing.features[name];
|
|
9259
|
+
for (const name of removedFlows) delete existing.flows[name];
|
|
9260
|
+
if (gitHash !== "unknown") {
|
|
9261
|
+
for (const feat of Object.values(features)) feat.refreshedHash = gitHash;
|
|
9262
|
+
for (const flow of Object.values(flows)) flow.refreshedHash = gitHash;
|
|
9022
9263
|
}
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
version: 2,
|
|
9051
|
-
createdAt: now,
|
|
9052
|
-
updatedAt: now,
|
|
9053
|
-
gitHash,
|
|
9054
|
-
features,
|
|
9055
|
-
flows
|
|
9056
|
-
};
|
|
9057
|
-
await saveSnapshot(rootDir, snapshot);
|
|
9058
|
-
await clearAllPartials(rootDir);
|
|
9059
|
-
return JSON.stringify({
|
|
9060
|
-
status: replaceMode ? "replaced" : "created",
|
|
9061
|
-
mode: replaceMode ? "replaced-from-partials" : "fresh",
|
|
9062
|
-
features: Object.keys(features).length,
|
|
9063
|
-
flows: Object.keys(flows).length
|
|
9264
|
+
existing.features = { ...existing.features, ...features };
|
|
9265
|
+
existing.flows = { ...existing.flows, ...flows };
|
|
9266
|
+
existing.updatedAt = now;
|
|
9267
|
+
existing.gitHash = gitHash;
|
|
9268
|
+
return { snapshot: existing, afterSave: () => clearAllPartials(rootDir), result: JSON.stringify({
|
|
9269
|
+
status: "updated",
|
|
9270
|
+
mode: "merged",
|
|
9271
|
+
features: Object.keys(existing.features).length,
|
|
9272
|
+
flows: Object.keys(existing.flows).length,
|
|
9273
|
+
removedFeatures: removedFeatures.length,
|
|
9274
|
+
removedFlows: removedFlows.length
|
|
9275
|
+
}) };
|
|
9276
|
+
}
|
|
9277
|
+
const snapshot = {
|
|
9278
|
+
version: 2,
|
|
9279
|
+
createdAt: now,
|
|
9280
|
+
updatedAt: now,
|
|
9281
|
+
gitHash,
|
|
9282
|
+
features,
|
|
9283
|
+
flows
|
|
9284
|
+
};
|
|
9285
|
+
return { snapshot, afterSave: () => clearAllPartials(rootDir), result: JSON.stringify({
|
|
9286
|
+
status: replaceMode ? "replaced" : "created",
|
|
9287
|
+
mode: replaceMode ? "replaced-from-partials" : "fresh",
|
|
9288
|
+
features: Object.keys(features).length,
|
|
9289
|
+
flows: Object.keys(flows).length
|
|
9290
|
+
}) };
|
|
9064
9291
|
});
|
|
9065
9292
|
}
|
|
9066
9293
|
async function getImpact(dir, files) {
|
|
9067
|
-
const rootDir =
|
|
9294
|
+
const rootDir = path35.resolve(dir);
|
|
9068
9295
|
const { analyzeImpact: analyzeImpact2 } = await Promise.resolve().then(() => (init_impact(), impact_exports));
|
|
9069
9296
|
const result = await analyzeImpact2(rootDir, files);
|
|
9070
9297
|
return JSON.stringify(result, null, 2);
|
|
9071
9298
|
}
|
|
9072
9299
|
var VERIFY_DEFAULT_SAMPLE = 5;
|
|
9073
|
-
var VERIFY_MAX_FILES_PER_ENTRY = 8;
|
|
9074
|
-
var VERIFY_SKELETON_CHARS = 500;
|
|
9075
9300
|
async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
|
|
9076
|
-
const rootDir =
|
|
9301
|
+
const rootDir = path35.resolve(dir);
|
|
9077
9302
|
const snapshot = await loadSnapshot(rootDir);
|
|
9078
9303
|
if (!snapshot) {
|
|
9079
9304
|
return JSON.stringify({
|
|
@@ -9107,25 +9332,13 @@ async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
|
|
|
9107
9332
|
const picked = entries.slice(0, Math.max(1, sample));
|
|
9108
9333
|
const toVerify = [];
|
|
9109
9334
|
for (const entry of picked) {
|
|
9110
|
-
const
|
|
9111
|
-
for (const filePath of entry.files.slice(0, VERIFY_MAX_FILES_PER_ENTRY)) {
|
|
9112
|
-
const full = await access.read(filePath);
|
|
9113
|
-
if (full) {
|
|
9114
|
-
skeletons.push({
|
|
9115
|
-
path: full.path,
|
|
9116
|
-
content: full.content.slice(0, VERIFY_SKELETON_CHARS)
|
|
9117
|
-
});
|
|
9118
|
-
} else {
|
|
9119
|
-
skeletons.push({ path: filePath, missing: true });
|
|
9120
|
-
}
|
|
9121
|
-
}
|
|
9335
|
+
const stored = entry.kind === "feature" ? snapshot.features[entry.name] : snapshot.flows[entry.name];
|
|
9122
9336
|
toVerify.push({
|
|
9123
9337
|
name: entry.name,
|
|
9124
9338
|
kind: entry.kind,
|
|
9125
9339
|
description: entry.description,
|
|
9126
9340
|
lastVerified: entry.verifiedAt ?? "never",
|
|
9127
|
-
|
|
9128
|
-
truncated: entry.files.length > VERIFY_MAX_FILES_PER_ENTRY
|
|
9341
|
+
...await prepareSnapshotReview(access, entry.kind, entry.name, stored)
|
|
9129
9342
|
});
|
|
9130
9343
|
}
|
|
9131
9344
|
const neverVerified = entries.filter((e) => !e.verifiedAt).length;
|
|
@@ -9134,58 +9347,94 @@ async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
|
|
|
9134
9347
|
totalEntries: entries.length,
|
|
9135
9348
|
neverVerified,
|
|
9136
9349
|
entries: toVerify,
|
|
9137
|
-
instructions: '
|
|
9350
|
+
instructions: 'Judge whether the files implement each claimed feature/flow, then call save_verification with verdicts: {"<entry name>": {"kind": "<feature|flow from the entry>", "reviewToken": "<token from the entry>", "ok": true|false, "note": "<required when ok is false>"}}. Tokens cover the entry and full contents of the previewed files (at most 8), including unavailable-file markers; previews themselves are truncated. This is a spot-check, not proof of unshown files. A conflict requires verify_snapshot and a fresh review; never reuse a verdict for changed content. If a feature and flow share a name, submit their verdicts in separate calls.'
|
|
9138
9351
|
});
|
|
9139
9352
|
}
|
|
9140
9353
|
async function saveVerification(dir, verdicts) {
|
|
9141
|
-
const rootDir =
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
|
|
9354
|
+
const rootDir = path35.resolve(dir);
|
|
9355
|
+
return updateSnapshot(rootDir, async (snapshot) => {
|
|
9356
|
+
if (!snapshot) {
|
|
9357
|
+
return { snapshot: null, result: JSON.stringify({ exists: false, stamped: [], hint: "No concept map exists. Build it and run verify_snapshot before recording verdicts." }) };
|
|
9358
|
+
}
|
|
9359
|
+
const stamped = [], unknown = [], failed = [];
|
|
9360
|
+
const conflicts = [], reviewRequired = [], invalid = [];
|
|
9361
|
+
const candidates = [];
|
|
9362
|
+
let access;
|
|
9363
|
+
for (const [name, verdict] of Object.entries(verdicts)) {
|
|
9364
|
+
if (!verdict.kind || !["feature", "flow"].includes(verdict.kind) || !/^[a-f0-9]{64}$/.test(verdict.reviewToken ?? "")) {
|
|
9365
|
+
reviewRequired.push(name);
|
|
9366
|
+
continue;
|
|
9367
|
+
}
|
|
9368
|
+
if (typeof verdict.ok !== "boolean" || !verdict.ok && !verdict.note?.trim()) {
|
|
9369
|
+
invalid.push(name);
|
|
9370
|
+
continue;
|
|
9371
|
+
}
|
|
9372
|
+
const collection = verdict.kind === "feature" ? snapshot.features : snapshot.flows;
|
|
9373
|
+
if (!Object.hasOwn(collection, name)) {
|
|
9374
|
+
unknown.push(name);
|
|
9375
|
+
conflicts.push(name);
|
|
9376
|
+
continue;
|
|
9377
|
+
}
|
|
9378
|
+
const entry = collection[name];
|
|
9379
|
+
access ??= await createFileAccess(rootDir);
|
|
9380
|
+
const current = await prepareSnapshotReview(access, verdict.kind, name, entry);
|
|
9381
|
+
if (current.reviewToken !== verdict.reviewToken) conflicts.push(name);
|
|
9382
|
+
else candidates.push({ name, verdict, entry });
|
|
9383
|
+
}
|
|
9384
|
+
const verifiedHash = candidates.length ? await getCurrentGitHash(rootDir) : "unknown";
|
|
9385
|
+
const freshAccess = candidates.length ? await createFileAccess(rootDir) : null;
|
|
9386
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9387
|
+
for (const { name, verdict, entry } of candidates) {
|
|
9388
|
+
const current = await prepareSnapshotReview(freshAccess, verdict.kind, name, entry);
|
|
9389
|
+
if (current.reviewToken !== verdict.reviewToken) {
|
|
9390
|
+
conflicts.push(name);
|
|
9391
|
+
continue;
|
|
9392
|
+
}
|
|
9393
|
+
entry.verifiedAt = now;
|
|
9394
|
+
entry.verifiedHash = verifiedHash;
|
|
9395
|
+
entry.verificationToken = verdict.reviewToken;
|
|
9396
|
+
if (verdict.ok) {
|
|
9397
|
+
delete entry.verificationFailed;
|
|
9398
|
+
delete entry.verificationNote;
|
|
9399
|
+
} else {
|
|
9400
|
+
entry.verificationFailed = true;
|
|
9401
|
+
entry.verificationNote = verdict.note.trim();
|
|
9402
|
+
failed.push(name);
|
|
9403
|
+
}
|
|
9404
|
+
stamped.push(name);
|
|
9405
|
+
}
|
|
9406
|
+
const rejected = conflicts.length + reviewRequired.length + invalid.length;
|
|
9407
|
+
const status = rejected ? stamped.length ? "partial" : conflicts.length ? "conflict" : reviewRequired.length ? "review_required" : "invalid" : stamped.length ? "saved" : "unchanged";
|
|
9408
|
+
const hints = [];
|
|
9409
|
+
if (conflicts.length) hints.push(`Entries [${conflicts.join(", ")}] changed or were deleted after review. Run verify_snapshot and review current evidence before retrying.`);
|
|
9410
|
+
if (reviewRequired.length) hints.push(`Entries [${reviewRequired.join(", ")}] need kind and reviewToken from verify_snapshot. Inspect that evidence before submitting a verdict.`);
|
|
9411
|
+
if (invalid.length) hints.push(`Entries [${invalid.join(", ")}] need a boolean ok and a non-empty note when ok is false.`);
|
|
9412
|
+
if (failed.length) hints.push(`Entries [${failed.join(", ")}] are mis-mapped. Re-map them: read their actual files, correct the entries, and call save_snapshot with only those entries (plus removeFeatures/removeFlows if a concept no longer exists). Then run verify_snapshot for fresh review tokens.`);
|
|
9413
|
+
if (!hints.length) hints.push(stamped.length ? "All submitted entries verified against the reviewed evidence. Re-run verify_snapshot periodically." : "No verdicts submitted; nothing changed.");
|
|
9414
|
+
if (stamped.length) snapshot.updatedAt = now;
|
|
9415
|
+
return { snapshot: stamped.length ? snapshot : null, result: JSON.stringify({
|
|
9416
|
+
status,
|
|
9417
|
+
stamped,
|
|
9418
|
+
unknown,
|
|
9419
|
+
failed,
|
|
9420
|
+
conflicts,
|
|
9421
|
+
reviewRequired,
|
|
9422
|
+
invalid,
|
|
9423
|
+
hint: hints.join(" ")
|
|
9424
|
+
}) };
|
|
9176
9425
|
});
|
|
9177
9426
|
}
|
|
9178
9427
|
async function saveDecision(dir, input) {
|
|
9179
|
-
const rootDir =
|
|
9428
|
+
const rootDir = path35.resolve(dir);
|
|
9180
9429
|
const { upsertDecision: upsertDecision2 } = await Promise.resolve().then(() => (init_decisions(), decisions_exports));
|
|
9181
9430
|
const result = await upsertDecision2(rootDir, input);
|
|
9182
9431
|
return JSON.stringify(result);
|
|
9183
9432
|
}
|
|
9184
9433
|
async function reviewDecision2(dir, input) {
|
|
9185
|
-
return JSON.stringify(await reviewDecision(
|
|
9434
|
+
return JSON.stringify(await reviewDecision(path35.resolve(dir), input));
|
|
9186
9435
|
}
|
|
9187
9436
|
async function getContext(dir, task, files) {
|
|
9188
|
-
const rootDir =
|
|
9437
|
+
const rootDir = path35.resolve(dir);
|
|
9189
9438
|
const { assembleContext: assembleContext2 } = await Promise.resolve().then(() => (init_assemble(), assemble_exports));
|
|
9190
9439
|
const bundle = await assembleContext2(rootDir, task, files);
|
|
9191
9440
|
return JSON.stringify(bundle);
|
|
@@ -9234,7 +9483,7 @@ async function masonInit(dir, options = {}) {
|
|
|
9234
9483
|
return JSON.stringify(await setupProject2(dir, options), null, 2);
|
|
9235
9484
|
}
|
|
9236
9485
|
if (options.host) throw new Error("host applies only to mode: setup.");
|
|
9237
|
-
const rootDir =
|
|
9486
|
+
const rootDir = path35.resolve(dir);
|
|
9238
9487
|
const marker = await loadProjectMarker(rootDir);
|
|
9239
9488
|
const { loadSetup: loadSetup2 } = await Promise.resolve().then(() => (init_model(), model_exports));
|
|
9240
9489
|
const setup = await loadSetup2(rootDir);
|
|
@@ -9254,7 +9503,7 @@ async function masonInit(dir, options = {}) {
|
|
|
9254
9503
|
);
|
|
9255
9504
|
}
|
|
9256
9505
|
async function masonCompleteInit(dir, options = {}) {
|
|
9257
|
-
const rootDir =
|
|
9506
|
+
const rootDir = path35.resolve(dir);
|
|
9258
9507
|
const existing = await loadProjectMarker(rootDir);
|
|
9259
9508
|
const features = await loadProjectFeatures(rootDir);
|
|
9260
9509
|
const marker = {
|
|
@@ -9363,7 +9612,7 @@ async function masonSetConfluence(input) {
|
|
|
9363
9612
|
);
|
|
9364
9613
|
}
|
|
9365
9614
|
async function exportToConfluenceTool(dir, overrides) {
|
|
9366
|
-
const rootDir =
|
|
9615
|
+
const rootDir = path35.resolve(dir);
|
|
9367
9616
|
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports2));
|
|
9368
9617
|
const { exportToConfluence: exportToConfluence2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
9369
9618
|
const config = await loadConfig2();
|
|
@@ -9402,10 +9651,10 @@ function createMcpServer() {
|
|
|
9402
9651
|
const server = new McpServer(
|
|
9403
9652
|
{
|
|
9404
9653
|
name: "mason",
|
|
9405
|
-
version: "0.
|
|
9654
|
+
version: "0.19.0"
|
|
9406
9655
|
},
|
|
9407
9656
|
{
|
|
9408
|
-
instructions: (setupNotice ? setupNotice + "\n\n" : "") + 'Mason retrieves recorded decisions, file impact, and optional feature/flow maps. Use get_context with the task and known files, and get_impact before editing. Save learned rationale and constraints with save_decision; review and commit the local records through the project workflow. These tools work without initialization or a concept map. Consult trust and diagnostics: changed or unknown freshness needs source inspection,
|
|
9657
|
+
instructions: (setupNotice ? setupNotice + "\n\n" : "") + 'Mason retrieves recorded decisions, file impact, and optional feature/flow maps. Use get_context with the task and known files, and get_impact before editing. Save learned rationale and constraints with save_decision; review and commit the local records through the project workflow. These tools work without initialization or a concept map. Consult trust and diagnostics: changed or unknown freshness needs source inspection, stale or unknown map verification needs a fresh review, recorded failed verdicts need correction, and proposals are suggestions and legacy records are unreviewed. Accepted decisions are recorded team constraints, still subject to freshness checks. Use review_decision to prepare code evidence and record only authorized acceptance, reaffirmation, or retirement. Never invent a reviewer or treat these recorded identities as authenticated approval. mason_init returns documentation audit and committed-diff review findings with a quickstart guide. Use mode: "map" only when a full architecture map is requested. A missing map is not a setup failure; use decisions and source evidence. get_snapshot provides architecture navigation when a map is available. The mason-audit and mason-review CLIs also work without setup.'
|
|
9409
9658
|
}
|
|
9410
9659
|
);
|
|
9411
9660
|
server.tool(
|
|
@@ -9527,7 +9776,7 @@ function createMcpServer() {
|
|
|
9527
9776
|
);
|
|
9528
9777
|
server.tool(
|
|
9529
9778
|
"get_snapshot",
|
|
9530
|
-
"Return the optional feature-to-file architecture map with drift and trust evidence. If no map exists, returns exists:false plus project structure, Git signals, and test pairs. Decision capture, get_context, and get_impact still work. No initialization required.",
|
|
9779
|
+
"Return the optional feature-to-file architecture map with drift and trust evidence. Map verification is rechecked against sampled source contents; stale or unknown verification requires a fresh review, and recordedVerdict preserves the historical verdict. If no map exists, returns exists:false plus project structure, Git signals, and test pairs. Decision capture, get_context, and get_impact still work. No initialization required.",
|
|
9531
9780
|
{
|
|
9532
9781
|
dir: z24.string().describe("Absolute path to the project root directory")
|
|
9533
9782
|
},
|
|
@@ -9540,7 +9789,7 @@ function createMcpServer() {
|
|
|
9540
9789
|
);
|
|
9541
9790
|
server.tool(
|
|
9542
9791
|
"get_context",
|
|
9543
|
-
"Assemble task context: matching decisions with rationale, approval, owner, sources, last review, and freshness, plus related tests, file impact, and any available map entries. Proposals are suggestions; legacy records are unreviewed; accepted decisions are constraints subject to freshness. No initialization or map required. Pass task and optional files. map.status and diagnostics preserve missing or invalid knowledge. Impact covers up to three unique files, expanding directory anchors.",
|
|
9792
|
+
"Assemble task context: matching decisions with rationale, approval, owner, sources, last review, and freshness, plus related tests, file impact, and any available map entries. Map verification is rechecked against sampled source contents; stale or unknown verification requires a fresh review, and recordedVerdict preserves the historical verdict. Proposals are suggestions; legacy records are unreviewed; accepted decisions are constraints subject to freshness. No initialization or map required. Pass task and optional files. map.status and diagnostics preserve missing or invalid knowledge. Impact covers up to three unique files, expanding directory anchors.",
|
|
9544
9793
|
{
|
|
9545
9794
|
dir: z24.string().describe("Absolute path to the project root directory"),
|
|
9546
9795
|
task: z24.string().describe("The task, bug, or change request in natural language \u2014 e.g. 'add rate limiting to the API client' or a ticket description"),
|
|
@@ -9716,7 +9965,7 @@ function createMcpServer() {
|
|
|
9716
9965
|
);
|
|
9717
9966
|
server.tool(
|
|
9718
9967
|
"verify_snapshot",
|
|
9719
|
-
"Spot-check the concept map's CORRECTNESS (drift checks freshness; this checks entries were right to begin with). Returns a sample of entries \u2014 always the never-verified and least-recently-verified first \u2014 with skeletons of their claimed files, for you to judge whether the files actually implement what the entry claims. Report verdicts back via save_verification. Run periodically, or after an automated refresh wrote entries no human reviewed.",
|
|
9968
|
+
"Spot-check the concept map's CORRECTNESS (drift checks freshness; this checks entries were right to begin with). Returns a sample of entries \u2014 always the never-verified and least-recently-verified first \u2014 with skeletons of their claimed files, for you to judge whether the files actually implement what the entry claims. Report verdicts back via save_verification with each entry\u2019s kind and reviewToken. Tokens bind the entry and bounded source evidence; changed evidence requires a fresh review. Run periodically, or after an automated refresh wrote entries no human reviewed.",
|
|
9720
9969
|
{
|
|
9721
9970
|
dir: z24.string().describe("Absolute path to the project root directory"),
|
|
9722
9971
|
sample: z24.number().int().optional().describe("Entries to sample (default 5)")
|
|
@@ -9728,11 +9977,13 @@ function createMcpServer() {
|
|
|
9728
9977
|
);
|
|
9729
9978
|
server.tool(
|
|
9730
9979
|
"save_verification",
|
|
9731
|
-
"Record verify_snapshot verdicts. Entries judged ok are stamped verifiedAt; failures are flagged verificationFailed with your note and surface in get_context, get_snapshot, and mason_check_drift until corrected. Verdict notes are required for failures.",
|
|
9980
|
+
"Record verify_snapshot verdicts using each entry\u2019s kind and reviewToken. Missing tokens request a fresh review; changed or deleted entries return conflicts without being stamped. Entries judged ok are stamped verifiedAt; failures are flagged verificationFailed with your note and surface in get_context, get_snapshot, and mason_check_drift until corrected. Verdict notes are required for failures.",
|
|
9732
9981
|
{
|
|
9733
9982
|
dir: z24.string().describe("Absolute path to the project root directory"),
|
|
9734
9983
|
verdicts: z24.record(
|
|
9735
9984
|
z24.object({
|
|
9985
|
+
kind: z24.enum(["feature", "flow"]).optional().describe("Entry kind returned by verify_snapshot; required to record a verdict"),
|
|
9986
|
+
reviewToken: z24.string().optional().describe("Copy the reviewToken from verify_snapshot after inspecting its evidence; required to record a verdict"),
|
|
9736
9987
|
ok: z24.boolean(),
|
|
9737
9988
|
note: z24.string().optional().describe("One line on what's wrong \u2014 required when ok is false")
|
|
9738
9989
|
})
|
|
@@ -9781,14 +10032,592 @@ function createMcpServer() {
|
|
|
9781
10032
|
);
|
|
9782
10033
|
return server;
|
|
9783
10034
|
}
|
|
9784
|
-
async function startMcpServer() {
|
|
10035
|
+
async function startMcpServer(onClose) {
|
|
9785
10036
|
const server = createMcpServer();
|
|
10037
|
+
if (onClose) server.server.onclose = onClose;
|
|
9786
10038
|
const transport = new StdioServerTransport();
|
|
9787
10039
|
await server.connect(transport);
|
|
9788
10040
|
}
|
|
9789
10041
|
|
|
10042
|
+
// src/distribution/updates.ts
|
|
10043
|
+
init_store();
|
|
10044
|
+
init_storage();
|
|
10045
|
+
import fs33 from "fs/promises";
|
|
10046
|
+
import path41 from "path";
|
|
10047
|
+
import { spawn as spawn3 } from "child_process";
|
|
10048
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
10049
|
+
import { z as z30 } from "zod";
|
|
10050
|
+
|
|
10051
|
+
// src/distribution/state.ts
|
|
10052
|
+
init_storage();
|
|
10053
|
+
import os5 from "os";
|
|
10054
|
+
import path37 from "path";
|
|
10055
|
+
import { fileURLToPath } from "url";
|
|
10056
|
+
import fs28 from "fs/promises";
|
|
10057
|
+
import { z as z26 } from "zod";
|
|
10058
|
+
|
|
10059
|
+
// src/distribution/path.ts
|
|
10060
|
+
import fs27 from "fs/promises";
|
|
10061
|
+
import os4 from "os";
|
|
10062
|
+
import path36 from "path";
|
|
10063
|
+
import { execFile as execFile12 } from "child_process";
|
|
10064
|
+
import { promisify as promisify12 } from "util";
|
|
10065
|
+
import { createHash as createHash7, randomUUID as randomUUID7 } from "crypto";
|
|
10066
|
+
import { z as z25 } from "zod";
|
|
10067
|
+
var execute2 = promisify12(execFile12);
|
|
10068
|
+
var pathChangeSchema = z25.discriminatedUnion("kind", [
|
|
10069
|
+
z25.object({ kind: z25.literal("profile"), file: z25.string(), block: z25.string(), created: z25.boolean() }),
|
|
10070
|
+
z25.object({ kind: z25.literal("windows"), entry: z25.string(), created: z25.boolean() })
|
|
10071
|
+
]);
|
|
10072
|
+
var quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
|
10073
|
+
async function configurePath(bin, previous, save, options = {}) {
|
|
10074
|
+
const env = options.env ?? process.env, platform = options.platform ?? process.platform;
|
|
10075
|
+
const changes = [...previous];
|
|
10076
|
+
const currentPath = platform === "win32" ? Object.entries(env).find(([key]) => key.toLowerCase() === "path")?.[1] : env.PATH;
|
|
10077
|
+
const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) => path36.posix.isAbsolute(entry) && path36.posix.resolve(entry) === path36.posix.resolve(bin));
|
|
10078
|
+
const setupHint = "To connect a project, run one of these inside its directory:\n Codex: mason setup --host codex\n Claude Code: mason setup --host claude";
|
|
10079
|
+
const message2 = (alreadyOnPath ? "" : "Open a new terminal to use mason.\n") + setupHint;
|
|
10080
|
+
const manual = (reason2) => ({ status: "manual", changes, message: `${reason2}
|
|
10081
|
+
${alreadyOnPath ? "" : `Add ${bin} to your PATH.
|
|
10082
|
+
`}${setupHint}` });
|
|
10083
|
+
if (env.MASON_NO_MODIFY_PATH === "1") return manual("Automatic PATH setup is disabled (MASON_NO_MODIFY_PATH=1).");
|
|
10084
|
+
if (/[\r\n\0]/.test(bin) || bin.includes(platform === "win32" ? ";" : ":")) return manual("This installation path cannot be represented safely in PATH.");
|
|
10085
|
+
try {
|
|
10086
|
+
if (platform === "win32") {
|
|
10087
|
+
const current = await windowsPath("read");
|
|
10088
|
+
if (!hasWindowsEntry(current.value, bin)) {
|
|
10089
|
+
if (!changes.some((c) => c.kind === "windows" && c.entry === bin)) {
|
|
10090
|
+
changes.push({ kind: "windows", entry: bin, created: current.value === null });
|
|
10091
|
+
await save(changes);
|
|
10092
|
+
}
|
|
10093
|
+
await windowsPath("add", bin, current);
|
|
10094
|
+
}
|
|
10095
|
+
return { status: "configured", changes, message: message2 };
|
|
10096
|
+
}
|
|
10097
|
+
const shell = path36.basename(env.SHELL ?? os4.userInfo().shell ?? "");
|
|
10098
|
+
const userHome = options.userHome ?? os4.homedir();
|
|
10099
|
+
let profiles2;
|
|
10100
|
+
if (!["bash", "zsh", "sh"].includes(shell)) return manual(`Automatic PATH setup is unavailable for ${shell || "an unknown shell"}.`);
|
|
10101
|
+
if (env.MASON_PROFILE) profiles2 = [path36.resolve(env.MASON_PROFILE)];
|
|
10102
|
+
else if (shell === "zsh") profiles2 = [path36.join(env.ZDOTDIR || userHome, ".zshrc")];
|
|
10103
|
+
else if (shell === "sh") profiles2 = [path36.join(userHome, ".profile")];
|
|
10104
|
+
else {
|
|
10105
|
+
const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path36.join(userHome, name));
|
|
10106
|
+
let login = candidates[2];
|
|
10107
|
+
for (const file of candidates) if (await fs27.lstat(file).then(() => true, (error) => {
|
|
10108
|
+
if (error.code === "ENOENT") return false;
|
|
10109
|
+
throw error;
|
|
10110
|
+
})) {
|
|
10111
|
+
login = file;
|
|
10112
|
+
break;
|
|
10113
|
+
}
|
|
10114
|
+
profiles2 = [path36.join(userHome, ".bashrc"), login];
|
|
10115
|
+
}
|
|
10116
|
+
const marker = `# >>> mason PATH ${createHash7("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
|
|
10117
|
+
for (const profile of profiles2) {
|
|
10118
|
+
const stat = await fs27.lstat(profile).catch((error) => {
|
|
10119
|
+
if (error.code === "ENOENT") return null;
|
|
10120
|
+
throw error;
|
|
10121
|
+
});
|
|
10122
|
+
const file = stat?.isSymbolicLink() ? await fs27.realpath(profile) : path36.resolve(profile);
|
|
10123
|
+
const original = await fs27.readFile(file).catch((error) => {
|
|
10124
|
+
if (error.code === "ENOENT" && !stat) return null;
|
|
10125
|
+
throw error;
|
|
10126
|
+
});
|
|
10127
|
+
if (original && !Buffer.from(original.toString("utf8")).equals(original)) return manual(`Shell profile is not UTF-8: ${profile}.`);
|
|
10128
|
+
const text2 = original?.toString("utf8") ?? "";
|
|
10129
|
+
const owned = changes.find((c) => c.kind === "profile" && c.file === file);
|
|
10130
|
+
if (owned && text2.includes(owned.block)) continue;
|
|
10131
|
+
if (text2.includes(marker) || owned && text2.includes("# <<< mason PATH")) return manual(`Mason's PATH block was edited; retained ${profile}.`);
|
|
10132
|
+
const newline = text2.includes("\r\n") ? "\r\n" : "\n";
|
|
10133
|
+
const block = owned?.block ?? ("\n" + marker + '\ncase ":${PATH-}:" in\n *:' + quote(bin) + ":*) ;;\n *) export PATH=" + quote(bin) + ':"${PATH-}" ;;\nesac\n# <<< mason PATH <<<\n').replaceAll("\n", newline);
|
|
10134
|
+
if (!owned) {
|
|
10135
|
+
changes.push({ kind: "profile", file, block, created: original === null });
|
|
10136
|
+
await save(changes);
|
|
10137
|
+
}
|
|
10138
|
+
await fs27.mkdir(path36.dirname(file), { recursive: true });
|
|
10139
|
+
const handle = await fs27.open(file, original === null ? "ax+" : "a+", 384);
|
|
10140
|
+
try {
|
|
10141
|
+
if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0))) throw new Error(`Shell profile changed during installation: ${profile}`);
|
|
10142
|
+
await handle.writeFile(block);
|
|
10143
|
+
await handle.sync();
|
|
10144
|
+
} finally {
|
|
10145
|
+
await handle.close();
|
|
10146
|
+
}
|
|
10147
|
+
}
|
|
10148
|
+
return { status: "configured", changes, message: message2 };
|
|
10149
|
+
} catch (error) {
|
|
10150
|
+
return manual(`Could not configure PATH: ${error instanceof Error ? error.message : String(error)}`);
|
|
10151
|
+
}
|
|
10152
|
+
}
|
|
10153
|
+
var normalizeWindowsEntry = (value) => path36.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
|
|
10154
|
+
var hasWindowsEntry = (value, entry, env = process.env) => {
|
|
10155
|
+
const variables = new Map(Object.entries(env).map(([key, value2]) => [key.toLowerCase(), value2]));
|
|
10156
|
+
const expand = (part) => part.replace(/%([^%]+)%/g, (match, key) => variables.get(key.toLowerCase()) ?? match);
|
|
10157
|
+
return (value ?? "").split(";").some((part) => normalizeWindowsEntry(expand(part)) === normalizeWindowsEntry(entry));
|
|
10158
|
+
};
|
|
10159
|
+
async function windowsPath(action, entry = "", expected, removeEmpty = false) {
|
|
10160
|
+
const payload = Buffer.from(JSON.stringify({ action, entry, expected, removeEmpty })).toString("base64");
|
|
10161
|
+
const script = `$ErrorActionPreference = 'Stop'
|
|
10162
|
+
[Console]::OutputEncoding = New-Object Text.UTF8Encoding($false)
|
|
10163
|
+
$p = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${payload}')) | ConvertFrom-Json
|
|
10164
|
+
$key = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment')
|
|
10165
|
+
try {
|
|
10166
|
+
$value = $key.GetValue('Path', $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
|
|
10167
|
+
$kind = if ($null -eq $value) { $null } else { $key.GetValueKind('Path').ToString() }
|
|
10168
|
+
if ($null -ne $kind -and $kind -notin @('String','ExpandString')) { throw 'User PATH is not a string.' }
|
|
10169
|
+
if ($p.action -eq 'read') { @{ value = $value; kind = $kind } | ConvertTo-Json -Compress; exit 0 }
|
|
10170
|
+
if ($p.expected -and ($value -cne $p.expected.value -or $kind -cne $p.expected.kind)) { throw 'User PATH changed concurrently; retry the operation.' }
|
|
10171
|
+
$parts = if ($null -eq $value -or $value -eq '') { @() } else { @($value.Split(';')) }
|
|
10172
|
+
if ($p.action -eq 'add') { $next = (@($p.entry) + $parts) -join ';' }
|
|
10173
|
+
else { $next = (@($parts | Where-Object { $_ -cne $p.entry })) -join ';' }
|
|
10174
|
+
if ($next -cne $value) {
|
|
10175
|
+
$writeKind = if ($kind) { [Microsoft.Win32.RegistryValueKind]$kind } else { [Microsoft.Win32.RegistryValueKind]::ExpandString }
|
|
10176
|
+
if ($p.action -eq 'remove' -and $p.removeEmpty -and $next -eq '') { $key.DeleteValue('Path', $false) }
|
|
10177
|
+
else { $key.SetValue('Path', $next, $writeKind) }
|
|
10178
|
+
Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public class MasonEnvironment { [DllImport("user32.dll", CharSet=CharSet.Unicode, SetLastError=true)] public static extern IntPtr SendMessageTimeout(IntPtr h, uint m, UIntPtr w, string l, uint f, uint t, out UIntPtr r); }'
|
|
10179
|
+
$result = [UIntPtr]::Zero
|
|
10180
|
+
[void][MasonEnvironment]::SendMessageTimeout([IntPtr]0xffff, 0x1a, [UIntPtr]::Zero, 'Environment', 2, 1000, [ref]$result)
|
|
10181
|
+
}
|
|
10182
|
+
@{ value = $next; kind = $kind } | ConvertTo-Json -Compress
|
|
10183
|
+
} finally { $key.Dispose() }
|
|
10184
|
+
`;
|
|
10185
|
+
const { stdout } = await execute2("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { windowsHide: true, timeout: 15e3, maxBuffer: 1024 * 1024 });
|
|
10186
|
+
return JSON.parse(stdout.replace(/^\uFEFF/, "").trim());
|
|
10187
|
+
}
|
|
10188
|
+
|
|
10189
|
+
// src/distribution/state.ts
|
|
10190
|
+
var releaseVersion = z26.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/);
|
|
10191
|
+
var bundleId = z26.string().regex(/^[a-f0-9]{24}$/);
|
|
10192
|
+
var updatePolicySchema = z26.object({
|
|
10193
|
+
enabled: z26.boolean(),
|
|
10194
|
+
revision: z26.string(),
|
|
10195
|
+
pinnedVersion: releaseVersion.optional(),
|
|
10196
|
+
mirror: z26.string().optional()
|
|
10197
|
+
});
|
|
10198
|
+
var recordSchema3 = z26.object({
|
|
10199
|
+
format: z26.literal(1),
|
|
10200
|
+
home: z26.string(),
|
|
10201
|
+
bin: z26.string(),
|
|
10202
|
+
current: bundleId,
|
|
10203
|
+
version: releaseVersion,
|
|
10204
|
+
launchers: z26.record(z26.string()),
|
|
10205
|
+
previousLaunchers: z26.record(z26.string()).optional(),
|
|
10206
|
+
pathChanges: z26.array(pathChangeSchema).optional(),
|
|
10207
|
+
updates: updatePolicySchema.optional(),
|
|
10208
|
+
previous: z26.object({ id: bundleId, version: releaseVersion }).optional(),
|
|
10209
|
+
pending: z26.object({ id: bundleId, version: releaseVersion, policyRevision: z26.string() }).optional()
|
|
10210
|
+
});
|
|
10211
|
+
async function readInstallation(home) {
|
|
10212
|
+
const raw = await readStoreJson(home, "install.json");
|
|
10213
|
+
if (!raw) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
|
|
10214
|
+
const record = recordSchema3.parse(raw);
|
|
10215
|
+
if (record.home !== await fs28.realpath(home)) throw new Error("Invalid standalone installation receipt.");
|
|
10216
|
+
return record;
|
|
10217
|
+
}
|
|
10218
|
+
function standaloneLocation() {
|
|
10219
|
+
const bundle = path37.resolve(path37.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
10220
|
+
const home = path37.dirname(path37.dirname(bundle));
|
|
10221
|
+
if (path37.dirname(bundle) !== path37.join(home, "versions") || !/^[a-f0-9]{24}$/.test(path37.basename(bundle))) {
|
|
10222
|
+
return null;
|
|
10223
|
+
}
|
|
10224
|
+
return { home, bundle };
|
|
10225
|
+
}
|
|
10226
|
+
async function currentInstallation() {
|
|
10227
|
+
const location2 = standaloneLocation();
|
|
10228
|
+
if (!location2) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
|
|
10229
|
+
const { home, bundle } = location2;
|
|
10230
|
+
const record = await readInstallation(home);
|
|
10231
|
+
return { home, bundle, record };
|
|
10232
|
+
}
|
|
10233
|
+
function automaticUpdatesAllowed(record, env = process.env) {
|
|
10234
|
+
return record.updates?.enabled === true && !record.updates.pinnedVersion && !record.updates.mirror && !env.CI && env.MASON_NO_AUTO_UPDATE !== "1" && !env.MASON_RELEASE_BASE && !env.MASON_VERSION;
|
|
10235
|
+
}
|
|
10236
|
+
function newerStableVersion(candidate, installed) {
|
|
10237
|
+
if (!/^\d+\.\d+\.\d+$/.test(candidate)) return false;
|
|
10238
|
+
const a = candidate.split(".").map(BigInt), b = installed.split("-")[0].split(".").map(BigInt);
|
|
10239
|
+
for (let i = 0; i < 3; i++) {
|
|
10240
|
+
if (a[i] !== b[i]) return a[i] > b[i];
|
|
10241
|
+
}
|
|
10242
|
+
return installed.includes("-");
|
|
10243
|
+
}
|
|
10244
|
+
|
|
10245
|
+
// src/distribution/bundle.ts
|
|
10246
|
+
init_storage();
|
|
10247
|
+
import fs29 from "fs/promises";
|
|
10248
|
+
import path38 from "path";
|
|
10249
|
+
import { createHash as createHash8 } from "crypto";
|
|
10250
|
+
import { z as z27 } from "zod";
|
|
10251
|
+
var targets = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
|
|
10252
|
+
var digest2 = z27.string().regex(/^[a-f0-9]{64}$/);
|
|
10253
|
+
var bundleSchema = z27.object({
|
|
10254
|
+
format: z27.literal(1),
|
|
10255
|
+
version: z27.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
|
|
10256
|
+
target: z27.enum(targets),
|
|
10257
|
+
nodeVersion: z27.string(),
|
|
10258
|
+
files: z27.record(digest2)
|
|
10259
|
+
}).strict();
|
|
10260
|
+
var sha256 = (bytes) => createHash8("sha256").update(bytes).digest("hex");
|
|
10261
|
+
async function readBundle(root) {
|
|
10262
|
+
const bytes = await fs29.readFile(await storePath(root, "bundle.json"));
|
|
10263
|
+
const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
|
|
10264
|
+
const windows = manifest.target.startsWith("win32-");
|
|
10265
|
+
for (const file of Object.keys(manifest.files)) {
|
|
10266
|
+
if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") || path38.isAbsolute(file)) {
|
|
10267
|
+
throw new Error("Unsafe path in Mason bundle: " + file);
|
|
10268
|
+
}
|
|
10269
|
+
}
|
|
10270
|
+
for (const required of [windows ? "node.exe" : "node", "app/package.json", "app/dist/mason.js", "app/dist/mason-auto.js", "app/dist/mason-mcp.js"]) {
|
|
10271
|
+
if (!manifest.files[required]) throw new Error("Incomplete Mason bundle: " + required);
|
|
10272
|
+
}
|
|
10273
|
+
return { manifest, manifestHash: sha256(bytes) };
|
|
10274
|
+
}
|
|
10275
|
+
async function verifyBundle(root, expectedHash) {
|
|
10276
|
+
const bundle = await readBundle(root);
|
|
10277
|
+
if (expectedHash && bundle.manifestHash !== expectedHash) throw new Error("Mason bundle manifest changed.");
|
|
10278
|
+
if (bundle.manifest.target !== `${process.platform}-${process.arch}`) throw new Error("Mason bundle is for another platform. Install this platform's bundle and rerun setup.");
|
|
10279
|
+
const files = Object.keys(bundle.manifest.files);
|
|
10280
|
+
for (let i = 0; i < files.length; i += 8) await Promise.all(files.slice(i, i + 8).map(async (file) => {
|
|
10281
|
+
if (sha256(await fs29.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
|
|
10282
|
+
}));
|
|
10283
|
+
return bundle;
|
|
10284
|
+
}
|
|
10285
|
+
|
|
10286
|
+
// src/distribution/runtime.ts
|
|
10287
|
+
init_store();
|
|
10288
|
+
init_storage();
|
|
10289
|
+
import fs30 from "fs/promises";
|
|
10290
|
+
import { unlinkSync } from "fs";
|
|
10291
|
+
import os6 from "os";
|
|
10292
|
+
import path39 from "path";
|
|
10293
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
10294
|
+
import { z as z28 } from "zod";
|
|
10295
|
+
var leaseSchema = z28.object({ pid: z28.number().int().positive(), host: z28.string(), bundle: z28.string().regex(/^[a-f0-9]{24}$/) });
|
|
10296
|
+
|
|
10297
|
+
// src/distribution/install.ts
|
|
10298
|
+
import fs32 from "fs/promises";
|
|
10299
|
+
import os8 from "os";
|
|
10300
|
+
import path40 from "path";
|
|
10301
|
+
import { execFile as execFile13, spawn as spawn2 } from "child_process";
|
|
10302
|
+
import { promisify as promisify13 } from "util";
|
|
10303
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
10304
|
+
init_storage();
|
|
10305
|
+
init_store();
|
|
10306
|
+
|
|
10307
|
+
// src/distribution/sessions.ts
|
|
10308
|
+
init_storage();
|
|
10309
|
+
import fs31 from "fs/promises";
|
|
10310
|
+
import os7 from "os";
|
|
10311
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
10312
|
+
import { z as z29 } from "zod";
|
|
10313
|
+
var leaseSchema2 = z29.object({ pid: z29.number().int().positive(), host: z29.string() });
|
|
10314
|
+
async function hasRunningMcp(home) {
|
|
10315
|
+
const directory = await storePath(home, "sessions");
|
|
10316
|
+
const files = await fs31.readdir(directory).catch((error) => {
|
|
10317
|
+
if (error.code === "ENOENT") return [];
|
|
10318
|
+
throw error;
|
|
10319
|
+
});
|
|
10320
|
+
for (const name of files) {
|
|
10321
|
+
if (!/^[a-f0-9-]+\.json$/.test(name)) return true;
|
|
10322
|
+
const relative = "sessions/" + name;
|
|
10323
|
+
try {
|
|
10324
|
+
const raw = await readStoreJson(home, relative);
|
|
10325
|
+
if (raw === null) continue;
|
|
10326
|
+
const lease = leaseSchema2.parse(raw);
|
|
10327
|
+
if (lease.host !== os7.hostname()) return true;
|
|
10328
|
+
try {
|
|
10329
|
+
process.kill(lease.pid, 0);
|
|
10330
|
+
return true;
|
|
10331
|
+
} catch (error) {
|
|
10332
|
+
if (error.code !== "ESRCH") return true;
|
|
10333
|
+
}
|
|
10334
|
+
await fs31.rm(await storePath(home, relative), { force: true });
|
|
10335
|
+
} catch {
|
|
10336
|
+
return true;
|
|
10337
|
+
}
|
|
10338
|
+
}
|
|
10339
|
+
return false;
|
|
10340
|
+
}
|
|
10341
|
+
async function registerMcp(home) {
|
|
10342
|
+
const name = `sessions/${randomUUID9()}.json`;
|
|
10343
|
+
await writeStoreJson(home, name, { pid: process.pid, host: os7.hostname() });
|
|
10344
|
+
return async () => {
|
|
10345
|
+
await fs31.rm(await storePath(home, name), { force: true }).catch(() => {
|
|
10346
|
+
});
|
|
10347
|
+
};
|
|
10348
|
+
}
|
|
10349
|
+
|
|
10350
|
+
// src/distribution/install.ts
|
|
10351
|
+
var shQuote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
|
10352
|
+
var psQuote = (s) => "'" + s.replace(/'/g, "''") + "'";
|
|
10353
|
+
var WINDOWS_BATCH_LAUNCHER = '@echo off\r\ngoto #_mason_handoff_# 2>NUL || title %COMSPEC% & powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0mason-launcher.ps1" %*\r\n';
|
|
10354
|
+
var launcherNames = ["mason", "mason.cmd", "mason.ps1", "mason-launcher.ps1"];
|
|
10355
|
+
async function replace(file, content2, mode = 384) {
|
|
10356
|
+
const temp = file + ".tmp-" + randomUUID10();
|
|
10357
|
+
try {
|
|
10358
|
+
await fs32.writeFile(temp, content2, { mode, flag: "wx" });
|
|
10359
|
+
await fs32.rename(temp, file);
|
|
10360
|
+
} finally {
|
|
10361
|
+
await fs32.rm(temp, { force: true });
|
|
10362
|
+
}
|
|
10363
|
+
}
|
|
10364
|
+
function assertUpdatePolicy(record, version, revision) {
|
|
10365
|
+
if (!record || !automaticUpdatesAllowed(record, { ...process.env, MASON_VERSION: "" }) || record.updates?.revision !== revision || !newerStableVersion(version, record.version)) {
|
|
10366
|
+
throw new Error("Update policy or installed version changed; download was not activated.");
|
|
10367
|
+
}
|
|
10368
|
+
}
|
|
10369
|
+
async function installLocked2(home, source, bundle, progress, options, alreadyStaged = false) {
|
|
10370
|
+
const raw = await readStoreJson(home, "install.json");
|
|
10371
|
+
const previous = raw === null ? null : recordSchema3.parse(raw);
|
|
10372
|
+
if (options.stageOnly) assertUpdatePolicy(previous, bundle.manifest.version, options.policyRevision);
|
|
10373
|
+
if (!previous) for (const file of await fs32.readdir(home)) {
|
|
10374
|
+
if (file === ".install-lock") continue;
|
|
10375
|
+
const directory = path40.join(home, file);
|
|
10376
|
+
const stat = await fs32.lstat(directory);
|
|
10377
|
+
if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs32.readdir(directory)).length) {
|
|
10378
|
+
throw new Error("Mason installation directory is not empty and has no ownership receipt.");
|
|
10379
|
+
}
|
|
10380
|
+
}
|
|
10381
|
+
const selectedBin = path40.resolve(process.env.MASON_BIN_DIR ?? previous?.bin ?? (process.platform === "win32" ? path40.join(home, "bin") : path40.join(os8.homedir(), ".local/bin")));
|
|
10382
|
+
await fs32.mkdir(selectedBin, { recursive: true });
|
|
10383
|
+
const bin = await fs32.realpath(selectedBin);
|
|
10384
|
+
if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
|
|
10385
|
+
const id = bundle.manifestHash.slice(0, 24);
|
|
10386
|
+
const destination = await storePath(home, "versions/" + id);
|
|
10387
|
+
if (options.stageOnly) {
|
|
10388
|
+
const receipt2 = { ...previous, pending: { id, version: bundle.manifest.version, policyRevision: options.policyRevision } };
|
|
10389
|
+
await writeStoreJson(home, "install.json", receipt2);
|
|
10390
|
+
return { home, bin, version: bundle.manifest.version, previousVersion: previous.version, path: { message: "Update staged for a future MCP launch." } };
|
|
10391
|
+
}
|
|
10392
|
+
const launchers = process.platform === "win32" ? {
|
|
10393
|
+
"mason.cmd": WINDOWS_BATCH_LAUNCHER,
|
|
10394
|
+
"mason-launcher.ps1": `$ErrorActionPreference = 'Stop'
|
|
10395
|
+
$previousToken = $env:MASON_UNINSTALL_TOKEN
|
|
10396
|
+
$token = [Guid]::NewGuid().ToString()
|
|
10397
|
+
$env:MASON_UNINSTALL_TOKEN = $token
|
|
10398
|
+
try {
|
|
10399
|
+
& ${psQuote(path40.join(destination, "node.exe"))} ${psQuote(path40.join(destination, "app/dist/mason.js"))} @args
|
|
10400
|
+
$code = $LASTEXITCODE
|
|
10401
|
+
if ($code -eq 0 -and $args.Count -eq 1 -and $args[0] -eq 'uninstall') {
|
|
10402
|
+
if ([IO.File]::ReadAllText(${psQuote(path40.join(home, ".uninstall-request.txt"))}) -cne $token) { throw 'Mason did not authorize installation cleanup.' }
|
|
10403
|
+
Remove-Item -LiteralPath ${psQuote(home)} -Recurse -Force
|
|
10404
|
+
Write-Output ${psQuote("Removed standalone installation: " + home)}
|
|
10405
|
+
}
|
|
10406
|
+
exit $code
|
|
10407
|
+
} catch { [Console]::Error.WriteLine($_.Exception.Message); exit 2 }
|
|
10408
|
+
finally { $env:MASON_UNINSTALL_TOKEN = $previousToken }
|
|
10409
|
+
`
|
|
10410
|
+
} : { mason: `#!/bin/sh
|
|
10411
|
+
exec ${shQuote(path40.join(destination, "node"))} ${shQuote(path40.join(destination, "app/dist/mason.js"))} "$@"
|
|
10412
|
+
` };
|
|
10413
|
+
for (const [name, content2] of Object.entries(launchers)) {
|
|
10414
|
+
const file = await storePath(bin, name);
|
|
10415
|
+
const old = await fs32.readFile(file, "utf8").catch((error) => {
|
|
10416
|
+
if (error.code === "ENOENT") return null;
|
|
10417
|
+
throw error;
|
|
10418
|
+
});
|
|
10419
|
+
if (old !== null && old !== previous?.launchers[name] && old !== previous?.previousLaunchers?.[name] && old !== content2) throw new Error("Refusing to replace an unrelated or edited launcher: " + file);
|
|
10420
|
+
}
|
|
10421
|
+
const retired = Object.entries({ ...previous?.previousLaunchers, ...previous?.launchers }).filter(([name]) => !(name in launchers));
|
|
10422
|
+
const checkRetired = async (name, expected) => {
|
|
10423
|
+
if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
|
|
10424
|
+
const actual = await fs32.readFile(await storePath(bin, name), "utf8").catch((error) => {
|
|
10425
|
+
if (error.code === "ENOENT") return null;
|
|
10426
|
+
throw error;
|
|
10427
|
+
});
|
|
10428
|
+
if (actual !== null && actual !== expected) throw new Error("Retired launcher was edited; retained: " + name);
|
|
10429
|
+
};
|
|
10430
|
+
for (const [name, expected] of retired) await checkRetired(name, expected);
|
|
10431
|
+
progress?.step(`Installing Mason ${bundle.manifest.version}`);
|
|
10432
|
+
const updates = options.preservePolicy && previous?.updates ? { ...previous.updates } : {
|
|
10433
|
+
...previous?.updates ?? { enabled: !previous && !process.env.CI && process.env.MASON_NO_AUTO_UPDATE !== "1" },
|
|
10434
|
+
revision: randomUUID10(),
|
|
10435
|
+
pinnedVersion: process.env.MASON_VERSION?.replace(/^v/, "") || void 0,
|
|
10436
|
+
mirror: process.env.MASON_RELEASE_BASE || previous?.updates?.mirror
|
|
10437
|
+
};
|
|
10438
|
+
if (options.rollback) {
|
|
10439
|
+
updates.pinnedVersion = bundle.manifest.version;
|
|
10440
|
+
updates.revision = randomUUID10();
|
|
10441
|
+
}
|
|
10442
|
+
const receipt = {
|
|
10443
|
+
format: 1,
|
|
10444
|
+
home,
|
|
10445
|
+
bin,
|
|
10446
|
+
current: id,
|
|
10447
|
+
version: bundle.manifest.version,
|
|
10448
|
+
launchers,
|
|
10449
|
+
pathChanges: previous?.pathChanges ?? [],
|
|
10450
|
+
updates,
|
|
10451
|
+
previous: previous && previous.current !== id ? { id: previous.current, version: previous.version } : previous?.previous
|
|
10452
|
+
};
|
|
10453
|
+
await writeStoreJson(home, "install.json", previous ? { ...previous, previousLaunchers: launchers } : receipt);
|
|
10454
|
+
try {
|
|
10455
|
+
if (!alreadyStaged) await copyBundle(home, source, destination, bundle);
|
|
10456
|
+
for (const [name, content2] of Object.entries(launchers)) await replace(await storePath(bin, name, true), content2, 493);
|
|
10457
|
+
for (const [name, expected] of retired) {
|
|
10458
|
+
await checkRetired(name, expected);
|
|
10459
|
+
await fs32.rm(await storePath(bin, name), { force: true });
|
|
10460
|
+
}
|
|
10461
|
+
await writeStoreJson(home, "install.json", receipt);
|
|
10462
|
+
} catch (error) {
|
|
10463
|
+
if (previous) {
|
|
10464
|
+
for (const [name, content2] of Object.entries(previous.launchers)) await replace(await storePath(bin, name, true), content2, 493);
|
|
10465
|
+
await writeStoreJson(home, "install.json", previous);
|
|
10466
|
+
}
|
|
10467
|
+
throw error;
|
|
10468
|
+
}
|
|
10469
|
+
if (options.preservePolicy) return { home, bin, version: receipt.version, previousVersion: previous?.version, path: { message: "Installation selected." } };
|
|
10470
|
+
progress?.step("Checking terminal command availability");
|
|
10471
|
+
const pathResult = await configurePath(bin, receipt.pathChanges ?? [], async (changes) => {
|
|
10472
|
+
receipt.pathChanges = changes;
|
|
10473
|
+
await writeStoreJson(home, "install.json", receipt);
|
|
10474
|
+
});
|
|
10475
|
+
return { home, bin, version: bundle.manifest.version, previousVersion: previous?.version, path: pathResult };
|
|
10476
|
+
}
|
|
10477
|
+
async function copyBundle(home, source, destination, bundle) {
|
|
10478
|
+
const exists = await fs32.lstat(destination).then(() => true, (error) => {
|
|
10479
|
+
if (error.code === "ENOENT") return false;
|
|
10480
|
+
throw error;
|
|
10481
|
+
});
|
|
10482
|
+
if (exists) {
|
|
10483
|
+
await verifyBundle(destination, bundle.manifestHash);
|
|
10484
|
+
return;
|
|
10485
|
+
}
|
|
10486
|
+
const stage = await storePath(home, "versions/.install-" + randomUUID10(), true);
|
|
10487
|
+
await fs32.mkdir(stage);
|
|
10488
|
+
try {
|
|
10489
|
+
for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
|
|
10490
|
+
const from = await storePath(source, file), to = await storePath(stage, file, true);
|
|
10491
|
+
await fs32.copyFile(from, to);
|
|
10492
|
+
await fs32.chmod(to, (await fs32.stat(from)).mode & 511);
|
|
10493
|
+
}
|
|
10494
|
+
await verifyBundle(stage, bundle.manifestHash);
|
|
10495
|
+
try {
|
|
10496
|
+
await fs32.rename(stage, destination);
|
|
10497
|
+
} catch (error) {
|
|
10498
|
+
try {
|
|
10499
|
+
await verifyBundle(destination, bundle.manifestHash);
|
|
10500
|
+
} catch {
|
|
10501
|
+
throw error;
|
|
10502
|
+
}
|
|
10503
|
+
}
|
|
10504
|
+
} finally {
|
|
10505
|
+
await fs32.rm(stage, { recursive: true, force: true });
|
|
10506
|
+
}
|
|
10507
|
+
}
|
|
10508
|
+
async function prepareStandaloneMcp(home) {
|
|
10509
|
+
const before = await readInstallation(home), candidate = before.pending;
|
|
10510
|
+
let verified, verificationError;
|
|
10511
|
+
if (candidate && automaticUpdatesAllowed(before) && !await hasRunningMcp(home)) {
|
|
10512
|
+
try {
|
|
10513
|
+
verified = await verifyBundle(await storePath(home, "versions/" + candidate.id));
|
|
10514
|
+
if (verified.manifestHash.slice(0, 24) !== candidate.id || verified.manifest.version !== candidate.version) throw new Error("Staged update changed.");
|
|
10515
|
+
} catch (error) {
|
|
10516
|
+
verificationError = error;
|
|
10517
|
+
}
|
|
10518
|
+
}
|
|
10519
|
+
return withLock(home, ".install-lock", async () => {
|
|
10520
|
+
let record = await readInstallation(home);
|
|
10521
|
+
const pending = record.pending;
|
|
10522
|
+
if (pending && candidate?.id === pending.id && (verified || verificationError) && automaticUpdatesAllowed(record) && pending.policyRevision === record.updates?.revision && newerStableVersion(pending.version, record.version) && !await hasRunningMcp(home)) {
|
|
10523
|
+
try {
|
|
10524
|
+
if (verificationError) throw verificationError;
|
|
10525
|
+
const source = await storePath(home, "versions/" + pending.id);
|
|
10526
|
+
await installLocked2(home, source, verified, void 0, { preservePolicy: true }, true);
|
|
10527
|
+
record = await readInstallation(home);
|
|
10528
|
+
} catch (error) {
|
|
10529
|
+
record = await readInstallation(home);
|
|
10530
|
+
delete record.pending;
|
|
10531
|
+
await writeStoreJson(home, "install.json", record);
|
|
10532
|
+
await writeStoreJson(home, "update-status.json", { lastAttempt: Date.now(), error: String(error) });
|
|
10533
|
+
}
|
|
10534
|
+
}
|
|
10535
|
+
const release = await registerMcp(home);
|
|
10536
|
+
return { bundle: await storePath(home, "versions/" + record.current), release };
|
|
10537
|
+
});
|
|
10538
|
+
}
|
|
10539
|
+
|
|
10540
|
+
// src/distribution/updates.ts
|
|
10541
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
10542
|
+
var RELEASE_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
10543
|
+
var releaseListSchema = z30.array(z30.object({ tag_name: z30.string(), draft: z30.boolean(), prerelease: z30.boolean(), published_at: z30.string().nullable() }));
|
|
10544
|
+
var updateManifestSchema = z30.object({
|
|
10545
|
+
format: z30.literal(1),
|
|
10546
|
+
version: z30.string().regex(/^\d+\.\d+\.\d+$/),
|
|
10547
|
+
publishedAt: z30.string().datetime(),
|
|
10548
|
+
assets: z30.record(z30.string().regex(/^[a-f0-9]{64}$/))
|
|
10549
|
+
}).strict();
|
|
10550
|
+
var statusSchema2 = z30.object({ lastAttempt: z30.number(), availableVersion: z30.string().optional(), error: z30.string().optional() });
|
|
10551
|
+
async function scheduleAutomaticUpdate() {
|
|
10552
|
+
try {
|
|
10553
|
+
const { home, record } = await currentInstallation();
|
|
10554
|
+
if (!automaticUpdatesAllowed(record)) return;
|
|
10555
|
+
const status = statusSchema2.safeParse(await readStoreJson(home, "update-status.json"));
|
|
10556
|
+
const now = Date.now();
|
|
10557
|
+
if (status.success && status.data.lastAttempt <= now && now - status.data.lastAttempt < CHECK_INTERVAL_MS) return;
|
|
10558
|
+
const bundle = await storePath(home, "versions/" + record.current);
|
|
10559
|
+
const child = spawn3(
|
|
10560
|
+
path41.join(bundle, process.platform === "win32" ? "node.exe" : "node"),
|
|
10561
|
+
[path41.join(bundle, "app/dist/mason.js"), "internal-update"],
|
|
10562
|
+
{ cwd: home, env: process.env, stdio: "ignore", detached: true, windowsHide: true }
|
|
10563
|
+
);
|
|
10564
|
+
child.on("error", () => {
|
|
10565
|
+
});
|
|
10566
|
+
child.unref();
|
|
10567
|
+
} catch {
|
|
10568
|
+
}
|
|
10569
|
+
}
|
|
10570
|
+
async function startMcpWithUpdates(start) {
|
|
10571
|
+
const current = standaloneLocation();
|
|
10572
|
+
if (!current) {
|
|
10573
|
+
await start();
|
|
10574
|
+
return;
|
|
10575
|
+
}
|
|
10576
|
+
const session = await prepareStandaloneMcp(current.home);
|
|
10577
|
+
if (session.bundle !== current.bundle) {
|
|
10578
|
+
const child = spawn3(
|
|
10579
|
+
path41.join(session.bundle, process.platform === "win32" ? "node.exe" : "node"),
|
|
10580
|
+
[path41.join(session.bundle, "app/dist/mason-mcp.js"), ...process.argv.slice(2)],
|
|
10581
|
+
{ stdio: "inherit", windowsHide: true }
|
|
10582
|
+
);
|
|
10583
|
+
const interrupt = () => {
|
|
10584
|
+
child.kill("SIGINT");
|
|
10585
|
+
}, terminate = () => {
|
|
10586
|
+
child.kill("SIGTERM");
|
|
10587
|
+
};
|
|
10588
|
+
process.on("SIGINT", interrupt);
|
|
10589
|
+
process.on("SIGTERM", terminate);
|
|
10590
|
+
try {
|
|
10591
|
+
process.exitCode = await new Promise((resolve2, reject) => {
|
|
10592
|
+
child.on("error", reject);
|
|
10593
|
+
child.on("exit", (code) => resolve2(code ?? 1));
|
|
10594
|
+
});
|
|
10595
|
+
} finally {
|
|
10596
|
+
process.off("SIGINT", interrupt);
|
|
10597
|
+
process.off("SIGTERM", terminate);
|
|
10598
|
+
await session.release();
|
|
10599
|
+
}
|
|
10600
|
+
return;
|
|
10601
|
+
}
|
|
10602
|
+
const timer = setInterval(() => {
|
|
10603
|
+
void scheduleAutomaticUpdate();
|
|
10604
|
+
}, 60 * 60 * 1e3);
|
|
10605
|
+
timer.unref();
|
|
10606
|
+
const stop = () => {
|
|
10607
|
+
clearInterval(timer);
|
|
10608
|
+
void session?.release();
|
|
10609
|
+
};
|
|
10610
|
+
try {
|
|
10611
|
+
await start(stop);
|
|
10612
|
+
void scheduleAutomaticUpdate();
|
|
10613
|
+
} catch (error) {
|
|
10614
|
+
stop();
|
|
10615
|
+
throw error;
|
|
10616
|
+
}
|
|
10617
|
+
}
|
|
10618
|
+
|
|
9790
10619
|
// bin/mason-mcp.ts
|
|
9791
|
-
startMcpServer
|
|
10620
|
+
startMcpWithUpdates(startMcpServer).catch((err) => {
|
|
9792
10621
|
process.stderr.write(`Mason MCP server error: ${err}
|
|
9793
10622
|
`);
|
|
9794
10623
|
process.exit(1);
|