mason-context 0.16.0 → 0.16.2

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,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.2 — 2026-09-09
4
+
5
+ - Guard generated hooks when Mason is absent, and keep a checkout without local host setup quiet. Preserve active failure diagnostics and strict MCP startup; setup/status/teardown recognize the generated guard variants without project wrapper scripts.
6
+ - Observe known read-only tools without rescanning audit inputs or advancing verification. Keep before/after capture for shell and unknown tools. Add a reproducible full-CLI hook benchmark and publish its measured scope and limits.
7
+ - Document hook inputs, retained data, local versus optional networked features, MCP installation, and company mirrors. Require an explicit version with a custom release mirror to avoid public version discovery.
8
+ - Exercise core CLI/MCP and hook paths with denied network operations, raw-input retention markers, inactive/invalid setup, and changed evidence before potentially mutating tools. On macOS, apply OS network denial to the tested process tree too.
9
+
10
+ ## 0.16.1 — 2026-09-08
11
+
12
+ - Resolve references using literal import paths and declared Kotlin/Java package symbols. Separate explicit path mentions and uncertain name candidates; suppress config/document-name collisions and keep weak candidates out of compact task context with an omission count.
13
+ - Include investigations in project-context guidance and route reusable project lessons into sourced proposals, preserving uncertainty and unknown attribution.
14
+ - Add a two-session knowledge evaluation with ordinary prompts, independent digest-bound review, fresh-checkout reuse, negative controls, and current-guidance/file-notes comparisons. Mechanism validation and live agent outcomes are reported separately.
15
+
3
16
  ## 0.16.0 — 2026-09-08
4
17
 
5
18
  - Add `mason teardown` to disconnect all project hosts, or one with `--host codex|claude`. Support `--dir`, `--dry-run` and `--json`; retain decisions, configuration, maps, repair evidence and ignore rules. Global uninstall remains separate.
package/README.md CHANGED
@@ -12,6 +12,8 @@
12
12
 
13
13
  Works through MCP, with automatic documentation checks for **Codex** and **Claude Code**. No concept map required.
14
14
 
15
+ Core checks run locally with no model calls or telemetry. Results become context for your existing assistant. [Data, network access, and team installation](docs/data-and-network.md) covers storage, optional networked features, and internal mirrors.
16
+
15
17
  ## Get started
16
18
 
17
19
  Install on macOS or Linux:
@@ -74,6 +76,7 @@ Earlier read-only decision-retrieval evaluations scored **9.0/10 with Mason vs 7
74
76
  ## Documentation
75
77
 
76
78
  - [Setup, hooks, and upgrades](docs/setup.md)
79
+ - [Data and network behavior](docs/data-and-network.md) · [Hook performance](docs/hook-performance.md)
77
80
  - [Audits, repair verification, and CI evidence](docs/checks.md)
78
81
  - [Decisions, tools, and optional architecture maps](docs/reference.md)
79
82
  - [Roadmap](ROADMAP.md) · [Changelog](CHANGELOG.md) · [MIT license](LICENSE)
