@mutmutco/codex-plugin 4.5.163 → 4.5.164

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,6 +1,6 @@
1
1
  {
2
2
  "name": "mmi",
3
- "version": "4.5.163",
3
+ "version": "4.5.164",
4
4
  "mmiCompat": "4.x",
5
5
  "description": "MMI workflow skills and org gates delivery.",
6
6
  "author": {
package/cli/dist/main.cjs CHANGED
@@ -19051,10 +19051,10 @@ var rollout_plan_default = {
19051
19051
  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)."
19052
19052
  },
19053
19053
  baseline: {
19054
- version: "4.5.163",
19055
- tag: "v4.5.163",
19056
- commit: "0d3f3c9549d7",
19057
- npm: "@mutmutco/cli@4.5.163"
19054
+ version: "4.5.164",
19055
+ tag: "v4.5.164",
19056
+ commit: "a5f70231e564",
19057
+ npm: "@mutmutco/cli@4.5.164"
19058
19058
  },
19059
19059
  exitCriterion: "fleet-n-of-n",
19060
19060
  hubOnlyShortcut: "forbidden",
@@ -19071,14 +19071,14 @@ var rollout_plan_default = {
19071
19071
  repo: "mutmutco/mmi-hub",
19072
19072
  role: "canary",
19073
19073
  schedule: "train",
19074
- v3Target: "v4.5.163"
19074
+ v3Target: "v4.5.164"
19075
19075
  }
19076
19076
  ],
19077
19077
  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.",
19078
19078
  rollback: {
19079
19079
  independent: true,
19080
- mechanism: "npm dist-tag latest -> 4.5.163 and redeploy the Hub Lambda from tag v4.5.163 (0d3f3c9549d7); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
19081
- v3Target: "v4.5.163 (@mutmutco/cli@4.5.163, tag commit 0d3f3c9549d7 \u2014 last known-good release carrying the repo-index v4-only contract)"
19080
+ mechanism: "npm dist-tag latest -> 4.5.164 and redeploy the Hub Lambda from tag v4.5.164 (a5f70231e564); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
19081
+ v3Target: "v4.5.164 (@mutmutco/cli@4.5.164, tag commit a5f70231e564 \u2014 last known-good release carrying the repo-index v4-only contract)"
19082
19082
  }
19083
19083
  },
