mason-context 0.11.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.
@@ -0,0 +1,4487 @@
1
+ #!/usr/bin/env node
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
+ };
11
+
12
+ // src/utils/paths.ts
13
+ import path from "path";
14
+ function normalizeRepoPath(value) {
15
+ const slash = value.replace(/\\/g, "/");
16
+ if (!slash || slash.includes("\0") || path.posix.isAbsolute(slash) || /^[A-Za-z]:/.test(slash)) return null;
17
+ if (slash.split("/").includes("..")) return null;
18
+ const normalized = path.posix.normalize(slash).replace(/\/$/, "");
19
+ return normalized === "." ? null : normalized;
20
+ }
21
+ function isWithinRoot(root, candidate) {
22
+ const relative = path.relative(root, candidate);
23
+ return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
24
+ }
25
+ function anchorMatches(anchor, file) {
26
+ const a = normalizeRepoPath(anchor);
27
+ const f = normalizeRepoPath(file);
28
+ return a !== null && f !== null && (a === f || f.startsWith(`${a}/`));
29
+ }
30
+ function matchingPaths(anchors, files) {
31
+ return [...new Set(files)].filter((file) => anchors.some((anchor) => anchorMatches(anchor, file)));
32
+ }
33
+ var init_paths = __esm({
34
+ "src/utils/paths.ts"() {
35
+ "use strict";
36
+ }
37
+ });
38
+
39
+ // src/utils/files.ts
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
+ }
51
+ async function readBoundedFile(file, maxBytes) {
52
+ const handle = await fs.open(file, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
53
+ try {
54
+ const stat = await handle.stat();
55
+ if (!stat.isFile() || stat.size > maxBytes) return null;
56
+ const buffer = Buffer.alloc(Math.min(maxBytes + 1, stat.size + 1));
57
+ let bytes = 0;
58
+ while (bytes < buffer.length) {
59
+ const result = await handle.read(buffer, bytes, buffer.length - bytes, null);
60
+ if (result.bytesRead === 0) break;
61
+ bytes += result.bytesRead;
62
+ }
63
+ return bytes === buffer.length ? null : buffer.subarray(0, bytes).toString("utf8");
64
+ } finally {
65
+ await handle.close();
66
+ }
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
+ });
180
+
181
+ // src/utils/storage.ts
182
+ import fs2 from "fs/promises";
183
+ import path3 from "path";
184
+ import { randomUUID } from "crypto";
185
+ async function storePath(root, relative, createParents = false) {
186
+ const normalized = normalizeRepoPath(relative);
187
+ if (!normalized) throw new Error(`Invalid store path: ${relative}`);
188
+ let current = await fs2.realpath(root);
189
+ const parts = normalized.split("/");
190
+ for (let i = 0; i < parts.length; i++) {
191
+ current = path3.join(current, parts[i]);
192
+ let stat;
193
+ try {
194
+ stat = await fs2.lstat(current);
195
+ } catch (error) {
196
+ if (error.code !== "ENOENT") throw error;
197
+ if (createParents && i < parts.length - 1) {
198
+ try {
199
+ await fs2.mkdir(current);
200
+ } catch (mkdirError) {
201
+ if (mkdirError.code !== "EEXIST") throw mkdirError;
202
+ }
203
+ stat = await fs2.lstat(current);
204
+ }
205
+ }
206
+ if (stat?.isSymbolicLink()) throw new Error(`Symlink in store path: ${relative}`);
207
+ }
208
+ return current;
209
+ }
210
+ async function readStoreJson(root, relative) {
211
+ try {
212
+ const file = await storePath(root, relative);
213
+ const raw = await readBoundedFile(file, 10 * 1024 * 1024);
214
+ if (raw === null) throw new Error("file is not regular or exceeds 10 MiB");
215
+ const parsed = JSON.parse(raw);
216
+ if (parsed === null) throw new Error("expected a JSON object, received null");
217
+ return parsed;
218
+ } catch (error) {
219
+ if (error.code === "ENOENT") return null;
220
+ throw new Error(`Invalid Mason store ${relative}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
221
+ }
222
+ }
223
+ async function writeStoreJson(root, relative, value) {
224
+ const payload = JSON.stringify(value, null, 2) + "\n";
225
+ if (Buffer.byteLength(payload) > 10 * 1024 * 1024) {
226
+ throw new Error(`Mason store ${relative} exceeds 10 MiB`);
227
+ }
228
+ const file = await storePath(root, relative, true);
229
+ const temporary = path3.join(path3.dirname(file), `.${path3.basename(file)}.${randomUUID()}.tmp`);
230
+ try {
231
+ const handle = await fs2.open(temporary, "wx", 384);
232
+ try {
233
+ await handle.writeFile(payload, "utf8");
234
+ await handle.sync();
235
+ } finally {
236
+ await handle.close();
237
+ }
238
+ await fs2.rename(temporary, file);
239
+ } finally {
240
+ await fs2.rm(temporary, { force: true });
241
+ }
242
+ }
243
+ var init_storage = __esm({
244
+ "src/utils/storage.ts"() {
245
+ "use strict";
246
+ init_paths();
247
+ init_files();
248
+ }
249
+ });
250
+
251
+ // src/automation/execution.ts
252
+ import os from "os";
253
+ import { randomUUID as randomUUID2 } from "crypto";
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
+ });
374
+
375
+ // src/test-map.ts
376
+ import path4 from "path";
377
+ async function buildTestMap(dir) {
378
+ const rootDir = path4.resolve(dir);
379
+ const access = await createFileAccess(rootDir);
380
+ const testPatterns = [
381
+ "**/*.test.*",
382
+ "**/*.spec.*",
383
+ "**/*Test.kt",
384
+ "**/*Test.java",
385
+ "**/*Tests.kt",
386
+ "**/*Tests.java",
387
+ "**/test_*.py",
388
+ "**/*_test.py",
389
+ "**/*_test.go",
390
+ "**/*Tests.swift",
391
+ "**/*Test.swift",
392
+ "**/*_test.rs"
393
+ ];
394
+ const testFiles = await access.list(testPatterns);
395
+ const sourceFiles = await access.list();
396
+ const sourceByBaseName = /* @__PURE__ */ new Map();
397
+ for (const file of sourceFiles) {
398
+ if (testFiles.includes(file)) continue;
399
+ const baseName = path4.basename(file).replace(/\.[^.]+$/, "");
400
+ const existing = sourceByBaseName.get(baseName) ?? [];
401
+ existing.push(file);
402
+ sourceByBaseName.set(baseName, existing);
403
+ }
404
+ const paired = [];
405
+ const unmatched = [];
406
+ for (const testFile of testFiles) {
407
+ const testBaseName = path4.basename(testFile).replace(/\.[^.]+$/, "");
408
+ const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
409
+ if (!sourceName) {
410
+ unmatched.push(testFile);
411
+ continue;
412
+ }
413
+ const candidates = sourceByBaseName.get(sourceName);
414
+ if (candidates && candidates.length > 0) {
415
+ const testDir = path4.dirname(testFile);
416
+ const bestMatch = candidates.reduce((best, candidate) => {
417
+ const candidateDir = path4.dirname(candidate);
418
+ const bestDir = path4.dirname(best);
419
+ const candidateOverlap = commonSegments(testDir, candidateDir);
420
+ const bestOverlap = commonSegments(testDir, bestDir);
421
+ return candidateOverlap > bestOverlap ? candidate : best;
422
+ });
423
+ paired.push({
424
+ test: testFile,
425
+ source: bestMatch,
426
+ confidence: candidates.length === 1 ? "exact" : "best-guess"
427
+ });
428
+ } else {
429
+ unmatched.push(testFile);
430
+ }
431
+ }
432
+ return { totalTestFiles: testFiles.length, paired, unmatched };
433
+ }
434
+ function commonSegments(pathA, pathB) {
435
+ const segsA = pathA.split("/");
436
+ const segsB = pathB.split("/");
437
+ let count3 = 0;
438
+ for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
439
+ if (segsA[i] === segsB[i]) count3++;
440
+ else break;
441
+ }
442
+ return count3;
443
+ }
444
+ var init_test_map = __esm({
445
+ "src/test-map.ts"() {
446
+ "use strict";
447
+ init_files();
448
+ }
449
+ });
450
+
451
+ // src/snapshot/snapshot.ts
452
+ import path5 from "path";
453
+ import { execFile as execFile2 } from "child_process";
454
+ import { promisify as promisify2 } from "util";
455
+ import { z as z2 } from "zod";
456
+ async function inspectSnapshot(rootDir) {
457
+ try {
458
+ const raw = await readStoreJson(rootDir, ".mason/snapshot.json");
459
+ const snapshot = raw === null ? null : snapshotSchema.parse(raw);
460
+ return { status: snapshot ? "available" : "missing", snapshot, diagnostics: [] };
461
+ } catch (error) {
462
+ return { status: "invalid", snapshot: null, diagnostics: [{
463
+ path: ".mason/snapshot.json",
464
+ message: error instanceof Error ? error.message : String(error)
465
+ }] };
466
+ }
467
+ }
468
+ async function getCurrentGitHash(rootDir) {
469
+ try {
470
+ const { stdout } = await exec2("git", ["rev-parse", "HEAD"], {
471
+ cwd: rootDir
472
+ });
473
+ return stdout.trim();
474
+ } catch {
475
+ return "unknown";
476
+ }
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
+ });
514
+
515
+ // src/drift/drift.ts
516
+ import fs3 from "fs/promises";
517
+ import path6 from "path";
518
+ import { execFile as execFile3 } from "child_process";
519
+ import { promisify as promisify3 } from "util";
520
+ function parseChanges(output) {
521
+ const fields = output.split("\0");
522
+ const changes = [];
523
+ for (let i = 0; i < fields.length && fields[i]; ) {
524
+ const code = fields[i++];
525
+ const first = fields[i++];
526
+ if (!first) break;
527
+ const second = /^[RC]/.test(code) ? fields[i++] : void 0;
528
+ const change = second ? code.startsWith("R") ? { status: "renamed", path: second, previousPath: first } : { status: "added", path: second } : { status: code === "A" ? "added" : code === "D" ? "deleted" : "modified", path: first };
529
+ if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/"))) continue;
530
+ changes.push(change);
531
+ }
532
+ return changes;
533
+ }
534
+ function touchedPaths(changes) {
535
+ return [...new Set(changes.flatMap((c) => c.previousPath ? [c.previousPath, c.path] : [c.path]))].sort();
536
+ }
537
+ async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
538
+ if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-")) return null;
539
+ try {
540
+ const { stdout } = await exec3("git", ["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 });
541
+ return parseChanges(stdout);
542
+ } catch {
543
+ return null;
544
+ }
545
+ }
546
+ async function getWorkingTree(resolvedRoot) {
547
+ try {
548
+ const [diff, untracked] = await Promise.all([
549
+ exec3("git", ["diff", "--name-status", "-z", "-M", "HEAD", "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }),
550
+ exec3("git", ["ls-files", "-z", "--others", "--exclude-standard"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 })
551
+ ]);
552
+ const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
553
+ return { available: true, changedFiles: [.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])].sort(), untrackedFiles };
554
+ } catch {
555
+ return { available: false, changedFiles: [], untrackedFiles: [] };
556
+ }
557
+ }
558
+ var exec3;
559
+ var init_drift = __esm({
560
+ "src/drift/drift.ts"() {
561
+ "use strict";
562
+ init_snapshot();
563
+ init_paths();
564
+ exec3 = promisify3(execFile3);
565
+ }
566
+ });
567
+
568
+ // src/audit/tree.ts
569
+ function glyphIndex(line) {
570
+ for (const glyph of GLYPHS) {
571
+ const idx = line.indexOf(glyph);
572
+ if (idx !== -1) return idx;
573
+ }
574
+ return -1;
575
+ }
576
+ function isSpacerLine(line) {
577
+ return /^[\s│|]*$/.test(line);
578
+ }
579
+ function entryName(afterGlyph) {
580
+ let name = afterGlyph.replace(/^\s+/, "");
581
+ const hash2 = name.search(/\s+#/);
582
+ if (hash2 !== -1) name = name.slice(0, hash2);
583
+ const columns = name.search(/\s{2,}/);
584
+ if (columns !== -1) name = name.slice(0, columns);
585
+ name = name.trim();
586
+ if (!name || /\s/.test(name)) return null;
587
+ return name;
588
+ }
589
+ function extractTreeClaims(blockLines, blockStartLine) {
590
+ const glyphLines = blockLines.filter((l) => glyphIndex(l) !== -1).length;
591
+ if (glyphLines < MIN_GLYPH_LINES) return [];
592
+ const claims = [];
593
+ const stack = [];
594
+ let rootPrefix = "";
595
+ let started = false;
596
+ for (let i = 0; i < blockLines.length; i++) {
597
+ const line = blockLines[i];
598
+ const col = glyphIndex(line);
599
+ if (col === -1) {
600
+ if (isSpacerLine(line)) continue;
601
+ if (!started) {
602
+ const candidate = line.trim();
603
+ if (candidate.endsWith("/") && !/\s/.test(candidate)) {
604
+ rootPrefix = candidate.replace(/\/+$/, "");
605
+ claims.push({
606
+ path: rootPrefix,
607
+ line: blockStartLine + i,
608
+ excerpt: candidate
609
+ });
610
+ }
611
+ continue;
612
+ }
613
+ return claims;
614
+ }
615
+ started = true;
616
+ const name = entryName(line.slice(col + GLYPHS[0].length));
617
+ if (name === null) return claims;
618
+ while (stack.length > 0 && stack[stack.length - 1].col >= col) {
619
+ stack.pop();
620
+ }
621
+ const isDir = name.endsWith("/");
622
+ const cleanName = name.replace(/\/+$/, "");
623
+ const segments = [
624
+ ...rootPrefix ? [rootPrefix] : [],
625
+ ...stack.map((s) => s.name),
626
+ cleanName
627
+ ];
628
+ claims.push({
629
+ path: segments.join("/"),
630
+ line: blockStartLine + i,
631
+ excerpt: name
632
+ });
633
+ if (isDir) stack.push({ col, name: cleanName });
634
+ }
635
+ return claims;
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
+ });
645
+
646
+ // src/audit/claims.ts
647
+ function normalizePathToken(token) {
648
+ let t = token.trim();
649
+ if (!t) return null;
650
+ if (/\s/.test(t)) return null;
651
+ if (t.includes("://") || t.includes("\\")) return null;
652
+ if (/[*?[\]{}<>$`]/.test(t)) return null;
653
+ if (t.startsWith("/") || t.startsWith("~") || t.startsWith("./") || t.startsWith("../")) {
654
+ return null;
655
+ }
656
+ t = t.replace(/:\d+(?:-\d+)?$/, "");
657
+ if (t.includes(":")) return null;
658
+ const normalized = t.replace(/\/+$/, "");
659
+ if (!normalized) return null;
660
+ if (normalized.split("/").some((seg) => /^\.+$/.test(seg))) return null;
661
+ if (normalized.includes("/")) return normalized;
662
+ return ROOT_FILE_NAMES.has(normalized) ? normalized : null;
663
+ }
664
+ function exactTokenPath(line) {
665
+ const trimmed = line.trim();
666
+ if (!trimmed || /\s/.test(trimmed) || !trimmed.includes("/")) return null;
667
+ return normalizePathToken(trimmed);
668
+ }
669
+ function computeIgnoredLines(lines) {
670
+ const ignored = new Array(lines.length).fill(false);
671
+ let inRegion = false;
672
+ let ignoreNext = false;
673
+ for (let i = 0; i < lines.length; i++) {
674
+ const line = lines[i];
675
+ if (line.includes(IGNORE_START)) {
676
+ inRegion = true;
677
+ ignored[i] = true;
678
+ continue;
679
+ }
680
+ if (line.includes(IGNORE_END)) {
681
+ inRegion = false;
682
+ ignored[i] = true;
683
+ continue;
684
+ }
685
+ if (inRegion) {
686
+ ignored[i] = true;
687
+ continue;
688
+ }
689
+ if (ignoreNext) {
690
+ if (line.trim().length === 0) continue;
691
+ ignored[i] = true;
692
+ ignoreNext = false;
693
+ continue;
694
+ }
695
+ if (line.includes(IGNORE_LINE)) {
696
+ ignored[i] = true;
697
+ const rest = line.replace(IGNORE_LINE, "").trim();
698
+ if (rest.length === 0) ignoreNext = true;
699
+ }
700
+ }
701
+ return ignored;
702
+ }
703
+ function extractClaims(content2) {
704
+ const lines = content2.split("\n");
705
+ const ignored = computeIgnoredLines(lines);
706
+ const paths = /* @__PURE__ */ new Map();
707
+ const counts = [];
708
+ const commands = /* @__PURE__ */ new Map();
709
+ const addPath = (claim) => {
710
+ if (!paths.has(claim.path)) paths.set(claim.path, claim);
711
+ };
712
+ const addCommand = (claim) => {
713
+ if (!commands.has(claim.scriptName)) commands.set(claim.scriptName, claim);
714
+ };
715
+ let inFence = false;
716
+ let fenceInfo = "";
717
+ let fenceMarker = "";
718
+ let blockLines = [];
719
+ let blockStartLine = 0;
720
+ const processBlock = () => {
721
+ for (const claim of extractTreeClaims(blockLines, blockStartLine)) {
722
+ addPath(claim);
723
+ }
724
+ for (let i = 0; i < blockLines.length; i++) {
725
+ const exact = exactTokenPath(blockLines[i]);
726
+ if (exact) {
727
+ addPath({
728
+ path: exact,
729
+ line: blockStartLine + i,
730
+ excerpt: blockLines[i].trim()
731
+ });
732
+ }
733
+ }
734
+ };
735
+ for (let i = 0; i < lines.length; i++) {
736
+ const line = lines[i];
737
+ const lineNo = i + 1;
738
+ const fenceMatch = line.match(/^\s*(```+|~~~+)(.*)$/);
739
+ if (fenceMatch) {
740
+ if (!inFence) {
741
+ inFence = true;
742
+ fenceMarker = fenceMatch[1][0];
743
+ fenceInfo = fenceMatch[2].trim().toLowerCase();
744
+ blockLines = [];
745
+ blockStartLine = lineNo + 1;
746
+ } else if (fenceMatch[1][0] === fenceMarker) {
747
+ inFence = false;
748
+ processBlock();
749
+ }
750
+ continue;
751
+ }
752
+ if (inFence) {
753
+ blockLines.push(ignored[i] ? "" : line);
754
+ if (!ignored[i] && SHELL_FENCE_INFOS.has(fenceInfo)) {
755
+ for (const m of line.matchAll(COMMAND_RE)) {
756
+ addCommand({
757
+ scriptName: m[2],
758
+ invocation: m[0],
759
+ line: lineNo,
760
+ excerpt: m[0]
761
+ });
762
+ }
763
+ }
764
+ continue;
765
+ }
766
+ if (ignored[i]) continue;
767
+ for (const m of line.matchAll(/`([^`]+)`/g)) {
768
+ const normalized = normalizePathToken(m[1]);
769
+ if (normalized) {
770
+ addPath({ path: normalized, line: lineNo, excerpt: m[1] });
771
+ }
772
+ }
773
+ for (const m of line.matchAll(/"([A-Za-z][\w.@-]*(?:\/[\w.@-]+)+\/?)"/g)) {
774
+ const normalized = normalizePathToken(m[1]);
775
+ if (normalized) {
776
+ addPath({ path: normalized, line: lineNo, excerpt: m[1] });
777
+ }
778
+ }
779
+ for (const m of line.matchAll(COUNT_RE)) {
780
+ const rest = line.slice((m.index ?? 0) + m[0].length);
781
+ if (COUNT_DENYLIST_RE.test(rest)) continue;
782
+ counts.push({
783
+ count: Number.parseInt(m[1], 10),
784
+ unit: m[2].toLowerCase(),
785
+ line: lineNo,
786
+ excerpt: m[0]
787
+ });
788
+ }
789
+ for (const m of line.matchAll(COMMAND_RE)) {
790
+ addCommand({
791
+ scriptName: m[2],
792
+ invocation: m[0],
793
+ line: lineNo,
794
+ excerpt: m[0]
795
+ });
796
+ }
797
+ }
798
+ if (inFence) processBlock();
799
+ return {
800
+ paths: [...paths.values()],
801
+ counts,
802
+ commands: [...commands.values()]
803
+ };
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
+ });
850
+
851
+ // src/audit/git.ts
852
+ import { execFile as execFile4 } from "child_process";
853
+ import { promisify as promisify4 } from "util";
854
+ function parseCommitLine(line) {
855
+ const parts = line.split(" ");
856
+ if (parts.length < 3 || !parts[0]) return null;
857
+ return { hash: parts[0], date: parts[1], subject: parts.slice(2).join(" ") };
858
+ }
859
+ async function lastCommitOf(resolvedRoot, relPath) {
860
+ try {
861
+ const { stdout } = await exec4(
862
+ "git",
863
+ ["log", "-1", `--format=${COMMIT_FORMAT}`, "--", relPath],
864
+ { cwd: resolvedRoot }
865
+ );
866
+ const line = stdout.trim().split("\n")[0];
867
+ return line ? parseCommitLine(line) : null;
868
+ } catch {
869
+ return null;
870
+ }
871
+ }
872
+ async function deletingCommitOf(resolvedRoot, relPath) {
873
+ try {
874
+ const { stdout } = await exec4(
875
+ "git",
876
+ [
877
+ "log",
878
+ "-1",
879
+ "--diff-filter=D",
880
+ `--format=${COMMIT_FORMAT}`,
881
+ "--",
882
+ relPath
883
+ ],
884
+ { cwd: resolvedRoot }
885
+ );
886
+ const line = stdout.trim().split("\n")[0];
887
+ return line ? parseCommitLine(line) : null;
888
+ } catch {
889
+ return null;
890
+ }
891
+ }
892
+ async function firstCommitOf(resolvedRoot, relPath) {
893
+ try {
894
+ const { stdout } = await exec4(
895
+ "git",
896
+ ["log", "--reverse", `--format=${COMMIT_FORMAT}`, "--", relPath],
897
+ { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
898
+ );
899
+ const line = stdout.trim().split("\n")[0];
900
+ return line ? parseCommitLine(line) : null;
901
+ } catch {
902
+ return null;
903
+ }
904
+ }
905
+ async function commitsTouchingSince(resolvedRoot, fromHash, pathspecs) {
906
+ if (!fromHash || fromHash === "unknown") return null;
907
+ try {
908
+ const { stdout } = await exec4(
909
+ "git",
910
+ [
911
+ "log",
912
+ `${fromHash}..HEAD`,
913
+ `--format=%x01${COMMIT_FORMAT}`,
914
+ "--name-only",
915
+ "--",
916
+ ...pathspecs
917
+ ],
918
+ { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
919
+ );
920
+ const commits = [];
921
+ for (const block of stdout.split("")) {
922
+ if (!block.trim()) continue;
923
+ const lines = block.split("\n").filter((l) => l.trim().length > 0);
924
+ const ref = parseCommitLine(lines[0]);
925
+ if (!ref) continue;
926
+ commits.push({ ...ref, files: lines.slice(1).map((l) => l.trim()) });
927
+ }
928
+ return { commits, total: commits.length };
929
+ } catch {
930
+ return null;
931
+ }
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
+ });
941
+
942
+ // src/audit/docs.ts
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";
947
+ async function isDirty(resolvedRoot, relPath) {
948
+ try {
949
+ const { stdout } = await exec5(
950
+ "git",
951
+ ["status", "--porcelain", "--", relPath],
952
+ { cwd: resolvedRoot }
953
+ );
954
+ return stdout.trim().length > 0;
955
+ } catch {
956
+ return false;
957
+ }
958
+ }
959
+ async function discoverDocs(resolvedRoot) {
960
+ const docs = await Promise.all(DOC_CANDIDATES.map(async (candidate) => {
961
+ let content2;
962
+ try {
963
+ content2 = await fs4.readFile(path7.join(resolvedRoot, candidate), "utf-8");
964
+ } catch {
965
+ return null;
966
+ }
967
+ const [lastCommit, dirty] = await Promise.all([lastCommitOf(resolvedRoot, candidate), isDirty(resolvedRoot, candidate)]);
968
+ return {
969
+ path: candidate,
970
+ content: content2,
971
+ lineCount: content2.split("\n").length,
972
+ lastCommit,
973
+ dirty,
974
+ claims: extractClaims(content2)
975
+ };
976
+ }));
977
+ return docs.filter((doc) => doc !== null);
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
+ });
993
+
994
+ // src/audit/types.ts
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
+ });
1009
+
1010
+ // src/audit/checks/deleted-reference.ts
1011
+ import fs5 from "fs/promises";
1012
+ import path8 from "path";
1013
+ async function exists(absPath) {
1014
+ try {
1015
+ await fs5.access(absPath);
1016
+ return true;
1017
+ } catch {
1018
+ return false;
1019
+ }
1020
+ }
1021
+ async function checkDeletedReferences(ctx) {
1022
+ const result = emptyResult();
1023
+ for (const doc of ctx.docs) {
1024
+ const changes = ctx.changesSinceDoc.get(doc.path);
1025
+ const renames = /* @__PURE__ */ new Map();
1026
+ for (const change of changes ?? []) {
1027
+ if (change.status === "renamed" && change.previousPath) {
1028
+ renames.set(change.previousPath, change.path);
1029
+ }
1030
+ }
1031
+ for (const claim of doc.claims.paths) {
1032
+ if (claim.path === ".mason" || claim.path.startsWith(".mason/")) {
1033
+ continue;
1034
+ }
1035
+ if (await exists(path8.join(ctx.root, claim.path))) continue;
1036
+ const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };
1037
+ const renamedTo = renames.get(claim.path) ?? null;
1038
+ if (renamedTo) {
1039
+ result.issues.push({
1040
+ type: "deleted-reference",
1041
+ message: `\`${claim.path}\` was renamed to \`${renamedTo}\``,
1042
+ anchor,
1043
+ confidence: "certain",
1044
+ evidence: {
1045
+ kind: "missing-path",
1046
+ claimed: claim.path,
1047
+ renamedTo,
1048
+ deletedInCommit: null,
1049
+ everTracked: true,
1050
+ parentDirExists: true
1051
+ }
1052
+ });
1053
+ continue;
1054
+ }
1055
+ const tracked = await lastCommitOf(ctx.root, claim.path);
1056
+ if (tracked) {
1057
+ const deleted = await deletingCommitOf(ctx.root, claim.path);
1058
+ const detail = deleted ? ` \u2013 deleted in ${deleted.hash.slice(0, 7)} "${deleted.subject}" (${deleted.date.slice(0, 10)})` : "";
1059
+ result.issues.push({
1060
+ type: "deleted-reference",
1061
+ message: `\`${claim.path}\` no longer exists${detail}`,
1062
+ anchor,
1063
+ confidence: "certain",
1064
+ evidence: {
1065
+ kind: "missing-path",
1066
+ claimed: claim.path,
1067
+ renamedTo: null,
1068
+ deletedInCommit: deleted,
1069
+ everTracked: true,
1070
+ parentDirExists: await exists(
1071
+ path8.join(ctx.root, path8.dirname(claim.path))
1072
+ )
1073
+ }
1074
+ });
1075
+ continue;
1076
+ }
1077
+ const parentDirExists = await exists(
1078
+ path8.join(ctx.root, path8.dirname(claim.path))
1079
+ );
1080
+ if (!parentDirExists) continue;
1081
+ const issue = {
1082
+ type: "deleted-reference",
1083
+ message: `\`${claim.path}\` does not exist (never tracked in git \u2013 possible typo or invented path)`,
1084
+ anchor,
1085
+ confidence: "likely",
1086
+ evidence: {
1087
+ kind: "missing-path",
1088
+ claimed: claim.path,
1089
+ renamedTo: null,
1090
+ deletedInCommit: null,
1091
+ everTracked: false,
1092
+ parentDirExists: true
1093
+ }
1094
+ };
1095
+ result.issues.push(issue);
1096
+ }
1097
+ }
1098
+ return result;
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
+ });
1107
+
1108
+ // src/audit/checks/new-module.ts
1109
+ import fg2 from "fast-glob";
1110
+ import path9 from "path";
1111
+ function escapeRegExp(text2) {
1112
+ return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1113
+ }
1114
+ function isMentioned(combinedDocs, name) {
1115
+ const re = new RegExp(
1116
+ `(^|[^A-Za-z0-9_-])${escapeRegExp(name)}(/|[^A-Za-z0-9_-]|$)`,
1117
+ "im"
1118
+ );
1119
+ return re.test(combinedDocs);
1120
+ }
1121
+ async function listSubdirs(absDir) {
1122
+ const dirs = await fg2("*", {
1123
+ cwd: absDir,
1124
+ onlyDirectories: true,
1125
+ suppressErrors: true
1126
+ });
1127
+ return dirs.filter((d) => !DIR_DENYLIST.has(d)).sort();
1128
+ }
1129
+ async function countSourceFiles(absDir) {
1130
+ const files = await fg2(SOURCE_GLOB, {
1131
+ cwd: absDir,
1132
+ ignore: SOURCE_IGNORE,
1133
+ suppressErrors: true
1134
+ });
1135
+ return files.length;
1136
+ }
1137
+ async function checkNewModules(ctx) {
1138
+ const result = emptyResult();
1139
+ if (ctx.docs.length === 0) return result;
1140
+ const combinedDocs = ctx.docs.map((d) => d.content).join("\n");
1141
+ const primaryDoc = ctx.docs[0].path;
1142
+ const checkedDocs = ctx.docs.map((d) => d.path);
1143
+ const flag = async (dir, sourceFileCount) => {
1144
+ result.issues.push({
1145
+ type: "new-module",
1146
+ message: `directory \`${dir}/\` contains ${sourceFileCount} source file${sourceFileCount === 1 ? "" : "s"} but is not mentioned in any context file`,
1147
+ anchor: { doc: primaryDoc, line: null, excerpt: dir },
1148
+ confidence: "likely",
1149
+ evidence: {
1150
+ kind: "unmentioned-dir",
1151
+ dir,
1152
+ sourceFileCount,
1153
+ firstCommit: await firstCommitOf(ctx.root, dir),
1154
+ checkedDocs
1155
+ }
1156
+ });
1157
+ };
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);
1167
+ const topMentioned = isMentioned(combinedDocs, topDir);
1168
+ if (!topMentioned) {
1169
+ const count3 = await countSourceFiles(absTop);
1170
+ if (count3 >= 1) candidates.push({ dir: topDir, sourceFileCount: count3 });
1171
+ continue;
1172
+ }
1173
+ const subdirs = await listSubdirs(absTop);
1174
+ const mentioned = subdirs.filter((s) => isMentioned(combinedDocs, s));
1175
+ if (mentioned.length < ENUMERATION_THRESHOLD) continue;
1176
+ for (const sub of subdirs) {
1177
+ if (isMentioned(combinedDocs, sub)) continue;
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 });
1181
+ }
1182
+ }
1183
+ }
1184
+ return candidates;
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
+ });
1216
+
1217
+ // src/audit/checks/stale-count.ts
1218
+ import fs6 from "fs/promises";
1219
+ import path10 from "path";
1220
+ import fg3 from "fast-glob";
1221
+ async function readIfExists(absPath) {
1222
+ try {
1223
+ return await fs6.readFile(absPath, "utf-8");
1224
+ } catch {
1225
+ return null;
1226
+ }
1227
+ }
1228
+ async function countGradleModules(root) {
1229
+ for (const name of ["settings.gradle.kts", "settings.gradle"]) {
1230
+ const content2 = await readIfExists(path10.join(root, name));
1231
+ if (content2 === null) continue;
1232
+ const members = [];
1233
+ for (const call of content2.matchAll(/include\s*\(([^)]*)\)/g)) {
1234
+ for (const proj of call[1].matchAll(/["']([^"']+)["']/g)) {
1235
+ members.push(proj[1]);
1236
+ }
1237
+ }
1238
+ if (members.length === 0) return null;
1239
+ return { actual: members.length, countedFrom: name, members };
1240
+ }
1241
+ return null;
1242
+ }
1243
+ async function countNpmWorkspaces(root) {
1244
+ const pkgRaw = await readIfExists(path10.join(root, "package.json"));
1245
+ if (pkgRaw !== null) {
1246
+ try {
1247
+ const pkg = JSON.parse(pkgRaw);
1248
+ const globs = Array.isArray(pkg.workspaces) ? pkg.workspaces : Array.isArray(pkg.workspaces?.packages) ? pkg.workspaces.packages : [];
1249
+ if (globs.length > 0) {
1250
+ const matched = await fg3(
1251
+ globs.map((g) => `${g.replace(/\/+$/, "")}/package.json`),
1252
+ { cwd: root, ignore: ["**/node_modules/**"] }
1253
+ );
1254
+ return {
1255
+ actual: matched.length,
1256
+ countedFrom: "package.json workspaces",
1257
+ members: matched.map((m) => path10.dirname(m)).sort()
1258
+ };
1259
+ }
1260
+ } catch {
1261
+ }
1262
+ }
1263
+ const pnpmRaw = await readIfExists(path10.join(root, "pnpm-workspace.yaml"));
1264
+ if (pnpmRaw !== null) {
1265
+ const globs = [];
1266
+ let inPackages = false;
1267
+ for (const line of pnpmRaw.split("\n")) {
1268
+ if (/^packages\s*:/.test(line)) {
1269
+ inPackages = true;
1270
+ continue;
1271
+ }
1272
+ if (inPackages) {
1273
+ const entry = line.match(/^\s*-\s*["']?([^"'#\s]+)/);
1274
+ if (entry) {
1275
+ if (!entry[1].startsWith("!")) globs.push(entry[1]);
1276
+ } else if (line.trim().length > 0 && !line.startsWith(" ")) {
1277
+ inPackages = false;
1278
+ }
1279
+ }
1280
+ }
1281
+ if (globs.length > 0) {
1282
+ const matched = await fg3(
1283
+ globs.map((g) => `${g.replace(/\/+$/, "")}/package.json`),
1284
+ { cwd: root, ignore: ["**/node_modules/**"] }
1285
+ );
1286
+ return {
1287
+ actual: matched.length,
1288
+ countedFrom: "pnpm-workspace.yaml",
1289
+ members: matched.map((m) => path10.dirname(m)).sort()
1290
+ };
1291
+ }
1292
+ }
1293
+ return null;
1294
+ }
1295
+ async function countCargoCrates(root) {
1296
+ const content2 = await readIfExists(path10.join(root, "Cargo.toml"));
1297
+ if (content2 === null) return null;
1298
+ const membersBlock = content2.match(/members\s*=\s*\[([\s\S]*?)\]/);
1299
+ if (!membersBlock) return null;
1300
+ const entries = [...membersBlock[1].matchAll(/["']([^"']+)["']/g)].map(
1301
+ (m) => m[1]
1302
+ );
1303
+ if (entries.length === 0) return null;
1304
+ const members = /* @__PURE__ */ new Set();
1305
+ for (const entry of entries) {
1306
+ if (/[*?[\]{}]/.test(entry)) {
1307
+ const matched = await fg3(`${entry.replace(/\/+$/, "")}/Cargo.toml`, {
1308
+ cwd: root,
1309
+ ignore: ["**/target/**"]
1310
+ });
1311
+ for (const m of matched) members.add(path10.dirname(m));
1312
+ } else if (await readIfExists(path10.join(root, entry, "Cargo.toml")) !== null) {
1313
+ members.add(entry);
1314
+ }
1315
+ }
1316
+ if (members.size === 0) return null;
1317
+ return {
1318
+ actual: members.size,
1319
+ countedFrom: "Cargo.toml workspace members",
1320
+ members: [...members].sort()
1321
+ };
1322
+ }
1323
+ async function resolveCountSource(root, claim) {
1324
+ const unit = claim.unit.replace(/s$/, "");
1325
+ if (unit === "module") return countGradleModules(root);
1326
+ if (unit === "workspace") return countNpmWorkspaces(root);
1327
+ if (unit === "crate") return countCargoCrates(root);
1328
+ return await countNpmWorkspaces(root) ?? await countCargoCrates(root) ?? await countGradleModules(root);
1329
+ }
1330
+ async function checkStaleCounts(ctx) {
1331
+ const result = emptyResult();
1332
+ for (const doc of ctx.docs) {
1333
+ for (const claim of doc.claims.counts) {
1334
+ const source = await resolveCountSource(ctx.root, claim);
1335
+ if (source === null) {
1336
+ result.skipped.push({
1337
+ check: "stale-count",
1338
+ doc: doc.path,
1339
+ reason: `${doc.path}: cannot resolve a workspace manifest for "${claim.excerpt}"`
1340
+ });
1341
+ continue;
1342
+ }
1343
+ if (source.actual === claim.count) continue;
1344
+ result.issues.push({
1345
+ type: "stale-count",
1346
+ message: `says "${claim.excerpt}" but ${source.countedFrom} resolves to ${source.actual}`,
1347
+ anchor: { doc: doc.path, line: claim.line, excerpt: claim.excerpt },
1348
+ confidence: "certain",
1349
+ evidence: {
1350
+ kind: "count-mismatch",
1351
+ claimed: claim.count,
1352
+ actual: source.actual,
1353
+ unit: claim.unit,
1354
+ countedFrom: source.countedFrom,
1355
+ members: source.members.slice(0, MEMBERS_CAP)
1356
+ }
1357
+ });
1358
+ }
1359
+ }
1360
+ return result;
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
+ });
1370
+
1371
+ // src/audit/checks/dead-command.ts
1372
+ import fs7 from "fs/promises";
1373
+ import path11 from "path";
1374
+ import fg4 from "fast-glob";
1375
+ async function scriptsOf(absManifest) {
1376
+ try {
1377
+ const pkg = JSON.parse(await fs7.readFile(absManifest, "utf-8"));
1378
+ return pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? Object.keys(pkg.scripts) : [];
1379
+ } catch {
1380
+ return null;
1381
+ }
1382
+ }
1383
+ async function checkDeadCommands(ctx) {
1384
+ const result = emptyResult();
1385
+ const commandClaims = ctx.docs.flatMap(
1386
+ (doc) => doc.claims.commands.map((claim) => ({ doc, claim }))
1387
+ );
1388
+ if (commandClaims.length === 0) return result;
1389
+ const rootScripts = await scriptsOf(path11.join(ctx.root, "package.json"));
1390
+ if (rootScripts === null) {
1391
+ result.skipped.push({
1392
+ check: "dead-command",
1393
+ reason: "no package.json at the repo root"
1394
+ });
1395
+ return result;
1396
+ }
1397
+ const rootSet = new Set(rootScripts);
1398
+ let workspaceScripts = null;
1399
+ let manifestsChecked = ["package.json"];
1400
+ const loadWorkspaceScripts = async () => {
1401
+ if (workspaceScripts !== null) return workspaceScripts;
1402
+ workspaceScripts = /* @__PURE__ */ new Set();
1403
+ const manifests = await commandManifests(ctx.root);
1404
+ manifestsChecked = ["package.json", ...manifests.sort()];
1405
+ for (const manifest of manifests) {
1406
+ const scripts = await scriptsOf(path11.join(ctx.root, manifest));
1407
+ for (const name of scripts ?? []) workspaceScripts.add(name);
1408
+ }
1409
+ return workspaceScripts;
1410
+ };
1411
+ for (const { doc, claim } of commandClaims) {
1412
+ if (rootSet.has(claim.scriptName)) continue;
1413
+ const elsewhere = await loadWorkspaceScripts();
1414
+ if (elsewhere.has(claim.scriptName)) continue;
1415
+ result.issues.push({
1416
+ type: "dead-command",
1417
+ message: `\`${claim.invocation}\` refers to script "${claim.scriptName}", which exists in no package.json`,
1418
+ anchor: { doc: doc.path, line: claim.line, excerpt: claim.excerpt },
1419
+ confidence: "certain",
1420
+ evidence: {
1421
+ kind: "missing-script",
1422
+ scriptName: claim.scriptName,
1423
+ invocation: claim.invocation,
1424
+ manifestsChecked,
1425
+ availableScripts: rootScripts.slice(0, AVAILABLE_SCRIPTS_CAP)
1426
+ }
1427
+ });
1428
+ }
1429
+ return result;
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
+ });
1503
+
1504
+ // src/audit/checks/deps-changed.ts
1505
+ async function checkDepsChanged(ctx) {
1506
+ const result = emptyResult();
1507
+ result.suppressedAdvisories = [];
1508
+ const releaseOnly = /* @__PURE__ */ new Map();
1509
+ for (const doc of ctx.docs) {
1510
+ if (!doc.lastCommit) {
1511
+ result.skipped.push({
1512
+ check: "deps-changed",
1513
+ doc: doc.path,
1514
+ reason: `${doc.path} has no commit history`
1515
+ });
1516
+ continue;
1517
+ }
1518
+ if (doc.dirty) {
1519
+ result.skipped.push({
1520
+ check: "deps-changed",
1521
+ doc: doc.path,
1522
+ reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
1523
+ });
1524
+ }
1525
+ const range = await commitsTouchingSince(
1526
+ ctx.root,
1527
+ doc.lastCommit.hash,
1528
+ MANIFEST_PATHSPECS
1529
+ );
1530
+ if (range === null) {
1531
+ result.skipped.push({
1532
+ check: "deps-changed",
1533
+ doc: doc.path,
1534
+ reason: `${doc.path}: commit range unreachable (shallow clone?)`
1535
+ });
1536
+ continue;
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;
1547
+ if (range.total === 0) continue;
1548
+ const latest = range.commits[0];
1549
+ (doc.dirty ? result.suppressedAdvisories : result.advisories).push({
1550
+ type: "deps-changed",
1551
+ message: `dependency manifests touched by ${range.total} commit${range.total === 1 ? "" : "s"} since ${doc.path} was last committed (latest: ${latest.hash.slice(0, 7)} "${latest.subject}")`,
1552
+ anchor: { doc: doc.path, line: null, excerpt: null },
1553
+ evidence: {
1554
+ kind: "doc-behind-manifests",
1555
+ docLastCommit: doc.lastCommit,
1556
+ manifestCommits: range.commits.slice(0, MANIFEST_COMMITS_CAP),
1557
+ totalCommits: range.total
1558
+ }
1559
+ });
1560
+ }
1561
+ return result;
1562
+ }
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
+ });
1587
+
1588
+ // src/context/lexical.ts
1589
+ import path12 from "path";
1590
+ var init_lexical = __esm({
1591
+ "src/context/lexical.ts"() {
1592
+ "use strict";
1593
+ }
1594
+ });
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
+ }
1610
+ });
1611
+
1612
+ // src/decisions/provenance.ts
1613
+ import { z as z3 } from "zod";
1614
+ function decisionContent(record) {
1615
+ return {
1616
+ title: record.title,
1617
+ body: record.body,
1618
+ category: record.category,
1619
+ files: record.files,
1620
+ ...typeof record.owner === "string" ? { owner: record.owner } : {},
1621
+ sources: Array.isArray(record.sources) ? record.sources : []
1622
+ };
1623
+ }
1624
+ function decisionApproval(record) {
1625
+ return record.version === 1 ? "unreviewed" : record.approval;
1626
+ }
1627
+ function effectiveDecision(record) {
1628
+ if (record.version !== 2 || record.status !== "active" || record.approval !== "proposed") return record;
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];
1633
+ return {
1634
+ ...record,
1635
+ ...event.content,
1636
+ owner: event.content.owner,
1637
+ approval: "accepted",
1638
+ revision: event.revision,
1639
+ refreshedHash: event.refreshedHash,
1640
+ updatedAt: event.at,
1641
+ history: record.history.slice(0, index2 + 1)
1642
+ };
1643
+ }
1644
+ function decisionProvenance(record, freshness = "unknown") {
1645
+ const approval = decisionApproval(record);
1646
+ const review = record.version === 2 ? [...record.history].reverse().find((e) => ["accepted", "reaffirmed"].includes(e.kind) && e.revision === record.revision) : void 0;
1647
+ return {
1648
+ approval,
1649
+ revision: record.version === 2 ? record.revision : 0,
1650
+ owner: record.version === 2 ? record.owner ?? null : null,
1651
+ sources: record.version === 2 ? record.sources : [],
1652
+ guidance: record.status !== "active" ? "historical" : approval === "accepted" ? "constraint" : approval === "proposed" ? "proposal" : "unreviewed",
1653
+ reviewRequired: record.status === "active" && (approval !== "accepted" || freshness !== "current"),
1654
+ lastReview: review ? { reviewer: review.actor, at: review.at, note: review.note, gitHash: review.refreshedHash } : null
1655
+ };
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
+ });
1770
+
1771
+ // src/decisions/decisions.ts
1772
+ import fs8 from "fs/promises";
1773
+ import path13 from "path";
1774
+ import { createHash } from "crypto";
1775
+ async function loadDecisionStore(rootDir) {
1776
+ const records = [];
1777
+ const diagnostics = [];
1778
+ let entries;
1779
+ try {
1780
+ entries = await fs8.readdir(await storePath(rootDir, ".mason/decisions"));
1781
+ } catch (error) {
1782
+ if (error.code !== "ENOENT") diagnostics.push({ path: ".mason/decisions", message: String(error) });
1783
+ return { records, diagnostics };
1784
+ }
1785
+ for (const entry of entries.sort()) {
1786
+ if (!entry.endsWith(".json")) continue;
1787
+ const relative = `.mason/decisions/${entry}`;
1788
+ try {
1789
+ const record = decisionSchema.parse(await readStoreJson(rootDir, relative));
1790
+ if (entry !== `${record.id}.json`) throw new Error("Record id does not match its filename");
1791
+ records.push(record);
1792
+ } catch (error) {
1793
+ diagnostics.push({ path: relative, message: error instanceof Error ? error.message : String(error) });
1794
+ }
1795
+ }
1796
+ return { records, diagnostics };
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
+ });
1808
+
1809
+ // src/decisions/drift.ts
1810
+ import path14 from "path";
1811
+ async function computeDecisionDrift(rootDir, decisions) {
1812
+ const resolvedRoot = path14.resolve(rootDir);
1813
+ const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
1814
+ const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
1815
+ const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
1816
+ const changesByHash = /* @__PURE__ */ new Map();
1817
+ const inspect = async (record) => {
1818
+ if (record.files.length === 0) return { freshness: "unknown", changedFiles: [] };
1819
+ let touched = changesByHash.get(record.refreshedHash);
1820
+ if (touched === void 0) {
1821
+ const changes = record.refreshedHash === head && head !== "unknown" ? [] : await getChangesWithStatus(resolvedRoot, record.refreshedHash);
1822
+ touched = changes === null ? null : touchedPaths(changes);
1823
+ changesByHash.set(record.refreshedHash, touched);
1824
+ }
1825
+ if (touched === null) report.historyAvailable = false;
1826
+ const hits = touched ? matchingPaths(record.files, touched) : [];
1827
+ const localHits = matchingPaths(record.files, workingTree.changedFiles);
1828
+ return { freshness: touched === null || !workingTree.available ? "unknown" : hits.length || localHits.length ? "changed" : "current", changedFiles: hits };
1829
+ };
1830
+ for (const record of store.records) {
1831
+ if (record.status !== "active") continue;
1832
+ const effective = effectiveDecision(record);
1833
+ const state = await inspect(effective);
1834
+ report.freshness[record.id] = state.freshness;
1835
+ if (state.changedFiles.length) report.staleDecisions[record.id] = state.changedFiles;
1836
+ if (effective !== record) (report.pendingProposals ??= {})[record.id] = await inspect(record);
1837
+ }
1838
+ return report;
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
+ });
1850
+
1851
+ // src/audit/checks/decision-anchor.ts
1852
+ async function checkDecisionAnchors(ctx) {
1853
+ const result = emptyResult();
1854
+ if (!ctx.decisionsPresent) return result;
1855
+ const store = await loadDecisionStore(ctx.root);
1856
+ const records = store.records;
1857
+ for (const diagnostic of store.diagnostics) result.skipped.push({ check: "decision-anchor-drift", reason: `${diagnostic.path}: ${diagnostic.message}` });
1858
+ const drift = await computeDecisionDrift(ctx.root, records);
1859
+ if (!drift.historyAvailable) {
1860
+ result.skipped.push({
1861
+ check: "decision-anchor-drift",
1862
+ reason: "some decision base commits are unreachable (shallow clone?)"
1863
+ });
1864
+ }
1865
+ const changed = records.flatMap((record) => [
1866
+ { record: effectiveDecision(record), changedFiles: drift.staleDecisions[record.id] ?? [], freshness: drift.freshness?.[record.id] ?? "unknown" },
1867
+ { record, changedFiles: drift.pendingProposals?.[record.id]?.changedFiles ?? [], freshness: drift.pendingProposals?.[record.id]?.freshness ?? "unknown" }
1868
+ ]);
1869
+ for (const { record, changedFiles, freshness } of changed) {
1870
+ if (!changedFiles.length) continue;
1871
+ const id = record.id;
1872
+ const provenance = decisionProvenance(record, freshness);
1873
+ result.advisories.push({
1874
+ type: "decision-anchor-drift",
1875
+ message: `decision "${record.title}" (${provenance.approval}) has anchor files that changed since its evidence baseline \u2013 needs human review`,
1876
+ anchor: {
1877
+ doc: `.mason/decisions/${id}.json`,
1878
+ line: null,
1879
+ excerpt: record.title
1880
+ },
1881
+ evidence: {
1882
+ kind: "decision-anchor",
1883
+ provenance,
1884
+ decisionId: id,
1885
+ title: record.title,
1886
+ changedFiles,
1887
+ refreshedHash: record.refreshedHash
1888
+ }
1889
+ });
1890
+ }
1891
+ return result;
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
+ });
1902
+
1903
+ // src/audit/checks/index.ts
1904
+ function emptyResult() {
1905
+ return { issues: [], advisories: [], skipped: [] };
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
+ });
1927
+
1928
+ // src/audit/audit.ts
1929
+ import fs9 from "fs/promises";
1930
+ import path15 from "path";
1931
+ async function computeAudit(rootDir, options = {}) {
1932
+ const resolvedRoot = path15.resolve(rootDir);
1933
+ const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
1934
+ if (docs.length === 0) return null;
1935
+ const report = {
1936
+ version: 1,
1937
+ root: resolvedRoot,
1938
+ gitAvailable: headHash !== "unknown",
1939
+ headHash,
1940
+ checksRun: [],
1941
+ docs: docs.map((d) => ({
1942
+ path: d.path,
1943
+ lastCommit: d.lastCommit,
1944
+ dirty: d.dirty,
1945
+ lineCount: d.lineCount
1946
+ })),
1947
+ decisionsChecked: false,
1948
+ issues: [],
1949
+ advisories: [],
1950
+ suppressedAdvisories: [],
1951
+ skippedChecks: [],
1952
+ clean: true
1953
+ };
1954
+ if (!report.gitAvailable) return report;
1955
+ const changesSinceDoc = /* @__PURE__ */ new Map();
1956
+ for (const doc of docs) {
1957
+ changesSinceDoc.set(
1958
+ doc.path,
1959
+ doc.lastCommit ? await getChangesWithStatus(resolvedRoot, doc.lastCommit.hash) : null
1960
+ );
1961
+ }
1962
+ let decisionsPresent = false;
1963
+ try {
1964
+ await fs9.access(path15.join(resolvedRoot, ".mason", "decisions"));
1965
+ decisionsPresent = true;
1966
+ } catch {
1967
+ }
1968
+ report.decisionsChecked = decisionsPresent;
1969
+ const ctx = {
1970
+ root: resolvedRoot,
1971
+ docs,
1972
+ headHash,
1973
+ changesSinceDoc,
1974
+ decisionsPresent
1975
+ };
1976
+ const selected = options.checks ?? ALL_CHECKS;
1977
+ for (const name of ALL_CHECKS) {
1978
+ if (!selected.includes(name)) continue;
1979
+ const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, ctx) : CHECKS[name](ctx));
1980
+ report.checksRun.push(name);
1981
+ report.issues.push(...issues);
1982
+ report.advisories.push(...advisories);
1983
+ report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
1984
+ report.skippedChecks.push(...skipped);
1985
+ }
1986
+ report.clean = report.issues.length === 0;
1987
+ return report;
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
+ });
1999
+
2000
+ // src/audit/repair.ts
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";
2005
+ function findingId(finding) {
2006
+ const e = finding.evidence;
2007
+ let key;
2008
+ switch (e.kind) {
2009
+ case "missing-path":
2010
+ key = e.claimed;
2011
+ break;
2012
+ case "unmentioned-dir":
2013
+ key = e.dir;
2014
+ break;
2015
+ case "count-mismatch":
2016
+ key = [e.unit.replace(/s$/, ""), e.countedFrom];
2017
+ break;
2018
+ case "missing-script":
2019
+ key = e.scriptName;
2020
+ break;
2021
+ case "doc-behind-manifests":
2022
+ key = null;
2023
+ break;
2024
+ case "decision-anchor":
2025
+ key = [e.decisionId, e.provenance?.revision, e.provenance?.approval];
2026
+ break;
2027
+ }
2028
+ return digest([finding.type, finding.anchor.doc, key]);
2029
+ }
2030
+ function allFindings(report) {
2031
+ return [...report.issues, ...report.advisories, ...report.suppressedAdvisories ?? []];
2032
+ }
2033
+ async function docState(root) {
2034
+ const docs = [];
2035
+ for (const doc of DOC_CANDIDATES) {
2036
+ try {
2037
+ const content2 = await readBoundedFile(await storePath(root, doc), 10 * 1024 * 1024);
2038
+ if (content2 === null) throw new Error("Context file is not regular or exceeds 10 MiB: " + doc);
2039
+ docs.push([doc, digest(content2)]);
2040
+ } catch (error) {
2041
+ if (error.code !== "ENOENT") throw error;
2042
+ docs.push([doc, null]);
2043
+ }
2044
+ }
2045
+ return digest(docs);
2046
+ }
2047
+ async function stableAudit(root, checks, options = {}) {
2048
+ const head = await getCurrentGitHash(root);
2049
+ const before = await docState(root);
2050
+ const report = await computeAudit(root, { ...options, checks });
2051
+ if (head !== await getCurrentGitHash(root) || before !== await docState(root) || report && report.headHash !== head) {
2052
+ throw new Error("HEAD or context files changed during the audit; retry against a stable checkout.");
2053
+ }
2054
+ return report;
2055
+ }
2056
+ async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
2057
+ const root = await fs10.realpath(rootDir);
2058
+ const selected = z4.array(checkSchema).nonempty().parse(checks);
2059
+ const report = await stableAudit(root, selected, options);
2060
+ if (!report) throw new Error("No context files found to prepare a repair.");
2061
+ if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
2062
+ const storedReport = reportSchema.parse(report);
2063
+ const payload = {
2064
+ kind: "mason-audit-repair",
2065
+ version: 1,
2066
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2067
+ report: storedReport
2068
+ };
2069
+ const baselinePath = ".mason/reports/repairs/" + randomUUID3() + ".json";
2070
+ await writeStoreJson(root, baselinePath, { ...payload, digest: digest(payload) });
2071
+ return { version: 1, action: "prepare", baselinePath, report };
2072
+ }
2073
+ async function verifyRepair(rootDir, baselinePath, options = {}) {
2074
+ const root = await fs10.realpath(rootDir);
2075
+ const declaredRoot = path16.resolve(rootDir);
2076
+ const relative = path16.isAbsolute(baselinePath) ? path16.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
2077
+ const stored = baselineSchema.parse(await readStoreJson(root, relative));
2078
+ const { digest: savedDigest, ...payload } = stored;
2079
+ if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
2080
+ if (stored.report.root !== root) throw new Error("Repair baseline belongs to a different repository.");
2081
+ const original = stored.report;
2082
+ const diagnostics = [];
2083
+ let current = null;
2084
+ try {
2085
+ current = await stableAudit(root, original.checksRun, options);
2086
+ if (!current) diagnostics.push("No context files remain available to audit.");
2087
+ else if (!current.gitAvailable) diagnostics.push("Git history is unavailable.");
2088
+ for (const doc of original.docs) {
2089
+ if (!original.issues.some((f) => f.anchor.doc === doc.path) || !current?.docs.some((d) => d.path === doc.path)) continue;
2090
+ const content2 = await readBoundedFile(await storePath(root, doc.path), 10 * 1024 * 1024);
2091
+ if (content2 === null || !content2.trim()) {
2092
+ diagnostics.push("Original context file " + doc.path + " is empty or unreadable; losing its claims does not verify a repair.");
2093
+ }
2094
+ }
2095
+ if (await getChangesWithStatus(root, original.headHash) === null) {
2096
+ diagnostics.push("The original audit commit is unavailable; repair history cannot be verified.");
2097
+ }
2098
+ } catch (error) {
2099
+ diagnostics.push(error instanceof Error ? error.message : String(error));
2100
+ }
2101
+ const currentById = new Map((current ? allFindings(current) : []).map((f) => [findingId(f), f]));
2102
+ const originalFindings = allFindings(original);
2103
+ const originalIds = new Set(originalFindings.map(findingId));
2104
+ const missingDocs = original.docs.filter((doc) => !current?.docs.some((d) => d.path === doc.path));
2105
+ for (const doc of missingDocs) diagnostics.push("Original context file " + doc.path + " is unavailable; removing it does not verify a repair.");
2106
+ const findings = originalFindings.map((finding) => {
2107
+ const id = findingId(finding);
2108
+ const now = currentById.get(id);
2109
+ const base = { id, original: finding, ...now ? { current: now } : {} };
2110
+ if (diagnostics.length || !current) {
2111
+ return { ...base, status: "unverified", reason: "The original audit scope could not be verified. See diagnostics." };
2112
+ }
2113
+ if ("confidence" in finding && now) {
2114
+ return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
2115
+ }
2116
+ const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
2117
+ if (!current.checksRun?.includes(finding.type) || skipped.length) {
2118
+ return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
2119
+ }
2120
+ if (!("confidence" in finding)) {
2121
+ return {
2122
+ ...base,
2123
+ status: "review-required",
2124
+ reason: "An audit cannot establish that this advisory was reviewed. Retain its original evidence and report a separate assessment; editing or committing the doc is not approval."
2125
+ };
2126
+ }
2127
+ return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
2128
+ });
2129
+ const newFindings = [...currentById].filter(([id]) => !originalIds.has(id)).map(([, f]) => f);
2130
+ const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
2131
+ for (const f of findings) counts[f.status]++;
2132
+ const incomplete = diagnostics.length > 0 || counts.unverified > 0 || counts["review-required"] > 0 || (current?.skippedChecks.length ?? 0) > 0 || newFindings.some((f) => !("confidence" in f));
2133
+ const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
2134
+ return {
2135
+ version: 1,
2136
+ action: "verify",
2137
+ baselinePath: relative,
2138
+ baselineHead: original.headHash,
2139
+ currentHead: current?.gitAvailable ? current.headHash : null,
2140
+ status: incomplete ? "incomplete" : issuesRemain ? "issues-remain" : "verified",
2141
+ findings,
2142
+ newFindings,
2143
+ diagnostics,
2144
+ currentAudit: current,
2145
+ counts,
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."
2147
+ };
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
+ });
2252
+
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
+ });
2262
+ import fs11 from "fs/promises";
2263
+ import path17 from "path";
2264
+ import { createHash as createHash3 } from "crypto";
2265
+ import { execFile as execFile7 } from "child_process";
2266
+ import { promisify as promisify7 } from "util";
2267
+ import fg5 from "fast-glob";
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;
2271
+ }
2272
+ async function workspace(dir) {
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());
2275
+ let branch;
2276
+ try {
2277
+ branch = (await git2(root, "symbolic-ref", "--quiet", "HEAD")).trim();
2278
+ } catch {
2279
+ branch = "detached";
2280
+ }
2281
+ return { root, gitDir, branch, directory: ".mason/reports/automation/" + hash([root, gitDir, branch]).slice(0, 24) };
2282
+ }
2283
+ async function content(root, file) {
2284
+ try {
2285
+ const value = await readBoundedFile(await storePath(root, file), 10 * 1024 * 1024);
2286
+ if (value === null) throw new Error("Unreadable or oversized audit input: " + file);
2287
+ return value;
2288
+ } catch (error) {
2289
+ if (error.code === "ENOENT") return null;
2290
+ throw error;
2291
+ }
2292
+ }
2293
+ async function readInputs(root) {
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),
2297
+ fg5("**/*", {
2298
+ cwd: root,
2299
+ dot: true,
2300
+ onlyFiles: false,
2301
+ followSymbolicLinks: false,
2302
+ objectMode: true,
2303
+ ignore: SOURCE_IGNORE
2304
+ }),
2305
+ git2(root, "rev-parse", "--git-path", "shallow"),
2306
+ git2(root, "for-each-ref", "--format=%(refname) %(objectname)", "refs/replace")
2307
+ ]);
2308
+ const entries = inventory.filter((f) => !internal(f.path) && f.path !== ".git").sort((a, b) => a.path.localeCompare(b.path));
2309
+ const files = entries.map((f) => f.path);
2310
+ if (files.length > 1e5) throw new Error("Automation input inventory exceeds 100,000 paths; use an explicit scoped audit.");
2311
+ const head = headText.trim();
2312
+ let shallow = null;
2313
+ try {
2314
+ shallow = await fs11.readFile(path17.resolve(root, shallowPath.trim()), "utf8");
2315
+ } catch (error) {
2316
+ if (error.code !== "ENOENT") throw error;
2317
+ }
2318
+ const docs = {};
2319
+ const docContents = [];
2320
+ const claims = [];
2321
+ for (const file of DOC_CANDIDATES) {
2322
+ const text2 = await content(root, file);
2323
+ docs[file] = text2 === null ? null : hash(text2);
2324
+ docContents.push([file, text2]);
2325
+ for (const claim of text2 ? extractClaims(text2).paths : []) {
2326
+ if (internal(claim.path) || claim.path.startsWith(".mason/")) continue;
2327
+ const exists2 = async (p) => fs11.access(path17.resolve(root, p)).then(() => true, () => false);
2328
+ claims.push([claim.path, await exists2(claim.path), await exists2(path17.dirname(claim.path))]);
2329
+ }
2330
+ }
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.");
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];
2352
+ const keys = {
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])
2359
+ };
2360
+ return { fingerprint: hash(keys), head, docs, keys };
2361
+ }
2362
+ function checkCache(raw, inputs) {
2363
+ let entries = {};
2364
+ let diagnostic = null;
2365
+ if (raw !== null) {
2366
+ const parsed = cacheSchema.safeParse(raw);
2367
+ if (parsed.success && parsed.data.digest === hash(parsed.data.entries)) entries = parsed.data.entries;
2368
+ else diagnostic = "Discarded an invalid automation cache; checks are being recomputed.";
2369
+ }
2370
+ const ran = /* @__PURE__ */ new Set(), reused = /* @__PURE__ */ new Set();
2371
+ const options = { runCheck: async (name, ctx) => {
2372
+ if (entries[name]?.key === inputs.keys[name]) {
2373
+ reused.add(name);
2374
+ return structuredClone(entries[name].result);
2375
+ }
2376
+ const result = await CHECKS[name](ctx);
2377
+ ran.add(name);
2378
+ if (!result.skipped.length) entries[name] = { key: inputs.keys[name], result };
2379
+ else delete entries[name];
2380
+ return result;
2381
+ } };
2382
+ return { options, ran, reused, diagnostic, serialize: () => {
2383
+ const canonical = cacheSchema.shape.entries.parse(entries);
2384
+ return { version: 1, entries: canonical, digest: hash(canonical) };
2385
+ } };
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
+ });
2407
+
2408
+ // src/automation/store.ts
2409
+ import fs12 from "fs/promises";
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
+ }
2419
+ async function withLock(root, directory, run) {
2420
+ const file = await storePath(root, directory + "/lock", true);
2421
+ const deadline = Date.now() + 5e3;
2422
+ let handle;
2423
+ while (!handle) {
2424
+ try {
2425
+ handle = await fs12.open(file, "wx", 384);
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
+ }
2436
+ } catch (error) {
2437
+ if (error.code !== "EEXIST") throw error;
2438
+ try {
2439
+ const owner = JSON.parse(await fs12.readFile(file, "utf8"));
2440
+ if (owner.host === os2.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
2441
+ try {
2442
+ process.kill(owner.pid, 0);
2443
+ } catch (probe) {
2444
+ if (probe.code === "ESRCH") {
2445
+ const reclaim = file + ".reclaim";
2446
+ let guard;
2447
+ try {
2448
+ guard = await fs12.open(reclaim, "wx", 384);
2449
+ const current = JSON.parse(await fs12.readFile(file, "utf8"));
2450
+ if (current.pid === owner.pid && current.host === owner.host) await fs12.unlink(file);
2451
+ } finally {
2452
+ if (guard) {
2453
+ await guard.close();
2454
+ await fs12.rm(reclaim, { force: true });
2455
+ }
2456
+ }
2457
+ }
2458
+ }
2459
+ }
2460
+ } catch {
2461
+ }
2462
+ if (Date.now() >= deadline) throw new Error("Automation is busy or its lock needs inspection: " + file);
2463
+ await new Promise((resolve) => setTimeout(resolve, 40));
2464
+ }
2465
+ }
2466
+ try {
2467
+ return await run();
2468
+ } finally {
2469
+ await handle.close();
2470
+ await fs12.unlink(file);
2471
+ }
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
+ });
2504
+
2505
+ // src/automation/runtime.ts
2506
+ import { randomUUID as randomUUID4 } from "crypto";
2507
+ function summarize(report) {
2508
+ const open = report.findings.filter((f) => f.status !== "resolved");
2509
+ return [
2510
+ `Mason: ${report.status}; ${report.counts.unresolved} unresolved, ${report.counts["review-required"]} need review, ${report.counts.unverified} unverified.`,
2511
+ ...open.slice(0, 4).map((f) => `[${f.status}] ${cleanText(f.original.anchor.doc)}: ${cleanText(f.original.message)}`),
2512
+ ...open.length > 4 ? [`${open.length - 4} more findings in the report.`] : [],
2513
+ ...report.diagnostics.slice(0, 2).map(cleanText),
2514
+ `Evidence: ${report.reportPath}. Resume/check with mason_automation(action: "check") or mason-auto check.`,
2515
+ "Keep original evidence. Address findings relevant to the authorized task; report unrelated findings and unresolved advisories without approving them."
2516
+ ].join("\n");
2517
+ }
2518
+ async function automate(dir, event) {
2519
+ const ws = await workspace(dir);
2520
+ return withLock(ws.root, ws.directory, () => recordExecution(ws.root, ws.directory, event.event, async () => {
2521
+ const inputs = await readInputs(ws.root);
2522
+ const statePath = ws.directory + "/state.json";
2523
+ const raw = await readStoreJson(ws.root, statePath);
2524
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2525
+ const state = raw === null ? {
2526
+ version: 1,
2527
+ root: ws.root,
2528
+ gitDir: ws.gitDir,
2529
+ branch: ws.branch,
2530
+ baselines: [],
2531
+ sessions: {},
2532
+ updatedAt: now,
2533
+ fingerprint: null,
2534
+ latest: null
2535
+ } : parseState(raw);
2536
+ if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
2537
+ throw new Error("Automation state belongs to another branch or worktree; original evidence was retained.");
2538
+ }
2539
+ if (ws.branch === "detached" && state.latest) {
2540
+ const previous = await readStoreJson(ws.root, state.latest);
2541
+ if (!previous?.head || !/^[a-f0-9]{40,64}$/.test(previous.head)) throw new Error("The previous detached checkout evidence is unavailable.");
2542
+ try {
2543
+ await git2(ws.root, "merge-base", "--is-ancestor", previous.head, inputs.head);
2544
+ } catch {
2545
+ throw new Error("Detached checkout moved to a different history; original repair evidence was retained. Inspect that baseline explicitly.");
2546
+ }
2547
+ }
2548
+ const key = event.host && event.sessionId ? hash([event.host, event.sessionId]) : null;
2549
+ const newSession = key !== null && !state.sessions[key];
2550
+ if (key && !state.sessions[key]) {
2551
+ const keys = Object.keys(state.sessions).sort((a, b) => state.sessions[a].lastUsed.localeCompare(state.sessions[b].lastUsed));
2552
+ for (const expired of keys.slice(0, Math.max(0, keys.length - 31))) delete state.sessions[expired];
2553
+ state.sessions[key] = {
2554
+ host: event.host,
2555
+ seen: null,
2556
+ continued: false,
2557
+ initialIssues: [],
2558
+ initialDocs: inputs.docs,
2559
+ lastUsed: now,
2560
+ mutationObserved: false,
2561
+ pending: {},
2562
+ coverageGaps: [],
2563
+ events: {}
2564
+ };
2565
+ }
2566
+ const session = key ? state.sessions[key] : null;
2567
+ if (session) {
2568
+ session.lastUsed = now;
2569
+ session.events[event.event] = { at: now, count: (session.events[event.event]?.count ?? 0) + 1 };
2570
+ if (event.event === "before_tool" && event.mutating && event.toolId) {
2571
+ if (Object.keys(session.pending).length >= 128) throw new Error("Too many unfinished tool calls to track pre-edit evidence.");
2572
+ session.pending[event.toolId] = inputs.fingerprint;
2573
+ }
2574
+ if (event.event === "after_tool" && event.mutating) {
2575
+ session.mutationObserved = true;
2576
+ if (!event.toolId || !session.pending[event.toolId]) {
2577
+ const gap = "A tool completed without an observed matching pre-tool capture; pre-edit coverage is unknown.";
2578
+ if (!session.coverageGaps.includes(gap)) session.coverageGaps.push(gap);
2579
+ }
2580
+ if (event.toolId) delete session.pending[event.toolId];
2581
+ }
2582
+ }
2583
+ if (!state.baselines.length && Object.values(inputs.docs).every((value) => value === null)) {
2584
+ const report2 = {
2585
+ version: 1,
2586
+ status: "unavailable",
2587
+ root: ws.root,
2588
+ branch: ws.branch,
2589
+ head: inputs.head,
2590
+ baselinePaths: [],
2591
+ reportPath: ws.directory + "/checks/" + randomUUID4() + ".json",
2592
+ findings: [],
2593
+ diagnostics: ["No AGENTS.md, CLAUDE.md, or .claude/CLAUDE.md exists. Documentation capture is unavailable; other Mason tools remain usable."],
2594
+ checks: { ran: [], reused: [], skipped: [] },
2595
+ counts: { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 },
2596
+ capture: "unknown",
2597
+ scope: SCOPE
2598
+ };
2599
+ const notify2 = !session || session.seen !== "no-docs";
2600
+ if (session) session.seen = "no-docs";
2601
+ state.fingerprint = inputs.fingerprint;
2602
+ state.latest = report2.reportPath;
2603
+ state.updatedAt = now;
2604
+ await writeStoreJson(ws.root, report2.reportPath, report2);
2605
+ await writeStoreJson(ws.root, statePath, state);
2606
+ return { report: report2, message: notify2 ? summarize(report2) : null, continueOnce: false };
2607
+ }
2608
+ let cached = null;
2609
+ const diagnostics = [];
2610
+ try {
2611
+ cached = await readStoreJson(ws.root, ws.directory + "/cache.json");
2612
+ } catch {
2613
+ diagnostics.push("Unreadable automation cache; checks are being recomputed.");
2614
+ }
2615
+ const cache = checkCache(cached, inputs);
2616
+ if (cache.diagnostic) diagnostics.push(cache.diagnostic);
2617
+ const saveBaseline = async () => {
2618
+ if (state.baselines.length >= 128) throw new Error("128 retained baselines need review; automatic capture stopped without discarding original evidence.");
2619
+ const prepared = await prepareRepair(ws.root, ALL_CHECKS, cache.options);
2620
+ state.baselines.push({ path: prepared.baselinePath, at: now, event: event.event, fingerprint: inputs.fingerprint });
2621
+ };
2622
+ if (!state.baselines.length) await saveBaseline();
2623
+ const verifications = [];
2624
+ for (const baseline of state.baselines) verifications.push(await verifyRepair(ws.root, baseline.path, cache.options));
2625
+ const known = new Set(verifications.flatMap((v) => v.findings.map((f) => f.id)));
2626
+ if (verifications.some((v) => v.newFindings.some((f) => !known.has(findingId(f))))) {
2627
+ await saveBaseline();
2628
+ verifications.push(await verifyRepair(ws.root, state.baselines.at(-1).path, cache.options));
2629
+ }
2630
+ const merged = /* @__PURE__ */ new Map();
2631
+ for (const verification of verifications) {
2632
+ for (const finding of verification.findings) {
2633
+ const previous = merged.get(finding.id);
2634
+ if (!previous || priority[finding.status] > priority[previous.status]) merged.set(finding.id, finding);
2635
+ }
2636
+ diagnostics.push(...verification.diagnostics);
2637
+ }
2638
+ if (newSession && session) session.initialIssues = [...merged.values()].filter((f) => f.status === "unresolved").map((f) => f.id);
2639
+ const current = verifications.at(-1).currentAudit;
2640
+ const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
2641
+ for (const finding of merged.values()) counts[finding.status]++;
2642
+ if (session) diagnostics.push(...session.coverageGaps);
2643
+ const capture = session && !session.coverageGaps.length && (session.events.session_start || session.events.before_tool) ? "observed" : "unknown";
2644
+ const after = await readInputs(ws.root);
2645
+ const currentWs = await workspace(ws.root);
2646
+ if (after.fingerprint !== inputs.fingerprint || currentWs.directory !== ws.directory) {
2647
+ throw new Error("Repository inputs or branch changed during automation; no current verification was recorded. Retry on a stable checkout.");
2648
+ }
2649
+ const report = {
2650
+ version: 1,
2651
+ status: diagnostics.length || verifications.some((v) => v.status === "incomplete") ? "incomplete" : counts.unresolved ? "issues-remain" : "verified",
2652
+ root: ws.root,
2653
+ branch: ws.branch,
2654
+ head: inputs.head,
2655
+ baselinePaths: state.baselines.map((b) => b.path),
2656
+ reportPath: ws.directory + "/checks/" + randomUUID4() + ".json",
2657
+ findings: [...merged.values()],
2658
+ diagnostics: [...new Set(diagnostics)],
2659
+ checks: { ran: [...cache.ran], reused: [...cache.reused].filter((name) => !cache.ran.has(name)), skipped: current?.skippedChecks ?? [] },
2660
+ counts,
2661
+ capture,
2662
+ scope: SCOPE
2663
+ };
2664
+ const signature = hash([report.status, report.findings, report.diagnostics, report.checks.skipped]);
2665
+ const relevant = report.findings.some((f) => f.status === "unresolved" && session && (!session.initialIssues.includes(f.id) || session.initialDocs[f.original.anchor.doc] !== inputs.docs[f.original.anchor.doc]));
2666
+ const continueOnce = event.event === "task_end" && !!session?.mutationObserved && relevant && !session.continued && !event.stopHookActive;
2667
+ const notify = !session || newSession || signature !== session.seen || continueOnce;
2668
+ if (session) {
2669
+ session.seen = signature;
2670
+ if (continueOnce) session.continued = true;
2671
+ }
2672
+ const persistReport = !state.latest || state.fingerprint !== inputs.fingerprint || notify || event.event === "task_end";
2673
+ if (!persistReport) report.reportPath = state.latest;
2674
+ state.updatedAt = now;
2675
+ state.fingerprint = inputs.fingerprint;
2676
+ state.latest = report.reportPath;
2677
+ if (persistReport) await writeStoreJson(ws.root, report.reportPath, report);
2678
+ if (cache.ran.size || cached === null || cache.diagnostic) await writeStoreJson(ws.root, ws.directory + "/cache.json", cache.serialize());
2679
+ await writeStoreJson(ws.root, statePath, state);
2680
+ return { report, message: notify ? summarize(report) : null, continueOnce };
2681
+ }));
2682
+ }
2683
+ async function automationStatus(dir) {
2684
+ const ws = await workspace(dir);
2685
+ const execution = await executionStatus(ws.root, ws.directory);
2686
+ const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
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);
2689
+ if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) throw new Error("Automation state belongs to another workspace.");
2690
+ const inputs = await readInputs(ws.root);
2691
+ const latest = state.latest ? await readStoreJson(ws.root, state.latest) : null;
2692
+ const hosts = {};
2693
+ for (const session of Object.values(state.sessions)) {
2694
+ const host = hosts[session.host] ??= { sessions: 0, observedEvents: [] };
2695
+ host.sessions++;
2696
+ host.observedEvents = [.../* @__PURE__ */ new Set([...host.observedEvents, ...Object.keys(session.events)])];
2697
+ }
2698
+ const unfinished = ["failed", "unknown", "running"].includes(execution.status);
2699
+ return {
2700
+ version: 1,
2701
+ status: unfinished ? "unavailable" : inputs.fingerprint === state.fingerprint ? "current" : "changed",
2702
+ root: ws.root,
2703
+ branch: ws.branch,
2704
+ baselinePaths: state.baselines.map((b) => b.path),
2705
+ reportPath: state.latest,
2706
+ verificationStatus: unfinished ? "unavailable" : latest?.status ?? "unavailable",
2707
+ hosts,
2708
+ execution,
2709
+ note: "Observed events do not prove all tool paths are intercepted. Run check to verify the retained evidence."
2710
+ };
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
+ });
2727
+
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
+ }
2774
+ });
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";
2866
+ function normalizeHook(host, raw) {
2867
+ hostSchema.parse(host);
2868
+ const input2 = inputSchema.parse(raw);
2869
+ const readOnly = host === "claude" ? /^(Read|Glob|Grep|WebSearch|WebFetch)$/ : /^(read_file|list_dir|grep_files)$/;
2870
+ return { cwd: input2.cwd, name: input2.hook_event_name, event: {
2871
+ event: lifecycle[input2.hook_event_name],
2872
+ host,
2873
+ sessionId: input2.session_id,
2874
+ toolId: input2.tool_use_id,
2875
+ mutating: !!input2.tool_name && !readOnly.test(input2.tool_name),
2876
+ stopHookActive: input2.stop_hook_active || input2.permission_mode === "plan"
2877
+ } };
2878
+ }
2879
+ async function runAutomationHook(host, stdin) {
2880
+ let name = "";
2881
+ try {
2882
+ if (Buffer.byteLength(stdin) > 1024 * 1024) throw new Error("Hook input exceeds 1 MiB.");
2883
+ const input2 = normalizeHook(host, JSON.parse(stdin));
2884
+ name = input2.name;
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");
2893
+ if (!result.message) return null;
2894
+ if (name === "Stop") {
2895
+ return result.continueOnce ? { decision: "block", reason: result.message } : { systemMessage: result.message };
2896
+ }
2897
+ return { hookSpecificOutput: { hookEventName: name, additionalContext: result.message } };
2898
+ } catch (error) {
2899
+ const message2 = failureMessage(error);
2900
+ return { systemMessage: message2, ...["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse"].includes(name) ? { hookSpecificOutput: { hookEventName: name, additionalContext: message2 } } : {} };
2901
+ }
2902
+ }
2903
+ function hookConfig(host, command = "npx --no-install --package mason-context mason-auto") {
2904
+ const handler = { type: "command", command: command + " hook --host " + host, timeout: 30 };
2905
+ return { hooks: Object.fromEntries(HOOK_EVENTS.map((name) => [
2906
+ name,
2907
+ [{ ...["PreToolUse", "PostToolUse"].includes(name) ? { matcher: ".*" } : {}, hooks: [{ ...handler }] }]
2908
+ ])) };
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
+ });
2937
+
2938
+ // src/automation/install.ts
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";
2948
+ async function installAutomation(dir, host, command) {
2949
+ const ws = await workspace(dir);
2950
+ return withLock(ws.root, ".mason/reports/automation-install", () => installLocked(ws.root, host, command));
2951
+ }
2952
+ async function planAutomationInstall(root, host, command) {
2953
+ const file = configPath(host);
2954
+ const existing = automationConfigSchema.parse(await readStoreJson(root, file) ?? {});
2955
+ const record = recordSchema.parse(await readStoreJson(root, ".mason/automation.json") ?? { version: 1, hosts: {} });
2956
+ const desired = hookConfig(host, command);
2957
+ const newCommand = desired.hooks.SessionStart[0].hooks[0].command;
2958
+ const previous = record.hosts[host]?.command;
2959
+ const hooks = existing.hooks ?? {};
2960
+ for (const event of HOOK_EVENTS) {
2961
+ hooks[event] = (hooks[event] ?? []).map((group) => ({
2962
+ ...group,
2963
+ hooks: group.hooks.filter((handler) => !(handler.type === "command" && typeof handler.command === "string" && (handler.command === previous || handler.command === newCommand)))
2964
+ })).filter((group) => group.hooks.length);
2965
+ hooks[event].push(...desired.hooks[event]);
2966
+ }
2967
+ record.hosts[host] = { command: newCommand };
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);
2973
+ await writeStoreJson(root, ".mason/automation.json", record);
2974
+ return {
2975
+ version: 1,
2976
+ host,
2977
+ configPath: file,
2978
+ status: "configured",
2979
+ command: newCommand,
2980
+ events: HOOK_EVENTS,
2981
+ next: host === "codex" ? "Review/trust these hooks using Codex /hooks and start a new session. mason-auto status reports observed events separately from configuration." : "Start a new Claude Code session. mason-auto status reports observed events separately from configuration.",
2982
+ note: "Install mason-context in the project before using the default command. Ignore .mason/reports/ to keep local evidence out of commits. Hooks preserve evidence and suggest scoped repairs; they do not approve edits or decisions."
2983
+ };
2984
+ }
2985
+ async function installedAutomation(dir) {
2986
+ const ws = await workspace(dir);
2987
+ const raw = await readStoreJson(ws.root, ".mason/automation.json");
2988
+ if (raw === null) return {};
2989
+ const record = recordSchema.parse(raw);
2990
+ const result = {};
2991
+ for (const host of ["claude", "codex"]) {
2992
+ const expected = record.hosts[host];
2993
+ if (!expected) continue;
2994
+ const current = automationConfigSchema.parse(await readStoreJson(ws.root, configPath(host)) ?? {});
2995
+ const configuredEvents = HOOK_EVENTS.filter((event) => current.hooks?.[event]?.some((group) => group.hooks.some((handler) => handler.type === "command" && handler.command === expected.command)));
2996
+ result[host] = {
2997
+ configPath: configPath(host),
2998
+ configuredEvents,
2999
+ status: current.disableAllHooks === true ? "disabled" : configuredEvents.length === HOOK_EVENTS.length ? "configured" : "incomplete",
3000
+ runtime: "Host version, trust, policy, and tool coverage still determine execution; inspect observed events."
3001
+ };
3002
+ }
3003
+ return result;
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
+ });
4383
+
4384
+ // src/automation/cli.ts
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]
4392
+
4393
+ setup [--host claude|codex] Install a pinned runtime, MCP, instructions and hooks; retain the initial audit
4394
+ install --host claude|codex Merge lifecycle hooks into this project's host config
4395
+ config --host claude|codex Print the host config without writing
4396
+ status Read configured hooks and observed runtime events
4397
+ check Capture/resume and verify retained audit evidence
4398
+ hook --host claude|codex Handle host JSON on stdin
4399
+
4400
+ --dir <path> Project directory (defaults to cwd)
4401
+ --command <prefix> Installed executable prefix for install/config
4402
+ --json Machine-readable output (status also uses JSON when piped)
4403
+
4404
+ check exits 0 for verified checks, 1 for issues, 2 for incomplete/unavailable.
4405
+ Hooks are advisory and exit 0; a failed capture is reported explicitly.
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
+ }
4422
+ async function runAutomationCli(argv2, stdin = "", io = {
4423
+ out: (s) => process.stdout.write(s + "\n"),
4424
+ err: (s) => process.stderr.write(s + "\n")
4425
+ }) {
4426
+ let action = "";
4427
+ try {
4428
+ const { values, positionals } = parseCli(argv2);
4429
+ if (values.help || !positionals.length) {
4430
+ io.out(USAGE);
4431
+ return 0;
4432
+ }
4433
+ if (positionals.length !== 1) throw new Error("Expected one command.");
4434
+ [action] = positionals;
4435
+ const dir = values.dir ?? process.cwd();
4436
+ if (action === "hook") {
4437
+ const output = await runAutomationHook(hostSchema.parse(values.host), stdin);
4438
+ if (output) io.out(JSON.stringify(output));
4439
+ return 0;
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
+ }
4448
+ if (action === "install" || action === "config") {
4449
+ const host = hostSchema.parse(values.host);
4450
+ io.out(JSON.stringify(action === "install" ? await installAutomation(dir, host, values.command) : hookConfig(host, values.command), null, 2));
4451
+ return 0;
4452
+ }
4453
+ if (values.host || values.command) throw new Error("--host and --command apply only to install/config/hook.");
4454
+ if (action === "status") {
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));
4459
+ return 0;
4460
+ }
4461
+ if (action !== "check") throw new Error("Unknown automation command: " + action);
4462
+ const { report } = await automate(dir, { event: "task_end" });
4463
+ io.out(values.json ? JSON.stringify(report, null, 2) : summarize(report));
4464
+ return report.status === "verified" ? 0 : report.status === "issues-remain" ? 1 : 2;
4465
+ } catch (error) {
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 }));
4469
+ return 0;
4470
+ }
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);
4473
+ return 2;
4474
+ }
4475
+ }
4476
+
4477
+ // bin/mason-auto.ts
4478
+ var argv = process.argv.slice(2);
4479
+ var input = "";
4480
+ if (isHookCommand(argv) && !process.stdin.isTTY) {
4481
+ for await (const chunk of process.stdin) {
4482
+ input += chunk.toString();
4483
+ if (Buffer.byteLength(input) > 1024 * 1024) break;
4484
+ }
4485
+ }
4486
+ process.exitCode = await runAutomationCli(argv, input);
4487
+ //# sourceMappingURL=mason-auto.js.map