mason-context 0.17.1 → 0.17.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.
@@ -346,6 +346,39 @@ var init_store = __esm({
346
346
  }
347
347
  });
348
348
 
349
+ // src/utils/profile.ts
350
+ import { AsyncLocalStorage } from "async_hooks";
351
+ async function profilePhase(label, run) {
352
+ const phases = profiles.getStore();
353
+ if (!phases) return run();
354
+ const start = performance.now();
355
+ try {
356
+ return await run();
357
+ } finally {
358
+ const timing = phases[label] ??= { calls: 0, totalMs: 0 };
359
+ timing.calls++;
360
+ timing.totalMs += performance.now() - start;
361
+ }
362
+ }
363
+ async function withProfile(run, emit) {
364
+ const phases = {};
365
+ const start = performance.now();
366
+ return profiles.run(phases, async () => {
367
+ try {
368
+ return await run();
369
+ } finally {
370
+ emit(JSON.stringify({ kind: "mason-profile", version: 1, totalMs: performance.now() - start, phases }));
371
+ }
372
+ });
373
+ }
374
+ var profiles;
375
+ var init_profile = __esm({
376
+ "src/utils/profile.ts"() {
377
+ "use strict";
378
+ profiles = new AsyncLocalStorage();
379
+ }
380
+ });
381
+
349
382
  // src/audit/commands.ts
350
383
  import path4 from "path";
