@mutmutco/cli 3.29.0 → 3.30.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 +1030 -706
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -3408,9 +3408,9 @@ function useColor() {
3408
3408
  var program = new Command();
3409
3409
 
3410
3410
  // src/index.ts
3411
- var import_promises7 = require("node:fs/promises");
3412
- var import_node_fs28 = require("node:fs");
3413
- var import_node_child_process13 = require("node:child_process");
3411
+ var import_promises6 = require("node:fs/promises");
3412
+ var import_node_fs27 = require("node:fs");
3413
+ var import_node_child_process12 = require("node:child_process");
3414
3414
 
3415
3415
  // src/cli-shared.ts
3416
3416
  var import_node_child_process3 = require("node:child_process");
@@ -4949,19 +4949,6 @@ async function fetchTrainAuthority(repo, deps) {
4949
4949
  return { ok: false, error: e.message };
4950
4950
  }
4951
4951
  }
4952
- async function mintWikiToken(repo, deps) {
4953
- const res = await postJson("/wiki-mint", { repo }, deps, "POST", { noRetry: true });
4954
- if (res.error) return { ok: false, error: res.error };
4955
- const body = res.body ?? {};
4956
- if (res.status === 426) return { ok: false, error: upgradeRequiredError({ status: 426 }, res.body) };
4957
- if (res.status === 403) return { ok: false, error: `master-admin only (HTTP 403)${body.error ? ` \u2014 ${body.error}` : ""}` };
4958
- if (res.status === 404) {
4959
- return { ok: false, error: "the Hub API did not recognize /wiki-mint (HTTP 404) \u2014 the deployed Hub predates this command; it answers once a Hub release carrying it is deployed" };
4960
- }
4961
- if (!res.ok) return { ok: false, error: `wiki-mint HTTP ${res.status}${body.error ? ` \u2014 ${body.error}` : ""}` };
4962
- if (!body.token || !body.expiresAt) return { ok: false, error: "malformed wiki-mint response (no token)" };
4963
- return { ok: true, mint: { token: body.token, expiresAt: body.expiresAt, repository: body.repository ?? repo, permissions: body.permissions ?? {} } };
4964
- }
4965
4952
  async function fetchProjectsList(deps) {
4966
4953
  if (!deps.baseUrl) return null;
4967
4954
  const token = await deps.token();
@@ -5039,6 +5026,39 @@ async function fetchDeployFactsBySlug(slug, deps) {
5039
5026
  return null;
5040
5027
  }
5041
5028
  }
