@mutmutco/cli 4.3.37 → 4.3.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.cjs +272 -91
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -4149,6 +4149,20 @@ var HOUSE_MAP = {
4149
4149
  onboard: "core"
4150
4150
  // repo-readiness orientation (front door — torn toward oracle, kept core)
4151
4151
  };
4152
+ var HOUSE_ALIASES = {
4153
+ issue: ["devops"],
4154
+ // filing/managing issues is oracle, but reads as shipping work
4155
+ "issue create": ["devops"]
4156
+ };
4157
+ function isDeclaredHouseAlias(houseToken, lookupPath) {
4158
+ const segments = lookupPath.split(" ");
4159
+ for (let end = segments.length; end > 0; end -= 1) {
4160
+ const key = segments.slice(0, end).join(" ");
4161
+ const aliases = HOUSE_ALIASES[key];
4162
+ if (aliases) return aliases.includes(houseToken);
4163
+ }
4164
+ return false;
4165
+ }
4152
4166
  function houseForPath(path2) {
4153
4167
  if (path2 === "") return "core";
4154
4168
  const segments = path2.split(" ");
@@ -5256,6 +5270,39 @@ function unknownCommandDomainGuide(parentPath, token) {
5256
5270
  return GUIDES.get(`${parentPath}:${token}`);
5257
5271
  }
5258
5272
 
5273
+ // src/verb-alias.ts
5274
+ var VERB_ALIAS_TABLE = [
5275
+ // #6489: `board` has no `status` leaf (read|claim|show|move|done|doctor only); the read that
5276
+ // answers "what is this issue/PR's board column" is `board show <ref>`.
5277
+ { typed: ["oracle", "board", "status"], canonical: ["oracle", "board", "show"] },
5278
+ { typed: ["board", "status"], canonical: ["board", "show"] },
5279
+ // #6494: there is no top-level or housed `registry` command; org/registry project metadata is
5280
+ // `oracle org project get`.
5281
+ { typed: ["registry"], canonical: ["oracle", "org", "project", "get"] },
5282
+ // #6486: filing/managing an issue reads as devops work (it ships something) but issues are oracle's
5283
+ // live truth; the real command is `oracle issue create`. (A narrower house-token-only reroute for
5284
+ // this same case may also exist elsewhere — this entry keeps the general table a complete answer on
5285
+ // its own, not dependent on that mechanism landing.)
5286
+ { typed: ["devops", "issue", "create"], canonical: ["oracle", "issue", "create"] }
5287
+ ];
5288
+ var TABLE_BY_LENGTH = [...VERB_ALIAS_TABLE].sort((a, b) => b.typed.length - a.typed.length);
5289
+ function matchesAt(argv, start, tokens2) {
5290
+ if (start + tokens2.length > argv.length) return false;
5291
+ return tokens2.every((tok, i) => argv[start + i] === tok);
5292
+ }
5293
+ function applyVerbAlias(argv) {
5294
+ const entry = TABLE_BY_LENGTH.find((e) => matchesAt(argv, 2, e.typed));
5295
+ if (!entry) return void 0;
5296
+ argv.splice(2, entry.typed.length, ...entry.canonical);
5297
+ return { typed: entry.typed.join(" "), canonical: entry.canonical.join(" ") };
5298
+ }
5299
+ function resolveVerbAliasShim(argv) {
5300
+ const hit = applyVerbAlias(argv);
5301
+ if (!hit) return;
5302
+ process.stderr.write(`mmi-cli: '${hit.typed}' is '${hit.canonical}' \u2014 ran it for you
5303
+ `);
5304
+ }
5305
+
5259
5306
  // src/command-composition.ts
5260
5307
  var import_node_os22 = require("node:os");
5261
5308
  var import_node_path47 = require("node:path");
@@ -7195,6 +7242,50 @@ async function readRepoSurfaceLabels(repo, deps = {}) {
7195
7242
  return void 0;
7196
7243
  }
7197
7244
  }
