mason-context 0.16.1 → 0.16.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +15 -8
- package/dist/mason-audit.js +86 -66
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-auto.js +989 -776
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-drift.js +13 -0
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js +13 -0
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +1385 -1177
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +62 -49
- package/dist/mason-review.js.map +1 -1
- package/dist/mason.js +1060 -842
- package/dist/mason.js.map +1 -1
- package/package.json +2 -1
package/dist/mason-auto.js
CHANGED
|
@@ -248,320 +248,101 @@ var init_storage = __esm({
|
|
|
248
248
|
}
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
-
// src/automation/
|
|
251
|
+
// src/automation/store.ts
|
|
252
|
+
import fs3 from "fs/promises";
|
|
252
253
|
import os from "os";
|
|
253
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
254
254
|
import { z } from "zod";
|
|
255
|
-
function
|
|
256
|
-
try {
|
|
257
|
-
return executionSchema.parse(raw);
|
|
258
|
-
} catch (error) {
|
|
259
|
-
throw new Error("Invalid automation execution store; receipt history was retained.", { cause: error });
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
function automationFailure(error) {
|
|
263
|
-
const recorded = failureSchema.safeParse(error?.failure);
|
|
264
|
-
if (recorded.success) return recorded.data;
|
|
265
|
-
const message2 = (error instanceof Error ? error.message : String(error)).replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 700);
|
|
266
|
-
const codes = /* @__PURE__ */ new Set();
|
|
267
|
-
let cause = error;
|
|
268
|
-
for (let i = 0; cause && i < 8; i++) {
|
|
269
|
-
codes.add(String(cause.code));
|
|
270
|
-
cause = cause.cause;
|
|
271
|
-
}
|
|
272
|
-
const code = codes.has("ENOSPC") || codes.has("EDQUOT") ? "storage-full" : /changed during|changed while|changed between/.test(message2) ? "inputs-changed" : /Automation is busy/.test(message2) ? "busy" : /not a git repository|unknown revision|bad revision|different history|unreachable/.test(message2) ? "history-unavailable" : error instanceof z.ZodError || error instanceof SyntaxError || /Hook input|Expected one command|Unknown automation command|--host/.test(message2) ? "invalid-input" : /store|baseline|modified|symbolic link|Symlink|automation state|state belongs/.test(message2) ? "invalid-evidence" : [...codes].some((c) => /^E[A-Z]+$/.test(c)) ? "io-error" : "internal";
|
|
273
|
-
return { code, message: message2, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
|
|
274
|
-
}
|
|
275
|
-
function failureMessage(error) {
|
|
276
|
-
const failure = automationFailure(error);
|
|
277
|
-
return `Mason automation unavailable [${failure.code}]; evidence capture/verification was not established. ${failure.message}` + (failure.receiptRecorded ? "" : " No durable failure receipt was recorded.");
|
|
278
|
-
}
|
|
279
|
-
async function recordExecution(root, directory, event, run) {
|
|
280
|
-
const file = directory + "/execution.json";
|
|
281
|
-
const raw = await readStoreJson(root, file);
|
|
282
|
-
const log = raw === null ? { version: 1, attempts: [], discardedAttempts: 0 } : parseExecution(raw);
|
|
283
|
-
for (const attempt2 of log.attempts) if (attempt2.status === "running") attempt2.status = "unknown";
|
|
284
|
-
log.discardedAttempts += Math.max(0, log.attempts.length - 31);
|
|
285
|
-
log.attempts = log.attempts.slice(-31);
|
|
286
|
-
const started = performance.now();
|
|
287
|
-
const attempt = {
|
|
288
|
-
id: randomUUID2(),
|
|
289
|
-
event,
|
|
290
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
291
|
-
pid: process.pid,
|
|
292
|
-
host: os.hostname(),
|
|
293
|
-
status: "running"
|
|
294
|
-
};
|
|
295
|
-
log.attempts.push(attempt);
|
|
296
|
-
await writeStoreJson(root, file, log);
|
|
255
|
+
function parseState(raw) {
|
|
297
256
|
try {
|
|
298
|
-
|
|
299
|
-
Object.assign(attempt, {
|
|
300
|
-
status: "completed",
|
|
301
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
302
|
-
durationMs: performance.now() - started,
|
|
303
|
-
verificationStatus: result.report.status,
|
|
304
|
-
reportPath: result.report.reportPath
|
|
305
|
-
});
|
|
306
|
-
await writeStoreJson(root, file, log);
|
|
307
|
-
return result;
|
|
257
|
+
return stateSchema.parse(raw);
|
|
308
258
|
} catch (error) {
|
|
309
|
-
|
|
310
|
-
Object.assign(attempt, { status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), durationMs: performance.now() - started, failure });
|
|
311
|
-
delete attempt.verificationStatus;
|
|
312
|
-
delete attempt.reportPath;
|
|
313
|
-
try {
|
|
314
|
-
await writeStoreJson(root, file, { ...log, attempts: log.attempts.map((a) => a === attempt ? { ...a, failure: { ...failure, receiptRecorded: true } } : a) });
|
|
315
|
-
failure.receiptRecorded = true;
|
|
316
|
-
} catch {
|
|
317
|
-
}
|
|
318
|
-
throw Object.assign(new Error(failure.message, { cause: error }), { failure });
|
|
259
|
+
throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
|
|
319
260
|
}
|
|
320
261
|
}
|
|
321
|
-
async function
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (log.attempts.some((attempt) => attempt.status === "running")) {
|
|
262
|
+
async function withLock(root, directory, run, waitMs = 5e3) {
|
|
263
|
+
const file = await storePath(root, directory + "/lock", true);
|
|
264
|
+
const deadline = Date.now() + waitMs;
|
|
265
|
+
let handle;
|
|
266
|
+
while (!handle) {
|
|
327
267
|
try {
|
|
328
|
-
|
|
329
|
-
} catch {
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
for (const attempt of log.attempts) {
|
|
333
|
-
if (attempt.status !== "running") continue;
|
|
334
|
-
let alive = false;
|
|
335
|
-
if (attempt.host === os.hostname() && lock?.pid === attempt.pid && lock.host === attempt.host) {
|
|
268
|
+
handle = await fs3.open(file, "wx", 384);
|
|
336
269
|
try {
|
|
337
|
-
|
|
338
|
-
alive = true;
|
|
270
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
|
|
339
271
|
} catch (error) {
|
|
340
|
-
|
|
272
|
+
await handle.close().catch(() => {
|
|
273
|
+
});
|
|
274
|
+
handle = void 0;
|
|
275
|
+
await fs3.rm(file, { force: true }).catch(() => {
|
|
276
|
+
});
|
|
277
|
+
throw error;
|
|
341
278
|
}
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (error.code !== "EEXIST") throw error;
|
|
281
|
+
try {
|
|
282
|
+
const owner = JSON.parse(await fs3.readFile(file, "utf8"));
|
|
283
|
+
if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
284
|
+
try {
|
|
285
|
+
process.kill(owner.pid, 0);
|
|
286
|
+
} catch (probe) {
|
|
287
|
+
if (probe.code === "ESRCH") {
|
|
288
|
+
const reclaim = file + ".reclaim";
|
|
289
|
+
let guard;
|
|
290
|
+
try {
|
|
291
|
+
guard = await fs3.open(reclaim, "wx", 384);
|
|
292
|
+
const current = JSON.parse(await fs3.readFile(file, "utf8"));
|
|
293
|
+
if (current.pid === owner.pid && current.host === owner.host) await fs3.unlink(file);
|
|
294
|
+
} finally {
|
|
295
|
+
if (guard) {
|
|
296
|
+
await guard.close();
|
|
297
|
+
await fs3.rm(reclaim, { force: true });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
if (Date.now() >= deadline) throw new Error("Automation is busy or its lock needs inspection: " + file);
|
|
306
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
342
307
|
}
|
|
343
|
-
if (!alive) attempt.status = "unknown";
|
|
344
|
-
}
|
|
345
|
-
return { status: log.attempts.at(-1)?.status ?? "not-observed", attempts: log.attempts, discardedAttempts: log.discardedAttempts };
|
|
346
|
-
}
|
|
347
|
-
var failureSchema, attemptSchema, executionSchema;
|
|
348
|
-
var init_execution = __esm({
|
|
349
|
-
"src/automation/execution.ts"() {
|
|
350
|
-
"use strict";
|
|
351
|
-
init_storage();
|
|
352
|
-
failureSchema = z.object({
|
|
353
|
-
code: z.enum(["inputs-changed", "storage-full", "busy", "invalid-input", "history-unavailable", "invalid-evidence", "io-error", "internal"]),
|
|
354
|
-
message: z.string(),
|
|
355
|
-
retryable: z.boolean(),
|
|
356
|
-
receiptRecorded: z.boolean()
|
|
357
|
-
});
|
|
358
|
-
attemptSchema = z.object({
|
|
359
|
-
id: z.string(),
|
|
360
|
-
event: z.string(),
|
|
361
|
-
startedAt: z.string(),
|
|
362
|
-
finishedAt: z.string().optional(),
|
|
363
|
-
durationMs: z.number().nonnegative().optional(),
|
|
364
|
-
pid: z.number().int().positive(),
|
|
365
|
-
host: z.string(),
|
|
366
|
-
status: z.enum(["running", "completed", "failed", "unknown"]),
|
|
367
|
-
verificationStatus: z.string().optional(),
|
|
368
|
-
reportPath: z.string().optional(),
|
|
369
|
-
failure: failureSchema.optional()
|
|
370
|
-
});
|
|
371
|
-
executionSchema = z.object({ version: z.literal(1), attempts: z.array(attemptSchema).max(32), discardedAttempts: z.number().int().nonnegative().default(0) });
|
|
372
|
-
}
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
// src/test-map.ts
|
|
376
|
-
import path4 from "path";
|
|
377
|
-
async function buildTestMap(dir) {
|
|
378
|
-
const rootDir = path4.resolve(dir);
|
|
379
|
-
const access = await createFileAccess(rootDir);
|
|
380
|
-
const testPatterns = [
|
|
381
|
-
"**/*.test.*",
|
|
382
|
-
"**/*.spec.*",
|
|
383
|
-
"**/*Test.kt",
|
|
384
|
-
"**/*Test.java",
|
|
385
|
-
"**/*Tests.kt",
|
|
386
|
-
"**/*Tests.java",
|
|
387
|
-
"**/test_*.py",
|
|
388
|
-
"**/*_test.py",
|
|
389
|
-
"**/*_test.go",
|
|
390
|
-
"**/*Tests.swift",
|
|
391
|
-
"**/*Test.swift",
|
|
392
|
-
"**/*_test.rs"
|
|
393
|
-
];
|
|
394
|
-
const testFiles = await access.list(testPatterns);
|
|
395
|
-
const sourceFiles = await access.list();
|
|
396
|
-
const sourceByBaseName = /* @__PURE__ */ new Map();
|
|
397
|
-
for (const file of sourceFiles) {
|
|
398
|
-
if (testFiles.includes(file)) continue;
|
|
399
|
-
const baseName = path4.basename(file).replace(/\.[^.]+$/, "");
|
|
400
|
-
const existing = sourceByBaseName.get(baseName) ?? [];
|
|
401
|
-
existing.push(file);
|
|
402
|
-
sourceByBaseName.set(baseName, existing);
|
|
403
|
-
}
|
|
404
|
-
const paired = [];
|
|
405
|
-
const unmatched = [];
|
|
406
|
-
for (const testFile of testFiles) {
|
|
407
|
-
const testBaseName = path4.basename(testFile).replace(/\.[^.]+$/, "");
|
|
408
|
-
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
409
|
-
if (!sourceName) {
|
|
410
|
-
unmatched.push(testFile);
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
|
-
const candidates = sourceByBaseName.get(sourceName);
|
|
414
|
-
if (candidates && candidates.length > 0) {
|
|
415
|
-
const testDir = path4.dirname(testFile);
|
|
416
|
-
const bestMatch = candidates.reduce((best, candidate) => {
|
|
417
|
-
const candidateDir = path4.dirname(candidate);
|
|
418
|
-
const bestDir = path4.dirname(best);
|
|
419
|
-
const candidateOverlap = commonSegments(testDir, candidateDir);
|
|
420
|
-
const bestOverlap = commonSegments(testDir, bestDir);
|
|
421
|
-
return candidateOverlap > bestOverlap ? candidate : best;
|
|
422
|
-
});
|
|
423
|
-
paired.push({
|
|
424
|
-
test: testFile,
|
|
425
|
-
source: bestMatch,
|
|
426
|
-
confidence: candidates.length === 1 ? "exact" : "best-guess"
|
|
427
|
-
});
|
|
428
|
-
} else {
|
|
429
|
-
unmatched.push(testFile);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
return { totalTestFiles: testFiles.length, paired, unmatched };
|
|
433
|
-
}
|
|
434
|
-
function commonSegments(pathA, pathB) {
|
|
435
|
-
const segsA = pathA.split("/");
|
|
436
|
-
const segsB = pathB.split("/");
|
|
437
|
-
let count3 = 0;
|
|
438
|
-
for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
|
|
439
|
-
if (segsA[i] === segsB[i]) count3++;
|
|
440
|
-
else break;
|
|
441
|
-
}
|
|
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
308
|
}
|
|
467
|
-
}
|
|
468
|
-
async function getCurrentGitHash(rootDir) {
|
|
469
309
|
try {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
} catch {
|
|
475
|
-
return "unknown";
|
|
310
|
+
return await run();
|
|
311
|
+
} finally {
|
|
312
|
+
await handle.close();
|
|
313
|
+
await fs3.unlink(file);
|
|
476
314
|
}
|
|
477
315
|
}
|
|
478
|
-
var
|
|
479
|
-
var
|
|
480
|
-
"src/
|
|
316
|
+
var hostSchema, events, stateSchema;
|
|
317
|
+
var init_store = __esm({
|
|
318
|
+
"src/automation/store.ts"() {
|
|
481
319
|
"use strict";
|
|
482
|
-
init_files();
|
|
483
|
-
init_files();
|
|
484
320
|
init_storage();
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
flows: z2.record(flowSchema)
|
|
511
|
-
}).passthrough();
|
|
512
|
-
}
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
// src/drift/drift.ts
|
|
516
|
-
import fs3 from "fs/promises";
|
|
517
|
-
import path6 from "path";
|
|
518
|
-
import { execFile as execFile3 } from "child_process";
|
|
519
|
-
import { promisify as promisify3 } from "util";
|
|
520
|
-
function parseChanges(output) {
|
|
521
|
-
const fields = output.split("\0");
|
|
522
|
-
const changes = [];
|
|
523
|
-
for (let i = 0; i < fields.length && fields[i]; ) {
|
|
524
|
-
const code = fields[i++];
|
|
525
|
-
const first = fields[i++];
|
|
526
|
-
if (!first) break;
|
|
527
|
-
const second = /^[RC]/.test(code) ? fields[i++] : void 0;
|
|
528
|
-
const change = second ? code.startsWith("R") ? { status: "renamed", path: second, previousPath: first } : { status: "added", path: second } : { status: code === "A" ? "added" : code === "D" ? "deleted" : "modified", path: first };
|
|
529
|
-
if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/"))) continue;
|
|
530
|
-
changes.push(change);
|
|
531
|
-
}
|
|
532
|
-
return changes;
|
|
533
|
-
}
|
|
534
|
-
function touchedPaths(changes) {
|
|
535
|
-
return [...new Set(changes.flatMap((c) => c.previousPath ? [c.previousPath, c.path] : [c.path]))].sort();
|
|
536
|
-
}
|
|
537
|
-
async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
|
|
538
|
-
if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-")) return null;
|
|
539
|
-
try {
|
|
540
|
-
const { stdout } = await exec3("git", ["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 });
|
|
541
|
-
return parseChanges(stdout);
|
|
542
|
-
} catch {
|
|
543
|
-
return null;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
async function getWorkingTree(resolvedRoot) {
|
|
547
|
-
try {
|
|
548
|
-
const [diff, untracked] = await Promise.all([
|
|
549
|
-
exec3("git", ["diff", "--name-status", "-z", "-M", "HEAD", "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }),
|
|
550
|
-
exec3("git", ["ls-files", "-z", "--others", "--exclude-standard"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 })
|
|
551
|
-
]);
|
|
552
|
-
const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
|
|
553
|
-
return { available: true, changedFiles: [.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])].sort(), untrackedFiles };
|
|
554
|
-
} catch {
|
|
555
|
-
return { available: false, changedFiles: [], untrackedFiles: [] };
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
var exec3;
|
|
559
|
-
var init_drift = __esm({
|
|
560
|
-
"src/drift/drift.ts"() {
|
|
561
|
-
"use strict";
|
|
562
|
-
init_snapshot();
|
|
563
|
-
init_paths();
|
|
564
|
-
exec3 = promisify3(execFile3);
|
|
321
|
+
hostSchema = z.enum(["claude", "codex"]);
|
|
322
|
+
events = ["session_start", "turn_start", "before_tool", "after_tool", "task_end"];
|
|
323
|
+
stateSchema = z.object({
|
|
324
|
+
version: z.literal(1),
|
|
325
|
+
root: z.string(),
|
|
326
|
+
gitDir: z.string(),
|
|
327
|
+
branch: z.string(),
|
|
328
|
+
baselines: z.array(z.object({ path: z.string(), at: z.string(), event: z.string(), fingerprint: z.string() })).max(128),
|
|
329
|
+
sessions: z.record(z.object({
|
|
330
|
+
host: hostSchema,
|
|
331
|
+
seen: z.string().nullable(),
|
|
332
|
+
continued: z.boolean(),
|
|
333
|
+
initialIssues: z.array(z.string()),
|
|
334
|
+
initialDocs: z.record(z.string().nullable()),
|
|
335
|
+
lastUsed: z.string(),
|
|
336
|
+
mutationObserved: z.boolean(),
|
|
337
|
+
pending: z.record(z.string()),
|
|
338
|
+
coverageGaps: z.array(z.string()),
|
|
339
|
+
events: z.record(z.object({ at: z.string(), count: z.number().int().positive() }))
|
|
340
|
+
})),
|
|
341
|
+
updatedAt: z.string(),
|
|
342
|
+
fingerprint: z.string().nullable(),
|
|
343
|
+
latest: z.string().nullable(),
|
|
344
|
+
analysis: z.object({ id: z.string(), fingerprint: z.string(), completedAt: z.number(), path: z.string() }).optional()
|
|
345
|
+
});
|
|
565
346
|
}
|
|
566
347
|
});
|
|
567
348
|
|
|
@@ -849,8 +630,8 @@ var init_claims = __esm({
|
|
|
849
630
|
});
|
|
850
631
|
|
|
851
632
|
// src/audit/git.ts
|
|
852
|
-
import { execFile as
|
|
853
|
-
import { promisify as
|
|
633
|
+
import { execFile as execFile2 } from "child_process";
|
|
634
|
+
import { promisify as promisify2 } from "util";
|
|
854
635
|
function parseCommitLine(line) {
|
|
855
636
|
const parts = line.split(" ");
|
|
856
637
|
if (parts.length < 3 || !parts[0]) return null;
|
|
@@ -858,7 +639,7 @@ function parseCommitLine(line) {
|
|
|
858
639
|
}
|
|
859
640
|
async function lastCommitOf(resolvedRoot, relPath) {
|
|
860
641
|
try {
|
|
861
|
-
const { stdout } = await
|
|
642
|
+
const { stdout } = await exec2(
|
|
862
643
|
"git",
|
|
863
644
|
["log", "-1", `--format=${COMMIT_FORMAT}`, "--", relPath],
|
|
864
645
|
{ cwd: resolvedRoot }
|
|
@@ -871,7 +652,7 @@ async function lastCommitOf(resolvedRoot, relPath) {
|
|
|
871
652
|
}
|
|
872
653
|
async function deletingCommitOf(resolvedRoot, relPath) {
|
|
873
654
|
try {
|
|
874
|
-
const { stdout } = await
|
|
655
|
+
const { stdout } = await exec2(
|
|
875
656
|
"git",
|
|
876
657
|
[
|
|
877
658
|
"log",
|
|
@@ -891,7 +672,7 @@ async function deletingCommitOf(resolvedRoot, relPath) {
|
|
|
891
672
|
}
|
|
892
673
|
async function firstCommitOf(resolvedRoot, relPath) {
|
|
893
674
|
try {
|
|
894
|
-
const { stdout } = await
|
|
675
|
+
const { stdout } = await exec2(
|
|
895
676
|
"git",
|
|
896
677
|
["log", "--reverse", `--format=${COMMIT_FORMAT}`, "--", relPath],
|
|
897
678
|
{ cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
|
|
@@ -905,7 +686,7 @@ async function firstCommitOf(resolvedRoot, relPath) {
|
|
|
905
686
|
async function commitsTouchingSince(resolvedRoot, fromHash, pathspecs) {
|
|
906
687
|
if (!fromHash || fromHash === "unknown") return null;
|
|
907
688
|
try {
|
|
908
|
-
const { stdout } = await
|
|
689
|
+
const { stdout } = await exec2(
|
|
909
690
|
"git",
|
|
910
691
|
[
|
|
911
692
|
"log",
|
|
@@ -930,23 +711,23 @@ async function commitsTouchingSince(resolvedRoot, fromHash, pathspecs) {
|
|
|
930
711
|
return null;
|
|
931
712
|
}
|
|
932
713
|
}
|
|
933
|
-
var
|
|
714
|
+
var exec2, COMMIT_FORMAT;
|
|
934
715
|
var init_git = __esm({
|
|
935
716
|
"src/audit/git.ts"() {
|
|
936
717
|
"use strict";
|
|
937
|
-
|
|
718
|
+
exec2 = promisify2(execFile2);
|
|
938
719
|
COMMIT_FORMAT = "%H%x09%cI%x09%s";
|
|
939
720
|
}
|
|
940
721
|
});
|
|
941
722
|
|
|
942
723
|
// src/audit/docs.ts
|
|
943
724
|
import fs4 from "fs/promises";
|
|
944
|
-
import
|
|
945
|
-
import { execFile as
|
|
946
|
-
import { promisify as
|
|
725
|
+
import path4 from "path";
|
|
726
|
+
import { execFile as execFile3 } from "child_process";
|
|
727
|
+
import { promisify as promisify3 } from "util";
|
|
947
728
|
async function isDirty(resolvedRoot, relPath) {
|
|
948
729
|
try {
|
|
949
|
-
const { stdout } = await
|
|
730
|
+
const { stdout } = await exec3(
|
|
950
731
|
"git",
|
|
951
732
|
["status", "--porcelain", "--", relPath],
|
|
952
733
|
{ cwd: resolvedRoot }
|
|
@@ -960,7 +741,7 @@ async function discoverDocs(resolvedRoot) {
|
|
|
960
741
|
const docs = await Promise.all(DOC_CANDIDATES.map(async (candidate) => {
|
|
961
742
|
let content2;
|
|
962
743
|
try {
|
|
963
|
-
content2 = await fs4.readFile(
|
|
744
|
+
content2 = await fs4.readFile(path4.join(resolvedRoot, candidate), "utf-8");
|
|
964
745
|
} catch {
|
|
965
746
|
return null;
|
|
966
747
|
}
|
|
@@ -976,18 +757,211 @@ async function discoverDocs(resolvedRoot) {
|
|
|
976
757
|
}));
|
|
977
758
|
return docs.filter((doc) => doc !== null);
|
|
978
759
|
}
|
|
979
|
-
var
|
|
760
|
+
var exec3, DOC_CANDIDATES;
|
|
980
761
|
var init_docs = __esm({
|
|
981
762
|
"src/audit/docs.ts"() {
|
|
982
763
|
"use strict";
|
|
983
|
-
init_claims();
|
|
984
|
-
init_git();
|
|
764
|
+
init_claims();
|
|
765
|
+
init_git();
|
|
766
|
+
exec3 = promisify3(execFile3);
|
|
767
|
+
DOC_CANDIDATES = [
|
|
768
|
+
"AGENTS.md",
|
|
769
|
+
"CLAUDE.md",
|
|
770
|
+
".claude/CLAUDE.md"
|
|
771
|
+
];
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
// src/test-map.ts
|
|
776
|
+
import path5 from "path";
|
|
777
|
+
async function buildTestMap(dir) {
|
|
778
|
+
const rootDir = path5.resolve(dir);
|
|
779
|
+
const access = await createFileAccess(rootDir);
|
|
780
|
+
const testPatterns = [
|
|
781
|
+
"**/*.test.*",
|
|
782
|
+
"**/*.spec.*",
|
|
783
|
+
"**/*Test.kt",
|
|
784
|
+
"**/*Test.java",
|
|
785
|
+
"**/*Tests.kt",
|
|
786
|
+
"**/*Tests.java",
|
|
787
|
+
"**/test_*.py",
|
|
788
|
+
"**/*_test.py",
|
|
789
|
+
"**/*_test.go",
|
|
790
|
+
"**/*Tests.swift",
|
|
791
|
+
"**/*Test.swift",
|
|
792
|
+
"**/*_test.rs"
|
|
793
|
+
];
|
|
794
|
+
const testFiles = await access.list(testPatterns);
|
|
795
|
+
const sourceFiles = await access.list();
|
|
796
|
+
const sourceByBaseName = /* @__PURE__ */ new Map();
|
|
797
|
+
for (const file of sourceFiles) {
|
|
798
|
+
if (testFiles.includes(file)) continue;
|
|
799
|
+
const baseName = path5.basename(file).replace(/\.[^.]+$/, "");
|
|
800
|
+
const existing = sourceByBaseName.get(baseName) ?? [];
|
|
801
|
+
existing.push(file);
|
|
802
|
+
sourceByBaseName.set(baseName, existing);
|
|
803
|
+
}
|
|
804
|
+
const paired = [];
|
|
805
|
+
const unmatched = [];
|
|
806
|
+
for (const testFile of testFiles) {
|
|
807
|
+
const testBaseName = path5.basename(testFile).replace(/\.[^.]+$/, "");
|
|
808
|
+
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
809
|
+
if (!sourceName) {
|
|
810
|
+
unmatched.push(testFile);
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
const candidates = sourceByBaseName.get(sourceName);
|
|
814
|
+
if (candidates && candidates.length > 0) {
|
|
815
|
+
const testDir = path5.dirname(testFile);
|
|
816
|
+
const bestMatch = candidates.reduce((best, candidate) => {
|
|
817
|
+
const candidateDir = path5.dirname(candidate);
|
|
818
|
+
const bestDir = path5.dirname(best);
|
|
819
|
+
const candidateOverlap = commonSegments(testDir, candidateDir);
|
|
820
|
+
const bestOverlap = commonSegments(testDir, bestDir);
|
|
821
|
+
return candidateOverlap > bestOverlap ? candidate : best;
|
|
822
|
+
});
|
|
823
|
+
paired.push({
|
|
824
|
+
test: testFile,
|
|
825
|
+
source: bestMatch,
|
|
826
|
+
confidence: candidates.length === 1 ? "exact" : "best-guess"
|
|
827
|
+
});
|
|
828
|
+
} else {
|
|
829
|
+
unmatched.push(testFile);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return { totalTestFiles: testFiles.length, paired, unmatched };
|
|
833
|
+
}
|
|
834
|
+
function commonSegments(pathA, pathB) {
|
|
835
|
+
const segsA = pathA.split("/");
|
|
836
|
+
const segsB = pathB.split("/");
|
|
837
|
+
let count3 = 0;
|
|
838
|
+
for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
|
|
839
|
+
if (segsA[i] === segsB[i]) count3++;
|
|
840
|
+
else break;
|
|
841
|
+
}
|
|
842
|
+
return count3;
|
|
843
|
+
}
|
|
844
|
+
var init_test_map = __esm({
|
|
845
|
+
"src/test-map.ts"() {
|
|
846
|
+
"use strict";
|
|
847
|
+
init_files();
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
// src/snapshot/snapshot.ts
|
|
852
|
+
import path6 from "path";
|
|
853
|
+
import { execFile as execFile4 } from "child_process";
|
|
854
|
+
import { promisify as promisify4 } from "util";
|
|
855
|
+
import { z as z2 } from "zod";
|
|
856
|
+
async function inspectSnapshot(rootDir) {
|
|
857
|
+
try {
|
|
858
|
+
const raw = await readStoreJson(rootDir, ".mason/snapshot.json");
|
|
859
|
+
const snapshot = raw === null ? null : snapshotSchema.parse(raw);
|
|
860
|
+
return { status: snapshot ? "available" : "missing", snapshot, diagnostics: [] };
|
|
861
|
+
} catch (error) {
|
|
862
|
+
return { status: "invalid", snapshot: null, diagnostics: [{
|
|
863
|
+
path: ".mason/snapshot.json",
|
|
864
|
+
message: error instanceof Error ? error.message : String(error)
|
|
865
|
+
}] };
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
async function getCurrentGitHash(rootDir) {
|
|
869
|
+
try {
|
|
870
|
+
const { stdout } = await exec4("git", ["rev-parse", "HEAD"], {
|
|
871
|
+
cwd: rootDir
|
|
872
|
+
});
|
|
873
|
+
return stdout.trim();
|
|
874
|
+
} catch {
|
|
875
|
+
return "unknown";
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
var exec4, repoPath, verificationFields, featureSchema, flowSchema, snapshotSchema;
|
|
879
|
+
var init_snapshot = __esm({
|
|
880
|
+
"src/snapshot/snapshot.ts"() {
|
|
881
|
+
"use strict";
|
|
882
|
+
init_files();
|
|
883
|
+
init_files();
|
|
884
|
+
init_storage();
|
|
885
|
+
init_paths();
|
|
886
|
+
init_test_map();
|
|
887
|
+
exec4 = promisify4(execFile4);
|
|
888
|
+
repoPath = z2.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
|
|
889
|
+
verificationFields = {
|
|
890
|
+
refreshedHash: z2.string().optional(),
|
|
891
|
+
verifiedAt: z2.string().optional(),
|
|
892
|
+
verifiedHash: z2.string().optional(),
|
|
893
|
+
verificationFailed: z2.boolean().optional(),
|
|
894
|
+
verificationNote: z2.string().optional()
|
|
895
|
+
};
|
|
896
|
+
featureSchema = z2.object({
|
|
897
|
+
description: z2.string(),
|
|
898
|
+
files: z2.array(repoPath),
|
|
899
|
+
tests: z2.array(repoPath).optional(),
|
|
900
|
+
type: z2.enum(["capability", "infrastructure"]).optional(),
|
|
901
|
+
...verificationFields
|
|
902
|
+
}).passthrough();
|
|
903
|
+
flowSchema = z2.object({ description: z2.string(), chain: z2.array(repoPath), ...verificationFields }).passthrough();
|
|
904
|
+
snapshotSchema = z2.object({
|
|
905
|
+
version: z2.literal(2),
|
|
906
|
+
createdAt: z2.string(),
|
|
907
|
+
updatedAt: z2.string(),
|
|
908
|
+
gitHash: z2.string(),
|
|
909
|
+
features: z2.record(featureSchema),
|
|
910
|
+
flows: z2.record(flowSchema)
|
|
911
|
+
}).passthrough();
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
// src/drift/drift.ts
|
|
916
|
+
import fs5 from "fs/promises";
|
|
917
|
+
import path7 from "path";
|
|
918
|
+
import { execFile as execFile5 } from "child_process";
|
|
919
|
+
import { promisify as promisify5 } from "util";
|
|
920
|
+
function parseChanges(output) {
|
|
921
|
+
const fields = output.split("\0");
|
|
922
|
+
const changes = [];
|
|
923
|
+
for (let i = 0; i < fields.length && fields[i]; ) {
|
|
924
|
+
const code = fields[i++];
|
|
925
|
+
const first = fields[i++];
|
|
926
|
+
if (!first) break;
|
|
927
|
+
const second = /^[RC]/.test(code) ? fields[i++] : void 0;
|
|
928
|
+
const change = second ? code.startsWith("R") ? { status: "renamed", path: second, previousPath: first } : { status: "added", path: second } : { status: code === "A" ? "added" : code === "D" ? "deleted" : "modified", path: first };
|
|
929
|
+
if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/"))) continue;
|
|
930
|
+
changes.push(change);
|
|
931
|
+
}
|
|
932
|
+
return changes;
|
|
933
|
+
}
|
|
934
|
+
function touchedPaths(changes) {
|
|
935
|
+
return [...new Set(changes.flatMap((c) => c.previousPath ? [c.previousPath, c.path] : [c.path]))].sort();
|
|
936
|
+
}
|
|
937
|
+
async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
|
|
938
|
+
if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-")) return null;
|
|
939
|
+
try {
|
|
940
|
+
const { stdout } = await exec5("git", ["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 });
|
|
941
|
+
return parseChanges(stdout);
|
|
942
|
+
} catch {
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
async function getWorkingTree(resolvedRoot) {
|
|
947
|
+
try {
|
|
948
|
+
const [diff, untracked] = await Promise.all([
|
|
949
|
+
exec5("git", ["diff", "--name-status", "-z", "-M", "HEAD", "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }),
|
|
950
|
+
exec5("git", ["ls-files", "-z", "--others", "--exclude-standard"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 })
|
|
951
|
+
]);
|
|
952
|
+
const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
|
|
953
|
+
return { available: true, changedFiles: [.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])].sort(), untrackedFiles };
|
|
954
|
+
} catch {
|
|
955
|
+
return { available: false, changedFiles: [], untrackedFiles: [] };
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
var exec5;
|
|
959
|
+
var init_drift = __esm({
|
|
960
|
+
"src/drift/drift.ts"() {
|
|
961
|
+
"use strict";
|
|
962
|
+
init_snapshot();
|
|
963
|
+
init_paths();
|
|
985
964
|
exec5 = promisify5(execFile5);
|
|
986
|
-
DOC_CANDIDATES = [
|
|
987
|
-
"AGENTS.md",
|
|
988
|
-
"CLAUDE.md",
|
|
989
|
-
".claude/CLAUDE.md"
|
|
990
|
-
];
|
|
991
965
|
}
|
|
992
966
|
});
|
|
993
967
|
|
|
@@ -1008,11 +982,11 @@ var init_types = __esm({
|
|
|
1008
982
|
});
|
|
1009
983
|
|
|
1010
984
|
// src/audit/checks/deleted-reference.ts
|
|
1011
|
-
import
|
|
985
|
+
import fs6 from "fs/promises";
|
|
1012
986
|
import path8 from "path";
|
|
1013
987
|
async function exists(absPath) {
|
|
1014
988
|
try {
|
|
1015
|
-
await
|
|
989
|
+
await fs6.access(absPath);
|
|
1016
990
|
return true;
|
|
1017
991
|
} catch {
|
|
1018
992
|
return false;
|
|
@@ -1106,14 +1080,14 @@ var init_deleted_reference = __esm({
|
|
|
1106
1080
|
});
|
|
1107
1081
|
|
|
1108
1082
|
// src/audit/inputs.ts
|
|
1109
|
-
import
|
|
1083
|
+
import fs7 from "fs/promises";
|
|
1110
1084
|
import * as nativeFs from "fs";
|
|
1111
1085
|
import path9 from "path";
|
|
1112
1086
|
import { execFile as execFile6 } from "child_process";
|
|
1113
1087
|
import { promisify as promisify6 } from "util";
|
|
1114
1088
|
import fg2 from "fast-glob";
|
|
1115
1089
|
async function auditInputPath(root, file) {
|
|
1116
|
-
if (file === ".") return
|
|
1090
|
+
if (file === ".") return fs7.realpath(root);
|
|
1117
1091
|
try {
|
|
1118
1092
|
return await storePath(root, file);
|
|
1119
1093
|
} catch (error) {
|
|
@@ -1230,7 +1204,7 @@ var init_inputs = __esm({
|
|
|
1230
1204
|
|
|
1231
1205
|
// src/audit/checks/new-module.ts
|
|
1232
1206
|
import fg3 from "fast-glob";
|
|
1233
|
-
import
|
|
1207
|
+
import fs8 from "fs/promises";
|
|
1234
1208
|
import path10 from "path";
|
|
1235
1209
|
function escapeRegExp(text2) {
|
|
1236
1210
|
return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -1273,7 +1247,7 @@ async function moduleCandidates(root, combinedDocs) {
|
|
|
1273
1247
|
const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
|
|
1274
1248
|
const sources = sourcePaths.filter((file) => !file.split("/").some((part) => part.startsWith(".")));
|
|
1275
1249
|
const listSubdirs = async (dir) => {
|
|
1276
|
-
const entries = await
|
|
1250
|
+
const entries = await fs8.readdir(await auditInputPath(root, dir), { withFileTypes: true });
|
|
1277
1251
|
const dirs = entries.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink()) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)).map((entry) => path10.posix.join(dir, entry.name));
|
|
1278
1252
|
const ignored = await gitIgnoredPaths(root, dirs);
|
|
1279
1253
|
const visible = new Set(dirs.filter((dir2) => !ignored.has(dir2) || sources.some((file) => file.startsWith(dir2 + "/"))));
|
|
@@ -1900,15 +1874,16 @@ var init_provenance = __esm({
|
|
|
1900
1874
|
});
|
|
1901
1875
|
|
|
1902
1876
|
// src/decisions/decisions.ts
|
|
1903
|
-
import
|
|
1877
|
+
import fs9 from "fs/promises";
|
|
1904
1878
|
import path13 from "path";
|
|
1905
1879
|
import { createHash } from "crypto";
|
|
1880
|
+
import { z as z4 } from "zod";
|
|
1906
1881
|
async function loadDecisionStore(rootDir) {
|
|
1907
1882
|
const records = [];
|
|
1908
1883
|
const diagnostics = [];
|
|
1909
1884
|
let entries;
|
|
1910
1885
|
try {
|
|
1911
|
-
entries = await
|
|
1886
|
+
entries = await fs9.readdir(await storePath(rootDir, ".mason/decisions"));
|
|
1912
1887
|
} catch (error) {
|
|
1913
1888
|
if (error.code !== "ENOENT") diagnostics.push({ path: ".mason/decisions", message: String(error) });
|
|
1914
1889
|
return { records, diagnostics };
|
|
@@ -1926,6 +1901,7 @@ async function loadDecisionStore(rootDir) {
|
|
|
1926
1901
|
}
|
|
1927
1902
|
return { records, diagnostics };
|
|
1928
1903
|
}
|
|
1904
|
+
var TITLE_MAX_CHARS, BODY_RECOMMENDED_CHARS, BODY_MAX_CHARS, boundedText, decisionTitleSchema, decisionBodySchema;
|
|
1929
1905
|
var init_decisions = __esm({
|
|
1930
1906
|
"src/decisions/decisions.ts"() {
|
|
1931
1907
|
"use strict";
|
|
@@ -1934,6 +1910,18 @@ var init_decisions = __esm({
|
|
|
1934
1910
|
init_snapshot();
|
|
1935
1911
|
init_lexical();
|
|
1936
1912
|
init_provenance();
|
|
1913
|
+
TITLE_MAX_CHARS = 80;
|
|
1914
|
+
BODY_RECOMMENDED_CHARS = 1500;
|
|
1915
|
+
BODY_MAX_CHARS = 2500;
|
|
1916
|
+
boundedText = (field, max, hint) => z4.string({
|
|
1917
|
+
errorMap: (issue, ctx) => ({ message: issue.code === "too_big" ? `${field} has ${ctx.data.length} characters; maximum is ${max} (${ctx.data.length - max} over). ${hint}` : ctx.defaultError })
|
|
1918
|
+
}).trim().min(1, `${field} must be non-empty`).max(max);
|
|
1919
|
+
decisionTitleSchema = boundedText("title", TITLE_MAX_CHARS, "Tighten it to a specific headline.");
|
|
1920
|
+
decisionBodySchema = boundedText(
|
|
1921
|
+
"body",
|
|
1922
|
+
BODY_MAX_CHARS,
|
|
1923
|
+
`Aim for ${BODY_RECOMMENDED_CHARS} characters or fewer. Preserve the rule, reason, and exceptions; put supporting references in sources.`
|
|
1924
|
+
);
|
|
1937
1925
|
}
|
|
1938
1926
|
});
|
|
1939
1927
|
|
|
@@ -2057,7 +2045,7 @@ var init_checks = __esm({
|
|
|
2057
2045
|
});
|
|
2058
2046
|
|
|
2059
2047
|
// src/audit/audit.ts
|
|
2060
|
-
import
|
|
2048
|
+
import fs10 from "fs/promises";
|
|
2061
2049
|
import path15 from "path";
|
|
2062
2050
|
async function computeAudit(rootDir, options = {}) {
|
|
2063
2051
|
const resolvedRoot = path15.resolve(rootDir);
|
|
@@ -2092,7 +2080,7 @@ async function computeAudit(rootDir, options = {}) {
|
|
|
2092
2080
|
}
|
|
2093
2081
|
let decisionsPresent = false;
|
|
2094
2082
|
try {
|
|
2095
|
-
await
|
|
2083
|
+
await fs10.access(path15.join(resolvedRoot, ".mason", "decisions"));
|
|
2096
2084
|
decisionsPresent = true;
|
|
2097
2085
|
} catch {
|
|
2098
2086
|
}
|
|
@@ -2129,10 +2117,10 @@ var init_audit = __esm({
|
|
|
2129
2117
|
});
|
|
2130
2118
|
|
|
2131
2119
|
// src/audit/repair.ts
|
|
2132
|
-
import
|
|
2120
|
+
import fs11 from "fs/promises";
|
|
2133
2121
|
import path16 from "path";
|
|
2134
|
-
import { createHash as createHash2, randomUUID as
|
|
2135
|
-
import { z as
|
|
2122
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
2123
|
+
import { z as z5 } from "zod";
|
|
2136
2124
|
function findingId(finding) {
|
|
2137
2125
|
const e = finding.evidence;
|
|
2138
2126
|
let key;
|
|
@@ -2185,8 +2173,8 @@ async function stableAudit(root, checks, options = {}) {
|
|
|
2185
2173
|
return report;
|
|
2186
2174
|
}
|
|
2187
2175
|
async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
|
|
2188
|
-
const root = await
|
|
2189
|
-
const selected =
|
|
2176
|
+
const root = await fs11.realpath(rootDir);
|
|
2177
|
+
const selected = z5.array(checkSchema).nonempty().parse(checks);
|
|
2190
2178
|
const report = await stableAudit(root, selected, options);
|
|
2191
2179
|
if (!report) throw new Error("No context files found to prepare a repair.");
|
|
2192
2180
|
if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
|
|
@@ -2197,12 +2185,12 @@ async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
|
|
|
2197
2185
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2198
2186
|
report: storedReport
|
|
2199
2187
|
};
|
|
2200
|
-
const baselinePath = ".mason/reports/repairs/" +
|
|
2188
|
+
const baselinePath = ".mason/reports/repairs/" + randomUUID2() + ".json";
|
|
2201
2189
|
await writeStoreJson(root, baselinePath, { ...payload, digest: digest(payload) });
|
|
2202
2190
|
return { version: 1, action: "prepare", baselinePath, report };
|
|
2203
2191
|
}
|
|
2204
2192
|
async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
2205
|
-
const root = await
|
|
2193
|
+
const root = await fs11.realpath(rootDir);
|
|
2206
2194
|
const declaredRoot = path16.resolve(rootDir);
|
|
2207
2195
|
const relative = path16.isAbsolute(baselinePath) ? path16.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
|
|
2208
2196
|
const stored = baselineSchema.parse(await readStoreJson(root, relative));
|
|
@@ -2277,7 +2265,7 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
|
2277
2265
|
scope: "Original audit checks over current context files and repository evidence. Resolved means no longer detected by that check. Advisories require separate review; this is not a certification of documentation or application correctness."
|
|
2278
2266
|
};
|
|
2279
2267
|
}
|
|
2280
|
-
var checkSchema, commitSchema, anchorSchema, count, evidenceSchema, findingSchema, issueSchema, advisorySchema, checkResultSchema, reportSchema, baselineSchema, digest;
|
|
2268
|
+
var checkSchema, commitSchema, anchorSchema, count, evidenceSchema, findingSchema, issueSchema, advisorySchema, checkResultSchema, reportSchema, baselineSchema, digest, repairFindingSchema;
|
|
2281
2269
|
var init_repair = __esm({
|
|
2282
2270
|
"src/audit/repair.ts"() {
|
|
2283
2271
|
"use strict";
|
|
@@ -2289,95 +2277,102 @@ var init_repair = __esm({
|
|
|
2289
2277
|
init_files();
|
|
2290
2278
|
init_paths();
|
|
2291
2279
|
init_types();
|
|
2292
|
-
checkSchema =
|
|
2293
|
-
commitSchema =
|
|
2294
|
-
anchorSchema =
|
|
2295
|
-
count =
|
|
2296
|
-
evidenceSchema =
|
|
2297
|
-
|
|
2298
|
-
kind:
|
|
2299
|
-
claimed:
|
|
2300
|
-
renamedTo:
|
|
2280
|
+
checkSchema = z5.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
|
|
2281
|
+
commitSchema = z5.object({ hash: z5.string().regex(/^[a-f0-9]{40,64}$/), date: z5.string(), subject: z5.string() });
|
|
2282
|
+
anchorSchema = z5.object({ doc: z5.string(), line: z5.number().int().positive().nullable(), excerpt: z5.string().nullable() });
|
|
2283
|
+
count = z5.number().int().nonnegative();
|
|
2284
|
+
evidenceSchema = z5.discriminatedUnion("kind", [
|
|
2285
|
+
z5.object({
|
|
2286
|
+
kind: z5.literal("missing-path"),
|
|
2287
|
+
claimed: z5.string(),
|
|
2288
|
+
renamedTo: z5.string().nullable(),
|
|
2301
2289
|
deletedInCommit: commitSchema.nullable(),
|
|
2302
|
-
everTracked:
|
|
2303
|
-
parentDirExists:
|
|
2290
|
+
everTracked: z5.boolean(),
|
|
2291
|
+
parentDirExists: z5.boolean()
|
|
2304
2292
|
}),
|
|
2305
|
-
|
|
2306
|
-
kind:
|
|
2307
|
-
dir:
|
|
2293
|
+
z5.object({
|
|
2294
|
+
kind: z5.literal("unmentioned-dir"),
|
|
2295
|
+
dir: z5.string(),
|
|
2308
2296
|
sourceFileCount: count,
|
|
2309
2297
|
firstCommit: commitSchema.nullable(),
|
|
2310
|
-
checkedDocs:
|
|
2298
|
+
checkedDocs: z5.array(z5.string())
|
|
2311
2299
|
}),
|
|
2312
|
-
|
|
2313
|
-
kind:
|
|
2300
|
+
z5.object({
|
|
2301
|
+
kind: z5.literal("count-mismatch"),
|
|
2314
2302
|
claimed: count,
|
|
2315
2303
|
actual: count,
|
|
2316
|
-
unit:
|
|
2317
|
-
countedFrom:
|
|
2318
|
-
members:
|
|
2304
|
+
unit: z5.string(),
|
|
2305
|
+
countedFrom: z5.string(),
|
|
2306
|
+
members: z5.array(z5.string())
|
|
2319
2307
|
}),
|
|
2320
|
-
|
|
2321
|
-
kind:
|
|
2322
|
-
scriptName:
|
|
2323
|
-
invocation:
|
|
2324
|
-
manifestsChecked:
|
|
2325
|
-
availableScripts:
|
|
2308
|
+
z5.object({
|
|
2309
|
+
kind: z5.literal("missing-script"),
|
|
2310
|
+
scriptName: z5.string(),
|
|
2311
|
+
invocation: z5.string(),
|
|
2312
|
+
manifestsChecked: z5.array(z5.string()),
|
|
2313
|
+
availableScripts: z5.array(z5.string())
|
|
2326
2314
|
}),
|
|
2327
|
-
|
|
2328
|
-
kind:
|
|
2315
|
+
z5.object({
|
|
2316
|
+
kind: z5.literal("doc-behind-manifests"),
|
|
2329
2317
|
docLastCommit: commitSchema,
|
|
2330
|
-
manifestCommits:
|
|
2318
|
+
manifestCommits: z5.array(commitSchema.extend({ files: z5.array(z5.string()) })),
|
|
2331
2319
|
totalCommits: count
|
|
2332
2320
|
}),
|
|
2333
|
-
|
|
2334
|
-
kind:
|
|
2335
|
-
decisionId:
|
|
2336
|
-
title:
|
|
2337
|
-
changedFiles:
|
|
2338
|
-
refreshedHash:
|
|
2339
|
-
provenance:
|
|
2321
|
+
z5.object({
|
|
2322
|
+
kind: z5.literal("decision-anchor"),
|
|
2323
|
+
decisionId: z5.string(),
|
|
2324
|
+
title: z5.string(),
|
|
2325
|
+
changedFiles: z5.array(z5.string()),
|
|
2326
|
+
refreshedHash: z5.string(),
|
|
2327
|
+
provenance: z5.object({}).passthrough().optional()
|
|
2340
2328
|
})
|
|
2341
2329
|
]);
|
|
2342
|
-
findingSchema =
|
|
2330
|
+
findingSchema = z5.object({ message: z5.string(), anchor: anchorSchema, evidence: evidenceSchema });
|
|
2343
2331
|
issueSchema = findingSchema.extend({
|
|
2344
|
-
type:
|
|
2345
|
-
confidence:
|
|
2332
|
+
type: z5.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
|
|
2333
|
+
confidence: z5.enum(["certain", "likely"])
|
|
2346
2334
|
});
|
|
2347
|
-
advisorySchema = findingSchema.extend({ type:
|
|
2348
|
-
checkResultSchema =
|
|
2349
|
-
issues:
|
|
2350
|
-
advisories:
|
|
2351
|
-
suppressedAdvisories:
|
|
2352
|
-
skipped:
|
|
2335
|
+
advisorySchema = findingSchema.extend({ type: z5.enum(["deps-changed", "decision-anchor-drift"]) });
|
|
2336
|
+
checkResultSchema = z5.object({
|
|
2337
|
+
issues: z5.array(issueSchema),
|
|
2338
|
+
advisories: z5.array(advisorySchema),
|
|
2339
|
+
suppressedAdvisories: z5.array(advisorySchema).optional(),
|
|
2340
|
+
skipped: z5.array(z5.object({ check: z5.string(), reason: z5.string(), doc: z5.string().optional() }))
|
|
2353
2341
|
});
|
|
2354
|
-
reportSchema =
|
|
2355
|
-
version:
|
|
2356
|
-
root:
|
|
2357
|
-
gitAvailable:
|
|
2342
|
+
reportSchema = z5.object({
|
|
2343
|
+
version: z5.literal(1),
|
|
2344
|
+
root: z5.string(),
|
|
2345
|
+
gitAvailable: z5.literal(true),
|
|
2358
2346
|
headHash: commitSchema.shape.hash,
|
|
2359
|
-
checksRun:
|
|
2360
|
-
docs:
|
|
2361
|
-
path:
|
|
2347
|
+
checksRun: z5.array(checkSchema).nonempty(),
|
|
2348
|
+
docs: z5.array(z5.object({
|
|
2349
|
+
path: z5.enum(DOC_CANDIDATES),
|
|
2362
2350
|
lastCommit: commitSchema.nullable(),
|
|
2363
|
-
dirty:
|
|
2351
|
+
dirty: z5.boolean(),
|
|
2364
2352
|
lineCount: count
|
|
2365
2353
|
})).nonempty(),
|
|
2366
|
-
decisionsChecked:
|
|
2367
|
-
clean:
|
|
2368
|
-
issues:
|
|
2369
|
-
advisories:
|
|
2370
|
-
suppressedAdvisories:
|
|
2371
|
-
skippedChecks:
|
|
2354
|
+
decisionsChecked: z5.boolean(),
|
|
2355
|
+
clean: z5.boolean(),
|
|
2356
|
+
issues: z5.array(issueSchema),
|
|
2357
|
+
advisories: z5.array(advisorySchema),
|
|
2358
|
+
suppressedAdvisories: z5.array(advisorySchema).optional(),
|
|
2359
|
+
skippedChecks: z5.array(z5.object({ check: z5.string(), reason: z5.string(), doc: z5.string().optional() }))
|
|
2372
2360
|
});
|
|
2373
|
-
baselineSchema =
|
|
2374
|
-
kind:
|
|
2375
|
-
version:
|
|
2376
|
-
createdAt:
|
|
2361
|
+
baselineSchema = z5.object({
|
|
2362
|
+
kind: z5.literal("mason-audit-repair"),
|
|
2363
|
+
version: z5.literal(1),
|
|
2364
|
+
createdAt: z5.string().datetime(),
|
|
2377
2365
|
report: reportSchema,
|
|
2378
|
-
digest:
|
|
2366
|
+
digest: z5.string().regex(/^[a-f0-9]{64}$/)
|
|
2379
2367
|
});
|
|
2380
2368
|
digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
|
|
2369
|
+
repairFindingSchema = z5.object({
|
|
2370
|
+
id: z5.string(),
|
|
2371
|
+
original: z5.union([issueSchema, advisorySchema]),
|
|
2372
|
+
status: z5.enum(["resolved", "unresolved", "review-required", "unverified"]),
|
|
2373
|
+
reason: z5.string(),
|
|
2374
|
+
current: z5.union([issueSchema, advisorySchema]).optional()
|
|
2375
|
+
});
|
|
2381
2376
|
}
|
|
2382
2377
|
});
|
|
2383
2378
|
|
|
@@ -2390,19 +2385,19 @@ __export(evidence_exports, {
|
|
|
2390
2385
|
readInputs: () => readInputs,
|
|
2391
2386
|
workspace: () => workspace
|
|
2392
2387
|
});
|
|
2393
|
-
import
|
|
2388
|
+
import fs12 from "fs/promises";
|
|
2394
2389
|
import path17 from "path";
|
|
2395
2390
|
import { createHash as createHash3 } from "crypto";
|
|
2396
2391
|
import { execFile as execFile8 } from "child_process";
|
|
2397
2392
|
import { promisify as promisify8 } from "util";
|
|
2398
2393
|
import fg4 from "fast-glob";
|
|
2399
|
-
import { z as
|
|
2394
|
+
import { z as z6 } from "zod";
|
|
2400
2395
|
async function git2(root, ...args) {
|
|
2401
2396
|
return (await exec7("git", args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout;
|
|
2402
2397
|
}
|
|
2403
2398
|
async function workspace(dir) {
|
|
2404
|
-
const root = await
|
|
2405
|
-
const gitDir = await
|
|
2399
|
+
const root = await fs12.realpath((await git2(dir, "rev-parse", "--show-toplevel")).trim());
|
|
2400
|
+
const gitDir = await fs12.realpath((await git2(root, "rev-parse", "--absolute-git-dir")).trim());
|
|
2406
2401
|
let branch;
|
|
2407
2402
|
try {
|
|
2408
2403
|
branch = (await git2(root, "symbolic-ref", "--quiet", "HEAD")).trim();
|
|
@@ -2421,7 +2416,7 @@ async function readInputs(root) {
|
|
|
2421
2416
|
const head = headText.trim();
|
|
2422
2417
|
let shallow = null;
|
|
2423
2418
|
try {
|
|
2424
|
-
shallow = await
|
|
2419
|
+
shallow = await fs12.readFile(path17.resolve(root, shallowPath.trim()), "utf8");
|
|
2425
2420
|
} catch (error) {
|
|
2426
2421
|
if (error.code !== "ENOENT") throw error;
|
|
2427
2422
|
}
|
|
@@ -2434,7 +2429,7 @@ async function readInputs(root) {
|
|
|
2434
2429
|
docContents.push([file, text2]);
|
|
2435
2430
|
for (const claim of text2 ? extractClaims(text2).paths : []) {
|
|
2436
2431
|
if (internal(claim.path) || claim.path.startsWith(".mason/")) continue;
|
|
2437
|
-
const exists2 = async (p) =>
|
|
2432
|
+
const exists2 = async (p) => fs12.access(path17.resolve(root, p)).then(() => true, () => false);
|
|
2438
2433
|
claims.push([claim.path, await exists2(claim.path), await exists2(path17.dirname(claim.path))]);
|
|
2439
2434
|
}
|
|
2440
2435
|
}
|
|
@@ -2452,7 +2447,7 @@ async function readInputs(root) {
|
|
|
2452
2447
|
}
|
|
2453
2448
|
const needsWorkspaceScripts = rootScripts !== null && commandClaims.some((claim) => !rootScripts.includes(claim.scriptName));
|
|
2454
2449
|
const decisionDirectory = await storePath(root, ".mason/decisions");
|
|
2455
|
-
const decisionPresence = await
|
|
2450
|
+
const decisionPresence = await fs12.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
|
|
2456
2451
|
if (error.code === "ENOENT") return "absent";
|
|
2457
2452
|
throw error;
|
|
2458
2453
|
});
|
|
@@ -2521,113 +2516,219 @@ var init_evidence = __esm({
|
|
|
2521
2516
|
init_dead_command();
|
|
2522
2517
|
init_storage();
|
|
2523
2518
|
exec7 = promisify8(execFile8);
|
|
2524
|
-
engineVersion = true ? "0.16.
|
|
2519
|
+
engineVersion = true ? "0.16.3" : "development";
|
|
2525
2520
|
hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
|
|
2526
2521
|
content = readAuditInput;
|
|
2527
2522
|
internal = (file) => file === ".mason" || file === ".mason/reports" || file.startsWith(".mason/reports/");
|
|
2528
|
-
cacheSchema =
|
|
2523
|
+
cacheSchema = z6.object({ version: z6.literal(1), entries: z6.record(z6.object({ key: z6.string(), result: checkResultSchema })), digest: z6.string() });
|
|
2529
2524
|
}
|
|
2530
2525
|
});
|
|
2531
2526
|
|
|
2532
|
-
// src/automation/
|
|
2533
|
-
import fs12 from "fs/promises";
|
|
2527
|
+
// src/automation/execution.ts
|
|
2534
2528
|
import os2 from "os";
|
|
2535
|
-
import
|
|
2536
|
-
|
|
2529
|
+
import fs13 from "fs/promises";
|
|
2530
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2531
|
+
import { z as z7 } from "zod";
|
|
2532
|
+
function parseExecution(raw) {
|
|
2537
2533
|
try {
|
|
2538
|
-
return
|
|
2534
|
+
return executionSchema.parse(raw);
|
|
2539
2535
|
} catch (error) {
|
|
2540
|
-
throw new Error("Invalid automation
|
|
2536
|
+
throw new Error("Invalid automation execution store; receipt history was retained.", { cause: error });
|
|
2541
2537
|
}
|
|
2542
2538
|
}
|
|
2543
|
-
|
|
2544
|
-
const
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2539
|
+
function automationFailure(error) {
|
|
2540
|
+
const recorded = failureSchema.safeParse(error?.failure);
|
|
2541
|
+
if (recorded.success) return recorded.data;
|
|
2542
|
+
const message2 = (error instanceof Error ? error.message : String(error)).replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 700);
|
|
2543
|
+
const codes = /* @__PURE__ */ new Set();
|
|
2544
|
+
let cause = error;
|
|
2545
|
+
for (let i = 0; cause && i < 8; i++) {
|
|
2546
|
+
codes.add(String(cause.code));
|
|
2547
|
+
cause = cause.cause;
|
|
2548
|
+
}
|
|
2549
|
+
const code = codes.has("ENOSPC") || codes.has("EDQUOT") ? "storage-full" : /changed during|changed while|changed between/.test(message2) ? "inputs-changed" : /Automation is busy/.test(message2) ? "busy" : /not a git repository|unknown revision|bad revision|different history|unreachable/.test(message2) ? "history-unavailable" : error instanceof z7.ZodError || error instanceof SyntaxError || /Hook input|Expected one command|Unknown automation command|--host/.test(message2) ? "invalid-input" : /store|baseline|modified|symbolic link|Symlink|automation state|state belongs/.test(message2) ? "invalid-evidence" : [...codes].some((c) => /^E[A-Z]+$/.test(c)) ? "io-error" : "internal";
|
|
2550
|
+
return { code, message: message2, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
|
|
2551
|
+
}
|
|
2552
|
+
function failureMessage(error) {
|
|
2553
|
+
const failure = automationFailure(error);
|
|
2554
|
+
return `Mason automation unavailable [${failure.code}]; evidence capture/verification was not established. ${failure.message}` + (failure.receiptRecorded ? "" : " No durable failure receipt was recorded.");
|
|
2555
|
+
}
|
|
2556
|
+
async function failureNotifications(root, directory) {
|
|
2557
|
+
const raw = await readStoreJson(root, directory + "/notifications.json");
|
|
2558
|
+
return raw === null ? { version: 1, sessions: {} } : notificationsSchema.parse(raw);
|
|
2559
|
+
}
|
|
2560
|
+
async function hookFailureMessage(dir, host, sessionId, error) {
|
|
2561
|
+
const failure = automationFailure(error);
|
|
2562
|
+
if (!failure.receiptRecorded) return failureMessage(error);
|
|
2563
|
+
try {
|
|
2564
|
+
const ws = await workspace(dir);
|
|
2565
|
+
return await withLock(ws.root, ws.directory + "/notifications", async () => {
|
|
2566
|
+
const notices = await failureNotifications(ws.root, ws.directory);
|
|
2567
|
+
const key = hash([host, sessionId]), signature = hash([failure.code, failure.message]);
|
|
2568
|
+
const previous = notices.sessions[key];
|
|
2569
|
+
const execution = await executionStatus(ws.root, ws.directory);
|
|
2570
|
+
const recovered = previous && execution.attempts.some((a) => a.status === "completed" && a.startedAt > previous.lastAt);
|
|
2571
|
+
const repeat = previous?.signature === signature && !recovered;
|
|
2572
|
+
notices.sessions[key] = {
|
|
2573
|
+
signature,
|
|
2574
|
+
code: failure.code,
|
|
2575
|
+
message: failure.message,
|
|
2576
|
+
count: repeat ? previous.count + 1 : 1,
|
|
2577
|
+
lastAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2578
|
+
};
|
|
2579
|
+
notices.sessions = Object.fromEntries(Object.entries(notices.sessions).sort(([, a], [, b]) => b.lastAt.localeCompare(a.lastAt)).slice(0, 32));
|
|
2580
|
+
await writeStoreJson(ws.root, ws.directory + "/notifications.json", notices);
|
|
2581
|
+
return repeat ? null : failureMessage(error) + " Repeated failures in this session are recorded in mason status --json.";
|
|
2582
|
+
});
|
|
2583
|
+
} catch {
|
|
2584
|
+
return failureMessage(error);
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
async function refreshRunning(root, directory, attempts) {
|
|
2588
|
+
for (const attempt of attempts) {
|
|
2589
|
+
if (attempt.status !== "running") continue;
|
|
2590
|
+
let alive = false;
|
|
2548
2591
|
try {
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
})
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
});
|
|
2558
|
-
throw error;
|
|
2592
|
+
const owner = await readStoreJson(root, attempt.lease ? leasePath(directory, attempt.id) : directory + "/lock");
|
|
2593
|
+
if (attempt.host === os2.hostname() && owner?.pid === attempt.pid && owner.host === attempt.host) {
|
|
2594
|
+
try {
|
|
2595
|
+
process.kill(attempt.pid, 0);
|
|
2596
|
+
alive = true;
|
|
2597
|
+
} catch (error) {
|
|
2598
|
+
alive = error.code === "EPERM";
|
|
2599
|
+
}
|
|
2559
2600
|
}
|
|
2601
|
+
} catch {
|
|
2602
|
+
}
|
|
2603
|
+
if (!alive) attempt.status = "unknown";
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
async function editExecution(root, directory, edit) {
|
|
2607
|
+
return withLock(root, directory + "/execution-lock", async () => {
|
|
2608
|
+
const file = directory + "/execution.json";
|
|
2609
|
+
const raw = await readStoreJson(root, file);
|
|
2610
|
+
const log = raw === null ? { version: 1, attempts: [], discardedAttempts: 0 } : parseExecution(raw);
|
|
2611
|
+
await refreshRunning(root, directory, log.attempts);
|
|
2612
|
+
edit(log);
|
|
2613
|
+
const finished = log.attempts.filter((a) => a.status !== "running");
|
|
2614
|
+
const discard = new Set(finished.slice(0, Math.max(0, finished.length - 32)).map((a) => a.id));
|
|
2615
|
+
log.discardedAttempts += discard.size;
|
|
2616
|
+
log.attempts = log.attempts.filter((a) => !discard.has(a.id));
|
|
2617
|
+
await writeStoreJson(root, file, log);
|
|
2618
|
+
});
|
|
2619
|
+
}
|
|
2620
|
+
async function recordExecution(root, directory, event, run) {
|
|
2621
|
+
const started = performance.now();
|
|
2622
|
+
const attempt = {
|
|
2623
|
+
id: randomUUID3(),
|
|
2624
|
+
event,
|
|
2625
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2626
|
+
pid: process.pid,
|
|
2627
|
+
host: os2.hostname(),
|
|
2628
|
+
status: "running",
|
|
2629
|
+
lease: true
|
|
2630
|
+
};
|
|
2631
|
+
const owner = leasePath(directory, attempt.id);
|
|
2632
|
+
const finish = async (changes) => editExecution(root, directory, (log) => {
|
|
2633
|
+
const current = log.attempts.find((a) => a.id === attempt.id);
|
|
2634
|
+
if (!current) throw new Error("Automation execution receipt disappeared; completion could not be recorded.");
|
|
2635
|
+
Object.assign(current, changes, { finishedAt: (/* @__PURE__ */ new Date()).toISOString(), durationMs: performance.now() - started });
|
|
2636
|
+
});
|
|
2637
|
+
await writeStoreJson(root, owner, { pid: attempt.pid, host: attempt.host });
|
|
2638
|
+
try {
|
|
2639
|
+
await editExecution(root, directory, (log) => {
|
|
2640
|
+
if (log.attempts.filter((a) => a.status === "running").length >= 128) throw new Error("Too many active automation executions; existing receipts were retained.");
|
|
2641
|
+
log.attempts.push(attempt);
|
|
2642
|
+
});
|
|
2643
|
+
try {
|
|
2644
|
+
const result = await run();
|
|
2645
|
+
await finish({ status: "completed", verificationStatus: result.report.status, reportPath: result.report.reportPath });
|
|
2646
|
+
return result;
|
|
2560
2647
|
} catch (error) {
|
|
2561
|
-
|
|
2648
|
+
const failure = automationFailure(error);
|
|
2562
2649
|
try {
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
try {
|
|
2566
|
-
process.kill(owner.pid, 0);
|
|
2567
|
-
} catch (probe) {
|
|
2568
|
-
if (probe.code === "ESRCH") {
|
|
2569
|
-
const reclaim = file + ".reclaim";
|
|
2570
|
-
let guard;
|
|
2571
|
-
try {
|
|
2572
|
-
guard = await fs12.open(reclaim, "wx", 384);
|
|
2573
|
-
const current = JSON.parse(await fs12.readFile(file, "utf8"));
|
|
2574
|
-
if (current.pid === owner.pid && current.host === owner.host) await fs12.unlink(file);
|
|
2575
|
-
} finally {
|
|
2576
|
-
if (guard) {
|
|
2577
|
-
await guard.close();
|
|
2578
|
-
await fs12.rm(reclaim, { force: true });
|
|
2579
|
-
}
|
|
2580
|
-
}
|
|
2581
|
-
}
|
|
2582
|
-
}
|
|
2583
|
-
}
|
|
2650
|
+
await finish({ status: "failed", failure: { ...failure, receiptRecorded: true } });
|
|
2651
|
+
failure.receiptRecorded = true;
|
|
2584
2652
|
} catch {
|
|
2585
2653
|
}
|
|
2586
|
-
|
|
2587
|
-
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
2654
|
+
throw Object.assign(new Error(failure.message, { cause: error }), { failure });
|
|
2588
2655
|
}
|
|
2589
|
-
}
|
|
2590
|
-
try {
|
|
2591
|
-
return await run();
|
|
2592
2656
|
} finally {
|
|
2593
|
-
await
|
|
2594
|
-
|
|
2657
|
+
await fs13.rm(await storePath(root, owner), { force: true }).catch(() => {
|
|
2658
|
+
});
|
|
2595
2659
|
}
|
|
2596
2660
|
}
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2661
|
+
async function executionStatus(root, directory) {
|
|
2662
|
+
const raw = await readStoreJson(root, directory + "/execution.json");
|
|
2663
|
+
if (raw === null) return { status: "not-observed", attempts: [] };
|
|
2664
|
+
const log = parseExecution(raw);
|
|
2665
|
+
await refreshRunning(root, directory, log.attempts);
|
|
2666
|
+
const completed = [...log.attempts].reverse().find((a) => a.status === "completed");
|
|
2667
|
+
const outstanding = [...log.attempts].reverse().find((a) => ["failed", "unknown"].includes(a.status) && (!completed || (a.finishedAt ?? a.startedAt) >= completed.startedAt));
|
|
2668
|
+
const status = log.attempts.some((a) => a.status === "running") ? "running" : outstanding?.status ?? log.attempts.at(-1)?.status ?? "not-observed";
|
|
2669
|
+
return { status, attempts: log.attempts, discardedAttempts: log.discardedAttempts };
|
|
2670
|
+
}
|
|
2671
|
+
var failureSchema, attemptSchema, executionSchema, notificationsSchema, leasePath;
|
|
2672
|
+
var init_execution = __esm({
|
|
2673
|
+
"src/automation/execution.ts"() {
|
|
2600
2674
|
"use strict";
|
|
2675
|
+
init_store();
|
|
2676
|
+
init_evidence();
|
|
2601
2677
|
init_storage();
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
gitDir: z6.string(),
|
|
2608
|
-
branch: z6.string(),
|
|
2609
|
-
baselines: z6.array(z6.object({ path: z6.string(), at: z6.string(), event: z6.string(), fingerprint: z6.string() })).max(128),
|
|
2610
|
-
sessions: z6.record(z6.object({
|
|
2611
|
-
host: hostSchema,
|
|
2612
|
-
seen: z6.string().nullable(),
|
|
2613
|
-
continued: z6.boolean(),
|
|
2614
|
-
initialIssues: z6.array(z6.string()),
|
|
2615
|
-
initialDocs: z6.record(z6.string().nullable()),
|
|
2616
|
-
lastUsed: z6.string(),
|
|
2617
|
-
mutationObserved: z6.boolean(),
|
|
2618
|
-
pending: z6.record(z6.string()),
|
|
2619
|
-
coverageGaps: z6.array(z6.string()),
|
|
2620
|
-
events: z6.record(z6.object({ at: z6.string(), count: z6.number().int().positive() }))
|
|
2621
|
-
})),
|
|
2622
|
-
updatedAt: z6.string(),
|
|
2623
|
-
fingerprint: z6.string().nullable(),
|
|
2624
|
-
latest: z6.string().nullable()
|
|
2678
|
+
failureSchema = z7.object({
|
|
2679
|
+
code: z7.enum(["inputs-changed", "storage-full", "busy", "invalid-input", "history-unavailable", "invalid-evidence", "io-error", "internal"]),
|
|
2680
|
+
message: z7.string(),
|
|
2681
|
+
retryable: z7.boolean(),
|
|
2682
|
+
receiptRecorded: z7.boolean()
|
|
2625
2683
|
});
|
|
2684
|
+
attemptSchema = z7.object({
|
|
2685
|
+
id: z7.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
2686
|
+
lease: z7.literal(true).optional(),
|
|
2687
|
+
event: z7.string(),
|
|
2688
|
+
startedAt: z7.string(),
|
|
2689
|
+
finishedAt: z7.string().optional(),
|
|
2690
|
+
durationMs: z7.number().nonnegative().optional(),
|
|
2691
|
+
pid: z7.number().int().positive(),
|
|
2692
|
+
host: z7.string(),
|
|
2693
|
+
status: z7.enum(["running", "completed", "failed", "unknown"]),
|
|
2694
|
+
verificationStatus: z7.string().optional(),
|
|
2695
|
+
reportPath: z7.string().optional(),
|
|
2696
|
+
failure: failureSchema.optional()
|
|
2697
|
+
});
|
|
2698
|
+
executionSchema = z7.object({ version: z7.literal(1), attempts: z7.array(attemptSchema).max(160), discardedAttempts: z7.number().int().nonnegative().default(0) });
|
|
2699
|
+
notificationsSchema = z7.object({ version: z7.literal(1), sessions: z7.record(z7.object({
|
|
2700
|
+
signature: z7.string(),
|
|
2701
|
+
code: failureSchema.shape.code,
|
|
2702
|
+
message: z7.string(),
|
|
2703
|
+
count: z7.number().int().positive(),
|
|
2704
|
+
lastAt: z7.string()
|
|
2705
|
+
})) });
|
|
2706
|
+
leasePath = (directory, id) => `${directory}/executions/${id}.json`;
|
|
2626
2707
|
}
|
|
2627
2708
|
});
|
|
2628
2709
|
|
|
2629
2710
|
// src/automation/runtime.ts
|
|
2630
2711
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
2712
|
+
import { z as z8 } from "zod";
|
|
2713
|
+
async function observeReadOnlyTool(dir, event) {
|
|
2714
|
+
const ws = await workspace(dir);
|
|
2715
|
+
await withLock(ws.root, ws.directory, async () => {
|
|
2716
|
+
const statePath = ws.directory + "/state.json";
|
|
2717
|
+
const raw = await readStoreJson(ws.root, statePath);
|
|
2718
|
+
if (raw === null) return;
|
|
2719
|
+
const state = parseState(raw);
|
|
2720
|
+
if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
|
|
2721
|
+
throw new Error("Automation state belongs to another branch or worktree; original evidence was retained.");
|
|
2722
|
+
}
|
|
2723
|
+
const session = event.host && event.sessionId ? state.sessions[hash([event.host, event.sessionId])] : null;
|
|
2724
|
+
if (!session) return;
|
|
2725
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2726
|
+
session.lastUsed = now;
|
|
2727
|
+
session.events[event.event] = { at: now, count: (session.events[event.event]?.count ?? 0) + 1 };
|
|
2728
|
+
await writeStoreJson(ws.root, statePath, state);
|
|
2729
|
+
});
|
|
2730
|
+
return { root: ws.root, directory: ws.directory };
|
|
2731
|
+
}
|
|
2631
2732
|
function summarize(report) {
|
|
2632
2733
|
const open = report.findings.filter((f) => f.status !== "resolved");
|
|
2633
2734
|
return [
|
|
@@ -2639,176 +2740,234 @@ function summarize(report) {
|
|
|
2639
2740
|
"Keep original evidence. Address findings relevant to the authorized task; report unrelated findings and unresolved advisories without approving them."
|
|
2640
2741
|
].join("\n");
|
|
2641
2742
|
}
|
|
2743
|
+
async function loadState(ws) {
|
|
2744
|
+
const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
|
|
2745
|
+
const state = raw === null ? {
|
|
2746
|
+
version: 1,
|
|
2747
|
+
root: ws.root,
|
|
2748
|
+
gitDir: ws.gitDir,
|
|
2749
|
+
branch: ws.branch,
|
|
2750
|
+
baselines: [],
|
|
2751
|
+
sessions: {},
|
|
2752
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2753
|
+
fingerprint: null,
|
|
2754
|
+
latest: null
|
|
2755
|
+
} : parseState(raw);
|
|
2756
|
+
if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
|
|
2757
|
+
throw new Error("Automation state belongs to another branch or worktree; original evidence was retained.");
|
|
2758
|
+
}
|
|
2759
|
+
return state;
|
|
2760
|
+
}
|
|
2761
|
+
async function sharedAnalysis(ws, state, inputs, startedAt) {
|
|
2762
|
+
const pointer = state.analysis;
|
|
2763
|
+
if (!pointer || pointer.fingerprint !== inputs.fingerprint || pointer.completedAt < startedAt) return null;
|
|
2764
|
+
const stored = sharedSchema.parse(await readStoreJson(ws.root, pointer.path));
|
|
2765
|
+
const a = stored.analysis;
|
|
2766
|
+
if (stored.digest !== hash(a) || a.id !== pointer.id || a.fingerprint !== inputs.fingerprint || a.completedAt !== pointer.completedAt || a.report.root !== ws.root || a.report.branch !== ws.branch || a.report.head !== inputs.head || hash(a.report.baselinePaths) !== hash(state.baselines.map((b) => b.path))) {
|
|
2767
|
+
throw new Error("Invalid shared automation evidence; run a new check.");
|
|
2768
|
+
}
|
|
2769
|
+
if (a.baselineDigest !== await baselineDigest(ws, state)) throw new Error("Repair baseline was modified; use the original baseline.");
|
|
2770
|
+
return a;
|
|
2771
|
+
}
|
|
2772
|
+
async function verifyInputs(ws, inputs) {
|
|
2773
|
+
const [after, currentWs] = await Promise.all([readInputs(ws.root), workspace(ws.root)]);
|
|
2774
|
+
if (after.fingerprint !== inputs.fingerprint || currentWs.directory !== ws.directory) {
|
|
2775
|
+
throw new Error("Repository inputs or branch changed during automation; no current verification was recorded. Retry on a stable checkout.");
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
async function computeAnalysis(ws, state, inputs, event) {
|
|
2779
|
+
const baselines = [...state.baselines];
|
|
2780
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2781
|
+
const empty = {
|
|
2782
|
+
version: 1,
|
|
2783
|
+
status: "unavailable",
|
|
2784
|
+
root: ws.root,
|
|
2785
|
+
branch: ws.branch,
|
|
2786
|
+
head: inputs.head,
|
|
2787
|
+
baselinePaths: [],
|
|
2788
|
+
reportPath: "",
|
|
2789
|
+
findings: [],
|
|
2790
|
+
diagnostics: ["No AGENTS.md, CLAUDE.md, or .claude/CLAUDE.md exists. Documentation capture is unavailable; other Mason tools remain usable."],
|
|
2791
|
+
checks: { ran: [], reused: [], skipped: [] },
|
|
2792
|
+
counts: { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 },
|
|
2793
|
+
capture: "unknown",
|
|
2794
|
+
scope: SCOPE
|
|
2795
|
+
};
|
|
2796
|
+
if (!baselines.length && Object.values(inputs.docs).every((value) => value === null)) return { baselines, report: empty, cache: null };
|
|
2797
|
+
if (ws.branch === "detached" && state.latest) {
|
|
2798
|
+
const previous = await readStoreJson(ws.root, state.latest);
|
|
2799
|
+
if (!previous?.head || !/^[a-f0-9]{40,64}$/.test(previous.head)) throw new Error("The previous detached checkout evidence is unavailable.");
|
|
2800
|
+
try {
|
|
2801
|
+
await git2(ws.root, "merge-base", "--is-ancestor", previous.head, inputs.head);
|
|
2802
|
+
} catch {
|
|
2803
|
+
throw new Error("Detached checkout moved to a different history; original repair evidence was retained. Inspect that baseline explicitly.");
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
let cached = null;
|
|
2807
|
+
const diagnostics = [];
|
|
2808
|
+
try {
|
|
2809
|
+
cached = await readStoreJson(ws.root, ws.directory + "/cache.json");
|
|
2810
|
+
} catch {
|
|
2811
|
+
diagnostics.push("Unreadable automation cache; checks are being recomputed.");
|
|
2812
|
+
}
|
|
2813
|
+
const cache = checkCache(cached, inputs);
|
|
2814
|
+
if (cache.diagnostic) diagnostics.push(cache.diagnostic);
|
|
2815
|
+
const saveBaseline = async () => {
|
|
2816
|
+
if (baselines.length >= 128) throw new Error("128 retained baselines need review; automatic capture stopped without discarding original evidence.");
|
|
2817
|
+
const prepared = await prepareRepair(ws.root, ALL_CHECKS, cache.options);
|
|
2818
|
+
baselines.push({ path: prepared.baselinePath, at: now, event: event.event, fingerprint: inputs.fingerprint });
|
|
2819
|
+
};
|
|
2820
|
+
if (!baselines.length) await saveBaseline();
|
|
2821
|
+
const verifications = [];
|
|
2822
|
+
for (const baseline of baselines) verifications.push(await verifyRepair(ws.root, baseline.path, cache.options));
|
|
2823
|
+
const known = new Set(verifications.flatMap((v) => v.findings.map((f) => f.id)));
|
|
2824
|
+
if (verifications.some((v) => v.newFindings.some((f) => !known.has(findingId(f))))) {
|
|
2825
|
+
await saveBaseline();
|
|
2826
|
+
verifications.push(await verifyRepair(ws.root, baselines.at(-1).path, cache.options));
|
|
2827
|
+
}
|
|
2828
|
+
const merged = /* @__PURE__ */ new Map();
|
|
2829
|
+
for (const verification of verifications) {
|
|
2830
|
+
for (const finding of verification.findings) {
|
|
2831
|
+
const previous = merged.get(finding.id);
|
|
2832
|
+
if (!previous || priority[finding.status] > priority[previous.status]) merged.set(finding.id, finding);
|
|
2833
|
+
}
|
|
2834
|
+
diagnostics.push(...verification.diagnostics);
|
|
2835
|
+
}
|
|
2836
|
+
const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
|
|
2837
|
+
for (const finding of merged.values()) counts[finding.status]++;
|
|
2838
|
+
const report = {
|
|
2839
|
+
version: 1,
|
|
2840
|
+
status: diagnostics.length || verifications.some((v) => v.status === "incomplete") ? "incomplete" : counts.unresolved ? "issues-remain" : "verified",
|
|
2841
|
+
root: ws.root,
|
|
2842
|
+
branch: ws.branch,
|
|
2843
|
+
head: inputs.head,
|
|
2844
|
+
baselinePaths: baselines.map((b) => b.path),
|
|
2845
|
+
reportPath: "",
|
|
2846
|
+
findings: [...merged.values()],
|
|
2847
|
+
diagnostics: [...new Set(diagnostics)],
|
|
2848
|
+
checks: { ran: [...cache.ran], reused: [...cache.reused].filter((name) => !cache.ran.has(name)), skipped: verifications.at(-1).currentAudit?.skippedChecks ?? [] },
|
|
2849
|
+
counts,
|
|
2850
|
+
capture: "unknown",
|
|
2851
|
+
scope: SCOPE
|
|
2852
|
+
};
|
|
2853
|
+
return { baselines, report, cache: cache.ran.size || cached === null || cache.diagnostic ? cache.serialize() : null };
|
|
2854
|
+
}
|
|
2855
|
+
async function analyze(ws, inputs, event, startedAt) {
|
|
2856
|
+
return withLock(ws.root, ws.directory + "/analysis/" + inputs.fingerprint, async () => {
|
|
2857
|
+
const before = await loadState(ws);
|
|
2858
|
+
const shared = await sharedAnalysis(ws, before, inputs, startedAt);
|
|
2859
|
+
if (shared) return { analysis: shared, shared: true };
|
|
2860
|
+
const originalDigest = await baselineDigest(ws, before);
|
|
2861
|
+
const computed = await computeAnalysis(ws, before, inputs, event);
|
|
2862
|
+
await verifyInputs(ws, inputs);
|
|
2863
|
+
if (originalDigest !== await baselineDigest(ws, before)) throw new Error("Repair baseline was modified during automation; original evidence must be inspected.");
|
|
2864
|
+
const id = randomUUID4();
|
|
2865
|
+
const analysis = analysisSchema.parse({
|
|
2866
|
+
id,
|
|
2867
|
+
fingerprint: inputs.fingerprint,
|
|
2868
|
+
completedAt: Date.now(),
|
|
2869
|
+
baselineDigest: await baselineDigest(ws, { ...before, baselines: computed.baselines }),
|
|
2870
|
+
report: computed.report
|
|
2871
|
+
});
|
|
2872
|
+
const analysisPath = ws.directory + "/checks/analysis-" + inputs.fingerprint + ".json";
|
|
2873
|
+
await withLock(ws.root, ws.directory, async () => {
|
|
2874
|
+
const state = await loadState(ws);
|
|
2875
|
+
if (hash(state.baselines) !== hash(before.baselines) || state.analysis?.id !== before.analysis?.id) {
|
|
2876
|
+
throw new Error("Automation evidence changed during analysis; retry against the retained baselines.");
|
|
2877
|
+
}
|
|
2878
|
+
await writeStoreJson(ws.root, analysisPath, { analysis, digest: hash(analysis) });
|
|
2879
|
+
if (computed.cache) await writeStoreJson(ws.root, ws.directory + "/cache.json", computed.cache);
|
|
2880
|
+
state.baselines = computed.baselines;
|
|
2881
|
+
state.analysis = { id, fingerprint: inputs.fingerprint, completedAt: analysis.completedAt, path: analysisPath };
|
|
2882
|
+
await writeStoreJson(ws.root, ws.directory + "/state.json", state);
|
|
2883
|
+
});
|
|
2884
|
+
return { analysis, shared: false };
|
|
2885
|
+
}, Math.max(0, 25e3 - (Date.now() - startedAt)));
|
|
2886
|
+
}
|
|
2642
2887
|
async function automate(dir, event) {
|
|
2888
|
+
const startedAt = Date.now();
|
|
2643
2889
|
const ws = await workspace(dir);
|
|
2644
|
-
return
|
|
2890
|
+
return recordExecution(ws.root, ws.directory, event.event, async () => {
|
|
2645
2891
|
const inputs = await readInputs(ws.root);
|
|
2646
|
-
const
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
root: ws.root,
|
|
2652
|
-
gitDir: ws.gitDir,
|
|
2653
|
-
branch: ws.branch,
|
|
2654
|
-
baselines: [],
|
|
2655
|
-
sessions: {},
|
|
2656
|
-
updatedAt: now,
|
|
2657
|
-
fingerprint: null,
|
|
2658
|
-
latest: null
|
|
2659
|
-
} : parseState(raw);
|
|
2660
|
-
if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
|
|
2661
|
-
throw new Error("Automation state belongs to another branch or worktree; original evidence was retained.");
|
|
2662
|
-
}
|
|
2663
|
-
if (ws.branch === "detached" && state.latest) {
|
|
2664
|
-
const previous = await readStoreJson(ws.root, state.latest);
|
|
2665
|
-
if (!previous?.head || !/^[a-f0-9]{40,64}$/.test(previous.head)) throw new Error("The previous detached checkout evidence is unavailable.");
|
|
2666
|
-
try {
|
|
2667
|
-
await git2(ws.root, "merge-base", "--is-ancestor", previous.head, inputs.head);
|
|
2668
|
-
} catch {
|
|
2669
|
-
throw new Error("Detached checkout moved to a different history; original repair evidence was retained. Inspect that baseline explicitly.");
|
|
2892
|
+
const result = await analyze(ws, inputs, event, startedAt);
|
|
2893
|
+
if (result.shared) {
|
|
2894
|
+
await verifyInputs(ws, inputs);
|
|
2895
|
+
if (result.analysis.baselineDigest !== await baselineDigest(ws, await loadState(ws))) {
|
|
2896
|
+
throw new Error("Repair baseline was modified during shared automation; original evidence must be inspected.");
|
|
2670
2897
|
}
|
|
2671
2898
|
}
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
const
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2899
|
+
return withLock(ws.root, ws.directory, async () => {
|
|
2900
|
+
const state = await loadState(ws);
|
|
2901
|
+
if (state.analysis?.id !== result.analysis.id) throw new Error("Automation evidence changed during publication; retry against the current inputs.");
|
|
2902
|
+
const report = structuredClone(result.analysis.report);
|
|
2903
|
+
if (result.shared) report.checks = { ...report.checks, ran: [], reused: [.../* @__PURE__ */ new Set([...report.checks.ran, ...report.checks.reused])], shared: true };
|
|
2904
|
+
report.reportPath = ws.directory + "/checks/" + randomUUID4() + ".json";
|
|
2905
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2906
|
+
const key = event.host && event.sessionId ? hash([event.host, event.sessionId]) : null;
|
|
2907
|
+
const newSession = key !== null && !state.sessions[key];
|
|
2908
|
+
if (key && !state.sessions[key]) {
|
|
2909
|
+
const keys = Object.keys(state.sessions).sort((a, b) => state.sessions[a].lastUsed.localeCompare(state.sessions[b].lastUsed));
|
|
2910
|
+
const removable = keys.filter((k) => !Object.keys(state.sessions[k].pending).length);
|
|
2911
|
+
for (const expired of removable.slice(0, Math.max(0, keys.length - 31))) delete state.sessions[expired];
|
|
2912
|
+
if (Object.keys(state.sessions).length >= 128) throw new Error("Too many sessions with unfinished tool calls; original evidence was retained.");
|
|
2913
|
+
state.sessions[key] = {
|
|
2914
|
+
host: event.host,
|
|
2915
|
+
seen: null,
|
|
2916
|
+
continued: false,
|
|
2917
|
+
initialIssues: report.findings.filter((f) => f.status === "unresolved").map((f) => f.id),
|
|
2918
|
+
initialDocs: inputs.docs,
|
|
2919
|
+
lastUsed: now,
|
|
2920
|
+
mutationObserved: false,
|
|
2921
|
+
pending: {},
|
|
2922
|
+
coverageGaps: [],
|
|
2923
|
+
events: {}
|
|
2924
|
+
};
|
|
2697
2925
|
}
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2926
|
+
const session = key ? state.sessions[key] : null;
|
|
2927
|
+
if (session) {
|
|
2928
|
+
session.lastUsed = now;
|
|
2929
|
+
session.events[event.event] = { at: now, count: (session.events[event.event]?.count ?? 0) + 1 };
|
|
2930
|
+
if (event.event === "before_tool" && event.mutating && event.toolId) {
|
|
2931
|
+
if (Object.keys(session.pending).length >= 128) throw new Error("Too many unfinished tool calls to track pre-edit evidence.");
|
|
2932
|
+
session.pending[event.toolId] = inputs.fingerprint;
|
|
2703
2933
|
}
|
|
2704
|
-
if (event.
|
|
2934
|
+
if (event.event === "after_tool" && event.mutating) {
|
|
2935
|
+
session.mutationObserved = true;
|
|
2936
|
+
if (!event.toolId || !session.pending[event.toolId]) {
|
|
2937
|
+
const gap = "A tool completed without an observed matching pre-tool capture; pre-edit coverage is unknown.";
|
|
2938
|
+
if (!session.coverageGaps.includes(gap)) session.coverageGaps.push(gap);
|
|
2939
|
+
}
|
|
2940
|
+
if (event.toolId) delete session.pending[event.toolId];
|
|
2941
|
+
}
|
|
2942
|
+
report.diagnostics.push(...session.coverageGaps);
|
|
2943
|
+
if (session.coverageGaps.length && report.status !== "unavailable") report.status = "incomplete";
|
|
2944
|
+
report.capture = report.status !== "unavailable" && !session.coverageGaps.length && (session.events.session_start || session.events.before_tool) ? "observed" : "unknown";
|
|
2705
2945
|
}
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
const
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
head: inputs.head,
|
|
2714
|
-
baselinePaths: [],
|
|
2715
|
-
reportPath: ws.directory + "/checks/" + randomUUID4() + ".json",
|
|
2716
|
-
findings: [],
|
|
2717
|
-
diagnostics: ["No AGENTS.md, CLAUDE.md, or .claude/CLAUDE.md exists. Documentation capture is unavailable; other Mason tools remain usable."],
|
|
2718
|
-
checks: { ran: [], reused: [], skipped: [] },
|
|
2719
|
-
counts: { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 },
|
|
2720
|
-
capture: "unknown",
|
|
2721
|
-
scope: SCOPE
|
|
2722
|
-
};
|
|
2723
|
-
const notify2 = !session || session.seen !== "no-docs";
|
|
2724
|
-
if (session) session.seen = "no-docs";
|
|
2725
|
-
state.fingerprint = inputs.fingerprint;
|
|
2726
|
-
state.latest = report2.reportPath;
|
|
2727
|
-
state.updatedAt = now;
|
|
2728
|
-
await writeStoreJson(ws.root, report2.reportPath, report2);
|
|
2729
|
-
await writeStoreJson(ws.root, statePath, state);
|
|
2730
|
-
return { report: report2, message: notify2 ? summarize(report2) : null, continueOnce: false };
|
|
2731
|
-
}
|
|
2732
|
-
let cached = null;
|
|
2733
|
-
const diagnostics = [];
|
|
2734
|
-
try {
|
|
2735
|
-
cached = await readStoreJson(ws.root, ws.directory + "/cache.json");
|
|
2736
|
-
} catch {
|
|
2737
|
-
diagnostics.push("Unreadable automation cache; checks are being recomputed.");
|
|
2738
|
-
}
|
|
2739
|
-
const cache = checkCache(cached, inputs);
|
|
2740
|
-
if (cache.diagnostic) diagnostics.push(cache.diagnostic);
|
|
2741
|
-
const saveBaseline = async () => {
|
|
2742
|
-
if (state.baselines.length >= 128) throw new Error("128 retained baselines need review; automatic capture stopped without discarding original evidence.");
|
|
2743
|
-
const prepared = await prepareRepair(ws.root, ALL_CHECKS, cache.options);
|
|
2744
|
-
state.baselines.push({ path: prepared.baselinePath, at: now, event: event.event, fingerprint: inputs.fingerprint });
|
|
2745
|
-
};
|
|
2746
|
-
if (!state.baselines.length) await saveBaseline();
|
|
2747
|
-
const verifications = [];
|
|
2748
|
-
for (const baseline of state.baselines) verifications.push(await verifyRepair(ws.root, baseline.path, cache.options));
|
|
2749
|
-
const known = new Set(verifications.flatMap((v) => v.findings.map((f) => f.id)));
|
|
2750
|
-
if (verifications.some((v) => v.newFindings.some((f) => !known.has(findingId(f))))) {
|
|
2751
|
-
await saveBaseline();
|
|
2752
|
-
verifications.push(await verifyRepair(ws.root, state.baselines.at(-1).path, cache.options));
|
|
2753
|
-
}
|
|
2754
|
-
const merged = /* @__PURE__ */ new Map();
|
|
2755
|
-
for (const verification of verifications) {
|
|
2756
|
-
for (const finding of verification.findings) {
|
|
2757
|
-
const previous = merged.get(finding.id);
|
|
2758
|
-
if (!previous || priority[finding.status] > priority[previous.status]) merged.set(finding.id, finding);
|
|
2946
|
+
const signature = hash([report.status, report.findings, report.diagnostics, report.checks.skipped]);
|
|
2947
|
+
const relevant = report.findings.some((f) => f.status === "unresolved" && session && (!session.initialIssues.includes(f.id) || session.initialDocs[f.original.anchor.doc] !== inputs.docs[f.original.anchor.doc]));
|
|
2948
|
+
const continueOnce = event.event === "task_end" && !!session?.mutationObserved && relevant && !session.continued && !event.stopHookActive;
|
|
2949
|
+
const notify = !session || newSession || signature !== session.seen || continueOnce;
|
|
2950
|
+
if (session) {
|
|
2951
|
+
session.seen = signature;
|
|
2952
|
+
if (continueOnce) session.continued = true;
|
|
2759
2953
|
}
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
if (after.fingerprint !== inputs.fingerprint || currentWs.directory !== ws.directory) {
|
|
2771
|
-
throw new Error("Repository inputs or branch changed during automation; no current verification was recorded. Retry on a stable checkout.");
|
|
2772
|
-
}
|
|
2773
|
-
const report = {
|
|
2774
|
-
version: 1,
|
|
2775
|
-
status: diagnostics.length || verifications.some((v) => v.status === "incomplete") ? "incomplete" : counts.unresolved ? "issues-remain" : "verified",
|
|
2776
|
-
root: ws.root,
|
|
2777
|
-
branch: ws.branch,
|
|
2778
|
-
head: inputs.head,
|
|
2779
|
-
baselinePaths: state.baselines.map((b) => b.path),
|
|
2780
|
-
reportPath: ws.directory + "/checks/" + randomUUID4() + ".json",
|
|
2781
|
-
findings: [...merged.values()],
|
|
2782
|
-
diagnostics: [...new Set(diagnostics)],
|
|
2783
|
-
checks: { ran: [...cache.ran], reused: [...cache.reused].filter((name) => !cache.ran.has(name)), skipped: current?.skippedChecks ?? [] },
|
|
2784
|
-
counts,
|
|
2785
|
-
capture,
|
|
2786
|
-
scope: SCOPE
|
|
2787
|
-
};
|
|
2788
|
-
const signature = hash([report.status, report.findings, report.diagnostics, report.checks.skipped]);
|
|
2789
|
-
const relevant = report.findings.some((f) => f.status === "unresolved" && session && (!session.initialIssues.includes(f.id) || session.initialDocs[f.original.anchor.doc] !== inputs.docs[f.original.anchor.doc]));
|
|
2790
|
-
const continueOnce = event.event === "task_end" && !!session?.mutationObserved && relevant && !session.continued && !event.stopHookActive;
|
|
2791
|
-
const notify = !session || newSession || signature !== session.seen || continueOnce;
|
|
2792
|
-
if (session) {
|
|
2793
|
-
session.seen = signature;
|
|
2794
|
-
if (continueOnce) session.continued = true;
|
|
2795
|
-
}
|
|
2796
|
-
const persistReport = !state.latest || state.fingerprint !== inputs.fingerprint || notify || event.event === "task_end";
|
|
2797
|
-
if (!persistReport) report.reportPath = state.latest;
|
|
2798
|
-
state.updatedAt = now;
|
|
2799
|
-
state.fingerprint = inputs.fingerprint;
|
|
2800
|
-
state.latest = report.reportPath;
|
|
2801
|
-
if (persistReport) await writeStoreJson(ws.root, report.reportPath, report);
|
|
2802
|
-
if (cache.ran.size || cached === null || cache.diagnostic) await writeStoreJson(ws.root, ws.directory + "/cache.json", cache.serialize());
|
|
2803
|
-
await writeStoreJson(ws.root, statePath, state);
|
|
2804
|
-
return { report, message: notify ? summarize(report) : null, continueOnce };
|
|
2805
|
-
}));
|
|
2954
|
+
const persistReport = !state.latest || state.fingerprint !== inputs.fingerprint || notify || event.event === "task_end";
|
|
2955
|
+
if (!persistReport) report.reportPath = state.latest;
|
|
2956
|
+
state.updatedAt = now;
|
|
2957
|
+
state.fingerprint = inputs.fingerprint;
|
|
2958
|
+
state.latest = report.reportPath;
|
|
2959
|
+
if (persistReport) await writeStoreJson(ws.root, report.reportPath, report);
|
|
2960
|
+
await writeStoreJson(ws.root, ws.directory + "/state.json", state);
|
|
2961
|
+
return { report, message: notify ? summarize(report) : null, continueOnce };
|
|
2962
|
+
});
|
|
2963
|
+
});
|
|
2806
2964
|
}
|
|
2807
2965
|
async function automationStatus(dir) {
|
|
2808
2966
|
const ws = await workspace(dir);
|
|
2809
2967
|
const execution = await executionStatus(ws.root, ws.directory);
|
|
2968
|
+
const notifications = await failureNotifications(ws.root, ws.directory);
|
|
2810
2969
|
const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
|
|
2811
|
-
if (raw === null) return { version: 1, status: execution.status === "not-observed" ? "not-observed" : "unavailable", root: ws.root, branch: ws.branch, baselinePaths: [], hosts: {}, execution };
|
|
2970
|
+
if (raw === null) return { version: 1, status: execution.status === "not-observed" ? "not-observed" : "unavailable", root: ws.root, branch: ws.branch, baselinePaths: [], hosts: {}, execution, notifications };
|
|
2812
2971
|
const state = parseState(raw);
|
|
2813
2972
|
if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) throw new Error("Automation state belongs to another workspace.");
|
|
2814
2973
|
const inputs = await readInputs(ws.root);
|
|
@@ -2830,10 +2989,11 @@ async function automationStatus(dir) {
|
|
|
2830
2989
|
verificationStatus: unfinished ? "unavailable" : latest?.status ?? "unavailable",
|
|
2831
2990
|
hosts: hosts2,
|
|
2832
2991
|
execution,
|
|
2992
|
+
notifications,
|
|
2833
2993
|
note: "Observed events do not prove all tool paths are intercepted. Run check to verify the retained evidence."
|
|
2834
2994
|
};
|
|
2835
2995
|
}
|
|
2836
|
-
var SCOPE, priority, cleanText;
|
|
2996
|
+
var SCOPE, priority, cleanText, analysisSchema, sharedSchema, baselineDigest;
|
|
2837
2997
|
var init_runtime = __esm({
|
|
2838
2998
|
"src/automation/runtime.ts"() {
|
|
2839
2999
|
"use strict";
|
|
@@ -2846,11 +3006,40 @@ var init_runtime = __esm({
|
|
|
2846
3006
|
SCOPE = "Documentation audit evidence only. Hook receipts show observed events, not complete interception. Resolved claims no longer fail their checks; advisories need separate review. Repair only within the user's task authorization.";
|
|
2847
3007
|
priority = { resolved: 0, "review-required": 1, unresolved: 2, unverified: 3 };
|
|
2848
3008
|
cleanText = (text2) => text2.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 250);
|
|
3009
|
+
analysisSchema = z8.object({
|
|
3010
|
+
id: z8.string(),
|
|
3011
|
+
fingerprint: z8.string(),
|
|
3012
|
+
completedAt: z8.number(),
|
|
3013
|
+
baselineDigest: z8.string(),
|
|
3014
|
+
report: z8.object({
|
|
3015
|
+
version: z8.literal(1),
|
|
3016
|
+
status: z8.enum(["verified", "issues-remain", "incomplete", "unavailable"]),
|
|
3017
|
+
root: z8.string(),
|
|
3018
|
+
branch: z8.string(),
|
|
3019
|
+
head: z8.string(),
|
|
3020
|
+
baselinePaths: z8.array(z8.string()),
|
|
3021
|
+
reportPath: z8.string(),
|
|
3022
|
+
findings: z8.array(repairFindingSchema),
|
|
3023
|
+
diagnostics: z8.array(z8.string()),
|
|
3024
|
+
checks: z8.object({
|
|
3025
|
+
ran: z8.array(z8.string()),
|
|
3026
|
+
reused: z8.array(z8.string()),
|
|
3027
|
+
skipped: z8.array(z8.object({ check: z8.string(), reason: z8.string(), doc: z8.string().optional() }))
|
|
3028
|
+
}),
|
|
3029
|
+
counts: z8.object({ resolved: z8.number(), unresolved: z8.number(), "review-required": z8.number(), unverified: z8.number() }),
|
|
3030
|
+
capture: z8.enum(["observed", "unknown"]),
|
|
3031
|
+
scope: z8.string()
|
|
3032
|
+
})
|
|
3033
|
+
});
|
|
3034
|
+
sharedSchema = z8.object({ analysis: analysisSchema, digest: z8.string() });
|
|
3035
|
+
baselineDigest = async (ws, state) => hash(await Promise.all(
|
|
3036
|
+
state.baselines.map(async (b) => [b.path, await readStoreJson(ws.root, b.path)])
|
|
3037
|
+
));
|
|
2849
3038
|
}
|
|
2850
3039
|
});
|
|
2851
3040
|
|
|
2852
3041
|
// src/setup/model.ts
|
|
2853
|
-
import { z as
|
|
3042
|
+
import { z as z9 } from "zod";
|
|
2854
3043
|
async function loadSetup(root) {
|
|
2855
3044
|
const raw = await readStoreJson(root, SETUP_PATH);
|
|
2856
3045
|
if (raw === null) return null;
|
|
@@ -2868,27 +3057,27 @@ var init_model = __esm({
|
|
|
2868
3057
|
"src/setup/model.ts"() {
|
|
2869
3058
|
"use strict";
|
|
2870
3059
|
init_storage();
|
|
2871
|
-
setupHostSchema =
|
|
2872
|
-
configuredVersion:
|
|
2873
|
-
revision:
|
|
2874
|
-
fingerprint:
|
|
2875
|
-
mcpFingerprint:
|
|
2876
|
-
instructions:
|
|
3060
|
+
setupHostSchema = z9.object({
|
|
3061
|
+
configuredVersion: z9.string(),
|
|
3062
|
+
revision: z9.string().uuid(),
|
|
3063
|
+
fingerprint: z9.string(),
|
|
3064
|
+
mcpFingerprint: z9.string(),
|
|
3065
|
+
instructions: z9.array(z9.string())
|
|
2877
3066
|
});
|
|
2878
|
-
setupSchema =
|
|
3067
|
+
setupSchema = z9.object({ version: z9.literal(2), hosts: z9.object({
|
|
2879
3068
|
codex: setupHostSchema.optional(),
|
|
2880
3069
|
claude: setupHostSchema.optional()
|
|
2881
3070
|
}) });
|
|
2882
3071
|
SETUP_PATH = ".mason/local/setup.json";
|
|
2883
|
-
receiptSchema =
|
|
2884
|
-
version:
|
|
2885
|
-
host:
|
|
2886
|
-
status:
|
|
2887
|
-
initialReportPath:
|
|
2888
|
-
initialBaselinePaths:
|
|
2889
|
-
root:
|
|
2890
|
-
revision:
|
|
2891
|
-
configuredAt:
|
|
3072
|
+
receiptSchema = z9.object({
|
|
3073
|
+
version: z9.literal(1),
|
|
3074
|
+
host: z9.enum(["codex", "claude"]),
|
|
3075
|
+
status: z9.enum(["installing", "configured"]),
|
|
3076
|
+
initialReportPath: z9.string(),
|
|
3077
|
+
initialBaselinePaths: z9.array(z9.string()),
|
|
3078
|
+
root: z9.string(),
|
|
3079
|
+
revision: z9.string().uuid(),
|
|
3080
|
+
configuredAt: z9.string().optional()
|
|
2892
3081
|
});
|
|
2893
3082
|
}
|
|
2894
3083
|
});
|
|
@@ -2896,6 +3085,7 @@ var init_model = __esm({
|
|
|
2896
3085
|
// src/setup/launcher.ts
|
|
2897
3086
|
import { execFile as execFile9 } from "child_process";
|
|
2898
3087
|
import { promisify as promisify9 } from "util";
|
|
3088
|
+
import fs14 from "fs/promises";
|
|
2899
3089
|
function globalCommand(args, platform = process.platform) {
|
|
2900
3090
|
return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args] } : { command: "mason", args };
|
|
2901
3091
|
}
|
|
@@ -2920,7 +3110,7 @@ var init_launcher = __esm({
|
|
|
2920
3110
|
"use strict";
|
|
2921
3111
|
init_evidence();
|
|
2922
3112
|
init_model();
|
|
2923
|
-
executingVersion = () => true ? "0.16.
|
|
3113
|
+
executingVersion = () => true ? "0.16.3" : "development";
|
|
2924
3114
|
mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
|
|
2925
3115
|
hookCommand = (host) => `mason --setup-host ${host} auto`;
|
|
2926
3116
|
}
|
|
@@ -2933,8 +3123,8 @@ __export(observations_exports, {
|
|
|
2933
3123
|
observeActivation: () => observeActivation,
|
|
2934
3124
|
readObservation: () => readObservation
|
|
2935
3125
|
});
|
|
2936
|
-
import
|
|
2937
|
-
import { z as
|
|
3126
|
+
import fs15 from "fs/promises";
|
|
3127
|
+
import { z as z10 } from "zod";
|
|
2938
3128
|
function observationPath(directory, host, revision, version = executingVersion()) {
|
|
2939
3129
|
return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
|
|
2940
3130
|
}
|
|
@@ -2949,9 +3139,9 @@ async function observeActivation(dir, event, options = {}) {
|
|
|
2949
3139
|
const host = process.env.MASON_SETUP_HOST, revision = process.env.MASON_SETUP_REVISION;
|
|
2950
3140
|
if (host !== "codex" && host !== "claude" || !revision || !process.env.MASON_SETUP_ROOT) return null;
|
|
2951
3141
|
try {
|
|
2952
|
-
const capturedDirectory = options.reportPath?.match(/^(\.mason\/reports\/automation\/[a-f0-9]{24})\/checks\//)?.[1];
|
|
2953
|
-
const ws = capturedDirectory ? { root: await
|
|
2954
|
-
if (ws.root !== await
|
|
3142
|
+
const capturedDirectory = options.directory?.match(/^\.mason\/reports\/automation\/[a-f0-9]{24}$/)?.[0] ?? options.reportPath?.match(/^(\.mason\/reports\/automation\/[a-f0-9]{24})\/checks\//)?.[1];
|
|
3143
|
+
const ws = capturedDirectory ? { root: await fs15.realpath(dir), directory: capturedDirectory } : await workspace(dir);
|
|
3144
|
+
if (ws.root !== await fs15.realpath(process.env.MASON_SETUP_ROOT)) return null;
|
|
2955
3145
|
const setup = await loadSetup(ws.root);
|
|
2956
3146
|
if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
|
|
2957
3147
|
const directory = ws.directory + "/activation";
|
|
@@ -2999,45 +3189,59 @@ var init_observations = __esm({
|
|
|
2999
3189
|
init_storage();
|
|
3000
3190
|
init_model();
|
|
3001
3191
|
init_launcher();
|
|
3002
|
-
observationSchema =
|
|
3003
|
-
version:
|
|
3004
|
-
masonVersion:
|
|
3005
|
-
root:
|
|
3006
|
-
revision:
|
|
3007
|
-
host:
|
|
3008
|
-
contextCalls:
|
|
3009
|
-
lastContextAt:
|
|
3010
|
-
sessions:
|
|
3011
|
-
events:
|
|
3012
|
-
at:
|
|
3013
|
-
verificationStatus:
|
|
3014
|
-
reportPath:
|
|
3192
|
+
observationSchema = z10.object({
|
|
3193
|
+
version: z10.literal(1),
|
|
3194
|
+
masonVersion: z10.string(),
|
|
3195
|
+
root: z10.string(),
|
|
3196
|
+
revision: z10.string(),
|
|
3197
|
+
host: z10.enum(["codex", "claude"]),
|
|
3198
|
+
contextCalls: z10.number().int().nonnegative(),
|
|
3199
|
+
lastContextAt: z10.string().optional(),
|
|
3200
|
+
sessions: z10.record(z10.object({
|
|
3201
|
+
events: z10.array(z10.enum(events)),
|
|
3202
|
+
at: z10.string(),
|
|
3203
|
+
verificationStatus: z10.string().optional(),
|
|
3204
|
+
reportPath: z10.string().optional()
|
|
3015
3205
|
}))
|
|
3016
3206
|
});
|
|
3017
3207
|
}
|
|
3018
3208
|
});
|
|
3019
3209
|
|
|
3020
3210
|
// src/automation/adapters.ts
|
|
3021
|
-
import { z as
|
|
3211
|
+
import { z as z11 } from "zod";
|
|
3022
3212
|
function normalizeHook(host, raw) {
|
|
3023
3213
|
hostSchema.parse(host);
|
|
3024
3214
|
const input2 = inputSchema.parse(raw);
|
|
3025
|
-
const readOnly = host === "claude" ? /^(Read|Glob|Grep|WebSearch|WebFetch)$/ : /^(read_file|list_dir|grep_files)
|
|
3026
|
-
return { cwd: input2.cwd, name: input2.hook_event_name, event: {
|
|
3215
|
+
const readOnly = (host === "claude" ? /^(Read|Glob|Grep|WebSearch|WebFetch)$/ : /^(read_file|list_dir|grep_files|Read|Glob|Grep)$/).test(input2.tool_name ?? "");
|
|
3216
|
+
return { cwd: input2.cwd, name: input2.hook_event_name, readOnly, event: {
|
|
3027
3217
|
event: lifecycle[input2.hook_event_name],
|
|
3028
3218
|
host,
|
|
3029
3219
|
sessionId: input2.session_id,
|
|
3030
3220
|
toolId: input2.tool_use_id,
|
|
3031
|
-
mutating: !!input2.tool_name && !readOnly
|
|
3221
|
+
mutating: !!input2.tool_name && !readOnly,
|
|
3032
3222
|
stopHookActive: input2.stop_hook_active || input2.permission_mode === "plan"
|
|
3033
3223
|
} };
|
|
3034
3224
|
}
|
|
3035
3225
|
async function runAutomationHook(host, stdin) {
|
|
3036
3226
|
let name = "";
|
|
3227
|
+
let hook;
|
|
3037
3228
|
try {
|
|
3038
3229
|
if (Buffer.byteLength(stdin) > 1024 * 1024) throw new Error("Hook input exceeds 1 MiB.");
|
|
3039
|
-
|
|
3230
|
+
let raw;
|
|
3231
|
+
try {
|
|
3232
|
+
raw = JSON.parse(stdin);
|
|
3233
|
+
} catch {
|
|
3234
|
+
throw new Error("Hook input is not valid JSON.");
|
|
3235
|
+
}
|
|
3236
|
+
const input2 = normalizeHook(host, raw);
|
|
3237
|
+
hook = input2;
|
|
3040
3238
|
name = input2.name;
|
|
3239
|
+
if (["before_tool", "after_tool"].includes(input2.event.event) && input2.readOnly) {
|
|
3240
|
+
const observed = await observeReadOnlyTool(input2.cwd, input2.event);
|
|
3241
|
+
const { observeActivation: observeActivation3 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
|
|
3242
|
+
const warning2 = await observeActivation3(observed.root, input2.event.event, { sessionId: input2.event.sessionId, directory: observed.directory });
|
|
3243
|
+
return warning2 ? { systemMessage: warning2 } : null;
|
|
3244
|
+
}
|
|
3041
3245
|
const result = await automate(input2.cwd, input2.event);
|
|
3042
3246
|
const { observeActivation: observeActivation2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
|
|
3043
3247
|
const warning = await observeActivation2(result.report.root, input2.event.event, {
|
|
@@ -3052,12 +3256,20 @@ async function runAutomationHook(host, stdin) {
|
|
|
3052
3256
|
}
|
|
3053
3257
|
return { hookSpecificOutput: { hookEventName: name, additionalContext: result.message } };
|
|
3054
3258
|
} catch (error) {
|
|
3055
|
-
const message2 = failureMessage(error);
|
|
3259
|
+
const message2 = hook ? await hookFailureMessage(hook.cwd, host, hook.event.sessionId, error) : failureMessage(error);
|
|
3260
|
+
if (!message2) return null;
|
|
3056
3261
|
return { systemMessage: message2, ...["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse"].includes(name) ? { hookSpecificOutput: { hookEventName: name, additionalContext: message2 } } : {} };
|
|
3057
3262
|
}
|
|
3058
3263
|
}
|
|
3264
|
+
function managedHookCommand(host, platform = process.platform) {
|
|
3265
|
+
const command = `mason --setup-host ${host} auto hook --host ${host}`;
|
|
3266
|
+
return platform === "win32" ? `cmd.exe /d /s /c "where mason >nul 2>nul & if errorlevel 1 (exit /b 0) else (${command})"` : `if command -v mason >/dev/null 2>&1; then ${command}; fi`;
|
|
3267
|
+
}
|
|
3268
|
+
function knownManagedHookCommands(host) {
|
|
3269
|
+
return [managedHookCommand(host, "linux"), managedHookCommand(host, "win32"), `mason --setup-host ${host} auto hook --host ${host}`];
|
|
3270
|
+
}
|
|
3059
3271
|
function hookConfig(host, command = "npx --no-install --package mason-context mason-auto") {
|
|
3060
|
-
const handler = { type: "command", command: command + " hook --host " + host, timeout: 30 };
|
|
3272
|
+
const handler = { type: "command", command: command === `mason --setup-host ${host} auto` ? managedHookCommand(host) : command + " hook --host " + host, timeout: 30 };
|
|
3061
3273
|
return { hooks: Object.fromEntries(HOOK_EVENTS.map((name) => [
|
|
3062
3274
|
name,
|
|
3063
3275
|
[{ ...["PreToolUse", "PostToolUse"].includes(name) ? { matcher: ".*" } : {}, hooks: [{ ...handler }] }]
|
|
@@ -3070,15 +3282,15 @@ var init_adapters = __esm({
|
|
|
3070
3282
|
init_execution();
|
|
3071
3283
|
init_runtime();
|
|
3072
3284
|
init_store();
|
|
3073
|
-
inputSchema =
|
|
3074
|
-
cwd:
|
|
3075
|
-
session_id:
|
|
3076
|
-
hook_event_name:
|
|
3077
|
-
tool_name:
|
|
3078
|
-
tool_use_id:
|
|
3079
|
-
tool_input:
|
|
3080
|
-
stop_hook_active:
|
|
3081
|
-
permission_mode:
|
|
3285
|
+
inputSchema = z11.object({
|
|
3286
|
+
cwd: z11.string().min(1),
|
|
3287
|
+
session_id: z11.string().min(1).max(500),
|
|
3288
|
+
hook_event_name: z11.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
|
|
3289
|
+
tool_name: z11.string().optional(),
|
|
3290
|
+
tool_use_id: z11.string().max(500).optional(),
|
|
3291
|
+
tool_input: z11.unknown().optional(),
|
|
3292
|
+
stop_hook_active: z11.boolean().optional(),
|
|
3293
|
+
permission_mode: z11.string().optional()
|
|
3082
3294
|
});
|
|
3083
3295
|
lifecycle = {
|
|
3084
3296
|
SessionStart: "session_start",
|
|
@@ -3098,7 +3310,7 @@ __export(files_exports, {
|
|
|
3098
3310
|
managedBlock: () => managedBlock,
|
|
3099
3311
|
readText: () => readText
|
|
3100
3312
|
});
|
|
3101
|
-
import
|
|
3313
|
+
import fs16 from "fs/promises";
|
|
3102
3314
|
import path18 from "path";
|
|
3103
3315
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
3104
3316
|
async function readText(root, file) {
|
|
@@ -3128,14 +3340,14 @@ async function applyEdit(root, edit) {
|
|
|
3128
3340
|
if (edit.after === null) {
|
|
3129
3341
|
const file2 = await storePath(root, edit.path);
|
|
3130
3342
|
if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
|
|
3131
|
-
await
|
|
3343
|
+
await fs16.unlink(file2);
|
|
3132
3344
|
return true;
|
|
3133
3345
|
}
|
|
3134
3346
|
const file = await storePath(root, edit.path, true);
|
|
3135
3347
|
const temporary = path18.join(path18.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
|
|
3136
3348
|
try {
|
|
3137
|
-
const mode = await
|
|
3138
|
-
const handle = await
|
|
3349
|
+
const mode = await fs16.stat(file).then((s) => s.mode & 511, () => 384);
|
|
3350
|
+
const handle = await fs16.open(temporary, "wx", mode);
|
|
3139
3351
|
try {
|
|
3140
3352
|
await handle.writeFile(edit.after, "utf8");
|
|
3141
3353
|
await handle.sync();
|
|
@@ -3143,10 +3355,10 @@ async function applyEdit(root, edit) {
|
|
|
3143
3355
|
await handle.close();
|
|
3144
3356
|
}
|
|
3145
3357
|
if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
|
|
3146
|
-
await
|
|
3358
|
+
await fs16.rename(temporary, file);
|
|
3147
3359
|
return true;
|
|
3148
3360
|
} finally {
|
|
3149
|
-
await
|
|
3361
|
+
await fs16.rm(temporary, { force: true });
|
|
3150
3362
|
}
|
|
3151
3363
|
}
|
|
3152
3364
|
var init_files2 = __esm({
|
|
@@ -3167,7 +3379,7 @@ __export(ownership_exports, {
|
|
|
3167
3379
|
ownershipEdit: () => ownershipEdit,
|
|
3168
3380
|
ownershipSchema: () => ownershipSchema
|
|
3169
3381
|
});
|
|
3170
|
-
import { z as
|
|
3382
|
+
import { z as z12 } from "zod";
|
|
3171
3383
|
import { parse } from "smol-toml";
|
|
3172
3384
|
function blockSpan(text2, start, end) {
|
|
3173
3385
|
const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
|
|
@@ -3218,30 +3430,30 @@ var init_ownership = __esm({
|
|
|
3218
3430
|
["<!-- mason:agents:start -->", "<!-- mason:agents:end -->"],
|
|
3219
3431
|
["# mason:mcp:start", "# mason:mcp:end"]
|
|
3220
3432
|
];
|
|
3221
|
-
fileSchema =
|
|
3222
|
-
created:
|
|
3223
|
-
initialHash:
|
|
3224
|
-
blocks:
|
|
3225
|
-
mcpHash:
|
|
3226
|
-
hookCommands:
|
|
3227
|
-
hooks:
|
|
3433
|
+
fileSchema = z12.object({
|
|
3434
|
+
created: z12.boolean(),
|
|
3435
|
+
initialHash: z12.string(),
|
|
3436
|
+
blocks: z12.record(z12.string()),
|
|
3437
|
+
mcpHash: z12.string().optional(),
|
|
3438
|
+
hookCommands: z12.array(z12.string()).optional(),
|
|
3439
|
+
hooks: z12.record(z12.array(z12.string())).optional()
|
|
3228
3440
|
});
|
|
3229
|
-
ownershipSchema =
|
|
3441
|
+
ownershipSchema = z12.object({ version: z12.literal(1), files: z12.record(fileSchema) });
|
|
3230
3442
|
}
|
|
3231
3443
|
});
|
|
3232
3444
|
|
|
3233
3445
|
// src/mcp/init.ts
|
|
3234
|
-
import { z as
|
|
3446
|
+
import { z as z13 } from "zod";
|
|
3235
3447
|
var markerSchema, CLAUDE_MD_SECTION, ASSISTANT_SETUP, QUICKSTART_PLAYBOOK, MAP_PLAYBOOK;
|
|
3236
3448
|
var init_init = __esm({
|
|
3237
3449
|
"src/mcp/init.ts"() {
|
|
3238
3450
|
"use strict";
|
|
3239
3451
|
init_storage();
|
|
3240
3452
|
init_files2();
|
|
3241
|
-
markerSchema =
|
|
3242
|
-
version:
|
|
3243
|
-
initializedAt:
|
|
3244
|
-
features:
|
|
3453
|
+
markerSchema = z13.object({
|
|
3454
|
+
version: z13.literal(1),
|
|
3455
|
+
initializedAt: z13.string(),
|
|
3456
|
+
features: z13.object({ confluence: z13.boolean().optional() }).passthrough().optional()
|
|
3245
3457
|
}).passthrough();
|
|
3246
3458
|
CLAUDE_MD_SECTION = `<!-- mason:start -->
|
|
3247
3459
|
## Mason project knowledge
|
|
@@ -3348,7 +3560,7 @@ __export(config_exports, {
|
|
|
3348
3560
|
});
|
|
3349
3561
|
import { isDeepStrictEqual } from "util";
|
|
3350
3562
|
import { parse as parse2, stringify } from "smol-toml";
|
|
3351
|
-
import { z as
|
|
3563
|
+
import { z as z14 } from "zod";
|
|
3352
3564
|
async function instructionEdits(root, host) {
|
|
3353
3565
|
const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
|
|
3354
3566
|
const existing = found.find((f) => f.text !== null);
|
|
@@ -3464,7 +3676,7 @@ var init_config = __esm({
|
|
|
3464
3676
|
init_install();
|
|
3465
3677
|
init_files2();
|
|
3466
3678
|
init_launcher();
|
|
3467
|
-
object =
|
|
3679
|
+
object = z14.record(z14.unknown());
|
|
3468
3680
|
mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
|
|
3469
3681
|
TOML_START = "# mason:mcp:start";
|
|
3470
3682
|
TOML_END = "# mason:mcp:end";
|
|
@@ -3484,7 +3696,7 @@ __export(install_exports, {
|
|
|
3484
3696
|
planAutomationInstall: () => planAutomationInstall,
|
|
3485
3697
|
recordSchema: () => recordSchema
|
|
3486
3698
|
});
|
|
3487
|
-
import { z as
|
|
3699
|
+
import { z as z15 } from "zod";
|
|
3488
3700
|
async function installationRecord(root) {
|
|
3489
3701
|
return recordSchema.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
|
|
3490
3702
|
}
|
|
@@ -3502,7 +3714,7 @@ async function planAutomationInstall(root, host, command) {
|
|
|
3502
3714
|
const previous = record.hosts[host]?.command;
|
|
3503
3715
|
const ownershipText = await readText(root, OWNERSHIP_PATH);
|
|
3504
3716
|
const pending = ownershipText === null ? [] : ownershipSchema.parse(JSON.parse(ownershipText)).files[file]?.hookCommands ?? [];
|
|
3505
|
-
const owned = /* @__PURE__ */ new Set([previous, ...pending, newCommand]);
|
|
3717
|
+
const owned = /* @__PURE__ */ new Set([previous, ...pending, newCommand, ...knownManagedHookCommands(host)]);
|
|
3506
3718
|
const hooks = existing.hooks ?? {};
|
|
3507
3719
|
for (const event of HOOK_EVENTS) {
|
|
3508
3720
|
hooks[event] = (hooks[event] ?? []).map((group) => ({
|
|
@@ -3565,9 +3777,9 @@ var init_install = __esm({
|
|
|
3565
3777
|
init_store();
|
|
3566
3778
|
init_files2();
|
|
3567
3779
|
init_ownership();
|
|
3568
|
-
groupSchema =
|
|
3569
|
-
automationConfigSchema =
|
|
3570
|
-
recordSchema =
|
|
3780
|
+
groupSchema = z15.object({ hooks: z15.array(z15.object({ type: z15.string(), command: z15.string().optional() }).passthrough()) }).passthrough();
|
|
3781
|
+
automationConfigSchema = z15.object({ hooks: z15.record(z15.array(groupSchema)).optional() }).passthrough();
|
|
3782
|
+
recordSchema = z15.object({ version: z15.literal(1), hosts: z15.record(z15.object({ command: z15.string() })) });
|
|
3571
3783
|
AUTOMATION_PATH = ".mason/local/automation.json";
|
|
3572
3784
|
configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
|
|
3573
3785
|
}
|
|
@@ -3582,7 +3794,7 @@ __export(teardown_exports, {
|
|
|
3582
3794
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
3583
3795
|
import { parse as parse3, stringify as stringify2 } from "smol-toml";
|
|
3584
3796
|
import fg5 from "fast-glob";
|
|
3585
|
-
import { z as
|
|
3797
|
+
import { z as z16 } from "zod";
|
|
3586
3798
|
async function planTeardown(root, selected) {
|
|
3587
3799
|
const edits = [], diagnostics = [];
|
|
3588
3800
|
const inputs = /* @__PURE__ */ new Map();
|
|
@@ -3606,6 +3818,7 @@ async function planTeardown(root, selected) {
|
|
|
3606
3818
|
const commands = (host) => new Set([
|
|
3607
3819
|
automation?.hosts[host]?.command,
|
|
3608
3820
|
...ownership.files[configPath(host)]?.hookCommands ?? [],
|
|
3821
|
+
...knownManagedHookCommands(host),
|
|
3609
3822
|
hookConfig(host, hookCommand(host)).hooks.SessionStart[0].hooks[0].command,
|
|
3610
3823
|
hookConfig(host).hooks.SessionStart[0].hooks[0].command
|
|
3611
3824
|
].filter((command) => !!command));
|
|
@@ -3728,7 +3941,7 @@ async function planTeardown(root, selected) {
|
|
|
3728
3941
|
if (!remainingHost) await attempt(".mason/local/project.json", async () => {
|
|
3729
3942
|
const before = await read(".mason/local/project.json");
|
|
3730
3943
|
if (before === null) return;
|
|
3731
|
-
|
|
3944
|
+
z16.object({ version: z16.literal(1), initializedAt: z16.string() }).strict().parse(JSON.parse(before));
|
|
3732
3945
|
cleanup.push({ path: ".mason/local/project.json", before, after: null });
|
|
3733
3946
|
});
|
|
3734
3947
|
if (!diagnostics.length) {
|
|
@@ -3800,7 +4013,7 @@ var init_teardown = __esm({
|
|
|
3800
4013
|
init_model();
|
|
3801
4014
|
init_ownership();
|
|
3802
4015
|
hosts = ["codex", "claude"];
|
|
3803
|
-
object2 =
|
|
4016
|
+
object2 = z16.record(z16.unknown());
|
|
3804
4017
|
json = (value) => JSON.stringify(value, null, 2) + "\n";
|
|
3805
4018
|
}
|
|
3806
4019
|
});
|
|
@@ -3927,7 +4140,7 @@ var init_types2 = __esm({
|
|
|
3927
4140
|
});
|
|
3928
4141
|
|
|
3929
4142
|
// src/review/evidence/vitest.ts
|
|
3930
|
-
import { z as
|
|
4143
|
+
import { z as z17 } from "zod";
|
|
3931
4144
|
function parseVitest(raw, sourceRoot) {
|
|
3932
4145
|
const report = schema.parse(raw);
|
|
3933
4146
|
const findings = [], diagnostics = [];
|
|
@@ -3976,24 +4189,24 @@ var init_vitest = __esm({
|
|
|
3976
4189
|
"use strict";
|
|
3977
4190
|
init_paths2();
|
|
3978
4191
|
init_types2();
|
|
3979
|
-
count2 =
|
|
3980
|
-
schema =
|
|
3981
|
-
success:
|
|
4192
|
+
count2 = z17.number().int().nonnegative();
|
|
4193
|
+
schema = z17.object({
|
|
4194
|
+
success: z17.boolean(),
|
|
3982
4195
|
numTotalTests: count2,
|
|
3983
4196
|
numPassedTests: count2,
|
|
3984
4197
|
numFailedTests: count2,
|
|
3985
4198
|
numPendingTests: count2,
|
|
3986
4199
|
numTodoTests: count2,
|
|
3987
4200
|
numFailedTestSuites: count2,
|
|
3988
|
-
testResults:
|
|
3989
|
-
name:
|
|
3990
|
-
status:
|
|
3991
|
-
message:
|
|
3992
|
-
assertionResults:
|
|
3993
|
-
fullName:
|
|
3994
|
-
status:
|
|
3995
|
-
failureMessages:
|
|
3996
|
-
location:
|
|
4201
|
+
testResults: z17.array(z17.object({
|
|
4202
|
+
name: z17.string().min(1),
|
|
4203
|
+
status: z17.enum(["passed", "failed"]),
|
|
4204
|
+
message: z17.string().optional(),
|
|
4205
|
+
assertionResults: z17.array(z17.object({
|
|
4206
|
+
fullName: z17.string(),
|
|
4207
|
+
status: z17.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
|
|
4208
|
+
failureMessages: z17.array(z17.string()).nullable().optional(),
|
|
4209
|
+
location: z17.object({ line: count2, column: count2 }).nullable().optional()
|
|
3997
4210
|
}))
|
|
3998
4211
|
}))
|
|
3999
4212
|
});
|
|
@@ -4001,7 +4214,7 @@ var init_vitest = __esm({
|
|
|
4001
4214
|
});
|
|
4002
4215
|
|
|
4003
4216
|
// src/review/evidence/sarif.ts
|
|
4004
|
-
import { z as
|
|
4217
|
+
import { z as z18 } from "zod";
|
|
4005
4218
|
function parseSarif(raw, sourceRoot) {
|
|
4006
4219
|
const report = schema2.parse(raw), findings = [], diagnostics = [];
|
|
4007
4220
|
let active = 0, suppressed = 0, absent = 0, unresolved = 0;
|
|
@@ -4092,29 +4305,29 @@ var init_sarif = __esm({
|
|
|
4092
4305
|
"use strict";
|
|
4093
4306
|
init_paths2();
|
|
4094
4307
|
init_types2();
|
|
4095
|
-
index =
|
|
4096
|
-
artifact =
|
|
4097
|
-
message =
|
|
4098
|
-
level =
|
|
4099
|
-
location =
|
|
4100
|
-
schema2 =
|
|
4101
|
-
version:
|
|
4102
|
-
runs:
|
|
4103
|
-
tool:
|
|
4104
|
-
invocations:
|
|
4105
|
-
versionControlProvenance:
|
|
4106
|
-
originalUriBaseIds:
|
|
4107
|
-
artifacts:
|
|
4108
|
-
results:
|
|
4109
|
-
ruleId:
|
|
4308
|
+
index = z18.number().int().nonnegative();
|
|
4309
|
+
artifact = z18.object({ uri: z18.string().optional(), uriBaseId: z18.string().optional(), index: index.optional() });
|
|
4310
|
+
message = z18.object({ text: z18.string().optional(), markdown: z18.string().optional(), id: z18.string().optional(), arguments: z18.array(z18.string()).optional() });
|
|
4311
|
+
level = z18.enum(["error", "warning", "note", "none"]);
|
|
4312
|
+
location = z18.object({ physicalLocation: z18.object({ artifactLocation: artifact.optional(), region: z18.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
|
|
4313
|
+
schema2 = z18.object({
|
|
4314
|
+
version: z18.literal("2.1.0"),
|
|
4315
|
+
runs: z18.array(z18.object({
|
|
4316
|
+
tool: z18.object({ driver: z18.object({ name: z18.string().min(1), rules: z18.array(z18.object({ id: z18.string(), defaultConfiguration: z18.object({ level: level.optional() }).optional(), messageStrings: z18.record(message).optional() })).optional(), globalMessageStrings: z18.record(message).optional() }) }),
|
|
4317
|
+
invocations: z18.array(z18.object({ executionSuccessful: z18.boolean(), commandLine: z18.string().optional(), toolExecutionNotifications: z18.array(z18.object({ level: level.optional(), message })).optional() })).optional(),
|
|
4318
|
+
versionControlProvenance: z18.array(z18.object({ revisionId: z18.string().optional() })).optional(),
|
|
4319
|
+
originalUriBaseIds: z18.record(artifact).optional(),
|
|
4320
|
+
artifacts: z18.array(z18.object({ location: artifact.optional() })).optional(),
|
|
4321
|
+
results: z18.array(z18.object({
|
|
4322
|
+
ruleId: z18.string().optional(),
|
|
4110
4323
|
ruleIndex: index.optional(),
|
|
4111
4324
|
message,
|
|
4112
4325
|
level: level.optional(),
|
|
4113
|
-
kind:
|
|
4114
|
-
baselineState:
|
|
4115
|
-
suppressions:
|
|
4116
|
-
locations:
|
|
4117
|
-
relatedLocations:
|
|
4326
|
+
kind: z18.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
|
|
4327
|
+
baselineState: z18.enum(["new", "unchanged", "updated", "absent"]).optional(),
|
|
4328
|
+
suppressions: z18.array(z18.object({ kind: z18.enum(["inSource", "external"]), status: z18.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
|
|
4329
|
+
locations: z18.array(location).optional(),
|
|
4330
|
+
relatedLocations: z18.array(location).optional()
|
|
4118
4331
|
})).optional()
|
|
4119
4332
|
}))
|
|
4120
4333
|
});
|
|
@@ -4123,7 +4336,7 @@ var init_sarif = __esm({
|
|
|
4123
4336
|
|
|
4124
4337
|
// src/review/evidence.ts
|
|
4125
4338
|
import path19 from "path";
|
|
4126
|
-
import { z as
|
|
4339
|
+
import { z as z19 } from "zod";
|
|
4127
4340
|
function relativeArtifact(root, file) {
|
|
4128
4341
|
const relative = path19.isAbsolute(file) ? path19.relative(root, file) : file;
|
|
4129
4342
|
const normalized = normalizeRepoPath(relative);
|
|
@@ -4164,7 +4377,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
4164
4377
|
let manifest, raw;
|
|
4165
4378
|
try {
|
|
4166
4379
|
manifest = relativeArtifact(root, supplied);
|
|
4167
|
-
raw =
|
|
4380
|
+
raw = z19.object({ version: z19.literal(1), checks: z19.array(z19.unknown()) }).parse(await readStoreJson(root, manifest));
|
|
4168
4381
|
} catch (error) {
|
|
4169
4382
|
output.diagnostics.push(`${supplied}: ${String(error)}`);
|
|
4170
4383
|
continue;
|
|
@@ -4317,25 +4530,25 @@ var init_evidence2 = __esm({
|
|
|
4317
4530
|
init_vitest();
|
|
4318
4531
|
init_sarif();
|
|
4319
4532
|
init_types2();
|
|
4320
|
-
checkSchema2 =
|
|
4321
|
-
id:
|
|
4322
|
-
kind:
|
|
4323
|
-
tool:
|
|
4324
|
-
command:
|
|
4325
|
-
commit:
|
|
4326
|
-
workingTreeClean:
|
|
4327
|
-
source:
|
|
4328
|
-
sourceRoot:
|
|
4329
|
-
status:
|
|
4330
|
-
reason:
|
|
4331
|
-
exitCode:
|
|
4332
|
-
report:
|
|
4533
|
+
checkSchema2 = z19.object({
|
|
4534
|
+
id: z19.string().min(1).max(100),
|
|
4535
|
+
kind: z19.enum(["tests", "static-analysis", "security", "complexity", "duplication"]),
|
|
4536
|
+
tool: z19.string().min(1).max(200),
|
|
4537
|
+
command: z19.string().min(1).max(2e3),
|
|
4538
|
+
commit: z19.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
|
|
4539
|
+
workingTreeClean: z19.boolean().optional(),
|
|
4540
|
+
source: z19.string().max(2e3).optional(),
|
|
4541
|
+
sourceRoot: z19.string().min(1).max(4e3).optional(),
|
|
4542
|
+
status: z19.enum(["completed", "skipped", "unavailable"]).default("completed"),
|
|
4543
|
+
reason: z19.string().min(1).max(2e3).optional(),
|
|
4544
|
+
exitCode: z19.number().int().nullable().optional(),
|
|
4545
|
+
report: z19.object({ format: z19.enum(["vitest-json", "sarif"]), path: z19.string().min(1).max(4e3) }).optional()
|
|
4333
4546
|
});
|
|
4334
4547
|
}
|
|
4335
4548
|
});
|
|
4336
4549
|
|
|
4337
4550
|
// src/review/review.ts
|
|
4338
|
-
import
|
|
4551
|
+
import fs17 from "fs/promises";
|
|
4339
4552
|
import path20 from "path";
|
|
4340
4553
|
import { execFile as execFile12 } from "child_process";
|
|
4341
4554
|
import { promisify as promisify12 } from "util";
|
|
@@ -4415,7 +4628,7 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
4415
4628
|
analyzed,
|
|
4416
4629
|
async (relPath) => {
|
|
4417
4630
|
try {
|
|
4418
|
-
await
|
|
4631
|
+
await fs17.access(path20.join(resolvedRoot, relPath));
|
|
4419
4632
|
return true;
|
|
4420
4633
|
} catch {
|
|
4421
4634
|
return false;
|
|
@@ -4569,7 +4782,7 @@ async function setupStatus(dir) {
|
|
|
4569
4782
|
version: 1,
|
|
4570
4783
|
status: pending.length ? "incomplete" : "not-configured",
|
|
4571
4784
|
hosts: {},
|
|
4572
|
-
next: pending.length ? "Setup did not finish. Rerun mason-auto setup --host " + pending[0] + " to resume using the retained original evidence." : "Run mason
|
|
4785
|
+
next: pending.length ? "Setup did not finish. Rerun mason-auto setup --host " + pending[0] + " to resume using the retained original evidence." : "Project hooks are inactive until local setup. Run mason setup --host codex or --host claude."
|
|
4573
4786
|
};
|
|
4574
4787
|
}
|
|
4575
4788
|
const hosts2 = {};
|