@mutmutco/cli 3.97.0 → 3.99.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.cjs +1108 -528
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -3416,8 +3416,8 @@ var program = new Command();
3416
3416
 
3417
3417
  // src/index.ts
3418
3418
  var import_promises11 = require("node:fs/promises");
3419
- var import_node_fs39 = require("node:fs");
3420
- var import_node_child_process18 = require("node:child_process");
3419
+ var import_node_fs40 = require("node:fs");
3420
+ var import_node_child_process19 = require("node:child_process");
3421
3421
 
3422
3422
  // src/cli-shared.ts
3423
3423
  var import_node_child_process3 = require("node:child_process");
@@ -5779,6 +5779,7 @@ function buildGcPlan(inputs) {
5779
5779
  const preservedBranches2 = new Set(inputs.preservedBranches ?? []);
5780
5780
  const preservedWorktrees = new Set((inputs.worktrees ?? []).filter((w) => w.preserved).map((w) => w.branch));
5781
5781
  const mergedIntoBase = new Set((inputs.mergedIntoBase ?? []).map((b) => b.trim()).filter(Boolean));
5782
+ const prLookupFailures = new Map((inputs.prLookupFailures ?? []).map((f) => [f.branch.trim(), f.detail]));
5782
5783
  const skipped = [];
5783
5784
  const branches = [];
5784
5785
  const skipTrackingBranches = /* @__PURE__ */ new Set();
@@ -5794,6 +5795,11 @@ function buildGcPlan(inputs) {
5794
5795
  skipped.push({ branch, reason: "protected" });
5795
5796
  continue;
5796
5797
  }
5798
+ if (prLookupFailures.has(branch)) {
5799
+ skipped.push({ branch, reason: "pr-lookup-failed", detail: prLookupFailures.get(branch) });
5800
+ skipTrackingBranches.add(branch);
5801
+ continue;
5802
+ }
5797
5803
  const prSet = prs.get(branch);
5798
5804
  if (prSet?.some((pr2) => pr2.state === "OPEN")) {
5799
5805
  skipped.push({ branch, reason: "open-pr" });
@@ -5838,6 +5844,7 @@ function buildGcPlan(inputs) {
5838
5844
  const branch = branchForTrackingRef(ref, remote);
5839
5845
  if (!branch || protectedBranches.has(branch)) return null;
5840
5846
  if (branch === inputs.currentBranch || dirtyWorktrees.has(branch) || unpushedWorktrees.has(branch) || preservedBranches2.has(branch) || preservedWorktrees.has(branch) || skipTrackingBranches.has(branch)) return null;
5847
+ if (prLookupFailures.has(branch)) return null;
5841
5848
  const prSet = prs.get(branch);
5842
5849
  if (prSet?.some((pr2) => pr2.state === "OPEN")) return null;
5843
5850
  const state = closedState(prSet);
@@ -6999,7 +7006,7 @@ function commandLadderHint() {
6999
7006
  }
7000
7007
 
7001
7008
  // src/index.ts
7002
- var import_node_path37 = require("node:path");
7009
+ var import_node_path38 = require("node:path");
7003
7010
 
7004
7011
  // src/merge-ci-policy.ts
7005
7012
  function resolveMergeCiPolicy(input) {
@@ -7643,6 +7650,9 @@ var CMD_RE = /`mmi-cli ((?:[a-z][a-z-]*)(?: [a-z][a-z-]*){0,3})/g;
7643
7650
  var RETIRED_RE = /\b(retired|historical|removed|deleted|gone|no longer|superseded|legacy)\b/i;
7644
7651
  var ROOT_DOCS = ["README.md", "architecture.md"];
7645
7652
  var SKIP_WALK = ["docs/Archive/", "docs/incidents/", "docs/research/"];
7653
+ function refFirstSegment(ref) {
7654
+ return ref.replace(/^(\.\/)+/, "").split("/")[0];
7655
+ }
7646
7656
  function stripFences(markdown) {
7647
7657
  let inFence = false;
7648
7658
  return markdown.split(/\r?\n/).map((line) => {
@@ -7745,6 +7755,12 @@ function extractCommands(markdown) {
7745
7755
  }
7746
7756
  function checkRefs(root, deps, docs2) {
7747
7757
  const { exists, isIgnored = () => /* @__PURE__ */ new Set() } = deps;
7758
+ const allFirstSegments = /* @__PURE__ */ new Set();
7759
+ for (const markdown of Object.values(docs2)) {
7760
+ for (const { ref } of extractRefs(markdown)) allFirstSegments.add(refFirstSegment(ref));
7761
+ }
7762
+ const tracked = allFirstSegments.size ? deps.trackedFirstSegments?.([...allFirstSegments]) ?? null : null;
7763
+ const firstVerifiable = (first) => tracked ? tracked.has(first) : exists((0, import_node_path12.join)(root, first));
7748
7764
  const candidates = [];
7749
7765
  const direct = [];
7750
7766
  for (const [doc, markdown] of Object.entries(docs2)) {
@@ -7781,8 +7797,7 @@ function checkRefs(root, deps, docs2) {
7781
7797
  }
7782
7798
  }
7783
7799
  for (const { ref, line } of extractRefs(markdown)) {
7784
- const first = ref.split("/")[0];
7785
- if (!exists((0, import_node_path12.join)(root, first))) continue;
7800
+ if (!firstVerifiable(refFirstSegment(ref))) continue;
7786
7801
  if (!exists((0, import_node_path12.join)(root, ref))) candidates.push({ kind: "missing-path", doc, line, detail: ref });
7787
7802
  }
7788
7803
  for (const { target, line, resolved, missing } of links) {
@@ -7872,18 +7887,43 @@ function defaultIsIgnored(root, relPaths, exec = import_node_child_process5.exec
7872
7887
  throw error;
7873
7888
  }
7874
7889
  }
7890
+ function defaultTrackedFirstSegments(root, firstSegments, exec = import_node_child_process5.execFileSync) {
7891
+ if (firstSegments.length === 0) return /* @__PURE__ */ new Set();
7892
+ try {
7893
+ const out = exec("git", ["ls-files", "-z", "--", ...firstSegments.map((s) => `:(literal)${s}`)], {
7894
+ cwd: root,
7895
+ encoding: "utf8",
7896
+ maxBuffer: CHECK_IGNORE_MAX_BUFFER
7897
+ });
7898
+ const tracked = /* @__PURE__ */ new Set();
7899
+ for (const path2 of out.split("\0")) {
7900
+ if (path2) tracked.add(path2.split("/")[0]);
7901
+ }
7902
+ return tracked;
7903
+ } catch (error) {
7904
+ if (error?.status === 128) return null;
7905
+ if (error?.code === "ENOENT") return null;
7906
+ if (error?.code === "ENOBUFS") {
7907
+ throw new Error(
7908
+ `git ls-files produced more than ${CHECK_IGNORE_MAX_BUFFER} bytes for ${firstSegments.length} segment(s) \u2014 the tracked set cannot be read, and treating it as empty would silently skip every ref (#4208)`
7909
+ );
7910
+ }
7911
+ throw error;
7912
+ }
7913
+ }
7875
7914
  function runDocRefs(root, deps = {}) {
7876
7915
  const readFile9 = deps.readFile ?? readFileOrNull;
7877
7916
  const exists = deps.exists ?? import_node_fs13.existsSync;
7878
7917
  const listDocs = deps.listDocs ?? defaultListDocs;
7879
7918
  const isIgnored = deps.isIgnored ?? ((paths) => defaultIsIgnored(root, paths));
7919
+ const trackedFirstSegments = deps.trackedFirstSegments ?? ((segs) => defaultTrackedFirstSegments(root, segs));
7880
7920
  const commandPaths = deps.commandPaths ?? null;
7881
7921
  const walked = listDocs(root);
7882
7922
  const ignoredDocs = walked.length ? isIgnored(walked) : /* @__PURE__ */ new Set();
7883
7923
  const docs2 = Object.fromEntries(
7884
7924
  walked.filter((rel) => !ignoredDocs.has(rel)).map((rel) => [rel, readFile9((0, import_node_path12.join)(root, rel))]).filter(([, body]) => body != null)
7885
7925
  );
7886
- const refResult = checkRefs(root, { exists, isIgnored }, docs2);
7926
+ const refResult = checkRefs(root, { exists, isIgnored, trackedFirstSegments }, docs2);
7887
7927
  const findings = [
7888
7928
  ...checkPins(root, readFile9, docs2).findings,
7889
7929
  ...refResult.findings
@@ -9218,15 +9258,15 @@ function healJervCodePackageRegistration(opts = {}) {
9218
9258
  return { available: true, ok: true, changed: false, version: null, detail: "skipped \u2014 no installed MMI clone carries the pi wrapper (install the Claude plugin first)" };
9219
9259
  }
9220
9260
  const packagePath = `${clone.path.replace(/\\/g, "/").replace(/\/+$/, "")}/${JERVCODE_WRAPPER_DIR}`;
9221
- const settingsPath = (0, import_node_path14.join)(agentDir, "settings.json");
9222
- const settings = readPiSettings(settingsPath);
9261
+ const settingsPath2 = (0, import_node_path14.join)(agentDir, "settings.json");
9262
+ const settings = readPiSettings(settingsPath2);
9223
9263
  if (settings === null) {
9224
9264
  return {
9225
9265
  available: true,
9226
9266
  ok: false,
9227
9267
  changed: false,
9228
9268
  version: clone.version,
9229
- detail: `package NOT registered \u2014 ${settingsPath} is not a strict JSON object, so mmi left it alone; add ${packagePath} to its packages array by hand`
9269
+ detail: `package NOT registered \u2014 ${settingsPath2} is not a strict JSON object, so mmi left it alone; add ${packagePath} to its packages array by hand`
9230
9270
  };
9231
9271
  }
9232
9272
  const current = settings ?? {};
@@ -9237,12 +9277,12 @@ function healJervCodePackageRegistration(opts = {}) {
9237
9277
  }
9238
9278
  current.packages = merged.next;
9239
9279
  try {
9240
- (0, import_node_fs15.mkdirSync)((0, import_node_path14.dirname)(settingsPath), { recursive: true });
9241
- const tmp = `${settingsPath}.tmp-${process.pid}`;
9280
+ (0, import_node_fs15.mkdirSync)((0, import_node_path14.dirname)(settingsPath2), { recursive: true });
9281
+ const tmp = `${settingsPath2}.tmp-${process.pid}`;
9242
9282
  (0, import_node_fs15.writeFileSync)(tmp, `${JSON.stringify(current, null, 2)}
9243
9283
  `, "utf8");
9244
9284
  try {
9245
- (0, import_node_fs15.renameSync)(tmp, settingsPath);
9285
+ (0, import_node_fs15.renameSync)(tmp, settingsPath2);
9246
9286
  } catch (renameError) {
9247
9287
  (0, import_node_fs15.rmSync)(tmp, { force: true });
9248
9288
  throw renameError;
@@ -12011,6 +12051,12 @@ async function auditRepoCi(repo, deps) {
12011
12051
  label: "delete_branch_on_merge enabled",
12012
12052
  detail: info.delete_branch_on_merge === true ? void 0 : "false or unavailable"
12013
12053
  });
12054
+ checks.push({
12055
+ ok: info.has_wiki === false,
12056
+ label: "has_wiki disabled",
12057
+ detail: info.has_wiki === false ? void 0 : "wikis are retired org-wide",
12058
+ remediation: `gh api -X PATCH repos/${repo} -F has_wiki=false`
12059
+ });
12014
12060
  const hasCanonicalGateWorkflow = repoClass === "hub" ? true : repoClass === "content" ? true : await contentExists(deps, repo, baseBranch, PRODUCT_GATE_PATH);
12015
12061
  let prWorkflowPaths = repoClass === "deployable" && hasCanonicalGateWorkflow ? [PRODUCT_GATE_PATH] : [];
12016
12062
  if (repoClass === "deployable" && !hasCanonicalGateWorkflow) {
@@ -12269,7 +12315,7 @@ async function applyCiReconcileMergeSettingsFromReport(repo, deps, report) {
12269
12315
  const applied = [];
12270
12316
  const skipped = [];
12271
12317
  const errors = [];
12272
- const mergeChecks = report.checks.filter((c) => c.label.startsWith("allow_") || c.label.startsWith("delete_branch"));
12318
+ const mergeChecks = report.checks.filter((c) => c.label.startsWith("allow_") || c.label.startsWith("delete_branch") || c.label.startsWith("has_wiki"));
12273
12319
  const needsPatch = mergeChecks.some((c) => !c.ok);
12274
12320
  if (!needsPatch) {
12275
12321
  skipped.push("merge settings already canonical");
@@ -12280,10 +12326,11 @@ async function applyCiReconcileMergeSettingsFromReport(repo, deps, report) {
12280
12326
  body: {
12281
12327
  allow_auto_merge: true,
12282
12328
  allow_squash_merge: true,
12283
- delete_branch_on_merge: true
12329
+ delete_branch_on_merge: true,
12330
+ has_wiki: false
12284
12331
  }
12285
12332
  });
12286
- applied.push("allow_auto_merge, allow_squash_merge, delete_branch_on_merge");
12333
+ applied.push("allow_auto_merge, allow_squash_merge, delete_branch_on_merge, has_wiki=false");
12287
12334
  } catch (e) {
12288
12335
  errors.push(e.message);
12289
12336
  }
@@ -12466,7 +12513,7 @@ async function parkProductRuleset(repo, deps) {
12466
12513
  return result;
12467
12514
  }
12468
12515
  function reconcileOwnedFailures(report) {
12469
- return report.checks.filter((check) => !check.ok).filter((check) => check.label.startsWith("allow_") || check.label.startsWith("delete_branch") || check.label.startsWith("gate workflow committed on ") || check.label.startsWith("product ruleset reference committed on ") || check.label === "product required status checks active" || check.label === RULESET_REFERENCE_MATCH_LABEL || check.label === REQUIRED_CONTEXTS_EMITTED_LABEL || check.label === TAG_ADDRESSABLE_CONTEXTS_LABEL).map((check) => check.label);
12516
+ return report.checks.filter((check) => !check.ok).filter((check) => check.label.startsWith("allow_") || check.label.startsWith("delete_branch") || check.label.startsWith("has_wiki") || check.label.startsWith("gate workflow committed on ") || check.label.startsWith("product ruleset reference committed on ") || check.label === "product required status checks active" || check.label === RULESET_REFERENCE_MATCH_LABEL || check.label === REQUIRED_CONTEXTS_EMITTED_LABEL || check.label === TAG_ADDRESSABLE_CONTEXTS_LABEL).map((check) => check.label);
12470
12517
  }
12471
12518
  async function finalizeCiReconcile(repo, deps, result, before, pendingReason) {
12472
12519
  if (result.errors.length > 0) {
@@ -13757,23 +13804,6 @@ async function fetchSchedulesList(deps) {
13757
13804
  return null;
13758
13805
  }
13759
13806
  }
13760
- async function fetchDocsAuditList(deps) {
13761
- if (!deps.baseUrl) return { notArmed: true };
13762
- try {
13763
- const token = await deps.token();
13764
- if (!token) return { notArmed: true };
13765
- const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/docs-audit/list`, {
13766
- method: "GET",
13767
- headers: { Authorization: `Bearer ${token}` }
13768
- });
13769
- if (res.status === 404) return { notArmed: true };
13770
- if (!res.ok) return { ok: false, error: `docs-audit list HTTP ${res.status}` };
13771
- const body = await res.json();
13772
- return { ok: true, rows: Array.isArray(body?.docsAudits) ? body.docsAudits : [] };
13773
- } catch (e) {
13774
- return { ok: false, error: e.message };
13775
- }
13776
- }
13777
13807
  async function fetchOrgConfig(deps) {
13778
13808
  if (!deps.baseUrl) return null;
13779
13809
  const token = await deps.token();
@@ -13824,9 +13854,6 @@ async function retireProject(slug, deps) {
13824
13854
  async function setDeployCoords(slug, payload, deps) {
13825
13855
  return postJson(`/projects/${encodeURIComponent(slug)}/deploy`, payload, deps);
13826
13856
  }
13827
- async function recordDocsAudit(verdict, deps) {
13828
- return postJson("/docs-audit/record", { ...verdict }, deps);
13829
- }
13830
13857
  async function postSchedulesLift(repo, schedules, deps) {
13831
13858
  const res = await postJson("/schedules/lift", { repo, schedules }, deps);
13832
13859
  if (!res.ok && res.status === 404) return { ...res, unreachable: "route-absent" };
@@ -15547,13 +15574,27 @@ async function isOrgRegisteredRepo(cfg, deps = {}) {
15547
15574
  if (!read.ok) return false;
15548
15575
  return read.project !== null;
15549
15576
  }
15550
- async function ghPrs(limit) {
15551
- const args = (state) => ["pr", "list", "--state", state, "--limit", String(limit), "--json", "number,headRefName,headRefOid,state"];
15552
- const [open2, closed] = await Promise.all([
15553
- execFileP2("gh", args("open"), { timeout: GC_GH_TIMEOUT_MS }),
15554
- execFileP2("gh", args("closed"), { timeout: GC_GH_TIMEOUT_MS })
15555
- ]);
15556
- return [...JSON.parse(open2.stdout || "[]"), ...JSON.parse(closed.stdout || "[]")];
15577
+ var GC_PR_LOOKUP_CONCURRENCY = 8;
15578
+ async function resolveBranchPrs(branches, limit) {
15579
+ const queue = [...new Set(branches.map((b) => b.trim()).filter(Boolean))];
15580
+ const prs = [];
15581
+ const failures = [];
15582
+ const args = (branch) => ["pr", "list", "--head", branch, "--state", "all", "--limit", String(limit), "--json", "number,headRefName,headRefOid,state"];
15583
+ let next = 0;
15584
+ const worker = async () => {
15585
+ for (let i = next++; i < queue.length; i = next++) {
15586
+ const branch = queue[i];
15587
+ try {
15588
+ const { stdout } = await execFileP2("gh", args(branch), { timeout: GC_GH_TIMEOUT_MS });
15589
+ const rows = JSON.parse(stdout || "[]");
15590
+ prs.push(...rows.filter((pr2) => pr2.headRefName?.trim() === branch));
15591
+ } catch (e) {
15592
+ failures.push({ branch, detail: e.message });
15593
+ }
15594
+ }
15595
+ };
15596
+ await Promise.all(Array.from({ length: Math.min(GC_PR_LOOKUP_CONCURRENCY, queue.length) }, worker));
15597
+ return { prs, failures };
15557
15598
  }
15558
15599
  async function localBranchHeads() {
15559
15600
  try {
@@ -15697,21 +15738,26 @@ function resolveExplicitScanRoot(explicitRoot, repoRoot2) {
15697
15738
  return explicitRepoWorktreesRoot(explicitRoot, repoRoot2, rootDirs);
15698
15739
  }
15699
15740
  async function gcPlan(remote, limit, opts = {}) {
15700
- const [branches, heads, current, stale, prs, worktrees, siblingDirs, preserved, mergedIntoBase] = await Promise.all([
15741
+ const [branches, heads, current, stale, worktrees, siblingDirs, preserved, mergedIntoBase] = await Promise.all([
15701
15742
  gitOut(["branch", "--format=%(refname:short)"]),
15702
15743
  localBranchHeads(),
15703
15744
  gitOut(["rev-parse", "--abbrev-ref", "HEAD"]),
15704
15745
  collectStaleTrackingRefs(remote, {
15705
15746
  execGit: (args) => execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })
15706
15747
  }),
15707
- ghPrs(limit),
15708
15748
  worktreeBranches(),
15709
15749
  siblingWorktreeDirs(opts.root),
15710
15750
  preservedBranches(),
15711
15751
  branchesMergedIntoBase(remote)
15712
15752
  ]);
15753
+ const localBranches = branches.split(/\r?\n/).map((b) => b.trim()).filter(Boolean);
15754
+ const { prs, failures } = await resolveBranchPrs(
15755
+ [...localBranches, ...stale.map((ref) => branchForTrackingRef(ref, remote)).filter((b) => Boolean(b))].filter((b) => !isProtectedBranch(b)),
15756
+ limit
15757
+ );
15713
15758
  return buildGcPlan({
15714
- localBranches: branches.split(/\r?\n/).map((b) => b.trim()).filter(Boolean),
15759
+ localBranches,
15760
+ prLookupFailures: failures,
15715
15761
  localBranchHeads: heads,
15716
15762
  currentBranch: current,
15717
15763
  siblingWorktreeDirs: siblingDirs,
@@ -17813,8 +17859,6 @@ function consolidateCommandNamespaces(program3) {
17813
17859
  move(program3, plugin, "plugin-heal", "heal");
17814
17860
  move(program3, plugin, "plugin-prune", "prune");
17815
17861
  move(program3, plugin, "session-start");
17816
- const docs2 = child(program3, "docs");
17817
- move(program3, docs2, "docs-audit", "audit");
17818
17862
  const train = child(program3, "train");
17819
17863
  const fullTrack2 = child(program3, "full-track");
17820
17864
  move(fullTrack2, train, "readiness");
@@ -17825,18 +17869,25 @@ function consolidateCommandNamespaces(program3) {
17825
17869
  move(program3, stage, "port-range");
17826
17870
  }
17827
17871
 
17872
+ // src/pi-plugin-registration.ts
17873
+ var import_node_fs22 = require("node:fs");
17874
+ var import_node_path20 = require("node:path");
17875
+
17828
17876
  // src/plugin-cache-prune.ts
17829
17877
  var PLUGIN_CACHE_KEEP = 2;
17830
17878
  var VERSION_DIR = /^\d+\.\d+\.\d+(?:[-+].*)?$/;
17831
17879
  function isVersionDirName(name) {
17832
17880
  return VERSION_DIR.test(name);
17833
17881
  }
17834
- function selectPrunablePluginVersions(names, currentVersion) {
17882
+ function selectPrunablePluginVersions(names, currentVersion, installedVersion) {
17835
17883
  const versions = [...new Set(names)].filter(isVersionDirName);
17836
17884
  if (versions.length <= PLUGIN_CACHE_KEEP) return [];
17885
+ if (!currentVersion && !installedVersion) return [];
17837
17886
  const newestFirst = [...versions].sort((a, b) => compareVersions(b, a));
17838
17887
  const keep = /* @__PURE__ */ new Set();
17888
+ keep.add(newestFirst[0]);
17839
17889
  if (currentVersion && versions.includes(currentVersion)) keep.add(currentVersion);
17890
+ if (installedVersion && versions.includes(installedVersion)) keep.add(installedVersion);
17840
17891
  for (const v of newestFirst) {
17841
17892
  if (keep.size >= PLUGIN_CACHE_KEEP) break;
17842
17893
  keep.add(v);
@@ -17964,7 +18015,7 @@ function buildPluginCachePlan(home, running, deps, opts = {}) {
17964
18015
  return absent;
17965
18016
  }
17966
18017
  const versions = names.filter(isVersionDirName);
17967
- const prune = selectPrunablePluginVersions(versions, running);
18018
+ const prune = selectPrunablePluginVersions(versions, running, opts.installedVersion);
17968
18019
  const pruneSet = new Set(prune);
17969
18020
  const keep = [...versions].sort((a, b) => compareVersions(b, a)).filter((v) => !pruneSet.has(v));
17970
18021
  const bytes = opts.withBytes ? prune.reduce((sum, v) => sum + deps.dirBytes(`${cacheRoot}/${v}`), 0) : 0;
@@ -18063,6 +18114,68 @@ function renderPluginCachePlan(plan, applied) {
18063
18114
  return lines.join("\n");
18064
18115
  }
18065
18116
 
18117
+ // src/pi-plugin-registration.ts
18118
+ function isMmiPiPackage(entry) {
18119
+ return /[\\/]mutmutco[\\/]mmi[\\/][^\\/]+[\\/]\.pi-plugin\/?$/.test(entry);
18120
+ }
18121
+ function expectedPiPluginPath(home, env, installedVersion) {
18122
+ const root = env.CLAUDE_PLUGIN_ROOT?.trim();
18123
+ if (root && /[\\/]mutmutco[\\/]mmi[\\/]/.test(root)) return (0, import_node_path20.join)(root, ".pi-plugin");
18124
+ const version = installedVersion ?? runningPluginVersion(env);
18125
+ if (!version) return void 0;
18126
+ return (0, import_node_path20.join)(home, ".claude", "plugins", "cache", "mutmutco", "mmi", version, ".pi-plugin");
18127
+ }
18128
+ function settingsPath(home) {
18129
+ return (0, import_node_path20.join)(home, ".pi", "agent", "settings.json");
18130
+ }
18131
+ function readPiPluginState(home, env, installedVersion) {
18132
+ if (!(0, import_node_fs22.existsSync)((0, import_node_path20.join)(home, ".pi", "agent"))) return void 0;
18133
+ const expectedPath = expectedPiPluginPath(home, env, installedVersion);
18134
+ if (!expectedPath || !(0, import_node_fs22.existsSync)(expectedPath)) return void 0;
18135
+ const file = settingsPath(home);
18136
+ if (!(0, import_node_fs22.existsSync)(file)) return { expectedPath, settingsReadable: true };
18137
+ try {
18138
+ const parsed = JSON.parse((0, import_node_fs22.readFileSync)(file, "utf8"));
18139
+ const packages = Array.isArray(parsed.packages) ? parsed.packages : [];
18140
+ return { expectedPath, registeredPath: packages.find((p) => typeof p === "string" && isMmiPiPackage(p)), settingsReadable: true };
18141
+ } catch {
18142
+ return { expectedPath, settingsReadable: false };
18143
+ }
18144
+ }
18145
+ function healPiPluginRegistration(home, env, installedVersion) {
18146
+ const state = readPiPluginState(home, env, installedVersion);
18147
+ if (!state) return { ok: false, detail: "no ~/.pi/agent or no installed .pi-plugin payload" };
18148
+ if (!state.settingsReadable) return { ok: false, detail: "settings.json unreadable \u2014 nothing written (fail closed)" };
18149
+ const file = settingsPath(home);
18150
+ try {
18151
+ const parsed = (0, import_node_fs22.existsSync)(file) ? JSON.parse((0, import_node_fs22.readFileSync)(file, "utf8")) : {};
18152
+ const packages = Array.isArray(parsed.packages) ? parsed.packages : [];
18153
+ const next = packages.filter((p) => !(typeof p === "string" && isMmiPiPackage(p)));
18154
+ next.push(state.expectedPath);
18155
+ (0, import_node_fs22.writeFileSync)(file, `${JSON.stringify({ ...parsed, packages: next }, null, 2)}
18156
+ `);
18157
+ return { ok: true, detail: state.registeredPath ? `replaced ${state.registeredPath}` : `registered ${state.expectedPath}` };
18158
+ } catch (e) {
18159
+ return { ok: false, detail: e.message };
18160
+ }
18161
+ }
18162
+
18163
+ // src/statusline-invalidate.ts
18164
+ var import_node_child_process10 = require("node:child_process");
18165
+ function invalidateStatuslineBoardCache() {
18166
+ try {
18167
+ const child2 = (0, import_node_child_process10.spawn)("jerv-cli", ["lane", "ops", "invalidate-board"], {
18168
+ detached: true,
18169
+ stdio: "ignore",
18170
+ windowsHide: true
18171
+ });
18172
+ child2.on("error", () => {
18173
+ });
18174
+ child2.unref();
18175
+ } catch {
18176
+ }
18177
+ }
18178
+
18066
18179
  // src/skill-lesson.ts
18067
18180
  var SKILL_LESSON_LABEL = "skill-lesson";
18068
18181
  var SKILL_NAMES = ["bootstrap", "browser-automation", "doctor", "epic", "hotfix", "mmi", "onboard", "rcand", "release", "resume", "secrets", "stage", "worktree"];
@@ -18302,12 +18415,12 @@ function renderVerifyBroker(input) {
18302
18415
  }
18303
18416
 
18304
18417
  // src/hotfix-coverage.ts
18305
- var import_node_child_process10 = require("node:child_process");
18418
+ var import_node_child_process11 = require("node:child_process");
18306
18419
  var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
18307
18420
  function checkHotfixCoverage(options = {}) {
18308
18421
  const { cwd = process.cwd(), mainRef = "origin/main", rcRef = "origin/rc", manifestPaths = [] } = options;
18309
18422
  const ack = (options.ack ?? []).filter(Boolean);
18310
- const git2 = options.git ?? ((args, opts) => (0, import_node_child_process10.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
18423
+ const git2 = options.git ?? ((args, opts) => (0, import_node_child_process11.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
18311
18424
  const revList = (range) => {
18312
18425
  const out = git2(["rev-list", "--no-merges", range]).trim();
18313
18426
  return out ? out.split("\n") : [];
@@ -18375,7 +18488,7 @@ function checkHotfixCoverage(options = {}) {
18375
18488
  }
18376
18489
  function checkHotfixCarries(options) {
18377
18490
  const { cwd = process.cwd(), branch, baseRef, targets } = options;
18378
- const git2 = options.git ?? ((args, opts) => (0, import_node_child_process10.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
18491
+ const git2 = options.git ?? ((args, opts) => (0, import_node_child_process11.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
18379
18492
  const isAncestor = (sha, ref) => {
18380
18493
  try {
18381
18494
  git2(["merge-base", "--is-ancestor", sha, ref]);
@@ -19489,9 +19602,9 @@ function renderAccessReport(report) {
19489
19602
 
19490
19603
  // src/repo-index.ts
19491
19604
  var import_node_crypto4 = require("node:crypto");
19492
- var import_node_child_process11 = require("node:child_process");
19493
- var import_node_fs22 = require("node:fs");
19494
- var import_node_path20 = require("node:path");
19605
+ var import_node_child_process12 = require("node:child_process");
19606
+ var import_node_fs23 = require("node:fs");
19607
+ var import_node_path21 = require("node:path");
19495
19608
  var REPO_INDEX_SCHEMA = 1;
19496
19609
  var HARD_DENY = [
19497
19610
  /(^|\/)\.env(\.|$)/i,
@@ -19616,11 +19729,11 @@ function loadReadmeHints(cwd, candidatePaths) {
19616
19729
  }
19617
19730
  for (const rel of readmes) {
19618
19731
  if (isHardDeniedPath(rel)) continue;
19619
- const abs = (0, import_node_path20.join)(cwd, ...rel.split("/"));
19620
- if (!(0, import_node_fs22.existsSync)(abs)) continue;
19732
+ const abs = (0, import_node_path21.join)(cwd, ...rel.split("/"));
19733
+ if (!(0, import_node_fs23.existsSync)(abs)) continue;
19621
19734
  let text;
19622
19735
  try {
19623
- text = (0, import_node_fs22.readFileSync)(abs, "utf8");
19736
+ text = (0, import_node_fs23.readFileSync)(abs, "utf8");
19624
19737
  } catch {
19625
19738
  continue;
19626
19739
  }
@@ -19633,9 +19746,9 @@ function loadReadmeHints(cwd, candidatePaths) {
19633
19746
  return hints;
19634
19747
  }
19635
19748
  function toPosix(p) {
19636
- return p.split(import_node_path20.sep).join("/");
19749
+ return p.split(import_node_path21.sep).join("/");
19637
19750
  }
19638
- function listCandidatePaths(cwd, exec = import_node_child_process11.execFileSync) {
19751
+ function listCandidatePaths(cwd, exec = import_node_child_process12.execFileSync) {
19639
19752
  try {
19640
19753
  const out = exec("git", ["ls-files", "-z", "-c", "-o", "--exclude-standard"], {
19641
19754
  cwd,
@@ -19655,11 +19768,11 @@ function rebuildRepoIndex(cwd, repoSlug2) {
19655
19768
  for (const rel of candidates) {
19656
19769
  if (ignored.has(rel)) continue;
19657
19770
  if (isHardDeniedPath(rel)) continue;
19658
- const abs = (0, import_node_path20.join)(cwd, ...rel.split("/"));
19659
- if (!(0, import_node_fs22.existsSync)(abs)) continue;
19771
+ const abs = (0, import_node_path21.join)(cwd, ...rel.split("/"));
19772
+ if (!(0, import_node_fs23.existsSync)(abs)) continue;
19660
19773
  let text;
19661
19774
  try {
19662
- text = (0, import_node_fs22.readFileSync)(abs, "utf8");
19775
+ text = (0, import_node_fs23.readFileSync)(abs, "utf8");
19663
19776
  } catch {
19664
19777
  continue;
19665
19778
  }
@@ -19684,16 +19797,16 @@ function rebuildRepoIndex(cwd, repoSlug2) {
19684
19797
  entries
19685
19798
  };
19686
19799
  const store = repoIndexStorePath(cwd);
19687
- (0, import_node_fs22.mkdirSync)((0, import_node_path20.dirname)(store), { recursive: true });
19688
- (0, import_node_fs22.writeFileSync)(store, `${JSON.stringify(projection, null, 2)}
19800
+ (0, import_node_fs23.mkdirSync)((0, import_node_path21.dirname)(store), { recursive: true });
19801
+ (0, import_node_fs23.writeFileSync)(store, `${JSON.stringify(projection, null, 2)}
19689
19802
  `, "utf8");
19690
19803
  return projection;
19691
19804
  }
19692
19805
  function loadRepoIndex(cwd) {
19693
19806
  const store = repoIndexStorePath(cwd);
19694
- if (!(0, import_node_fs22.existsSync)(store)) return null;
19807
+ if (!(0, import_node_fs23.existsSync)(store)) return null;
19695
19808
  try {
19696
- const raw = JSON.parse((0, import_node_fs22.readFileSync)(store, "utf8"));
19809
+ const raw = JSON.parse((0, import_node_fs23.readFileSync)(store, "utf8"));
19697
19810
  if (raw?.schema !== REPO_INDEX_SCHEMA || !Array.isArray(raw.entries)) return null;
19698
19811
  return raw;
19699
19812
  } catch {
@@ -19758,14 +19871,14 @@ function searchRepoIndex(idx, query, limit = 20) {
19758
19871
  }
19759
19872
  return out;
19760
19873
  }
19761
- function inferRepoSlug(cwd, exec = import_node_child_process11.execFileSync) {
19874
+ function inferRepoSlug(cwd, exec = import_node_child_process12.execFileSync) {
19762
19875
  try {
19763
19876
  const url = String(exec("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" })).trim();
19764
19877
  const m = /[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(url);
19765
19878
  if (m?.[1]) return m[1].toLowerCase();
19766
19879
  } catch {
19767
19880
  }
19768
- return ((0, import_node_path20.basename)(cwd) || "local").toLowerCase();
19881
+ return ((0, import_node_path21.basename)(cwd) || "local").toLowerCase();
19769
19882
  }
19770
19883
 
19771
19884
  // src/repo-index-cloud-client.ts
@@ -19905,10 +20018,10 @@ async function gcRepoIndexCloud(deps) {
19905
20018
  }
19906
20019
 
19907
20020
  // src/repo-index-sync.ts
19908
- var import_node_fs23 = require("node:fs");
20021
+ var import_node_fs24 = require("node:fs");
19909
20022
  var import_node_os9 = require("node:os");
19910
- var import_node_path21 = require("node:path");
19911
- var import_node_child_process12 = require("node:child_process");
20023
+ var import_node_path22 = require("node:path");
20024
+ var import_node_child_process13 = require("node:child_process");
19912
20025
  var MAX_EMBED_BACKFILL_ROUNDS = 40;
19913
20026
  function normalizeRepo(raw) {
19914
20027
  const t = raw.trim().replace(/\.git$/, "");
@@ -19928,7 +20041,7 @@ function rosterRepos(projects) {
19928
20041
  }
19929
20042
  function shallowClone(repo, dest, token) {
19930
20043
  const basic = Buffer.from(`x-access-token:${token}`, "utf8").toString("base64");
19931
- (0, import_node_child_process12.execFileSync)(
20044
+ (0, import_node_child_process13.execFileSync)(
19932
20045
  "git",
19933
20046
  ["-c", `http.extraHeader=Authorization: Basic ${basic}`, "clone", "--depth", "1", "--single-branch", `https://github.com/${repo}.git`, dest],
19934
20047
  { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }
@@ -19951,7 +20064,7 @@ async function syncEstateRepoIndex(opts) {
19951
20064
  const failed = [];
19952
20065
  const skipped = [];
19953
20066
  for (const repo of repos) {
19954
- const dir = (0, import_node_fs23.mkdtempSync)((0, import_node_path21.join)((0, import_node_os9.tmpdir)(), "mmi-repo-index-"));
20067
+ const dir = (0, import_node_fs24.mkdtempSync)((0, import_node_path22.join)((0, import_node_os9.tmpdir)(), "mmi-repo-index-"));
19955
20068
  try {
19956
20069
  shallowClone(repo, dir, opts.githubToken);
19957
20070
  const built = rebuildRepoIndex(dir, repo);
@@ -20001,7 +20114,7 @@ async function syncEstateRepoIndex(opts) {
20001
20114
  failed.push({ repo, error: e.message });
20002
20115
  } finally {
20003
20116
  try {
20004
- (0, import_node_fs23.rmSync)(dir, { recursive: true, force: true });
20117
+ (0, import_node_fs24.rmSync)(dir, { recursive: true, force: true });
20005
20118
  } catch {
20006
20119
  }
20007
20120
  }
@@ -20010,7 +20123,7 @@ async function syncEstateRepoIndex(opts) {
20010
20123
  }
20011
20124
 
20012
20125
  // src/repo-index-health.ts
20013
- var import_node_fs24 = require("node:fs");
20126
+ var import_node_fs25 = require("node:fs");
20014
20127
 
20015
20128
  // testdata/repo-index-golden-queries.json
20016
20129
  var repo_index_golden_queries_default = {
@@ -20052,7 +20165,7 @@ function assertGoldenSuite(raw, source) {
20052
20165
  function loadGoldenSuite(path2) {
20053
20166
  let text;
20054
20167
  try {
20055
- text = (0, import_node_fs24.readFileSync)(path2, "utf8");
20168
+ text = (0, import_node_fs25.readFileSync)(path2, "utf8");
20056
20169
  } catch (e) {
20057
20170
  throw new Error(`golden suite unreadable at ${path2}: ${e.message}`);
20058
20171
  }
@@ -20195,9 +20308,9 @@ async function runRepoIndexHealth(opts) {
20195
20308
  }
20196
20309
 
20197
20310
  // src/spawn-policy-core.ts
20198
- var import_node_child_process13 = require("node:child_process");
20199
- var import_node_fs25 = require("node:fs");
20200
- var import_node_path22 = require("node:path");
20311
+ var import_node_child_process14 = require("node:child_process");
20312
+ var import_node_fs26 = require("node:fs");
20313
+ var import_node_path23 = require("node:path");
20201
20314
  var SPAWNERS = ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync"];
20202
20315
  var CALL_SOURCE = String.raw`(^|[^.\w$])(${SPAWNERS.join("|")})\s*\(`;
20203
20316
  var SOURCE_EXT = /\.(ts|mts|cts|js|mjs|cjs)$/;
@@ -20266,7 +20379,7 @@ function findViolationsInSource(raw) {
20266
20379
  return found;
20267
20380
  }
20268
20381
  function policedFiles(root) {
20269
- const r = (0, import_node_child_process13.spawnSync)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
20382
+ const r = (0, import_node_child_process14.spawnSync)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
20270
20383
  cwd: root,
20271
20384
  encoding: "utf8",
20272
20385
  windowsHide: true,
@@ -20283,7 +20396,7 @@ function runSpawnPolicy(root) {
20283
20396
  for (const file of files) {
20284
20397
  let raw;
20285
20398
  try {
20286
- raw = (0, import_node_fs25.readFileSync)((0, import_node_path22.join)(root, file), "utf8");
20399
+ raw = (0, import_node_fs26.readFileSync)((0, import_node_path23.join)(root, file), "utf8");
20287
20400
  } catch {
20288
20401
  continue;
20289
20402
  }
@@ -20300,9 +20413,9 @@ function runSpawnPolicy(root) {
20300
20413
  }
20301
20414
 
20302
20415
  // src/test-policy-core.ts
20303
- var import_node_child_process14 = require("node:child_process");
20304
- var import_node_fs26 = require("node:fs");
20305
- var import_node_path23 = require("node:path");
20416
+ var import_node_child_process15 = require("node:child_process");
20417
+ var import_node_fs27 = require("node:fs");
20418
+ var import_node_path24 = require("node:path");
20306
20419
  var POLICY_FILE = "test-policy.json";
20307
20420
  var TEST_RE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
20308
20421
  var PY_TEST_RE = /(?:^|\/)test_[^/]*\.py$|_test\.py$/;
@@ -20355,7 +20468,7 @@ function isTestPath(path2) {
20355
20468
  return TEST_RE.test(path2) || PY_TEST_RE.test(path2);
20356
20469
  }
20357
20470
  function loadPolicy(root, readFile9 = readFileOrNull2) {
20358
- const raw = readFile9((0, import_node_path23.join)(root, POLICY_FILE));
20471
+ const raw = readFile9((0, import_node_path24.join)(root, POLICY_FILE));
20359
20472
  if (raw == null) return { mandatory: [], declared: false };
20360
20473
  try {
20361
20474
  return { ...JSON.parse(raw), declared: true };
@@ -20365,7 +20478,7 @@ function loadPolicy(root, readFile9 = readFileOrNull2) {
20365
20478
  }
20366
20479
  function readFileOrNull2(path2) {
20367
20480
  try {
20368
- return (0, import_node_fs26.readFileSync)(path2, "utf8");
20481
+ return (0, import_node_fs27.readFileSync)(path2, "utf8");
20369
20482
  } catch {
20370
20483
  return null;
20371
20484
  }
@@ -20392,12 +20505,12 @@ function classify(changed, policy, present = () => false) {
20392
20505
  const removedProtected = [...removed].filter((p) => protectedBy.has(p)).map((p) => ({ path: p, why: protectedBy.get(p) ?? "" }));
20393
20506
  return { mandatoryHits, untestedHits, testChanges, addedTests, removedProtected };
20394
20507
  }
20395
- function unresolvedProtectedEntries(policy, root, exists = (path2) => (0, import_node_fs26.existsSync)(path2)) {
20396
- return (policy.protected ?? []).map((p) => p.path).filter((p) => !exists((0, import_node_path23.join)(root, p)));
20508
+ function unresolvedProtectedEntries(policy, root, exists = (path2) => (0, import_node_fs27.existsSync)(path2)) {
20509
+ return (policy.protected ?? []).map((p) => p.path).filter((p) => !exists((0, import_node_path24.join)(root, p)));
20397
20510
  }
20398
- function unresolvedSatisfiers(policy, root, exists = (path2) => (0, import_node_fs26.existsSync)(path2)) {
20511
+ function unresolvedSatisfiers(policy, root, exists = (path2) => (0, import_node_fs27.existsSync)(path2)) {
20399
20512
  const declared = (policy.mandatory ?? []).flatMap((m) => m.satisfiedBy ?? []);
20400
- return [...new Set(declared)].filter((p) => !exists((0, import_node_path23.join)(root, p)));
20513
+ return [...new Set(declared)].filter((p) => !exists((0, import_node_path24.join)(root, p)));
20401
20514
  }
20402
20515
  function evaluate(changed, policy, present = () => false) {
20403
20516
  const { mandatoryHits, untestedHits, testChanges, addedTests, removedProtected } = classify(changed, policy, present);
@@ -20430,14 +20543,14 @@ function evaluate(changed, policy, present = () => false) {
20430
20543
  return findings;
20431
20544
  }
20432
20545
  function git(args, cwd) {
20433
- return (0, import_node_child_process14.execFileSync)("git", args, { windowsHide: true, cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
20546
+ return (0, import_node_child_process15.execFileSync)("git", args, { windowsHide: true, cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
20434
20547
  }
20435
20548
  var COAUTHOR_KEY = "Co-authored-by";
20436
20549
  var GH_MESSAGE_SEPARATOR = /^-{5,}$/;
20437
20550
  var LIFTED_KEYS = new RegExp(`^(?:${TRAILER_KEY}|${COAUTHOR_KEY}):`, "i");
20438
20551
  function parseTrailers(message, cwd) {
20439
20552
  try {
20440
- return (0, import_node_child_process14.execFileSync)("git", ["interpret-trailers", "--parse", "--unfold"], {
20553
+ return (0, import_node_child_process15.execFileSync)("git", ["interpret-trailers", "--parse", "--unfold"], {
20441
20554
  windowsHide: true,
20442
20555
  cwd,
20443
20556
  input: message,
@@ -20579,13 +20692,13 @@ function changedFilesSince(base, cwd) {
20579
20692
  }
20580
20693
  function runTestPolicy(root, deps = {}) {
20581
20694
  const policy = deps.policy ?? loadPolicy(root);
20582
- const exists = deps.exists ?? ((path2) => (0, import_node_fs26.existsSync)(path2));
20695
+ const exists = deps.exists ?? ((path2) => (0, import_node_fs27.existsSync)(path2));
20583
20696
  const counts = { mandatoryCount: (policy.mandatory ?? []).length, protectedCount: (policy.protected ?? []).length };
20584
20697
  const base = deps.changed ? "(injected)" : resolveBase(root, deps.base);
20585
20698
  const refusal = deps.changed ? null : untrustworthyRange(root, base);
20586
20699
  const changed = deps.changed ?? (refusal ? [] : changedFilesSince(base, root));
20587
20700
  const lookup = deps.override !== void 0 ? { override: deps.override, refusals: [] } : !deps.changed && !refusal ? readOverride(base, root) : { override: null, refusals: [] };
20588
- const present = (path2) => exists((0, import_node_path23.join)(root, path2));
20701
+ const present = (path2) => exists((0, import_node_path24.join)(root, path2));
20589
20702
  const removedByThisDiff = removedPaths(changed);
20590
20703
  const staleFindings = [];
20591
20704
  const unresolved = unresolvedProtectedEntries(policy, root, exists).filter((p) => !removedByThisDiff.has(p));
@@ -20622,127 +20735,9 @@ function runTestPolicy(root, deps = {}) {
20622
20735
  return result;
20623
20736
  }
20624
20737
 
20625
- // src/docs-audit-command.ts
20626
- function serializeOutcome(outcome) {
20627
- switch (outcome.kind) {
20628
- case "clean":
20629
- return "clean";
20630
- case "refreshed":
20631
- return `refreshed-${outcome.count}`;
20632
- case "failed":
20633
- return "failed";
20634
- }
20635
- }
20636
- var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
20637
- function isValidIsoDate(date) {
20638
- if (typeof date !== "string" || !ISO_DATE_RE.test(date)) return false;
20639
- const parsed = Date.parse(`${date}T00:00:00Z`);
20640
- if (Number.isNaN(parsed)) return false;
20641
- return new Date(parsed).toISOString().slice(0, 10) === date;
20642
- }
20643
- var OUTCOME_WIRE_RE = /^(clean|refreshed-[1-9]\d*|failed)$/;
20644
- function isValidOutcomeWire(outcome) {
20645
- return typeof outcome === "string" && OUTCOME_WIRE_RE.test(outcome);
20646
- }
20647
- function docsAuditRecord(input) {
20648
- const repo = input.repo.trim();
20649
- const date = input.date.trim();
20650
- const shaRange = input.shaRange.trim();
20651
- const checkerVendor = input.checkerVendor.trim();
20652
- if (!repo) throw new Error("docs audit record: repo is required");
20653
- if (!date) throw new Error("docs audit record: date is required");
20654
- if (!isValidIsoDate(date)) throw new Error(`docs audit record: date must be a real ISO date (YYYY-MM-DD), got "${date}"`);
20655
- if (!shaRange) throw new Error("docs audit record: shaRange is required");
20656
- if (!checkerVendor) throw new Error("docs audit record: checkerVendor is required");
20657
- if (input.outcome.kind === "refreshed" && !(Number.isInteger(input.outcome.count) && input.outcome.count > 0)) {
20658
- throw new Error("docs audit record: a `refreshed` outcome must carry a positive integer count");
20659
- }
20660
- if (input.outcome.kind === "failed" && !input.outcome.reason.trim()) {
20661
- throw new Error("docs audit record: a `failed` outcome must carry a reason");
20662
- }
20663
- const outcome = serializeOutcome(input.outcome);
20664
- if (!isValidOutcomeWire(outcome)) {
20665
- throw new Error(`docs audit record: outcome serialized to an invalid wire form "${outcome}"`);
20666
- }
20667
- return { repo, date, shaRange, outcome, checkerVendor };
20668
- }
20669
- function ageInDays(verdictDate, today) {
20670
- const a = Date.parse(`${verdictDate}T00:00:00Z`);
20671
- const b = Date.parse(`${today}T00:00:00Z`);
20672
- return Math.max(0, Math.round((b - a) / 864e5));
20673
- }
20674
- function docsAuditStatus(fetch2, opts) {
20675
- if ("notArmed" in fetch2) {
20676
- return { ok: true, state: "not-armed", line: `docs audit: ${opts.repo} janitor not armed (registry route not live yet)` };
20677
- }
20678
- if (!fetch2.ok) {
20679
- return { ok: false, state: "error", line: `docs audit: ${opts.repo} registry read failed \u2014 ${fetch2.error}` };
20680
- }
20681
- if (!isValidIsoDate(opts.today)) {
20682
- throw new Error(`docs audit status: today must be a real ISO date (YYYY-MM-DD), got "${opts.today}"`);
20683
- }
20684
- if (fetch2.verdict === null) {
20685
- const armedAt = opts.armedAt;
20686
- if (armedAt && isValidIsoDate(armedAt)) {
20687
- const cadenceDays = opts.cadenceDays ?? 7;
20688
- const graceDays = opts.graceDays ?? 3;
20689
- const sinceArmed = ageInDays(armedAt, opts.today);
20690
- if (sinceArmed <= cadenceDays + graceDays) {
20691
- return {
20692
- ok: true,
20693
- state: "awaiting-first-tick",
20694
- line: `docs audit: ${opts.repo} armed ${armedAt} (${sinceArmed}d ago) \u2014 no verdict expected until the first tick`
20695
- };
20696
- }
20697
- return {
20698
- ok: false,
20699
- state: "missing",
20700
- line: `docs audit: ${opts.repo} no verdict on record ${sinceArmed}d after arming ${armedAt} \u2014 janitor blind or dead`
20701
- };
20702
- }
20703
- return { ok: false, state: "missing", line: `docs audit: ${opts.repo} no verdict on record \u2014 janitor blind or dead` };
20704
- }
20705
- const verdict = fetch2.verdict;
20706
- for (const field of ["repo", "shaRange", "checkerVendor"]) {
20707
- const value = verdict[field];
20708
- if (typeof value !== "string" || !value.trim()) {
20709
- const shown = typeof value === "string" ? `"${value}"` : `type ${Array.isArray(value) ? "array" : typeof value}`;
20710
- return {
20711
- ok: false,
20712
- state: "malformed",
20713
- line: `docs audit: ${opts.repo} malformed verdict ${field} (${shown}, expected a non-empty string) \u2014 registry record corrupt, treating as RED`
20714
- };
20715
- }
20716
- }
20717
- if (!isValidIsoDate(verdict.date)) {
20718
- return {
20719
- ok: false,
20720
- state: "malformed",
20721
- line: `docs audit: ${opts.repo} malformed verdict date "${verdict.date}" \u2014 registry record corrupt, treating as RED`
20722
- };
20723
- }
20724
- if (!isValidOutcomeWire(verdict.outcome)) {
20725
- return {
20726
- ok: false,
20727
- state: "malformed",
20728
- line: `docs audit: ${opts.repo} malformed verdict outcome "${verdict.outcome}" (expected clean|refreshed-N|failed) \u2014 registry record corrupt, treating as RED`
20729
- };
20730
- }
20731
- const cadence = opts.cadenceDays ?? 7;
20732
- const grace = opts.graceDays ?? 3;
20733
- const age = ageInDays(verdict.date, opts.today);
20734
- if (age > cadence + grace) {
20735
- return { ok: false, state: "stale", line: `docs audit: ${opts.repo} last verdict ${age}d old \u2014 janitor blind or dead` };
20736
- }
20737
- if (verdict.outcome === "failed") {
20738
- return { ok: false, state: "failed", line: `docs audit: ${opts.repo} last run FAILED (${verdict.date}) \u2014 janitor needs attention` };
20739
- }
20740
- return { ok: true, state: "clean", line: `docs audit: ${opts.repo} ${verdict.outcome} (${verdict.date}, ${verdict.checkerVendor})` };
20741
- }
20742
-
20743
20738
  // src/project-info-sync.ts
20744
- var import_node_fs27 = require("node:fs");
20745
- var import_node_path24 = require("node:path");
20739
+ var import_node_fs28 = require("node:fs");
20740
+ var import_node_path25 = require("node:path");
20746
20741
  var UPDATE_PROJECT_INFO = `mutation($projectId: ID!, $shortDescription: String!, $readme: String!) {
20747
20742
  updateProjectV2(input: { projectId: $projectId, shortDescription: $shortDescription, readme: $readme }) {
20748
20743
  projectV2 { id }
@@ -20787,14 +20782,14 @@ function sharedName(entries, fallback) {
20787
20782
  }
20788
20783
  function buildProjectInfoSyncPlan(targetRepo2, project2, projects, repoRoot2) {
20789
20784
  if (!project2.projectId) throw new Error(`org project sync-info: ${targetRepo2} registry META has no projectId`);
20790
- const readmePath = (0, import_node_path24.join)(repoRoot2, "README.md");
20791
- if (!(0, import_node_fs27.existsSync)(readmePath)) throw new Error(`org project sync-info: ${targetRepo2} has no README.md`);
20785
+ const readmePath = (0, import_node_path25.join)(repoRoot2, "README.md");
20786
+ if (!(0, import_node_fs28.existsSync)(readmePath)) throw new Error(`org project sync-info: ${targetRepo2} has no README.md`);
20792
20787
  const entries = entriesFor(project2, projects);
20793
20788
  const memberRepos = [...new Set(entries.flatMap((entry) => entry.repos ?? []))].filter((repo) => /^[^/]+\/[^/]+$/.test(repo)).sort((a, b) => a.localeCompare(b));
20794
20789
  const projectName = sharedName(entries, project2.name?.trim() || targetRepo2.split("/").pop() || targetRepo2);
20795
20790
  if (!memberRepos.length) throw new Error(`org project sync-info: project ${projectName} has no registered member repos`);
20796
20791
  const entryNames = entries.map((entry) => entry.name?.trim()).filter((name) => Boolean(name));
20797
- const shortDescription = memberRepos.length === 1 ? shortDescriptionFromReadme((0, import_node_fs27.readFileSync)(readmePath, "utf8")) : `Shared work across ${new Intl.ListFormat("en", { type: "conjunction" }).format(entryNames)}.`;
20792
+ const shortDescription = memberRepos.length === 1 ? shortDescriptionFromReadme((0, import_node_fs28.readFileSync)(readmePath, "utf8")) : `Shared work across ${new Intl.ListFormat("en", { type: "conjunction" }).format(entryNames)}.`;
20798
20793
  const lines = [
20799
20794
  `# ${projectName}`,
20800
20795
  "",
@@ -20813,8 +20808,8 @@ function buildProjectInfoSyncPlan(targetRepo2, project2, projects, repoRoot2) {
20813
20808
  const targetBase = `https://github.com/${targetRepo2}`;
20814
20809
  const targetBranch = branchFor(targetRepo2, projects);
20815
20810
  const orgDocs = [
20816
- (0, import_node_fs27.existsSync)((0, import_node_path24.join)(repoRoot2, "docs", "org-readme.md")) ? `- [Org identity](${targetBase}/blob/${targetBranch}/docs/org-readme.md)` : "",
20817
- (0, import_node_fs27.existsSync)((0, import_node_path24.join)(repoRoot2, "docs", "org-architecture.md")) ? `- [Org architecture](${targetBase}/blob/${targetBranch}/docs/org-architecture.md)` : ""
20811
+ (0, import_node_fs28.existsSync)((0, import_node_path25.join)(repoRoot2, "docs", "org-readme.md")) ? `- [Org identity](${targetBase}/blob/${targetBranch}/docs/org-readme.md)` : "",
20812
+ (0, import_node_fs28.existsSync)((0, import_node_path25.join)(repoRoot2, "docs", "org-architecture.md")) ? `- [Org architecture](${targetBase}/blob/${targetBranch}/docs/org-architecture.md)` : ""
20818
20813
  ].filter(Boolean);
20819
20814
  if (orgDocs.length) lines.push("", "## Organisation docs", "", ...orgDocs);
20820
20815
  return { projectId: project2.projectId, projectName, targetRepo: targetRepo2, memberRepos, shortDescription, readme: `${lines.join("\n")}
@@ -21673,8 +21668,8 @@ function writeError(res) {
21673
21668
  }
21674
21669
 
21675
21670
  // src/secrets-commands.ts
21676
- var import_node_fs28 = require("node:fs");
21677
- var import_node_path25 = require("node:path");
21671
+ var import_node_fs29 = require("node:fs");
21672
+ var import_node_path26 = require("node:path");
21678
21673
  var import_node_os10 = require("node:os");
21679
21674
 
21680
21675
  // src/project-runtime.ts
@@ -21798,18 +21793,18 @@ function collectMap(value, previous = []) {
21798
21793
  return [...previous, value];
21799
21794
  }
21800
21795
  async function decryptRailsCredentials(input) {
21801
- const appDir = (0, import_node_path25.resolve)(input.appDir ?? process.cwd());
21796
+ const appDir = (0, import_node_path26.resolve)(input.appDir ?? process.cwd());
21802
21797
  const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
21803
21798
  const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
21804
- const credentialsPath = (0, import_node_path25.resolve)(appDir, credentialsFile);
21805
- const masterKeyPath = (0, import_node_path25.resolve)(appDir, masterKeyFile);
21799
+ const credentialsPath = (0, import_node_path26.resolve)(appDir, credentialsFile);
21800
+ const masterKeyPath = (0, import_node_path26.resolve)(appDir, masterKeyFile);
21806
21801
  const env = {
21807
21802
  ...process.env,
21808
21803
  MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
21809
21804
  MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
21810
21805
  };
21811
- if ((0, import_node_fs28.existsSync)(masterKeyPath)) {
21812
- env.RAILS_MASTER_KEY = (0, import_node_fs28.readFileSync)(masterKeyPath, "utf8").trim();
21806
+ if ((0, import_node_fs29.existsSync)(masterKeyPath)) {
21807
+ env.RAILS_MASTER_KEY = (0, import_node_fs29.readFileSync)(masterKeyPath, "utf8").trim();
21813
21808
  }
21814
21809
  const script = [
21815
21810
  'require "json"',
@@ -21819,9 +21814,9 @@ async function decryptRailsCredentials(input) {
21819
21814
  'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
21820
21815
  "puts JSON.generate(config.config)"
21821
21816
  ].join("\n");
21822
- const scriptDir = (0, import_node_fs28.mkdtempSync)((0, import_node_path25.join)((0, import_node_os10.tmpdir)(), "mmi-rails-decrypt-"));
21823
- const scriptPath = (0, import_node_path25.join)(scriptDir, "decrypt.rb");
21824
- (0, import_node_fs28.writeFileSync)(scriptPath, script, "utf8");
21817
+ const scriptDir = (0, import_node_fs29.mkdtempSync)((0, import_node_path26.join)((0, import_node_os10.tmpdir)(), "mmi-rails-decrypt-"));
21818
+ const scriptPath = (0, import_node_path26.join)(scriptDir, "decrypt.rb");
21819
+ (0, import_node_fs29.writeFileSync)(scriptPath, script, "utf8");
21825
21820
  try {
21826
21821
  const args = ["exec", "ruby", scriptPath];
21827
21822
  const cmd = process.platform === "win32" ? "cmd.exe" : "bundle";
@@ -21833,7 +21828,7 @@ async function decryptRailsCredentials(input) {
21833
21828
  });
21834
21829
  return JSON.parse(stdout);
21835
21830
  } finally {
21836
- (0, import_node_fs28.rmSync)(scriptDir, { recursive: true, force: true });
21831
+ (0, import_node_fs29.rmSync)(scriptDir, { recursive: true, force: true });
21837
21832
  }
21838
21833
  }
21839
21834
  async function readSecretStdin() {
@@ -21923,7 +21918,7 @@ function registerSecretsCommands(program3) {
21923
21918
  let body;
21924
21919
  if (o.file) {
21925
21920
  try {
21926
- body = (0, import_node_fs28.readFileSync)((0, import_node_path25.resolve)(o.file), "utf8");
21921
+ body = (0, import_node_fs29.readFileSync)((0, import_node_path26.resolve)(o.file), "utf8");
21927
21922
  } catch (e) {
21928
21923
  return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
21929
21924
  }
@@ -22028,7 +22023,7 @@ function registerSecretsCommands(program3) {
22028
22023
  {
22029
22024
  ...d,
22030
22025
  decryptRailsCredentials,
22031
- removeFile: (path2) => (0, import_node_fs28.unlinkSync)((0, import_node_path25.resolve)(o.appDir ?? process.cwd(), path2))
22026
+ removeFile: (path2) => (0, import_node_fs29.unlinkSync)((0, import_node_path26.resolve)(o.appDir ?? process.cwd(), path2))
22032
22027
  },
22033
22028
  {
22034
22029
  repo: o.repo,
@@ -22192,7 +22187,7 @@ async function activateAppActor(commandPath3, env, mint) {
22192
22187
  }
22193
22188
 
22194
22189
  // src/box-commands.ts
22195
- var import_node_fs29 = require("node:fs");
22190
+ var import_node_fs30 = require("node:fs");
22196
22191
 
22197
22192
  // src/box.ts
22198
22193
  var BOX_KEYS = {
@@ -22395,7 +22390,7 @@ function registerBoxCommands(program3) {
22395
22390
  }
22396
22391
  if (o.json) console.log(JSON.stringify({ box: found, incomplete }, null, 2));
22397
22392
  else if (o.ssh && o.script) {
22398
- (0, import_node_fs29.writeFileSync)(o.script, sshRecipeScript(found), "utf8");
22393
+ (0, import_node_fs30.writeFileSync)(o.script, sshRecipeScript(found), "utf8");
22399
22394
  console.log(`wrote ${o.script} \u2014 run: bash "${o.script}"`);
22400
22395
  } else if (o.ssh) console.log(`${formatSshRecipe(found)}
22401
22396
  ${SSH_RECIPE_AGENT_NOTE}`);
@@ -22410,7 +22405,7 @@ ${SSH_RECIPE_AGENT_NOTE}`);
22410
22405
 
22411
22406
  // src/schedules-commands.ts
22412
22407
  var import_promises4 = require("node:fs/promises");
22413
- var import_node_child_process15 = require("node:child_process");
22408
+ var import_node_child_process16 = require("node:child_process");
22414
22409
  var import_node_util7 = require("node:util");
22415
22410
 
22416
22411
  // src/schedules.ts
@@ -22721,7 +22716,7 @@ function cadenceStale(registryCadence, liveCadence) {
22721
22716
  if (!liveCrons.length) return false;
22722
22717
  return !liveCrons.every((cron) => registered.has(cron));
22723
22718
  }
22724
- function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflowNames = /* @__PURE__ */ new Set()) {
22719
+ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflowNames = /* @__PURE__ */ new Set(), disabledWorkflowNames = /* @__PURE__ */ new Set()) {
22725
22720
  const registryGithub = registry2.filter((r) => r.executor === "github-actions");
22726
22721
  const liveByName = new Map(liveGithub.map((e) => [e.name, e]));
22727
22722
  const registryById = new Map(registryGithub.map((r) => [r.id, r]));
@@ -22752,6 +22747,18 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
22752
22747
  continue;
22753
22748
  }
22754
22749
  if (activeWorkflowNames.has(r.id)) continue;
22750
+ if (disabledWorkflowNames.has(r.id)) {
22751
+ if (readRepos.has(r.repo)) {
22752
+ drifts.push({
22753
+ class: "registered-but-disabled",
22754
+ name: r.id,
22755
+ executor: r.executor || "github-actions",
22756
+ detail: "workflow file present but DISABLED in GitHub Actions \u2014 the dispatcher cannot run it",
22757
+ remedy: `\`gh workflow enable\` it in ${r.repo} to re-arm (or, if it was retired on purpose, re-run schedules register so the replace prunes the SCHEDULE# row) \u2014 never prune a lane parked on purpose`
22758
+ });
22759
+ }
22760
+ continue;
22761
+ }
22755
22762
  if (readRepos.has(r.repo)) {
22756
22763
  drifts.push({
22757
22764
  class: "registered-but-dead",
@@ -22827,7 +22834,7 @@ function registeredButUnarmedDrifts(registry2, liveEntries, opts) {
22827
22834
  }
22828
22835
  return drifts.sort((a, b) => a.name.localeCompare(b.name));
22829
22836
  }
22830
- function assembleReconciliation(githubEntries2, readRepos, registry2, activeWorkflowNames = /* @__PURE__ */ new Set(), harbour = {}) {
22837
+ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWorkflowNames = /* @__PURE__ */ new Set(), harbour = {}, disabledWorkflowNames = /* @__PURE__ */ new Set()) {
22831
22838
  if (registry2 === null) {
22832
22839
  return {
22833
22840
  reconciliation: [],
@@ -22837,7 +22844,7 @@ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWork
22837
22844
  }
22838
22845
  const live = githubEntries2.filter((e) => e.executor === "github-actions");
22839
22846
  const drifts = [
22840
- ...reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames),
22847
+ ...reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames, disabledWorkflowNames),
22841
22848
  ...registeredButUnarmedDrifts(registry2, harbour.awsEntries ?? [], {
22842
22849
  schedulerRead: Boolean(harbour.schedulerRead),
22843
22850
  now: harbour.now
@@ -22847,7 +22854,7 @@ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWork
22847
22854
  }
22848
22855
 
22849
22856
  // src/schedules-commands.ts
22850
- var execFileP5 = (0, import_node_util7.promisify)(import_node_child_process15.execFile);
22857
+ var execFileP5 = (0, import_node_util7.promisify)(import_node_child_process16.execFile);
22851
22858
  var AWS_REGION = "eu-central-1";
22852
22859
  var AWS_TIMEOUT_MS = 3e4;
22853
22860
  var AWS_RETRY_DELAY_MS = 1500;
@@ -22871,9 +22878,15 @@ async function repoWorkflowEntries(client, repo) {
22871
22878
  const entries = [];
22872
22879
  const failures = [];
22873
22880
  const workflows = [];
22881
+ const disabled = [];
22874
22882
  for (const wf of workflowsList) {
22875
- if (wf?.state !== "active" || typeof wf.path !== "string" || !wf.path) continue;
22883
+ if (typeof wf?.path !== "string" || !wf.path) continue;
22876
22884
  if (!wf.path.startsWith(".github/workflows/")) continue;
22885
+ if (wf.state !== "active") {
22886
+ const basename5 = wf.path.split("/").pop() ?? wf.path;
22887
+ disabled.push(`${repo}/${basename5.replace(/\.ya?ml$/, "")}`);
22888
+ continue;
22889
+ }
22877
22890
  try {
22878
22891
  const contents = await client.rest(
22879
22892
  "GET",
@@ -22892,7 +22905,7 @@ async function repoWorkflowEntries(client, repo) {
22892
22905
  else throw e;
22893
22906
  }
22894
22907
  }
22895
- return { entries, failures, workflows };
22908
+ return { entries, failures, workflows, disabled };
22896
22909
  }
22897
22910
  async function githubEntries(client) {
22898
22911
  const entries = [];
@@ -22900,12 +22913,13 @@ async function githubEntries(client) {
22900
22913
  const drift = [];
22901
22914
  const readRepos = [];
22902
22915
  const workflows = [];
22916
+ const disabledWorkflowNames = [];
22903
22917
  let repos;
22904
22918
  try {
22905
22919
  const listing = await client.restPaginate(`/orgs/${ORG}/repos?per_page=100`);
22906
22920
  repos = listing.filter((r) => typeof r?.name === "string" && r.archived !== true).map((r) => r.name).sort();
22907
22921
  } catch (e) {
22908
- return { entries, incomplete: [`github: could not list ${ORG} repos \u2014 ${e.message}`], drift, reconciliation: [], readRepos, workflowNames: [] };
22922
+ return { entries, incomplete: [`github: could not list ${ORG} repos \u2014 ${e.message}`], drift, reconciliation: [], readRepos, workflowNames: [], disabledWorkflowNames: [] };
22909
22923
  }
22910
22924
  const results = await Promise.all(
22911
22925
  repos.map(async (repo) => {
@@ -22922,6 +22936,7 @@ async function githubEntries(client) {
22922
22936
  readRepos.push(r.repo);
22923
22937
  entries.push(...r.entries);
22924
22938
  workflows.push(...r.workflows);
22939
+ disabledWorkflowNames.push(...r.disabled);
22925
22940
  if (r.failures.length) {
22926
22941
  drift.push(`${r.repo}: ${r.failures.length} workflow record(s) listed active with no file on the default branch (deleted one-offs?) \u2014 deregister them: ${r.failures.map((f) => f.split(": ")[1]).join(", ")}`);
22927
22942
  }
@@ -22929,7 +22944,7 @@ async function githubEntries(client) {
22929
22944
  }
22930
22945
  const reconciliation = [...strayCronDrifts(workflows), ...unlauncheredLlmDrifts(entries)];
22931
22946
  drift.push(...reconciliation.map(renderDrift));
22932
- return { entries, incomplete, drift, reconciliation, readRepos, workflowNames: workflows.map((w) => w.name) };
22947
+ return { entries, incomplete, drift, reconciliation, readRepos, workflowNames: workflows.map((w) => w.name), disabledWorkflowNames };
22933
22948
  }
22934
22949
  async function awsJson(args) {
22935
22950
  const run = async () => {
@@ -22984,7 +22999,7 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
22984
22999
  // #3286: the harbour side joins registry rows against the live aws-scheduler clocks by scheduleId.
22985
23000
  awsEntries: aws.entries,
22986
23001
  schedulerRead: Boolean(aws.schedulerRead)
22987
- });
23002
+ }, new Set(gh.disabledWorkflowNames));
22988
23003
  const selfManaged = /* @__PURE__ */ new Set();
22989
23004
  for (const proj of projects ?? []) {
22990
23005
  if (proj?.schedulesMode !== "self-managed") continue;
@@ -23084,7 +23099,7 @@ function registerSchedulesCommands(program3) {
23084
23099
 
23085
23100
  // src/file-lock.ts
23086
23101
  var import_promises5 = require("node:fs/promises");
23087
- var import_node_path26 = require("node:path");
23102
+ var import_node_path27 = require("node:path");
23088
23103
  var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
23089
23104
  var IMMEDIATE_RETRY_BUDGET = 3;
23090
23105
  var FileLockBusyError = class extends Error {
@@ -23169,7 +23184,7 @@ async function releaseFileLock(lockPath, guard) {
23169
23184
  }
23170
23185
  async function withFileLock(lockPath, opts, fn) {
23171
23186
  const resolved = resolveFileLockOpts(opts);
23172
- await (0, import_promises5.mkdir)((0, import_node_path26.dirname)(lockPath), { recursive: true }).catch(() => void 0);
23187
+ await (0, import_promises5.mkdir)((0, import_node_path27.dirname)(lockPath), { recursive: true }).catch(() => void 0);
23173
23188
  const guard = await acquireFileLock(lockPath, resolved, Date.now() + resolved.maxWaitMs);
23174
23189
  try {
23175
23190
  return await fn();
@@ -23180,7 +23195,7 @@ async function withFileLock(lockPath, opts, fn) {
23180
23195
 
23181
23196
  // src/schedules-lift-command.ts
23182
23197
  var import_promises6 = require("node:fs/promises");
23183
- var import_node_path27 = require("node:path");
23198
+ var import_node_path28 = require("node:path");
23184
23199
 
23185
23200
  // src/schedules-lift.ts
23186
23201
  var SCHEDULE_HEADER_FIELDS = ["schedule", "what", "owner", "cadence", "executor", "llm", "output", "breaks", "kill"];
@@ -23286,7 +23301,7 @@ async function readWorkflowFiles(dir) {
23286
23301
  const files = [];
23287
23302
  for (const name of names.sort()) {
23288
23303
  if (!/\.ya?ml$/.test(name)) continue;
23289
- files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises6.readFile)((0, import_node_path27.join)(dir, name), "utf8") });
23304
+ files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises6.readFile)((0, import_node_path28.join)(dir, name), "utf8") });
23290
23305
  }
23291
23306
  return files;
23292
23307
  }
@@ -23828,11 +23843,12 @@ function registerQueryCommands(program3) {
23828
23843
  }
23829
23844
 
23830
23845
  // src/bootstrap-commands.ts
23831
- var import_node_fs30 = require("node:fs");
23846
+ var import_node_fs31 = require("node:fs");
23832
23847
  var import_node_os11 = require("node:os");
23833
- var import_node_path28 = require("node:path");
23848
+ var import_node_path29 = require("node:path");
23834
23849
 
23835
23850
  // src/bootstrap-drift.ts
23851
+ var import_node_crypto6 = require("node:crypto");
23836
23852
  function byteComparableSeeds(manifest, cls) {
23837
23853
  return manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self" && s.classes.includes(cls));
23838
23854
  }
@@ -23841,6 +23857,9 @@ function compareSeedBytes(hubContent, repoContent) {
23841
23857
  const normalize = (s) => s.replace(/\r\n/g, "\n");
23842
23858
  return normalize(hubContent) === normalize(repoContent) ? "match" : "drift";
23843
23859
  }
23860
+ function seedContentHash(content) {
23861
+ return (0, import_node_crypto6.createHash)("sha256").update(content.replace(/\r\n/g, "\n"), "utf8").digest("hex");
23862
+ }
23844
23863
  function auditRepoSeedDrift(repo, seeds, hubContents, repoReads) {
23845
23864
  const byTarget = new Map(repoReads.map((r) => [r.target, r.content]));
23846
23865
  const slug = repo.includes("/") ? repo.slice(repo.indexOf("/") + 1).toLowerCase() : repo.toLowerCase();
@@ -23863,11 +23882,14 @@ function auditRepoSeedDrift(repo, seeds, hubContents, repoReads) {
23863
23882
  findings.push({ repo, target: seed.target, state: "waived", detail: `${state} \u2014 waived: ${why}` });
23864
23883
  continue;
23865
23884
  }
23885
+ const repoContent = byTarget.get(seed.target);
23866
23886
  findings.push({
23867
23887
  repo,
23868
23888
  target: seed.target,
23869
23889
  state,
23870
- detail: state === "absent" ? "declared org-owned in the manifest but not present on the base branch" : "differs from MMI-Hub's copy \u2014 propagate with `mmi-cli bootstrap apply <repo> --only <target> --execute`, or, if this repo is RIGHT to differ, declare a waiver for it on the seed in the manifest (#3842)"
23890
+ detail: state === "absent" ? "declared org-owned in the manifest but not present on the base branch" : "differs from MMI-Hub's copy \u2014 fix this in MMI-Hub and let it propagate (#4233); `mmi-cli bootstrap apply <repo> --only <target> --execute` remains for THIS repo's own bootstrap/onboarding, never a fleet-wide fan-out (#4241 fails a hand-edited fleet copy at the merge gate on repos carrying the new seed), or, if this repo is RIGHT to differ, declare a waiver for it on the seed in the manifest (#3842)",
23891
+ // #4242: only a real 'drift' has bytes worth hashing — 'absent' has none to compare against history.
23892
+ ...state === "drift" && repoContent != null ? { contentHash: seedContentHash(repoContent) } : {}
23871
23893
  });
23872
23894
  }
23873
23895
  return findings;
@@ -23886,6 +23908,115 @@ function renderSeedDriftReport(findings, reposAudited, seedsPerRepo) {
23886
23908
  return lines.join("\n");
23887
23909
  }
23888
23910
 
23911
+ // src/bootstrap-propagate.ts
23912
+ function assertPropagationCoverage(rosterCount, independentRegistryCount) {
23913
+ if (rosterCount === 0) {
23914
+ throw new Error("bootstrap propagate: roster read 0 repos \u2014 refusing to report a plan from a roster this command could not establish (#4232 coverage invariant)");
23915
+ }
23916
+ if (rosterCount < independentRegistryCount) {
23917
+ throw new Error(
23918
+ `bootstrap propagate: roster read ${rosterCount} repo(s) but the independent registry count is ${independentRegistryCount} \u2014 this read did not cover the fleet (#4232 coverage invariant)`
23919
+ );
23920
+ }
23921
+ }
23922
+ function assignWaves(repos, canarySlug) {
23923
+ const waves = /* @__PURE__ */ new Map();
23924
+ if (!canarySlug) return waves;
23925
+ const rest = repos.filter((r) => r.slug !== canarySlug).slice().sort((a, b) => a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0);
23926
+ const canary = repos.find((r) => r.slug === canarySlug);
23927
+ if (canary) waves.set(canary.repo, 0);
23928
+ const wave1Count = Math.ceil(rest.length * 0.25);
23929
+ rest.forEach((r, i) => waves.set(r.repo, i < wave1Count ? 1 : 2));
23930
+ return waves;
23931
+ }
23932
+ function statusFor(read) {
23933
+ if (!read) return { status: "pending", record: {} };
23934
+ if (read.drift === "match") return { status: "match", record: { mergeSha: read.pr?.mergeSha } };
23935
+ const pr2 = read.pr;
23936
+ if (!pr2) return { status: "pending", record: {} };
23937
+ if (pr2.state === "closed") return { status: "closed-unmerged", record: { prNumber: pr2.number, prUrl: pr2.url } };
23938
+ if (pr2.checks === "red") return { status: "red", record: { prNumber: pr2.number, prUrl: pr2.url } };
23939
+ return { status: "open-pending", record: { prNumber: pr2.number, prUrl: pr2.url, mergeSha: pr2.mergeSha } };
23940
+ }
23941
+ var HALTING_STATUSES = /* @__PURE__ */ new Set(["red", "closed-unmerged"]);
23942
+ function planPropagationTick(input) {
23943
+ const { target, repos, canarySlug, reads, isWorkflowSeed, functionGateSatisfied } = input;
23944
+ const readByRepo = new Map(reads.map((r) => [r.repo, r]));
23945
+ const records = [];
23946
+ const opened = [];
23947
+ const waived = repos.filter((r) => r.waiver);
23948
+ for (const r of waived) {
23949
+ records.push({ repo: r.repo, target, wave: null, status: "skipped-waived", action: "none", detail: `waived: ${r.waiver}` });
23950
+ }
23951
+ const eligible = repos.filter((r) => !r.waiver);
23952
+ const refusedNoCanary = !canarySlug || !eligible.some((r) => r.slug === canarySlug);
23953
+ if (refusedNoCanary) {
23954
+ for (const r of eligible) {
23955
+ records.push({ repo: r.repo, target, wave: null, status: "not-yet-reached", action: "none", detail: "no canary declared for this target \u2014 refusing to plan any wave (#4238 canary requirement)" });
23956
+ }
23957
+ return { target, reposInScope: repos.length, canary: null, refusedNoCanary: true, halted: false, haltReason: null, opened, records };
23958
+ }
23959
+ const waveOf = assignWaves(eligible, canarySlug);
23960
+ const byWave = { 0: [], 1: [], 2: [] };
23961
+ for (const r of eligible) byWave[waveOf.get(r.repo) ?? 2].push(r);
23962
+ let halted = false;
23963
+ let haltReason = null;
23964
+ let waveGateOpen = true;
23965
+ for (const waveNum of [0, 1, 2]) {
23966
+ const waveRepos = byWave[waveNum];
23967
+ if (!waveRepos.length) continue;
23968
+ if (!waveGateOpen || halted) {
23969
+ for (const r of waveRepos) {
23970
+ records.push({ repo: r.repo, target, wave: waveNum, status: "not-yet-reached", action: "none", detail: "prior wave not yet converged" });
23971
+ }
23972
+ continue;
23973
+ }
23974
+ let waveAllMatch = true;
23975
+ let waveHasRed = false;
23976
+ for (const r of waveRepos) {
23977
+ const { status, record } = statusFor(readByRepo.get(r.repo));
23978
+ const shouldOpen = status === "pending";
23979
+ if (shouldOpen) opened.push(r.repo);
23980
+ records.push({
23981
+ repo: r.repo,
23982
+ target,
23983
+ wave: waveNum,
23984
+ status,
23985
+ action: shouldOpen ? "open-pr" : "none",
23986
+ detail: shouldOpen ? "no open propagation PR and not yet matching \u2014 opening this tick" : status,
23987
+ ...record
23988
+ });
23989
+ if (status !== "match") waveAllMatch = false;
23990
+ if (HALTING_STATUSES.has(status)) waveHasRed = true;
23991
+ }
23992
+ if (waveHasRed) {
23993
+ halted = true;
23994
+ haltReason = `wave ${waveNum} has a red or closed-unmerged PR \u2014 halting; no further wave opens (#4238 halt-and-alarm)`;
23995
+ waveGateOpen = false;
23996
+ continue;
23997
+ }
23998
+ if (waveNum === 0 && isWorkflowSeed && !functionGateSatisfied) {
23999
+ waveGateOpen = false;
24000
+ continue;
24001
+ }
24002
+ waveGateOpen = waveAllMatch;
24003
+ }
24004
+ return { target, reposInScope: repos.length, canary: canarySlug, refusedNoCanary: false, halted, haltReason, opened, records };
24005
+ }
24006
+ function renderPropagationReport(plan) {
24007
+ const lines = [`bootstrap propagate \u2014 target ${plan.target}: ${plan.reposInScope} repo(s) in scope, canary=${plan.canary ?? "NONE"}`];
24008
+ if (plan.refusedNoCanary) {
24009
+ lines.push(" REFUSED \u2014 no canary declared for this target; plan every repo not-yet-reached");
24010
+ return lines.join("\n");
24011
+ }
24012
+ for (const r of plan.records) {
24013
+ const wave2 = r.wave == null ? "-" : String(r.wave);
24014
+ lines.push(` wave${wave2.padEnd(2)} ${r.status.padEnd(16)} ${r.repo}${r.prNumber ? ` PR#${r.prNumber}` : ""} \u2014 ${r.detail}`);
24015
+ }
24016
+ lines.push(plan.halted ? ` HALTED \u2014 ${plan.haltReason}` : ` opened this tick: ${plan.opened.length ? plan.opened.join(", ") : "(none)"}`);
24017
+ return lines.join("\n");
24018
+ }
24019
+
23889
24020
  // src/bootstrap-verify.ts
23890
24021
  var TRAIN_BRANCHES2 = ["development", "rc", "main"];
23891
24022
  var requiredDocs = ["README.md", "architecture.md", "docs/decisions/README.md", "docs/index.md"];
@@ -24102,6 +24233,11 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
24102
24233
  const repoInfo = await restJson3(deps, `repos/${repo}`, {});
24103
24234
  checks.push({ ok: Boolean(repoInfo.default_branch), label: "repo exists" });
24104
24235
  checks.push({ ok: repoInfo.default_branch === baseBranch, label: `default branch is ${baseBranch}`, detail: repoInfo.default_branch || "missing" });
24236
+ checks.push({
24237
+ ok: repoInfo.has_wiki === false,
24238
+ label: "has_wiki disabled",
24239
+ detail: repoInfo.has_wiki === false ? void 0 : `wikis are retired org-wide \u2014 gh api -X PATCH repos/${repo} -F has_wiki=false`
24240
+ });
24105
24241
  const branchList = await restPagedJson2(deps, `repos/${repo}/branches`, []);
24106
24242
  const branchNames = new Set(branchList.map((b) => b.name));
24107
24243
  for (const branch of branchesWanted) {
@@ -24543,13 +24679,13 @@ function registerBootstrapCommands(program3) {
24543
24679
  client: defaultGitHubClient(),
24544
24680
  projectMeta: meta,
24545
24681
  deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
24546
- readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs30.existsSync)(path2) ? (0, import_node_fs30.readFileSync)(path2, "utf8") : null,
24682
+ readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs31.existsSync)(path2) ? (0, import_node_fs31.readFileSync)(path2, "utf8") : null,
24547
24683
  // requiredGcpApis is stored as an array by a JSON write, but `org project set --var KEY=VALUE` stores a raw
24548
24684
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
24549
24685
  // #3689: the same committed map the org access audit reads (#3664), so a sanctioned admin is not a
24550
24686
  // permanent bootstrap failure on one surface and an intended state on the other. Absent file → no
24551
24687
  // sanction, which is the pre-#3664 behaviour.
24552
- sanctionedAdmins: (0, import_node_fs30.existsSync)("access-matrix.json") ? entriesValueByCanonicalRepo(loadSanctionedAdmins((0, import_node_fs30.readFileSync)("access-matrix.json", "utf8")), repo) : void 0,
24688
+ sanctionedAdmins: (0, import_node_fs31.existsSync)("access-matrix.json") ? entriesValueByCanonicalRepo(loadSanctionedAdmins((0, import_node_fs31.readFileSync)("access-matrix.json", "utf8")), repo) : void 0,
24553
24689
  requiredGcpApis: (() => {
24554
24690
  const v = meta?.requiredGcpApis;
24555
24691
  if (Array.isArray(v)) return v;
@@ -24602,12 +24738,12 @@ function registerBootstrapCommands(program3) {
24602
24738
  bootstrap.command("drift").description("#3818: compare every org-owned whole-file seed against MMI-Hub's copy across the registry roster; read-only").option("--repo <owner/repo>", "audit one repo instead of the roster (never a fleet verdict)").option("--json", "machine-readable output").action(async () => {
24603
24739
  const o = { repo: rawValue("--repo", ""), json: rawFlag("--json") };
24604
24740
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
24605
- if (!(0, import_node_fs30.existsSync)(manifestPath)) return fail(`bootstrap drift: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the reference this compares against`);
24606
- const manifest = loadBootstrapSeeds((0, import_node_fs30.readFileSync)(manifestPath, "utf8"));
24741
+ if (!(0, import_node_fs31.existsSync)(manifestPath)) return fail(`bootstrap drift: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the reference this compares against`);
24742
+ const manifest = loadBootstrapSeeds((0, import_node_fs31.readFileSync)(manifestPath, "utf8"));
24607
24743
  const hubContents = /* @__PURE__ */ new Map();
24608
24744
  for (const s of manifest.seeds) {
24609
24745
  if (s.ownership !== "org" || s.source !== "self") continue;
24610
- hubContents.set(s.target, (0, import_node_fs30.existsSync)(s.target) ? (0, import_node_fs30.readFileSync)(s.target, "utf8") : null);
24746
+ hubContents.set(s.target, (0, import_node_fs31.existsSync)(s.target) ? (0, import_node_fs31.readFileSync)(s.target, "utf8") : null);
24611
24747
  }
24612
24748
  let targets;
24613
24749
  let classOf = (_repo) => "deployable";
@@ -24684,8 +24820,8 @@ function registerBootstrapCommands(program3) {
24684
24820
  return fail(`bootstrap apply: ${e.message}`);
24685
24821
  }
24686
24822
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
24687
- if (!(0, import_node_fs30.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
24688
- const manifest = loadBootstrapSeeds((0, import_node_fs30.readFileSync)(manifestPath, "utf8"));
24823
+ if (!(0, import_node_fs31.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
24824
+ const manifest = loadBootstrapSeeds((0, import_node_fs31.readFileSync)(manifestPath, "utf8"));
24689
24825
  const baseBranch = o.class === "content" ? "main" : "development";
24690
24826
  const slug = parsedRepo.slug;
24691
24827
  const onlyTarget = o.only.trim();
@@ -24696,16 +24832,16 @@ function registerBootstrapCommands(program3) {
24696
24832
  ${known}`);
24697
24833
  }
24698
24834
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
24699
- const readFile9 = (p) => (0, import_node_fs30.existsSync)(p) ? (0, import_node_fs30.readFileSync)(p, "utf8") : null;
24835
+ const readFile9 = (p) => (0, import_node_fs31.existsSync)(p) ? (0, import_node_fs31.readFileSync)(p, "utf8") : null;
24700
24836
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
24701
24837
  const putSeed = async (target, content, ref, sha) => {
24702
- const tmp = (0, import_node_path28.join)((0, import_node_os11.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
24703
- (0, import_node_fs30.writeFileSync)(tmp, JSON.stringify(contentPutBody(target, content, ref, sha)), "utf8");
24838
+ const tmp = (0, import_node_path29.join)((0, import_node_os11.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
24839
+ (0, import_node_fs31.writeFileSync)(tmp, JSON.stringify(contentPutBody(target, content, ref, sha)), "utf8");
24704
24840
  try {
24705
24841
  await gh(contentPutInputArgs(repo, target, tmp));
24706
24842
  } finally {
24707
24843
  try {
24708
- (0, import_node_fs30.unlinkSync)(tmp);
24844
+ (0, import_node_fs31.unlinkSync)(tmp);
24709
24845
  } catch {
24710
24846
  }
24711
24847
  }
@@ -24879,6 +25015,14 @@ function registerBootstrapCommands(program3) {
24879
25015
  });
24880
25016
  applied.push(autoMergeEnabled ? `seed: PR ${seedPrUrl} (base ${baseBranch} protected; auto-merge enabled)` : `seed: PR ${seedPrUrl} (base ${baseBranch} protected; auto-merge refused \u2014 PR is already clean. Land it: mmi-cli pr land <n> --repo ${repo})`);
24881
25017
  }
25018
+ if (o.execute && !onlyTarget) {
25019
+ try {
25020
+ await gh(["api", "-X", "PATCH", `repos/${repo}`, "-F", "has_wiki=false"]);
25021
+ applied.push("has_wiki=false (wikis retired org-wide)");
25022
+ } catch (e) {
25023
+ applied.push(`has_wiki=false (failed: ${e.message})`);
25024
+ }
25025
+ }
24882
25026
  if (o.execute && !onlyTarget && o.class === "deployable") {
24883
25027
  try {
24884
25028
  await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-f", "delete_branch_on_merge=true"]);
@@ -24959,15 +25103,183 @@ LIVE apply to ${repo}:
24959
25103
  ${applied.join("\n ")}`);
24960
25104
  }
24961
25105
  });
25106
+ bootstrap.command("propagate").description("#4238: re-entrant canary\u2192wave tick \u2014 plan (or, with --execute, open) per-repo PRs fanning an org-owned seed out to the fleet").option("--target <path>", "the manifest target to propagate (an ownership:org + source:self seed, e.g. .github/workflows/agent-pr.yml)").option("--execute", "LIVE tick via gh (master-gated) \u2014 opens/reuses per-repo seed-propagate PRs; dry-run prints the plan only").option("--json", "machine-readable output").action(async () => {
25107
+ const o = { target: rawValue("--target", ""), execute: rawFlag("--execute"), json: rawFlag("--json") };
25108
+ const manifestPath = "skills/bootstrap/seeds/manifest.json";
25109
+ if (!(0, import_node_fs31.existsSync)(manifestPath)) return fail(`bootstrap propagate: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the desired state this tick propagates`);
25110
+ const manifest = loadBootstrapSeeds((0, import_node_fs31.readFileSync)(manifestPath, "utf8"));
25111
+ const propagatable = manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self");
25112
+ if (!o.target) {
25113
+ return fail(`bootstrap propagate: --target <path> is required \u2014 one of:
25114
+ ${propagatable.map((s) => s.target).join("\n ")}`);
25115
+ }
25116
+ const seed = propagatable.find((s) => s.target === o.target);
25117
+ if (!seed) return fail(`bootstrap propagate: --target '${o.target}' names no ownership:org + source:self seed in ${manifestPath}. Propagatable targets:
25118
+ ${propagatable.map((s) => s.target).join("\n ")}`);
25119
+ if (!(0, import_node_fs31.existsSync)(seed.target)) return fail(`bootstrap propagate: the Hub's own copy of '${seed.target}' is missing \u2014 nothing to propagate`);
25120
+ const hubContent = (0, import_node_fs31.readFileSync)(seed.target, "utf8");
25121
+ const isWorkflowSeed = seed.target.startsWith(".github/workflows/");
25122
+ const cfg = await loadConfig();
25123
+ const projects = await fetchProjectsList(registryClientDeps(cfg));
25124
+ if (!projects || projects.length === 0) {
25125
+ return failGraceful("bootstrap propagate: the registry roster is unreadable or empty \u2014 refusing to plan a tick from a scope this command could not establish (#4232 coverage invariant)");
25126
+ }
25127
+ const rosterRepos2 = collectRegistryRepos(projects).filter((r) => r.toLowerCase() !== "mutmutco/mmi-hub");
25128
+ let independentCount = rosterRepos2.length;
25129
+ if ((0, import_node_fs31.existsSync)("projects.json")) {
25130
+ try {
25131
+ const local = JSON.parse((0, import_node_fs31.readFileSync)("projects.json", "utf8"));
25132
+ const localRepos = /* @__PURE__ */ new Set();
25133
+ for (const p of local.projects ?? []) for (const r of p.repos ?? []) {
25134
+ const full = (r.includes("/") ? r : `mutmutco/${r}`).toLowerCase();
25135
+ if (full !== "mutmutco/mmi-hub") localRepos.add(full);
25136
+ }
25137
+ if (localRepos.size > 0) independentCount = localRepos.size;
25138
+ } catch {
25139
+ }
25140
+ }
25141
+ try {
25142
+ assertPropagationCoverage(rosterRepos2.length, independentCount);
25143
+ } catch (e) {
25144
+ return fail(e.message);
25145
+ }
25146
+ const bySlugMeta = new Map(projects.flatMap((p) => (p.repos ?? []).map((r) => [(r.includes("/") ? r : `mutmutco/${r}`).toLowerCase(), p])));
25147
+ const classOf = (repo) => bySlugMeta.get(repo.toLowerCase())?.class ?? "deployable";
25148
+ const canaryProject = projects.find((p) => p.seedCanary === true);
25149
+ const canarySlug = canaryProject ? (canaryProject.repos ?? [])[0]?.split("/").pop()?.toLowerCase() ?? null : null;
25150
+ const repos = rosterRepos2.map((repo) => {
25151
+ const slug = repo.split("/").pop().toLowerCase();
25152
+ const waiver = seed.waivers?.[slug];
25153
+ return { repo, slug, waiver };
25154
+ });
25155
+ const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
25156
+ const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
25157
+ const branchPrefix = "seed-propagate";
25158
+ const reads = [];
25159
+ for (const r of repos) {
25160
+ if (r.waiver) continue;
25161
+ const baseBranch = classOf(r.repo) === "content" ? "main" : "development";
25162
+ let content = null;
25163
+ try {
25164
+ const resp = await gh(["api", `repos/${r.repo}/contents/${enc(seed.target)}?ref=${baseBranch}`]);
25165
+ const parsed = JSON.parse(resp.stdout);
25166
+ content = parsed.encoding === "base64" && typeof parsed.content === "string" ? Buffer.from(parsed.content, "base64").toString("utf8") : null;
25167
+ } catch {
25168
+ content = null;
25169
+ }
25170
+ const drift = compareSeedBytes(hubContent, content);
25171
+ let pr2;
25172
+ try {
25173
+ const branch = `${branchPrefix}-${r.slug}`;
25174
+ const listed = await gh(["pr", "list", "--repo", r.repo, "--head", branch, "--base", baseBranch, "--state", "all", "--json", "number,url,state,statusCheckRollup,mergeCommit", "--limit", "1"]);
25175
+ const arr = JSON.parse(listed.stdout || "[]");
25176
+ const p = arr[0];
25177
+ if (p) {
25178
+ const rollup = p.statusCheckRollup ?? [];
25179
+ const checks = rollup.length === 0 ? "none" : rollup.some((c) => c.conclusion === "FAILURE" || c.state === "FAILURE") ? "red" : rollup.every((c) => c.conclusion === "SUCCESS" || c.state === "SUCCESS") ? "success" : "pending";
25180
+ pr2 = { number: p.number, url: p.url, state: p.state === "MERGED" ? "merged" : p.state === "CLOSED" ? "closed" : "open", checks, mergeSha: p.mergeCommit?.oid };
25181
+ }
25182
+ } catch {
25183
+ pr2 = void 0;
25184
+ }
25185
+ reads.push({ repo: r.repo, drift: drift === "waived" ? "match" : drift, pr: pr2 });
25186
+ }
25187
+ let functionGateSatisfied = !isWorkflowSeed;
25188
+ if (isWorkflowSeed && canarySlug) {
25189
+ const canaryRepo = repos.find((r) => r.slug === canarySlug)?.repo;
25190
+ const canaryRead = reads.find((r) => r.repo === canaryRepo);
25191
+ if (canaryRead?.drift === "match") {
25192
+ try {
25193
+ const workflowFile = seed.target.split("/").pop();
25194
+ const runs = await gh(["api", `repos/${canaryRepo}/actions/workflows/${workflowFile}/runs?status=success&per_page=1`]);
25195
+ const parsed = JSON.parse(runs.stdout);
25196
+ functionGateSatisfied = Array.isArray(parsed.workflow_runs) && parsed.workflow_runs.length > 0;
25197
+ } catch {
25198
+ functionGateSatisfied = false;
25199
+ }
25200
+ }
25201
+ }
25202
+ const plan = planPropagationTick({ target: seed.target, repos, canarySlug, reads, isWorkflowSeed, functionGateSatisfied });
25203
+ if (o.execute) {
25204
+ if (plan.refusedNoCanary) return fail("bootstrap propagate --execute: no canary declared for this target \u2014 refusing to write (set seedCanary:true on exactly one registry repo)");
25205
+ const headSha = (await gh(["api", "repos/mutmutco/MMI-Hub/commits/development", "--jq", ".sha"])).stdout.trim();
25206
+ for (const rec of plan.records) {
25207
+ if (rec.action !== "open-pr") continue;
25208
+ const repoEntry = repos.find((r) => r.repo === rec.repo);
25209
+ const baseBranch = classOf(rec.repo) === "content" ? "main" : "development";
25210
+ const branch = `${branchPrefix}-${repoEntry.slug}`;
25211
+ const baseRef = await gh(["api", `repos/${rec.repo}/git/ref/heads/${baseBranch}`, "--jq", ".object.sha"]);
25212
+ const baseSha = baseRef.stdout.trim();
25213
+ let branchExists = true;
25214
+ try {
25215
+ await gh(["api", `repos/${rec.repo}/git/ref/heads/${branch}`]);
25216
+ } catch {
25217
+ branchExists = false;
25218
+ }
25219
+ if (!branchExists) await gh(["api", `repos/${rec.repo}/git/refs`, "-f", `ref=refs/heads/${branch}`, "-f", `sha=${baseSha}`]);
25220
+ let existingSha;
25221
+ try {
25222
+ const cur = await gh(["api", `repos/${rec.repo}/contents/${enc(seed.target)}?ref=${branch}`]);
25223
+ existingSha = JSON.parse(cur.stdout).sha;
25224
+ } catch {
25225
+ existingSha = void 0;
25226
+ }
25227
+ const tmp = (0, import_node_path29.join)((0, import_node_os11.tmpdir)(), `mmi-propagate-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
25228
+ (0, import_node_fs31.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, hubContent, branch, existingSha)), "utf8");
25229
+ try {
25230
+ await gh(contentPutInputArgs(rec.repo, seed.target, tmp));
25231
+ } finally {
25232
+ try {
25233
+ (0, import_node_fs31.unlinkSync)(tmp);
25234
+ } catch {
25235
+ }
25236
+ }
25237
+ const openPrs = await gh(["pr", "list", "--repo", rec.repo, "--head", branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
25238
+ const prDecision = decideSeedPrAction(JSON.parse(openPrs.stdout || "[]"));
25239
+ let prUrl;
25240
+ if (prDecision.action === "reuse") {
25241
+ prUrl = prDecision.url;
25242
+ } else {
25243
+ const created = await ghCreate([
25244
+ "pr",
25245
+ "create",
25246
+ "--repo",
25247
+ rec.repo,
25248
+ "--base",
25249
+ baseBranch,
25250
+ "--head",
25251
+ branch,
25252
+ "--title",
25253
+ `chore: propagate org-owned ${seed.target} from MMI-Hub (wave ${rec.wave})`,
25254
+ "--body",
25255
+ `Auto-opened by \`mmi-cli bootstrap propagate --target ${seed.target} --execute\` (#4238), wave ${rec.wave}.
25256
+
25257
+ Propagates MMI-Hub@${headSha} 's copy of \`${seed.target}\` to this repo \u2014 the file this PR carries and nothing else.
25258
+
25259
+ Rollback: \`mmi-cli bootstrap rollback ${rec.repo} --target ${seed.target} --execute\` (#4240) reverts this PR's merge commit; never re-run propagate with old bytes.`
25260
+ ]);
25261
+ prUrl = created.url;
25262
+ }
25263
+ if (rec.wave !== 0) {
25264
+ await gh(["pr", "merge", prUrl, "--repo", rec.repo, "--auto", "--squash"]).catch(() => {
25265
+ });
25266
+ }
25267
+ rec.prUrl = prUrl;
25268
+ }
25269
+ }
25270
+ if (o.json) console.log(JSON.stringify(plan, null, 2));
25271
+ else console.log(renderPropagationReport(plan));
25272
+ if (plan.halted) process.exitCode = 1;
25273
+ });
24962
25274
  }
24963
25275
 
24964
25276
  // src/stage-commands.ts
24965
- var import_node_fs32 = require("node:fs");
24966
- var import_node_path30 = require("node:path");
25277
+ var import_node_fs33 = require("node:fs");
25278
+ var import_node_path31 = require("node:path");
24967
25279
 
24968
25280
  // src/port-registry.ts
24969
- var import_node_fs31 = require("node:fs");
24970
- var import_node_path29 = require("node:path");
25281
+ var import_node_fs32 = require("node:fs");
25282
+ var import_node_path30 = require("node:path");
24971
25283
 
24972
25284
  // ../infra/port-geometry.mjs
24973
25285
  var PORT_BLOCK = 100;
@@ -24981,8 +25293,8 @@ function nextPortBlock(registry2) {
24981
25293
  return [base, base + PORT_SPAN];
24982
25294
  }
24983
25295
  function loadPortRegistry(path2) {
24984
- if (!(0, import_node_fs31.existsSync)(path2)) return {};
24985
- const raw = JSON.parse((0, import_node_fs31.readFileSync)(path2, "utf8"));
25296
+ if (!(0, import_node_fs32.existsSync)(path2)) return {};
25297
+ const raw = JSON.parse((0, import_node_fs32.readFileSync)(path2, "utf8"));
24986
25298
  const out = {};
24987
25299
  for (const [key, value] of Object.entries(raw)) {
24988
25300
  if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
@@ -24996,9 +25308,9 @@ function ensurePortRange(repo, path2) {
24996
25308
  const existing = registry2[repo];
24997
25309
  if (existing) return existing;
24998
25310
  const range = nextPortBlock(registry2);
24999
- const raw = (0, import_node_fs31.existsSync)(path2) ? JSON.parse((0, import_node_fs31.readFileSync)(path2, "utf8")) : {};
25311
+ const raw = (0, import_node_fs32.existsSync)(path2) ? JSON.parse((0, import_node_fs32.readFileSync)(path2, "utf8")) : {};
25000
25312
  raw[repo] = range;
25001
- (0, import_node_fs31.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
25313
+ (0, import_node_fs32.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
25002
25314
  return range;
25003
25315
  }
25004
25316
  function portCursorSeed(registry2) {
@@ -25020,22 +25332,22 @@ function existingPortRange(repo, registry2) {
25020
25332
  return registry2[repo] ?? null;
25021
25333
  }
25022
25334
  function portRangeInfraAt(root, source) {
25023
- const registryPath = (0, import_node_path29.join)(root, "infra", "port-ranges.json");
25024
- const ddbScriptPath = (0, import_node_path29.join)(root, "infra", "port-ddb.mjs");
25025
- if (!(0, import_node_fs31.existsSync)(registryPath) || !(0, import_node_fs31.existsSync)(ddbScriptPath)) return null;
25335
+ const registryPath = (0, import_node_path30.join)(root, "infra", "port-ranges.json");
25336
+ const ddbScriptPath = (0, import_node_path30.join)(root, "infra", "port-ddb.mjs");
25337
+ if (!(0, import_node_fs32.existsSync)(registryPath) || !(0, import_node_fs32.existsSync)(ddbScriptPath)) return null;
25026
25338
  return { root, source, registryPath, ddbScriptPath };
25027
25339
  }
25028
25340
  function resolvePortRangeInfra(cwd, packageDir) {
25029
25341
  const direct = portRangeInfraAt(cwd, "cwd");
25030
25342
  if (direct) return direct;
25031
- for (let dir = cwd; ; dir = (0, import_node_path29.dirname)(dir)) {
25032
- const sibling = portRangeInfraAt((0, import_node_path29.join)(dir, "MMI-Hub"), "sibling-hub");
25343
+ for (let dir = cwd; ; dir = (0, import_node_path30.dirname)(dir)) {
25344
+ const sibling = portRangeInfraAt((0, import_node_path30.join)(dir, "MMI-Hub"), "sibling-hub");
25033
25345
  if (sibling) return sibling;
25034
- const parent = (0, import_node_path29.dirname)(dir);
25346
+ const parent = (0, import_node_path30.dirname)(dir);
25035
25347
  if (parent === dir) break;
25036
25348
  }
25037
25349
  if (packageDir) {
25038
- const pkgRoot = (0, import_node_path29.join)(packageDir, "..", "..");
25350
+ const pkgRoot = (0, import_node_path30.join)(packageDir, "..", "..");
25039
25351
  const pkgFrom = portRangeInfraAt(pkgRoot, "pkg-root");
25040
25352
  if (pkgFrom) return pkgFrom;
25041
25353
  }
@@ -25229,8 +25541,8 @@ function registerStageCommands(program3) {
25229
25541
  const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
25230
25542
  return decideStage({
25231
25543
  registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
25232
- hasCompose: (0, import_node_fs32.existsSync)((0, import_node_path30.join)(process.cwd(), "docker-compose.yml")),
25233
- hasEnvExample: (0, import_node_fs32.existsSync)((0, import_node_path30.join)(process.cwd(), ".env.example"))
25544
+ hasCompose: (0, import_node_fs33.existsSync)((0, import_node_path31.join)(process.cwd(), "docker-compose.yml")),
25545
+ hasEnvExample: (0, import_node_fs33.existsSync)((0, import_node_path31.join)(process.cwd(), ".env.example"))
25234
25546
  });
25235
25547
  }
25236
25548
  async function fetchStageVaultEnvMerge() {
@@ -25499,6 +25811,7 @@ function registerBoardCommands(program3) {
25499
25811
  force: o.force,
25500
25812
  allowPartial: o.allowPartial
25501
25813
  });
25814
+ invalidateStatuslineBoardCache();
25502
25815
  if (o.json) return console.log(JSON.stringify(result));
25503
25816
  console.log(result.partial ? `Partially claimed ${result.item.ref}: ${result.warning}` : result.alreadyClaimed ? `Already claimed ${result.item.ref} - In Progress (no change)` : `Claimed ${result.item.ref} - In Progress`);
25504
25817
  } catch (e) {
@@ -25515,6 +25828,7 @@ function registerBoardCommands(program3) {
25515
25828
  force: o.force,
25516
25829
  allowPartial: o.allowPartial
25517
25830
  });
25831
+ if (bulk.results.some((r) => r.claimed)) invalidateStatuslineBoardCache();
25518
25832
  if (o.json) {
25519
25833
  console.log(JSON.stringify(bulk.results));
25520
25834
  } else {
@@ -25559,6 +25873,7 @@ function registerBoardCommands(program3) {
25559
25873
  repo: o.repo,
25560
25874
  allowPartial: o.allowPartial
25561
25875
  });
25876
+ if (bulk.results.some((r) => r.moved)) invalidateStatuslineBoardCache();
25562
25877
  if (o.json) {
25563
25878
  console.log(JSON.stringify(bulk.results));
25564
25879
  } else {
@@ -25583,6 +25898,7 @@ function registerBoardCommands(program3) {
25583
25898
  if (issueRefs.length === 1) {
25584
25899
  try {
25585
25900
  const result = await moveBoardItem({ config: await loadConfigForBoardSelector2(issueRefs[0], o.repo), selector: issueRefs[0], status: canonicalStatus, repo: o.repo, allowPartial: o.allowPartial });
25901
+ invalidateStatuslineBoardCache();
25586
25902
  if (o.json) return console.log(JSON.stringify(result));
25587
25903
  console.log(result.partial ? `Partially moved ${result.item.ref}: ${result.warning}` : `Moved ${result.item.ref} -> ${result.status}`);
25588
25904
  } catch (e) {
@@ -25634,6 +25950,7 @@ function registerBoardCommands(program3) {
25634
25950
  force: o.force,
25635
25951
  allowPartial: o.allowPartial
25636
25952
  });
25953
+ invalidateStatuslineBoardCache();
25637
25954
  if (o.json) return console.log(JSON.stringify(result));
25638
25955
  console.log(result.partial ? `Partially unclaimed ${result.item.ref}: ${result.warning}` : `Unclaimed ${result.item.ref} -> ${result.status}`);
25639
25956
  } catch (e) {
@@ -25668,11 +25985,11 @@ function registerBoardCommands(program3) {
25668
25985
  }
25669
25986
 
25670
25987
  // src/merge-cleanup.ts
25671
- var import_node_fs33 = require("node:fs");
25988
+ var import_node_fs34 = require("node:fs");
25672
25989
  var import_promises8 = require("node:fs/promises");
25673
- var import_node_path32 = require("node:path");
25990
+ var import_node_path33 = require("node:path");
25674
25991
  var import_node_os12 = require("node:os");
25675
- var import_node_child_process16 = require("node:child_process");
25992
+ var import_node_child_process17 = require("node:child_process");
25676
25993
 
25677
25994
  // src/board-advance.ts
25678
25995
  function repoOf2(ref) {
@@ -25758,7 +26075,7 @@ function boardAdvanceFailureMessage(result) {
25758
26075
 
25759
26076
  // src/deferred-registry-store.ts
25760
26077
  var import_promises7 = require("node:fs/promises");
25761
- var import_node_path31 = require("node:path");
26078
+ var import_node_path32 = require("node:path");
25762
26079
  var sleep2 = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
25763
26080
  async function atomicWrite(target, contents) {
25764
26081
  const tmp = `${target}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
@@ -25809,12 +26126,12 @@ function makeDeferredWorktreeStore(registryPath, lockOpts = {}) {
25809
26126
  },
25810
26127
  // Standalone atomic write — THROWS on failure (no best-effort swallow, #2846).
25811
26128
  write: async (entries) => {
25812
- await (0, import_promises7.mkdir)((0, import_node_path31.dirname)(registryPath), { recursive: true });
26129
+ await (0, import_promises7.mkdir)((0, import_node_path32.dirname)(registryPath), { recursive: true });
25813
26130
  await atomicWrite(registryPath, serializeDeferredWorktrees(entries));
25814
26131
  },
25815
26132
  // Serialized read-modify-write under the repo-wide lock (#2846).
25816
26133
  update: async (mutate) => {
25817
- await (0, import_promises7.mkdir)((0, import_node_path31.dirname)(registryPath), { recursive: true });
26134
+ await (0, import_promises7.mkdir)((0, import_node_path32.dirname)(registryPath), { recursive: true });
25818
26135
  const deadline = Date.now() + opts.maxWaitMs;
25819
26136
  for (; ; ) {
25820
26137
  const guard = await acquireLock(lockPath, opts, deadline);
@@ -25955,6 +26272,22 @@ function assertPrMergeHousekeepingClean(startingPath, context, options = {}) {
25955
26272
  if (verdict.blocked) throw new Error(verdict.reason);
25956
26273
  return housekeeping;
25957
26274
  }
26275
+ async function bestEffortLeaseClose(wtPath, exec = (cmd, args) => execFileP2(cmd, args, { timeout: GIT_TIMEOUT_MS })) {
26276
+ const step = "close jerv worktree lease";
26277
+ try {
26278
+ await exec("jerv-cli", ["lease", "close", "--ref", wtPath]);
26279
+ return { step, status: "done" };
26280
+ } catch (e) {
26281
+ const err = e;
26282
+ const detail = `${err.message}
26283
+ ${err.stderr ?? ""}`;
26284
+ if (/ENOENT|not found|not recognized/i.test(detail)) {
26285
+ return { step, status: "skipped: jerv-cli not on PATH" };
26286
+ }
26287
+ const msg = (err.stderr?.trim() || err.message).split("\n")[0];
26288
+ return { step, status: `failed: ${msg}` };
26289
+ }
26290
+ }
25958
26291
  async function applyGcPlan(plan, remote, opts = {}) {
25959
26292
  const result = { removedBranches: [], removedRemoteBranches: [], removedTrackingRefs: [], removedWorktreeDirs: [], refused: [], failed: [], pruned: false };
25960
26293
  const beforeWorktrees = parseWorktreePorcelain(
@@ -25962,7 +26295,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
25962
26295
  );
25963
26296
  const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
25964
26297
  const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
25965
- const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path32.dirname)((0, import_node_path32.dirname)(worktreeGitRoot)) : repoRoot2;
26298
+ const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path33.dirname)((0, import_node_path33.dirname)(worktreeGitRoot)) : repoRoot2;
25966
26299
  const gcActor = describeActor({ env: process.env, surface: detectSurface(process.env), cwd: process.cwd() });
25967
26300
  const owners = readWorktreeOwners(primaryRepoRoot);
25968
26301
  const removalNow = Date.now();
@@ -25993,7 +26326,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
25993
26326
  const cleanup = await cleanupPrMergeLocalBranch(branch.branch, {
25994
26327
  beforeWorktrees,
25995
26328
  startingPath: branch.worktreePath,
25996
- pathExists: (p) => (0, import_node_fs33.existsSync)(p),
26329
+ pathExists: (p) => (0, import_node_fs34.existsSync)(p),
25997
26330
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
25998
26331
  teardownWorktreeStage,
25999
26332
  deferredStore,
@@ -26003,6 +26336,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
26003
26336
  removeWorktreeDir: wtDeps.removeWorktreeDir,
26004
26337
  removalContext: { primaryRoot: primaryRepoRoot, actor: gcActor, command: "worktree gc", force: opts.force }
26005
26338
  });
26339
+ if (cleanup.worktree?.status === "removed") await bestEffortLeaseClose(cleanup.worktree.path);
26006
26340
  return cleanup;
26007
26341
  },
26008
26342
  cleanupRemoteBranch: (branch, expectedHeadOid) => deleteReviewedRemoteBranch(remote, branch.branch, expectedHeadOid),
@@ -26021,7 +26355,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
26021
26355
  let removalAttempted = false;
26022
26356
  try {
26023
26357
  const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
26024
- realpath: (path2) => (0, import_node_fs33.realpathSync)(path2)
26358
+ realpath: (path2) => (0, import_node_fs34.realpathSync)(path2)
26025
26359
  });
26026
26360
  if (!cleanupTarget.ok) {
26027
26361
  result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
@@ -26049,6 +26383,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
26049
26383
  owner,
26050
26384
  reason: owner ? "branch merged/closed or directory dead; owner registration was stale" : "branch merged/closed or directory dead; no owner registered"
26051
26385
  });
26386
+ await bestEffortLeaseClose(wt.path);
26052
26387
  } catch (e) {
26053
26388
  const error = e.message.split("\n")[0];
26054
26389
  result.failed.push(`${wt.path}: ${error}`);
@@ -26107,13 +26442,13 @@ async function composeOverrideBodyFile(prNumber, repoArgs, gh) {
26107
26442
  const commits = JSON.parse(raw).commits ?? [];
26108
26443
  const body = squashBodyWithOverride(commits.map((c) => ({ headline: c.messageHeadline ?? "", body: c.messageBody ?? "" })), process.cwd());
26109
26444
  if (!body) return void 0;
26110
- const dir = (0, import_node_fs33.mkdtempSync)((0, import_node_path32.join)((0, import_node_os12.tmpdir)(), "mmi-squash-body-"));
26111
- const path2 = (0, import_node_path32.join)(dir, "body.txt");
26112
- (0, import_node_fs33.writeFileSync)(path2, `${body}
26445
+ const dir = (0, import_node_fs34.mkdtempSync)((0, import_node_path33.join)((0, import_node_os12.tmpdir)(), "mmi-squash-body-"));
26446
+ const path2 = (0, import_node_path33.join)(dir, "body.txt");
26447
+ (0, import_node_fs34.writeFileSync)(path2, `${body}
26113
26448
  `, "utf8");
26114
26449
  return { path: path2, cleanup: () => {
26115
26450
  try {
26116
- (0, import_node_fs33.rmSync)(dir, { recursive: true, force: true });
26451
+ (0, import_node_fs34.rmSync)(dir, { recursive: true, force: true });
26117
26452
  } catch {
26118
26453
  }
26119
26454
  } };
@@ -26221,7 +26556,7 @@ async function remoteBranchExists2(branch, options = {}) {
26221
26556
  }
26222
26557
  var COMPOSE_TIMEOUT_MS = 12e4;
26223
26558
  function spawnDeferredGcSweep() {
26224
- spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process16.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
26559
+ spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process17.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
26225
26560
  }
26226
26561
  async function createDeferredWorktreeStore() {
26227
26562
  try {
@@ -26235,13 +26570,13 @@ var realWorktreeDirRemover = {
26235
26570
  probe: (p) => {
26236
26571
  let st;
26237
26572
  try {
26238
- st = (0, import_node_fs33.lstatSync)(p);
26573
+ st = (0, import_node_fs34.lstatSync)(p);
26239
26574
  } catch {
26240
26575
  return null;
26241
26576
  }
26242
26577
  if (st.isSymbolicLink()) return "link";
26243
26578
  try {
26244
- (0, import_node_fs33.readlinkSync)(p);
26579
+ (0, import_node_fs34.readlinkSync)(p);
26245
26580
  return "link";
26246
26581
  } catch {
26247
26582
  }
@@ -26249,7 +26584,7 @@ var realWorktreeDirRemover = {
26249
26584
  },
26250
26585
  readdir: (p) => {
26251
26586
  try {
26252
- return (0, import_node_fs33.readdirSync)(p);
26587
+ return (0, import_node_fs34.readdirSync)(p);
26253
26588
  } catch {
26254
26589
  return [];
26255
26590
  }
@@ -26258,9 +26593,9 @@ var realWorktreeDirRemover = {
26258
26593
  // leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
26259
26594
  detachLink: (p) => {
26260
26595
  try {
26261
- (0, import_node_fs33.rmdirSync)(p);
26596
+ (0, import_node_fs34.rmdirSync)(p);
26262
26597
  } catch {
26263
- (0, import_node_fs33.unlinkSync)(p);
26598
+ (0, import_node_fs34.unlinkSync)(p);
26264
26599
  }
26265
26600
  },
26266
26601
  removeTree: (p) => (0, import_promises8.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
@@ -26293,9 +26628,9 @@ async function worktreeHasStageState(worktreePath) {
26293
26628
  }
26294
26629
  }
26295
26630
  function stageStateFileBelongsToWorktree(statePath, worktreePath) {
26296
- if (!(0, import_node_fs33.existsSync)(statePath)) return false;
26631
+ if (!(0, import_node_fs34.existsSync)(statePath)) return false;
26297
26632
  try {
26298
- const state = JSON.parse((0, import_node_fs33.readFileSync)(statePath, "utf8"));
26633
+ const state = JSON.parse((0, import_node_fs34.readFileSync)(statePath, "utf8"));
26299
26634
  const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
26300
26635
  return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
26301
26636
  } catch {
@@ -26626,10 +26961,11 @@ async function checkDocsIndexAtHead(opts, deps) {
26626
26961
  }
26627
26962
 
26628
26963
  // src/worktree-lifecycle-commands.ts
26629
- var import_node_fs34 = require("node:fs");
26964
+ var import_node_fs35 = require("node:fs");
26630
26965
  var import_promises9 = require("node:fs/promises");
26631
- var import_node_path33 = require("node:path");
26966
+ var import_node_path34 = require("node:path");
26632
26967
  var GH_TIMEOUT_MS = 2e4;
26968
+ var STALE_PR_LOOKUP_LIMIT = 20;
26633
26969
  var DEFAULT_BASE = "origin/development";
26634
26970
  var DEFAULT_REMOTE = "origin";
26635
26971
  var PROTECTED_BRANCHES2 = /* @__PURE__ */ new Set(["development", "main", "master", "rc"]);
@@ -26774,7 +27110,7 @@ function classifyStaleLeaks(input) {
26774
27110
  var defaultOrphanDirScanDeps = {
26775
27111
  listDirs: (root) => {
26776
27112
  try {
26777
- return (0, import_node_fs34.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path33.join)(root, e.name));
27113
+ return (0, import_node_fs35.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path34.join)(root, e.name));
26778
27114
  } catch {
26779
27115
  return [];
26780
27116
  }
@@ -26789,13 +27125,16 @@ function scanOrphanDirs(worktreesRoot, worktreeGitRoot, deps = defaultOrphanDirS
26789
27125
  }
26790
27126
  return candidates;
26791
27127
  }
26792
- function formatStaleLeaks(leaks) {
26793
- if (!leaks.length) return "worktree list --stale: no leaks found";
26794
- const lines = [`worktree list --stale: ${leaks.length} leak(s)`];
27128
+ function formatStaleLeaks(leaks, prLookupFailures = []) {
27129
+ const lines = leaks.length ? [`worktree list --stale: ${leaks.length} leak(s)`] : ["worktree list --stale: no leaks found"];
26795
27130
  for (const leak of leaks) {
26796
27131
  lines.push(` [${leak.kind}] ${leak.ref} \u2014 ${leak.detail}`);
26797
27132
  lines.push(` fix: ${leak.remediation}`);
26798
27133
  }
27134
+ if (prLookupFailures.length) {
27135
+ lines.push(` INCOMPLETE: merge state could not be read for ${prLookupFailures.length} branch(es) \u2014 they are NOT covered above`);
27136
+ for (const f of prLookupFailures) lines.push(` ${f.branch}${f.detail ? ` \u2014 ${f.detail}` : ""}`);
27137
+ }
26799
27138
  return lines.join("\n");
26800
27139
  }
26801
27140
  async function repoRootOf() {
@@ -26927,13 +27266,13 @@ function registerWorktreeCommands(program3) {
26927
27266
  const detached = headBorn && !symbolicBranch;
26928
27267
  const branch = symbolicBranch || (detached ? "HEAD" : "");
26929
27268
  if (!wtPath || !branch) return fail("worktree land: not inside a git worktree");
26930
- const gitFile = (0, import_node_path33.join)(wtPath, ".git");
26931
- const isLinked = (0, import_node_fs34.existsSync)(gitFile) && (0, import_node_fs34.statSync)(gitFile).isFile();
27269
+ const gitFile = (0, import_node_path34.join)(wtPath, ".git");
27270
+ const isLinked = (0, import_node_fs35.existsSync)(gitFile) && (0, import_node_fs35.statSync)(gitFile).isFile();
26932
27271
  if (apply && !isLinked) {
26933
27272
  return fail("worktree land: run from inside the linked worktree you want to land (this is the primary checkout)");
26934
27273
  }
26935
27274
  const commonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
26936
- const primaryCheckout = commonDir ? (0, import_node_path33.dirname)(commonDir) : wtPath;
27275
+ const primaryCheckout = commonDir ? (0, import_node_path34.dirname)(commonDir) : wtPath;
26937
27276
  const localBranchNames = await execFileP2("git", ["-C", primaryCheckout, "for-each-ref", "--format=%(refname:short)", "refs/heads"], { timeout: GIT_TIMEOUT_MS }).then(({ stdout }) => new Set((stdout || "").split("\n").map((l) => l.trim()).filter(Boolean))).catch(() => void 0);
26938
27277
  const orphan = classifyOrphanedWorktree({
26939
27278
  branch,
@@ -27066,6 +27405,7 @@ function registerWorktreeCommands(program3) {
27066
27405
  }
27067
27406
  }
27068
27407
  report.push(await bestEffortGit(["worktree", "prune"], primaryCheckout, "prune worktree metadata"));
27408
+ report.push(await bestEffortLeaseClose(wtPath));
27069
27409
  const result = {
27070
27410
  dryRun: false,
27071
27411
  ...plan,
@@ -27096,8 +27436,9 @@ function registerWorktreeCommands(program3) {
27096
27436
  const ctx = await gatherWorktreeContext();
27097
27437
  if (o.stale) {
27098
27438
  const leaks = classifyStaleLeaks(ctx);
27099
- if (o.json) return console.log(JSON.stringify({ stale: leaks, count: leaks.length }, null, 2));
27100
- return console.log(formatStaleLeaks(leaks));
27439
+ const failures = ctx.prLookupFailures ?? [];
27440
+ if (o.json) return console.log(JSON.stringify({ stale: leaks, count: leaks.length, prLookupFailures: failures, complete: failures.length === 0 }, null, 2));
27441
+ return console.log(formatStaleLeaks(leaks, failures));
27101
27442
  }
27102
27443
  if (o.json) return console.log(JSON.stringify({ worktrees: ctx.worktrees }, null, 2));
27103
27444
  if (!ctx.worktrees.length) return console.log("worktree list: no worktrees");
@@ -27121,22 +27462,18 @@ async function gatherWorktreeContext() {
27121
27462
  const branchOut = (await execFileP2("git", ["branch", "--format=%(refname:short)"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout;
27122
27463
  const localBranches = branchOut.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
27123
27464
  const currentBranch2 = (await execFileP2("git", ["rev-parse", "--abbrev-ref", "HEAD"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || void 0;
27465
+ const { prs, failures: prLookupFailures } = await resolveBranchPrs(localBranches.filter((b) => !PROTECTED_BRANCHES2.has(b)), STALE_PR_LOOKUP_LIMIT);
27124
27466
  const openPrBranches = /* @__PURE__ */ new Set();
27125
27467
  const closedBranches = /* @__PURE__ */ new Set();
27126
- try {
27127
- const { stdout } = await execFileP2("gh", ["pr", "list", "--state", "all", "--limit", "200", "--json", "headRefName,state"], { timeout: GH_TIMEOUT_MS });
27128
- const prs = JSON.parse(stdout || "[]");
27129
- const byBranch = /* @__PURE__ */ new Map();
27130
- for (const pr2 of prs) {
27131
- const arr = byBranch.get(pr2.headRefName) ?? [];
27132
- arr.push(pr2.state);
27133
- byBranch.set(pr2.headRefName, arr);
27134
- }
27135
- for (const [br, states] of byBranch) {
27136
- if (states.some((s) => s === "OPEN")) openPrBranches.add(br);
27137
- else if (states.some((s) => s === "MERGED" || s === "CLOSED")) closedBranches.add(br);
27138
- }
27139
- } catch {
27468
+ const byBranch = /* @__PURE__ */ new Map();
27469
+ for (const pr2 of prs) {
27470
+ const arr = byBranch.get(pr2.headRefName) ?? [];
27471
+ arr.push(pr2.state);
27472
+ byBranch.set(pr2.headRefName, arr);
27473
+ }
27474
+ for (const [br, states] of byBranch) {
27475
+ if (states.some((s) => s === "OPEN")) openPrBranches.add(br);
27476
+ else if (states.some((s) => s === "MERGED" || s === "CLOSED")) closedBranches.add(br);
27140
27477
  }
27141
27478
  const stages = [];
27142
27479
  for (const wt of worktrees) {
@@ -27144,16 +27481,16 @@ async function gatherWorktreeContext() {
27144
27481
  if (s) stages.push({ path: wt.path, port: s.port });
27145
27482
  }
27146
27483
  const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
27147
- const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path33.dirname)((0, import_node_path33.dirname)(worktreeGitRoot)) : repoRoot2;
27484
+ const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path34.dirname)((0, import_node_path34.dirname)(worktreeGitRoot)) : repoRoot2;
27148
27485
  const wtRoot = siblingMmiWorktreesRoot(primaryRepoRoot);
27149
27486
  let orphanDirs = [];
27150
- if ((0, import_node_fs34.existsSync)(wtRoot)) {
27487
+ if ((0, import_node_fs35.existsSync)(wtRoot)) {
27151
27488
  orphanDirs = scanOrphanDirs(wtRoot, worktreeGitRoot, {
27152
27489
  ...defaultOrphanDirScanDeps,
27153
27490
  listDirs: (root) => worktreeScanDirs(root, primaryRepoRoot, defaultOrphanDirScanDeps.listDirs, isRepoCheckoutDir)
27154
27491
  });
27155
27492
  }
27156
- return { worktrees, localBranches, currentBranch: currentBranch2, openPrBranches, closedBranches, stages, orphanDirs };
27493
+ return { worktrees, localBranches, currentBranch: currentBranch2, openPrBranches, closedBranches, stages, orphanDirs, prLookupFailures };
27157
27494
  }
27158
27495
  async function bestEffortGit(args, cwd, step, timeoutMs = GIT_TIMEOUT_MS) {
27159
27496
  try {
@@ -27173,8 +27510,8 @@ ${err.stderr ?? ""}`;
27173
27510
  }
27174
27511
 
27175
27512
  // src/issue-commands.ts
27176
- var import_node_fs35 = require("node:fs");
27177
- var import_node_crypto6 = require("node:crypto");
27513
+ var import_node_fs36 = require("node:fs");
27514
+ var import_node_crypto7 = require("node:crypto");
27178
27515
  var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
27179
27516
  var ReparentConflictError = class extends Error {
27180
27517
  constructor(message, payload) {
@@ -27191,7 +27528,7 @@ async function editIssue(client, options, deps = {}) {
27191
27528
  const url = `https://github.com/${repo}/issues/${parsed.number}`;
27192
27529
  const patch = {};
27193
27530
  let bodyChanged = false;
27194
- const textDeps = () => deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs35.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
27531
+ const textDeps = () => deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs36.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
27195
27532
  if (options.titleFile !== void 0) {
27196
27533
  patch.title = await resolveIssueTitle({ title: options.title, titleFile: options.titleFile }, textDeps());
27197
27534
  } else if (options.title !== void 0) {
@@ -27250,6 +27587,44 @@ async function editIssue(client, options, deps = {}) {
27250
27587
  ...parentResult ? { parent: parentResult } : {}
27251
27588
  };
27252
27589
  }
27590
+ var EVIDENCE_COMMENT_URL_RE = /^https:\/\/github\.com\/([\w.-]+\/[\w.-]+)\/issues\/(\d+)#issuecomment-(\d+)$/;
27591
+ async function verifyCloseEvidence(client, evidence, repo, issueNumber, duplicateOf) {
27592
+ const match = evidence.trim().match(EVIDENCE_COMMENT_URL_RE);
27593
+ if (!match) {
27594
+ throw new Error(
27595
+ `--evidence must be a canonical issue-comment URL (https://github.com/<owner>/<repo>/issues/<n>#issuecomment-<id>), got: ${evidence}`
27596
+ );
27597
+ }
27598
+ const [, evidenceRepo, evidenceIssue, commentId] = match;
27599
+ if (evidenceRepo.toLowerCase() !== repo.toLowerCase() || Number(evidenceIssue) !== issueNumber) {
27600
+ throw new Error(
27601
+ `--evidence comment belongs to ${evidenceRepo}#${evidenceIssue}, not the issue being closed (${repo}#${issueNumber})`
27602
+ );
27603
+ }
27604
+ let comment;
27605
+ try {
27606
+ comment = await client.rest(
27607
+ "GET",
27608
+ `repos/${repo}/issues/comments/${commentId}`
27609
+ );
27610
+ } catch (e) {
27611
+ if (e instanceof GitHubApiError && e.status === 404) {
27612
+ throw new Error(`--evidence comment ${commentId} does not exist on ${repo} \u2014 nothing to anchor the close to`);
27613
+ }
27614
+ throw e;
27615
+ }
27616
+ const expectedAnchor = `/issues/${issueNumber}#issuecomment-${commentId}`;
27617
+ if (!comment.html_url || !comment.html_url.toLowerCase().endsWith(expectedAnchor.toLowerCase())) {
27618
+ throw new Error(
27619
+ `--evidence comment ${commentId} is not a comment on ${repo}#${issueNumber} (it lives at ${comment.html_url ?? "unknown"})`
27620
+ );
27621
+ }
27622
+ if (duplicateOf !== void 0 && !new RegExp(`#${duplicateOf}\\b`).test(comment.body ?? "")) {
27623
+ throw new Error(
27624
+ `--reason duplicate-of ${duplicateOf}: the --evidence comment must cite #${duplicateOf} so the duplicate has an auditable destination`
27625
+ );
27626
+ }
27627
+ }
27253
27628
  async function closeIssue(client, options, deps = {}) {
27254
27629
  const parsed = parseIssueRef(options.ref);
27255
27630
  const repo = parsed.repo ?? options.defaultRepo;
@@ -27257,6 +27632,9 @@ async function closeIssue(client, options, deps = {}) {
27257
27632
  const url = `https://github.com/${repo}/issues/${parsed.number}`;
27258
27633
  const reason = options.reason ?? "completed";
27259
27634
  const stateReason = reason === "duplicate-of" ? "not_planned" : reason === "not-planned" ? "not_planned" : "completed";
27635
+ if (options.evidence !== void 0) {
27636
+ await verifyCloseEvidence(client, options.evidence, repo, parsed.number, reason === "duplicate-of" ? options.duplicateOf : void 0);
27637
+ }
27260
27638
  await client.rest("PATCH", `repos/${repo}/issues/${parsed.number}`, {
27261
27639
  body: { state: "closed", state_reason: stateReason }
27262
27640
  });
@@ -27424,7 +27802,7 @@ function rowIdempotencyKey(batchKey, spec) {
27424
27802
  const identity = `${spec.type}
27425
27803
  ${spec.title.trim()}
27426
27804
  ${spec.body ?? ""}`;
27427
- const hash = (0, import_node_crypto6.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
27805
+ const hash = (0, import_node_crypto7.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
27428
27806
  return `${batchKey}:${hash}`;
27429
27807
  }
27430
27808
  var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo", "surface"]);
@@ -27643,7 +28021,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
27643
28021
  }
27644
28022
  });
27645
28023
  mutating(
27646
- issue2.command("close <ref>").description("close an issue and move its board item to Done (--reason completed|not-planned|duplicate-of --duplicate-of <n>)").option("--reason <reason>", "completed | not-planned | duplicate-of (defaults to completed)").option("--duplicate-of <number>", "the issue number this duplicates (use with --reason duplicate-of)").option("--comment <text>", "a closing comment to post").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)"),
28024
+ issue2.command("close <ref>").description("close an issue and move its board item to Done (--reason completed|not-planned|duplicate-of --duplicate-of <n>)").option("--reason <reason>", "completed | not-planned | duplicate-of (defaults to completed)").option("--duplicate-of <number>", "the issue number this duplicates (use with --reason duplicate-of)").option("--comment <text>", "a closing comment to post").option("--evidence <comment-url>", "URL of an existing comment on this issue carrying the close evidence \u2014 verified before closing (JPT#4668; the sanctioned agent-shell close shape)").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)"),
27647
28025
  (opts, args) => {
27648
28026
  let reason;
27649
28027
  try {
@@ -27672,7 +28050,8 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
27672
28050
  defaultRepo,
27673
28051
  reason: parsed.reason,
27674
28052
  duplicateOf: o.duplicateOf ? Number(o.duplicateOf) : void 0,
27675
- comment: o.comment
28053
+ comment: o.comment,
28054
+ evidence: o.evidence
27676
28055
  });
27677
28056
  console.log(JSON.stringify(result));
27678
28057
  } catch (e) {
@@ -27754,7 +28133,7 @@ function extendCreateCommand(issue2, batchAttach) {
27754
28133
  if (opts.batch) {
27755
28134
  let specs;
27756
28135
  try {
27757
- const raw = (0, import_node_fs35.readFileSync)(opts.batch, "utf8");
28136
+ const raw = (0, import_node_fs36.readFileSync)(opts.batch, "utf8");
27758
28137
  specs = JSON.parse(raw);
27759
28138
  if (!Array.isArray(specs)) throw new Error("batch file must contain a JSON array");
27760
28139
  } catch (e) {
@@ -27803,6 +28182,7 @@ ${lines}`, {
27803
28182
  surface: batchSurface,
27804
28183
  noSurface: batchNoSurface
27805
28184
  }, { attach: batchAttach });
28185
+ if (result.created.some((row) => !row.idempotent)) invalidateStatuslineBoardCache();
27806
28186
  console.log(JSON.stringify(result));
27807
28187
  if (result.failures.length) process.exitCode = 1;
27808
28188
  } catch (e) {
@@ -27828,8 +28208,8 @@ ${lines}`, {
27828
28208
  }
27829
28209
 
27830
28210
  // src/train-commands.ts
27831
- var import_node_fs36 = require("node:fs");
27832
- var import_node_path34 = require("node:path");
28211
+ var import_node_fs37 = require("node:fs");
28212
+ var import_node_path35 = require("node:path");
27833
28213
 
27834
28214
  // src/train-status.ts
27835
28215
  function buildTrainStatusReport(input) {
@@ -27869,7 +28249,7 @@ function formatTrainStatus(r) {
27869
28249
  // src/train-commands.ts
27870
28250
  function readRepoVersion() {
27871
28251
  try {
27872
- return JSON.parse((0, import_node_fs36.readFileSync)((0, import_node_path34.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
28252
+ return JSON.parse((0, import_node_fs37.readFileSync)((0, import_node_path35.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
27873
28253
  } catch {
27874
28254
  return void 0;
27875
28255
  }
@@ -28015,9 +28395,9 @@ function registerDeployCommands(program3) {
28015
28395
  }
28016
28396
 
28017
28397
  // src/discovery-commands.ts
28018
- var import_node_fs37 = require("node:fs");
28398
+ var import_node_fs38 = require("node:fs");
28019
28399
  var import_node_os13 = require("node:os");
28020
- var import_node_path35 = require("node:path");
28400
+ var import_node_path36 = require("node:path");
28021
28401
  var GC_GH_TIMEOUT_MS3 = 2e4;
28022
28402
  async function collectStatus() {
28023
28403
  const repo = await resolveRepo();
@@ -28207,8 +28587,8 @@ async function collectOnboardStatus(opts = {}) {
28207
28587
  }
28208
28588
  const home = (0, import_node_os13.homedir)();
28209
28589
  const plugin = onboardPluginGate({
28210
- readKnown: () => readFileSyncSafe((0, import_node_path35.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs37.readFileSync),
28211
- readSettings: () => readFileSyncSafe((0, import_node_path35.join)(home, ".claude", "settings.json"), import_node_fs37.readFileSync)
28590
+ readKnown: () => readFileSyncSafe((0, import_node_path36.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs38.readFileSync),
28591
+ readSettings: () => readFileSyncSafe((0, import_node_path36.join)(home, ".claude", "settings.json"), import_node_fs38.readFileSync)
28212
28592
  });
28213
28593
  return { track, board, registry: registry2, secrets, plugin, estateCli, nextCommand };
28214
28594
  }
@@ -29985,6 +30365,10 @@ function surfaceRestartAction(descriptor) {
29985
30365
  }
29986
30366
 
29987
30367
  // src/doctor-clean.ts
30368
+ function doctorSnapshotFingerprint(checks) {
30369
+ const rows = [...checks].sort((a, b) => `${a.id ?? a.label}`.localeCompare(`${b.id ?? b.label}`)).map((c) => [c.id ?? c.label, c.ok, c.verified ?? null, c.detail ?? null]);
30370
+ return JSON.stringify(rows);
30371
+ }
29988
30372
  function checkGithubAuth(probe) {
29989
30373
  const login = probe.login?.trim();
29990
30374
  const authed = Boolean(login);
@@ -30351,7 +30735,7 @@ function gcReapable(plan) {
30351
30735
  }
30352
30736
  async function runDoctorClean(opts, io, deps) {
30353
30737
  const full = !opts.fast && !opts.banner && !opts.preflight;
30354
- const applyEnv = full || Boolean(opts.preflight);
30738
+ const applyEnv = full;
30355
30739
  const applyRepo = full && opts.repoWrites !== false;
30356
30740
  const lane = {
30357
30741
  banner: Boolean(opts.banner),
@@ -30378,11 +30762,14 @@ async function runDoctorClean(opts, io, deps) {
30378
30762
  const checks = [];
30379
30763
  let restartPending = false;
30380
30764
  const streamed = /* @__PURE__ */ new Set();
30765
+ const streamedLines = /* @__PURE__ */ new Set();
30766
+ const alreadyShown = (c) => streamed.has(c) || streamedLines.has(renderCheckLine(c));
30381
30767
  const worthPrinting = (c) => Boolean(opts.verbose) || !c.ok || Boolean(c.warn);
30382
30768
  const emitNow = (check) => {
30383
30769
  checks.push(check);
30384
- if (opts.json || !worthPrinting(check)) return;
30770
+ if (opts.json || !streamingPass || !worthPrinting(check)) return;
30385
30771
  streamed.add(check);
30772
+ streamedLines.add(renderCheckLine(check));
30386
30773
  io.log(renderCheckLine(check));
30387
30774
  if (opts.verbose) for (const evidence of check.verbose ?? []) io.log(`${VERBOSE_INDENT}${evidence}`);
30388
30775
  };
@@ -30394,13 +30781,35 @@ async function runDoctorClean(opts, io, deps) {
30394
30781
  };
30395
30782
  const releasedNote = deps.releasedVersionNote?.();
30396
30783
  let pluginHealed = false;
30784
+ const spentHeals = /* @__PURE__ */ new Set();
30785
+ let healChangedThisPass = false;
30786
+ const markHealChanged = () => {
30787
+ healChangedThisPass = true;
30788
+ };
30789
+ const traceHeal = (kind) => opts.healTrace?.(kind);
30790
+ const spendOnce = (kind) => {
30791
+ if (spentHeals.has(kind)) return false;
30792
+ spentHeals.add(kind);
30793
+ return true;
30794
+ };
30795
+ const spentRows = /* @__PURE__ */ new Map();
30397
30796
  async function runPluginRow() {
30398
30797
  if (!registryEvidence) return;
30798
+ const spentRow = spentRows.get("plugin-chain");
30799
+ if (spentRow) {
30800
+ emitNow(spentRow);
30801
+ if (registryEvidence.descriptor.trustOwner === "operator" && (registryEvidence.guardState === "healthy" || pluginHealed)) {
30802
+ const trust = checkCodexHookTrust(deps.pluginTrustState?.(), registryEvidence.descriptor.displayName);
30803
+ if (trust) emitNow(trust);
30804
+ }
30805
+ return;
30806
+ }
30399
30807
  const diagnosis = diagnoseSurface({ ...registryEvidence, releasedVersion: released });
30400
30808
  const repair = planSurfaceRepair(diagnosis);
30401
- if (applyEnv && deps.healPlugin && repair?.supported) {
30809
+ if (applyEnv && deps.healPlugin && repair?.supported && spendOnce("plugin-chain")) {
30402
30810
  const { descriptor } = registryEvidence;
30403
30811
  healIntent(`${descriptor.displayName} plugin \u2014 healing via ${descriptor.installMechanism} (${descriptor.installLocator})`);
30812
+ traceHeal("plugin-chain");
30404
30813
  const heal = await deps.healPlugin(healStep);
30405
30814
  pluginHealed = heal.ok;
30406
30815
  const row = buildSurfaceDoctorCheck(diagnoseSurface({
@@ -30413,7 +30822,12 @@ async function runDoctorClean(opts, io, deps) {
30413
30822
  row.fix = `${heal.detail} \u2014 another doctor on this machine is healing it now; re-run once it finishes`;
30414
30823
  }
30415
30824
  emitNow(row);
30825
+ if (heal.skipped) spentHeals.delete("plugin-chain");
30416
30826
  if (!heal.skipped) restartPending = true;
30827
+ if (heal.ok && !heal.skipped) {
30828
+ markHealChanged();
30829
+ spentRows.set("plugin-chain", row);
30830
+ }
30417
30831
  } else if (diagnosis.state !== "skipped") {
30418
30832
  const row = buildSurfaceDoctorCheck(diagnosis);
30419
30833
  emitNow(row);
@@ -30425,13 +30839,18 @@ async function runDoctorClean(opts, io, deps) {
30425
30839
  }
30426
30840
  }
30427
30841
  async function runCliRow() {
30842
+ const spentRow = spentRows.get("cli-self-update");
30843
+ if (spentRow) {
30844
+ emitNow(spentRow);
30845
+ return;
30846
+ }
30428
30847
  const missing = deps.missingCliCommands?.() ?? [];
30429
30848
  const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
30430
30849
  const cliReport = buildVersionLagReport(cliInput);
30431
30850
  const capabilityGap = missing.length > 0 && Boolean(cliReport.releasedVersion);
30432
30851
  const shouldUpdate = Boolean(
30433
30852
  applyEnv && deps.updateCli && (versionAutoUpdateAction(cliReport) === "npm" || capabilityGap)
30434
- );
30853
+ ) && spendOnce("cli-self-update");
30435
30854
  if (!shouldUpdate) {
30436
30855
  const cli = checkCliVersion(cliInput, releasedNote);
30437
30856
  if (cli) {
@@ -30461,29 +30880,40 @@ async function runDoctorClean(opts, io, deps) {
30461
30880
  const target = cliReport.releasedVersion;
30462
30881
  const intent = capabilityGap && cliReport.ok ? `mmi-cli \u2014 self-updating to ${target} via npm install -g (missing commands: ${missing.join(", ")})` : `mmi-cli \u2014 self-updating ${cliReport.currentVersion} \u2192 ${target} via npm install -g`;
30463
30882
  healIntent(intent);
30883
+ traceHeal("cli-self-update");
30464
30884
  const heal = await deps.updateCli(target, healStep);
30885
+ if (heal.skipped) spentHeals.delete("cli-self-update");
30886
+ if (heal.ok && !heal.skipped) markHealChanged();
30465
30887
  const healEvidence = [
30466
30888
  `running: ${cliReport.currentVersion}`,
30467
30889
  `published: ${target ?? "(unknown)"}`,
30468
30890
  `heal: ${heal.detail}`,
30469
30891
  ...missing.length ? [`missing commands before heal: ${missing.join(", ")}`] : []
30470
30892
  ];
30471
- emitNow(heal.ok ? {
30472
- id: "cli-version",
30473
- ok: true,
30474
- label: "mmi-cli",
30475
- detail: capabilityGap && cliReport.ok ? `${cliReport.currentVersion} \u2192 ${target} \u2014 self-updated via npm install -g (was missing: ${missing.join(", ")}); the next mmi-cli invocation runs the new version` : `${cliReport.currentVersion} \u2192 ${target} \u2014 self-updated via npm install -g; the next mmi-cli invocation runs the new version`,
30476
- verbose: healEvidence
30477
- } : {
30478
- id: "cli-version",
30479
- ok: false,
30480
- label: "mmi-cli",
30481
- detail: capabilityGap && cliReport.ok ? `missing commands: ${missing.join(", ")}` : `${cliReport.currentVersion} \u2192 ${target}`,
30482
- // #3489: same split as the plugin heal above — a lock-contention skip is not this run's gap.
30483
- ...heal.skipped ? { reportOnly: true } : {},
30484
- fix: heal.skipped ? `${heal.detail} \u2014 another doctor on this machine is updating it now; re-run once it finishes` : `self-update failed (${heal.detail}) \u2014 run \`${cliUpdateCommand(target)}\``,
30485
- verbose: healEvidence
30486
- });
30893
+ if (heal.ok) {
30894
+ const healedRow = {
30895
+ id: "cli-version",
30896
+ ok: true,
30897
+ label: "mmi-cli",
30898
+ detail: capabilityGap && cliReport.ok ? `${cliReport.currentVersion} \u2192 ${target} \u2014 self-updated via npm install -g (was missing: ${missing.join(", ")}); the next mmi-cli invocation runs the new version` : `${cliReport.currentVersion} \u2192 ${target} \u2014 self-updated via npm install -g; the next mmi-cli invocation runs the new version`,
30899
+ verbose: healEvidence
30900
+ };
30901
+ spentRows.set("cli-self-update", healedRow);
30902
+ emitNow(healedRow);
30903
+ return;
30904
+ }
30905
+ emitNow(
30906
+ {
30907
+ id: "cli-version",
30908
+ ok: false,
30909
+ label: "mmi-cli",
30910
+ detail: capabilityGap && cliReport.ok ? `missing commands: ${missing.join(", ")}` : `${cliReport.currentVersion} \u2192 ${target}`,
30911
+ // #3489: same split as the plugin heal above — a lock-contention skip is not this run's gap.
30912
+ ...heal.skipped ? { reportOnly: true } : {},
30913
+ fix: heal.skipped ? `${heal.detail} \u2014 another doctor on this machine is updating it now; re-run once it finishes` : `self-update failed (${heal.detail}) \u2014 run \`${cliUpdateCommand(target)}\``,
30914
+ verbose: healEvidence
30915
+ }
30916
+ );
30487
30917
  }
30488
30918
  async function runGithubAuthRow() {
30489
30919
  emitNow(checkGithubAuth({ login, ghInstalled: ghInstalled2, reach }));
@@ -30505,6 +30935,8 @@ async function runDoctorClean(opts, io, deps) {
30505
30935
  if (gi.ok) {
30506
30936
  emitNow({ ok: true, id: "gitignore-block", label: "gitignore block", verbose: giEvidence });
30507
30937
  } else if (applyRepo && gi.content && deps.writeGitignore(gi.content)) {
30938
+ traceHeal("repo-cleans");
30939
+ markHealChanged();
30508
30940
  emitNow({ ok: true, id: "gitignore-block", label: "gitignore block", detail: "rewrote managed block", verbose: giEvidence });
30509
30941
  restartPending = true;
30510
30942
  } else {
@@ -30512,8 +30944,100 @@ async function runDoctorClean(opts, io, deps) {
30512
30944
  }
30513
30945
  }
30514
30946
  async function runPluginCacheRow() {
30947
+ if (applyEnv && deps.prunePluginCache && spendOnce("cache-prune")) {
30948
+ const cache = deps.pluginCache();
30949
+ if (cache.stale.length === 0 && cache.staging.length === 0) {
30950
+ emitNow(checkPluginCache(cache));
30951
+ return;
30952
+ }
30953
+ healIntent(`plugin cache \u2014 pruning ${cache.stale.length} superseded version(s) (guarded)`);
30954
+ traceHeal("cache-prune");
30955
+ const outcome = deps.prunePluginCache();
30956
+ if (outcome.removed.length) markHealChanged();
30957
+ const evidence = [
30958
+ ...outcome.removed.map((v) => `pruned: ${v}`),
30959
+ ...outcome.held.map((h) => `still held: ${h.version} (${h.error}) \u2014 never forced`),
30960
+ ...outcome.kept.map((k) => `kept: ${k.version} \u2014 ${k.reason}`)
30961
+ ];
30962
+ if (outcome.held.length === 0) {
30963
+ emitNow({
30964
+ id: "plugin-cache",
30965
+ ok: true,
30966
+ label: "plugin cache",
30967
+ detail: outcome.removed.length ? `pruned ${outcome.removed.length} superseded version(s)` : "nothing provably superseded (guards kept every candidate)",
30968
+ verbose: evidence
30969
+ });
30970
+ } else {
30971
+ emitNow({
30972
+ id: "plugin-cache",
30973
+ ok: false,
30974
+ reportOnly: true,
30975
+ label: "plugin cache",
30976
+ detail: `${outcome.removed.length} pruned; ${outcome.held.length} still held by a live session`,
30977
+ fix: "close the session holding it and re-run `mmi-cli doctor`",
30978
+ verbose: evidence
30979
+ });
30980
+ }
30981
+ return;
30982
+ }
30515
30983
  emitNow(checkPluginCache(deps.pluginCache()));
30516
30984
  }
30985
+ async function runPiPluginRow() {
30986
+ const state = deps.piPluginState?.();
30987
+ if (!state) return;
30988
+ if (!state.settingsReadable) {
30989
+ emitNow({
30990
+ id: "pi-plugin",
30991
+ ok: false,
30992
+ reportOnly: true,
30993
+ label: "pi plugin",
30994
+ detail: "settings.json unreadable",
30995
+ fix: "repair ~/.pi/agent/settings.json, then re-run doctor",
30996
+ verbose: ["~/.pi/agent exists but settings.json could not be parsed \u2014 fail closed, nothing written"]
30997
+ });
30998
+ return;
30999
+ }
31000
+ const current = state.registeredPath === state.expectedPath;
31001
+ if (current) {
31002
+ emitNow({
31003
+ id: "pi-plugin",
31004
+ ok: true,
31005
+ label: "pi plugin",
31006
+ detail: "package registered in settings.json",
31007
+ verbose: [`registered: ${state.registeredPath}`]
31008
+ });
31009
+ return;
31010
+ }
31011
+ if (applyEnv && deps.healPiPlugin) {
31012
+ healIntent(`pi plugin \u2014 registering ${state.expectedPath}`);
31013
+ traceHeal("env-heals");
31014
+ const heal = deps.healPiPlugin();
31015
+ if (heal.ok) markHealChanged();
31016
+ emitNow(heal.ok ? {
31017
+ id: "pi-plugin",
31018
+ ok: true,
31019
+ label: "pi plugin",
31020
+ detail: state.registeredPath ? "replaced stale package path" : "registered package in settings.json",
31021
+ verbose: [`was: ${state.registeredPath ?? "(absent)"}`, `now: ${state.expectedPath}`, `heal: ${heal.detail}`]
31022
+ } : {
31023
+ id: "pi-plugin",
31024
+ ok: false,
31025
+ label: "pi plugin",
31026
+ detail: state.registeredPath ? "stale package path" : "package not registered",
31027
+ fix: `heal failed (${heal.detail}) \u2014 add ${state.expectedPath} to packages[] in ~/.pi/agent/settings.json`,
31028
+ verbose: [`expected: ${state.expectedPath}`]
31029
+ });
31030
+ return;
31031
+ }
31032
+ emitNow({
31033
+ id: "pi-plugin",
31034
+ ok: false,
31035
+ label: "pi plugin",
31036
+ detail: state.registeredPath ? "stale package path" : "package not registered",
31037
+ fix: "run `mmi-cli doctor` to register the mmi .pi-plugin in ~/.pi/agent/settings.json",
31038
+ verbose: [`expected: ${state.expectedPath}`, `registered: ${state.registeredPath ?? "(absent)"}`]
31039
+ });
31040
+ }
30517
31041
  async function runSessionPayloadRow() {
30518
31042
  const payload = checkSessionPayload(deps.sessionPayload());
30519
31043
  if (payload) emitNow(payload);
@@ -30523,7 +31047,11 @@ async function runDoctorClean(opts, io, deps) {
30523
31047
  const healed = deps.healMarketplacePins();
30524
31048
  if (healed) {
30525
31049
  healIntent(`marketplace pins \u2014 ${healed.detail}`);
30526
- if (healed.wrote) restartPending = true;
31050
+ if (healed.wrote) {
31051
+ traceHeal("env-heals");
31052
+ markHealChanged();
31053
+ restartPending = true;
31054
+ }
30527
31055
  }
30528
31056
  }
30529
31057
  for (const row of deps.marketplaceRows()) emitNow(row);
@@ -30566,6 +31094,8 @@ async function runDoctorClean(opts, io, deps) {
30566
31094
  let healedWrite = false;
30567
31095
  if (applyRepo && probe?.drift && deps.healDocsIndex) {
30568
31096
  healIntent("docs index \u2014 regenerating docs/index.md");
31097
+ traceHeal("repo-cleans");
31098
+ markHealChanged();
30569
31099
  try {
30570
31100
  probe = deps.healDocsIndex(root);
30571
31101
  healedWrite = !probe.drift;
@@ -30786,7 +31316,11 @@ async function runDoctorClean(opts, io, deps) {
30786
31316
  `skipped: ${sweep.skipped}`
30787
31317
  ]
30788
31318
  });
30789
- if (sweep.removed.length) restartPending = true;
31319
+ if (sweep.removed.length) {
31320
+ traceHeal("repo-cleans");
31321
+ markHealChanged();
31322
+ restartPending = true;
31323
+ }
30790
31324
  } catch (e) {
30791
31325
  const message = e instanceof Error ? e.message : String(e);
30792
31326
  emitNow({
@@ -30824,7 +31358,11 @@ async function runDoctorClean(opts, io, deps) {
30824
31358
  ...r.failed.map((f) => `FAILED: ${f}`)
30825
31359
  ] : ["nothing reapable"]
30826
31360
  });
30827
- if (reaped) restartPending = true;
31361
+ if (reaped) {
31362
+ traceHeal("repo-cleans");
31363
+ markHealChanged();
31364
+ restartPending = true;
31365
+ }
30828
31366
  } else {
30829
31367
  const n = gcReapable(plan);
30830
31368
  const gcEvidence = [
@@ -30851,36 +31389,62 @@ async function runDoctorClean(opts, io, deps) {
30851
31389
  const applied = deps.executeScratchGc(repoRoot2, { apply: true });
30852
31390
  const pruned = applied.applied?.pruned.length ?? 0;
30853
31391
  emitNow({ id: "scratch", ok: true, label: "scratch", detail: pruned ? `removed ${pruned} aged item(s)` : "nothing stale", verbose: scratchEvidence });
30854
- if (pruned) restartPending = true;
31392
+ if (pruned) {
31393
+ traceHeal("repo-cleans");
31394
+ markHealChanged();
31395
+ restartPending = true;
31396
+ }
30855
31397
  } else {
30856
31398
  emitNow(scratch.plan.safeAuto.length === 0 ? { id: "scratch", ok: true, label: "scratch", verbose: scratchEvidence } : { id: "scratch", ok: false, label: "scratch", detail: `${scratch.plan.safeAuto.length} aged item(s)`, fix: "run `mmi-cli doctor` (without --no-repo-writes)", verbose: scratchEvidence });
30857
31399
  }
30858
31400
  }
30859
31401
  const prefix = [
30860
- { id: "plugin", when: true, run: runPluginRow },
30861
31402
  { id: "cli-version", when: true, run: runCliRow },
31403
+ { id: "plugin", when: true, run: runPluginRow },
31404
+ { id: "plugin-cache", when: true, run: runPluginCacheRow },
30862
31405
  { id: "github-auth", when: true, run: runGithubAuthRow },
30863
31406
  { id: "aws-identity", when: true, run: runAwsRow },
30864
31407
  { id: "repo-worktrees", when: true, run: runRepoWorktreesRow },
30865
- { id: "gitignore-block", when: isOrgRepo, run: runGitignoreRow },
30866
- { id: "plugin-cache", when: true, run: runPluginCacheRow },
30867
31408
  { id: "sessionstart-payload", when: true, run: runSessionPayloadRow },
30868
- { id: "marketplace", when: true, run: runMarketplaceRows }
31409
+ { id: "marketplace", when: true, run: runMarketplaceRows },
31410
+ { id: "pi-plugin", when: true, run: runPiPluginRow },
31411
+ { id: "gitignore-block", when: isOrgRepo, run: runGitignoreRow }
30869
31412
  ];
30870
- for (const entry of prefix) if (entry.when) await entry.run();
30871
- const parallel = [
30872
- { id: "docs-index", when: isOrgRepo && lane.full, run: runDocsIndexRow },
30873
- // Hub cloud probe (short timeout). Full lane + `--self`; `--preflight` may emit command-absent only (#4156).
30874
- { id: "repo-index", when: isOrgRepo && (lane.full || Boolean(opts.self) || lane.preflight), run: runRepoIndexRow },
30875
- { id: "board-doctor", when: isOrgRepo && lane.full, run: runBoardDoctorRow },
30876
- { id: "schedules-drift", when: isOrgRepo && lane.full, run: runSchedulesDriftRow }
30877
- ];
30878
- await Promise.all(parallel.filter((entry) => entry.when).map((entry) => entry.run()));
30879
- const suffix = [
30880
- { id: "train-branches", when: isOrgRepo && lane.full, run: runTrainSyncRow },
30881
- { id: "housekeeper", when: isOrgRepo && lane.full, run: runHousekeeperRows }
30882
- ];
30883
- for (const entry of suffix) if (entry.when) await entry.run();
31413
+ const maxPasses = 3;
31414
+ let passes = 0;
31415
+ let prevFingerprint;
31416
+ let streamingPass = true;
31417
+ for (; ; ) {
31418
+ passes += 1;
31419
+ opts.onPass?.(passes);
31420
+ checks.length = 0;
31421
+ healChangedThisPass = false;
31422
+ for (const entry of prefix) if (entry.when) await entry.run();
31423
+ const parallelRows = [
31424
+ { id: "docs-index", when: isOrgRepo && lane.full, run: runDocsIndexRow },
31425
+ // Hub cloud probe (short timeout). Full lane + `--self`; `--preflight` may emit command-absent only (#4156).
31426
+ { id: "repo-index", when: isOrgRepo && (lane.full || Boolean(opts.self) || lane.preflight), run: runRepoIndexRow },
31427
+ { id: "board-doctor", when: isOrgRepo && lane.full, run: runBoardDoctorRow },
31428
+ { id: "schedules-drift", when: isOrgRepo && lane.full, run: runSchedulesDriftRow }
31429
+ ];
31430
+ await Promise.all(parallelRows.filter((entry) => entry.when).map((entry) => entry.run()));
31431
+ const suffix = [
31432
+ { id: "train-branches", when: isOrgRepo && lane.full, run: runTrainSyncRow },
31433
+ { id: "housekeeper", when: isOrgRepo && lane.full, run: runHousekeeperRows }
31434
+ ];
31435
+ for (const entry of suffix) if (entry.when) await entry.run();
31436
+ if (!lane.full) break;
31437
+ const fingerprint = doctorSnapshotFingerprint(checks);
31438
+ const stillActionable = checks.some((c) => !c.ok && !c.reportOnly);
31439
+ if (!healChangedThisPass || !stillActionable || fingerprint === prevFingerprint) break;
31440
+ if (passes >= maxPasses) {
31441
+ healIntent(`convergence \u2014 pass bound (${maxPasses}) reached with rows still moving; re-run doctor to continue`);
31442
+ break;
31443
+ }
31444
+ prevFingerprint = fingerprint;
31445
+ streamingPass = false;
31446
+ healIntent(`convergence \u2014 pass ${passes} healed; re-measuring (pass ${passes + 1}/${maxPasses})`);
31447
+ }
30884
31448
  const exitCode = doctorReportExitCode(checks);
30885
31449
  if (opts.json) {
30886
31450
  const payload = opts.verbose ? checks : checks.map(({ verbose: _evidence, ...check }) => check);
@@ -30893,7 +31457,7 @@ async function runDoctorClean(opts, io, deps) {
30893
31457
  return exitCode;
30894
31458
  }
30895
31459
  if (opts.banner) {
30896
- const actionable = checks.filter((c) => (!c.ok || c.warn) && !streamed.has(c));
31460
+ const actionable = checks.filter((c) => (!c.ok || c.warn) && !alreadyShown(c));
30897
31461
  for (const c of actionable) {
30898
31462
  io.log(renderReport([c], { restartPending: false }));
30899
31463
  if (opts.verbose) for (const evidence of c.verbose ?? []) io.log(`${VERBOSE_INDENT}${evidence}`);
@@ -30901,7 +31465,7 @@ async function runDoctorClean(opts, io, deps) {
30901
31465
  if (restartPending) io.log(`\u21BB ${restartAction.charAt(0).toUpperCase()}${restartAction.slice(1)} to finish.`);
30902
31466
  return 0;
30903
31467
  }
30904
- const rest = checks.filter((c) => !streamed.has(c));
31468
+ const rest = checks.filter((c) => !alreadyShown(c));
30905
31469
  const shown = opts.verbose ? rest : rest.filter((c) => !c.ok || c.warn);
30906
31470
  const lines = [];
30907
31471
  for (const check of shown) {
@@ -30933,17 +31497,17 @@ function parseOriginRepo(remoteUrl) {
30933
31497
  }
30934
31498
  function ghHostsConfigPath(env, platform2) {
30935
31499
  const sep3 = platform2 === "win32" ? "\\" : "/";
30936
- const join33 = (...parts) => parts.join(sep3);
31500
+ const join34 = (...parts) => parts.join(sep3);
30937
31501
  const explicit = env.GH_CONFIG_DIR?.trim();
30938
- if (explicit) return join33(explicit, "hosts.yml");
31502
+ if (explicit) return join34(explicit, "hosts.yml");
30939
31503
  if (platform2 === "win32") {
30940
31504
  const appData = (env.AppData ?? env.APPDATA)?.trim();
30941
- return appData ? join33(appData, "GitHub CLI", "hosts.yml") : void 0;
31505
+ return appData ? join34(appData, "GitHub CLI", "hosts.yml") : void 0;
30942
31506
  }
30943
31507
  const xdg = env.XDG_CONFIG_HOME?.trim();
30944
- if (xdg) return join33(xdg, "gh", "hosts.yml");
31508
+ if (xdg) return join34(xdg, "gh", "hosts.yml");
30945
31509
  const home = env.HOME?.trim();
30946
- return home ? join33(home, ".config", "gh", "hosts.yml") : void 0;
31510
+ return home ? join34(home, ".config", "gh", "hosts.yml") : void 0;
30947
31511
  }
30948
31512
  function parseGhHostsAccounts(yaml, host = "github.com") {
30949
31513
  let hostIndent = null;
@@ -30993,17 +31557,17 @@ function ghAccountCaveat(announcedLogin, accounts) {
30993
31557
  }
30994
31558
 
30995
31559
  // src/doctor-io.ts
30996
- var import_node_fs38 = require("node:fs");
31560
+ var import_node_fs39 = require("node:fs");
30997
31561
  var import_node_os14 = require("node:os");
30998
- var import_node_path36 = require("node:path");
30999
- var import_node_child_process17 = require("node:child_process");
31562
+ var import_node_path37 = require("node:path");
31563
+ var import_node_child_process18 = require("node:child_process");
31000
31564
  var import_node_util8 = require("node:util");
31001
- var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process17.execFile);
31565
+ var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process18.execFile);
31002
31566
  var MMI_PLUGIN_ID2 = "mmi@mutmutco";
31003
31567
  function installedClaudePluginVersion() {
31004
31568
  try {
31005
31569
  const file = JSON.parse(
31006
- (0, import_node_fs38.readFileSync)((0, import_node_path36.join)((0, import_node_os14.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
31570
+ (0, import_node_fs39.readFileSync)((0, import_node_path37.join)((0, import_node_os14.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
31007
31571
  );
31008
31572
  const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
31009
31573
  if (versions.length === 0) return void 0;
@@ -31014,7 +31578,7 @@ function installedClaudePluginVersion() {
31014
31578
  }
31015
31579
  function manifestVersion(path2) {
31016
31580
  try {
31017
- const manifest = JSON.parse((0, import_node_fs38.readFileSync)(path2, "utf8"));
31581
+ const manifest = JSON.parse((0, import_node_fs39.readFileSync)(path2, "utf8"));
31018
31582
  return typeof manifest.version === "string" && manifest.version.trim() ? manifest.version.trim() : void 0;
31019
31583
  } catch {
31020
31584
  return void 0;
@@ -31024,32 +31588,32 @@ function installedSurfacePluginVersion(surface) {
31024
31588
  const token = surfaceToken(surface);
31025
31589
  if (token === "kilo") {
31026
31590
  try {
31027
- const stamp = (0, import_node_fs38.readFileSync)((0, import_node_path36.join)((0, import_node_os14.homedir)(), ".kilo", ".mmi-kilo-version"), "utf8").trim();
31591
+ const stamp = (0, import_node_fs39.readFileSync)((0, import_node_path37.join)((0, import_node_os14.homedir)(), ".kilo", ".mmi-kilo-version"), "utf8").trim();
31028
31592
  return stamp || void 0;
31029
31593
  } catch {
31030
31594
  return void 0;
31031
31595
  }
31032
31596
  }
31033
31597
  if (token === "cursor") {
31034
- return manifestVersion((0, import_node_path36.join)(cursorLocalPluginRoot(), ".cursor-plugin", "plugin.json"));
31598
+ return manifestVersion((0, import_node_path37.join)(cursorLocalPluginRoot(), ".cursor-plugin", "plugin.json"));
31035
31599
  }
31036
31600
  if (token === "jervcode") {
31037
31601
  const entry = mmiPiWrapperEntry();
31038
31602
  if (!entry) return void 0;
31039
- return manifestVersion((0, import_node_path36.join)(decodeURIComponent(entry.replace(/^file:\/\/\/?/, "")), "package.json"));
31603
+ return manifestVersion((0, import_node_path37.join)(decodeURIComponent(entry.replace(/^file:\/\/\/?/, "")), "package.json"));
31040
31604
  }
31041
31605
  if (token === "kimi") {
31042
- return manifestVersion((0, import_node_path36.join)(surfaceConfigRoot(surface), "plugins", "managed", "mmi", ".kimi-plugin", "plugin.json"));
31606
+ return manifestVersion((0, import_node_path37.join)(surfaceConfigRoot(surface), "plugins", "managed", "mmi", ".kimi-plugin", "plugin.json"));
31043
31607
  }
31044
31608
  if (token === "claude") return installedClaudePluginVersion();
31045
31609
  if (token !== "codex") return void 0;
31046
31610
  try {
31047
- const raw = process.platform === "win32" ? (0, import_node_child_process17.execFileSync)("cmd.exe", ["/c", "codex", "plugin", "list", "--json"], {
31611
+ const raw = process.platform === "win32" ? (0, import_node_child_process18.execFileSync)("cmd.exe", ["/c", "codex", "plugin", "list", "--json"], {
31048
31612
  encoding: "utf8",
31049
31613
  stdio: ["ignore", "pipe", "ignore"],
31050
31614
  timeout: 15e3,
31051
31615
  windowsHide: true
31052
- }) : (0, import_node_child_process17.execFileSync)("codex", ["plugin", "list", "--json"], {
31616
+ }) : (0, import_node_child_process18.execFileSync)("codex", ["plugin", "list", "--json"], {
31053
31617
  encoding: "utf8",
31054
31618
  stdio: ["ignore", "pipe", "ignore"],
31055
31619
  timeout: 15e3,
@@ -31067,7 +31631,7 @@ function installedActivePluginVersion(surface = detectSurface(process.env)) {
31067
31631
  }
31068
31632
  function worktreeRootSync() {
31069
31633
  try {
31070
- const out = (0, import_node_child_process17.execFileSync)("git", ["rev-parse", "--show-toplevel"], { windowsHide: true, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
31634
+ const out = (0, import_node_child_process18.execFileSync)("git", ["rev-parse", "--show-toplevel"], { windowsHide: true, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
31071
31635
  let root = out.endsWith("\n") ? out.slice(0, -1) : out;
31072
31636
  if (process.platform === "win32" && root.endsWith("\r")) root = root.slice(0, -1);
31073
31637
  return root || null;
@@ -31077,13 +31641,13 @@ function worktreeRootSync() {
31077
31641
  }
31078
31642
  var gitignorePath = () => {
31079
31643
  const root = worktreeRootSync();
31080
- return root === null ? null : (0, import_node_path36.join)(root, ".gitignore");
31644
+ return root === null ? null : (0, import_node_path37.join)(root, ".gitignore");
31081
31645
  };
31082
31646
  function readGitignore() {
31083
31647
  const path2 = gitignorePath();
31084
31648
  if (path2 === null) return null;
31085
31649
  try {
31086
- return (0, import_node_fs38.readFileSync)(path2, "utf8");
31650
+ return (0, import_node_fs39.readFileSync)(path2, "utf8");
31087
31651
  } catch {
31088
31652
  return null;
31089
31653
  }
@@ -31092,7 +31656,7 @@ function writeGitignore(content) {
31092
31656
  const path2 = gitignorePath();
31093
31657
  if (path2 === null) return false;
31094
31658
  try {
31095
- (0, import_node_fs38.writeFileSync)(path2, content, "utf8");
31659
+ (0, import_node_fs39.writeFileSync)(path2, content, "utf8");
31096
31660
  return true;
31097
31661
  } catch {
31098
31662
  return false;
@@ -31116,28 +31680,11 @@ async function repoRoot() {
31116
31680
  }
31117
31681
  function hasRepoLocalWorktrees() {
31118
31682
  const root = worktreeRootSync();
31119
- return root !== null && (0, import_node_fs38.existsSync)((0, import_node_path36.join)(root, ".worktrees"));
31683
+ return root !== null && (0, import_node_fs39.existsSync)((0, import_node_path37.join)(root, ".worktrees"));
31120
31684
  }
31121
31685
 
31122
31686
  // src/index.ts
31123
31687
  var execFileGitRun = async (file, args) => (await execFileP2(file, args, { timeout: GIT_TIMEOUT_MS })).stdout;
31124
- async function currentRepoFullName() {
31125
- const remote = (await gitOut(["remote", "get-url", "origin"])).replace(/\.git$/, "");
31126
- const parts = remote.split(/[:/]/).filter(Boolean);
31127
- if (parts.length >= 2) return `${parts[parts.length - 2]}/${parts[parts.length - 1]}`;
31128
- return `mutmutco/${await repoSlug()}`;
31129
- }
31130
- async function readDocsAuditFetch(repo) {
31131
- const list = await fetchDocsAuditList(registryClientDeps(await loadConfig()));
31132
- if ("notArmed" in list) return { notArmed: true };
31133
- if (!list.ok) return { ok: false, error: list.error };
31134
- const wanted = repo.toLowerCase();
31135
- const row = list.rows.find((r) => String(r.repo ?? "").toLowerCase() === wanted);
31136
- return {
31137
- ok: true,
31138
- verdict: row ? { repo: row.repo, date: row.date, shaRange: row.shaRange, outcome: row.outcome, checkerVendor: row.checkerVendor } : null
31139
- };
31140
- }
31141
31688
  async function githubRepoReachProbe() {
31142
31689
  const remote = await execFileP2("git", ["remote", "get-url", "origin"], { timeout: GIT_TIMEOUT_MS }).then((r) => r.stdout).catch(() => "");
31143
31690
  const repo = parseOriginRepo(remote);
@@ -31152,8 +31699,8 @@ ${r.stderr ?? ""}`).catch(() => "");
31152
31699
  function ghMultiAccountCaveat(announcedLogin) {
31153
31700
  try {
31154
31701
  const hostsPath = ghHostsConfigPath(process.env, process.platform);
31155
- if (!hostsPath || !(0, import_node_fs39.existsSync)(hostsPath)) return void 0;
31156
- return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0, import_node_fs39.readFileSync)(hostsPath, "utf8")));
31702
+ if (!hostsPath || !(0, import_node_fs40.existsSync)(hostsPath)) return void 0;
31703
+ return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0, import_node_fs40.readFileSync)(hostsPath, "utf8")));
31157
31704
  } catch {
31158
31705
  return void 0;
31159
31706
  }
@@ -31161,7 +31708,7 @@ function ghMultiAccountCaveat(announcedLogin) {
31161
31708
  var ENV_HEAL_LOCK_STALE_MS = 10 * 6e4;
31162
31709
  var ENV_HEAL_LOCK_MAX_WAIT_MS = 2 * 6e4;
31163
31710
  function envHealLockPath(home) {
31164
- return (0, import_node_path37.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
31711
+ return (0, import_node_path38.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
31165
31712
  }
31166
31713
  async function withEnvHealLock(what, run) {
31167
31714
  try {
@@ -31276,6 +31823,45 @@ function mmiDoctorDeps(opts = {}) {
31276
31823
  stagingBytes: plan.stagingBytes
31277
31824
  };
31278
31825
  },
31826
+ // #4199: guarded auto-prune (docs/doctor-contract.md § Guarded cache prune). The plan is rebuilt
31827
+ // HERE, from live evidence, immediately before the delete — plan is the revalidation. Guards live in
31828
+ // `selectPrunablePluginVersions` (never running/newest/installed; unreadable evidence keeps the dir,
31829
+ // named). No `force`: a dir whose delete refuses (a session holds it) surfaces as `held`, never freed
31830
+ // out from under the holder.
31831
+ prunePluginCache: () => {
31832
+ const surface = detectSurface(process.env);
31833
+ const configRoot = surfaceConfigRoot(surface);
31834
+ const running = surface === "codex" ? installedActivePluginVersion(surface) : runningPluginVersion(process.env, resolveClientVersion());
31835
+ const installed = installedActivePluginVersion(surface);
31836
+ const plan = buildPluginCachePlan(
31837
+ (0, import_node_os15.homedir)(),
31838
+ running,
31839
+ pluginCacheFsDeps(configRoot, () => 0),
31840
+ { configRoot, includeStaging: surface !== "codex", installedVersion: installed }
31841
+ );
31842
+ const result = applyPluginCachePlan(
31843
+ plan,
31844
+ (p) => (0, import_node_fs40.rmSync)(p, { recursive: true }),
31845
+ stagingApplyFsGuard(configRoot)
31846
+ );
31847
+ return {
31848
+ removed: [...result.removed, ...result.removedStaging],
31849
+ held: [
31850
+ ...result.failed,
31851
+ ...result.failedStaging.map((f) => ({ version: f.name, error: f.error }))
31852
+ ],
31853
+ kept: [
31854
+ ...plan.keep.map((version) => ({
31855
+ version,
31856
+ reason: version === running ? "running" : version === installed ? "installed" : "newest / keep policy"
31857
+ })),
31858
+ ...result.skippedStaging.map((s) => ({ version: s.name, reason: s.reason }))
31859
+ ]
31860
+ };
31861
+ },
31862
+ // #4201: mmi `.pi-plugin` registration in ~/.pi/agent/settings.json — mirror of jerv-cli's own heal.
31863
+ piPluginState: () => readPiPluginState((0, import_node_os15.homedir)(), process.env),
31864
+ healPiPlugin: () => healPiPluginRegistration((0, import_node_os15.homedir)(), process.env),
31279
31865
  // #3470: what the SessionStart hook LAST emitted, measured at emission by the session-start verb below.
31280
31866
  // A local record read ? cheap enough for every lane, including the banner.
31281
31867
  sessionPayload: () => readSessionPayload(process.cwd()),
@@ -31287,14 +31873,14 @@ function mmiDoctorDeps(opts = {}) {
31287
31873
  const home = (0, import_node_os15.homedir)();
31288
31874
  const rows = marketplaceRows(
31289
31875
  MMI_MARKETPLACE_NAME,
31290
- readFileSyncSafe((0, import_node_path37.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs39.readFileSync),
31291
- readFileSyncSafe((0, import_node_path37.join)(home, ".claude", "settings.json"), import_node_fs39.readFileSync),
31876
+ readFileSyncSafe((0, import_node_path38.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs40.readFileSync),
31877
+ readFileSyncSafe((0, import_node_path38.join)(home, ".claude", "settings.json"), import_node_fs40.readFileSync),
31292
31878
  // #3974: this CLI heals these rows, so report mode names the doctor run rather than the hand
31293
31879
  // edit. Unconditional ? it is a fact about mmi-cli, not about the lane this run is on.
31294
31880
  true
31295
31881
  );
31296
31882
  const pending = readMarketplacePinPending(
31297
- (0, import_node_path37.join)(home, ".claude", "plugins", ".mmi-marketplace-pin-pending.json"),
31883
+ (0, import_node_path38.join)(home, ".claude", "plugins", ".mmi-marketplace-pin-pending.json"),
31298
31884
  MMI_MARKETPLACE_NAME
31299
31885
  );
31300
31886
  if (!pending) return rows;
@@ -31320,9 +31906,9 @@ function mmiDoctorDeps(opts = {}) {
31320
31906
  if (detectSurface(process.env) === "codex") return void 0;
31321
31907
  const home = (0, import_node_os15.homedir)();
31322
31908
  const names = [MMI_MARKETPLACE_NAME];
31323
- const result = applyOrgMarketplacePins((0, import_node_path37.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), names);
31909
+ const result = applyOrgMarketplacePins((0, import_node_path38.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), names);
31324
31910
  if (result?.wrote) {
31325
- writeMarketplacePinPending((0, import_node_path37.join)(home, ".claude", "plugins", ".mmi-marketplace-pin-pending.json"), names);
31911
+ writeMarketplacePinPending((0, import_node_path38.join)(home, ".claude", "plugins", ".mmi-marketplace-pin-pending.json"), names);
31326
31912
  }
31327
31913
  return result;
31328
31914
  } catch {
@@ -31338,7 +31924,7 @@ function mmiDoctorDeps(opts = {}) {
31338
31924
  // adopted the generated routing index (MMG-Unlive: `docs/Archive/**` only, no index, no gate.yml) would
31339
31925
  // get a permanent ? demanding an artifact it never asked for.
31340
31926
  docsIndexState: (root) => {
31341
- if (!(0, import_node_fs39.existsSync)((0, import_node_path37.join)(root, DOCS_INDEX_PATH))) return void 0;
31927
+ if (!(0, import_node_fs40.existsSync)((0, import_node_path38.join)(root, DOCS_INDEX_PATH))) return void 0;
31342
31928
  const real = createDocsIndexDeps(root);
31343
31929
  let docs2;
31344
31930
  const listDocs = () => docs2 ??= real.listDocs();
@@ -31347,7 +31933,7 @@ function mmiDoctorDeps(opts = {}) {
31347
31933
  },
31348
31934
  // #4168: working-tree heal — write if drifted, then re-check. Commit remains the operator's step.
31349
31935
  healDocsIndex: (root) => {
31350
- if (!(0, import_node_fs39.existsSync)((0, import_node_path37.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
31936
+ if (!(0, import_node_fs40.existsSync)((0, import_node_path38.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
31351
31937
  const real = createDocsIndexDeps(root);
31352
31938
  let docs2;
31353
31939
  const listDocs = () => docs2 ??= real.listDocs();
@@ -31365,6 +31951,7 @@ function mmiDoctorDeps(opts = {}) {
31365
31951
  worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
31366
31952
  removalContext
31367
31953
  );
31954
+ for (const removedPath of result.removed) await bestEffortLeaseClose(removedPath);
31368
31955
  return {
31369
31956
  removed: result.removed,
31370
31957
  stillQueued: result.stillDeferred.length,
@@ -31681,19 +32268,19 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
31681
32268
  });
31682
32269
  var rules = program2.command("rules").description("org-managed .gitignore delivery");
31683
32270
  rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").option("--json", "machine-readable output").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
31684
- const path2 = (0, import_node_path37.join)(process.cwd(), ".gitignore");
31685
- const current = (0, import_node_fs39.existsSync)(path2) ? (0, import_node_fs39.readFileSync)(path2, "utf8") : null;
32271
+ const path2 = (0, import_node_path38.join)(process.cwd(), ".gitignore");
32272
+ const current = (0, import_node_fs40.existsSync)(path2) ? (0, import_node_fs40.readFileSync)(path2, "utf8") : null;
31686
32273
  const plan = planManagedGitignore(current);
31687
32274
  const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
31688
32275
  if (opts.json) {
31689
- if (opts.write && plan.changed) (0, import_node_fs39.writeFileSync)(path2, plan.content, "utf8");
32276
+ if (opts.write && plan.changed) (0, import_node_fs40.writeFileSync)(path2, plan.content, "utf8");
31690
32277
  console.log(JSON.stringify(plan, null, 2));
31691
32278
  if (!opts.write && plan.changed) process.exitCode = 1;
31692
32279
  return;
31693
32280
  }
31694
32281
  if (opts.write) {
31695
32282
  if (plan.changed) {
31696
- (0, import_node_fs39.writeFileSync)(path2, plan.content, "utf8");
32283
+ (0, import_node_fs40.writeFileSync)(path2, plan.content, "utf8");
31697
32284
  console.log(`mmi-cli org rules gitignore: updated .gitignore (${drift})`);
31698
32285
  } else {
31699
32286
  console.log("mmi-cli org rules gitignore: up to date");
@@ -31819,6 +32406,7 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
31819
32406
  worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
31820
32407
  { removalContext }
31821
32408
  );
32409
+ for (const removedPath of result.removed) await bestEffortLeaseClose(removedPath);
31822
32410
  if (o.json) return console.log(JSON.stringify(result));
31823
32411
  if (!o.quiet || result.removed.length || result.stillDeferred.length || result.skipped.length) {
31824
32412
  if (result.removed.length) console.log(`worktree gc sweep-deferred: removed ${result.removed.length} worktree(s)`);
@@ -31834,7 +32422,7 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
31834
32422
  process.exit(process.exitCode ?? 0);
31835
32423
  });
31836
32424
  });
31837
- gcCmd.option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR local+remote branches, linked worktrees, and stale tracking refs").option("--json", "machine-readable output").option("--scratch", "prune safe local scratch (#1864) instead of git branches/refs; local plans are surfaced advisory-only, never auto-pruned").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to inspect per state", "200").option("--force", "remove worktrees even when the ownership registry shows another session created or worked in them recently (#3580)").option("--root <path>", "sweep an explicitly named worktrees root instead of the authoritative ../mmi-worktrees (#3471) ? descends into this repo container when present; ownership, dead-dir, and content guards still apply").action(async (o) => {
32425
+ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR local+remote branches, linked worktrees, and stale tracking refs").option("--json", "machine-readable output").option("--scratch", "prune safe local scratch (#1864) instead of git branches/refs; local plans are surfaced advisory-only, never auto-pruned").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to read PER BRANCH ? an effort bound, not a correctness one: merge state is resolved per branch, so no branch is ever skipped for being old (#4227)", "200").option("--force", "remove worktrees even when the ownership registry shows another session created or worked in them recently (#3580)").option("--root <path>", "sweep an explicitly named worktrees root instead of the authoritative ../mmi-worktrees (#3471) ? descends into this repo container when present; ownership, dead-dir, and content guards still apply").action(async (o) => {
31838
32426
  if (o.apply && o.dryRun) return fail("worktree gc: choose either --dry-run or --apply");
31839
32427
  if (o.scratch) {
31840
32428
  try {
@@ -31850,8 +32438,8 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
31850
32438
  if (!Number.isFinite(limit) || limit < 1) return fail("worktree gc: --limit must be a positive integer");
31851
32439
  let root;
31852
32440
  if (o.root !== void 0) {
31853
- root = (0, import_node_path37.resolve)(o.root);
31854
- if (!(0, import_node_fs39.existsSync)(root) || !(0, import_node_fs39.statSync)(root).isDirectory()) return fail(`worktree gc: --root ${o.root} is not a directory`);
32441
+ root = (0, import_node_path38.resolve)(o.root);
32442
+ if (!(0, import_node_fs40.existsSync)(root) || !(0, import_node_fs40.statSync)(root).isDirectory()) return fail(`worktree gc: --root ${o.root} is not a directory`);
31855
32443
  const gcRepoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
31856
32444
  if (isPathUnderDirectory(gcRepoRoot, root)) {
31857
32445
  return fail(`worktree gc: --root ${root} contains this checkout ? name a worktrees root, not the repo or an ancestor of it`);
@@ -31866,13 +32454,14 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
31866
32454
  if (o.apply) {
31867
32455
  const deferredStore = await createDeferredWorktreeStore();
31868
32456
  const removalContext = await currentWorktreeRemovalContext("worktree gc", o.force);
31869
- await sweepDeferredWorktrees(
32457
+ const sweepResult = await sweepDeferredWorktrees(
31870
32458
  deferredStore,
31871
32459
  // #2841: same `-c core.fsmonitor=false` guard as the detached `gc sweep-deferred` worker ? keep a
31872
32460
  // git fsmonitor daemon from inheriting this sweep's stdio pipe and wedging the git call on Windows.
31873
32461
  worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
31874
32462
  removalContext
31875
32463
  ).catch(() => void 0);
32464
+ for (const removedPath of sweepResult?.removed ?? []) await bestEffortLeaseClose(removedPath);
31876
32465
  applyResult = await applyGcPlan(plan, o.remote, { root, force: o.force });
31877
32466
  }
31878
32467
  if (o.json) {
@@ -31893,11 +32482,11 @@ var WORKTREE_SETUP_LOCK_TTL_MS = 10 * 6e4;
31893
32482
  function runWorktreeInstall(command, cwd, quiet, opts) {
31894
32483
  const stdio = quiet ? "ignore" : "inherit";
31895
32484
  return new Promise((resolve5, reject) => {
31896
- const child2 = opts?.shell ? (0, import_node_child_process18.spawn)(command, { cwd, stdio, windowsHide: true, shell: true }) : (() => {
32485
+ const child2 = opts?.shell ? (0, import_node_child_process19.spawn)(command, { cwd, stdio, windowsHide: true, shell: true }) : (() => {
31897
32486
  const [bin, ...args] = command.split(" ");
31898
32487
  const file = isWin2 ? "cmd.exe" : bin;
31899
32488
  const spawnArgs = isWin2 ? ["/c", bin, ...args] : args;
31900
- return (0, import_node_child_process18.spawn)(file, spawnArgs, { cwd, stdio, windowsHide: true });
32489
+ return (0, import_node_child_process19.spawn)(file, spawnArgs, { cwd, stdio, windowsHide: true });
31901
32490
  })();
31902
32491
  const timer = setTimeout(() => {
31903
32492
  try {
@@ -31930,7 +32519,7 @@ async function currentWorktreeRemovalContext(command, force) {
31930
32519
  };
31931
32520
  }
31932
32521
  async function unprovenWorktreeReason(wtPath, repoRoot2) {
31933
- if (!(0, import_node_fs39.existsSync)(wtPath)) return `${wtPath} does not exist on disk`;
32522
+ if (!(0, import_node_fs40.existsSync)(wtPath)) return `${wtPath} does not exist on disk`;
31934
32523
  const porcelain = (await execFileP2("git", ["-C", repoRoot2, "worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout;
31935
32524
  const registered = parseWorktreePorcelainEntries(porcelain);
31936
32525
  if (!registered.length) {
@@ -31952,26 +32541,26 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
31952
32541
  function acquireWorktreeSetupLock(worktreeRoot) {
31953
32542
  const lockPath = repoRuntimeStatePath(worktreeRoot, "worktree-setup.lock");
31954
32543
  const take = () => {
31955
- const fd = (0, import_node_fs39.openSync)(lockPath, "wx");
32544
+ const fd = (0, import_node_fs40.openSync)(lockPath, "wx");
31956
32545
  try {
31957
- (0, import_node_fs39.writeSync)(fd, String(Date.now()));
32546
+ (0, import_node_fs40.writeSync)(fd, String(Date.now()));
31958
32547
  } finally {
31959
- (0, import_node_fs39.closeSync)(fd);
32548
+ (0, import_node_fs40.closeSync)(fd);
31960
32549
  }
31961
32550
  return () => {
31962
32551
  try {
31963
- (0, import_node_fs39.rmSync)(lockPath, { force: true });
32552
+ (0, import_node_fs40.rmSync)(lockPath, { force: true });
31964
32553
  } catch {
31965
32554
  }
31966
32555
  };
31967
32556
  };
31968
32557
  try {
31969
- (0, import_node_fs39.mkdirSync)((0, import_node_path37.dirname)(lockPath), { recursive: true });
32558
+ (0, import_node_fs40.mkdirSync)((0, import_node_path38.dirname)(lockPath), { recursive: true });
31970
32559
  return take();
31971
32560
  } catch {
31972
32561
  try {
31973
- if (Date.now() - (0, import_node_fs39.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
31974
- (0, import_node_fs39.rmSync)(lockPath, { force: true });
32562
+ if (Date.now() - (0, import_node_fs40.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
32563
+ (0, import_node_fs40.rmSync)(lockPath, { force: true });
31975
32564
  return take();
31976
32565
  }
31977
32566
  } catch {
@@ -32108,6 +32697,34 @@ withExamples(mutating(
32108
32697
  const owner = { path: wtPath, branch, createdAt, lastSeenAt: createdAt, actor: createActor };
32109
32698
  recordWorktreeOwner(repoRoot2, owner);
32110
32699
  appendWorktreeEvent(repoRoot2, { action: "created", command: "worktree create", target: wtPath, branch, actor: owner.actor });
32700
+ let lease;
32701
+ try {
32702
+ await execFileP2("jerv-cli", [
32703
+ "lease",
32704
+ "open",
32705
+ "--kind",
32706
+ "worktree",
32707
+ "--ref",
32708
+ wtPath,
32709
+ ...selector ? ["--repo", selector.repo] : [],
32710
+ // 72h, not the 24h default: a feature worktree routinely sits untouched over a weekend, and
32711
+ // an expired lease is what makes a CLEAN, unheld tree reapable. Erring long costs disk;
32712
+ // erring short reaps a tree someone is still using between sessions.
32713
+ "--ttl",
32714
+ "72",
32715
+ "--note",
32716
+ `${branch} via mmi-cli worktree create`
32717
+ ], { timeout: GIT_TIMEOUT_MS });
32718
+ lease = { ok: true };
32719
+ } catch (e) {
32720
+ const detail = (e.stderr?.trim() || e.message.trim()).split("\n")[0];
32721
+ lease = { ok: false, error: detail };
32722
+ if (!o.json) {
32723
+ console.error(
32724
+ ` worktree created, but no jerv lease was opened for it: ${detail} \u2014 nothing on the lease plane can expire this tree, so it will need manual cleanup (MMI-Hub#4231).`
32725
+ );
32726
+ }
32727
+ }
32111
32728
  if (o.json) {
32112
32729
  return console.log(JSON.stringify({
32113
32730
  branch,
@@ -32115,6 +32732,7 @@ withExamples(mutating(
32115
32732
  base,
32116
32733
  resumed,
32117
32734
  ...report,
32735
+ lease,
32118
32736
  ...issueForm && selector ? { issue: `${selector.repo}#${selector.number}` } : {},
32119
32737
  ...issueForm && o.claim ? { claim: claimError ? { ok: false, error: claimError } : claim } : {}
32120
32738
  }, null, 2));
@@ -32226,7 +32844,7 @@ function scheduleRelatedDiscovery(o) {
32226
32844
  try {
32227
32845
  const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body, "--fail-soft"];
32228
32846
  if (o.repo) args.push("--repo", o.repo);
32229
- spawnDetachedSelf(args, { spawn: import_node_child_process18.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
32847
+ spawnDetachedSelf(args, { spawn: import_node_child_process19.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
32230
32848
  } catch {
32231
32849
  }
32232
32850
  }
@@ -32554,48 +33172,6 @@ tests.command("policy").description("enforce this repo's test-policy.json agains
32554
33172
  await failGraceful(e.message);
32555
33173
  }
32556
33174
  });
32557
- var docsAudit = program2.command("docs-audit").description("the docs janitor verdict ledger ? record a dated run verdict, or read the dead-man status back");
32558
- docsAudit.command("record").description("write a dated janitor verdict for one repo to the registry ledger (master-only server-side)").option("--repo <owner/name>", "the repo the verdict is for (default: the current repo)").option("--date <YYYY-MM-DD>", "the ISO day the run examined (default: today)").requiredOption("--sha-range <a..b>", "the git range the janitor read (e.g. <lastVerdictSha>..HEAD)").addOption(new Option("--outcome <kind>", "clean | refreshed | failed").makeOptionMandatory().choices(["clean", "refreshed", "failed"])).option("--count <n>", "docs refreshed (required when --outcome refreshed)").option("--reason <text>", "why the run failed (required when --outcome failed)").requiredOption("--checker-vendor <vendor>", "which vendor's model actually ran the check").action(async (o) => {
32559
- try {
32560
- const repo = o.repo ?? await currentRepoFullName();
32561
- const date = o.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
32562
- let outcome;
32563
- if (o.outcome === "clean") outcome = { kind: "clean" };
32564
- else if (o.outcome === "refreshed") outcome = { kind: "refreshed", count: Number(o.count) };
32565
- else if (o.outcome === "failed") outcome = { kind: "failed", reason: o.reason ?? "" };
32566
- else return failGraceful(`docs audit record: --outcome must be clean|refreshed|failed, got "${o.outcome}"`);
32567
- const verdict = docsAuditRecord({ repo, date, shaRange: o.shaRange, outcome, checkerVendor: o.checkerVendor });
32568
- await reportWrite("docs-audit record", await recordDocsAudit(verdict, registryClientDeps(await loadConfig())));
32569
- } catch (e) {
32570
- await failGraceful(e.message);
32571
- }
32572
- });
32573
- docsAudit.command("status").description("read the janitor dead-man verdict back for one repo ? missing/stale/failed is RED; a not-yet-armed registry route is an informational exit 0").option("--repo <owner/name>", "the repo to check (default: the current repo)").option("--json", "machine-readable output ? the discrete state rather than the sentence").action(async (o) => {
32574
- try {
32575
- const repo = o.repo ?? await currentRepoFullName();
32576
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
32577
- const fetched = await readDocsAuditFetch(repo);
32578
- const result = docsAuditStatus(fetched, { repo, today });
32579
- if (o.json) {
32580
- const verdict = "ok" in fetched && fetched.ok ? fetched.verdict : null;
32581
- console.log(JSON.stringify({
32582
- repo,
32583
- armed: !("notArmed" in fetched),
32584
- ok: result.ok,
32585
- state: result.state,
32586
- date: verdict?.date ?? null,
32587
- outcome: verdict?.outcome ?? null,
32588
- checkerVendor: verdict?.checkerVendor ?? null,
32589
- line: result.line
32590
- }, null, 2));
32591
- } else {
32592
- console.log(result.line);
32593
- }
32594
- if (!result.ok) process.exitCode = 1;
32595
- } catch (e) {
32596
- await failGraceful(e.message);
32597
- }
32598
- });
32599
33175
  async function reportWrite(label, res) {
32600
33176
  if (res.ok) {
32601
33177
  console.log(JSON.stringify(res.body));
@@ -32832,7 +33408,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
32832
33408
  if (dupe) return fail(`org project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
32833
33409
  if (o.secretsFile) {
32834
33410
  try {
32835
- vars.push(`secrets=${(0, import_node_fs39.readFileSync)(o.secretsFile, "utf8")}`);
33411
+ vars.push(`secrets=${(0, import_node_fs40.readFileSync)(o.secretsFile, "utf8")}`);
32836
33412
  } catch (e) {
32837
33413
  return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
32838
33414
  }
@@ -33255,6 +33831,7 @@ withExamples(mutating(
33255
33831
  }
33256
33832
  }
33257
33833
  if (o.related !== false) scheduleRelatedDiscovery({ repo: o.repo, number: created.number, title, body });
33834
+ invalidateStatuslineBoardCache();
33258
33835
  console.log(JSON.stringify({
33259
33836
  ...created,
33260
33837
  label: issueType,
@@ -33555,6 +34132,7 @@ withExamples(pr.command("create").description("create a PR and print {number,url
33555
34132
  return fail(`pr create: ${docsCheck.detail} ? ${docsCheck.fix}`);
33556
34133
  }
33557
34134
  const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo, draft: o.draft }));
34135
+ invalidateStatuslineBoardCache();
33558
34136
  console.log(JSON.stringify(created));
33559
34137
  }), [
33560
34138
  'mmi-cli pr create --title "Add the schema" --body "Closes #2680"',
@@ -33584,11 +34162,11 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
33584
34162
  }
33585
34163
  });
33586
34164
  async function listCiWorkflowPaths(cwd = process.cwd()) {
33587
- const wfDir = (0, import_node_path37.join)(cwd, ".github", "workflows");
33588
- if (!(0, import_node_fs39.existsSync)(wfDir)) return [];
33589
- return (0, import_node_fs39.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
34165
+ const wfDir = (0, import_node_path38.join)(cwd, ".github", "workflows");
34166
+ if (!(0, import_node_fs40.existsSync)(wfDir)) return [];
34167
+ return (0, import_node_fs40.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
33590
34168
  try {
33591
- return workflowReportsPrChecks((0, import_node_fs39.readFileSync)((0, import_node_path37.join)(wfDir, name), "utf8"));
34169
+ return workflowReportsPrChecks((0, import_node_fs40.readFileSync)((0, import_node_path38.join)(wfDir, name), "utf8"));
33592
34170
  } catch {
33593
34171
  return true;
33594
34172
  }
@@ -33620,16 +34198,16 @@ function ciAuditDeps() {
33620
34198
  // gate re-seed step is skipped gracefully rather than failing mid-run.
33621
34199
  readSeedFile: (path2) => {
33622
34200
  if (!root) return null;
33623
- const fullPath = (0, import_node_path37.join)(root, path2);
33624
- return (0, import_node_fs39.existsSync)(fullPath) ? (0, import_node_fs39.readFileSync)(fullPath, "utf8") : null;
34201
+ const fullPath = (0, import_node_path38.join)(root, path2);
34202
+ return (0, import_node_fs40.existsSync)(fullPath) ? (0, import_node_fs40.readFileSync)(fullPath, "utf8") : null;
33625
34203
  }
33626
34204
  };
33627
34205
  }
33628
34206
  function hubRoot() {
33629
- const fromPkg = (0, import_node_path37.join)(__dirname, "..", "..");
34207
+ const fromPkg = (0, import_node_path38.join)(__dirname, "..", "..");
33630
34208
  const marker = "skills/bootstrap/seeds/manifest.json";
33631
- if ((0, import_node_fs39.existsSync)((0, import_node_path37.join)(fromPkg, marker))) return fromPkg;
33632
- if ((0, import_node_fs39.existsSync)((0, import_node_path37.join)(process.cwd(), marker))) return process.cwd();
34209
+ if ((0, import_node_fs40.existsSync)((0, import_node_path38.join)(fromPkg, marker))) return fromPkg;
34210
+ if ((0, import_node_fs40.existsSync)((0, import_node_path38.join)(process.cwd(), marker))) return process.cwd();
33633
34211
  return null;
33634
34212
  }
33635
34213
  pr.command("ci-policy").description("report merge CI policy: wait-for-checks vs no-ci (for grind/build agents)").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current checkout)").action(async (o) => {
@@ -33940,7 +34518,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
33940
34518
  localCleanup = await cleanupPrMergeLocalBranch(headRef, {
33941
34519
  beforeWorktrees,
33942
34520
  startingPath,
33943
- pathExists: (p) => (0, import_node_fs39.existsSync)(p),
34521
+ pathExists: (p) => (0, import_node_fs40.existsSync)(p),
33944
34522
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
33945
34523
  teardownWorktreeStage,
33946
34524
  deferredStore,
@@ -33954,6 +34532,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
33954
34532
  preserveWorktree: o.preserveWorktree,
33955
34533
  removalContext
33956
34534
  });
34535
+ if (localCleanup.worktree?.status === "removed") await bestEffortLeaseClose(localCleanup.worktree.path);
33957
34536
  } catch (e) {
33958
34537
  localCleanup = {
33959
34538
  branch: headRef,
@@ -33966,6 +34545,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
33966
34545
  };
33967
34546
  }
33968
34547
  const boardAdvance = await advanceClosedIssuesToDone2(number, repoForPostCleanup);
34548
+ invalidateStatuslineBoardCache();
33969
34549
  console.log(JSON.stringify({
33970
34550
  ...buildPrMergeResultPayload({
33971
34551
  number,
@@ -34434,19 +35014,19 @@ access.command("audit").description("audit collaborator roles + train-branch pus
34434
35014
  targets = resolution.targets;
34435
35015
  }
34436
35016
  const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
34437
- const fileMatrix = (0, import_node_fs39.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs39.readFileSync)("access-matrix.json", "utf8")) : {};
35017
+ const fileMatrix = (0, import_node_fs40.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs40.readFileSync)("access-matrix.json", "utf8")) : {};
34438
35018
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
34439
35019
  const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
34440
- const fileContracts = (0, import_node_fs39.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs39.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
35020
+ const fileContracts = (0, import_node_fs40.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs40.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
34441
35021
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
34442
- const sanctioned = (0, import_node_fs39.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs39.readFileSync)("access-matrix.json", "utf8")) : {};
35022
+ const sanctioned = (0, import_node_fs40.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs40.readFileSync)("access-matrix.json", "utf8")) : {};
34443
35023
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess, sanctioned);
34444
35024
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
34445
35025
  if (!report.ok) process.exitCode = 1;
34446
35026
  });
34447
35027
  access.command("capabilities").description("enumerate your effective vault reach ? every credential NAME + tier + scope you can read/use across project + org/master tiers (names only, no values) (#1615)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsCapabilities(d, o)));
34448
35028
  var isWin2 = process.platform === "win32";
34449
- program2.command("doctor").description("heal CLI/plugin wiring and clean up repo cruft ? repairs run by default (#3975); use --verbose for the full audit checklist").option("--banner", "one-line resume summary; silent when all gates pass").option("--fast", "offline-safe fast lane; read-only, skips slow checks and network probes").option("--preflight", "eager version/plugin-heal (env repairs only ? never the repo working tree) with upfront notice when stale; silent when healthy (#1871)").option("--verbose", "print the evidence behind every check ? probes, resolved paths, versions compared, and the names behind each count (#2977)").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (an output format ? repairs still run by lane)").option("--apply", "deprecated no-op: repairs run by default now (#3975); kept so older instructions still parse").option("--no-repo-writes", "env/plugin repairs only ? never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for train preflights)").option("--self", "verify CLI/plugin version parity and gh auth reach; suggests plugin-heal on a hard gap (reads the published version, so not offline-safe) (#2689)").addHelpText("after", "\nExit codes:\n 0 no hard gaps remain; advisory gaps may exist\n 1 one or more included hard checks failed\n\nA plain run heals env drift (CLI, plugin, marketplace pins) and cleans repo cruft (gitignore block,\nmerged branches, dead worktrees, aged scratch) automatically (#3975). --no-repo-writes keeps the\nworking tree untouched for train preflights; --banner/--fast/--self are read-only lanes.\n\n--banner keeps the legacy SessionStart contract and returns 0 unless the process crashes.\n").action(async (opts) => {
35029
+ program2.command("doctor").description("heal CLI/plugin wiring and clean up repo cruft ? repairs run by default (#3975); use --verbose for the full audit checklist").option("--banner", "one-line resume summary; silent when all gates pass").option("--fast", "offline-safe fast lane; read-only, skips slow checks and network probes").option("--preflight", "read-only fast gate for automation: measures and reports with the shared exit code, zero writes (#4199, canon); heal env drift with a plain run or --no-repo-writes").option("--verbose", "print the evidence behind every check ? probes, resolved paths, versions compared, and the names behind each count (#2977)").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (an output format ? repairs still run by lane)").option("--apply", "deprecated no-op: repairs run by default now (#3975); kept so older instructions still parse").option("--no-repo-writes", "env/plugin repairs only ? never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for train preflights)").option("--self", "verify CLI/plugin version parity and gh auth reach; suggests plugin-heal on a hard gap (reads the published version, so not offline-safe) (#2689)").addHelpText("after", "\nExit codes:\n 0 no hard gaps remain; advisory gaps may exist\n 1 one or more included hard checks failed\n\nA plain run heals env drift (CLI, plugin, marketplace pins) and cleans repo cruft (gitignore block,\nmerged branches, dead worktrees, aged scratch) automatically (#3975). --no-repo-writes keeps the\nworking tree untouched for train preflights; --banner/--fast/--self are read-only lanes.\n\n--banner keeps the legacy SessionStart contract and returns 0 unless the process crashes.\n").action(async (opts) => {
34450
35030
  if (opts.guide) {
34451
35031
  consoleIo.log("MMI Agentic Onboarding: docs/Architecture/agentic-dev-environment.md");
34452
35032
  return;
@@ -34471,16 +35051,16 @@ function directoryBytes(path2) {
34471
35051
  let total = 0;
34472
35052
  let entries;
34473
35053
  try {
34474
- entries = (0, import_node_fs39.readdirSync)(path2, { withFileTypes: true });
35054
+ entries = (0, import_node_fs40.readdirSync)(path2, { withFileTypes: true });
34475
35055
  } catch {
34476
35056
  return 0;
34477
35057
  }
34478
35058
  for (const entry of entries) {
34479
- const child2 = (0, import_node_path37.join)(path2, entry.name);
35059
+ const child2 = (0, import_node_path38.join)(path2, entry.name);
34480
35060
  if (entry.isDirectory()) total += directoryBytes(child2);
34481
35061
  else {
34482
35062
  try {
34483
- total += (0, import_node_fs39.statSync)(child2).size;
35063
+ total += (0, import_node_fs40.statSync)(child2).size;
34484
35064
  } catch {
34485
35065
  }
34486
35066
  }
@@ -34488,25 +35068,25 @@ function directoryBytes(path2) {
34488
35068
  return total;
34489
35069
  }
34490
35070
  function listDirEntries(dir) {
34491
- return (0, import_node_fs39.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
35071
+ return (0, import_node_fs40.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
34492
35072
  }
34493
35073
  function readInstalledPluginRefs(configRoot) {
34494
35074
  const p = installedPluginsPathForConfig(configRoot);
34495
- if (!(0, import_node_fs39.existsSync)(p)) return [];
35075
+ if (!(0, import_node_fs40.existsSync)(p)) return [];
34496
35076
  try {
34497
- return installedPluginPaths((0, import_node_fs39.readFileSync)(p, "utf8"));
35077
+ return installedPluginPaths((0, import_node_fs40.readFileSync)(p, "utf8"));
34498
35078
  } catch {
34499
35079
  return null;
34500
35080
  }
34501
35081
  }
34502
35082
  function pluginCacheFsDeps(configRoot, dirBytes) {
34503
35083
  return {
34504
- exists: (p) => (0, import_node_fs39.existsSync)(p),
34505
- listVersionDirs: (root) => (0, import_node_fs39.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
35084
+ exists: (p) => (0, import_node_fs40.existsSync)(p),
35085
+ listVersionDirs: (root) => (0, import_node_fs40.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
34506
35086
  dirBytes,
34507
- listStagingDirs: (root) => (0, import_node_fs39.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
35087
+ listStagingDirs: (root) => (0, import_node_fs40.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
34508
35088
  try {
34509
- return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path37.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs39.statSync)(p).mtimeMs) };
35089
+ return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path38.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs40.statSync)(p).mtimeMs) };
34510
35090
  } catch {
34511
35091
  return { name: d.name, mtimeMs: Date.now() };
34512
35092
  }
@@ -34520,10 +35100,10 @@ function stagingApplyFsGuard(configRoot) {
34520
35100
  return {
34521
35101
  referencedPaths: () => readInstalledPluginRefs(configRoot),
34522
35102
  mtimeMs: (name) => {
34523
- const p = (0, import_node_path37.join)(stagingRoot, name);
34524
- if (!(0, import_node_fs39.existsSync)(p)) return null;
35103
+ const p = (0, import_node_path38.join)(stagingRoot, name);
35104
+ if (!(0, import_node_fs40.existsSync)(p)) return null;
34525
35105
  try {
34526
- return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs39.statSync)(q).mtimeMs);
35106
+ return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs40.statSync)(q).mtimeMs);
34527
35107
  } catch {
34528
35108
  return null;
34529
35109
  }
@@ -34549,7 +35129,7 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
34549
35129
  { withBytes: true, configRoot, includeStaging: surface !== "codex" }
34550
35130
  );
34551
35131
  const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
34552
- const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs39.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
35132
+ const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs40.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
34553
35133
  const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
34554
35134
  if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
34555
35135
  else console.log(renderPluginCachePlan(plan, result));
@@ -34612,7 +35192,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
34612
35192
  for (const line of scratchGcLines(process.cwd())) bannerIo.log(line);
34613
35193
  const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
34614
35194
  if (worktreeBanner) {
34615
- spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process18.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
35195
+ spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process19.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
34616
35196
  bannerIo.log(worktreeBanner);
34617
35197
  }
34618
35198
  if (isLinkedWorktree(process.cwd())) {