@mutmutco/cli 3.76.0 → 3.77.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 +304 -151
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -3416,7 +3416,7 @@ var program = new Command();
3416
3416
 
3417
3417
  // src/index.ts
3418
3418
  var import_promises10 = require("node:fs/promises");
3419
- var import_node_fs33 = require("node:fs");
3419
+ var import_node_fs34 = require("node:fs");
3420
3420
  var import_node_child_process15 = require("node:child_process");
3421
3421
 
3422
3422
  // src/cli-shared.ts
@@ -6602,7 +6602,7 @@ function commandLadderHint() {
6602
6602
  }
6603
6603
 
6604
6604
  // src/index.ts
6605
- var import_node_path32 = require("node:path");
6605
+ var import_node_path33 = require("node:path");
6606
6606
 
6607
6607
  // src/merge-ci-policy.ts
6608
6608
  function resolveMergeCiPolicy(input) {
@@ -14866,7 +14866,8 @@ function trainPlan(command, options = {}) {
14866
14866
  { label: "fold the version bump into the release commit (Hub: full distribution set; app repos: root package manifest) \u2014 runs inside the apply step, no separate bump PR", gated: true },
14867
14867
  { label: "tag release and publish GitHub Release", gated: true },
14868
14868
  { label: "trigger the repo deploy path from the release event", command: "hub-serverless: deploy.yml + publish.yml auto-fire on the release; registry-publish: own publish.yml auto-fires, watched on that repo, never a central dispatch (#2428); other models deploy via their own workflow", gated: true },
14869
- { label: "roll development forward", gated: true }
14869
+ { label: "roll development forward", gated: true },
14870
+ { label: "synchronize the owning Project short description + thin README from the repo docs and current member repos", command: "mmi-cli org project sync-info --apply", gated: true }
14870
14871
  ];
14871
14872
  }
14872
14873
  if (options.dev) {
@@ -14881,7 +14882,8 @@ function trainPlan(command, options = {}) {
14881
14882
  { label: "tag release and publish GitHub Release", gated: true },
14882
14883
  { label: "trigger the deploy path for this repo model, returning Hub Actions run id/url data (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch", gated: true },
14883
14884
  { label: "retire the rc runtime (rc is ephemeral \u2014 non-fatal, reported as rcRetirement)", command: "mmi-cli runtime tenant control <owner/repo> rc retire", gated: true },
14884
- { label: "roll development forward and align rc to the released main", gated: true }
14885
+ { label: "roll development forward and align rc to the released main", gated: true },
14886
+ { label: "synchronize the owning Project short description + thin README from the repo docs and current member repos", command: "mmi-cli org project sync-info --apply", gated: true }
14885
14887
  ];
14886
14888
  }
14887
14889
  return [
@@ -14894,7 +14896,8 @@ function trainPlan(command, options = {}) {
14894
14896
  { label: "fold the version bump into the release commit (app repos: root package manifest) \u2014 runs inside the apply step, no separate bump PR", gated: true },
14895
14897
  { label: "tag release and publish GitHub Release", gated: true },
14896
14898
  { label: "trigger the deploy path for this repo model, returning Hub Actions run id/url data (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch; hub-serverless: no manual dispatch, deploy.yml + publish.yml auto-fire on the release, correlate/watch those runs; registry-publish: no manual dispatch, own publish.yml auto-fires on the release, correlate/watch that run on the product repo (#2428)", gated: true },
14897
- { label: "roll development forward", gated: true }
14899
+ { label: "roll development forward", gated: true },
14900
+ { label: "synchronize the owning Project short description + thin README from the repo docs and current member repos", command: "mmi-cli org project sync-info --apply", gated: true }
14898
14901
  ];
14899
14902
  }
14900
14903
  return [
@@ -18623,6 +18626,101 @@ function docsAuditStatus(fetch2, opts) {
18623
18626
  return { ok: true, state: "clean", line: `docs audit: ${opts.repo} ${verdict.outcome} (${verdict.date}, ${verdict.checkerVendor})` };
18624
18627
  }
18625
18628
 
18629
+ // src/project-info-sync.ts
18630
+ var import_node_fs22 = require("node:fs");
18631
+ var import_node_path20 = require("node:path");
18632
+ var UPDATE_PROJECT_INFO = `mutation($projectId: ID!, $shortDescription: String!, $readme: String!) {
18633
+ updateProjectV2(input: { projectId: $projectId, shortDescription: $shortDescription, readme: $readme }) {
18634
+ projectV2 { id }
18635
+ }
18636
+ }`;
18637
+ function shortDescriptionFromReadme(markdown) {
18638
+ const lines = markdown.replace(/\r/g, "").split("\n");
18639
+ const h1 = lines.findIndex((line) => /^#\s+\S/.test(line.trim()));
18640
+ const paragraph = [];
18641
+ for (const raw of lines.slice(h1 >= 0 ? h1 + 1 : 0)) {
18642
+ const line = raw.trim();
18643
+ if (!line) {
18644
+ if (paragraph.length) break;
18645
+ continue;
18646
+ }
18647
+ if (/^(?:#|<!--|!\[|\[!\[|<img\b)/i.test(line)) {
18648
+ if (paragraph.length) break;
18649
+ continue;
18650
+ }
18651
+ paragraph.push(line);
18652
+ }
18653
+ const text = paragraph.join(" ").replace(/!\[[^\]]*]\([^)]*\)/g, "").replace(/\[([^\]]+)]\([^)]*\)/g, "$1").replace(/[*_`~]|<[^>]+>/g, "").replace(/\s+/g, " ").trim();
18654
+ if (!text) throw new Error("org project sync-info: README.md has no reader-facing description below its H1");
18655
+ const sentence = text.match(/^.*?[.!?](?=\s|$)/)?.[0] ?? text;
18656
+ return sentence.length <= 240 ? sentence : `${sentence.slice(0, 237).trimEnd()}...`;
18657
+ }
18658
+ function entriesFor(project2, projects) {
18659
+ return project2.projectId ? projects.filter((entry) => entry.projectId === project2.projectId) : [project2];
18660
+ }
18661
+ function branchFor(repo, projects) {
18662
+ const entry = projects.find((p) => (p.repos ?? []).some((r) => r.toLowerCase() === repo.toLowerCase()));
18663
+ return typeof entry?.branch === "string" && entry.branch.trim() ? entry.branch.trim() : entry?.releaseTrack === "trunk" || entry?.class === "content" ? "main" : "development";
18664
+ }
18665
+ function sharedName(entries, fallback) {
18666
+ const names = entries.map((entry) => entry.name?.trim()).filter((name) => Boolean(name));
18667
+ if (names.length <= 1) return names[0] ?? fallback;
18668
+ let prefix = names[0];
18669
+ for (const name of names.slice(1)) {
18670
+ while (prefix && !name.toLowerCase().startsWith(prefix.toLowerCase())) prefix = prefix.slice(0, -1);
18671
+ }
18672
+ return prefix.replace(/[-_\s]+$/, "") || fallback;
18673
+ }
18674
+ function buildProjectInfoSyncPlan(targetRepo2, project2, projects, repoRoot2) {
18675
+ if (!project2.projectId) throw new Error(`org project sync-info: ${targetRepo2} registry META has no projectId`);
18676
+ const readmePath = (0, import_node_path20.join)(repoRoot2, "README.md");
18677
+ if (!(0, import_node_fs22.existsSync)(readmePath)) throw new Error(`org project sync-info: ${targetRepo2} has no README.md`);
18678
+ const entries = entriesFor(project2, projects);
18679
+ const memberRepos = [...new Set(entries.flatMap((entry) => entry.repos ?? []))].filter((repo) => /^[^/]+\/[^/]+$/.test(repo)).sort((a, b) => a.localeCompare(b));
18680
+ const projectName = sharedName(entries, project2.name?.trim() || targetRepo2.split("/").pop() || targetRepo2);
18681
+ if (!memberRepos.length) throw new Error(`org project sync-info: project ${projectName} has no registered member repos`);
18682
+ const entryNames = entries.map((entry) => entry.name?.trim()).filter((name) => Boolean(name));
18683
+ const shortDescription = memberRepos.length === 1 ? shortDescriptionFromReadme((0, import_node_fs22.readFileSync)(readmePath, "utf8")) : `Shared work across ${new Intl.ListFormat("en", { type: "conjunction" }).format(entryNames)}.`;
18684
+ const lines = [
18685
+ `# ${projectName}`,
18686
+ "",
18687
+ shortDescription,
18688
+ "",
18689
+ "## Member repos",
18690
+ "",
18691
+ ...memberRepos.map((repo) => {
18692
+ const entry = projects.find((p) => (p.repos ?? []).some((r) => r.toLowerCase() === repo.toLowerCase()));
18693
+ const name = entry?.name?.trim() || repo.split("/")[1];
18694
+ const base = `https://github.com/${repo}`;
18695
+ const branch = branchFor(repo, projects);
18696
+ return `- [${name}](${base}) \u2014 [README](${base}/blob/${branch}/README.md) \xB7 [architecture](${base}/blob/${branch}/architecture.md)`;
18697
+ })
18698
+ ];
18699
+ const targetBase = `https://github.com/${targetRepo2}`;
18700
+ const targetBranch = branchFor(targetRepo2, projects);
18701
+ const orgDocs = [
18702
+ (0, import_node_fs22.existsSync)((0, import_node_path20.join)(repoRoot2, "docs", "org-readme.md")) ? `- [Org identity](${targetBase}/blob/${targetBranch}/docs/org-readme.md)` : "",
18703
+ (0, import_node_fs22.existsSync)((0, import_node_path20.join)(repoRoot2, "docs", "org-architecture.md")) ? `- [Org architecture](${targetBase}/blob/${targetBranch}/docs/org-architecture.md)` : ""
18704
+ ].filter(Boolean);
18705
+ if (orgDocs.length) lines.push("", "## Organisation docs", "", ...orgDocs);
18706
+ return { projectId: project2.projectId, projectName, targetRepo: targetRepo2, memberRepos, shortDescription, readme: `${lines.join("\n")}
18707
+ ` };
18708
+ }
18709
+ async function syncProjectInfo(plan, client, apply) {
18710
+ if (apply) {
18711
+ await client.graphql(UPDATE_PROJECT_INFO, {
18712
+ projectId: plan.projectId,
18713
+ shortDescription: plan.shortDescription,
18714
+ readme: plan.readme
18715
+ });
18716
+ }
18717
+ return {
18718
+ ...plan,
18719
+ applied: apply,
18720
+ note: apply ? `Project ${plan.projectName} information synchronized` : `Project ${plan.projectName} information would be synchronized (dry-run; pass --apply)`
18721
+ };
18722
+ }
18723
+
18626
18724
  // src/oauth.ts
