mason-context 0.17.4 → 0.18.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/dist/mason-mcp.js CHANGED
@@ -1098,6 +1098,105 @@ var init_scope = __esm({
1098
1098
  }
1099
1099
  });
1100
1100
 
1101
+ // src/utils/store-lock.ts
1102
+ import fs6 from "fs/promises";
1103
+ import os from "os";
1104
+ async function lockDiagnostic(file) {
1105
+ let reason2;
1106
+ try {
1107
+ const owner = JSON.parse(await fs6.readFile(file, "utf8"));
1108
+ if (!owner || typeof owner.host !== "string" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
1109
+ reason2 = "Lock owner data is incomplete or malformed.";
1110
+ } else if (owner.host !== os.hostname()) {
1111
+ reason2 = "The lock belongs to another host; its process cannot be checked locally.";
1112
+ } else {
1113
+ try {
1114
+ process.kill(owner.pid, 0);
1115
+ reason2 = `Local owner PID ${owner.pid} is still running.`;
1116
+ } catch (error) {
1117
+ 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.`;
1118
+ }
1119
+ }
1120
+ } catch (error) {
1121
+ reason2 = error instanceof SyntaxError ? "Lock owner data is incomplete or malformed." : "Lock owner data could not be read; the lock may have changed.";
1122
+ }
1123
+ if (await fs6.lstat(file + ".reclaim").then(() => true, () => false)) {
1124
+ reason2 += " A recovery guard also needs inspection: " + file + ".reclaim.";
1125
+ }
1126
+ 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.";
1127
+ }
1128
+ async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason store") {
1129
+ const file = await storePath(root, directory + "/lock", true);
1130
+ const deadline = Date.now() + waitMs;
1131
+ let handle;
1132
+ while (!handle) {
1133
+ try {
1134
+ handle = await fs6.open(file, "wx", 384);
1135
+ try {
1136
+ await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
1137
+ } catch (error) {
1138
+ await handle.close().catch(() => {
1139
+ });
1140
+ handle = void 0;
1141
+ await fs6.rm(file, { force: true }).catch(() => {
1142
+ });
1143
+ throw error;
1144
+ }
1145
+ } catch (error) {
1146
+ if (error.code !== "EEXIST") throw error;
1147
+ try {
1148
+ const owner = JSON.parse(await fs6.readFile(file, "utf8"));
1149
+ if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
1150
+ try {
1151
+ process.kill(owner.pid, 0);
1152
+ } catch (probe) {
1153
+ if (probe.code === "ESRCH") {
1154
+ const reclaim = file + ".reclaim";
1155
+ let guard;
1156
+ try {
1157
+ guard = await fs6.open(reclaim, "wx", 384);
1158
+ const current = JSON.parse(await fs6.readFile(file, "utf8"));
1159
+ if (current.pid === owner.pid && current.host === owner.host) await fs6.unlink(file);
1160
+ } finally {
1161
+ if (guard) {
1162
+ await guard.close();
1163
+ await fs6.rm(reclaim, { force: true });
1164
+ }
1165
+ }
1166
+ }
1167
+ }
1168
+ }
1169
+ } catch {
1170
+ }
1171
+ if (Date.now() >= deadline) throw new Error(label + " is busy or its lock needs inspection: " + file + ". " + await lockDiagnostic(file));
1172
+ await new Promise((resolve2) => setTimeout(resolve2, 40));
1173
+ }
1174
+ }
1175
+ try {
1176
+ return await run();
1177
+ } finally {
1178
+ await handle.close();
1179
+ await fs6.unlink(file);
1180
+ }
1181
+ }
1182
+ var init_store_lock = __esm({
1183
+ "src/utils/store-lock.ts"() {
1184
+ "use strict";
1185
+ init_storage();
1186
+ }
1187
+ });
1188
+
1189
+ // src/snapshot/lock.ts
1190
+ function withSnapshotWrite(root, run, waitMs = 5e3) {
1191
+ return withStoreLock(root, ".mason/local/snapshot-write", run, waitMs, "Snapshot store");
1192
+ }
1193
+ var init_lock = __esm({
1194
+ "src/snapshot/lock.ts"() {
1195
+ "use strict";
1196
+ init_store_lock();
1197
+ }
1198
+ });
1199
+
1101
1200
  // src/test-map.ts
1102
1201
  var test_map_exports = {};
1103
1202
  __export(test_map_exports, {
@@ -1203,8 +1302,15 @@ async function inspectSnapshot(rootDir) {
1203
1302
  }] };
1204
1303
  }
1205
1304
  }
1206
- async function saveSnapshot(rootDir, snapshot) {
1207
- await writeStoreJson(rootDir, ".mason/snapshot.json", snapshotSchema.parse(snapshot));
1305
+ async function updateSnapshot(rootDir, update) {
1306
+ return withSnapshotWrite(rootDir, async () => {
1307
+ const change = await update(await loadSnapshot(rootDir));
1308
+ if (change.snapshot) {
1309
+ await writeStoreJson(rootDir, ".mason/snapshot.json", snapshotSchema.parse(change.snapshot));
1310
+ await change.afterSave?.();
1311
+ }
1312
+ return change.result;
1313
+ });
1208
1314
  }
1209
1315
  async function getCurrentGitHash(rootDir) {
1210
1316
  try {
@@ -1277,6 +1383,7 @@ var init_snapshot = __esm({
1277
1383
  init_files();
1278
1384
  init_files();
1279
1385
  init_storage();
1386
+ init_lock();
1280
1387
  init_paths();
1281
1388
  init_test_map();
1282
1389
  repoPath = z.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
@@ -1284,6 +1391,7 @@ var init_snapshot = __esm({
1284
1391
  refreshedHash: z.string().optional(),
1285
1392
  verifiedAt: z.string().optional(),
1286
1393
  verifiedHash: z.string().optional(),
1394
+ verificationToken: z.string().optional(),
1287
1395
  verificationFailed: z.boolean().optional(),
1288
1396
  verificationNote: z.string().optional()
1289
1397
  };
@@ -1311,7 +1419,7 @@ var init_snapshot = __esm({
1311
1419
  });
1312
1420
 
1313
1421
  // src/drift/drift.ts
1314
- import fs6 from "fs/promises";
1422
+ import fs7 from "fs/promises";
1315
1423
  import path11 from "path";
1316
1424
  function parseChanges(output) {
1317
1425
  const fields = output.split("\0");
@@ -1379,7 +1487,7 @@ async function findGhostFiles(resolvedRoot, mappedFiles) {
1379
1487
  const ghosts = [];
1380
1488
  for (const file of mappedFiles) {
1381
1489
  try {
1382
- await fs6.access(path11.join(resolvedRoot, file));
1490
+ await fs7.access(path11.join(resolvedRoot, file));
1383
1491
  } catch {
1384
1492
  ghosts.push(file);
1385
1493
  }
@@ -1561,7 +1669,7 @@ var init_deleted_reference = __esm({
1561
1669
 
1562
1670
  // src/audit/checks/new-module.ts
1563
1671
  import fg4 from "fast-glob";
1564
- import fs7 from "fs/promises";
1672
+ import fs8 from "fs/promises";
1565
1673
  import path13 from "path";
1566
1674
  function escapeRegExp(text2) {
1567
1675
  return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -1613,7 +1721,7 @@ async function collectModuleCandidates(root, combinedDocs) {
1613
1721
  const byParent = /* @__PURE__ */ new Map();
1614
1722
  for (let offset = 0; offset < parents.length; offset += 16) {
1615
1723
  await Promise.all(parents.slice(offset, offset + 16).map(async (dir) => {
1616
- const entries = await fs7.readdir(await auditInputPath(root, dir), { withFileTypes: true });
1724
+ const entries = await fs8.readdir(await auditInputPath(root, dir), { withFileTypes: true });
1617
1725
  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
1726
  byParent.set(dir, dirs);
1619
1727
  }));
@@ -2236,7 +2344,9 @@ function assessTrust(entry, freshness) {
2236
2344
  }
2237
2345
  function trustHint(states) {
2238
2346
  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.");
2347
+ 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.");
2348
+ 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.");
2349
+ 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
2350
  if (states.some((s) => s.freshness === "unknown")) parts.push("Freshness is unknown for some returned entries; inspect their files before relying on them.");
2241
2351
  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
2352
  if (!parts.length) parts.push("No changes detected in the returned anchors. This does not prove the descriptions are correct.");
@@ -2461,7 +2571,7 @@ __export(decisions_exports, {
2461
2571
  upsertDecision: () => upsertDecision,
2462
2572
  withDecisionWrite: () => withDecisionWrite
2463
2573
  });
2464
- import fs8 from "fs/promises";
2574
+ import fs9 from "fs/promises";
2465
2575
  import path17 from "path";
2466
2576
  import { createHash } from "crypto";
2467
2577
  import { z as z3 } from "zod";
@@ -2470,7 +2580,7 @@ async function loadDecisionStore(rootDir) {
2470
2580
  const diagnostics = [];
2471
2581
  let entries;
2472
2582
  try {
2473
- entries = await fs8.readdir(await storePath(rootDir, ".mason/decisions"));
2583
+ entries = await fs9.readdir(await storePath(rootDir, ".mason/decisions"));
2474
2584
  } catch (error) {
2475
2585
  if (error.code !== "ENOENT") diagnostics.push({ path: ".mason/decisions", message: String(error) });
2476
2586
  return { records, diagnostics };
@@ -2529,7 +2639,7 @@ async function withDecisionWrite(root, operation) {
2529
2639
  const lockPath = await storePath(root, ".mason/decisions/.write-lock", true);
2530
2640
  let lock;
2531
2641
  try {
2532
- lock = await fs8.open(lockPath, "wx", 384);
2642
+ lock = await fs9.open(lockPath, "wx", 384);
2533
2643
  } catch (error) {
2534
2644
  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
2645
  throw error;
@@ -2538,7 +2648,7 @@ async function withDecisionWrite(root, operation) {
2538
2648
  return await operation();
2539
2649
  } finally {
2540
2650
  await lock.close();
2541
- await fs8.unlink(lockPath);
2651
+ await fs9.unlink(lockPath);
2542
2652
  }
2543
2653
  }
2544
2654
  async function upsertDecision(rootDir, input) {
@@ -2560,7 +2670,7 @@ async function upsertDecision(rootDir, input) {
2560
2670
  if (input.files && files.length < input.files.length) warnings.push("some anchor paths were outside the repo or duplicated and were dropped");
2561
2671
  for (const file of files) {
2562
2672
  try {
2563
- await fs8.access(path17.join(rootDir, file));
2673
+ await fs9.access(path17.join(rootDir, file));
2564
2674
  } catch {
2565
2675
  warnings.push(`anchor file does not exist on disk: ${file}`);
2566
2676
  }
@@ -2910,8 +3020,6 @@ var init_findings = __esm({
2910
3020
  });
2911
3021
 
2912
3022
  // src/automation/store.ts
2913
- import fs9 from "fs/promises";
2914
- import os from "os";
2915
3023
  import { z as z5 } from "zod";
2916
3024
  function parseState(raw) {
2917
3025
  try {
@@ -2920,65 +3028,14 @@ function parseState(raw) {
2920
3028
  throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
2921
3029
  }
2922
3030
  }
2923
- async function withLock(root, directory, run, waitMs = 5e3) {
2924
- const file = await storePath(root, directory + "/lock", true);
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
- }
3031
+ function withLock(root, directory, run, waitMs = 5e3) {
3032
+ return withStoreLock(root, directory, run, waitMs, "Automation");
2976
3033
  }
2977
3034
  var hostSchema, events, stateSchema;
2978
3035
  var init_store = __esm({
2979
3036
  "src/automation/store.ts"() {
2980
3037
  "use strict";
2981
- init_storage();
3038
+ init_store_lock();
2982
3039
  hostSchema = z5.enum(["claude", "codex"]);
2983
3040
  events = ["session_start", "turn_start", "before_tool", "after_tool", "task_end"];
2984
3041
  stateSchema = z5.object({
@@ -3761,7 +3818,7 @@ var init_evidence = __esm({
3761
3818
  init_dead_command();
3762
3819
  init_storage();
3763
3820
  init_advisory_review();
3764
- engineVersion = true ? "0.17.4" : "development";
3821
+ engineVersion = true ? "0.18.0" : "development";
3765
3822
  hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
3766
3823
  content = readAuditInput;
3767
3824
  cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
@@ -3853,12 +3910,124 @@ var init_launcher = __esm({
3853
3910
  "use strict";
3854
3911
  init_evidence();
3855
3912
  init_model();
3856
- executingVersion = () => true ? "0.17.4" : "development";
3913
+ executingVersion = () => true ? "0.18.0" : "development";
3857
3914
  mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
3858
3915
  hookCommand = (host) => `mason --setup-host ${host} auto`;
3859
3916
  }
3860
3917
  });
3861
3918
 
3919
+ // src/snapshot/review.ts
3920
+ import { createHash as createHash5 } from "crypto";
3921
+ function snapshotEntryContent(entry) {
3922
+ return {
3923
+ description: entry.description,
3924
+ files: "files" in entry ? entry.files : void 0,
3925
+ chain: "chain" in entry ? entry.chain : void 0,
3926
+ tests: "files" in entry ? entry.tests : void 0,
3927
+ type: "files" in entry ? normalizeFeatureType(entry.type) : void 0
3928
+ };
3929
+ }
3930
+ function reviewToken(kind, name, entry, evidence) {
3931
+ return createHash5("sha256").update(JSON.stringify({
3932
+ version: 1,
3933
+ kind,
3934
+ name,
3935
+ entry: snapshotEntryContent(entry),
3936
+ evidence
3937
+ })).digest("hex");
3938
+ }
3939
+ async function prepareSnapshotReview(access, kind, name, entry) {
3940
+ const files = "files" in entry ? entry.files : entry.chain;
3941
+ const evidence = [];
3942
+ const skeletons = [];
3943
+ for (const file of files.slice(0, VERIFY_MAX_FILES_PER_ENTRY)) {
3944
+ const full = await access.read(file);
3945
+ evidence.push({ path: file, hash: full ? sourceHash(full.content) : null });
3946
+ skeletons.push(full ? { path: full.path, content: full.content.slice(0, VERIFY_SKELETON_CHARS) } : { path: file, missing: true });
3947
+ }
3948
+ return { reviewToken: reviewToken(kind, name, entry, evidence), skeletons, truncated: files.length > VERIFY_MAX_FILES_PER_ENTRY };
3949
+ }
3950
+ function createSnapshotEvidenceReader(access) {
3951
+ const hashes = /* @__PURE__ */ new Map();
3952
+ return async (kind, name, entry) => {
3953
+ const files = "files" in entry ? entry.files : entry.chain;
3954
+ const evidence = [];
3955
+ for (const file of files.slice(0, VERIFY_MAX_FILES_PER_ENTRY)) {
3956
+ let hash2 = hashes.get(file);
3957
+ if (!hash2) {
3958
+ hash2 = access.read(file).then((full) => full ? sourceHash(full.content) : null);
3959
+ hashes.set(file, hash2);
3960
+ }
3961
+ evidence.push({ path: file, hash: await hash2 });
3962
+ }
3963
+ return {
3964
+ reviewToken: reviewToken(kind, name, entry, evidence),
3965
+ evidenceAvailable: evidence.length > 0 && evidence.every((file) => file.hash !== null)
3966
+ };
3967
+ };
3968
+ }
3969
+ var VERIFY_MAX_FILES_PER_ENTRY, VERIFY_SKELETON_CHARS, sourceHash;
3970
+ var init_review = __esm({
3971
+ "src/snapshot/review.ts"() {
3972
+ "use strict";
3973
+ init_snapshot();
3974
+ VERIFY_MAX_FILES_PER_ENTRY = 8;
3975
+ VERIFY_SKELETON_CHARS = 500;
3976
+ sourceHash = (content2) => createHash5("sha256").update(content2).digest("hex");
3977
+ }
3978
+ });
3979
+
3980
+ // src/snapshot/trust.ts
3981
+ function createSnapshotTrustReader(root) {
3982
+ let reader;
3983
+ return async (kind, name, entry, freshness) => {
3984
+ const trust = assessTrust(entry, freshness);
3985
+ const verdict = entry.verificationFailed ? "failed" : entry.verifiedAt ? "passed" : void 0;
3986
+ if (!verdict) return trust;
3987
+ trust.recordedVerdict = verdict;
3988
+ const unknown = (reason2) => {
3989
+ trust.verification = "unknown";
3990
+ trust.reasons.push(reason2);
3991
+ return trust;
3992
+ };
3993
+ 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.");
3994
+ try {
3995
+ reader ??= createFileAccess(root).then(createSnapshotEvidenceReader).catch(() => null);
3996
+ const readReview = await reader;
3997
+ if (!readReview) return unknown("Verification source evidence could not be read under the current file policy.");
3998
+ const review = await readReview(kind, name, entry);
3999
+ if (!review.evidenceAvailable) {
4000
+ return unknown("Verification source evidence is missing, excluded, unreadable, or empty; the recorded verdict cannot be confirmed.");
4001
+ }
4002
+ if (review.reviewToken !== entry.verificationToken) {
4003
+ trust.verification = "stale";
4004
+ trust.reasons.push("The entry or sampled source contents differ from the recorded verification evidence; obtain a fresh review.");
4005
+ }
4006
+ return trust;
4007
+ } catch {
4008
+ return unknown("Verification source evidence could not be read under the current file policy.");
4009
+ }
4010
+ };
4011
+ }
4012
+ async function readSnapshotTrustIndex(readTrust, snapshot, drift) {
4013
+ const features = /* @__PURE__ */ Object.create(null), flows = /* @__PURE__ */ Object.create(null);
4014
+ for (const [name, entry] of Object.entries(snapshot.features)) {
4015
+ features[name] = await readTrust("feature", name, entry, drift?.featureFreshness?.[name] ?? "unknown");
4016
+ }
4017
+ for (const [name, entry] of Object.entries(snapshot.flows)) {
4018
+ flows[name] = await readTrust("flow", name, entry, drift?.flowFreshness?.[name] ?? "unknown");
4019
+ }
4020
+ return { features, flows };
4021
+ }
4022
+ var init_trust2 = __esm({
4023
+ "src/snapshot/trust.ts"() {
4024
+ "use strict";
4025
+ init_trust();
4026
+ init_files();
4027
+ init_review();
4028
+ }
4029
+ });
4030
+
3862
4031
  // src/setup/files.ts
3863
4032
  var files_exports = {};
3864
4033
  __export(files_exports, {
@@ -4893,7 +5062,7 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
4893
5062
  - Before editing a file \u2192 \`get_impact\` to check references, tests, and historical change partners.
4894
5063
  - 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
5064
  - 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 verification 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.
5065
+ - 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
5066
  - 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
5067
  - 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
5068
  - \`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.
@@ -5666,7 +5835,7 @@ async function computeReview(rootDir, base, options = {}) {
5666
5835
  return finalize();
5667
5836
  }
5668
5837
  var exec5, MAX_ANALYZED_FILES;
5669
- var init_review = __esm({
5838
+ var init_review2 = __esm({
5670
5839
  "src/review/review.ts"() {
5671
5840
  "use strict";
5672
5841
  init_drift();
@@ -5755,7 +5924,7 @@ var init_onboarding = __esm({
5755
5924
  "src/mcp/onboarding.ts"() {
5756
5925
  "use strict";
5757
5926
  init_audit();
5758
- init_review();
5927
+ init_review2();
5759
5928
  init_drift();
5760
5929
  init_snapshot();
5761
5930
  init_provenance();
@@ -6171,6 +6340,7 @@ async function assembleContext(rootDir, task, files) {
6171
6340
  };
6172
6341
  }
6173
6342
  const drift = await computeDrift(resolvedRoot);
6343
+ const readTrust = createSnapshotTrustReader(resolvedRoot);
6174
6344
  const featureScores = Object.entries(snapshot.features).map(([name, feat]) => ({
6175
6345
  name,
6176
6346
  feat,
@@ -6197,14 +6367,7 @@ async function assembleContext(rootDir, task, files) {
6197
6367
  Object.assign(bundle, await collectImpact(resolvedRoot, [...anchorFiles, ...Object.values(decisions).flatMap((d) => [...d.files, ...d.pendingProposal?.files ?? []])]));
6198
6368
  bundle.diagnostics = store.diagnostics;
6199
6369
  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
- };
6370
+ bundle.trust = await readSnapshotTrustIndex(readTrust, snapshot, drift);
6208
6371
  bundle.hint += " " + trustHint([
6209
6372
  ...Object.values(bundle.trust.features),
6210
6373
  ...Object.values(bundle.trust.flows),
@@ -6217,7 +6380,7 @@ async function assembleContext(rootDir, task, files) {
6217
6380
  const features = {};
6218
6381
  const staleMatches = [];
6219
6382
  for (const { name, feat, score } of featureScores) {
6220
- const trust = assessTrust(feat, drift?.featureFreshness?.[name] ?? "unknown");
6383
+ const trust = await readTrust("feature", name, feat, drift?.featureFreshness?.[name] ?? "unknown");
6221
6384
  const stale2 = trust.freshness !== "current";
6222
6385
  if (stale2) staleMatches.push(name);
6223
6386
  features[name] = {
@@ -6232,7 +6395,7 @@ async function assembleContext(rootDir, task, files) {
6232
6395
  }
6233
6396
  const flows = {};
6234
6397
  for (const { name, flow, score } of flowScores) {
6235
- const trust = assessTrust(flow, drift?.flowFreshness?.[name] ?? "unknown");
6398
+ const trust = await readTrust("flow", name, flow, drift?.flowFreshness?.[name] ?? "unknown");
6236
6399
  const stale2 = trust.freshness !== "current";
6237
6400
  if (stale2) staleMatches.push(name);
6238
6401
  flows[name] = {
@@ -6317,6 +6480,7 @@ var init_assemble = __esm({
6317
6480
  "use strict";
6318
6481
  init_snapshot();
6319
6482
  init_files();
6483
+ init_trust2();
6320
6484
  init_drift();
6321
6485
  init_impact();
6322
6486
  init_lexical();
@@ -6979,9 +7143,9 @@ var init_renderer = __esm({
6979
7143
  // src/confluence/diff.ts
6980
7144
  import fs24 from "fs/promises";
6981
7145
  import path33 from "path";
6982
- import { createHash as createHash5 } from "crypto";
7146
+ import { createHash as createHash6 } from "crypto";
6983
7147
  function hashDescription(description) {
6984
- return createHash5("sha256").update(description, "utf8").digest("hex");
7148
+ return createHash6("sha256").update(description, "utf8").digest("hex");
6985
7149
  }
6986
7150
  function syncStateDir(rootDir) {
6987
7151
  return path33.join(rootDir, ".mason");
@@ -8207,8 +8371,8 @@ async function reviewState(root, record) {
8207
8371
  changedFiles: touchedPaths(committed),
8208
8372
  localChanges: matchingPaths(anchors, workingTree.changedFiles)
8209
8373
  };
8210
- const reviewToken = createHash4("sha256").update(JSON.stringify({ record, evidence, workingTreeAvailable: workingTree.available })).digest("hex");
8211
- return { reviewToken, evidence, committed, workingTreeAvailable: workingTree.available };
8374
+ const reviewToken2 = createHash4("sha256").update(JSON.stringify({ record, evidence, workingTreeAvailable: workingTree.available })).digest("hex");
8375
+ return { reviewToken: reviewToken2, evidence, committed, workingTreeAvailable: workingTree.available };
8212
8376
  }
8213
8377
  async function previews(root, record, state) {
8214
8378
  const access = await createFileAccess(root);
@@ -8314,7 +8478,9 @@ async function reviewDecision(root, input) {
8314
8478
  // src/mcp/tools.ts
8315
8479
  init_drift2();
8316
8480
  init_snapshot();
8481
+ init_trust2();
8317
8482
  init_drift();
8483
+ init_review();
8318
8484
 
8319
8485
  // src/snapshot/prompt.ts
8320
8486
  var BATCH_SYSTEM_PROMPT = `You are Mason, building one piece of a larger concept-to-files map via a Map-Reduce pattern.
@@ -8411,6 +8577,7 @@ ${JSON.stringify({ partials }, null, 2)}`;
8411
8577
  }
8412
8578
 
8413
8579
  // src/snapshot/partials.ts
8580
+ init_lock();
8414
8581
  init_storage();
8415
8582
  init_snapshot();
8416
8583
  import fs16 from "fs/promises";
@@ -8426,7 +8593,7 @@ var partialSchema = z11.object({
8426
8593
  var scopeSchema2 = z11.object({ files: z11.array(z11.string()), savedAt: z11.string() });
8427
8594
  async function savePartial(rootDir, partial) {
8428
8595
  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));
8596
+ await withSnapshotWrite(rootDir, () => writeStoreJson(rootDir, `${DIRECTORY}/${partial.batchId}.json`, partialSchema.parse(partial)));
8430
8597
  }
8431
8598
  async function loadAllPartials(rootDir) {
8432
8599
  let entries;
@@ -8446,14 +8613,16 @@ async function loadAllPartials(rootDir) {
8446
8613
  return partials.sort((a, b) => a.offset - b.offset);
8447
8614
  }
8448
8615
  async function saveScope(rootDir, files) {
8449
- await writeStoreJson(rootDir, `${DIRECTORY}/scope.json`, { files, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
8616
+ await withSnapshotWrite(rootDir, () => writeStoreJson(rootDir, `${DIRECTORY}/scope.json`, { files, savedAt: (/* @__PURE__ */ new Date()).toISOString() }));
8450
8617
  }
8451
8618
  async function loadScope(rootDir) {
8452
8619
  const raw = await readStoreJson(rootDir, `${DIRECTORY}/scope.json`);
8453
8620
  return raw === null ? null : scopeSchema2.parse(raw).files;
8454
8621
  }
8455
8622
  async function clearScope(rootDir) {
8456
- await fs16.rm(await storePath(rootDir, `${DIRECTORY}/scope.json`), { force: true });
8623
+ await withSnapshotWrite(rootDir, async () => {
8624
+ await fs16.rm(await storePath(rootDir, `${DIRECTORY}/scope.json`), { force: true });
8625
+ });
8457
8626
  }
8458
8627
  async function clearAllPartials(rootDir) {
8459
8628
  await fs16.rm(await storePath(rootDir, DIRECTORY), { recursive: true, force: true });
@@ -8719,9 +8888,9 @@ async function getSnapshot(dir) {
8719
8888
  const store = await loadDecisionStore2(rootDir);
8720
8889
  const decisionRecords = store.records;
8721
8890
  const decisionDrift = await computeDecisionDrift(rootDir, decisionRecords);
8891
+ const readTrust = createSnapshotTrustReader(rootDir);
8722
8892
  const trust = {
8723
- features: Object.fromEntries(Object.entries(snapshot.features).map(([name, entry]) => [name, assessTrust(entry, drift?.featureFreshness?.[name] ?? "unknown")])),
8724
- flows: Object.fromEntries(Object.entries(snapshot.flows).map(([name, entry]) => [name, assessTrust(entry, drift?.flowFreshness?.[name] ?? "unknown")])),
8893
+ ...await readSnapshotTrustIndex(readTrust, snapshot, drift),
8725
8894
  decisions: Object.fromEntries(decisionRecords.filter((d) => d.status === "active").map((d) => [d.id, decisionTrust(effectiveDecision(d), decisionDrift.freshness?.[d.id] ?? "unknown")]))
8726
8895
  };
8727
8896
  output.trust = trust;
@@ -8981,86 +9150,82 @@ function sanitizePaths(rootDir, files) {
8981
9150
  }
8982
9151
  async function saveSnapshotData(dir, features, flows, removeFeatures = [], removeFlows = []) {
8983
9152
  const rootDir = path34.resolve(dir);
8984
- const gitHash = await getCurrentGitHash(rootDir);
8985
- const now = (/* @__PURE__ */ new Date()).toISOString();
8986
- for (const feat of Object.values(features)) {
8987
- feat.files = sanitizePaths(rootDir, feat.files);
8988
- if (feat.tests) feat.tests = sanitizePaths(rootDir, feat.tests);
8989
- feat.type = normalizeFeatureType(feat.type);
8990
- }
8991
- for (const flow of Object.values(flows)) {
8992
- flow.chain = sanitizePaths(rootDir, flow.chain);
8993
- }
8994
- const partials = await loadAllPartials(rootDir);
8995
- const replaceMode = partials.length > 0;
8996
- const previous = await loadSnapshot(rootDir);
8997
- const existing = replaceMode ? null : previous;
8998
- const preserveVerification = (next, old) => {
8999
- if (!old) return;
9000
- const semantic = (entry) => JSON.stringify({
9001
- description: entry.description,
9002
- files: "files" in entry ? entry.files : void 0,
9003
- chain: "chain" in entry ? entry.chain : void 0,
9004
- tests: "files" in entry ? entry.tests : void 0,
9005
- type: "files" in entry ? normalizeFeatureType(entry.type) : void 0
9006
- });
9007
- if (semantic(next) !== semantic(old)) return;
9008
- next.verifiedAt = old.verifiedAt;
9009
- next.verifiedHash = old.verifiedHash;
9010
- next.verificationFailed = old.verificationFailed;
9011
- next.verificationNote = old.verificationNote;
9012
- };
9013
- for (const [name, entry] of Object.entries(features)) preserveVerification(entry, previous?.features[name]);
9014
- for (const [name, entry] of Object.entries(flows)) preserveVerification(entry, previous?.flows[name]);
9015
- if (existing) {
9016
- if (existing.gitHash !== "unknown") {
9017
- for (const feat of Object.values(existing.features)) {
9018
- feat.refreshedHash ??= existing.gitHash;
9153
+ return updateSnapshot(rootDir, async (previous) => {
9154
+ const gitHash = await getCurrentGitHash(rootDir);
9155
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9156
+ for (const feat of Object.values(features)) {
9157
+ feat.files = sanitizePaths(rootDir, feat.files);
9158
+ if (feat.tests) feat.tests = sanitizePaths(rootDir, feat.tests);
9159
+ feat.type = normalizeFeatureType(feat.type);
9160
+ }
9161
+ for (const flow of Object.values(flows)) {
9162
+ flow.chain = sanitizePaths(rootDir, flow.chain);
9163
+ }
9164
+ const partials = await loadAllPartials(rootDir);
9165
+ const replaceMode = partials.length > 0;
9166
+ const existing = replaceMode ? null : previous;
9167
+ const preserveVerification = (next, old) => {
9168
+ delete next.verifiedAt;
9169
+ delete next.verifiedHash;
9170
+ delete next.verificationToken;
9171
+ delete next.verificationFailed;
9172
+ delete next.verificationNote;
9173
+ if (!old) return;
9174
+ if (JSON.stringify(snapshotEntryContent(next)) !== JSON.stringify(snapshotEntryContent(old))) return;
9175
+ next.verificationToken = old.verificationToken;
9176
+ next.verifiedAt = old.verifiedAt;
9177
+ next.verifiedHash = old.verifiedHash;
9178
+ next.verificationFailed = old.verificationFailed;
9179
+ next.verificationNote = old.verificationNote;
9180
+ };
9181
+ for (const [name, entry] of Object.entries(features)) preserveVerification(entry, previous?.features[name]);
9182
+ for (const [name, entry] of Object.entries(flows)) preserveVerification(entry, previous?.flows[name]);
9183
+ if (existing) {
9184
+ if (existing.gitHash !== "unknown") {
9185
+ for (const feat of Object.values(existing.features)) {
9186
+ feat.refreshedHash ??= existing.gitHash;
9187
+ }
9188
+ for (const flow of Object.values(existing.flows)) {
9189
+ flow.refreshedHash ??= existing.gitHash;
9190
+ }
9019
9191
  }
9020
- for (const flow of Object.values(existing.flows)) {
9021
- flow.refreshedHash ??= existing.gitHash;
9192
+ const removedFeatures = removeFeatures.filter(
9193
+ (name) => name in existing.features
9194
+ );
9195
+ const removedFlows = removeFlows.filter((name) => name in existing.flows);
9196
+ for (const name of removedFeatures) delete existing.features[name];
9197
+ for (const name of removedFlows) delete existing.flows[name];
9198
+ if (gitHash !== "unknown") {
9199
+ for (const feat of Object.values(features)) feat.refreshedHash = gitHash;
9200
+ for (const flow of Object.values(flows)) flow.refreshedHash = gitHash;
9022
9201
  }
9023
- }
9024
- const removedFeatures = removeFeatures.filter(
9025
- (name) => name in existing.features
9026
- );
9027
- const removedFlows = removeFlows.filter((name) => name in existing.flows);
9028
- for (const name of removedFeatures) delete existing.features[name];
9029
- for (const name of removedFlows) delete existing.flows[name];
9030
- if (gitHash !== "unknown") {
9031
- for (const feat of Object.values(features)) feat.refreshedHash = gitHash;
9032
- for (const flow of Object.values(flows)) flow.refreshedHash = gitHash;
9033
- }
9034
- existing.features = { ...existing.features, ...features };
9035
- existing.flows = { ...existing.flows, ...flows };
9036
- existing.updatedAt = now;
9037
- existing.gitHash = gitHash;
9038
- await saveSnapshot(rootDir, existing);
9039
- await clearAllPartials(rootDir);
9040
- return JSON.stringify({
9041
- status: "updated",
9042
- mode: "merged",
9043
- features: Object.keys(existing.features).length,
9044
- flows: Object.keys(existing.flows).length,
9045
- removedFeatures: removedFeatures.length,
9046
- removedFlows: removedFlows.length
9047
- });
9048
- }
9049
- const snapshot = {
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
9202
+ existing.features = { ...existing.features, ...features };
9203
+ existing.flows = { ...existing.flows, ...flows };
9204
+ existing.updatedAt = now;
9205
+ existing.gitHash = gitHash;
9206
+ return { snapshot: existing, afterSave: () => clearAllPartials(rootDir), result: JSON.stringify({
9207
+ status: "updated",
9208
+ mode: "merged",
9209
+ features: Object.keys(existing.features).length,
9210
+ flows: Object.keys(existing.flows).length,
9211
+ removedFeatures: removedFeatures.length,
9212
+ removedFlows: removedFlows.length
9213
+ }) };
9214
+ }
9215
+ const snapshot = {
9216
+ version: 2,
9217
+ createdAt: now,
9218
+ updatedAt: now,
9219
+ gitHash,
9220
+ features,
9221
+ flows
9222
+ };
9223
+ return { snapshot, afterSave: () => clearAllPartials(rootDir), result: JSON.stringify({
9224
+ status: replaceMode ? "replaced" : "created",
9225
+ mode: replaceMode ? "replaced-from-partials" : "fresh",
9226
+ features: Object.keys(features).length,
9227
+ flows: Object.keys(flows).length
9228
+ }) };
9064
9229
  });
9065
9230
  }
9066
9231
  async function getImpact(dir, files) {
@@ -9070,8 +9235,6 @@ async function getImpact(dir, files) {
9070
9235
  return JSON.stringify(result, null, 2);
9071
9236
  }
9072
9237
  var VERIFY_DEFAULT_SAMPLE = 5;
9073
- var VERIFY_MAX_FILES_PER_ENTRY = 8;
9074
- var VERIFY_SKELETON_CHARS = 500;
9075
9238
  async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
9076
9239
  const rootDir = path34.resolve(dir);
9077
9240
  const snapshot = await loadSnapshot(rootDir);
@@ -9107,25 +9270,13 @@ async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
9107
9270
  const picked = entries.slice(0, Math.max(1, sample));
9108
9271
  const toVerify = [];
9109
9272
  for (const entry of picked) {
9110
- const skeletons = [];
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
- }
9273
+ const stored = entry.kind === "feature" ? snapshot.features[entry.name] : snapshot.flows[entry.name];
9122
9274
  toVerify.push({
9123
9275
  name: entry.name,
9124
9276
  kind: entry.kind,
9125
9277
  description: entry.description,
9126
9278
  lastVerified: entry.verifiedAt ?? "never",
9127
- skeletons,
9128
- truncated: entry.files.length > VERIFY_MAX_FILES_PER_ENTRY
9279
+ ...await prepareSnapshotReview(access, entry.kind, entry.name, stored)
9129
9280
  });
9130
9281
  }
9131
9282
  const neverVerified = entries.filter((e) => !e.verifiedAt).length;
@@ -9134,45 +9285,81 @@ async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
9134
9285
  totalEntries: entries.length,
9135
9286
  neverVerified,
9136
9287
  entries: toVerify,
9137
- instructions: 'For each entry, judge from the skeletons whether the listed files actually implement the claimed feature/flow (missing files count against it). Then call save_verification with verdicts: {"<entry name>": {"ok": true|false, "note": "<one line, required when ok is false>"}}. Be skeptical \u2014 a plausible description is not evidence; the files must show it.'
9288
+ 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
9289
  });
9139
9290
  }
9140
9291
  async function saveVerification(dir, verdicts) {
9141
9292
  const rootDir = path34.resolve(dir);
9142
- const snapshot = await loadSnapshot(rootDir);
9143
- if (!snapshot) {
9144
- return JSON.stringify({ exists: false, hint: "No concept map exists." });
9145
- }
9146
- const now = (/* @__PURE__ */ new Date()).toISOString();
9147
- const verifiedHash = await getCurrentGitHash(rootDir);
9148
- const stamped = [];
9149
- const unknown = [];
9150
- const failed = [];
9151
- for (const [name, verdict] of Object.entries(verdicts)) {
9152
- const entry = snapshot.features[name] ?? snapshot.flows[name];
9153
- if (!entry) {
9154
- unknown.push(name);
9155
- continue;
9156
- }
9157
- entry.verifiedAt = now;
9158
- entry.verifiedHash = verifiedHash;
9159
- if (verdict.ok) {
9160
- delete entry.verificationFailed;
9161
- delete entry.verificationNote;
9162
- } else {
9163
- entry.verificationFailed = true;
9164
- entry.verificationNote = verdict.note ?? "verification failed";
9165
- failed.push(name);
9166
- }
9167
- stamped.push(name);
9168
- }
9169
- snapshot.updatedAt = now;
9170
- await saveSnapshot(rootDir, snapshot);
9171
- return JSON.stringify({
9172
- stamped,
9173
- unknown,
9174
- failed,
9175
- hint: failed.length > 0 ? `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).` : "All sampled entries verified. Re-run verify_snapshot periodically \u2014 it always picks the least-recently-verified entries next."
9293
+ return updateSnapshot(rootDir, async (snapshot) => {
9294
+ if (!snapshot) {
9295
+ return { snapshot: null, result: JSON.stringify({ exists: false, stamped: [], hint: "No concept map exists. Build it and run verify_snapshot before recording verdicts." }) };
9296
+ }
9297
+ const stamped = [], unknown = [], failed = [];
9298
+ const conflicts = [], reviewRequired = [], invalid = [];
9299
+ const candidates = [];
9300
+ let access;
9301
+ for (const [name, verdict] of Object.entries(verdicts)) {
9302
+ if (!verdict.kind || !["feature", "flow"].includes(verdict.kind) || !/^[a-f0-9]{64}$/.test(verdict.reviewToken ?? "")) {
9303
+ reviewRequired.push(name);
9304
+ continue;
9305
+ }
9306
+ if (typeof verdict.ok !== "boolean" || !verdict.ok && !verdict.note?.trim()) {
9307
+ invalid.push(name);
9308
+ continue;
9309
+ }
9310
+ const collection = verdict.kind === "feature" ? snapshot.features : snapshot.flows;
9311
+ if (!Object.hasOwn(collection, name)) {
9312
+ unknown.push(name);
9313
+ conflicts.push(name);
9314
+ continue;
9315
+ }
9316
+ const entry = collection[name];
9317
+ access ??= await createFileAccess(rootDir);
9318
+ const current = await prepareSnapshotReview(access, verdict.kind, name, entry);
9319
+ if (current.reviewToken !== verdict.reviewToken) conflicts.push(name);
9320
+ else candidates.push({ name, verdict, entry });
9321
+ }
9322
+ const verifiedHash = candidates.length ? await getCurrentGitHash(rootDir) : "unknown";
9323
+ const freshAccess = candidates.length ? await createFileAccess(rootDir) : null;
9324
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9325
+ for (const { name, verdict, entry } of candidates) {
9326
+ const current = await prepareSnapshotReview(freshAccess, verdict.kind, name, entry);
9327
+ if (current.reviewToken !== verdict.reviewToken) {
9328
+ conflicts.push(name);
9329
+ continue;
9330
+ }
9331
+ entry.verifiedAt = now;
9332
+ entry.verifiedHash = verifiedHash;
9333
+ entry.verificationToken = verdict.reviewToken;
9334
+ if (verdict.ok) {
9335
+ delete entry.verificationFailed;
9336
+ delete entry.verificationNote;
9337
+ } else {
9338
+ entry.verificationFailed = true;
9339
+ entry.verificationNote = verdict.note.trim();
9340
+ failed.push(name);
9341
+ }
9342
+ stamped.push(name);
9343
+ }
9344
+ const rejected = conflicts.length + reviewRequired.length + invalid.length;
9345
+ const status = rejected ? stamped.length ? "partial" : conflicts.length ? "conflict" : reviewRequired.length ? "review_required" : "invalid" : stamped.length ? "saved" : "unchanged";
9346
+ const hints = [];
9347
+ if (conflicts.length) hints.push(`Entries [${conflicts.join(", ")}] changed or were deleted after review. Run verify_snapshot and review current evidence before retrying.`);
9348
+ if (reviewRequired.length) hints.push(`Entries [${reviewRequired.join(", ")}] need kind and reviewToken from verify_snapshot. Inspect that evidence before submitting a verdict.`);
9349
+ if (invalid.length) hints.push(`Entries [${invalid.join(", ")}] need a boolean ok and a non-empty note when ok is false.`);
9350
+ 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.`);
9351
+ 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.");
9352
+ if (stamped.length) snapshot.updatedAt = now;
9353
+ return { snapshot: stamped.length ? snapshot : null, result: JSON.stringify({
9354
+ status,
9355
+ stamped,
9356
+ unknown,
9357
+ failed,
9358
+ conflicts,
9359
+ reviewRequired,
9360
+ invalid,
9361
+ hint: hints.join(" ")
9362
+ }) };
9176
9363
  });
