@mutmutco/codex-plugin 4.6.29 → 4.6.31

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.6.29",
3
+ "version": "4.6.31",
4
4
  "mmiCompat": "4.x",
5
5
  "description": "MMI workflow skills and org gates delivery.",
6
6
  "author": {
package/cli/dist/main.cjs CHANGED
@@ -24526,19 +24526,7 @@ function formatBulkClaimSummary(results) {
24526
24526
  const notClaimed = skipped.map((r) => `${r.ref}${/already claimed by /i.test(r.reason) ? " (held by another lane)" : ""}`);
24527
24527
  return `claimed ${landed.length} of ${results.length}: ${landed.length ? `${landed.join(", ")} landed` : "none landed"}; not claimed: ${notClaimed.join(", ")}`;
24528
24528
  }
24529
- async function claimBoardIssues(options, deps = {}) {
24530
- const cfg = resolveBoardConfig(options.config);
24531
- const { collected, snapshot } = await collectBoardPreferringSnapshot(cfg, options, deps);
24532
- const selectors = [];
24533
- const seen = /* @__PURE__ */ new Set();
24534
- for (const raw of options.selectors) {
24535
- const selector = parseIssueSelector(raw, collected.repo, options.repo);
24536
- const key = `${selector.repo.toLowerCase()}#${selector.number}`;
24537
- if (seen.has(key)) continue;
24538
- seen.add(key);
24539
- selectors.push(selector);
24540
- }
24541
- const ctx = await prepareClaimContext(options, selectors, deps, collected, snapshot);
24529
+ async function runBulkClaims(ctx, selectors, options) {
24542
24530
  const results = new Array(selectors.length);
24543
24531
  let next = 0;
24544
24532
  const worker = async () => {
@@ -24555,6 +24543,71 @@ async function claimBoardIssues(options, deps = {}) {
24555
24543
  }
24556
24544
  };
24557
24545
  await Promise.all(Array.from({ length: Math.min(CLAIM_CONCURRENCY, selectors.length) }, () => worker()));
24546
+ return results;
24547
+ }
24548
+ function claimArgsError(refs, all) {
24549
+ if (all && refs.length) return `--all claims the whole board; drop the explicit refs (${refs.join(" ")}) or drop --all`;
24550
+ if (!all && !refs.length) return "missing issue ref: pass one or more refs, or --all for every open claimable item on the board";
24551
+ return void 0;
24552
+ }
24553
+ async function claimAllBoardIssues(options, deps = {}) {
24554
+ const cfg = resolveBoardConfig(options.config);
24555
+ const { collected, snapshot } = await collectBoardPreferringSnapshot(cfg, options, deps);
24556
+ const ctx = await prepareClaimContext(options, [], deps, collected, snapshot);
24557
+ const { primary } = ctx.report;
24558
+ const assignee = options.assignee ?? "@me";
24559
+ const login = assignee === "@me" ? ctx.report.viewer : assignee.replace(/^@/, "");
24560
+ const selectors = [];
24561
+ const skipped = [];
24562
+ const seen = /* @__PURE__ */ new Set();
24563
+ for (const item of [...primary.userOwned, ...primary.claimable, ...primary.unownedInFlight, ...primary.taken]) {
24564
+ const key = item.ref.toLowerCase();
24565
+ if (seen.has(key)) continue;
24566
+ seen.add(key);
24567
+ if (item.contentType !== "Issue") {
24568
+ skipped.push({ ref: item.ref, reason: "not an issue" });
24569
+ continue;
24570
+ }
24571
+ if (item.state !== "OPEN") {
24572
+ skipped.push({ ref: item.ref, reason: `issue is ${item.state.toLowerCase()} but its card is ${item.status}` });
24573
+ continue;
24574
+ }
24575
+ if (item.status === "In Review") {
24576
+ skipped.push({ ref: item.ref, reason: "In Review \u2014 a whole-board claim does not pull review cards back to In Progress; claim it by ref to reclaim" });
24577
+ continue;
24578
+ }
24579
+ const verdict = evaluateClaim(item, login);
24580
+ if (!verdict.ok) {
24581
+ skipped.push({ ref: item.ref, reason: verdict.reason.replace(/ \(never chain separate claims.*$/, "") });
24582
+ continue;
24583
+ }
24584
+ selectors.push({ repo: item.repository, number: item.number });
24585
+ }
24586
+ ctx.selectedRefs = new Set(selectors.map((s) => `${s.repo}#${s.number}`.toLowerCase()));
24587
+ const results = await runBulkClaims(ctx, selectors, options);
24588
+ return {
24589
+ viewer: ctx.report.viewer,
24590
+ repo: ctx.report.repo,
24591
+ results,
24592
+ skipped,
24593
+ failed: results.filter((result) => !result.claimed).length,
24594
+ ...ctx.report.warnings.length ? { warnings: [...ctx.report.warnings] } : {}
24595
+ };
24596
+ }
24597
+ async function claimBoardIssues(options, deps = {}) {
24598
+ const cfg = resolveBoardConfig(options.config);
24599
+ const { collected, snapshot } = await collectBoardPreferringSnapshot(cfg, options, deps);
24600
+ const selectors = [];
24601
+ const seen = /* @__PURE__ */ new Set();
24602
+ for (const raw of options.selectors) {
24603
+ const selector = parseIssueSelector(raw, collected.repo, options.repo);
24604
+ const key = `${selector.repo.toLowerCase()}#${selector.number}`;
24605
+ if (seen.has(key)) continue;
24606
+ seen.add(key);
24607
+ selectors.push(selector);
24608
+ }
24609
+ const ctx = await prepareClaimContext(options, selectors, deps, collected, snapshot);
24610
+ const results = await runBulkClaims(ctx, selectors, options);
24558
24611
  return {
24559
24612
  viewer: ctx.report.viewer,
24560
24613
  repo: ctx.report.repo,
@@ -25980,7 +26033,7 @@ var BLESSED_RUN_WITH_BUDGET_SHA = "cb31a94c89729beac83500ad4d301132dcfe2e4c";
25980
26033
  var BLESSED_RUNNER_GATE_SHA = "7f9a25df2ce7198c67407b42d0c548f75ce3f430";
25981
26034
  var BLESSED_RUNNER_CHECK_POLICY_SHA = "67bb2012710a96d21be825c29a5d5c2b4b8001e2";
25982
26035
  var BLESSED_KIT_AGENT_FILES_SHA = "e976df208d891b215844386c1cac0f161946b6fa";
25983
- var BLESSED_JEV_FAILURES_SHA = "d5278c6133318b33727483ddfdfddc8f841e3ef6";
26036
+ var BLESSED_JEV_FAILURES_SHA = "20ce7d5e0d85cdc7e14bbacbc8b6afdabee6bd41";
25984
26037
  var BLESSED_VERDICT_RECEIPT_SHA = "";
25985
26038
  var BLESSED_RUNBOOK_GUARD_SHA = "e661e0aedc0e58b73c738bf6cdfeac65e5914584";
25986
26039
  var REMOTE_USE = /^mutmutco\/MMI-Hub\/\.github\/actions\/run-with-budget@(\S+)$/;
@@ -28248,10 +28301,10 @@ var rollout_plan_default = {
28248
28301
  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)."
28249
28302
  },
28250
28303
  baseline: {
28251
- version: "4.6.29",
28252
- tag: "v4.6.29",
28253
- commit: "36e01a038f52",
28254
- npm: "@mutmutco/cli@4.6.29"
28304
+ version: "4.6.31",
28305
+ tag: "v4.6.31",
28306
+ commit: "53e22eaf158d",
28307
+ npm: "@mutmutco/cli@4.6.31"
28255
28308
  },
28256
28309
  exitCriterion: "fleet-n-of-n",
28257
28310
  hubOnlyShortcut: "forbidden",
@@ -28268,14 +28321,14 @@ var rollout_plan_default = {
28268
28321
  repo: "mutmutco/mmi-hub",
28269
28322
  role: "canary",
28270
28323
  schedule: "train",
28271
- v3Target: "v4.6.29"
28324
+ v3Target: "v4.6.31"
28272
28325
  }
28273
28326
  ],
28274
28327
  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.",
28275
28328
  rollback: {
28276
28329
  independent: true,
28277
- mechanism: "npm dist-tag latest -> 4.6.29 and redeploy the Hub Lambda from tag v4.6.29 (36e01a038f52); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
28278
- v3Target: "v4.6.29 (@mutmutco/cli@4.6.29, tag commit 36e01a038f52 \u2014 last known-good release carrying the repo-index v4-only contract)"
28330
+ mechanism: "npm dist-tag latest -> 4.6.31 and redeploy the Hub Lambda from tag v4.6.31 (53e22eaf158d); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
28331
+ v3Target: "v4.6.31 (@mutmutco/cli@4.6.31, tag commit 53e22eaf158d \u2014 last known-good release carrying the repo-index v4-only contract)"
28279
28332
  }
28280
28333
  },
28281
28334
  {
@@ -39363,8 +39416,9 @@ function blessedHubActionPins(constants = gate_budget_exports) {
39363
39416
  }
39364
39417
  return pins;
39365
39418
  }
39419
+ var PIN_COMMIT_FORMAT = "commit-format:runbook-unaffected";
39366
39420
  function pinTableDigest(pins) {
39367
- return (0, import_node_crypto11.createHash)("sha1").update([...pins].map(([a, s]) => `${a}@${s}`).sort().join("\n")).digest("hex");
39421
+ return (0, import_node_crypto11.createHash)("sha1").update([PIN_COMMIT_FORMAT, ...[...pins].map(([a, s]) => `${a}@${s}`).sort()].join("\n")).digest("hex");
39368
39422
  }
39369
39423
  var HUB_ACTION_USE = /^(\s*-?\s*uses:\s*['"]?mutmutco\/mmi-hub\/\.github\/actions\/)([\w.-]+)@([^\s'"#]+)([^\n]*)$/i;
39370
39424
  function staleHubActionPins(body, pins = blessedHubActionPins()) {
@@ -39484,7 +39538,9 @@ async function runPinTick(input) {
39484
39538
  if (!next.changes.length) continue;
39485
39539
  lines2.push(...next.changes.map((c) => `- \`${path2}:${c.line}\` ${c.action}: \`${c.from.slice(0, 12)}\` \u2192 \`${c.to.slice(0, 12)}\``));
39486
39540
  const tmp = (0, import_node_path29.join)((0, import_node_os17.tmpdir)(), `mmi-pin-propagate-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
39487
- const put = { message: `ci: pin MMI-Hub actions in ${path2} to the blessed SHAs`, content: encodeGitHubContents(next.body), branch };
39541
+ const put = { message: `ci: pin MMI-Hub actions in ${path2} to the blessed SHAs
39542
+
39543
+ Runbook-Unaffected: SHA-only MMI-Hub action pin move; no workflow step changes`, content: encodeGitHubContents(next.body), branch };
39488
39544
  if (file.sha) put.sha = file.sha;
39489
39545
  (0, import_node_fs32.writeFileSync)(tmp, JSON.stringify(put), "utf8");
39490
39546
  try {
@@ -42451,9 +42507,19 @@ async function runPrLandOnce(prNumber, options, deps) {
42451
42507
  }
42452
42508
  }
42453
42509
  }
42454
- const checksWaitError = (checksWait) => {
42510
+ const isConflicting = (state) => state.mergeStateStatus === "DIRTY" || state.mergeable === "CONFLICTING";
42511
+ const conflictError = () => `pr land #${prNumber}: branch conflicts with base \u2014 \`git fetch origin\`, merge origin/development into the branch (never rebase a pushed branch), resolve the conflicts, regenerate any checked-in artifacts, commit and push, then re-run \`mmi-cli devops pr land ${prNumber} --repo ${repo}\` once${hasQueue ? `; ${prQueueReadHint(prNumber, repo)}` : ""}`;
42512
+ const readPrState = async () => {
42513
+ const state = await deps.readPrState(prNumber, repo);
42514
+ if (!isConflicting(state)) return state;
42515
+ await (deps.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms))))(PR_HEAD_UPDATE_API_READ_DELAY_MS);
42516
+ return deps.readPrState(prNumber, repo);
42517
+ };
42518
+ let prState;
42519
+ const checksWaitError = async (checksWait) => {
42455
42520
  if (checksWait.status === "conflicting") {
42456
- return `checks-wait conflicting: ${checksWait.reason ?? "PR is conflicting with the base branch"}`;
42521
+ prState = await readPrState();
42522
+ return isConflicting(prState) ? conflictError() : void 0;
42457
42523
  }
42458
42524
  if (checksWait.status === "failure" || checksWait.status === "timeout" || checksWait.status === "rate-limited") {
42459
42525
  return `checks-wait ${checksWait.status}${checksWait.detail ? `: ${checksWait.detail}` : ""}`;
@@ -42461,11 +42527,11 @@ async function runPrLandOnce(prNumber, options, deps) {
42461
42527
  return void 0;
42462
42528
  };
42463
42529
  base.checksWait = await deps.waitForChecks(prNumber, repo);
42464
- const waitError = checksWaitError(base.checksWait);
42465
- if (waitError) return { ...base, error: hasQueue ? `${waitError} \u2014 ${prQueueReadHint(prNumber, repo)}` : waitError };
42530
+ const waitError = await checksWaitError(base.checksWait);
42531
+ if (waitError) return { ...base, error: hasQueue && base.checksWait.status !== "conflicting" ? `${waitError} \u2014 ${prQueueReadHint(prNumber, repo)}` : waitError };
42466
42532
  const driftAfterChecks = await headDrift("after checks");
42467
42533
  if (driftAfterChecks) return driftAfterChecks;
42468
- let prState = await deps.readPrState(prNumber, repo);
42534
+ prState = prState ?? await readPrState();
42469
42535
  if (!hasQueue && pinned && prState.mergeStateStatus === "BEHIND") {
42470
42536
  return {
42471
42537
  ...base,
@@ -42480,16 +42546,11 @@ async function runPrLandOnce(prNumber, options, deps) {
42480
42546
  return { ...base, error: `pr land #${prNumber}: ${e.message}` };
42481
42547
  }
42482
42548
  base.checksWait = await deps.waitForChecks(prNumber, repo);
42483
- const reWaitError = checksWaitError(base.checksWait);
42484
- if (reWaitError) return { ...base, error: hasQueue ? `${reWaitError} \u2014 ${prQueueReadHint(prNumber, repo)}` : reWaitError };
42485
- prState = await deps.readPrState(prNumber, repo);
42486
- }
42487
- if (prState.mergeStateStatus === "DIRTY" || prState.mergeable === "CONFLICTING") {
42488
- return {
42489
- ...base,
42490
- error: `pr land #${prNumber}: branch conflicts with base \u2014 rebase/merge development, rebuild any checked-in artifacts, then re-push before landing${hasQueue ? `; ${prQueueReadHint(prNumber, repo)}` : ""}`
42491
- };
42549
+ const reWaitError = await checksWaitError(base.checksWait);
42550
+ if (reWaitError) return { ...base, error: hasQueue && base.checksWait.status !== "conflicting" ? `${reWaitError} \u2014 ${prQueueReadHint(prNumber, repo)}` : reWaitError };
42551
+ prState = await readPrState();
42492
42552
  }
42553
+ if (isConflicting(prState)) return { ...base, error: conflictError() };
42493
42554
  if (hasQueue && !deps.enqueueQueue) throw new Error("queue-only enqueue adapter unavailable; no merge dispatched");
42494
42555
  const merge = await mergeAutoWithTransientRetry(prNumber, repo, hasQueue ? { ...deps, mergeAuto: (pr, target) => deps.enqueueQueue(pr, target, pinned) } : deps);
42495
42556
  base.mergeStatus = merge.mergeStatus;
@@ -44060,13 +44121,13 @@ async function inspectQueuePreparation(pr, repo, options) {
44060
44121
  const parsed = parseRestClosingGuardInput({ state: pull.state, title: pull.title, body: pull.body }, commits, repo);
44061
44122
  parsed.closing = closing;
44062
44123
  const policy = { title: repository.squash_merge_commit_title, body: repository.squash_merge_commit_message };
44063
- const preparation = (reason, targets, detail) => ({
44124
+ const preparation = (reason, targets, detail, nextAction) => ({
44064
44125
  mergeStatus: "failed",
44065
44126
  preparation: {
44066
44127
  status: "preparation-needed",
44067
44128
  reason,
44068
44129
  issueTargets: targets.map((number) => `${repo}#${number}`),
44069
- nextAction: reason === "queue-message" ? `PR author: if PR #${pr} is meant to close ${targets.length ? targets.map((number) => `#${number}`).join(", ") : "#N"}, add ${targets.length ? targets.map((number) => `\`Closes #${number}\``).join(", ") : "`Closes #N`"} to the PR body (\`mmi-cli devops pr edit ${pr} --repo ${repo} --body-file .jerv/tmp/pr-${pr}-body.md\`) and rerun \`mmi-cli devops pr land ${pr} --repo ${repo}\` without pushing a new head; if ${targets.length ? targets.map((number) => `#${number}`).join(", ") : "#N"} must stay open, remove the closing phrases from the source commit or PR text using ${targets.length ? targets.map((number) => `\`Part of #${number}\``).join(", ") : "`Part of #N`"} / ${targets.length ? targets.map((number) => `\`Refs #${number}\``).join(", ") : "`Refs #N`"}, then re-run checks and \`pr land\` with the prepared head. A queue cannot accept --squash-body-file.` : "PR author: prepare the source commit messages and required trailers without a body override; re-run checks and pr land. GitHub may append coauthors even to one source commit, so source consolidation alone does not prove the generated message passes test policy. The combined queue candidate must pass with the updated shared trailer reader. Keep repository settings unchanged."
44130
+ nextAction: nextAction ?? (reason === "queue-message" ? `PR author: if PR #${pr} is meant to close ${targets.length ? targets.map((number) => `#${number}`).join(", ") : "#N"}, add ${targets.length ? targets.map((number) => `\`Closes #${number}\``).join(", ") : "`Closes #N`"} to the PR body (\`mmi-cli devops pr edit ${pr} --repo ${repo} --body-file .jerv/tmp/pr-${pr}-body.md\`) and rerun \`mmi-cli devops pr land ${pr} --repo ${repo}\` without pushing a new head; if ${targets.length ? targets.map((number) => `#${number}`).join(", ") : "#N"} must stay open, remove the closing phrases from the source commit or PR text using ${targets.length ? targets.map((number) => `\`Part of #${number}\``).join(", ") : "`Part of #N`"} / ${targets.length ? targets.map((number) => `\`Refs #${number}\``).join(", ") : "`Refs #N`"}, then re-run checks and \`pr land\` with the prepared head. A queue cannot accept --squash-body-file.` : "PR author: prepare the source commit messages and required trailers without a body override; re-run checks and pr land. GitHub may append coauthors even to one source commit, so source consolidation alone does not prove the generated message passes test policy. The combined queue candidate must pass with the updated shared trailer reader. Keep repository settings unchanged.")
44070
44131
  },
44071
44132
  error: `queue preparation needed: ${detail}`
44072
44133
  });
@@ -44149,7 +44210,17 @@ ${pull.body}`;
44149
44210
  }
44150
44211
  };
44151
44212
  }
44152
- return { kind: "refused", result: preparation("queue-message-override", [], "the requested sanitized body or trailer arrangement cannot be sent to queue admission") };
44213
+ const trailerKeys = (message2) => {
44214
+ const keys = [...new Set(parseTrailers(message2.trim(), process.cwd()).map((trailer) => trailer.split(":")[0].trim()))];
44215
+ return keys.length ? keys.join(", ") : "none";
44216
+ };
44217
+ const source = options.bodyText != null ? "the explicit body override" : "the body composed from the source commit messages";
44218
+ const detail = `the requested sanitized body or trailer arrangement cannot be sent to queue admission: queue merge method ${method}, squash body policy ${policy.body ?? "unknown"}; required body is ${source} (trailer keys: ${trailerKeys(`${headline}
44219
+
44220
+ ${requiredBody}`)}), but the message the queue would send carries trailer keys: ${trailerKeys(actualMessage)}`;
44221
+ const next = options.bodyText != null ? `PR author: a merge queue accepts no body override; drop --body/--squash-body-file and place the required trailer(s) where the queue's ${method}${method === "SQUASH" ? ` (${policy.body})` : ""} message reads them, then rerun \`mmi-cli devops pr land ${pr} --repo ${repo}\`. Keep repository settings unchanged.` : method !== "SQUASH" ? `PR author: the queue's ${method} method keeps the original commit messages, so the required trailer(s) must sit in a source commit message's final trailer block; amend the commit, push, re-run checks and \`mmi-cli devops pr land ${pr} --repo ${repo}\`. Keep repository settings unchanged.` : policy.body === "BLANK" ? `PR author: squash body policy BLANK sends an empty body, so no trailer from the commits or PR body reaches the queue commit; the waiver cannot survive queue admission with unchanged repository settings \u2014 remove the need for the override or ask a maintainer. Keep repository settings unchanged.` : `PR author: squash body policy ${policy.body} does not reproduce the composed body; consolidate the required trailer(s) into one source commit message's final trailer block, push, re-run checks and \`mmi-cli devops pr land ${pr} --repo ${repo}\`. Keep repository settings unchanged.`;
44222
+ const caveat = " GitHub may append coauthors even to one source commit, so source consolidation alone does not prove the generated message passes test policy; the combined queue candidate must pass with the shared trailer reader.";
44223
+ return { kind: "refused", result: preparation("queue-message-override", [], detail, next + caveat) };
44153
44224
  }
44154
44225
  return { kind: "ready", head, pull, read, queueRead, write, ...verdict.message ? { warning: verdict.message } : {} };
44155
44226
  }
@@ -46215,9 +46286,41 @@ function registerBoardCommands(program3) {
46215
46286
  }
46216
46287
  });
46217
46288
  withExamples(mutating(
46218
- board.command("claim <issues...>").description("claim issues: assign them and move their Project v2 Status to In Progress \u2014 idempotent, so an item already yours and In Progress succeeds unchanged (one or more refs); the board scan rides the Hub snapshot, same as board read").addHelpText("after", "\nclaim reads the board through the same Hub snapshot leg as `board read` (the App-installation\ncredential, never your personal GraphQL pool); the direct user-auth read is an emergency fallback\nand is named in a Warning line after the verdict (#6162).\n\nevery claim stamps a lane-identity marker comment on the issue (`<!-- mmi-claim: \u2026 -->`,\nsurface/session@host) so other agents can attribute the hold (#3727). The session is the\nhost-exported id when the surface provides one, otherwise a per-process `synth-` fallback \u2014\na claim is never anonymous (#5245). `board show`, doctor and unclaim read the latest marker.\n\nsame-owner resume (#6035): when the prior marker was posted by YOUR login on THIS host and its\nlocal session is verifiably dead (transcript probe), the claim proceeds as a resume without\n--force and names the evidence. A live, foreign-host, or unprobeable prior lane still refuses.\n\nreclaim from In Review (#6339): an In Review item with no holder, or one held by the claiming\nlogin, is claimable \u2014 it moves back to In Progress and the receipt names the status it came from.\nAn In Review item another login holds is refused (ask the holder, or wait for the review to land),\nand Done stays refused. The board status is the authority; no PR state is consulted.\n").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--force", "take an item already claimed by another lane that shows live evidence of active work (#3727)").option("--check", "read-only: run every claim gate and report the verdict, writing nothing \u2014 exits 1 with the same refusal a real claim would raise (#4511)").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails"),
46289
+ board.command("claim [issues...]").description("claim issues: assign them and move their Project v2 Status to In Progress \u2014 idempotent, so an item already yours and In Progress succeeds unchanged (one or more refs, or --all for the whole board); the board scan rides the Hub snapshot, same as board read").addHelpText("after", "\nclaim reads the board through the same Hub snapshot leg as `board read` (the App-installation\ncredential, never your personal GraphQL pool); the direct user-auth read is an emergency fallback\nand is named in a Warning line after the verdict (#6162).\n\nevery claim stamps a lane-identity marker comment on the issue (`<!-- mmi-claim: \u2026 -->`,\nsurface/session@host) so other agents can attribute the hold (#3727). The session is the\nhost-exported id when the surface provides one, otherwise a per-process `synth-` fallback \u2014\na claim is never anonymous (#5245). `board show`, doctor and unclaim read the latest marker.\n\nsame-owner resume (#6035): when the prior marker was posted by YOUR login on THIS host and its\nlocal session is verifiably dead (transcript probe), the claim proceeds as a resume without\n--force and names the evidence. A live, foreign-host, or unprobeable prior lane still refuses.\n\nreclaim from In Review (#6339): an In Review item with no holder, or one held by the claiming\nlogin, is claimable \u2014 it moves back to In Progress and the receipt names the status it came from.\nAn In Review item another login holds is refused (ask the holder, or wait for the review to land),\nand Done stays refused. The board status is the authority; no PR state is consulted.\n\nwhole board (#9930): --all reads the board once and claims every open item of this repo's primary\nboard that the per-ref claim may take (yours, unassigned Todo, unowned In Progress), carrying each\nitem's branch/worktree lane evidence forward like a per-ref claim. It prints one line per item, then\nthe refs it skipped and why (assigned to another login, In Review, closed, not an issue). Without\n--force a live foreign lane is skipped with its reason, never stolen; --force takes it as the\nper-ref --force would. --all with explicit refs is an error.\n").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--force", "take an item already claimed by another lane that shows live evidence of active work (#3727)").option("--all", "claim every open claimable item on the current repo's board in one pass (combine with --force/--repo; not with explicit refs) (#9930)").option("--check", "read-only: run every claim gate and report the verdict, writing nothing \u2014 exits 1 with the same refusal a real claim would raise (#4511)").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails"),
46219
46290
  (_opts, args) => ({ command: "oracle board claim", issues: args[0] ?? [] })
46220
- ).action(async (issueRefs, o) => {
46291
+ ).action(async (issueRefs = [], o) => {
46292
+ const argsError = claimArgsError(issueRefs, o.all);
46293
+ if (argsError) return failGraceful(`oracle board claim failed: ${argsError}`);
46294
+ if (o.all) {
46295
+ try {
46296
+ const config = await loadConfigForBoardSelector2("", o.repo);
46297
+ const all = await claimAllBoardIssues({
46298
+ config,
46299
+ repo: o.repo,
46300
+ assignee: o.for,
46301
+ force: o.force,
46302
+ check: o.check,
46303
+ allowPartial: o.allowPartial
46304
+ }, { snapshot: registryClientDeps(config) });
46305
+ if (o.json) {
46306
+ console.log(JSON.stringify({ results: all.results, skipped: all.skipped }));
46307
+ printClaimWarnings(all.warnings, true);
46308
+ } else {
46309
+ for (const result of all.results) {
46310
+ console.log(result.claimed ? claimVerdict(result.ref, result) : `Skipped ${result.ref}: ${result.reason}`);
46311
+ }
46312
+ for (const skip of all.skipped) console.log(`Skipped ${skip.ref}: ${skip.reason}`);
46313
+ const landed = all.results.filter((r) => r.claimed).length;
46314
+ console.log(`claim --all: ${landed} of ${all.results.length} attempted item(s) claimed; ${all.skipped.length} not attempted`);
46315
+ printClaimWarnings(all.warnings);
46316
+ }
46317
+ if (all.failed > 0) process.exitCode = 1;
46318
+ } catch (e) {
46319
+ if (refuseRateLimited(e, o.json)) return;
46320
+ return failGraceful(`oracle board claim failed: ${e.message}`);
46321
+ }
46322
+ return;
46323
+ }
46221
46324
  if (issueRefs.length === 1) {
46222
46325
  const issueRef = issueRefs[0];
46223
46326
  try {
@@ -46276,11 +46379,13 @@ function registerBoardCommands(program3) {
46276
46379
  }), [
46277
46380
  "mmi-cli oracle board claim 2680",
46278
46381
  "mmi-cli oracle board claim 2680 2681 --for teammate-login",
46279
- "mmi-cli oracle board claim 2680 --check"
46382
+ "mmi-cli oracle board claim 2680 --check",
46383
+ "mmi-cli oracle board claim --all --force"
46280
46384
  ], [
46281
46385
  "Pass raw issue numbers/refs, not URLs.",
46282
46386
  "Claim already assigns and moves Status to In Progress, so do not also board move it.",
46283
46387
  "Multiple refs are handled as a batch and return per-item results.",
46388
+ "To claim the whole board, use --all (with --force to take live lanes) instead of a hand-built ref list.",
46284
46389
  "--check is the live gate read (it calls GitHub); --dry-run only echoes the parsed argv plan.",
46285
46390
  // #5552: agents guess `oracle issue claim`; that route does not exist — board claim is the only write.
46286
46391
  "Never run `oracle issue claim` \u2014 claims are board mutations; only `oracle board claim <ref>` is valid.",
@@ -60790,9 +60895,13 @@ async function checkDocsIndexAtHead(opts, deps) {
60790
60895
  }
60791
60896
 
60792
60897
  // src/pr-create-preparation.ts
60898
+ function rejectionReason(error) {
60899
+ const text = (error instanceof Error ? error.message : String(error)).trim().split("\n")[0] || "no error detail";
60900
+ return text.length > 300 ? `${text.slice(0, 300)}\u2026` : text;
60901
+ }
60793
60902
  async function preparePrCreate(input, deps = createPreparationDeps(input)) {
60794
60903
  const observations = [];
60795
- const refs = await deps.candidate().catch(() => ({ findings: ["candidate refs unavailable; verify the repository and pushed head/base before retrying"] }));
60904
+ const refs = await deps.candidate().catch((error) => ({ findings: [`candidate refs unavailable (${rejectionReason(error)}); verify the repository and pushed head/base before retrying`] }));
60796
60905
  observations.push(...refs.findings.map((detail) => ({ check: "refs", state: "unavailable", detail })));
60797
60906
  if (refs.candidate) observations.push({ check: "refs", state: "passed", detail: `candidate ${refs.candidate.headSha} against ${refs.candidate.baseSha}` });
60798
60907
  const reads = await Promise.allSettled([
@@ -60803,10 +60912,10 @@ async function preparePrCreate(input, deps = createPreparationDeps(input)) {
60803
60912
  const [claims, docs, policy] = reads;
60804
60913
  if (claims.status === "fulfilled") {
60805
60914
  observations.push(...claims.value.findings.length ? claims.value.findings.map((detail) => ({ check: "claims", state: "failed", detail })) : [{ check: "claims", state: "passed", detail: "issue and claim evidence read" }]);
60806
- } else observations.push({ check: "claims", state: "unavailable", detail: "issue/claim evidence unavailable; retry the read" });
60807
- if (docs.status === "rejected") observations.push({ check: "docs", state: "unavailable", detail: "committed docs observation unavailable; retry the read" });
60915
+ } else observations.push({ check: "claims", state: "unavailable", detail: `issue/claim evidence unavailable (${rejectionReason(claims.reason)}); retry the read` });
60916
+ if (docs.status === "rejected") observations.push({ check: "docs", state: "unavailable", detail: `committed docs observation unavailable (${rejectionReason(docs.reason)}); retry the read` });
60808
60917
  else if (docs.value) observations.push({ check: "docs", state: docs.value.state, detail: [docs.value.detail, docs.value.fix].filter(Boolean).join(" \u2014 ") });
60809
- if (policy.status === "rejected") observations.push({ check: "policy", state: "unavailable", detail: "committed test-policy observation unavailable; check policy validity and fetch full candidate history before retrying" });
60918
+ if (policy.status === "rejected") observations.push({ check: "policy", state: "unavailable", detail: `committed test-policy observation unavailable (${rejectionReason(policy.reason)}); check policy validity and fetch full candidate history before retrying` });
60810
60919
  else if (policy.value) observations.push(policy.value);
60811
60920
  const findings = observations.filter((item) => item.state === "failed" || item.state === "unavailable");
60812
60921
  let attempted = false;
@@ -60938,7 +61047,12 @@ function createPreparationDeps(input, io = {}) {
60938
61047
  return result("failed", `${refusal.error} \u2014 ${refusal.preparation.nextAction}`);
60939
61048
  }
60940
61049
  }
60941
- const policy = runTestPolicy(candidate.root, { candidate: { head: candidate.headSha, base: candidate.baseSha } });
61050
+ let policy;
61051
+ try {
61052
+ policy = runTestPolicy(candidate.root, { candidate: { head: candidate.headSha, base: candidate.baseSha } });
61053
+ } catch (error) {
61054
+ return result("unavailable", `committed test policy could not be evaluated: ${rejectionReason(error)} \u2014 fix test-policy.json if it is malformed, or run \`git fetch --unshallow\` if a history read failed`);
61055
+ }
60942
61056
  return result(policy.ok ? "passed" : "failed", policy.ok ? `committed test policy passed at ${candidate.headSha}` : policy.findings.map((finding) => finding.detail).join("\n"));
60943
61057
  }
60944
61058
  };
@@ -64968,9 +65082,13 @@ function projectInfoOutcome(projectInfoSync) {
64968
65082
  }
64969
65083
  function buildReleaseVerdict(commandName, result, projectInfoSync) {
64970
65084
  const alignmentPending = result.devRollForward?.status === "pr-pending" || result.rcAlignment?.status === "pr-pending";
65085
+ const deployOutcome = reduceFollowUpOutcomes([
65086
+ deployFollowUpOutcome(result.deployStatus),
65087
+ deployFollowUpOutcome(aggregateWorkflowRuns(result.workflowRuns ?? []))
65088
+ ]);
64971
65089
  const followUpStatus = deriveTrainFollowUpStatus({
64972
65090
  projectInfo: projectInfoOutcome(projectInfoSync),
64973
- deploy: deployFollowUpOutcome(result.deployStatus),
65091
+ deploy: deployOutcome,
64974
65092
  rcRetirement: result.rcRetirement === "failed" ? result.rcRetirementCategory === "wait-timeout" ? "unresolved" : "failure" : "ok",
64975
65093
  alignment: alignmentPending ? "unresolved" : "ok",
64976
65094
  foldPort: "ok"
@@ -64990,9 +65108,14 @@ function buildReleaseVerdict(commandName, result, projectInfoSync) {
64990
65108
  }
64991
65109
  function buildResumeReleaseVerdict(result, projectInfoSync) {
64992
65110
  const resumeStatus = resumeFollowUpOf(result.state);
65111
+ const deployOutcome = reduceFollowUpOutcomes([
65112
+ // No dispatch (e.g. an alignment-only resume) keeps the old 'ok' deploy dimension.
65113
+ deployFollowUpOutcome(result.dispatch?.deployStatus ?? "success"),
65114
+ deployFollowUpOutcome(aggregateWorkflowRuns(result.dispatch?.workflowRuns ?? []))
65115
+ ]);
64993
65116
  const followUpStatus = deriveTrainFollowUpStatus({
64994
65117
  projectInfo: projectInfoOutcome(projectInfoSync),
64995
- deploy: "ok",
65118
+ deploy: deployOutcome,
64996
65119
  rcRetirement: "ok",
64997
65120
  alignment: resumeStatus === "failed" ? "failure" : resumeStatus === "pending" ? "unresolved" : "ok",
64998
65121
  foldPort: "ok"
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.6.29",
3
+ "version": "4.6.31",
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.6.29",
3
+ "version": "4.6.31",
4
4
  "description": "MMI workflow skills and org gates delivery.",
5
5
  "author": {
6
6
  "name": "MMI Future",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  ".github/workflows/agent-pr.yml": {
3
- "blob": "dff394ae296224bc1d40f2ab4953d30a89bc5a91",
4
- "reason": "runs-on only: the two seconds-long guard legs moved off the busy CI pool to the reserved journey/deploy pool with a 5-minute cap (#9211); job logic, steps, permissions and operator responsibilities unchanged"
3
+ "blob": "e85a80c65b26531a330917fb4b5d8f563a0f0d0c",
4
+ "reason": "sole-write-path guard reads retry transient GitHub API 5xx/429/transport failures up to 3 attempts, then fail closed with the same messages (#9911); permissions, refs, guarded paths and operator responsibilities unchanged"
5
5
  },
6
6
  "scripts/next-version.mjs": {
7
7
  "blob": "2c1985b59edc779695479559074e7654c1574a11",
@@ -1,2 +1,2 @@
1
- In the suite lead (MM-Work) a plain rcand, release or hotfix refuses: run `mmi-cli devops suite rcand --owner-go` or `mmi-cli devops suite release --owner-go` instead (a lead-only cut needs `--suite-solo --owner-go`). On your own, run only the read-only previews `mmi-cli devops suite rcand --dry-run` and `mmi-cli devops suite hotfix <member> --dry-run`; `suite release` and `suite abandon` have no dry-run, so never run them yourself. A real suite rcand, release, hotfix or abandon runs only on the owner's go, executed by the owner or the suite orchestrator. A suite member cannot run standalone rcand, release or hotfix: cut from the suite lead; only a backward-compatible solo backend release/hotfix with explicit owner approval may pass `--suite-solo --owner-go`, and its MM-Work development pin PR must land before the suite is in sync.
1
+ In the suite lead (MM-Work) a plain rcand, release or hotfix refuses: run `mmi-cli devops suite rcand --owner-go` or `mmi-cli devops suite release --owner-go` instead (a lead-only cut needs `--suite-solo --owner-go`). On your own, run only the read-only previews `mmi-cli devops suite rcand --dry-run` and `mmi-cli devops suite hotfix <member> --dry-run`; `suite release` and `suite abandon` have no dry-run, so never run them yourself. A real suite rcand, release, hotfix or abandon runs only on the owner's go, executed by the owner or the suite orchestrator. A suite member cannot run standalone rcand, release or hotfix: cut from the suite lead; only a backward-compatible solo backend release/hotfix with explicit owner approval may pass `--suite-solo --owner-go`, and its MM-Work development pin PR must land before the suite is in sync. A suite member never asks for, proposes or prompts an rc, release or hotfix — the suite orchestrator is the only voice that brings a cut to Jervaise, so a member session states what landed and stops instead of offering a cut.
2
2
 
@@ -1,4 +1,4 @@
1
- In the suite lead (MM-Work) a plain rcand, release or hotfix refuses: the suite path is `mmi-cli devops suite rcand --owner-go` or `mmi-cli devops suite release --owner-go` (a lead-only cut needs `--suite-solo --owner-go`). `--owner-go` records Jervaise's explicit go for that one cut; it is never the agent's own flag. Never pass it on your own authority: a held train or a stale MM-Work cut (`suite abandon --owner-go`, then `suite rcand --owner-go`) is handed to the suite orchestrator with that exact command sequence, and runs only after the owner says go. On your own, run only the read-only previews `mmi-cli devops suite rcand --dry-run` and `mmi-cli devops suite hotfix <member> --dry-run`; `suite release` and `suite abandon` have no dry-run, so never run them yourself. A real suite rcand, release, hotfix or abandon runs only on the owner's go, executed by the owner or the suite orchestrator. A suite member cannot run standalone rcand, release or hotfix: cut from the suite lead; only a backward-compatible solo backend release/hotfix with explicit owner approval may pass `--suite-solo --owner-go`, and its MM-Work development pin PR must land before the suite is in sync.
1
+ In the suite lead (MM-Work) a plain rcand, release or hotfix refuses: the suite path is `mmi-cli devops suite rcand --owner-go` or `mmi-cli devops suite release --owner-go` (a lead-only cut needs `--suite-solo --owner-go`). `--owner-go` records Jervaise's explicit go for that one cut; it is never the agent's own flag. Never pass it on your own authority: a held train or a stale MM-Work cut (`suite abandon --owner-go`, then `suite rcand --owner-go`) is handed to the suite orchestrator with that exact command sequence, and runs only after the owner says go. On your own, run only the read-only previews `mmi-cli devops suite rcand --dry-run` and `mmi-cli devops suite hotfix <member> --dry-run`; `suite release` and `suite abandon` have no dry-run, so never run them yourself. A real suite rcand, release, hotfix or abandon runs only on the owner's go, executed by the owner or the suite orchestrator. A suite member cannot run standalone rcand, release or hotfix: cut from the suite lead; only a backward-compatible solo backend release/hotfix with explicit owner approval may pass `--suite-solo --owner-go`, and its MM-Work development pin PR must land before the suite is in sync. A suite member never asks for, proposes or prompts an rc, release or hotfix — the suite orchestrator is the only voice that brings a cut to Jervaise, so a member session states what landed and stops instead of offering a cut.
2
2
 
3
3
  Support sessions (MMI-Hub, Jerv-Hub, Jerv-JervCode) are not suite members and hold standing owner authorization (2026-10-25) to cut and ship releases at their discretion during suite runs without repeated per-release approval, and under the owner's 2026-11-01 ruling each support session strives during a suite run to finish its own board and cut a patch release for landed-but-unreleased merges so suite members pick the update up in that run instead of waiting for a later cut, keeping all standard release preflight, gate, and verification checks.
4
4
 
@@ -11,7 +11,7 @@ Bare `minor|major|patch` (default `patch`), never a flag. A production release n
11
11
 
12
12
  ## Lane
13
13
 
14
- Read `mmi-cli oracle org project get {owner}/{repo} --json` and name the lane ([verification.md](verification.md) Lane rules); unreadable META → stop. [suite.md](suite.md) for MM-Work; [vercel.md](vercel.md) for Vercel.
14
+ Read `mmi-cli oracle org project get {owner}/{repo} --json` and name the lane ([verification.md](verification.md) Lane rules); unreadable META → stop. On `releaseTrack: single` (MM-Work and its modules) or `trunk` there is no release lane. On single track a merge to development auto-deploys the `main` stage, and release, rcand and hotfix refuse. A merged single-track change is live once that deploy is green. A trunk repo follows its own declared publishing workflow. Never tell the owner a landed change waits on a release. [suite.md](suite.md) for suite cuts; [vercel.md](vercel.md) for Vercel.
15
15
 
16
16
  ## Preflight
17
17
 
@@ -1,3 +1,5 @@
1
+ MM-Work and most of its modules are `releaseTrack: single` ([work-and-releases.md](../org-guide/references/work-and-releases.md)). A merge to development auto-deploys their `main` stage, so a landed change is live once that deploy is green. Never tell the owner such a change waits on a release. Read each repo's `releaseTrack` with `mmi-cli oracle org project get <owner/repo> --json` before naming any cut. A suite cut also cuts the MM-Work lead's rcand, which refuses while MM-Work is single track.
2
+
1
3
  In the suite lead (MM-Work) a plain rcand, release or hotfix refuses: the suite path is `mmi-cli devops suite rcand --owner-go` or `mmi-cli devops suite release --owner-go` (a lead-only cut needs `--suite-solo --owner-go`). `--owner-go` records Jervaise's explicit go for that one cut; it is never the agent's own flag. Never pass it on your own authority: a held train or a stale MM-Work cut (`suite abandon --owner-go`, then `suite rcand --owner-go`) is handed to the suite orchestrator with that exact command sequence, and runs only after the owner says go. On your own, run only the read-only previews `mmi-cli devops suite rcand --dry-run` and `mmi-cli devops suite hotfix <member> --dry-run`; `suite release` and `suite abandon` have no dry-run, so never run them yourself. A real suite rcand, release, hotfix or abandon runs only on the owner's go, executed by the owner or the suite orchestrator. A suite member cannot run standalone rcand, release or hotfix: cut from the suite lead; only a backward-compatible solo backend release/hotfix with explicit owner approval may pass `--suite-solo --owner-go`, and its MM-Work development pin PR must land before the suite is in sync. A suite member never asks for, proposes or prompts an rc, release or hotfix — the suite orchestrator is the only voice that brings a cut to Jervaise, so a member session states what landed and stops instead of offering a cut.
2
4
 
3
5
  Support sessions (MMI-Hub, Jerv-Hub, Jerv-JervCode) are not suite members and hold standing owner authorization (2026-10-25) to cut and ship releases at their discretion during suite runs without repeated per-release approval, and under the owner's 2026-11-01 ruling each support session strives during a suite run to finish its own board and cut a patch release for landed-but-unreleased merges so suite members pick the update up in that run instead of waiting for a later cut, keeping all standard release preflight, gate, and verification checks.