mason-context 0.15.0 → 0.16.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/dist/mason.js CHANGED
@@ -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.15.0" : "development";
2524
+ engineVersion = true ? "0.16.0" : "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/");
@@ -2813,9 +2813,9 @@ async function automationStatus(dir) {
2813
2813
  if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) throw new Error("Automation state belongs to another workspace.");
2814
2814
  const inputs = await readInputs(ws.root);
2815
2815
  const latest = state.latest ? await readStoreJson(ws.root, state.latest) : null;
2816
- const hosts = {};
2816
+ const hosts2 = {};
2817
2817
  for (const session of Object.values(state.sessions)) {
2818
- const host = hosts[session.host] ??= { sessions: 0, observedEvents: [] };
2818
+ const host = hosts2[session.host] ??= { sessions: 0, observedEvents: [] };
2819
2819
  host.sessions++;
2820
2820
  host.observedEvents = [.../* @__PURE__ */ new Set([...host.observedEvents, ...Object.keys(session.events)])];
2821
2821
  }
@@ -2828,7 +2828,7 @@ async function automationStatus(dir) {
2828
2828
  baselinePaths: state.baselines.map((b) => b.path),
2829
2829
  reportPath: state.latest,
2830
2830
  verificationStatus: unfinished ? "unavailable" : latest?.status ?? "unavailable",
2831
- hosts,
2831
+ hosts: hosts2,
2832
2832
  execution,
2833
2833
  note: "Observed events do not prove all tool paths are intercepted. Run check to verify the retained evidence."
2834
2834
  };
