@mutmutco/cli 4.2.2 → 4.2.4

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 +140 -51
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -3416,7 +3416,7 @@ var program = new Command();
3416
3416
  // src/index.ts
3417
3417
  var import_promises8 = require("node:fs/promises");
3418
3418
  var import_node_fs43 = require("node:fs");
3419
- var import_node_child_process19 = require("node:child_process");
3419
+ var import_node_child_process20 = require("node:child_process");
3420
3420
 
3421
3421
  // src/cli-shared.ts
3422
3422
  var import_node_child_process3 = require("node:child_process");
@@ -11802,10 +11802,10 @@ var rollout_plan_default = {
11802
11802
  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)."
11803
11803
  },
11804
11804
  baseline: {
11805
- version: "4.2.2",
11806
- tag: "v4.2.2",
11807
- commit: "c347a86b94cc",
11808
- npm: "@mutmutco/cli@4.2.2"
11805
+ version: "4.2.4",
11806
+ tag: "v4.2.4",
11807
+ commit: "91aa25abb871",
11808
+ npm: "@mutmutco/cli@4.2.4"
11809
11809
  },
11810
11810
  exitCriterion: "fleet-n-of-n",
11811
11811
  hubOnlyShortcut: "forbidden",
@@ -11822,14 +11822,14 @@ var rollout_plan_default = {
11822
11822
  repo: "mutmutco/mmi-hub",
11823
11823
  role: "canary",
11824
11824
  schedule: "train",
11825
- v3Target: "v4.2.2"
11825
+ v3Target: "v4.2.4"
11826
11826
  }
11827
11827
  ],
11828
11828
  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.",
11829
11829
  rollback: {
11830
11830
  independent: true,
11831
- mechanism: "npm dist-tag latest -> 4.2.2 and redeploy the Hub Lambda from tag v4.2.2 (c347a86b94cc); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
11832
- v3Target: "v4.2.2 (@mutmutco/cli@4.2.2, tag commit c347a86b94cc \u2014 last known-good release carrying the repo-index v4-only contract)"
11831
+ mechanism: "npm dist-tag latest -> 4.2.4 and redeploy the Hub Lambda from tag v4.2.4 (91aa25abb871); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
11832
+ v3Target: "v4.2.4 (@mutmutco/cli@4.2.4, tag commit 91aa25abb871 \u2014 last known-good release carrying the repo-index v4-only contract)"
11833
11833
  }
11834
11834
  },
