@mutmutco/cli 3.25.0 → 3.27.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 +221 -68
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -3616,13 +3616,29 @@ var CLI_EXIT_WATCHDOG_MS = 1e4;
3616
3616
  async function finishCliRun(watchdogMs = CLI_EXIT_WATCHDOG_MS) {
3617
3617
  const timer = setTimeout(() => {
3618
3618
  console.error(
3619
- `mmi-cli: command finished but the process did not exit within ${watchdogMs}ms \u2014 a handle leaked; forcing exit (#2904). Please report this.`
3619
+ `mmi-cli: command finished but the process did not exit within ${watchdogMs}ms \u2014 a handle leaked (${describeActiveHandles()}); forcing exit (#2904). Please report this.`
3620
3620
  );
3621
3621
  void flushStdio().finally(() => process.exit(typeof process.exitCode === "number" ? process.exitCode : 0));
3622
3622
  }, watchdogMs);
3623
3623
  timer.unref?.();
3624
3624
  await closeHttpPool();
3625
3625
  }
3626
+ function describeActiveHandles() {
3627
+ let summary;
3628
+ try {
3629
+ const counts = /* @__PURE__ */ new Map();
3630
+ for (const type of process.getActiveResourcesInfo()) counts.set(type, (counts.get(type) ?? 0) + 1);
3631
+ summary = [...counts.entries()].sort().map(([type, n]) => n > 1 ? `${type}\xD7${n}` : type).join(", ") || "none reported";
3632
+ } catch {
3633
+ summary = "unavailable";
3634
+ }
3635
+ try {
3636
+ const children = (process._getActiveHandles?.() ?? []).filter((h) => h?.constructor?.name === "ChildProcess").map((h) => `${h.spawnfile ?? "?"}(pid=${h.pid ?? "?"})`);
3637
+ if (children.length) summary += `; live children: ${children.join(", ")}`;
3638
+ } catch {
3639
+ }
3640
+ return summary;
3641
+ }
3626
3642
  async function failGraceful(msg) {
3627
3643
  console.error(`mmi-cli ${msg}`);
3628
3644
  return cleanExit(1);
@@ -5453,6 +5469,7 @@ function buildPrMergeArgs(input) {
5453
5469
  function basePolicyBlocksImmediateMerge(message) {
5454
5470
  return /base branch policy prohibits|protected branch|required status check|merge queue/i.test(message);
5455
5471
  }
5472
+ var PR_MERGE_ENQUEUED_EXIT_CODE = 2;
5456
5473
  function ghPrMergeLocalBranchDeleteWarning(message) {
5457
5474
  return /used by worktree|cannot delete branch/i.test(message);
5458
5475
  }
@@ -6196,7 +6213,7 @@ function formatGcPlan(plan, apply) {
6196
6213
  }
6197
6214
 
6198
6215
  // src/project-model.ts
6199
- var PROJECT_TYPES = ["web-app", "hub-service", "content", "desktop-game", "non-deployable", "cli-tool", "worker"];
6216
+ var PROJECT_TYPES = ["web-app", "hub-service", "content", "desktop-app", "desktop-game", "non-deployable", "cli-tool", "worker"];
6200
6217
  var DEPLOY_MODELS = ["hub-serverless", "serverless", "tenant-container", "solo-container", "registry-publish", "content", "none"];
6201
6218
  var RELEASE_TRACKS = ["full", "direct", "trunk"];
6202
6219
  var PROJECT_TYPE_SET = new Set(PROJECT_TYPES);
@@ -6233,12 +6250,12 @@ function resolveDeployModel(meta, repo) {
6233
6250
  const projectType = resolveProjectType(meta, repo);
6234
6251
  if (projectType === "content" || meta?.class === "content") return "content";
6235
6252
  if (projectType === "hub-service" || repoIsHub(repo)) return "hub-serverless";
6236
- if (projectType === "desktop-game" || projectType === "non-deployable") return "none";
6253
+ if (projectType === "desktop-app" || projectType === "desktop-game" || projectType === "non-deployable") return "none";
6237
6254
  if (projectType === "cli-tool") return "registry-publish";
6238
6255
  return "tenant-container";
6239
6256
  }
6240
6257
  function projectTypeClearsWebProfile(projectType, deployModel) {
6241
- return projectType === "content" || projectType === "desktop-game" || projectType === "non-deployable" || projectType === "cli-tool" || deployModel === "content" || deployModel === "none" || deployModel === "registry-publish";
6258
+ return projectType === "content" || projectType === "desktop-app" || projectType === "desktop-game" || projectType === "non-deployable" || projectType === "cli-tool" || deployModel === "content" || deployModel === "none" || deployModel === "registry-publish";
6242
6259
  }
6243
6260
  function inferReleaseTrackFromBranches(hints) {
6244
6261
  if (hints.hasRcBranch) return void 0;
@@ -9200,7 +9217,7 @@ function resolveMergeCiPolicy(input) {
9200
9217
  (p) => /^\.github\/workflows\/[^/]+\.(ya?ml)$/i.test(p.replace(/\\/g, "/"))
9201
9218
  );
9202
9219
  if (!ciWorkflows.length) {
9203
- return { policy: "no-ci", reason: "no .github/workflows CI" };
9220
+ return { policy: "no-ci", reason: "no pull_request-triggered .github/workflows CI" };
9204
9221
  }
9205
9222
  return { policy: "wait-for-checks", reason: ciWorkflows.join(", ") };
9206
9223
  }
@@ -9211,13 +9228,18 @@ function parseGhPrChecksOutput(stdout) {
9211
9228
  const lines = text.split(/\r?\n/).filter((l) => l.trim());
9212
9229
  if (!lines.length) return "no-checks-reported";
9213
9230
  let anyPending = false;
9231
+ let anyFailure = false;
9214
9232
  for (const line of lines) {
9215
9233
  const parts = line.includes(" ") ? line.split(" ") : line.split(/\s+/);
9216
9234
  const state = (parts[1] ?? "").toLowerCase();
9217
- if (state === "fail" || state === "failure" || state === "error") return "failure";
9235
+ if (state === "fail" || state === "failure" || state === "error") {
9236
+ anyFailure = true;
9237
+ continue;
9238
+ }
9218
9239
  if (state === "pass" || state === "success" || state === "skipping" || state === "skipped") continue;
9219
9240
  anyPending = true;
9220
9241
  }
9242
+ if (anyFailure) return anyPending ? "failing" : "failure";
9221
9243
  return anyPending ? "pending" : "success";
9222
9244
  }
9223
9245
  function parseGhPrChecksResult(stdout, stderr) {
@@ -9230,14 +9252,16 @@ function parseGhPrChecksResult(stdout, stderr) {
9230
9252
  function pollStateFromParsed(parsed) {
9231
9253
  return parsed === "error" ? "failure" : parsed;
9232
9254
  }
9255
+ var PR_CHECKS_TIMEOUT_EXIT_CODE = 2;
9233
9256
  var PR_CHECKS_POLL_MS = 3e4;
9234
- var PR_CHECKS_TIMEOUT_MS = 10 * 6e4;
9257
+ var PR_CHECKS_TIMEOUT_MS = 30 * 6e4;
9235
9258
  var PR_CHECKS_SUCCESS_CONFIRMATIONS = 2;
9236
9259
  var PR_CHECKS_CONFIRM_MS = 5e3;
9260
+ var PR_CHECKS_FAILURE_CONFIRMATIONS = 2;
9237
9261
  var NO_CI_LIVE_CHECKS_CONTRADICTION_REASON = "ci:none contradicted by live check runs on PR head";
9238
9262
  function decidePrMergeNoCiGuard(checks, policyReason = "registry META ci:none") {
9239
9263
  if (checks === "no-checks-reported") return { action: "proceed" };
9240
- const staleNote = `registry says no-ci (${policyReason}), but live PR checks exist`;
9264
+ const staleNote = `merge CI policy says no-ci (${policyReason}), but live PR checks exist`;
9241
9265
  if (checks === "success") {
9242
9266
  return { action: "proceed", note: `${staleNote}; proceeding because they are green` };
9243
9267
  }
@@ -9264,12 +9288,39 @@ async function waitForPrChecks(deps) {
9264
9288
  queuedStates.push(firstState);
9265
9289
  }
9266
9290
  const now = deps.now ?? (() => Date.now());
9267
- const deadline = now() + PR_CHECKS_TIMEOUT_MS;
9291
+ const timeoutMs = deps.timeoutMs ?? PR_CHECKS_TIMEOUT_MS;
9292
+ const started = now();
9293
+ const deadline = started + timeoutMs;
9294
+ const report = (state) => {
9295
+ try {
9296
+ const elapsedMs = now() - started;
9297
+ deps.progress?.({ state, elapsedMs, remainingMs: Math.max(0, deadline - now()) });
9298
+ } catch {
9299
+ }
9300
+ };
9268
9301
  let lastDetail = "pending";
9269
9302
  let successStreak = 0;
9303
+ let failureStreak = 0;
9304
+ report("starting");
9270
9305
  while (now() < deadline) {
9271
9306
  const state = queuedStates.shift() ?? await deps.pollChecks();
9272
- if (state === "failure") return { policy, status: "failure", reason, detail: lastDetail };
9307
+ report(state);
9308
+ if (state !== "success") successStreak = 0;
9309
+ if (state !== "failure") failureStreak = 0;
9310
+ if (state === "failure") {
9311
+ failureStreak += 1;
9312
+ if (failureStreak >= PR_CHECKS_FAILURE_CONFIRMATIONS) {
9313
+ return { policy, status: "failure", reason, detail: "checks-failure" };
9314
+ }
9315
+ lastDetail = "confirming-failure";
9316
+ await deps.sleep(PR_CHECKS_POLL_MS);
9317
+ continue;
9318
+ }
9319
+ if (state === "failing") {
9320
+ lastDetail = "failing (a check failed; others still running)";
9321
+ await deps.sleep(PR_CHECKS_POLL_MS);
9322
+ continue;
9323
+ }
9273
9324
  if (state === "success") {
9274
9325
  successStreak += 1;
9275
9326
  if (successStreak >= PR_CHECKS_SUCCESS_CONFIRMATIONS) {
@@ -9279,7 +9330,6 @@ async function waitForPrChecks(deps) {
9279
9330
  await deps.sleep(PR_CHECKS_CONFIRM_MS);
9280
9331
  continue;
9281
9332
  }
9282
- successStreak = 0;
9283
9333
  if (state === "no-checks-reported") {
9284
9334
  lastDetail = "no-checks-reported (waiting for workflow to queue)";
9285
9335
  await deps.sleep(PR_CHECKS_POLL_MS);
@@ -9288,7 +9338,7 @@ async function waitForPrChecks(deps) {
9288
9338
  lastDetail = state;
9289
9339
  await deps.sleep(PR_CHECKS_POLL_MS);
9290
9340
  }
9291
- return { policy, status: "timeout", reason, detail: lastDetail };
9341
+ return { policy, status: "timeout", reason, detail: lastDetail, waitedMs: now() - started };
9292
9342
  }
9293
9343
 
9294
9344
  // src/bootstrap-ruleset.ts
@@ -9376,6 +9426,11 @@ function workflowTriggersPullRequest(yaml) {
9376
9426
  if (!onBlock) return false;
9377
9427
  return /\bpull_request\b/.test(onBlock);
9378
9428
  }
9429
+ function workflowReportsPrChecks(yaml) {
9430
+ const onBlock = extractOnBlock(yaml);
9431
+ if (!onBlock) return false;
9432
+ return /\bpull_request(_target)?\b/.test(onBlock);
9433
+ }
9379
9434
  function extractOnBlock(yaml) {
9380
9435
  const lines = yaml.split(/\r?\n/);
9381
9436
  let inOn = false;
@@ -9608,6 +9663,10 @@ function seedMatchesDeployModel(seed, deployModel) {
9608
9663
  if (!seed.deployModels?.length) return true;
9609
9664
  return deployModel != null && seed.deployModels.includes(deployModel);
9610
9665
  }
9666
+ function seedMatchesProjectType(seed, projectType) {
9667
+ if (!seed.projectTypes?.length) return true;
9668
+ return projectType != null && seed.projectTypes.includes(projectType);
9669
+ }
9611
9670
  function planSeedAction(seed, exists) {
9612
9671
  if (seed.source === "fanout") {
9613
9672
  return { target: seed.target, action: "skip", ownership: "fanout", reason: "delivered by the fanout pipeline" };
@@ -9620,18 +9679,25 @@ function planSeedAction(seed, exists) {
9620
9679
  }
9621
9680
  return exists ? { target: seed.target, action: "update", ownership: "org", reason: "org-owned, refresh to current" } : { target: seed.target, action: "create", ownership: "org", reason: "org-owned, missing" };
9622
9681
  }
9623
- function reconcileSeedAction(action, content, isManagedBlock) {
9682
+ function reconcileSeedAction(action, content, isManagedBlock, remoteContent = null) {
9624
9683
  if (action.action === "skip") return action;
9625
9684
  if (content == null) {
9626
9685
  return { ...action, action: "skip", reason: "no resolvable content" };
9627
9686
  }
9628
- if (isManagedBlock) return action;
9629
- const unfilled = missingPlaceholders(content);
9630
- if (unfilled.length) {
9631
- return { ...action, action: "skip", reason: `unfilled: ${unfilled.join(", ")} \u2014 pass --var` };
9687
+ if (!isManagedBlock) {
9688
+ const unfilled = missingPlaceholders(content);
9689
+ if (unfilled.length) {
9690
+ return { ...action, action: "skip", reason: `unfilled: ${unfilled.join(", ")} \u2014 pass --var` };
9691
+ }
9692
+ }
9693
+ if (action.action === "update" && remoteContent != null && sameIgnoringEol(content, remoteContent)) {
9694
+ return { ...action, action: "skip", reason: "already current" };
9632
9695
  }
9633
9696
  return action;
9634
9697
  }
9698
+ function sameIgnoringEol(a, b) {
9699
+ return a.replace(/\r\n/g, "\n") === b.replace(/\r\n/g, "\n");
9700
+ }
9635
9701
  function renderSeedPlan(actions) {
9636
9702
  const lines = ["bootstrap apply \u2014 seed plan (dry-run; no mutations):"];
9637
9703
  for (const a of actions) {
@@ -9915,12 +9981,12 @@ async function listWorkflowPaths(deps, repo, branch) {
9915
9981
  `repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`
9916
9982
  );
9917
9983
  return (entries ?? []).filter((e) => e.type === "file" && /\.ya?ml$/i.test(e.name ?? "")).map((e) => `.github/workflows/${e.name}`);
9918
- } catch {
9919
- return [];
9984
+ } catch (e) {
9985
+ return e?.status === 404 ? [] : void 0;
9920
9986
  }
9921
9987
  }
9922
9988
  async function resolveEmittedPrContexts(deps, repo, branch) {
9923
- const paths = await listWorkflowPaths(deps, repo, branch);
9989
+ const paths = await listWorkflowPaths(deps, repo, branch) ?? [];
9924
9990
  const workflows = [];
9925
9991
  for (const path2 of paths) {
9926
9992
  const body = await fetchFileContent(deps, repo, branch, path2);
@@ -9973,12 +10039,25 @@ async function resolveRepoMergeCiPolicy(repo, deps) {
9973
10039
  }
9974
10040
  const baseBranch = "development";
9975
10041
  const hasGate = repoClass === "hub" ? true : await contentExists(deps, repo, baseBranch, PRODUCT_GATE_PATH);
10042
+ const workflowPaths = repoClass === "hub" ? [".github/workflows/gate.yml"] : hasGate ? [PRODUCT_GATE_PATH] : await listWorkflowPaths(deps, repo, baseBranch);
10043
+ if (workflowPaths === void 0) {
10044
+ return { policy: "wait-for-checks", reason: "workflows listing unreadable (API error) \u2014 failing safe to wait-for-checks" };
10045
+ }
10046
+ const prTriggered = await filterPullRequestTriggered(deps, repo, baseBranch, workflowPaths);
9976
10047
  return resolveMergeCiPolicy({
9977
- workflowPaths: hasGate ? repoClass === "hub" ? [".github/workflows/gate.yml"] : [PRODUCT_GATE_PATH] : [],
10048
+ workflowPaths: prTriggered,
9978
10049
  registryCi: meta?.ci,
9979
10050
  registryRequiredChecks: meta?.requiredChecks
9980
10051
  });
9981
10052
  }
10053
+ async function filterPullRequestTriggered(deps, repo, branch, paths) {
10054
+ const kept = [];
10055
+ for (const path2 of paths) {
10056
+ const body = await fetchFileContent(deps, repo, branch, path2);
10057
+ if (body == null || workflowReportsPrChecks(body)) kept.push(path2);
10058
+ }
10059
+ return kept;
10060
+ }
9982
10061
  async function auditRepoCi(repo, deps) {
9983
10062
  const meta = await deps.getProjectMeta(slugFromRepo(repo));
9984
10063
  const repoClass = classifyRepo(repo, meta);
@@ -11927,7 +12006,6 @@ function bootstrapPlan(repo, repoClass) {
11927
12006
  { label: `apply branch protection / allowlist: ${protectedBranches}`, gated: true },
11928
12007
  { label: "attach GitHub Project v2 and register Hub registry META", gated: true },
11929
12008
  { label: "seed README.md and architecture.md", gated: true },
11930
- { label: "commit .cursor/rules/<repo>.mdc", gated: true },
11931
12009
  { label: `register fanout target on ${repoClass === "content" ? "main" : "development"}`, gated: true }
11932
12010
  ];
11933
12011
  }
@@ -14957,9 +15035,6 @@ async function contentText(deps, repo, branch, path2) {
14957
15035
  return null;
14958
15036
  }
14959
15037
  }
14960
- function repoSlugFromFullName(repo) {
14961
- return (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
14962
- }
14963
15038
  async function getProtection(deps, repo, branch) {
14964
15039
  try {
14965
15040
  return await deps.client.rest("GET", `repos/${repo}/branches/${branch}/protection`) ?? {};
@@ -15109,13 +15184,6 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
15109
15184
  label: "architecture.md is filled (no template placeholders)",
15110
15185
  detail: archPlaceholders.length ? `unfilled: ${archPlaceholders.join(", ")}` : void 0
15111
15186
  });
15112
- const agentRulesPath = `.cursor/rules/${repoSlugFromFullName(repo)}.mdc`;
15113
- const agentRulesOk = await contentExists2(deps, repo, baseBranch, agentRulesPath);
15114
- checks.push({
15115
- ok: agentRulesOk,
15116
- label: "Cursor agent rules file exists",
15117
- detail: agentRulesOk ? void 0 : `expected: ${agentRulesPath}`
15118
- });
15119
15187
  const labels = await restPagedJson2(deps, `repos/${repo}/labels`, []);
15120
15188
  const labelNames = new Set(labels.map((l) => l.name));
15121
15189
  for (const label of requiredLabels) {
@@ -15699,10 +15767,16 @@ function attestedLine(att) {
15699
15767
  var CONTRACT_UNDECLARED_LINE = "No runtime secrets declared \u2014 declare requiredRuntimeSecrets (a per-stage name map) in the registry META, or attest the app needs none with an explicit empty stage map ({ dev: [], rc: [], main: [] }).";
15700
15768
  function appGapsFor(meta, model, slug, projectType, mainDeployFact) {
15701
15769
  const attested = appAttestationOf(meta);
15702
- const isTenantWeb = !(projectType === "content" || model === "content") && projectType !== "desktop-game" && projectType !== "cli-tool" && !(projectType === "non-deployable" || model === "none") && model !== "hub-serverless" && model !== "serverless" && model !== "registry-publish";
15770
+ const isTenantWeb = !(projectType === "content" || model === "content") && projectType !== "desktop-app" && projectType !== "desktop-game" && projectType !== "cli-tool" && !(projectType === "non-deployable" || model === "none") && model !== "hub-serverless" && model !== "serverless" && model !== "registry-publish";
15703
15771
  const contractUndeclared = isTenantWeb && Boolean(meta) && !hasRuntimeSecretContract(meta?.requiredRuntimeSecrets);
15704
15772
  if (attested) return contractUndeclared ? [attestedLine(attested), CONTRACT_UNDECLARED_LINE] : [attestedLine(attested)];
15705
15773
  if (projectType === "content" || model === "content") return ["Content repo: keep app-owned work to docs/content changes; release train does not apply."];
15774
+ if (projectType === "desktop-app") {
15775
+ return [
15776
+ "Desktop app: no Hub deploy coords, Google OAuth, DWD, or tenant control by default; keep app-owned installer/packaging outside the tenant train.",
15777
+ "Keep README.md and architecture.md explicit about the per-OS build and distribution path (installer, and any registry publish alongside it) so readiness does not guess web infra."
15778
+ ];
15779
+ }
15706
15780
  if (projectType === "desktop-game") {
15707
15781
  return [
15708
15782
  "Desktop game: no Hub deploy coords, Google OAuth, DWD, or tenant control by default; keep app-owned release packaging outside the tenant train.",
@@ -15836,7 +15910,7 @@ function buildV2HealPatch(repoOrSlug, meta, mainDeployFact) {
15836
15910
  patch.requiredRuntimeSecrets = next;
15837
15911
  }
15838
15912
  }
15839
- const appOwnedGaps = confidentType ? appGapsFor(meta, model, slug, confidentType, mainDeployFact) : [`Project type is unset and not derivable \u2014 classify with \`mmi-cli project set ${repo} --project-type <web-app|hub-service|content|desktop-game|non-deployable|cli-tool|worker> --deploy-model <tenant-container|solo-container|hub-serverless|serverless|registry-publish|content|none>\` before heal completes the v2 fields (prevents defaulting a non-web repo to tenant-container).`];
15913
+ const appOwnedGaps = confidentType ? appGapsFor(meta, model, slug, confidentType, mainDeployFact) : [`Project type is unset and not derivable \u2014 classify with \`mmi-cli project set ${repo} --project-type <web-app|hub-service|content|desktop-app|desktop-game|non-deployable|cli-tool|worker> --deploy-model <tenant-container|solo-container|hub-serverless|serverless|registry-publish|content|none>\` before heal completes the v2 fields (prevents defaulting a non-web repo to tenant-container).`];
15840
15914
  if (boardRegistryGaps(meta).length) appOwnedGaps.unshift(boardRegistryGapMessage(repo));
15841
15915
  return { slug, patch, appOwnedGaps };
15842
15916
  }
@@ -16447,6 +16521,15 @@ function settableVarHelp() {
16447
16521
  });
16448
16522
  return `META field to set (repeatable): ${keys.join(", ")}`;
16449
16523
  }
16524
+ function duplicateVarKeyAcrossFlags(vars, sets) {
16525
+ const keyOf = (v) => v.includes("=") ? v.slice(0, v.indexOf("=")) : v;
16526
+ const varKeys = new Set(vars.map(keyOf));
16527
+ for (const s of sets) {
16528
+ const key = keyOf(s);
16529
+ if (varKeys.has(key)) return key;
16530
+ }
16531
+ return null;
16532
+ }
16450
16533
  function buildProjectSetPatch(input) {
16451
16534
  const patch = {};
16452
16535
  if (input.class) {
@@ -16475,7 +16558,11 @@ function buildProjectSetPatch(input) {
16475
16558
  }
16476
16559
  for (const value of input.vars) {
16477
16560
  const eq = value.indexOf("=");
16478
- if (eq <= 0) throw new Error(`project set: --var must be KEY=VALUE (got "${value}")`);
16561
+ if (eq <= 0) {
16562
+ const looksPositional = value.includes("/") || !value.includes("=");
16563
+ const hint = looksPositional ? " \u2014 if that is the [owner/repo] argument, put it BEFORE --var; a variadic flag swallows the tokens after it" : "";
16564
+ throw new Error(`project set: --var must be KEY=VALUE (got "${value}")${hint}`);
16565
+ }
16479
16566
  const key = value.slice(0, eq);
16480
16567
  const raw = value.slice(eq + 1);
16481
16568
  if (!SETTABLE_VAR_KEY_SET.has(key)) {
@@ -18200,6 +18287,28 @@ function validateBatchSpecs(specs) {
18200
18287
  }
18201
18288
  return { ok: errors.length === 0, errors, validated };
18202
18289
  }
18290
+ var NORTH_STAR_LABEL_COLOR = "5319e7";
18291
+ function northStarLabelsForBatch(specs) {
18292
+ const labels = /* @__PURE__ */ new Set();
18293
+ for (const spec of specs) {
18294
+ if (spec.northStar) labels.add(northStarLabel(normalizeNorthStarSlug(spec.northStar)));
18295
+ for (const l of spec.labels ?? []) if (l.startsWith("northstar:")) labels.add(l);
18296
+ }
18297
+ return [...labels].sort((a, b) => a.localeCompare(b));
18298
+ }
18299
+ async function ensureNorthStarLabels(client, repo, specs) {
18300
+ const minted = [];
18301
+ for (const name of northStarLabelsForBatch(specs)) {
18302
+ try {
18303
+ await client.rest("POST", `repos/${repo}/labels`, {
18304
+ body: { name, color: NORTH_STAR_LABEL_COLOR, description: `North Star: ${name.slice("northstar:".length)}` }
18305
+ });
18306
+ minted.push(name);
18307
+ } catch {
18308
+ }
18309
+ }
18310
+ return minted;
18311
+ }
18203
18312
  async function createIssuesBatch(specs, options, deps = {}) {
18204
18313
  const client = deps.client ?? defaultGitHubClient();
18205
18314
  const validation = validateBatchSpecs(specs);
@@ -18210,6 +18319,7 @@ ${lines}`);
18210
18319
  }
18211
18320
  const repo = await resolveRepo(options.repo);
18212
18321
  if (!repo) throw new Error("could not resolve repo \u2014 pass --repo owner/repo");
18322
+ await ensureNorthStarLabels(client, repo, validation.validated.map((v) => v.spec));
18213
18323
  const created = [];
18214
18324
  const failures = [];
18215
18325
  for (const entry of validation.validated) {
@@ -19588,7 +19698,7 @@ function registerPrLifecycleCommands(program3) {
19588
19698
  const { state, rows } = await snap();
19589
19699
  const output = { state, checks: rows };
19590
19700
  console.log(JSON.stringify(output));
19591
- if (state === "failure") process.exitCode = 1;
19701
+ if (state === "failure" || state === "failing") process.exitCode = 1;
19592
19702
  return;
19593
19703
  }
19594
19704
  const deadline = Date.now() + CHECKS_WATCH_TIMEOUT_MS;
@@ -20509,11 +20619,15 @@ function acquireWorktreeSetupLock(worktreeRoot) {
20509
20619
  }
20510
20620
  }
20511
20621
  var worktree = program2.command("worktree").description("self-provisioning worktrees \u2014 install deps + copy local-only config");
20512
- worktree.command("create <branch>").description("create a worktree from a base ref and provision it (install deps + copy local-only config)").option("--from <ref>", "base ref to branch from", "origin/development").option("--path <path>", "worktree path (default: ../mmi-worktrees/<branch>)").option("--remote <name>", "remote to fetch when --from is a <remote>/<branch> ref", "origin").option("--json", "machine-readable output").action(async (branch, o) => {
20622
+ worktree.command("create <branch>").description("create a worktree from a base ref and provision it (install deps + copy local-only config)").option("--from <ref>", "base ref to branch from", "origin/development").option("--base <ref>", "alias of --from (git's own word for the base ref)").option("--path <path>", "worktree path (default: ../mmi-worktrees/<branch>)").option("--remote <name>", "remote to fetch when --from is a <remote>/<branch> ref", "origin").option("--json", "machine-readable output").action(async (branch, o, cmd) => {
20513
20623
  try {
20624
+ if (o.base !== void 0 && cmd.getOptionValueSource("from") === "cli" && o.base !== o.from) {
20625
+ return fail(`worktree create: --from and --base are the same option (--base is an alias); got --from ${o.from} and --base ${o.base} \u2014 pass one`);
20626
+ }
20627
+ const fromRef = o.base ?? o.from;
20514
20628
  const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
20515
20629
  const wtPath = o.path ?? defaultWorktreePath(repoRoot2, branch);
20516
- const { base, fetchBranch } = resolveWorktreeBase(o.from, o.remote);
20630
+ const { base, fetchBranch } = resolveWorktreeBase(fromRef, o.remote);
20517
20631
  if (fetchBranch) {
20518
20632
  const fetchErr = await execFileP2("git", ["fetch", o.remote, fetchBranch], { timeout: GH_MUTATION_TIMEOUT_MS }).then(() => void 0).catch((e) => (e instanceof Error ? e.message : String(e)).split("\n")[0]);
20519
20633
  if (fetchErr) console.error(` warning: could not fetch ${o.remote}/${fetchBranch} (${fetchErr}); base ${base} may be stale`);
@@ -20969,7 +21083,7 @@ project.command("attest [owner/repo]").description("attest this repo's app-owned
20969
21083
  const res = await attestAppGaps(slugOf(target), repo, registryClientDeps(cfg));
20970
21084
  return reportWrite("project attest", res);
20971
21085
  });
20972
- project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--var <KEY=VALUE...>", settableVarHelp()).option("--secrets-file <path>", "read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (clear all with --unset secrets)").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets, edgeDomains, requiredGcpApis, publishRequired").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
21086
+ project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", "read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (clear all with --unset secrets)").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets, edgeDomains, requiredGcpApis, publishRequired").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
20973
21087
  const cfg = await loadConfig();
20974
21088
  let target;
20975
21089
  try {
@@ -20979,7 +21093,10 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
20979
21093
  }
20980
21094
  const slug = slugOf(target);
20981
21095
  const repo = target.includes("/") ? target : `mutmutco/${slug}`;
20982
- const vars = rawValues("--var");
21096
+ const setAlias = o.set ?? [];
21097
+ const vars = [...o.var ?? [], ...setAlias];
21098
+ const dupe = duplicateVarKeyAcrossFlags(o.var ?? [], setAlias);
21099
+ if (dupe) return fail(`project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
20983
21100
  if (o.secretsFile) {
20984
21101
  try {
20985
21102
  vars.push(`secrets=${(0, import_node_fs24.readFileSync)(o.secretsFile, "utf8")}`);
@@ -20995,7 +21112,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
20995
21112
  deployModel: o.deployModel,
20996
21113
  releaseTrack: o.releaseTrack,
20997
21114
  vars,
20998
- unsets: rawValues("--unset"),
21115
+ unsets: o.unset ?? [],
20999
21116
  clearWebProfile: Boolean(o.clearWebProfile)
21000
21117
  });
21001
21118
  } catch (e) {
@@ -21614,7 +21731,13 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
21614
21731
  async function listCiWorkflowPaths(cwd = process.cwd()) {
21615
21732
  const wfDir = (0, import_node_path23.join)(cwd, ".github", "workflows");
21616
21733
  if (!(0, import_node_fs24.existsSync)(wfDir)) return [];
21617
- return (0, import_node_fs24.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
21734
+ return (0, import_node_fs24.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
21735
+ try {
21736
+ return workflowReportsPrChecks((0, import_node_fs24.readFileSync)((0, import_node_path23.join)(wfDir, name), "utf8"));
21737
+ } catch {
21738
+ return true;
21739
+ }
21740
+ }).map((name) => `.github/workflows/${name}`);
21618
21741
  }
21619
21742
  async function resolveMergeCiPolicyForCheckout(repoOpt) {
21620
21743
  const repo = repoOpt ?? await resolveRepo();
@@ -21671,17 +21794,30 @@ async function pollGhPrChecks(prNumber, repoArgs) {
21671
21794
  }
21672
21795
  return state;
21673
21796
  }
21674
- pr.command("checks-wait <number>").description("bounded wait for PR checks; skips immediately on no-ci repos (#1432)").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (number, o) => {
21797
+ pr.command("checks-wait <number>").description(`bounded wait for PR checks; skips immediately on no-ci repos (#1432). Default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m; --timeout raises it. Exit 1 = a check FAILED, exit ${PR_CHECKS_TIMEOUT_EXIT_CODE} = the wait window expired (re-arm)`).option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--timeout <minutes>", `wait budget in minutes (default ${PR_CHECKS_TIMEOUT_MS / 6e4}) \u2014 raise it for serial self-hosted e2e queues`).action(async (number, o) => {
21675
21798
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
21799
+ let timeoutMs;
21800
+ if (o.timeout !== void 0) {
21801
+ const minutes = Number(o.timeout);
21802
+ if (!Number.isFinite(minutes) || minutes <= 0) return fail("pr checks-wait: --timeout must be a positive number of minutes");
21803
+ timeoutMs = Math.round(minutes * 6e4);
21804
+ }
21676
21805
  const result = await waitForPrChecks({
21677
21806
  resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
21678
21807
  pollChecks: () => pollGhPrChecks(number, repoArgs),
21679
21808
  sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
21680
- log: (message) => console.warn(message)
21809
+ log: (message) => console.warn(message),
21810
+ timeoutMs,
21811
+ // Liveness on stderr, one line per poll. A silent bounded wait is indistinguishable from a hang, and
21812
+ // an agent harness kills it on its own (shorter) deadline before the verdict ever prints (#2940).
21813
+ progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr checks-wait: ${state} \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
21681
21814
  });
21682
21815
  if (o.json) printLine(JSON.stringify(result));
21683
- else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
21684
- if (result.status === "failure" || result.status === "timeout") process.exitCode = 1;
21816
+ else if (result.status === "timeout") {
21817
+ printLine(`pr checks-wait: timeout \u2014 waited ${Math.round((result.waitedMs ?? 0) / 6e4)}m, last state: ${result.detail ?? "pending"}. No check failed; re-run to keep waiting, or pass --timeout <minutes>.`);
21818
+ } else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
21819
+ if (result.status === "failure") process.exitCode = 1;
21820
+ if (result.status === "timeout") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
21685
21821
  });
21686
21822
  async function ghMergeAutoEnqueue(prNumber, repo, method) {
21687
21823
  const args = repo ? ["--repo", repo] : [];
@@ -21764,7 +21900,10 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
21764
21900
  resolvePolicy: () => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
21765
21901
  pollChecks: () => pollGhPrChecks(prNumber, repo ? ["--repo", repo] : []),
21766
21902
  sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
21767
- log: (message) => console.warn(message)
21903
+ log: (message) => console.warn(message),
21904
+ // `pr land` inherits the same (raised) checks budget, so it needs the same liveness — otherwise the
21905
+ // 30m wait is SILENT and reads exactly like the hang #2940 was filed about, only three times longer.
21906
+ progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr land: waiting on checks \u2014 ${state}, ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
21768
21907
  }),
21769
21908
  // #2425: re-probe used ONLY to decide whether a failed `gh pr merge --auto` is worth retrying — a
21770
21909
  // fresh read of state/mergeable/checks, independent of the merge call's own (possibly stale) error.
@@ -21946,6 +22085,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
21946
22085
  }
21947
22086
  const remoteBefore = await remoteBranchExists2(headRef);
21948
22087
  let remoteDeleteAttempted = false;
22088
+ let upgradedToAuto = false;
21949
22089
  let remoteNotAttemptedReason = headIsProtected ? "protected-branch" : void 0;
21950
22090
  await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: o.auto, deleteBranch: !headIsProtected }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch(async (e) => {
21951
22091
  const message = String(e.message || "");
@@ -21969,14 +22109,30 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
21969
22109
  return;
21970
22110
  }
21971
22111
  if (!o.auto && basePolicyBlocksImmediateMerge(message)) {
21972
- throw new Error(`gh pr merge ${number}: the base-branch policy blocks an immediate merge \u2014 re-run with --auto to merge once required checks pass.`);
22112
+ console.warn(`pr merge: the base-branch policy blocks an immediate merge \u2014 upgrading to --auto (merges once required checks pass).`);
22113
+ upgradedToAuto = true;
22114
+ await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: true, deleteBranch: !headIsProtected }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
22115
+ const m2 = String(e2.message || "");
22116
+ if (/already been merged/i.test(m2)) {
22117
+ remoteNotAttemptedReason = "pr-already-merged";
22118
+ return;
22119
+ }
22120
+ const note2 = timeoutKillNote(e2, GH_MUTATION_TIMEOUT_MS);
22121
+ if (note2) throw new Error(`gh pr merge ${number}: ${note2}`);
22122
+ if (!ghPrMergeLocalBranchDeleteWarning(m2)) throw e2;
22123
+ });
22124
+ return;
21973
22125
  }
21974
22126
  if (!ghPrMergeLocalBranchDeleteWarning(message)) throw e;
21975
22127
  });
21976
- if (o.auto) {
22128
+ if (o.auto || upgradedToAuto) {
21977
22129
  const state = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS5 }).catch(() => ({ stdout: "" }))).stdout.trim();
21978
22130
  if (state !== "MERGED") {
21979
- console.log(JSON.stringify({ mergeStatus: "auto-merge-enqueued", pr: number, branch: headRef, state: state || "unknown" }));
22131
+ console.log(JSON.stringify({ mergeStatus: "auto-merge-enqueued", pr: number, branch: headRef, state: state || "unknown", upgradedToAuto: upgradedToAuto || void 0 }));
22132
+ if (upgradedToAuto && !o.auto) {
22133
+ console.warn(`pr merge: PR #${number} is ENQUEUED, not merged \u2014 auto-merge will land it once required checks pass. Exiting ${PR_MERGE_ENQUEUED_EXIT_CODE} so a chained command does not treat this as a completed merge.`);
22134
+ process.exitCode = PR_MERGE_ENQUEUED_EXIT_CODE;
22135
+ }
21980
22136
  return;
21981
22137
  }
21982
22138
  }
@@ -22310,19 +22466,6 @@ function rawValue(flag, fallback) {
22310
22466
  const inline = process.argv.find((arg) => arg.startsWith(`${flag}=`));
22311
22467
  return inline ? inline.slice(flag.length + 1) : fallback;
22312
22468
  }
22313
- function rawValues(flag) {
22314
- const out = [];
22315
- for (let i = 0; i < process.argv.length; i++) {
22316
- const arg = process.argv[i];
22317
- if (arg === flag && process.argv[i + 1]) {
22318
- out.push(process.argv[i + 1]);
22319
- i++;
22320
- continue;
22321
- }
22322
- if (arg.startsWith(`${flag}=`)) out.push(arg.slice(flag.length + 1));
22323
- }
22324
- return out;
22325
- }
22326
22469
  function stagePortFromArgv() {
22327
22470
  const raw = rawValue("--port", "");
22328
22471
  if (!raw) return void 0;
@@ -22890,7 +23033,7 @@ bootstrap.command("org-ruleset").description("drift-check the codified org no-ag
22890
23033
  else console.log(renderOrgRulesetDriftReport(plan));
22891
23034
  if (plan.action !== "noop") process.exitCode = 1;
22892
23035
  });
22893
- bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").option("--class <class>", "deployable | content", "deployable").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo) => {
23036
+ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").option("--class <class>", "deployable | content", "deployable").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
22894
23037
  const o = {
22895
23038
  class: rawValue("--class", "deployable"),
22896
23039
  projectType: rawValue("--project-type", ""),
@@ -22916,7 +23059,7 @@ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: i
22916
23059
  const readFile4 = (p) => (0, import_node_fs24.existsSync)(p) ? (0, import_node_fs24.readFileSync)(p, "utf8") : null;
22917
23060
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
22918
23061
  const rawVars = {};
22919
- for (const value of rawValues("--var")) {
23062
+ for (const value of cmdOpts.var ?? []) {
22920
23063
  const eq = value.indexOf("=");
22921
23064
  if (eq > 0) rawVars[value.slice(0, eq)] = value.slice(eq + 1);
22922
23065
  }
@@ -22937,6 +23080,7 @@ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: i
22937
23080
  const actions = [];
22938
23081
  const applied = [];
22939
23082
  let applyDeployModel;
23083
+ let applyProjectType;
22940
23084
  try {
22941
23085
  const payload = buildRegisterPayload(repo, o.class, vars, {
22942
23086
  projectType: o.projectType || void 0,
@@ -22944,6 +23088,7 @@ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: i
22944
23088
  releaseTrack: o.releaseTrack || void 0
22945
23089
  });
22946
23090
  applyDeployModel = payload.deployModel;
23091
+ applyProjectType = payload.projectType;
22947
23092
  } catch {
22948
23093
  }
22949
23094
  let seedPlan = { mode: "direct", ref: baseBranch, reason: "dry-run (no protection probe)" };
@@ -22968,6 +23113,7 @@ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: i
22968
23113
  for (const seed of manifest.seeds) {
22969
23114
  if (!seed.classes.includes(o.class)) continue;
22970
23115
  if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
23116
+ if (!seedMatchesProjectType(seed, applyProjectType)) continue;
22971
23117
  const resolved = { ...seed, target: seed.target.replace("{{REPO_SLUG}}", slug) };
22972
23118
  let exists = false;
22973
23119
  let sha;
@@ -22991,7 +23137,7 @@ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: i
22991
23137
  const planned = planSeedAction(resolved, exists);
22992
23138
  const isBlock = resolved.source === "managed-block";
22993
23139
  const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile4) : null;
22994
- const action = reconcileSeedAction(planned, content, isBlock);
23140
+ const action = reconcileSeedAction(planned, content, isBlock, remoteContent);
22995
23141
  actions.push(action);
22996
23142
  if (o.execute && (action.action === "create" || action.action === "update")) {
22997
23143
  await gh(contentPutArgs(repo, resolved.target, content, seedPlan.ref, action.action === "update" ? sha : void 0));
@@ -23024,10 +23170,17 @@ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: i
23024
23170
  ]);
23025
23171
  seedPrUrl = created.url;
23026
23172
  }
23173
+ let autoMergeEnabled = true;
23027
23174
  await gh(["pr", "merge", seedPrUrl, "--repo", repo, "--auto", "--squash"]).catch((e) => {
23028
- if (!/already/i.test(String(e.message ?? ""))) throw e;
23175
+ const message = String(e.message ?? "");
23176
+ if (/already/i.test(message)) return;
23177
+ if (/clean status|enablePullRequestAutoMerge/i.test(message)) {
23178
+ autoMergeEnabled = false;
23179
+ return;
23180
+ }
23181
+ throw e;
23029
23182
  });
23030
- applied.push(`seed: PR ${seedPrUrl} (base ${baseBranch} protected; auto-merge enabled)`);
23183
+ applied.push(autoMergeEnabled ? `seed: PR ${seedPrUrl} (base ${baseBranch} protected; auto-merge enabled)` : `seed: PR ${seedPrUrl} (base ${baseBranch} protected; auto-merge refused \u2014 PR is already clean. Land it: mmi-cli pr land <n> --repo ${repo})`);
23031
23184
  }
23032
23185
  if (o.execute && o.class === "deployable") {
23033
23186
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.25.0",
3
+ "version": "3.27.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 plugin's session-start hook drives.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",