mason-context 0.17.4 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ if (!import.meta.url.endsWith("/mason-runtime.js")) await import("./mason-runtime.js").then(m => m.registerRuntime());
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
5
  var __esm = (fn, res) => function __init() {
@@ -305,18 +306,34 @@ var init_storage = __esm({
305
306
  }
306
307
  });
307
308
 
308
- // src/automation/store.ts
309
+ // src/utils/store-lock.ts
309
310
  import fs3 from "fs/promises";
310
311
  import os from "os";
311
- import { z } from "zod";
312
- function parseState(raw) {
312
+ async function lockDiagnostic(file) {
313
+ let reason2;
313
314
  try {
314
- return stateSchema.parse(raw);
315
+ const owner = JSON.parse(await fs3.readFile(file, "utf8"));
316
+ if (!owner || typeof owner.host !== "string" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
317
+ reason2 = "Lock owner data is incomplete or malformed.";
318
+ } else if (owner.host !== os.hostname()) {
319
+ reason2 = "The lock belongs to another host; its process cannot be checked locally.";
320
+ } else {
321
+ try {
322
+ process.kill(owner.pid, 0);
323
+ reason2 = `Local owner PID ${owner.pid} is still running.`;
324
+ } catch (error) {
325
+ reason2 = error.code === "ESRCH" ? `Local owner PID ${owner.pid} has exited, but lock recovery has not completed.` : `Local owner PID ${owner.pid} could not be checked.`;
326
+ }
327
+ }
315
328
  } catch (error) {
316
- throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
329
+ reason2 = error instanceof SyntaxError ? "Lock owner data is incomplete or malformed." : "Lock owner data could not be read; the lock may have changed.";
317
330
  }
331
+ if (await fs3.lstat(file + ".reclaim").then(() => true, () => false)) {
332
+ reason2 += " A recovery guard also needs inspection: " + file + ".reclaim.";
333
+ }
334
+ return reason2 + " Retry if a writer is active. For manual recovery, stop all Mason writers for this checkout, confirm they have exited (including other hosts), then remove only confirmed abandoned lock and lock.reclaim files. See docs/reference.md#snapshot-repairs-and-verification.";
318
335
  }
319
- async function withLock(root, directory, run, waitMs = 5e3) {
336
+ async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason store") {
320
337
  const file = await storePath(root, directory + "/lock", true);
321
338
  const deadline = Date.now() + waitMs;
322
339
  let handle;
@@ -359,7 +376,7 @@ async function withLock(root, directory, run, waitMs = 5e3) {
359
376
  }
360
377
  } catch {
361
378
  }
362
- if (Date.now() >= deadline) throw new Error("Automation is busy or its lock needs inspection: " + file);
379
+ if (Date.now() >= deadline) throw new Error(label + " is busy or its lock needs inspection: " + file + ". " + await lockDiagnostic(file));
363
380
  await new Promise((resolve) => setTimeout(resolve, 40));
364
381
  }
365
382
  }
@@ -370,11 +387,30 @@ async function withLock(root, directory, run, waitMs = 5e3) {
370
387
  await fs3.unlink(file);
371
388
  }
372
389
  }
390
+ var init_store_lock = __esm({
391
+ "src/utils/store-lock.ts"() {
392
+ "use strict";
393
+ init_storage();
394
+ }
395
+ });
396
+
397
+ // src/automation/store.ts
398
+ import { z } from "zod";
399
+ function parseState(raw) {
400
+ try {
401
+ return stateSchema.parse(raw);
402
+ } catch (error) {
403
+ throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
404
+ }
405
+ }
406
+ function withLock(root, directory, run, waitMs = 5e3) {
407
+ return withStoreLock(root, directory, run, waitMs, "Automation");
408
+ }
373
409
  var hostSchema, events, stateSchema;
