mason-context 0.16.1 → 0.16.3

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
@@ -248,320 +248,101 @@ var init_storage = __esm({
248
248
  }
249
249
  });
250
250
 
251
- // src/automation/execution.ts
251
+ // src/automation/store.ts
252
+ import fs3 from "fs/promises";
252
253
  import os from "os";
253
- import { randomUUID as randomUUID2 } from "crypto";
254
254
  import { z } from "zod";
255
- function parseExecution(raw) {
256
- try {
257
- return executionSchema.parse(raw);
258
- } catch (error) {
259
- throw new Error("Invalid automation execution store; receipt history was retained.", { cause: error });
260
- }
261
- }
262
- function automationFailure(error) {
263
- const recorded = failureSchema.safeParse(error?.failure);
264
- if (recorded.success) return recorded.data;
265
- const message2 = (error instanceof Error ? error.message : String(error)).replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 700);
266
- const codes = /* @__PURE__ */ new Set();
267
- let cause = error;
268
- for (let i = 0; cause && i < 8; i++) {
269
- codes.add(String(cause.code));
270
- cause = cause.cause;
271
- }
272
- const code = codes.has("ENOSPC") || codes.has("EDQUOT") ? "storage-full" : /changed during|changed while|changed between/.test(message2) ? "inputs-changed" : /Automation is busy/.test(message2) ? "busy" : /not a git repository|unknown revision|bad revision|different history|unreachable/.test(message2) ? "history-unavailable" : error instanceof z.ZodError || error instanceof SyntaxError || /Hook input|Expected one command|Unknown automation command|--host/.test(message2) ? "invalid-input" : /store|baseline|modified|symbolic link|Symlink|automation state|state belongs/.test(message2) ? "invalid-evidence" : [...codes].some((c) => /^E[A-Z]+$/.test(c)) ? "io-error" : "internal";
273
- return { code, message: message2, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
274
- }
275
- function failureMessage(error) {
276
- const failure = automationFailure(error);
277
- return `Mason automation unavailable [${failure.code}]; evidence capture/verification was not established. ${failure.message}` + (failure.receiptRecorded ? "" : " No durable failure receipt was recorded.");
278
- }
279
- async function recordExecution(root, directory, event, run) {
280
- const file = directory + "/execution.json";
281
- const raw = await readStoreJson(root, file);
282
- const log = raw === null ? { version: 1, attempts: [], discardedAttempts: 0 } : parseExecution(raw);
283
- for (const attempt2 of log.attempts) if (attempt2.status === "running") attempt2.status = "unknown";
284
- log.discardedAttempts += Math.max(0, log.attempts.length - 31);
285
- log.attempts = log.attempts.slice(-31);
286
- const started = performance.now();
287
- const attempt = {
288
- id: randomUUID2(),
289
- event,
290
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
291
- pid: process.pid,
292
- host: os.hostname(),
293
- status: "running"
294
- };
295
- log.attempts.push(attempt);
296
- await writeStoreJson(root, file, log);
255
+ function parseState(raw) {
297
256
  try {
298
- const result = await run();
299
- Object.assign(attempt, {
300
- status: "completed",
301
- finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
302
- durationMs: performance.now() - started,
303
- verificationStatus: result.report.status,
304
- reportPath: result.report.reportPath
305
- });
306
- await writeStoreJson(root, file, log);
307
- return result;
257
+ return stateSchema.parse(raw);
308
258
  } catch (error) {
309
- const failure = automationFailure(error);
310
- Object.assign(attempt, { status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), durationMs: performance.now() - started, failure });
311
- delete attempt.verificationStatus;
312
- delete attempt.reportPath;
313
- try {
314
- await writeStoreJson(root, file, { ...log, attempts: log.attempts.map((a) => a === attempt ? { ...a, failure: { ...failure, receiptRecorded: true } } : a) });
315
- failure.receiptRecorded = true;
316
- } catch {
317
- }
318
- throw Object.assign(new Error(failure.message, { cause: error }), { failure });
259
+ throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
319
260
  }
320
261
  }
321
- async function executionStatus(root, directory) {
322
- const raw = await readStoreJson(root, directory + "/execution.json");
323
- if (raw === null) return { status: "not-observed", attempts: [] };
324
- const log = parseExecution(raw);
325
- let lock = null;
326
- if (log.attempts.some((attempt) => attempt.status === "running")) {
262
+ async function withLock(root, directory, run, waitMs = 5e3) {
263
+ const file = await storePath(root, directory + "/lock", true);
264
+ const deadline = Date.now() + waitMs;
265
+ let handle;
266
+ while (!handle) {
327
267
  try {
328
- lock = await readStoreJson(root, directory + "/lock");
329
- } catch {
330
- }
331
- }
332
- for (const attempt of log.attempts) {
333
- if (attempt.status !== "running") continue;
334
- let alive = false;
335
- if (attempt.host === os.hostname() && lock?.pid === attempt.pid && lock.host === attempt.host) {
268
+ handle = await fs3.open(file, "wx", 384);
336
269
  try {
337
- process.kill(attempt.pid, 0);
338
- alive = true;
270
+ await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
339
271
  } catch (error) {
340
- alive = error.code === "EPERM";
272
+ await handle.close().catch(() => {
273
+ });
274
+ handle = void 0;
275
+ await fs3.rm(file, { force: true }).catch(() => {
276
+ });
277
+ throw error;
341
278
  }
279
+ } catch (error) {
280
+ if (error.code !== "EEXIST") throw error;
281
+ try {
282
+ const owner = JSON.parse(await fs3.readFile(file, "utf8"));
283
+ if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
284
+ try {
285
+ process.kill(owner.pid, 0);
286
+ } catch (probe) {
287
+ if (probe.code === "ESRCH") {
288
+ const reclaim = file + ".reclaim";
289
+ let guard;
290
+ try {
291
+ guard = await fs3.open(reclaim, "wx", 384);
292
+ const current = JSON.parse(await fs3.readFile(file, "utf8"));
293
+ if (current.pid === owner.pid && current.host === owner.host) await fs3.unlink(file);
294
+ } finally {
295
+ if (guard) {
296
+ await guard.close();
297
+ await fs3.rm(reclaim, { force: true });
298
+ }
299
+ }
300
+ }
301
+ }
302
+ }
303
+ } catch {
304
+ }
305
+ if (Date.now() >= deadline) throw new Error("Automation is busy or its lock needs inspection: " + file);
306
+ await new Promise((resolve) => setTimeout(resolve, 40));
342
307
  }
343
- if (!alive) attempt.status = "unknown";
344
- }
345
- return { status: log.attempts.at(-1)?.status ?? "not-observed", attempts: log.attempts, discardedAttempts: log.discardedAttempts };
346
- }
347
- var failureSchema, attemptSchema, executionSchema;
348
- var init_execution = __esm({
349
- "src/automation/execution.ts"() {
350
- "use strict";
351
- init_storage();
352
- failureSchema = z.object({
353
- code: z.enum(["inputs-changed", "storage-full", "busy", "invalid-input", "history-unavailable", "invalid-evidence", "io-error", "internal"]),
354
- message: z.string(),
355
- retryable: z.boolean(),
356
- receiptRecorded: z.boolean()
357
- });
358
- attemptSchema = z.object({
359
- id: z.string(),
360
- event: z.string(),
361
- startedAt: z.string(),
362
- finishedAt: z.string().optional(),
363
- durationMs: z.number().nonnegative().optional(),
364
- pid: z.number().int().positive(),
365
- host: z.string(),
366
- status: z.enum(["running", "completed", "failed", "unknown"]),
367
- verificationStatus: z.string().optional(),
368
- reportPath: z.string().optional(),
369
- failure: failureSchema.optional()
370
- });
371
- executionSchema = z.object({ version: z.literal(1), attempts: z.array(attemptSchema).max(32), discardedAttempts: z.number().int().nonnegative().default(0) });
372
- }
373
- });
374
-
375
- // src/test-map.ts
376
- import path4 from "path";
377
- async function buildTestMap(dir) {
378
- const rootDir = path4.resolve(dir);
379
- const access = await createFileAccess(rootDir);
380
- const testPatterns = [
381
- "**/*.test.*",
382
- "**/*.spec.*",
383
- "**/*Test.kt",
384
- "**/*Test.java",
385
- "**/*Tests.kt",
386
- "**/*Tests.java",
387
- "**/test_*.py",
388
- "**/*_test.py",
389
- "**/*_test.go",
390
- "**/*Tests.swift",
391
- "**/*Test.swift",
392
- "**/*_test.rs"
393
- ];
394
- const testFiles = await access.list(testPatterns);
395
- const sourceFiles = await access.list();
396
- const sourceByBaseName = /* @__PURE__ */ new Map();
397
- for (const file of sourceFiles) {
398
- if (testFiles.includes(file)) continue;
399
- const baseName = path4.basename(file).replace(/\.[^.]+$/, "");
400
- const existing = sourceByBaseName.get(baseName) ?? [];
401
- existing.push(file);
402
- sourceByBaseName.set(baseName, existing);
403
- }
404
- const paired = [];
405
- const unmatched = [];
406
- for (const testFile of testFiles) {
407
- const testBaseName = path4.basename(testFile).replace(/\.[^.]+$/, "");
408
- const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
409
- if (!sourceName) {
410
- unmatched.push(testFile);
411
- continue;
412
- }
413
- const candidates = sourceByBaseName.get(sourceName);
414
- if (candidates && candidates.length > 0) {
415
- const testDir = path4.dirname(testFile);
416
- const bestMatch = candidates.reduce((best, candidate) => {
417
- const candidateDir = path4.dirname(candidate);
418
- const bestDir = path4.dirname(best);
419
- const candidateOverlap = commonSegments(testDir, candidateDir);
420
- const bestOverlap = commonSegments(testDir, bestDir);
421
- return candidateOverlap > bestOverlap ? candidate : best;
422
- });
423
- paired.push({
424
- test: testFile,
425
- source: bestMatch,
426
- confidence: candidates.length === 1 ? "exact" : "best-guess"
427
- });
428
- } else {
429
- unmatched.push(testFile);
430
- }
431
- }
432
- return { totalTestFiles: testFiles.length, paired, unmatched };
433
- }
434
- function commonSegments(pathA, pathB) {
435
- const segsA = pathA.split("/");
436
- const segsB = pathB.split("/");
437
- let count3 = 0;
438
- for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
439
- if (segsA[i] === segsB[i]) count3++;
440
- else break;
441
308
  }
442
- return count3;
443
- }
444
- var init_test_map = __esm({
445
- "src/test-map.ts"() {
446
- "use strict";
447
- init_files();
448
- }
449
- });
450
-
451
- // src/snapshot/snapshot.ts
452
- import path5 from "path";
453
- import { execFile as execFile2 } from "child_process";
454
- import { promisify as promisify2 } from "util";
455
- import { z as z2 } from "zod";
456
- async function inspectSnapshot(rootDir) {
457
309
  try {
458
- const raw = await readStoreJson(rootDir, ".mason/snapshot.json");
459
- const snapshot = raw === null ? null : snapshotSchema.parse(raw);
460
- return { status: snapshot ? "available" : "missing", snapshot, diagnostics: [] };
461
- } catch (error) {
462
- return { status: "invalid", snapshot: null, diagnostics: [{
463
- path: ".mason/snapshot.json",
464
- message: error instanceof Error ? error.message : String(error)
465
- }] };
466
- }
467
- }
468
- async function getCurrentGitHash(rootDir) {
469
- try {
470
- const { stdout } = await exec2("git", ["rev-parse", "HEAD"], {
471
- cwd: rootDir
472
- });
473
- return stdout.trim();
474
- } catch {
475
- return "unknown";
310
+ return await run();
311
+ } finally {
312
+ await handle.close();
313
+ await fs3.unlink(file);
476
314
  }
477
315
  }
