@mutmutco/cli 4.4.5 → 4.4.7

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.
Files changed (2) hide show
  1. package/dist/main.cjs +303 -71
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -1112,6 +1112,7 @@ var init_cli_shared = __esm({
1112
1112
  // src/board-write.ts
1113
1113
  var board_write_exports = {};
1114
1114
  __export(board_write_exports, {
1115
+ addIssueToProjectViaHub: () => addIssueToProjectViaHub,
1115
1116
  writeHubBoardField: () => writeHubBoardField
1116
1117
  });
1117
1118
  async function writeHubBoardField(request) {
@@ -1125,6 +1126,18 @@ async function writeHubBoardField(request) {
1125
1126
  const body = await res.json().catch(() => null);
1126
1127
  if (!res.ok || body?.updated !== true) throw new Error(`board field write HTTP ${res.status}: ${body?.error ?? "unconfirmed result"}`);
1127
1128
  }
1129
+ async function addIssueToProjectViaHub(cfg, contentNodeId) {
1130
+ const config = await loadConfig();
1131
+ const res = await fetch(`${config.sagaApiUrl?.replace(/\/$/, "")}/board/attach`, {
1132
+ method: "POST",
1133
+ headers: await hubHeaders({ "content-type": "application/json" }),
1134
+ body: JSON.stringify({ projectId: cfg.projectId, contentNodeId }),
1135
+ signal: AbortSignal.timeout(25e3)
1136
+ });
1137
+ const body = await res.json().catch(() => null);
1138
+ if (!res.ok || !body?.itemId) throw new Error(`board attach HTTP ${res.status}: ${body?.error ?? "unconfirmed result"}`);
1139
+ return body.itemId;
1140
+ }
1128
1141
  var init_board_write = __esm({
1129
1142
  "src/board-write.ts"() {
1130
1143
  "use strict";
@@ -7288,37 +7301,71 @@ function pathWords(text) {
7288
7301
  function proseWords(text) {
7289
7302
  return text.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
7290
7303
  }
7304
+ function countWords(words) {
7305
+ const counts = /* @__PURE__ */ new Map();
7306
+ for (const w of words) counts.set(w, (counts.get(w) ?? 0) + 1);
7307
+ return counts;
7308
+ }
7309
+ var TITLE_WEIGHT = 3;
7310
+ var PATH_WEIGHT = 4;
7311
+ var BODY_WEIGHT = 1;
7291
7312
  function inferSurface(candidates, context = {}) {
7292
7313
  const sorted = [...candidates].sort();
7293
7314
  if (sorted.length === 0) throw new Error("inferSurface: no surface:* candidates to choose from");
7294
- const pWords = new Set(pathWords(context.body ?? ""));
7295
- const tWords = /* @__PURE__ */ new Set([...proseWords(context.title ?? ""), ...proseWords(context.body ?? "")]);
7315
+ const titleCounts = countWords(proseWords([context.title ?? "", context.type ?? ""].join(" ")));
7316
+ const pathCounts = countWords(pathWords(context.body ?? ""));
7317
+ const bodyCounts = countWords(proseWords(context.body ?? ""));
7318
+ const scoreOf = (label) => {
7319
+ let score = 0;
7320
+ let fromPath = false;
7321
+ let fromTitle = false;
7322
+ for (const word of surfaceWords(label)) {
7323
+ const title = titleCounts.get(word) ?? 0;
7324
+ const path2 = pathCounts.get(word) ?? 0;
7325
+ const body = bodyCounts.get(word) ?? 0;
7326
+ if (title > 0) fromTitle = true;
7327
+ if (path2 > 0) fromPath = true;
7328
+ score += title * TITLE_WEIGHT + path2 * PATH_WEIGHT + body * BODY_WEIGHT;
7329
+ }
7330
+ return { score, fromPath, fromTitle };
7331
+ };
7296
7332
  let best = sorted[0];
7297
7333
  let bestScore = 0;
7298
7334
  let bestFromPath = false;
7335
+ let bestFromTitle = false;
7336
+ const scores = /* @__PURE__ */ new Map();
7299
7337
  for (const label of sorted) {
7300
- const words = surfaceWords(label);
7301
- if (!words.length) continue;
7302
- const pathHits = words.filter((w) => pWords.has(w)).length;
7303
- const proseHits = words.filter((w) => tWords.has(w)).length;
7304
- const score = pathHits * 2 + proseHits;
7338
+ const { score, fromPath, fromTitle } = scoreOf(label);
7339
+ scores.set(label, score);
7305
7340
  if (score > bestScore) {
7306
7341
  bestScore = score;
7307
7342
  best = label;
7308
- bestFromPath = pathHits > 0;
7343
+ bestFromPath = fromPath;
7344
+ bestFromTitle = fromTitle;
7309
7345
  }
7310
7346
  }
7311
- if (bestScore === 0) {
7312
- return {
7313
- label: sorted[0],
7314
- reason: `no path or title/body word matched any of ${sorted.length} surface label(s) \u2014 defaulted to the alphabetically-first`
7315
- };
7316
- }
7347
+ const tied = sorted.filter((label) => (scores.get(label) ?? 0) === bestScore);
7348
+ const uncertain = bestScore === 0 || bestScore < TITLE_WEIGHT || tied.length > 1;
7349
+ const alternatives = uncertain ? sorted.filter((label) => label !== best && (scores.get(label) ?? 0) > 0).sort((a, b) => (scores.get(b) ?? 0) - (scores.get(a) ?? 0) || (a < b ? -1 : a > b ? 1 : 0)).slice(0, 3) : [];
7350
+ const reason = bestScore === 0 ? `no path or title/body word matched any of ${sorted.length} surface label(s) \u2014 defaulted to the alphabetically-first` : bestFromPath ? "matched a path mentioned in the issue body" : bestFromTitle ? "matched a word in the issue title/body" : "matched a word in the issue body";
7317
7351
  return {
7318
7352
  label: best,
7319
- reason: bestFromPath ? "matched a path mentioned in the issue body" : "matched a word in the issue title/body"
7353
+ reason,
7354
+ ...uncertain ? { uncertain: true } : {},
7355
+ ...alternatives.length ? { alternatives } : {}
7320
7356
  };
7321
7357
  }
7358
+ function surfaceInferenceSuffix(inferred) {
7359
+ if (!inferred.uncertain) return "";
7360
+ const alts = inferred.alternatives?.length ? ` Other candidates: ${inferred.alternatives.join(", ")}.` : "";
7361
+ return ` This pick is low-confidence \u2014 pass --surface <value> to choose explicitly.${alts}`;
7362
+ }
7363
+ function surfaceInferenceNote(inferred) {
7364
+ const base = `mmi-cli: no --surface given \u2014 filed under ${inferred.label} (${inferred.reason}).`;
7365
+ return inferred.uncertain ? `${base}${surfaceInferenceSuffix(inferred)}
7366
+ ` : `${base} Pass --surface to choose.
7367
+ `;
7368
+ }
7322
7369
  async function checkSurfaceRequirement(input, deps = {}) {
7323
7370
  if (input.waiver) {
7324
7371
  const reason = input.waiver.reason.trim();
@@ -7350,7 +7397,7 @@ async function checkSurfaceRequirement(input, deps = {}) {
7350
7397
  }
7351
7398
  if (known.length === 0) return { enforcing: false, taxonomyAbsent: true };
7352
7399
  if (labelsCarrySurface(input.labels)) return { enforcing: true };
7353
- return { enforcing: true, inferred: inferSurface(known, { title: input.title, body: input.body }) };
7400
+ return { enforcing: true, inferred: inferSurface(known, { title: input.title, type: input.type, body: input.body }) };
7354
7401
  }
7355
7402
  function conflictingSurfaceInputs(surfaceFlag, labels) {
7356
7403
  if (!surfaceFlag) return void 0;
@@ -11114,11 +11161,24 @@ mutation($projectId: ID!, $itemId: ID!) {
11114
11161
  async function updateItemSingleSelect(client, projectId, itemId, fieldId, optionId) {
11115
11162
  try {
11116
11163
  await client.graphql(UPDATE_ITEM_FIELD_MUTATION, { projectId, itemId, fieldId, optionId });
11164
+ return { credential: "user" };
11117
11165
  } catch (error) {
11118
11166
  const errors = error.graphqlErrors;
11119
- if (!errors?.length || !errors.every((entry) => entry.type === "INSUFFICIENT_SCOPES")) throw error;
11120
- const { writeHubBoardField: writeHubBoardField2 } = await Promise.resolve().then(() => (init_board_write(), board_write_exports));
11121
- await writeHubBoardField2({ projectId, itemId, fieldId, optionId });
11167
+ if (errors?.length && errors.every((entry) => entry.type === "INSUFFICIENT_SCOPES")) {
11168
+ const { writeHubBoardField: writeHubBoardField2 } = await Promise.resolve().then(() => (init_board_write(), board_write_exports));
11169
+ await writeHubBoardField2({ projectId, itemId, fieldId, optionId });
11170
+ return { credential: "app_installation" };
11171
+ }
11172
+ if (isGitHubRateLimitError(error)) {
11173
+ const { writeHubBoardField: writeHubBoardField2 } = await Promise.resolve().then(() => (init_board_write(), board_write_exports));
11174
+ try {
11175
+ await writeHubBoardField2({ projectId, itemId, fieldId, optionId });
11176
+ return { credential: "app_installation" };
11177
+ } catch {
11178
+ throw error;
11179
+ }
11180
+ }
11181
+ throw error;
11122
11182
  }
11123
11183
  }
11124
11184
  function parseIssueSelector(selector, defaultRepo, expectedRepo) {
@@ -12361,8 +12421,9 @@ async function moveBoardItem(options, deps = {}) {
12361
12421
  throw boardNotFoundError(`${selector.repo}#${selector.number}`, { owner: cfg.projectOwner, number: cfg.projectNumber });
12362
12422
  }
12363
12423
  const optionId = cfg.statusOptions[options.status];
12424
+ let credential = "user";
12364
12425
  try {
12365
- await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, optionId);
12426
+ ({ credential } = await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, optionId));
12366
12427
  } catch (e) {
12367
12428
  if (options.status === "Done" && isArchivedItemRefusal(ghError(e))) {
12368
12429
  return {
@@ -12384,7 +12445,9 @@ async function moveBoardItem(options, deps = {}) {
12384
12445
  viewer: lookup.viewer,
12385
12446
  repo: currentRepo,
12386
12447
  status: options.status,
12387
- partial: false
12448
+ partial: false,
12449
+ credential
12450
+ // #6834: which credential served the write — the user token or the Hub App leg.
12388
12451
  };
12389
12452
  }
12390
12453
  async function resolveClaimWritable(collected, client, snapshot, unscanned) {
@@ -12550,8 +12613,9 @@ async function claimOneBoardItem(ctx, selector, options) {
12550
12613
  throw new Error(`claim failed before board status changed: ${ghError(e)}`);
12551
12614
  }
12552
12615
  await postClaimMarkerComment(client, item, ctx.session);
12616
+ let credential = "user";
12553
12617
  try {
12554
- await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, cfg.statusOptions["In Progress"]);
12618
+ ({ credential } = await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, cfg.statusOptions["In Progress"]));
12555
12619
  } catch (e) {
12556
12620
  const warning = `partial claim: ${item.ref} was assigned to @${assignedLogin}, but Status was not moved to In Progress (${ghError(e)})`;
12557
12621
  if (!options.allowPartial) throw new Error(warning);
@@ -12568,6 +12632,7 @@ async function claimOneBoardItem(ctx, selector, options) {
12568
12632
  repo: report.repo,
12569
12633
  status: "In Progress",
12570
12634
  partial: false,
12635
+ credential,
12571
12636
  ...claimedReceipt()
12572
12637
  };
12573
12638
  }
@@ -12601,7 +12666,7 @@ async function claimBoardIssues(options, deps = {}) {
12601
12666
  const ref = `${selector.repo}#${selector.number}`;
12602
12667
  try {
12603
12668
  const result = await claimOneBoardItem(ctx, selector, { ...options, bulk: true });
12604
- results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, resumeEvidence: result.resumeEvidence, reclaimedFrom: result.reclaimedFrom, alreadyClaimed: result.alreadyClaimed, renewed: result.renewed, checked: result.checked };
12669
+ results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, resumeEvidence: result.resumeEvidence, reclaimedFrom: result.reclaimedFrom, alreadyClaimed: result.alreadyClaimed, renewed: result.renewed, checked: result.checked, credential: result.credential };
12605
12670
  } catch (e) {
12606
12671
  results[index] = { ref, claimed: false, reason: e.message };
12607
12672
  }
@@ -12661,13 +12726,14 @@ async function moveBoardIssues(options, deps = {}) {
12661
12726
  const idx = next++;
12662
12727
  const { item, ref, index: resultIdx } = resolvedList[idx];
12663
12728
  try {
12664
- await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, statusOptionId);
12729
+ const { credential } = await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, statusOptionId);
12665
12730
  results[resultIdx] = {
12666
12731
  ref,
12667
12732
  moved: true,
12668
12733
  item: { ...item, status: options.status, statusOptionId },
12669
12734
  status: options.status,
12670
- partial: false
12735
+ partial: false,
12736
+ credential
12671
12737
  };
12672
12738
  } catch (e) {
12673
12739
  const warning = `partial move: ${ref} status was not changed to ${options.status} (${ghError(e)})`;
@@ -12734,8 +12800,9 @@ async function unclaimBoardIssue(options, deps = {}) {
12734
12800
  return { item, viewer, repo: currentRepo, status: item.status, partial: true, warning };
12735
12801
  }
12736
12802
  const optionId = cfg.statusOptions[toStatus];
12803
+ let unclaimCredential = "user";
12737
12804
  try {
12738
- await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, optionId);
12805
+ ({ credential: unclaimCredential } = await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, optionId));
12739
12806
  } catch (e) {
12740
12807
  const warning = `partial unclaim: ${item.ref} status was not changed to ${toStatus} (${ghError(e)})`;
12741
12808
  if (!options.allowPartial) throw new Error(warning);
@@ -12746,7 +12813,8 @@ async function unclaimBoardIssue(options, deps = {}) {
12746
12813
  viewer,
12747
12814
  repo: currentRepo,
12748
12815
  status: toStatus,
12749
- partial: false
12816
+ partial: false,
12817
+ credential: unclaimCredential
12750
12818
  };
12751
12819
  }
12752
12820
  async function setBoardItemPriority(client, cfg, itemId, priority) {
@@ -15870,10 +15938,10 @@ var rollout_plan_default = {
15870
15938
  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)."
15871
15939
  },
15872
15940
  baseline: {
15873
- version: "4.4.5",
15874
- tag: "v4.4.5",
15875
- commit: "202cb38f947b",
15876
- npm: "@mutmutco/cli@4.4.5"
15941
+ version: "4.4.7",
15942
+ tag: "v4.4.7",
15943
+ commit: "f0781ed0cbda",
15944
+ npm: "@mutmutco/cli@4.4.7"
15877
15945
  },
15878
15946
  exitCriterion: "fleet-n-of-n",
15879
15947
  hubOnlyShortcut: "forbidden",
@@ -15890,14 +15958,14 @@ var rollout_plan_default = {
15890
15958
  repo: "mutmutco/mmi-hub",
15891
15959
  role: "canary",
15892
15960
  schedule: "train",
15893
- v3Target: "v4.4.5"
15961
+ v3Target: "v4.4.7"
15894
15962
  }
15895
15963
  ],
15896
15964
  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.",
15897
15965
  rollback: {
15898
15966
  independent: true,
15899
- mechanism: "npm dist-tag latest -> 4.4.5 and redeploy the Hub Lambda from tag v4.4.5 (202cb38f947b); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15900
- v3Target: "v4.4.5 (@mutmutco/cli@4.4.5, tag commit 202cb38f947b \u2014 last known-good release carrying the repo-index v4-only contract)"
15967
+ mechanism: "npm dist-tag latest -> 4.4.7 and redeploy the Hub Lambda from tag v4.4.7 (f0781ed0cbda); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15968
+ v3Target: "v4.4.7 (@mutmutco/cli@4.4.7, tag commit f0781ed0cbda \u2014 last known-good release carrying the repo-index v4-only contract)"
15901
15969
  }
15902
15970
  },
15903
15971
  {
@@ -22814,6 +22882,62 @@ async function waitForFoldIndexLock(deps, startBranch, resumeCommand) {
22814
22882
  await sleep2(Math.min(GIT_INDEX_LOCK_POLL_MS, Math.max(1, deadline - now())));
22815
22883
  }
22816
22884
  }
22885
+ function isIndexLockFailure(message2) {
22886
+ return /index\.lock/i.test(message2) && /File exists/i.test(message2);
22887
+ }
22888
+ async function clearFoldChildIndexLock(deps) {
22889
+ const lockPath = requireValue(
22890
+ clean2(await deps.run("git", ["rev-parse", "--git-path", "index.lock"])),
22891
+ "git index lock path"
22892
+ );
22893
+ try {
22894
+ await (0, import_promises3.unlink)(lockPath);
22895
+ } catch (e) {
22896
+ if (e.code === "ENOENT") return void 0;
22897
+ throw e;
22898
+ }
22899
+ return lockPath;
22900
+ }
22901
+ async function resetFoldMainWithStaleLockRetry(deps, preFoldMainSha, killedCause) {
22902
+ try {
22903
+ await deps.run("git", ["reset", "--hard", preFoldMainSha]);
22904
+ return { ok: true, lockNote: "" };
22905
+ } catch (firstError) {
22906
+ const firstMessage = firstError instanceof Error ? firstError.message : String(firstError);
22907
+ if (!isIndexLockFailure(firstMessage)) return { ok: false, error: firstError, lockNote: "" };
22908
+ if (!foldChildWasKilled(killedCause)) {
22909
+ return {
22910
+ ok: false,
22911
+ error: firstError,
22912
+ lockNote: "the git index lock was NOT removed \u2014 no killed fold child is attributable, so its holder may still be alive (resolve or wait it out, then retry)"
22913
+ };
22914
+ }
22915
+ let clearedLockPath;
22916
+ try {
22917
+ clearedLockPath = await clearFoldChildIndexLock(deps);
22918
+ } catch (e) {
22919
+ return {
22920
+ ok: false,
22921
+ error: firstError,
22922
+ lockNote: `the fold child's stale git index lock could not be removed: ${e instanceof Error ? e.message : String(e)}`
22923
+ };
22924
+ }
22925
+ if (!clearedLockPath) return { ok: false, error: firstError, lockNote: "" };
22926
+ try {
22927
+ await deps.run("git", ["reset", "--hard", preFoldMainSha]);
22928
+ return {
22929
+ ok: true,
22930
+ lockNote: `removed the stale git index lock ${clearedLockPath} left by the fold child the train killed (signal), then retried the reset`
22931
+ };
22932
+ } catch (retryError) {
22933
+ return {
22934
+ ok: false,
22935
+ error: retryError,
22936
+ lockNote: `removed the stale git index lock ${clearedLockPath} left by the fold child the train killed (signal) and retried the reset once, which failed again`
22937
+ };
22938
+ }
22939
+ }
22940
+ }
22817
22941
  async function recoverFailedRcand(deps, cause, preRcSha) {
22818
22942
  const causeMessage = cause instanceof Error ? cause.message : String(cause);
22819
22943
  const branch = await currentBranch(deps).catch(() => "");
@@ -22857,10 +22981,13 @@ Recovery sequence:
22857
22981
  3. mmi-cli devops rcand --apply`
22858
22982
  );
22859
22983
  }
22984
+ function foldChildWasKilled(causeMessage) {
22985
+ return /\bSIGTERM\b|was killed \(signal/.test(causeMessage);
22986
+ }
22860
22987
  function foldFailureAnchor(causeMessage) {
22861
- if (/\bSIGTERM\b|was killed \(signal/.test(causeMessage)) return "docs/Guides/train-troubleshooting.md#fold-npm-ci-sigterm";
22862
- if (/\bEPERM\b|\bEBUSY\b/.test(causeMessage)) return "docs/Guides/train-troubleshooting.md#fold-eperm-ebusy";
22863
- if (/^(?:npm|node) .* failed\b/.test(causeMessage)) return "docs/Guides/train-troubleshooting.md#fold-lifecycle-script";
22988
+ if (foldChildWasKilled(causeMessage)) return troubleshootingAnchor("fold-npm-ci-sigterm");
22989
+ if (/\bEPERM\b|\bEBUSY\b/.test(causeMessage)) return troubleshootingAnchor("fold-eperm-ebusy");
22990
+ if (/^(?:npm|node) .* failed\b/.test(causeMessage)) return troubleshootingAnchor("fold-lifecycle-script");
22864
22991
  return void 0;
22865
22992
  }
22866
22993
  async function recoverFailedFold(deps, cause, startBranch, preFoldMainSha, resumeCommand) {
@@ -22889,13 +23016,12 @@ fold failed with an in-progress merge; automatic \`git merge --abort\` failed: $
22889
23016
  if (probe.branch === "main" && preFoldMainSha) {
22890
23017
  const mainDescendsFromPreFold = await deps.run("git", ["merge-base", "--is-ancestor", preFoldMainSha, "main"]).then(() => true).catch(() => false);
22891
23018
  if (mainDescendsFromPreFold) {
22892
- try {
22893
- await deps.run("git", ["reset", "--hard", preFoldMainSha]);
22894
- } catch (e) {
23019
+ const attempt = await resetFoldMainWithStaleLockRetry(deps, preFoldMainSha, rawCause);
23020
+ if (!attempt.ok) {
22895
23021
  return foldFailureGuidance(
22896
23022
  `${causeMessage}
22897
23023
 
22898
- fold failed after this run's merge/fold committed locally on main; automatic \`git reset --hard ${shaLabel(preFoldMainSha)}\` failed: ${e instanceof Error ? e.message : String(e)}.`,
23024
+ fold failed after this run's merge/fold committed locally on main; automatic \`git reset --hard ${shaLabel(preFoldMainSha)}\` failed: ${attempt.error instanceof Error ? attempt.error.message : String(attempt.error)}${attempt.lockNote ? `; ${attempt.lockNote}` : ""}.`,
22899
23025
  await probeFoldFailureState(deps),
22900
23026
  startBranch,
22901
23027
  preFoldMainSha,
@@ -22910,7 +23036,7 @@ fold failed after this run's merge/fold committed locally on main; automatic \`g
22910
23036
  startBranch,
22911
23037
  preFoldMainSha,
22912
23038
  resumeCommand,
22913
- `local main was reset to its pre-fold state ${shaLabel(preFoldMainSha)} (discarding only this run's merge/fold commit(s) at ${shaLabel(probe.mainSha)})${aheadNote}`
23039
+ `local main was reset to its pre-fold state ${shaLabel(preFoldMainSha)} (discarding only this run's merge/fold commit(s) at ${shaLabel(probe.mainSha)})${attempt.lockNote ? `; ${attempt.lockNote}` : ""}${aheadNote}`
22914
23040
  );
22915
23041
  }
22916
23042
  }
@@ -33529,10 +33655,10 @@ function formatOwnDetail(cmd) {
33529
33655
  }
33530
33656
  lines2.push("");
33531
33657
  }
33532
- const ownOptions = cmd.options.filter((opt) => opt.flags !== "--help" && opt.flags !== "-V, --version");
33533
- if (ownOptions.length) {
33658
+ const ownOptions2 = cmd.options.filter((opt) => opt.flags !== "--help" && opt.flags !== "-V, --version");
33659
+ if (ownOptions2.length) {
33534
33660
  lines2.push("Options:");
33535
- for (const opt of ownOptions) {
33661
+ for (const opt of ownOptions2) {
33536
33662
  const tags = [];
33537
33663
  if (opt.mandatory) tags.push("required");
33538
33664
  if (opt.default !== void 0) tags.push(`default: ${JSON.stringify(opt.default)}`);
@@ -33594,6 +33720,56 @@ function formatExplainLoop(playbook) {
33594
33720
  }
33595
33721
  return lines2.join("\n");
33596
33722
  }
33723
+ function ownOptions(cmd) {
33724
+ return cmd.options.filter((opt) => opt.flags !== "--help" && opt.flags !== "-V, --version");
33725
+ }
33726
+ function briefCommand(cmd) {
33727
+ return {
33728
+ path: cmd.path,
33729
+ ...cmd.description ? { description: cmd.description } : {},
33730
+ arguments: cmd.arguments.map((arg) => ({
33731
+ name: arg.name,
33732
+ required: arg.required,
33733
+ ...arg.variadic ? { variadic: true } : {},
33734
+ ...arg.description ? { description: arg.description } : {}
33735
+ })),
33736
+ options: ownOptions(cmd).map((opt) => ({
33737
+ flags: opt.flags,
33738
+ takesValue: opt.takesValue,
33739
+ ...opt.mandatory ? { required: true } : {},
33740
+ ...opt.description ? { description: opt.description } : {}
33741
+ }))
33742
+ };
33743
+ }
33744
+ function explainBriefManifest(manifest, command) {
33745
+ return {
33746
+ schema_version: 1,
33747
+ scope: "command",
33748
+ brief: true,
33749
+ name: manifest.name,
33750
+ ...manifest.version ? { version: manifest.version } : {},
33751
+ command: briefCommand(command),
33752
+ ...command.subcommands.length ? { children: command.subcommands.map(briefCommand) } : {}
33753
+ };
33754
+ }
33755
+ function formatExplainBrief(cmd, rootName) {
33756
+ const lines2 = [`${rootName} ${cmd.path}${cmd.description ? ` \u2014 ${cmd.description}` : ""}`];
33757
+ for (const arg of cmd.arguments) {
33758
+ const flag = arg.required ? `<${arg.name}>` : `[${arg.name}]`;
33759
+ lines2.push(` ${flag} ${arg.required ? "required" : "optional"}${arg.description ? ` ${arg.description}` : ""}`);
33760
+ }
33761
+ for (const opt of ownOptions(cmd)) {
33762
+ lines2.push(` ${opt.flags} ${opt.takesValue ? "takes-value" : "flag"}${opt.mandatory ? " required" : ""}${opt.description ? ` ${opt.description}` : ""}`);
33763
+ }
33764
+ for (const child2 of cmd.subcommands) {
33765
+ const args = child2.arguments.map((arg) => arg.required ? `<${arg.name}>` : `[${arg.name}]`).join(" ");
33766
+ lines2.push(`${child2.path}${args ? ` ${args}` : ""}${child2.description ? ` \u2014 ${child2.description}` : ""}`);
33767
+ for (const opt of ownOptions(child2)) {
33768
+ lines2.push(` ${opt.flags} ${opt.takesValue ? "takes-value" : "flag"}${opt.mandatory ? " required" : ""}${opt.description ? ` ${opt.description}` : ""}`);
33769
+ }
33770
+ }
33771
+ return lines2.join("\n");
33772
+ }
33597
33773
  function catalogCommandPaths(manifest) {
33598
33774
  const acc = [];
33599
33775
  const walk2 = (command) => {
@@ -33728,7 +33904,7 @@ function findCommandInManifest(manifest, commandPath3) {
33728
33904
  return visit(manifest.tree);
33729
33905
  }
33730
33906
  function registerExplainCommand(program3) {
33731
- program3.command("explain").description("print a command's purpose, flags, and examples from the live schema \u2014 one grounding call replaces trial-and-error").argument("[command...]", 'the command path to explain (e.g. "issue create")').addOption(new Option("--loop <name>", "print the canonical command sequence: agent | start-work | ship-pr | hotfix").choices(Object.keys(LOOP_PLAYBOOKS))).option("--json", "machine-readable focused command, route, or loop detail").option("--out <path>", "write the output to a UTF-8 file instead of stdout (#5802 byte contract)").option("--recursive", "include full schemas for immediate children (default: compact child index)").option("--full", "alias for --recursive").action((commandArgs, opts) => {
33907
+ program3.command("explain").description("print a command's purpose, flags, and examples from the live schema \u2014 one grounding call replaces trial-and-error").argument("[command...]", 'the command path to explain (e.g. "issue create")').addOption(new Option("--loop <name>", "print the canonical command sequence: agent | start-work | ship-pr | hotfix").choices(Object.keys(LOOP_PLAYBOOKS))).option("--json", "machine-readable focused command, route, or loop detail").option("--brief", "compact per-verb flags, value requirements, and one-line descriptions (no schemas or prose)").option("--out <path>", "write the output to a UTF-8 file instead of stdout (#5802 byte contract)").option("--recursive", "include full schemas for immediate children (default: compact child index)").option("--full", "alias for --recursive").action((commandArgs, opts) => {
33732
33908
  const emit = (output) => {
33733
33909
  if (!opts.out) console.log(output);
33734
33910
  else console.log(`Wrote explain output to ${opts.out} (UTF-8, ${writeUtf8Receipt(opts.out, output)} bytes)`);
@@ -33767,6 +33943,10 @@ function registerExplainCommand(program3) {
33767
33943
  return;
33768
33944
  }
33769
33945
  const recursive = Boolean(opts.recursive || opts.full);
33946
+ if (opts.brief) {
33947
+ emit(opts.json ? JSON.stringify(explainBriefManifest(manifest, command), null, 2) : formatExplainBrief(command, manifest.name));
33948
+ return;
33949
+ }
33770
33950
  emit(opts.json ? JSON.stringify(explainCommandManifest(manifest, command, { recursive }), null, 2) : command.subcommands.length ? formatExplainGroup(command, manifest.name) : formatExplainCommand(command, manifest.name));
33771
33951
  });
33772
33952
  }
@@ -34438,10 +34618,10 @@ async function preflightBatchSurfaces(validated, rowRepo, options) {
34438
34618
  continue;
34439
34619
  }
34440
34620
  if (known.length === 0) continue;
34441
- const inferred = inferSurface(known, { title: spec.title, body: spec.body });
34621
+ const inferred = inferSurface(known, { title: spec.title, type: spec.type, body: spec.body });
34442
34622
  spec.labels = [...spec.labels ?? [], inferred.label];
34443
34623
  process.stderr.write(
34444
- `mmi-cli: no --surface given \u2014 row ${row} filed under ${inferred.label} (${inferred.reason}). Pass --surface to choose.
34624
+ `mmi-cli: no --surface given \u2014 row ${row} filed under ${inferred.label} (${inferred.reason}). Pass --surface to choose.${surfaceInferenceSuffix(inferred)}
34445
34625
  `
34446
34626
  );
34447
34627
  }
@@ -37583,6 +37763,10 @@ function spawnDetachedSelf(args, deps, opts = {}) {
37583
37763
  var import_node_path39 = require("node:path");
37584
37764
 
37585
37765
  // src/attach-to-project.ts
37766
+ async function attachViaHubApp(cfg, contentNodeId) {
37767
+ const { addIssueToProjectViaHub: addIssueToProjectViaHub2 } = await Promise.resolve().then(() => (init_board_write(), board_write_exports));
37768
+ return addIssueToProjectViaHub2(cfg, contentNodeId);
37769
+ }
37586
37770
  function boardAttachRateLimitedReceipt(resetEpochSeconds) {
37587
37771
  return {
37588
37772
  onBoard: false,
@@ -40625,7 +40809,7 @@ async function attachToProject(issueNumber, repo, priority) {
40625
40809
  }
40626
40810
  }
40627
40811
  }
40628
- return { projectItemId, onBoard: true };
40812
+ return { projectItemId, onBoard: true, credential: "user" };
40629
40813
  } catch (e) {
40630
40814
  const err = e;
40631
40815
  const detail = (err.stderr || err.message || String(e)).trim();
@@ -40637,11 +40821,33 @@ async function attachToProject(issueNumber, repo, priority) {
40637
40821
  { repo: targetRepo2, number: issueNumber },
40638
40822
  priority
40639
40823
  );
40640
- return { projectItemId, onBoard: true };
40824
+ return { projectItemId, onBoard: true, credential: "user" };
40641
40825
  }
40642
40826
  return { onBoard: true };
40643
40827
  }
40644
40828
  if (isRateLimitText(detail)) {
40829
+ if (targetRepo2) {
40830
+ try {
40831
+ const boardCfg = await loadConfigForRepo(targetRepo2);
40832
+ const viewArgs2 = ["issue", "view", String(issueNumber), "--json", "id", "--jq", ".id"];
40833
+ if (targetRepo2) viewArgs2.push("--repo", targetRepo2);
40834
+ const contentId2 = (await execFileP("gh", viewArgs2, { timeout: 1e4 })).stdout.trim();
40835
+ if (contentId2 && boardCfg.projectId) {
40836
+ const projectItemId = await attachViaHubApp(boardCfg, contentId2);
40837
+ if (priority) {
40838
+ try {
40839
+ await setBoardItemPriority(defaultGitHubClient(), boardCfg, projectItemId, priority);
40840
+ } catch (e2) {
40841
+ const err2 = e2;
40842
+ process.stderr.write(`warning: issue #${issueNumber} board Priority not set: ${(err2.stderr || err2.message || String(e2)).trim()}
40843
+ `);
40844
+ }
40845
+ }
40846
+ return { projectItemId, onBoard: true, credential: "app_installation" };
40847
+ }
40848
+ } catch {
40849
+ }
40850
+ }
40645
40851
  process.stderr.write(`warning: issue #${issueNumber} created but board attach rate-limited: ${detail}
40646
40852
  `);
40647
40853
  let resetEpochSeconds;
@@ -40750,6 +40956,7 @@ function registerCollaborationCommands(program3) {
40750
40956
  repo: planRepo,
40751
40957
  labels: [...planLabels, ...surface ? [surface] : []],
40752
40958
  title,
40959
+ type,
40753
40960
  body: opts.body
40754
40961
  });
40755
40962
  if (warn) process.stderr.write(`${warn}
@@ -40757,10 +40964,7 @@ function registerCollaborationCommands(program3) {
40757
40964
  if (refusal) fail(refusal.message, refusal.payload);
40758
40965
  if (inferred && !surface) {
40759
40966
  planInferred = inferred;
40760
- process.stderr.write(
40761
- `mmi-cli: no --surface given \u2014 filed under ${inferred.label} (${inferred.reason}). Pass --surface to choose.
40762
- `
40763
- );
40967
+ process.stderr.write(surfaceInferenceNote(inferred));
40764
40968
  }
40765
40969
  }
40766
40970
  return {
@@ -40770,7 +40974,9 @@ function registerCollaborationCommands(program3) {
40770
40974
  priority,
40771
40975
  repo: opts.repo,
40772
40976
  ...surface ? { surface } : {},
40773
- ...planInferred ? { surface_inferred: true, surface: planInferred.label, surface_reason: planInferred.reason } : {}
40977
+ ...planInferred ? { surface_inferred: true, surface: planInferred.label, surface_reason: planInferred.reason } : {},
40978
+ ...planInferred?.uncertain ? { surface_uncertain: true } : {},
40979
+ ...planInferred?.alternatives?.length ? { surface_alternatives: planInferred.alternatives } : {}
40774
40980
  };
40775
40981
  }
40776
40982
  ).action(async (o) => {
@@ -40814,7 +41020,7 @@ function registerCollaborationCommands(program3) {
40814
41020
  return fail(`issue create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
40815
41021
  }
40816
41022
  {
40817
- const surfaceCheck = await checkSurfaceRequirement({ repo: targetRepo2, labels: extraLabels, title, body });
41023
+ const surfaceCheck = await checkSurfaceRequirement({ repo: targetRepo2, labels: extraLabels, title, type: issueType, body });
40818
41024
  const { refusal, warn, inferred } = surfaceCheck;
40819
41025
  if (warn) process.stderr.write(`${warn}
40820
41026
  `);
@@ -40844,10 +41050,7 @@ function registerCollaborationCommands(program3) {
40844
41050
  labels: extraLabels.length ? extraLabels : void 0
40845
41051
  });
40846
41052
  surfaceInferred = inferred;
40847
- process.stderr.write(
40848
- `mmi-cli: no --surface given \u2014 filed under ${inferred.label} (${inferred.reason}). Pass --surface to choose.
40849
- `
40850
- );
41053
+ process.stderr.write(surfaceInferenceNote(inferred));
40851
41054
  }
40852
41055
  if (refusal && !surfaceWaived()) return fail(refusal.message, refusal.payload);
40853
41056
  }
@@ -40859,7 +41062,7 @@ function registerCollaborationCommands(program3) {
40859
41062
  return;
40860
41063
  }
40861
41064
  const attached = await attachToProject(created.number, targetRepo2, priority);
40862
- const { projectItemId, onBoard } = attached;
41065
+ const { projectItemId, onBoard, credential: attachCredential } = attached;
40863
41066
  let parent;
40864
41067
  let parentLinkError;
40865
41068
  if (o.parent !== void 0) {
@@ -40879,6 +41082,7 @@ function registerCollaborationCommands(program3) {
40879
41082
  priority,
40880
41083
  projectItemId,
40881
41084
  onBoard,
41085
+ ...attachCredential ? { credential: attachCredential } : {},
40882
41086
  // #5489: partial receipt when the GitHub issue landed but Project v2 attach hit GraphQL quota.
40883
41087
  ...attached.boardAttach ? {
40884
41088
  boardAttach: attached.boardAttach,
@@ -40887,7 +41091,10 @@ function registerCollaborationCommands(program3) {
40887
41091
  } : {},
40888
41092
  ...parentLinkFields(parent, parentLinkError),
40889
41093
  // #1164: surfaced so a caller can see (and override with --surface) a pick it never asked for.
40890
- ...surfaceInferred ? { surface_inferred: true, surface: surfaceInferred.label, surface_reason: surfaceInferred.reason } : {}
41094
+ ...surfaceInferred ? { surface_inferred: true, surface: surfaceInferred.label, surface_reason: surfaceInferred.reason } : {},
41095
+ // #6818: a weak/ambiguous pick says so, with its runner-ups, so the caller can pass --surface.
41096
+ ...surfaceInferred?.uncertain ? { surface_uncertain: true } : {},
41097
+ ...surfaceInferred?.alternatives?.length ? { surface_alternatives: surfaceInferred.alternatives } : {}
40891
41098
  }));
40892
41099
  }), [
40893
41100
  'mmi-cli oracle issue create --type task --title "Wire the schema"',
@@ -41511,10 +41718,21 @@ ${list}`);
41511
41718
  return cwdRepo?.toLowerCase() === repo.split("/").slice(-2).join("/").toLowerCase();
41512
41719
  }
41513
41720
  async function prLandUpdateBranch(prNumber, repo, explicitRepo) {
41514
- const viewed = JSON.parse((await execFileP("gh", ["pr", "view", prNumber, "--repo", repo, "--json", "headRefName,baseRefName"], { timeout: GC_GH_TIMEOUT_MS4 })).stdout);
41515
- const localCheckedOut = await prHeadCheckedOutHere(viewed.headRefName, repo, explicitRepo);
41516
- console.warn(`pr land: PR #${prNumber} is BEHIND ${viewed.baseRefName} \u2014 updating ${viewed.headRefName} from the base (${localCheckedOut ? "local merge commit + push" : "GitHub update-branch"}) and re-waiting the checks once (#6263).`);
41517
- return updatePrHeadForMerge({ prNumber, repo, head: viewed.headRefName, base: viewed.baseRefName, localCheckedOut });
41721
+ let head = "";
41722
+ let base = "";
41723
+ const rested = await fetchRestPrSnapshot(prNumber, repo).catch(() => void 0);
41724
+ if (rested && rested.headRef && rested.baseRef) {
41725
+ console.warn("pr land: PR head/base read via REST (App-capable poll identity) instead of gh GraphQL (#6834).");
41726
+ head = rested.headRef;
41727
+ base = rested.baseRef;
41728
+ } else {
41729
+ const viewed = JSON.parse((await execFileP("gh", ["pr", "view", prNumber, "--repo", repo, "--json", "headRefName,baseRefName"], { timeout: GC_GH_TIMEOUT_MS4 })).stdout);
41730
+ head = viewed.headRefName;
41731
+ base = viewed.baseRefName;
41732
+ }
41733
+ const localCheckedOut = await prHeadCheckedOutHere(head, repo, explicitRepo);
41734
+ console.warn(`pr land: PR #${prNumber} is BEHIND ${base} \u2014 updating ${head} from the base (${localCheckedOut ? "local merge commit + push" : "GitHub update-branch"}) and re-waiting the checks once (#6263).`);
41735
+ return updatePrHeadForMerge({ prNumber, repo, head, base, localCheckedOut });
41518
41736
  }
41519
41737
  class PrHeadBehindBaseError extends Error {
41520
41738
  }
@@ -41555,9 +41773,22 @@ ${list}`);
41555
41773
  if (landClosingGuardVerdict.message) console.warn(landClosingGuardVerdict.message);
41556
41774
  const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false }, {
41557
41775
  resolveRepo: async (prNumber, repoOpt) => {
41558
- const args = repoOpt ? ["--repo", repoOpt] : repoArgs;
41559
- const viewed = (await execFileP("gh", ["pr", "view", prNumber, ...args, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS4 })).stdout.trim();
41560
- const [repoFromGh, base] = viewed.split(/\s+/);
41776
+ const knownRepo = repoOpt ?? await resolveRepo(o.repo).catch(() => void 0) ?? o.repo;
41777
+ let repoFromGh = "";
41778
+ let base = "";
41779
+ if (knownRepo) {
41780
+ const rested = await fetchRestPrSnapshot(prNumber, knownRepo).catch(() => void 0);
41781
+ if (rested) {
41782
+ console.warn("pr land: PR repo/base read via REST (App-capable poll identity) instead of gh GraphQL (#6834).");
41783
+ repoFromGh = knownRepo;
41784
+ base = rested.baseRef;
41785
+ }
41786
+ }
41787
+ if (!repoFromGh || !base) {
41788
+ const args = repoOpt ? ["--repo", repoOpt] : repoArgs;
41789
+ const viewed = (await execFileP("gh", ["pr", "view", prNumber, ...args, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS4 })).stdout.trim();
41790
+ [repoFromGh, base] = viewed.split(/\s+/);
41791
+ }
41561
41792
  const repo = repoOpt ?? repoFromGh;
41562
41793
  if (!repo) throw new Error("pr land: could not resolve PR repo");
41563
41794
  let track;
@@ -41683,8 +41914,9 @@ ${list}`);
41683
41914
  console.error(line);
41684
41915
  }
41685
41916
  }
41686
- if (o.json) printLine(JSON.stringify(result));
41687
- else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
41917
+ const credential = await pollToken().catch(() => void 0) ? "app_installation" : "user";
41918
+ if (o.json) printLine(JSON.stringify({ ...result, credential }));
41919
+ else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""} (credential: ${credential})`);
41688
41920
  if (result.status === "failed") process.exitCode = 1;
41689
41921
  });
41690
41922
  jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo); from a foreign checkout the remote probe/delete address this repo and local cleanup runs only in the verified sibling checkout ../<repo>, else localBranch reports skipped-foreign-cwd and the receipt carries foreignCwd: true (#6148)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").addOption(new Option("--disable-auto", "disable a queued auto-merge without merging").conflicts(["auto", "wait", "squash", "merge", "rebase", "preserveWorktree", "gc", "squashBodyFile", "force"])).option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m) \u2014 run as a background/monitor task or under a shell timeout above that budget; a short foreground timeout (e.g. 120s) kills it after checks pass and leaves the PR open (#6027)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888); also the merge settings-proof bypass when delete_branch_on_merge cannot be proven (#6210)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--squash-body-file <path>", "squash commit body (overrides GitHub COMMIT_MESSAGES); use when a pushed commit mentions close/fix/resolve + #N that must not close (#5723)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict")).action(async (number, o) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.4.5",
3
+ "version": "4.4.7",
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",