@mutmutco/cli 2.16.0 → 2.17.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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.cjs +393 -49
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -45,7 +45,7 @@ mmi-cli doctor --json
45
45
  - `mmi-cli board read|claim|show|move|done|backfill-priority` reads and moves GitHub Project work.
46
46
  - `mmi-cli tenant control <owner/repo> <stage> <status|start|stop|restart>` runs bounded dev/rc box control for project-admins through the Hub API; main remains master-only.
47
47
  - `mmi-cli stage`, `stage start`, `stage stop`, `stage run`, and `port-range <repo>` manage the local gitignored stage and its port block; `stage-live` explains that remote rc/live move only via `/rcand` · `/release` · `/hotfix`.
48
- - `mmi-cli rcand`, `release`, and `hotfix` render guarded train plans; the train triggers the Hub's central tenant deployer, so product repos carry no deploy file.
48
+ - `mmi-cli rcand`, `release`, and `hotfix` render guarded train plans; product trains trigger the Hub's central tenant deployer, while MMI-Hub releases directly from `development` to `main`.
49
49
  - `mmi-cli bootstrap`, `bootstrap verify`, and `bootstrap apply` plan, audit, and seed repo onboarding.
50
50
  - `mmi-cli access audit` checks collaborator roles and train-branch allowlists.
51
51
  - `mmi-cli doctor` checks GitHub auth, repo config, CLI availability, plugin install/config/version state, and stale MMI plugin cache dirs, auto-repairing the safe gaps.
package/dist/index.cjs CHANGED
@@ -5113,6 +5113,47 @@ function buildRemoteBranchCleanupReport(branch, input) {
5113
5113
  if (input.existsAfter === false) return { name: branch, status: "deleted" };
5114
5114
  return { name: branch, status: "not-attempted", reason: input.reason ?? "remote-check-unavailable" };
5115
5115
  }
5116
+ async function buildPrMergeRemoteBranchCleanupReport(branch, deps, input) {
5117
+ const existsAfter = input.attempted ? await deps.exists(branch, { prune: true }) : void 0;
5118
+ return buildRemoteBranchCleanupReport(branch, {
5119
+ attempted: input.attempted,
5120
+ existedBefore: input.existedBefore,
5121
+ existsAfter,
5122
+ reason: input.reason
5123
+ });
5124
+ }
5125
+ function summarizePrMergeCleanupStatus(input) {
5126
+ if (input.remoteBranch.status === "failed") return "warnings";
5127
+ if (input.localBranch.status === "failed") return "warnings";
5128
+ if (input.localBranch.reason === "worktree-removal-failed") return "warnings";
5129
+ if (input.worktree?.status === "failed") return "warnings";
5130
+ return "clean";
5131
+ }
5132
+ function buildPrMergeResultPayload(input) {
5133
+ return {
5134
+ mergeStatus: "merged",
5135
+ merged: input.number,
5136
+ branch: input.branch,
5137
+ method: input.method,
5138
+ cleanupStatus: summarizePrMergeCleanupStatus({
5139
+ remoteBranch: input.remoteBranch,
5140
+ localBranch: input.localCleanup.localBranch,
5141
+ worktree: input.localCleanup.worktree
5142
+ }),
5143
+ remoteBranch: input.remoteBranch,
5144
+ localBranch: input.localCleanup.localBranch,
5145
+ worktree: input.localCleanup.worktree
5146
+ };
5147
+ }
5148
+ async function checkRemoteBranchExists(branch, deps, options = {}) {
5149
+ if (!branch) return void 0;
5150
+ try {
5151
+ if (options.prune) await deps.execGit(["fetch", "origin", "--prune"]).catch(() => void 0);
5152
+ return (await deps.execGit(["ls-remote", "--heads", "origin", branch])).trim().length > 0;
5153
+ } catch {
5154
+ return void 0;
5155
+ }
5156
+ }
5116
5157
  var DEFAULT_PROTECTED = /* @__PURE__ */ new Set(["development", "main", "master", "rc"]);