19084
19084
  {
@@ -40963,6 +40963,22 @@ async function runPrLand(prNumber, options, deps) {
40963
40963
  }
40964
40964
  const ciPolicy = await deps.resolveCiPolicy(repo);
40965
40965
  base.ciPolicy = ciPolicy;
40966
+ const pinned = options.expectHead;
40967
+ if (pinned) base.pinnedHead = { expected: pinned };
40968
+ const headDrift = async (stage) => {
40969
+ if (!pinned) return void 0;
40970
+ if (!deps.readHeadSha) throw new Error("pinned head reader unavailable");
40971
+ const observed = await deps.readHeadSha(prNumber, repo);
40972
+ base.pinnedHead = { expected: pinned, observed };
40973
+ if (observed === pinned) return void 0;
40974
+ return {
40975
+ ...base,
40976
+ pinnedHead: { ...base.pinnedHead, refused: "drift" },
40977
+ error: `pr land #${prNumber}: head ${observed} is not the pinned ${pinned} (${stage}) \u2014 refusing to land a head that was not checked (--expect-head)`
40978
+ };
40979
+ };
40980
+ const drift = await headDrift("before landing");
40981
+ if (drift) return drift;
40966
40982
  const hasQueue = await deps.hasMergeQueue?.(repo, resolved.base) ?? false;
40967
40983
  let queueSnapshot;
40968
40984
  if (hasQueue) {
@@ -40986,7 +41002,16 @@ async function runPrLand(prNumber, options, deps) {
40986
41002
  base.checksWait = await deps.waitForChecks(prNumber, repo);
40987
41003
  const waitError = checksWaitError(base.checksWait);
40988
41004
  if (waitError) return { ...base, error: waitError };
41005
+ const driftAfterChecks = await headDrift("after checks");
41006
+ if (driftAfterChecks) return driftAfterChecks;
40989
41007
  let prState = await deps.readPrState(prNumber, repo);
41008
+ if (pinned && prState.mergeStateStatus === "BEHIND") {
41009
+ return {
41010
+ ...base,
41011
+ pinnedHead: { ...base.pinnedHead, refused: "behind" },
41012
+ error: `pr land #${prNumber}: head is BEHIND ${resolved.base} and pinned by --expect-head \u2014 refusing to update it. Update and re-check the head yourself, then land with the new SHA`
41013
+ };
41014
+ }
40990
41015
  if (prState.mergeStateStatus === "BEHIND" && deps.updateBranch) {
40991
41016
  try {
40992
41017
  base.branchUpdated = await deps.updateBranch(prNumber, repo);
@@ -41080,6 +41105,7 @@ async function pollPrMergeQueue(pr, options, deps, base, snapshot, deadlineMs) {
41080
41105
  const result2 = {
41081
41106
  ...base,
41082
41107
  mergeQueue: { ...base.mergeQueue, state: "ejected", ...run ? { ejectedRun: run } : {} },
41108
+ ...base.pinnedHead ? { pinnedHead: { ...base.pinnedHead, refused: "ejected" } } : {},
41083
41109
  error: `merge queue ejected PR #${pr} \u2014 ${run ? `merge_group run ${run.id} ${run.conclusion} ${run.url}` : "merge_group run not found"}`
41084
41110
  };
41085
41111
  deps.queueProgress?.(result2);
@@ -41105,7 +41131,7 @@ async function landMany(prs, landOne, cleanup, hooks) {
41105
41131
  const safe = async (pr) => landOne(pr).catch((e) => ({ status: "failed", repo: "", pr, base: "", error: e.message }));
41106
41132
  return Promise.all(prs.map(async (pr) => {
41107
41133
  let result = await safe(pr);
41108
- if (prLandOutcome(result) === "ejected") {
41134
+ if (prLandOutcome(result) === "ejected" && !result.pinnedHead) {
41109
41135
  hooks?.onRetry?.(pr, result);
41110
41136
  result = { ...await safe(pr), retriedAfterEjection: true };
41111
41137
  }
@@ -41985,6 +42011,7 @@ function buildPrMergeArgs(input) {
41985
42011
  const args = ["pr", "merge", input.number, ...input.repoArgs, input.method];
41986
42012
  if (input.deleteBranch !== false) args.push("--delete-branch");
41987
42013
  if (input.bodyFile) args.push("--body-file", input.bodyFile);
42014
+ if (input.matchHeadCommit) args.push("--match-head-commit", input.matchHeadCommit);
41988
42015
  if (input.auto) args.push("--auto");
41989
42016
  return args;
41990
42017
  }
@@ -42226,12 +42253,12 @@ async function ghMergeAutoEnqueue(prNumber, repo, method, extra) {
42226
42253
  bodyText: method === "--squash" ? extra?.bodyText : void 0
42227
42254
  });
42228
42255
  try {
42229
- return await mergeAutoEnqueueWithBody(prNumber, args, method, io, overrideBody?.path, extra?.mergeQueue);
42256
+ return await mergeAutoEnqueueWithBody(prNumber, args, method, io, overrideBody?.path, extra?.mergeQueue, extra?.matchHeadCommit);
42230
42257
  } finally {
42231
42258
  overrideBody?.cleanup();
42232
42259
  }
42233
42260
  }
42234
- async function mergeAutoEnqueueWithBody(prNumber, args, method, io, bodyFile, mergeQueue = false) {
42261
+ async function mergeAutoEnqueueWithBody(prNumber, args, method, io, bodyFile, mergeQueue = false, matchHeadCommit) {
42235
42262
  const headRef = (await io.gh(["pr", "view", prNumber, ...args, "--json", "headRefName", "--jq", ".headRefName"], GC_GH_TIMEOUT_MS).catch(() => "")).trim();
42236
42263
  const deleteBranch = !mergeQueue && !isProtectedBranch(headRef);
42237
42264
  const readMergeState = () => readGhPrStateWithRetry(
@@ -42251,7 +42278,7 @@ async function mergeAutoEnqueueWithBody(prNumber, args, method, io, bodyFile, me
42251
42278
  return s.ok && s.state === "MERGED";
42252
42279
  },
42253
42280
  reEnqueue: async () => {
42254
- await io.gh(buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: true, deleteBranch, bodyFile }), GH_MUTATION_TIMEOUT_MS);
42281
+ await io.gh(buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: true, deleteBranch, bodyFile, matchHeadCommit }), GH_MUTATION_TIMEOUT_MS);
42255
42282
  }
42256
42283
  });
42257
42284
  if (confirmation === "merged") return { mergeStatus: "merged" };
@@ -42259,7 +42286,7 @@ async function mergeAutoEnqueueWithBody(prNumber, args, method, io, bodyFile, me
42259
42286
  return { mergeStatus: "failed", error: "auto-merge did not stick \u2014 GitHub reported no autoMergeRequest after enqueue; retry once the PR has a pending check" };
42260
42287
  };
42261
42288
  try {
42262
- await io.gh(buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: true, deleteBranch, bodyFile }), GH_MUTATION_TIMEOUT_MS);
42289
+ await io.gh(buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: true, deleteBranch, bodyFile, matchHeadCommit }), GH_MUTATION_TIMEOUT_MS);
42263
42290
  } catch (e) {
42264
42291
  const message2 = String(e.message || "");
42265
42292
  if (/already been merged/i.test(message2)) return { mergeStatus: "merged" };
@@ -42267,7 +42294,7 @@ async function mergeAutoEnqueueWithBody(prNumber, args, method, io, bodyFile, me
42267
42294
  if (note) return { mergeStatus: "failed", error: note };
42268
42295
  if (mergeAutoRejectedPrAlreadyClean(message2)) {
42269
42296
  try {
42270
- await io.gh(buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: false, deleteBranch, bodyFile }), GH_MUTATION_TIMEOUT_MS);
42297
+ await io.gh(buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: false, deleteBranch, bodyFile, matchHeadCommit }), GH_MUTATION_TIMEOUT_MS);
42271
42298
  } catch (e2) {
42272
42299
  const m2 = String(e2.message || "");
42273
42300
  if (/already been merged/i.test(m2)) return { mergeStatus: "merged" };
@@ -42278,7 +42305,7 @@ async function mergeAutoEnqueueWithBody(prNumber, args, method, io, bodyFile, me
42278
42305
  return { mergeStatus: "failed", error: `could not confirm PR state after clean-status fallback: ${afterDirect.error}` };
42279
42306
  }
42280
42307
  try {
42281
- await io.gh(buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: true, deleteBranch, bodyFile }), GH_MUTATION_TIMEOUT_MS);
42308
+ await io.gh(buildPrMergeArgs({ number: prNumber, repoArgs: args, method, auto: true, deleteBranch, bodyFile, matchHeadCommit }), GH_MUTATION_TIMEOUT_MS);
42282
42309
  } catch (e3) {
42283
42310
  const m3 = String(e3.message || "");
42284
42311
  if (/already been merged/i.test(m3)) return { mergeStatus: "merged" };
@@ -47844,9 +47871,14 @@ ${list}`);
47844
47871
  }
47845
47872
  class PrHeadBehindBaseError extends Error {
47846
47873
  }
47847
- 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) => {
47874
+ 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)").option("--expect-head <sha>", "pin the PR head to this full 40-hex SHA (#8456): checked before landing, after checks and by GitHub at enqueue (--match-head-commit); never updates a BEHIND head and never retries an ejection \u2014 it stops with pinnedHead.refused instead. One PR only").action(async (numbers, opts) => {
47848
47875
  const timeoutMinutes = opts.timeout === void 0 ? void 0 : Number(opts.timeout);
47849
47876
  if (timeoutMinutes !== void 0 && !(timeoutMinutes > 0)) return fail("pr land: --timeout must be a positive number of minutes");
47877
+ if (opts.expectHead !== void 0) {
47878
+ if (!/^[0-9a-f]{40}$/i.test(opts.expectHead)) return fail("pr land: --expect-head needs the full 40-hex head SHA");
47879
+ if (numbers.length !== 1) return fail("pr land: --expect-head pins one PR head; land one PR per call");
47880
+ opts.expectHead = opts.expectHead.toLowerCase();
47881
+ }
47850
47882
  const queueTimeoutMs = timeoutMinutes !== void 0 ? Math.round(timeoutMinutes * 6e4) : PR_LAND_QUEUE_TIMEOUT_MS * Math.ceil(numbers.length / PR_LAND_QUEUE_BATCH_SIZE);
47851
47883
  let inFlight = numbers.length;
47852
47884
  const landOne = async (numberArg) => {
@@ -47887,7 +47919,12 @@ ${list}`);
47887
47919
  }
