@mutmutco/claude-plugin 4.6.30 → 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,7 +1,7 @@
1
1
  {
2
2
  "name": "mmi",
3
3
  "displayName": "MMI",
4
- "version": "4.6.30",
4
+ "version": "4.6.31",
5
5
  "mmiCompat": "4.x",
6
6
  "description": "MMI workflow skills and org gates delivery.",
7
7
  "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.30",
28252
- tag: "v4.6.30",
28253
- commit: "52569fa5c01e",
28254
- npm: "@mutmutco/cli@4.6.30"
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.30"
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.30 and redeploy the Hub Lambda from tag v4.6.30 (52569fa5c01e); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
28278
- v3Target: "v4.6.30 (@mutmutco/cli@4.6.30, tag commit 52569fa5c01e \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
  {
@@ -42455,7 +42508,7 @@ async function runPrLandOnce(prNumber, options, deps) {
42455
42508
  }
42456
42509
  }
42457
42510
  const isConflicting = (state) => state.mergeStateStatus === "DIRTY" || state.mergeable === "CONFLICTING";
42458
- const conflictError = () => `pr land #${prNumber}: branch conflicts with base \u2014 merge origin/development into the branch (never rebase a pushed branch), rebuild any checked-in artifacts, then push before landing${hasQueue ? `; ${prQueueReadHint(prNumber, repo)}` : ""}`;
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)}` : ""}`;
42459
42512
  const readPrState = async () => {
42460
42513
  const state = await deps.readPrState(prNumber, repo);
42461
42514
  if (!isConflicting(state)) return state;
@@ -44068,13 +44121,13 @@ async function inspectQueuePreparation(pr, repo, options) {
44068
44121
  const parsed = parseRestClosingGuardInput({ state: pull.state, title: pull.title, body: pull.body }, commits, repo);
44069
44122
  parsed.closing = closing;
44070
44123
  const policy = { title: repository.squash_merge_commit_title, body: repository.squash_merge_commit_message };
44071
- const preparation = (reason, targets, detail) => ({
44124
+ const preparation = (reason, targets, detail, nextAction) => ({
44072
44125
  mergeStatus: "failed",
44073
44126
  preparation: {
44074
44127
  status: "preparation-needed",
44075
44128
  reason,
44076
44129
  issueTargets: targets.map((number) => `${repo}#${number}`),
44077
- 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.")
44078
44131
  },
44079
44132
  error: `queue preparation needed: ${detail}`
44080
44133
  });
@@ -44157,7 +44210,17 @@ ${pull.body}`;
44157
44210
  }
44158
44211
  };
44159
44212
  }
44160
- 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) };
44161
44224
  }
44162
44225
  return { kind: "ready", head, pull, read, queueRead, write, ...verdict.message ? { warning: verdict.message } : {} };
44163
44226
  }
@@ -46223,9 +46286,41 @@ function registerBoardCommands(program3) {
46223
46286
  }
46224
46287
  });
46225
46288
  withExamples(mutating(
46226
- 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"),
46227
46290
  (_opts, args) => ({ command: "oracle board claim", issues: args[0] ?? [] })
46228
- ).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
+ }
46229
46324
  if (issueRefs.length === 1) {
46230
46325
  const issueRef = issueRefs[0];
46231
46326
  try {
@@ -46284,11 +46379,13 @@ function registerBoardCommands(program3) {
46284
46379
  }), [
46285
46380
  "mmi-cli oracle board claim 2680",
46286
46381
  "mmi-cli oracle board claim 2680 2681 --for teammate-login",
46287
- "mmi-cli oracle board claim 2680 --check"
46382
+ "mmi-cli oracle board claim 2680 --check",
46383
+ "mmi-cli oracle board claim --all --force"
46288
46384
  ], [
46289
46385
  "Pass raw issue numbers/refs, not URLs.",
46290
46386
  "Claim already assigns and moves Status to In Progress, so do not also board move it.",
46291
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.",
46292
46389
  "--check is the live gate read (it calls GitHub); --dry-run only echoes the parsed argv plan.",
46293
46390
  // #5552: agents guess `oracle issue claim`; that route does not exist — board claim is the only write.
46294
46391
  "Never run `oracle issue claim` \u2014 claims are board mutations; only `oracle board claim <ref>` is valid.",
@@ -60798,9 +60895,13 @@ async function checkDocsIndexAtHead(opts, deps) {
60798
60895
  }
60799
60896
 
60800
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
+ }
60801
60902
  async function preparePrCreate(input, deps = createPreparationDeps(input)) {
60802
60903
  const observations = [];
60803
- 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`] }));
60804
60905
  observations.push(...refs.findings.map((detail) => ({ check: "refs", state: "unavailable", detail })));