478
- var exec2, repoPath, verificationFields, featureSchema, flowSchema, snapshotSchema;
479
- var init_snapshot = __esm({
480
- "src/snapshot/snapshot.ts"() {
316
+ var hostSchema, events, stateSchema;
317
+ var init_store = __esm({
318
+ "src/automation/store.ts"() {
481
319
  "use strict";
482
- init_files();
483
- init_files();
484
320
  init_storage();
485
- init_paths();
486
- init_test_map();
487
- exec2 = promisify2(execFile2);
488
- repoPath = z2.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
489
- verificationFields = {
490
- refreshedHash: z2.string().optional(),
491
- verifiedAt: z2.string().optional(),
492
- verifiedHash: z2.string().optional(),
493
- verificationFailed: z2.boolean().optional(),
494
- verificationNote: z2.string().optional()
495
- };
496
- featureSchema = z2.object({
497
- description: z2.string(),
498
- files: z2.array(repoPath),
499
- tests: z2.array(repoPath).optional(),
500
- type: z2.enum(["capability", "infrastructure"]).optional(),
501
- ...verificationFields
502
- }).passthrough();
503
- flowSchema = z2.object({ description: z2.string(), chain: z2.array(repoPath), ...verificationFields }).passthrough();
504
- snapshotSchema = z2.object({
505
- version: z2.literal(2),
506
- createdAt: z2.string(),
507
- updatedAt: z2.string(),
508
- gitHash: z2.string(),
509
- features: z2.record(featureSchema),
510
- flows: z2.record(flowSchema)
511
- }).passthrough();
512
- }
513
- });
514
-
515
- // src/drift/drift.ts
516
- import fs3 from "fs/promises";
517
- import path6 from "path";
518
- import { execFile as execFile3 } from "child_process";
519
- import { promisify as promisify3 } from "util";
520
- function parseChanges(output) {
521
- const fields = output.split("\0");
522
- const changes = [];
523
- for (let i = 0; i < fields.length && fields[i]; ) {
524
- const code = fields[i++];
525
- const first = fields[i++];
526
- if (!first) break;
527
- const second = /^[RC]/.test(code) ? fields[i++] : void 0;
528
- const change = second ? code.startsWith("R") ? { status: "renamed", path: second, previousPath: first } : { status: "added", path: second } : { status: code === "A" ? "added" : code === "D" ? "deleted" : "modified", path: first };
529
- if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/"))) continue;
530
- changes.push(change);
531
- }
532
- return changes;
533
- }
534
- function touchedPaths(changes) {
535
- return [...new Set(changes.flatMap((c) => c.previousPath ? [c.previousPath, c.path] : [c.path]))].sort();
536
- }
537
- async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
538
- if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-")) return null;
539
- try {
540
- const { stdout } = await exec3("git", ["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 });
541
- return parseChanges(stdout);
542
- } catch {
543
- return null;
544
- }
545
- }
546
- async function getWorkingTree(resolvedRoot) {
547
- try {
548
- const [diff, untracked] = await Promise.all([
549
- exec3("git", ["diff", "--name-status", "-z", "-M", "HEAD", "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }),
550
- exec3("git", ["ls-files", "-z", "--others", "--exclude-standard"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 })
551
- ]);
552
- const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
553
- return { available: true, changedFiles: [.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])].sort(), untrackedFiles };
554
- } catch {
555
- return { available: false, changedFiles: [], untrackedFiles: [] };
556
- }
557
- }
558
- var exec3;
559
- var init_drift = __esm({
560
- "src/drift/drift.ts"() {
561
- "use strict";
562
- init_snapshot();
563
- init_paths();
564
- exec3 = promisify3(execFile3);
321
+ hostSchema = z.enum(["claude", "codex"]);
322
+ events = ["session_start", "turn_start", "before_tool", "after_tool", "task_end"];
323
+ stateSchema = z.object({
324
+ version: z.literal(1),
325
+ root: z.string(),
326
+ gitDir: z.string(),
327
+ branch: z.string(),
328
+ baselines: z.array(z.object({ path: z.string(), at: z.string(), event: z.string(), fingerprint: z.string() })).max(128),
329
+ sessions: z.record(z.object({
330
+ host: hostSchema,
331
+ seen: z.string().nullable(),
332
+ continued: z.boolean(),
333
+ initialIssues: z.array(z.string()),
334
+ initialDocs: z.record(z.string().nullable()),
335
+ lastUsed: z.string(),
336
+ mutationObserved: z.boolean(),
337
+ pending: z.record(z.string()),
338
+ coverageGaps: z.array(z.string()),
339
+ events: z.record(z.object({ at: z.string(), count: z.number().int().positive() }))
340
+ })),
341
+ updatedAt: z.string(),
342
+ fingerprint: z.string().nullable(),
343
+ latest: z.string().nullable(),
344
+ analysis: z.object({ id: z.string(), fingerprint: z.string(), completedAt: z.number(), path: z.string() }).optional()
345
+ });
565
346
  }
566
347
  });
567
348
 
@@ -849,8 +630,8 @@ var init_claims = __esm({
849
630
  });
850
631
 
851
632
  // src/audit/git.ts
852
- import { execFile as execFile4 } from "child_process";
853
- import { promisify as promisify4 } from "util";
633
+ import { execFile as execFile2 } from "child_process";
634
+ import { promisify as promisify2 } from "util";
854
635
  function parseCommitLine(line) {
855
636
  const parts = line.split(" ");
856
637
  if (parts.length < 3 || !parts[0]) return null;
@@ -858,7 +639,7 @@ function parseCommitLine(line) {
858
639
  }
859
640
  async function lastCommitOf(resolvedRoot, relPath) {
860
641
  try {
861
- const { stdout } = await exec4(
642
+ const { stdout } = await exec2(
862
643
  "git",
863
644
  ["log", "-1", `--format=${COMMIT_FORMAT}`, "--", relPath],
864
645
  { cwd: resolvedRoot }
@@ -871,7 +652,7 @@ async function lastCommitOf(resolvedRoot, relPath) {
871
652
  }
872
653
  async function deletingCommitOf(resolvedRoot, relPath) {
873
654
  try {
874
- const { stdout } = await exec4(
655
+ const { stdout } = await exec2(
875
656
  "git",
876
657
  [
877
658
  "log",
@@ -891,7 +672,7 @@ async function deletingCommitOf(resolvedRoot, relPath) {
891
672
  }
892
673
  async function firstCommitOf(resolvedRoot, relPath) {
893
674
  try {
894
- const { stdout } = await exec4(
675
+ const { stdout } = await exec2(
895
676
  "git",
896
677
  ["log", "--reverse", `--format=${COMMIT_FORMAT}`, "--", relPath],
897
678
  { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
@@ -905,7 +686,7 @@ async function firstCommitOf(resolvedRoot, relPath) {
905
686
  async function commitsTouchingSince(resolvedRoot, fromHash, pathspecs) {
906
687
  if (!fromHash || fromHash === "unknown") return null;
907
688
  try {
908
- const { stdout } = await exec4(
689
+ const { stdout } = await exec2(
909
690
  "git",
910
691
  [
911
692
  "log",
@@ -930,23 +711,23 @@ async function commitsTouchingSince(resolvedRoot, fromHash, pathspecs) {
930
711
  return null;
931
712
  }
932
713
  }
933
- var exec4, COMMIT_FORMAT;
714
+ var exec2, COMMIT_FORMAT;
934
715
  var init_git = __esm({
935
716
  "src/audit/git.ts"() {
936
717
  "use strict";
937
- exec4 = promisify4(execFile4);
718
+ exec2 = promisify2(execFile2);
938
719
  COMMIT_FORMAT = "%H%x09%cI%x09%s";
939
720
  }
940
721
  });
941
722
 
942
723
  // src/audit/docs.ts
943
724
  import fs4 from "fs/promises";
944
- import path7 from "path";
945
- import { execFile as execFile5 } from "child_process";
946
- import { promisify as promisify5 } from "util";
725
+ import path4 from "path";
726
+ import { execFile as execFile3 } from "child_process";
727
+ import { promisify as promisify3 } from "util";
947
728
  async function isDirty(resolvedRoot, relPath) {
948
729
  try {
949
- const { stdout } = await exec5(
730
+ const { stdout } = await exec3(
950
731
  "git",
951
732
  ["status", "--porcelain", "--", relPath],
952
733
  { cwd: resolvedRoot }
@@ -960,7 +741,7 @@ async function discoverDocs(resolvedRoot) {
960
741
  const docs = await Promise.all(DOC_CANDIDATES.map(async (candidate) => {
961
742
  let content2;
962
743
  try {
963
- content2 = await fs4.readFile(path7.join(resolvedRoot, candidate), "utf-8");
744
+ content2 = await fs4.readFile(path4.join(resolvedRoot, candidate), "utf-8");
964
745
  } catch {
965
746
  return null;
966
747
  }
@@ -976,13 +757,13 @@ async function discoverDocs(resolvedRoot) {
976
757
  }));
977
758
  return docs.filter((doc) => doc !== null);
978
759
  }
979
- var exec5, DOC_CANDIDATES;
760
+ var exec3, DOC_CANDIDATES;
980
761
  var init_docs = __esm({
981
762
  "src/audit/docs.ts"() {
982
763
  "use strict";
983
764
  init_claims();
984
765
  init_git();
985
- exec5 = promisify5(execFile5);
766
+ exec3 = promisify3(execFile3);
986
767
  DOC_CANDIDATES = [
987
768
  "AGENTS.md",
988
769
  "CLAUDE.md",
@@ -991,6 +772,199 @@ var init_docs = __esm({
991
772
  }
992
773
  });
993
774
 
775
+ // src/test-map.ts
776
+ import path5 from "path";
777
+ async function buildTestMap(dir) {
778
+ const rootDir = path5.resolve(dir);
779
+ const access = await createFileAccess(rootDir);
780
+ const testPatterns = [
781
+ "**/*.test.*",
782
+ "**/*.spec.*",
783
+ "**/*Test.kt",
784
+ "**/*Test.java",
785
+ "**/*Tests.kt",
786
+ "**/*Tests.java",
787
+ "**/test_*.py",
788
+ "**/*_test.py",
789
+ "**/*_test.go",
790
+ "**/*Tests.swift",
791
+ "**/*Test.swift",
792
+ "**/*_test.rs"
793
+ ];
794
+ const testFiles = await access.list(testPatterns);
795
+ const sourceFiles = await access.list();
796
+ const sourceByBaseName = /* @__PURE__ */ new Map();
797
+ for (const file of sourceFiles) {
798
+ if (testFiles.includes(file)) continue;
799
+ const baseName = path5.basename(file).replace(/\.[^.]+$/, "");
800
+ const existing = sourceByBaseName.get(baseName) ?? [];
801
+ existing.push(file);
802
+ sourceByBaseName.set(baseName, existing);
803
+ }
804
+ const paired = [];
805
+ const unmatched = [];
806
+ for (const testFile of testFiles) {
807
+ const testBaseName = path5.basename(testFile).replace(/\.[^.]+$/, "");
808
+ const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
809
+ if (!sourceName) {
810
+ unmatched.push(testFile);
811
+ continue;
812
+ }
813
+ const candidates = sourceByBaseName.get(sourceName);
814
+ if (candidates && candidates.length > 0) {
815
+ const testDir = path5.dirname(testFile);
816
+ const bestMatch = candidates.reduce((best, candidate) => {
817
+ const candidateDir = path5.dirname(candidate);
818
+ const bestDir = path5.dirname(best);
819
+ const candidateOverlap = commonSegments(testDir, candidateDir);
820
+ const bestOverlap = commonSegments(testDir, bestDir);
821
+ return candidateOverlap > bestOverlap ? candidate : best;
822
+ });
823
+ paired.push({
824
+ test: testFile,
825
+ source: bestMatch,
826
+ confidence: candidates.length === 1 ? "exact" : "best-guess"
827
+ });
828
+ } else {
829
+ unmatched.push(testFile);
830
+ }
831
+ }
832
+ return { totalTestFiles: testFiles.length, paired, unmatched };
833
+ }
834
+ function commonSegments(pathA, pathB) {
835
+ const segsA = pathA.split("/");
836
+ const segsB = pathB.split("/");
837
+ let count3 = 0;
838
+ for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
839
+ if (segsA[i] === segsB[i]) count3++;
840
+ else break;
841
+ }
842
+ return count3;
843
+ }
844
+ var init_test_map = __esm({
845
+ "src/test-map.ts"() {
846
+ "use strict";
847
+ init_files();
848
+ }
849
+ });
850
+
851
+ // src/snapshot/snapshot.ts
852
+ import path6 from "path";
853
+ import { execFile as execFile4 } from "child_process";
854
+ import { promisify as promisify4 } from "util";
855
+ import { z as z2 } from "zod";
856
+ async function inspectSnapshot(rootDir) {
857
+ try {
858
+ const raw = await readStoreJson(rootDir, ".mason/snapshot.json");
859
+ const snapshot = raw === null ? null : snapshotSchema.parse(raw);
860
+ return { status: snapshot ? "available" : "missing", snapshot, diagnostics: [] };
861
+ } catch (error) {
862
+ return { status: "invalid", snapshot: null, diagnostics: [{
863
+ path: ".mason/snapshot.json",
864
+ message: error instanceof Error ? error.message : String(error)
865
+ }] };
866
+ }
867
+ }
868
+ async function getCurrentGitHash(rootDir) {
869
+ try {
870
+ const { stdout } = await exec4("git", ["rev-parse", "HEAD"], {
871
+ cwd: rootDir
872
+ });
873
+ return stdout.trim();
874
+ } catch {
875
+ return "unknown";
876
+ }
877
+ }
878
+ var exec4, repoPath, verificationFields, featureSchema, flowSchema, snapshotSchema;
879
+ var init_snapshot = __esm({
880
+ "src/snapshot/snapshot.ts"() {
881
+ "use strict";
882
+ init_files();
883
+ init_files();
884
+ init_storage();
885
+ init_paths();
886
+ init_test_map();
887
+ exec4 = promisify4(execFile4);
888
+ repoPath = z2.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
889
+ verificationFields = {
890
+ refreshedHash: z2.string().optional(),
891
+ verifiedAt: z2.string().optional(),
892
+ verifiedHash: z2.string().optional(),
893
+ verificationFailed: z2.boolean().optional(),
894
+ verificationNote: z2.string().optional()
895
+ };
896
+ featureSchema = z2.object({
897
+ description: z2.string(),
898
+ files: z2.array(repoPath),
899
+ tests: z2.array(repoPath).optional(),
900
+ type: z2.enum(["capability", "infrastructure"]).optional(),
901
+ ...verificationFields
902
+ }).passthrough();
903
+ flowSchema = z2.object({ description: z2.string(), chain: z2.array(repoPath), ...verificationFields }).passthrough();
904
+ snapshotSchema = z2.object({
905
+ version: z2.literal(2),
906
+ createdAt: z2.string(),
907
+ updatedAt: z2.string(),
908
+ gitHash: z2.string(),
909
+ features: z2.record(featureSchema),
910
+ flows: z2.record(flowSchema)
911
+ }).passthrough();
912
+ }
913
+ });
914
+
915
+ // src/drift/drift.ts
916
+ import fs5 from "fs/promises";
917
+ import path7 from "path";
918
+ import { execFile as execFile5 } from "child_process";
919
+ import { promisify as promisify5 } from "util";
920
+ function parseChanges(output) {
921
+ const fields = output.split("\0");
922
+ const changes = [];
923
+ for (let i = 0; i < fields.length && fields[i]; ) {
924
+ const code = fields[i++];
925
+ const first = fields[i++];
926
+ if (!first) break;
927
+ const second = /^[RC]/.test(code) ? fields[i++] : void 0;
928
+ const change = second ? code.startsWith("R") ? { status: "renamed", path: second, previousPath: first } : { status: "added", path: second } : { status: code === "A" ? "added" : code === "D" ? "deleted" : "modified", path: first };
929
+ if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/"))) continue;
930
+ changes.push(change);
931
+ }
932
+ return changes;
933
+ }
934
+ function touchedPaths(changes) {
935
+ return [...new Set(changes.flatMap((c) => c.previousPath ? [c.previousPath, c.path] : [c.path]))].sort();
936
+ }
937
+ async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
938
+ if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-")) return null;
939
+ try {
940
+ const { stdout } = await exec5("git", ["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 });
941
+ return parseChanges(stdout);
942
+ } catch {
943
+ return null;
944
+ }
945
+ }
946
+ async function getWorkingTree(resolvedRoot) {
947
+ try {
948
+ const [diff, untracked] = await Promise.all([
949
+ exec5("git", ["diff", "--name-status", "-z", "-M", "HEAD", "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }),
950
+ exec5("git", ["ls-files", "-z", "--others", "--exclude-standard"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 })
951
+ ]);
952
+ const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
953
+ return { available: true, changedFiles: [.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])].sort(), untrackedFiles };
954
+ } catch {
955
+ return { available: false, changedFiles: [], untrackedFiles: [] };
956
+ }
957
+ }
958
+ var exec5;
959
+ var init_drift = __esm({
960
+ "src/drift/drift.ts"() {
961
+ "use strict";
962
+ init_snapshot();
963
+ init_paths();
964
+ exec5 = promisify5(execFile5);
965
+ }
966
+ });
967
+
994
968
  // src/audit/types.ts
995
969
  var ALL_CHECKS;
996
970
  var init_types = __esm({
@@ -1008,11 +982,11 @@ var init_types = __esm({
1008
982
  });
1009
983
 
1010
984
  // src/audit/checks/deleted-reference.ts
1011
- import fs5 from "fs/promises";
985
+ import fs6 from "fs/promises";
1012
986
  import path8 from "path";
1013
987
  async function exists(absPath) {
1014
988
  try {
1015
- await fs5.access(absPath);
989
+ await fs6.access(absPath);
1016
990
  return true;
1017
991
  } catch {
1018
992
  return false;
@@ -1106,14 +1080,14 @@ var init_deleted_reference = __esm({
1106
1080
  });
1107
1081
 
1108
1082
  // src/audit/inputs.ts
1109
- import fs6 from "fs/promises";
1083
+ import fs7 from "fs/promises";
1110
1084
  import * as nativeFs from "fs";
1111
1085
  import path9 from "path";
1112
1086
  import { execFile as execFile6 } from "child_process";
1113
1087
  import { promisify as promisify6 } from "util";
1114
1088
  import fg2 from "fast-glob";
1115
1089
  async function auditInputPath(root, file) {
1116
- if (file === ".") return fs6.realpath(root);
1090
+ if (file === ".") return fs7.realpath(root);
1117
1091
  try {
1118
1092
  return await storePath(root, file);
1119
1093
  } catch (error) {
@@ -1230,7 +1204,7 @@ var init_inputs = __esm({
1230
1204
 
1231
1205
  // src/audit/checks/new-module.ts
1232
1206
  import fg3 from "fast-glob";
1233
- import fs7 from "fs/promises";
1207
+ import fs8 from "fs/promises";
1234
1208
  import path10 from "path";
1235
1209
  function escapeRegExp(text2) {
1236
1210
  return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -1273,7 +1247,7 @@ async function moduleCandidates(root, combinedDocs) {
1273
1247
  const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
1274
1248
  const sources = sourcePaths.filter((file) => !file.split("/").some((part) => part.startsWith(".")));
1275
1249
  const listSubdirs = async (dir) => {
1276
- const entries = await fs7.readdir(await auditInputPath(root, dir), { withFileTypes: true });
1250
+ const entries = await fs8.readdir(await auditInputPath(root, dir), { withFileTypes: true });
1277
1251
  const dirs = entries.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink()) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)).map((entry) => path10.posix.join(dir, entry.name));
1278
1252
  const ignored = await gitIgnoredPaths(root, dirs);
1279
1253
  const visible = new Set(dirs.filter((dir2) => !ignored.has(dir2) || sources.some((file) => file.startsWith(dir2 + "/"))));
@@ -1900,15 +1874,16 @@ var init_provenance = __esm({
1900
1874
  });
1901
1875
 
1902
1876
  // src/decisions/decisions.ts
1903
- import fs8 from "fs/promises";
1877
+ import fs9 from "fs/promises";
1904
1878
  import path13 from "path";
1905
1879
  import { createHash } from "crypto";
1880
+ import { z as z4 } from "zod";
1906
1881
  async function loadDecisionStore(rootDir) {
1907
1882
  const records = [];
1908
1883
  const diagnostics = [];
1909
1884
  let entries;
1910
1885
  try {
1911
- entries = await fs8.readdir(await storePath(rootDir, ".mason/decisions"));
1886
+ entries = await fs9.readdir(await storePath(rootDir, ".mason/decisions"));
1912
1887
  } catch (error) {
1913
1888
  if (error.code !== "ENOENT") diagnostics.push({ path: ".mason/decisions", message: String(error) });
1914
1889
  return { records, diagnostics };
@@ -1926,6 +1901,7 @@ async function loadDecisionStore(rootDir) {
1926
1901
  }
1927
1902
  return { records, diagnostics };
1928
1903
  }
1904
+ var TITLE_MAX_CHARS, BODY_RECOMMENDED_CHARS, BODY_MAX_CHARS, boundedText, decisionTitleSchema, decisionBodySchema;
1929
1905
  var init_decisions = __esm({
1930
1906
  "src/decisions/decisions.ts"() {
1931
1907
  "use strict";
@@ -1934,6 +1910,18 @@ var init_decisions = __esm({
1934
1910
  init_snapshot();
1935
1911
  init_lexical();
1936
1912
  init_provenance();
1913
+ TITLE_MAX_CHARS = 80;
1914
+ BODY_RECOMMENDED_CHARS = 1500;
1915
+ BODY_MAX_CHARS = 2500;
1916
+ boundedText = (field, max, hint) => z4.string({
1917
+ errorMap: (issue, ctx) => ({ message: issue.code === "too_big" ? `${field} has ${ctx.data.length} characters; maximum is ${max} (${ctx.data.length - max} over). ${hint}` : ctx.defaultError })
1918
+ }).trim().min(1, `${field} must be non-empty`).max(max);
1919
+ decisionTitleSchema = boundedText("title", TITLE_MAX_CHARS, "Tighten it to a specific headline.");
1920
+ decisionBodySchema = boundedText(
1921
+ "body",
1922
+ BODY_MAX_CHARS,
1923
+ `Aim for ${BODY_RECOMMENDED_CHARS} characters or fewer. Preserve the rule, reason, and exceptions; put supporting references in sources.`
1924
+ );
1937
1925
  }
1938
1926
  });
1939
1927
 
@@ -2057,7 +2045,7 @@ var init_checks = __esm({
2057
2045
  });
2058
2046
 
2059
2047
  // src/audit/audit.ts
2060
- import fs9 from "fs/promises";
2048
+ import fs10 from "fs/promises";
2061
2049
  import path15 from "path";
2062
2050
  async function computeAudit(rootDir, options = {}) {
2063
2051
  const resolvedRoot = path15.resolve(rootDir);
@@ -2092,7 +2080,7 @@ async function computeAudit(rootDir, options = {}) {
2092
2080
  }
2093
2081
  let decisionsPresent = false;
2094
2082
  try {
2095
- await fs9.access(path15.join(resolvedRoot, ".mason", "decisions"));
2083
+ await fs10.access(path15.join(resolvedRoot, ".mason", "decisions"));
2096
2084
  decisionsPresent = true;
2097
2085
  } catch {
2098
2086
  }
@@ -2129,10 +2117,10 @@ var init_audit = __esm({
2129
2117
  });
2130
2118
 
2131
2119
  // src/audit/repair.ts
2132
- import fs10 from "fs/promises";
2120
+ import fs11 from "fs/promises";
2133
2121
  import path16 from "path";
2134
- import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
2135
- import { z as z4 } from "zod";
2122
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
2123
+ import { z as z5 } from "zod";
2136
2124
  function findingId(finding) {
2137
2125
  const e = finding.evidence;
2138
2126
  let key;
@@ -2185,8 +2173,8 @@ async function stableAudit(root, checks, options = {}) {
2185
2173
  return report;
2186
2174
  }
2187
2175
  async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
2188
- const root = await fs10.realpath(rootDir);
2189
- const selected = z4.array(checkSchema).nonempty().parse(checks);
2176
+ const root = await fs11.realpath(rootDir);
2177
+ const selected = z5.array(checkSchema).nonempty().parse(checks);
2190
2178
  const report = await stableAudit(root, selected, options);
2191
2179
  if (!report) throw new Error("No context files found to prepare a repair.");
2192
2180
  if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
@@ -2197,12 +2185,12 @@ async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
2197
2185
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2198
2186
  report: storedReport
2199
2187
  };
2200
- const baselinePath = ".mason/reports/repairs/" + randomUUID3() + ".json";
2188
+ const baselinePath = ".mason/reports/repairs/" + randomUUID2() + ".json";
2201
2189
  await writeStoreJson(root, baselinePath, { ...payload, digest: digest(payload) });
2202
2190
  return { version: 1, action: "prepare", baselinePath, report };
2203
2191
  }
2204
2192
  async function verifyRepair(rootDir, baselinePath, options = {}) {
2205
- const root = await fs10.realpath(rootDir);
2193
+ const root = await fs11.realpath(rootDir);
2206
2194
  const declaredRoot = path16.resolve(rootDir);
2207
2195
  const relative = path16.isAbsolute(baselinePath) ? path16.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
2208
2196
  const stored = baselineSchema.parse(await readStoreJson(root, relative));
@@ -2277,7 +2265,7 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
2277
2265
  scope: "Original audit checks over current context files and repository evidence. Resolved means no longer detected by that check. Advisories require separate review; this is not a certification of documentation or application correctness."
2278
2266
  };
2279
2267
  }
2280
- var checkSchema, commitSchema, anchorSchema, count, evidenceSchema, findingSchema, issueSchema, advisorySchema, checkResultSchema, reportSchema, baselineSchema, digest;
2268
+ var checkSchema, commitSchema, anchorSchema, count, evidenceSchema, findingSchema, issueSchema, advisorySchema, checkResultSchema, reportSchema, baselineSchema, digest, repairFindingSchema;
2281
2269
  var init_repair = __esm({
2282
2270
  "src/audit/repair.ts"() {
2283
2271
  "use strict";
@@ -2289,95 +2277,102 @@ var init_repair = __esm({
2289
2277
  init_files();
2290
2278
  init_paths();
2291
2279
  init_types();
2292
- checkSchema = z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
2293
- commitSchema = z4.object({ hash: z4.string().regex(/^[a-f0-9]{40,64}$/), date: z4.string(), subject: z4.string() });
2294
- anchorSchema = z4.object({ doc: z4.string(), line: z4.number().int().positive().nullable(), excerpt: z4.string().nullable() });
2295
- count = z4.number().int().nonnegative();
2296
- evidenceSchema = z4.discriminatedUnion("kind", [
2297
- z4.object({
2298
- kind: z4.literal("missing-path"),
2299
- claimed: z4.string(),
2300
- renamedTo: z4.string().nullable(),
2280
+ checkSchema = z5.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
2281
+ commitSchema = z5.object({ hash: z5.string().regex(/^[a-f0-9]{40,64}$/), date: z5.string(), subject: z5.string() });
2282
+ anchorSchema = z5.object({ doc: z5.string(), line: z5.number().int().positive().nullable(), excerpt: z5.string().nullable() });
2283
+ count = z5.number().int().nonnegative();
2284
+ evidenceSchema = z5.discriminatedUnion("kind", [
2285
+ z5.object({
2286
+ kind: z5.literal("missing-path"),
2287
+ claimed: z5.string(),
2288
+ renamedTo: z5.string().nullable(),
2301
2289
  deletedInCommit: commitSchema.nullable(),
2302
- everTracked: z4.boolean(),
2303
- parentDirExists: z4.boolean()
2290
+ everTracked: z5.boolean(),
2291
+ parentDirExists: z5.boolean()
2304
2292
  }),
2305
- z4.object({
2306
- kind: z4.literal("unmentioned-dir"),
2307
- dir: z4.string(),
2293
+ z5.object({
2294
+ kind: z5.literal("unmentioned-dir"),
2295
+ dir: z5.string(),
2308
2296
  sourceFileCount: count,
2309
2297
  firstCommit: commitSchema.nullable(),
2310
- checkedDocs: z4.array(z4.string())
2298
+ checkedDocs: z5.array(z5.string())
2311
2299
  }),
2312
- z4.object({
2313
- kind: z4.literal("count-mismatch"),
2300
+ z5.object({
2301
+ kind: z5.literal("count-mismatch"),
2314
2302
  claimed: count,
2315
2303
  actual: count,
2316
- unit: z4.string(),
2317
- countedFrom: z4.string(),
2318
- members: z4.array(z4.string())
2304
+ unit: z5.string(),
2305
+ countedFrom: z5.string(),
2306
+ members: z5.array(z5.string())
2319
2307
  }),
2320
- z4.object({
2321
- kind: z4.literal("missing-script"),
2322
- scriptName: z4.string(),
2323
- invocation: z4.string(),
2324
- manifestsChecked: z4.array(z4.string()),
2325
- availableScripts: z4.array(z4.string())
2308
+ z5.object({
2309
+ kind: z5.literal("missing-script"),
2310
+ scriptName: z5.string(),
2311
+ invocation: z5.string(),
2312
+ manifestsChecked: z5.array(z5.string()),
2313
+ availableScripts: z5.array(z5.string())
2326
2314
  }),
2327
- z4.object({
2328
- kind: z4.literal("doc-behind-manifests"),
2315
+ z5.object({
2316
+ kind: z5.literal("doc-behind-manifests"),
2329
2317
  docLastCommit: commitSchema,
2330
- manifestCommits: z4.array(commitSchema.extend({ files: z4.array(z4.string()) })),
2318
+ manifestCommits: z5.array(commitSchema.extend({ files: z5.array(z5.string()) })),
2331
2319
  totalCommits: count
2332
2320
  }),
2333
- z4.object({
2334
- kind: z4.literal("decision-anchor"),
2335
- decisionId: z4.string(),
2336
- title: z4.string(),
2337
- changedFiles: z4.array(z4.string()),
2338
- refreshedHash: z4.string(),
2339
- provenance: z4.object({}).passthrough().optional()
2321
+ z5.object({
2322
+ kind: z5.literal("decision-anchor"),
2323
+ decisionId: z5.string(),
2324
+ title: z5.string(),
2325
+ changedFiles: z5.array(z5.string()),
2326
+ refreshedHash: z5.string(),
2327
+ provenance: z5.object({}).passthrough().optional()
2340
2328
  })
2341
2329
  ]);
2342
- findingSchema = z4.object({ message: z4.string(), anchor: anchorSchema, evidence: evidenceSchema });
2330
+ findingSchema = z5.object({ message: z5.string(), anchor: anchorSchema, evidence: evidenceSchema });
2343
2331
  issueSchema = findingSchema.extend({
2344
- type: z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
2345
- confidence: z4.enum(["certain", "likely"])
2332
+ type: z5.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
2333
+ confidence: z5.enum(["certain", "likely"])
2346
2334
  });
2347
- advisorySchema = findingSchema.extend({ type: z4.enum(["deps-changed", "decision-anchor-drift"]) });
2348
- checkResultSchema = z4.object({
2349
- issues: z4.array(issueSchema),
2350
- advisories: z4.array(advisorySchema),
2351
- suppressedAdvisories: z4.array(advisorySchema).optional(),
2352
- skipped: z4.array(z4.object({ check: z4.string(), reason: z4.string(), doc: z4.string().optional() }))
2335
+ advisorySchema = findingSchema.extend({ type: z5.enum(["deps-changed", "decision-anchor-drift"]) });
2336
+ checkResultSchema = z5.object({
2337
+ issues: z5.array(issueSchema),
2338
+ advisories: z5.array(advisorySchema),
2339
+ suppressedAdvisories: z5.array(advisorySchema).optional(),
2340
+ skipped: z5.array(z5.object({ check: z5.string(), reason: z5.string(), doc: z5.string().optional() }))
2353
2341
  });
2354
- reportSchema = z4.object({
2355
- version: z4.literal(1),
2356
- root: z4.string(),
2357
- gitAvailable: z4.literal(true),
2342
+ reportSchema = z5.object({
2343
+ version: z5.literal(1),
2344
+ root: z5.string(),
2345
+ gitAvailable: z5.literal(true),
2358
2346
  headHash: commitSchema.shape.hash,
2359
- checksRun: z4.array(checkSchema).nonempty(),
2360
- docs: z4.array(z4.object({
2361
- path: z4.enum(DOC_CANDIDATES),
2347
+ checksRun: z5.array(checkSchema).nonempty(),
2348
+ docs: z5.array(z5.object({
2349
+ path: z5.enum(DOC_CANDIDATES),
2362
2350
  lastCommit: commitSchema.nullable(),
2363
- dirty: z4.boolean(),
2351
+ dirty: z5.boolean(),
2364
2352
  lineCount: count
2365
2353
  })).nonempty(),
2366
- decisionsChecked: z4.boolean(),
2367
- clean: z4.boolean(),
2368
- issues: z4.array(issueSchema),
2369
- advisories: z4.array(advisorySchema),
2370
- suppressedAdvisories: z4.array(advisorySchema).optional(),
2371
- skippedChecks: z4.array(z4.object({ check: z4.string(), reason: z4.string(), doc: z4.string().optional() }))
2354
+ decisionsChecked: z5.boolean(),
2355
+ clean: z5.boolean(),
2356
+ issues: z5.array(issueSchema),
2357
+ advisories: z5.array(advisorySchema),
2358
+ suppressedAdvisories: z5.array(advisorySchema).optional(),
2359
+ skippedChecks: z5.array(z5.object({ check: z5.string(), reason: z5.string(), doc: z5.string().optional() }))
2372
2360
  });
2373
- baselineSchema = z4.object({
2374
- kind: z4.literal("mason-audit-repair"),
2375
- version: z4.literal(1),
2376
- createdAt: z4.string().datetime(),
2361
+ baselineSchema = z5.object({
2362
+ kind: z5.literal("mason-audit-repair"),
2363
+ version: z5.literal(1),
2364
+ createdAt: z5.string().datetime(),
2377
2365
  report: reportSchema,
2378
- digest: z4.string().regex(/^[a-f0-9]{64}$/)
2366
+ digest: z5.string().regex(/^[a-f0-9]{64}$/)
2379
2367
  });
2380
2368
  digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
2369
+ repairFindingSchema = z5.object({
2370
+ id: z5.string(),
2371
+ original: z5.union([issueSchema, advisorySchema]),
2372
+ status: z5.enum(["resolved", "unresolved", "review-required", "unverified"]),
2373
+ reason: z5.string(),
2374
+ current: z5.union([issueSchema, advisorySchema]).optional()
2375
+ });
2381
2376
  }
2382
2377
  });
2383
2378
 
@@ -2390,19 +2385,19 @@ __export(evidence_exports, {
2390
2385
  readInputs: () => readInputs,
2391
2386
  workspace: () => workspace
2392
2387
  });
2393
- import fs11 from "fs/promises";
2388
+ import fs12 from "fs/promises";
2394
2389
  import path17 from "path";
2395
2390
  import { createHash as createHash3 } from "crypto";
2396
2391
  import { execFile as execFile8 } from "child_process";
2397
2392
  import { promisify as promisify8 } from "util";
2398
2393
  import fg4 from "fast-glob";
2399
- import { z as z5 } from "zod";
2394
+ import { z as z6 } from "zod";
2400
2395
  async function git2(root, ...args2) {
2401
2396
  return (await exec7("git", args2, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout;
2402
2397
  }
2403
2398
  async function workspace(dir) {
2404
- const root = await fs11.realpath((await git2(dir, "rev-parse", "--show-toplevel")).trim());
2405
- const gitDir = await fs11.realpath((await git2(root, "rev-parse", "--absolute-git-dir")).trim());
2399
+ const root = await fs12.realpath((await git2(dir, "rev-parse", "--show-toplevel")).trim());
2400
+ const gitDir = await fs12.realpath((await git2(root, "rev-parse", "--absolute-git-dir")).trim());
2406
2401
  let branch;
2407
2402
  try {
2408
2403
  branch = (await git2(root, "symbolic-ref", "--quiet", "HEAD")).trim();
@@ -2421,7 +2416,7 @@ async function readInputs(root) {
2421
2416
  const head = headText.trim();
2422
2417
  let shallow = null;
2423
2418
  try {
2424
- shallow = await fs11.readFile(path17.resolve(root, shallowPath.trim()), "utf8");
2419
+ shallow = await fs12.readFile(path17.resolve(root, shallowPath.trim()), "utf8");
2425
2420
  } catch (error) {
2426
2421
  if (error.code !== "ENOENT") throw error;
2427
2422
  }
@@ -2434,7 +2429,7 @@ async function readInputs(root) {
2434
2429
  docContents.push([file, text2]);
2435
2430
  for (const claim of text2 ? extractClaims(text2).paths : []) {
2436
2431
  if (internal(claim.path) || claim.path.startsWith(".mason/")) continue;
2437
- const exists2 = async (p) => fs11.access(path17.resolve(root, p)).then(() => true, () => false);
2432
+ const exists2 = async (p) => fs12.access(path17.resolve(root, p)).then(() => true, () => false);
2438
2433
  claims.push([claim.path, await exists2(claim.path), await exists2(path17.dirname(claim.path))]);
2439
2434
  }
2440
2435
  }
@@ -2452,7 +2447,7 @@ async function readInputs(root) {
2452
2447
  }
2453
2448
  const needsWorkspaceScripts = rootScripts !== null && commandClaims.some((claim) => !rootScripts.includes(claim.scriptName));
2454
2449
  const decisionDirectory = await storePath(root, ".mason/decisions");
2455
- const decisionPresence = await fs11.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
2450
+ const decisionPresence = await fs12.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
2456
2451
  if (error.code === "ENOENT") return "absent";
2457
2452
  throw error;
2458
2453
  });
@@ -2521,113 +2516,219 @@ var init_evidence = __esm({
2521
2516
  init_dead_command();
2522
2517
  init_storage();
2523
2518
  exec7 = promisify8(execFile8);
2524
- engineVersion = true ? "0.16.1" : "development";
2519
+ engineVersion = true ? "0.16.3" : "development";
2525
2520
  hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
2526
2521
  content = readAuditInput;
2527
2522
  internal = (file) => file === ".mason" || file === ".mason/reports" || file.startsWith(".mason/reports/");
2528
- cacheSchema = z5.object({ version: z5.literal(1), entries: z5.record(z5.object({ key: z5.string(), result: checkResultSchema })), digest: z5.string() });
2523
+ cacheSchema = z6.object({ version: z6.literal(1), entries: z6.record(z6.object({ key: z6.string(), result: checkResultSchema })), digest: z6.string() });
2529
2524
  }
2530
2525
  });
2531
2526
 
2532
- // src/automation/store.ts
2533
- import fs12 from "fs/promises";
2527
+ // src/automation/execution.ts
2534
2528
  import os2 from "os";
2535
- import { z as z6 } from "zod";
2536
- function parseState(raw) {
2529
+ import fs13 from "fs/promises";
2530
+ import { randomUUID as randomUUID3 } from "crypto";
2531
+ import { z as z7 } from "zod";
2532
+ function parseExecution(raw) {
2537
2533
  try {
2538
- return stateSchema.parse(raw);
2534
+ return executionSchema.parse(raw);
2539
2535
  } catch (error) {
2540
- throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
2536
+ throw new Error("Invalid automation execution store; receipt history was retained.", { cause: error });
2541
2537
  }
2542
2538
  }
2543
- async function withLock(root, directory, run) {
2544
- const file = await storePath(root, directory + "/lock", true);
2545
- const deadline = Date.now() + 5e3;
2546
- let handle;
2547
- while (!handle) {
2539
+ function automationFailure(error) {
2540
+ const recorded = failureSchema.safeParse(error?.failure);
2541
+ if (recorded.success) return recorded.data;
2542
+ const message2 = (error instanceof Error ? error.message : String(error)).replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 700);
2543
+ const codes = /* @__PURE__ */ new Set();
2544
+ let cause = error;
2545
+ for (let i = 0; cause && i < 8; i++) {
2546
+ codes.add(String(cause.code));
2547
+ cause = cause.cause;
2548
+ }
2549
+ const code = codes.has("ENOSPC") || codes.has("EDQUOT") ? "storage-full" : /changed during|changed while|changed between/.test(message2) ? "inputs-changed" : /Automation is busy/.test(message2) ? "busy" : /not a git repository|unknown revision|bad revision|different history|unreachable/.test(message2) ? "history-unavailable" : error instanceof z7.ZodError || error instanceof SyntaxError || /Hook input|Expected one command|Unknown automation command|--host/.test(message2) ? "invalid-input" : /store|baseline|modified|symbolic link|Symlink|automation state|state belongs/.test(message2) ? "invalid-evidence" : [...codes].some((c) => /^E[A-Z]+$/.test(c)) ? "io-error" : "internal";
2550
+ return { code, message: message2, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
2551
+ }
2552
+ function failureMessage(error) {
2553
+ const failure = automationFailure(error);
2554
+ return `Mason automation unavailable [${failure.code}]; evidence capture/verification was not established. ${failure.message}` + (failure.receiptRecorded ? "" : " No durable failure receipt was recorded.");
2555
+ }
2556
+ async function failureNotifications(root, directory) {
2557
+ const raw = await readStoreJson(root, directory + "/notifications.json");
2558
+ return raw === null ? { version: 1, sessions: {} } : notificationsSchema.parse(raw);
2559
+ }
2560
+ async function hookFailureMessage(dir, host, sessionId, error) {
2561
+ const failure = automationFailure(error);
2562
+ if (!failure.receiptRecorded) return failureMessage(error);
2563
+ try {
2564
+ const ws = await workspace(dir);
2565
+ return await withLock(ws.root, ws.directory + "/notifications", async () => {
2566
+ const notices = await failureNotifications(ws.root, ws.directory);
2567
+ const key = hash([host, sessionId]), signature = hash([failure.code, failure.message]);
2568
+ const previous = notices.sessions[key];
2569
+ const execution = await executionStatus(ws.root, ws.directory);
2570
+ const recovered = previous && execution.attempts.some((a) => a.status === "completed" && a.startedAt > previous.lastAt);
2571
+ const repeat = previous?.signature === signature && !recovered;
2572
+ notices.sessions[key] = {
2573
+ signature,
2574
+ code: failure.code,
2575
+ message: failure.message,
2576
+ count: repeat ? previous.count + 1 : 1,
2577
+ lastAt: (/* @__PURE__ */ new Date()).toISOString()
2578
+ };
2579
+ notices.sessions = Object.fromEntries(Object.entries(notices.sessions).sort(([, a], [, b]) => b.lastAt.localeCompare(a.lastAt)).slice(0, 32));
2580
+ await writeStoreJson(ws.root, ws.directory + "/notifications.json", notices);
2581
+ return repeat ? null : failureMessage(error) + " Repeated failures in this session are recorded in mason status --json.";
2582
+ });
2583
+ } catch {
2584
+ return failureMessage(error);
2585
+ }
2586
+ }
2587
+ async function refreshRunning(root, directory, attempts) {
2588
+ for (const attempt of attempts) {
2589
+ if (attempt.status !== "running") continue;
2590
+ let alive = false;
2548
2591
  try {
2549
- handle = await fs12.open(file, "wx", 384);
2550
- try {
2551
- await handle.writeFile(JSON.stringify({ pid: process.pid, host: os2.hostname() }));
2552
- } catch (error) {
2553
- await handle.close().catch(() => {
2554
- });
2555
- handle = void 0;
2556
- await fs12.rm(file, { force: true }).catch(() => {
2557
- });
2558
- throw error;
2592
+ const owner = await readStoreJson(root, attempt.lease ? leasePath(directory, attempt.id) : directory + "/lock");
2593
+ if (attempt.host === os2.hostname() && owner?.pid === attempt.pid && owner.host === attempt.host) {
2594
+ try {
2595
+ process.kill(attempt.pid, 0);
2596
+ alive = true;
2597
+ } catch (error) {
2598
+ alive = error.code === "EPERM";
2599
+ }
2559
2600
  }
2601
+ } catch {
2602
+ }
2603
+ if (!alive) attempt.status = "unknown";
2604
+ }
2605
+ }
2606
+ async function editExecution(root, directory, edit) {
2607
+ return withLock(root, directory + "/execution-lock", async () => {
2608
+ const file = directory + "/execution.json";
2609
+ const raw = await readStoreJson(root, file);
2610
+ const log = raw === null ? { version: 1, attempts: [], discardedAttempts: 0 } : parseExecution(raw);
2611
+ await refreshRunning(root, directory, log.attempts);
2612
+ edit(log);
2613
+ const finished = log.attempts.filter((a) => a.status !== "running");
2614
+ const discard = new Set(finished.slice(0, Math.max(0, finished.length - 32)).map((a) => a.id));
2615
+ log.discardedAttempts += discard.size;
2616
+ log.attempts = log.attempts.filter((a) => !discard.has(a.id));
2617
+ await writeStoreJson(root, file, log);
2618
+ });
2619
+ }
2620
+ async function recordExecution(root, directory, event, run) {
2621
+ const started = performance.now();
2622
+ const attempt = {
2623
+ id: randomUUID3(),
2624
+ event,
2625
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2626
+ pid: process.pid,
2627
+ host: os2.hostname(),
2628
+ status: "running",
2629
+ lease: true
2630
+ };
2631
+ const owner = leasePath(directory, attempt.id);
2632
+ const finish = async (changes) => editExecution(root, directory, (log) => {
2633
+ const current = log.attempts.find((a) => a.id === attempt.id);
2634
+ if (!current) throw new Error("Automation execution receipt disappeared; completion could not be recorded.");
2635
+ Object.assign(current, changes, { finishedAt: (/* @__PURE__ */ new Date()).toISOString(), durationMs: performance.now() - started });
2636
+ });
2637
+ await writeStoreJson(root, owner, { pid: attempt.pid, host: attempt.host });
2638
+ try {
2639
+ await editExecution(root, directory, (log) => {
2640
+ if (log.attempts.filter((a) => a.status === "running").length >= 128) throw new Error("Too many active automation executions; existing receipts were retained.");
2641
+ log.attempts.push(attempt);
2642
+ });
2643
+ try {
2644
+ const result = await run();
2645
+ await finish({ status: "completed", verificationStatus: result.report.status, reportPath: result.report.reportPath });
2646
+ return result;
2560
2647
  } catch (error) {
2561
- if (error.code !== "EEXIST") throw error;
2648
+ const failure = automationFailure(error);
2562
2649
  try {
2563
- const owner = JSON.parse(await fs12.readFile(file, "utf8"));
2564
- if (owner.host === os2.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
2565
- try {
2566
- process.kill(owner.pid, 0);
2567
- } catch (probe) {
2568
- if (probe.code === "ESRCH") {
2569
- const reclaim = file + ".reclaim";
2570
- let guard;
2571
- try {
2572
- guard = await fs12.open(reclaim, "wx", 384);
2573
- const current = JSON.parse(await fs12.readFile(file, "utf8"));
2574
- if (current.pid === owner.pid && current.host === owner.host) await fs12.unlink(file);
2575
- } finally {
2576
- if (guard) {
2577
- await guard.close();
2578
- await fs12.rm(reclaim, { force: true });
2579
- }
2580
- }
2581
- }
2582
- }
2583
- }
2650
+ await finish({ status: "failed", failure: { ...failure, receiptRecorded: true } });
2651
+ failure.receiptRecorded = true;
2584
2652
  } catch {
2585
2653
  }
2586
- if (Date.now() >= deadline) throw new Error("Automation is busy or its lock needs inspection: " + file);
2587
- await new Promise((resolve) => setTimeout(resolve, 40));
2654
+ throw Object.assign(new Error(failure.message, { cause: error }), { failure });
2588
2655
  }
2589
- }
2590
- try {
2591
- return await run();
2592
2656
  } finally {
2593
- await handle.close();
2594
- await fs12.unlink(file);
2657
+ await fs13.rm(await storePath(root, owner), { force: true }).catch(() => {
2658
+ });
2595
2659
  }
2596
2660
  }
2597
- var hostSchema, events, stateSchema;
2598
- var init_store = __esm({
2599
- "src/automation/store.ts"() {
2661
+ async function executionStatus(root, directory) {
2662
+ const raw = await readStoreJson(root, directory + "/execution.json");
2663
+ if (raw === null) return { status: "not-observed", attempts: [] };
2664
+ const log = parseExecution(raw);
2665
+ await refreshRunning(root, directory, log.attempts);
2666
+ const completed = [...log.attempts].reverse().find((a) => a.status === "completed");
2667
+ const outstanding = [...log.attempts].reverse().find((a) => ["failed", "unknown"].includes(a.status) && (!completed || (a.finishedAt ?? a.startedAt) >= completed.startedAt));
2668
+ const status = log.attempts.some((a) => a.status === "running") ? "running" : outstanding?.status ?? log.attempts.at(-1)?.status ?? "not-observed";
2669
+ return { status, attempts: log.attempts, discardedAttempts: log.discardedAttempts };
2670
+ }
2671
+ var failureSchema, attemptSchema, executionSchema, notificationsSchema, leasePath;
2672
+ var init_execution = __esm({
2673
+ "src/automation/execution.ts"() {
2600
2674
  "use strict";
2675
+ init_store();
2676
+ init_evidence();
2601
2677
  init_storage();
2602
- hostSchema = z6.enum(["claude", "codex"]);
2603
- events = ["session_start", "turn_start", "before_tool", "after_tool", "task_end"];
2604
- stateSchema = z6.object({
2605
- version: z6.literal(1),
2606
- root: z6.string(),
2607
- gitDir: z6.string(),
2608
- branch: z6.string(),
2609
- baselines: z6.array(z6.object({ path: z6.string(), at: z6.string(), event: z6.string(), fingerprint: z6.string() })).max(128),
2610
- sessions: z6.record(z6.object({
2611
- host: hostSchema,
2612
- seen: z6.string().nullable(),
2613
- continued: z6.boolean(),
2614
- initialIssues: z6.array(z6.string()),
2615
- initialDocs: z6.record(z6.string().nullable()),
2616
- lastUsed: z6.string(),
2617
- mutationObserved: z6.boolean(),
2618
- pending: z6.record(z6.string()),
2619
- coverageGaps: z6.array(z6.string()),
2620
- events: z6.record(z6.object({ at: z6.string(), count: z6.number().int().positive() }))
2621
- })),
2622
- updatedAt: z6.string(),
2623
- fingerprint: z6.string().nullable(),
2624
- latest: z6.string().nullable()
2678
+ failureSchema = z7.object({
2679
+ code: z7.enum(["inputs-changed", "storage-full", "busy", "invalid-input", "history-unavailable", "invalid-evidence", "io-error", "internal"]),
2680
+ message: z7.string(),
2681
+ retryable: z7.boolean(),
2682
+ receiptRecorded: z7.boolean()
2683
+ });
2684
+ attemptSchema = z7.object({
2685
+ id: z7.string().regex(/^[a-zA-Z0-9_-]+$/),
2686
+ lease: z7.literal(true).optional(),
2687
+ event: z7.string(),
2688
+ startedAt: z7.string(),
2689
+ finishedAt: z7.string().optional(),
2690
+ durationMs: z7.number().nonnegative().optional(),
2691
+ pid: z7.number().int().positive(),
2692
+ host: z7.string(),
2693
+ status: z7.enum(["running", "completed", "failed", "unknown"]),
2694
+ verificationStatus: z7.string().optional(),
2695
+ reportPath: z7.string().optional(),
2696
+ failure: failureSchema.optional()
2625
2697
  });
2698
+ executionSchema = z7.object({ version: z7.literal(1), attempts: z7.array(attemptSchema).max(160), discardedAttempts: z7.number().int().nonnegative().default(0) });
2699
+ notificationsSchema = z7.object({ version: z7.literal(1), sessions: z7.record(z7.object({
2700
+ signature: z7.string(),
2701
+ code: failureSchema.shape.code,
2702
+ message: z7.string(),
2703
+ count: z7.number().int().positive(),
2704
+ lastAt: z7.string()
2705
+ })) });
2706
+ leasePath = (directory, id) => `${directory}/executions/${id}.json`;
2626
2707
  }
2627
2708
  });
2628
2709
 
2629
2710
  // src/automation/runtime.ts
2630
2711
  import { randomUUID as randomUUID4 } from "crypto";
2712
+ import { z as z8 } from "zod";
2713
+ async function observeReadOnlyTool(dir, event) {
2714
+ const ws = await workspace(dir);
2715
+ await withLock(ws.root, ws.directory, async () => {
2716
+ const statePath = ws.directory + "/state.json";
2717
+ const raw = await readStoreJson(ws.root, statePath);
2718
+ if (raw === null) return;
2719
+ const state = parseState(raw);
2720
+ if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
2721
+ throw new Error("Automation state belongs to another branch or worktree; original evidence was retained.");
2722
+ }
2723
+ const session = event.host && event.sessionId ? state.sessions[hash([event.host, event.sessionId])] : null;
2724
+ if (!session) return;
2725
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2726
+ session.lastUsed = now;
2727
+ session.events[event.event] = { at: now, count: (session.events[event.event]?.count ?? 0) + 1 };
2728
+ await writeStoreJson(ws.root, statePath, state);
2729
+ });
2730
+ return { root: ws.root, directory: ws.directory };
2731
+ }
2631
2732
  function summarize(report) {
2632
2733
  const open = report.findings.filter((f) => f.status !== "resolved");
2633
2734
  return [
@@ -2639,176 +2740,234 @@ function summarize(report) {
2639
2740
  "Keep original evidence. Address findings relevant to the authorized task; report unrelated findings and unresolved advisories without approving them."
2640
2741
  ].join("\n");
2641
2742
  }
2743
+ async function loadState(ws) {
2744
+ const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
2745
+ const state = raw === null ? {
2746
+ version: 1,
2747
+ root: ws.root,
2748
+ gitDir: ws.gitDir,
2749
+ branch: ws.branch,
2750
+ baselines: [],
2751
+ sessions: {},
2752
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2753
+ fingerprint: null,
2754
+ latest: null
2755
+ } : parseState(raw);
2756
+ if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
2757
+ throw new Error("Automation state belongs to another branch or worktree; original evidence was retained.");
2758
+ }
2759
+ return state;
2760
+ }
2761
+ async function sharedAnalysis(ws, state, inputs, startedAt) {
2762
+ const pointer = state.analysis;
2763
+ if (!pointer || pointer.fingerprint !== inputs.fingerprint || pointer.completedAt < startedAt) return null;
2764
+ const stored = sharedSchema.parse(await readStoreJson(ws.root, pointer.path));
2765
+ const a = stored.analysis;
2766
+ if (stored.digest !== hash(a) || a.id !== pointer.id || a.fingerprint !== inputs.fingerprint || a.completedAt !== pointer.completedAt || a.report.root !== ws.root || a.report.branch !== ws.branch || a.report.head !== inputs.head || hash(a.report.baselinePaths) !== hash(state.baselines.map((b) => b.path))) {
2767
+ throw new Error("Invalid shared automation evidence; run a new check.");
2768
+ }
2769
+ if (a.baselineDigest !== await baselineDigest(ws, state)) throw new Error("Repair baseline was modified; use the original baseline.");
2770
+ return a;
2771
+ }
2772
+ async function verifyInputs(ws, inputs) {
2773
+ const [after, currentWs] = await Promise.all([readInputs(ws.root), workspace(ws.root)]);
2774
+ if (after.fingerprint !== inputs.fingerprint || currentWs.directory !== ws.directory) {
2775
+ throw new Error("Repository inputs or branch changed during automation; no current verification was recorded. Retry on a stable checkout.");
2776
+ }
2777
+ }
2778
+ async function computeAnalysis(ws, state, inputs, event) {
2779
+ const baselines = [...state.baselines];
2780
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2781
+ const empty = {
2782
+ version: 1,
2783
+ status: "unavailable",
2784
+ root: ws.root,
2785
+ branch: ws.branch,
2786
+ head: inputs.head,
2787
+ baselinePaths: [],
2788
+ reportPath: "",
2789
+ findings: [],
2790
+ diagnostics: ["No AGENTS.md, CLAUDE.md, or .claude/CLAUDE.md exists. Documentation capture is unavailable; other Mason tools remain usable."],
2791
+ checks: { ran: [], reused: [], skipped: [] },
2792
+ counts: { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 },
2793
+ capture: "unknown",
2794
+ scope: SCOPE
2795
+ };
2796
+ if (!baselines.length && Object.values(inputs.docs).every((value) => value === null)) return { baselines, report: empty, cache: null };
2797
+ if (ws.branch === "detached" && state.latest) {
2798
+ const previous = await readStoreJson(ws.root, state.latest);
2799
+ if (!previous?.head || !/^[a-f0-9]{40,64}$/.test(previous.head)) throw new Error("The previous detached checkout evidence is unavailable.");
2800
+ try {
2801
+ await git2(ws.root, "merge-base", "--is-ancestor", previous.head, inputs.head);
2802
+ } catch {
2803
+ throw new Error("Detached checkout moved to a different history; original repair evidence was retained. Inspect that baseline explicitly.");
2804
+ }
2805
+ }
2806
+ let cached = null;
2807
+ const diagnostics = [];
2808
+ try {
2809
+ cached = await readStoreJson(ws.root, ws.directory + "/cache.json");
2810
+ } catch {
2811
+ diagnostics.push("Unreadable automation cache; checks are being recomputed.");
2812
+ }
2813
+ const cache = checkCache(cached, inputs);
2814
+ if (cache.diagnostic) diagnostics.push(cache.diagnostic);
2815
+ const saveBaseline = async () => {
2816
+ if (baselines.length >= 128) throw new Error("128 retained baselines need review; automatic capture stopped without discarding original evidence.");
2817
+ const prepared = await prepareRepair(ws.root, ALL_CHECKS, cache.options);
2818
+ baselines.push({ path: prepared.baselinePath, at: now, event: event.event, fingerprint: inputs.fingerprint });
2819
+ };
2820
+ if (!baselines.length) await saveBaseline();
2821
+ const verifications = [];
2822
+ for (const baseline of baselines) verifications.push(await verifyRepair(ws.root, baseline.path, cache.options));
2823
+ const known = new Set(verifications.flatMap((v) => v.findings.map((f) => f.id)));
2824
+ if (verifications.some((v) => v.newFindings.some((f) => !known.has(findingId(f))))) {
2825
+ await saveBaseline();
2826
+ verifications.push(await verifyRepair(ws.root, baselines.at(-1).path, cache.options));
2827
+ }
2828
+ const merged = /* @__PURE__ */ new Map();
2829
+ for (const verification of verifications) {
2830
+ for (const finding of verification.findings) {
2831
+ const previous = merged.get(finding.id);
2832
+ if (!previous || priority[finding.status] > priority[previous.status]) merged.set(finding.id, finding);
2833
+ }
2834
+ diagnostics.push(...verification.diagnostics);
2835
+ }
2836
+ const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
2837
+ for (const finding of merged.values()) counts[finding.status]++;
2838
+ const report = {
2839
+ version: 1,
2840
+ status: diagnostics.length || verifications.some((v) => v.status === "incomplete") ? "incomplete" : counts.unresolved ? "issues-remain" : "verified",
2841
+ root: ws.root,
2842
+ branch: ws.branch,
2843
+ head: inputs.head,
2844
+ baselinePaths: baselines.map((b) => b.path),
2845
+ reportPath: "",
2846
+ findings: [...merged.values()],
2847
+ diagnostics: [...new Set(diagnostics)],
2848
+ checks: { ran: [...cache.ran], reused: [...cache.reused].filter((name) => !cache.ran.has(name)), skipped: verifications.at(-1).currentAudit?.skippedChecks ?? [] },
2849
+ counts,
2850
+ capture: "unknown",
2851
+ scope: SCOPE
2852
+ };
2853
+ return { baselines, report, cache: cache.ran.size || cached === null || cache.diagnostic ? cache.serialize() : null };
2854
+ }
2855
+ async function analyze(ws, inputs, event, startedAt) {
2856
+ return withLock(ws.root, ws.directory + "/analysis/" + inputs.fingerprint, async () => {
2857
+ const before = await loadState(ws);
2858
+ const shared = await sharedAnalysis(ws, before, inputs, startedAt);
2859
+ if (shared) return { analysis: shared, shared: true };
2860
+ const originalDigest = await baselineDigest(ws, before);
2861
+ const computed = await computeAnalysis(ws, before, inputs, event);
2862
+ await verifyInputs(ws, inputs);
2863
+ if (originalDigest !== await baselineDigest(ws, before)) throw new Error("Repair baseline was modified during automation; original evidence must be inspected.");
2864
+ const id = randomUUID4();
2865
+ const analysis = analysisSchema.parse({
2866
+ id,
2867
+ fingerprint: inputs.fingerprint,
2868
+ completedAt: Date.now(),
2869
+ baselineDigest: await baselineDigest(ws, { ...before, baselines: computed.baselines }),
2870
+ report: computed.report
2871
+ });
2872
+ const analysisPath = ws.directory + "/checks/analysis-" + inputs.fingerprint + ".json";
2873
+ await withLock(ws.root, ws.directory, async () => {
2874
+ const state = await loadState(ws);
2875
+ if (hash(state.baselines) !== hash(before.baselines) || state.analysis?.id !== before.analysis?.id) {
2876
+ throw new Error("Automation evidence changed during analysis; retry against the retained baselines.");
2877
+ }
2878
+ await writeStoreJson(ws.root, analysisPath, { analysis, digest: hash(analysis) });
2879
+ if (computed.cache) await writeStoreJson(ws.root, ws.directory + "/cache.json", computed.cache);
2880
+ state.baselines = computed.baselines;
2881
+ state.analysis = { id, fingerprint: inputs.fingerprint, completedAt: analysis.completedAt, path: analysisPath };
2882
+ await writeStoreJson(ws.root, ws.directory + "/state.json", state);
2883
+ });
2884
+ return { analysis, shared: false };
2885
+ }, Math.max(0, 25e3 - (Date.now() - startedAt)));
2886
+ }
2642
2887
  async function automate(dir, event) {
2888
+ const startedAt = Date.now();
2643
2889
  const ws = await workspace(dir);
2644
- return withLock(ws.root, ws.directory, () => recordExecution(ws.root, ws.directory, event.event, async () => {
2890
+ return recordExecution(ws.root, ws.directory, event.event, async () => {
2645
2891
  const inputs = await readInputs(ws.root);
2646
- const statePath = ws.directory + "/state.json";
2647
- const raw = await readStoreJson(ws.root, statePath);
2648
- const now = (/* @__PURE__ */ new Date()).toISOString();
2649
- const state = raw === null ? {
2650
- version: 1,
2651
- root: ws.root,
2652
- gitDir: ws.gitDir,
2653
- branch: ws.branch,
2654
- baselines: [],
2655
- sessions: {},
2656
- updatedAt: now,
2657
- fingerprint: null,
2658
- latest: null
2659
- } : parseState(raw);
2660
- if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
2661
- throw new Error("Automation state belongs to another branch or worktree; original evidence was retained.");
2662
- }
2663
- if (ws.branch === "detached" && state.latest) {
2664
- const previous = await readStoreJson(ws.root, state.latest);
2665
- if (!previous?.head || !/^[a-f0-9]{40,64}$/.test(previous.head)) throw new Error("The previous detached checkout evidence is unavailable.");
2666
- try {
2667
- await git2(ws.root, "merge-base", "--is-ancestor", previous.head, inputs.head);
2668
- } catch {
2669
- throw new Error("Detached checkout moved to a different history; original repair evidence was retained. Inspect that baseline explicitly.");
2892
+ const result = await analyze(ws, inputs, event, startedAt);
2893
+ if (result.shared) {
2894
+ await verifyInputs(ws, inputs);
2895
+ if (result.analysis.baselineDigest !== await baselineDigest(ws, await loadState(ws))) {
2896
+ throw new Error("Repair baseline was modified during shared automation; original evidence must be inspected.");
2670
2897
  }
2671
2898
  }
2672
- const key = event.host && event.sessionId ? hash([event.host, event.sessionId]) : null;
2673
- const newSession = key !== null && !state.sessions[key];
2674
- if (key && !state.sessions[key]) {
2675
- const keys = Object.keys(state.sessions).sort((a, b) => state.sessions[a].lastUsed.localeCompare(state.sessions[b].lastUsed));
2676
- for (const expired of keys.slice(0, Math.max(0, keys.length - 31))) delete state.sessions[expired];
2677
- state.sessions[key] = {
2678
- host: event.host,
2679
- seen: null,
2680
- continued: false,
2681
- initialIssues: [],
2682
- initialDocs: inputs.docs,
2683
- lastUsed: now,
2684
- mutationObserved: false,
2685
- pending: {},
2686
- coverageGaps: [],
2687
- events: {}
2688
- };
2689
- }
2690
- const session = key ? state.sessions[key] : null;
2691
- if (session) {
2692
- session.lastUsed = now;
2693
- session.events[event.event] = { at: now, count: (session.events[event.event]?.count ?? 0) + 1 };
2694
- if (event.event === "before_tool" && event.mutating && event.toolId) {
2695
- if (Object.keys(session.pending).length >= 128) throw new Error("Too many unfinished tool calls to track pre-edit evidence.");
2696
- session.pending[event.toolId] = inputs.fingerprint;
2899
+ return withLock(ws.root, ws.directory, async () => {
2900
+ const state = await loadState(ws);
2901
+ if (state.analysis?.id !== result.analysis.id) throw new Error("Automation evidence changed during publication; retry against the current inputs.");
2902
+ const report = structuredClone(result.analysis.report);
2903
+ if (result.shared) report.checks = { ...report.checks, ran: [], reused: [.../* @__PURE__ */ new Set([...report.checks.ran, ...report.checks.reused])], shared: true };
2904
+ report.reportPath = ws.directory + "/checks/" + randomUUID4() + ".json";
2905
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2906
+ const key = event.host && event.sessionId ? hash([event.host, event.sessionId]) : null;
2907
+ const newSession = key !== null && !state.sessions[key];
2908
+ if (key && !state.sessions[key]) {
2909
+ const keys = Object.keys(state.sessions).sort((a, b) => state.sessions[a].lastUsed.localeCompare(state.sessions[b].lastUsed));
2910
+ const removable = keys.filter((k) => !Object.keys(state.sessions[k].pending).length);
2911
+ for (const expired of removable.slice(0, Math.max(0, keys.length - 31))) delete state.sessions[expired];
2912
+ if (Object.keys(state.sessions).length >= 128) throw new Error("Too many sessions with unfinished tool calls; original evidence was retained.");
2913
+ state.sessions[key] = {
2914
+ host: event.host,
2915
+ seen: null,
2916
+ continued: false,
2917
+ initialIssues: report.findings.filter((f) => f.status === "unresolved").map((f) => f.id),
2918
+ initialDocs: inputs.docs,
2919
+ lastUsed: now,
2920
+ mutationObserved: false,
2921
+ pending: {},
2922
+ coverageGaps: [],
2923
+ events: {}
2924
+ };
2697
2925
  }
2698
- if (event.event === "after_tool" && event.mutating) {
2699
- session.mutationObserved = true;
2700
- if (!event.toolId || !session.pending[event.toolId]) {
2701
- const gap = "A tool completed without an observed matching pre-tool capture; pre-edit coverage is unknown.";
2702
- if (!session.coverageGaps.includes(gap)) session.coverageGaps.push(gap);
2926
+ const session = key ? state.sessions[key] : null;
2927
+ if (session) {
2928
+ session.lastUsed = now;
2929
+ session.events[event.event] = { at: now, count: (session.events[event.event]?.count ?? 0) + 1 };
2930
+ if (event.event === "before_tool" && event.mutating && event.toolId) {
2931
+ if (Object.keys(session.pending).length >= 128) throw new Error("Too many unfinished tool calls to track pre-edit evidence.");
2932
+ session.pending[event.toolId] = inputs.fingerprint;
2933
+ }
2934
+ if (event.event === "after_tool" && event.mutating) {
2935
+ session.mutationObserved = true;
2936
+ if (!event.toolId || !session.pending[event.toolId]) {
2937
+ const gap = "A tool completed without an observed matching pre-tool capture; pre-edit coverage is unknown.";
2938
+ if (!session.coverageGaps.includes(gap)) session.coverageGaps.push(gap);
2939
+ }
2940
+ if (event.toolId) delete session.pending[event.toolId];
2703
2941
  }
2704
- if (event.toolId) delete session.pending[event.toolId];
2942
+ report.diagnostics.push(...session.coverageGaps);
2943
+ if (session.coverageGaps.length && report.status !== "unavailable") report.status = "incomplete";
2944
+ report.capture = report.status !== "unavailable" && !session.coverageGaps.length && (session.events.session_start || session.events.before_tool) ? "observed" : "unknown";
2705
2945
  }
2706
- }
2707
- if (!state.baselines.length && Object.values(inputs.docs).every((value) => value === null)) {
2708
- const report2 = {
2709
- version: 1,
2710
- status: "unavailable",
2711
- root: ws.root,
2712
- branch: ws.branch,
2713
- head: inputs.head,
2714
- baselinePaths: [],
2715
- reportPath: ws.directory + "/checks/" + randomUUID4() + ".json",
2716
- findings: [],
2717
- diagnostics: ["No AGENTS.md, CLAUDE.md, or .claude/CLAUDE.md exists. Documentation capture is unavailable; other Mason tools remain usable."],
2718
- checks: { ran: [], reused: [], skipped: [] },
2719
- counts: { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 },
2720
- capture: "unknown",
2721
- scope: SCOPE
2722
- };
2723
- const notify2 = !session || session.seen !== "no-docs";
2724
- if (session) session.seen = "no-docs";
2725
- state.fingerprint = inputs.fingerprint;
2726
- state.latest = report2.reportPath;
2727
- state.updatedAt = now;
2728
- await writeStoreJson(ws.root, report2.reportPath, report2);
2729
- await writeStoreJson(ws.root, statePath, state);
2730
- return { report: report2, message: notify2 ? summarize(report2) : null, continueOnce: false };
2731
- }
2732
- let cached = null;
2733
- const diagnostics = [];
2734
- try {
2735
- cached = await readStoreJson(ws.root, ws.directory + "/cache.json");
2736
- } catch {
2737
- diagnostics.push("Unreadable automation cache; checks are being recomputed.");
2738
- }
2739
- const cache = checkCache(cached, inputs);
2740
- if (cache.diagnostic) diagnostics.push(cache.diagnostic);
2741
- const saveBaseline = async () => {
2742
- if (state.baselines.length >= 128) throw new Error("128 retained baselines need review; automatic capture stopped without discarding original evidence.");
2743
- const prepared = await prepareRepair(ws.root, ALL_CHECKS, cache.options);
2744
- state.baselines.push({ path: prepared.baselinePath, at: now, event: event.event, fingerprint: inputs.fingerprint });
2745
- };
2746
- if (!state.baselines.length) await saveBaseline();
2747
- const verifications = [];
2748
- for (const baseline of state.baselines) verifications.push(await verifyRepair(ws.root, baseline.path, cache.options));
2749
- const known = new Set(verifications.flatMap((v) => v.findings.map((f) => f.id)));
2750
- if (verifications.some((v) => v.newFindings.some((f) => !known.has(findingId(f))))) {
2751
- await saveBaseline();
2752
- verifications.push(await verifyRepair(ws.root, state.baselines.at(-1).path, cache.options));
2753
- }
2754
- const merged = /* @__PURE__ */ new Map();
2755
- for (const verification of verifications) {
2756
- for (const finding of verification.findings) {
2757
- const previous = merged.get(finding.id);
2758
- if (!previous || priority[finding.status] > priority[previous.status]) merged.set(finding.id, finding);
2946
+ const signature = hash([report.status, report.findings, report.diagnostics, report.checks.skipped]);
2947
+ const relevant = report.findings.some((f) => f.status === "unresolved" && session && (!session.initialIssues.includes(f.id) || session.initialDocs[f.original.anchor.doc] !== inputs.docs[f.original.anchor.doc]));
2948
+ const continueOnce = event.event === "task_end" && !!session?.mutationObserved && relevant && !session.continued && !event.stopHookActive;
2949
+ const notify = !session || newSession || signature !== session.seen || continueOnce;
2950
+ if (session) {
2951
+ session.seen = signature;
2952
+ if (continueOnce) session.continued = true;
2759
2953
  }
2760
- diagnostics.push(...verification.diagnostics);
2761
- }
2762
- if (newSession && session) session.initialIssues = [...merged.values()].filter((f) => f.status === "unresolved").map((f) => f.id);
2763
- const current = verifications.at(-1).currentAudit;
2764
- const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
2765
- for (const finding of merged.values()) counts[finding.status]++;
2766
- if (session) diagnostics.push(...session.coverageGaps);
2767
- const capture = session && !session.coverageGaps.length && (session.events.session_start || session.events.before_tool) ? "observed" : "unknown";
2768
- const after = await readInputs(ws.root);
2769
- const currentWs = await workspace(ws.root);
2770
- if (after.fingerprint !== inputs.fingerprint || currentWs.directory !== ws.directory) {
2771
- throw new Error("Repository inputs or branch changed during automation; no current verification was recorded. Retry on a stable checkout.");
2772
- }
2773
- const report = {
2774
- version: 1,
2775
- status: diagnostics.length || verifications.some((v) => v.status === "incomplete") ? "incomplete" : counts.unresolved ? "issues-remain" : "verified",
2776
- root: ws.root,
2777
- branch: ws.branch,
2778
- head: inputs.head,
2779
- baselinePaths: state.baselines.map((b) => b.path),
2780
- reportPath: ws.directory + "/checks/" + randomUUID4() + ".json",
2781
- findings: [...merged.values()],
2782
- diagnostics: [...new Set(diagnostics)],
2783
- checks: { ran: [...cache.ran], reused: [...cache.reused].filter((name) => !cache.ran.has(name)), skipped: current?.skippedChecks ?? [] },
2784
- counts,
2785
- capture,
2786
- scope: SCOPE
2787
- };
2788
- const signature = hash([report.status, report.findings, report.diagnostics, report.checks.skipped]);
2789
- const relevant = report.findings.some((f) => f.status === "unresolved" && session && (!session.initialIssues.includes(f.id) || session.initialDocs[f.original.anchor.doc] !== inputs.docs[f.original.anchor.doc]));
2790
- const continueOnce = event.event === "task_end" && !!session?.mutationObserved && relevant && !session.continued && !event.stopHookActive;
2791
- const notify = !session || newSession || signature !== session.seen || continueOnce;
2792
- if (session) {
2793
- session.seen = signature;
2794
- if (continueOnce) session.continued = true;
2795
- }
2796
- const persistReport = !state.latest || state.fingerprint !== inputs.fingerprint || notify || event.event === "task_end";
2797
- if (!persistReport) report.reportPath = state.latest;
2798
- state.updatedAt = now;
2799
- state.fingerprint = inputs.fingerprint;
2800
- state.latest = report.reportPath;
2801
- if (persistReport) await writeStoreJson(ws.root, report.reportPath, report);
2802
- if (cache.ran.size || cached === null || cache.diagnostic) await writeStoreJson(ws.root, ws.directory + "/cache.json", cache.serialize());
2803
- await writeStoreJson(ws.root, statePath, state);
2804
- return { report, message: notify ? summarize(report) : null, continueOnce };
2805
- }));
2954
+ const persistReport = !state.latest || state.fingerprint !== inputs.fingerprint || notify || event.event === "task_end";
2955
+ if (!persistReport) report.reportPath = state.latest;
2956
+ state.updatedAt = now;
2957
+ state.fingerprint = inputs.fingerprint;
2958
+ state.latest = report.reportPath;
2959
+ if (persistReport) await writeStoreJson(ws.root, report.reportPath, report);
2960
+ await writeStoreJson(ws.root, ws.directory + "/state.json", state);
2961
+ return { report, message: notify ? summarize(report) : null, continueOnce };
2962
+ });
2963
+ });
2806
2964
  }
2807
2965
  async function automationStatus(dir) {
2808
2966
  const ws = await workspace(dir);
2809
2967
  const execution = await executionStatus(ws.root, ws.directory);
2968
+ const notifications = await failureNotifications(ws.root, ws.directory);
2810
2969
  const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
2811
- if (raw === null) return { version: 1, status: execution.status === "not-observed" ? "not-observed" : "unavailable", root: ws.root, branch: ws.branch, baselinePaths: [], hosts: {}, execution };
2970
+ if (raw === null) return { version: 1, status: execution.status === "not-observed" ? "not-observed" : "unavailable", root: ws.root, branch: ws.branch, baselinePaths: [], hosts: {}, execution, notifications };
2812
2971
  const state = parseState(raw);
2813
2972
  if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) throw new Error("Automation state belongs to another workspace.");
2814
2973
  const inputs = await readInputs(ws.root);
@@ -2830,10 +2989,11 @@ async function automationStatus(dir) {
2830
2989
  verificationStatus: unfinished ? "unavailable" : latest?.status ?? "unavailable",
2831
2990
  hosts: hosts2,
2832
2991
  execution,
2992
+ notifications,
2833
2993
  note: "Observed events do not prove all tool paths are intercepted. Run check to verify the retained evidence."
2834
2994
  };
2835
2995
  }
2836
- var SCOPE, priority, cleanText;
2996
+ var SCOPE, priority, cleanText, analysisSchema, sharedSchema, baselineDigest;
2837
2997
  var init_runtime = __esm({
2838
2998
  "src/automation/runtime.ts"() {
2839
2999
  "use strict";
@@ -2846,11 +3006,40 @@ var init_runtime = __esm({
2846
3006
  SCOPE = "Documentation audit evidence only. Hook receipts show observed events, not complete interception. Resolved claims no longer fail their checks; advisories need separate review. Repair only within the user's task authorization.";
2847
3007
  priority = { resolved: 0, "review-required": 1, unresolved: 2, unverified: 3 };
2848
3008
  cleanText = (text2) => text2.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 250);
3009
+ analysisSchema = z8.object({
3010
+ id: z8.string(),
3011
+ fingerprint: z8.string(),
3012
+ completedAt: z8.number(),
3013
+ baselineDigest: z8.string(),
3014
+ report: z8.object({
3015
+ version: z8.literal(1),
3016
+ status: z8.enum(["verified", "issues-remain", "incomplete", "unavailable"]),
3017
+ root: z8.string(),
3018
+ branch: z8.string(),
3019
+ head: z8.string(),
3020
+ baselinePaths: z8.array(z8.string()),
3021
+ reportPath: z8.string(),
3022
+ findings: z8.array(repairFindingSchema),
3023
+ diagnostics: z8.array(z8.string()),
3024
+ checks: z8.object({
3025
+ ran: z8.array(z8.string()),
3026
+ reused: z8.array(z8.string()),
3027
+ skipped: z8.array(z8.object({ check: z8.string(), reason: z8.string(), doc: z8.string().optional() }))
3028
+ }),
3029
+ counts: z8.object({ resolved: z8.number(), unresolved: z8.number(), "review-required": z8.number(), unverified: z8.number() }),
3030
+ capture: z8.enum(["observed", "unknown"]),
3031
+ scope: z8.string()
3032
+ })
3033
+ });
3034
+ sharedSchema = z8.object({ analysis: analysisSchema, digest: z8.string() });
3035
+ baselineDigest = async (ws, state) => hash(await Promise.all(
3036
+ state.baselines.map(async (b) => [b.path, await readStoreJson(ws.root, b.path)])
3037
+ ));
2849
3038
  }
2850
3039
  });
2851
3040
 
2852
3041
  // src/setup/model.ts
2853
- import { z as z7 } from "zod";
3042
+ import { z as z9 } from "zod";
2854
3043
  async function loadSetup(root) {
2855
3044
  const raw = await readStoreJson(root, SETUP_PATH);
2856
3045
  if (raw === null) return null;
@@ -2868,27 +3057,27 @@ var init_model = __esm({
2868
3057
  "src/setup/model.ts"() {
2869
3058
  "use strict";
2870
3059
  init_storage();
2871
- setupHostSchema = z7.object({
2872
- configuredVersion: z7.string(),
2873
- revision: z7.string().uuid(),
2874
- fingerprint: z7.string(),
2875
- mcpFingerprint: z7.string(),
2876
- instructions: z7.array(z7.string())
3060
+ setupHostSchema = z9.object({
3061
+ configuredVersion: z9.string(),
3062
+ revision: z9.string().uuid(),
3063
+ fingerprint: z9.string(),
3064
+ mcpFingerprint: z9.string(),
3065
+ instructions: z9.array(z9.string())
2877
3066
  });
2878
- setupSchema = z7.object({ version: z7.literal(2), hosts: z7.object({
3067
+ setupSchema = z9.object({ version: z9.literal(2), hosts: z9.object({
2879
3068
  codex: setupHostSchema.optional(),
2880
3069
  claude: setupHostSchema.optional()
2881
3070
  }) });
2882
3071
  SETUP_PATH = ".mason/local/setup.json";
2883
- receiptSchema = z7.object({
2884
- version: z7.literal(1),
2885
- host: z7.enum(["codex", "claude"]),
2886
- status: z7.enum(["installing", "configured"]),
2887
- initialReportPath: z7.string(),
2888
- initialBaselinePaths: z7.array(z7.string()),
2889
- root: z7.string(),
2890
- revision: z7.string().uuid(),
2891
- configuredAt: z7.string().optional()
3072
+ receiptSchema = z9.object({
3073
+ version: z9.literal(1),
3074
+ host: z9.enum(["codex", "claude"]),
3075
+ status: z9.enum(["installing", "configured"]),
3076
+ initialReportPath: z9.string(),
3077
+ initialBaselinePaths: z9.array(z9.string()),
3078
+ root: z9.string(),
3079
+ revision: z9.string().uuid(),
3080
+ configuredAt: z9.string().optional()
2892
3081
  });
2893
3082
  }
2894
3083
  });
@@ -2905,6 +3094,7 @@ __export(launcher_exports, {
2905
3094
  });
2906
3095
  import { execFile as execFile9 } from "child_process";
2907
3096
  import { promisify as promisify9 } from "util";
3097
+ import fs14 from "fs/promises";
2908
3098
  function globalCommand(args2, platform = process.platform) {
2909
3099
  return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args2] } : { command: "mason", args: args2 };
2910
3100
  }
@@ -2923,13 +3113,17 @@ async function installedCommand() {
2923
3113
  return { available: false, version: null, message: "An up-to-date mason command is not available on this process's PATH. Install or upgrade Mason, then reopen your terminal or desktop assistant and rerun mason setup." };
2924
3114
  }
2925
3115
  }
2926
- async function prepareLaunch(host) {
2927
- const ws = await workspace(process.cwd());
2928
- const entry = (await loadSetup(ws.root))?.hosts[host];
2929
- if (!entry) throw new Error(`Mason is not configured locally for ${host}. Run mason setup --host ${host} in this checkout.`);
2930
- process.env.MASON_SETUP_ROOT = ws.root;
3116
+ async function prepareLaunch(host, options = {}) {
3117
+ const root = await fs14.realpath((await git2(process.cwd(), "rev-parse", "--show-toplevel")).trim());
3118
+ const entry = (await loadSetup(root))?.hosts[host];
3119
+ if (!entry) {
3120
+ if (options.allowInactive) return false;
3121
+ throw new Error(`Mason is not configured locally for ${host}. Run mason setup --host ${host} in this checkout.`);
3122
+ }
3123
+ process.env.MASON_SETUP_ROOT = root;
2931
3124
  process.env.MASON_SETUP_HOST = host;
2932
3125
  process.env.MASON_SETUP_REVISION = entry.revision;
3126
+ return true;
2933
3127
  }
2934
3128
  var executingVersion, mcpCommand, hookCommand;
2935
3129
  var init_launcher = __esm({
@@ -2937,7 +3131,7 @@ var init_launcher = __esm({
2937
3131
  "use strict";
2938
3132
  init_evidence();
2939
3133
  init_model();
2940
- executingVersion = () => true ? "0.16.1" : "development";
3134
+ executingVersion = () => true ? "0.16.3" : "development";
2941
3135
  mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
2942
3136
  hookCommand = (host) => `mason --setup-host ${host} auto`;
2943
3137
  }
@@ -2950,8 +3144,8 @@ __export(observations_exports, {
2950
3144
  observeActivation: () => observeActivation,
2951
3145
  readObservation: () => readObservation
2952
3146
  });
2953
- import fs13 from "fs/promises";
2954
- import { z as z8 } from "zod";
3147
+ import fs15 from "fs/promises";
3148
+ import { z as z10 } from "zod";
2955
3149
  function observationPath(directory, host, revision, version = executingVersion()) {
2956
3150
  return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
2957
3151
  }
@@ -2966,9 +3160,9 @@ async function observeActivation(dir, event, options = {}) {
2966
3160
  const host = process.env.MASON_SETUP_HOST, revision = process.env.MASON_SETUP_REVISION;
2967
3161
  if (host !== "codex" && host !== "claude" || !revision || !process.env.MASON_SETUP_ROOT) return null;
2968
3162
  try {
2969
- const capturedDirectory = options.reportPath?.match(/^(\.mason\/reports\/automation\/[a-f0-9]{24})\/checks\//)?.[1];
2970
- const ws = capturedDirectory ? { root: await fs13.realpath(dir), directory: capturedDirectory } : await workspace(dir);
2971
- if (ws.root !== await fs13.realpath(process.env.MASON_SETUP_ROOT)) return null;
3163
+ const capturedDirectory = options.directory?.match(/^\.mason\/reports\/automation\/[a-f0-9]{24}$/)?.[0] ?? options.reportPath?.match(/^(\.mason\/reports\/automation\/[a-f0-9]{24})\/checks\//)?.[1];
3164
+ const ws = capturedDirectory ? { root: await fs15.realpath(dir), directory: capturedDirectory } : await workspace(dir);
3165
+ if (ws.root !== await fs15.realpath(process.env.MASON_SETUP_ROOT)) return null;
2972
3166
  const setup = await loadSetup(ws.root);
2973
3167
  if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
2974
3168
  const directory = ws.directory + "/activation";
@@ -3016,45 +3210,59 @@ var init_observations = __esm({
3016
3210
  init_storage();
3017
3211
  init_model();
3018
3212
  init_launcher();
3019
- observationSchema = z8.object({
3020
- version: z8.literal(1),
3021
- masonVersion: z8.string(),
3022
- root: z8.string(),
3023
- revision: z8.string(),
3024
- host: z8.enum(["codex", "claude"]),
3025
- contextCalls: z8.number().int().nonnegative(),
3026
- lastContextAt: z8.string().optional(),
3027
- sessions: z8.record(z8.object({
3028
- events: z8.array(z8.enum(events)),
3029
- at: z8.string(),
3030
- verificationStatus: z8.string().optional(),
3031
- reportPath: z8.string().optional()
3213
+ observationSchema = z10.object({
3214
+ version: z10.literal(1),
3215
+ masonVersion: z10.string(),
3216
+ root: z10.string(),
3217
+ revision: z10.string(),
3218
+ host: z10.enum(["codex", "claude"]),
3219
+ contextCalls: z10.number().int().nonnegative(),
3220
+ lastContextAt: z10.string().optional(),
3221
+ sessions: z10.record(z10.object({
3222
+ events: z10.array(z10.enum(events)),
3223
+ at: z10.string(),
3224
+ verificationStatus: z10.string().optional(),
3225
+ reportPath: z10.string().optional()
3032
3226
  }))
3033
3227
  });
3034
3228
  }
3035
3229
  });
3036
3230
 
3037
3231
  // src/automation/adapters.ts
3038
- import { z as z9 } from "zod";
3232
+ import { z as z11 } from "zod";
3039
3233
  function normalizeHook(host, raw) {
3040
3234
  hostSchema.parse(host);
3041
3235
  const input = inputSchema.parse(raw);
3042
- const readOnly = host === "claude" ? /^(Read|Glob|Grep|WebSearch|WebFetch)$/ : /^(read_file|list_dir|grep_files)$/;
3043
- return { cwd: input.cwd, name: input.hook_event_name, event: {
3236
+ const readOnly = (host === "claude" ? /^(Read|Glob|Grep|WebSearch|WebFetch)$/ : /^(read_file|list_dir|grep_files|Read|Glob|Grep)$/).test(input.tool_name ?? "");
3237
+ return { cwd: input.cwd, name: input.hook_event_name, readOnly, event: {
3044
3238
  event: lifecycle[input.hook_event_name],
3045
3239
  host,
3046
3240
  sessionId: input.session_id,
3047
3241
  toolId: input.tool_use_id,
3048
- mutating: !!input.tool_name && !readOnly.test(input.tool_name),
3242
+ mutating: !!input.tool_name && !readOnly,
3049
3243
  stopHookActive: input.stop_hook_active || input.permission_mode === "plan"
3050
3244
  } };
3051
3245
  }
3052
3246
  async function runAutomationHook(host, stdin) {
3053
3247
  let name = "";
3248
+ let hook;
3054
3249
  try {
3055
3250
  if (Buffer.byteLength(stdin) > 1024 * 1024) throw new Error("Hook input exceeds 1 MiB.");
3056
- const input = normalizeHook(host, JSON.parse(stdin));
3251
+ let raw;
3252
+ try {
3253
+ raw = JSON.parse(stdin);
3254
+ } catch {
3255
+ throw new Error("Hook input is not valid JSON.");
3256
+ }
3257
+ const input = normalizeHook(host, raw);
3258
+ hook = input;
3057
3259
  name = input.name;
3260
+ if (["before_tool", "after_tool"].includes(input.event.event) && input.readOnly) {
3261
+ const observed = await observeReadOnlyTool(input.cwd, input.event);
3262
+ const { observeActivation: observeActivation3 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
3263
+ const warning2 = await observeActivation3(observed.root, input.event.event, { sessionId: input.event.sessionId, directory: observed.directory });
3264
+ return warning2 ? { systemMessage: warning2 } : null;
3265
+ }
3058
3266
  const result = await automate(input.cwd, input.event);
3059
3267
  const { observeActivation: observeActivation2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
3060
3268
  const warning = await observeActivation2(result.report.root, input.event.event, {
@@ -3069,12 +3277,20 @@ async function runAutomationHook(host, stdin) {
3069
3277
  }
3070
3278
  return { hookSpecificOutput: { hookEventName: name, additionalContext: result.message } };
3071
3279
  } catch (error) {
3072
- const message2 = failureMessage(error);
3280
+ const message2 = hook ? await hookFailureMessage(hook.cwd, host, hook.event.sessionId, error) : failureMessage(error);
3281
+ if (!message2) return null;
3073
3282
  return { systemMessage: message2, ...["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse"].includes(name) ? { hookSpecificOutput: { hookEventName: name, additionalContext: message2 } } : {} };
3074
3283
  }
3075
3284
  }
3285
+ function managedHookCommand(host, platform = process.platform) {
3286
+ const command2 = `mason --setup-host ${host} auto hook --host ${host}`;
3287
+ return platform === "win32" ? `cmd.exe /d /s /c "where mason >nul 2>nul & if errorlevel 1 (exit /b 0) else (${command2})"` : `if command -v mason >/dev/null 2>&1; then ${command2}; fi`;
3288
+ }
3289
+ function knownManagedHookCommands(host) {
3290
+ return [managedHookCommand(host, "linux"), managedHookCommand(host, "win32"), `mason --setup-host ${host} auto hook --host ${host}`];
3291
+ }
3076
3292
  function hookConfig(host, command2 = "npx --no-install --package mason-context mason-auto") {
3077
- const handler = { type: "command", command: command2 + " hook --host " + host, timeout: 30 };
3293
+ const handler = { type: "command", command: command2 === `mason --setup-host ${host} auto` ? managedHookCommand(host) : command2 + " hook --host " + host, timeout: 30 };
3078
3294
  return { hooks: Object.fromEntries(HOOK_EVENTS.map((name) => [
3079
3295
  name,
3080
3296
  [{ ...["PreToolUse", "PostToolUse"].includes(name) ? { matcher: ".*" } : {}, hooks: [{ ...handler }] }]
@@ -3087,15 +3303,15 @@ var init_adapters = __esm({
3087
3303
  init_execution();
3088
3304
  init_runtime();
3089
3305
  init_store();
3090
- inputSchema = z9.object({
3091
- cwd: z9.string().min(1),
3092
- session_id: z9.string().min(1).max(500),
3093
- hook_event_name: z9.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
3094
- tool_name: z9.string().optional(),
3095
- tool_use_id: z9.string().max(500).optional(),
3096
- tool_input: z9.unknown().optional(),
3097
- stop_hook_active: z9.boolean().optional(),
3098
- permission_mode: z9.string().optional()
3306
+ inputSchema = z11.object({
3307
+ cwd: z11.string().min(1),
3308
+ session_id: z11.string().min(1).max(500),
3309
+ hook_event_name: z11.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
3310
+ tool_name: z11.string().optional(),
3311
+ tool_use_id: z11.string().max(500).optional(),
3312
+ tool_input: z11.unknown().optional(),
3313
+ stop_hook_active: z11.boolean().optional(),
3314
+ permission_mode: z11.string().optional()
3099
3315
  });
3100
3316
  lifecycle = {
3101
3317
  SessionStart: "session_start",
@@ -3115,7 +3331,7 @@ __export(files_exports, {
3115
3331
  managedBlock: () => managedBlock,
3116
3332
  readText: () => readText
3117
3333
  });
3118
- import fs14 from "fs/promises";
3334
+ import fs16 from "fs/promises";
3119
3335
  import path18 from "path";
3120
3336
  import { randomUUID as randomUUID5 } from "crypto";
3121
3337
  async function readText(root, file) {
@@ -3145,14 +3361,14 @@ async function applyEdit(root, edit) {
3145
3361
  if (edit.after === null) {
3146
3362
  const file2 = await storePath(root, edit.path);
3147
3363
  if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
3148
- await fs14.unlink(file2);
3364
+ await fs16.unlink(file2);
3149
3365
  return true;
3150
3366
  }
3151
3367
  const file = await storePath(root, edit.path, true);
3152
3368
  const temporary = path18.join(path18.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
3153
3369
  try {
3154
- const mode = await fs14.stat(file).then((s) => s.mode & 511, () => 384);
3155
- const handle = await fs14.open(temporary, "wx", mode);
3370
+ const mode = await fs16.stat(file).then((s) => s.mode & 511, () => 384);
3371
+ const handle = await fs16.open(temporary, "wx", mode);
3156
3372
  try {
3157
3373
  await handle.writeFile(edit.after, "utf8");
3158
3374
  await handle.sync();
@@ -3160,10 +3376,10 @@ async function applyEdit(root, edit) {
3160
3376
  await handle.close();
3161
3377
  }
3162
3378
  if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
3163
- await fs14.rename(temporary, file);
3379
+ await fs16.rename(temporary, file);
3164
3380
  return true;
3165
3381
  } finally {
3166
- await fs14.rm(temporary, { force: true });
3382
+ await fs16.rm(temporary, { force: true });
3167
3383
  }
3168
3384
  }
3169
3385
  var init_files2 = __esm({
@@ -3184,7 +3400,7 @@ __export(ownership_exports, {
3184
3400
  ownershipEdit: () => ownershipEdit,
3185
3401
  ownershipSchema: () => ownershipSchema
3186
3402
  });
3187
- import { z as z10 } from "zod";
3403
+ import { z as z12 } from "zod";
3188
3404
  import { parse } from "smol-toml";
3189
3405
  function blockSpan(text2, start, end) {
3190
3406
  const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
@@ -3235,30 +3451,30 @@ var init_ownership = __esm({
3235
3451
  ["<!-- mason:agents:start -->", "<!-- mason:agents:end -->"],
3236
3452
  ["# mason:mcp:start", "# mason:mcp:end"]
3237
3453
  ];
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()
3454
+ fileSchema = z12.object({
3455
+ created: z12.boolean(),
3456
+ initialHash: z12.string(),
3457
+ blocks: z12.record(z12.string()),
3458
+ mcpHash: z12.string().optional(),
3459
+ hookCommands: z12.array(z12.string()).optional(),
3460
+ hooks: z12.record(z12.array(z12.string())).optional()
3245
3461
  });
3246
- ownershipSchema = z10.object({ version: z10.literal(1), files: z10.record(fileSchema) });
3462
+ ownershipSchema = z12.object({ version: z12.literal(1), files: z12.record(fileSchema) });
3247
3463
  }
3248
3464
  });
3249
3465
 
3250
3466
  // src/mcp/init.ts
3251
- import { z as z11 } from "zod";
3467
+ import { z as z13 } from "zod";
3252
3468
  var markerSchema, CLAUDE_MD_SECTION, ASSISTANT_SETUP, QUICKSTART_PLAYBOOK, MAP_PLAYBOOK;
3253
3469
  var init_init = __esm({
3254
3470
  "src/mcp/init.ts"() {
3255
3471
  "use strict";
3256
3472
  init_storage();
3257
3473
  init_files2();
3258
- markerSchema = z11.object({
3259
- version: z11.literal(1),
3260
- initializedAt: z11.string(),
3261
- features: z11.object({ confluence: z11.boolean().optional() }).passthrough().optional()
3474
+ markerSchema = z13.object({
3475
+ version: z13.literal(1),
3476
+ initializedAt: z13.string(),
3477
+ features: z13.object({ confluence: z13.boolean().optional() }).passthrough().optional()
3262
3478
  }).passthrough();
3263
3479
  CLAUDE_MD_SECTION = `<!-- mason:start -->
3264
3480
  ## Mason project knowledge
@@ -3365,7 +3581,7 @@ __export(config_exports, {
3365
3581
  });
3366
3582
  import { isDeepStrictEqual } from "util";
3367
3583
  import { parse as parse2, stringify } from "smol-toml";
3368
- import { z as z12 } from "zod";
3584
+ import { z as z14 } from "zod";
3369
3585
  async function instructionEdits(root, host) {
3370
3586
  const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
3371
3587
  const existing = found.find((f) => f.text !== null);
@@ -3481,7 +3697,7 @@ var init_config = __esm({
3481
3697
  init_install();
3482
3698
  init_files2();
3483
3699
  init_launcher();
3484
- object = z12.record(z12.unknown());
3700
+ object = z14.record(z14.unknown());
3485
3701
  mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
3486
3702
  TOML_START = "# mason:mcp:start";
3487
3703
  TOML_END = "# mason:mcp:end";
@@ -3501,7 +3717,7 @@ __export(install_exports, {
3501
3717
  planAutomationInstall: () => planAutomationInstall,
3502
3718
  recordSchema: () => recordSchema
3503
3719
  });
3504
- import { z as z13 } from "zod";
3720
+ import { z as z15 } from "zod";
3505
3721
  async function installationRecord(root) {
3506
3722
  return recordSchema.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
3507
3723
  }
@@ -3519,7 +3735,7 @@ async function planAutomationInstall(root, host, command2) {
3519
3735
  const previous = record.hosts[host]?.command;
3520
3736
  const ownershipText = await readText(root, OWNERSHIP_PATH);
3521
3737
  const pending = ownershipText === null ? [] : ownershipSchema.parse(JSON.parse(ownershipText)).files[file]?.hookCommands ?? [];
3522
- const owned = /* @__PURE__ */ new Set([previous, ...pending, newCommand]);
3738
+ const owned = /* @__PURE__ */ new Set([previous, ...pending, newCommand, ...knownManagedHookCommands(host)]);
3523
3739
  const hooks = existing.hooks ?? {};
3524
3740
  for (const event of HOOK_EVENTS) {
3525
3741
  hooks[event] = (hooks[event] ?? []).map((group) => ({
@@ -3582,9 +3798,9 @@ var init_install = __esm({
3582
3798
  init_store();
3583
3799
  init_files2();
3584
3800
  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() })) });
3801
+ groupSchema = z15.object({ hooks: z15.array(z15.object({ type: z15.string(), command: z15.string().optional() }).passthrough()) }).passthrough();
3802
+ automationConfigSchema = z15.object({ hooks: z15.record(z15.array(groupSchema)).optional() }).passthrough();
3803
+ recordSchema = z15.object({ version: z15.literal(1), hosts: z15.record(z15.object({ command: z15.string() })) });
3588
3804
  AUTOMATION_PATH = ".mason/local/automation.json";
3589
3805
  configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
3590
3806
  }
@@ -3599,7 +3815,7 @@ __export(teardown_exports, {
3599
3815
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
3600
3816
  import { parse as parse3, stringify as stringify2 } from "smol-toml";
3601
3817
  import fg5 from "fast-glob";
3602
- import { z as z14 } from "zod";
3818
+ import { z as z16 } from "zod";
3603
3819
  async function planTeardown(root, selected) {
3604
3820
  const edits = [], diagnostics = [];
3605
3821
  const inputs = /* @__PURE__ */ new Map();
@@ -3623,6 +3839,7 @@ async function planTeardown(root, selected) {
3623
3839
  const commands = (host) => new Set([
3624
3840
  automation?.hosts[host]?.command,
3625
3841
  ...ownership.files[configPath(host)]?.hookCommands ?? [],
3842
+ ...knownManagedHookCommands(host),
3626
3843
  hookConfig(host, hookCommand(host)).hooks.SessionStart[0].hooks[0].command,
3627
3844
  hookConfig(host).hooks.SessionStart[0].hooks[0].command
3628
3845
  ].filter((command2) => !!command2));
@@ -3745,7 +3962,7 @@ async function planTeardown(root, selected) {
3745
3962
  if (!remainingHost) await attempt(".mason/local/project.json", async () => {
3746
3963
  const before = await read(".mason/local/project.json");
3747
3964
  if (before === null) return;
3748
- z14.object({ version: z14.literal(1), initializedAt: z14.string() }).strict().parse(JSON.parse(before));
3965
+ z16.object({ version: z16.literal(1), initializedAt: z16.string() }).strict().parse(JSON.parse(before));
3749
3966
  cleanup.push({ path: ".mason/local/project.json", before, after: null });
3750
3967
  });
3751
3968
  if (!diagnostics.length) {
@@ -3817,7 +4034,7 @@ var init_teardown = __esm({
3817
4034
  init_model();
3818
4035
  init_ownership();
3819
4036
  hosts = ["codex", "claude"];
3820
- object2 = z14.record(z14.unknown());
4037
+ object2 = z16.record(z16.unknown());
3821
4038
  json = (value) => JSON.stringify(value, null, 2) + "\n";
3822
4039
  }
3823
4040
  });
@@ -3944,7 +4161,7 @@ var init_types2 = __esm({
3944
4161
  });
3945
4162
 
3946
4163
  // src/review/evidence/vitest.ts
3947
- import { z as z15 } from "zod";
4164
+ import { z as z17 } from "zod";
3948
4165
  function parseVitest(raw, sourceRoot) {
3949
4166
  const report = schema.parse(raw);
3950
4167
  const findings = [], diagnostics = [];
@@ -3993,24 +4210,24 @@ var init_vitest = __esm({
3993
4210
  "use strict";
3994
4211
  init_paths2();
3995
4212
  init_types2();
3996
- count2 = z15.number().int().nonnegative();
3997
- schema = z15.object({
3998
- success: z15.boolean(),
4213
+ count2 = z17.number().int().nonnegative();
4214
+ schema = z17.object({
4215
+ success: z17.boolean(),
3999
4216
  numTotalTests: count2,
4000
4217
  numPassedTests: count2,
4001
4218
  numFailedTests: count2,
4002
4219
  numPendingTests: count2,
4003
4220
  numTodoTests: count2,
4004
4221
  numFailedTestSuites: count2,
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()
4222
+ testResults: z17.array(z17.object({
4223
+ name: z17.string().min(1),
4224
+ status: z17.enum(["passed", "failed"]),
4225
+ message: z17.string().optional(),
4226
+ assertionResults: z17.array(z17.object({
4227
+ fullName: z17.string(),
4228
+ status: z17.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
4229
+ failureMessages: z17.array(z17.string()).nullable().optional(),
4230
+ location: z17.object({ line: count2, column: count2 }).nullable().optional()
4014
4231
  }))
4015
4232
  }))
4016
4233
  });
@@ -4018,7 +4235,7 @@ var init_vitest = __esm({
4018
4235
  });
4019
4236
 
4020
4237
  // src/review/evidence/sarif.ts
4021
- import { z as z16 } from "zod";
4238
+ import { z as z18 } from "zod";
4022
4239
  function parseSarif(raw, sourceRoot) {
4023
4240
  const report = schema2.parse(raw), findings = [], diagnostics = [];
4024
4241
  let active = 0, suppressed = 0, absent = 0, unresolved = 0;
@@ -4109,29 +4326,29 @@ var init_sarif = __esm({
4109
4326
  "use strict";
4110
4327
  init_paths2();
4111
4328
  init_types2();
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(),
4329
+ index = z18.number().int().nonnegative();
4330
+ artifact = z18.object({ uri: z18.string().optional(), uriBaseId: z18.string().optional(), index: index.optional() });
4331
+ message = z18.object({ text: z18.string().optional(), markdown: z18.string().optional(), id: z18.string().optional(), arguments: z18.array(z18.string()).optional() });
4332
+ level = z18.enum(["error", "warning", "note", "none"]);
4333
+ location = z18.object({ physicalLocation: z18.object({ artifactLocation: artifact.optional(), region: z18.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
4334
+ schema2 = z18.object({
4335
+ version: z18.literal("2.1.0"),
4336
+ runs: z18.array(z18.object({
4337
+ tool: z18.object({ driver: z18.object({ name: z18.string().min(1), rules: z18.array(z18.object({ id: z18.string(), defaultConfiguration: z18.object({ level: level.optional() }).optional(), messageStrings: z18.record(message).optional() })).optional(), globalMessageStrings: z18.record(message).optional() }) }),
4338
+ invocations: z18.array(z18.object({ executionSuccessful: z18.boolean(), commandLine: z18.string().optional(), toolExecutionNotifications: z18.array(z18.object({ level: level.optional(), message })).optional() })).optional(),
4339
+ versionControlProvenance: z18.array(z18.object({ revisionId: z18.string().optional() })).optional(),
4340
+ originalUriBaseIds: z18.record(artifact).optional(),
4341
+ artifacts: z18.array(z18.object({ location: artifact.optional() })).optional(),
4342
+ results: z18.array(z18.object({
4343
+ ruleId: z18.string().optional(),
4127
4344
  ruleIndex: index.optional(),
4128
4345
  message,
4129
4346
  level: level.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()
4347
+ kind: z18.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
4348
+ baselineState: z18.enum(["new", "unchanged", "updated", "absent"]).optional(),
4349
+ suppressions: z18.array(z18.object({ kind: z18.enum(["inSource", "external"]), status: z18.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
4350
+ locations: z18.array(location).optional(),
4351
+ relatedLocations: z18.array(location).optional()
4135
4352
  })).optional()
4136
4353
  }))
4137
4354
  });
@@ -4140,7 +4357,7 @@ var init_sarif = __esm({
4140
4357
 
4141
4358
  // src/review/evidence.ts
4142
4359
  import path19 from "path";
4143
- import { z as z17 } from "zod";
4360
+ import { z as z19 } from "zod";
4144
4361
  function relativeArtifact(root, file) {
4145
4362
  const relative = path19.isAbsolute(file) ? path19.relative(root, file) : file;
4146
4363
  const normalized = normalizeRepoPath(relative);
@@ -4181,7 +4398,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
4181
4398
  let manifest, raw;
4182
4399
  try {
4183
4400
  manifest = relativeArtifact(root, supplied);
4184
- raw = z17.object({ version: z17.literal(1), checks: z17.array(z17.unknown()) }).parse(await readStoreJson(root, manifest));
4401
+ raw = z19.object({ version: z19.literal(1), checks: z19.array(z19.unknown()) }).parse(await readStoreJson(root, manifest));
4185
4402
  } catch (error) {
4186
4403
  output.diagnostics.push(`${supplied}: ${String(error)}`);
4187
4404
  continue;
@@ -4334,25 +4551,25 @@ var init_evidence2 = __esm({
4334
4551
  init_vitest();
4335
4552
  init_sarif();
4336
4553
  init_types2();
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()
4554
+ checkSchema2 = z19.object({
4555
+ id: z19.string().min(1).max(100),
4556
+ kind: z19.enum(["tests", "static-analysis", "security", "complexity", "duplication"]),
4557
+ tool: z19.string().min(1).max(200),
4558
+ command: z19.string().min(1).max(2e3),
4559
+ commit: z19.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
4560
+ workingTreeClean: z19.boolean().optional(),
4561
+ source: z19.string().max(2e3).optional(),
4562
+ sourceRoot: z19.string().min(1).max(4e3).optional(),
4563
+ status: z19.enum(["completed", "skipped", "unavailable"]).default("completed"),
4564
+ reason: z19.string().min(1).max(2e3).optional(),
4565
+ exitCode: z19.number().int().nullable().optional(),
4566
+ report: z19.object({ format: z19.enum(["vitest-json", "sarif"]), path: z19.string().min(1).max(4e3) }).optional()
4350
4567
  });
4351
4568
  }
4352
4569
  });
4353
4570
 
4354
4571
  // src/review/review.ts
4355
- import fs15 from "fs/promises";
4572
+ import fs17 from "fs/promises";
4356
4573
  import path20 from "path";
4357
4574
  import { execFile as execFile12 } from "child_process";
4358
4575
  import { promisify as promisify12 } from "util";
@@ -4432,7 +4649,7 @@ async function computeReview(rootDir, base, options = {}) {
4432
4649
  analyzed,
4433
4650
  async (relPath) => {
4434
4651
  try {
4435
- await fs15.access(path20.join(resolvedRoot, relPath));
4652
+ await fs17.access(path20.join(resolvedRoot, relPath));
4436
4653
  return true;
4437
4654
  } catch {
4438
4655
  return false;
@@ -4586,7 +4803,7 @@ async function setupStatus(dir) {
4586
4803
  version: 1,
4587
4804
  status: pending.length ? "incomplete" : "not-configured",
4588
4805
  hosts: {},
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."
4806
+ next: pending.length ? "Setup did not finish. Rerun mason-auto setup --host " + pending[0] + " to resume using the retained original evidence." : "Project hooks are inactive until local setup. Run mason setup --host codex or --host claude."
4590
4807
  };
4591
4808
  }
4592
4809
  const hosts2 = {};
@@ -4795,12 +5012,12 @@ var init_setup = __esm({
4795
5012
  });
4796
5013
 
4797
5014
  // src/distribution/bundle.ts
4798
- import fs16 from "fs/promises";
5015
+ import fs18 from "fs/promises";
4799
5016
  import path21 from "path";
4800
5017
  import { createHash as createHash4 } from "crypto";
4801
- import { z as z18 } from "zod";
5018
+ import { z as z20 } from "zod";
4802
5019
  async function readBundle(root) {
4803
- const bytes = await fs16.readFile(await storePath(root, "bundle.json"));
5020
+ const bytes = await fs18.readFile(await storePath(root, "bundle.json"));
4804
5021
  const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
4805
5022
  const windows = manifest.target.startsWith("win32-");
4806
5023
  for (const file of Object.keys(manifest.files)) {
@@ -4819,7 +5036,7 @@ async function verifyBundle(root, expectedHash) {
4819
5036
  if (bundle.manifest.target !== `${process.platform}-${process.arch}`) throw new Error("Mason bundle is for another platform. Install this platform's bundle and rerun setup.");
4820
5037
  const files = Object.keys(bundle.manifest.files);
4821
5038
  for (let i = 0; i < files.length; i += 8) await Promise.all(files.slice(i, i + 8).map(async (file) => {
4822
- if (sha256(await fs16.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
5039
+ if (sha256(await fs18.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
4823
5040
  }));
4824
5041
  return bundle;
4825
5042
  }
@@ -4829,26 +5046,26 @@ var init_bundle = __esm({
4829
5046
  "use strict";
4830
5047
  init_storage();
4831
5048
  targets = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
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)
5049
+ digest2 = z20.string().regex(/^[a-f0-9]{64}$/);
5050
+ bundleSchema = z20.object({
5051
+ format: z20.literal(1),
5052
+ version: z20.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
5053
+ target: z20.enum(targets),
5054
+ nodeVersion: z20.string(),
5055
+ files: z20.record(digest2)
4839
5056
  }).strict();
4840
5057
  sha256 = (bytes) => createHash4("sha256").update(bytes).digest("hex");
4841
5058
  }
4842
5059
  });
4843
5060
 
4844
5061
  // src/distribution/path.ts
4845
- import fs17 from "fs/promises";
5062
+ import fs19 from "fs/promises";
4846
5063
  import os3 from "os";
4847
5064
  import path22 from "path";
4848
5065
  import { execFile as execFile13 } from "child_process";
4849
5066
  import { promisify as promisify13 } from "util";
4850
5067
  import { createHash as createHash5, randomUUID as randomUUID7 } from "crypto";
4851
- import { z as z19 } from "zod";
5068
+ import { z as z21 } from "zod";
4852
5069
  async function configurePath(bin, previous, save, options = {}) {
4853
5070
  const env = options.env ?? process.env, platform = options.platform ?? process.platform;
4854
5071
  const changes = [...previous];
@@ -4881,7 +5098,7 @@ ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --h
4881
5098
  else {
4882
5099
  const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path22.join(userHome, name));
4883
5100
  let login = candidates[2];
4884
- for (const file of candidates) if (await fs17.lstat(file).then(() => true, (error) => {
5101
+ for (const file of candidates) if (await fs19.lstat(file).then(() => true, (error) => {
4885
5102
  if (error.code === "ENOENT") return false;
4886
5103
  throw error;
4887
5104
  })) {
@@ -4892,12 +5109,12 @@ ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --h
4892
5109
  }
4893
5110
  const marker = `# >>> mason PATH ${createHash5("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
4894
5111
  for (const profile of profiles) {
4895
- const stat = await fs17.lstat(profile).catch((error) => {
5112
+ const stat = await fs19.lstat(profile).catch((error) => {
4896
5113
  if (error.code === "ENOENT") return null;
4897
5114
  throw error;
4898
5115
  });
4899
- const file = stat?.isSymbolicLink() ? await fs17.realpath(profile) : path22.resolve(profile);
4900
- const original = await fs17.readFile(file).catch((error) => {
5116
+ const file = stat?.isSymbolicLink() ? await fs19.realpath(profile) : path22.resolve(profile);
5117
+ const original = await fs19.readFile(file).catch((error) => {
4901
5118
  if (error.code === "ENOENT" && !stat) return null;
4902
5119
  throw error;
4903
5120
  });
@@ -4912,8 +5129,8 @@ ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --h
4912
5129
  changes.push({ kind: "profile", file, block, created: original === null });
4913
5130
  await save(changes);
4914
5131
  }
4915
- await fs17.mkdir(path22.dirname(file), { recursive: true });
4916
- const handle = await fs17.open(file, original === null ? "ax+" : "a+", 384);
5132
+ await fs19.mkdir(path22.dirname(file), { recursive: true });
5133
+ const handle = await fs19.open(file, original === null ? "ax+" : "a+", 384);
4917
5134
  try {
4918
5135
  if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0))) throw new Error(`Shell profile changed during installation: ${profile}`);
4919
5136
  await handle.writeFile(block);
@@ -4937,13 +5154,13 @@ async function removePath(changes) {
4937
5154
  await windowsPath("remove", change.entry, current, change.created);
4938
5155
  continue;
4939
5156
  }
4940
- const stat = await fs17.lstat(change.file).catch((error) => {
5157
+ const stat = await fs19.lstat(change.file).catch((error) => {
4941
5158
  if (error.code === "ENOENT") return null;
4942
5159
  throw error;
4943
5160
  });
4944
5161
  if (!stat) continue;
4945
5162
  if (!stat.isFile()) throw new Error("profile type changed");
4946
- const bytes = await fs17.readFile(change.file), text2 = bytes.toString("utf8");
5163
+ const bytes = await fs19.readFile(change.file), text2 = bytes.toString("utf8");
4947
5164
  if (!Buffer.from(text2).equals(bytes)) throw new Error("profile encoding changed");
4948
5165
  const index2 = text2.indexOf(change.block);
4949
5166
  if (index2 < 0 || text2.indexOf(change.block, index2 + change.block.length) >= 0) throw new Error("PATH block was edited or removed");
@@ -4953,7 +5170,7 @@ async function removePath(changes) {
4953
5170
  const temporary = change.file + ".mason-" + randomUUID7();
4954
5171
  try {
4955
5172
  if (stat.nlink > 1) throw new Error("profile has multiple hard links");
4956
- const handle = await fs17.open(temporary, "wx", stat.mode & 511);
5173
+ const handle = await fs19.open(temporary, "wx", stat.mode & 511);
4957
5174
  try {
4958
5175
  await handle.writeFile(next);
4959
5176
  await handle.chmod(stat.mode & 511);
@@ -4962,11 +5179,11 @@ async function removePath(changes) {
4962
5179
  } finally {
4963
5180
  await handle.close();
4964
5181
  }
4965
- const current = await fs17.lstat(change.file);
4966
- if (!current.isFile() || current.ino !== stat.ino || !(await fs17.readFile(change.file)).equals(bytes)) throw new Error("profile changed during uninstall");
4967
- await fs17.rename(temporary, change.file);
5182
+ const current = await fs19.lstat(change.file);
5183
+ if (!current.isFile() || current.ino !== stat.ino || !(await fs19.readFile(change.file)).equals(bytes)) throw new Error("profile changed during uninstall");
5184
+ await fs19.rename(temporary, change.file);
4968
5185
  } finally {
4969
- await fs17.rm(temporary, { force: true });
5186
+ await fs19.rm(temporary, { force: true });
4970
5187
  }
4971
5188
  } catch (error) {
4972
5189
  warnings.push(`Retained PATH setting ${change.kind === "profile" ? change.file : change.entry}: ${error instanceof Error ? error.message : String(error)}`);
@@ -5008,9 +5225,9 @@ var init_path = __esm({
5008
5225
  "src/distribution/path.ts"() {
5009
5226
  "use strict";
5010
5227
  execute2 = promisify13(execFile13);
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() })
5228
+ pathChangeSchema = z21.discriminatedUnion("kind", [
5229
+ z21.object({ kind: z21.literal("profile"), file: z21.string(), block: z21.string(), created: z21.boolean() }),
5230
+ z21.object({ kind: z21.literal("windows"), entry: z21.string(), created: z21.boolean() })
5014
5231
  ]);
5015
5232
  quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
5016
5233
  normalizeWindowsEntry = (value) => path22.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
@@ -5031,41 +5248,41 @@ __export(install_exports2, {
5031
5248
  uninstallStandalone: () => uninstallStandalone,
5032
5249
  upgradeStandalone: () => upgradeStandalone
5033
5250
  });
5034
- import fs18 from "fs/promises";
5251
+ import fs20 from "fs/promises";
5035
5252
  import os4 from "os";
5036
5253
  import path23 from "path";
5037
5254
  import { fileURLToPath } from "url";
5038
5255
  import { spawn } from "child_process";
5039
5256
  import { randomUUID as randomUUID8 } from "crypto";
5040
- import { z as z20 } from "zod";
5257
+ import { z as z22 } from "zod";
5041
5258
  async function replace(file, content2, mode = 384) {
5042
5259
  const temp = file + ".tmp-" + randomUUID8();
5043
5260
  try {
5044
- await fs18.writeFile(temp, content2, { mode, flag: "wx" });
5045
- await fs18.rename(temp, file);
5261
+ await fs20.writeFile(temp, content2, { mode, flag: "wx" });
5262
+ await fs20.rename(temp, file);
5046
5263
  } finally {
5047
- await fs18.rm(temp, { force: true });
5264
+ await fs20.rm(temp, { force: true });
5048
5265
  }
5049
5266
  }
5050
5267
  async function installStandalone(source) {
5051
5268
  const bundle = await verifyBundle(source);
5052
5269
  const selectedHome = path23.resolve(defaultHome());
5053
- await fs18.mkdir(selectedHome, { recursive: true });
5054
- const home = await fs18.realpath(selectedHome);
5270
+ await fs20.mkdir(selectedHome, { recursive: true });
5271
+ const home = await fs20.realpath(selectedHome);
5055
5272
  const selectedBin = path23.resolve(process.env.MASON_BIN_DIR ?? (process.platform === "win32" ? path23.join(home, "bin") : path23.join(os4.homedir(), ".local/bin")));
5056
5273
  return withLock(home, ".install-lock", async () => {
5057
5274
  const raw = await readStoreJson(home, "install.json");
5058
5275
  const previous = raw === null ? null : recordSchema2.parse(raw);
5059
- if (!previous) for (const file of await fs18.readdir(home)) {
5276
+ if (!previous) for (const file of await fs20.readdir(home)) {
5060
5277
  if (file === ".install-lock") continue;
5061
5278
  const directory = path23.join(home, file);
5062
- const stat = await fs18.lstat(directory);
5063
- if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs18.readdir(directory)).length) {
5279
+ const stat = await fs20.lstat(directory);
5280
+ if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs20.readdir(directory)).length) {
5064
5281
  throw new Error("Mason installation directory is not empty and has no ownership receipt.");
5065
5282
  }
5066
5283
  }
5067
- await fs18.mkdir(selectedBin, { recursive: true });
5068
- const bin = await fs18.realpath(selectedBin);
5284
+ await fs20.mkdir(selectedBin, { recursive: true });
5285
+ const bin = await fs20.realpath(selectedBin);
5069
5286
  if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
5070
5287
  const id = bundle.manifestHash.slice(0, 24);
5071
5288
  const destination = await storePath(home, "versions/" + id);
@@ -5092,7 +5309,7 @@ exec ${shQuote(path23.join(destination, "node"))} ${shQuote(path23.join(destinat
5092
5309
  ` };
5093
5310
  for (const [name, content2] of Object.entries(launchers)) {
5094
5311
  const file = await storePath(bin, name);
5095
- const old = await fs18.readFile(file, "utf8").catch((error) => {
5312
+ const old = await fs20.readFile(file, "utf8").catch((error) => {
5096
5313
  if (error.code === "ENOENT") return null;
5097
5314
  throw error;
5098
5315
  });
@@ -5101,7 +5318,7 @@ exec ${shQuote(path23.join(destination, "node"))} ${shQuote(path23.join(destinat
5101
5318
  const retired = Object.entries({ ...previous?.previousLaunchers, ...previous?.launchers }).filter(([name]) => !(name in launchers));
5102
5319
  const checkRetired = async (name, expected) => {
5103
5320
  if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
5104
- const actual = await fs18.readFile(await storePath(bin, name), "utf8").catch((error) => {
5321
+ const actual = await fs20.readFile(await storePath(bin, name), "utf8").catch((error) => {
5105
5322
  if (error.code === "ENOENT") return null;
5106
5323
  throw error;
5107
5324
  });
@@ -5111,28 +5328,28 @@ exec ${shQuote(path23.join(destination, "node"))} ${shQuote(path23.join(destinat
5111
5328
  const receipt = { format: 1, home, bin, current: id, version: bundle.manifest.version, launchers, pathChanges: previous?.pathChanges ?? [] };
5112
5329
  await writeStoreJson(home, "install.json", { ...receipt, previousLaunchers: previous?.previousLaunchers ?? previous?.launchers });
5113
5330
  const stage = await storePath(home, "versions/.install-" + randomUUID8(), true);
5114
- await fs18.mkdir(stage);
5331
+ await fs20.mkdir(stage);
5115
5332
  try {
5116
5333
  for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
5117
5334
  const from = await storePath(source, file), to = await storePath(stage, file, true);
5118
- await fs18.copyFile(from, to);
5119
- await fs18.chmod(to, (await fs18.stat(from)).mode & 511);
5335
+ await fs20.copyFile(from, to);
5336
+ await fs20.chmod(to, (await fs20.stat(from)).mode & 511);
5120
5337
  }
5121
5338
  await verifyBundle(stage, bundle.manifestHash);
5122
- const exists2 = await fs18.lstat(destination).then(() => true, (error) => {
5339
+ const exists2 = await fs20.lstat(destination).then(() => true, (error) => {
5123
5340
  if (error.code === "ENOENT") return false;
5124
5341
  throw error;
5125
5342
  });
5126
5343
  if (exists2) await verifyBundle(destination, bundle.manifestHash);
5127
- else await fs18.rename(stage, destination);
5344
+ else await fs20.rename(stage, destination);
5128
5345
  for (const [name, content2] of Object.entries(launchers)) await replace(await storePath(bin, name, true), content2, 493);
5129
5346
  for (const [name, expected] of retired) {
5130
5347
  await checkRetired(name, expected);
5131
- await fs18.rm(await storePath(bin, name), { force: true });
5348
+ await fs20.rm(await storePath(bin, name), { force: true });
5132
5349
  }
5133
5350
  await writeStoreJson(home, "install.json", receipt);
5134
5351
  } finally {
5135
- await fs18.rm(stage, { recursive: true, force: true });
5352
+ await fs20.rm(stage, { recursive: true, force: true });
5136
5353
  }
5137
5354
  const pathResult = await configurePath(bin, receipt.pathChanges, async (changes) => {
5138
5355
  receipt.pathChanges = changes;
@@ -5147,7 +5364,7 @@ async function currentInstallation() {
5147
5364
  const raw = await readStoreJson(home, "install.json");
5148
5365
  if (!raw) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
5149
5366
  const record = recordSchema2.parse(raw);
5150
- if (record.home !== await fs18.realpath(home) || path23.dirname(bundle) !== path23.join(home, "versions")) throw new Error("Invalid standalone installation receipt.");
5367
+ if (record.home !== await fs20.realpath(home) || path23.dirname(bundle) !== path23.join(home, "versions")) throw new Error("Invalid standalone installation receipt.");
5151
5368
  return { home, bundle, record };
5152
5369
  }
5153
5370
  async function upgradeStandalone(version) {
@@ -5176,7 +5393,7 @@ async function uninstallStandalone() {
5176
5393
  for (const [name, expected] of Object.entries(record.launchers)) {
5177
5394
  if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
5178
5395
  const file = await storePath(record.bin, name);
5179
- const actual = await fs18.readFile(file, "utf8").catch((error) => {
5396
+ const actual = await fs20.readFile(file, "utf8").catch((error) => {
5180
5397
  if (error.code === "ENOENT") return null;
5181
5398
  throw error;
5182
5399
  });
@@ -5184,9 +5401,9 @@ async function uninstallStandalone() {
5184
5401
  }
5185
5402
  for (const warning of await removePath(record.pathChanges ?? [])) console.error(warning);
5186
5403
  if (process.platform === "win32") await replace(await storePath(home, ".uninstall-request.txt", true), token);
5187
- for (const name of Object.keys(record.launchers)) await fs18.rm(await storePath(record.bin, name), { force: true });
5404
+ for (const name of Object.keys(record.launchers)) await fs20.rm(await storePath(record.bin, name), { force: true });
5188
5405
  });
5189
- if (process.platform !== "win32") await fs18.rm(home, { recursive: true, force: true });
5406
+ if (process.platform !== "win32") await fs20.rm(home, { recursive: true, force: true });
5190
5407
  return home;
5191
5408
  }
5192
5409
  var recordSchema2, shQuote, psQuote, WINDOWS_BATCH_LAUNCHER, launcherNames, defaultHome;
@@ -5197,15 +5414,15 @@ var init_install2 = __esm({
5197
5414
  init_storage();
5198
5415
  init_store();
5199
5416
  init_path();
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()
5417
+ recordSchema2 = z22.object({
5418
+ format: z22.literal(1),
5419
+ home: z22.string(),
5420
+ bin: z22.string(),
5421
+ current: z22.string().regex(/^[a-f0-9]{24}$/),
5422
+ version: z22.string(),
5423
+ launchers: z22.record(z22.string()),
5424
+ previousLaunchers: z22.record(z22.string()).optional(),
5425
+ pathChanges: z22.array(pathChangeSchema).optional()
5209
5426
  });
5210
5427
  shQuote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
5211
5428
  psQuote = (s) => "'" + s.replace(/'/g, "''") + "'";
@@ -5341,7 +5558,7 @@ var inputArgs = process.argv.slice(2);
5341
5558
  var managedLaunch = inputArgs[0] === "--setup-host";
5342
5559
  var setupHost = managedLaunch ? inputArgs.splice(0, 2)[1] : void 0;
5343
5560
  var [command, ...args] = inputArgs;
5344
- var usage = `Mason ${"0.16.1"}
5561
+ var usage = `Mason ${"0.16.3"}
5345
5562
 
5346
5563
  Usage: mason <command> [options]
5347
5564
 
@@ -5362,11 +5579,12 @@ try {
5362
5579
  if (managedLaunch) {
5363
5580
  if (!setupHost || !["codex", "claude"].includes(setupHost) || !["mcp", "auto"].includes(command)) throw new Error("Invalid setup invocation.");
5364
5581
  const { prepareLaunch: prepareLaunch2 } = await Promise.resolve().then(() => (init_launcher(), launcher_exports));
5365
- await prepareLaunch2(setupHost);
5582
+ const active = await prepareLaunch2(setupHost, { allowInactive: command === "auto" && isHookCommand(args) });
5583
+ if (!active) process.exit(0);
5366
5584
  }
5367
- if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.16.1" }));
5585
+ if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.16.3" }));
5368
5586
  else if (!command || ["--help", "-h", "help"].includes(command)) console.log(usage);
5369
- else if (["--version", "-v"].includes(command)) console.log("0.16.1");
5587
+ else if (["--version", "-v"].includes(command)) console.log("0.16.3");
5370
5588
  else if (command === "internal-install") {
5371
5589
  if (args.length) throw new Error("internal-install takes no arguments.");
5372
5590
  const { installStandalone: installStandalone2 } = await Promise.resolve().then(() => (init_install2(), install_exports2));