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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.0 — 2026-09-17
4
+
5
+ - Add automatic updates for new public standalone installations, with existing installations opting in through `mason updates enable`. Check at most daily for stable releases at least 24 hours old; leave npm, pinned versions, company mirrors, and CI under explicit control.
6
+ - Verify automatic downloads against Sigstore-signed release metadata from the exact tagged publishing workflow, stage and health-check each bundle, and activate at a future MCP launch only after other Mason MCP servers exit. Keep update work and diagnostics out of hook and MCP protocol streams.
7
+ - Add `mason updates` status, enable/disable and pin/unpin controls, `MASON_NO_AUTO_UPDATE=1`, persisted company mirror selection, and an offline `mason rollback` that restores and pins the previous installation. Retain `mason upgrade` for explicit immediate updates.
8
+ - Track standalone processes before startup and reclaim unused update bundles while preserving current, rollback, staged, and running versions. Retain legacy or uncertain bundles conservatively.
9
+ - Let linked Git worktrees inherit host activation from the primary checkout, with local setup and teardown taking precedence; keep audit evidence and observations local to each worktree.
10
+ - Limit dependency-change advisories to documents containing dependency guidance, preserve committed evidence through local edits, and distinguish current findings from historical advisories awaiting review.
11
+
12
+ ## 0.18.0 — 2026-09-16
13
+
14
+ - Fix overlapping snapshot writes restoring old descriptions and deleted flows. Snapshot saves, verdicts, and partial writes share a per-checkout cross-process lock; reads, replacement, and consolidation cleanup stay within the protected operation.
15
+ - Bind map verification to entry kind, content, and bounded source evidence through review tokens. Changed or deleted entries return conflicts; older calls without tokens request a fresh review without recording a verdict. Existing snapshot files remain compatible; callers must include the returned entry kind and review token to record a verdict. Restart all Mason MCP processes for a checkout after upgrading; older binaries do not honor the new snapshot lock.
16
+ - Recheck saved map verification evidence on retrieval. Changed evidence reports stale verification; unavailable evidence and legacy verdicts without tokens report unknown. Preserve historical verdicts and failure reasons, including when reviewed local edits are reverted to a clean checkout.
17
+ - Diagnose incomplete lock owners and interrupted recovery guards, document safe manual cleanup after writers stop, and cover both interruption windows with controlled process-termination tests.
18
+ - Cover the reported overwrite with a controlled regression, exercise both call orders through one MCP session and separate processes, and test interrupted writers, contention, partial cleanup, and stale review evidence. Run the snapshot regressions in the existing macOS, Linux, and Windows distribution matrix before publishing.
19
+
20
+ Run `mason upgrade` for standalone installations, or `npm install -g mason-context@0.18.0` for npm installations. Restart **every Mason MCP process** sharing a checkout after upgrading: older processes do not participate in the new snapshot lock. Existing decisions and snapshot files are preserved. Older snapshot verdicts remain visible but report unknown verification until reviewed again with an evidence token. Existing integrations do not need setup again.
21
+
3
22
  ## 0.17.4 — 2026-09-15
4
23
 
5
24
  - Share Git document/source inventories within each automation inspection, with a separate fresh inventory before publication. Batch module ignore checks by directory level and reuse only identical scoped history queries.
package/README.md CHANGED
@@ -34,6 +34,8 @@ irm https://github.com/adrianczuczka/mason/releases/latest/download/install.ps1
34
34
 