18627
18725
  var DEFAULT_DOMAINS = ["mutatismutandis.co", "mutmut.co"];
18628
18726
  var DEFAULT_CALLBACK_PATH = "/api/auth/callback";
@@ -19461,8 +19559,8 @@ function writeError(res) {
19461
19559
  }
19462
19560
 
19463
19561
  // src/secrets-commands.ts
19464
- var import_node_fs22 = require("node:fs");
19465
- var import_node_path20 = require("node:path");
19562
+ var import_node_fs23 = require("node:fs");
19563
+ var import_node_path21 = require("node:path");
19466
19564
  var import_node_os8 = require("node:os");
19467
19565
 
19468
19566
  // src/project-runtime.ts
@@ -19586,18 +19684,18 @@ function collectMap(value, previous = []) {
19586
19684
  return [...previous, value];
19587
19685
  }
19588
19686
  async function decryptRailsCredentials(input) {
19589
- const appDir = (0, import_node_path20.resolve)(input.appDir ?? process.cwd());
19687
+ const appDir = (0, import_node_path21.resolve)(input.appDir ?? process.cwd());
19590
19688
  const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
19591
19689
  const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
19592
- const credentialsPath = (0, import_node_path20.resolve)(appDir, credentialsFile);
19593
- const masterKeyPath = (0, import_node_path20.resolve)(appDir, masterKeyFile);
19690
+ const credentialsPath = (0, import_node_path21.resolve)(appDir, credentialsFile);
19691
+ const masterKeyPath = (0, import_node_path21.resolve)(appDir, masterKeyFile);
19594
19692
  const env = {
19595
19693
  ...process.env,
19596
19694
  MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
19597
19695
  MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
19598
19696
  };
19599
- if ((0, import_node_fs22.existsSync)(masterKeyPath)) {
19600
- env.RAILS_MASTER_KEY = (0, import_node_fs22.readFileSync)(masterKeyPath, "utf8").trim();
19697
+ if ((0, import_node_fs23.existsSync)(masterKeyPath)) {
19698
+ env.RAILS_MASTER_KEY = (0, import_node_fs23.readFileSync)(masterKeyPath, "utf8").trim();
19601
19699
  }
19602
19700
  const script = [
19603
19701
  'require "json"',
@@ -19607,9 +19705,9 @@ async function decryptRailsCredentials(input) {
19607
19705
  'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
19608
19706
  "puts JSON.generate(config.config)"
19609
19707
  ].join("\n");
19610
- const scriptDir = (0, import_node_fs22.mkdtempSync)((0, import_node_path20.join)((0, import_node_os8.tmpdir)(), "mmi-rails-decrypt-"));
19611
- const scriptPath = (0, import_node_path20.join)(scriptDir, "decrypt.rb");
19612
- (0, import_node_fs22.writeFileSync)(scriptPath, script, "utf8");
19708
+ const scriptDir = (0, import_node_fs23.mkdtempSync)((0, import_node_path21.join)((0, import_node_os8.tmpdir)(), "mmi-rails-decrypt-"));
19709
+ const scriptPath = (0, import_node_path21.join)(scriptDir, "decrypt.rb");
19710
+ (0, import_node_fs23.writeFileSync)(scriptPath, script, "utf8");
19613
19711
  try {
19614
19712
  const args = ["exec", "ruby", scriptPath];
19615
19713
  const cmd = process.platform === "win32" ? "cmd.exe" : "bundle";
@@ -19621,7 +19719,7 @@ async function decryptRailsCredentials(input) {
19621
19719
  });
19622
19720
  return JSON.parse(stdout);
19623
19721
  } finally {
19624
- (0, import_node_fs22.rmSync)(scriptDir, { recursive: true, force: true });
19722
+ (0, import_node_fs23.rmSync)(scriptDir, { recursive: true, force: true });
19625
19723
  }
19626
19724
  }
19627
19725
  async function readSecretStdin() {
@@ -19711,7 +19809,7 @@ function registerSecretsCommands(program3) {
19711
19809
  let body;
19712
19810
  if (o.file) {
19713
19811
  try {
19714
- body = (0, import_node_fs22.readFileSync)((0, import_node_path20.resolve)(o.file), "utf8");
19812
+ body = (0, import_node_fs23.readFileSync)((0, import_node_path21.resolve)(o.file), "utf8");
19715
19813
  } catch (e) {
19716
19814
  return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
19717
19815
  }
@@ -19816,7 +19914,7 @@ function registerSecretsCommands(program3) {
19816
19914
  {
19817
19915
  ...d,
19818
19916
  decryptRailsCredentials,
19819
- removeFile: (path2) => (0, import_node_fs22.unlinkSync)((0, import_node_path20.resolve)(o.appDir ?? process.cwd(), path2))
19917
+ removeFile: (path2) => (0, import_node_fs23.unlinkSync)((0, import_node_path21.resolve)(o.appDir ?? process.cwd(), path2))
19820
19918
  },
19821
19919
  {
19822
19920
  repo: o.repo,
@@ -20010,7 +20108,7 @@ function checkGithubPools(probe) {
20010
20108
  }
20011
20109
 
20012
20110
  // src/box-commands.ts
20013
- var import_node_fs23 = require("node:fs");
20111
+ var import_node_fs24 = require("node:fs");
20014
20112
 
20015
20113
  // src/box.ts
20016
20114
  var BOX_KEYS = {
@@ -20213,7 +20311,7 @@ function registerBoxCommands(program3) {
20213
20311
  }
20214
20312
  if (o.json) console.log(JSON.stringify({ box: found, incomplete }, null, 2));
20215
20313
  else if (o.ssh && o.script) {
20216
- (0, import_node_fs23.writeFileSync)(o.script, sshRecipeScript(found), "utf8");
20314
+ (0, import_node_fs24.writeFileSync)(o.script, sshRecipeScript(found), "utf8");
20217
20315
  console.log(`wrote ${o.script} \u2014 run: bash "${o.script}"`);
20218
20316
  } else if (o.ssh) console.log(`${formatSshRecipe(found)}
20219
20317
  ${SSH_RECIPE_AGENT_NOTE}`);
@@ -20905,7 +21003,7 @@ function registerSchedulesCommands(program3) {
20905
21003
 
20906
21004
  // src/file-lock.ts
20907
21005
  var import_promises4 = require("node:fs/promises");
20908
- var import_node_path21 = require("node:path");
21006
+ var import_node_path22 = require("node:path");
20909
21007
  var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
20910
21008
  var IMMEDIATE_RETRY_BUDGET = 3;
20911
21009
  var FileLockBusyError = class extends Error {
@@ -20990,7 +21088,7 @@ async function releaseFileLock(lockPath, guard) {
20990
21088
  }
20991
21089
  async function withFileLock(lockPath, opts, fn) {
20992
21090
  const resolved = resolveFileLockOpts(opts);
20993
- await (0, import_promises4.mkdir)((0, import_node_path21.dirname)(lockPath), { recursive: true }).catch(() => void 0);
21091
+ await (0, import_promises4.mkdir)((0, import_node_path22.dirname)(lockPath), { recursive: true }).catch(() => void 0);
20994
21092
  const guard = await acquireFileLock(lockPath, resolved, Date.now() + resolved.maxWaitMs);
20995
21093
  try {
20996
21094
  return await fn();
@@ -21001,7 +21099,7 @@ async function withFileLock(lockPath, opts, fn) {
21001
21099
 
21002
21100
  // src/schedules-lift-command.ts
21003
21101
  var import_promises5 = require("node:fs/promises");
21004
- var import_node_path22 = require("node:path");
21102
+ var import_node_path23 = require("node:path");
21005
21103
 
21006
21104
  // src/schedules-lift.ts
21007
21105
  var SCHEDULE_HEADER_FIELDS = ["schedule", "what", "owner", "cadence", "executor", "llm", "output", "breaks", "kill"];
@@ -21107,7 +21205,7 @@ async function readWorkflowFiles(dir) {
21107
21205
  const files = [];
21108
21206
  for (const name of names.sort()) {
21109
21207
  if (!/\.ya?ml$/.test(name)) continue;
21110
- files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises5.readFile)((0, import_node_path22.join)(dir, name), "utf8") });
21208
+ files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises5.readFile)((0, import_node_path23.join)(dir, name), "utf8") });
21111
21209
  }
21112
21210
  return files;
21113
21211
  }
@@ -21649,9 +21747,9 @@ function registerQueryCommands(program3) {
21649
21747
  }
21650
21748
 
21651
21749
  // src/bootstrap-commands.ts
21652
- var import_node_fs24 = require("node:fs");
21750
+ var import_node_fs25 = require("node:fs");
21653
21751
  var import_node_os9 = require("node:os");
21654
- var import_node_path23 = require("node:path");
21752
+ var import_node_path24 = require("node:path");
21655
21753
 
21656
21754
  // src/bootstrap-drift.ts
21657
21755
  function byteComparableSeeds(manifest, cls) {
@@ -22265,13 +22363,13 @@ function registerBootstrapCommands(program3) {
22265
22363
  client: defaultGitHubClient(),
22266
22364
  projectMeta: meta,
22267
22365
  deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
22268
- readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs24.existsSync)(path2) ? (0, import_node_fs24.readFileSync)(path2, "utf8") : null,
22366
+ readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs25.existsSync)(path2) ? (0, import_node_fs25.readFileSync)(path2, "utf8") : null,
22269
22367
  // requiredGcpApis is stored as an array by a JSON write, but `org project set --var KEY=VALUE` stores a raw
22270
22368
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
22271
22369
  // #3689: the same committed map the org access audit reads (#3664), so a sanctioned admin is not a
22272
22370
  // permanent bootstrap failure on one surface and an intended state on the other. Absent file → no
22273
22371
  // sanction, which is the pre-#3664 behaviour.
22274
- sanctionedAdmins: (0, import_node_fs24.existsSync)("access-matrix.json") ? entriesValueByCanonicalRepo(loadSanctionedAdmins((0, import_node_fs24.readFileSync)("access-matrix.json", "utf8")), repo) : void 0,
22372
+ sanctionedAdmins: (0, import_node_fs25.existsSync)("access-matrix.json") ? entriesValueByCanonicalRepo(loadSanctionedAdmins((0, import_node_fs25.readFileSync)("access-matrix.json", "utf8")), repo) : void 0,
22275
22373
  requiredGcpApis: (() => {
22276
22374
  const v = meta?.requiredGcpApis;
22277
22375
  if (Array.isArray(v)) return v;
@@ -22309,12 +22407,12 @@ function registerBootstrapCommands(program3) {
22309
22407
  bootstrap.command("drift").description("#3818: compare every org-owned whole-file seed against MMI-Hub's copy across the registry roster; read-only").option("--repo <owner/repo>", "audit one repo instead of the roster (never a fleet verdict)").option("--json", "machine-readable output").action(async () => {
22310
22408
  const o = { repo: rawValue("--repo", ""), json: rawFlag("--json") };
22311
22409
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
22312
- if (!(0, import_node_fs24.existsSync)(manifestPath)) return fail(`bootstrap drift: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the reference this compares against`);
22313
- const manifest = loadBootstrapSeeds((0, import_node_fs24.readFileSync)(manifestPath, "utf8"));
22410
+ if (!(0, import_node_fs25.existsSync)(manifestPath)) return fail(`bootstrap drift: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the reference this compares against`);
22411
+ const manifest = loadBootstrapSeeds((0, import_node_fs25.readFileSync)(manifestPath, "utf8"));
22314
22412
  const hubContents = /* @__PURE__ */ new Map();
22315
22413
  for (const s of manifest.seeds) {
22316
22414
  if (s.ownership !== "org" || s.source !== "self") continue;
22317
- hubContents.set(s.target, (0, import_node_fs24.existsSync)(s.target) ? (0, import_node_fs24.readFileSync)(s.target, "utf8") : null);
22415
+ hubContents.set(s.target, (0, import_node_fs25.existsSync)(s.target) ? (0, import_node_fs25.readFileSync)(s.target, "utf8") : null);
22318
22416
  }
22319
22417
  let targets;
22320
22418
  let classOf = (_repo) => "deployable";
@@ -22391,8 +22489,8 @@ function registerBootstrapCommands(program3) {
22391
22489
  return fail(`bootstrap apply: ${e.message}`);
22392
22490
  }
22393
22491
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
22394
- if (!(0, import_node_fs24.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`);
22395
- const manifest = loadBootstrapSeeds((0, import_node_fs24.readFileSync)(manifestPath, "utf8"));
22492
+ if (!(0, import_node_fs25.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`);
22493
+ const manifest = loadBootstrapSeeds((0, import_node_fs25.readFileSync)(manifestPath, "utf8"));
22396
22494
  const baseBranch = o.class === "content" ? "main" : "development";
22397
22495
  const slug = parsedRepo.slug;
22398
22496
  const onlyTarget = o.only.trim();
@@ -22403,16 +22501,16 @@ function registerBootstrapCommands(program3) {
22403
22501
  ${known}`);
22404
22502
  }
22405
22503
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
22406
- const readFile9 = (p) => (0, import_node_fs24.existsSync)(p) ? (0, import_node_fs24.readFileSync)(p, "utf8") : null;
22504
+ const readFile9 = (p) => (0, import_node_fs25.existsSync)(p) ? (0, import_node_fs25.readFileSync)(p, "utf8") : null;
22407
22505
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
22408
22506
  const putSeed = async (target, content, ref, sha) => {
22409
- const tmp = (0, import_node_path23.join)((0, import_node_os9.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
22410
- (0, import_node_fs24.writeFileSync)(tmp, JSON.stringify(contentPutBody(target, content, ref, sha)), "utf8");
22507
+ const tmp = (0, import_node_path24.join)((0, import_node_os9.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
22508
+ (0, import_node_fs25.writeFileSync)(tmp, JSON.stringify(contentPutBody(target, content, ref, sha)), "utf8");
22411
22509
  try {
22412
22510
  await gh(contentPutInputArgs(repo, target, tmp));
22413
22511
  } finally {
22414
22512
  try {
22415
- (0, import_node_fs24.unlinkSync)(tmp);
22513
+ (0, import_node_fs25.unlinkSync)(tmp);
22416
22514
  } catch {
22417
22515
  }
22418
22516
  }
@@ -22664,12 +22762,12 @@ LIVE apply to ${repo}:
22664
22762
  }
22665
22763
 
22666
22764
  // src/stage-commands.ts
22667
- var import_node_fs26 = require("node:fs");
22668
- var import_node_path25 = require("node:path");
22765
+ var import_node_fs27 = require("node:fs");
22766
+ var import_node_path26 = require("node:path");
22669
22767
 
22670
22768
  // src/port-registry.ts
22671
- var import_node_fs25 = require("node:fs");
22672
- var import_node_path24 = require("node:path");
22769
+ var import_node_fs26 = require("node:fs");
22770
+ var import_node_path25 = require("node:path");
22673
22771
 
22674
22772
  // ../infra/port-geometry.mjs
22675
22773
  var PORT_BLOCK = 100;
@@ -22683,8 +22781,8 @@ function nextPortBlock(registry2) {
22683
22781
  return [base, base + PORT_SPAN];
22684
22782
  }
22685
22783
  function loadPortRegistry(path2) {
22686
- if (!(0, import_node_fs25.existsSync)(path2)) return {};
22687
- const raw = JSON.parse((0, import_node_fs25.readFileSync)(path2, "utf8"));
22784
+ if (!(0, import_node_fs26.existsSync)(path2)) return {};
22785
+ const raw = JSON.parse((0, import_node_fs26.readFileSync)(path2, "utf8"));
22688
22786
  const out = {};
22689
22787
  for (const [key, value] of Object.entries(raw)) {
22690
22788
  if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
@@ -22698,9 +22796,9 @@ function ensurePortRange(repo, path2) {
22698
22796
  const existing = registry2[repo];
22699
22797
  if (existing) return existing;
22700
22798
  const range = nextPortBlock(registry2);
22701
- const raw = (0, import_node_fs25.existsSync)(path2) ? JSON.parse((0, import_node_fs25.readFileSync)(path2, "utf8")) : {};
22799
+ const raw = (0, import_node_fs26.existsSync)(path2) ? JSON.parse((0, import_node_fs26.readFileSync)(path2, "utf8")) : {};
22702
22800
  raw[repo] = range;
22703
- (0, import_node_fs25.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
22801
+ (0, import_node_fs26.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
22704
22802
  return range;
22705
22803
  }
22706
22804
  function portCursorSeed(registry2) {
@@ -22722,22 +22820,22 @@ function existingPortRange(repo, registry2) {
22722
22820
  return registry2[repo] ?? null;
22723
22821
  }
22724
22822
  function portRangeInfraAt(root, source) {
22725
- const registryPath = (0, import_node_path24.join)(root, "infra", "port-ranges.json");
22726
- const ddbScriptPath = (0, import_node_path24.join)(root, "infra", "port-ddb.mjs");
22727
- if (!(0, import_node_fs25.existsSync)(registryPath) || !(0, import_node_fs25.existsSync)(ddbScriptPath)) return null;
22823
+ const registryPath = (0, import_node_path25.join)(root, "infra", "port-ranges.json");
22824
+ const ddbScriptPath = (0, import_node_path25.join)(root, "infra", "port-ddb.mjs");
22825
+ if (!(0, import_node_fs26.existsSync)(registryPath) || !(0, import_node_fs26.existsSync)(ddbScriptPath)) return null;
22728
22826
  return { root, source, registryPath, ddbScriptPath };
22729
22827
  }
22730
22828
  function resolvePortRangeInfra(cwd, packageDir) {
22731
22829
  const direct = portRangeInfraAt(cwd, "cwd");
22732
22830
  if (direct) return direct;
22733
- for (let dir = cwd; ; dir = (0, import_node_path24.dirname)(dir)) {
22734
- const sibling = portRangeInfraAt((0, import_node_path24.join)(dir, "MMI-Hub"), "sibling-hub");
22831
+ for (let dir = cwd; ; dir = (0, import_node_path25.dirname)(dir)) {
22832
+ const sibling = portRangeInfraAt((0, import_node_path25.join)(dir, "MMI-Hub"), "sibling-hub");
22735
22833
  if (sibling) return sibling;
22736
- const parent = (0, import_node_path24.dirname)(dir);
22834
+ const parent = (0, import_node_path25.dirname)(dir);
22737
22835
  if (parent === dir) break;
22738
22836
  }
22739
22837
  if (packageDir) {
22740
- const pkgRoot = (0, import_node_path24.join)(packageDir, "..", "..");
22838
+ const pkgRoot = (0, import_node_path25.join)(packageDir, "..", "..");
22741
22839
  const pkgFrom = portRangeInfraAt(pkgRoot, "pkg-root");
22742
22840
  if (pkgFrom) return pkgFrom;
22743
22841
  }
@@ -22913,8 +23011,8 @@ function registerStageCommands(program3) {
22913
23011
  const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
22914
23012
  return decideStage({
22915
23013
  registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
22916
- hasCompose: (0, import_node_fs26.existsSync)((0, import_node_path25.join)(process.cwd(), "docker-compose.yml")),
22917
- hasEnvExample: (0, import_node_fs26.existsSync)((0, import_node_path25.join)(process.cwd(), ".env.example"))
23014
+ hasCompose: (0, import_node_fs27.existsSync)((0, import_node_path26.join)(process.cwd(), "docker-compose.yml")),
23015
+ hasEnvExample: (0, import_node_fs27.existsSync)((0, import_node_path26.join)(process.cwd(), ".env.example"))
22918
23016
  });
22919
23017
  }
22920
23018
  async function fetchStageVaultEnvMerge() {
@@ -23352,9 +23450,9 @@ function registerBoardCommands(program3) {
23352
23450
  }
23353
23451
 
23354
23452
  // src/merge-cleanup.ts
23355
- var import_node_fs27 = require("node:fs");
23453
+ var import_node_fs28 = require("node:fs");
23356
23454
  var import_promises7 = require("node:fs/promises");
23357
- var import_node_path27 = require("node:path");
23455
+ var import_node_path28 = require("node:path");
23358
23456
  var import_node_os10 = require("node:os");
23359
23457
  var import_node_child_process13 = require("node:child_process");
23360
23458
 
@@ -23442,7 +23540,7 @@ function boardAdvanceFailureMessage(result) {
23442
23540
 
23443
23541
  // src/deferred-registry-store.ts
23444
23542
  var import_promises6 = require("node:fs/promises");
23445
- var import_node_path26 = require("node:path");
23543
+ var import_node_path27 = require("node:path");
23446
23544
  var sleep2 = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
23447
23545
  async function atomicWrite(target, contents) {
23448
23546
  const tmp = `${target}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
@@ -23493,12 +23591,12 @@ function makeDeferredWorktreeStore(registryPath, lockOpts = {}) {
23493
23591
  },
23494
23592
  // Standalone atomic write — THROWS on failure (no best-effort swallow, #2846).
23495
23593
  write: async (entries) => {
23496
- await (0, import_promises6.mkdir)((0, import_node_path26.dirname)(registryPath), { recursive: true });
23594
+ await (0, import_promises6.mkdir)((0, import_node_path27.dirname)(registryPath), { recursive: true });
23497
23595
  await atomicWrite(registryPath, serializeDeferredWorktrees(entries));
23498
23596
  },
23499
23597
  // Serialized read-modify-write under the repo-wide lock (#2846).
23500
23598
  update: async (mutate) => {
23501
- await (0, import_promises6.mkdir)((0, import_node_path26.dirname)(registryPath), { recursive: true });
23599
+ await (0, import_promises6.mkdir)((0, import_node_path27.dirname)(registryPath), { recursive: true });
23502
23600
  const deadline = Date.now() + opts.maxWaitMs;
23503
23601
  for (; ; ) {
23504
23602
  const guard = await acquireLock(lockPath, opts, deadline);
@@ -23659,7 +23757,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
23659
23757
  );
23660
23758
  const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
23661
23759
  const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
23662
- const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path27.dirname)((0, import_node_path27.dirname)(worktreeGitRoot)) : repoRoot2;
23760
+ const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path28.dirname)((0, import_node_path28.dirname)(worktreeGitRoot)) : repoRoot2;
23663
23761
  const gcActor = describeActor({ env: process.env, surface: detectSurface(process.env), cwd: process.cwd() });
23664
23762
  const owners = readWorktreeOwners(primaryRepoRoot);
23665
23763
  const removalNow = Date.now();
@@ -23690,7 +23788,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
23690
23788
  const cleanup = await cleanupPrMergeLocalBranch(branch.branch, {
23691
23789
  beforeWorktrees,
23692
23790
  startingPath: branch.worktreePath,
23693
- pathExists: (p) => (0, import_node_fs27.existsSync)(p),
23791
+ pathExists: (p) => (0, import_node_fs28.existsSync)(p),
23694
23792
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
23695
23793
  teardownWorktreeStage,
23696
23794
  deferredStore,
@@ -23718,7 +23816,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
23718
23816
  for (const wt of worktreeDirsToRemove) {
23719
23817
  try {
23720
23818
  const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
23721
- realpath: (path2) => (0, import_node_fs27.realpathSync)(path2)
23819
+ realpath: (path2) => (0, import_node_fs28.realpathSync)(path2)
23722
23820
  });
23723
23821
  if (!cleanupTarget.ok) {
23724
23822
  result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
@@ -23783,13 +23881,13 @@ async function composeOverrideBodyFile(prNumber, repoArgs, gh) {
23783
23881
  const commits = JSON.parse(raw).commits ?? [];
23784
23882
  const body = squashBodyWithOverride(commits.map((c) => ({ headline: c.messageHeadline ?? "", body: c.messageBody ?? "" })), process.cwd());
23785
23883
  if (!body) return void 0;
23786
- const dir = (0, import_node_fs27.mkdtempSync)((0, import_node_path27.join)((0, import_node_os10.tmpdir)(), "mmi-squash-body-"));
23787
- const path2 = (0, import_node_path27.join)(dir, "body.txt");
23788
- (0, import_node_fs27.writeFileSync)(path2, `${body}
23884
+ const dir = (0, import_node_fs28.mkdtempSync)((0, import_node_path28.join)((0, import_node_os10.tmpdir)(), "mmi-squash-body-"));
23885
+ const path2 = (0, import_node_path28.join)(dir, "body.txt");
23886
+ (0, import_node_fs28.writeFileSync)(path2, `${body}
23789
23887
  `, "utf8");
23790
23888
  return { path: path2, cleanup: () => {
23791
23889
  try {
23792
- (0, import_node_fs27.rmSync)(dir, { recursive: true, force: true });
23890
+ (0, import_node_fs28.rmSync)(dir, { recursive: true, force: true });
23793
23891
  } catch {
23794
23892
  }
23795
23893
  } };
@@ -23911,13 +24009,13 @@ var realWorktreeDirRemover = {
23911
24009
  probe: (p) => {
23912
24010
  let st;
23913
24011
  try {
23914
- st = (0, import_node_fs27.lstatSync)(p);
24012
+ st = (0, import_node_fs28.lstatSync)(p);
23915
24013
  } catch {
23916
24014
  return null;
23917
24015
  }
23918
24016
  if (st.isSymbolicLink()) return "link";
23919
24017
  try {
23920
- (0, import_node_fs27.readlinkSync)(p);
24018
+ (0, import_node_fs28.readlinkSync)(p);
23921
24019
  return "link";
23922
24020
  } catch {
23923
24021
  }
@@ -23925,7 +24023,7 @@ var realWorktreeDirRemover = {
23925
24023
  },
23926
24024
  readdir: (p) => {
23927
24025
  try {
23928
- return (0, import_node_fs27.readdirSync)(p);
24026
+ return (0, import_node_fs28.readdirSync)(p);
23929
24027
  } catch {
23930
24028
  return [];
23931
24029
  }
@@ -23934,9 +24032,9 @@ var realWorktreeDirRemover = {
23934
24032
  // leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
23935
24033
  detachLink: (p) => {
23936
24034
  try {
23937
- (0, import_node_fs27.rmdirSync)(p);
24035
+ (0, import_node_fs28.rmdirSync)(p);
23938
24036
  } catch {
23939
- (0, import_node_fs27.unlinkSync)(p);
24037
+ (0, import_node_fs28.unlinkSync)(p);
23940
24038
  }
23941
24039
  },
23942
24040
  removeTree: (p) => (0, import_promises7.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
@@ -23969,9 +24067,9 @@ async function worktreeHasStageState(worktreePath) {
23969
24067
  }
23970
24068
  }
23971
24069
  function stageStateFileBelongsToWorktree(statePath, worktreePath) {
23972
- if (!(0, import_node_fs27.existsSync)(statePath)) return false;
24070
+ if (!(0, import_node_fs28.existsSync)(statePath)) return false;
23973
24071
  try {
23974
- const state = JSON.parse((0, import_node_fs27.readFileSync)(statePath, "utf8"));
24072
+ const state = JSON.parse((0, import_node_fs28.readFileSync)(statePath, "utf8"));
23975
24073
  const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
23976
24074
  return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
23977
24075
  } catch {
@@ -24178,9 +24276,9 @@ async function fetchRestCorePool(gh = defaultGhApi) {
24178
24276
  }
24179
24277
 
24180
24278
  // src/worktree-lifecycle-commands.ts
24181
- var import_node_fs28 = require("node:fs");
24279
+ var import_node_fs29 = require("node:fs");
24182
24280
  var import_promises8 = require("node:fs/promises");
24183
- var import_node_path28 = require("node:path");
24281
+ var import_node_path29 = require("node:path");
24184
24282
  var GH_TIMEOUT_MS = 2e4;
24185
24283
  var DEFAULT_BASE = "origin/development";
24186
24284
  var DEFAULT_REMOTE = "origin";
@@ -24316,7 +24414,7 @@ function classifyStaleLeaks(input) {
24316
24414
  var defaultOrphanDirScanDeps = {
24317
24415
  listDirs: (root) => {
24318
24416
  try {
24319
- return (0, import_node_fs28.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path28.join)(root, e.name));
24417
+ return (0, import_node_fs29.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path29.join)(root, e.name));
24320
24418
  } catch {
24321
24419
  return [];
24322
24420
  }
@@ -24463,13 +24561,13 @@ function registerWorktreeCommands(program3) {
24463
24561
  const headBorn = await execFileP2("git", ["-C", wtPath || ".", "rev-parse", "--verify", "--quiet", "HEAD"], { timeout: GIT_TIMEOUT_MS }).then(() => true).catch(() => false);
24464
24562
  const branch = headBorn ? (await execFileP2("git", ["rev-parse", "--abbrev-ref", "HEAD"], { timeout: GIT_TIMEOUT_MS })).stdout.trim() : (await execFileP2("git", ["symbolic-ref", "--quiet", "--short", "HEAD"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
24465
24563
  if (!wtPath || !branch) return fail("worktree land: not inside a git worktree");
24466
- const gitFile = (0, import_node_path28.join)(wtPath, ".git");
24467
- const isLinked = (0, import_node_fs28.existsSync)(gitFile) && (0, import_node_fs28.statSync)(gitFile).isFile();
24564
+ const gitFile = (0, import_node_path29.join)(wtPath, ".git");
24565
+ const isLinked = (0, import_node_fs29.existsSync)(gitFile) && (0, import_node_fs29.statSync)(gitFile).isFile();
24468
24566
  if (apply && !isLinked) {
24469
24567
  return fail("worktree land: run from inside the linked worktree you want to land (this is the primary checkout)");
24470
24568
  }
24471
24569
  const commonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
24472
- const primaryCheckout = commonDir ? (0, import_node_path28.dirname)(commonDir) : wtPath;
24570
+ const primaryCheckout = commonDir ? (0, import_node_path29.dirname)(commonDir) : wtPath;
24473
24571
  const localBranchNames = await execFileP2("git", ["-C", primaryCheckout, "for-each-ref", "--format=%(refname:short)", "refs/heads"], { timeout: GIT_TIMEOUT_MS }).then(({ stdout }) => new Set((stdout || "").split("\n").map((l) => l.trim()).filter(Boolean))).catch(() => void 0);
24474
24572
  const orphan = classifyOrphanedWorktree({
24475
24573
  branch,
@@ -24656,10 +24754,10 @@ async function gatherWorktreeContext() {
24656
24754
  if (s) stages.push({ path: wt.path, port: s.port });
24657
24755
  }
24658
24756
  const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
24659
- const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path28.dirname)((0, import_node_path28.dirname)(worktreeGitRoot)) : repoRoot2;
24757
+ const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path29.dirname)((0, import_node_path29.dirname)(worktreeGitRoot)) : repoRoot2;
24660
24758
  const wtRoot = siblingMmiWorktreesRoot(primaryRepoRoot);
24661
24759
  let orphanDirs = [];
24662
- if ((0, import_node_fs28.existsSync)(wtRoot)) {
24760
+ if ((0, import_node_fs29.existsSync)(wtRoot)) {
24663
24761
  orphanDirs = scanOrphanDirs(wtRoot, worktreeGitRoot, {
24664
24762
  ...defaultOrphanDirScanDeps,
24665
24763
  listDirs: (root) => worktreeScanDirs(root, primaryRepoRoot, defaultOrphanDirScanDeps.listDirs, isRepoCheckoutDir)
@@ -24682,7 +24780,7 @@ async function bestEffortGit(args, cwd, step, timeoutMs = GIT_TIMEOUT_MS) {
24682
24780
  }
24683
24781
 
24684
24782
  // src/issue-commands.ts
24685
- var import_node_fs29 = require("node:fs");
24783
+ var import_node_fs30 = require("node:fs");
24686
24784
  var import_node_crypto5 = require("node:crypto");
24687
24785
  var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
24688
24786
  var ReparentConflictError = class extends Error {
@@ -24700,7 +24798,7 @@ async function editIssue(client, options, deps = {}) {
24700
24798
  const url = `https://github.com/${repo}/issues/${parsed.number}`;
24701
24799
  const patch = {};
24702
24800
  let bodyChanged = false;
24703
- const textDeps = () => deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs29.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
24801
+ const textDeps = () => deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs30.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
24704
24802
  if (options.titleFile !== void 0) {
24705
24803
  patch.title = await resolveIssueTitle({ title: options.title, titleFile: options.titleFile }, textDeps());
24706
24804
  } else if (options.title !== void 0) {
@@ -25263,7 +25361,7 @@ function extendCreateCommand(issue2, batchAttach) {
25263
25361
  if (opts.batch) {
25264
25362
  let specs;
25265
25363
  try {
25266
- const raw = (0, import_node_fs29.readFileSync)(opts.batch, "utf8");
25364
+ const raw = (0, import_node_fs30.readFileSync)(opts.batch, "utf8");
25267
25365
  specs = JSON.parse(raw);
25268
25366
  if (!Array.isArray(specs)) throw new Error("batch file must contain a JSON array");
25269
25367
  } catch (e) {
@@ -25337,8 +25435,8 @@ ${lines}`, {
25337
25435
  }
25338
25436
 
25339
25437
  // src/train-commands.ts
25340
- var import_node_fs30 = require("node:fs");
25341
- var import_node_path29 = require("node:path");
25438
+ var import_node_fs31 = require("node:fs");
25439
+ var import_node_path30 = require("node:path");
25342
25440
 
25343
25441
  // src/train-status.ts
25344
25442
  function buildTrainStatusReport(input) {
@@ -25378,7 +25476,7 @@ function formatTrainStatus(r) {
25378
25476
  // src/train-commands.ts
25379
25477
  function readRepoVersion() {
25380
25478
  try {
25381
- return JSON.parse((0, import_node_fs30.readFileSync)((0, import_node_path29.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
25479
+ return JSON.parse((0, import_node_fs31.readFileSync)((0, import_node_path30.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
25382
25480
  } catch {
25383
25481
  return void 0;
25384
25482
  }
@@ -25524,9 +25622,9 @@ function registerDeployCommands(program3) {
25524
25622
  }
25525
25623
 
25526
25624
  // src/discovery-commands.ts
25527
- var import_node_fs31 = require("node:fs");
25625
+ var import_node_fs32 = require("node:fs");
25528
25626
  var import_node_os11 = require("node:os");
25529
- var import_node_path30 = require("node:path");
25627
+ var import_node_path31 = require("node:path");
25530
25628
  var GC_GH_TIMEOUT_MS3 = 2e4;
25531
25629
  async function collectStatus() {
25532
25630
  let branch = "";
@@ -25703,8 +25801,8 @@ async function collectOnboardStatus() {
25703
25801
  }
25704
25802
  const home = (0, import_node_os11.homedir)();
25705
25803
  const plugin = onboardPluginGate({
25706
- readKnown: () => readFileSyncSafe((0, import_node_path30.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs31.readFileSync),
25707
- readSettings: () => readFileSyncSafe((0, import_node_path30.join)(home, ".claude", "settings.json"), import_node_fs31.readFileSync)
25804
+ readKnown: () => readFileSyncSafe((0, import_node_path31.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs32.readFileSync),
25805
+ readSettings: () => readFileSyncSafe((0, import_node_path31.join)(home, ".claude", "settings.json"), import_node_fs32.readFileSync)
25708
25806
  });
25709
25807
  return { track, board, registry: registry2, secrets, plugin, nextCommand };
25710
25808
  }
@@ -27216,17 +27314,17 @@ function parseOriginRepo(remoteUrl) {
27216
27314
  }
27217
27315
  function ghHostsConfigPath(env, platform2) {
27218
27316
  const sep2 = platform2 === "win32" ? "\\" : "/";
27219
- const join27 = (...parts) => parts.join(sep2);
27317
+ const join28 = (...parts) => parts.join(sep2);
27220
27318
  const explicit = env.GH_CONFIG_DIR?.trim();
27221
- if (explicit) return join27(explicit, "hosts.yml");
27319
+ if (explicit) return join28(explicit, "hosts.yml");
27222
27320
  if (platform2 === "win32") {
27223
27321
  const appData = (env.AppData ?? env.APPDATA)?.trim();
27224
- return appData ? join27(appData, "GitHub CLI", "hosts.yml") : void 0;
27322
+ return appData ? join28(appData, "GitHub CLI", "hosts.yml") : void 0;
27225
27323
  }
27226
27324
  const xdg = env.XDG_CONFIG_HOME?.trim();
27227
- if (xdg) return join27(xdg, "gh", "hosts.yml");
27325
+ if (xdg) return join28(xdg, "gh", "hosts.yml");
27228
27326
  const home = env.HOME?.trim();
27229
- return home ? join27(home, ".config", "gh", "hosts.yml") : void 0;
27327
+ return home ? join28(home, ".config", "gh", "hosts.yml") : void 0;
27230
27328
  }
27231
27329
  function parseGhHostsAccounts(yaml, host = "github.com") {
27232
27330
  let hostIndent = null;
@@ -27276,9 +27374,9 @@ function ghAccountCaveat(announcedLogin, accounts) {
27276
27374
  }
27277
27375
 
27278
27376
  // src/doctor-io.ts
27279
- var import_node_fs32 = require("node:fs");
27377
+ var import_node_fs33 = require("node:fs");
27280
27378
  var import_node_os12 = require("node:os");
27281
- var import_node_path31 = require("node:path");
27379
+ var import_node_path32 = require("node:path");
27282
27380
  var import_node_child_process14 = require("node:child_process");
27283
27381
  var import_node_util8 = require("node:util");
27284
27382
  var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process14.execFile);
@@ -27286,7 +27384,7 @@ var MMI_PLUGIN_ID2 = "mmi@mutmutco";
27286
27384
  function installedClaudePluginVersion() {
27287
27385
  try {
27288
27386
  const file = JSON.parse(
27289
- (0, import_node_fs32.readFileSync)((0, import_node_path31.join)((0, import_node_os12.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
27387
+ (0, import_node_fs33.readFileSync)((0, import_node_path32.join)((0, import_node_os12.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
27290
27388
  );
27291
27389
  const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
27292
27390
  if (versions.length === 0) return void 0;
@@ -27326,13 +27424,13 @@ function worktreeRootSync() {
27326
27424
  }
27327
27425
  var gitignorePath = () => {
27328
27426
  const root = worktreeRootSync();
27329
- return root === null ? null : (0, import_node_path31.join)(root, ".gitignore");
27427
+ return root === null ? null : (0, import_node_path32.join)(root, ".gitignore");
27330
27428
  };
27331
27429
  function readGitignore() {
27332
27430
  const path2 = gitignorePath();
27333
27431
  if (path2 === null) return null;
27334
27432
  try {
27335
- return (0, import_node_fs32.readFileSync)(path2, "utf8");
27433
+ return (0, import_node_fs33.readFileSync)(path2, "utf8");
27336
27434
  } catch {
27337
27435
  return null;
27338
27436
  }
@@ -27341,7 +27439,7 @@ function writeGitignore(content) {
27341
27439
  const path2 = gitignorePath();
27342
27440
  if (path2 === null) return false;
27343
27441
  try {
27344
- (0, import_node_fs32.writeFileSync)(path2, content, "utf8");
27442
+ (0, import_node_fs33.writeFileSync)(path2, content, "utf8");
27345
27443
  return true;
27346
27444
  } catch {
27347
27445
  return false;
@@ -27365,7 +27463,7 @@ async function repoRoot() {
27365
27463
  }
27366
27464
  function hasRepoLocalWorktrees() {
27367
27465
  const root = worktreeRootSync();
27368
- return root !== null && (0, import_node_fs32.existsSync)((0, import_node_path31.join)(root, ".worktrees"));
27466
+ return root !== null && (0, import_node_fs33.existsSync)((0, import_node_path32.join)(root, ".worktrees"));
27369
27467
  }
27370
27468
 
27371
27469
  // src/index.ts
@@ -27401,8 +27499,8 @@ ${r.stderr ?? ""}`).catch(() => "");
27401
27499
  function ghMultiAccountCaveat(announcedLogin) {
27402
27500
  try {
27403
27501
  const hostsPath = ghHostsConfigPath(process.env, process.platform);
27404
- if (!hostsPath || !(0, import_node_fs33.existsSync)(hostsPath)) return void 0;
27405
- return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0, import_node_fs33.readFileSync)(hostsPath, "utf8")));
27502
+ if (!hostsPath || !(0, import_node_fs34.existsSync)(hostsPath)) return void 0;
27503
+ return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0, import_node_fs34.readFileSync)(hostsPath, "utf8")));
27406
27504
  } catch {
27407
27505
  return void 0;
27408
27506
  }
@@ -27410,7 +27508,7 @@ function ghMultiAccountCaveat(announcedLogin) {
27410
27508
  var ENV_HEAL_LOCK_STALE_MS = 10 * 6e4;
27411
27509
  var ENV_HEAL_LOCK_MAX_WAIT_MS = 2 * 6e4;
27412
27510
  function envHealLockPath(home) {
27413
- return (0, import_node_path32.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
27511
+ return (0, import_node_path33.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
27414
27512
  }
27415
27513
  async function withEnvHealLock(what, run) {
27416
27514
  try {
@@ -27574,8 +27672,8 @@ function mmiDoctorDeps(opts = {}) {
27574
27672
  const home = (0, import_node_os13.homedir)();
27575
27673
  return marketplaceRows(
27576
27674
  MMI_MARKETPLACE_NAME,
27577
- readFileSyncSafe((0, import_node_path32.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs33.readFileSync),
27578
- readFileSyncSafe((0, import_node_path32.join)(home, ".claude", "settings.json"), import_node_fs33.readFileSync)
27675
+ readFileSyncSafe((0, import_node_path33.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs34.readFileSync),
27676
+ readFileSyncSafe((0, import_node_path33.join)(home, ".claude", "settings.json"), import_node_fs34.readFileSync)
27579
27677
  );
27580
27678
  } catch {
27581
27679
  return [];
@@ -27781,19 +27879,19 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
27781
27879
  });
27782
27880
  var rules = program2.command("rules").description("org-managed .gitignore delivery");
27783
27881
  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) => {
27784
- const path2 = (0, import_node_path32.join)(process.cwd(), ".gitignore");
27785
- const current = (0, import_node_fs33.existsSync)(path2) ? (0, import_node_fs33.readFileSync)(path2, "utf8") : null;
27882
+ const path2 = (0, import_node_path33.join)(process.cwd(), ".gitignore");
27883
+ const current = (0, import_node_fs34.existsSync)(path2) ? (0, import_node_fs34.readFileSync)(path2, "utf8") : null;
27786
27884
  const plan = planManagedGitignore(current);
27787
27885
  const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
27788
27886
  if (opts.json) {
27789
- if (opts.write && plan.changed) (0, import_node_fs33.writeFileSync)(path2, plan.content, "utf8");
27887
+ if (opts.write && plan.changed) (0, import_node_fs34.writeFileSync)(path2, plan.content, "utf8");
27790
27888
  console.log(JSON.stringify(plan, null, 2));
27791
27889
  if (!opts.write && plan.changed) process.exitCode = 1;
27792
27890
  return;
27793
27891
  }
27794
27892
  if (opts.write) {
27795
27893
  if (plan.changed) {
27796
- (0, import_node_fs33.writeFileSync)(path2, plan.content, "utf8");
27894
+ (0, import_node_fs34.writeFileSync)(path2, plan.content, "utf8");
27797
27895
  console.log(`mmi-cli org rules gitignore: updated .gitignore (${drift})`);
27798
27896
  } else {
27799
27897
  console.log("mmi-cli org rules gitignore: up to date");
@@ -27948,8 +28046,8 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
27948
28046
  if (!Number.isFinite(limit) || limit < 1) return fail("worktree gc: --limit must be a positive integer");
27949
28047
  let root;
27950
28048
  if (o.root !== void 0) {
27951
- root = (0, import_node_path32.resolve)(o.root);
27952
- if (!(0, import_node_fs33.existsSync)(root) || !(0, import_node_fs33.statSync)(root).isDirectory()) return fail(`worktree gc: --root ${o.root} is not a directory`);
28049
+ root = (0, import_node_path33.resolve)(o.root);
28050
+ if (!(0, import_node_fs34.existsSync)(root) || !(0, import_node_fs34.statSync)(root).isDirectory()) return fail(`worktree gc: --root ${o.root} is not a directory`);
27953
28051
  const gcRepoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
27954
28052
  if (isPathUnderDirectory(gcRepoRoot, root)) {
27955
28053
  return fail(`worktree gc: --root ${root} contains this checkout \u2014 name a worktrees root, not the repo or an ancestor of it`);
@@ -28014,7 +28112,7 @@ async function primaryCheckoutRoot(from) {
28014
28112
  return primaryCheckoutRootOf(async (args) => (await execFileP2("git", ["-C", from, ...args], { timeout: GIT_TIMEOUT_MS })).stdout);
28015
28113
  }
28016
28114
  async function unprovenWorktreeReason(wtPath, repoRoot2) {
28017
- if (!(0, import_node_fs33.existsSync)(wtPath)) return `${wtPath} does not exist on disk`;
28115
+ if (!(0, import_node_fs34.existsSync)(wtPath)) return `${wtPath} does not exist on disk`;
28018
28116
  const porcelain = (await execFileP2("git", ["-C", repoRoot2, "worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout;
28019
28117
  const registered = parseWorktreePorcelainEntries(porcelain);
28020
28118
  if (!registered.length) {
@@ -28035,26 +28133,26 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
28035
28133
  function acquireWorktreeSetupLock(worktreeRoot) {
28036
28134
  const lockPath = repoRuntimeStatePath(worktreeRoot, "worktree-setup.lock");
28037
28135
  const take = () => {
28038
- const fd = (0, import_node_fs33.openSync)(lockPath, "wx");
28136
+ const fd = (0, import_node_fs34.openSync)(lockPath, "wx");
28039
28137
  try {
28040
- (0, import_node_fs33.writeSync)(fd, String(Date.now()));
28138
+ (0, import_node_fs34.writeSync)(fd, String(Date.now()));
28041
28139
  } finally {
28042
- (0, import_node_fs33.closeSync)(fd);
28140
+ (0, import_node_fs34.closeSync)(fd);
28043
28141
  }
28044
28142
  return () => {
28045
28143
  try {
28046
- (0, import_node_fs33.rmSync)(lockPath, { force: true });
28144
+ (0, import_node_fs34.rmSync)(lockPath, { force: true });
28047
28145
  } catch {
28048
28146
  }
28049
28147
  };
28050
28148
  };
28051
28149
  try {
28052
- (0, import_node_fs33.mkdirSync)((0, import_node_path32.dirname)(lockPath), { recursive: true });
28150
+ (0, import_node_fs34.mkdirSync)((0, import_node_path33.dirname)(lockPath), { recursive: true });
28053
28151
  return take();
28054
28152
  } catch {
28055
28153
  try {
28056
- if (Date.now() - (0, import_node_fs33.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
28057
- (0, import_node_fs33.rmSync)(lockPath, { force: true });
28154
+ if (Date.now() - (0, import_node_fs34.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
28155
+ (0, import_node_fs34.rmSync)(lockPath, { force: true });
28058
28156
  return take();
28059
28157
  }
28060
28158
  } catch {
@@ -28520,6 +28618,34 @@ var project = program2.command("project").description("the DDB org registry \u20
28520
28618
  async function projectTarget(commandName, explicitTarget) {
28521
28619
  return requireProjectTarget(commandName, explicitTarget, explicitTarget ? void 0 : await resolveRepo());
28522
28620
  }
28621
+ async function runProjectInfoSync(target, apply) {
28622
+ const currentRepo = await resolveRepo();
28623
+ if (!currentRepo) throw new Error("org project sync-info: run from the repository whose Project information is being synchronized");
28624
+ if (target.toLowerCase() !== currentRepo.toLowerCase() && slugOf(target) !== slugOf(currentRepo)) {
28625
+ throw new Error(`org project sync-info: ${target} is not the current checkout (${currentRepo}); run from the target repository`);
28626
+ }
28627
+ const targetRepo2 = currentRepo;
28628
+ const cfg = await loadConfig();
28629
+ const registry2 = registryClientDeps(cfg);
28630
+ const [read, projects] = await Promise.all([
28631
+ fetchProjectBySlugChecked(slugOf(targetRepo2), registry2),
28632
+ fetchProjectsList(registry2)
28633
+ ]);
28634
+ if (!read.ok) throw new Error(`org project sync-info: Hub registry read failed (${read.error})`);
28635
+ if (!read.project) throw new Error(`org project sync-info: no registry META for ${targetRepo2}`);
28636
+ if (!projects) throw new Error("org project sync-info: Hub project list unavailable");
28637
+ if (apply) {
28638
+ const authority = await fetchTrainAuthority(targetRepo2, registry2);
28639
+ if (!authority.ok) throw new Error(`org project sync-info: train authority unverified (${authority.error})`);
28640
+ if (!authority.authority.train) {
28641
+ throw new Error(`org project sync-info: ${authority.authority.login} has no train authority for ${targetRepo2}`);
28642
+ }
28643
+ }
28644
+ const repoRoot2 = await gitOut(["rev-parse", "--show-toplevel"]);
28645
+ if (!repoRoot2) throw new Error("org project sync-info: cannot resolve the current repository root");
28646
+ const plan = buildProjectInfoSyncPlan(targetRepo2, read.project, projects, repoRoot2);
28647
+ return syncProjectInfo(plan, defaultGitHubClient(), apply);
28648
+ }
28523
28649
  project.command("list").description("list all projects (identity + board, never deploy coords)").option("--json", "machine-readable output").action(async (o) => {
28524
28650
  const cfg = await loadConfig();
28525
28651
  const projects = await fetchProjectsList(registryClientDeps(cfg));
@@ -28560,6 +28686,18 @@ deploys run centrally (tenant-deploy.yml); product repos carry no deploy files.
28560
28686
  );
28561
28687
  }
28562
28688
  });
28689
+ project.command("sync-info [owner/repo]").description("synchronize the owning GitHub Project's short description + thin README from this repo's README and the registry's current member repos; dry-run by default").option("--apply", "write the synchronized Project information (train-authority gated)").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
28690
+ let target;
28691
+ try {
28692
+ target = await projectTarget("org project sync-info", repoOrSlug);
28693
+ const result = await runProjectInfoSync(target, Boolean(o.apply));
28694
+ if (o.json) return printLine(JSON.stringify(result, null, 2));
28695
+ printLine(`org project sync-info: ${result.note}`);
28696
+ printLine(` members: ${result.memberRepos.join(", ")}`);
28697
+ } catch (e) {
28698
+ return failGraceful(e.message);
28699
+ }
28700
+ });
28563
28701
  var projectDeploy = project.command("deploy").description("read nonsecret DEPLOY# facts (domain, port, deploy path, substrate, host presence)");
28564
28702
  projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# facts for one project; defaults to the current repo").addOption(new Option("--stage <stage>", "dev | rc | main").choices(["dev", "rc", "main"])).option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
28565
28703
  const cfg = await loadConfig();
@@ -28592,7 +28730,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
28592
28730
  if (dupe) return fail(`org project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
28593
28731
  if (o.secretsFile) {
28594
28732
  try {
28595
- vars.push(`secrets=${(0, import_node_fs33.readFileSync)(o.secretsFile, "utf8")}`);
28733
+ vars.push(`secrets=${(0, import_node_fs34.readFileSync)(o.secretsFile, "utf8")}`);
28596
28734
  } catch (e) {
28597
28735
  return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
28598
28736
  }
@@ -29328,11 +29466,11 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
29328
29466
  }
29329
29467
  });
29330
29468
  async function listCiWorkflowPaths(cwd = process.cwd()) {
29331
- const wfDir = (0, import_node_path32.join)(cwd, ".github", "workflows");
29332
- if (!(0, import_node_fs33.existsSync)(wfDir)) return [];
29333
- return (0, import_node_fs33.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
29469
+ const wfDir = (0, import_node_path33.join)(cwd, ".github", "workflows");
29470
+ if (!(0, import_node_fs34.existsSync)(wfDir)) return [];
29471
+ return (0, import_node_fs34.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
29334
29472
  try {
29335
- return workflowReportsPrChecks((0, import_node_fs33.readFileSync)((0, import_node_path32.join)(wfDir, name), "utf8"));
29473
+ return workflowReportsPrChecks((0, import_node_fs34.readFileSync)((0, import_node_path33.join)(wfDir, name), "utf8"));
29336
29474
  } catch {
29337
29475
  return true;
29338
29476
  }
@@ -29364,16 +29502,16 @@ function ciAuditDeps() {
29364
29502
  // gate re-seed step is skipped gracefully rather than failing mid-run.
29365
29503
  readSeedFile: (path2) => {
29366
29504
  if (!root) return null;
29367
- const fullPath = (0, import_node_path32.join)(root, path2);
29368
- return (0, import_node_fs33.existsSync)(fullPath) ? (0, import_node_fs33.readFileSync)(fullPath, "utf8") : null;
29505
+ const fullPath = (0, import_node_path33.join)(root, path2);
29506
+ return (0, import_node_fs34.existsSync)(fullPath) ? (0, import_node_fs34.readFileSync)(fullPath, "utf8") : null;
29369
29507
  }
29370
29508
  };
29371
29509
  }
29372
29510
  function hubRoot() {
29373
- const fromPkg = (0, import_node_path32.join)(__dirname, "..", "..");
29511
+ const fromPkg = (0, import_node_path33.join)(__dirname, "..", "..");
29374
29512
  const marker = "skills/bootstrap/seeds/manifest.json";
29375
- if ((0, import_node_fs33.existsSync)((0, import_node_path32.join)(fromPkg, marker))) return fromPkg;
29376
- if ((0, import_node_fs33.existsSync)((0, import_node_path32.join)(process.cwd(), marker))) return process.cwd();
29513
+ if ((0, import_node_fs34.existsSync)((0, import_node_path33.join)(fromPkg, marker))) return fromPkg;
29514
+ if ((0, import_node_fs34.existsSync)((0, import_node_path33.join)(process.cwd(), marker))) return process.cwd();
29377
29515
  return null;
29378
29516
  }
29379
29517
  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) => {
@@ -29676,7 +29814,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
29676
29814
  localCleanup = await cleanupPrMergeLocalBranch(headRef, {
29677
29815
  beforeWorktrees,
29678
29816
  startingPath,
29679
- pathExists: (p) => (0, import_node_fs33.existsSync)(p),
29817
+ pathExists: (p) => (0, import_node_fs34.existsSync)(p),
29680
29818
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
29681
29819
  teardownWorktreeStage,
29682
29820
  deferredStore,
@@ -29842,6 +29980,9 @@ function renderTrainApply(commandName, r) {
29842
29980
  if (r.checkout) {
29843
29981
  base = `${base}; checkout: ${r.checkout.note}`;
29844
29982
  }
29983
+ if (r.projectInfoSync) {
29984
+ base = `${base}; project info: ${r.projectInfoSync.note}`;
29985
+ }
29845
29986
  return r.announceNote ? `${base}; announce: ${r.announceNote}` : base;
29846
29987
  }
29847
29988
  function renderTenantRedeploy(r) {
@@ -29910,7 +30051,19 @@ for (const commandName of ["rcand", "release"]) {
29910
30051
  try {
29911
30052
  const ack = (o.ack ?? "").split(",").map((s) => s.trim()).filter(Boolean);
29912
30053
  const result = await runTrainApply(commandName, trainApplyDeps(), { watch: o.watch, announceSummaryFile: o.announceSummaryFile, ack, dev: o.dev });
29913
- return printLine(o.json ? JSON.stringify(result, null, 2) : renderTrainApply(commandName, result));
30054
+ let projectInfoSync;
30055
+ if (commandName === "release") {
30056
+ try {
30057
+ projectInfoSync = await runProjectInfoSync(result.repo, true);
30058
+ } catch (e) {
30059
+ const error = e.message;
30060
+ projectInfoSync = { applied: false, note: `FAILED \u2014 ${error}`, error };
30061
+ }
30062
+ }
30063
+ const reported = { ...result, ...projectInfoSync ? { projectInfoSync } : {} };
30064
+ printLine(o.json ? JSON.stringify(reported, null, 2) : renderTrainApply(commandName, reported));
30065
+ if (projectInfoSync && "error" in projectInfoSync) process.exitCode = 1;
30066
+ return;
29914
30067
  } catch (e) {
29915
30068
  return failGraceful(`${commandName}: ${e.message}`);
29916
30069
  }
@@ -30063,12 +30216,12 @@ access.command("audit").description("audit collaborator roles + train-branch pus
30063
30216
  targets = resolution.targets;
30064
30217
  }
30065
30218
  const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
30066
- const fileMatrix = (0, import_node_fs33.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs33.readFileSync)("access-matrix.json", "utf8")) : {};
30219
+ const fileMatrix = (0, import_node_fs34.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs34.readFileSync)("access-matrix.json", "utf8")) : {};
30067
30220
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
30068
30221
  const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
30069
- const fileContracts = (0, import_node_fs33.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs33.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
30222
+ const fileContracts = (0, import_node_fs34.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs34.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
30070
30223
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
30071
- const sanctioned = (0, import_node_fs33.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs33.readFileSync)("access-matrix.json", "utf8")) : {};
30224
+ const sanctioned = (0, import_node_fs34.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs34.readFileSync)("access-matrix.json", "utf8")) : {};
30072
30225
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess, sanctioned);
30073
30226
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
30074
30227
  if (!report.ok) process.exitCode = 1;
@@ -30101,16 +30254,16 @@ function directoryBytes(path2) {
30101
30254
  let total = 0;
30102
30255
  let entries;
30103
30256
  try {
30104
- entries = (0, import_node_fs33.readdirSync)(path2, { withFileTypes: true });
30257
+ entries = (0, import_node_fs34.readdirSync)(path2, { withFileTypes: true });
30105
30258
  } catch {
30106
30259
  return 0;
30107
30260
  }
30108
30261
  for (const entry of entries) {
30109
- const child2 = (0, import_node_path32.join)(path2, entry.name);
30262
+ const child2 = (0, import_node_path33.join)(path2, entry.name);
30110
30263
  if (entry.isDirectory()) total += directoryBytes(child2);
30111
30264
  else {
30112
30265
  try {
30113
- total += (0, import_node_fs33.statSync)(child2).size;
30266
+ total += (0, import_node_fs34.statSync)(child2).size;
30114
30267
  } catch {
30115
30268
  }
30116
30269
  }
@@ -30118,25 +30271,25 @@ function directoryBytes(path2) {
30118
30271
  return total;
30119
30272
  }
30120
30273
  function listDirEntries(dir) {
30121
- return (0, import_node_fs33.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
30274
+ return (0, import_node_fs34.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
30122
30275
  }
30123
30276
  function readInstalledPluginRefs(configRoot) {
30124
30277
  const p = installedPluginsPathForConfig(configRoot);
30125
- if (!(0, import_node_fs33.existsSync)(p)) return [];
30278
+ if (!(0, import_node_fs34.existsSync)(p)) return [];
30126
30279
  try {
30127
- return installedPluginPaths((0, import_node_fs33.readFileSync)(p, "utf8"));
30280
+ return installedPluginPaths((0, import_node_fs34.readFileSync)(p, "utf8"));
30128
30281
  } catch {
30129
30282
  return null;
30130
30283
  }
30131
30284
  }
30132
30285
  function pluginCacheFsDeps(configRoot, dirBytes) {
30133
30286
  return {
30134
- exists: (p) => (0, import_node_fs33.existsSync)(p),
30135
- listVersionDirs: (root) => (0, import_node_fs33.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
30287
+ exists: (p) => (0, import_node_fs34.existsSync)(p),
30288
+ listVersionDirs: (root) => (0, import_node_fs34.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
30136
30289
  dirBytes,
30137
- listStagingDirs: (root) => (0, import_node_fs33.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
30290
+ listStagingDirs: (root) => (0, import_node_fs34.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
30138
30291
  try {
30139
- return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path32.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs33.statSync)(p).mtimeMs) };
30292
+ return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path33.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs34.statSync)(p).mtimeMs) };
30140
30293
  } catch {
30141
30294
  return { name: d.name, mtimeMs: Date.now() };
30142
30295
  }
@@ -30150,10 +30303,10 @@ function stagingApplyFsGuard(configRoot) {
30150
30303
  return {
30151
30304
  referencedPaths: () => readInstalledPluginRefs(configRoot),
30152
30305
  mtimeMs: (name) => {
30153
- const p = (0, import_node_path32.join)(stagingRoot, name);
30154
- if (!(0, import_node_fs33.existsSync)(p)) return null;
30306
+ const p = (0, import_node_path33.join)(stagingRoot, name);
30307
+ if (!(0, import_node_fs34.existsSync)(p)) return null;
30155
30308
  try {
30156
- return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs33.statSync)(q).mtimeMs);
30309
+ return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs34.statSync)(q).mtimeMs);
30157
30310
  } catch {
30158
30311
  return null;
30159
30312
  }
@@ -30179,7 +30332,7 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
30179
30332
  { withBytes: true, configRoot, includeStaging: surface !== "codex" }
30180
30333
  );
30181
30334
  const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
30182
- const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs33.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
30335
+ const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs34.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
30183
30336
  const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
30184
30337
  if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
30185
30338
  else console.log(renderPluginCachePlan(plan, result));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.76.0",
3
+ "version": "3.77.0",
4
4
  "description": "MMI Future CLI — the org dev toolbox (board, registry, keyless secrets, release train, bootstrap, doctor) and the cross-IDE engine the MMI plugin's skills and gates drive on Claude, Codex, and Kimi.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",