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/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.23.1 — 2026-10-21
6
+
7
+ - Wait up to 60 seconds for the transparency log when signing the standalone update manifest. With the 5-second default, a slow log write timed out after succeeding and the retry failed with `409 entry already exists`, so the 0.23.0 release produced no GitHub release downloads.
8
+ - Stop publishing to Smithery on release: Smithery now rejects server updates (`403 Updating servers has been removed`). Releases continue to npm, the official MCP Registry and GitHub release downloads; the existing Smithery listing is no longer updated.
9
+
10
+ ## 0.23.0 — 2026-10-21
11
+
12
+ - `save_decision` guidance asks for the constrained functions, types, or settings to be named in backticks, so drift notices can judge relevance. Anchors remain whole files or directories, and anchored changes still require inspection; symbol anchors and automatic clearing of trivial drift were considered and not adopted.
13
+ - Attribute decision drift on a branch to the branch or to the default branch it builds on. Previously a rebase made every decision the default branch had changed look like this checkout's unchecked work. Drift in anchors this branch touched is reported as before; drift that arrived only from the default branch is listed once per session, never blocks Stop, and is reported as usual on the default branch. `review_decision(prepare)` reports the same split in `previews.attribution`. Explicit audits still report all drift.
14
+ - `review_decision(action: "prepare")` previews source, diff and anchor commits since the latest inspection still valid for the record and scope, instead of since the original evidence baseline, and reports the commit used in `previews.diffSince`. Earlier changes were already inspected, so long-lived records no longer return months of truncated history. Pass `diffFrom: "capture"` for the full history. Responses include the latest two inspections and count older ones; the record file keeps them all. Review evidence, tokens and verdict requirements are unchanged.
15
+ - Stop failed, denied and interrupted tool calls from filling the automation store. Previously every new session failed once 128 sessions held unfinished calls, or once `state.json` passed 10 MiB, until the file was edited by hand. Claude configurations now register `PostToolUseFailure`; run `mason init --host claude` to add it, as older configurations stay compatible without it. Pending captures are dropped when the turn ends or after 24 idle hours. Sessions are evicted least recently used even with unfinished calls. `state.json` is written compactly and shrunk instead of failing when oversized, including files left by earlier versions. `mason status --json` reports unfinished tool calls, and an attempt still in flight no longer makes status `unavailable`.
16
+ - Show the changed lines in decision drift notices, including the Claude Stop review, so most irrelevant edits can be dismissed without another tool call. Hunks are diffed against the exact captured content of the last inspection or save. Each notice reports code identifiers named by the decision (backticked names, calls, camelCase or snake_case) that the changed lines or their enclosing declarations mention, or `Low relevance` when none are; decisions naming no identifiers are not assessed. Relevance is a name match only: findings, inspection requirements and Stop blocking are unchanged.
17
+ - Merge decision records automatically when branches each append inspections or review history, instead of conflicting inside the JSON on every rebase of a stacked branch. `mason init` adds a `.gitattributes` entry for `.mason/decisions/*.json`; `mason setup` configures the local `mason-decision` Git merge driver, and `mason status` reports a committed attribute without it. Two different changes to the same field still conflict, and Git's ordinary text conflict is used whenever Mason refuses a merge or is not on PATH. Run `mason init --host <host>` to add the attribute, commit `.gitattributes`, then run `mason setup` in each checkout.
18
+ - Leave Git-ignored files beneath a directory anchor out of its capture instead of marking the capture incomplete, so local setup files no longer block `review_decision` inspection or make the anchor's freshness unknown. Anchors naming only ignored content still stay unknown. An incomplete capture now leaves only its own decision's earlier drift findings unverified instead of every decision's.
19
+
5
20
  ## 0.22.0 — 2026-10-12
6
21
 
7
22
  - Separate CLI scopes: `mason init` prepares shared repository integration without activating it; `mason setup` activates compatible existing configuration using local state only. `mason teardown` now disconnects locally; use `--project` explicitly for shared removal. Initialization and teardown support read-only `--dry-run` previews. Compatible older instructions, supported legacy hooks and JSON formatting do not force shared rewrites; status reports optional template updates separately. MCP exposes matching `init`/`setup` modes. Setup no longer triggers background update checks. Migrate scripts that used setup to generate configuration to `mason init` followed by `mason setup`; scripts removing shared configuration must add `teardown --project`.