35
35
  No Node or npm required. Git is required. The installer configures PATH for Bash, Zsh, and Windows. [Platform details](docs/distribution.md) · [npm installation](docs/setup.md#unified-project-setup)
36
36
 
37
+ New standard standalone installs update automatically in the background. Updates activate on a future MCP launch after existing Mason MCP servers exit. Run `mason updates` to inspect or change this preference. Existing installations require one manual upgrade and `mason updates enable`. [Update controls](docs/distribution.md#automatic-updates)
38
+
37
39
  Run this in your Git repository (open a new terminal if the installer asks):
38
40
 
39
41
  ```bash
@@ -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() {
@@ -234,6 +235,102 @@ var init_storage = __esm({
234
235
  }
235
236
  });
236
237
 
238
+ // src/utils/store-lock.ts
239
+ import fs3 from "fs/promises";
240
+ import os from "os";
241
+ async function lockDiagnostic(file) {
242
+ let reason;
243
+ try {
244
+ const owner = JSON.parse(await fs3.readFile(file, "utf8"));
245
+ if (!owner || typeof owner.host !== "string" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
246
+ reason = "Lock owner data is incomplete or malformed.";
247
+ } else if (owner.host !== os.hostname()) {
248
+ reason = "The lock belongs to another host; its process cannot be checked locally.";
249
+ } else {
250
+ try {
251
+ process.kill(owner.pid, 0);
252
+ reason = `Local owner PID ${owner.pid} is still running.`;
253
+ } catch (error) {
254
+ reason = 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.`;
255
+ }
256
+ }
257
+ } catch (error) {
258
+ reason = error instanceof SyntaxError ? "Lock owner data is incomplete or malformed." : "Lock owner data could not be read; the lock may have changed.";
259
+ }
260
+ if (await fs3.lstat(file + ".reclaim").then(() => true, () => false)) {
261
+ reason += " A recovery guard also needs inspection: " + file + ".reclaim.";
262
+ }
263
+ return reason + " 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.";
264
+ }
265
+ async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason store") {
266
+ const file = await storePath(root, directory + "/lock", true);
267
+ const deadline = Date.now() + waitMs;
268
+ let handle;
269
+ while (!handle) {
270
+ try {
271
+ handle = await fs3.open(file, "wx", 384);
272
+ try {
273
+ await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
274
+ } catch (error) {
275
+ await handle.close().catch(() => {
276
+ });
277
+ handle = void 0;
278
+ await fs3.rm(file, { force: true }).catch(() => {
279
+ });
280
+ throw error;
281
+ }
282
+ } catch (error) {
283
+ if (error.code !== "EEXIST") throw error;
284
+ try {
285
+ const owner = JSON.parse(await fs3.readFile(file, "utf8"));
286
+ if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
287
+ try {
288
+ process.kill(owner.pid, 0);
289
+ } catch (probe) {
290
+ if (probe.code === "ESRCH") {
291
+ const reclaim = file + ".reclaim";
292
+ let guard;
293
+ try {
294
+ guard = await fs3.open(reclaim, "wx", 384);
295
+ const current = JSON.parse(await fs3.readFile(file, "utf8"));
296
+ if (current.pid === owner.pid && current.host === owner.host) await fs3.unlink(file);
297
+ } finally {
298
+ if (guard) {
299
+ await guard.close();
300
+ await fs3.rm(reclaim, { force: true });
301
+ }
302
+ }
303
+ }
304
+ }
305
+ }
306
+ } catch {
307
+ }
308
+ if (Date.now() >= deadline) throw new Error(label + " is busy or its lock needs inspection: " + file + ". " + await lockDiagnostic(file));
309
+ await new Promise((resolve) => setTimeout(resolve, 40));
310
+ }
311
+ }
312
+ try {
313
+ return await run();
314
+ } finally {
315
+ await handle.close();
316
+ await fs3.unlink(file);
317
+ }
318
+ }
319
+ var init_store_lock = __esm({
320
+ "src/utils/store-lock.ts"() {
321
+ "use strict";
322
+ init_storage();
323
+ }
324
+ });
325
+
326
+ // src/snapshot/lock.ts
327
+ var init_lock = __esm({
328
+ "src/snapshot/lock.ts"() {
329
+ "use strict";
330
+ init_store_lock();
331
+ }
332
+ });
333
+
237
334
  // src/test-map.ts
238
335
  import path4 from "path";