5029
+ async function fetchSchedulesList(deps) {
5030
+ if (!deps.baseUrl) return null;
5031
+ const token = await deps.token();
5032
+ if (!token) return null;
5033
+ try {
5034
+ const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/schedules/list`, {
5035
+ method: "GET",
5036
+ headers: { Authorization: `Bearer ${token}` }
5037
+ });
5038
+ if (!res.ok) return null;
5039
+ const body = await res.json();
5040
+ return Array.isArray(body?.schedules) ? body.schedules : null;
5041
+ } catch {
5042
+ return null;
5043
+ }
5044
+ }
5045
+ async function fetchDocsAuditList(deps) {
5046
+ if (!deps.baseUrl) return { notArmed: true };
5047
+ try {
5048
+ const token = await deps.token();
5049
+ if (!token) return { notArmed: true };
5050
+ const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/docs-audit/list`, {
5051
+ method: "GET",
5052
+ headers: { Authorization: `Bearer ${token}` }
5053
+ });
5054
+ if (res.status === 404) return { notArmed: true };
5055
+ if (!res.ok) return { ok: false, error: `docs-audit list HTTP ${res.status}` };
5056
+ const body = await res.json();
5057
+ return { ok: true, rows: Array.isArray(body?.docsAudits) ? body.docsAudits : [] };
5058
+ } catch (e) {
5059
+ return { ok: false, error: e.message };
5060
+ }
5061
+ }
5042
5062
  async function fetchOrgConfig(deps) {
5043
5063
  if (!deps.baseUrl) return null;
5044
5064
  const token = await deps.token();
@@ -5092,6 +5112,9 @@ async function attestAppGaps(slug, repo, deps) {
5092
5112
  async function setDeployCoords(slug, payload, deps) {
5093
5113
  return postJson(`/projects/${encodeURIComponent(slug)}/deploy`, payload, deps);
5094
5114
  }
5115
+ async function recordDocsAudit(verdict, deps) {
5116
+ return postJson("/docs-audit/record", { ...verdict }, deps);
5117
+ }
5095
5118
  async function tenantControl(payload, deps) {
5096
5119
  return postJson("/tenant-control", payload, deps, "POST", { noRetry: true });
5097
5120
  }
@@ -5257,7 +5280,16 @@ async function removeWorktreeWithRecovery(wtPath, deps) {
5257
5280
  const backoff = deps.backoffMs ?? [250, 1e3];
5258
5281
  let attempts = 0;
5259
5282
  let lastError;
5260
- for (let i = 0; i < maxAttempts; i++) {
5283
+ let safeForGitPrimary = true;
5284
+ if (deps.detachReparsePoints) {
5285
+ try {
5286
+ deps.detachReparsePoints(wtPath);
5287
+ } catch (e) {
5288
+ safeForGitPrimary = false;
5289
+ lastError = e;
5290
+ }
5291
+ }
5292
+ for (let i = 0; safeForGitPrimary && i < maxAttempts; i++) {
5261
5293
  attempts++;
5262
5294
  try {
5263
5295
  await deps.git(["worktree", "remove", "--force", wtPath]);
@@ -5977,6 +6009,8 @@ async function cleanupPrMergeLocalBranch(branch, options) {
5977
6009
  const removeDeps = {
5978
6010
  git,
5979
6011
  sleep: options.sleep ?? defaultSleep,
6012
+ detachReparsePoints: options.detachReparsePoints,
6013
+ // #3064: junction-safe deferred sweep too
5980
6014
  removeWorktreeDir: options.removeWorktreeDir
5981
6015
  };
5982
6016
  await sweepDeferredWorktrees(options.deferredStore, removeDeps).catch(() => void 0);
@@ -5995,6 +6029,8 @@ async function cleanupPrMergeLocalBranch(branch, options) {
5995
6029
  const outcome = await removeWorktreeWithRecovery(wtPath, {
5996
6030
  git,
5997
6031
  sleep: options.sleep ?? defaultSleep,
6032
+ detachReparsePoints: options.detachReparsePoints,
6033
+ // #3064: reach the actual pr-merge teardown path
5998
6034
  removeWorktreeDir: options.removeWorktreeDir
5999
6035
  });
6000
6036
  if (outcome.status === "removed") {
@@ -8152,7 +8188,9 @@ async function unclaimBoardIssue(options, deps = {}) {
8152
8188
  }
8153
8189
  const toStatus = options.toStatus ?? "Todo";
8154
8190
  try {
8155
- await client.rest("DELETE", `repos/${item.repository}/issues/${item.number}/assignees`);
8191
+ await client.rest("DELETE", `repos/${item.repository}/issues/${item.number}/assignees`, {
8192
+ body: { assignees: item.assignees }
8193
+ });
8156
8194
  } catch (e) {
8157
8195
  const warning = `partial unclaim: ${item.ref} assignees were not cleared (${ghError(e)})`;
8158
8196
  if (!options.allowPartial) throw new Error(warning);
@@ -8994,7 +9032,7 @@ function commandLadderHint() {
8994
9032
  }
8995
9033
 
8996
9034
  // src/index.ts
8997
- var import_node_path24 = require("node:path");
9035
+ var import_node_path23 = require("node:path");
8998
9036
 
8999
9037
  // src/merge-ci-policy.ts
9000
9038
  function resolveMergeCiPolicy(input) {
@@ -9512,9 +9550,6 @@ function seedMatchesProjectType(seed, projectType) {
9512
9550
  return projectType != null && seed.projectTypes.includes(projectType);
9513
9551
  }
9514
9552
  function planSeedAction(seed, exists) {
9515
- if (seed.source === "fanout") {
9516
- return { target: seed.target, action: "skip", ownership: "fanout", reason: "delivered by the fanout pipeline" };
9517
- }
9518
9553
  if (seed.source === "managed-block") {
9519
9554
  return exists ? { target: seed.target, action: "update", ownership: "org", reason: "org-managed block merged in-place (repo-owned lines preserved)" } : { target: seed.target, action: "create", ownership: "org", reason: "org-managed block; .gitignore absent, created" };
9520
9555
  }
@@ -9620,7 +9655,6 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
9620
9655
  name: vars.NAME || parsedRepo.name,
9621
9656
  division: vars.DIVISION || parsedRepo.name.split("-")[0] || void 0,
9622
9657
  repos: [`${parsedRepo.owner}/${slug}`],
9623
- wikiRepo: vars.WIKI_REPO || parsedRepo.fullName,
9624
9658
  branch: vars.BRANCH || (cls === "content" ? "main" : "development"),
9625
9659
  class: cls,
9626
9660
  projectType,
@@ -9688,43 +9722,7 @@ function boardFieldsQueryArgs(projectId) {
9688
9722
  const query = "query($id: ID!) { node(id: $id) { ... on ProjectV2 { number fields(first: 50) { nodes { ... on ProjectV2SingleSelectField { id name options { id name } } } } } } }";
9689
9723
  return ["api", "graphql", "-f", `query=${query}`, "-f", `id=${projectId}`];
9690
9724
  }
9691
- function serializeRegistry(obj) {
9692
- return `${JSON.stringify(obj, null, 2)}
9693
- `;
9694
- }
9695
- function planFanoutRegistration(fanoutTargetsRaw, projectsRaw, entry) {
9696
- const fanout = JSON.parse(fanoutTargetsRaw);
9697
- const projects = JSON.parse(projectsRaw);
9698
- const fanoutRepos = Array.isArray(fanout.repos) ? fanout.repos : [];
9699
- const projectEntries = Array.isArray(projects.projects) ? projects.projects : [];
9700
- const name = entry.name ?? entry.repo;
9701
- const canonName = entry.repo.toLowerCase();
9702
- const inFanout = fanoutRepos.some((r) => typeof r.repo === "string" && r.repo.toLowerCase() === canonName);
9703
- const inProjects = projectEntries.some(
9704
- (p) => p.slug === entry.slug || Array.isArray(p.repos) && p.repos.some((full) => String(full).split("/").pop()?.toLowerCase() === canonName)
9705
- );
9706
- if (inFanout || inProjects) {
9707
- return { changed: false, fanoutTargets: fanoutTargetsRaw, projects: projectsRaw };
9708
- }
9709
- const projectEntry = {
9710
- name,
9711
- slug: entry.slug,
9712
- projectId: entry.projectId,
9713
- wikiRepo: entry.wikiRepo,
9714
- repos: [`mutmutco/${entry.repo}`]
9715
- };
9716
- if (entry.cls === "content") projectEntry.branch = "main";
9717
- for (const k of Object.keys(projectEntry)) if (projectEntry[k] === void 0) delete projectEntry[k];
9718
- const nextProjects = { ...projects, projects: [...projectEntries, projectEntry] };
9719
- const fanoutEntry = { repo: entry.repo, branch: entry.branch, class: entry.cls };
9720
- const nextFanout = { ...fanout, repos: [...fanoutRepos, fanoutEntry] };
9721
- return {
9722
- changed: true,
9723
- fanoutTargets: serializeRegistry(nextFanout),
9724
- projects: serializeRegistry(nextProjects)
9725
- };
9726
- }
9727
- function decideFanoutPrAction(openPrs) {
9725
+ function decideSeedPrAction(openPrs) {
9728
9726
  const list = Array.isArray(openPrs) ? openPrs : [];
9729
9727
  for (const pr2 of list) {
9730
9728
  const url = pr2?.url;
@@ -11055,7 +11053,7 @@ async function executeWaveLand(plan, deps, opts = { preserveWorktree: true }) {
11055
11053
  }
11056
11054
 
11057
11055
  // src/index.ts
11058
- var import_node_os9 = require("node:os");
11056
+ var import_node_os8 = require("node:os");
11059
11057
 
11060
11058
  // src/attach-to-project.ts
11061
11059
  async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn = (m) => process.stderr.write(m), retry = {}) {
@@ -11170,6 +11168,21 @@ function parseAddedItemId(stdout) {
11170
11168
  function isAlreadyOnBoardError(stderr) {
11171
11169
  return /already exists in (?:this|the) project/i.test(stderr);
11172
11170
  }
11171
+ var CLOSING_KEYWORD = "clos(?:e|es|ed)|fix(?:es|ed)?|resolve(?:s|d)?";
11172
+ var CLOSING_CHAIN = new RegExp(
11173
+ `\\b(${CLOSING_KEYWORD})(\\s+)#(\\d+)((?:\\s*(?:,|,?\\s+and)\\s*#\\d+)+)`,
11174
+ "gi"
11175
+ );
11176
+ function normalizeClosingDirectives(body) {
11177
+ if (!body.includes("#")) return body;
11178
+ const segments = body.split(/(```[\s\S]*?```|~~~[\s\S]*?~~~|`[^`\n]*`)/g);
11179
+ return segments.map(
11180
+ (seg, i) => i % 2 === 1 ? seg : seg.replace(
11181
+ CLOSING_CHAIN,
11182
+ (_m, kw, ws, first, tail) => `${kw}${ws}#${first}${tail.replace(/#(\d+)/g, "closes #$1")}`
11183
+ )
11184
+ ).join("");
11185
+ }
11173
11186
  function buildPrArgs({ title, body, base, head, repo, draft }) {
11174
11187
  const args = ["pr", "create"];
11175
11188
  if (repo) args.push("--repo", repo);
@@ -11654,9 +11667,106 @@ function runningPluginVersion(env, cliVersion) {
11654
11667
  if (leaf && isVersionDirName(leaf)) return leaf;
11655
11668
  return cliVersion && isVersionDirName(cliVersion) ? cliVersion : void 0;
11656
11669
  }
11670
+ function pluginCacheStagingRoot(home) {
11671
+ return `${home}/.claude/plugins/cache`;
11672
+ }
11673
+ function installedPluginsPath(home) {
11674
+ return `${home}/.claude/plugins/installed_plugins.json`;
11675
+ }
11676
+ var STAGING_DIR = /^temp_git_\d{13}_[a-z0-9]+$/i;
11677
+ function isStagingDirName(name) {
11678
+ return STAGING_DIR.test(name);
11679
+ }
11680
+ var STAGING_AGE_FLOOR_MS = 5 * 60 * 1e3;
11681
+ function installedPluginPaths(json) {
11682
+ const parsed = JSON.parse(json);
11683
+ if (parsed.plugins === void 0 || parsed.plugins === null) return null;
11684
+ const out = [];
11685
+ for (const entries of Object.values(parsed.plugins)) {
11686
+ for (const e of entries ?? []) if (e?.installPath) out.push(e.installPath);
11687
+ }
11688
+ return out;
11689
+ }
11690
+ function newestMtimeMs(path2, listDir, mtimeOf) {
11691
+ let newest = 0;
11692
+ try {
11693
+ newest = mtimeOf(path2);
11694
+ } catch {
11695
+ }
11696
+ let entries;
11697
+ try {
11698
+ entries = listDir(path2);
11699
+ } catch {
11700
+ return newest;
11701
+ }
11702
+ for (const e of entries) {
11703
+ const child = `${path2}/${e.name}`;
11704
+ let m = 0;
11705
+ if (e.isDirectory) m = newestMtimeMs(child, listDir, mtimeOf);
11706
+ else {
11707
+ try {
11708
+ m = mtimeOf(child);
11709
+ } catch {
11710
+ m = 0;
11711
+ }
11712
+ }
11713
+ if (m > newest) newest = m;
11714
+ }
11715
+ return newest;
11716
+ }
11717
+ function normStagingPath(p) {
11718
+ return p.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
11719
+ }
11720
+ function stagingIsReferenced(dirPath, referenced) {
11721
+ const d = normStagingPath(dirPath);
11722
+ for (const r of referenced) {
11723
+ if (r === d || r.startsWith(`${d}/`)) return true;
11724
+ }
11725
+ return false;
11726
+ }
11727
+ function stagingDirIsSweepable(stagingRoot, entry, referencedPaths, now) {
11728
+ if (referencedPaths === null) return false;
11729
+ if (!isStagingDirName(entry.name)) return false;
11730
+ if (now - entry.mtimeMs < STAGING_AGE_FLOOR_MS) return false;
11731
+ const referenced = new Set(referencedPaths.map(normStagingPath));
11732
+ return !stagingIsReferenced(`${stagingRoot}/${entry.name}`, referenced);
11733
+ }
11734
+ function selectOrphanStagingDirs(stagingRoot, entries, referencedPaths, now, bytesOf) {
11735
+ if (referencedPaths === null) return [];
11736
+ return entries.filter((e) => stagingDirIsSweepable(stagingRoot, e, referencedPaths, now)).map((e) => ({ name: e.name, ageMs: now - e.mtimeMs, bytes: bytesOf(e.name) }));
11737
+ }
11738
+ function humanStagingAge(ms) {
11739
+ const minutes = Math.floor(ms / 6e4);
11740
+ if (minutes < 60) return `${minutes}m old`;
11741
+ const hours = Math.floor(minutes / 60);
11742
+ if (hours < 24) return `${hours}h old`;
11743
+ return `${Math.floor(hours / 24)}d old`;
11744
+ }
11657
11745
  function buildPluginCachePlan(home, running, deps, opts = {}) {
11658
11746
  const cacheRoot = pluginCacheRoot(home);
11659
- const absent = { cacheRoot, present: false, keep: [], prune: [], bytes: 0, running };
11747
+ const stagingRoot = pluginCacheStagingRoot(home);
11748
+ let stagingEntries;
11749
+ try {
11750
+ stagingEntries = deps.listStagingDirs(stagingRoot);
11751
+ } catch {
11752
+ stagingEntries = [];
11753
+ }
11754
+ let referenced;
11755
+ try {
11756
+ referenced = deps.readInstalledPluginPaths();
11757
+ } catch {
11758
+ referenced = null;
11759
+ }
11760
+ const staging = selectOrphanStagingDirs(
11761
+ stagingRoot,
11762
+ stagingEntries,
11763
+ referenced,
11764
+ deps.now(),
11765
+ (name) => opts.withBytes ? deps.dirBytes(`${stagingRoot}/${name}`) : 0
11766
+ );
11767
+ const stagingBytes = staging.reduce((sum, s) => sum + s.bytes, 0);
11768
+ const stagingFields = { stagingRoot, staging, stagingBytes };
11769
+ const absent = { cacheRoot, present: false, keep: [], prune: [], bytes: 0, running, ...stagingFields };
11660
11770
  if (!deps.exists(cacheRoot)) return absent;
11661
11771
  let names;
11662
11772
  try {
@@ -11669,9 +11779,9 @@ function buildPluginCachePlan(home, running, deps, opts = {}) {
11669
11779
  const pruneSet = new Set(prune);
11670
11780
  const keep = [...versions].sort((a, b) => compareVersions(b, a)).filter((v) => !pruneSet.has(v));
11671
11781
  const bytes = opts.withBytes ? prune.reduce((sum, v) => sum + deps.dirBytes(`${cacheRoot}/${v}`), 0) : 0;
11672
- return { cacheRoot, present: true, keep, prune, bytes, running };
11782
+ return { cacheRoot, present: true, keep, prune, bytes, running, ...stagingFields };
11673
11783
  }
11674
- function applyPluginCachePlan(plan, remove) {
11784
+ function applyPluginCachePlan(plan, remove, stagingGuard) {
11675
11785
  const removed = [];
11676
11786
  const failed = [];
11677
11787
  for (const version of plan.prune) {
@@ -11682,28 +11792,85 @@ function applyPluginCachePlan(plan, remove) {
11682
11792
  failed.push({ version, error: e.message });
11683
11793
  }
11684
11794
  }
11685
- return { removed, failed, bytes: plan.bytes };
11795
+ const removedStaging = [];
11796
+ const failedStaging = [];
11797
+ const skippedStaging = [];
11798
+ const guardRefs = stagingGuard ? stagingGuard.referencedPaths() : void 0;
11799
+ const guardNow = stagingGuard ? stagingGuard.now() : 0;
11800
+ for (const s of plan.staging) {
11801
+ if (stagingGuard) {
11802
+ if (guardRefs == null) {
11803
+ skippedStaging.push({ name: s.name, reason: "reference set became unknown before apply" });
11804
+ continue;
11805
+ }
11806
+ const m = stagingGuard.mtimeMs(s.name);
11807
+ if (m === null) {
11808
+ skippedStaging.push({ name: s.name, reason: "dir vanished before apply" });
11809
+ continue;
11810
+ }
11811
+ if (!stagingDirIsSweepable(plan.stagingRoot, { name: s.name, mtimeMs: m }, guardRefs, guardNow)) {
11812
+ skippedStaging.push({ name: s.name, reason: "became referenced or active (under age floor) before apply" });
11813
+ continue;
11814
+ }
11815
+ }
11816
+ try {
11817
+ remove(`${plan.stagingRoot}/${s.name}`);
11818
+ removedStaging.push(s.name);
11819
+ } catch (e) {
11820
+ failedStaging.push({ name: s.name, error: e.message });
11821
+ }
11822
+ }
11823
+ return {
11824
+ removed,
11825
+ failed,
11826
+ bytes: plan.bytes,
11827
+ removedStaging,
11828
+ failedStaging,
11829
+ skippedStaging,
11830
+ stagingBytes: plan.stagingBytes
11831
+ };
11686
11832
  }
11687
11833
  var CONCURRENT_SESSION_WARNING = "only the version THIS session runs from is protected \u2014 a concurrent Claude session running from a pruned version will break mid-session; close other sessions before --apply";
11688
11834
  function renderPluginCachePlan(plan, applied) {
11689
- if (!plan.present) return `plugin-prune: no plugin cache at ${plan.cacheRoot} \u2014 nothing to prune`;
11690
11835
  const mb = (b) => `${(b / 1e6).toFixed(1)} MB`;
11691
- const lines = [` cache: ${plan.cacheRoot}`];
11692
- const keepLabel = plan.keep.map((v) => v === plan.running ? `${v} (running)` : v).join(", ") || "(none)";
11693
- lines.push(` keep ${keepLabel}`);
11694
- if (plan.prune.length === 0) {
11695
- lines.push(" prune nothing stale");
11696
- return lines.join("\n");
11836
+ if (!plan.present && plan.staging.length === 0) {
11837
+ return `plugin-prune: no plugin cache at ${plan.cacheRoot} \u2014 nothing to prune`;
11838
+ }
11839
+ const lines = [];
11840
+ if (plan.present) {
11841
+ lines.push(` cache: ${plan.cacheRoot}`);
11842
+ const keepLabel = plan.keep.map((v) => v === plan.running ? `${v} (running)` : v).join(", ") || "(none)";
11843
+ lines.push(` keep ${keepLabel}`);
11844
+ if (plan.prune.length === 0) {
11845
+ lines.push(" prune nothing stale");
11846
+ } else {
11847
+ const size = plan.bytes > 0 ? `, ${mb(plan.bytes)}` : "";
11848
+ lines.push(` prune ${plan.prune.join(" ")} (${plan.prune.length} dirs${size})`);
11849
+ lines.push(` ! ${CONCURRENT_SESSION_WARNING}`);
11850
+ }
11851
+ }
11852
+ if (plan.staging.length > 0) {
11853
+ lines.push(` staging: ${plan.stagingRoot}`);
11854
+ for (const s of plan.staging) {
11855
+ const size = s.bytes > 0 ? `, ${mb(s.bytes)}` : "";
11856
+ lines.push(` orphan ${s.name} (${humanStagingAge(s.ageMs)}${size})`);
11857
+ }
11858
+ const total = plan.stagingBytes > 0 ? `, ${mb(plan.stagingBytes)}` : "";
11859
+ lines.push(` ${plan.staging.length} orphaned staging dir(s)${total}`);
11697
11860
  }
11698
- const size = plan.bytes > 0 ? `, ${mb(plan.bytes)}` : "";
11699
- lines.push(` prune ${plan.prune.join(" ")} (${plan.prune.length} dirs${size})`);
11700
- lines.push(` ! ${CONCURRENT_SESSION_WARNING}`);
11701
11861
  if (!applied) {
11702
- lines.push(" \u2192 re-run with --apply to delete");
11862
+ if (plan.prune.length > 0 || plan.staging.length > 0) lines.push(" \u2192 re-run with --apply to delete");
11703
11863
  return lines.join("\n");
11704
11864
  }
11705
- lines.push(` removed ${applied.removed.length} dirs (${mb(applied.bytes)})`);
11706
- for (const f of applied.failed) lines.push(` \u2717 ${f.version}: ${f.error}`);
11865
+ if (plan.prune.length > 0) {
11866
+ lines.push(` removed ${applied.removed.length} version dir(s) (${mb(applied.bytes)})`);
11867
+ for (const f of applied.failed) lines.push(` \u2717 ${f.version}: ${f.error}`);
11868
+ }
11869
+ if (plan.staging.length > 0) {
11870
+ lines.push(` removed ${applied.removedStaging.length} staging dir(s) (${mb(applied.stagingBytes)})`);
11871
+ for (const f of applied.failedStaging) lines.push(` \u2717 ${f.name}: ${f.error}`);
11872
+ for (const s of applied.skippedStaging) lines.push(` ~ skipped ${s.name}: ${s.reason}`);
11873
+ }
11707
11874
  return lines.join("\n");
11708
11875
  }
11709
11876
 
@@ -11880,8 +12047,7 @@ function bootstrapPlan(repo, repoClass) {
11880
12047
  { label: `provision branch model: ${branchModel}`, gated: true },
11881
12048
  { label: `apply branch protection / allowlist: ${protectedBranches}`, gated: true },
11882
12049
  { label: "attach GitHub Project v2 and register Hub registry META", gated: true },
11883
- { label: "seed README.md and architecture.md", gated: true },
11884
- { label: `register fanout target on ${repoClass === "content" ? "main" : "development"}`, gated: true }
12050
+ { label: "seed README.md and architecture.md", gated: true }
11885
12051
  ];
11886
12052
  }
11887
12053
  function renderSteps(title, steps) {
@@ -13534,6 +13700,38 @@ function checkHotfixCoverage(options = {}) {
13534
13700
  const uncovered = commits.filter((c) => c.coverage === "uncovered");
13535
13701
  return { ok: uncovered.length === 0, mainRef, rcRef, commits, uncovered };
13536
13702
  }
13703
+ function checkHotfixCarries(options) {
13704
+ const { cwd = process.cwd(), branch, baseRef, targets } = options;
13705
+ const git = options.git ?? ((args, opts) => (0, import_node_child_process8.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
13706
+ const isAncestor = (sha, ref) => {
13707
+ try {
13708
+ git(["merge-base", "--is-ancestor", sha, ref]);
13709
+ return true;
13710
+ } catch {
13711
+ return false;
13712
+ }
13713
+ };
13714
+ const revList = (range) => {
13715
+ const out = git(["rev-list", "--no-merges", range]).trim();
13716
+ return out ? out.split("\n") : [];
13717
+ };
13718
+ const cherrySources = (sha) => {
13719
+ const message = git(["log", "-1", "--format=%B", sha]);
13720
+ return [...message.matchAll(CHERRY_TRAILER)].map((m) => m[1]);
13721
+ };
13722
+ const carried = /* @__PURE__ */ new Set();
13723
+ for (const sha of revList(`${baseRef}..${branch}`)) {
13724
+ for (const source of cherrySources(sha)) carried.add(source);
13725
+ }
13726
+ const shaMatches = (a, b) => a.startsWith(b) || b.startsWith(a);
13727
+ const evaluated = targets.map((t) => {
13728
+ if (isAncestor(t.sha, branch)) return { ...t, carried: true, via: "ancestor" };
13729
+ if ([...carried].some((s) => shaMatches(s, t.sha))) return { ...t, carried: true, via: "trailer" };
13730
+ return { ...t, carried: false };
13731
+ });
13732
+ const missing = evaluated.filter((t) => !t.carried);
13733
+ return { ok: missing.length === 0, branch, baseRef, targets: evaluated, missing };
13734
+ }
13537
13735
 
13538
13736
  // src/tenant-sweep.ts
13539
13737
  function isRcBearingTenant(p) {
@@ -13635,6 +13833,7 @@ async function resolveHotfixDeployModel(deps, ctx) {
13635
13833
  const meta = requireProjectMetaForTrain(load, ctx.repo);
13636
13834
  return resolveDeployModel2(meta, ctx.repo);
13637
13835
  }
13836
+ var HOTFIX_CARRIES_MARKER = /<!--\s*mmi-hotfix-carries:\s*([^>]+?)\s*-->/i;
13638
13837
  async function findHotfixPr(deps, ctx, tag) {
13639
13838
  const out = await deps.run("gh", [
13640
13839
  "pr",
@@ -13648,7 +13847,7 @@ async function findHotfixPr(deps, ctx, tag) {
13648
13847
  "--limit",
13649
13848
  "50",
13650
13849
  "--json",
13651
- "number,state,url,mergeCommit,headRefName"
13850
+ "number,state,url,body,mergeCommit,headRefName"
13652
13851
  ]);
13653
13852
  const rows = JSON.parse(out || "[]");
13654
13853
  const branch = hotfixBranch(tag);
@@ -13670,6 +13869,31 @@ async function resolveHotfixSource(deps, ctx, from) {
13670
13869
  if (!sha) throw new Error(`could not resolve commit ${from}`);
13671
13870
  return { sha, label: sha.slice(0, 7) };
13672
13871
  }
13872
+ function splitCarrySpecs(values = []) {
13873
+ return values.flatMap((v) => v.split(",")).map((s) => s.trim()).filter(Boolean);
13874
+ }
13875
+ function carrySpecsFromPrBody(body) {
13876
+ const marker = HOTFIX_CARRIES_MARKER.exec(body ?? "");
13877
+ if (marker) return splitCarrySpecs([marker[1]]);
13878
+ const specs = [];
13879
+ for (const m of (body ?? "").matchAll(/\bPR\s+#(\d+)\b/gi)) specs.push(`#${m[1]}`);
13880
+ for (const m of (body ?? "").matchAll(/\b[0-9a-f]{7,40}\b/gi)) specs.push(m[0]);
13881
+ return [...new Set(specs)];
13882
+ }
13883
+ async function resolveHotfixCarryTargets(deps, ctx, pr2, explicit = []) {
13884
+ const specs = splitCarrySpecs(explicit).length > 0 ? splitCarrySpecs(explicit) : carrySpecsFromPrBody(pr2.body);
13885
+ const targets = [];
13886
+ for (const spec of specs) {
13887
+ const { sha, label } = await resolveHotfixSource(deps, ctx, spec);
13888
+ if (!targets.some((t) => t.sha === sha)) targets.push({ sha, label });
13889
+ }
13890
+ return targets;
13891
+ }
13892
+ async function hotfixBaseTagForRelease(deps, tag) {
13893
+ const tags = (await deps.run("git", ["tag", "--list", "v[0-9]*.[0-9]*.[0-9]*", "--merged", "origin/main", "--sort=-v:refname"])).split("\n").map((s) => s.trim()).filter((t) => /^v\d+\.\d+\.\d+$/.test(t));
13894
+ const current = normalizeHotfixVersion(tag).tag;
13895
+ return tags.find((t) => compareHotfixVersions(t, current) < 0) ?? tags.find((t) => t !== current) ?? "origin/main^";
13896
+ }
13673
13897
  async function runHotfixStart(deps, options) {
13674
13898
  const ctx = await buildTrainApplyContext(deps);
13675
13899
  const deployModel = await resolveHotfixDeployModel(deps, ctx);
@@ -13739,6 +13963,8 @@ async function runHotfixStart(deps, options) {
13739
13963
  "--body",
13740
13964
  `Hotfix ${tag}: cherry-pick of ${label} onto origin/main${bumpNote}.
13741
13965
 
13966
+ <!-- mmi-hotfix-carries: ${sha} -->
13967
+
13742
13968
  Merge this PR (human-initiated), then run \`mmi-cli hotfix release ${tag}\`.`
13743
13969
  ]));
13744
13970
  notes.push(`opened hotfix PR ${prUrl} \u2014 merge it (human-initiated), then: mmi-cli hotfix release ${tag}`);
@@ -13795,6 +14021,15 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
13795
14021
  await deps.run("git", ["merge-base", "--is-ancestor", mergedSha, "origin/main"]).catch(() => {
13796
14022
  throw new Error(`merged hotfix SHA ${mergedSha.slice(0, 7)} is not on origin/main \u2014 refusing to tag`);
13797
14023
  });
14024
+ const carryTargets = await resolveHotfixCarryTargets(deps, ctx, pr2, options.carries);
14025
+ if (carryTargets.length > 0) {
14026
+ const baseRef = await hotfixBaseTagForRelease(deps, tag);
14027
+ const carries = deps.hotfixCarries ? deps.hotfixCarries({ branch: mergedSha, baseRef, targets: carryTargets }) : { ok: true, branch: mergedSha, baseRef, targets: carryTargets.map((t) => ({ ...t, carried: true, via: "ancestor" })), missing: [] };
14028
+ if (!carries.ok) {
14029
+ const missing = carries.missing.map((t) => t.label ?? t.sha.slice(0, 12)).join(", ");
14030
+ throw new Error(`hotfix-carries: ${carries.missing.length} declared fix target(s) missing from ${tag} before tagging: ${missing}. Cherry-pick the missing fix(es) with -x onto ${hotfixBranch(tag)}, merge that PR, then rerun hotfix release.`);
14031
+ }
14032
+ }
13798
14033
  const tagPush = await ensureTagPushed(deps, tag, mergedSha);
13799
14034
  const tagPushSince = Date.now();
13800
14035
  const tagNote = tagPush.note;
@@ -14301,7 +14536,7 @@ async function auditTrainBranch(repo, branch, owners, deps, projectAdmins = /* @
14301
14536
  branch,
14302
14537
  kind: "app-bypass-missing",
14303
14538
  severity: "high",
14304
- detail: `the ${LOCKED_APP} App is missing from the ${branch} allowlist \u2014 fanout/promotions will break`,
14539
+ detail: `the ${LOCKED_APP} App is missing from the ${branch} allowlist \u2014 release promotions will break`,
14305
14540
  remediation: `gh api -X POST repos/${repo}/branches/${branch}/protection/restrictions/apps -f apps[]="${LOCKED_APP}"`
14306
14541
  });
14307
14542
  }
@@ -14369,6 +14604,20 @@ async function auditPluginReadAccess(owners, projectAdmins, deps) {
14369
14604
  return findings;
14370
14605
  }
14371
14606
  async function auditOrgAccess(targets, deps, matrix = {}, dataAccess) {
14607
+ if (targets.length === 0) {
14608
+ return {
14609
+ ok: false,
14610
+ owners: [],
14611
+ orgFindings: [{
14612
+ repo: OWNER2,
14613
+ kind: "empty-target-inventory",
14614
+ severity: "high",
14615
+ detail: "access audit received an empty repo inventory \u2014 zero repos to audit; a green audit of nothing is a false pass",
14616
+ remediation: `confirm the Hub registry is reachable and returns projects, then re-run \`mmi-cli access audit\`; or audit one repo explicitly with \`mmi-cli access audit --repo <owner/repo>\``
14617
+ }],
14618
+ repos: []
14619
+ };
14620
+ }
14372
14621
  let owners;
14373
14622
  try {
14374
14623
  owners = new Set(await resolveOwners(deps));
@@ -14396,10 +14645,8 @@ async function auditOrgAccess(targets, deps, matrix = {}, dataAccess) {
14396
14645
  const ok = orgFindings.every((f) => f.severity !== "high") && repos.every((r) => r.ok);
14397
14646
  return { ok, owners: [...owners], orgFindings, repos };
14398
14647
  }
14399
- function loadAccessTargets(projectsJson, fanoutJson) {
14648
+ function loadAccessTargets(projectsJson) {
14400
14649
  const projects = safeJson(projectsJson, {}).projects ?? [];
14401
- const fanout = fanoutJson ? safeJson(fanoutJson, {}).repos ?? [] : [];
14402
- const legacyContentNames = new Set(fanout.filter((r) => r.class === "content" && r.repo).map((r) => r.repo.toLowerCase()));
14403
14650
  const seen = /* @__PURE__ */ new Set();
14404
14651
  const targets = [];
14405
14652
  for (const project2 of projects) {
@@ -14410,13 +14657,28 @@ function loadAccessTargets(projectsJson, fanoutJson) {
14410
14657
  if (seen.has(repo)) continue;
14411
14658
  seen.add(repo);
14412
14659
  const repoName = repo.split("/").pop()?.toLowerCase() ?? repo.toLowerCase();
14413
- const cls = project2.class === "content" || project2.deployModel === "content" || embeddedContent.has(repo.toLowerCase()) || embeddedContent.has(repoName) || legacyContentNames.has(repo.toLowerCase()) || legacyContentNames.has(repoName) ? "content" : "deployable";
14660
+ const cls = project2.class === "content" || project2.deployModel === "content" || embeddedContent.has(repo.toLowerCase()) || embeddedContent.has(repoName) ? "content" : "deployable";
14414
14661
  const releaseTrack = cls === "content" ? "trunk" : resolveReleaseTrack(project2, void 0, repo);
14415
14662
  targets.push({ repo, class: cls, releaseTrack });
14416
14663
  }
14417
14664
  }
14418
14665
  return targets;
14419
14666
  }
14667
+ function resolveOrgAuditTargets(registryProjects) {
14668
+ if (registryProjects === null) {
14669
+ return {
14670
+ ok: false,
14671
+ finding: {
14672
+ repo: OWNER2,
14673
+ kind: "registry-unreadable",
14674
+ severity: "high",
14675
+ detail: "the project registry SSOT (Hub API) was unreachable or unconfigured \u2014 the audit target set is unknown; auditing the stale committed projects.json instead could pass green while missing live repos",
14676
+ remediation: `confirm the Hub API base URL + token are configured and reachable, then re-run \`mmi-cli access audit\`; or audit one repo explicitly with \`mmi-cli access audit --repo <owner/repo>\``
14677
+ }
14678
+ };
14679
+ }
14680
+ return { ok: true, targets: loadAccessTargets(JSON.stringify({ projects: registryProjects })) };
14681
+ }
14420
14682
  function loadAccessMatrix(matrixJson) {
14421
14683
  if (!matrixJson) return {};
14422
14684
  return safeJson(matrixJson, {}).projectAdmins ?? {};
@@ -14471,279 +14733,202 @@ function renderAccessReport(report) {
14471
14733
  return lines.join("\n");
14472
14734
  }
14473
14735
 
14474
- // src/wiki-publish-command.ts
14475
- async function wikiPublish(deps, opts) {
14476
- if (!opts.pagesDir) {
14477
- if (opts.json) {
14478
- deps.log(JSON.stringify({ ok: false, error: "pages-dir is required" }, null, 2));
14479
- } else {
14480
- deps.err("wiki publish: <pages-dir> is required");
14481
- }
14482
- return false;
14483
- }
14484
- const minted = await deps.mint(opts.repo);
14485
- if (!minted.ok) {
14486
- if (opts.json) {
14487
- deps.log(JSON.stringify({ ok: false, error: minted.error }, null, 2));
14488
- } else {
14489
- deps.err(`wiki publish failed: could not mint a scoped wiki token for ${opts.repo}: ${minted.error}`);
14490
- }
14491
- return false;
14492
- }
14493
- const ok = deps.publish({ repo: opts.repo, token: minted.mint.token, pagesDir: opts.pagesDir, json: opts.json });
14494
- if (!ok) {
14495
- return false;
14496
- }
14497
- if (opts.json) {
14498
- deps.log(JSON.stringify({ ok: true, repo: opts.repo }, null, 2));
14499
- } else {
14500
- deps.log(`wiki published for ${opts.repo} (scoped token, expired/discarded)`);
14501
- }
14502
- return true;
14503
- }
14504
-
14505
- // src/wiki-publish-git.ts
14506
- var import_node_child_process9 = require("node:child_process");
14736
+ // src/docs-index-command.ts
14507
14737
  var import_node_fs16 = require("node:fs");
14508
- var import_node_os5 = require("node:os");
14509
14738
  var import_node_path14 = require("node:path");
14510
- var WIKI_STAMP_FILE = ".wiki-state.json";
14511
- function redactWikiSecrets(msg) {
14512
- return msg.replace(/x-access-token:[^@\s]+@/gi, "x-access-token:***@").replace(/(AUTHORIZATION:\s*basic\s+)[A-Za-z0-9+/=]+/gi, "$1***");
14513
- }
14514
- function wikiAuthArgs(token) {
14515
- return ["-c", `http.extraheader=AUTHORIZATION: basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`];
14516
- }
14517
- function publishWikiViaGit(deps, opts) {
14518
- const dir = deps.mkdtemp();
14519
- const url = `https://github.com/${opts.repo}.wiki.git`;
14520
- const authArgs = wikiAuthArgs(opts.token);
14521
- const json = opts.json ?? false;
14522
- try {
14523
- deps.gitRun(["init", "-q"], dir);
14524
- deps.gitRun(["remote", "add", "origin", url], dir);
14525
- if (deps.gitProbe([...authArgs, "fetch", "--depth", "1", "origin", "master"], dir)) {
14526
- deps.gitRun(["reset", "--hard", "FETCH_HEAD"], dir);
14527
- }
14528
- let copied = 0;
14529
- for (const f of deps.readdir(opts.pagesDir)) {
14530
- if (f.endsWith(".md")) {
14531
- deps.copyFile((0, import_node_path14.join)(opts.pagesDir, f), (0, import_node_path14.join)(dir, f));
14532
- copied++;
14533
- }
14534
- }
14535
- if (!copied) {
14536
- if (!json) deps.err(`wiki publish: no .md pages found in ${opts.pagesDir}`);
14537
- return false;
14538
- }
14539
- if (deps.fileExists((0, import_node_path14.join)(opts.pagesDir, WIKI_STAMP_FILE))) {
14540
- deps.copyFile((0, import_node_path14.join)(opts.pagesDir, WIKI_STAMP_FILE), (0, import_node_path14.join)(dir, WIKI_STAMP_FILE));
14739
+ var DOCS_INDEX_PATH = "docs/index.md";
14740
+ var GENERATED_HEADER = "<!-- Generated by `mmi-cli docs index --write`. Do not edit by hand. -->";
14741
+ function escapeCell(text) {
14742
+ return text.replace(/\r?\n/g, " ").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
14743
+ }
14744
+ function extractDocMeta(relPath, raw) {
14745
+ const lines = raw.split(/\r?\n/);
14746
+ let i = 0;
14747
+ if (lines[i]?.trim() === "---") {
14748
+ i++;
14749
+ while (i < lines.length && lines[i].trim() !== "---") i++;
14750
+ if (i < lines.length) i++;
14751
+ }
14752
+ let title = "";
14753
+ let scope = "";
14754
+ for (; i < lines.length; i++) {
14755
+ const line = lines[i];
14756
+ const trimmed = line.trim();
14757
+ if (trimmed === "") continue;
14758
+ if (trimmed.startsWith("<!--")) {
14759
+ while (i < lines.length && !lines[i].includes("-->")) i++;
14760
+ continue;
14541
14761
  }
14542
- deps.gitRun(["add", "-A"], dir);
14543
- if (deps.gitProbe(["diff", "--cached", "--quiet"], dir)) {
14544
- if (!json) deps.log("wiki publish: already up to date");
14545
- return true;
14762
+ if (!title && trimmed.startsWith("# ")) {
14763
+ title = trimmed.slice(2).trim();
14764
+ continue;
14546
14765
  }
14547
- deps.gitRun(
14548
- [
14549
- "-c",
14550
- "user.email=mmi-github-app[bot]@users.noreply.github.com",
14551
- "-c",
14552
- "user.name=mmi-github-app[bot]",
14553
- "commit",
14554
- "-m",
14555
- "docs(wiki): refresh MMI Future project wiki"
14556
- ],
14557
- dir
14558
- );
14559
- try {
14560
- deps.gitRun([...authArgs, "push", "origin", "HEAD:master"], dir);
14561
- } catch (e) {
14562
- if (!json) deps.err(
14563
- `wiki publish failed: push to ${opts.repo}.wiki.git rejected: ${redactWikiSecrets(e.message)} \u2014 if the wiki advanced before this push, re-run wiki publish to pick up the latest wiki state`
14564
- );
14565
- return false;
14766
+ if (!title) {
14767
+ break;
14566
14768
  }
14567
- if (!json) deps.log(`wiki publish: published ${copied} page(s) to ${opts.repo} wiki`);
14568
- return true;
14569
- } catch (e) {
14570
- if (!json) deps.err(`wiki publish failed: ${redactWikiSecrets(e.message)}`);
14571
- return false;
14572
- } finally {
14573
- deps.rm(dir);
14769
+ if (trimmed.startsWith("#")) continue;
14770
+ scope = escapeCell(trimmed);
14771
+ break;
14574
14772
  }
14575
- }
14576
- function readWikiStampViaGit(deps, opts) {
14577
- const dir = deps.mkdtemp();
14578
- const url = `https://github.com/${opts.repo}.wiki.git`;
14579
- const authArgs = wikiAuthArgs(opts.token);
14580
- try {
14581
- deps.gitRun(["init", "-q"], dir);
14582
- deps.gitRun(["remote", "add", "origin", url], dir);
14583
- if (!deps.gitProbe([...authArgs, "ls-remote", "origin"], dir)) {
14584
- return { ok: false, error: `could not reach ${opts.repo}.wiki.git to read the wiki stamp \u2014 auth or network failure` };
14585
- }
14586
- if (!deps.gitProbe([...authArgs, "fetch", "--depth", "1", "origin", "master"], dir)) {
14587
- return { ok: true, stamp: {} };
14588
- }
14589
- deps.gitRun(["reset", "--hard", "FETCH_HEAD"], dir);
14590
- const stampPath = (0, import_node_path14.join)(dir, WIKI_STAMP_FILE);
14591
- if (!deps.fileExists(stampPath)) return { ok: true, stamp: {} };
14592
- let parsed;
14593
- try {
14594
- parsed = JSON.parse(deps.readFile(stampPath));
14595
- } catch (e) {
14596
- deps.err(
14597
- `wiki stamp: ${WIKI_STAMP_FILE} in ${opts.repo}.wiki.git is unreadable (${redactWikiSecrets(e.message)}) \u2014 treating as absent (full backfill); the next wiki publish rewrites it`
14598
- );
14599
- return { ok: true, stamp: {} };
14600
- }
14601
- return { ok: true, stamp: parsed && typeof parsed === "object" ? parsed : {} };
14602
- } catch (e) {
14603
- return { ok: false, error: redactWikiSecrets(e.message) };
14604
- } finally {
14605
- deps.rm(dir);
14773
+ if (!title) {
14774
+ const base = relPath.split("/").pop() ?? relPath;
14775
+ title = base.replace(/\.md$/, "");
14606
14776
  }
14777
+ return { path: relPath, title, scope };
14607
14778
  }
14608
- function realWikiGitPublishDeps() {
14609
- return {
14610
- mkdtemp: () => (0, import_node_fs16.mkdtempSync)((0, import_node_path14.join)((0, import_node_os5.tmpdir)(), "wiki-")),
14611
- readdir: (dir) => (0, import_node_fs16.readdirSync)(dir),
14612
- copyFile: (src, dest) => (0, import_node_fs16.copyFileSync)(src, dest),
14613
- fileExists: (p) => (0, import_node_fs16.existsSync)(p),
14614
- readFile: (p) => (0, import_node_fs16.readFileSync)(p, "utf8"),
14615
- rm: (p) => (0, import_node_fs16.rmSync)(p, { recursive: true, force: true }),
14616
- gitProbe: (args, cwd) => {
14617
- try {
14618
- (0, import_node_child_process9.execFileSync)("git", args, { cwd, stdio: "pipe" });
14619
- return true;
14620
- } catch {
14621
- return false;
14622
- }
14623
- },
14624
- gitRun: (args, cwd) => {
14625
- (0, import_node_child_process9.execFileSync)("git", args, { cwd, stdio: "inherit" });
14626
- },
14627
- log: (msg) => console.log(msg),
14628
- err: (msg) => console.error(msg)
14629
- };
14630
- }
14631
-
14632
- // src/wiki-pages.ts
14633
- var import_node_crypto4 = require("node:crypto");
14634
- var import_node_fs17 = require("node:fs");
14635
- var import_node_path15 = require("node:path");
14636
- var MARKER_RE = /<!--\s*wiki:page\s+([^>]*?)-->/g;
14637
- var attr = (raw, key) => {
14638
- const m = new RegExp(`${key}\\s*=\\s*"([^"]*)"`).exec(raw);
14639
- return m ? m[1].trim() : "";
14640
- };
14641
- var sanitizeSlug = (s) => String(s ?? "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
14642
- function extractWikiMarkers(text, sourcePath = "") {
14643
- const out = [];
14644
- for (const m of String(text ?? "").matchAll(MARKER_RE)) {
14645
- const raw = m[1];
14646
- const slug = sanitizeSlug(attr(raw, "slug"));
14647
- const title = attr(raw, "title");
14648
- if (!slug || !title) continue;
14649
- out.push({ slug, title, artifact: attr(raw, "artifact"), sourcePath });
14779
+ function renderDocsIndex(entries) {
14780
+ const lines = [
14781
+ GENERATED_HEADER,
14782
+ "# Documentation index",
14783
+ "",
14784
+ "Generated from the `docs/` tree by `mmi-cli docs index --write`. `--check` fails on drift, so this index",
14785
+ "can never diverge from the records it lists.",
14786
+ "",
14787
+ "| Document | Scope |",
14788
+ "| --- | --- |"
14789
+ ];
14790
+ for (const entry of entries) {
14791
+ lines.push(`| [${escapeCell(entry.title)}](${entry.path}) | ${entry.scope} |`);
14650
14792
  }
14651
- return out;
14793
+ return lines.join("\n") + "\n";
14652
14794
  }
14653
- var DURABLE_GLOBS = [
14654
- "architecture.md",
14655
- "docs/Architecture",
14656
- "docs/org-architecture.md",
14657
- "docs/org-readme.md"
14658
- ];
14659
- var walkMarkdown = (root, rel) => {
14660
- const abs = (0, import_node_path15.join)(root, rel);
14661
- let st;
14662
- try {
14663
- st = (0, import_node_fs17.statSync)(abs);
14664
- } catch {
14665
- return [];
14795
+ function docsIndex(deps, opts = {}) {
14796
+ const entries = deps.listDocs().slice().sort().map((relPath) => extractDocMeta(relPath, deps.readDoc(relPath)));
14797
+ const content = renderDocsIndex(entries);
14798
+ const current = deps.readIndex();
14799
+ const drift = current !== content;
14800
+ let wrote = false;
14801
+ if (!opts.check && drift) {
14802
+ deps.writeIndex(content);
14803
+ wrote = true;
14666
14804
  }
14667
- if (st.isFile()) return rel.endsWith(".md") ? [rel] : [];
14668
- return (0, import_node_fs17.readdirSync)(abs).flatMap((name) => walkMarkdown(root, (0, import_node_path15.join)(rel, name)));
14669
- };
14670
- function collectFeaturePages({ listDocs, readDoc }) {
14671
- const seen = /* @__PURE__ */ new Set();
14672
- const pages = [];
14673
- for (const path2 of listDocs()) {
14674
- for (const mk of extractWikiMarkers(readDoc(path2), path2)) {
14675
- if (seen.has(mk.slug)) continue;
14676
- seen.add(mk.slug);
14677
- pages.push(mk);
14805
+ return { content, drift, wrote };
14806
+ }
14807
+ function walkMarkdown(dir) {
14808
+ const out = [];
14809
+ const stack = [dir];
14810
+ while (stack.length) {
14811
+ const current = stack.pop();
14812
+ for (const entry of (0, import_node_fs16.readdirSync)(current, { withFileTypes: true })) {
14813
+ const full = (0, import_node_path14.join)(current, entry.name);
14814
+ if (entry.isDirectory()) {
14815
+ stack.push(full);
14816
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
14817
+ out.push((0, import_node_path14.relative)(dir, full).split(import_node_path14.sep).join("/"));
14818
+ }
14678
14819
  }
14679
14820
  }
14680
- return pages;
14821
+ return out;
14681
14822
  }
14682
- function fsReaders(rootDir) {
14823
+ function createDocsIndexDeps(repoRoot2) {
14824
+ const docsDir = (0, import_node_path14.join)(repoRoot2, "docs");
14825
+ const indexPath = (0, import_node_path14.join)(repoRoot2, DOCS_INDEX_PATH);
14683
14826
  return {
14684
- listDocs: () => DURABLE_GLOBS.flatMap((g) => walkMarkdown(rootDir, g)),
14685
- readDoc: (p) => {
14686
- try {
14687
- return (0, import_node_fs17.readFileSync)((0, import_node_path15.join)(rootDir, p), "utf8");
14688
- } catch {
14689
- return "";
14690
- }
14691
- }
14827
+ listDocs: () => (0, import_node_fs16.existsSync)(docsDir) ? walkMarkdown(docsDir).filter((p) => p !== "index.md").sort() : [],
14828
+ readDoc: (relPath) => (0, import_node_fs16.readFileSync)((0, import_node_path14.join)(docsDir, relPath), "utf8"),
14829
+ readIndex: () => (0, import_node_fs16.existsSync)(indexPath) ? (0, import_node_fs16.readFileSync)(indexPath, "utf8") : null,
14830
+ writeIndex: (content) => (0, import_node_fs16.writeFileSync)(indexPath, content, "utf8")
14692
14831
  };
14693
14832
  }
14694
- function hashPageInputs({ slug, title, source }) {
14695
- return (0, import_node_crypto4.createHash)("sha256").update(JSON.stringify([slug ?? "", title ?? "", source ?? ""])).digest("hex");
14696
- }
14697
- function computePageStamp(pages, readSource) {
14698
- const stamp = {};
14699
- for (const p of pages ?? []) {
14700
- stamp[p.slug] = {
14701
- hash: hashPageInputs({ slug: p.slug, title: p.title, source: readSource(p.sourcePath) }),
14702
- sourcePath: p.sourcePath
14833
+
14834
+ // src/docs-audit-command.ts
14835
+ function serializeOutcome(outcome) {
14836
+ switch (outcome.kind) {
14837
+ case "clean":
14838
+ return "clean";
14839
+ case "refreshed":
14840
+ return `refreshed-${outcome.count}`;
14841
+ case "failed":
14842
+ return "failed";
14843
+ }
14844
+ }
14845
+ var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
14846
+ function isValidIsoDate(date) {
14847
+ if (typeof date !== "string" || !ISO_DATE_RE.test(date)) return false;
14848
+ const parsed = Date.parse(`${date}T00:00:00Z`);
14849
+ if (Number.isNaN(parsed)) return false;
14850
+ return new Date(parsed).toISOString().slice(0, 10) === date;
14851
+ }
14852
+ var OUTCOME_WIRE_RE = /^(clean|refreshed-[1-9]\d*|failed)$/;
14853
+ function isValidOutcomeWire(outcome) {
14854
+ return typeof outcome === "string" && OUTCOME_WIRE_RE.test(outcome);
14855
+ }
14856
+ function docsAuditRecord(input) {
14857
+ const repo = input.repo.trim();
14858
+ const date = input.date.trim();
14859
+ const shaRange = input.shaRange.trim();
14860
+ const checkerVendor = input.checkerVendor.trim();
14861
+ if (!repo) throw new Error("docs-audit record: repo is required");
14862
+ if (!date) throw new Error("docs-audit record: date is required");
14863
+ if (!isValidIsoDate(date)) throw new Error(`docs-audit record: date must be a real ISO date (YYYY-MM-DD), got "${date}"`);
14864
+ if (!shaRange) throw new Error("docs-audit record: shaRange is required");
14865
+ if (!checkerVendor) throw new Error("docs-audit record: checkerVendor is required");
14866
+ if (input.outcome.kind === "refreshed" && !(Number.isInteger(input.outcome.count) && input.outcome.count > 0)) {
14867
+ throw new Error("docs-audit record: a `refreshed` outcome must carry a positive integer count");
14868
+ }
14869
+ if (input.outcome.kind === "failed" && !input.outcome.reason.trim()) {
14870
+ throw new Error("docs-audit record: a `failed` outcome must carry a reason");
14871
+ }
14872
+ const outcome = serializeOutcome(input.outcome);
14873
+ if (!isValidOutcomeWire(outcome)) {
14874
+ throw new Error(`docs-audit record: outcome serialized to an invalid wire form "${outcome}"`);
14875
+ }
14876
+ return { repo, date, shaRange, outcome, checkerVendor };
14877
+ }
14878
+ function ageInDays(verdictDate, today) {
14879
+ const a = Date.parse(`${verdictDate}T00:00:00Z`);
14880
+ const b = Date.parse(`${today}T00:00:00Z`);
14881
+ return Math.max(0, Math.round((b - a) / 864e5));
14882
+ }
14883
+ function docsAuditStatus(fetch2, opts) {
14884
+ if ("notArmed" in fetch2) {
14885
+ return { ok: true, state: "not-armed", line: `docs-audit: ${opts.repo} janitor not armed (registry route not live yet)` };
14886
+ }
14887
+ if (!fetch2.ok) {
14888
+ return { ok: false, state: "error", line: `docs-audit: ${opts.repo} registry read failed \u2014 ${fetch2.error}` };
14889
+ }
14890
+ if (fetch2.verdict === null) {
14891
+ return { ok: false, state: "missing", line: `docs-audit: ${opts.repo} no verdict on record \u2014 janitor blind or dead` };
14892
+ }
14893
+ const verdict = fetch2.verdict;
14894
+ for (const field of ["repo", "shaRange", "checkerVendor"]) {
14895
+ const value = verdict[field];
14896
+ if (typeof value !== "string" || !value.trim()) {
14897
+ const shown = typeof value === "string" ? `"${value}"` : `type ${Array.isArray(value) ? "array" : typeof value}`;
14898
+ return {
14899
+ ok: false,
14900
+ state: "malformed",
14901
+ line: `docs-audit: ${opts.repo} malformed verdict ${field} (${shown}, expected a non-empty string) \u2014 registry record corrupt, treating as RED`
14902
+ };
14903
+ }
14904
+ }
14905
+ if (!isValidIsoDate(verdict.date)) {
14906
+ return {
14907
+ ok: false,
14908
+ state: "malformed",
14909
+ line: `docs-audit: ${opts.repo} malformed verdict date "${verdict.date}" \u2014 registry record corrupt, treating as RED`
14703
14910
  };
14704
14911
  }
14705
- return stamp;
14706
- }
14707
- function selectChangedPages(pages, prevStamp, readSource) {
14708
- const prev = prevStamp && typeof prevStamp === "object" ? prevStamp : {};
14709
- const backfill = Object.keys(prev).length === 0;
14710
- return (pages ?? []).filter((p) => {
14711
- if (backfill) return true;
14712
- const prior = prev[p.slug];
14713
- if (!prior) return true;
14714
- const cur = hashPageInputs({ slug: p.slug, title: p.title, source: readSource(p.sourcePath) });
14715
- return prior.hash !== cur;
14716
- });
14717
- }
14718
-
14719
- // src/wiki-plan-command.ts
14720
- async function wikiPlan(deps, opts) {
14721
- const wikiRepo = `${opts.repo}.wiki.git`;
14722
- const pages = deps.collectPages();
14723
- if (pages.length === 0) {
14724
- emit(deps, { repo: opts.repo, wikiRepo, backfill: false, pages: [], stamp: {} });
14725
- return true;
14912
+ if (!isValidOutcomeWire(verdict.outcome)) {
14913
+ return {
14914
+ ok: false,
14915
+ state: "malformed",
14916
+ line: `docs-audit: ${opts.repo} malformed verdict outcome "${verdict.outcome}" (expected clean|refreshed-N|failed) \u2014 registry record corrupt, treating as RED`
14917
+ };
14726
14918
  }
14727
- const read = await deps.mintAndReadStamp(opts.repo);
14728
- if (!read.ok) {
14729
- deps.err(`wiki plan failed: could not read the wiki stamp for ${opts.repo}: ${read.error}`);
14730
- return false;
14919
+ if (!isValidIsoDate(opts.today)) {
14920
+ throw new Error(`docs-audit status: today must be a real ISO date (YYYY-MM-DD), got "${opts.today}"`);
14731
14921
  }
14732
- const prevStamp = read.stamp;
14733
- const backfill = Object.keys(prevStamp).length === 0;
14734
- const changed = new Set(selectChangedPages(pages, prevStamp, deps.readSource).map((p) => p.slug));
14735
- const stamp = computePageStamp(pages, deps.readSource);
14736
- emit(deps, {
14737
- repo: opts.repo,
14738
- wikiRepo,
14739
- backfill,
14740
- pages: pages.map((p) => ({ ...p, changed: changed.has(p.slug) })),
14741
- stamp
14742
- });
14743
- return true;
14744
- }
14745
- function emit(deps, result) {
14746
- deps.out(JSON.stringify(result, null, 2));
14922
+ const cadence = opts.cadenceDays ?? 7;
14923
+ const grace = opts.graceDays ?? 3;
14924
+ const age = ageInDays(verdict.date, opts.today);
14925
+ if (age > cadence + grace) {
14926
+ return { ok: false, state: "stale", line: `docs-audit: ${opts.repo} last verdict ${age}d old \u2014 janitor blind or dead` };
14927
+ }
14928
+ if (verdict.outcome === "failed") {
14929
+ return { ok: false, state: "failed", line: `docs-audit: ${opts.repo} last run FAILED (${verdict.date}) \u2014 janitor needs attention` };
14930
+ }
14931
+ return { ok: true, state: "clean", line: `docs-audit: ${opts.repo} ${verdict.outcome} (${verdict.date}, ${verdict.checkerVendor})` };
14747
14932
  }
14748
14933
 
14749
14934
  // src/project-readiness.ts
@@ -15562,7 +15747,6 @@ var SETTABLE_VAR_KEYS = [
15562
15747
  "projectOwner",
15563
15748
  "projectNumber",
15564
15749
  "branch",
15565
- "wikiRepo",
15566
15750
  "vaultPath",
15567
15751
  "repos",
15568
15752
  "oauth",
@@ -15943,9 +16127,9 @@ function writeError(res) {
15943
16127
  }
15944
16128
 
15945
16129
  // src/secrets-commands.ts
15946
- var import_node_fs18 = require("node:fs");
15947
- var import_node_path16 = require("node:path");
15948
- var import_node_os6 = require("node:os");
16130
+ var import_node_fs17 = require("node:fs");
16131
+ var import_node_path15 = require("node:path");
16132
+ var import_node_os5 = require("node:os");
15949
16133
 
15950
16134
  // src/secrets-diff.ts
15951
16135
  var TIMEOUT_MS2 = 8e3;
@@ -16047,18 +16231,18 @@ function collectMap(value, previous = []) {
16047
16231
  return [...previous, value];
16048
16232
  }
16049
16233
  async function decryptRailsCredentials(input) {
16050
- const appDir = (0, import_node_path16.resolve)(input.appDir ?? process.cwd());
16234
+ const appDir = (0, import_node_path15.resolve)(input.appDir ?? process.cwd());
16051
16235
  const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
16052
16236
  const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
16053
- const credentialsPath = (0, import_node_path16.resolve)(appDir, credentialsFile);
16054
- const masterKeyPath = (0, import_node_path16.resolve)(appDir, masterKeyFile);
16237
+ const credentialsPath = (0, import_node_path15.resolve)(appDir, credentialsFile);
16238
+ const masterKeyPath = (0, import_node_path15.resolve)(appDir, masterKeyFile);
16055
16239
  const env = {
16056
16240
  ...process.env,
16057
16241
  MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
16058
16242
  MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
16059
16243
  };
16060
- if ((0, import_node_fs18.existsSync)(masterKeyPath)) {
16061
- env.RAILS_MASTER_KEY = (0, import_node_fs18.readFileSync)(masterKeyPath, "utf8").trim();
16244
+ if ((0, import_node_fs17.existsSync)(masterKeyPath)) {
16245
+ env.RAILS_MASTER_KEY = (0, import_node_fs17.readFileSync)(masterKeyPath, "utf8").trim();
16062
16246
  }
16063
16247
  const script = [
16064
16248
  'require "json"',
@@ -16068,9 +16252,9 @@ async function decryptRailsCredentials(input) {
16068
16252
  'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
16069
16253
  "puts JSON.generate(config.config)"
16070
16254
  ].join("\n");
16071
- const scriptDir = (0, import_node_fs18.mkdtempSync)((0, import_node_path16.join)((0, import_node_os6.tmpdir)(), "mmi-rails-decrypt-"));
16072
- const scriptPath = (0, import_node_path16.join)(scriptDir, "decrypt.rb");
16073
- (0, import_node_fs18.writeFileSync)(scriptPath, script, "utf8");
16255
+ const scriptDir = (0, import_node_fs17.mkdtempSync)((0, import_node_path15.join)((0, import_node_os5.tmpdir)(), "mmi-rails-decrypt-"));
16256
+ const scriptPath = (0, import_node_path15.join)(scriptDir, "decrypt.rb");
16257
+ (0, import_node_fs17.writeFileSync)(scriptPath, script, "utf8");
16074
16258
  try {
16075
16259
  const args = ["exec", "ruby", scriptPath];
16076
16260
  const cmd = process.platform === "win32" ? "cmd.exe" : "bundle";
@@ -16082,7 +16266,7 @@ async function decryptRailsCredentials(input) {
16082
16266
  });
16083
16267
  return JSON.parse(stdout);
16084
16268
  } finally {
16085
- (0, import_node_fs18.rmSync)(scriptDir, { recursive: true, force: true });
16269
+ (0, import_node_fs17.rmSync)(scriptDir, { recursive: true, force: true });
16086
16270
  }
16087
16271
  }
16088
16272
  async function readSecretStdin() {
@@ -16157,7 +16341,7 @@ function registerSecretsCommands(program3) {
16157
16341
  secrets.command("org-catalog").description("MASTER-ONLY: set the _org/<provider>/* catalog declarations from a JSON file (#2244)").requiredOption("--file <path>", 'JSON file: { "entries": { "<provider>/<KEY>": {key,purpose,group,owner,provider,stages[],consumers[]} } }').action((o) => withSecrets(async (d) => {
16158
16342
  let body;
16159
16343
  try {
16160
- body = (0, import_node_fs18.readFileSync)((0, import_node_path16.resolve)(o.file), "utf8");
16344
+ body = (0, import_node_fs17.readFileSync)((0, import_node_path15.resolve)(o.file), "utf8");
16161
16345
  } catch (e) {
16162
16346
  return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
16163
16347
  }
@@ -16261,7 +16445,7 @@ function registerSecretsCommands(program3) {
16261
16445
  {
16262
16446
  ...d,
16263
16447
  decryptRailsCredentials,
16264
- removeFile: (path2) => (0, import_node_fs18.unlinkSync)((0, import_node_path16.resolve)(o.appDir ?? process.cwd(), path2))
16448
+ removeFile: (path2) => (0, import_node_fs17.unlinkSync)((0, import_node_path15.resolve)(o.appDir ?? process.cwd(), path2))
16265
16449
  },
16266
16450
  {
16267
16451
  repo: o.repo,
@@ -16286,7 +16470,7 @@ function registerSecretsCommands(program3) {
16286
16470
  }
16287
16471
 
16288
16472
  // src/app-actor.ts
16289
- var import_node_crypto5 = require("node:crypto");
16473
+ var import_node_crypto4 = require("node:crypto");
16290
16474
  var APP_ACTOR_ENV = "MMI_ACTOR";
16291
16475
  var APP_VAULT_REPO = "mutmutco/MMI-Hub";
16292
16476
  var APP_VAULT_KEYS = ["GITHUB_APP_ID", "GITHUB_APP_INSTALLATION_ID", "GITHUB_APP_PRIVATE_KEY"];
@@ -16330,7 +16514,7 @@ function mintAppJwt(appId, privateKeyPem, nowSec) {
16330
16514
  exp: now + APP_JWT_TTL_S,
16331
16515
  iss: appId
16332
16516
  }));
16333
- const signer = (0, import_node_crypto5.createSign)("RSA-SHA256");
16517
+ const signer = (0, import_node_crypto4.createSign)("RSA-SHA256");
16334
16518
  signer.update(`${header}.${payload}`);
16335
16519
  return `${header}.${payload}.${signer.sign(privateKeyPem, "base64url")}`;
16336
16520
  }
@@ -16636,7 +16820,7 @@ function registerBoxCommands(program3) {
16636
16820
 
16637
16821
  // src/schedules-commands.ts
16638
16822
  var import_promises2 = require("node:fs/promises");
16639
- var import_node_child_process10 = require("node:child_process");
16823
+ var import_node_child_process9 = require("node:child_process");
16640
16824
  var import_node_util7 = require("node:util");
16641
16825
 
16642
16826
  // src/schedules.ts
@@ -16780,9 +16964,82 @@ function spliceDoc(docText, generatedSection) {
16780
16964
  }
16781
16965
  return docText.slice(0, start) + generatedSection + docText.slice(end + DOC_END_MARKER.length);
16782
16966
  }
16967
+ function splitCadence(cadence) {
16968
+ return cadence.split("+").map((c) => c.replace(/\s+/g, " ").trim()).filter(Boolean);
16969
+ }
16970
+ var CRON_FIELD = "[0-9*/,-]+";
16971
+ var CRON_TOKEN_RE = new RegExp(`${CRON_FIELD}(?:\\s+${CRON_FIELD}){4}`, "g");
16972
+ function extractCronTokens(cadence) {
16973
+ const out = /* @__PURE__ */ new Set();
16974
+ for (const m of cadence.match(CRON_TOKEN_RE) ?? []) out.add(m.replace(/\s+/g, " ").trim());
16975
+ return out;
16976
+ }
16977
+ function cadenceStale(registryCadence, liveCadence) {
16978
+ const registered = extractCronTokens(registryCadence);
16979
+ const liveCrons = splitCadence(liveCadence);
16980
+ if (!liveCrons.length) return false;
16981
+ return !liveCrons.every((cron) => registered.has(cron));
16982
+ }
16983
+ function reconcileGithubActions(liveGithub, registry2, readRepos) {
16984
+ const registryGithub = registry2.filter((r) => r.executor === "github-actions");
16985
+ const liveByName = new Map(liveGithub.map((e) => [e.name, e]));
16986
+ const registryById = new Map(registryGithub.map((r) => [r.id, r]));
16987
+ const drifts = [];
16988
+ for (const e of liveGithub) {
16989
+ if (!registryById.has(e.name)) {
16990
+ drifts.push({
16991
+ class: "live-but-unregistered",
16992
+ name: e.name,
16993
+ executor: e.executor,
16994
+ detail: "armed with no SCHEDULE# row",
16995
+ remedy: 'add the eight-field header (docs/schedules.md "The entry template") and re-run the schedules lift so it registers a SCHEDULE# row'
16996
+ });
16997
+ }
16998
+ }
16999
+ for (const r of registryGithub) {
17000
+ const live = liveByName.get(r.id);
17001
+ if (live) {
17002
+ if (cadenceStale(r.cadence, live.cadence)) {
17003
+ drifts.push({
17004
+ class: "file-vs-registry-stale",
17005
+ name: r.id,
17006
+ executor: r.executor || live.executor,
17007
+ detail: `live cadence \`${live.cadence}\` disagrees with registered \`${r.cadence}\``,
17008
+ remedy: "re-run the schedules lift to refresh the SCHEDULE# row from the current header/cron"
17009
+ });
17010
+ }
17011
+ continue;
17012
+ }
17013
+ if (readRepos.has(r.repo)) {
17014
+ drifts.push({
17015
+ class: "registered-but-dead",
17016
+ name: r.id,
17017
+ executor: r.executor || "github-actions",
17018
+ detail: "registered with no armed workflow",
17019
+ remedy: `remove it from ${r.repo} (or re-arm the workflow), then re-run the schedules lift to prune the stale SCHEDULE# row`
17020
+ });
17021
+ }
17022
+ }
17023
+ return drifts.sort((a, b) => a.class.localeCompare(b.class) || a.name.localeCompare(b.name));
17024
+ }
17025
+ function renderDrift(d) {
17026
+ return `${d.class}: ${d.name} (${d.executor}) \u2014 ${d.detail}; remedy: ${d.remedy}`;
17027
+ }
17028
+ function assembleReconciliation(githubEntries2, readRepos, registry2) {
17029
+ if (registry2 === null) {
17030
+ return {
17031
+ reconciliation: [],
17032
+ driftLines: [],
17033
+ incomplete: ["registry: could not read GET /schedules/list \u2014 reconciliation skipped (Hub API unreachable or not authenticated)"]
17034
+ };
17035
+ }
17036
+ const live = githubEntries2.filter((e) => e.executor === "github-actions");
17037
+ const drifts = reconcileGithubActions(live, registry2, readRepos);
17038
+ return { reconciliation: drifts, driftLines: drifts.map(renderDrift), incomplete: [] };
17039
+ }
16783
17040
 
16784
17041
  // src/schedules-commands.ts
16785
- var execFileP5 = (0, import_node_util7.promisify)(import_node_child_process10.execFile);
17042
+ var execFileP5 = (0, import_node_util7.promisify)(import_node_child_process9.execFile);
16786
17043
  var AWS_REGION = "eu-central-1";
16787
17044
  var AWS_TIMEOUT_MS = 3e4;
16788
17045
  var AWS_RETRY_DELAY_MS = 1500;
@@ -16829,12 +17086,13 @@ async function githubEntries(client) {
16829
17086
  const entries = [];
16830
17087
  const incomplete = [];
16831
17088
  const drift = [];
17089
+ const readRepos = [];
16832
17090
  let repos;
16833
17091
  try {
16834
17092
  const listing = await client.restPaginate(`/orgs/${ORG}/repos?per_page=100`);
16835
17093
  repos = listing.filter((r) => typeof r?.name === "string" && r.archived !== true).map((r) => r.name).sort();
16836
17094
  } catch (e) {
16837
- return { entries, incomplete: [`github: could not list ${ORG} repos \u2014 ${e.message}`], drift };
17095
+ return { entries, incomplete: [`github: could not list ${ORG} repos \u2014 ${e.message}`], drift, readRepos };
16838
17096
  }
16839
17097
  const results = await Promise.all(
16840
17098
  repos.map(async (repo) => {
@@ -16848,13 +17106,14 @@ async function githubEntries(client) {
16848
17106
  for (const r of results) {
16849
17107
  if ("error" in r) incomplete.push(`github: ${r.repo}: ${r.error}`);
16850
17108
  else {
17109
+ readRepos.push(r.repo);
16851
17110
  entries.push(...r.entries);
16852
17111
  if (r.failures.length) {
16853
17112
  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(", ")}`);
16854
17113
  }
16855
17114
  }
16856
17115
  }
16857
- return { entries, incomplete, drift };
17116
+ return { entries, incomplete, drift, readRepos };
16858
17117
  }
16859
17118
  async function awsJson(args) {
16860
17119
  const run = async () => {
@@ -16891,12 +17150,21 @@ async function awsEntries() {
16891
17150
  }
16892
17151
  return { entries, incomplete, drift: [] };
16893
17152
  }
16894
- async function fetchNotebook(client = defaultGitHubClient()) {
16895
- const [gh, aws] = await Promise.all([githubEntries(client), awsEntries()]);
17153
+ async function defaultRegistryRead() {
17154
+ return fetchSchedulesList(registryClientDeps(await loadConfig()));
17155
+ }
17156
+ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defaultRegistryRead) {
17157
+ const [gh, aws, registry2] = await Promise.all([
17158
+ githubEntries(client),
17159
+ awsEntries(),
17160
+ readRegistry().catch(() => null)
17161
+ ]);
17162
+ const recon = assembleReconciliation(gh.entries, new Set(gh.readRepos), registry2);
16896
17163
  return {
16897
17164
  entries: sortEntries([...gh.entries, ...aws.entries, ...DECLARED_ENTRIES]),
16898
- incomplete: [...gh.incomplete, ...aws.incomplete],
16899
- drift: [...gh.drift, ...aws.drift]
17165
+ incomplete: [...gh.incomplete, ...aws.incomplete, ...recon.incomplete],
17166
+ drift: [...gh.drift, ...aws.drift, ...recon.driftLines],
17167
+ reconciliation: recon.reconciliation
16900
17168
  };
16901
17169
  }
16902
17170
  function warnIncomplete2(incomplete) {
@@ -16909,7 +17177,7 @@ function reportDrift(drift) {
16909
17177
  function registerSchedulesCommands(program3) {
16910
17178
  program3.command("schedules").description("the org schedules notebook \u2014 every armed cron, timer, and scheduled LLM lane, resolved live (jerv side lives in jerv-cli schedules)").option("--json", "machine-readable output (consumed by jerv-cli schedules --all)").option("--doc <path>", "splice the generated inventory into the given doc between the schedules:inventory markers (docs/schedules.md)").action(async (o) => {
16911
17179
  try {
16912
- const { entries, incomplete, drift } = await fetchNotebook();
17180
+ const { entries, incomplete, drift, reconciliation } = await fetchNotebook();
16913
17181
  if (o.doc) {
16914
17182
  if (incomplete.length) {
16915
17183
  warnIncomplete2(incomplete);
@@ -16924,7 +17192,7 @@ function registerSchedulesCommands(program3) {
16924
17192
  return;
16925
17193
  }
16926
17194
  if (o.json) {
16927
- console.log(JSON.stringify({ org: entries, incomplete, drift }, null, 2));
17195
+ console.log(JSON.stringify({ org: entries, incomplete, drift, reconciliation }, null, 2));
16928
17196
  } else {
16929
17197
  console.log(formatSchedulesTable(entries));
16930
17198
  reportDrift(drift);
@@ -17396,11 +17664,11 @@ function registerQueryCommands(program3) {
17396
17664
  }
17397
17665
 
17398
17666
  // src/bootstrap-commands.ts
17399
- var import_node_fs19 = require("node:fs");
17667
+ var import_node_fs18 = require("node:fs");
17400
17668
 
17401
17669
  // src/bootstrap-verify.ts
17402
17670
  var TRAIN_BRANCHES2 = ["development", "rc", "main"];
17403
- var requiredDocs = ["README.md", "architecture.md"];
17671
+ var requiredDocs = ["README.md", "architecture.md", "docs/decisions/README.md"];
17404
17672
  var requiredIssueTemplates = [
17405
17673
  ".github/ISSUE_TEMPLATE/bug.yml",
17406
17674
  ".github/ISSUE_TEMPLATE/feature.yml",
@@ -17561,7 +17829,6 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
17561
17829
  const repoInfo = await restJson3(deps, `repos/${repo}`, {});
17562
17830
  checks.push({ ok: Boolean(repoInfo.default_branch), label: "repo exists" });
17563
17831
  checks.push({ ok: repoInfo.default_branch === baseBranch, label: `default branch is ${baseBranch}`, detail: repoInfo.default_branch || "missing" });
17564
- checks.push({ ok: repoInfo.has_wiki === true, label: "wiki enabled", detail: repoInfo.has_wiki === true ? void 0 : "has_wiki is false or unavailable" });
17565
17832
  const branchList = await restPagedJson2(deps, `repos/${repo}/branches`, []);
17566
17833
  const branchNames = new Set(branchList.map((b) => b.name));
17567
17834
  for (const branch of branchesWanted) {
@@ -17763,8 +18030,6 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
17763
18030
  });
17764
18031
  }
17765
18032
  }
17766
- const fanout = repo === "mutmutco/MMI-Hub" ? true : localRegistryCheck(deps, ".github/fanout-targets.json", (json) => Array.isArray(json?.repos) && json.repos.some((r) => repoRefsMatch(r.repo ?? "", repo.split("/")[1]) && r.branch === baseBranch));
17767
- if (fanout != null) checks.push({ ok: fanout, label: `fanout target registered on ${baseBranch}` });
17768
18033
  const projectRegistry = localRegistryCheck(deps, "projects.json", (json) => Array.isArray(json?.projects) && projectRegistryIncludesRepo(json.projects, repo));
17769
18034
  if (projectRegistry != null) checks.push({ ok: projectRegistry, label: "project registry includes repo" });
17770
18035
  const rulesetList = await restJson3(deps, `repos/${repo}/rulesets?includes_parents=true`, []);
@@ -17944,7 +18209,7 @@ function registerBootstrapCommands(program3) {
17944
18209
  client: defaultGitHubClient(),
17945
18210
  projectMeta: meta,
17946
18211
  deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
17947
- readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs19.existsSync)(path2) ? (0, import_node_fs19.readFileSync)(path2, "utf8") : null,
18212
+ readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs18.existsSync)(path2) ? (0, import_node_fs18.readFileSync)(path2, "utf8") : null,
17948
18213
  // requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
17949
18214
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
17950
18215
  requiredGcpApis: (() => {
@@ -17995,12 +18260,12 @@ function registerBootstrapCommands(program3) {
17995
18260
  return fail(`bootstrap apply: ${e.message}`);
17996
18261
  }
17997
18262
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
17998
- if (!(0, import_node_fs19.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`);
17999
- const manifest = loadBootstrapSeeds((0, import_node_fs19.readFileSync)(manifestPath, "utf8"));
18263
+ if (!(0, import_node_fs18.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`);
18264
+ const manifest = loadBootstrapSeeds((0, import_node_fs18.readFileSync)(manifestPath, "utf8"));
18000
18265
  const baseBranch = o.class === "content" ? "main" : "development";
18001
18266
  const slug = parsedRepo.slug;
18002
18267
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
18003
- const readFile5 = (p) => (0, import_node_fs19.existsSync)(p) ? (0, import_node_fs19.readFileSync)(p, "utf8") : null;
18268
+ const readFile5 = (p) => (0, import_node_fs18.existsSync)(p) ? (0, import_node_fs18.readFileSync)(p, "utf8") : null;
18004
18269
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
18005
18270
  const rawVars = {};
18006
18271
  for (const value of cmdOpts.var ?? []) {
@@ -18062,21 +18327,19 @@ function registerBootstrapCommands(program3) {
18062
18327
  let exists = false;
18063
18328
  let sha;
18064
18329
  let remoteContent = null;
18065
- if (resolved.source !== "fanout") {
18330
+ try {
18331
+ const r = await gh(["api", `repos/${repo}/contents/${enc(resolved.target)}?ref=${seedPlan.ref}`]);
18332
+ exists = true;
18066
18333
  try {
18067
- const r = await gh(["api", `repos/${repo}/contents/${enc(resolved.target)}?ref=${seedPlan.ref}`]);
18068
- exists = true;
18069
- try {
18070
- const parsed = JSON.parse(r.stdout);
18071
- sha = parsed.sha;
18072
- if (parsed.encoding === "base64" && typeof parsed.content === "string") {
18073
- remoteContent = Buffer.from(parsed.content, "base64").toString("utf8");
18074
- }
18075
- } catch {
18334
+ const parsed = JSON.parse(r.stdout);
18335
+ sha = parsed.sha;
18336
+ if (parsed.encoding === "base64" && typeof parsed.content === "string") {
18337
+ remoteContent = Buffer.from(parsed.content, "base64").toString("utf8");
18076
18338
  }
18077
18339
  } catch {
18078
- exists = false;
18079
18340
  }
18341
+ } catch {
18342
+ exists = false;
18080
18343
  }
18081
18344
  const planned = planSeedAction(resolved, exists);
18082
18345
  const isBlock = resolved.source === "managed-block";
@@ -18094,7 +18357,7 @@ function registerBootstrapCommands(program3) {
18094
18357
  await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-f", "delete_branch_on_merge=true"]).catch(() => {
18095
18358
  });
18096
18359
  const openPrs = await gh(["pr", "list", "--repo", repo, "--head", seedPlan.branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
18097
- const prDecision = decideFanoutPrAction(JSON.parse(openPrs.stdout || "[]"));
18360
+ const prDecision = decideSeedPrAction(JSON.parse(openPrs.stdout || "[]"));
18098
18361
  if (prDecision.action === "reuse") {
18099
18362
  seedPrUrl = prDecision.url;
18100
18363
  } else {
@@ -18187,83 +18450,7 @@ function registerBootstrapCommands(program3) {
18187
18450
  applied.push(`ddb register ${registerPayload.slug} (failed: ${why})`);
18188
18451
  }
18189
18452
  }
18190
- let fanoutPrUrl;
18191
- if (o.execute) {
18192
- const fanoutEntry = {
18193
- repo: parsedRepo.name,
18194
- slug,
18195
- projectId: vars.PROJECT_ID || void 0,
18196
- wikiRepo: vars.WIKI_REPO || parsedRepo.fullName,
18197
- branch: baseBranch,
18198
- cls: o.class,
18199
- name: vars.NAME || parsedRepo.name
18200
- };
18201
- const readHubFile = async (path2) => {
18202
- const r = await gh(["api", `repos/${HUB_REPO}/contents/${enc(path2)}?ref=development`]);
18203
- const parsed = JSON.parse(r.stdout);
18204
- if (parsed.encoding !== "base64" || typeof parsed.content !== "string" || !parsed.sha) {
18205
- throw new Error(`could not read ${HUB_REPO}/${path2}`);
18206
- }
18207
- return { content: Buffer.from(parsed.content, "base64").toString("utf8"), sha: parsed.sha };
18208
- };
18209
- try {
18210
- const fanoutFile = await readHubFile(".github/fanout-targets.json");
18211
- const projectsFile = await readHubFile("projects.json");
18212
- const plan = planFanoutRegistration(fanoutFile.content, projectsFile.content, fanoutEntry);
18213
- if (!plan.changed) {
18214
- applied.push(`fanout: already registered (${parsedRepo.name})`);
18215
- } else {
18216
- const branchName = `bootstrap-register-fanout-${slug}`;
18217
- const headSha = (await gh(["api", `repos/${HUB_REPO}/git/ref/heads/development`, "--jq", ".object.sha"])).stdout.trim();
18218
- try {
18219
- await gh(["api", "-X", "POST", `repos/${HUB_REPO}/git/refs`, "-f", `ref=refs/heads/${branchName}`, "-f", `sha=${headSha}`]);
18220
- } catch (e) {
18221
- if (!/Reference already exists|already exists/i.test(String(e.message ?? ""))) throw e;
18222
- }
18223
- const branchFileSha = async (path2) => {
18224
- try {
18225
- const r = await gh(["api", `repos/${HUB_REPO}/contents/${enc(path2)}?ref=${branchName}`, "--jq", ".sha"]);
18226
- return r.stdout.trim() || void 0;
18227
- } catch (e) {
18228
- if (/404|Not Found/i.test(String(e.message ?? ""))) return void 0;
18229
- throw e;
18230
- }
18231
- };
18232
- const fanoutBranchSha = await branchFileSha(".github/fanout-targets.json");
18233
- const projectsBranchSha = await branchFileSha("projects.json");
18234
- await gh(contentPutArgs(HUB_REPO, ".github/fanout-targets.json", plan.fanoutTargets, branchName, fanoutBranchSha));
18235
- await gh(contentPutArgs(HUB_REPO, "projects.json", plan.projects, branchName, projectsBranchSha));
18236
- const openPrs = await gh(["pr", "list", "--repo", HUB_REPO, "--head", branchName, "--base", "development", "--state", "open", "--json", "number,url"]);
18237
- const prDecision = decideFanoutPrAction(JSON.parse(openPrs.stdout || "[]"));
18238
- if (prDecision.action === "reuse") {
18239
- fanoutPrUrl = prDecision.url;
18240
- } else {
18241
- const created = await ghCreate([
18242
- "pr",
18243
- "create",
18244
- "--repo",
18245
- HUB_REPO,
18246
- "--base",
18247
- "development",
18248
- "--head",
18249
- branchName,
18250
- "--title",
18251
- `bootstrap: register ${parsedRepo.name} for the org-managed .gitignore fanout`,
18252
- "--body",
18253
- `Auto-opened by \`mmi-cli bootstrap apply --execute ${repo}\` (#933): adds ${parsedRepo.name} to projects.json + .github/fanout-targets.json so the org-managed .gitignore block reaches it (hub-v3 retired the whole-spine fanout).`
18254
- ]);
18255
- fanoutPrUrl = created.url;
18256
- }
18257
- await gh(["pr", "merge", fanoutPrUrl, "--repo", HUB_REPO, "--auto", "--squash"]).catch((e) => {
18258
- if (!/already/i.test(String(e.message ?? ""))) throw e;
18259
- });
18260
- applied.push(`fanout: PR ${fanoutPrUrl} (auto-merge enabled)`);
18261
- }
18262
- } catch (e) {
18263
- return failGraceful(`bootstrap apply: fanout registration failed: ${e.message}`);
18264
- }
18265
- }
18266
- if (o.json) console.log(JSON.stringify({ repo, class: o.class, execute: o.execute, seedDelivery: seedPlan.mode, seedPrUrl, actions, applied, ddbWrites, fanoutPrUrl }, null, 2));
18453
+ if (o.json) console.log(JSON.stringify({ repo, class: o.class, execute: o.execute, seedDelivery: seedPlan.mode, seedPrUrl, actions, applied, ddbWrites }, null, 2));
18267
18454
  else {
18268
18455
  console.log(renderSeedPlan(actions));
18269
18456
  if (o.execute) console.log(`
@@ -18274,13 +18461,13 @@ LIVE apply to ${repo}:
18274
18461
  }
18275
18462
 
18276
18463
  // src/stage-commands.ts
18277
- var import_node_fs21 = require("node:fs");
18278
- var import_node_path18 = require("node:path");
18279
-
18280
- // src/port-registry.ts
18281
18464
  var import_node_fs20 = require("node:fs");
18282
18465
  var import_node_path17 = require("node:path");
18283
18466
 
18467
+ // src/port-registry.ts
18468
+ var import_node_fs19 = require("node:fs");
18469
+ var import_node_path16 = require("node:path");
18470
+
18284
18471
  // ../infra/port-geometry.mjs
18285
18472
  var PORT_BLOCK = 100;
18286
18473
  var PORT_SPAN = 10;
@@ -18293,8 +18480,8 @@ function nextPortBlock(registry2) {
18293
18480
  return [base, base + PORT_SPAN];
18294
18481
  }
18295
18482
  function loadPortRegistry(path2) {
18296
- if (!(0, import_node_fs20.existsSync)(path2)) return {};
18297
- const raw = JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
18483
+ if (!(0, import_node_fs19.existsSync)(path2)) return {};
18484
+ const raw = JSON.parse((0, import_node_fs19.readFileSync)(path2, "utf8"));
18298
18485
  const out = {};
18299
18486
  for (const [key, value] of Object.entries(raw)) {
18300
18487
  if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
@@ -18308,9 +18495,9 @@ function ensurePortRange(repo, path2) {
18308
18495
  const existing = registry2[repo];
18309
18496
  if (existing) return existing;
18310
18497
  const range = nextPortBlock(registry2);
18311
- const raw = (0, import_node_fs20.existsSync)(path2) ? JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8")) : {};
18498
+ const raw = (0, import_node_fs19.existsSync)(path2) ? JSON.parse((0, import_node_fs19.readFileSync)(path2, "utf8")) : {};
18312
18499
  raw[repo] = range;
18313
- (0, import_node_fs20.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
18500
+ (0, import_node_fs19.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
18314
18501
  return range;
18315
18502
  }
18316
18503
  function portCursorSeed(registry2) {
@@ -18332,22 +18519,22 @@ function existingPortRange(repo, registry2) {
18332
18519
  return registry2[repo] ?? null;
18333
18520
  }
18334
18521
  function portRangeInfraAt(root, source) {
18335
- const registryPath = (0, import_node_path17.join)(root, "infra", "port-ranges.json");
18336
- const ddbScriptPath = (0, import_node_path17.join)(root, "infra", "port-ddb.mjs");
18337
- if (!(0, import_node_fs20.existsSync)(registryPath) || !(0, import_node_fs20.existsSync)(ddbScriptPath)) return null;
18522
+ const registryPath = (0, import_node_path16.join)(root, "infra", "port-ranges.json");
18523
+ const ddbScriptPath = (0, import_node_path16.join)(root, "infra", "port-ddb.mjs");
18524
+ if (!(0, import_node_fs19.existsSync)(registryPath) || !(0, import_node_fs19.existsSync)(ddbScriptPath)) return null;
18338
18525
  return { root, source, registryPath, ddbScriptPath };
18339
18526
  }
18340
18527
  function resolvePortRangeInfra(cwd, packageDir) {
18341
18528
  const direct = portRangeInfraAt(cwd, "cwd");
18342
18529
  if (direct) return direct;
18343
- for (let dir = cwd; ; dir = (0, import_node_path17.dirname)(dir)) {
18344
- const sibling = portRangeInfraAt((0, import_node_path17.join)(dir, "MMI-Hub"), "sibling-hub");
18530
+ for (let dir = cwd; ; dir = (0, import_node_path16.dirname)(dir)) {
18531
+ const sibling = portRangeInfraAt((0, import_node_path16.join)(dir, "MMI-Hub"), "sibling-hub");
18345
18532
  if (sibling) return sibling;
18346
- const parent = (0, import_node_path17.dirname)(dir);
18533
+ const parent = (0, import_node_path16.dirname)(dir);
18347
18534
  if (parent === dir) break;
18348
18535
  }
18349
18536
  if (packageDir) {
18350
- const pkgRoot = (0, import_node_path17.join)(packageDir, "..", "..");
18537
+ const pkgRoot = (0, import_node_path16.join)(packageDir, "..", "..");
18351
18538
  const pkgFrom = portRangeInfraAt(pkgRoot, "pkg-root");
18352
18539
  if (pkgFrom) return pkgFrom;
18353
18540
  }
@@ -18523,8 +18710,8 @@ function registerStageCommands(program3) {
18523
18710
  const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
18524
18711
  return decideStage({
18525
18712
  registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
18526
- hasCompose: (0, import_node_fs21.existsSync)((0, import_node_path18.join)(process.cwd(), "docker-compose.yml")),
18527
- hasEnvExample: (0, import_node_fs21.existsSync)((0, import_node_path18.join)(process.cwd(), ".env.example"))
18713
+ hasCompose: (0, import_node_fs20.existsSync)((0, import_node_path17.join)(process.cwd(), "docker-compose.yml")),
18714
+ hasEnvExample: (0, import_node_fs20.existsSync)((0, import_node_path17.join)(process.cwd(), ".env.example"))
18528
18715
  });
18529
18716
  }
18530
18717
  async function fetchStageVaultEnvMerge() {
@@ -18848,6 +19035,27 @@ function registerBoardCommands(program3) {
18848
19035
  expected: [...BOARD_STATUSES]
18849
19036
  });
18850
19037
  }
19038
+ async function runBulkMove(issueRefs, o, status, failLabel) {
19039
+ try {
19040
+ const bulk = await moveBoardIssues({
19041
+ config: await loadConfigForBoardSelector2(issueRefs[0], o.repo),
19042
+ selectors: issueRefs,
19043
+ status,
19044
+ repo: o.repo,
19045
+ allowPartial: o.allowPartial
19046
+ });
19047
+ if (o.json) {
19048
+ console.log(JSON.stringify(bulk.results));
19049
+ } else {
19050
+ for (const result of bulk.results) {
19051
+ console.log(result.moved ? result.partial ? `Partially moved ${result.ref}: ${result.warning}` : `Moved ${result.ref} -> ${result.status}` : `Skipped ${result.ref}: ${result.reason}`);
19052
+ }
19053
+ }
19054
+ if (bulk.failed > 0) process.exitCode = 1;
19055
+ } catch (e) {
19056
+ return failGraceful(`${failLabel} failed: ${e.message}`);
19057
+ }
19058
+ }
18851
19059
  withExamples(mutating(
18852
19060
  board.command("move <status> <issues...>").description(`move one or more board items to a Status: ${BOARD_STATUSES.join(", ")} (quote multi-word statuses)`).option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--allow-partial", "return success JSON if the item resolves but the status move fails"),
18853
19061
  (_opts, args) => {
@@ -18867,25 +19075,7 @@ function registerBoardCommands(program3) {
18867
19075
  }
18868
19076
  return;
18869
19077
  }
18870
- try {
18871
- const bulk = await moveBoardIssues({
18872
- config: await loadConfigForBoardSelector2(issueRefs[0], o.repo),
18873
- selectors: issueRefs,
18874
- status: canonicalStatus,
18875
- repo: o.repo,
18876
- allowPartial: o.allowPartial
18877
- });
18878
- if (o.json) {
18879
- console.log(JSON.stringify(bulk.results));
18880
- } else {
18881
- for (const result of bulk.results) {
18882
- console.log(result.moved ? result.partial ? `Partially moved ${result.ref}: ${result.warning}` : `Moved ${result.ref} -> ${result.status}` : `Skipped ${result.ref}: ${result.reason}`);
18883
- }
18884
- }
18885
- if (bulk.failed > 0) process.exitCode = 1;
18886
- } catch (e) {
18887
- return failGraceful(`board move failed: ${e.message}`);
18888
- }
19078
+ await runBulkMove(issueRefs, o, canonicalStatus, "board move");
18889
19079
  }), [
18890
19080
  'mmi-cli board move "In Progress" 2680',
18891
19081
  "mmi-cli board move Done 2680 2681 2682"
@@ -18929,25 +19119,7 @@ function registerBoardCommands(program3) {
18929
19119
  }
18930
19120
  return;
18931
19121
  }
18932
- try {
18933
- const bulk = await moveBoardIssues({
18934
- config: await loadConfigForBoardSelector2(issueRefs[0], o.repo),
18935
- selectors: issueRefs,
18936
- status: "Done",
18937
- repo: o.repo,
18938
- allowPartial: o.allowPartial
18939
- });
18940
- if (o.json) {
18941
- console.log(JSON.stringify(bulk.results));
18942
- } else {
18943
- for (const result of bulk.results) {
18944
- console.log(result.moved ? result.partial ? `Partially moved ${result.ref}: ${result.warning}` : `Moved ${result.ref} -> ${result.status}` : `Skipped ${result.ref}: ${result.reason}`);
18945
- }
18946
- }
18947
- if (bulk.failed > 0) process.exitCode = 1;
18948
- } catch (e) {
18949
- return failGraceful(`board done failed: ${e.message}`);
18950
- }
19122
+ await runBulkMove(issueRefs, o, "Done", "board done");
18951
19123
  });
18952
19124
  board.command("unclaim <issue>").description("clear the assignee and reset the board Status (defaults to Todo) \u2014 the inverse of claim").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--to-status <status>", `set Status after unclaim (default Todo; one of ${BOARD_STATUSES.join(", ")})`).option("--allow-partial", "return success JSON if the assignee clear or status move fails").action(async (issueRef, o) => {
18953
19125
  const toStatus = o.toStatus ? resolveBoardStatus(o.toStatus) : void 0;
@@ -18990,9 +19162,9 @@ function registerBoardCommands(program3) {
18990
19162
  }
18991
19163
 
18992
19164
  // src/merge-cleanup.ts
18993
- var import_node_fs22 = require("node:fs");
19165
+ var import_node_fs21 = require("node:fs");
18994
19166
  var import_promises4 = require("node:fs/promises");
18995
- var import_node_child_process11 = require("node:child_process");
19167
+ var import_node_child_process10 = require("node:child_process");
18996
19168
 
18997
19169
  // src/board-advance.ts
18998
19170
  function repoOf2(ref) {
@@ -19078,7 +19250,7 @@ function boardAdvanceFailureMessage(result) {
19078
19250
 
19079
19251
  // src/deferred-registry-store.ts
19080
19252
  var import_promises3 = require("node:fs/promises");
19081
- var import_node_path19 = require("node:path");
19253
+ var import_node_path18 = require("node:path");
19082
19254
  var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
19083
19255
  async function atomicWrite(target, contents) {
19084
19256
  const tmp = `${target}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
@@ -19182,12 +19354,12 @@ function makeDeferredWorktreeStore(registryPath, lockOpts = {}) {
19182
19354
  },
19183
19355
  // Standalone atomic write — THROWS on failure (no best-effort swallow, #2846).
19184
19356
  write: async (entries) => {
19185
- await (0, import_promises3.mkdir)((0, import_node_path19.dirname)(registryPath), { recursive: true });
19357
+ await (0, import_promises3.mkdir)((0, import_node_path18.dirname)(registryPath), { recursive: true });
19186
19358
  await atomicWrite(registryPath, serializeDeferredWorktrees(entries));
19187
19359
  },
19188
19360
  // Serialized read-modify-write under the repo-wide lock (#2846).
19189
19361
  update: async (mutate) => {
19190
- await (0, import_promises3.mkdir)((0, import_node_path19.dirname)(registryPath), { recursive: true });
19362
+ await (0, import_promises3.mkdir)((0, import_node_path18.dirname)(registryPath), { recursive: true });
19191
19363
  const deadline = Date.now() + opts.maxWaitMs;
19192
19364
  for (; ; ) {
19193
19365
  const guard = await acquireLock(lockPath, opts, deadline);
@@ -19281,12 +19453,24 @@ async function deleteReviewedRemoteBranch(remote, branch, expectedHeadOid) {
19281
19453
  }
19282
19454
  return "deleted";
19283
19455
  }
19284
- function assertPrMergeHousekeepingClean(startingPath, context) {
19285
- const housekeeping = buildPrMergeScratchHousekeeping(startingPath || process.cwd());
19286
- const scratchStatus = housekeeping.scratch?.status;
19287
- if (scratchStatus === "warnings" || scratchStatus === "failed") {
19288
- throw new Error(`${context} housekeeping blocked merge: scratch ${scratchStatus}; run mmi-cli gc --scratch --dry-run`);
19289
- }
19456
+ function evaluatePrMergeHousekeeping(report, context, force = false) {
19457
+ const scratch = report.scratch;
19458
+ const failed = scratch?.status === "failed";
19459
+ const prunable = scratch?.safeAuto ?? 0;
19460
+ const unprunable = scratch?.skipped ?? 0;
19461
+ if (!(failed || prunable > 0 || unprunable > 0)) return { blocked: false };
19462
+ if (force) return { blocked: false };
19463
+ const detail = failed ? `scratch housekeeping failed${scratch?.error ? `: ${scratch.error}` : ""}` : `${prunable} prunable + ${unprunable} un-prunable scratch item(s) remain after cleanup`;
19464
+ return {
19465
+ blocked: true,
19466
+ reason: `${context} housekeeping blocked merge: ${detail}. Run \`mmi-cli gc --scratch --apply\` to clear it, then retry \u2014 or re-run \`mmi-cli ${context} --force\` to acknowledge and land anyway. (Kept advisory plans/ scratch never blocks: it is gitignored and can never reach origin.)`
19467
+ };
19468
+ }
19469
+ function assertPrMergeHousekeepingClean(startingPath, context, options = {}) {
19470
+ const build = options.buildHousekeeping ?? ((repoRoot2) => buildPrMergeScratchHousekeeping(repoRoot2));
19471
+ const housekeeping = build(startingPath || process.cwd());
19472
+ const verdict = evaluatePrMergeHousekeeping(housekeeping, context, options.force);
19473
+ if (verdict.blocked) throw new Error(verdict.reason);
19290
19474
  return housekeeping;
19291
19475
  }
19292
19476
  async function applyGcPlan(plan, remote) {
@@ -19297,16 +19481,21 @@ async function applyGcPlan(plan, remote) {
19297
19481
  const deferredStore = await createDeferredWorktreeStore();
19298
19482
  const branchTracking = await applyBranchAndTrackingCleanup(plan, {
19299
19483
  localBranchHeads,
19300
- cleanupBranch: (branch, expectedHeadOid) => cleanupPrMergeLocalBranch(branch.branch, {
19301
- beforeWorktrees,
19302
- startingPath: branch.worktreePath,
19303
- pathExists: (p) => (0, import_node_fs22.existsSync)(p),
19304
- execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
19305
- teardownWorktreeStage,
19306
- deferredStore,
19307
- expectedHeadOid,
19308
- removeWorktreeDir: worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout).removeWorktreeDir
19309
- }),
19484
+ cleanupBranch: (branch, expectedHeadOid) => {
19485
+ const wtDeps = worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout);
19486
+ return cleanupPrMergeLocalBranch(branch.branch, {
19487
+ beforeWorktrees,
19488
+ startingPath: branch.worktreePath,
19489
+ pathExists: (p) => (0, import_node_fs21.existsSync)(p),
19490
+ execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
19491
+ teardownWorktreeStage,
19492
+ deferredStore,
19493
+ expectedHeadOid,
19494
+ detachReparsePoints: wtDeps.detachReparsePoints,
19495
+ // #3064: junction-safe teardown
19496
+ removeWorktreeDir: wtDeps.removeWorktreeDir
19497
+ });
19498
+ },
19310
19499
  cleanupRemoteBranch: (branch, expectedHeadOid) => deleteReviewedRemoteBranch(remote, branch.branch, expectedHeadOid),
19311
19500
  cleanupTrackingRefs: (trackingRefs, unsafeBranches) => applyTrackingRefCleanup(trackingRefs, remote, unsafeBranches, {
19312
19501
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout
@@ -19323,7 +19512,7 @@ async function applyGcPlan(plan, remote) {
19323
19512
  for (const wt of plan.worktreeDirs) {
19324
19513
  try {
19325
19514
  const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
19326
- realpath: (path2) => (0, import_node_fs22.realpathSync)(path2)
19515
+ realpath: (path2) => (0, import_node_fs21.realpathSync)(path2)
19327
19516
  });
19328
19517
  if (!cleanupTarget.ok) {
19329
19518
  result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
@@ -19436,7 +19625,7 @@ async function remoteBranchExists2(branch, options = {}) {
19436
19625
  }
19437
19626
  var COMPOSE_TIMEOUT_MS = 12e4;
19438
19627
  function spawnDeferredGcSweep() {
19439
- spawnDetachedSelf(["gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process11.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
19628
+ spawnDetachedSelf(["gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process10.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
19440
19629
  }
19441
19630
  async function createDeferredWorktreeStore() {
19442
19631
  try {
@@ -19450,13 +19639,13 @@ var realWorktreeDirRemover = {
19450
19639
  probe: (p) => {
19451
19640
  let st;
19452
19641
  try {
19453
- st = (0, import_node_fs22.lstatSync)(p);
19642
+ st = (0, import_node_fs21.lstatSync)(p);
19454
19643
  } catch {
19455
19644
  return null;
19456
19645
  }
19457
19646
  if (st.isSymbolicLink()) return "link";
19458
19647
  try {
19459
- (0, import_node_fs22.readlinkSync)(p);
19648
+ (0, import_node_fs21.readlinkSync)(p);
19460
19649
  return "link";
19461
19650
  } catch {
19462
19651
  }
@@ -19464,7 +19653,7 @@ var realWorktreeDirRemover = {
19464
19653
  },
19465
19654
  readdir: (p) => {
19466
19655
  try {
19467
- return (0, import_node_fs22.readdirSync)(p);
19656
+ return (0, import_node_fs21.readdirSync)(p);
19468
19657
  } catch {
19469
19658
  return [];
19470
19659
  }
@@ -19473,9 +19662,9 @@ var realWorktreeDirRemover = {
19473
19662
  // leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
19474
19663
  detachLink: (p) => {
19475
19664
  try {
19476
- (0, import_node_fs22.rmdirSync)(p);
19665
+ (0, import_node_fs21.rmdirSync)(p);
19477
19666
  } catch {
19478
- (0, import_node_fs22.unlinkSync)(p);
19667
+ (0, import_node_fs21.unlinkSync)(p);
19479
19668
  }
19480
19669
  },
19481
19670
  removeTree: (p) => (0, import_promises4.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
@@ -19491,6 +19680,9 @@ function worktreeRemoveDeps(execGit) {
19491
19680
  return {
19492
19681
  git: execGit,
19493
19682
  sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
19683
+ // #3064: unlink any reparse point (esp. a `node_modules` junction to base) before the git-native
19684
+ // `worktree remove --force`, which would otherwise recurse through it and empty the base checkout.
19685
+ detachReparsePoints: (worktreePath) => detachReparsePoints(worktreePath, realWorktreeDirRemover),
19494
19686
  removeWorktreeDir: async (worktreePath) => removeWorktreeTree(worktreePath, await resolvePrimaryCheckout(execGit), realWorktreeDirRemover)
19495
19687
  };
19496
19688
  }
@@ -19505,9 +19697,9 @@ async function worktreeHasStageState(worktreePath) {
19505
19697
  }
19506
19698
  }
19507
19699
  function stageStateFileBelongsToWorktree(statePath, worktreePath) {
19508
- if (!(0, import_node_fs22.existsSync)(statePath)) return false;
19700
+ if (!(0, import_node_fs21.existsSync)(statePath)) return false;
19509
19701
  try {
19510
- const state = JSON.parse((0, import_node_fs22.readFileSync)(statePath, "utf8"));
19702
+ const state = JSON.parse((0, import_node_fs21.readFileSync)(statePath, "utf8"));
19511
19703
  const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
19512
19704
  return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
19513
19705
  } catch {
@@ -19635,9 +19827,8 @@ async function fetchRestCorePool(gh = defaultGhApi) {
19635
19827
  }
19636
19828
 
19637
19829
  // src/worktree-lifecycle-commands.ts
19638
- var import_node_fs23 = require("node:fs");
19639
- var import_promises5 = require("node:fs/promises");
19640
- var import_node_path20 = require("node:path");
19830
+ var import_node_fs22 = require("node:fs");
19831
+ var import_node_path19 = require("node:path");
19641
19832
  var GH_TIMEOUT_MS = 2e4;
19642
19833
  var DEFAULT_BASE = "origin/development";
19643
19834
  var DEFAULT_REMOTE = "origin";
@@ -19775,13 +19966,13 @@ function registerWorktreeCommands(program3) {
19775
19966
  if (PROTECTED_BRANCHES2.has(branch)) {
19776
19967
  return fail(`worktree land: refusing to land protected branch '${branch}'`, { code: ERROR_CODES.ERR_BAD_ENUM });
19777
19968
  }
19778
- const gitFile = (0, import_node_path20.join)(wtPath, ".git");
19779
- const isLinked = (0, import_node_fs23.existsSync)(gitFile) && (0, import_node_fs23.statSync)(gitFile).isFile();
19969
+ const gitFile = (0, import_node_path19.join)(wtPath, ".git");
19970
+ const isLinked = (0, import_node_fs22.existsSync)(gitFile) && (0, import_node_fs22.statSync)(gitFile).isFile();
19780
19971
  if (apply && !isLinked) {
19781
19972
  return fail("worktree land: run from inside the linked worktree you want to land (this is the primary checkout)");
19782
19973
  }
19783
19974
  const commonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
19784
- const primaryCheckout = commonDir ? (0, import_node_path20.dirname)(commonDir) : wtPath;
19975
+ const primaryCheckout = commonDir ? (0, import_node_path19.dirname)(commonDir) : wtPath;
19785
19976
  const stageSummary = readStageSummary(wtPath);
19786
19977
  const hasStage = stageSummary != null;
19787
19978
  const steps = landSteps(branch, true, hasStage);
@@ -19817,11 +20008,10 @@ function registerWorktreeCommands(program3) {
19817
20008
  }
19818
20009
  }
19819
20010
  releaseCwdIfUnderWorktree(wtPath, primaryCheckout);
19820
- const removeOutcome = await removeWorktreeWithRecovery(wtPath, {
19821
- git: async (args) => (await execFileP2("git", ["-C", primaryCheckout, ...args], { timeout: GIT_TIMEOUT_MS })).stdout,
19822
- sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
19823
- removeWorktreeDir: (p) => (0, import_promises5.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
19824
- });
20011
+ const removeOutcome = await removeWorktreeWithRecovery(
20012
+ wtPath,
20013
+ worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-C", primaryCheckout, ...args], { timeout: GIT_TIMEOUT_MS })).stdout)
20014
+ );
19825
20015
  report.push({
19826
20016
  step: "remove worktree",
19827
20017
  status: removeOutcome.status === "removed" ? removeOutcome.recovery ? `done (${removeOutcome.recovery})` : "done" : `failed: ${removeOutcome.error ?? "lock held"} \u2014 run: git -C "${primaryCheckout}" worktree remove --force "${wtPath}"`
@@ -19894,10 +20084,10 @@ async function gatherWorktreeContext() {
19894
20084
  }
19895
20085
  const orphanDirs = [];
19896
20086
  const wtRoot = siblingMmiWorktreesRoot(repoRoot2);
19897
- if ((0, import_node_fs23.existsSync)(wtRoot)) {
20087
+ if ((0, import_node_fs22.existsSync)(wtRoot)) {
19898
20088
  let entries = [];
19899
20089
  try {
19900
- entries = (0, import_node_fs23.readdirSync)(wtRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path20.join)(wtRoot, e.name));
20090
+ entries = (0, import_node_fs22.readdirSync)(wtRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path19.join)(wtRoot, e.name));
19901
20091
  } catch {
19902
20092
  }
19903
20093
  const known = new Set(worktrees.map((w) => w.path));
@@ -19922,8 +20112,8 @@ async function bestEffortGit(args, cwd, step, timeoutMs = GIT_TIMEOUT_MS) {
19922
20112
  }
19923
20113
 
19924
20114
  // src/issue-commands.ts
19925
- var import_node_fs24 = require("node:fs");
19926
- var import_node_crypto6 = require("node:crypto");
20115
+ var import_node_fs23 = require("node:fs");
20116
+ var import_node_crypto5 = require("node:crypto");
19927
20117
  var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
19928
20118
  async function editIssue(client, options, deps = {}) {
19929
20119
  const parsed = parseIssueRef(options.ref);
@@ -19937,7 +20127,7 @@ async function editIssue(client, options, deps = {}) {
19937
20127
  if (options.body !== void 0 || options.bodyFile !== void 0) {
19938
20128
  let body;
19939
20129
  if (options.bodyFile) {
19940
- const td = deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs24.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
20130
+ const td = deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs23.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
19941
20131
  body = await resolveIssueBody({ body: options.body, bodyFile: options.bodyFile }, td);
19942
20132
  } else {
19943
20133
  body = options.body ?? "";
@@ -20172,7 +20362,7 @@ function rowIdempotencyKey(batchKey, spec) {
20172
20362
  const identity = `${spec.type}
20173
20363
  ${spec.title.trim()}
20174
20364
  ${spec.body ?? ""}`;
20175
- const hash = (0, import_node_crypto6.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
20365
+ const hash = (0, import_node_crypto5.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
20176
20366
  return `${batchKey}:${hash}`;
20177
20367
  }
20178
20368
  var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "northStar", "parent", "repo"]);
@@ -20464,7 +20654,7 @@ function extendCreateCommand(issue2) {
20464
20654
  if (opts.batch) {
20465
20655
  let specs;
20466
20656
  try {
20467
- const raw = (0, import_node_fs24.readFileSync)(opts.batch, "utf8");
20657
+ const raw = (0, import_node_fs23.readFileSync)(opts.batch, "utf8");
20468
20658
  specs = JSON.parse(raw);
20469
20659
  if (!Array.isArray(specs)) throw new Error("batch file must contain a JSON array");
20470
20660
  } catch (e) {
@@ -20508,13 +20698,13 @@ ${lines}`);
20508
20698
  }
20509
20699
 
20510
20700
  // src/train-commands.ts
20511
- var import_node_fs26 = require("node:fs");
20512
- var import_node_path22 = require("node:path");
20513
-
20514
- // src/plugin-guard-io.ts
20515
20701
  var import_node_fs25 = require("node:fs");
20516
20702
  var import_node_path21 = require("node:path");
20517
- var import_node_os7 = require("node:os");
20703
+
20704
+ // src/plugin-guard-io.ts
20705
+ var import_node_fs24 = require("node:fs");
20706
+ var import_node_path20 = require("node:path");
20707
+ var import_node_os6 = require("node:os");
20518
20708
 
20519
20709
  // src/plugin-guard.ts
20520
20710
  function buildPluginGuardDecision(i) {
@@ -20641,13 +20831,13 @@ var NPM_VIEW_TIMEOUT_MS = 15e3;
20641
20831
  function runHostBin(bin, args, opts) {
20642
20832
  return isWin ? execFileP2("cmd.exe", ["/c", bin, ...args], opts) : execFileP2(bin, args, opts);
20643
20833
  }
20644
- var installedPluginsPath = (surface = detectSurface(process.env)) => {
20834
+ var installedPluginsPath2 = (surface = detectSurface(process.env)) => {
20645
20835
  const homeDir = surface === "codex" ? ".codex" : ".claude";
20646
- return (0, import_node_path21.join)((0, import_node_os7.homedir)(), homeDir, "plugins", "installed_plugins.json");
20836
+ return (0, import_node_path20.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
20647
20837
  };
20648
20838
  function readInstalledPlugins(surface = detectSurface(process.env)) {
20649
20839
  try {
20650
- return JSON.parse((0, import_node_fs25.readFileSync)(installedPluginsPath(surface), "utf8"));
20840
+ return JSON.parse((0, import_node_fs24.readFileSync)(installedPluginsPath2(surface), "utf8"));
20651
20841
  } catch {
20652
20842
  return null;
20653
20843
  }
@@ -20655,13 +20845,13 @@ function readInstalledPlugins(surface = detectSurface(process.env)) {
20655
20845
  function marketplaceCloneCandidates(surface, home) {
20656
20846
  if (surface === "codex") {
20657
20847
  return [
20658
- (0, import_node_path21.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
20659
- (0, import_node_path21.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
20848
+ (0, import_node_path20.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
20849
+ (0, import_node_path20.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
20660
20850
  ];
20661
20851
  }
20662
- return [(0, import_node_path21.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
20852
+ return [(0, import_node_path20.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
20663
20853
  }
20664
- function marketplaceClonePresent(surface, home, exists = import_node_fs25.existsSync) {
20854
+ function marketplaceClonePresent(surface, home, exists = import_node_fs24.existsSync) {
20665
20855
  return marketplaceCloneCandidates(surface, home).some(exists);
20666
20856
  }
20667
20857
  async function fetchNpmReleasedVersion() {
@@ -20678,8 +20868,8 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
20678
20868
  return {
20679
20869
  isOrgRepo,
20680
20870
  installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
20681
- marketplaceClonePresent: marketplaceClonePresent(surface, (0, import_node_os7.homedir)()),
20682
- pluginCachePresent: (0, import_node_fs25.existsSync)((0, import_node_path21.join)((0, import_node_os7.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
20871
+ marketplaceClonePresent: marketplaceClonePresent(surface, (0, import_node_os6.homedir)()),
20872
+ pluginCachePresent: (0, import_node_fs24.existsSync)((0, import_node_path20.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
20683
20873
  };
20684
20874
  }
20685
20875
  function claudePluginGuardState(isOrgRepo) {
@@ -20804,7 +20994,7 @@ function formatTrainStatus(r) {
20804
20994
  // src/train-commands.ts
20805
20995
  function readRepoVersion() {
20806
20996
  try {
20807
- return JSON.parse((0, import_node_fs26.readFileSync)((0, import_node_path22.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
20997
+ return JSON.parse((0, import_node_fs25.readFileSync)((0, import_node_path21.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
20808
20998
  } catch {
20809
20999
  return void 0;
20810
21000
  }
@@ -21280,7 +21470,7 @@ function registerExplainCommand(program3) {
21280
21470
  }
21281
21471
 
21282
21472
  // src/pr-commands.ts
21283
- var import_promises6 = require("node:fs/promises");
21473
+ var import_promises5 = require("node:fs/promises");
21284
21474
  var GC_GH_TIMEOUT_MS4 = 2e4;
21285
21475
  var CHECKS_WATCH_POLL_MS = 15e3;
21286
21476
  var CHECKS_WATCH_TIMEOUT_MS = 10 * 6e4;
@@ -21424,10 +21614,10 @@ function registerPrLifecycleCommands(program3) {
21424
21614
  let body;
21425
21615
  try {
21426
21616
  if (o.title || o.titleFile) {
21427
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
21617
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises5.readFile, readStdin });
21428
21618
  }
21429
21619
  if (o.body || o.bodyFile) {
21430
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
21620
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises5.readFile, readStdin });
21431
21621
  }
21432
21622
  } catch (e) {
21433
21623
  return fail(`pr edit: ${e.message}`);
@@ -21457,7 +21647,7 @@ function registerPrLifecycleCommands(program3) {
21457
21647
  }
21458
21648
  let body;
21459
21649
  try {
21460
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
21650
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises5.readFile, readStdin });
21461
21651
  } catch (e) {
21462
21652
  return fail(`pr comment: ${e.message}`);
21463
21653
  }
@@ -21945,9 +22135,10 @@ function checkCliVersion(input) {
21945
22135
  function checkPluginCache(input) {
21946
22136
  const evidence = [
21947
22137
  `cached versions: ${input.cached}`,
21948
- `stale: ${input.stale.length ? input.stale.join(", ") : "(none)"}`
22138
+ `stale: ${input.stale.length ? input.stale.join(", ") : "(none)"}`,
22139
+ `orphaned staging dirs: ${input.staging.length ? input.staging.join(", ") : "(none)"}`
21949
22140
  ];
21950
- if (input.stale.length === 0) {
22141
+ if (input.stale.length === 0 && input.staging.length === 0) {
21951
22142
  return {
21952
22143
  ok: true,
21953
22144
  label: "plugin cache",
@@ -21955,11 +22146,21 @@ function checkPluginCache(input) {
21955
22146
  verbose: evidence
21956
22147
  };
21957
22148
  }
21958
- const size = input.bytes > 0 ? `, ${(input.bytes / 1e6).toFixed(1)} MB` : "";
22149
+ const parts = [];
22150
+ if (input.stale.length) {
22151
+ const size = input.bytes > 0 ? `, ${(input.bytes / 1e6).toFixed(1)} MB` : "";
22152
+ parts.push(`${input.stale.length} stale${size}`);
22153
+ }
22154
+ if (input.staging.length) {
22155
+ const size = input.stagingBytes > 0 ? `, ${(input.stagingBytes / 1e6).toFixed(1)} MB` : "";
22156
+ parts.push(`${input.staging.length} orphaned staging dir(s)${size}`);
22157
+ }
21959
22158
  return {
21960
22159
  ok: false,
21961
22160
  label: "plugin cache",
21962
- detail: `${input.cached} versions cached (${input.stale.length} stale${size})`,
22161
+ // Lead with the versioned-cache framing when there are stale versions; otherwise report the staging litter
22162
+ // on its own so a cache with zero versioned dirs still gets an honest ✗.
22163
+ detail: input.stale.length ? `${input.cached} versions cached (${parts.join("; ")})` : parts.join("; "),
21963
22164
  fix: "run `mmi-cli plugin-prune` to review, then `mmi-cli plugin-prune --apply` to delete",
21964
22165
  verbose: evidence
21965
22166
  };
@@ -21999,12 +22200,27 @@ function checkSchedules(probe) {
21999
22200
  ok: false,
22000
22201
  label: "schedules",
22001
22202
  detail: `${probe.drift.length} drift finding(s)`,
22002
- fix: "run `mmi-cli schedules` \u2014 something dead is still registered; fix it in the owning repo",
22203
+ fix: "run `mmi-cli schedules` \u2014 file/registry/live disagree; each drift line names its per-class remedy, applied in the owning repo",
22003
22204
  verbose: evidence
22004
22205
  };
22005
22206
  }
22006
22207
  return { ok: true, label: "schedules", detail: `${probe.armed} armed`, verbose: evidence };
22007
22208
  }
22209
+ function checkDocsAudit(probe) {
22210
+ if (!probe) return null;
22211
+ if (!probe.armed) {
22212
+ return { ok: true, label: "docs-audit", detail: "janitor not armed", verbose: [probe.detail] };
22213
+ }
22214
+ if (!probe.ok) {
22215
+ return {
22216
+ ok: false,
22217
+ label: "docs-audit",
22218
+ fix: `${probe.detail} \u2014 re-run the janitor in the owning repo or re-arm its schedule (the remedy never lives in this doctor)`,
22219
+ verbose: [probe.detail]
22220
+ };
22221
+ }
22222
+ return { ok: true, label: "docs-audit", detail: probe.detail, verbose: [probe.detail] };
22223
+ }
22008
22224
  function planGitignore(current) {
22009
22225
  const { content, changed } = upsertManagedGitignoreBlock(current);
22010
22226
  return changed ? { ok: false, content } : { ok: true };
@@ -22063,6 +22279,15 @@ async function runDoctorClean(opts, io, deps) {
22063
22279
  const sched = checkSchedules(probe);
22064
22280
  if (sched) checks.push(sched);
22065
22281
  }
22282
+ if (!opts.fast && !opts.banner && !opts.preflight && deps.docsAudit) {
22283
+ const probe = await deps.docsAudit().catch((e) => ({
22284
+ armed: true,
22285
+ ok: false,
22286
+ detail: `docs-audit probe failed \u2014 ${e.message}`
22287
+ }));
22288
+ const docsAudit2 = checkDocsAudit(probe);
22289
+ if (docsAudit2) checks.push(docsAudit2);
22290
+ }
22066
22291
  if (isOrgRepo && !opts.fast && !opts.banner) {
22067
22292
  try {
22068
22293
  checks.push(checkTrainSync(await deps.syncTrain()));
@@ -22147,17 +22372,17 @@ async function runDoctorClean(opts, io, deps) {
22147
22372
  }
22148
22373
 
22149
22374
  // src/doctor-io.ts
22150
- var import_node_fs27 = require("node:fs");
22151
- var import_node_os8 = require("node:os");
22152
- var import_node_path23 = require("node:path");
22153
- var import_node_child_process12 = require("node:child_process");
22375
+ var import_node_fs26 = require("node:fs");
22376
+ var import_node_os7 = require("node:os");
22377
+ var import_node_path22 = require("node:path");
22378
+ var import_node_child_process11 = require("node:child_process");
22154
22379
  var import_node_util8 = require("node:util");
22155
- var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process12.execFile);
22380
+ var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process11.execFile);
22156
22381
  var MMI_PLUGIN_ID2 = "mmi@mutmutco";
22157
22382
  function installedClaudePluginVersion() {
22158
22383
  try {
22159
22384
  const file = JSON.parse(
22160
- (0, import_node_fs27.readFileSync)((0, import_node_path23.join)((0, import_node_os8.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
22385
+ (0, import_node_fs26.readFileSync)((0, import_node_path22.join)((0, import_node_os7.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
22161
22386
  );
22162
22387
  const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
22163
22388
  if (versions.length === 0) return void 0;
@@ -22168,7 +22393,7 @@ function installedClaudePluginVersion() {
22168
22393
  }
22169
22394
  function worktreeRootSync() {
22170
22395
  try {
22171
- const out = (0, import_node_child_process12.execFileSync)("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
22396
+ const out = (0, import_node_child_process11.execFileSync)("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
22172
22397
  let root = out.endsWith("\n") ? out.slice(0, -1) : out;
22173
22398
  if (process.platform === "win32" && root.endsWith("\r")) root = root.slice(0, -1);
22174
22399
  return root || null;
@@ -22178,13 +22403,13 @@ function worktreeRootSync() {
22178
22403
  }
22179
22404
  var gitignorePath = () => {
22180
22405
  const root = worktreeRootSync();
22181
- return root === null ? null : (0, import_node_path23.join)(root, ".gitignore");
22406
+ return root === null ? null : (0, import_node_path22.join)(root, ".gitignore");
22182
22407
  };
22183
22408
  function readGitignore() {
22184
22409
  const path2 = gitignorePath();
22185
22410
  if (path2 === null) return null;
22186
22411
  try {
22187
- return (0, import_node_fs27.readFileSync)(path2, "utf8");
22412
+ return (0, import_node_fs26.readFileSync)(path2, "utf8");
22188
22413
  } catch {
22189
22414
  return null;
22190
22415
  }
@@ -22193,7 +22418,7 @@ function writeGitignore(content) {
22193
22418
  const path2 = gitignorePath();
22194
22419
  if (path2 === null) return false;
22195
22420
  try {
22196
- (0, import_node_fs27.writeFileSync)(path2, content, "utf8");
22421
+ (0, import_node_fs26.writeFileSync)(path2, content, "utf8");
22197
22422
  return true;
22198
22423
  } catch {
22199
22424
  return false;
@@ -22217,11 +22442,27 @@ async function repoRoot() {
22217
22442
  }
22218
22443
  function hasRepoLocalWorktrees() {
22219
22444
  const root = worktreeRootSync();
22220
- return root !== null && (0, import_node_fs27.existsSync)((0, import_node_path23.join)(root, ".worktrees"));
22445
+ return root !== null && (0, import_node_fs26.existsSync)((0, import_node_path22.join)(root, ".worktrees"));
22221
22446
  }
22222
22447
 
22223
22448
  // src/index.ts
22224
22449
  var execFileGitRun = async (file, args) => (await execFileP2(file, args, { timeout: GIT_TIMEOUT_MS })).stdout;
22450
+ async function currentRepoFullName() {
22451
+ const remote = (await gitOut(["remote", "get-url", "origin"])).replace(/\.git$/, "");
22452
+ const parts = remote.split(/[:/]/).filter(Boolean);
22453
+ if (parts.length >= 2) return `${parts[parts.length - 2]}/${parts[parts.length - 1]}`;
22454
+ return `mutmutco/${await repoSlug()}`;
22455
+ }
22456
+ async function readDocsAuditFetch(repo) {
22457
+ const list = await fetchDocsAuditList(registryClientDeps(await loadConfig()));
22458
+ if ("notArmed" in list) return { notArmed: true };
22459
+ if (!list.ok) return { ok: false, error: list.error };
22460
+ const row = list.rows.find((r) => r.repo === repo);
22461
+ return {
22462
+ ok: true,
22463
+ verdict: row ? { repo: row.repo, date: row.date, shaRange: row.shaRange, outcome: row.outcome, checkerVendor: row.checkerVendor } : null
22464
+ };
22465
+ }
22225
22466
  function mmiDoctorDeps() {
22226
22467
  return {
22227
22468
  githubLogin,
@@ -22265,12 +22506,18 @@ function mmiDoctorDeps() {
22265
22506
  return { personal, app };
22266
22507
  },
22267
22508
  pluginCache: () => {
22268
- const plan = buildPluginCachePlan((0, import_node_os9.homedir)(), runningPluginVersion(process.env, resolveClientVersion()), {
22269
- exists: (p) => (0, import_node_fs28.existsSync)(p),
22270
- listVersionDirs: (root) => (0, import_node_fs28.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
22271
- dirBytes: () => 0
22272
- });
22273
- return { stale: plan.prune, bytes: plan.bytes, cached: plan.keep.length + plan.prune.length };
22509
+ const plan = buildPluginCachePlan(
22510
+ (0, import_node_os8.homedir)(),
22511
+ runningPluginVersion(process.env, resolveClientVersion()),
22512
+ pluginCacheFsDeps((0, import_node_os8.homedir)(), () => 0)
22513
+ );
22514
+ return {
22515
+ stale: plan.prune,
22516
+ bytes: plan.bytes,
22517
+ cached: plan.keep.length + plan.prune.length,
22518
+ staging: plan.staging.map((s) => s.name),
22519
+ stagingBytes: plan.stagingBytes
22520
+ };
22274
22521
  },
22275
22522
  // #3008: the schedules-notebook drift probe, compacted for the check. Full doctor only — the gather in
22276
22523
  // runDoctorClean skips this dep entirely on fast/banner/preflight runs.
@@ -22283,6 +22530,15 @@ function mmiDoctorDeps() {
22283
22530
  incomplete,
22284
22531
  drift
22285
22532
  };
22533
+ },
22534
+ // #3075: the docs-audit dead-man verdict probe, compacted for the check. Full doctor only, exactly like
22535
+ // schedulesNotebook — a registry read the SessionStart banner / --preflight / --fast lanes never pay for.
22536
+ // The 404/absent route maps to `armed:false` (an informational "not armed" line), never a false RED.
22537
+ docsAudit: async () => {
22538
+ const repo = await currentRepoFullName();
22539
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
22540
+ const status = docsAuditStatus(await readDocsAuditFetch(repo), { repo, today });
22541
+ return { armed: status.state !== "not-armed", ok: status.ok, detail: status.line };
22286
22542
  }
22287
22543
  };
22288
22544
  }
@@ -22357,19 +22613,19 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
22357
22613
  });
22358
22614
  var rules = program2.command("rules").description("org-managed .gitignore delivery");
22359
22615
  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) => {
22360
- const path2 = (0, import_node_path24.join)(process.cwd(), ".gitignore");
22361
- const current = (0, import_node_fs28.existsSync)(path2) ? (0, import_node_fs28.readFileSync)(path2, "utf8") : null;
22616
+ const path2 = (0, import_node_path23.join)(process.cwd(), ".gitignore");
22617
+ const current = (0, import_node_fs27.existsSync)(path2) ? (0, import_node_fs27.readFileSync)(path2, "utf8") : null;
22362
22618
  const plan = planManagedGitignore(current);
22363
22619
  const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
22364
22620
  if (opts.json) {
22365
- if (opts.write && plan.changed) (0, import_node_fs28.writeFileSync)(path2, plan.content, "utf8");
22621
+ if (opts.write && plan.changed) (0, import_node_fs27.writeFileSync)(path2, plan.content, "utf8");
22366
22622
  console.log(JSON.stringify(plan, null, 2));
22367
22623
  if (!opts.write && plan.changed) process.exitCode = 1;
22368
22624
  return;
22369
22625
  }
22370
22626
  if (opts.write) {
22371
22627
  if (plan.changed) {
22372
- (0, import_node_fs28.writeFileSync)(path2, plan.content, "utf8");
22628
+ (0, import_node_fs27.writeFileSync)(path2, plan.content, "utf8");
22373
22629
  console.log(`mmi-cli rules gitignore: updated .gitignore (${drift})`);
22374
22630
  } else {
22375
22631
  console.log("mmi-cli rules gitignore: up to date");
@@ -22523,7 +22779,7 @@ function runWorktreeInstall(command, cwd, quiet) {
22523
22779
  const file = isWin2 ? "cmd.exe" : bin;
22524
22780
  const spawnArgs = isWin2 ? ["/c", bin, ...args] : args;
22525
22781
  return new Promise((resolve5, reject) => {
22526
- const child = (0, import_node_child_process13.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
22782
+ const child = (0, import_node_child_process12.spawn)(file, spawnArgs, { cwd, stdio: quiet ? "ignore" : "inherit", windowsHide: true });
22527
22783
  const timer = setTimeout(() => {
22528
22784
  try {
22529
22785
  child.kill();
@@ -22545,7 +22801,7 @@ function runWorktreeInstall(command, cwd, quiet) {
22545
22801
  async function primaryCheckoutRoot(worktreeRoot) {
22546
22802
  try {
22547
22803
  const out = (await execFileP2("git", ["-C", worktreeRoot, "rev-parse", "--path-format=absolute", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
22548
- return out ? (0, import_node_path24.dirname)(out) : void 0;
22804
+ return out ? (0, import_node_path23.dirname)(out) : void 0;
22549
22805
  } catch {
22550
22806
  return void 0;
22551
22807
  }
@@ -22560,26 +22816,26 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
22560
22816
  function acquireWorktreeSetupLock(worktreeRoot) {
22561
22817
  const lockPath = repoRuntimeStatePath(worktreeRoot, "worktree-setup.lock");
22562
22818
  const take = () => {
22563
- const fd = (0, import_node_fs28.openSync)(lockPath, "wx");
22819
+ const fd = (0, import_node_fs27.openSync)(lockPath, "wx");
22564
22820
  try {
22565
- (0, import_node_fs28.writeSync)(fd, String(Date.now()));
22821
+ (0, import_node_fs27.writeSync)(fd, String(Date.now()));
22566
22822
  } finally {
22567
- (0, import_node_fs28.closeSync)(fd);
22823
+ (0, import_node_fs27.closeSync)(fd);
22568
22824
  }
22569
22825
  return () => {
22570
22826
  try {
22571
- (0, import_node_fs28.rmSync)(lockPath, { force: true });
22827
+ (0, import_node_fs27.rmSync)(lockPath, { force: true });
22572
22828
  } catch {
22573
22829
  }
22574
22830
  };
22575
22831
  };
22576
22832
  try {
22577
- (0, import_node_fs28.mkdirSync)((0, import_node_path24.dirname)(lockPath), { recursive: true });
22833
+ (0, import_node_fs27.mkdirSync)((0, import_node_path23.dirname)(lockPath), { recursive: true });
22578
22834
  return take();
22579
22835
  } catch {
22580
22836
  try {
22581
- if (Date.now() - (0, import_node_fs28.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
22582
- (0, import_node_fs28.rmSync)(lockPath, { force: true });
22837
+ if (Date.now() - (0, import_node_fs27.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
22838
+ (0, import_node_fs27.rmSync)(lockPath, { force: true });
22583
22839
  return take();
22584
22840
  }
22585
22841
  } catch {
@@ -22759,7 +23015,7 @@ function scheduleRelatedDiscovery(o) {
22759
23015
  try {
22760
23016
  const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body, "--fail-soft"];
22761
23017
  if (o.repo) args.push("--repo", o.repo);
22762
- spawnDetachedSelf(args, { spawn: import_node_child_process13.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
23018
+ spawnDetachedSelf(args, { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
22763
23019
  } catch {
22764
23020
  }
22765
23021
  }
@@ -22767,6 +23023,50 @@ registerSecretsCommands(program2);
22767
23023
  registerEdgeCommands(program2);
22768
23024
  registerBoxCommands(program2);
22769
23025
  registerSchedulesCommands(program2);
23026
+ var docs = program2.command("docs").description("generated docs surfaces \u2014 the routing index (org knowledge layer)");
23027
+ docs.command("index").description("regenerate docs/index.md from the docs/ tree (--write, the default) or fail on drift (--check) \u2014 the generated routing index, never hand-maintained").option("--check", "compare against the committed docs/index.md and exit 1 on drift; never write").option("--write", "regenerate docs/index.md when it has drifted (the default)").action(async (o) => {
23028
+ try {
23029
+ const root = await repoRoot();
23030
+ const result = docsIndex(createDocsIndexDeps(root), { check: Boolean(o.check) });
23031
+ if (o.check) {
23032
+ if (result.drift) {
23033
+ return failGraceful(`docs index: ${DOCS_INDEX_PATH} is stale \u2014 run \`mmi-cli docs index --write\` and commit the result`);
23034
+ }
23035
+ console.log(`docs index: ${DOCS_INDEX_PATH} is current`);
23036
+ return;
23037
+ }
23038
+ console.log(result.wrote ? `docs index: wrote ${DOCS_INDEX_PATH}` : `docs index: ${DOCS_INDEX_PATH} already current`);
23039
+ } catch (e) {
23040
+ await failGraceful(e.message);
23041
+ }
23042
+ });
23043
+ var docsAudit = program2.command("docs-audit").description("the docs janitor verdict ledger \u2014 record a dated run verdict, or read the dead-man status back");
23044
+ 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)").requiredOption("--outcome <kind>", "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) => {
23045
+ try {
23046
+ const repo = o.repo ?? await currentRepoFullName();
23047
+ const date = o.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
23048
+ let outcome;
23049
+ if (o.outcome === "clean") outcome = { kind: "clean" };
23050
+ else if (o.outcome === "refreshed") outcome = { kind: "refreshed", count: Number(o.count) };
23051
+ else if (o.outcome === "failed") outcome = { kind: "failed", reason: o.reason ?? "" };
23052
+ else return failGraceful(`docs-audit record: --outcome must be clean|refreshed|failed, got "${o.outcome}"`);
23053
+ const verdict = docsAuditRecord({ repo, date, shaRange: o.shaRange, outcome, checkerVendor: o.checkerVendor });
23054
+ await reportWrite("docs-audit record", await recordDocsAudit(verdict, registryClientDeps(await loadConfig())));
23055
+ } catch (e) {
23056
+ await failGraceful(e.message);
23057
+ }
23058
+ });
23059
+ docsAudit.command("status").description("read the janitor dead-man verdict back for one repo \u2014 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)").action(async (o) => {
23060
+ try {
23061
+ const repo = o.repo ?? await currentRepoFullName();
23062
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
23063
+ const result = docsAuditStatus(await readDocsAuditFetch(repo), { repo, today });
23064
+ console.log(result.line);
23065
+ if (!result.ok) process.exitCode = 1;
23066
+ } catch (e) {
23067
+ await failGraceful(e.message);
23068
+ }
23069
+ });
22770
23070
  async function reportWrite(label, res) {
22771
23071
  if (res.ok) {
22772
23072
  console.log(JSON.stringify(res.body));
@@ -23104,7 +23404,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
23104
23404
  if (dupe) return fail(`project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
23105
23405
  if (o.secretsFile) {
23106
23406
  try {
23107
- vars.push(`secrets=${(0, import_node_fs28.readFileSync)(o.secretsFile, "utf8")}`);
23407
+ vars.push(`secrets=${(0, import_node_fs27.readFileSync)(o.secretsFile, "utf8")}`);
23108
23408
  } catch (e) {
23109
23409
  return fail(`project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
23110
23410
  }
@@ -23400,8 +23700,8 @@ withExamples(mutating(
23400
23700
  let extraLabels = [];
23401
23701
  try {
23402
23702
  issueType = resolveCreateType(o.type, "issue create");
23403
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
23404
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
23703
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
23704
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
23405
23705
  if (o.northStar !== void 0) {
23406
23706
  northStarSlug = normalizeNorthStarSlug(o.northStar);
23407
23707
  body = appendNorthStarLine(body, northStarSlug);
@@ -23546,7 +23846,7 @@ issue.command("comment <ref>").description("post a Markdown comment to an issue
23546
23846
  }
23547
23847
  let body;
23548
23848
  try {
23549
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
23849
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
23550
23850
  } catch (e) {
23551
23851
  return fail(`issue comment: ${e.message}`);
23552
23852
  }
@@ -23606,8 +23906,8 @@ program2.command("report").description("file a friction report on the Hub board
23606
23906
  let title;
23607
23907
  const sourceRepo = o.repo ?? await resolveRepo(void 0);
23608
23908
  try {
23609
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
23610
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
23909
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
23910
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
23611
23911
  priority = resolveCreatePriority(o.priority, "report");
23612
23912
  if (!ISSUE_TYPES.includes(o.type)) {
23613
23913
  throw new Error(`unknown issue type "${o.type}" \u2014 expected one of: ${ISSUE_TYPES.join(", ")}`);
@@ -23656,8 +23956,8 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
23656
23956
  let args;
23657
23957
  try {
23658
23958
  skill = assertSkillName(o.skill);
23659
- rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
23660
- const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
23959
+ rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
23960
+ const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
23661
23961
  title = buildSkillLessonTitle(skill, rawTitle);
23662
23962
  priority = resolveCreatePriority(o.priority, "skill-lesson");
23663
23963
  body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
@@ -23708,11 +24008,12 @@ withExamples(pr.command("create").description("create a PR and print {number,url
23708
24008
  let body;
23709
24009
  let title;
23710
24010
  try {
23711
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
23712
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
24011
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
24012
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
23713
24013
  } catch (e) {
23714
24014
  return fail(`pr create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
23715
24015
  }
24016
+ body = normalizeClosingDirectives(body);
23716
24017
  const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo, draft: o.draft }));
23717
24018
  console.log(JSON.stringify(created));
23718
24019
  }), [
@@ -23743,11 +24044,11 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
23743
24044
  }
23744
24045
  });
23745
24046
  async function listCiWorkflowPaths(cwd = process.cwd()) {
23746
- const wfDir = (0, import_node_path24.join)(cwd, ".github", "workflows");
23747
- if (!(0, import_node_fs28.existsSync)(wfDir)) return [];
23748
- return (0, import_node_fs28.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
24047
+ const wfDir = (0, import_node_path23.join)(cwd, ".github", "workflows");
24048
+ if (!(0, import_node_fs27.existsSync)(wfDir)) return [];
24049
+ return (0, import_node_fs27.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
23749
24050
  try {
23750
- return workflowReportsPrChecks((0, import_node_fs28.readFileSync)((0, import_node_path24.join)(wfDir, name), "utf8"));
24051
+ return workflowReportsPrChecks((0, import_node_fs27.readFileSync)((0, import_node_path23.join)(wfDir, name), "utf8"));
23751
24052
  } catch {
23752
24053
  return true;
23753
24054
  }
@@ -23774,16 +24075,16 @@ function ciAuditDeps() {
23774
24075
  // gate re-seed step is skipped gracefully rather than failing mid-run.
23775
24076
  readSeedFile: (path2) => {
23776
24077
  if (!root) return null;
23777
- const fullPath = (0, import_node_path24.join)(root, path2);
23778
- return (0, import_node_fs28.existsSync)(fullPath) ? (0, import_node_fs28.readFileSync)(fullPath, "utf8") : null;
24078
+ const fullPath = (0, import_node_path23.join)(root, path2);
24079
+ return (0, import_node_fs27.existsSync)(fullPath) ? (0, import_node_fs27.readFileSync)(fullPath, "utf8") : null;
23779
24080
  }
23780
24081
  };
23781
24082
  }
23782
24083
  function hubRoot() {
23783
- const fromPkg = (0, import_node_path24.join)(__dirname, "..", "..");
24084
+ const fromPkg = (0, import_node_path23.join)(__dirname, "..", "..");
23784
24085
  const marker = "skills/bootstrap/seeds/manifest.json";
23785
- if ((0, import_node_fs28.existsSync)((0, import_node_path24.join)(fromPkg, marker))) return fromPkg;
23786
- if ((0, import_node_fs28.existsSync)((0, import_node_path24.join)(process.cwd(), marker))) return process.cwd();
24086
+ if ((0, import_node_fs27.existsSync)((0, import_node_path23.join)(fromPkg, marker))) return fromPkg;
24087
+ if ((0, import_node_fs27.existsSync)((0, import_node_path23.join)(process.cwd(), marker))) return process.cwd();
23787
24088
  return null;
23788
24089
  }
23789
24090
  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) => {
@@ -23824,10 +24125,10 @@ pr.command("checks-wait <number>").description(`bounded wait for PR checks; skip
23824
24125
  if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
23825
24126
  if (result.status === "timeout" || result.status === "rate-limited") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
23826
24127
  });
23827
- pr.command("land <number>").description("agent merge path (#1440): train probe \u2192 checks-wait \u2192 merge --auto \u2192 poll enqueued \u2014 development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--preserve-worktree", "after merge, keep the local PR worktree/stage/branch for an active batch (#1888)").action(async (number, o) => {
24128
+ pr.command("land <number>").description("agent merge path (#1440): train probe \u2192 checks-wait \u2192 merge --auto \u2192 poll enqueued \u2014 development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--preserve-worktree", "after merge, keep the local PR worktree/stage/branch for an active batch (#1888)").option("--force", "acknowledge and land past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012)").action(async (number, o) => {
23828
24129
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
23829
24130
  const startingPath = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
23830
- assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr land");
24131
+ assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr land", { force: o.force });
23831
24132
  const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false }, {
23832
24133
  resolveRepo: async (prNumber, repoOpt) => {
23833
24134
  const args = repoOpt ? ["--repo", repoOpt] : repoArgs;
@@ -23917,13 +24218,13 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
23917
24218
  }
23918
24219
  if (result.status === "failed" || result.cleanupError) process.exitCode = 1;
23919
24220
  });
23920
- pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch; on no-ci repos run pr ci-policy / checks-wait first (#1432)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").option("--preserve-worktree", "keep the local PR worktree/stage/branch for an active multi-issue batch (#1888)").action(async (number, o) => {
24221
+ pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch; on no-ci repos run pr ci-policy / checks-wait first (#1432)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").option("--preserve-worktree", "keep the local PR worktree/stage/branch for an active multi-issue batch (#1888)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012)").action(async (number, o) => {
23921
24222
  const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
23922
24223
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
23923
24224
  const [headRef, baseRef, headRefOid] = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName,baseRefName,headRefOid", "--jq", '.headRefName + " " + .baseRefName + " " + (.headRefOid // "")'], { timeout: GC_GH_TIMEOUT_MS2 })).stdout.trim().split(/\s+/);
23924
24225
  const headIsProtected = isProtectedBranch(headRef);
23925
24226
  const startingPath = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
23926
- const housekeeping = assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr merge");
24227
+ const housekeeping = assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr merge", { force: o.force });
23927
24228
  const beforeWorktrees = parseWorktreePorcelain(
23928
24229
  (await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
23929
24230
  );
@@ -23997,15 +24298,18 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
23997
24298
  const deferredStore = await createDeferredWorktreeStore();
23998
24299
  let localCleanup;
23999
24300
  try {
24301
+ const wtDeps = worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout);
24000
24302
  localCleanup = await cleanupPrMergeLocalBranch(headRef, {
24001
24303
  beforeWorktrees,
24002
24304
  startingPath,
24003
- pathExists: (p) => (0, import_node_fs28.existsSync)(p),
24305
+ pathExists: (p) => (0, import_node_fs27.existsSync)(p),
24004
24306
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
24005
24307
  teardownWorktreeStage,
24006
24308
  deferredStore,
24007
24309
  expectedHeadOid: headRefOid || void 0,
24008
- removeWorktreeDir: worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout).removeWorktreeDir,
24310
+ detachReparsePoints: wtDeps.detachReparsePoints,
24311
+ // #3064: junction-safe teardown
24312
+ removeWorktreeDir: wtDeps.removeWorktreeDir,
24009
24313
  // After merge, return the local checkout to the (fast-forwarded) branch the PR merged into so
24010
24314
  // grind/build never leave the primary parked on a dead feature branch (#1606).
24011
24315
  returnToBranch: baseRef,
@@ -24094,6 +24398,7 @@ function trainApplyDeps() {
24094
24398
  // fold's version metadata, which the candidate replaces with its own. (The Hub's wider distribution
24095
24399
  // set never reaches this guard: the Hub is direct-track.)
24096
24400
  hotfixCoverage: (input) => checkHotfixCoverage({ ...input, manifestPaths: ["package.json", "package-lock.json"] }),
24401
+ hotfixCarries: (input) => checkHotfixCarries(input),
24097
24402
  // #2737: enqueue GitHub auto-merge on the `main -> development` alignment PR the release opens, so the
24098
24403
  // train lands the back-merge itself instead of leaving a manual step. Same enqueue path `pr land` uses,
24099
24404
  // but method `--merge` (NOT squash): the misalignment guard needs the merge parentage to survive.
@@ -24101,8 +24406,8 @@ function trainApplyDeps() {
24101
24406
  // Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
24102
24407
  announce: (args) => announceRelease({
24103
24408
  run: async (file, cmdArgs) => (await execFileP2(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
24104
- readFile: (path2) => (0, import_promises7.readFile)(path2, "utf8"),
24105
- removeFile: (path2) => (0, import_promises7.unlink)(path2)
24409
+ readFile: (path2) => (0, import_promises6.readFile)(path2, "utf8"),
24410
+ removeFile: (path2) => (0, import_promises6.unlink)(path2)
24106
24411
  }, args),
24107
24412
  fetchEdgeDomains: async (slug) => {
24108
24413
  const proj = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
@@ -24249,7 +24554,7 @@ var hotfixCmd = program2.command("hotfix").description("stepwise hotfix orchestr
24249
24554
  console.log(o.json ? JSON.stringify({ command: "hotfix", steps }, null, 2) : renderSteps("mmi-cli hotfix: dry-run plan", steps));
24250
24555
  });
24251
24556
  hotfixCmd.command("start").description("cherry-pick a merged development PR (or SHA) onto hotfix/vX.Y.Z from origin/main, bump the distribution, open the main-base PR").requiredOption("--from <pr#|sha>", "merged development PR number or commit SHA to cherry-pick").option("--json", "machine-readable output").action(async (o) => runHotfixSub("start", () => runHotfixStart(trainApplyDeps(), { from: o.from }), o.json, renderHotfixStart));
24252
- hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated summary lines for the Hub Slack announcement (#883)").action(async (version, o) => runHotfixSub("release", () => runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile }), o.json, renderHotfixRelease));
24557
+ hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated summary lines for the Hub Slack announcement (#883)").option("--carries <pr#|sha[,pr#|sha...]>", "declared fix target(s) this hotfix must carry; each must be proven present before tagging (#3056)").action(async (version, o) => runHotfixSub("release", () => runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, carries: o.carries ? [o.carries] : [] }), o.json, renderHotfixRelease));
24253
24558
  hotfixCmd.command("status [version]").description("derive the full hotfix pipeline state from live git/gh reads and name the exact next subcommand").option("--json", "machine-readable output").action(async (version, o) => runHotfixSub("status", () => runHotfixStatus(trainApplyDeps(), version), o.json, renderHotfixStatus));
24254
24559
  var ci = program2.command("ci").description("org CI + merge-readiness audit and reconcile");
24255
24560
  ci.command("audit").description("read-only fleet scan: gate workflow, ruleset contexts, auto-merge, registry META (#1440)").option("--json", "machine-readable output").option("--markdown", "fleet summary table for issue comments").option("--repo <owner/repo>", "audit one repo instead of the full registry").action(async (o) => {
@@ -24284,43 +24589,6 @@ ${r.repo}: applied=[${r.applied.join("; ")}] skipped=[${r.skipped.join("; ")}]${
24284
24589
  }
24285
24590
  if (!audit.ok) process.exitCode = 1;
24286
24591
  });
24287
- var wiki = program2.command("wiki").description("release wiki lane \u2014 publish generated pages via a short-lived, repo-scoped minted token");
24288
- wiki.command("publish <pages-dir>").description("mint a 1h repo-scoped contents:write token (master-gated) and publish the generated .md pages to <repo>.wiki.git; the token stays in memory (never printed/committed). Runs from any org repo checkout \u2014 the publisher ships inside mmi-cli, no repo-local script required. Fails loud on the credential gap \u2014 never a silent skip").option("--repo <owner/repo>", "target repo (defaults to the cwd repo)").option("--json", "machine-readable output").action(async (pagesDir, o) => {
24289
- const repo = await resolveRepo(o.repo);
24290
- if (!repo) return fail("wiki publish: could not determine the target repo (pass --repo owner/name)");
24291
- const cfg = await loadConfig();
24292
- const ok = await wikiPublish(
24293
- {
24294
- mint: (r) => mintWikiToken(r, registryClientDeps(cfg)),
24295
- publish: (o2) => publishWikiViaGit(realWikiGitPublishDeps(), o2),
24296
- log: (msg) => console.log(msg),
24297
- err: (msg) => console.error(msg)
24298
- },
24299
- { repo, pagesDir, json: o.json }
24300
- );
24301
- if (!ok) process.exitCode = 1;
24302
- });
24303
- wiki.command("plan").description("compute the release wiki plan for the cwd repo \u2014 collect its opt-in `wiki:page` pages, read the previous incremental stamp from <repo>.wiki.git (scoped minted token), and print a JSON plan naming which pages changed plus the fresh full stamp. Runs from ANY org repo checkout (no repo-local script). Fails loud on the credential/read gap \u2014 never a silent skip").option("--repo <owner/repo>", "target repo (defaults to the cwd repo)").action(async (o) => {
24304
- const repo = await resolveRepo(o.repo);
24305
- if (!repo) return fail("wiki plan: could not determine the target repo (pass --repo owner/name)");
24306
- const cfg = await loadConfig();
24307
- const readers = fsReaders(process.cwd());
24308
- const ok = await wikiPlan(
24309
- {
24310
- collectPages: () => collectFeaturePages(readers),
24311
- readSource: (p) => readers.readDoc(p),
24312
- mintAndReadStamp: async (r) => {
24313
- const minted = await mintWikiToken(r, registryClientDeps(cfg));
24314
- if (!minted.ok) return { ok: false, error: minted.error };
24315
- return readWikiStampViaGit(realWikiGitPublishDeps(), { repo: r, token: minted.mint.token });
24316
- },
24317
- out: (json) => console.log(json),
24318
- err: (msg) => console.error(msg)
24319
- },
24320
- { repo }
24321
- );
24322
- if (!ok) process.exitCode = 1;
24323
- });
24324
24592
  registerBootstrapCommands(program2);
24325
24593
  var access = program2.command("access").description("org access audit (read-only)");
24326
24594
  access.command("role [repo]").description("D14 train authority for a repo (server-side Hub check): master | project-admin | developer").option("--json", "machine-readable output").action(async (repoArg) => {
@@ -24358,16 +24626,20 @@ access.command("audit").description("audit collaborator roles + train-branch pus
24358
24626
  const repoClass = o.class;
24359
24627
  targets = [{ repo: o.repo, class: repoClass, releaseTrack: repoClass === "content" ? "trunk" : resolveReleaseTrack(meta, void 0, o.repo) }];
24360
24628
  } else {
24361
- const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs28.existsSync)("projects.json") ? (0, import_node_fs28.readFileSync)("projects.json", "utf8") : null;
24362
- if (!projectsJson) return failGraceful("access audit: no project registry \u2014 Hub API unreachable and projects.json not found; run from the MMI-Hub repo root or pass --repo <owner/repo>");
24363
- const fanoutJson = (0, import_node_fs28.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs28.readFileSync)(".github/fanout-targets.json", "utf8") : null;
24364
- targets = loadAccessTargets(projectsJson, fanoutJson);
24629
+ const resolution = resolveOrgAuditTargets(registryProjects);
24630
+ if (!resolution.ok) {
24631
+ const report2 = { ok: false, owners: [], orgFindings: [resolution.finding], repos: [] };
24632
+ console.log(o.json ? JSON.stringify(report2, null, 2) : renderAccessReport(report2));
24633
+ process.exitCode = 1;
24634
+ return;
24635
+ }
24636
+ targets = resolution.targets;
24365
24637
  }
24366
24638
  const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
24367
- const fileMatrix = (0, import_node_fs28.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs28.readFileSync)("access-matrix.json", "utf8")) : {};
24639
+ const fileMatrix = (0, import_node_fs27.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs27.readFileSync)("access-matrix.json", "utf8")) : {};
24368
24640
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
24369
24641
  const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
24370
- const fileContracts = (0, import_node_fs28.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs28.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
24642
+ const fileContracts = (0, import_node_fs27.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs27.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
24371
24643
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
24372
24644
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
24373
24645
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
@@ -24377,7 +24649,7 @@ access.command("capabilities").description("enumerate your effective vault reach
24377
24649
  var isWin2 = process.platform === "win32";
24378
24650
  program2.command("doctor").description("check onboarding gates and auto-heal CLI/plugin wiring; use --verbose for the full audit checklist").option("--banner", "one-line resume summary; silent when all gates pass").option("--fast", "offline-safe fast lane; skips slow checks and network probes").option("--preflight", "eager version/plugin heal with upfront notice when stale; silent when healthy (#1871)").option("--verbose", "print the evidence behind every check \u2014 probes, resolved paths, versions compared, and the names behind each count (#2977)").option("--ci", "stable CI output contract; no color, exit 0 for clear/advisory-only, 1 for hard gaps, 2 for doctor/tool failure").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (read-only inspection \u2014 performs no repairs)").option("--apply", "perform the same auto-repairs as the interactive run (combine with --json for a machine-readable repair run)").option("--no-repo-writes", "env/plugin repairs only \u2014 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, PATH, gh auth scopes, and hook wiring (offline-safe; suggests plugin-heal on a hard gap) (#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 2 doctor/tool execution failed before a normal verdict\n\n--banner keeps the legacy SessionStart contract and returns 0 unless the process crashes.\n").action(async (opts) => {
24379
24651
  if (opts.guide) {
24380
- consoleIo.log("MMI Agentic Onboarding: https://github.com/mutmutco/MMI-Hub/wiki/Agentic-Dev-Environment-Guide");
24652
+ consoleIo.log("MMI Agentic Onboarding: docs/Architecture/agentic-dev-environment.md");
24381
24653
  return;
24382
24654
  }
24383
24655
  const noRepoWrites = opts.repoWrites === false;
@@ -24400,33 +24672,79 @@ function directoryBytes(path2) {
24400
24672
  let total = 0;
24401
24673
  let entries;
24402
24674
  try {
24403
- entries = (0, import_node_fs28.readdirSync)(path2, { withFileTypes: true });
24675
+ entries = (0, import_node_fs27.readdirSync)(path2, { withFileTypes: true });
24404
24676
  } catch {
24405
24677
  return 0;
24406
24678
  }
24407
24679
  for (const entry of entries) {
24408
- const child = (0, import_node_path24.join)(path2, entry.name);
24680
+ const child = (0, import_node_path23.join)(path2, entry.name);
24409
24681
  if (entry.isDirectory()) total += directoryBytes(child);
24410
24682
  else {
24411
24683
  try {
24412
- total += (0, import_node_fs28.statSync)(child).size;
24684
+ total += (0, import_node_fs27.statSync)(child).size;
24413
24685
  } catch {
24414
24686
  }
24415
24687
  }
24416
24688
  }
24417
24689
  return total;
24418
24690
  }
24419
- program2.command("plugin-prune").description(`prune stale cached MMI plugin versions \u2014 keeps the running one + the newest (${PLUGIN_CACHE_KEEP} total); dry-run unless --apply (#2903)`).option("--apply", "actually delete the stale version dirs (default: report only)").option("--json", "machine-readable output").action((o) => {
24420
- const plan = buildPluginCachePlan((0, import_node_os9.homedir)(), runningPluginVersion(process.env, resolveClientVersion()), {
24421
- exists: (p) => (0, import_node_fs28.existsSync)(p),
24422
- listVersionDirs: (root) => (0, import_node_fs28.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
24423
- dirBytes: directoryBytes
24424
- }, { withBytes: true });
24425
- const result = o.apply && plan.prune.length > 0 ? applyPluginCachePlan(plan, (p) => (0, import_node_fs28.rmSync)(p, { recursive: true, force: true })) : void 0;
24691
+ function listDirEntries(dir) {
24692
+ return (0, import_node_fs27.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
24693
+ }
24694
+ function readInstalledPluginRefs(home) {
24695
+ const p = installedPluginsPath(home);
24696
+ if (!(0, import_node_fs27.existsSync)(p)) return [];
24697
+ try {
24698
+ return installedPluginPaths((0, import_node_fs27.readFileSync)(p, "utf8"));
24699
+ } catch {
24700
+ return null;
24701
+ }
24702
+ }
24703
+ function pluginCacheFsDeps(home, dirBytes) {
24704
+ return {
24705
+ exists: (p) => (0, import_node_fs27.existsSync)(p),
24706
+ listVersionDirs: (root) => (0, import_node_fs27.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
24707
+ dirBytes,
24708
+ listStagingDirs: (root) => (0, import_node_fs27.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
24709
+ try {
24710
+ return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path23.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs27.statSync)(p).mtimeMs) };
24711
+ } catch {
24712
+ return { name: d.name, mtimeMs: Date.now() };
24713
+ }
24714
+ }),
24715
+ readInstalledPluginPaths: () => readInstalledPluginRefs(home),
24716
+ now: () => Date.now()
24717
+ };
24718
+ }
24719
+ function stagingApplyFsGuard(home) {
24720
+ const stagingRoot = pluginCacheStagingRoot(home);
24721
+ return {
24722
+ referencedPaths: () => readInstalledPluginRefs(home),
24723
+ mtimeMs: (name) => {
24724
+ const p = (0, import_node_path23.join)(stagingRoot, name);
24725
+ if (!(0, import_node_fs27.existsSync)(p)) return null;
24726
+ try {
24727
+ return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs27.statSync)(q).mtimeMs);
24728
+ } catch {
24729
+ return null;
24730
+ }
24731
+ },
24732
+ now: () => Date.now()
24733
+ };
24734
+ }
24735
+ program2.command("plugin-prune").description(`prune stale cached MMI plugin versions (keeps running + newest, ${PLUGIN_CACHE_KEEP} total) and orphaned temp_git_* staging dirs; dry-run unless --apply (#2903, #2990)`).option("--apply", "actually delete the stale version dirs + orphaned staging dirs (default: report only)").option("--json", "machine-readable output").action((o) => {
24736
+ const plan = buildPluginCachePlan(
24737
+ (0, import_node_os8.homedir)(),
24738
+ runningPluginVersion(process.env, resolveClientVersion()),
24739
+ pluginCacheFsDeps((0, import_node_os8.homedir)(), directoryBytes),
24740
+ { withBytes: true }
24741
+ );
24742
+ const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
24743
+ const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs27.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard((0, import_node_os8.homedir)())) : void 0;
24426
24744
  const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
24427
24745
  if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
24428
24746
  else console.log(renderPluginCachePlan(plan, result));
24429
- if (result?.failed.length) process.exitCode = 1;
24747
+ if (result?.failed.length || result?.failedStaging.length) process.exitCode = 1;
24430
24748
  });
24431
24749
  program2.command("session-start").description("run the SessionStart verbs (whoami, board slice, doctor) in one process").action(async () => {
24432
24750
  if (isInsideRepoSubdir(process.cwd())) {
@@ -24455,7 +24773,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
24455
24773
  readBoard,
24456
24774
  // #1813: warm the slice cache out-of-band (detached) so the ~20s live read
24457
24775
  // never costs banner time and next session's glance renders instantly within budget.
24458
- scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn: import_node_child_process13.spawn, execPath: process.execPath, scriptPath: process.argv[1] })
24776
+ scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] })
24459
24777
  }),
24460
24778
  // command-ladder hint (#2609): compact gh→mmi-cli cheat sheet so agents reach for mmi-cli first,
24461
24779
  // not after a denied gh call. <1KB, pure in-memory — no network, no fs, fail-soft.
@@ -24486,12 +24804,18 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
24486
24804
  for (const line of scratchGcLines(process.cwd())) consoleIo.log(line);
24487
24805
  const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
24488
24806
  if (worktreeBanner) {
24489
- spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process13.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
24807
+ spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process12.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
24490
24808
  consoleIo.log(worktreeBanner);
24491
24809
  }
24492
24810
  appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "session-start hook completed" });
24493
24811
  });
24494
24812
  installProcessBackstop();
24813
+ program2.command("lane", { hidden: true }).action(() => {
24814
+ fail("lane orchestration lives in jerv-cli \u2014 try: jerv-cli lane --help");
24815
+ });
24816
+ program2.command("fusion", { hidden: true }).action(() => {
24817
+ fail("fusion orchestration lives in jerv-cli \u2014 try: jerv-cli fusion --help");
24818
+ });
24495
24819
  program2.parseAsync().then(() => finishCliRun()).catch((e) => failGraceful(e.message));
24496
24820
  // Annotate the CommonJS export names for ESM import in node:
24497
24821
  0 && (module.exports = {