@mutmutco/claude-plugin 4.6.37 → 4.6.39

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.37",
4
+ "version": "4.6.39",
5
5
  "mmiCompat": "4.x",
6
6
  "description": "MMI workflow skills and org gates delivery.",
7
7
  "author": {
package/cli/dist/main.cjs CHANGED
@@ -15691,6 +15691,9 @@ var APP_SUBPROCESS_ROUTED_COMMANDS = /* @__PURE__ */ new Set([
15691
15691
  // #9871: queue enqueue + auto-merge fallback ride ghFilingAppEnv (#9490/#9528; receipt credential app_installation).
15692
15692
  "pr land"
15693
15693
  ]);
15694
+ function ineligibleActorNote(commandPath4) {
15695
+ return commandPath4 === "board move" ? "app actor: board move retains the existing process identity; status-write credential is selected separately and reported in its receipt" : `app actor: note, not a failure \u2014 '${commandPath4}' is personal-only (viewer/@me/authorship), so it ran on the personal token despite ${APP_ACTOR_ENV}=app`;
15696
+ }
15694
15697
  async function activateAppActor(commandPath4, env, mint, note = (line) => process.stderr.write(`${line}
15695
15698
  `)) {
15696
15699
  activeQuotaScope = void 0;
@@ -15703,7 +15706,7 @@ async function activateAppActor(commandPath4, env, mint, note = (line) => proces
15703
15706
  if (!APP_ELIGIBLE_COMMANDS.has(commandPath4) && !APP_WRITE_COMMANDS.has(commandPath4)) {
15704
15707
  if (requested === "app" && !notedIneligibleCommand && !APP_SUBPROCESS_ROUTED_COMMANDS.has(commandPath4)) {
15705
15708
  notedIneligibleCommand = true;
15706
- note(`app actor: note, not a failure \u2014 '${commandPath4}' is personal-only (viewer/@me/authorship), so it ran on the personal token despite ${APP_ACTOR_ENV}=app`);
15709
+ note(ineligibleActorNote(commandPath4));
15707
15710
  }
15708
15711
  return "personal";
15709
15712
  }
@@ -22767,23 +22770,8 @@ var BOARD_WRITE_REFUSAL_TYPES = /* @__PURE__ */ new Set(["INSUFFICIENT_SCOPES",
22767
22770
  async function updateItemSingleSelect(client, projectId, itemId, fieldId, optionId) {
22768
22771
  if (process.env.MMI_ACTOR?.trim() === "app") {
22769
22772
  const { writeHubBoardField: writeHubBoardField2 } = await Promise.resolve().then(() => (init_board_write(), board_write_exports));
22770
- try {
22771
- await writeHubBoardField2({ projectId, itemId, fieldId, optionId });
22772
- return { credential: "app_installation" };
22773
- } catch (error) {
22774
- const refusal = error;
22775
- if (refusal?.rateLimited !== true || refusal.status !== 503 && refusal.status !== 429) throw error;
22776
- try {
22777
- const identity = await githubKeyringTokenRead();
22778
- if (identity.state !== "ok") throw error;
22779
- const owner = createGitHubClient({ token: async () => identity.token });
22780
- await owner.graphql(UPDATE_ITEM_FIELD_MUTATION, { projectId, itemId, fieldId, optionId });
22781
- } catch {
22782
- throw error;
22783
- }
22784
- process.stderr.write("board write: Hub App allowance exhausted \u2014 wrote with your own GitHub login (#9053)\n");
22785
- return { credential: "user" };
22786
- }
22773
+ await writeHubBoardField2({ projectId, itemId, fieldId, optionId });
22774
+ return { credential: "app_installation" };
22787
22775
  }
22788
22776
  try {
22789
22777
  await client.graphql(UPDATE_ITEM_FIELD_MUTATION, { projectId, itemId, fieldId, optionId });
@@ -22928,19 +22916,20 @@ function renderBoardItem(item) {
22928
22916
  }
22929
22917
  return lines2.join("\n");
22930
22918
  }
22919
+ var CACHED_CONFIRM_CLAUSE = " \xB7 items closed/created after the build may still differ \u2014 confirm with --direct";
22931
22920
  function renderCachedBoardSource(report) {
22932
22921
  if (typeof report.stale?.ageMs === "number") {
22933
22922
  const ageSeconds = Math.floor(report.stale.ageMs / 1e3);
22934
- return `source: cache \xB7 built ${report.builtAt ? `${report.builtAt} (${ageSeconds}s old)` : `${ageSeconds}s ago`} \xB7 stale`;
22923
+ return `source: cache \xB7 built ${report.builtAt ? `${report.builtAt} (${ageSeconds}s old)` : `${ageSeconds}s ago`} \xB7 stale${CACHED_CONFIRM_CLAUSE}`;
22935
22924
  }
22936
22925
  if (report.builtAt) {
22937
22926
  const builtMs = Date.parse(report.builtAt);
22938
22927
  if (Number.isFinite(builtMs)) {
22939
- return `source: cache \xB7 built ${report.builtAt} (${Math.max(0, Math.floor((Date.now() - builtMs) / 1e3))}s old) \xB7 fresh`;
22928
+ return `source: cache \xB7 built ${report.builtAt} (${Math.max(0, Math.floor((Date.now() - builtMs) / 1e3))}s old) \xB7 fresh${CACHED_CONFIRM_CLAUSE}`;
22940
22929
  }
22941
- return `source: cache \xB7 built ${report.builtAt} \xB7 fresh`;
22930
+ return `source: cache \xB7 built ${report.builtAt} \xB7 fresh${CACHED_CONFIRM_CLAUSE}`;
22942
22931
  }
22943
- return "source: cache";
22932
+ return `source: cache${CACHED_CONFIRM_CLAUSE}`;
22944
22933
  }
22945
22934
  function renderBoardReport(report) {
22946
22935
  const lines2 = [`Board \xB7 ${report.project.title} \xB7 @${report.viewer}`, report.source === "cache" ? renderCachedBoardSource(report) : renderBoardSource()];
@@ -25235,6 +25224,20 @@ function upsertManagedSeedBlock(current, desired, begin, end) {
25235
25224
  action: "replace"
25236
25225
  };
25237
25226
  }
25227
+ async function headStrictlyBehind(execGit, head, remoteSha, defaultBranch) {
25228
+ try {
25229
+ await execGit(["fetch", "--no-tags", "--quiet", "origin", `refs/heads/${defaultBranch}`]);
25230
+ } catch (e) {
25231
+ return { superseded: false, unknown: `could not fetch origin/${defaultBranch} to check ancestry (${e.message})` };
25232
+ }
25233
+ try {
25234
+ await execGit(["merge-base", "--is-ancestor", head, remoteSha]);
25235
+ return { superseded: true };
25236
+ } catch (e) {
25237
+ if (e.code === 1) return { superseded: false };
25238
+ return { superseded: false, unknown: `could not prove whether this checkout is an ancestor of origin/${defaultBranch} (${e.message}) \u2014 a shallow checkout cannot prove ancestry, so fetch full history` };
25239
+ }
25240
+ }
25238
25241
  async function resolveHubSeedSource(execGit, defaultBranch = "development") {
25239
25242
  let status;
25240
25243
  try {
@@ -25262,9 +25265,19 @@ remedy (non-destructive): run this command from a clean dedicated worktree \u201
25262
25265
  return { ok: false, reason: `\`git ls-remote origin refs/heads/${defaultBranch}\` returned nothing \u2014 refusing to read seeds without a known upstream revision` };
25263
25266
  }
25264
25267
  if (head !== remoteSha) {
25268
+ const behind = await headStrictlyBehind(execGit, head, remoteSha, defaultBranch);
25269
+ if (behind.superseded) {
25270
+ return {
25271
+ ok: false,
25272
+ superseded: true,
25273
+ head,
25274
+ remoteSha,
25275
+ reason: `this checkout (${head}) is behind origin/${defaultBranch} (${remoteSha}) \u2014 a newer commit overtook this run, whose own queued run owns the tick; seeding nothing and exiting green`
25276
+ };
25277
+ }
25265
25278
  return {
25266
25279
  ok: false,
25267
- reason: `this checkout (${head}) is not origin/${defaultBranch} (${remoteSha}) \u2014 fast-forward to origin/${defaultBranch} first; a stale or ahead checkout must not seed the fleet`
25280
+ reason: `this checkout (${head}) is not origin/${defaultBranch} (${remoteSha}) \u2014 fast-forward to origin/${defaultBranch} first; a stale or ahead checkout must not seed the fleet${behind.unknown ? ` (${behind.unknown})` : ""}`
25268
25281
  };
25269
25282
  }
25270
25283
  return { ok: true, sha: head };
@@ -28392,10 +28405,10 @@ var rollout_plan_default = {
28392
28405
  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)."
28393
28406
  },
28394
28407
  baseline: {
28395
- version: "4.6.37",
28396
- tag: "v4.6.37",
28397
- commit: "94211a24e48a",
28398
- npm: "@mutmutco/cli@4.6.37"
28408
+ version: "4.6.39",
28409
+ tag: "v4.6.39",
28410
+ commit: "2669c496b69e",
28411
+ npm: "@mutmutco/cli@4.6.39"
28399
28412
  },
28400
28413
  exitCriterion: "fleet-n-of-n",
28401
28414
  hubOnlyShortcut: "forbidden",
@@ -28412,14 +28425,14 @@ var rollout_plan_default = {
28412
28425
  repo: "mutmutco/mmi-hub",
28413
28426
  role: "canary",
28414
28427
  schedule: "train",
28415
- v3Target: "v4.6.37"
28428
+ v3Target: "v4.6.39"
28416
28429
  }
28417
28430
  ],
28418
28431
  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.",
28419
28432
  rollback: {
28420
28433
  independent: true,
28421
- mechanism: "npm dist-tag latest -> 4.6.37 and redeploy the Hub Lambda from tag v4.6.37 (94211a24e48a); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
28422
- v3Target: "v4.6.37 (@mutmutco/cli@4.6.37, tag commit 94211a24e48a \u2014 last known-good release carrying the repo-index v4-only contract)"
28434
+ mechanism: "npm dist-tag latest -> 4.6.39 and redeploy the Hub Lambda from tag v4.6.39 (2669c496b69e); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
28435
+ v3Target: "v4.6.39 (@mutmutco/cli@4.6.39, tag commit 2669c496b69e \u2014 last known-good release carrying the repo-index v4-only contract)"
28423
28436
  }
28424
28437
  },
28425
28438
  {
@@ -44002,7 +44015,7 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
44002
44015
  for (let readAttempt = 0; readAttempt < maxReads; readAttempt++) {
44003
44016
  if (readAttempt) await sleep3(readAttempt * 1e3);
44004
44017
  try {
44005
- const observed = await readPrMergeQueue(pr, repo, read);
44018
+ const observed = await readPrMergeQueue(pr, repo, read, options.sleep);
44006
44019
  if (observed.state === "MERGED" && observed.headRefOid === head) return markReenqueued({ mergeStatus: "merged" });
44007
44020
  if (observed.isInMergeQueue && observed.headRefOid === head) {
44008
44021
  const entry = observed.mergeQueueEntry;
@@ -44044,13 +44057,13 @@ async function submitQueueAdmission(pr, repo, head, read, write, options, resume
44044
44057
  if (receipt && (receipt.state === "enqueued" || receipt.state === "merged")) {
44045
44058
  if (receipt.head.toLowerCase() !== head.toLowerCase()) {
44046
44059
  try {
44047
- const observed = await readPrMergeQueue(pr, repo, read);
44060
+ const observed = await readPrMergeQueue(pr, repo, read, options.sleep);
44048
44061
  superseded = observed.state === "OPEN" && !observed.isInMergeQueue;
44049
44062
  } catch {
44050
44063
  }
44051
44064
  } else {
44052
44065
  try {
44053
- const observed = await readPrMergeQueue(pr, repo, read);
44066
+ const observed = await readPrMergeQueue(pr, repo, read, options.sleep);
44054
44067
  if (observed.state === "OPEN" && !observed.isInMergeQueue && observed.headRefOid?.toLowerCase() === receipt.head.toLowerCase()) {
44055
44068
  const boundMs = receipt.queueEntry ? Date.parse(receipt.queueEntry.enqueuedAt) : receipt.observedAt + UNWITNESSED_EJECTION_GRACE_MS;
44056
44069
  const events = await readArrayPages(
@@ -44253,6 +44266,15 @@ ${list}
44253
44266
  Co-authored-by: GitHub <noreply@github.com>
44254
44267
  `;
44255
44268
  }
44269
+ var WAIVER_KEYS = /^(?:Test-Policy-Override|Runbook-Unaffected)\s*:/i;
44270
+ function clipTrailer(trailer) {
44271
+ return trailer.length > 120 ? `${trailer.slice(0, 117)}...` : trailer;
44272
+ }
44273
+ function describeWaiverText(label, message2, cwd) {
44274
+ const block = parseTrailers(message2, cwd).filter((trailer) => WAIVER_KEYS.test(trailer));
44275
+ const found = block.length ? `final trailer block holds ${block.map((trailer) => `\`${clipTrailer(trailer)}\``).join(" and ")}` : "git parses no Test-Policy-Override or Runbook-Unaffected trailer there";
44276
+ return `${label}: ${found}`;
44277
+ }
44256
44278
  function checkQueueSquashOverride(title, pr, messages, root) {
44257
44279
  if (messages.length <= 1 || !messages.some((message2) => /^\s*Test-Policy-Override\s*:/im.test(`${message2.headline}
44258
44280
  ${message2.body}`)) || readMessageOverride2(githubQueueSquashMessage(title, pr, messages), root)) return void 0;
@@ -44386,7 +44408,8 @@ ${pull.body}`;
44386
44408
  }
44387
44409
  const requiredBody = options.bodyText ?? composedBody;
44388
44410
  const prBodyPreservesOverrides = requiredBody != null && method === "SQUASH" && policy.body === "PR_BODY" && options.bodyText == null && prBodyPreservesCommitOverrides(messages, headline, pull.body, process.cwd());
44389
- if (requiredBody && !prBodyPreservesOverrides && (method !== "SQUASH" || requiredBody.trim() !== body.trim())) {
44411
+ const queueKeepsWaiver = options.bodyText == null && method !== "SQUASH" && Boolean(readMessageOverride2(actualMessage, process.cwd()));
44412
+ if (requiredBody && !prBodyPreservesOverrides && !queueKeepsWaiver && (method !== "SQUASH" || requiredBody.trim() !== body.trim())) {
44390
44413
  if (method === "SQUASH" && policy.body === "PR_BODY" && options.bodyText == null) {
44391
44414
  return {
44392
44415
  kind: "refused",
@@ -44398,7 +44421,9 @@ ${pull.body}`;
44398
44421
  issueTargets: [],
44399
44422
  nextAction: `PR author: this repository squashes with PR_BODY, so place the required Test-Policy-Override trailer(s) in the PR body final trailer block (e.g. \`mmi-cli devops pr edit ${pr} --repo ${repo} --body-file .jerv/tmp/pr-${pr}-body.md\`) without a body override, then rerun \`pr land\`. Keep repository settings unchanged.`
44400
44423
  },
44401
- error: "queue preparation needed: repository squash policy is PR_BODY, and the PR body does not carry the required Test-Policy-Override trailer(s) in its final Git-recognized trailer block"
44424
+ error: `queue preparation needed: repository squash policy is PR_BODY, and the PR body does not carry the required Test-Policy-Override trailer(s) in its final Git-recognized trailer block \u2014 ${describeWaiverText("the PR body", pull.body, process.cwd())}; ${describeWaiverText("the source commit messages", messages.map((message2) => `${message2.headline}
44425
+
44426
+ ${message2.body}`).join("\n\n"), process.cwd())}`
44402
44427
  }
44403
44428
  };
44404
44429
  }
@@ -44407,9 +44432,12 @@ ${pull.body}`;
44407
44432
  return keys.length ? keys.join(", ") : "none";
44408
44433
  };
44409
44434
  const source = options.bodyText != null ? "the explicit body override" : "the body composed from the source commit messages";
44435
+ const sentLabel = method === "SQUASH" ? `squash ${policy.body ?? "unknown"}, ${messages.length} commit message${messages.length === 1 ? "" : "s"}` : `${method}${repository.merge_commit_message === "PR_BODY" ? " + PR body" : ""}`;
44410
44436
  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}
44411
44437
 
44412
- ${requiredBody}`)}), but the message the queue would send carries trailer keys: ${trailerKeys(actualMessage)}`;
44438
+ ${requiredBody}`)}), but the message the queue would send carries trailer keys: ${trailerKeys(actualMessage)}. ${describeWaiverText(`the message the queue sends (${sentLabel})`, actualMessage, process.cwd())}; ${describeWaiverText("the source commit messages", messages.map((message2) => `${message2.headline}
44439
+
44440
+ ${message2.body}`).join("\n\n"), process.cwd())}.`;
44413
44441
  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.`;
44414
44442
  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.";
44415
44443
  return { kind: "refused", result: preparation("queue-message-override", [], detail, next + caveat) };
@@ -44459,7 +44487,7 @@ async function abandonQueueAdmission(pr, repo, options = {}) {
44459
44487
  }
44460
44488
  let snapshot;
44461
44489
  try {
44462
- snapshot = await readPrMergeQueue(pr, repo, options.read ?? actorReadApi);
44490
+ snapshot = await readPrMergeQueue(pr, repo, options.read ?? actorReadApi, options.sleep);
44463
44491
  } catch (error) {
44464
44492
  return refuse(`the PR and queue state could not be read (${error.message}); the admission is unchanged`);
44465
44493
  }
@@ -44484,13 +44512,19 @@ function decideQueueMergeRoute(hasQueue, method, headRef) {
44484
44512
  if (!hasQueue) return "normal";
44485
44513
  return method === "--merge" && /^(release-align|hotfix-fold)\//.test(headRef) ? "admin-true-merge" : "pr-land";
44486
44514
  }
44487
- async function readPrMergeQueue(pr, repo, gh2 = actorReadApi) {
44515
+ async function readPrMergeQueue(pr, repo, gh2 = actorReadApi, sleep2 = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms))) {
44488
44516
  if (!/^\d+$/.test(pr)) throw new Error("merge queue read requires a PR number");
44489
- const result = await queueGraphql(
44490
- repo,
44491
- `pullRequest(number:${pr}) { state isInMergeQueue mergeQueueEntry { state position enqueuedAt headCommit { oid } } autoMergeRequest { enabledAt } headRefOid }`,
44492
- gh2
44493
- );
44517
+ const fields = `pullRequest(number:${pr}) { state isInMergeQueue mergeQueueEntry { state position enqueuedAt headCommit { oid } } autoMergeRequest { enabledAt } headRefOid }`;
44518
+ let result;
44519
+ for (let attempt = 0; ; attempt++) {
44520
+ try {
44521
+ result = await queueGraphql(repo, fields, gh2);
44522
+ break;
44523
+ } catch (error) {
44524
+ if (attempt >= 2 || !isUpstreamGitHubFault(execErrorText(error))) throw error;
44525
+ await sleep2(2e3 * (attempt + 1));
44526
+ }
44527
+ }
44494
44528
  if (!result.pullRequest) throw new Error(`merge queue PR #${pr} not found`);
44495
44529
  const entry = result.pullRequest.mergeQueueEntry;
44496
44530
  if (entry && (entry.enqueuedAt !== void 0 && !Number.isFinite(Date.parse(entry.enqueuedAt)) || entry.headCommit && !/^[a-f0-9]{40}$/i.test(entry.headCommit.oid))) throw new Error("queue entry identity is unreadable");
@@ -44919,7 +44953,7 @@ async function dispatchWorkflow(repo, workflow, ref, inputs, deps) {
44919
44953
  const fields = ["-f", `ref=${ref}`];
44920
44954
  const names = /* @__PURE__ */ new Set();
44921
44955
  for (const input of inputs) {
44922
- const match = /^([A-Za-z_][A-Za-z0-9_]{0,63})=([^\r\n]{0,2048})$/.exec(input);
44956
+ const match = /^([A-Za-z_][A-Za-z0-9_-]{0,63})=([^\r\n]{0,2048})$/.exec(input);
44923
44957
  if (!match || names.has(match[1])) throw new Error("run dispatch: invalid or duplicate input");
44924
44958
  names.add(match[1]);
44925
44959
  fields.push("-f", `inputs[${match[1]}]=${match[2]}`);
@@ -46446,7 +46480,7 @@ function refuseNotClaimable(e, json) {
46446
46480
  process.exitCode = 1;
46447
46481
  return true;
46448
46482
  }
46449
- var BOARD_READ_HELP = "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nWhen using Windows PowerShell 5.1, avoid shell redirects: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n\nThe --json report is an OBJECT \u2014 never an array:\n {project, viewer, repo, primary, secondary, items, warnings, partial[, source, builtAt, eventCursor, ...]}\n`primary` is this repo and `secondary` the other repos in scope; each is a bucket object\n {userOwned, claimable, taken, unownedInFlight}\nand each bucket is an array of items. Filter the BUCKET for ownership context:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n node -e \"const b=require('./.jerv/tmp/board.json');console.log(b.primary.claimable.length)\"\n\n`items` (#9974) is that same flat row array, already on the full report: {ref,number,title,status,assignees,labels,bucket,claim,openPullRequests}\n node -e \"const b=require('./.jerv/tmp/board.json');for(const i of b.items)console.log(i.number,i.status,i.title)\"\nIt is built from the same flattener as --flat, so `const rows = j.items || j` is now safe on either form.\n\nAdd --flat with --json for that array as the WHOLE output (no project/viewer/warnings envelope):\n(#9595: claim is the parsed mmi-claim evidence object, null when unclaimed; openPullRequests lists the OPEN\n PRs whose closing-issue body links name the item. On the FULL JSON object that list lives at\n `report.pullRequests` \u2014 there is no top-level `openPullRequests`, and #9596 hand-written projections\n that assume one crash on `.filter`.)\n\nNever sniff the envelope \u2014 read `report.items` (or `primary`/`secondary`) by name. `j.items ?? j.rows ?? j`,\n`Object.values(j)` and any generic \"find the list\" wrapper are still guesses: before `items` existed they\nthrew `items is not iterable` before any row was read (#9956), and `j.rows` has never been a key. For a\nnumber|status|claim|title projection across non-Done rows, read --json --flat --out .jerv/tmp/board.json\nand filter that array locally (identical rows to `report.items`):\n mmi-cli oracle board read --json --flat --out .jerv/tmp/board.json\n\nA cached snapshot read renders `source: cache \xB7 built <timestamp> \xB7 fresh|stale` (#9584) and carries\n`builtAt` under --json, so a same-turn re-read of the same builtAt buys nothing new.\n\nUnder --json, parse stdout ONLY and check the exit code first. A failure still prints a\nhuman-readable reason on stderr, so never pipe 2>&1 into a JSON parser (#9151) \u2014 the merged\ntext is not JSON and hides the real board failure.\n\nA zero-byte or empty .jerv/tmp/*.json means the read FAILED \u2014 `Unexpected end of JSON input` is\nthe symptom of shell-redirected empty stdout, not malformed JSON \u2014 re-run with --out (which writes\nthe file only on success) and check the exit code before parsing.\n\nChanging an existing item's Status is a move, never a hand-written `gh api graphql`\n`updateProjectV2ItemFieldValue` mutation with copied `PVT_`/`PVTI_` node ids and a manual Status\nfield-id lookup:\n mmi-cli oracle board move <status> <ref>\n";
46483
+ var BOARD_READ_HELP = "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nWhen using Windows PowerShell 5.1, avoid shell redirects: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n\nThe --json report is an OBJECT \u2014 never an array:\n {project, viewer, repo, primary, secondary, items, warnings, partial[, source, builtAt, eventCursor, ...]}\n`primary` is this repo and `secondary` the other repos in scope; each is a bucket object\n {userOwned, claimable, taken, unownedInFlight}\nand each bucket is an array of items. Filter the BUCKET for ownership context:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n node -e \"const b=require('./.jerv/tmp/board.json');console.log(b.primary.claimable.length)\"\n\n`items` (#9974) is that same flat row array, already on the full report: {ref,number,title,status,assignees,labels,bucket,claim,openPullRequests}\n node -e \"const b=require('./.jerv/tmp/board.json');for(const i of b.items)console.log(i.number,i.status,i.title)\"\nIt is built from the same flattener as --flat, so `const rows = j.items || j` is now safe on either form.\n\nAdd --flat with --json for that array as the WHOLE output (no project/viewer/warnings envelope):\n(#9595: claim is the parsed mmi-claim evidence object, null when unclaimed; openPullRequests lists the OPEN\n PRs whose closing-issue body links name the item. On the FULL JSON object that list lives at\n `report.pullRequests` \u2014 there is no top-level `openPullRequests`, and #9596 hand-written projections\n that assume one crash on `.filter`.)\n\nNever sniff the envelope \u2014 read `report.items` (or `primary`/`secondary`) by name. `j.items ?? j.rows ?? j`,\n`Object.values(j)` and any generic \"find the list\" wrapper are still guesses: before `items` existed they\nthrew `items is not iterable` before any row was read (#9956), and `j.rows` has never been a key. For a\nnumber|status|claim|title projection across non-Done rows, read --json --flat --out .jerv/tmp/board.json\nand filter that array locally (identical rows to `report.items`):\n mmi-cli oracle board read --json --flat --out .jerv/tmp/board.json\n\nA cached snapshot read renders `source: cache \xB7 built <timestamp> \xB7 fresh|stale` (#9584) and carries\n`builtAt` under --json, so a same-turn re-read of the same builtAt buys nothing new. That age grades the\nCOPY, not its content: the line ends `\xB7 items closed/created after the build may still differ \u2014 confirm\nwith --direct` (#10028), so re-read with --direct before treating a row as still open or still present.\n\nUnder --json, parse stdout ONLY and check the exit code first. A failure still prints a\nhuman-readable reason on stderr, so never pipe 2>&1 into a JSON parser (#9151) \u2014 the merged\ntext is not JSON and hides the real board failure.\n\nA zero-byte or empty .jerv/tmp/*.json means the read FAILED \u2014 `Unexpected end of JSON input` is\nthe symptom of shell-redirected empty stdout, not malformed JSON \u2014 re-run with --out (which writes\nthe file only on success) and check the exit code before parsing.\n\nChanging an existing item's Status is a move, never a hand-written `gh api graphql`\n`updateProjectV2ItemFieldValue` mutation with copied `PVT_`/`PVTI_` node ids and a manual Status\nfield-id lookup:\n mmi-cli oracle board move <status> <ref>\n";
46450
46484
  function flattenBoardReport(report) {
46451
46485
  const closingByRef = /* @__PURE__ */ new Map();
46452
46486
  for (const pull of report.pullRequests ?? []) {
@@ -51148,6 +51182,13 @@ var declaredProjectAdmins = (meta) => {
51148
51182
  return Array.isArray(list) ? list.filter((entry) => typeof entry === "string") : [];
51149
51183
  };
51150
51184
  var execGitForSeedSource = async (args) => (await execFileP("git", args, { timeout: GIT_TIMEOUT_MS })).stdout;
51185
+ function reportSuperseded(prefix, source, json) {
51186
+ const head = source.head.slice(0, 8);
51187
+ const remote = source.remoteSha.slice(0, 8);
51188
+ const summary = `${prefix}: superseded \u2014 this checkout (${head}) is behind origin/development (${remote}); a newer run owns this tick, nothing seeded`;
51189
+ if (json) console.log(JSON.stringify({ status: "superseded", checkoutSha: source.head, remoteSha: source.remoteSha, summary }, null, 2));
51190
+ else console.log(summary);
51191
+ }
51151
51192
  async function gateRenderDiff(current, rendered) {
51152
51193
  const dir = (0, import_node_fs40.mkdtempSync)((0, import_node_path37.join)((0, import_node_os21.tmpdir)(), "mmi-gate-diff-"));
51153
51194
  const before = (0, import_node_path37.join)(dir, "committed-gate.yml");
@@ -51235,7 +51276,10 @@ async function propagationScope(classes, what) {
51235
51276
  }
51236
51277
  async function propagatePins(apply, json) {
51237
51278
  const source = await resolveHubSeedSource(execGitForSeedSource);
51238
- if (apply && !source.ok) return fail(`bootstrap propagate --pins --apply: ${source.reason}`);
51279
+ if (apply && !source.ok) {
51280
+ if (source.superseded) return reportSuperseded("bootstrap propagate --pins --apply", source, json);
51281
+ return fail(`bootstrap propagate --pins --apply: ${source.reason}`);
51282
+ }
51239
51283
  const { classOf, inClassRepos, canarySlug, sourcePilot } = await propagationScope(null, "hub action pins");
51240
51284
  const gh2 = async (args) => execFileP("gh", args, { timeout: 2e4 });
51241
51285
  const repos = [];
@@ -52360,7 +52404,10 @@ LIVE apply to ${repo}:
52360
52404
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
52361
52405
  if (!(0, import_node_fs40.existsSync)(manifestPath)) return fail(`bootstrap propagate: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the desired state this tick propagates`);
52362
52406
  const seedSource = await resolveHubSeedSource(execGitForSeedSource);
52363
- if (!seedSource.ok) return fail(`bootstrap propagate: ${seedSource.reason}`);
52407
+ if (!seedSource.ok) {
52408
+ if (seedSource.superseded) return reportSuperseded("bootstrap propagate", seedSource, o.json);
52409
+ return fail(`bootstrap propagate: ${seedSource.reason}`);
52410
+ }
52364
52411
  const manifest = loadBootstrapSeeds((0, import_node_fs40.readFileSync)(manifestPath, "utf8"));
52365
52412
  const propagatable = manifest.seeds.filter(isPropagatableSeed);
52366
52413
  if (!o.target) {
@@ -53359,7 +53406,7 @@ async function stageStatus(member, stage, deps) {
53359
53406
  if (pin) {
53360
53407
  served = pin;
53361
53408
  source = "registry-pin";
53362
- } else if (stage === "dev" || !member.bundles[stage]) {
53409
+ } else {
53363
53410
  try {
53364
53411
  const status = await deps.deploy(member.repo, stage);
53365
53412
  deployStatus = status;
@@ -53505,10 +53552,12 @@ async function newestGreen(deps, repo) {
53505
53552
  async function readSuiteDev(deps) {
53506
53553
  const report = await deps.status();
53507
53554
  return Promise.all(report.members.map(async (m) => {
53508
- const st = m.stages.find((s) => s.stage === "dev");
53555
+ const track = deps.releaseTrack ? await deps.releaseTrack(m.repo).catch(() => void 0) : void 0;
53556
+ const stage = track === "single" ? "main" : "dev";
53557
+ const st = m.stages.find((s) => s.stage === stage);
53509
53558
  const row = { repo: m.repo, head: st.head, served: st.served };
53510
- if (st.source === "n/a") return { ...row, state: "converged", note: "no dev stage" };
53511
- if (st.lag === false) return { ...row, state: "converged", note: "dev serves the development head" };
53559
+ if (st.source === "n/a") return { ...row, state: "converged", note: `no ${stage} stage` };
53560
+ if (st.lag === false) return { ...row, state: "converged", note: `${stage} serves the development head` };
53512
53561
  let green;
53513
53562
  try {
53514
53563
  green = await newestGreen(deps, m.repo);
@@ -53516,10 +53565,10 @@ async function readSuiteDev(deps) {
53516
53565
  return { ...row, state: "unknown", note: `checks unreadable (${e.message})` };
53517
53566
  }
53518
53567
  const last = st.deployStatus?.lastRun;
53519
- if (st.served === "unknown") return { ...row, green, state: "unknown", note: `dev version unproven (health probe failed or no deploy record)${last?.url ? `; last deploy ${last.conclusion ?? last.status}: ${last.url}` : ""}` };
53520
- if (same(st.served, green)) return { ...row, green, state: "converged", note: `dev serves the newest green SHA; head ${short2(st.head)} is not green yet` };
53568
+ if (st.served === "unknown") return { ...row, green, state: "unknown", note: `${stage} version unproven (health probe failed or no deploy record)${last?.url ? `; last deploy ${last.conclusion ?? last.status}: ${last.url}` : ""}` };
53569
+ if (same(st.served, green)) return { ...row, green, state: "converged", note: `${stage} serves the newest green SHA; head ${short2(st.head)} is not green yet` };
53521
53570
  if (!green) return { ...row, state: "blocked", note: `no green SHA in the last ${WALK} development commits` };
53522
- if (last?.conclusion === "failure") return { ...row, green, state: "blocked", note: `last dev deploy failed: ${last.url ?? "no run url"}` };
53571
+ if (last?.conclusion === "failure") return { ...row, green, state: "blocked", note: `last ${stage} deploy failed: ${last.url ?? "no run url"}` };
53523
53572
  return { ...row, green, state: "lagging", note: `newest green ${short2(green)} not deployed yet; the dev auto-deploy sweep (every 15 min) picks it up` };
53524
53573
  }));
53525
53574
  }
@@ -58738,6 +58787,11 @@ ${lines2}`);
58738
58787
  if (deps.attach) {
58739
58788
  const attached = await deps.attach(row.number, rowRepo(spec), rowPriority);
58740
58789
  row.onBoard = attached.onBoard;
58790
+ if (attached.prioritySet === false) {
58791
+ row.status = "partial";
58792
+ row.prioritySet = false;
58793
+ row.priorityError = attached.priorityError;
58794
+ }
58741
58795
  if (attached.projectItemId) row.projectItemId = attached.projectItemId;
58742
58796
  if (attached.boardAttach) {
58743
58797
  row.boardAttach = attached.boardAttach;
@@ -63536,8 +63590,10 @@ async function attachToProject(issueNumber, repo, priority) {
63536
63590
  await setBoardItemPriority(defaultGitHubClient(), cfg, projectItemId2, priority);
63537
63591
  } catch (e) {
63538
63592
  const err = e;
63539
- process.stderr.write(`warning: issue #${issueNumber} board Priority not set: ${(err.stderr || err.message || String(e)).trim()}
63593
+ const priorityError = (err.stderr || err.message || String(e)).trim();
63594
+ process.stderr.write(`warning: issue #${issueNumber} board Priority not set: ${priorityError}; issue and board attachment already exist; do not recreate the issue
63540
63595
  `);
63596
+ return { projectItemId: projectItemId2, onBoard: true, credential: "app_installation", prioritySet: false, priorityError };
63541
63597
  }
63542
63598
  }
63543
63599
  return { projectItemId: projectItemId2, onBoard: true, credential: "app_installation" };
@@ -63887,12 +63943,13 @@ function registerCollaborationCommands(program3) {
63887
63943
  `);
63888
63944
  const linkRecovery = parentRecoveryRef !== void 0 && !parent ? `mmi-cli oracle issue link-child ${parentRecoveryRef} ${childRef}` : void 0;
63889
63945
  console.log(JSON.stringify({
63890
- status: boardRecovery || linkRecovery ? "partial" : "created",
63946
+ status: boardRecovery || linkRecovery || attached.prioritySet === false ? "partial" : "created",
63891
63947
  ...created,
63892
63948
  label: issueType,
63893
63949
  priority,
63894
63950
  projectItemId,
63895
63951
  onBoard,
63952
+ ...attached.prioritySet === false ? { prioritySet: false, priorityError: attached.priorityError } : {},
63896
63953
  ...attachCredential ? { credential: attachCredential } : {},
63897
63954
  // #5489: partial receipt when the GitHub issue landed but Project v2 attach hit GraphQL quota.
63898
63955
  ...!onBoard ? {
@@ -64279,8 +64336,9 @@ ${list}`);
64279
64336
  }
64280
64337
  }
64281
64338
  const created = requireGhCreateOk(await ghCreate(args), "skill-lesson");
64282
- const { projectItemId, onBoard } = await attachToProject(created.number, targetRepo2, priority);
64283
- console.log(JSON.stringify({ ...created, projectItemId, onBoard }));
64339
+ const attached = await attachToProject(created.number, targetRepo2, priority);
64340
+ const { projectItemId, onBoard } = attached;
64341
+ console.log(JSON.stringify({ ...created, projectItemId, onBoard, ...attached.prioritySet === false ? { status: "partial", prioritySet: false, priorityError: attached.priorityError } : {} }));
64284
64342
  });
64285
64343
  const pr = program3.command("pr").description("pull requests \u2014 reliable create with structured output");
64286
64344
  withExamples(pr.command("create").description("create a PR and print {number,url} JSON").option("--title <title>", "PR title").option("--title-file <path|->", "read the PR title from a UTF-8 file, or from stdin with -").option("--body <body>", "PR body (markdown)").option("--body-file <path|->", "read PR body from a UTF-8 file, or from stdin with -").option("--base <branch>", "base branch (defaults to the repo default)").option("--head <branch>", "head branch (defaults to the current branch)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--draft", "open the PR in draft state (#2667)").option("--no-zeroci", "compatibility option; pr create does not mint receipts").option("--json", "machine-readable output (default; accepted for parity)").action(async (o) => {
@@ -67123,6 +67181,8 @@ function registerSuiteCommands(program3) {
67123
67181
  const status = suiteStatusDeps(github, registry2);
67124
67182
  const deps = {
67125
67183
  status: () => readSuiteStatus(status),
67184
+ // #10020: a single-track member's one live stage is `main`; judge that row instead of the dev one.
67185
+ releaseTrack: status.releaseTrack,
67126
67186
  commits: async (repo, limit) => (await github.rest("GET", `repos/${repo}/commits?sha=development&per_page=${limit}`) ?? []).map((c) => c.sha),
67127
67187
  checkRuns: async (repo, sha) => (await github.rest("GET", `repos/${repo}/commits/${sha}/check-runs?per_page=100`))?.check_runs ?? [],
67128
67188
  requiredChecks: async (repo) => {
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.6.37",
3
+ "version": "4.6.39",
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.37",
3
+ "version": "4.6.39",
4
4
  "description": "MMI workflow skills and org gates delivery.",
5
5
  "author": {
6
6
  "name": "MMI Future",
@@ -83,7 +83,9 @@ jobs:
83
83
  # that runs the full job; main must never become skippable via bootstrap vars alone while the
84
84
  # org ruleset still requires `gate` on main.
85
85
  if: ${{ github.event_name != 'push' || github.ref_name == '{{GATE_FULL_RUN_BRANCH}}' || github.ref_name == 'main' || github.ref_type == 'tag' }}
86
- runs-on: ubuntu-latest # #9248: included GitHub-hosted capacity, first choice for CI.
86
+ # #9248: included GitHub-hosted capacity, first choice for CI.
87
+ # #9668: GATE_HOSTED_SPILL_QUEUE=off routes it to the owned mmi-live lanes at Actions run-out.
88
+ runs-on: ${{ vars.GATE_HOSTED_SPILL_QUEUE == 'off' && fromJSON('["self-hosted","linux","x64","mmi-live"]') || 'ubuntu-latest' }}
87
89
  defaults:
88
90
  run: { working-directory: {{GATE_WORKDIR}} }{{GATE_SERVICES_YAML}}
89
91
  steps:{{GATE_VERDICT_STEP_YAML}}
@@ -21,7 +21,8 @@ jobs:
21
21
  # the earlier "included Ubuntu for light pre-gates" permission; it creates no new runner class — standard
22
22
  # included hosted labels are allowed (`ubuntu-*`, `windows-*`, `macos-*`, ARM variants; #8977); only
23
23
  # larger/paid images, GPU and custom runner groups stay refused.
24
- runs-on: ubuntu-latest
24
+ # #9668: GATE_HOSTED_SPILL_QUEUE=off routes it to the owned mmi-live lanes at Actions run-out.
25
+ runs-on: ${{ vars.GATE_HOSTED_SPILL_QUEUE == 'off' && fromJSON('["self-hosted","linux","x64","mmi-live"]') || 'ubuntu-latest' }}
25
26
  timeout-minutes: 10
26
27
  steps:{{GATE_VERDICT_STEP_YAML}}
27
28
  - if: ${{ steps.development-gate.outputs.reuse != 'true' }}