7245
+ function surfaceWords(label) {
7246
+ return label.slice(SURFACE_PREFIX.length).toLowerCase().split(/[-_]+/).filter(Boolean);
7247
+ }
7248
+ function pathWords(text) {
7249
+ const words = [];
7250
+ for (const hit of text.match(/[\w.-]+(?:\/[\w.-]+)+/g) ?? []) {
7251
+ for (const segment of hit.split("/")) words.push(...segment.toLowerCase().split(/[-_.]+/).filter(Boolean));
7252
+ }
7253
+ return words;
7254
+ }
7255
+ function proseWords(text) {
7256
+ return text.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
7257
+ }
7258
+ function inferSurface(candidates, context = {}) {
7259
+ const sorted = [...candidates].sort();
7260
+ if (sorted.length === 0) throw new Error("inferSurface: no surface:* candidates to choose from");
7261
+ const pWords = new Set(pathWords(context.body ?? ""));
7262
+ const tWords = /* @__PURE__ */ new Set([...proseWords(context.title ?? ""), ...proseWords(context.body ?? "")]);
7263
+ let best = sorted[0];
7264
+ let bestScore = 0;
7265
+ let bestFromPath = false;
7266
+ for (const label of sorted) {
7267
+ const words = surfaceWords(label);
7268
+ if (!words.length) continue;
7269
+ const pathHits = words.filter((w) => pWords.has(w)).length;
7270
+ const proseHits = words.filter((w) => tWords.has(w)).length;
7271
+ const score = pathHits * 2 + proseHits;
7272
+ if (score > bestScore) {
7273
+ bestScore = score;
7274
+ best = label;
7275
+ bestFromPath = pathHits > 0;
7276
+ }
7277
+ }
7278
+ if (bestScore === 0) {
7279
+ return {
7280
+ label: sorted[0],
7281
+ reason: `no path or title/body word matched any of ${sorted.length} surface label(s) \u2014 defaulted to the alphabetically-first`
7282
+ };
7283
+ }
7284
+ return {
7285
+ label: best,
7286
+ reason: bestFromPath ? "matched a path mentioned in the issue body" : "matched a word in the issue title/body"
7287
+ };
7288
+ }
7198
7289
  async function checkSurfaceRequirement(input, deps = {}) {
7199
7290
  if (input.waiver) {
7200
7291
  const reason = input.waiver.reason.trim();
@@ -7226,20 +7317,7 @@ async function checkSurfaceRequirement(input, deps = {}) {
7226
7317
  }
7227
7318
  if (known.length === 0) return { enforcing: false, taxonomyAbsent: true };
7228
7319
  if (labelsCarrySurface(input.labels)) return { enforcing: true };
7229
- const command = input.command ?? "issue create";
7230
- const where = input.rowLabel ? `${input.rowLabel}: ` : "";
7231
- return {
7232
- enforcing: true,
7233
- refusal: {
7234
- message: `${command}: ${where}${input.repo} requires every open issue to carry exactly one surface:* label, and this one sets none \u2014 filing it unlabeled reds the board check on every open PR in that repo. Pass --surface <value>, or --no-surface if this filing is genuinely exempt`,
7235
- payload: {
7236
- code: ERROR_CODES.ERR_MISSING_FLAG,
7237
- offending_flag: "--surface",
7238
- // Advisory, not a closed enum: the board rule is "exactly one surface:* label", whatever its value.
7239
- expected: [...known].sort()
7240
- }
7241
- }
7242
- };
7320
+ return { enforcing: true, inferred: inferSurface(known, { title: input.title, body: input.body }) };
7243
7321
  }
7244
7322
  function conflictingSurfaceInputs(surfaceFlag, labels) {
7245
7323
  if (!surfaceFlag) return void 0;
@@ -15617,10 +15695,10 @@ var rollout_plan_default = {
15617
15695
  note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
15618
15696
  },
15619
15697
  baseline: {
15620
- version: "4.3.37",
15621
- tag: "v4.3.37",
15622
- commit: "37821457da10",
15623
- npm: "@mutmutco/cli@4.3.37"
15698
+ version: "4.3.39",
15699
+ tag: "v4.3.39",
15700
+ commit: "cc67ff24bbec",
15701
+ npm: "@mutmutco/cli@4.3.39"
15624
15702
  },
15625
15703
  exitCriterion: "fleet-n-of-n",
15626
15704
  hubOnlyShortcut: "forbidden",
@@ -15637,14 +15715,14 @@ var rollout_plan_default = {
15637
15715
  repo: "mutmutco/mmi-hub",
15638
15716
  role: "canary",
15639
15717
  schedule: "train",
15640
- v3Target: "v4.3.37"
15718
+ v3Target: "v4.3.39"
15641
15719
  }
15642
15720
  ],
15643
15721
  rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
15644
15722
  rollback: {
15645
15723
  independent: true,
15646
- mechanism: "npm dist-tag latest -> 4.3.37 and redeploy the Hub Lambda from tag v4.3.37 (37821457da10); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15647
- v3Target: "v4.3.37 (@mutmutco/cli@4.3.37, tag commit 37821457da10 \u2014 last known-good release carrying the repo-index v4-only contract)"
15724
+ mechanism: "npm dist-tag latest -> 4.3.39 and redeploy the Hub Lambda from tag v4.3.39 (cc67ff24bbec); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15725
+ v3Target: "v4.3.39 (@mutmutco/cli@4.3.39, tag commit cc67ff24bbec \u2014 last known-good release carrying the repo-index v4-only contract)"
15648
15726
  }
15649
15727
  },
