mason-context 0.15.0 → 0.16.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.
@@ -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.1" : "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
  };
@@ -2920,7 +2920,7 @@ var init_launcher = __esm({
2920
2920
  "use strict";
2921
2921
  init_evidence();
2922
2922
  init_model();
2923
- executingVersion = () => true ? "0.15.0" : "development";
2923
+ executingVersion = () => true ? "0.16.1" : "development";
2924
2924
  mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
2925
2925
  hookCommand = (host) => `mason --setup-host ${host} auto`;
2926
2926
  }
@@ -3124,7 +3124,13 @@ function managedBlock(text2, start, end, body) {
3124
3124
  async function applyEdit(root, edit) {
3125
3125
  const current = await readText(root, edit.path);
3126
3126
  if (current === edit.after) return false;
3127
- if (current !== edit.before) throw new Error("Setup input changed during installation: " + edit.path + ". Rerun setup to resume.");
3127
+ if (current !== edit.before) throw new Error("Integration input changed during update: " + edit.path + ". Rerun the requested command to resume.");
3128
+ if (edit.after === null) {
3129
+ const file2 = await storePath(root, edit.path);
3130
+ if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
3131
+ await fs14.unlink(file2);
3132
+ return true;
3133
+ }
3128
3134
  const file = await storePath(root, edit.path, true);
3129
3135
  const temporary = path18.join(path18.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
3130
3136
  try {
@@ -3136,7 +3142,7 @@ async function applyEdit(root, edit) {
3136
3142
  } finally {
3137
3143
  await handle.close();
3138
3144
  }
3139
- if (await readText(root, edit.path) !== edit.before) throw new Error("Setup input changed during installation: " + edit.path);
3145
+ if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
3140
3146
  await fs14.rename(temporary, file);
3141
3147
  return true;
3142
3148
  } finally {
@@ -3151,27 +3157,100 @@ var init_files2 = __esm({
3151
3157
  }
3152
3158
  });
3153
3159
 
3154
- // src/mcp/init.ts
3160
+ // src/setup/ownership.ts
3161
+ var ownership_exports = {};
3162
+ __export(ownership_exports, {
3163
+ BLOCKS: () => BLOCKS,
3164
+ OWNERSHIP_PATH: () => OWNERSHIP_PATH,
3165
+ blockSpan: () => blockSpan,
3166
+ completedHookOwnership: () => completedHookOwnership,
3167
+ ownershipEdit: () => ownershipEdit,
3168
+ ownershipSchema: () => ownershipSchema
3169
+ });
3155
3170
  import { z as z10 } from "zod";
3171
+ import { parse } from "smol-toml";
3172
+ function blockSpan(text2, start, end) {
3173
+ const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
3174
+ if (!starts && !ends) return null;
3175
+ const from = text2.indexOf(start), to = text2.indexOf(end) + end.length;
3176
+ if (starts !== 1 || ends !== 1 || to <= from) throw new Error("Ambiguous Mason markers; inspect the marked block manually.");
3177
+ return { from, to, text: text2.slice(from, to) };
3178
+ }
3179
+ async function ownershipEdit(root, edits, hookCommands = {}) {
3180
+ const before = await readText(root, OWNERSHIP_PATH);
3181
+ const record = before === null ? { version: 1, files: {} } : ownershipSchema.parse(JSON.parse(before));
3182
+ for (const edit of edits) {
3183
+ const previous = record.files[edit.path];
3184
+ const entry = {
3185
+ created: edit.before === null || (previous?.created ?? false),
3186
+ initialHash: edit.before === null || previous?.initialHash === hash(edit.before) ? hash(edit.after) : previous?.initialHash ?? hash(edit.after),
3187
+ blocks: Object.fromEntries(BLOCKS.flatMap(([start, end]) => {
3188
+ const block = blockSpan(edit.after, start, end);
3189
+ return block ? [[start, hash(block.text)]] : [];
3190
+ }))
3191
+ };
3192
+ 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;
3193
+ const hooks = edit.path === ".codex/hooks.json" || edit.path === ".claude/settings.json" ? JSON.parse(edit.after).hooks : void 0;
3194
+ record.files[edit.path] = {
3195
+ ...entry,
3196
+ ...mcp === void 0 ? {} : { mcpHash: hash(mcp) },
3197
+ ...hookCommands[edit.path] ? { hookCommands: [.../* @__PURE__ */ new Set([...previous?.hookCommands ?? [], hookCommands[edit.path]])] } : {},
3198
+ ...hooks === void 0 ? {} : { hooks: Object.fromEntries(Object.entries(hooks).map(([event, groups]) => [event, groups.flatMap((group) => group.hooks.map(hash))])) }
3199
+ };
3200
+ }
3201
+ return { path: OWNERSHIP_PATH, before, after: JSON.stringify(record, null, 2) + "\n" };
3202
+ }
3203
+ async function completedHookOwnership(root, file, command) {
3204
+ const before = await readText(root, OWNERSHIP_PATH);
3205
+ const record = ownershipSchema.parse(JSON.parse(before ?? "null"));
3206
+ record.files[file].hookCommands = [command];
3207
+ return { path: OWNERSHIP_PATH, before, after: JSON.stringify(record, null, 2) + "\n" };
3208
+ }
3209
+ var OWNERSHIP_PATH, BLOCKS, fileSchema, ownershipSchema;
3210
+ var init_ownership = __esm({
3211
+ "src/setup/ownership.ts"() {
3212
+ "use strict";
3213
+ init_evidence();
3214
+ init_files2();
3215
+ OWNERSHIP_PATH = ".mason/local/integration.json";
3216
+ BLOCKS = [
3217
+ ["<!-- mason:start -->", "<!-- mason:end -->"],
3218
+ ["<!-- mason:agents:start -->", "<!-- mason:agents:end -->"],
3219
+ ["# mason:mcp:start", "# mason:mcp:end"]
3220
+ ];
3221
+ fileSchema = z10.object({
3222
+ created: z10.boolean(),
3223
+ initialHash: z10.string(),
3224
+ blocks: z10.record(z10.string()),
3225
+ mcpHash: z10.string().optional(),
3226
+ hookCommands: z10.array(z10.string()).optional(),
3227
+ hooks: z10.record(z10.array(z10.string())).optional()
3228
+ });
3229
+ ownershipSchema = z10.object({ version: z10.literal(1), files: z10.record(fileSchema) });
3230
+ }
3231
+ });
3232
+
3233
+ // src/mcp/init.ts
3234
+ import { z as z11 } from "zod";
3156
3235
  var markerSchema, CLAUDE_MD_SECTION, ASSISTANT_SETUP, QUICKSTART_PLAYBOOK, MAP_PLAYBOOK;
3157
3236
  var init_init = __esm({
3158
3237
  "src/mcp/init.ts"() {
3159
3238
  "use strict";
3160
3239
  init_storage();
3161
3240
  init_files2();
3162
- markerSchema = z10.object({
3163
- version: z10.literal(1),
3164
- initializedAt: z10.string(),
3165
- features: z10.object({ confluence: z10.boolean().optional() }).passthrough().optional()
3241
+ markerSchema = z11.object({
3242
+ version: z11.literal(1),
3243
+ initializedAt: z11.string(),
3244
+ features: z11.object({ confluence: z11.boolean().optional() }).passthrough().optional()
3166
3245
  }).passthrough();
3167
3246
  CLAUDE_MD_SECTION = `<!-- mason:start -->
3168
3247
  ## Mason project knowledge
3169
3248
 
3170
3249
  Mason provides recorded decisions and file impact over MCP. A concept map is optional.
3171
3250
 
3172
- - 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.
3251
+ - 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.
3173
3252
  - Before editing a file \u2192 \`get_impact\` to check references, tests, and historical change partners.
3174
- - 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.
3253
+ - 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.
3175
3254
  - 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.
3176
3255
  - 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.
3177
3256
  - 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.
@@ -3268,8 +3347,8 @@ __export(config_exports, {
3268
3347
  mcpPath: () => mcpPath
3269
3348
  });
3270
3349
  import { isDeepStrictEqual } from "util";
3271
- import { parse, stringify } from "smol-toml";
3272
- import { z as z11 } from "zod";
3350
+ import { parse as parse2, stringify } from "smol-toml";
3351
+ import { z as z12 } from "zod";
3273
3352
  async function instructionEdits(root, host) {
3274
3353
  const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
3275
3354
  const existing = found.find((f) => f.text !== null);
@@ -3310,7 +3389,7 @@ async function mcpEdit(root, host) {
3310
3389
  const servers2 = object.parse(config2.mcpServers ?? {});
3311
3390
  return { path: file, before, after: JSON.stringify({ ...config2, mcpServers: { ...servers2, mason: managedMcp(servers2.mason, host) } }, null, 2) + "\n" };
3312
3391
  }
3313
- const config = parse(before ?? "");
3392
+ const config = parse2(before ?? "");
3314
3393
  const managed = (before ?? "").includes(TOML_START);
3315
3394
  const servers = config.mcp_servers;
3316
3395
  const desired = managedMcp(servers?.mason, host);
@@ -3322,7 +3401,7 @@ async function mcpEdit(root, host) {
3322
3401
  for (const span of spans.reverse()) base = base.slice(0, span.start) + base.slice(span.end);
3323
3402
  }
3324
3403
  const after = managedBlock(base, TOML_START, TOML_END, stringify({ mcp_servers: { mason: desired } }).trimEnd());
3325
- const parsed = parse(after);
3404
+ const parsed = parse2(after);
3326
3405
  const expected = { ...config, mcp_servers: { ...servers ?? {}, mason: desired } };
3327
3406
  if (!isDeepStrictEqual(parsed, expected)) throw new Error("Could not safely configure the Mason MCP entry without changing other settings.");
3328
3407
  return { path: file, before, after };
@@ -3360,7 +3439,7 @@ async function ancillaryEdits(root) {
3360
3439
  }
3361
3440
  async function inspectHostConfig(root, host, plannedMcp) {
3362
3441
  const text2 = plannedMcp ?? await readText(root, mcpPath(host));
3363
- const config = host === "codex" ? parse(text2 ?? "") : object.parse(JSON.parse(text2 ?? "{}"));
3442
+ const config = host === "codex" ? parse2(text2 ?? "") : object.parse(JSON.parse(text2 ?? "{}"));
3364
3443
  const servers = host === "codex" ? config.mcp_servers : config.mcpServers;
3365
3444
  const hooks = automationConfigSchema.parse(JSON.parse(await readText(root, configPath(host)) ?? "{}"));
3366
3445
  const expected = hookConfig(host, hookCommand(host));
@@ -3385,7 +3464,7 @@ var init_config = __esm({
3385
3464
  init_install();
3386
3465
  init_files2();
3387
3466
  init_launcher();
3388
- object = z11.record(z11.unknown());
3467
+ object = z12.record(z12.unknown());
3389
3468
  mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
3390
3469
  TOML_START = "# mason:mcp:start";
3391
3470
  TOML_END = "# mason:mcp:end";
@@ -3402,24 +3481,28 @@ __export(install_exports, {
3402
3481
  configPath: () => configPath,
3403
3482
  installAutomation: () => installAutomation,
3404
3483
  installedAutomation: () => installedAutomation,
3405
- planAutomationInstall: () => planAutomationInstall
3484
+ planAutomationInstall: () => planAutomationInstall,
3485
+ recordSchema: () => recordSchema
3406
3486
  });
3407
- import { z as z12 } from "zod";
3487
+ import { z as z13 } from "zod";
3408
3488
  async function installationRecord(root) {
3409
3489
  return recordSchema.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
3410
3490
  }
3411
3491
  async function installAutomation(dir, host, command) {
3412
3492
  const ws = await workspace(dir);
3413
- return withLock(ws.root, ".mason/reports/automation-install", () => installLocked(ws.root, host, command));
3493
+ return withLock(ws.root, ".mason/reports/setup-lock", () => installLocked(ws.root, host, command));
3414
3494
  }
3415
3495
  async function planAutomationInstall(root, host, command) {
3416
3496
  const file = configPath(host);
3417
- const existing = automationConfigSchema.parse(await readStoreJson(root, file) ?? {});
3497
+ const before = await readText(root, file);
3498
+ const existing = automationConfigSchema.parse(JSON.parse(before ?? "{}"));
3418
3499
  const record = await installationRecord(root);
3419
3500
  const desired = hookConfig(host, command);
3420
3501
  const newCommand = desired.hooks.SessionStart[0].hooks[0].command;
3421
3502
  const previous = record.hosts[host]?.command;
3422
- const owned = /* @__PURE__ */ new Set([previous, newCommand]);
3503
+ const ownershipText = await readText(root, OWNERSHIP_PATH);
3504
+ const pending = ownershipText === null ? [] : ownershipSchema.parse(JSON.parse(ownershipText)).files[file]?.hookCommands ?? [];
3505
+ const owned = /* @__PURE__ */ new Set([previous, ...pending, newCommand]);
3423
3506
  const hooks = existing.hooks ?? {};
3424
3507
  for (const event of HOOK_EVENTS) {
3425
3508
  hooks[event] = (hooks[event] ?? []).map((group) => ({
@@ -3429,15 +3512,20 @@ async function planAutomationInstall(root, host, command) {
3429
3512
  hooks[event].push(...desired.hooks[event]);
3430
3513
  }
3431
3514
  record.hosts[host] = { command: newCommand };
3432
- return { file, config: { ...existing, hooks }, record, newCommand };
3515
+ return { file, before, config: { ...existing, hooks }, record, newCommand };
3433
3516
  }
3434
3517
  async function installLocked(root, host, command) {
3435
- const { file, config, record, newCommand } = await planAutomationInstall(root, host, command);
3518
+ const { file, before, config, record, newCommand } = await planAutomationInstall(root, host, command);
3436
3519
  const { ancillaryEdits: ancillaryEdits2 } = await Promise.resolve().then(() => (init_config(), config_exports));
3437
3520
  const { applyEdit: applyEdit2 } = await Promise.resolve().then(() => (init_files2(), files_exports));
3438
- for (const edit of await ancillaryEdits2(root)) await applyEdit2(root, edit);
3439
- await writeStoreJson(root, file, config);
3521
+ const { ownershipEdit: ownershipEdit2, completedHookOwnership: completedHookOwnership2 } = await Promise.resolve().then(() => (init_ownership(), ownership_exports));
3522
+ const edit = { path: file, before, after: JSON.stringify(config, null, 2) + "\n" };
3523
+ const ancillary = await ancillaryEdits2(root);
3524
+ await applyEdit2(root, await ownershipEdit2(root, [edit], { [file]: newCommand }));
3525
+ for (const edit2 of ancillary) await applyEdit2(root, edit2);
3526
+ await applyEdit2(root, edit);
3440
3527
  await writeStoreJson(root, AUTOMATION_PATH, record);
3528
+ await applyEdit2(root, await completedHookOwnership2(root, file, newCommand));
3441
3529
  return {
3442
3530
  version: 1,
3443
3531
  host,
@@ -3475,14 +3563,248 @@ var init_install = __esm({
3475
3563
  init_evidence();
3476
3564
  init_adapters();
3477
3565
  init_store();
3478
- groupSchema = z12.object({ hooks: z12.array(z12.object({ type: z12.string(), command: z12.string().optional() }).passthrough()) }).passthrough();
3479
- automationConfigSchema = z12.object({ hooks: z12.record(z12.array(groupSchema)).optional() }).passthrough();
3480
- recordSchema = z12.object({ version: z12.literal(1), hosts: z12.record(z12.object({ command: z12.string() })) });
3566
+ init_files2();
3567
+ init_ownership();
3568
+ groupSchema = z13.object({ hooks: z13.array(z13.object({ type: z13.string(), command: z13.string().optional() }).passthrough()) }).passthrough();
3569
+ automationConfigSchema = z13.object({ hooks: z13.record(z13.array(groupSchema)).optional() }).passthrough();
3570
+ recordSchema = z13.object({ version: z13.literal(1), hosts: z13.record(z13.object({ command: z13.string() })) });
3481
3571
  AUTOMATION_PATH = ".mason/local/automation.json";
3482
3572
  configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
3483
3573
  }
3484
3574
  });
3485
3575
 
3576
+ // src/setup/teardown.ts
3577
+ var teardown_exports = {};
3578
+ __export(teardown_exports, {
3579
+ summarizeTeardown: () => summarizeTeardown,
3580
+ teardownProject: () => teardownProject
3581
+ });
3582
+ import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
3583
+ import { parse as parse3, stringify as stringify2 } from "smol-toml";
3584
+ import fg5 from "fast-glob";
3585
+ import { z as z14 } from "zod";
3586
+ async function planTeardown(root, selected) {
3587
+ const edits = [], diagnostics = [];
3588
+ const inputs = /* @__PURE__ */ new Map();
3589
+ const read = async (file) => {
3590
+ if (!inputs.has(file)) inputs.set(file, await readText(root, file));
3591
+ return inputs.get(file);
3592
+ };
3593
+ const attempt = async (file, action) => {
3594
+ try {
3595
+ await action();
3596
+ } catch (error) {
3597
+ diagnostics.push({ path: file, message: error instanceof Error ? error.message : String(error) });
3598
+ }
3599
+ };
3600
+ const ownershipBefore = await read(OWNERSHIP_PATH);
3601
+ const ownership = ownershipBefore === null ? { version: 1, files: {} } : ownershipSchema.parse(JSON.parse(ownershipBefore));
3602
+ const setupBefore = await read(SETUP_PATH);
3603
+ const setup = setupBefore === null ? null : setupSchema.parse(JSON.parse(setupBefore));
3604
+ const automationBefore = await read(AUTOMATION_PATH);
3605
+ const automation = automationBefore === null ? null : recordSchema.parse(JSON.parse(automationBefore));
3606
+ const commands = (host) => new Set([
3607
+ automation?.hosts[host]?.command,
3608
+ ...ownership.files[configPath(host)]?.hookCommands ?? [],
3609
+ hookConfig(host, hookCommand(host)).hooks.SessionStart[0].hooks[0].command,
3610
+ hookConfig(host).hooks.SessionStart[0].hooks[0].command
3611
+ ].filter((command) => !!command));
3612
+ const possibleHook = (command, host) => commands(host).has(command) || /\bmason(?:-auto|-hook)?\b/.test(command);
3613
+ const add = (file, before, after, empty) => {
3614
+ edits.push({ path: file, before, after: ownership.files[file]?.created && (empty || ownership.files[file].initialHash === hash(before)) ? null : after });
3615
+ };
3616
+ let remainingHost = hosts.some((host) => !selected.includes(host) && (setup?.hosts[host] || automation?.hosts[host] || ownership.files[mcpPath(host)] || ownership.files[configPath(host)]));
3617
+ for (const host of hosts.filter((host2) => !selected.includes(host2))) {
3618
+ await attempt(mcpPath(host), async () => {
3619
+ const text2 = await read(mcpPath(host));
3620
+ const config = host === "codex" ? parse3(text2 ?? "") : object2.parse(JSON.parse(text2 ?? "{}"));
3621
+ const servers = object2.parse((host === "codex" ? config.mcp_servers : config.mcpServers) ?? {});
3622
+ remainingHost ||= servers.mason !== void 0;
3623
+ });
3624
+ await attempt(configPath(host), async () => {
3625
+ const config = automationConfigSchema.parse(JSON.parse(await read(configPath(host)) ?? "{}"));
3626
+ remainingHost ||= Object.values(config.hooks ?? {}).some((groups) => groups.some((group) => group.hooks.some((handler) => handler.command && possibleHook(handler.command, host))));
3627
+ });
3628
+ }
3629
+ for (const host of selected) {
3630
+ const mcpFile = mcpPath(host);
3631
+ await attempt(mcpFile, async () => {
3632
+ const before = await read(mcpFile);
3633
+ if (before === null) return;
3634
+ const config = host === "codex" ? parse3(before) : object2.parse(JSON.parse(before));
3635
+ const key = host === "codex" ? "mcp_servers" : "mcpServers";
3636
+ const servers = object2.parse(config[key] ?? {});
3637
+ if (servers.mason === void 0) {
3638
+ 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.");
3639
+ return;
3640
+ }
3641
+ const expected = ownership.files[mcpFile]?.mcpHash ?? hash(mcpCommand(host));
3642
+ if (hash(servers.mason) !== expected) throw new Error("The Mason MCP entry was edited or its ownership is uncertain; retained for manual cleanup.");
3643
+ delete servers.mason;
3644
+ const desired = { ...config, [key]: servers };
3645
+ if (!Object.keys(servers).length) delete desired[key];
3646
+ if (host === "claude") {
3647
+ add(mcpFile, before, json(desired), !Object.keys(desired).length);
3648
+ return;
3649
+ }
3650
+ const [start, end] = BLOCKS[2], block = blockSpan(before, start, end);
3651
+ if (!block) throw new Error("Mason MCP markers are missing; retained for manual cleanup.");
3652
+ const expectedBlock = ownership.files[mcpFile]?.blocks[start];
3653
+ const canonicalBlock = managedBlock("", start, end, stringify2({ mcp_servers: { mason: mcpCommand(host) } }).trimEnd()).trimEnd();
3654
+ if (expectedBlock ? hash(block.text) !== expectedBlock : block.text.replace(/\r\n/g, "\n") !== canonicalBlock) {
3655
+ throw new Error("The Mason MCP block was edited; retained for manual cleanup.");
3656
+ }
3657
+ const after = before.slice(0, block.from) + before.slice(block.to).replace(/^\r?\n/, "");
3658
+ const parsed = parse3(after);
3659
+ if (!Object.keys(servers).length && parsed[key] !== void 0) desired[key] = {};
3660
+ if (!isDeepStrictEqual2(parsed, desired)) throw new Error("Removing the marked block would change other TOML settings; retained for manual cleanup.");
3661
+ add(mcpFile, before, after, !after.trim());
3662
+ });
3663
+ const hookFile = configPath(host);
3664
+ await attempt(hookFile, async () => {
3665
+ const before = await read(hookFile);
3666
+ if (before === null) return;
3667
+ const config = automationConfigSchema.parse(JSON.parse(before));
3668
+ let changed = false;
3669
+ for (const [event, groups] of Object.entries(config.hooks ?? {})) {
3670
+ const known = ownership.files[hookFile]?.hooks?.[event];
3671
+ const handlers = groups.flatMap((group) => group.hooks);
3672
+ if (known && !handlers.some((handler) => handler.type === "command" && handler.command && commands(host).has(handler.command)) && handlers.some((handler) => !known.includes(hash(handler)))) {
3673
+ diagnostics.push({ path: hookFile, message: "A recorded Mason hook is missing beside a new or edited handler (" + event + "); inspect ownership manually." });
3674
+ }
3675
+ const kept = groups.flatMap((group) => {
3676
+ const handlers2 = group.hooks.filter((handler) => {
3677
+ if (handler.type === "command" && handler.command && commands(host).has(handler.command)) {
3678
+ changed = true;
3679
+ return false;
3680
+ }
3681
+ if (handler.command && possibleHook(handler.command, host)) diagnostics.push({
3682
+ path: hookFile,
3683
+ message: "An unrecognized Mason hook was retained for manual cleanup (" + event + ")."
3684
+ });
3685
+ return true;
3686
+ });
3687
+ return handlers2.length === group.hooks.length ? [group] : handlers2.length ? [{ ...group, hooks: handlers2 }] : [];
3688
+ });
3689
+ if (kept.length || !groups.length) config.hooks[event] = kept;
3690
+ else delete config.hooks[event];
3691
+ }
3692
+ if (!changed) return;
3693
+ if (!Object.keys(config.hooks ?? {}).length) delete config.hooks;
3694
+ add(hookFile, before, json(config), !Object.keys(config).length);
3695
+ });
3696
+ }
3697
+ const instructionEditStart = edits.length;
3698
+ if (!remainingHost && !diagnostics.length) for (const file of DOC_CANDIDATES) await attempt(file, async () => {
3699
+ const before = await read(file);
3700
+ if (before === null) return;
3701
+ let after = before;
3702
+ for (const [start, end] of BLOCKS.slice(0, 2)) {
3703
+ const block = blockSpan(after, start, end);
3704
+ if (!block) continue;
3705
+ const pointer = "Mason project knowledge and shared project instructions:\n@" + (file.startsWith(".claude/") ? "../AGENTS.md" : "AGENTS.md");
3706
+ const canonical = [CLAUDE_MD_SECTION, managedBlock("", start, end, pointer).trimEnd()];
3707
+ const expected = ownership.files[file]?.blocks[start];
3708
+ if (expected ? hash(block.text) !== expected : !canonical.includes(block.text.replace(/\r\n/g, "\n"))) {
3709
+ throw new Error("The Mason instruction block was edited or is unrecognized; retained for manual cleanup.");
3710
+ }
3711
+ after = after.slice(0, block.from) + after.slice(block.to).replace(/^\r?\n/, "");
3712
+ }
3713
+ if (after !== before) add(file, before, after, !after.trim());
3714
+ });
3715
+ if (diagnostics.length) edits.splice(instructionEditStart);
3716
+ if (!diagnostics.length) {
3717
+ const cleanup = [];
3718
+ const receiptFiles = await fg5(
3719
+ selected.map((host) => ".mason/reports/automation/*/setup-" + host + ".json"),
3720
+ { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false }
3721
+ );
3722
+ if (receiptFiles.length > 1e3) throw new Error("Too many setup receipts; inspect local setup history before teardown.");
3723
+ for (const file of receiptFiles.sort()) await attempt(file, async () => {
3724
+ const host = file.endsWith("setup-codex.json") ? "codex" : "claude";
3725
+ await loadSetupReceipt(root, file.slice(0, file.lastIndexOf("/")), host);
3726
+ cleanup.push({ path: file, before: await read(file), after: null });
3727
+ });
3728
+ if (!remainingHost) await attempt(".mason/local/project.json", async () => {
3729
+ const before = await read(".mason/local/project.json");
3730
+ if (before === null) return;
3731
+ z14.object({ version: z14.literal(1), initializedAt: z14.string() }).strict().parse(JSON.parse(before));
3732
+ cleanup.push({ path: ".mason/local/project.json", before, after: null });
3733
+ });
3734
+ if (!diagnostics.length) {
3735
+ for (const host of selected) {
3736
+ if (setup) delete setup.hosts[host];
3737
+ if (automation) delete automation.hosts[host];
3738
+ delete ownership.files[mcpPath(host)];
3739
+ delete ownership.files[configPath(host)];
3740
+ }
3741
+ if (!remainingHost) for (const file of DOC_CANDIDATES) delete ownership.files[file];
3742
+ if (setupBefore !== null) cleanup.push({ path: SETUP_PATH, before: setupBefore, after: Object.keys(setup.hosts).length ? json(setup) : null });
3743
+ if (automationBefore !== null) cleanup.push({ path: AUTOMATION_PATH, before: automationBefore, after: Object.keys(automation.hosts).length ? json(automation) : null });
3744
+ if (ownershipBefore !== null) cleanup.push({ path: OWNERSHIP_PATH, before: ownershipBefore, after: Object.keys(ownership.files).length ? json(ownership) : null });
3745
+ edits.push(...cleanup);
3746
+ }
3747
+ }
3748
+ return { edits: edits.filter((edit) => edit.before !== edit.after), diagnostics, remainingHost, inputs };
3749
+ }
3750
+ async function teardownProject(dir, options = {}) {
3751
+ const { root } = await workspace(dir);
3752
+ const selected = options.host ? [options.host] : [...hosts];
3753
+ const run = async (apply) => {
3754
+ const plan = await planTeardown(root, selected);
3755
+ if (apply) {
3756
+ for (const [file, before] of plan.inputs) if (await readText(root, file) !== before) throw new Error("Input changed during teardown: " + file + ". Rerun teardown.");
3757
+ for (const edit of plan.edits) await applyEdit(root, edit);
3758
+ }
3759
+ return {
3760
+ version: 1,
3761
+ action: "teardown",
3762
+ status: plan.diagnostics.length ? "incomplete" : "complete",
3763
+ root,
3764
+ hosts: selected,
3765
+ dryRun: !!options.dryRun,
3766
+ changes: plan.edits.map((edit) => ({ path: edit.path, action: edit.after === null ? "delete" : "update" })),
3767
+ diagnostics: plan.diagnostics,
3768
+ sharedInstructionsRetained: plan.remainingHost || !!plan.diagnostics.length,
3769
+ 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."
3770
+ };
3771
+ };
3772
+ const preview = await run(false);
3773
+ if (options.dryRun || !preview.changes.length) return preview;
3774
+ return withLock(root, ".mason/reports/setup-lock", () => run(true));
3775
+ }
3776
+ function summarizeTeardown(result) {
3777
+ return [
3778
+ `Mason teardown${result.dryRun ? " preview" : ""}: ${result.status}.`,
3779
+ ...result.changes.map((change) => ` ${result.dryRun ? "Would " : ""}${change.action}: ${change.path}`),
3780
+ ...result.diagnostics.map((item) => ` Retained ${item.path}: ${item.message}`),
3781
+ ...!result.changes.length ? ["No changes."] : [],
3782
+ "Shared knowledge, repair evidence, ignore rules and the installed Mason command are retained.",
3783
+ ...result.sharedInstructionsRetained ? ["Shared instruction cleanup is deferred while another host or incomplete removal needs inspection."] : [],
3784
+ ...result.dryRun ? ["Preview only; no files were changed."] : [result.next]
3785
+ ].join("\n");
3786
+ }
3787
+ var hosts, object2, json;
3788
+ var init_teardown = __esm({
3789
+ "src/setup/teardown.ts"() {
3790
+ "use strict";
3791
+ init_evidence();
3792
+ init_store();
3793
+ init_install();
3794
+ init_adapters();
3795
+ init_docs();
3796
+ init_init();
3797
+ init_files2();
3798
+ init_config();
3799
+ init_launcher();
3800
+ init_model();
3801
+ init_ownership();
3802
+ hosts = ["codex", "claude"];
3803
+ object2 = z14.record(z14.unknown());
3804
+ json = (value) => JSON.stringify(value, null, 2) + "\n";
3805
+ }
3806
+ });
3807
+
3486
3808
  // src/review/cochange.ts
3487
3809
  import { execFile as execFile11 } from "child_process";
3488
3810
  import { promisify as promisify11 } from "util";
@@ -3605,7 +3927,7 @@ var init_types2 = __esm({
3605
3927
  });
3606
3928
 
3607
3929
  // src/review/evidence/vitest.ts
3608
- import { z as z13 } from "zod";
3930
+ import { z as z15 } from "zod";
3609
3931
  function parseVitest(raw, sourceRoot) {
3610
3932
  const report = schema.parse(raw);
3611
3933
  const findings = [], diagnostics = [];
@@ -3654,24 +3976,24 @@ var init_vitest = __esm({
3654
3976
  "use strict";
3655
3977
  init_paths2();
3656
3978
  init_types2();
3657
- count2 = z13.number().int().nonnegative();
3658
- schema = z13.object({
3659
- success: z13.boolean(),
3979
+ count2 = z15.number().int().nonnegative();
3980
+ schema = z15.object({
3981
+ success: z15.boolean(),
3660
3982
  numTotalTests: count2,
3661
3983
  numPassedTests: count2,
3662
3984
  numFailedTests: count2,
3663
3985
  numPendingTests: count2,
3664
3986
  numTodoTests: count2,
3665
3987
  numFailedTestSuites: count2,
3666
- testResults: z13.array(z13.object({
3667
- name: z13.string().min(1),
3668
- status: z13.enum(["passed", "failed"]),
3669
- message: z13.string().optional(),
3670
- assertionResults: z13.array(z13.object({
3671
- fullName: z13.string(),
3672
- status: z13.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
3673
- failureMessages: z13.array(z13.string()).nullable().optional(),
3674
- location: z13.object({ line: count2, column: count2 }).nullable().optional()
3988
+ testResults: z15.array(z15.object({
3989
+ name: z15.string().min(1),
3990
+ status: z15.enum(["passed", "failed"]),
3991
+ message: z15.string().optional(),
3992
+ assertionResults: z15.array(z15.object({
3993
+ fullName: z15.string(),
3994
+ status: z15.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
3995
+ failureMessages: z15.array(z15.string()).nullable().optional(),
3996
+ location: z15.object({ line: count2, column: count2 }).nullable().optional()
3675
3997
  }))
3676
3998
  }))
3677
3999
  });
@@ -3679,7 +4001,7 @@ var init_vitest = __esm({
3679
4001
  });
3680
4002
 
3681
4003
  // src/review/evidence/sarif.ts
3682
- import { z as z14 } from "zod";
4004
+ import { z as z16 } from "zod";
3683
4005
  function parseSarif(raw, sourceRoot) {
3684
4006
  const report = schema2.parse(raw), findings = [], diagnostics = [];
3685
4007
  let active = 0, suppressed = 0, absent = 0, unresolved = 0;
@@ -3770,29 +4092,29 @@ var init_sarif = __esm({
3770
4092
  "use strict";
3771
4093
  init_paths2();
3772
4094
  init_types2();
3773
- index = z14.number().int().nonnegative();
3774
- artifact = z14.object({ uri: z14.string().optional(), uriBaseId: z14.string().optional(), index: index.optional() });
3775
- message = z14.object({ text: z14.string().optional(), markdown: z14.string().optional(), id: z14.string().optional(), arguments: z14.array(z14.string()).optional() });
3776
- level = z14.enum(["error", "warning", "note", "none"]);
3777
- location = z14.object({ physicalLocation: z14.object({ artifactLocation: artifact.optional(), region: z14.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
3778
- schema2 = z14.object({
3779
- version: z14.literal("2.1.0"),
3780
- runs: z14.array(z14.object({
3781
- 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() }) }),
3782
- invocations: z14.array(z14.object({ executionSuccessful: z14.boolean(), commandLine: z14.string().optional(), toolExecutionNotifications: z14.array(z14.object({ level: level.optional(), message })).optional() })).optional(),
3783
- versionControlProvenance: z14.array(z14.object({ revisionId: z14.string().optional() })).optional(),
3784
- originalUriBaseIds: z14.record(artifact).optional(),
3785
- artifacts: z14.array(z14.object({ location: artifact.optional() })).optional(),
3786
- results: z14.array(z14.object({
3787
- ruleId: z14.string().optional(),
4095
+ index = z16.number().int().nonnegative();
4096
+ artifact = z16.object({ uri: z16.string().optional(), uriBaseId: z16.string().optional(), index: index.optional() });
4097
+ message = z16.object({ text: z16.string().optional(), markdown: z16.string().optional(), id: z16.string().optional(), arguments: z16.array(z16.string()).optional() });
4098
+ level = z16.enum(["error", "warning", "note", "none"]);
4099
+ location = z16.object({ physicalLocation: z16.object({ artifactLocation: artifact.optional(), region: z16.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
4100
+ schema2 = z16.object({
4101
+ version: z16.literal("2.1.0"),
4102
+ runs: z16.array(z16.object({
4103
+ 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() }) }),
4104
+ invocations: z16.array(z16.object({ executionSuccessful: z16.boolean(), commandLine: z16.string().optional(), toolExecutionNotifications: z16.array(z16.object({ level: level.optional(), message })).optional() })).optional(),
4105
+ versionControlProvenance: z16.array(z16.object({ revisionId: z16.string().optional() })).optional(),
4106
+ originalUriBaseIds: z16.record(artifact).optional(),
4107
+ artifacts: z16.array(z16.object({ location: artifact.optional() })).optional(),
4108
+ results: z16.array(z16.object({
4109
+ ruleId: z16.string().optional(),
3788
4110
  ruleIndex: index.optional(),
3789
4111
  message,
3790
4112
  level: level.optional(),
3791
- kind: z14.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
3792
- baselineState: z14.enum(["new", "unchanged", "updated", "absent"]).optional(),
3793
- suppressions: z14.array(z14.object({ kind: z14.enum(["inSource", "external"]), status: z14.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
3794
- locations: z14.array(location).optional(),
3795
- relatedLocations: z14.array(location).optional()
4113
+ kind: z16.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
4114
+ baselineState: z16.enum(["new", "unchanged", "updated", "absent"]).optional(),
4115
+ suppressions: z16.array(z16.object({ kind: z16.enum(["inSource", "external"]), status: z16.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
4116
+ locations: z16.array(location).optional(),
4117
+ relatedLocations: z16.array(location).optional()
3796
4118
  })).optional()
3797
4119
  }))
3798
4120
  });
@@ -3801,7 +4123,7 @@ var init_sarif = __esm({
3801
4123
 
3802
4124
  // src/review/evidence.ts
3803
4125
  import path19 from "path";
3804
- import { z as z15 } from "zod";
4126
+ import { z as z17 } from "zod";
3805
4127
  function relativeArtifact(root, file) {
3806
4128
  const relative = path19.isAbsolute(file) ? path19.relative(root, file) : file;
3807
4129
  const normalized = normalizeRepoPath(relative);
@@ -3842,7 +4164,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
3842
4164
  let manifest, raw;
3843
4165
  try {
3844
4166
  manifest = relativeArtifact(root, supplied);
3845
- raw = z15.object({ version: z15.literal(1), checks: z15.array(z15.unknown()) }).parse(await readStoreJson(root, manifest));
4167
+ raw = z17.object({ version: z17.literal(1), checks: z17.array(z17.unknown()) }).parse(await readStoreJson(root, manifest));
3846
4168
  } catch (error) {
3847
4169
  output.diagnostics.push(`${supplied}: ${String(error)}`);
3848
4170
  continue;
@@ -3995,19 +4317,19 @@ var init_evidence2 = __esm({
3995
4317
  init_vitest();
3996
4318
  init_sarif();
3997
4319
  init_types2();
3998
- checkSchema2 = z15.object({
3999
- id: z15.string().min(1).max(100),
4000
- kind: z15.enum(["tests", "static-analysis", "security", "complexity", "duplication"]),
4001
- tool: z15.string().min(1).max(200),
4002
- command: z15.string().min(1).max(2e3),
4003
- commit: z15.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
4004
- workingTreeClean: z15.boolean().optional(),
4005
- source: z15.string().max(2e3).optional(),
4006
- sourceRoot: z15.string().min(1).max(4e3).optional(),
4007
- status: z15.enum(["completed", "skipped", "unavailable"]).default("completed"),
4008
- reason: z15.string().min(1).max(2e3).optional(),
4009
- exitCode: z15.number().int().nullable().optional(),
4010
- report: z15.object({ format: z15.enum(["vitest-json", "sarif"]), path: z15.string().min(1).max(4e3) }).optional()
4320
+ checkSchema2 = z17.object({
4321
+ id: z17.string().min(1).max(100),
4322
+ kind: z17.enum(["tests", "static-analysis", "security", "complexity", "duplication"]),
4323
+ tool: z17.string().min(1).max(200),
4324
+ command: z17.string().min(1).max(2e3),
4325
+ commit: z17.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
4326
+ workingTreeClean: z17.boolean().optional(),
4327
+ source: z17.string().max(2e3).optional(),
4328
+ sourceRoot: z17.string().min(1).max(4e3).optional(),
4329
+ status: z17.enum(["completed", "skipped", "unavailable"]).default("completed"),
4330
+ reason: z17.string().min(1).max(2e3).optional(),
4331
+ exitCode: z17.number().int().nullable().optional(),
4332
+ report: z17.object({ format: z17.enum(["vitest-json", "sarif"]), path: z17.string().min(1).max(4e3) }).optional()
4011
4333
  });
4012
4334
  }
4013
4335
  });
@@ -4233,7 +4555,7 @@ __export(status_exports, {
4233
4555
  setupStatus: () => setupStatus,
4234
4556
  summarizeActivation: () => summarizeActivation
4235
4557
  });
4236
- import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
4558
+ import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
4237
4559
  async function setupStatus(dir) {
4238
4560
  const ws = await workspace(dir);
4239
4561
  const setup = await loadSetup(ws.root);
@@ -4250,7 +4572,7 @@ async function setupStatus(dir) {
4250
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."
4251
4573
  };
4252
4574
  }
4253
- const hosts = {};
4575
+ const hosts2 = {};
4254
4576
  const installed = await installedCommand();
4255
4577
  const automation = await automationStatus(ws.root);
4256
4578
  for (const host of ["codex", "claude"]) {
@@ -4259,8 +4581,8 @@ async function setupStatus(dir) {
4259
4581
  const instructions = await instructionEdits(ws.root, host);
4260
4582
  const instructionsCurrent = instructions.every((edit) => edit.before === edit.after);
4261
4583
  const config = await inspectHostConfig(ws.root, host, void 0);
4262
- const mcp = !config.mcpDisabled && hash(config.mcp) === entry.mcpFingerprint && isDeepStrictEqual2({ command: config.mcp?.command, args: config.mcp?.args }, mcpCommand(host));
4263
- const hooks = isDeepStrictEqual2(config.hooks, hookConfig(host, hookCommand(host)).hooks) && !config.disabled;
4584
+ const mcp = !config.mcpDisabled && hash(config.mcp) === entry.mcpFingerprint && isDeepStrictEqual3({ command: config.mcp?.command, args: config.mcp?.args }, mcpCommand(host));
4585
+ const hooks = isDeepStrictEqual3(config.hooks, hookConfig(host, hookCommand(host)).hooks) && !config.disabled;
4264
4586
  const local = await loadSetupReceipt(ws.root, ws.directory, host);
4265
4587
  const configured = local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
4266
4588
  const observation = await readObservation(ws.root, ws.directory, host, entry.revision, installed.version ?? "unavailable");
@@ -4274,7 +4596,7 @@ async function setupStatus(dir) {
4274
4596
  if (!complete) pending.push("Review/trust the host configuration, then complete a normal task to observe the full hook lifecycle.");
4275
4597
  if (automation.status === "unavailable") pending.push("The latest automation attempt did not establish verification. Run mason-auto check and inspect its diagnostics.");
4276
4598
  const healthy = installed.available && mcp && hooks && instructionsCurrent && configured && automation.status !== "unavailable";
4277
- hosts[host] = {
4599
+ hosts2[host] = {
4278
4600
  status: healthy ? complete && observation?.contextCalls ? "active" : "pending" : "attention",
4279
4601
  runtime: installed.available ? "global (" + installed.version + ")" : "unavailable-on-path",
4280
4602
  mcp: mcp ? "configured" : "changed",
@@ -4286,13 +4608,13 @@ async function setupStatus(dir) {
4286
4608
  pending
4287
4609
  };
4288
4610
  }
4289
- const statuses = Object.values(hosts).map((host) => host.status);
4611
+ const statuses = Object.values(hosts2).map((host) => host.status);
4290
4612
  const decisions = await loadDecisionStore(ws.root);
4291
4613
  return {
4292
4614
  version: 1,
4293
4615
  status: statuses.length && statuses.every((status) => status === "active") ? "active" : statuses.includes("attention") ? "attention" : "pending",
4294
4616
  root: ws.root,
4295
- hosts,
4617
+ hosts: hosts2,
4296
4618
  decisionRecords: decisions.records.length,
4297
4619
  diagnostics: decisions.diagnostics,
4298
4620
  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."
@@ -4361,6 +4683,8 @@ async function setupProject(dir, options = {}) {
4361
4683
  const setupBefore = await readText(ws.root, SETUP_PATH);
4362
4684
  const mcpFingerprint = hash((await inspectHostConfig(ws.root, host, mcp.after)).mcp);
4363
4685
  const desiredHooks = hookConfig(host, hookCommand(host)).hooks;
4686
+ const configuredHook = desiredHooks.SessionStart[0].hooks[0].command;
4687
+ const ownership = await ownershipEdit(ws.root, [...instructions, mcp, hooks[0]], { [hooks[0].path]: configuredHook });
4364
4688
  const fingerprint = hash({
4365
4689
  command: "mason",
4366
4690
  mcp: mcpFingerprint,
@@ -4387,8 +4711,10 @@ async function setupProject(dir, options = {}) {
4387
4711
  revision
4388
4712
  });
4389
4713
  const changedFiles = [];
4714
+ if (await applyEdit(ws.root, ownership)) changedFiles.push(ownership.path);
4390
4715
  for (const edit of edits) if (await applyEdit(ws.root, edit)) changedFiles.push(edit.path);
4391
4716
  if (await applyEdit(ws.root, { path: SETUP_PATH, before: setupBefore, after: JSON.stringify(setup, null, 2) + "\n" })) changedFiles.push(SETUP_PATH);
4717
+ if (await applyEdit(ws.root, await completedHookOwnership(ws.root, hooks[0].path, configuredHook)) && !changedFiles.includes(ownership.path)) changedFiles.push(ownership.path);
4392
4718
  const checked = await automate(ws.root, { event: "turn_start" });
4393
4719
  const configured = await inspectHostConfig(ws.root, host, void 0);
4394
4720
  await writeStoreJson(ws.root, receiptPath, {
@@ -4447,6 +4773,7 @@ var init_setup = __esm({
4447
4773
  init_adapters();
4448
4774
  init_model();
4449
4775
  init_status();
4776
+ init_ownership();
4450
4777
  }
4451
4778
  });
4452
4779
 
@@ -4457,9 +4784,23 @@ init_adapters();
4457
4784
  init_install();
4458
4785
  init_store();
4459
4786
  import { parseArgs } from "util";
4460
- var USAGE = `Usage: mason-auto <setup|install|config|status|check|hook> [options]
4787
+ var TEARDOWN_USAGE = `Usage: mason teardown [options]
4788
+
4789
+ Disconnect Mason from this Git project. Defaults to all project hosts.
4790
+
4791
+ --host codex|claude Disconnect only this host
4792
+ --dir <path> Project directory (defaults to cwd; subdirectories work)
4793
+ --dry-run Preview changes without writing
4794
+ --json Machine-readable changes and diagnostics
4795
+
4796
+ Keeps shared knowledge, repair evidence, ignore rules and the Mason installation.
4797
+ Edited or ambiguous entries are retained for manual cleanup. Restart assistants
4798
+ after teardown. Run mason setup --host codex or --host claude to reconnect.
4799
+ Exit 0: complete/no-op. Exit 2: cleanup needs attention (also in dry runs).`;
4800
+ var USAGE = `Usage: mason-auto <setup|teardown|install|config|status|check|hook> [options]
4461
4801
 
4462
4802
  setup [--host claude|codex] Connect MCP, instructions and hooks to mason on PATH; retain the initial audit
4803
+ teardown [--host claude|codex] Disconnect one host or all project hosts; retain knowledge and evidence
4463
4804
  install --host claude|codex Merge lifecycle hooks into this project's host config
4464
4805
  config --host claude|codex Print the host config without writing
4465
4806
  status Read configured hooks and observed runtime events
@@ -4468,9 +4809,11 @@ var USAGE = `Usage: mason-auto <setup|install|config|status|check|hook> [options
4468
4809
 
4469
4810
  --dir <path> Project directory (defaults to cwd)
4470
4811
  --command <prefix> Installed executable prefix for install/config
4812
+ --dry-run Preview teardown without writing
4471
4813
  --json Machine-readable output (status also uses JSON when piped)
4472
4814
 
4473
4815
  check exits 0 for verified checks, 1 for issues, 2 for incomplete/unavailable.
4816
+ teardown exits 0 for complete/no-op, 2 when cleanup needs attention (also in dry runs).
4474
4817
  Hooks are advisory and exit 0; a failed capture is reported explicitly.
4475
4818
  Local evidence is written under .mason/reports/. No LLM calls or source edits.`;
4476
4819
  var parseCli = (argv2) => parseArgs({ args: argv2, allowPositionals: true, options: {
@@ -4478,7 +4821,8 @@ var parseCli = (argv2) => parseArgs({ args: argv2, allowPositionals: true, optio
4478
4821
  host: { type: "string" },
4479
4822
  command: { type: "string" },
4480
4823
  json: { type: "boolean" },
4481
- help: { type: "boolean", short: "h" }
4824
+ help: { type: "boolean", short: "h" },
4825
+ "dry-run": { type: "boolean" }
4482
4826
  } });
4483
4827
  function isHookCommand(argv2) {
4484
4828
  try {
@@ -4496,12 +4840,20 @@ async function runAutomationCli(argv2, stdin = "", io = {
4496
4840
  try {
4497
4841
  const { values, positionals } = parseCli(argv2);
4498
4842
  if (values.help || !positionals.length) {
4499
- io.out(USAGE);
4843
+ io.out(positionals[0] === "teardown" ? TEARDOWN_USAGE : USAGE);
4500
4844
  return 0;
4501
4845
  }
4502
4846
  if (positionals.length !== 1) throw new Error("Expected one command.");
4503
4847
  [action] = positionals;
4504
4848
  const dir = values.dir ?? process.cwd();
4849
+ if (values["dry-run"] !== void 0 && action !== "teardown") throw new Error("--dry-run applies only to teardown.");
4850
+ if (action === "teardown") {
4851
+ if (values.command !== void 0) throw new Error("--command applies only to install/config.");
4852
+ const { teardownProject: teardownProject2, summarizeTeardown: summarizeTeardown2 } = await Promise.resolve().then(() => (init_teardown(), teardown_exports));
4853
+ const result = await teardownProject2(dir, { host: values.host === void 0 ? void 0 : hostSchema.parse(values.host), dryRun: values["dry-run"] });
4854
+ io.out(values.json ? JSON.stringify(result, null, 2) : summarizeTeardown2(result));
4855
+ return result.status === "complete" ? 0 : 2;
4856
+ }
4505
4857
  if (action === "hook") {
4506
4858
  const output = await runAutomationHook(hostSchema.parse(values.host), stdin);
4507
4859
  if (output) io.out(JSON.stringify(output));
@@ -4532,12 +4884,12 @@ async function runAutomationCli(argv2, stdin = "", io = {
4532
4884
  io.out(values.json ? JSON.stringify(report, null, 2) : summarize(report));
4533
4885
  return report.status === "verified" ? 0 : report.status === "issues-remain" ? 1 : 2;
4534
4886
  } catch (error) {
4535
- const message2 = action === "setup" ? "Mason setup incomplete: " + automationFailure(error).message + ". Rerun the same setup command to resume; retained audit evidence is preserved." : failureMessage(error);
4887
+ 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);
4536
4888
  if (action === "hook" || isHookCommand(argv2)) {
4537
4889
  io.out(JSON.stringify({ systemMessage: message2 }));
4538
4890
  return 0;
4539
4891
  }
4540
- if (argv2.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) }));
4892
+ if (argv2.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) }));
4541
4893
  else io.err(message2);
4542
4894
  return 2;
4543
4895
  }