mason-context 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,39 +1,13 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/automation/cli.ts
4
- import { parseArgs } from "util";
5
-
6
- // src/automation/runtime.ts
7
- import { randomUUID as randomUUID3 } from "crypto";
8
-
9
- // src/audit/repair.ts
10
- import fs10 from "fs/promises";
11
- import path16 from "path";
12
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
13
- import { z as z3 } from "zod";
14
-
15
- // src/audit/audit.ts
16
- import fs9 from "fs/promises";
17
- import path15 from "path";
18
-
19
- // src/drift/drift.ts
20
- import fs3 from "fs/promises";
21
- import path6 from "path";
22
- import { execFile as execFile3 } from "child_process";
23
- import { promisify as promisify3 } from "util";
24
-
25
- // src/snapshot/snapshot.ts
26
- import path5 from "path";
27
- import { execFile as execFile2 } from "child_process";
28
- import { promisify as promisify2 } from "util";
29
-
30
- // src/utils/files.ts
31
- import fs from "fs/promises";
32
- import { constants } from "fs";
33
- import path2 from "path";
34
- import { execFile } from "child_process";
35
- import { promisify } from "util";
36
- import fg from "fast-glob";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
37
11
 
38
12
  // src/utils/paths.ts
39
13
  import path from "path";
@@ -56,35 +30,24 @@ function anchorMatches(anchor, file) {
56
30
  function matchingPaths(anchors, files) {
57
31
  return [...new Set(files)].filter((file) => anchors.some((anchor) => anchorMatches(anchor, file)));
58
32
  }
33
+ var init_paths = __esm({
34
+ "src/utils/paths.ts"() {
35
+ "use strict";
36
+ }
37
+ });
59
38
 
60
39
  // src/utils/files.ts
61
- var exec = promisify(execFile);
62
- var SOURCE_EXTENSIONS = ["ts", "tsx", "js", "jsx", "mts", "cts", "mjs", "cjs", "vue", "svelte", "kt", "kts", "java", "py", "go", "rs", "swift", "rb", "cs", "cpp", "c", "h", "hpp", "dart", "php"];
63
- var SOURCE_GLOB = `**/*.{${SOURCE_EXTENSIONS.join(",")}}`;
64
- var SOURCE_IGNORE = [
65
- "**/node_modules/**",
66
- "**/dist/**",
67
- "**/build/**",
68
- "**/.gradle/**",
69
- "**/target/**",
70
- "**/.git/**",
71
- "**/.mason/**",
72
- "**/vendor/**",
73
- "**/__pycache__/**",
74
- "**/venv/**",
75
- "**/.venv/**",
76
- "**/*.min.*",
77
- "**/*.map",
78
- "**/*.lock",
79
- "**/generated/**",
80
- "**/*.generated.*",
81
- "**/R.java",
82
- "**/BuildConfig.java",
83
- "**/package-lock.json",
84
- "**/yarn.lock",
85
- "**/pnpm-lock.yaml"
86
- ];
87
- var MAX_SOURCE_BYTES = 1024 * 1024;
40
+ import fs from "fs/promises";
41
+ import { constants } from "fs";
42
+ import path2 from "path";
43
+ import { execFile } from "child_process";
44
+ import { promisify } from "util";
45
+ import fg from "fast-glob";
46
+ function isSensitiveFile(file) {
47
+ return file.split(/[\\/]/).some(
48
+ (part) => /^(?:\.env(?:\..*)?|id_rsa.*|id_ed25519.*)$|\.(?:pem|key|p12|pfx|jks|keystore)$|credentials\.|secret|^local\.properties$/i.test(part)
49
+ );
50
+ }
88
51
  async function readBoundedFile(file, maxBytes) {
89
52
  const handle = await fs.open(file, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
90
53
  try {
@@ -102,6 +65,118 @@ async function readBoundedFile(file, maxBytes) {
102
65
  await handle.close();
103
66
  }
104
67
  }
68
+ async function loadProjectConfig(root) {
69
+ try {
70
+ const canonicalRoot = await fs.realpath(root);
71
+ const configPath2 = await fs.realpath(path2.join(root, ".mason/config.json"));
72
+ if (!isWithinRoot(canonicalRoot, configPath2)) throw new Error("Project configuration resolves outside the repository");
73
+ const raw = await readBoundedFile(configPath2, 64 * 1024);
74
+ if (raw === null) throw new Error("Project configuration is not a regular file or exceeds 64 KiB");
75
+ const value = JSON.parse(raw);
76
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected a configuration object");
77
+ const config = {};
78
+ for (const key of ["patterns", "alwaysInclude", "ignore"]) {
79
+ if (value[key] === void 0) continue;
80
+ if (!Array.isArray(value[key]) || !value[key].every((s) => typeof s === "string")) {
81
+ throw new Error(`Configuration ${key} must be an array of strings`);
82
+ }
83
+ config[key] = value[key];
84
+ }
85
+ return config;
86
+ } catch (error) {
87
+ if (error.code === "ENOENT") return {};
88
+ throw new Error(`Cannot apply project file policy: ${error instanceof Error ? error.message : String(error)}`);
89
+ }
90
+ }
91
+ async function createFileAccess(rootDir) {
92
+ const root = path2.resolve(rootDir);
93
+ const canonicalRoot = await fs.realpath(root).catch(() => root);
94
+ const config = await loadProjectConfig(root);
95
+ const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
96
+ let gitFiles = null;
97
+ try {
98
+ const { stdout } = await exec("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], { cwd: root, maxBuffer: 50 * 1024 * 1024 });
99
+ gitFiles = new Set(stdout.split("\0").filter(Boolean));
100
+ } catch {
101
+ let inGit = false;
102
+ try {
103
+ await exec("git", ["rev-parse", "--git-dir"], { cwd: root });
104
+ inGit = true;
105
+ } catch {
106
+ }
107
+ if (inGit) throw new Error("Cannot enumerate Git files safely");
108
+ }
109
+ async function resolve(file) {
110
+ const relative = normalizeRepoPath(file);
111
+ if (!relative || isSensitiveFile(relative) || gitFiles && !gitFiles.has(relative)) return null;
112
+ const candidate = path2.join(root, relative);
113
+ try {
114
+ const real = await fs.realpath(candidate);
115
+ if (!isWithinRoot(canonicalRoot, real) || isSensitiveFile(path2.relative(canonicalRoot, real))) return null;
116
+ const stat = await fs.stat(real);
117
+ if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES) return null;
118
+ if (gitFiles && !gitFiles.has(path2.relative(canonicalRoot, real).split(path2.sep).join("/"))) return null;
119
+ return real;
120
+ } catch {
121
+ return null;
122
+ }
123
+ }
124
+ async function list(patterns = SOURCE_GLOB, options = {}) {
125
+ const found = await fg(patterns, { cwd: root, ignore, followSymbolicLinks: false, ...options });
126
+ const safe = await Promise.all(found.map(async (f) => await resolve(f) ? f : null));
127
+ return safe.filter((f) => f !== null).sort();
128
+ }
129
+ async function read(file) {
130
+ const relative = normalizeRepoPath(file);
131
+ if (!relative) return null;
132
+ const real = await resolve(relative);
133
+ if (!real) return null;
134
+ for (const rel of /* @__PURE__ */ new Set([relative, path2.relative(canonicalRoot, real).split(path2.sep).join("/")])) {
135
+ if (!(await fg(fg.escapePath(rel), { cwd: root, ignore, dot: true })).length) return null;
136
+ }
137
+ try {
138
+ const content2 = await readBoundedFile(real, MAX_SOURCE_BYTES);
139
+ return content2 === null ? null : { path: relative, content: content2, totalLines: content2.split("\n").length };
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+ return { root, config, list, read };
145
+ }
146
+ var exec, SOURCE_EXTENSIONS, SOURCE_GLOB, SOURCE_IGNORE, MAX_SOURCE_BYTES;
147
+ var init_files = __esm({
148
+ "src/utils/files.ts"() {
149
+ "use strict";
150
+ init_paths();
151
+ exec = promisify(execFile);
152
+ SOURCE_EXTENSIONS = ["ts", "tsx", "js", "jsx", "mts", "cts", "mjs", "cjs", "vue", "svelte", "kt", "kts", "java", "py", "go", "rs", "swift", "rb", "cs", "cpp", "c", "h", "hpp", "dart", "php"];
153
+ SOURCE_GLOB = `**/*.{${SOURCE_EXTENSIONS.join(",")}}`;
154
+ SOURCE_IGNORE = [
155
+ "**/node_modules/**",
156
+ "**/dist/**",
157
+ "**/build/**",
158
+ "**/.gradle/**",
159
+ "**/target/**",
160
+ "**/.git/**",
161
+ "**/.mason/**",
162
+ "**/vendor/**",
163
+ "**/__pycache__/**",
164
+ "**/venv/**",
165
+ "**/.venv/**",
166
+ "**/*.min.*",
167
+ "**/*.map",
168
+ "**/*.lock",
169
+ "**/generated/**",
170
+ "**/*.generated.*",
171
+ "**/R.java",
172
+ "**/BuildConfig.java",
173
+ "**/package-lock.json",
174
+ "**/yarn.lock",
175
+ "**/pnpm-lock.yaml"
176
+ ];
177
+ MAX_SOURCE_BYTES = 1024 * 1024;
178
+ }
179
+ });
105
180
 
106
181
  // src/utils/storage.ts
107
182
  import fs2 from "fs/promises";
@@ -142,7 +217,7 @@ async function readStoreJson(root, relative) {
142
217
  return parsed;
143
218
  } catch (error) {
144
219
  if (error.code === "ENOENT") return null;
145
- throw new Error(`Invalid Mason store ${relative}: ${error instanceof Error ? error.message : String(error)}`);
220
+ throw new Error(`Invalid Mason store ${relative}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
146
221
  }
147
222
  }
148
223
  async function writeStoreJson(root, relative, value) {
@@ -165,39 +240,231 @@ async function writeStoreJson(root, relative, value) {
165
240
  await fs2.rm(temporary, { force: true });
166
241
  }
167
242
  }
243
+ var init_storage = __esm({
244
+ "src/utils/storage.ts"() {
245
+ "use strict";
246
+ init_paths();
247
+ init_files();
248
+ }
249
+ });
168
250
 
169
- // src/snapshot/snapshot.ts
251
+ // src/automation/execution.ts
252
+ import os from "os";
253
+ import { randomUUID as randomUUID2 } from "crypto";
170
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);
297
+ 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;
308
+ } 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 });
319
+ }
320
+ }
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")) {
327
+ 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) {
336
+ try {
337
+ process.kill(attempt.pid, 0);
338
+ alive = true;
339
+ } catch (error) {
340
+ alive = error.code === "EPERM";
341
+ }
342
+ }
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
+ });
171
374
 
172
375
  // src/test-map.ts
173
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
+ }
442
+ return count3;
443
+ }
444
+ var init_test_map = __esm({
445
+ "src/test-map.ts"() {
446
+ "use strict";
447
+ init_files();
448
+ }
449
+ });
174
450
 
175
451
  // src/snapshot/snapshot.ts
176
- var exec2 = promisify2(execFile2);
177
- var repoPath = z.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
178
- var verificationFields = {
179
- refreshedHash: z.string().optional(),
180
- verifiedAt: z.string().optional(),
181
- verifiedHash: z.string().optional(),
182
- verificationFailed: z.boolean().optional(),
183
- verificationNote: z.string().optional()
184
- };
185
- var featureSchema = z.object({
186
- description: z.string(),
187
- files: z.array(repoPath),
188
- tests: z.array(repoPath).optional(),
189
- type: z.enum(["capability", "infrastructure"]).optional(),
190
- ...verificationFields
191
- }).passthrough();
192
- var flowSchema = z.object({ description: z.string(), chain: z.array(repoPath), ...verificationFields }).passthrough();
193
- var snapshotSchema = z.object({
194
- version: z.literal(2),
195
- createdAt: z.string(),
196
- updatedAt: z.string(),
197
- gitHash: z.string(),
198
- features: z.record(featureSchema),
199
- flows: z.record(flowSchema)
200
- }).passthrough();
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
+ 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
+ }
201
468
  async function getCurrentGitHash(rootDir) {
202
469
  try {
203
470
  const { stdout } = await exec2("git", ["rev-parse", "HEAD"], {
@@ -208,9 +475,48 @@ async function getCurrentGitHash(rootDir) {
208
475
  return "unknown";
209
476
  }
210
477
  }
478
+ var exec2, repoPath, verificationFields, featureSchema, flowSchema, snapshotSchema;
479
+ var init_snapshot = __esm({
480
+ "src/snapshot/snapshot.ts"() {
481
+ "use strict";
482
+ init_files();
483
+ init_files();
484
+ 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
+ });
211
514
 
212
515
  // src/drift/drift.ts
213
- var exec3 = promisify3(execFile3);
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";
214
520
  function parseChanges(output) {
215
521
  const fields = output.split("\0");
216
522
  const changes = [];
@@ -249,16 +555,17 @@ async function getWorkingTree(resolvedRoot) {
249
555
  return { available: false, changedFiles: [], untrackedFiles: [] };
250
556
  }
251
557
  }
252
-
253
- // src/audit/docs.ts
254
- import fs4 from "fs/promises";
255
- import path7 from "path";
256
- import { execFile as execFile5 } from "child_process";
257
- import { promisify as promisify5 } from "util";
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);
565
+ }
566
+ });
258
567
 
259
568
  // src/audit/tree.ts
260
- var MIN_GLYPH_LINES = 3;
261
- var GLYPHS = ["\u251C\u2500\u2500", "\u2514\u2500\u2500"];
262
569
  function glyphIndex(line) {
263
570
  for (const glyph of GLYPHS) {
264
571
  const idx = line.indexOf(glyph);
@@ -327,46 +634,16 @@ function extractTreeClaims(blockLines, blockStartLine) {
327
634
  }
328
635
  return claims;
329
636
  }
637
+ var MIN_GLYPH_LINES, GLYPHS;
638
+ var init_tree = __esm({
639
+ "src/audit/tree.ts"() {
640
+ "use strict";
641
+ MIN_GLYPH_LINES = 3;
642
+ GLYPHS = ["\u251C\u2500\u2500", "\u2514\u2500\u2500"];
643
+ }
644
+ });
330
645
 
331
646
  // src/audit/claims.ts
332
- var ROOT_FILE_NAMES = /* @__PURE__ */ new Set([
333
- "package.json",
334
- "package-lock.json",
335
- "pnpm-workspace.yaml",
336
- "tsconfig.json",
337
- "tsup.config.ts",
338
- "vitest.config.ts",
339
- "Makefile",
340
- "Dockerfile",
341
- "docker-compose.yml",
342
- "Cargo.toml",
343
- "go.mod",
344
- "go.sum",
345
- "pyproject.toml",
346
- "requirements.txt",
347
- "Gemfile",
348
- "composer.json",
349
- "settings.gradle.kts",
350
- "settings.gradle",
351
- "build.gradle.kts",
352
- "build.gradle",
353
- "manifest.json",
354
- "server.json",
355
- "README.md",
356
- "CHANGELOG.md",
357
- "LICENSE",
358
- "CLAUDE.md",
359
- "AGENTS.md",
360
- ".gitignore",
361
- ".env.example"
362
- ]);
363
- var SHELL_FENCE_INFOS = /* @__PURE__ */ new Set(["", "bash", "sh", "shell", "console", "zsh"]);
364
- var COMMAND_RE = /\b(npm|pnpm|yarn)\s+run\s+([A-Za-z0-9:_.-]+)/g;
365
- var COUNT_RE = /(\d+)\s+(modules?|packages?|workspaces?|crates?)\b/gi;
366
- var COUNT_DENYLIST_RE = /^\s*(manager|registr|lock|json)/i;
367
- var IGNORE_LINE = "<!-- mason:ignore -->";
368
- var IGNORE_START = "<!-- mason:ignore-start -->";
369
- var IGNORE_END = "<!-- mason:ignore-end -->";
370
647
  function normalizePathToken(token) {
371
648
  let t = token.trim();
372
649
  if (!t) return null;
@@ -525,12 +802,55 @@ function extractClaims(content2) {
525
802
  commands: [...commands.values()]
526
803
  };
527
804
  }
805
+ var ROOT_FILE_NAMES, SHELL_FENCE_INFOS, COMMAND_RE, COUNT_RE, COUNT_DENYLIST_RE, IGNORE_LINE, IGNORE_START, IGNORE_END;
806
+ var init_claims = __esm({
807
+ "src/audit/claims.ts"() {
808
+ "use strict";
809
+ init_tree();
810
+ ROOT_FILE_NAMES = /* @__PURE__ */ new Set([
811
+ "package.json",
812
+ "package-lock.json",
813
+ "pnpm-workspace.yaml",
814
+ "tsconfig.json",
815
+ "tsup.config.ts",
816
+ "vitest.config.ts",
817
+ "Makefile",
818
+ "Dockerfile",
819
+ "docker-compose.yml",
820
+ "Cargo.toml",
821
+ "go.mod",
822
+ "go.sum",
823
+ "pyproject.toml",
824
+ "requirements.txt",
825
+ "Gemfile",
826
+ "composer.json",
827
+ "settings.gradle.kts",
828
+ "settings.gradle",
829
+ "build.gradle.kts",
830
+ "build.gradle",
831
+ "manifest.json",
832
+ "server.json",
833
+ "README.md",
834
+ "CHANGELOG.md",
835
+ "LICENSE",
836
+ "CLAUDE.md",
837
+ "AGENTS.md",
838
+ ".gitignore",
839
+ ".env.example"
840
+ ]);
841
+ SHELL_FENCE_INFOS = /* @__PURE__ */ new Set(["", "bash", "sh", "shell", "console", "zsh"]);
842
+ COMMAND_RE = /\b(npm|pnpm|yarn)\s+run\s+([A-Za-z0-9:_.-]+)/g;
843
+ COUNT_RE = /(\d+)\s+(modules?|packages?|workspaces?|crates?)\b/gi;
844
+ COUNT_DENYLIST_RE = /^\s*(manager|registr|lock|json)/i;
845
+ IGNORE_LINE = "<!-- mason:ignore -->";
846
+ IGNORE_START = "<!-- mason:ignore-start -->";
847
+ IGNORE_END = "<!-- mason:ignore-end -->";
848
+ }
849
+ });
528
850
 
529
851
  // src/audit/git.ts
530
852
  import { execFile as execFile4 } from "child_process";
531
853
  import { promisify as promisify4 } from "util";
532
- var exec4 = promisify4(execFile4);
533
- var COMMIT_FORMAT = "%H%x09%cI%x09%s";
534
854
  function parseCommitLine(line) {
535
855
  const parts = line.split(" ");
536
856
  if (parts.length < 3 || !parts[0]) return null;
@@ -610,14 +930,20 @@ async function commitsTouchingSince(resolvedRoot, fromHash, pathspecs) {
610
930
  return null;
611
931
  }
612
932
  }
933
+ var exec4, COMMIT_FORMAT;
934
+ var init_git = __esm({
935
+ "src/audit/git.ts"() {
936
+ "use strict";
937
+ exec4 = promisify4(execFile4);
938
+ COMMIT_FORMAT = "%H%x09%cI%x09%s";
939
+ }
940
+ });
613
941
 
614
942
  // src/audit/docs.ts
615
- var exec5 = promisify5(execFile5);
616
- var DOC_CANDIDATES = [
617
- "AGENTS.md",
618
- "CLAUDE.md",
619
- ".claude/CLAUDE.md"
620
- ];
943
+ 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";
621
947
  async function isDirty(resolvedRoot, relPath) {
622
948
  try {
623
949
  const { stdout } = await exec5(
@@ -631,35 +957,55 @@ async function isDirty(resolvedRoot, relPath) {
631
957
  }
632
958
  }
633
959
  async function discoverDocs(resolvedRoot) {
634
- const docs = [];
635
- for (const candidate of DOC_CANDIDATES) {
960
+ const docs = await Promise.all(DOC_CANDIDATES.map(async (candidate) => {
636
961
  let content2;
637
962
  try {
638
963
  content2 = await fs4.readFile(path7.join(resolvedRoot, candidate), "utf-8");
639
964
  } catch {
640
- continue;
965
+ return null;
641
966
  }
642
- docs.push({
967
+ const [lastCommit, dirty] = await Promise.all([lastCommitOf(resolvedRoot, candidate), isDirty(resolvedRoot, candidate)]);
968
+ return {
643
969
  path: candidate,
644
970
  content: content2,
645
971
  lineCount: content2.split("\n").length,
646
- lastCommit: await lastCommitOf(resolvedRoot, candidate),
647
- dirty: await isDirty(resolvedRoot, candidate),
972
+ lastCommit,
973
+ dirty,
648
974
  claims: extractClaims(content2)
649
- });
650
- }
651
- return docs;
975
+ };
976
+ }));
977
+ return docs.filter((doc) => doc !== null);
652
978
  }
979
+ var exec5, DOC_CANDIDATES;
980
+ var init_docs = __esm({
981
+ "src/audit/docs.ts"() {
982
+ "use strict";
983
+ init_claims();
984
+ init_git();
985
+ exec5 = promisify5(execFile5);
986
+ DOC_CANDIDATES = [
987
+ "AGENTS.md",
988
+ "CLAUDE.md",
989
+ ".claude/CLAUDE.md"
990
+ ];
991
+ }
992
+ });
653
993
 
654
994
  // src/audit/types.ts
655
- var ALL_CHECKS = [
656
- "deleted-reference",
657
- "new-module",
658
- "stale-count",
659
- "dead-command",
660
- "deps-changed",
661
- "decision-anchor-drift"
662
- ];
995
+ var ALL_CHECKS;
996
+ var init_types = __esm({
997
+ "src/audit/types.ts"() {
998
+ "use strict";
999
+ ALL_CHECKS = [
1000
+ "deleted-reference",
1001
+ "new-module",
1002
+ "stale-count",
1003
+ "dead-command",
1004
+ "deps-changed",
1005
+ "decision-anchor-drift"
1006
+ ];
1007
+ }
1008
+ });
663
1009
 
664
1010
  // src/audit/checks/deleted-reference.ts
665
1011
  import fs5 from "fs/promises";
@@ -751,31 +1097,17 @@ async function checkDeletedReferences(ctx) {
751
1097
  }
752
1098
  return result;
753
1099
  }
1100
+ var init_deleted_reference = __esm({
1101
+ "src/audit/checks/deleted-reference.ts"() {
1102
+ "use strict";
1103
+ init_git();
1104
+ init_checks();
1105
+ }
1106
+ });
754
1107
 
755
1108
  // src/audit/checks/new-module.ts
756
1109
  import fg2 from "fast-glob";
757
1110
  import path9 from "path";
758
- var DIR_DENYLIST = /* @__PURE__ */ new Set([
759
- "node_modules",
760
- "dist",
761
- "build",
762
- "out",
763
- "coverage",
764
- "target",
765
- "vendor",
766
- "__pycache__",
767
- "venv",
768
- ".venv",
769
- ".git",
770
- ".gradle",
771
- ".mason",
772
- ".claude",
773
- ".github",
774
- ".vscode",
775
- ".idea"
776
- ]);
777
- var SECOND_LEVEL_MIN_SOURCE_FILES = 2;
778
- var ENUMERATION_THRESHOLD = 2;
779
1111
  function escapeRegExp(text2) {
780
1112
  return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
781
1113
  }
@@ -823,12 +1155,19 @@ async function checkNewModules(ctx) {
823
1155
  }
824
1156
  });
825
1157
  };
826
- for (const topDir of await listSubdirs(ctx.root)) {
827
- const absTop = path9.join(ctx.root, topDir);
1158
+ for (const candidate of await moduleCandidates(ctx.root, combinedDocs)) {
1159
+ await flag(candidate.dir, candidate.sourceFileCount);
1160
+ }
1161
+ return result;
1162
+ }
1163
+ async function moduleCandidates(root, combinedDocs) {
1164
+ const candidates = [];
1165
+ for (const topDir of await listSubdirs(root)) {
1166
+ const absTop = path9.join(root, topDir);
828
1167
  const topMentioned = isMentioned(combinedDocs, topDir);
829
1168
  if (!topMentioned) {
830
- const count2 = await countSourceFiles(absTop);
831
- if (count2 >= 1) await flag(topDir, count2);
1169
+ const count3 = await countSourceFiles(absTop);
1170
+ if (count3 >= 1) candidates.push({ dir: topDir, sourceFileCount: count3 });
832
1171
  continue;
833
1172
  }
834
1173
  const subdirs = await listSubdirs(absTop);
@@ -836,20 +1175,49 @@ async function checkNewModules(ctx) {
836
1175
  if (mentioned.length < ENUMERATION_THRESHOLD) continue;
837
1176
  for (const sub of subdirs) {
838
1177
  if (isMentioned(combinedDocs, sub)) continue;
839
- const count2 = await countSourceFiles(path9.join(absTop, sub));
840
- if (count2 >= SECOND_LEVEL_MIN_SOURCE_FILES) {
841
- await flag(`${topDir}/${sub}`, count2);
1178
+ const count3 = await countSourceFiles(path9.join(absTop, sub));
1179
+ if (count3 >= SECOND_LEVEL_MIN_SOURCE_FILES) {
1180
+ candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount: count3 });
842
1181
  }
843
1182
  }
844
1183
  }
845
- return result;
1184
+ return candidates;
846
1185
  }
1186
+ var DIR_DENYLIST, SECOND_LEVEL_MIN_SOURCE_FILES, ENUMERATION_THRESHOLD;
1187
+ var init_new_module = __esm({
1188
+ "src/audit/checks/new-module.ts"() {
1189
+ "use strict";
1190
+ init_snapshot();
1191
+ init_git();
1192
+ init_checks();
1193
+ DIR_DENYLIST = /* @__PURE__ */ new Set([
1194
+ "node_modules",
1195
+ "dist",
1196
+ "build",
1197
+ "out",
1198
+ "coverage",
1199
+ "target",
1200
+ "vendor",
1201
+ "__pycache__",
1202
+ "venv",
1203
+ ".venv",
1204
+ ".git",
1205
+ ".gradle",
1206
+ ".mason",
1207
+ ".claude",
1208
+ ".github",
1209
+ ".vscode",
1210
+ ".idea"
1211
+ ]);
1212
+ SECOND_LEVEL_MIN_SOURCE_FILES = 2;
1213
+ ENUMERATION_THRESHOLD = 2;
1214
+ }
1215
+ });
847
1216
 
848
1217
  // src/audit/checks/stale-count.ts
849
1218
  import fs6 from "fs/promises";
850
1219
  import path10 from "path";
851
1220
  import fg3 from "fast-glob";
852
- var MEMBERS_CAP = 50;
853
1221
  async function readIfExists(absPath) {
854
1222
  try {
855
1223
  return await fs6.readFile(absPath, "utf-8");
@@ -991,12 +1359,19 @@ async function checkStaleCounts(ctx) {
991
1359
  }
992
1360
  return result;
993
1361
  }
1362
+ var MEMBERS_CAP;
1363
+ var init_stale_count = __esm({
1364
+ "src/audit/checks/stale-count.ts"() {
1365
+ "use strict";
1366
+ init_checks();
1367
+ MEMBERS_CAP = 50;
1368
+ }
1369
+ });
994
1370
 
995
1371
  // src/audit/checks/dead-command.ts
996
1372
  import fs7 from "fs/promises";
997
1373
  import path11 from "path";
998
1374
  import fg4 from "fast-glob";
999
- var AVAILABLE_SCRIPTS_CAP = 30;
1000
1375
  async function scriptsOf(absManifest) {
1001
1376
  try {
1002
1377
  const pkg = JSON.parse(await fs7.readFile(absManifest, "utf-8"));
@@ -1025,18 +1400,10 @@ async function checkDeadCommands(ctx) {
1025
1400
  const loadWorkspaceScripts = async () => {
1026
1401
  if (workspaceScripts !== null) return workspaceScripts;
1027
1402
  workspaceScripts = /* @__PURE__ */ new Set();
1028
- const manifests = await fg4("**/package.json", {
1029
- cwd: ctx.root,
1030
- ignore: [
1031
- "**/node_modules/**",
1032
- "**/dist/**",
1033
- "**/build/**",
1034
- "package.json"
1035
- ]
1036
- });
1403
+ const manifests = await commandManifests(ctx.root);
1037
1404
  manifestsChecked = ["package.json", ...manifests.sort()];
1038
- for (const manifest2 of manifests) {
1039
- const scripts = await scriptsOf(path11.join(ctx.root, manifest2));
1405
+ for (const manifest of manifests) {
1406
+ const scripts = await scriptsOf(path11.join(ctx.root, manifest));
1040
1407
  for (const name of scripts ?? []) workspaceScripts.add(name);
1041
1408
  }
1042
1409
  return workspaceScripts;
@@ -1061,26 +1428,84 @@ async function checkDeadCommands(ctx) {
1061
1428
  }
1062
1429
  return result;
1063
1430
  }
1431
+ function commandManifests(root) {
1432
+ return fg4("**/package.json", {
1433
+ cwd: root,
1434
+ ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", ".mason/reports/**", "package.json"]
1435
+ });
1436
+ }
1437
+ var AVAILABLE_SCRIPTS_CAP;
1438
+ var init_dead_command = __esm({
1439
+ "src/audit/checks/dead-command.ts"() {
1440
+ "use strict";
1441
+ init_checks();
1442
+ AVAILABLE_SCRIPTS_CAP = 30;
1443
+ }
1444
+ });
1445
+
1446
+ // src/audit/release-metadata.ts
1447
+ import { execFile as execFile6 } from "child_process";
1448
+ import { promisify as promisify6 } from "util";
1449
+ async function git(root, args) {
1450
+ return (await exec6("git", args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
1451
+ }
1452
+ function withoutAndroidReleaseValues(text2) {
1453
+ if (/\/\*|"""|'''/.test(text2) || !/id\s*\(?\s*["']com\.android\.(application|library)["']/.test(text2)) return null;
1454
+ const scopes = [];
1455
+ const normalized = [];
1456
+ let assignments = 0;
1457
+ for (const line of text2.split("\n")) {
1458
+ const code = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\/\/.*$/g, (token) => token.startsWith("//") ? "" : '""');
1459
+ const assignment = line.match(/^(\s*)(versionName|versionCode)(\s*(?:=\s*|\s+))("[A-Za-z0-9._+-]+"|'[A-Za-z0-9._+-]+'|\d+)(\s*)$/);
1460
+ if (assignment && scopes.join("/") === "android/defaultConfig" && (assignment[2] === "versionCode" ? /^\d+$/.test(assignment[4]) : /^["']/.test(assignment[4]))) {
1461
+ normalized.push(assignment[1] + assignment[2] + assignment[3] + "<release-value>" + assignment[5]);
1462
+ assignments++;
1463
+ } else {
1464
+ if (/\bversion(?:Name|Code)\b/.test(line)) return null;
1465
+ normalized.push(line);
1466
+ }
1467
+ const named = code.match(/^\s*(android|defaultConfig)\s*\{\s*$/)?.[1];
1468
+ for (const brace of code.matchAll(/[{}]/g)) {
1469
+ if (brace[0] === "{") scopes.push(named ?? "unknown");
1470
+ else if (!scopes.length) return null;
1471
+ else scopes.pop();
1472
+ }
1473
+ }
1474
+ return assignments && !scopes.length ? normalized.join("\n") : null;
1475
+ }
1476
+ async function releaseMetadataOnly(root, commit) {
1477
+ if (!commit.files.length || !commit.files.every((file) => /(^|\/)build\.gradle(?:\.kts)?$/.test(file))) return false;
1478
+ try {
1479
+ const parents = (await git(root, ["rev-list", "--parents", "-n", "1", commit.hash])).trim().split(/\s+/);
1480
+ if (parents.length !== 2) return false;
1481
+ for (const file of commit.files) {
1482
+ const raw = await git(root, ["diff", "--raw", "-z", "--no-renames", "--no-ext-diff", "--no-textconv", parents[1], commit.hash, "--", file]);
1483
+ if (!/^:(100644|100755) \1 [a-f0-9]+ [a-f0-9]+ M\0/.test(raw)) return false;
1484
+ const [before, after] = await Promise.all([
1485
+ git(root, ["show", parents[1] + ":" + file]),
1486
+ git(root, ["show", commit.hash + ":" + file])
1487
+ ]);
1488
+ const previous = withoutAndroidReleaseValues(before);
1489
+ if (previous === null || previous !== withoutAndroidReleaseValues(after)) return false;
1490
+ }
1491
+ return true;
1492
+ } catch {
1493
+ return false;
1494
+ }
1495
+ }
1496
+ var exec6;
1497
+ var init_release_metadata = __esm({
1498
+ "src/audit/release-metadata.ts"() {
1499
+ "use strict";
1500
+ exec6 = promisify6(execFile6);
1501
+ }
1502
+ });
1064
1503
 
1065
1504
  // src/audit/checks/deps-changed.ts
1066
- var MANIFEST_COMMITS_CAP = 10;
1067
- var MANIFEST_PATHSPECS = [
1068
- ":(glob)**/package.json",
1069
- ":(glob)**/build.gradle.kts",
1070
- ":(glob)**/build.gradle",
1071
- "settings.gradle.kts",
1072
- "settings.gradle",
1073
- "gradle/libs.versions.toml",
1074
- ":(glob)**/Cargo.toml",
1075
- "go.mod",
1076
- "pyproject.toml",
1077
- "requirements.txt",
1078
- "Gemfile",
1079
- "composer.json"
1080
- ];
1081
1505
  async function checkDepsChanged(ctx) {
1082
1506
  const result = emptyResult();
1083
1507
  result.suppressedAdvisories = [];
1508
+ const releaseOnly = /* @__PURE__ */ new Map();
1084
1509
  for (const doc of ctx.docs) {
1085
1510
  if (!doc.lastCommit) {
1086
1511
  result.skipped.push({
@@ -1110,6 +1535,15 @@ async function checkDepsChanged(ctx) {
1110
1535
  });
1111
1536
  continue;
1112
1537
  }
1538
+ const relevant = [];
1539
+ for (const commit of range.commits) {
1540
+ if (!releaseOnly.has(commit.hash) && releaseOnly.size < 100) {
1541
+ releaseOnly.set(commit.hash, await releaseMetadataOnly(ctx.root, commit));
1542
+ }
1543
+ if (!releaseOnly.get(commit.hash)) relevant.push(commit);
1544
+ }
1545
+ range.commits = relevant;
1546
+ range.total = relevant.length;
1113
1547
  if (range.total === 0) continue;
1114
1548
  const latest = range.commits[0];
1115
1549
  (doc.dirty ? result.suppressedAdvisories : result.advisories).push({
@@ -1126,111 +1560,57 @@ async function checkDepsChanged(ctx) {
1126
1560
  }
1127
1561
  return result;
1128
1562
  }
1129
-
1130
- // src/decisions/drift.ts
1131
- import path14 from "path";
1132
-
1133
- // src/decisions/decisions.ts
1134
- import fs8 from "fs/promises";
1135
- import path13 from "path";
1136
- import { createHash } from "crypto";
1563
+ var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
1564
+ var init_deps_changed = __esm({
1565
+ "src/audit/checks/deps-changed.ts"() {
1566
+ "use strict";
1567
+ init_release_metadata();
1568
+ init_git();
1569
+ init_checks();
1570
+ MANIFEST_COMMITS_CAP = 10;
1571
+ MANIFEST_PATHSPECS = [
1572
+ ":(glob)**/package.json",
1573
+ ":(glob)**/build.gradle.kts",
1574
+ ":(glob)**/build.gradle",
1575
+ "settings.gradle.kts",
1576
+ "settings.gradle",
1577
+ "gradle/libs.versions.toml",
1578
+ ":(glob)**/Cargo.toml",
1579
+ "go.mod",
1580
+ "pyproject.toml",
1581
+ "requirements.txt",
1582
+ "Gemfile",
1583
+ "composer.json"
1584
+ ];
1585
+ }
1586
+ });
1137
1587
 
1138
1588
  // src/context/lexical.ts
1139
1589
  import path12 from "path";
1140
-
1141
- // src/decisions/provenance.ts
1142
- import { z as z2 } from "zod";
1143
- var text = (max) => z2.string().trim().min(1).max(max);
1144
- var decisionSourceSchema = z2.object({
1145
- kind: z2.enum(["pull_request", "issue", "incident", "discussion", "document", "other"]),
1146
- reference: text(1e3),
1147
- note: text(500).optional()
1148
- }).strict();
1149
- var attributionSchema = z2.object({
1150
- owner: text(200).nullable().optional(),
1151
- sources: z2.array(decisionSourceSchema).max(20).optional(),
1152
- actor: text(200).optional()
1153
- });
1154
- var contentSchema = z2.object({
1155
- title: z2.string().min(1),
1156
- body: z2.string().min(1),
1157
- category: z2.enum(["decision", "gotcha", "deprecation", "convention"]),
1158
- files: z2.array(z2.string().refine((f) => normalizeRepoPath(f) !== null)),
1159
- owner: text(200).optional(),
1160
- sources: z2.array(decisionSourceSchema).max(20)
1161
- });
1162
- var approvalSchema = z2.enum(["unreviewed", "proposed", "accepted"]);
1163
- var statusSchema = z2.enum(["active", "superseded", "retired"]);
1164
- var reviewEvidenceSchema = z2.object({
1165
- baseHash: z2.string(),
1166
- headHash: z2.string(),
1167
- historyAvailable: z2.boolean(),
1168
- changedFiles: z2.array(z2.string()),
1169
- localChanges: z2.array(z2.string())
1170
- });
1171
- var eventSchema = z2.object({
1172
- kind: z2.enum(["imported", "created", "revised", "accepted", "reaffirmed", "retired", "superseded"]),
1173
- at: z2.string().datetime(),
1174
- actor: text(200).optional(),
1175
- note: text(1500).optional(),
1176
- revision: z2.number().int().positive(),
1177
- content: contentSchema,
1178
- approval: approvalSchema,
1179
- status: statusSchema,
1180
- refreshedHash: z2.string(),
1181
- evidence: reviewEvidenceSchema.optional()
1590
+ var init_lexical = __esm({
1591
+ "src/context/lexical.ts"() {
1592
+ "use strict";
1593
+ }
1182
1594
  });
1183
- var legacySchema = z2.object({
1184
- version: z2.literal(1),
1185
- id: z2.string().regex(/^[a-zA-Z0-9_-]+$/),
1186
- title: z2.string().min(1),
1187
- body: z2.string().min(1),
1188
- category: contentSchema.shape.category,
1189
- files: contentSchema.shape.files,
1190
- createdAt: z2.string(),
1191
- updatedAt: z2.string(),
1192
- refreshedHash: z2.string(),
1193
- status: z2.enum(["active", "superseded"]),
1194
- supersededBy: z2.string().optional()
1195
- }).passthrough();
1196
- var currentSchema = legacySchema.extend({
1197
- version: z2.literal(2),
1198
- status: statusSchema,
1199
- approval: approvalSchema,
1200
- revision: z2.number().int().positive(),
1201
- owner: text(200).optional(),
1202
- sources: z2.array(decisionSourceSchema).max(20),
1203
- history: z2.array(eventSchema).min(1)
1204
- }).superRefine((record, ctx) => {
1205
- const invalid = (message) => ctx.addIssue({ code: "custom", message });
1206
- const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
1207
- let previous;
1208
- for (const event of record.history) {
1209
- if (!previous) {
1210
- if (!["created", "imported"].includes(event.kind) || event.revision !== 1) invalid("History must begin with creation or legacy import at revision 1");
1211
- if (event.approval !== (event.kind === "created" ? "proposed" : "unreviewed")) invalid("Initial records cannot claim acceptance");
1212
- } else {
1213
- if (["created", "imported"].includes(event.kind)) invalid("History cannot restart");
1214
- if (previous.status !== "active") invalid("Archived decisions cannot be changed");
1215
- if (event.revision !== previous.revision + (event.kind === "revised" ? 1 : 0)) invalid("Invalid revision sequence");
1216
- if (event.kind !== "revised" && !same(event.content, previous.content)) invalid("A review cannot silently revise decision content");
1217
- if (event.kind === "reaffirmed" && previous.approval !== "accepted") invalid("Only accepted decisions can be reaffirmed");
1218
- if (event.kind === "accepted" && previous.approval === "accepted") invalid("Use reaffirmation for an accepted decision");
1219
- const approval = event.kind === "revised" ? "proposed" : ["accepted", "reaffirmed"].includes(event.kind) ? "accepted" : previous.approval;
1220
- if (event.approval !== approval) invalid("Approval disagrees with review history");
1221
- if (!["accepted", "reaffirmed"].includes(event.kind) && event.refreshedHash !== previous.refreshedHash) invalid("Only a review can refresh the evidence baseline");
1222
- }
1223
- if (event.kind !== "imported" && event.status !== (event.kind === "retired" ? "retired" : event.kind === "superseded" ? "superseded" : "active")) invalid("Lifecycle disagrees with history");
1224
- if (["accepted", "reaffirmed", "retired"].includes(event.kind) && (!event.actor || !event.note || !event.evidence)) invalid("Reviews require a named reviewer, reason, and code evidence");
1225
- if (["accepted", "reaffirmed"].includes(event.kind)) {
1226
- if (!event.content.owner || !event.content.sources.length) invalid("Accepted decisions require an owner and source");
1227
- if (!event.evidence || !/^[a-f0-9]{40,64}$/.test(event.evidence.headHash) || event.refreshedHash !== event.evidence.headHash || event.evidence.localChanges.length) invalid("Acceptance requires a committed evidence baseline");
1228
- }
1229
- previous = event;
1230
- }
1231
- if (!previous || !same(previous.content, decisionContent(record)) || previous.approval !== record.approval || previous.status !== record.status || previous.revision !== record.revision || previous.refreshedHash !== record.refreshedHash) invalid("Decision does not match the final history event");
1595
+
1596
+ // src/context/trust.ts
1597
+ function assessTrust(entry, freshness) {
1598
+ const verification = entry.verificationFailed ? "failed" : entry.verifiedAt ? "passed" : "unverified";
1599
+ const reasons = [];
1600
+ if (freshness === "unknown") reasons.push("Anchors, history, or working-tree evidence are unavailable; verify before relying on this entry.");
1601
+ if (freshness === "changed") reasons.push("Anchored files changed; verify against current code before relying on this entry.");
1602
+ if (verification === "failed") reasons.push(`Verification failed: ${entry.verificationNote ?? "re-map this entry before relying on it"}`);
1603
+ if (verification === "unverified") reasons.push("No correctness verification has been recorded.");
1604
+ return { freshness, verification, verifiedAt: entry.verifiedAt, verifiedHash: entry.verifiedHash, reasons };
1605
+ }
1606
+ var init_trust = __esm({
1607
+ "src/context/trust.ts"() {
1608
+ "use strict";
1609
+ }
1232
1610
  });
1233
- var decisionSchema = z2.union([legacySchema, currentSchema]);
1611
+
1612
+ // src/decisions/provenance.ts
1613
+ import { z as z3 } from "zod";
1234
1614
  function decisionContent(record) {
1235
1615
  return {
1236
1616
  title: record.title,
@@ -1246,10 +1626,10 @@ function decisionApproval(record) {
1246
1626
  }
1247
1627
  function effectiveDecision(record) {
1248
1628
  if (record.version !== 2 || record.status !== "active" || record.approval !== "proposed") return record;
1249
- let index = record.history.length - 1;
1250
- while (index >= 0 && !["accepted", "reaffirmed"].includes(record.history[index].kind)) index--;
1251
- if (index < 0) return record;
1252
- const event = record.history[index];
1629
+ let index2 = record.history.length - 1;
1630
+ while (index2 >= 0 && !["accepted", "reaffirmed"].includes(record.history[index2].kind)) index2--;
1631
+ if (index2 < 0) return record;
1632
+ const event = record.history[index2];
1253
1633
  return {
1254
1634
  ...record,
1255
1635
  ...event.content,
@@ -1258,7 +1638,7 @@ function effectiveDecision(record) {
1258
1638
  revision: event.revision,
1259
1639
  refreshedHash: event.refreshedHash,
1260
1640
  updatedAt: event.at,
1261
- history: record.history.slice(0, index + 1)
1641
+ history: record.history.slice(0, index2 + 1)
1262
1642
  };
1263
1643
  }
1264
1644
  function decisionProvenance(record, freshness = "unknown") {
@@ -1274,8 +1654,124 @@ function decisionProvenance(record, freshness = "unknown") {
1274
1654
  lastReview: review ? { reviewer: review.actor, at: review.at, note: review.note, gitHash: review.refreshedHash } : null
1275
1655
  };
1276
1656
  }
1657
+ function decisionTrust(record, freshness) {
1658
+ const review = decisionProvenance(record, freshness).lastReview;
1659
+ return assessTrust(review ? { verifiedAt: review.at, verifiedHash: review.gitHash } : {}, freshness);
1660
+ }
1661
+ function revisionKnowledge(record, freshness) {
1662
+ return { ...decisionContent(record), ...decisionProvenance(record, freshness), trust: decisionTrust(record, freshness) };
1663
+ }
1664
+ function decisionKnowledge(record, freshness = "unknown", proposalFreshness = "unknown") {
1665
+ const effective = effectiveDecision(record);
1666
+ return {
1667
+ ...revisionKnowledge(effective, freshness),
1668
+ ...effective !== record ? { pendingProposal: revisionKnowledge(record, proposalFreshness) } : {}
1669
+ };
1670
+ }
1671
+ var text, decisionSourceSchema, attributionSchema, contentSchema, approvalSchema, statusSchema, reviewEvidenceSchema, eventSchema, legacySchema, currentSchema, decisionSchema;
1672
+ var init_provenance = __esm({
1673
+ "src/decisions/provenance.ts"() {
1674
+ "use strict";
1675
+ init_paths();
1676
+ init_trust();
1677
+ text = (max) => z3.string().trim().min(1).max(max);
1678
+ decisionSourceSchema = z3.object({
1679
+ kind: z3.enum(["pull_request", "issue", "incident", "discussion", "document", "other"]),
1680
+ reference: text(1e3),
1681
+ note: text(500).optional()
1682
+ }).strict();
1683
+ attributionSchema = z3.object({
1684
+ owner: text(200).nullable().optional(),
1685
+ sources: z3.array(decisionSourceSchema).max(20).optional(),
1686
+ actor: text(200).optional()
1687
+ });
1688
+ contentSchema = z3.object({
1689
+ title: z3.string().min(1),
1690
+ body: z3.string().min(1),
1691
+ category: z3.enum(["decision", "gotcha", "deprecation", "convention"]),
1692
+ files: z3.array(z3.string().refine((f) => normalizeRepoPath(f) !== null)),
1693
+ owner: text(200).optional(),
1694
+ sources: z3.array(decisionSourceSchema).max(20)
1695
+ });
1696
+ approvalSchema = z3.enum(["unreviewed", "proposed", "accepted"]);
1697
+ statusSchema = z3.enum(["active", "superseded", "retired"]);
1698
+ reviewEvidenceSchema = z3.object({
1699
+ baseHash: z3.string(),
1700
+ headHash: z3.string(),
1701
+ historyAvailable: z3.boolean(),
1702
+ changedFiles: z3.array(z3.string()),
1703
+ localChanges: z3.array(z3.string())
1704
+ });
1705
+ eventSchema = z3.object({
1706
+ kind: z3.enum(["imported", "created", "revised", "accepted", "reaffirmed", "retired", "superseded"]),
1707
+ at: z3.string().datetime(),
1708
+ actor: text(200).optional(),
1709
+ note: text(1500).optional(),
1710
+ revision: z3.number().int().positive(),
1711
+ content: contentSchema,
1712
+ approval: approvalSchema,
1713
+ status: statusSchema,
1714
+ refreshedHash: z3.string(),
1715
+ evidence: reviewEvidenceSchema.optional()
1716
+ });
1717
+ legacySchema = z3.object({
1718
+ version: z3.literal(1),
1719
+ id: z3.string().regex(/^[a-zA-Z0-9_-]+$/),
1720
+ title: z3.string().min(1),
1721
+ body: z3.string().min(1),
1722
+ category: contentSchema.shape.category,
1723
+ files: contentSchema.shape.files,
1724
+ createdAt: z3.string(),
1725
+ updatedAt: z3.string(),
1726
+ refreshedHash: z3.string(),
1727
+ status: z3.enum(["active", "superseded"]),
1728
+ supersededBy: z3.string().optional()
1729
+ }).passthrough();
1730
+ currentSchema = legacySchema.extend({
1731
+ version: z3.literal(2),
1732
+ status: statusSchema,
1733
+ approval: approvalSchema,
1734
+ revision: z3.number().int().positive(),
1735
+ owner: text(200).optional(),
1736
+ sources: z3.array(decisionSourceSchema).max(20),
1737
+ history: z3.array(eventSchema).min(1)
1738
+ }).superRefine((record, ctx) => {
1739
+ const invalid = (message2) => ctx.addIssue({ code: "custom", message: message2 });
1740
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
1741
+ let previous;
1742
+ for (const event of record.history) {
1743
+ if (!previous) {
1744
+ if (!["created", "imported"].includes(event.kind) || event.revision !== 1) invalid("History must begin with creation or legacy import at revision 1");
1745
+ if (event.approval !== (event.kind === "created" ? "proposed" : "unreviewed")) invalid("Initial records cannot claim acceptance");
1746
+ } else {
1747
+ if (["created", "imported"].includes(event.kind)) invalid("History cannot restart");
1748
+ if (previous.status !== "active") invalid("Archived decisions cannot be changed");
1749
+ if (event.revision !== previous.revision + (event.kind === "revised" ? 1 : 0)) invalid("Invalid revision sequence");
1750
+ if (event.kind !== "revised" && !same(event.content, previous.content)) invalid("A review cannot silently revise decision content");
1751
+ if (event.kind === "reaffirmed" && previous.approval !== "accepted") invalid("Only accepted decisions can be reaffirmed");
1752
+ if (event.kind === "accepted" && previous.approval === "accepted") invalid("Use reaffirmation for an accepted decision");
1753
+ const approval = event.kind === "revised" ? "proposed" : ["accepted", "reaffirmed"].includes(event.kind) ? "accepted" : previous.approval;
1754
+ if (event.approval !== approval) invalid("Approval disagrees with review history");
1755
+ if (!["accepted", "reaffirmed"].includes(event.kind) && event.refreshedHash !== previous.refreshedHash) invalid("Only a review can refresh the evidence baseline");
1756
+ }
1757
+ if (event.kind !== "imported" && event.status !== (event.kind === "retired" ? "retired" : event.kind === "superseded" ? "superseded" : "active")) invalid("Lifecycle disagrees with history");
1758
+ if (["accepted", "reaffirmed", "retired"].includes(event.kind) && (!event.actor || !event.note || !event.evidence)) invalid("Reviews require a named reviewer, reason, and code evidence");
1759
+ if (["accepted", "reaffirmed"].includes(event.kind)) {
1760
+ if (!event.content.owner || !event.content.sources.length) invalid("Accepted decisions require an owner and source");
1761
+ if (!event.evidence || !/^[a-f0-9]{40,64}$/.test(event.evidence.headHash) || event.refreshedHash !== event.evidence.headHash || event.evidence.localChanges.length) invalid("Acceptance requires a committed evidence baseline");
1762
+ }
1763
+ previous = event;
1764
+ }
1765
+ if (!previous || !same(previous.content, decisionContent(record)) || previous.approval !== record.approval || previous.status !== record.status || previous.revision !== record.revision || previous.refreshedHash !== record.refreshedHash) invalid("Decision does not match the final history event");
1766
+ });
1767
+ decisionSchema = z3.union([legacySchema, currentSchema]);
1768
+ }
1769
+ });
1277
1770
 
1278
1771
  // src/decisions/decisions.ts
1772
+ import fs8 from "fs/promises";
1773
+ import path13 from "path";
1774
+ import { createHash } from "crypto";
1279
1775
  async function loadDecisionStore(rootDir) {
1280
1776
  const records = [];
1281
1777
  const diagnostics = [];
@@ -1299,8 +1795,19 @@ async function loadDecisionStore(rootDir) {
1299
1795
  }
1300
1796
  return { records, diagnostics };
1301
1797
  }
1798
+ var init_decisions = __esm({
1799
+ "src/decisions/decisions.ts"() {
1800
+ "use strict";
1801
+ init_storage();
1802
+ init_paths();
1803
+ init_snapshot();
1804
+ init_lexical();
1805
+ init_provenance();
1806
+ }
1807
+ });
1302
1808
 
1303
1809
  // src/decisions/drift.ts
1810
+ import path14 from "path";
1304
1811
  async function computeDecisionDrift(rootDir, decisions) {
1305
1812
  const resolvedRoot = path14.resolve(rootDir);
1306
1813
  const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
@@ -1330,6 +1837,16 @@ async function computeDecisionDrift(rootDir, decisions) {
1330
1837
  }
1331
1838
  return report;
1332
1839
  }
1840
+ var init_drift2 = __esm({
1841
+ "src/decisions/drift.ts"() {
1842
+ "use strict";
1843
+ init_drift();
1844
+ init_paths();
1845
+ init_snapshot();
1846
+ init_decisions();
1847
+ init_provenance();
1848
+ }
1849
+ });
1333
1850
 
1334
1851
  // src/audit/checks/decision-anchor.ts
1335
1852
  async function checkDecisionAnchors(ctx) {
@@ -1373,26 +1890,48 @@ async function checkDecisionAnchors(ctx) {
1373
1890
  }
1374
1891
  return result;
1375
1892
  }
1893
+ var init_decision_anchor = __esm({
1894
+ "src/audit/checks/decision-anchor.ts"() {
1895
+ "use strict";
1896
+ init_drift2();
1897
+ init_decisions();
1898
+ init_provenance();
1899
+ init_checks();
1900
+ }
1901
+ });
1376
1902
 
1377
1903
  // src/audit/checks/index.ts
1378
- var CHECKS = {
1379
- "deleted-reference": checkDeletedReferences,
1380
- "new-module": checkNewModules,
1381
- "stale-count": checkStaleCounts,
1382
- "dead-command": checkDeadCommands,
1383
- "deps-changed": checkDepsChanged,
1384
- "decision-anchor-drift": checkDecisionAnchors
1385
- };
1386
1904
  function emptyResult() {
1387
1905
  return { issues: [], advisories: [], skipped: [] };
1388
1906
  }
1907
+ var CHECKS;
1908
+ var init_checks = __esm({
1909
+ "src/audit/checks/index.ts"() {
1910
+ "use strict";
1911
+ init_deleted_reference();
1912
+ init_new_module();
1913
+ init_stale_count();
1914
+ init_dead_command();
1915
+ init_deps_changed();
1916
+ init_decision_anchor();
1917
+ CHECKS = {
1918
+ "deleted-reference": checkDeletedReferences,
1919
+ "new-module": checkNewModules,
1920
+ "stale-count": checkStaleCounts,
1921
+ "dead-command": checkDeadCommands,
1922
+ "deps-changed": checkDepsChanged,
1923
+ "decision-anchor-drift": checkDecisionAnchors
1924
+ };
1925
+ }
1926
+ });
1389
1927
 
1390
1928
  // src/audit/audit.ts
1929
+ import fs9 from "fs/promises";
1930
+ import path15 from "path";
1391
1931
  async function computeAudit(rootDir, options = {}) {
1392
1932
  const resolvedRoot = path15.resolve(rootDir);
1393
- const docs = await discoverDocs(resolvedRoot);
1933
+ const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
1394
1934
  if (docs.length === 0) return null;
1395
- const headHash = await getCurrentGitHash(resolvedRoot);
1396
1935
  const report = {
1397
1936
  version: 1,
1398
1937
  root: resolvedRoot,
@@ -1447,97 +1986,22 @@ async function computeAudit(rootDir, options = {}) {
1447
1986
  report.clean = report.issues.length === 0;
1448
1987
  return report;
1449
1988
  }
1989
+ var init_audit = __esm({
1990
+ "src/audit/audit.ts"() {
1991
+ "use strict";
1992
+ init_drift();
1993
+ init_snapshot();
1994
+ init_docs();
1995
+ init_types();
1996
+ init_checks();
1997
+ }
1998
+ });
1450
1999
 
1451
2000
  // src/audit/repair.ts
1452
- var checkSchema = z3.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
1453
- var commitSchema = z3.object({ hash: z3.string().regex(/^[a-f0-9]{40,64}$/), date: z3.string(), subject: z3.string() });
1454
- var anchorSchema = z3.object({ doc: z3.string(), line: z3.number().int().positive().nullable(), excerpt: z3.string().nullable() });
1455
- var count = z3.number().int().nonnegative();
1456
- var evidenceSchema = z3.discriminatedUnion("kind", [
1457
- z3.object({
1458
- kind: z3.literal("missing-path"),
1459
- claimed: z3.string(),
1460
- renamedTo: z3.string().nullable(),
1461
- deletedInCommit: commitSchema.nullable(),
1462
- everTracked: z3.boolean(),
1463
- parentDirExists: z3.boolean()
1464
- }),
1465
- z3.object({
1466
- kind: z3.literal("unmentioned-dir"),
1467
- dir: z3.string(),
1468
- sourceFileCount: count,
1469
- firstCommit: commitSchema.nullable(),
1470
- checkedDocs: z3.array(z3.string())
1471
- }),
1472
- z3.object({
1473
- kind: z3.literal("count-mismatch"),
1474
- claimed: count,
1475
- actual: count,
1476
- unit: z3.string(),
1477
- countedFrom: z3.string(),
1478
- members: z3.array(z3.string())
1479
- }),
1480
- z3.object({
1481
- kind: z3.literal("missing-script"),
1482
- scriptName: z3.string(),
1483
- invocation: z3.string(),
1484
- manifestsChecked: z3.array(z3.string()),
1485
- availableScripts: z3.array(z3.string())
1486
- }),
1487
- z3.object({
1488
- kind: z3.literal("doc-behind-manifests"),
1489
- docLastCommit: commitSchema,
1490
- manifestCommits: z3.array(commitSchema.extend({ files: z3.array(z3.string()) })),
1491
- totalCommits: count
1492
- }),
1493
- z3.object({
1494
- kind: z3.literal("decision-anchor"),
1495
- decisionId: z3.string(),
1496
- title: z3.string(),
1497
- changedFiles: z3.array(z3.string()),
1498
- refreshedHash: z3.string(),
1499
- provenance: z3.object({}).passthrough().optional()
1500
- })
1501
- ]);
1502
- var findingSchema = z3.object({ message: z3.string(), anchor: anchorSchema, evidence: evidenceSchema });
1503
- var issueSchema = findingSchema.extend({
1504
- type: z3.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
1505
- confidence: z3.enum(["certain", "likely"])
1506
- });
1507
- var advisorySchema = findingSchema.extend({ type: z3.enum(["deps-changed", "decision-anchor-drift"]) });
1508
- var checkResultSchema = z3.object({
1509
- issues: z3.array(issueSchema),
1510
- advisories: z3.array(advisorySchema),
1511
- suppressedAdvisories: z3.array(advisorySchema).optional(),
1512
- skipped: z3.array(z3.object({ check: z3.string(), reason: z3.string(), doc: z3.string().optional() }))
1513
- });
1514
- var reportSchema = z3.object({
1515
- version: z3.literal(1),
1516
- root: z3.string(),
1517
- gitAvailable: z3.literal(true),
1518
- headHash: commitSchema.shape.hash,
1519
- checksRun: z3.array(checkSchema).nonempty(),
1520
- docs: z3.array(z3.object({
1521
- path: z3.enum(DOC_CANDIDATES),
1522
- lastCommit: commitSchema.nullable(),
1523
- dirty: z3.boolean(),
1524
- lineCount: count
1525
- })).nonempty(),
1526
- decisionsChecked: z3.boolean(),
1527
- clean: z3.boolean(),
1528
- issues: z3.array(issueSchema),
1529
- advisories: z3.array(advisorySchema),
1530
- suppressedAdvisories: z3.array(advisorySchema).optional(),
1531
- skippedChecks: z3.array(z3.object({ check: z3.string(), reason: z3.string(), doc: z3.string().optional() }))
1532
- });
1533
- var baselineSchema = z3.object({
1534
- kind: z3.literal("mason-audit-repair"),
1535
- version: z3.literal(1),
1536
- createdAt: z3.string().datetime(),
1537
- report: reportSchema,
1538
- digest: z3.string().regex(/^[a-f0-9]{64}$/)
1539
- });
1540
- var digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
2001
+ import fs10 from "fs/promises";
2002
+ import path16 from "path";
2003
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
2004
+ import { z as z4 } from "zod";
1541
2005
  function findingId(finding) {
1542
2006
  const e = finding.evidence;
1543
2007
  let key;
@@ -1591,7 +2055,7 @@ async function stableAudit(root, checks, options = {}) {
1591
2055
  }
1592
2056
  async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
1593
2057
  const root = await fs10.realpath(rootDir);
1594
- const selected = z3.array(checkSchema).nonempty().parse(checks);
2058
+ const selected = z4.array(checkSchema).nonempty().parse(checks);
1595
2059
  const report = await stableAudit(root, selected, options);
1596
2060
  if (!report) throw new Error("No context files found to prepare a repair.");
1597
2061
  if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
@@ -1602,7 +2066,7 @@ async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
1602
2066
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1603
2067
  report: storedReport
1604
2068
  };
1605
- const baselinePath = ".mason/reports/repairs/" + randomUUID2() + ".json";
2069
+ const baselinePath = ".mason/reports/repairs/" + randomUUID3() + ".json";
1606
2070
  await writeStoreJson(root, baselinePath, { ...payload, digest: digest(payload) });
1607
2071
  return { version: 1, action: "prepare", baselinePath, report };
1608
2072
  }
@@ -1682,27 +2146,135 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
1682
2146
  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."
1683
2147
  };
1684
2148
  }
2149
+ var checkSchema, commitSchema, anchorSchema, count, evidenceSchema, findingSchema, issueSchema, advisorySchema, checkResultSchema, reportSchema, baselineSchema, digest;
2150
+ var init_repair = __esm({
2151
+ "src/audit/repair.ts"() {
2152
+ "use strict";
2153
+ init_audit();
2154
+ init_docs();
2155
+ init_snapshot();
2156
+ init_drift();
2157
+ init_storage();
2158
+ init_files();
2159
+ init_paths();
2160
+ init_types();
2161
+ checkSchema = z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
2162
+ commitSchema = z4.object({ hash: z4.string().regex(/^[a-f0-9]{40,64}$/), date: z4.string(), subject: z4.string() });
2163
+ anchorSchema = z4.object({ doc: z4.string(), line: z4.number().int().positive().nullable(), excerpt: z4.string().nullable() });
2164
+ count = z4.number().int().nonnegative();
2165
+ evidenceSchema = z4.discriminatedUnion("kind", [
2166
+ z4.object({
2167
+ kind: z4.literal("missing-path"),
2168
+ claimed: z4.string(),
2169
+ renamedTo: z4.string().nullable(),
2170
+ deletedInCommit: commitSchema.nullable(),
2171
+ everTracked: z4.boolean(),
2172
+ parentDirExists: z4.boolean()
2173
+ }),
2174
+ z4.object({
2175
+ kind: z4.literal("unmentioned-dir"),
2176
+ dir: z4.string(),
2177
+ sourceFileCount: count,
2178
+ firstCommit: commitSchema.nullable(),
2179
+ checkedDocs: z4.array(z4.string())
2180
+ }),
2181
+ z4.object({
2182
+ kind: z4.literal("count-mismatch"),
2183
+ claimed: count,
2184
+ actual: count,
2185
+ unit: z4.string(),
2186
+ countedFrom: z4.string(),
2187
+ members: z4.array(z4.string())
2188
+ }),
2189
+ z4.object({
2190
+ kind: z4.literal("missing-script"),
2191
+ scriptName: z4.string(),
2192
+ invocation: z4.string(),
2193
+ manifestsChecked: z4.array(z4.string()),
2194
+ availableScripts: z4.array(z4.string())
2195
+ }),
2196
+ z4.object({
2197
+ kind: z4.literal("doc-behind-manifests"),
2198
+ docLastCommit: commitSchema,
2199
+ manifestCommits: z4.array(commitSchema.extend({ files: z4.array(z4.string()) })),
2200
+ totalCommits: count
2201
+ }),
2202
+ z4.object({
2203
+ kind: z4.literal("decision-anchor"),
2204
+ decisionId: z4.string(),
2205
+ title: z4.string(),
2206
+ changedFiles: z4.array(z4.string()),
2207
+ refreshedHash: z4.string(),
2208
+ provenance: z4.object({}).passthrough().optional()
2209
+ })
2210
+ ]);
2211
+ findingSchema = z4.object({ message: z4.string(), anchor: anchorSchema, evidence: evidenceSchema });
2212
+ issueSchema = findingSchema.extend({
2213
+ type: z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
2214
+ confidence: z4.enum(["certain", "likely"])
2215
+ });
2216
+ advisorySchema = findingSchema.extend({ type: z4.enum(["deps-changed", "decision-anchor-drift"]) });
2217
+ checkResultSchema = z4.object({
2218
+ issues: z4.array(issueSchema),
2219
+ advisories: z4.array(advisorySchema),
2220
+ suppressedAdvisories: z4.array(advisorySchema).optional(),
2221
+ skipped: z4.array(z4.object({ check: z4.string(), reason: z4.string(), doc: z4.string().optional() }))
2222
+ });
2223
+ reportSchema = z4.object({
2224
+ version: z4.literal(1),
2225
+ root: z4.string(),
2226
+ gitAvailable: z4.literal(true),
2227
+ headHash: commitSchema.shape.hash,
2228
+ checksRun: z4.array(checkSchema).nonempty(),
2229
+ docs: z4.array(z4.object({
2230
+ path: z4.enum(DOC_CANDIDATES),
2231
+ lastCommit: commitSchema.nullable(),
2232
+ dirty: z4.boolean(),
2233
+ lineCount: count
2234
+ })).nonempty(),
2235
+ decisionsChecked: z4.boolean(),
2236
+ clean: z4.boolean(),
2237
+ issues: z4.array(issueSchema),
2238
+ advisories: z4.array(advisorySchema),
2239
+ suppressedAdvisories: z4.array(advisorySchema).optional(),
2240
+ skippedChecks: z4.array(z4.object({ check: z4.string(), reason: z4.string(), doc: z4.string().optional() }))
2241
+ });
2242
+ baselineSchema = z4.object({
2243
+ kind: z4.literal("mason-audit-repair"),
2244
+ version: z4.literal(1),
2245
+ createdAt: z4.string().datetime(),
2246
+ report: reportSchema,
2247
+ digest: z4.string().regex(/^[a-f0-9]{64}$/)
2248
+ });
2249
+ digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
2250
+ }
2251
+ });
1685
2252
 
1686
2253
  // src/automation/evidence.ts
2254
+ var evidence_exports = {};
2255
+ __export(evidence_exports, {
2256
+ checkCache: () => checkCache,
2257
+ git: () => git2,
2258
+ hash: () => hash,
2259
+ readInputs: () => readInputs,
2260
+ workspace: () => workspace
2261
+ });
1687
2262
  import fs11 from "fs/promises";
1688
2263
  import path17 from "path";
1689
2264
  import { createHash as createHash3 } from "crypto";
1690
- import { execFile as execFile6 } from "child_process";
1691
- import { promisify as promisify6 } from "util";
2265
+ import { execFile as execFile7 } from "child_process";
2266
+ import { promisify as promisify7 } from "util";
1692
2267
  import fg5 from "fast-glob";
1693
- import { z as z4 } from "zod";
1694
- var exec6 = promisify6(execFile6);
1695
- var engineVersion = true ? "0.12.0" : "development";
1696
- var hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
1697
- async function git(root, ...args) {
1698
- return (await exec6("git", args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout;
2268
+ import { z as z5 } from "zod";
2269
+ async function git2(root, ...args) {
2270
+ return (await exec7("git", args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout;
1699
2271
  }
1700
2272
  async function workspace(dir) {
1701
- const root = await fs11.realpath((await git(dir, "rev-parse", "--show-toplevel")).trim());
1702
- const gitDir = await fs11.realpath((await git(root, "rev-parse", "--absolute-git-dir")).trim());
2273
+ const root = await fs11.realpath((await git2(dir, "rev-parse", "--show-toplevel")).trim());
2274
+ const gitDir = await fs11.realpath((await git2(root, "rev-parse", "--absolute-git-dir")).trim());
1703
2275
  let branch;
1704
2276
  try {
1705
- branch = (await git(root, "symbolic-ref", "--quiet", "HEAD")).trim();
2277
+ branch = (await git2(root, "symbolic-ref", "--quiet", "HEAD")).trim();
1706
2278
  } catch {
1707
2279
  branch = "detached";
1708
2280
  }
@@ -1718,23 +2290,20 @@ async function content(root, file) {
1718
2290
  throw error;
1719
2291
  }
1720
2292
  }
1721
- var manifest = /(^|\/)(package\.json|pnpm-workspace\.yaml|Cargo\.toml|settings\.gradle(?:\.kts)?|build\.gradle(?:\.kts)?|libs\.versions\.toml|go\.mod|pyproject\.toml|requirements\.txt|Gemfile|composer\.json)$/;
1722
- var internal = (file) => file === ".mason" || file === ".mason/reports" || file.startsWith(".mason/reports/");
1723
2293
  async function readInputs(root) {
1724
- const [headText, status, inventory, index, shallowPath, replacements] = await Promise.all([
1725
- git(root, "rev-parse", "HEAD"),
1726
- git(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
2294
+ const [headText, docStatus, inventory, shallowPath, replacements] = await Promise.all([
2295
+ git2(root, "rev-parse", "HEAD"),
2296
+ git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...DOC_CANDIDATES),
1727
2297
  fg5("**/*", {
1728
2298
  cwd: root,
1729
2299
  dot: true,
1730
2300
  onlyFiles: false,
1731
2301
  followSymbolicLinks: false,
1732
2302
  objectMode: true,
1733
- ignore: ["**/.git/**", "**/node_modules/**", ".mason/reports/**"]
2303
+ ignore: SOURCE_IGNORE
1734
2304
  }),
1735
- git(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports"),
1736
- git(root, "rev-parse", "--git-path", "shallow"),
1737
- git(root, "for-each-ref", "--format=%(refname) %(objectname)", "refs/replace")
2305
+ git2(root, "rev-parse", "--git-path", "shallow"),
2306
+ git2(root, "for-each-ref", "--format=%(refname) %(objectname)", "refs/replace")
1738
2307
  ]);
1739
2308
  const entries = inventory.filter((f) => !internal(f.path) && f.path !== ".git").sort((a, b) => a.path.localeCompare(b.path));
1740
2309
  const files = entries.map((f) => f.path);
@@ -1759,28 +2328,37 @@ async function readInputs(root) {
1759
2328
  claims.push([claim.path, await exists2(claim.path), await exists2(path17.dirname(claim.path))]);
1760
2329
  }
1761
2330
  }
1762
- const metadata = [];
1763
- for (const file of files) {
1764
- if (manifest.test(file) || file.startsWith(".mason/decisions/") && file.endsWith(".json") || file === ".mason/config.json") {
1765
- metadata.push([file, await content(root, file)]);
1766
- }
1767
- }
1768
2331
  if (entries.some((f) => f.dirent.isSymbolicLink())) throw new Error("Automation inventory contains a symbolic link; use an explicit audit to inspect its scope. No cached verification was recorded.");
1769
- const common = [1, engineVersion, head, shallow, replacements, docContents];
1770
- const layout = hash(entries.map((f) => [f.path, f.dirent.isDirectory() ? "directory" : "file"]));
1771
- const manifests = hash(metadata.filter(([file]) => manifest.test(file)));
1772
- const decisions = hash(metadata.filter(([file]) => !manifest.test(file)));
2332
+ const combinedDocs = docContents.map(([, text2]) => text2 ?? "").join("\n");
2333
+ const countClaims = docContents.flatMap(([, text2]) => text2 ? extractClaims(text2).counts : []);
2334
+ const decisionDirectory = await storePath(root, ".mason/decisions");
2335
+ const decisionPresence = await fs11.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
2336
+ if (error.code === "ENOENT") return "absent";
2337
+ throw error;
2338
+ });
2339
+ const [modules, counts, manifests, decisionFiles] = await Promise.all([
2340
+ combinedDocs ? moduleCandidates(root, combinedDocs) : [],
2341
+ Promise.all(countClaims.map((claim) => resolveCountSource(root, claim))),
2342
+ commandManifests(root),
2343
+ fg5(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
2344
+ ]);
2345
+ const packages = await Promise.all(["package.json", ...manifests.sort()].map(async (file) => [file, await content(root, file)]));
2346
+ const decisions = await Promise.all(decisionFiles.sort().map(async (file) => [file, await content(root, file)]));
2347
+ const [status, index2] = decisions.length ? await Promise.all([
2348
+ git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
2349
+ git2(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
2350
+ ]) : ["", ""];
2351
+ const common = [2, engineVersion, head, shallow, replacements, docContents];
1773
2352
  const keys = {
1774
- "deleted-reference": hash([common, layout, claims]),
1775
- "new-module": hash([common, layout]),
1776
- "stale-count": hash([common, layout, manifests]),
1777
- "dead-command": hash([common, layout, manifests]),
1778
- "deps-changed": hash([common, status]),
1779
- "decision-anchor-drift": hash([common, layout, decisions, status, index])
2353
+ "deleted-reference": hash([common, claims, docStatus]),
2354
+ "new-module": hash([common, modules]),
2355
+ "stale-count": hash([common, counts]),
2356
+ "dead-command": hash([common, packages]),
2357
+ "deps-changed": hash([common, docStatus]),
2358
+ "decision-anchor-drift": hash([common, decisionPresence, decisions, status, index2])
1780
2359
  };
1781
2360
  return { fingerprint: hash(keys), head, docs, keys };
1782
2361
  }
1783
- var cacheSchema = z4.object({ version: z4.literal(1), entries: z4.record(z4.object({ key: z4.string(), result: checkResultSchema })), digest: z4.string() });
1784
2362
  function checkCache(raw, inputs) {
1785
2363
  let entries = {};
1786
2364
  let diagnostic = null;
@@ -1806,34 +2384,38 @@ function checkCache(raw, inputs) {
1806
2384
  return { version: 1, entries: canonical, digest: hash(canonical) };
1807
2385
  } };
1808
2386
  }
2387
+ var exec7, engineVersion, hash, internal, cacheSchema;
2388
+ var init_evidence = __esm({
2389
+ "src/automation/evidence.ts"() {
2390
+ "use strict";
2391
+ init_docs();
2392
+ init_claims();
2393
+ init_repair();
2394
+ init_checks();
2395
+ init_files();
2396
+ init_new_module();
2397
+ init_stale_count();
2398
+ init_dead_command();
2399
+ init_storage();
2400
+ exec7 = promisify7(execFile7);
2401
+ engineVersion = true ? "0.13.0" : "development";
2402
+ hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
2403
+ internal = (file) => file === ".mason" || file === ".mason/reports" || file.startsWith(".mason/reports/");
2404
+ cacheSchema = z5.object({ version: z5.literal(1), entries: z5.record(z5.object({ key: z5.string(), result: checkResultSchema })), digest: z5.string() });
2405
+ }
2406
+ });
1809
2407
 
1810
2408
  // src/automation/store.ts
1811
2409
  import fs12 from "fs/promises";
1812
- import os from "os";
1813
- import { z as z5 } from "zod";
1814
- var hostSchema = z5.enum(["claude", "codex"]);
1815
- var stateSchema = z5.object({
1816
- version: z5.literal(1),
1817
- root: z5.string(),
1818
- gitDir: z5.string(),
1819
- branch: z5.string(),
1820
- baselines: z5.array(z5.object({ path: z5.string(), at: z5.string(), event: z5.string(), fingerprint: z5.string() })).max(128),
1821
- sessions: z5.record(z5.object({
1822
- host: hostSchema,
1823
- seen: z5.string().nullable(),
1824
- continued: z5.boolean(),
1825
- initialIssues: z5.array(z5.string()),
1826
- initialDocs: z5.record(z5.string().nullable()),
1827
- lastUsed: z5.string(),
1828
- mutationObserved: z5.boolean(),
1829
- pending: z5.record(z5.string()),
1830
- coverageGaps: z5.array(z5.string()),
1831
- events: z5.record(z5.object({ at: z5.string(), count: z5.number().int().positive() }))
1832
- })),
1833
- updatedAt: z5.string(),
1834
- fingerprint: z5.string().nullable(),
1835
- latest: z5.string().nullable()
1836
- });
2410
+ import os2 from "os";
2411
+ import { z as z6 } from "zod";
2412
+ function parseState(raw) {
2413
+ try {
2414
+ return stateSchema.parse(raw);
2415
+ } catch (error) {
2416
+ throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
2417
+ }
2418
+ }
1837
2419
  async function withLock(root, directory, run) {
1838
2420
  const file = await storePath(root, directory + "/lock", true);
1839
2421
  const deadline = Date.now() + 5e3;
@@ -1841,12 +2423,21 @@ async function withLock(root, directory, run) {
1841
2423
  while (!handle) {
1842
2424
  try {
1843
2425
  handle = await fs12.open(file, "wx", 384);
1844
- await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
2426
+ try {
2427
+ await handle.writeFile(JSON.stringify({ pid: process.pid, host: os2.hostname() }));
2428
+ } catch (error) {
2429
+ await handle.close().catch(() => {
2430
+ });
2431
+ handle = void 0;
2432
+ await fs12.rm(file, { force: true }).catch(() => {
2433
+ });
2434
+ throw error;
2435
+ }
1845
2436
  } catch (error) {
1846
2437
  if (error.code !== "EEXIST") throw error;
1847
2438
  try {
1848
2439
  const owner = JSON.parse(await fs12.readFile(file, "utf8"));
1849
- if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
2440
+ if (owner.host === os2.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
1850
2441
  try {
1851
2442
  process.kill(owner.pid, 0);
1852
2443
  } catch (probe) {
@@ -1879,11 +2470,40 @@ async function withLock(root, directory, run) {
1879
2470
  await fs12.unlink(file);
1880
2471
  }
1881
2472
  }
2473
+ var hostSchema, events, stateSchema;
2474
+ var init_store = __esm({
2475
+ "src/automation/store.ts"() {
2476
+ "use strict";
2477
+ init_storage();
2478
+ hostSchema = z6.enum(["claude", "codex"]);
2479
+ events = ["session_start", "turn_start", "before_tool", "after_tool", "task_end"];
2480
+ stateSchema = z6.object({
2481
+ version: z6.literal(1),
2482
+ root: z6.string(),
2483
+ gitDir: z6.string(),
2484
+ branch: z6.string(),
2485
+ baselines: z6.array(z6.object({ path: z6.string(), at: z6.string(), event: z6.string(), fingerprint: z6.string() })).max(128),
2486
+ sessions: z6.record(z6.object({
2487
+ host: hostSchema,
2488
+ seen: z6.string().nullable(),
2489
+ continued: z6.boolean(),
2490
+ initialIssues: z6.array(z6.string()),
2491
+ initialDocs: z6.record(z6.string().nullable()),
2492
+ lastUsed: z6.string(),
2493
+ mutationObserved: z6.boolean(),
2494
+ pending: z6.record(z6.string()),
2495
+ coverageGaps: z6.array(z6.string()),
2496
+ events: z6.record(z6.object({ at: z6.string(), count: z6.number().int().positive() }))
2497
+ })),
2498
+ updatedAt: z6.string(),
2499
+ fingerprint: z6.string().nullable(),
2500
+ latest: z6.string().nullable()
2501
+ });
2502
+ }
2503
+ });
1882
2504
 
1883
2505
  // src/automation/runtime.ts
1884
- var 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.";
1885
- var priority = { resolved: 0, "review-required": 1, unresolved: 2, unverified: 3 };
1886
- var cleanText = (text2) => text2.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 250);
2506
+ import { randomUUID as randomUUID4 } from "crypto";
1887
2507
  function summarize(report) {
1888
2508
  const open = report.findings.filter((f) => f.status !== "resolved");
1889
2509
  return [
@@ -1897,7 +2517,7 @@ function summarize(report) {
1897
2517
  }
1898
2518
  async function automate(dir, event) {
1899
2519
  const ws = await workspace(dir);
1900
- return withLock(ws.root, ws.directory, async () => {
2520
+ return withLock(ws.root, ws.directory, () => recordExecution(ws.root, ws.directory, event.event, async () => {
1901
2521
  const inputs = await readInputs(ws.root);
1902
2522
  const statePath = ws.directory + "/state.json";
1903
2523
  const raw = await readStoreJson(ws.root, statePath);
@@ -1912,7 +2532,7 @@ async function automate(dir, event) {
1912
2532
  updatedAt: now,
1913
2533
  fingerprint: null,
1914
2534
  latest: null
1915
- } : stateSchema.parse(raw);
2535
+ } : parseState(raw);
1916
2536
  if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
1917
2537
  throw new Error("Automation state belongs to another branch or worktree; original evidence was retained.");
1918
2538
  }
@@ -1920,7 +2540,7 @@ async function automate(dir, event) {
1920
2540
  const previous = await readStoreJson(ws.root, state.latest);
1921
2541
  if (!previous?.head || !/^[a-f0-9]{40,64}$/.test(previous.head)) throw new Error("The previous detached checkout evidence is unavailable.");
1922
2542
  try {
1923
- await git(ws.root, "merge-base", "--is-ancestor", previous.head, inputs.head);
2543
+ await git2(ws.root, "merge-base", "--is-ancestor", previous.head, inputs.head);
1924
2544
  } catch {
1925
2545
  throw new Error("Detached checkout moved to a different history; original repair evidence was retained. Inspect that baseline explicitly.");
1926
2546
  }
@@ -1968,7 +2588,7 @@ async function automate(dir, event) {
1968
2588
  branch: ws.branch,
1969
2589
  head: inputs.head,
1970
2590
  baselinePaths: [],
1971
- reportPath: ws.directory + "/checks/" + randomUUID3() + ".json",
2591
+ reportPath: ws.directory + "/checks/" + randomUUID4() + ".json",
1972
2592
  findings: [],
1973
2593
  diagnostics: ["No AGENTS.md, CLAUDE.md, or .claude/CLAUDE.md exists. Documentation capture is unavailable; other Mason tools remain usable."],
1974
2594
  checks: { ran: [], reused: [], skipped: [] },
@@ -2033,7 +2653,7 @@ async function automate(dir, event) {
2033
2653
  branch: ws.branch,
2034
2654
  head: inputs.head,
2035
2655
  baselinePaths: state.baselines.map((b) => b.path),
2036
- reportPath: ws.directory + "/checks/" + randomUUID3() + ".json",
2656
+ reportPath: ws.directory + "/checks/" + randomUUID4() + ".json",
2037
2657
  findings: [...merged.values()],
2038
2658
  diagnostics: [...new Set(diagnostics)],
2039
2659
  checks: { ran: [...cache.ran], reused: [...cache.reused].filter((name) => !cache.ran.has(name)), skipped: current?.skippedChecks ?? [] },
@@ -2055,16 +2675,17 @@ async function automate(dir, event) {
2055
2675
  state.fingerprint = inputs.fingerprint;
2056
2676
  state.latest = report.reportPath;
2057
2677
  if (persistReport) await writeStoreJson(ws.root, report.reportPath, report);
2058
- await writeStoreJson(ws.root, ws.directory + "/cache.json", cache.serialize());
2678
+ if (cache.ran.size || cached === null || cache.diagnostic) await writeStoreJson(ws.root, ws.directory + "/cache.json", cache.serialize());
2059
2679
  await writeStoreJson(ws.root, statePath, state);
2060
2680
  return { report, message: notify ? summarize(report) : null, continueOnce };
2061
- });
2681
+ }));
2062
2682
  }
2063
2683
  async function automationStatus(dir) {
2064
2684
  const ws = await workspace(dir);
2685
+ const execution = await executionStatus(ws.root, ws.directory);
2065
2686
  const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
2066
- if (raw === null) return { version: 1, status: "not-observed", root: ws.root, branch: ws.branch, baselinePaths: [], hosts: {} };
2067
- const state = stateSchema.parse(raw);
2687
+ if (raw === null) return { version: 1, status: execution.status === "not-observed" ? "not-observed" : "unavailable", root: ws.root, branch: ws.branch, baselinePaths: [], hosts: {}, execution };
2688
+ const state = parseState(raw);
2068
2689
  if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) throw new Error("Automation state belongs to another workspace.");
2069
2690
  const inputs = await readInputs(ws.root);
2070
2691
  const latest = state.latest ? await readStoreJson(ws.root, state.latest) : null;
@@ -2074,38 +2695,174 @@ async function automationStatus(dir) {
2074
2695
  host.sessions++;
2075
2696
  host.observedEvents = [.../* @__PURE__ */ new Set([...host.observedEvents, ...Object.keys(session.events)])];
2076
2697
  }
2698
+ const unfinished = ["failed", "unknown", "running"].includes(execution.status);
2077
2699
  return {
2078
2700
  version: 1,
2079
- status: inputs.fingerprint === state.fingerprint ? "current" : "changed",
2701
+ status: unfinished ? "unavailable" : inputs.fingerprint === state.fingerprint ? "current" : "changed",
2080
2702
  root: ws.root,
2081
2703
  branch: ws.branch,
2082
2704
  baselinePaths: state.baselines.map((b) => b.path),
2083
2705
  reportPath: state.latest,
2084
- verificationStatus: latest?.status ?? "unavailable",
2706
+ verificationStatus: unfinished ? "unavailable" : latest?.status ?? "unavailable",
2085
2707
  hosts,
2708
+ execution,
2086
2709
  note: "Observed events do not prove all tool paths are intercepted. Run check to verify the retained evidence."
2087
2710
  };
2088
2711
  }
2712
+ var SCOPE, priority, cleanText;
2713
+ var init_runtime = __esm({
2714
+ "src/automation/runtime.ts"() {
2715
+ "use strict";
2716
+ init_execution();
2717
+ init_repair();
2718
+ init_types();
2719
+ init_storage();
2720
+ init_evidence();
2721
+ init_store();
2722
+ 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.";
2723
+ priority = { resolved: 0, "review-required": 1, unresolved: 2, unverified: 3 };
2724
+ cleanText = (text2) => text2.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 250);
2725
+ }
2726
+ });
2089
2727
 
2090
- // src/automation/adapters.ts
2091
- import { z as z6 } from "zod";
2092
- var inputSchema = z6.object({
2093
- cwd: z6.string().min(1),
2094
- session_id: z6.string().min(1).max(500),
2095
- hook_event_name: z6.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
2096
- tool_name: z6.string().optional(),
2097
- tool_use_id: z6.string().max(500).optional(),
2098
- tool_input: z6.unknown().optional(),
2099
- stop_hook_active: z6.boolean().optional(),
2100
- permission_mode: z6.string().optional()
2728
+ // src/setup/model.ts
2729
+ import { z as z7 } from "zod";
2730
+ async function loadSetup(root) {
2731
+ const raw = await readStoreJson(root, ".mason/setup.json");
2732
+ if (raw === null) return null;
2733
+ return setupSchema.parse(raw);
2734
+ }
2735
+ async function loadSetupReceipt(root, directory, host) {
2736
+ const raw = await readStoreJson(root, directory + "/setup-" + host + ".json");
2737
+ if (raw === null) return null;
2738
+ const receipt = receiptSchema.parse(raw);
2739
+ if (receipt.root !== root || receipt.host !== host) throw new Error("Setup receipt belongs to another installation.");
2740
+ return receipt;
2741
+ }
2742
+ var runtimeSchema, setupHostSchema, setupSchema, receiptSchema;
2743
+ var init_model = __esm({
2744
+ "src/setup/model.ts"() {
2745
+ "use strict";
2746
+ init_storage();
2747
+ runtimeSchema = z7.object({
2748
+ id: z7.string().regex(/^[a-f0-9]{24}$/),
2749
+ version: z7.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
2750
+ hashes: z7.record(z7.string().regex(/^[a-f0-9]{64}$/))
2751
+ });
2752
+ setupHostSchema = z7.object({
2753
+ runtime: runtimeSchema,
2754
+ revision: z7.string().uuid(),
2755
+ fingerprint: z7.string(),
2756
+ mcpFingerprint: z7.string(),
2757
+ instructions: z7.array(z7.string())
2758
+ });
2759
+ setupSchema = z7.object({ version: z7.literal(1), hosts: z7.object({
2760
+ codex: setupHostSchema.optional(),
2761
+ claude: setupHostSchema.optional()
2762
+ }) });
2763
+ receiptSchema = z7.object({
2764
+ version: z7.literal(1),
2765
+ host: z7.enum(["codex", "claude"]),
2766
+ status: z7.enum(["installing", "configured"]),
2767
+ initialReportPath: z7.string(),
2768
+ initialBaselinePaths: z7.array(z7.string()),
2769
+ root: z7.string(),
2770
+ revision: z7.string().uuid(),
2771
+ configuredAt: z7.string().optional()
2772
+ });
2773
+ }
2101
2774
  });
2102
- var lifecycle = {
2103
- SessionStart: "session_start",
2104
- UserPromptSubmit: "turn_start",
2105
- PreToolUse: "before_tool",
2106
- PostToolUse: "after_tool",
2107
- Stop: "task_end"
2108
- };
2775
+
2776
+ // src/setup/observations.ts
2777
+ var observations_exports = {};
2778
+ __export(observations_exports, {
2779
+ observationPath: () => observationPath,
2780
+ observeActivation: () => observeActivation,
2781
+ readObservation: () => readObservation
2782
+ });
2783
+ import fs13 from "fs/promises";
2784
+ import { z as z8 } from "zod";
2785
+ function observationPath(directory, host, revision) {
2786
+ return `${directory}/activation/${host}-${revision}.json`;
2787
+ }
2788
+ async function readObservation(root, directory, host, revision) {
2789
+ const raw = await readStoreJson(root, observationPath(directory, host, revision));
2790
+ if (raw === null) return null;
2791
+ const record = observationSchema.parse(raw);
2792
+ if (record.root !== root || record.host !== host || record.revision !== revision) throw new Error("Activation receipt belongs to another installation.");
2793
+ return record;
2794
+ }
2795
+ async function observeActivation(dir, event, options = {}) {
2796
+ const host = process.env.MASON_SETUP_HOST, revision = process.env.MASON_SETUP_REVISION;
2797
+ if (host !== "codex" && host !== "claude" || !revision || !process.env.MASON_SETUP_ROOT) return null;
2798
+ try {
2799
+ const capturedDirectory = options.reportPath?.match(/^(\.mason\/reports\/automation\/[a-f0-9]{24})\/checks\//)?.[1];
2800
+ const ws = capturedDirectory ? { root: await fs13.realpath(dir), directory: capturedDirectory } : await workspace(dir);
2801
+ if (ws.root !== await fs13.realpath(process.env.MASON_SETUP_ROOT)) return null;
2802
+ const setup = await loadSetup(ws.root);
2803
+ if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
2804
+ const directory = ws.directory + "/activation";
2805
+ await withLock(ws.root, directory, async () => {
2806
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2807
+ const record = await readObservation(ws.root, ws.directory, host, revision) ?? {
2808
+ version: 1,
2809
+ root: ws.root,
2810
+ revision,
2811
+ host,
2812
+ contextCalls: 0,
2813
+ sessions: {}
2814
+ };
2815
+ if (event === "context") {
2816
+ record.contextCalls++;
2817
+ record.lastContextAt = now;
2818
+ } else if (options.sessionId) {
2819
+ const key = hash(options.sessionId);
2820
+ const session = record.sessions[key] ?? { events: [], at: now };
2821
+ if (event !== "task_end" && session.events.includes(event)) return;
2822
+ session.events = [.../* @__PURE__ */ new Set([...session.events, event])];
2823
+ session.at = now;
2824
+ if (event === "task_end") {
2825
+ session.verificationStatus = options.verificationStatus;
2826
+ session.reportPath = options.reportPath;
2827
+ }
2828
+ record.sessions[key] = session;
2829
+ const ordered = Object.entries(record.sessions).sort(([, a], [, b]) => b.at.localeCompare(a.at));
2830
+ record.sessions = Object.fromEntries(ordered.slice(0, 32));
2831
+ } else return;
2832
+ await writeStoreJson(ws.root, observationPath(ws.directory, host, revision), record);
2833
+ });
2834
+ return null;
2835
+ } catch (error) {
2836
+ return "Mason activation observation could not be saved; activation coverage is unknown. " + (error instanceof Error ? error.message : String(error)).replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, 300);
2837
+ }
2838
+ }
2839
+ var observationSchema;
2840
+ var init_observations = __esm({
2841
+ "src/setup/observations.ts"() {
2842
+ "use strict";
2843
+ init_evidence();
2844
+ init_store();
2845
+ init_storage();
2846
+ init_model();
2847
+ observationSchema = z8.object({
2848
+ version: z8.literal(1),
2849
+ root: z8.string(),
2850
+ revision: z8.string(),
2851
+ host: z8.enum(["codex", "claude"]),
2852
+ contextCalls: z8.number().int().nonnegative(),
2853
+ lastContextAt: z8.string().optional(),
2854
+ sessions: z8.record(z8.object({
2855
+ events: z8.array(z8.enum(events)),
2856
+ at: z8.string(),
2857
+ verificationStatus: z8.string().optional(),
2858
+ reportPath: z8.string().optional()
2859
+ }))
2860
+ });
2861
+ }
2862
+ });
2863
+
2864
+ // src/automation/adapters.ts
2865
+ import { z as z9 } from "zod";
2109
2866
  function normalizeHook(host, raw) {
2110
2867
  hostSchema.parse(host);
2111
2868
  const input2 = inputSchema.parse(raw);
@@ -2126,17 +2883,23 @@ async function runAutomationHook(host, stdin) {
2126
2883
  const input2 = normalizeHook(host, JSON.parse(stdin));
2127
2884
  name = input2.name;
2128
2885
  const result = await automate(input2.cwd, input2.event);
2886
+ const { observeActivation: observeActivation2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
2887
+ const warning = await observeActivation2(result.report.root, input2.event.event, {
2888
+ sessionId: input2.event.sessionId,
2889
+ verificationStatus: result.report.status,
2890
+ reportPath: result.report.reportPath
2891
+ });
2892
+ if (warning) result.message = [result.message, warning].filter(Boolean).join("\n");
2129
2893
  if (!result.message) return null;
2130
2894
  if (name === "Stop") {
2131
2895
  return result.continueOnce ? { decision: "block", reason: result.message } : { systemMessage: result.message };
2132
2896
  }
2133
2897
  return { hookSpecificOutput: { hookEventName: name, additionalContext: result.message } };
2134
2898
  } catch (error) {
2135
- const message = "Mason automation unavailable; evidence capture/verification was not established. " + (error instanceof Error ? error.message : String(error)).replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 700);
2136
- return { systemMessage: message, ...["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse"].includes(name) ? { hookSpecificOutput: { hookEventName: name, additionalContext: message } } : {} };
2899
+ const message2 = failureMessage(error);
2900
+ return { systemMessage: message2, ...["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse"].includes(name) ? { hookSpecificOutput: { hookEventName: name, additionalContext: message2 } } : {} };
2137
2901
  }
2138
2902
  }
2139
- var HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"];
2140
2903
  function hookConfig(host, command = "npx --no-install --package mason-context mason-auto") {
2141
2904
  const handler = { type: "command", command: command + " hook --host " + host, timeout: 30 };
2142
2905
  return { hooks: Object.fromEntries(HOOK_EVENTS.map((name) => [
@@ -2144,20 +2907,51 @@ function hookConfig(host, command = "npx --no-install --package mason-context ma
2144
2907
  [{ ...["PreToolUse", "PostToolUse"].includes(name) ? { matcher: ".*" } : {}, hooks: [{ ...handler }] }]
2145
2908
  ])) };
2146
2909
  }
2910
+ var inputSchema, lifecycle, HOOK_EVENTS;
2911
+ var init_adapters = __esm({
2912
+ "src/automation/adapters.ts"() {
2913
+ "use strict";
2914
+ init_execution();
2915
+ init_runtime();
2916
+ init_store();
2917
+ inputSchema = z9.object({
2918
+ cwd: z9.string().min(1),
2919
+ session_id: z9.string().min(1).max(500),
2920
+ hook_event_name: z9.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
2921
+ tool_name: z9.string().optional(),
2922
+ tool_use_id: z9.string().max(500).optional(),
2923
+ tool_input: z9.unknown().optional(),
2924
+ stop_hook_active: z9.boolean().optional(),
2925
+ permission_mode: z9.string().optional()
2926
+ });
2927
+ lifecycle = {
2928
+ SessionStart: "session_start",
2929
+ UserPromptSubmit: "turn_start",
2930
+ PreToolUse: "before_tool",
2931
+ PostToolUse: "after_tool",
2932
+ Stop: "task_end"
2933
+ };
2934
+ HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"];
2935
+ }
2936
+ });
2147
2937
 
2148
2938
  // src/automation/install.ts
2149
- import { z as z7 } from "zod";
2150
- var groupSchema = z7.object({ hooks: z7.array(z7.object({ type: z7.string(), command: z7.string().optional() }).passthrough()) }).passthrough();
2151
- var configSchema = z7.object({ hooks: z7.record(z7.array(groupSchema)).optional() }).passthrough();
2152
- var recordSchema = z7.object({ version: z7.literal(1), hosts: z7.record(z7.object({ command: z7.string() })) });
2153
- var configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
2939
+ var install_exports = {};
2940
+ __export(install_exports, {
2941
+ automationConfigSchema: () => automationConfigSchema,
2942
+ configPath: () => configPath,
2943
+ installAutomation: () => installAutomation,
2944
+ installedAutomation: () => installedAutomation,
2945
+ planAutomationInstall: () => planAutomationInstall
2946
+ });
2947
+ import { z as z10 } from "zod";
2154
2948
  async function installAutomation(dir, host, command) {
2155
2949
  const ws = await workspace(dir);
2156
2950
  return withLock(ws.root, ".mason/reports/automation-install", () => installLocked(ws.root, host, command));
2157
2951
  }
2158
- async function installLocked(root, host, command) {
2952
+ async function planAutomationInstall(root, host, command) {
2159
2953
  const file = configPath(host);
2160
- const existing = configSchema.parse(await readStoreJson(root, file) ?? {});
2954
+ const existing = automationConfigSchema.parse(await readStoreJson(root, file) ?? {});
2161
2955
  const record = recordSchema.parse(await readStoreJson(root, ".mason/automation.json") ?? { version: 1, hosts: {} });
2162
2956
  const desired = hookConfig(host, command);
2163
2957
  const newCommand = desired.hooks.SessionStart[0].hooks[0].command;
@@ -2171,7 +2965,11 @@ async function installLocked(root, host, command) {
2171
2965
  hooks[event].push(...desired.hooks[event]);
2172
2966
  }
2173
2967
  record.hosts[host] = { command: newCommand };
2174
- await writeStoreJson(root, file, { ...existing, hooks });
2968
+ return { file, config: { ...existing, hooks }, record, newCommand };
2969
+ }
2970
+ async function installLocked(root, host, command) {
2971
+ const { file, config, record, newCommand } = await planAutomationInstall(root, host, command);
2972
+ await writeStoreJson(root, file, config);
2175
2973
  await writeStoreJson(root, ".mason/automation.json", record);
2176
2974
  return {
2177
2975
  version: 1,
@@ -2193,7 +2991,7 @@ async function installedAutomation(dir) {
2193
2991
  for (const host of ["claude", "codex"]) {
2194
2992
  const expected = record.hosts[host];
2195
2993
  if (!expected) continue;
2196
- const current = configSchema.parse(await readStoreJson(ws.root, configPath(host)) ?? {});
2994
+ const current = automationConfigSchema.parse(await readStoreJson(ws.root, configPath(host)) ?? {});
2197
2995
  const configuredEvents = HOOK_EVENTS.filter((event) => current.hooks?.[event]?.some((group) => group.hooks.some((handler) => handler.type === "command" && handler.command === expected.command)));
2198
2996
  result[host] = {
2199
2997
  configPath: configPath(host),
@@ -2204,10 +3002,1395 @@ async function installedAutomation(dir) {
2204
3002
  }
2205
3003
  return result;
2206
3004
  }
3005
+ var groupSchema, automationConfigSchema, recordSchema, configPath;
3006
+ var init_install = __esm({
3007
+ "src/automation/install.ts"() {
3008
+ "use strict";
3009
+ init_storage();
3010
+ init_evidence();
3011
+ init_adapters();
3012
+ init_store();
3013
+ groupSchema = z10.object({ hooks: z10.array(z10.object({ type: z10.string(), command: z10.string().optional() }).passthrough()) }).passthrough();
3014
+ automationConfigSchema = z10.object({ hooks: z10.record(z10.array(groupSchema)).optional() }).passthrough();
3015
+ recordSchema = z10.object({ version: z10.literal(1), hosts: z10.record(z10.object({ command: z10.string() })) });
3016
+ configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
3017
+ }
3018
+ });
3019
+
3020
+ // src/mcp/init.ts
3021
+ import { z as z11 } from "zod";
3022
+ async function loadProjectMarker(rootDir) {
3023
+ const raw = await readStoreJson(rootDir, ".mason/project.json");
3024
+ return raw === null ? null : markerSchema.parse(raw);
3025
+ }
3026
+ async function saveProjectMarker(rootDir, marker) {
3027
+ await writeStoreJson(rootDir, ".mason/project.json", markerSchema.parse(marker));
3028
+ }
3029
+ var markerSchema, CLAUDE_MD_SECTION, ASSISTANT_SETUP, QUICKSTART_PLAYBOOK, MAP_PLAYBOOK;
3030
+ var init_init = __esm({
3031
+ "src/mcp/init.ts"() {
3032
+ "use strict";
3033
+ init_storage();
3034
+ markerSchema = z11.object({
3035
+ version: z11.literal(1),
3036
+ initializedAt: z11.string(),
3037
+ features: z11.object({ confluence: z11.boolean().optional() }).optional()
3038
+ }).passthrough();
3039
+ CLAUDE_MD_SECTION = `<!-- mason:start -->
3040
+ ## Mason project knowledge
3041
+
3042
+ Mason provides recorded decisions and file impact over MCP. A concept map is optional.
3043
+
3044
+ - Task, bug, or change request \u2192 \`get_context\` with the task text and known files: matching decisions, related tests, impact, and any available map entries.
3045
+ - Before editing a file \u2192 \`get_impact\` to check references, tests, and historical change partners.
3046
+ - Learned something the code cannot explain (a failed approach, an incident's cause, a workaround's reason, a review-settled convention) \u2192 \`save_decision\` with rationale, anchors, and any known owner, sources, and recorder. It creates a proposal immediately without setup or a map. Never invent attribution or record code-derivable facts, session trivia, or secrets.
3047
+ - Consult trust metadata before relying on entries: unknown or changed freshness requires inspection, and failed verification means the description must be corrected. Check approval too: proposals are suggestions, legacy records are unreviewed, and accepted decisions are recorded constraints subject to freshness checks. An accepted revision remains operative while a pending proposal is reviewed; keep both versions and their freshness distinct.
3048
+ - Asked to review or re-verify a decision \u2192 \`review_decision\` first to inspect content, sources, history, and code changes. Record acceptance, reaffirmation, or retirement only when authorized by the user or a cited team review, with the actual reviewer and reason. Never infer approval from unchanged code. Review and commit the local record through the normal project workflow.
3049
+ - For an architectural overview, use \`get_snapshot\` if a map is available. If \`map.status\` is missing or invalid, use available decisions and source evidence; do not start building a map unless requested.
3050
+ - \`mason_init\` returns documentation audit and committed-diff review results, plus a short setup guide. Pass \`evidence\` with local CI manifest paths to include test and analysis results; the CLI equivalent is \`mason-review --evidence <manifest>\`. State skipped, unavailable, stale, or unknown checks explicitly. Related accepted decisions identify review context, not proven violations.
3051
+
3052
+ - When documentation repair is authorized, use \`mason_repair(action: "prepare")\` before edits, keep its baselinePath, and use \`mason_repair(action: "verify", baselinePath)\` after edits and any final doc commit. Report every original finding's outcome and any new findings. Suppressed advisories remain unresolved; editing a doc does not approve it.
3053
+
3054
+ - When Mason automation is installed, use \`mason_automation(action: "status")\` to inspect configured hooks and observed events, and \`mason_automation(action: "check")\` to resume its retained repair evidence. CLI fallback: \`mason-auto status\` / \`mason-auto check\`. Preserve existing baselines across sessions. Automatic checks do not authorize unrelated repairs or approve advisories.
3055
+
3056
+ Inspect source for what the retrieved context does not answer.
3057
+ <!-- mason:end -->`;
3058
+ ASSISTANT_SETUP = `UNIFIED SETUP
3059
+ When the user requests Mason setup, call mason_init again with mode: "setup" and host: "codex" or "claude" for the assistant being configured. This uses the same engine as mason-auto setup: retain the initial audit before instruction edits, install a pinned private runtime, merge project MCP configuration and hooks, update marker-delimited instructions and Git ignore rules, and record setup. A concept map is optional.
3060
+ If the user only requested inspection or review, report findings without running setup. The setup result distinguishes configured from observed activation. Explain the host's native trust step and request a new session; never trust hooks on the user's behalf or claim activation from generated configuration alone. After a normal task finishes, mason_automation(action: "status") or mason-auto status reports observed context use and hook events. Existing findings remain reviewable; setup does not approve advisories or manufacture decisions.
3061
+
3062
+ The managed project guidance is:
3063
+ ${CLAUDE_MD_SECTION}`;
3064
+ QUICKSTART_PLAYBOOK = `Start with the audit and review results included in this response. No concept map is required.
3065
+
3066
+ 1. Explain the actionable findings with their source evidence. Separate audit issues, advisories, and skipped checks. The review covers committed changes from the merge base to HEAD; workingTree paths are not included in that review. An unavailable or empty check is not proof that the project is correct. Use the CLI for full output if a summary is truncated.
3067
+ 2. Address findings within the user's requested scope. Setup alone authorizes the Mason runtime, host configuration, hooks, instructions, and ignore rules; rewriting existing project claims requires repair scope. If repair is authorized, call \`mason_repair(dir, action: "prepare")\` before editing; it saves the full original findings even when this summary is truncated. Inspect relevant source, make grounded edits, then call \`mason_repair(dir, action: "verify", baselinePath)\` with that same baseline, including after any final doc commit. Report resolved, unresolved, review-required, unverified, and new findings. Keep suppressed advisories visible even when setup has already dirtied a doc. Do not invent a decision just to populate the store.
3068
+ 3. When the task reveals a real lesson or constraint, call \`save_decision\` with title, body, category, anchors, and known owner/source/actor information. Missing attribution can be added later. The tool writes a local proposal; editing it preserves revision history and requires a new acceptance, while any earlier accepted revision remains operative. An unchanged save never refreshes its evidence. When decision review is requested, \`review_decision\` prepares the record and code evidence before any authorized verdict. Review and commit records through the normal workflow. Retrieve it on the next relevant task with \`get_context(dir, task, files)\`.
3069
+
3070
+ ${ASSISTANT_SETUP}
3071
+
3072
+ OPTIONAL ARCHITECTURE MAP
3073
+ A full map adds feature and flow navigation. Build it only if the user requests it, by calling \`mason_init(dir, mode: "map")\`. Decision capture and task context do not depend on that build.`;
3074
+ MAP_PLAYBOOK = `The user has requested a full architecture map. Follow this Map-Reduce workflow to cover the codebase. Report the audit and review findings included in this response before beginning.
3075
+
3076
+ PHASE 1 \u2014 Map (loop until done)
3077
+ Goal: process every file in the codebase, batch by batch, producing a partial concept map per batch.
3078
+
3079
+ 1. Call \`generate_snapshot_batch(dir)\` (omit offset on the first call).
3080
+ The response includes:
3081
+ - \`batchId\`: identifier for this batch
3082
+ - \`offset\`, \`nextOffset\`, \`totalFiles\`: progress markers
3083
+ - \`instructions\`: the system prompt for the batch step
3084
+ - \`prompt\`: the files in this batch (skeletons + a few deeper bodies)
3085
+ 2. Following the \`instructions\`, derive features and flows that involve ONLY the files in this batch. Use product-natural feature names ("home screen", not "HomeScreenAndroid") so the reduce step can merge platform variants.
3086
+ 3. Call \`save_partial_snapshot(dir, batchId, offset, features, flows)\` to persist the partial.
3087
+ 4. If \`nextOffset\` is null, the Map phase is done. Otherwise call \`generate_snapshot_batch(dir, offset=nextOffset)\` and repeat from step 2.
3088
+
3089
+ Briefly tell the user "Batch N of M done" each iteration so they see progress.
3090
+
3091
+ CRITICAL RULES FOR PHASE 1:
3092
+ - Derive features and file paths ONLY from what appears verbatim in the \`prompt\` field of each batch response. NEVER invent paths from memory, prior projects, or what you assume a project of this kind would contain. If you have not seen a path in a batch \`prompt\`, do not put it in \`features.files\` or \`flows.chain\`.
3093
+ - Process batches SEQUENTIALLY: one \`generate_snapshot_batch\` \u2192 derive \u2192 one \`save_partial_snapshot\` \u2192 next \`generate_snapshot_batch\`. Do not parallelise. Do not call \`save_snapshot\` during this phase \u2014 that is a Phase 2 step.
3094
+ - You must walk every batch until \`nextOffset\` is null. Do not stop early. Do not skip ahead to reduce until every batch has been saved as a partial.
3095
+
3096
+ PHASE 2 \u2014 Reduce (once)
3097
+ Goal: merge all partial maps into one coherent product-shaped catalog.
3098
+
3099
+ 1. Call \`reduce_snapshot(dir)\`. It returns every partial map plus reconciliation instructions.
3100
+ 2. Follow the instructions to produce a unified \`features\` and \`flows\` map. Specifically: merge platform variants ("home Android" + "home iOS" \u2192 "home screen"), dedupe near-duplicates, reconcile descriptions, and ensure every file from every partial appears somewhere in the final map.
3101
+ 3. Call \`save_snapshot(dir, features, flows)\` ONCE with the unified map. Mason detects that partials exist and replaces the snapshot wholesale (rather than merging with any earlier state) and then clears the partials. Do not call \`save_snapshot\` more than once per Map-Reduce run.
3102
+
3103
+ ${ASSISTANT_SETUP}
3104
+
3105
+ If a build is interrupted, its partials remain in \`.mason/partial-snapshots/\`. Re-run \`mason_init(dir, mode: "map")\` to obtain this workflow again.`;
3106
+ }
3107
+ });
3108
+
3109
+ // src/review/cochange.ts
3110
+ import { execFile as execFile8 } from "child_process";
3111
+ import { promisify as promisify8 } from "util";
3112
+ async function buildMatrix(resolvedRoot) {
3113
+ try {
3114
+ const { stdout: shallow } = await exec8("git", ["rev-parse", "--is-shallow-repository"], { cwd: resolvedRoot });
3115
+ if (shallow.trim() === "true") return null;
3116
+ const { stdout } = await exec8(
3117
+ "git",
3118
+ ["log", `-n${HISTORY_COMMITS}`, "--format=%x01", "--name-only", "-M"],
3119
+ { cwd: resolvedRoot, maxBuffer: 100 * 1024 * 1024 }
3120
+ );
3121
+ const commitsByFile = /* @__PURE__ */ new Map();
3122
+ const blocks = stdout.split("");
3123
+ let index2 = 0;
3124
+ for (const block of blocks) {
3125
+ const files = block.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith(".mason/"));
3126
+ if (files.length === 0 || files.length > MAX_COMMIT_FILES) continue;
3127
+ for (const file of files) {
3128
+ let set = commitsByFile.get(file);
3129
+ if (!set) {
3130
+ set = /* @__PURE__ */ new Set();
3131
+ commitsByFile.set(file, set);
3132
+ }
3133
+ set.add(index2);
3134
+ }
3135
+ index2++;
3136
+ }
3137
+ return { commitsByFile, totalCommits: index2 };
3138
+ } catch {
3139
+ return null;
3140
+ }
3141
+ }
3142
+ async function findMissingPartners(resolvedRoot, changedFiles, existsOnDisk, allChangedFiles = changedFiles) {
3143
+ const matrix = await buildMatrix(resolvedRoot);
3144
+ if (!matrix) return null;
3145
+ const changedSet = new Set(allChangedFiles);
3146
+ const findings = [];
3147
+ for (const changedFile of changedFiles) {
3148
+ const fileCommits = matrix.commitsByFile.get(changedFile);
3149
+ if (!fileCommits || fileCommits.size < MIN_FILE_COMMITS) continue;
3150
+ for (const [partner, partnerCommits] of matrix.commitsByFile) {
3151
+ if (partner === changedFile || changedSet.has(partner)) continue;
3152
+ let shared = 0;
3153
+ for (const c of fileCommits) {
3154
+ if (partnerCommits.has(c)) shared++;
3155
+ }
3156
+ if (shared < MIN_SHARED_COMMITS) continue;
3157
+ const rate = shared / fileCommits.size;
3158
+ if (rate < MIN_COCHANGE_RATE) continue;
3159
+ if (!await existsOnDisk(partner)) continue;
3160
+ findings.push({
3161
+ changedFile,
3162
+ missingPartner: partner,
3163
+ sharedCommits: shared,
3164
+ fileCommits: fileCommits.size,
3165
+ rate: Math.round(rate * 100) / 100
3166
+ });
3167
+ }
3168
+ }
3169
+ findings.sort((a, b) => b.rate - a.rate || b.sharedCommits - a.sharedCommits);
3170
+ return findings;
3171
+ }
3172
+ var exec8, HISTORY_COMMITS, MIN_FILE_COMMITS, MIN_SHARED_COMMITS, MIN_COCHANGE_RATE, MAX_COMMIT_FILES;
3173
+ var init_cochange = __esm({
3174
+ "src/review/cochange.ts"() {
3175
+ "use strict";
3176
+ exec8 = promisify8(execFile8);
3177
+ HISTORY_COMMITS = 1500;
3178
+ MIN_FILE_COMMITS = 5;
3179
+ MIN_SHARED_COMMITS = 4;
3180
+ MIN_COCHANGE_RATE = 0.6;
3181
+ MAX_COMMIT_FILES = 30;
3182
+ }
3183
+ });
3184
+
3185
+ // src/review/evidence/paths.ts
3186
+ function evidencePath(value, sourceRoot, uri = false) {
3187
+ if (value.length > 4e3) return null;
3188
+ let file = value;
3189
+ try {
3190
+ if (uri) {
3191
+ if (file.startsWith("file:")) {
3192
+ const url = new URL(file);
3193
+ if (url.hostname && url.hostname !== "localhost") return null;
3194
+ file = decodeURIComponent(url.pathname).replace(/^\/([A-Za-z]:\/)/, "$1");
3195
+ } else {
3196
+ if (/^[a-z][a-z0-9+.-]*:/i.test(file)) return null;
3197
+ file = decodeURIComponent(file);
3198
+ }
3199
+ }
3200
+ } catch {
3201
+ return null;
3202
+ }
3203
+ file = file.replace(/\\/g, "/");
3204
+ const root = sourceRoot.replace(/\\/g, "/").replace(/\/+$/, "");
3205
+ if (file.startsWith("/") || /^[A-Za-z]:/.test(file)) {
3206
+ if (!file.startsWith(root + "/")) return null;
3207
+ file = file.slice(root.length + 1);
3208
+ }
3209
+ return normalizeRepoPath(file);
3210
+ }
3211
+ var init_paths2 = __esm({
3212
+ "src/review/evidence/paths.ts"() {
3213
+ "use strict";
3214
+ init_paths();
3215
+ }
3216
+ });
3217
+
3218
+ // src/review/evidence/types.ts
3219
+ function messagePreview(message2) {
3220
+ return { message: message2.slice(0, 4e3), ...message2.length > 4e3 ? { truncated: true } : {} };
3221
+ }
3222
+ var MAX_FINDINGS;
3223
+ var init_types2 = __esm({
3224
+ "src/review/evidence/types.ts"() {
3225
+ "use strict";
3226
+ MAX_FINDINGS = 200;
3227
+ }
3228
+ });
3229
+
3230
+ // src/review/evidence/vitest.ts
3231
+ import { z as z12 } from "zod";
3232
+ function parseVitest(raw, sourceRoot) {
3233
+ const report = schema.parse(raw);
3234
+ const findings = [], diagnostics = [];
3235
+ let passed = 0, failed = 0, skipped = 0;
3236
+ for (const [index2, suite] of report.testResults.entries()) {
3237
+ const file = evidencePath(suite.name, sourceRoot);
3238
+ if (!file) diagnostics.push(`Test path is outside the declared checkout or invalid: ${suite.name}`);
3239
+ let suiteFailures = 0;
3240
+ for (const [testIndex, test] of suite.assertionResults.entries()) {
3241
+ if (test.status === "passed") passed++;
3242
+ else if (test.status === "failed") {
3243
+ failed++;
3244
+ suiteFailures++;
3245
+ findings.push({
3246
+ id: `${index2}:${testIndex}`,
3247
+ ...messagePreview([test.fullName, ...test.failureMessages ?? []].join("\n")),
3248
+ severity: "error",
3249
+ state: "active",
3250
+ locations: file ? [{ file, ...test.location ? { line: test.location.line, column: test.location.column } : {} }] : []
3251
+ });
3252
+ } else skipped++;
3253
+ }
3254
+ if (suite.status === "failed" && !suiteFailures) {
3255
+ findings.push({ id: `${index2}:suite`, ...messagePreview(suite.message || `Test suite failed: ${suite.name}`), severity: "error", state: "active", locations: file ? [{ file }] : [] });
3256
+ }
3257
+ }
3258
+ if (passed !== report.numPassedTests || failed !== report.numFailedTests || skipped !== report.numPendingTests + report.numTodoTests || passed + failed + skipped !== report.numTotalTests) {
3259
+ throw new Error("Vitest summary counts disagree with its assertion results");
3260
+ }
3261
+ const hasFailures = failed > 0 || report.numFailedTestSuites > 0 || report.testResults.some((s) => s.status === "failed");
3262
+ if (report.success && hasFailures) throw new Error("Vitest success conflicts with failed tests or suites");
3263
+ const outcome = hasFailures || !report.success ? "failed" : !report.numTotalTests ? "unavailable" : !passed ? "skipped" : "passed";
3264
+ if (!report.numTotalTests) diagnostics.push("No tests executed; an empty report is not passing test evidence.");
3265
+ if (skipped) diagnostics.push(`${skipped} tests were skipped, pending, disabled, or todo.`);
3266
+ return {
3267
+ outcome,
3268
+ findings,
3269
+ counts: { total: report.numTotalTests, passed, failed, skipped, failedSuites: report.numFailedTestSuites },
3270
+ incomplete: skipped > 0 || diagnostics.length > 0,
3271
+ diagnostics
3272
+ };
3273
+ }
3274
+ var count2, schema;
3275
+ var init_vitest = __esm({
3276
+ "src/review/evidence/vitest.ts"() {
3277
+ "use strict";
3278
+ init_paths2();
3279
+ init_types2();
3280
+ count2 = z12.number().int().nonnegative();
3281
+ schema = z12.object({
3282
+ success: z12.boolean(),
3283
+ numTotalTests: count2,
3284
+ numPassedTests: count2,
3285
+ numFailedTests: count2,
3286
+ numPendingTests: count2,
3287
+ numTodoTests: count2,
3288
+ numFailedTestSuites: count2,
3289
+ testResults: z12.array(z12.object({
3290
+ name: z12.string().min(1),
3291
+ status: z12.enum(["passed", "failed"]),
3292
+ message: z12.string().optional(),
3293
+ assertionResults: z12.array(z12.object({
3294
+ fullName: z12.string(),
3295
+ status: z12.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
3296
+ failureMessages: z12.array(z12.string()).nullable().optional(),
3297
+ location: z12.object({ line: count2, column: count2 }).nullable().optional()
3298
+ }))
3299
+ }))
3300
+ });
3301
+ }
3302
+ });
3303
+
3304
+ // src/review/evidence/sarif.ts
3305
+ import { z as z13 } from "zod";
3306
+ function parseSarif(raw, sourceRoot) {
3307
+ const report = schema2.parse(raw), findings = [], diagnostics = [];
3308
+ let active = 0, suppressed = 0, absent = 0, unresolved = 0;
3309
+ let executed = report.runs.length > 0, executionFailed = false;
3310
+ const reportedCommits = [], reportedCommands = [], reportedTools = [];
3311
+ for (const [runIndex, run] of report.runs.entries()) {
3312
+ reportedTools.push(run.tool.driver.name);
3313
+ reportedCommits.push(...(run.versionControlProvenance ?? []).flatMap((v) => v.revisionId ? [v.revisionId] : []));
3314
+ if (!run.invocations?.length) executed = false;
3315
+ for (const invocation of run.invocations ?? []) {
3316
+ if (invocation.commandLine) reportedCommands.push(invocation.commandLine);
3317
+ if (!invocation.executionSuccessful) executionFailed = true;
3318
+ for (const notification of invocation.toolExecutionNotifications ?? []) {
3319
+ diagnostics.push(notification.message.text ?? notification.message.markdown ?? "SARIF tool execution notification");
3320
+ if (notification.level === "error") executionFailed = true;
3321
+ }
3322
+ }
3323
+ if (!run.results) {
3324
+ diagnostics.push(`Run ${runIndex} omits results; analysis output is unavailable.`);
3325
+ executed = false;
3326
+ }
3327
+ const resolve = (ref, seen = /* @__PURE__ */ new Set()) => {
3328
+ if (!ref.uri && ref.index !== void 0) {
3329
+ const key = `artifact:${ref.index}`;
3330
+ if (seen.has(key)) return null;
3331
+ const entry = run.artifacts?.[ref.index]?.location;
3332
+ return entry ? resolve(entry, /* @__PURE__ */ new Set([...seen, key])) : null;
3333
+ }
3334
+ if (!ref.uri) return null;
3335
+ if (!ref.uriBaseId) return ref.uri;
3336
+ if (seen.has(ref.uriBaseId)) return null;
3337
+ const base = run.originalUriBaseIds?.[ref.uriBaseId];
3338
+ if (!base) return null;
3339
+ const prefix = resolve(base, /* @__PURE__ */ new Set([...seen, ref.uriBaseId]));
3340
+ if (!prefix) return null;
3341
+ return /^[a-z][a-z0-9+.-]*:/i.test(ref.uri) ? ref.uri : prefix + ref.uri;
3342
+ };
3343
+ for (const [resultIndex, result] of (run.results ?? []).entries()) {
3344
+ const kind = result.kind ?? "fail";
3345
+ const isSuppressed = result.suppressions?.some((s) => s.status === "accepted");
3346
+ if (result.suppressions?.some((s) => !s.status || s.status === "underReview")) {
3347
+ diagnostics.push(`Suppression state unresolved for result ${runIndex}:${resultIndex}.`);
3348
+ unresolved++;
3349
+ }
3350
+ const state = result.baselineState === "absent" ? "absent" : isSuppressed ? "suppressed" : kind === "fail" ? "active" : "informational";
3351
+ if (state === "active") active++;
3352
+ if (state === "absent") absent++;
3353
+ if (state === "suppressed") suppressed++;
3354
+ if (["open", "review"].includes(kind) && state === "informational") {
3355
+ unresolved++;
3356
+ diagnostics.push(`Result ${runIndex}:${resultIndex} needs further analysis or review.`);
3357
+ }
3358
+ if (kind === "pass" || kind === "notApplicable") continue;
3359
+ const rule = result.ruleIndex !== void 0 ? run.tool.driver.rules?.[result.ruleIndex] : run.tool.driver.rules?.find((rule2) => rule2.id === result.ruleId);
3360
+ const template = result.message.id ? rule?.messageStrings?.[result.message.id] ?? run.tool.driver.globalMessageStrings?.[result.message.id] : void 0;
3361
+ const rawMessage = result.message.text ?? result.message.markdown ?? template?.text ?? template?.markdown;
3362
+ if (!rawMessage) diagnostics.push(`Message cannot be resolved for result ${runIndex}:${resultIndex}.`);
3363
+ const rendered = (rawMessage ?? `Unresolved SARIF message ${result.message.id ?? ""}`).replace(/\{(\d+)\}/g, (match, n) => result.message.arguments?.[Number(n)] ?? match);
3364
+ const locations = [];
3365
+ for (const entry of [...result.locations ?? [], ...result.relatedLocations ?? []]) {
3366
+ const ref = entry.physicalLocation?.artifactLocation;
3367
+ const uri = ref ? resolve(ref) : null;
3368
+ const file = uri ? evidencePath(uri, sourceRoot, true) : null;
3369
+ if (file) locations.push({ file, line: entry.physicalLocation?.region?.startLine, column: entry.physicalLocation?.region?.startColumn });
3370
+ else diagnostics.push(`Unresolved or out-of-checkout location for result ${runIndex}:${resultIndex}.`);
3371
+ }
3372
+ const severity = result.level ?? rule?.defaultConfiguration?.level ?? (kind === "fail" ? "warning" : "none");
3373
+ findings.push({ id: `${runIndex}:${resultIndex}`, ruleId: result.ruleId ?? rule?.id, ...messagePreview(rendered), severity: severity === "none" ? "note" : severity, state, locations });
3374
+ }
3375
+ }
3376
+ if (!report.runs.length) diagnostics.push("SARIF contains no analysis runs.");
3377
+ if (executionFailed) diagnostics.push("The SARIF tool reported an unsuccessful analysis invocation.");
3378
+ return {
3379
+ outcome: executionFailed || !report.runs.length || report.runs.some((r) => !r.results) ? "unavailable" : active ? "failed" : "passed",
3380
+ findings,
3381
+ counts: { active, suppressed, absent, unresolved },
3382
+ incomplete: diagnostics.length > 0,
3383
+ diagnostics,
3384
+ reportedCommits,
3385
+ reportedCommands,
3386
+ reportedTools,
3387
+ executed: executed && !executionFailed
3388
+ };
3389
+ }
3390
+ var index, artifact, message, level, location, schema2;
3391
+ var init_sarif = __esm({
3392
+ "src/review/evidence/sarif.ts"() {
3393
+ "use strict";
3394
+ init_paths2();
3395
+ init_types2();
3396
+ index = z13.number().int().nonnegative();
3397
+ artifact = z13.object({ uri: z13.string().optional(), uriBaseId: z13.string().optional(), index: index.optional() });
3398
+ message = z13.object({ text: z13.string().optional(), markdown: z13.string().optional(), id: z13.string().optional(), arguments: z13.array(z13.string()).optional() });
3399
+ level = z13.enum(["error", "warning", "note", "none"]);
3400
+ location = z13.object({ physicalLocation: z13.object({ artifactLocation: artifact.optional(), region: z13.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
3401
+ schema2 = z13.object({
3402
+ version: z13.literal("2.1.0"),
3403
+ runs: z13.array(z13.object({
3404
+ tool: z13.object({ driver: z13.object({ name: z13.string().min(1), rules: z13.array(z13.object({ id: z13.string(), defaultConfiguration: z13.object({ level: level.optional() }).optional(), messageStrings: z13.record(message).optional() })).optional(), globalMessageStrings: z13.record(message).optional() }) }),
3405
+ invocations: z13.array(z13.object({ executionSuccessful: z13.boolean(), commandLine: z13.string().optional(), toolExecutionNotifications: z13.array(z13.object({ level: level.optional(), message })).optional() })).optional(),
3406
+ versionControlProvenance: z13.array(z13.object({ revisionId: z13.string().optional() })).optional(),
3407
+ originalUriBaseIds: z13.record(artifact).optional(),
3408
+ artifacts: z13.array(z13.object({ location: artifact.optional() })).optional(),
3409
+ results: z13.array(z13.object({
3410
+ ruleId: z13.string().optional(),
3411
+ ruleIndex: index.optional(),
3412
+ message,
3413
+ level: level.optional(),
3414
+ kind: z13.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
3415
+ baselineState: z13.enum(["new", "unchanged", "updated", "absent"]).optional(),
3416
+ suppressions: z13.array(z13.object({ kind: z13.enum(["inSource", "external"]), status: z13.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
3417
+ locations: z13.array(location).optional(),
3418
+ relatedLocations: z13.array(location).optional()
3419
+ })).optional()
3420
+ }))
3421
+ });
3422
+ }
3423
+ });
3424
+
3425
+ // src/review/evidence.ts
3426
+ import path18 from "path";
3427
+ import { z as z14 } from "zod";
3428
+ function relativeArtifact(root, file) {
3429
+ const relative = path18.isAbsolute(file) ? path18.relative(root, file) : file;
3430
+ const normalized = normalizeRepoPath(relative);
3431
+ if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
3432
+ return normalized;
3433
+ }
3434
+ function linkFinding(finding, changed, pairs, decisions, freshness) {
3435
+ const located = [...new Set(finding.locations.map((l) => l.file))];
3436
+ const paired = pairs.filter((pair) => located.includes(pair.test));
3437
+ const relatedChangedFiles = located.filter((file) => changed.has(file)).map((file) => ({ file, relationship: "direct" }));
3438
+ for (const pair of paired) {
3439
+ if (changed.has(pair.source) && !relatedChangedFiles.some((f) => f.file === pair.source)) relatedChangedFiles.push({ file: pair.source, relationship: "paired-test", confidence: pair.confidence });
3440
+ }
3441
+ const allFiles = [.../* @__PURE__ */ new Set([...located, ...paired.map((pair) => pair.source)])];
3442
+ const acceptedDecisions = decisions.map(effectiveDecision).filter((d) => d.status === "active" && decisionApproval(d) === "accepted").map((d) => ({ d, viaFiles: matchingPaths(d.files, allFiles) })).filter((match) => match.viaFiles.length).map(({ d, viaFiles }) => {
3443
+ const state = freshness[d.id] ?? "unknown", provenance = decisionProvenance(d, state);
3444
+ return { id: d.id, title: d.title, owner: provenance.owner, freshness: state, reviewRequired: provenance.reviewRequired, viaFiles };
3445
+ });
3446
+ return { ...finding, relatedChangedFiles, acceptedDecisions: acceptedDecisions.slice(0, 5), totalAcceptedDecisions: acceptedDecisions.length };
3447
+ }
3448
+ async function collectReviewEvidence(root, manifests, changedFiles, decisions, decisionFreshness = {}, reviewedHead) {
3449
+ const [headHash, workingTree] = await Promise.all([reviewedHead ?? getCurrentGitHash(root), getWorkingTree(root)]);
3450
+ const output = {
3451
+ version: 1,
3452
+ scope: "committed",
3453
+ headHash,
3454
+ status: "unavailable",
3455
+ checks: [],
3456
+ diagnostics: [],
3457
+ workingTree,
3458
+ summary: { passed: 0, failed: 0, skipped: 0, unavailable: 0, stale: 0, unknown: 0 },
3459
+ hint: "Evidence describes the recorded check runs for a commit, not uncommitted edits or complete test coverage. Commands and CI provenance are imported assertions, not authenticated execution. File and test-pair associations identify relevant decisions; they do not prove a decision was violated. Missing checks must be declared in the manifest to be reported."
3460
+ };
3461
+ const seen = /* @__PURE__ */ new Set(), changed = new Set(changedFiles);
3462
+ let pairs;
3463
+ if (manifests.length > 10) output.diagnostics.push("Only the first 10 evidence manifests were imported.");
3464
+ for (const supplied of manifests.slice(0, 10)) {
3465
+ let manifest, raw;
3466
+ try {
3467
+ manifest = relativeArtifact(root, supplied);
3468
+ raw = z14.object({ version: z14.literal(1), checks: z14.array(z14.unknown()) }).parse(await readStoreJson(root, manifest));
3469
+ } catch (error) {
3470
+ output.diagnostics.push(`${supplied}: ${String(error)}`);
3471
+ continue;
3472
+ }
3473
+ if (!raw.checks.length) output.diagnostics.push(`${manifest}: no checks declared.`);
3474
+ for (const [index2, item] of raw.checks.entries()) {
3475
+ if (output.checks.length >= 50) {
3476
+ output.diagnostics.push("Only the first 50 checks were imported.");
3477
+ break;
3478
+ }
3479
+ const input2 = checkSchema2.safeParse(item);
3480
+ if (!input2.success) {
3481
+ output.diagnostics.push(`${manifest} check ${index2}: ${input2.error.message}`);
3482
+ continue;
3483
+ }
3484
+ const check = input2.data;
3485
+ if (seen.has(check.id)) {
3486
+ output.diagnostics.push(`Duplicate check id ${check.id}; give different runs distinct ids.`);
3487
+ continue;
3488
+ }
3489
+ seen.add(check.id);
3490
+ const result = {
3491
+ id: check.id,
3492
+ kind: check.kind,
3493
+ tool: check.tool,
3494
+ command: check.command,
3495
+ commit: check.commit?.toLowerCase() ?? null,
3496
+ workingTreeClean: check.workingTreeClean ?? null,
3497
+ source: check.source ?? null,
3498
+ manifest,
3499
+ report: check.report ?? null,
3500
+ outcome: "unavailable",
3501
+ freshness: "unknown",
3502
+ findings: [],
3503
+ totalFindings: 0,
3504
+ counts: {},
3505
+ incomplete: false,
3506
+ diagnostics: [],
3507
+ truncated: false
3508
+ };
3509
+ output.checks.push(result);
3510
+ if (check.workingTreeClean !== true) result.diagnostics.push("The check's working tree was dirty or not recorded; its results cannot be attributed to the claimed commit alone.");
3511
+ else if (result.commit && headHash !== "unknown") result.freshness = result.commit === headHash.toLowerCase() ? "current" : "stale";
3512
+ else result.diagnostics.push("The tested commit or reviewed HEAD is unknown.");
3513
+ if (check.status !== "completed") {
3514
+ result.outcome = check.status;
3515
+ result.diagnostics.push(check.reason ?? "No reason was recorded for this skipped or unavailable check.");
3516
+ continue;
3517
+ }
3518
+ try {
3519
+ if (!check.report) throw new Error("Completed check has no report artifact.");
3520
+ if (check.kind === "tests" !== (check.report.format === "vitest-json")) throw new Error("Test checks require vitest-json; analysis checks require sarif.");
3521
+ const file = relativeArtifact(root, check.report.path);
3522
+ const report = await readStoreJson(root, file);
3523
+ if (report === null) throw new Error(`Report artifact is missing: ${file}`);
3524
+ const sourceRoot = check.sourceRoot ?? root;
3525
+ if (!path18.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
3526
+ const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : parseSarif(report, sourceRoot);
3527
+ result.outcome = parsed.outcome;
3528
+ result.counts = parsed.counts;
3529
+ result.incomplete = parsed.incomplete;
3530
+ result.diagnostics.push(...parsed.diagnostics);
3531
+ result.reportedCommands = parsed.reportedCommands;
3532
+ result.reportedTools = parsed.reportedTools;
3533
+ if (parsed.reportedCommits?.some((commit) => !result.commit || commit.toLowerCase() !== result.commit)) {
3534
+ result.freshness = "unknown";
3535
+ result.incomplete = true;
3536
+ result.diagnostics.push("Report revision metadata conflicts with, or cannot be tied to, the manifest's tested commit.");
3537
+ }
3538
+ if (check.exitCode == null && !parsed.executed) {
3539
+ result.incomplete = true;
3540
+ result.diagnostics.push("Neither an exit code nor successful analysis invocation was recorded; completion cannot be confirmed.");
3541
+ }
3542
+ if (check.exitCode != null && check.exitCode !== 0 && result.outcome === "passed") {
3543
+ result.outcome = "failed";
3544
+ result.diagnostics.push(`Check command exited ${check.exitCode} despite a report with no active failures.`);
3545
+ }
3546
+ if (check.report.format === "vitest-json" && pairs === void 0) {
3547
+ try {
3548
+ pairs = (await buildTestMap(root)).paired;
3549
+ } catch (error) {
3550
+ pairs = [];
3551
+ output.diagnostics.push(`Test pairing unavailable: ${String(error)}`);
3552
+ }
3553
+ }
3554
+ result.totalFindings = parsed.findings.length;
3555
+ const testPairs = check.kind === "tests" ? pairs ?? [] : [];
3556
+ const relevant = /* @__PURE__ */ new Set([...changed, ...testPairs.filter((p) => changed.has(p.source)).map((p) => p.test)]);
3557
+ const touchesChange = (f) => f.locations.some((l) => relevant.has(l.file));
3558
+ parsed.findings.sort((a, b) => Number(b.state === "active") - Number(a.state === "active") || Number(touchesChange(b)) - Number(touchesChange(a)));
3559
+ result.findings = parsed.findings.slice(0, MAX_FINDINGS).map((f) => linkFinding(f, changed, testPairs, decisions, decisionFreshness));
3560
+ result.truncated = parsed.findings.length > MAX_FINDINGS || result.findings.some((f) => f.truncated || f.totalAcceptedDecisions > f.acceptedDecisions.length);
3561
+ } catch (error) {
3562
+ result.outcome = "unavailable";
3563
+ result.incomplete = true;
3564
+ result.diagnostics.push(String(error));
3565
+ }
3566
+ }
3567
+ }
3568
+ if (!output.checks.length) output.diagnostics.push("No readable checks were imported.");
3569
+ for (const check of output.checks) {
3570
+ output.summary[check.outcome]++;
3571
+ if (check.freshness !== "current") output.summary[check.freshness]++;
3572
+ }
3573
+ if (output.checks.some((c) => c.outcome === "failed" && c.freshness === "current")) output.status = "failed";
3574
+ else if (!output.checks.length || output.checks.every((c) => c.outcome === "unavailable")) output.status = "unavailable";
3575
+ else if (output.diagnostics.length || output.checks.some((c) => c.outcome !== "passed" || c.freshness !== "current" || c.incomplete)) output.status = "incomplete";
3576
+ else output.status = "passed";
3577
+ return output;
3578
+ }
3579
+ function summarizeEvidence(evidence) {
3580
+ const previews = (values, limit) => values.slice(0, limit).map((value) => value.slice(0, 2e3));
3581
+ const checks = evidence.checks.slice(0, 10).map((check) => {
3582
+ const findings = check.findings.slice(0, 5).map((finding) => ({
3583
+ ...finding,
3584
+ ruleId: finding.ruleId?.slice(0, 200),
3585
+ locations: finding.locations.slice(0, 5),
3586
+ totalLocations: finding.locations.length,
3587
+ relatedChangedFiles: finding.relatedChangedFiles.slice(0, 5),
3588
+ totalRelatedChangedFiles: finding.relatedChangedFiles.length,
3589
+ acceptedDecisions: finding.acceptedDecisions.map((decision) => ({ ...decision, viaFiles: decision.viaFiles.slice(0, 5) })),
3590
+ truncated: finding.truncated || finding.locations.length > 5 || finding.relatedChangedFiles.length > 5 || (finding.ruleId?.length ?? 0) > 200 || finding.acceptedDecisions.some((d) => d.viaFiles.length > 5)
3591
+ }));
3592
+ return {
3593
+ ...check,
3594
+ findings,
3595
+ reportedCommands: check.reportedCommands ? previews(check.reportedCommands, 5) : void 0,
3596
+ reportedTools: check.reportedTools ? previews(check.reportedTools, 5) : void 0,
3597
+ diagnostics: previews(check.diagnostics, 10),
3598
+ truncated: check.truncated || check.findings.length > 5 || findings.some((f) => f.truncated) || check.diagnostics.length > 10 || check.diagnostics.some((d) => d.length > 2e3) || [check.reportedCommands ?? [], check.reportedTools ?? []].some((values) => values.length > 5 || values.some((v) => v.length > 2e3))
3599
+ };
3600
+ });
3601
+ return {
3602
+ ...evidence,
3603
+ checks,
3604
+ diagnostics: previews(evidence.diagnostics, 20),
3605
+ truncated: evidence.checks.length > 10 || evidence.diagnostics.length > 20 || evidence.diagnostics.some((d) => d.length > 2e3) || checks.some((c) => c.truncated)
3606
+ };
3607
+ }
3608
+ var checkSchema2;
3609
+ var init_evidence2 = __esm({
3610
+ "src/review/evidence.ts"() {
3611
+ "use strict";
3612
+ init_storage();
3613
+ init_paths();
3614
+ init_snapshot();
3615
+ init_drift();
3616
+ init_test_map();
3617
+ init_provenance();
3618
+ init_vitest();
3619
+ init_sarif();
3620
+ init_types2();
3621
+ checkSchema2 = z14.object({
3622
+ id: z14.string().min(1).max(100),
3623
+ kind: z14.enum(["tests", "static-analysis", "security", "complexity", "duplication"]),
3624
+ tool: z14.string().min(1).max(200),
3625
+ command: z14.string().min(1).max(2e3),
3626
+ commit: z14.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
3627
+ workingTreeClean: z14.boolean().optional(),
3628
+ source: z14.string().max(2e3).optional(),
3629
+ sourceRoot: z14.string().min(1).max(4e3).optional(),
3630
+ status: z14.enum(["completed", "skipped", "unavailable"]).default("completed"),
3631
+ reason: z14.string().min(1).max(2e3).optional(),
3632
+ exitCode: z14.number().int().nullable().optional(),
3633
+ report: z14.object({ format: z14.enum(["vitest-json", "sarif"]), path: z14.string().min(1).max(4e3) }).optional()
3634
+ });
3635
+ }
3636
+ });
3637
+
3638
+ // src/review/review.ts
3639
+ import fs14 from "fs/promises";
3640
+ import path19 from "path";
3641
+ import { execFile as execFile9 } from "child_process";
3642
+ import { promisify as promisify9 } from "util";
3643
+ async function resolveMergeBase(resolvedRoot, base, head) {
3644
+ try {
3645
+ const { stdout } = await exec9("git", ["merge-base", base, head], {
3646
+ cwd: resolvedRoot
3647
+ });
3648
+ return stdout.trim() || null;
3649
+ } catch {
3650
+ return null;
3651
+ }
3652
+ }
3653
+ async function defaultBase(resolvedRoot) {
3654
+ for (const ref of ["origin/HEAD", "origin/main", "origin/master", "main"]) {
3655
+ try {
3656
+ await exec9("git", ["rev-parse", "--verify", "--quiet", ref], {
3657
+ cwd: resolvedRoot
3658
+ });
3659
+ return ref;
3660
+ } catch {
3661
+ }
3662
+ }
3663
+ return null;
3664
+ }
3665
+ function anchorsTouched(record, changedFiles) {
3666
+ return matchingPaths(record.files, changedFiles);
3667
+ }
3668
+ async function computeReview(rootDir, base, options = {}) {
3669
+ const resolvedRoot = path19.resolve(rootDir);
3670
+ const head = await getCurrentGitHash(resolvedRoot);
3671
+ if (head === "unknown") return null;
3672
+ const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
3673
+ if (!mergeBase) return null;
3674
+ const changes = await getChangesWithStatus(resolvedRoot, mergeBase, head);
3675
+ if (changes === null) return null;
3676
+ const changedFiles = touchedPaths(changes);
3677
+ const report = {
3678
+ version: 1,
3679
+ root: resolvedRoot,
3680
+ base,
3681
+ mergeBase,
3682
+ changedFiles,
3683
+ missingPartners: [],
3684
+ touchedDecisions: [],
3685
+ historyAvailable: true,
3686
+ truncated: false
3687
+ };
3688
+ const store = await loadDecisionStore(resolvedRoot);
3689
+ report.diagnostics = store.diagnostics;
3690
+ const decisionDrift = await computeDecisionDrift(resolvedRoot, store.records);
3691
+ if (options.evidence !== void 0) {
3692
+ report.evidence = await collectReviewEvidence(resolvedRoot, options.evidence, changedFiles, store.records, decisionDrift.freshness, head);
3693
+ if (store.diagnostics.length) {
3694
+ report.evidence.diagnostics.push("Invalid decision records make knowledge associations incomplete; consult review diagnostics.");
3695
+ if (report.evidence.status === "passed") report.evidence.status = "incomplete";
3696
+ }
3697
+ }
3698
+ const finalize = async () => {
3699
+ if (report.evidence && await getCurrentGitHash(resolvedRoot) !== head) {
3700
+ report.evidence.diagnostics.push("HEAD changed during the review; rerun to obtain consistent change and knowledge associations.");
3701
+ for (const check of report.evidence.checks) check.freshness = "unknown";
3702
+ report.evidence.summary.stale = 0;
3703
+ report.evidence.summary.unknown = report.evidence.checks.length;
3704
+ if (report.evidence.status !== "unavailable") report.evidence.status = "incomplete";
3705
+ }
3706
+ return report;
3707
+ };
3708
+ if (changedFiles.length === 0) return finalize();
3709
+ let analyzed = changedFiles;
3710
+ if (changedFiles.length > MAX_ANALYZED_FILES) {
3711
+ analyzed = changedFiles.slice(0, MAX_ANALYZED_FILES);
3712
+ report.truncated = true;
3713
+ }
3714
+ const partners = await findMissingPartners(
3715
+ resolvedRoot,
3716
+ analyzed,
3717
+ async (relPath) => {
3718
+ try {
3719
+ await fs14.access(path19.join(resolvedRoot, relPath));
3720
+ return true;
3721
+ } catch {
3722
+ return false;
3723
+ }
3724
+ },
3725
+ changedFiles
3726
+ );
3727
+ if (partners === null) {
3728
+ report.historyAvailable = false;
3729
+ } else {
3730
+ report.missingPartners = partners;
3731
+ }
3732
+ const decisions = store.records;
3733
+ for (const record of decisions) {
3734
+ if (record.status !== "active") continue;
3735
+ const effective = effectiveDecision(record);
3736
+ const touched = anchorsTouched(effective, changedFiles);
3737
+ const proposalTouched = effective !== record ? anchorsTouched(record, changedFiles) : [];
3738
+ if (touched.length > 0 || proposalTouched.length > 0) {
3739
+ const { pendingProposal, ...knowledge } = decisionKnowledge(record, decisionDrift.freshness?.[record.id] ?? "unknown", decisionDrift.pendingProposals?.[record.id]?.freshness ?? "unknown");
3740
+ report.touchedDecisions.push({
3741
+ ...knowledge,
3742
+ ...pendingProposal ? { pendingProposal: { ...pendingProposal, touchedFiles: proposalTouched } } : {},
3743
+ id: record.id,
3744
+ anchors: effective.files,
3745
+ freshness: decisionDrift.freshness?.[record.id] ?? "unknown",
3746
+ touchedFiles: touched
3747
+ });
3748
+ }
3749
+ }
3750
+ return finalize();
3751
+ }
3752
+ var exec9, MAX_ANALYZED_FILES;
3753
+ var init_review = __esm({
3754
+ "src/review/review.ts"() {
3755
+ "use strict";
3756
+ init_drift();
3757
+ init_provenance();
3758
+ init_decisions();
3759
+ init_paths();
3760
+ init_drift2();
3761
+ init_cochange();
3762
+ init_evidence2();
3763
+ init_snapshot();
3764
+ exec9 = promisify9(execFile9);
3765
+ MAX_ANALYZED_FILES = 50;
3766
+ }
3767
+ });
3768
+
3769
+ // src/mcp/onboarding.ts
3770
+ async function auditSummary(root) {
3771
+ try {
3772
+ const report = await computeAudit(root);
3773
+ if (!report) return { status: "no-context-files", reason: "No AGENTS.md, CLAUDE.md, or .claude/CLAUDE.md found to audit." };
3774
+ if (!report.gitAvailable) return { status: "unavailable", reason: "Audit needs readable Git history to verify documentation claims.", docs: report.docs };
3775
+ return {
3776
+ ...report,
3777
+ status: "complete",
3778
+ issues: report.issues.slice(0, MAX_FINDINGS2),
3779
+ advisories: report.advisories.slice(0, MAX_FINDINGS2),
3780
+ suppressedAdvisories: (report.suppressedAdvisories ?? []).slice(0, MAX_FINDINGS2),
3781
+ counts: {
3782
+ issues: report.issues.length,
3783
+ advisories: report.advisories.length,
3784
+ suppressedAdvisories: report.suppressedAdvisories?.length ?? 0
3785
+ },
3786
+ truncated: report.issues.length > MAX_FINDINGS2 || report.advisories.length > MAX_FINDINGS2 || (report.suppressedAdvisories?.length ?? 0) > MAX_FINDINGS2
3787
+ };
3788
+ } catch (error) {
3789
+ return { status: "unavailable", reason: reason(error) };
3790
+ }
3791
+ }
3792
+ async function reviewSummary(root, requestedBase, evidence) {
3793
+ const workingTree = await getWorkingTree(root);
3794
+ const scope = "committed";
3795
+ try {
3796
+ const base = requestedBase ?? await defaultBase(root);
3797
+ if (!base) return { status: "unavailable", scope, workingTree, reason: "No default review base resolves. Pass base to mason_init, or run mason-review --base <ref>." };
3798
+ const report = await computeReview(root, base, { evidence });
3799
+ if (!report) return { status: "unavailable", scope, base, workingTree, reason: "The review base, merge base, or committed diff could not be read." };
3800
+ return {
3801
+ ...report,
3802
+ scope,
3803
+ workingTree,
3804
+ ...report.evidence ? { evidence: summarizeEvidence(report.evidence) } : {},
3805
+ status: !report.historyAvailable ? "unavailable" : report.changedFiles.length ? "complete" : "no-changes",
3806
+ ...!report.historyAvailable ? { reason: "Co-change history could not be read; review findings are incomplete." } : {},
3807
+ changedFiles: report.changedFiles.slice(0, MAX_FINDINGS2),
3808
+ missingPartners: report.missingPartners.slice(0, MAX_FINDINGS2),
3809
+ touchedDecisions: report.touchedDecisions.slice(0, MAX_FINDINGS2),
3810
+ counts: { changedFiles: report.changedFiles.length, missingPartners: report.missingPartners.length, touchedDecisions: report.touchedDecisions.length },
3811
+ truncated: report.truncated || [report.changedFiles, report.missingPartners, report.touchedDecisions].some((list) => list.length > MAX_FINDINGS2),
3812
+ hint: "Reviews cover merge-base..HEAD. Uncommitted paths are reported separately in workingTree; they have not been reviewed."
3813
+ };
3814
+ } catch (error) {
3815
+ return { status: "unavailable", scope, workingTree, reason: reason(error) };
3816
+ }
3817
+ }
3818
+ async function inspectOnboarding(root, base, evidence) {
3819
+ const [audit, review, map, decisions] = await Promise.all([
3820
+ auditSummary(root),
3821
+ reviewSummary(root, base, evidence),
3822
+ inspectSnapshot(root),
3823
+ loadDecisionStore(root)
3824
+ ]);
3825
+ return {
3826
+ audit,
3827
+ review,
3828
+ map: { status: map.status },
3829
+ decisions: {
3830
+ active: decisions.records.filter((record) => record.status === "active").length,
3831
+ ...Object.fromEntries(["accepted", "proposed", "unreviewed"].map((approval) => [approval, decisions.records.filter((record) => record.status === "active" && decisionApproval(effectiveDecision(record)) === approval).length])),
3832
+ pendingProposals: decisions.records.filter((record) => effectiveDecision(record) !== record).length
3833
+ },
3834
+ diagnostics: [...map.diagnostics, ...decisions.diagnostics]
3835
+ };
3836
+ }
3837
+ var MAX_FINDINGS2, reason;
3838
+ var init_onboarding = __esm({
3839
+ "src/mcp/onboarding.ts"() {
3840
+ "use strict";
3841
+ init_audit();
3842
+ init_review();
3843
+ init_drift();
3844
+ init_snapshot();
3845
+ init_provenance();
3846
+ init_evidence2();
3847
+ init_decisions();
3848
+ MAX_FINDINGS2 = 20;
3849
+ reason = (error) => error instanceof Error ? error.message : String(error);
3850
+ }
3851
+ });
3852
+
3853
+ // src/setup/files.ts
3854
+ import fs15 from "fs/promises";
3855
+ import path20 from "path";
3856
+ import { randomUUID as randomUUID5 } from "crypto";
3857
+ async function readText(root, file) {
3858
+ try {
3859
+ const text2 = await readBoundedFile(await storePath(root, file), 2 * 1024 * 1024);
3860
+ if (text2 === null) throw new Error("Setup input is not a regular file or exceeds 2 MiB: " + file);
3861
+ return text2;
3862
+ } catch (error) {
3863
+ if (error.code === "ENOENT") return null;
3864
+ throw error;
3865
+ }
3866
+ }
3867
+ function managedBlock(text2, start, end, body) {
3868
+ const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
3869
+ if (starts !== ends || starts > 1 || starts === 1 && text2.indexOf(end) < text2.indexOf(start)) {
3870
+ throw new Error("Ambiguous Mason instruction/configuration markers; repair the marked block before setup.");
3871
+ }
3872
+ const eol = text2.includes("\r\n") ? "\r\n" : "\n";
3873
+ const block = [start, body.replace(/\r?\n/g, eol), end].join(eol);
3874
+ if (starts) return text2.slice(0, text2.indexOf(start)) + block + text2.slice(text2.indexOf(end) + end.length);
3875
+ return text2 + (text2.length && !text2.endsWith("\n") ? eol : "") + (text2.length ? eol : "") + block + eol;
3876
+ }
3877
+ async function applyEdit(root, edit) {
3878
+ const current = await readText(root, edit.path);
3879
+ if (current === edit.after) return false;
3880
+ if (current !== edit.before) throw new Error("Setup input changed during installation: " + edit.path + ". Rerun setup to resume.");
3881
+ const file = await storePath(root, edit.path, true);
3882
+ const temporary = path20.join(path20.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
3883
+ try {
3884
+ const mode = await fs15.stat(file).then((s) => s.mode & 511, () => 384);
3885
+ const handle = await fs15.open(temporary, "wx", mode);
3886
+ try {
3887
+ await handle.writeFile(edit.after, "utf8");
3888
+ await handle.sync();
3889
+ } finally {
3890
+ await handle.close();
3891
+ }
3892
+ if (await readText(root, edit.path) !== edit.before) throw new Error("Setup input changed during installation: " + edit.path);
3893
+ await fs15.rename(temporary, file);
3894
+ return true;
3895
+ } finally {
3896
+ await fs15.rm(temporary, { force: true });
3897
+ }
3898
+ }
3899
+ var init_files2 = __esm({
3900
+ "src/setup/files.ts"() {
3901
+ "use strict";
3902
+ init_files();
3903
+ init_storage();
3904
+ }
3905
+ });
3906
+
3907
+ // src/setup/launcher.ts
3908
+ var BOOTSTRAP, mcpCommand, hookCommand, LAUNCHER;
3909
+ var init_launcher = __esm({
3910
+ "src/setup/launcher.ts"() {
3911
+ "use strict";
3912
+ BOOTSTRAP = "require(require('node:path').join(require('node:child_process').execFileSync('git',['rev-parse','--show-toplevel'],{encoding:'utf8'}).trim(),'.mason','run.cjs'))";
3913
+ mcpCommand = (host) => ({ command: "node", args: ["-e", BOOTSTRAP, "--", host, "mcp"] });
3914
+ hookCommand = (host) => `node -e "${BOOTSTRAP}" -- ${host} auto`;
3915
+ LAUNCHER = `// Managed by mason-auto setup. Rerun setup to install this checkout's pinned runtime.
3916
+ const fs = require('node:fs');
3917
+ const path = require('node:path');
3918
+ const {pathToFileURL} = require('node:url');
3919
+ const launchArgs = process.argv.slice(1);
3920
+ (async () => {
3921
+ const root = path.dirname(__dirname);
3922
+ const args = launchArgs;
3923
+ const [host, command, ...rest] = args;
3924
+ if (!['codex','claude'].includes(host) || !['auto','mcp'].includes(command)) throw new Error('Invalid Mason launcher arguments.');
3925
+ const setup = JSON.parse(fs.readFileSync(path.join(__dirname, 'setup.json'), 'utf8'));
3926
+ const entry = setup.hosts?.[host];
3927
+ if (setup.version !== 1 || !entry || !/^[a-f0-9]{24}$/.test(entry.runtime?.id)) throw new Error('Mason is not configured for this host. Rerun mason-auto setup.');
3928
+ const binary = path.join(__dirname, 'runtime', entry.runtime.id, 'node_modules', 'mason-context', 'dist', 'mason-' + command + '.js');
3929
+ if (!fs.existsSync(binary)) throw new Error('The pinned Mason runtime is missing in this checkout. Rerun mason-auto setup --host ' + host + '.');
3930
+ process.env.MASON_SETUP_ROOT = root;
3931
+ process.env.MASON_SETUP_HOST = host;
3932
+ process.env.MASON_SETUP_REVISION = entry.revision;
3933
+ process.argv = [process.execPath, binary, ...rest];
3934
+ await import(pathToFileURL(binary).href);
3935
+ })().catch(error => {
3936
+ const message = 'Mason setup unavailable: ' + error.message;
3937
+ if (launchArgs[1] === 'auto' && launchArgs[2] === 'hook') { console.log(JSON.stringify({systemMessage: message})); process.exitCode = 0; }
3938
+ else { console.error(message); process.exitCode = 2; }
3939
+ });
3940
+ `;
3941
+ }
3942
+ });
3943
+
3944
+ // src/setup/config.ts
3945
+ import { isDeepStrictEqual } from "util";
3946
+ import { parse, stringify } from "smol-toml";
3947
+ import { z as z15 } from "zod";
3948
+ async function instructionEdits(root, host) {
3949
+ const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
3950
+ const existing = found.find((f) => f.text !== null);
3951
+ const primary = host === "codex" && found[0].text === null ? { path: "AGENTS.md", text: null } : existing ?? { path: "AGENTS.md", text: null };
3952
+ const originalGuidance = primary.text === null && existing ? "Follow the existing project conventions in " + existing.path + ".\n" : "";
3953
+ const edits = [{
3954
+ path: primary.path,
3955
+ before: primary.text,
3956
+ after: managedBlock(primary.text ?? originalGuidance, "<!-- mason:start -->", "<!-- mason:end -->", CLAUDE_MD_SECTION.split("\n").slice(1, -1).join("\n"))
3957
+ }];
3958
+ if (primary.path === "AGENTS.md") {
3959
+ const secondary = found.filter((f) => f.path !== "AGENTS.md" && f.text !== null);
3960
+ if (host === "claude" && !secondary.length) secondary.push({ path: "CLAUDE.md", text: null });
3961
+ for (const file of secondary) {
3962
+ const text2 = file.text ?? "";
3963
+ const legacy = text2.includes("<!-- mason:start -->") || text2.includes("<!-- mason:end -->");
3964
+ const imported = "@" + (file.path.startsWith(".claude/") ? "../AGENTS.md" : "AGENTS.md");
3965
+ if (text2.trim() === imported) continue;
3966
+ const pointer = "Mason project knowledge and shared project instructions:\n" + imported;
3967
+ edits.push({
3968
+ path: file.path,
3969
+ before: file.text,
3970
+ after: managedBlock(text2, legacy ? "<!-- mason:start -->" : POINTER_START, legacy ? "<!-- mason:end -->" : POINTER_END, pointer)
3971
+ });
3972
+ }
3973
+ }
3974
+ return edits;
3975
+ }
3976
+ function managedMcp(previous, host) {
3977
+ const options = previous === void 0 ? {} : object.parse(previous);
3978
+ for (const key of ["command", "args", "url", "type", "headers", "http_headers", "env_http_headers", "bearer_token_env_var"]) delete options[key];
3979
+ return { ...options, ...mcpCommand(host) };
3980
+ }
3981
+ async function mcpEdit(root, host) {
3982
+ const file = mcpPath(host), before = await readText(root, file);
3983
+ if (host === "claude") {
3984
+ const config2 = before === null ? {} : object.parse(JSON.parse(before));
3985
+ const servers2 = object.parse(config2.mcpServers ?? {});
3986
+ return { path: file, before, after: JSON.stringify({ ...config2, mcpServers: { ...servers2, mason: managedMcp(servers2.mason, host) } }, null, 2) + "\n" };
3987
+ }
3988
+ const config = parse(before ?? "");
3989
+ const managed = (before ?? "").includes(TOML_START);
3990
+ const servers = config.mcp_servers;
3991
+ const desired = managedMcp(servers?.mason, host);
3992
+ let base = before ?? "";
3993
+ if (servers?.mason && !managed) {
3994
+ const headers = [...base.matchAll(/^\s*\[\[?.+?\]\]?[^\S\r\n]*(?:#.*)?$/gm)];
3995
+ const spans = headers.flatMap((header, index2) => /^\s*\[mcp_servers\.mason(?:\.[A-Za-z0-9_-]+)*\]/.test(header[0]) ? [{ start: header.index, end: headers[index2 + 1]?.index ?? base.length }] : []);
3996
+ if (!spans.length) throw new Error("Cannot safely migrate the existing Mason MCP TOML entry. Use an explicit [mcp_servers.mason] table and rerun setup; the file was retained.");
3997
+ for (const span of spans.reverse()) base = base.slice(0, span.start) + base.slice(span.end);
3998
+ }
3999
+ const after = managedBlock(base, TOML_START, TOML_END, stringify({ mcp_servers: { mason: desired } }).trimEnd());
4000
+ const parsed = parse(after);
4001
+ const expected = { ...config, mcp_servers: { ...servers ?? {}, mason: desired } };
4002
+ if (!isDeepStrictEqual(parsed, expected)) throw new Error("Could not safely configure the Mason MCP entry without changing other settings.");
4003
+ return { path: file, before, after };
4004
+ }
4005
+ async function hookEdits(root, host) {
4006
+ const file = configPath(host), before = await readText(root, file);
4007
+ const { planAutomationInstall: planAutomationInstall2 } = await Promise.resolve().then(() => (init_install(), install_exports));
4008
+ const plan = await planAutomationInstall2(root, host, hookCommand(host));
4009
+ return [
4010
+ { path: file, before, after: JSON.stringify(plan.config, null, 2) + "\n" },
4011
+ { path: ".mason/automation.json", before: await readText(root, ".mason/automation.json"), after: JSON.stringify(plan.record, null, 2) + "\n" }
4012
+ ];
4013
+ }
4014
+ async function ancillaryEdits(root) {
4015
+ const ignore = await readText(root, ".gitignore");
4016
+ const { git: git3 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
4017
+ let parentIgnored = false;
4018
+ try {
4019
+ parentIgnored = !!(await git3(root, "check-ignore", "--no-index", ".mason")).trim();
4020
+ } catch (error) {
4021
+ if (error.code !== 1) throw error;
4022
+ }
4023
+ const retainParentRule = parentIgnored || !!ignore?.replace(/\r\n/g, "\n").includes("# mason:ignore:start\n!/.mason/\n/.mason/*");
4024
+ const rules = [...retainParentRule ? ["!/.mason/", "/.mason/*"] : [], "!/.mason/decisions/", "!/.mason/decisions/**", "!/.mason/setup.json", "!/.mason/automation.json", "!/.mason/project.json", "!/.mason/run.cjs", "/.mason/reports/", "/.mason/runtime/"].join("\n");
4025
+ return [
4026
+ { path: ".gitignore", before: ignore, after: managedBlock(ignore ?? "", "# mason:ignore:start", "# mason:ignore:end", rules) },
4027
+ { path: ".mason/run.cjs", before: await readText(root, ".mason/run.cjs"), after: LAUNCHER }
4028
+ ];
4029
+ }
4030
+ async function inspectHostConfig(root, host, plannedMcp) {
4031
+ const text2 = plannedMcp ?? await readText(root, mcpPath(host));
4032
+ const config = host === "codex" ? parse(text2 ?? "") : object.parse(JSON.parse(text2 ?? "{}"));
4033
+ const servers = host === "codex" ? config.mcp_servers : config.mcpServers;
4034
+ const hooks = automationConfigSchema.parse(JSON.parse(await readText(root, configPath(host)) ?? "{}"));
4035
+ const expected = hookConfig(host, hookCommand(host));
4036
+ const mcp = servers?.mason === void 0 ? null : object.parse(servers.mason);
4037
+ return {
4038
+ mcp,
4039
+ mcpDisabled: mcp?.enabled === false,
4040
+ hooks: Object.fromEntries(Object.keys(expected.hooks).map((event) => [
4041
+ event,
4042
+ (hooks.hooks?.[event] ?? []).flatMap((group) => group.hooks.filter((handler) => handler.command === expected.hooks.SessionStart[0].hooks[0].command).map((handler) => ({ ...group, hooks: [handler] })) ?? [])
4043
+ ])),
4044
+ disabled: hooks.disableAllHooks === true || config.features?.hooks === false || config.features?.codex_hooks === false
4045
+ };
4046
+ }
4047
+ var object, mcpPath, TOML_START, TOML_END, POINTER_START, POINTER_END;
4048
+ var init_config = __esm({
4049
+ "src/setup/config.ts"() {
4050
+ "use strict";
4051
+ init_init();
4052
+ init_docs();
4053
+ init_adapters();
4054
+ init_install();
4055
+ init_files2();
4056
+ init_launcher();
4057
+ object = z15.record(z15.unknown());
4058
+ mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
4059
+ TOML_START = "# mason:mcp:start";
4060
+ TOML_END = "# mason:mcp:end";
4061
+ POINTER_START = "<!-- mason:agents:start -->";
4062
+ POINTER_END = "<!-- mason:agents:end -->";
4063
+ }
4064
+ });
4065
+
4066
+ // src/setup/runtime.ts
4067
+ import fs16 from "fs/promises";
4068
+ import path21 from "path";
4069
+ import { fileURLToPath } from "url";
4070
+ import { randomUUID as randomUUID6, createHash as createHash4 } from "crypto";
4071
+ import { execFile as execFile10 } from "child_process";
4072
+ import { promisify as promisify10 } from "util";
4073
+ async function packageRoot() {
4074
+ let directory = path21.dirname(fileURLToPath(import.meta.url));
4075
+ for (let i = 0; i < 8; i++) {
4076
+ try {
4077
+ const pkg = JSON.parse(await fs16.readFile(path21.join(directory, "package.json"), "utf8"));
4078
+ if (pkg.name === "mason-context") return directory;
4079
+ } catch {
4080
+ }
4081
+ directory = path21.dirname(directory);
4082
+ }
4083
+ throw new Error("Cannot locate the executing Mason distribution. Reinstall Mason and rerun setup.");
4084
+ }
4085
+ async function sourceRuntime() {
4086
+ const source = await packageRoot();
4087
+ const pkg = JSON.parse(await fs16.readFile(path21.join(source, "package.json"), "utf8"));
4088
+ const hashes = Object.fromEntries(await Promise.all(BINARIES.map(async (file) => [file, checksum(await fs16.readFile(path21.join(source, file)))])));
4089
+ return { source, runtime: runtimeSchema.parse({ id: hash([pkg.version, pkg.dependencies, hashes]).slice(0, 24), version: pkg.version, hashes }) };
4090
+ }
4091
+ async function verifyRuntime(root, runtime) {
4092
+ try {
4093
+ const saved = await readStoreJson(root, `.mason/runtime/${runtime.id}/receipt.json`);
4094
+ if (JSON.stringify(saved) !== JSON.stringify(runtime)) return false;
4095
+ const base = `.mason/runtime/${runtime.id}/node_modules/mason-context/`;
4096
+ const pkg = await readStoreJson(root, base + "package.json");
4097
+ if (pkg?.name !== "mason-context" || pkg.version !== runtime.version) return false;
4098
+ for (const file of BINARIES) {
4099
+ if (checksum(await fs16.readFile(await storePath(root, base + file))) !== runtime.hashes[file]) return false;
4100
+ }
4101
+ return true;
4102
+ } catch {
4103
+ return false;
4104
+ }
4105
+ }
4106
+ async function installRuntime(root, selected) {
4107
+ const { runtime, source } = selected;
4108
+ if (await verifyRuntime(root, runtime)) return runtime;
4109
+ const relative = `.mason/runtime/${runtime.id}`;
4110
+ const target = await storePath(root, relative, true);
4111
+ const stage = await storePath(root, ".mason/runtime/.install-" + randomUUID6(), true);
4112
+ await fs16.mkdir(stage);
4113
+ try {
4114
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
4115
+ const options = { timeout: 12e4, maxBuffer: 2 * 1024 * 1024, windowsHide: true };
4116
+ const packed = JSON.parse((await exec10(npm, ["pack", "--ignore-scripts", "--json", "--pack-destination", stage], { ...options, cwd: source })).stdout);
4117
+ const filename = packed[0]?.filename;
4118
+ if (typeof filename !== "string" || path21.basename(filename) !== filename) throw new Error("npm did not produce a Mason package archive.");
4119
+ await fs16.rename(path21.join(stage, filename), path21.join(stage, "mason.tgz"));
4120
+ await fs16.writeFile(path21.join(stage, "package.json"), JSON.stringify({ name: "mason-project-runtime", private: true, version: "1.0.0" }));
4121
+ await exec10(npm, ["install", "--ignore-scripts", "--omit=dev", "--no-audit", "--no-fund", "--save-exact", "./mason.tgz"], { ...options, cwd: stage });
4122
+ for (const file of BINARIES) {
4123
+ if (checksum(await fs16.readFile(path21.join(stage, "node_modules/mason-context", file))) !== runtime.hashes[file]) throw new Error("Installed Mason binary differs from the selected distribution.");
4124
+ }
4125
+ await fs16.writeFile(path21.join(stage, "receipt.json"), JSON.stringify(runtime, null, 2) + "\n");
4126
+ try {
4127
+ await fs16.rename(target, target + ".previous-" + randomUUID6());
4128
+ } catch (error) {
4129
+ if (error.code !== "ENOENT") throw error;
4130
+ }
4131
+ await fs16.rename(stage, target);
4132
+ if (!await verifyRuntime(root, runtime)) throw new Error("The installed Mason runtime could not be verified.");
4133
+ return runtime;
4134
+ } finally {
4135
+ await fs16.rm(stage, { recursive: true, force: true });
4136
+ }
4137
+ }
4138
+ var exec10, checksum, BINARIES;
4139
+ var init_runtime2 = __esm({
4140
+ "src/setup/runtime.ts"() {
4141
+ "use strict";
4142
+ init_model();
4143
+ init_storage();
4144
+ init_evidence();
4145
+ exec10 = promisify10(execFile10);
4146
+ checksum = (bytes) => createHash4("sha256").update(bytes).digest("hex");
4147
+ BINARIES = ["dist/mason-auto.js", "dist/mason-mcp.js"];
4148
+ }
4149
+ });
4150
+
4151
+ // src/setup/status.ts
4152
+ var status_exports = {};
4153
+ __export(status_exports, {
4154
+ setupStatus: () => setupStatus,
4155
+ summarizeActivation: () => summarizeActivation
4156
+ });
4157
+ import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
4158
+ async function setupStatus(dir) {
4159
+ const ws = await workspace(dir);
4160
+ const setup = await loadSetup(ws.root);
4161
+ if (!setup) {
4162
+ const partial = await Promise.all(["codex", "claude"].map(async (host) => ({
4163
+ host,
4164
+ receipt: await loadSetupReceipt(ws.root, ws.directory, host)
4165
+ })));
4166
+ const pending = partial.filter((item) => item.receipt !== null).map((item) => item.host);
4167
+ return {
4168
+ version: 1,
4169
+ status: pending.length ? "incomplete" : "not-configured",
4170
+ hosts: {},
4171
+ 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."
4172
+ };
4173
+ }
4174
+ const hosts = {};
4175
+ const launcherCurrent = await readText(ws.root, ".mason/run.cjs") === LAUNCHER;
4176
+ const automation = await automationStatus(ws.root);
4177
+ for (const host of ["codex", "claude"]) {
4178
+ const entry = setup.hosts[host];
4179
+ if (!entry) continue;
4180
+ const instructions = await instructionEdits(ws.root, host);
4181
+ const instructionsCurrent = instructions.every((edit) => edit.before === edit.after);
4182
+ const installed = await verifyRuntime(ws.root, entry.runtime);
4183
+ const config = await inspectHostConfig(ws.root, host);
4184
+ const mcp = !config.mcpDisabled && hash(config.mcp) === entry.mcpFingerprint && isDeepStrictEqual2({ command: config.mcp?.command, args: config.mcp?.args }, mcpCommand(host));
4185
+ const hooks = isDeepStrictEqual2(config.hooks, hookConfig(host, hookCommand(host)).hooks) && !config.disabled;
4186
+ const local = await loadSetupReceipt(ws.root, ws.directory, host);
4187
+ const configured = local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
4188
+ const observation = await readObservation(ws.root, ws.directory, host, entry.revision);
4189
+ const sessions = Object.values(observation?.sessions ?? {}).sort((a, b) => b.at.localeCompare(a.at));
4190
+ const complete = sessions.find((session) => events.every((event) => session.events.includes(event)));
4191
+ const observedEvents = complete?.events ?? sessions[0]?.events ?? [];
4192
+ const pending = [];
4193
+ if (!installed || !launcherCurrent) pending.push("Install this checkout's pinned runtime by rerunning setup.");
4194
+ if (!mcp || !hooks || !instructionsCurrent || !configured) pending.push("Rerun setup to reconcile project configuration; inspect any disabled host settings.");
4195
+ if (!observation?.contextCalls) pending.push("Start a new assistant session and request task context through Mason's get_context tool.");
4196
+ if (!complete) pending.push("Review/trust the host configuration, then complete a normal task to observe the full hook lifecycle.");
4197
+ if (automation.status === "unavailable") pending.push("The latest automation attempt did not establish verification. Run mason-auto check and inspect its diagnostics.");
4198
+ const healthy = installed && launcherCurrent && mcp && hooks && instructionsCurrent && configured && automation.status !== "unavailable";
4199
+ hosts[host] = {
4200
+ status: healthy ? complete && observation?.contextCalls ? "active" : "pending" : "attention",
4201
+ runtime: installed && launcherCurrent ? "installed" : "missing-or-changed",
4202
+ mcp: mcp ? "configured" : "changed",
4203
+ instructions: instructionsCurrent ? "current" : "changed",
4204
+ hookConfiguration: hooks ? "configured" : "disabled-or-changed",
4205
+ observedEvents,
4206
+ contextCalls: observation?.contextCalls ?? 0,
4207
+ verificationStatus: automation.status === "current" ? automation.verificationStatus ?? "unavailable" : "unavailable",
4208
+ pending
4209
+ };
4210
+ }
4211
+ const statuses = Object.values(hosts).map((host) => host.status);
4212
+ const decisions = await loadDecisionStore(ws.root);
4213
+ return {
4214
+ version: 1,
4215
+ status: statuses.length && statuses.every((status) => status === "active") ? "active" : statuses.includes("attention") ? "attention" : "pending",
4216
+ root: ws.root,
4217
+ hosts,
4218
+ decisionRecords: decisions.records.length,
4219
+ diagnostics: decisions.diagnostics,
4220
+ scope: "Activation receipts record observed use after setup, not complete interception, correct repairs, or measured usefulness. Host trust and higher-priority settings may prevent execution. Receipts are local to this worktree and branch."
4221
+ };
4222
+ }
4223
+ function summarizeActivation(status) {
4224
+ const lines = ["Mason setup: " + status.status + "."];
4225
+ for (const [host, state] of Object.entries(status.hosts)) {
4226
+ lines.push(
4227
+ `${host}: ${state.status}`,
4228
+ ` Runtime: ${state.runtime}; MCP: ${state.mcp}; instructions: ${state.instructions}.`,
4229
+ ` Hooks: ${state.hookConfiguration}; observed: ${state.observedEvents.join(", ") || "none"}.`,
4230
+ ` Task context requests: ${state.contextCalls}; verification: ${state.verificationStatus}.`,
4231
+ ...state.pending.map((message2) => " Next: " + message2)
4232
+ );
4233
+ }
4234
+ if ("decisionRecords" in status) lines.push(`Decision records: ${status.decisionRecords}.`);
4235
+ if (status.next) lines.push(status.next);
4236
+ return lines.join("\n");
4237
+ }
4238
+ var init_status = __esm({
4239
+ "src/setup/status.ts"() {
4240
+ "use strict";
4241
+ init_evidence();
4242
+ init_store();
4243
+ init_runtime();
4244
+ init_decisions();
4245
+ init_adapters();
4246
+ init_model();
4247
+ init_runtime2();
4248
+ init_config();
4249
+ init_launcher();
4250
+ init_files2();
4251
+ init_observations();
4252
+ }
4253
+ });
4254
+
4255
+ // src/setup/setup.ts
4256
+ var setup_exports = {};
4257
+ __export(setup_exports, {
4258
+ selectHost: () => selectHost,
4259
+ setupProject: () => setupProject,
4260
+ summarizeSetup: () => summarizeSetup
4261
+ });
4262
+ import { randomUUID as randomUUID7 } from "crypto";
4263
+ async function selectHost(root, explicit) {
4264
+ if (explicit) return explicit;
4265
+ if (process.env.MASON_SETUP_HOST === "codex" || process.env.MASON_SETUP_HOST === "claude") return process.env.MASON_SETUP_HOST;
4266
+ const found = [];
4267
+ if (await readText(root, ".codex/config.toml") !== null || await readText(root, ".codex/hooks.json") !== null) found.push("codex");
4268
+ if (await readText(root, ".claude/settings.json") !== null || await readText(root, ".mcp.json") !== null) found.push("claude");
4269
+ if (found.length === 1) return found[0];
4270
+ throw new Error("Choose the assistant for setup with --host codex or --host claude.");
4271
+ }
4272
+ async function setupProject(dir, options = {}) {
4273
+ const ws = await workspace(dir);
4274
+ const host = await selectHost(ws.root, options.host);
4275
+ return withLock(ws.root, ".mason/reports/setup-lock", async () => {
4276
+ const existing = await loadSetup(ws.root);
4277
+ const previousReceipt = await loadSetupReceipt(ws.root, ws.directory, host);
4278
+ const marker = await loadProjectMarker(ws.root);
4279
+ const instructions = await instructionEdits(ws.root, host);
4280
+ const mcp = await mcpEdit(ws.root, host);
4281
+ const hooks = await hookEdits(ws.root, host);
4282
+ const ancillary = await ancillaryEdits(ws.root);
4283
+ const edits = [...ancillary, ...instructions, mcp, ...hooks];
4284
+ const setupBefore = await readText(ws.root, ".mason/setup.json");
4285
+ const selected = await sourceRuntime();
4286
+ const mcpFingerprint = hash((await inspectHostConfig(ws.root, host, mcp.after)).mcp);
4287
+ const desiredHooks = hookConfig(host, hookCommand(host)).hooks;
4288
+ const fingerprint = hash({
4289
+ runtime: selected.runtime,
4290
+ launcher: LAUNCHER,
4291
+ mcp: mcpFingerprint,
4292
+ hooks: desiredHooks,
4293
+ instructions: instructions.map((edit) => edit.path)
4294
+ });
4295
+ const previous = existing?.hosts[host];
4296
+ const changed = edits.some((edit) => edit.before !== edit.after) || previous?.fingerprint !== fingerprint;
4297
+ const revision = !changed && previous ? previous.revision : randomUUID7();
4298
+ const setup = existing ?? { version: 1, hosts: {} };
4299
+ setup.hosts[host] = { runtime: selected.runtime, revision, fingerprint, mcpFingerprint, instructions: instructions.map((e) => e.path) };
4300
+ const initial = await automate(ws.root, { event: "turn_start" });
4301
+ const findings = await inspectOnboarding(ws.root, options.base, options.evidence);
4302
+ const receiptPath = ws.directory + "/setup-" + host + ".json";
4303
+ const initialReportPath = previousReceipt?.initialReportPath ?? initial.report.reportPath;
4304
+ const initialBaselinePaths = previousReceipt?.initialBaselinePaths ?? initial.report.baselinePaths;
4305
+ await writeStoreJson(ws.root, receiptPath, {
4306
+ version: 1,
4307
+ host,
4308
+ status: "installing",
4309
+ initialReportPath,
4310
+ initialBaselinePaths,
4311
+ root: ws.root,
4312
+ revision
4313
+ });
4314
+ await installRuntime(ws.root, selected);
4315
+ const changedFiles = [];
4316
+ for (const edit of edits) if (await applyEdit(ws.root, edit)) changedFiles.push(edit.path);
4317
+ if (await applyEdit(ws.root, { path: ".mason/setup.json", before: setupBefore, after: JSON.stringify(setup, null, 2) + "\n" })) changedFiles.push(".mason/setup.json");
4318
+ if (!marker) {
4319
+ await saveProjectMarker(ws.root, { version: 1, initializedAt: (/* @__PURE__ */ new Date()).toISOString() });
4320
+ changedFiles.push(".mason/project.json");
4321
+ }
4322
+ const checked = await automate(ws.root, { event: "turn_start" });
4323
+ const configured = await inspectHostConfig(ws.root, host);
4324
+ await writeStoreJson(ws.root, receiptPath, {
4325
+ version: 1,
4326
+ host,
4327
+ status: "configured",
4328
+ initialReportPath,
4329
+ initialBaselinePaths,
4330
+ root: ws.root,
4331
+ revision,
4332
+ configuredAt: (/* @__PURE__ */ new Date()).toISOString()
4333
+ });
4334
+ return {
4335
+ version: 1,
4336
+ action: "setup",
4337
+ status: "configured",
4338
+ host,
4339
+ root: ws.root,
4340
+ runtime: selected.runtime,
4341
+ changedFiles,
4342
+ initialReportPath,
4343
+ findings,
4344
+ reportPath: checked.report.reportPath,
4345
+ activation: await setupStatus(ws.root),
4346
+ next: configured.disabled || configured.mcpDisabled ? "Mason hooks or MCP are disabled in project configuration. Review that setting before activation." : host === "codex" ? "Review/trust this project's MCP configuration and hooks in Codex (/hooks in the CLI), then start a new session and give it a normal task." : "Approve the project MCP server in Claude Code, then start a new session and give it a normal task."
4347
+ };
4348
+ });
4349
+ }
4350
+ function summarizeSetup(result) {
4351
+ const audit = result.findings.audit;
4352
+ return [
4353
+ `Mason configured for ${result.host}.`,
4354
+ " Runtime installed; MCP server and lifecycle hooks configured.",
4355
+ " Project instructions updated; original audit evidence retained.",
4356
+ ` ${result.changedFiles.length} shared files changed.`,
4357
+ `Initial audit: ${audit.status}${"counts" in audit ? `; ${audit.counts.issues} issues, ${audit.counts.advisories + audit.counts.suppressedAdvisories} advisories` : ""}.`,
4358
+ ..."issues" in audit ? audit.issues.slice(0, 3).map((f) => " " + f.message) : [],
4359
+ `Original evidence: ${result.initialReportPath}`,
4360
+ "Activation: " + result.activation.status + ".",
4361
+ result.next,
4362
+ "After the task finishes, run mason-auto status. Configuration alone does not establish activation."
4363
+ ].join("\n");
4364
+ }
4365
+ var init_setup = __esm({
4366
+ "src/setup/setup.ts"() {
4367
+ "use strict";
4368
+ init_evidence();
4369
+ init_store();
4370
+ init_runtime();
4371
+ init_init();
4372
+ init_onboarding();
4373
+ init_storage();
4374
+ init_files2();
4375
+ init_config();
4376
+ init_launcher();
4377
+ init_adapters();
4378
+ init_model();
4379
+ init_runtime2();
4380
+ init_status();
4381
+ }
4382
+ });
2207
4383
 
2208
4384
  // src/automation/cli.ts
2209
- var USAGE = `Usage: mason-auto <install|config|status|check|hook> [options]
4385
+ init_execution();
4386
+ init_runtime();
4387
+ init_adapters();
4388
+ init_install();
4389
+ init_store();
4390
+ import { parseArgs } from "util";
4391
+ var USAGE = `Usage: mason-auto <setup|install|config|status|check|hook> [options]
2210
4392
 
4393
+ setup [--host claude|codex] Install a pinned runtime, MCP, instructions and hooks; retain the initial audit
2211
4394
  install --host claude|codex Merge lifecycle hooks into this project's host config
2212
4395
  config --host claude|codex Print the host config without writing
2213
4396
  status Read configured hooks and observed runtime events
@@ -2216,35 +4399,52 @@ var USAGE = `Usage: mason-auto <install|config|status|check|hook> [options]
2216
4399
 
2217
4400
  --dir <path> Project directory (defaults to cwd)
2218
4401
  --command <prefix> Installed executable prefix for install/config
2219
- --json Machine-readable check output (status always uses JSON)
4402
+ --json Machine-readable output (status also uses JSON when piped)
2220
4403
 
2221
4404
  check exits 0 for verified checks, 1 for issues, 2 for incomplete/unavailable.
2222
4405
  Hooks are advisory and exit 0; a failed capture is reported explicitly.
2223
4406
  Local evidence is written under .mason/reports/. No LLM calls or source edits.`;
4407
+ var parseCli = (argv2) => parseArgs({ args: argv2, allowPositionals: true, options: {
4408
+ dir: { type: "string" },
4409
+ host: { type: "string" },
4410
+ command: { type: "string" },
4411
+ json: { type: "boolean" },
4412
+ help: { type: "boolean", short: "h" }
4413
+ } });
4414
+ function isHookCommand(argv2) {
4415
+ try {
4416
+ const parsed = parseCli(argv2);
4417
+ return parsed.positionals[0] === "hook" && !parsed.values.help;
4418
+ } catch {
4419
+ return false;
4420
+ }
4421
+ }
2224
4422
  async function runAutomationCli(argv2, stdin = "", io = {
2225
4423
  out: (s) => process.stdout.write(s + "\n"),
2226
4424
  err: (s) => process.stderr.write(s + "\n")
2227
4425
  }) {
4426
+ let action = "";
2228
4427
  try {
2229
- const { values, positionals } = parseArgs({ args: argv2, allowPositionals: true, options: {
2230
- dir: { type: "string" },
2231
- host: { type: "string" },
2232
- command: { type: "string" },
2233
- json: { type: "boolean" },
2234
- help: { type: "boolean", short: "h" }
2235
- } });
4428
+ const { values, positionals } = parseCli(argv2);
2236
4429
  if (values.help || !positionals.length) {
2237
4430
  io.out(USAGE);
2238
4431
  return 0;
2239
4432
  }
2240
4433
  if (positionals.length !== 1) throw new Error("Expected one command.");
2241
- const [action] = positionals;
4434
+ [action] = positionals;
2242
4435
  const dir = values.dir ?? process.cwd();
2243
4436
  if (action === "hook") {
2244
4437
  const output = await runAutomationHook(hostSchema.parse(values.host), stdin);
2245
4438
  if (output) io.out(JSON.stringify(output));
2246
4439
  return 0;
2247
4440
  }
4441
+ if (action === "setup") {
4442
+ if (values.command) throw new Error("--command applies to install/config, not managed setup.");
4443
+ const { setupProject: setupProject2, summarizeSetup: summarizeSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
4444
+ const result = await setupProject2(dir, { host: values.host ? hostSchema.parse(values.host) : void 0 });
4445
+ io.out(values.json ? JSON.stringify(result, null, 2) : summarizeSetup2(result));
4446
+ return 0;
4447
+ }
2248
4448
  if (action === "install" || action === "config") {
2249
4449
  const host = hostSchema.parse(values.host);
2250
4450
  io.out(JSON.stringify(action === "install" ? await installAutomation(dir, host, values.command) : hookConfig(host, values.command), null, 2));
@@ -2252,7 +4452,10 @@ async function runAutomationCli(argv2, stdin = "", io = {
2252
4452
  }
2253
4453
  if (values.host || values.command) throw new Error("--host and --command apply only to install/config/hook.");
2254
4454
  if (action === "status") {
2255
- io.out(JSON.stringify({ ...await automationStatus(dir), configured: await installedAutomation(dir) }, null, 2));
4455
+ const { setupStatus: setupStatus2, summarizeActivation: summarizeActivation2 } = await Promise.resolve().then(() => (init_status(), status_exports));
4456
+ const setup = await setupStatus2(dir);
4457
+ const result = { ...await automationStatus(dir), configured: await installedAutomation(dir), setup };
4458
+ io.out(values.json || !process.stdout.isTTY ? JSON.stringify(result, null, 2) : summarizeActivation2(setup));
2256
4459
  return 0;
2257
4460
  }
2258
4461
  if (action !== "check") throw new Error("Unknown automation command: " + action);
@@ -2260,12 +4463,13 @@ async function runAutomationCli(argv2, stdin = "", io = {
2260
4463
  io.out(values.json ? JSON.stringify(report, null, 2) : summarize(report));
2261
4464
  return report.status === "verified" ? 0 : report.status === "issues-remain" ? 1 : 2;
2262
4465
  } catch (error) {
2263
- const message = "Mason automation unavailable: " + (error instanceof Error ? error.message : String(error));
2264
- if (argv2.includes("hook")) {
2265
- io.out(JSON.stringify({ systemMessage: message }));
4466
+ const message2 = action === "setup" ? "Mason setup incomplete: " + automationFailure(error).message + ". Rerun the same setup command to resume; retained audit evidence is preserved." : failureMessage(error);
4467
+ if (action === "hook" || isHookCommand(argv2)) {
4468
+ io.out(JSON.stringify({ systemMessage: message2 }));
2266
4469
  return 0;
2267
4470
  }
2268
- io.err(message);
4471
+ if (argv2.includes("--json")) io.out(JSON.stringify({ version: 1, ...action === "setup" ? { action, status: "incomplete", next: "Rerun the same setup command to resume." } : { status: "unavailable" }, failure: automationFailure(error) }));
4472
+ else io.err(message2);
2269
4473
  return 2;
2270
4474
  }
2271
4475
  }
@@ -2273,7 +4477,7 @@ async function runAutomationCli(argv2, stdin = "", io = {
2273
4477
  // bin/mason-auto.ts
2274
4478
  var argv = process.argv.slice(2);
2275
4479
  var input = "";
2276
- if (argv.includes("hook") && !argv.some((a) => a === "--help" || a === "-h") && !process.stdin.isTTY) {
4480
+ if (isHookCommand(argv) && !process.stdin.isTTY) {
2277
4481
  for await (const chunk of process.stdin) {
2278
4482
  input += chunk.toString();
2279
4483
  if (Buffer.byteLength(input) > 1024 * 1024) break;