351
384
  function splitCommands(line) {
@@ -949,16 +982,27 @@ import path6 from "path";
949
982
  import { execFile as execFile4 } from "child_process";
950
983
  import { promisify as promisify4 } from "util";
951
984
  import fg3 from "fast-glob";
952
- async function isDirty(resolvedRoot, relPath) {
985
+ async function dirtyDocs(resolvedRoot, files) {
986
+ if (!files.length) return /* @__PURE__ */ new Set();
953
987
  try {
954
- const { stdout } = await exec3(
955
- "git",
956
- ["status", "--porcelain", "--", `:(literal)${relPath}`],
957
- { cwd: resolvedRoot }
958
- );
959
- return stdout.trim().length > 0;
988
+ const dirty = /* @__PURE__ */ new Set();
989
+ for (let offset = 0; offset < files.length; offset += 256) {
990
+ const { stdout } = await exec3(
991
+ "git",
992
+ ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...files.slice(offset, offset + 256).map((file) => `:(literal)${file}`)],
993
+ { cwd: resolvedRoot, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 }
994
+ );
995
+ const entries = stdout.split("\0");
996
+ for (let i = 0; i < entries.length; i++) {
997
+ if (!entries[i]) continue;
998
+ const status = entries[i].slice(0, 2);
999
+ dirty.add(entries[i].slice(3));
1000
+ if (/[RC]/.test(status) && entries[i + 1]) dirty.add(entries[++i]);
1001
+ }
1002
+ }
1003
+ return dirty;
960
1004
  } catch {
961
- return true;
1005
+ return new Set(files);
962
1006
  }
963
1007
  }
964
1008
  async function discoverDocPaths(root) {
@@ -1039,21 +1083,30 @@ function documentScope(file) {
1039
1083
  return path6.posix.basename(parent).toLowerCase() === ".claude" ? path6.posix.dirname(parent) : parent;
1040
1084
  }
1041
1085
  async function discoverDocs(resolvedRoot) {
1086
+ return profilePhase("audit.documents", () => readDocs(resolvedRoot));
1087
+ }
1088
+ async function readDocs(resolvedRoot) {
1042
1089
  const docs = [];
1043
- for (const candidate of await discoverDocPaths(resolvedRoot)) {
1044
- const content2 = await readAuditInput(resolvedRoot, candidate);
1045
- if (content2 === null) throw new Error("Document disappeared during discovery: " + candidate);
1046
- const [lastCommit, dirty] = await Promise.all([lastCommitOf(resolvedRoot, candidate), isDirty(resolvedRoot, candidate)]);
1047
- docs.push({
1048
- path: candidate,
1049
- content: content2,
1050
- scope: documentScope(candidate),
1051
- kind: /^readme\.md$/i.test(path6.posix.basename(candidate)) ? "readme" : "instructions",
1052
- lineCount: content2.split("\n").length,
1053
- lastCommit,
1054
- dirty,
1055
- claims: extractClaims(content2)
1056
- });
1090
+ const files = await discoverDocPaths(resolvedRoot);
1091
+ const dirty = await dirtyDocs(resolvedRoot, files);
1092
+ for (let offset = 0; offset < files.length; offset += 4) {
1093
+ docs.push(...await Promise.all(files.slice(offset, offset + 4).map(async (candidate) => {
1094
+ const [content2, lastCommit] = await Promise.all([
1095
+ readAuditInput(resolvedRoot, candidate),
1096
+ profilePhase("audit.document-history", () => lastCommitOf(resolvedRoot, candidate))
1097
+ ]);
1098
+ if (content2 === null) throw new Error("Document disappeared during discovery: " + candidate);
1099
+ return {
1100
+ path: candidate,
1101
+ content: content2,
1102
+ scope: documentScope(candidate),
1103
+ kind: /^readme\.md$/i.test(path6.posix.basename(candidate)) ? "readme" : "instructions",
1104
+ lineCount: content2.split("\n").length,
1105
+ lastCommit,
1106
+ dirty: dirty.has(candidate),
1107
+ claims: extractClaims(content2)
1108
+ };
1109
+ })));
1057
1110
  }
1058
1111
  return docs;
1059
1112
  }
@@ -1061,6 +1114,7 @@ var exec3, DOC_CANDIDATES;
1061
1114
  var init_docs = __esm({
1062
1115
  "src/audit/docs.ts"() {
1063
1116
  "use strict";
1117
+ init_profile();
1064
1118
  init_claims();
1065
1119
  init_git();
1066
1120
  init_inputs();
@@ -2586,6 +2640,9 @@ var init_advisory_review = __esm({
2586
2640
  import fs11 from "fs/promises";
2587
2641
  import path18 from "path";
2588
2642
  async function computeAudit(rootDir, options = {}) {
2643
+ return profilePhase("audit.current", () => auditCurrent(rootDir, options));
2644
+ }
2645
+ async function auditCurrent(rootDir, options) {
2589
2646
  const resolvedRoot = path18.resolve(rootDir);
2590
2647
  const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
2591
2648
  if (docs.length === 0) return null;
@@ -2612,11 +2669,11 @@ async function computeAudit(rootDir, options = {}) {
2612
2669
  };
2613
2670
  if (!report.gitAvailable) return report;
2614
2671
  const changesSinceDoc = /* @__PURE__ */ new Map();
2672
+ const changesByCommit = /* @__PURE__ */ new Map();
2615
2673
  for (const doc of docs) {
2616
- changesSinceDoc.set(
2617
- doc.path,
2618
- doc.lastCommit ? await getChangesWithStatus(resolvedRoot, doc.lastCommit.hash) : null
2619
- );
2674
+ const hash2 = doc.lastCommit?.hash;
2675
+ if (hash2 && !changesByCommit.has(hash2)) changesByCommit.set(hash2, await getChangesWithStatus(resolvedRoot, hash2));
2676
+ changesSinceDoc.set(doc.path, hash2 ? changesByCommit.get(hash2) : null);
2620
2677
  }
2621
2678
  let decisionsPresent = false;
2622
2679
  try {
@@ -2650,6 +2707,7 @@ async function computeAudit(rootDir, options = {}) {
2650
2707
  var init_audit = __esm({
2651
2708
  "src/audit/audit.ts"() {
2652
2709
  "use strict";
2710
+ init_profile();
2653
2711
  init_drift();
2654
2712
  init_snapshot();
2655
2713
  init_docs();
@@ -2685,6 +2743,9 @@ async function docState(root) {
2685
2743
  return digest(docs);
2686
2744
  }
2687
2745
  async function stableAudit(root, checks, options = {}) {
2746
+ return profilePhase("repair.audit", () => readStableAudit(root, checks, options));
2747
+ }
2748
+ async function readStableAudit(root, checks, options) {
2688
2749
  const head = await getCurrentGitHash(root);
2689
2750
  const before = await docState(root);
2690
2751
  const report = await computeAudit(root, { ...options, checks });
@@ -2693,10 +2754,44 @@ async function stableAudit(root, checks, options = {}) {
2693
2754
  }
2694
2755
  return report;
2695
2756
  }
2696
- async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
2757
+ async function withRepairSession(root, options, run, cachedAudit) {
2758
+ const before = await Promise.all([getCurrentGitHash(root), docState(root), advisoryReviewInventory(root).then(digest)]);
2759
+ const audits = /* @__PURE__ */ new Map();
2760
+ const documents = /* @__PURE__ */ new Map();
2761
+ const history = /* @__PURE__ */ new Map();
2762
+ const assessments = /* @__PURE__ */ new Map();
2763
+ const memo = (cache, key, read) => {
2764
+ if (!cache.has(key)) cache.set(key, read());
2765
+ return cache.get(key);
2766
+ };
2767
+ const session = {
2768
+ audit: (checks) => memo(audits, [...checks].sort().join(","), async () => {
2769
+ if (cachedAudit && cachedAudit.headHash === before[0] && cachedAudit.root === root && digest([...checks].sort()) === digest([...cachedAudit.checksRun ?? []].sort())) {
2770
+ const current = structuredClone(cachedAudit);
2771
+ const reviews = (await Promise.all(allFindings(current).map((f) => assessAdvisory(root, f, before[0])))).filter((r) => r !== null);
2772
+ delete current.advisoryReviews;
2773
+ if (reviews.length) current.advisoryReviews = reviews;
2774
+ return current;
2775
+ }
2776
+ return computeAudit(root, { ...options, checks });
2777
+ }),
2778
+ document: (file) => memo(documents, file, async () => readBoundedFile(await storePath(root, file), 10 * 1024 * 1024)),
2779
+ history: (head) => memo(history, head, () => getChangesWithStatus(root, head)),
2780
+ assessment: (dir, finding, head) => memo(assessments, digest([dir, finding, head]), () => assessAdvisory(dir, finding, head))
2781
+ };
2782
+ const result = await run({
2783
+ prepare: () => prepareOriginal(root, ALL_CHECKS, options, session),
2784
+ verify: (baseline) => profilePhase("repair.verify", () => verifyOriginal(root, baseline, options, session)),
2785
+ currentAudit: () => session.audit(ALL_CHECKS)
2786
+ });
2787
+ const after = await Promise.all([getCurrentGitHash(root), docState(root), advisoryReviewInventory(root).then(digest)]);
2788
+ if (digest(before) !== digest(after)) throw new Error("HEAD, context files or advisory review records changed during verification; retry against stable evidence.");
2789
+ return result;
2790
+ }
2791
+ async function prepareOriginal(rootDir, checks, options, session) {
2697
2792
  const root = await fs12.realpath(rootDir);
2698
2793
  const selected = z7.array(checkSchema).nonempty().parse(checks);
2699
- const report = await stableAudit(root, selected, options);
2794
+ const report = await (session ? session.audit(selected) : stableAudit(root, selected, options));
2700
2795
  if (!report) throw new Error("No context files found to prepare a repair.");
2701
2796
  if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
2702
2797
  const storedReport = reportSchema.parse(report);
@@ -2721,24 +2816,24 @@ async function loadRepairBaseline(rootDir, baselinePath) {
2721
2816
  const original = stored.report;
2722
2817
  return { root, relative, original };
2723
2818
  }
2724
- async function verifyRepair(rootDir, baselinePath, options = {}) {
2819
+ async function verifyOriginal(rootDir, baselinePath, options, session) {
2725
2820
  const { root, relative, original } = await loadRepairBaseline(rootDir, baselinePath);
2726
2821
  const diagnostics = [];
2727
2822
  let reviewDigest;
2728
2823
  let current = null;
2729
2824
  try {
2730
- reviewDigest = digest(await advisoryReviewInventory(root));
2731
- current = await stableAudit(root, original.checksRun, options);
2825
+ if (!session) reviewDigest = digest(await advisoryReviewInventory(root));
2826
+ current = await (session ? session.audit(original.checksRun) : stableAudit(root, original.checksRun, options));
2732
2827
  if (!current) diagnostics.push("No context files remain available to audit.");
2733
2828
  else if (!current.gitAvailable) diagnostics.push("Git history is unavailable.");
2734
2829
  for (const doc of original.docs) {
2735
2830
  if (!allFindings(original).some((f) => f.anchor.doc === doc.path) || !current?.docs.some((d) => d.path === doc.path)) continue;
2736
- const content2 = await readBoundedFile(await storePath(root, doc.path), 10 * 1024 * 1024);
2831
+ const content2 = await (session ? session.document(doc.path) : readBoundedFile(await storePath(root, doc.path), 10 * 1024 * 1024));
2737
2832
  if (content2 === null || !content2.trim()) {
2738
2833
  diagnostics.push("Original context file " + doc.path + " is empty or unreadable; losing its claims does not verify a repair.");
2739
2834
  }
2740
2835
  }
2741
- if (await getChangesWithStatus(root, original.headHash) === null) {
2836
+ if (await (session ? session.history(original.headHash) : getChangesWithStatus(root, original.headHash)) === null) {
2742
2837
  diagnostics.push("The original audit commit is unavailable; repair history cannot be verified.");
2743
2838
  }
2744
2839
  } catch (error) {
@@ -2763,7 +2858,7 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
2763
2858
  if (!current.checksRun?.includes(finding.type) || skipped.length) {
2764
2859
  return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
2765
2860
  }
2766
- const review = await assessAdvisory(root, now ?? finding, original.headHash);
2861
+ const review = await (session?.assessment ?? assessAdvisory)(root, now ?? finding, original.headHash);
2767
2862
  if (review) {
2768
2863
  if (review.status === "unverified") return { ...base, review, status: "unverified", reason: review.reason };
2769
2864
  if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
@@ -2778,7 +2873,7 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
2778
2873
  }
2779
2874
  return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
2780
2875
  }));
2781
- try {
2876
+ if (!session) try {
2782
2877
  if (reviewDigest !== digest(await advisoryReviewInventory(root)) || current?.headHash !== await getCurrentGitHash(root)) {
2783
2878
  diagnostics.push("HEAD or advisory review records changed during verification; repeat against stable evidence.");
2784
2879
  for (const finding of findings) {
@@ -2818,6 +2913,7 @@ var count2, reportSchema, baselineSchema, repairFindingSchema;
2818
2913
  var init_repair = __esm({
2819
2914
  "src/audit/repair.ts"() {
2820
2915
  "use strict";
2916
+ init_profile();
2821
2917
  init_audit();
2822
2918
  init_docs();
2823
2919
  init_snapshot();
@@ -2878,20 +2974,20 @@ import { promisify as promisify9 } from "util";
2878
2974
  import fg5 from "fast-glob";
2879
2975
  import { z as z8 } from "zod";
2880
2976
  async function git3(root, ...args) {
2881
- return (await exec7("git", args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout;
2977
+ return profilePhase("automation.git", async () => (await exec7("git", args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout);
2882
2978
  }
2883
2979
  async function workspace(dir) {
2884
2980
  const root = await fs13.realpath((await git3(dir, "rev-parse", "--show-toplevel")).trim());
2885
- const gitDir = await fs13.realpath((await git3(root, "rev-parse", "--absolute-git-dir")).trim());
2886
- let branch;
2887
- try {
2888
- branch = (await git3(root, "symbolic-ref", "--quiet", "HEAD")).trim();
2889
- } catch {
2890
- branch = "detached";
2891
- }
2981
+ const [gitDir, branch] = await Promise.all([
2982
+ git3(root, "rev-parse", "--absolute-git-dir").then((value) => fs13.realpath(value.trim())),
2983
+ git3(root, "symbolic-ref", "--quiet", "HEAD").then((value) => value.trim(), () => "detached")
2984
+ ]);
2892
2985
  return { root, gitDir, branch, directory: ".mason/reports/automation/" + hash([root, gitDir, branch]).slice(0, 24) };
2893
2986
  }
2894
2987
  async function readInputs(root) {
2988
+ return profilePhase("automation.inputs", () => collectInputs(root));
2989
+ }
2990
+ async function collectInputs(root) {
2895
2991
  const docPaths = await discoverDocPaths(root);
2896
2992
  const [headText, docStatus, shallowPath, replacements] = await Promise.all([
2897
2993
  git3(root, "rev-parse", "HEAD"),
@@ -2908,21 +3004,29 @@ async function readInputs(root) {
2908
3004
  }
2909
3005
  const docs = {};
2910
3006
  const docContents = [];
3007
+ const parsedDocs = [];
2911
3008
  const claims = [];
3009
+ const presence = /* @__PURE__ */ new Map();
3010
+ const exists = (file) => {
3011
+ if (!presence.has(file)) presence.set(file, pathExists(root, file));
3012
+ return presence.get(file);
3013
+ };
2912
3014
  for (const file of docPaths) {
2913
3015
  const text2 = await content(root, file);
2914
3016
  docs[file] = text2 === null ? null : hash(text2);
2915
3017
  docContents.push([file, text2]);
2916
- for (const claim of text2 ? extractClaims(text2).paths : []) {
3018
+ const parsed = { path: file, content: text2 ?? "", claims: extractClaims(text2 ?? "") };
3019
+ parsedDocs.push(parsed);
3020
+ for (const claim of parsed.claims.paths) {
2917
3021
  const scope = pathClaimScope(file, claim);
2918
3022
  if (scope?.candidates.some(gitMetadataPath)) continue;
2919
3023
  for (const candidate of scope?.candidates ?? []) {
2920
3024
  if (optionalMasonPath(candidate)) continue;
2921
- claims.push([candidate, await pathExists(root, candidate), await pathExists(root, path20.posix.dirname(candidate))]);
3025
+ const [filePresent, parentPresent] = await Promise.all([exists(candidate), exists(path20.posix.dirname(candidate))]);
3026
+ claims.push([candidate, filePresent, parentPresent]);
2922
3027
  }
2923
3028
  }
2924
3029
  }
2925
- const parsedDocs = docContents.map(([file, text2]) => ({ path: file, content: text2 ?? "", claims: extractClaims(text2 ?? "") }));
2926
3030
  const combinedDocs = moduleDocumentation(parsedDocs);
2927
3031
  const countClaims = parsedDocs.flatMap((doc) => doc.claims.counts.map((claim) => ({ doc: doc.path, claim })));
2928
3032
  const decisionDirectory = await storePath(root, ".mason/decisions");
@@ -2944,7 +3048,7 @@ async function readInputs(root) {
2944
3048
  git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
2945
3049
  git3(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
2946
3050
  ]) : ["", ""];
2947
- const common = [4, engineVersion, head, shallow, replacements, docContents];
3051
+ const common = [5, engineVersion, head, shallow, replacements, docContents];
2948
3052
  const keys = {
2949
3053
  "deleted-reference": hash([common, claims, docStatus]),
2950
3054
  "new-module": hash([common, modules]),
@@ -2969,21 +3073,29 @@ function checkCache(raw, inputs) {
2969
3073
  reused.add(name);
2970
3074
  return structuredClone(entries[name].result);
2971
3075
  }
2972
- const result = await CHECKS[name](ctx);
3076
+ const result = await profilePhase("check." + name, () => CHECKS[name](ctx));
2973
3077
  ran.add(name);
2974
3078
  if (!result.skipped.length) entries[name] = { key: inputs.keys[name], result };
2975
3079
  else delete entries[name];
2976
3080
  return result;
2977
3081
  } };
2978
- return { options, ran, reused, diagnostic, serialize: () => {
2979
- const canonical = cacheSchema.shape.entries.parse(entries);
2980
- return { version: 1, entries: canonical, digest: hash(canonical) };
2981
- } };
3082
+ return {
3083
+ options,
3084
+ ran,
3085
+ reused,
3086
+ diagnostic,
3087
+ has: (name) => entries[name]?.key === inputs.keys[name] && !entries[name].result.skipped.length,
3088
+ serialize: () => {
3089
+ const canonical = cacheSchema.shape.entries.parse(entries);
3090
+ return { version: 1, entries: canonical, digest: hash(canonical) };
3091
+ }
3092
+ };
2982
3093
  }
2983
3094
  var exec7, engineVersion, hash, content, cacheSchema;
2984
3095
  var init_evidence = __esm({
2985
3096
  "src/automation/evidence.ts"() {
2986
3097
  "use strict";
3098
+ init_profile();
2987
3099
  init_docs();
2988
3100
  init_scope();
2989
3101
  init_claims();
@@ -2996,7 +3108,7 @@ var init_evidence = __esm({
2996
3108
  init_storage();
2997
3109
  init_advisory_review();
2998
3110
  exec7 = promisify9(execFile9);
2999
- engineVersion = true ? "0.17.1" : "development";
3111
+ engineVersion = true ? "0.17.3" : "development";
3000
3112
  hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
3001
3113
  content = readAuditInput;
3002
3114
  cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
@@ -3190,7 +3302,7 @@ var init_execution = __esm({
3190
3302
  import { randomUUID as randomUUID4 } from "crypto";
3191
3303
  import { z as z10 } from "zod";
3192
3304
  async function observeReadOnlyTool(dir, event) {
3193
- const ws = await workspace(dir);
3305
+ const ws = await profilePhase("automation.workspace", () => workspace(dir));
3194
3306
  await withLock(ws.root, ws.directory, async () => {
3195
3307
  const statePath = ws.directory + "/state.json";
3196
3308
  const raw = await readStoreJson(ws.root, statePath);
@@ -3237,24 +3349,41 @@ async function loadState(ws) {
3237
3349
  }
3238
3350
  return state;
3239
3351
  }
3240
- async function sharedAnalysis(ws, state, inputs, startedAt) {
3352
+ async function matchingAnalysis(ws, state, inputs) {
3241
3353
  const pointer = state.analysis;
3242
- if (!pointer || pointer.fingerprint !== inputs.fingerprint || pointer.completedAt < startedAt) return null;
3243
- const stored = sharedSchema.parse(await readStoreJson(ws.root, pointer.path));
3354
+ if (!pointer || pointer.fingerprint !== inputs.fingerprint) return { analysis: null };
3355
+ if (pointer.path !== ws.directory + "/checks/analysis-" + inputs.fingerprint + ".json") {
3356
+ throw new Error("Invalid automation analysis cache path; inspect the retained state.");
3357
+ }
3358
+ const invalid = { analysis: null, diagnostic: "Discarded an invalid current-audit cache; audit preparation and original verification were repeated." };
3359
+ let raw;
3360
+ try {
3361
+ raw = await readStoreJson(ws.root, pointer.path);
3362
+ } catch (error) {
3363
+ const cause = error.cause;
3364
+ if (cause instanceof SyntaxError || cause instanceof Error && cause.message === "expected a JSON object, received null") return invalid;
3365
+ throw error;
3366
+ }
3367
+ if (raw === null) return { analysis: null };
3368
+ const parsed = sharedSchema.safeParse(raw);
3369
+ if (!parsed.success) return invalid;
3370
+ const stored = parsed.data;
3244
3371
  const a = stored.analysis;
3245
3372
  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))) {
3246
- throw new Error("Invalid shared automation evidence; run a new check.");
3373
+ return invalid;
3247
3374
  }
3248
3375
  if (a.baselineDigest !== await baselineDigest(ws, state)) throw new Error("Repair baseline was modified; use the original baseline.");
3249
- return a;
3376
+ return { analysis: a };
3250
3377
  }
3251
3378
  async function verifyInputs(ws, inputs) {
3252
- const [after, currentWs] = await Promise.all([readInputs(ws.root), workspace(ws.root)]);
3253
- if (after.fingerprint !== inputs.fingerprint || currentWs.directory !== ws.directory) {
3254
- throw new Error("Repository inputs or branch changed during automation; no current verification was recorded. Retry on a stable checkout.");
3255
- }
3379
+ return profilePhase("automation.revalidate", async () => {
3380
+ const [after, currentWs] = await Promise.all([readInputs(ws.root), workspace(ws.root)]);
3381
+ if (after.fingerprint !== inputs.fingerprint || currentWs.directory !== ws.directory) {
3382
+ throw new Error("Repository inputs or branch changed during automation; no current verification was recorded. Retry on a stable checkout.");
3383
+ }
3384
+ });
3256
3385
  }
3257
- async function computeAnalysis(ws, state, inputs, event) {
3386
+ async function computeAnalysis(ws, state, inputs, event, previousAudit, cacheDiagnostic) {
3258
3387
  const baselines = [...state.baselines];
3259
3388
  const now = (/* @__PURE__ */ new Date()).toISOString();
3260
3389
  const empty = {
@@ -3283,7 +3412,7 @@ async function computeAnalysis(ws, state, inputs, event) {
3283
3412
  }
3284
3413
  }
3285
3414
  let cached = null;
3286
- const diagnostics = [];
3415
+ const diagnostics = cacheDiagnostic ? [cacheDiagnostic] : [];
3287
3416
  try {
3288
3417
  cached = await readStoreJson(ws.root, ws.directory + "/cache.json");
3289
3418
  } catch {
@@ -3291,19 +3420,26 @@ async function computeAnalysis(ws, state, inputs, event) {
3291
3420
  }
3292
3421
  const cache = checkCache(cached, inputs);
3293
3422
  if (cache.diagnostic) diagnostics.push(cache.diagnostic);
3294
- const saveBaseline = async () => {
3295
- if (baselines.length >= 128) throw new Error("128 retained baselines need review; automatic capture stopped without discarding original evidence.");
3296
- const prepared = await prepareRepair(ws.root, ALL_CHECKS, cache.options);
3297
- baselines.push({ path: prepared.baselinePath, at: now, event: event.event, fingerprint: inputs.fingerprint });
3298
- };
3299
- if (!baselines.length) await saveBaseline();
3300
- const verifications = [];
3301
- for (const baseline of baselines) verifications.push(await verifyRepair(ws.root, baseline.path, cache.options));
3302
- const known = new Set(verifications.flatMap((v) => v.findings.map((f) => f.id)));
3303
- if (verifications.some((v) => v.newFindings.some((f) => !known.has(findingId(f))))) {
3304
- await saveBaseline();
3305
- verifications.push(await verifyRepair(ws.root, baselines.at(-1).path, cache.options));
3306
- }
3423
+ const cachedAudit = previousAudit?.gitAvailable && previousAudit.headHash === inputs.head && previousAudit.root === ws.root && !previousAudit.skippedChecks.length && ALL_CHECKS.every((name) => previousAudit.checksRun?.includes(name) && cache.has(name)) ? previousAudit : void 0;
3424
+ if (cachedAudit) for (const name of ALL_CHECKS) cache.reused.add(name);
3425
+ let currentAudit = null;
3426
+ const verifications = await withRepairSession(ws.root, cache.options, async (repairs) => {
3427
+ const saveBaseline = async () => {
3428
+ if (baselines.length >= 128) throw new Error("128 retained baselines need review; automatic capture stopped without discarding original evidence.");
3429
+ const prepared = await repairs.prepare();
3430
+ baselines.push({ path: prepared.baselinePath, at: now, event: event.event, fingerprint: inputs.fingerprint });
3431
+ };
3432
+ if (!baselines.length) await saveBaseline();
3433
+ const verifications2 = [];
3434
+ for (const baseline of baselines) verifications2.push(await repairs.verify(baseline.path));
3435
+ const known = new Set(verifications2.flatMap((v) => v.findings.map((f) => f.id)));
3436
+ if (verifications2.some((v) => v.newFindings.some((f) => !known.has(findingId(f))))) {
3437
+ await saveBaseline();
3438
+ verifications2.push(await repairs.verify(baselines.at(-1).path));
3439
+ }
3440
+ currentAudit = await repairs.currentAudit();
3441
+ return verifications2;
3442
+ }, cachedAudit);
3307
3443
  const merged = /* @__PURE__ */ new Map();
3308
3444
  for (const verification of verifications) {
3309
3445
  for (const finding of verification.findings) {
@@ -3324,20 +3460,20 @@ async function computeAnalysis(ws, state, inputs, event) {
3324
3460
  reportPath: "",
3325
3461
  findings: [...merged.values()],
3326
3462
  diagnostics: [...new Set(diagnostics)],
3327
- checks: { ran: [...cache.ran], reused: [...cache.reused].filter((name) => !cache.ran.has(name)), skipped: verifications.at(-1).currentAudit?.skippedChecks ?? [] },
3463
+ checks: { ran: [...cache.ran], reused: [...cache.reused].filter((name) => !cache.ran.has(name)), skipped: verifications.at(-1).currentAudit?.skippedChecks ?? [], ...cachedAudit ? { auditReused: true } : {} },
3328
3464
  counts,
3329
3465
  capture: "unknown",
3330
3466
  scope: SCOPE
3331
3467
  };
3332
- return { baselines, report, cache: cache.ran.size || cached === null || cache.diagnostic ? cache.serialize() : null };
3468
+ return { baselines, report, currentAudit, cache: cache.ran.size || cached === null || cache.diagnostic ? cache.serialize() : null };
3333
3469
  }
3334
3470
  async function analyze(ws, inputs, event, startedAt) {
3335
3471
  return withLock(ws.root, ws.directory + "/analysis/" + inputs.fingerprint, async () => {
3336
3472
  const before = await loadState(ws);
3337
- const shared = await sharedAnalysis(ws, before, inputs, startedAt);
3338
- if (shared) return { analysis: shared, shared: true };
3473
+ const { analysis: previous, diagnostic } = await matchingAnalysis(ws, before, inputs);
3474
+ if (previous && previous.completedAt >= startedAt) return { analysis: previous, shared: true };
3339
3475
  const originalDigest = await baselineDigest(ws, before);
3340
- const computed = await computeAnalysis(ws, before, inputs, event);
3476
+ const computed = await computeAnalysis(ws, before, inputs, event, previous?.currentAudit, diagnostic);
3341
3477
  await verifyInputs(ws, inputs);
3342
3478
  if (originalDigest !== await baselineDigest(ws, before)) throw new Error("Repair baseline was modified during automation; original evidence must be inspected.");
3343
3479
  const id = randomUUID4();
@@ -3346,7 +3482,8 @@ async function analyze(ws, inputs, event, startedAt) {
3346
3482
  fingerprint: inputs.fingerprint,
3347
3483
  completedAt: Date.now(),
3348
3484
  baselineDigest: await baselineDigest(ws, { ...before, baselines: computed.baselines }),
3349
- report: computed.report
3485
+ report: computed.report,
3486
+ currentAudit: computed.currentAudit
3350
3487
  });
3351
3488
  const analysisPath = ws.directory + "/checks/analysis-" + inputs.fingerprint + ".json";
3352
3489
  await withLock(ws.root, ws.directory, async () => {
@@ -3365,17 +3502,17 @@ async function analyze(ws, inputs, event, startedAt) {
3365
3502
  }
3366
3503
  async function automate(dir, event) {
3367
3504
  const startedAt = Date.now();
3368
- const ws = await workspace(dir);
3505
+ const ws = await profilePhase("automation.workspace", () => workspace(dir));
3369
3506
  return recordExecution(ws.root, ws.directory, event.event, async () => {
3370
3507
  const inputs = await readInputs(ws.root);
3371
- const result = await analyze(ws, inputs, event, startedAt);
3508
+ const result = await profilePhase("automation.analysis", () => analyze(ws, inputs, event, startedAt));
3372
3509
  if (result.shared) {
3373
3510
  await verifyInputs(ws, inputs);
3374
3511
  if (result.analysis.baselineDigest !== await baselineDigest(ws, await loadState(ws))) {
3375
3512
  throw new Error("Repair baseline was modified during shared automation; original evidence must be inspected.");
3376
3513
  }
3377
3514
  }
3378
- return withLock(ws.root, ws.directory, async () => {
3515
+ return profilePhase("automation.publish", () => withLock(ws.root, ws.directory, async () => {
3379
3516
  const state = await loadState(ws);
3380
3517
  if (state.analysis?.id !== result.analysis.id) throw new Error("Automation evidence changed during publication; retry against the current inputs.");
3381
3518
  const report = structuredClone(result.analysis.report);
@@ -3438,11 +3575,11 @@ async function automate(dir, event) {
3438
3575
  if (persistReport) await writeStoreJson(ws.root, report.reportPath, report);
3439
3576
  await writeStoreJson(ws.root, ws.directory + "/state.json", state);
3440
3577
  return { report, message: notify ? summarize(report) : null, continueOnce };
3441
- });
3578
+ }));
3442
3579
  });
3443
3580
  }
3444
3581
  async function automationStatus(dir) {
3445
- const ws = await workspace(dir);
3582
+ const ws = await profilePhase("automation.workspace", () => workspace(dir));
3446
3583
  const execution = await executionStatus(ws.root, ws.directory);
3447
3584
  const notifications = await failureNotifications(ws.root, ws.directory);
3448
3585
  const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
@@ -3476,6 +3613,7 @@ var SCOPE, priority, cleanText, analysisSchema, sharedSchema, baselineDigest;
3476
3613
  var init_runtime = __esm({
3477
3614
  "src/automation/runtime.ts"() {
3478
3615
  "use strict";
3616
+ init_profile();
3479
3617
  init_execution();
3480
3618
  init_repair();
3481
3619
  init_types();
@@ -3490,6 +3628,7 @@ var init_runtime = __esm({
3490
3628
  fingerprint: z10.string(),
3491
3629
  completedAt: z10.number(),
3492
3630
  baselineDigest: z10.string(),
3631
+ currentAudit: reportSchema.nullable().optional(),
3493
3632
  report: z10.object({
3494
3633
  version: z10.literal(1),
3495
3634
  status: z10.enum(["verified", "issues-remain", "incomplete", "unavailable"]),
@@ -3503,7 +3642,8 @@ var init_runtime = __esm({
3503
3642
  checks: z10.object({
3504
3643
  ran: z10.array(z10.string()),
3505
3644
  reused: z10.array(z10.string()),
3506
- skipped: z10.array(z10.object({ check: z10.string(), reason: z10.string(), doc: z10.string().optional() }))
3645
+ skipped: z10.array(z10.object({ check: z10.string(), reason: z10.string(), doc: z10.string().optional() })),
3646
+ auditReused: z10.boolean().optional()
3507
3647
  }),
3508
3648
  counts: z10.object({ resolved: z10.number(), unresolved: z10.number(), "review-required": z10.number(), unverified: z10.number() }),
3509
3649
  capture: z10.enum(["observed", "unknown"]),
@@ -3511,9 +3651,9 @@ var init_runtime = __esm({
3511
3651
  })
3512
3652
  });
3513
3653
  sharedSchema = z10.object({ analysis: analysisSchema, digest: z10.string() });
3514
- baselineDigest = async (ws, state) => hash(await Promise.all(
3654
+ baselineDigest = async (ws, state) => profilePhase("automation.baselines", async () => hash(await Promise.all(
3515
3655
  state.baselines.map(async (b) => [b.path, await readStoreJson(ws.root, b.path)])
3516
- ));
3656
+ )));
3517
3657
  }
3518
3658
  });
3519
3659
 
@@ -3650,7 +3790,7 @@ var init_launcher = __esm({
3650
3790
  "use strict";
3651
3791
  init_evidence();
3652
3792
  init_model();
3653
- executingVersion = () => true ? "0.17.1" : "development";
3793
+ executingVersion = () => true ? "0.17.3" : "development";
3654
3794
  mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
3655
3795
  hookCommand = (host) => `mason --setup-host ${host} auto`;
3656
3796
  }
@@ -5491,6 +5631,7 @@ async function selectHost(root, explicit) {
5491
5631
  throw new Error("Choose the assistant for setup with --host codex or --host claude.");
5492
5632
  }
5493
5633
  async function setupProject(dir, options = {}) {
5634
+ options.progress?.step("Checking project configuration");
5494
5635
  const ws = await workspace(dir);
5495
5636
  const host = await selectHost(ws.root, options.host);
5496
5637
  return withLock(ws.root, ".mason/reports/setup-lock", async () => {
@@ -5519,11 +5660,14 @@ async function setupProject(dir, options = {}) {
5519
5660
  const revision = !changed && previous ? previous.revision : randomUUID6();
5520
5661
  const setup = existing ?? { version: 2, hosts: {} };
5521
5662
  setup.hosts[host] = { configuredVersion: installed.version, revision, fingerprint, mcpFingerprint, instructions: instructions.map((e) => e.path) };
5663
+ options.progress?.step("Auditing documentation and preserving original findings");
5522
5664
  const initial = await automate(ws.root, { event: "turn_start" });
5665
+ options.progress?.step("Reviewing project context and committed changes");
5523
5666
  const findings = await inspectOnboarding(ws.root, options.base, options.evidence);
5524
5667
  const receiptPath = ws.directory + "/setup-" + host + ".json";
5525
5668
  const initialReportPath = previousReceipt?.initialReportPath ?? initial.report.reportPath;
5526
5669
  const initialBaselinePaths = previousReceipt?.initialBaselinePaths ?? initial.report.baselinePaths;
5670
+ options.progress?.step(`Configuring ${host === "codex" ? "Codex" : "Claude Code"} integration`);
5527
5671
  await writeStoreJson(ws.root, receiptPath, {
5528
5672
  version: 1,
5529
5673
  host,
@@ -5538,6 +5682,7 @@ async function setupProject(dir, options = {}) {
5538
5682
  for (const edit of edits) if (await applyEdit(ws.root, edit)) changedFiles.push(edit.path);
5539
5683
  if (await applyEdit(ws.root, { path: SETUP_PATH, before: setupBefore, after: JSON.stringify(setup, null, 2) + "\n" })) changedFiles.push(SETUP_PATH);
5540
5684
  if (await applyEdit(ws.root, await completedHookOwnership(ws.root, hooks[0].path, configuredHook)) && !changedFiles.includes(ownership.path)) changedFiles.push(ownership.path);
5685
+ options.progress?.step("Checking configuration and retained findings");
5541
5686
  const checked = await automate(ws.root, { event: "turn_start" });
5542
5687
  const configured = await inspectHostConfig(ws.root, host, void 0);
5543
5688
  await writeStoreJson(ws.root, receiptPath, {
@@ -5607,6 +5752,58 @@ init_adapters();
5607
5752
  init_install();
5608
5753
  init_store();
5609
5754
  import { parseArgs } from "util";
5755
+
5756
+ // src/utils/progress.ts
5757
+ function createProgress(options = {}) {
5758
+ const write = options.write ?? ((text2) => {
5759
+ process.stderr.write(text2);
5760
+ });
5761
+ const interactive = options.interactive ?? (!!process.stderr.isTTY && process.env.TERM !== "dumb" && !process.env.CI);
5762
+ const columns = options.columns ?? (() => process.stderr.columns || 80);
5763
+ const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
5764
+ let message2 = "", started = 0, frame = 0;
5765
+ let timer;
5766
+ const elapsed = () => {
5767
+ const seconds = Math.floor((Date.now() - started) / 1e3);
5768
+ return seconds ? ` (${seconds}s)` : "";
5769
+ };
5770
+ const render = (prefix) => {
5771
+ const text2 = `${prefix} ${message2}${elapsed()}`;
5772
+ const width = Math.max(1, columns() - 1);
5773
+ write("\r\x1B[2K" + (text2.length > width ? text2.slice(0, width - 1) + "\u2026" : text2));
5774
+ };
5775
+ const stop = (success = true) => {
5776
+ if (timer) clearInterval(timer);
5777
+ timer = void 0;
5778
+ if (message2 && interactive) {
5779
+ render(success ? "\u2713" : "\u2717");
5780
+ write("\n");
5781
+ }
5782
+ message2 = "";
5783
+ };
5784
+ return {
5785
+ step(next) {
5786
+ stop();
5787
+ message2 = next.replace(/[\x00-\x1f\x7f-\x9f]/g, " ");
5788
+ started = Date.now();
5789
+ frame = 0;
5790
+ if (!interactive) {
5791
+ write(`Mason: ${message2}\u2026
5792
+ `);
5793
+ return;
5794
+ }
5795
+ render(frames[frame]);
5796
+ timer = setInterval(() => {
5797
+ render(frames[++frame % frames.length]);
5798
+ }, 100);
5799
+ timer.unref();
5800
+ },
5801
+ stop
5802
+ };
5803
+ }
5804
+
5805
+ // src/automation/cli.ts
5806
+ init_profile();
5610
5807
  var TEARDOWN_USAGE = `Usage: mason teardown [options]
5611
5808
 
5612
5809
  Disconnect Mason from this Git project. Defaults to all project hosts.
@@ -5634,6 +5831,7 @@ var USAGE = `Usage: mason-auto <setup|teardown|install|config|status|check|hook>
5634
5831
  --command <prefix> Installed executable prefix for install/config
5635
5832
  --dry-run Preview teardown without writing
5636
5833
  --json Machine-readable output (status also uses JSON when piped)
5834
+ --profile Local phase timings on stderr for check/hook; no file contents
5637
5835
 
5638
5836
  check exits 0 for verified checks, 1 for issues, 2 for incomplete/unavailable.
5639
5837
  teardown exits 0 for complete/no-op, 2 when cleanup needs attention (also in dry runs).
@@ -5645,7 +5843,8 @@ var parseCli = (argv2) => parseArgs({ args: argv2, allowPositionals: true, optio
5645
5843
  command: { type: "string" },
5646
5844
  json: { type: "boolean" },
5647
5845
  help: { type: "boolean", short: "h" },
5648
- "dry-run": { type: "boolean" }
5846
+ "dry-run": { type: "boolean" },
5847
+ profile: { type: "boolean" }
5649
5848
  } });
5650
5849
  function isHookCommand(argv2) {
5651
5850
  try {
@@ -5655,11 +5854,13 @@ function isHookCommand(argv2) {
5655
5854
  return false;
5656
5855
  }
5657
5856
  }
5658
- async function runAutomationCli(argv2, stdin = "", io = {
5857
+ var defaultIo = {
5659
5858
  out: (s) => process.stdout.write(s + "\n"),
5660
5859
  err: (s) => process.stderr.write(s + "\n")
5661
- }, options = {}) {
5860
+ };
5861
+ async function runAutomationCli(argv2, stdin = "", io = defaultIo, options = {}) {
5662
5862
  let action = "";
5863
+ let progress;
5663
5864
  try {
5664
5865
  const { values, positionals } = parseCli(argv2);
5665
5866
  if (values.help || !positionals.length) {
@@ -5668,6 +5869,8 @@ async function runAutomationCli(argv2, stdin = "", io = {
5668
5869
  }
5669
5870
  if (positionals.length !== 1) throw new Error("Expected one command.");
5670
5871
  [action] = positionals;
5872
+ if (values.profile && !["check", "hook"].includes(action)) throw new Error("--profile applies only to check/hook.");
5873
+ const measured = (run) => values.profile ? withProfile(run, io.err) : run();
5671
5874
  const dir = values.dir ?? process.cwd();
5672
5875
  if (values["dry-run"] !== void 0 && action !== "teardown") throw new Error("--dry-run applies only to teardown.");
5673
5876
  if (action === "teardown") {
@@ -5680,14 +5883,20 @@ async function runAutomationCli(argv2, stdin = "", io = {
5680
5883
  if (action === "hook") {
5681
5884
  const host = hostSchema.parse(values.host);
5682
5885
  if (options.managedHost && options.managedHost !== host) throw new Error("Managed hook --host does not match its launcher.");
5683
- const output = await runAutomationHook(host, stdin, { managed: !!options.managedHost });
5886
+ const output = await measured(() => runAutomationHook(host, stdin, { managed: !!options.managedHost }));
5684
5887
  if (output) io.out(JSON.stringify(output));
5685
5888
  return 0;
5686
5889
  }
5687
5890
  if (action === "setup") {
5688
5891
  if (values.command) throw new Error("--command applies to install/config, not managed setup.");
5689
5892
  const { setupProject: setupProject2, summarizeSetup: summarizeSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
5690
- const result = await setupProject2(dir, { host: values.host ? hostSchema.parse(values.host) : void 0 });
5893
+ const host = values.host ? hostSchema.parse(values.host) : void 0;
5894
+ if (!values.json) progress = createProgress(io === defaultIo ? {} : {
5895
+ write: (text2) => io.err(text2.replace(/\n$/, "")),
5896
+ interactive: false
5897
+ });
5898
+ const result = await setupProject2(dir, { host, progress });
5899
+ progress?.stop();
5691
5900
  io.out(values.json ? JSON.stringify(result, null, 2) : summarizeSetup2(result));
5692
5901
  return 0;
5693
5902
  }
@@ -5705,10 +5914,11 @@ async function runAutomationCli(argv2, stdin = "", io = {
5705
5914
  return 0;
5706
5915
  }
5707
5916
  if (action !== "check") throw new Error("Unknown automation command: " + action);
5708
- const { report } = await automate(dir, { event: "task_end" });
5917
+ const { report } = await measured(() => automate(dir, { event: "task_end" }));
5709
5918
  io.out(values.json ? JSON.stringify(report, null, 2) : summarize(report));
5710
5919
  return report.status === "verified" ? 0 : report.status === "issues-remain" ? 1 : 2;
5711
5920
  } catch (error) {
5921
+ progress?.stop(false);
5712
5922
  const message2 = action === "setup" ? "Mason setup incomplete: " + automationFailure(error).message + ". Rerun the same setup command to resume; retained audit evidence is preserved." : action === "teardown" ? "Mason teardown incomplete: " + automationFailure(error).message + ". Inspect the reported input, then rerun teardown; knowledge and repair evidence are retained." : failureMessage(error);
5713
5923
  if (action === "hook" || isHookCommand(argv2)) {
5714
5924
  io.out(JSON.stringify({ systemMessage: message2 }));