60805
60906
  if (refs.candidate) observations.push({ check: "refs", state: "passed", detail: `candidate ${refs.candidate.headSha} against ${refs.candidate.baseSha}` });
60806
60907
  const reads = await Promise.allSettled([
@@ -60811,10 +60912,10 @@ async function preparePrCreate(input, deps = createPreparationDeps(input)) {
60811
60912
  const [claims, docs, policy] = reads;
60812
60913
  if (claims.status === "fulfilled") {
60813
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" }]);
60814
- } else observations.push({ check: "claims", state: "unavailable", detail: "issue/claim evidence unavailable; retry the read" });
60815
- 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` });
60816
60917
  else if (docs.value) observations.push({ check: "docs", state: docs.value.state, detail: [docs.value.detail, docs.value.fix].filter(Boolean).join(" \u2014 ") });
60817
- 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` });
60818
60919
  else if (policy.value) observations.push(policy.value);
60819
60920
  const findings = observations.filter((item) => item.state === "failed" || item.state === "unavailable");
60820
60921
  let attempted = false;
@@ -60946,7 +61047,12 @@ function createPreparationDeps(input, io = {}) {
60946
61047
  return result("failed", `${refusal.error} \u2014 ${refusal.preparation.nextAction}`);
60947
61048
  }
60948
61049
  }
60949
- 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
+ }
60950
61056
  return result(policy.ok ? "passed" : "failed", policy.ok ? `committed test policy passed at ${candidate.headSha}` : policy.findings.map((finding) => finding.detail).join("\n"));
60951
61057
  }
60952
61058
  };
@@ -64976,9 +65082,13 @@ function projectInfoOutcome(projectInfoSync) {
64976
65082
  }
64977
65083
  function buildReleaseVerdict(commandName, result, projectInfoSync) {
64978
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
+ ]);
64979
65089
  const followUpStatus = deriveTrainFollowUpStatus({
64980
65090
  projectInfo: projectInfoOutcome(projectInfoSync),
64981
- deploy: deployFollowUpOutcome(result.deployStatus),
65091
+ deploy: deployOutcome,
64982
65092
  rcRetirement: result.rcRetirement === "failed" ? result.rcRetirementCategory === "wait-timeout" ? "unresolved" : "failure" : "ok",
64983
65093
  alignment: alignmentPending ? "unresolved" : "ok",
64984
65094
  foldPort: "ok"
@@ -64998,9 +65108,14 @@ function buildReleaseVerdict(commandName, result, projectInfoSync) {
64998
65108
  }
64999
65109
  function buildResumeReleaseVerdict(result, projectInfoSync) {
65000
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
+ ]);
65001
65116
  const followUpStatus = deriveTrainFollowUpStatus({
65002
65117
  projectInfo: projectInfoOutcome(projectInfoSync),
65003
- deploy: "ok",
65118
+ deploy: deployOutcome,
65004
65119
  rcRetirement: "ok",
65005
65120
  alignment: resumeStatus === "failed" ? "failure" : resumeStatus === "pending" ? "unresolved" : "ok",
65006
65121
  foldPort: "ok"
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.6.30",
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/claude-plugin",
3
- "version": "4.6.30",
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",
@@ -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.