9177
9364
  }
9178
9365
  async function saveDecision(dir, input) {
@@ -9402,10 +9589,10 @@ function createMcpServer() {
9402
9589
  const server = new McpServer(
9403
9590
  {
9404
9591
  name: "mason",
9405
- version: "0.17.4"
9592
+ version: "0.18.0"
9406
9593
  },
9407
9594
  {
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, failed verification needs 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.'
9595
+ 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
9596
  }
9410
9597
  );
9411
9598
  server.tool(
@@ -9527,7 +9714,7 @@ function createMcpServer() {
9527
9714
  );
9528
9715
  server.tool(
9529
9716
  "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.",
9717
+ "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
9718
  {
9532
9719
  dir: z24.string().describe("Absolute path to the project root directory")
9533
9720
  },
@@ -9540,7 +9727,7 @@ function createMcpServer() {
9540
9727
  );
9541
9728
  server.tool(
9542
9729
  "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.",
9730
+ "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
9731
  {
9545
9732
  dir: z24.string().describe("Absolute path to the project root directory"),
9546
9733
  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 +9903,7 @@ function createMcpServer() {
9716
9903
  );
9717
9904
  server.tool(
9718
9905
  "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.",
9906
+ "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
9907
  {
9721
9908
  dir: z24.string().describe("Absolute path to the project root directory"),
9722
9909
  sample: z24.number().int().optional().describe("Entries to sample (default 5)")
@@ -9728,11 +9915,13 @@ function createMcpServer() {
9728
9915
  );
9729
9916
  server.tool(
9730
9917
  "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.",
9918
+ "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
9919
  {
9733
9920
  dir: z24.string().describe("Absolute path to the project root directory"),
9734
9921
  verdicts: z24.record(
9735
9922
  z24.object({
9923
+ kind: z24.enum(["feature", "flow"]).optional().describe("Entry kind returned by verify_snapshot; required to record a verdict"),
9924
+ reviewToken: z24.string().optional().describe("Copy the reviewToken from verify_snapshot after inspecting its evidence; required to record a verdict"),
9736
9925
  ok: z24.boolean(),
9737
9926
  note: z24.string().optional().describe("One line on what's wrong \u2014 required when ok is false")
9738
9927
  })