mason-context 0.19.2 → 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);
@@ -3304,8 +3310,8 @@ function parseState(raw) {
3304
3310
  throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
3305
3311
  }
3306
3312
  }
3307
- function withLock(root, directory, run, waitMs = 5e3) {
3308
- return withStoreLock(root, directory, run, waitMs, "Automation");
3313
+ function withLock(root, directory2, run, waitMs = 5e3) {
3314
+ return withStoreLock(root, directory2, run, waitMs, "Automation");
3309
3315
  }
3310
3316
  var hostSchema, events, stateSchema;
3311
3317
  var init_store = __esm({
@@ -4129,7 +4135,7 @@ var init_evidence = __esm({
4129
4135
  init_dead_command();
4130
4136
  init_storage();
4131
4137
  init_advisory_review();
4132
- engineVersion = true ? "0.19.2" : "development";
4138
+ engineVersion = true ? "0.20.1" : "development";
4133
4139
  hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
4134
4140
  content = readAuditInput;
4135
4141
  cacheSchema = z9.object({ version: z9.literal(1), entries: z9.record(z9.object({ key: z9.string(), result: checkResultSchema })), digest: z9.string() });
@@ -4182,8 +4188,8 @@ async function effectiveSetup(root) {
4182
4188
  if (inactive && setup) for (const host of inactiveHostsSchema.parse(inactive).hosts) delete setup.hosts[host];
4183
4189
  return { root: inheritedRoot, setup };
4184
4190
  }
4185
- async function loadSetupReceipt(root, directory, host) {
4186
- 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");
4187
4193
  if (raw === null) return null;
4188
4194
  const receipt = receiptSchema.parse(raw);
4189
4195
  if (receipt.root !== root || receipt.host !== host) throw new Error("Setup receipt belongs to another installation.");
@@ -4255,7 +4261,7 @@ var init_launcher = __esm({
4255
4261
  "use strict";
4256
4262
  init_evidence();
4257
4263
  init_model();
4258
- executingVersion = () => true ? "0.19.2" : "development";
4264
+ executingVersion = () => true ? "0.20.1" : "development";
4259
4265
  mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
4260
4266
  hookCommand = (host) => `mason --setup-host ${host} auto`;
4261
4267
  }
@@ -4439,6 +4445,146 @@ var init_files2 = __esm({
4439
4445
  }
4440
4446
  });
4441
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
+
4442
4588
  // src/automation/execution.ts
4443
4589
  var execution_exports = {};
4444
4590
  __export(execution_exports, {
@@ -4450,9 +4596,9 @@ __export(execution_exports, {
4450
4596
  recordExecution: () => recordExecution
4451
4597
  });
4452
4598
  import os2 from "os";
4453
- import fs19 from "fs/promises";
4599
+ import fs20 from "fs/promises";
4454
4600
  import { randomUUID as randomUUID4 } from "crypto";
4455
- import { z as z13 } from "zod";
4601
+ import { z as z14 } from "zod";
4456
4602
  function parseExecution(raw) {
4457
4603
  try {
4458
4604
  return executionSchema.parse(raw);
@@ -4470,15 +4616,15 @@ function automationFailure(error) {
4470
4616
  codes.add(String(cause.code));
4471
4617
  cause = cause.cause;
4472
4618
  }
4473
- 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 z13.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";
4474
4620
  return { code, message: message2, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
4475
4621
  }
4476
4622
  function failureMessage(error) {
4477
4623
  const failure = automationFailure(error);
4478
4624
  return `Mason automation unavailable [${failure.code}]; evidence capture/verification was not established. ${failure.message}` + (failure.receiptRecorded ? "" : " No durable failure receipt was recorded.");
4479
4625
  }
4480
- async function failureNotifications(root, directory) {
4481
- const raw = await readStoreJson(root, directory + "/notifications.json");
4626
+ async function failureNotifications(root, directory2) {
4627
+ const raw = await readStoreJson(root, directory2 + "/notifications.json");
4482
4628
  return raw === null ? { version: 1, sessions: {} } : notificationsSchema.parse(raw);
4483
4629
  }
4484
4630
  async function hookFailureMessage(dir, host, sessionId, error) {
@@ -4508,12 +4654,12 @@ async function hookFailureMessage(dir, host, sessionId, error) {
4508
4654
  return failureMessage(error);
4509
4655
  }
4510
4656
  }
4511
- async function refreshRunning(root, directory, attempts) {
4657
+ async function refreshRunning(root, directory2, attempts) {
4512
4658
  for (const attempt of attempts) {
4513
4659
  if (attempt.status !== "running") continue;
4514
4660
  let alive = false;
4515
4661
  try {
4516
- 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");
4517
4663
  if (attempt.host === os2.hostname() && owner?.pid === attempt.pid && owner.host === attempt.host) {
4518
4664
  try {
4519
4665
  process.kill(attempt.pid, 0);
@@ -4527,12 +4673,12 @@ async function refreshRunning(root, directory, attempts) {
4527
4673
  if (!alive) attempt.status = "unknown";
4528
4674
  }
4529
4675
  }
4530
- async function editExecution(root, directory, edit) {
4531
- return withLock(root, directory + "/execution-lock", async () => {
4532
- 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";
4533
4679
  const raw = await readStoreJson(root, file);
4534
4680
  const log = raw === null ? { version: 1, attempts: [], discardedAttempts: 0 } : parseExecution(raw);
4535
- await refreshRunning(root, directory, log.attempts);
4681
+ await refreshRunning(root, directory2, log.attempts);
4536
4682
  edit(log);
4537
4683
  const finished = log.attempts.filter((a) => a.status !== "running");
4538
4684
  const discard = new Set(finished.slice(0, Math.max(0, finished.length - 32)).map((a) => a.id));
@@ -4541,7 +4687,7 @@ async function editExecution(root, directory, edit) {
4541
4687
  await writeStoreJson(root, file, log);
4542
4688
  });
4543
4689
  }
4544
- async function recordExecution(root, directory, event, run) {
4690
+ async function recordExecution(root, directory2, event, run) {
4545
4691
  const started = performance.now();
4546
4692
  const attempt = {
4547
4693
  id: randomUUID4(),
@@ -4552,15 +4698,15 @@ async function recordExecution(root, directory, event, run) {
4552
4698
  status: "running",
4553
4699
  lease: true
4554
4700
  };
4555
- const owner = leasePath(directory, attempt.id);
4556
- const finish = async (changes) => editExecution(root, directory, (log) => {
4701
+ const owner = leasePath(directory2, attempt.id);
4702
+ const finish = async (changes) => editExecution(root, directory2, (log) => {
4557
4703
  const current = log.attempts.find((a) => a.id === attempt.id);
4558
4704
  if (!current) throw new Error("Automation execution receipt disappeared; completion could not be recorded.");
4559
4705
  Object.assign(current, changes, { finishedAt: (/* @__PURE__ */ new Date()).toISOString(), durationMs: performance.now() - started });
4560
4706
  });
4561
4707
  await writeStoreJson(root, owner, { pid: attempt.pid, host: attempt.host });
4562
4708
  try {
4563
- await editExecution(root, directory, (log) => {
4709
+ await editExecution(root, directory2, (log) => {
4564
4710
  if (log.attempts.filter((a) => a.status === "running").length >= 128) throw new Error("Too many active automation executions; existing receipts were retained.");
4565
4711
  log.attempts.push(attempt);
4566
4712
  });
@@ -4578,15 +4724,15 @@ async function recordExecution(root, directory, event, run) {
4578
4724
  throw Object.assign(new Error(failure.message, { cause: error }), { failure });
4579
4725
  }
4580
4726
  } finally {
4581
- await fs19.rm(await storePath(root, owner), { force: true }).catch(() => {
4727
+ await fs20.rm(await storePath(root, owner), { force: true }).catch(() => {
4582
4728
  });
4583
4729
  }
4584
4730
  }
4585
- async function executionStatus(root, directory) {
4586
- const raw = await readStoreJson(root, directory + "/execution.json");
4731
+ async function executionStatus(root, directory2) {
4732
+ const raw = await readStoreJson(root, directory2 + "/execution.json");
4587
4733
  if (raw === null) return { status: "not-observed", attempts: [] };
4588
4734
  const log = parseExecution(raw);
4589
- await refreshRunning(root, directory, log.attempts);
4735
+ await refreshRunning(root, directory2, log.attempts);
4590
4736
  const completed = [...log.attempts].reverse().find((a) => a.status === "completed");
4591
4737
  const outstanding = [...log.attempts].reverse().find((a) => ["failed", "unknown"].includes(a.status) && (!completed || (a.finishedAt ?? a.startedAt) >= completed.startedAt));
4592
4738
  const status = log.attempts.some((a) => a.status === "running") ? "running" : outstanding?.status ?? log.attempts.at(-1)?.status ?? "not-observed";
@@ -4599,35 +4745,35 @@ var init_execution = __esm({
4599
4745
  init_store();
4600
4746
  init_evidence();
4601
4747
  init_storage();
4602
- failureSchema = z13.object({
4603
- code: z13.enum(["inputs-changed", "storage-full", "busy", "invalid-input", "history-unavailable", "invalid-evidence", "io-error", "internal"]),
4604
- message: z13.string(),
4605
- retryable: z13.boolean(),
4606
- receiptRecorded: z13.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()
4607
4753
  });
4608
- attemptSchema = z13.object({
4609
- id: z13.string().regex(/^[a-zA-Z0-9_-]+$/),
4610
- lease: z13.literal(true).optional(),
4611
- event: z13.string(),
4612
- startedAt: z13.string(),
4613
- finishedAt: z13.string().optional(),
4614
- durationMs: z13.number().nonnegative().optional(),
4615
- pid: z13.number().int().positive(),
4616
- host: z13.string(),
4617
- status: z13.enum(["running", "completed", "failed", "unknown"]),
4618
- verificationStatus: z13.string().optional(),
4619
- reportPath: z13.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(),
4620
4766
  failure: failureSchema.optional()
4621
4767
  });
4622
- executionSchema = z13.object({ version: z13.literal(1), attempts: z13.array(attemptSchema).max(160), discardedAttempts: z13.number().int().nonnegative().default(0) });
4623
- notificationsSchema = z13.object({ version: z13.literal(1), sessions: z13.record(z13.object({
4624
- signature: z13.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(),
4625
4771
  code: failureSchema.shape.code,
4626
- message: z13.string(),
4627
- count: z13.number().int().positive(),
4628
- lastAt: z13.string()
4772
+ message: z14.string(),
4773
+ count: z14.number().int().positive(),
4774
+ lastAt: z14.string()
4629
4775
  })) });
4630
- leasePath = (directory, id) => `${directory}/executions/${id}.json`;
4776
+ leasePath = (directory2, id) => `${directory2}/executions/${id}.json`;
4631
4777
  }
4632
4778
  });
4633
4779
 
@@ -4702,12 +4848,12 @@ __export(runtime_exports, {
4702
4848
  summarize: () => summarize
4703
4849
  });
4704
4850
  import { randomUUID as randomUUID5 } from "crypto";
4705
- import { z as z14 } from "zod";
4851
+ import { z as z15 } from "zod";
4706
4852
  async function observeReadOnlyTool(dir, event) {
4707
4853
  const ws = await profilePhase("automation.workspace", () => workspace(dir));
4708
4854
  await withLock(ws.root, ws.directory, async () => {
4709
- const statePath = ws.directory + "/state.json";
4710
- const raw = await readStoreJson(ws.root, statePath);
4855
+ const statePath2 = ws.directory + "/state.json";
4856
+ const raw = await readStoreJson(ws.root, statePath2);
4711
4857
  if (raw === null) return;
4712
4858
  const state = parseState(raw);
4713
4859
  if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
@@ -4718,7 +4864,7 @@ async function observeReadOnlyTool(dir, event) {
4718
4864
  const now = (/* @__PURE__ */ new Date()).toISOString();
4719
4865
  session.lastUsed = now;
4720
4866
  session.events[event.event] = { at: now, count: (session.events[event.event]?.count ?? 0) + 1 };
4721
- await writeStoreJson(ws.root, statePath, state);
4867
+ await writeStoreJson(ws.root, statePath2, state);
4722
4868
  });
4723
4869
  return { root: ws.root, directory: ws.directory };
4724
4870
  }
@@ -4733,7 +4879,7 @@ function summarize(report) {
4733
4879
  }),
4734
4880
  ...open.length > 4 ? [`${open.length - 4} more findings in the report.`] : [],
4735
4881
  ...report.diagnostics.slice(0, 2).map(cleanText),
4736
- `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.`,
4737
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."
4738
4884
  ].join("\n");
4739
4885
  }
@@ -4792,7 +4938,7 @@ async function verifyInputs(ws, inputs) {
4792
4938
  async function computeAnalysis(ws, state, inputs, event, previousAudit, cacheDiagnostic) {
4793
4939
  const baselines = [...state.baselines];
4794
4940
  const now = (/* @__PURE__ */ new Date()).toISOString();
4795
- const empty = {
4941
+ const empty2 = {
4796
4942
  version: 1,
4797
4943
  status: "unavailable",
4798
4944
  root: ws.root,
@@ -4809,7 +4955,7 @@ async function computeAnalysis(ws, state, inputs, event, previousAudit, cacheDia
4809
4955
  };
4810
4956
  if (!baselines.length && Object.values(inputs.docs).every((value) => value === null)) {
4811
4957
  await verifyInputs(ws, inputs);
4812
- return { baselines, report: empty, cache: null };
4958
+ return { baselines, report: empty2, cache: null };
4813
4959
  }
4814
4960
  if (ws.branch === "detached" && state.latest) {
4815
4961
  const previous = await readStoreJson(ws.root, state.latest);
@@ -4911,7 +5057,7 @@ async function analyze(ws, inputs, event, startedAt) {
4911
5057
  async function automate(dir, event) {
4912
5058
  const startedAt = Date.now();
4913
5059
  const ws = await profilePhase("automation.workspace", () => workspace(dir));
4914
- 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 () => {
4915
5061
  const inputs = await readInputs(ws.root);
4916
5062
  const result = await profilePhase("automation.analysis", () => analyze(ws, inputs, event, startedAt));
4917
5063
  if (result.shared) {
@@ -4928,7 +5074,7 @@ async function automate(dir, event) {
4928
5074
  let baseline = { findings: report.findings, head: report.head };
4929
5075
  if (state.latest) {
4930
5076
  try {
4931
- const previous = z14.object({ findings: z14.array(repairFindingSchema), head: z14.string() }).safeParse(await readStoreJson(ws.root, state.latest));
5077
+ const previous = z15.object({ findings: z15.array(repairFindingSchema), head: z15.string() }).safeParse(await readStoreJson(ws.root, state.latest));
4932
5078
  if (!previous.success) throw new Error("Unsupported saved report");
4933
5079
  baseline = previous.data;
4934
5080
  } catch {
@@ -4996,9 +5142,15 @@ async function automate(dir, event) {
4996
5142
  state.latest = report.reportPath;
4997
5143
  if (persistReport) await writeStoreJson(ws.root, report.reportPath, report);
4998
5144
  await writeStoreJson(ws.root, ws.directory + "/state.json", state);
4999
- return { report, message: message2 };
5145
+ return { report, message: message2, observedAt: now, delivery: { agent: feedback.slice(0, 4), completion: selected.slice(0, 4) } };
5000
5146
  }));
5001
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;
5002
5154
  }
5003
5155
  async function automationStatus(dir) {
5004
5156
  const { pendingKnowledge: pendingKnowledge2 } = await Promise.resolve().then(() => (init_pending(), pending_exports));
@@ -5039,6 +5191,7 @@ var SCOPE, priority, cleanText, analysisSchema, sharedSchema, baselineDigest;
5039
5191
  var init_runtime = __esm({
5040
5192
  "src/automation/runtime.ts"() {
5041
5193
  "use strict";
5194
+ init_usefulness();
5042
5195
  init_notifications();
5043
5196
  init_profile();
5044
5197
  init_inspection();
@@ -5051,34 +5204,34 @@ var init_runtime = __esm({
5051
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.";
5052
5205
  priority = { resolved: 0, "review-required": 1, unresolved: 2, unverified: 3 };
5053
5206
  cleanText = (text2) => text2.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 250);
5054
- analysisSchema = z14.object({
5055
- id: z14.string(),
5056
- fingerprint: z14.string(),
5057
- completedAt: z14.number(),
5058
- baselineDigest: z14.string(),
5207
+ analysisSchema = z15.object({
5208
+ id: z15.string(),
5209
+ fingerprint: z15.string(),
5210
+ completedAt: z15.number(),
5211
+ baselineDigest: z15.string(),
5059
5212
  currentAudit: reportSchema.nullable().optional(),
5060
- report: z14.object({
5061
- version: z14.literal(1),
5062
- status: z14.enum(["verified", "issues-remain", "incomplete", "unavailable"]),
5063
- root: z14.string(),
5064
- branch: z14.string(),
5065
- head: z14.string(),
5066
- baselinePaths: z14.array(z14.string()),
5067
- reportPath: z14.string(),
5068
- findings: z14.array(repairFindingSchema),
5069
- diagnostics: z14.array(z14.string()),
5070
- checks: z14.object({
5071
- ran: z14.array(z14.string()),
5072
- reused: z14.array(z14.string()),
5073
- skipped: z14.array(z14.object({ check: z14.string(), reason: z14.string(), doc: z14.string().optional() })),
5074
- auditReused: z14.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()
5075
5228
  }),
5076
- counts: z14.object({ resolved: z14.number(), unresolved: z14.number(), "review-required": z14.number(), unverified: z14.number() }),
5077
- capture: z14.enum(["observed", "unknown"]),
5078
- scope: z14.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()
5079
5232
  })
5080
5233
  });
5081
- sharedSchema = z14.object({ analysis: analysisSchema, digest: z14.string() });
5234
+ sharedSchema = z15.object({ analysis: analysisSchema, digest: z15.string() });
5082
5235
  baselineDigest = async (ws, state) => profilePhase("automation.baselines", async () => hash(await Promise.all(
5083
5236
  state.baselines.map(async (b) => [b.path, await readStoreJson(ws.root, b.path)])
5084
5237
  )));
@@ -5086,7 +5239,7 @@ var init_runtime = __esm({
5086
5239
  });
5087
5240
 
5088
5241
  // src/automation/hook-root.ts
5089
- import fs20 from "fs/promises";
5242
+ import fs21 from "fs/promises";
5090
5243
  import path28 from "path";
5091
5244
  var init_hook_root = __esm({
5092
5245
  "src/automation/hook-root.ts"() {
@@ -5096,7 +5249,7 @@ var init_hook_root = __esm({
5096
5249
  });
5097
5250
 
5098
5251
  // src/automation/adapters.ts
5099
- import { z as z15 } from "zod";
5252
+ import { z as z16 } from "zod";
5100
5253
  function managedHookCommand(host, platform = process.platform) {
5101
5254
  const command = `mason --setup-host ${host} auto hook --host ${host}`;
5102
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`;
@@ -5107,7 +5260,7 @@ function knownManagedHookCommands(host) {
5107
5260
  function compatibleManagedHookCommands(host, platform = process.platform) {
5108
5261
  return [managedHookCommand(host, platform), `mason --setup-host ${host} auto hook --host ${host}`];
5109
5262
  }
5110
- 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") {
5111
5264
  const handler = { type: "command", command: command === `mason --setup-host ${host} auto` ? managedHookCommand(host) : command + " hook --host " + host, timeout: 30 };
5112
5265
  return { hooks: Object.fromEntries(HOOK_EVENTS.map((name) => [
5113
5266
  name,
@@ -5118,19 +5271,21 @@ var inputSchema, HOOK_EVENTS;
5118
5271
  var init_adapters = __esm({
5119
5272
  "src/automation/adapters.ts"() {
5120
5273
  "use strict";
5274
+ init_usefulness();
5121
5275
  init_execution();
5122
5276
  init_runtime();
5123
5277
  init_store();
5124
5278
  init_hook_root();
5125
- inputSchema = z15.object({
5126
- cwd: z15.string().min(1),
5127
- session_id: z15.string().min(1).max(500),
5128
- hook_event_name: z15.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
5129
- tool_name: z15.string().optional(),
5130
- tool_use_id: z15.string().max(500).optional(),
5131
- tool_input: z15.unknown().optional(),
5132
- stop_hook_active: z15.boolean().optional(),
5133
- permission_mode: z15.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()
5134
5289
  });
5135
5290
  HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"];
5136
5291
  }
@@ -5146,7 +5301,7 @@ __export(ownership_exports, {
5146
5301
  ownershipEdit: () => ownershipEdit,
5147
5302
  ownershipSchema: () => ownershipSchema
5148
5303
  });
5149
- import { z as z16 } from "zod";
5304
+ import { z as z17 } from "zod";
5150
5305
  import { parse } from "smol-toml";
5151
5306
  function blockSpan(text2, start, end) {
5152
5307
  const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
@@ -5197,15 +5352,15 @@ var init_ownership = __esm({
5197
5352
  ["<!-- mason:agents:start -->", "<!-- mason:agents:end -->"],
5198
5353
  ["# mason:mcp:start", "# mason:mcp:end"]
5199
5354
  ];
5200
- fileSchema = z16.object({
5201
- created: z16.boolean(),
5202
- initialHash: z16.string(),
5203
- blocks: z16.record(z16.string()),
5204
- mcpHash: z16.string().optional(),
5205
- hookCommands: z16.array(z16.string()).optional(),
5206
- hooks: z16.record(z16.array(z16.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()
5207
5362
  });
5208
- ownershipSchema = z16.object({ version: z16.literal(1), files: z16.record(fileSchema) });
5363
+ ownershipSchema = z17.object({ version: z17.literal(1), files: z17.record(fileSchema) });
5209
5364
  }
5210
5365
  });
5211
5366
 
@@ -5214,13 +5369,13 @@ var install_exports = {};
5214
5369
  __export(install_exports, {
5215
5370
  AUTOMATION_PATH: () => AUTOMATION_PATH,
5216
5371
  automationConfigSchema: () => automationConfigSchema,
5217
- configPath: () => configPath,
5372
+ configPath: () => configPath2,
5218
5373
  installAutomation: () => installAutomation,
5219
5374
  installedAutomation: () => installedAutomation,
5220
5375
  planAutomationInstall: () => planAutomationInstall,
5221
5376
  recordSchema: () => recordSchema2
5222
5377
  });
5223
- import { z as z17 } from "zod";
5378
+ import { z as z18 } from "zod";
5224
5379
  async function installationRecord(root) {
5225
5380
  return recordSchema2.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
5226
5381
  }
@@ -5229,7 +5384,7 @@ async function installAutomation(dir, host, command) {
5229
5384
  return withLock(ws.root, ".mason/reports/setup-lock", () => installLocked(ws.root, host, command));
5230
5385
  }
5231
5386
  async function planAutomationInstall(root, host, command) {
5232
- const file = configPath(host);
5387
+ const file = configPath2(host);
5233
5388
  const before = await readText(root, file);
5234
5389
  const existing = automationConfigSchema.parse(JSON.parse(before ?? "{}"));
5235
5390
  const record = await installationRecord(root);
@@ -5269,7 +5424,7 @@ async function installLocked(root, host, command) {
5269
5424
  status: "configured",
5270
5425
  command: newCommand,
5271
5426
  events: HOOK_EVENTS,
5272
- 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.",
5273
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."
5274
5429
  };
5275
5430
  }
@@ -5280,10 +5435,10 @@ async function installedAutomation(dir) {
5280
5435
  for (const host of ["claude", "codex"]) {
5281
5436
  const expected = record.hosts[host];
5282
5437
  if (!expected) continue;
5283
- const current = automationConfigSchema.parse(await readStoreJson(ws.root, configPath(host)) ?? {});
5438
+ const current = automationConfigSchema.parse(await readStoreJson(ws.root, configPath2(host)) ?? {});
5284
5439
  const configuredEvents = HOOK_EVENTS.filter((event) => current.hooks?.[event]?.some((group) => group.hooks.some((handler) => handler.type === "command" && handler.command === expected.command)));
5285
5440
  result[host] = {
5286
- configPath: configPath(host),
5441
+ configPath: configPath2(host),
5287
5442
  configuredEvents,
5288
5443
  status: current.disableAllHooks === true ? "disabled" : configuredEvents.length === HOOK_EVENTS.length ? "configured" : "incomplete",
5289
5444
  runtime: "Host version, trust, policy, and tool coverage still determine execution; inspect observed events."
@@ -5291,7 +5446,7 @@ async function installedAutomation(dir) {
5291
5446
  }
5292
5447
  return result;
5293
5448
  }
5294
- var groupSchema, automationConfigSchema, recordSchema2, AUTOMATION_PATH, configPath;
5449
+ var groupSchema, automationConfigSchema, recordSchema2, AUTOMATION_PATH, configPath2;
5295
5450
  var init_install = __esm({
5296
5451
  "src/automation/install.ts"() {
5297
5452
  "use strict";
@@ -5301,11 +5456,11 @@ var init_install = __esm({
5301
5456
  init_store();
5302
5457
  init_files2();
5303
5458
  init_ownership();
5304
- groupSchema = z17.object({ hooks: z17.array(z17.object({ type: z17.string(), command: z17.string().optional() }).passthrough()) }).passthrough();
5305
- automationConfigSchema = z17.object({ hooks: z17.record(z17.array(groupSchema)).optional() }).passthrough();
5306
- recordSchema2 = z17.object({ version: z17.literal(1), hosts: z17.record(z17.object({ command: z17.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() })) });
5307
5462
  AUTOMATION_PATH = ".mason/local/automation.json";
5308
- configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
5463
+ configPath2 = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
5309
5464
  }
5310
5465
  });
5311
5466
 
@@ -5321,7 +5476,7 @@ __export(config_exports, {
5321
5476
  });
5322
5477
  import { isDeepStrictEqual } from "util";
5323
5478
  import { parse as parse2, stringify } from "smol-toml";
5324
- import { z as z18 } from "zod";
5479
+ import { z as z19 } from "zod";
5325
5480
  async function instructionEdits(root, host) {
5326
5481
  const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
5327
5482
  const existing = found.find((f) => f.text !== null);
@@ -5380,7 +5535,7 @@ async function mcpEdit(root, host) {
5380
5535
  return { path: file, before, after };
5381
5536
  }
5382
5537
  async function hookEdits(root, host) {
5383
- const file = configPath(host), before = await readText(root, file);
5538
+ const file = configPath2(host), before = await readText(root, file);
5384
5539
  const { planAutomationInstall: planAutomationInstall2 } = await Promise.resolve().then(() => (init_install(), install_exports));
5385
5540
  const plan = await planAutomationInstall2(root, host, hookCommand(host));
5386
5541
  return [
@@ -5408,7 +5563,7 @@ async function inspectHostConfig(root, host, plannedMcp) {
5408
5563
  const text2 = plannedMcp ?? await readText(root, mcpPath(host));
5409
5564
  const config = host === "codex" ? parse2(text2 ?? "") : object.parse(JSON.parse(text2 ?? "{}"));
5410
5565
  const servers = host === "codex" ? config.mcp_servers : config.mcpServers;
5411
- const hooks = automationConfigSchema.parse(JSON.parse(await readText(root, configPath(host)) ?? "{}"));
5566
+ const hooks = automationConfigSchema.parse(JSON.parse(await readText(root, configPath2(host)) ?? "{}"));
5412
5567
  const expected = hookConfig(host, hookCommand(host));
5413
5568
  const compatible = compatibleManagedHookCommands(host);
5414
5569
  const mcp = servers?.mason === void 0 ? null : object.parse(servers.mason);
@@ -5432,7 +5587,7 @@ var init_config = __esm({
5432
5587
  init_install();
5433
5588
  init_files2();
5434
5589
  init_launcher();
5435
- object = z18.record(z18.unknown());
5590
+ object = z19.record(z19.unknown());
5436
5591
  mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
5437
5592
  TOML_START = "# mason:mcp:start";
5438
5593
  TOML_END = "# mason:mcp:end";
@@ -5442,7 +5597,7 @@ var init_config = __esm({
5442
5597
  });
5443
5598
 
5444
5599
  // src/mcp/init.ts
5445
- import { z as z19 } from "zod";
5600
+ import { z as z20 } from "zod";
5446
5601
  async function loadProjectMarker(rootDir) {
5447
5602
  const raw = await readStoreJson(rootDir, ".mason/local/project.json");
5448
5603
  if (raw === null) return null;
@@ -5450,8 +5605,8 @@ async function loadProjectMarker(rootDir) {
5450
5605
  return { ...marker, features: await loadProjectFeatures(rootDir) };
5451
5606
  }
5452
5607
  async function loadProjectFeatures(rootDir) {
5453
- const config = z19.record(z19.unknown()).parse(await readStoreJson(rootDir, ".mason/config.json") ?? {});
5454
- return z19.object({ confluence: z19.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 ?? {});
5455
5610
  }
5456
5611
  async function saveProjectMarker(rootDir, marker) {
5457
5612
  const { ancillaryEdits: ancillaryEdits2 } = await Promise.resolve().then(() => (init_config(), config_exports));
@@ -5462,10 +5617,10 @@ async function projectMarkerEdits(rootDir, marker) {
5462
5617
  const file = ".mason/local/project.json";
5463
5618
  const edits = [{ path: file, before: await readText(rootDir, file), after: JSON.stringify(local, null, 2) + "\n" }];
5464
5619
  if (features && Object.keys(features).length) {
5465
- const configPath2 = ".mason/config.json", before = await readText(rootDir, configPath2);
5466
- const config = z19.record(z19.unknown()).parse(JSON.parse(before ?? "{}"));
5467
- config.features = { ...z19.record(z19.unknown()).parse(config.features ?? {}), ...features };
5468
- 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" });
5469
5624
  }
5470
5625
  return edits;
5471
5626
  }
@@ -5478,10 +5633,10 @@ var init_init = __esm({
5478
5633
  "use strict";
5479
5634
  init_storage();
5480
5635
  init_files2();
5481
- markerSchema = z19.object({
5482
- version: z19.literal(1),
5483
- initializedAt: z19.string(),
5484
- features: z19.object({ confluence: z19.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()
5485
5640
  }).passthrough();
5486
5641
  CLAUDE_MD_SECTION = `<!-- mason:start -->
5487
5642
  ## Mason project knowledge
@@ -5503,13 +5658,13 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
5503
5658
 
5504
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.
5505
5660
 
5506
- - 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.
5507
5662
 
5508
5663
  Inspect source for what the retrieved context does not answer.
5509
5664
  <!-- mason:end -->`;
5510
5665
  ASSISTANT_SETUP = `UNIFIED SETUP
5511
- 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.
5512
- 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.
5513
5668
 
5514
5669
  The managed project guidance is:
5515
5670
  ${CLAUDE_MD_SECTION}`;
@@ -5680,7 +5835,7 @@ var init_types2 = __esm({
5680
5835
  });
5681
5836
 
5682
5837
  // src/review/evidence/vitest.ts
5683
- import { z as z20 } from "zod";
5838
+ import { z as z21 } from "zod";
5684
5839
  function parseVitest(raw, sourceRoot) {
5685
5840
  const report = schema.parse(raw);
5686
5841
  const findings = [], diagnostics = [];
@@ -5729,24 +5884,24 @@ var init_vitest = __esm({
5729
5884
  "use strict";
5730
5885
  init_paths2();
5731
5886
  init_types2();
5732
- count3 = z20.number().int().nonnegative();
5733
- schema = z20.object({
5734
- success: z20.boolean(),
5887
+ count3 = z21.number().int().nonnegative();
5888
+ schema = z21.object({
5889
+ success: z21.boolean(),
5735
5890
  numTotalTests: count3,
5736
5891
  numPassedTests: count3,
5737
5892
  numFailedTests: count3,
5738
5893
  numPendingTests: count3,
5739
5894
  numTodoTests: count3,
5740
5895
  numFailedTestSuites: count3,
5741
- testResults: z20.array(z20.object({
5742
- name: z20.string().min(1),
5743
- status: z20.enum(["passed", "failed"]),
5744
- message: z20.string().optional(),
5745
- assertionResults: z20.array(z20.object({
5746
- fullName: z20.string(),
5747
- status: z20.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
5748
- failureMessages: z20.array(z20.string()).nullable().optional(),
5749
- location: z20.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()
5750
5905
  }))
5751
5906
  }))
5752
5907
  });
@@ -5754,7 +5909,7 @@ var init_vitest = __esm({
5754
5909
  });
5755
5910
 
5756
5911
  // src/review/evidence/sarif.ts
5757
- import { z as z21 } from "zod";
5912
+ import { z as z22 } from "zod";
5758
5913
  function parseSarif(raw, sourceRoot) {
5759
5914
  const report = schema2.parse(raw), findings = [], diagnostics = [];
5760
5915
  let active = 0, suppressed = 0, absent = 0, unresolved = 0;
@@ -5845,29 +6000,29 @@ var init_sarif = __esm({
5845
6000
  "use strict";
5846
6001
  init_paths2();
5847
6002
  init_types2();
5848
- index = z21.number().int().nonnegative();
5849
- artifact = z21.object({ uri: z21.string().optional(), uriBaseId: z21.string().optional(), index: index.optional() });
5850
- message = z21.object({ text: z21.string().optional(), markdown: z21.string().optional(), id: z21.string().optional(), arguments: z21.array(z21.string()).optional() });
5851
- level = z21.enum(["error", "warning", "note", "none"]);
5852
- location = z21.object({ physicalLocation: z21.object({ artifactLocation: artifact.optional(), region: z21.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
5853
- schema2 = z21.object({
5854
- version: z21.literal("2.1.0"),
5855
- runs: z21.array(z21.object({
5856
- tool: z21.object({ driver: z21.object({ name: z21.string().min(1), rules: z21.array(z21.object({ id: z21.string(), defaultConfiguration: z21.object({ level: level.optional() }).optional(), messageStrings: z21.record(message).optional() })).optional(), globalMessageStrings: z21.record(message).optional() }) }),
5857
- invocations: z21.array(z21.object({ executionSuccessful: z21.boolean(), commandLine: z21.string().optional(), toolExecutionNotifications: z21.array(z21.object({ level: level.optional(), message })).optional() })).optional(),
5858
- versionControlProvenance: z21.array(z21.object({ revisionId: z21.string().optional() })).optional(),
5859
- originalUriBaseIds: z21.record(artifact).optional(),
5860
- artifacts: z21.array(z21.object({ location: artifact.optional() })).optional(),
5861
- results: z21.array(z21.object({
5862
- ruleId: z21.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(),
5863
6018
  ruleIndex: index.optional(),
5864
6019
  message,
5865
6020
  level: level.optional(),
5866
- kind: z21.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
5867
- baselineState: z21.enum(["new", "unchanged", "updated", "absent"]).optional(),
5868
- suppressions: z21.array(z21.object({ kind: z21.enum(["inSource", "external"]), status: z21.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
5869
- locations: z21.array(location).optional(),
5870
- relatedLocations: z21.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()
5871
6026
  })).optional()
5872
6027
  }))
5873
6028
  });
@@ -5875,7 +6030,7 @@ var init_sarif = __esm({
5875
6030
  });
5876
6031
 
5877
6032
  // src/review/evidence/check.ts
5878
- import { z as z22 } from "zod";
6033
+ import { z as z23 } from "zod";
5879
6034
  function parseCheck(raw, sourceRoot) {
5880
6035
  const report = schema3.parse(raw);
5881
6036
  const diagnostics = [], findings = [];
@@ -5915,17 +6070,17 @@ var init_check = __esm({
5915
6070
  "use strict";
5916
6071
  init_paths2();
5917
6072
  init_types2();
5918
- schema3 = z22.object({
5919
- version: z22.literal(1),
5920
- commit: z22.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).optional(),
5921
- results: z22.array(z22.object({
5922
- name: z22.string().min(1).max(1e3),
5923
- status: z22.enum(["passed", "failed", "skipped"]),
5924
- message: z22.string().max(1e5).optional(),
5925
- locations: z22.array(z22.object({
5926
- file: z22.string().min(1).max(4e3),
5927
- line: z22.number().int().positive().optional(),
5928
- column: z22.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()
5929
6084
  })).max(100).default([])
5930
6085
  })).max(1e5)
5931
6086
  });
@@ -5934,7 +6089,7 @@ var init_check = __esm({
5934
6089
 
5935
6090
  // src/review/evidence.ts
5936
6091
  import path29 from "path";
5937
- import { z as z23 } from "zod";
6092
+ import { z as z24 } from "zod";
5938
6093
  function relativeArtifact(root, file) {
5939
6094
  const relative = path29.isAbsolute(file) ? path29.relative(root, file) : file;
5940
6095
  const normalized = normalizeRepoPath(relative);
@@ -5975,7 +6130,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
5975
6130
  let manifest, raw;
5976
6131
  try {
5977
6132
  manifest = relativeArtifact(root, supplied);
5978
- raw = z23.object({ version: z23.literal(1), checks: z23.array(z23.unknown()) }).parse(await readStoreJson(root, manifest));
6133
+ raw = z24.object({ version: z24.literal(1), checks: z24.array(z24.unknown()) }).parse(await readStoreJson(root, manifest));
5979
6134
  } catch (error) {
5980
6135
  output.diagnostics.push(`${supplied}: ${String(error)}`);
5981
6136
  continue;
@@ -6130,31 +6285,31 @@ var init_evidence2 = __esm({
6130
6285
  init_sarif();
6131
6286
  init_check();
6132
6287
  init_types2();
6133
- checkSchema2 = z23.object({
6134
- id: z23.string().min(1).max(100),
6135
- kind: z23.enum(["tests", "static-analysis", "security", "complexity", "duplication", "documentation"]),
6136
- tool: z23.string().min(1).max(200),
6137
- command: z23.string().min(1).max(2e3),
6138
- commit: z23.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
6139
- workingTreeClean: z23.boolean().optional(),
6140
- source: z23.string().max(2e3).optional(),
6141
- sourceRoot: z23.string().min(1).max(4e3).optional(),
6142
- status: z23.enum(["completed", "skipped", "unavailable"]).default("completed"),
6143
- reason: z23.string().min(1).max(2e3).optional(),
6144
- exitCode: z23.number().int().nullable().optional(),
6145
- environment: z23.object({
6146
- toolVersion: z23.string().min(1).max(200),
6147
- runtime: z23.string().min(1).max(200).optional(),
6148
- platform: z23.string().min(1).max(200).optional(),
6149
- configuration: z23.array(z23.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()
6150
6305
  }).optional(),
6151
- report: z23.object({ format: z23.enum(["vitest-json", "sarif", "mason-check-json"]), path: z23.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()
6152
6307
  });
6153
6308
  }
6154
6309
  });
6155
6310
 
6156
6311
  // src/review/review.ts
6157
- import fs21 from "fs/promises";
6312
+ import fs22 from "fs/promises";
6158
6313
  import path30 from "path";
6159
6314
  import { execFile as execFile7 } from "child_process";
6160
6315
  import { promisify as promisify7 } from "util";
@@ -6234,7 +6389,7 @@ async function computeReview(rootDir, base, options = {}) {
6234
6389
  analyzed,
6235
6390
  async (relPath) => {
6236
6391
  try {
6237
- await fs21.access(path30.join(resolvedRoot, relPath));
6392
+ await fs22.access(path30.join(resolvedRoot, relPath));
6238
6393
  return true;
6239
6394
  } catch {
6240
6395
  return false;
@@ -6710,12 +6865,12 @@ __export(assemble_exports, {
6710
6865
  assembleContext: () => assembleContext
6711
6866
  });
6712
6867
  import path33 from "path";
6713
- import fs22 from "fs/promises";
6868
+ import fs23 from "fs/promises";
6714
6869
  async function collectImpact(root, candidates) {
6715
6870
  const targets2 = /* @__PURE__ */ new Set();
6716
6871
  let sourceFiles;
6717
6872
  for (const candidate of sanitizeRepoPaths(candidates)) {
6718
- const stat = await fs22.stat(path33.join(root, candidate)).catch(() => null);
6873
+ const stat = await fs23.stat(path33.join(root, candidate)).catch(() => null);
6719
6874
  if (stat?.isDirectory()) {
6720
6875
  sourceFiles ??= await (await createFileAccess(root)).list();
6721
6876
  for (const file of sourceFiles) {
@@ -6937,13 +7092,13 @@ __export(observations_exports, {
6937
7092
  observeActivation: () => observeActivation,
6938
7093
  readObservation: () => readObservation
6939
7094
  });
6940
- import fs23 from "fs/promises";
6941
- import { z as z24 } from "zod";
6942
- function observationPath(directory, host, revision, version = executingVersion()) {
6943
- 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`;
6944
7099
  }
6945
- async function readObservation(root, directory, host, revision, version = executingVersion()) {
6946
- 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));
6947
7102
  if (raw === null) return null;
6948
7103
  const record = observationSchema.parse(raw);
6949
7104
  if (record.root !== root || record.host !== host || record.revision !== revision || record.masonVersion !== version) throw new Error("Activation receipt belongs to another installation.");
@@ -6960,12 +7115,12 @@ async function observeActivation(dir, event, options = {}) {
6960
7115
  return inactiveSetupMessage();
6961
7116
  }
6962
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];
6963
- const ws = capturedDirectory ? { root: await fs23.realpath(dir), directory: capturedDirectory } : await workspace(dir);
6964
- 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;
6965
7120
  const { setup } = await effectiveSetup(ws.root);
6966
7121
  if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
6967
- const directory = ws.directory + "/activation";
6968
- await withLock(ws.root, directory, async () => {
7122
+ const directory2 = ws.directory + "/activation";
7123
+ await withLock(ws.root, directory2, async () => {
6969
7124
  const now = (/* @__PURE__ */ new Date()).toISOString();
6970
7125
  const record = await readObservation(ws.root, ws.directory, host, revision) ?? {
6971
7126
  version: 1,
@@ -7009,19 +7164,19 @@ var init_observations = __esm({
7009
7164
  init_storage();
7010
7165
  init_model();
7011
7166
  init_launcher();
7012
- observationSchema = z24.object({
7013
- version: z24.literal(1),
7014
- masonVersion: z24.string(),
7015
- root: z24.string(),
7016
- revision: z24.string(),
7017
- host: z24.enum(["codex", "claude"]),
7018
- contextCalls: z24.number().int().nonnegative(),
7019
- lastContextAt: z24.string().optional(),
7020
- sessions: z24.record(z24.object({
7021
- events: z24.array(z24.enum(events)),
7022
- at: z24.string(),
7023
- verificationStatus: z24.string().optional(),
7024
- reportPath: z24.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()
7025
7180
  }))
7026
7181
  });
7027
7182
  }
@@ -7048,7 +7203,7 @@ async function setupStatus(dir) {
7048
7203
  version: 1,
7049
7204
  status: pending.length ? "incomplete" : "not-configured",
7050
7205
  hosts: {},
7051
- 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."
7052
7207
  };
7053
7208
  }
7054
7209
  const hosts = {};
@@ -7082,7 +7237,7 @@ async function setupStatus(dir) {
7082
7237
  if (!mcp || !hooks || !instructionsCurrent || !configured) pending.push("Rerun setup to reconcile project configuration; inspect any disabled host settings.");
7083
7238
  if (!observation?.contextCalls) pending.push("Start a new assistant session and request task context through Mason's get_context tool.");
7084
7239
  if (!complete) pending.push("Review/trust the host configuration, then complete a normal task to observe the full hook lifecycle.");
7085
- 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.");
7086
7241
  const healthy = installed.available && mcp && hooks && instructionsCurrent && configured && automation.status !== "unavailable";
7087
7242
  hosts[host] = {
7088
7243
  status: healthy ? complete && observation?.contextCalls ? "active" : "pending" : "attention",
@@ -7216,6 +7371,11 @@ async function setupProject(dir, options = {}) {
7216
7371
  options.progress?.step("Checking configuration and retained findings");
7217
7372
  const checked = await automate(ws.root, { event: "turn_start" });
7218
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
+ }));
7219
7379
  await writeStoreJson(ws.root, receiptPath, {
7220
7380
  version: 1,
7221
7381
  host,
@@ -7238,6 +7398,7 @@ async function setupProject(dir, options = {}) {
7238
7398
  findings,
7239
7399
  reportPath: checked.report.reportPath,
7240
7400
  activation: await setupStatus(ws.root),
7401
+ stats,
7241
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."
7242
7403
  };
7243
7404
  });
@@ -7249,18 +7410,20 @@ function summarizeSetup(result) {
7249
7410
  " MCP server and lifecycle hooks use mason from PATH.",
7250
7411
  " Project instructions updated; original audit evidence retained.",
7251
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.",
7252
7414
  `Initial audit: ${audit.status}${"counts" in audit ? `; ${audit.counts.issues} issues, ${audit.counts.advisories + audit.counts.suppressedAdvisories} advisories` : ""}.`,
7253
7415
  ..."issues" in audit ? audit.issues.slice(0, 3).map((f) => " " + f.message) : [],
7254
7416
  `Original evidence: ${result.initialReportPath}`,
7255
7417
  "Activation: " + result.activation.status + ".",
7256
7418
  result.next,
7257
7419
  "If the assistant cannot find mason, restart the desktop app so it picks up PATH.",
7258
- "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."
7259
7421
  ].join("\n");
7260
7422
  }
7261
7423
  var init_setup = __esm({
7262
7424
  "src/setup/setup.ts"() {
7263
7425
  "use strict";
7426
+ init_usefulness();
7264
7427
  init_evidence();
7265
7428
  init_store();
7266
7429
  init_runtime();
@@ -7398,7 +7561,7 @@ __export(config_exports2, {
7398
7561
  saveConfluenceConfig: () => saveConfluenceConfig,
7399
7562
  validateProvider: () => validateProvider
7400
7563
  });
7401
- import fs24 from "fs/promises";
7564
+ import fs25 from "fs/promises";
7402
7565
  import path34 from "path";
7403
7566
  import os3 from "os";
7404
7567
  import { execFile as execFile9 } from "child_process";
@@ -7411,15 +7574,15 @@ function configFile() {
7411
7574
  }
7412
7575
  async function loadConfig() {
7413
7576
  try {
7414
- const raw = await fs24.readFile(configFile(), "utf-8");
7577
+ const raw = await fs25.readFile(configFile(), "utf-8");
7415
7578
  return JSON.parse(raw);
7416
7579
  } catch {
7417
7580
  return null;
7418
7581
  }
7419
7582
  }
7420
7583
  async function saveConfig(config) {
7421
- await fs24.mkdir(configDir(), { recursive: true });
7422
- 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");
7423
7586
  }
7424
7587
  function getDefaultModel(provider) {
7425
7588
  return DEFAULT_MODELS[provider];
@@ -7579,7 +7742,7 @@ var init_renderer = __esm({
7579
7742
  });
7580
7743
 
7581
7744
  // src/confluence/diff.ts
7582
- import fs25 from "fs/promises";
7745
+ import fs26 from "fs/promises";
7583
7746
  import path35 from "path";
7584
7747
  import { createHash as createHash6 } from "crypto";
7585
7748
  function hashDescription(description) {
@@ -7593,7 +7756,7 @@ function syncStatePath(rootDir) {
7593
7756
  }
7594
7757
  async function loadSyncState(rootDir) {
7595
7758
  try {
7596
- const raw = await fs25.readFile(syncStatePath(rootDir), "utf-8");
7759
+ const raw = await fs26.readFile(syncStatePath(rootDir), "utf-8");
7597
7760
  const parsed = JSON.parse(raw);
7598
7761
  if (parsed.version !== 2) return null;
7599
7762
  return parsed;
@@ -7602,8 +7765,8 @@ async function loadSyncState(rootDir) {
7602
7765
  }
7603
7766
  }
7604
7767
  async function saveSyncState(rootDir, state) {
7605
- await fs25.mkdir(syncStateDir(rootDir), { recursive: true });
7606
- await fs25.writeFile(
7768
+ await fs26.mkdir(syncStateDir(rootDir), { recursive: true });
7769
+ await fs26.writeFile(
7607
7770
  syncStatePath(rootDir),
7608
7771
  JSON.stringify(state, null, 2),
7609
7772
  "utf-8"
@@ -8156,12 +8319,12 @@ init_advisory_review();
8156
8319
  init_decisions();
8157
8320
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8158
8321
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8159
- import { z as z25 } from "zod";
8322
+ import { z as z26 } from "zod";
8160
8323
 
8161
8324
  // src/mcp/tools.ts
8162
8325
  init_repair();
8163
8326
  init_advisory_review();
8164
- import fs26 from "fs/promises";
8327
+ import fs27 from "fs/promises";
8165
8328
  import path36 from "path";
8166
8329
  import { execFile as execFile11 } from "child_process";
8167
8330
  import { promisify as promisify11 } from "util";
@@ -9135,7 +9298,7 @@ async function detectProjectSnapshot(rootDir) {
9135
9298
  const present = [];
9136
9299
  for (const file of buildFiles) {
9137
9300
  try {
9138
- await fs26.access(path36.join(rootDir, file));
9301
+ await fs27.access(path36.join(rootDir, file));
9139
9302
  present.push(file);
9140
9303
  } catch {
9141
9304
  }
@@ -10027,7 +10190,7 @@ function createMcpServer() {
10027
10190
  const server = new McpServer(
10028
10191
  {
10029
10192
  name: "mason",
10030
- version: "0.19.2"
10193
+ version: "0.20.1"
10031
10194
  },
10032
10195
  {
10033
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.'
@@ -10037,11 +10200,11 @@ function createMcpServer() {
10037
10200
  "mason_init",
10038
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.",
10039
10202
  {
10040
- dir: z25.string().describe("Absolute path to the project root directory"),
10041
- mode: z25.enum(["quickstart", "map", "setup"]).optional().default("quickstart").describe("Quickstart inspects without edits; map requests an architecture build; setup installs the shared onboarding flow."),
10042
- host: z25.enum(["codex", "claude"]).optional().describe("Assistant to configure in setup mode; inferred only when unambiguous."),
10043
- base: z25.string().optional().describe("Git ref for committed-diff review. Defaults to the first available main branch ref."),
10044
- evidence: z25.array(z25.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.")
10045
10208
  },
10046
10209
  async ({ dir, mode, host, base, evidence }) => {
10047
10210
  const result = await masonInit(dir, { mode, host, base, evidence });
@@ -10052,8 +10215,8 @@ function createMcpServer() {
10052
10215
  "mason_automation",
10053
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.",
10054
10217
  {
10055
- dir: z25.string().describe("Absolute path to the project directory"),
10056
- action: z25.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.")
10057
10220
  },
10058
10221
  async ({ dir, action }) => ({ content: [{ type: "text", text: await masonAutomation(dir, action) }] })
10059
10222
  );
@@ -10061,10 +10224,10 @@ function createMcpServer() {
10061
10224
  "mason_repair",
10062
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.",
10063
10226
  {
10064
- dir: z25.string().describe("Absolute path to the project root directory"),
10065
- action: z25.enum(["prepare", "verify"]),
10066
- baselinePath: z25.string().optional().describe("Original baseline path returned by prepare; required for verify."),
10067
- checks: z25.array(z25.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.")
10068
10231
  },
10069
10232
  async ({ dir, action, baselinePath, checks }) => {
10070
10233
  const result = await masonRepair(dir, { action, baselinePath, checks });
@@ -10074,15 +10237,15 @@ function createMcpServer() {
10074
10237
  server.tool(
10075
10238
  "review_advisory",
10076
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.",
10077
- { dir: z25.string().describe("Absolute path to the project root"), ...advisoryReviewRequest.shape },
10240
+ { dir: z26.string().describe("Absolute path to the project root"), ...advisoryReviewRequest.shape },
10078
10241
  async ({ dir, ...request }) => ({ content: [{ type: "text", text: await masonReviewAdvisory(dir, request) }] })
10079
10242
  );
10080
10243
  server.tool(
10081
10244
  "mason_complete_init",
10082
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.",
10083
10246
  {
10084
- dir: z25.string().describe("Absolute path to the project root directory"),
10085
- confluenceConfigured: z25.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")
10086
10249
  },
10087
10250
  async ({ dir, confluenceConfigured }) => {
10088
10251
  const result = await masonCompleteInit(dir, { confluenceConfigured });
@@ -10093,11 +10256,11 @@ function createMcpServer() {
10093
10256
  "mason_set_confluence",
10094
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.",
10095
10258
  {
10096
- baseUrl: z25.string().describe("Confluence base URL. Accepts `acme`, `acme.atlassian.net`, or `https://acme.atlassian.net` (normalized automatically)."),
10097
- email: z25.string().describe("User's Atlassian account email"),
10098
- apiToken: z25.string().describe("API token from id.atlassian.com/manage-profile/security/api-tokens"),
10099
- spaceKey: z25.string().optional().describe("Confluence space key. Omit on the first call to list available spaces."),
10100
- parentPageId: z25.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")
10101
10264
  },
10102
10265
  async ({ baseUrl, email, apiToken, spaceKey, parentPageId }) => {
10103
10266
  const result = await masonSetConfluence({
@@ -10114,7 +10277,7 @@ function createMcpServer() {
10114
10277
  "full_analysis",
10115
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.",
10116
10279
  {
10117
- dir: z25.string().describe("Absolute path to the project root directory")
10280
+ dir: z26.string().describe("Absolute path to the project root directory")
10118
10281
  },
10119
10282
  async ({ dir }) => {
10120
10283
  const result = await fullAnalysis(dir);
@@ -10127,7 +10290,7 @@ function createMcpServer() {
10127
10290
  "analyze_project",
10128
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.",
10129
10292
  {
10130
- dir: z25.string().describe("Absolute path to the project root directory")
10293
+ dir: z26.string().describe("Absolute path to the project root directory")
10131
10294
  },
10132
10295
  async ({ dir }) => {
10133
10296
  const result = await analyzeProject(dir);
@@ -10140,8 +10303,8 @@ function createMcpServer() {
10140
10303
  "get_code_samples",
10141
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.",
10142
10305
  {
10143
- dir: z25.string().describe("Absolute path to the project root directory"),
10144
- count: z25.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)")
10145
10308
  },
10146
10309
  async ({ dir, count: count4 }) => {
10147
10310
  const result = await getCodeSamples(dir, count4);
@@ -10154,7 +10317,7 @@ function createMcpServer() {
10154
10317
  "get_snapshot",
10155
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.",
10156
10319
  {
10157
- dir: z25.string().describe("Absolute path to the project root directory")
10320
+ dir: z26.string().describe("Absolute path to the project root directory")
10158
10321
  },
10159
10322
  async ({ dir }) => {
10160
10323
  const result = await getSnapshot(dir);
@@ -10167,9 +10330,9 @@ function createMcpServer() {
10167
10330
  "get_context",
10168
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.",
10169
10332
  {
10170
- dir: z25.string().describe("Absolute path to the project root directory"),
10171
- task: z25.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"),
10172
- files: z25.array(z25.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.")
10173
10336
  },
10174
10337
  async ({ dir, task, files }) => {
10175
10338
  const result = await getContext(dir, task, files);
@@ -10185,10 +10348,10 @@ function createMcpServer() {
10185
10348
  "generate_snapshot_batch",
10186
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.",
10187
10350
  {
10188
- dir: z25.string().describe("Absolute path to the project root directory"),
10189
- offset: z25.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."),
10190
- batchSize: z25.number().int().optional().describe("Files per batch. Defaults to 50."),
10191
- files: z25.array(z25.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.")
10192
10355
  },
10193
10356
  async ({ dir, offset, batchSize, files }) => {
10194
10357
  const result = await generateSnapshotBatch(dir, offset, batchSize, files);
@@ -10201,23 +10364,23 @@ function createMcpServer() {
10201
10364
  "save_partial_snapshot",
10202
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.",
10203
10366
  {
10204
- dir: z25.string().describe("Absolute path to the project root directory"),
10205
- batchId: z25.string().describe("The `batchId` returned by `generate_snapshot_batch`."),
10206
- offset: z25.number().int().describe("The `offset` returned by `generate_snapshot_batch`. Used to order partials in the reduce step."),
10207
- features: z25.record(
10208
- z25.object({
10209
- description: z25.string(),
10210
- files: z25.array(z25.string()),
10211
- tests: z25.array(z25.string()).optional(),
10212
- type: z25.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(
10213
10376
  '"capability" (user-facing functionality) or "infrastructure" (internal plumbing with no end user \u2014 DI/service wiring, config, logging, adapters). Defaults to "capability".'
10214
10377
  )
10215
10378
  })
10216
10379
  ).describe("Partial features for this batch only \u2014 files outside the batch will be added by other partials."),
10217
- flows: z25.record(
10218
- z25.object({
10219
- description: z25.string(),
10220
- chain: z25.array(z25.string())
10380
+ flows: z26.record(
10381
+ z26.object({
10382
+ description: z26.string(),
10383
+ chain: z26.array(z26.string())
10221
10384
  })
10222
10385
  ).describe("Partial flows whose entire chain is in this batch. Cross-batch flows are reconstructed in reduce.")
10223
10386
  },
@@ -10232,7 +10395,7 @@ function createMcpServer() {
10232
10395
  "reduce_snapshot",
10233
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).",
10234
10397
  {
10235
- dir: z25.string().describe("Absolute path to the project root directory")
10398
+ dir: z26.string().describe("Absolute path to the project root directory")
10236
10399
  },
10237
10400
  async ({ dir }) => {
10238
10401
  const result = await reduceSnapshot(dir);
@@ -10245,25 +10408,25 @@ function createMcpServer() {
10245
10408
  "save_snapshot",
10246
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.",
10247
10410
  {
10248
- dir: z25.string().describe("Absolute path to the project root directory"),
10249
- features: z25.record(
10250
- z25.object({
10251
- description: z25.string().describe("One-line description of the feature"),
10252
- files: z25.array(z25.string()).describe("File paths that implement this feature"),
10253
- tests: z25.array(z25.string()).optional().describe("Test file paths for this feature"),
10254
- type: z25.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(
10255
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".'
10256
10419
  )
10257
10420
  })
10258
10421
  ).describe("Map of feature names to their implementing files"),
10259
- flows: z25.record(
10260
- z25.object({
10261
- description: z25.string().describe("One-line description of the flow"),
10262
- chain: z25.array(z25.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")
10263
10426
  })
10264
10427
  ).describe("Map of flow names to ordered file chains"),
10265
- removeFeatures: z25.array(z25.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."),
10266
- removeFlows: z25.array(z25.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.")
10267
10430
  },
10268
10431
  async ({ dir, features, flows, removeFeatures, removeFlows }) => {
10269
10432
  const result = await saveSnapshotData(
@@ -10282,17 +10445,17 @@ function createMcpServer() {
10282
10445
  "save_decision",
10283
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.",
10284
10447
  {
10285
- dir: z25.string().describe("Absolute path to the project root directory"),
10448
+ dir: z26.string().describe("Absolute path to the project root directory"),
10286
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.`),
10287
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.`),
10288
- category: z25.enum(["decision", "gotcha", "deprecation", "convention"]),
10289
- files: z25.array(z25.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."),
10290
- id: z25.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."),
10291
- supersedes: z25.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."),
10292
10455
  owner: attributionSchema.shape.owner.describe("Responsible person or team, when known. Null clears it. Required for acceptance."),
10293
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."),
10294
10457
  actor: attributionSchema.shape.actor.describe("Known person or agent recording this revision. Omit if unknown; do not infer from Git identity."),
10295
- force: z25.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")
10296
10459
  },
10297
10460
  async ({ dir, title, body, category, files, id, supersedes, force, owner, sources, actor }) => {
10298
10461
  const result = await saveDecision(dir, {
@@ -10314,12 +10477,12 @@ function createMcpServer() {
10314
10477
  "review_decision",
10315
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.",
10316
10479
  {
10317
- dir: z25.string().describe("Absolute path to the project root directory"),
10318
- id: z25.string().regex(/^[a-zA-Z0-9_-]+$/).describe("Decision id from get_context or save_decision"),
10319
- action: z25.enum(["prepare", "accept", "reaffirm", "retire"]).optional().default("prepare").describe("Prepare is read-only. Other actions record an explicitly authorized review."),
10320
- reviewer: z25.string().trim().min(1).max(200).optional().describe("Identity of the actual reviewer; required for a verdict. Never invent one."),
10321
- note: z25.string().trim().min(1).max(1500).optional().describe("Review rationale; required for a verdict. Cite evidence for the decision."),
10322
- reviewToken: z25.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")
10323
10486
  },
10324
10487
  async ({ dir, ...input }) => {
10325
10488
  const result = await reviewDecision2(dir, input);
@@ -10330,7 +10493,7 @@ function createMcpServer() {
10330
10493
  "mason_check_drift",
10331
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.",
10332
10495
  {
10333
- dir: z25.string().describe("Absolute path to the project root directory")
10496
+ dir: z26.string().describe("Absolute path to the project root directory")
10334
10497
  },
10335
10498
  async ({ dir }) => {
10336
10499
  const result = await checkDrift(dir);
@@ -10343,8 +10506,8 @@ function createMcpServer() {
10343
10506
  "verify_snapshot",
10344
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.",
10345
10508
  {
10346
- dir: z25.string().describe("Absolute path to the project root directory"),
10347
- sample: z25.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)")
10348
10511
  },
10349
10512
  async ({ dir, sample }) => {
10350
10513
  const result = await verifySnapshot(dir, sample);
@@ -10355,13 +10518,13 @@ function createMcpServer() {
10355
10518
  "save_verification",
10356
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.",
10357
10520
  {
10358
- dir: z25.string().describe("Absolute path to the project root directory"),
10359
- verdicts: z25.record(
10360
- z25.object({
10361
- kind: z25.enum(["feature", "flow"]).optional().describe("Entry kind returned by verify_snapshot; required to record a verdict"),
10362
- reviewToken: z25.string().optional().describe("Copy the reviewToken from verify_snapshot after inspecting its evidence; required to record a verdict"),
10363
- ok: z25.boolean(),
10364
- note: z25.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")
10365
10528
  })
10366
10529
  ).describe("Entry name \u2192 verdict, exactly as returned by verify_snapshot")
10367
10530
  },
@@ -10374,8 +10537,8 @@ function createMcpServer() {
10374
10537
  "get_impact",
10375
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.",
10376
10539
  {
10377
- dir: z25.string().describe("Absolute path to the project root directory"),
10378
- files: z25.array(z25.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'])")
10379
10542
  },
10380
10543
  async ({ dir, files }) => {
10381
10544
  const result = await getImpact(dir, files);
@@ -10388,12 +10551,12 @@ function createMcpServer() {
10388
10551
  "export_to_confluence",
10389
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.",
10390
10553
  {
10391
- dir: z25.string().describe("Absolute path to the project root directory"),
10392
- spaceKey: z25.string().optional().describe("Override the configured space key"),
10393
- parentPageId: z25.string().optional().describe("Override the configured parent page ID"),
10394
- indexPageTitle: z25.string().optional().describe("Title of the index page (default: 'Mason \u2014 System Map')"),
10395
- changelogPageTitle: z25.string().optional().describe("Title of the changelog page (default: 'Mason \u2014 Changelog')"),
10396
- featurePagePrefix: z25.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: ')")
10397
10560
  },
10398
10561
  async ({ dir, spaceKey, parentPageId, indexPageTitle, changelogPageTitle, featurePagePrefix }) => {
10399
10562
  const result = await exportToConfluenceTool(dir, {
@@ -10418,32 +10581,32 @@ async function startMcpServer(onClose) {
10418
10581
  // src/distribution/updates.ts
10419
10582
  init_store();
10420
10583
  init_storage();
10421
- import fs33 from "fs/promises";
10584
+ import fs34 from "fs/promises";
10422
10585
  import path42 from "path";
10423
10586
  import { spawn as spawn3 } from "child_process";
10424
10587
  import { randomUUID as randomUUID11 } from "crypto";
10425
- import { z as z31 } from "zod";
10588
+ import { z as z32 } from "zod";
10426
10589
 
10427
10590
  // src/distribution/state.ts
10428
10591
  init_storage();
10429
10592
  import os5 from "os";
10430
10593
  import path38 from "path";
10431
10594
  import { fileURLToPath } from "url";
10432
- import fs28 from "fs/promises";
10433
- import { z as z27 } from "zod";
10595
+ import fs29 from "fs/promises";
10596
+ import { z as z28 } from "zod";
10434
10597
 
10435
10598
  // src/distribution/path.ts
10436
- import fs27 from "fs/promises";
10599
+ import fs28 from "fs/promises";
10437
10600
  import os4 from "os";
10438
10601
  import path37 from "path";
10439
10602
  import { execFile as execFile12 } from "child_process";
10440
10603
  import { promisify as promisify12 } from "util";
10441
10604
  import { createHash as createHash7, randomUUID as randomUUID7 } from "crypto";
10442
- import { z as z26 } from "zod";
10605
+ import { z as z27 } from "zod";
10443
10606
  var execute2 = promisify12(execFile12);
10444
- var pathChangeSchema = z26.discriminatedUnion("kind", [
10445
- z26.object({ kind: z26.literal("profile"), file: z26.string(), block: z26.string(), created: z26.boolean() }),
10446
- z26.object({ kind: z26.literal("windows"), entry: z26.string(), created: z26.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() })
10447
10610
  ]);
10448
10611
  var quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
10449
10612
  async function configurePath(bin, previous, save, options = {}) {
@@ -10480,7 +10643,7 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
10480
10643
  else {
10481
10644
  const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path37.join(userHome, name));
10482
10645
  let login = candidates[2];
10483
- 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) => {
10484
10647
  if (error.code === "ENOENT") return false;
10485
10648
  throw error;
10486
10649
  })) {
@@ -10491,12 +10654,12 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
10491
10654
  }
10492
10655
  const marker = `# >>> mason PATH ${createHash7("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
10493
10656
  for (const profile of profiles2) {
10494
- const stat = await fs27.lstat(profile).catch((error) => {
10657
+ const stat = await fs28.lstat(profile).catch((error) => {
10495
10658
  if (error.code === "ENOENT") return null;
10496
10659
  throw error;
10497
10660
  });
10498
- const file = stat?.isSymbolicLink() ? await fs27.realpath(profile) : path37.resolve(profile);
10499
- 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) => {
10500
10663
  if (error.code === "ENOENT" && !stat) return null;
10501
10664
  throw error;
10502
10665
  });
@@ -10511,8 +10674,8 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
10511
10674
  changes.push({ kind: "profile", file, block, created: original === null });
10512
10675
  await save(changes);
10513
10676
  }
10514
- await fs27.mkdir(path37.dirname(file), { recursive: true });
10515
- 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);
10516
10679
  try {
10517
10680
  if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0))) throw new Error(`Shell profile changed during installation: ${profile}`);
10518
10681
  await handle.writeFile(block);
@@ -10563,32 +10726,32 @@ try {
10563
10726
  }
10564
10727
 
10565
10728
  // src/distribution/state.ts
10566
- var releaseVersion = z27.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/);
10567
- var bundleId = z27.string().regex(/^[a-f0-9]{24}$/);
10568
- var updatePolicySchema = z27.object({
10569
- enabled: z27.boolean(),
10570
- revision: z27.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(),
10571
10734
  pinnedVersion: releaseVersion.optional(),
10572
- mirror: z27.string().optional()
10735
+ mirror: z28.string().optional()
10573
10736
  });
10574
- var recordSchema3 = z27.object({
10575
- format: z27.literal(1),
10576
- home: z27.string(),
10577
- bin: z27.string(),
10737
+ var recordSchema3 = z28.object({
10738
+ format: z28.literal(1),
10739
+ home: z28.string(),
10740
+ bin: z28.string(),
10578
10741
  current: bundleId,
10579
10742
  version: releaseVersion,
10580
- launchers: z27.record(z27.string()),
10581
- previousLaunchers: z27.record(z27.string()).optional(),
10582
- pathChanges: z27.array(pathChangeSchema).optional(),
10743
+ launchers: z28.record(z28.string()),
10744
+ previousLaunchers: z28.record(z28.string()).optional(),
10745
+ pathChanges: z28.array(pathChangeSchema).optional(),
10583
10746
  updates: updatePolicySchema.optional(),
10584
- previous: z27.object({ id: bundleId, version: releaseVersion }).optional(),
10585
- pending: z27.object({ id: bundleId, version: releaseVersion, policyRevision: z27.string() }).optional()
10747
+ previous: z28.object({ id: bundleId, version: releaseVersion }).optional(),
10748
+ pending: z28.object({ id: bundleId, version: releaseVersion, policyRevision: z28.string() }).optional()
10586
10749
  });
10587
10750
  async function readInstallation(home) {
10588
10751
  const raw = await readStoreJson(home, "install.json");
10589
10752
  if (!raw) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
10590
10753
  const record = recordSchema3.parse(raw);
10591
- 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.");
10592
10755
  return record;
10593
10756
  }
10594
10757
  function standaloneLocation() {
@@ -10620,22 +10783,22 @@ function newerStableVersion(candidate, installed) {
10620
10783
 
10621
10784
  // src/distribution/bundle.ts
10622
10785
  init_storage();
10623
- import fs29 from "fs/promises";
10786
+ import fs30 from "fs/promises";
10624
10787
  import path39 from "path";
10625
10788
  import { createHash as createHash8 } from "crypto";
10626
- import { z as z28 } from "zod";
10789
+ import { z as z29 } from "zod";
10627
10790
  var targets = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
10628
- var digest2 = z28.string().regex(/^[a-f0-9]{64}$/);
10629
- var bundleSchema = z28.object({
10630
- format: z28.literal(1),
10631
- version: z28.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
10632
- target: z28.enum(targets),
10633
- nodeVersion: z28.string(),
10634
- files: z28.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)
10635
10798
  }).strict();
10636
10799
  var sha256 = (bytes) => createHash8("sha256").update(bytes).digest("hex");
10637
10800
  async function readBundle(root) {
10638
- const bytes = await fs29.readFile(await storePath(root, "bundle.json"));
10801
+ const bytes = await fs30.readFile(await storePath(root, "bundle.json"));
10639
10802
  const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
10640
10803
  const windows = manifest.target.startsWith("win32-");
10641
10804
  for (const file of Object.keys(manifest.files)) {
@@ -10643,7 +10806,7 @@ async function readBundle(root) {
10643
10806
  throw new Error("Unsafe path in Mason bundle: " + file);
10644
10807
  }
10645
10808
  }
10646
- 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"]) {
10647
10810
  if (!manifest.files[required]) throw new Error("Incomplete Mason bundle: " + required);
10648
10811
  }
10649
10812
  return { manifest, manifestHash: sha256(bytes) };
@@ -10654,7 +10817,7 @@ async function verifyBundle(root, expectedHash) {
10654
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.");
10655
10818
  const files = Object.keys(bundle.manifest.files);
10656
10819
  for (let i = 0; i < files.length; i += 8) await Promise.all(files.slice(i, i + 8).map(async (file) => {
10657
- 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);
10658
10821
  }));
10659
10822
  return bundle;
10660
10823
  }
@@ -10662,21 +10825,21 @@ async function verifyBundle(root, expectedHash) {
10662
10825
  // src/distribution/runtime.ts
10663
10826
  init_store();
10664
10827
  init_storage();
10665
- import fs30 from "fs/promises";
10828
+ import fs31 from "fs/promises";
10666
10829
  import { unlinkSync } from "fs";
10667
10830
  import os6 from "os";
10668
10831
  import path40 from "path";
10669
10832
  import { randomUUID as randomUUID8 } from "crypto";
10670
- import { z as z29 } from "zod";
10833
+ import { z as z30 } from "zod";
10671
10834
 
10672
10835
  // src/distribution/trust.ts
10673
10836
  import tls from "tls";
10674
10837
 
10675
10838
  // src/distribution/runtime.ts
10676
- var leaseSchema = z29.object({ pid: z29.number().int().positive(), host: z29.string(), bundle: z29.string().regex(/^[a-f0-9]{24}$/) });
10839
+ var leaseSchema = z30.object({ pid: z30.number().int().positive(), host: z30.string(), bundle: z30.string().regex(/^[a-f0-9]{24}$/) });
10677
10840
 
10678
10841
  // src/distribution/install.ts
10679
- import fs32 from "fs/promises";
10842
+ import fs33 from "fs/promises";
10680
10843
  import os8 from "os";
10681
10844
  import path41 from "path";
10682
10845
  import { execFile as execFile13, spawn as spawn2 } from "child_process";
@@ -10687,14 +10850,14 @@ init_store();
10687
10850
 
10688
10851
  // src/distribution/sessions.ts
10689
10852
  init_storage();
10690
- import fs31 from "fs/promises";
10853
+ import fs32 from "fs/promises";
10691
10854
  import os7 from "os";
10692
10855
  import { randomUUID as randomUUID9 } from "crypto";
10693
- import { z as z30 } from "zod";
10694
- var leaseSchema2 = z30.object({ pid: z30.number().int().positive(), host: z30.string() });
10856
+ import { z as z31 } from "zod";
10857
+ var leaseSchema2 = z31.object({ pid: z31.number().int().positive(), host: z31.string() });
10695
10858
  async function hasRunningMcp(home) {
10696
- const directory = await storePath(home, "sessions");
10697
- const files = await fs31.readdir(directory).catch((error) => {
10859
+ const directory2 = await storePath(home, "sessions");
10860
+ const files = await fs32.readdir(directory2).catch((error) => {
10698
10861
  if (error.code === "ENOENT") return [];
10699
10862
  throw error;
10700
10863
  });
@@ -10712,7 +10875,7 @@ async function hasRunningMcp(home) {
10712
10875
  } catch (error) {
10713
10876
  if (error.code !== "ESRCH") return true;
10714
10877
  }
10715
- await fs31.rm(await storePath(home, relative), { force: true });
10878
+ await fs32.rm(await storePath(home, relative), { force: true });
10716
10879
  } catch {
10717
10880
  return true;
10718
10881
  }
@@ -10723,7 +10886,7 @@ async function registerMcp(home) {
10723
10886
  const name = `sessions/${randomUUID9()}.json`;
10724
10887
  await writeStoreJson(home, name, { pid: process.pid, host: os7.hostname() });
10725
10888
  return async () => {
10726
- await fs31.rm(await storePath(home, name), { force: true }).catch(() => {
10889
+ await fs32.rm(await storePath(home, name), { force: true }).catch(() => {
10727
10890
  });
10728
10891
  };
10729
10892
  }
@@ -10736,10 +10899,10 @@ var launcherNames = ["mason", "mason.cmd", "mason.ps1", "mason-launcher.ps1"];
10736
10899
  async function replace(file, content2, mode = 384) {
10737
10900
  const temp = file + ".tmp-" + randomUUID10();
10738
10901
  try {
10739
- await fs32.writeFile(temp, content2, { mode, flag: "wx" });
10740
- await fs32.rename(temp, file);
10902
+ await fs33.writeFile(temp, content2, { mode, flag: "wx" });
10903
+ await fs33.rename(temp, file);
10741
10904
  } finally {
10742
- await fs32.rm(temp, { force: true });
10905
+ await fs33.rm(temp, { force: true });
10743
10906
  }
10744
10907
  }
10745
10908
  function assertUpdatePolicy(record, version, revision) {
@@ -10751,17 +10914,17 @@ async function installLocked2(home, source, bundle, progress, options, alreadySt
10751
10914
  const raw = await readStoreJson(home, "install.json");
10752
10915
  const previous = raw === null ? null : recordSchema3.parse(raw);
10753
10916
  if (options.stageOnly) assertUpdatePolicy(previous, bundle.manifest.version, options.policyRevision);
10754
- if (!previous) for (const file of await fs32.readdir(home)) {
10917
+ if (!previous) for (const file of await fs33.readdir(home)) {
10755
10918
  if (file === ".install-lock") continue;
10756
- const directory = path41.join(home, file);
10757
- const stat = await fs32.lstat(directory);
10758
- 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) {
10759
10922
  throw new Error("Mason installation directory is not empty and has no ownership receipt.");
10760
10923
  }
10761
10924
  }
10762
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")));
10763
- await fs32.mkdir(selectedBin, { recursive: true });
10764
- const bin = await fs32.realpath(selectedBin);
10926
+ await fs33.mkdir(selectedBin, { recursive: true });
10927
+ const bin = await fs33.realpath(selectedBin);
10765
10928
  if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
10766
10929
  const id = bundle.manifestHash.slice(0, 24);
10767
10930
  const destination = await storePath(home, "versions/" + id);
@@ -10793,7 +10956,7 @@ exec ${shQuote(path41.join(destination, "node"))} ${shQuote(path41.join(destinat
10793
10956
  ` };
10794
10957
  for (const [name, content2] of Object.entries(launchers)) {
10795
10958
  const file = await storePath(bin, name);
10796
- const old = await fs32.readFile(file, "utf8").catch((error) => {
10959
+ const old = await fs33.readFile(file, "utf8").catch((error) => {
10797
10960
  if (error.code === "ENOENT") return null;
10798
10961
  throw error;
10799
10962
  });
@@ -10802,7 +10965,7 @@ exec ${shQuote(path41.join(destination, "node"))} ${shQuote(path41.join(destinat
10802
10965
  const retired = Object.entries({ ...previous?.previousLaunchers, ...previous?.launchers }).filter(([name]) => !(name in launchers));
10803
10966
  const checkRetired = async (name, expected) => {
10804
10967
  if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
10805
- 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) => {
10806
10969
  if (error.code === "ENOENT") return null;
10807
10970
  throw error;
10808
10971
  });
@@ -10837,7 +11000,7 @@ exec ${shQuote(path41.join(destination, "node"))} ${shQuote(path41.join(destinat
10837
11000
  for (const [name, content2] of Object.entries(launchers)) await replace(await storePath(bin, name, true), content2, 493);
10838
11001
  for (const [name, expected] of retired) {
10839
11002
  await checkRetired(name, expected);
10840
- await fs32.rm(await storePath(bin, name), { force: true });
11003
+ await fs33.rm(await storePath(bin, name), { force: true });
10841
11004
  }
10842
11005
  await writeStoreJson(home, "install.json", receipt);
10843
11006
  } catch (error) {
@@ -10856,7 +11019,7 @@ exec ${shQuote(path41.join(destination, "node"))} ${shQuote(path41.join(destinat
10856
11019
  return { home, bin, version: bundle.manifest.version, previousVersion: previous?.version, path: pathResult };
10857
11020
  }
10858
11021
  async function copyBundle(home, source, destination, bundle) {
10859
- const exists = await fs32.lstat(destination).then(() => true, (error) => {
11022
+ const exists = await fs33.lstat(destination).then(() => true, (error) => {
10860
11023
  if (error.code === "ENOENT") return false;
10861
11024
  throw error;
10862
11025
  });
@@ -10865,16 +11028,16 @@ async function copyBundle(home, source, destination, bundle) {
10865
11028
  return;
10866
11029
  }
10867
11030
  const stage = await storePath(home, "versions/.install-" + randomUUID10(), true);
10868
- await fs32.mkdir(stage);
11031
+ await fs33.mkdir(stage);
10869
11032
  try {
10870
11033
  for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
10871
11034
  const from = await storePath(source, file), to = await storePath(stage, file, true);
10872
- await fs32.copyFile(from, to);
10873
- 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);
10874
11037
  }
10875
11038
  await verifyBundle(stage, bundle.manifestHash);
10876
11039
  try {
10877
- await fs32.rename(stage, destination);
11040
+ await fs33.rename(stage, destination);
10878
11041
  } catch (error) {
10879
11042
  try {
10880
11043
  await verifyBundle(destination, bundle.manifestHash);
@@ -10883,7 +11046,7 @@ async function copyBundle(home, source, destination, bundle) {
10883
11046
  }
10884
11047
  }
10885
11048
  } finally {
10886
- await fs32.rm(stage, { recursive: true, force: true });
11049
+ await fs33.rm(stage, { recursive: true, force: true });
10887
11050
  }
10888
11051
  }
10889
11052
  async function prepareStandaloneMcp(home) {
@@ -10921,14 +11084,14 @@ async function prepareStandaloneMcp(home) {
10921
11084
  // src/distribution/updates.ts
10922
11085
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
10923
11086
  var RELEASE_AGE_MS = 24 * 60 * 60 * 1e3;
10924
- var releaseListSchema = z31.array(z31.object({ tag_name: z31.string(), draft: z31.boolean(), prerelease: z31.boolean(), published_at: z31.string().nullable() }));
10925
- var updateManifestSchema = z31.object({
10926
- format: z31.literal(1),
10927
- version: z31.string().regex(/^\d+\.\d+\.\d+$/),
10928
- publishedAt: z31.string().datetime(),
10929
- assets: z31.record(z31.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}$/))
10930
11093
  }).strict();
10931
- var statusSchema2 = z31.object({ lastAttempt: z31.number(), availableVersion: z31.string().optional(), error: z31.string().optional() });
11094
+ var statusSchema2 = z32.object({ lastAttempt: z32.number(), availableVersion: z32.string().optional(), error: z32.string().optional() });
10932
11095
  async function scheduleAutomaticUpdate() {
10933
11096
  try {
10934
11097
  const { home, record } = await currentInstallation();