@mutmutco/cli 3.38.0 → 3.40.0

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 +356 -53
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -32,6 +32,7 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  DEFAULT_PRIORITY: () => DEFAULT_PRIORITY,
34
34
  awsCallerArn: () => awsCallerArn,
35
+ classifyParseError: () => classifyParseError,
35
36
  gcPlan: () => gcPlan,
36
37
  isOrgRegisteredRepo: () => isOrgRegisteredRepo,
37
38
  registryClientDeps: () => registryClientDeps,
@@ -4763,6 +4764,13 @@ async function filterDependencyBlockedClaimables(items, client, opts = {}) {
4763
4764
  return { claimable, blocked, warnings };
4764
4765
  }
4765
4766
 
4767
+ // src/registry-degrade.ts
4768
+ function registryDegradeError(error) {
4769
+ return new Error(
4770
+ `Hub registry read failed (${error}) \u2014 board coords could not be discovered. This is NOT necessarily the registry: a local DNS/network fault reaches this path as a timeout too. Check in order \u2014 (1) resolve the Hub API host (fails in seconds and rules out the local-network class), (2) if it resolves, retry: a Lambda cold start clears shortly, (3) if it still fails, check auth (a rejected credential is not a timeout).`
4771
+ );
4772
+ }
4773
+
4766
4774
  // ../infra/registry-endpoints.mjs
4767
4775
  var PROJECTS_LIST_PATH = "/projects/list";
4768
4776
  var ORG_CONFIG_PATH = "/org/config";
@@ -6557,20 +6565,56 @@ async function fetchSecretsDrift(deps, repo) {
6557
6565
  if (!data || data.mode !== "drift") return null;
6558
6566
  return data.drift;
6559
6567
  }
