@mutmutco/claude-plugin 4.5.149 → 4.5.150

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mmi",
3
3
  "displayName": "MMI",
4
- "version": "4.5.149",
4
+ "version": "4.5.150",
5
5
  "mmiCompat": "4.x",
6
6
  "description": "MMI workflow skills and org gates delivery.",
7
7
  "author": {
package/cli/dist/main.cjs CHANGED
@@ -16296,6 +16296,9 @@ function checkRunbookClaims(root, docs, deps) {
16296
16296
  }
16297
16297
  return findings;
16298
16298
  }
16299
+ function localRunbookBase(refExists) {
16300
+ return ["origin/development", "origin/main"].find(refExists);
16301
+ }
16299
16302
  function checkRunbookCoverage(docs, base, git3) {
16300
16303
  const changed = git3(["diff", "--name-only", `${base}...HEAD`]).split(/\r?\n/).filter(Boolean);
16301
16304
  const findings = [];
@@ -18666,10 +18669,10 @@ var rollout_plan_default = {
18666
18669
  note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
18667
18670
  },
18668
18671
  baseline: {
18669
- version: "4.5.149",
18670
- tag: "v4.5.149",
18671
- commit: "b6fc6812e054",
18672
- npm: "@mutmutco/cli@4.5.149"
18672
+ version: "4.5.150",
18673
+ tag: "v4.5.150",
18674
+ commit: "4dc71e3761ec",
18675
+ npm: "@mutmutco/cli@4.5.150"
18673
18676
  },
18674
18677
  exitCriterion: "fleet-n-of-n",
18675
18678
  hubOnlyShortcut: "forbidden",
@@ -18686,14 +18689,14 @@ var rollout_plan_default = {
18686
18689
  repo: "mutmutco/mmi-hub",
18687
18690
  role: "canary",
18688
18691
  schedule: "train",
18689
- v3Target: "v4.5.149"
18692
+ v3Target: "v4.5.150"
18690
18693
  }
18691
18694
  ],
18692
18695
  rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
18693
18696
  rollback: {
18694
18697
  independent: true,
18695
- mechanism: "npm dist-tag latest -> 4.5.149 and redeploy the Hub Lambda from tag v4.5.149 (b6fc6812e054); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
18696
- v3Target: "v4.5.149 (@mutmutco/cli@4.5.149, tag commit b6fc6812e054 \u2014 last known-good release carrying the repo-index v4-only contract)"
18698
+ mechanism: "npm dist-tag latest -> 4.5.150 and redeploy the Hub Lambda from tag v4.5.150 (4dc71e3761ec); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
18699
+ v3Target: "v4.5.150 (@mutmutco/cli@4.5.150, tag commit 4dc71e3761ec \u2014 last known-good release carrying the repo-index v4-only contract)"
18697
18700
  }
18698
18701
  },
18699
18702
  {
@@ -40299,7 +40302,8 @@ function boardAdvanceFailureMessage(result) {
40299
40302
  }
40300
40303
 
40301
40304
  // src/pr-land.ts
40302
- var PR_LAND_QUEUE_TIMEOUT_MS = 45 * 6e4;
40305
+ var PR_LAND_QUEUE_TIMEOUT_MS = 75 * 6e4;
40306
+ var PR_LAND_QUEUE_BATCH_SIZE = 5;
40303
40307
  function prHeadBehindBase(input) {
40304
40308
  if ((input.mergeStateStatus ?? "").toUpperCase() === "BEHIND") return true;
40305
40309
  return /out[- ]of[- ]date|not up to date|up to date with the base|update (?:the|your) branch/i.test(input.message ?? "");
@@ -40353,6 +40357,12 @@ async function updatePrHeadFromBase(input) {
40353
40357
  throw new Error(`GitHub accepted the update of ${input.head} from ${input.base} but the head did not move \u2014 check the PR, then rerun`);
40354
40358
  }
40355
40359
  var PR_LAND_POLL_MS = 6e4;
40360
+ var PR_LAND_POLL_MAX_MS = 5 * 6e4;
40361
+ function prLandPollDelayMs(attempt, waiting) {
40362
+ const load = Math.max(1, Math.ceil(waiting / PR_LAND_QUEUE_BATCH_SIZE));
40363
+ const age = Math.min(2, 1 + Math.floor(attempt / 15) * 0.5);
40364
+ return Math.min(PR_LAND_POLL_MAX_MS, Math.round(PR_LAND_POLL_MS * load * age));
40365
+ }
40356
40366
  var PR_LAND_EJECT_CONFIRM_MS = 2e4;
40357
40367
  var PR_LAND_ENQUEUE_TIMEOUT_MS = 10 * 6e4;
40358
40368
  var PR_LAND_STATE_READ_RETRIES = 3;
@@ -40474,8 +40484,8 @@ async function runPrLand(prNumber, options, deps) {
40474
40484
  if (merge.mergeStatus === "merged") {
40475
40485
  return queueMerged(base, deps);
40476
40486
  }
40477
- const deadline = (deps.now ?? (() => Date.now()))() + PR_LAND_QUEUE_TIMEOUT_MS;
40478
- await (deps.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms))))(PR_LAND_POLL_MS);
40487
+ const deadline = (deps.now ?? (() => Date.now()))() + (options.queueTimeoutMs ?? PR_LAND_QUEUE_TIMEOUT_MS);
40488
+ await (deps.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms))))(deps.pollDelayMs?.(0) ?? PR_LAND_POLL_MS);
40479
40489
  return pollPrMergeQueue(prNumber, options, deps, base, void 0, deadline);
