@mutmutco/cli 4.1.20 → 4.2.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 +297 -88
  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_fs42 = require("node:fs");
3419
- var import_node_child_process17 = require("node:child_process");
3419
+ var import_node_child_process18 = require("node:child_process");
3420
3420
 
3421
3421
  // src/cli-shared.ts
3422
3422
  var import_node_child_process3 = require("node:child_process");
@@ -5538,7 +5538,7 @@ function commandLadderHint() {
5538
5538
  }
5539
5539
 
5540
5540
  // src/index.ts
5541
- var import_node_path38 = require("node:path");
5541
+ var import_node_path39 = require("node:path");
5542
5542
 
5543
5543
  // src/merge-ci-policy.ts
5544
5544
  function resolveMergeCiPolicy(input) {
@@ -7287,6 +7287,7 @@ function parseOwnerRepo(repo) {
7287
7287
  if (owner.includes("\\") || name.includes("\\")) throw new Error("repo must be owner/repo");
7288
7288
  return { owner, name, slug: name.toLowerCase(), fullName: `${owner}/${name}` };
7289
7289
  }
7290
+ var MANAGED_DELETE_BRANCH_ON_MERGE = false;
7290
7291
  var DEFAULT_GATE_CMD = "npm run check";
7291
7292
  var DEFAULT_GATE_MAX_SECONDS = "300";
7292
7293
  var DEFAULT_GATE_PY_VERSION = "3.11";
@@ -11761,10 +11762,10 @@ var rollout_plan_default = {
11761
11762
  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)."
11762
11763
  },
11763
11764
  baseline: {
11764
- version: "4.1.20",
11765
- tag: "v4.1.20",
11766
- commit: "12c94bf54a89",
11767
- npm: "@mutmutco/cli@4.1.20"
11765
+ version: "4.2.0",
11766
+ tag: "v4.2.0",
11767
+ commit: "d4802646ffca",
11768
+ npm: "@mutmutco/cli@4.2.0"
11768
11769
  },
11769
11770
  exitCriterion: "fleet-n-of-n",
11770
11771
  hubOnlyShortcut: "forbidden",
@@ -11781,14 +11782,14 @@ var rollout_plan_default = {
11781
11782
  repo: "mutmutco/mmi-hub",
11782
11783
  role: "canary",
11783
11784
  schedule: "train",
11784
- v3Target: "v4.1.20"
11785
+ v3Target: "v4.2.0"
11785
11786
  }
11786
11787
  ],
11787
11788
  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.",
11788
11789
  rollback: {
11789
11790
  independent: true,
11790
- mechanism: "npm dist-tag latest -> 4.1.20 and redeploy the Hub Lambda from tag v4.1.20 (12c94bf54a89); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
11791
- v3Target: "v4.1.20 (@mutmutco/cli@4.1.20, tag commit 12c94bf54a89 \u2014 last known-good release carrying the repo-index v4-only contract)"
11791
+ mechanism: "npm dist-tag latest -> 4.2.0 and redeploy the Hub Lambda from tag v4.2.0 (d4802646ffca); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
11792
+ v3Target: "v4.2.0 (@mutmutco/cli@4.2.0, tag commit d4802646ffca \u2014 last known-good release carrying the repo-index v4-only contract)"
11792
11793
  }
11793
11794
  },