@@ -2521,7 +2521,7 @@ var init_evidence = __esm({
2521
2521
  init_dead_command();
2522
2522
  init_storage();
2523
2523
  exec7 = promisify8(execFile8);
2524
- engineVersion = true ? "0.16.0" : "development";
2524
+ engineVersion = true ? "0.16.2" : "development";
2525
2525
  hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
2526
2526
  content = readAuditInput;
2527
2527
  internal = (file) => file === ".mason" || file === ".mason/reports" || file.startsWith(".mason/reports/");
@@ -2628,6 +2628,25 @@ var init_store = __esm({
2628
2628
 
2629
2629
  // src/automation/runtime.ts
2630
2630
  import { randomUUID as randomUUID4 } from "crypto";
2631
+ async function observeReadOnlyTool(dir, event) {
2632
+ const ws = await workspace(dir);
2633
+ await withLock(ws.root, ws.directory, async () => {
2634
+ const statePath = ws.directory + "/state.json";
2635
+ const raw = await readStoreJson(ws.root, statePath);
2636
+ if (raw === null) return;
2637
+ const state = parseState(raw);
2638
+ if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
2639
+ throw new Error("Automation state belongs to another branch or worktree; original evidence was retained.");
2640
+ }
2641
+ const session = event.host && event.sessionId ? state.sessions[hash([event.host, event.sessionId])] : null;
2642
+ if (!session) return;
2643
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2644
+ session.lastUsed = now;
2645
+ session.events[event.event] = { at: now, count: (session.events[event.event]?.count ?? 0) + 1 };
2646
+ await writeStoreJson(ws.root, statePath, state);
2647
+ });
2648
+ return { root: ws.root, directory: ws.directory };
2649
+ }
2631
2650
  function summarize(report) {
2632
2651
  const open = report.findings.filter((f) => f.status !== "resolved");
2633
2652
  return [
@@ -2896,6 +2915,7 @@ var init_model = __esm({
2896
2915
  // src/setup/launcher.ts
2897
2916
  import { execFile as execFile9 } from "child_process";
2898
2917
  import { promisify as promisify9 } from "util";
2918
+ import fs13 from "fs/promises";
2899
2919
  function globalCommand(args, platform = process.platform) {
2900
2920
  return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args] } : { command: "mason", args };
2901
2921
  }
@@ -2920,7 +2940,7 @@ var init_launcher = __esm({
2920
2940
  "use strict";
2921
2941
  init_evidence();
2922
2942
  init_model();
2923
- executingVersion = () => true ? "0.16.0" : "development";
2943
+ executingVersion = () => true ? "0.16.2" : "development";
2924
2944
  mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
2925
2945
  hookCommand = (host) => `mason --setup-host ${host} auto`;
2926
2946
  }
@@ -2933,7 +2953,7 @@ __export(observations_exports, {
2933
2953
  observeActivation: () => observeActivation,
2934
2954
  readObservation: () => readObservation
2935
2955
  });
2936
- import fs13 from "fs/promises";
2956
+ import fs14 from "fs/promises";
2937
2957
  import { z as z8 } from "zod";
2938
2958
  function observationPath(directory, host, revision, version = executingVersion()) {
2939
2959
  return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
@@ -2949,9 +2969,9 @@ async function observeActivation(dir, event, options = {}) {
2949
2969
  const host = process.env.MASON_SETUP_HOST, revision = process.env.MASON_SETUP_REVISION;
2950
2970
  if (host !== "codex" && host !== "claude" || !revision || !process.env.MASON_SETUP_ROOT) return null;
2951
2971
  try {
2952
- const capturedDirectory = options.reportPath?.match(/^(\.mason\/reports\/automation\/[a-f0-9]{24})\/checks\//)?.[1];
2953
- const ws = capturedDirectory ? { root: await fs13.realpath(dir), directory: capturedDirectory } : await workspace(dir);
2954
- if (ws.root !== await fs13.realpath(process.env.MASON_SETUP_ROOT)) return null;
2972
+ 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];
2973
+ const ws = capturedDirectory ? { root: await fs14.realpath(dir), directory: capturedDirectory } : await workspace(dir);
2974
+ if (ws.root !== await fs14.realpath(process.env.MASON_SETUP_ROOT)) return null;
2955
2975
  const setup = await loadSetup(ws.root);
2956
2976
  if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
2957
2977
  const directory = ws.directory + "/activation";
@@ -3022,13 +3042,13 @@ import { z as z9 } from "zod";
3022
3042
  function normalizeHook(host, raw) {
3023
3043
  hostSchema.parse(host);
3024
3044
  const input2 = inputSchema.parse(raw);
3025
- const readOnly = host === "claude" ? /^(Read|Glob|Grep|WebSearch|WebFetch)$/ : /^(read_file|list_dir|grep_files)$/;
3026
- return { cwd: input2.cwd, name: input2.hook_event_name, event: {
3045
+ const readOnly = (host === "claude" ? /^(Read|Glob|Grep|WebSearch|WebFetch)$/ : /^(read_file|list_dir|grep_files|Read|Glob|Grep)$/).test(input2.tool_name ?? "");
3046
+ return { cwd: input2.cwd, name: input2.hook_event_name, readOnly, event: {
3027
3047
  event: lifecycle[input2.hook_event_name],
3028
3048
  host,
3029
3049
  sessionId: input2.session_id,
3030
3050
  toolId: input2.tool_use_id,
3031
- mutating: !!input2.tool_name && !readOnly.test(input2.tool_name),
3051
+ mutating: !!input2.tool_name && !readOnly,
3032
3052
  stopHookActive: input2.stop_hook_active || input2.permission_mode === "plan"
3033
3053
  } };
3034
3054
  }
@@ -3036,8 +3056,20 @@ async function runAutomationHook(host, stdin) {
3036
3056
  let name = "";
3037
3057
  try {
3038
3058
  if (Buffer.byteLength(stdin) > 1024 * 1024) throw new Error("Hook input exceeds 1 MiB.");
3039
- const input2 = normalizeHook(host, JSON.parse(stdin));
3059
+ let raw;
3060
+ try {
3061
+ raw = JSON.parse(stdin);
3062
+ } catch {
3063
+ throw new Error("Hook input is not valid JSON.");
3064
+ }
3065
+ const input2 = normalizeHook(host, raw);
3040
3066
  name = input2.name;
3067
+ if (["before_tool", "after_tool"].includes(input2.event.event) && input2.readOnly) {
3068
+ const observed = await observeReadOnlyTool(input2.cwd, input2.event);
3069
+ const { observeActivation: observeActivation3 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
3070
+ const warning2 = await observeActivation3(observed.root, input2.event.event, { sessionId: input2.event.sessionId, directory: observed.directory });
3071
+ return warning2 ? { systemMessage: warning2 } : null;
3072
+ }
3041
3073
  const result = await automate(input2.cwd, input2.event);
3042
3074
  const { observeActivation: observeActivation2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
3043
3075
  const warning = await observeActivation2(result.report.root, input2.event.event, {
@@ -3056,8 +3088,15 @@ async function runAutomationHook(host, stdin) {
3056
3088
  return { systemMessage: message2, ...["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse"].includes(name) ? { hookSpecificOutput: { hookEventName: name, additionalContext: message2 } } : {} };
3057
3089
  }
3058
3090
  }
3091
+ function managedHookCommand(host, platform = process.platform) {
3092
+ const command = `mason --setup-host ${host} auto hook --host ${host}`;
3093
+ 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`;
3094
+ }
3095
+ function knownManagedHookCommands(host) {
3096
+ return [managedHookCommand(host, "linux"), managedHookCommand(host, "win32"), `mason --setup-host ${host} auto hook --host ${host}`];
3097
+ }
3059
3098
  function hookConfig(host, command = "npx --no-install --package mason-context mason-auto") {
3060
- const handler = { type: "command", command: command + " hook --host " + host, timeout: 30 };
3099
+ const handler = { type: "command", command: command === `mason --setup-host ${host} auto` ? managedHookCommand(host) : command + " hook --host " + host, timeout: 30 };
3061
3100
  return { hooks: Object.fromEntries(HOOK_EVENTS.map((name) => [
3062
3101
  name,
3063
3102
  [{ ...["PreToolUse", "PostToolUse"].includes(name) ? { matcher: ".*" } : {}, hooks: [{ ...handler }] }]
@@ -3098,7 +3137,7 @@ __export(files_exports, {
3098
3137
  managedBlock: () => managedBlock,
3099
3138
  readText: () => readText
3100
3139
  });
3101
- import fs14 from "fs/promises";
3140
+ import fs15 from "fs/promises";
3102
3141
  import path18 from "path";
3103
3142
  import { randomUUID as randomUUID5 } from "crypto";
3104
3143
  async function readText(root, file) {
@@ -3128,14 +3167,14 @@ async function applyEdit(root, edit) {
3128
3167
  if (edit.after === null) {
3129
3168
  const file2 = await storePath(root, edit.path);
3130
3169
  if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
3131
- await fs14.unlink(file2);
3170
+ await fs15.unlink(file2);
3132
3171
  return true;
3133
3172
  }
3134
3173
  const file = await storePath(root, edit.path, true);
3135
3174
  const temporary = path18.join(path18.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
3136
3175
  try {
3137
- const mode = await fs14.stat(file).then((s) => s.mode & 511, () => 384);
3138
- const handle = await fs14.open(temporary, "wx", mode);
3176
+ const mode = await fs15.stat(file).then((s) => s.mode & 511, () => 384);
3177
+ const handle = await fs15.open(temporary, "wx", mode);
3139
3178
  try {
3140
3179
  await handle.writeFile(edit.after, "utf8");
3141
3180
  await handle.sync();
@@ -3143,10 +3182,10 @@ async function applyEdit(root, edit) {
3143
3182
  await handle.close();
3144
3183
  }
3145
3184
  if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
3146
- await fs14.rename(temporary, file);
3185
+ await fs15.rename(temporary, file);
3147
3186
  return true;
3148
3187
  } finally {
3149
- await fs14.rm(temporary, { force: true });
3188
+ await fs15.rm(temporary, { force: true });
3150
3189
  }
3151
3190
  }
3152
3191
  var init_files2 = __esm({
@@ -3248,9 +3287,9 @@ var init_init = __esm({
3248
3287
 
3249
3288
  Mason provides recorded decisions and file impact over MCP. A concept map is optional.
3250
3289
 
3251
- - Task, bug, or change request \u2192 \`get_context\` with the task text and known files: matching decisions, related tests, impact, and any available map entries.
3290
+ - Task, bug, change request, or investigation of why the project works this way \u2192 \`get_context\` with the task text and known files: matching decisions, related tests, impact, and any available map entries.
3252
3291
  - Before editing a file \u2192 \`get_impact\` to check references, tests, and historical change partners.
3253
- - Learned something the code cannot explain (a failed approach, an incident's cause, a workaround's reason, a review-settled convention) \u2192 \`save_decision\` with rationale, anchors, and any known owner, sources, and recorder. It creates a proposal immediately without setup or a map. Never invent attribution or record code-derivable facts, session trivia, or secrets.
3292
+ - 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.
3254
3293
  - 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.
3255
3294
  - 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.
3256
3295
  - 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.
@@ -3502,7 +3541,7 @@ async function planAutomationInstall(root, host, command) {
3502
3541
  const previous = record.hosts[host]?.command;
3503
3542
  const ownershipText = await readText(root, OWNERSHIP_PATH);
3504
3543
  const pending = ownershipText === null ? [] : ownershipSchema.parse(JSON.parse(ownershipText)).files[file]?.hookCommands ?? [];
3505
- const owned = /* @__PURE__ */ new Set([previous, ...pending, newCommand]);
3544
+ const owned = /* @__PURE__ */ new Set([previous, ...pending, newCommand, ...knownManagedHookCommands(host)]);
3506
3545
  const hooks = existing.hooks ?? {};
3507
3546
  for (const event of HOOK_EVENTS) {
3508
3547
  hooks[event] = (hooks[event] ?? []).map((group) => ({
@@ -3606,6 +3645,7 @@ async function planTeardown(root, selected) {
3606
3645
  const commands = (host) => new Set([
3607
3646
  automation?.hosts[host]?.command,
3608
3647
  ...ownership.files[configPath(host)]?.hookCommands ?? [],
3648
+ ...knownManagedHookCommands(host),
3609
3649
  hookConfig(host, hookCommand(host)).hooks.SessionStart[0].hooks[0].command,
3610
3650
  hookConfig(host).hooks.SessionStart[0].hooks[0].command
3611
3651
  ].filter((command) => !!command));
@@ -4335,7 +4375,7 @@ var init_evidence2 = __esm({
4335
4375
  });
4336
4376
 
4337
4377
  // src/review/review.ts
4338
- import fs15 from "fs/promises";
4378
+ import fs16 from "fs/promises";
4339
4379
  import path20 from "path";
4340
4380
  import { execFile as execFile12 } from "child_process";
4341
4381
  import { promisify as promisify12 } from "util";
@@ -4415,7 +4455,7 @@ async function computeReview(rootDir, base, options = {}) {
4415
4455
  analyzed,
4416
4456
  async (relPath) => {
4417
4457
  try {
4418
- await fs15.access(path20.join(resolvedRoot, relPath));
4458
+ await fs16.access(path20.join(resolvedRoot, relPath));
4419
4459
  return true;
4420
4460
  } catch {
4421
4461
  return false;
@@ -4569,7 +4609,7 @@ async function setupStatus(dir) {
4569
4609
  version: 1,
4570
4610
  status: pending.length ? "incomplete" : "not-configured",
4571
4611
  hosts: {},
4572
- next: pending.length ? "Setup did not finish. Rerun mason-auto setup --host " + pending[0] + " to resume using the retained original evidence." : "Run mason-auto setup --host codex or --host claude."
4612
+ next: pending.length ? "Setup did not finish. Rerun mason-auto setup --host " + pending[0] + " to resume using the retained original evidence." : "Project hooks are inactive until local setup. Run mason setup --host codex or --host claude."
4573
4613
  };
4574
4614
  }
4575
4615
  const hosts2 = {};