40480
40490
  }
40481
40491
  if (merge.mergeStatus === "merged") {
@@ -40514,7 +40524,9 @@ function queueMerged(base, deps) {
40514
40524
  async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
40515
40525
  const now = deps.now ?? (() => Date.now());
40516
40526
  const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
40517
- const deadline = deadlineMs ?? now() + PR_LAND_QUEUE_TIMEOUT_MS;
40527
+ const timeoutMs = options.queueTimeoutMs ?? PR_LAND_QUEUE_TIMEOUT_MS;
40528
+ const deadline = deadlineMs ?? now() + timeoutMs;
40529
+ let attempt = 0;
40518
40530
  while (now() < deadline) {
40519
40531
  const read = snapshot ?? await deps.readMergeQueue(pr, base.repo);
40520
40532
  snapshot = void 0;
@@ -40544,16 +40556,45 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
40544
40556
  deps.queueProgress?.(result2);
40545
40557
  return result2;
40546
40558
  }
40547
- await sleep2(PR_LAND_POLL_MS);
40559
+ await sleep2(deps.pollDelayMs?.(++attempt) ?? PR_LAND_POLL_MS);
40548
40560
  }
40549
40561
  const result = {
40550
40562
  ...base,
40551
40563
  mergeQueue: { ...base.mergeQueue, state: "timeout" },
40552
- error: "merge queue did not reach MERGED within 45 minutes \u2014 rerun pr land to resume polling"
40564
+ error: `merge queue did not reach MERGED within ${Math.round(timeoutMs / 6e4)} minutes \u2014 rerun pr land to resume polling`
40553
40565
  };
40554
40566
  deps.queueProgress?.(result);
40555
40567
  return result;
40556
40568
  }
40569
+ function prLandOutcome(result) {
40570
+ if (result.status !== "failed" && result.status !== "queued") return "merged";
40571
+ if (result.status === "queued") return "queued";
40572
+ const state = result.mergeQueue?.state;
40573
+ return state === "ejected" || state === "timeout" ? state : "failed";
40574
+ }
40575
+ async function landMany(prs, landOne, cleanup, hooks) {
40576
+ const safe = async (pr) => landOne(pr).catch((e) => ({ status: "failed", repo: "", pr, base: "", error: e.message }));
40577
+ return Promise.all(prs.map(async (pr) => {
40578
+ let result = await safe(pr);
40579
+ if (prLandOutcome(result) === "ejected") {
40580
+ hooks?.onRetry?.(pr, result);
40581
+ result = { ...await safe(pr), retriedAfterEjection: true };
40582
+ }
40583
+ const outcome = prLandOutcome(result);
40584
+ const cleaned = result.status === "merged-via-queue" ? await cleanup(result).catch((e) => `cleanup failed: ${e.message}`) : void 0;
40585
+ hooks?.onDone?.();
40586
+ return { pr, outcome, result, ...cleaned ? { cleanup: cleaned } : {} };
40587
+ }));
40588
+ }
40589
+ function renderPrLandTable(entries) {
40590
+ const rows = entries.map((e) => [
40591
+ `#${e.pr}`,
40592
+ e.outcome + (e.result.retriedAfterEjection ? " (after retry)" : ""),
40593
+ [e.result.error, e.cleanup].filter(Boolean).join(" | ") || "-"
40594
+ ]);
40595
+ const width = (i) => Math.max(...rows.map((r) => r[i].length));
40596
+ return rows.map((r) => `${r[0].padEnd(width(0))} ${r[1].padEnd(width(1))} ${r[2]}`).join("\n");
40597
+ }
40557
40598
 
40558
40599
  // src/test-policy-core.ts
40559
40600
  var import_node_child_process13 = require("node:child_process");
