@mutmutco/cli 3.37.0 → 3.39.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 +416 -63
  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) {
@@ -7330,7 +7395,7 @@ async function loadConfigForBoardSelector(selector, repoOption) {
7330
7395
  if (!floor.sagaApiUrl) return stripMutableBoardConfig(floor);
7331
7396
  const slug = targetRepo2 ? slugOf(targetRepo2) : await repoSlug();
7332
7397
  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`);
7398
+ if (!read.ok) throw registryDegradeError(read.error);
7334
7399
  return read.project ? boardConfigFromProject(read.project, floor) : stripMutableBoardConfig(floor);
7335
7400
  }
7336
7401
 
@@ -8878,6 +8943,7 @@ function commandLadderHint() {
8878
8943
  lines.push(`${e.gh.padEnd(pad)} \u2192 ${e.replacement}`);
8879
8944
  }
8880
8945
  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`.");
8946
+ 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
8947
  return lines.join("\n");
8882
8948
  }
8883
8949
 
@@ -8903,6 +8969,40 @@ function resolveMergeCiPolicy(input) {
8903
8969
  }
8904
8970
  return { policy: "wait-for-checks", reason: ciWorkflows.join(", ") };
8905
8971
  }
8972
+ function describePrMergeEnqueuedReason(checks) {
8973
+ switch (checks) {
8974
+ case "pending":
8975
+ return {
8976
+ reason: "checks-pending",
8977
+ message: "checks are still pending; auto-merge will land once they settle, so retry later is appropriate"
8978
+ };
8979
+ case "failure":
8980
+ return {
8981
+ reason: "checks-failing",
8982
+ message: "GitHub reports a red check; auto-merge will not land until the required checks pass, so retrying now is pointless"
8983
+ };
8984
+ case "failing":
8985
+ return {
8986
+ reason: "checks-failing",
8987
+ message: "GitHub reports a red check while others are still running; wait for checks to settle before retrying"
8988
+ };
8989
+ case "no-checks-reported":
8990
+ return {
8991
+ reason: "no-checks-reported",
8992
+ message: "GitHub reports no checks; auto-merge remains queued until the base-branch policy permits the merge"
8993
+ };
8994
+ case "success":
8995
+ return {
8996
+ reason: "checks-passing",
8997
+ message: "reported checks are passing, but the base-branch policy or merge queue has not completed the merge yet"
8998
+ };
8999
+ default:
9000
+ return {
9001
+ reason: "checks-unavailable",
9002
+ message: "the current check verdict could not be read; auto-merge remains queued, so inspect checks before retrying"
9003
+ };
9004
+ }
9005
+ }
8906
9006
  function parseGhPrChecksOutput(stdout) {
8907
9007
  const text = stdout.trim();
8908
9008
  if (!text) return "no-checks-reported";
@@ -11236,6 +11336,17 @@ function buildIssueArgs({ type, title, body, priority, repo, labels }) {
11236
11336
  for (const label of labels ?? []) args.push("--label", label);
11237
11337
  return args;
11238
11338
  }
11339
+ async function ensureLabelsExist(labels, repo, deps = {}) {
11340
+ const run = deps.run ?? execFileP2;
11341
+ for (const label of new Set(labels)) {
11342
+ const args = ["label", "create", label, "--color", "ededed"];
11343
+ if (repo) args.push("--repo", repo);
11344
+ try {
11345
+ await run("gh", args, { timeout: GH_MUTATION_TIMEOUT_MS });
11346
+ } catch {
11347
+ }
11348
+ }
11349
+ }
11239
11350
  async function bodyArgsViaFile(args, deps = {}) {
11240
11351
  const i = args.indexOf("--body");
11241
11352
  if (i === -1 || i + 1 >= args.length) return { args, cleanup: async () => {
@@ -11306,14 +11417,27 @@ function buildPrArgs({ title, body, base, head, repo, draft }) {
11306
11417
  if (draft) args.push("--draft");
11307
11418
  return args;
11308
11419
  }
11420
+ function parseExistingPr(stderr) {
11421
+ if (!/already exists/i.test(stderr)) return void 0;
11422
+ const match = /https:\/\/\S*\/pull\/(\d+)/.exec(stderr);
11423
+ if (!match) return void 0;
11424
+ return { number: Number(match[1]), url: match[0] };
11425
+ }
11309
11426
  async function ghCreate(args) {
11310
11427
  const swapped = await bodyArgsViaFile(args);
11311
11428
  try {
11312
11429
  const { stdout } = await execFileP2("gh", swapped.args, { timeout: GH_MUTATION_TIMEOUT_MS });
11313
11430
  return parseCreatedUrl(stdout);
11314
11431
  } catch (e) {
11315
- await swapped.cleanup();
11316
11432
  const err = e;
11433
+ const existing = parseExistingPr(`${err.stderr ?? ""}
11434
+ ${err.message ?? ""}`);
11435
+ if (existing) {
11436
+ await swapped.cleanup();
11437
+ console.warn(`${args[0]} create: a PR already exists for this branch \u2014 #${existing.number} (nothing to do)`);
11438
+ return { ...existing, existing: true };
11439
+ }
11440
+ await swapped.cleanup();
11317
11441
  const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
11318
11442
  fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
11319
11443
  } finally {
@@ -11961,6 +12085,10 @@ function parseNpmVersion(stdout) {
11961
12085
  function staleTrainCliMessage(report, commandName) {
11962
12086
  return `running mmi-cli ${report.currentVersion} is stale against released ${report.releasedVersion}; this is recoverable \u2014 run \`npm install -g @mutmutco/cli@latest\` to update to ${report.releasedVersion} (a standalone CLI shadows a plugin-provided mmi-cli on PATH and needs no Claude restart), then rerun ${commandName} so it uses the current train path. This gate runs before any train mutation, so this invocation changed nothing; any partial train state (local merge, pushed tag) is from an earlier run and the rerun resumes it safely`;
11963
12087
  }
12088
+ function versionAutoUpdateAction(report, releasedSource) {
12089
+ if (report.ok || report.staleAgainst !== "released") return "none";
12090
+ return releasedSource === "gh" ? "npm-unreachable" : "npm";
12091
+ }
11964
12092
 
11965
12093
  // src/plugin-cache-prune.ts
11966
12094
  var PLUGIN_CACHE_KEEP = 2;
@@ -16401,14 +16529,31 @@ function registerSecretsCommands(program3) {
16401
16529
  });
16402
16530
  if (!report.ok) process.exitCode = 1;
16403
16531
  });
16404
- 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) => {
16532
+ 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) => {
16533
+ if (o.show) {
16534
+ if (o.file || o.replace || o.remove?.length) return fail("secrets org-catalog: --show does not combine with --file/--replace/--remove");
16535
+ if (!await secretsOrgCatalogShow(d, { json: o.json })) process.exitCode = 1;
16536
+ return;
16537
+ }
16538
+ if (o.replace) {
16539
+ if (!o.file) return fail("secrets org-catalog: --replace needs --file (the complete catalog to write)");
16540
+ if (!o.yes) {
16541
+ 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.");
16542
+ }
16543
+ }
16544
+ if (!o.file && !o.remove?.length) {
16545
+ return fail("secrets org-catalog: nothing to do \u2014 pass --show, --file <path> (merged by default), or --remove <provider/KEY>");
16546
+ }
16405
16547
  let body;
16406
- try {
16407
- body = (0, import_node_fs18.readFileSync)((0, import_node_path16.resolve)(o.file), "utf8");
16408
- } catch (e) {
16409
- return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
16548
+ if (o.file) {
16549
+ try {
16550
+ body = (0, import_node_fs18.readFileSync)((0, import_node_path16.resolve)(o.file), "utf8");
16551
+ } catch (e) {
16552
+ return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
16553
+ }
16410
16554
  }
16411
- if (!await secretsOrgCatalogSet(d, body)) process.exitCode = 1;
16555
+ const ok = body !== void 0 ? await secretsOrgCatalogSet(d, body, { replace: o.replace, remove: o.remove }) : await secretsOrgCatalogRemove(d, o.remove ?? []);
16556
+ if (!ok) process.exitCode = 1;
16412
16557
  }));
16413
16558
  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) => {
16414
16559
  if (!["dev", "rc", "main"].includes(o.stage)) {
@@ -16481,7 +16626,7 @@ function registerSecretsCommands(program3) {
16481
16626
  const ok = await secretsSet(d, key, o);
16482
16627
  if (!ok) process.exitCode = 1;
16483
16628
  });
16484
- 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);
16629
+ 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);
16485
16630
  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) => {
16486
16631
  const stages = ["dev", "rc", "main"];
16487
16632
  if (!stages.includes(o.from) || !stages.includes(o.to)) {
@@ -16627,7 +16772,7 @@ async function mintInstallationToken(deps) {
16627
16772
  }
16628
16773
  return { token: body.token, expiresAt: body.expires_at };
16629
16774
  }
16630
- async function activateAppActor(commandPath2, env, mint) {
16775
+ async function activateAppActor(commandPath3, env, mint) {
16631
16776
  const requested = (env[APP_ACTOR_ENV] ?? "").trim();
16632
16777
  if (!requested) return "personal";
16633
16778
  if (requested !== "app") {
@@ -16635,8 +16780,8 @@ async function activateAppActor(commandPath2, env, mint) {
16635
16780
  }
16636
16781
  delete env.GH_TOKEN;
16637
16782
  delete env.GITHUB_TOKEN;
16638
- if (!APP_ELIGIBLE_COMMANDS.has(commandPath2)) {
16639
- 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`);
16783
+ if (!APP_ELIGIBLE_COMMANDS.has(commandPath3)) {
16784
+ 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`);
16640
16785
  }
16641
16786
  const minted = await mint();
16642
16787
  env.GH_TOKEN = minted.token;
@@ -16960,9 +17105,17 @@ function awsScheduleEntry(payload) {
16960
17105
  if (typeof Name !== "string" || typeof ScheduleExpression !== "string") return null;
16961
17106
  if (State !== "ENABLED" || isJervResource(Name)) return null;
16962
17107
  let target = "";
17108
+ let scheduleId;
16963
17109
  if (Target && typeof Target === "object") {
16964
- const { Arn } = Target;
17110
+ const { Arn, Input } = Target;
16965
17111
  if (typeof Arn === "string") target = Arn.split(":").pop() ?? "";
17112
+ if (typeof Input === "string") {
17113
+ try {
17114
+ const parsed = JSON.parse(Input);
17115
+ if (typeof parsed?.scheduleId === "string" && parsed.scheduleId) scheduleId = parsed.scheduleId;
17116
+ } catch {
17117
+ }
17118
+ }
16966
17119
  }
16967
17120
  return {
16968
17121
  name: Name,
@@ -16970,7 +17123,8 @@ function awsScheduleEntry(payload) {
16970
17123
  executor: target ? `aws-scheduler \u2192 ${target}` : "aws-scheduler",
16971
17124
  llm: readLlmDeclaration(typeof Description === "string" ? Description : void 0),
16972
17125
  resolved: "live",
16973
- source: `aws scheduler schedule ${Name} (eu-central-1)`
17126
+ source: `aws scheduler schedule ${Name} (eu-central-1)`,
17127
+ ...scheduleId ? { scheduleId } : {}
16974
17128
  };
16975
17129
  }
16976
17130
  function awsScheduleRefs(payload) {
@@ -17138,7 +17292,28 @@ function unlauncheredLlmDrifts(entries) {
17138
17292
  }
17139
17293
  return drifts.sort((a, b) => a.name.localeCompare(b.name));
17140
17294
  }
17141
- function assembleReconciliation(githubEntries2, readRepos, registry2, activeWorkflowNames = /* @__PURE__ */ new Set()) {
17295
+ var HARBOUR_ARM_GRACE_MS = 30 * 60 * 1e3;
17296
+ function registeredButUnarmedDrifts(registry2, liveEntries, opts) {
17297
+ if (!opts.schedulerRead) return [];
17298
+ const now = opts.now ?? Date.now();
17299
+ const armed = new Set(liveEntries.map((e) => e.scheduleId).filter((id) => Boolean(id)));
17300
+ const drifts = [];
17301
+ for (const row of registry2) {
17302
+ if (row.executor === "github-actions") continue;
17303
+ if (armed.has(row.id)) continue;
17304
+ const stamped = row.updatedAt ? Date.parse(row.updatedAt) : NaN;
17305
+ if (Number.isFinite(stamped) && now - stamped < HARBOUR_ARM_GRACE_MS) continue;
17306
+ drifts.push({
17307
+ class: "registered-but-unarmed",
17308
+ name: row.id,
17309
+ executor: row.executor,
17310
+ detail: "registered as a harbour lane but no aws-scheduler clock dispatches it \u2014 it will never fire",
17311
+ remedy: "check the fleet-clock reconciler tick; re-run `mmi-cli org schedules register` for the repo and verify with `mmi-cli org schedules`"
17312
+ });
17313
+ }
17314
+ return drifts.sort((a, b) => a.name.localeCompare(b.name));
17315
+ }
17316
+ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWorkflowNames = /* @__PURE__ */ new Set(), harbour = {}) {
17142
17317
  if (registry2 === null) {
17143
17318
  return {
17144
17319
  reconciliation: [],
@@ -17147,7 +17322,13 @@ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWork
17147
17322
  };
17148
17323
  }
17149
17324
  const live = githubEntries2.filter((e) => e.executor === "github-actions");
17150
- const drifts = reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames);
17325
+ const drifts = [
17326
+ ...reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames),
17327
+ ...registeredButUnarmedDrifts(registry2, harbour.awsEntries ?? [], {
17328
+ schedulerRead: Boolean(harbour.schedulerRead),
17329
+ now: harbour.now
17330
+ })
17331
+ ];
17151
17332
  return { reconciliation: drifts, driftLines: drifts.map(renderDrift), incomplete: [] };
17152
17333
  }
17153
17334
 
@@ -17260,6 +17441,7 @@ async function awsEntries() {
17260
17441
  } catch (e) {
17261
17442
  incomplete.push(`aws: events list-rules failed \u2014 ${e.message}`);
17262
17443
  }
17444
+ let schedulerRead = false;
17263
17445
  try {
17264
17446
  const refs = awsScheduleRefs(await awsJson(["scheduler", "list-schedules"]));
17265
17447
  for (const ref of refs) {
@@ -17267,11 +17449,12 @@ async function awsEntries() {
17267
17449
  const entry = awsScheduleEntry(await awsJson(args));
17268
17450
  if (entry) entries.push(entry);
17269
17451
  }
17452
+ schedulerRead = true;
17270
17453
  } catch (e) {
17271
17454
  incomplete.push(`aws: scheduler listing failed \u2014 ${e.message}`);
17272
17455
  }
17273
17456
  const reconciliation = unlauncheredLlmDrifts(entries);
17274
- return { entries, incomplete, drift: reconciliation.map(renderDrift), reconciliation };
17457
+ return { entries, incomplete, drift: reconciliation.map(renderDrift), reconciliation, schedulerRead };
17275
17458
  }
17276
17459
  async function defaultRegistryRead() {
17277
17460
  return fetchSchedulesList(registryClientDeps(await loadConfig()));
@@ -17283,7 +17466,11 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
17283
17466
  readRegistry().catch(() => null),
17284
17467
  readProjects().catch(() => null)
17285
17468
  ]);
17286
- const recon = assembleReconciliation(gh.entries, new Set(gh.readRepos), registry2, new Set(gh.workflowNames));
17469
+ const recon = assembleReconciliation(gh.entries, new Set(gh.readRepos), registry2, new Set(gh.workflowNames), {
17470
+ // #3286: the harbour side joins registry rows against the live aws-scheduler clocks by scheduleId.
17471
+ awsEntries: aws.entries,
17472
+ schedulerRead: Boolean(aws.schedulerRead)
17473
+ });
17287
17474
  const selfManaged = /* @__PURE__ */ new Set();
17288
17475
  for (const proj of projects ?? []) {
17289
17476
  if (proj?.schedulesMode !== "self-managed") continue;
@@ -17526,17 +17713,33 @@ async function runSchedulesLift(opts, deps = {}) {
17526
17713
  }
17527
17714
  return { repo, records, payload, response };
17528
17715
  }
17716
+ var FLEET_CLOCK_RECONCILE_WINDOW = "~15 minutes";
17717
+ 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}).
17718
+ Verify with \`mmi-cli org schedules\` after that window; an empty result before it means "not reconciled yet", not "will never fire".`;
17719
+ var ARMING_REPORT = {
17720
+ armed: false,
17721
+ state: "pending-reconcile",
17722
+ withinMinutes: 15,
17723
+ verifyWith: "mmi-cli org schedules",
17724
+ report: "registered successfully \u2014 arms on the next fleet-clock tick, not yet",
17725
+ 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.`
17726
+ };
17727
+ function withArmingReport(body) {
17728
+ if (body === null || typeof body !== "object" || Array.isArray(body)) return body;
17729
+ return { ...body, arming: ARMING_REPORT };
17730
+ }
17529
17731
  function registerSchedulesLiftCommand(program3, deps = {}) {
17530
17732
  const schedules = program3.commands.find((c) => c.name() === "schedules");
17531
17733
  if (!schedules) throw new Error("schedules lift: registerSchedulesCommands must run first \u2014 the lift attaches to the `schedules` command");
17532
- 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) => {
17734
+ 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) => {
17533
17735
  try {
17534
17736
  const result = await runSchedulesLift({ repo: o.repo, dir: o.dir, dryRun: Boolean(o.dryRun) }, deps);
17535
17737
  if (o.dryRun) {
17536
17738
  console.log(JSON.stringify(result.payload, null, 2));
17537
17739
  return;
17538
17740
  }
17539
- console.log(JSON.stringify(result.response?.body));
17741
+ console.log(JSON.stringify(withArmingReport(result.response?.body)));
17742
+ console.error(ARMING_IS_EVENTUAL_NOTE);
17540
17743
  } catch (e) {
17541
17744
  const err = e;
17542
17745
  if (err instanceof RegistryUnreachableError) {
@@ -19271,9 +19474,6 @@ function registerStageCommands(program3) {
19271
19474
 
19272
19475
  // src/config-load.ts
19273
19476
  var discoveredConfig = null;
19274
- function registryDegradeError(error) {
19275
- 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`);
19276
- }
19277
19477
  async function loadConfigOrDiscover() {
19278
19478
  if (discoveredConfig) return discoveredConfig;
19279
19479
  const floor = await loadConfig();
@@ -20721,6 +20921,7 @@ function validateBatchSpecs(specs) {
20721
20921
  return { ok: errors.length === 0, errors, validated };
20722
20922
  }
20723
20923
  async function createIssuesBatch(specs, options, deps = {}) {
20924
+ const ensureLabels = deps.ensureLabels ?? ensureLabelsExist;
20724
20925
  const client = deps.client ?? defaultGitHubClient();
20725
20926
  const validation = validateBatchSpecs(specs);
20726
20927
  if (!validation.ok) {
@@ -20733,6 +20934,14 @@ ${lines}`);
20733
20934
  throw new Error("could not resolve repo \u2014 pass --repo owner/repo or set repo per row");
20734
20935
  }
20735
20936
  const rowRepo = (spec) => spec.repo ?? defaultRepo;
20937
+ const labelsByRepo = /* @__PURE__ */ new Map();
20938
+ for (const { spec } of validation.validated) {
20939
+ if (!spec.labels?.length) continue;
20940
+ const bucket = labelsByRepo.get(rowRepo(spec)) ?? /* @__PURE__ */ new Set();
20941
+ for (const label of spec.labels) bucket.add(label);
20942
+ labelsByRepo.set(rowRepo(spec), bucket);
20943
+ }
20944
+ for (const [repo, labels] of labelsByRepo) await ensureLabels([...labels], repo);
20736
20945
  const created = [];
20737
20946
  const failures = [];
20738
20947
  for (const entry of validation.validated) {
@@ -21140,6 +21349,15 @@ async function fetchNpmReleasedVersion() {
21140
21349
  return void 0;
21141
21350
  }
21142
21351
  }
21352
+ var NPM_INSTALL_TIMEOUT_MS = 12e4;
21353
+ async function npmSelfUpdateCli() {
21354
+ try {
21355
+ await runHostBin("npm", ["install", "-g", "@mutmutco/cli@latest"], { timeout: NPM_INSTALL_TIMEOUT_MS });
21356
+ return { ok: true, detail: "npm install -g @mutmutco/cli@latest exited 0" };
21357
+ } catch (e) {
21358
+ return { ok: false, detail: e.message.trim().slice(0, 200).replace(/\s+/g, " ") };
21359
+ }
21360
+ }
21143
21361
  function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRepo = false) {
21144
21362
  const homeDir = surface === "codex" ? ".codex" : ".claude";
21145
21363
  const installed = readInstalledPlugins(surface);
@@ -21198,6 +21416,17 @@ async function applyPluginHeal(surface, log, opts) {
21198
21416
  }
21199
21417
  return true;
21200
21418
  }
21419
+ async function healClaudePluginForDoctor(surface = detectSurface(process.env)) {
21420
+ if (surfaceToken(surface) !== "claude") {
21421
+ return { ok: false, detail: `not a Claude surface (${surface}) \u2014 no Hub-shipped plugin to reinstall` };
21422
+ }
21423
+ const steps = [];
21424
+ const ok = await applyPluginHeal(surface, (msg) => steps.push(msg.trim()));
21425
+ return {
21426
+ ok,
21427
+ detail: ok ? "marketplace remove \u2192 add \u2192 install succeeded" : `\`claude plugin\` reinstall failed or was skipped${steps.length ? ` (${steps[steps.length - 1]})` : ""}`
21428
+ };
21429
+ }
21201
21430
  async function runGuard(readOrigin) {
21202
21431
  try {
21203
21432
  const surface = detectSurface(process.env);
@@ -21733,9 +21962,9 @@ function formatExplainLoop(playbook) {
21733
21962
  }
21734
21963
  return lines.join("\n");
21735
21964
  }
21736
- function findCommandInManifest(manifest, commandPath2) {
21965
+ function findCommandInManifest(manifest, commandPath3) {
21737
21966
  const visit = (command) => {
21738
- if (command.path === commandPath2) return command;
21967
+ if (command.path === commandPath3) return command;
21739
21968
  for (const child2 of command.subcommands) {
21740
21969
  const found = visit(child2);
21741
21970
  if (found) return found;
@@ -21760,10 +21989,10 @@ function registerExplainCommand(program3) {
21760
21989
  console.log(formatManifestHuman(manifest));
21761
21990
  return;
21762
21991
  }
21763
- const commandPath2 = commandArgs.join(" ");
21764
- const command = findCommandInManifest(manifest, commandPath2);
21992
+ const commandPath3 = commandArgs.join(" ");
21993
+ const command = findCommandInManifest(manifest, commandPath3);
21765
21994
  if (!command) {
21766
- fail(`explain: unknown command "${commandPath2}"`, { code: ERROR_CODES.ERR_NOT_FOUND });
21995
+ fail(`explain: unknown command "${commandPath3}"`, { code: ERROR_CODES.ERR_NOT_FOUND });
21767
21996
  return;
21768
21997
  }
21769
21998
  console.log(command.subcommands.length ? formatExplainGroup(command, manifest.name) : formatExplainCommand(command, manifest.name));
@@ -22392,6 +22621,11 @@ function checkRepoWorktrees(probe) {
22392
22621
  ]
22393
22622
  };
22394
22623
  }
22624
+ function pluginHealTrigger(probe) {
22625
+ if (probe.guardState === "no-install" || probe.guardState === "unresolved") return "unresolved";
22626
+ const behind = Boolean(probe.installed && probe.released) && compareVersions(probe.installed, probe.released) < 0;
22627
+ return behind ? "behind" : null;
22628
+ }
22395
22629
  function checkClaudePlugin(probe) {
22396
22630
  const { installed, released, guardState } = probe;
22397
22631
  const evidence = [
@@ -22399,20 +22633,22 @@ function checkClaudePlugin(probe) {
22399
22633
  `released: ${released ?? "(not checked \u2014 offline or --fast)"}`,
22400
22634
  `resolvable: ${guardState}`
22401
22635
  ];
22402
- if (guardState === "no-install" || guardState === "unresolved") {
22636
+ const trigger = pluginHealTrigger(probe);
22637
+ if (trigger === "unresolved") {
22403
22638
  return {
22404
22639
  ok: false,
22405
22640
  label: guardState === "no-install" ? "Claude plugin \u2014 not installed" : "Claude plugin \u2014 unresolved (marketplace/cache missing)",
22406
- fix: "run `mmi-cli plugin heal` to reinstall the MMI marketplace + plugin, then restart Claude",
22641
+ fix: "run `mmi-cli doctor --apply` (or `mmi-cli plugin heal`) to reinstall the MMI marketplace + plugin, then restart Claude",
22407
22642
  verbose: evidence
22408
22643
  };
22409
22644
  }
22410
- const behind = Boolean(installed && released) && compareVersions(installed, released) < 0;
22411
- if (behind) {
22645
+ if (trigger === "behind") {
22412
22646
  return {
22413
22647
  ok: false,
22414
22648
  label: `Claude plugin ${installed} \u2192 ${released}`,
22415
- fix: "run /plugin, update MMI (reinstall if it shows a legacy row), then restart Claude",
22649
+ // Name the CLI verb that actually fixes it, like every other red row `/plugin` is the manual
22650
+ // fallback, not the first resort (#3282).
22651
+ fix: "run `mmi-cli doctor --apply` (or `mmi-cli plugin heal`) to reinstall it, then restart Claude",
22416
22652
  verbose: evidence
22417
22653
  };
22418
22654
  }
@@ -22564,10 +22800,53 @@ async function runDoctorClean(opts, io, deps) {
22564
22800
  checks.push({ ok: false, label: "gitignore block", fix: "run `mmi-cli doctor --apply` to write the org-managed .gitignore block", verbose: giEvidence });
22565
22801
  }
22566
22802
  }
22567
- const plugin = checkClaudePlugin({ installed, released, guardState: deps.pluginGuardState(isOrgRepo) });
22568
- checks.push(plugin);
22569
- if (!plugin.ok) restartPending = true;
22570
- checks.push(checkCliVersion({ currentVersion: deps.currentCliVersion(), releasedVersion: released }));
22803
+ const pluginProbe = { installed, released, guardState: deps.pluginGuardState(isOrgRepo) };
22804
+ const healTrigger = pluginHealTrigger(pluginProbe);
22805
+ if (apply && deps.healPlugin && healTrigger) {
22806
+ const heal = await deps.healPlugin();
22807
+ const label = healTrigger === "behind" ? `Claude plugin ${installed} \u2192 ${released}` : "Claude plugin \u2014 reinstalled";
22808
+ const healEvidence = [
22809
+ `installed: ${installed ?? "(none)"}`,
22810
+ `released: ${released ?? "(not checked \u2014 offline or --fast)"}`,
22811
+ `resolvable: ${pluginProbe.guardState}`,
22812
+ `heal: ${heal.detail}`
22813
+ ];
22814
+ checks.push(heal.ok ? {
22815
+ ok: true,
22816
+ label,
22817
+ detail: "reinstalled via the MMI marketplace (remove \u2192 add \u2192 install)",
22818
+ verbose: healEvidence
22819
+ } : {
22820
+ ok: false,
22821
+ label,
22822
+ fix: `auto-heal failed (${heal.detail}) \u2014 run \`mmi-cli plugin heal\`, then restart Claude`,
22823
+ verbose: healEvidence
22824
+ });
22825
+ restartPending = true;
22826
+ } else {
22827
+ const plugin = checkClaudePlugin(pluginProbe);
22828
+ checks.push(plugin);
22829
+ if (!plugin.ok) restartPending = true;
22830
+ }
22831
+ const cliInput = { currentVersion: deps.currentCliVersion(), releasedVersion: released };
22832
+ const cliReport = buildVersionLagReport(cliInput);
22833
+ if (apply && deps.updateCli && versionAutoUpdateAction(cliReport) === "npm") {
22834
+ const heal = await deps.updateCli();
22835
+ const healEvidence = [`running: ${cliReport.currentVersion}`, `published: ${cliReport.releasedVersion}`, `heal: ${heal.detail}`];
22836
+ checks.push(heal.ok ? {
22837
+ ok: true,
22838
+ label: `mmi-cli ${cliReport.currentVersion} \u2192 ${cliReport.releasedVersion}`,
22839
+ detail: "self-updated via npm install -g; the next mmi-cli invocation runs the new version",
22840
+ verbose: healEvidence
22841
+ } : {
22842
+ ok: false,
22843
+ label: `mmi-cli ${cliReport.currentVersion} \u2192 ${cliReport.releasedVersion}`,
22844
+ fix: `self-update failed (${heal.detail}) \u2014 run \`npm install -g @mutmutco/cli@latest\``,
22845
+ verbose: healEvidence
22846
+ });
22847
+ } else {
22848
+ checks.push(checkCliVersion(cliInput));
22849
+ }
22571
22850
  checks.push(checkPluginCache(deps.pluginCache()));
22572
22851
  if (!opts.fast && !opts.banner && !opts.preflight) {
22573
22852
  const probe = await deps.schedulesNotebook().catch((e) => ({
@@ -22773,6 +23052,11 @@ function mmiDoctorDeps() {
22773
23052
  installedPluginVersion: installedClaudePluginVersion,
22774
23053
  pluginGuardState: claudePluginGuardState,
22775
23054
  releasedVersion: fetchNpmReleasedVersion,
23055
+ // #3272: the --apply self-heal for a stale running CLI — npm global, shadows any plugin shim (#2879).
23056
+ updateCli: npmSelfUpdateCli,
23057
+ // #3282: the --apply self-heal for a stale/unresolved Claude plugin — the same marketplace reinstall
23058
+ // `mmi-cli plugin heal` drives. Takes effect on the next Claude reload, so the row asks for a restart.
23059
+ healPlugin: () => healClaudePluginForDoctor(),
22776
23060
  currentCliVersion: resolveClientVersion,
22777
23061
  readGitignore,
22778
23062
  writeGitignore,
@@ -22873,8 +23157,57 @@ function argvWantsJson2() {
22873
23157
  return process.argv.some((a) => a === "--json" || a.startsWith("--json="));
22874
23158
  }
22875
23159
  var unknownFlagJsonHandled = false;
23160
+ var PARSE_HINT_SENTINEL = "@@mmi-parse-hint@@";
23161
+ 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";
23162
+ 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)`;
23163
+ var lastParseErrorKind = "other";
23164
+ function classifyParseError(plain) {
23165
+ if (/unknown command/i.test(plain)) return "unknown-command";
23166
+ if (/unknown option|missing required argument|too many arguments|argument missing/i.test(plain)) {
23167
+ return "bad-arguments";
23168
+ }
23169
+ return "other";
23170
+ }
23171
+ function resolveCommandFromArgv(root, argv) {
23172
+ let current = root;
23173
+ for (const token of argv) {
23174
+ if (token.startsWith("-")) break;
23175
+ const next = current.commands.find(
23176
+ (c) => c.name() === token || c.aliases().includes(token)
23177
+ );
23178
+ if (!next) break;
23179
+ current = next;
23180
+ }
23181
+ return current === root ? void 0 : current;
23182
+ }
23183
+ function commandPath2(cmd) {
23184
+ const parts = [];
23185
+ let node = cmd;
23186
+ while (node && node.parent) {
23187
+ parts.unshift(node.name());
23188
+ node = node.parent;
23189
+ }
23190
+ return parts.join(" ");
23191
+ }
23192
+ function resolveParseHint() {
23193
+ if (lastParseErrorKind !== "bad-arguments") return STALE_HINT;
23194
+ const cmd = resolveCommandFromArgv(program2, process.argv.slice(2));
23195
+ if (!cmd) return `(${DISCOVERY_HINT})`;
23196
+ const path2 = commandPath2(cmd);
23197
+ return [
23198
+ `Usage: mmi-cli ${path2} ${cmd.usage()}`,
23199
+ "This command exists and your mmi-cli is not the problem \u2014 the ARGUMENTS did not parse.",
23200
+ `Run \`mmi-cli ${path2} --help\` for its signature, \`mmi-cli explain ${path2}\` for detail, or \`mmi-cli commands\` for the full map.`
23201
+ ].join("\n");
23202
+ }
22876
23203
  function envelopeAwareWriteErr(str) {
22877
23204
  const plain = str.replace(/\[[0-9;]*m/g, "");
23205
+ if (plain.includes(PARSE_HINT_SENTINEL)) {
23206
+ if (unknownFlagJsonHandled && argvWantsJson2()) return;
23207
+ process.stderr.write(str.replace(PARSE_HINT_SENTINEL, resolveParseHint()));
23208
+ return;
23209
+ }
23210
+ lastParseErrorKind = classifyParseError(plain);
22878
23211
  const match = /unknown option '([^']+)'/.exec(plain);
22879
23212
  if (match) {
22880
23213
  const flag = match[1];
@@ -22899,7 +23232,7 @@ function envelopeAwareWriteErr(str) {
22899
23232
  process.stderr.write(str);
22900
23233
  }
22901
23234
  var program2 = new Command();
22902
- 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)");
23235
+ 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);
22903
23236
  function appActorDeps() {
22904
23237
  return {
22905
23238
  fetchSecret: async (key) => fetchSecretValue(makeSecretsDeps(await loadConfig()), key, { repo: APP_VAULT_REPO }),
@@ -23858,6 +24191,7 @@ withExamples(mutating(
23858
24191
  let title;
23859
24192
  let issueType;
23860
24193
  let extraLabels = [];
24194
+ let targetRepo2;
23861
24195
  try {
23862
24196
  issueType = resolveCreateType(o.type, "issue create");
23863
24197
  title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
@@ -23865,33 +24199,30 @@ withExamples(mutating(
23865
24199
  if (o.idempotencyKey) body = appendIdempotencyMarker(body, o.idempotencyKey);
23866
24200
  priority = resolveCreatePriority(o.priority, "issue create");
23867
24201
  extraLabels = [...o.label ?? []];
24202
+ targetRepo2 = await resolveRepo(o.repo);
24203
+ if (!targetRepo2) {
24204
+ return fail("issue create: could not resolve the target repo \u2014 run inside a git checkout or pass --repo <owner/repo>");
24205
+ }
23868
24206
  args = buildIssueArgs({
23869
24207
  type: issueType,
23870
24208
  title,
23871
24209
  body,
23872
24210
  priority,
23873
- repo: o.repo,
24211
+ repo: targetRepo2,
23874
24212
  labels: extraLabels.length ? extraLabels : void 0
23875
24213
  });
23876
24214
  if (o.parent !== void 0) parseIssueRef(o.parent);
23877
24215
  } catch (e) {
23878
24216
  return fail(`issue create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
23879
24217
  }
23880
- for (const label of extraLabels) {
23881
- const la = ["label", "create", label, "--color", "ededed"];
23882
- if (o.repo) la.push("--repo", o.repo);
23883
- try {
23884
- await execFileP2("gh", la, { timeout: GH_MUTATION_TIMEOUT_MS });
23885
- } catch {
23886
- }
23887
- }
24218
+ await ensureLabelsExist(extraLabels, targetRepo2);
23888
24219
  const created = await ghCreate(args);
23889
- const { projectItemId, onBoard } = await attachToProject(created.number, o.repo, priority);
24220
+ const { projectItemId, onBoard } = await attachToProject(created.number, targetRepo2, priority);
23890
24221
  let parent;
23891
24222
  let parentLinkError;
23892
24223
  if (o.parent !== void 0) {
23893
24224
  try {
23894
- parent = await linkSubIssue(ghRunner2, o.parent, created.url, o.repo);
24225
+ parent = await linkSubIssue(ghRunner2, o.parent, created.url, targetRepo2);
23895
24226
  } catch (e) {
23896
24227
  const err = e;
23897
24228
  parentLinkError = (err.stderr || err.message || String(e)).trim();
@@ -24369,7 +24700,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
24369
24700
  }
24370
24701
  if (result.status === "failed" || result.cleanupError) process.exitCode = 1;
24371
24702
  });
24372
- 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) => {
24703
+ 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) => {
24373
24704
  const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
24374
24705
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
24375
24706
  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+/);
@@ -24380,6 +24711,25 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
24380
24711
  (await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
24381
24712
  );
24382
24713
  const ciPolicy = await resolveMergeCiPolicyForCheckout(o.repo);
24714
+ if (o.wait) {
24715
+ const repo = await requireRepo(o.repo);
24716
+ const baseBranch = await fetchRestPrSnapshot(number, repo).then((s) => s.baseRef).catch(() => "development");
24717
+ const wait = await waitForPrChecks({
24718
+ resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
24719
+ pollChecks: () => pollRestPrChecks(number, repo),
24720
+ pollMergeable: () => pollRestPrMergeable(number, repo),
24721
+ pollRateLimit: () => fetchRestCorePool(),
24722
+ baseBranch,
24723
+ sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
24724
+ log: (message) => console.warn(message),
24725
+ progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr merge: --wait checks \u2014 ${state}, ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
24726
+ });
24727
+ if (wait.status !== "success" && wait.status !== "skipped") {
24728
+ console.warn(`pr merge: --wait stopped before merge \u2014 ${wait.status}${wait.reason ? `: ${wait.reason}` : ""}${wait.detail ? ` (${wait.detail})` : ""}`);
24729
+ process.exitCode = wait.status === "timeout" || wait.status === "rate-limited" ? PR_CHECKS_TIMEOUT_EXIT_CODE : 1;
24730
+ return;
24731
+ }
24732
+ }
24383
24733
  if (ciPolicy.policy === "no-ci") {
24384
24734
  const guard = decidePrMergeNoCiGuard(await pollGhPrChecks(number, repoArgs), ciPolicy.reason);
24385
24735
  if (guard.action === "refuse") throw new Error(`gh pr merge ${number}: ${guard.message}`);
@@ -24430,9 +24780,11 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
24430
24780
  if (o.auto || upgradedToAuto) {
24431
24781
  const state = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS2 }).catch(() => ({ stdout: "" }))).stdout.trim();
24432
24782
  if (state !== "MERGED") {
24433
- console.log(JSON.stringify({ mergeStatus: "auto-merge-enqueued", pr: number, branch: headRef, state: state || "unknown", upgradedToAuto: upgradedToAuto || void 0 }));
24783
+ const enqueued = describePrMergeEnqueuedReason(await pollGhPrChecks(number, repoArgs).catch(() => void 0));
24784
+ console.log(JSON.stringify({ mergeStatus: "auto-merge-enqueued", enqueuedReason: enqueued.reason, pr: number, branch: headRef, state: state || "unknown", upgradedToAuto: upgradedToAuto || void 0 }));
24785
+ console.warn(`pr merge: PR #${number} is ENQUEUED, not merged \u2014 ${enqueued.message}.`);
24434
24786
  if (upgradedToAuto && !o.auto) {
24435
- 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.`);
24787
+ console.warn(`pr merge: exiting ${PR_MERGE_ENQUEUED_EXIT_CODE} so a chained command does not treat this as a completed merge.`);
24436
24788
  process.exitCode = PR_MERGE_ENQUEUED_EXIT_CODE;
24437
24789
  }
24438
24790
  return;
@@ -24974,6 +25326,7 @@ program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGr
24974
25326
  0 && (module.exports = {
24975
25327
  DEFAULT_PRIORITY,
24976
25328
  awsCallerArn,
25329
+ classifyParseError,
24977
25330
  gcPlan,
24978
25331
  isOrgRegisteredRepo,
24979
25332
  registryClientDeps,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.37.0",
3
+ "version": "3.39.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",