11794
11795
  {
@@ -15824,7 +15825,7 @@ function assertNpmMajorPreflight(opts) {
15824
15825
  if (localMajor === void 0 || expectedMajor === void 0) return;
15825
15826
  if (localMajor === expectedMajor) return;
15826
15827
  throw new Error(
15827
- `${opts.repo}: Step 0a npm-major preflight failed \u2014 local npm ${localNpm} (major ${localMajor}) \u2260 CI npm ${expectedNpm} (major ${expectedMajor}). Align local npm before --apply: \`npm install -g npm@${expectedNpm}\`. This is an npm-major mismatch (#5666/#5616/#4578), not a lockfile or publish-surface problem. When gate.yml has no bundled npm pin, read the publish workflow npm pin; otherwise compare \`npm -v\` with the green gate log's toolchain-preflight line (#3446).`
15828
+ `${opts.repo}: Step 0a npm-major preflight failed \u2014 local npm ${localNpm} (major ${localMajor}) \u2260 CI npm ${expectedNpm} (major ${expectedMajor}). Supported repair (preserves Node): \`npm install -g npm@${expectedNpm}\`; then verify with \`node -v && npm -v\` and rerun the same release command. This is an npm-major mismatch (#5666/#5616/#4578), not a lockfile or publish-surface problem. When gate.yml has no bundled npm pin, read the publish workflow npm pin; otherwise compare \`npm -v\` with the green gate log's toolchain-preflight line (#3446).`
15828
15829
  );
15829
15830
  }
15830
15831
  async function assertNpmMajorPreflightFromWorkflows(deps, repo) {
@@ -16108,15 +16109,9 @@ async function runRcandResume(deps, options = {}) {
16108
16109
  note: `reused immutable ${tag}; pushed its exact SHA to rc and dispatched the rc deploy`
16109
16110
  };
16110
16111
  }
16111
- async function runReleaseResume(deps, options = {}) {
16112
+ async function runReleaseResumePipeline(deps, options, prepared) {
16112
16113
  const watch = options.watch ?? false;
16113
- const ctx = await buildTrainApplyContext(deps);
16114
- await requireCleanTree(deps);
16115
- await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
16116
- const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
16117
- const branchHints = await loadReleaseTrackBranchHints(deps);
16118
- const directTrack = isHubControlRepo(ctx.repo) || resolveReleaseTrack(meta, branchHints) === "direct";
16119
- const deployModel = await preflight(deps, ctx, "main", meta);
16114
+ const { ctx, meta, branchHints, directTrack, deployModel } = prepared;
16120
16115
  const tags = clean2(await deps.run("git", ["tag", "--list", "v[0-9]*.[0-9]*.[0-9]*", "--sort=-v:refname"])).split("\n").map((t) => t.trim()).filter((t) => /^v\d+\.\d+\.\d+$/.test(t));
16121
16116
  const tag = tags[0];
16122
16117
  if (!tag) throw new Error("release --resume: no final v* release tag found \u2014 there is no release to resume");
@@ -16224,6 +16219,36 @@ async function runReleaseResume(deps, options = {}) {
16224
16219
  note: followUp === "failed" ? `resumed ${tag} (version preserved, not re-cut) \u2014 but a watched deploy/publish run FAILED; the Release stands, re-run the failed job (do NOT re-tag)` : followUp === "pending" ? `resumed ${tag} (version preserved, not re-cut) \u2014 a deploy/publish or alignment follow-up is unresolved; watch it to completion` : `resumed and completed ${tag} \u2014 the original version was preserved, not re-cut`
16225
16220
  };
16226
16221
  }
16222
+ async function withCheckoutRestoredAfterReleaseResume(deps, startBranch, operation) {
16223
+ let result;
16224
+ try {
16225
+ result = await operation();
16226
+ } catch (error) {
16227
+ let checkoutNote;
16228
+ try {
16229
+ checkoutNote = (await restoreCheckoutAfterRelease(deps, startBranch)).note;
16230
+ } catch (restoreError) {
16231
+ checkoutNote = `checkout restoration failed while returning to ${startBranch}: ${restoreError instanceof Error ? restoreError.message : String(restoreError)}`;
16232
+ }
16233
+ throw new Error(`${error instanceof Error ? error.message : String(error)}; ${checkoutNote}`);
16234
+ }
16235
+ return withCheckoutRestoredAfterRelease(deps, result, startBranch);
16236
+ }
16237
+ async function runReleaseResume(deps, options = {}) {
16238
+ const ctx = await buildTrainApplyContext(deps);
16239
+ await requireCleanTree(deps);
16240
+ await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
16241
+ const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
16242
+ const branchHints = await loadReleaseTrackBranchHints(deps);
16243
+ const directTrack = isHubControlRepo(ctx.repo) || resolveReleaseTrack(meta, branchHints) === "direct";
16244
+ const deployModel = await preflight(deps, ctx, "main", meta);
16245
+ const startBranch = directTrack ? "development" : "rc";
16246
+ return withCheckoutRestoredAfterReleaseResume(
16247
+ deps,
16248
+ startBranch,
16249
+ () => runReleaseResumePipeline(deps, options, { ctx, meta, branchHints, directTrack, deployModel })
16250
+ );
16251
+ }
16227
16252
  async function runReleaseAbort(deps, options = {}) {
16228
16253
  if (!options.approved) {
16229
16254
  throw new Error("release --abort requires --apply after explicit approval; nothing was written");
@@ -17130,7 +17155,7 @@ async function auditRepoCi(repo, deps) {
17130
17155
  ok: info.allow_auto_merge === true,
17131
17156
  label: "allow_auto_merge enabled",
17132
17157
  detail: info.allow_auto_merge === true ? void 0 : "false or unavailable",
17133
- remediation: `gh api -X PATCH repos/${repo} -f allow_auto_merge=true -f allow_squash_merge=true -f delete_branch_on_merge=true`
17158
+ remediation: `gh api -X PATCH repos/${repo} -f allow_auto_merge=true -f allow_squash_merge=true -F delete_branch_on_merge=${MANAGED_DELETE_BRANCH_ON_MERGE}`
17134
17159
  });
17135
17160
  checks.push({
17136
17161
  ok: info.allow_squash_merge === true,
@@ -17138,9 +17163,9 @@ async function auditRepoCi(repo, deps) {
17138
17163
  detail: info.allow_squash_merge === true ? void 0 : "false or unavailable"
17139
17164
  });
17140
17165
  checks.push({
17141
- ok: info.delete_branch_on_merge === true,
17142
- label: "delete_branch_on_merge enabled",
17143
- detail: info.delete_branch_on_merge === true ? void 0 : "false or unavailable"
17166
+ ok: info.delete_branch_on_merge === MANAGED_DELETE_BRANCH_ON_MERGE,
17167
+ label: "delete_branch_on_merge disabled",
17168
+ detail: info.delete_branch_on_merge === MANAGED_DELETE_BRANCH_ON_MERGE ? void 0 : "merged branches must survive until delayed cleanup proves no open PR uses them as a base (#5789)"
17144
17169
  });
17145
17170
  checks.push({
17146
17171
  ok: info.has_wiki === false,
@@ -17447,11 +17472,11 @@ async function applyCiReconcileMergeSettingsFromReport(repo, deps, report) {
17447
17472
  body: {
17448
17473
  allow_auto_merge: true,
17449
17474
  allow_squash_merge: true,
17450
- delete_branch_on_merge: true,
17475
+ delete_branch_on_merge: MANAGED_DELETE_BRANCH_ON_MERGE,
17451
17476
  has_wiki: false
17452
17477
  }
17453
17478
  });
17454
- applied.push("allow_auto_merge, allow_squash_merge, delete_branch_on_merge, has_wiki=false");
17479
+ applied.push("allow_auto_merge, allow_squash_merge, preserve merged remote branches, has_wiki=false");
17455
17480
  } catch (e) {
17456
17481
  errors.push(e.message);
17457
17482
  }
@@ -20892,6 +20917,11 @@ async function readBoard(options, deps = {}) {
20892
20917
  report[scope].claimable = filtered.claimable;
20893
20918
  report.warnings.push(...filtered.warnings);
20894
20919
  }
20920
+ for (const scope of ["primary", "secondary"]) {
20921
+ const demoted = await demoteLiveClaimedClaimables(report[scope], client);
20922
+ report.warnings.push(...demoted.warnings);
20923
+ report.partial = report.partial || demoted.partial;
20924
+ }
20895
20925
  return report;
20896
20926
  }
20897
20927
  function findBoardItem(items, selector, board) {
@@ -21753,6 +21783,66 @@ function boardItemClaim(item) {
21753
21783
  currentlyClaimed: item.assignees.length > 0 && item.status === "In Progress"
21754
21784
  };
21755
21785
  }
21786
+ function classifyClaimableMarker(comments, actor, now = Date.now()) {
21787
+ const marker = comments ? latestClaimMarker(comments) : void 0;
21788
+ if (!marker) return { verdict: "claimable" };
21789
+ const ownership = laneOwnership(marker, actor);
21790
+ if (ownership === "mine") return { verdict: "owned", holder: describeClaimMarker(marker) };
21791
+ const age = now - Date.parse(marker.ts);
21792
+ if (Number.isFinite(age) && age >= 0 && age < CLAIM_MARKER_LIVE_MS) {
21793
+ return { verdict: "taken", holder: describeClaimMarker(marker) };
21794
+ }
21795
+ return { verdict: "claimable" };
21796
+ }
21797
+ async function demoteLiveClaimedClaimables(scope, client, actor = describeSessionIdentity(), deps = {}) {
21798
+ const warnings = [];
21799
+ let partial = false;
21800
+ const fetchComments = deps.fetchComments ?? (async (item) => {
21801
+ if (item.details) return item.details.comments;
21802
+ const comments = await client.restPaginate(`repos/${item.repository}/issues/${item.number}/comments`);
21803
+ return comments.map((comment) => ({ body: comment.body ?? "" }));
21804
+ });
21805
+ const stillClaimable = [];
21806
+ const movedTaken = [];
21807
+ const movedOwned = [];
21808
+ for (const item of scope.claimable) {
21809
+ if (item.contentType !== "Issue") {
21810
+ stillClaimable.push(item);
21811
+ continue;
21812
+ }
21813
+ let comments;
21814
+ try {
21815
+ comments = await fetchComments(item);
21816
+ } catch {
21817
+ comments = void 0;
21818
+ }
21819
+ if (comments === void 0) {
21820
+ partial = true;
21821
+ warnings.push(`${item.ref} left claimable: claim-marker evidence could not be read`);
21822
+ continue;
21823
+ }
21824
+ const verdict = classifyClaimableMarker(comments, actor, deps.now);
21825
+ if (verdict.verdict === "claimable") {
21826
+ stillClaimable.push(item);
21827
+ } else if (verdict.verdict === "owned") {
21828
+ movedOwned.push(item);
21829
+ warnings.push(`${item.ref} is held by your own lane (${verdict.holder}) \u2014 moved to user-owned`);
21830
+ } else {
21831
+ movedTaken.push(item);
21832
+ warnings.push(`${item.ref} is held by a live claim marker (lane ${verdict.holder}) \u2014 no longer claimable`);
21833
+ }
21834
+ }
21835
+ scope.claimable = stillClaimable;
21836
+ if (movedTaken.length) {
21837
+ scope.taken.push(...movedTaken);
21838
+ scope.taken.sort(compareItems);
21839
+ }
21840
+ if (movedOwned.length) {
21841
+ scope.userOwned.push(...movedOwned);
21842
+ scope.userOwned.sort(compareItems);
21843
+ }
21844
+ return { warnings, partial };
21845
+ }
21756
21846
  async function postClaimMarkerComment(client, item, actor = describeSessionIdentity()) {
21757
21847
  try {
21758
21848
  const marker = {
@@ -23374,11 +23464,69 @@ function renderPluginCachePlan(plan, applied) {
23374
23464
  return lines.join("\n");
23375
23465
  }
23376
23466
 
23377
- // src/statusline-invalidate.ts
23467
+ // src/train-self-converge.ts
23378
23468
  var import_node_child_process10 = require("node:child_process");
23469
+ var SELF_CONVERGE_INSTALL_TIMEOUT_MS = 12e4;
23470
+ var SELF_CONVERGE_VERSION_TIMEOUT_MS = 3e4;
23471
+ var isWin2 = process.platform === "win32";
23472
+ var defaultDeps = {
23473
+ // Pin the exact released version (never `@latest` — a cached packument resolves the tag to the
23474
+ // PREVIOUS version in exactly this post-publish window, #3422). execFileP rejects on nonzero exit.
23475
+ update: async (target) => {
23476
+ const args = ["install", "-g", `@mutmutco/cli@${target}`];
23477
+ if (isWin2) {
23478
+ await execFileP2("cmd.exe", ["/c", "npm", ...args], { timeout: SELF_CONVERGE_INSTALL_TIMEOUT_MS });
23479
+ } else {
23480
+ await execFileP2("npm", args, { timeout: SELF_CONVERGE_INSTALL_TIMEOUT_MS });
23481
+ }
23482
+ },
23483
+ // Read the version the freshly installed `mmi-cli` on PATH actually resolves to. Unparseable or
23484
+ // failed read answers undefined so the flow reports not-taken-effect instead of guessing green.
23485
+ readInstalledVersion: async () => {
23486
+ try {
23487
+ const { stdout } = isWin2 ? await execFileP2("cmd.exe", ["/c", "mmi-cli", "--version"], { timeout: SELF_CONVERGE_VERSION_TIMEOUT_MS }) : await execFileP2("mmi-cli", ["--version"], { timeout: SELF_CONVERGE_VERSION_TIMEOUT_MS });
23488
+ return parseNpmVersion(stdout);
23489
+ } catch {
23490
+ return void 0;
23491
+ }
23492
+ },
23493
+ // Re-exec the SAME command line in the fresh CLI with inherited stdio (spawn, not execFile, so
23494
+ // output streams live) and hand its exit code back — the stale parent exits with it (#5762).
23495
+ reexec: () => new Promise((resolve6) => {
23496
+ const args = process.argv.slice(2);
23497
+ const child2 = isWin2 ? (0, import_node_child_process10.spawn)("cmd.exe", ["/c", "mmi-cli", ...args], { stdio: "inherit", windowsHide: true }) : (0, import_node_child_process10.spawn)("mmi-cli", args, { stdio: "inherit", windowsHide: true });
23498
+ child2.on("exit", (code) => resolve6(code ?? 1));
23499
+ child2.on("error", () => resolve6(1));
23500
+ }),
23501
+ // stderr only — stdout may be carrying --json output.
23502
+ warn: (line) => console.error(line)
23503
+ };
23504
+ async function selfConvergeTrainCli(input) {
23505
+ const deps = { ...defaultDeps, ...input.deps };
23506
+ try {
23507
+ deps.warn(
23508
+ `train: running mmi-cli ${input.currentVersion} is stale against released ${input.releasedVersion} \u2014 self-converging via npm install -g @mutmutco/cli@${input.releasedVersion} before resuming ${input.commandName}`
23509
+ );
23510
+ await deps.update(input.releasedVersion);
23511
+ const installed = await deps.readInstalledVersion();
23512
+ if (!installed || compareVersions(installed, input.releasedVersion) < 0) {
23513
+ return {
23514
+ kind: "failed",
23515
+ error: `self-converge did not take effect: mmi-cli on PATH still resolves to ${installed ?? "unknown"}, expected >= ${input.releasedVersion}`
23516
+ };
23517
+ }
23518
+ const reexecCode = await deps.reexec();
23519
+ return { kind: "converged", reexecCode };
23520
+ } catch (e) {
23521
+ return { kind: "failed", error: e instanceof Error ? e.message : String(e) };
23522
+ }
23523
+ }
23524
+
23525
+ // src/statusline-invalidate.ts
23526
+ var import_node_child_process11 = require("node:child_process");
23379
23527
  function invalidateStatuslineBoardCache() {
23380
23528
  try {
23381
- const child2 = (0, import_node_child_process10.spawn)("jerv-cli", ["lane", "ops", "invalidate-board"], {
23529
+ const child2 = (0, import_node_child_process11.spawn)("jerv-cli", ["lane", "ops", "invalidate-board"], {
23382
23530
  detached: true,
23383
23531
  stdio: "ignore",
23384
23532
  windowsHide: true
@@ -23651,12 +23799,12 @@ async function putTenantArtifact(repo, stage, inputPath, deps) {
23651
23799
  }
23652
23800
 
23653
23801
  // src/hotfix-coverage.ts
23654
- var import_node_child_process11 = require("node:child_process");
23802
+ var import_node_child_process12 = require("node:child_process");
23655
23803
  var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
23656
23804
  function checkHotfixCoverage(options = {}) {
23657
23805
  const { cwd = process.cwd(), mainRef = "origin/main", rcRef = "origin/rc", manifestPaths = [] } = options;
23658
23806
  const ack = (options.ack ?? []).filter(Boolean);
23659
- const git2 = options.git ?? ((args, opts) => (0, import_node_child_process11.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
23807
+ const git2 = options.git ?? ((args, opts) => (0, import_node_child_process12.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
23660
23808
  const revList = (range) => {
23661
23809
  const out = git2(["rev-list", "--no-merges", range]).trim();
23662
23810
  return out ? out.split("\n") : [];
@@ -23724,7 +23872,7 @@ function checkHotfixCoverage(options = {}) {
23724
23872
  }
23725
23873
  function checkHotfixCarries(options) {
23726
23874
  const { cwd = process.cwd(), branch, baseRef, targets } = options;
23727
- const git2 = options.git ?? ((args, opts) => (0, import_node_child_process11.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
23875
+ const git2 = options.git ?? ((args, opts) => (0, import_node_child_process12.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
23728
23876
  const isAncestor = (sha, ref) => {
23729
23877
  try {
23730
23878
  git2(["merge-base", "--is-ancestor", sha, ref]);
@@ -24821,7 +24969,7 @@ function renderDeployPortDoctor(report) {
24821
24969
  }
24822
24970
 
24823
24971
  // src/spawn-policy-core.ts
24824
- var import_node_child_process12 = require("node:child_process");
24972
+ var import_node_child_process13 = require("node:child_process");
24825
24973
  var import_node_fs24 = require("node:fs");
24826
24974
  var import_node_path21 = require("node:path");
24827
24975
  var SPAWNERS = ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync"];
@@ -24892,7 +25040,7 @@ function findViolationsInSource(raw) {
24892
25040
  return found;
24893
25041
  }
24894
25042
  function policedFiles(root) {
24895
- const r = (0, import_node_child_process12.spawnSync)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
25043
+ const r = (0, import_node_child_process13.spawnSync)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
24896
25044
  cwd: root,
24897
25045
  encoding: "utf8",
24898
25046
  windowsHide: true,
@@ -24926,7 +25074,7 @@ function runSpawnPolicy(root) {
24926
25074
  }
24927
25075
 
24928
25076
  // src/test-policy-core.ts
24929
- var import_node_child_process13 = require("node:child_process");
25077
+ var import_node_child_process14 = require("node:child_process");
24930
25078
  var import_node_fs25 = require("node:fs");
24931
25079
  var import_node_path22 = require("node:path");
24932
25080
 
@@ -25374,14 +25522,14 @@ function evaluate(changed, policy, present = () => false) {
25374
25522
  return findings;
25375
25523
  }
25376
25524
  function git(args, cwd) {
25377
- return (0, import_node_child_process13.execFileSync)("git", args, { windowsHide: true, cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
25525
+ return (0, import_node_child_process14.execFileSync)("git", args, { windowsHide: true, cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
25378
25526
  }
25379
25527
  var COAUTHOR_KEY = "Co-authored-by";
25380
25528
  var GH_MESSAGE_SEPARATOR = /^-{5,}$/;
25381
25529
  var LIFTED_KEYS = new RegExp(`^(?:${TRAILER_KEY}|${COAUTHOR_KEY}):`, "i");
25382
25530
  function parseTrailers(message, cwd) {
25383
25531
  try {
25384
- return (0, import_node_child_process13.execFileSync)("git", ["interpret-trailers", "--parse", "--unfold"], {
25532
+ return (0, import_node_child_process14.execFileSync)("git", ["interpret-trailers", "--parse", "--unfold"], {
25385
25533
  windowsHide: true,
25386
25534
  cwd,
25387
25535
  input: message,
@@ -25594,7 +25742,7 @@ function runTestPolicy(root, deps = {}) {
25594
25742
  }
25595
25743
 
25596
25744
  // src/dist-drift.ts
25597
- var import_node_child_process14 = require("node:child_process");
25745
+ var import_node_child_process15 = require("node:child_process");
25598
25746
  var import_node_crypto7 = require("node:crypto");
25599
25747
  var import_node_fs27 = require("node:fs");
25600
25748
  var import_node_os13 = require("node:os");
@@ -25739,7 +25887,7 @@ function bomPathFor(root) {
25739
25887
  }
25740
25888
  }
25741
25889
  function rebuildTo(packageRoot, outDir) {
25742
- (0, import_node_child_process14.execFileSync)(process.execPath, ["build.mjs"], {
25890
+ (0, import_node_child_process15.execFileSync)(process.execPath, ["build.mjs"], {
25743
25891
  cwd: packageRoot,
25744
25892
  env: { ...process.env, MMI_DIST_OUTDIR: outDir },
25745
25893
  windowsHide: true,
@@ -27456,9 +27604,9 @@ ${SSH_RECIPE_AGENT_NOTE}`);
27456
27604
 
27457
27605
  // src/schedules-commands.ts
27458
27606
  var import_promises4 = require("node:fs/promises");
27459
- var import_node_child_process15 = require("node:child_process");
27607
+ var import_node_child_process16 = require("node:child_process");
27460
27608
  var import_node_util7 = require("node:util");
27461
- var execFileP5 = (0, import_node_util7.promisify)(import_node_child_process15.execFile);
27609
+ var execFileP5 = (0, import_node_util7.promisify)(import_node_child_process16.execFile);
27462
27610
  var AWS_REGION = "eu-central-1";
27463
27611
  var AWS_TIMEOUT_MS = 3e4;
27464
27612
  var AWS_RETRY_DELAY_MS = 1500;
@@ -29551,7 +29699,7 @@ function registerBootstrapCommands(program3) {
29551
29699
  }
29552
29700
  let seedPrUrl;
29553
29701
  if (o.execute && seedPlan.mode === "pr" && seedPlan.branch && seededToBranch > 0) {
29554
- await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-f", "delete_branch_on_merge=true"]).catch(() => {
29702
+ await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-F", `delete_branch_on_merge=${MANAGED_DELETE_BRANCH_ON_MERGE}`]).catch(() => {
29555
29703
  });
29556
29704
  const openPrs = await gh(["pr", "list", "--repo", repo, "--head", seedPlan.branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
29557
29705
  const prDecision = decideSeedPrAction(JSON.parse(openPrs.stdout || "[]"));
@@ -29600,8 +29748,8 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
29600
29748
  }
29601
29749
  if (o.execute && !onlyTarget && o.class === "deployable") {
29602
29750
  try {
29603
- await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-f", "delete_branch_on_merge=true"]);
29604
- applied.push("merge settings: allow_auto_merge, squash, delete-branch-on-merge");
29751
+ await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-F", `delete_branch_on_merge=${MANAGED_DELETE_BRANCH_ON_MERGE}`]);
29752
+ applied.push("merge settings: allow_auto_merge, squash, preserve merged remote branches");
29605
29753
  } catch (e) {
29606
29754
  applied.push(`merge settings (failed: ${e.message})`);
29607
29755
  }
@@ -30794,6 +30942,12 @@ function ghPrMergeLocalBranchDeleteWarning(message) {
30794
30942
  function mergeAutoRejectedPrAlreadyClean(message) {
30795
30943
  return /clean status \(enablePullRequestAutoMerge\)|is in clean status/i.test(message);
30796
30944
  }
30945
+ function assertRemoteBranchPreservedOnMerge(repo, deleteBranchOnMerge) {
30946
+ if (deleteBranchOnMerge === false) return;
30947
+ throw new Error(
30948
+ `pr merge: ${repo} does not prove delete_branch_on_merge=false. Automatic deletion can irreversibly close stacked PRs. Run \`mmi-cli devops ci reconcile --repo ${repo} --apply\`, then retry; remote branch cleanup is delayed.`
30949
+ );
30950
+ }
30797
30951
  function mergeMethodFromParentCount(parentCount) {
30798
30952
  if (parentCount >= 2) return "merge";
30799
30953
  return void 0;
@@ -31043,6 +31197,7 @@ async function remoteBranchExists2(branch, options = {}) {
31043
31197
 
31044
31198
  // src/worktree-merge-cleanup.ts
31045
31199
  var import_node_fs36 = require("node:fs");
31200
+ var import_node_path34 = require("node:path");
31046
31201
 
31047
31202
  // src/worktree-evidence-archive.ts
31048
31203
  var import_node_fs35 = require("node:fs");
@@ -31221,6 +31376,18 @@ function formatArtifactsArchiveReceipt(archive) {
31221
31376
  function normPath2(p) {
31222
31377
  return p.replace(/\\/g, "/").replace(/\/+$/, "");
31223
31378
  }
31379
+ function removeResidueDirectory(wtPath) {
31380
+ try {
31381
+ try {
31382
+ if ((0, import_node_fs36.lstatSync)((0, import_node_path34.join)(wtPath, "node_modules")).isSymbolicLink()) (0, import_node_fs36.rmdirSync)((0, import_node_path34.join)(wtPath, "node_modules"));
31383
+ } catch {
31384
+ }
31385
+ (0, import_node_fs36.rmSync)(wtPath, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
31386
+ return { ok: true };
31387
+ } catch (e) {
31388
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
31389
+ }
31390
+ }
31224
31391
  function samePath(a, b) {
31225
31392
  const na = normPath2(a);
31226
31393
  const nb = normPath2(b);
@@ -31328,6 +31495,16 @@ async function removeWorktreeWithReconcile(wtPath, git2, listWorktrees, pathExis
31328
31495
  };
31329
31496
  }
31330
31497
  if (!isWorktreePathRegistered(worktrees, wtPath)) return pruneAndVerify("reconciled-unregistered");
31498
+ await git2(["worktree", "prune"]).catch(() => "");
31499
+ try {
31500
+ worktrees = await listWorktrees();
31501
+ } catch (e) {
31502
+ return {
31503
+ status: "failed",
31504
+ error: `could not verify worktree registration after prune: ${formatGitCommandError(e)}; path=${wtPath}`
31505
+ };
31506
+ }
31507
+ if (!isWorktreePathRegistered(worktrees, wtPath)) return pruneAndVerify("reconciled-pruned");
31331
31508
  const retryError = await attemptRemove();
31332
31509
  if (!retryError) return pruneAndVerify("reconciled-after-retry");
31333
31510
  if (isNotAWorkingTreeMessage(retryError) || isNotAWorkingTreeMessage(firstError)) {
@@ -31493,6 +31670,15 @@ async function cleanupPrMergeLocalBranch(branch, options) {
31493
31670
  stageTeardown,
31494
31671
  tmpEvidenceCount: tmpEvidence.length
31495
31672
  };
31673
+ if (pathExists(wtPath)) {
31674
+ const residue = options.removeResidueDir?.(wtPath) ?? removeResidueDirectory(wtPath);
31675
+ if (residue.ok) {
31676
+ report.worktree.residue = "swept";
31677
+ } else {
31678
+ report.worktree.residue = "left";
31679
+ report.worktree.residueError = residue.error;
31680
+ }
31681
+ }
31496
31682
  try {
31497
31683
  await git2(["branch", "-D", branch]);
31498
31684
  report.localBranch = { name: branch, status: "deleted" };
@@ -31520,6 +31706,9 @@ function renderPrMergeCleanupLines(cleanup) {
31520
31706
  } else if (wt.status === "preserved") {
31521
31707
  lines.push(`pr merge: preserved worktree ${wt.path} (--preserve-worktree)`);
31522
31708
  }
31709
+ if (wt.residue === "left") {
31710
+ lines.push(`pr merge: worktree ${wt.path} registration is gone but residue remains \u2014 delete by hand: ${wt.residueError ?? wt.path}`);
31711
+ }
31523
31712
  if (wt.artifactsArchive?.status === "archived" && wt.artifactsArchive.path) {
31524
31713
  lines.push(`pr merge: archived worktree evidence to ${wt.artifactsArchive.path}`);
31525
31714
  } else if (wt.artifactsArchive?.status === "blocked" && wt.artifactsArchive.error) {
@@ -33107,7 +33296,7 @@ ${lines}`, {
33107
33296
 
33108
33297
  // src/train-commands.ts
33109
33298
  var import_node_fs38 = require("node:fs");
33110
- var import_node_path34 = require("node:path");
33299
+ var import_node_path35 = require("node:path");
33111
33300
  var RELEASE_BUMP_INTENTS = ["major", "minor", "patch"];
33112
33301
  function resolveReleaseBumpIntent(raw) {
33113
33302
  const intent = typeof raw === "string" ? raw.trim() : "";
@@ -33118,7 +33307,7 @@ function resolveReleaseBumpIntent(raw) {
33118
33307
  }
33119
33308
  function readRepoVersion() {
33120
33309
  try {
33121
- return JSON.parse((0, import_node_fs38.readFileSync)((0, import_node_path34.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
33310
+ return JSON.parse((0, import_node_fs38.readFileSync)((0, import_node_path35.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
33122
33311
  } catch {
33123
33312
  return void 0;
33124
33313
  }
@@ -33277,7 +33466,7 @@ function registerDeployCommands(program3) {
33277
33466
  // src/discovery-commands.ts
33278
33467
  var import_node_fs39 = require("node:fs");
33279
33468
  var import_node_os17 = require("node:os");
33280
- var import_node_path35 = require("node:path");
33469
+ var import_node_path36 = require("node:path");
33281
33470
  var GC_GH_TIMEOUT_MS2 = 2e4;
33282
33471
  async function collectStatus() {
33283
33472
  const repo = await resolveRepo();
@@ -33467,8 +33656,8 @@ async function collectOnboardStatus(opts = {}) {
33467
33656
  }
33468
33657
  const home = (0, import_node_os17.homedir)();
33469
33658
  const plugin = onboardPluginGate({
33470
- readKnown: () => readFileSyncSafe((0, import_node_path35.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs39.readFileSync),
33471
- readSettings: () => readFileSyncSafe((0, import_node_path35.join)(home, ".claude", "settings.json"), import_node_fs39.readFileSync)
33659
+ readKnown: () => readFileSyncSafe((0, import_node_path36.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs39.readFileSync),
33660
+ readSettings: () => readFileSyncSafe((0, import_node_path36.join)(home, ".claude", "settings.json"), import_node_fs39.readFileSync)
33472
33661
  });
33473
33662
  return { track, board, registry: registry2, secrets, plugin, doors: opts.doors ?? [], nextCommand };
33474
33663
  }
@@ -33777,7 +33966,10 @@ function findCommandInManifest(manifest, commandPath3) {
33777
33966
  return house ? houseRootAsParent(manifest, house) : void 0;
33778
33967
  }
33779
33968
  const visit = (command) => {
33780
- if (command.path === commandPath3 || command.flat_path === commandPath3) return command;
33969
+ const aliasMatches = command.aliases?.some(
33970
+ (alias) => [command.path, command.flat_path].some((path2) => `${path2.slice(0, path2.lastIndexOf(" ") + 1)}${alias}` === commandPath3)
33971
+ );
33972
+ if (command.path === commandPath3 || command.flat_path === commandPath3 || aliasMatches) return command;
33781
33973
  for (const child2 of command.subcommands) {
33782
33974
  const found = visit(child2);
33783
33975
  if (found) return found;
@@ -34229,7 +34421,7 @@ function registerPrLifecycleCommands(program3) {
34229
34421
 
34230
34422
  // src/post-merge-recon.ts
34231
34423
  var import_node_fs40 = require("node:fs");
34232
- var import_node_path36 = require("node:path");
34424
+ var import_node_path37 = require("node:path");
34233
34425
 
34234
34426
  // src/cross-repo-filing-issue.ts
34235
34427
  function crossRepoFilingRetryCommand(prRepo, prNumber) {
@@ -34395,7 +34587,7 @@ function buildPostMergeReconRecovery(input) {
34395
34587
  }
34396
34588
  function writePostMergeReconRecovery(cwd, recovery) {
34397
34589
  const path2 = postMergeReconStatePath(cwd, recovery.repo, recovery.pr);
34398
- (0, import_node_fs40.mkdirSync)((0, import_node_path36.dirname)(path2), { recursive: true });
34590
+ (0, import_node_fs40.mkdirSync)((0, import_node_path37.dirname)(path2), { recursive: true });
34399
34591
  (0, import_node_fs40.writeFileSync)(path2, `${JSON.stringify(recovery, null, 2)}
34400
34592
  `, "utf8");
34401
34593
  return path2;
@@ -37461,17 +37653,17 @@ function parseOriginRepo(remoteUrl) {
37461
37653
  }
37462
37654
  function ghHostsConfigPath(env, platform2) {
37463
37655
  const sep3 = platform2 === "win32" ? "\\" : "/";
37464
- const join32 = (...parts) => parts.join(sep3);
37656
+ const join33 = (...parts) => parts.join(sep3);
37465
37657
  const explicit = env.GH_CONFIG_DIR?.trim();
37466
- if (explicit) return join32(explicit, "hosts.yml");
37658
+ if (explicit) return join33(explicit, "hosts.yml");
37467
37659
  if (platform2 === "win32") {
37468
37660
  const appData = (env.AppData ?? env.APPDATA)?.trim();
37469
- return appData ? join32(appData, "GitHub CLI", "hosts.yml") : void 0;
37661
+ return appData ? join33(appData, "GitHub CLI", "hosts.yml") : void 0;
37470
37662
  }
37471
37663
  const xdg = env.XDG_CONFIG_HOME?.trim();
37472
- if (xdg) return join32(xdg, "gh", "hosts.yml");
37664
+ if (xdg) return join33(xdg, "gh", "hosts.yml");
37473
37665
  const home = env.HOME?.trim();
37474
- return home ? join32(home, ".config", "gh", "hosts.yml") : void 0;
37666
+ return home ? join33(home, ".config", "gh", "hosts.yml") : void 0;
37475
37667
  }
37476
37668
  function parseGhHostsAccounts(yaml, host = "github.com") {
37477
37669
  let hostIndent = null;
@@ -37523,8 +37715,8 @@ function ghAccountCaveat(announcedLogin, accounts) {
37523
37715
  // src/doctor-io.ts
37524
37716
  var import_node_fs41 = require("node:fs");
37525
37717
  var import_node_os18 = require("node:os");
37526
- var import_node_path37 = require("node:path");
37527
- var import_node_child_process16 = require("node:child_process");
37718
+ var import_node_path38 = require("node:path");
37719
+ var import_node_child_process17 = require("node:child_process");
37528
37720
  var import_node_util8 = require("node:util");
37529
37721
 
37530
37722
  // src/discard-sink.ts
@@ -37533,13 +37725,13 @@ function nodeDiscardSinkPath(platform2 = process.platform) {
37533
37725
  }
37534
37726
 
37535
37727
  // src/doctor-io.ts
37536
- var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process16.execFile);
37728
+ var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process17.execFile);
37537
37729
  function execFileCapture(file, args, opts = {}) {
37538
37730
  const sink = nodeDiscardSinkPath();
37539
37731
  const inFd = (0, import_node_fs41.openSync)(sink, "r");
37540
37732
  const errFd = (0, import_node_fs41.openSync)(sink, "w");
37541
37733
  try {
37542
- return (0, import_node_child_process16.execFileSync)(file, args, {
37734
+ return (0, import_node_child_process17.execFileSync)(file, args, {
37543
37735
  ...opts,
37544
37736
  encoding: "utf8",
37545
37737
  stdio: [inFd, "pipe", errFd]
@@ -37553,7 +37745,7 @@ var MMI_PLUGIN_ID2 = "mmi@mutmutco";
37553
37745
  function installedClaudePluginVersion() {
37554
37746
  try {
37555
37747
  const file = JSON.parse(
37556
- (0, import_node_fs41.readFileSync)((0, import_node_path37.join)((0, import_node_os18.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
37748
+ (0, import_node_fs41.readFileSync)((0, import_node_path38.join)((0, import_node_os18.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
37557
37749
  );
37558
37750
  const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
37559
37751
  if (versions.length === 0) return void 0;
@@ -37574,7 +37766,7 @@ function readHermesPluginEvidence(env = process.env) {
37574
37766
  const host = hermesConfigRoot(env);
37575
37767
  const root = hermesPluginRoot(env);
37576
37768
  const installRecordPresent = (0, import_node_fs41.existsSync)(root);
37577
- const manifestPath = (0, import_node_path37.join)(root, "plugin.yaml");
37769
+ const manifestPath = (0, import_node_path38.join)(root, "plugin.yaml");
37578
37770
  let installedVersion;
37579
37771
  let manifest = "missing";
37580
37772
  try {
@@ -37598,14 +37790,14 @@ function readHermesPluginEvidence(env = process.env) {
37598
37790
  }
37599
37791
  let skills = false;
37600
37792
  try {
37601
- skills = (0, import_node_fs41.existsSync)((0, import_node_path37.join)(root, "skills")) && (0, import_node_fs41.statSync)((0, import_node_path37.join)(root, "skills")).isDirectory();
37793
+ skills = (0, import_node_fs41.existsSync)((0, import_node_path38.join)(root, "skills")) && (0, import_node_fs41.statSync)((0, import_node_path38.join)(root, "skills")).isDirectory();
37602
37794
  } catch {
37603
37795
  }
37604
37796
  return {
37605
37797
  hostPresent: (0, import_node_fs41.existsSync)(host),
37606
37798
  installRecordPresent,
37607
37799
  manifest,
37608
- payloadPresent: (0, import_node_fs41.existsSync)((0, import_node_path37.join)(root, "__init__.py")) && skills && manifest === "valid",
37800
+ payloadPresent: (0, import_node_fs41.existsSync)((0, import_node_path38.join)(root, "__init__.py")) && skills && manifest === "valid",
37609
37801
  ...installedVersion ? { installedVersion } : {}
37610
37802
  };
37611
37803
  }
@@ -37613,7 +37805,7 @@ function installedSurfacePluginVersion(surface) {
37613
37805
  const token = surfaceToken(surface);
37614
37806
  if (token === "kilo") {
37615
37807
  try {
37616
- const stamp = (0, import_node_fs41.readFileSync)((0, import_node_path37.join)((0, import_node_os18.homedir)(), ".kilo", ".mmi-kilo-version"), "utf8").trim();
37808
+ const stamp = (0, import_node_fs41.readFileSync)((0, import_node_path38.join)((0, import_node_os18.homedir)(), ".kilo", ".mmi-kilo-version"), "utf8").trim();
37617
37809
  return stamp || void 0;
37618
37810
  } catch {
37619
37811
  return void 0;
@@ -37621,13 +37813,13 @@ function installedSurfacePluginVersion(surface) {
37621
37813
  }
37622
37814
  if (token === "hermes") return readHermesPluginEvidence().installedVersion;
37623
37815
  if (token === "cursor") {
37624
- return manifestVersion((0, import_node_path37.join)(cursorLocalPluginRoot(), ".cursor-plugin", "plugin.json"));
37816
+ return manifestVersion((0, import_node_path38.join)(cursorLocalPluginRoot(), ".cursor-plugin", "plugin.json"));
37625
37817
  }
37626
37818
  if (token === "jervcode") {
37627
37819
  return installedJervCodePackageVersion();
37628
37820
  }
37629
37821
  if (token === "kimi") {
37630
- return manifestVersion((0, import_node_path37.join)(surfaceConfigRoot(surface), "plugins", "managed", "mmi", ".kimi-plugin", "plugin.json"));
37822
+ return manifestVersion((0, import_node_path38.join)(surfaceConfigRoot(surface), "plugins", "managed", "mmi", ".kimi-plugin", "plugin.json"));
37631
37823
  }
37632
37824
  if (token === "claude") return installedClaudePluginVersion();
37633
37825
  if (token !== "codex") return void 0;
@@ -37661,7 +37853,7 @@ function worktreeRootSync() {
37661
37853
  }
37662
37854
  var gitignorePath = () => {
37663
37855
  const root = worktreeRootSync();
37664
- return root === null ? null : (0, import_node_path37.join)(root, ".gitignore");
37856
+ return root === null ? null : (0, import_node_path38.join)(root, ".gitignore");
37665
37857
  };
37666
37858
  function readGitignore() {
37667
37859
  const path2 = gitignorePath();
@@ -37683,7 +37875,7 @@ function writeGitignore(content) {
37683
37875
  }
37684
37876
  }
37685
37877
  function lineEndingState(root) {
37686
- const attributesPresent = (0, import_node_fs41.existsSync)((0, import_node_path37.join)(root, ".gitattributes"));
37878
+ const attributesPresent = (0, import_node_fs41.existsSync)((0, import_node_path38.join)(root, ".gitattributes"));
37687
37879
  try {
37688
37880
  const output = execFileCapture("git", ["-C", root, "ls-files", "--eol", "--", ":(glob)**/*.sh"], {
37689
37881
  windowsHide: true
@@ -37759,7 +37951,7 @@ function ghMultiAccountCaveat(announcedLogin) {
37759
37951
  var ENV_HEAL_LOCK_STALE_MS = 10 * 6e4;
37760
37952
  var ENV_HEAL_LOCK_MAX_WAIT_MS = 2 * 6e4;
37761
37953
  function envHealLockPath(home) {
37762
- return (0, import_node_path38.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
37954
+ return (0, import_node_path39.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
37763
37955
  }
37764
37956
  async function withEnvHealLock(what, run) {
37765
37957
  try {
@@ -37934,7 +38126,7 @@ function mmiDoctorDeps(opts = {}) {
37934
38126
  // adopted the generated routing index (MMG-Unlive: `docs/Archive/**` only, no index, no gate.yml) would
37935
38127
  // get a permanent — demanding an artifact it never asked for.
37936
38128
  docsIndexState: (root) => {
37937
- if (!(0, import_node_fs42.existsSync)((0, import_node_path38.join)(root, DOCS_INDEX_PATH))) return void 0;
38129
+ if (!(0, import_node_fs42.existsSync)((0, import_node_path39.join)(root, DOCS_INDEX_PATH))) return void 0;
37938
38130
  const real = createDocsIndexDeps(root);
37939
38131
  let docs2;
37940
38132
  const listDocs = () => docs2 ??= real.listDocs();
@@ -37943,7 +38135,7 @@ function mmiDoctorDeps(opts = {}) {
37943
38135
  },
37944
38136
  // #4168: working-tree heal — write if drifted, then re-check. Commit remains the operator's step.
37945
38137
  healDocsIndex: (root) => {
37946
- if (!(0, import_node_fs42.existsSync)((0, import_node_path38.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
38138
+ if (!(0, import_node_fs42.existsSync)((0, import_node_path39.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
37947
38139
  const real = createDocsIndexDeps(root);
37948
38140
  let docs2;
37949
38141
  const listDocs = () => docs2 ??= real.listDocs();
@@ -38040,7 +38232,18 @@ async function requireFreshTrainCli(commandName) {
38040
38232
  releasedVersion
38041
38233
  });
38042
38234
  if (report.ok) return;
38043
- throw new Error(staleTrainCliMessage(report, commandName));
38235
+ if (process.env.MMI_TRAIN_NO_SELF_UPDATE === "1") {
38236
+ throw new Error(staleTrainCliMessage(report, commandName));
38237
+ }
38238
+ const converge = await selfConvergeTrainCli({
38239
+ currentVersion: resolveClientVersion(),
38240
+ releasedVersion: report.releasedVersion,
38241
+ commandName
38242
+ });
38243
+ if (converge.kind === "failed") {
38244
+ throw new Error(`${commandName}: ${converge.error}. ${staleTrainCliMessage(report, commandName)}`);
38245
+ }
38246
+ process.exit(converge.reexecCode);
38044
38247
  }
38045
38248
  var cachedCommandPaths;
38046
38249
  function allCommandPaths() {
@@ -38253,7 +38456,7 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
38253
38456
  });
38254
38457
  var rules = program2.command("rules").description("org-managed .gitignore delivery");
38255
38458
  rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").option("--json", "machine-readable output").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
38256
- const path2 = (0, import_node_path38.join)(process.cwd(), ".gitignore");
38459
+ const path2 = (0, import_node_path39.join)(process.cwd(), ".gitignore");
38257
38460
  const current = (0, import_node_fs42.existsSync)(path2) ? (0, import_node_fs42.readFileSync)(path2, "utf8") : null;
38258
38461
  const plan = planManagedGitignore(current);
38259
38462
  const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
@@ -38451,7 +38654,7 @@ function scheduleRelatedDiscovery(o) {
38451
38654
  try {
38452
38655
  const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body, "--fail-soft"];
38453
38656
  if (o.repo) args.push("--repo", o.repo);
38454
- spawnDetachedSelf(args, { spawn: import_node_child_process17.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
38657
+ spawnDetachedSelf(args, { spawn: import_node_child_process18.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
38455
38658
  } catch {
38456
38659
  }
38457
38660
  }
@@ -39666,11 +39869,11 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
39666
39869
  }
39667
39870
  });
39668
39871
  async function listCiWorkflowPaths(cwd = process.cwd()) {
39669
- const wfDir = (0, import_node_path38.join)(cwd, ".github", "workflows");
39872
+ const wfDir = (0, import_node_path39.join)(cwd, ".github", "workflows");
39670
39873
  if (!(0, import_node_fs42.existsSync)(wfDir)) return [];
39671
39874
  return (0, import_node_fs42.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
39672
39875
  try {
39673
- return workflowReportsPrChecks((0, import_node_fs42.readFileSync)((0, import_node_path38.join)(wfDir, name), "utf8"));
39876
+ return workflowReportsPrChecks((0, import_node_fs42.readFileSync)((0, import_node_path39.join)(wfDir, name), "utf8"));
39674
39877
  } catch {
39675
39878
  return true;
39676
39879
  }
@@ -39736,16 +39939,16 @@ function ciAuditDeps() {
39736
39939
  // gate re-seed step is skipped gracefully rather than failing mid-run.
39737
39940
  readSeedFile: (path2) => {
39738
39941
  if (!root) return null;
39739
- const fullPath = (0, import_node_path38.join)(root, path2);
39942
+ const fullPath = (0, import_node_path39.join)(root, path2);
39740
39943
  return (0, import_node_fs42.existsSync)(fullPath) ? (0, import_node_fs42.readFileSync)(fullPath, "utf8") : null;
39741
39944
  }
39742
39945
  };
39743
39946
  }
39744
39947
  function hubRoot() {
39745
- const fromPkg = (0, import_node_path38.join)(__dirname, "..", "..");
39948
+ const fromPkg = (0, import_node_path39.join)(__dirname, "..", "..");
39746
39949
  const marker = "skills/bootstrap/seeds/manifest.json";
39747
- if ((0, import_node_fs42.existsSync)((0, import_node_path38.join)(fromPkg, marker))) return fromPkg;
39748
- if ((0, import_node_fs42.existsSync)((0, import_node_path38.join)(process.cwd(), marker))) return process.cwd();
39950
+ if ((0, import_node_fs42.existsSync)((0, import_node_path39.join)(fromPkg, marker))) return fromPkg;
39951
+ if ((0, import_node_fs42.existsSync)((0, import_node_path39.join)(process.cwd(), marker))) return process.cwd();
39749
39952
  return null;
39750
39953
  }
39751
39954
  async function waitLoopCorePool(label) {
@@ -40021,11 +40224,11 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
40021
40224
  const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
40022
40225
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
40023
40226
  const repoForPostCleanup = await resolveRepo(o.repo) ?? o.repo;
40024
- const prMeta = await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName,baseRefName,headRefOid", "--jq", "{head: .headRefName, base: .baseRefName, oid: .headRefOid}"], { timeout: GC_GH_TIMEOUT_MS }).then((r) => JSON.parse(r.stdout)).catch(async (e) => {
40227
+ const prMeta = await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName,baseRefName,headRefOid,state", "--jq", "{head: .headRefName, base: .baseRefName, oid: .headRefOid, state: .state}"], { timeout: GC_GH_TIMEOUT_MS }).then((r) => JSON.parse(r.stdout)).catch(async (e) => {
40025
40228
  if (!isGitHubRateLimitError(e) || !repoForPostCleanup) throw e;
40026
40229
  console.warn(`pr merge: gh GraphQL rate-limited \u2014 reading PR #${number} via REST instead (#4588).`);
40027
40230
  const snapshot = await fetchRestPrSnapshot(number, repoForPostCleanup);
40028
- return { head: snapshot.headRef, base: snapshot.baseRef, oid: snapshot.headSha };
40231
+ return { head: snapshot.headRef, base: snapshot.baseRef, oid: snapshot.headSha, state: snapshot.merged ? "MERGED" : snapshot.state.toUpperCase() };
40029
40232
  });
40030
40233
  const headRef = prMeta.head;
40031
40234
  const baseRef = prMeta.base;
@@ -40060,6 +40263,11 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
40060
40263
  return;
40061
40264
  }
40062
40265
  if (closingGuardVerdict.message) console.warn(closingGuardVerdict.message);
40266
+ if (prMeta.state !== "MERGED") {
40267
+ if (!repoForPostCleanup) throw new Error("pr merge: repository is unreadable; cannot prove remote branch preservation");
40268
+ const repoSettings = await defaultGitHubClient().rest("GET", `repos/${repoForPostCleanup}`);
40269
+ assertRemoteBranchPreservedOnMerge(repoForPostCleanup, repoSettings.delete_branch_on_merge);
40270
+ }
40063
40271
  const startingPath = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
40064
40272
  const housekeeping = assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr merge", { force: o.force });
40065
40273
  const beforeWorktreesRead = await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).then((r) => ({ state: "ok", stdout: r.stdout })).catch((e) => ({ state: "failed", error: e.message || "git worktree list failed" }));
@@ -40107,7 +40315,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
40107
40315
  }
40108
40316
  const remoteBefore = await remoteBranchExists2(headRef);
40109
40317
  let upgradedToAuto = false;
40110
- let remoteNotAttemptedReason = "host-owned-lifecycle";
40318
+ let remoteNotAttemptedReason = "preserved-delayed-cleanup";
40111
40319
  const overrideBody = mergeSquashBody ? { ...writeSquashBodyFile(mergeSquashBody), text: mergeSquashBody } : await composeOverrideBodyFile(
40112
40320
  number,
40113
40321
  repoArgs,
@@ -40326,7 +40534,7 @@ function trainApplyDeps() {
40326
40534
  run: async (file, args) => {
40327
40535
  const timeout = file === "node" && args[1] === "prepare" ? NODE_PREPARE_TIMEOUT_MS : file === "node" && args[1] === "verify" ? NODE_VERIFY_TIMEOUT_MS : file === "npm" ? NPM_TRAIN_TIMEOUT_MS : file === "bash" && args[0] === "scripts/jerv-gateway-release-deploy.sh" ? JERV_GATEWAY_DEPLOY_TIMEOUT_MS : file !== "gh" ? GIT_TIMEOUT_MS : args[0] === "run" && args[1] === "watch" ? GH_RUN_WATCH_TIMEOUT_MS : GH_TRAIN_TIMEOUT_MS;
40328
40536
  try {
40329
- return isWin2 && file === "npm" ? (await execFileP2("cmd.exe", ["/c", "npm", ...args], { timeout })).stdout : (await execFileP2(file, args, { timeout })).stdout;
40537
+ return isWin3 && file === "npm" ? (await execFileP2("cmd.exe", ["/c", "npm", ...args], { timeout })).stdout : (await execFileP2(file, args, { timeout })).stdout;
40330
40538
  } catch (e) {
40331
40539
  throw surfaceTrainSubprocessFailure(e, file, args);
40332
40540
  }
@@ -40422,6 +40630,7 @@ function renderReleaseResume(r) {
40422
40630
  if (r.dispatch) lines.push(` deploy: ${r.dispatch.note}`);
40423
40631
  if (r.devRollForward) lines.push(` development: ${r.devRollForward.note}`);
40424
40632
  if (r.rcAlignment) lines.push(` rc: ${r.rcAlignment.note}`);
40633
+ if (r.checkout) lines.push(` checkout: ${r.checkout.note}`);
40425
40634
  return lines.join("\n");
40426
40635
  }
40427
40636
  function renderReleaseAbort(r) {
@@ -40762,7 +40971,7 @@ function hotfixStatusDeps() {
40762
40971
  run: async (file, args) => {
40763
40972
  const timeout = file === "npm" ? STATUS_NPM_MS : file === "gh" ? STATUS_GH_MS : STATUS_GIT_MS;
40764
40973
  try {
40765
- return isWin2 && file === "npm" ? (await execFileP2("cmd.exe", ["/c", "npm", ...args], { timeout })).stdout : (await execFileP2(file, args, { timeout })).stdout;
40974
+ return isWin3 && file === "npm" ? (await execFileP2("cmd.exe", ["/c", "npm", ...args], { timeout })).stdout : (await execFileP2(file, args, { timeout })).stdout;
40766
40975
  } catch (e) {
40767
40976
  throw surfaceTrainSubprocessFailure(e, file, args);
40768
40977
  }
@@ -40866,7 +41075,7 @@ access.command("audit").description("audit collaborator roles + train-branch pus
40866
41075
  if (!report.ok) process.exitCode = 1;
40867
41076
  });
40868
41077
  access.command("capabilities").description("enumerate your effective vault reach \u2014 every credential NAME + tier + scope you can read/use across project + org/master tiers (names only, no values) (#1615)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsCapabilities(d, o)));
40869
- var isWin2 = process.platform === "win32";
41078
+ var isWin3 = process.platform === "win32";
40870
41079
  program2.command("doctor").description("safely heal active plugin wiring and report hygiene; repository cleanup requires explicit --apply (#5023), while version convergence belongs to `mmi-hub update`").option("--banner", "one-line resume summary; silent when all gates pass").option("--fast", "offline-safe fast lane; read-only, skips slow checks and network probes").option("--preflight", "read-only fast gate for automation: measures and reports with the shared exit code, zero writes").option("--verbose", "print the evidence behind every check \u2014 probes, resolved paths, versions compared, and the names behind each count (#2977)").option("--guide", "print the canonical MMI Agentic Onboarding URL").option("--json", "machine-readable output (an output format \u2014 repairs still run by lane)").option("--apply", "advanced explicit lane: also apply guarded repository maintenance (gitignore, docs index, board mechanics, merged branches, aged scratch); the host owns workspace cleanup").option("--no-repo-writes", "compatibility spelling for the safe default: env/plugin repairs only, never mutate the repository").option("--self", "verify CLI/plugin version parity and gh auth reach; suggests plugin-heal on a hard gap (reads the published version, so not offline-safe) (#2689)").addHelpText("after", "\nExit codes:\n 0 no hard gaps remain; advisory gaps may exist\n 1 one or more included hard checks failed\n\nA plain run may heal safe machine-global plugin wiring but never mutates the repository. Run\n`mmi-cli doctor --apply` only when you explicitly want the guarded full repository cleanup lane.\nCLI and host version convergence belongs to `mmi-hub update`; doctor reports lag and last-run evidence.\n--banner/--fast/--self/--preflight are read-only lanes.\n").action(async (opts) => {
40871
41080
  if (opts.guide) {
40872
41081
  consoleIo.log(`MMI Agentic Onboarding: ${CANONICAL_ONBOARDING_URL}`);
@@ -40897,7 +41106,7 @@ function directoryBytes(path2) {
40897
41106
  return 0;
40898
41107
  }
40899
41108
  for (const entry of entries) {
40900
- const child2 = (0, import_node_path38.join)(path2, entry.name);
41109
+ const child2 = (0, import_node_path39.join)(path2, entry.name);
40901
41110
  if (entry.isDirectory()) total += directoryBytes(child2);
40902
41111
  else {
40903
41112
  try {
@@ -40927,7 +41136,7 @@ function pluginCacheFsDeps(configRoot, dirBytes) {
40927
41136
  dirBytes,
40928
41137
  listStagingDirs: (root) => (0, import_node_fs42.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
40929
41138
  try {
40930
- return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path38.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs42.statSync)(p).mtimeMs) };
41139
+ return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path39.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs42.statSync)(p).mtimeMs) };
40931
41140
  } catch {
40932
41141
  return { name: d.name, mtimeMs: Date.now() };
40933
41142
  }
@@ -40941,7 +41150,7 @@ function stagingApplyFsGuard(configRoot) {
40941
41150
  return {
40942
41151
  referencedPaths: () => readInstalledPluginRefs(configRoot),
40943
41152
  mtimeMs: (name) => {
40944
- const p = (0, import_node_path38.join)(stagingRoot, name);
41153
+ const p = (0, import_node_path39.join)(stagingRoot, name);
40945
41154
  if (!(0, import_node_fs42.existsSync)(p)) return null;
40946
41155
  try {
40947
41156
  return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs42.statSync)(q).mtimeMs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.1.20",
3
+ "version": "4.2.0",
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",