mason-context 0.22.0 → 0.23.1

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