5117
5158
  function groupedPrs(prs) {
5118
5159
  const out = /* @__PURE__ */ new Map();
@@ -5314,7 +5355,7 @@ function branchMissingFromList(branch, stdout) {
5314
5355
  return !names.includes(branch);
5315
5356
  }
5316
5357
  function shellQuote(value) {
5317
- return `"${value.replace(/(["\\])/g, "\\$1")}"`;
5358
+ return `'${value.replace(/'/g, "''")}'`;
5318
5359
  }
5319
5360
  function safeWorktreeRemoveCommand(safeCwd, targetPath) {
5320
5361
  const prefix = safeCwd ? `git -C ${shellQuote(safeCwd)}` : "git";
@@ -5340,7 +5381,11 @@ async function cleanupPrMergeLocalBranch(branch, options) {
5340
5381
  const wtPath = selectPrMergeCleanupWorktree(branch, beforeWorktrees, afterWorktrees, options.startingPath);
5341
5382
  const safeCwd = selectSafeWorktreeCwd([...afterWorktrees, ...beforeWorktrees], wtPath);
5342
5383
  const git = (args) => safeCwd ? options.execGit(["-C", safeCwd, ...args]) : options.execGit(args);
5343
- if (wtPath) {
5384
+ const mainWorktreePath = beforeWorktrees[0]?.path ?? afterWorktrees[0]?.path;
5385
+ const mainWorktreeTarget = Boolean(wtPath && mainWorktreePath && samePath(wtPath, mainWorktreePath));
5386
+ if (wtPath && mainWorktreeTarget) {
5387
+ report.worktree = { path: wtPath, status: "not-attempted", reason: "main-worktree" };
5388
+ } else if (wtPath) {
5344
5389
  let stageTeardown;
5345
5390
  if (options.teardownWorktreeStage) {
5346
5391
  try {
@@ -5408,6 +5453,43 @@ function formatGcPlan(plan2, apply) {
5408
5453
  }
5409
5454
 
5410
5455
  // src/command-plans.ts
5456
+ function parseTrainTag(tag) {
5457
+ const m = /^v(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?$/.exec(tag);
5458
+ return m ? { major: +m[1], minor: +m[2], patch: +m[3], rc: m[4] ? +m[4] : null } : null;
5459
+ }
5460
+ function cmpTrainRelease(a, b) {
5461
+ return a.major - b.major || a.minor - b.minor || a.patch - b.patch;
5462
+ }
5463
+ function trainPlanTargetsFromTags(tags, explicitRaw) {
5464
+ const all = tags.map(parseTrainTag).filter((v) => Boolean(v));
5465
+ const release = all.find((v) => v.rc === null) ?? { major: 0, minor: 0, patch: 0, rc: null };
5466
+ const targets = {};
5467
+ let explicit = null;
5468
+ if (explicitRaw) {
5469
+ const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(String(explicitRaw).trim());
5470
+ if (!m) {
5471
+ targets.plannedReleaseError = `MMI_RELEASE_VERSION must be X.Y.Z (got ${explicitRaw})`;
5472
+ } else {
5473
+ explicit = { major: +m[1], minor: +m[2], patch: +m[3], rc: null };
5474
+ if (cmpTrainRelease(explicit, release) <= 0) {
5475
+ targets.plannedReleaseError = `MMI_RELEASE_VERSION ${explicitRaw} must be ahead of the latest release v${release.major}.${release.minor}.${release.patch}`;
5476
+ }
5477
+ }
5478
+ }
5479
+ if (!targets.plannedReleaseError) {
5480
+ const planned = explicit ?? { major: release.major, minor: release.minor + 1, patch: 0, rc: null };
5481
+ targets.plannedRelease = `v${planned.major}.${planned.minor}.${planned.patch}`;
5482
+ }
5483
+ let rcs = all.filter((v) => v.rc !== null);
5484
+ if (explicit) rcs = rcs.filter((v) => v.major === explicit.major && v.minor === explicit.minor && v.patch === explicit.patch);
5485
+ rcs.sort((a, b) => cmpTrainRelease(b, a) || (b.rc ?? 0) - (a.rc ?? 0));
5486
+ if (rcs[0]) {
5487
+ targets.existingRcRelease = `v${rcs[0].major}.${rcs[0].minor}.${rcs[0].patch}`;
5488
+ } else {
5489
+ targets.existingRcReleaseError = explicit ? `no rc tag for ${explicit.major}.${explicit.minor}.${explicit.patch} to release` : "no rc tag to release";
5490
+ }
5491
+ return targets;
5492
+ }
5411
5493
  function stagePlan(stage2 = {}, stops = true) {
5412
5494
  return [
5413
5495
  ...stops ? [{ label: "force-kill previous local stage", command: "mmi-cli stage stop --apply" }] : [],
@@ -5436,20 +5518,49 @@ function stageLivePlan() {
5436
5518
  { label: "remote rc/live environments move through the gated promotion train", command: "mmi-cli rcand && mmi-cli release && mmi-cli hotfix", gated: true }
5437
5519
  ];
5438
5520
  }
5439
- function trainPlan(command) {
5521
+ function rcandVersionStep(targets) {
5522
+ const planned = targets.plannedRelease ? `plannedRelease=${targets.plannedRelease}` : "plannedRelease=(unresolved)";
5523
+ const existing = targets.existingRcRelease ? `existingRcRelease=${targets.existingRcRelease}` : "existingRcRelease=(none resolved yet)";
5524
+ return {
5525
+ label: `resolve train version targets (${planned}; ${existing})`,
5526
+ command: "plannedRelease: node scripts/next-version.mjs cycle; existing rc release only: node scripts/next-version.mjs release",
5527
+ gated: true
5528
+ };
5529
+ }
5530
+ function trainPlan(command, options = {}) {
5531
+ const isHub = options.repo?.toLowerCase() === "mutmutco/mmi-hub";
5440
5532
  if (command === "rcand") {
5533
+ if (isHub) {
5534
+ return [
5535
+ { label: "MMI-Hub releases skip rc; use /release from development instead", command: "mmi-cli release --apply", gated: true }
5536
+ ];
5537
+ }
5441
5538
  return [
5442
5539
  { label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
5443
5540
  { label: "verify current branch is development", gated: true },
5541
+ rcandVersionStep(options),
5444
5542
  { label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
5445
5543
  { label: "preflight required rc secret names", command: "mmi-cli secrets preflight --stage rc --repo <owner/repo>", gated: true },
5446
- { label: "for Hub distribution changes, verify the release bump is already landed before touching rc", command: "node scripts/release-distribution.mjs verify <release-tag> --skip-npm-view", gated: true },
5544
+ { label: "for Hub distribution changes, verify the release bump is already landed before touching rc", command: "node scripts/release-distribution.mjs verify <release-tag> --skip-npm-view; if missing: mmi-cli train prep --apply", gated: true },
5447
5545
  { label: "merge development to rc", gated: true },
5448
- { label: "trigger the deploy path for this repo model, returning the Hub Actions run id/url (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 auto-fires on rc push", gated: true },
5546
+ { 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 auto-fires on rc push, correlate/watch that run", gated: true },
5449
5547
  { label: "after a failed deploy, retry the existing rc ref (no re-tag/merge)", command: "mmi-cli tenant redeploy <owner/repo> rc --watch", gated: true }
5450
5548
  ];
5451
5549
  }
5452
5550
  if (command === "release") {
5551
+ if (isHub) {
5552
+ return [
5553
+ { label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
5554
+ { label: "verify current branch is development", gated: true },
5555
+ { label: "verify registry META for this project", command: "mmi-cli project get <owner/repo>", gated: true },
5556
+ { label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
5557
+ { label: "Hub releases skip rc; verify the distribution bump is already landed on development", command: "node scripts/release-distribution.mjs verify <release-tag> --skip-npm-view", gated: true },
5558
+ { label: "merge development to main", gated: true },
5559
+ { label: "tag release and publish GitHub Release", gated: true },
5560
+ { label: "trigger the Hub deploy path from the release event", command: "hub-serverless: deploy.yml + publish.yml auto-fire on the release", gated: true },
5561
+ { label: "roll development forward", gated: true }
5562
+ ];
5563
+ }
5453
5564
  return [
5454
5565
  { label: "verify operator is a master-admin org owner", command: "gh api orgs/<owner>/memberships/<login> --jq .role", gated: true },
5455
5566
  { label: "verify current branch is rc", gated: true },
@@ -5457,9 +5568,9 @@ function trainPlan(command) {
5457
5568
  { label: "preflight required main secret names", command: "mmi-cli secrets preflight --stage main --repo <owner/repo>", gated: true },
5458
5569
  { label: "verify every main-only hotfix commit is covered by the rc candidate", command: "node scripts/hotfix-coverage.mjs", gated: true },
5459
5570
  { label: "merge rc to main", gated: true },
5460
- { label: "for Hub distribution changes, verify the promoted SHA carries the release bump", command: "node scripts/release-distribution.mjs verify <release-tag> --skip-npm-view", gated: true },
5571
+ { label: "for Hub distribution changes, verify the promoted SHA carries the release bump", command: "node scripts/release-distribution.mjs verify <release-tag> --skip-npm-view; if missing: mmi-cli train prep --apply", gated: true },
5461
5572
  { label: "tag release and publish GitHub Release", gated: true },
5462
- { label: "trigger the deploy path for this repo model, returning the Hub Actions run id/url (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", gated: true },
5573
+ { 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", gated: true },
5463
5574
  { label: "roll development forward", gated: true }
5464
5575
  ];
5465
5576
  }
@@ -5546,9 +5657,9 @@ function stalePosixFields(config, shell2) {
5546
5657
  }
5547
5658
  function sanitizeLocalStage(local, stale) {
5548
5659
  if (!stale.length) return local;
5549
- const clean3 = { ...local };
5550
- for (const field of stale) delete clean3[field];
5551
- return clean3;
5660
+ const clean4 = { ...local };
5661
+ for (const field of stale) delete clean4[field];
5662
+ return clean4;
5552
5663
  }
5553
5664
  function staleNote(staleFields, outcome) {
5554
5665
  const list = staleFields.join(", ");
@@ -6338,6 +6449,30 @@ async function verifyHubDistributionVersion(deps, model, releaseTag) {
6338
6449
  if (model !== "hub-serverless") return;
6339
6450
  await deps.run("node", ["scripts/release-distribution.mjs", "verify", releaseTag, "--skip-npm-view"]);
6340
6451
  }
6452
+ async function verifyPublishedRelease(deps, repo, tag, expectedTarget, expectedSha) {
6453
+ const out = await deps.run("gh", ["release", "view", tag, "--repo", repo, "--json", "tagName,targetCommitish"]);
6454
+ let release;
6455
+ try {
6456
+ release = JSON.parse(out);
6457
+ } catch {
6458
+ throw new Error(`Release ${tag} metadata was not valid JSON`);
6459
+ }
6460
+ if (release.tagName !== tag) {
6461
+ throw new Error(`Release metadata tag is ${String(release.tagName || "(missing)")}, expected ${tag}`);
6462
+ }
6463
+ if (release.targetCommitish !== expectedTarget) {
6464
+ throw new Error(`Release ${tag} targetCommitish is ${String(release.targetCommitish || "(missing)")}, expected ${expectedTarget}`);
6465
+ }
6466
+ const tagSha = requireValue(clean(await deps.run("git", ["rev-parse", `${tag}^{commit}`])), "release tag sha");
6467
+ if (tagSha !== expectedSha) {
6468
+ throw new Error(`Release ${tag} tag points at ${tagSha}, expected ${expectedSha}`);
6469
+ }
6470
+ const branchOut = clean(await deps.run("git", ["ls-remote", "origin", `refs/heads/${expectedTarget}`]));
6471
+ const branchSha = requireValue(branchOut.split(/\s+/)[0] ?? "", `origin/${expectedTarget} sha`);
6472
+ if (branchSha !== expectedSha) {
6473
+ throw new Error(`origin/${expectedTarget} points at ${branchSha}, expected ${expectedSha}`);
6474
+ }
6475
+ }
6341
6476
  var ORG_SPINE_FILES = [
6342
6477
  "AGENTS.md",
6343
6478
  "CLAUDE.md",
@@ -6414,6 +6549,9 @@ async function requireBranch(deps, branch) {
6414
6549
  if (current !== branch) throw new Error(`must run from ${branch}, currently on ${current || "(unknown)"}`);
6415
6550
  }
6416
6551
  var HUB_REPO2 = "mutmutco/MMI-Hub";
6552
+ function isHubControlRepo(repo) {
6553
+ return repo.toLowerCase() === HUB_REPO2.toLowerCase();
6554
+ }
6417
6555
  var CORRELATE_ATTEMPTS = 5;
6418
6556
  var CORRELATE_DELAY_MS = 1500;
6419
6557
  var CORRELATE_SKEW_SLACK_MS = 1e4;
@@ -6451,6 +6589,42 @@ async function correlateTenantRun(deps, since) {
6451
6589
  }
6452
6590
  return {};
6453
6591
  }
6592
+ async function correlateWorkflowRun(deps, args) {
6593
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
6594
+ const threshold = args.since - CORRELATE_SKEW_SLACK_MS;
6595
+ let lastError;
6596
+ let parsedAnyResponse = false;
6597
+ for (let attempt = 0; attempt < CORRELATE_ATTEMPTS; attempt++) {
6598
+ if (attempt > 0) await sleep(CORRELATE_DELAY_MS);
6599
+ const listArgs = [
6600
+ "run",
6601
+ "list",
6602
+ "--repo",
6603
+ HUB_REPO2,
6604
+ "--workflow",
6605
+ args.workflow,
6606
+ "--event",
6607
+ args.event,
6608
+ ...args.branch ? ["--branch", args.branch] : [],
6609
+ "--limit",
6610
+ "10",
6611
+ "--json",
6612
+ "databaseId,url,event,createdAt,status,conclusion,headSha"
6613
+ ];
6614
+ let rows;
6615
+ try {
6616
+ rows = JSON.parse(await deps.run("gh", listArgs));
6617
+ parsedAnyResponse = true;
6618
+ } catch {
6619
+ lastError = new Error(`could not list ${args.workflow} runs`);
6620
+ continue;
6621
+ }
6622
+ const match = rows.filter((r) => r.event === args.event && r.headSha === args.headSha && typeof r.databaseId === "number").map((r) => ({ row: r, created: Date.parse(r.createdAt ?? "") })).filter((c) => Number.isFinite(c.created) && c.created >= threshold).sort((a, b) => b.created - a.created)[0];
6623
+ if (match) return { runId: match.row.databaseId, runUrl: match.row.url };
6624
+ }
6625
+ if (!parsedAnyResponse && lastError) throw lastError;
6626
+ return {};
6627
+ }
6454
6628
  async function watchTenantRun(deps, runId) {
6455
6629
  if (runId == null) return "pending";
6456
6630
  try {
@@ -6460,6 +6634,16 @@ async function watchTenantRun(deps, runId) {
6460
6634
  return "failure";
6461
6635
  }
6462
6636
  }
6637
+ async function watchWorkflowRun(deps, workflow, run) {
6638
+ if (run.runId == null) return { workflow, conclusion: "pending" };
6639
+ const conclusion = await watchTenantRun(deps, run.runId);
6640
+ return { workflow, runId: run.runId, runUrl: run.runUrl, conclusion };
6641
+ }
6642
+ function aggregateWorkflowRuns(runs) {
6643
+ if (runs.some((r) => r.conclusion === "failure")) return "failure";
6644
+ if (runs.some((r) => r.conclusion === "pending")) return "pending";
6645
+ return "success";
6646
+ }
6463
6647
  function isNotFoundError(e) {
6464
6648
  const msg = `${e.message ?? e} ${String(e.stderr ?? "")}`;
6465
6649
  return /HTTP 404|Not Found|\(404\)/i.test(msg);
@@ -6578,7 +6762,7 @@ async function resolveRcResumeTag(deps, base, sha) {
6578
6762
  const note = leftovers.length > 0 ? `resuming existing RC tag ${newest} already on ${sha.slice(0, 7)} (newest of ${sorted.length}); harmless leftover tag(s) on the same SHA: ${leftovers.join(", ")}` : `resuming existing RC tag ${newest} already on ${sha.slice(0, 7)} instead of minting a fresh rc`;
6579
6763
  return { tag: newest, note };
6580
6764
  }
6581
- async function dispatchDeploy(deps, ctx, stage2, ref, model, watch) {
6765
+ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince, autoRunHeadSha) {
6582
6766
  if (model === "tenant-container") {
6583
6767
  const since = (deps.now ?? Date.now)();
6584
6768
  await deps.run("gh", [
@@ -6601,9 +6785,30 @@ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch) {
6601
6785
  return { note: `dispatched tenant-deploy.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
6602
6786
  }
6603
6787
  if (model === "hub-serverless") {
6788
+ const note = ref === "rc" ? "no manual dispatch: deploy.yml auto-fires on the rc push (rc stage)" : "no manual dispatch: deploy.yml + publish.yml auto-fire on the published Release (prod)";
6789
+ if (!watch) return { note, deployStatus: "pending" };
6790
+ if (!autoRunHeadSha) return { note, deployStatus: "pending" };
6791
+ const since = autoRunSince ?? (deps.now ?? Date.now)();
6792
+ const targets = ref === "rc" ? [{ workflow: "deploy.yml", event: "push", branch: "rc" }] : [
6793
+ { workflow: "deploy.yml", event: "release" },
6794
+ { workflow: "publish.yml", event: "release" }
6795
+ ];
6796
+ const workflowRuns = [];
6797
+ for (const target of targets) {
6798
+ try {
6799
+ const run = await correlateWorkflowRun(deps, { ...target, since, headSha: autoRunHeadSha });
6800
+ workflowRuns.push(await watchWorkflowRun(deps, target.workflow, run));
6801
+ } catch {
6802
+ workflowRuns.push({ workflow: target.workflow, conclusion: "failure" });
6803
+ }
6804
+ }
6805
+ const primary = workflowRuns[0];
6604
6806
  return {
6605
- note: ref === "rc" ? "no manual dispatch: deploy.yml auto-fires on the rc push (rc stage)" : "no manual dispatch: deploy.yml + publish.yml auto-fire on the published Release (prod)",
6606
- deployStatus: "pending"
6807
+ note,
6808
+ runId: primary?.runId,
6809
+ runUrl: primary?.runUrl,
6810
+ workflowRuns,
6811
+ deployStatus: aggregateWorkflowRuns(workflowRuns)
6607
6812
  };
6608
6813
  }
6609
6814
  return { note: `no manual dispatch: ${model} repo deploys via its own push-triggered workflow`, deployStatus: "pending" };
@@ -6638,6 +6843,9 @@ async function runTrainApply(command, deps, options = {}) {
6638
6843
  "nothing to promote: origin/development is not ahead of origin/rc"
6639
6844
  );
6640
6845
  const deployModel2 = await preflight(deps, ctx, "rc");
6846
+ if (isHubControlRepo(ctx.repo) && deployModel2 === "hub-serverless") {
6847
+ throw new Error("MMI-Hub releases directly from development to main; run mmi-cli release --apply from development instead of rcand");
6848
+ }
6641
6849
  const releaseBase = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release base");
6642
6850
  await verifyHubDistributionVersion(deps, deployModel2, releaseBase);
6643
6851
  await deps.run("git", ["checkout", "rc"]);
@@ -6650,9 +6858,70 @@ async function runTrainApply(command, deps, options = {}) {
6650
6858
  await ensureTagPushed(deps, tag2, rcSha);
6651
6859
  const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "rc");
6652
6860
  const checks2 = await waitForRequiredTrainChecks(deps, ctx, rcSha, requiredChecks2);
6861
+ const autoRunSince2 = (deps.now ?? Date.now)();
6653
6862
  await deps.run("git", ["push", "origin", "rc"]);
6654
- const d2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch);
6655
- return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, deployModel: deployModel2, promoted: true, checks: checks2, resumeNote, dispatch: d2.note, runId: d2.runId, runUrl: d2.runUrl, deployStatus: d2.deployStatus };
6863
+ const d2 = await dispatchDeploy(deps, ctx, "rc", "rc", deployModel2, watch, autoRunSince2, rcSha);
6864
+ return { ...ctx, command, stage: "rc", ref: "rc", tag: tag2, deployModel: deployModel2, promoted: true, checks: checks2, resumeNote, dispatch: d2.note, runId: d2.runId, runUrl: d2.runUrl, workflowRuns: d2.workflowRuns, deployStatus: d2.deployStatus };
6865
+ }
6866
+ if (isHubControlRepo(ctx.repo)) {
6867
+ await requireBranch(deps, "development");
6868
+ await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
6869
+ ensurePositiveCount(
6870
+ await deps.run("git", ["rev-list", "--count", "origin/main..origin/development"]),
6871
+ "nothing to release: origin/development is not ahead of origin/main"
6872
+ );
6873
+ const deployModel2 = await preflight(deps, ctx, "main");
6874
+ if (deployModel2 !== "hub-serverless") {
6875
+ throw new Error(`MMI-Hub direct release requires deployModel=hub-serverless, got ${deployModel2}`);
6876
+ }
6877
+ const tag2 = requireValue(clean(await deps.run("node", ["scripts/next-version.mjs", "cycle"])), "release tag");
6878
+ await verifyHubDistributionVersion(deps, deployModel2, tag2);
6879
+ const predicted2 = await predictMergeConflicts(deps, "origin/main", "origin/development");
6880
+ const predictedNonSpine2 = predicted2.filter((f) => !isSpinePath(f));
6881
+ if (predictedNonSpine2.length > 0) {
6882
+ throw new Error(
6883
+ `development -> main merge would conflict on non-spine path(s): ${predictedNonSpine2.join(", ")} \u2014 no merge was started. The train is misaligned: reconcile main and development via an approved alignment PR, then rerun release.`
6884
+ );
6885
+ }
6886
+ await deps.run("git", ["checkout", "main"]);
6887
+ await deps.run("git", ["pull", "--ff-only", "origin", "main"]);
6888
+ if (predicted2.length === 0) {
6889
+ await deps.run("git", ["merge", "development", "--no-edit"]);
6890
+ } else {
6891
+ await mergeWithSpineResolution(deps, "development", "development -> main", "theirs");
6892
+ }
6893
+ const releaseSha2 = requireValue(clean(await deps.run("git", ["rev-parse", "main"])), "release sha");
6894
+ await ensureTagPushed(deps, tag2, releaseSha2);
6895
+ const requiredChecks2 = await discoverRequiredCheckContexts(deps, ctx, "main");
6896
+ const checks2 = await waitForRequiredTrainChecks(deps, ctx, releaseSha2, requiredChecks2);
6897
+ await deps.run("git", ["push", "origin", "main"]);
6898
+ const releaseUrl2 = clean(await deps.run("gh", ["release", "create", tag2, "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
6899
+ const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
6900
+ const autoRunSince2 = (deps.now ?? Date.now)();
6901
+ const d2 = await dispatchDeploy(deps, ctx, "main", "main", deployModel2, watch, autoRunSince2, releaseSha2);
6902
+ await deps.run("git", ["checkout", "development"]);
6903
+ await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
6904
+ await deps.run("git", ["merge", "main", "--no-edit"]);
6905
+ await deps.run("git", ["push", "origin", "development"]);
6906
+ return {
6907
+ ...ctx,
6908
+ command,
6909
+ stage: "main",
6910
+ ref: "main",
6911
+ tag: tag2,
6912
+ deployModel: deployModel2,
6913
+ promoted: true,
6914
+ checks: checks2,
6915
+ dispatch: d2.note,
6916
+ runId: d2.runId,
6917
+ runUrl: d2.runUrl,
6918
+ workflowRuns: d2.workflowRuns,
6919
+ deployStatus: d2.deployStatus,
6920
+ rcRetirement: "not-applicable",
6921
+ rcRetirementNote: "Hub releases skip rc; no rc runtime to retire",
6922
+ announceNote: announceNote2,
6923
+ release: { tag: tag2, url: releaseUrl2, targetSha: releaseSha2 }
6924
+ };
6656
6925
  }
6657
6926
  await requireBranch(deps, "rc");
6658
6927
  ensurePositiveCount(
@@ -6682,9 +6951,11 @@ async function runTrainApply(command, deps, options = {}) {
6682
6951
  const requiredChecks = await discoverRequiredCheckContexts(deps, ctx, "main");
6683
6952
  const checks = await waitForRequiredTrainChecks(deps, ctx, releaseSha, requiredChecks);
6684
6953
  await deps.run("git", ["push", "origin", "main"]);
6685
- const releaseUrl = clean(await deps.run("gh", ["release", "create", tag, "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
6954
+ const autoRunSince = (deps.now ?? Date.now)();
6955
+ const releaseUrl = clean(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
6956
+ await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
6686
6957
  const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
6687
- const d = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch);
6958
+ const d = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha);
6688
6959
  const retirement = await retireRcRuntime(deps, ctx, deployModel, d.deployStatus, releasedRcSha);
6689
6960
  await deps.run("git", ["checkout", "development"]);
6690
6961
  await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
@@ -6703,6 +6974,7 @@ async function runTrainApply(command, deps, options = {}) {
6703
6974
  dispatch: d.note,
6704
6975
  runId: d.runId,
6705
6976
  runUrl: d.runUrl,
6977
+ workflowRuns: d.workflowRuns,
6706
6978
  deployStatus: d.deployStatus,
6707
6979
  rcRetirement: retirement.status,
6708
6980
  rcRetirementNote: retirement.note,
@@ -6814,14 +7086,42 @@ async function runTenantRedeploy(deps, options) {
6814
7086
  throw new Error(`${repo} is ${deployModel}, not tenant-container \u2014 there is no central tenant-deploy run to retry (its deploy fires from its own workflow)`);
6815
7087
  }
6816
7088
  const d = await dispatchDeploy(deps, ctx, stage2, ref, deployModel, watch);
6817
- return { ...ctx, command: "tenant-redeploy", stage: stage2, ref, deployModel, dispatch: d.note, runId: d.runId, runUrl: d.runUrl, deployStatus: d.deployStatus };
7089
+ return { ...ctx, command: "tenant-redeploy", stage: stage2, ref, deployModel, dispatch: d.note, runId: d.runId, runUrl: d.runUrl, workflowRuns: d.workflowRuns, deployStatus: d.deployStatus };
7090
+ }
7091
+
7092
+ // src/train-prep.ts
7093
+ function clean2(text) {
7094
+ return text.trim();
7095
+ }
7096
+ function normalizeVersion2(target) {
7097
+ const version = target.replace(/^v/, "");
7098
+ if (!/^\d+\.\d+\.\d+$/.test(version)) {
7099
+ throw new Error(`invalid train prep target ${target || "(empty)"}`);
7100
+ }
7101
+ return version;
7102
+ }
7103
+ function parseChangedFiles(out) {
7104
+ return out.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
7105
+ }
7106
+ async function runTrainPrep(deps, options = {}) {
7107
+ const target = clean2(await deps.run("node", ["scripts/next-version.mjs", "cycle"]));
7108
+ const version = normalizeVersion2(target);
7109
+ if (!options.apply) {
7110
+ return { target, version, applied: false, command: "mmi-cli train prep --apply", stagedFiles: [] };
7111
+ }
7112
+ await deps.run("node", ["scripts/release-distribution.mjs", "prepare", target]);
7113
+ const stagedFiles = parseChangedFiles(await deps.run("node", ["scripts/release-distribution.mjs", "changed-files"]));
7114
+ if (stagedFiles.length === 0) throw new Error("no locked distribution files returned by release-distribution changed-files");
7115
+ await deps.run("git", ["add", "--", ...stagedFiles]);
7116
+ await deps.run("node", ["scripts/release-distribution.mjs", "verify", target, "--skip-npm-view"]);
7117
+ return { target, version, applied: true, command: "mmi-cli pr create --base development --head <current-branch>", stagedFiles };
6818
7118
  }
6819
7119
 
6820
7120
  // src/hotfix-apply.ts
6821
7121
  var HOTFIX_RELEASE_WORKFLOWS = ["deploy.yml", "publish.yml"];
6822
7122
  var HOTFIX_RUN_FIND_ATTEMPTS = 10;
6823
7123
  var HOTFIX_RUN_FIND_DELAY_MS = 15e3;
6824
- function clean2(out) {
7124
+ function clean3(out) {
6825
7125
  return out.trim();
6826
7126
  }
6827
7127
  function sleeper(deps) {
@@ -6875,7 +7175,7 @@ async function resolveHotfixSource(deps, ctx, from) {
6875
7175
  if (!sha2) throw new Error(`PR #${num} has no merge commit recorded \u2014 name the commit SHA explicitly`);
6876
7176
  return { sha: sha2, label: `PR #${num} (${sha2.slice(0, 7)})` };
6877
7177
  }
6878
- const sha = clean2(await deps.run("git", ["rev-parse", "--verify", `${from}^{commit}`]));
7178
+ const sha = clean3(await deps.run("git", ["rev-parse", "--verify", `${from}^{commit}`]));
6879
7179
  if (!sha) throw new Error(`could not resolve commit ${from}`);
6880
7180
  return { sha, label: sha.slice(0, 7) };
6881
7181
  }
@@ -6902,7 +7202,7 @@ async function runHotfixStart(deps, options) {
6902
7202
  };
6903
7203
  }
6904
7204
  const { sha, label } = await resolveHotfixSource(deps, ctx, options.from);
6905
- const remoteBranch = clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
7205
+ const remoteBranch = clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${branch}`]));
6906
7206
  if (remoteBranch) {
6907
7207
  await deps.run("git", ["checkout", branch]);
6908
7208
  await deps.run("git", ["pull", "--ff-only", "origin", branch]);
@@ -6928,7 +7228,7 @@ async function runHotfixStart(deps, options) {
6928
7228
  }
6929
7229
  await deps.run("git", ["push", "-u", "origin", branch]);
6930
7230
  }
6931
- const prUrl = clean2(await deps.run("gh", [
7231
+ const prUrl = clean3(await deps.run("gh", [
6932
7232
  "pr",
6933
7233
  "create",
6934
7234
  "--repo",
@@ -7011,7 +7311,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
7011
7311
  if (releaseExists) {
7012
7312
  releaseNote = `Release ${tag} already exists \u2014 resumed without recreating`;
7013
7313
  } else {
7014
- const tagCommit = clean2(await deps.run("git", ["rev-parse", `${tag}^{commit}`]));
7314
+ const tagCommit = clean3(await deps.run("git", ["rev-parse", `${tag}^{commit}`]));
7015
7315
  await deps.run("gh", ["release", "create", tag, "--repo", ctx.repo, "--target", tagCommit, "--generate-notes", "--latest"]);
7016
7316
  releaseNote = `Release ${tag} created (target ${tagCommit.slice(0, 7)})`;
7017
7317
  if (deps.announce) {
@@ -7022,7 +7322,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
7022
7322
  for (const workflow of HOTFIX_RELEASE_WORKFLOWS) {
7023
7323
  runs.push(await watchReleaseRun(deps, ctx, workflow, mergedSha));
7024
7324
  }
7025
- const previousRef = clean2(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
7325
+ const previousRef = clean3(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
7026
7326
  let verifyNote;
7027
7327
  const publishSucceeded = runs.find((r) => r.workflow === "publish.yml")?.conclusion === "success";
7028
7328
  try {
@@ -7090,9 +7390,9 @@ async function runHotfixStatus(deps, versionInput) {
7090
7390
  return { ...ctx, command: "hotfix-status", ...facts, ...deriveHotfixState(facts) };
7091
7391
  }
7092
7392
  async function gatherHotfixFacts(deps, ctx, tag, version) {
7093
- const branchExists = Boolean(clean2(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
7393
+ const branchExists = Boolean(clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
7094
7394
  const pr2 = await findHotfixPr(deps, ctx, tag);
7095
- const remoteTag = clean2(await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]));
7395
+ const remoteTag = clean3(await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]));
7096
7396
  const tagPushed = Boolean(remoteTag);
7097
7397
  const tagSha = remoteTag.split(/\s+/)[0] || "";
7098
7398
  let releaseExists = false;
@@ -7126,7 +7426,7 @@ async function gatherHotfixFacts(deps, ctx, tag, version) {
7126
7426
  }
7127
7427
  }
7128
7428
  }
7129
- const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean2, () => "unknown");
7429
+ const npmVersion = await deps.run("npm", ["view", "@mutmutco/cli", "version", "--silent"]).then(clean3, () => "unknown");
7130
7430
  const devVersion = await deps.run("git", ["show", "origin/development:cli/package.json"]).then(
7131
7431
  (out) => {
7132
7432
  try {
@@ -8697,8 +8997,8 @@ function resolveKbSource(rawBase) {
8697
8997
  return { owner: m[1], repo: m[2], ref: m[3] };
8698
8998
  }
8699
8999
  function buildKbGetArgs(src, path2) {
8700
- const clean3 = path2.replace(/^\/+/, "");
8701
- return ["api", `repos/${src.owner}/${src.repo}/contents/${clean3}?ref=${src.ref}`, "-H", "Accept: application/vnd.github.raw"];
9000
+ const clean4 = path2.replace(/^\/+/, "");
9001
+ return ["api", `repos/${src.owner}/${src.repo}/contents/${clean4}?ref=${src.ref}`, "-H", "Accept: application/vnd.github.raw"];
8702
9002
  }
8703
9003
  function buildKbTreeArgs(src) {
8704
9004
  return ["api", `repos/${src.owner}/${src.repo}/git/trees/${src.ref}?recursive=1`];
@@ -10832,14 +11132,10 @@ pr.command("create").description("create a PR and print {number,url} JSON").requ
10832
11132
  const created = await ghCreate(buildPrArgs({ title: o.title, body, base: o.base, head: o.head, repo: o.repo }));
10833
11133
  console.log(JSON.stringify(created));
10834
11134
  });
10835
- async function remoteBranchExists(branch) {
10836
- if (!branch) return void 0;
10837
- try {
10838
- const { stdout } = await execFileP4("git", ["ls-remote", "--heads", "origin", branch], { timeout: GIT_TIMEOUT_MS });
10839
- return stdout.trim().length > 0;
10840
- } catch {
10841
- return void 0;
10842
- }
11135
+ async function remoteBranchExists(branch, options = {}) {
11136
+ return checkRemoteBranchExists(branch, {
11137
+ execGit: async (args) => (await execFileP4("git", args, { timeout: GIT_TIMEOUT_MS })).stdout
11138
+ }, options);
10843
11139
  }
10844
11140
  var COMPOSE_TIMEOUT_MS = 12e4;
10845
11141
  function teardownWorktreeStage(worktreePath) {
@@ -10877,11 +11173,14 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
10877
11173
  if (!/used by worktree|cannot delete branch/i.test(message)) throw e;
10878
11174
  });
10879
11175
  if (!remoteNotAttemptedReason) remoteDeleteAttempted = true;
10880
- const remoteAfter = repoArgs.length ? void 0 : await remoteBranchExists(headRef);
10881
- const remoteBranch = buildRemoteBranchCleanupReport(headRef, {
11176
+ const remoteBranch = repoArgs.length ? buildRemoteBranchCleanupReport(headRef, {
11177
+ attempted: false,
11178
+ reason: remoteNotAttemptedReason
11179
+ }) : await buildPrMergeRemoteBranchCleanupReport(headRef, {
11180
+ exists: remoteBranchExists
11181
+ }, {
10882
11182
  attempted: remoteDeleteAttempted,
10883
11183
  existedBefore: remoteBefore,
10884
- existsAfter: remoteAfter,
10885
11184
  reason: remoteNotAttemptedReason
10886
11185
  });
10887
11186
  const localCleanup = repoArgs.length ? {
@@ -10894,14 +11193,13 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
10894
11193
  execGit: async (args) => (await execFileP4("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
10895
11194
  teardownWorktreeStage
10896
11195
  });
10897
- console.log(JSON.stringify({
10898
- merged: number,
11196
+ console.log(JSON.stringify(buildPrMergeResultPayload({
11197
+ number,
10899
11198
  branch: headRef,
10900
11199
  method: method.slice(2),
10901
11200
  remoteBranch,
10902
- localBranch: localCleanup.localBranch,
10903
- worktree: localCleanup.worktree
10904
- }));
11201
+ localCleanup
11202
+ })));
10905
11203
  });
10906
11204
  async function runBoardRead(o) {
10907
11205
  try {
@@ -11218,9 +11516,22 @@ function trainApplyDeps() {
11218
11516
  }
11219
11517
  };
11220
11518
  }
11519
+ function trainPrepDeps() {
11520
+ return {
11521
+ run: async (file, args) => {
11522
+ const timeout = file === "node" && args[1] === "prepare" ? NODE_PREPARE_TIMEOUT_MS : GIT_TIMEOUT_MS;
11523
+ return (await execFileP4(file, args, { timeout })).stdout;
11524
+ }
11525
+ };
11526
+ }
11527
+ function formatWorkflowRun(r) {
11528
+ const ref = r.runUrl ?? (r.runId != null ? String(r.runId) : "unresolved");
11529
+ return `${r.workflow} ${ref} ${r.conclusion.toUpperCase()}`;
11530
+ }
11221
11531
  function renderDeployLine(d) {
11222
11532
  const parts = [d.dispatch];
11223
- if (d.runUrl) parts.push(`run ${d.runUrl}`);
11533
+ if (d.workflowRuns?.length) parts.push(`runs: ${d.workflowRuns.map(formatWorkflowRun).join(", ")}`);
11534
+ else if (d.runUrl) parts.push(`run ${d.runUrl}`);
11224
11535
  if (d.deployStatus === "success") parts.push("deploy: SUCCEEDED");
11225
11536
  else if (d.deployStatus === "failure") parts.push("deploy: FAILED (promotion stands; retry the deploy, do not re-tag)");
11226
11537
  else if (d.runId != null) parts.push(`deploy: dispatched (watch: gh run watch ${d.runId} --repo mutmutco/MMI-Hub --exit-status)`);
@@ -11235,8 +11546,37 @@ function renderTrainApply(commandName, r) {
11235
11546
  function renderTenantRedeploy(r) {
11236
11547
  return `mmi-cli tenant redeploy: ${r.repo} ${r.stage} (ref=${r.ref}) [${r.deployModel}]; ${renderDeployLine(r)}`;
11237
11548
  }
11549
+ async function resolveRcandPlanTargets() {
11550
+ try {
11551
+ const tags = (await execFileP4("git", ["tag", "--list", "v*", "--sort=-v:refname"], { timeout: 1e4 })).stdout.trim().split("\n").filter(Boolean);
11552
+ return trainPlanTargetsFromTags(tags, process.env.MMI_RELEASE_VERSION);
11553
+ } catch (e) {
11554
+ return { plannedReleaseError: e.message, existingRcReleaseError: e.message };
11555
+ }
11556
+ }
11557
+ function renderTrainPrep(r) {
11558
+ if (!r.applied) {
11559
+ return `mmi-cli train prep: target ${r.target}; run ${r.command} to prepare and stage locked distribution files`;
11560
+ }
11561
+ return [
11562
+ `mmi-cli train prep: prepared ${r.target}`,
11563
+ ` - staged locked files: ${r.stagedFiles.join(", ")}`,
11564
+ " - stopped before promotion; no push, merge, tag, release, or deploy was run",
11565
+ ` - next: ${r.command}`
11566
+ ].join("\n");
11567
+ }
11568
+ var trainCmd = program2.command("train").description("release train helpers that stop before promotion");
11569
+ trainCmd.command("prep").description("prepare and stage the Hub distribution bump for the next release train").option("--json", "machine-readable output").option("--apply", "run release-distribution prepare, exact-file stage, verify, then stop").action(async (o) => {
11570
+ try {
11571
+ await requireFreshTrainCli("train prep");
11572
+ const result = await runTrainPrep(trainPrepDeps(), { apply: o.apply });
11573
+ printLine(o.json ? JSON.stringify(result, null, 2) : renderTrainPrep(result));
11574
+ } catch (e) {
11575
+ fail(`train prep: ${e.message}`);
11576
+ }
11577
+ });
11238
11578
  for (const commandName of ["rcand", "release"]) {
11239
- program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the dispatched tenant-deploy.yml run and report its outcome (tenant-container)").option("--apply", "execute the guarded master-only train after explicit approval").option("--announce-summary-file <path>", "release only: agent-curated summary lines for the Hub Slack announcement (#883)").action(async (o) => {
11579
+ program2.command(commandName).description(`plan ${commandName} train operations; mutations require explicit master-admin approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded master-only train after explicit approval").option("--announce-summary-file <path>", "release only: agent-curated summary lines for the Hub Slack announcement (#883)").action(async (o) => {
11240
11580
  try {
11241
11581
  await requireFreshTrainCli(commandName);
11242
11582
  } catch (e) {
@@ -11250,8 +11590,12 @@ for (const commandName of ["rcand", "release"]) {
11250
11590
  return fail(`${commandName}: ${e.message}`);
11251
11591
  }
11252
11592
  }
11253
- const steps = trainPlan(commandName);
11254
- console.log(o.json ? JSON.stringify({ command: commandName, steps }, null, 2) : renderSteps(`mmi-cli ${commandName}: dry-run plan`, steps));
11593
+ const repo = await resolveRepo();
11594
+ const targets = commandName === "rcand" ? await resolveRcandPlanTargets() : void 0;
11595
+ const steps = trainPlan(commandName, { ...targets ?? {}, repo });
11596
+ console.log(
11597
+ o.json ? JSON.stringify({ command: commandName, ...targets ?? {}, repo, steps }, null, 2) : renderSteps(`mmi-cli ${commandName}: dry-run plan`, steps)
11598
+ );
11255
11599
  });
11256
11600
  }
11257
11601
  function renderHotfixStart(r) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "2.16.0",
3
+ "version": "2.17.0",
4
4
  "description": "MMI Future CLI — delivers the org rules (whole-file), plus saga and KB access. The cross-IDE engine the plugin's SessionStart hook drives.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",