6560
- async function secretsOrgCatalogSet(deps, fileBody) {
6561
- let parsed;
6568
+ function formatOrgCatalog(entries) {
6569
+ const keys = Object.keys(entries).sort();
6570
+ if (!keys.length) return "org-infra catalog: no declarations";
6571
+ const width = Math.max(...keys.map((k) => k.length));
6572
+ const lines = keys.map((k) => {
6573
+ const e = entries[k];
6574
+ const stages = e.stages?.length ? e.stages.join(",") : "shared";
6575
+ return ` ${k.padEnd(width)} owner: ${e.owner} stages: ${stages} consumers: ${(e.consumers ?? []).join(",")} \u2014 ${e.purpose}`;
6576
+ });
6577
+ return [
6578
+ `org-infra secret catalog \u2014 ${keys.length} declaration(s) (_org/<provider>/<KEY>; declarations only, values never live here)`,
6579
+ ...lines
6580
+ ].join("\n");
6581
+ }
6582
+ function formatOrgCatalogOutcome(out) {
6583
+ const verb = out.mode === "replace" ? "replaced" : "merged";
6584
+ return `org-infra catalog ${verb}: ${out.added ?? 0} added, ${out.updated ?? 0} updated, ${out.unchanged ?? 0} unchanged, ${out.removed ?? 0} removed (${out.entries ?? "?"} entries total)`;
6585
+ }
6586
+ async function secretsOrgCatalogShow(deps, opts) {
6587
+ let res;
6562
6588
  try {
6563
- parsed = JSON.parse(fileBody);
6564
- } catch {
6565
- deps.err('secrets org-catalog: file is not valid JSON ({ entries: { "<provider>/<KEY>": {...} } })');
6589
+ res = await deps.fetch(`${deps.apiUrl}/catalog/org-infra`, {
6590
+ method: "GET",
6591
+ headers: await deps.headers(),
6592
+ signal: AbortSignal.timeout(TIMEOUT_MS)
6593
+ });
6594
+ } catch (e) {
6595
+ deps.err(`secrets org-catalog: ${e.message}`);
6596
+ return false;
6597
+ }
6598
+ if (!res.ok) {
6599
+ deps.err(await upgradeMessage(res) ?? `secrets org-catalog failed: HTTP ${res.status}${await readErr(res)}`);
6566
6600
  return false;
6567
6601
  }
6602
+ const cat = await res.json();
6603
+ const entries = cat.entries ?? {};
6604
+ if (opts.json) {
6605
+ deps.log(JSON.stringify({ entries }, null, 2));
6606
+ return true;
6607
+ }
6608
+ deps.log(formatOrgCatalog(entries));
6609
+ return true;
6610
+ }
6611
+ async function postOrgCatalog(deps, payload) {
6568
6612
  let res;
6569
6613
  try {
6570
6614
  res = await deps.fetch(`${deps.apiUrl}/catalog/org-infra`, {
6571
6615
  method: "POST",
6572
6616
  headers: await deps.headers({ "content-type": "application/json" }),
6573
- body: JSON.stringify(parsed),
6617
+ body: JSON.stringify(payload),
6574
6618
  signal: AbortSignal.timeout(TIMEOUT_MS)
6575
6619
  });
6576
6620
  } catch (e) {
@@ -6581,10 +6625,30 @@ async function secretsOrgCatalogSet(deps, fileBody) {
6581
6625
  deps.err(await upgradeMessage(res) ?? `secrets org-catalog failed: HTTP ${res.status}${await readErr(res)}`);
6582
6626
  return false;
6583
6627
  }
6584
- const out = await res.json();
6585
- deps.log(`org-infra catalog updated: ${out.entries ?? "?"} entries`);
6628
+ deps.log(formatOrgCatalogOutcome(await res.json()));
6586
6629
  return true;
6587
6630
  }
6631
+ async function secretsOrgCatalogSet(deps, fileBody, opts = {}) {
6632
+ let parsed;
6633
+ try {
6634
+ parsed = JSON.parse(fileBody);
6635
+ } catch {
6636
+ deps.err('secrets org-catalog: file is not valid JSON ({ entries: { "<provider>/<KEY>": {...} } })');
6637
+ return false;
6638
+ }
6639
+ if (parsed && typeof parsed === "object" && ("mode" in parsed || "remove" in parsed)) {
6640
+ deps.err('secrets org-catalog: the file carries { entries } only \u2014 use --replace / --remove flags, not "mode"/"remove" file fields');
6641
+ return false;
6642
+ }
6643
+ return postOrgCatalog(deps, {
6644
+ entries: parsed.entries,
6645
+ ...opts.replace ? { mode: "replace" } : {},
6646
+ ...opts.remove?.length ? { remove: opts.remove } : {}
6647
+ });
6648
+ }
6649
+ async function secretsOrgCatalogRemove(deps, remove2) {
6650
+ return postOrgCatalog(deps, { entries: {}, remove: remove2 });
6651
+ }
6588
6652
  var DEFAULT_RUNTIME_SECRET_NAMES = ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"];
6589
6653
  function stringList(v) {
6590
6654
  return Array.isArray(v) ? v.filter((x) => typeof x === "string" && isValidSecretKey(x)) : [];
@@ -6715,10 +6779,11 @@ async function putSecret(deps, key, value, opts) {
6715
6779
  return false;
6716
6780
  }
6717
6781
  const repo = await targetRepo(deps, opts);
6782
+ const slug = opts.slug?.toLowerCase();
6718
6783
  const res = await deps.fetch(`${deps.apiUrl}/secrets/set`, {
6719
6784
  method: "POST",
6720
6785
  headers: await deps.headers({ "content-type": "application/json" }),
6721
- body: JSON.stringify({ repo, key, value }),
6786
+ body: JSON.stringify({ repo, key, value, ...slug ? { slug } : {} }),
6722
6787
  signal: AbortSignal.timeout(TIMEOUT_MS)
6723
6788
  });
6724
6789
  if (!res.ok) {
@@ -6732,7 +6797,7 @@ async function putSecret(deps, key, value, opts) {
6732
6797
  const provider = providerForSecretKey(key);
6733
6798
  if (provider) {
6734
6799
  const result = await verifySecretValue(deps, key, value, opts);
6735
- const tier = classifyTier(await vaultSlug(deps, opts), key);
6800
+ const tier = classifyTier(slug ?? await vaultSlug(deps, opts), key);
6736
6801
  if (result.ok) {
6737
6802
  deps.log(`set ${key} (${tier} tier); ${result.message}`);
6738
6803
  return true;
@@ -6745,7 +6810,7 @@ async function putSecret(deps, key, value, opts) {
6745
6810
  deps.err(`set ${key} (${tier} tier), but ${result.message}`);
6746
6811
  return false;
6747
6812
  }
6748
- deps.log(`set ${key} (${classifyTier(await vaultSlug(deps, opts), key)} tier)`);
6813
+ deps.log(`set ${key} (${classifyTier(slug ?? await vaultSlug(deps, opts), key)} tier)`);
6749
6814
  return true;
6750
6815
  }
6751
6816
  async function secretsSet(deps, key, opts) {
@@ -6899,10 +6964,11 @@ async function secretsImportRailsCredentials(deps, opts) {
6899
6964
  async function secretsRemove(deps, key, opts) {
6900
6965
  if (!isValidSecretKey(key)) return deps.err(`invalid secret key ${JSON.stringify(key)}`);
6901
6966
  const repo = await targetRepo(deps, opts);
6967
+ const slug = opts.slug?.toLowerCase();
6902
6968
  const res = await deps.fetch(`${deps.apiUrl}/secrets/rm`, {
6903
6969
  method: "POST",
6904
6970
  headers: await deps.headers({ "content-type": "application/json" }),
6905
- body: JSON.stringify({ repo, key }),
6971
+ body: JSON.stringify({ repo, key, ...slug ? { slug } : {} }),
6906
6972
  signal: AbortSignal.timeout(TIMEOUT_MS)
6907
6973
  });
6908
6974
  if (!res.ok) {
@@ -7330,7 +7396,7 @@ async function loadConfigForBoardSelector(selector, repoOption) {
7330
7396
  if (!floor.sagaApiUrl) return stripMutableBoardConfig(floor);
7331
7397
  const slug = targetRepo2 ? slugOf(targetRepo2) : await repoSlug();
7332
7398
  const read = await fetchProjectBySlugChecked(slug, registryClientDeps(floor));
7333
- if (!read.ok) throw new Error(`Hub registry read failed (${read.error}) \u2014 board coords could not be discovered; likely transient (cold start, network, or auth blip) \u2014 retry shortly`);
7399
+ if (!read.ok) throw registryDegradeError(read.error);
7334
7400
  return read.project ? boardConfigFromProject(read.project, floor) : stripMutableBoardConfig(floor);
7335
7401
  }
7336
7402
 
@@ -8878,6 +8944,7 @@ function commandLadderHint() {
8878
8944
  lines.push(`${e.gh.padEnd(pad)} \u2192 ${e.replacement}`);
8879
8945
  }
8880
8946
  lines.push("[mmi] Schedules doctrine (#3228): scheduled jobs + LLM crons go through the Hub registry \u2014 eight-field header \u2192 PR \u2192 `mmi-cli org schedules register`; LLM crons ONLY via the harbour (llm: yes \u21D2 executor cursor-agent). Never hand-build an unregistered cron. Opt out per repo: `mmi-cli org schedules mode self-managed`.");
8947
+ lines.push('[mmi] Registering \u2260 armed (#3281): `org schedules register` writes SCHEDULE# rows; the fleet-clock reconciler arms them within ~15 min. Report "registered successfully \u2014 arms on the next fleet-clock tick", never "armed"/"live". The success JSON carries an `arming` block. Before that window an absent clock means "not reconciled yet", NOT "will never fire" \u2014 verify with `mmi-cli org schedules` after it.');
8881
8948
  return lines.join("\n");
8882
8949
  }
8883
8950
 
@@ -8903,6 +8970,40 @@ function resolveMergeCiPolicy(input) {
8903
8970
  }
8904
8971
  return { policy: "wait-for-checks", reason: ciWorkflows.join(", ") };
8905
8972
  }
8973
+ function describePrMergeEnqueuedReason(checks) {
8974
+ switch (checks) {
8975
+ case "pending":
8976
+ return {
8977
+ reason: "checks-pending",
8978
+ message: "checks are still pending; auto-merge will land once they settle, so retry later is appropriate"
8979
+ };
8980
+ case "failure":
8981
+ return {
8982
+ reason: "checks-failing",
8983
+ message: "GitHub reports a red check; auto-merge will not land until the required checks pass, so retrying now is pointless"
8984
+ };
8985
+ case "failing":
8986
+ return {
8987
+ reason: "checks-failing",
8988
+ message: "GitHub reports a red check while others are still running; wait for checks to settle before retrying"
8989
+ };
8990
+ case "no-checks-reported":
8991
+ return {
8992
+ reason: "no-checks-reported",
8993
+ message: "GitHub reports no checks; auto-merge remains queued until the base-branch policy permits the merge"
8994
+ };
8995
+ case "success":
8996
+ return {
8997
+ reason: "checks-passing",
8998
+ message: "reported checks are passing, but the base-branch policy or merge queue has not completed the merge yet"
8999
+ };
9000
+ default:
9001
+ return {
9002
+ reason: "checks-unavailable",
9003
+ message: "the current check verdict could not be read; auto-merge remains queued, so inspect checks before retrying"
9004
+ };
9005
+ }
9006
+ }
8906
9007
  function parseGhPrChecksOutput(stdout) {
8907
9008
  const text = stdout.trim();
8908
9009
  if (!text) return "no-checks-reported";
@@ -11317,14 +11418,27 @@ function buildPrArgs({ title, body, base, head, repo, draft }) {
11317
11418
  if (draft) args.push("--draft");
11318
11419
  return args;
11319
11420
  }
11421
+ function parseExistingPr(stderr) {
11422
+ if (!/already exists/i.test(stderr)) return void 0;
11423
+ const match = /https:\/\/\S*\/pull\/(\d+)/.exec(stderr);
11424
+ if (!match) return void 0;
11425
+ return { number: Number(match[1]), url: match[0] };
11426
+ }
11320
11427
  async function ghCreate(args) {
11321
11428
  const swapped = await bodyArgsViaFile(args);
11322
11429
  try {
11323
11430
  const { stdout } = await execFileP2("gh", swapped.args, { timeout: GH_MUTATION_TIMEOUT_MS });
11324
11431
  return parseCreatedUrl(stdout);
11325
11432
  } catch (e) {
11326
- await swapped.cleanup();
11327
11433
  const err = e;
11434
+ const existing = parseExistingPr(`${err.stderr ?? ""}
11435
+ ${err.message ?? ""}`);
11436
+ if (existing) {
11437
+ await swapped.cleanup();
11438
+ console.warn(`${args[0]} create: a PR already exists for this branch \u2014 #${existing.number} (nothing to do)`);
11439
+ return { ...existing, existing: true };
11440
+ }
11441
+ await swapped.cleanup();
11328
11442
  const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
11329
11443
  fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
11330
11444
  } finally {
@@ -16416,14 +16530,31 @@ function registerSecretsCommands(program3) {
16416
16530
  });
16417
16531
  if (!report.ok) process.exitCode = 1;
16418
16532
  });
16419
- secrets.command("org-catalog").description("MASTER-ONLY: set the _org/<provider>/* catalog declarations from a JSON file (#2244)").requiredOption("--file <path>", 'JSON file: { "entries": { "<provider>/<KEY>": {key,purpose,group,owner,provider,stages[],consumers[]} } }').action((o) => withSecrets(async (d) => {
16533
+ secrets.command("org-catalog").description("the _org/<provider>/* catalog declarations (#2244) \u2014 --show reads (any org member); writes are master-only and MERGE by default (#3297)").option("--show", "print the stored catalog (declarations only \u2014 names, purpose, owner; never values)").option("--json", "--show output as the exact { entries } JSON shape --file accepts (lossless read-edit-write round-trip)").option("--file <path>", 'JSON file: { "entries": { "<provider>/<KEY>": {key,purpose,group,owner,provider,stages[],consumers[]} } } \u2014 merged into the stored catalog').option("--replace", "DESTRUCTIVE: replace the whole catalog with --file exactly (requires --yes)").option("--remove <provider/KEY>", "delete one declaration from the stored catalog (repeatable)", collectMap, []).option("--yes", "confirm the destructive --replace").action((o) => withSecrets(async (d) => {
16534
+ if (o.show) {
16535
+ if (o.file || o.replace || o.remove?.length) return fail("secrets org-catalog: --show does not combine with --file/--replace/--remove");
16536
+ if (!await secretsOrgCatalogShow(d, { json: o.json })) process.exitCode = 1;
16537
+ return;
16538
+ }
16539
+ if (o.replace) {
16540
+ if (!o.file) return fail("secrets org-catalog: --replace needs --file (the complete catalog to write)");
16541
+ if (!o.yes) {
16542
+ return fail("secrets org-catalog --replace is DESTRUCTIVE \u2014 it deletes every declaration absent from the file (the #3297 data-loss form). Re-run with --yes to confirm; prefer the default merge, or --remove <provider/KEY> to delete one declaration.");
16543
+ }
16544
+ }
16545
+ if (!o.file && !o.remove?.length) {
16546
+ return fail("secrets org-catalog: nothing to do \u2014 pass --show, --file <path> (merged by default), or --remove <provider/KEY>");
16547
+ }
16420
16548
  let body;
16421
- try {
16422
- body = (0, import_node_fs18.readFileSync)((0, import_node_path16.resolve)(o.file), "utf8");
16423
- } catch (e) {
16424
- return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
16549
+ if (o.file) {
16550
+ try {
16551
+ body = (0, import_node_fs18.readFileSync)((0, import_node_path16.resolve)(o.file), "utf8");
16552
+ } catch (e) {
16553
+ return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
16554
+ }
16425
16555
  }
16426
- if (!await secretsOrgCatalogSet(d, body)) process.exitCode = 1;
16556
+ const ok = body !== void 0 ? await secretsOrgCatalogSet(d, body, { replace: o.replace, remove: o.remove }) : await secretsOrgCatalogRemove(d, o.remove ?? []);
16557
+ if (!ok) process.exitCode = 1;
16427
16558
  }));
16428
16559
  secrets.command("preflight").description("check required stage secret names for a deploy/train without reading values").requiredOption("--stage <dev|rc|main>", "stage to check").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--required <KEY...>", "required keys; bare keys are scoped under --stage").option("--skip-compose-guard", "skip the #2813 fileless-compose \u2194 DEPLOY#.noEnvFile promotion guard").option("--json", "machine-readable output").action(async (o) => {
16429
16560
  if (!["dev", "rc", "main"].includes(o.stage)) {
@@ -16496,7 +16627,7 @@ function registerSecretsCommands(program3) {
16496
16627
  const ok = await secretsSet(d, key, o);
16497
16628
  if (!ok) process.exitCode = 1;
16498
16629
  });
16499
- secrets.command("set <key>").description("write/rotate a secret; value from stdin (never an argument). DECLARE-FIRST (#2528): the key must be a declared catalog coordinate \u2014 bare <KEY> = the stageless canonical, <stage>/<KEY> = a declared per-stage override; undeclared writes are rejected with the fix").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(setHandler);
16630
+ secrets.command("set <key>").description("write/rotate a secret; value from stdin (never an argument). DECLARE-FIRST (#2528): the key must be a declared catalog coordinate \u2014 bare <KEY> = the stageless canonical, <stage>/<KEY> = a declared per-stage override; undeclared writes are rejected with the fix. --slug _org writes an org-infra <provider>/<KEY> coordinate (#3298) \u2014 master or an exact grant, declare-first enforced server-side").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for the value write \u2014 master or an exact grant (#3298)").action(setHandler);
16500
16631
  secrets.command("copy").description("copy provider keys between stages in your own project vault (audit-logged; encryption keys blocked)").requiredOption("--from <stage>", "source tier: dev, rc, or main").requiredOption("--to <stage>", "destination tier: dev, rc, or main").requiredOption("--keys <names>", "comma-separated secret names (encryption keys blocked)").option("--dry-run", "report copies without writing").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets(async (d) => {
16501
16632
  const stages = ["dev", "rc", "main"];
16502
16633
  if (!stages.includes(o.from) || !stages.includes(o.to)) {
@@ -16537,7 +16668,7 @@ function registerSecretsCommands(program3) {
16537
16668
  );
16538
16669
  if (!ok) process.exitCode = 1;
16539
16670
  }));
16540
- secrets.command("rm <key>").description("remove a secret from your own full project vault; org-infra requires master or an exact grant").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsRemove(d, key, o)));
16671
+ secrets.command("rm <key>").description("remove a secret from your own full project vault; org-infra requires master or an exact grant. --slug _org removes an org-infra <provider>/<KEY> value (#3315)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for the value removal \u2014 master or an exact grant (#3315)").action((key, o) => withSecrets((d) => secretsRemove(d, key, o)));
16541
16672
  secrets.command("use <key> [command...]").description("consume a secret KEYLESS: own-project full tree or an exactly granted org-infra key; injects into command env and never prints it").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--slug <slug>", "org-infra namespace (e.g. _org) for a granted org secret").option("--name <ENVVAR>", "env var name to inject under (default: the key leaf, UPPER_SNAKE)").action((key, command, o) => withSecrets(async (d) => {
16542
16673
  const ok = await secretsUse(d, key, { ...o, command });
16543
16674
  if (ok === false) process.exitCode = 1;
@@ -16642,7 +16773,7 @@ async function mintInstallationToken(deps) {
16642
16773
  }
16643
16774
  return { token: body.token, expiresAt: body.expires_at };
16644
16775
  }
16645
- async function activateAppActor(commandPath2, env, mint) {
16776
+ async function activateAppActor(commandPath3, env, mint) {
16646
16777
  const requested = (env[APP_ACTOR_ENV] ?? "").trim();
16647
16778
  if (!requested) return "personal";
16648
16779
  if (requested !== "app") {
@@ -16650,8 +16781,8 @@ async function activateAppActor(commandPath2, env, mint) {
16650
16781
  }
16651
16782
  delete env.GH_TOKEN;
16652
16783
  delete env.GITHUB_TOKEN;
16653
- if (!APP_ELIGIBLE_COMMANDS.has(commandPath2)) {
16654
- throw new AppActorError(`app actor: '${commandPath2}' is personal-only \u2014 App identity would break viewer/@me/authorship semantics; unset ${APP_ACTOR_ENV} for this command`);
16784
+ if (!APP_ELIGIBLE_COMMANDS.has(commandPath3)) {
16785
+ throw new AppActorError(`app actor: '${commandPath3}' is personal-only \u2014 App identity would break viewer/@me/authorship semantics; unset ${APP_ACTOR_ENV} for this command`);
16655
16786
  }
16656
16787
  const minted = await mint();
16657
16788
  env.GH_TOKEN = minted.token;
@@ -16975,9 +17106,17 @@ function awsScheduleEntry(payload) {
16975
17106
  if (typeof Name !== "string" || typeof ScheduleExpression !== "string") return null;
16976
17107
  if (State !== "ENABLED" || isJervResource(Name)) return null;
16977
17108
  let target = "";
17109
+ let scheduleId;
16978
17110
  if (Target && typeof Target === "object") {
16979
- const { Arn } = Target;
17111
+ const { Arn, Input } = Target;
16980
17112
  if (typeof Arn === "string") target = Arn.split(":").pop() ?? "";
17113
+ if (typeof Input === "string") {
17114
+ try {
17115
+ const parsed = JSON.parse(Input);
17116
+ if (typeof parsed?.scheduleId === "string" && parsed.scheduleId) scheduleId = parsed.scheduleId;
17117
+ } catch {
17118
+ }
17119
+ }
16981
17120
  }
16982
17121
  return {
16983
17122
  name: Name,
@@ -16985,7 +17124,8 @@ function awsScheduleEntry(payload) {
16985
17124
  executor: target ? `aws-scheduler \u2192 ${target}` : "aws-scheduler",
16986
17125
  llm: readLlmDeclaration(typeof Description === "string" ? Description : void 0),
16987
17126
  resolved: "live",
16988
- source: `aws scheduler schedule ${Name} (eu-central-1)`
17127
+ source: `aws scheduler schedule ${Name} (eu-central-1)`,
17128
+ ...scheduleId ? { scheduleId } : {}
16989
17129
  };
16990
17130
  }
16991
17131
  function awsScheduleRefs(payload) {
@@ -17153,7 +17293,28 @@ function unlauncheredLlmDrifts(entries) {
17153
17293
  }
17154
17294
  return drifts.sort((a, b) => a.name.localeCompare(b.name));
17155
17295
  }
17156
- function assembleReconciliation(githubEntries2, readRepos, registry2, activeWorkflowNames = /* @__PURE__ */ new Set()) {
17296
+ var HARBOUR_ARM_GRACE_MS = 30 * 60 * 1e3;
17297
+ function registeredButUnarmedDrifts(registry2, liveEntries, opts) {
17298
+ if (!opts.schedulerRead) return [];
17299
+ const now = opts.now ?? Date.now();
17300
+ const armed = new Set(liveEntries.map((e) => e.scheduleId).filter((id) => Boolean(id)));
17301
+ const drifts = [];
17302
+ for (const row of registry2) {
17303
+ if (row.executor === "github-actions") continue;
17304
+ if (armed.has(row.id)) continue;
17305
+ const stamped = row.updatedAt ? Date.parse(row.updatedAt) : NaN;
17306
+ if (Number.isFinite(stamped) && now - stamped < HARBOUR_ARM_GRACE_MS) continue;
17307
+ drifts.push({
17308
+ class: "registered-but-unarmed",
17309
+ name: row.id,
17310
+ executor: row.executor,
17311
+ detail: "registered as a harbour lane but no aws-scheduler clock dispatches it \u2014 it will never fire",
17312
+ remedy: "check the fleet-clock reconciler tick; re-run `mmi-cli org schedules register` for the repo and verify with `mmi-cli org schedules`"
17313
+ });
17314
+ }
17315
+ return drifts.sort((a, b) => a.name.localeCompare(b.name));
17316
+ }
17317
+ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWorkflowNames = /* @__PURE__ */ new Set(), harbour = {}) {
17157
17318
  if (registry2 === null) {
17158
17319
  return {
17159
17320
  reconciliation: [],
@@ -17162,7 +17323,13 @@ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWork
17162
17323
  };
17163
17324
  }
17164
17325
  const live = githubEntries2.filter((e) => e.executor === "github-actions");
17165
- const drifts = reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames);
17326
+ const drifts = [
17327
+ ...reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames),
17328
+ ...registeredButUnarmedDrifts(registry2, harbour.awsEntries ?? [], {
17329
+ schedulerRead: Boolean(harbour.schedulerRead),
17330
+ now: harbour.now
17331
+ })
17332
+ ];
17166
17333
  return { reconciliation: drifts, driftLines: drifts.map(renderDrift), incomplete: [] };
17167
17334
  }
17168
17335
 
@@ -17275,6 +17442,7 @@ async function awsEntries() {
17275
17442
  } catch (e) {
17276
17443
  incomplete.push(`aws: events list-rules failed \u2014 ${e.message}`);
17277
17444
  }
17445
+ let schedulerRead = false;
17278
17446
  try {
17279
17447
  const refs = awsScheduleRefs(await awsJson(["scheduler", "list-schedules"]));
17280
17448
  for (const ref of refs) {
@@ -17282,11 +17450,12 @@ async function awsEntries() {
17282
17450
  const entry = awsScheduleEntry(await awsJson(args));
17283
17451
  if (entry) entries.push(entry);
17284
17452
  }
17453
+ schedulerRead = true;
17285
17454
  } catch (e) {
17286
17455
  incomplete.push(`aws: scheduler listing failed \u2014 ${e.message}`);
17287
17456
  }
17288
17457
  const reconciliation = unlauncheredLlmDrifts(entries);
17289
- return { entries, incomplete, drift: reconciliation.map(renderDrift), reconciliation };
17458
+ return { entries, incomplete, drift: reconciliation.map(renderDrift), reconciliation, schedulerRead };
17290
17459
  }
17291
17460
  async function defaultRegistryRead() {
17292
17461
  return fetchSchedulesList(registryClientDeps(await loadConfig()));
@@ -17298,7 +17467,11 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
17298
17467
  readRegistry().catch(() => null),
17299
17468
  readProjects().catch(() => null)
17300
17469
  ]);
17301
- const recon = assembleReconciliation(gh.entries, new Set(gh.readRepos), registry2, new Set(gh.workflowNames));
17470
+ const recon = assembleReconciliation(gh.entries, new Set(gh.readRepos), registry2, new Set(gh.workflowNames), {
17471
+ // #3286: the harbour side joins registry rows against the live aws-scheduler clocks by scheduleId.
17472
+ awsEntries: aws.entries,
17473
+ schedulerRead: Boolean(aws.schedulerRead)
17474
+ });
17302
17475
  const selfManaged = /* @__PURE__ */ new Set();
17303
17476
  for (const proj of projects ?? []) {
17304
17477
  if (proj?.schedulesMode !== "self-managed") continue;
@@ -17541,17 +17714,33 @@ async function runSchedulesLift(opts, deps = {}) {
17541
17714
  }
17542
17715
  return { repo, records, payload, response };
17543
17716
  }
17717
+ var FLEET_CLOCK_RECONCILE_WINDOW = "~15 minutes";
17718
+ var ARMING_IS_EVENTUAL_NOTE = ` registered \u2014 not yet armed. The fleet-clock reconciler arms SCHEDULE# rows on its next tick (${FLEET_CLOCK_RECONCILE_WINDOW}).
17719
+ Verify with \`mmi-cli org schedules\` after that window; an empty result before it means "not reconciled yet", not "will never fire".`;
17720
+ var ARMING_REPORT = {
17721
+ armed: false,
17722
+ state: "pending-reconcile",
17723
+ withinMinutes: 15,
17724
+ verifyWith: "mmi-cli org schedules",
17725
+ report: "registered successfully \u2014 arms on the next fleet-clock tick, not yet",
17726
+ note: `SCHEDULE# rows are written now; the fleet-clock reconciler arms them within ${FLEET_CLOCK_RECONCILE_WINDOW}. Before that window an absent clock means "not reconciled yet", NOT "will never fire" \u2014 do not report this as armed, live, or failed.`
17727
+ };
17728
+ function withArmingReport(body) {
17729
+ if (body === null || typeof body !== "object" || Array.isArray(body)) return body;
17730
+ return { ...body, arming: ARMING_REPORT };
17731
+ }
17544
17732
  function registerSchedulesLiftCommand(program3, deps = {}) {
17545
17733
  const schedules = program3.commands.find((c) => c.name() === "schedules");
17546
17734
  if (!schedules) throw new Error("schedules lift: registerSchedulesCommands must run first \u2014 the lift attaches to the `schedules` command");
17547
- schedules.command("register").alias("lift").description("register this repo's eight-field workflow headers as SCHEDULE# registry rows (C2, #3186; renamed from `lift`, #3219) \u2014 a per-repo replace; aborts the whole registration on any header violation; exits 75 when the registry is unreachable (#3187)").option("--repo <name>", "bare repo name for the join key (defaults to the origin remote basename, case-preserved)").option("--dir <path>", "workflows directory to lift (defaults to .github/workflows)").option("--dry-run", "print the {repo, schedules} body that would be POSTed; never write").action(async (o) => {
17735
+ schedules.command("register").alias("lift").description(`register this repo's eight-field workflow headers as SCHEDULE# registry rows (C2, #3186; renamed from \`lift\`, #3219) \u2014 a per-repo replace; aborts the whole registration on any header violation; exits 75 when the registry is unreachable (#3187). Registering does NOT arm the clock: the fleet-clock reconciler arms the rows within ${FLEET_CLOCK_RECONCILE_WINDOW}, and the success JSON carries an \`arming\` block saying so (#3281)`).option("--repo <name>", "bare repo name for the join key (defaults to the origin remote basename, case-preserved)").option("--dir <path>", "workflows directory to lift (defaults to .github/workflows)").option("--dry-run", "print the {repo, schedules} body that would be POSTed; never write").action(async (o) => {
17548
17736
  try {
17549
17737
  const result = await runSchedulesLift({ repo: o.repo, dir: o.dir, dryRun: Boolean(o.dryRun) }, deps);
17550
17738
  if (o.dryRun) {
17551
17739
  console.log(JSON.stringify(result.payload, null, 2));
17552
17740
  return;
17553
17741
  }
17554
- console.log(JSON.stringify(result.response?.body));
17742
+ console.log(JSON.stringify(withArmingReport(result.response?.body)));
17743
+ console.error(ARMING_IS_EVENTUAL_NOTE);
17555
17744
  } catch (e) {
17556
17745
  const err = e;
17557
17746
  if (err instanceof RegistryUnreachableError) {
@@ -19286,9 +19475,6 @@ function registerStageCommands(program3) {
19286
19475
 
19287
19476
  // src/config-load.ts
19288
19477
  var discoveredConfig = null;
19289
- function registryDegradeError(error) {
19290
- return new Error(`Hub registry read failed (${error}) \u2014 board coords could not be discovered; likely transient (cold start, network, or auth blip) \u2014 retry shortly`);
19291
- }
19292
19478
  async function loadConfigOrDiscover() {
19293
19479
  if (discoveredConfig) return discoveredConfig;
19294
19480
  const floor = await loadConfig();
@@ -21231,6 +21417,17 @@ async function applyPluginHeal(surface, log, opts) {
21231
21417
  }
21232
21418
  return true;
21233
21419
  }
21420
+ async function healClaudePluginForDoctor(surface = detectSurface(process.env)) {
21421
+ if (surfaceToken(surface) !== "claude") {
21422
+ return { ok: false, detail: `not a Claude surface (${surface}) \u2014 no Hub-shipped plugin to reinstall` };
21423
+ }
21424
+ const steps = [];
21425
+ const ok = await applyPluginHeal(surface, (msg) => steps.push(msg.trim()));
21426
+ return {
21427
+ ok,
21428
+ detail: ok ? "marketplace remove \u2192 add \u2192 install succeeded" : `\`claude plugin\` reinstall failed or was skipped${steps.length ? ` (${steps[steps.length - 1]})` : ""}`
21429
+ };
21430
+ }
21234
21431
  async function runGuard(readOrigin) {
21235
21432
  try {
21236
21433
  const surface = detectSurface(process.env);
@@ -21766,9 +21963,9 @@ function formatExplainLoop(playbook) {
21766
21963
  }
21767
21964
  return lines.join("\n");
21768
21965
  }
21769
- function findCommandInManifest(manifest, commandPath2) {
21966
+ function findCommandInManifest(manifest, commandPath3) {
21770
21967
  const visit = (command) => {
21771
- if (command.path === commandPath2) return command;
21968
+ if (command.path === commandPath3) return command;
21772
21969
  for (const child2 of command.subcommands) {
21773
21970
  const found = visit(child2);
21774
21971
  if (found) return found;
@@ -21793,10 +21990,10 @@ function registerExplainCommand(program3) {
21793
21990
  console.log(formatManifestHuman(manifest));
21794
21991
  return;
21795
21992
  }
21796
- const commandPath2 = commandArgs.join(" ");
21797
- const command = findCommandInManifest(manifest, commandPath2);
21993
+ const commandPath3 = commandArgs.join(" ");
21994
+ const command = findCommandInManifest(manifest, commandPath3);
21798
21995
  if (!command) {
21799
- fail(`explain: unknown command "${commandPath2}"`, { code: ERROR_CODES.ERR_NOT_FOUND });
21996
+ fail(`explain: unknown command "${commandPath3}"`, { code: ERROR_CODES.ERR_NOT_FOUND });
21800
21997
  return;
21801
21998
  }
21802
21999
  console.log(command.subcommands.length ? formatExplainGroup(command, manifest.name) : formatExplainCommand(command, manifest.name));
@@ -22425,6 +22622,11 @@ function checkRepoWorktrees(probe) {
22425
22622
  ]
22426
22623
  };
22427
22624
  }
22625
+ function pluginHealTrigger(probe) {
22626
+ if (probe.guardState === "no-install" || probe.guardState === "unresolved") return "unresolved";
22627
+ const behind = Boolean(probe.installed && probe.released) && compareVersions(probe.installed, probe.released) < 0;
22628
+ return behind ? "behind" : null;
22629
+ }
22428
22630
  function checkClaudePlugin(probe) {
22429
22631
  const { installed, released, guardState } = probe;
22430
22632
  const evidence = [
@@ -22432,20 +22634,22 @@ function checkClaudePlugin(probe) {
22432
22634
  `released: ${released ?? "(not checked \u2014 offline or --fast)"}`,
22433
22635
  `resolvable: ${guardState}`
22434
22636
  ];
22435
- if (guardState === "no-install" || guardState === "unresolved") {
22637
+ const trigger = pluginHealTrigger(probe);
22638
+ if (trigger === "unresolved") {
22436
22639
  return {
22437
22640
  ok: false,
22438
22641
  label: guardState === "no-install" ? "Claude plugin \u2014 not installed" : "Claude plugin \u2014 unresolved (marketplace/cache missing)",
22439
- fix: "run `mmi-cli plugin heal` to reinstall the MMI marketplace + plugin, then restart Claude",
22642
+ fix: "run `mmi-cli doctor --apply` (or `mmi-cli plugin heal`) to reinstall the MMI marketplace + plugin, then restart Claude",
22440
22643
  verbose: evidence
22441
22644
  };
22442
22645
  }
22443
- const behind = Boolean(installed && released) && compareVersions(installed, released) < 0;
22444
- if (behind) {
22646
+ if (trigger === "behind") {
22445
22647
  return {
22446
22648
  ok: false,
22447
22649
  label: `Claude plugin ${installed} \u2192 ${released}`,
22448
- fix: "run /plugin, update MMI (reinstall if it shows a legacy row), then restart Claude",
22650
+ // Name the CLI verb that actually fixes it, like every other red row `/plugin` is the manual
22651
+ // fallback, not the first resort (#3282).
22652
+ fix: "run `mmi-cli doctor --apply` (or `mmi-cli plugin heal`) to reinstall it, then restart Claude",
22449
22653
  verbose: evidence
22450
22654
  };
22451
22655
  }
@@ -22597,9 +22801,34 @@ async function runDoctorClean(opts, io, deps) {
22597
22801
  checks.push({ ok: false, label: "gitignore block", fix: "run `mmi-cli doctor --apply` to write the org-managed .gitignore block", verbose: giEvidence });
22598
22802
  }
22599
22803
  }
22600
- const plugin = checkClaudePlugin({ installed, released, guardState: deps.pluginGuardState(isOrgRepo) });
22601
- checks.push(plugin);
22602
- if (!plugin.ok) restartPending = true;
22804
+ const pluginProbe = { installed, released, guardState: deps.pluginGuardState(isOrgRepo) };
22805
+ const healTrigger = pluginHealTrigger(pluginProbe);
22806
+ if (apply && deps.healPlugin && healTrigger) {
22807
+ const heal = await deps.healPlugin();
22808
+ const label = healTrigger === "behind" ? `Claude plugin ${installed} \u2192 ${released}` : "Claude plugin \u2014 reinstalled";
22809
+ const healEvidence = [
22810
+ `installed: ${installed ?? "(none)"}`,
22811
+ `released: ${released ?? "(not checked \u2014 offline or --fast)"}`,
22812
+ `resolvable: ${pluginProbe.guardState}`,
22813
+ `heal: ${heal.detail}`
22814
+ ];
22815
+ checks.push(heal.ok ? {
22816
+ ok: true,
22817
+ label,
22818
+ detail: "reinstalled via the MMI marketplace (remove \u2192 add \u2192 install)",
22819
+ verbose: healEvidence
22820
+ } : {
22821
+ ok: false,
22822
+ label,
22823
+ fix: `auto-heal failed (${heal.detail}) \u2014 run \`mmi-cli plugin heal\`, then restart Claude`,
22824
+ verbose: healEvidence
22825
+ });
22826
+ restartPending = true;
22827
+ } else {
22828
+ const plugin = checkClaudePlugin(pluginProbe);
22829
+ checks.push(plugin);
22830
+ if (!plugin.ok) restartPending = true;
22831
+ }
22603
22832
  const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
22604
22833
  const cliReport = buildVersionLagReport(cliInput);
22605
22834
  if (apply && deps.updateCli && versionAutoUpdateAction(cliReport) === "npm") {
@@ -22826,6 +23055,9 @@ function mmiDoctorDeps() {
22826
23055
  releasedVersion: fetchNpmReleasedVersion,
22827
23056
  // #3272: the --apply self-heal for a stale running CLI — npm global, shadows any plugin shim (#2879).
22828
23057
  updateCli: npmSelfUpdateCli,
23058
+ // #3282: the --apply self-heal for a stale/unresolved Claude plugin — the same marketplace reinstall
23059
+ // `mmi-cli plugin heal` drives. Takes effect on the next Claude reload, so the row asks for a restart.
23060
+ healPlugin: () => healClaudePluginForDoctor(),
22829
23061
  currentCliVersion: resolveClientVersion,
22830
23062
  readGitignore,
22831
23063
  writeGitignore,
@@ -22926,8 +23158,57 @@ function argvWantsJson2() {
22926
23158
  return process.argv.some((a) => a === "--json" || a.startsWith("--json="));
22927
23159
  }
22928
23160
  var unknownFlagJsonHandled = false;
23161
+ var PARSE_HINT_SENTINEL = "@@mmi-parse-hint@@";
23162
+ var DISCOVERY_HINT = "run `mmi-cli commands` for the agentic-coding map, `mmi-cli explain <command>` for detail, or `mmi-cli commands --json --primary` for machine discovery";
23163
+ var STALE_HINT = `(${DISCOVERY_HINT}). If you expected this command to exist, your installed mmi-cli may be stale \u2014 update it (see: mmi-cli doctor)`;
23164
+ var lastParseErrorKind = "other";
23165
+ function classifyParseError(plain) {
23166
+ if (/unknown command/i.test(plain)) return "unknown-command";
23167
+ if (/unknown option|missing required argument|too many arguments|argument missing/i.test(plain)) {
23168
+ return "bad-arguments";
23169
+ }
23170
+ return "other";
23171
+ }
23172
+ function resolveCommandFromArgv(root, argv) {
23173
+ let current = root;
23174
+ for (const token of argv) {
23175
+ if (token.startsWith("-")) break;
23176
+ const next = current.commands.find(
23177
+ (c) => c.name() === token || c.aliases().includes(token)
23178
+ );
23179
+ if (!next) break;
23180
+ current = next;
23181
+ }
23182
+ return current === root ? void 0 : current;
23183
+ }
23184
+ function commandPath2(cmd) {
23185
+ const parts = [];
23186
+ let node = cmd;
23187
+ while (node && node.parent) {
23188
+ parts.unshift(node.name());
23189
+ node = node.parent;
23190
+ }
23191
+ return parts.join(" ");
23192
+ }
23193
+ function resolveParseHint() {
23194
+ if (lastParseErrorKind !== "bad-arguments") return STALE_HINT;
23195
+ const cmd = resolveCommandFromArgv(program2, process.argv.slice(2));
23196
+ if (!cmd) return `(${DISCOVERY_HINT})`;
23197
+ const path2 = commandPath2(cmd);
23198
+ return [
23199
+ `Usage: mmi-cli ${path2} ${cmd.usage()}`,
23200
+ "This command exists and your mmi-cli is not the problem \u2014 the ARGUMENTS did not parse.",
23201
+ `Run \`mmi-cli ${path2} --help\` for its signature, \`mmi-cli explain ${path2}\` for detail, or \`mmi-cli commands\` for the full map.`
23202
+ ].join("\n");
23203
+ }
22929
23204
  function envelopeAwareWriteErr(str) {
22930
23205
  const plain = str.replace(/\[[0-9;]*m/g, "");
23206
+ if (plain.includes(PARSE_HINT_SENTINEL)) {
23207
+ if (unknownFlagJsonHandled && argvWantsJson2()) return;
23208
+ process.stderr.write(str.replace(PARSE_HINT_SENTINEL, resolveParseHint()));
23209
+ return;
23210
+ }
23211
+ lastParseErrorKind = classifyParseError(plain);
22931
23212
  const match = /unknown option '([^']+)'/.exec(plain);
22932
23213
  if (match) {
22933
23214
  const flag = match[1];
@@ -22952,7 +23233,7 @@ function envelopeAwareWriteErr(str) {
22952
23233
  process.stderr.write(str);
22953
23234
  }
22954
23235
  var program2 = new Command();
22955
- program2.name("mmi-cli").description("MMI Future Hub CLI \u2014 the org control plane for agentic coding.").version(resolveClientVersion()).configureOutput({ writeErr: envelopeAwareWriteErr }).showHelpAfterError("(run `mmi-cli commands` for the agentic-coding map, `mmi-cli explain <command>` for detail, or `mmi-cli commands --json --primary` for machine discovery). If you expected this command to exist, your installed mmi-cli may be stale \u2014 update it (see: mmi-cli doctor)");
23236
+ program2.name("mmi-cli").description("MMI Future Hub CLI \u2014 the org control plane for agentic coding.").version(resolveClientVersion()).configureOutput({ writeErr: envelopeAwareWriteErr }).showHelpAfterError(PARSE_HINT_SENTINEL);
22956
23237
  function appActorDeps() {
22957
23238
  return {
22958
23239
  fetchSecret: async (key) => fetchSecretValue(makeSecretsDeps(await loadConfig()), key, { repo: APP_VAULT_REPO }),
@@ -24420,7 +24701,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
24420
24701
  }
24421
24702
  if (result.status === "failed" || result.cleanupError) process.exitCode = 1;
24422
24703
  });
24423
- pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch; on no-ci repos run pr ci-policy / checks-wait first (#1432)").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)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").option("--preserve-worktree", "keep the local PR worktree/stage/branch for an active multi-issue batch (#1888)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012)").action(async (number, o) => {
24704
+ pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch; on no-ci repos run pr ci-policy / checks-wait first (#1432)").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)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m)`).option("--preserve-worktree", "keep the local PR worktree/stage/branch for an active multi-issue batch (#1888)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012)").action(async (number, o) => {
24424
24705
  const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
24425
24706
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
24426
24707
  const [headRef, baseRef, headRefOid] = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName,baseRefName,headRefOid", "--jq", '.headRefName + " " + .baseRefName + " " + (.headRefOid // "")'], { timeout: GC_GH_TIMEOUT_MS2 })).stdout.trim().split(/\s+/);
@@ -24431,6 +24712,25 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
24431
24712
  (await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
24432
24713
  );
24433
24714
  const ciPolicy = await resolveMergeCiPolicyForCheckout(o.repo);
24715
+ if (o.wait) {
24716
+ const repo = await requireRepo(o.repo);
24717
+ const baseBranch = await fetchRestPrSnapshot(number, repo).then((s) => s.baseRef).catch(() => "development");
24718
+ const wait = await waitForPrChecks({
24719
+ resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
24720
+ pollChecks: () => pollRestPrChecks(number, repo),
24721
+ pollMergeable: () => pollRestPrMergeable(number, repo),
24722
+ pollRateLimit: () => fetchRestCorePool(),
24723
+ baseBranch,
24724
+ sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
24725
+ log: (message) => console.warn(message),
24726
+ progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr merge: --wait checks \u2014 ${state}, ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
24727
+ });
24728
+ if (wait.status !== "success" && wait.status !== "skipped") {
24729
+ console.warn(`pr merge: --wait stopped before merge \u2014 ${wait.status}${wait.reason ? `: ${wait.reason}` : ""}${wait.detail ? ` (${wait.detail})` : ""}`);
24730
+ process.exitCode = wait.status === "timeout" || wait.status === "rate-limited" ? PR_CHECKS_TIMEOUT_EXIT_CODE : 1;
24731
+ return;
24732
+ }
24733
+ }
24434
24734
  if (ciPolicy.policy === "no-ci") {
24435
24735
  const guard = decidePrMergeNoCiGuard(await pollGhPrChecks(number, repoArgs), ciPolicy.reason);
24436
24736
  if (guard.action === "refuse") throw new Error(`gh pr merge ${number}: ${guard.message}`);
@@ -24481,9 +24781,11 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
24481
24781
  if (o.auto || upgradedToAuto) {
24482
24782
  const state = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS2 }).catch(() => ({ stdout: "" }))).stdout.trim();
24483
24783
  if (state !== "MERGED") {
24484
- console.log(JSON.stringify({ mergeStatus: "auto-merge-enqueued", pr: number, branch: headRef, state: state || "unknown", upgradedToAuto: upgradedToAuto || void 0 }));
24784
+ const enqueued = describePrMergeEnqueuedReason(await pollGhPrChecks(number, repoArgs).catch(() => void 0));
24785
+ console.log(JSON.stringify({ mergeStatus: "auto-merge-enqueued", enqueuedReason: enqueued.reason, pr: number, branch: headRef, state: state || "unknown", upgradedToAuto: upgradedToAuto || void 0 }));
24786
+ console.warn(`pr merge: PR #${number} is ENQUEUED, not merged \u2014 ${enqueued.message}.`);
24485
24787
  if (upgradedToAuto && !o.auto) {
24486
- console.warn(`pr merge: PR #${number} is ENQUEUED, not merged \u2014 auto-merge will land it once required checks pass. Exiting ${PR_MERGE_ENQUEUED_EXIT_CODE} so a chained command does not treat this as a completed merge.`);
24788
+ console.warn(`pr merge: exiting ${PR_MERGE_ENQUEUED_EXIT_CODE} so a chained command does not treat this as a completed merge.`);
24487
24789
  process.exitCode = PR_MERGE_ENQUEUED_EXIT_CODE;
24488
24790
  }
24489
24791
  return;
@@ -25025,6 +25327,7 @@ program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGr
25025
25327
  0 && (module.exports = {
25026
25328
  DEFAULT_PRIORITY,
25027
25329
  awsCallerArn,
25330
+ classifyParseError,
25028
25331
  gcPlan,
25029
25332
  isOrgRegisteredRepo,
25030
25333
  registryClientDeps,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.38.0",
3
+ "version": "3.40.0",
4
4
  "description": "MMI Future CLI — the org dev toolbox (board, registry, keyless secrets, release train, bootstrap, doctor) and the cross-IDE engine the plugin's session-start hook drives.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",