15650
15728
  {
@@ -21645,13 +21723,9 @@ async function runTrainDoctor(input) {
21645
21723
  add(linked ? { code: "worktree-isolated", severity: "blocker", source: "local", title: `this is a linked worktree on ${currentBranch2 || "(detached)"}, not the lane's start branch ${startBranch} (#2770)`, remedy: `run the ${lane} lane from the primary checkout on ${startBranch}; a linked worktree never carries the train` } : { code: "branch-mismatch", severity: "blocker", source: "local", title: `on ${currentBranch2 || "(detached)"}; the ${lane} lane starts from ${startBranch}`, remedy: `git checkout ${startBranch}, then rerun; --heal never switches branches \u2014 the branch you stand on names the lane you meant, and healing it would run the train off a branch you did not pick (#6432)` });
21646
21724
  }
21647
21725
  const stage = laneStage(lane);
21648
- const workflowsRef = `origin/${startBranch ?? "main"}`;
21649
- const workflows = deps.readWorkflows(cwd, workflowsRef);
21650
- if (workflows === null) {
21651
- add({ code: "workflows-unreadable", severity: "blocker", source: "local", title: `.github/workflows at ${workflowsRef} could not be read from ${cwd} \u2014 the tag-addressability (#5428) and npm-major (#5666) preflights did not run`, remedy: `run from a checkout whose git can read ${workflowsRef} (fetch origin first), then rerun; an unread workflow set is unverified, not green` });
21652
- }
21726
+ let missing = [];
21653
21727
  if (hints) {
21654
- const missing = [
21728
+ missing = [
21655
21729
  ...!hints.hasDevelopmentBranch && lane !== "hotfix" ? ["development"] : [],
21656
21730
  ...!hints.hasMainBranch ? ["main"] : [],
21657
21731
  // #6318: a `--dev` release never reads, merges or tags rc — a missing rc branch cannot block it.
@@ -21659,30 +21733,38 @@ async function runTrainDoctor(input) {
21659
21733
  ];
21660
21734
  if (missing.length) add({ code: "bootstrap-gap", severity: "blocker", source: "origin", title: `train branch(es) missing on origin: ${missing.join(", ")}`, remedy: `bootstrap the repo train: \`mmi-cli devops bootstrap apply ${repo} --execute\` (from the MMI-Hub root), then rerun` });
21661
21735
  }
21662
- try {
21663
- const required = await discoverRequiredCheckContexts(train, ctx, stage);
21664
- if (required.length === 0) {
21665
- add({ code: "bootstrap-gap", severity: "warning", source: "origin", title: `no ruleset requires a status check on ${stage} \u2014 the train tags without a check wait (the GitHub push gate is the backstop)`, remedy: `activate the product ruleset: \`mmi-cli devops bootstrap apply ${repo} --execute\` (from the MMI-Hub root), or \`mmi-cli devops ci audit --repo ${repo}\`` });
21666
- } else {
21667
- try {
21668
- assertTagAddressableRequiredContexts({ readWorkflows: () => workflows }, required, repo);
21669
- } catch (e) {
21670
- add({ code: "contexts-not-tag-addressable", severity: "blocker", source: "local", title: `required ${stage} check(s) cannot materialize on a tag push (#5428)`, remedy: message(e) });
21671
- }
21672
- if (startBranch) {
21673
- const tip = await git3(["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${startBranch}`]);
21674
- const checkRuns = JSON.parse(await train.run("gh", ["api", `repos/${repo}/commits/${tip}/check-runs`, "--jq", TRAIN_CHECK_RUNS_JQ]));
21675
- const statuses = JSON.parse(await train.run("gh", ["api", `repos/${repo}/commits/${tip}/status`, "--jq", TRAIN_COMMIT_STATUS_JQ]));
21676
- const observed = required.filter((c) => checkRuns.some((r) => r.name === c) || statuses.some((s) => s.context === c));
21677
- const red = observed.filter((c) => resolveContextState(c, checkRuns, statuses) === "failed");
21678
- const inFlight = observed.filter((c) => resolveContextState(c, checkRuns, statuses) === "pending");
21679
- const at = `${startBranch}@${tip.slice(0, 12)}`;
21680
- if (red.length) add({ code: "required-check-red", severity: "blocker", source: "origin", title: `required ${stage} check(s) failed on the lane's start branch tip: ${red.join(", ")} at ${at}`, remedy: `--heal never clears this one \u2014 a red trunk needs a fix, not a heal: land the failure's fix on ${startBranch} through a CI-gated PR, then rerun once its run is green (#6461)` });
21681
- else if (inFlight.length) add({ code: "required-check-red", severity: "warning", source: "origin", title: `required ${stage} check(s) still in flight on the lane's start branch tip: ${inFlight.join(", ")} at ${at}`, remedy: `wait for the run to conclude and rerun \u2014 the tip's verdict is not known yet, and the train would tag before it is` });
21736
+ const startBranchMissing = startBranch != null && missing.includes(startBranch);
21737
+ const workflowsRef = `origin/${startBranch ?? "main"}`;
21738
+ const workflows = startBranchMissing ? null : deps.readWorkflows(cwd, workflowsRef);
21739
+ if (workflows === null && !startBranchMissing) {
21740
+ add({ code: "workflows-unreadable", severity: "blocker", source: "local", title: `.github/workflows at ${workflowsRef} could not be read from ${cwd} \u2014 the tag-addressability (#5428) and npm-major (#5666) preflights did not run`, remedy: `run from a checkout whose git can read ${workflowsRef} (fetch origin first), then rerun; an unread workflow set is unverified, not green` });
21741
+ }
21742
+ if (!startBranchMissing) {
21743
+ try {
21744
+ const required = await discoverRequiredCheckContexts(train, ctx, stage);
21745
+ if (required.length === 0) {
21746
+ add({ code: "bootstrap-gap", severity: "warning", source: "origin", title: `no ruleset requires a status check on ${stage} \u2014 the train tags without a check wait (the GitHub push gate is the backstop)`, remedy: `activate the product ruleset: \`mmi-cli devops bootstrap apply ${repo} --execute\` (from the MMI-Hub root), or \`mmi-cli devops ci audit --repo ${repo}\`` });
21747
+ } else {
21748
+ try {
21749
+ assertTagAddressableRequiredContexts({ readWorkflows: () => workflows }, required, repo);
21750
+ } catch (e) {
21751
+ add({ code: "contexts-not-tag-addressable", severity: "blocker", source: "local", title: `required ${stage} check(s) cannot materialize on a tag push (#5428)`, remedy: message(e) });
21752
+ }
21753
+ if (startBranch) {
21754
+ const tip = await git3(["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${startBranch}`]);
21755
+ const checkRuns = JSON.parse(await train.run("gh", ["api", `repos/${repo}/commits/${tip}/check-runs`, "--jq", TRAIN_CHECK_RUNS_JQ]));
21756
+ const statuses = JSON.parse(await train.run("gh", ["api", `repos/${repo}/commits/${tip}/status`, "--jq", TRAIN_COMMIT_STATUS_JQ]));
21757
+ const observed = required.filter((c) => checkRuns.some((r) => r.name === c) || statuses.some((s) => s.context === c));
21758
+ const red = observed.filter((c) => resolveContextState(c, checkRuns, statuses) === "failed");
21759
+ const inFlight = observed.filter((c) => resolveContextState(c, checkRuns, statuses) === "pending");
21760
+ const at = `${startBranch}@${tip.slice(0, 12)}`;
21761
+ if (red.length) add({ code: "required-check-red", severity: "blocker", source: "origin", title: `required ${stage} check(s) failed on the lane's start branch tip: ${red.join(", ")} at ${at}`, remedy: `--heal never clears this one \u2014 a red trunk needs a fix, not a heal: land the failure's fix on ${startBranch} through a CI-gated PR, then rerun once its run is green (#6461)` });
21762
+ else if (inFlight.length) add({ code: "required-check-red", severity: "warning", source: "origin", title: `required ${stage} check(s) still in flight on the lane's start branch tip: ${inFlight.join(", ")} at ${at}`, remedy: `wait for the run to conclude and rerun \u2014 the tip's verdict is not known yet, and the train would tag before it is` });
21763
+ }
21682
21764
  }
21765
+ } catch (e) {
21766
+ unverified(`required status checks on ${stage}`, e);
21683
21767
  }
21684
- } catch (e) {
21685
- unverified(`required status checks on ${stage}`, e);
21686
21768
  }
21687
21769
  let ledgerTag;
21688
21770
  let ledgerPath;
@@ -24606,7 +24688,7 @@ function ciReconcileExitFailed(input) {
24606
24688
  }
24607
24689
  return input.applyResults.some((result) => result.errors.length > 0 || result.postApply == null || result.postApply.state !== "clean" && result.postApply.state !== "repaired");
24608
24690
  }
24609
- async function authoritativeSafetyHold(authority, deps, repo, baseBranch) {
24691
+ async function authoritativeSafetyHold(authority, deps, repo, baseBranch, meta) {
24610
24692
  if (authority.coversReleaseBranches) {
24611
24693
  const prOnly = authority.contexts.filter((context) => TRAIN_PR_ONLY_CONTEXTS.has(context));
24612
24694
  if (prOnly.length > 0) {
@@ -24620,6 +24702,22 @@ async function authoritativeSafetyHold(authority, deps, repo, baseBranch) {
24620
24702
  if (missing.length > 0) {
24621
24703
  return `${authority.source} requires context(s) with no pull_request workflow emitter [${missing.join(", ")}]; no source or live mutation was made`;
24622
24704
  }
24705
+ if (authority.coversReleaseBranches) {
24706
+ const track = resolveReleaseTrack(meta, void 0, repo);
24707
+ const releaseBranches = branchesForTrack(track).filter((b) => b !== baseBranch);
24708
+ const ignore = /* @__PURE__ */ new Set([...AGENT_PR_BOOKKEEPING_CONTEXTS, ...TRAIN_PR_ONLY_CONTEXTS]);
24709
+ const requiredForReleaseBranches = authority.contexts.filter((context) => !ignore.has(context));
24710
+ for (const branch of releaseBranches) {
24711
+ if (await branchPresence(deps, repo, branch) !== true) continue;
24712
+ const emittedThere = await resolveEmittedPrContexts(deps, repo, branch);
24713
+ if (emittedThere === void 0) continue;
24714
+ const emittedThereSet = new Set(emittedThere);
24715
+ const unreachable = requiredForReleaseBranches.filter((context) => !emittedThereSet.has(context));
24716
+ if (unreachable.length > 0) {
24717
+ return `${authority.source} requires context(s) [${unreachable.join(", ")}] on ${branch}, but ${branch}'s own workflows emit none of them \u2014 a base-${branch} PR (e.g. a hotfix) could never satisfy the gate; no source or live mutation was made. Remove [${unreachable.join(", ")}] from ${branch} requirements, or add the emitting workflow to ${branch}`;
24718
+ }
24719
+ }
24720
+ }
24623
24721
  return null;
24624
24722
  }
24625
24723
  async function applyCiReconcileRepo(repo, deps) {
@@ -24636,7 +24734,7 @@ async function applyCiReconcileRepo(repo, deps) {
24636
24734
  empty.errors.push(`${PRODUCT_RULESET_REF} is not parseable: ${e.message}`);
24637
24735
  return finalizeCiReconcile(repo, deps, empty, report);
24638
24736
  }
24639
- const hold = await authoritativeSafetyHold(authority, deps, repo, baseBranch);
24737
+ const hold = await authoritativeSafetyHold(authority, deps, repo, baseBranch, meta);
24640
24738
  if (hold) {
24641
24739
  empty.skipped.push(hold);
24642
24740
  return finalizeCiReconcile(repo, deps, empty, report, hold);
@@ -24644,7 +24742,7 @@ async function applyCiReconcileRepo(repo, deps) {
24644
24742
  } else if (report.class === "deployable" && !report.explicitNoCi) {
24645
24743
  const registryAuthority = registryAuthorityWithoutReference(meta, repo);
24646
24744
  if (registryAuthority) {
24647
- const hold = await authoritativeSafetyHold(registryAuthority, deps, repo, baseBranch);
24745
+ const hold = await authoritativeSafetyHold(registryAuthority, deps, repo, baseBranch, meta);
24648
24746
  if (hold) {
24649
24747
  empty.skipped.push(hold);
24650
24748
  return finalizeCiReconcile(repo, deps, empty, report, hold);
@@ -24673,7 +24771,7 @@ async function applyCiReconcileRepo(repo, deps) {
24673
24771
  result.errors.push(`${PRODUCT_RULESET_REF} is not parseable: ${e.message}`);
24674
24772
  return finalizeCiReconcile(repo, deps, result, report);
24675
24773
  }
24676
- const hold = await authoritativeSafetyHold(authority, deps, repo, baseBranch);
24774
+ const hold = await authoritativeSafetyHold(authority, deps, repo, baseBranch, meta);
24677
24775
  if (hold) {
24678
24776
  result.skipped.push(hold);
24679
24777
  return finalizeCiReconcile(repo, deps, result, report, hold);
@@ -29253,8 +29351,11 @@ function parseOauthVar(raw) {
29253
29351
  const out = {};
29254
29352
  for (const [key, value] of Object.entries(map)) {
29255
29353
  if (key === "subdomains" || key === "domains") {
29256
- if (!Array.isArray(value) || value.some((v) => typeof v !== "string" || !v.trim())) {
29257
- throw new Error(`org project set: oauth.${key} must be an array of non-empty strings`);
29354
+ const allowEmpty = key === "subdomains";
29355
+ if (!Array.isArray(value) || value.some((v) => typeof v !== "string" || v.trim() === "" && !(allowEmpty && v === ""))) {
29356
+ throw new Error(
29357
+ allowEmpty ? `org project set: oauth.${key} must be an array of strings ("" selects the apex \u2014 the domain itself)` : `org project set: oauth.${key} must be an array of non-empty strings`
29358
+ );
29258
29359
  }
29259
29360
  out[key] = value.map((v) => v.trim());
29260
29361
  } else if (key === "callbackPath") {
@@ -33492,18 +33593,33 @@ async function preflightBatchSurfaces(validated, rowRepo, options) {
33492
33593
  }
33493
33594
  if (options.noSurface) return [];
33494
33595
  const errors = [];
33495
- const cache = /* @__PURE__ */ new Map();
33596
+ const censusCache = /* @__PURE__ */ new Map();
33597
+ const censusFor = (repo) => {
33598
+ let pending = censusCache.get(repo);
33599
+ if (!pending) {
33600
+ pending = readRepoSurfaceLabels(repo);
33601
+ censusCache.set(repo, pending);
33602
+ }
33603
+ return pending;
33604
+ };
33496
33605
  for (const { row, spec } of validated) {
33606
+ if (labelsCarrySurface(spec.labels)) continue;
33497
33607
  const repo = rowRepo(spec);
33498
- const key = [repo, labelsCarrySurface(spec.labels) ? "has" : "none"].join("::");
33499
- let verdict = cache.get(key);
33500
- if (!verdict) {
33501
- verdict = await checkSurfaceRequirement({ repo, labels: spec.labels, command: "issue create --batch" });
33502
- cache.set(key, verdict);
33503
- if (verdict.warn) process.stderr.write(`${verdict.warn}
33504
- `);
33608
+ const known = await censusFor(repo);
33609
+ if (known === void 0) {
33610
+ process.stderr.write(
33611
+ `warning: could not read ${repo}'s labels, so row ${row}'s surface-label requirement was not checked; if that board enforces one surface:* label per open issue, add one with \`mmi-cli oracle issue edit <n> --add-label surface:<value>\`
33612
+ `
33613
+ );
33614
+ continue;
33505
33615
  }
33506
- if (verdict.refusal) errors.push({ row, error: verdict.refusal.message });
33616
+ if (known.length === 0) continue;
33617
+ const inferred = inferSurface(known, { title: spec.title, body: spec.body });
33618
+ spec.labels = [...spec.labels ?? [], inferred.label];
33619
+ process.stderr.write(
33620
+ `mmi-cli: no --surface given \u2014 row ${row} filed under ${inferred.label} (${inferred.reason}). Pass --surface to choose.
33621
+ `
33622
+ );
33507
33623
  }
33508
33624
  return errors;
33509
33625
  }
@@ -35597,8 +35713,8 @@ function registerSecretsCommands(program3) {
35597
35713
  const secrets = program3.command("secrets").description("project vault \u2014 project-admins self-serve their own repo's full tree (stageless + dev/rc/main); org-infra namespaces are master-gated");
35598
35714
  secrets.command("where").description("print where this repo's secrets live \u2014 the two-tier vault layout + well-known keys (no values)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsWhere(d, o)));
35599
35715
  secrets.command("list").description("list secret NAMES + tier for THIS repo (never values). To locate a key you cannot place \u2014 including org-infra (_org) \u2014 use the cross-slug view: `mmi-cli oracle org access capabilities` (#3436)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsList(d, o)));
35600
- secrets.command("find <intent>").description("resolve a plain-language intent to canonical secret names + the exact keyless-use command (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((intent, o) => withSecrets(async (d) => {
35601
- if (!await secretsFind(d, intent, o)) process.exitCode = 1;
35716
+ secrets.command("find <intent...>").description("resolve a plain-language intent to canonical secret names + the exact keyless-use command (master-only, #2244); an unquoted multi-word intent is joined with spaces (#6495)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((intentWords, o) => withSecrets(async (d) => {
35717
+ if (!await secretsFind(d, intentWords.join(" "), o)) process.exitCode = 1;
35602
35718
  }));
35603
35719
  secrets.command("catalog").description("the secret catalog \u2014 grouped, with each secret's exact keyless-use command (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--drift", "include the bidirectional reconciler drift report").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsCatalog(d, o)));
35604
35720
  secrets.command("doctor").description("vault drift report \u2014 declared-missing / orphan / off-scheme / duplicate (master-only, #2244)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets(async (d) => {
@@ -37080,8 +37196,20 @@ function matchedMandatoryGlobs(paths, mandatory) {
37080
37196
  return list.some((path2) => re.test(path2));
37081
37197
  });
37082
37198
  }
37083
- function evaluateTestCommandPolicy({ paths, mandatory, regulated = true, override = null, addedPaths = [] } = {}) {
37199
+ function evaluateTestCommandPolicy({ paths, mandatory, regulated = true, override = null, addedPaths = [], structuralRefusal = false } = {}) {
37084
37200
  const configuredMandatoryCount = mandatoryGlobList(mandatory).length;
37201
+ if (structuralRefusal) {
37202
+ return {
37203
+ configuredMandatoryCount,
37204
+ matchedMandatoryGlobs: [],
37205
+ matchedMandatoryCount: 0,
37206
+ testCommandsAllowed: false,
37207
+ editsExistingTest: false,
37208
+ reasonId: "test-command-outside-mandatory-zone",
37209
+ testCommandNote: null,
37210
+ commandClasses: { allowed: [], refused: [TEST_COMMAND_CLASS] }
37211
+ };
37212
+ }
37085
37213
  if (!regulated) {
37086
37214
  return {
37087
37215
  configuredMandatoryCount: 0,
@@ -37090,25 +37218,30 @@ function evaluateTestCommandPolicy({ paths, mandatory, regulated = true, overrid
37090
37218
  testCommandsAllowed: true,
37091
37219
  editsExistingTest: false,
37092
37220
  reasonId: null,
37221
+ testCommandNote: null,
37093
37222
  commandClasses: { allowed: [TEST_COMMAND_CLASS], refused: [] }
37094
37223
  };
37095
37224
  }
37096
37225
  const matched = matchedMandatoryGlobs(paths, mandatory);
37097
37226
  const overrideAuthorizes = Array.isArray(override?.kinds) && override.kinds.includes(TEST_WORK_KIND);
37098
37227
  const existingTestEdited = editsExistingTest(paths, addedPaths);
37099
- const testCommandsAllowed = matched.length > 0 || overrideAuthorizes || existingTestEdited;
37228
+ const inMandatoryZone = matched.length > 0 || overrideAuthorizes || existingTestEdited;
37229
+ const testCommandsAllowed = true;
37100
37230
  return {
37101
37231
  configuredMandatoryCount,
37102
37232
  matchedMandatoryGlobs: matched,
37103
37233
  matchedMandatoryCount: matched.length,
37104
37234
  testCommandsAllowed,
37105
- /** #5842: true when the diff edits a test file that already existed — one of the facts that can
37106
- * authorize the class, named so a refusal can say what was missing. */
37235
+ /** #5842: true when the diff edits a test file that already existed — one of the facts that used
37236
+ * to authorize the class, kept for callers that still branch on it. */
37107
37237
  editsExistingTest: existingTestEdited,
37108
- reasonId: testCommandsAllowed ? null : "test-command-outside-mandatory-zone",
37238
+ reasonId: null,
37239
+ /** Informational only (#1166): set when the diff is outside the mandatory zone, so a caller can
37240
+ * note that CI does not require a test here — never a reason to refuse the run. */
37241
+ testCommandNote: inMandatoryZone ? null : "test-command-outside-mandatory-zone",
37109
37242
  commandClasses: {
37110
- allowed: testCommandsAllowed ? [TEST_COMMAND_CLASS] : [],
37111
- refused: testCommandsAllowed ? [] : [TEST_COMMAND_CLASS]
37243
+ allowed: [TEST_COMMAND_CLASS],
37244
+ refused: []
37112
37245
  }
37113
37246
  };
37114
37247
  }
@@ -37778,7 +37911,12 @@ function runTestPolicy(root, deps = {}) {
37778
37911
  addedPaths: changed.filter((f) => f.status === "A" || f.status === "R").map((f) => f.path),
37779
37912
  mandatory: policy.mandatory,
37780
37913
  regulated: policy.declared !== false || policyRefusals.length > 0,
37781
- override: override && lookup.refusals.length === 0 ? { kinds: override.kinds } : null
37914
+ override: override && lookup.refusals.length === 0 ? { kinds: override.kinds } : null,
37915
+ // #1166: a still-blocking pre-diff finding (untrusted range, unresolvable base, a missing policy
37916
+ // on the hotfix lane, a malformed trailer, an unwaived stale entry) means the diff itself was
37917
+ // never evaluated — the mandatory-zone question this evaluator now answers permissively was
37918
+ // never reached, so the refusal stands exactly as before.
37919
+ structuralRefusal: blocking.length > 0
37782
37920
  });
37783
37921
  const result = {
37784
37922
  ok: findings.length === 0,
@@ -37792,6 +37930,7 @@ function runTestPolicy(root, deps = {}) {
37792
37930
  matchedMandatoryCount: commandPolicy.matchedMandatoryCount,
37793
37931
  testCommandsAllowed: commandPolicy.testCommandsAllowed,
37794
37932
  testCommandReasonId: commandPolicy.reasonId,
37933
+ testCommandNote: commandPolicy.testCommandNote,
37795
37934
  commandClasses: commandPolicy.commandClasses
37796
37935
  };
37797
37936
  if (override) {
@@ -39808,7 +39947,7 @@ function registerCollaborationCommands(program3) {
39808
39947
  }
39809
39948
  const issue = program3.command("issue").description("issues \u2014 create and view with structured JSON (view; show, get and read alias view for board-verb/gh-shaped callers). Claims are board mutations: use `oracle board claim`, never `oracle issue claim` or the flat `board claim`");
39810
39949
  withExamples(mutating(
39811
- issue.command("create").description("create an issue (type \u2014 label) and print {number,url,label} JSON").addOption(new Option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").choices([...ISSUE_TYPES])).option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file. `-` (stdin) needs a heredoc, which the agent inline-body guard denies (#1473/#2125) \u2014 prefer a real path; a title with backticks needs --title-file for the same reason (#3381)").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--surface <surface>", "issue surface, with or without the surface: prefix (#3789). Required when the target repo runs the one-surface-label board rule; any value satisfies it, so this is not a closed enum").option("--no-surface", "file without a surface label on a repo that requires one \u2014 for a genuinely exempt filing (e.g. a coop proof issue that spans every surface)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment"),
39950
+ issue.command("create").description("create an issue (type \u2014 label) and print {number,url,label} JSON").addOption(new Option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").choices([...ISSUE_TYPES])).option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file. `-` (stdin) needs a heredoc, which the agent inline-body guard denies (#1473/#2125) \u2014 prefer a real path; a title with backticks needs --title-file for the same reason (#3381)").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--surface <surface>", "issue surface, with or without the surface: prefix (#3789). Any value satisfies the one-surface-label board rule, so this is not a closed enum. Omit it on an enforcing repo and one is inferred from the title/body (#1164) \u2014 the pick is printed and included in --json as surface_inferred/surface/surface_reason").option("--no-surface", "file without a surface label on a repo that requires one \u2014 for a genuinely exempt filing (e.g. a coop proof issue that spans every surface). Suppresses inference too").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment"),
39812
39951
  // --dry-run/--validate-only plan: resolve the same title source and validate the same type, priority,
39813
39952
  // and surface contract as the real action. A plan that echoes the title-file PATH instead of its value
39814
39953
  // is not a plan of the mutation that will run (#3914).
@@ -39832,14 +39971,24 @@ function registerCollaborationCommands(program3) {
39832
39971
  if (clash) fail(clash.message, clash.payload);
39833
39972
  const planRepo = opts.batch || surfaceWaived() ? void 0 : await resolveRepo(opts.repo);
39834
39973
  const surface = resolveCreateSurface(opts);
39974
+ let planInferred;
39835
39975
  if (planRepo) {
39836
- const { refusal, warn } = await checkSurfaceRequirement({
39976
+ const { refusal, warn, inferred } = await checkSurfaceRequirement({
39837
39977
  repo: planRepo,
39838
- labels: [...planLabels, ...surface ? [surface] : []]
39978
+ labels: [...planLabels, ...surface ? [surface] : []],
39979
+ title,
39980
+ body: opts.body
39839
39981
  });
39840
39982
  if (warn) process.stderr.write(`${warn}
39841
39983
  `);
39842
39984
  if (refusal) fail(refusal.message, refusal.payload);
39985
+ if (inferred && !surface) {
39986
+ planInferred = inferred;
39987
+ process.stderr.write(
39988
+ `mmi-cli: no --surface given \u2014 filed under ${inferred.label} (${inferred.reason}). Pass --surface to choose.
39989
+ `
39990
+ );
39991
+ }
39843
39992
  }
39844
39993
  return {
39845
39994
  command: "issue create",
@@ -39847,7 +39996,8 @@ function registerCollaborationCommands(program3) {
39847
39996
  title,
39848
39997
  priority,
39849
39998
  repo: opts.repo,
39850
- ...surface ? { surface } : {}
39999
+ ...surface ? { surface } : {},
40000
+ ...planInferred ? { surface_inferred: true, surface: planInferred.label, surface_reason: planInferred.reason } : {}
39851
40001
  };
39852
40002
  }
39853
40003
  ).action(async (o) => {
@@ -39859,6 +40009,7 @@ function registerCollaborationCommands(program3) {
39859
40009
  let extraLabels = [];
39860
40010
  let targetRepo2;
39861
40011
  let surfaceFlagLabel;
40012
+ let surfaceInferred;
39862
40013
  try {
39863
40014
  issueType = resolveCreateType(o.type, "issue create", o.label);
39864
40015
  title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
@@ -39890,8 +40041,8 @@ function registerCollaborationCommands(program3) {
39890
40041
  return fail(`issue create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
39891
40042
  }
39892
40043
  {
39893
- const surfaceCheck = await checkSurfaceRequirement({ repo: targetRepo2, labels: extraLabels });
39894
- const { refusal, warn } = surfaceCheck;
40044
+ const surfaceCheck = await checkSurfaceRequirement({ repo: targetRepo2, labels: extraLabels, title, body });
40045
+ const { refusal, warn, inferred } = surfaceCheck;
39895
40046
  if (warn) process.stderr.write(`${warn}
39896
40047
  `);
39897
40048
  if (shouldWithdrawSurfaceFlag(surfaceCheck) && surfaceFlagLabel) {
@@ -39906,6 +40057,22 @@ function registerCollaborationCommands(program3) {
39906
40057
  });
39907
40058
  process.stderr.write(
39908
40059
  `warning: --surface ${surfaceFlagLabel} was dropped \u2014 ${targetRepo2} defines no surface:* labels, and creating one here would switch the one-surface-label rule on for every later filing in it. Use --label ${surfaceFlagLabel} if you really mean to start that taxonomy.
40060
+ `
40061
+ );
40062
+ }
40063
+ if (inferred && !surfaceFlagLabel && !surfaceWaived()) {
40064
+ extraLabels = [...extraLabels, inferred.label];
40065
+ args = buildIssueArgs({
40066
+ type: issueType,
40067
+ title,
40068
+ body,
40069
+ priority,
40070
+ repo: targetRepo2,
40071
+ labels: extraLabels.length ? extraLabels : void 0
40072
+ });
40073
+ surfaceInferred = inferred;
40074
+ process.stderr.write(
40075
+ `mmi-cli: no --surface given \u2014 filed under ${inferred.label} (${inferred.reason}). Pass --surface to choose.
39909
40076
  `
39910
40077
  );
39911
40078
  }
@@ -39946,7 +40113,9 @@ function registerCollaborationCommands(program3) {
39946
40113
  ...attached.resetAt ? { resetAt: attached.resetAt } : {},
39947
40114
  ...attached.resetEpochSeconds !== void 0 ? { resetEpochSeconds: attached.resetEpochSeconds } : {}
39948
40115
  } : {},
39949
- ...parentLinkFields(parent, parentLinkError)
40116
+ ...parentLinkFields(parent, parentLinkError),
40117
+ // #1164: surfaced so a caller can see (and override with --surface) a pick it never asked for.
40118
+ ...surfaceInferred ? { surface_inferred: true, surface: surfaceInferred.label, surface_reason: surfaceInferred.reason } : {}
39950
40119
  }));
39951
40120
  }), [
39952
40121
  'mmi-cli oracle issue create --type task --title "Wire the schema"',
@@ -42091,7 +42260,7 @@ function registerDeveloperCommands(program3) {
42091
42260
  }
42092
42261
  });
42093
42262
  const tests = program3.command("tests").description("a repo's test-policy.json \u2014 the opt-in test contract and its enforcement");
42094
- tests.command("policy").description("enforce this repo's test-policy.json against the diff: a mandatory-zone change must carry a test, an unrequested new test file is refused, a `protected` test file may not be deleted or renamed away, and a `protected` entry naming a missing file is refused. Override any of them with a `Test-Policy-Override: <reason>` commit trailer (#3605)").option("--json", "machine-readable result: { ok, base, policySource: { ref, sha, path }, root, changedCount, mandatoryCount, matchedMandatoryCount, matchedMandatoryGlobs, testCommandsAllowed, commandClasses, findings[] }").option("--base <ref>", "comparison base (default: TEST_POLICY_BASE, then origin/development, then origin/main)").option("--policy-ref <commit>", "load test-policy.json from the exact fetched origin/development commit and audit its protected/satisfiedBy paths against that commit's tree, not this checkout; valid only with --base origin/main").addOption(new Option("--repo <owner/repo>", "not accepted \u2014 tests policy reads the current checkout").hideHelp()).action(async (o) => {
42263
+ tests.command("policy").description("enforce this repo's test-policy.json against the diff: a mandatory-zone change must carry a test, an unrequested new test file is refused, a `protected` test file may not be deleted or renamed away, and a `protected` entry naming a missing file is refused. Override any of them with a `Test-Policy-Override: <reason>` commit trailer (#3605)").option("--json", "machine-readable result: { ok, base, policySource: { ref, sha, path }, root, changedCount, mandatoryCount, matchedMandatoryCount, matchedMandatoryGlobs, testCommandsAllowed, testCommandNote, commandClasses, findings[] }").option("--base <ref>", "comparison base (default: TEST_POLICY_BASE, then origin/development, then origin/main)").option("--policy-ref <commit>", "load test-policy.json from the exact fetched origin/development commit and audit its protected/satisfiedBy paths against that commit's tree, not this checkout; valid only with --base origin/main").addOption(new Option("--repo <owner/repo>", "not accepted \u2014 tests policy reads the current checkout").hideHelp()).action(async (o) => {
42095
42264
  try {
42096
42265
  if (o.repo) {
42097
42266
  const rerun = `mmi-cli tests policy${o.base ? ` --base ${o.base}` : ""}${o.policyRef ? ` --policy-ref ${o.policyRef}` : ""}${o.json ? " --json" : ""}`;
@@ -42114,7 +42283,7 @@ function registerDeveloperCommands(program3) {
42114
42283
  return;
42115
42284
  }
42116
42285
  if (result.ok) {
42117
- const commandVerdict = result.testCommandsAllowed ? "test commands: allowed" : `test commands: refused [${result.testCommandReasonId}] \u2014 resolved repo: ${result.root}; if that is not the repo under test, start the command with a literal Set-Location "<your worktree>"`;
42286
+ const commandVerdict = result.testCommandsAllowed ? result.testCommandNote ? `test commands: allowed (informational [${result.testCommandNote}] \u2014 CI does not require a test here)` : "test commands: allowed" : `test commands: refused [${result.testCommandReasonId}] \u2014 resolved repo: ${result.root}; if that is not the repo under test, start the command with a literal Set-Location "<your worktree>"`;
42118
42287
  console.log(
42119
42288
  `tests policy: OK (${result.changedCount} changed file(s); ${result.matchedMandatoryCount} of ${result.mandatoryCount} mandatory glob(s) matched, ${result.protectedCount} protected file(s); ${commandVerdict}).`
42120
42289
  );
@@ -45008,7 +45177,7 @@ project.command("list").description("list all projects (identity + board, never
45008
45177
  console.log(`${p.slug ?? "?"} - ${p.name ?? ""}${p.division ? ` [${p.division}]` : ""}${p.class ? ` (${p.class})` : ""}${p.projectType ? ` <${p.projectType}>` : ""}${p.deployModel ? ` {${p.deployModel}}` : ""}`);
45009
45178
  }
45010
45179
  });
45011
- project.command("get [owner/repo]").description("a project's META (board ids + pointers) by repo or slug; defaults to the current repo \u2014 identity, NOT deploy coords").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
45180
+ project.command("get [owner/repo]").description("a project's META (board ids + pointers) by repo or slug; defaults to the current repo \u2014 identity, NOT deploy coords").option("--json", "machine-readable output").action(async (repoOrSlug) => {
45012
45181
  const cfg = await loadConfig();
45013
45182
  let target;
45014
45183
  try {
@@ -45024,7 +45193,7 @@ project.command("get [owner/repo]").description("a project's META (board ids + p
45024
45193
  return failGraceful(`org project get: no registry META for ${target} (unknown or unbootstrapped)`);
45025
45194
  }
45026
45195
  console.log(JSON.stringify(read.project));
45027
- if (!o.json) {
45196
+ if (!rawFlag("--json")) {
45028
45197
  const m = read.project;
45029
45198
  const track = resolveReleaseTrack(m, void 0, target);
45030
45199
  const stages = branchesForTrack(track).join(" -> ");
@@ -45050,8 +45219,7 @@ project.command("sync-info [owner/repo]").description("synchronize the owning Gi
45050
45219
  return failGraceful(e.message);
45051
45220
  }
45052
45221
  });
45053
- var projectDeploy = project.command("deploy").description("read nonsecret DEPLOY# facts (domain, port, deploy path, substrate, host presence)");
45054
- projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# facts for one project; defaults to the current repo").addOption(new Option("--stage <stage>", "dev | rc | main").choices(["dev", "rc", "main"])).option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
45222
+ async function runProjectDeployGet(repoOrSlug, o) {
45055
45223
  const cfg = await loadConfig();
45056
45224
  let target;
45057
45225
  try {
@@ -45065,7 +45233,9 @@ projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# fa
45065
45233
  if (stage && !["dev", "rc", "main"].includes(stage)) return fail("org project deploy get: --stage must be dev, rc, or main");
45066
45234
  const payload = stage ? { slug: out.slug, stage, deploy: out.stages[stage] ?? null } : out;
45067
45235
  console.log(JSON.stringify(payload));
45068
- });
45236
+ }
45237
+ var projectDeploy = project.command("deploy").description("read nonsecret DEPLOY# facts (domain, port, deploy path, substrate, host presence)").argument("[ownerRepo]", "shorthand for `get <owner/repo>`").addOption(new Option("--stage <stage>", "dev | rc | main").choices(["dev", "rc", "main"])).option("--json", "machine-readable output").action(async (ownerRepo, o) => runProjectDeployGet(ownerRepo, o));
45238
+ projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# facts for one project; defaults to the current repo").addOption(new Option("--stage <stage>", "dev | rc | main").choices(["dev", "rc", "main"])).option("--json", "machine-readable output").action(async (repoOrSlug, o) => runProjectDeployGet(repoOrSlug, o));
45069
45239
  projectDeploy.command("doctor").description("read-only estate scan for duplicate Hetzner (sshHost, port) DEPLOY# coordinates (#5431); master-only; never auto-reassigns ports").option("--json", "machine-readable output").action(async (o) => {
45070
45240
  const cfg = await loadConfig();
45071
45241
  const report = await fetchDeployPortCollisions(registryClientDeps(cfg));
@@ -45313,11 +45483,13 @@ oauth.command("plan", { isDefault: true }).description("print the canonical JS o
45313
45483
  oc = parseOauthConfig(meta ?? {}, slug);
45314
45484
  } catch (e) {
45315
45485
  const message2 = e.message;
45486
+ const declareCommand = `mmi-cli oracle org project set ${o.repo ?? `mutmutco/${slug}`} --var 'oauth={"subdomains":["${defaultSubdomain(slug)}"],"callbackPath":"${DEFAULT_CALLBACK_PATH}"}'`;
45316
45487
  if (/^oauth is not configured for /.test(message2)) {
45317
- return failGraceful(
45488
+ return failGracefulEnvelope(
45318
45489
  `org oauth plan: ${message2}. Declare it in the registry META first:
45319
- mmi-cli oracle org project set ${o.repo ?? `mutmutco/${slug}`} --var 'oauth={"subdomains":["${defaultSubdomain(slug)}"],"callbackPath":"${DEFAULT_CALLBACK_PATH}"}'
45320
- New repo? The GCP project and its Google Auth Platform consent screen must exist too \u2014 docs/Guides/oauth-provision.md \u2014 New repo.`
45490
+ ${declareCommand}
45491
+ New repo? The GCP project and its Google Auth Platform consent screen must exist too \u2014 docs/Guides/oauth-provision.md \u2014 New repo.`,
45492
+ { code: ERROR_CODES.ERR_STATE_CONFLICT, corrected_command: declareCommand }
45321
45493
  );
45322
45494
  }
45323
45495
  return failGraceful(`org oauth plan: ${message2}`);
@@ -45704,6 +45876,14 @@ function resolveHouseShim(argv) {
45704
45876
  argv.splice(2, 1);
45705
45877
  return;
45706
45878
  }
45879
+ if (actual && isDeclaredHouseAlias(houseToken, lookupPath)) {
45880
+ process.stderr.write(
45881
+ `mmi-cli: '${lookupPath}' lives in house '${actual}', not '${houseToken}' \u2014 routed to \`mmi-cli ${canonicalPathFor(lookupPath)}\`
45882
+ `
45883
+ );
45884
+ argv.splice(2, 1);
45885
+ return;
45886
+ }
45707
45887
  if (actual) {
45708
45888
  refuseHouseShim(
45709
45889
  lookupPath,
@@ -45732,6 +45912,7 @@ function printHouseRootHelp(house) {
45732
45912
  );
45733
45913
  consoleIo.log(lines2.join("\n"));
45734
45914
  }
45915
+ resolveVerbAliasShim(process.argv);
45735
45916
  resolveHouseShim(process.argv);
45736
45917
  program2.parseAsync(process.argv).then(() => finishCliRun()).catch((e) => failGraceful(e.message));
45737
45918
  // Annotate the CommonJS export names for ESM import in node:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.3.37",
3
+ "version": "4.3.39",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",