@mutmutco/cli 3.6.0 → 3.7.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 +82 -139
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -5897,7 +5897,7 @@ function parseGhPrChecksOutput(stdout) {
5897
5897
  if (!lines.length) return "no-checks-reported";
5898
5898
  let anyPending = false;
5899
5899
  for (const line of lines) {
5900
- const parts = line.split(/\s+/);
5900
+ const parts = line.includes(" ") ? line.split(" ") : line.split(/\s+/);
5901
5901
  const state = (parts[1] ?? "").toLowerCase();
5902
5902
  if (state === "fail" || state === "failure" || state === "error") return "failure";
5903
5903
  if (state === "pass" || state === "success" || state === "skipping" || state === "skipped") continue;
@@ -5916,22 +5916,46 @@ var PR_CHECKS_POLL_MS = 3e4;
5916
5916
  var PR_CHECKS_TIMEOUT_MS = 10 * 6e4;
5917
5917
  var PR_CHECKS_SUCCESS_CONFIRMATIONS = 2;
5918
5918
  var PR_CHECKS_CONFIRM_MS = 5e3;
5919
+ var NO_CI_LIVE_CHECKS_CONTRADICTION_REASON = "ci:none contradicted by live check runs on PR head";
5920
+ function decidePrMergeNoCiGuard(checks, policyReason = "registry META ci:none") {
5921
+ if (checks === "no-checks-reported") return { action: "proceed" };
5922
+ const staleNote = `registry says no-ci (${policyReason}), but live PR checks exist`;
5923
+ if (checks === "success") {
5924
+ return { action: "proceed", note: `${staleNote}; proceeding because they are green` };
5925
+ }
5926
+ return {
5927
+ action: "refuse",
5928
+ message: [
5929
+ `${staleNote} and are not green (${checks}).`,
5930
+ "Fix the checks or use `mmi-cli pr land` so the CLI waits.",
5931
+ "Then correct registry META: `mmi-cli project set <owner/repo> --var requiredChecks=<check-run-display-names>`."
5932
+ ].join(" ")
5933
+ };
5934
+ }
5919
5935
  async function waitForPrChecks(deps) {
5920
- const { policy, reason } = await deps.resolvePolicy();
5936
+ let { policy, reason } = await deps.resolvePolicy();
5937
+ const queuedStates = [];
5921
5938
  if (policy === "no-ci") {
5922
- return { policy, status: "skipped", reason };
5939
+ const firstState = await deps.pollChecks();
5940
+ if (firstState === "no-checks-reported") {
5941
+ return { policy, status: "skipped", reason };
5942
+ }
5943
+ reason = NO_CI_LIVE_CHECKS_CONTRADICTION_REASON;
5944
+ deps.log?.(`merge CI policy contradiction: ${reason}; waiting for live PR checks`);
5945
+ policy = "wait-for-checks";
5946
+ queuedStates.push(firstState);
5923
5947
  }
5924
5948
  const now = deps.now ?? (() => Date.now());
5925
5949
  const deadline = now() + PR_CHECKS_TIMEOUT_MS;
5926
5950
  let lastDetail = "pending";
5927
5951
  let successStreak = 0;
5928
5952
  while (now() < deadline) {
5929
- const state = await deps.pollChecks();
5930
- if (state === "failure") return { policy, status: "failure", detail: lastDetail };
5953
+ const state = queuedStates.shift() ?? await deps.pollChecks();
5954
+ if (state === "failure") return { policy, status: "failure", reason, detail: lastDetail };
5931
5955
  if (state === "success") {
5932
5956
  successStreak += 1;
5933
5957
  if (successStreak >= PR_CHECKS_SUCCESS_CONFIRMATIONS) {
5934
- return { policy, status: "success", detail: "checks-success" };
5958
+ return { policy, status: "success", reason, detail: "checks-success" };
5935
5959
  }
5936
5960
  lastDetail = "confirming-success";
5937
5961
  await deps.sleep(PR_CHECKS_CONFIRM_MS);
@@ -5946,7 +5970,7 @@ async function waitForPrChecks(deps) {
5946
5970
  lastDetail = state;
5947
5971
  await deps.sleep(PR_CHECKS_POLL_MS);
5948
5972
  }
5949
- return { policy, status: "timeout", detail: lastDetail };
5973
+ return { policy, status: "timeout", reason, detail: lastDetail };
5950
5974
  }
5951
5975
 
5952
5976
  // src/bootstrap-ruleset.ts
@@ -6736,6 +6760,22 @@ async function auditRepoCi(repo, deps) {
6736
6760
  detail: info.delete_branch_on_merge === true ? void 0 : "false or unavailable"
6737
6761
  });
6738
6762
  const hasGateWorkflow = repoClass === "hub" ? true : repoClass === "content" ? true : await contentExists(deps, repo, baseBranch, PRODUCT_GATE_PATH);
6763
+ let emittedPrContexts;
6764
+ const getEmittedPrContexts = async () => {
6765
+ emittedPrContexts ??= await resolveEmittedPrContexts(deps, repo, baseBranch);
6766
+ return emittedPrContexts;
6767
+ };
6768
+ if (explicitNoCi) {
6769
+ const emitted = await getEmittedPrContexts();
6770
+ if (emitted.length > 0) {
6771
+ checks.push({
6772
+ ok: false,
6773
+ label: "registry no-ci contradicts PR workflows",
6774
+ detail: `registry META declares no-ci but pull_request workflows emit [${emitted.join(", ")}]`,
6775
+ remediation: `Fix the workflows or correct registry META: mmi-cli project set ${repo} --var requiredChecks=${JSON.stringify(emitted)}`
6776
+ });
6777
+ }
6778
+ }
6739
6779
  if (deployableGated) {
6740
6780
  checks.push({
6741
6781
  ok: hasGateWorkflow,
@@ -6770,7 +6810,7 @@ async function auditRepoCi(repo, deps) {
6770
6810
  remediation: hasRequiredChecks ? void 0 : `Import ${PRODUCT_RULESET_REF} as an active repository ruleset (GitHub \u2192 Settings \u2192 Rules \u2192 Rulesets) \u2014 target: bootstrap --apply automation (#1440)`
6771
6811
  });
6772
6812
  if (hasGateWorkflow) {
6773
- const emitted = registryRequiredContexts(meta) ?? await resolveEmittedPrContexts(deps, repo, baseBranch);
6813
+ const emitted = registryRequiredContexts(meta) ?? await getEmittedPrContexts();
6774
6814
  if (emitted.length > 0) {
6775
6815
  const aligned = contextsMatchRuleset(statusChecks, new Set(emitted));
6776
6816
  checks.push({
@@ -13365,11 +13405,12 @@ function authorizeBodyHasMismatch(body) {
13365
13405
  }
13366
13406
 
13367
13407
  // src/project-set.ts
13368
- var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "gate"];
13408
+ var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "gate"];
13369
13409
  var UNSET_KEY_SET = new Set(UNSET_KEYS);
13370
13410
  var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
13371
- var SECRET_CONSUMERS = ["runtime", "lambda", "actions", "agent", "box"];
13411
+ var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
13372
13412
  var SECRET_ENV_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
13413
+ var BUILD_SECRET_REF_RE = /^(?:[A-Z_][A-Z0-9_]*=)?(?:[A-Z_][A-Z0-9_]*|(?:mm-fofu|_org\/[a-z0-9][a-z0-9-]*):[A-Z_][A-Z0-9_]*|@github-packages-token)$/;
13373
13414
  function parseSecretsCatalogVar(raw) {
13374
13415
  let parsed;
13375
13416
  try {
@@ -13433,6 +13474,21 @@ function parseRuntimeSecretsVar(raw) {
13433
13474
  }
13434
13475
  return out;
13435
13476
  }
13477
+ function parseBuildSecretsVar(raw) {
13478
+ let parsed;
13479
+ try {
13480
+ parsed = JSON.parse(raw);
13481
+ } catch {
13482
+ throw new Error('project set: requiredBuildSecrets must be JSON, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]');
13483
+ }
13484
+ if (!Array.isArray(parsed)) {
13485
+ throw new Error("project set: requiredBuildSecrets must be a flat array (build secrets are not stage-mapped)");
13486
+ }
13487
+ if (parsed.some((n) => typeof n !== "string" || !BUILD_SECRET_REF_RE.test(n) || n === "@github-packages-token")) {
13488
+ throw new Error("project set: requiredBuildSecrets must be a flat array of ENVID=<ref> or bare <ref> strings");
13489
+ }
13490
+ return parsed;
13491
+ }
13436
13492
  function parseEdgeDomainsVar(raw) {
13437
13493
  let parsed;
13438
13494
  try {
@@ -13657,6 +13713,7 @@ var SETTABLE_VAR_KEYS = [
13657
13713
  "consumesDesignSystem",
13658
13714
  "requiredGcpApis",
13659
13715
  "requiredRuntimeSecrets",
13716
+ "requiredBuildSecrets",
13660
13717
  "edgeDomains",
13661
13718
  "statusFieldId",
13662
13719
  "statusOptions",
@@ -13680,7 +13737,8 @@ var SETTABLE_VAR_HINTS = {
13680
13737
  oauth: "JSON {subdomains,domains,callbackPath,fofuSubdomain}",
13681
13738
  requiredGcpApis: "comma-string",
13682
13739
  requiredRuntimeSecrets: 'JSON stage map, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}',
13683
- secrets: "JSON catalog map keyed by KEY {key,purpose,group,owner,stages[],consumers[]} \u2014 prefer --secrets-file",
13740
+ requiredBuildSecrets: 'JSON flat array, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]',
13741
+ secrets: "JSON catalog map keyed by KEY {key,purpose,group,owner,stages[],consumers[]} \u2014 merged per entry; clear with --unset secrets; prefer --secrets-file",
13684
13742
  edgeDomains: "JSON {dev,rc,main} domain map",
13685
13743
  statusOptions: "JSON name\u2192id map",
13686
13744
  priorityOptions: "JSON {Urgent,High,Medium,Low}\u2192id map",
@@ -13736,6 +13794,8 @@ function buildProjectSetPatch(input) {
13736
13794
  patch[key] = n;
13737
13795
  } else if (key === "requiredRuntimeSecrets") {
13738
13796
  patch[key] = parseRuntimeSecretsVar(raw);
13797
+ } else if (key === "requiredBuildSecrets") {
13798
+ patch[key] = parseBuildSecretsVar(raw);
13739
13799
  } else if (key === "secrets") {
13740
13800
  patch[key] = parseSecretsCatalogVar(raw);
13741
13801
  } else if (key === "edgeDomains") {
@@ -15715,59 +15775,6 @@ function buildInstalledPluginVersionCheck(input) {
15715
15775
  staleSurfaces: stale
15716
15776
  };
15717
15777
  }
15718
- var MANAGED_PLUGINS = [
15719
- {
15720
- // Marketplace-qualified key exactly as installed_plugins.json stores it (same convention as
15721
- // MMI_PLUGIN_ID = 'mmi@mutmutco') — a bare 'jervaise-powertools' would never match a real record
15722
- // and the check would be a permanent no-op. The same `<marketplace>/<name>` segments also address its
15723
- // Cursor Team Marketplace cache dir (~/.cursor/plugins/cache/jervaise/jervaise-powertools/<version>/).
15724
- id: "jervaise-powertools@jervaise",
15725
- label: "jervaise-powertools",
15726
- healCommand: "jerv-cli doctor --apply",
15727
- opencodePackage: "@jervaise/opencode-jerv"
15728
- }
15729
- ];
15730
- function parseManagedPluginId(id) {
15731
- const at = id.lastIndexOf("@");
15732
- if (at <= 0 || at === id.length - 1) return null;
15733
- return { name: id.slice(0, at), marketplace: id.slice(at + 1) };
15734
- }
15735
- var MANAGED_PLUGIN_DRIFT_LABEL = "managed org plugin version drift (cross-surface)";
15736
- function managedPluginDriftFix(drifted) {
15737
- return drifted.map((d) => {
15738
- const versions = d.versions.map((v) => `${v.version} (${v.surface})`).join(" vs ");
15739
- return `${d.label} drift: ${versions} \u2192 run \`${d.healCommand}\``;
15740
- }).join(" ; ");
15741
- }
15742
- function buildManagedPluginDriftCheck(input) {
15743
- const base = { ok: true, label: MANAGED_PLUGIN_DRIFT_LABEL, fix: "" };
15744
- if (!input.isOrgRepo) return base;
15745
- const registry2 = input.plugins ?? MANAGED_PLUGINS;
15746
- const drifted = [];
15747
- const normalize = (v) => v.replace(/^v/, "");
15748
- for (const descriptor of registry2) {
15749
- const versions = [];
15750
- for (const source of input.sources) {
15751
- if (source.directVersions && descriptor.id in source.directVersions) {
15752
- const version2 = source.directVersions[descriptor.id];
15753
- if (isSemverVersion(version2)) versions.push({ surface: source.surface, version: normalize(version2) });
15754
- continue;
15755
- }
15756
- const records = source.installed?.plugins?.[descriptor.id];
15757
- if (!Array.isArray(records) || records.length === 0) continue;
15758
- const recordVersion = bestRecord(records).version;
15759
- const version = isSemverVersion(recordVersion) ? recordVersion : highestSemver(source.cacheVersions?.[descriptor.id] ?? []);
15760
- if (!isSemverVersion(version)) continue;
15761
- versions.push({ surface: source.surface, version: normalize(version) });
15762
- }
15763
- if (versions.length < 2) continue;
15764
- const distinctVersions = new Set(versions.map((v) => v.version));
15765
- if (distinctVersions.size < 2) continue;
15766
- drifted.push({ pluginId: descriptor.id, label: descriptor.label, healCommand: descriptor.healCommand, versions });
15767
- }
15768
- if (drifted.length === 0) return base;
15769
- return { ...base, ok: false, fix: managedPluginDriftFix(drifted), drifted };
15770
- }
15771
15778
  var OPENCODE_VERSION_LABEL = "installed OpenCode MMI adapter version (vs latest release)";
15772
15779
  function buildOpencodeVersionCheck(input) {
15773
15780
  const fix = pluginRecoveryFix("opencode");
@@ -17235,77 +17242,6 @@ function installedPluginSources() {
17235
17242
  }
17236
17243
  });
17237
17244
  }
17238
- function managedPluginSurfaceSources() {
17239
- const claudeCodex = installedPluginSources().map(({ surface, installed }) => {
17240
- const cacheVersions = {};
17241
- for (const descriptor of MANAGED_PLUGINS) {
17242
- const segments = parseManagedPluginId(descriptor.id);
17243
- if (!segments) continue;
17244
- try {
17245
- cacheVersions[descriptor.id] = (0, import_node_fs18.readdirSync)(
17246
- (0, import_node_path18.join)((0, import_node_os7.homedir)(), `.${surface}`, "plugins", "cache", segments.marketplace, segments.name),
17247
- { withFileTypes: true }
17248
- ).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
17249
- } catch {
17250
- }
17251
- }
17252
- return { surface, installed, cacheVersions };
17253
- });
17254
- return [
17255
- ...claudeCodex,
17256
- { surface: "cursor", directVersions: managedPluginCursorDirectVersions() },
17257
- { surface: "opencode", directVersions: managedPluginOpencodeDirectVersions() }
17258
- ];
17259
- }
17260
- function managedPluginCursorDirectVersions() {
17261
- const out = {};
17262
- for (const descriptor of MANAGED_PLUGINS) {
17263
- const segments = parseManagedPluginId(descriptor.id);
17264
- if (!segments) continue;
17265
- try {
17266
- const versions = (0, import_node_fs18.readdirSync)(
17267
- (0, import_node_path18.join)((0, import_node_os7.homedir)(), ".cursor", "plugins", "cache", segments.marketplace, segments.name),
17268
- { withFileTypes: true }
17269
- ).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name);
17270
- out[descriptor.id] = highestSemver(versions);
17271
- } catch {
17272
- }
17273
- }
17274
- return out;
17275
- }
17276
- function managedPluginOpencodeDirectVersions() {
17277
- const out = {};
17278
- for (const descriptor of MANAGED_PLUGINS) {
17279
- if (!descriptor.opencodePackage) continue;
17280
- out[descriptor.id] = readOpencodePackageVersion(descriptor.opencodePackage);
17281
- }
17282
- return out;
17283
- }
17284
- function readOpencodePackageVersionFrom(packageJsonPath) {
17285
- try {
17286
- const parsed = JSON.parse((0, import_node_fs18.readFileSync)(packageJsonPath, "utf8"));
17287
- return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : void 0;
17288
- } catch {
17289
- return void 0;
17290
- }
17291
- }
17292
- function readOpencodePackageVersion(packageName) {
17293
- const [scope, name] = packageName.startsWith("@") ? packageName.slice(1).split("/", 2) : [void 0, packageName];
17294
- if (!name) return void 0;
17295
- const diskCandidates = [
17296
- (0, import_node_path18.join)(opencodeConfigDir(), "node_modules", ...scope ? [`@${scope}`] : [], name, "package.json"),
17297
- (0, import_node_path18.join)((0, import_node_os7.homedir)(), ".cache", "opencode", "node_modules", ...scope ? [`@${scope}`] : [], name, "package.json")
17298
- ];
17299
- const diskVersion = diskCandidates.map(readOpencodePackageVersionFrom).find((v) => v !== void 0);
17300
- const packagesRoot = (0, import_node_path18.join)((0, import_node_os7.homedir)(), ".cache", "opencode", "packages", ...scope ? [`@${scope}`] : []);
17301
- let cacheVersion;
17302
- try {
17303
- const cacheVersions = (0, import_node_fs18.readdirSync)(packagesRoot, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.startsWith(`${name}@`) && !e.name.startsWith(".")).map((e) => readOpencodePackageVersionFrom((0, import_node_path18.join)(packagesRoot, e.name, "node_modules", ...scope ? [`@${scope}`] : [], name, "package.json"))).filter((v) => Boolean(v));
17304
- if (cacheVersions.length) cacheVersion = cacheVersions.reduce((lowest, v) => compareVersions(v, lowest) < 0 ? v : lowest);
17305
- } catch {
17306
- }
17307
- return pickOpencodeActiveVersion({ cacheVersion, diskVersion });
17308
- }
17309
17245
  function readClaudeSettings() {
17310
17246
  try {
17311
17247
  return JSON.parse((0, import_node_fs18.readFileSync)((0, import_node_path18.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
@@ -18192,7 +18128,6 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
18192
18128
  }
18193
18129
  }
18194
18130
  checks.push(installedVersionCheck);
18195
- checks.push(buildManagedPluginDriftCheck({ isOrgRepo, sources: managedPluginSurfaceSources() }));
18196
18131
  let openCodeConfigSnapshot = opencodeConfigSnapshot();
18197
18132
  const inspectOpenCode = surface === "opencode" || openCodeConfigSnapshot.hasConfig || runExtended;
18198
18133
  if (inspectOpenCode) {
@@ -19551,7 +19486,7 @@ project.command("attest [owner/repo]").description("attest this repo's app-owned
19551
19486
  const res = await attestAppGaps(slugOf(target), repo, registryClientDeps(cfg));
19552
19487
  return reportWrite("project attest", res);
19553
19488
  });
19554
- 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 set it as --var secrets (robust for large maps)").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, 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) => {
19489
+ 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) => {
19555
19490
  const cfg = await loadConfig();
19556
19491
  let target;
19557
19492
  try {
@@ -20185,7 +20120,8 @@ pr.command("checks-wait <number>").description("bounded wait for PR checks; skip
20185
20120
  const result = await waitForPrChecks({
20186
20121
  resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
20187
20122
  pollChecks: () => pollGhPrChecks(number, repoArgs),
20188
- sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms))
20123
+ sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
20124
+ log: (message) => console.warn(message)
20189
20125
  });
20190
20126
  if (o.json) printLine(JSON.stringify(result));
20191
20127
  else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
@@ -20212,7 +20148,8 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
20212
20148
  waitForChecks: (prNumber, repo) => waitForPrChecks({
20213
20149
  resolvePolicy: () => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
20214
20150
  pollChecks: () => pollGhPrChecks(prNumber, repo ? ["--repo", repo] : []),
20215
- sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms))
20151
+ sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
20152
+ log: (message) => console.warn(message)
20216
20153
  }),
20217
20154
  // #2425: re-probe used ONLY to decide whether a failed `gh pr merge --auto` is worth retrying — a
20218
20155
  // fresh read of state/mergeable/checks, independent of the merge call's own (possibly stale) error.
@@ -20423,6 +20360,12 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
20423
20360
  const beforeWorktrees = parseWorktreePorcelain(
20424
20361
  (await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
20425
20362
  );
20363
+ const ciPolicy = await resolveMergeCiPolicyForCheckout(o.repo);
20364
+ if (ciPolicy.policy === "no-ci") {
20365
+ const guard = decidePrMergeNoCiGuard(await pollGhPrChecks(number, repoArgs), ciPolicy.reason);
20366
+ if (guard.action === "refuse") throw new Error(`gh pr merge ${number}: ${guard.message}`);
20367
+ if (guard.note) console.warn(`pr merge: ${guard.note}`);
20368
+ }
20426
20369
  const remoteBefore = await remoteBranchExists2(headRef);
20427
20370
  let remoteDeleteAttempted = false;
20428
20371
  let remoteNotAttemptedReason;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.6.0",
3
+ "version": "3.7.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",