@@ -405,11 +405,12 @@ async function storePath(root, relative, createParents = false) {
405
405
  }
406
406
  return current;
407
407
  }
408
- async function readStoreJson(root, relative) {
408
+ async function readStoreJson(root, relative, maxBytes = STORE_MAX_BYTES) {
409
409
  try {
410
410
  const file = await storePath(root, relative);
411
- const raw = await readBoundedFile(file, 10 * 1024 * 1024);
412
- if (raw === null) throw new Error("file is not regular or exceeds 10 MiB");
411
+ const raw = await readBoundedFile(file, maxBytes);
412
+ if (raw === null)
413
+ throw new Error(`file is not regular or exceeds ${maxBytes / 1024 / 1024} MiB`);
413
414
  const parsed = JSON.parse(raw);
414
415
  if (parsed === null) throw new Error("expected a JSON object, received null");
415
416
  return parsed;
@@ -421,9 +422,9 @@ async function readStoreJson(root, relative) {
421
422
  );
422
423
  }
423
424
  }
424
- async function writeStoreJson(root, relative, value) {
425
- const payload = JSON.stringify(value, null, 2) + "\n";
426
- if (Buffer.byteLength(payload) > 10 * 1024 * 1024) {
425
+ async function writeStoreJson(root, relative, value, options = {}) {
426
+ const payload = JSON.stringify(value, null, options.compact ? void 0 : 2) + "\n";
427
+ if (Buffer.byteLength(payload) > STORE_MAX_BYTES) {
427
428
  throw new Error(`Mason store ${relative} exceeds 10 MiB`);
428
429
  }
429
430
  const file = await storePath(root, relative, true);
@@ -441,11 +442,13 @@ async function writeStoreJson(root, relative, value) {
441
442
  await fs2.rm(temporary, { force: true });
442
443
  }
443
444
  }
445
+ var STORE_MAX_BYTES;
444
446
  var init_storage = __esm({
445
447
  "src/utils/storage.ts"() {
446
448
  "use strict";
447
449
  init_paths();
448
450
  init_files();
451
+ STORE_MAX_BYTES = 10 * 1024 * 1024;
449
452
  }
450
453
  });
451
454
 
@@ -2458,7 +2461,12 @@ async function captureAnchorScopes(root, scopes2) {
2458
2461
  );
2459
2462
  const inventory = access.inventory();
2460
2463
  if (!inventory) throw new Error("Git anchor inventory unavailable");
2461
- const ignored = await inspectionGit(
2464
+ const visible = indexMatchingPaths(
2465
+ union.map((anchor) => [anchor]),
2466
+ inventory
2467
+ );
2468
+ const hidden = new Set(union.filter((_, index) => !visible[index].length));
2469
+ const ignored = hidden.size ? (await inspectionGit(
2462
2470
  [
2463
2471
  "ls-files",
2464
2472
  "-z",
@@ -2466,13 +2474,11 @@ async function captureAnchorScopes(root, scopes2) {
2466
2474
  "--ignored",
2467
2475
  "--exclude-standard",
2468
2476
  "--",
2469
- ...union.map((anchor) => `:(literal)${anchor}`)
2477
+ ...[...hidden].map((anchor) => `:(literal)${anchor}`)
2470
2478
  ],
2471
2479
  { cwd: root, maxBuffer: 8 * 1024 * 1024 }
2472
- );
2473
- const files = [
2474
- .../* @__PURE__ */ new Set([...inventory, ...ignored.stdout.split("\0").filter(Boolean)])
2475
- ].sort();
2480
+ )).stdout.split("\0").filter(Boolean) : [];
2481
+ const files = [.../* @__PURE__ */ new Set([...inventory, ...ignored])].sort();
2476
2482
  const patterns = await Promise.all(
2477
2483
  union.map(async (anchor) => {
2478
2484
  const literal2 = fg7.escapePath(anchor);
@@ -2488,7 +2494,7 @@ async function captureAnchorScopes(root, scopes2) {
2488
2494
  const filesByScope = new Map(
2489
2495
  scopes2.map((scope, index) => [JSON.stringify([...new Set(scope)].sort()), indexed[index]])
2490
2496
  );
2491
- return { filesByScope, eligible };
2497
+ return { filesByScope, eligible, hidden, ignored: new Set(ignored) };
2492
2498
  };
2493
2499
  return Promise.all(
2494
2500
  scopes2.map((scope) => {
@@ -2500,8 +2506,10 @@ async function captureAnchorScopes(root, scopes2) {
2500
2506
  const capture = { anchors, files: {}, complete: true };
2501
2507
  if (!anchors.length) return capture;
2502
2508
  try {
2503
- const { eligible, filesByScope } = await (context ??= prepareContext());
2504
- const files = filesByScope.get(JSON.stringify(anchors));
2509
+ const { eligible, filesByScope, hidden, ignored } = await (context ??= prepareContext());
2510
+ const files = filesByScope.get(JSON.stringify(anchors)).filter(
2511
+ (file) => !ignored.has(file) || anchors.some((anchor) => hidden.has(anchor) && anchorMatches(anchor, file))
2512
+ );
2505
2513
  if (files.length > 2e3) return { ...capture, complete: false };
2506
2514
  for (const file of files) {
2507
2515
  const evidence = await inspectionRead(
@@ -2910,6 +2918,35 @@ var init_decisions = __esm({
2910
2918
  }
2911
2919
  });
2912
2920
 
2921
+ // src/decisions/excerpts.ts
2922
+ import { createHash as createHash4 } from "crypto";
2923
+ import { execFile as execFile2 } from "child_process";
2924
+ import { promisify as promisify2 } from "util";
2925
+ function driftBaseline(record) {
2926
+ const effective = effectiveDecision(record);
2927
+ const inspection = latestDecisionInspection(
2928
+ record,
2929
+ decisionApproval(effective) === "accepted" ? "operative" : "all"
2930
+ );
2931
+ return {
2932
+ record: effective,
2933
+ capture: inspection?.capture ?? (effective.version === 2 ? effective.capture : void 0),
2934
+ commits: [inspection?.headHash, effective.refreshedHash].filter(
2935
+ (hash) => !!hash && /^[a-f0-9]{40,64}$/.test(hash)
2936
+ )
2937
+ };
2938
+ }
2939
+ var exec;
2940
+ var init_excerpts = __esm({
2941
+ "src/decisions/excerpts.ts"() {
2942
+ "use strict";
2943
+ init_files();
2944
+ init_paths();
2945
+ init_provenance();
2946
+ exec = promisify2(execFile2);
2947
+ }
2948
+ });
2949
+
2913
2950
  // src/decisions/drift.ts
2914
2951
  import path17 from "path";
2915
2952
  async function computeDecisionDrift(rootDir, decisions) {
@@ -2974,15 +3011,11 @@ async function computeDecisionDrift(rootDir, decisions) {
2974
3011
  };
2975
3012
  for (const record of store.records) {
2976
3013
  if (record.status !== "active") continue;
2977
- const effective = effectiveDecision(record);
2978
- const inspection = latestDecisionInspection(
2979
- record,
2980
- decisionApproval(effective) === "accepted" ? "operative" : "all"
2981
- );
2982
- const state = await inspect(effective, inspection?.capture);
3014
+ const baseline = driftBaseline(record);
3015
+ const state = await inspect(baseline.record, baseline.capture);
2983
3016
  report.freshness[record.id] = state.freshness;
2984
3017
  if (state.changedFiles.length) report.staleDecisions[record.id] = state.changedFiles;
2985
- if (effective !== record)
3018
+ if (baseline.record !== record)
2986
3019
  (report.pendingProposals ??= {})[record.id] = await inspect(
2987
3020
  record,
2988
3021
  latestDecisionInspection(record)?.capture
@@ -2998,13 +3031,14 @@ var init_drift2 = __esm({
2998
3031
  init_paths();
2999
3032
  init_snapshot();
3000
3033
  init_decisions();
3034
+ init_excerpts();
3001
3035
  init_provenance();
3002
3036
  }
3003
3037
  });
3004
3038
 
3005
3039
  // src/audit/checks/decision-anchor.ts
3006
3040
  async function checkDecisionAnchors(ctx) {
3007
- const result = emptyResult();
3041
+ const result = { ...emptyResult(), incompleteDecisions: [] };
3008
3042
  if (!ctx.decisionsPresent) return result;
3009
3043
  const store = await loadDecisionStore(ctx.root);
3010
3044
  const records = store.records;
@@ -3036,6 +3070,7 @@ async function checkDecisionAnchors(ctx) {
3036
3070
  check: "decision-anchor-drift",
3037
3071
  reason: `Anchor evidence for decision ${record.id} is incomplete or unreadable`
3038
3072
  });
3073
+ result.incompleteDecisions.push(record.id);
3039
3074
  continue;
3040
3075
  }
3041
3076
  if (!changedFiles.length) continue;
@@ -3097,7 +3132,7 @@ var init_checks = __esm({
3097
3132
  });
3098
3133
 
3099
3134
  // src/audit/findings.ts
3100
- import { createHash as createHash4 } from "crypto";
3135
+ import { createHash as createHash5 } from "crypto";
3101
3136
  import { z as z6 } from "zod";
3102
3137
  function findingId(finding) {
3103
3138
  const e = finding.evidence;
@@ -3128,7 +3163,7 @@ var digest, checkSchema, commitSchema, anchorSchema, count, scopeSchema, evidenc
3128
3163
  var init_findings = __esm({
3129
3164
  "src/audit/findings.ts"() {
3130
3165
  "use strict";
3131
- digest = (value) => createHash4("sha256").update(JSON.stringify(value)).digest("hex");
3166
+ digest = (value) => createHash5("sha256").update(JSON.stringify(value)).digest("hex");
3132
3167
  checkSchema = z6.enum([
3133
3168
  "deleted-reference",
3134
3169
  "new-module",
@@ -3235,6 +3270,13 @@ var init_findings = __esm({
3235
3270
  }
3236
3271
  });
3237
3272
 
3273
+ // src/automation/pending-tools.ts
3274
+ var init_pending_tools = __esm({
3275
+ "src/automation/pending-tools.ts"() {
3276
+ "use strict";
3277
+ }
3278
+ });
3279
+
3238
3280
  // src/automation/stop-review.ts
3239
3281
  import { z as z7 } from "zod";
3240
3282
  var evidenceSchema2, stopReviewSchema;
@@ -3243,6 +3285,7 @@ var init_stop_review = __esm({
3243
3285
  "use strict";
3244
3286
  init_anchors();
3245
3287
  init_findings();
3288
+ init_pending_tools();
3246
3289
  evidenceSchema2 = z7.record(z7.object({ digest: z7.string(), capture: anchorCaptureSchema }));
3247
3290
  stopReviewSchema = z7.object({
3248
3291
  pending: z7.record(z7.object({ head: z7.string(), evidence: evidenceSchema2 })),
@@ -3279,13 +3322,14 @@ import { z as z9 } from "zod";
3279
3322
  function withLock(root, directory, run, waitMs = 5e3) {
3280
3323
  return withStoreLock(root, directory, run, waitMs, "Automation");
3281
3324
  }
3282
- var hostSchema, stateSchema;
3325
+ var hostSchema, stateSchema, IDLE_PENDING_MS, STATE_READ_MAX_BYTES;
3283
3326
  var init_store = __esm({
3284
3327
  "src/automation/store.ts"() {
3285
3328
  "use strict";
3286
3329
  init_store_lock();
3287
3330
  init_stop_review();
3288
3331
  init_notifications();
3332
+ init_storage();
3289
3333
  hostSchema = z9.enum(["claude", "codex"]);
3290
3334
  stateSchema = z9.object({
3291
3335
  version: z9.literal(1),
@@ -3300,6 +3344,8 @@ var init_store = __esm({
3300
3344
  host: hostSchema,
3301
3345
  lastUsed: z9.string(),
3302
3346
  notifiedDiagnostics: z9.array(z9.string()).default([]),
3347
+ /** Digest of the upstream drift notice already shown to this session. */
3348
+ upstreamNotice: z9.string().optional(),
3303
3349
  pending: z9.record(z9.string()),
3304
3350
  stopReview: stopReviewSchema.optional(),
3305
3351
  coverageGaps: z9.array(z9.string()),
@@ -3314,6 +3360,8 @@ var init_store = __esm({
3314
3360
  agentNotifications: notificationStateSchema.optional(),
3315
3361
  analysis: z9.object({ id: z9.string(), fingerprint: z9.string(), completedAt: z9.number(), path: z9.string() }).optional()
3316
3362
  });
3363
+ IDLE_PENDING_MS = 24 * 60 * 60 * 1e3;
3364
+ STATE_READ_MAX_BYTES = 64 * 1024 * 1024;
3317
3365
  }
3318
3366
  });
3319
3367