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-mcp.js CHANGED
@@ -483,11 +483,12 @@ async function storePath(root, relative, createParents = false) {
483
483
  }
484
484
  return current;
485
485
  }
486
- async function readStoreJson(root, relative) {
486
+ async function readStoreJson(root, relative, maxBytes = STORE_MAX_BYTES) {
487
487
  try {
488
488
  const file = await storePath(root, relative);
489
- const raw = await readBoundedFile(file, 10 * 1024 * 1024);
490
- if (raw === null) throw new Error("file is not regular or exceeds 10 MiB");
489
+ const raw = await readBoundedFile(file, maxBytes);
490
+ if (raw === null)
491
+ throw new Error(`file is not regular or exceeds ${maxBytes / 1024 / 1024} MiB`);
491
492
  const parsed = JSON.parse(raw);
492
493
  if (parsed === null) throw new Error("expected a JSON object, received null");
493
494
  return parsed;
@@ -499,9 +500,9 @@ async function readStoreJson(root, relative) {
499
500
  );
500
501
  }
501
502
  }
502
- async function writeStoreJson(root, relative, value) {
503
- const payload = JSON.stringify(value, null, 2) + "\n";
504
- if (Buffer.byteLength(payload) > 10 * 1024 * 1024) {
503
+ async function writeStoreJson(root, relative, value, options = {}) {
504
+ const payload = JSON.stringify(value, null, options.compact ? void 0 : 2) + "\n";
505
+ if (Buffer.byteLength(payload) > STORE_MAX_BYTES) {
505
506
  throw new Error(`Mason store ${relative} exceeds 10 MiB`);
506
507
  }
507
508
  const file = await storePath(root, relative, true);
@@ -519,11 +520,13 @@ async function writeStoreJson(root, relative, value) {
519
520
  await fs2.rm(temporary, { force: true });
520
521
  }
521
522
  }
523
+ var STORE_MAX_BYTES;
522
524
  var init_storage = __esm({
523
525
  "src/utils/storage.ts"() {
524
526
  "use strict";
525
527
  init_paths();
526
528
  init_files();
529
+ STORE_MAX_BYTES = 10 * 1024 * 1024;
527
530
  }
528
531
  });
529
532
 
@@ -544,7 +547,12 @@ async function captureAnchorScopes(root, scopes2) {
544
547
  );
545
548
  const inventory = access.inventory();
546
549
  if (!inventory) throw new Error("Git anchor inventory unavailable");
547
- const ignored = await inspectionGit(
550
+ const visible = indexMatchingPaths(
551
+ union.map((anchor) => [anchor]),
552
+ inventory
553
+ );
554
+ const hidden = new Set(union.filter((_, index2) => !visible[index2].length));
555
+ const ignored = hidden.size ? (await inspectionGit(
548
556
  [
549
557
  "ls-files",
550
558
  "-z",
@@ -552,13 +560,11 @@ async function captureAnchorScopes(root, scopes2) {
552
560
  "--ignored",
553
561
  "--exclude-standard",
554
562
  "--",
555
- ...union.map((anchor) => `:(literal)${anchor}`)
563
+ ...[...hidden].map((anchor) => `:(literal)${anchor}`)
556
564
  ],
557
565
  { cwd: root, maxBuffer: 8 * 1024 * 1024 }
558
- );
559
- const files = [
560
- .../* @__PURE__ */ new Set([...inventory, ...ignored.stdout.split("\0").filter(Boolean)])
561
- ].sort();
566
+ )).stdout.split("\0").filter(Boolean) : [];
567
+ const files = [.../* @__PURE__ */ new Set([...inventory, ...ignored])].sort();
562
568
  const patterns = await Promise.all(
563
569
  union.map(async (anchor) => {
564
570
  const literal2 = fg3.escapePath(anchor);
@@ -574,7 +580,7 @@ async function captureAnchorScopes(root, scopes2) {
574
580
  const filesByScope = new Map(
575
581
  scopes2.map((scope, index2) => [JSON.stringify([...new Set(scope)].sort()), indexed[index2]])
576
582
  );
577
- return { filesByScope, eligible };
583
+ return { filesByScope, eligible, hidden, ignored: new Set(ignored) };
578
584
  };
579
585
  return Promise.all(
580
586
  scopes2.map((scope) => {
@@ -586,8 +592,10 @@ async function captureAnchorScopes(root, scopes2) {
586
592
  const capture = { anchors, files: {}, complete: true };
587
593
  if (!anchors.length) return capture;
588
594
  try {
589
- const { eligible, filesByScope } = await (context ??= prepareContext());
590
- const files = filesByScope.get(JSON.stringify(anchors));
595
+ const { eligible, filesByScope, hidden, ignored } = await (context ??= prepareContext());
596
+ const files = filesByScope.get(JSON.stringify(anchors)).filter(
597
+ (file) => !ignored.has(file) || anchors.some((anchor) => hidden.has(anchor) && anchorMatches(anchor, file))
598
+ );
591
599
  if (files.length > 2e3) return { ...capture, complete: false };
592
600
  for (const file of files) {
593
601
  const evidence = await inspectionRead(
@@ -3626,6 +3634,195 @@ var init_deps_changed = __esm({
3626
3634
  }
3627
3635
  });
3628
3636
 
3637
+ // src/decisions/excerpts.ts
3638
+ import { createHash as createHash4 } from "crypto";
3639
+ import { execFile as execFile2 } from "child_process";
3640
+ import { promisify as promisify2 } from "util";
3641
+ function driftBaseline(record) {
3642
+ const effective = effectiveDecision(record);
3643
+ const inspection = latestDecisionInspection(
3644
+ record,
3645
+ decisionApproval(effective) === "accepted" ? "operative" : "all"
3646
+ );
3647
+ return {
3648
+ record: effective,
3649
+ capture: inspection?.capture ?? (effective.version === 2 ? effective.capture : void 0),
3650
+ commits: [inspection?.headHash, effective.refreshedHash].filter(
3651
+ (hash2) => !!hash2 && /^[a-f0-9]{40,64}$/.test(hash2)
3652
+ )
3653
+ };
3654
+ }
3655
+ async function capturedCommit(root, file, hash2, preferred) {
3656
+ if (/[\n\r]/.test(file)) return null;
3657
+ const { stdout } = await exec(
3658
+ "git",
3659
+ ["--literal-pathspecs", "log", "-n", String(HISTORY_DEPTH), "--format=%H", "--", file],
3660
+ { cwd: root, maxBuffer: 1024 * 1024 }
3661
+ );
3662
+ const commits = [.../* @__PURE__ */ new Set([...preferred, ...stdout.split("\n").filter(Boolean)])];
3663
+ const batch = exec("git", ["cat-file", "--batch"], {
3664
+ cwd: root,
3665
+ encoding: "buffer",
3666
+ maxBuffer: 64 * 1024 * 1024
3667
+ });
3668
+ batch.child.stdin.end(commits.map((commit) => `${commit}:${file}
3669
+ `).join(""));
3670
+ const output = (await batch).stdout;
3671
+ let position = 0;
3672
+ for (const commit of commits) {
3673
+ const end = output.indexOf(10, position);
3674
+ if (end < 0) break;
3675
+ const header = output.subarray(position, end).toString("utf8").split(" ");
3676
+ position = end + 1;
3677
+ if (header[1] !== "blob") continue;
3678
+ const size = Number(header[2]);
3679
+ const bytes = output.subarray(position, position + size);
3680
+ position += size + 1;
3681
+ if (createHash4("sha256").update(bytes).digest("hex") === hash2) return commit;
3682
+ }
3683
+ return null;
3684
+ }
3685
+ function namedIdentifiers(text2) {
3686
+ const names = /* @__PURE__ */ new Set();
3687
+ const add = (name) => {
3688
+ if (name.length >= 3) names.add(name);
3689
+ };
3690
+ for (const [, span] of text2.matchAll(/`([^`]+)`/g))
3691
+ for (const [name] of span.matchAll(/[A-Za-z_$][\w$]*(?:-[A-Za-z0-9_$]+)*/g)) add(name);
3692
+ for (const [, name] of text2.matchAll(/([A-Za-z_$][\w$]*)\(/g)) add(name);
3693
+ for (const [name] of text2.matchAll(/[A-Za-z_$][\w$]*/g))
3694
+ if (/[a-z][A-Z]|[_$]/.test(name)) add(name);
3695
+ return [...names];
3696
+ }
3697
+ function codeTokens(code) {
3698
+ const tokens2 = /* @__PURE__ */ new Set();
3699
+ for (const [name] of code.matchAll(/[A-Za-z_$][\w$]*/g)) {
3700
+ tokens2.add(name.toLowerCase());
3701
+ for (const part of name.split(/_+|(?<=[a-z0-9])(?=[A-Z])/))
3702
+ if (part.length >= 3) tokens2.add(part.toLowerCase());
3703
+ }
3704
+ return tokens2;
3705
+ }
3706
+ function parseHunks(diff) {
3707
+ const hunks = [];
3708
+ for (const line of diff.split("\n")) {
3709
+ if (line.startsWith("@@")) hunks.push({ header: line, lines: [] });
3710
+ else if (hunks.length && /^[+-]/.test(line)) hunks.at(-1).lines.push(line);
3711
+ }
3712
+ return hunks;
3713
+ }
3714
+ async function driftExcerpt(root, record, changedFiles) {
3715
+ const baseline = driftBaseline(record);
3716
+ const excerpt = { mentions: [], relevance: "unknown", files: [], omittedFiles: [] };
3717
+ const named = namedIdentifiers(`${baseline.record.title}
3718
+ ${baseline.record.body}`);
3719
+ const found = /* @__PURE__ */ new Set();
3720
+ let complete = !!baseline.capture && changedFiles.length <= MAX_DIFFED_FILES;
3721
+ const access = await createFileAccess(root);
3722
+ for (const [index2, file] of changedFiles.slice(0, MAX_DIFFED_FILES).entries()) {
3723
+ let hunks = null;
3724
+ let unavailable;
3725
+ const captured = baseline.capture?.files[file];
3726
+ try {
3727
+ if (!baseline.capture?.anchors.some((anchor) => anchorMatches(anchor, file))) {
3728
+ unavailable = "not in the captured anchor scope";
3729
+ } else if (captured === void 0) {
3730
+ const source = await access.read(file);
3731
+ if (source)
3732
+ hunks = [
3733
+ {
3734
+ header: "(added since capture)",
3735
+ lines: source.content.replace(/\r?\n$/, "").split("\n").map((line) => "+" + line)
3736
+ }
3737
+ ];
3738
+ else unavailable = "added since capture but unreadable";
3739
+ } else {
3740
+ const commit = await capturedCommit(root, file, captured, baseline.commits);
3741
+ if (!commit) unavailable = "captured content is not in recent Git history";
3742
+ else {
3743
+ const { stdout } = await exec(
3744
+ "git",
3745
+ [
3746
+ "--literal-pathspecs",
3747
+ "diff",
3748
+ "--no-ext-diff",
3749
+ "--no-textconv",
3750
+ "--no-color",
3751
+ "--no-renames",
3752
+ "-U0",
3753
+ commit,
3754
+ "--",
3755
+ file
3756
+ ],
3757
+ { cwd: root, maxBuffer: 1024 * 1024 }
3758
+ );
3759
+ hunks = parseHunks(stdout);
3760
+ }
3761
+ }
3762
+ } catch {
3763
+ unavailable = "diff could not be read within its bounds";
3764
+ }
3765
+ if (!hunks?.length) complete = false;
3766
+ const code = (hunks ?? []).map((hunk) => [hunk.header, ...hunk.lines.map((line) => line.slice(1))].join("\n")).join("\n");
3767
+ const tokens2 = codeTokens(code);
3768
+ for (const name of named)
3769
+ if (name.includes("-") ? code.toLowerCase().includes(name.toLowerCase()) : tokens2.has(name.toLowerCase()))
3770
+ found.add(name);
3771
+ if (index2 >= MAX_SHOWN_FILES) excerpt.omittedFiles.push(file);
3772
+ else
3773
+ excerpt.files.push({
3774
+ path: file,
3775
+ hunks: (hunks ?? []).slice(0, MAX_SHOWN_HUNKS).map((hunk) => ({ ...hunk, lines: hunk.lines.slice(0, MAX_HUNK_LINES) })),
3776
+ omittedHunks: Math.max(0, (hunks?.length ?? 0) - MAX_SHOWN_HUNKS),
3777
+ ...unavailable ? { unavailable } : {}
3778
+ });
3779
+ }
3780
+ excerpt.omittedFiles.push(...changedFiles.slice(MAX_DIFFED_FILES));
3781
+ excerpt.mentions = [...found].sort();
3782
+ excerpt.relevance = found.size ? "mentioned" : complete && named.length ? "low" : "unknown";
3783
+ if (excerpt.relevance === "unknown")
3784
+ excerpt.unknownReason = named.length ? "incomplete" : "no-identifiers";
3785
+ return excerpt;
3786
+ }
3787
+ function formatDriftExcerpt(title, id, excerpt) {
3788
+ const safe = (value, max = 160) => value.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, max);
3789
+ const lines = [`Changed lines for decision ${JSON.stringify(safe(title, 250))} (${safe(id)}):`];
3790
+ lines.push(
3791
+ 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."
3792
+ );
3793
+ for (const file of excerpt.files) {
3794
+ if (file.unavailable) {
3795
+ lines.push(` ${safe(file.path)}: ${file.unavailable}.`);
3796
+ continue;
3797
+ }
3798
+ for (const hunk of file.hunks)
3799
+ lines.push(
3800
+ ` ${safe(file.path)} ${safe(hunk.header, 120)}`,
3801
+ ...hunk.lines.map((line) => " " + safe(line))
3802
+ );
3803
+ if (!file.hunks.length) lines.push(` ${safe(file.path)}: changed without text hunks.`);
3804
+ if (file.omittedHunks) lines.push(` ${safe(file.path)}: ${file.omittedHunks} more hunk(s).`);
3805
+ }
3806
+ if (excerpt.omittedFiles.length)
3807
+ lines.push(` ${excerpt.omittedFiles.length} more changed file(s) not shown.`);
3808
+ return lines.join("\n");
3809
+ }
3810
+ var exec, MAX_DIFFED_FILES, MAX_SHOWN_FILES, MAX_SHOWN_HUNKS, MAX_HUNK_LINES, HISTORY_DEPTH;
3811
+ var init_excerpts = __esm({
3812
+ "src/decisions/excerpts.ts"() {
3813
+ "use strict";
3814
+ init_files();
3815
+ init_paths();
3816
+ init_provenance();
3817
+ exec = promisify2(execFile2);
3818
+ MAX_DIFFED_FILES = 8;
3819
+ MAX_SHOWN_FILES = 3;
3820
+ MAX_SHOWN_HUNKS = 2;
3821
+ MAX_HUNK_LINES = 8;
3822
+ HISTORY_DEPTH = 20;
3823
+ }
3824
+ });
3825
+
3629
3826
  // src/decisions/drift.ts
3630
3827
  import path17 from "path";
3631
3828
  async function computeDecisionDrift(rootDir, decisions) {
@@ -3690,15 +3887,11 @@ async function computeDecisionDrift(rootDir, decisions) {
3690
3887
  };
3691
3888
  for (const record of store.records) {
3692
3889
  if (record.status !== "active") continue;
3693
- const effective = effectiveDecision(record);
3694
- const inspection = latestDecisionInspection(
3695
- record,
3696
- decisionApproval(effective) === "accepted" ? "operative" : "all"
3697
- );
3698
- const state = await inspect(effective, inspection?.capture);
3890
+ const baseline = driftBaseline(record);
3891
+ const state = await inspect(baseline.record, baseline.capture);
3699
3892
  report.freshness[record.id] = state.freshness;
3700
3893
  if (state.changedFiles.length) report.staleDecisions[record.id] = state.changedFiles;
3701
- if (effective !== record)
3894
+ if (baseline.record !== record)
3702
3895
  (report.pendingProposals ??= {})[record.id] = await inspect(
3703
3896
  record,
3704
3897
  latestDecisionInspection(record)?.capture
@@ -3714,13 +3907,14 @@ var init_drift2 = __esm({
3714
3907
  init_paths();
3715
3908
  init_snapshot();
3716
3909
  init_decisions();
3910
+ init_excerpts();
3717
3911
  init_provenance();
3718
3912
  }
3719
3913
  });
3720
3914
 
3721
3915
  // src/audit/checks/decision-anchor.ts
3722
3916
  async function checkDecisionAnchors(ctx) {
3723
- const result = emptyResult();
3917
+ const result = { ...emptyResult(), incompleteDecisions: [] };
3724
3918
  if (!ctx.decisionsPresent) return result;
3725
3919
  const store = await loadDecisionStore(ctx.root);
3726
3920
  const records = store.records;
@@ -3752,6 +3946,7 @@ async function checkDecisionAnchors(ctx) {
3752
3946
  check: "decision-anchor-drift",
3753
3947
  reason: `Anchor evidence for decision ${record.id} is incomplete or unreadable`
3754
3948
  });
3949
+ result.incompleteDecisions.push(record.id);
3755
3950
  continue;
3756
3951
  }
3757
3952
  if (!changedFiles.length) continue;
@@ -3813,7 +4008,7 @@ var init_checks = __esm({
3813
4008
  });
3814
4009
 
3815
4010
  // src/audit/findings.ts
3816
- import { createHash as createHash4 } from "crypto";
4011
+ import { createHash as createHash5 } from "crypto";
3817
4012
  import { z as z6 } from "zod";
3818
4013
  function findingId(finding) {
3819
4014
  const e = finding.evidence;
@@ -3844,7 +4039,7 @@ var digest, checkSchema, commitSchema, anchorSchema, count, scopeSchema, evidenc
3844
4039
  var init_findings = __esm({
3845
4040
  "src/audit/findings.ts"() {
3846
4041
  "use strict";
3847
- digest = (value) => createHash4("sha256").update(JSON.stringify(value)).digest("hex");
4042
+ digest = (value) => createHash5("sha256").update(JSON.stringify(value)).digest("hex");
3848
4043
  checkSchema = z6.enum([
3849
4044
  "deleted-reference",
3850
4045
  "new-module",
@@ -3951,12 +4146,25 @@ var init_findings = __esm({
3951
4146
  }
3952
4147
  });
3953
4148
 
4149
+ // src/automation/pending-tools.ts
4150
+ function boundPending(pending) {
4151
+ for (const key of Object.keys(pending).slice(0, -PENDING_LIMIT + 1)) delete pending[key];
4152
+ }
4153
+ var PENDING_LIMIT;
4154
+ var init_pending_tools = __esm({
4155
+ "src/automation/pending-tools.ts"() {
4156
+ "use strict";
4157
+ PENDING_LIMIT = 32;
4158
+ }
4159
+ });
4160
+
3954
4161
  // src/automation/stop-review.ts
3955
4162
  import { z as z7 } from "zod";
3956
4163
  function observeDecisionEdits(session, inputs, event) {
3957
4164
  if (event.host !== "claude" || !event.mutating || !event.toolId) return;
3958
4165
  const state = session.stopReview ??= empty();
3959
4166
  if (event.event === "before_tool") {
4167
+ boundPending(state.pending);
3960
4168
  state.pending[event.toolId] = { head: inputs.head, evidence: inputs.decisionEvidence };
3961
4169
  } else if (event.event === "after_tool") {
3962
4170
  const before = state.pending[event.toolId];
@@ -4013,7 +4221,7 @@ function decisionStopReview(session, inputs, report, reentry) {
4013
4221
  }
4014
4222
  decisions.set(evidence.decisionId, value);
4015
4223
  }
4016
- if (!decisions.size) return { reason: null, block: false };
4224
+ if (!decisions.size) return { reason: null, block: false, decisions: [] };
4017
4225
  let fresh = false;
4018
4226
  for (const [id, value] of decisions) {
4019
4227
  const previous = state.delivered[id];
@@ -4036,7 +4244,7 @@ function decisionStopReview(session, inputs, report, reentry) {
4036
4244
  "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.",
4037
4245
  `Evidence: ${report.reportPath}. Concurrent changes can contribute; captures do not prove authorship. This reminder does not resolve the finding.`
4038
4246
  ].join("\n");
4039
- return { reason: reason2, block: !reentry && fresh };
4247
+ return { reason: reason2, block: !reentry && fresh, decisions: [...decisions.keys()] };
4040
4248
  }
4041
4249
  var evidenceSchema2, stopReviewSchema, empty;
4042
4250
  var init_stop_review = __esm({
@@ -4044,6 +4252,7 @@ var init_stop_review = __esm({
4044
4252
  "use strict";
4045
4253
  init_anchors();
4046
4254
  init_findings();
4255
+ init_pending_tools();
4047
4256
  evidenceSchema2 = z7.record(z7.object({ digest: z7.string(), capture: anchorCaptureSchema }));
4048
4257
  stopReviewSchema = z7.object({
4049
4258
  pending: z7.record(z7.object({ head: z7.string(), evidence: evidenceSchema2 })),
@@ -4200,16 +4409,41 @@ function parseState(raw) {
4200
4409
  throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
4201
4410
  }
4202
4411
  }
4412
+ function clearPendingTools(session) {
4413
+ session.pending = {};
4414
+ if (session.stopReview) session.stopReview.pending = {};
4415
+ }
4416
+ function pruneSessions(state, now, active, limit = SESSION_LIMIT) {
4417
+ for (const session of Object.values(state.sessions))
4418
+ if (now.getTime() - Date.parse(session.lastUsed) > IDLE_PENDING_MS) clearPendingTools(session);
4419
+ const evictable = Object.keys(state.sessions).filter((key) => key !== active).sort((a, b) => state.sessions[a].lastUsed.localeCompare(state.sessions[b].lastUsed));
4420
+ const excess = Object.keys(state.sessions).length - limit;
4421
+ for (const key of evictable.slice(0, Math.max(0, excess))) delete state.sessions[key];
4422
+ }
4423
+ function serializeState(state, active) {
4424
+ let payload = JSON.stringify(state) + "\n";
4425
+ while (Buffer.byteLength(payload) > STORE_MAX_BYTES) {
4426
+ const before = Object.keys(state.sessions).length;
4427
+ pruneSessions(state, /* @__PURE__ */ new Date(), active, before - 1);
4428
+ if (Object.keys(state.sessions).length === before)
4429
+ throw new Error(
4430
+ "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."
4431
+ );
4432
+ payload = JSON.stringify(state) + "\n";
4433
+ }
4434
+ return state;
4435
+ }
4203
4436
  function withLock(root, directory3, run, waitMs = 5e3) {
4204
4437
  return withStoreLock(root, directory3, run, waitMs, "Automation");
4205
4438
  }
4206
- var hostSchema, events, stateSchema;
4439
+ var hostSchema, events, stateSchema, SESSION_LIMIT, IDLE_PENDING_MS, STATE_READ_MAX_BYTES;
4207
4440
  var init_store = __esm({
4208
4441
  "src/automation/store.ts"() {
4209
4442
  "use strict";
4210
4443
  init_store_lock();
4211
4444
  init_stop_review();
4212
4445
  init_notifications();
4446
+ init_storage();
4213
4447
  hostSchema = z9.enum(["claude", "codex"]);
4214
4448
  events = [
4215
4449
  "session_start",
@@ -4231,6 +4465,8 @@ var init_store = __esm({
4231
4465
  host: hostSchema,
4232
4466
  lastUsed: z9.string(),
4233
4467
  notifiedDiagnostics: z9.array(z9.string()).default([]),
4468
+ /** Digest of the upstream drift notice already shown to this session. */
4469
+ upstreamNotice: z9.string().optional(),
4234
4470
  pending: z9.record(z9.string()),
4235
4471
  stopReview: stopReviewSchema.optional(),
4236
4472
  coverageGaps: z9.array(z9.string()),
@@ -4245,6 +4481,9 @@ var init_store = __esm({
4245
4481
  agentNotifications: notificationStateSchema.optional(),
4246
4482
  analysis: z9.object({ id: z9.string(), fingerprint: z9.string(), completedAt: z9.number(), path: z9.string() }).optional()
4247
4483
  });
4484
+ SESSION_LIMIT = 32;
4485
+ IDLE_PENDING_MS = 24 * 60 * 60 * 1e3;
4486
+ STATE_READ_MAX_BYTES = 64 * 1024 * 1024;
4248
4487
  }
4249
4488
  });
4250
4489
 
@@ -4977,9 +5216,18 @@ var init_repair = __esm({
4977
5216
  });
4978
5217
 
4979
5218
  // src/automation/evidence.ts
5219
+ var evidence_exports = {};
5220
+ __export(evidence_exports, {
5221
+ checkCache: () => checkCache,
5222
+ git: () => git3,
5223
+ hash: () => hash,
5224
+ readGuidanceInputs: () => readGuidanceInputs,
5225
+ readInputs: () => readInputs,
5226
+ workspace: () => workspace
5227
+ });
4980
5228
  import fs14 from "fs/promises";
4981
5229
  import path20 from "path";
4982
- import { createHash as createHash5 } from "crypto";
5230
+ import { createHash as createHash6 } from "crypto";
4983
5231
  import { z as z12 } from "zod";
4984
5232
  async function git3(root, ...args) {
4985
5233
  return profilePhase(
@@ -5246,8 +5494,8 @@ var init_evidence = __esm({
5246
5494
  init_dead_command();
5247
5495
  init_storage();
5248
5496
  init_advisory_review();
5249
- engineVersion = true ? "0.22.0" : "development";
5250
- hash = (value) => createHash5("sha256").update(JSON.stringify(value)).digest("hex");
5497
+ engineVersion = true ? "0.23.1" : "development";
5498
+ hash = (value) => createHash6("sha256").update(JSON.stringify(value)).digest("hex");
5251
5499
  content = readAuditInput;
5252
5500
  cacheSchema = z12.object({
5253
5501
  version: z12.literal(1),
@@ -5358,8 +5606,8 @@ var init_model = __esm({
5358
5606
 
5359
5607
  // src/setup/launcher.ts
5360
5608
  import { z as z14 } from "zod";
5361
- import { execFile as execFile2 } from "child_process";
5362
- import { promisify as promisify2 } from "util";
5609
+ import { execFile as execFile3 } from "child_process";
5610
+ import { promisify as promisify3 } from "util";
5363
5611
  import fs16 from "fs/promises";
5364
5612
  function globalCommand(args, platform = process.platform) {
5365
5613
  return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args] } : { command: "mason", args };
@@ -5367,7 +5615,7 @@ function globalCommand(args, platform = process.platform) {
5367
5615
  async function installedCommand() {
5368
5616
  const invocation = globalCommand(["internal-integration-version"]);
5369
5617
  try {
5370
- const { stdout } = await promisify2(execFile2)(invocation.command, invocation.args, {
5618
+ const { stdout } = await promisify3(execFile3)(invocation.command, invocation.args, {
5371
5619
  timeout: 5e3,
5372
5620
  maxBuffer: 8192,
5373
5621
  windowsHide: true
@@ -5396,14 +5644,66 @@ var init_launcher = __esm({
5396
5644
  "use strict";
5397
5645
  init_evidence();
5398
5646
  init_model();
5399
- executingVersion = () => true ? "0.22.0" : "development";
5647
+ executingVersion = () => true ? "0.23.1" : "development";
5400
5648
  mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
5401
5649
  hookCommand = (host) => `mason --setup-host ${host}`;
5402
5650
  }
5403
5651
  });
5404
5652
 
5653
+ // src/decisions/attribution.ts
5654
+ async function branchAttribution(root) {
5655
+ const git4 = async (...args) => (await execGit(args, { cwd: root, maxBuffer: 16 * 1024 * 1024 })).stdout.trim();
5656
+ let target = null;
5657
+ for (const ref of TARGETS) {
5658
+ try {
5659
+ await git4("rev-parse", "--verify", "--quiet", `${ref}^{commit}`);
5660
+ target = ref === "refs/remotes/origin/HEAD" ? await git4("symbolic-ref", "--short", ref) : ref.replace(/^refs\/(remotes|heads)\//, "");
5661
+ break;
5662
+ } catch {
5663
+ }
5664
+ }
5665
+ if (!target) return null;
5666
+ const current = await git4("symbolic-ref", "--quiet", "--short", "HEAD").catch(() => "");
5667
+ if (current && (target === current || target.endsWith("/" + current))) return null;
5668
+ let mergeBase, head;
5669
+ try {
5670
+ [mergeBase, head] = await Promise.all([
5671
+ git4("merge-base", "HEAD", target),
5672
+ git4("rev-parse", "HEAD")
5673
+ ]);
5674
+ } catch {
5675
+ return null;
5676
+ }
5677
+ if (!mergeBase || mergeBase === head) return null;
5678
+ const [committed, workingTree] = await Promise.all([
5679
+ git4("diff", "--name-only", "--no-renames", "-z", mergeBase, "HEAD"),
5680
+ getWorkingTree(root)
5681
+ ]);
5682
+ if (!workingTree.available) return null;
5683
+ return {
5684
+ target,
5685
+ mergeBase,
5686
+ branchFiles: /* @__PURE__ */ new Set([...committed.split("\0").filter(Boolean), ...workingTree.changedFiles])
5687
+ };
5688
+ }
5689
+ var TARGETS;
5690
+ var init_attribution = __esm({
5691
+ "src/decisions/attribution.ts"() {
5692
+ "use strict";
5693
+ init_drift();
5694
+ init_git_read();
5695
+ TARGETS = [
5696
+ "refs/remotes/origin/HEAD",
5697
+ "refs/remotes/origin/main",
5698
+ "refs/remotes/origin/master",
5699
+ "refs/heads/main",
5700
+ "refs/heads/master"
5701
+ ];
5702
+ }
5703
+ });
5704
+
5405
5705
  // src/snapshot/review.ts
5406
- import { createHash as createHash7 } from "crypto";
5706
+ import { createHash as createHash8 } from "crypto";
5407
5707
  function snapshotEntryContent(entry) {
5408
5708
  return {
5409
5709
  description: entry.description,
@@ -5414,7 +5714,7 @@ function snapshotEntryContent(entry) {
5414
5714
  };
5415
5715
  }
5416
5716
  function reviewToken(kind, name, entry, evidence) {
5417
- return createHash7("sha256").update(
5717
+ return createHash8("sha256").update(
5418
5718
  JSON.stringify({
5419
5719
  version: 1,
5420
5720
  kind,
@@ -5467,7 +5767,7 @@ var init_review = __esm({
5467
5767
  init_snapshot();
5468
5768
  VERIFY_MAX_FILES_PER_ENTRY = 8;
5469
5769
  VERIFY_SKELETON_CHARS = 500;
5470
- sourceHash = (content2) => createHash7("sha256").update(content2).digest("hex");
5770
+ sourceHash = (content2) => createHash8("sha256").update(content2).digest("hex");
5471
5771
  }
5472
5772
  });
5473
5773
 
@@ -5929,8 +6229,9 @@ async function executionStatus(root, directory3) {
5929
6229
  const outstanding = [...log.attempts].reverse().find(
5930
6230
  (a) => ["failed", "unknown"].includes(a.status) && (!completed || (a.finishedAt ?? a.startedAt) >= completed.startedAt)
5931
6231
  );
5932
- const status = log.attempts.some((a) => a.status === "running") ? "running" : outstanding?.status ?? log.attempts.at(-1)?.status ?? "not-observed";
5933
- return { status, attempts: log.attempts, discardedAttempts: log.discardedAttempts };
6232
+ const inFlight = log.attempts.some((a) => a.status === "running");
6233
+ const status = outstanding?.status ?? (completed ? "completed" : inFlight ? "running" : log.attempts.at(-1)?.status ?? "not-observed");
6234
+ return { status, inFlight, attempts: log.attempts, discardedAttempts: log.discardedAttempts };
5934
6235
  }
5935
6236
  var failureSchema, attemptSchema, executionSchema, notificationsSchema, leasePath;
5936
6237
  var init_execution = __esm({
@@ -6103,7 +6404,7 @@ async function observeReadOnlyTool(dir, event) {
6103
6404
  const ws = await profilePhase("automation.workspace", () => workspace(dir));
6104
6405
  await withLock(ws.root, ws.directory, async () => {
6105
6406
  const statePath2 = ws.directory + "/state.json";
6106
- const raw = await readStoreJson(ws.root, statePath2);
6407
+ const raw = await readStoreJson(ws.root, statePath2, STATE_READ_MAX_BYTES);
6107
6408
  if (raw === null) return;
6108
6409
  const state = parseState(raw);
6109
6410
  if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
@@ -6116,7 +6417,14 @@ async function observeReadOnlyTool(dir, event) {
6116
6417
  const now = (/* @__PURE__ */ new Date()).toISOString();
6117
6418
  session.lastUsed = now;
6118
6419
  session.events[event.event] = { at: now, count: (session.events[event.event]?.count ?? 0) + 1 };
6119
- await writeStoreJson(ws.root, statePath2, state);
6420
+ await writeStoreJson(
6421
+ ws.root,
6422
+ statePath2,
6423
+ serializeState(state, hash([event.host, event.sessionId])),
6424
+ {
6425
+ compact: true
6426
+ }
6427
+ );
6120
6428
  });
6121
6429
  return { root: ws.root, directory: ws.directory };
6122
6430
  }
@@ -6136,7 +6444,7 @@ function summarize(report) {
6136
6444
  ].join("\n");
6137
6445
  }
6138
6446
  async function loadState(ws) {
6139
- const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
6447
+ const raw = await readStoreJson(ws.root, ws.directory + "/state.json", STATE_READ_MAX_BYTES);
6140
6448
  const state = raw === null ? {
6141
6449
  version: 1,
6142
6450
  root: ws.root,
@@ -6368,13 +6676,47 @@ async function analyze(ws, inputs, event, startedAt) {
6368
6676
  completedAt: analysis.completedAt,
6369
6677
  path: analysisPath
6370
6678
  };
6371
- await writeStoreJson(ws.root, ws.directory + "/state.json", state);
6679
+ await writeStoreJson(ws.root, ws.directory + "/state.json", serializeState(state, null), {
6680
+ compact: true
6681
+ });
6372
6682
  });
6373
6683
  return { analysis, shared: false };
6374
6684
  },
6375
6685
  Math.max(0, 25e3 - (Date.now() - startedAt))
6376
6686
  );
6377
6687
  }
6688
+ function upstreamDriftNotice(drift) {
6689
+ const listed = drift.decisions.slice(0, 5).map((decision) => `${JSON.stringify(cleanText(decision.title))} (${cleanText(decision.id)})`);
6690
+ const more = drift.decisions.length - listed.length;
6691
+ return [
6692
+ `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` : ""}.`,
6693
+ `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.`
6694
+ ].join("\n");
6695
+ }
6696
+ async function driftExcerpts(root, report, ids) {
6697
+ const unique = [...new Set(ids)].slice(0, 4);
6698
+ if (!unique.length) return null;
6699
+ const records = (await loadDecisionStore(root)).records;
6700
+ const sections = [];
6701
+ for (const id of unique) {
6702
+ const record = records.find((candidate) => candidate.id === id);
6703
+ const changed = /* @__PURE__ */ new Set();
6704
+ let title = record?.title ?? id;
6705
+ for (const finding of report.findings) {
6706
+ const evidence = finding.current?.evidence;
6707
+ if (finding.status === "resolved" || evidence?.kind !== "decision-anchor") continue;
6708
+ if (evidence.decisionId !== id) continue;
6709
+ title = evidence.title;
6710
+ for (const file of evidence.changedFiles) changed.add(file);
6711
+ }
6712
+ if (!record || !changed.size) continue;
6713
+ try {
6714
+ sections.push(formatDriftExcerpt(title, id, await driftExcerpt(root, record, [...changed])));
6715
+ } catch {
6716
+ }
6717
+ }
6718
+ return sections.join("\n") || null;
6719
+ }
6378
6720
  async function guidanceAnalysis(ws, inputs) {
6379
6721
  const checked = await checkDecisionAnchors({
6380
6722
  root: ws.root,
@@ -6383,7 +6725,27 @@ async function guidanceAnalysis(ws, inputs) {
6383
6725
  changesSinceDoc: /* @__PURE__ */ new Map(),
6384
6726
  decisionsPresent: true
6385
6727
  });
6386
- const findings = checked.advisories.map((current) => ({
6728
+ const attribution = checked.advisories.length ? await branchAttribution(ws.root).catch(() => null) : null;
6729
+ const own = new Set(
6730
+ checked.advisories.flatMap(
6731
+ (advisory) => advisory.evidence.kind === "decision-anchor" && advisory.evidence.changedFiles.some((file) => attribution?.branchFiles.has(file)) ? [advisory.evidence.decisionId] : []
6732
+ )
6733
+ );
6734
+ const upstream = /* @__PURE__ */ new Map();
6735
+ const advisories = checked.advisories.filter((advisory) => {
6736
+ const evidence = advisory.evidence;
6737
+ if (!attribution || evidence.kind !== "decision-anchor" || own.has(evidence.decisionId))
6738
+ return true;
6739
+ const entry = upstream.get(evidence.decisionId) ?? {
6740
+ id: evidence.decisionId,
6741
+ title: evidence.title,
6742
+ changedFiles: []
6743
+ };
6744
+ entry.changedFiles = [.../* @__PURE__ */ new Set([...entry.changedFiles, ...evidence.changedFiles])].sort();
6745
+ upstream.set(evidence.decisionId, entry);
6746
+ return false;
6747
+ });
6748
+ const findings = advisories.map((current) => ({
6387
6749
  id: findingId(current),
6388
6750
  original: current,
6389
6751
  current,
@@ -6395,13 +6757,16 @@ async function guidanceAnalysis(ws, inputs) {
6395
6757
  const prior = z19.object({ findings: z19.array(repairFindingSchema) }).parse(await readStoreJson(ws.root, before.guidance.path));
6396
6758
  const currentIds = new Set(findings.map((finding) => finding.id));
6397
6759
  const records = (await loadDecisionStore(ws.root)).records;
6760
+ const incomplete = new Set(checked.incompleteDecisions);
6761
+ const sharedSkip = checked.skipped.length > checked.incompleteDecisions.length;
6398
6762
  for (const previous of prior.findings) {
6399
6763
  if (currentIds.has(previous.id) || previous.original.evidence.kind !== "decision-anchor")
6400
6764
  continue;
6401
6765
  const evidence = previous.original.evidence;
6766
+ if (upstream.has(evidence.decisionId)) continue;
6402
6767
  const record = records.find((record2) => record2.id === evidence.decisionId);
6403
6768
  const capture = record && inputs.decisionEvidence[record.id]?.capture;
6404
- const resolved = record?.status === "retired" || !!record && !!capture?.complete && capture.anchors.length > 0 && !checked.skipped.length;
6769
+ const resolved = record?.status === "retired" || !!record && !!capture?.complete && capture.anchors.length > 0 && !sharedSkip && !incomplete.has(record.id);
6405
6770
  findings.push({
6406
6771
  id: previous.id,
6407
6772
  original: previous.original,
@@ -6426,6 +6791,13 @@ async function guidanceAnalysis(ws, inputs) {
6426
6791
  baselinePaths: [],
6427
6792
  reportPath: "",
6428
6793
  findings,
6794
+ ...attribution && upstream.size ? {
6795
+ upstreamDrift: {
6796
+ target: attribution.target,
6797
+ mergeBase: attribution.mergeBase,
6798
+ decisions: [...upstream.values()]
6799
+ }
6800
+ } : {},
6429
6801
  diagnostics: [
6430
6802
  ...checked.skipped.map((item) => item.reason),
6431
6803
  ...findings.filter((finding) => finding.status === "unverified").map((finding) => `Decision ${finding.id}: ${finding.reason}`)
@@ -6503,17 +6875,13 @@ async function runAutomation(dir, event, guidance) {
6503
6875
  const now = (/* @__PURE__ */ new Date()).toISOString();
6504
6876
  const key = event.host && event.sessionId ? hash([event.host, event.sessionId]) : null;
6505
6877
  const newSession = key !== null && !state.sessions[key];
6878
+ pruneSessions(
6879
+ state,
6880
+ new Date(now),
6881
+ newSession ? null : key,
6882
+ newSession ? SESSION_LIMIT - 1 : SESSION_LIMIT
6883
+ );
6506
6884
  if (key && !state.sessions[key]) {
6507
- const keys = Object.keys(state.sessions).sort(
6508
- (a, b) => state.sessions[a].lastUsed.localeCompare(state.sessions[b].lastUsed)
6509
- );
6510
- const removable = keys.filter((k) => !Object.keys(state.sessions[k].pending).length);
6511
- for (const expired of removable.slice(0, Math.max(0, keys.length - 31)))
6512
- delete state.sessions[expired];
6513
- if (Object.keys(state.sessions).length >= 128)
6514
- throw new Error(
6515
- "Too many sessions with unfinished tool calls; original evidence was retained."
6516
- );
6517
6885
  state.sessions[key] = {
6518
6886
  host: event.host,
6519
6887
  notifiedDiagnostics: [],
@@ -6533,8 +6901,7 @@ async function runAutomation(dir, event, guidance) {
6533
6901
  count: (session.events[event.event]?.count ?? 0) + 1
6534
6902
  };
6535
6903
  if (event.event === "before_tool" && event.mutating && event.toolId) {
6536
- if (Object.keys(session.pending).length >= 128)
6537
- throw new Error("Too many unfinished tool calls to track pre-edit evidence.");
6904
+ boundPending(session.pending);
6538
6905
  session.pending[event.toolId] = inputs.fingerprint;
6539
6906
  }
6540
6907
  if (event.event === "after_tool" && event.mutating) {
@@ -6560,11 +6927,23 @@ async function runAutomation(dir, event, guidance) {
6560
6927
  ...diagnostics
6561
6928
  ])
6562
6929
  ];
6563
- const stopReview = session && event.host === "claude" && event.event === "task_end" && config.stopBlock === "drift" ? decisionStopReview(session, inputs, report, !!event.stopHookActive) : { reason: null, block: false };
6930
+ const stopReview = session && event.host === "claude" && event.event === "task_end" && config.stopBlock === "drift" ? decisionStopReview(session, inputs, report, !!event.stopHookActive) : { reason: null, block: false, decisions: [] };
6931
+ const excerpts = session ? await driftExcerpts(ws.root, report, [
6932
+ ...stopReview.decisions,
6933
+ ...[...selected, ...feedback].flatMap((finding) => {
6934
+ const evidence = (finding.current ?? finding.original).evidence;
6935
+ return evidence.kind === "decision-anchor" ? [evidence.decisionId] : [];
6936
+ })
6937
+ ]) : null;
6938
+ const upstream = report.upstreamDrift ? upstreamDriftNotice(report.upstreamDrift) : null;
6939
+ const upstreamNotice = session && upstream && session.upstreamNotice !== hash(upstream) ? upstream : null;
6940
+ if (session) session.upstreamNotice = upstream ? hash(upstream) : void 0;
6564
6941
  const message2 = !session ? summarize(report) : [
6565
6942
  stopReview.reason,
6566
6943
  completionSummary(selected, report.reportPath),
6567
6944
  completionSummary(feedback, report.reportPath),
6945
+ excerpts,
6946
+ upstreamNotice,
6568
6947
  ...diagnostics.length ? [
6569
6948
  `Mason: verification limited. ${diagnostics.slice(0, 2).map(cleanText).join(" ")} Evidence: ${report.reportPath}.`
6570
6949
  ] : []
@@ -6582,7 +6961,10 @@ async function runAutomation(dir, event, guidance) {
6582
6961
  state.latest = report.reportPath;
6583
6962
  }
6584
6963
  if (persistReport) await writeStoreJson(ws.root, report.reportPath, report);
6585
- await writeStoreJson(ws.root, ws.directory + "/state.json", state);
6964
+ if (session && event.event === "task_end") clearPendingTools(session);
6965
+ await writeStoreJson(ws.root, ws.directory + "/state.json", serializeState(state, key), {
6966
+ compact: true
6967
+ });
6586
6968
  return {
6587
6969
  report,
6588
6970
  message: message2,
@@ -6620,7 +7002,7 @@ async function automationStatus(dir) {
6620
7002
  const knowledge = await pendingKnowledge2(ws.root);
6621
7003
  const execution = await executionStatus(ws.root, ws.directory);
6622
7004
  const notifications = await failureNotifications(ws.root, ws.directory);
6623
- const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
7005
+ const raw = await readStoreJson(ws.root, ws.directory + "/state.json", STATE_READ_MAX_BYTES);
6624
7006
  if (raw === null)
6625
7007
  return {
6626
7008
  version: 1,
@@ -6648,7 +7030,10 @@ async function automationStatus(dir) {
6648
7030
  host.sessions++;
6649
7031
  host.observedEvents = [.../* @__PURE__ */ new Set([...host.observedEvents, ...Object.keys(session.events)])];
6650
7032
  }
6651
- const unfinished = ["failed", "unknown", "running"].includes(execution.status);
7033
+ const unfinished = ["failed", "unknown"].includes(execution.status);
7034
+ const pendingSessions = Object.values(state.sessions).filter(
7035
+ (session) => Object.keys(session.pending).length
7036
+ );
6652
7037
  return {
6653
7038
  version: 1,
6654
7039
  stopBlock: config.stopBlock,
@@ -6661,6 +7046,12 @@ async function automationStatus(dir) {
6661
7046
  auditReportPath: state.latest,
6662
7047
  verificationStatus: unfinished ? "unavailable" : latest?.status ?? "unavailable",
6663
7048
  hosts,
7049
+ sessions: Object.keys(state.sessions).length,
7050
+ unfinishedToolCalls: pendingSessions.reduce(
7051
+ (total, session) => total + Object.keys(session.pending).length,
7052
+ 0
7053
+ ),
7054
+ oldestUnfinishedSessionUsedAt: pendingSessions.map((session) => session.lastUsed).sort()[0] ?? null,
6664
7055
  execution,
6665
7056
  notifications,
6666
7057
  knowledge,
@@ -6673,6 +7064,8 @@ var init_runtime = __esm({
6673
7064
  "src/automation/runtime.ts"() {
6674
7065
  "use strict";
6675
7066
  init_decisions();
7067
+ init_excerpts();
7068
+ init_attribution();
6676
7069
  init_decision_anchor();
6677
7070
  init_config();
6678
7071
  init_stop_review();
@@ -6685,6 +7078,7 @@ var init_runtime = __esm({
6685
7078
  init_types();
6686
7079
  init_storage();
6687
7080
  init_evidence();
7081
+ init_pending_tools();
6688
7082
  init_store();
6689
7083
  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.";
6690
7084
  priority = { resolved: 0, "review-required": 1, unresolved: 2, unverified: 3 };
@@ -6750,7 +7144,7 @@ import { z as z20 } from "zod";
6750
7144
  import fs21 from "fs/promises";
6751
7145
  import path24 from "path";
6752
7146
  import os3 from "os";
6753
- import { createHash as createHash8 } from "crypto";
7147
+ import { createHash as createHash9 } from "crypto";
6754
7148
  var hookStdinSchema;
6755
7149
  var init_hook = __esm({
6756
7150
  "src/hook/hook.ts"() {
@@ -6773,6 +7167,9 @@ var init_hook = __esm({
6773
7167
 
6774
7168
  // src/automation/adapters.ts
6775
7169
  import { z as z21 } from "zod";
7170
+ function hookEvents(host) {
7171
+ return [...HOOK_EVENTS, ...OPTIONAL_HOOK_EVENTS[host]];
7172
+ }
6776
7173
  function managedHookCommand(host, platform = process.platform, legacy = false) {
6777
7174
  const command = `mason --setup-host ${host}${legacy ? " auto" : ""} hook --host ${host}`;
6778
7175
  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`;
@@ -6805,11 +7202,11 @@ function hookConfig(host, command = "npx --no-install --package mason-context ma
6805
7202
  };
6806
7203
  return {
6807
7204
  hooks: Object.fromEntries(
6808
- HOOK_EVENTS.map((name) => [
7205
+ hookEvents(host).map((name) => [
6809
7206
  name,
6810
7207
  [
6811
7208
  {
6812
- ...["PreToolUse", "PostToolUse"].includes(name) ? { matcher: ".*" } : {},
7209
+ ...["PreToolUse", "PostToolUse", "PostToolUseFailure"].includes(name) ? { matcher: ".*" } : {},
6813
7210
  hooks: [{ ...handler }]
6814
7211
  }
6815
7212
  ]
@@ -6817,7 +7214,7 @@ function hookConfig(host, command = "npx --no-install --package mason-context ma
6817
7214
  )
6818
7215
  };
6819
7216
  }
6820
- var inputSchema, HOOK_EVENTS;
7217
+ var inputSchema, HOOK_EVENTS, OPTIONAL_HOOK_EVENTS;
6821
7218
  var init_adapters = __esm({
6822
7219
  "src/automation/adapters.ts"() {
6823
7220
  "use strict";
@@ -6835,6 +7232,7 @@ var init_adapters = __esm({
6835
7232
  "UserPromptSubmit",
6836
7233
  "PreToolUse",
6837
7234
  "PostToolUse",
7235
+ "PostToolUseFailure",
6838
7236
  "Stop"
6839
7237
  ]),
6840
7238
  tool_name: z21.string().optional(),
@@ -6851,6 +7249,10 @@ var init_adapters = __esm({
6851
7249
  "PostToolUse",
6852
7250
  "Stop"
6853
7251
  ];
7252
+ OPTIONAL_HOOK_EVENTS = {
7253
+ claude: ["PostToolUseFailure"],
7254
+ codex: []
7255
+ };
6854
7256
  }
6855
7257
  });
6856
7258
 
@@ -6983,7 +7385,7 @@ async function planAutomationInstall(root, host, command) {
6983
7385
  const pending = ownershipText === null ? [] : ownershipSchema.parse(JSON.parse(ownershipText)).files[file]?.hookCommands ?? [];
6984
7386
  const owned = /* @__PURE__ */ new Set([previous, ...pending, newCommand, ...knownManagedHookCommands(host)]);
6985
7387
  const hooks = existing.hooks ?? {};
6986
- for (const event of HOOK_EVENTS) {
7388
+ for (const event of hookEvents(host)) {
6987
7389
  hooks[event] = (hooks[event] ?? []).map((group) => ({
6988
7390
  ...group,
6989
7391
  hooks: group.hooks.filter(
@@ -7017,7 +7419,7 @@ async function installLocked(root, host, command) {
7017
7419
  configPath: file,
7018
7420
  status: "configured",
7019
7421
  command: newCommand,
7020
- events: HOOK_EVENTS,
7422
+ events: hookEvents(host),
7021
7423
  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.",
7022
7424
  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."
7023
7425
  };
@@ -7032,7 +7434,7 @@ async function installedAutomation(dir) {
7032
7434
  const current = automationConfigSchema.parse(
7033
7435
  await readStoreJson(ws.root, configPath3(host)) ?? {}
7034
7436
  );
7035
- const configuredEvents = HOOK_EVENTS.filter(
7437
+ const configuredEvents = hookEvents(host).filter(
7036
7438
  (event) => current.hooks?.[event]?.some(
7037
7439
  (group) => group.hooks.some(
7038
7440
  (handler) => handler.type === "command" && handler.command === expected.command
@@ -7042,7 +7444,7 @@ async function installedAutomation(dir) {
7042
7444
  result[host] = {
7043
7445
  configPath: configPath3(host),
7044
7446
  configuredEvents,
7045
- status: current.disableAllHooks === true ? "disabled" : configuredEvents.length === HOOK_EVENTS.length ? "configured" : "incomplete",
7447
+ status: current.disableAllHooks === true ? "disabled" : HOOK_EVENTS.every((event) => configuredEvents.includes(event)) ? "configured" : "incomplete",
7046
7448
  runtime: "Host version, trust, policy, and tool coverage still determine execution; inspect observed events."
7047
7449
  };
7048
7450
  }
@@ -7071,16 +7473,33 @@ var init_install = __esm({
7071
7473
  }
7072
7474
  });
7073
7475
 
7476
+ // src/decisions/merge.ts
7477
+ import fs22 from "fs/promises";
7478
+ var MERGE_DRIVER_NAME, MERGE_ATTRIBUTE, MERGE_DRIVER_COMMAND;
7479
+ var init_merge = __esm({
7480
+ "src/decisions/merge.ts"() {
7481
+ "use strict";
7482
+ init_files();
7483
+ init_provenance();
7484
+ MERGE_DRIVER_NAME = "mason-decision";
7485
+ MERGE_ATTRIBUTE = `.mason/decisions/*.json merge=${MERGE_DRIVER_NAME}`;
7486
+ 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";
7487
+ }
7488
+ });
7489
+
7074
7490
  // src/setup/config.ts
7075
7491
  var config_exports = {};
7076
7492
  __export(config_exports, {
7077
7493
  ancillaryEdits: () => ancillaryEdits,
7494
+ configureDecisionMerge: () => configureDecisionMerge,
7495
+ decisionMergeStatus: () => decisionMergeStatus,
7078
7496
  hookEdits: () => hookEdits,
7079
7497
  inspectHostConfig: () => inspectHostConfig,
7080
7498
  inspectIntegration: () => inspectIntegration,
7081
7499
  instructionEdits: () => instructionEdits,
7082
7500
  mcpEdit: () => mcpEdit,
7083
- mcpPath: () => mcpPath
7501
+ mcpPath: () => mcpPath,
7502
+ mergeDriverSummary: () => mergeDriverSummary
7084
7503
  });
7085
7504
  import { isDeepStrictEqual } from "util";
7086
7505
  import { parse as parse2, stringify } from "smol-toml";
@@ -7202,6 +7621,7 @@ async function hookEdits(root, host) {
7202
7621
  }
7203
7622
  async function ancillaryEdits(root) {
7204
7623
  const ignore = await readText(root, ".gitignore");
7624
+ const attributes = await readText(root, ".gitattributes");
7205
7625
  const rules = [
7206
7626
  "!/.mason/",
7207
7627
  "/.mason/*",
@@ -7219,9 +7639,45 @@ async function ancillaryEdits(root) {
7219
7639
  path: ".gitignore",
7220
7640
  before: ignore,
7221
7641
  after: managedBlock(ignore ?? "", "# mason:ignore:start", "# mason:ignore:end", rules)
7642
+ },
7643
+ // Unconfigured checkouts keep Git's text merge; mason setup enables the driver locally.
7644
+ {
7645
+ path: ".gitattributes",
7646
+ before: attributes,
7647
+ after: managedBlock(
7648
+ attributes ?? "",
7649
+ "# mason:merge:start",
7650
+ "# mason:merge:end",
7651
+ MERGE_ATTRIBUTE
7652
+ )
7222
7653
  }
7223
7654
  ];
7224
7655
  }
7656
+ async function decisionMergeStatus(root) {
7657
+ const { git: git4 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
7658
+ const attribute = (await git4(root, "check-attr", "merge", "--", ".mason/decisions/record.json")).trim();
7659
+ const driver = await git4(root, "config", "--get", `merge.${MERGE_DRIVER_NAME}.driver`).then(
7660
+ (value) => value.trim(),
7661
+ () => null
7662
+ );
7663
+ return {
7664
+ attribute: attribute.endsWith(`: merge: ${MERGE_DRIVER_NAME}`),
7665
+ driver: driver === null ? "missing" : driver === MERGE_DRIVER_COMMAND ? "configured" : "custom"
7666
+ };
7667
+ }
7668
+ function mergeDriverSummary(state) {
7669
+ 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.";
7670
+ return state.attribute ? driver : driver + " The .gitattributes entry is missing; a maintainer can add it with mason init.";
7671
+ }
7672
+ async function configureDecisionMerge(root) {
7673
+ const status = await decisionMergeStatus(root);
7674
+ if (status.driver !== "missing") return status;
7675
+ const { git: git4 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
7676
+ const section = `merge.${MERGE_DRIVER_NAME}`;
7677
+ await git4(root, "config", "--local", `${section}.name`, "Mason decision record merge");
7678
+ await git4(root, "config", "--local", `${section}.driver`, MERGE_DRIVER_COMMAND);
7679
+ return { ...status, driver: "configured" };
7680
+ }
7225
7681
  async function inspectHostConfig(root, host, plannedMcp) {
7226
7682
  const text2 = plannedMcp ?? await readText(root, mcpPath(host));
7227
7683
  const config = host === "codex" ? parse2(text2 ?? "") : object.parse(JSON.parse(text2 ?? "{}"));
@@ -7272,7 +7728,10 @@ async function inspectIntegration(root, host) {
7272
7728
  ])
7273
7729
  );
7274
7730
  const mcpCompatible = isDeepStrictEqual({ command: config.mcp?.command, args: config.mcp?.args }, mcpCommand(host)) && (config.mcp?.type === void 0 || config.mcp.type === "stdio");
7275
- const hooksCompatible = isDeepStrictEqual(normalizedHooks, expectedHooks);
7731
+ const required = new Set(HOOK_EVENTS);
7732
+ const hooksCompatible = Object.entries(expectedHooks).every(
7733
+ ([event, groups]) => isDeepStrictEqual(normalizedHooks[event], groups) || !required.has(event) && !normalizedHooks[event]?.length
7734
+ );
7276
7735
  return {
7277
7736
  ...config,
7278
7737
  instructions,
@@ -7294,6 +7753,7 @@ var init_config2 = __esm({
7294
7753
  init_install();
7295
7754
  init_files2();
7296
7755
  init_launcher();
7756
+ init_merge();
7297
7757
  object = z24.record(z24.unknown());
7298
7758
  mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
7299
7759
  TOML_START = "# mason:mcp:start";
@@ -7618,8 +8078,8 @@ __export(impact_exports, {
7618
8078
  analyzeImpact: () => analyzeImpact
7619
8079
  });
7620
8080
  import path29 from "path";
7621
- import { execFile as execFile6 } from "child_process";
7622
- import { promisify as promisify6 } from "util";
8081
+ import { execFile as execFile7 } from "child_process";
8082
+ import { promisify as promisify7 } from "util";
7623
8083
  async function analyzeImpact(rootDir, targetFiles) {
7624
8084
  const resolvedRoot = path29.resolve(rootDir);
7625
8085
  const resolvedTargets = await resolveTargetFiles(resolvedRoot, targetFiles);
@@ -7664,7 +8124,7 @@ async function getCochangeFiles(rootDir, targetFiles) {
7664
8124
  let totalTargetCommits = 0;
7665
8125
  for (const targetFile of targetFiles) {
7666
8126
  try {
7667
- const { stdout: commitLog } = await exec4(
8127
+ const { stdout: commitLog } = await exec5(
7668
8128
  "git",
7669
8129
  ["log", "--format=%H", "-n", "500", "--", targetFile],
7670
8130
  { cwd: rootDir, maxBuffer: 5e6 }
@@ -7674,7 +8134,7 @@ async function getCochangeFiles(rootDir, targetFiles) {
7674
8134
  if (commits.length === 0) continue;
7675
8135
  for (const commit of commits) {
7676
8136
  try {
7677
- const { stdout: filesInCommit } = await exec4(
8137
+ const { stdout: filesInCommit } = await exec5(
7678
8138
  "git",
7679
8139
  ["diff-tree", "--no-commit-id", "--name-only", "-r", commit],
7680
8140
  { cwd: rootDir }
@@ -7751,14 +8211,14 @@ async function getRelatedTests(rootDir, targetFiles) {
7751
8211
  }
7752
8212
  return results;
7753
8213
  }
7754
- var exec4;
8214
+ var exec5;
7755
8215
  var init_impact = __esm({
7756
8216
  "src/impact/impact.ts"() {
7757
8217
  "use strict";
7758
8218
  init_files();
7759
8219
  init_paths();
7760
8220
  init_references();
7761
- exec4 = promisify6(execFile6);
8221
+ exec5 = promisify7(execFile7);
7762
8222
  }
7763
8223
  });
7764
8224
 
@@ -7768,12 +8228,12 @@ __export(assemble_exports, {
7768
8228
  assembleContext: () => assembleContext
7769
8229
  });
7770
8230
  import path30 from "path";
7771
- import fs23 from "fs/promises";
8231
+ import fs24 from "fs/promises";
7772
8232
  async function collectImpact(root, candidates) {
7773
8233
  const targets2 = /* @__PURE__ */ new Set();
7774
8234
  let sourceFiles;
7775
8235
  for (const candidate of sanitizeRepoPaths(candidates)) {
7776
- const stat = await fs23.stat(path30.join(root, candidate)).catch(() => null);
8236
+ const stat = await fs24.stat(path30.join(root, candidate)).catch(() => null);
7777
8237
  if (stat?.isDirectory()) {
7778
8238
  sourceFiles ??= await (await createFileAccess(root)).list();
7779
8239
  for (const file of sourceFiles) {
@@ -8046,7 +8506,7 @@ __export(observations_exports, {
8046
8506
  observeActivation: () => observeActivation,
8047
8507
  readObservation: () => readObservation
8048
8508
  });
8049
- import fs24 from "fs/promises";
8509
+ import fs25 from "fs/promises";
8050
8510
  import { z as z30 } from "zod";
8051
8511
  function observationPath(directory3, host, revision, version = executingVersion()) {
8052
8512
  return `${directory3}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
@@ -8070,8 +8530,8 @@ async function observeActivation(dir, event, options = {}) {
8070
8530
  return inactiveSetupMessage();
8071
8531
  }
8072
8532
  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];
8073
- const ws = capturedDirectory ? { root: await fs24.realpath(dir), directory: capturedDirectory } : await workspace(dir);
8074
- if (ws.root !== await fs24.realpath(process.env.MASON_SETUP_ROOT)) return null;
8533
+ const ws = capturedDirectory ? { root: await fs25.realpath(dir), directory: capturedDirectory } : await workspace(dir);
8534
+ if (ws.root !== await fs25.realpath(process.env.MASON_SETUP_ROOT)) return null;
8075
8535
  const { setup } = await effectiveSetup(ws.root);
8076
8536
  if (setup?.hosts[host]?.revision !== revision)
8077
8537
  return "Mason setup changed; restart the assistant to observe the current integration.";
@@ -8152,6 +8612,7 @@ async function setupStatus(dir) {
8152
8612
  const ws = await workspace(dir);
8153
8613
  const activation = await effectiveSetup(ws.root);
8154
8614
  const setup = activation.setup;
8615
+ const mergeDriver = await decisionMergeStatus(ws.root);
8155
8616
  if (!setup || !Object.keys(setup.hosts).length) {
8156
8617
  const partial = await Promise.all(
8157
8618
  ["codex", "claude"].map(async (host) => ({
@@ -8182,6 +8643,7 @@ async function setupStatus(dir) {
8182
8643
  status: pending.length ? "incomplete" : "not-configured",
8183
8644
  hosts: {},
8184
8645
  configuration,
8646
+ mergeDriver,
8185
8647
  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."
8186
8648
  };
8187
8649
  }
@@ -8256,6 +8718,7 @@ async function setupStatus(dir) {
8256
8718
  hosts,
8257
8719
  activationRoot: activation.root,
8258
8720
  decisionRecords: decisions.records.length,
8721
+ mergeDriver,
8259
8722
  diagnostics: decisions.diagnostics,
8260
8723
  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."
8261
8724
  };
@@ -8278,6 +8741,8 @@ function summarizeActivation(status) {
8278
8741
  lines.push(...state.updates.map((message2) => " Optional: " + message2));
8279
8742
  }
8280
8743
  if ("decisionRecords" in status) lines.push(`Decision records: ${status.decisionRecords}.`);
8744
+ if (status.mergeDriver.attribute || status.mergeDriver.driver !== "missing")
8745
+ lines.push(mergeDriverSummary(status.mergeDriver));
8281
8746
  if (status.next) lines.push(status.next);
8282
8747
  return lines.join("\n");
8283
8748
  }
@@ -8395,6 +8860,7 @@ async function setupProject(dir, options = {}) {
8395
8860
  after: JSON.stringify(setup, null, 2) + "\n"
8396
8861
  }))
8397
8862
  changedFiles.push(SETUP_PATH);
8863
+ const mergeDriver = await configureDecisionMerge(ws.root);
8398
8864
  options.progress?.step("Checking agent configuration");
8399
8865
  for (const input of sharedInputs)
8400
8866
  if (await readText(ws.root, input.path) !== input.before)
@@ -8418,6 +8884,7 @@ async function setupProject(dir, options = {}) {
8418
8884
  runtime: { kind: "global", command: "mason", version: installed.version },
8419
8885
  changedFiles,
8420
8886
  receiptPath,
8887
+ mergeDriver,
8421
8888
  activation: await setupStatus(ws.root),
8422
8889
  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."
8423
8890
  };
@@ -8429,6 +8896,7 @@ function summarizeSetup(result) {
8429
8896
  " MCP server and lifecycle hooks use mason from PATH.",
8430
8897
  " Existing shared configuration validated; only local activation state was written.",
8431
8898
  ` ${result.changedFiles.length} files changed.`,
8899
+ " " + mergeDriverSummary(result.mergeDriver),
8432
8900
  "Activation: " + result.activation.status + ".",
8433
8901
  result.next,
8434
8902
  "If the assistant cannot find mason, restart the desktop app so it picks up PATH.",
@@ -8526,15 +8994,17 @@ init_snapshot();
8526
8994
  init_drift();
8527
8995
  init_paths();
8528
8996
  init_files();
8529
- import { createHash as createHash6 } from "crypto";
8530
- import { execFile as execFile3 } from "child_process";
8531
- import { promisify as promisify3 } from "util";
8997
+ init_attribution();
8998
+ import { createHash as createHash7 } from "crypto";
8999
+ import { execFile as execFile4 } from "child_process";
9000
+ import { promisify as promisify4 } from "util";
8532
9001
  import { z as z15 } from "zod";
8533
- var exec = promisify3(execFile3);
9002
+ var exec2 = promisify4(execFile4);
8534
9003
  var requestSchema = z15.object({
8535
9004
  id: z15.string().regex(/^[a-zA-Z0-9_-]+$/),
8536
9005
  action: z15.enum(["prepare", "inspect", "accept", "reaffirm", "retire"]).default("prepare"),
8537
9006
  inspectionScope: z15.enum(["all", "operative"]).default("all"),
9007
+ diffFrom: z15.enum(["lastReview", "capture"]).default("lastReview"),
8538
9008
  inspector: z15.string().trim().min(1).max(200).optional(),
8539
9009
  reviewer: z15.string().trim().min(1).max(200).optional(),
8540
9010
  note: z15.string().trim().min(1).max(1500).optional(),
@@ -8561,7 +9031,7 @@ async function reviewState(root, record, scope = "all") {
8561
9031
  localChanges: matchingPaths(anchors, workingTree.changedFiles)
8562
9032
  };
8563
9033
  const capture = await captureAnchors(root, anchors);
8564
- const reviewToken2 = createHash6("sha256").update(
9034
+ const reviewToken2 = createHash7("sha256").update(
8565
9035
  JSON.stringify({
8566
9036
  record,
8567
9037
  evidence,
@@ -8572,11 +9042,54 @@ async function reviewState(root, record, scope = "all") {
8572
9042
  ).digest("hex");
8573
9043
  return { reviewToken: reviewToken2, evidence, capture, committed, workingTreeAvailable: workingTree.available };
8574
9044
  }
8575
- async function previews(root, record, state) {
9045
+ var RETURNED_INSPECTIONS = 2;
9046
+ var MAX_PREVIEW_COMMITS = 20;
9047
+ async function diffSince(root, record, scope, diffFrom) {
9048
+ const baseline = (scope === "operative" ? effectiveDecision(record) : record).refreshedHash;
9049
+ const inspection = diffFrom === "lastReview" ? latestDecisionInspection(record, scope) : void 0;
9050
+ if (inspection && /^[a-f0-9]{40,64}$/.test(inspection.headHash)) {
9051
+ try {
9052
+ await exec2("git", ["cat-file", "-e", `${inspection.headHash}^{commit}`], { cwd: root });
9053
+ return { from: "inspection", commit: inspection.headHash, at: inspection.at };
9054
+ } catch {
9055
+ return {
9056
+ from: "baseline",
9057
+ commit: baseline,
9058
+ note: "The latest inspection's commit is not available locally; the diff starts at the evidence baseline."
9059
+ };
9060
+ }
9061
+ }
9062
+ return { from: "baseline", commit: baseline };
9063
+ }
9064
+ function trimInspections(record) {
9065
+ return record.version === 2 && (record.inspections?.length ?? 0) > RETURNED_INSPECTIONS ? { ...record, inspections: record.inspections.slice(-RETURNED_INSPECTIONS) } : record;
9066
+ }
9067
+ async function previews(root, state, since) {
8576
9068
  const access = await createFileAccess(root);
8577
9069
  const anchors = state.capture.anchors;
9070
+ const git4 = (args) => exec2("git", ["--literal-pathspecs", ...args, "--", ...anchors], {
9071
+ cwd: root,
9072
+ maxBuffer: 1024 * 1024
9073
+ }).then(({ stdout }) => stdout);
9074
+ const range = state.evidence.headHash === "unknown" ? null : [since.commit, state.evidence.headHash];
9075
+ let changedSince = [];
9076
+ let commits = [];
9077
+ if (range && anchors.length)
9078
+ try {
9079
+ changedSince = (await git4(["diff", "--name-only", "--no-renames", ...range])).split("\n").filter(Boolean);
9080
+ commits = (await git4([
9081
+ "log",
9082
+ "--oneline",
9083
+ "--no-decorate",
9084
+ "-n",
9085
+ String(MAX_PREVIEW_COMMITS + 1),
9086
+ `${range[0]}..${range[1]}`
9087
+ ])).split("\n").filter(Boolean);
9088
+ } catch {
9089
+ }
8578
9090
  const candidates = [
8579
9091
  .../* @__PURE__ */ new Set([
9092
+ ...changedSince,
8580
9093
  ...state.evidence.changedFiles,
8581
9094
  ...state.evidence.localChanges,
8582
9095
  ...anchors,
@@ -8587,7 +9100,6 @@ async function previews(root, record, state) {
8587
9100
  ];
8588
9101
  const files = [];
8589
9102
  const omittedFiles = [];
8590
- const diffPaths = [];
8591
9103
  for (const file of candidates) {
8592
9104
  if (files.length >= 6) {
8593
9105
  omittedFiles.push(file);
@@ -8603,37 +9115,46 @@ async function previews(root, record, state) {
8603
9115
  preview: source.content.slice(0, 2e3),
8604
9116
  totalLines: source.totalLines
8605
9117
  });
8606
- if (state.evidence.changedFiles.includes(file)) diffPaths.push(file);
8607
9118
  }
8608
9119
  let diff = null;
8609
9120
  let diffUnavailable;
8610
- if (state.evidence.historyAvailable && diffPaths.length) {
9121
+ if (range && changedSince.length) {
8611
9122
  try {
8612
- const { stdout } = await exec(
8613
- "git",
8614
- [
8615
- "--literal-pathspecs",
8616
- "diff",
8617
- "--no-ext-diff",
8618
- "--no-textconv",
8619
- "--no-color",
8620
- "--no-renames",
8621
- "-U3",
8622
- record.refreshedHash,
8623
- state.evidence.headHash,
8624
- "--",
8625
- ...diffPaths
8626
- ],
8627
- { cwd: root, maxBuffer: 1024 * 1024 }
8628
- );
9123
+ const stdout = await git4([
9124
+ "diff",
9125
+ "--no-ext-diff",
9126
+ "--no-textconv",
9127
+ "--no-color",
9128
+ "--no-renames",
9129
+ "-U3",
9130
+ ...range
9131
+ ]);
8629
9132
  diff = stdout.slice(0, 16e3);
8630
9133
  if (stdout.length > diff.length)
8631
9134
  diffUnavailable = "Diff preview was truncated; inspect the full diff before reviewing.";
8632
9135
  } catch {
8633
9136
  diffUnavailable = "Diff preview could not be read within its size bound; inspect the diff separately.";
8634
9137
  }
9138
+ } else if (range && !changedSince.length && since.commit !== state.evidence.headHash) {
9139
+ try {
9140
+ await exec2("git", ["cat-file", "-e", `${since.commit}^{commit}`], { cwd: root });
9141
+ } catch {
9142
+ diffUnavailable = "The diff base is not available locally; inspect the history separately.";
9143
+ }
8635
9144
  }
9145
+ const branch = changedSince.length ? await branchAttribution(root).catch(() => null) : null;
8636
9146
  return {
9147
+ diffSince: since,
9148
+ ...branch ? {
9149
+ attribution: {
9150
+ target: branch.target,
9151
+ mergeBase: branch.mergeBase,
9152
+ branchFiles: changedSince.filter((file) => branch.branchFiles.has(file)),
9153
+ targetFiles: changedSince.filter((file) => !branch.branchFiles.has(file))
9154
+ }
9155
+ } : {},
9156
+ commits: commits.slice(0, MAX_PREVIEW_COMMITS),
9157
+ ...commits.length > MAX_PREVIEW_COMMITS ? { commitsTruncated: true } : {},
8637
9158
  files,
8638
9159
  diff,
8639
9160
  diffUnavailable,
@@ -8669,10 +9190,12 @@ async function reviewDecision(root, input) {
8669
9190
  };
8670
9191
  const state = await reviewState(root, record, request.inspectionScope);
8671
9192
  const operativeDecision = effectiveDecision(record);
9193
+ const inspections2 = record.version === 2 ? record.inspections?.length ?? 0 : 0;
8672
9194
  return {
8673
9195
  status: "prepared",
8674
- record,
8675
- ...operativeDecision !== record && request.inspectionScope === "all" ? { operativeDecision } : {},
9196
+ record: trimInspections(record),
9197
+ ...inspections2 > RETURNED_INSPECTIONS ? { omittedInspections: inspections2 - RETURNED_INSPECTIONS } : {},
9198
+ ...operativeDecision !== record && request.inspectionScope === "all" ? { operativeDecision: trimInspections(operativeDecision) } : {},
8676
9199
  inspectionScope: request.inspectionScope,
8677
9200
  ...request.inspectionScope === "operative" ? {
8678
9201
  inspectionTarget: {
@@ -8693,8 +9216,8 @@ async function reviewDecision(root, input) {
8693
9216
  diagnostics,
8694
9217
  previews: await previews(
8695
9218
  root,
8696
- request.inspectionScope === "operative" ? operativeDecision : record,
8697
- state
9219
+ state,
9220
+ await diffSince(root, record, request.inspectionScope, request.diffFrom)
8698
9221
  ),
8699
9222
  hint: request.inspectionScope === "operative" ? "Inspect only the accepted inspectionTarget against its source and diff. Use the same inspectionScope: operative and token with action inspect. This reports no contradiction for that revision only; pending content remains unassessed and visible. Do not attest to the draft or infer human approval. If the accepted guidance is contradicted or uncertain, explain that instead of recording no contradiction." : "Inspect the proposed content, operativeDecision, sources, history, and code changes. A pending proposal leaves the prior accepted revision operative; accepting it replaces that revision, and retirement withdraws the entire decision including its proposal. Evidence covers both revisions' anchors. After inspecting the relevant code, action inspect with inspector, note and reviewToken records no_contradiction_found without human approval; it does not accept or reaffirm the decision. Only record acceptance or reaffirmation when the user or cited team review has authorized it. Supply that reviewer's identity, a reason, and this reviewToken. Do not invent identities or infer agreement from unchanged code. Acceptance and reaffirmation require committed anchor changes; retirement is available independently. Missing old history remains visible in the event even if a reviewer establishes a new baseline at HEAD. Saved reviews are local assertions for normal PR review, not authenticated approvals."
8700
9223
  };
@@ -8850,15 +9373,15 @@ init_provenance();
8850
9373
  init_decisions();
8851
9374
  init_paths();
8852
9375
  init_drift2();
8853
- import fs22 from "fs/promises";
9376
+ import fs23 from "fs/promises";
8854
9377
  import path27 from "path";
8855
- import { execFile as execFile5 } from "child_process";
8856
- import { promisify as promisify5 } from "util";
9378
+ import { execFile as execFile6 } from "child_process";
9379
+ import { promisify as promisify6 } from "util";
8857
9380
 
8858
9381
  // src/review/cochange.ts
8859
- import { execFile as execFile4 } from "child_process";
8860
- import { promisify as promisify4 } from "util";
8861
- var exec2 = promisify4(execFile4);
9382
+ import { execFile as execFile5 } from "child_process";
9383
+ import { promisify as promisify5 } from "util";
9384
+ var exec3 = promisify5(execFile5);
8862
9385
  var HISTORY_COMMITS = 1500;
8863
9386
  var MIN_FILE_COMMITS = 5;
8864
9387
  var MIN_SHARED_COMMITS = 4;
@@ -8866,11 +9389,11 @@ var MIN_COCHANGE_RATE = 0.6;
8866
9389
  var MAX_COMMIT_FILES = 30;
8867
9390
  async function buildMatrix(resolvedRoot) {
8868
9391
  try {
8869
- const { stdout: shallow } = await exec2("git", ["rev-parse", "--is-shallow-repository"], {
9392
+ const { stdout: shallow } = await exec3("git", ["rev-parse", "--is-shallow-repository"], {
8870
9393
  cwd: resolvedRoot
8871
9394
  });
8872
9395
  if (shallow.trim() === "true") return null;
8873
- const { stdout } = await exec2(
9396
+ const { stdout } = await exec3(
8874
9397
  "git",
8875
9398
  ["log", `-n${HISTORY_COMMITS}`, "--format=%x01", "--name-only", "-M"],
8876
9399
  { cwd: resolvedRoot, maxBuffer: 100 * 1024 * 1024 }
@@ -9642,11 +10165,11 @@ function summarizeEvidence(evidence) {
9642
10165
 
9643
10166
  // src/review/review.ts
9644
10167
  init_snapshot();
9645
- var exec3 = promisify5(execFile5);
10168
+ var exec4 = promisify6(execFile6);
9646
10169
  var MAX_ANALYZED_FILES = 50;
9647
10170
  async function resolveMergeBase(resolvedRoot, base, head) {
9648
10171
  try {
9649
- const { stdout } = await exec3("git", ["merge-base", base, head], {
10172
+ const { stdout } = await exec4("git", ["merge-base", base, head], {
9650
10173
  cwd: resolvedRoot
9651
10174
  });
9652
10175
  return stdout.trim() || null;
@@ -9657,7 +10180,7 @@ async function resolveMergeBase(resolvedRoot, base, head) {
9657
10180
  async function defaultBase(resolvedRoot) {
9658
10181
  for (const ref of ["origin/HEAD", "origin/main", "origin/master", "main"]) {
9659
10182
  try {
9660
- await exec3("git", ["rev-parse", "--verify", "--quiet", ref], {
10183
+ await exec4("git", ["rev-parse", "--verify", "--quiet", ref], {
9661
10184
  cwd: resolvedRoot
9662
10185
  });
9663
10186
  return ref;
@@ -9731,7 +10254,7 @@ async function computeReview(rootDir, base, options = {}) {
9731
10254
  analyzed,
9732
10255
  async (relPath) => {
9733
10256
  try {
9734
- await fs22.access(path27.join(resolvedRoot, relPath));
10257
+ await fs23.access(path27.join(resolvedRoot, relPath));
9735
10258
  return true;
9736
10259
  } catch {
9737
10260
  return false;
@@ -10398,7 +10921,7 @@ function createMcpServer() {
10398
10921
  const server = new McpServer(
10399
10922
  {
10400
10923
  name: "mason",
10401
- version: "0.22.0"
10924
+ version: "0.23.1"
10402
10925
  },
10403
10926
  {
10404
10927
  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 and inspect code evidence without claiming human approval; acceptance, reaffirmation, or retirement require an authorized review. 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. 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."
@@ -10541,7 +11064,7 @@ function createMcpServer() {
10541
11064
  `Short, specific headline (max ${TITLE_MAX_CHARS} characters). Generates a stable id on creation; revising the title keeps the existing id.`
10542
11065
  ),
10543
11066
  body: decisionBodySchema.describe(
10544
- `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.`
11067
+ `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. Name the functions, types, or settings the decision constrains in backticks, such as \`refresh()\`; drift notices use those names to judge whether a change is relevant.`
10545
11068
  ),
10546
11069
  category: z31.enum(["decision", "gotcha", "deprecation", "convention"]),
10547
11070
  files: z31.array(z31.string()).optional().describe(
@@ -10582,7 +11105,7 @@ function createMcpServer() {
10582
11105
  );
10583
11106
  server.tool(
10584
11107
  "review_decision",
10585
- "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. For accepted-guidance inspection use inspectionScope operative on both prepare and inspect; pending proposals remain unassessed. Default all scope covers both revisions. After source inspection, record inspect with inspector, note and token to report no contradiction found without human acceptance. For human review, prepare with all scope, then record accept, reaffirm, or retire with its 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.",
11108
+ "Prepare a decision review: returns the record and history with its latest two inspections (older ones are counted), any operative accepted revision, provenance, changes, and source, diff and commit previews since the latest valid inspection for both sets of anchors, and a reviewToken. For accepted-guidance inspection use inspectionScope operative on both prepare and inspect; pending proposals remain unassessed. Default all scope covers both revisions. After source inspection, record inspect with inspector, note and token to report no contradiction found without human acceptance. For human review, prepare with all scope, then record accept, reaffirm, or retire with its 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.",
10586
11109
  {
10587
11110
  dir: z31.string().describe("Absolute path to the project root directory"),
10588
11111
  id: z31.string().regex(/^[a-zA-Z0-9_-]+$/).describe("Decision id from get_context or save_decision"),
@@ -10592,6 +11115,9 @@ function createMcpServer() {
10592
11115
  inspectionScope: z31.enum(["all", "operative"]).optional().default("all").describe(
10593
11116
  "For prepare/inspect only: all covers the full record and both anchor sets (default); operative covers only an active accepted revision, leaving a pending draft unassessed. Use the same scope when submitting its token."
10594
11117
  ),
11118
+ diffFrom: z31.enum(["lastReview", "capture"]).optional().default("lastReview").describe(
11119
+ "For prepare only: lastReview previews changes since the latest inspection still valid for this record and scope (default); capture previews the full history since the evidence baseline."
11120
+ ),
10595
11121
  inspector: z31.string().trim().min(1).max(200).optional().describe(
10596
11122
  "Actual agent or person performing inspection; required for inspect. Does not claim human approval."
10597
11123
  ),
@@ -10679,7 +11205,7 @@ async function startMcpServer(onClose) {
10679
11205
  init_validation();
10680
11206
  init_store();
10681
11207
  init_storage();
10682
- import fs31 from "fs/promises";
11208
+ import fs32 from "fs/promises";
10683
11209
  import path37 from "path";
10684
11210
  import { spawn as spawn2 } from "child_process";
10685
11211
  import { randomUUID as randomUUID11 } from "crypto";
@@ -10690,19 +11216,19 @@ init_storage();
10690
11216
  import os5 from "os";
10691
11217
  import path33 from "path";
10692
11218
  import { fileURLToPath } from "url";
10693
- import fs26 from "fs/promises";
11219
+ import fs27 from "fs/promises";
10694
11220
  import { z as z33 } from "zod";
10695
11221
 
10696
11222
  // src/distribution/path.ts
10697
11223
  init_validation();
10698
- import fs25 from "fs/promises";
11224
+ import fs26 from "fs/promises";
10699
11225
  import os4 from "os";
10700
11226
  import path32 from "path";
10701
- import { execFile as execFile7 } from "child_process";
10702
- import { promisify as promisify7 } from "util";
10703
- import { createHash as createHash9, randomUUID as randomUUID7 } from "crypto";
11227
+ import { execFile as execFile8 } from "child_process";
11228
+ import { promisify as promisify8 } from "util";
11229
+ import { createHash as createHash10, randomUUID as randomUUID7 } from "crypto";
10704
11230
  import { z as z32 } from "zod";
10705
- var execute2 = promisify7(execFile7);
11231
+ var execute2 = promisify8(execFile8);
10706
11232
  var pathChangeSchema = z32.discriminatedUnion("kind", [
10707
11233
  z32.object({
10708
11234
  kind: z32.literal("profile"),
@@ -10759,7 +11285,7 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
10759
11285
  );
10760
11286
  let login = candidates[2];
10761
11287
  for (const file of candidates)
10762
- if (await fs25.lstat(file).then(
11288
+ if (await fs26.lstat(file).then(
10763
11289
  () => true,
10764
11290
  (error) => {
10765
11291
  if (hasErrorCode(error, "ENOENT")) return false;
@@ -10771,14 +11297,14 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
10771
11297
  }
10772
11298
  profiles2 = [path32.join(userHome, ".bashrc"), login];
10773
11299
  }
10774
- const marker = `# >>> mason PATH ${createHash9("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
11300
+ const marker = `# >>> mason PATH ${createHash10("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
10775
11301
  for (const profile of profiles2) {
10776
- const stat = await fs25.lstat(profile).catch((error) => {
11302
+ const stat = await fs26.lstat(profile).catch((error) => {
10777
11303
  if (hasErrorCode(error, "ENOENT")) return null;
10778
11304
  throw error;
10779
11305
  });
10780
- const file = stat?.isSymbolicLink() ? await fs25.realpath(profile) : path32.resolve(profile);
10781
- const original = await fs25.readFile(file).catch((error) => {
11306
+ const file = stat?.isSymbolicLink() ? await fs26.realpath(profile) : path32.resolve(profile);
11307
+ const original = await fs26.readFile(file).catch((error) => {
10782
11308
  if (hasErrorCode(error, "ENOENT") && !stat) return null;
10783
11309
  throw error;
10784
11310
  });
@@ -10797,8 +11323,8 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
10797
11323
  changes.push({ kind: "profile", file, block, created: original === null });
10798
11324
  await save(changes);
10799
11325
  }
10800
- await fs25.mkdir(path32.dirname(file), { recursive: true });
10801
- const handle = await fs25.open(file, original === null ? "ax+" : "a+", 384);
11326
+ await fs26.mkdir(path32.dirname(file), { recursive: true });
11327
+ const handle = await fs26.open(file, original === null ? "ax+" : "a+", 384);
10802
11328
  try {
10803
11329
  if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0)))
10804
11330
  throw new Error(`Shell profile changed during installation: ${profile}`);
@@ -10893,7 +11419,7 @@ async function readInstallation(home) {
10893
11419
  "Run this command from a standalone installation. npm users can upgrade with npm."
10894
11420
  );
10895
11421
  const record = recordSchema3.parse(raw);
10896
- if (record.home !== await fs26.realpath(home))
11422
+ if (record.home !== await fs27.realpath(home))
10897
11423
  throw new Error("Invalid standalone installation receipt.");
10898
11424
  return record;
10899
11425
  }
@@ -10929,9 +11455,9 @@ function newerStableVersion(candidate, installed) {
10929
11455
 
10930
11456
  // src/distribution/bundle.ts
10931
11457
  init_storage();
10932
- import fs27 from "fs/promises";
11458
+ import fs28 from "fs/promises";
10933
11459
  import path34 from "path";
10934
- import { createHash as createHash10 } from "crypto";
11460
+ import { createHash as createHash11 } from "crypto";
10935
11461
  import { z as z34 } from "zod";
10936
11462
  var targets = [
10937
11463
  "darwin-arm64",
@@ -10949,9 +11475,9 @@ var bundleSchema = z34.object({
10949
11475
  nodeVersion: z34.string(),
10950
11476
  files: z34.record(digest2)
10951
11477
  }).strict();
10952
- var sha256 = (bytes) => createHash10("sha256").update(bytes).digest("hex");
11478
+ var sha256 = (bytes) => createHash11("sha256").update(bytes).digest("hex");
10953
11479
  async function readBundle(root) {
10954
- const bytes = await fs27.readFile(await storePath(root, "bundle.json"));
11480
+ const bytes = await fs28.readFile(await storePath(root, "bundle.json"));
10955
11481
  const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
10956
11482
  const windows = manifest.target.startsWith("win32-");
10957
11483
  for (const file of Object.keys(manifest.files)) {
@@ -10981,7 +11507,7 @@ async function verifyBundle(root, expectedHash) {
10981
11507
  for (let i = 0; i < files.length; i += 8)
10982
11508
  await Promise.all(
10983
11509
  files.slice(i, i + 8).map(async (file) => {
10984
- if (sha256(await fs27.readFile(await storePath(root, file))) !== bundle.manifest.files[file])
11510
+ if (sha256(await fs28.readFile(await storePath(root, file))) !== bundle.manifest.files[file])
10985
11511
  throw new Error("Mason bundle checksum mismatch: " + file);
10986
11512
  })
10987
11513
  );
@@ -10992,7 +11518,7 @@ async function verifyBundle(root, expectedHash) {
10992
11518
  init_validation();
10993
11519
  init_store();
10994
11520
  init_storage();
10995
- import fs28 from "fs/promises";
11521
+ import fs29 from "fs/promises";
10996
11522
  import { unlinkSync } from "fs";
10997
11523
  import os6 from "os";
10998
11524
  import path35 from "path";
@@ -11011,11 +11537,11 @@ var leaseSchema = z35.object({
11011
11537
 
11012
11538
  // src/distribution/install.ts
11013
11539
  init_validation();
11014
- import fs30 from "fs/promises";
11540
+ import fs31 from "fs/promises";
11015
11541
  import os8 from "os";
11016
11542
  import path36 from "path";
11017
- import { execFile as execFile8, spawn } from "child_process";
11018
- import { promisify as promisify8 } from "util";
11543
+ import { execFile as execFile9, spawn } from "child_process";
11544
+ import { promisify as promisify9 } from "util";
11019
11545
  import { randomUUID as randomUUID10 } from "crypto";
11020
11546
  init_storage();
11021
11547
  init_store();
@@ -11023,14 +11549,14 @@ init_store();
11023
11549
  // src/distribution/sessions.ts
11024
11550
  init_validation();
11025
11551
  init_storage();
11026
- import fs29 from "fs/promises";
11552
+ import fs30 from "fs/promises";
11027
11553
  import os7 from "os";
11028
11554
  import { randomUUID as randomUUID9 } from "crypto";
11029
11555
  import { z as z36 } from "zod";
11030
11556
  var leaseSchema2 = z36.object({ pid: z36.number().int().positive(), host: z36.string() });
11031
11557
  async function hasRunningMcp(home) {
11032
11558
  const directory3 = await storePath(home, "sessions");
11033
- const files = await fs29.readdir(directory3).catch((error) => {
11559
+ const files = await fs30.readdir(directory3).catch((error) => {
11034
11560
  if (hasErrorCode(error, "ENOENT")) return [];
11035
11561
  throw error;
11036
11562
  });
@@ -11048,7 +11574,7 @@ async function hasRunningMcp(home) {
11048
11574
  } catch (error) {
11049
11575
  if (error.code !== "ESRCH") return true;
11050
11576
  }
11051
- await fs29.rm(await storePath(home, relative), { force: true });
11577
+ await fs30.rm(await storePath(home, relative), { force: true });
11052
11578
  } catch {
11053
11579
  return true;
11054
11580
  }
@@ -11059,7 +11585,7 @@ async function registerMcp(home) {
11059
11585
  const name = `sessions/${randomUUID9()}.json`;
11060
11586
  await writeStoreJson(home, name, { pid: process.pid, host: os7.hostname() });
11061
11587
  return async () => {
11062
- await fs29.rm(await storePath(home, name), { force: true }).catch(() => {
11588
+ await fs30.rm(await storePath(home, name), { force: true }).catch(() => {
11063
11589
  });
11064
11590
  };
11065
11591
  }
@@ -11072,10 +11598,10 @@ var launcherNames = ["mason", "mason.cmd", "mason.ps1", "mason-launcher.ps1"];
11072
11598
  async function replace(file, content2, mode = 384) {
11073
11599
  const temp = file + ".tmp-" + randomUUID10();
11074
11600
  try {
11075
- await fs30.writeFile(temp, content2, { mode, flag: "wx" });
11076
- await fs30.rename(temp, file);
11601
+ await fs31.writeFile(temp, content2, { mode, flag: "wx" });
11602
+ await fs31.rename(temp, file);
11077
11603
  } finally {
11078
- await fs30.rm(temp, { force: true });
11604
+ await fs31.rm(temp, { force: true });
11079
11605
  }
11080
11606
  }
11081
11607
  function assertUpdatePolicy(record, version, revision) {
@@ -11089,19 +11615,19 @@ async function installLocked2(home, source, bundle, progress, options, alreadySt
11089
11615
  if (options.stageOnly)
11090
11616
  assertUpdatePolicy(previous, bundle.manifest.version, options.policyRevision);
11091
11617
  if (!previous)
11092
- for (const file of await fs30.readdir(home)) {
11618
+ for (const file of await fs31.readdir(home)) {
11093
11619
  if (file === ".install-lock") continue;
11094
11620
  const directory3 = path36.join(home, file);
11095
- const stat = await fs30.lstat(directory3);
11096
- if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs30.readdir(directory3)).length) {
11621
+ const stat = await fs31.lstat(directory3);
11622
+ if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs31.readdir(directory3)).length) {
11097
11623
  throw new Error("Mason installation directory is not empty and has no ownership receipt.");
11098
11624
  }
11099
11625
  }
11100
11626
  const selectedBin = path36.resolve(
11101
11627
  process.env.MASON_BIN_DIR ?? previous?.bin ?? (process.platform === "win32" ? path36.join(home, "bin") : path36.join(os8.homedir(), ".local/bin"))
11102
11628
  );
11103
- await fs30.mkdir(selectedBin, { recursive: true });
11104
- const bin = await fs30.realpath(selectedBin);
11629
+ await fs31.mkdir(selectedBin, { recursive: true });
11630
+ const bin = await fs31.realpath(selectedBin);
11105
11631
  if (previous && (previous.home !== home || previous.bin !== bin))
11106
11632
  throw new Error("Installation paths differ from the existing Mason receipt.");
11107
11633
  const id = bundle.manifestHash.slice(0, 24);
@@ -11145,7 +11671,7 @@ exec ${shQuote(path36.join(destination, "node"))} ${shQuote(path36.join(destinat
11145
11671
  };
11146
11672
  for (const [name, content2] of Object.entries(launchers)) {
11147
11673
  const file = await storePath(bin, name);
11148
- const old = await fs30.readFile(file, "utf8").catch((error) => {
11674
+ const old = await fs31.readFile(file, "utf8").catch((error) => {
11149
11675
  if (hasErrorCode(error, "ENOENT")) return null;
11150
11676
  throw error;
11151
11677
  });
@@ -11157,7 +11683,7 @@ exec ${shQuote(path36.join(destination, "node"))} ${shQuote(path36.join(destinat
11157
11683
  );
11158
11684
  const checkRetired = async (name, expected) => {
11159
11685
  if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
11160
- const actual = await fs30.readFile(await storePath(bin, name), "utf8").catch((error) => {
11686
+ const actual = await fs31.readFile(await storePath(bin, name), "utf8").catch((error) => {
11161
11687
  if (hasErrorCode(error, "ENOENT")) return null;
11162
11688
  throw error;
11163
11689
  });
@@ -11200,7 +11726,7 @@ exec ${shQuote(path36.join(destination, "node"))} ${shQuote(path36.join(destinat
11200
11726
  await replace(await storePath(bin, name, true), content2, 493);
11201
11727
  for (const [name, expected] of retired) {
11202
11728
  await checkRetired(name, expected);
11203
- await fs30.rm(await storePath(bin, name), { force: true });
11729
+ await fs31.rm(await storePath(bin, name), { force: true });
11204
11730
  }
11205
11731
  await writeStoreJson(home, "install.json", receipt);
11206
11732
  } catch (error) {
@@ -11233,7 +11759,7 @@ exec ${shQuote(path36.join(destination, "node"))} ${shQuote(path36.join(destinat
11233
11759
  };
11234
11760
  }
11235
11761
  async function copyBundle(home, source, destination, bundle) {
11236
- const exists = await fs30.lstat(destination).then(
11762
+ const exists = await fs31.lstat(destination).then(
11237
11763
  () => true,
11238
11764
  (error) => {
11239
11765
  if (hasErrorCode(error, "ENOENT")) return false;
@@ -11245,16 +11771,16 @@ async function copyBundle(home, source, destination, bundle) {
11245
11771
  return;
11246
11772
  }
11247
11773
  const stage = await storePath(home, "versions/.install-" + randomUUID10(), true);
11248
- await fs30.mkdir(stage);
11774
+ await fs31.mkdir(stage);
11249
11775
  try {
11250
11776
  for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
11251
11777
  const from = await storePath(source, file), to = await storePath(stage, file, true);
11252
- await fs30.copyFile(from, to);
11253
- await fs30.chmod(to, (await fs30.stat(from)).mode & 511);
11778
+ await fs31.copyFile(from, to);
11779
+ await fs31.chmod(to, (await fs31.stat(from)).mode & 511);
11254
11780
  }
11255
11781
  await verifyBundle(stage, bundle.manifestHash);
11256
11782
  try {
11257
- await fs30.rename(stage, destination);
11783
+ await fs31.rename(stage, destination);
11258
11784
  } catch (error) {
11259
11785
  try {
11260
11786
  await verifyBundle(destination, bundle.manifestHash);
@@ -11263,7 +11789,7 @@ async function copyBundle(home, source, destination, bundle) {
11263
11789
  }
11264
11790
  }
11265
11791
  } finally {
11266
- await fs30.rm(stage, { recursive: true, force: true });
11792
+ await fs31.rm(stage, { recursive: true, force: true });
11267
11793
  }
11268
11794
  }
11269
11795
  async function prepareStandaloneMcp(home) {