239
336
  var init_test_map = __esm({
@@ -264,6 +361,7 @@ var init_snapshot = __esm({
264
361
  init_files();
265
362
  init_files();
266
363
  init_storage();
364
+ init_lock();
267
365
  init_paths();
268
366
  init_test_map();
269
367
  repoPath = z.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
@@ -271,6 +369,7 @@ var init_snapshot = __esm({
271
369
  refreshedHash: z.string().optional(),
272
370
  verifiedAt: z.string().optional(),
273
371
  verifiedHash: z.string().optional(),
372
+ verificationToken: z.string().optional(),
274
373
  verificationFailed: z.boolean().optional(),
275
374
  verificationNote: z.string().optional()
276
375
  };
@@ -294,7 +393,7 @@ var init_snapshot = __esm({
294
393
  });
295
394
 
296
395
  // src/drift/drift.ts
297
- import fs3 from "fs/promises";
396
+ import fs4 from "fs/promises";
298
397
  import path6 from "path";
299
398
  function parseChanges(output) {
300
399
  const fields = output.split("\0");
@@ -855,12 +954,12 @@ var init_git = __esm({
855
954
  });
856
955
 
857
956
  // src/audit/inputs.ts
858
- import fs4 from "fs/promises";
957
+ import fs5 from "fs/promises";
859
958
  import * as nativeFs from "fs";
860
959
  import path9 from "path";
861
960
  import fg2 from "fast-glob";
862
961
  async function auditInputPath(root, file) {
863
- if (file === ".") return fs4.realpath(root);
962
+ if (file === ".") return fs5.realpath(root);
864
963
  try {
865
964
  return await storePath(root, file);
866
965
  } catch (error) {
@@ -965,7 +1064,7 @@ var init_inputs = __esm({
965
1064
  });
966
1065
 
967
1066
  // src/audit/docs.ts
968
- import fs5 from "fs/promises";
1067
+ import fs6 from "fs/promises";
969
1068
  import path10 from "path";
970
1069
  import fg3 from "fast-glob";
971
1070
  async function dirtyDocs(resolvedRoot, files) {
@@ -1037,7 +1136,7 @@ async function localDocPaths(root) {
1037
1136
  for (const dir of [".", ".claude"]) {
1038
1137
  let entries;
1039
1138
  try {
1040
- entries = await fs5.readdir(await auditInputPath(root, dir), { withFileTypes: true });
1139
+ entries = await fs6.readdir(await auditInputPath(root, dir), { withFileTypes: true });
1041
1140
  } catch (error) {
1042
1141
  if (error.code === "ENOENT") continue;
1043
1142
  throw error;
@@ -1116,7 +1215,7 @@ var init_types = __esm({
1116
1215
  });
1117
1216
 
1118
1217
  // src/audit/scope.ts
1119
- import fs6 from "fs/promises";
1218
+ import fs7 from "fs/promises";
1120
1219
  import path11 from "path";
1121
1220
  function scopedPath(directory, value) {
1122
1221
  if (path11.posix.isAbsolute(value) || value.includes("\\") || /[\x00-\x1f]/.test(value)) return null;
@@ -1136,7 +1235,7 @@ function pathClaimScope(doc, claim) {
1136
1235
  }
1137
1236
  async function pathExists(root, file) {
1138
1237
  try {
1139
- await fs6.access(await auditInputPath(root, file));
1238
+ await fs7.access(await auditInputPath(root, file));
1140
1239
  return true;
1141
1240
  } catch (error) {
1142
1241
  if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return false;
@@ -1226,7 +1325,7 @@ var init_deleted_reference = __esm({
1226
1325
 
1227
1326
  // src/audit/checks/new-module.ts
1228
1327
  import fg4 from "fast-glob";
1229
- import fs7 from "fs/promises";
1328
+ import fs8 from "fs/promises";
1230
1329
  import path13 from "path";
1231
1330
  function escapeRegExp(text2) {
1232
1331
  return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -1278,7 +1377,7 @@ async function collectModuleCandidates(root, combinedDocs) {
1278
1377
  const byParent = /* @__PURE__ */ new Map();
1279
1378
  for (let offset = 0; offset < parents.length; offset += 16) {
1280
1379
  await Promise.all(parents.slice(offset, offset + 16).map(async (dir) => {
1281
- const entries = await fs7.readdir(await auditInputPath(root, dir), { withFileTypes: true });
1380
+ const entries = await fs8.readdir(await auditInputPath(root, dir), { withFileTypes: true });
1282
1381
  const dirs = entries.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink() && sources.some((file) => file.startsWith(path13.posix.join(dir, entry.name) + "/"))) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)).map((entry) => path13.posix.join(dir, entry.name));
1283
1382
  byParent.set(dir, dirs);
1284
1383
  }));
@@ -1691,6 +1790,10 @@ var init_release_metadata = __esm({
1691
1790
  });
1692
1791
 
1693
1792
  // src/audit/checks/deps-changed.ts
1793
+ function hasDependencyContent(content) {
1794
+ const text2 = content.replace(/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g, "").replace(/<!--[\s\S]*?-->/g, "");
1795
+ 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);
1796
+ }
1694
1797
  function manifestPathspecs(doc) {
1695
1798
  const scope = documentScope(doc);
1696
1799
  if (scope === ".") return MANIFEST_PATHSPECS;
@@ -1717,6 +1820,19 @@ async function checkDepsChanged(ctx) {
1717
1820
  reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
1718
1821
  });
1719
1822
  }
1823
+ let content = doc.content;
1824
+ if (doc.dirty) {
1825
+ try {
1826
+ content = (await inspectionGit(
1827
+ ["show", `${doc.lastCommit.hash}:${doc.path}`],
1828
+ { cwd: ctx.root, maxBuffer: 10 * 1024 * 1024, timeout: 1e4 }
1829
+ )).stdout;
1830
+ } catch {
1831
+ result.skipped.push({ check: "deps-changed", doc: doc.path, reason: `${doc.path}: committed dependency content is unavailable` });
1832
+ continue;
1833
+ }
1834
+ }
1835
+ if (!hasDependencyContent(content)) continue;
1720
1836
  const range = await commitsTouchingSince(
1721
1837
  ctx.root,
1722
1838
  doc.lastCommit.hash,
@@ -1762,6 +1878,7 @@ var init_deps_changed = __esm({
1762
1878
  init_docs();
1763
1879
  init_release_metadata();
1764
1880
  init_git();
1881
+ init_inspection();
1765
1882
  init_checks();
1766
1883
  MANIFEST_COMMITS_CAP = 10;
1767
1884
  MANIFEST_PATHSPECS = [
@@ -1942,7 +2059,7 @@ var init_provenance = __esm({
1942
2059
  });
1943
2060
 
1944
2061
  // src/decisions/decisions.ts
1945
- import fs8 from "fs/promises";
2062
+ import fs9 from "fs/promises";
1946
2063
  import path17 from "path";
1947
2064
  import { createHash } from "crypto";
1948
2065
  import { z as z3 } from "zod";
@@ -1951,7 +2068,7 @@ async function loadDecisionStore(rootDir) {
1951
2068
  const diagnostics = [];
1952
2069
  let entries;
1953
2070
  try {
1954
- entries = await fs8.readdir(await storePath(rootDir, ".mason/decisions"));
2071
+ entries = await fs9.readdir(await storePath(rootDir, ".mason/decisions"));
1955
2072
  } catch (error) {
1956
2073
  if (error.code !== "ENOENT") diagnostics.push({ path: ".mason/decisions", message: String(error) });
1957
2074
  return { records, diagnostics };
@@ -2219,68 +2336,15 @@ var init_findings = __esm({
2219
2336
  });
2220
2337
 
2221
2338
  // src/automation/store.ts
2222
- import fs9 from "fs/promises";
2223
- import os from "os";
2224
2339
  import { z as z5 } from "zod";
2225
- async function withLock(root, directory, run, waitMs = 5e3) {
2226
- const file = await storePath(root, directory + "/lock", true);
2227
- const deadline = Date.now() + waitMs;
2228
- let handle;
2229
- while (!handle) {
2230
- try {
2231
- handle = await fs9.open(file, "wx", 384);
2232
- try {
2233
- await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
2234
- } catch (error) {
2235
- await handle.close().catch(() => {
2236
- });
2237
- handle = void 0;
2238
- await fs9.rm(file, { force: true }).catch(() => {
2239
- });
2240
- throw error;
2241
- }
2242
- } catch (error) {
2243
- if (error.code !== "EEXIST") throw error;
2244
- try {
2245
- const owner = JSON.parse(await fs9.readFile(file, "utf8"));
2246
- if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
2247
- try {
2248
- process.kill(owner.pid, 0);
2249
- } catch (probe) {
2250
- if (probe.code === "ESRCH") {
2251
- const reclaim = file + ".reclaim";
2252
- let guard;
2253
- try {
2254
- guard = await fs9.open(reclaim, "wx", 384);
2255
- const current = JSON.parse(await fs9.readFile(file, "utf8"));
2256
- if (current.pid === owner.pid && current.host === owner.host) await fs9.unlink(file);
2257
- } finally {
2258
- if (guard) {
2259
- await guard.close();
2260
- await fs9.rm(reclaim, { force: true });
2261
- }
2262
- }
2263
- }
2264
- }
2265
- }
2266
- } catch {
2267
- }
2268
- if (Date.now() >= deadline) throw new Error("Automation is busy or its lock needs inspection: " + file);
2269
- await new Promise((resolve) => setTimeout(resolve, 40));
2270
- }
2271
- }
2272
- try {
2273
- return await run();
2274
- } finally {
2275
- await handle.close();
2276
- await fs9.unlink(file);
2277
- }
2340
+ function withLock(root, directory, run, waitMs = 5e3) {
2341
+ return withStoreLock(root, directory, run, waitMs, "Automation");
2278
2342
  }
2279
2343
  var hostSchema, stateSchema;
2280
2344
  var init_store = __esm({
2281
2345
  "src/automation/store.ts"() {
2282
2346
  "use strict";
2283
- init_storage();
2347
+ init_store_lock();
2284
2348
  hostSchema = z5.enum(["claude", "codex"]);
2285
2349
  stateSchema = z5.object({
2286
2350
  version: z5.literal(1),
@@ -2477,7 +2541,7 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
2477
2541
  return {
2478
2542
  ...base,
2479
2543
  status: "review-required",
2480
- 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."
2544
+ 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."
2481
2545
  };
2482
2546
  }
2483
2547
  return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };