mason-context 0.19.1 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/dist/mason-audit.js +115 -88
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +939 -577
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js.map +1 -1
- package/dist/mason-runtime.js +173 -64
- package/dist/mason-runtime.js.map +1 -1
- package/dist/mason.js +1264 -758
- package/dist/mason.js.map +1 -1
- package/package.json +2 -2
- package/dist/mason-auto.js +0 -6281
- package/dist/mason-auto.js.map +0 -1
package/dist/mason.js
CHANGED
|
@@ -10,33 +10,6 @@ var __export = (target, all) => {
|
|
|
10
10
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
// src/utils/paths.ts
|
|
14
|
-
import path from "path";
|
|
15
|
-
function normalizeRepoPath(value) {
|
|
16
|
-
const slash = value.replace(/\\/g, "/");
|
|
17
|
-
if (!slash || slash.includes("\0") || path.posix.isAbsolute(slash) || /^[A-Za-z]:/.test(slash)) return null;
|
|
18
|
-
if (slash.split("/").includes("..")) return null;
|
|
19
|
-
const normalized = path.posix.normalize(slash).replace(/\/$/, "");
|
|
20
|
-
return normalized === "." ? null : normalized;
|
|
21
|
-
}
|
|
22
|
-
function isWithinRoot(root, candidate) {
|
|
23
|
-
const relative = path.relative(root, candidate);
|
|
24
|
-
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
25
|
-
}
|
|
26
|
-
function anchorMatches(anchor, file) {
|
|
27
|
-
const a = normalizeRepoPath(anchor);
|
|
28
|
-
const f = normalizeRepoPath(file);
|
|
29
|
-
return a !== null && f !== null && (a === f || f.startsWith(`${a}/`));
|
|
30
|
-
}
|
|
31
|
-
function matchingPaths(anchors, files) {
|
|
32
|
-
return [...new Set(files)].filter((file) => anchors.some((anchor) => anchorMatches(anchor, file)));
|
|
33
|
-
}
|
|
34
|
-
var init_paths = __esm({
|
|
35
|
-
"src/utils/paths.ts"() {
|
|
36
|
-
"use strict";
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
|
|
40
13
|
// src/utils/profile.ts
|
|
41
14
|
import { AsyncLocalStorage } from "async_hooks";
|
|
42
15
|
async function profilePhase(label, run) {
|
|
@@ -96,6 +69,33 @@ var init_git_read = __esm({
|
|
|
96
69
|
}
|
|
97
70
|
});
|
|
98
71
|
|
|
72
|
+
// src/utils/paths.ts
|
|
73
|
+
import path from "path";
|
|
74
|
+
function normalizeRepoPath(value) {
|
|
75
|
+
const slash = value.replace(/\\/g, "/");
|
|
76
|
+
if (!slash || slash.includes("\0") || path.posix.isAbsolute(slash) || /^[A-Za-z]:/.test(slash)) return null;
|
|
77
|
+
if (slash.split("/").includes("..")) return null;
|
|
78
|
+
const normalized = path.posix.normalize(slash).replace(/\/$/, "");
|
|
79
|
+
return normalized === "." ? null : normalized;
|
|
80
|
+
}
|
|
81
|
+
function isWithinRoot(root, candidate) {
|
|
82
|
+
const relative = path.relative(root, candidate);
|
|
83
|
+
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
84
|
+
}
|
|
85
|
+
function anchorMatches(anchor, file) {
|
|
86
|
+
const a = normalizeRepoPath(anchor);
|
|
87
|
+
const f = normalizeRepoPath(file);
|
|
88
|
+
return a !== null && f !== null && (a === f || f.startsWith(`${a}/`));
|
|
89
|
+
}
|
|
90
|
+
function matchingPaths(anchors, files) {
|
|
91
|
+
return [...new Set(files)].filter((file) => anchors.some((anchor) => anchorMatches(anchor, file)));
|
|
92
|
+
}
|
|
93
|
+
var init_paths = __esm({
|
|
94
|
+
"src/utils/paths.ts"() {
|
|
95
|
+
"use strict";
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
99
|
// src/audit/types.ts
|
|
100
100
|
var types_exports = {};
|
|
101
101
|
__export(types_exports, {
|
|
@@ -146,9 +146,9 @@ async function readBoundedFile(file, maxBytes) {
|
|
|
146
146
|
async function loadProjectConfig(root) {
|
|
147
147
|
try {
|
|
148
148
|
const canonicalRoot = await fs.realpath(root);
|
|
149
|
-
const
|
|
150
|
-
if (!isWithinRoot(canonicalRoot,
|
|
151
|
-
const raw = await readBoundedFile(
|
|
149
|
+
const configPath3 = await fs.realpath(path2.join(root, ".mason/config.json"));
|
|
150
|
+
if (!isWithinRoot(canonicalRoot, configPath3)) throw new Error("Project configuration resolves outside the repository");
|
|
151
|
+
const raw = await readBoundedFile(configPath3, 64 * 1024);
|
|
152
152
|
if (raw === null) throw new Error("Project configuration is not a regular file or exceeds 64 KiB");
|
|
153
153
|
const value = JSON.parse(raw);
|
|
154
154
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected a configuration object");
|
|
@@ -347,139 +347,6 @@ var init_storage = __esm({
|
|
|
347
347
|
}
|
|
348
348
|
});
|
|
349
349
|
|
|
350
|
-
// src/utils/store-lock.ts
|
|
351
|
-
import fs3 from "fs/promises";
|
|
352
|
-
import os from "os";
|
|
353
|
-
async function lockDiagnostic(file) {
|
|
354
|
-
let reason2;
|
|
355
|
-
try {
|
|
356
|
-
const owner = JSON.parse(await fs3.readFile(file, "utf8"));
|
|
357
|
-
if (!owner || typeof owner.host !== "string" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
|
|
358
|
-
reason2 = "Lock owner data is incomplete or malformed.";
|
|
359
|
-
} else if (owner.host !== os.hostname()) {
|
|
360
|
-
reason2 = "The lock belongs to another host; its process cannot be checked locally.";
|
|
361
|
-
} else {
|
|
362
|
-
try {
|
|
363
|
-
process.kill(owner.pid, 0);
|
|
364
|
-
reason2 = `Local owner PID ${owner.pid} is still running.`;
|
|
365
|
-
} catch (error) {
|
|
366
|
-
reason2 = error.code === "ESRCH" ? `Local owner PID ${owner.pid} has exited, but lock recovery has not completed.` : `Local owner PID ${owner.pid} could not be checked.`;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
} catch (error) {
|
|
370
|
-
reason2 = error instanceof SyntaxError ? "Lock owner data is incomplete or malformed." : "Lock owner data could not be read; the lock may have changed.";
|
|
371
|
-
}
|
|
372
|
-
if (await fs3.lstat(file + ".reclaim").then(() => true, () => false)) {
|
|
373
|
-
reason2 += " A recovery guard also needs inspection: " + file + ".reclaim.";
|
|
374
|
-
}
|
|
375
|
-
return reason2 + " Retry if a writer is active. For manual recovery, stop all Mason writers for this checkout, confirm they have exited (including other hosts), then remove only confirmed abandoned lock and lock.reclaim files. See docs/reference.md#snapshot-repairs-and-verification.";
|
|
376
|
-
}
|
|
377
|
-
async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason store") {
|
|
378
|
-
const file = await storePath(root, directory + "/lock", true);
|
|
379
|
-
const deadline = Date.now() + waitMs;
|
|
380
|
-
let handle;
|
|
381
|
-
while (!handle) {
|
|
382
|
-
try {
|
|
383
|
-
handle = await fs3.open(file, "wx", 384);
|
|
384
|
-
try {
|
|
385
|
-
await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
|
|
386
|
-
} catch (error) {
|
|
387
|
-
await handle.close().catch(() => {
|
|
388
|
-
});
|
|
389
|
-
handle = void 0;
|
|
390
|
-
await fs3.rm(file, { force: true }).catch(() => {
|
|
391
|
-
});
|
|
392
|
-
throw error;
|
|
393
|
-
}
|
|
394
|
-
} catch (error) {
|
|
395
|
-
if (error.code !== "EEXIST") throw error;
|
|
396
|
-
try {
|
|
397
|
-
const owner = JSON.parse(await fs3.readFile(file, "utf8"));
|
|
398
|
-
if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
399
|
-
try {
|
|
400
|
-
process.kill(owner.pid, 0);
|
|
401
|
-
} catch (probe) {
|
|
402
|
-
if (probe.code === "ESRCH") {
|
|
403
|
-
const reclaim = file + ".reclaim";
|
|
404
|
-
let guard;
|
|
405
|
-
try {
|
|
406
|
-
guard = await fs3.open(reclaim, "wx", 384);
|
|
407
|
-
const current = JSON.parse(await fs3.readFile(file, "utf8"));
|
|
408
|
-
if (current.pid === owner.pid && current.host === owner.host) await fs3.unlink(file);
|
|
409
|
-
} finally {
|
|
410
|
-
if (guard) {
|
|
411
|
-
await guard.close();
|
|
412
|
-
await fs3.rm(reclaim, { force: true });
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
} catch {
|
|
419
|
-
}
|
|
420
|
-
if (Date.now() >= deadline) throw new Error(label + " is busy or its lock needs inspection: " + file + ". " + await lockDiagnostic(file));
|
|
421
|
-
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
try {
|
|
425
|
-
return await run();
|
|
426
|
-
} finally {
|
|
427
|
-
await handle.close();
|
|
428
|
-
await fs3.unlink(file);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
var init_store_lock = __esm({
|
|
432
|
-
"src/utils/store-lock.ts"() {
|
|
433
|
-
"use strict";
|
|
434
|
-
init_storage();
|
|
435
|
-
}
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
// src/automation/store.ts
|
|
439
|
-
import { z } from "zod";
|
|
440
|
-
function parseState(raw) {
|
|
441
|
-
try {
|
|
442
|
-
return stateSchema.parse(raw);
|
|
443
|
-
} catch (error) {
|
|
444
|
-
throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
function withLock(root, directory, run, waitMs = 5e3) {
|
|
448
|
-
return withStoreLock(root, directory, run, waitMs, "Automation");
|
|
449
|
-
}
|
|
450
|
-
var hostSchema, events, stateSchema;
|
|
451
|
-
var init_store = __esm({
|
|
452
|
-
"src/automation/store.ts"() {
|
|
453
|
-
"use strict";
|
|
454
|
-
init_store_lock();
|
|
455
|
-
hostSchema = z.enum(["claude", "codex"]);
|
|
456
|
-
events = ["session_start", "turn_start", "before_tool", "after_tool", "task_end"];
|
|
457
|
-
stateSchema = z.object({
|
|
458
|
-
version: z.literal(1),
|
|
459
|
-
root: z.string(),
|
|
460
|
-
gitDir: z.string(),
|
|
461
|
-
branch: z.string(),
|
|
462
|
-
baselines: z.array(z.object({ path: z.string(), at: z.string(), event: z.string(), fingerprint: z.string() })).max(128),
|
|
463
|
-
sessions: z.record(z.object({
|
|
464
|
-
host: hostSchema,
|
|
465
|
-
seen: z.string().nullable(),
|
|
466
|
-
continued: z.boolean(),
|
|
467
|
-
initialIssues: z.array(z.string()),
|
|
468
|
-
initialDocs: z.record(z.string().nullable()),
|
|
469
|
-
lastUsed: z.string(),
|
|
470
|
-
mutationObserved: z.boolean(),
|
|
471
|
-
pending: z.record(z.string()),
|
|
472
|
-
coverageGaps: z.array(z.string()),
|
|
473
|
-
events: z.record(z.object({ at: z.string(), count: z.number().int().positive() }))
|
|
474
|
-
})),
|
|
475
|
-
updatedAt: z.string(),
|
|
476
|
-
fingerprint: z.string().nullable(),
|
|
477
|
-
latest: z.string().nullable(),
|
|
478
|
-
analysis: z.object({ id: z.string(), fingerprint: z.string(), completedAt: z.number(), path: z.string() }).optional()
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
});
|
|
482
|
-
|
|
483
350
|
// src/audit/inspection.ts
|
|
484
351
|
import fg2 from "fast-glob";
|
|
485
352
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
@@ -532,12 +399,12 @@ var init_inspection = __esm({
|
|
|
532
399
|
});
|
|
533
400
|
|
|
534
401
|
// src/audit/inputs.ts
|
|
535
|
-
import
|
|
402
|
+
import fs3 from "fs/promises";
|
|
536
403
|
import * as nativeFs from "fs";
|
|
537
404
|
import path5 from "path";
|
|
538
405
|
import fg3 from "fast-glob";
|
|
539
406
|
async function auditInputPath(root, file) {
|
|
540
|
-
if (file === ".") return
|
|
407
|
+
if (file === ".") return fs3.realpath(root);
|
|
541
408
|
try {
|
|
542
409
|
return await storePath(root, file);
|
|
543
410
|
} catch (error) {
|
|
@@ -560,15 +427,15 @@ async function readAuditInput(root, file) {
|
|
|
560
427
|
async function auditGlob(root, patterns, options) {
|
|
561
428
|
if (Array.isArray(patterns) && !patterns.length) return [];
|
|
562
429
|
const directories = /* @__PURE__ */ new Set();
|
|
563
|
-
const readdir2 = ((
|
|
564
|
-
const relative = path5.relative(root,
|
|
430
|
+
const readdir2 = ((directory2, opts, callback) => {
|
|
431
|
+
const relative = path5.relative(root, directory2).split(path5.sep).join("/") || ".";
|
|
565
432
|
directories.add(relative);
|
|
566
433
|
if (directories.size > MAX_INPUTS) {
|
|
567
434
|
callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
|
|
568
435
|
return;
|
|
569
436
|
}
|
|
570
437
|
auditInputPath(root, relative).then(() => {
|
|
571
|
-
nativeFs.readdir(
|
|
438
|
+
nativeFs.readdir(directory2, opts, callback);
|
|
572
439
|
}, callback);
|
|
573
440
|
});
|
|
574
441
|
const lstat2 = ((file, callback) => {
|
|
@@ -698,25 +565,25 @@ function commandClaims(line, lineNo, cwd, shell = false) {
|
|
|
698
565
|
if (text2.startsWith("#") || /^(?:echo|printf)\b/.test(text2)) continue;
|
|
699
566
|
const cd = text2.match(/^cd\s+(.+)$/);
|
|
700
567
|
if (cd) {
|
|
701
|
-
const
|
|
702
|
-
cwd =
|
|
568
|
+
const directory2 = literal(cd[1]);
|
|
569
|
+
cwd = directory2 === null || cwd === null || /\s/.test(directory2) && !/^["']/.test(cd[1]) ? null : path6.posix.join(cwd ?? ".", directory2);
|
|
703
570
|
continue;
|
|
704
571
|
}
|
|
705
572
|
for (const match of text2.matchAll(COMMAND)) {
|
|
706
573
|
const flags = match[2].trim();
|
|
707
574
|
const selector = flags.match(/^(?:--prefix|--dir|-C)(?:\s+|=)(.+)$/);
|
|
708
|
-
let
|
|
575
|
+
let directory2 = cwd;
|
|
709
576
|
let scopeUnknown;
|
|
710
577
|
if (shell && match.index !== 0 || match[3].startsWith("-")) scopeUnknown = "Unsupported shell wrapper or options before the script.";
|
|
711
578
|
if (selector) {
|
|
712
579
|
const selected = literal(selector[1]);
|
|
713
|
-
|
|
580
|
+
directory2 = selected === null || cwd === null ? null : path6.posix.join(cwd ?? ".", selected);
|
|
714
581
|
} else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
|
|
715
582
|
const tail = text2.slice(match.index + match[0].length).split(/\s--(?:\s|$)/)[0];
|
|
716
583
|
if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(tail)) {
|
|
717
584
|
scopeUnknown = "Package selection or conditional options after the script need review.";
|
|
718
585
|
}
|
|
719
|
-
if (/[|()]/.test(text2) || /\b(?:pushd|popd|if|for|while)\b/.test(text2) ||
|
|
586
|
+
if (/[|()]/.test(text2) || /\b(?:pushd|popd|if|for|while)\b/.test(text2) || directory2 === null) {
|
|
720
587
|
scopeUnknown = "The command's working directory or shell control flow could not be resolved.";
|
|
721
588
|
}
|
|
722
589
|
claims.push({
|
|
@@ -724,7 +591,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
|
|
|
724
591
|
invocation: match[0],
|
|
725
592
|
excerpt: match[0],
|
|
726
593
|
line: lineNo,
|
|
727
|
-
...
|
|
594
|
+
...directory2 !== void 0 && directory2 !== null ? { directory: directory2 } : {},
|
|
728
595
|
...scopeUnknown ? { scopeUnknown } : {}
|
|
729
596
|
});
|
|
730
597
|
}
|
|
@@ -1131,7 +998,7 @@ var init_git = __esm({
|
|
|
1131
998
|
});
|
|
1132
999
|
|
|
1133
1000
|
// src/audit/docs.ts
|
|
1134
|
-
import
|
|
1001
|
+
import fs4 from "fs/promises";
|
|
1135
1002
|
import path7 from "path";
|
|
1136
1003
|
import fg5 from "fast-glob";
|
|
1137
1004
|
async function dirtyDocs(resolvedRoot, files) {
|
|
@@ -1203,7 +1070,7 @@ async function localDocPaths(root) {
|
|
|
1203
1070
|
for (const dir of [".", ".claude"]) {
|
|
1204
1071
|
let entries;
|
|
1205
1072
|
try {
|
|
1206
|
-
entries = await
|
|
1073
|
+
entries = await fs4.readdir(await auditInputPath(root, dir), { withFileTypes: true });
|
|
1207
1074
|
} catch (error) {
|
|
1208
1075
|
if (error.code === "ENOENT") continue;
|
|
1209
1076
|
throw error;
|
|
@@ -1267,27 +1134,27 @@ var init_docs = __esm({
|
|
|
1267
1134
|
});
|
|
1268
1135
|
|
|
1269
1136
|
// src/audit/scope.ts
|
|
1270
|
-
import
|
|
1137
|
+
import fs5 from "fs/promises";
|
|
1271
1138
|
import path8 from "path";
|
|
1272
|
-
function scopedPath(
|
|
1139
|
+
function scopedPath(directory2, value) {
|
|
1273
1140
|
if (path8.posix.isAbsolute(value) || value.includes("\\") || /[\x00-\x1f]/.test(value)) return null;
|
|
1274
|
-
const resolved = path8.posix.normalize(path8.posix.join(
|
|
1141
|
+
const resolved = path8.posix.normalize(path8.posix.join(directory2, value));
|
|
1275
1142
|
return resolved === ".." || resolved.startsWith("../") ? null : resolved;
|
|
1276
1143
|
}
|
|
1277
1144
|
function pathClaimScope(doc, claim) {
|
|
1278
|
-
const
|
|
1279
|
-
const local = scopedPath(
|
|
1145
|
+
const directory2 = claim.relativeTo === "document" ? path8.posix.dirname(doc) : documentScope(doc);
|
|
1146
|
+
const local = scopedPath(directory2, claim.path);
|
|
1280
1147
|
if (!local) return null;
|
|
1281
|
-
const candidates = claim.relativeTo === "document" ||
|
|
1148
|
+
const candidates = claim.relativeTo === "document" || directory2 === "." ? [local] : [...new Set([local, scopedPath(".", claim.path)].filter((p) => p !== null))];
|
|
1282
1149
|
return {
|
|
1283
|
-
basis: claim.relativeTo === "document" ? "document-link" :
|
|
1284
|
-
directory,
|
|
1150
|
+
basis: claim.relativeTo === "document" ? "document-link" : directory2 === "." ? "repository" : "document",
|
|
1151
|
+
directory: directory2,
|
|
1285
1152
|
candidates
|
|
1286
1153
|
};
|
|
1287
1154
|
}
|
|
1288
1155
|
async function pathExists(root, file) {
|
|
1289
1156
|
try {
|
|
1290
|
-
await
|
|
1157
|
+
await fs5.access(await auditInputPath(root, file));
|
|
1291
1158
|
return true;
|
|
1292
1159
|
} catch (error) {
|
|
1293
1160
|
if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return false;
|
|
@@ -1308,6 +1175,100 @@ var init_scope = __esm({
|
|
|
1308
1175
|
}
|
|
1309
1176
|
});
|
|
1310
1177
|
|
|
1178
|
+
// src/utils/store-lock.ts
|
|
1179
|
+
import fs6 from "fs/promises";
|
|
1180
|
+
import os from "os";
|
|
1181
|
+
async function lockDiagnostic(file) {
|
|
1182
|
+
let reason2;
|
|
1183
|
+
try {
|
|
1184
|
+
const owner = JSON.parse(await fs6.readFile(file, "utf8"));
|
|
1185
|
+
if (!owner || typeof owner.host !== "string" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
|
|
1186
|
+
reason2 = "Lock owner data is incomplete or malformed.";
|
|
1187
|
+
} else if (owner.host !== os.hostname()) {
|
|
1188
|
+
reason2 = "The lock belongs to another host; its process cannot be checked locally.";
|
|
1189
|
+
} else {
|
|
1190
|
+
try {
|
|
1191
|
+
process.kill(owner.pid, 0);
|
|
1192
|
+
reason2 = `Local owner PID ${owner.pid} is still running.`;
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
reason2 = error.code === "ESRCH" ? `Local owner PID ${owner.pid} has exited, but lock recovery has not completed.` : `Local owner PID ${owner.pid} could not be checked.`;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
reason2 = error instanceof SyntaxError ? "Lock owner data is incomplete or malformed." : "Lock owner data could not be read; the lock may have changed.";
|
|
1199
|
+
}
|
|
1200
|
+
if (await fs6.lstat(file + ".reclaim").then(() => true, () => false)) {
|
|
1201
|
+
reason2 += " A recovery guard also needs inspection: " + file + ".reclaim.";
|
|
1202
|
+
}
|
|
1203
|
+
return reason2 + " Retry if a writer is active. For manual recovery, stop all Mason writers for this checkout, confirm they have exited (including other hosts), then remove only confirmed abandoned lock and lock.reclaim files. See docs/reference.md#snapshot-repairs-and-verification.";
|
|
1204
|
+
}
|
|
1205
|
+
async function withStoreLock(root, directory2, run, waitMs = 5e3, label = "Mason store") {
|
|
1206
|
+
const file = await storePath(root, directory2 + "/lock", true);
|
|
1207
|
+
const deadline = Date.now() + waitMs;
|
|
1208
|
+
let handle;
|
|
1209
|
+
while (!handle) {
|
|
1210
|
+
try {
|
|
1211
|
+
handle = await fs6.open(file, "wx", 384);
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
if (process.platform === "win32" && error.code === "EPERM") {
|
|
1214
|
+
if (Date.now() >= deadline) throw error;
|
|
1215
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
if (error.code !== "EEXIST") throw error;
|
|
1219
|
+
try {
|
|
1220
|
+
const owner = JSON.parse(await fs6.readFile(file, "utf8"));
|
|
1221
|
+
if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
1222
|
+
try {
|
|
1223
|
+
process.kill(owner.pid, 0);
|
|
1224
|
+
} catch (probe) {
|
|
1225
|
+
if (probe.code === "ESRCH") {
|
|
1226
|
+
const reclaim = file + ".reclaim";
|
|
1227
|
+
let guard;
|
|
1228
|
+
try {
|
|
1229
|
+
guard = await fs6.open(reclaim, "wx", 384);
|
|
1230
|
+
const current = JSON.parse(await fs6.readFile(file, "utf8"));
|
|
1231
|
+
if (current.pid === owner.pid && current.host === owner.host) await fs6.unlink(file);
|
|
1232
|
+
} finally {
|
|
1233
|
+
if (guard) {
|
|
1234
|
+
await guard.close();
|
|
1235
|
+
await fs6.rm(reclaim, { force: true });
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
} catch {
|
|
1242
|
+
}
|
|
1243
|
+
if (Date.now() >= deadline) throw new Error(label + " is busy or its lock needs inspection: " + file + ". " + await lockDiagnostic(file));
|
|
1244
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
1245
|
+
}
|
|
1246
|
+
if (handle) {
|
|
1247
|
+
try {
|
|
1248
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
|
|
1249
|
+
} catch (error) {
|
|
1250
|
+
await handle.close().catch(() => {
|
|
1251
|
+
});
|
|
1252
|
+
await fs6.rm(file, { force: true }).catch(() => {
|
|
1253
|
+
});
|
|
1254
|
+
throw error;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
try {
|
|
1259
|
+
return await run();
|
|
1260
|
+
} finally {
|
|
1261
|
+
await handle.close();
|
|
1262
|
+
await fs6.unlink(file);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
var init_store_lock = __esm({
|
|
1266
|
+
"src/utils/store-lock.ts"() {
|
|
1267
|
+
"use strict";
|
|
1268
|
+
init_storage();
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1311
1272
|
// src/snapshot/lock.ts
|
|
1312
1273
|
var init_lock = __esm({
|
|
1313
1274
|
"src/snapshot/lock.ts"() {
|
|
@@ -1394,7 +1355,7 @@ var init_test_map = __esm({
|
|
|
1394
1355
|
|
|
1395
1356
|
// src/snapshot/snapshot.ts
|
|
1396
1357
|
import path10 from "path";
|
|
1397
|
-
import { z
|
|
1358
|
+
import { z } from "zod";
|
|
1398
1359
|
async function inspectSnapshot(rootDir) {
|
|
1399
1360
|
try {
|
|
1400
1361
|
const raw = await readStoreJson(rootDir, ".mason/snapshot.json");
|
|
@@ -1428,30 +1389,30 @@ var init_snapshot = __esm({
|
|
|
1428
1389
|
init_lock();
|
|
1429
1390
|
init_paths();
|
|
1430
1391
|
init_test_map();
|
|
1431
|
-
repoPath =
|
|
1392
|
+
repoPath = z.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
|
|
1432
1393
|
verificationFields = {
|
|
1433
|
-
refreshedHash:
|
|
1434
|
-
verifiedAt:
|
|
1435
|
-
verifiedHash:
|
|
1436
|
-
verificationToken:
|
|
1437
|
-
verificationFailed:
|
|
1438
|
-
verificationNote:
|
|
1394
|
+
refreshedHash: z.string().optional(),
|
|
1395
|
+
verifiedAt: z.string().optional(),
|
|
1396
|
+
verifiedHash: z.string().optional(),
|
|
1397
|
+
verificationToken: z.string().optional(),
|
|
1398
|
+
verificationFailed: z.boolean().optional(),
|
|
1399
|
+
verificationNote: z.string().optional()
|
|
1439
1400
|
};
|
|
1440
|
-
featureSchema =
|
|
1441
|
-
description:
|
|
1442
|
-
files:
|
|
1443
|
-
tests:
|
|
1444
|
-
type:
|
|
1401
|
+
featureSchema = z.object({
|
|
1402
|
+
description: z.string(),
|
|
1403
|
+
files: z.array(repoPath),
|
|
1404
|
+
tests: z.array(repoPath).optional(),
|
|
1405
|
+
type: z.enum(["capability", "infrastructure"]).optional(),
|
|
1445
1406
|
...verificationFields
|
|
1446
1407
|
}).passthrough();
|
|
1447
|
-
flowSchema =
|
|
1448
|
-
snapshotSchema =
|
|
1449
|
-
version:
|
|
1450
|
-
createdAt:
|
|
1451
|
-
updatedAt:
|
|
1452
|
-
gitHash:
|
|
1453
|
-
features:
|
|
1454
|
-
flows:
|
|
1408
|
+
flowSchema = z.object({ description: z.string(), chain: z.array(repoPath), ...verificationFields }).passthrough();
|
|
1409
|
+
snapshotSchema = z.object({
|
|
1410
|
+
version: z.literal(2),
|
|
1411
|
+
createdAt: z.string(),
|
|
1412
|
+
updatedAt: z.string(),
|
|
1413
|
+
gitHash: z.string(),
|
|
1414
|
+
features: z.record(featureSchema),
|
|
1415
|
+
flows: z.record(flowSchema)
|
|
1455
1416
|
}).passthrough();
|
|
1456
1417
|
}
|
|
1457
1418
|
});
|
|
@@ -1897,19 +1858,19 @@ async function commandInputs(root, docs) {
|
|
|
1897
1858
|
const resolutions = [];
|
|
1898
1859
|
for (const doc of docs) for (const claim of doc.claims.commands) {
|
|
1899
1860
|
const base = documentScope(doc.path);
|
|
1900
|
-
const
|
|
1861
|
+
const directory2 = scopedPath(base, claim.directory ?? ".");
|
|
1901
1862
|
const scope = {
|
|
1902
1863
|
basis: claim.directory === void 0 ? "document" : "explicit",
|
|
1903
|
-
directory:
|
|
1864
|
+
directory: directory2 ?? base,
|
|
1904
1865
|
candidates: []
|
|
1905
1866
|
};
|
|
1906
1867
|
const resolution = { doc: doc.path, claim, scope, manifest: null, scripts: [], elsewhere: [] };
|
|
1907
1868
|
resolutions.push(resolution);
|
|
1908
|
-
if (claim.scopeUnknown ||
|
|
1869
|
+
if (claim.scopeUnknown || directory2 === null) {
|
|
1909
1870
|
resolution.problem = claim.scopeUnknown ?? "The command's directory leaves the repository.";
|
|
1910
1871
|
continue;
|
|
1911
1872
|
}
|
|
1912
|
-
let cursor =
|
|
1873
|
+
let cursor = directory2;
|
|
1913
1874
|
while (true) {
|
|
1914
1875
|
const manifest = path15.posix.join(cursor, "package.json");
|
|
1915
1876
|
scope.candidates.push(manifest);
|
|
@@ -1925,7 +1886,7 @@ async function commandInputs(root, docs) {
|
|
|
1925
1886
|
cursor = path15.posix.dirname(cursor);
|
|
1926
1887
|
}
|
|
1927
1888
|
if (!resolution.manifest && !resolution.problem) {
|
|
1928
|
-
resolution.problem = `No package.json for the command's scope (${
|
|
1889
|
+
resolution.problem = `No package.json for the command's scope (${directory2}).`;
|
|
1929
1890
|
}
|
|
1930
1891
|
if (resolution.problem || resolution.scripts.includes(claim.scriptName) || claim.directory !== void 0) continue;
|
|
1931
1892
|
inventory ??= await commandManifests(root);
|
|
@@ -2263,7 +2224,7 @@ var init_trust = __esm({
|
|
|
2263
2224
|
});
|
|
2264
2225
|
|
|
2265
2226
|
// src/decisions/provenance.ts
|
|
2266
|
-
import { z as
|
|
2227
|
+
import { z as z2 } from "zod";
|
|
2267
2228
|
function decisionContent(record) {
|
|
2268
2229
|
return {
|
|
2269
2230
|
title: record.title,
|
|
@@ -2327,67 +2288,67 @@ var init_provenance = __esm({
|
|
|
2327
2288
|
"use strict";
|
|
2328
2289
|
init_paths();
|
|
2329
2290
|
init_trust();
|
|
2330
|
-
text = (max) =>
|
|
2331
|
-
decisionSourceSchema =
|
|
2332
|
-
kind:
|
|
2291
|
+
text = (max) => z2.string().trim().min(1).max(max);
|
|
2292
|
+
decisionSourceSchema = z2.object({
|
|
2293
|
+
kind: z2.enum(["pull_request", "issue", "incident", "discussion", "document", "other"]),
|
|
2333
2294
|
reference: text(1e3),
|
|
2334
2295
|
note: text(500).optional()
|
|
2335
2296
|
}).strict();
|
|
2336
|
-
attributionSchema =
|
|
2297
|
+
attributionSchema = z2.object({
|
|
2337
2298
|
owner: text(200).nullable().optional(),
|
|
2338
|
-
sources:
|
|
2299
|
+
sources: z2.array(decisionSourceSchema).max(20).optional(),
|
|
2339
2300
|
actor: text(200).optional()
|
|
2340
2301
|
});
|
|
2341
|
-
contentSchema =
|
|
2342
|
-
title:
|
|
2343
|
-
body:
|
|
2344
|
-
category:
|
|
2345
|
-
files:
|
|
2302
|
+
contentSchema = z2.object({
|
|
2303
|
+
title: z2.string().min(1),
|
|
2304
|
+
body: z2.string().min(1),
|
|
2305
|
+
category: z2.enum(["decision", "gotcha", "deprecation", "convention"]),
|
|
2306
|
+
files: z2.array(z2.string().refine((f) => normalizeRepoPath(f) !== null)),
|
|
2346
2307
|
owner: text(200).optional(),
|
|
2347
|
-
sources:
|
|
2308
|
+
sources: z2.array(decisionSourceSchema).max(20)
|
|
2348
2309
|
});
|
|
2349
|
-
approvalSchema =
|
|
2350
|
-
statusSchema =
|
|
2351
|
-
reviewEvidenceSchema =
|
|
2352
|
-
baseHash:
|
|
2353
|
-
headHash:
|
|
2354
|
-
historyAvailable:
|
|
2355
|
-
changedFiles:
|
|
2356
|
-
localChanges:
|
|
2310
|
+
approvalSchema = z2.enum(["unreviewed", "proposed", "accepted"]);
|
|
2311
|
+
statusSchema = z2.enum(["active", "superseded", "retired"]);
|
|
2312
|
+
reviewEvidenceSchema = z2.object({
|
|
2313
|
+
baseHash: z2.string(),
|
|
2314
|
+
headHash: z2.string(),
|
|
2315
|
+
historyAvailable: z2.boolean(),
|
|
2316
|
+
changedFiles: z2.array(z2.string()),
|
|
2317
|
+
localChanges: z2.array(z2.string())
|
|
2357
2318
|
});
|
|
2358
|
-
eventSchema =
|
|
2359
|
-
kind:
|
|
2360
|
-
at:
|
|
2319
|
+
eventSchema = z2.object({
|
|
2320
|
+
kind: z2.enum(["imported", "created", "revised", "accepted", "reaffirmed", "retired", "superseded"]),
|
|
2321
|
+
at: z2.string().datetime(),
|
|
2361
2322
|
actor: text(200).optional(),
|
|
2362
2323
|
note: text(1500).optional(),
|
|
2363
|
-
revision:
|
|
2324
|
+
revision: z2.number().int().positive(),
|
|
2364
2325
|
content: contentSchema,
|
|
2365
2326
|
approval: approvalSchema,
|
|
2366
2327
|
status: statusSchema,
|
|
2367
|
-
refreshedHash:
|
|
2328
|
+
refreshedHash: z2.string(),
|
|
2368
2329
|
evidence: reviewEvidenceSchema.optional()
|
|
2369
2330
|
});
|
|
2370
|
-
legacySchema =
|
|
2371
|
-
version:
|
|
2372
|
-
id:
|
|
2373
|
-
title:
|
|
2374
|
-
body:
|
|
2331
|
+
legacySchema = z2.object({
|
|
2332
|
+
version: z2.literal(1),
|
|
2333
|
+
id: z2.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
2334
|
+
title: z2.string().min(1),
|
|
2335
|
+
body: z2.string().min(1),
|
|
2375
2336
|
category: contentSchema.shape.category,
|
|
2376
2337
|
files: contentSchema.shape.files,
|
|
2377
|
-
createdAt:
|
|
2378
|
-
updatedAt:
|
|
2379
|
-
refreshedHash:
|
|
2380
|
-
status:
|
|
2381
|
-
supersededBy:
|
|
2338
|
+
createdAt: z2.string(),
|
|
2339
|
+
updatedAt: z2.string(),
|
|
2340
|
+
refreshedHash: z2.string(),
|
|
2341
|
+
status: z2.enum(["active", "superseded"]),
|
|
2342
|
+
supersededBy: z2.string().optional()
|
|
2382
2343
|
}).passthrough();
|
|
2383
2344
|
currentSchema = legacySchema.extend({
|
|
2384
|
-
version:
|
|
2345
|
+
version: z2.literal(2),
|
|
2385
2346
|
status: statusSchema,
|
|
2386
2347
|
approval: approvalSchema,
|
|
2387
|
-
revision:
|
|
2348
|
+
revision: z2.number().int().positive(),
|
|
2388
2349
|
owner: text(200).optional(),
|
|
2389
|
-
sources:
|
|
2390
|
-
history:
|
|
2350
|
+
sources: z2.array(decisionSourceSchema).max(20),
|
|
2351
|
+
history: z2.array(eventSchema).min(1)
|
|
2391
2352
|
}).superRefine((record, ctx) => {
|
|
2392
2353
|
const invalid = (message2) => ctx.addIssue({ code: "custom", message: message2 });
|
|
2393
2354
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
@@ -2417,7 +2378,7 @@ var init_provenance = __esm({
|
|
|
2417
2378
|
}
|
|
2418
2379
|
if (!previous || !same(previous.content, decisionContent(record)) || previous.approval !== record.approval || previous.status !== record.status || previous.revision !== record.revision || previous.refreshedHash !== record.refreshedHash) invalid("Decision does not match the final history event");
|
|
2419
2380
|
});
|
|
2420
|
-
decisionSchema =
|
|
2381
|
+
decisionSchema = z2.union([legacySchema, currentSchema]);
|
|
2421
2382
|
}
|
|
2422
2383
|
});
|
|
2423
2384
|
|
|
@@ -2425,7 +2386,7 @@ var init_provenance = __esm({
|
|
|
2425
2386
|
import fs9 from "fs/promises";
|
|
2426
2387
|
import path18 from "path";
|
|
2427
2388
|
import { createHash } from "crypto";
|
|
2428
|
-
import { z as
|
|
2389
|
+
import { z as z3 } from "zod";
|
|
2429
2390
|
async function loadDecisionStore(rootDir) {
|
|
2430
2391
|
const records = [];
|
|
2431
2392
|
const diagnostics = [];
|
|
@@ -2461,7 +2422,7 @@ var init_decisions = __esm({
|
|
|
2461
2422
|
TITLE_MAX_CHARS = 80;
|
|
2462
2423
|
BODY_RECOMMENDED_CHARS = 1500;
|
|
2463
2424
|
BODY_MAX_CHARS = 2500;
|
|
2464
|
-
boundedText = (field, max, hint) =>
|
|
2425
|
+
boundedText = (field, max, hint) => z3.string({
|
|
2465
2426
|
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 })
|
|
2466
2427
|
}).trim().min(1, `${field} must be non-empty`).max(max);
|
|
2467
2428
|
decisionTitleSchema = boundedText("title", TITLE_MAX_CHARS, "Tighten it to a specific headline.");
|
|
@@ -2594,7 +2555,7 @@ var init_checks = __esm({
|
|
|
2594
2555
|
|
|
2595
2556
|
// src/audit/findings.ts
|
|
2596
2557
|
import { createHash as createHash2 } from "crypto";
|
|
2597
|
-
import { z as
|
|
2558
|
+
import { z as z4 } from "zod";
|
|
2598
2559
|
function findingId(finding) {
|
|
2599
2560
|
const e = finding.evidence;
|
|
2600
2561
|
let key;
|
|
@@ -2625,84 +2586,241 @@ var init_findings = __esm({
|
|
|
2625
2586
|
"src/audit/findings.ts"() {
|
|
2626
2587
|
"use strict";
|
|
2627
2588
|
digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
|
|
2628
|
-
checkSchema =
|
|
2629
|
-
commitSchema =
|
|
2630
|
-
anchorSchema =
|
|
2631
|
-
count =
|
|
2632
|
-
scopeSchema =
|
|
2633
|
-
basis:
|
|
2634
|
-
directory:
|
|
2635
|
-
candidates:
|
|
2589
|
+
checkSchema = z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
|
|
2590
|
+
commitSchema = z4.object({ hash: z4.string().regex(/^[a-f0-9]{40,64}$/), date: z4.string(), subject: z4.string() });
|
|
2591
|
+
anchorSchema = z4.object({ doc: z4.string(), line: z4.number().int().positive().nullable(), excerpt: z4.string().nullable() });
|
|
2592
|
+
count = z4.number().int().nonnegative();
|
|
2593
|
+
scopeSchema = z4.object({
|
|
2594
|
+
basis: z4.enum(["repository", "document", "document-link", "explicit"]),
|
|
2595
|
+
directory: z4.string(),
|
|
2596
|
+
candidates: z4.array(z4.string())
|
|
2636
2597
|
});
|
|
2637
|
-
evidenceSchema =
|
|
2638
|
-
|
|
2639
|
-
kind:
|
|
2640
|
-
claimed:
|
|
2641
|
-
renamedTo:
|
|
2598
|
+
evidenceSchema = z4.discriminatedUnion("kind", [
|
|
2599
|
+
z4.object({
|
|
2600
|
+
kind: z4.literal("missing-path"),
|
|
2601
|
+
claimed: z4.string(),
|
|
2602
|
+
renamedTo: z4.string().nullable(),
|
|
2642
2603
|
deletedInCommit: commitSchema.nullable(),
|
|
2643
|
-
everTracked:
|
|
2644
|
-
parentDirExists:
|
|
2645
|
-
resolvedPath:
|
|
2604
|
+
everTracked: z4.boolean(),
|
|
2605
|
+
parentDirExists: z4.boolean(),
|
|
2606
|
+
resolvedPath: z4.string().optional(),
|
|
2646
2607
|
scope: scopeSchema.optional()
|
|
2647
2608
|
}),
|
|
2648
|
-
|
|
2649
|
-
kind:
|
|
2650
|
-
dir:
|
|
2609
|
+
z4.object({
|
|
2610
|
+
kind: z4.literal("unmentioned-dir"),
|
|
2611
|
+
dir: z4.string(),
|
|
2651
2612
|
sourceFileCount: count,
|
|
2652
2613
|
firstCommit: commitSchema.nullable(),
|
|
2653
|
-
checkedDocs:
|
|
2614
|
+
checkedDocs: z4.array(z4.string())
|
|
2654
2615
|
}),
|
|
2655
|
-
|
|
2656
|
-
kind:
|
|
2616
|
+
z4.object({
|
|
2617
|
+
kind: z4.literal("count-mismatch"),
|
|
2657
2618
|
claimed: count,
|
|
2658
2619
|
actual: count,
|
|
2659
|
-
unit:
|
|
2660
|
-
countedFrom:
|
|
2661
|
-
members:
|
|
2620
|
+
unit: z4.string(),
|
|
2621
|
+
countedFrom: z4.string(),
|
|
2622
|
+
members: z4.array(z4.string())
|
|
2662
2623
|
}),
|
|
2663
|
-
|
|
2664
|
-
kind:
|
|
2665
|
-
scriptName:
|
|
2666
|
-
invocation:
|
|
2667
|
-
manifestsChecked:
|
|
2668
|
-
availableScripts:
|
|
2624
|
+
z4.object({
|
|
2625
|
+
kind: z4.literal("missing-script"),
|
|
2626
|
+
scriptName: z4.string(),
|
|
2627
|
+
invocation: z4.string(),
|
|
2628
|
+
manifestsChecked: z4.array(z4.string()),
|
|
2629
|
+
availableScripts: z4.array(z4.string()),
|
|
2669
2630
|
scope: scopeSchema.optional()
|
|
2670
2631
|
}),
|
|
2671
|
-
|
|
2672
|
-
kind:
|
|
2632
|
+
z4.object({
|
|
2633
|
+
kind: z4.literal("doc-behind-manifests"),
|
|
2673
2634
|
docLastCommit: commitSchema,
|
|
2674
|
-
manifestCommits:
|
|
2635
|
+
manifestCommits: z4.array(commitSchema.extend({ files: z4.array(z4.string()) })),
|
|
2675
2636
|
totalCommits: count,
|
|
2676
|
-
matches:
|
|
2677
|
-
matchingIncomplete:
|
|
2637
|
+
matches: z4.array(z4.object({ dependency: z4.string(), manifest: z4.string(), before: z4.string().nullable(), after: z4.string().nullable(), line: z4.number().int().positive(), excerpt: z4.string() })).optional(),
|
|
2638
|
+
matchingIncomplete: z4.boolean().optional()
|
|
2678
2639
|
}),
|
|
2679
|
-
|
|
2680
|
-
kind:
|
|
2681
|
-
decisionId:
|
|
2682
|
-
title:
|
|
2683
|
-
changedFiles:
|
|
2684
|
-
refreshedHash:
|
|
2685
|
-
provenance:
|
|
2640
|
+
z4.object({
|
|
2641
|
+
kind: z4.literal("decision-anchor"),
|
|
2642
|
+
decisionId: z4.string(),
|
|
2643
|
+
title: z4.string(),
|
|
2644
|
+
changedFiles: z4.array(z4.string()),
|
|
2645
|
+
refreshedHash: z4.string(),
|
|
2646
|
+
provenance: z4.object({}).passthrough().optional()
|
|
2686
2647
|
})
|
|
2687
2648
|
]);
|
|
2688
|
-
findingSchema =
|
|
2649
|
+
findingSchema = z4.object({ message: z4.string(), anchor: anchorSchema, evidence: evidenceSchema });
|
|
2689
2650
|
issueSchema = findingSchema.extend({
|
|
2690
|
-
type:
|
|
2691
|
-
confidence:
|
|
2651
|
+
type: z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
|
|
2652
|
+
confidence: z4.enum(["certain", "likely"])
|
|
2692
2653
|
});
|
|
2693
|
-
advisorySchema = findingSchema.extend({ type: checkSchema, resolution:
|
|
2694
|
-
checkResultSchema =
|
|
2695
|
-
issues:
|
|
2696
|
-
advisories:
|
|
2697
|
-
suppressedAdvisories:
|
|
2698
|
-
skipped:
|
|
2654
|
+
advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z4.enum(["recheck", "informational"]).optional() });
|
|
2655
|
+
checkResultSchema = z4.object({
|
|
2656
|
+
issues: z4.array(issueSchema),
|
|
2657
|
+
advisories: z4.array(advisorySchema),
|
|
2658
|
+
suppressedAdvisories: z4.array(advisorySchema).optional(),
|
|
2659
|
+
skipped: z4.array(z4.object({ check: z4.string(), reason: z4.string(), doc: z4.string().optional() }))
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
});
|
|
2663
|
+
|
|
2664
|
+
// src/automation/notifications.ts
|
|
2665
|
+
import { z as z5 } from "zod";
|
|
2666
|
+
function condition(finding) {
|
|
2667
|
+
const source = finding.current ?? finding.original;
|
|
2668
|
+
const e = source.evidence;
|
|
2669
|
+
const id = e.kind === "decision-anchor" ? digest([source.type, source.anchor.doc, e.decisionId]) : finding.id;
|
|
2670
|
+
const result = { level: "confidence" in source ? 2 : 1, facts: [] };
|
|
2671
|
+
switch (e.kind) {
|
|
2672
|
+
case "count-mismatch":
|
|
2673
|
+
result.magnitude = Math.abs(e.actual - e.claimed);
|
|
2674
|
+
break;
|
|
2675
|
+
case "unmentioned-dir":
|
|
2676
|
+
result.magnitude = e.sourceFileCount;
|
|
2677
|
+
break;
|
|
2678
|
+
case "decision-anchor":
|
|
2679
|
+
result.facts = [...e.changedFiles].sort();
|
|
2680
|
+
break;
|
|
2681
|
+
case "doc-behind-manifests":
|
|
2682
|
+
result.facts = (e.matches ?? []).map((m) => JSON.stringify([m.manifest, m.dependency, m.after])).sort();
|
|
2683
|
+
break;
|
|
2684
|
+
}
|
|
2685
|
+
return { id, condition: result };
|
|
2686
|
+
}
|
|
2687
|
+
function notificationConditions(findings) {
|
|
2688
|
+
const active = findings.filter((f) => f.status !== "resolved");
|
|
2689
|
+
const currentIds = new Set(active.filter((f) => f.current).map((f) => condition(f).id));
|
|
2690
|
+
const result = {};
|
|
2691
|
+
for (const f of active) {
|
|
2692
|
+
const value = condition(f);
|
|
2693
|
+
if (!f.current && currentIds.has(value.id)) continue;
|
|
2694
|
+
const previous = result[value.id];
|
|
2695
|
+
result[value.id] = previous ? {
|
|
2696
|
+
level: Math.max(previous.level, value.condition.level),
|
|
2697
|
+
facts: [.../* @__PURE__ */ new Set([...previous.facts, ...value.condition.facts])].sort(),
|
|
2698
|
+
...previous.magnitude !== void 0 || value.condition.magnitude !== void 0 ? { magnitude: Math.max(previous.magnitude ?? 0, value.condition.magnitude ?? 0) } : {}
|
|
2699
|
+
} : value.condition;
|
|
2700
|
+
}
|
|
2701
|
+
return result;
|
|
2702
|
+
}
|
|
2703
|
+
function createNotificationState(findings, head) {
|
|
2704
|
+
return { version: 1, baselineHead: head, baseline: notificationConditions(findings), delivered: {}, cleared: [] };
|
|
2705
|
+
}
|
|
2706
|
+
function worsened(current, previous) {
|
|
2707
|
+
return !previous || current.level > previous.level || (current.magnitude ?? 0) > (previous.magnitude ?? 0) || current.facts.some((fact) => !previous.facts.includes(fact));
|
|
2708
|
+
}
|
|
2709
|
+
function completionFindings(state, findings) {
|
|
2710
|
+
const current = notificationConditions(findings);
|
|
2711
|
+
const cleared = new Set(state.cleared);
|
|
2712
|
+
const resolvedIds = new Set(findings.filter((f) => f.status === "resolved").map((f) => condition(f).id));
|
|
2713
|
+
for (const id of Object.keys(state.baseline)) if (resolvedIds.has(id) && !current[id]) cleared.add(id);
|
|
2714
|
+
state.cleared = [...cleared];
|
|
2715
|
+
const selected = [];
|
|
2716
|
+
const selectedIds = /* @__PURE__ */ new Set();
|
|
2717
|
+
const delivered = { ...state.delivered };
|
|
2718
|
+
for (const finding of findings) {
|
|
2719
|
+
const value = condition(finding);
|
|
2720
|
+
const previous = state.delivered[value.id];
|
|
2721
|
+
value.condition = current[value.id] ?? value.condition;
|
|
2722
|
+
if (finding.status === "unverified" || !finding.current && finding.status !== "resolved") {
|
|
2723
|
+
continue;
|
|
2724
|
+
}
|
|
2725
|
+
if (finding.status === "resolved") {
|
|
2726
|
+
if (!current[value.id]) delete delivered[value.id];
|
|
2727
|
+
continue;
|
|
2728
|
+
}
|
|
2729
|
+
const eligible = cleared.has(value.id) || worsened(value.condition, state.baseline[value.id]);
|
|
2730
|
+
if (!eligible) continue;
|
|
2731
|
+
if (!selectedIds.has(value.id) && worsened(value.condition, previous)) {
|
|
2732
|
+
selectedIds.add(value.id);
|
|
2733
|
+
const source = finding.current;
|
|
2734
|
+
selected.push(source.evidence.kind === "decision-anchor" ? { ...finding, current: { ...source, evidence: { ...source.evidence, changedFiles: value.condition.facts } } } : finding);
|
|
2735
|
+
if (selected.length <= MAX_COMPLETION_FINDINGS) delivered[value.id] = value.condition;
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
state.delivered = delivered;
|
|
2739
|
+
return selected;
|
|
2740
|
+
}
|
|
2741
|
+
function completionSummary(findings, reportPath) {
|
|
2742
|
+
if (!findings.length) return null;
|
|
2743
|
+
const safe = (value) => value.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 250);
|
|
2744
|
+
return [
|
|
2745
|
+
`Mason: ${findings.length} new or worsened finding(s) since the retained baseline.`,
|
|
2746
|
+
...findings.slice(0, MAX_COMPLETION_FINDINGS).map((f) => {
|
|
2747
|
+
const source = f.current ?? f.original;
|
|
2748
|
+
const detail = source.evidence.kind === "decision-anchor" ? `Changes to ${source.evidence.changedFiles.join(", ")} may affect decision "${source.evidence.title}". Inspect the diff if relevant; acceptance is not required.` : source.message;
|
|
2749
|
+
return `[${f.status === "unresolved" ? "issue" : "advisory"}] ${safe(source.anchor.doc)}: ${safe(detail)}`;
|
|
2750
|
+
}),
|
|
2751
|
+
...findings.length > MAX_COMPLETION_FINDINGS ? [`${findings.length - MAX_COMPLETION_FINDINGS} more in the report; pending later feedback.`] : [],
|
|
2752
|
+
`Evidence: ${reportPath}. Explicit audit: mason_automation(action: "check").`,
|
|
2753
|
+
"Inspect findings within the authorized work. Concurrent changes may also contribute; this summary does not establish who introduced them."
|
|
2754
|
+
].join("\n");
|
|
2755
|
+
}
|
|
2756
|
+
var conditionSchema, MAX_COMPLETION_FINDINGS, notificationStateSchema;
|
|
2757
|
+
var init_notifications = __esm({
|
|
2758
|
+
"src/automation/notifications.ts"() {
|
|
2759
|
+
"use strict";
|
|
2760
|
+
init_findings();
|
|
2761
|
+
conditionSchema = z5.object({
|
|
2762
|
+
level: z5.number(),
|
|
2763
|
+
facts: z5.array(z5.string()),
|
|
2764
|
+
magnitude: z5.number().optional()
|
|
2765
|
+
});
|
|
2766
|
+
MAX_COMPLETION_FINDINGS = 4;
|
|
2767
|
+
notificationStateSchema = z5.object({
|
|
2768
|
+
version: z5.literal(1),
|
|
2769
|
+
baselineHead: z5.string(),
|
|
2770
|
+
baseline: z5.record(conditionSchema),
|
|
2771
|
+
delivered: z5.record(conditionSchema),
|
|
2772
|
+
cleared: z5.array(z5.string())
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2775
|
+
});
|
|
2776
|
+
|
|
2777
|
+
// src/automation/store.ts
|
|
2778
|
+
import { z as z6 } from "zod";
|
|
2779
|
+
function parseState(raw) {
|
|
2780
|
+
try {
|
|
2781
|
+
return stateSchema.parse(raw);
|
|
2782
|
+
} catch (error) {
|
|
2783
|
+
throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
function withLock(root, directory2, run, waitMs = 5e3) {
|
|
2787
|
+
return withStoreLock(root, directory2, run, waitMs, "Automation");
|
|
2788
|
+
}
|
|
2789
|
+
var hostSchema, events, stateSchema;
|
|
2790
|
+
var init_store = __esm({
|
|
2791
|
+
"src/automation/store.ts"() {
|
|
2792
|
+
"use strict";
|
|
2793
|
+
init_store_lock();
|
|
2794
|
+
init_notifications();
|
|
2795
|
+
hostSchema = z6.enum(["claude", "codex"]);
|
|
2796
|
+
events = ["session_start", "turn_start", "before_tool", "after_tool", "task_end"];
|
|
2797
|
+
stateSchema = z6.object({
|
|
2798
|
+
version: z6.literal(1),
|
|
2799
|
+
root: z6.string(),
|
|
2800
|
+
gitDir: z6.string(),
|
|
2801
|
+
branch: z6.string(),
|
|
2802
|
+
baselines: z6.array(z6.object({ path: z6.string(), at: z6.string(), event: z6.string(), fingerprint: z6.string() })).max(128),
|
|
2803
|
+
sessions: z6.record(z6.object({
|
|
2804
|
+
host: hostSchema,
|
|
2805
|
+
lastUsed: z6.string(),
|
|
2806
|
+
notifiedDiagnostics: z6.array(z6.string()).default([]),
|
|
2807
|
+
pending: z6.record(z6.string()),
|
|
2808
|
+
coverageGaps: z6.array(z6.string()),
|
|
2809
|
+
events: z6.record(z6.object({ at: z6.string(), count: z6.number().int().positive() }))
|
|
2810
|
+
})),
|
|
2811
|
+
updatedAt: z6.string(),
|
|
2812
|
+
fingerprint: z6.string().nullable(),
|
|
2813
|
+
latest: z6.string().nullable(),
|
|
2814
|
+
notifications: notificationStateSchema.optional(),
|
|
2815
|
+
agentNotifications: notificationStateSchema.optional(),
|
|
2816
|
+
analysis: z6.object({ id: z6.string(), fingerprint: z6.string(), completedAt: z6.number(), path: z6.string() }).optional()
|
|
2699
2817
|
});
|
|
2700
2818
|
}
|
|
2701
2819
|
});
|
|
2702
2820
|
|
|
2703
2821
|
// src/audit/advisory-review.ts
|
|
2704
2822
|
import fs10 from "fs/promises";
|
|
2705
|
-
import { z as
|
|
2823
|
+
import { z as z7 } from "zod";
|
|
2706
2824
|
async function readRecord(root, id) {
|
|
2707
2825
|
const raw = await readStoreJson(root, recordPath(id));
|
|
2708
2826
|
if (raw === null) return null;
|
|
@@ -2848,41 +2966,41 @@ var init_advisory_review = __esm({
|
|
|
2848
2966
|
init_decisions();
|
|
2849
2967
|
init_drift2();
|
|
2850
2968
|
git2 = async (root, ...args2) => (await (["log", "ls-tree", "ls-files", "merge-base"].includes(args2[0]) ? inspectionGit : execGit)(args2, { cwd: root, timeout: 1e4, maxBuffer: 8 * 1024 * 1024 })).stdout;
|
|
2851
|
-
hashSchema =
|
|
2852
|
-
commitSchema2 =
|
|
2853
|
-
evidenceSchema2 =
|
|
2969
|
+
hashSchema = z7.string().regex(/^[a-f0-9]{64}$/);
|
|
2970
|
+
commitSchema2 = z7.string().regex(/^[a-f0-9]{40,64}$/);
|
|
2971
|
+
evidenceSchema2 = z7.object({
|
|
2854
2972
|
head: commitSchema2,
|
|
2855
2973
|
fingerprint: hashSchema,
|
|
2856
|
-
scope:
|
|
2857
|
-
tree:
|
|
2974
|
+
scope: z7.array(z7.string()).min(1).max(1e3),
|
|
2975
|
+
tree: z7.string().max(2 * 1024 * 1024),
|
|
2858
2976
|
lastScopeCommit: commitSchema2,
|
|
2859
|
-
paths:
|
|
2977
|
+
paths: z7.array(z7.object({ path: z7.string(), exists: z7.boolean() })).max(1e3)
|
|
2860
2978
|
});
|
|
2861
|
-
dismissalReasonSchema =
|
|
2862
|
-
eventSchema2 =
|
|
2863
|
-
at:
|
|
2864
|
-
reviewer:
|
|
2865
|
-
note:
|
|
2866
|
-
outcome:
|
|
2979
|
+
dismissalReasonSchema = z7.enum(["no-dependency-claims", "unrelated-manifest-change"]);
|
|
2980
|
+
eventSchema2 = z7.object({
|
|
2981
|
+
at: z7.string().datetime(),
|
|
2982
|
+
reviewer: z7.string().trim().min(1).max(200).optional(),
|
|
2983
|
+
note: z7.string().trim().min(1).max(2500),
|
|
2984
|
+
outcome: z7.enum(["addressed", "inapplicable", "deferred", "dismissed"]),
|
|
2867
2985
|
reasonCode: dismissalReasonSchema.optional(),
|
|
2868
2986
|
finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
|
|
2869
2987
|
evidence: evidenceSchema2
|
|
2870
2988
|
});
|
|
2871
|
-
recordSchema =
|
|
2872
|
-
reviewSummarySchema =
|
|
2989
|
+
recordSchema = z7.object({ version: z7.literal(1), id: hashSchema, events: z7.array(eventSchema2).min(1).max(200), digest: hashSchema });
|
|
2990
|
+
reviewSummarySchema = z7.object({
|
|
2873
2991
|
id: hashSchema,
|
|
2874
|
-
status:
|
|
2875
|
-
reason:
|
|
2876
|
-
recordPath:
|
|
2877
|
-
reviewer:
|
|
2878
|
-
note:
|
|
2879
|
-
outcome:
|
|
2880
|
-
reviewedHead:
|
|
2992
|
+
status: z7.enum(["current", "reopened", "deferred", "unverified"]),
|
|
2993
|
+
reason: z7.string(),
|
|
2994
|
+
recordPath: z7.string().optional(),
|
|
2995
|
+
reviewer: z7.string().optional(),
|
|
2996
|
+
note: z7.string().optional(),
|
|
2997
|
+
outcome: z7.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired", "dismissed"]).optional(),
|
|
2998
|
+
reviewedHead: z7.string().optional()
|
|
2881
2999
|
});
|
|
2882
|
-
advisoryReviewRequest =
|
|
2883
|
-
baselinePath:
|
|
3000
|
+
advisoryReviewRequest = z7.object({
|
|
3001
|
+
baselinePath: z7.string().min(1),
|
|
2884
3002
|
findingId: hashSchema,
|
|
2885
|
-
action:
|
|
3003
|
+
action: z7.enum(["prepare", "addressed", "inapplicable", "deferred", "dismiss"]).default("prepare"),
|
|
2886
3004
|
reasonCode: dismissalReasonSchema.optional(),
|
|
2887
3005
|
reviewer: eventSchema2.shape.reviewer.optional(),
|
|
2888
3006
|
note: eventSchema2.shape.note.optional(),
|
|
@@ -2987,7 +3105,7 @@ var init_audit = __esm({
|
|
|
2987
3105
|
import fs12 from "fs/promises";
|
|
2988
3106
|
import path21 from "path";
|
|
2989
3107
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2990
|
-
import { z as
|
|
3108
|
+
import { z as z8 } from "zod";
|
|
2991
3109
|
function recheckable(finding) {
|
|
2992
3110
|
return !("confidence" in finding) && finding.resolution === "recheck" && ["deleted-reference", "dead-command", "new-module"].includes(finding.type) && ["missing-path", "missing-script", "unmentioned-dir"].includes(finding.evidence.kind);
|
|
2993
3111
|
}
|
|
@@ -3055,7 +3173,7 @@ async function withRepairSession(root, options, run, validate, cachedAudit) {
|
|
|
3055
3173
|
}
|
|
3056
3174
|
async function prepareOriginal(rootDir, checks, options, session) {
|
|
3057
3175
|
const root = await fs12.realpath(rootDir);
|
|
3058
|
-
const selected =
|
|
3176
|
+
const selected = z8.array(checkSchema).nonempty().parse(checks);
|
|
3059
3177
|
const report = await (session ? session.audit(selected) : stableAudit(root, selected, options));
|
|
3060
3178
|
if (!report) throw new Error("No context files found to prepare a repair.");
|
|
3061
3179
|
if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
|
|
@@ -3196,42 +3314,42 @@ var init_repair = __esm({
|
|
|
3196
3314
|
init_findings();
|
|
3197
3315
|
init_advisory_review();
|
|
3198
3316
|
init_findings();
|
|
3199
|
-
count2 =
|
|
3200
|
-
reportSchema =
|
|
3201
|
-
version:
|
|
3202
|
-
root:
|
|
3203
|
-
gitAvailable:
|
|
3317
|
+
count2 = z8.number().int().nonnegative();
|
|
3318
|
+
reportSchema = z8.object({
|
|
3319
|
+
version: z8.literal(1),
|
|
3320
|
+
root: z8.string(),
|
|
3321
|
+
gitAvailable: z8.literal(true),
|
|
3204
3322
|
headHash: commitSchema.shape.hash,
|
|
3205
|
-
checksRun:
|
|
3206
|
-
docs:
|
|
3207
|
-
path:
|
|
3208
|
-
scope:
|
|
3209
|
-
kind:
|
|
3323
|
+
checksRun: z8.array(checkSchema).nonempty(),
|
|
3324
|
+
docs: z8.array(z8.object({
|
|
3325
|
+
path: z8.string().refine((value) => normalizeRepoPath(value) === value),
|
|
3326
|
+
scope: z8.string().optional(),
|
|
3327
|
+
kind: z8.enum(["instructions", "readme", "guide"]).optional(),
|
|
3210
3328
|
lastCommit: commitSchema.nullable(),
|
|
3211
|
-
dirty:
|
|
3329
|
+
dirty: z8.boolean(),
|
|
3212
3330
|
lineCount: count2
|
|
3213
3331
|
})).nonempty(),
|
|
3214
|
-
decisionsChecked:
|
|
3215
|
-
clean:
|
|
3216
|
-
excludedChecks:
|
|
3217
|
-
issues:
|
|
3218
|
-
advisories:
|
|
3219
|
-
suppressedAdvisories:
|
|
3220
|
-
skippedChecks:
|
|
3332
|
+
decisionsChecked: z8.boolean(),
|
|
3333
|
+
clean: z8.boolean(),
|
|
3334
|
+
excludedChecks: z8.array(z8.object({ check: checkSchema, doc: z8.string() })).optional(),
|
|
3335
|
+
issues: z8.array(issueSchema),
|
|
3336
|
+
advisories: z8.array(advisorySchema),
|
|
3337
|
+
suppressedAdvisories: z8.array(advisorySchema).optional(),
|
|
3338
|
+
skippedChecks: z8.array(z8.object({ check: z8.string(), reason: z8.string(), doc: z8.string().optional() }))
|
|
3221
3339
|
});
|
|
3222
|
-
baselineSchema =
|
|
3223
|
-
kind:
|
|
3224
|
-
version:
|
|
3225
|
-
createdAt:
|
|
3340
|
+
baselineSchema = z8.object({
|
|
3341
|
+
kind: z8.literal("mason-audit-repair"),
|
|
3342
|
+
version: z8.literal(1),
|
|
3343
|
+
createdAt: z8.string().datetime(),
|
|
3226
3344
|
report: reportSchema,
|
|
3227
|
-
digest:
|
|
3345
|
+
digest: z8.string().regex(/^[a-f0-9]{64}$/)
|
|
3228
3346
|
});
|
|
3229
|
-
repairFindingSchema =
|
|
3230
|
-
id:
|
|
3231
|
-
original:
|
|
3232
|
-
status:
|
|
3233
|
-
reason:
|
|
3234
|
-
current:
|
|
3347
|
+
repairFindingSchema = z8.object({
|
|
3348
|
+
id: z8.string(),
|
|
3349
|
+
original: z8.union([issueSchema, advisorySchema]),
|
|
3350
|
+
status: z8.enum(["resolved", "unresolved", "review-required", "unverified"]),
|
|
3351
|
+
reason: z8.string(),
|
|
3352
|
+
current: z8.union([issueSchema, advisorySchema]).optional(),
|
|
3235
3353
|
review: reviewSummarySchema.optional()
|
|
3236
3354
|
});
|
|
3237
3355
|
}
|
|
@@ -3242,7 +3360,7 @@ import fs13 from "fs/promises";
|
|
|
3242
3360
|
import path22 from "path";
|
|
3243
3361
|
import { createHash as createHash3 } from "crypto";
|
|
3244
3362
|
import fg7 from "fast-glob";
|
|
3245
|
-
import { z as
|
|
3363
|
+
import { z as z9 } from "zod";
|
|
3246
3364
|
async function git3(root, ...args2) {
|
|
3247
3365
|
return profilePhase("automation.git", async () => (await execGit(args2, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout);
|
|
3248
3366
|
}
|
|
@@ -3385,18 +3503,248 @@ var init_evidence = __esm({
|
|
|
3385
3503
|
init_dead_command();
|
|
3386
3504
|
init_storage();
|
|
3387
3505
|
init_advisory_review();
|
|
3388
|
-
engineVersion = true ? "0.
|
|
3506
|
+
engineVersion = true ? "0.20.1" : "development";
|
|
3389
3507
|
hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
|
|
3390
3508
|
content = readAuditInput;
|
|
3391
|
-
cacheSchema =
|
|
3509
|
+
cacheSchema = z9.object({ version: z9.literal(1), entries: z9.record(z9.object({ key: z9.string(), result: checkResultSchema })), digest: z9.string() });
|
|
3510
|
+
}
|
|
3511
|
+
});
|
|
3512
|
+
|
|
3513
|
+
// src/automation/usefulness.ts
|
|
3514
|
+
import fs14 from "fs/promises";
|
|
3515
|
+
import { z as z10 } from "zod";
|
|
3516
|
+
async function enabled(root) {
|
|
3517
|
+
const raw = await readStoreJson(root, configPath);
|
|
3518
|
+
return raw !== null && configSchema.parse(raw).enabled;
|
|
3519
|
+
}
|
|
3520
|
+
async function configureUsefulness(dir, value, options = {}) {
|
|
3521
|
+
const ws = await workspace(dir);
|
|
3522
|
+
const configured = await withStoreLock(ws.root, directory, async () => {
|
|
3523
|
+
if (options.onlyIfUnset) {
|
|
3524
|
+
const existing = await readStoreJson(ws.root, configPath);
|
|
3525
|
+
if (existing !== null) return configSchema.parse(existing).enabled;
|
|
3526
|
+
}
|
|
3527
|
+
const ignore = await storePath(ws.root, directory + "/.gitignore", true);
|
|
3528
|
+
try {
|
|
3529
|
+
await fs14.writeFile(ignore, "*\n", { flag: "wx", mode: 384 });
|
|
3530
|
+
} catch (error) {
|
|
3531
|
+
if (error.code !== "EEXIST") throw error;
|
|
3532
|
+
}
|
|
3533
|
+
await writeStoreJson(ws.root, configPath, { version: 1, enabled: value });
|
|
3534
|
+
return value;
|
|
3535
|
+
});
|
|
3536
|
+
return { enabled: configured, storage: directory, note: "Local only. Disabling retains prior observations." };
|
|
3537
|
+
}
|
|
3538
|
+
async function observeUsefulness(root, event, report, agent, completion, elapsedMs, observedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3539
|
+
if (!event.host || !event.sessionId || !await enabled(root)) return;
|
|
3540
|
+
const ws = await workspace(root);
|
|
3541
|
+
await withStoreLock(ws.root, directory, async () => {
|
|
3542
|
+
if (!await enabled(root)) return;
|
|
3543
|
+
const raw = await readStoreJson(root, statePath(ws));
|
|
3544
|
+
const state = raw === null ? empty() : stateSchema2.parse(raw);
|
|
3545
|
+
const now = observedAt;
|
|
3546
|
+
const key = hash([event.host, event.sessionId]);
|
|
3547
|
+
const session = state.sessions[key] ??= {
|
|
3548
|
+
host: event.host,
|
|
3549
|
+
updatedAt: now,
|
|
3550
|
+
checks: 0,
|
|
3551
|
+
checkMs: 0,
|
|
3552
|
+
findings: {},
|
|
3553
|
+
decisions: [],
|
|
3554
|
+
retrievalObserved: false,
|
|
3555
|
+
receiptIds: [],
|
|
3556
|
+
reportPath: report.reportPath,
|
|
3557
|
+
truncated: false
|
|
3558
|
+
};
|
|
3559
|
+
const newer = now >= session.updatedAt;
|
|
3560
|
+
if (newer) {
|
|
3561
|
+
session.updatedAt = now;
|
|
3562
|
+
session.reportPath = report.reportPath;
|
|
3563
|
+
}
|
|
3564
|
+
session.checks++;
|
|
3565
|
+
session.checkMs += Math.max(0, elapsedMs);
|
|
3566
|
+
if (newer) {
|
|
3567
|
+
if (event.event === "task_end") session.endedAt = now;
|
|
3568
|
+
else delete session.endedAt;
|
|
3569
|
+
}
|
|
3570
|
+
let latestFindings = [];
|
|
3571
|
+
if (!newer && [...agent.slice(0, 4), ...completion.slice(0, 4)].some((f) => !session.findings[f.id])) {
|
|
3572
|
+
try {
|
|
3573
|
+
if (!session.reportPath.startsWith(ws.directory + "/checks/")) throw new Error("Unexpected report path");
|
|
3574
|
+
latestFindings = z10.object({ findings: z10.array(z10.object({ id: z10.string(), status: findingSchema2.shape.status })) }).parse(await readStoreJson(root, session.reportPath)).findings;
|
|
3575
|
+
} catch {
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
for (const [channel, findings] of [["agent", agent], ["completion", completion]]) {
|
|
3579
|
+
for (const finding of findings.slice(0, 4)) {
|
|
3580
|
+
if (!session.findings[finding.id] && Object.keys(session.findings).length >= 200) {
|
|
3581
|
+
session.truncated = true;
|
|
3582
|
+
continue;
|
|
3583
|
+
}
|
|
3584
|
+
const saved = session.findings[finding.id] ??= {
|
|
3585
|
+
type: finding.original.type,
|
|
3586
|
+
channels: [],
|
|
3587
|
+
status: newer ? finding.status : latestFindings.find((f) => f.id === finding.id)?.status ?? "unknown",
|
|
3588
|
+
deliveredAt: now,
|
|
3589
|
+
observedAt: newer ? now : session.updatedAt
|
|
3590
|
+
};
|
|
3591
|
+
if (!saved.channels.includes(channel)) saved.channels.push(channel);
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
for (const [id, saved] of Object.entries(session.findings)) {
|
|
3595
|
+
if (now < saved.observedAt) continue;
|
|
3596
|
+
const current = report.findings.find((f) => f.id === id);
|
|
3597
|
+
saved.status = current?.status ?? "unknown";
|
|
3598
|
+
saved.observedAt = now;
|
|
3599
|
+
}
|
|
3600
|
+
if (event.retrievedDecisionIds !== void 0 && event.toolId) {
|
|
3601
|
+
const receipt = hash([event.toolId]);
|
|
3602
|
+
if (!session.receiptIds.includes(receipt)) {
|
|
3603
|
+
session.retrievalObserved = true;
|
|
3604
|
+
const decisions = [.../* @__PURE__ */ new Set([...session.decisions, ...event.retrievedDecisionIds.map((id) => hash([id]))])];
|
|
3605
|
+
if (decisions.length > 200) session.truncated = true;
|
|
3606
|
+
session.decisions = decisions.slice(0, 200);
|
|
3607
|
+
session.receiptIds = [...session.receiptIds, receipt].slice(-200);
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
const keys = Object.keys(state.sessions).sort((a, b) => state.sessions[b].updatedAt.localeCompare(state.sessions[a].updatedAt));
|
|
3611
|
+
for (const key2 of keys.slice(50)) delete state.sessions[key2];
|
|
3612
|
+
await writeStoreJson(root, statePath(ws), state);
|
|
3613
|
+
});
|
|
3614
|
+
}
|
|
3615
|
+
async function rateUsefulness(dir, sessionId, findingId2, rating) {
|
|
3616
|
+
const ws = await workspace(dir);
|
|
3617
|
+
return withStoreLock(ws.root, directory, async () => {
|
|
3618
|
+
const state = stateSchema2.parse(await readStoreJson(ws.root, statePath(ws)));
|
|
3619
|
+
const finding = state.sessions[sessionId]?.findings[findingId2];
|
|
3620
|
+
if (!finding) throw new Error("Unknown retained session or finding; inspect mason stats --json.");
|
|
3621
|
+
finding.rating = usefulnessRating.parse(rating);
|
|
3622
|
+
await writeStoreJson(ws.root, statePath(ws), state);
|
|
3623
|
+
return { session: sessionId, finding: findingId2, rating };
|
|
3624
|
+
});
|
|
3625
|
+
}
|
|
3626
|
+
async function usefulnessStatus(dir, sessionId) {
|
|
3627
|
+
const ws = await workspace(dir);
|
|
3628
|
+
const raw = await readStoreJson(ws.root, statePath(ws));
|
|
3629
|
+
const state = raw === null ? empty() : stateSchema2.parse(raw);
|
|
3630
|
+
if (sessionId !== void 0 && !Object.hasOwn(state.sessions, sessionId)) throw new Error("Unknown retained session; inspect mason stats --json for session IDs.");
|
|
3631
|
+
const sessions = Object.entries(state.sessions).filter(([id]) => sessionId === void 0 || id === sessionId).sort(([, a], [, b]) => b.updatedAt.localeCompare(a.updatedAt)).map(([id, s]) => ({
|
|
3632
|
+
id,
|
|
3633
|
+
...s,
|
|
3634
|
+
receiptIds: void 0,
|
|
3635
|
+
delivered: Object.keys(s.findings).length,
|
|
3636
|
+
resolved: Object.values(s.findings).filter((f) => f.status === "resolved").length,
|
|
3637
|
+
unknown: Object.values(s.findings).filter((f) => f.status === "unknown" || f.status === "unverified").length,
|
|
3638
|
+
open: Object.values(s.findings).filter((f) => f.status === "unresolved" || f.status === "review-required").length,
|
|
3639
|
+
ratings: Object.fromEntries(usefulnessRating.options.map((rating) => [rating, Object.values(s.findings).filter((f) => f.rating === rating).length])),
|
|
3640
|
+
deferred: Object.values(s.findings).filter((f) => f.rating === "deferred").length,
|
|
3641
|
+
retrievedDecisions: s.retrievalObserved ? s.decisions.length : null,
|
|
3642
|
+
application: "unknown",
|
|
3643
|
+
modelTokens: null
|
|
3644
|
+
}));
|
|
3645
|
+
const sum = (pick) => sessions.reduce((total, s) => total + pick(s), 0);
|
|
3646
|
+
const retrievalObservedSessions = sessions.filter((s) => s.retrievalObserved).length;
|
|
3647
|
+
return {
|
|
3648
|
+
version: 1,
|
|
3649
|
+
enabled: await enabled(ws.root),
|
|
3650
|
+
branch: ws.branch,
|
|
3651
|
+
sessionId: sessionId ?? null,
|
|
3652
|
+
summary: {
|
|
3653
|
+
sessions: sessions.length,
|
|
3654
|
+
delivered: sum((s) => s.delivered),
|
|
3655
|
+
resolved: sum((s) => s.resolved),
|
|
3656
|
+
open: sum((s) => s.open),
|
|
3657
|
+
unknown: sum((s) => s.unknown),
|
|
3658
|
+
ratings: Object.fromEntries(usefulnessRating.options.map((rating) => [rating, sum((s) => s.ratings[rating])])),
|
|
3659
|
+
retrievedDecisions: retrievalObservedSessions ? sum((s) => s.retrievedDecisions ?? 0) : null,
|
|
3660
|
+
retrievalObservedSessions,
|
|
3661
|
+
checks: sum((s) => s.checks),
|
|
3662
|
+
checkMs: sum((s) => s.checkMs),
|
|
3663
|
+
truncatedSessions: sessions.filter((s) => s.truncated).length,
|
|
3664
|
+
application: "unknown",
|
|
3665
|
+
modelTokens: null
|
|
3666
|
+
},
|
|
3667
|
+
note: "Counts sum distinct findings and returned records within each retained session; the same item may appear in multiple sessions. Status reflects the last observed check, not a fresh verification. Up to 50 sessions and 200 findings/records per session are retained. Resolution after a notice does not establish causation. Ratings are supplied feedback, not authenticated judgments. Lesson application, later-session reuse and model-token cost are unknown.",
|
|
3668
|
+
sessions
|
|
3669
|
+
};
|
|
3670
|
+
}
|
|
3671
|
+
function summarizeUsefulness(result) {
|
|
3672
|
+
const s = result.summary;
|
|
3673
|
+
return [
|
|
3674
|
+
`Mason stats \u2014 tracking ${result.enabled ? "enabled" : "disabled"} (local only)`,
|
|
3675
|
+
`${s.sessions} retained session(s)${result.sessionId ? " selected" : " in this branch"}. Counts are summed per session.`,
|
|
3676
|
+
`Findings: ${s.delivered} delivered; ${s.resolved} observed resolved; ${s.open} open; ${s.unknown} unknown.`,
|
|
3677
|
+
`Feedback: ${s.ratings.helpful} helpful; ${s.ratings["already-knew"]} already knew; ${s.ratings.irrelevant} irrelevant; ${s.ratings.deferred} deferred.`,
|
|
3678
|
+
`Decision records returned: ${s.retrievedDecisions ?? "unknown"} (${s.retrievalObservedSessions}/${s.sessions} sessions with retrieval receipts); application unknown.`,
|
|
3679
|
+
`Automation: ${(s.checkMs / 1e3).toFixed(2)} seconds across ${s.checks} observations; model-token cost unknown.`,
|
|
3680
|
+
...s.truncatedSessions ? [`${s.truncatedSessions} session(s) have truncated observations.`] : [],
|
|
3681
|
+
...result.sessionId ? result.sessions.flatMap((session) => [
|
|
3682
|
+
`Session ${session.id} (${session.host}) \u2014 ${session.endedAt ? "last observed stop " + session.endedAt : "no final stop observed"}`,
|
|
3683
|
+
`Evidence: ${session.reportPath}`,
|
|
3684
|
+
...Object.entries(session.findings).map(([id, f]) => ` ${id}: ${f.type}; ${f.status}; ${f.channels.join("/")}${f.rating ? "; rated " + f.rating : ""}`)
|
|
3685
|
+
]) : [s.sessions ? "Use --json for session IDs, then --session <id> for details." : "No retained sessions observed."],
|
|
3686
|
+
"Last observed checks only. Resolution after a notice does not prove Mason caused it."
|
|
3687
|
+
].join("\n");
|
|
3688
|
+
}
|
|
3689
|
+
function retrievedDecisions(tool, response) {
|
|
3690
|
+
if (!/(?:^|__)mason__get_context$/.test(tool ?? "")) return void 0;
|
|
3691
|
+
try {
|
|
3692
|
+
let value = typeof response === "string" ? JSON.parse(response) : response;
|
|
3693
|
+
if (value?.isError) return void 0;
|
|
3694
|
+
if (Array.isArray(value?.content)) {
|
|
3695
|
+
const text2 = value.content.find((item) => item?.type === "text" && typeof item.text === "string")?.text;
|
|
3696
|
+
if (!text2) return void 0;
|
|
3697
|
+
value = JSON.parse(text2);
|
|
3698
|
+
}
|
|
3699
|
+
if (!value || typeof value.decisions !== "object" || !value.decisions || Array.isArray(value.decisions)) return void 0;
|
|
3700
|
+
return Object.keys(value.decisions).filter((id) => /^[a-zA-Z0-9_-]{1,200}$/.test(id)).slice(0, 200);
|
|
3701
|
+
} catch {
|
|
3702
|
+
return void 0;
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
var directory, configPath, configSchema, usefulnessRating, findingSchema2, sessionSchema, stateSchema2, empty, statePath;
|
|
3706
|
+
var init_usefulness = __esm({
|
|
3707
|
+
"src/automation/usefulness.ts"() {
|
|
3708
|
+
"use strict";
|
|
3709
|
+
init_evidence();
|
|
3710
|
+
init_storage();
|
|
3711
|
+
init_store_lock();
|
|
3712
|
+
directory = ".mason/local/usefulness";
|
|
3713
|
+
configPath = directory + "/config.json";
|
|
3714
|
+
configSchema = z10.object({ version: z10.literal(1), enabled: z10.boolean() });
|
|
3715
|
+
usefulnessRating = z10.enum(["helpful", "already-knew", "irrelevant", "deferred"]);
|
|
3716
|
+
findingSchema2 = z10.object({
|
|
3717
|
+
type: z10.string(),
|
|
3718
|
+
channels: z10.array(z10.enum(["agent", "completion"])),
|
|
3719
|
+
status: z10.enum(["resolved", "unresolved", "review-required", "unverified", "unknown"]),
|
|
3720
|
+
deliveredAt: z10.string(),
|
|
3721
|
+
observedAt: z10.string(),
|
|
3722
|
+
rating: usefulnessRating.optional()
|
|
3723
|
+
});
|
|
3724
|
+
sessionSchema = z10.object({
|
|
3725
|
+
host: z10.string(),
|
|
3726
|
+
updatedAt: z10.string(),
|
|
3727
|
+
endedAt: z10.string().optional(),
|
|
3728
|
+
checks: z10.number(),
|
|
3729
|
+
checkMs: z10.number(),
|
|
3730
|
+
findings: z10.record(findingSchema2),
|
|
3731
|
+
decisions: z10.array(z10.string()),
|
|
3732
|
+
retrievalObserved: z10.boolean(),
|
|
3733
|
+
receiptIds: z10.array(z10.string()),
|
|
3734
|
+
reportPath: z10.string(),
|
|
3735
|
+
truncated: z10.boolean()
|
|
3736
|
+
});
|
|
3737
|
+
stateSchema2 = z10.object({ version: z10.literal(1), sessions: z10.record(sessionSchema) });
|
|
3738
|
+
empty = () => ({ version: 1, sessions: {} });
|
|
3739
|
+
statePath = (ws) => `${directory}/${hash([ws.root, ws.gitDir, ws.branch]).slice(0, 24)}.json`;
|
|
3392
3740
|
}
|
|
3393
3741
|
});
|
|
3394
3742
|
|
|
3395
3743
|
// src/automation/execution.ts
|
|
3396
3744
|
import os2 from "os";
|
|
3397
|
-
import
|
|
3745
|
+
import fs15 from "fs/promises";
|
|
3398
3746
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
3399
|
-
import { z as
|
|
3747
|
+
import { z as z11 } from "zod";
|
|
3400
3748
|
function parseExecution(raw) {
|
|
3401
3749
|
try {
|
|
3402
3750
|
return executionSchema.parse(raw);
|
|
@@ -3414,15 +3762,15 @@ function automationFailure(error) {
|
|
|
3414
3762
|
codes.add(String(cause.code));
|
|
3415
3763
|
cause = cause.cause;
|
|
3416
3764
|
}
|
|
3417
|
-
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
|
|
3765
|
+
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 z11.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";
|
|
3418
3766
|
return { code, message: message2, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
|
|
3419
3767
|
}
|
|
3420
3768
|
function failureMessage(error) {
|
|
3421
3769
|
const failure = automationFailure(error);
|
|
3422
3770
|
return `Mason automation unavailable [${failure.code}]; evidence capture/verification was not established. ${failure.message}` + (failure.receiptRecorded ? "" : " No durable failure receipt was recorded.");
|
|
3423
3771
|
}
|
|
3424
|
-
async function failureNotifications(root,
|
|
3425
|
-
const raw = await readStoreJson(root,
|
|
3772
|
+
async function failureNotifications(root, directory2) {
|
|
3773
|
+
const raw = await readStoreJson(root, directory2 + "/notifications.json");
|
|
3426
3774
|
return raw === null ? { version: 1, sessions: {} } : notificationsSchema.parse(raw);
|
|
3427
3775
|
}
|
|
3428
3776
|
async function hookFailureMessage(dir, host, sessionId, error) {
|
|
@@ -3452,12 +3800,12 @@ async function hookFailureMessage(dir, host, sessionId, error) {
|
|
|
3452
3800
|
return failureMessage(error);
|
|
3453
3801
|
}
|
|
3454
3802
|
}
|
|
3455
|
-
async function refreshRunning(root,
|
|
3803
|
+
async function refreshRunning(root, directory2, attempts) {
|
|
3456
3804
|
for (const attempt of attempts) {
|
|
3457
3805
|
if (attempt.status !== "running") continue;
|
|
3458
3806
|
let alive = false;
|
|
3459
3807
|
try {
|
|
3460
|
-
const owner = await readStoreJson(root, attempt.lease ? leasePath(
|
|
3808
|
+
const owner = await readStoreJson(root, attempt.lease ? leasePath(directory2, attempt.id) : directory2 + "/lock");
|
|
3461
3809
|
if (attempt.host === os2.hostname() && owner?.pid === attempt.pid && owner.host === attempt.host) {
|
|
3462
3810
|
try {
|
|
3463
3811
|
process.kill(attempt.pid, 0);
|
|
@@ -3471,12 +3819,12 @@ async function refreshRunning(root, directory, attempts) {
|
|
|
3471
3819
|
if (!alive) attempt.status = "unknown";
|
|
3472
3820
|
}
|
|
3473
3821
|
}
|
|
3474
|
-
async function editExecution(root,
|
|
3475
|
-
return withLock(root,
|
|
3476
|
-
const file =
|
|
3822
|
+
async function editExecution(root, directory2, edit) {
|
|
3823
|
+
return withLock(root, directory2 + "/execution-lock", async () => {
|
|
3824
|
+
const file = directory2 + "/execution.json";
|
|
3477
3825
|
const raw = await readStoreJson(root, file);
|
|
3478
3826
|
const log = raw === null ? { version: 1, attempts: [], discardedAttempts: 0 } : parseExecution(raw);
|
|
3479
|
-
await refreshRunning(root,
|
|
3827
|
+
await refreshRunning(root, directory2, log.attempts);
|
|
3480
3828
|
edit(log);
|
|
3481
3829
|
const finished = log.attempts.filter((a) => a.status !== "running");
|
|
3482
3830
|
const discard = new Set(finished.slice(0, Math.max(0, finished.length - 32)).map((a) => a.id));
|
|
@@ -3485,7 +3833,7 @@ async function editExecution(root, directory, edit) {
|
|
|
3485
3833
|
await writeStoreJson(root, file, log);
|
|
3486
3834
|
});
|
|
3487
3835
|
}
|
|
3488
|
-
async function recordExecution(root,
|
|
3836
|
+
async function recordExecution(root, directory2, event, run) {
|
|
3489
3837
|
const started = performance.now();
|
|
3490
3838
|
const attempt = {
|
|
3491
3839
|
id: randomUUID3(),
|
|
@@ -3496,15 +3844,15 @@ async function recordExecution(root, directory, event, run) {
|
|
|
3496
3844
|
status: "running",
|
|
3497
3845
|
lease: true
|
|
3498
3846
|
};
|
|
3499
|
-
const owner = leasePath(
|
|
3500
|
-
const finish = async (changes) => editExecution(root,
|
|
3847
|
+
const owner = leasePath(directory2, attempt.id);
|
|
3848
|
+
const finish = async (changes) => editExecution(root, directory2, (log) => {
|
|
3501
3849
|
const current = log.attempts.find((a) => a.id === attempt.id);
|
|
3502
3850
|
if (!current) throw new Error("Automation execution receipt disappeared; completion could not be recorded.");
|
|
3503
3851
|
Object.assign(current, changes, { finishedAt: (/* @__PURE__ */ new Date()).toISOString(), durationMs: performance.now() - started });
|
|
3504
3852
|
});
|
|
3505
3853
|
await writeStoreJson(root, owner, { pid: attempt.pid, host: attempt.host });
|
|
3506
3854
|
try {
|
|
3507
|
-
await editExecution(root,
|
|
3855
|
+
await editExecution(root, directory2, (log) => {
|
|
3508
3856
|
if (log.attempts.filter((a) => a.status === "running").length >= 128) throw new Error("Too many active automation executions; existing receipts were retained.");
|
|
3509
3857
|
log.attempts.push(attempt);
|
|
3510
3858
|
});
|
|
@@ -3522,15 +3870,15 @@ async function recordExecution(root, directory, event, run) {
|
|
|
3522
3870
|
throw Object.assign(new Error(failure.message, { cause: error }), { failure });
|
|
3523
3871
|
}
|
|
3524
3872
|
} finally {
|
|
3525
|
-
await
|
|
3873
|
+
await fs15.rm(await storePath(root, owner), { force: true }).catch(() => {
|
|
3526
3874
|
});
|
|
3527
3875
|
}
|
|
3528
3876
|
}
|
|
3529
|
-
async function executionStatus(root,
|
|
3530
|
-
const raw = await readStoreJson(root,
|
|
3877
|
+
async function executionStatus(root, directory2) {
|
|
3878
|
+
const raw = await readStoreJson(root, directory2 + "/execution.json");
|
|
3531
3879
|
if (raw === null) return { status: "not-observed", attempts: [] };
|
|
3532
3880
|
const log = parseExecution(raw);
|
|
3533
|
-
await refreshRunning(root,
|
|
3881
|
+
await refreshRunning(root, directory2, log.attempts);
|
|
3534
3882
|
const completed = [...log.attempts].reverse().find((a) => a.status === "completed");
|
|
3535
3883
|
const outstanding = [...log.attempts].reverse().find((a) => ["failed", "unknown"].includes(a.status) && (!completed || (a.finishedAt ?? a.startedAt) >= completed.startedAt));
|
|
3536
3884
|
const status = log.attempts.some((a) => a.status === "running") ? "running" : outstanding?.status ?? log.attempts.at(-1)?.status ?? "not-observed";
|
|
@@ -3543,46 +3891,108 @@ var init_execution = __esm({
|
|
|
3543
3891
|
init_store();
|
|
3544
3892
|
init_evidence();
|
|
3545
3893
|
init_storage();
|
|
3546
|
-
failureSchema =
|
|
3547
|
-
code:
|
|
3548
|
-
message:
|
|
3549
|
-
retryable:
|
|
3550
|
-
receiptRecorded:
|
|
3894
|
+
failureSchema = z11.object({
|
|
3895
|
+
code: z11.enum(["inputs-changed", "storage-full", "busy", "invalid-input", "history-unavailable", "invalid-evidence", "io-error", "internal"]),
|
|
3896
|
+
message: z11.string(),
|
|
3897
|
+
retryable: z11.boolean(),
|
|
3898
|
+
receiptRecorded: z11.boolean()
|
|
3551
3899
|
});
|
|
3552
|
-
attemptSchema =
|
|
3553
|
-
id:
|
|
3554
|
-
lease:
|
|
3555
|
-
event:
|
|
3556
|
-
startedAt:
|
|
3557
|
-
finishedAt:
|
|
3558
|
-
durationMs:
|
|
3559
|
-
pid:
|
|
3560
|
-
host:
|
|
3561
|
-
status:
|
|
3562
|
-
verificationStatus:
|
|
3563
|
-
reportPath:
|
|
3900
|
+
attemptSchema = z11.object({
|
|
3901
|
+
id: z11.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
3902
|
+
lease: z11.literal(true).optional(),
|
|
3903
|
+
event: z11.string(),
|
|
3904
|
+
startedAt: z11.string(),
|
|
3905
|
+
finishedAt: z11.string().optional(),
|
|
3906
|
+
durationMs: z11.number().nonnegative().optional(),
|
|
3907
|
+
pid: z11.number().int().positive(),
|
|
3908
|
+
host: z11.string(),
|
|
3909
|
+
status: z11.enum(["running", "completed", "failed", "unknown"]),
|
|
3910
|
+
verificationStatus: z11.string().optional(),
|
|
3911
|
+
reportPath: z11.string().optional(),
|
|
3564
3912
|
failure: failureSchema.optional()
|
|
3565
3913
|
});
|
|
3566
|
-
executionSchema =
|
|
3567
|
-
notificationsSchema =
|
|
3568
|
-
signature:
|
|
3914
|
+
executionSchema = z11.object({ version: z11.literal(1), attempts: z11.array(attemptSchema).max(160), discardedAttempts: z11.number().int().nonnegative().default(0) });
|
|
3915
|
+
notificationsSchema = z11.object({ version: z11.literal(1), sessions: z11.record(z11.object({
|
|
3916
|
+
signature: z11.string(),
|
|
3569
3917
|
code: failureSchema.shape.code,
|
|
3570
|
-
message:
|
|
3571
|
-
count:
|
|
3572
|
-
lastAt:
|
|
3918
|
+
message: z11.string(),
|
|
3919
|
+
count: z11.number().int().positive(),
|
|
3920
|
+
lastAt: z11.string()
|
|
3573
3921
|
})) });
|
|
3574
|
-
leasePath = (
|
|
3922
|
+
leasePath = (directory2, id) => `${directory2}/executions/${id}.json`;
|
|
3923
|
+
}
|
|
3924
|
+
});
|
|
3925
|
+
|
|
3926
|
+
// src/decisions/pending.ts
|
|
3927
|
+
var pending_exports = {};
|
|
3928
|
+
__export(pending_exports, {
|
|
3929
|
+
pendingKnowledge: () => pendingKnowledge
|
|
3930
|
+
});
|
|
3931
|
+
async function pendingKnowledge(root, now = Date.now()) {
|
|
3932
|
+
try {
|
|
3933
|
+
const [store, status] = await Promise.all([
|
|
3934
|
+
loadDecisionStore(root),
|
|
3935
|
+
execGit(
|
|
3936
|
+
["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".mason/decisions"],
|
|
3937
|
+
{ cwd: root, timeout: 1e4, maxBuffer: 1024 * 1024 }
|
|
3938
|
+
)
|
|
3939
|
+
]);
|
|
3940
|
+
const changed = /* @__PURE__ */ new Set();
|
|
3941
|
+
const entries = status.stdout.split("\0");
|
|
3942
|
+
for (let i = 0; i < entries.length; i++) {
|
|
3943
|
+
const entry = entries[i];
|
|
3944
|
+
if (!entry) continue;
|
|
3945
|
+
const file = entry.slice(3);
|
|
3946
|
+
if (/^\.mason\/decisions\/[^/]+\.json$/.test(file)) changed.add(file);
|
|
3947
|
+
if (/[RC]/.test(entry.slice(0, 2))) i++;
|
|
3948
|
+
}
|
|
3949
|
+
const records = new Map(store.records.map((record) => [`.mason/decisions/${record.id}.json`, record]));
|
|
3950
|
+
const age = (at) => {
|
|
3951
|
+
const time = at ? Date.parse(at) : NaN;
|
|
3952
|
+
return Number.isFinite(time) ? `${Math.max(0, Math.floor((now - time) / 864e5))} days` : "unknown age";
|
|
3953
|
+
};
|
|
3954
|
+
const safe = (value) => value.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ");
|
|
3955
|
+
const list = (items) => items.slice(0, 5).map(safe).join("; ") + (items.length > 5 ? `; ${items.length - 5} more` : "");
|
|
3956
|
+
const proposals = store.records.filter((record) => record.status === "active" && decisionApproval(record) === "proposed").map((record) => {
|
|
3957
|
+
const history = record.version === 2 ? record.history : [];
|
|
3958
|
+
let lastReview = -1;
|
|
3959
|
+
history.forEach((event, index2) => {
|
|
3960
|
+
if (event.kind === "accepted" || event.kind === "reaffirmed") lastReview = index2;
|
|
3961
|
+
});
|
|
3962
|
+
const since = history.slice(lastReview + 1).find((event) => event.kind === "created" || event.kind === "revised")?.at ?? record.createdAt;
|
|
3963
|
+
return { record, since };
|
|
3964
|
+
}).sort((a, b) => {
|
|
3965
|
+
const timestamp = (value) => Number.isFinite(Date.parse(value)) ? Date.parse(value) : Infinity;
|
|
3966
|
+
return timestamp(a.since) - timestamp(b.since) || a.record.id.localeCompare(b.record.id);
|
|
3967
|
+
});
|
|
3968
|
+
const lines = [];
|
|
3969
|
+
if (changed.size) lines.push(`${changed.size} uncommitted decision record(s): ${list([...changed].sort().map((file) => `${file} (saved-record age: ${age(records.get(file)?.updatedAt)})`))}.`);
|
|
3970
|
+
if (proposals.length) lines.push(`${proposals.length} proposal(s) awaiting review (oldest first): ${list(proposals.map(({ record, since }) => `.mason/decisions/${record.id}.json (${age(since)})`))}.`);
|
|
3971
|
+
if (store.diagnostics.length) lines.push(`${store.diagnostics.length} decision record(s) could not be read; pending knowledge summary is incomplete.`);
|
|
3972
|
+
return lines.length ? `Mason pending knowledge:
|
|
3973
|
+
${lines.join("\n")}
|
|
3974
|
+
Informational only; review and commit through the normal project workflow. No approval is implied.` : null;
|
|
3975
|
+
} catch {
|
|
3976
|
+
return "Mason pending knowledge: unavailable; decision records or Git status could not be read.";
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
var init_pending = __esm({
|
|
3980
|
+
"src/decisions/pending.ts"() {
|
|
3981
|
+
"use strict";
|
|
3982
|
+
init_git_read();
|
|
3983
|
+
init_decisions();
|
|
3984
|
+
init_provenance();
|
|
3575
3985
|
}
|
|
3576
3986
|
});
|
|
3577
3987
|
|
|
3578
3988
|
// src/automation/runtime.ts
|
|
3579
3989
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
3580
|
-
import { z as
|
|
3990
|
+
import { z as z12 } from "zod";
|
|
3581
3991
|
async function observeReadOnlyTool(dir, event) {
|
|
3582
3992
|
const ws = await profilePhase("automation.workspace", () => workspace(dir));
|
|
3583
3993
|
await withLock(ws.root, ws.directory, async () => {
|
|
3584
|
-
const
|
|
3585
|
-
const raw = await readStoreJson(ws.root,
|
|
3994
|
+
const statePath2 = ws.directory + "/state.json";
|
|
3995
|
+
const raw = await readStoreJson(ws.root, statePath2);
|
|
3586
3996
|
if (raw === null) return;
|
|
3587
3997
|
const state = parseState(raw);
|
|
3588
3998
|
if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
|
|
@@ -3593,7 +4003,7 @@ async function observeReadOnlyTool(dir, event) {
|
|
|
3593
4003
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3594
4004
|
session.lastUsed = now;
|
|
3595
4005
|
session.events[event.event] = { at: now, count: (session.events[event.event]?.count ?? 0) + 1 };
|
|
3596
|
-
await writeStoreJson(ws.root,
|
|
4006
|
+
await writeStoreJson(ws.root, statePath2, state);
|
|
3597
4007
|
});
|
|
3598
4008
|
return { root: ws.root, directory: ws.directory };
|
|
3599
4009
|
}
|
|
@@ -3608,7 +4018,7 @@ function summarize(report) {
|
|
|
3608
4018
|
}),
|
|
3609
4019
|
...open.length > 4 ? [`${open.length - 4} more findings in the report.`] : [],
|
|
3610
4020
|
...report.diagnostics.slice(0, 2).map(cleanText),
|
|
3611
|
-
`Evidence: ${report.reportPath}. Resume/check with mason_automation(action: "check") or mason
|
|
4021
|
+
`Evidence: ${report.reportPath}. Resume/check with mason_automation(action: "check") or mason check.`,
|
|
3612
4022
|
"Keep original evidence. Address findings relevant to the authorized task; report unrelated findings with a brief scope reason and suggested follow-up. Use review_advisory for authorized assessments; otherwise leave advisories open without approving them."
|
|
3613
4023
|
].join("\n");
|
|
3614
4024
|
}
|
|
@@ -3667,7 +4077,7 @@ async function verifyInputs(ws, inputs) {
|
|
|
3667
4077
|
async function computeAnalysis(ws, state, inputs, event, previousAudit, cacheDiagnostic) {
|
|
3668
4078
|
const baselines = [...state.baselines];
|
|
3669
4079
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3670
|
-
const
|
|
4080
|
+
const empty2 = {
|
|
3671
4081
|
version: 1,
|
|
3672
4082
|
status: "unavailable",
|
|
3673
4083
|
root: ws.root,
|
|
@@ -3684,7 +4094,7 @@ async function computeAnalysis(ws, state, inputs, event, previousAudit, cacheDia
|
|
|
3684
4094
|
};
|
|
3685
4095
|
if (!baselines.length && Object.values(inputs.docs).every((value) => value === null)) {
|
|
3686
4096
|
await verifyInputs(ws, inputs);
|
|
3687
|
-
return { baselines, report:
|
|
4097
|
+
return { baselines, report: empty2, cache: null };
|
|
3688
4098
|
}
|
|
3689
4099
|
if (ws.branch === "detached" && state.latest) {
|
|
3690
4100
|
const previous = await readStoreJson(ws.root, state.latest);
|
|
@@ -3786,7 +4196,7 @@ async function analyze(ws, inputs, event, startedAt) {
|
|
|
3786
4196
|
async function automate(dir, event) {
|
|
3787
4197
|
const startedAt = Date.now();
|
|
3788
4198
|
const ws = await profilePhase("automation.workspace", () => workspace(dir));
|
|
3789
|
-
|
|
4199
|
+
const output = await recordExecution(ws.root, ws.directory, event.event, () => withRepositoryInspection(ws.root, async () => {
|
|
3790
4200
|
const inputs = await readInputs(ws.root);
|
|
3791
4201
|
const result = await profilePhase("automation.analysis", () => analyze(ws, inputs, event, startedAt));
|
|
3792
4202
|
if (result.shared) {
|
|
@@ -3799,6 +4209,19 @@ async function automate(dir, event) {
|
|
|
3799
4209
|
const state = await loadState(ws);
|
|
3800
4210
|
if (state.analysis?.id !== result.analysis.id) throw new Error("Automation evidence changed during publication; retry against the current inputs.");
|
|
3801
4211
|
const report = structuredClone(result.analysis.report);
|
|
4212
|
+
if (!state.notifications) {
|
|
4213
|
+
let baseline = { findings: report.findings, head: report.head };
|
|
4214
|
+
if (state.latest) {
|
|
4215
|
+
try {
|
|
4216
|
+
const previous = z12.object({ findings: z12.array(repairFindingSchema), head: z12.string() }).safeParse(await readStoreJson(ws.root, state.latest));
|
|
4217
|
+
if (!previous.success) throw new Error("Unsupported saved report");
|
|
4218
|
+
baseline = previous.data;
|
|
4219
|
+
} catch {
|
|
4220
|
+
report.diagnostics.push("The saved notification baseline could not be read; initialized from the current audit. Original repair evidence is retained. Run an explicit check for all outstanding findings.");
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
state.notifications = createNotificationState(baseline.findings, baseline.head);
|
|
4224
|
+
}
|
|
3802
4225
|
if (result.shared) report.checks = { ...report.checks, ran: [], reused: [.../* @__PURE__ */ new Set([...report.checks.ran, ...report.checks.reused])], shared: true };
|
|
3803
4226
|
report.reportPath = ws.directory + "/checks/" + randomUUID4() + ".json";
|
|
3804
4227
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -3811,12 +4234,8 @@ async function automate(dir, event) {
|
|
|
3811
4234
|
if (Object.keys(state.sessions).length >= 128) throw new Error("Too many sessions with unfinished tool calls; original evidence was retained.");
|
|
3812
4235
|
state.sessions[key] = {
|
|
3813
4236
|
host: event.host,
|
|
3814
|
-
|
|
3815
|
-
continued: false,
|
|
3816
|
-
initialIssues: report.findings.filter((f) => f.status === "unresolved").map((f) => f.id),
|
|
3817
|
-
initialDocs: inputs.docs,
|
|
4237
|
+
notifiedDiagnostics: [],
|
|
3818
4238
|
lastUsed: now,
|
|
3819
|
-
mutationObserved: false,
|
|
3820
4239
|
pending: {},
|
|
3821
4240
|
coverageGaps: [],
|
|
3822
4241
|
events: {}
|
|
@@ -3831,7 +4250,6 @@ async function automate(dir, event) {
|
|
|
3831
4250
|
session.pending[event.toolId] = inputs.fingerprint;
|
|
3832
4251
|
}
|
|
3833
4252
|
if (event.event === "after_tool" && event.mutating) {
|
|
3834
|
-
session.mutationObserved = true;
|
|
3835
4253
|
if (!event.toolId || !session.pending[event.toolId]) {
|
|
3836
4254
|
const gap = "A tool completed without an observed matching pre-tool capture in this checkout; pre-edit coverage is unknown. The tool may have changed checkouts; captures are kept separately for each worktree.";
|
|
3837
4255
|
if (!session.coverageGaps.includes(gap)) session.coverageGaps.push(gap);
|
|
@@ -3842,31 +4260,45 @@ async function automate(dir, event) {
|
|
|
3842
4260
|
if (session.coverageGaps.length && report.status !== "unavailable") report.status = "incomplete";
|
|
3843
4261
|
report.capture = report.status !== "unavailable" && !session.coverageGaps.length && (session.events.session_start || session.events.before_tool) ? "observed" : "unknown";
|
|
3844
4262
|
}
|
|
3845
|
-
const
|
|
3846
|
-
|
|
3847
|
-
const
|
|
3848
|
-
const
|
|
3849
|
-
if (session)
|
|
3850
|
-
session.
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
const
|
|
4263
|
+
const selected = session && event.event === "task_end" ? completionFindings(state.notifications, report.findings) : [];
|
|
4264
|
+
state.agentNotifications ??= { ...structuredClone(state.notifications), delivered: {} };
|
|
4265
|
+
const feedback = session && ["turn_start", "after_tool"].includes(event.event) ? completionFindings(state.agentNotifications, report.findings) : [];
|
|
4266
|
+
const diagnostics = session ? report.diagnostics.filter((d) => !session.notifiedDiagnostics.includes(d)).slice(0, 2) : [];
|
|
4267
|
+
if (session) session.notifiedDiagnostics = [.../* @__PURE__ */ new Set([
|
|
4268
|
+
...session.notifiedDiagnostics.filter((d) => report.diagnostics.includes(d)),
|
|
4269
|
+
...diagnostics
|
|
4270
|
+
])];
|
|
4271
|
+
const message2 = !session ? summarize(report) : [
|
|
4272
|
+
completionSummary(selected, report.reportPath),
|
|
4273
|
+
completionSummary(feedback, report.reportPath),
|
|
4274
|
+
...diagnostics.length ? [`Mason: verification limited. ${diagnostics.slice(0, 2).map(cleanText).join(" ")} Evidence: ${report.reportPath}.`] : []
|
|
4275
|
+
].filter(Boolean).join("\n") || null;
|
|
4276
|
+
const notify = message2 !== null;
|
|
4277
|
+
const persistReport = !state.latest || state.fingerprint !== inputs.fingerprint || notify || newSession || event.event === "task_end";
|
|
3854
4278
|
if (!persistReport) report.reportPath = state.latest;
|
|
3855
4279
|
state.updatedAt = now;
|
|
3856
4280
|
state.fingerprint = inputs.fingerprint;
|
|
3857
4281
|
state.latest = report.reportPath;
|
|
3858
4282
|
if (persistReport) await writeStoreJson(ws.root, report.reportPath, report);
|
|
3859
4283
|
await writeStoreJson(ws.root, ws.directory + "/state.json", state);
|
|
3860
|
-
return { report, message:
|
|
4284
|
+
return { report, message: message2, observedAt: now, delivery: { agent: feedback.slice(0, 4), completion: selected.slice(0, 4) } };
|
|
3861
4285
|
}));
|
|
3862
4286
|
}));
|
|
4287
|
+
try {
|
|
4288
|
+
await observeUsefulness(ws.root, event, output.report, output.delivery.agent, output.delivery.completion, Date.now() - startedAt, output.observedAt);
|
|
4289
|
+
} catch {
|
|
4290
|
+
if (event.event === "task_end") output.message = [output.message, "Mason: local usefulness tracking unavailable; inspect mason stats --json."].filter(Boolean).join("\n");
|
|
4291
|
+
}
|
|
4292
|
+
return output;
|
|
3863
4293
|
}
|
|
3864
4294
|
async function automationStatus(dir) {
|
|
4295
|
+
const { pendingKnowledge: pendingKnowledge2 } = await Promise.resolve().then(() => (init_pending(), pending_exports));
|
|
3865
4296
|
const ws = await profilePhase("automation.workspace", () => workspace(dir));
|
|
4297
|
+
const knowledge = await pendingKnowledge2(ws.root);
|
|
3866
4298
|
const execution = await executionStatus(ws.root, ws.directory);
|
|
3867
4299
|
const notifications = await failureNotifications(ws.root, ws.directory);
|
|
3868
4300
|
const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
|
|
3869
|
-
if (raw === null) return { version: 1, status: execution.status === "not-observed" ? "not-observed" : "unavailable", root: ws.root, branch: ws.branch, baselinePaths: [], hosts: {}, execution, notifications };
|
|
4301
|
+
if (raw === null) return { version: 1, status: execution.status === "not-observed" ? "not-observed" : "unavailable", root: ws.root, branch: ws.branch, baselinePaths: [], hosts: {}, execution, notifications, knowledge };
|
|
3870
4302
|
const state = parseState(raw);
|
|
3871
4303
|
if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) throw new Error("Automation state belongs to another workspace.");
|
|
3872
4304
|
const inputs = await readInputs(ws.root);
|
|
@@ -3889,6 +4321,8 @@ async function automationStatus(dir) {
|
|
|
3889
4321
|
hosts: hosts2,
|
|
3890
4322
|
execution,
|
|
3891
4323
|
notifications,
|
|
4324
|
+
knowledge,
|
|
4325
|
+
notificationBaselineHead: state.notifications?.baselineHead ?? null,
|
|
3892
4326
|
note: "Observed events do not prove all tool paths are intercepted. Run check to verify the retained evidence."
|
|
3893
4327
|
};
|
|
3894
4328
|
}
|
|
@@ -3896,6 +4330,8 @@ var SCOPE, priority, cleanText, analysisSchema, sharedSchema, baselineDigest;
|
|
|
3896
4330
|
var init_runtime = __esm({
|
|
3897
4331
|
"src/automation/runtime.ts"() {
|
|
3898
4332
|
"use strict";
|
|
4333
|
+
init_usefulness();
|
|
4334
|
+
init_notifications();
|
|
3899
4335
|
init_profile();
|
|
3900
4336
|
init_inspection();
|
|
3901
4337
|
init_execution();
|
|
@@ -3907,34 +4343,34 @@ var init_runtime = __esm({
|
|
|
3907
4343
|
SCOPE = "Documentation audit evidence only. Hook receipts show observed events, not complete interception. Resolved findings no longer fail their checks or have a recorded assessment covering current evidence. Repair and review only within the user's task authorization.";
|
|
3908
4344
|
priority = { resolved: 0, "review-required": 1, unresolved: 2, unverified: 3 };
|
|
3909
4345
|
cleanText = (text2) => text2.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 250);
|
|
3910
|
-
analysisSchema =
|
|
3911
|
-
id:
|
|
3912
|
-
fingerprint:
|
|
3913
|
-
completedAt:
|
|
3914
|
-
baselineDigest:
|
|
4346
|
+
analysisSchema = z12.object({
|
|
4347
|
+
id: z12.string(),
|
|
4348
|
+
fingerprint: z12.string(),
|
|
4349
|
+
completedAt: z12.number(),
|
|
4350
|
+
baselineDigest: z12.string(),
|
|
3915
4351
|
currentAudit: reportSchema.nullable().optional(),
|
|
3916
|
-
report:
|
|
3917
|
-
version:
|
|
3918
|
-
status:
|
|
3919
|
-
root:
|
|
3920
|
-
branch:
|
|
3921
|
-
head:
|
|
3922
|
-
baselinePaths:
|
|
3923
|
-
reportPath:
|
|
3924
|
-
findings:
|
|
3925
|
-
diagnostics:
|
|
3926
|
-
checks:
|
|
3927
|
-
ran:
|
|
3928
|
-
reused:
|
|
3929
|
-
skipped:
|
|
3930
|
-
auditReused:
|
|
4352
|
+
report: z12.object({
|
|
4353
|
+
version: z12.literal(1),
|
|
4354
|
+
status: z12.enum(["verified", "issues-remain", "incomplete", "unavailable"]),
|
|
4355
|
+
root: z12.string(),
|
|
4356
|
+
branch: z12.string(),
|
|
4357
|
+
head: z12.string(),
|
|
4358
|
+
baselinePaths: z12.array(z12.string()),
|
|
4359
|
+
reportPath: z12.string(),
|
|
4360
|
+
findings: z12.array(repairFindingSchema),
|
|
4361
|
+
diagnostics: z12.array(z12.string()),
|
|
4362
|
+
checks: z12.object({
|
|
4363
|
+
ran: z12.array(z12.string()),
|
|
4364
|
+
reused: z12.array(z12.string()),
|
|
4365
|
+
skipped: z12.array(z12.object({ check: z12.string(), reason: z12.string(), doc: z12.string().optional() })),
|
|
4366
|
+
auditReused: z12.boolean().optional()
|
|
3931
4367
|
}),
|
|
3932
|
-
counts:
|
|
3933
|
-
capture:
|
|
3934
|
-
scope:
|
|
4368
|
+
counts: z12.object({ resolved: z12.number(), unresolved: z12.number(), "review-required": z12.number(), unverified: z12.number() }),
|
|
4369
|
+
capture: z12.enum(["observed", "unknown"]),
|
|
4370
|
+
scope: z12.string()
|
|
3935
4371
|
})
|
|
3936
4372
|
});
|
|
3937
|
-
sharedSchema =
|
|
4373
|
+
sharedSchema = z12.object({ analysis: analysisSchema, digest: z12.string() });
|
|
3938
4374
|
baselineDigest = async (ws, state) => profilePhase("automation.baselines", async () => hash(await Promise.all(
|
|
3939
4375
|
state.baselines.map(async (b) => [b.path, await readStoreJson(ws.root, b.path)])
|
|
3940
4376
|
)));
|
|
@@ -3942,12 +4378,12 @@ var init_runtime = __esm({
|
|
|
3942
4378
|
});
|
|
3943
4379
|
|
|
3944
4380
|
// src/automation/hook-root.ts
|
|
3945
|
-
import
|
|
4381
|
+
import fs16 from "fs/promises";
|
|
3946
4382
|
import path23 from "path";
|
|
3947
4383
|
async function repositoryRoot(dir) {
|
|
3948
4384
|
if (!path23.isAbsolute(dir)) throw new Error("Hook input working directory must be absolute.");
|
|
3949
4385
|
try {
|
|
3950
|
-
return await
|
|
4386
|
+
return await fs16.realpath((await git3(dir, "rev-parse", "--show-toplevel")).trim());
|
|
3951
4387
|
} catch (error) {
|
|
3952
4388
|
if (error.code === "ENOENT" || /not a git repository/.test(String(error.stderr))) return null;
|
|
3953
4389
|
throw error;
|
|
@@ -3975,8 +4411,8 @@ var init_hook_root = __esm({
|
|
|
3975
4411
|
});
|
|
3976
4412
|
|
|
3977
4413
|
// src/setup/model.ts
|
|
3978
|
-
import { z as
|
|
3979
|
-
import
|
|
4414
|
+
import { z as z13 } from "zod";
|
|
4415
|
+
import fs17 from "fs/promises";
|
|
3980
4416
|
import path24 from "path";
|
|
3981
4417
|
async function loadSetup(root) {
|
|
3982
4418
|
const raw = await readStoreJson(root, SETUP_PATH);
|
|
@@ -3986,20 +4422,20 @@ async function loadSetup(root) {
|
|
|
3986
4422
|
async function effectiveSetup(root) {
|
|
3987
4423
|
const local = await loadSetup(root);
|
|
3988
4424
|
if (local) return { root, setup: local };
|
|
3989
|
-
const common = await
|
|
3990
|
-
const own = await
|
|
4425
|
+
const common = await fs17.realpath(path24.resolve(root, (await git3(root, "rev-parse", "--git-common-dir")).trim()));
|
|
4426
|
+
const own = await fs17.realpath((await git3(root, "rev-parse", "--absolute-git-dir")).trim());
|
|
3991
4427
|
if (common === own) return { root, setup: null };
|
|
3992
4428
|
const primary = (await git3(root, "worktree", "list", "--porcelain", "-z")).split("\0\0")[0].split("\0");
|
|
3993
4429
|
const candidate = primary.find((field) => field.startsWith("worktree "))?.slice(9);
|
|
3994
4430
|
if (!candidate || primary.includes("bare")) return { root, setup: null };
|
|
3995
4431
|
let inheritedRoot;
|
|
3996
4432
|
try {
|
|
3997
|
-
inheritedRoot = await
|
|
4433
|
+
inheritedRoot = await fs17.realpath((await git3(candidate, "rev-parse", "--show-toplevel")).trim());
|
|
3998
4434
|
} catch (error) {
|
|
3999
4435
|
if (/must be run in a work tree/.test(String(error.stderr))) return { root, setup: null };
|
|
4000
4436
|
throw error;
|
|
4001
4437
|
}
|
|
4002
|
-
const inheritedCommon = await
|
|
4438
|
+
const inheritedCommon = await fs17.realpath(path24.resolve(
|
|
4003
4439
|
inheritedRoot,
|
|
4004
4440
|
(await git3(inheritedRoot, "rev-parse", "--git-common-dir")).trim()
|
|
4005
4441
|
));
|
|
@@ -4009,8 +4445,8 @@ async function effectiveSetup(root) {
|
|
|
4009
4445
|
if (inactive && setup) for (const host of inactiveHostsSchema.parse(inactive).hosts) delete setup.hosts[host];
|
|
4010
4446
|
return { root: inheritedRoot, setup };
|
|
4011
4447
|
}
|
|
4012
|
-
async function loadSetupReceipt(root,
|
|
4013
|
-
const raw = await readStoreJson(root,
|
|
4448
|
+
async function loadSetupReceipt(root, directory2, host) {
|
|
4449
|
+
const raw = await readStoreJson(root, directory2 + "/setup-" + host + ".json");
|
|
4014
4450
|
if (raw === null) return null;
|
|
4015
4451
|
const receipt = receiptSchema.parse(raw);
|
|
4016
4452
|
if (receipt.root !== root || receipt.host !== host) throw new Error("Setup receipt belongs to another installation.");
|
|
@@ -4022,29 +4458,29 @@ var init_model = __esm({
|
|
|
4022
4458
|
"use strict";
|
|
4023
4459
|
init_evidence();
|
|
4024
4460
|
init_storage();
|
|
4025
|
-
setupHostSchema =
|
|
4026
|
-
configuredVersion:
|
|
4027
|
-
revision:
|
|
4028
|
-
fingerprint:
|
|
4029
|
-
mcpFingerprint:
|
|
4030
|
-
instructions:
|
|
4461
|
+
setupHostSchema = z13.object({
|
|
4462
|
+
configuredVersion: z13.string(),
|
|
4463
|
+
revision: z13.string().uuid(),
|
|
4464
|
+
fingerprint: z13.string(),
|
|
4465
|
+
mcpFingerprint: z13.string(),
|
|
4466
|
+
instructions: z13.array(z13.string())
|
|
4031
4467
|
});
|
|
4032
|
-
setupSchema =
|
|
4468
|
+
setupSchema = z13.object({ version: z13.literal(2), hosts: z13.object({
|
|
4033
4469
|
codex: setupHostSchema.optional(),
|
|
4034
4470
|
claude: setupHostSchema.optional()
|
|
4035
4471
|
}) });
|
|
4036
4472
|
SETUP_PATH = ".mason/local/setup.json";
|
|
4037
4473
|
INACTIVE_HOSTS_PATH = ".mason/local/inactive-hosts.json";
|
|
4038
|
-
inactiveHostsSchema =
|
|
4039
|
-
receiptSchema =
|
|
4040
|
-
version:
|
|
4041
|
-
host:
|
|
4042
|
-
status:
|
|
4043
|
-
initialReportPath:
|
|
4044
|
-
initialBaselinePaths:
|
|
4045
|
-
root:
|
|
4046
|
-
revision:
|
|
4047
|
-
configuredAt:
|
|
4474
|
+
inactiveHostsSchema = z13.object({ version: z13.literal(1), hosts: z13.array(z13.enum(["claude", "codex"])) });
|
|
4475
|
+
receiptSchema = z13.object({
|
|
4476
|
+
version: z13.literal(1),
|
|
4477
|
+
host: z13.enum(["codex", "claude"]),
|
|
4478
|
+
status: z13.enum(["installing", "configured"]),
|
|
4479
|
+
initialReportPath: z13.string(),
|
|
4480
|
+
initialBaselinePaths: z13.array(z13.string()),
|
|
4481
|
+
root: z13.string(),
|
|
4482
|
+
revision: z13.string().uuid(),
|
|
4483
|
+
configuredAt: z13.string().optional()
|
|
4048
4484
|
});
|
|
4049
4485
|
}
|
|
4050
4486
|
});
|
|
@@ -4062,7 +4498,7 @@ __export(launcher_exports, {
|
|
|
4062
4498
|
});
|
|
4063
4499
|
import { execFile as execFile2 } from "child_process";
|
|
4064
4500
|
import { promisify as promisify2 } from "util";
|
|
4065
|
-
import
|
|
4501
|
+
import fs18 from "fs/promises";
|
|
4066
4502
|
function globalCommand(args2, platform = process.platform) {
|
|
4067
4503
|
return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args2] } : { command: "mason", args: args2 };
|
|
4068
4504
|
}
|
|
@@ -4082,7 +4518,7 @@ async function installedCommand() {
|
|
|
4082
4518
|
}
|
|
4083
4519
|
}
|
|
4084
4520
|
async function prepareLaunch(host, options = {}) {
|
|
4085
|
-
const root = await
|
|
4521
|
+
const root = await fs18.realpath((await git3(options.dir ?? process.cwd(), "rev-parse", "--show-toplevel")).trim());
|
|
4086
4522
|
const entry = (await effectiveSetup(root)).setup?.hosts[host];
|
|
4087
4523
|
process.env.MASON_SETUP_ROOT = root;
|
|
4088
4524
|
process.env.MASON_SETUP_HOST = host;
|
|
@@ -4105,7 +4541,7 @@ var init_launcher = __esm({
|
|
|
4105
4541
|
"use strict";
|
|
4106
4542
|
init_evidence();
|
|
4107
4543
|
init_model();
|
|
4108
|
-
executingVersion = () => true ? "0.
|
|
4544
|
+
executingVersion = () => true ? "0.20.1" : "development";
|
|
4109
4545
|
mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
|
|
4110
4546
|
hookCommand = (host) => `mason --setup-host ${host} auto`;
|
|
4111
4547
|
}
|
|
@@ -4118,13 +4554,13 @@ __export(observations_exports, {
|
|
|
4118
4554
|
observeActivation: () => observeActivation,
|
|
4119
4555
|
readObservation: () => readObservation
|
|
4120
4556
|
});
|
|
4121
|
-
import
|
|
4122
|
-
import { z as
|
|
4123
|
-
function observationPath(
|
|
4124
|
-
return `${
|
|
4557
|
+
import fs19 from "fs/promises";
|
|
4558
|
+
import { z as z14 } from "zod";
|
|
4559
|
+
function observationPath(directory2, host, revision, version = executingVersion()) {
|
|
4560
|
+
return `${directory2}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
|
|
4125
4561
|
}
|
|
4126
|
-
async function readObservation(root,
|
|
4127
|
-
const raw = await readStoreJson(root, observationPath(
|
|
4562
|
+
async function readObservation(root, directory2, host, revision, version = executingVersion()) {
|
|
4563
|
+
const raw = await readStoreJson(root, observationPath(directory2, host, revision, version));
|
|
4128
4564
|
if (raw === null) return null;
|
|
4129
4565
|
const record = observationSchema.parse(raw);
|
|
4130
4566
|
if (record.root !== root || record.host !== host || record.revision !== revision || record.masonVersion !== version) throw new Error("Activation receipt belongs to another installation.");
|
|
@@ -4141,12 +4577,12 @@ async function observeActivation(dir, event, options = {}) {
|
|
|
4141
4577
|
return inactiveSetupMessage();
|
|
4142
4578
|
}
|
|
4143
4579
|
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];
|
|
4144
|
-
const ws = capturedDirectory ? { root: await
|
|
4145
|
-
if (ws.root !== await
|
|
4580
|
+
const ws = capturedDirectory ? { root: await fs19.realpath(dir), directory: capturedDirectory } : await workspace(dir);
|
|
4581
|
+
if (ws.root !== await fs19.realpath(process.env.MASON_SETUP_ROOT)) return null;
|
|
4146
4582
|
const { setup } = await effectiveSetup(ws.root);
|
|
4147
4583
|
if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
|
|
4148
|
-
const
|
|
4149
|
-
await withLock(ws.root,
|
|
4584
|
+
const directory2 = ws.directory + "/activation";
|
|
4585
|
+
await withLock(ws.root, directory2, async () => {
|
|
4150
4586
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4151
4587
|
const record = await readObservation(ws.root, ws.directory, host, revision) ?? {
|
|
4152
4588
|
version: 1,
|
|
@@ -4190,26 +4626,26 @@ var init_observations = __esm({
|
|
|
4190
4626
|
init_storage();
|
|
4191
4627
|
init_model();
|
|
4192
4628
|
init_launcher();
|
|
4193
|
-
observationSchema =
|
|
4194
|
-
version:
|
|
4195
|
-
masonVersion:
|
|
4196
|
-
root:
|
|
4197
|
-
revision:
|
|
4198
|
-
host:
|
|
4199
|
-
contextCalls:
|
|
4200
|
-
lastContextAt:
|
|
4201
|
-
sessions:
|
|
4202
|
-
events:
|
|
4203
|
-
at:
|
|
4204
|
-
verificationStatus:
|
|
4205
|
-
reportPath:
|
|
4629
|
+
observationSchema = z14.object({
|
|
4630
|
+
version: z14.literal(1),
|
|
4631
|
+
masonVersion: z14.string(),
|
|
4632
|
+
root: z14.string(),
|
|
4633
|
+
revision: z14.string(),
|
|
4634
|
+
host: z14.enum(["codex", "claude"]),
|
|
4635
|
+
contextCalls: z14.number().int().nonnegative(),
|
|
4636
|
+
lastContextAt: z14.string().optional(),
|
|
4637
|
+
sessions: z14.record(z14.object({
|
|
4638
|
+
events: z14.array(z14.enum(events)),
|
|
4639
|
+
at: z14.string(),
|
|
4640
|
+
verificationStatus: z14.string().optional(),
|
|
4641
|
+
reportPath: z14.string().optional()
|
|
4206
4642
|
}))
|
|
4207
4643
|
});
|
|
4208
4644
|
}
|
|
4209
4645
|
});
|
|
4210
4646
|
|
|
4211
4647
|
// src/automation/adapters.ts
|
|
4212
|
-
import { z as
|
|
4648
|
+
import { z as z15 } from "zod";
|
|
4213
4649
|
function normalizeHook(host, raw) {
|
|
4214
4650
|
hostSchema.parse(host);
|
|
4215
4651
|
const input = inputSchema.parse(raw);
|
|
@@ -4220,7 +4656,7 @@ function normalizeHook(host, raw) {
|
|
|
4220
4656
|
sessionId: input.session_id,
|
|
4221
4657
|
toolId: input.tool_use_id,
|
|
4222
4658
|
mutating: !!input.tool_name && !readOnly,
|
|
4223
|
-
|
|
4659
|
+
...input.hook_event_name === "PostToolUse" ? { retrievedDecisionIds: retrievedDecisions(input.tool_name, input.tool_response) } : {}
|
|
4224
4660
|
} };
|
|
4225
4661
|
}
|
|
4226
4662
|
async function runAutomationHook(host, stdin, options = {}) {
|
|
@@ -4258,7 +4694,7 @@ async function runAutomationHook(host, stdin, options = {}) {
|
|
|
4258
4694
|
if (warning) result.message = [result.message, warning].filter(Boolean).join("\n");
|
|
4259
4695
|
if (!result.message) return null;
|
|
4260
4696
|
if (name === "Stop") {
|
|
4261
|
-
return
|
|
4697
|
+
return { systemMessage: result.message };
|
|
4262
4698
|
}
|
|
4263
4699
|
return { hookSpecificOutput: { hookEventName: name, additionalContext: result.message } };
|
|
4264
4700
|
} catch (error) {
|
|
@@ -4277,7 +4713,7 @@ function knownManagedHookCommands(host) {
|
|
|
4277
4713
|
function compatibleManagedHookCommands(host, platform = process.platform) {
|
|
4278
4714
|
return [managedHookCommand(host, platform), `mason --setup-host ${host} auto hook --host ${host}`];
|
|
4279
4715
|
}
|
|
4280
|
-
function hookConfig(host, command2 = "npx --no-install --package mason-context mason
|
|
4716
|
+
function hookConfig(host, command2 = "npx --no-install --package mason-context mason auto") {
|
|
4281
4717
|
const handler = { type: "command", command: command2 === `mason --setup-host ${host} auto` ? managedHookCommand(host) : command2 + " hook --host " + host, timeout: 30 };
|
|
4282
4718
|
return { hooks: Object.fromEntries(HOOK_EVENTS.map((name) => [
|
|
4283
4719
|
name,
|
|
@@ -4288,19 +4724,21 @@ var inputSchema, lifecycle, HOOK_EVENTS;
|
|
|
4288
4724
|
var init_adapters = __esm({
|
|
4289
4725
|
"src/automation/adapters.ts"() {
|
|
4290
4726
|
"use strict";
|
|
4727
|
+
init_usefulness();
|
|
4291
4728
|
init_execution();
|
|
4292
4729
|
init_runtime();
|
|
4293
4730
|
init_store();
|
|
4294
4731
|
init_hook_root();
|
|
4295
|
-
inputSchema =
|
|
4296
|
-
cwd:
|
|
4297
|
-
session_id:
|
|
4298
|
-
hook_event_name:
|
|
4299
|
-
tool_name:
|
|
4300
|
-
tool_use_id:
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4732
|
+
inputSchema = z15.object({
|
|
4733
|
+
cwd: z15.string().min(1),
|
|
4734
|
+
session_id: z15.string().min(1).max(500),
|
|
4735
|
+
hook_event_name: z15.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
|
|
4736
|
+
tool_name: z15.string().optional(),
|
|
4737
|
+
tool_use_id: z15.string().max(500).optional(),
|
|
4738
|
+
tool_response: z15.unknown().optional(),
|
|
4739
|
+
tool_input: z15.unknown().optional(),
|
|
4740
|
+
stop_hook_active: z15.boolean().optional(),
|
|
4741
|
+
permission_mode: z15.string().optional()
|
|
4304
4742
|
});
|
|
4305
4743
|
lifecycle = {
|
|
4306
4744
|
SessionStart: "session_start",
|
|
@@ -4320,7 +4758,7 @@ __export(files_exports, {
|
|
|
4320
4758
|
managedBlock: () => managedBlock,
|
|
4321
4759
|
readText: () => readText
|
|
4322
4760
|
});
|
|
4323
|
-
import
|
|
4761
|
+
import fs20 from "fs/promises";
|
|
4324
4762
|
import path25 from "path";
|
|
4325
4763
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
4326
4764
|
async function readText(root, file) {
|
|
@@ -4350,14 +4788,14 @@ async function applyEdit(root, edit) {
|
|
|
4350
4788
|
if (edit.after === null) {
|
|
4351
4789
|
const file2 = await storePath(root, edit.path);
|
|
4352
4790
|
if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
|
|
4353
|
-
await
|
|
4791
|
+
await fs20.unlink(file2);
|
|
4354
4792
|
return true;
|
|
4355
4793
|
}
|
|
4356
4794
|
const file = await storePath(root, edit.path, true);
|
|
4357
4795
|
const temporary = path25.join(path25.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
|
|
4358
4796
|
try {
|
|
4359
|
-
const mode = await
|
|
4360
|
-
const handle = await
|
|
4797
|
+
const mode = await fs20.stat(file).then((s) => s.mode & 511, () => 384);
|
|
4798
|
+
const handle = await fs20.open(temporary, "wx", mode);
|
|
4361
4799
|
try {
|
|
4362
4800
|
await handle.writeFile(edit.after, "utf8");
|
|
4363
4801
|
await handle.sync();
|
|
@@ -4365,10 +4803,10 @@ async function applyEdit(root, edit) {
|
|
|
4365
4803
|
await handle.close();
|
|
4366
4804
|
}
|
|
4367
4805
|
if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
|
|
4368
|
-
await
|
|
4806
|
+
await fs20.rename(temporary, file);
|
|
4369
4807
|
return true;
|
|
4370
4808
|
} finally {
|
|
4371
|
-
await
|
|
4809
|
+
await fs20.rm(temporary, { force: true });
|
|
4372
4810
|
}
|
|
4373
4811
|
}
|
|
4374
4812
|
var init_files2 = __esm({
|
|
@@ -4389,7 +4827,7 @@ __export(ownership_exports, {
|
|
|
4389
4827
|
ownershipEdit: () => ownershipEdit,
|
|
4390
4828
|
ownershipSchema: () => ownershipSchema
|
|
4391
4829
|
});
|
|
4392
|
-
import { z as
|
|
4830
|
+
import { z as z16 } from "zod";
|
|
4393
4831
|
import { parse } from "smol-toml";
|
|
4394
4832
|
function blockSpan(text2, start, end) {
|
|
4395
4833
|
const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
|
|
@@ -4440,30 +4878,30 @@ var init_ownership = __esm({
|
|
|
4440
4878
|
["<!-- mason:agents:start -->", "<!-- mason:agents:end -->"],
|
|
4441
4879
|
["# mason:mcp:start", "# mason:mcp:end"]
|
|
4442
4880
|
];
|
|
4443
|
-
fileSchema =
|
|
4444
|
-
created:
|
|
4445
|
-
initialHash:
|
|
4446
|
-
blocks:
|
|
4447
|
-
mcpHash:
|
|
4448
|
-
hookCommands:
|
|
4449
|
-
hooks:
|
|
4881
|
+
fileSchema = z16.object({
|
|
4882
|
+
created: z16.boolean(),
|
|
4883
|
+
initialHash: z16.string(),
|
|
4884
|
+
blocks: z16.record(z16.string()),
|
|
4885
|
+
mcpHash: z16.string().optional(),
|
|
4886
|
+
hookCommands: z16.array(z16.string()).optional(),
|
|
4887
|
+
hooks: z16.record(z16.array(z16.string())).optional()
|
|
4450
4888
|
});
|
|
4451
|
-
ownershipSchema =
|
|
4889
|
+
ownershipSchema = z16.object({ version: z16.literal(1), files: z16.record(fileSchema) });
|
|
4452
4890
|
}
|
|
4453
4891
|
});
|
|
4454
4892
|
|
|
4455
4893
|
// src/mcp/init.ts
|
|
4456
|
-
import { z as
|
|
4894
|
+
import { z as z17 } from "zod";
|
|
4457
4895
|
var markerSchema, CLAUDE_MD_SECTION, ASSISTANT_SETUP, QUICKSTART_PLAYBOOK, MAP_PLAYBOOK;
|
|
4458
4896
|
var init_init = __esm({
|
|
4459
4897
|
"src/mcp/init.ts"() {
|
|
4460
4898
|
"use strict";
|
|
4461
4899
|
init_storage();
|
|
4462
4900
|
init_files2();
|
|
4463
|
-
markerSchema =
|
|
4464
|
-
version:
|
|
4465
|
-
initializedAt:
|
|
4466
|
-
features:
|
|
4901
|
+
markerSchema = z17.object({
|
|
4902
|
+
version: z17.literal(1),
|
|
4903
|
+
initializedAt: z17.string(),
|
|
4904
|
+
features: z17.object({ confluence: z17.boolean().optional() }).passthrough().optional()
|
|
4467
4905
|
}).passthrough();
|
|
4468
4906
|
CLAUDE_MD_SECTION = `<!-- mason:start -->
|
|
4469
4907
|
## Mason project knowledge
|
|
@@ -4472,9 +4910,10 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
|
|
|
4472
4910
|
|
|
4473
4911
|
- Task, bug, change request, or investigation of why the project works this way \u2192 \`get_context\` with the task text and known files: matching decisions, related tests, impact, and any available map entries.
|
|
4474
4912
|
- Before editing a file \u2192 \`get_impact\` to check references, tests, and historical change partners.
|
|
4475
|
-
- Investigation or correction reveals a reusable project lesson \u2192 \`save_decision\`
|
|
4913
|
+
- Investigation or correction reveals a reusable project lesson \u2192 \`save_decision\` as soon as the lesson is supported by evidence; do not wait for implementation, task completion, or merge. Record the established observation now and identify unimplemented remedies as tentative. Capture only the lasting constraint and reason, with anchors and known sources, owner, and recorder. This creates a proposal; never infer approval. Preserve uncertain causes and unknown attribution. Route project lessons here; keep personal preferences in agent memory. Skip code summaries, current checkout status, temporary task permissions, session trivia, and secrets. No qualifying lesson means no record.
|
|
4476
4914
|
- Before saving, compare the lesson with matching \`get_context\` records and any pending proposal. If new evidence changes the same lesson's assumptions, scope, or recommended action, revise it with \`save_decision(id: existingId)\`, including when it is accepted. Preserve supported rationale and sources, replace obsolete instructions, and add the new evidence. Create a separate record for a genuinely distinct lesson; skip unchanged restatements. Revision preserves history and leaves the accepted version operative until authorized review.
|
|
4477
4915
|
- Consult trust metadata before relying on entries: unknown or changed freshness requires inspection, stale or unknown map verification requires a fresh evidence review, and a recorded failed verdict means the description must be corrected. Check approval too: proposals are suggestions, legacy records are unreviewed, and accepted decisions are recorded constraints subject to freshness checks. An accepted revision remains operative while a pending proposal is reviewed; keep both versions and their freshness distinct.
|
|
4916
|
+
- Asked to review pending project knowledge \u2192 \`mason_automation(action: "status")\` lists pending proposals and uncommitted records. For ordinary tasks, inspect matching proposals through \`get_context\`; do not request acceptance merely because a proposal exists.
|
|
4478
4917
|
- Asked to review or re-verify a decision \u2192 \`review_decision\` first to inspect content, sources, history, and code changes. Record acceptance, reaffirmation, or retirement only when authorized by the user or a cited team review, with the actual reviewer and reason. Never infer approval from unchanged code. Review and commit the local record through the normal project workflow.
|
|
4479
4918
|
- For an architectural overview, use \`get_snapshot\` if a map is available. If \`map.status\` is missing or invalid, use available decisions and source evidence; do not start building a map unless requested.
|
|
4480
4919
|
- \`mason_init\` returns documentation audit and committed-diff review results, plus a short setup guide. Pass \`evidence\` with local CI manifest paths to include test and analysis results; the CLI equivalent is \`mason-review --evidence <manifest>\`. State skipped, unavailable, stale, or unknown checks explicitly. Related accepted decisions identify review context, not proven violations.
|
|
@@ -4484,13 +4923,13 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
|
|
|
4484
4923
|
|
|
4485
4924
|
- For a dependency advisory, \`review_advisory(action: "dismiss", baselinePath, findingId, reasonCode, note)\` records a one-step dismissal without reviewer or token. Use \`no-dependency-claims\` for a document with no dependency claims (reopens on document content changes), or \`unrelated-manifest-change\` for reviewed changes that do not affect its guidance (reopens on document or manifest changes). Keep the relevant scope committed; commit the dismissal to share it. This never approves a decision.
|
|
4486
4925
|
|
|
4487
|
-
- When Mason automation is installed, use \`mason_automation(action: "status")\` to inspect configured hooks and observed events, and \`mason_automation(action: "check")\` to resume its retained repair evidence. CLI fallback: \`mason
|
|
4926
|
+
- When Mason automation is installed, use \`mason_automation(action: "status")\` to inspect configured hooks and observed events, and \`mason_automation(action: "check")\` to resume its retained repair evidence. CLI fallback: \`mason status\` / \`mason check\`. Preserve existing baselines across sessions. Automatic checks do not authorize unrelated repairs or approve advisories.
|
|
4488
4927
|
|
|
4489
4928
|
Inspect source for what the retrieved context does not answer.
|
|
4490
4929
|
<!-- mason:end -->`;
|
|
4491
4930
|
ASSISTANT_SETUP = `UNIFIED SETUP
|
|
4492
|
-
When the user requests Mason setup, call mason_init again with mode: "setup" and host: "codex" or "claude" for the assistant being configured. This uses the same engine as mason
|
|
4493
|
-
If the user only requested inspection or review, report findings without running setup. The setup result distinguishes configured from observed activation. Explain the host's native trust step and request a new session; never trust hooks on the user's behalf or claim activation from generated configuration alone. After a normal task finishes, mason_automation(action: "status") or mason
|
|
4931
|
+
When the user requests Mason setup, call mason_init again with mode: "setup" and host: "codex" or "claude" for the assistant being configured. This uses the same engine as mason setup: retain the initial audit before instruction edits, use the installed mason command on PATH, merge project MCP configuration and hooks, update marker-delimited instructions and Git ignore rules, and record setup. A concept map is optional.
|
|
4932
|
+
If the user only requested inspection or review, report findings without running setup. The setup result distinguishes configured from observed activation. Explain the host's native trust step and request a new session; never trust hooks on the user's behalf or claim activation from generated configuration alone. After a normal task finishes, mason_automation(action: "status") or mason status reports observed context use and hook events. Existing findings remain reviewable; setup does not approve advisories or manufacture decisions.
|
|
4494
4933
|
|
|
4495
4934
|
The managed project guidance is:
|
|
4496
4935
|
${CLAUDE_MD_SECTION}`;
|
|
@@ -4498,7 +4937,7 @@ ${CLAUDE_MD_SECTION}`;
|
|
|
4498
4937
|
|
|
4499
4938
|
1. Explain the actionable findings with their source evidence. Separate audit issues, advisories, and skipped checks. The review covers committed changes from the merge base to HEAD; workingTree paths are not included in that review. An unavailable or empty check is not proof that the project is correct. Use the CLI for full output if a summary is truncated.
|
|
4500
4939
|
2. Address findings within the user's requested scope. Setup alone authorizes Mason host configuration, hooks, instructions, and ignore rules; rewriting existing project claims requires repair scope. If repair is authorized, call \`mason_repair(dir, action: "prepare")\` before editing; it saves the full original findings even when this summary is truncated. Inspect relevant source, make grounded edits, then call \`mason_repair(dir, action: "verify", baselinePath)\` with that same baseline, including after any final doc commit. Report resolved, unresolved, review-required, unverified, and new findings. Keep suppressed advisories visible even when setup has already dirtied a doc. Do not invent a decision just to populate the store.
|
|
4501
|
-
3. When the task reveals a real lesson or constraint, compare matching \`get_context\` records first. If its assumptions, scope, or recommended action changed, revise that record using \`save_decision(id: existingId)\`; create a separate record for a genuinely distinct lesson and skip unchanged restatements. Include title, body, category, anchors, and known owner/source/actor information. Preserve supported rationale and sources, replace obsolete instructions, and add new evidence. Missing attribution can be added later. The tool writes a local proposal; editing it preserves revision history and requires a new acceptance, while any earlier accepted revision remains operative. An unchanged save never refreshes its evidence. When decision review is requested, \`review_decision\` prepares the record and code evidence before any authorized verdict. Review and commit records through the normal workflow. Retrieve it on the next relevant task with \`get_context(dir, task, files)\`.
|
|
4940
|
+
3. Capture a supported lesson as soon as it is established; do not wait for implementation, task completion, or merge. Keep unimplemented remedies tentative. When the task reveals a real lesson or constraint, compare matching \`get_context\` records first. If its assumptions, scope, or recommended action changed, revise that record using \`save_decision(id: existingId)\`; create a separate record for a genuinely distinct lesson and skip unchanged restatements. Include title, body, category, anchors, and known owner/source/actor information. Preserve supported rationale and sources, replace obsolete instructions, and add new evidence. Missing attribution can be added later. The tool writes a local proposal; editing it preserves revision history and requires a new acceptance, while any earlier accepted revision remains operative. An unchanged save never refreshes its evidence. When decision review is requested, \`review_decision\` prepares the record and code evidence before any authorized verdict. Review and commit records through the normal workflow. Retrieve it on the next relevant task with \`get_context(dir, task, files)\`.
|
|
4502
4941
|
|
|
4503
4942
|
${ASSISTANT_SETUP}
|
|
4504
4943
|
|
|
@@ -4551,7 +4990,7 @@ __export(config_exports, {
|
|
|
4551
4990
|
});
|
|
4552
4991
|
import { isDeepStrictEqual } from "util";
|
|
4553
4992
|
import { parse as parse2, stringify } from "smol-toml";
|
|
4554
|
-
import { z as
|
|
4993
|
+
import { z as z18 } from "zod";
|
|
4555
4994
|
async function instructionEdits(root, host) {
|
|
4556
4995
|
const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
|
|
4557
4996
|
const existing = found.find((f) => f.text !== null);
|
|
@@ -4610,7 +5049,7 @@ async function mcpEdit(root, host) {
|
|
|
4610
5049
|
return { path: file, before, after };
|
|
4611
5050
|
}
|
|
4612
5051
|
async function hookEdits(root, host) {
|
|
4613
|
-
const file =
|
|
5052
|
+
const file = configPath2(host), before = await readText(root, file);
|
|
4614
5053
|
const { planAutomationInstall: planAutomationInstall2 } = await Promise.resolve().then(() => (init_install(), install_exports));
|
|
4615
5054
|
const plan = await planAutomationInstall2(root, host, hookCommand(host));
|
|
4616
5055
|
return [
|
|
@@ -4638,7 +5077,7 @@ async function inspectHostConfig(root, host, plannedMcp) {
|
|
|
4638
5077
|
const text2 = plannedMcp ?? await readText(root, mcpPath(host));
|
|
4639
5078
|
const config = host === "codex" ? parse2(text2 ?? "") : object.parse(JSON.parse(text2 ?? "{}"));
|
|
4640
5079
|
const servers = host === "codex" ? config.mcp_servers : config.mcpServers;
|
|
4641
|
-
const hooks = automationConfigSchema.parse(JSON.parse(await readText(root,
|
|
5080
|
+
const hooks = automationConfigSchema.parse(JSON.parse(await readText(root, configPath2(host)) ?? "{}"));
|
|
4642
5081
|
const expected = hookConfig(host, hookCommand(host));
|
|
4643
5082
|
const compatible = compatibleManagedHookCommands(host);
|
|
4644
5083
|
const mcp = servers?.mason === void 0 ? null : object.parse(servers.mason);
|
|
@@ -4662,7 +5101,7 @@ var init_config = __esm({
|
|
|
4662
5101
|
init_install();
|
|
4663
5102
|
init_files2();
|
|
4664
5103
|
init_launcher();
|
|
4665
|
-
object =
|
|
5104
|
+
object = z18.record(z18.unknown());
|
|
4666
5105
|
mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
|
|
4667
5106
|
TOML_START = "# mason:mcp:start";
|
|
4668
5107
|
TOML_END = "# mason:mcp:end";
|
|
@@ -4676,13 +5115,13 @@ var install_exports = {};
|
|
|
4676
5115
|
__export(install_exports, {
|
|
4677
5116
|
AUTOMATION_PATH: () => AUTOMATION_PATH,
|
|
4678
5117
|
automationConfigSchema: () => automationConfigSchema,
|
|
4679
|
-
configPath: () =>
|
|
5118
|
+
configPath: () => configPath2,
|
|
4680
5119
|
installAutomation: () => installAutomation,
|
|
4681
5120
|
installedAutomation: () => installedAutomation,
|
|
4682
5121
|
planAutomationInstall: () => planAutomationInstall,
|
|
4683
5122
|
recordSchema: () => recordSchema2
|
|
4684
5123
|
});
|
|
4685
|
-
import { z as
|
|
5124
|
+
import { z as z19 } from "zod";
|
|
4686
5125
|
async function installationRecord(root) {
|
|
4687
5126
|
return recordSchema2.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
|
|
4688
5127
|
}
|
|
@@ -4691,7 +5130,7 @@ async function installAutomation(dir, host, command2) {
|
|
|
4691
5130
|
return withLock(ws.root, ".mason/reports/setup-lock", () => installLocked(ws.root, host, command2));
|
|
4692
5131
|
}
|
|
4693
5132
|
async function planAutomationInstall(root, host, command2) {
|
|
4694
|
-
const file =
|
|
5133
|
+
const file = configPath2(host);
|
|
4695
5134
|
const before = await readText(root, file);
|
|
4696
5135
|
const existing = automationConfigSchema.parse(JSON.parse(before ?? "{}"));
|
|
4697
5136
|
const record = await installationRecord(root);
|
|
@@ -4731,7 +5170,7 @@ async function installLocked(root, host, command2) {
|
|
|
4731
5170
|
status: "configured",
|
|
4732
5171
|
command: newCommand,
|
|
4733
5172
|
events: HOOK_EVENTS,
|
|
4734
|
-
next: host === "codex" ? "Review/trust these hooks using Codex /hooks and start a new session. mason
|
|
5173
|
+
next: host === "codex" ? "Review/trust these hooks using Codex /hooks and start a new session. mason status reports observed events separately from configuration." : "Start a new Claude Code session. mason status reports observed events separately from configuration.",
|
|
4735
5174
|
note: "Install mason-context in the project before using the default command. Ignore .mason/reports/ to keep local evidence out of commits. Hooks preserve evidence and suggest scoped repairs; they do not approve edits or decisions."
|
|
4736
5175
|
};
|
|
4737
5176
|
}
|
|
@@ -4742,10 +5181,10 @@ async function installedAutomation(dir) {
|
|
|
4742
5181
|
for (const host of ["claude", "codex"]) {
|
|
4743
5182
|
const expected = record.hosts[host];
|
|
4744
5183
|
if (!expected) continue;
|
|
4745
|
-
const current = automationConfigSchema.parse(await readStoreJson(ws.root,
|
|
5184
|
+
const current = automationConfigSchema.parse(await readStoreJson(ws.root, configPath2(host)) ?? {});
|
|
4746
5185
|
const configuredEvents = HOOK_EVENTS.filter((event) => current.hooks?.[event]?.some((group) => group.hooks.some((handler) => handler.type === "command" && handler.command === expected.command)));
|
|
4747
5186
|
result[host] = {
|
|
4748
|
-
configPath:
|
|
5187
|
+
configPath: configPath2(host),
|
|
4749
5188
|
configuredEvents,
|
|
4750
5189
|
status: current.disableAllHooks === true ? "disabled" : configuredEvents.length === HOOK_EVENTS.length ? "configured" : "incomplete",
|
|
4751
5190
|
runtime: "Host version, trust, policy, and tool coverage still determine execution; inspect observed events."
|
|
@@ -4753,7 +5192,7 @@ async function installedAutomation(dir) {
|
|
|
4753
5192
|
}
|
|
4754
5193
|
return result;
|
|
4755
5194
|
}
|
|
4756
|
-
var groupSchema, automationConfigSchema, recordSchema2, AUTOMATION_PATH,
|
|
5195
|
+
var groupSchema, automationConfigSchema, recordSchema2, AUTOMATION_PATH, configPath2;
|
|
4757
5196
|
var init_install = __esm({
|
|
4758
5197
|
"src/automation/install.ts"() {
|
|
4759
5198
|
"use strict";
|
|
@@ -4763,11 +5202,11 @@ var init_install = __esm({
|
|
|
4763
5202
|
init_store();
|
|
4764
5203
|
init_files2();
|
|
4765
5204
|
init_ownership();
|
|
4766
|
-
groupSchema =
|
|
4767
|
-
automationConfigSchema =
|
|
4768
|
-
recordSchema2 =
|
|
5205
|
+
groupSchema = z19.object({ hooks: z19.array(z19.object({ type: z19.string(), command: z19.string().optional() }).passthrough()) }).passthrough();
|
|
5206
|
+
automationConfigSchema = z19.object({ hooks: z19.record(z19.array(groupSchema)).optional() }).passthrough();
|
|
5207
|
+
recordSchema2 = z19.object({ version: z19.literal(1), hosts: z19.record(z19.object({ command: z19.string() })) });
|
|
4769
5208
|
AUTOMATION_PATH = ".mason/local/automation.json";
|
|
4770
|
-
|
|
5209
|
+
configPath2 = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
|
|
4771
5210
|
}
|
|
4772
5211
|
});
|
|
4773
5212
|
|
|
@@ -4778,11 +5217,11 @@ __export(teardown_exports, {
|
|
|
4778
5217
|
teardownProject: () => teardownProject
|
|
4779
5218
|
});
|
|
4780
5219
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
4781
|
-
import
|
|
5220
|
+
import fs21 from "fs/promises";
|
|
4782
5221
|
import path26 from "path";
|
|
4783
5222
|
import { parse as parse3, stringify as stringify2 } from "smol-toml";
|
|
4784
5223
|
import fg8 from "fast-glob";
|
|
4785
|
-
import { z as
|
|
5224
|
+
import { z as z20 } from "zod";
|
|
4786
5225
|
async function planTeardown(root, selected) {
|
|
4787
5226
|
const edits = [], diagnostics = [];
|
|
4788
5227
|
const inputs = /* @__PURE__ */ new Map();
|
|
@@ -4801,7 +5240,7 @@ async function planTeardown(root, selected) {
|
|
|
4801
5240
|
const ownership = ownershipBefore === null ? { version: 1, files: {} } : ownershipSchema.parse(JSON.parse(ownershipBefore));
|
|
4802
5241
|
const setupBefore = await read(SETUP_PATH);
|
|
4803
5242
|
const setup = setupBefore === null ? null : setupSchema.parse(JSON.parse(setupBefore));
|
|
4804
|
-
const linked = (await workspace(root)).gitDir !== await
|
|
5243
|
+
const linked = (await workspace(root)).gitDir !== await fs21.realpath(
|
|
4805
5244
|
path26.resolve(root, (await git3(root, "rev-parse", "--git-common-dir")).trim())
|
|
4806
5245
|
);
|
|
4807
5246
|
const inactiveBefore = linked && !setup ? await read(INACTIVE_HOSTS_PATH) : null;
|
|
@@ -4810,16 +5249,16 @@ async function planTeardown(root, selected) {
|
|
|
4810
5249
|
const automation = automationBefore === null ? null : recordSchema2.parse(JSON.parse(automationBefore));
|
|
4811
5250
|
const commands2 = (host) => new Set([
|
|
4812
5251
|
automation?.hosts[host]?.command,
|
|
4813
|
-
...ownership.files[
|
|
5252
|
+
...ownership.files[configPath2(host)]?.hookCommands ?? [],
|
|
4814
5253
|
...knownManagedHookCommands(host),
|
|
4815
5254
|
hookConfig(host, hookCommand(host)).hooks.SessionStart[0].hooks[0].command,
|
|
4816
5255
|
hookConfig(host).hooks.SessionStart[0].hooks[0].command
|
|
4817
5256
|
].filter((command2) => !!command2));
|
|
4818
5257
|
const possibleHook = (command2, host) => commands2(host).has(command2) || /\bmason(?:-auto|-hook)?\b/.test(command2);
|
|
4819
|
-
const add = (file, before, after,
|
|
4820
|
-
edits.push({ path: file, before, after: ownership.files[file]?.created && (
|
|
5258
|
+
const add = (file, before, after, empty2) => {
|
|
5259
|
+
edits.push({ path: file, before, after: ownership.files[file]?.created && (empty2 || ownership.files[file].initialHash === hash(before)) ? null : after });
|
|
4821
5260
|
};
|
|
4822
|
-
let remainingHost = hosts.some((host) => !selected.includes(host) && (setup?.hosts[host] || automation?.hosts[host] || ownership.files[mcpPath(host)] || ownership.files[
|
|
5261
|
+
let remainingHost = hosts.some((host) => !selected.includes(host) && (setup?.hosts[host] || automation?.hosts[host] || ownership.files[mcpPath(host)] || ownership.files[configPath2(host)]));
|
|
4823
5262
|
for (const host of hosts.filter((host2) => !selected.includes(host2))) {
|
|
4824
5263
|
await attempt(mcpPath(host), async () => {
|
|
4825
5264
|
const text2 = await read(mcpPath(host));
|
|
@@ -4827,8 +5266,8 @@ async function planTeardown(root, selected) {
|
|
|
4827
5266
|
const servers = object2.parse((host === "codex" ? config.mcp_servers : config.mcpServers) ?? {});
|
|
4828
5267
|
remainingHost ||= servers.mason !== void 0;
|
|
4829
5268
|
});
|
|
4830
|
-
await attempt(
|
|
4831
|
-
const config = automationConfigSchema.parse(JSON.parse(await read(
|
|
5269
|
+
await attempt(configPath2(host), async () => {
|
|
5270
|
+
const config = automationConfigSchema.parse(JSON.parse(await read(configPath2(host)) ?? "{}"));
|
|
4832
5271
|
remainingHost ||= Object.values(config.hooks ?? {}).some((groups) => groups.some((group) => group.hooks.some((handler) => handler.command && possibleHook(handler.command, host))));
|
|
4833
5272
|
});
|
|
4834
5273
|
}
|
|
@@ -4870,7 +5309,7 @@ async function planTeardown(root, selected) {
|
|
|
4870
5309
|
if (!isDeepStrictEqual2(parsed, desired)) throw new Error("Removing the marked block would change other TOML settings; retained for manual cleanup.");
|
|
4871
5310
|
add(mcpFile, before, after, !after.trim());
|
|
4872
5311
|
});
|
|
4873
|
-
const hookFile =
|
|
5312
|
+
const hookFile = configPath2(host);
|
|
4874
5313
|
await attempt(hookFile, async () => {
|
|
4875
5314
|
const before = await read(hookFile);
|
|
4876
5315
|
if (before === null) return;
|
|
@@ -4938,7 +5377,7 @@ async function planTeardown(root, selected) {
|
|
|
4938
5377
|
if (!remainingHost) await attempt(".mason/local/project.json", async () => {
|
|
4939
5378
|
const before = await read(".mason/local/project.json");
|
|
4940
5379
|
if (before === null) return;
|
|
4941
|
-
|
|
5380
|
+
z20.object({ version: z20.literal(1), initializedAt: z20.string() }).strict().parse(JSON.parse(before));
|
|
4942
5381
|
cleanup.push({ path: ".mason/local/project.json", before, after: null });
|
|
4943
5382
|
});
|
|
4944
5383
|
if (!diagnostics.length) {
|
|
@@ -4946,7 +5385,7 @@ async function planTeardown(root, selected) {
|
|
|
4946
5385
|
if (setup) delete setup.hosts[host];
|
|
4947
5386
|
if (automation) delete automation.hosts[host];
|
|
4948
5387
|
delete ownership.files[mcpPath(host)];
|
|
4949
|
-
delete ownership.files[
|
|
5388
|
+
delete ownership.files[configPath2(host)];
|
|
4950
5389
|
}
|
|
4951
5390
|
if (!remainingHost) for (const file of DOC_CANDIDATES) delete ownership.files[file];
|
|
4952
5391
|
if (setupBefore !== null) cleanup.push({
|
|
@@ -5019,7 +5458,7 @@ var init_teardown = __esm({
|
|
|
5019
5458
|
init_model();
|
|
5020
5459
|
init_ownership();
|
|
5021
5460
|
hosts = ["codex", "claude"];
|
|
5022
|
-
object2 =
|
|
5461
|
+
object2 = z20.record(z20.unknown());
|
|
5023
5462
|
json = (value) => JSON.stringify(value, null, 2) + "\n";
|
|
5024
5463
|
}
|
|
5025
5464
|
});
|
|
@@ -5146,7 +5585,7 @@ var init_types2 = __esm({
|
|
|
5146
5585
|
});
|
|
5147
5586
|
|
|
5148
5587
|
// src/review/evidence/vitest.ts
|
|
5149
|
-
import { z as
|
|
5588
|
+
import { z as z21 } from "zod";
|
|
5150
5589
|
function parseVitest(raw, sourceRoot) {
|
|
5151
5590
|
const report = schema.parse(raw);
|
|
5152
5591
|
const findings = [], diagnostics = [];
|
|
@@ -5195,24 +5634,24 @@ var init_vitest = __esm({
|
|
|
5195
5634
|
"use strict";
|
|
5196
5635
|
init_paths2();
|
|
5197
5636
|
init_types2();
|
|
5198
|
-
count3 =
|
|
5199
|
-
schema =
|
|
5200
|
-
success:
|
|
5637
|
+
count3 = z21.number().int().nonnegative();
|
|
5638
|
+
schema = z21.object({
|
|
5639
|
+
success: z21.boolean(),
|
|
5201
5640
|
numTotalTests: count3,
|
|
5202
5641
|
numPassedTests: count3,
|
|
5203
5642
|
numFailedTests: count3,
|
|
5204
5643
|
numPendingTests: count3,
|
|
5205
5644
|
numTodoTests: count3,
|
|
5206
5645
|
numFailedTestSuites: count3,
|
|
5207
|
-
testResults:
|
|
5208
|
-
name:
|
|
5209
|
-
status:
|
|
5210
|
-
message:
|
|
5211
|
-
assertionResults:
|
|
5212
|
-
fullName:
|
|
5213
|
-
status:
|
|
5214
|
-
failureMessages:
|
|
5215
|
-
location:
|
|
5646
|
+
testResults: z21.array(z21.object({
|
|
5647
|
+
name: z21.string().min(1),
|
|
5648
|
+
status: z21.enum(["passed", "failed"]),
|
|
5649
|
+
message: z21.string().optional(),
|
|
5650
|
+
assertionResults: z21.array(z21.object({
|
|
5651
|
+
fullName: z21.string(),
|
|
5652
|
+
status: z21.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
|
|
5653
|
+
failureMessages: z21.array(z21.string()).nullable().optional(),
|
|
5654
|
+
location: z21.object({ line: count3, column: count3 }).nullable().optional()
|
|
5216
5655
|
}))
|
|
5217
5656
|
}))
|
|
5218
5657
|
});
|
|
@@ -5220,7 +5659,7 @@ var init_vitest = __esm({
|
|
|
5220
5659
|
});
|
|
5221
5660
|
|
|
5222
5661
|
// src/review/evidence/sarif.ts
|
|
5223
|
-
import { z as
|
|
5662
|
+
import { z as z22 } from "zod";
|
|
5224
5663
|
function parseSarif(raw, sourceRoot) {
|
|
5225
5664
|
const report = schema2.parse(raw), findings = [], diagnostics = [];
|
|
5226
5665
|
let active = 0, suppressed = 0, absent = 0, unresolved = 0;
|
|
@@ -5311,29 +5750,29 @@ var init_sarif = __esm({
|
|
|
5311
5750
|
"use strict";
|
|
5312
5751
|
init_paths2();
|
|
5313
5752
|
init_types2();
|
|
5314
|
-
index =
|
|
5315
|
-
artifact =
|
|
5316
|
-
message =
|
|
5317
|
-
level =
|
|
5318
|
-
location =
|
|
5319
|
-
schema2 =
|
|
5320
|
-
version:
|
|
5321
|
-
runs:
|
|
5322
|
-
tool:
|
|
5323
|
-
invocations:
|
|
5324
|
-
versionControlProvenance:
|
|
5325
|
-
originalUriBaseIds:
|
|
5326
|
-
artifacts:
|
|
5327
|
-
results:
|
|
5328
|
-
ruleId:
|
|
5753
|
+
index = z22.number().int().nonnegative();
|
|
5754
|
+
artifact = z22.object({ uri: z22.string().optional(), uriBaseId: z22.string().optional(), index: index.optional() });
|
|
5755
|
+
message = z22.object({ text: z22.string().optional(), markdown: z22.string().optional(), id: z22.string().optional(), arguments: z22.array(z22.string()).optional() });
|
|
5756
|
+
level = z22.enum(["error", "warning", "note", "none"]);
|
|
5757
|
+
location = z22.object({ physicalLocation: z22.object({ artifactLocation: artifact.optional(), region: z22.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
|
|
5758
|
+
schema2 = z22.object({
|
|
5759
|
+
version: z22.literal("2.1.0"),
|
|
5760
|
+
runs: z22.array(z22.object({
|
|
5761
|
+
tool: z22.object({ driver: z22.object({ name: z22.string().min(1), rules: z22.array(z22.object({ id: z22.string(), defaultConfiguration: z22.object({ level: level.optional() }).optional(), messageStrings: z22.record(message).optional() })).optional(), globalMessageStrings: z22.record(message).optional() }) }),
|
|
5762
|
+
invocations: z22.array(z22.object({ executionSuccessful: z22.boolean(), commandLine: z22.string().optional(), toolExecutionNotifications: z22.array(z22.object({ level: level.optional(), message })).optional() })).optional(),
|
|
5763
|
+
versionControlProvenance: z22.array(z22.object({ revisionId: z22.string().optional() })).optional(),
|
|
5764
|
+
originalUriBaseIds: z22.record(artifact).optional(),
|
|
5765
|
+
artifacts: z22.array(z22.object({ location: artifact.optional() })).optional(),
|
|
5766
|
+
results: z22.array(z22.object({
|
|
5767
|
+
ruleId: z22.string().optional(),
|
|
5329
5768
|
ruleIndex: index.optional(),
|
|
5330
5769
|
message,
|
|
5331
5770
|
level: level.optional(),
|
|
5332
|
-
kind:
|
|
5333
|
-
baselineState:
|
|
5334
|
-
suppressions:
|
|
5335
|
-
locations:
|
|
5336
|
-
relatedLocations:
|
|
5771
|
+
kind: z22.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
|
|
5772
|
+
baselineState: z22.enum(["new", "unchanged", "updated", "absent"]).optional(),
|
|
5773
|
+
suppressions: z22.array(z22.object({ kind: z22.enum(["inSource", "external"]), status: z22.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
|
|
5774
|
+
locations: z22.array(location).optional(),
|
|
5775
|
+
relatedLocations: z22.array(location).optional()
|
|
5337
5776
|
})).optional()
|
|
5338
5777
|
}))
|
|
5339
5778
|
});
|
|
@@ -5341,7 +5780,7 @@ var init_sarif = __esm({
|
|
|
5341
5780
|
});
|
|
5342
5781
|
|
|
5343
5782
|
// src/review/evidence/check.ts
|
|
5344
|
-
import { z as
|
|
5783
|
+
import { z as z23 } from "zod";
|
|
5345
5784
|
function parseCheck(raw, sourceRoot) {
|
|
5346
5785
|
const report = schema3.parse(raw);
|
|
5347
5786
|
const diagnostics = [], findings = [];
|
|
@@ -5381,17 +5820,17 @@ var init_check = __esm({
|
|
|
5381
5820
|
"use strict";
|
|
5382
5821
|
init_paths2();
|
|
5383
5822
|
init_types2();
|
|
5384
|
-
schema3 =
|
|
5385
|
-
version:
|
|
5386
|
-
commit:
|
|
5387
|
-
results:
|
|
5388
|
-
name:
|
|
5389
|
-
status:
|
|
5390
|
-
message:
|
|
5391
|
-
locations:
|
|
5392
|
-
file:
|
|
5393
|
-
line:
|
|
5394
|
-
column:
|
|
5823
|
+
schema3 = z23.object({
|
|
5824
|
+
version: z23.literal(1),
|
|
5825
|
+
commit: z23.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).optional(),
|
|
5826
|
+
results: z23.array(z23.object({
|
|
5827
|
+
name: z23.string().min(1).max(1e3),
|
|
5828
|
+
status: z23.enum(["passed", "failed", "skipped"]),
|
|
5829
|
+
message: z23.string().max(1e5).optional(),
|
|
5830
|
+
locations: z23.array(z23.object({
|
|
5831
|
+
file: z23.string().min(1).max(4e3),
|
|
5832
|
+
line: z23.number().int().positive().optional(),
|
|
5833
|
+
column: z23.number().int().positive().optional()
|
|
5395
5834
|
})).max(100).default([])
|
|
5396
5835
|
})).max(1e5)
|
|
5397
5836
|
});
|
|
@@ -5400,7 +5839,7 @@ var init_check = __esm({
|
|
|
5400
5839
|
|
|
5401
5840
|
// src/review/evidence.ts
|
|
5402
5841
|
import path27 from "path";
|
|
5403
|
-
import { z as
|
|
5842
|
+
import { z as z24 } from "zod";
|
|
5404
5843
|
function relativeArtifact(root, file) {
|
|
5405
5844
|
const relative = path27.isAbsolute(file) ? path27.relative(root, file) : file;
|
|
5406
5845
|
const normalized = normalizeRepoPath(relative);
|
|
@@ -5441,7 +5880,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
5441
5880
|
let manifest, raw;
|
|
5442
5881
|
try {
|
|
5443
5882
|
manifest = relativeArtifact(root, supplied);
|
|
5444
|
-
raw =
|
|
5883
|
+
raw = z24.object({ version: z24.literal(1), checks: z24.array(z24.unknown()) }).parse(await readStoreJson(root, manifest));
|
|
5445
5884
|
} catch (error) {
|
|
5446
5885
|
output.diagnostics.push(`${supplied}: ${String(error)}`);
|
|
5447
5886
|
continue;
|
|
@@ -5596,31 +6035,31 @@ var init_evidence2 = __esm({
|
|
|
5596
6035
|
init_sarif();
|
|
5597
6036
|
init_check();
|
|
5598
6037
|
init_types2();
|
|
5599
|
-
checkSchema2 =
|
|
5600
|
-
id:
|
|
5601
|
-
kind:
|
|
5602
|
-
tool:
|
|
5603
|
-
command:
|
|
5604
|
-
commit:
|
|
5605
|
-
workingTreeClean:
|
|
5606
|
-
source:
|
|
5607
|
-
sourceRoot:
|
|
5608
|
-
status:
|
|
5609
|
-
reason:
|
|
5610
|
-
exitCode:
|
|
5611
|
-
environment:
|
|
5612
|
-
toolVersion:
|
|
5613
|
-
runtime:
|
|
5614
|
-
platform:
|
|
5615
|
-
configuration:
|
|
6038
|
+
checkSchema2 = z24.object({
|
|
6039
|
+
id: z24.string().min(1).max(100),
|
|
6040
|
+
kind: z24.enum(["tests", "static-analysis", "security", "complexity", "duplication", "documentation"]),
|
|
6041
|
+
tool: z24.string().min(1).max(200),
|
|
6042
|
+
command: z24.string().min(1).max(2e3),
|
|
6043
|
+
commit: z24.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
|
|
6044
|
+
workingTreeClean: z24.boolean().optional(),
|
|
6045
|
+
source: z24.string().max(2e3).optional(),
|
|
6046
|
+
sourceRoot: z24.string().min(1).max(4e3).optional(),
|
|
6047
|
+
status: z24.enum(["completed", "skipped", "unavailable"]).default("completed"),
|
|
6048
|
+
reason: z24.string().min(1).max(2e3).optional(),
|
|
6049
|
+
exitCode: z24.number().int().nullable().optional(),
|
|
6050
|
+
environment: z24.object({
|
|
6051
|
+
toolVersion: z24.string().min(1).max(200),
|
|
6052
|
+
runtime: z24.string().min(1).max(200).optional(),
|
|
6053
|
+
platform: z24.string().min(1).max(200).optional(),
|
|
6054
|
+
configuration: z24.array(z24.string().min(1).max(4e3)).max(100).optional()
|
|
5616
6055
|
}).optional(),
|
|
5617
|
-
report:
|
|
6056
|
+
report: z24.object({ format: z24.enum(["vitest-json", "sarif", "mason-check-json"]), path: z24.string().min(1).max(4e3) }).optional()
|
|
5618
6057
|
});
|
|
5619
6058
|
}
|
|
5620
6059
|
});
|
|
5621
6060
|
|
|
5622
6061
|
// src/review/review.ts
|
|
5623
|
-
import
|
|
6062
|
+
import fs22 from "fs/promises";
|
|
5624
6063
|
import path28 from "path";
|
|
5625
6064
|
import { execFile as execFile4 } from "child_process";
|
|
5626
6065
|
import { promisify as promisify4 } from "util";
|
|
@@ -5700,7 +6139,7 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
5700
6139
|
analyzed,
|
|
5701
6140
|
async (relPath) => {
|
|
5702
6141
|
try {
|
|
5703
|
-
await
|
|
6142
|
+
await fs22.access(path28.join(resolvedRoot, relPath));
|
|
5704
6143
|
return true;
|
|
5705
6144
|
} catch {
|
|
5706
6145
|
return false;
|
|
@@ -5855,7 +6294,7 @@ async function setupStatus(dir) {
|
|
|
5855
6294
|
version: 1,
|
|
5856
6295
|
status: pending.length ? "incomplete" : "not-configured",
|
|
5857
6296
|
hosts: {},
|
|
5858
|
-
next: pending.length ? "Setup did not finish. Rerun mason
|
|
6297
|
+
next: pending.length ? "Setup did not finish. Rerun mason 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."
|
|
5859
6298
|
};
|
|
5860
6299
|
}
|
|
5861
6300
|
const hosts2 = {};
|
|
@@ -5889,7 +6328,7 @@ async function setupStatus(dir) {
|
|
|
5889
6328
|
if (!mcp || !hooks || !instructionsCurrent || !configured) pending.push("Rerun setup to reconcile project configuration; inspect any disabled host settings.");
|
|
5890
6329
|
if (!observation?.contextCalls) pending.push("Start a new assistant session and request task context through Mason's get_context tool.");
|
|
5891
6330
|
if (!complete) pending.push("Review/trust the host configuration, then complete a normal task to observe the full hook lifecycle.");
|
|
5892
|
-
if (automation.status === "unavailable") pending.push("The latest automation attempt did not establish verification. Run mason
|
|
6331
|
+
if (automation.status === "unavailable") pending.push("The latest automation attempt did not establish verification. Run mason check and inspect its diagnostics.");
|
|
5893
6332
|
const healthy = installed.available && mcp && hooks && instructionsCurrent && configured && automation.status !== "unavailable";
|
|
5894
6333
|
hosts2[host] = {
|
|
5895
6334
|
status: healthy ? complete && observation?.contextCalls ? "active" : "pending" : "attention",
|
|
@@ -6023,6 +6462,11 @@ async function setupProject(dir, options = {}) {
|
|
|
6023
6462
|
options.progress?.step("Checking configuration and retained findings");
|
|
6024
6463
|
const checked = await automate(ws.root, { event: "turn_start" });
|
|
6025
6464
|
const configured = await inspectHostConfig(ws.root, host, void 0);
|
|
6465
|
+
const stats = await configureUsefulness(ws.root, true, { onlyIfUnset: true }).catch(() => ({
|
|
6466
|
+
enabled: null,
|
|
6467
|
+
storage: ".mason/local/usefulness",
|
|
6468
|
+
note: "Local stats unavailable. Inspect mason stats --json, or use mason stats --disable to reset the preference. Existing observations were retained."
|
|
6469
|
+
}));
|
|
6026
6470
|
await writeStoreJson(ws.root, receiptPath, {
|
|
6027
6471
|
version: 1,
|
|
6028
6472
|
host,
|
|
@@ -6045,6 +6489,7 @@ async function setupProject(dir, options = {}) {
|
|
|
6045
6489
|
findings,
|
|
6046
6490
|
reportPath: checked.report.reportPath,
|
|
6047
6491
|
activation: await setupStatus(ws.root),
|
|
6492
|
+
stats,
|
|
6048
6493
|
next: configured.disabled || configured.mcpDisabled ? "Mason hooks or MCP are disabled in project configuration. Review that setting before activation." : host === "codex" ? "Review/trust this project's MCP configuration and hooks in Codex (/hooks in the CLI), then start a new session and give it a normal task." : "Approve the project MCP server in Claude Code, then start a new session and give it a normal task."
|
|
6049
6494
|
};
|
|
6050
6495
|
});
|
|
@@ -6056,18 +6501,20 @@ function summarizeSetup(result) {
|
|
|
6056
6501
|
" MCP server and lifecycle hooks use mason from PATH.",
|
|
6057
6502
|
" Project instructions updated; original audit evidence retained.",
|
|
6058
6503
|
` ${result.changedFiles.length} files changed.`,
|
|
6504
|
+
result.stats.enabled === null ? result.stats.note : result.stats.enabled ? "Local stats enabled: metadata stays in .mason/local/usefulness/; nothing is uploaded. View with mason stats; disable with mason stats --disable." : "Local stats remain disabled by your saved preference. Enable with mason stats --enable.",
|
|
6059
6505
|
`Initial audit: ${audit.status}${"counts" in audit ? `; ${audit.counts.issues} issues, ${audit.counts.advisories + audit.counts.suppressedAdvisories} advisories` : ""}.`,
|
|
6060
6506
|
..."issues" in audit ? audit.issues.slice(0, 3).map((f) => " " + f.message) : [],
|
|
6061
6507
|
`Original evidence: ${result.initialReportPath}`,
|
|
6062
6508
|
"Activation: " + result.activation.status + ".",
|
|
6063
6509
|
result.next,
|
|
6064
6510
|
"If the assistant cannot find mason, restart the desktop app so it picks up PATH.",
|
|
6065
|
-
"After the task finishes, run mason
|
|
6511
|
+
"After the task finishes, run mason status. Configuration alone does not establish activation."
|
|
6066
6512
|
].join("\n");
|
|
6067
6513
|
}
|
|
6068
6514
|
var init_setup = __esm({
|
|
6069
6515
|
"src/setup/setup.ts"() {
|
|
6070
6516
|
"use strict";
|
|
6517
|
+
init_usefulness();
|
|
6071
6518
|
init_evidence();
|
|
6072
6519
|
init_store();
|
|
6073
6520
|
init_runtime();
|
|
@@ -6084,13 +6531,13 @@ var init_setup = __esm({
|
|
|
6084
6531
|
});
|
|
6085
6532
|
|
|
6086
6533
|
// src/distribution/path.ts
|
|
6087
|
-
import
|
|
6534
|
+
import fs23 from "fs/promises";
|
|
6088
6535
|
import os3 from "os";
|
|
6089
6536
|
import path29 from "path";
|
|
6090
6537
|
import { execFile as execFile5 } from "child_process";
|
|
6091
6538
|
import { promisify as promisify5 } from "util";
|
|
6092
6539
|
import { createHash as createHash4, randomUUID as randomUUID7 } from "crypto";
|
|
6093
|
-
import { z as
|
|
6540
|
+
import { z as z25 } from "zod";
|
|
6094
6541
|
async function configurePath(bin, previous, save, options = {}) {
|
|
6095
6542
|
const env = options.env ?? process.env, platform = options.platform ?? process.platform;
|
|
6096
6543
|
const changes = [...previous];
|
|
@@ -6125,7 +6572,7 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
|
|
|
6125
6572
|
else {
|
|
6126
6573
|
const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path29.join(userHome, name));
|
|
6127
6574
|
let login = candidates[2];
|
|
6128
|
-
for (const file of candidates) if (await
|
|
6575
|
+
for (const file of candidates) if (await fs23.lstat(file).then(() => true, (error) => {
|
|
6129
6576
|
if (error.code === "ENOENT") return false;
|
|
6130
6577
|
throw error;
|
|
6131
6578
|
})) {
|
|
@@ -6136,12 +6583,12 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
|
|
|
6136
6583
|
}
|
|
6137
6584
|
const marker = `# >>> mason PATH ${createHash4("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
|
|
6138
6585
|
for (const profile of profiles2) {
|
|
6139
|
-
const stat = await
|
|
6586
|
+
const stat = await fs23.lstat(profile).catch((error) => {
|
|
6140
6587
|
if (error.code === "ENOENT") return null;
|
|
6141
6588
|
throw error;
|
|
6142
6589
|
});
|
|
6143
|
-
const file = stat?.isSymbolicLink() ? await
|
|
6144
|
-
const original = await
|
|
6590
|
+
const file = stat?.isSymbolicLink() ? await fs23.realpath(profile) : path29.resolve(profile);
|
|
6591
|
+
const original = await fs23.readFile(file).catch((error) => {
|
|
6145
6592
|
if (error.code === "ENOENT" && !stat) return null;
|
|
6146
6593
|
throw error;
|
|
6147
6594
|
});
|
|
@@ -6156,8 +6603,8 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
|
|
|
6156
6603
|
changes.push({ kind: "profile", file, block, created: original === null });
|
|
6157
6604
|
await save(changes);
|
|
6158
6605
|
}
|
|
6159
|
-
await
|
|
6160
|
-
const handle = await
|
|
6606
|
+
await fs23.mkdir(path29.dirname(file), { recursive: true });
|
|
6607
|
+
const handle = await fs23.open(file, original === null ? "ax+" : "a+", 384);
|
|
6161
6608
|
try {
|
|
6162
6609
|
if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0))) throw new Error(`Shell profile changed during installation: ${profile}`);
|
|
6163
6610
|
await handle.writeFile(block);
|
|
@@ -6181,13 +6628,13 @@ async function removePath(changes) {
|
|
|
6181
6628
|
await windowsPath("remove", change.entry, current, change.created);
|
|
6182
6629
|
continue;
|
|
6183
6630
|
}
|
|
6184
|
-
const stat = await
|
|
6631
|
+
const stat = await fs23.lstat(change.file).catch((error) => {
|
|
6185
6632
|
if (error.code === "ENOENT") return null;
|
|
6186
6633
|
throw error;
|
|
6187
6634
|
});
|
|
6188
6635
|
if (!stat) continue;
|
|
6189
6636
|
if (!stat.isFile()) throw new Error("profile type changed");
|
|
6190
|
-
const bytes = await
|
|
6637
|
+
const bytes = await fs23.readFile(change.file), text2 = bytes.toString("utf8");
|
|
6191
6638
|
if (!Buffer.from(text2).equals(bytes)) throw new Error("profile encoding changed");
|
|
6192
6639
|
const index2 = text2.indexOf(change.block);
|
|
6193
6640
|
if (index2 < 0 || text2.indexOf(change.block, index2 + change.block.length) >= 0) throw new Error("PATH block was edited or removed");
|
|
@@ -6197,7 +6644,7 @@ async function removePath(changes) {
|
|
|
6197
6644
|
const temporary = change.file + ".mason-" + randomUUID7();
|
|
6198
6645
|
try {
|
|
6199
6646
|
if (stat.nlink > 1) throw new Error("profile has multiple hard links");
|
|
6200
|
-
const handle = await
|
|
6647
|
+
const handle = await fs23.open(temporary, "wx", stat.mode & 511);
|
|
6201
6648
|
try {
|
|
6202
6649
|
await handle.writeFile(next);
|
|
6203
6650
|
await handle.chmod(stat.mode & 511);
|
|
@@ -6206,11 +6653,11 @@ async function removePath(changes) {
|
|
|
6206
6653
|
} finally {
|
|
6207
6654
|
await handle.close();
|
|
6208
6655
|
}
|
|
6209
|
-
const current = await
|
|
6210
|
-
if (!current.isFile() || current.ino !== stat.ino || !(await
|
|
6211
|
-
await
|
|
6656
|
+
const current = await fs23.lstat(change.file);
|
|
6657
|
+
if (!current.isFile() || current.ino !== stat.ino || !(await fs23.readFile(change.file)).equals(bytes)) throw new Error("profile changed during uninstall");
|
|
6658
|
+
await fs23.rename(temporary, change.file);
|
|
6212
6659
|
} finally {
|
|
6213
|
-
await
|
|
6660
|
+
await fs23.rm(temporary, { force: true });
|
|
6214
6661
|
}
|
|
6215
6662
|
} catch (error) {
|
|
6216
6663
|
warnings.push(`Retained PATH setting ${change.kind === "profile" ? change.file : change.entry}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -6252,9 +6699,9 @@ var init_path = __esm({
|
|
|
6252
6699
|
"src/distribution/path.ts"() {
|
|
6253
6700
|
"use strict";
|
|
6254
6701
|
execute2 = promisify5(execFile5);
|
|
6255
|
-
pathChangeSchema =
|
|
6256
|
-
|
|
6257
|
-
|
|
6702
|
+
pathChangeSchema = z25.discriminatedUnion("kind", [
|
|
6703
|
+
z25.object({ kind: z25.literal("profile"), file: z25.string(), block: z25.string(), created: z25.boolean() }),
|
|
6704
|
+
z25.object({ kind: z25.literal("windows"), entry: z25.string(), created: z25.boolean() })
|
|
6258
6705
|
]);
|
|
6259
6706
|
quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
|
6260
6707
|
normalizeWindowsEntry = (value) => path29.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
|
|
@@ -6282,13 +6729,13 @@ __export(state_exports, {
|
|
|
6282
6729
|
import os4 from "os";
|
|
6283
6730
|
import path30 from "path";
|
|
6284
6731
|
import { fileURLToPath } from "url";
|
|
6285
|
-
import
|
|
6286
|
-
import { z as
|
|
6732
|
+
import fs24 from "fs/promises";
|
|
6733
|
+
import { z as z26 } from "zod";
|
|
6287
6734
|
async function readInstallation(home) {
|
|
6288
6735
|
const raw = await readStoreJson(home, "install.json");
|
|
6289
6736
|
if (!raw) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
|
|
6290
6737
|
const record = recordSchema3.parse(raw);
|
|
6291
|
-
if (record.home !== await
|
|
6738
|
+
if (record.home !== await fs24.realpath(home)) throw new Error("Invalid standalone installation receipt.");
|
|
6292
6739
|
return record;
|
|
6293
6740
|
}
|
|
6294
6741
|
function standaloneLocation() {
|
|
@@ -6323,38 +6770,38 @@ var init_state = __esm({
|
|
|
6323
6770
|
"use strict";
|
|
6324
6771
|
init_storage();
|
|
6325
6772
|
init_path();
|
|
6326
|
-
releaseVersion =
|
|
6327
|
-
bundleId =
|
|
6328
|
-
updatePolicySchema =
|
|
6329
|
-
enabled:
|
|
6330
|
-
revision:
|
|
6773
|
+
releaseVersion = z26.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/);
|
|
6774
|
+
bundleId = z26.string().regex(/^[a-f0-9]{24}$/);
|
|
6775
|
+
updatePolicySchema = z26.object({
|
|
6776
|
+
enabled: z26.boolean(),
|
|
6777
|
+
revision: z26.string(),
|
|
6331
6778
|
pinnedVersion: releaseVersion.optional(),
|
|
6332
|
-
mirror:
|
|
6779
|
+
mirror: z26.string().optional()
|
|
6333
6780
|
});
|
|
6334
|
-
recordSchema3 =
|
|
6335
|
-
format:
|
|
6336
|
-
home:
|
|
6337
|
-
bin:
|
|
6781
|
+
recordSchema3 = z26.object({
|
|
6782
|
+
format: z26.literal(1),
|
|
6783
|
+
home: z26.string(),
|
|
6784
|
+
bin: z26.string(),
|
|
6338
6785
|
current: bundleId,
|
|
6339
6786
|
version: releaseVersion,
|
|
6340
|
-
launchers:
|
|
6341
|
-
previousLaunchers:
|
|
6342
|
-
pathChanges:
|
|
6787
|
+
launchers: z26.record(z26.string()),
|
|
6788
|
+
previousLaunchers: z26.record(z26.string()).optional(),
|
|
6789
|
+
pathChanges: z26.array(pathChangeSchema).optional(),
|
|
6343
6790
|
updates: updatePolicySchema.optional(),
|
|
6344
|
-
previous:
|
|
6345
|
-
pending:
|
|
6791
|
+
previous: z26.object({ id: bundleId, version: releaseVersion }).optional(),
|
|
6792
|
+
pending: z26.object({ id: bundleId, version: releaseVersion, policyRevision: z26.string() }).optional()
|
|
6346
6793
|
});
|
|
6347
6794
|
defaultHome = () => process.env.MASON_HOME ?? (process.platform === "win32" ? path30.join(process.env.LOCALAPPDATA ?? os4.homedir(), "Mason") : path30.join(os4.homedir(), ".local/share/mason"));
|
|
6348
6795
|
}
|
|
6349
6796
|
});
|
|
6350
6797
|
|
|
6351
6798
|
// src/distribution/bundle.ts
|
|
6352
|
-
import
|
|
6799
|
+
import fs25 from "fs/promises";
|
|
6353
6800
|
import path31 from "path";
|
|
6354
6801
|
import { createHash as createHash5 } from "crypto";
|
|
6355
|
-
import { z as
|
|
6802
|
+
import { z as z27 } from "zod";
|
|
6356
6803
|
async function readBundle(root) {
|
|
6357
|
-
const bytes = await
|
|
6804
|
+
const bytes = await fs25.readFile(await storePath(root, "bundle.json"));
|
|
6358
6805
|
const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
|
|
6359
6806
|
const windows = manifest.target.startsWith("win32-");
|
|
6360
6807
|
for (const file of Object.keys(manifest.files)) {
|
|
@@ -6362,7 +6809,7 @@ async function readBundle(root) {
|
|
|
6362
6809
|
throw new Error("Unsafe path in Mason bundle: " + file);
|
|
6363
6810
|
}
|
|
6364
6811
|
}
|
|
6365
|
-
for (const required of [windows ? "node.exe" : "node", "app/package.json", "app/dist/mason.js", "app/dist/mason-
|
|
6812
|
+
for (const required of [windows ? "node.exe" : "node", "app/package.json", "app/dist/mason.js", "app/dist/mason-mcp.js"]) {
|
|
6366
6813
|
if (!manifest.files[required]) throw new Error("Incomplete Mason bundle: " + required);
|
|
6367
6814
|
}
|
|
6368
6815
|
return { manifest, manifestHash: sha256(bytes) };
|
|
@@ -6373,7 +6820,7 @@ async function verifyBundle(root, expectedHash) {
|
|
|
6373
6820
|
if (bundle.manifest.target !== `${process.platform}-${process.arch}`) throw new Error("Mason bundle is for another platform. Install this platform's bundle and rerun setup.");
|
|
6374
6821
|
const files = Object.keys(bundle.manifest.files);
|
|
6375
6822
|
for (let i = 0; i < files.length; i += 8) await Promise.all(files.slice(i, i + 8).map(async (file) => {
|
|
6376
|
-
if (sha256(await
|
|
6823
|
+
if (sha256(await fs25.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
|
|
6377
6824
|
}));
|
|
6378
6825
|
return bundle;
|
|
6379
6826
|
}
|
|
@@ -6383,40 +6830,56 @@ var init_bundle = __esm({
|
|
|
6383
6830
|
"use strict";
|
|
6384
6831
|
init_storage();
|
|
6385
6832
|
targets = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
|
|
6386
|
-
digest2 =
|
|
6387
|
-
bundleSchema =
|
|
6388
|
-
format:
|
|
6389
|
-
version:
|
|
6390
|
-
target:
|
|
6391
|
-
nodeVersion:
|
|
6392
|
-
files:
|
|
6833
|
+
digest2 = z27.string().regex(/^[a-f0-9]{64}$/);
|
|
6834
|
+
bundleSchema = z27.object({
|
|
6835
|
+
format: z27.literal(1),
|
|
6836
|
+
version: z27.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
|
|
6837
|
+
target: z27.enum(targets),
|
|
6838
|
+
nodeVersion: z27.string(),
|
|
6839
|
+
files: z27.record(digest2)
|
|
6393
6840
|
}).strict();
|
|
6394
6841
|
sha256 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
|
|
6395
6842
|
}
|
|
6396
6843
|
});
|
|
6397
6844
|
|
|
6845
|
+
// src/distribution/trust.ts
|
|
6846
|
+
import tls from "tls";
|
|
6847
|
+
function releaseRequestError(error) {
|
|
6848
|
+
const cause = error instanceof Error ? error.cause : void 0;
|
|
6849
|
+
const code = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : void 0;
|
|
6850
|
+
const detail = code ?? (error instanceof Error ? error.message : String(error));
|
|
6851
|
+
const certificateError = code && /CERT|SELF_SIGNED|UNABLE_TO_VERIFY_LEAF_SIGNATURE/.test(code);
|
|
6852
|
+
const hint = certificateError ? " Check that your company's CA certificate is trusted by your operating system, or provide its PEM file with NODE_EXTRA_CA_CERTS." : " Check network and proxy access to GitHub.";
|
|
6853
|
+
return new Error(`Could not check the latest Mason release on GitHub (${detail}).${hint}`, { cause: error });
|
|
6854
|
+
}
|
|
6855
|
+
var init_trust2 = __esm({
|
|
6856
|
+
"src/distribution/trust.ts"() {
|
|
6857
|
+
"use strict";
|
|
6858
|
+
}
|
|
6859
|
+
});
|
|
6860
|
+
|
|
6398
6861
|
// src/distribution/runtime.ts
|
|
6399
|
-
import
|
|
6862
|
+
import fs26 from "fs/promises";
|
|
6400
6863
|
import { unlinkSync } from "fs";
|
|
6401
6864
|
import os5 from "os";
|
|
6402
6865
|
import path32 from "path";
|
|
6403
6866
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
6404
|
-
import { z as
|
|
6867
|
+
import { z as z28 } from "zod";
|
|
6405
6868
|
async function pruneUnusedVersions(home) {
|
|
6406
6869
|
const versions = await storePath(home, "versions");
|
|
6407
6870
|
const candidates = [];
|
|
6408
6871
|
const initial = await readInstallation(home);
|
|
6409
6872
|
const initialKeep = /* @__PURE__ */ new Set([initial.current, initial.previous?.id, initial.pending?.id]);
|
|
6410
|
-
for (const name of await
|
|
6873
|
+
for (const name of await fs26.readdir(versions)) {
|
|
6411
6874
|
const retired = /^\.retired-([a-f0-9]{24})-[a-f0-9-]+$/.exec(name);
|
|
6412
6875
|
const id = retired?.[1] ?? name;
|
|
6413
6876
|
if (!/^[a-f0-9]{24}$/.test(id) || initialKeep.has(id)) continue;
|
|
6414
6877
|
try {
|
|
6415
|
-
const
|
|
6416
|
-
await
|
|
6417
|
-
const bundle = await verifyBundle(
|
|
6878
|
+
const directory2 = await storePath(home, `versions/${name}`);
|
|
6879
|
+
await fs26.access(await storePath(directory2, "app/dist/mason-runtime.js"));
|
|
6880
|
+
const bundle = await verifyBundle(directory2);
|
|
6418
6881
|
if (bundle.manifestHash.slice(0, 24) === id && bundle.manifest.files["app/dist/mason-runtime.js"])
|
|
6419
|
-
candidates.push({ id, directory, retired: !!retired });
|
|
6882
|
+
candidates.push({ id, directory: directory2, retired: !!retired });
|
|
6420
6883
|
} catch {
|
|
6421
6884
|
}
|
|
6422
6885
|
}
|
|
@@ -6425,8 +6888,8 @@ async function pruneUnusedVersions(home) {
|
|
|
6425
6888
|
await withLock(home, ".install-lock", async () => {
|
|
6426
6889
|
const record = await readInstallation(home);
|
|
6427
6890
|
const keep = /* @__PURE__ */ new Set([record.current, record.previous?.id, record.pending?.id]);
|
|
6428
|
-
const
|
|
6429
|
-
const leases = await
|
|
6891
|
+
const directory2 = await storePath(home, "runtimes");
|
|
6892
|
+
const leases = await fs26.readdir(directory2).catch((error) => {
|
|
6430
6893
|
if (error.code === "ENOENT") return [];
|
|
6431
6894
|
throw error;
|
|
6432
6895
|
});
|
|
@@ -6442,7 +6905,7 @@ async function pruneUnusedVersions(home) {
|
|
|
6442
6905
|
keep.add(lease.bundle);
|
|
6443
6906
|
} catch (error) {
|
|
6444
6907
|
if (error.code !== "ESRCH") return;
|
|
6445
|
-
await
|
|
6908
|
+
await fs26.rm(await storePath(home, `runtimes/${name}`), { force: true });
|
|
6446
6909
|
}
|
|
6447
6910
|
} catch {
|
|
6448
6911
|
return;
|
|
@@ -6452,13 +6915,13 @@ async function pruneUnusedVersions(home) {
|
|
|
6452
6915
|
if (keep.has(candidate.id)) continue;
|
|
6453
6916
|
try {
|
|
6454
6917
|
const retired = candidate.retired ? candidate.directory : await storePath(home, `versions/.retired-${candidate.id}-${randomUUID8()}`);
|
|
6455
|
-
if (!candidate.retired) await
|
|
6918
|
+
if (!candidate.retired) await fs26.rename(candidate.directory, retired);
|
|
6456
6919
|
garbage.push(retired);
|
|
6457
6920
|
} catch {
|
|
6458
6921
|
}
|
|
6459
6922
|
}
|
|
6460
6923
|
});
|
|
6461
|
-
for (const
|
|
6924
|
+
for (const directory2 of garbage) await fs26.rm(directory2, { recursive: true }).catch(() => {
|
|
6462
6925
|
});
|
|
6463
6926
|
}
|
|
6464
6927
|
var leaseSchema;
|
|
@@ -6469,18 +6932,19 @@ var init_runtime2 = __esm({
|
|
|
6469
6932
|
init_storage();
|
|
6470
6933
|
init_state();
|
|
6471
6934
|
init_bundle();
|
|
6472
|
-
|
|
6935
|
+
init_trust2();
|
|
6936
|
+
leaseSchema = z28.object({ pid: z28.number().int().positive(), host: z28.string(), bundle: z28.string().regex(/^[a-f0-9]{24}$/) });
|
|
6473
6937
|
}
|
|
6474
6938
|
});
|
|
6475
6939
|
|
|
6476
6940
|
// src/distribution/sessions.ts
|
|
6477
|
-
import
|
|
6941
|
+
import fs27 from "fs/promises";
|
|
6478
6942
|
import os6 from "os";
|
|
6479
6943
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
6480
|
-
import { z as
|
|
6944
|
+
import { z as z29 } from "zod";
|
|
6481
6945
|
async function hasRunningMcp(home) {
|
|
6482
|
-
const
|
|
6483
|
-
const files = await
|
|
6946
|
+
const directory2 = await storePath(home, "sessions");
|
|
6947
|
+
const files = await fs27.readdir(directory2).catch((error) => {
|
|
6484
6948
|
if (error.code === "ENOENT") return [];
|
|
6485
6949
|
throw error;
|
|
6486
6950
|
});
|
|
@@ -6498,7 +6962,7 @@ async function hasRunningMcp(home) {
|
|
|
6498
6962
|
} catch (error) {
|
|
6499
6963
|
if (error.code !== "ESRCH") return true;
|
|
6500
6964
|
}
|
|
6501
|
-
await
|
|
6965
|
+
await fs27.rm(await storePath(home, relative), { force: true });
|
|
6502
6966
|
} catch {
|
|
6503
6967
|
return true;
|
|
6504
6968
|
}
|
|
@@ -6509,7 +6973,7 @@ async function registerMcp(home) {
|
|
|
6509
6973
|
const name = `sessions/${randomUUID9()}.json`;
|
|
6510
6974
|
await writeStoreJson(home, name, { pid: process.pid, host: os6.hostname() });
|
|
6511
6975
|
return async () => {
|
|
6512
|
-
await
|
|
6976
|
+
await fs27.rm(await storePath(home, name), { force: true }).catch(() => {
|
|
6513
6977
|
});
|
|
6514
6978
|
};
|
|
6515
6979
|
}
|
|
@@ -6518,7 +6982,7 @@ var init_sessions = __esm({
|
|
|
6518
6982
|
"src/distribution/sessions.ts"() {
|
|
6519
6983
|
"use strict";
|
|
6520
6984
|
init_storage();
|
|
6521
|
-
leaseSchema2 =
|
|
6985
|
+
leaseSchema2 = z29.object({ pid: z29.number().int().positive(), host: z29.string() });
|
|
6522
6986
|
}
|
|
6523
6987
|
});
|
|
6524
6988
|
|
|
@@ -6533,7 +6997,7 @@ __export(install_exports2, {
|
|
|
6533
6997
|
uninstallStandalone: () => uninstallStandalone,
|
|
6534
6998
|
upgradeStandalone: () => upgradeStandalone
|
|
6535
6999
|
});
|
|
6536
|
-
import
|
|
7000
|
+
import fs28 from "fs/promises";
|
|
6537
7001
|
import os7 from "os";
|
|
6538
7002
|
import path33 from "path";
|
|
6539
7003
|
import { execFile as execFile6, spawn } from "child_process";
|
|
@@ -6542,18 +7006,18 @@ import { randomUUID as randomUUID10 } from "crypto";
|
|
|
6542
7006
|
async function replace(file, content2, mode = 384) {
|
|
6543
7007
|
const temp = file + ".tmp-" + randomUUID10();
|
|
6544
7008
|
try {
|
|
6545
|
-
await
|
|
6546
|
-
await
|
|
7009
|
+
await fs28.writeFile(temp, content2, { mode, flag: "wx" });
|
|
7010
|
+
await fs28.rename(temp, file);
|
|
6547
7011
|
} finally {
|
|
6548
|
-
await
|
|
7012
|
+
await fs28.rm(temp, { force: true });
|
|
6549
7013
|
}
|
|
6550
7014
|
}
|
|
6551
7015
|
async function installStandalone(source, progress2, options = {}) {
|
|
6552
7016
|
progress2?.step("Checking installation files and location");
|
|
6553
7017
|
const bundle = await verifyBundle(source);
|
|
6554
7018
|
const selectedHome = path33.resolve(defaultHome());
|
|
6555
|
-
if (!options.stageOnly) await
|
|
6556
|
-
const home = await
|
|
7019
|
+
if (!options.stageOnly) await fs28.mkdir(selectedHome, { recursive: true });
|
|
7020
|
+
const home = await fs28.realpath(selectedHome);
|
|
6557
7021
|
if (options.stageOnly) {
|
|
6558
7022
|
assertUpdatePolicy(await readInstallation(home), bundle.manifest.version, options.policyRevision);
|
|
6559
7023
|
const destination = await storePath(home, "versions/" + bundle.manifestHash.slice(0, 24));
|
|
@@ -6576,17 +7040,17 @@ async function installLocked2(home, source, bundle, progress2, options, alreadyS
|
|
|
6576
7040
|
const raw = await readStoreJson(home, "install.json");
|
|
6577
7041
|
const previous = raw === null ? null : recordSchema3.parse(raw);
|
|
6578
7042
|
if (options.stageOnly) assertUpdatePolicy(previous, bundle.manifest.version, options.policyRevision);
|
|
6579
|
-
if (!previous) for (const file of await
|
|
7043
|
+
if (!previous) for (const file of await fs28.readdir(home)) {
|
|
6580
7044
|
if (file === ".install-lock") continue;
|
|
6581
|
-
const
|
|
6582
|
-
const stat = await
|
|
6583
|
-
if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await
|
|
7045
|
+
const directory2 = path33.join(home, file);
|
|
7046
|
+
const stat = await fs28.lstat(directory2);
|
|
7047
|
+
if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs28.readdir(directory2)).length) {
|
|
6584
7048
|
throw new Error("Mason installation directory is not empty and has no ownership receipt.");
|
|
6585
7049
|
}
|
|
6586
7050
|
}
|
|
6587
7051
|
const selectedBin = path33.resolve(process.env.MASON_BIN_DIR ?? previous?.bin ?? (process.platform === "win32" ? path33.join(home, "bin") : path33.join(os7.homedir(), ".local/bin")));
|
|
6588
|
-
await
|
|
6589
|
-
const bin = await
|
|
7052
|
+
await fs28.mkdir(selectedBin, { recursive: true });
|
|
7053
|
+
const bin = await fs28.realpath(selectedBin);
|
|
6590
7054
|
if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
|
|
6591
7055
|
const id = bundle.manifestHash.slice(0, 24);
|
|
6592
7056
|
const destination = await storePath(home, "versions/" + id);
|
|
@@ -6618,7 +7082,7 @@ exec ${shQuote(path33.join(destination, "node"))} ${shQuote(path33.join(destinat
|
|
|
6618
7082
|
` };
|
|
6619
7083
|
for (const [name, content2] of Object.entries(launchers)) {
|
|
6620
7084
|
const file = await storePath(bin, name);
|
|
6621
|
-
const old = await
|
|
7085
|
+
const old = await fs28.readFile(file, "utf8").catch((error) => {
|
|
6622
7086
|
if (error.code === "ENOENT") return null;
|
|
6623
7087
|
throw error;
|
|
6624
7088
|
});
|
|
@@ -6627,7 +7091,7 @@ exec ${shQuote(path33.join(destination, "node"))} ${shQuote(path33.join(destinat
|
|
|
6627
7091
|
const retired = Object.entries({ ...previous?.previousLaunchers, ...previous?.launchers }).filter(([name]) => !(name in launchers));
|
|
6628
7092
|
const checkRetired = async (name, expected) => {
|
|
6629
7093
|
if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
|
|
6630
|
-
const actual = await
|
|
7094
|
+
const actual = await fs28.readFile(await storePath(bin, name), "utf8").catch((error) => {
|
|
6631
7095
|
if (error.code === "ENOENT") return null;
|
|
6632
7096
|
throw error;
|
|
6633
7097
|
});
|
|
@@ -6662,7 +7126,7 @@ exec ${shQuote(path33.join(destination, "node"))} ${shQuote(path33.join(destinat
|
|
|
6662
7126
|
for (const [name, content2] of Object.entries(launchers)) await replace(await storePath(bin, name, true), content2, 493);
|
|
6663
7127
|
for (const [name, expected] of retired) {
|
|
6664
7128
|
await checkRetired(name, expected);
|
|
6665
|
-
await
|
|
7129
|
+
await fs28.rm(await storePath(bin, name), { force: true });
|
|
6666
7130
|
}
|
|
6667
7131
|
await writeStoreJson(home, "install.json", receipt);
|
|
6668
7132
|
} catch (error) {
|
|
@@ -6681,7 +7145,7 @@ exec ${shQuote(path33.join(destination, "node"))} ${shQuote(path33.join(destinat
|
|
|
6681
7145
|
return { home, bin, version: bundle.manifest.version, previousVersion: previous?.version, path: pathResult };
|
|
6682
7146
|
}
|
|
6683
7147
|
async function copyBundle(home, source, destination, bundle) {
|
|
6684
|
-
const exists = await
|
|
7148
|
+
const exists = await fs28.lstat(destination).then(() => true, (error) => {
|
|
6685
7149
|
if (error.code === "ENOENT") return false;
|
|
6686
7150
|
throw error;
|
|
6687
7151
|
});
|
|
@@ -6690,16 +7154,16 @@ async function copyBundle(home, source, destination, bundle) {
|
|
|
6690
7154
|
return;
|
|
6691
7155
|
}
|
|
6692
7156
|
const stage = await storePath(home, "versions/.install-" + randomUUID10(), true);
|
|
6693
|
-
await
|
|
7157
|
+
await fs28.mkdir(stage);
|
|
6694
7158
|
try {
|
|
6695
7159
|
for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
|
|
6696
7160
|
const from = await storePath(source, file), to = await storePath(stage, file, true);
|
|
6697
|
-
await
|
|
6698
|
-
await
|
|
7161
|
+
await fs28.copyFile(from, to);
|
|
7162
|
+
await fs28.chmod(to, (await fs28.stat(from)).mode & 511);
|
|
6699
7163
|
}
|
|
6700
7164
|
await verifyBundle(stage, bundle.manifestHash);
|
|
6701
7165
|
try {
|
|
6702
|
-
await
|
|
7166
|
+
await fs28.rename(stage, destination);
|
|
6703
7167
|
} catch (error) {
|
|
6704
7168
|
try {
|
|
6705
7169
|
await verifyBundle(destination, bundle.manifestHash);
|
|
@@ -6708,7 +7172,7 @@ async function copyBundle(home, source, destination, bundle) {
|
|
|
6708
7172
|
}
|
|
6709
7173
|
}
|
|
6710
7174
|
} finally {
|
|
6711
|
-
await
|
|
7175
|
+
await fs28.rm(stage, { recursive: true, force: true });
|
|
6712
7176
|
}
|
|
6713
7177
|
}
|
|
6714
7178
|
async function prepareStandaloneMcp(home) {
|
|
@@ -6768,6 +7232,8 @@ async function upgradeStandalone(version, progress2) {
|
|
|
6768
7232
|
method: "HEAD",
|
|
6769
7233
|
redirect: "follow",
|
|
6770
7234
|
signal: AbortSignal.timeout(3e4)
|
|
7235
|
+
}).catch((error) => {
|
|
7236
|
+
throw releaseRequestError(error);
|
|
6771
7237
|
});
|
|
6772
7238
|
if (!response.ok) throw new Error(`Latest release request failed (HTTP ${response.status}).`);
|
|
6773
7239
|
selected = new URL(response.url).pathname.split("/").pop()?.replace(/^v/, "");
|
|
@@ -6825,7 +7291,7 @@ async function uninstallStandalone() {
|
|
|
6825
7291
|
for (const [name, expected] of Object.entries(record.launchers)) {
|
|
6826
7292
|
if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
|
|
6827
7293
|
const file = await storePath(record.bin, name);
|
|
6828
|
-
const actual = await
|
|
7294
|
+
const actual = await fs28.readFile(file, "utf8").catch((error) => {
|
|
6829
7295
|
if (error.code === "ENOENT") return null;
|
|
6830
7296
|
throw error;
|
|
6831
7297
|
});
|
|
@@ -6833,9 +7299,9 @@ async function uninstallStandalone() {
|
|
|
6833
7299
|
}
|
|
6834
7300
|
for (const warning of await removePath(record.pathChanges ?? [])) console.error(warning);
|
|
6835
7301
|
if (process.platform === "win32") await replace(await storePath(home, ".uninstall-request.txt", true), token);
|
|
6836
|
-
for (const name of Object.keys(record.launchers)) await
|
|
7302
|
+
for (const name of Object.keys(record.launchers)) await fs28.rm(await storePath(record.bin, name), { force: true });
|
|
6837
7303
|
});
|
|
6838
|
-
if (process.platform !== "win32") await
|
|
7304
|
+
if (process.platform !== "win32") await fs28.rm(home, { recursive: true, force: true });
|
|
6839
7305
|
return home;
|
|
6840
7306
|
}
|
|
6841
7307
|
var shQuote, psQuote, WINDOWS_BATCH_LAUNCHER, launcherNames;
|
|
@@ -6848,6 +7314,7 @@ var init_install2 = __esm({
|
|
|
6848
7314
|
init_path();
|
|
6849
7315
|
init_state();
|
|
6850
7316
|
init_sessions();
|
|
7317
|
+
init_trust2();
|
|
6851
7318
|
init_state();
|
|
6852
7319
|
shQuote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
|
6853
7320
|
psQuote = (s) => "'" + s.replace(/'/g, "''") + "'";
|
|
@@ -6869,11 +7336,11 @@ __export(updates_exports, {
|
|
|
6869
7336
|
updateSettings: () => updateSettings,
|
|
6870
7337
|
updateStatus: () => updateStatus
|
|
6871
7338
|
});
|
|
6872
|
-
import
|
|
7339
|
+
import fs29 from "fs/promises";
|
|
6873
7340
|
import path34 from "path";
|
|
6874
7341
|
import { spawn as spawn2 } from "child_process";
|
|
6875
7342
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
6876
|
-
import { z as
|
|
7343
|
+
import { z as z30 } from "zod";
|
|
6877
7344
|
async function boundedDownload(url) {
|
|
6878
7345
|
const response = await fetch(url, { signal: AbortSignal.timeout(15e3), redirect: "follow" });
|
|
6879
7346
|
if (!response.ok || !response.body) throw new Error(`Update metadata request failed (HTTP ${response.status}).`);
|
|
@@ -6976,7 +7443,7 @@ async function runAutomaticUpdate(home, dependencies = { fetchRelease: fetchVeri
|
|
|
6976
7443
|
} catch (error) {
|
|
6977
7444
|
status.error = error instanceof Error ? error.message : String(error);
|
|
6978
7445
|
}
|
|
6979
|
-
if (await
|
|
7446
|
+
if (await fs29.access(path34.join(home, "install.json")).then(() => true, () => false)) await writeStoreJson(home, "update-status.json", status);
|
|
6980
7447
|
}, 0);
|
|
6981
7448
|
}
|
|
6982
7449
|
async function scheduleAutomaticUpdate() {
|
|
@@ -7086,14 +7553,14 @@ var init_updates = __esm({
|
|
|
7086
7553
|
CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
7087
7554
|
RELEASE_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
7088
7555
|
releaseBase = "https://github.com/adrianczuczka/mason/releases";
|
|
7089
|
-
releaseListSchema =
|
|
7090
|
-
updateManifestSchema =
|
|
7091
|
-
format:
|
|
7092
|
-
version:
|
|
7093
|
-
publishedAt:
|
|
7094
|
-
assets:
|
|
7556
|
+
releaseListSchema = z30.array(z30.object({ tag_name: z30.string(), draft: z30.boolean(), prerelease: z30.boolean(), published_at: z30.string().nullable() }));
|
|
7557
|
+
updateManifestSchema = z30.object({
|
|
7558
|
+
format: z30.literal(1),
|
|
7559
|
+
version: z30.string().regex(/^\d+\.\d+\.\d+$/),
|
|
7560
|
+
publishedAt: z30.string().datetime(),
|
|
7561
|
+
assets: z30.record(z30.string().regex(/^[a-f0-9]{64}$/))
|
|
7095
7562
|
}).strict();
|
|
7096
|
-
statusSchema2 =
|
|
7563
|
+
statusSchema2 = z30.object({ lastAttempt: z30.number(), availableVersion: z30.string().optional(), error: z30.string().optional() });
|
|
7097
7564
|
}
|
|
7098
7565
|
});
|
|
7099
7566
|
|
|
@@ -7101,6 +7568,7 @@ var init_updates = __esm({
|
|
|
7101
7568
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7102
7569
|
|
|
7103
7570
|
// src/automation/cli.ts
|
|
7571
|
+
init_usefulness();
|
|
7104
7572
|
init_execution();
|
|
7105
7573
|
init_runtime();
|
|
7106
7574
|
init_adapters();
|
|
@@ -7172,13 +7640,27 @@ Keeps shared knowledge, repair evidence, ignore rules and the Mason installation
|
|
|
7172
7640
|
Edited or ambiguous entries are retained for manual cleanup. Restart assistants
|
|
7173
7641
|
after teardown. Run mason setup --host codex or --host claude to reconnect.
|
|
7174
7642
|
Exit 0: complete/no-op. Exit 2: cleanup needs attention (also in dry runs).`;
|
|
7175
|
-
var
|
|
7643
|
+
var STATS_USAGE = `Usage: mason stats [options]
|
|
7644
|
+
|
|
7645
|
+
Show aggregate local observations, or inspect one session.
|
|
7646
|
+
|
|
7647
|
+
--enable | --disable Enable/disable collection (setup enables it)
|
|
7648
|
+
--json Include session/finding IDs and evidence paths
|
|
7649
|
+
--dir <path> Project directory (defaults to cwd)
|
|
7650
|
+
--session <id> Filter to one retained session
|
|
7651
|
+
--session <id> --finding <id> --rating helpful|already-knew|irrelevant|deferred
|
|
7652
|
+
Supply feedback without changing audit status
|
|
7653
|
+
|
|
7654
|
+
No uploads. Observed resolution does not establish causation.`;
|
|
7655
|
+
var USAGE = `Usage: mason auto <setup|teardown|install|config|status|check|hook|stats> [options]
|
|
7176
7656
|
|
|
7177
7657
|
setup [--host claude|codex] Connect MCP, instructions and hooks to mason on PATH; retain the initial audit
|
|
7178
7658
|
teardown [--host claude|codex] Disconnect one host or all project hosts; retain knowledge and evidence
|
|
7179
7659
|
install --host claude|codex Merge lifecycle hooks into this project's host config
|
|
7180
7660
|
config --host claude|codex Print the host config without writing
|
|
7181
7661
|
status Read configured hooks and observed runtime events
|
|
7662
|
+
stats Aggregate local observations; --enable/--disable to configure
|
|
7663
|
+
Rate with --session <id> --finding <id> --rating helpful|already-knew|irrelevant|deferred
|
|
7182
7664
|
check Capture/resume and verify retained audit evidence
|
|
7183
7665
|
hook --host claude|codex Handle host JSON on stdin
|
|
7184
7666
|
|
|
@@ -7199,7 +7681,12 @@ var parseCli = (argv) => parseArgs({ args: argv, allowPositionals: true, options
|
|
|
7199
7681
|
json: { type: "boolean" },
|
|
7200
7682
|
help: { type: "boolean", short: "h" },
|
|
7201
7683
|
"dry-run": { type: "boolean" },
|
|
7202
|
-
profile: { type: "boolean" }
|
|
7684
|
+
profile: { type: "boolean" },
|
|
7685
|
+
enable: { type: "boolean" },
|
|
7686
|
+
disable: { type: "boolean" },
|
|
7687
|
+
session: { type: "string" },
|
|
7688
|
+
finding: { type: "string" },
|
|
7689
|
+
rating: { type: "string" }
|
|
7203
7690
|
} });
|
|
7204
7691
|
function isHookCommand(argv) {
|
|
7205
7692
|
try {
|
|
@@ -7219,12 +7706,14 @@ async function runAutomationCli(argv, stdin = "", io = defaultIo, options = {})
|
|
|
7219
7706
|
try {
|
|
7220
7707
|
const { values, positionals } = parseCli(argv);
|
|
7221
7708
|
if (values.help || !positionals.length) {
|
|
7222
|
-
io.out(positionals[0] === "teardown" ? TEARDOWN_USAGE : USAGE);
|
|
7709
|
+
io.out(positionals[0] === "teardown" ? TEARDOWN_USAGE : positionals[0] === "stats" ? STATS_USAGE : USAGE);
|
|
7223
7710
|
return 0;
|
|
7224
7711
|
}
|
|
7225
7712
|
if (positionals.length !== 1) throw new Error("Expected one command.");
|
|
7226
7713
|
[action] = positionals;
|
|
7227
7714
|
if (values.profile && !["check", "hook"].includes(action)) throw new Error("--profile applies only to check/hook.");
|
|
7715
|
+
const trackingOptions = [values.enable, values.disable, values.session, values.finding, values.rating].some((v) => v !== void 0);
|
|
7716
|
+
if (trackingOptions && action !== "stats") throw new Error("Tracking options apply only to stats.");
|
|
7228
7717
|
const measured = (run) => values.profile ? withProfile(run, io.err) : run();
|
|
7229
7718
|
const dir = values.dir ?? process.cwd();
|
|
7230
7719
|
if (values["dry-run"] !== void 0 && action !== "teardown") throw new Error("--dry-run applies only to teardown.");
|
|
@@ -7261,11 +7750,26 @@ async function runAutomationCli(argv, stdin = "", io = defaultIo, options = {})
|
|
|
7261
7750
|
return 0;
|
|
7262
7751
|
}
|
|
7263
7752
|
if (values.host || values.command) throw new Error("--host and --command apply only to install/config/hook.");
|
|
7753
|
+
if (action === "stats") {
|
|
7754
|
+
const configuring = values.enable !== void 0 || values.disable !== void 0;
|
|
7755
|
+
const rating = values.finding !== void 0 || values.rating !== void 0;
|
|
7756
|
+
if (values.enable !== void 0 && values.disable !== void 0 || configuring && (rating || values.session !== void 0)) throw new Error("Choose tracking configuration or session inspection/feedback separately.");
|
|
7757
|
+
if (rating && (!values.session || !values.finding || !values.rating)) throw new Error("Rating requires --session, --finding, and --rating.");
|
|
7758
|
+
if (configuring) {
|
|
7759
|
+
const result2 = await configureUsefulness(dir, !!values.enable);
|
|
7760
|
+
io.out(values.json ? JSON.stringify(result2, null, 2) : `Local stats ${result2.enabled ? "enabled" : "disabled"}. Existing observations retained. View with mason stats.`);
|
|
7761
|
+
return 0;
|
|
7762
|
+
}
|
|
7763
|
+
if (rating) await rateUsefulness(dir, values.session, values.finding, usefulnessRating.parse(values.rating));
|
|
7764
|
+
const result = await usefulnessStatus(dir, values.session);
|
|
7765
|
+
io.out(values.json ? JSON.stringify(result, null, 2) : summarizeUsefulness(result));
|
|
7766
|
+
return 0;
|
|
7767
|
+
}
|
|
7264
7768
|
if (action === "status") {
|
|
7265
7769
|
const { setupStatus: setupStatus2, summarizeActivation: summarizeActivation2 } = await Promise.resolve().then(() => (init_status(), status_exports));
|
|
7266
7770
|
const setup = await setupStatus2(dir);
|
|
7267
7771
|
const result = { ...await automationStatus(dir), configured: await installedAutomation(dir), setup };
|
|
7268
|
-
io.out(values.json || !process.stdout.isTTY ? JSON.stringify(result, null, 2) : summarizeActivation2(setup));
|
|
7772
|
+
io.out(values.json || !process.stdout.isTTY ? JSON.stringify(result, null, 2) : [summarizeActivation2(setup), result.knowledge].filter(Boolean).join("\n\n"));
|
|
7269
7773
|
return 0;
|
|
7270
7774
|
}
|
|
7271
7775
|
if (action !== "check") throw new Error("Unknown automation command: " + action);
|
|
@@ -7290,13 +7794,15 @@ var inputArgs = process.argv.slice(2);
|
|
|
7290
7794
|
var managedLaunch = inputArgs[0] === "--setup-host";
|
|
7291
7795
|
var setupHost = managedLaunch ? inputArgs.splice(0, 2)[1] : void 0;
|
|
7292
7796
|
var [command, ...args] = inputArgs;
|
|
7293
|
-
var usage = `Mason ${"0.
|
|
7797
|
+
var usage = `Mason ${"0.20.1"}
|
|
7294
7798
|
|
|
7295
7799
|
Usage: mason <command> [options]
|
|
7296
7800
|
|
|
7297
7801
|
setup --host codex|claude Connect this project to your assistant
|
|
7298
7802
|
teardown [--host codex|claude] Disconnect project integrations; retain knowledge
|
|
7299
7803
|
status Show configuration, observed use and verification
|
|
7804
|
+
stats Show local stats and feedback
|
|
7805
|
+
auto Configure and run lifecycle hooks
|
|
7300
7806
|
check Resume and verify retained documentation findings
|
|
7301
7807
|
audit Audit project instructions
|
|
7302
7808
|
review --base <ref> Review committed changes
|
|
@@ -7308,7 +7814,7 @@ Usage: mason <command> [options]
|
|
|
7308
7814
|
rollback Restore and pin the previous standalone version
|
|
7309
7815
|
uninstall Remove that installation; retain project data
|
|
7310
7816
|
|
|
7311
|
-
Use mason <command> --help for options. npm users can
|
|
7817
|
+
Use mason <command> --help for options. npm users can also use
|
|
7312
7818
|
mason-audit, mason-review, mason-drift, mason-hook and mason-mcp.`;
|
|
7313
7819
|
var progress;
|
|
7314
7820
|
try {
|
|
@@ -7319,9 +7825,9 @@ try {
|
|
|
7319
7825
|
await prepareLaunch2(setupHost, { allowInactive: command === "mcp" });
|
|
7320
7826
|
}
|
|
7321
7827
|
}
|
|
7322
|
-
if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.
|
|
7828
|
+
if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.20.1" }));
|
|
7323
7829
|
else if (!command || ["--help", "-h", "help"].includes(command)) console.log(usage);
|
|
7324
|
-
else if (["--version", "-v"].includes(command)) console.log("0.
|
|
7830
|
+
else if (["--version", "-v"].includes(command)) console.log("0.20.1");
|
|
7325
7831
|
else if (command === "internal-update") {
|
|
7326
7832
|
if (args.length) throw new Error("internal-update takes no arguments.");
|
|
7327
7833
|
const { currentInstallation: currentInstallation2 } = await Promise.resolve().then(() => (init_state(), state_exports));
|
|
@@ -7390,7 +7896,7 @@ Automatic updates: ${status.effective ? "enabled" : "inactive"} (preference: ${s
|
|
|
7390
7896
|
process.exitCode = await upgradeStandalone2(args[0], progress);
|
|
7391
7897
|
} else console.log((process.platform === "win32" ? "Finishing installation cleanup after runtime exit: " : "Removed standalone installation: ") + await uninstallStandalone2() + ". Project configuration and knowledge were retained. Reinstall Mason to use its integrations.");
|
|
7392
7898
|
}
|
|
7393
|
-
} else if (["setup", "teardown", "status", "check", "auto"].includes(command)) {
|
|
7899
|
+
} else if (["setup", "teardown", "status", "check", "stats", "auto"].includes(command)) {
|
|
7394
7900
|
const forwarded = command === "auto" ? args : [command, ...args];
|
|
7395
7901
|
let input = "";
|
|
7396
7902
|
if (isHookCommand(forwarded) && !process.stdin.isTTY) for await (const chunk of process.stdin) {
|