11835
11835
  {
@@ -24031,6 +24031,11 @@ function renderSweep(r) {
24031
24031
 
24032
24032
  // src/hotfix-apply.ts
24033
24033
  var HOTFIX_RELEASE_WORKFLOWS = ["deploy.yml", "publish.yml"];
24034
+ function hotfixStatusWorkflows(deployModel) {
24035
+ if (deployModel === "registry-publish") return ["publish.yml"];
24036
+ if (deployModel === void 0 || deployModel === "hub-serverless" || deployModel === "tenant-container" || deployModel === "solo-container") return HOTFIX_RELEASE_WORKFLOWS;
24037
+ return [];
24038
+ }
24034
24039
  var HOTFIX_RUN_FIND_ATTEMPTS = 10;
24035
24040
  var HOTFIX_RUN_FIND_DELAY_MS = 15e3;
24036
24041
  var HOTFIX_VERIFY_ATTEMPTS = 5;
@@ -24666,29 +24671,35 @@ async function runHotfixStatus(deps, versionInput) {
24666
24671
  const work = (async () => {
24667
24672
  const ctx = await buildTrainApplyContext(deps);
24668
24673
  await deps.run("git", ["fetch", "origin", "--tags"]);
24674
+ let workflows = HOTFIX_RELEASE_WORKFLOWS;
24675
+ let warnings = [];
24676
+ try {
24677
+ workflows = hotfixStatusWorkflows(await resolveHotfixDeployModel(deps, ctx));
24678
+ } catch (e) {
24679
+ warnings = [`deploy model unverified (${e.message ?? e}) \u2014 probed the default deploy.yml+publish.yml run list, so an unproven run may hold the verdict at unverified`];
24680
+ }
24669
24681
  let tag;
24670
24682
  let version;
24671
- let warnings = [];
24672
24683
  if (versionInput) {
24673
24684
  ({ tag, version } = normalizeHotfixVersion(versionInput));
24674
24685
  } else {
24675
24686
  const latest = await deps.run("git", ["tag", "--list", "v[0-9]*.[0-9]*.[0-9]*", "--merged", "origin/main", "--sort=-v:refname"]).then((out) => out.split("\n").map((s) => s.trim()).find((t) => /^v\d+\.\d+\.\d+$/.test(t)));
24676
24687
  if (latest) {
24677
- const latestFacts = await gatherHotfixFacts(deps, ctx, latest, latest.slice(1));
24688
+ const latestFacts = await gatherHotfixFacts(deps, ctx, latest, latest.slice(1), workflows);
24678
24689
  const latestDerived = deriveHotfixState(latestFacts);
24679
24690
  if (latestDerived.state !== "complete") {
24680
24691
  return { ...ctx, command: "hotfix-status", ...latestFacts, ...latestDerived, warnings };
24681
24692
  }
24682
24693
  }
24683
- const found = await findInFlightHotfixVersion(deps, ctx, latest);
24684
- warnings = supersededHotfixWarnings(found.superseded, latest);
24694
+ const found = await findInFlightHotfixVersion(deps, ctx, latest, workflows);
24695
+ warnings = [...warnings, ...supersededHotfixWarnings(found.superseded, latest)];
24685
24696
  if (found.inFlight) {
24686
24697
  ({ tag, version } = found.inFlight);
24687
24698
  } else {
24688
24699
  ({ tag, version } = await deriveHotfixVersion(deps));
24689
24700
  }
24690
24701
  }
24691
- const facts = await gatherHotfixFacts(deps, ctx, tag, version);
24702
+ const facts = await gatherHotfixFacts(deps, ctx, tag, version, workflows);
24692
24703
  return { ...ctx, command: "hotfix-status", ...facts, ...deriveHotfixState(facts), warnings };
24693
24704
  })();
24694
24705
  let timer;
@@ -24707,7 +24718,7 @@ async function runHotfixStatus(deps, versionInput) {
24707
24718
  if (timer) clearTimeout(timer);
24708
24719
  }
24709
24720
  }
24710
- async function gatherHotfixFacts(deps, ctx, tag, version) {
24721
+ async function gatherHotfixFacts(deps, ctx, tag, version, workflows = HOTFIX_RELEASE_WORKFLOWS) {
24711
24722
  const branchExists = Boolean(clean3(await deps.run("git", ["ls-remote", "origin", `refs/heads/${hotfixBranch(tag)}`])));
24712
24723
  const pr2 = await findHotfixPr(deps, ctx, tag);
24713
24724
  const remoteTag = clean3(await deps.run("git", ["ls-remote", "origin", `refs/tags/${tag}`]));
@@ -24716,7 +24727,7 @@ async function gatherHotfixFacts(deps, ctx, tag, version) {
24716
24727
  const releaseExists = await hotfixReleaseExists(deps, ctx, tag);
24717
24728
  const runs = [];
24718
24729
  if (releaseExists && tagSha) {
24719
- for (const workflow of HOTFIX_RELEASE_WORKFLOWS) {
24730
+ for (const workflow of workflows) {
24720
24731
  try {
24721
24732
  const out = await deps.run("gh", [
24722
24733
  "run",
@@ -24772,7 +24783,7 @@ function supersededHotfixWarnings(superseded, latestMainTag) {
24772
24783
  if (superseded.length === 0) return [];
24773
24784
  return [`skipped superseded hotfix marker(s) ${superseded.join(", ")} \u2014 merged to main but never released and at/below the latest released tag ${latestMainTag ?? "(none)"}; later releases already absorbed them (#976). Not actionable \u2014 run mmi-cli devops hotfix start for a new fix.`];
24774
24785
  }
24775
- async function findInFlightHotfixVersion(deps, ctx, latestMainTag) {
24786
+ async function findInFlightHotfixVersion(deps, ctx, latestMainTag, workflows = HOTFIX_RELEASE_WORKFLOWS) {
24776
24787
  const tags = /* @__PURE__ */ new Set();
24777
24788
  const out = await deps.run("gh", [
24778
24789
  "pr",
@@ -24802,13 +24813,13 @@ async function findInFlightHotfixVersion(deps, ctx, latestMainTag) {
24802
24813
  const fresh = latestMainTag ? sorted.filter((t) => compareHotfixVersions(t, latestMainTag) > 0) : sorted;
24803
24814
  for (const tag of fresh) {
24804
24815
  const version = tag.slice(1);
24805
- const facts = await gatherHotfixFacts(deps, ctx, tag, version);
24816
+ const facts = await gatherHotfixFacts(deps, ctx, tag, version, workflows);
24806
24817
  if (deriveHotfixState(facts).state !== "complete") return { inFlight: { tag, version }, superseded: [] };
24807
24818
  }
24808
24819
  const stale = latestMainTag ? sorted.filter((t) => compareHotfixVersions(t, latestMainTag) <= 0) : [];
24809
24820
  const superseded = [];
24810
24821
  for (const tag of stale) {
24811
- const facts = await gatherHotfixFacts(deps, ctx, tag, tag.slice(1));
24822
+ const facts = await gatherHotfixFacts(deps, ctx, tag, tag.slice(1), workflows);
24812
24823
  if (deriveHotfixState(facts).state !== "complete") superseded.push(tag);
24813
24824
  }
24814
24825
  return { inFlight: null, superseded };
@@ -25152,6 +25163,16 @@ var WAIVABLE_KINDS = [
25152
25163
  "stale-satisfied-by"
25153
25164
  ];
25154
25165
  var TEST_WORK_KIND = "unrequested-test-file";
25166
+ var TEST_FILE_RE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
25167
+ var PY_TEST_FILE_RE = /(?:^|\/)test_[^/]*\.py$|_test\.py$/;
25168
+ function isTestPath(path2) {
25169
+ return typeof path2 === "string" && (TEST_FILE_RE.test(path2) || PY_TEST_FILE_RE.test(path2));
25170
+ }
25171
+ function editsExistingTest(paths, addedPaths = []) {
25172
+ const added = new Set(Array.isArray(addedPaths) ? addedPaths : []);
25173
+ const list = Array.isArray(paths) ? paths : [];
25174
+ return list.some((path2) => isTestPath(path2) && !added.has(path2));
25175
+ }
25155
25176
  function translateGlob(glob) {
25156
25177
  let out = "";
25157
25178
  for (let i = 0; i < glob.length; i += 1) {
@@ -25194,7 +25215,7 @@ function matchedMandatoryGlobs(paths, mandatory) {
25194
25215
  return list.some((path2) => re.test(path2));
25195
25216
  });
25196
25217
  }
25197
- function evaluateTestCommandPolicy({ paths, mandatory, regulated = true, override = null } = {}) {
25218
+ function evaluateTestCommandPolicy({ paths, mandatory, regulated = true, override = null, addedPaths = [] } = {}) {
25198
25219
  const configuredMandatoryCount = mandatoryGlobList(mandatory).length;
25199
25220
  if (!regulated) {
25200
25221
  return {
@@ -25202,18 +25223,23 @@ function evaluateTestCommandPolicy({ paths, mandatory, regulated = true, overrid
25202
25223
  matchedMandatoryGlobs: [],
25203
25224
  matchedMandatoryCount: 0,
25204
25225
  testCommandsAllowed: true,
25226
+ editsExistingTest: false,
25205
25227
  reasonId: null,
25206
25228
  commandClasses: { allowed: [TEST_COMMAND_CLASS], refused: [] }
25207
25229
  };
25208
25230
  }
25209
25231
  const matched = matchedMandatoryGlobs(paths, mandatory);
25210
25232
  const overrideAuthorizes = Array.isArray(override?.kinds) && override.kinds.includes(TEST_WORK_KIND);
25211
- const testCommandsAllowed = matched.length > 0 || overrideAuthorizes;
25233
+ const existingTestEdited = editsExistingTest(paths, addedPaths);
25234
+ const testCommandsAllowed = matched.length > 0 || overrideAuthorizes || existingTestEdited;
25212
25235
  return {
25213
25236
  configuredMandatoryCount,
25214
25237
  matchedMandatoryGlobs: matched,
25215
25238
  matchedMandatoryCount: matched.length,
25216
25239
  testCommandsAllowed,
25240
+ /** #5842: true when the diff edits a test file that already existed — one of the facts that can
25241
+ * authorize the class, named so a refusal can say what was missing. */
25242
+ editsExistingTest: existingTestEdited,
25217
25243
  reasonId: testCommandsAllowed ? null : "test-command-outside-mandatory-zone",
25218
25244
  commandClasses: {
25219
25245
  allowed: testCommandsAllowed ? [TEST_COMMAND_CLASS] : [],
@@ -25291,8 +25317,8 @@ function readOverride(base, cwd) {
25291
25317
 
25292
25318
  // src/test-policy-core.ts
25293
25319
  var POLICY_FILE = "test-policy.json";
25294
- var TEST_RE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
25295
- var PY_TEST_RE = /(?:^|\/)test_[^/]*\.py$|_test\.py$/;
25320
+ var TEST_RE = TEST_FILE_RE;
25321
+ var PY_TEST_RE = PY_TEST_FILE_RE;
25296
25322
  function translate(glob) {
25297
25323
  let out = "";
25298
25324
  for (let i = 0; i < glob.length; i++) {
@@ -25327,7 +25353,7 @@ function translate(glob) {
25327
25353
  function globToRegExp2(glob) {
25328
25354
  return new RegExp(`^${translate(glob)}$`);
25329
25355
  }
25330
- function isTestPath(path2) {
25356
+ function isTestPath2(path2) {
25331
25357
  return TEST_RE.test(path2) || PY_TEST_RE.test(path2);
25332
25358
  }
25333
25359
  var SUPPORTED_SOURCE = /\.[cm]?[jt]s$/;
@@ -25561,7 +25587,7 @@ function annotateChangeMeaning(changed, policy, read) {
25561
25587
  const matchers = (policy.mandatory ?? []).map((m) => globToRegExp2(m.glob));
25562
25588
  return changed.map((file) => {
25563
25589
  if (file.status !== "M" || !SUPPORTED_SOURCE.test(file.path)) return file;
25564
- if (!matchers.some((re) => re.test(file.path)) && !isTestPath(file.path)) return file;
25590
+ if (!matchers.some((re) => re.test(file.path)) && !isTestPath2(file.path)) return file;
25565
25591
  let pair;
25566
25592
  try {
25567
25593
  pair = read(file.path);
@@ -25598,7 +25624,7 @@ function removedPaths(changed) {
25598
25624
  function classify(changed, policy, present = () => false) {
25599
25625
  const matchers = (policy.mandatory ?? []).map((m) => ({ ...m, re: globToRegExp2(m.glob) }));
25600
25626
  const mandatoryHits = changed.filter((f) => matchers.some((m) => m.re.test(f.path)));
25601
- const testChanges = changed.filter((f) => isTestPath(f.path));
25627
+ const testChanges = changed.filter((f) => isTestPath2(f.path));
25602
25628
  const addedTests = testChanges.filter((f) => f.status === "A");
25603
25629
  const removed = removedPaths(changed);
25604
25630
  const discharged = (m) => (m.satisfiedBy?.length ?? 0) > 0 && m.satisfiedBy.every((p) => present(p) && !removed.has(p));
@@ -25787,6 +25813,9 @@ function runTestPolicy(root, deps = {}) {
25787
25813
  const findings = blocking.length > 0 ? blocking : sift(evaluate(changed, policy, present));
25788
25814
  const commandPolicy = evaluateTestCommandPolicy({
25789
25815
  paths: changed.map((f) => f.path),
25816
+ // #5842: `A` is added, `R` is a rename whose new name did not exist at the base either. Both are
25817
+ // test work this diff CREATED, which only a waiver may authorize (#5804).
25818
+ addedPaths: changed.filter((f) => f.status === "A" || f.status === "R").map((f) => f.path),
25790
25819
  mandatory: policy.mandatory,
25791
25820
  regulated: policy.declared !== false,
25792
25821
  override: override && lookup.refusals.length === 0 ? { kinds: override.kinds } : null
@@ -32419,7 +32448,53 @@ async function fetchRestCorePool(gh = defaultGhApi) {
32419
32448
  }
32420
32449
 
32421
32450
  // src/pr-create-docs-check.ts
32451
+ var import_node_child_process18 = require("node:child_process");
32422
32452
  var GIT_TIMEOUT_MS2 = 15e3;
32453
+ function catFileBatch(root, ref, paths) {
32454
+ if (paths.length === 0) return Promise.resolve([]);
32455
+ return new Promise((resolve6) => {
32456
+ const chunks = [];
32457
+ let settled = false;
32458
+ const child2 = (0, import_node_child_process18.spawn)("git", ["-C", root, "cat-file", "--batch", "--buffer"], { windowsHide: true });
32459
+ const finish = () => {
32460
+ if (settled) return;
32461
+ settled = true;
32462
+ clearTimeout(timer);
32463
+ resolve6(parseCatFileBatch(Buffer.concat(chunks), paths.length));
32464
+ };
32465
+ const timer = setTimeout(() => {
32466
+ child2.kill("SIGKILL");
32467
+ finish();
32468
+ }, GIT_TIMEOUT_MS2);
32469
+ timer.unref?.();
32470
+ child2.stdout.on("data", (c) => chunks.push(c));
32471
+ child2.on("error", finish);
32472
+ child2.stdin.on("error", () => {
32473
+ });
32474
+ child2.on("close", finish);
32475
+ child2.stdin.end(paths.map((path2) => `${ref}:${path2}
32476
+ `).join(""));
32477
+ });
32478
+ }
32479
+ function parseCatFileBatch(out, expected) {
32480
+ const results = [];
32481
+ let at = 0;
32482
+ while (results.length < expected) {
32483
+ const eol = out.indexOf(10, at);
32484
+ if (eol === -1) break;
32485
+ const header = out.toString("utf8", at, eol);
32486
+ at = eol + 1;
32487
+ if (header.endsWith(" missing")) {
32488
+ results.push(void 0);
32489
+ continue;
32490
+ }
32491
+ const size = Number(header.slice(header.lastIndexOf(" ") + 1));
32492
+ if (!Number.isInteger(size) || size < 0 || at + size > out.length) break;
32493
+ results.push(out.toString("utf8", at, at + size));
32494
+ at = at + size + 1;
32495
+ }
32496
+ return results;
32497
+ }
32423
32498
  function createPrCreateDocsIndexDeps() {
32424
32499
  return {
32425
32500
  worktreeRoot: async () => {
@@ -32448,20 +32523,13 @@ function createPrCreateDocsIndexDeps() {
32448
32523
  },
32449
32524
  listDocsAtRef: async (root, ref) => {
32450
32525
  try {
32451
- const { stdout } = await execFileP2("git", ["-C", root, "ls-tree", "-r", "--name-only", ref, "--", "docs"], { timeout: GIT_TIMEOUT_MS2 });
32452
- return stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
32526
+ const { stdout } = await execFileP2("git", ["-C", root, "ls-tree", "-r", "-z", "--name-only", ref, "--", "docs"], { timeout: GIT_TIMEOUT_MS2 });
32527
+ return stdout.split("\0").filter(Boolean);
32453
32528
  } catch {
32454
32529
  return [];
32455
32530
  }
32456
32531
  },
32457
- readAtRef: async (root, ref, path2) => {
32458
- try {
32459
- const { stdout } = await execFileP2("git", ["-C", root, "show", `${ref}:${path2}`], { timeout: GIT_TIMEOUT_MS2 });
32460
- return stdout;
32461
- } catch {
32462
- return void 0;
32463
- }
32464
- }
32532
+ readManyAtRef: (root, ref, paths) => catFileBatch(root, ref, paths)
32465
32533
  };
32466
32534
  }
32467
32535
  async function checkDocsIndexAtHead(opts, deps) {
@@ -32476,10 +32544,10 @@ async function checkDocsIndexAtHead(opts, deps) {
32476
32544
  const paths = await deps.listDocsAtRef(root, ref);
32477
32545
  if (!paths.includes(DOCS_INDEX_PATH)) return void 0;
32478
32546
  const relDocs = paths.filter((p) => p.startsWith("docs/") && p.endsWith(".md")).map((p) => p.slice("docs/".length)).filter(isRoutableDocsPath);
32479
- const [indexContent, docContents] = await Promise.all([
32480
- deps.readAtRef(root, ref, DOCS_INDEX_PATH),
32481
- Promise.all(relDocs.map((rel) => deps.readAtRef(root, ref, `docs/${rel}`)))
32482
- ]);
32547
+ const contents = await deps.readManyAtRef(root, ref, [DOCS_INDEX_PATH, ...relDocs.map((rel) => `docs/${rel}`)]);
32548
+ const [indexContent, ...docContents] = contents;
32549
+ if (contents.length !== relDocs.length + 1) return void 0;
32550
+ if (indexContent === void 0 || docContents.some((c) => c === void 0)) return void 0;
32483
32551
  const contentByPath = new Map(relDocs.map((rel, i) => [rel, docContents[i] ?? ""]));
32484
32552
  const atRefDeps = {
32485
32553
  listDocs: () => relDocs,
@@ -32495,7 +32563,7 @@ async function checkDocsIndexAtHead(opts, deps) {
32495
32563
  }
32496
32564
  return {
32497
32565
  ok: false,
32498
- detail: `${DOCS_INDEX_PATH} is stale at the commit this PR would open from \u2014 it no longer matches the docs/ tree there`,
32566
+ detail: `${DOCS_INDEX_PATH} is stale at the commit this PR would open from \u2014 it no longer matches the docs/ tree there (checked ${ref} over ${relDocs.length} indexed doc${relDocs.length === 1 ? "" : "s"})`,
32499
32567
  fix: "run `mmi-cli oracle docs index --write`, commit docs/index.md, and push again before retrying `pr create`"
32500
32568
  };
32501
32569
  }
@@ -36637,6 +36705,9 @@ function diagnoseSurface(evidence) {
36637
36705
  if (evidence.manifest === "invalid") return { ...base, state: "corrupt" };
36638
36706
  if (!evidence.releasedVersion) return { ...base, state: "freshness-unknown" };
36639
36707
  if (evidence.installedVersion && evidence.releasedVersion && compareVersions(evidence.installedVersion, evidence.releasedVersion) < 0) {
36708
+ if (evidence.stagedVersion && evidence.stagedVersion === evidence.releasedVersion) {
36709
+ return { ...base, state: "staged" };
36710
+ }
36640
36711
  return { ...base, state: "stale" };
36641
36712
  }
36642
36713
  return { ...base, state: "clean" };
@@ -36647,14 +36718,16 @@ function reloadInstruction(descriptor) {
36647
36718
  return `${verb} ${descriptor.displayName}`;
36648
36719
  }
36649
36720
  function planSurfaceRepair(diagnosis) {
36650
- if (diagnosis.state === "clean" || diagnosis.state === "pending-reload" || diagnosis.state === "skipped" || diagnosis.state === "freshness-unknown") return null;
36721
+ if (diagnosis.state === "clean" || diagnosis.state === "pending-reload" || diagnosis.state === "staged" || diagnosis.state === "skipped" || diagnosis.state === "freshness-unknown") return null;
36651
36722
  const descriptor = diagnosis.descriptor;
36652
36723
  if (descriptor.repairOwner === "mmi-hub") {
36653
36724
  return {
36654
36725
  supported: false,
36655
36726
  owner: "mmi-hub",
36656
36727
  changes: [],
36657
- instruction: `run \`mmi-hub update\` to converge the MMI-managed Hermes plugin, then ${reloadInstruction(descriptor)}`
36728
+ // #5833: name the SURFACE this row is about — the literal "Hermes plugin" sent every other
36729
+ // mmi-hub-owned surface (Claude Code included) chasing a different host that was already current.
36730
+ instruction: `run \`mmi-hub update\` to converge the MMI-managed ${descriptor.displayName} plugin, then ${reloadInstruction(descriptor)}`
36658
36731
  };
36659
36732
  }
36660
36733
  if (descriptor.repairOwner !== "mmi-cli") {
@@ -36681,6 +36754,8 @@ function buildSurfaceDoctorCheck(diagnosis) {
36681
36754
  skipped: "skipped \u2014 host not active",
36682
36755
  clean: diagnosis.repairDetail ? `clean \u2014 repaired and verified (${diagnosis.repairDetail})` : `clean${versions ? ` \u2014 ${versions}` : ""}`,
36683
36756
  "pending-reload": `pending ${descriptor.token === "hermes" ? "fresh process" : descriptor.reload === "workspace" ? "reload" : "restart"} \u2014 repaired to ${diagnosis.releasedVersion}, but ${diagnosis.installedVersion} is still the version this host has loaded${diagnosis.repairDetail ? ` (${diagnosis.repairDetail})` : ""}`,
36757
+ // #5833: the update already staged the released tree; the wording mirrors `mmi-hub status`.
36758
+ staged: `loaded ${diagnosis.installedVersion}; staged ${diagnosis.releasedVersion} for next launch \u2014 ${descriptor.token === "hermes" ? "start a fresh process" : descriptor.reload === "workspace" ? "reload" : "restart"} ${descriptor.displayName} to converge`,
36684
36759
  "freshness-unknown": `${diagnosis.installedVersion ?? "installed"} \u2014 freshness UNKNOWN, the published version could not be read`,
36685
36760
  stale: `stale${versions ? ` \u2014 ${versions}` : ""}`,
36686
36761
  missing: "missing \u2014 no install record",
@@ -36692,14 +36767,14 @@ function buildSurfaceDoctorCheck(diagnosis) {
36692
36767
  id: `${descriptor.token}-plugin`,
36693
36768
  surface: descriptor.token,
36694
36769
  state,
36695
- ok: state === "clean" || state === "skipped" || state === "pending-reload",
36770
+ ok: state === "clean" || state === "skipped" || state === "pending-reload" || state === "staged",
36696
36771
  ...state === "freshness-unknown" ? { reportOnly: true } : {},
36697
- // Nothing is broken, so this is not a ✗ — but it is the one OK row an operator must act on, and the
36772
+ // Nothing is broken, so this is not a ✗ — but these are the OK rows an operator must act on, and the
36698
36773
  // short default lane and the SessionStart banner both filter to failures unless a row says otherwise.
36699
- ...state === "pending-reload" ? { warn: true } : {},
36774
+ ...state === "pending-reload" || state === "staged" ? { warn: true } : {},
36700
36775
  label: `${descriptor.displayName} plugin`,
36701
36776
  detail: detailByState[state],
36702
- ...plan ? { fix: plan.instruction } : state === "pending-reload" ? { fix: reloadInstruction(descriptor) } : state === "freshness-unknown" ? { fix: "check `mmi-cli --version` against `npm view @mutmutco/cli version`, then rerun doctor" } : {},
36777
+ ...plan ? { fix: plan.instruction } : state === "pending-reload" || state === "staged" ? { fix: reloadInstruction(descriptor) } : state === "freshness-unknown" ? { fix: "check `mmi-cli --version` against `npm view @mutmutco/cli version`, then rerun doctor" } : {},
36703
36778
  verbose: [
36704
36779
  // The three numbers the legacy builder used to print. They belong here now that this is the only
36705
36780
  // plugin row, and a report that names a state without naming the versions behind it is not evidence.
@@ -37807,7 +37882,7 @@ function ghAccountCaveat(announcedLogin, accounts) {
37807
37882
  var import_node_fs42 = require("node:fs");
37808
37883
  var import_node_os18 = require("node:os");
37809
37884
  var import_node_path39 = require("node:path");
37810
- var import_node_child_process18 = require("node:child_process");
37885
+ var import_node_child_process19 = require("node:child_process");
37811
37886
  var import_node_util8 = require("node:util");
37812
37887
 
37813
37888
  // src/discard-sink.ts
@@ -37816,13 +37891,13 @@ function nodeDiscardSinkPath(platform2 = process.platform) {
37816
37891
  }
37817
37892
 
37818
37893
  // src/doctor-io.ts
37819
- var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process18.execFile);
37894
+ var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process19.execFile);
37820
37895
  function execFileCapture(file, args, opts = {}) {
37821
37896
  const sink = nodeDiscardSinkPath();
37822
37897
  const inFd = (0, import_node_fs42.openSync)(sink, "r");
37823
37898
  const errFd = (0, import_node_fs42.openSync)(sink, "w");
37824
37899
  try {
37825
- return (0, import_node_child_process18.execFileSync)(file, args, {
37900
+ return (0, import_node_child_process19.execFileSync)(file, args, {
37826
37901
  ...opts,
37827
37902
  encoding: "utf8",
37828
37903
  stdio: [inFd, "pipe", errFd]
@@ -37833,6 +37908,18 @@ function execFileCapture(file, args, opts = {}) {
37833
37908
  }
37834
37909
  }
37835
37910
  var MMI_PLUGIN_ID2 = "mmi@mutmutco";
37911
+ function stagedClaudePluginVersion() {
37912
+ try {
37913
+ const stagingRoot = (0, import_node_path39.join)((0, import_node_os18.homedir)(), ".claude", "plugins", "staging");
37914
+ const record = JSON.parse((0, import_node_fs42.readFileSync)((0, import_node_path39.join)(stagingRoot, "launch-target.json"), "utf8"));
37915
+ if (typeof record.target !== "string" || typeof record.incoming !== "string") return void 0;
37916
+ const manifest = JSON.parse((0, import_node_fs42.readFileSync)((0, import_node_path39.join)(record.incoming, ".claude-plugin", "plugin.json"), "utf8"));
37917
+ if (typeof manifest.version !== "string") return void 0;
37918
+ return manifest.version === record.target ? record.target : void 0;
37919
+ } catch {
37920
+ return void 0;
37921
+ }
37922
+ }
37836
37923
  function installedClaudePluginVersion() {
37837
37924
  try {
37838
37925
  const file = JSON.parse(
@@ -38091,6 +38178,7 @@ function mmiDoctorDeps(opts = {}) {
38091
38178
  const hermes = token === "hermes" ? readHermesPluginEvidence(process.env) : void 0;
38092
38179
  const kimi = token === "kimi" ? kimiPluginHostEvidence(surfaceConfigRoot("kimi")) : void 0;
38093
38180
  const installedVersion = hermes?.installedVersion ?? installedSurfacePluginVersion(runtimeSurface);
38181
+ const stagedVersion = token === "claude" ? stagedClaudePluginVersion() : void 0;
38094
38182
  surfaceEvidence = {
38095
38183
  descriptor,
38096
38184
  // Hermes' root is the host evidence: a configured but uninstalled MMI tree is missing, while an
@@ -38102,7 +38190,8 @@ function mmiDoctorDeps(opts = {}) {
38102
38190
  manifest: hermes?.manifest ?? (installedVersion ? "valid" : snapshot.installRecordPresent ? "invalid" : "missing"),
38103
38191
  guardState: buildPluginGuardDecision(snapshot).state,
38104
38192
  ...kimi?.receipt ? { receipt: kimi.receipt } : {},
38105
- ...installedVersion ? { installedVersion } : {}
38193
+ ...installedVersion ? { installedVersion } : {},
38194
+ ...stagedVersion ? { stagedVersion } : {}
38106
38195
  };
38107
38196
  return surfaceEvidence;
38108
38197
  };
@@ -38751,7 +38840,7 @@ function scheduleRelatedDiscovery(o) {
38751
38840
  try {
38752
38841
  const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body, "--fail-soft"];
38753
38842
  if (o.repo) args.push("--repo", o.repo);
38754
- spawnDetachedSelf(args, { spawn: import_node_child_process19.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
38843
+ spawnDetachedSelf(args, { spawn: import_node_child_process20.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
38755
38844
  } catch {
38756
38845
  }
38757
38846
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.2.2",
3
+ "version": "4.2.4",
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",