47888
47920
  if (landClosingGuardVerdict.message) console.warn(landClosingGuardVerdict.message);
47889
47921
  let mergeQueueEnabled = false;
47890
- const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false, enqueueOnly: o.enqueueOnly, queueTimeoutMs }, {
47922
+ const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false, enqueueOnly: o.enqueueOnly, queueTimeoutMs, expectHead: o.expectHead }, {
47923
+ readHeadSha: async (prNumber, repo) => {
47924
+ const sha = (await fetchRestPrSnapshot(prNumber, repo)).headSha;
47925
+ if (!sha) throw new Error(`pr land: could not read the head SHA of PR #${prNumber}`);
47926
+ return sha.toLowerCase();
47927
+ },
47891
47928
  pollDelayMs: (attempt) => prLandPollDelayMs(attempt, inFlight),
47892
47929
  hasMergeQueue: async (repo, base) => mergeQueueEnabled = await hasPrMergeQueue(repo, base),
47893
47930
  readMergeQueue: readPrMergeQueue,
@@ -47985,9 +48022,11 @@ ${list}`);
47985
48022
  mergeAuto: (prNumber, repo) => ghMergeAutoEnqueue(prNumber, repo, "--squash", {
47986
48023
  allowedClosing: landClosingGuardInput?.closing,
47987
48024
  bodyText: landSquashBody,
47988
- mergeQueue: mergeQueueEnabled
48025
+ mergeQueue: mergeQueueEnabled,
48026
+ matchHeadCommit: o.expectHead
47989
48027
  }),
47990
- updateBranch: (prNumber, repo) => prLandUpdateBranch(prNumber, repo, Boolean(o.repo)),
48028
+ // #8456: a pinned land has no update lane at all.
48029
+ ...o.expectHead ? {} : { updateBranch: (prNumber, repo) => prLandUpdateBranch(prNumber, repo, Boolean(o.repo)) },
47991
48030
  pollMerged: async (prNumber, repo, deadlineMs) => {
47992
48031
  let lastFailure;
47993
48032
  while (Date.now() < deadlineMs) {
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.163",
3
+ "version": "4.5.164",
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/codex-plugin",
3
- "version": "4.5.163",
3
+ "version": "4.5.164",
4
4
  "description": "MMI workflow skills and org gates delivery.",
5
5
  "author": {
6
6
  "name": "MMI Future",