374
410
  var init_store = __esm({
375
411
  "src/automation/store.ts"() {
376
412
  "use strict";
377
- init_storage();
413
+ init_store_lock();
378
414
  hostSchema = z.enum(["claude", "codex"]);
379
415
  events = ["session_start", "turn_start", "before_tool", "after_tool", "task_end"];
380
416
  stateSchema = z.object({
@@ -1204,6 +1240,14 @@ var init_scope = __esm({
1204
1240
  }
1205
1241
  });
1206
1242
 
1243
+ // src/snapshot/lock.ts
1244
+ var init_lock = __esm({
1245
+ "src/snapshot/lock.ts"() {
1246
+ "use strict";
1247
+ init_store_lock();
1248
+ }
1249
+ });
1250
+
1207
1251
  // src/test-map.ts
1208
1252
  import path9 from "path";
1209
1253
  async function buildTestMap(dir) {
@@ -1313,6 +1357,7 @@ var init_snapshot = __esm({
1313
1357
  init_files();
1314
1358
  init_files();
1315
1359
  init_storage();
1360
+ init_lock();
1316
1361
  init_paths();
1317
1362
  init_test_map();
1318
1363
  repoPath = z2.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
@@ -1320,6 +1365,7 @@ var init_snapshot = __esm({
1320
1365
  refreshedHash: z2.string().optional(),
1321
1366
  verifiedAt: z2.string().optional(),
1322
1367
  verifiedHash: z2.string().optional(),
1368
+ verificationToken: z2.string().optional(),
1323
1369
  verificationFailed: z2.boolean().optional(),
1324
1370
  verificationNote: z2.string().optional()
1325
1371
  };
@@ -1942,6 +1988,10 @@ var init_release_metadata = __esm({
1942
1988
  });
1943
1989
 
1944
1990
  // src/audit/checks/deps-changed.ts
1991
+ function hasDependencyContent(content2) {
1992
+ const text2 = content2.replace(/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g, "").replace(/<!--[\s\S]*?-->/g, "");
1993
+ return /\b(?:dependenc(?:y|ies)|librar(?:y|ies)|frameworks?|tech(?:nology)? stack|prerequisites|requirements)\b/i.test(text2) || /\b(?:package\.json|build\.gradle(?:\.kts)?|settings\.gradle(?:\.kts)?|libs\.versions\.toml|Cargo\.toml|go\.mod|pyproject\.toml|requirements\.txt|Gemfile|composer\.json)\b/i.test(text2) || /\b(?:node(?:\.js)?|npm|pnpm|yarn|bun|deno|python|ruby|rust|go|java|jdk|kotlin|gradle|swift|php|react|vue|angular|compose|ktor|room)\s*(?:version\s*)?[`*:=>~^v\s-]*\d+(?:\.\d+)*\b/i.test(text2) || /\b(?:npm|pnpm|yarn|pip3?|cargo|gem|composer)\s+(?:install|add|require)\b/i.test(text2);
1994
+ }
1945
1995
  function manifestPathspecs(doc) {
1946
1996
  const scope = documentScope(doc);
1947
1997
  if (scope === ".") return MANIFEST_PATHSPECS;
@@ -1968,6 +2018,19 @@ async function checkDepsChanged(ctx) {
1968
2018
  reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
1969
2019
  });
1970
2020
  }
2021
+ let content2 = doc.content;
2022
+ if (doc.dirty) {
2023
+ try {
2024
+ content2 = (await inspectionGit(
2025
+ ["show", `${doc.lastCommit.hash}:${doc.path}`],
2026
+ { cwd: ctx.root, maxBuffer: 10 * 1024 * 1024, timeout: 1e4 }
2027
+ )).stdout;
2028
+ } catch {
2029
+ result.skipped.push({ check: "deps-changed", doc: doc.path, reason: `${doc.path}: committed dependency content is unavailable` });
2030
+ continue;
2031
+ }
2032
+ }
2033
+ if (!hasDependencyContent(content2)) continue;
1971
2034
  const range = await commitsTouchingSince(
1972
2035
  ctx.root,
1973
2036
  doc.lastCommit.hash,
@@ -2013,6 +2076,7 @@ var init_deps_changed = __esm({
2013
2076
  init_docs();
2014
2077
  init_release_metadata();
2015
2078
  init_git();
2079
+ init_inspection();
2016
2080
  init_checks();
2017
2081
  MANIFEST_COMMITS_CAP = 10;
2018
2082
  MANIFEST_PATHSPECS = [
@@ -2908,7 +2972,7 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
2908
2972
  return {
2909
2973
  ...base,
2910
2974
  status: "review-required",
2911
- reason: "An audit cannot establish that this advisory was reviewed. Retain its original evidence and report a separate assessment; editing or committing the doc is not approval."
2975
+ reason: (now ? "The current check still reports this advisory. " : finding.type === "deps-changed" ? "The current check no longer reports this condition; the historical advisory remains awaiting assessment. " : "The current check no longer reports the original finding; retained evidence still requires assessment. ") + "An audit cannot establish that this advisory was reviewed. Retain its original evidence and report a separate assessment; editing or committing the doc is not approval."
2912
2976
  };
2913
2977
  }
2914
2978
  return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
@@ -3086,7 +3150,7 @@ async function collectInputs(root) {
3086
3150
  git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
3087
3151
  git3(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
3088
3152
  ]) : ["", ""];
3089
- const common = [5, engineVersion, head, shallow, replacements, docContents];
3153
+ const common = [6, engineVersion, head, shallow, replacements, docContents];
3090
3154
  const keys = {
3091
3155
  "deleted-reference": hash([common, claims, docStatus]),
3092
3156
  "new-module": hash([common, modules]),
@@ -3146,7 +3210,7 @@ var init_evidence = __esm({
3146
3210
  init_dead_command();
3147
3211
  init_storage();
3148
3212
  init_advisory_review();
3149
- engineVersion = true ? "0.17.4" : "development";
3213
+ engineVersion = true ? "0.19.0" : "development";
3150
3214
  hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
3151
3215
  content = readAuditInput;
3152
3216
  cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
@@ -3360,13 +3424,17 @@ async function observeReadOnlyTool(dir, event) {
3360
3424
  }
3361
3425
  function summarize(report) {
3362
3426
  const open = report.findings.filter((f) => f.status !== "resolved");
3427
+ const onlyReview = report.status === "incomplete" && !report.diagnostics.length && !report.counts.unresolved && !report.counts.unverified && report.counts["review-required"] > 0;
3363
3428
  return [
3364
- `Mason: ${report.status}; ${report.counts.unresolved} unresolved, ${report.counts["review-required"]} need review, ${report.counts.unverified} unverified.`,
3365
- ...open.slice(0, 4).map((f) => `[${f.status}] ${cleanText(f.original.anchor.doc)}: ${cleanText(f.original.message)}`),
3429
+ onlyReview ? `Mason: ${report.counts["review-required"]} advisor${report.counts["review-required"] === 1 ? "y" : "ies"} awaiting review; no unresolved issues.` : `Mason: ${report.status}; ${report.counts.unresolved} unresolved, ${report.counts["review-required"]} need review, ${report.counts.unverified} unverified.`,
3430
+ ...open.slice(0, 4).map((f) => {
3431
+ const detail = f.current ? f.current.message : f.status === "review-required" ? `${f.original.type}: current check no longer reports ${f.original.type === "deps-changed" ? "this condition" : "the original finding"}; historical evidence awaits assessment.` : `Original evidence: ${f.original.message}. ${f.reason}`;
3432
+ return `[${f.status}] ${cleanText(f.original.anchor.doc)}: ${cleanText(detail)}`;
3433
+ }),
3366
3434
  ...open.length > 4 ? [`${open.length - 4} more findings in the report.`] : [],
3367
3435
  ...report.diagnostics.slice(0, 2).map(cleanText),
3368
3436
  `Evidence: ${report.reportPath}. Resume/check with mason_automation(action: "check") or mason-auto check.`,
3369
- "Keep original evidence. Address findings relevant to the authorized task; report unrelated findings and unresolved advisories without approving them."
3437
+ "Keep original evidence. Address findings relevant to the authorized task; report unrelated findings with a brief scope reason and suggested follow-up. Use review_advisory for authorized assessments; otherwise leave advisories open without approving them."
3370
3438
  ].join("\n");
3371
3439
  }
3372
3440
  async function loadState(ws) {
@@ -3590,7 +3658,7 @@ async function automate(dir, event) {
3590
3658
  if (event.event === "after_tool" && event.mutating) {
3591
3659
  session.mutationObserved = true;
3592
3660
  if (!event.toolId || !session.pending[event.toolId]) {
3593
- const gap = "A tool completed without an observed matching pre-tool capture; pre-edit coverage is unknown.";
3661
+ const gap = "A tool completed without an observed matching pre-tool capture in this checkout; pre-edit coverage is unknown. The tool may have changed checkouts; captures are kept separately for each worktree.";
3594
3662
  if (!session.coverageGaps.includes(gap)) session.coverageGaps.push(gap);
3595
3663
  }
3596
3664
  if (event.toolId) delete session.pending[event.toolId];
@@ -3733,11 +3801,39 @@ var init_hook_root = __esm({
3733
3801
 
3734
3802
  // src/setup/model.ts
3735
3803
  import { z as z11 } from "zod";
3804
+ import fs16 from "fs/promises";
3805
+ import path23 from "path";
3736
3806
  async function loadSetup(root) {
3737
3807
  const raw = await readStoreJson(root, SETUP_PATH);
3738
3808
  if (raw === null) return null;
3739
3809
  return setupSchema.parse(raw);
3740
3810
  }
3811
+ async function effectiveSetup(root) {
3812
+ const local = await loadSetup(root);
3813
+ if (local) return { root, setup: local };
3814
+ const common = await fs16.realpath(path23.resolve(root, (await git3(root, "rev-parse", "--git-common-dir")).trim()));
3815
+ const own = await fs16.realpath((await git3(root, "rev-parse", "--absolute-git-dir")).trim());
3816
+ if (common === own) return { root, setup: null };
3817
+ const primary = (await git3(root, "worktree", "list", "--porcelain", "-z")).split("\0\0")[0].split("\0");
3818
+ const candidate = primary.find((field) => field.startsWith("worktree "))?.slice(9);
3819
+ if (!candidate || primary.includes("bare")) return { root, setup: null };
3820
+ let inheritedRoot;
3821
+ try {
3822
+ inheritedRoot = await fs16.realpath((await git3(candidate, "rev-parse", "--show-toplevel")).trim());
3823
+ } catch (error) {
3824
+ if (/must be run in a work tree/.test(String(error.stderr))) return { root, setup: null };
3825
+ throw error;
3826
+ }
3827
+ const inheritedCommon = await fs16.realpath(path23.resolve(
3828
+ inheritedRoot,
3829
+ (await git3(inheritedRoot, "rev-parse", "--git-common-dir")).trim()
3830
+ ));
3831
+ if (inheritedCommon !== common) throw new Error("Linked worktree setup does not belong to the same Git repository.");
3832
+ const setup = await loadSetup(inheritedRoot);
3833
+ const inactive = await readStoreJson(root, INACTIVE_HOSTS_PATH);
3834
+ if (inactive && setup) for (const host of inactiveHostsSchema.parse(inactive).hosts) delete setup.hosts[host];
3835
+ return { root: inheritedRoot, setup };
3836
+ }
3741
3837
  async function loadSetupReceipt(root, directory, host) {
3742
3838
  const raw = await readStoreJson(root, directory + "/setup-" + host + ".json");
3743
3839
  if (raw === null) return null;
@@ -3745,10 +3841,11 @@ async function loadSetupReceipt(root, directory, host) {
3745
3841
  if (receipt.root !== root || receipt.host !== host) throw new Error("Setup receipt belongs to another installation.");
3746
3842
  return receipt;
3747
3843
  }
3748
- var setupHostSchema, setupSchema, SETUP_PATH, receiptSchema;
3844
+ var setupHostSchema, setupSchema, SETUP_PATH, INACTIVE_HOSTS_PATH, inactiveHostsSchema, receiptSchema;
3749
3845
  var init_model = __esm({
3750
3846
  "src/setup/model.ts"() {
3751
3847
  "use strict";
3848
+ init_evidence();
3752
3849
  init_storage();
3753
3850
  setupHostSchema = z11.object({
3754
3851
  configuredVersion: z11.string(),
@@ -3762,6 +3859,8 @@ var init_model = __esm({
3762
3859
  claude: setupHostSchema.optional()
3763
3860
  }) });
3764
3861
  SETUP_PATH = ".mason/local/setup.json";
3862
+ INACTIVE_HOSTS_PATH = ".mason/local/inactive-hosts.json";
3863
+ inactiveHostsSchema = z11.object({ version: z11.literal(1), hosts: z11.array(z11.enum(["claude", "codex"])) });
3765
3864
  receiptSchema = z11.object({
3766
3865
  version: z11.literal(1),
3767
3866
  host: z11.enum(["codex", "claude"]),
@@ -3788,7 +3887,7 @@ __export(launcher_exports, {
3788
3887
  });
3789
3888
  import { execFile as execFile2 } from "child_process";
3790
3889
  import { promisify as promisify2 } from "util";
3791
- import fs16 from "fs/promises";
3890
+ import fs17 from "fs/promises";
3792
3891
  function globalCommand(args, platform = process.platform) {
3793
3892
  return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args] } : { command: "mason", args };
3794
3893
  }
@@ -3808,8 +3907,8 @@ async function installedCommand() {
3808
3907
  }
3809
3908
  }
3810
3909
  async function prepareLaunch(host, options = {}) {
3811
- const root = await fs16.realpath((await git3(options.dir ?? process.cwd(), "rev-parse", "--show-toplevel")).trim());
3812
- const entry = (await loadSetup(root))?.hosts[host];
3910
+ const root = await fs17.realpath((await git3(options.dir ?? process.cwd(), "rev-parse", "--show-toplevel")).trim());
3911
+ const entry = (await effectiveSetup(root)).setup?.hosts[host];
3813
3912
  process.env.MASON_SETUP_ROOT = root;
3814
3913
  process.env.MASON_SETUP_HOST = host;
3815
3914
  delete process.env.MASON_SETUP_REVISION;
@@ -3831,7 +3930,7 @@ var init_launcher = __esm({
3831
3930
  "use strict";
3832
3931
  init_evidence();
3833
3932
  init_model();
3834
- executingVersion = () => true ? "0.17.4" : "development";
3933
+ executingVersion = () => true ? "0.19.0" : "development";
3835
3934
  mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
3836
3935
  hookCommand = (host) => `mason --setup-host ${host} auto`;
3837
3936
  }
@@ -3844,7 +3943,7 @@ __export(observations_exports, {
3844
3943
  observeActivation: () => observeActivation,
3845
3944
  readObservation: () => readObservation
3846
3945
  });
3847
- import fs17 from "fs/promises";
3946
+ import fs18 from "fs/promises";
3848
3947
  import { z as z12 } from "zod";
3849
3948
  function observationPath(directory, host, revision, version = executingVersion()) {
3850
3949
  return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
@@ -3861,15 +3960,15 @@ async function observeActivation(dir, event, options = {}) {
3861
3960
  if (host !== "codex" && host !== "claude" || !process.env.MASON_SETUP_ROOT) return null;
3862
3961
  try {
3863
3962
  if (!revision) {
3864
- if ((await loadSetup(process.env.MASON_SETUP_ROOT))?.hosts[host]) {
3963
+ if ((await effectiveSetup(process.env.MASON_SETUP_ROOT)).setup?.hosts[host]) {
3865
3964
  return "Mason setup is now present. Restart the assistant to connect this MCP session to the configured integration; activation has not been established by this session.";
3866
3965
  }
3867
3966
  return inactiveSetupMessage();
3868
3967
  }
3869
3968
  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];
3870
- const ws = capturedDirectory ? { root: await fs17.realpath(dir), directory: capturedDirectory } : await workspace(dir);
3871
- if (ws.root !== await fs17.realpath(process.env.MASON_SETUP_ROOT)) return null;
3872
- const setup = await loadSetup(ws.root);
3969
+ const ws = capturedDirectory ? { root: await fs18.realpath(dir), directory: capturedDirectory } : await workspace(dir);
3970
+ if (ws.root !== await fs18.realpath(process.env.MASON_SETUP_ROOT)) return null;
3971
+ const { setup } = await effectiveSetup(ws.root);
3873
3972
  if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
3874
3973
  const directory = ws.directory + "/activation";
3875
3974
  await withLock(ws.root, directory, async () => {
@@ -4046,8 +4145,8 @@ __export(files_exports, {
4046
4145
  managedBlock: () => managedBlock,
4047
4146
  readText: () => readText
4048
4147
  });
4049
- import fs18 from "fs/promises";
4050
- import path23 from "path";
4148
+ import fs19 from "fs/promises";
4149
+ import path24 from "path";
4051
4150
  import { randomUUID as randomUUID5 } from "crypto";
4052
4151
  async function readText(root, file) {
4053
4152
  try {
@@ -4076,14 +4175,14 @@ async function applyEdit(root, edit) {
4076
4175
  if (edit.after === null) {
4077
4176
  const file2 = await storePath(root, edit.path);
4078
4177
  if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
4079
- await fs18.unlink(file2);
4178
+ await fs19.unlink(file2);
4080
4179
  return true;
4081
4180
  }
4082
4181
  const file = await storePath(root, edit.path, true);
4083
- const temporary = path23.join(path23.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
4182
+ const temporary = path24.join(path24.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
4084
4183
  try {
4085
- const mode = await fs18.stat(file).then((s) => s.mode & 511, () => 384);
4086
- const handle = await fs18.open(temporary, "wx", mode);
4184
+ const mode = await fs19.stat(file).then((s) => s.mode & 511, () => 384);
4185
+ const handle = await fs19.open(temporary, "wx", mode);
4087
4186
  try {
4088
4187
  await handle.writeFile(edit.after, "utf8");
4089
4188
  await handle.sync();
@@ -4091,10 +4190,10 @@ async function applyEdit(root, edit) {
4091
4190
  await handle.close();
4092
4191
  }
4093
4192
  if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
4094
- await fs18.rename(temporary, file);
4193
+ await fs19.rename(temporary, file);
4095
4194
  return true;
4096
4195
  } finally {
4097
- await fs18.rm(temporary, { force: true });
4196
+ await fs19.rm(temporary, { force: true });
4098
4197
  }
4099
4198
  }
4100
4199
  var init_files2 = __esm({
@@ -4200,7 +4299,7 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
4200
4299
  - Before editing a file \u2192 \`get_impact\` to check references, tests, and historical change partners.
4201
4300
  - Investigation or correction reveals a reusable project lesson \u2192 \`save_decision\` before finishing. Capture only the lasting constraint and reason, with anchors and known sources, owner, and recorder. This creates a proposal; never infer approval. Preserve uncertain causes and unknown attribution. Route project lessons here; keep personal preferences in agent memory. Skip code summaries, current checkout status, temporary task permissions, session trivia, and secrets. No qualifying lesson means no record.
4202
4301
  - Before saving, compare the lesson with matching \`get_context\` records and any pending proposal. If new evidence changes the same lesson's assumptions, scope, or recommended action, revise it with \`save_decision(id: existingId)\`, including when it is accepted. Preserve supported rationale and sources, replace obsolete instructions, and add the new evidence. Create a separate record for a genuinely distinct lesson; skip unchanged restatements. Revision preserves history and leaves the accepted version operative until authorized review.
4203
- - Consult trust metadata before relying on entries: unknown or changed freshness requires inspection, and failed verification means the description must be corrected. Check approval too: proposals are suggestions, legacy records are unreviewed, and accepted decisions are recorded constraints subject to freshness checks. An accepted revision remains operative while a pending proposal is reviewed; keep both versions and their freshness distinct.
4302
+ - Consult trust metadata before relying on entries: unknown or changed freshness requires inspection, stale or unknown map verification requires a fresh evidence review, and a recorded failed verdict means the description must be corrected. Check approval too: proposals are suggestions, legacy records are unreviewed, and accepted decisions are recorded constraints subject to freshness checks. An accepted revision remains operative while a pending proposal is reviewed; keep both versions and their freshness distinct.
4204
4303
  - Asked to review or re-verify a decision \u2192 \`review_decision\` first to inspect content, sources, history, and code changes. Record acceptance, reaffirmation, or retirement only when authorized by the user or a cited team review, with the actual reviewer and reason. Never infer approval from unchanged code. Review and commit the local record through the normal project workflow.
4205
4304
  - For an architectural overview, use \`get_snapshot\` if a map is available. If \`map.status\` is missing or invalid, use available decisions and source evidence; do not start building a map unless requested.
4206
4305
  - \`mason_init\` returns documentation audit and committed-diff review results, plus a short setup guide. Pass \`evidence\` with local CI manifest paths to include test and analysis results; the CLI equivalent is \`mason-review --evidence <manifest>\`. State skipped, unavailable, stale, or unknown checks explicitly. Related accepted decisions identify review context, not proven violations.
@@ -4502,6 +4601,8 @@ __export(teardown_exports, {
4502
4601
  teardownProject: () => teardownProject
4503
4602
  });
4504
4603
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
4604
+ import fs20 from "fs/promises";
4605
+ import path25 from "path";
4505
4606
  import { parse as parse3, stringify as stringify2 } from "smol-toml";
4506
4607
  import fg6 from "fast-glob";
4507
4608
  import { z as z18 } from "zod";
@@ -4523,6 +4624,11 @@ async function planTeardown(root, selected) {
4523
4624
  const ownership = ownershipBefore === null ? { version: 1, files: {} } : ownershipSchema.parse(JSON.parse(ownershipBefore));
4524
4625
  const setupBefore = await read(SETUP_PATH);
4525
4626
  const setup = setupBefore === null ? null : setupSchema.parse(JSON.parse(setupBefore));
4627
+ const linked = (await workspace(root)).gitDir !== await fs20.realpath(
4628
+ path25.resolve(root, (await git3(root, "rev-parse", "--git-common-dir")).trim())
4629
+ );
4630
+ const inactiveBefore = linked && !setup ? await read(INACTIVE_HOSTS_PATH) : null;
4631
+ const inactive = inactiveBefore === null ? { version: 1, hosts: [] } : inactiveHostsSchema.parse(JSON.parse(inactiveBefore));
4526
4632
  const automationBefore = await read(AUTOMATION_PATH);
4527
4633
  const automation = automationBefore === null ? null : recordSchema2.parse(JSON.parse(automationBefore));
4528
4634
  const commands2 = (host) => new Set([
@@ -4666,7 +4772,16 @@ async function planTeardown(root, selected) {
4666
4772
  delete ownership.files[configPath(host)];
4667
4773
  }
4668
4774
  if (!remainingHost) for (const file of DOC_CANDIDATES) delete ownership.files[file];
4669
- if (setupBefore !== null) cleanup.push({ path: SETUP_PATH, before: setupBefore, after: Object.keys(setup.hosts).length ? json(setup) : null });
4775
+ if (setupBefore !== null) cleanup.push({
4776
+ path: SETUP_PATH,
4777
+ before: setupBefore,
4778
+ after: linked || Object.keys(setup.hosts).length ? json(setup) : null
4779
+ });
4780
+ else if (linked) cleanup.push({
4781
+ path: INACTIVE_HOSTS_PATH,
4782
+ before: inactiveBefore,
4783
+ after: json({ version: 1, hosts: [.../* @__PURE__ */ new Set([...inactive.hosts, ...selected])].sort() })
4784
+ });
4670
4785
  if (automationBefore !== null) cleanup.push({ path: AUTOMATION_PATH, before: automationBefore, after: Object.keys(automation.hosts).length ? json(automation) : null });
4671
4786
  if (ownershipBefore !== null) cleanup.push({ path: OWNERSHIP_PATH, before: ownershipBefore, after: Object.keys(ownership.files).length ? json(ownership) : null });
4672
4787
  edits.push(...cleanup);
@@ -5107,10 +5222,10 @@ var init_check = __esm({
5107
5222
  });
5108
5223
 
5109
5224
  // src/review/evidence.ts
5110
- import path24 from "path";
5225
+ import path26 from "path";
5111
5226
  import { z as z22 } from "zod";
5112
5227
  function relativeArtifact(root, file) {
5113
- const relative = path24.isAbsolute(file) ? path24.relative(root, file) : file;
5228
+ const relative = path26.isAbsolute(file) ? path26.relative(root, file) : file;
5114
5229
  const normalized = normalizeRepoPath(relative);
5115
5230
  if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
5116
5231
  return normalized;
@@ -5207,7 +5322,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
5207
5322
  const report = await readStoreJson(root, file);
5208
5323
  if (report === null) throw new Error(`Report artifact is missing: ${file}`);
5209
5324
  const sourceRoot = check.sourceRoot ?? root;
5210
- if (!path24.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
5325
+ if (!path26.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
5211
5326
  const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : check.report.format === "mason-check-json" ? parseCheck(report, sourceRoot) : parseSarif(report, sourceRoot);
5212
5327
  result.outcome = parsed.outcome;
5213
5328
  result.counts = parsed.counts;
@@ -5328,8 +5443,8 @@ var init_evidence2 = __esm({
5328
5443
  });
5329
5444
 
5330
5445
  // src/review/review.ts
5331
- import fs19 from "fs/promises";
5332
- import path25 from "path";
5446
+ import fs21 from "fs/promises";
5447
+ import path27 from "path";
5333
5448
  import { execFile as execFile4 } from "child_process";
5334
5449
  import { promisify as promisify4 } from "util";
5335
5450
  async function resolveMergeBase(resolvedRoot, base, head) {
@@ -5358,7 +5473,7 @@ function anchorsTouched(record, changedFiles) {
5358
5473
  return matchingPaths(record.files, changedFiles);
5359
5474
  }
5360
5475
  async function computeReview(rootDir, base, options = {}) {
5361
- const resolvedRoot = path25.resolve(rootDir);
5476
+ const resolvedRoot = path27.resolve(rootDir);
5362
5477
  const head = await getCurrentGitHash(resolvedRoot);
5363
5478
  if (head === "unknown") return null;
5364
5479
  const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
@@ -5408,7 +5523,7 @@ async function computeReview(rootDir, base, options = {}) {
5408
5523
  analyzed,
5409
5524
  async (relPath) => {
5410
5525
  try {
5411
- await fs19.access(path25.join(resolvedRoot, relPath));
5526
+ await fs21.access(path27.join(resolvedRoot, relPath));
5412
5527
  return true;
5413
5528
  } catch {
5414
5529
  return false;
@@ -5551,8 +5666,9 @@ __export(status_exports, {
5551
5666
  import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
5552
5667
  async function setupStatus(dir) {
5553
5668
  const ws = await workspace(dir);
5554
- const setup = await loadSetup(ws.root);
5555
- if (!setup) {
5669
+ const activation = await effectiveSetup(ws.root);
5670
+ const setup = activation.setup;
5671
+ if (!setup || !Object.keys(setup.hosts).length) {
5556
5672
  const partial = await Promise.all(["codex", "claude"].map(async (host) => ({
5557
5673
  host,
5558
5674
  receipt: await loadSetupReceipt(ws.root, ws.directory, host)
@@ -5586,7 +5702,7 @@ async function setupStatus(dir) {
5586
5702
  ]));
5587
5703
  const hooks = isDeepStrictEqual3(compatibleHooks, expectedHooks) && !config.disabled;
5588
5704
  const local = await loadSetupReceipt(ws.root, ws.directory, host);
5589
- const configured = local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
5705
+ const configured = activation.root !== ws.root || local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
5590
5706
  const observation = await readObservation(ws.root, ws.directory, host, entry.revision, installed.version ?? "unavailable");
5591
5707
  const sessions = Object.values(observation?.sessions ?? {}).sort((a, b) => b.at.localeCompare(a.at));
5592
5708
  const complete = sessions.find((session) => events.every((event) => session.events.includes(event)));
@@ -5618,6 +5734,7 @@ async function setupStatus(dir) {
5618
5734
  status: statuses.length && statuses.every((status) => status === "active") ? "active" : statuses.includes("attention") ? "attention" : "pending",
5619
5735
  root: ws.root,
5620
5736
  hosts: hosts2,
5737
+ activationRoot: activation.root,
5621
5738
  decisionRecords: decisions.records.length,
5622
5739
  diagnostics: decisions.diagnostics,
5623
5740
  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."
@@ -5625,6 +5742,9 @@ async function setupStatus(dir) {
5625
5742
  }
5626
5743
  function summarizeActivation(status) {
5627
5744
  const lines = ["Mason setup: " + status.status + "."];
5745
+ if (status.activationRoot && status.activationRoot !== status.root) {
5746
+ lines.push("Activation inherited from " + status.activationRoot + "; evidence belongs to this worktree.");
5747
+ }
5628
5748
  for (const [host, state] of Object.entries(status.hosts)) {
5629
5749
  lines.push(
5630
5750
  `${host}: ${state.status}`,