mason-context 0.16.2 → 0.16.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mason-mcp.js CHANGED
@@ -685,87 +685,435 @@ var init_snapshot = __esm({
685
685
  }
686
686
  });
687
687
 
688
- // src/drift/drift.ts
689
- import fs3 from "fs/promises";
688
+ // src/context/lexical.ts
690
689
  import path6 from "path";
691
- import { execFile as execFile3 } from "child_process";
692
- import { promisify as promisify3 } from "util";
693
- function parseChanges(output) {
694
- const fields = output.split("\0");
695
- const changes = [];
696
- for (let i = 0; i < fields.length && fields[i]; ) {
697
- const code = fields[i++];
698
- const first = fields[i++];
699
- if (!first) break;
700
- const second = /^[RC]/.test(code) ? fields[i++] : void 0;
701
- const change = second ? code.startsWith("R") ? { status: "renamed", path: second, previousPath: first } : { status: "added", path: second } : { status: code === "A" ? "added" : code === "D" ? "deleted" : "modified", path: first };
702
- if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/"))) continue;
703
- changes.push(change);
704
- }
705
- return changes;
690
+ function tokenize(text2) {
691
+ return text2.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2 && !STOPWORDS.has(t));
706
692
  }
707
- function touchedPaths(changes) {
708
- return [...new Set(changes.flatMap((c) => c.previousPath ? [c.previousPath, c.path] : [c.path]))].sort();
693
+ function stem(token) {
694
+ return token.length > 3 && token.endsWith("s") ? token.slice(0, -1) : token;
709
695
  }
710
- async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
711
- if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-")) return null;
712
- try {
713
- const { stdout } = await exec3("git", ["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 });
714
- return parseChanges(stdout);
715
- } catch {
716
- return null;
696
+ function tokenSet(text2) {
697
+ return new Set(tokenize(text2).map(stem));
698
+ }
699
+ function scoreEntry(taskTokens, entry) {
700
+ const nameTokens = tokenSet(entry.name);
701
+ const descTokens = tokenSet(entry.description);
702
+ const fileTokens = tokenSet(entry.files.map((f) => path6.basename(f)).join(" "));
703
+ let score = 0;
704
+ for (const token of taskTokens) {
705
+ if (nameTokens.has(token)) score += 3;
706
+ else if (descTokens.has(token)) score += 1;
707
+ else if (fileTokens.has(token)) score += 1;
717
708
  }
709
+ return score;
718
710
  }
719
- async function getWorkingTree(resolvedRoot) {
720
- try {
721
- const [diff, untracked] = await Promise.all([
722
- exec3("git", ["diff", "--name-status", "-z", "-M", "HEAD", "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }),
723
- exec3("git", ["ls-files", "-z", "--others", "--exclude-standard"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 })
711
+ function jaccard(a, b) {
712
+ if (a.size === 0 && b.size === 0) return 0;
713
+ let intersection = 0;
714
+ for (const token of a) if (b.has(token)) intersection++;
715
+ return intersection / (a.size + b.size - intersection);
716
+ }
717
+ var STOPWORDS;
718
+ var init_lexical = __esm({
719
+ "src/context/lexical.ts"() {
720
+ "use strict";
721
+ STOPWORDS = /* @__PURE__ */ new Set([
722
+ "the",
723
+ "a",
724
+ "an",
725
+ "and",
726
+ "or",
727
+ "of",
728
+ "to",
729
+ "in",
730
+ "on",
731
+ "for",
732
+ "with",
733
+ "how",
734
+ "does",
735
+ "do",
736
+ "is",
737
+ "are",
738
+ "was",
739
+ "what",
740
+ "where",
741
+ "which",
742
+ "why",
743
+ "when",
744
+ "who",
745
+ "i",
746
+ "we",
747
+ "my",
748
+ "our",
749
+ "you",
750
+ "your",
751
+ "it",
752
+ "its",
753
+ "this",
754
+ "that",
755
+ "these",
756
+ "those",
757
+ "can",
758
+ "could",
759
+ "should",
760
+ "would",
761
+ "will",
762
+ "want",
763
+ "need",
764
+ "please",
765
+ "about",
766
+ "into",
767
+ "from",
768
+ "when",
769
+ "there",
770
+ "any",
771
+ "all",
772
+ "some",
773
+ "not",
774
+ "but",
775
+ "also",
776
+ "just",
777
+ "like",
778
+ "get",
779
+ "make",
780
+ "use",
781
+ "new",
782
+ "work",
783
+ "works",
784
+ "working",
785
+ "implement",
786
+ "implemented",
787
+ "change",
788
+ "changed",
789
+ "file",
790
+ "files",
791
+ "code"
724
792
  ]);
725
- const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
726
- return { available: true, changedFiles: [.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])].sort(), untrackedFiles };
727
- } catch {
728
- return { available: false, changedFiles: [], untrackedFiles: [] };
729
793
  }
730
- }
731
- async function countCommitsBehind(resolvedRoot, fromHash) {
732
- if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-")) return null;
794
+ });
795
+
796
+ // src/decisions/decisions.ts
797
+ var decisions_exports = {};
798
+ __export(decisions_exports, {
799
+ BODY_MAX_CHARS: () => BODY_MAX_CHARS,
800
+ BODY_RECOMMENDED_CHARS: () => BODY_RECOMMENDED_CHARS,
801
+ MAX_ACTIVE_DECISIONS: () => MAX_ACTIVE_DECISIONS,
802
+ TITLE_MAX_CHARS: () => TITLE_MAX_CHARS,
803
+ decisionBodySchema: () => decisionBodySchema,
804
+ decisionIdFor: () => decisionIdFor,
805
+ decisionTitleSchema: () => decisionTitleSchema,
806
+ findNearDuplicate: () => findNearDuplicate,
807
+ loadDecisionStore: () => loadDecisionStore,
808
+ loadDecisions: () => loadDecisions,
809
+ saveDecisionRecord: () => saveDecisionRecord,
810
+ upsertDecision: () => upsertDecision,
811
+ withDecisionWrite: () => withDecisionWrite
812
+ });
813
+ import fs3 from "fs/promises";
814
+ import path7 from "path";
815
+ import { createHash } from "crypto";
816
+ import { z as z3 } from "zod";
817
+ async function loadDecisionStore(rootDir) {
818
+ const records = [];
819
+ const diagnostics = [];
820
+ let entries;
733
821
  try {
734
- const { stdout } = await exec3(
735
- "git",
736
- ["rev-list", "--count", `${fromHash}..HEAD`],
737
- { cwd: resolvedRoot }
822
+ entries = await fs3.readdir(await storePath(rootDir, ".mason/decisions"));
823
+ } catch (error) {
824
+ if (error.code !== "ENOENT") diagnostics.push({ path: ".mason/decisions", message: String(error) });
825
+ return { records, diagnostics };
826
+ }
827
+ for (const entry of entries.sort()) {
828
+ if (!entry.endsWith(".json")) continue;
829
+ const relative = `.mason/decisions/${entry}`;
830
+ try {
831
+ const record = decisionSchema.parse(await readStoreJson(rootDir, relative));
832
+ if (entry !== `${record.id}.json`) throw new Error("Record id does not match its filename");
833
+ records.push(record);
834
+ } catch (error) {
835
+ diagnostics.push({ path: relative, message: error instanceof Error ? error.message : String(error) });
836
+ }
837
+ }
838
+ return { records, diagnostics };
839
+ }
840
+ async function loadDecisions(rootDir) {
841
+ return (await loadDecisionStore(rootDir)).records;
842
+ }
843
+ async function saveDecisionRecord(rootDir, record) {
844
+ const validated = decisionSchema.parse(record);
845
+ await writeStoreJson(rootDir, `.mason/decisions/${validated.id}.json`, validated);
846
+ }
847
+ function decisionIdFor(title, body, existingIds) {
848
+ const fullSlug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
849
+ let slug = fullSlug.slice(0, 60);
850
+ if (fullSlug.length > 60 && fullSlug[60] !== "-") {
851
+ const boundary = slug.lastIndexOf("-");
852
+ if (boundary > 0) slug = slug.slice(0, boundary);
853
+ }
854
+ slug = slug.replace(/-+$/, "") || "decision";
855
+ if (!existingIds.has(slug)) return slug;
856
+ const suffix = createHash("sha1").update(title + body).digest("hex").slice(0, 6);
857
+ return `${slug}-${suffix}`;
858
+ }
859
+ function findNearDuplicate(candidate, existing) {
860
+ const candidateTokens = tokenSet(`${candidate.title} ${candidate.body}`);
861
+ const candidateFiles = new Set(candidate.files);
862
+ let best = null;
863
+ for (const record of existing) {
864
+ if (record.status !== "active") continue;
865
+ const similarity = jaccard(
866
+ candidateTokens,
867
+ tokenSet(`${record.title} ${record.body}`)
738
868
  );
739
- const count3 = Number.parseInt(stdout.trim(), 10);
740
- return Number.isNaN(count3) ? null : count3;
741
- } catch {
742
- return null;
869
+ const sharesFile = record.files.some((f) => candidateFiles.has(f));
870
+ const threshold = sharesFile ? DUPLICATE_JACCARD_WITH_SHARED_FILE : DUPLICATE_JACCARD;
871
+ if (similarity >= threshold && (!best || similarity > best.similarity)) {
872
+ best = { record, similarity };
873
+ }
743
874
  }
875
+ return best;
744
876
  }
745
- function collectMappedFiles(snapshot) {
746
- const mappedFiles = /* @__PURE__ */ new Set();
747
- for (const feature of Object.values(snapshot.features)) {
748
- for (const f of feature.files) mappedFiles.add(f);
749
- for (const t of feature.tests ?? []) mappedFiles.add(t);
877
+ async function withDecisionWrite(root, operation) {
878
+ const lockPath = await storePath(root, ".mason/decisions/.write-lock", true);
879
+ let lock;
880
+ try {
881
+ lock = await fs3.open(lockPath, "wx", 384);
882
+ } catch (error) {
883
+ if (error.code === "EEXIST") return { status: "error", error: "Decision store is locked by another write. Retry after it finishes; an abandoned .mason/decisions/.write-lock must be removed only after confirming no writer is running." };
884
+ throw error;
750
885
  }
751
- for (const flow of Object.values(snapshot.flows)) {
752
- for (const f of flow.chain) mappedFiles.add(f);
886
+ try {
887
+ return await operation();
888
+ } finally {
889
+ await lock.close();
890
+ await fs3.unlink(lockPath);
753
891
  }
754
- return mappedFiles;
755
892
  }
756
- async function findGhostFiles(resolvedRoot, mappedFiles) {
757
- const ghosts = [];
758
- for (const file of mappedFiles) {
759
- try {
760
- await fs3.access(path6.join(resolvedRoot, file));
761
- } catch {
762
- ghosts.push(file);
893
+ async function upsertDecision(rootDir, input) {
894
+ const parsedTitle = decisionTitleSchema.safeParse(input.title), parsedBody = decisionBodySchema.safeParse(input.body);
895
+ if (!parsedTitle.success) return { status: "error", error: parsedTitle.error.issues[0].message };
896
+ if (!parsedBody.success) return { status: "error", error: parsedBody.error.issues[0].message };
897
+ const title = parsedTitle.data, body = parsedBody.data;
898
+ const attribution = attributionSchema.safeParse(input);
899
+ if (!attribution.success) return { status: "error", error: attribution.error.message };
900
+ if (input.id && input.supersedes) return { status: "error", error: "Use either id to revise or supersedes to replace a record, not both." };
901
+ return withDecisionWrite(rootDir, async () => {
902
+ const store = await loadDecisionStore(rootDir);
903
+ if (store.diagnostics.length) return { status: "error", error: "Repair malformed decision records before saving: " + store.diagnostics.map((d) => d.path).join(", ") };
904
+ const existing = store.records, byId = new Map(existing.map((r) => [r.id, r]));
905
+ const now = (/* @__PURE__ */ new Date()).toISOString(), head = await getCurrentGitHash(rootDir);
906
+ const warnings = [];
907
+ if (body.length > BODY_RECOMMENDED_CHARS) warnings.push(`body has ${body.length} characters, above the recommended ${BODY_RECOMMENDED_CHARS}; matching context and hooks include it in full. Keep the rule, reason, and exceptions concise; use sources for supporting references.`);
908
+ const files = sanitizeRepoPaths(input.files ?? []);
909
+ if (input.files && files.length < input.files.length) warnings.push("some anchor paths were outside the repo or duplicated and were dropped");
910
+ for (const file of files) {
911
+ try {
912
+ await fs3.access(path7.join(rootDir, file));
913
+ } catch {
914
+ warnings.push(`anchor file does not exist on disk: ${file}`);
915
+ }
763
916
  }
764
- }
765
- return ghosts.sort();
766
- }
917
+ const hint = "Saved locally for review and commit. Proposals are not accepted constraints; an existing accepted revision remains operative while its replacement is proposed. Use review_decision to inspect evidence and record an authorized acceptance or reaffirmation.";
918
+ if (input.id) {
919
+ const original = byId.get(input.id);
920
+ if (!original) return { status: "error", error: `no decision with id "${input.id}"` };
921
+ if (original.status !== "active") return { status: "error", error: "Archived records cannot be revised; create a new proposal." };
922
+ const record2 = importLegacy(original, now);
923
+ const content3 = decisionContent({
924
+ ...record2,
925
+ title,
926
+ body,
927
+ category: input.category,
928
+ files: input.files !== void 0 ? files : record2.files,
929
+ owner: attribution.data.owner === void 0 ? record2.owner : attribution.data.owner ?? void 0,
930
+ sources: attribution.data.sources ?? record2.sources
931
+ });
932
+ if (JSON.stringify(content3) === JSON.stringify(decisionContent(record2))) {
933
+ return {
934
+ status: "unchanged",
935
+ id: record2.id,
936
+ totalActive: existing.filter((r) => r.status === "active").length,
937
+ approval: decisionApproval(original),
938
+ warnings,
939
+ hint: "Unchanged content; no review or freshness stamp was written. Use review_decision for explicit re-verification."
940
+ };
941
+ }
942
+ const revision = record2.revision + 1;
943
+ const updated = {
944
+ ...record2,
945
+ ...content3,
946
+ owner: content3.owner,
947
+ updatedAt: now,
948
+ revision,
949
+ approval: "proposed",
950
+ history: [...record2.history, { kind: "revised", at: now, actor: attribution.data.actor, revision, content: content3, approval: "proposed", status: "active", refreshedHash: record2.refreshedHash }]
951
+ };
952
+ await saveDecisionRecord(rootDir, updated);
953
+ return { status: "updated", id: record2.id, totalActive: existing.filter((r) => r.status === "active").length, approval: "proposed", warnings, hint };
954
+ }
955
+ const old = input.supersedes ? byId.get(input.supersedes) : void 0;
956
+ if (input.supersedes && !old) return { status: "error", error: `no decision with id "${input.supersedes}" to supersede` };
957
+ if (old && (old.status !== "active" || decisionApproval(effectiveDecision(old)) === "accepted")) {
958
+ return { status: "error", error: "A proposal cannot supersede an accepted or archived record. Create and review the replacement separately, then explicitly retire the old decision with review_decision." };
959
+ }
960
+ if (!input.force) {
961
+ const duplicate = findNearDuplicate({ title, body, files }, existing);
962
+ if (duplicate) return { status: "duplicate_suspected", existing: duplicate.record, hint: `A similar decision exists ("${duplicate.record.title}"). Call save_decision with id="${duplicate.record.id}" to revise it, or force:true if distinct.` };
963
+ }
964
+ const id = decisionIdFor(title, body, new Set(byId.keys()));
965
+ if (byId.has(id)) return { status: "error", error: `Decision id collision: ${id}. Choose a distinct title or revise the existing record.` };
966
+ const content2 = decisionContent({ title, body, category: input.category, files, owner: attribution.data.owner ?? void 0, sources: attribution.data.sources ?? [] });
967
+ const record = {
968
+ ...content2,
969
+ version: 2,
970
+ id,
971
+ createdAt: now,
972
+ updatedAt: now,
973
+ refreshedHash: head,
974
+ status: "active",
975
+ approval: "proposed",
976
+ revision: 1,
977
+ history: [{ kind: "created", at: now, actor: attribution.data.actor, revision: 1, content: content2, approval: "proposed", status: "active", refreshedHash: head }]
978
+ };
979
+ await saveDecisionRecord(rootDir, record);
980
+ if (old) {
981
+ const imported = importLegacy(old, now);
982
+ await saveDecisionRecord(rootDir, {
983
+ ...imported,
984
+ status: "superseded",
985
+ supersededBy: id,
986
+ updatedAt: now,
987
+ history: [...imported.history, {
988
+ kind: "superseded",
989
+ at: now,
990
+ actor: attribution.data.actor,
991
+ note: `Replaced by proposal ${id}`,
992
+ revision: imported.revision,
993
+ content: decisionContent(imported),
994
+ approval: imported.approval,
995
+ status: "superseded",
996
+ refreshedHash: imported.refreshedHash
997
+ }]
998
+ });
999
+ }
1000
+ const totalActive = existing.filter((r) => r.status === "active").length + (old ? 0 : 1);
1001
+ const result = { status: old ? "superseded_and_created" : "created", id, totalActive, approval: "proposed", warnings, hint };
1002
+ if (totalActive > MAX_ACTIVE_DECISIONS) {
1003
+ result.pruneCandidates = existing.filter((r) => r.status !== "active").map((r) => r.id).slice(0, 10);
1004
+ warnings.push(`${totalActive} active decisions exceeds the soft cap of ${MAX_ACTIVE_DECISIONS} \u2014 consider a cleanup PR (archived records first)`);
1005
+ }
1006
+ return result;
1007
+ });
1008
+ }
1009
+ var TITLE_MAX_CHARS, BODY_RECOMMENDED_CHARS, BODY_MAX_CHARS, MAX_ACTIVE_DECISIONS, boundedText, decisionTitleSchema, decisionBodySchema, DUPLICATE_JACCARD, DUPLICATE_JACCARD_WITH_SHARED_FILE;
1010
+ var init_decisions = __esm({
1011
+ "src/decisions/decisions.ts"() {
1012
+ "use strict";
1013
+ init_storage();
1014
+ init_paths();
1015
+ init_snapshot();
1016
+ init_lexical();
1017
+ init_provenance();
1018
+ TITLE_MAX_CHARS = 80;
1019
+ BODY_RECOMMENDED_CHARS = 1500;
1020
+ BODY_MAX_CHARS = 2500;
1021
+ MAX_ACTIVE_DECISIONS = 150;
1022
+ boundedText = (field, max, hint) => z3.string({
1023
+ 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 })
1024
+ }).trim().min(1, `${field} must be non-empty`).max(max);
1025
+ decisionTitleSchema = boundedText("title", TITLE_MAX_CHARS, "Tighten it to a specific headline.");
1026
+ decisionBodySchema = boundedText(
1027
+ "body",
1028
+ BODY_MAX_CHARS,
1029
+ `Aim for ${BODY_RECOMMENDED_CHARS} characters or fewer. Preserve the rule, reason, and exceptions; put supporting references in sources.`
1030
+ );
1031
+ DUPLICATE_JACCARD = 0.5;
1032
+ DUPLICATE_JACCARD_WITH_SHARED_FILE = 0.35;
1033
+ }
1034
+ });
1035
+
1036
+ // src/drift/drift.ts
1037
+ import fs4 from "fs/promises";
1038
+ import path8 from "path";
1039
+ import { execFile as execFile3 } from "child_process";
1040
+ import { promisify as promisify3 } from "util";
1041
+ function parseChanges(output) {
1042
+ const fields = output.split("\0");
1043
+ const changes = [];
1044
+ for (let i = 0; i < fields.length && fields[i]; ) {
1045
+ const code = fields[i++];
1046
+ const first = fields[i++];
1047
+ if (!first) break;
1048
+ const second = /^[RC]/.test(code) ? fields[i++] : void 0;
1049
+ const change = second ? code.startsWith("R") ? { status: "renamed", path: second, previousPath: first } : { status: "added", path: second } : { status: code === "A" ? "added" : code === "D" ? "deleted" : "modified", path: first };
1050
+ if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/"))) continue;
1051
+ changes.push(change);
1052
+ }
1053
+ return changes;
1054
+ }
1055
+ function touchedPaths(changes) {
1056
+ return [...new Set(changes.flatMap((c) => c.previousPath ? [c.previousPath, c.path] : [c.path]))].sort();
1057
+ }
1058
+ async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
1059
+ if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-")) return null;
1060
+ try {
1061
+ const { stdout } = await exec3("git", ["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 });
1062
+ return parseChanges(stdout);
1063
+ } catch {
1064
+ return null;
1065
+ }
1066
+ }
1067
+ async function getWorkingTree(resolvedRoot) {
1068
+ try {
1069
+ const [diff, untracked] = await Promise.all([
1070
+ exec3("git", ["diff", "--name-status", "-z", "-M", "HEAD", "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }),
1071
+ exec3("git", ["ls-files", "-z", "--others", "--exclude-standard"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 })
1072
+ ]);
1073
+ const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
1074
+ return { available: true, changedFiles: [.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])].sort(), untrackedFiles };
1075
+ } catch {
1076
+ return { available: false, changedFiles: [], untrackedFiles: [] };
1077
+ }
1078
+ }
1079
+ async function countCommitsBehind(resolvedRoot, fromHash) {
1080
+ if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-")) return null;
1081
+ try {
1082
+ const { stdout } = await exec3(
1083
+ "git",
1084
+ ["rev-list", "--count", `${fromHash}..HEAD`],
1085
+ { cwd: resolvedRoot }
1086
+ );
1087
+ const count3 = Number.parseInt(stdout.trim(), 10);
1088
+ return Number.isNaN(count3) ? null : count3;
1089
+ } catch {
1090
+ return null;
1091
+ }
1092
+ }
1093
+ function collectMappedFiles(snapshot) {
1094
+ const mappedFiles = /* @__PURE__ */ new Set();
1095
+ for (const feature of Object.values(snapshot.features)) {
1096
+ for (const f of feature.files) mappedFiles.add(f);
1097
+ for (const t of feature.tests ?? []) mappedFiles.add(t);
1098
+ }
1099
+ for (const flow of Object.values(snapshot.flows)) {
1100
+ for (const f of flow.chain) mappedFiles.add(f);
1101
+ }
1102
+ return mappedFiles;
1103
+ }
1104
+ async function findGhostFiles(resolvedRoot, mappedFiles) {
1105
+ const ghosts = [];
1106
+ for (const file of mappedFiles) {
1107
+ try {
1108
+ await fs4.access(path8.join(resolvedRoot, file));
1109
+ } catch {
1110
+ ghosts.push(file);
1111
+ }
1112
+ }
1113
+ return ghosts.sort();
1114
+ }
767
1115
  async function computeDrift(rootDir) {
768
- const root = path6.resolve(rootDir);
1116
+ const root = path8.resolve(rootDir);
769
1117
  const snapshot = await loadSnapshot(root);
770
1118
  if (!snapshot) return null;
771
1119
  const [headHash, workingTree] = await Promise.all([getCurrentGitHash(root), getWorkingTree(root)]);
@@ -1229,8 +1577,8 @@ var init_git = __esm({
1229
1577
  });
1230
1578
 
1231
1579
  // src/audit/docs.ts
1232
- import fs4 from "fs/promises";
1233
- import path7 from "path";
1580
+ import fs5 from "fs/promises";
1581
+ import path9 from "path";
1234
1582
  import { execFile as execFile5 } from "child_process";
1235
1583
  import { promisify as promisify5 } from "util";
1236
1584
  async function isDirty(resolvedRoot, relPath) {
@@ -1249,7 +1597,7 @@ async function discoverDocs(resolvedRoot) {
1249
1597
  const docs = await Promise.all(DOC_CANDIDATES.map(async (candidate) => {
1250
1598
  let content2;
1251
1599
  try {
1252
- content2 = await fs4.readFile(path7.join(resolvedRoot, candidate), "utf-8");
1600
+ content2 = await fs5.readFile(path9.join(resolvedRoot, candidate), "utf-8");
1253
1601
  } catch {
1254
1602
  return null;
1255
1603
  }
@@ -1297,11 +1645,11 @@ var init_types = __esm({
1297
1645
  });
1298
1646
 
1299
1647
  // src/audit/checks/deleted-reference.ts
1300
- import fs5 from "fs/promises";
1301
- import path8 from "path";
1648
+ import fs6 from "fs/promises";
1649
+ import path10 from "path";
1302
1650
  async function exists(absPath) {
1303
1651
  try {
1304
- await fs5.access(absPath);
1652
+ await fs6.access(absPath);
1305
1653
  return true;
1306
1654
  } catch {
1307
1655
  return false;
@@ -1321,7 +1669,7 @@ async function checkDeletedReferences(ctx) {
1321
1669
  if (claim.path === ".mason" || claim.path.startsWith(".mason/")) {
1322
1670
  continue;
1323
1671
  }
1324
- if (await exists(path8.join(ctx.root, claim.path))) continue;
1672
+ if (await exists(path10.join(ctx.root, claim.path))) continue;
1325
1673
  const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };
1326
1674
  const renamedTo = renames.get(claim.path) ?? null;
1327
1675
  if (renamedTo) {
@@ -1357,14 +1705,14 @@ async function checkDeletedReferences(ctx) {
1357
1705
  deletedInCommit: deleted,
1358
1706
  everTracked: true,
1359
1707
  parentDirExists: await exists(
1360
- path8.join(ctx.root, path8.dirname(claim.path))
1708
+ path10.join(ctx.root, path10.dirname(claim.path))
1361
1709
  )
1362
1710
  }
1363
1711
  });
1364
1712
  continue;
1365
1713
  }
1366
1714
  const parentDirExists = await exists(
1367
- path8.join(ctx.root, path8.dirname(claim.path))
1715
+ path10.join(ctx.root, path10.dirname(claim.path))
1368
1716
  );
1369
1717
  if (!parentDirExists) continue;
1370
1718
  const issue = {
@@ -1395,14 +1743,14 @@ var init_deleted_reference = __esm({
1395
1743
  });
1396
1744
 
1397
1745
  // src/audit/inputs.ts
1398
- import fs6 from "fs/promises";
1746
+ import fs7 from "fs/promises";
1399
1747
  import * as nativeFs from "fs";
1400
- import path9 from "path";
1748
+ import path11 from "path";
1401
1749
  import { execFile as execFile6 } from "child_process";
1402
1750
  import { promisify as promisify6 } from "util";
1403
1751
  import fg2 from "fast-glob";
1404
1752
  async function auditInputPath(root, file) {
1405
- if (file === ".") return fs6.realpath(root);
1753
+ if (file === ".") return fs7.realpath(root);
1406
1754
  try {
1407
1755
  return await storePath(root, file);
1408
1756
  } catch (error) {
@@ -1426,7 +1774,7 @@ async function auditGlob(root, patterns, options) {
1426
1774
  if (Array.isArray(patterns) && !patterns.length) return [];
1427
1775
  const directories = /* @__PURE__ */ new Set();
1428
1776
  const readdir2 = ((directory, opts, callback) => {
1429
- const relative = path9.relative(root, directory).split(path9.sep).join("/") || ".";
1777
+ const relative = path11.relative(root, directory).split(path11.sep).join("/") || ".";
1430
1778
  directories.add(relative);
1431
1779
  if (directories.size > MAX_INPUTS) {
1432
1780
  callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
@@ -1437,7 +1785,7 @@ async function auditGlob(root, patterns, options) {
1437
1785
  }, callback);
1438
1786
  });
1439
1787
  const lstat2 = ((file, callback) => {
1440
- const relative = path9.relative(root, file).split(path9.sep).join("/") || ".";
1788
+ const relative = path11.relative(root, file).split(path11.sep).join("/") || ".";
1441
1789
  auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
1442
1790
  });
1443
1791
  const stream = fg2.stream(patterns, {
@@ -1519,8 +1867,8 @@ var init_inputs = __esm({
1519
1867
 
1520
1868
  // src/audit/checks/new-module.ts
1521
1869
  import fg3 from "fast-glob";
1522
- import fs7 from "fs/promises";
1523
- import path10 from "path";
1870
+ import fs8 from "fs/promises";
1871
+ import path12 from "path";
1524
1872
  function escapeRegExp(text2) {
1525
1873
  return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1526
1874
  }
@@ -1562,8 +1910,8 @@ async function moduleCandidates(root, combinedDocs) {
1562
1910
  const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
1563
1911
  const sources = sourcePaths.filter((file) => !file.split("/").some((part) => part.startsWith(".")));
1564
1912
  const listSubdirs = async (dir) => {
1565
- const entries = await fs7.readdir(await auditInputPath(root, dir), { withFileTypes: true });
1566
- const dirs = entries.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink()) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)).map((entry) => path10.posix.join(dir, entry.name));
1913
+ const entries = await fs8.readdir(await auditInputPath(root, dir), { withFileTypes: true });
1914
+ const dirs = entries.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink()) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)).map((entry) => path12.posix.join(dir, entry.name));
1567
1915
  const ignored = await gitIgnoredPaths(root, dirs);
1568
1916
  const visible = new Set(dirs.filter((dir2) => !ignored.has(dir2) || sources.some((file) => file.startsWith(dir2 + "/"))));
1569
1917
  return (await auditGlob(root, dir === "." ? "*" : `${fg3.escapePath(dir)}/*`, {
@@ -1571,7 +1919,7 @@ async function moduleCandidates(root, combinedDocs) {
1571
1919
  onlyDirectories: true,
1572
1920
  label: "Module directory discovery",
1573
1921
  select: (file) => visible.has(file)
1574
- })).map((file) => path10.posix.basename(file));
1922
+ })).map((file) => path12.posix.basename(file));
1575
1923
  };
1576
1924
  const countSourceFiles = async (dir) => {
1577
1925
  const files = sources.filter((file) => file.startsWith(dir + "/"));
@@ -1640,7 +1988,7 @@ var init_new_module = __esm({
1640
1988
  });
1641
1989
 
1642
1990
  // src/audit/checks/stale-count.ts
1643
- import path11 from "path";
1991
+ import path13 from "path";
1644
1992
  async function countGradleModules(root) {
1645
1993
  for (const name of ["settings.gradle.kts", "settings.gradle"]) {
1646
1994
  const content2 = await readAuditInput(root, name);
@@ -1674,7 +2022,7 @@ async function countNpmWorkspaces(root) {
1674
2022
  return {
1675
2023
  actual: matched.length,
1676
2024
  countedFrom: "package.json workspaces",
1677
- members: matched.map((m) => path11.dirname(m)).sort()
2025
+ members: matched.map((m) => path13.dirname(m)).sort()
1678
2026
  };
1679
2027
  }
1680
2028
  }
@@ -1705,7 +2053,7 @@ async function countNpmWorkspaces(root) {
1705
2053
  return {
1706
2054
  actual: matched.length,
1707
2055
  countedFrom: "pnpm-workspace.yaml",
1708
- members: matched.map((m) => path11.dirname(m)).sort()
2056
+ members: matched.map((m) => path13.dirname(m)).sort()
1709
2057
  };
1710
2058
  }
1711
2059
  }
@@ -1727,8 +2075,8 @@ async function countCargoCrates(root) {
1727
2075
  ignore: ["**/target/**"],
1728
2076
  label: "Cargo workspace discovery"
1729
2077
  });
1730
- for (const m of matched) members.add(path11.dirname(m));
1731
- } else if (await readAuditInput(root, path11.posix.join(entry, "Cargo.toml")) !== null) {
2078
+ for (const m of matched) members.add(path13.dirname(m));
2079
+ } else if (await readAuditInput(root, path13.posix.join(entry, "Cargo.toml")) !== null) {
1732
2080
  members.add(entry);
1733
2081
  }
1734
2082
  }
@@ -1885,450 +2233,123 @@ function withoutAndroidReleaseValues(text2) {
1885
2233
  normalized.push(line);
1886
2234
  }
1887
2235
  const named = code.match(/^\s*(android|defaultConfig)\s*\{\s*$/)?.[1];
1888
- for (const brace of code.matchAll(/[{}]/g)) {
1889
- if (brace[0] === "{") scopes.push(named ?? "unknown");
1890
- else if (!scopes.length) return null;
1891
- else scopes.pop();
1892
- }
1893
- }
1894
- return assignments && !scopes.length ? normalized.join("\n") : null;
1895
- }
1896
- async function releaseMetadataOnly(root, commit) {
1897
- if (!commit.files.length || !commit.files.every((file) => /(^|\/)build\.gradle(?:\.kts)?$/.test(file))) return false;
1898
- try {
1899
- const parents = (await git(root, ["rev-list", "--parents", "-n", "1", commit.hash])).trim().split(/\s+/);
1900
- if (parents.length !== 2) return false;
1901
- for (const file of commit.files) {
1902
- const raw = await git(root, ["diff", "--raw", "-z", "--no-renames", "--no-ext-diff", "--no-textconv", parents[1], commit.hash, "--", file]);
1903
- if (!/^:(100644|100755) \1 [a-f0-9]+ [a-f0-9]+ M\0/.test(raw)) return false;
1904
- const [before, after] = await Promise.all([
1905
- git(root, ["show", parents[1] + ":" + file]),
1906
- git(root, ["show", commit.hash + ":" + file])
1907
- ]);
1908
- const previous = withoutAndroidReleaseValues(before);
1909
- if (previous === null || previous !== withoutAndroidReleaseValues(after)) return false;
1910
- }
1911
- return true;
1912
- } catch {
1913
- return false;
1914
- }
1915
- }
1916
- var exec6;
1917
- var init_release_metadata = __esm({
1918
- "src/audit/release-metadata.ts"() {
1919
- "use strict";
1920
- exec6 = promisify7(execFile7);
1921
- }
1922
- });
1923
-
1924
- // src/audit/checks/deps-changed.ts
1925
- async function checkDepsChanged(ctx) {
1926
- const result = emptyResult();
1927
- result.suppressedAdvisories = [];
1928
- const releaseOnly = /* @__PURE__ */ new Map();
1929
- for (const doc of ctx.docs) {
1930
- if (!doc.lastCommit) {
1931
- result.skipped.push({
1932
- check: "deps-changed",
1933
- doc: doc.path,
1934
- reason: `${doc.path} has no commit history`
1935
- });
1936
- continue;
1937
- }
1938
- if (doc.dirty) {
1939
- result.skipped.push({
1940
- check: "deps-changed",
1941
- doc: doc.path,
1942
- reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
1943
- });
1944
- }
1945
- const range = await commitsTouchingSince(
1946
- ctx.root,
1947
- doc.lastCommit.hash,
1948
- MANIFEST_PATHSPECS
1949
- );
1950
- if (range === null) {
1951
- result.skipped.push({
1952
- check: "deps-changed",
1953
- doc: doc.path,
1954
- reason: `${doc.path}: commit range unreachable (shallow clone?)`
1955
- });
1956
- continue;
1957
- }
1958
- const relevant = [];
1959
- for (const commit of range.commits) {
1960
- if (!releaseOnly.has(commit.hash) && releaseOnly.size < 100) {
1961
- releaseOnly.set(commit.hash, await releaseMetadataOnly(ctx.root, commit));
1962
- }
1963
- if (!releaseOnly.get(commit.hash)) relevant.push(commit);
1964
- }
1965
- range.commits = relevant;
1966
- range.total = relevant.length;
1967
- if (range.total === 0) continue;
1968
- const latest = range.commits[0];
1969
- (doc.dirty ? result.suppressedAdvisories : result.advisories).push({
1970
- type: "deps-changed",
1971
- message: `dependency manifests touched by ${range.total} commit${range.total === 1 ? "" : "s"} since ${doc.path} was last committed (latest: ${latest.hash.slice(0, 7)} "${latest.subject}")`,
1972
- anchor: { doc: doc.path, line: null, excerpt: null },
1973
- evidence: {
1974
- kind: "doc-behind-manifests",
1975
- docLastCommit: doc.lastCommit,
1976
- manifestCommits: range.commits.slice(0, MANIFEST_COMMITS_CAP),
1977
- totalCommits: range.total
1978
- }
1979
- });
1980
- }
1981
- return result;
1982
- }
1983
- var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
1984
- var init_deps_changed = __esm({
1985
- "src/audit/checks/deps-changed.ts"() {
1986
- "use strict";
1987
- init_release_metadata();
1988
- init_git();
1989
- init_checks();
1990
- MANIFEST_COMMITS_CAP = 10;
1991
- MANIFEST_PATHSPECS = [
1992
- ":(glob)**/package.json",
1993
- ":(glob)**/build.gradle.kts",
1994
- ":(glob)**/build.gradle",
1995
- "settings.gradle.kts",
1996
- "settings.gradle",
1997
- "gradle/libs.versions.toml",
1998
- ":(glob)**/Cargo.toml",
1999
- "go.mod",
2000
- "pyproject.toml",
2001
- "requirements.txt",
2002
- "Gemfile",
2003
- "composer.json"
2004
- ];
2005
- }
2006
- });
2007
-
2008
- // src/context/lexical.ts
2009
- import path12 from "path";
2010
- function tokenize(text2) {
2011
- return text2.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2 && !STOPWORDS.has(t));
2012
- }
2013
- function stem(token) {
2014
- return token.length > 3 && token.endsWith("s") ? token.slice(0, -1) : token;
2015
- }
2016
- function tokenSet(text2) {
2017
- return new Set(tokenize(text2).map(stem));
2018
- }
2019
- function scoreEntry(taskTokens, entry) {
2020
- const nameTokens = tokenSet(entry.name);
2021
- const descTokens = tokenSet(entry.description);
2022
- const fileTokens = tokenSet(entry.files.map((f) => path12.basename(f)).join(" "));
2023
- let score = 0;
2024
- for (const token of taskTokens) {
2025
- if (nameTokens.has(token)) score += 3;
2026
- else if (descTokens.has(token)) score += 1;
2027
- else if (fileTokens.has(token)) score += 1;
2028
- }
2029
- return score;
2030
- }
2031
- function jaccard(a, b) {
2032
- if (a.size === 0 && b.size === 0) return 0;
2033
- let intersection = 0;
2034
- for (const token of a) if (b.has(token)) intersection++;
2035
- return intersection / (a.size + b.size - intersection);
2036
- }
2037
- var STOPWORDS;
2038
- var init_lexical = __esm({
2039
- "src/context/lexical.ts"() {
2040
- "use strict";
2041
- STOPWORDS = /* @__PURE__ */ new Set([
2042
- "the",
2043
- "a",
2044
- "an",
2045
- "and",
2046
- "or",
2047
- "of",
2048
- "to",
2049
- "in",
2050
- "on",
2051
- "for",
2052
- "with",
2053
- "how",
2054
- "does",
2055
- "do",
2056
- "is",
2057
- "are",
2058
- "was",
2059
- "what",
2060
- "where",
2061
- "which",
2062
- "why",
2063
- "when",
2064
- "who",
2065
- "i",
2066
- "we",
2067
- "my",
2068
- "our",
2069
- "you",
2070
- "your",
2071
- "it",
2072
- "its",
2073
- "this",
2074
- "that",
2075
- "these",
2076
- "those",
2077
- "can",
2078
- "could",
2079
- "should",
2080
- "would",
2081
- "will",
2082
- "want",
2083
- "need",
2084
- "please",
2085
- "about",
2086
- "into",
2087
- "from",
2088
- "when",
2089
- "there",
2090
- "any",
2091
- "all",
2092
- "some",
2093
- "not",
2094
- "but",
2095
- "also",
2096
- "just",
2097
- "like",
2098
- "get",
2099
- "make",
2100
- "use",
2101
- "new",
2102
- "work",
2103
- "works",
2104
- "working",
2105
- "implement",
2106
- "implemented",
2107
- "change",
2108
- "changed",
2109
- "file",
2110
- "files",
2111
- "code"
2112
- ]);
2113
- }
2114
- });
2115
-
2116
- // src/decisions/decisions.ts
2117
- var decisions_exports = {};
2118
- __export(decisions_exports, {
2119
- BODY_MAX_CHARS: () => BODY_MAX_CHARS,
2120
- MAX_ACTIVE_DECISIONS: () => MAX_ACTIVE_DECISIONS,
2121
- TITLE_MAX_CHARS: () => TITLE_MAX_CHARS,
2122
- decisionIdFor: () => decisionIdFor,
2123
- findNearDuplicate: () => findNearDuplicate,
2124
- loadDecisionStore: () => loadDecisionStore,
2125
- loadDecisions: () => loadDecisions,
2126
- saveDecisionRecord: () => saveDecisionRecord,
2127
- upsertDecision: () => upsertDecision,
2128
- withDecisionWrite: () => withDecisionWrite
2129
- });
2130
- import fs8 from "fs/promises";
2131
- import path13 from "path";
2132
- import { createHash } from "crypto";
2133
- async function loadDecisionStore(rootDir) {
2134
- const records = [];
2135
- const diagnostics = [];
2136
- let entries;
2137
- try {
2138
- entries = await fs8.readdir(await storePath(rootDir, ".mason/decisions"));
2139
- } catch (error) {
2140
- if (error.code !== "ENOENT") diagnostics.push({ path: ".mason/decisions", message: String(error) });
2141
- return { records, diagnostics };
2142
- }
2143
- for (const entry of entries.sort()) {
2144
- if (!entry.endsWith(".json")) continue;
2145
- const relative = `.mason/decisions/${entry}`;
2146
- try {
2147
- const record = decisionSchema.parse(await readStoreJson(rootDir, relative));
2148
- if (entry !== `${record.id}.json`) throw new Error("Record id does not match its filename");
2149
- records.push(record);
2150
- } catch (error) {
2151
- diagnostics.push({ path: relative, message: error instanceof Error ? error.message : String(error) });
2152
- }
2153
- }
2154
- return { records, diagnostics };
2155
- }
2156
- async function loadDecisions(rootDir) {
2157
- return (await loadDecisionStore(rootDir)).records;
2158
- }
2159
- async function saveDecisionRecord(rootDir, record) {
2160
- const validated = decisionSchema.parse(record);
2161
- await writeStoreJson(rootDir, `.mason/decisions/${validated.id}.json`, validated);
2162
- }
2163
- function decisionIdFor(title, body, existingIds) {
2164
- const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/, "");
2165
- if (!existingIds.has(slug)) return slug || "decision";
2166
- const suffix = createHash("sha1").update(title + body).digest("hex").slice(0, 6);
2167
- return `${slug}-${suffix}`;
2168
- }
2169
- function findNearDuplicate(candidate, existing) {
2170
- const candidateTokens = tokenSet(`${candidate.title} ${candidate.body}`);
2171
- const candidateFiles = new Set(candidate.files);
2172
- let best = null;
2173
- for (const record of existing) {
2174
- if (record.status !== "active") continue;
2175
- const similarity = jaccard(
2176
- candidateTokens,
2177
- tokenSet(`${record.title} ${record.body}`)
2178
- );
2179
- const sharesFile = record.files.some((f) => candidateFiles.has(f));
2180
- const threshold = sharesFile ? DUPLICATE_JACCARD_WITH_SHARED_FILE : DUPLICATE_JACCARD;
2181
- if (similarity >= threshold && (!best || similarity > best.similarity)) {
2182
- best = { record, similarity };
2183
- }
2184
- }
2185
- return best;
2186
- }
2187
- async function withDecisionWrite(root, operation) {
2188
- const lockPath = await storePath(root, ".mason/decisions/.write-lock", true);
2189
- let lock;
2190
- try {
2191
- lock = await fs8.open(lockPath, "wx", 384);
2192
- } catch (error) {
2193
- if (error.code === "EEXIST") return { status: "error", error: "Decision store is locked by another write. Retry after it finishes; an abandoned .mason/decisions/.write-lock must be removed only after confirming no writer is running." };
2194
- throw error;
2195
- }
2196
- try {
2197
- return await operation();
2198
- } finally {
2199
- await lock.close();
2200
- await fs8.unlink(lockPath);
2201
- }
2202
- }
2203
- async function upsertDecision(rootDir, input) {
2204
- const title = input.title.trim(), body = input.body.trim();
2205
- if (!title || !body) return { status: "error", error: "title and body must be non-empty" };
2206
- if (title.length > TITLE_MAX_CHARS) return { status: "error", error: `title exceeds ${TITLE_MAX_CHARS} chars \u2014 tighten it to a specific headline` };
2207
- if (body.length > BODY_MAX_CHARS) return { status: "error", error: `body exceeds ${BODY_MAX_CHARS} chars \u2014 record the decision, not the transcript` };
2208
- const attribution = attributionSchema.safeParse(input);
2209
- if (!attribution.success) return { status: "error", error: attribution.error.message };
2210
- if (input.id && input.supersedes) return { status: "error", error: "Use either id to revise or supersedes to replace a record, not both." };
2211
- return withDecisionWrite(rootDir, async () => {
2212
- const store = await loadDecisionStore(rootDir);
2213
- if (store.diagnostics.length) return { status: "error", error: "Repair malformed decision records before saving: " + store.diagnostics.map((d) => d.path).join(", ") };
2214
- const existing = store.records, byId = new Map(existing.map((r) => [r.id, r]));
2215
- const now = (/* @__PURE__ */ new Date()).toISOString(), head = await getCurrentGitHash(rootDir);
2216
- const warnings = [];
2217
- const files = sanitizeRepoPaths(input.files ?? []);
2218
- if (input.files && files.length < input.files.length) warnings.push("some anchor paths were outside the repo or duplicated and were dropped");
2219
- for (const file of files) {
2220
- try {
2221
- await fs8.access(path13.join(rootDir, file));
2222
- } catch {
2223
- warnings.push(`anchor file does not exist on disk: ${file}`);
2224
- }
2225
- }
2226
- const hint = "Saved locally for review and commit. Proposals are not accepted constraints; an existing accepted revision remains operative while its replacement is proposed. Use review_decision to inspect evidence and record an authorized acceptance or reaffirmation.";
2227
- if (input.id) {
2228
- const original = byId.get(input.id);
2229
- if (!original) return { status: "error", error: `no decision with id "${input.id}"` };
2230
- if (original.status !== "active") return { status: "error", error: "Archived records cannot be revised; create a new proposal." };
2231
- const record2 = importLegacy(original, now);
2232
- const content3 = decisionContent({
2233
- ...record2,
2234
- title,
2235
- body,
2236
- category: input.category,
2237
- files: input.files !== void 0 ? files : record2.files,
2238
- owner: attribution.data.owner === void 0 ? record2.owner : attribution.data.owner ?? void 0,
2239
- sources: attribution.data.sources ?? record2.sources
2240
- });
2241
- if (JSON.stringify(content3) === JSON.stringify(decisionContent(record2))) {
2242
- return {
2243
- status: "unchanged",
2244
- id: record2.id,
2245
- totalActive: existing.filter((r) => r.status === "active").length,
2246
- approval: decisionApproval(original),
2247
- warnings,
2248
- hint: "Unchanged content; no review or freshness stamp was written. Use review_decision for explicit re-verification."
2249
- };
2250
- }
2251
- const revision = record2.revision + 1;
2252
- const updated = {
2253
- ...record2,
2254
- ...content3,
2255
- owner: content3.owner,
2256
- updatedAt: now,
2257
- revision,
2258
- approval: "proposed",
2259
- history: [...record2.history, { kind: "revised", at: now, actor: attribution.data.actor, revision, content: content3, approval: "proposed", status: "active", refreshedHash: record2.refreshedHash }]
2260
- };
2261
- await saveDecisionRecord(rootDir, updated);
2262
- return { status: "updated", id: record2.id, totalActive: existing.filter((r) => r.status === "active").length, approval: "proposed", warnings, hint };
2236
+ for (const brace of code.matchAll(/[{}]/g)) {
2237
+ if (brace[0] === "{") scopes.push(named ?? "unknown");
2238
+ else if (!scopes.length) return null;
2239
+ else scopes.pop();
2263
2240
  }
2264
- const old = input.supersedes ? byId.get(input.supersedes) : void 0;
2265
- if (input.supersedes && !old) return { status: "error", error: `no decision with id "${input.supersedes}" to supersede` };
2266
- if (old && (old.status !== "active" || decisionApproval(effectiveDecision(old)) === "accepted")) {
2267
- return { status: "error", error: "A proposal cannot supersede an accepted or archived record. Create and review the replacement separately, then explicitly retire the old decision with review_decision." };
2241
+ }
2242
+ return assignments && !scopes.length ? normalized.join("\n") : null;
2243
+ }
2244
+ async function releaseMetadataOnly(root, commit) {
2245
+ if (!commit.files.length || !commit.files.every((file) => /(^|\/)build\.gradle(?:\.kts)?$/.test(file))) return false;
2246
+ try {
2247
+ const parents = (await git(root, ["rev-list", "--parents", "-n", "1", commit.hash])).trim().split(/\s+/);
2248
+ if (parents.length !== 2) return false;
2249
+ for (const file of commit.files) {
2250
+ const raw = await git(root, ["diff", "--raw", "-z", "--no-renames", "--no-ext-diff", "--no-textconv", parents[1], commit.hash, "--", file]);
2251
+ if (!/^:(100644|100755) \1 [a-f0-9]+ [a-f0-9]+ M\0/.test(raw)) return false;
2252
+ const [before, after] = await Promise.all([
2253
+ git(root, ["show", parents[1] + ":" + file]),
2254
+ git(root, ["show", commit.hash + ":" + file])
2255
+ ]);
2256
+ const previous = withoutAndroidReleaseValues(before);
2257
+ if (previous === null || previous !== withoutAndroidReleaseValues(after)) return false;
2268
2258
  }
2269
- if (!input.force) {
2270
- const duplicate = findNearDuplicate({ title, body, files }, existing);
2271
- if (duplicate) return { status: "duplicate_suspected", existing: duplicate.record, hint: `A similar decision exists ("${duplicate.record.title}"). Call save_decision with id="${duplicate.record.id}" to revise it, or force:true if distinct.` };
2259
+ return true;
2260
+ } catch {
2261
+ return false;
2262
+ }
2263
+ }
2264
+ var exec6;
2265
+ var init_release_metadata = __esm({
2266
+ "src/audit/release-metadata.ts"() {
2267
+ "use strict";
2268
+ exec6 = promisify7(execFile7);
2269
+ }
2270
+ });
2271
+
2272
+ // src/audit/checks/deps-changed.ts
2273
+ async function checkDepsChanged(ctx) {
2274
+ const result = emptyResult();
2275
+ result.suppressedAdvisories = [];
2276
+ const releaseOnly = /* @__PURE__ */ new Map();
2277
+ for (const doc of ctx.docs) {
2278
+ if (!doc.lastCommit) {
2279
+ result.skipped.push({
2280
+ check: "deps-changed",
2281
+ doc: doc.path,
2282
+ reason: `${doc.path} has no commit history`
2283
+ });
2284
+ continue;
2272
2285
  }
2273
- const id = decisionIdFor(title, body, new Set(byId.keys()));
2274
- if (byId.has(id)) return { status: "error", error: `Decision id collision: ${id}. Choose a distinct title or revise the existing record.` };
2275
- const content2 = decisionContent({ title, body, category: input.category, files, owner: attribution.data.owner ?? void 0, sources: attribution.data.sources ?? [] });
2276
- const record = {
2277
- ...content2,
2278
- version: 2,
2279
- id,
2280
- createdAt: now,
2281
- updatedAt: now,
2282
- refreshedHash: head,
2283
- status: "active",
2284
- approval: "proposed",
2285
- revision: 1,
2286
- history: [{ kind: "created", at: now, actor: attribution.data.actor, revision: 1, content: content2, approval: "proposed", status: "active", refreshedHash: head }]
2287
- };
2288
- await saveDecisionRecord(rootDir, record);
2289
- if (old) {
2290
- const imported = importLegacy(old, now);
2291
- await saveDecisionRecord(rootDir, {
2292
- ...imported,
2293
- status: "superseded",
2294
- supersededBy: id,
2295
- updatedAt: now,
2296
- history: [...imported.history, {
2297
- kind: "superseded",
2298
- at: now,
2299
- actor: attribution.data.actor,
2300
- note: `Replaced by proposal ${id}`,
2301
- revision: imported.revision,
2302
- content: decisionContent(imported),
2303
- approval: imported.approval,
2304
- status: "superseded",
2305
- refreshedHash: imported.refreshedHash
2306
- }]
2286
+ if (doc.dirty) {
2287
+ result.skipped.push({
2288
+ check: "deps-changed",
2289
+ doc: doc.path,
2290
+ reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
2307
2291
  });
2308
2292
  }
2309
- const totalActive = existing.filter((r) => r.status === "active").length + (old ? 0 : 1);
2310
- const result = { status: old ? "superseded_and_created" : "created", id, totalActive, approval: "proposed", warnings, hint };
2311
- if (totalActive > MAX_ACTIVE_DECISIONS) {
2312
- result.pruneCandidates = existing.filter((r) => r.status !== "active").map((r) => r.id).slice(0, 10);
2313
- warnings.push(`${totalActive} active decisions exceeds the soft cap of ${MAX_ACTIVE_DECISIONS} \u2014 consider a cleanup PR (archived records first)`);
2293
+ const range = await commitsTouchingSince(
2294
+ ctx.root,
2295
+ doc.lastCommit.hash,
2296
+ MANIFEST_PATHSPECS
2297
+ );
2298
+ if (range === null) {
2299
+ result.skipped.push({
2300
+ check: "deps-changed",
2301
+ doc: doc.path,
2302
+ reason: `${doc.path}: commit range unreachable (shallow clone?)`
2303
+ });
2304
+ continue;
2314
2305
  }
2315
- return result;
2316
- });
2306
+ const relevant = [];
2307
+ for (const commit of range.commits) {
2308
+ if (!releaseOnly.has(commit.hash) && releaseOnly.size < 100) {
2309
+ releaseOnly.set(commit.hash, await releaseMetadataOnly(ctx.root, commit));
2310
+ }
2311
+ if (!releaseOnly.get(commit.hash)) relevant.push(commit);
2312
+ }
2313
+ range.commits = relevant;
2314
+ range.total = relevant.length;
2315
+ if (range.total === 0) continue;
2316
+ const latest = range.commits[0];
2317
+ (doc.dirty ? result.suppressedAdvisories : result.advisories).push({
2318
+ type: "deps-changed",
2319
+ message: `dependency manifests touched by ${range.total} commit${range.total === 1 ? "" : "s"} since ${doc.path} was last committed (latest: ${latest.hash.slice(0, 7)} "${latest.subject}")`,
2320
+ anchor: { doc: doc.path, line: null, excerpt: null },
2321
+ evidence: {
2322
+ kind: "doc-behind-manifests",
2323
+ docLastCommit: doc.lastCommit,
2324
+ manifestCommits: range.commits.slice(0, MANIFEST_COMMITS_CAP),
2325
+ totalCommits: range.total
2326
+ }
2327
+ });
2328
+ }
2329
+ return result;
2317
2330
  }
2318
- var TITLE_MAX_CHARS, BODY_MAX_CHARS, MAX_ACTIVE_DECISIONS, DUPLICATE_JACCARD, DUPLICATE_JACCARD_WITH_SHARED_FILE;
2319
- var init_decisions = __esm({
2320
- "src/decisions/decisions.ts"() {
2331
+ var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
2332
+ var init_deps_changed = __esm({
2333
+ "src/audit/checks/deps-changed.ts"() {
2321
2334
  "use strict";
2322
- init_storage();
2323
- init_paths();
2324
- init_snapshot();
2325
- init_lexical();
2326
- init_provenance();
2327
- TITLE_MAX_CHARS = 80;
2328
- BODY_MAX_CHARS = 1500;
2329
- MAX_ACTIVE_DECISIONS = 150;
2330
- DUPLICATE_JACCARD = 0.5;
2331
- DUPLICATE_JACCARD_WITH_SHARED_FILE = 0.35;
2335
+ init_release_metadata();
2336
+ init_git();
2337
+ init_checks();
2338
+ MANIFEST_COMMITS_CAP = 10;
2339
+ MANIFEST_PATHSPECS = [
2340
+ ":(glob)**/package.json",
2341
+ ":(glob)**/build.gradle.kts",
2342
+ ":(glob)**/build.gradle",
2343
+ "settings.gradle.kts",
2344
+ "settings.gradle",
2345
+ "gradle/libs.versions.toml",
2346
+ ":(glob)**/Cargo.toml",
2347
+ "go.mod",
2348
+ "pyproject.toml",
2349
+ "requirements.txt",
2350
+ "Gemfile",
2351
+ "composer.json"
2352
+ ];
2332
2353
  }
2333
2354
  });
2334
2355
 
@@ -2527,7 +2548,7 @@ var init_audit = __esm({
2527
2548
  import fs10 from "fs/promises";
2528
2549
  import path16 from "path";
2529
2550
  import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
2530
- import { z as z3 } from "zod";
2551
+ import { z as z4 } from "zod";
2531
2552
  function findingId(finding) {
2532
2553
  const e = finding.evidence;
2533
2554
  let key;
@@ -2581,7 +2602,7 @@ async function stableAudit(root, checks, options = {}) {
2581
2602
  }
2582
2603
  async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
2583
2604
  const root = await fs10.realpath(rootDir);
2584
- const selected = z3.array(checkSchema).nonempty().parse(checks);
2605
+ const selected = z4.array(checkSchema).nonempty().parse(checks);
2585
2606
  const report = await stableAudit(root, selected, options);
2586
2607
  if (!report) throw new Error("No context files found to prepare a repair.");
2587
2608
  if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
@@ -2672,7 +2693,7 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
2672
2693
  scope: "Original audit checks over current context files and repository evidence. Resolved means no longer detected by that check. Advisories require separate review; this is not a certification of documentation or application correctness."
2673
2694
  };
2674
2695
  }
2675
- var checkSchema, commitSchema, anchorSchema, count, evidenceSchema, findingSchema, issueSchema, advisorySchema, checkResultSchema, reportSchema, baselineSchema, digest;
2696
+ var checkSchema, commitSchema, anchorSchema, count, evidenceSchema, findingSchema, issueSchema, advisorySchema, checkResultSchema, reportSchema, baselineSchema, digest, repairFindingSchema;
2676
2697
  var init_repair = __esm({
2677
2698
  "src/audit/repair.ts"() {
2678
2699
  "use strict";
@@ -2684,95 +2705,102 @@ var init_repair = __esm({
2684
2705
  init_files();
2685
2706
  init_paths();
2686
2707
  init_types();
2687
- checkSchema = z3.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
2688
- commitSchema = z3.object({ hash: z3.string().regex(/^[a-f0-9]{40,64}$/), date: z3.string(), subject: z3.string() });
2689
- anchorSchema = z3.object({ doc: z3.string(), line: z3.number().int().positive().nullable(), excerpt: z3.string().nullable() });
2690
- count = z3.number().int().nonnegative();
2691
- evidenceSchema = z3.discriminatedUnion("kind", [
2692
- z3.object({
2693
- kind: z3.literal("missing-path"),
2694
- claimed: z3.string(),
2695
- renamedTo: z3.string().nullable(),
2708
+ checkSchema = z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
2709
+ commitSchema = z4.object({ hash: z4.string().regex(/^[a-f0-9]{40,64}$/), date: z4.string(), subject: z4.string() });
2710
+ anchorSchema = z4.object({ doc: z4.string(), line: z4.number().int().positive().nullable(), excerpt: z4.string().nullable() });
2711
+ count = z4.number().int().nonnegative();
2712
+ evidenceSchema = z4.discriminatedUnion("kind", [
2713
+ z4.object({
2714
+ kind: z4.literal("missing-path"),
2715
+ claimed: z4.string(),
2716
+ renamedTo: z4.string().nullable(),
2696
2717
  deletedInCommit: commitSchema.nullable(),
2697
- everTracked: z3.boolean(),
2698
- parentDirExists: z3.boolean()
2718
+ everTracked: z4.boolean(),
2719
+ parentDirExists: z4.boolean()
2699
2720
  }),
2700
- z3.object({
2701
- kind: z3.literal("unmentioned-dir"),
2702
- dir: z3.string(),
2721
+ z4.object({
2722
+ kind: z4.literal("unmentioned-dir"),
2723
+ dir: z4.string(),
2703
2724
  sourceFileCount: count,
2704
2725
  firstCommit: commitSchema.nullable(),
2705
- checkedDocs: z3.array(z3.string())
2726
+ checkedDocs: z4.array(z4.string())
2706
2727
  }),
2707
- z3.object({
2708
- kind: z3.literal("count-mismatch"),
2728
+ z4.object({
2729
+ kind: z4.literal("count-mismatch"),
2709
2730
  claimed: count,
2710
2731
  actual: count,
2711
- unit: z3.string(),
2712
- countedFrom: z3.string(),
2713
- members: z3.array(z3.string())
2732
+ unit: z4.string(),
2733
+ countedFrom: z4.string(),
2734
+ members: z4.array(z4.string())
2714
2735
  }),
2715
- z3.object({
2716
- kind: z3.literal("missing-script"),
2717
- scriptName: z3.string(),
2718
- invocation: z3.string(),
2719
- manifestsChecked: z3.array(z3.string()),
2720
- availableScripts: z3.array(z3.string())
2736
+ z4.object({
2737
+ kind: z4.literal("missing-script"),
2738
+ scriptName: z4.string(),
2739
+ invocation: z4.string(),
2740
+ manifestsChecked: z4.array(z4.string()),
2741
+ availableScripts: z4.array(z4.string())
2721
2742
  }),
2722
- z3.object({
2723
- kind: z3.literal("doc-behind-manifests"),
2743
+ z4.object({
2744
+ kind: z4.literal("doc-behind-manifests"),
2724
2745
  docLastCommit: commitSchema,
2725
- manifestCommits: z3.array(commitSchema.extend({ files: z3.array(z3.string()) })),
2746
+ manifestCommits: z4.array(commitSchema.extend({ files: z4.array(z4.string()) })),
2726
2747
  totalCommits: count
2727
2748
  }),
2728
- z3.object({
2729
- kind: z3.literal("decision-anchor"),
2730
- decisionId: z3.string(),
2731
- title: z3.string(),
2732
- changedFiles: z3.array(z3.string()),
2733
- refreshedHash: z3.string(),
2734
- provenance: z3.object({}).passthrough().optional()
2749
+ z4.object({
2750
+ kind: z4.literal("decision-anchor"),
2751
+ decisionId: z4.string(),
2752
+ title: z4.string(),
2753
+ changedFiles: z4.array(z4.string()),
2754
+ refreshedHash: z4.string(),
2755
+ provenance: z4.object({}).passthrough().optional()
2735
2756
  })
2736
2757
  ]);
2737
- findingSchema = z3.object({ message: z3.string(), anchor: anchorSchema, evidence: evidenceSchema });
2758
+ findingSchema = z4.object({ message: z4.string(), anchor: anchorSchema, evidence: evidenceSchema });
2738
2759
  issueSchema = findingSchema.extend({
2739
- type: z3.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
2740
- confidence: z3.enum(["certain", "likely"])
2760
+ type: z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
2761
+ confidence: z4.enum(["certain", "likely"])
2741
2762
  });
2742
- advisorySchema = findingSchema.extend({ type: z3.enum(["deps-changed", "decision-anchor-drift"]) });
2743
- checkResultSchema = z3.object({
2744
- issues: z3.array(issueSchema),
2745
- advisories: z3.array(advisorySchema),
2746
- suppressedAdvisories: z3.array(advisorySchema).optional(),
2747
- skipped: z3.array(z3.object({ check: z3.string(), reason: z3.string(), doc: z3.string().optional() }))
2763
+ advisorySchema = findingSchema.extend({ type: z4.enum(["deps-changed", "decision-anchor-drift"]) });
2764
+ checkResultSchema = z4.object({
2765
+ issues: z4.array(issueSchema),
2766
+ advisories: z4.array(advisorySchema),
2767
+ suppressedAdvisories: z4.array(advisorySchema).optional(),
2768
+ skipped: z4.array(z4.object({ check: z4.string(), reason: z4.string(), doc: z4.string().optional() }))
2748
2769
  });
2749
- reportSchema = z3.object({
2750
- version: z3.literal(1),
2751
- root: z3.string(),
2752
- gitAvailable: z3.literal(true),
2770
+ reportSchema = z4.object({
2771
+ version: z4.literal(1),
2772
+ root: z4.string(),
2773
+ gitAvailable: z4.literal(true),
2753
2774
  headHash: commitSchema.shape.hash,
2754
- checksRun: z3.array(checkSchema).nonempty(),
2755
- docs: z3.array(z3.object({
2756
- path: z3.enum(DOC_CANDIDATES),
2775
+ checksRun: z4.array(checkSchema).nonempty(),
2776
+ docs: z4.array(z4.object({
2777
+ path: z4.enum(DOC_CANDIDATES),
2757
2778
  lastCommit: commitSchema.nullable(),
2758
- dirty: z3.boolean(),
2779
+ dirty: z4.boolean(),
2759
2780
  lineCount: count
2760
2781
  })).nonempty(),
2761
- decisionsChecked: z3.boolean(),
2762
- clean: z3.boolean(),
2763
- issues: z3.array(issueSchema),
2764
- advisories: z3.array(advisorySchema),
2765
- suppressedAdvisories: z3.array(advisorySchema).optional(),
2766
- skippedChecks: z3.array(z3.object({ check: z3.string(), reason: z3.string(), doc: z3.string().optional() }))
2782
+ decisionsChecked: z4.boolean(),
2783
+ clean: z4.boolean(),
2784
+ issues: z4.array(issueSchema),
2785
+ advisories: z4.array(advisorySchema),
2786
+ suppressedAdvisories: z4.array(advisorySchema).optional(),
2787
+ skippedChecks: z4.array(z4.object({ check: z4.string(), reason: z4.string(), doc: z4.string().optional() }))
2767
2788
  });
2768
- baselineSchema = z3.object({
2769
- kind: z3.literal("mason-audit-repair"),
2770
- version: z3.literal(1),
2771
- createdAt: z3.string().datetime(),
2789
+ baselineSchema = z4.object({
2790
+ kind: z4.literal("mason-audit-repair"),
2791
+ version: z4.literal(1),
2792
+ createdAt: z4.string().datetime(),
2772
2793
  report: reportSchema,
2773
- digest: z3.string().regex(/^[a-f0-9]{64}$/)
2794
+ digest: z4.string().regex(/^[a-f0-9]{64}$/)
2774
2795
  });
2775
2796
  digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
2797
+ repairFindingSchema = z4.object({
2798
+ id: z4.string(),
2799
+ original: z4.union([issueSchema, advisorySchema]),
2800
+ status: z4.enum(["resolved", "unresolved", "review-required", "unverified"]),
2801
+ reason: z4.string(),
2802
+ current: z4.union([issueSchema, advisorySchema]).optional()
2803
+ });
2776
2804
  }
2777
2805
  });
2778
2806
 
@@ -2865,134 +2893,101 @@ var init_files2 = __esm({
2865
2893
  }
2866
2894
  });
2867
2895
 
2868
- // src/automation/execution.ts
2869
- var execution_exports = {};
2870
- __export(execution_exports, {
2871
- automationFailure: () => automationFailure,
2872
- executionStatus: () => executionStatus,
2873
- failureMessage: () => failureMessage,
2874
- recordExecution: () => recordExecution
2875
- });
2896
+ // src/automation/store.ts
2897
+ import fs14 from "fs/promises";
2876
2898
  import os from "os";
2877
- import { randomUUID as randomUUID4 } from "crypto";
2878
- import { z as z6 } from "zod";
2879
- function parseExecution(raw) {
2880
- try {
2881
- return executionSchema.parse(raw);
2882
- } catch (error) {
2883
- throw new Error("Invalid automation execution store; receipt history was retained.", { cause: error });
2884
- }
2885
- }
2886
- function automationFailure(error) {
2887
- const recorded = failureSchema.safeParse(error?.failure);
2888
- if (recorded.success) return recorded.data;
2889
- const message2 = (error instanceof Error ? error.message : String(error)).replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 700);
2890
- const codes = /* @__PURE__ */ new Set();
2891
- let cause = error;
2892
- for (let i = 0; cause && i < 8; i++) {
2893
- codes.add(String(cause.code));
2894
- cause = cause.cause;
2895
- }
2896
- 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 z6.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";
2897
- return { code, message: message2, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
2898
- }
2899
- function failureMessage(error) {
2900
- const failure = automationFailure(error);
2901
- return `Mason automation unavailable [${failure.code}]; evidence capture/verification was not established. ${failure.message}` + (failure.receiptRecorded ? "" : " No durable failure receipt was recorded.");
2902
- }
2903
- async function recordExecution(root, directory, event, run) {
2904
- const file = directory + "/execution.json";
2905
- const raw = await readStoreJson(root, file);
2906
- const log = raw === null ? { version: 1, attempts: [], discardedAttempts: 0 } : parseExecution(raw);
2907
- for (const attempt2 of log.attempts) if (attempt2.status === "running") attempt2.status = "unknown";
2908
- log.discardedAttempts += Math.max(0, log.attempts.length - 31);
2909
- log.attempts = log.attempts.slice(-31);
2910
- const started = performance.now();
2911
- const attempt = {
2912
- id: randomUUID4(),
2913
- event,
2914
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2915
- pid: process.pid,
2916
- host: os.hostname(),
2917
- status: "running"
2918
- };
2919
- log.attempts.push(attempt);
2920
- await writeStoreJson(root, file, log);
2899
+ import { z as z7 } from "zod";
2900
+ function parseState(raw) {
2921
2901
  try {
2922
- const result = await run();
2923
- Object.assign(attempt, {
2924
- status: "completed",
2925
- finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
2926
- durationMs: performance.now() - started,
2927
- verificationStatus: result.report.status,
2928
- reportPath: result.report.reportPath
2929
- });
2930
- await writeStoreJson(root, file, log);
2931
- return result;
2902
+ return stateSchema.parse(raw);
2932
2903
  } catch (error) {
2933
- const failure = automationFailure(error);
2934
- Object.assign(attempt, { status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), durationMs: performance.now() - started, failure });
2935
- delete attempt.verificationStatus;
2936
- delete attempt.reportPath;
2937
- try {
2938
- await writeStoreJson(root, file, { ...log, attempts: log.attempts.map((a) => a === attempt ? { ...a, failure: { ...failure, receiptRecorded: true } } : a) });
2939
- failure.receiptRecorded = true;
2940
- } catch {
2941
- }
2942
- throw Object.assign(new Error(failure.message, { cause: error }), { failure });
2904
+ throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
2943
2905
  }
2944
2906
  }
2945
- async function executionStatus(root, directory) {
2946
- const raw = await readStoreJson(root, directory + "/execution.json");
2947
- if (raw === null) return { status: "not-observed", attempts: [] };
2948
- const log = parseExecution(raw);
2949
- let lock = null;
2950
- if (log.attempts.some((attempt) => attempt.status === "running")) {
2907
+ async function withLock(root, directory, run, waitMs = 5e3) {
2908
+ const file = await storePath(root, directory + "/lock", true);
2909
+ const deadline = Date.now() + waitMs;
2910
+ let handle;
2911
+ while (!handle) {
2951
2912
  try {
2952
- lock = await readStoreJson(root, directory + "/lock");
2953
- } catch {
2954
- }
2955
- }
2956
- for (const attempt of log.attempts) {
2957
- if (attempt.status !== "running") continue;
2958
- let alive = false;
2959
- if (attempt.host === os.hostname() && lock?.pid === attempt.pid && lock.host === attempt.host) {
2913
+ handle = await fs14.open(file, "wx", 384);
2960
2914
  try {
2961
- process.kill(attempt.pid, 0);
2962
- alive = true;
2915
+ await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
2963
2916
  } catch (error) {
2964
- alive = error.code === "EPERM";
2917
+ await handle.close().catch(() => {
2918
+ });
2919
+ handle = void 0;
2920
+ await fs14.rm(file, { force: true }).catch(() => {
2921
+ });
2922
+ throw error;
2923
+ }
2924
+ } catch (error) {
2925
+ if (error.code !== "EEXIST") throw error;
2926
+ try {
2927
+ const owner = JSON.parse(await fs14.readFile(file, "utf8"));
2928
+ if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
2929
+ try {
2930
+ process.kill(owner.pid, 0);
2931
+ } catch (probe) {
2932
+ if (probe.code === "ESRCH") {
2933
+ const reclaim = file + ".reclaim";
2934
+ let guard;
2935
+ try {
2936
+ guard = await fs14.open(reclaim, "wx", 384);
2937
+ const current = JSON.parse(await fs14.readFile(file, "utf8"));
2938
+ if (current.pid === owner.pid && current.host === owner.host) await fs14.unlink(file);
2939
+ } finally {
2940
+ if (guard) {
2941
+ await guard.close();
2942
+ await fs14.rm(reclaim, { force: true });
2943
+ }
2944
+ }
2945
+ }
2946
+ }
2947
+ }
2948
+ } catch {
2965
2949
  }
2950
+ if (Date.now() >= deadline) throw new Error("Automation is busy or its lock needs inspection: " + file);
2951
+ await new Promise((resolve2) => setTimeout(resolve2, 40));
2966
2952
  }
2967
- if (!alive) attempt.status = "unknown";
2968
2953
  }
2969
- return { status: log.attempts.at(-1)?.status ?? "not-observed", attempts: log.attempts, discardedAttempts: log.discardedAttempts };
2954
+ try {
2955
+ return await run();
2956
+ } finally {
2957
+ await handle.close();
2958
+ await fs14.unlink(file);
2959
+ }
2970
2960
  }
2971
- var failureSchema, attemptSchema, executionSchema;
2972
- var init_execution = __esm({
2973
- "src/automation/execution.ts"() {
2961
+ var hostSchema, events, stateSchema;
2962
+ var init_store = __esm({
2963
+ "src/automation/store.ts"() {
2974
2964
  "use strict";
2975
2965
  init_storage();
2976
- failureSchema = z6.object({
2977
- code: z6.enum(["inputs-changed", "storage-full", "busy", "invalid-input", "history-unavailable", "invalid-evidence", "io-error", "internal"]),
2978
- message: z6.string(),
2979
- retryable: z6.boolean(),
2980
- receiptRecorded: z6.boolean()
2981
- });
2982
- attemptSchema = z6.object({
2983
- id: z6.string(),
2984
- event: z6.string(),
2985
- startedAt: z6.string(),
2986
- finishedAt: z6.string().optional(),
2987
- durationMs: z6.number().nonnegative().optional(),
2988
- pid: z6.number().int().positive(),
2989
- host: z6.string(),
2990
- status: z6.enum(["running", "completed", "failed", "unknown"]),
2991
- verificationStatus: z6.string().optional(),
2992
- reportPath: z6.string().optional(),
2993
- failure: failureSchema.optional()
2966
+ hostSchema = z7.enum(["claude", "codex"]);
2967
+ events = ["session_start", "turn_start", "before_tool", "after_tool", "task_end"];
2968
+ stateSchema = z7.object({
2969
+ version: z7.literal(1),
2970
+ root: z7.string(),
2971
+ gitDir: z7.string(),
2972
+ branch: z7.string(),
2973
+ baselines: z7.array(z7.object({ path: z7.string(), at: z7.string(), event: z7.string(), fingerprint: z7.string() })).max(128),
2974
+ sessions: z7.record(z7.object({
2975
+ host: hostSchema,
2976
+ seen: z7.string().nullable(),
2977
+ continued: z7.boolean(),
2978
+ initialIssues: z7.array(z7.string()),
2979
+ initialDocs: z7.record(z7.string().nullable()),
2980
+ lastUsed: z7.string(),
2981
+ mutationObserved: z7.boolean(),
2982
+ pending: z7.record(z7.string()),
2983
+ coverageGaps: z7.array(z7.string()),
2984
+ events: z7.record(z7.object({ at: z7.string(), count: z7.number().int().positive() }))
2985
+ })),
2986
+ updatedAt: z7.string(),
2987
+ fingerprint: z7.string().nullable(),
2988
+ latest: z7.string().nullable(),
2989
+ analysis: z7.object({ id: z7.string(), fingerprint: z7.string(), completedAt: z7.number(), path: z7.string() }).optional()
2994
2990
  });
2995
- executionSchema = z6.object({ version: z6.literal(1), attempts: z6.array(attemptSchema).max(32), discardedAttempts: z6.number().int().nonnegative().default(0) });
2996
2991
  }
2997
2992
  });
2998
2993
 
@@ -3005,19 +3000,19 @@ __export(evidence_exports, {
3005
3000
  readInputs: () => readInputs,
3006
3001
  workspace: () => workspace
3007
3002
  });
3008
- import fs14 from "fs/promises";
3003
+ import fs15 from "fs/promises";
3009
3004
  import path20 from "path";
3010
3005
  import { createHash as createHash4 } from "crypto";
3011
3006
  import { execFile as execFile12 } from "child_process";
3012
3007
  import { promisify as promisify12 } from "util";
3013
3008
  import fg5 from "fast-glob";
3014
- import { z as z7 } from "zod";
3009
+ import { z as z8 } from "zod";
3015
3010
  async function git2(root, ...args) {
3016
3011
  return (await exec11("git", args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout;
3017
3012
  }
3018
3013
  async function workspace(dir) {
3019
- const root = await fs14.realpath((await git2(dir, "rev-parse", "--show-toplevel")).trim());
3020
- const gitDir = await fs14.realpath((await git2(root, "rev-parse", "--absolute-git-dir")).trim());
3014
+ const root = await fs15.realpath((await git2(dir, "rev-parse", "--show-toplevel")).trim());
3015
+ const gitDir = await fs15.realpath((await git2(root, "rev-parse", "--absolute-git-dir")).trim());
3021
3016
  let branch;
3022
3017
  try {
3023
3018
  branch = (await git2(root, "symbolic-ref", "--quiet", "HEAD")).trim();
@@ -3036,7 +3031,7 @@ async function readInputs(root) {
3036
3031
  const head = headText.trim();
3037
3032
  let shallow = null;
3038
3033
  try {
3039
- shallow = await fs14.readFile(path20.resolve(root, shallowPath.trim()), "utf8");
3034
+ shallow = await fs15.readFile(path20.resolve(root, shallowPath.trim()), "utf8");
3040
3035
  } catch (error) {
3041
3036
  if (error.code !== "ENOENT") throw error;
3042
3037
  }
@@ -3049,7 +3044,7 @@ async function readInputs(root) {
3049
3044
  docContents.push([file, text2]);
3050
3045
  for (const claim of text2 ? extractClaims(text2).paths : []) {
3051
3046
  if (internal(claim.path) || claim.path.startsWith(".mason/")) continue;
3052
- const exists2 = async (p) => fs14.access(path20.resolve(root, p)).then(() => true, () => false);
3047
+ const exists2 = async (p) => fs15.access(path20.resolve(root, p)).then(() => true, () => false);
3053
3048
  claims.push([claim.path, await exists2(claim.path), await exists2(path20.dirname(claim.path))]);
3054
3049
  }
3055
3050
  }
@@ -3067,7 +3062,7 @@ async function readInputs(root) {
3067
3062
  }
3068
3063
  const needsWorkspaceScripts = rootScripts !== null && commandClaims.some((claim) => !rootScripts.includes(claim.scriptName));
3069
3064
  const decisionDirectory = await storePath(root, ".mason/decisions");
3070
- const decisionPresence = await fs14.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
3065
+ const decisionPresence = await fs15.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
3071
3066
  if (error.code === "ENOENT") return "absent";
3072
3067
  throw error;
3073
3068
  });
@@ -3136,108 +3131,203 @@ var init_evidence = __esm({
3136
3131
  init_dead_command();
3137
3132
  init_storage();
3138
3133
  exec11 = promisify12(execFile12);
3139
- engineVersion = true ? "0.16.2" : "development";
3134
+ engineVersion = true ? "0.16.3" : "development";
3140
3135
  hash = (value) => createHash4("sha256").update(JSON.stringify(value)).digest("hex");
3141
3136
  content = readAuditInput;
3142
3137
  internal = (file) => file === ".mason" || file === ".mason/reports" || file.startsWith(".mason/reports/");
3143
- cacheSchema = z7.object({ version: z7.literal(1), entries: z7.record(z7.object({ key: z7.string(), result: checkResultSchema })), digest: z7.string() });
3138
+ cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
3144
3139
  }
3145
3140
  });
3146
3141
 
3147
- // src/automation/store.ts
3148
- import fs15 from "fs/promises";
3142
+ // src/automation/execution.ts
3143
+ var execution_exports = {};
3144
+ __export(execution_exports, {
3145
+ automationFailure: () => automationFailure,
3146
+ executionStatus: () => executionStatus,
3147
+ failureMessage: () => failureMessage,
3148
+ failureNotifications: () => failureNotifications,
3149
+ hookFailureMessage: () => hookFailureMessage,
3150
+ recordExecution: () => recordExecution
3151
+ });
3149
3152
  import os2 from "os";
3150
- import { z as z8 } from "zod";
3151
- function parseState(raw) {
3153
+ import fs16 from "fs/promises";
3154
+ import { randomUUID as randomUUID4 } from "crypto";
3155
+ import { z as z9 } from "zod";
3156
+ function parseExecution(raw) {
3152
3157
  try {
3153
- return stateSchema.parse(raw);
3158
+ return executionSchema.parse(raw);
3154
3159
  } catch (error) {
3155
- throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
3160
+ throw new Error("Invalid automation execution store; receipt history was retained.", { cause: error });
3156
3161
  }
3157
3162
  }
3158
- async function withLock(root, directory, run) {
3159
- const file = await storePath(root, directory + "/lock", true);
3160
- const deadline = Date.now() + 5e3;
3161
- let handle;
3162
- while (!handle) {
3163
+ function automationFailure(error) {
3164
+ const recorded = failureSchema.safeParse(error?.failure);
3165
+ if (recorded.success) return recorded.data;
3166
+ const message2 = (error instanceof Error ? error.message : String(error)).replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 700);
3167
+ const codes = /* @__PURE__ */ new Set();
3168
+ let cause = error;
3169
+ for (let i = 0; cause && i < 8; i++) {
3170
+ codes.add(String(cause.code));
3171
+ cause = cause.cause;
3172
+ }
3173
+ 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 z9.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";
3174
+ return { code, message: message2, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
3175
+ }
3176
+ function failureMessage(error) {
3177
+ const failure = automationFailure(error);
3178
+ return `Mason automation unavailable [${failure.code}]; evidence capture/verification was not established. ${failure.message}` + (failure.receiptRecorded ? "" : " No durable failure receipt was recorded.");
3179
+ }
3180
+ async function failureNotifications(root, directory) {
3181
+ const raw = await readStoreJson(root, directory + "/notifications.json");
3182
+ return raw === null ? { version: 1, sessions: {} } : notificationsSchema.parse(raw);
3183
+ }
3184
+ async function hookFailureMessage(dir, host, sessionId, error) {
3185
+ const failure = automationFailure(error);
3186
+ if (!failure.receiptRecorded) return failureMessage(error);
3187
+ try {
3188
+ const ws = await workspace(dir);
3189
+ return await withLock(ws.root, ws.directory + "/notifications", async () => {
3190
+ const notices = await failureNotifications(ws.root, ws.directory);
3191
+ const key = hash([host, sessionId]), signature = hash([failure.code, failure.message]);
3192
+ const previous = notices.sessions[key];
3193
+ const execution = await executionStatus(ws.root, ws.directory);
3194
+ const recovered = previous && execution.attempts.some((a) => a.status === "completed" && a.startedAt > previous.lastAt);
3195
+ const repeat = previous?.signature === signature && !recovered;
3196
+ notices.sessions[key] = {
3197
+ signature,
3198
+ code: failure.code,
3199
+ message: failure.message,
3200
+ count: repeat ? previous.count + 1 : 1,
3201
+ lastAt: (/* @__PURE__ */ new Date()).toISOString()
3202
+ };
3203
+ notices.sessions = Object.fromEntries(Object.entries(notices.sessions).sort(([, a], [, b]) => b.lastAt.localeCompare(a.lastAt)).slice(0, 32));
3204
+ await writeStoreJson(ws.root, ws.directory + "/notifications.json", notices);
3205
+ return repeat ? null : failureMessage(error) + " Repeated failures in this session are recorded in mason status --json.";
3206
+ });
3207
+ } catch {
3208
+ return failureMessage(error);
3209
+ }
3210
+ }
3211
+ async function refreshRunning(root, directory, attempts) {
3212
+ for (const attempt of attempts) {
3213
+ if (attempt.status !== "running") continue;
3214
+ let alive = false;
3163
3215
  try {
3164
- handle = await fs15.open(file, "wx", 384);
3165
- try {
3166
- await handle.writeFile(JSON.stringify({ pid: process.pid, host: os2.hostname() }));
3167
- } catch (error) {
3168
- await handle.close().catch(() => {
3169
- });
3170
- handle = void 0;
3171
- await fs15.rm(file, { force: true }).catch(() => {
3172
- });
3173
- throw error;
3216
+ const owner = await readStoreJson(root, attempt.lease ? leasePath(directory, attempt.id) : directory + "/lock");
3217
+ if (attempt.host === os2.hostname() && owner?.pid === attempt.pid && owner.host === attempt.host) {
3218
+ try {
3219
+ process.kill(attempt.pid, 0);
3220
+ alive = true;
3221
+ } catch (error) {
3222
+ alive = error.code === "EPERM";
3223
+ }
3174
3224
  }
3225
+ } catch {
3226
+ }
3227
+ if (!alive) attempt.status = "unknown";
3228
+ }
3229
+ }
3230
+ async function editExecution(root, directory, edit) {
3231
+ return withLock(root, directory + "/execution-lock", async () => {
3232
+ const file = directory + "/execution.json";
3233
+ const raw = await readStoreJson(root, file);
3234
+ const log = raw === null ? { version: 1, attempts: [], discardedAttempts: 0 } : parseExecution(raw);
3235
+ await refreshRunning(root, directory, log.attempts);
3236
+ edit(log);
3237
+ const finished = log.attempts.filter((a) => a.status !== "running");
3238
+ const discard = new Set(finished.slice(0, Math.max(0, finished.length - 32)).map((a) => a.id));
3239
+ log.discardedAttempts += discard.size;
3240
+ log.attempts = log.attempts.filter((a) => !discard.has(a.id));
3241
+ await writeStoreJson(root, file, log);
3242
+ });
3243
+ }
3244
+ async function recordExecution(root, directory, event, run) {
3245
+ const started = performance.now();
3246
+ const attempt = {
3247
+ id: randomUUID4(),
3248
+ event,
3249
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3250
+ pid: process.pid,
3251
+ host: os2.hostname(),
3252
+ status: "running",
3253
+ lease: true
3254
+ };
3255
+ const owner = leasePath(directory, attempt.id);
3256
+ const finish = async (changes) => editExecution(root, directory, (log) => {
3257
+ const current = log.attempts.find((a) => a.id === attempt.id);
3258
+ if (!current) throw new Error("Automation execution receipt disappeared; completion could not be recorded.");
3259
+ Object.assign(current, changes, { finishedAt: (/* @__PURE__ */ new Date()).toISOString(), durationMs: performance.now() - started });
3260
+ });
3261
+ await writeStoreJson(root, owner, { pid: attempt.pid, host: attempt.host });
3262
+ try {
3263
+ await editExecution(root, directory, (log) => {
3264
+ if (log.attempts.filter((a) => a.status === "running").length >= 128) throw new Error("Too many active automation executions; existing receipts were retained.");
3265
+ log.attempts.push(attempt);
3266
+ });
3267
+ try {
3268
+ const result = await run();
3269
+ await finish({ status: "completed", verificationStatus: result.report.status, reportPath: result.report.reportPath });
3270
+ return result;
3175
3271
  } catch (error) {
3176
- if (error.code !== "EEXIST") throw error;
3272
+ const failure = automationFailure(error);
3177
3273
  try {
3178
- const owner = JSON.parse(await fs15.readFile(file, "utf8"));
3179
- if (owner.host === os2.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
3180
- try {
3181
- process.kill(owner.pid, 0);
3182
- } catch (probe) {
3183
- if (probe.code === "ESRCH") {
3184
- const reclaim = file + ".reclaim";
3185
- let guard;
3186
- try {
3187
- guard = await fs15.open(reclaim, "wx", 384);
3188
- const current = JSON.parse(await fs15.readFile(file, "utf8"));
3189
- if (current.pid === owner.pid && current.host === owner.host) await fs15.unlink(file);
3190
- } finally {
3191
- if (guard) {
3192
- await guard.close();
3193
- await fs15.rm(reclaim, { force: true });
3194
- }
3195
- }
3196
- }
3197
- }
3198
- }
3274
+ await finish({ status: "failed", failure: { ...failure, receiptRecorded: true } });
3275
+ failure.receiptRecorded = true;
3199
3276
  } catch {
3200
3277
  }
3201
- if (Date.now() >= deadline) throw new Error("Automation is busy or its lock needs inspection: " + file);
3202
- await new Promise((resolve2) => setTimeout(resolve2, 40));
3278
+ throw Object.assign(new Error(failure.message, { cause: error }), { failure });
3203
3279
  }
3204
- }
3205
- try {
3206
- return await run();
3207
3280
  } finally {
3208
- await handle.close();
3209
- await fs15.unlink(file);
3281
+ await fs16.rm(await storePath(root, owner), { force: true }).catch(() => {
3282
+ });
3210
3283
  }
3211
3284
  }
3212
- var hostSchema, events, stateSchema;
3213
- var init_store = __esm({
3214
- "src/automation/store.ts"() {
3285
+ async function executionStatus(root, directory) {
3286
+ const raw = await readStoreJson(root, directory + "/execution.json");
3287
+ if (raw === null) return { status: "not-observed", attempts: [] };
3288
+ const log = parseExecution(raw);
3289
+ await refreshRunning(root, directory, log.attempts);
3290
+ const completed = [...log.attempts].reverse().find((a) => a.status === "completed");
3291
+ const outstanding = [...log.attempts].reverse().find((a) => ["failed", "unknown"].includes(a.status) && (!completed || (a.finishedAt ?? a.startedAt) >= completed.startedAt));
3292
+ const status = log.attempts.some((a) => a.status === "running") ? "running" : outstanding?.status ?? log.attempts.at(-1)?.status ?? "not-observed";
3293
+ return { status, attempts: log.attempts, discardedAttempts: log.discardedAttempts };
3294
+ }
3295
+ var failureSchema, attemptSchema, executionSchema, notificationsSchema, leasePath;
3296
+ var init_execution = __esm({
3297
+ "src/automation/execution.ts"() {
3215
3298
  "use strict";
3299
+ init_store();
3300
+ init_evidence();
3216
3301
  init_storage();
3217
- hostSchema = z8.enum(["claude", "codex"]);
3218
- events = ["session_start", "turn_start", "before_tool", "after_tool", "task_end"];
3219
- stateSchema = z8.object({
3220
- version: z8.literal(1),
3221
- root: z8.string(),
3222
- gitDir: z8.string(),
3223
- branch: z8.string(),
3224
- baselines: z8.array(z8.object({ path: z8.string(), at: z8.string(), event: z8.string(), fingerprint: z8.string() })).max(128),
3225
- sessions: z8.record(z8.object({
3226
- host: hostSchema,
3227
- seen: z8.string().nullable(),
3228
- continued: z8.boolean(),
3229
- initialIssues: z8.array(z8.string()),
3230
- initialDocs: z8.record(z8.string().nullable()),
3231
- lastUsed: z8.string(),
3232
- mutationObserved: z8.boolean(),
3233
- pending: z8.record(z8.string()),
3234
- coverageGaps: z8.array(z8.string()),
3235
- events: z8.record(z8.object({ at: z8.string(), count: z8.number().int().positive() }))
3236
- })),
3237
- updatedAt: z8.string(),
3238
- fingerprint: z8.string().nullable(),
3239
- latest: z8.string().nullable()
3302
+ failureSchema = z9.object({
3303
+ code: z9.enum(["inputs-changed", "storage-full", "busy", "invalid-input", "history-unavailable", "invalid-evidence", "io-error", "internal"]),
3304
+ message: z9.string(),
3305
+ retryable: z9.boolean(),
3306
+ receiptRecorded: z9.boolean()
3307
+ });
3308
+ attemptSchema = z9.object({
3309
+ id: z9.string().regex(/^[a-zA-Z0-9_-]+$/),
3310
+ lease: z9.literal(true).optional(),
3311
+ event: z9.string(),
3312
+ startedAt: z9.string(),
3313
+ finishedAt: z9.string().optional(),
3314
+ durationMs: z9.number().nonnegative().optional(),
3315
+ pid: z9.number().int().positive(),
3316
+ host: z9.string(),
3317
+ status: z9.enum(["running", "completed", "failed", "unknown"]),
3318
+ verificationStatus: z9.string().optional(),
3319
+ reportPath: z9.string().optional(),
3320
+ failure: failureSchema.optional()
3240
3321
  });
3322
+ executionSchema = z9.object({ version: z9.literal(1), attempts: z9.array(attemptSchema).max(160), discardedAttempts: z9.number().int().nonnegative().default(0) });
3323
+ notificationsSchema = z9.object({ version: z9.literal(1), sessions: z9.record(z9.object({
3324
+ signature: z9.string(),
3325
+ code: failureSchema.shape.code,
3326
+ message: z9.string(),
3327
+ count: z9.number().int().positive(),
3328
+ lastAt: z9.string()
3329
+ })) });
3330
+ leasePath = (directory, id) => `${directory}/executions/${id}.json`;
3241
3331
  }
3242
3332
  });
3243
3333
 
@@ -3250,6 +3340,7 @@ __export(runtime_exports, {
3250
3340
  summarize: () => summarize
3251
3341
  });
3252
3342
  import { randomUUID as randomUUID5 } from "crypto";
3343
+ import { z as z10 } from "zod";
3253
3344
  async function observeReadOnlyTool(dir, event) {
3254
3345
  const ws = await workspace(dir);
3255
3346
  await withLock(ws.root, ws.directory, async () => {
@@ -3280,176 +3371,234 @@ function summarize(report) {
3280
3371
  "Keep original evidence. Address findings relevant to the authorized task; report unrelated findings and unresolved advisories without approving them."
3281
3372
  ].join("\n");
3282
3373
  }
3374
+ async function loadState(ws) {
3375
+ const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
3376
+ const state = raw === null ? {
3377
+ version: 1,
3378
+ root: ws.root,
3379
+ gitDir: ws.gitDir,
3380
+ branch: ws.branch,
3381
+ baselines: [],
3382
+ sessions: {},
3383
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3384
+ fingerprint: null,
3385
+ latest: null
3386
+ } : parseState(raw);
3387
+ if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
3388
+ throw new Error("Automation state belongs to another branch or worktree; original evidence was retained.");
3389
+ }
3390
+ return state;
3391
+ }
3392
+ async function sharedAnalysis(ws, state, inputs, startedAt) {
3393
+ const pointer = state.analysis;
3394
+ if (!pointer || pointer.fingerprint !== inputs.fingerprint || pointer.completedAt < startedAt) return null;
3395
+ const stored = sharedSchema.parse(await readStoreJson(ws.root, pointer.path));
3396
+ const a = stored.analysis;
3397
+ if (stored.digest !== hash(a) || a.id !== pointer.id || a.fingerprint !== inputs.fingerprint || a.completedAt !== pointer.completedAt || a.report.root !== ws.root || a.report.branch !== ws.branch || a.report.head !== inputs.head || hash(a.report.baselinePaths) !== hash(state.baselines.map((b) => b.path))) {
3398
+ throw new Error("Invalid shared automation evidence; run a new check.");
3399
+ }
3400
+ if (a.baselineDigest !== await baselineDigest(ws, state)) throw new Error("Repair baseline was modified; use the original baseline.");
3401
+ return a;
3402
+ }
3403
+ async function verifyInputs(ws, inputs) {
3404
+ const [after, currentWs] = await Promise.all([readInputs(ws.root), workspace(ws.root)]);
3405
+ if (after.fingerprint !== inputs.fingerprint || currentWs.directory !== ws.directory) {
3406
+ throw new Error("Repository inputs or branch changed during automation; no current verification was recorded. Retry on a stable checkout.");
3407
+ }
3408
+ }
3409
+ async function computeAnalysis(ws, state, inputs, event) {
3410
+ const baselines = [...state.baselines];
3411
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3412
+ const empty = {
3413
+ version: 1,
3414
+ status: "unavailable",
3415
+ root: ws.root,
3416
+ branch: ws.branch,
3417
+ head: inputs.head,
3418
+ baselinePaths: [],
3419
+ reportPath: "",
3420
+ findings: [],
3421
+ diagnostics: ["No AGENTS.md, CLAUDE.md, or .claude/CLAUDE.md exists. Documentation capture is unavailable; other Mason tools remain usable."],
3422
+ checks: { ran: [], reused: [], skipped: [] },
3423
+ counts: { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 },
3424
+ capture: "unknown",
3425
+ scope: SCOPE
3426
+ };
3427
+ if (!baselines.length && Object.values(inputs.docs).every((value) => value === null)) return { baselines, report: empty, cache: null };
3428
+ if (ws.branch === "detached" && state.latest) {
3429
+ const previous = await readStoreJson(ws.root, state.latest);
3430
+ if (!previous?.head || !/^[a-f0-9]{40,64}$/.test(previous.head)) throw new Error("The previous detached checkout evidence is unavailable.");
3431
+ try {
3432
+ await git2(ws.root, "merge-base", "--is-ancestor", previous.head, inputs.head);
3433
+ } catch {
3434
+ throw new Error("Detached checkout moved to a different history; original repair evidence was retained. Inspect that baseline explicitly.");
3435
+ }
3436
+ }
3437
+ let cached = null;
3438
+ const diagnostics = [];
3439
+ try {
3440
+ cached = await readStoreJson(ws.root, ws.directory + "/cache.json");
3441
+ } catch {
3442
+ diagnostics.push("Unreadable automation cache; checks are being recomputed.");
3443
+ }
3444
+ const cache = checkCache(cached, inputs);
3445
+ if (cache.diagnostic) diagnostics.push(cache.diagnostic);
3446
+ const saveBaseline = async () => {
3447
+ if (baselines.length >= 128) throw new Error("128 retained baselines need review; automatic capture stopped without discarding original evidence.");
3448
+ const prepared = await prepareRepair(ws.root, ALL_CHECKS, cache.options);
3449
+ baselines.push({ path: prepared.baselinePath, at: now, event: event.event, fingerprint: inputs.fingerprint });
3450
+ };
3451
+ if (!baselines.length) await saveBaseline();
3452
+ const verifications = [];
3453
+ for (const baseline of baselines) verifications.push(await verifyRepair(ws.root, baseline.path, cache.options));
3454
+ const known = new Set(verifications.flatMap((v) => v.findings.map((f) => f.id)));
3455
+ if (verifications.some((v) => v.newFindings.some((f) => !known.has(findingId(f))))) {
3456
+ await saveBaseline();
3457
+ verifications.push(await verifyRepair(ws.root, baselines.at(-1).path, cache.options));
3458
+ }
3459
+ const merged = /* @__PURE__ */ new Map();
3460
+ for (const verification of verifications) {
3461
+ for (const finding of verification.findings) {
3462
+ const previous = merged.get(finding.id);
3463
+ if (!previous || priority[finding.status] > priority[previous.status]) merged.set(finding.id, finding);
3464
+ }
3465
+ diagnostics.push(...verification.diagnostics);
3466
+ }
3467
+ const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
3468
+ for (const finding of merged.values()) counts[finding.status]++;
3469
+ const report = {
3470
+ version: 1,
3471
+ status: diagnostics.length || verifications.some((v) => v.status === "incomplete") ? "incomplete" : counts.unresolved ? "issues-remain" : "verified",
3472
+ root: ws.root,
3473
+ branch: ws.branch,
3474
+ head: inputs.head,
3475
+ baselinePaths: baselines.map((b) => b.path),
3476
+ reportPath: "",
3477
+ findings: [...merged.values()],
3478
+ diagnostics: [...new Set(diagnostics)],
3479
+ checks: { ran: [...cache.ran], reused: [...cache.reused].filter((name) => !cache.ran.has(name)), skipped: verifications.at(-1).currentAudit?.skippedChecks ?? [] },
3480
+ counts,
3481
+ capture: "unknown",
3482
+ scope: SCOPE
3483
+ };
3484
+ return { baselines, report, cache: cache.ran.size || cached === null || cache.diagnostic ? cache.serialize() : null };
3485
+ }
3486
+ async function analyze(ws, inputs, event, startedAt) {
3487
+ return withLock(ws.root, ws.directory + "/analysis/" + inputs.fingerprint, async () => {
3488
+ const before = await loadState(ws);
3489
+ const shared = await sharedAnalysis(ws, before, inputs, startedAt);
3490
+ if (shared) return { analysis: shared, shared: true };
3491
+ const originalDigest = await baselineDigest(ws, before);
3492
+ const computed = await computeAnalysis(ws, before, inputs, event);
3493
+ await verifyInputs(ws, inputs);
3494
+ if (originalDigest !== await baselineDigest(ws, before)) throw new Error("Repair baseline was modified during automation; original evidence must be inspected.");
3495
+ const id = randomUUID5();
3496
+ const analysis = analysisSchema.parse({
3497
+ id,
3498
+ fingerprint: inputs.fingerprint,
3499
+ completedAt: Date.now(),
3500
+ baselineDigest: await baselineDigest(ws, { ...before, baselines: computed.baselines }),
3501
+ report: computed.report
3502
+ });
3503
+ const analysisPath = ws.directory + "/checks/analysis-" + inputs.fingerprint + ".json";
3504
+ await withLock(ws.root, ws.directory, async () => {
3505
+ const state = await loadState(ws);
3506
+ if (hash(state.baselines) !== hash(before.baselines) || state.analysis?.id !== before.analysis?.id) {
3507
+ throw new Error("Automation evidence changed during analysis; retry against the retained baselines.");
3508
+ }
3509
+ await writeStoreJson(ws.root, analysisPath, { analysis, digest: hash(analysis) });
3510
+ if (computed.cache) await writeStoreJson(ws.root, ws.directory + "/cache.json", computed.cache);
3511
+ state.baselines = computed.baselines;
3512
+ state.analysis = { id, fingerprint: inputs.fingerprint, completedAt: analysis.completedAt, path: analysisPath };
3513
+ await writeStoreJson(ws.root, ws.directory + "/state.json", state);
3514
+ });
3515
+ return { analysis, shared: false };
3516
+ }, Math.max(0, 25e3 - (Date.now() - startedAt)));
3517
+ }
3283
3518
  async function automate(dir, event) {
3519
+ const startedAt = Date.now();
3284
3520
  const ws = await workspace(dir);
3285
- return withLock(ws.root, ws.directory, () => recordExecution(ws.root, ws.directory, event.event, async () => {
3521
+ return recordExecution(ws.root, ws.directory, event.event, async () => {
3286
3522
  const inputs = await readInputs(ws.root);
3287
- const statePath = ws.directory + "/state.json";
3288
- const raw = await readStoreJson(ws.root, statePath);
3289
- const now = (/* @__PURE__ */ new Date()).toISOString();
3290
- const state = raw === null ? {
3291
- version: 1,
3292
- root: ws.root,
3293
- gitDir: ws.gitDir,
3294
- branch: ws.branch,
3295
- baselines: [],
3296
- sessions: {},
3297
- updatedAt: now,
3298
- fingerprint: null,
3299
- latest: null
3300
- } : parseState(raw);
3301
- if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
3302
- throw new Error("Automation state belongs to another branch or worktree; original evidence was retained.");
3303
- }
3304
- if (ws.branch === "detached" && state.latest) {
3305
- const previous = await readStoreJson(ws.root, state.latest);
3306
- if (!previous?.head || !/^[a-f0-9]{40,64}$/.test(previous.head)) throw new Error("The previous detached checkout evidence is unavailable.");
3307
- try {
3308
- await git2(ws.root, "merge-base", "--is-ancestor", previous.head, inputs.head);
3309
- } catch {
3310
- throw new Error("Detached checkout moved to a different history; original repair evidence was retained. Inspect that baseline explicitly.");
3523
+ const result = await analyze(ws, inputs, event, startedAt);
3524
+ if (result.shared) {
3525
+ await verifyInputs(ws, inputs);
3526
+ if (result.analysis.baselineDigest !== await baselineDigest(ws, await loadState(ws))) {
3527
+ throw new Error("Repair baseline was modified during shared automation; original evidence must be inspected.");
3311
3528
  }
3312
3529
  }
3313
- const key = event.host && event.sessionId ? hash([event.host, event.sessionId]) : null;
3314
- const newSession = key !== null && !state.sessions[key];
3315
- if (key && !state.sessions[key]) {
3316
- const keys = Object.keys(state.sessions).sort((a, b) => state.sessions[a].lastUsed.localeCompare(state.sessions[b].lastUsed));
3317
- for (const expired of keys.slice(0, Math.max(0, keys.length - 31))) delete state.sessions[expired];
3318
- state.sessions[key] = {
3319
- host: event.host,
3320
- seen: null,
3321
- continued: false,
3322
- initialIssues: [],
3323
- initialDocs: inputs.docs,
3324
- lastUsed: now,
3325
- mutationObserved: false,
3326
- pending: {},
3327
- coverageGaps: [],
3328
- events: {}
3329
- };
3330
- }
3331
- const session = key ? state.sessions[key] : null;
3332
- if (session) {
3333
- session.lastUsed = now;
3334
- session.events[event.event] = { at: now, count: (session.events[event.event]?.count ?? 0) + 1 };
3335
- if (event.event === "before_tool" && event.mutating && event.toolId) {
3336
- if (Object.keys(session.pending).length >= 128) throw new Error("Too many unfinished tool calls to track pre-edit evidence.");
3337
- session.pending[event.toolId] = inputs.fingerprint;
3530
+ return withLock(ws.root, ws.directory, async () => {
3531
+ const state = await loadState(ws);
3532
+ if (state.analysis?.id !== result.analysis.id) throw new Error("Automation evidence changed during publication; retry against the current inputs.");
3533
+ const report = structuredClone(result.analysis.report);
3534
+ if (result.shared) report.checks = { ...report.checks, ran: [], reused: [.../* @__PURE__ */ new Set([...report.checks.ran, ...report.checks.reused])], shared: true };
3535
+ report.reportPath = ws.directory + "/checks/" + randomUUID5() + ".json";
3536
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3537
+ const key = event.host && event.sessionId ? hash([event.host, event.sessionId]) : null;
3538
+ const newSession = key !== null && !state.sessions[key];
3539
+ if (key && !state.sessions[key]) {
3540
+ const keys = Object.keys(state.sessions).sort((a, b) => state.sessions[a].lastUsed.localeCompare(state.sessions[b].lastUsed));
3541
+ const removable = keys.filter((k) => !Object.keys(state.sessions[k].pending).length);
3542
+ for (const expired of removable.slice(0, Math.max(0, keys.length - 31))) delete state.sessions[expired];
3543
+ if (Object.keys(state.sessions).length >= 128) throw new Error("Too many sessions with unfinished tool calls; original evidence was retained.");
3544
+ state.sessions[key] = {
3545
+ host: event.host,
3546
+ seen: null,
3547
+ continued: false,
3548
+ initialIssues: report.findings.filter((f) => f.status === "unresolved").map((f) => f.id),
3549
+ initialDocs: inputs.docs,
3550
+ lastUsed: now,
3551
+ mutationObserved: false,
3552
+ pending: {},
3553
+ coverageGaps: [],
3554
+ events: {}
3555
+ };
3338
3556
  }
3339
- if (event.event === "after_tool" && event.mutating) {
3340
- session.mutationObserved = true;
3341
- if (!event.toolId || !session.pending[event.toolId]) {
3342
- const gap = "A tool completed without an observed matching pre-tool capture; pre-edit coverage is unknown.";
3343
- if (!session.coverageGaps.includes(gap)) session.coverageGaps.push(gap);
3557
+ const session = key ? state.sessions[key] : null;
3558
+ if (session) {
3559
+ session.lastUsed = now;
3560
+ session.events[event.event] = { at: now, count: (session.events[event.event]?.count ?? 0) + 1 };
3561
+ if (event.event === "before_tool" && event.mutating && event.toolId) {
3562
+ if (Object.keys(session.pending).length >= 128) throw new Error("Too many unfinished tool calls to track pre-edit evidence.");
3563
+ session.pending[event.toolId] = inputs.fingerprint;
3344
3564
  }
3345
- if (event.toolId) delete session.pending[event.toolId];
3565
+ if (event.event === "after_tool" && event.mutating) {
3566
+ session.mutationObserved = true;
3567
+ if (!event.toolId || !session.pending[event.toolId]) {
3568
+ const gap = "A tool completed without an observed matching pre-tool capture; pre-edit coverage is unknown.";
3569
+ if (!session.coverageGaps.includes(gap)) session.coverageGaps.push(gap);
3570
+ }
3571
+ if (event.toolId) delete session.pending[event.toolId];
3572
+ }
3573
+ report.diagnostics.push(...session.coverageGaps);
3574
+ if (session.coverageGaps.length && report.status !== "unavailable") report.status = "incomplete";
3575
+ report.capture = report.status !== "unavailable" && !session.coverageGaps.length && (session.events.session_start || session.events.before_tool) ? "observed" : "unknown";
3346
3576
  }
3347
- }
3348
- if (!state.baselines.length && Object.values(inputs.docs).every((value) => value === null)) {
3349
- const report2 = {
3350
- version: 1,
3351
- status: "unavailable",
3352
- root: ws.root,
3353
- branch: ws.branch,
3354
- head: inputs.head,
3355
- baselinePaths: [],
3356
- reportPath: ws.directory + "/checks/" + randomUUID5() + ".json",
3357
- findings: [],
3358
- diagnostics: ["No AGENTS.md, CLAUDE.md, or .claude/CLAUDE.md exists. Documentation capture is unavailable; other Mason tools remain usable."],
3359
- checks: { ran: [], reused: [], skipped: [] },
3360
- counts: { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 },
3361
- capture: "unknown",
3362
- scope: SCOPE
3363
- };
3364
- const notify2 = !session || session.seen !== "no-docs";
3365
- if (session) session.seen = "no-docs";
3366
- state.fingerprint = inputs.fingerprint;
3367
- state.latest = report2.reportPath;
3368
- state.updatedAt = now;
3369
- await writeStoreJson(ws.root, report2.reportPath, report2);
3370
- await writeStoreJson(ws.root, statePath, state);
3371
- return { report: report2, message: notify2 ? summarize(report2) : null, continueOnce: false };
3372
- }
3373
- let cached = null;
3374
- const diagnostics = [];
3375
- try {
3376
- cached = await readStoreJson(ws.root, ws.directory + "/cache.json");
3377
- } catch {
3378
- diagnostics.push("Unreadable automation cache; checks are being recomputed.");
3379
- }
3380
- const cache = checkCache(cached, inputs);
3381
- if (cache.diagnostic) diagnostics.push(cache.diagnostic);
3382
- const saveBaseline = async () => {
3383
- if (state.baselines.length >= 128) throw new Error("128 retained baselines need review; automatic capture stopped without discarding original evidence.");
3384
- const prepared = await prepareRepair(ws.root, ALL_CHECKS, cache.options);
3385
- state.baselines.push({ path: prepared.baselinePath, at: now, event: event.event, fingerprint: inputs.fingerprint });
3386
- };
3387
- if (!state.baselines.length) await saveBaseline();
3388
- const verifications = [];
3389
- for (const baseline of state.baselines) verifications.push(await verifyRepair(ws.root, baseline.path, cache.options));
3390
- const known = new Set(verifications.flatMap((v) => v.findings.map((f) => f.id)));
3391
- if (verifications.some((v) => v.newFindings.some((f) => !known.has(findingId(f))))) {
3392
- await saveBaseline();
3393
- verifications.push(await verifyRepair(ws.root, state.baselines.at(-1).path, cache.options));
3394
- }
3395
- const merged = /* @__PURE__ */ new Map();
3396
- for (const verification of verifications) {
3397
- for (const finding of verification.findings) {
3398
- const previous = merged.get(finding.id);
3399
- if (!previous || priority[finding.status] > priority[previous.status]) merged.set(finding.id, finding);
3577
+ const signature = hash([report.status, report.findings, report.diagnostics, report.checks.skipped]);
3578
+ const relevant = report.findings.some((f) => f.status === "unresolved" && session && (!session.initialIssues.includes(f.id) || session.initialDocs[f.original.anchor.doc] !== inputs.docs[f.original.anchor.doc]));
3579
+ const continueOnce = event.event === "task_end" && !!session?.mutationObserved && relevant && !session.continued && !event.stopHookActive;
3580
+ const notify = !session || newSession || signature !== session.seen || continueOnce;
3581
+ if (session) {
3582
+ session.seen = signature;
3583
+ if (continueOnce) session.continued = true;
3400
3584
  }
3401
- diagnostics.push(...verification.diagnostics);
3402
- }
3403
- if (newSession && session) session.initialIssues = [...merged.values()].filter((f) => f.status === "unresolved").map((f) => f.id);
3404
- const current = verifications.at(-1).currentAudit;
3405
- const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
3406
- for (const finding of merged.values()) counts[finding.status]++;
3407
- if (session) diagnostics.push(...session.coverageGaps);
3408
- const capture = session && !session.coverageGaps.length && (session.events.session_start || session.events.before_tool) ? "observed" : "unknown";
3409
- const after = await readInputs(ws.root);
3410
- const currentWs = await workspace(ws.root);
3411
- if (after.fingerprint !== inputs.fingerprint || currentWs.directory !== ws.directory) {
3412
- throw new Error("Repository inputs or branch changed during automation; no current verification was recorded. Retry on a stable checkout.");
3413
- }
3414
- const report = {
3415
- version: 1,
3416
- status: diagnostics.length || verifications.some((v) => v.status === "incomplete") ? "incomplete" : counts.unresolved ? "issues-remain" : "verified",
3417
- root: ws.root,
3418
- branch: ws.branch,
3419
- head: inputs.head,
3420
- baselinePaths: state.baselines.map((b) => b.path),
3421
- reportPath: ws.directory + "/checks/" + randomUUID5() + ".json",
3422
- findings: [...merged.values()],
3423
- diagnostics: [...new Set(diagnostics)],
3424
- checks: { ran: [...cache.ran], reused: [...cache.reused].filter((name) => !cache.ran.has(name)), skipped: current?.skippedChecks ?? [] },
3425
- counts,
3426
- capture,
3427
- scope: SCOPE
3428
- };
3429
- const signature = hash([report.status, report.findings, report.diagnostics, report.checks.skipped]);
3430
- const relevant = report.findings.some((f) => f.status === "unresolved" && session && (!session.initialIssues.includes(f.id) || session.initialDocs[f.original.anchor.doc] !== inputs.docs[f.original.anchor.doc]));
3431
- const continueOnce = event.event === "task_end" && !!session?.mutationObserved && relevant && !session.continued && !event.stopHookActive;
3432
- const notify = !session || newSession || signature !== session.seen || continueOnce;
3433
- if (session) {
3434
- session.seen = signature;
3435
- if (continueOnce) session.continued = true;
3436
- }
3437
- const persistReport = !state.latest || state.fingerprint !== inputs.fingerprint || notify || event.event === "task_end";
3438
- if (!persistReport) report.reportPath = state.latest;
3439
- state.updatedAt = now;
3440
- state.fingerprint = inputs.fingerprint;
3441
- state.latest = report.reportPath;
3442
- if (persistReport) await writeStoreJson(ws.root, report.reportPath, report);
3443
- if (cache.ran.size || cached === null || cache.diagnostic) await writeStoreJson(ws.root, ws.directory + "/cache.json", cache.serialize());
3444
- await writeStoreJson(ws.root, statePath, state);
3445
- return { report, message: notify ? summarize(report) : null, continueOnce };
3446
- }));
3585
+ const persistReport = !state.latest || state.fingerprint !== inputs.fingerprint || notify || event.event === "task_end";
3586
+ if (!persistReport) report.reportPath = state.latest;
3587
+ state.updatedAt = now;
3588
+ state.fingerprint = inputs.fingerprint;
3589
+ state.latest = report.reportPath;
3590
+ if (persistReport) await writeStoreJson(ws.root, report.reportPath, report);
3591
+ await writeStoreJson(ws.root, ws.directory + "/state.json", state);
3592
+ return { report, message: notify ? summarize(report) : null, continueOnce };
3593
+ });
3594
+ });
3447
3595
  }
3448
3596
  async function automationStatus(dir) {
3449
3597
  const ws = await workspace(dir);
3450
3598
  const execution = await executionStatus(ws.root, ws.directory);
3599
+ const notifications = await failureNotifications(ws.root, ws.directory);
3451
3600
  const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
3452
- if (raw === null) return { version: 1, status: execution.status === "not-observed" ? "not-observed" : "unavailable", root: ws.root, branch: ws.branch, baselinePaths: [], hosts: {}, execution };
3601
+ if (raw === null) return { version: 1, status: execution.status === "not-observed" ? "not-observed" : "unavailable", root: ws.root, branch: ws.branch, baselinePaths: [], hosts: {}, execution, notifications };
3453
3602
  const state = parseState(raw);
3454
3603
  if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) throw new Error("Automation state belongs to another workspace.");
3455
3604
  const inputs = await readInputs(ws.root);
@@ -3471,10 +3620,11 @@ async function automationStatus(dir) {
3471
3620
  verificationStatus: unfinished ? "unavailable" : latest?.status ?? "unavailable",
3472
3621
  hosts,
3473
3622
  execution,
3623
+ notifications,
3474
3624
  note: "Observed events do not prove all tool paths are intercepted. Run check to verify the retained evidence."
3475
3625
  };
3476
3626
  }
3477
- var SCOPE, priority, cleanText;
3627
+ var SCOPE, priority, cleanText, analysisSchema, sharedSchema, baselineDigest;
3478
3628
  var init_runtime = __esm({
3479
3629
  "src/automation/runtime.ts"() {
3480
3630
  "use strict";
@@ -3487,11 +3637,40 @@ var init_runtime = __esm({
3487
3637
  SCOPE = "Documentation audit evidence only. Hook receipts show observed events, not complete interception. Resolved claims no longer fail their checks; advisories need separate review. Repair only within the user's task authorization.";
3488
3638
  priority = { resolved: 0, "review-required": 1, unresolved: 2, unverified: 3 };
3489
3639
  cleanText = (text2) => text2.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 250);
3640
+ analysisSchema = z10.object({
3641
+ id: z10.string(),
3642
+ fingerprint: z10.string(),
3643
+ completedAt: z10.number(),
3644
+ baselineDigest: z10.string(),
3645
+ report: z10.object({
3646
+ version: z10.literal(1),
3647
+ status: z10.enum(["verified", "issues-remain", "incomplete", "unavailable"]),
3648
+ root: z10.string(),
3649
+ branch: z10.string(),
3650
+ head: z10.string(),
3651
+ baselinePaths: z10.array(z10.string()),
3652
+ reportPath: z10.string(),
3653
+ findings: z10.array(repairFindingSchema),
3654
+ diagnostics: z10.array(z10.string()),
3655
+ checks: z10.object({
3656
+ ran: z10.array(z10.string()),
3657
+ reused: z10.array(z10.string()),
3658
+ skipped: z10.array(z10.object({ check: z10.string(), reason: z10.string(), doc: z10.string().optional() }))
3659
+ }),
3660
+ counts: z10.object({ resolved: z10.number(), unresolved: z10.number(), "review-required": z10.number(), unverified: z10.number() }),
3661
+ capture: z10.enum(["observed", "unknown"]),
3662
+ scope: z10.string()
3663
+ })
3664
+ });
3665
+ sharedSchema = z10.object({ analysis: analysisSchema, digest: z10.string() });
3666
+ baselineDigest = async (ws, state) => hash(await Promise.all(
3667
+ state.baselines.map(async (b) => [b.path, await readStoreJson(ws.root, b.path)])
3668
+ ));
3490
3669
  }
3491
3670
  });
3492
3671
 
3493
3672
  // src/automation/adapters.ts
3494
- import { z as z9 } from "zod";
3673
+ import { z as z11 } from "zod";
3495
3674
  function managedHookCommand(host, platform = process.platform) {
3496
3675
  const command = `mason --setup-host ${host} auto hook --host ${host}`;
3497
3676
  return platform === "win32" ? `cmd.exe /d /s /c "where mason >nul 2>nul & if errorlevel 1 (exit /b 0) else (${command})"` : `if command -v mason >/dev/null 2>&1; then ${command}; fi`;
@@ -3513,15 +3692,15 @@ var init_adapters = __esm({
3513
3692
  init_execution();
3514
3693
  init_runtime();
3515
3694
  init_store();
3516
- inputSchema = z9.object({
3517
- cwd: z9.string().min(1),
3518
- session_id: z9.string().min(1).max(500),
3519
- hook_event_name: z9.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
3520
- tool_name: z9.string().optional(),
3521
- tool_use_id: z9.string().max(500).optional(),
3522
- tool_input: z9.unknown().optional(),
3523
- stop_hook_active: z9.boolean().optional(),
3524
- permission_mode: z9.string().optional()
3695
+ inputSchema = z11.object({
3696
+ cwd: z11.string().min(1),
3697
+ session_id: z11.string().min(1).max(500),
3698
+ hook_event_name: z11.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
3699
+ tool_name: z11.string().optional(),
3700
+ tool_use_id: z11.string().max(500).optional(),
3701
+ tool_input: z11.unknown().optional(),
3702
+ stop_hook_active: z11.boolean().optional(),
3703
+ permission_mode: z11.string().optional()
3525
3704
  });
3526
3705
  HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"];
3527
3706
  }
@@ -3537,7 +3716,7 @@ __export(ownership_exports, {
3537
3716
  ownershipEdit: () => ownershipEdit,
3538
3717
  ownershipSchema: () => ownershipSchema
3539
3718
  });
3540
- import { z as z10 } from "zod";
3719
+ import { z as z12 } from "zod";
3541
3720
  import { parse } from "smol-toml";
3542
3721
  function blockSpan(text2, start, end) {
3543
3722
  const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
@@ -3588,15 +3767,15 @@ var init_ownership = __esm({
3588
3767
  ["<!-- mason:agents:start -->", "<!-- mason:agents:end -->"],
3589
3768
  ["# mason:mcp:start", "# mason:mcp:end"]
3590
3769
  ];
3591
- fileSchema = z10.object({
3592
- created: z10.boolean(),
3593
- initialHash: z10.string(),
3594
- blocks: z10.record(z10.string()),
3595
- mcpHash: z10.string().optional(),
3596
- hookCommands: z10.array(z10.string()).optional(),
3597
- hooks: z10.record(z10.array(z10.string())).optional()
3770
+ fileSchema = z12.object({
3771
+ created: z12.boolean(),
3772
+ initialHash: z12.string(),
3773
+ blocks: z12.record(z12.string()),
3774
+ mcpHash: z12.string().optional(),
3775
+ hookCommands: z12.array(z12.string()).optional(),
3776
+ hooks: z12.record(z12.array(z12.string())).optional()
3598
3777
  });
3599
- ownershipSchema = z10.object({ version: z10.literal(1), files: z10.record(fileSchema) });
3778
+ ownershipSchema = z12.object({ version: z12.literal(1), files: z12.record(fileSchema) });
3600
3779
  }
3601
3780
  });
3602
3781
 
@@ -3611,7 +3790,7 @@ __export(install_exports, {
3611
3790
  planAutomationInstall: () => planAutomationInstall,
3612
3791
  recordSchema: () => recordSchema
3613
3792
  });
3614
- import { z as z11 } from "zod";
3793
+ import { z as z13 } from "zod";
3615
3794
  async function installationRecord(root) {
3616
3795
  return recordSchema.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
3617
3796
  }
@@ -3692,9 +3871,9 @@ var init_install = __esm({
3692
3871
  init_store();
3693
3872
  init_files2();
3694
3873
  init_ownership();
3695
- groupSchema = z11.object({ hooks: z11.array(z11.object({ type: z11.string(), command: z11.string().optional() }).passthrough()) }).passthrough();
3696
- automationConfigSchema = z11.object({ hooks: z11.record(z11.array(groupSchema)).optional() }).passthrough();
3697
- recordSchema = z11.object({ version: z11.literal(1), hosts: z11.record(z11.object({ command: z11.string() })) });
3874
+ groupSchema = z13.object({ hooks: z13.array(z13.object({ type: z13.string(), command: z13.string().optional() }).passthrough()) }).passthrough();
3875
+ automationConfigSchema = z13.object({ hooks: z13.record(z13.array(groupSchema)).optional() }).passthrough();
3876
+ recordSchema = z13.object({ version: z13.literal(1), hosts: z13.record(z13.object({ command: z13.string() })) });
3698
3877
  AUTOMATION_PATH = ".mason/local/automation.json";
3699
3878
  configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
3700
3879
  }
@@ -3709,7 +3888,7 @@ __export(model_exports, {
3709
3888
  setupHostSchema: () => setupHostSchema,
3710
3889
  setupSchema: () => setupSchema
3711
3890
  });
3712
- import { z as z12 } from "zod";
3891
+ import { z as z14 } from "zod";
3713
3892
  async function loadSetup(root) {
3714
3893
  const raw = await readStoreJson(root, SETUP_PATH);
3715
3894
  if (raw === null) return null;
@@ -3727,27 +3906,27 @@ var init_model = __esm({
3727
3906
  "src/setup/model.ts"() {
3728
3907
  "use strict";
3729
3908
  init_storage();
3730
- setupHostSchema = z12.object({
3731
- configuredVersion: z12.string(),
3732
- revision: z12.string().uuid(),
3733
- fingerprint: z12.string(),
3734
- mcpFingerprint: z12.string(),
3735
- instructions: z12.array(z12.string())
3909
+ setupHostSchema = z14.object({
3910
+ configuredVersion: z14.string(),
3911
+ revision: z14.string().uuid(),
3912
+ fingerprint: z14.string(),
3913
+ mcpFingerprint: z14.string(),
3914
+ instructions: z14.array(z14.string())
3736
3915
  });
3737
- setupSchema = z12.object({ version: z12.literal(2), hosts: z12.object({
3916
+ setupSchema = z14.object({ version: z14.literal(2), hosts: z14.object({
3738
3917
  codex: setupHostSchema.optional(),
3739
3918
  claude: setupHostSchema.optional()
3740
3919
  }) });
3741
3920
  SETUP_PATH = ".mason/local/setup.json";
3742
- receiptSchema = z12.object({
3743
- version: z12.literal(1),
3744
- host: z12.enum(["codex", "claude"]),
3745
- status: z12.enum(["installing", "configured"]),
3746
- initialReportPath: z12.string(),
3747
- initialBaselinePaths: z12.array(z12.string()),
3748
- root: z12.string(),
3749
- revision: z12.string().uuid(),
3750
- configuredAt: z12.string().optional()
3921
+ receiptSchema = z14.object({
3922
+ version: z14.literal(1),
3923
+ host: z14.enum(["codex", "claude"]),
3924
+ status: z14.enum(["installing", "configured"]),
3925
+ initialReportPath: z14.string(),
3926
+ initialBaselinePaths: z14.array(z14.string()),
3927
+ root: z14.string(),
3928
+ revision: z14.string().uuid(),
3929
+ configuredAt: z14.string().optional()
3751
3930
  });
3752
3931
  }
3753
3932
  });
@@ -3755,7 +3934,7 @@ var init_model = __esm({
3755
3934
  // src/setup/launcher.ts
3756
3935
  import { execFile as execFile13 } from "child_process";
3757
3936
  import { promisify as promisify13 } from "util";
3758
- import fs16 from "fs/promises";
3937
+ import fs17 from "fs/promises";
3759
3938
  function globalCommand(args, platform = process.platform) {
3760
3939
  return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args] } : { command: "mason", args };
3761
3940
  }
@@ -3780,7 +3959,7 @@ var init_launcher = __esm({
3780
3959
  "use strict";
3781
3960
  init_evidence();
3782
3961
  init_model();
3783
- executingVersion = () => true ? "0.16.2" : "development";
3962
+ executingVersion = () => true ? "0.16.3" : "development";
3784
3963
  mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
3785
3964
  hookCommand = (host) => `mason --setup-host ${host} auto`;
3786
3965
  }
@@ -3798,7 +3977,7 @@ __export(config_exports, {
3798
3977
  });
3799
3978
  import { isDeepStrictEqual } from "util";
3800
3979
  import { parse as parse2, stringify } from "smol-toml";
3801
- import { z as z13 } from "zod";
3980
+ import { z as z15 } from "zod";
3802
3981
  async function instructionEdits(root, host) {
3803
3982
  const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
3804
3983
  const existing = found.find((f) => f.text !== null);
@@ -3914,7 +4093,7 @@ var init_config = __esm({
3914
4093
  init_install();
3915
4094
  init_files2();
3916
4095
  init_launcher();
3917
- object = z13.record(z13.unknown());
4096
+ object = z15.record(z15.unknown());
3918
4097
  mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
3919
4098
  TOML_START = "# mason:mcp:start";
3920
4099
  TOML_END = "# mason:mcp:end";
@@ -3924,7 +4103,7 @@ var init_config = __esm({
3924
4103
  });
3925
4104
 
3926
4105
  // src/mcp/init.ts
3927
- import { z as z14 } from "zod";
4106
+ import { z as z16 } from "zod";
3928
4107
  async function loadProjectMarker(rootDir) {
3929
4108
  const raw = await readStoreJson(rootDir, ".mason/local/project.json");
3930
4109
  if (raw === null) return null;
@@ -3932,8 +4111,8 @@ async function loadProjectMarker(rootDir) {
3932
4111
  return { ...marker, features: await loadProjectFeatures(rootDir) };
3933
4112
  }
3934
4113
  async function loadProjectFeatures(rootDir) {
3935
- const config = z14.record(z14.unknown()).parse(await readStoreJson(rootDir, ".mason/config.json") ?? {});
3936
- return z14.object({ confluence: z14.boolean().optional() }).passthrough().parse(config.features ?? {});
4114
+ const config = z16.record(z16.unknown()).parse(await readStoreJson(rootDir, ".mason/config.json") ?? {});
4115
+ return z16.object({ confluence: z16.boolean().optional() }).passthrough().parse(config.features ?? {});
3937
4116
  }
3938
4117
  async function saveProjectMarker(rootDir, marker) {
3939
4118
  const { ancillaryEdits: ancillaryEdits2 } = await Promise.resolve().then(() => (init_config(), config_exports));
@@ -3945,8 +4124,8 @@ async function projectMarkerEdits(rootDir, marker) {
3945
4124
  const edits = [{ path: file, before: await readText(rootDir, file), after: JSON.stringify(local, null, 2) + "\n" }];
3946
4125
  if (features && Object.keys(features).length) {
3947
4126
  const configPath2 = ".mason/config.json", before = await readText(rootDir, configPath2);
3948
- const config = z14.record(z14.unknown()).parse(JSON.parse(before ?? "{}"));
3949
- config.features = { ...z14.record(z14.unknown()).parse(config.features ?? {}), ...features };
4127
+ const config = z16.record(z16.unknown()).parse(JSON.parse(before ?? "{}"));
4128
+ config.features = { ...z16.record(z16.unknown()).parse(config.features ?? {}), ...features };
3950
4129
  edits.push({ path: configPath2, before, after: JSON.stringify(config, null, 2) + "\n" });
3951
4130
  }
3952
4131
  return edits;
@@ -3960,10 +4139,10 @@ var init_init = __esm({
3960
4139
  "use strict";
3961
4140
  init_storage();
3962
4141
  init_files2();
3963
- markerSchema = z14.object({
3964
- version: z14.literal(1),
3965
- initializedAt: z14.string(),
3966
- features: z14.object({ confluence: z14.boolean().optional() }).passthrough().optional()
4142
+ markerSchema = z16.object({
4143
+ version: z16.literal(1),
4144
+ initializedAt: z16.string(),
4145
+ features: z16.object({ confluence: z16.boolean().optional() }).passthrough().optional()
3967
4146
  }).passthrough();
3968
4147
  CLAUDE_MD_SECTION = `<!-- mason:start -->
3969
4148
  ## Mason project knowledge
@@ -4157,7 +4336,7 @@ var init_types2 = __esm({
4157
4336
  });
4158
4337
 
4159
4338
  // src/review/evidence/vitest.ts
4160
- import { z as z15 } from "zod";
4339
+ import { z as z17 } from "zod";
4161
4340
  function parseVitest(raw, sourceRoot) {
4162
4341
  const report = schema.parse(raw);
4163
4342
  const findings = [], diagnostics = [];
@@ -4206,24 +4385,24 @@ var init_vitest = __esm({
4206
4385
  "use strict";
4207
4386
  init_paths2();
4208
4387
  init_types2();
4209
- count2 = z15.number().int().nonnegative();
4210
- schema = z15.object({
4211
- success: z15.boolean(),
4388
+ count2 = z17.number().int().nonnegative();
4389
+ schema = z17.object({
4390
+ success: z17.boolean(),
4212
4391
  numTotalTests: count2,
4213
4392
  numPassedTests: count2,
4214
4393
  numFailedTests: count2,
4215
4394
  numPendingTests: count2,
4216
4395
  numTodoTests: count2,
4217
4396
  numFailedTestSuites: count2,
4218
- testResults: z15.array(z15.object({
4219
- name: z15.string().min(1),
4220
- status: z15.enum(["passed", "failed"]),
4221
- message: z15.string().optional(),
4222
- assertionResults: z15.array(z15.object({
4223
- fullName: z15.string(),
4224
- status: z15.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
4225
- failureMessages: z15.array(z15.string()).nullable().optional(),
4226
- location: z15.object({ line: count2, column: count2 }).nullable().optional()
4397
+ testResults: z17.array(z17.object({
4398
+ name: z17.string().min(1),
4399
+ status: z17.enum(["passed", "failed"]),
4400
+ message: z17.string().optional(),
4401
+ assertionResults: z17.array(z17.object({
4402
+ fullName: z17.string(),
4403
+ status: z17.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
4404
+ failureMessages: z17.array(z17.string()).nullable().optional(),
4405
+ location: z17.object({ line: count2, column: count2 }).nullable().optional()
4227
4406
  }))
4228
4407
  }))
4229
4408
  });
@@ -4231,7 +4410,7 @@ var init_vitest = __esm({
4231
4410
  });
4232
4411
 
4233
4412
  // src/review/evidence/sarif.ts
4234
- import { z as z16 } from "zod";
4413
+ import { z as z18 } from "zod";
4235
4414
  function parseSarif(raw, sourceRoot) {
4236
4415
  const report = schema2.parse(raw), findings = [], diagnostics = [];
4237
4416
  let active = 0, suppressed = 0, absent = 0, unresolved = 0;
@@ -4322,29 +4501,29 @@ var init_sarif = __esm({
4322
4501
  "use strict";
4323
4502
  init_paths2();
4324
4503
  init_types2();
4325
- index = z16.number().int().nonnegative();
4326
- artifact = z16.object({ uri: z16.string().optional(), uriBaseId: z16.string().optional(), index: index.optional() });
4327
- message = z16.object({ text: z16.string().optional(), markdown: z16.string().optional(), id: z16.string().optional(), arguments: z16.array(z16.string()).optional() });
4328
- level = z16.enum(["error", "warning", "note", "none"]);
4329
- location = z16.object({ physicalLocation: z16.object({ artifactLocation: artifact.optional(), region: z16.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
4330
- schema2 = z16.object({
4331
- version: z16.literal("2.1.0"),
4332
- runs: z16.array(z16.object({
4333
- tool: z16.object({ driver: z16.object({ name: z16.string().min(1), rules: z16.array(z16.object({ id: z16.string(), defaultConfiguration: z16.object({ level: level.optional() }).optional(), messageStrings: z16.record(message).optional() })).optional(), globalMessageStrings: z16.record(message).optional() }) }),
4334
- invocations: z16.array(z16.object({ executionSuccessful: z16.boolean(), commandLine: z16.string().optional(), toolExecutionNotifications: z16.array(z16.object({ level: level.optional(), message })).optional() })).optional(),
4335
- versionControlProvenance: z16.array(z16.object({ revisionId: z16.string().optional() })).optional(),
4336
- originalUriBaseIds: z16.record(artifact).optional(),
4337
- artifacts: z16.array(z16.object({ location: artifact.optional() })).optional(),
4338
- results: z16.array(z16.object({
4339
- ruleId: z16.string().optional(),
4504
+ index = z18.number().int().nonnegative();
4505
+ artifact = z18.object({ uri: z18.string().optional(), uriBaseId: z18.string().optional(), index: index.optional() });
4506
+ message = z18.object({ text: z18.string().optional(), markdown: z18.string().optional(), id: z18.string().optional(), arguments: z18.array(z18.string()).optional() });
4507
+ level = z18.enum(["error", "warning", "note", "none"]);
4508
+ location = z18.object({ physicalLocation: z18.object({ artifactLocation: artifact.optional(), region: z18.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
4509
+ schema2 = z18.object({
4510
+ version: z18.literal("2.1.0"),
4511
+ runs: z18.array(z18.object({
4512
+ tool: z18.object({ driver: z18.object({ name: z18.string().min(1), rules: z18.array(z18.object({ id: z18.string(), defaultConfiguration: z18.object({ level: level.optional() }).optional(), messageStrings: z18.record(message).optional() })).optional(), globalMessageStrings: z18.record(message).optional() }) }),
4513
+ invocations: z18.array(z18.object({ executionSuccessful: z18.boolean(), commandLine: z18.string().optional(), toolExecutionNotifications: z18.array(z18.object({ level: level.optional(), message })).optional() })).optional(),
4514
+ versionControlProvenance: z18.array(z18.object({ revisionId: z18.string().optional() })).optional(),
4515
+ originalUriBaseIds: z18.record(artifact).optional(),
4516
+ artifacts: z18.array(z18.object({ location: artifact.optional() })).optional(),
4517
+ results: z18.array(z18.object({
4518
+ ruleId: z18.string().optional(),
4340
4519
  ruleIndex: index.optional(),
4341
4520
  message,
4342
4521
  level: level.optional(),
4343
- kind: z16.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
4344
- baselineState: z16.enum(["new", "unchanged", "updated", "absent"]).optional(),
4345
- suppressions: z16.array(z16.object({ kind: z16.enum(["inSource", "external"]), status: z16.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
4346
- locations: z16.array(location).optional(),
4347
- relatedLocations: z16.array(location).optional()
4522
+ kind: z18.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
4523
+ baselineState: z18.enum(["new", "unchanged", "updated", "absent"]).optional(),
4524
+ suppressions: z18.array(z18.object({ kind: z18.enum(["inSource", "external"]), status: z18.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
4525
+ locations: z18.array(location).optional(),
4526
+ relatedLocations: z18.array(location).optional()
4348
4527
  })).optional()
4349
4528
  }))
4350
4529
  });
@@ -4353,7 +4532,7 @@ var init_sarif = __esm({
4353
4532
 
4354
4533
  // src/review/evidence.ts
4355
4534
  import path21 from "path";
4356
- import { z as z17 } from "zod";
4535
+ import { z as z19 } from "zod";
4357
4536
  function relativeArtifact(root, file) {
4358
4537
  const relative = path21.isAbsolute(file) ? path21.relative(root, file) : file;
4359
4538
  const normalized = normalizeRepoPath(relative);
@@ -4394,7 +4573,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
4394
4573
  let manifest, raw;
4395
4574
  try {
4396
4575
  manifest = relativeArtifact(root, supplied);
4397
- raw = z17.object({ version: z17.literal(1), checks: z17.array(z17.unknown()) }).parse(await readStoreJson(root, manifest));
4576
+ raw = z19.object({ version: z19.literal(1), checks: z19.array(z19.unknown()) }).parse(await readStoreJson(root, manifest));
4398
4577
  } catch (error) {
4399
4578
  output.diagnostics.push(`${supplied}: ${String(error)}`);
4400
4579
  continue;
@@ -4547,25 +4726,25 @@ var init_evidence2 = __esm({
4547
4726
  init_vitest();
4548
4727
  init_sarif();
4549
4728
  init_types2();
4550
- checkSchema2 = z17.object({
4551
- id: z17.string().min(1).max(100),
4552
- kind: z17.enum(["tests", "static-analysis", "security", "complexity", "duplication"]),
4553
- tool: z17.string().min(1).max(200),
4554
- command: z17.string().min(1).max(2e3),
4555
- commit: z17.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
4556
- workingTreeClean: z17.boolean().optional(),
4557
- source: z17.string().max(2e3).optional(),
4558
- sourceRoot: z17.string().min(1).max(4e3).optional(),
4559
- status: z17.enum(["completed", "skipped", "unavailable"]).default("completed"),
4560
- reason: z17.string().min(1).max(2e3).optional(),
4561
- exitCode: z17.number().int().nullable().optional(),
4562
- report: z17.object({ format: z17.enum(["vitest-json", "sarif"]), path: z17.string().min(1).max(4e3) }).optional()
4729
+ checkSchema2 = z19.object({
4730
+ id: z19.string().min(1).max(100),
4731
+ kind: z19.enum(["tests", "static-analysis", "security", "complexity", "duplication"]),
4732
+ tool: z19.string().min(1).max(200),
4733
+ command: z19.string().min(1).max(2e3),
4734
+ commit: z19.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
4735
+ workingTreeClean: z19.boolean().optional(),
4736
+ source: z19.string().max(2e3).optional(),
4737
+ sourceRoot: z19.string().min(1).max(4e3).optional(),
4738
+ status: z19.enum(["completed", "skipped", "unavailable"]).default("completed"),
4739
+ reason: z19.string().min(1).max(2e3).optional(),
4740
+ exitCode: z19.number().int().nullable().optional(),
4741
+ report: z19.object({ format: z19.enum(["vitest-json", "sarif"]), path: z19.string().min(1).max(4e3) }).optional()
4563
4742
  });
4564
4743
  }
4565
4744
  });
4566
4745
 
4567
4746
  // src/review/review.ts
4568
- import fs17 from "fs/promises";
4747
+ import fs18 from "fs/promises";
4569
4748
  import path22 from "path";
4570
4749
  import { execFile as execFile15 } from "child_process";
4571
4750
  import { promisify as promisify15 } from "util";
@@ -4645,7 +4824,7 @@ async function computeReview(rootDir, base, options = {}) {
4645
4824
  analyzed,
4646
4825
  async (relPath) => {
4647
4826
  try {
4648
- await fs17.access(path22.join(resolvedRoot, relPath));
4827
+ await fs18.access(path22.join(resolvedRoot, relPath));
4649
4828
  return true;
4650
4829
  } catch {
4651
4830
  return false;
@@ -5121,12 +5300,12 @@ __export(assemble_exports, {
5121
5300
  assembleContext: () => assembleContext
5122
5301
  });
5123
5302
  import path25 from "path";
5124
- import fs18 from "fs/promises";
5303
+ import fs19 from "fs/promises";
5125
5304
  async function collectImpact(root, candidates) {
5126
5305
  const targets = /* @__PURE__ */ new Set();
5127
5306
  let sourceFiles;
5128
5307
  for (const candidate of sanitizeRepoPaths(candidates)) {
5129
- const stat = await fs18.stat(path25.join(root, candidate)).catch(() => null);
5308
+ const stat = await fs19.stat(path25.join(root, candidate)).catch(() => null);
5130
5309
  if (stat?.isDirectory()) {
5131
5310
  sourceFiles ??= await (await createFileAccess(root)).list();
5132
5311
  for (const file of sourceFiles) {
@@ -5353,8 +5532,8 @@ __export(observations_exports, {
5353
5532
  observeActivation: () => observeActivation,
5354
5533
  readObservation: () => readObservation
5355
5534
  });
5356
- import fs19 from "fs/promises";
5357
- import { z as z18 } from "zod";
5535
+ import fs20 from "fs/promises";
5536
+ import { z as z20 } from "zod";
5358
5537
  function observationPath(directory, host, revision, version = executingVersion()) {
5359
5538
  return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
5360
5539
  }
@@ -5370,8 +5549,8 @@ async function observeActivation(dir, event, options = {}) {
5370
5549
  if (host !== "codex" && host !== "claude" || !revision || !process.env.MASON_SETUP_ROOT) return null;
5371
5550
  try {
5372
5551
  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];
5373
- const ws = capturedDirectory ? { root: await fs19.realpath(dir), directory: capturedDirectory } : await workspace(dir);
5374
- if (ws.root !== await fs19.realpath(process.env.MASON_SETUP_ROOT)) return null;
5552
+ const ws = capturedDirectory ? { root: await fs20.realpath(dir), directory: capturedDirectory } : await workspace(dir);
5553
+ if (ws.root !== await fs20.realpath(process.env.MASON_SETUP_ROOT)) return null;
5375
5554
  const setup = await loadSetup(ws.root);
5376
5555
  if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
5377
5556
  const directory = ws.directory + "/activation";
@@ -5419,19 +5598,19 @@ var init_observations = __esm({
5419
5598
  init_storage();
5420
5599
  init_model();
5421
5600
  init_launcher();
5422
- observationSchema = z18.object({
5423
- version: z18.literal(1),
5424
- masonVersion: z18.string(),
5425
- root: z18.string(),
5426
- revision: z18.string(),
5427
- host: z18.enum(["codex", "claude"]),
5428
- contextCalls: z18.number().int().nonnegative(),
5429
- lastContextAt: z18.string().optional(),
5430
- sessions: z18.record(z18.object({
5431
- events: z18.array(z18.enum(events)),
5432
- at: z18.string(),
5433
- verificationStatus: z18.string().optional(),
5434
- reportPath: z18.string().optional()
5601
+ observationSchema = z20.object({
5602
+ version: z20.literal(1),
5603
+ masonVersion: z20.string(),
5604
+ root: z20.string(),
5605
+ revision: z20.string(),
5606
+ host: z20.enum(["codex", "claude"]),
5607
+ contextCalls: z20.number().int().nonnegative(),
5608
+ lastContextAt: z20.string().optional(),
5609
+ sessions: z20.record(z20.object({
5610
+ events: z20.array(z20.enum(events)),
5611
+ at: z20.string(),
5612
+ verificationStatus: z20.string().optional(),
5613
+ reportPath: z20.string().optional()
5435
5614
  }))
5436
5615
  });
5437
5616
  }
@@ -5787,7 +5966,7 @@ __export(config_exports2, {
5787
5966
  saveConfluenceConfig: () => saveConfluenceConfig,
5788
5967
  validateProvider: () => validateProvider
5789
5968
  });
5790
- import fs20 from "fs/promises";
5969
+ import fs21 from "fs/promises";
5791
5970
  import path26 from "path";
5792
5971
  import os3 from "os";
5793
5972
  import { execFile as execFile17 } from "child_process";
@@ -5800,15 +5979,15 @@ function configFile() {
5800
5979
  }
5801
5980
  async function loadConfig() {
5802
5981
  try {
5803
- const raw = await fs20.readFile(configFile(), "utf-8");
5982
+ const raw = await fs21.readFile(configFile(), "utf-8");
5804
5983
  return JSON.parse(raw);
5805
5984
  } catch {
5806
5985
  return null;
5807
5986
  }
5808
5987
  }
5809
5988
  async function saveConfig(config) {
5810
- await fs20.mkdir(configDir(), { recursive: true });
5811
- await fs20.writeFile(configFile(), JSON.stringify(config, null, 2), "utf-8");
5989
+ await fs21.mkdir(configDir(), { recursive: true });
5990
+ await fs21.writeFile(configFile(), JSON.stringify(config, null, 2), "utf-8");
5812
5991
  }
5813
5992
  function getDefaultModel(provider) {
5814
5993
  return DEFAULT_MODELS[provider];
@@ -5968,7 +6147,7 @@ var init_renderer = __esm({
5968
6147
  });
5969
6148
 
5970
6149
  // src/confluence/diff.ts
5971
- import fs21 from "fs/promises";
6150
+ import fs22 from "fs/promises";
5972
6151
  import path27 from "path";
5973
6152
  import { createHash as createHash5 } from "crypto";
5974
6153
  function hashDescription(description) {
@@ -5982,7 +6161,7 @@ function syncStatePath(rootDir) {
5982
6161
  }
5983
6162
  async function loadSyncState(rootDir) {
5984
6163
  try {
5985
- const raw = await fs21.readFile(syncStatePath(rootDir), "utf-8");
6164
+ const raw = await fs22.readFile(syncStatePath(rootDir), "utf-8");
5986
6165
  const parsed = JSON.parse(raw);
5987
6166
  if (parsed.version !== 2) return null;
5988
6167
  return parsed;
@@ -5991,8 +6170,8 @@ async function loadSyncState(rootDir) {
5991
6170
  }
5992
6171
  }
5993
6172
  async function saveSyncState(rootDir, state) {
5994
- await fs21.mkdir(syncStateDir(rootDir), { recursive: true });
5995
- await fs21.writeFile(
6173
+ await fs22.mkdir(syncStateDir(rootDir), { recursive: true });
6174
+ await fs22.writeFile(
5996
6175
  syncStatePath(rootDir),
5997
6176
  JSON.stringify(state, null, 2),
5998
6177
  "utf-8"
@@ -6540,13 +6719,14 @@ var init_sync = __esm({
6540
6719
 
6541
6720
  // src/mcp/server.ts
6542
6721
  init_provenance();
6722
+ init_decisions();
6543
6723
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6544
6724
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6545
- import { z as z19 } from "zod";
6725
+ import { z as z21 } from "zod";
6546
6726
 
6547
6727
  // src/mcp/tools.ts
6548
6728
  init_repair();
6549
- import fs22 from "fs/promises";
6729
+ import fs23 from "fs/promises";
6550
6730
  import path28 from "path";
6551
6731
  import { execFile as execFile19 } from "child_process";
6552
6732
  import { promisify as promisify19 } from "util";
@@ -7152,14 +7332,14 @@ init_files();
7152
7332
  import { createHash as createHash3 } from "crypto";
7153
7333
  import { execFile as execFile11 } from "child_process";
7154
7334
  import { promisify as promisify11 } from "util";
7155
- import { z as z4 } from "zod";
7335
+ import { z as z5 } from "zod";
7156
7336
  var exec10 = promisify11(execFile11);
7157
- var requestSchema = z4.object({
7158
- id: z4.string().regex(/^[a-zA-Z0-9_-]+$/),
7159
- action: z4.enum(["prepare", "accept", "reaffirm", "retire"]).default("prepare"),
7160
- reviewer: z4.string().trim().min(1).max(200).optional(),
7161
- note: z4.string().trim().min(1).max(1500).optional(),
7162
- reviewToken: z4.string().regex(/^[a-f0-9]{64}$/).optional()
7337
+ var requestSchema = z5.object({
7338
+ id: z5.string().regex(/^[a-zA-Z0-9_-]+$/),
7339
+ action: z5.enum(["prepare", "accept", "reaffirm", "retire"]).default("prepare"),
7340
+ reviewer: z5.string().trim().min(1).max(200).optional(),
7341
+ note: z5.string().trim().min(1).max(1500).optional(),
7342
+ reviewToken: z5.string().regex(/^[a-f0-9]{64}$/).optional()
7163
7343
  });
7164
7344
  async function reviewState(root, record) {
7165
7345
  const [headHash, workingTree, changes] = await Promise.all([
@@ -7383,16 +7563,16 @@ ${JSON.stringify({ partials }, null, 2)}`;
7383
7563
  init_storage();
7384
7564
  init_snapshot();
7385
7565
  import fs12 from "fs/promises";
7386
- import { z as z5 } from "zod";
7566
+ import { z as z6 } from "zod";
7387
7567
  var DIRECTORY = ".mason/partial-snapshots";
7388
- var partialSchema = z5.object({
7389
- batchId: z5.string().regex(/^[a-zA-Z0-9_-]+$/),
7390
- offset: z5.number().int().nonnegative(),
7391
- features: z5.record(featureSchema),
7392
- flows: z5.record(flowSchema),
7393
- savedAt: z5.string()
7568
+ var partialSchema = z6.object({
7569
+ batchId: z6.string().regex(/^[a-zA-Z0-9_-]+$/),
7570
+ offset: z6.number().int().nonnegative(),
7571
+ features: z6.record(featureSchema),
7572
+ flows: z6.record(flowSchema),
7573
+ savedAt: z6.string()
7394
7574
  });
7395
- var scopeSchema = z5.object({ files: z5.array(z5.string()), savedAt: z5.string() });
7575
+ var scopeSchema = z6.object({ files: z6.array(z6.string()), savedAt: z6.string() });
7396
7576
  async function savePartial(rootDir, partial) {
7397
7577
  if (!/^[a-zA-Z0-9_-]+$/.test(partial.batchId)) throw new Error(`Invalid batchId: ${partial.batchId}`);
7398
7578
  await writeStoreJson(rootDir, `${DIRECTORY}/${partial.batchId}.json`, partialSchema.parse(partial));
@@ -7497,7 +7677,7 @@ async function detectProjectSnapshot(rootDir) {
7497
7677
  const present = [];
7498
7678
  for (const file of buildFiles) {
7499
7679
  try {
7500
- await fs22.access(path28.join(rootDir, file));
7680
+ await fs23.access(path28.join(rootDir, file));
7501
7681
  present.push(file);
7502
7682
  } catch {
7503
7683
  }
@@ -8363,7 +8543,7 @@ function createMcpServer() {
8363
8543
  const server = new McpServer(
8364
8544
  {
8365
8545
  name: "mason",
8366
- version: "0.16.2"
8546
+ version: "0.16.3"
8367
8547
  },
8368
8548
  {
8369
8549
  instructions: 'Mason retrieves recorded decisions, file impact, and optional feature/flow maps. Use get_context with the task and known files, and get_impact before editing. Save learned rationale and constraints with save_decision; review and commit the local records through the project workflow. These tools work without initialization or a concept map. Consult trust and diagnostics: changed or unknown freshness needs source inspection, failed verification needs correction, and proposals are suggestions and legacy records are unreviewed. Accepted decisions are recorded team constraints, still subject to freshness checks. Use review_decision to prepare code evidence and record only authorized acceptance, reaffirmation, or retirement. Never invent a reviewer or treat these recorded identities as authenticated approval. mason_init returns documentation audit and committed-diff review findings with a quickstart guide. Use mode: "map" only when a full architecture map is requested. A missing map is not a setup failure; use decisions and source evidence. get_snapshot provides architecture navigation when a map is available. The mason-audit and mason-review CLIs also work without setup.'
@@ -8373,11 +8553,11 @@ function createMcpServer() {
8373
8553
  "mason_init",
8374
8554
  "Inspect this project now: returns documentation audit findings, committed-diff review findings, decision/map status, and a quickstart playbook. Quickstart and map modes are read-only and deterministic. Explicit mode: setup configures MCP, instructions and lifecycle hooks to use mason on PATH while retaining original audit evidence; use it only when the user requests setup. Optional host selects codex or claude. Optional base selects the review comparison; evidence imports CI manifests with check outcomes, commit freshness, and links to changed files and accepted decisions. mode: map returns the full Map-Reduce build workflow. Repeat calls refresh findings even after setup.",
8375
8555
  {
8376
- dir: z19.string().describe("Absolute path to the project root directory"),
8377
- mode: z19.enum(["quickstart", "map", "setup"]).optional().default("quickstart").describe("Quickstart inspects without edits; map requests an architecture build; setup installs the shared onboarding flow."),
8378
- host: z19.enum(["codex", "claude"]).optional().describe("Assistant to configure in setup mode; inferred only when unambiguous."),
8379
- base: z19.string().optional().describe("Git ref for committed-diff review. Defaults to the first available main branch ref."),
8380
- evidence: z19.array(z19.string()).max(10).optional().describe("Repository-local CI evidence manifests to include in the review. Imports Vitest JSON and SARIF without executing check commands.")
8556
+ dir: z21.string().describe("Absolute path to the project root directory"),
8557
+ mode: z21.enum(["quickstart", "map", "setup"]).optional().default("quickstart").describe("Quickstart inspects without edits; map requests an architecture build; setup installs the shared onboarding flow."),
8558
+ host: z21.enum(["codex", "claude"]).optional().describe("Assistant to configure in setup mode; inferred only when unambiguous."),
8559
+ base: z21.string().optional().describe("Git ref for committed-diff review. Defaults to the first available main branch ref."),
8560
+ evidence: z21.array(z21.string()).max(10).optional().describe("Repository-local CI evidence manifests to include in the review. Imports Vitest JSON and SARIF without executing check commands.")
8381
8561
  },
8382
8562
  async ({ dir, mode, host, base, evidence }) => {
8383
8563
  const result = await masonInit(dir, { mode, host, base, evidence });
@@ -8388,8 +8568,8 @@ function createMcpServer() {
8388
8568
  "mason_automation",
8389
8569
  "Inspect installed automation and observed host events, or resume/check retained documentation repair evidence across sessions. status is read-only; check saves local baselines and verification reports without editing source or approving advisories. Returns concise results with a full report path. Works without a map.",
8390
8570
  {
8391
- dir: z19.string().describe("Absolute path to the project directory"),
8392
- action: z19.enum(["status", "check"]).describe("Inspect configuration and receipts, or capture/resume and verify original audit evidence.")
8571
+ dir: z21.string().describe("Absolute path to the project directory"),
8572
+ action: z21.enum(["status", "check"]).describe("Inspect configuration and receipts, or capture/resume and verify original audit evidence.")
8393
8573
  },
8394
8574
  async ({ dir, action }) => ({ content: [{ type: "text", text: await masonAutomation(dir, action) }] })
8395
8575
  );
@@ -8397,10 +8577,10 @@ function createMcpServer() {
8397
8577
  "mason_repair",
8398
8578
  "Track documentation repairs against original audit evidence. prepare saves a local baseline and returns a scoped work order; verify reads that baseline and reports resolved, unresolved, review-required, unverified, and new findings. Suppressed advisories remain unresolved. Does not edit documentation or approve decisions. No map required.",
8399
8579
  {
8400
- dir: z19.string().describe("Absolute path to the project root directory"),
8401
- action: z19.enum(["prepare", "verify"]),
8402
- baselinePath: z19.string().optional().describe("Original baseline path returned by prepare; required for verify."),
8403
- checks: z19.array(z19.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"])).min(1).optional().describe("Optional audit check subset for prepare. Verification always uses the original checks.")
8580
+ dir: z21.string().describe("Absolute path to the project root directory"),
8581
+ action: z21.enum(["prepare", "verify"]),
8582
+ baselinePath: z21.string().optional().describe("Original baseline path returned by prepare; required for verify."),
8583
+ checks: z21.array(z21.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"])).min(1).optional().describe("Optional audit check subset for prepare. Verification always uses the original checks.")
8404
8584
  },
8405
8585
  async ({ dir, action, baselinePath, checks }) => {
8406
8586
  const result = await masonRepair(dir, { action, baselinePath, checks });
@@ -8411,8 +8591,8 @@ function createMcpServer() {
8411
8591
  "mason_complete_init",
8412
8592
  "Record assistant instruction setup locally in ignored .mason/local/project.json, with feature settings in shared .mason/config.json. Other tools work without this marker. Repeated calls preserve the original setup time and existing settings; pass confluenceConfigured only to change that setting.",
8413
8593
  {
8414
- dir: z19.string().describe("Absolute path to the project root directory"),
8415
- confluenceConfigured: z19.boolean().optional().describe("Set the Confluence setup status; omit to preserve the existing value")
8594
+ dir: z21.string().describe("Absolute path to the project root directory"),
8595
+ confluenceConfigured: z21.boolean().optional().describe("Set the Confluence setup status; omit to preserve the existing value")
8416
8596
  },
8417
8597
  async ({ dir, confluenceConfigured }) => {
8418
8598
  const result = await masonCompleteInit(dir, { confluenceConfigured });
@@ -8423,11 +8603,11 @@ function createMcpServer() {
8423
8603
  "mason_set_confluence",
8424
8604
  "Configure Confluence credentials. Two-step flow: (1) call without `spaceKey` to validate the credentials and receive a list of available spaces \u2014 relay them to the user. (2) call again with the same `baseUrl`/`email`/`apiToken` plus the chosen `spaceKey` to persist. Credentials are stored in `~/.mason/config.json`. Warn the user that the API token will be visible in chat history before they paste it.",
8425
8605
  {
8426
- baseUrl: z19.string().describe("Confluence base URL. Accepts `acme`, `acme.atlassian.net`, or `https://acme.atlassian.net` (normalized automatically)."),
8427
- email: z19.string().describe("User's Atlassian account email"),
8428
- apiToken: z19.string().describe("API token from id.atlassian.com/manage-profile/security/api-tokens"),
8429
- spaceKey: z19.string().optional().describe("Confluence space key. Omit on the first call to list available spaces."),
8430
- parentPageId: z19.string().optional().describe("Optional parent page ID under which Mason's index page is created")
8606
+ baseUrl: z21.string().describe("Confluence base URL. Accepts `acme`, `acme.atlassian.net`, or `https://acme.atlassian.net` (normalized automatically)."),
8607
+ email: z21.string().describe("User's Atlassian account email"),
8608
+ apiToken: z21.string().describe("API token from id.atlassian.com/manage-profile/security/api-tokens"),
8609
+ spaceKey: z21.string().optional().describe("Confluence space key. Omit on the first call to list available spaces."),
8610
+ parentPageId: z21.string().optional().describe("Optional parent page ID under which Mason's index page is created")
8431
8611
  },
8432
8612
  async ({ baseUrl, email, apiToken, spaceKey, parentPageId }) => {
8433
8613
  const result = await masonSetConfluence({
@@ -8444,7 +8624,7 @@ function createMcpServer() {
8444
8624
  "full_analysis",
8445
8625
  "One-shot orientation for a project WITHOUT a concept map (get_snapshot returned exists:false). Returns git history stats, project structure with file counts, curated code sample previews (~60 lines each), and test-to-source mapping. On a mapped project, prefer get_snapshot \u2014 it is cheaper and answers feature/architecture questions directly.",
8446
8626
  {
8447
- dir: z19.string().describe("Absolute path to the project root directory")
8627
+ dir: z21.string().describe("Absolute path to the project root directory")
8448
8628
  },
8449
8629
  async ({ dir }) => {
8450
8630
  const result = await fullAnalysis(dir);
@@ -8457,7 +8637,7 @@ function createMcpServer() {
8457
8637
  "analyze_project",
8458
8638
  "Run git history analysis on a codebase. Returns commit convention patterns, stale directories, and frequently changed files. These are aggregate stats across hundreds of commits that would be expensive to compute manually.",
8459
8639
  {
8460
- dir: z19.string().describe("Absolute path to the project root directory")
8640
+ dir: z21.string().describe("Absolute path to the project root directory")
8461
8641
  },
8462
8642
  async ({ dir }) => {
8463
8643
  const result = await analyzeProject(dir);
@@ -8470,8 +8650,8 @@ function createMcpServer() {
8470
8650
  "get_code_samples",
8471
8651
  "Get previews (first ~60 lines) of representative source files from the codebase. Includes entry points, config files, hot files (frequently changed), test examples, and one file per directory for breadth. Read files natively for full content.",
8472
8652
  {
8473
- dir: z19.string().describe("Absolute path to the project root directory"),
8474
- count: z19.number().optional().default(15).describe("Maximum number of files to sample (default: 15)")
8653
+ dir: z21.string().describe("Absolute path to the project root directory"),
8654
+ count: z21.number().optional().default(15).describe("Maximum number of files to sample (default: 15)")
8475
8655
  },
8476
8656
  async ({ dir, count: count3 }) => {
8477
8657
  const result = await getCodeSamples(dir, count3);
@@ -8484,7 +8664,7 @@ function createMcpServer() {
8484
8664
  "get_snapshot",
8485
8665
  "Return the optional feature-to-file architecture map with drift and trust evidence. If no map exists, returns exists:false plus project structure, Git signals, and test pairs. Decision capture, get_context, and get_impact still work. No initialization required.",
8486
8666
  {
8487
- dir: z19.string().describe("Absolute path to the project root directory")
8667
+ dir: z21.string().describe("Absolute path to the project root directory")
8488
8668
  },
8489
8669
  async ({ dir }) => {
8490
8670
  const result = await getSnapshot(dir);
@@ -8497,9 +8677,9 @@ function createMcpServer() {
8497
8677
  "get_context",
8498
8678
  "Assemble task context: matching decisions with rationale, approval, owner, sources, last review, and freshness, plus related tests, file impact, and any available map entries. Proposals are suggestions; legacy records are unreviewed; accepted decisions are constraints subject to freshness. No initialization or map required. Pass task and optional files. map.status and diagnostics preserve missing or invalid knowledge. Impact covers up to three unique files, expanding directory anchors.",
8499
8679
  {
8500
- dir: z19.string().describe("Absolute path to the project root directory"),
8501
- task: z19.string().describe("The task, bug, or change request in natural language \u2014 e.g. 'add rate limiting to the API client' or a ticket description"),
8502
- files: z19.array(z19.string()).optional().describe("Optional file paths already known to be involved (e.g. from a diff or stack trace). Entries containing them are boosted above pure text matches.")
8680
+ dir: z21.string().describe("Absolute path to the project root directory"),
8681
+ task: z21.string().describe("The task, bug, or change request in natural language \u2014 e.g. 'add rate limiting to the API client' or a ticket description"),
8682
+ files: z21.array(z21.string()).optional().describe("Optional file paths already known to be involved (e.g. from a diff or stack trace). Entries containing them are boosted above pure text matches.")
8503
8683
  },
8504
8684
  async ({ dir, task, files }) => {
8505
8685
  const result = await getContext(dir, task, files);
@@ -8515,10 +8695,10 @@ function createMcpServer() {
8515
8695
  "generate_snapshot_batch",
8516
8696
  "Map step of the concept-map build. Returns one batch of source files (skeletons of every file in the batch plus a few deeper-read bodies for grounding), along with a system prompt instructing you to derive features and flows for ONLY this batch. Call repeatedly with the returned `nextOffset` until it is null, calling `save_partial_snapshot` between each call. Use product-natural feature names so partials merge cleanly in the reduce step.",
8517
8697
  {
8518
- dir: z19.string().describe("Absolute path to the project root directory"),
8519
- offset: z19.number().int().optional().describe("0-indexed file offset to start the batch at. Omit on the first call; pass the `nextOffset` from the previous response for subsequent calls."),
8520
- batchSize: z19.number().int().optional().describe("Files per batch. Defaults to 50."),
8521
- files: z19.array(z19.string()).optional().describe("Scope the batch walk to this explicit file list \u2014 e.g. the drift set from mason_check_drift (changedFiles + unmappedFiles). Pass the SAME list on every batch call of one refresh run. Triggers refresh mode: reduce_snapshot will merge the partials into the existing map instead of rebuilding it.")
8698
+ dir: z21.string().describe("Absolute path to the project root directory"),
8699
+ offset: z21.number().int().optional().describe("0-indexed file offset to start the batch at. Omit on the first call; pass the `nextOffset` from the previous response for subsequent calls."),
8700
+ batchSize: z21.number().int().optional().describe("Files per batch. Defaults to 50."),
8701
+ files: z21.array(z21.string()).optional().describe("Scope the batch walk to this explicit file list \u2014 e.g. the drift set from mason_check_drift (changedFiles + unmappedFiles). Pass the SAME list on every batch call of one refresh run. Triggers refresh mode: reduce_snapshot will merge the partials into the existing map instead of rebuilding it.")
8522
8702
  },
8523
8703
  async ({ dir, offset, batchSize, files }) => {
8524
8704
  const result = await generateSnapshotBatch(dir, offset, batchSize, files);
@@ -8531,23 +8711,23 @@ function createMcpServer() {
8531
8711
  "save_partial_snapshot",
8532
8712
  "Persist the partial concept map you derived for one batch. Call this once per batch, with the `batchId` from the `generate_snapshot_batch` response. Partials accumulate in `.mason/partial-snapshots/` and are merged in the reduce step.",
8533
8713
  {
8534
- dir: z19.string().describe("Absolute path to the project root directory"),
8535
- batchId: z19.string().describe("The `batchId` returned by `generate_snapshot_batch`."),
8536
- offset: z19.number().int().describe("The `offset` returned by `generate_snapshot_batch`. Used to order partials in the reduce step."),
8537
- features: z19.record(
8538
- z19.object({
8539
- description: z19.string(),
8540
- files: z19.array(z19.string()),
8541
- tests: z19.array(z19.string()).optional(),
8542
- type: z19.enum(["capability", "infrastructure"]).optional().describe(
8714
+ dir: z21.string().describe("Absolute path to the project root directory"),
8715
+ batchId: z21.string().describe("The `batchId` returned by `generate_snapshot_batch`."),
8716
+ offset: z21.number().int().describe("The `offset` returned by `generate_snapshot_batch`. Used to order partials in the reduce step."),
8717
+ features: z21.record(
8718
+ z21.object({
8719
+ description: z21.string(),
8720
+ files: z21.array(z21.string()),
8721
+ tests: z21.array(z21.string()).optional(),
8722
+ type: z21.enum(["capability", "infrastructure"]).optional().describe(
8543
8723
  '"capability" (user-facing functionality) or "infrastructure" (internal plumbing with no end user \u2014 DI/service wiring, config, logging, adapters). Defaults to "capability".'
8544
8724
  )
8545
8725
  })
8546
8726
  ).describe("Partial features for this batch only \u2014 files outside the batch will be added by other partials."),
8547
- flows: z19.record(
8548
- z19.object({
8549
- description: z19.string(),
8550
- chain: z19.array(z19.string())
8727
+ flows: z21.record(
8728
+ z21.object({
8729
+ description: z21.string(),
8730
+ chain: z21.array(z21.string())
8551
8731
  })
8552
8732
  ).describe("Partial flows whose entire chain is in this batch. Cross-batch flows are reconstructed in reduce.")
8553
8733
  },
@@ -8562,7 +8742,7 @@ function createMcpServer() {
8562
8742
  "reduce_snapshot",
8563
8743
  "Reduce step of the concept-map build. Returns every partial snapshot plus a system prompt asking you to merge them into one coherent project-wide map. Resolve platform variants into single product features, dedupe near-duplicates, and ensure no file is dropped. After producing the unified map, call `save_snapshot` to persist it (this also clears the partials).",
8564
8744
  {
8565
- dir: z19.string().describe("Absolute path to the project root directory")
8745
+ dir: z21.string().describe("Absolute path to the project root directory")
8566
8746
  },
8567
8747
  async ({ dir }) => {
8568
8748
  const result = await reduceSnapshot(dir);
@@ -8575,25 +8755,25 @@ function createMcpServer() {
8575
8755
  "save_snapshot",
8576
8756
  "Save a concept-to-files map as a persistent project snapshot. Maps feature names and data flows to the files that implement them. Persists across conversations \u2014 future sessions can call get_snapshot to instantly find relevant files. No API key needed \u2014 you are the LLM generating the map.",
8577
8757
  {
8578
- dir: z19.string().describe("Absolute path to the project root directory"),
8579
- features: z19.record(
8580
- z19.object({
8581
- description: z19.string().describe("One-line description of the feature"),
8582
- files: z19.array(z19.string()).describe("File paths that implement this feature"),
8583
- tests: z19.array(z19.string()).optional().describe("Test file paths for this feature"),
8584
- type: z19.enum(["capability", "infrastructure"]).optional().describe(
8758
+ dir: z21.string().describe("Absolute path to the project root directory"),
8759
+ features: z21.record(
8760
+ z21.object({
8761
+ description: z21.string().describe("One-line description of the feature"),
8762
+ files: z21.array(z21.string()).describe("File paths that implement this feature"),
8763
+ tests: z21.array(z21.string()).optional().describe("Test file paths for this feature"),
8764
+ type: z21.enum(["capability", "infrastructure"]).optional().describe(
8585
8765
  'Classification: "capability" for user-facing functionality, "infrastructure" for internal plumbing with no end user (DI/service wiring, config, logging, adapters). Capabilities are published to Confluence; infrastructure stays in the AI concept map only. Defaults to "capability".'
8586
8766
  )
8587
8767
  })
8588
8768
  ).describe("Map of feature names to their implementing files"),
8589
- flows: z19.record(
8590
- z19.object({
8591
- description: z19.string().describe("One-line description of the flow"),
8592
- chain: z19.array(z19.string()).describe("Ordered list of file paths showing data/call flow")
8769
+ flows: z21.record(
8770
+ z21.object({
8771
+ description: z21.string().describe("One-line description of the flow"),
8772
+ chain: z21.array(z21.string()).describe("Ordered list of file paths showing data/call flow")
8593
8773
  })
8594
8774
  ).describe("Map of flow names to ordered file chains"),
8595
- removeFeatures: z19.array(z19.string()).optional().describe("Feature names to delete from the existing map \u2014 for features that were renamed or no longer exist. Applied before merging; only meaningful on incremental saves."),
8596
- removeFlows: z19.array(z19.string()).optional().describe("Flow names to delete from the existing map. Applied before merging; only meaningful on incremental saves.")
8775
+ removeFeatures: z21.array(z21.string()).optional().describe("Feature names to delete from the existing map \u2014 for features that were renamed or no longer exist. Applied before merging; only meaningful on incremental saves."),
8776
+ removeFlows: z21.array(z21.string()).optional().describe("Flow names to delete from the existing map. Applied before merging; only meaningful on incremental saves.")
8597
8777
  },
8598
8778
  async ({ dir, features, flows, removeFeatures, removeFlows }) => {
8599
8779
  const result = await saveSnapshotData(
@@ -8612,17 +8792,17 @@ function createMcpServer() {
8612
8792
  "save_decision",
8613
8793
  "Capture or revise a decision proposal with rationale, anchors, optional owner, sources, and a known actor. No setup or map required. Writes a local record and preserves content history. Changes create a pending proposal while the last accepted revision remains operative; unchanged content does not re-verify or refresh it. Use review_decision for authorized acceptance or reaffirmation. A proposal cannot supersede a record with an operative accepted revision; review its replacement and retire the original separately.",
8614
8794
  {
8615
- dir: z19.string().describe("Absolute path to the project root directory"),
8616
- title: z19.string().max(80).describe("Short, specific headline \u2014 becomes the stable record id"),
8617
- body: z19.string().max(1500).describe("The knowledge itself: what was tried/decided, why, and what to avoid. Must contain information NOT derivable by reading the code."),
8618
- category: z19.enum(["decision", "gotcha", "deprecation", "convention"]),
8619
- files: z19.array(z19.string()).optional().describe("Repo-relative files or directory prefixes this applies to. Matching is shared by retrieval, hooks, review, and drift checking; changes flag the decision for re-verification."),
8620
- id: z19.string().optional().describe("Existing id to revise. Changed content becomes a proposal; unchanged content leaves the review and freshness untouched."),
8621
- supersedes: z19.string().optional().describe("Id of an unreviewed record or proposal with no accepted revision to replace. Operative accepted decisions require separate review and retirement."),
8795
+ dir: z21.string().describe("Absolute path to the project root directory"),
8796
+ title: decisionTitleSchema.describe(`Short, specific headline (max ${TITLE_MAX_CHARS} characters). Generates a stable id on creation; revising the title keeps the existing id.`),
8797
+ body: decisionBodySchema.describe(`The knowledge itself: what was tried/decided, why, and what to avoid. Must contain information NOT derivable by reading the code. Aim for ${BODY_RECOMMENDED_CHARS} characters or fewer; up to ${BODY_MAX_CHARS} is accepted with a warning above the target. Matching context and hooks include the full body. Preserve exceptions; use sources for supporting references.`),
8798
+ category: z21.enum(["decision", "gotcha", "deprecation", "convention"]),
8799
+ files: z21.array(z21.string()).optional().describe("Repo-relative files or directory prefixes this applies to. Matching is shared by retrieval, hooks, review, and drift checking; changes flag the decision for re-verification."),
8800
+ id: z21.string().optional().describe("Existing id to revise. Changed content becomes a proposal; unchanged content leaves the review and freshness untouched."),
8801
+ supersedes: z21.string().optional().describe("Id of an unreviewed record or proposal with no accepted revision to replace. Operative accepted decisions require separate review and retirement."),
8622
8802
  owner: attributionSchema.shape.owner.describe("Responsible person or team, when known. Null clears it. Required for acceptance."),
8623
8803
  sources: attributionSchema.shape.sources.describe("Known PR, issue, incident, discussion, or document references. Omit to preserve; [] clears. At least one is required for acceptance."),
8624
8804
  actor: attributionSchema.shape.actor.describe("Known person or agent recording this revision. Omit if unknown; do not infer from Git identity."),
8625
- force: z19.boolean().optional().describe("Save even when a near-duplicate was detected")
8805
+ force: z21.boolean().optional().describe("Save even when a near-duplicate was detected")
8626
8806
  },
8627
8807
  async ({ dir, title, body, category, files, id, supersedes, force, owner, sources, actor }) => {
8628
8808
  const result = await saveDecision(dir, {
@@ -8644,12 +8824,12 @@ function createMcpServer() {
8644
8824
  "review_decision",
8645
8825
  "Prepare a decision review: returns the full record and history, any operative accepted revision, provenance, changes and previews for both sets of anchors, and a reviewToken. Then record accept, reaffirm, or retire with that token, the authorized reviewer, and a reason. Acceptance replaces the operative revision; retirement withdraws the entire record including its proposal. Acceptance requires owner, source, readable Git HEAD, and committed anchor changes. Changed records or code invalidate the token. Identities and approvals are recorded assertions for normal PR review, not authenticated proof.",
8646
8826
  {
8647
- dir: z19.string().describe("Absolute path to the project root directory"),
8648
- id: z19.string().regex(/^[a-zA-Z0-9_-]+$/).describe("Decision id from get_context or save_decision"),
8649
- action: z19.enum(["prepare", "accept", "reaffirm", "retire"]).optional().default("prepare").describe("Prepare is read-only. Other actions record an explicitly authorized review."),
8650
- reviewer: z19.string().trim().min(1).max(200).optional().describe("Identity of the actual reviewer; required for a verdict. Never invent one."),
8651
- note: z19.string().trim().min(1).max(1500).optional().describe("Review rationale; required for a verdict. Cite evidence for the decision."),
8652
- reviewToken: z19.string().regex(/^[a-f0-9]{64}$/).optional().describe("Token from the prepared review; rejects stale record or code revisions")
8827
+ dir: z21.string().describe("Absolute path to the project root directory"),
8828
+ id: z21.string().regex(/^[a-zA-Z0-9_-]+$/).describe("Decision id from get_context or save_decision"),
8829
+ action: z21.enum(["prepare", "accept", "reaffirm", "retire"]).optional().default("prepare").describe("Prepare is read-only. Other actions record an explicitly authorized review."),
8830
+ reviewer: z21.string().trim().min(1).max(200).optional().describe("Identity of the actual reviewer; required for a verdict. Never invent one."),
8831
+ note: z21.string().trim().min(1).max(1500).optional().describe("Review rationale; required for a verdict. Cite evidence for the decision."),
8832
+ reviewToken: z21.string().regex(/^[a-f0-9]{64}$/).optional().describe("Token from the prepared review; rejects stale record or code revisions")
8653
8833
  },
8654
8834
  async ({ dir, ...input }) => {
8655
8835
  const result = await reviewDecision2(dir, input);
@@ -8660,7 +8840,7 @@ function createMcpServer() {
8660
8840
  "mason_check_drift",
8661
8841
  "Check how far the concept map has drifted from HEAD. Deterministic (git + filesystem, no LLM). Returns which features/flows are stale and the changed files behind them, new source files not yet mapped, ghost files (mapped but deleted), renames, and a `recommendation`: `up-to-date` (nothing to do), `incremental` (update just the stale entries via save_snapshot), or `full-rebuild` (re-run the Map-Reduce build). Call this before trusting the map in a long session, or periodically to keep the map and any synced wikis fresh.",
8662
8842
  {
8663
- dir: z19.string().describe("Absolute path to the project root directory")
8843
+ dir: z21.string().describe("Absolute path to the project root directory")
8664
8844
  },
8665
8845
  async ({ dir }) => {
8666
8846
  const result = await checkDrift(dir);
@@ -8673,8 +8853,8 @@ function createMcpServer() {
8673
8853
  "verify_snapshot",
8674
8854
  "Spot-check the concept map's CORRECTNESS (drift checks freshness; this checks entries were right to begin with). Returns a sample of entries \u2014 always the never-verified and least-recently-verified first \u2014 with skeletons of their claimed files, for you to judge whether the files actually implement what the entry claims. Report verdicts back via save_verification. Run periodically, or after an automated refresh wrote entries no human reviewed.",
8675
8855
  {
8676
- dir: z19.string().describe("Absolute path to the project root directory"),
8677
- sample: z19.number().int().optional().describe("Entries to sample (default 5)")
8856
+ dir: z21.string().describe("Absolute path to the project root directory"),
8857
+ sample: z21.number().int().optional().describe("Entries to sample (default 5)")
8678
8858
  },
8679
8859
  async ({ dir, sample }) => {
8680
8860
  const result = await verifySnapshot(dir, sample);
@@ -8685,11 +8865,11 @@ function createMcpServer() {
8685
8865
  "save_verification",
8686
8866
  "Record verify_snapshot verdicts. Entries judged ok are stamped verifiedAt; failures are flagged verificationFailed with your note and surface in get_context, get_snapshot, and mason_check_drift until corrected. Verdict notes are required for failures.",
8687
8867
  {
8688
- dir: z19.string().describe("Absolute path to the project root directory"),
8689
- verdicts: z19.record(
8690
- z19.object({
8691
- ok: z19.boolean(),
8692
- note: z19.string().optional().describe("One line on what's wrong \u2014 required when ok is false")
8868
+ dir: z21.string().describe("Absolute path to the project root directory"),
8869
+ verdicts: z21.record(
8870
+ z21.object({
8871
+ ok: z21.boolean(),
8872
+ note: z21.string().optional().describe("One line on what's wrong \u2014 required when ok is false")
8693
8873
  })
8694
8874
  ).describe("Entry name \u2192 verdict, exactly as returned by verify_snapshot")
8695
8875
  },
@@ -8702,8 +8882,8 @@ function createMcpServer() {
8702
8882
  "get_impact",
8703
8883
  "Trace historical co-change partners, related tests, and references with evidence: resolved imports, explicit paths, or uncertain name candidates. Candidates are not proven dependencies. Deterministic and read-only; no initialization or concept map required.",
8704
8884
  {
8705
- dir: z19.string().describe("Absolute path to the project root directory"),
8706
- files: z19.array(z19.string()).describe("File paths or names to analyze (e.g., ['WeatherRepository.kt'] or ['src/services/auth.ts'])")
8885
+ dir: z21.string().describe("Absolute path to the project root directory"),
8886
+ files: z21.array(z21.string()).describe("File paths or names to analyze (e.g., ['WeatherRepository.kt'] or ['src/services/auth.ts'])")
8707
8887
  },
8708
8888
  async ({ dir, files }) => {
8709
8889
  const result = await getImpact(dir, files);
@@ -8716,12 +8896,12 @@ function createMcpServer() {
8716
8896
  "export_to_confluence",
8717
8897
  "Sync the project's concept map to Confluence as product-readable wiki pages: an index page, one page per feature (PM-language descriptions, no file paths), and a changelog page. Mason replaces managed page bodies; manual edits to those bodies are overwritten. Requires `mason_set_confluence` to have been called first.",
8718
8898
  {
8719
- dir: z19.string().describe("Absolute path to the project root directory"),
8720
- spaceKey: z19.string().optional().describe("Override the configured space key"),
8721
- parentPageId: z19.string().optional().describe("Override the configured parent page ID"),
8722
- indexPageTitle: z19.string().optional().describe("Title of the index page (default: 'Mason \u2014 System Map')"),
8723
- changelogPageTitle: z19.string().optional().describe("Title of the changelog page (default: 'Mason \u2014 Changelog')"),
8724
- featurePagePrefix: z19.string().optional().describe("Prefix for each feature page title (default: 'Feature: ')")
8899
+ dir: z21.string().describe("Absolute path to the project root directory"),
8900
+ spaceKey: z21.string().optional().describe("Override the configured space key"),
8901
+ parentPageId: z21.string().optional().describe("Override the configured parent page ID"),
8902
+ indexPageTitle: z21.string().optional().describe("Title of the index page (default: 'Mason \u2014 System Map')"),
8903
+ changelogPageTitle: z21.string().optional().describe("Title of the changelog page (default: 'Mason \u2014 Changelog')"),
8904
+ featurePagePrefix: z21.string().optional().describe("Prefix for each feature page title (default: 'Feature: ')")
8725
8905
  },
8726
8906
  async ({ dir, spaceKey, parentPageId, indexPageTitle, changelogPageTitle, featurePagePrefix }) => {
8727
8907
  const result = await exportToConfluenceTool(dir, {