@@ -2937,7 +2937,7 @@ var init_launcher = __esm({
2937
2937
  "use strict";
2938
2938
  init_evidence();
2939
2939
  init_model();
2940
- executingVersion = () => true ? "0.15.0" : "development";
2940
+ executingVersion = () => true ? "0.16.0" : "development";
2941
2941
  mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
2942
2942
  hookCommand = (host) => `mason --setup-host ${host} auto`;
2943
2943
  }
@@ -3141,7 +3141,13 @@ function managedBlock(text2, start, end, body) {
3141
3141
  async function applyEdit(root, edit) {
3142
3142
  const current = await readText(root, edit.path);
3143
3143
  if (current === edit.after) return false;
3144
- if (current !== edit.before) throw new Error("Setup input changed during installation: " + edit.path + ". Rerun setup to resume.");
3144
+ if (current !== edit.before) throw new Error("Integration input changed during update: " + edit.path + ". Rerun the requested command to resume.");
3145
+ if (edit.after === null) {
3146
+ const file2 = await storePath(root, edit.path);
3147
+ if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
3148
+ await fs14.unlink(file2);
3149
+ return true;
3150
+ }
3145
3151
  const file = await storePath(root, edit.path, true);
3146
3152
  const temporary = path18.join(path18.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
3147
3153
  try {
@@ -3153,7 +3159,7 @@ async function applyEdit(root, edit) {
3153
3159
  } finally {
3154
3160
  await handle.close();
3155
3161
  }
3156
- if (await readText(root, edit.path) !== edit.before) throw new Error("Setup input changed during installation: " + edit.path);
3162
+ if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
3157
3163
  await fs14.rename(temporary, file);
3158
3164
  return true;
3159
3165
  } finally {
@@ -3168,18 +3174,91 @@ var init_files2 = __esm({
3168
3174
  }
3169
3175
  });
3170
3176
 
3171
- // src/mcp/init.ts
3177
+ // src/setup/ownership.ts
3178
+ var ownership_exports = {};
3179
+ __export(ownership_exports, {
3180
+ BLOCKS: () => BLOCKS,
3181
+ OWNERSHIP_PATH: () => OWNERSHIP_PATH,
3182
+ blockSpan: () => blockSpan,
3183
+ completedHookOwnership: () => completedHookOwnership,
3184
+ ownershipEdit: () => ownershipEdit,
3185
+ ownershipSchema: () => ownershipSchema
3186
+ });
3172
3187
  import { z as z10 } from "zod";
3188
+ import { parse } from "smol-toml";
3189
+ function blockSpan(text2, start, end) {
3190
+ const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
3191
+ if (!starts && !ends) return null;
3192
+ const from = text2.indexOf(start), to = text2.indexOf(end) + end.length;
3193
+ if (starts !== 1 || ends !== 1 || to <= from) throw new Error("Ambiguous Mason markers; inspect the marked block manually.");
3194
+ return { from, to, text: text2.slice(from, to) };
3195
+ }
3196
+ async function ownershipEdit(root, edits, hookCommands = {}) {
3197
+ const before = await readText(root, OWNERSHIP_PATH);
3198
+ const record = before === null ? { version: 1, files: {} } : ownershipSchema.parse(JSON.parse(before));
3199
+ for (const edit of edits) {
3200
+ const previous = record.files[edit.path];
3201
+ const entry = {
3202
+ created: edit.before === null || (previous?.created ?? false),
3203
+ initialHash: edit.before === null || previous?.initialHash === hash(edit.before) ? hash(edit.after) : previous?.initialHash ?? hash(edit.after),
3204
+ blocks: Object.fromEntries(BLOCKS.flatMap(([start, end]) => {
3205
+ const block = blockSpan(edit.after, start, end);
3206
+ return block ? [[start, hash(block.text)]] : [];
3207
+ }))
3208
+ };
3209
+ const mcp = edit.path === ".codex/config.toml" ? parse(edit.after).mcp_servers?.mason : edit.path === ".mcp.json" ? JSON.parse(edit.after).mcpServers?.mason : void 0;
3210
+ const hooks = edit.path === ".codex/hooks.json" || edit.path === ".claude/settings.json" ? JSON.parse(edit.after).hooks : void 0;
3211
+ record.files[edit.path] = {
3212
+ ...entry,
3213
+ ...mcp === void 0 ? {} : { mcpHash: hash(mcp) },
3214
+ ...hookCommands[edit.path] ? { hookCommands: [.../* @__PURE__ */ new Set([...previous?.hookCommands ?? [], hookCommands[edit.path]])] } : {},
3215
+ ...hooks === void 0 ? {} : { hooks: Object.fromEntries(Object.entries(hooks).map(([event, groups]) => [event, groups.flatMap((group) => group.hooks.map(hash))])) }
3216
+ };
3217
+ }
3218
+ return { path: OWNERSHIP_PATH, before, after: JSON.stringify(record, null, 2) + "\n" };
3219
+ }
3220
+ async function completedHookOwnership(root, file, command2) {
3221
+ const before = await readText(root, OWNERSHIP_PATH);
3222
+ const record = ownershipSchema.parse(JSON.parse(before ?? "null"));
3223
+ record.files[file].hookCommands = [command2];
3224
+ return { path: OWNERSHIP_PATH, before, after: JSON.stringify(record, null, 2) + "\n" };
3225
+ }
3226
+ var OWNERSHIP_PATH, BLOCKS, fileSchema, ownershipSchema;
3227
+ var init_ownership = __esm({
3228
+ "src/setup/ownership.ts"() {
3229
+ "use strict";
3230
+ init_evidence();
3231
+ init_files2();
3232
+ OWNERSHIP_PATH = ".mason/local/integration.json";
3233
+ BLOCKS = [
3234
+ ["<!-- mason:start -->", "<!-- mason:end -->"],
3235
+ ["<!-- mason:agents:start -->", "<!-- mason:agents:end -->"],
3236
+ ["# mason:mcp:start", "# mason:mcp:end"]
3237
+ ];
3238
+ fileSchema = z10.object({
3239
+ created: z10.boolean(),
3240
+ initialHash: z10.string(),
3241
+ blocks: z10.record(z10.string()),
3242
+ mcpHash: z10.string().optional(),
3243
+ hookCommands: z10.array(z10.string()).optional(),
3244
+ hooks: z10.record(z10.array(z10.string())).optional()
3245
+ });
3246
+ ownershipSchema = z10.object({ version: z10.literal(1), files: z10.record(fileSchema) });
3247
+ }
3248
+ });
3249
+
3250
+ // src/mcp/init.ts
3251
+ import { z as z11 } from "zod";
3173
3252
  var markerSchema, CLAUDE_MD_SECTION, ASSISTANT_SETUP, QUICKSTART_PLAYBOOK, MAP_PLAYBOOK;
3174
3253
  var init_init = __esm({
3175
3254
  "src/mcp/init.ts"() {
3176
3255
  "use strict";
3177
3256
  init_storage();
3178
3257
  init_files2();
3179
- markerSchema = z10.object({
3180
- version: z10.literal(1),
3181
- initializedAt: z10.string(),
3182
- features: z10.object({ confluence: z10.boolean().optional() }).passthrough().optional()
3258
+ markerSchema = z11.object({
3259
+ version: z11.literal(1),
3260
+ initializedAt: z11.string(),
3261
+ features: z11.object({ confluence: z11.boolean().optional() }).passthrough().optional()
3183
3262
  }).passthrough();
3184
3263
  CLAUDE_MD_SECTION = `<!-- mason:start -->
3185
3264
  ## Mason project knowledge
@@ -3285,8 +3364,8 @@ __export(config_exports, {
3285
3364
  mcpPath: () => mcpPath
3286
3365
  });
3287
3366
  import { isDeepStrictEqual } from "util";
3288
- import { parse, stringify } from "smol-toml";
3289
- import { z as z11 } from "zod";
3367
+ import { parse as parse2, stringify } from "smol-toml";
3368
+ import { z as z12 } from "zod";
3290
3369
  async function instructionEdits(root, host) {
3291
3370
  const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
3292
3371
  const existing = found.find((f) => f.text !== null);
@@ -3327,7 +3406,7 @@ async function mcpEdit(root, host) {
3327
3406
  const servers2 = object.parse(config2.mcpServers ?? {});
3328
3407
  return { path: file, before, after: JSON.stringify({ ...config2, mcpServers: { ...servers2, mason: managedMcp(servers2.mason, host) } }, null, 2) + "\n" };
3329
3408
  }
3330
- const config = parse(before ?? "");
3409
+ const config = parse2(before ?? "");
3331
3410
  const managed = (before ?? "").includes(TOML_START);
3332
3411
  const servers = config.mcp_servers;
3333
3412
  const desired = managedMcp(servers?.mason, host);
@@ -3339,7 +3418,7 @@ async function mcpEdit(root, host) {
3339
3418
  for (const span of spans.reverse()) base = base.slice(0, span.start) + base.slice(span.end);
3340
3419
  }
3341
3420
  const after = managedBlock(base, TOML_START, TOML_END, stringify({ mcp_servers: { mason: desired } }).trimEnd());
3342
- const parsed = parse(after);
3421
+ const parsed = parse2(after);
3343
3422
  const expected = { ...config, mcp_servers: { ...servers ?? {}, mason: desired } };
3344
3423
  if (!isDeepStrictEqual(parsed, expected)) throw new Error("Could not safely configure the Mason MCP entry without changing other settings.");
3345
3424
  return { path: file, before, after };
@@ -3377,7 +3456,7 @@ async function ancillaryEdits(root) {
3377
3456
  }
3378
3457
  async function inspectHostConfig(root, host, plannedMcp) {
3379
3458
  const text2 = plannedMcp ?? await readText(root, mcpPath(host));
3380
- const config = host === "codex" ? parse(text2 ?? "") : object.parse(JSON.parse(text2 ?? "{}"));
3459
+ const config = host === "codex" ? parse2(text2 ?? "") : object.parse(JSON.parse(text2 ?? "{}"));
3381
3460
  const servers = host === "codex" ? config.mcp_servers : config.mcpServers;
3382
3461
  const hooks = automationConfigSchema.parse(JSON.parse(await readText(root, configPath(host)) ?? "{}"));
3383
3462
  const expected = hookConfig(host, hookCommand(host));
@@ -3402,7 +3481,7 @@ var init_config = __esm({
3402
3481
  init_install();
3403
3482
  init_files2();
3404
3483
  init_launcher();
3405
- object = z11.record(z11.unknown());
3484
+ object = z12.record(z12.unknown());
3406
3485
  mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
3407
3486
  TOML_START = "# mason:mcp:start";
3408
3487
  TOML_END = "# mason:mcp:end";
@@ -3419,24 +3498,28 @@ __export(install_exports, {
3419
3498
  configPath: () => configPath,
3420
3499
  installAutomation: () => installAutomation,
3421
3500
  installedAutomation: () => installedAutomation,
3422
- planAutomationInstall: () => planAutomationInstall
3501
+ planAutomationInstall: () => planAutomationInstall,
3502
+ recordSchema: () => recordSchema
3423
3503
  });
3424
- import { z as z12 } from "zod";
3504
+ import { z as z13 } from "zod";
3425
3505
  async function installationRecord(root) {
3426
3506
  return recordSchema.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
3427
3507
  }
3428
3508
  async function installAutomation(dir, host, command2) {
3429
3509
  const ws = await workspace(dir);
3430
- return withLock(ws.root, ".mason/reports/automation-install", () => installLocked(ws.root, host, command2));
3510
+ return withLock(ws.root, ".mason/reports/setup-lock", () => installLocked(ws.root, host, command2));
3431
3511
  }
3432
3512
  async function planAutomationInstall(root, host, command2) {
3433
3513
  const file = configPath(host);
3434
- const existing = automationConfigSchema.parse(await readStoreJson(root, file) ?? {});
3514
+ const before = await readText(root, file);
3515
+ const existing = automationConfigSchema.parse(JSON.parse(before ?? "{}"));
3435
3516
  const record = await installationRecord(root);
3436
3517
  const desired = hookConfig(host, command2);
3437
3518
  const newCommand = desired.hooks.SessionStart[0].hooks[0].command;
3438
3519
  const previous = record.hosts[host]?.command;
3439
- const owned = /* @__PURE__ */ new Set([previous, newCommand]);
3520
+ const ownershipText = await readText(root, OWNERSHIP_PATH);
3521
+ const pending = ownershipText === null ? [] : ownershipSchema.parse(JSON.parse(ownershipText)).files[file]?.hookCommands ?? [];
3522
+ const owned = /* @__PURE__ */ new Set([previous, ...pending, newCommand]);
3440
3523
  const hooks = existing.hooks ?? {};
3441
3524
  for (const event of HOOK_EVENTS) {
3442
3525
  hooks[event] = (hooks[event] ?? []).map((group) => ({
@@ -3446,15 +3529,20 @@ async function planAutomationInstall(root, host, command2) {
3446
3529
  hooks[event].push(...desired.hooks[event]);
3447
3530
  }
3448
3531
  record.hosts[host] = { command: newCommand };
3449
- return { file, config: { ...existing, hooks }, record, newCommand };
3532
+ return { file, before, config: { ...existing, hooks }, record, newCommand };
3450
3533
  }
3451
3534
  async function installLocked(root, host, command2) {
3452
- const { file, config, record, newCommand } = await planAutomationInstall(root, host, command2);
3535
+ const { file, before, config, record, newCommand } = await planAutomationInstall(root, host, command2);
3453
3536
  const { ancillaryEdits: ancillaryEdits2 } = await Promise.resolve().then(() => (init_config(), config_exports));
3454
3537
  const { applyEdit: applyEdit2 } = await Promise.resolve().then(() => (init_files2(), files_exports));
3455
- for (const edit of await ancillaryEdits2(root)) await applyEdit2(root, edit);
3456
- await writeStoreJson(root, file, config);
3538
+ const { ownershipEdit: ownershipEdit2, completedHookOwnership: completedHookOwnership2 } = await Promise.resolve().then(() => (init_ownership(), ownership_exports));
3539
+ const edit = { path: file, before, after: JSON.stringify(config, null, 2) + "\n" };
3540
+ const ancillary = await ancillaryEdits2(root);
3541
+ await applyEdit2(root, await ownershipEdit2(root, [edit], { [file]: newCommand }));
3542
+ for (const edit2 of ancillary) await applyEdit2(root, edit2);
3543
+ await applyEdit2(root, edit);
3457
3544
  await writeStoreJson(root, AUTOMATION_PATH, record);
3545
+ await applyEdit2(root, await completedHookOwnership2(root, file, newCommand));
3458
3546
  return {
3459
3547
  version: 1,
3460
3548
  host,
@@ -3492,14 +3580,248 @@ var init_install = __esm({
3492
3580
  init_evidence();
3493
3581
  init_adapters();
3494
3582
  init_store();
3495
- groupSchema = z12.object({ hooks: z12.array(z12.object({ type: z12.string(), command: z12.string().optional() }).passthrough()) }).passthrough();
3496
- automationConfigSchema = z12.object({ hooks: z12.record(z12.array(groupSchema)).optional() }).passthrough();
3497
- recordSchema = z12.object({ version: z12.literal(1), hosts: z12.record(z12.object({ command: z12.string() })) });
3583
+ init_files2();
3584
+ init_ownership();
3585
+ groupSchema = z13.object({ hooks: z13.array(z13.object({ type: z13.string(), command: z13.string().optional() }).passthrough()) }).passthrough();
3586
+ automationConfigSchema = z13.object({ hooks: z13.record(z13.array(groupSchema)).optional() }).passthrough();
3587
+ recordSchema = z13.object({ version: z13.literal(1), hosts: z13.record(z13.object({ command: z13.string() })) });
3498
3588
  AUTOMATION_PATH = ".mason/local/automation.json";
3499
3589
  configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
3500
3590
  }
3501
3591
  });
3502
3592
 
3593
+ // src/setup/teardown.ts
3594
+ var teardown_exports = {};
3595
+ __export(teardown_exports, {
3596
+ summarizeTeardown: () => summarizeTeardown,
3597
+ teardownProject: () => teardownProject
3598
+ });
3599
+ import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
3600
+ import { parse as parse3, stringify as stringify2 } from "smol-toml";
3601
+ import fg5 from "fast-glob";
3602
+ import { z as z14 } from "zod";
3603
+ async function planTeardown(root, selected) {
3604
+ const edits = [], diagnostics = [];
3605
+ const inputs = /* @__PURE__ */ new Map();
3606
+ const read = async (file) => {
3607
+ if (!inputs.has(file)) inputs.set(file, await readText(root, file));
3608
+ return inputs.get(file);
3609
+ };
3610
+ const attempt = async (file, action) => {
3611
+ try {
3612
+ await action();
3613
+ } catch (error) {
3614
+ diagnostics.push({ path: file, message: error instanceof Error ? error.message : String(error) });
3615
+ }
3616
+ };
3617
+ const ownershipBefore = await read(OWNERSHIP_PATH);
3618
+ const ownership = ownershipBefore === null ? { version: 1, files: {} } : ownershipSchema.parse(JSON.parse(ownershipBefore));
3619
+ const setupBefore = await read(SETUP_PATH);
3620
+ const setup = setupBefore === null ? null : setupSchema.parse(JSON.parse(setupBefore));
3621
+ const automationBefore = await read(AUTOMATION_PATH);
3622
+ const automation = automationBefore === null ? null : recordSchema.parse(JSON.parse(automationBefore));
3623
+ const commands = (host) => new Set([
3624
+ automation?.hosts[host]?.command,
3625
+ ...ownership.files[configPath(host)]?.hookCommands ?? [],
3626
+ hookConfig(host, hookCommand(host)).hooks.SessionStart[0].hooks[0].command,
3627
+ hookConfig(host).hooks.SessionStart[0].hooks[0].command
3628
+ ].filter((command2) => !!command2));
3629
+ const possibleHook = (command2, host) => commands(host).has(command2) || /\bmason(?:-auto|-hook)?\b/.test(command2);
3630
+ const add = (file, before, after, empty) => {
3631
+ edits.push({ path: file, before, after: ownership.files[file]?.created && (empty || ownership.files[file].initialHash === hash(before)) ? null : after });
3632
+ };
3633
+ let remainingHost = hosts.some((host) => !selected.includes(host) && (setup?.hosts[host] || automation?.hosts[host] || ownership.files[mcpPath(host)] || ownership.files[configPath(host)]));
3634
+ for (const host of hosts.filter((host2) => !selected.includes(host2))) {
3635
+ await attempt(mcpPath(host), async () => {
3636
+ const text2 = await read(mcpPath(host));
3637
+ const config = host === "codex" ? parse3(text2 ?? "") : object2.parse(JSON.parse(text2 ?? "{}"));
3638
+ const servers = object2.parse((host === "codex" ? config.mcp_servers : config.mcpServers) ?? {});
3639
+ remainingHost ||= servers.mason !== void 0;
3640
+ });
3641
+ await attempt(configPath(host), async () => {
3642
+ const config = automationConfigSchema.parse(JSON.parse(await read(configPath(host)) ?? "{}"));
3643
+ remainingHost ||= Object.values(config.hooks ?? {}).some((groups) => groups.some((group) => group.hooks.some((handler) => handler.command && possibleHook(handler.command, host))));
3644
+ });
3645
+ }
3646
+ for (const host of selected) {
3647
+ const mcpFile = mcpPath(host);
3648
+ await attempt(mcpFile, async () => {
3649
+ const before = await read(mcpFile);
3650
+ if (before === null) return;
3651
+ const config = host === "codex" ? parse3(before) : object2.parse(JSON.parse(before));
3652
+ const key = host === "codex" ? "mcp_servers" : "mcpServers";
3653
+ const servers = object2.parse(config[key] ?? {});
3654
+ if (servers.mason === void 0) {
3655
+ if (host === "codex" && BLOCKS[2].some((marker) => before.includes(marker))) throw new Error("The marked Mason MCP entry was removed or renamed; inspect the retained block manually.");
3656
+ return;
3657
+ }
3658
+ const expected = ownership.files[mcpFile]?.mcpHash ?? hash(mcpCommand(host));
3659
+ if (hash(servers.mason) !== expected) throw new Error("The Mason MCP entry was edited or its ownership is uncertain; retained for manual cleanup.");
3660
+ delete servers.mason;
3661
+ const desired = { ...config, [key]: servers };
3662
+ if (!Object.keys(servers).length) delete desired[key];
3663
+ if (host === "claude") {
3664
+ add(mcpFile, before, json(desired), !Object.keys(desired).length);
3665
+ return;
3666
+ }
3667
+ const [start, end] = BLOCKS[2], block = blockSpan(before, start, end);
3668
+ if (!block) throw new Error("Mason MCP markers are missing; retained for manual cleanup.");
3669
+ const expectedBlock = ownership.files[mcpFile]?.blocks[start];
3670
+ const canonicalBlock = managedBlock("", start, end, stringify2({ mcp_servers: { mason: mcpCommand(host) } }).trimEnd()).trimEnd();
3671
+ if (expectedBlock ? hash(block.text) !== expectedBlock : block.text.replace(/\r\n/g, "\n") !== canonicalBlock) {
3672
+ throw new Error("The Mason MCP block was edited; retained for manual cleanup.");
3673
+ }
3674
+ const after = before.slice(0, block.from) + before.slice(block.to).replace(/^\r?\n/, "");
3675
+ const parsed = parse3(after);
3676
+ if (!Object.keys(servers).length && parsed[key] !== void 0) desired[key] = {};
3677
+ if (!isDeepStrictEqual2(parsed, desired)) throw new Error("Removing the marked block would change other TOML settings; retained for manual cleanup.");
3678
+ add(mcpFile, before, after, !after.trim());
3679
+ });
3680
+ const hookFile = configPath(host);
3681
+ await attempt(hookFile, async () => {
3682
+ const before = await read(hookFile);
3683
+ if (before === null) return;
3684
+ const config = automationConfigSchema.parse(JSON.parse(before));
3685
+ let changed = false;
3686
+ for (const [event, groups] of Object.entries(config.hooks ?? {})) {
3687
+ const known = ownership.files[hookFile]?.hooks?.[event];
3688
+ const handlers = groups.flatMap((group) => group.hooks);
3689
+ if (known && !handlers.some((handler) => handler.type === "command" && handler.command && commands(host).has(handler.command)) && handlers.some((handler) => !known.includes(hash(handler)))) {
3690
+ diagnostics.push({ path: hookFile, message: "A recorded Mason hook is missing beside a new or edited handler (" + event + "); inspect ownership manually." });
3691
+ }
3692
+ const kept = groups.flatMap((group) => {
3693
+ const handlers2 = group.hooks.filter((handler) => {
3694
+ if (handler.type === "command" && handler.command && commands(host).has(handler.command)) {
3695
+ changed = true;
3696
+ return false;
3697
+ }
3698
+ if (handler.command && possibleHook(handler.command, host)) diagnostics.push({
3699
+ path: hookFile,
3700
+ message: "An unrecognized Mason hook was retained for manual cleanup (" + event + ")."
3701
+ });
3702
+ return true;
3703
+ });
3704
+ return handlers2.length === group.hooks.length ? [group] : handlers2.length ? [{ ...group, hooks: handlers2 }] : [];
3705
+ });
3706
+ if (kept.length || !groups.length) config.hooks[event] = kept;
3707
+ else delete config.hooks[event];
3708
+ }
3709
+ if (!changed) return;
3710
+ if (!Object.keys(config.hooks ?? {}).length) delete config.hooks;
3711
+ add(hookFile, before, json(config), !Object.keys(config).length);
3712
+ });
3713
+ }
3714
+ const instructionEditStart = edits.length;
3715
+ if (!remainingHost && !diagnostics.length) for (const file of DOC_CANDIDATES) await attempt(file, async () => {
3716
+ const before = await read(file);
3717
+ if (before === null) return;
3718
+ let after = before;
3719
+ for (const [start, end] of BLOCKS.slice(0, 2)) {
3720
+ const block = blockSpan(after, start, end);
3721
+ if (!block) continue;
3722
+ const pointer = "Mason project knowledge and shared project instructions:\n@" + (file.startsWith(".claude/") ? "../AGENTS.md" : "AGENTS.md");
3723
+ const canonical = [CLAUDE_MD_SECTION, managedBlock("", start, end, pointer).trimEnd()];
3724
+ const expected = ownership.files[file]?.blocks[start];
3725
+ if (expected ? hash(block.text) !== expected : !canonical.includes(block.text.replace(/\r\n/g, "\n"))) {
3726
+ throw new Error("The Mason instruction block was edited or is unrecognized; retained for manual cleanup.");
3727
+ }
3728
+ after = after.slice(0, block.from) + after.slice(block.to).replace(/^\r?\n/, "");
3729
+ }
3730
+ if (after !== before) add(file, before, after, !after.trim());
3731
+ });
3732
+ if (diagnostics.length) edits.splice(instructionEditStart);
3733
+ if (!diagnostics.length) {
3734
+ const cleanup = [];
3735
+ const receiptFiles = await fg5(
3736
+ selected.map((host) => ".mason/reports/automation/*/setup-" + host + ".json"),
3737
+ { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false }
3738
+ );
3739
+ if (receiptFiles.length > 1e3) throw new Error("Too many setup receipts; inspect local setup history before teardown.");
3740
+ for (const file of receiptFiles.sort()) await attempt(file, async () => {
3741
+ const host = file.endsWith("setup-codex.json") ? "codex" : "claude";
3742
+ await loadSetupReceipt(root, file.slice(0, file.lastIndexOf("/")), host);
3743
+ cleanup.push({ path: file, before: await read(file), after: null });
3744
+ });
3745
+ if (!remainingHost) await attempt(".mason/local/project.json", async () => {
3746
+ const before = await read(".mason/local/project.json");
3747
+ if (before === null) return;
3748
+ z14.object({ version: z14.literal(1), initializedAt: z14.string() }).strict().parse(JSON.parse(before));
3749
+ cleanup.push({ path: ".mason/local/project.json", before, after: null });
3750
+ });
3751
+ if (!diagnostics.length) {
3752
+ for (const host of selected) {
3753
+ if (setup) delete setup.hosts[host];
3754
+ if (automation) delete automation.hosts[host];
3755
+ delete ownership.files[mcpPath(host)];
3756
+ delete ownership.files[configPath(host)];
3757
+ }
3758
+ if (!remainingHost) for (const file of DOC_CANDIDATES) delete ownership.files[file];
3759
+ if (setupBefore !== null) cleanup.push({ path: SETUP_PATH, before: setupBefore, after: Object.keys(setup.hosts).length ? json(setup) : null });
3760
+ if (automationBefore !== null) cleanup.push({ path: AUTOMATION_PATH, before: automationBefore, after: Object.keys(automation.hosts).length ? json(automation) : null });
3761
+ if (ownershipBefore !== null) cleanup.push({ path: OWNERSHIP_PATH, before: ownershipBefore, after: Object.keys(ownership.files).length ? json(ownership) : null });
3762
+ edits.push(...cleanup);
3763
+ }
3764
+ }
3765
+ return { edits: edits.filter((edit) => edit.before !== edit.after), diagnostics, remainingHost, inputs };
3766
+ }
3767
+ async function teardownProject(dir, options = {}) {
3768
+ const { root } = await workspace(dir);
3769
+ const selected = options.host ? [options.host] : [...hosts];
3770
+ const run = async (apply) => {
3771
+ const plan = await planTeardown(root, selected);
3772
+ if (apply) {
3773
+ for (const [file, before] of plan.inputs) if (await readText(root, file) !== before) throw new Error("Input changed during teardown: " + file + ". Rerun teardown.");
3774
+ for (const edit of plan.edits) await applyEdit(root, edit);
3775
+ }
3776
+ return {
3777
+ version: 1,
3778
+ action: "teardown",
3779
+ status: plan.diagnostics.length ? "incomplete" : "complete",
3780
+ root,
3781
+ hosts: selected,
3782
+ dryRun: !!options.dryRun,
3783
+ changes: plan.edits.map((edit) => ({ path: edit.path, action: edit.after === null ? "delete" : "update" })),
3784
+ diagnostics: plan.diagnostics,
3785
+ sharedInstructionsRetained: plan.remainingHost || !!plan.diagnostics.length,
3786
+ next: plan.diagnostics.length ? "Inspect the retained entries, remove or correct them, then rerun teardown." : "Restart running assistants to unload the project integration. Run mason setup --host codex or --host claude to reconnect."
3787
+ };
3788
+ };
3789
+ const preview = await run(false);
3790
+ if (options.dryRun || !preview.changes.length) return preview;
3791
+ return withLock(root, ".mason/reports/setup-lock", () => run(true));
3792
+ }
3793
+ function summarizeTeardown(result) {
3794
+ return [
3795
+ `Mason teardown${result.dryRun ? " preview" : ""}: ${result.status}.`,
3796
+ ...result.changes.map((change) => ` ${result.dryRun ? "Would " : ""}${change.action}: ${change.path}`),
3797
+ ...result.diagnostics.map((item) => ` Retained ${item.path}: ${item.message}`),
3798
+ ...!result.changes.length ? ["No changes."] : [],
3799
+ "Shared knowledge, repair evidence, ignore rules and the installed Mason command are retained.",
3800
+ ...result.sharedInstructionsRetained ? ["Shared instruction cleanup is deferred while another host or incomplete removal needs inspection."] : [],
3801
+ ...result.dryRun ? ["Preview only; no files were changed."] : [result.next]
3802
+ ].join("\n");
3803
+ }
3804
+ var hosts, object2, json;
3805
+ var init_teardown = __esm({
3806
+ "src/setup/teardown.ts"() {
3807
+ "use strict";
3808
+ init_evidence();
3809
+ init_store();
3810
+ init_install();
3811
+ init_adapters();
3812
+ init_docs();
3813
+ init_init();
3814
+ init_files2();
3815
+ init_config();
3816
+ init_launcher();
3817
+ init_model();
3818
+ init_ownership();
3819
+ hosts = ["codex", "claude"];
3820
+ object2 = z14.record(z14.unknown());
3821
+ json = (value) => JSON.stringify(value, null, 2) + "\n";
3822
+ }
3823
+ });
3824
+
3503
3825
  // src/review/cochange.ts
3504
3826
  import { execFile as execFile11 } from "child_process";
3505
3827
  import { promisify as promisify11 } from "util";
@@ -3622,7 +3944,7 @@ var init_types2 = __esm({
3622
3944
  });
3623
3945
 
3624
3946
  // src/review/evidence/vitest.ts
3625
- import { z as z13 } from "zod";
3947
+ import { z as z15 } from "zod";
3626
3948
  function parseVitest(raw, sourceRoot) {
3627
3949
  const report = schema.parse(raw);
3628
3950
  const findings = [], diagnostics = [];
@@ -3671,24 +3993,24 @@ var init_vitest = __esm({
3671
3993
  "use strict";
3672
3994
  init_paths2();
3673
3995
  init_types2();
3674
- count2 = z13.number().int().nonnegative();
3675
- schema = z13.object({
3676
- success: z13.boolean(),
3996
+ count2 = z15.number().int().nonnegative();
3997
+ schema = z15.object({
3998
+ success: z15.boolean(),
3677
3999
  numTotalTests: count2,
3678
4000
  numPassedTests: count2,
3679
4001
  numFailedTests: count2,
3680
4002
  numPendingTests: count2,
3681
4003
  numTodoTests: count2,
3682
4004
  numFailedTestSuites: count2,
3683
- testResults: z13.array(z13.object({
3684
- name: z13.string().min(1),
3685
- status: z13.enum(["passed", "failed"]),
3686
- message: z13.string().optional(),
3687
- assertionResults: z13.array(z13.object({
3688
- fullName: z13.string(),
3689
- status: z13.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
3690
- failureMessages: z13.array(z13.string()).nullable().optional(),
3691
- location: z13.object({ line: count2, column: count2 }).nullable().optional()
4005
+ testResults: z15.array(z15.object({
4006
+ name: z15.string().min(1),
4007
+ status: z15.enum(["passed", "failed"]),
4008
+ message: z15.string().optional(),
4009
+ assertionResults: z15.array(z15.object({
4010
+ fullName: z15.string(),
4011
+ status: z15.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
4012
+ failureMessages: z15.array(z15.string()).nullable().optional(),
4013
+ location: z15.object({ line: count2, column: count2 }).nullable().optional()
3692
4014
  }))
3693
4015
  }))
3694
4016
  });
@@ -3696,7 +4018,7 @@ var init_vitest = __esm({
3696
4018
  });
3697
4019
 
3698
4020
  // src/review/evidence/sarif.ts
3699
- import { z as z14 } from "zod";
4021
+ import { z as z16 } from "zod";
3700
4022
  function parseSarif(raw, sourceRoot) {
3701
4023
  const report = schema2.parse(raw), findings = [], diagnostics = [];
3702
4024
  let active = 0, suppressed = 0, absent = 0, unresolved = 0;
@@ -3787,29 +4109,29 @@ var init_sarif = __esm({
3787
4109
  "use strict";
3788
4110
  init_paths2();
3789
4111
  init_types2();
3790
- index = z14.number().int().nonnegative();
3791
- artifact = z14.object({ uri: z14.string().optional(), uriBaseId: z14.string().optional(), index: index.optional() });
3792
- message = z14.object({ text: z14.string().optional(), markdown: z14.string().optional(), id: z14.string().optional(), arguments: z14.array(z14.string()).optional() });
3793
- level = z14.enum(["error", "warning", "note", "none"]);
3794
- location = z14.object({ physicalLocation: z14.object({ artifactLocation: artifact.optional(), region: z14.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
3795
- schema2 = z14.object({
3796
- version: z14.literal("2.1.0"),
3797
- runs: z14.array(z14.object({
3798
- tool: z14.object({ driver: z14.object({ name: z14.string().min(1), rules: z14.array(z14.object({ id: z14.string(), defaultConfiguration: z14.object({ level: level.optional() }).optional(), messageStrings: z14.record(message).optional() })).optional(), globalMessageStrings: z14.record(message).optional() }) }),
3799
- invocations: z14.array(z14.object({ executionSuccessful: z14.boolean(), commandLine: z14.string().optional(), toolExecutionNotifications: z14.array(z14.object({ level: level.optional(), message })).optional() })).optional(),
3800
- versionControlProvenance: z14.array(z14.object({ revisionId: z14.string().optional() })).optional(),
3801
- originalUriBaseIds: z14.record(artifact).optional(),
3802
- artifacts: z14.array(z14.object({ location: artifact.optional() })).optional(),
3803
- results: z14.array(z14.object({
3804
- ruleId: z14.string().optional(),
4112
+ index = z16.number().int().nonnegative();
4113
+ artifact = z16.object({ uri: z16.string().optional(), uriBaseId: z16.string().optional(), index: index.optional() });
4114
+ message = z16.object({ text: z16.string().optional(), markdown: z16.string().optional(), id: z16.string().optional(), arguments: z16.array(z16.string()).optional() });
4115
+ level = z16.enum(["error", "warning", "note", "none"]);
4116
+ location = z16.object({ physicalLocation: z16.object({ artifactLocation: artifact.optional(), region: z16.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
4117
+ schema2 = z16.object({
4118
+ version: z16.literal("2.1.0"),
4119
+ runs: z16.array(z16.object({
4120
+ tool: z16.object({ driver: z16.object({ name: z16.string().min(1), rules: z16.array(z16.object({ id: z16.string(), defaultConfiguration: z16.object({ level: level.optional() }).optional(), messageStrings: z16.record(message).optional() })).optional(), globalMessageStrings: z16.record(message).optional() }) }),
4121
+ invocations: z16.array(z16.object({ executionSuccessful: z16.boolean(), commandLine: z16.string().optional(), toolExecutionNotifications: z16.array(z16.object({ level: level.optional(), message })).optional() })).optional(),
4122
+ versionControlProvenance: z16.array(z16.object({ revisionId: z16.string().optional() })).optional(),
4123
+ originalUriBaseIds: z16.record(artifact).optional(),
4124
+ artifacts: z16.array(z16.object({ location: artifact.optional() })).optional(),
4125
+ results: z16.array(z16.object({
4126
+ ruleId: z16.string().optional(),
3805
4127
  ruleIndex: index.optional(),
3806
4128
  message,
3807
4129
  level: level.optional(),
3808
- kind: z14.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
3809
- baselineState: z14.enum(["new", "unchanged", "updated", "absent"]).optional(),
3810
- suppressions: z14.array(z14.object({ kind: z14.enum(["inSource", "external"]), status: z14.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
3811
- locations: z14.array(location).optional(),
3812
- relatedLocations: z14.array(location).optional()
4130
+ kind: z16.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
4131
+ baselineState: z16.enum(["new", "unchanged", "updated", "absent"]).optional(),
4132
+ suppressions: z16.array(z16.object({ kind: z16.enum(["inSource", "external"]), status: z16.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
4133
+ locations: z16.array(location).optional(),
4134
+ relatedLocations: z16.array(location).optional()
3813
4135
  })).optional()
3814
4136
  }))
3815
4137
  });
@@ -3818,7 +4140,7 @@ var init_sarif = __esm({
3818
4140
 
3819
4141
  // src/review/evidence.ts
3820
4142
  import path19 from "path";
3821
- import { z as z15 } from "zod";
4143
+ import { z as z17 } from "zod";
3822
4144
  function relativeArtifact(root, file) {
3823
4145
  const relative = path19.isAbsolute(file) ? path19.relative(root, file) : file;
3824
4146
  const normalized = normalizeRepoPath(relative);
@@ -3859,7 +4181,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
3859
4181
  let manifest, raw;
3860
4182
  try {
3861
4183
  manifest = relativeArtifact(root, supplied);
3862
- raw = z15.object({ version: z15.literal(1), checks: z15.array(z15.unknown()) }).parse(await readStoreJson(root, manifest));
4184
+ raw = z17.object({ version: z17.literal(1), checks: z17.array(z17.unknown()) }).parse(await readStoreJson(root, manifest));
3863
4185
  } catch (error) {
3864
4186
  output.diagnostics.push(`${supplied}: ${String(error)}`);
3865
4187
  continue;
@@ -4012,19 +4334,19 @@ var init_evidence2 = __esm({
4012
4334
  init_vitest();
4013
4335
  init_sarif();
4014
4336
  init_types2();
4015
- checkSchema2 = z15.object({
4016
- id: z15.string().min(1).max(100),
4017
- kind: z15.enum(["tests", "static-analysis", "security", "complexity", "duplication"]),
4018
- tool: z15.string().min(1).max(200),
4019
- command: z15.string().min(1).max(2e3),
4020
- commit: z15.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
4021
- workingTreeClean: z15.boolean().optional(),
4022
- source: z15.string().max(2e3).optional(),
4023
- sourceRoot: z15.string().min(1).max(4e3).optional(),
4024
- status: z15.enum(["completed", "skipped", "unavailable"]).default("completed"),
4025
- reason: z15.string().min(1).max(2e3).optional(),
4026
- exitCode: z15.number().int().nullable().optional(),
4027
- report: z15.object({ format: z15.enum(["vitest-json", "sarif"]), path: z15.string().min(1).max(4e3) }).optional()
4337
+ checkSchema2 = z17.object({
4338
+ id: z17.string().min(1).max(100),
4339
+ kind: z17.enum(["tests", "static-analysis", "security", "complexity", "duplication"]),
4340
+ tool: z17.string().min(1).max(200),
4341
+ command: z17.string().min(1).max(2e3),
4342
+ commit: z17.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
4343
+ workingTreeClean: z17.boolean().optional(),
4344
+ source: z17.string().max(2e3).optional(),
4345
+ sourceRoot: z17.string().min(1).max(4e3).optional(),
4346
+ status: z17.enum(["completed", "skipped", "unavailable"]).default("completed"),
4347
+ reason: z17.string().min(1).max(2e3).optional(),
4348
+ exitCode: z17.number().int().nullable().optional(),
4349
+ report: z17.object({ format: z17.enum(["vitest-json", "sarif"]), path: z17.string().min(1).max(4e3) }).optional()
4028
4350
  });
4029
4351
  }
4030
4352
  });
@@ -4250,7 +4572,7 @@ __export(status_exports, {
4250
4572
  setupStatus: () => setupStatus,
4251
4573
  summarizeActivation: () => summarizeActivation
4252
4574
  });
4253
- import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
4575
+ import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
4254
4576
  async function setupStatus(dir) {
4255
4577
  const ws = await workspace(dir);
4256
4578
  const setup = await loadSetup(ws.root);
@@ -4267,7 +4589,7 @@ async function setupStatus(dir) {
4267
4589
  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."
4268
4590
  };
4269
4591
  }
4270
- const hosts = {};
4592
+ const hosts2 = {};
4271
4593
  const installed = await installedCommand();
4272
4594
  const automation = await automationStatus(ws.root);
4273
4595
  for (const host of ["codex", "claude"]) {
@@ -4276,8 +4598,8 @@ async function setupStatus(dir) {
4276
4598
  const instructions = await instructionEdits(ws.root, host);
4277
4599
  const instructionsCurrent = instructions.every((edit) => edit.before === edit.after);
4278
4600
  const config = await inspectHostConfig(ws.root, host, void 0);
4279
- const mcp = !config.mcpDisabled && hash(config.mcp) === entry.mcpFingerprint && isDeepStrictEqual2({ command: config.mcp?.command, args: config.mcp?.args }, mcpCommand(host));
4280
- const hooks = isDeepStrictEqual2(config.hooks, hookConfig(host, hookCommand(host)).hooks) && !config.disabled;
4601
+ const mcp = !config.mcpDisabled && hash(config.mcp) === entry.mcpFingerprint && isDeepStrictEqual3({ command: config.mcp?.command, args: config.mcp?.args }, mcpCommand(host));
4602
+ const hooks = isDeepStrictEqual3(config.hooks, hookConfig(host, hookCommand(host)).hooks) && !config.disabled;
4281
4603
  const local = await loadSetupReceipt(ws.root, ws.directory, host);
4282
4604
  const configured = local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
4283
4605
  const observation = await readObservation(ws.root, ws.directory, host, entry.revision, installed.version ?? "unavailable");
@@ -4291,7 +4613,7 @@ async function setupStatus(dir) {
4291
4613
  if (!complete) pending.push("Review/trust the host configuration, then complete a normal task to observe the full hook lifecycle.");
4292
4614
  if (automation.status === "unavailable") pending.push("The latest automation attempt did not establish verification. Run mason-auto check and inspect its diagnostics.");
4293
4615
  const healthy = installed.available && mcp && hooks && instructionsCurrent && configured && automation.status !== "unavailable";
4294
- hosts[host] = {
4616
+ hosts2[host] = {
4295
4617
  status: healthy ? complete && observation?.contextCalls ? "active" : "pending" : "attention",
4296
4618
  runtime: installed.available ? "global (" + installed.version + ")" : "unavailable-on-path",
4297
4619
  mcp: mcp ? "configured" : "changed",
@@ -4303,13 +4625,13 @@ async function setupStatus(dir) {
4303
4625
  pending
4304
4626
  };
4305
4627
  }
4306
- const statuses = Object.values(hosts).map((host) => host.status);
4628
+ const statuses = Object.values(hosts2).map((host) => host.status);
4307
4629
  const decisions = await loadDecisionStore(ws.root);
4308
4630
  return {
4309
4631
  version: 1,
4310
4632
  status: statuses.length && statuses.every((status) => status === "active") ? "active" : statuses.includes("attention") ? "attention" : "pending",
4311
4633
  root: ws.root,
4312
- hosts,
4634
+ hosts: hosts2,
4313
4635
  decisionRecords: decisions.records.length,
4314
4636
  diagnostics: decisions.diagnostics,
4315
4637
  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."
@@ -4378,6 +4700,8 @@ async function setupProject(dir, options = {}) {
4378
4700
  const setupBefore = await readText(ws.root, SETUP_PATH);
4379
4701
  const mcpFingerprint = hash((await inspectHostConfig(ws.root, host, mcp.after)).mcp);
4380
4702
  const desiredHooks = hookConfig(host, hookCommand(host)).hooks;
4703
+ const configuredHook = desiredHooks.SessionStart[0].hooks[0].command;
4704
+ const ownership = await ownershipEdit(ws.root, [...instructions, mcp, hooks[0]], { [hooks[0].path]: configuredHook });
4381
4705
  const fingerprint = hash({
4382
4706
  command: "mason",
4383
4707
  mcp: mcpFingerprint,
@@ -4404,8 +4728,10 @@ async function setupProject(dir, options = {}) {
4404
4728
  revision
4405
4729
  });
4406
4730
  const changedFiles = [];
4731
+ if (await applyEdit(ws.root, ownership)) changedFiles.push(ownership.path);
4407
4732
  for (const edit of edits) if (await applyEdit(ws.root, edit)) changedFiles.push(edit.path);
4408
4733
  if (await applyEdit(ws.root, { path: SETUP_PATH, before: setupBefore, after: JSON.stringify(setup, null, 2) + "\n" })) changedFiles.push(SETUP_PATH);
4734
+ if (await applyEdit(ws.root, await completedHookOwnership(ws.root, hooks[0].path, configuredHook)) && !changedFiles.includes(ownership.path)) changedFiles.push(ownership.path);
4409
4735
  const checked = await automate(ws.root, { event: "turn_start" });
4410
4736
  const configured = await inspectHostConfig(ws.root, host, void 0);
4411
4737
  await writeStoreJson(ws.root, receiptPath, {
@@ -4464,6 +4790,7 @@ var init_setup = __esm({
4464
4790
  init_adapters();
4465
4791
  init_model();
4466
4792
  init_status();
4793
+ init_ownership();
4467
4794
  }
4468
4795
  });
4469
4796
 
@@ -4471,7 +4798,7 @@ var init_setup = __esm({
4471
4798
  import fs16 from "fs/promises";
4472
4799
  import path21 from "path";
4473
4800
  import { createHash as createHash4 } from "crypto";
4474
- import { z as z16 } from "zod";
4801
+ import { z as z18 } from "zod";
4475
4802
  async function readBundle(root) {
4476
4803
  const bytes = await fs16.readFile(await storePath(root, "bundle.json"));
4477
4804
  const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
@@ -4502,13 +4829,13 @@ var init_bundle = __esm({
4502
4829
  "use strict";
4503
4830
  init_storage();
4504
4831
  targets = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
4505
- digest2 = z16.string().regex(/^[a-f0-9]{64}$/);
4506
- bundleSchema = z16.object({
4507
- format: z16.literal(1),
4508
- version: z16.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
4509
- target: z16.enum(targets),
4510
- nodeVersion: z16.string(),
4511
- files: z16.record(digest2)
4832
+ digest2 = z18.string().regex(/^[a-f0-9]{64}$/);
4833
+ bundleSchema = z18.object({
4834
+ format: z18.literal(1),
4835
+ version: z18.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
4836
+ target: z18.enum(targets),
4837
+ nodeVersion: z18.string(),
4838
+ files: z18.record(digest2)
4512
4839
  }).strict();
4513
4840
  sha256 = (bytes) => createHash4("sha256").update(bytes).digest("hex");
4514
4841
  }
@@ -4521,7 +4848,7 @@ import path22 from "path";
4521
4848
  import { execFile as execFile13 } from "child_process";
4522
4849
  import { promisify as promisify13 } from "util";
4523
4850
  import { createHash as createHash5, randomUUID as randomUUID7 } from "crypto";
4524
- import { z as z17 } from "zod";
4851
+ import { z as z19 } from "zod";
4525
4852
  async function configurePath(bin, previous, save, options = {}) {
4526
4853
  const env = options.env ?? process.env, platform = options.platform ?? process.platform;
4527
4854
  const changes = [...previous];
@@ -4681,9 +5008,9 @@ var init_path = __esm({
4681
5008
  "src/distribution/path.ts"() {
4682
5009
  "use strict";
4683
5010
  execute2 = promisify13(execFile13);
4684
- pathChangeSchema = z17.discriminatedUnion("kind", [
4685
- z17.object({ kind: z17.literal("profile"), file: z17.string(), block: z17.string(), created: z17.boolean() }),
4686
- z17.object({ kind: z17.literal("windows"), entry: z17.string(), created: z17.boolean() })
5011
+ pathChangeSchema = z19.discriminatedUnion("kind", [
5012
+ z19.object({ kind: z19.literal("profile"), file: z19.string(), block: z19.string(), created: z19.boolean() }),
5013
+ z19.object({ kind: z19.literal("windows"), entry: z19.string(), created: z19.boolean() })
4687
5014
  ]);
4688
5015
  quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
4689
5016
  normalizeWindowsEntry = (value) => path22.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
@@ -4710,7 +5037,7 @@ import path23 from "path";
4710
5037
  import { fileURLToPath } from "url";
4711
5038
  import { spawn } from "child_process";
4712
5039
  import { randomUUID as randomUUID8 } from "crypto";
4713
- import { z as z18 } from "zod";
5040
+ import { z as z20 } from "zod";
4714
5041
  async function replace(file, content2, mode = 384) {
4715
5042
  const temp = file + ".tmp-" + randomUUID8();
4716
5043
  try {
@@ -4870,15 +5197,15 @@ var init_install2 = __esm({
4870
5197
  init_storage();
4871
5198
  init_store();
4872
5199
  init_path();
4873
- recordSchema2 = z18.object({
4874
- format: z18.literal(1),
4875
- home: z18.string(),
4876
- bin: z18.string(),
4877
- current: z18.string().regex(/^[a-f0-9]{24}$/),
4878
- version: z18.string(),
4879
- launchers: z18.record(z18.string()),
4880
- previousLaunchers: z18.record(z18.string()).optional(),
4881
- pathChanges: z18.array(pathChangeSchema).optional()
5200
+ recordSchema2 = z20.object({
5201
+ format: z20.literal(1),
5202
+ home: z20.string(),
5203
+ bin: z20.string(),
5204
+ current: z20.string().regex(/^[a-f0-9]{24}$/),
5205
+ version: z20.string(),
5206
+ launchers: z20.record(z20.string()),
5207
+ previousLaunchers: z20.record(z20.string()).optional(),
5208
+ pathChanges: z20.array(pathChangeSchema).optional()
4882
5209
  });
4883
5210
  shQuote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
4884
5211
  psQuote = (s) => "'" + s.replace(/'/g, "''") + "'";
@@ -4898,9 +5225,23 @@ init_adapters();
4898
5225
  init_install();
4899
5226
  init_store();
4900
5227
  import { parseArgs } from "util";
4901
- var USAGE = `Usage: mason-auto <setup|install|config|status|check|hook> [options]
5228
+ var TEARDOWN_USAGE = `Usage: mason teardown [options]
5229
+
5230
+ Disconnect Mason from this Git project. Defaults to all project hosts.
5231
+
5232
+ --host codex|claude Disconnect only this host
5233
+ --dir <path> Project directory (defaults to cwd; subdirectories work)
5234
+ --dry-run Preview changes without writing
5235
+ --json Machine-readable changes and diagnostics
5236
+
5237
+ Keeps shared knowledge, repair evidence, ignore rules and the Mason installation.
5238
+ Edited or ambiguous entries are retained for manual cleanup. Restart assistants
5239
+ after teardown. Run mason setup --host codex or --host claude to reconnect.
5240
+ Exit 0: complete/no-op. Exit 2: cleanup needs attention (also in dry runs).`;
5241
+ var USAGE = `Usage: mason-auto <setup|teardown|install|config|status|check|hook> [options]
4902
5242
 
4903
5243
  setup [--host claude|codex] Connect MCP, instructions and hooks to mason on PATH; retain the initial audit
5244
+ teardown [--host claude|codex] Disconnect one host or all project hosts; retain knowledge and evidence
4904
5245
  install --host claude|codex Merge lifecycle hooks into this project's host config
4905
5246
  config --host claude|codex Print the host config without writing
4906
5247
  status Read configured hooks and observed runtime events
@@ -4909,9 +5250,11 @@ var USAGE = `Usage: mason-auto <setup|install|config|status|check|hook> [options
4909
5250
 
4910
5251
  --dir <path> Project directory (defaults to cwd)
4911
5252
  --command <prefix> Installed executable prefix for install/config
5253
+ --dry-run Preview teardown without writing
4912
5254
  --json Machine-readable output (status also uses JSON when piped)
4913
5255
 
4914
5256
  check exits 0 for verified checks, 1 for issues, 2 for incomplete/unavailable.
5257
+ teardown exits 0 for complete/no-op, 2 when cleanup needs attention (also in dry runs).
4915
5258
  Hooks are advisory and exit 0; a failed capture is reported explicitly.
4916
5259
  Local evidence is written under .mason/reports/. No LLM calls or source edits.`;
4917
5260
  var parseCli = (argv) => parseArgs({ args: argv, allowPositionals: true, options: {
@@ -4919,7 +5262,8 @@ var parseCli = (argv) => parseArgs({ args: argv, allowPositionals: true, options
4919
5262
  host: { type: "string" },
4920
5263
  command: { type: "string" },
4921
5264
  json: { type: "boolean" },
4922
- help: { type: "boolean", short: "h" }
5265
+ help: { type: "boolean", short: "h" },
5266
+ "dry-run": { type: "boolean" }
4923
5267
  } });
4924
5268
  function isHookCommand(argv) {
4925
5269
  try {
@@ -4937,12 +5281,20 @@ async function runAutomationCli(argv, stdin = "", io = {
4937
5281
  try {
4938
5282
  const { values, positionals } = parseCli(argv);
4939
5283
  if (values.help || !positionals.length) {
4940
- io.out(USAGE);
5284
+ io.out(positionals[0] === "teardown" ? TEARDOWN_USAGE : USAGE);
4941
5285
  return 0;
4942
5286
  }
4943
5287
  if (positionals.length !== 1) throw new Error("Expected one command.");
4944
5288
  [action] = positionals;
4945
5289
  const dir = values.dir ?? process.cwd();
5290
+ if (values["dry-run"] !== void 0 && action !== "teardown") throw new Error("--dry-run applies only to teardown.");
5291
+ if (action === "teardown") {
5292
+ if (values.command !== void 0) throw new Error("--command applies only to install/config.");
5293
+ const { teardownProject: teardownProject2, summarizeTeardown: summarizeTeardown2 } = await Promise.resolve().then(() => (init_teardown(), teardown_exports));
5294
+ const result = await teardownProject2(dir, { host: values.host === void 0 ? void 0 : hostSchema.parse(values.host), dryRun: values["dry-run"] });
5295
+ io.out(values.json ? JSON.stringify(result, null, 2) : summarizeTeardown2(result));
5296
+ return result.status === "complete" ? 0 : 2;
5297
+ }
4946
5298
  if (action === "hook") {
4947
5299
  const output = await runAutomationHook(hostSchema.parse(values.host), stdin);
4948
5300
  if (output) io.out(JSON.stringify(output));
@@ -4973,12 +5325,12 @@ async function runAutomationCli(argv, stdin = "", io = {
4973
5325
  io.out(values.json ? JSON.stringify(report, null, 2) : summarize(report));
4974
5326
  return report.status === "verified" ? 0 : report.status === "issues-remain" ? 1 : 2;
4975
5327
  } catch (error) {
4976
- const message2 = action === "setup" ? "Mason setup incomplete: " + automationFailure(error).message + ". Rerun the same setup command to resume; retained audit evidence is preserved." : failureMessage(error);
5328
+ const message2 = action === "setup" ? "Mason setup incomplete: " + automationFailure(error).message + ". Rerun the same setup command to resume; retained audit evidence is preserved." : action === "teardown" ? "Mason teardown incomplete: " + automationFailure(error).message + ". Inspect the reported input, then rerun teardown; knowledge and repair evidence are retained." : failureMessage(error);
4977
5329
  if (action === "hook" || isHookCommand(argv)) {
4978
5330
  io.out(JSON.stringify({ systemMessage: message2 }));
4979
5331
  return 0;
4980
5332
  }
4981
- if (argv.includes("--json")) io.out(JSON.stringify({ version: 1, ...action === "setup" ? { action, status: "incomplete", next: "Rerun the same setup command to resume." } : { status: "unavailable" }, failure: automationFailure(error) }));
5333
+ if (argv.includes("--json")) io.out(JSON.stringify({ version: 1, ...["setup", "teardown"].includes(action) ? { action, status: "incomplete", next: "Inspect the reported input, then rerun the same command to resume." } : { status: "unavailable" }, failure: automationFailure(error) }));
4982
5334
  else io.err(message2);
4983
5335
  return 2;
4984
5336
  }
@@ -4989,11 +5341,12 @@ var inputArgs = process.argv.slice(2);
4989
5341
  var managedLaunch = inputArgs[0] === "--setup-host";
4990
5342
  var setupHost = managedLaunch ? inputArgs.splice(0, 2)[1] : void 0;
4991
5343
  var [command, ...args] = inputArgs;
4992
- var usage = `Mason ${"0.15.0"}
5344
+ var usage = `Mason ${"0.16.0"}
4993
5345
 
4994
5346
  Usage: mason <command> [options]
4995
5347
 
4996
5348
  setup --host codex|claude Connect this project to your assistant
5349
+ teardown [--host codex|claude] Disconnect project integrations; retain knowledge
4997
5350
  status Show configuration, observed use and verification
4998
5351
  check Resume and verify retained documentation findings
4999
5352
  audit Audit project instructions
@@ -5011,9 +5364,9 @@ try {
5011
5364
  const { prepareLaunch: prepareLaunch2 } = await Promise.resolve().then(() => (init_launcher(), launcher_exports));
5012
5365
  await prepareLaunch2(setupHost);
5013
5366
  }
5014
- if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.15.0" }));
5367
+ if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.16.0" }));
5015
5368
  else if (!command || ["--help", "-h", "help"].includes(command)) console.log(usage);
5016
- else if (["--version", "-v"].includes(command)) console.log("0.15.0");
5369
+ else if (["--version", "-v"].includes(command)) console.log("0.16.0");
5017
5370
  else if (command === "internal-install") {
5018
5371
  if (args.length) throw new Error("internal-install takes no arguments.");
5019
5372
  const { installStandalone: installStandalone2 } = await Promise.resolve().then(() => (init_install2(), install_exports2));
@@ -5022,14 +5375,14 @@ try {
5022
5375
  console.log(`Installed Mason ${installed.version} in ${installed.home}.
5023
5376
  ${installed.path.message}`);
5024
5377
  } else if (command === "upgrade" || command === "uninstall") {
5025
- if (args.includes("--help")) console.log(command === "upgrade" ? "Usage: mason upgrade [version]. Configured projects use the upgraded command on their next launch. Restart running assistants." : "Usage: mason uninstall. Removes the standalone user installation; project configuration and knowledge are retained; integrations need Mason reinstalled.");
5378
+ if (args.includes("--help")) console.log(command === "upgrade" ? "Usage: mason upgrade [version]. Configured projects use the upgraded command on their next launch. Restart running assistants." : "Usage: mason uninstall. Removes the standalone user installation; project configuration and knowledge are retained. Run mason teardown in each project first to disconnect its integrations.");
5026
5379
  else {
5027
5380
  const { upgradeStandalone: upgradeStandalone2, uninstallStandalone: uninstallStandalone2 } = await Promise.resolve().then(() => (init_install2(), install_exports2));
5028
5381
  if (args.length > (command === "upgrade" ? 1 : 0)) throw new Error("Unexpected arguments.");
5029
5382
  if (command === "upgrade") process.exitCode = await upgradeStandalone2(args[0]);
5030
5383
  else console.log((process.platform === "win32" ? "Finishing installation cleanup after runtime exit: " : "Removed standalone installation: ") + await uninstallStandalone2() + ". Project configuration and knowledge were retained. Reinstall Mason to use its integrations.");
5031
5384
  }
5032
- } else if (["setup", "status", "check", "auto"].includes(command)) {
5385
+ } else if (["setup", "teardown", "status", "check", "auto"].includes(command)) {
5033
5386
  const forwarded = command === "auto" ? args : [command, ...args];
5034
5387
  let input = "";
5035
5388
  if (isHookCommand(forwarded) && !process.stdin.isTTY) for await (const chunk of process.stdin) {