mason-context 0.19.1 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mason-mcp.js CHANGED
@@ -138,9 +138,9 @@ async function readBoundedFile(file, maxBytes) {
138
138
  async function loadProjectConfig(root) {
139
139
  try {
140
140
  const canonicalRoot = await fs.realpath(root);
141
- const configPath2 = await fs.realpath(path2.join(root, ".mason/config.json"));
142
- if (!isWithinRoot(canonicalRoot, configPath2)) throw new Error("Project configuration resolves outside the repository");
143
- const raw = await readBoundedFile(configPath2, 64 * 1024);
141
+ const configPath3 = await fs.realpath(path2.join(root, ".mason/config.json"));
142
+ if (!isWithinRoot(canonicalRoot, configPath3)) throw new Error("Project configuration resolves outside the repository");
143
+ const raw = await readBoundedFile(configPath3, 64 * 1024);
144
144
  if (raw === null) throw new Error("Project configuration is not a regular file or exceeds 64 KiB");
145
145
  const value = JSON.parse(raw);
146
146
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected a configuration object");
@@ -419,15 +419,15 @@ async function readAuditInput(root, file) {
419
419
  async function auditGlob(root, patterns, options) {
420
420
  if (Array.isArray(patterns) && !patterns.length) return [];
421
421
  const directories = /* @__PURE__ */ new Set();
422
- const readdir2 = ((directory, opts, callback) => {
423
- const relative = path5.relative(root, directory).split(path5.sep).join("/") || ".";
422
+ const readdir2 = ((directory2, opts, callback) => {
423
+ const relative = path5.relative(root, directory2).split(path5.sep).join("/") || ".";
424
424
  directories.add(relative);
425
425
  if (directories.size > MAX_INPUTS) {
426
426
  callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
427
427
  return;
428
428
  }
429
429
  auditInputPath(root, relative).then(() => {
430
- nativeFs.readdir(directory, opts, callback);
430
+ nativeFs.readdir(directory2, opts, callback);
431
431
  }, callback);
432
432
  });
433
433
  const lstat2 = ((file, callback) => {
@@ -557,25 +557,25 @@ function commandClaims(line, lineNo, cwd, shell = false) {
557
557
  if (text2.startsWith("#") || /^(?:echo|printf)\b/.test(text2)) continue;
558
558
  const cd = text2.match(/^cd\s+(.+)$/);
559
559
  if (cd) {
560
- const directory = literal(cd[1]);
561
- cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null : path6.posix.join(cwd ?? ".", directory);
560
+ const directory2 = literal(cd[1]);
561
+ cwd = directory2 === null || cwd === null || /\s/.test(directory2) && !/^["']/.test(cd[1]) ? null : path6.posix.join(cwd ?? ".", directory2);
562
562
  continue;
563
563
  }
564
564
  for (const match of text2.matchAll(COMMAND)) {
565
565
  const flags = match[2].trim();
566
566
  const selector = flags.match(/^(?:--prefix|--dir|-C)(?:\s+|=)(.+)$/);
567
- let directory = cwd;
567
+ let directory2 = cwd;
568
568
  let scopeUnknown;
569
569
  if (shell && match.index !== 0 || match[3].startsWith("-")) scopeUnknown = "Unsupported shell wrapper or options before the script.";
570
570
  if (selector) {
571
571
  const selected = literal(selector[1]);
572
- directory = selected === null || cwd === null ? null : path6.posix.join(cwd ?? ".", selected);
572
+ directory2 = selected === null || cwd === null ? null : path6.posix.join(cwd ?? ".", selected);
573
573
  } else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
574
574
  const tail = text2.slice(match.index + match[0].length).split(/\s--(?:\s|$)/)[0];
575
575
  if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(tail)) {
576
576
  scopeUnknown = "Package selection or conditional options after the script need review.";
577
577
  }
578
- if (/[|()]/.test(text2) || /\b(?:pushd|popd|if|for|while)\b/.test(text2) || directory === null) {
578
+ if (/[|()]/.test(text2) || /\b(?:pushd|popd|if|for|while)\b/.test(text2) || directory2 === null) {
579
579
  scopeUnknown = "The command's working directory or shell control flow could not be resolved.";
580
580
  }
581
581
  claims.push({
@@ -583,7 +583,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
583
583
  invocation: match[0],
584
584
  excerpt: match[0],
585
585
  line: lineNo,
586
- ...directory !== void 0 && directory !== null ? { directory } : {},
586
+ ...directory2 !== void 0 && directory2 !== null ? { directory: directory2 } : {},
587
587
  ...scopeUnknown ? { scopeUnknown } : {}
588
588
  });
589
589
  }
@@ -1128,19 +1128,19 @@ var init_docs = __esm({
1128
1128
  // src/audit/scope.ts
1129
1129
  import fs5 from "fs/promises";
1130
1130
  import path8 from "path";
1131
- function scopedPath(directory, value) {
1131
+ function scopedPath(directory2, value) {
1132
1132
  if (path8.posix.isAbsolute(value) || value.includes("\\") || /[\x00-\x1f]/.test(value)) return null;
1133
- const resolved = path8.posix.normalize(path8.posix.join(directory, value));
1133
+ const resolved = path8.posix.normalize(path8.posix.join(directory2, value));
1134
1134
  return resolved === ".." || resolved.startsWith("../") ? null : resolved;
1135
1135
  }
1136
1136
  function pathClaimScope(doc, claim) {
1137
- const directory = claim.relativeTo === "document" ? path8.posix.dirname(doc) : documentScope(doc);
1138
- const local = scopedPath(directory, claim.path);
1137
+ const directory2 = claim.relativeTo === "document" ? path8.posix.dirname(doc) : documentScope(doc);
1138
+ const local = scopedPath(directory2, claim.path);
1139
1139
  if (!local) return null;
1140
- const candidates = claim.relativeTo === "document" || directory === "." ? [local] : [...new Set([local, scopedPath(".", claim.path)].filter((p) => p !== null))];
1140
+ const candidates = claim.relativeTo === "document" || directory2 === "." ? [local] : [...new Set([local, scopedPath(".", claim.path)].filter((p) => p !== null))];
1141
1141
  return {
1142
- basis: claim.relativeTo === "document" ? "document-link" : directory === "." ? "repository" : "document",
1143
- directory,
1142
+ basis: claim.relativeTo === "document" ? "document-link" : directory2 === "." ? "repository" : "document",
1143
+ directory: directory2,
1144
1144
  candidates
1145
1145
  };
1146
1146
  }
@@ -1194,24 +1194,19 @@ async function lockDiagnostic(file) {
1194
1194
  }
1195
1195
  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.";
1196
1196
  }
1197
- async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason store") {
1198
- const file = await storePath(root, directory + "/lock", true);
1197
+ async function withStoreLock(root, directory2, run, waitMs = 5e3, label = "Mason store") {
1198
+ const file = await storePath(root, directory2 + "/lock", true);
1199
1199
  const deadline = Date.now() + waitMs;
1200
1200
  let handle;
1201
1201
  while (!handle) {
1202
1202
  try {
1203
1203
  handle = await fs6.open(file, "wx", 384);
1204
- try {
1205
- await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
1206
- } catch (error) {
1207
- await handle.close().catch(() => {
1208
- });
1209
- handle = void 0;
1210
- await fs6.rm(file, { force: true }).catch(() => {
1211
- });
1212
- throw error;
1213
- }
1214
1204
  } catch (error) {
1205
+ if (process.platform === "win32" && error.code === "EPERM") {
1206
+ if (Date.now() >= deadline) throw error;
1207
+ await new Promise((resolve2) => setTimeout(resolve2, 40));
1208
+ continue;
1209
+ }
1215
1210
  if (error.code !== "EEXIST") throw error;
1216
1211
  try {
1217
1212
  const owner = JSON.parse(await fs6.readFile(file, "utf8"));
@@ -1240,6 +1235,17 @@ async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason
1240
1235
  if (Date.now() >= deadline) throw new Error(label + " is busy or its lock needs inspection: " + file + ". " + await lockDiagnostic(file));
1241
1236
  await new Promise((resolve2) => setTimeout(resolve2, 40));
1242
1237
  }
1238
+ if (handle) {
1239
+ try {
1240
+ await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
1241
+ } catch (error) {
1242
+ await handle.close().catch(() => {
1243
+ });
1244
+ await fs6.rm(file, { force: true }).catch(() => {
1245
+ });
1246
+ throw error;
1247
+ }
1248
+ }
1243
1249
  }
1244
1250
  try {
1245
1251
  return await run();
@@ -2044,19 +2050,19 @@ async function commandInputs(root, docs) {
2044
2050
  const resolutions = [];
2045
2051
  for (const doc of docs) for (const claim of doc.claims.commands) {
2046
2052
  const base = documentScope(doc.path);
2047
- const directory = scopedPath(base, claim.directory ?? ".");
2053
+ const directory2 = scopedPath(base, claim.directory ?? ".");
2048
2054
  const scope = {
2049
2055
  basis: claim.directory === void 0 ? "document" : "explicit",
2050
- directory: directory ?? base,
2056
+ directory: directory2 ?? base,
2051
2057
  candidates: []
2052
2058
  };
2053
2059
  const resolution = { doc: doc.path, claim, scope, manifest: null, scripts: [], elsewhere: [] };
2054
2060
  resolutions.push(resolution);
2055
- if (claim.scopeUnknown || directory === null) {
2061
+ if (claim.scopeUnknown || directory2 === null) {
2056
2062
  resolution.problem = claim.scopeUnknown ?? "The command's directory leaves the repository.";
2057
2063
  continue;
2058
2064
  }
2059
- let cursor = directory;
2065
+ let cursor = directory2;
2060
2066
  while (true) {
2061
2067
  const manifest = path15.posix.join(cursor, "package.json");
2062
2068
  scope.candidates.push(manifest);
@@ -2072,7 +2078,7 @@ async function commandInputs(root, docs) {
2072
2078
  cursor = path15.posix.dirname(cursor);
2073
2079
  }
2074
2080
  if (!resolution.manifest && !resolution.problem) {
2075
- resolution.problem = `No package.json for the command's scope (${directory}).`;
2081
+ resolution.problem = `No package.json for the command's scope (${directory2}).`;
2076
2082
  }
2077
2083
  if (resolution.problem || resolution.scripts.includes(claim.scriptName) || claim.directory !== void 0) continue;
2078
2084
  inventory ??= await commandManifests(root);
@@ -3182,8 +3188,121 @@ var init_findings = __esm({
3182
3188
  }
3183
3189
  });
3184
3190
 
3185
- // src/automation/store.ts
3191
+ // src/automation/notifications.ts
3186
3192
  import { z as z5 } from "zod";
3193
+ function condition(finding) {
3194
+ const source = finding.current ?? finding.original;
3195
+ const e = source.evidence;
3196
+ const id = e.kind === "decision-anchor" ? digest([source.type, source.anchor.doc, e.decisionId]) : finding.id;
3197
+ const result = { level: "confidence" in source ? 2 : 1, facts: [] };
3198
+ switch (e.kind) {
3199
+ case "count-mismatch":
3200
+ result.magnitude = Math.abs(e.actual - e.claimed);
3201
+ break;
3202
+ case "unmentioned-dir":
3203
+ result.magnitude = e.sourceFileCount;
3204
+ break;
3205
+ case "decision-anchor":
3206
+ result.facts = [...e.changedFiles].sort();
3207
+ break;
3208
+ case "doc-behind-manifests":
3209
+ result.facts = (e.matches ?? []).map((m) => JSON.stringify([m.manifest, m.dependency, m.after])).sort();
3210
+ break;
3211
+ }
3212
+ return { id, condition: result };
3213
+ }
3214
+ function notificationConditions(findings) {
3215
+ const active = findings.filter((f) => f.status !== "resolved");
3216
+ const currentIds = new Set(active.filter((f) => f.current).map((f) => condition(f).id));
3217
+ const result = {};
3218
+ for (const f of active) {
3219
+ const value = condition(f);
3220
+ if (!f.current && currentIds.has(value.id)) continue;
3221
+ const previous = result[value.id];
3222
+ result[value.id] = previous ? {
3223
+ level: Math.max(previous.level, value.condition.level),
3224
+ facts: [.../* @__PURE__ */ new Set([...previous.facts, ...value.condition.facts])].sort(),
3225
+ ...previous.magnitude !== void 0 || value.condition.magnitude !== void 0 ? { magnitude: Math.max(previous.magnitude ?? 0, value.condition.magnitude ?? 0) } : {}
3226
+ } : value.condition;
3227
+ }
3228
+ return result;
3229
+ }
3230
+ function createNotificationState(findings, head) {
3231
+ return { version: 1, baselineHead: head, baseline: notificationConditions(findings), delivered: {}, cleared: [] };
3232
+ }
3233
+ function worsened(current, previous) {
3234
+ return !previous || current.level > previous.level || (current.magnitude ?? 0) > (previous.magnitude ?? 0) || current.facts.some((fact) => !previous.facts.includes(fact));
3235
+ }
3236
+ function completionFindings(state, findings) {
3237
+ const current = notificationConditions(findings);
3238
+ const cleared = new Set(state.cleared);
3239
+ const resolvedIds = new Set(findings.filter((f) => f.status === "resolved").map((f) => condition(f).id));
3240
+ for (const id of Object.keys(state.baseline)) if (resolvedIds.has(id) && !current[id]) cleared.add(id);
3241
+ state.cleared = [...cleared];
3242
+ const selected = [];
3243
+ const selectedIds = /* @__PURE__ */ new Set();
3244
+ const delivered = { ...state.delivered };
3245
+ for (const finding of findings) {
3246
+ const value = condition(finding);
3247
+ const previous = state.delivered[value.id];
3248
+ value.condition = current[value.id] ?? value.condition;
3249
+ if (finding.status === "unverified" || !finding.current && finding.status !== "resolved") {
3250
+ continue;
3251
+ }
3252
+ if (finding.status === "resolved") {
3253
+ if (!current[value.id]) delete delivered[value.id];
3254
+ continue;
3255
+ }
3256
+ const eligible = cleared.has(value.id) || worsened(value.condition, state.baseline[value.id]);
3257
+ if (!eligible) continue;
3258
+ if (!selectedIds.has(value.id) && worsened(value.condition, previous)) {
3259
+ selectedIds.add(value.id);
3260
+ const source = finding.current;
3261
+ selected.push(source.evidence.kind === "decision-anchor" ? { ...finding, current: { ...source, evidence: { ...source.evidence, changedFiles: value.condition.facts } } } : finding);
3262
+ if (selected.length <= MAX_COMPLETION_FINDINGS) delivered[value.id] = value.condition;
3263
+ }
3264
+ }
3265
+ state.delivered = delivered;
3266
+ return selected;
3267
+ }
3268
+ function completionSummary(findings, reportPath) {
3269
+ if (!findings.length) return null;
3270
+ const safe = (value) => value.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 250);
3271
+ return [
3272
+ `Mason: ${findings.length} new or worsened finding(s) since the retained baseline.`,
3273
+ ...findings.slice(0, MAX_COMPLETION_FINDINGS).map((f) => {
3274
+ const source = f.current ?? f.original;
3275
+ const detail = source.evidence.kind === "decision-anchor" ? `Changes to ${source.evidence.changedFiles.join(", ")} may affect decision "${source.evidence.title}". Inspect the diff if relevant; acceptance is not required.` : source.message;
3276
+ return `[${f.status === "unresolved" ? "issue" : "advisory"}] ${safe(source.anchor.doc)}: ${safe(detail)}`;
3277
+ }),
3278
+ ...findings.length > MAX_COMPLETION_FINDINGS ? [`${findings.length - MAX_COMPLETION_FINDINGS} more in the report; pending later feedback.`] : [],
3279
+ `Evidence: ${reportPath}. Explicit audit: mason_automation(action: "check").`,
3280
+ "Inspect findings within the authorized work. Concurrent changes may also contribute; this summary does not establish who introduced them."
3281
+ ].join("\n");
3282
+ }
3283
+ var conditionSchema, MAX_COMPLETION_FINDINGS, notificationStateSchema;
3284
+ var init_notifications = __esm({
3285
+ "src/automation/notifications.ts"() {
3286
+ "use strict";
3287
+ init_findings();
3288
+ conditionSchema = z5.object({
3289
+ level: z5.number(),
3290
+ facts: z5.array(z5.string()),
3291
+ magnitude: z5.number().optional()
3292
+ });
3293
+ MAX_COMPLETION_FINDINGS = 4;
3294
+ notificationStateSchema = z5.object({
3295
+ version: z5.literal(1),
3296
+ baselineHead: z5.string(),
3297
+ baseline: z5.record(conditionSchema),
3298
+ delivered: z5.record(conditionSchema),
3299
+ cleared: z5.array(z5.string())
3300
+ });
3301
+ }
3302
+ });
3303
+
3304
+ // src/automation/store.ts
3305
+ import { z as z6 } from "zod";
3187
3306
  function parseState(raw) {
3188
3307
  try {
3189
3308
  return stateSchema.parse(raw);
@@ -3191,45 +3310,44 @@ function parseState(raw) {
3191
3310
  throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
3192
3311
  }
3193
3312
  }
3194
- function withLock(root, directory, run, waitMs = 5e3) {
3195
- return withStoreLock(root, directory, run, waitMs, "Automation");
3313
+ function withLock(root, directory2, run, waitMs = 5e3) {
3314
+ return withStoreLock(root, directory2, run, waitMs, "Automation");
3196
3315
  }
3197
3316
  var hostSchema, events, stateSchema;
3198
3317
  var init_store = __esm({
3199
3318
  "src/automation/store.ts"() {
3200
3319
  "use strict";
3201
3320
  init_store_lock();
3202
- hostSchema = z5.enum(["claude", "codex"]);
3321
+ init_notifications();
3322
+ hostSchema = z6.enum(["claude", "codex"]);
3203
3323
  events = ["session_start", "turn_start", "before_tool", "after_tool", "task_end"];
3204
- stateSchema = z5.object({
3205
- version: z5.literal(1),
3206
- root: z5.string(),
3207
- gitDir: z5.string(),
3208
- branch: z5.string(),
3209
- baselines: z5.array(z5.object({ path: z5.string(), at: z5.string(), event: z5.string(), fingerprint: z5.string() })).max(128),
3210
- sessions: z5.record(z5.object({
3324
+ stateSchema = z6.object({
3325
+ version: z6.literal(1),
3326
+ root: z6.string(),
3327
+ gitDir: z6.string(),
3328
+ branch: z6.string(),
3329
+ baselines: z6.array(z6.object({ path: z6.string(), at: z6.string(), event: z6.string(), fingerprint: z6.string() })).max(128),
3330
+ sessions: z6.record(z6.object({
3211
3331
  host: hostSchema,
3212
- seen: z5.string().nullable(),
3213
- continued: z5.boolean(),
3214
- initialIssues: z5.array(z5.string()),
3215
- initialDocs: z5.record(z5.string().nullable()),
3216
- lastUsed: z5.string(),
3217
- mutationObserved: z5.boolean(),
3218
- pending: z5.record(z5.string()),
3219
- coverageGaps: z5.array(z5.string()),
3220
- events: z5.record(z5.object({ at: z5.string(), count: z5.number().int().positive() }))
3332
+ lastUsed: z6.string(),
3333
+ notifiedDiagnostics: z6.array(z6.string()).default([]),
3334
+ pending: z6.record(z6.string()),
3335
+ coverageGaps: z6.array(z6.string()),
3336
+ events: z6.record(z6.object({ at: z6.string(), count: z6.number().int().positive() }))
3221
3337
  })),
3222
- updatedAt: z5.string(),
3223
- fingerprint: z5.string().nullable(),
3224
- latest: z5.string().nullable(),
3225
- analysis: z5.object({ id: z5.string(), fingerprint: z5.string(), completedAt: z5.number(), path: z5.string() }).optional()
3338
+ updatedAt: z6.string(),
3339
+ fingerprint: z6.string().nullable(),
3340
+ latest: z6.string().nullable(),
3341
+ notifications: notificationStateSchema.optional(),
3342
+ agentNotifications: notificationStateSchema.optional(),
3343
+ analysis: z6.object({ id: z6.string(), fingerprint: z6.string(), completedAt: z6.number(), path: z6.string() }).optional()
3226
3344
  });
3227
3345
  }
3228
3346
  });
3229
3347
 
3230
3348
  // src/audit/advisory-review.ts
3231
3349
  import fs10 from "fs/promises";
3232
- import { z as z6 } from "zod";
3350
+ import { z as z7 } from "zod";
3233
3351
  async function readRecord(root, id) {
3234
3352
  const raw = await readStoreJson(root, recordPath(id));
3235
3353
  if (raw === null) return null;
@@ -3448,41 +3566,41 @@ var init_advisory_review = __esm({
3448
3566
  init_decisions();
3449
3567
  init_drift2();
3450
3568
  git2 = async (root, ...args) => (await (["log", "ls-tree", "ls-files", "merge-base"].includes(args[0]) ? inspectionGit : execGit)(args, { cwd: root, timeout: 1e4, maxBuffer: 8 * 1024 * 1024 })).stdout;
3451
- hashSchema = z6.string().regex(/^[a-f0-9]{64}$/);
3452
- commitSchema2 = z6.string().regex(/^[a-f0-9]{40,64}$/);
3453
- evidenceSchema2 = z6.object({
3569
+ hashSchema = z7.string().regex(/^[a-f0-9]{64}$/);
3570
+ commitSchema2 = z7.string().regex(/^[a-f0-9]{40,64}$/);
3571
+ evidenceSchema2 = z7.object({
3454
3572
  head: commitSchema2,
3455
3573
  fingerprint: hashSchema,
3456
- scope: z6.array(z6.string()).min(1).max(1e3),
3457
- tree: z6.string().max(2 * 1024 * 1024),
3574
+ scope: z7.array(z7.string()).min(1).max(1e3),
3575
+ tree: z7.string().max(2 * 1024 * 1024),
3458
3576
  lastScopeCommit: commitSchema2,
3459
- paths: z6.array(z6.object({ path: z6.string(), exists: z6.boolean() })).max(1e3)
3577
+ paths: z7.array(z7.object({ path: z7.string(), exists: z7.boolean() })).max(1e3)
3460
3578
  });
3461
- dismissalReasonSchema = z6.enum(["no-dependency-claims", "unrelated-manifest-change"]);
3462
- eventSchema2 = z6.object({
3463
- at: z6.string().datetime(),
3464
- reviewer: z6.string().trim().min(1).max(200).optional(),
3465
- note: z6.string().trim().min(1).max(2500),
3466
- outcome: z6.enum(["addressed", "inapplicable", "deferred", "dismissed"]),
3579
+ dismissalReasonSchema = z7.enum(["no-dependency-claims", "unrelated-manifest-change"]);
3580
+ eventSchema2 = z7.object({
3581
+ at: z7.string().datetime(),
3582
+ reviewer: z7.string().trim().min(1).max(200).optional(),
3583
+ note: z7.string().trim().min(1).max(2500),
3584
+ outcome: z7.enum(["addressed", "inapplicable", "deferred", "dismissed"]),
3467
3585
  reasonCode: dismissalReasonSchema.optional(),
3468
3586
  finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
3469
3587
  evidence: evidenceSchema2
3470
3588
  });
3471
- recordSchema = z6.object({ version: z6.literal(1), id: hashSchema, events: z6.array(eventSchema2).min(1).max(200), digest: hashSchema });
3472
- reviewSummarySchema = z6.object({
3589
+ recordSchema = z7.object({ version: z7.literal(1), id: hashSchema, events: z7.array(eventSchema2).min(1).max(200), digest: hashSchema });
3590
+ reviewSummarySchema = z7.object({
3473
3591
  id: hashSchema,
3474
- status: z6.enum(["current", "reopened", "deferred", "unverified"]),
3475
- reason: z6.string(),
3476
- recordPath: z6.string().optional(),
3477
- reviewer: z6.string().optional(),
3478
- note: z6.string().optional(),
3479
- outcome: z6.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired", "dismissed"]).optional(),
3480
- reviewedHead: z6.string().optional()
3592
+ status: z7.enum(["current", "reopened", "deferred", "unverified"]),
3593
+ reason: z7.string(),
3594
+ recordPath: z7.string().optional(),
3595
+ reviewer: z7.string().optional(),
3596
+ note: z7.string().optional(),
3597
+ outcome: z7.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired", "dismissed"]).optional(),
3598
+ reviewedHead: z7.string().optional()
3481
3599
  });
3482
- advisoryReviewRequest = z6.object({
3483
- baselinePath: z6.string().min(1),
3600
+ advisoryReviewRequest = z7.object({
3601
+ baselinePath: z7.string().min(1),
3484
3602
  findingId: hashSchema,
3485
- action: z6.enum(["prepare", "addressed", "inapplicable", "deferred", "dismiss"]).default("prepare"),
3603
+ action: z7.enum(["prepare", "addressed", "inapplicable", "deferred", "dismiss"]).default("prepare"),
3486
3604
  reasonCode: dismissalReasonSchema.optional(),
3487
3605
  reviewer: eventSchema2.shape.reviewer.optional(),
3488
3606
  note: eventSchema2.shape.note.optional(),
@@ -3600,7 +3718,7 @@ __export(repair_exports, {
3600
3718
  import fs12 from "fs/promises";
3601
3719
  import path21 from "path";
3602
3720
  import { randomUUID as randomUUID2 } from "crypto";
3603
- import { z as z7 } from "zod";
3721
+ import { z as z8 } from "zod";
3604
3722
  function recheckable(finding) {
3605
3723
  return !("confidence" in finding) && finding.resolution === "recheck" && ["deleted-reference", "dead-command", "new-module"].includes(finding.type) && ["missing-path", "missing-script", "unmentioned-dir"].includes(finding.evidence.kind);
3606
3724
  }
@@ -3671,7 +3789,7 @@ async function withRepairSession(root, options, run, validate, cachedAudit) {
3671
3789
  }
3672
3790
  async function prepareOriginal(rootDir, checks, options, session) {
3673
3791
  const root = await fs12.realpath(rootDir);
3674
- const selected = z7.array(checkSchema).nonempty().parse(checks);
3792
+ const selected = z8.array(checkSchema).nonempty().parse(checks);
3675
3793
  const report = await (session ? session.audit(selected) : stableAudit(root, selected, options));
3676
3794
  if (!report) throw new Error("No context files found to prepare a repair.");
3677
3795
  if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
@@ -3828,42 +3946,42 @@ var init_repair = __esm({
3828
3946
  init_findings();
3829
3947
  init_advisory_review();
3830
3948
  init_findings();
3831
- count2 = z7.number().int().nonnegative();
3832
- reportSchema = z7.object({
3833
- version: z7.literal(1),
3834
- root: z7.string(),
3835
- gitAvailable: z7.literal(true),
3949
+ count2 = z8.number().int().nonnegative();
3950
+ reportSchema = z8.object({
3951
+ version: z8.literal(1),
3952
+ root: z8.string(),
3953
+ gitAvailable: z8.literal(true),
3836
3954
  headHash: commitSchema.shape.hash,
3837
- checksRun: z7.array(checkSchema).nonempty(),
3838
- docs: z7.array(z7.object({
3839
- path: z7.string().refine((value) => normalizeRepoPath(value) === value),
3840
- scope: z7.string().optional(),
3841
- kind: z7.enum(["instructions", "readme", "guide"]).optional(),
3955
+ checksRun: z8.array(checkSchema).nonempty(),
3956
+ docs: z8.array(z8.object({
3957
+ path: z8.string().refine((value) => normalizeRepoPath(value) === value),
3958
+ scope: z8.string().optional(),
3959
+ kind: z8.enum(["instructions", "readme", "guide"]).optional(),
3842
3960
  lastCommit: commitSchema.nullable(),
3843
- dirty: z7.boolean(),
3961
+ dirty: z8.boolean(),
3844
3962
  lineCount: count2
3845
3963
  })).nonempty(),
3846
- decisionsChecked: z7.boolean(),
3847
- clean: z7.boolean(),
3848
- excludedChecks: z7.array(z7.object({ check: checkSchema, doc: z7.string() })).optional(),
3849
- issues: z7.array(issueSchema),
3850
- advisories: z7.array(advisorySchema),
3851
- suppressedAdvisories: z7.array(advisorySchema).optional(),
3852
- skippedChecks: z7.array(z7.object({ check: z7.string(), reason: z7.string(), doc: z7.string().optional() }))
3964
+ decisionsChecked: z8.boolean(),
3965
+ clean: z8.boolean(),
3966
+ excludedChecks: z8.array(z8.object({ check: checkSchema, doc: z8.string() })).optional(),
3967
+ issues: z8.array(issueSchema),
3968
+ advisories: z8.array(advisorySchema),
3969
+ suppressedAdvisories: z8.array(advisorySchema).optional(),
3970
+ skippedChecks: z8.array(z8.object({ check: z8.string(), reason: z8.string(), doc: z8.string().optional() }))
3853
3971
  });
3854
- baselineSchema = z7.object({
3855
- kind: z7.literal("mason-audit-repair"),
3856
- version: z7.literal(1),
3857
- createdAt: z7.string().datetime(),
3972
+ baselineSchema = z8.object({
3973
+ kind: z8.literal("mason-audit-repair"),
3974
+ version: z8.literal(1),
3975
+ createdAt: z8.string().datetime(),
3858
3976
  report: reportSchema,
3859
- digest: z7.string().regex(/^[a-f0-9]{64}$/)
3977
+ digest: z8.string().regex(/^[a-f0-9]{64}$/)
3860
3978
  });
3861
- repairFindingSchema = z7.object({
3862
- id: z7.string(),
3863
- original: z7.union([issueSchema, advisorySchema]),
3864
- status: z7.enum(["resolved", "unresolved", "review-required", "unverified"]),
3865
- reason: z7.string(),
3866
- current: z7.union([issueSchema, advisorySchema]).optional(),
3979
+ repairFindingSchema = z8.object({
3980
+ id: z8.string(),
3981
+ original: z8.union([issueSchema, advisorySchema]),
3982
+ status: z8.enum(["resolved", "unresolved", "review-required", "unverified"]),
3983
+ reason: z8.string(),
3984
+ current: z8.union([issueSchema, advisorySchema]).optional(),
3867
3985
  review: reviewSummarySchema.optional()
3868
3986
  });
3869
3987
  }
@@ -3874,7 +3992,7 @@ import fs13 from "fs/promises";
3874
3992
  import path22 from "path";
3875
3993
  import { createHash as createHash3 } from "crypto";
3876
3994
  import fg7 from "fast-glob";
3877
- import { z as z8 } from "zod";
3995
+ import { z as z9 } from "zod";
3878
3996
  async function git3(root, ...args) {
3879
3997
  return profilePhase("automation.git", async () => (await execGit(args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout);
3880
3998
  }
@@ -4017,10 +4135,10 @@ var init_evidence = __esm({
4017
4135
  init_dead_command();
4018
4136
  init_storage();
4019
4137
  init_advisory_review();
4020
- engineVersion = true ? "0.19.1" : "development";
4138
+ engineVersion = true ? "0.20.1" : "development";
4021
4139
  hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
4022
4140
  content = readAuditInput;
4023
- cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
4141
+ cacheSchema = z9.object({ version: z9.literal(1), entries: z9.record(z9.object({ key: z9.string(), result: checkResultSchema })), digest: z9.string() });
4024
4142
  }
4025
4143
  });
4026
4144
 
@@ -4036,7 +4154,7 @@ __export(model_exports, {
4036
4154
  setupHostSchema: () => setupHostSchema,
4037
4155
  setupSchema: () => setupSchema
4038
4156
  });
4039
- import { z as z9 } from "zod";
4157
+ import { z as z10 } from "zod";
4040
4158
  import fs14 from "fs/promises";
4041
4159
  import path23 from "path";
4042
4160
  async function loadSetup(root) {
@@ -4070,8 +4188,8 @@ async function effectiveSetup(root) {
4070
4188
  if (inactive && setup) for (const host of inactiveHostsSchema.parse(inactive).hosts) delete setup.hosts[host];
4071
4189
  return { root: inheritedRoot, setup };
4072
4190
  }
4073
- async function loadSetupReceipt(root, directory, host) {
4074
- const raw = await readStoreJson(root, directory + "/setup-" + host + ".json");
4191
+ async function loadSetupReceipt(root, directory2, host) {
4192
+ const raw = await readStoreJson(root, directory2 + "/setup-" + host + ".json");
4075
4193
  if (raw === null) return null;
4076
4194
  const receipt = receiptSchema.parse(raw);
4077
4195
  if (receipt.root !== root || receipt.host !== host) throw new Error("Setup receipt belongs to another installation.");
@@ -4083,29 +4201,29 @@ var init_model = __esm({
4083
4201
  "use strict";
4084
4202
  init_evidence();
4085
4203
  init_storage();
4086
- setupHostSchema = z9.object({
4087
- configuredVersion: z9.string(),
4088
- revision: z9.string().uuid(),
4089
- fingerprint: z9.string(),
4090
- mcpFingerprint: z9.string(),
4091
- instructions: z9.array(z9.string())
4204
+ setupHostSchema = z10.object({
4205
+ configuredVersion: z10.string(),
4206
+ revision: z10.string().uuid(),
4207
+ fingerprint: z10.string(),
4208
+ mcpFingerprint: z10.string(),
4209
+ instructions: z10.array(z10.string())
4092
4210
  });
4093
- setupSchema = z9.object({ version: z9.literal(2), hosts: z9.object({
4211
+ setupSchema = z10.object({ version: z10.literal(2), hosts: z10.object({
4094
4212
  codex: setupHostSchema.optional(),
4095
4213
  claude: setupHostSchema.optional()
4096
4214
  }) });
4097
4215
  SETUP_PATH = ".mason/local/setup.json";
4098
4216
  INACTIVE_HOSTS_PATH = ".mason/local/inactive-hosts.json";
4099
- inactiveHostsSchema = z9.object({ version: z9.literal(1), hosts: z9.array(z9.enum(["claude", "codex"])) });
4100
- receiptSchema = z9.object({
4101
- version: z9.literal(1),
4102
- host: z9.enum(["codex", "claude"]),
4103
- status: z9.enum(["installing", "configured"]),
4104
- initialReportPath: z9.string(),
4105
- initialBaselinePaths: z9.array(z9.string()),
4106
- root: z9.string(),
4107
- revision: z9.string().uuid(),
4108
- configuredAt: z9.string().optional()
4217
+ inactiveHostsSchema = z10.object({ version: z10.literal(1), hosts: z10.array(z10.enum(["claude", "codex"])) });
4218
+ receiptSchema = z10.object({
4219
+ version: z10.literal(1),
4220
+ host: z10.enum(["codex", "claude"]),
4221
+ status: z10.enum(["installing", "configured"]),
4222
+ initialReportPath: z10.string(),
4223
+ initialBaselinePaths: z10.array(z10.string()),
4224
+ root: z10.string(),
4225
+ revision: z10.string().uuid(),
4226
+ configuredAt: z10.string().optional()
4109
4227
  });
4110
4228
  }
4111
4229
  });
@@ -4143,7 +4261,7 @@ var init_launcher = __esm({
4143
4261
  "use strict";
4144
4262
  init_evidence();
4145
4263
  init_model();
4146
- executingVersion = () => true ? "0.19.1" : "development";
4264
+ executingVersion = () => true ? "0.20.1" : "development";
4147
4265
  mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
4148
4266
  hookCommand = (host) => `mason --setup-host ${host} auto`;
4149
4267
  }
@@ -4327,6 +4445,146 @@ var init_files2 = __esm({
4327
4445
  }
4328
4446
  });
4329
4447
 
4448
+ // src/automation/usefulness.ts
4449
+ import fs19 from "fs/promises";
4450
+ import { z as z13 } from "zod";
4451
+ async function enabled(root) {
4452
+ const raw = await readStoreJson(root, configPath);
4453
+ return raw !== null && configSchema.parse(raw).enabled;
4454
+ }
4455
+ async function configureUsefulness(dir, value, options = {}) {
4456
+ const ws = await workspace(dir);
4457
+ const configured = await withStoreLock(ws.root, directory, async () => {
4458
+ if (options.onlyIfUnset) {
4459
+ const existing = await readStoreJson(ws.root, configPath);
4460
+ if (existing !== null) return configSchema.parse(existing).enabled;
4461
+ }
4462
+ const ignore = await storePath(ws.root, directory + "/.gitignore", true);
4463
+ try {
4464
+ await fs19.writeFile(ignore, "*\n", { flag: "wx", mode: 384 });
4465
+ } catch (error) {
4466
+ if (error.code !== "EEXIST") throw error;
4467
+ }
4468
+ await writeStoreJson(ws.root, configPath, { version: 1, enabled: value });
4469
+ return value;
4470
+ });
4471
+ return { enabled: configured, storage: directory, note: "Local only. Disabling retains prior observations." };
4472
+ }
4473
+ async function observeUsefulness(root, event, report, agent, completion, elapsedMs, observedAt = (/* @__PURE__ */ new Date()).toISOString()) {
4474
+ if (!event.host || !event.sessionId || !await enabled(root)) return;
4475
+ const ws = await workspace(root);
4476
+ await withStoreLock(ws.root, directory, async () => {
4477
+ if (!await enabled(root)) return;
4478
+ const raw = await readStoreJson(root, statePath(ws));
4479
+ const state = raw === null ? empty() : stateSchema2.parse(raw);
4480
+ const now = observedAt;
4481
+ const key = hash([event.host, event.sessionId]);
4482
+ const session = state.sessions[key] ??= {
4483
+ host: event.host,
4484
+ updatedAt: now,
4485
+ checks: 0,
4486
+ checkMs: 0,
4487
+ findings: {},
4488
+ decisions: [],
4489
+ retrievalObserved: false,
4490
+ receiptIds: [],
4491
+ reportPath: report.reportPath,
4492
+ truncated: false
4493
+ };
4494
+ const newer = now >= session.updatedAt;
4495
+ if (newer) {
4496
+ session.updatedAt = now;
4497
+ session.reportPath = report.reportPath;
4498
+ }
4499
+ session.checks++;
4500
+ session.checkMs += Math.max(0, elapsedMs);
4501
+ if (newer) {
4502
+ if (event.event === "task_end") session.endedAt = now;
4503
+ else delete session.endedAt;
4504
+ }
4505
+ let latestFindings = [];
4506
+ if (!newer && [...agent.slice(0, 4), ...completion.slice(0, 4)].some((f) => !session.findings[f.id])) {
4507
+ try {
4508
+ if (!session.reportPath.startsWith(ws.directory + "/checks/")) throw new Error("Unexpected report path");
4509
+ latestFindings = z13.object({ findings: z13.array(z13.object({ id: z13.string(), status: findingSchema2.shape.status })) }).parse(await readStoreJson(root, session.reportPath)).findings;
4510
+ } catch {
4511
+ }
4512
+ }
4513
+ for (const [channel, findings] of [["agent", agent], ["completion", completion]]) {
4514
+ for (const finding of findings.slice(0, 4)) {
4515
+ if (!session.findings[finding.id] && Object.keys(session.findings).length >= 200) {
4516
+ session.truncated = true;
4517
+ continue;
4518
+ }
4519
+ const saved = session.findings[finding.id] ??= {
4520
+ type: finding.original.type,
4521
+ channels: [],
4522
+ status: newer ? finding.status : latestFindings.find((f) => f.id === finding.id)?.status ?? "unknown",
4523
+ deliveredAt: now,
4524
+ observedAt: newer ? now : session.updatedAt
4525
+ };
4526
+ if (!saved.channels.includes(channel)) saved.channels.push(channel);
4527
+ }
4528
+ }
4529
+ for (const [id, saved] of Object.entries(session.findings)) {
4530
+ if (now < saved.observedAt) continue;
4531
+ const current = report.findings.find((f) => f.id === id);
4532
+ saved.status = current?.status ?? "unknown";
4533
+ saved.observedAt = now;
4534
+ }
4535
+ if (event.retrievedDecisionIds !== void 0 && event.toolId) {
4536
+ const receipt = hash([event.toolId]);
4537
+ if (!session.receiptIds.includes(receipt)) {
4538
+ session.retrievalObserved = true;
4539
+ const decisions = [.../* @__PURE__ */ new Set([...session.decisions, ...event.retrievedDecisionIds.map((id) => hash([id]))])];
4540
+ if (decisions.length > 200) session.truncated = true;
4541
+ session.decisions = decisions.slice(0, 200);
4542
+ session.receiptIds = [...session.receiptIds, receipt].slice(-200);
4543
+ }
4544
+ }
4545
+ const keys = Object.keys(state.sessions).sort((a, b) => state.sessions[b].updatedAt.localeCompare(state.sessions[a].updatedAt));
4546
+ for (const key2 of keys.slice(50)) delete state.sessions[key2];
4547
+ await writeStoreJson(root, statePath(ws), state);
4548
+ });
4549
+ }
4550
+ var directory, configPath, configSchema, usefulnessRating, findingSchema2, sessionSchema, stateSchema2, empty, statePath;
4551
+ var init_usefulness = __esm({
4552
+ "src/automation/usefulness.ts"() {
4553
+ "use strict";
4554
+ init_evidence();
4555
+ init_storage();
4556
+ init_store_lock();
4557
+ directory = ".mason/local/usefulness";
4558
+ configPath = directory + "/config.json";
4559
+ configSchema = z13.object({ version: z13.literal(1), enabled: z13.boolean() });
4560
+ usefulnessRating = z13.enum(["helpful", "already-knew", "irrelevant", "deferred"]);
4561
+ findingSchema2 = z13.object({
4562
+ type: z13.string(),
4563
+ channels: z13.array(z13.enum(["agent", "completion"])),
4564
+ status: z13.enum(["resolved", "unresolved", "review-required", "unverified", "unknown"]),
4565
+ deliveredAt: z13.string(),
4566
+ observedAt: z13.string(),
4567
+ rating: usefulnessRating.optional()
4568
+ });
4569
+ sessionSchema = z13.object({
4570
+ host: z13.string(),
4571
+ updatedAt: z13.string(),
4572
+ endedAt: z13.string().optional(),
4573
+ checks: z13.number(),
4574
+ checkMs: z13.number(),
4575
+ findings: z13.record(findingSchema2),
4576
+ decisions: z13.array(z13.string()),
4577
+ retrievalObserved: z13.boolean(),
4578
+ receiptIds: z13.array(z13.string()),
4579
+ reportPath: z13.string(),
4580
+ truncated: z13.boolean()
4581
+ });
4582
+ stateSchema2 = z13.object({ version: z13.literal(1), sessions: z13.record(sessionSchema) });
4583
+ empty = () => ({ version: 1, sessions: {} });
4584
+ statePath = (ws) => `${directory}/${hash([ws.root, ws.gitDir, ws.branch]).slice(0, 24)}.json`;
4585
+ }
4586
+ });
4587
+
4330
4588
  // src/automation/execution.ts
4331
4589
  var execution_exports = {};
4332
4590
  __export(execution_exports, {
@@ -4338,9 +4596,9 @@ __export(execution_exports, {
4338
4596
  recordExecution: () => recordExecution
4339
4597
  });
4340
4598
  import os2 from "os";
4341
- import fs19 from "fs/promises";
4599
+ import fs20 from "fs/promises";
4342
4600
  import { randomUUID as randomUUID4 } from "crypto";
4343
- import { z as z12 } from "zod";
4601
+ import { z as z14 } from "zod";
4344
4602
  function parseExecution(raw) {
4345
4603
  try {
4346
4604
  return executionSchema.parse(raw);
@@ -4358,15 +4616,15 @@ function automationFailure(error) {
4358
4616
  codes.add(String(cause.code));
4359
4617
  cause = cause.cause;
4360
4618
  }
4361
- const code = codes.has("ENOSPC") || codes.has("EDQUOT") ? "storage-full" : /changed during|changed while|changed between/.test(message2) ? "inputs-changed" : /Automation is busy/.test(message2) ? "busy" : /not a git repository|unknown revision|bad revision|different history|unreachable/.test(message2) ? "history-unavailable" : error instanceof z12.ZodError || error instanceof SyntaxError || /Hook input|Expected one command|Unknown automation command|--host/.test(message2) ? "invalid-input" : /store|baseline|modified|symbolic link|Symlink|automation state|state belongs/.test(message2) ? "invalid-evidence" : [...codes].some((c) => /^E[A-Z]+$/.test(c)) ? "io-error" : "internal";
4619
+ const code = codes.has("ENOSPC") || codes.has("EDQUOT") ? "storage-full" : /changed during|changed while|changed between/.test(message2) ? "inputs-changed" : /Automation is busy/.test(message2) ? "busy" : /not a git repository|unknown revision|bad revision|different history|unreachable/.test(message2) ? "history-unavailable" : error instanceof z14.ZodError || error instanceof SyntaxError || /Hook input|Expected one command|Unknown automation command|--host/.test(message2) ? "invalid-input" : /store|baseline|modified|symbolic link|Symlink|automation state|state belongs/.test(message2) ? "invalid-evidence" : [...codes].some((c) => /^E[A-Z]+$/.test(c)) ? "io-error" : "internal";
4362
4620
  return { code, message: message2, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
4363
4621
  }
4364
4622
  function failureMessage(error) {
4365
4623
  const failure = automationFailure(error);
4366
4624
  return `Mason automation unavailable [${failure.code}]; evidence capture/verification was not established. ${failure.message}` + (failure.receiptRecorded ? "" : " No durable failure receipt was recorded.");
4367
4625
  }
4368
- async function failureNotifications(root, directory) {
4369
- const raw = await readStoreJson(root, directory + "/notifications.json");
4626
+ async function failureNotifications(root, directory2) {
4627
+ const raw = await readStoreJson(root, directory2 + "/notifications.json");
4370
4628
  return raw === null ? { version: 1, sessions: {} } : notificationsSchema.parse(raw);
4371
4629
  }
4372
4630
  async function hookFailureMessage(dir, host, sessionId, error) {
@@ -4396,12 +4654,12 @@ async function hookFailureMessage(dir, host, sessionId, error) {
4396
4654
  return failureMessage(error);
4397
4655
  }
4398
4656
  }
4399
- async function refreshRunning(root, directory, attempts) {
4657
+ async function refreshRunning(root, directory2, attempts) {
4400
4658
  for (const attempt of attempts) {
4401
4659
  if (attempt.status !== "running") continue;
4402
4660
  let alive = false;
4403
4661
  try {
4404
- const owner = await readStoreJson(root, attempt.lease ? leasePath(directory, attempt.id) : directory + "/lock");
4662
+ const owner = await readStoreJson(root, attempt.lease ? leasePath(directory2, attempt.id) : directory2 + "/lock");
4405
4663
  if (attempt.host === os2.hostname() && owner?.pid === attempt.pid && owner.host === attempt.host) {
4406
4664
  try {
4407
4665
  process.kill(attempt.pid, 0);
@@ -4415,12 +4673,12 @@ async function refreshRunning(root, directory, attempts) {
4415
4673
  if (!alive) attempt.status = "unknown";
4416
4674
  }
4417
4675
  }
4418
- async function editExecution(root, directory, edit) {
4419
- return withLock(root, directory + "/execution-lock", async () => {
4420
- const file = directory + "/execution.json";
4676
+ async function editExecution(root, directory2, edit) {
4677
+ return withLock(root, directory2 + "/execution-lock", async () => {
4678
+ const file = directory2 + "/execution.json";
4421
4679
  const raw = await readStoreJson(root, file);
4422
4680
  const log = raw === null ? { version: 1, attempts: [], discardedAttempts: 0 } : parseExecution(raw);
4423
- await refreshRunning(root, directory, log.attempts);
4681
+ await refreshRunning(root, directory2, log.attempts);
4424
4682
  edit(log);
4425
4683
  const finished = log.attempts.filter((a) => a.status !== "running");
4426
4684
  const discard = new Set(finished.slice(0, Math.max(0, finished.length - 32)).map((a) => a.id));
@@ -4429,7 +4687,7 @@ async function editExecution(root, directory, edit) {
4429
4687
  await writeStoreJson(root, file, log);
4430
4688
  });
4431
4689
  }
4432
- async function recordExecution(root, directory, event, run) {
4690
+ async function recordExecution(root, directory2, event, run) {
4433
4691
  const started = performance.now();
4434
4692
  const attempt = {
4435
4693
  id: randomUUID4(),
@@ -4440,15 +4698,15 @@ async function recordExecution(root, directory, event, run) {
4440
4698
  status: "running",
4441
4699
  lease: true
4442
4700
  };
4443
- const owner = leasePath(directory, attempt.id);
4444
- const finish = async (changes) => editExecution(root, directory, (log) => {
4701
+ const owner = leasePath(directory2, attempt.id);
4702
+ const finish = async (changes) => editExecution(root, directory2, (log) => {
4445
4703
  const current = log.attempts.find((a) => a.id === attempt.id);
4446
4704
  if (!current) throw new Error("Automation execution receipt disappeared; completion could not be recorded.");
4447
4705
  Object.assign(current, changes, { finishedAt: (/* @__PURE__ */ new Date()).toISOString(), durationMs: performance.now() - started });
4448
4706
  });
4449
4707
  await writeStoreJson(root, owner, { pid: attempt.pid, host: attempt.host });
4450
4708
  try {
4451
- await editExecution(root, directory, (log) => {
4709
+ await editExecution(root, directory2, (log) => {
4452
4710
  if (log.attempts.filter((a) => a.status === "running").length >= 128) throw new Error("Too many active automation executions; existing receipts were retained.");
4453
4711
  log.attempts.push(attempt);
4454
4712
  });
@@ -4466,15 +4724,15 @@ async function recordExecution(root, directory, event, run) {
4466
4724
  throw Object.assign(new Error(failure.message, { cause: error }), { failure });
4467
4725
  }
4468
4726
  } finally {
4469
- await fs19.rm(await storePath(root, owner), { force: true }).catch(() => {
4727
+ await fs20.rm(await storePath(root, owner), { force: true }).catch(() => {
4470
4728
  });
4471
4729
  }
4472
4730
  }
4473
- async function executionStatus(root, directory) {
4474
- const raw = await readStoreJson(root, directory + "/execution.json");
4731
+ async function executionStatus(root, directory2) {
4732
+ const raw = await readStoreJson(root, directory2 + "/execution.json");
4475
4733
  if (raw === null) return { status: "not-observed", attempts: [] };
4476
4734
  const log = parseExecution(raw);
4477
- await refreshRunning(root, directory, log.attempts);
4735
+ await refreshRunning(root, directory2, log.attempts);
4478
4736
  const completed = [...log.attempts].reverse().find((a) => a.status === "completed");
4479
4737
  const outstanding = [...log.attempts].reverse().find((a) => ["failed", "unknown"].includes(a.status) && (!completed || (a.finishedAt ?? a.startedAt) >= completed.startedAt));
4480
4738
  const status = log.attempts.some((a) => a.status === "running") ? "running" : outstanding?.status ?? log.attempts.at(-1)?.status ?? "not-observed";
@@ -4487,35 +4745,97 @@ var init_execution = __esm({
4487
4745
  init_store();
4488
4746
  init_evidence();
4489
4747
  init_storage();
4490
- failureSchema = z12.object({
4491
- code: z12.enum(["inputs-changed", "storage-full", "busy", "invalid-input", "history-unavailable", "invalid-evidence", "io-error", "internal"]),
4492
- message: z12.string(),
4493
- retryable: z12.boolean(),
4494
- receiptRecorded: z12.boolean()
4748
+ failureSchema = z14.object({
4749
+ code: z14.enum(["inputs-changed", "storage-full", "busy", "invalid-input", "history-unavailable", "invalid-evidence", "io-error", "internal"]),
4750
+ message: z14.string(),
4751
+ retryable: z14.boolean(),
4752
+ receiptRecorded: z14.boolean()
4495
4753
  });
4496
- attemptSchema = z12.object({
4497
- id: z12.string().regex(/^[a-zA-Z0-9_-]+$/),
4498
- lease: z12.literal(true).optional(),
4499
- event: z12.string(),
4500
- startedAt: z12.string(),
4501
- finishedAt: z12.string().optional(),
4502
- durationMs: z12.number().nonnegative().optional(),
4503
- pid: z12.number().int().positive(),
4504
- host: z12.string(),
4505
- status: z12.enum(["running", "completed", "failed", "unknown"]),
4506
- verificationStatus: z12.string().optional(),
4507
- reportPath: z12.string().optional(),
4754
+ attemptSchema = z14.object({
4755
+ id: z14.string().regex(/^[a-zA-Z0-9_-]+$/),
4756
+ lease: z14.literal(true).optional(),
4757
+ event: z14.string(),
4758
+ startedAt: z14.string(),
4759
+ finishedAt: z14.string().optional(),
4760
+ durationMs: z14.number().nonnegative().optional(),
4761
+ pid: z14.number().int().positive(),
4762
+ host: z14.string(),
4763
+ status: z14.enum(["running", "completed", "failed", "unknown"]),
4764
+ verificationStatus: z14.string().optional(),
4765
+ reportPath: z14.string().optional(),
4508
4766
  failure: failureSchema.optional()
4509
4767
  });
4510
- executionSchema = z12.object({ version: z12.literal(1), attempts: z12.array(attemptSchema).max(160), discardedAttempts: z12.number().int().nonnegative().default(0) });
4511
- notificationsSchema = z12.object({ version: z12.literal(1), sessions: z12.record(z12.object({
4512
- signature: z12.string(),
4768
+ executionSchema = z14.object({ version: z14.literal(1), attempts: z14.array(attemptSchema).max(160), discardedAttempts: z14.number().int().nonnegative().default(0) });
4769
+ notificationsSchema = z14.object({ version: z14.literal(1), sessions: z14.record(z14.object({
4770
+ signature: z14.string(),
4513
4771
  code: failureSchema.shape.code,
4514
- message: z12.string(),
4515
- count: z12.number().int().positive(),
4516
- lastAt: z12.string()
4772
+ message: z14.string(),
4773
+ count: z14.number().int().positive(),
4774
+ lastAt: z14.string()
4517
4775
  })) });
4518
- leasePath = (directory, id) => `${directory}/executions/${id}.json`;
4776
+ leasePath = (directory2, id) => `${directory2}/executions/${id}.json`;
4777
+ }
4778
+ });
4779
+
4780
+ // src/decisions/pending.ts
4781
+ var pending_exports = {};
4782
+ __export(pending_exports, {
4783
+ pendingKnowledge: () => pendingKnowledge
4784
+ });
4785
+ async function pendingKnowledge(root, now = Date.now()) {
4786
+ try {
4787
+ const [store, status] = await Promise.all([
4788
+ loadDecisionStore(root),
4789
+ execGit(
4790
+ ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".mason/decisions"],
4791
+ { cwd: root, timeout: 1e4, maxBuffer: 1024 * 1024 }
4792
+ )
4793
+ ]);
4794
+ const changed = /* @__PURE__ */ new Set();
4795
+ const entries = status.stdout.split("\0");
4796
+ for (let i = 0; i < entries.length; i++) {
4797
+ const entry = entries[i];
4798
+ if (!entry) continue;
4799
+ const file = entry.slice(3);
4800
+ if (/^\.mason\/decisions\/[^/]+\.json$/.test(file)) changed.add(file);
4801
+ if (/[RC]/.test(entry.slice(0, 2))) i++;
4802
+ }
4803
+ const records = new Map(store.records.map((record) => [`.mason/decisions/${record.id}.json`, record]));
4804
+ const age = (at) => {
4805
+ const time = at ? Date.parse(at) : NaN;
4806
+ return Number.isFinite(time) ? `${Math.max(0, Math.floor((now - time) / 864e5))} days` : "unknown age";
4807
+ };
4808
+ const safe = (value) => value.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ");
4809
+ const list = (items) => items.slice(0, 5).map(safe).join("; ") + (items.length > 5 ? `; ${items.length - 5} more` : "");
4810
+ const proposals = store.records.filter((record) => record.status === "active" && decisionApproval(record) === "proposed").map((record) => {
4811
+ const history = record.version === 2 ? record.history : [];
4812
+ let lastReview = -1;
4813
+ history.forEach((event, index2) => {
4814
+ if (event.kind === "accepted" || event.kind === "reaffirmed") lastReview = index2;
4815
+ });
4816
+ const since = history.slice(lastReview + 1).find((event) => event.kind === "created" || event.kind === "revised")?.at ?? record.createdAt;
4817
+ return { record, since };
4818
+ }).sort((a, b) => {
4819
+ const timestamp = (value) => Number.isFinite(Date.parse(value)) ? Date.parse(value) : Infinity;
4820
+ return timestamp(a.since) - timestamp(b.since) || a.record.id.localeCompare(b.record.id);
4821
+ });
4822
+ const lines = [];
4823
+ if (changed.size) lines.push(`${changed.size} uncommitted decision record(s): ${list([...changed].sort().map((file) => `${file} (saved-record age: ${age(records.get(file)?.updatedAt)})`))}.`);
4824
+ if (proposals.length) lines.push(`${proposals.length} proposal(s) awaiting review (oldest first): ${list(proposals.map(({ record, since }) => `.mason/decisions/${record.id}.json (${age(since)})`))}.`);
4825
+ if (store.diagnostics.length) lines.push(`${store.diagnostics.length} decision record(s) could not be read; pending knowledge summary is incomplete.`);
4826
+ return lines.length ? `Mason pending knowledge:
4827
+ ${lines.join("\n")}
4828
+ Informational only; review and commit through the normal project workflow. No approval is implied.` : null;
4829
+ } catch {
4830
+ return "Mason pending knowledge: unavailable; decision records or Git status could not be read.";
4831
+ }
4832
+ }
4833
+ var init_pending = __esm({
4834
+ "src/decisions/pending.ts"() {
4835
+ "use strict";
4836
+ init_git_read();
4837
+ init_decisions();
4838
+ init_provenance();
4519
4839
  }
4520
4840
  });
4521
4841
 
@@ -4528,12 +4848,12 @@ __export(runtime_exports, {
4528
4848
  summarize: () => summarize
4529
4849
  });
4530
4850
  import { randomUUID as randomUUID5 } from "crypto";
4531
- import { z as z13 } from "zod";
4851
+ import { z as z15 } from "zod";
4532
4852
  async function observeReadOnlyTool(dir, event) {
4533
4853
  const ws = await profilePhase("automation.workspace", () => workspace(dir));
4534
4854
  await withLock(ws.root, ws.directory, async () => {
4535
- const statePath = ws.directory + "/state.json";
4536
- const raw = await readStoreJson(ws.root, statePath);
4855
+ const statePath2 = ws.directory + "/state.json";
4856
+ const raw = await readStoreJson(ws.root, statePath2);
4537
4857
  if (raw === null) return;
4538
4858
  const state = parseState(raw);
4539
4859
  if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
@@ -4544,7 +4864,7 @@ async function observeReadOnlyTool(dir, event) {
4544
4864
  const now = (/* @__PURE__ */ new Date()).toISOString();
4545
4865
  session.lastUsed = now;
4546
4866
  session.events[event.event] = { at: now, count: (session.events[event.event]?.count ?? 0) + 1 };
4547
- await writeStoreJson(ws.root, statePath, state);
4867
+ await writeStoreJson(ws.root, statePath2, state);
4548
4868
  });
4549
4869
  return { root: ws.root, directory: ws.directory };
4550
4870
  }
@@ -4559,7 +4879,7 @@ function summarize(report) {
4559
4879
  }),
4560
4880
  ...open.length > 4 ? [`${open.length - 4} more findings in the report.`] : [],
4561
4881
  ...report.diagnostics.slice(0, 2).map(cleanText),
4562
- `Evidence: ${report.reportPath}. Resume/check with mason_automation(action: "check") or mason-auto check.`,
4882
+ `Evidence: ${report.reportPath}. Resume/check with mason_automation(action: "check") or mason check.`,
4563
4883
  "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."
4564
4884
  ].join("\n");
4565
4885
  }
@@ -4618,7 +4938,7 @@ async function verifyInputs(ws, inputs) {
4618
4938
  async function computeAnalysis(ws, state, inputs, event, previousAudit, cacheDiagnostic) {
4619
4939
  const baselines = [...state.baselines];
4620
4940
  const now = (/* @__PURE__ */ new Date()).toISOString();
4621
- const empty = {
4941
+ const empty2 = {
4622
4942
  version: 1,
4623
4943
  status: "unavailable",
4624
4944
  root: ws.root,
@@ -4635,7 +4955,7 @@ async function computeAnalysis(ws, state, inputs, event, previousAudit, cacheDia
4635
4955
  };
4636
4956
  if (!baselines.length && Object.values(inputs.docs).every((value) => value === null)) {
4637
4957
  await verifyInputs(ws, inputs);
4638
- return { baselines, report: empty, cache: null };
4958
+ return { baselines, report: empty2, cache: null };
4639
4959
  }
4640
4960
  if (ws.branch === "detached" && state.latest) {
4641
4961
  const previous = await readStoreJson(ws.root, state.latest);
@@ -4737,7 +5057,7 @@ async function analyze(ws, inputs, event, startedAt) {
4737
5057
  async function automate(dir, event) {
4738
5058
  const startedAt = Date.now();
4739
5059
  const ws = await profilePhase("automation.workspace", () => workspace(dir));
4740
- return recordExecution(ws.root, ws.directory, event.event, () => withRepositoryInspection(ws.root, async () => {
5060
+ const output = await recordExecution(ws.root, ws.directory, event.event, () => withRepositoryInspection(ws.root, async () => {
4741
5061
  const inputs = await readInputs(ws.root);
4742
5062
  const result = await profilePhase("automation.analysis", () => analyze(ws, inputs, event, startedAt));
4743
5063
  if (result.shared) {
@@ -4750,6 +5070,19 @@ async function automate(dir, event) {
4750
5070
  const state = await loadState(ws);
4751
5071
  if (state.analysis?.id !== result.analysis.id) throw new Error("Automation evidence changed during publication; retry against the current inputs.");
4752
5072
  const report = structuredClone(result.analysis.report);
5073
+ if (!state.notifications) {
5074
+ let baseline = { findings: report.findings, head: report.head };
5075
+ if (state.latest) {
5076
+ try {
5077
+ const previous = z15.object({ findings: z15.array(repairFindingSchema), head: z15.string() }).safeParse(await readStoreJson(ws.root, state.latest));
5078
+ if (!previous.success) throw new Error("Unsupported saved report");
5079
+ baseline = previous.data;
5080
+ } catch {
5081
+ report.diagnostics.push("The saved notification baseline could not be read; initialized from the current audit. Original repair evidence is retained. Run an explicit check for all outstanding findings.");
5082
+ }
5083
+ }
5084
+ state.notifications = createNotificationState(baseline.findings, baseline.head);
5085
+ }
4753
5086
  if (result.shared) report.checks = { ...report.checks, ran: [], reused: [.../* @__PURE__ */ new Set([...report.checks.ran, ...report.checks.reused])], shared: true };
4754
5087
  report.reportPath = ws.directory + "/checks/" + randomUUID5() + ".json";
4755
5088
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -4762,12 +5095,8 @@ async function automate(dir, event) {
4762
5095
  if (Object.keys(state.sessions).length >= 128) throw new Error("Too many sessions with unfinished tool calls; original evidence was retained.");
4763
5096
  state.sessions[key] = {
4764
5097
  host: event.host,
4765
- seen: null,
4766
- continued: false,
4767
- initialIssues: report.findings.filter((f) => f.status === "unresolved").map((f) => f.id),
4768
- initialDocs: inputs.docs,
5098
+ notifiedDiagnostics: [],
4769
5099
  lastUsed: now,
4770
- mutationObserved: false,
4771
5100
  pending: {},
4772
5101
  coverageGaps: [],
4773
5102
  events: {}
@@ -4782,7 +5111,6 @@ async function automate(dir, event) {
4782
5111
  session.pending[event.toolId] = inputs.fingerprint;
4783
5112
  }
4784
5113
  if (event.event === "after_tool" && event.mutating) {
4785
- session.mutationObserved = true;
4786
5114
  if (!event.toolId || !session.pending[event.toolId]) {
4787
5115
  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.";
4788
5116
  if (!session.coverageGaps.includes(gap)) session.coverageGaps.push(gap);
@@ -4793,31 +5121,45 @@ async function automate(dir, event) {
4793
5121
  if (session.coverageGaps.length && report.status !== "unavailable") report.status = "incomplete";
4794
5122
  report.capture = report.status !== "unavailable" && !session.coverageGaps.length && (session.events.session_start || session.events.before_tool) ? "observed" : "unknown";
4795
5123
  }
4796
- const signature = hash([report.status, report.findings, report.diagnostics, report.checks.skipped]);
4797
- const relevant = report.findings.some((f) => f.status === "unresolved" && session && (!session.initialIssues.includes(f.id) || session.initialDocs[f.original.anchor.doc] !== inputs.docs[f.original.anchor.doc]));
4798
- const continueOnce = event.event === "task_end" && !!session?.mutationObserved && relevant && !session.continued && !event.stopHookActive;
4799
- const notify = !session || newSession || signature !== session.seen || continueOnce;
4800
- if (session) {
4801
- session.seen = signature;
4802
- if (continueOnce) session.continued = true;
4803
- }
4804
- const persistReport = !state.latest || state.fingerprint !== inputs.fingerprint || notify || event.event === "task_end";
5124
+ const selected = session && event.event === "task_end" ? completionFindings(state.notifications, report.findings) : [];
5125
+ state.agentNotifications ??= { ...structuredClone(state.notifications), delivered: {} };
5126
+ const feedback = session && ["turn_start", "after_tool"].includes(event.event) ? completionFindings(state.agentNotifications, report.findings) : [];
5127
+ const diagnostics = session ? report.diagnostics.filter((d) => !session.notifiedDiagnostics.includes(d)).slice(0, 2) : [];
5128
+ if (session) session.notifiedDiagnostics = [.../* @__PURE__ */ new Set([
5129
+ ...session.notifiedDiagnostics.filter((d) => report.diagnostics.includes(d)),
5130
+ ...diagnostics
5131
+ ])];
5132
+ const message2 = !session ? summarize(report) : [
5133
+ completionSummary(selected, report.reportPath),
5134
+ completionSummary(feedback, report.reportPath),
5135
+ ...diagnostics.length ? [`Mason: verification limited. ${diagnostics.slice(0, 2).map(cleanText).join(" ")} Evidence: ${report.reportPath}.`] : []
5136
+ ].filter(Boolean).join("\n") || null;
5137
+ const notify = message2 !== null;
5138
+ const persistReport = !state.latest || state.fingerprint !== inputs.fingerprint || notify || newSession || event.event === "task_end";
4805
5139
  if (!persistReport) report.reportPath = state.latest;
4806
5140
  state.updatedAt = now;
4807
5141
  state.fingerprint = inputs.fingerprint;
4808
5142
  state.latest = report.reportPath;
4809
5143
  if (persistReport) await writeStoreJson(ws.root, report.reportPath, report);
4810
5144
  await writeStoreJson(ws.root, ws.directory + "/state.json", state);
4811
- return { report, message: notify ? summarize(report) : null, continueOnce };
5145
+ return { report, message: message2, observedAt: now, delivery: { agent: feedback.slice(0, 4), completion: selected.slice(0, 4) } };
4812
5146
  }));
4813
5147
  }));
5148
+ try {
5149
+ await observeUsefulness(ws.root, event, output.report, output.delivery.agent, output.delivery.completion, Date.now() - startedAt, output.observedAt);
5150
+ } catch {
5151
+ if (event.event === "task_end") output.message = [output.message, "Mason: local usefulness tracking unavailable; inspect mason stats --json."].filter(Boolean).join("\n");
5152
+ }
5153
+ return output;
4814
5154
  }
4815
5155
  async function automationStatus(dir) {
5156
+ const { pendingKnowledge: pendingKnowledge2 } = await Promise.resolve().then(() => (init_pending(), pending_exports));
4816
5157
  const ws = await profilePhase("automation.workspace", () => workspace(dir));
5158
+ const knowledge = await pendingKnowledge2(ws.root);
4817
5159
  const execution = await executionStatus(ws.root, ws.directory);
4818
5160
  const notifications = await failureNotifications(ws.root, ws.directory);
4819
5161
  const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
4820
- if (raw === null) return { version: 1, status: execution.status === "not-observed" ? "not-observed" : "unavailable", root: ws.root, branch: ws.branch, baselinePaths: [], hosts: {}, execution, notifications };
5162
+ if (raw === null) return { version: 1, status: execution.status === "not-observed" ? "not-observed" : "unavailable", root: ws.root, branch: ws.branch, baselinePaths: [], hosts: {}, execution, notifications, knowledge };
4821
5163
  const state = parseState(raw);
4822
5164
  if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) throw new Error("Automation state belongs to another workspace.");
4823
5165
  const inputs = await readInputs(ws.root);
@@ -4840,6 +5182,8 @@ async function automationStatus(dir) {
4840
5182
  hosts,
4841
5183
  execution,
4842
5184
  notifications,
5185
+ knowledge,
5186
+ notificationBaselineHead: state.notifications?.baselineHead ?? null,
4843
5187
  note: "Observed events do not prove all tool paths are intercepted. Run check to verify the retained evidence."
4844
5188
  };
4845
5189
  }
@@ -4847,6 +5191,8 @@ var SCOPE, priority, cleanText, analysisSchema, sharedSchema, baselineDigest;
4847
5191
  var init_runtime = __esm({
4848
5192
  "src/automation/runtime.ts"() {
4849
5193
  "use strict";
5194
+ init_usefulness();
5195
+ init_notifications();
4850
5196
  init_profile();
4851
5197
  init_inspection();
4852
5198
  init_execution();
@@ -4858,34 +5204,34 @@ var init_runtime = __esm({
4858
5204
  SCOPE = "Documentation audit evidence only. Hook receipts show observed events, not complete interception. Resolved findings no longer fail their checks or have a recorded assessment covering current evidence. Repair and review only within the user's task authorization.";
4859
5205
  priority = { resolved: 0, "review-required": 1, unresolved: 2, unverified: 3 };
4860
5206
  cleanText = (text2) => text2.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 250);
4861
- analysisSchema = z13.object({
4862
- id: z13.string(),
4863
- fingerprint: z13.string(),
4864
- completedAt: z13.number(),
4865
- baselineDigest: z13.string(),
5207
+ analysisSchema = z15.object({
5208
+ id: z15.string(),
5209
+ fingerprint: z15.string(),
5210
+ completedAt: z15.number(),
5211
+ baselineDigest: z15.string(),
4866
5212
  currentAudit: reportSchema.nullable().optional(),
4867
- report: z13.object({
4868
- version: z13.literal(1),
4869
- status: z13.enum(["verified", "issues-remain", "incomplete", "unavailable"]),
4870
- root: z13.string(),
4871
- branch: z13.string(),
4872
- head: z13.string(),
4873
- baselinePaths: z13.array(z13.string()),
4874
- reportPath: z13.string(),
4875
- findings: z13.array(repairFindingSchema),
4876
- diagnostics: z13.array(z13.string()),
4877
- checks: z13.object({
4878
- ran: z13.array(z13.string()),
4879
- reused: z13.array(z13.string()),
4880
- skipped: z13.array(z13.object({ check: z13.string(), reason: z13.string(), doc: z13.string().optional() })),
4881
- auditReused: z13.boolean().optional()
5213
+ report: z15.object({
5214
+ version: z15.literal(1),
5215
+ status: z15.enum(["verified", "issues-remain", "incomplete", "unavailable"]),
5216
+ root: z15.string(),
5217
+ branch: z15.string(),
5218
+ head: z15.string(),
5219
+ baselinePaths: z15.array(z15.string()),
5220
+ reportPath: z15.string(),
5221
+ findings: z15.array(repairFindingSchema),
5222
+ diagnostics: z15.array(z15.string()),
5223
+ checks: z15.object({
5224
+ ran: z15.array(z15.string()),
5225
+ reused: z15.array(z15.string()),
5226
+ skipped: z15.array(z15.object({ check: z15.string(), reason: z15.string(), doc: z15.string().optional() })),
5227
+ auditReused: z15.boolean().optional()
4882
5228
  }),
4883
- counts: z13.object({ resolved: z13.number(), unresolved: z13.number(), "review-required": z13.number(), unverified: z13.number() }),
4884
- capture: z13.enum(["observed", "unknown"]),
4885
- scope: z13.string()
5229
+ counts: z15.object({ resolved: z15.number(), unresolved: z15.number(), "review-required": z15.number(), unverified: z15.number() }),
5230
+ capture: z15.enum(["observed", "unknown"]),
5231
+ scope: z15.string()
4886
5232
  })
4887
5233
  });
4888
- sharedSchema = z13.object({ analysis: analysisSchema, digest: z13.string() });
5234
+ sharedSchema = z15.object({ analysis: analysisSchema, digest: z15.string() });
4889
5235
  baselineDigest = async (ws, state) => profilePhase("automation.baselines", async () => hash(await Promise.all(
4890
5236
  state.baselines.map(async (b) => [b.path, await readStoreJson(ws.root, b.path)])
4891
5237
  )));
@@ -4893,7 +5239,7 @@ var init_runtime = __esm({
4893
5239
  });
4894
5240
 
4895
5241
  // src/automation/hook-root.ts
4896
- import fs20 from "fs/promises";
5242
+ import fs21 from "fs/promises";
4897
5243
  import path28 from "path";
4898
5244
  var init_hook_root = __esm({
4899
5245
  "src/automation/hook-root.ts"() {
@@ -4903,7 +5249,7 @@ var init_hook_root = __esm({
4903
5249
  });
4904
5250
 
4905
5251
  // src/automation/adapters.ts
4906
- import { z as z14 } from "zod";
5252
+ import { z as z16 } from "zod";
4907
5253
  function managedHookCommand(host, platform = process.platform) {
4908
5254
  const command = `mason --setup-host ${host} auto hook --host ${host}`;
4909
5255
  return platform === "win32" ? `cmd.exe /d /s /c "where mason >nul 2>nul & if errorlevel 1 (exit /b 0) else (${command})"` : `if command -v mason >/dev/null 2>&1; then ${command}; fi`;
@@ -4914,7 +5260,7 @@ function knownManagedHookCommands(host) {
4914
5260
  function compatibleManagedHookCommands(host, platform = process.platform) {
4915
5261
  return [managedHookCommand(host, platform), `mason --setup-host ${host} auto hook --host ${host}`];
4916
5262
  }
4917
- function hookConfig(host, command = "npx --no-install --package mason-context mason-auto") {
5263
+ function hookConfig(host, command = "npx --no-install --package mason-context mason auto") {
4918
5264
  const handler = { type: "command", command: command === `mason --setup-host ${host} auto` ? managedHookCommand(host) : command + " hook --host " + host, timeout: 30 };
4919
5265
  return { hooks: Object.fromEntries(HOOK_EVENTS.map((name) => [
4920
5266
  name,
@@ -4925,19 +5271,21 @@ var inputSchema, HOOK_EVENTS;
4925
5271
  var init_adapters = __esm({
4926
5272
  "src/automation/adapters.ts"() {
4927
5273
  "use strict";
5274
+ init_usefulness();
4928
5275
  init_execution();
4929
5276
  init_runtime();
4930
5277
  init_store();
4931
5278
  init_hook_root();
4932
- inputSchema = z14.object({
4933
- cwd: z14.string().min(1),
4934
- session_id: z14.string().min(1).max(500),
4935
- hook_event_name: z14.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
4936
- tool_name: z14.string().optional(),
4937
- tool_use_id: z14.string().max(500).optional(),
4938
- tool_input: z14.unknown().optional(),
4939
- stop_hook_active: z14.boolean().optional(),
4940
- permission_mode: z14.string().optional()
5279
+ inputSchema = z16.object({
5280
+ cwd: z16.string().min(1),
5281
+ session_id: z16.string().min(1).max(500),
5282
+ hook_event_name: z16.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
5283
+ tool_name: z16.string().optional(),
5284
+ tool_use_id: z16.string().max(500).optional(),
5285
+ tool_response: z16.unknown().optional(),
5286
+ tool_input: z16.unknown().optional(),
5287
+ stop_hook_active: z16.boolean().optional(),
5288
+ permission_mode: z16.string().optional()
4941
5289
  });
4942
5290
  HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"];
4943
5291
  }
@@ -4953,7 +5301,7 @@ __export(ownership_exports, {
4953
5301
  ownershipEdit: () => ownershipEdit,
4954
5302
  ownershipSchema: () => ownershipSchema
4955
5303
  });
4956
- import { z as z15 } from "zod";
5304
+ import { z as z17 } from "zod";
4957
5305
  import { parse } from "smol-toml";
4958
5306
  function blockSpan(text2, start, end) {
4959
5307
  const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
@@ -5004,15 +5352,15 @@ var init_ownership = __esm({
5004
5352
  ["<!-- mason:agents:start -->", "<!-- mason:agents:end -->"],
5005
5353
  ["# mason:mcp:start", "# mason:mcp:end"]
5006
5354
  ];
5007
- fileSchema = z15.object({
5008
- created: z15.boolean(),
5009
- initialHash: z15.string(),
5010
- blocks: z15.record(z15.string()),
5011
- mcpHash: z15.string().optional(),
5012
- hookCommands: z15.array(z15.string()).optional(),
5013
- hooks: z15.record(z15.array(z15.string())).optional()
5355
+ fileSchema = z17.object({
5356
+ created: z17.boolean(),
5357
+ initialHash: z17.string(),
5358
+ blocks: z17.record(z17.string()),
5359
+ mcpHash: z17.string().optional(),
5360
+ hookCommands: z17.array(z17.string()).optional(),
5361
+ hooks: z17.record(z17.array(z17.string())).optional()
5014
5362
  });
5015
- ownershipSchema = z15.object({ version: z15.literal(1), files: z15.record(fileSchema) });
5363
+ ownershipSchema = z17.object({ version: z17.literal(1), files: z17.record(fileSchema) });
5016
5364
  }
5017
5365
  });
5018
5366
 
@@ -5021,13 +5369,13 @@ var install_exports = {};
5021
5369
  __export(install_exports, {
5022
5370
  AUTOMATION_PATH: () => AUTOMATION_PATH,
5023
5371
  automationConfigSchema: () => automationConfigSchema,
5024
- configPath: () => configPath,
5372
+ configPath: () => configPath2,
5025
5373
  installAutomation: () => installAutomation,
5026
5374
  installedAutomation: () => installedAutomation,
5027
5375
  planAutomationInstall: () => planAutomationInstall,
5028
5376
  recordSchema: () => recordSchema2
5029
5377
  });
5030
- import { z as z16 } from "zod";
5378
+ import { z as z18 } from "zod";
5031
5379
  async function installationRecord(root) {
5032
5380
  return recordSchema2.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
5033
5381
  }
@@ -5036,7 +5384,7 @@ async function installAutomation(dir, host, command) {
5036
5384
  return withLock(ws.root, ".mason/reports/setup-lock", () => installLocked(ws.root, host, command));
5037
5385
  }
5038
5386
  async function planAutomationInstall(root, host, command) {
5039
- const file = configPath(host);
5387
+ const file = configPath2(host);
5040
5388
  const before = await readText(root, file);
5041
5389
  const existing = automationConfigSchema.parse(JSON.parse(before ?? "{}"));
5042
5390
  const record = await installationRecord(root);
@@ -5076,7 +5424,7 @@ async function installLocked(root, host, command) {
5076
5424
  status: "configured",
5077
5425
  command: newCommand,
5078
5426
  events: HOOK_EVENTS,
5079
- next: host === "codex" ? "Review/trust these hooks using Codex /hooks and start a new session. mason-auto status reports observed events separately from configuration." : "Start a new Claude Code session. mason-auto status reports observed events separately from configuration.",
5427
+ next: host === "codex" ? "Review/trust these hooks using Codex /hooks and start a new session. mason status reports observed events separately from configuration." : "Start a new Claude Code session. mason status reports observed events separately from configuration.",
5080
5428
  note: "Install mason-context in the project before using the default command. Ignore .mason/reports/ to keep local evidence out of commits. Hooks preserve evidence and suggest scoped repairs; they do not approve edits or decisions."
5081
5429
  };
5082
5430
  }
@@ -5087,10 +5435,10 @@ async function installedAutomation(dir) {
5087
5435
  for (const host of ["claude", "codex"]) {
5088
5436
  const expected = record.hosts[host];
5089
5437
  if (!expected) continue;
5090
- const current = automationConfigSchema.parse(await readStoreJson(ws.root, configPath(host)) ?? {});
5438
+ const current = automationConfigSchema.parse(await readStoreJson(ws.root, configPath2(host)) ?? {});
5091
5439
  const configuredEvents = HOOK_EVENTS.filter((event) => current.hooks?.[event]?.some((group) => group.hooks.some((handler) => handler.type === "command" && handler.command === expected.command)));
5092
5440
  result[host] = {
5093
- configPath: configPath(host),
5441
+ configPath: configPath2(host),
5094
5442
  configuredEvents,
5095
5443
  status: current.disableAllHooks === true ? "disabled" : configuredEvents.length === HOOK_EVENTS.length ? "configured" : "incomplete",
5096
5444
  runtime: "Host version, trust, policy, and tool coverage still determine execution; inspect observed events."
@@ -5098,7 +5446,7 @@ async function installedAutomation(dir) {
5098
5446
  }
5099
5447
  return result;
5100
5448
  }
5101
- var groupSchema, automationConfigSchema, recordSchema2, AUTOMATION_PATH, configPath;
5449
+ var groupSchema, automationConfigSchema, recordSchema2, AUTOMATION_PATH, configPath2;
5102
5450
  var init_install = __esm({
5103
5451
  "src/automation/install.ts"() {
5104
5452
  "use strict";
@@ -5108,11 +5456,11 @@ var init_install = __esm({
5108
5456
  init_store();
5109
5457
  init_files2();
5110
5458
  init_ownership();
5111
- groupSchema = z16.object({ hooks: z16.array(z16.object({ type: z16.string(), command: z16.string().optional() }).passthrough()) }).passthrough();
5112
- automationConfigSchema = z16.object({ hooks: z16.record(z16.array(groupSchema)).optional() }).passthrough();
5113
- recordSchema2 = z16.object({ version: z16.literal(1), hosts: z16.record(z16.object({ command: z16.string() })) });
5459
+ groupSchema = z18.object({ hooks: z18.array(z18.object({ type: z18.string(), command: z18.string().optional() }).passthrough()) }).passthrough();
5460
+ automationConfigSchema = z18.object({ hooks: z18.record(z18.array(groupSchema)).optional() }).passthrough();
5461
+ recordSchema2 = z18.object({ version: z18.literal(1), hosts: z18.record(z18.object({ command: z18.string() })) });
5114
5462
  AUTOMATION_PATH = ".mason/local/automation.json";
5115
- configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
5463
+ configPath2 = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
5116
5464
  }
5117
5465
  });
5118
5466
 
@@ -5128,7 +5476,7 @@ __export(config_exports, {
5128
5476
  });
5129
5477
  import { isDeepStrictEqual } from "util";
5130
5478
  import { parse as parse2, stringify } from "smol-toml";
5131
- import { z as z17 } from "zod";
5479
+ import { z as z19 } from "zod";
5132
5480
  async function instructionEdits(root, host) {
5133
5481
  const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
5134
5482
  const existing = found.find((f) => f.text !== null);
@@ -5187,7 +5535,7 @@ async function mcpEdit(root, host) {
5187
5535
  return { path: file, before, after };
5188
5536
  }
5189
5537
  async function hookEdits(root, host) {
5190
- const file = configPath(host), before = await readText(root, file);
5538
+ const file = configPath2(host), before = await readText(root, file);
5191
5539
  const { planAutomationInstall: planAutomationInstall2 } = await Promise.resolve().then(() => (init_install(), install_exports));
5192
5540
  const plan = await planAutomationInstall2(root, host, hookCommand(host));
5193
5541
  return [
@@ -5215,7 +5563,7 @@ async function inspectHostConfig(root, host, plannedMcp) {
5215
5563
  const text2 = plannedMcp ?? await readText(root, mcpPath(host));
5216
5564
  const config = host === "codex" ? parse2(text2 ?? "") : object.parse(JSON.parse(text2 ?? "{}"));
5217
5565
  const servers = host === "codex" ? config.mcp_servers : config.mcpServers;
5218
- const hooks = automationConfigSchema.parse(JSON.parse(await readText(root, configPath(host)) ?? "{}"));
5566
+ const hooks = automationConfigSchema.parse(JSON.parse(await readText(root, configPath2(host)) ?? "{}"));
5219
5567
  const expected = hookConfig(host, hookCommand(host));
5220
5568
  const compatible = compatibleManagedHookCommands(host);
5221
5569
  const mcp = servers?.mason === void 0 ? null : object.parse(servers.mason);
@@ -5239,7 +5587,7 @@ var init_config = __esm({
5239
5587
  init_install();
5240
5588
  init_files2();
5241
5589
  init_launcher();
5242
- object = z17.record(z17.unknown());
5590
+ object = z19.record(z19.unknown());
5243
5591
  mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
5244
5592
  TOML_START = "# mason:mcp:start";
5245
5593
  TOML_END = "# mason:mcp:end";
@@ -5249,7 +5597,7 @@ var init_config = __esm({
5249
5597
  });
5250
5598
 
5251
5599
  // src/mcp/init.ts
5252
- import { z as z18 } from "zod";
5600
+ import { z as z20 } from "zod";
5253
5601
  async function loadProjectMarker(rootDir) {
5254
5602
  const raw = await readStoreJson(rootDir, ".mason/local/project.json");
5255
5603
  if (raw === null) return null;
@@ -5257,8 +5605,8 @@ async function loadProjectMarker(rootDir) {
5257
5605
  return { ...marker, features: await loadProjectFeatures(rootDir) };
5258
5606
  }
5259
5607
  async function loadProjectFeatures(rootDir) {
5260
- const config = z18.record(z18.unknown()).parse(await readStoreJson(rootDir, ".mason/config.json") ?? {});
5261
- return z18.object({ confluence: z18.boolean().optional() }).passthrough().parse(config.features ?? {});
5608
+ const config = z20.record(z20.unknown()).parse(await readStoreJson(rootDir, ".mason/config.json") ?? {});
5609
+ return z20.object({ confluence: z20.boolean().optional() }).passthrough().parse(config.features ?? {});
5262
5610
  }
5263
5611
  async function saveProjectMarker(rootDir, marker) {
5264
5612
  const { ancillaryEdits: ancillaryEdits2 } = await Promise.resolve().then(() => (init_config(), config_exports));
@@ -5269,10 +5617,10 @@ async function projectMarkerEdits(rootDir, marker) {
5269
5617
  const file = ".mason/local/project.json";
5270
5618
  const edits = [{ path: file, before: await readText(rootDir, file), after: JSON.stringify(local, null, 2) + "\n" }];
5271
5619
  if (features && Object.keys(features).length) {
5272
- const configPath2 = ".mason/config.json", before = await readText(rootDir, configPath2);
5273
- const config = z18.record(z18.unknown()).parse(JSON.parse(before ?? "{}"));
5274
- config.features = { ...z18.record(z18.unknown()).parse(config.features ?? {}), ...features };
5275
- edits.push({ path: configPath2, before, after: JSON.stringify(config, null, 2) + "\n" });
5620
+ const configPath3 = ".mason/config.json", before = await readText(rootDir, configPath3);
5621
+ const config = z20.record(z20.unknown()).parse(JSON.parse(before ?? "{}"));
5622
+ config.features = { ...z20.record(z20.unknown()).parse(config.features ?? {}), ...features };
5623
+ edits.push({ path: configPath3, before, after: JSON.stringify(config, null, 2) + "\n" });
5276
5624
  }
5277
5625
  return edits;
5278
5626
  }
@@ -5285,10 +5633,10 @@ var init_init = __esm({
5285
5633
  "use strict";
5286
5634
  init_storage();
5287
5635
  init_files2();
5288
- markerSchema = z18.object({
5289
- version: z18.literal(1),
5290
- initializedAt: z18.string(),
5291
- features: z18.object({ confluence: z18.boolean().optional() }).passthrough().optional()
5636
+ markerSchema = z20.object({
5637
+ version: z20.literal(1),
5638
+ initializedAt: z20.string(),
5639
+ features: z20.object({ confluence: z20.boolean().optional() }).passthrough().optional()
5292
5640
  }).passthrough();
5293
5641
  CLAUDE_MD_SECTION = `<!-- mason:start -->
5294
5642
  ## Mason project knowledge
@@ -5297,9 +5645,10 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
5297
5645
 
5298
5646
  - Task, bug, change request, or investigation of why the project works this way \u2192 \`get_context\` with the task text and known files: matching decisions, related tests, impact, and any available map entries.
5299
5647
  - Before editing a file \u2192 \`get_impact\` to check references, tests, and historical change partners.
5300
- - 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.
5648
+ - Investigation or correction reveals a reusable project lesson \u2192 \`save_decision\` as soon as the lesson is supported by evidence; do not wait for implementation, task completion, or merge. Record the established observation now and identify unimplemented remedies as tentative. 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.
5301
5649
  - 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.
5302
5650
  - 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.
5651
+ - Asked to review pending project knowledge \u2192 \`mason_automation(action: "status")\` lists pending proposals and uncommitted records. For ordinary tasks, inspect matching proposals through \`get_context\`; do not request acceptance merely because a proposal exists.
5303
5652
  - 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.
5304
5653
  - 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.
5305
5654
  - \`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.
@@ -5309,13 +5658,13 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
5309
5658
 
5310
5659
  - For a dependency advisory, \`review_advisory(action: "dismiss", baselinePath, findingId, reasonCode, note)\` records a one-step dismissal without reviewer or token. Use \`no-dependency-claims\` for a document with no dependency claims (reopens on document content changes), or \`unrelated-manifest-change\` for reviewed changes that do not affect its guidance (reopens on document or manifest changes). Keep the relevant scope committed; commit the dismissal to share it. This never approves a decision.
5311
5660
 
5312
- - When Mason automation is installed, use \`mason_automation(action: "status")\` to inspect configured hooks and observed events, and \`mason_automation(action: "check")\` to resume its retained repair evidence. CLI fallback: \`mason-auto status\` / \`mason-auto check\`. Preserve existing baselines across sessions. Automatic checks do not authorize unrelated repairs or approve advisories.
5661
+ - When Mason automation is installed, use \`mason_automation(action: "status")\` to inspect configured hooks and observed events, and \`mason_automation(action: "check")\` to resume its retained repair evidence. CLI fallback: \`mason status\` / \`mason check\`. Preserve existing baselines across sessions. Automatic checks do not authorize unrelated repairs or approve advisories.
5313
5662
 
5314
5663
  Inspect source for what the retrieved context does not answer.
5315
5664
  <!-- mason:end -->`;
5316
5665
  ASSISTANT_SETUP = `UNIFIED SETUP
5317
- When the user requests Mason setup, call mason_init again with mode: "setup" and host: "codex" or "claude" for the assistant being configured. This uses the same engine as mason-auto setup: retain the initial audit before instruction edits, use the installed mason command on PATH, merge project MCP configuration and hooks, update marker-delimited instructions and Git ignore rules, and record setup. A concept map is optional.
5318
- If the user only requested inspection or review, report findings without running setup. The setup result distinguishes configured from observed activation. Explain the host's native trust step and request a new session; never trust hooks on the user's behalf or claim activation from generated configuration alone. After a normal task finishes, mason_automation(action: "status") or mason-auto status reports observed context use and hook events. Existing findings remain reviewable; setup does not approve advisories or manufacture decisions.
5666
+ When the user requests Mason setup, call mason_init again with mode: "setup" and host: "codex" or "claude" for the assistant being configured. This uses the same engine as mason setup: retain the initial audit before instruction edits, use the installed mason command on PATH, merge project MCP configuration and hooks, update marker-delimited instructions and Git ignore rules, and record setup. A concept map is optional.
5667
+ If the user only requested inspection or review, report findings without running setup. The setup result distinguishes configured from observed activation. Explain the host's native trust step and request a new session; never trust hooks on the user's behalf or claim activation from generated configuration alone. After a normal task finishes, mason_automation(action: "status") or mason status reports observed context use and hook events. Existing findings remain reviewable; setup does not approve advisories or manufacture decisions.
5319
5668
 
5320
5669
  The managed project guidance is:
5321
5670
  ${CLAUDE_MD_SECTION}`;
@@ -5323,7 +5672,7 @@ ${CLAUDE_MD_SECTION}`;
5323
5672
 
5324
5673
  1. Explain the actionable findings with their source evidence. Separate audit issues, advisories, and skipped checks. The review covers committed changes from the merge base to HEAD; workingTree paths are not included in that review. An unavailable or empty check is not proof that the project is correct. Use the CLI for full output if a summary is truncated.
5325
5674
  2. Address findings within the user's requested scope. Setup alone authorizes Mason host configuration, hooks, instructions, and ignore rules; rewriting existing project claims requires repair scope. If repair is authorized, call \`mason_repair(dir, action: "prepare")\` before editing; it saves the full original findings even when this summary is truncated. Inspect relevant source, make grounded edits, then call \`mason_repair(dir, action: "verify", baselinePath)\` with that same baseline, including after any final doc commit. Report resolved, unresolved, review-required, unverified, and new findings. Keep suppressed advisories visible even when setup has already dirtied a doc. Do not invent a decision just to populate the store.
5326
- 3. When the task reveals a real lesson or constraint, compare matching \`get_context\` records first. If its assumptions, scope, or recommended action changed, revise that record using \`save_decision(id: existingId)\`; create a separate record for a genuinely distinct lesson and skip unchanged restatements. Include title, body, category, anchors, and known owner/source/actor information. Preserve supported rationale and sources, replace obsolete instructions, and add new evidence. Missing attribution can be added later. The tool writes a local proposal; editing it preserves revision history and requires a new acceptance, while any earlier accepted revision remains operative. An unchanged save never refreshes its evidence. When decision review is requested, \`review_decision\` prepares the record and code evidence before any authorized verdict. Review and commit records through the normal workflow. Retrieve it on the next relevant task with \`get_context(dir, task, files)\`.
5675
+ 3. Capture a supported lesson as soon as it is established; do not wait for implementation, task completion, or merge. Keep unimplemented remedies tentative. When the task reveals a real lesson or constraint, compare matching \`get_context\` records first. If its assumptions, scope, or recommended action changed, revise that record using \`save_decision(id: existingId)\`; create a separate record for a genuinely distinct lesson and skip unchanged restatements. Include title, body, category, anchors, and known owner/source/actor information. Preserve supported rationale and sources, replace obsolete instructions, and add new evidence. Missing attribution can be added later. The tool writes a local proposal; editing it preserves revision history and requires a new acceptance, while any earlier accepted revision remains operative. An unchanged save never refreshes its evidence. When decision review is requested, \`review_decision\` prepares the record and code evidence before any authorized verdict. Review and commit records through the normal workflow. Retrieve it on the next relevant task with \`get_context(dir, task, files)\`.
5327
5676
 
5328
5677
  ${ASSISTANT_SETUP}
5329
5678
 
@@ -5486,7 +5835,7 @@ var init_types2 = __esm({
5486
5835
  });
5487
5836
 
5488
5837
  // src/review/evidence/vitest.ts
5489
- import { z as z19 } from "zod";
5838
+ import { z as z21 } from "zod";
5490
5839
  function parseVitest(raw, sourceRoot) {
5491
5840
  const report = schema.parse(raw);
5492
5841
  const findings = [], diagnostics = [];
@@ -5535,24 +5884,24 @@ var init_vitest = __esm({
5535
5884
  "use strict";
5536
5885
  init_paths2();
5537
5886
  init_types2();
5538
- count3 = z19.number().int().nonnegative();
5539
- schema = z19.object({
5540
- success: z19.boolean(),
5887
+ count3 = z21.number().int().nonnegative();
5888
+ schema = z21.object({
5889
+ success: z21.boolean(),
5541
5890
  numTotalTests: count3,
5542
5891
  numPassedTests: count3,
5543
5892
  numFailedTests: count3,
5544
5893
  numPendingTests: count3,
5545
5894
  numTodoTests: count3,
5546
5895
  numFailedTestSuites: count3,
5547
- testResults: z19.array(z19.object({
5548
- name: z19.string().min(1),
5549
- status: z19.enum(["passed", "failed"]),
5550
- message: z19.string().optional(),
5551
- assertionResults: z19.array(z19.object({
5552
- fullName: z19.string(),
5553
- status: z19.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
5554
- failureMessages: z19.array(z19.string()).nullable().optional(),
5555
- location: z19.object({ line: count3, column: count3 }).nullable().optional()
5896
+ testResults: z21.array(z21.object({
5897
+ name: z21.string().min(1),
5898
+ status: z21.enum(["passed", "failed"]),
5899
+ message: z21.string().optional(),
5900
+ assertionResults: z21.array(z21.object({
5901
+ fullName: z21.string(),
5902
+ status: z21.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
5903
+ failureMessages: z21.array(z21.string()).nullable().optional(),
5904
+ location: z21.object({ line: count3, column: count3 }).nullable().optional()
5556
5905
  }))
5557
5906
  }))
5558
5907
  });
@@ -5560,7 +5909,7 @@ var init_vitest = __esm({
5560
5909
  });
5561
5910
 
5562
5911
  // src/review/evidence/sarif.ts
5563
- import { z as z20 } from "zod";
5912
+ import { z as z22 } from "zod";
5564
5913
  function parseSarif(raw, sourceRoot) {
5565
5914
  const report = schema2.parse(raw), findings = [], diagnostics = [];
5566
5915
  let active = 0, suppressed = 0, absent = 0, unresolved = 0;
@@ -5651,29 +6000,29 @@ var init_sarif = __esm({
5651
6000
  "use strict";
5652
6001
  init_paths2();
5653
6002
  init_types2();
5654
- index = z20.number().int().nonnegative();
5655
- artifact = z20.object({ uri: z20.string().optional(), uriBaseId: z20.string().optional(), index: index.optional() });
5656
- message = z20.object({ text: z20.string().optional(), markdown: z20.string().optional(), id: z20.string().optional(), arguments: z20.array(z20.string()).optional() });
5657
- level = z20.enum(["error", "warning", "note", "none"]);
5658
- location = z20.object({ physicalLocation: z20.object({ artifactLocation: artifact.optional(), region: z20.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
5659
- schema2 = z20.object({
5660
- version: z20.literal("2.1.0"),
5661
- runs: z20.array(z20.object({
5662
- tool: z20.object({ driver: z20.object({ name: z20.string().min(1), rules: z20.array(z20.object({ id: z20.string(), defaultConfiguration: z20.object({ level: level.optional() }).optional(), messageStrings: z20.record(message).optional() })).optional(), globalMessageStrings: z20.record(message).optional() }) }),
5663
- invocations: z20.array(z20.object({ executionSuccessful: z20.boolean(), commandLine: z20.string().optional(), toolExecutionNotifications: z20.array(z20.object({ level: level.optional(), message })).optional() })).optional(),
5664
- versionControlProvenance: z20.array(z20.object({ revisionId: z20.string().optional() })).optional(),
5665
- originalUriBaseIds: z20.record(artifact).optional(),
5666
- artifacts: z20.array(z20.object({ location: artifact.optional() })).optional(),
5667
- results: z20.array(z20.object({
5668
- ruleId: z20.string().optional(),
6003
+ index = z22.number().int().nonnegative();
6004
+ artifact = z22.object({ uri: z22.string().optional(), uriBaseId: z22.string().optional(), index: index.optional() });
6005
+ message = z22.object({ text: z22.string().optional(), markdown: z22.string().optional(), id: z22.string().optional(), arguments: z22.array(z22.string()).optional() });
6006
+ level = z22.enum(["error", "warning", "note", "none"]);
6007
+ location = z22.object({ physicalLocation: z22.object({ artifactLocation: artifact.optional(), region: z22.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
6008
+ schema2 = z22.object({
6009
+ version: z22.literal("2.1.0"),
6010
+ runs: z22.array(z22.object({
6011
+ tool: z22.object({ driver: z22.object({ name: z22.string().min(1), rules: z22.array(z22.object({ id: z22.string(), defaultConfiguration: z22.object({ level: level.optional() }).optional(), messageStrings: z22.record(message).optional() })).optional(), globalMessageStrings: z22.record(message).optional() }) }),
6012
+ invocations: z22.array(z22.object({ executionSuccessful: z22.boolean(), commandLine: z22.string().optional(), toolExecutionNotifications: z22.array(z22.object({ level: level.optional(), message })).optional() })).optional(),
6013
+ versionControlProvenance: z22.array(z22.object({ revisionId: z22.string().optional() })).optional(),
6014
+ originalUriBaseIds: z22.record(artifact).optional(),
6015
+ artifacts: z22.array(z22.object({ location: artifact.optional() })).optional(),
6016
+ results: z22.array(z22.object({
6017
+ ruleId: z22.string().optional(),
5669
6018
  ruleIndex: index.optional(),
5670
6019
  message,
5671
6020
  level: level.optional(),
5672
- kind: z20.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
5673
- baselineState: z20.enum(["new", "unchanged", "updated", "absent"]).optional(),
5674
- suppressions: z20.array(z20.object({ kind: z20.enum(["inSource", "external"]), status: z20.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
5675
- locations: z20.array(location).optional(),
5676
- relatedLocations: z20.array(location).optional()
6021
+ kind: z22.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
6022
+ baselineState: z22.enum(["new", "unchanged", "updated", "absent"]).optional(),
6023
+ suppressions: z22.array(z22.object({ kind: z22.enum(["inSource", "external"]), status: z22.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
6024
+ locations: z22.array(location).optional(),
6025
+ relatedLocations: z22.array(location).optional()
5677
6026
  })).optional()
5678
6027
  }))
5679
6028
  });
@@ -5681,7 +6030,7 @@ var init_sarif = __esm({
5681
6030
  });
5682
6031
 
5683
6032
  // src/review/evidence/check.ts
5684
- import { z as z21 } from "zod";
6033
+ import { z as z23 } from "zod";
5685
6034
  function parseCheck(raw, sourceRoot) {
5686
6035
  const report = schema3.parse(raw);
5687
6036
  const diagnostics = [], findings = [];
@@ -5721,17 +6070,17 @@ var init_check = __esm({
5721
6070
  "use strict";
5722
6071
  init_paths2();
5723
6072
  init_types2();
5724
- schema3 = z21.object({
5725
- version: z21.literal(1),
5726
- commit: z21.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).optional(),
5727
- results: z21.array(z21.object({
5728
- name: z21.string().min(1).max(1e3),
5729
- status: z21.enum(["passed", "failed", "skipped"]),
5730
- message: z21.string().max(1e5).optional(),
5731
- locations: z21.array(z21.object({
5732
- file: z21.string().min(1).max(4e3),
5733
- line: z21.number().int().positive().optional(),
5734
- column: z21.number().int().positive().optional()
6073
+ schema3 = z23.object({
6074
+ version: z23.literal(1),
6075
+ commit: z23.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).optional(),
6076
+ results: z23.array(z23.object({
6077
+ name: z23.string().min(1).max(1e3),
6078
+ status: z23.enum(["passed", "failed", "skipped"]),
6079
+ message: z23.string().max(1e5).optional(),
6080
+ locations: z23.array(z23.object({
6081
+ file: z23.string().min(1).max(4e3),
6082
+ line: z23.number().int().positive().optional(),
6083
+ column: z23.number().int().positive().optional()
5735
6084
  })).max(100).default([])
5736
6085
  })).max(1e5)
5737
6086
  });
@@ -5740,7 +6089,7 @@ var init_check = __esm({
5740
6089
 
5741
6090
  // src/review/evidence.ts
5742
6091
  import path29 from "path";
5743
- import { z as z22 } from "zod";
6092
+ import { z as z24 } from "zod";
5744
6093
  function relativeArtifact(root, file) {
5745
6094
  const relative = path29.isAbsolute(file) ? path29.relative(root, file) : file;
5746
6095
  const normalized = normalizeRepoPath(relative);
@@ -5781,7 +6130,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
5781
6130
  let manifest, raw;
5782
6131
  try {
5783
6132
  manifest = relativeArtifact(root, supplied);
5784
- raw = z22.object({ version: z22.literal(1), checks: z22.array(z22.unknown()) }).parse(await readStoreJson(root, manifest));
6133
+ raw = z24.object({ version: z24.literal(1), checks: z24.array(z24.unknown()) }).parse(await readStoreJson(root, manifest));
5785
6134
  } catch (error) {
5786
6135
  output.diagnostics.push(`${supplied}: ${String(error)}`);
5787
6136
  continue;
@@ -5936,31 +6285,31 @@ var init_evidence2 = __esm({
5936
6285
  init_sarif();
5937
6286
  init_check();
5938
6287
  init_types2();
5939
- checkSchema2 = z22.object({
5940
- id: z22.string().min(1).max(100),
5941
- kind: z22.enum(["tests", "static-analysis", "security", "complexity", "duplication", "documentation"]),
5942
- tool: z22.string().min(1).max(200),
5943
- command: z22.string().min(1).max(2e3),
5944
- commit: z22.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
5945
- workingTreeClean: z22.boolean().optional(),
5946
- source: z22.string().max(2e3).optional(),
5947
- sourceRoot: z22.string().min(1).max(4e3).optional(),
5948
- status: z22.enum(["completed", "skipped", "unavailable"]).default("completed"),
5949
- reason: z22.string().min(1).max(2e3).optional(),
5950
- exitCode: z22.number().int().nullable().optional(),
5951
- environment: z22.object({
5952
- toolVersion: z22.string().min(1).max(200),
5953
- runtime: z22.string().min(1).max(200).optional(),
5954
- platform: z22.string().min(1).max(200).optional(),
5955
- configuration: z22.array(z22.string().min(1).max(4e3)).max(100).optional()
6288
+ checkSchema2 = z24.object({
6289
+ id: z24.string().min(1).max(100),
6290
+ kind: z24.enum(["tests", "static-analysis", "security", "complexity", "duplication", "documentation"]),
6291
+ tool: z24.string().min(1).max(200),
6292
+ command: z24.string().min(1).max(2e3),
6293
+ commit: z24.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
6294
+ workingTreeClean: z24.boolean().optional(),
6295
+ source: z24.string().max(2e3).optional(),
6296
+ sourceRoot: z24.string().min(1).max(4e3).optional(),
6297
+ status: z24.enum(["completed", "skipped", "unavailable"]).default("completed"),
6298
+ reason: z24.string().min(1).max(2e3).optional(),
6299
+ exitCode: z24.number().int().nullable().optional(),
6300
+ environment: z24.object({
6301
+ toolVersion: z24.string().min(1).max(200),
6302
+ runtime: z24.string().min(1).max(200).optional(),
6303
+ platform: z24.string().min(1).max(200).optional(),
6304
+ configuration: z24.array(z24.string().min(1).max(4e3)).max(100).optional()
5956
6305
  }).optional(),
5957
- report: z22.object({ format: z22.enum(["vitest-json", "sarif", "mason-check-json"]), path: z22.string().min(1).max(4e3) }).optional()
6306
+ report: z24.object({ format: z24.enum(["vitest-json", "sarif", "mason-check-json"]), path: z24.string().min(1).max(4e3) }).optional()
5958
6307
  });
5959
6308
  }
5960
6309
  });
5961
6310
 
5962
6311
  // src/review/review.ts
5963
- import fs21 from "fs/promises";
6312
+ import fs22 from "fs/promises";
5964
6313
  import path30 from "path";
5965
6314
  import { execFile as execFile7 } from "child_process";
5966
6315
  import { promisify as promisify7 } from "util";
@@ -6040,7 +6389,7 @@ async function computeReview(rootDir, base, options = {}) {
6040
6389
  analyzed,
6041
6390
  async (relPath) => {
6042
6391
  try {
6043
- await fs21.access(path30.join(resolvedRoot, relPath));
6392
+ await fs22.access(path30.join(resolvedRoot, relPath));
6044
6393
  return true;
6045
6394
  } catch {
6046
6395
  return false;
@@ -6516,12 +6865,12 @@ __export(assemble_exports, {
6516
6865
  assembleContext: () => assembleContext
6517
6866
  });
6518
6867
  import path33 from "path";
6519
- import fs22 from "fs/promises";
6868
+ import fs23 from "fs/promises";
6520
6869
  async function collectImpact(root, candidates) {
6521
6870
  const targets2 = /* @__PURE__ */ new Set();
6522
6871
  let sourceFiles;
6523
6872
  for (const candidate of sanitizeRepoPaths(candidates)) {
6524
- const stat = await fs22.stat(path33.join(root, candidate)).catch(() => null);
6873
+ const stat = await fs23.stat(path33.join(root, candidate)).catch(() => null);
6525
6874
  if (stat?.isDirectory()) {
6526
6875
  sourceFiles ??= await (await createFileAccess(root)).list();
6527
6876
  for (const file of sourceFiles) {
@@ -6743,13 +7092,13 @@ __export(observations_exports, {
6743
7092
  observeActivation: () => observeActivation,
6744
7093
  readObservation: () => readObservation
6745
7094
  });
6746
- import fs23 from "fs/promises";
6747
- import { z as z23 } from "zod";
6748
- function observationPath(directory, host, revision, version = executingVersion()) {
6749
- return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
7095
+ import fs24 from "fs/promises";
7096
+ import { z as z25 } from "zod";
7097
+ function observationPath(directory2, host, revision, version = executingVersion()) {
7098
+ return `${directory2}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
6750
7099
  }
6751
- async function readObservation(root, directory, host, revision, version = executingVersion()) {
6752
- const raw = await readStoreJson(root, observationPath(directory, host, revision, version));
7100
+ async function readObservation(root, directory2, host, revision, version = executingVersion()) {
7101
+ const raw = await readStoreJson(root, observationPath(directory2, host, revision, version));
6753
7102
  if (raw === null) return null;
6754
7103
  const record = observationSchema.parse(raw);
6755
7104
  if (record.root !== root || record.host !== host || record.revision !== revision || record.masonVersion !== version) throw new Error("Activation receipt belongs to another installation.");
@@ -6766,12 +7115,12 @@ async function observeActivation(dir, event, options = {}) {
6766
7115
  return inactiveSetupMessage();
6767
7116
  }
6768
7117
  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];
6769
- const ws = capturedDirectory ? { root: await fs23.realpath(dir), directory: capturedDirectory } : await workspace(dir);
6770
- if (ws.root !== await fs23.realpath(process.env.MASON_SETUP_ROOT)) return null;
7118
+ const ws = capturedDirectory ? { root: await fs24.realpath(dir), directory: capturedDirectory } : await workspace(dir);
7119
+ if (ws.root !== await fs24.realpath(process.env.MASON_SETUP_ROOT)) return null;
6771
7120
  const { setup } = await effectiveSetup(ws.root);
6772
7121
  if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
6773
- const directory = ws.directory + "/activation";
6774
- await withLock(ws.root, directory, async () => {
7122
+ const directory2 = ws.directory + "/activation";
7123
+ await withLock(ws.root, directory2, async () => {
6775
7124
  const now = (/* @__PURE__ */ new Date()).toISOString();
6776
7125
  const record = await readObservation(ws.root, ws.directory, host, revision) ?? {
6777
7126
  version: 1,
@@ -6815,19 +7164,19 @@ var init_observations = __esm({
6815
7164
  init_storage();
6816
7165
  init_model();
6817
7166
  init_launcher();
6818
- observationSchema = z23.object({
6819
- version: z23.literal(1),
6820
- masonVersion: z23.string(),
6821
- root: z23.string(),
6822
- revision: z23.string(),
6823
- host: z23.enum(["codex", "claude"]),
6824
- contextCalls: z23.number().int().nonnegative(),
6825
- lastContextAt: z23.string().optional(),
6826
- sessions: z23.record(z23.object({
6827
- events: z23.array(z23.enum(events)),
6828
- at: z23.string(),
6829
- verificationStatus: z23.string().optional(),
6830
- reportPath: z23.string().optional()
7167
+ observationSchema = z25.object({
7168
+ version: z25.literal(1),
7169
+ masonVersion: z25.string(),
7170
+ root: z25.string(),
7171
+ revision: z25.string(),
7172
+ host: z25.enum(["codex", "claude"]),
7173
+ contextCalls: z25.number().int().nonnegative(),
7174
+ lastContextAt: z25.string().optional(),
7175
+ sessions: z25.record(z25.object({
7176
+ events: z25.array(z25.enum(events)),
7177
+ at: z25.string(),
7178
+ verificationStatus: z25.string().optional(),
7179
+ reportPath: z25.string().optional()
6831
7180
  }))
6832
7181
  });
6833
7182
  }
@@ -6854,7 +7203,7 @@ async function setupStatus(dir) {
6854
7203
  version: 1,
6855
7204
  status: pending.length ? "incomplete" : "not-configured",
6856
7205
  hosts: {},
6857
- next: pending.length ? "Setup did not finish. Rerun mason-auto setup --host " + pending[0] + " to resume using the retained original evidence." : "Project hooks are inactive until local setup. Run mason setup --host codex or --host claude."
7206
+ next: pending.length ? "Setup did not finish. Rerun mason setup --host " + pending[0] + " to resume using the retained original evidence." : "Project hooks are inactive until local setup. Run mason setup --host codex or --host claude."
6858
7207
  };
6859
7208
  }
6860
7209
  const hosts = {};
@@ -6888,7 +7237,7 @@ async function setupStatus(dir) {
6888
7237
  if (!mcp || !hooks || !instructionsCurrent || !configured) pending.push("Rerun setup to reconcile project configuration; inspect any disabled host settings.");
6889
7238
  if (!observation?.contextCalls) pending.push("Start a new assistant session and request task context through Mason's get_context tool.");
6890
7239
  if (!complete) pending.push("Review/trust the host configuration, then complete a normal task to observe the full hook lifecycle.");
6891
- if (automation.status === "unavailable") pending.push("The latest automation attempt did not establish verification. Run mason-auto check and inspect its diagnostics.");
7240
+ if (automation.status === "unavailable") pending.push("The latest automation attempt did not establish verification. Run mason check and inspect its diagnostics.");
6892
7241
  const healthy = installed.available && mcp && hooks && instructionsCurrent && configured && automation.status !== "unavailable";
6893
7242
  hosts[host] = {
6894
7243
  status: healthy ? complete && observation?.contextCalls ? "active" : "pending" : "attention",
@@ -7022,6 +7371,11 @@ async function setupProject(dir, options = {}) {
7022
7371
  options.progress?.step("Checking configuration and retained findings");
7023
7372
  const checked = await automate(ws.root, { event: "turn_start" });
7024
7373
  const configured = await inspectHostConfig(ws.root, host, void 0);
7374
+ const stats = await configureUsefulness(ws.root, true, { onlyIfUnset: true }).catch(() => ({
7375
+ enabled: null,
7376
+ storage: ".mason/local/usefulness",
7377
+ note: "Local stats unavailable. Inspect mason stats --json, or use mason stats --disable to reset the preference. Existing observations were retained."
7378
+ }));
7025
7379
  await writeStoreJson(ws.root, receiptPath, {
7026
7380
  version: 1,
7027
7381
  host,
@@ -7044,6 +7398,7 @@ async function setupProject(dir, options = {}) {
7044
7398
  findings,
7045
7399
  reportPath: checked.report.reportPath,
7046
7400
  activation: await setupStatus(ws.root),
7401
+ stats,
7047
7402
  next: configured.disabled || configured.mcpDisabled ? "Mason hooks or MCP are disabled in project configuration. Review that setting before activation." : host === "codex" ? "Review/trust this project's MCP configuration and hooks in Codex (/hooks in the CLI), then start a new session and give it a normal task." : "Approve the project MCP server in Claude Code, then start a new session and give it a normal task."
7048
7403
  };
7049
7404
  });
@@ -7055,18 +7410,20 @@ function summarizeSetup(result) {
7055
7410
  " MCP server and lifecycle hooks use mason from PATH.",
7056
7411
  " Project instructions updated; original audit evidence retained.",
7057
7412
  ` ${result.changedFiles.length} files changed.`,
7413
+ result.stats.enabled === null ? result.stats.note : result.stats.enabled ? "Local stats enabled: metadata stays in .mason/local/usefulness/; nothing is uploaded. View with mason stats; disable with mason stats --disable." : "Local stats remain disabled by your saved preference. Enable with mason stats --enable.",
7058
7414
  `Initial audit: ${audit.status}${"counts" in audit ? `; ${audit.counts.issues} issues, ${audit.counts.advisories + audit.counts.suppressedAdvisories} advisories` : ""}.`,
7059
7415
  ..."issues" in audit ? audit.issues.slice(0, 3).map((f) => " " + f.message) : [],
7060
7416
  `Original evidence: ${result.initialReportPath}`,
7061
7417
  "Activation: " + result.activation.status + ".",
7062
7418
  result.next,
7063
7419
  "If the assistant cannot find mason, restart the desktop app so it picks up PATH.",
7064
- "After the task finishes, run mason-auto status. Configuration alone does not establish activation."
7420
+ "After the task finishes, run mason status. Configuration alone does not establish activation."
7065
7421
  ].join("\n");
7066
7422
  }
7067
7423
  var init_setup = __esm({
7068
7424
  "src/setup/setup.ts"() {
7069
7425
  "use strict";
7426
+ init_usefulness();
7070
7427
  init_evidence();
7071
7428
  init_store();
7072
7429
  init_runtime();
@@ -7204,7 +7561,7 @@ __export(config_exports2, {
7204
7561
  saveConfluenceConfig: () => saveConfluenceConfig,
7205
7562
  validateProvider: () => validateProvider
7206
7563
  });
7207
- import fs24 from "fs/promises";
7564
+ import fs25 from "fs/promises";
7208
7565
  import path34 from "path";
7209
7566
  import os3 from "os";
7210
7567
  import { execFile as execFile9 } from "child_process";
@@ -7217,15 +7574,15 @@ function configFile() {
7217
7574
  }
7218
7575
  async function loadConfig() {
7219
7576
  try {
7220
- const raw = await fs24.readFile(configFile(), "utf-8");
7577
+ const raw = await fs25.readFile(configFile(), "utf-8");
7221
7578
  return JSON.parse(raw);
7222
7579
  } catch {
7223
7580
  return null;
7224
7581
  }
7225
7582
  }
7226
7583
  async function saveConfig(config) {
7227
- await fs24.mkdir(configDir(), { recursive: true });
7228
- await fs24.writeFile(configFile(), JSON.stringify(config, null, 2), "utf-8");
7584
+ await fs25.mkdir(configDir(), { recursive: true });
7585
+ await fs25.writeFile(configFile(), JSON.stringify(config, null, 2), "utf-8");
7229
7586
  }
7230
7587
  function getDefaultModel(provider) {
7231
7588
  return DEFAULT_MODELS[provider];
@@ -7385,7 +7742,7 @@ var init_renderer = __esm({
7385
7742
  });
7386
7743
 
7387
7744
  // src/confluence/diff.ts
7388
- import fs25 from "fs/promises";
7745
+ import fs26 from "fs/promises";
7389
7746
  import path35 from "path";
7390
7747
  import { createHash as createHash6 } from "crypto";
7391
7748
  function hashDescription(description) {
@@ -7399,7 +7756,7 @@ function syncStatePath(rootDir) {
7399
7756
  }
7400
7757
  async function loadSyncState(rootDir) {
7401
7758
  try {
7402
- const raw = await fs25.readFile(syncStatePath(rootDir), "utf-8");
7759
+ const raw = await fs26.readFile(syncStatePath(rootDir), "utf-8");
7403
7760
  const parsed = JSON.parse(raw);
7404
7761
  if (parsed.version !== 2) return null;
7405
7762
  return parsed;
@@ -7408,8 +7765,8 @@ async function loadSyncState(rootDir) {
7408
7765
  }
7409
7766
  }
7410
7767
  async function saveSyncState(rootDir, state) {
7411
- await fs25.mkdir(syncStateDir(rootDir), { recursive: true });
7412
- await fs25.writeFile(
7768
+ await fs26.mkdir(syncStateDir(rootDir), { recursive: true });
7769
+ await fs26.writeFile(
7413
7770
  syncStatePath(rootDir),
7414
7771
  JSON.stringify(state, null, 2),
7415
7772
  "utf-8"
@@ -7962,12 +8319,12 @@ init_advisory_review();
7962
8319
  init_decisions();
7963
8320
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7964
8321
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7965
- import { z as z24 } from "zod";
8322
+ import { z as z26 } from "zod";
7966
8323
 
7967
8324
  // src/mcp/tools.ts
7968
8325
  init_repair();
7969
8326
  init_advisory_review();
7970
- import fs26 from "fs/promises";
8327
+ import fs27 from "fs/promises";
7971
8328
  import path36 from "path";
7972
8329
  import { execFile as execFile11 } from "child_process";
7973
8330
  import { promisify as promisify11 } from "util";
@@ -8591,14 +8948,14 @@ init_files();
8591
8948
  import { createHash as createHash4 } from "crypto";
8592
8949
  import { execFile as execFile5 } from "child_process";
8593
8950
  import { promisify as promisify5 } from "util";
8594
- import { z as z10 } from "zod";
8951
+ import { z as z11 } from "zod";
8595
8952
  var exec3 = promisify5(execFile5);
8596
- var requestSchema = z10.object({
8597
- id: z10.string().regex(/^[a-zA-Z0-9_-]+$/),
8598
- action: z10.enum(["prepare", "accept", "reaffirm", "retire"]).default("prepare"),
8599
- reviewer: z10.string().trim().min(1).max(200).optional(),
8600
- note: z10.string().trim().min(1).max(1500).optional(),
8601
- reviewToken: z10.string().regex(/^[a-f0-9]{64}$/).optional()
8953
+ var requestSchema = z11.object({
8954
+ id: z11.string().regex(/^[a-zA-Z0-9_-]+$/),
8955
+ action: z11.enum(["prepare", "accept", "reaffirm", "retire"]).default("prepare"),
8956
+ reviewer: z11.string().trim().min(1).max(200).optional(),
8957
+ note: z11.string().trim().min(1).max(1500).optional(),
8958
+ reviewToken: z11.string().regex(/^[a-f0-9]{64}$/).optional()
8602
8959
  });
8603
8960
  async function reviewState(root, record) {
8604
8961
  const [headHash, workingTree, changes] = await Promise.all([
@@ -8825,16 +9182,16 @@ init_lock();
8825
9182
  init_storage();
8826
9183
  init_snapshot();
8827
9184
  import fs17 from "fs/promises";
8828
- import { z as z11 } from "zod";
9185
+ import { z as z12 } from "zod";
8829
9186
  var DIRECTORY = ".mason/partial-snapshots";
8830
- var partialSchema = z11.object({
8831
- batchId: z11.string().regex(/^[a-zA-Z0-9_-]+$/),
8832
- offset: z11.number().int().nonnegative(),
8833
- features: z11.record(featureSchema),
8834
- flows: z11.record(flowSchema),
8835
- savedAt: z11.string()
9187
+ var partialSchema = z12.object({
9188
+ batchId: z12.string().regex(/^[a-zA-Z0-9_-]+$/),
9189
+ offset: z12.number().int().nonnegative(),
9190
+ features: z12.record(featureSchema),
9191
+ flows: z12.record(flowSchema),
9192
+ savedAt: z12.string()
8836
9193
  });
8837
- var scopeSchema2 = z11.object({ files: z11.array(z11.string()), savedAt: z11.string() });
9194
+ var scopeSchema2 = z12.object({ files: z12.array(z12.string()), savedAt: z12.string() });
8838
9195
  async function savePartial(rootDir, partial) {
8839
9196
  if (!/^[a-zA-Z0-9_-]+$/.test(partial.batchId)) throw new Error(`Invalid batchId: ${partial.batchId}`);
8840
9197
  await withSnapshotWrite(rootDir, () => writeStoreJson(rootDir, `${DIRECTORY}/${partial.batchId}.json`, partialSchema.parse(partial)));
@@ -8941,7 +9298,7 @@ async function detectProjectSnapshot(rootDir) {
8941
9298
  const present = [];
8942
9299
  for (const file of buildFiles) {
8943
9300
  try {
8944
- await fs26.access(path36.join(rootDir, file));
9301
+ await fs27.access(path36.join(rootDir, file));
8945
9302
  present.push(file);
8946
9303
  } catch {
8947
9304
  }
@@ -9833,21 +10190,21 @@ function createMcpServer() {
9833
10190
  const server = new McpServer(
9834
10191
  {
9835
10192
  name: "mason",
9836
- version: "0.19.1"
10193
+ version: "0.20.1"
9837
10194
  },
9838
10195
  {
9839
- 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.'
10196
+ 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 as soon as evidence supports them, without waiting for implementation or merge; 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.'
9840
10197
  }
9841
10198
  );
9842
10199
  server.tool(
9843
10200
  "mason_init",
9844
10201
  "Inspect this project now: returns documentation audit findings, committed-diff review findings, decision/map status, and a quickstart playbook. Quickstart and map modes are read-only and deterministic. Explicit mode: setup configures MCP, instructions and lifecycle hooks to use mason on PATH while retaining original audit evidence; use it only when the user requests setup. Optional host selects codex or claude. Optional base selects the review comparison; evidence imports CI manifests with check outcomes, commit freshness, and links to changed files and accepted decisions. mode: map returns the full Map-Reduce build workflow. Repeat calls refresh findings even after setup.",
9845
10202
  {
9846
- dir: z24.string().describe("Absolute path to the project root directory"),
9847
- mode: z24.enum(["quickstart", "map", "setup"]).optional().default("quickstart").describe("Quickstart inspects without edits; map requests an architecture build; setup installs the shared onboarding flow."),
9848
- host: z24.enum(["codex", "claude"]).optional().describe("Assistant to configure in setup mode; inferred only when unambiguous."),
9849
- base: z24.string().optional().describe("Git ref for committed-diff review. Defaults to the first available main branch ref."),
9850
- evidence: z24.array(z24.string()).max(10).optional().describe("Repository-local CI evidence manifests to include in the review. Imports Vitest JSON, SARIF, and native-validator mason-check-json results without executing check commands.")
10203
+ dir: z26.string().describe("Absolute path to the project root directory"),
10204
+ mode: z26.enum(["quickstart", "map", "setup"]).optional().default("quickstart").describe("Quickstart inspects without edits; map requests an architecture build; setup installs the shared onboarding flow."),
10205
+ host: z26.enum(["codex", "claude"]).optional().describe("Assistant to configure in setup mode; inferred only when unambiguous."),
10206
+ base: z26.string().optional().describe("Git ref for committed-diff review. Defaults to the first available main branch ref."),
10207
+ evidence: z26.array(z26.string()).max(10).optional().describe("Repository-local CI evidence manifests to include in the review. Imports Vitest JSON, SARIF, and native-validator mason-check-json results without executing check commands.")
9851
10208
  },
9852
10209
  async ({ dir, mode, host, base, evidence }) => {
9853
10210
  const result = await masonInit(dir, { mode, host, base, evidence });
@@ -9856,10 +10213,10 @@ function createMcpServer() {
9856
10213
  );
9857
10214
  server.tool(
9858
10215
  "mason_automation",
9859
- "Inspect installed automation and observed host events, or resume/check retained documentation repair evidence across sessions. status is read-only; check saves local baselines and verification reports without editing source or approving advisories. Returns concise results with a full report path. Works without a map.",
10216
+ "Inspect installed automation, observed host events, pending decision records and the notification baseline, or check all retained documentation findings across sessions. status is read-only; check saves local baselines and verification reports without editing source, approving advisories or consuming pending completion notices. Returns concise results with a full report path. Works without a map.",
9860
10217
  {
9861
- dir: z24.string().describe("Absolute path to the project directory"),
9862
- action: z24.enum(["status", "check"]).describe("Inspect configuration and receipts, or capture/resume and verify original audit evidence.")
10218
+ dir: z26.string().describe("Absolute path to the project directory"),
10219
+ action: z26.enum(["status", "check"]).describe("Inspect configuration and receipts, or capture/resume and verify original audit evidence.")
9863
10220
  },
9864
10221
  async ({ dir, action }) => ({ content: [{ type: "text", text: await masonAutomation(dir, action) }] })
9865
10222
  );
@@ -9867,10 +10224,10 @@ function createMcpServer() {
9867
10224
  "mason_repair",
9868
10225
  "Track documentation repairs against original audit evidence. prepare saves a local baseline and returns a scoped work order; verify reads that baseline and reports resolved, unresolved, review-required, unverified, and new findings. Suppressed advisories remain unresolved. Does not edit documentation or approve decisions. No map required.",
9869
10226
  {
9870
- dir: z24.string().describe("Absolute path to the project root directory"),
9871
- action: z24.enum(["prepare", "verify"]),
9872
- baselinePath: z24.string().optional().describe("Original baseline path returned by prepare; required for verify."),
9873
- checks: z24.array(z24.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"])).min(1).optional().describe("Optional audit check subset for prepare. Verification always uses the original checks.")
10227
+ dir: z26.string().describe("Absolute path to the project root directory"),
10228
+ action: z26.enum(["prepare", "verify"]),
10229
+ baselinePath: z26.string().optional().describe("Original baseline path returned by prepare; required for verify."),
10230
+ checks: z26.array(z26.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"])).min(1).optional().describe("Optional audit check subset for prepare. Verification always uses the original checks.")
9874
10231
  },
9875
10232
  async ({ dir, action, baselinePath, checks }) => {
9876
10233
  const result = await masonRepair(dir, { action, baselinePath, checks });
@@ -9880,15 +10237,15 @@ function createMcpServer() {
9880
10237
  server.tool(
9881
10238
  "review_advisory",
9882
10239
  "Prepare an assessment of an original repair advisory and its exact repository scope, then record authorized addressed, inapplicable or deferred outcomes with reviewToken, reviewer and note. Relevant changes reopen reviews; unrelated commits preserve them. Records belong in Git. Decision findings route to review_decision; this tool never approves decisions. Recorded identities are assertions, not authenticated approvals. For dependency advisories, action dismiss with reasonCode and note records a one-step dismissal without a token or reviewer. no-dependency-claims reopens only on document content changes; unrelated-manifest-change also tracks scoped manifests.",
9883
- { dir: z24.string().describe("Absolute path to the project root"), ...advisoryReviewRequest.shape },
10240
+ { dir: z26.string().describe("Absolute path to the project root"), ...advisoryReviewRequest.shape },
9884
10241
  async ({ dir, ...request }) => ({ content: [{ type: "text", text: await masonReviewAdvisory(dir, request) }] })
9885
10242
  );
9886
10243
  server.tool(
9887
10244
  "mason_complete_init",
9888
10245
  "Record assistant instruction setup locally in ignored .mason/local/project.json, with feature settings in shared .mason/config.json. Other tools work without this marker. Repeated calls preserve the original setup time and existing settings; pass confluenceConfigured only to change that setting.",
9889
10246
  {
9890
- dir: z24.string().describe("Absolute path to the project root directory"),
9891
- confluenceConfigured: z24.boolean().optional().describe("Set the Confluence setup status; omit to preserve the existing value")
10247
+ dir: z26.string().describe("Absolute path to the project root directory"),
10248
+ confluenceConfigured: z26.boolean().optional().describe("Set the Confluence setup status; omit to preserve the existing value")
9892
10249
  },
9893
10250
  async ({ dir, confluenceConfigured }) => {
9894
10251
  const result = await masonCompleteInit(dir, { confluenceConfigured });
@@ -9899,11 +10256,11 @@ function createMcpServer() {
9899
10256
  "mason_set_confluence",
9900
10257
  "Configure Confluence credentials. Two-step flow: (1) call without `spaceKey` to validate the credentials and receive a list of available spaces \u2014 relay them to the user. (2) call again with the same `baseUrl`/`email`/`apiToken` plus the chosen `spaceKey` to persist. Credentials are stored in `~/.mason/config.json`. Warn the user that the API token will be visible in chat history before they paste it.",
9901
10258
  {
9902
- baseUrl: z24.string().describe("Confluence base URL. Accepts `acme`, `acme.atlassian.net`, or `https://acme.atlassian.net` (normalized automatically)."),
9903
- email: z24.string().describe("User's Atlassian account email"),
9904
- apiToken: z24.string().describe("API token from id.atlassian.com/manage-profile/security/api-tokens"),
9905
- spaceKey: z24.string().optional().describe("Confluence space key. Omit on the first call to list available spaces."),
9906
- parentPageId: z24.string().optional().describe("Optional parent page ID under which Mason's index page is created")
10259
+ baseUrl: z26.string().describe("Confluence base URL. Accepts `acme`, `acme.atlassian.net`, or `https://acme.atlassian.net` (normalized automatically)."),
10260
+ email: z26.string().describe("User's Atlassian account email"),
10261
+ apiToken: z26.string().describe("API token from id.atlassian.com/manage-profile/security/api-tokens"),
10262
+ spaceKey: z26.string().optional().describe("Confluence space key. Omit on the first call to list available spaces."),
10263
+ parentPageId: z26.string().optional().describe("Optional parent page ID under which Mason's index page is created")
9907
10264
  },
9908
10265
  async ({ baseUrl, email, apiToken, spaceKey, parentPageId }) => {
9909
10266
  const result = await masonSetConfluence({
@@ -9920,7 +10277,7 @@ function createMcpServer() {
9920
10277
  "full_analysis",
9921
10278
  "One-shot orientation for a project WITHOUT a concept map (get_snapshot returned exists:false). Returns git history stats, project structure with file counts, curated code sample previews (~60 lines each), and test-to-source mapping. On a mapped project, prefer get_snapshot \u2014 it is cheaper and answers feature/architecture questions directly.",
9922
10279
  {
9923
- dir: z24.string().describe("Absolute path to the project root directory")
10280
+ dir: z26.string().describe("Absolute path to the project root directory")
9924
10281
  },
9925
10282
  async ({ dir }) => {
9926
10283
  const result = await fullAnalysis(dir);
@@ -9933,7 +10290,7 @@ function createMcpServer() {
9933
10290
  "analyze_project",
9934
10291
  "Run git history analysis on a codebase. Returns commit convention patterns, stale directories, and frequently changed files. These are aggregate stats across hundreds of commits that would be expensive to compute manually.",
9935
10292
  {
9936
- dir: z24.string().describe("Absolute path to the project root directory")
10293
+ dir: z26.string().describe("Absolute path to the project root directory")
9937
10294
  },
9938
10295
  async ({ dir }) => {
9939
10296
  const result = await analyzeProject(dir);
@@ -9946,8 +10303,8 @@ function createMcpServer() {
9946
10303
  "get_code_samples",
9947
10304
  "Get previews (first ~60 lines) of representative source files from the codebase. Includes entry points, config files, hot files (frequently changed), test examples, and one file per directory for breadth. Read files natively for full content.",
9948
10305
  {
9949
- dir: z24.string().describe("Absolute path to the project root directory"),
9950
- count: z24.number().optional().default(15).describe("Maximum number of files to sample (default: 15)")
10306
+ dir: z26.string().describe("Absolute path to the project root directory"),
10307
+ count: z26.number().optional().default(15).describe("Maximum number of files to sample (default: 15)")
9951
10308
  },
9952
10309
  async ({ dir, count: count4 }) => {
9953
10310
  const result = await getCodeSamples(dir, count4);
@@ -9960,7 +10317,7 @@ function createMcpServer() {
9960
10317
  "get_snapshot",
9961
10318
  "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.",
9962
10319
  {
9963
- dir: z24.string().describe("Absolute path to the project root directory")
10320
+ dir: z26.string().describe("Absolute path to the project root directory")
9964
10321
  },
9965
10322
  async ({ dir }) => {
9966
10323
  const result = await getSnapshot(dir);
@@ -9973,9 +10330,9 @@ function createMcpServer() {
9973
10330
  "get_context",
9974
10331
  "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.",
9975
10332
  {
9976
- dir: z24.string().describe("Absolute path to the project root directory"),
9977
- 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"),
9978
- files: z24.array(z24.string()).optional().describe("Optional file paths already known to be involved (e.g. from a diff or stack trace). Entries containing them are boosted above pure text matches.")
10333
+ dir: z26.string().describe("Absolute path to the project root directory"),
10334
+ task: z26.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"),
10335
+ files: z26.array(z26.string()).optional().describe("Optional file paths already known to be involved (e.g. from a diff or stack trace). Entries containing them are boosted above pure text matches.")
9979
10336
  },
9980
10337
  async ({ dir, task, files }) => {
9981
10338
  const result = await getContext(dir, task, files);
@@ -9991,10 +10348,10 @@ function createMcpServer() {
9991
10348
  "generate_snapshot_batch",
9992
10349
  "Map step of the concept-map build. Returns one batch of source files (skeletons of every file in the batch plus a few deeper-read bodies for grounding), along with a system prompt instructing you to derive features and flows for ONLY this batch. Call repeatedly with the returned `nextOffset` until it is null, calling `save_partial_snapshot` between each call. Use product-natural feature names so partials merge cleanly in the reduce step.",
9993
10350
  {
9994
- dir: z24.string().describe("Absolute path to the project root directory"),
9995
- offset: z24.number().int().optional().describe("0-indexed file offset to start the batch at. Omit on the first call; pass the `nextOffset` from the previous response for subsequent calls."),
9996
- batchSize: z24.number().int().optional().describe("Files per batch. Defaults to 50."),
9997
- files: z24.array(z24.string()).optional().describe("Scope the batch walk to this explicit file list \u2014 e.g. the drift set from mason_check_drift (changedFiles + unmappedFiles). Pass the SAME list on every batch call of one refresh run. Triggers refresh mode: reduce_snapshot will merge the partials into the existing map instead of rebuilding it.")
10351
+ dir: z26.string().describe("Absolute path to the project root directory"),
10352
+ offset: z26.number().int().optional().describe("0-indexed file offset to start the batch at. Omit on the first call; pass the `nextOffset` from the previous response for subsequent calls."),
10353
+ batchSize: z26.number().int().optional().describe("Files per batch. Defaults to 50."),
10354
+ files: z26.array(z26.string()).optional().describe("Scope the batch walk to this explicit file list \u2014 e.g. the drift set from mason_check_drift (changedFiles + unmappedFiles). Pass the SAME list on every batch call of one refresh run. Triggers refresh mode: reduce_snapshot will merge the partials into the existing map instead of rebuilding it.")
9998
10355
  },
9999
10356
  async ({ dir, offset, batchSize, files }) => {
10000
10357
  const result = await generateSnapshotBatch(dir, offset, batchSize, files);
@@ -10007,23 +10364,23 @@ function createMcpServer() {
10007
10364
  "save_partial_snapshot",
10008
10365
  "Persist the partial concept map you derived for one batch. Call this once per batch, with the `batchId` from the `generate_snapshot_batch` response. Partials accumulate in `.mason/partial-snapshots/` and are merged in the reduce step.",
10009
10366
  {
10010
- dir: z24.string().describe("Absolute path to the project root directory"),
10011
- batchId: z24.string().describe("The `batchId` returned by `generate_snapshot_batch`."),
10012
- offset: z24.number().int().describe("The `offset` returned by `generate_snapshot_batch`. Used to order partials in the reduce step."),
10013
- features: z24.record(
10014
- z24.object({
10015
- description: z24.string(),
10016
- files: z24.array(z24.string()),
10017
- tests: z24.array(z24.string()).optional(),
10018
- type: z24.enum(["capability", "infrastructure"]).optional().describe(
10367
+ dir: z26.string().describe("Absolute path to the project root directory"),
10368
+ batchId: z26.string().describe("The `batchId` returned by `generate_snapshot_batch`."),
10369
+ offset: z26.number().int().describe("The `offset` returned by `generate_snapshot_batch`. Used to order partials in the reduce step."),
10370
+ features: z26.record(
10371
+ z26.object({
10372
+ description: z26.string(),
10373
+ files: z26.array(z26.string()),
10374
+ tests: z26.array(z26.string()).optional(),
10375
+ type: z26.enum(["capability", "infrastructure"]).optional().describe(
10019
10376
  '"capability" (user-facing functionality) or "infrastructure" (internal plumbing with no end user \u2014 DI/service wiring, config, logging, adapters). Defaults to "capability".'
10020
10377
  )
10021
10378
  })
10022
10379
  ).describe("Partial features for this batch only \u2014 files outside the batch will be added by other partials."),
10023
- flows: z24.record(
10024
- z24.object({
10025
- description: z24.string(),
10026
- chain: z24.array(z24.string())
10380
+ flows: z26.record(
10381
+ z26.object({
10382
+ description: z26.string(),
10383
+ chain: z26.array(z26.string())
10027
10384
  })
10028
10385
  ).describe("Partial flows whose entire chain is in this batch. Cross-batch flows are reconstructed in reduce.")
10029
10386
  },
@@ -10038,7 +10395,7 @@ function createMcpServer() {
10038
10395
  "reduce_snapshot",
10039
10396
  "Reduce step of the concept-map build. Returns every partial snapshot plus a system prompt asking you to merge them into one coherent project-wide map. Resolve platform variants into single product features, dedupe near-duplicates, and ensure no file is dropped. After producing the unified map, call `save_snapshot` to persist it (this also clears the partials).",
10040
10397
  {
10041
- dir: z24.string().describe("Absolute path to the project root directory")
10398
+ dir: z26.string().describe("Absolute path to the project root directory")
10042
10399
  },
10043
10400
  async ({ dir }) => {
10044
10401
  const result = await reduceSnapshot(dir);
@@ -10051,25 +10408,25 @@ function createMcpServer() {
10051
10408
  "save_snapshot",
10052
10409
  "Save a concept-to-files map as a persistent project snapshot. Maps feature names and data flows to the files that implement them. Persists across conversations \u2014 future sessions can call get_snapshot to instantly find relevant files. No API key needed \u2014 you are the LLM generating the map.",
10053
10410
  {
10054
- dir: z24.string().describe("Absolute path to the project root directory"),
10055
- features: z24.record(
10056
- z24.object({
10057
- description: z24.string().describe("One-line description of the feature"),
10058
- files: z24.array(z24.string()).describe("File paths that implement this feature"),
10059
- tests: z24.array(z24.string()).optional().describe("Test file paths for this feature"),
10060
- type: z24.enum(["capability", "infrastructure"]).optional().describe(
10411
+ dir: z26.string().describe("Absolute path to the project root directory"),
10412
+ features: z26.record(
10413
+ z26.object({
10414
+ description: z26.string().describe("One-line description of the feature"),
10415
+ files: z26.array(z26.string()).describe("File paths that implement this feature"),
10416
+ tests: z26.array(z26.string()).optional().describe("Test file paths for this feature"),
10417
+ type: z26.enum(["capability", "infrastructure"]).optional().describe(
10061
10418
  'Classification: "capability" for user-facing functionality, "infrastructure" for internal plumbing with no end user (DI/service wiring, config, logging, adapters). Capabilities are published to Confluence; infrastructure stays in the AI concept map only. Defaults to "capability".'
10062
10419
  )
10063
10420
  })
10064
10421
  ).describe("Map of feature names to their implementing files"),
10065
- flows: z24.record(
10066
- z24.object({
10067
- description: z24.string().describe("One-line description of the flow"),
10068
- chain: z24.array(z24.string()).describe("Ordered list of file paths showing data/call flow")
10422
+ flows: z26.record(
10423
+ z26.object({
10424
+ description: z26.string().describe("One-line description of the flow"),
10425
+ chain: z26.array(z26.string()).describe("Ordered list of file paths showing data/call flow")
10069
10426
  })
10070
10427
  ).describe("Map of flow names to ordered file chains"),
10071
- removeFeatures: z24.array(z24.string()).optional().describe("Feature names to delete from the existing map \u2014 for features that were renamed or no longer exist. Applied before merging; only meaningful on incremental saves."),
10072
- removeFlows: z24.array(z24.string()).optional().describe("Flow names to delete from the existing map. Applied before merging; only meaningful on incremental saves.")
10428
+ removeFeatures: z26.array(z26.string()).optional().describe("Feature names to delete from the existing map \u2014 for features that were renamed or no longer exist. Applied before merging; only meaningful on incremental saves."),
10429
+ removeFlows: z26.array(z26.string()).optional().describe("Flow names to delete from the existing map. Applied before merging; only meaningful on incremental saves.")
10073
10430
  },
10074
10431
  async ({ dir, features, flows, removeFeatures, removeFlows }) => {
10075
10432
  const result = await saveSnapshotData(
@@ -10086,19 +10443,19 @@ function createMcpServer() {
10086
10443
  );
10087
10444
  server.tool(
10088
10445
  "save_decision",
10089
- "Capture or revise a decision proposal with rationale, anchors, optional owner, sources, and a known actor. Compare matching get_context records and pending proposals before saving. When new evidence changes the same lesson's assumptions, scope, or recommended action, pass its existing id, including for accepted records. Preserve supported rationale and sources and replace obsolete instructions. Create a new record for a genuinely distinct lesson; skip unchanged restatements. No setup or map required. Writes a local record and preserves content history. Changes create a pending proposal while the last accepted revision remains operative; unchanged content does not re-verify or refresh it. Use review_decision for authorized acceptance or reaffirmation. A proposal cannot supersede a record with an operative accepted revision; review its replacement and retire the original separately.",
10446
+ "Capture or revise a decision proposal as soon as the lesson is supported by evidence, without waiting for implementation or merge. Keep unimplemented remedies tentative. Include rationale, anchors, optional owner, sources, and a known actor. Compare matching get_context records and pending proposals before saving. When new evidence changes the same lesson's assumptions, scope, or recommended action, pass its existing id, including for accepted records. Preserve supported rationale and sources and replace obsolete instructions. Create a new record for a genuinely distinct lesson; skip unchanged restatements. No setup or map required. Writes a local record and preserves content history. Changes create a pending proposal while the last accepted revision remains operative; unchanged content does not re-verify or refresh it. Use review_decision for authorized acceptance or reaffirmation. A proposal cannot supersede a record with an operative accepted revision; review its replacement and retire the original separately.",
10090
10447
  {
10091
- dir: z24.string().describe("Absolute path to the project root directory"),
10448
+ dir: z26.string().describe("Absolute path to the project root directory"),
10092
10449
  title: decisionTitleSchema.describe(`Short, specific headline (max ${TITLE_MAX_CHARS} characters). Generates a stable id on creation; revising the title keeps the existing id.`),
10093
10450
  body: decisionBodySchema.describe(`The knowledge itself: what was tried/decided, why, and what to avoid. Must contain information NOT derivable by reading the code. Aim for ${BODY_RECOMMENDED_CHARS} characters or fewer; up to ${BODY_MAX_CHARS} is accepted with a warning above the target. Matching context and hooks include the full body. Preserve exceptions; use sources for supporting references.`),
10094
- category: z24.enum(["decision", "gotcha", "deprecation", "convention"]),
10095
- files: z24.array(z24.string()).optional().describe("Repo-relative files or directory prefixes this applies to. Matching is shared by retrieval, hooks, review, and drift checking; changes flag the decision for re-verification."),
10096
- id: z24.string().optional().describe("Pass the matching record's existing id when evidence changes that lesson's assumptions, scope, or recommended action, including for accepted records. Changed content becomes a proposal while the accepted version remains operative; unchanged content leaves review and freshness untouched."),
10097
- supersedes: z24.string().optional().describe("Id of an unreviewed record or proposal with no accepted revision to replace. Operative accepted decisions require separate review and retirement."),
10451
+ category: z26.enum(["decision", "gotcha", "deprecation", "convention"]),
10452
+ files: z26.array(z26.string()).optional().describe("Repo-relative files or directory prefixes this applies to. Matching is shared by retrieval, hooks, review, and drift checking; changes flag the decision for re-verification."),
10453
+ id: z26.string().optional().describe("Pass the matching record's existing id when evidence changes that lesson's assumptions, scope, or recommended action, including for accepted records. Changed content becomes a proposal while the accepted version remains operative; unchanged content leaves review and freshness untouched."),
10454
+ supersedes: z26.string().optional().describe("Id of an unreviewed record or proposal with no accepted revision to replace. Operative accepted decisions require separate review and retirement."),
10098
10455
  owner: attributionSchema.shape.owner.describe("Responsible person or team, when known. Null clears it. Required for acceptance."),
10099
10456
  sources: attributionSchema.shape.sources.describe("Known PR, issue, incident, discussion, or document references. Omit to preserve; [] clears. At least one is required for acceptance."),
10100
10457
  actor: attributionSchema.shape.actor.describe("Known person or agent recording this revision. Omit if unknown; do not infer from Git identity."),
10101
- force: z24.boolean().optional().describe("Save even when a near-duplicate was detected")
10458
+ force: z26.boolean().optional().describe("Save even when a near-duplicate was detected")
10102
10459
  },
10103
10460
  async ({ dir, title, body, category, files, id, supersedes, force, owner, sources, actor }) => {
10104
10461
  const result = await saveDecision(dir, {
@@ -10120,12 +10477,12 @@ function createMcpServer() {
10120
10477
  "review_decision",
10121
10478
  "Prepare a decision review: returns the full record and history, any operative accepted revision, provenance, changes and previews for both sets of anchors, and a reviewToken. Then record accept, reaffirm, or retire with that token, the authorized reviewer, and a reason. Acceptance replaces the operative revision; retirement withdraws the entire record including its proposal. Acceptance requires owner, source, readable Git HEAD, and committed anchor changes. Changed records or code invalidate the token. Identities and approvals are recorded assertions for normal PR review, not authenticated proof.",
10122
10479
  {
10123
- dir: z24.string().describe("Absolute path to the project root directory"),
10124
- id: z24.string().regex(/^[a-zA-Z0-9_-]+$/).describe("Decision id from get_context or save_decision"),
10125
- action: z24.enum(["prepare", "accept", "reaffirm", "retire"]).optional().default("prepare").describe("Prepare is read-only. Other actions record an explicitly authorized review."),
10126
- reviewer: z24.string().trim().min(1).max(200).optional().describe("Identity of the actual reviewer; required for a verdict. Never invent one."),
10127
- note: z24.string().trim().min(1).max(1500).optional().describe("Review rationale; required for a verdict. Cite evidence for the decision."),
10128
- reviewToken: z24.string().regex(/^[a-f0-9]{64}$/).optional().describe("Token from the prepared review; rejects stale record or code revisions")
10480
+ dir: z26.string().describe("Absolute path to the project root directory"),
10481
+ id: z26.string().regex(/^[a-zA-Z0-9_-]+$/).describe("Decision id from get_context or save_decision"),
10482
+ action: z26.enum(["prepare", "accept", "reaffirm", "retire"]).optional().default("prepare").describe("Prepare is read-only. Other actions record an explicitly authorized review."),
10483
+ reviewer: z26.string().trim().min(1).max(200).optional().describe("Identity of the actual reviewer; required for a verdict. Never invent one."),
10484
+ note: z26.string().trim().min(1).max(1500).optional().describe("Review rationale; required for a verdict. Cite evidence for the decision."),
10485
+ reviewToken: z26.string().regex(/^[a-f0-9]{64}$/).optional().describe("Token from the prepared review; rejects stale record or code revisions")
10129
10486
  },
10130
10487
  async ({ dir, ...input }) => {
10131
10488
  const result = await reviewDecision2(dir, input);
@@ -10136,7 +10493,7 @@ function createMcpServer() {
10136
10493
  "mason_check_drift",
10137
10494
  "Check how far the concept map has drifted from HEAD. Deterministic (git + filesystem, no LLM). Returns which features/flows are stale and the changed files behind them, new source files not yet mapped, ghost files (mapped but deleted), renames, and a `recommendation`: `up-to-date` (nothing to do), `incremental` (update just the stale entries via save_snapshot), or `full-rebuild` (re-run the Map-Reduce build). Call this before trusting the map in a long session, or periodically to keep the map and any synced wikis fresh.",
10138
10495
  {
10139
- dir: z24.string().describe("Absolute path to the project root directory")
10496
+ dir: z26.string().describe("Absolute path to the project root directory")
10140
10497
  },
10141
10498
  async ({ dir }) => {
10142
10499
  const result = await checkDrift(dir);
@@ -10149,8 +10506,8 @@ function createMcpServer() {
10149
10506
  "verify_snapshot",
10150
10507
  "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.",
10151
10508
  {
10152
- dir: z24.string().describe("Absolute path to the project root directory"),
10153
- sample: z24.number().int().optional().describe("Entries to sample (default 5)")
10509
+ dir: z26.string().describe("Absolute path to the project root directory"),
10510
+ sample: z26.number().int().optional().describe("Entries to sample (default 5)")
10154
10511
  },
10155
10512
  async ({ dir, sample }) => {
10156
10513
  const result = await verifySnapshot(dir, sample);
@@ -10161,13 +10518,13 @@ function createMcpServer() {
10161
10518
  "save_verification",
10162
10519
  "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.",
10163
10520
  {
10164
- dir: z24.string().describe("Absolute path to the project root directory"),
10165
- verdicts: z24.record(
10166
- z24.object({
10167
- kind: z24.enum(["feature", "flow"]).optional().describe("Entry kind returned by verify_snapshot; required to record a verdict"),
10168
- reviewToken: z24.string().optional().describe("Copy the reviewToken from verify_snapshot after inspecting its evidence; required to record a verdict"),
10169
- ok: z24.boolean(),
10170
- note: z24.string().optional().describe("One line on what's wrong \u2014 required when ok is false")
10521
+ dir: z26.string().describe("Absolute path to the project root directory"),
10522
+ verdicts: z26.record(
10523
+ z26.object({
10524
+ kind: z26.enum(["feature", "flow"]).optional().describe("Entry kind returned by verify_snapshot; required to record a verdict"),
10525
+ reviewToken: z26.string().optional().describe("Copy the reviewToken from verify_snapshot after inspecting its evidence; required to record a verdict"),
10526
+ ok: z26.boolean(),
10527
+ note: z26.string().optional().describe("One line on what's wrong \u2014 required when ok is false")
10171
10528
  })
10172
10529
  ).describe("Entry name \u2192 verdict, exactly as returned by verify_snapshot")
10173
10530
  },
@@ -10180,8 +10537,8 @@ function createMcpServer() {
10180
10537
  "get_impact",
10181
10538
  "Trace historical co-change partners, related tests, and references with evidence: resolved imports, explicit paths, or uncertain name candidates. Candidates are not proven dependencies. Deterministic and read-only; no initialization or concept map required.",
10182
10539
  {
10183
- dir: z24.string().describe("Absolute path to the project root directory"),
10184
- files: z24.array(z24.string()).describe("File paths or names to analyze (e.g., ['WeatherRepository.kt'] or ['src/services/auth.ts'])")
10540
+ dir: z26.string().describe("Absolute path to the project root directory"),
10541
+ files: z26.array(z26.string()).describe("File paths or names to analyze (e.g., ['WeatherRepository.kt'] or ['src/services/auth.ts'])")
10185
10542
  },
10186
10543
  async ({ dir, files }) => {
10187
10544
  const result = await getImpact(dir, files);
@@ -10194,12 +10551,12 @@ function createMcpServer() {
10194
10551
  "export_to_confluence",
10195
10552
  "Sync the project's concept map to Confluence as product-readable wiki pages: an index page, one page per feature (PM-language descriptions, no file paths), and a changelog page. Mason replaces managed page bodies; manual edits to those bodies are overwritten. Requires `mason_set_confluence` to have been called first.",
10196
10553
  {
10197
- dir: z24.string().describe("Absolute path to the project root directory"),
10198
- spaceKey: z24.string().optional().describe("Override the configured space key"),
10199
- parentPageId: z24.string().optional().describe("Override the configured parent page ID"),
10200
- indexPageTitle: z24.string().optional().describe("Title of the index page (default: 'Mason \u2014 System Map')"),
10201
- changelogPageTitle: z24.string().optional().describe("Title of the changelog page (default: 'Mason \u2014 Changelog')"),
10202
- featurePagePrefix: z24.string().optional().describe("Prefix for each feature page title (default: 'Feature: ')")
10554
+ dir: z26.string().describe("Absolute path to the project root directory"),
10555
+ spaceKey: z26.string().optional().describe("Override the configured space key"),
10556
+ parentPageId: z26.string().optional().describe("Override the configured parent page ID"),
10557
+ indexPageTitle: z26.string().optional().describe("Title of the index page (default: 'Mason \u2014 System Map')"),
10558
+ changelogPageTitle: z26.string().optional().describe("Title of the changelog page (default: 'Mason \u2014 Changelog')"),
10559
+ featurePagePrefix: z26.string().optional().describe("Prefix for each feature page title (default: 'Feature: ')")
10203
10560
  },
10204
10561
  async ({ dir, spaceKey, parentPageId, indexPageTitle, changelogPageTitle, featurePagePrefix }) => {
10205
10562
  const result = await exportToConfluenceTool(dir, {
@@ -10224,32 +10581,32 @@ async function startMcpServer(onClose) {
10224
10581
  // src/distribution/updates.ts
10225
10582
  init_store();
10226
10583
  init_storage();
10227
- import fs33 from "fs/promises";
10584
+ import fs34 from "fs/promises";
10228
10585
  import path42 from "path";
10229
10586
  import { spawn as spawn3 } from "child_process";
10230
10587
  import { randomUUID as randomUUID11 } from "crypto";
10231
- import { z as z30 } from "zod";
10588
+ import { z as z32 } from "zod";
10232
10589
 
10233
10590
  // src/distribution/state.ts
10234
10591
  init_storage();
10235
10592
  import os5 from "os";
10236
10593
  import path38 from "path";
10237
10594
  import { fileURLToPath } from "url";
10238
- import fs28 from "fs/promises";
10239
- import { z as z26 } from "zod";
10595
+ import fs29 from "fs/promises";
10596
+ import { z as z28 } from "zod";
10240
10597
 
10241
10598
  // src/distribution/path.ts
10242
- import fs27 from "fs/promises";
10599
+ import fs28 from "fs/promises";
10243
10600
  import os4 from "os";
10244
10601
  import path37 from "path";
10245
10602
  import { execFile as execFile12 } from "child_process";
10246
10603
  import { promisify as promisify12 } from "util";
10247
10604
  import { createHash as createHash7, randomUUID as randomUUID7 } from "crypto";
10248
- import { z as z25 } from "zod";
10605
+ import { z as z27 } from "zod";
10249
10606
  var execute2 = promisify12(execFile12);
10250
- var pathChangeSchema = z25.discriminatedUnion("kind", [
10251
- z25.object({ kind: z25.literal("profile"), file: z25.string(), block: z25.string(), created: z25.boolean() }),
10252
- z25.object({ kind: z25.literal("windows"), entry: z25.string(), created: z25.boolean() })
10607
+ var pathChangeSchema = z27.discriminatedUnion("kind", [
10608
+ z27.object({ kind: z27.literal("profile"), file: z27.string(), block: z27.string(), created: z27.boolean() }),
10609
+ z27.object({ kind: z27.literal("windows"), entry: z27.string(), created: z27.boolean() })
10253
10610
  ]);
10254
10611
  var quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
10255
10612
  async function configurePath(bin, previous, save, options = {}) {
@@ -10286,7 +10643,7 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
10286
10643
  else {
10287
10644
  const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path37.join(userHome, name));
10288
10645
  let login = candidates[2];
10289
- for (const file of candidates) if (await fs27.lstat(file).then(() => true, (error) => {
10646
+ for (const file of candidates) if (await fs28.lstat(file).then(() => true, (error) => {
10290
10647
  if (error.code === "ENOENT") return false;
10291
10648
  throw error;
10292
10649
  })) {
@@ -10297,12 +10654,12 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
10297
10654
  }
10298
10655
  const marker = `# >>> mason PATH ${createHash7("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
10299
10656
  for (const profile of profiles2) {
10300
- const stat = await fs27.lstat(profile).catch((error) => {
10657
+ const stat = await fs28.lstat(profile).catch((error) => {
10301
10658
  if (error.code === "ENOENT") return null;
10302
10659
  throw error;
10303
10660
  });
10304
- const file = stat?.isSymbolicLink() ? await fs27.realpath(profile) : path37.resolve(profile);
10305
- const original = await fs27.readFile(file).catch((error) => {
10661
+ const file = stat?.isSymbolicLink() ? await fs28.realpath(profile) : path37.resolve(profile);
10662
+ const original = await fs28.readFile(file).catch((error) => {
10306
10663
  if (error.code === "ENOENT" && !stat) return null;
10307
10664
  throw error;
10308
10665
  });
@@ -10317,8 +10674,8 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
10317
10674
  changes.push({ kind: "profile", file, block, created: original === null });
10318
10675
  await save(changes);
10319
10676
  }
10320
- await fs27.mkdir(path37.dirname(file), { recursive: true });
10321
- const handle = await fs27.open(file, original === null ? "ax+" : "a+", 384);
10677
+ await fs28.mkdir(path37.dirname(file), { recursive: true });
10678
+ const handle = await fs28.open(file, original === null ? "ax+" : "a+", 384);
10322
10679
  try {
10323
10680
  if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0))) throw new Error(`Shell profile changed during installation: ${profile}`);
10324
10681
  await handle.writeFile(block);
@@ -10369,32 +10726,32 @@ try {
10369
10726
  }
10370
10727
 
10371
10728
  // src/distribution/state.ts
10372
- var releaseVersion = z26.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/);
10373
- var bundleId = z26.string().regex(/^[a-f0-9]{24}$/);
10374
- var updatePolicySchema = z26.object({
10375
- enabled: z26.boolean(),
10376
- revision: z26.string(),
10729
+ var releaseVersion = z28.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/);
10730
+ var bundleId = z28.string().regex(/^[a-f0-9]{24}$/);
10731
+ var updatePolicySchema = z28.object({
10732
+ enabled: z28.boolean(),
10733
+ revision: z28.string(),
10377
10734
  pinnedVersion: releaseVersion.optional(),
10378
- mirror: z26.string().optional()
10735
+ mirror: z28.string().optional()
10379
10736
  });
10380
- var recordSchema3 = z26.object({
10381
- format: z26.literal(1),
10382
- home: z26.string(),
10383
- bin: z26.string(),
10737
+ var recordSchema3 = z28.object({
10738
+ format: z28.literal(1),
10739
+ home: z28.string(),
10740
+ bin: z28.string(),
10384
10741
  current: bundleId,
10385
10742
  version: releaseVersion,
10386
- launchers: z26.record(z26.string()),
10387
- previousLaunchers: z26.record(z26.string()).optional(),
10388
- pathChanges: z26.array(pathChangeSchema).optional(),
10743
+ launchers: z28.record(z28.string()),
10744
+ previousLaunchers: z28.record(z28.string()).optional(),
10745
+ pathChanges: z28.array(pathChangeSchema).optional(),
10389
10746
  updates: updatePolicySchema.optional(),
10390
- previous: z26.object({ id: bundleId, version: releaseVersion }).optional(),
10391
- pending: z26.object({ id: bundleId, version: releaseVersion, policyRevision: z26.string() }).optional()
10747
+ previous: z28.object({ id: bundleId, version: releaseVersion }).optional(),
10748
+ pending: z28.object({ id: bundleId, version: releaseVersion, policyRevision: z28.string() }).optional()
10392
10749
  });
10393
10750
  async function readInstallation(home) {
10394
10751
  const raw = await readStoreJson(home, "install.json");
10395
10752
  if (!raw) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
10396
10753
  const record = recordSchema3.parse(raw);
10397
- if (record.home !== await fs28.realpath(home)) throw new Error("Invalid standalone installation receipt.");
10754
+ if (record.home !== await fs29.realpath(home)) throw new Error("Invalid standalone installation receipt.");
10398
10755
  return record;
10399
10756
  }
10400
10757
  function standaloneLocation() {
@@ -10426,22 +10783,22 @@ function newerStableVersion(candidate, installed) {
10426
10783
 
10427
10784
  // src/distribution/bundle.ts
10428
10785
  init_storage();
10429
- import fs29 from "fs/promises";
10786
+ import fs30 from "fs/promises";
10430
10787
  import path39 from "path";
10431
10788
  import { createHash as createHash8 } from "crypto";
10432
- import { z as z27 } from "zod";
10789
+ import { z as z29 } from "zod";
10433
10790
  var targets = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
10434
- var digest2 = z27.string().regex(/^[a-f0-9]{64}$/);
10435
- var bundleSchema = z27.object({
10436
- format: z27.literal(1),
10437
- version: z27.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
10438
- target: z27.enum(targets),
10439
- nodeVersion: z27.string(),
10440
- files: z27.record(digest2)
10791
+ var digest2 = z29.string().regex(/^[a-f0-9]{64}$/);
10792
+ var bundleSchema = z29.object({
10793
+ format: z29.literal(1),
10794
+ version: z29.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
10795
+ target: z29.enum(targets),
10796
+ nodeVersion: z29.string(),
10797
+ files: z29.record(digest2)
10441
10798
  }).strict();
10442
10799
  var sha256 = (bytes) => createHash8("sha256").update(bytes).digest("hex");
10443
10800
  async function readBundle(root) {
10444
- const bytes = await fs29.readFile(await storePath(root, "bundle.json"));
10801
+ const bytes = await fs30.readFile(await storePath(root, "bundle.json"));
10445
10802
  const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
10446
10803
  const windows = manifest.target.startsWith("win32-");
10447
10804
  for (const file of Object.keys(manifest.files)) {
@@ -10449,7 +10806,7 @@ async function readBundle(root) {
10449
10806
  throw new Error("Unsafe path in Mason bundle: " + file);
10450
10807
  }
10451
10808
  }
10452
- 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"]) {
10809
+ for (const required of [windows ? "node.exe" : "node", "app/package.json", "app/dist/mason.js", "app/dist/mason-mcp.js"]) {
10453
10810
  if (!manifest.files[required]) throw new Error("Incomplete Mason bundle: " + required);
10454
10811
  }
10455
10812
  return { manifest, manifestHash: sha256(bytes) };
@@ -10460,7 +10817,7 @@ async function verifyBundle(root, expectedHash) {
10460
10817
  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.");
10461
10818
  const files = Object.keys(bundle.manifest.files);
10462
10819
  for (let i = 0; i < files.length; i += 8) await Promise.all(files.slice(i, i + 8).map(async (file) => {
10463
- if (sha256(await fs29.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
10820
+ if (sha256(await fs30.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
10464
10821
  }));
10465
10822
  return bundle;
10466
10823
  }
@@ -10468,16 +10825,21 @@ async function verifyBundle(root, expectedHash) {
10468
10825
  // src/distribution/runtime.ts
10469
10826
  init_store();
10470
10827
  init_storage();
10471
- import fs30 from "fs/promises";
10828
+ import fs31 from "fs/promises";
10472
10829
  import { unlinkSync } from "fs";
10473
10830
  import os6 from "os";
10474
10831
  import path40 from "path";
10475
10832
  import { randomUUID as randomUUID8 } from "crypto";
10476
- import { z as z28 } from "zod";
10477
- var leaseSchema = z28.object({ pid: z28.number().int().positive(), host: z28.string(), bundle: z28.string().regex(/^[a-f0-9]{24}$/) });
10833
+ import { z as z30 } from "zod";
10834
+
10835
+ // src/distribution/trust.ts
10836
+ import tls from "tls";
10837
+
10838
+ // src/distribution/runtime.ts
10839
+ var leaseSchema = z30.object({ pid: z30.number().int().positive(), host: z30.string(), bundle: z30.string().regex(/^[a-f0-9]{24}$/) });
10478
10840
 
10479
10841
  // src/distribution/install.ts
10480
- import fs32 from "fs/promises";
10842
+ import fs33 from "fs/promises";
10481
10843
  import os8 from "os";
10482
10844
  import path41 from "path";
10483
10845
  import { execFile as execFile13, spawn as spawn2 } from "child_process";
@@ -10488,14 +10850,14 @@ init_store();
10488
10850
 
10489
10851
  // src/distribution/sessions.ts
10490
10852
  init_storage();
10491
- import fs31 from "fs/promises";
10853
+ import fs32 from "fs/promises";
10492
10854
  import os7 from "os";
10493
10855
  import { randomUUID as randomUUID9 } from "crypto";
10494
- import { z as z29 } from "zod";
10495
- var leaseSchema2 = z29.object({ pid: z29.number().int().positive(), host: z29.string() });
10856
+ import { z as z31 } from "zod";
10857
+ var leaseSchema2 = z31.object({ pid: z31.number().int().positive(), host: z31.string() });
10496
10858
  async function hasRunningMcp(home) {
10497
- const directory = await storePath(home, "sessions");
10498
- const files = await fs31.readdir(directory).catch((error) => {
10859
+ const directory2 = await storePath(home, "sessions");
10860
+ const files = await fs32.readdir(directory2).catch((error) => {
10499
10861
  if (error.code === "ENOENT") return [];
10500
10862
  throw error;
10501
10863
  });
@@ -10513,7 +10875,7 @@ async function hasRunningMcp(home) {
10513
10875
  } catch (error) {
10514
10876
  if (error.code !== "ESRCH") return true;
10515
10877
  }
10516
- await fs31.rm(await storePath(home, relative), { force: true });
10878
+ await fs32.rm(await storePath(home, relative), { force: true });
10517
10879
  } catch {
10518
10880
  return true;
10519
10881
  }
@@ -10524,7 +10886,7 @@ async function registerMcp(home) {
10524
10886
  const name = `sessions/${randomUUID9()}.json`;
10525
10887
  await writeStoreJson(home, name, { pid: process.pid, host: os7.hostname() });
10526
10888
  return async () => {
10527
- await fs31.rm(await storePath(home, name), { force: true }).catch(() => {
10889
+ await fs32.rm(await storePath(home, name), { force: true }).catch(() => {
10528
10890
  });
10529
10891
  };
10530
10892
  }
@@ -10537,10 +10899,10 @@ var launcherNames = ["mason", "mason.cmd", "mason.ps1", "mason-launcher.ps1"];
10537
10899
  async function replace(file, content2, mode = 384) {
10538
10900
  const temp = file + ".tmp-" + randomUUID10();
10539
10901
  try {
10540
- await fs32.writeFile(temp, content2, { mode, flag: "wx" });
10541
- await fs32.rename(temp, file);
10902
+ await fs33.writeFile(temp, content2, { mode, flag: "wx" });
10903
+ await fs33.rename(temp, file);
10542
10904
  } finally {
10543
- await fs32.rm(temp, { force: true });
10905
+ await fs33.rm(temp, { force: true });
10544
10906
  }
10545
10907
  }
10546
10908
  function assertUpdatePolicy(record, version, revision) {
@@ -10552,17 +10914,17 @@ async function installLocked2(home, source, bundle, progress, options, alreadySt
10552
10914
  const raw = await readStoreJson(home, "install.json");
10553
10915
  const previous = raw === null ? null : recordSchema3.parse(raw);
10554
10916
  if (options.stageOnly) assertUpdatePolicy(previous, bundle.manifest.version, options.policyRevision);
10555
- if (!previous) for (const file of await fs32.readdir(home)) {
10917
+ if (!previous) for (const file of await fs33.readdir(home)) {
10556
10918
  if (file === ".install-lock") continue;
10557
- const directory = path41.join(home, file);
10558
- const stat = await fs32.lstat(directory);
10559
- if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs32.readdir(directory)).length) {
10919
+ const directory2 = path41.join(home, file);
10920
+ const stat = await fs33.lstat(directory2);
10921
+ if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs33.readdir(directory2)).length) {
10560
10922
  throw new Error("Mason installation directory is not empty and has no ownership receipt.");
10561
10923
  }
10562
10924
  }
10563
10925
  const selectedBin = path41.resolve(process.env.MASON_BIN_DIR ?? previous?.bin ?? (process.platform === "win32" ? path41.join(home, "bin") : path41.join(os8.homedir(), ".local/bin")));
10564
- await fs32.mkdir(selectedBin, { recursive: true });
10565
- const bin = await fs32.realpath(selectedBin);
10926
+ await fs33.mkdir(selectedBin, { recursive: true });
10927
+ const bin = await fs33.realpath(selectedBin);
10566
10928
  if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
10567
10929
  const id = bundle.manifestHash.slice(0, 24);
10568
10930
  const destination = await storePath(home, "versions/" + id);
@@ -10594,7 +10956,7 @@ exec ${shQuote(path41.join(destination, "node"))} ${shQuote(path41.join(destinat
10594
10956
  ` };
10595
10957
  for (const [name, content2] of Object.entries(launchers)) {
10596
10958
  const file = await storePath(bin, name);
10597
- const old = await fs32.readFile(file, "utf8").catch((error) => {
10959
+ const old = await fs33.readFile(file, "utf8").catch((error) => {
10598
10960
  if (error.code === "ENOENT") return null;
10599
10961
  throw error;
10600
10962
  });
@@ -10603,7 +10965,7 @@ exec ${shQuote(path41.join(destination, "node"))} ${shQuote(path41.join(destinat
10603
10965
  const retired = Object.entries({ ...previous?.previousLaunchers, ...previous?.launchers }).filter(([name]) => !(name in launchers));
10604
10966
  const checkRetired = async (name, expected) => {
10605
10967
  if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
10606
- const actual = await fs32.readFile(await storePath(bin, name), "utf8").catch((error) => {
10968
+ const actual = await fs33.readFile(await storePath(bin, name), "utf8").catch((error) => {
10607
10969
  if (error.code === "ENOENT") return null;
10608
10970
  throw error;
10609
10971
  });
@@ -10638,7 +11000,7 @@ exec ${shQuote(path41.join(destination, "node"))} ${shQuote(path41.join(destinat
10638
11000
  for (const [name, content2] of Object.entries(launchers)) await replace(await storePath(bin, name, true), content2, 493);
10639
11001
  for (const [name, expected] of retired) {
10640
11002
  await checkRetired(name, expected);
10641
- await fs32.rm(await storePath(bin, name), { force: true });
11003
+ await fs33.rm(await storePath(bin, name), { force: true });
10642
11004
  }
10643
11005
  await writeStoreJson(home, "install.json", receipt);
10644
11006
  } catch (error) {
@@ -10657,7 +11019,7 @@ exec ${shQuote(path41.join(destination, "node"))} ${shQuote(path41.join(destinat
10657
11019
  return { home, bin, version: bundle.manifest.version, previousVersion: previous?.version, path: pathResult };
10658
11020
  }
10659
11021
  async function copyBundle(home, source, destination, bundle) {
10660
- const exists = await fs32.lstat(destination).then(() => true, (error) => {
11022
+ const exists = await fs33.lstat(destination).then(() => true, (error) => {
10661
11023
  if (error.code === "ENOENT") return false;
10662
11024
  throw error;
10663
11025
  });
@@ -10666,16 +11028,16 @@ async function copyBundle(home, source, destination, bundle) {
10666
11028
  return;
10667
11029
  }
10668
11030
  const stage = await storePath(home, "versions/.install-" + randomUUID10(), true);
10669
- await fs32.mkdir(stage);
11031
+ await fs33.mkdir(stage);
10670
11032
  try {
10671
11033
  for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
10672
11034
  const from = await storePath(source, file), to = await storePath(stage, file, true);
10673
- await fs32.copyFile(from, to);
10674
- await fs32.chmod(to, (await fs32.stat(from)).mode & 511);
11035
+ await fs33.copyFile(from, to);
11036
+ await fs33.chmod(to, (await fs33.stat(from)).mode & 511);
10675
11037
  }
10676
11038
  await verifyBundle(stage, bundle.manifestHash);
10677
11039
  try {
10678
- await fs32.rename(stage, destination);
11040
+ await fs33.rename(stage, destination);
10679
11041
  } catch (error) {
10680
11042
  try {
10681
11043
  await verifyBundle(destination, bundle.manifestHash);
@@ -10684,7 +11046,7 @@ async function copyBundle(home, source, destination, bundle) {
10684
11046
  }
10685
11047
  }
10686
11048
  } finally {
10687
- await fs32.rm(stage, { recursive: true, force: true });
11049
+ await fs33.rm(stage, { recursive: true, force: true });
10688
11050
  }
10689
11051
  }
10690
11052
  async function prepareStandaloneMcp(home) {
@@ -10722,14 +11084,14 @@ async function prepareStandaloneMcp(home) {
10722
11084
  // src/distribution/updates.ts
10723
11085
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
10724
11086
  var RELEASE_AGE_MS = 24 * 60 * 60 * 1e3;
10725
- var releaseListSchema = z30.array(z30.object({ tag_name: z30.string(), draft: z30.boolean(), prerelease: z30.boolean(), published_at: z30.string().nullable() }));
10726
- var updateManifestSchema = z30.object({
10727
- format: z30.literal(1),
10728
- version: z30.string().regex(/^\d+\.\d+\.\d+$/),
10729
- publishedAt: z30.string().datetime(),
10730
- assets: z30.record(z30.string().regex(/^[a-f0-9]{64}$/))
11087
+ var releaseListSchema = z32.array(z32.object({ tag_name: z32.string(), draft: z32.boolean(), prerelease: z32.boolean(), published_at: z32.string().nullable() }));
11088
+ var updateManifestSchema = z32.object({
11089
+ format: z32.literal(1),
11090
+ version: z32.string().regex(/^\d+\.\d+\.\d+$/),
11091
+ publishedAt: z32.string().datetime(),
11092
+ assets: z32.record(z32.string().regex(/^[a-f0-9]{64}$/))
10731
11093
  }).strict();
10732
- var statusSchema2 = z30.object({ lastAttempt: z30.number(), availableVersion: z30.string().optional(), error: z30.string().optional() });
11094
+ var statusSchema2 = z32.object({ lastAttempt: z32.number(), availableVersion: z32.string().optional(), error: z32.string().optional() });
10733
11095
  async function scheduleAutomaticUpdate() {
10734
11096
  try {
10735
11097
  const { home, record } = await currentInstallation();