@@ -47257,205 +47298,246 @@ ${list}`);
47257
47298
  }
47258
47299
  class PrHeadBehindBaseError extends Error {
47259
47300
  }
47260
- pr.command("land <number>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--enqueue-only", "return once queue membership is confirmed; without a merge queue, land normally").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "acknowledge and land past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict").action(async (number, o) => {
47261
- if (/^(?:[^/]+\/[^/]+)?#\d+$/.test(number.trim())) {
47262
- try {
47263
- const parsed = parseIssueRef(number, o.repo);
47264
- number = String(parsed.number);
47265
- if (parsed.repo) o.repo = parsed.repo;
47266
- } catch (e) {
47267
- return fail(`pr land: ${e.message}`);
47268
- }
47269
- }
47270
- const repoArgs = o.repo ? ["--repo", o.repo] : [];
47271
- const startingPath = (await execFileP("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
47272
- assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr land", { force: o.force });
47273
- const landRepoForGuard = await resolveRepo(o.repo).catch(() => void 0) ?? o.repo;
47274
- const landClosingGuardInput = await withAlreadyClosedCommitTargets(
47275
- await readClosingGuardInput(number, repoArgs, landRepoForGuard, "pr land"),
47276
- async (n) => {
47277
- if (!landRepoForGuard) return void 0;
47278
- const viewed = await ghJson(["issue", "view", String(n), "--repo", landRepoForGuard, "--json", "state"]);
47279
- return typeof viewed.state === "string" ? viewed.state : void 0;
47280
- }
47281
- );
47282
- const landSquashBody = squashBodyTextForMerge(landClosingGuardInput, true);
47283
- warnSquashBodyClosingStrip("pr land", landClosingGuardInput, landSquashBody);
47284
- const landClosingGuardVerdict = evaluateClosingGuard(landClosingGuardInput, {
47285
- force: o.force,
47286
- context: "pr land",
47287
- squashBodyText: landSquashBody
47288
- });
47289
- if (landClosingGuardVerdict.blocked) {
47290
- console.error(landClosingGuardVerdict.message);
47291
- process.exitCode = 1;
47292
- return;
47293
- }
47294
- if (landClosingGuardVerdict.message) console.warn(landClosingGuardVerdict.message);
47295
- let mergeQueueEnabled = false;
47296
- const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false, enqueueOnly: o.enqueueOnly }, {
47297
- hasMergeQueue: async (repo, base) => mergeQueueEnabled = await hasPrMergeQueue(repo, base),
47298
- readMergeQueue: readPrMergeQueue,
47299
- findMergeGroupRun: findPrMergeGroupRun,
47300
- queueProgress: (receipt) => {
47301
- if (!o.json) printLine(prMergeQueueLine(receipt));
47302
- },
47303
- resolveRepo: async (prNumber, repoOpt) => {
47304
- const knownRepo = repoOpt ?? await resolveRepo(o.repo).catch(() => void 0) ?? o.repo;
47305
- let repoFromGh = "";
47306
- let base = "";
47307
- if (knownRepo) {
47308
- const rested = await fetchRestPrSnapshot(prNumber, knownRepo).catch(() => void 0);
47309
- if (rested) {
47310
- console.warn("pr land: PR repo/base read via REST (App-capable poll identity) instead of gh GraphQL (#6834).");
47311
- repoFromGh = knownRepo;
47312
- base = rested.baseRef;
47313
- }
47314
- }
47315
- if (!repoFromGh || !base) {
47316
- const args = repoOpt ? ["--repo", repoOpt] : repoArgs;
47317
- const readEnv = await ghReadEnv();
47318
- const viewed = (await execFileP("gh", ["pr", "view", prNumber, ...args, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS, ...readEnv ? { env: readEnv } : {} })).stdout.trim();
47319
- [repoFromGh, base] = viewed.split(/\s+/);
47320
- }
47321
- const repo = repoOpt ?? repoFromGh;
47322
- if (!repo) throw new Error("pr land: could not resolve PR repo");
47323
- let track;
47301
+ pr.command("land <numbers...>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only. Several PR numbers land together (#8322): all are enqueued, waited on together, a queue-ejected PR is updated and re-enqueued once, a queue merge is followed by worktree/branch cleanup and board Done, and one table (or --json) reports each outcome").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--enqueue-only", "return once queue membership is confirmed; without a merge queue, land normally").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "acknowledge and land past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict").option("--timeout <minutes>", `merge-queue wait per PR (default ${PR_LAND_QUEUE_TIMEOUT_MS / 6e4} minutes, one window per ${PR_LAND_QUEUE_BATCH_SIZE} PRs \u2014 the queue's own check timeout is 60)`).option("--preserve-worktree", "after a queue merge keep the local worktree and branch (skips the post-merge cleanup)").action(async (numbers, opts) => {
47302
+ const timeoutMinutes = opts.timeout === void 0 ? void 0 : Number(opts.timeout);
47303
+ if (timeoutMinutes !== void 0 && !(timeoutMinutes > 0)) return fail("pr land: --timeout must be a positive number of minutes");
47304
+ const queueTimeoutMs = timeoutMinutes !== void 0 ? Math.round(timeoutMinutes * 6e4) : PR_LAND_QUEUE_TIMEOUT_MS * Math.ceil(numbers.length / PR_LAND_QUEUE_BATCH_SIZE);
47305
+ let inFlight = numbers.length;
47306
+ const landOne = async (numberArg) => {
47307
+ const o = { ...opts };
47308
+ let number = numberArg;
47309
+ const refused = (error) => ({ status: "failed", repo: o.repo ?? "", pr: number, base: "", error });
47310
+ if (/^(?:[^/]+\/[^/]+)?#\d+$/.test(number.trim())) {
47324
47311
  try {
47325
- const meta = await fetchProjectBySlug(slugOf(repo), registryClientDeps(await loadConfig()));
47326
- if (meta) track = resolveReleaseTrack(meta, void 0, repo);
47327
- } catch {
47312
+ const parsed = parseIssueRef(number, o.repo);
47313
+ number = String(parsed.number);
47314
+ if (parsed.repo) o.repo = parsed.repo;
47315
+ } catch (e) {
47316
+ return refused(e.message);
47328
47317
  }
47329
- if (isPromotionBase(base, track)) {
47330
- const shape = track ? `${track} track` : "unresolved track \u2014 strict reading";
47331
- throw new Error(`pr land: base branch ${base} is a promotion target (${shape}) \u2014 promotion merges stay human-only`);
47318
+ }
47319
+ const repoArgs = o.repo ? ["--repo", o.repo] : [];
47320
+ const startingPath = (await execFileP("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
47321
+ assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr land", { force: o.force });
47322
+ const landRepoForGuard = await resolveRepo(o.repo).catch(() => void 0) ?? o.repo;
47323
+ const landClosingGuardInput = await withAlreadyClosedCommitTargets(
47324
+ await readClosingGuardInput(number, repoArgs, landRepoForGuard, "pr land"),
47325
+ async (n) => {
47326
+ if (!landRepoForGuard) return void 0;
47327
+ const viewed = await ghJson(["issue", "view", String(n), "--repo", landRepoForGuard, "--json", "state"]);
47328
+ return typeof viewed.state === "string" ? viewed.state : void 0;
47332
47329
  }
47333
- return { repo, base };
47334
- },
47335
- fetchTrainAuthority: async (repo) => fetchTrainAuthority(repo, registryClientDeps(await loadConfig())),
47336
- resolveCiPolicy: (repo) => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
47337
- waitForChecks: async (prNumber, repo) => {
47338
- const requiredContexts = await fetchRequiredCheckContexts(repo, "development").catch(() => null);
47339
- return waitForPrChecks({
47340
- resolvePolicy: () => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
47341
- // #3024: REST-only wait loop — runPrLand's resolveRepo already guarantees `repo` is set.
47342
- pollChecks: () => pollRestPrChecks(prNumber, repo, void 0, requiredContexts),
47343
- // #2970: `pr land` only ever lands PRs based on development (resolveRepo above already rejects any
47344
- // other base), so the fast-fail message's base branch is always 'development' here.
47345
- pollMergeable: () => pollRestPrMergeable(prNumber, repo),
47346
- pollRateLimit: () => waitLoopCorePool("pr land"),
47347
- // #3388: `pr land` is the batch path — the one most likely to self-DOS the shared runner and
47348
- // then read its own wall-clock kill as a broken diff.
47349
- diagnoseFailure: () => waitLoopDiagnosis("pr land", prNumber, repo),
47350
- checksUrl: `https://github.com/${repo}/pull/${prNumber}/checks`,
47351
- checksDiagnosticCommand: `gh pr checks ${prNumber} --repo ${repo}`,
47352
- // #5400: same zero-runs delivery probe as checks-wait — do not burn the land budget on silence.
47353
- pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr land", prNumber, repo),
47354
- baseBranch: "development",
47355
- sleep: (ms) => new Promise((resolve8) => setTimeout(resolve8, ms)),
47356
- log: (message2) => console.warn(message2),
47357
- // `pr land` inherits the same (raised) checks budget, so it needs the same liveness — otherwise the
47358
- // 30m wait is SILENT and reads exactly like the hang #2940 was filed about, only three times longer.
47359
- 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`)
47360
- });
47361
- },
47362
- // #2425: re-probe used ONLY to decide whether a failed `gh pr merge --auto` is worth retrying — a
47363
- // fresh read of state/mergeable/checks, independent of the merge call's own (possibly stale) error.
47364
- // #4396: same required-status scoping as the wait loop above — a non-required red must not read
47365
- // as "not ready to retry" any more than it should have blocked the wait itself.
47366
- probeMergeReady: async (prNumber, repo) => {
47367
- const [snapshot, requiredContexts] = await Promise.all([
47368
- fetchRestPrSnapshot(prNumber, repo).catch(() => null),
47369
- // io-census-allow: a failed re-probe reads as open:false/mergeable:false below, the conservative 'not ready to retry' direction per the comment above
47370
- fetchRequiredCheckContexts(repo, "development").catch(() => null)
47371
- // io-census-allow: null is the established failed-state for this read (see pr-checks-rest.ts's partitionByRequired contract) — it makes every check required, the conservative direction
47372
- ]);
47373
- const checks = await pollRestPrChecks(prNumber, repo, void 0, requiredContexts).catch(() => "error");
47374
- return {
47375
- open: snapshot?.state === "open",
47376
- mergeable: snapshot?.mergeable === "MERGEABLE",
47377
- checksPassing: checks === "success" || checks === "no-checks-reported"
47378
- };
47379
- },
47380
- readPrState: async (prNumber, repo) => {
47381
- const args = repo ? ["--repo", repo] : [];
47382
- const readEnv = await ghReadEnv();
47383
- const result2 = await readGhPrStateWithRetry(async () => (await execFileP("gh", ["pr", "view", prNumber, ...args, "--json", "mergeStateStatus,mergeable,state"], { timeout: GC_GH_TIMEOUT_MS, ...readEnv ? { env: readEnv } : {} })).stdout);
47384
- if (!result2.ok) {
47385
- if (isGitHubRateLimitError(result2.error)) return fetchRestPrLandState(prNumber, repo);
47386
- throw new Error(`could not read PR state: ${result2.error}`);
47387
- }
47388
- return JSON.parse(result2.state);
47389
- },
47390
- mergeAuto: (prNumber, repo) => ghMergeAutoEnqueue(prNumber, repo, "--squash", {
47391
- allowedClosing: landClosingGuardInput?.closing,
47392
- bodyText: landSquashBody,
47393
- mergeQueue: mergeQueueEnabled
47394
- }),
47395
- updateBranch: (prNumber, repo) => prLandUpdateBranch(prNumber, repo, Boolean(o.repo)),
47396
- pollMerged: async (prNumber, repo, deadlineMs) => {
47397
- let lastFailure;
47398
- while (Date.now() < deadlineMs) {
47399
- const read = await pollRestPrMerged(prNumber, repo);
47400
- if (read.state === "merged") return true;
47401
- if (read.state === "failed") {
47402
- lastFailure = read.error;
47403
- console.warn(`pr land: merged-state read FAILED (${read.error}) \u2014 retrying; this is not evidence the PR is unmerged`);
47404
- } else {
47405
- lastFailure = void 0;
47330
+ );
47331
+ const landSquashBody = squashBodyTextForMerge(landClosingGuardInput, true);
47332
+ warnSquashBodyClosingStrip("pr land", landClosingGuardInput, landSquashBody);
47333
+ const landClosingGuardVerdict = evaluateClosingGuard(landClosingGuardInput, {
47334
+ force: o.force,
47335
+ context: "pr land",
47336
+ squashBodyText: landSquashBody
47337
+ });
47338
+ if (landClosingGuardVerdict.blocked) {
47339
+ console.error(landClosingGuardVerdict.message);
47340
+ return refused("refused by the closing-keyword guard (see the message above)");
47341
+ }
47342
+ if (landClosingGuardVerdict.message) console.warn(landClosingGuardVerdict.message);
47343
+ let mergeQueueEnabled = false;
47344
+ const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false, enqueueOnly: o.enqueueOnly, queueTimeoutMs }, {
47345
+ pollDelayMs: (attempt) => prLandPollDelayMs(attempt, inFlight),
47346
+ hasMergeQueue: async (repo, base) => mergeQueueEnabled = await hasPrMergeQueue(repo, base),
47347
+ readMergeQueue: readPrMergeQueue,
47348
+ findMergeGroupRun: findPrMergeGroupRun,
47349
+ queueProgress: (receipt) => {
47350
+ if (!o.json) printLine(prMergeQueueLine(receipt));
47351
+ },
47352
+ resolveRepo: async (prNumber, repoOpt) => {
47353
+ const knownRepo = repoOpt ?? await resolveRepo(o.repo).catch(() => void 0) ?? o.repo;
47354
+ let repoFromGh = "";
47355
+ let base = "";
47356
+ if (knownRepo) {
47357
+ const rested = await fetchRestPrSnapshot(prNumber, knownRepo).catch(() => void 0);
47358
+ if (rested) {
47359
+ console.warn("pr land: PR repo/base read via REST (App-capable poll identity) instead of gh GraphQL (#6834).");
47360
+ repoFromGh = knownRepo;
47361
+ base = rested.baseRef;
47362
+ }
47363
+ }
47364
+ if (!repoFromGh || !base) {
47365
+ const args = repoOpt ? ["--repo", repoOpt] : repoArgs;
47366
+ const readEnv = await ghReadEnv();
47367
+ const viewed = (await execFileP("gh", ["pr", "view", prNumber, ...args, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS, ...readEnv ? { env: readEnv } : {} })).stdout.trim();
47368
+ [repoFromGh, base] = viewed.split(/\s+/);
47406
47369
  }
47407
- await new Promise((resolve8) => setTimeout(resolve8, PR_LAND_POLL_MS));
47370
+ const repo = repoOpt ?? repoFromGh;
47371
+ if (!repo) throw new Error("pr land: could not resolve PR repo");
47372
+ let track;
47373
+ try {
47374
+ const meta = await fetchProjectBySlug(slugOf(repo), registryClientDeps(await loadConfig()));
47375
+ if (meta) track = resolveReleaseTrack(meta, void 0, repo);
47376
+ } catch {
47377
+ }
47378
+ if (isPromotionBase(base, track)) {
47379
+ const shape = track ? `${track} track` : "unresolved track \u2014 strict reading";
47380
+ throw new Error(`pr land: base branch ${base} is a promotion target (${shape}) \u2014 promotion merges stay human-only`);
47381
+ }
47382
+ return { repo, base };
47383
+ },
47384
+ fetchTrainAuthority: async (repo) => fetchTrainAuthority(repo, registryClientDeps(await loadConfig())),
47385
+ resolveCiPolicy: (repo) => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
47386
+ waitForChecks: async (prNumber, repo) => {
47387
+ const requiredContexts = await fetchRequiredCheckContexts(repo, "development").catch(() => null);
47388
+ return waitForPrChecks({
47389
+ resolvePolicy: () => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
47390
+ // #3024: REST-only wait loop — runPrLand's resolveRepo already guarantees `repo` is set.
47391
+ pollChecks: () => pollRestPrChecks(prNumber, repo, void 0, requiredContexts),
47392
+ // #2970: `pr land` only ever lands PRs based on development (resolveRepo above already rejects any
47393
+ // other base), so the fast-fail message's base branch is always 'development' here.
47394
+ pollMergeable: () => pollRestPrMergeable(prNumber, repo),
47395
+ pollRateLimit: () => waitLoopCorePool("pr land"),
47396
+ // #3388: `pr land` is the batch path — the one most likely to self-DOS the shared runner and
47397
+ // then read its own wall-clock kill as a broken diff.
47398
+ diagnoseFailure: () => waitLoopDiagnosis("pr land", prNumber, repo),
47399
+ checksUrl: `https://github.com/${repo}/pull/${prNumber}/checks`,
47400
+ checksDiagnosticCommand: `gh pr checks ${prNumber} --repo ${repo}`,
47401
+ // #5400: same zero-runs delivery probe as checks-wait — do not burn the land budget on silence.
47402
+ pollHeadWorkflowRunCount: () => waitLoopHeadWorkflowRunCount("pr land", prNumber, repo),
47403
+ baseBranch: "development",
47404
+ sleep: (ms) => new Promise((resolve8) => setTimeout(resolve8, ms)),
47405
+ log: (message2) => console.warn(message2),
47406
+ // `pr land` inherits the same (raised) checks budget, so it needs the same liveness — otherwise the
47407
+ // 30m wait is SILENT and reads exactly like the hang #2940 was filed about, only three times longer.
47408
+ 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`)
47409
+ });
47410
+ },
47411
+ // #2425: re-probe used ONLY to decide whether a failed `gh pr merge --auto` is worth retrying — a
47412
+ // fresh read of state/mergeable/checks, independent of the merge call's own (possibly stale) error.
47413
+ // #4396: same required-status scoping as the wait loop above — a non-required red must not read
47414
+ // as "not ready to retry" any more than it should have blocked the wait itself.
47415
+ probeMergeReady: async (prNumber, repo) => {
47416
+ const [snapshot, requiredContexts] = await Promise.all([
47417
+ fetchRestPrSnapshot(prNumber, repo).catch(() => null),
47418
+ // io-census-allow: a failed re-probe reads as open:false/mergeable:false below, the conservative 'not ready to retry' direction per the comment above
47419
+ fetchRequiredCheckContexts(repo, "development").catch(() => null)
47420
+ // io-census-allow: null is the established failed-state for this read (see pr-checks-rest.ts's partitionByRequired contract) — it makes every check required, the conservative direction
47421
+ ]);
47422
+ const checks = await pollRestPrChecks(prNumber, repo, void 0, requiredContexts).catch(() => "error");
47423
+ return {
47424
+ open: snapshot?.state === "open",
47425
+ mergeable: snapshot?.mergeable === "MERGEABLE",
47426
+ checksPassing: checks === "success" || checks === "no-checks-reported"
47427
+ };
47428
+ },
47429
+ readPrState: async (prNumber, repo) => {
47430
+ const args = repo ? ["--repo", repo] : [];
47431
+ const readEnv = await ghReadEnv();
47432
+ const result2 = await readGhPrStateWithRetry(async () => (await execFileP("gh", ["pr", "view", prNumber, ...args, "--json", "mergeStateStatus,mergeable,state"], { timeout: GC_GH_TIMEOUT_MS, ...readEnv ? { env: readEnv } : {} })).stdout);
47433
+ if (!result2.ok) {
47434
+ if (isGitHubRateLimitError(result2.error)) return fetchRestPrLandState(prNumber, repo);
47435
+ throw new Error(`could not read PR state: ${result2.error}`);
47436
+ }
47437
+ return JSON.parse(result2.state);
47438
+ },
47439
+ mergeAuto: (prNumber, repo) => ghMergeAutoEnqueue(prNumber, repo, "--squash", {
47440
+ allowedClosing: landClosingGuardInput?.closing,
47441
+ bodyText: landSquashBody,
47442
+ mergeQueue: mergeQueueEnabled
47443
+ }),
47444
+ updateBranch: (prNumber, repo) => prLandUpdateBranch(prNumber, repo, Boolean(o.repo)),
47445
+ pollMerged: async (prNumber, repo, deadlineMs) => {
47446
+ let lastFailure;
47447
+ while (Date.now() < deadlineMs) {
47448
+ const read = await pollRestPrMerged(prNumber, repo);
47449
+ if (read.state === "merged") return true;
47450
+ if (read.state === "failed") {
47451
+ lastFailure = read.error;
47452
+ console.warn(`pr land: merged-state read FAILED (${read.error}) \u2014 retrying; this is not evidence the PR is unmerged`);
47453
+ } else {
47454
+ lastFailure = void 0;
47455
+ }
47456
+ await new Promise((resolve8) => setTimeout(resolve8, PR_LAND_POLL_MS));
47457
+ }
47458
+ if (lastFailure) {
47459
+ throw new Error(
47460
+ `pr land: could not verify whether PR #${prNumber} merged \u2014 the last merged-state read FAILED (${lastFailure}). The auto-merge enqueue stands; read the PR directly (gh pr view ${prNumber} --repo ${repo} --json merged,state) and rerun cleanup if it merged.`
47461
+ );
47462
+ }
47463
+ return false;
47408
47464
  }
47409
- if (lastFailure) {
47410
- throw new Error(
47411
- `pr land: could not verify whether PR #${prNumber} merged \u2014 the last merged-state read FAILED (${lastFailure}). The auto-merge enqueue stands; read the PR directly (gh pr view ${prNumber} --repo ${repo} --json merged,state) and rerun cleanup if it merged.`
47412
- );
47465
+ });
47466
+ if (result.status !== "failed" && result.status !== "queued") {
47467
+ const boardAdvance = await advanceClosedIssuesToDone2(number, result.repo);
47468
+ const crossRepoFilingIssue = result.repo ? await reconcileCrossRepoFilingIssue(defaultGitHubClient(), result.repo, Number(number)) : { status: "failed", error: "could not resolve the PR repo for cross-repo filing-issue reconciliation" };
47469
+ const recovery = result.repo ? buildPostMergeReconRecovery({
47470
+ repo: result.repo,
47471
+ pr: number,
47472
+ boardAdvance,
47473
+ crossRepoFilingIssue
47474
+ }) : void 0;
47475
+ let recoveryPath;
47476
+ if (recovery) {
47477
+ try {
47478
+ recoveryPath = writePostMergeReconRecovery(process.cwd(), recovery);
47479
+ } catch {
47480
+ }
47481
+ } else if (result.repo) {
47482
+ clearPostMergeReconRecovery(process.cwd(), result.repo, number);
47483
+ }
47484
+ Object.assign(result, {
47485
+ ...boardAdvance.entries.length ? { boardAdvance: boardAdvance.entries } : {},
47486
+ boardAdvanceStatus: boardAdvance.status,
47487
+ ...crossRepoFilingIssue.status !== "not-applicable" ? { crossRepoFilingIssue } : {},
47488
+ ...recovery ? { postMergeReconRecovery: recovery } : {}
47489
+ });
47490
+ for (const line of postMergeReconWarnings({
47491
+ context: "pr land",
47492
+ boardAdvance,
47493
+ crossRepoFilingIssue,
47494
+ recovery,
47495
+ recoveryPath
47496
+ })) {
47497
+ console.error(line);
47413
47498
  }
47414
- return false;
47415
47499
  }
47416
- });
47417
- if (result.status !== "failed" && result.status !== "queued") {
47418
- const boardAdvance = await advanceClosedIssuesToDone2(number, result.repo);
47419
- const crossRepoFilingIssue = result.repo ? await reconcileCrossRepoFilingIssue(defaultGitHubClient(), result.repo, Number(number)) : { status: "failed", error: "could not resolve the PR repo for cross-repo filing-issue reconciliation" };
47420
- const recovery = result.repo ? buildPostMergeReconRecovery({
47421
- repo: result.repo,
47422
- pr: number,
47423
- boardAdvance,
47424
- crossRepoFilingIssue
47425
- }) : void 0;
47426
- let recoveryPath;
47427
- if (recovery) {
47428
- try {
47429
- recoveryPath = writePostMergeReconRecovery(process.cwd(), recovery);
47430
- } catch {
47431
- }
47432
- } else if (result.repo) {
47433
- clearPostMergeReconRecovery(process.cwd(), result.repo, number);
47500
+ return result;
47501
+ };
47502
+ const cwd0 = process.cwd();
47503
+ const primaryRoot0 = (await execFileP("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.match(/^worktree (.+)$/m)?.[1];
47504
+ const cleanupMerged = async (r) => {
47505
+ if (opts.preserveWorktree) return "cleanup skipped (--preserve-worktree)";
47506
+ const ran = await execFileP(process.execPath, [process.argv[1], ...canonicalArgvFor(["pr", "merge", r.pr, "--repo", r.repo])], {
47507
+ cwd: (0, import_node_fs45.existsSync)(cwd0) ? cwd0 : primaryRoot0,
47508
+ timeout: GH_MUTATION_TIMEOUT_MS * 3
47509
+ }).then((x) => ({ stdout: x.stdout, stderr: x.stderr }), (e) => ({ stdout: e.stdout ?? "", stderr: e.stderr ?? "" }));
47510
+ const json = ran.stdout.split("\n").reverse().find((l) => l.startsWith("{"));
47511
+ if (!json) return `cleanup failed: ${ran.stderr.trim().split("\n").pop() || "no receipt"}`;
47512
+ const c = JSON.parse(json);
47513
+ const part = (x) => x ? `${x.status}${x.reason ? ` (${x.reason})` : ""}` : "n/a";
47514
+ return `cleanup ${c.mergeStatus ?? "unknown"}: worktree ${part(c.worktree)}, branch ${part(c.localBranch)}`;
47515
+ };
47516
+ const entries = await landMany(numbers, landOne, cleanupMerged, {
47517
+ onRetry: (pr2, r) => console.warn(`pr land: queue ejected PR #${pr2} (${r.error}) \u2014 landing it once more (head updated from base if behind)`),
47518
+ onDone: () => {
47519
+ inFlight = Math.max(1, inFlight - 1);
47434
47520
  }
47435
- Object.assign(result, {
47436
- ...boardAdvance.entries.length ? { boardAdvance: boardAdvance.entries } : {},
47437
- boardAdvanceStatus: boardAdvance.status,
47438
- ...crossRepoFilingIssue.status !== "not-applicable" ? { crossRepoFilingIssue } : {},
47439
- ...recovery ? { postMergeReconRecovery: recovery } : {}
47440
- });
47441
- for (const line of postMergeReconWarnings({
47442
- context: "pr land",
47443
- boardAdvance,
47444
- crossRepoFilingIssue,
47445
- recovery,
47446
- recoveryPath
47447
- })) {
47448
- console.error(line);
47521
+ });
47522
+ const credential = lastRestPollSource() === "app" ? "app_installation" : "user";
47523
+ if (numbers.length === 1) {
47524
+ const { result, cleanup } = entries[0];
47525
+ if (opts.json) printLine(JSON.stringify({ ...result, ...cleanup ? { cleanup } : {}, credential }));
47526
+ else {
47527
+ printLine(
47528
+ `pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""} (credential: ${credential})` + (result.branchFinality ? ` \u2014 ${result.branchFinality}` : "")
47529
+ );
47530
+ if (cleanup) printLine(`pr land: ${cleanup}`);
47449
47531
  }
47532
+ } else if (opts.json) {
47533
+ printLine(JSON.stringify({ results: entries.map((e) => ({ ...e.result, outcome: e.outcome, ...e.cleanup ? { cleanup: e.cleanup } : {} })), summary: Object.fromEntries(["merged", "ejected", "timeout", "queued", "failed"].map((k) => [k, entries.filter((e) => e.outcome === k).length])), credential }));
47534
+ } else {
47535
+ printLine(renderPrLandTable(entries));
47536
+ printLine(`pr land: ${entries.filter((e) => e.outcome === "merged").length}/${entries.length} merged (credential: ${credential})`);
47450
47537
  }
47451
- const credential = lastRestPollSource() === "app" ? "app_installation" : "user";
47452
- if (o.json) printLine(JSON.stringify({ ...result, credential }));
47453
- else printLine(
47454
- `pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""} (credential: ${credential})` + (result.branchFinality ? ` \u2014 ${result.branchFinality}` : "")
47455
- );
47456
- if (result.status === "failed") process.exitCode = 1;
47538
+ if (entries.some((e) => e.outcome !== "merged" && e.outcome !== "queued")) process.exitCode = 1;
47457
47539
  });
47458
- jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo); from a foreign checkout the remote probe/delete address this repo and local cleanup runs only in the verified sibling checkout ../<repo>, else localBranch reports skipped-foreign-cwd and the receipt carries foreignCwd: true (#6148)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").addOption(new Option("--disable-auto", "disable a queued auto-merge without merging").conflicts(["auto", "wait", "squash", "merge", "rebase", "preserveWorktree", "gc", "squashBodyFile", "force"])).option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m) \u2014 run as a background/monitor task or under a shell timeout above that budget; a short foreground timeout (e.g. 120s) kills it after checks pass and leaves the PR open (#6027)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888); also the merge settings-proof bypass when delete_branch_on_merge cannot be proven (#6210)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--squash-body-file <path>", "squash commit body (overrides GitHub COMMIT_MESSAGES); use when a pushed commit mentions close/fix/resolve + #N that must not close (#5723)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict")).action(async (number, o) => {
47540
+ jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679); on a merge-queue base use `pr land <n> [<n>...]` instead (#8322) \u2014 this verb is refused there except for train PRs").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo); from a foreign checkout the remote probe/delete address this repo and local cleanup runs only in the verified sibling checkout ../<repo>, else localBranch reports skipped-foreign-cwd and the receipt carries foreignCwd: true (#6148)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").addOption(new Option("--disable-auto", "disable a queued auto-merge without merging").conflicts(["auto", "wait", "squash", "merge", "rebase", "preserveWorktree", "gc", "squashBodyFile", "force"])).option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m) \u2014 run as a background/monitor task or under a shell timeout above that budget; a short foreground timeout (e.g. 120s) kills it after checks pass and leaves the PR open (#6027)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888); also the merge settings-proof bypass when delete_branch_on_merge cannot be proven (#6210)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--squash-body-file <path>", "squash commit body (overrides GitHub COMMIT_MESSAGES); use when a pushed commit mentions close/fix/resolve + #N that must not close (#5723)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict")).action(async (number, o) => {
47459
47541
  const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
47460
47542
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
47461
47543
  if (o.disableAuto) {
@@ -47697,7 +47779,8 @@ ${list}`);
47697
47779
  });
47698
47780
  try {
47699
47781
  try {
47700
- await mergeOnce();
47782
+ if (prMeta.state === "MERGED") remoteNotAttemptedReason = "pr-already-merged";
47783
+ else await mergeOnce();
47701
47784
  } catch (e) {
47702
47785
  if (!(e instanceof PrHeadBehindBaseError)) throw e;
47703
47786
  const localCheckedOut = !foreignCwd && await prHeadCheckedOutHere(headRef, targetRepo2, Boolean(o.repo));
@@ -55803,8 +55886,19 @@ function registerDeveloperCommands(program3) {
55803
55886
  await failGraceful(e.message);
55804
55887
  }
55805
55888
  });
55806
- docs.command("refs").description("deterministic doc reference gate: repo paths, markdown links, mmi-cli command refs, and pins across docs, org-guide, mmi-help, README and architecture must resolve, or exit 1").option("--json", "machine-readable findings list: { ok, docCount, findings[], warnings[] }").addOption(new Option("--repo <owner/repo>", "not accepted \u2014 docs refs reads the current checkout").hideHelp()).option("--base <ref>", "require covered runbook sections to be touched against this merge base (#8126)").option("--runbooks-only", "check only operator runbooks (#8126)").option("--live", "check operator runbooks against registry tasks, vault names and HTTPS endpoints (implies --runbooks-only)").action(async (o) => {
55889
+ docs.command("refs").description("deterministic doc reference gate: repo paths, markdown links, mmi-cli command refs, and pins across docs, org-guide, mmi-help, README and architecture must resolve, or exit 1").option("--json", "machine-readable findings list: { ok, docCount, findings[], warnings[] }").addOption(new Option("--repo <owner/repo>", "not accepted \u2014 docs refs reads the current checkout").hideHelp()).option("--base <ref>", "require covered runbook sections to be touched against this merge base (#8126)").option("--runbooks-only", "check only operator runbooks (#8126)").option("--live", "check operator runbooks against registry tasks, vault names and HTTPS endpoints (implies --runbooks-only)").option("--local", "pre-push mode (#8323): the CI runbook guard against origin/development (else origin/main) \u2014 implies --runbooks-only, offline, same verdict as the gate step").action(async (o) => {
55807
55890
  try {
55891
+ if (o.local) {
55892
+ o.runbooksOnly = true;
55893
+ if (!o.base) {
55894
+ const exists = /* @__PURE__ */ new Set();
55895
+ for (const ref of ["origin/development", "origin/main"]) {
55896
+ if (await execFileP("git", ["rev-parse", "--verify", "--quiet", ref], { timeout: GIT_TIMEOUT_MS }).then(() => true, () => false)) exists.add(ref);
55897
+ }
55898
+ o.base = localRunbookBase((ref) => exists.has(ref));
55899
+ if (!o.base) return failGraceful("docs refs --local: neither origin/development nor origin/main exists \u2014 run `git fetch origin` or pass --base <ref>");
55900
+ }
55901
+ }
55808
55902
  if (o.repo) {
55809
55903
  const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), `mmi-cli oracle docs refs${o.json ? " --json" : ""}`, "docs refs");
55810
55904
  if (!guard.ok) return failGraceful(guard.message);
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.149",
3
+ "version": "4.5.150",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/claude-plugin",
3
- "version": "4.5.149",
3
+ "version": "4.5.150",
4
4
  "description": "MMI workflow skills and org gates delivery.",
5
5
  "author": {
6
6
  "name": "MMI Future",