@mutmutco/cli 4.1.20 → 4.2.1

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 +522 -263
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -3415,8 +3415,8 @@ var program = new Command();
3415
3415
 
3416
3416
  // src/index.ts
3417
3417
  var import_promises8 = require("node:fs/promises");
3418
- var import_node_fs42 = require("node:fs");
3419
- var import_node_child_process17 = require("node:child_process");
3418
+ var import_node_fs43 = require("node:fs");
3419
+ var import_node_child_process19 = 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_path40 = 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.1",
11766
+ tag: "v4.2.1",
11767
+ commit: "66e67b6905a1",
11768
+ npm: "@mutmutco/cli@4.2.1"
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.1"
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.1 and redeploy the Hub Lambda from tag v4.2.1 (66e67b6905a1); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
11792
+ v3Target: "v4.2.1 (@mutmutco/cli@4.2.1, tag commit 66e67b6905a1 \u2014 last known-good release carrying the repo-index v4-only contract)"
11792
11793
  }
11793
11794
  },
11794
11795
  {
@@ -14738,10 +14739,16 @@ async function correlateWorkflowRun(deps, args) {
14738
14739
  async function watchTenantRun(deps, runId, repo = HUB_REPO3) {
14739
14740
  if (runId == null) return "pending";
14740
14741
  let watchSaidSuccess = true;
14742
+ const startedAt = Date.now();
14743
+ deps.warn?.(`watching workflow run ${runId} (${repo})`);
14744
+ const heartbeat = deps.warn ? setInterval(() => deps.warn?.(`workflow run ${runId} (${repo}) still running \u2014 ${Math.floor((Date.now() - startedAt) / 6e4)}m elapsed`), 6e4) : void 0;
14745
+ heartbeat?.unref();
14741
14746
  try {
14742
14747
  await deps.run("gh", ["run", "watch", String(runId), "--repo", repo, "--exit-status"]);
14743
14748
  } catch {
14744
14749
  watchSaidSuccess = false;
14750
+ } finally {
14751
+ if (heartbeat) clearInterval(heartbeat);
14745
14752
  }
14746
14753
  const sleep2 = resolveSleep(deps);
14747
14754
  let confirmError;
@@ -15824,7 +15831,7 @@ function assertNpmMajorPreflight(opts) {
15824
15831
  if (localMajor === void 0 || expectedMajor === void 0) return;
15825
15832
  if (localMajor === expectedMajor) return;
15826
15833
  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).`
15834
+ `${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
15835
  );
15829
15836
  }
15830
15837
  async function assertNpmMajorPreflightFromWorkflows(deps, repo) {
@@ -16108,15 +16115,9 @@ async function runRcandResume(deps, options = {}) {
16108
16115
  note: `reused immutable ${tag}; pushed its exact SHA to rc and dispatched the rc deploy`
16109
16116
  };
16110
16117
  }
16111
- async function runReleaseResume(deps, options = {}) {
16118
+ async function runReleaseResumePipeline(deps, options, prepared) {
16112
16119
  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);
16120
+ const { ctx, meta, branchHints, directTrack, deployModel } = prepared;
16120
16121
  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
16122
  const tag = tags[0];
16122
16123
  if (!tag) throw new Error("release --resume: no final v* release tag found \u2014 there is no release to resume");
@@ -16224,6 +16225,36 @@ async function runReleaseResume(deps, options = {}) {
16224
16225
  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
16226
  };
16226
16227
  }
16228
+ async function withCheckoutRestoredAfterReleaseResume(deps, startBranch, operation) {
16229
+ let result;
16230
+ try {
16231
+ result = await operation();
16232
+ } catch (error) {
16233
+ let checkoutNote;
16234
+ try {
16235
+ checkoutNote = (await restoreCheckoutAfterRelease(deps, startBranch)).note;
16236
+ } catch (restoreError) {
16237
+ checkoutNote = `checkout restoration failed while returning to ${startBranch}: ${restoreError instanceof Error ? restoreError.message : String(restoreError)}`;
16238
+ }
16239
+ throw new Error(`${error instanceof Error ? error.message : String(error)}; ${checkoutNote}`);
16240
+ }
16241
+ return withCheckoutRestoredAfterRelease(deps, result, startBranch);
16242
+ }
16243
+ async function runReleaseResume(deps, options = {}) {
16244
+ const ctx = await buildTrainApplyContext(deps);
16245
+ await requireCleanTree(deps);
16246
+ await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
16247
+ const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
16248
+ const branchHints = await loadReleaseTrackBranchHints(deps);
16249
+ const directTrack = isHubControlRepo(ctx.repo) || resolveReleaseTrack(meta, branchHints) === "direct";
16250
+ const deployModel = await preflight(deps, ctx, "main", meta);
16251
+ const startBranch = directTrack ? "development" : "rc";
16252
+ return withCheckoutRestoredAfterReleaseResume(
16253
+ deps,
16254
+ startBranch,
16255
+ () => runReleaseResumePipeline(deps, options, { ctx, meta, branchHints, directTrack, deployModel })
16256
+ );
16257
+ }
16227
16258
  async function runReleaseAbort(deps, options = {}) {
16228
16259
  if (!options.approved) {
16229
16260
  throw new Error("release --abort requires --apply after explicit approval; nothing was written");
@@ -16313,8 +16344,9 @@ async function runReleasePublishRetry(deps, runId, options = {}) {
16313
16344
  }
16314
16345
  };
16315
16346
  const run = await readRun();
16316
- if (run.databaseId !== runId || run.workflowName !== "publish" || run.event !== "release" || run.status !== "completed" || run.conclusion !== "failure") {
16317
- throw new Error(`run ${runId} is not a completed failed Hub publish release run; nothing was written`);
16347
+ const retryableConclusion = run.conclusion === "failure" || run.conclusion === "cancelled";
16348
+ if (run.databaseId !== runId || run.workflowName !== "publish" || run.event !== "release" || run.status !== "completed" || !retryableConclusion) {
16349
+ throw new Error(`run ${runId} is not a completed failed or cancelled Hub publish release run; nothing was written`);
16318
16350
  }
16319
16351
  const tag = run.headBranch ?? "";
16320
16352
  const tagSha = run.headSha ?? "";
@@ -16325,13 +16357,16 @@ async function runReleasePublishRetry(deps, runId, options = {}) {
16325
16357
  throw new Error(`run ${runId} does not match origin ${tag}; nothing was written`);
16326
16358
  }
16327
16359
  await verifyPublishedRelease(deps, ctx.repo, tag, "main", tagSha);
16328
- await deps.run("gh", ["run", "rerun", String(runId), "--repo", ctx.repo, "--failed"]);
16329
- if (options.watch) {
16330
- await deps.run("gh", ["run", "watch", String(runId), "--repo", ctx.repo, "--exit-status"]);
16331
- const completed = await readRun();
16332
- if (completed.status !== "completed" || completed.conclusion !== "success") {
16333
- throw new Error(`publish run ${runId} did not finish successfully after retry`);
16334
- }
16360
+ await deps.run("gh", [
16361
+ "run",
16362
+ "rerun",
16363
+ String(runId),
16364
+ "--repo",
16365
+ ctx.repo,
16366
+ ...run.conclusion === "failure" ? ["--failed"] : []
16367
+ ]);
16368
+ if (options.watch && await watchTenantRun(deps, runId, ctx.repo) !== "success") {
16369
+ throw new Error(`publish run ${runId} did not finish successfully after retry`);
16335
16370
  }
16336
16371
  return {
16337
16372
  command: "release-retry-publish",
@@ -16341,7 +16376,7 @@ async function runReleasePublishRetry(deps, runId, options = {}) {
16341
16376
  runId,
16342
16377
  runUrl: run.url ?? `https://github.com/${ctx.repo}/actions/runs/${runId}`,
16343
16378
  status: options.watch ? "success" : "pending",
16344
- note: options.watch ? `publish run ${runId} retried and passed` : `publish run ${runId} failed jobs queued for retry`
16379
+ note: options.watch ? `publish run ${runId} retried and passed` : `publish run ${runId} queued for retry`
16345
16380
  };
16346
16381
  }
16347
16382
  async function runTrainApplyPipeline(mode, input) {
@@ -17130,7 +17165,7 @@ async function auditRepoCi(repo, deps) {
17130
17165
  ok: info.allow_auto_merge === true,
17131
17166
  label: "allow_auto_merge enabled",
17132
17167
  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`
17168
+ 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
17169
  });
17135
17170
  checks.push({
17136
17171
  ok: info.allow_squash_merge === true,
@@ -17138,9 +17173,9 @@ async function auditRepoCi(repo, deps) {
17138
17173
  detail: info.allow_squash_merge === true ? void 0 : "false or unavailable"
17139
17174
  });
17140
17175
  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"
17176
+ ok: info.delete_branch_on_merge === MANAGED_DELETE_BRANCH_ON_MERGE,
17177
+ label: "delete_branch_on_merge disabled",
17178
+ 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
17179
  });
17145
17180
  checks.push({
17146
17181
  ok: info.has_wiki === false,
@@ -17447,11 +17482,11 @@ async function applyCiReconcileMergeSettingsFromReport(repo, deps, report) {
17447
17482
  body: {
17448
17483
  allow_auto_merge: true,
17449
17484
  allow_squash_merge: true,
17450
- delete_branch_on_merge: true,
17485
+ delete_branch_on_merge: MANAGED_DELETE_BRANCH_ON_MERGE,
17451
17486
  has_wiki: false
17452
17487
  }
17453
17488
  });
17454
- applied.push("allow_auto_merge, allow_squash_merge, delete_branch_on_merge, has_wiki=false");
17489
+ applied.push("allow_auto_merge, allow_squash_merge, preserve merged remote branches, has_wiki=false");
17455
17490
  } catch (e) {
17456
17491
  errors.push(e.message);
17457
17492
  }
@@ -17682,6 +17717,12 @@ async function finalizeCiReconcile(repo, deps, result, before, pendingReason) {
17682
17717
  return result;
17683
17718
  }
17684
17719
  }
17720
+ function ciReconcileExitFailed(input) {
17721
+ if (!input.apply || input.applyResults.length === 0) {
17722
+ return !input.audit.ok || input.applyResults.some((result) => result.errors.length > 0 || result.postApply?.state === "failed");
17723
+ }
17724
+ return input.applyResults.some((result) => result.errors.length > 0 || result.postApply == null || result.postApply.state !== "clean" && result.postApply.state !== "repaired");
17725
+ }
17685
17726
  async function authoritativeSafetyHold(authority, deps, repo, baseBranch) {
17686
17727
  if (authority.coversReleaseBranches) {
17687
17728
  const prOnly = authority.contexts.filter((context) => TRAIN_PR_ONLY_CONTEXTS.has(context));
@@ -20892,6 +20933,11 @@ async function readBoard(options, deps = {}) {
20892
20933
  report[scope].claimable = filtered.claimable;
20893
20934
  report.warnings.push(...filtered.warnings);
20894
20935
  }
20936
+ for (const scope of ["primary", "secondary"]) {
20937
+ const demoted = await demoteLiveClaimedClaimables(report[scope], client);
20938
+ report.warnings.push(...demoted.warnings);
20939
+ report.partial = report.partial || demoted.partial;
20940
+ }
20895
20941
  return report;
20896
20942
  }
20897
20943
  function findBoardItem(items, selector, board) {
@@ -20903,11 +20949,11 @@ function findBoardItem(items, selector, board) {
20903
20949
  }
20904
20950
  async function resolveCurrentRepo(options, deps) {
20905
20951
  if (options.repo) return options.repo;
20906
- const git2 = deps.git ?? defaultGit;
20952
+ const git3 = deps.git ?? defaultGit;
20907
20953
  let gitFailure;
20908
20954
  let originUrl = "";
20909
20955
  try {
20910
- originUrl = await git2(["remote", "get-url", "origin"]);
20956
+ originUrl = await git3(["remote", "get-url", "origin"]);
20911
20957
  } catch (e) {
20912
20958
  const read = gitReadState(e);
20913
20959
  if (read.state === "failed") gitFailure = read.detail;
@@ -21753,6 +21799,66 @@ function boardItemClaim(item) {
21753
21799
  currentlyClaimed: item.assignees.length > 0 && item.status === "In Progress"
21754
21800
  };
21755
21801
  }
21802
+ function classifyClaimableMarker(comments, actor, now = Date.now()) {
21803
+ const marker = comments ? latestClaimMarker(comments) : void 0;
21804
+ if (!marker) return { verdict: "claimable" };
21805
+ const ownership = laneOwnership(marker, actor);
21806
+ if (ownership === "mine") return { verdict: "owned", holder: describeClaimMarker(marker) };
21807
+ const age = now - Date.parse(marker.ts);
21808
+ if (Number.isFinite(age) && age >= 0 && age < CLAIM_MARKER_LIVE_MS) {
21809
+ return { verdict: "taken", holder: describeClaimMarker(marker) };
21810
+ }
21811
+ return { verdict: "claimable" };
21812
+ }
21813
+ async function demoteLiveClaimedClaimables(scope, client, actor = describeSessionIdentity(), deps = {}) {
21814
+ const warnings = [];
21815
+ let partial = false;
21816
+ const fetchComments = deps.fetchComments ?? (async (item) => {
21817
+ if (item.details) return item.details.comments;
21818
+ const comments = await client.restPaginate(`repos/${item.repository}/issues/${item.number}/comments`);
21819
+ return comments.map((comment) => ({ body: comment.body ?? "" }));
21820
+ });
21821
+ const stillClaimable = [];
21822
+ const movedTaken = [];
21823
+ const movedOwned = [];
21824
+ for (const item of scope.claimable) {
21825
+ if (item.contentType !== "Issue") {
21826
+ stillClaimable.push(item);
21827
+ continue;
21828
+ }
21829
+ let comments;
21830
+ try {
21831
+ comments = await fetchComments(item);
21832
+ } catch {
21833
+ comments = void 0;
21834
+ }
21835
+ if (comments === void 0) {
21836
+ partial = true;
21837
+ warnings.push(`${item.ref} left claimable: claim-marker evidence could not be read`);
21838
+ continue;
21839
+ }
21840
+ const verdict = classifyClaimableMarker(comments, actor, deps.now);
21841
+ if (verdict.verdict === "claimable") {
21842
+ stillClaimable.push(item);
21843
+ } else if (verdict.verdict === "owned") {
21844
+ movedOwned.push(item);
21845
+ warnings.push(`${item.ref} is held by your own lane (${verdict.holder}) \u2014 moved to user-owned`);
21846
+ } else {
21847
+ movedTaken.push(item);
21848
+ warnings.push(`${item.ref} is held by a live claim marker (lane ${verdict.holder}) \u2014 no longer claimable`);
21849
+ }
21850
+ }
21851
+ scope.claimable = stillClaimable;
21852
+ if (movedTaken.length) {
21853
+ scope.taken.push(...movedTaken);
21854
+ scope.taken.sort(compareItems);
21855
+ }
21856
+ if (movedOwned.length) {
21857
+ scope.userOwned.push(...movedOwned);
21858
+ scope.userOwned.sort(compareItems);
21859
+ }
21860
+ return { warnings, partial };
21861
+ }
21756
21862
  async function postClaimMarkerComment(client, item, actor = describeSessionIdentity()) {
21757
21863
  try {
21758
21864
  const marker = {
@@ -23374,11 +23480,69 @@ function renderPluginCachePlan(plan, applied) {
23374
23480
  return lines.join("\n");
23375
23481
  }
23376
23482
 
23377
- // src/statusline-invalidate.ts
23483
+ // src/train-self-converge.ts
23378
23484
  var import_node_child_process10 = require("node:child_process");
23485
+ var SELF_CONVERGE_INSTALL_TIMEOUT_MS = 12e4;
23486
+ var SELF_CONVERGE_VERSION_TIMEOUT_MS = 3e4;
23487
+ var isWin2 = process.platform === "win32";
23488
+ var defaultDeps = {
23489
+ // Pin the exact released version (never `@latest` — a cached packument resolves the tag to the
23490
+ // PREVIOUS version in exactly this post-publish window, #3422). execFileP rejects on nonzero exit.
23491
+ update: async (target) => {
23492
+ const args = ["install", "-g", `@mutmutco/cli@${target}`];
23493
+ if (isWin2) {
23494
+ await execFileP2("cmd.exe", ["/c", "npm", ...args], { timeout: SELF_CONVERGE_INSTALL_TIMEOUT_MS });
23495
+ } else {
23496
+ await execFileP2("npm", args, { timeout: SELF_CONVERGE_INSTALL_TIMEOUT_MS });
23497
+ }
23498
+ },
23499
+ // Read the version the freshly installed `mmi-cli` on PATH actually resolves to. Unparseable or
23500
+ // failed read answers undefined so the flow reports not-taken-effect instead of guessing green.
23501
+ readInstalledVersion: async () => {
23502
+ try {
23503
+ 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 });
23504
+ return parseNpmVersion(stdout);
23505
+ } catch {
23506
+ return void 0;
23507
+ }
23508
+ },
23509
+ // Re-exec the SAME command line in the fresh CLI with inherited stdio (spawn, not execFile, so
23510
+ // output streams live) and hand its exit code back — the stale parent exits with it (#5762).
23511
+ reexec: () => new Promise((resolve6) => {
23512
+ const args = process.argv.slice(2);
23513
+ 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 });
23514
+ child2.on("exit", (code) => resolve6(code ?? 1));
23515
+ child2.on("error", () => resolve6(1));
23516
+ }),
23517
+ // stderr only — stdout may be carrying --json output.
23518
+ warn: (line) => console.error(line)
23519
+ };
23520
+ async function selfConvergeTrainCli(input) {
23521
+ const deps = { ...defaultDeps, ...input.deps };
23522
+ try {
23523
+ deps.warn(
23524
+ `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}`
23525
+ );
23526
+ await deps.update(input.releasedVersion);
23527
+ const installed = await deps.readInstalledVersion();
23528
+ if (!installed || compareVersions(installed, input.releasedVersion) < 0) {
23529
+ return {
23530
+ kind: "failed",
23531
+ error: `self-converge did not take effect: mmi-cli on PATH still resolves to ${installed ?? "unknown"}, expected >= ${input.releasedVersion}`
23532
+ };
23533
+ }
23534
+ const reexecCode = await deps.reexec();
23535
+ return { kind: "converged", reexecCode };
23536
+ } catch (e) {
23537
+ return { kind: "failed", error: e instanceof Error ? e.message : String(e) };
23538
+ }
23539
+ }
23540
+
23541
+ // src/statusline-invalidate.ts
23542
+ var import_node_child_process11 = require("node:child_process");
23379
23543
  function invalidateStatuslineBoardCache() {
23380
23544
  try {
23381
- const child2 = (0, import_node_child_process10.spawn)("jerv-cli", ["lane", "ops", "invalidate-board"], {
23545
+ const child2 = (0, import_node_child_process11.spawn)("jerv-cli", ["lane", "ops", "invalidate-board"], {
23382
23546
  detached: true,
23383
23547
  stdio: "ignore",
23384
23548
  windowsHide: true
@@ -23651,36 +23815,36 @@ async function putTenantArtifact(repo, stage, inputPath, deps) {
23651
23815
  }
23652
23816
 
23653
23817
  // src/hotfix-coverage.ts
23654
- var import_node_child_process11 = require("node:child_process");
23818
+ var import_node_child_process12 = require("node:child_process");
23655
23819
  var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
23656
23820
  function checkHotfixCoverage(options = {}) {
23657
23821
  const { cwd = process.cwd(), mainRef = "origin/main", rcRef = "origin/rc", manifestPaths = [] } = options;
23658
23822
  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"] }));
23823
+ const git3 = options.git ?? ((args, opts) => (0, import_node_child_process12.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
23660
23824
  const revList = (range) => {
23661
- const out = git2(["rev-list", "--no-merges", range]).trim();
23825
+ const out = git3(["rev-list", "--no-merges", range]).trim();
23662
23826
  return out ? out.split("\n") : [];
23663
23827
  };
23664
23828
  const isAncestor = (sha, ref) => {
23665
23829
  try {
23666
- git2(["merge-base", "--is-ancestor", sha, ref]);
23830
+ git3(["merge-base", "--is-ancestor", sha, ref]);
23667
23831
  return true;
23668
23832
  } catch {
23669
23833
  return false;
23670
23834
  }
23671
23835
  };
23672
23836
  const patchId = (sha) => {
23673
- const diff = git2(["show", "--no-color", "--pretty=format:", sha]);
23837
+ const diff = git3(["show", "--no-color", "--pretty=format:", sha]);
23674
23838
  if (!diff.trim()) return null;
23675
- const out = git2(["patch-id", "--stable"], { input: diff }).trim();
23839
+ const out = git3(["patch-id", "--stable"], { input: diff }).trim();
23676
23840
  return out ? out.split(" ")[0] : null;
23677
23841
  };
23678
23842
  const changedPaths = (sha) => {
23679
- const out = git2(["show", "--no-color", "--name-only", "--pretty=format:", sha]).trim();
23843
+ const out = git3(["show", "--no-color", "--name-only", "--pretty=format:", sha]).trim();
23680
23844
  return out ? out.split("\n") : [];
23681
23845
  };
23682
23846
  const cherrySources = (sha) => {
23683
- const message = git2(["log", "-1", "--format=%B", sha]);
23847
+ const message = git3(["log", "-1", "--format=%B", sha]);
23684
23848
  return [...message.matchAll(CHERRY_TRAILER)].map((m) => m[1]);
23685
23849
  };
23686
23850
  const mainOnly = revList(`${rcRef}..${mainRef}`);
@@ -23695,7 +23859,7 @@ function checkHotfixCoverage(options = {}) {
23695
23859
  }
23696
23860
  };
23697
23861
  const commits = mainOnly.map((sha) => {
23698
- const subject = git2(["log", "-1", "--format=%s", sha]).trim();
23862
+ const subject = git3(["log", "-1", "--format=%s", sha]).trim();
23699
23863
  const paths = changedPaths(sha);
23700
23864
  if (paths.length > 0 && paths.every((p) => manifestPaths.includes(p))) {
23701
23865
  return { sha, subject, coverage: "exempt-distribution" };
@@ -23724,21 +23888,21 @@ function checkHotfixCoverage(options = {}) {
23724
23888
  }
23725
23889
  function checkHotfixCarries(options) {
23726
23890
  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"] }));
23891
+ const git3 = options.git ?? ((args, opts) => (0, import_node_child_process12.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
23728
23892
  const isAncestor = (sha, ref) => {
23729
23893
  try {
23730
- git2(["merge-base", "--is-ancestor", sha, ref]);
23894
+ git3(["merge-base", "--is-ancestor", sha, ref]);
23731
23895
  return true;
23732
23896
  } catch {
23733
23897
  return false;
23734
23898
  }
23735
23899
  };
23736
23900
  const revList = (range) => {
23737
- const out = git2(["rev-list", "--no-merges", range]).trim();
23901
+ const out = git3(["rev-list", "--no-merges", range]).trim();
23738
23902
  return out ? out.split("\n") : [];
23739
23903
  };
23740
23904
  const cherrySources = (sha) => {
23741
- const message = git2(["log", "-1", "--format=%B", sha]);
23905
+ const message = git3(["log", "-1", "--format=%B", sha]);
23742
23906
  return [...message.matchAll(CHERRY_TRAILER)].map((m) => m[1]);
23743
23907
  };
23744
23908
  const carried = /* @__PURE__ */ new Set();
@@ -24821,7 +24985,7 @@ function renderDeployPortDoctor(report) {
24821
24985
  }
24822
24986
 
24823
24987
  // src/spawn-policy-core.ts
24824
- var import_node_child_process12 = require("node:child_process");
24988
+ var import_node_child_process13 = require("node:child_process");
24825
24989
  var import_node_fs24 = require("node:fs");
24826
24990
  var import_node_path21 = require("node:path");
24827
24991
  var SPAWNERS = ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync"];
@@ -24892,7 +25056,7 @@ function findViolationsInSource(raw) {
24892
25056
  return found;
24893
25057
  }
24894
25058
  function policedFiles(root) {
24895
- const r = (0, import_node_child_process12.spawnSync)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
25059
+ const r = (0, import_node_child_process13.spawnSync)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
24896
25060
  cwd: root,
24897
25061
  encoding: "utf8",
24898
25062
  windowsHide: true,
@@ -24926,12 +25090,25 @@ function runSpawnPolicy(root) {
24926
25090
  }
24927
25091
 
24928
25092
  // src/test-policy-core.ts
24929
- var import_node_child_process13 = require("node:child_process");
25093
+ var import_node_child_process15 = require("node:child_process");
24930
25094
  var import_node_fs25 = require("node:fs");
24931
25095
  var import_node_path22 = require("node:path");
24932
25096
 
24933
25097
  // ../scripts/test-command-policy-core.mjs
25098
+ var import_node_child_process14 = require("node:child_process");
24934
25099
  var TEST_COMMAND_CLASS = "test";
25100
+ var TRAILER_KEY = "Test-Policy-Override";
25101
+ var OVERRIDE_RE = /^Test-Policy-Override:\s*(.+)$/im;
25102
+ var REC = "";
25103
+ var FLD = "";
25104
+ var WAIVABLE_KINDS = [
25105
+ "mandatory-zone-untested",
25106
+ "unrequested-test-file",
25107
+ "protected-removed",
25108
+ "stale-protected-entry",
25109
+ "stale-satisfied-by"
25110
+ ];
25111
+ var TEST_WORK_KIND = "unrequested-test-file";
24935
25112
  function translateGlob(glob) {
24936
25113
  let out = "";
24937
25114
  for (let i = 0; i < glob.length; i += 1) {
@@ -24974,7 +25151,7 @@ function matchedMandatoryGlobs(paths, mandatory) {
24974
25151
  return list.some((path2) => re.test(path2));
24975
25152
  });
24976
25153
  }
24977
- function evaluateTestCommandPolicy({ paths, mandatory, regulated = true } = {}) {
25154
+ function evaluateTestCommandPolicy({ paths, mandatory, regulated = true, override = null } = {}) {
24978
25155
  const configuredMandatoryCount = mandatoryGlobList(mandatory).length;
24979
25156
  if (!regulated) {
24980
25157
  return {
@@ -24987,7 +25164,8 @@ function evaluateTestCommandPolicy({ paths, mandatory, regulated = true } = {})
24987
25164
  };
24988
25165
  }
24989
25166
  const matched = matchedMandatoryGlobs(paths, mandatory);
24990
- const testCommandsAllowed = matched.length > 0;
25167
+ const overrideAuthorizes = Array.isArray(override?.kinds) && override.kinds.includes(TEST_WORK_KIND);
25168
+ const testCommandsAllowed = matched.length > 0 || overrideAuthorizes;
24991
25169
  return {
24992
25170
  configuredMandatoryCount,
24993
25171
  matchedMandatoryGlobs: matched,
@@ -25000,22 +25178,78 @@ function evaluateTestCommandPolicy({ paths, mandatory, regulated = true } = {})
25000
25178
  }
25001
25179
  };
25002
25180
  }
25181
+ function git(args, cwd) {
25182
+ return (0, import_node_child_process14.execFileSync)("git", args, { windowsHide: true, cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
25183
+ }
25184
+ function parseScope(value) {
25185
+ const scoped = /^\[([^\]]*)\]\s*([\s\S]*)$/.exec(value);
25186
+ if (!scoped) return { kinds: [...WAIVABLE_KINDS], reason: value, unknown: [] };
25187
+ const named = scoped[1].split(",").map((k) => k.trim()).filter(Boolean);
25188
+ return {
25189
+ kinds: named,
25190
+ reason: scoped[2].trim(),
25191
+ unknown: named.filter((k) => !WAIVABLE_KINDS.includes(k))
25192
+ };
25193
+ }
25194
+ function invisibleTrailer(sha, line) {
25195
+ return {
25196
+ kind: "malformed-override-trailer",
25197
+ paths: [],
25198
+ detail: `OVERRIDE TRAILER GIT CANNOT SEE \u2014 commit ${sha.slice(0, 8)} carries
25199
+ ${line}
25200
+ but \`git log --format='%(trailers:key=${TRAILER_KEY})'\` reports nothing for it, so the waiver would
25201
+ exist only to the regex that granted it. The trailer was chosen over a flag because it can be
25202
+ AUDITED, and a reason the auditor cannot see is not a reason this gate accepts (#3628).
25203
+ Git reads trailers from the LAST paragraph only, and every line of that paragraph must be a
25204
+ trailer or an indented continuation \u2014 one bare line such as \`Closes #123\` disqualifies the whole
25205
+ block. Indent the continuation lines, and leave nothing but trailers in that paragraph.
25206
+ This is fixable forward: push a LATER commit on this branch carrying a well-formed trailer.
25207
+ The nearest waiver wins, and it supersedes this one \u2014 no force-push, no re-filed branch.`
25208
+ };
25209
+ }
25210
+ function unknownScope(sha, unknown) {
25211
+ return {
25212
+ kind: "malformed-override-trailer",
25213
+ paths: [],
25214
+ detail: `UNKNOWN OVERRIDE SCOPE \u2014 commit ${sha.slice(0, 8)} scopes its waiver to ${unknown.join(", ")}, which this
25215
+ gate cannot report.
25216
+ Waivable kinds: ${WAIVABLE_KINDS.join(", ")}.
25217
+ Refused rather than widened: treating a typo as "waive everything" is how a scoped waiver turns
25218
+ into a blanket exemption without anyone deciding it should.`
25219
+ };
25220
+ }
25221
+ function isShallowRepository(cwd) {
25222
+ try {
25223
+ return git(["rev-parse", "--is-shallow-repository"], cwd).trim() !== "false";
25224
+ } catch {
25225
+ return true;
25226
+ }
25227
+ }
25228
+ function readOverride(base, cwd) {
25229
+ const format = `%H${FLD}%(trailers:key=${TRAILER_KEY},valueonly,unfold)${FLD}%B${REC}`;
25230
+ const refusals = [];
25231
+ let override = null;
25232
+ for (const record of git(["log", `${base}..HEAD`, `--format=${format}`], cwd).split(REC)) {
25233
+ const [sha, trailer, body] = record.replace(/^\s+/, "").split(FLD);
25234
+ if (!sha) continue;
25235
+ if (override) break;
25236
+ const value = (trailer ?? "").trim();
25237
+ if (!value) {
25238
+ const shaped = OVERRIDE_RE.exec(body ?? "");
25239
+ if (shaped) refusals.push(invisibleTrailer(sha, shaped[0].trim()));
25240
+ continue;
25241
+ }
25242
+ const { kinds, reason, unknown } = parseScope(value);
25243
+ if (unknown.length > 0) refusals.push(unknownScope(sha, unknown));
25244
+ else override = { sha, reason, kinds };
25245
+ }
25246
+ return { override, refusals };
25247
+ }
25003
25248
 
25004
25249
  // src/test-policy-core.ts
25005
25250
  var POLICY_FILE = "test-policy.json";
25006
25251
  var TEST_RE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
25007
25252
  var PY_TEST_RE = /(?:^|\/)test_[^/]*\.py$|_test\.py$/;
25008
- var TRAILER_KEY = "Test-Policy-Override";
25009
- var OVERRIDE_RE = /^Test-Policy-Override:\s*(.+)$/im;
25010
- var REC = "";
25011
- var FLD = "";
25012
- var WAIVABLE_KINDS = [
25013
- "mandatory-zone-untested",
25014
- "unrequested-test-file",
25015
- "protected-removed",
25016
- "stale-protected-entry",
25017
- "stale-satisfied-by"
25018
- ];
25019
25253
  function translate(glob) {
25020
25254
  let out = "";
25021
25255
  for (let i = 0; i < glob.length; i++) {
@@ -25373,15 +25607,15 @@ function evaluate(changed, policy, present = () => false) {
25373
25607
  }
25374
25608
  return findings;
25375
25609
  }
25376
- function git(args, cwd) {
25377
- return (0, import_node_child_process13.execFileSync)("git", args, { windowsHide: true, cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
25610
+ function git2(args, cwd) {
25611
+ return (0, import_node_child_process15.execFileSync)("git", args, { windowsHide: true, cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
25378
25612
  }
25379
25613
  var COAUTHOR_KEY = "Co-authored-by";
25380
25614
  var GH_MESSAGE_SEPARATOR = /^-{5,}$/;
25381
25615
  var LIFTED_KEYS = new RegExp(`^(?:${TRAILER_KEY}|${COAUTHOR_KEY}):`, "i");
25382
25616
  function parseTrailers(message, cwd) {
25383
25617
  try {
25384
- return (0, import_node_child_process13.execFileSync)("git", ["interpret-trailers", "--parse", "--unfold"], {
25618
+ return (0, import_node_child_process15.execFileSync)("git", ["interpret-trailers", "--parse", "--unfold"], {
25385
25619
  windowsHide: true,
25386
25620
  cwd,
25387
25621
  input: message,
@@ -25418,19 +25652,12 @@ function resolveBase(cwd, explicit) {
25418
25652
  const candidates = [explicit, process.env.TEST_POLICY_BASE, "origin/development", "origin/main"].filter(Boolean);
25419
25653
  for (const ref of candidates) {
25420
25654
  try {
25421
- return git(["merge-base", "HEAD", ref], cwd).trim();
25655
+ return git2(["merge-base", "HEAD", ref], cwd).trim();
25422
25656
  } catch {
25423
25657
  }
25424
25658
  }
25425
25659
  return null;
25426
25660
  }
25427
- function isShallowRepository(cwd) {
25428
- try {
25429
- return git(["rev-parse", "--is-shallow-repository"], cwd).trim() !== "false";
25430
- } catch {
25431
- return true;
25432
- }
25433
- }
25434
25661
  function untrustworthyRange(cwd, base) {
25435
25662
  if (!base) {
25436
25663
  return {
@@ -25452,65 +25679,11 @@ function untrustworthyRange(cwd, base) {
25452
25679
  Fetch full history \u2014 actions/checkout with \`fetch-depth: 0\`, or \`git fetch --unshallow\`.`
25453
25680
  };
25454
25681
  }
25455
- function parseScope(value) {
25456
- const scoped = /^\[([^\]]*)\]\s*([\s\S]*)$/.exec(value);
25457
- if (!scoped) return { kinds: [...WAIVABLE_KINDS], reason: value, unknown: [] };
25458
- const named = scoped[1].split(",").map((k) => k.trim()).filter(Boolean);
25459
- return {
25460
- kinds: named,
25461
- reason: scoped[2].trim(),
25462
- unknown: named.filter((k) => !WAIVABLE_KINDS.includes(k))
25463
- };
25464
- }
25465
- function invisibleTrailer(sha, line) {
25466
- return {
25467
- kind: "malformed-override-trailer",
25468
- paths: [],
25469
- detail: `OVERRIDE TRAILER GIT CANNOT SEE \u2014 commit ${sha.slice(0, 8)} carries
25470
- ${line}
25471
- but \`git log --format='%(trailers:key=${TRAILER_KEY})'\` reports nothing for it, so the waiver would
25472
- exist only to the regex that granted it. The trailer was chosen over a flag because it can be
25473
- AUDITED, and a reason the auditor cannot see is not a reason this gate accepts (#3628).
25474
- Git reads trailers from the LAST paragraph only, and every line of that paragraph must be a
25475
- trailer or an indented continuation \u2014 one bare line such as \`Closes #123\` disqualifies the whole
25476
- block. Indent the continuation lines, and leave nothing but trailers in that paragraph.
25477
- This is fixable forward: push a LATER commit on this branch carrying a well-formed trailer.
25478
- The nearest waiver wins, and it supersedes this one \u2014 no force-push, no re-filed branch.`
25479
- };
25480
- }
25481
- function unknownScope(sha, unknown) {
25482
- return {
25483
- kind: "malformed-override-trailer",
25484
- paths: [],
25485
- detail: `UNKNOWN OVERRIDE SCOPE \u2014 commit ${sha.slice(0, 8)} scopes its waiver to ${unknown.join(", ")}, which this
25486
- gate cannot report.
25487
- Waivable kinds: ${WAIVABLE_KINDS.join(", ")}.
25488
- Refused rather than widened: treating a typo as "waive everything" is how a scoped waiver turns
25489
- into a blanket exemption without anyone deciding it should.`
25490
- };
25491
- }
25492
- function readOverride(base, cwd) {
25493
- const format = `%H${FLD}%(trailers:key=${TRAILER_KEY},valueonly,unfold)${FLD}%B${REC}`;
25494
- const refusals = [];
25495
- let override = null;
25496
- for (const record of git(["log", `${base}..HEAD`, `--format=${format}`], cwd).split(REC)) {
25497
- const [sha, trailer, body] = record.replace(/^\s+/, "").split(FLD);
25498
- if (!sha) continue;
25499
- if (override) break;
25500
- const value = (trailer ?? "").trim();
25501
- if (!value) {
25502
- const shaped = OVERRIDE_RE.exec(body ?? "");
25503
- if (shaped) refusals.push(invisibleTrailer(sha, shaped[0].trim()));
25504
- continue;
25505
- }
25506
- const { kinds, reason, unknown } = parseScope(value);
25507
- if (unknown.length > 0) refusals.push(unknownScope(sha, unknown));
25508
- else override = { sha, reason, kinds };
25509
- }
25510
- return { override, refusals };
25682
+ function readOverride2(base, cwd) {
25683
+ return readOverride(base, cwd);
25511
25684
  }
25512
25685
  function changedFilesSince(base, cwd) {
25513
- const raw = git(["diff", "--name-status", "-M", base, "--"], cwd).trim();
25686
+ const raw = git2(["diff", "--name-status", "-M", base, "--"], cwd).trim();
25514
25687
  const changed = raw ? raw.split("\n").map((line) => {
25515
25688
  const parts = line.split(" ");
25516
25689
  const norm = (p) => p.replace(/\\/g, "/");
@@ -25518,12 +25691,12 @@ function changedFilesSince(base, cwd) {
25518
25691
  if (parts.length > 2) row.from = norm(parts[1]);
25519
25692
  return row;
25520
25693
  }) : [];
25521
- const untracked = git(["ls-files", "--others", "--exclude-standard", "-z"], cwd).split("\0").filter(Boolean).map((path2) => ({ status: "A", path: path2.replace(/\\/g, "/") }));
25694
+ const untracked = git2(["ls-files", "--others", "--exclude-standard", "-z"], cwd).split("\0").filter(Boolean).map((path2) => ({ status: "A", path: path2.replace(/\\/g, "/") }));
25522
25695
  return [...changed, ...untracked];
25523
25696
  }
25524
25697
  function blobAt(base, path2, cwd) {
25525
25698
  try {
25526
- return git(["show", `${base}:${path2}`], cwd);
25699
+ return git2(["show", `${base}:${path2}`], cwd);
25527
25700
  } catch {
25528
25701
  return null;
25529
25702
  }
@@ -25539,7 +25712,7 @@ function runTestPolicy(root, deps = {}) {
25539
25712
  before: blobAt(base, path2, root),
25540
25713
  after: readFileOrNull2((0, import_node_path22.join)(root, path2))
25541
25714
  }));
25542
- const lookup = deps.override !== void 0 ? { override: deps.override, refusals: [] } : !deps.changed && !refusal ? readOverride(base, root) : { override: null, refusals: [] };
25715
+ const lookup = deps.override !== void 0 ? { override: deps.override, refusals: [] } : !deps.changed && !refusal ? readOverride2(base, root) : { override: null, refusals: [] };
25543
25716
  const present = (path2) => exists((0, import_node_path22.join)(root, path2));
25544
25717
  const removedByThisDiff = removedPaths(changed);
25545
25718
  const staleFindings = [];
@@ -25572,7 +25745,8 @@ function runTestPolicy(root, deps = {}) {
25572
25745
  const commandPolicy = evaluateTestCommandPolicy({
25573
25746
  paths: changed.map((f) => f.path),
25574
25747
  mandatory: policy.mandatory,
25575
- regulated: policy.declared !== false
25748
+ regulated: policy.declared !== false,
25749
+ override: override && lookup.refusals.length === 0 ? { kinds: override.kinds } : null
25576
25750
  });
25577
25751
  const result = {
25578
25752
  ok: findings.length === 0,
@@ -25594,7 +25768,7 @@ function runTestPolicy(root, deps = {}) {
25594
25768
  }
25595
25769
 
25596
25770
  // src/dist-drift.ts
25597
- var import_node_child_process14 = require("node:child_process");
25771
+ var import_node_child_process16 = require("node:child_process");
25598
25772
  var import_node_crypto7 = require("node:crypto");
25599
25773
  var import_node_fs27 = require("node:fs");
25600
25774
  var import_node_os13 = require("node:os");
@@ -25739,7 +25913,7 @@ function bomPathFor(root) {
25739
25913
  }
25740
25914
  }
25741
25915
  function rebuildTo(packageRoot, outDir) {
25742
- (0, import_node_child_process14.execFileSync)(process.execPath, ["build.mjs"], {
25916
+ (0, import_node_child_process16.execFileSync)(process.execPath, ["build.mjs"], {
25743
25917
  cwd: packageRoot,
25744
25918
  env: { ...process.env, MMI_DIST_OUTDIR: outDir },
25745
25919
  windowsHide: true,
@@ -27456,9 +27630,9 @@ ${SSH_RECIPE_AGENT_NOTE}`);
27456
27630
 
27457
27631
  // src/schedules-commands.ts
27458
27632
  var import_promises4 = require("node:fs/promises");
27459
- var import_node_child_process15 = require("node:child_process");
27633
+ var import_node_child_process17 = require("node:child_process");
27460
27634
  var import_node_util7 = require("node:util");
27461
- var execFileP5 = (0, import_node_util7.promisify)(import_node_child_process15.execFile);
27635
+ var execFileP5 = (0, import_node_util7.promisify)(import_node_child_process17.execFile);
27462
27636
  var AWS_REGION = "eu-central-1";
27463
27637
  var AWS_TIMEOUT_MS = 3e4;
27464
27638
  var AWS_RETRY_DELAY_MS = 1500;
@@ -29551,7 +29725,7 @@ function registerBootstrapCommands(program3) {
29551
29725
  }
29552
29726
  let seedPrUrl;
29553
29727
  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(() => {
29728
+ 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
29729
  });
29556
29730
  const openPrs = await gh(["pr", "list", "--repo", repo, "--head", seedPlan.branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
29557
29731
  const prDecision = decideSeedPrAction(JSON.parse(openPrs.stdout || "[]"));
@@ -29600,8 +29774,8 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
29600
29774
  }
29601
29775
  if (o.execute && !onlyTarget && o.class === "deployable") {
29602
29776
  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");
29777
+ 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}`]);
29778
+ applied.push("merge settings: allow_auto_merge, squash, preserve merged remote branches");
29605
29779
  } catch (e) {
29606
29780
  applied.push(`merge settings (failed: ${e.message})`);
29607
29781
  }
@@ -30784,6 +30958,9 @@ function buildPrMergeArgs(input) {
30784
30958
  if (input.auto) args.push("--auto");
30785
30959
  return args;
30786
30960
  }
30961
+ function buildPrDisableAutoArgs(input) {
30962
+ return ["pr", "merge", input.number, ...input.repoArgs, "--disable-auto"];
30963
+ }
30787
30964
  function basePolicyBlocksImmediateMerge(message) {
30788
30965
  return /base branch policy prohibits|protected branch|required status check|merge queue/i.test(message);
30789
30966
  }
@@ -30794,6 +30971,12 @@ function ghPrMergeLocalBranchDeleteWarning(message) {
30794
30971
  function mergeAutoRejectedPrAlreadyClean(message) {
30795
30972
  return /clean status \(enablePullRequestAutoMerge\)|is in clean status/i.test(message);
30796
30973
  }
30974
+ function assertRemoteBranchPreservedOnMerge(repo, deleteBranchOnMerge) {
30975
+ if (deleteBranchOnMerge === false) return;
30976
+ throw new Error(
30977
+ `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.`
30978
+ );
30979
+ }
30797
30980
  function mergeMethodFromParentCount(parentCount) {
30798
30981
  if (parentCount >= 2) return "merge";
30799
30982
  return void 0;
@@ -31043,6 +31226,7 @@ async function remoteBranchExists2(branch, options = {}) {
31043
31226
 
31044
31227
  // src/worktree-merge-cleanup.ts
31045
31228
  var import_node_fs36 = require("node:fs");
31229
+ var import_node_path34 = require("node:path");
31046
31230
 
31047
31231
  // src/worktree-evidence-archive.ts
31048
31232
  var import_node_fs35 = require("node:fs");
@@ -31221,6 +31405,18 @@ function formatArtifactsArchiveReceipt(archive) {
31221
31405
  function normPath2(p) {
31222
31406
  return p.replace(/\\/g, "/").replace(/\/+$/, "");
31223
31407
  }
31408
+ function removeResidueDirectory(wtPath) {
31409
+ try {
31410
+ try {
31411
+ 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"));
31412
+ } catch {
31413
+ }
31414
+ (0, import_node_fs36.rmSync)(wtPath, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
31415
+ return { ok: true };
31416
+ } catch (e) {
31417
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
31418
+ }
31419
+ }
31224
31420
  function samePath(a, b) {
31225
31421
  const na = normPath2(a);
31226
31422
  const nb = normPath2(b);
@@ -31261,19 +31457,19 @@ function formatWorktreeRemovalFailureDetail(options) {
31261
31457
  const detail = options.removeError.trim() || "git worktree remove exited nonzero";
31262
31458
  return `${detail}; registered=${options.registered ? "yes" : "no"}; pathExists=${options.pathExists ? "yes" : "no"}; path=${options.wtPath}`;
31263
31459
  }
31264
- async function resolveMergeBaseTimestampMs(git2, baseRef, branch) {
31460
+ async function resolveMergeBaseTimestampMs(git3, baseRef, branch) {
31265
31461
  try {
31266
- const mergeBase = (await git2(["merge-base", baseRef, branch])).trim();
31462
+ const mergeBase = (await git3(["merge-base", baseRef, branch])).trim();
31267
31463
  if (!mergeBase) return 0;
31268
- const sec = parseCommitTimestampSec(await git2(["show", "-s", "--format=%ct", mergeBase]));
31464
+ const sec = parseCommitTimestampSec(await git3(["show", "-s", "--format=%ct", mergeBase]));
31269
31465
  return sec ? sec * 1e3 : 0;
31270
31466
  } catch {
31271
31467
  return 0;
31272
31468
  }
31273
31469
  }
31274
- async function verifyBranchHead(git2, branch, expectedHeadOid) {
31470
+ async function verifyBranchHead(git3, branch, expectedHeadOid) {
31275
31471
  if (!expectedHeadOid) return { ok: false, reason: "branch-head-unverified" };
31276
- const currentHead = (await git2(["rev-parse", `refs/heads/${branch}`]).catch(() => "") || "").trim();
31472
+ const currentHead = (await git3(["rev-parse", `refs/heads/${branch}`]).catch(() => "") || "").trim();
31277
31473
  if (!currentHead) return { ok: false, reason: "branch-head-unverified" };
31278
31474
  if (currentHead !== expectedHeadOid) {
31279
31475
  return { ok: false, reason: "unpushed-branch", error: `${currentHead} != ${expectedHeadOid}` };
@@ -31288,17 +31484,17 @@ async function teardownWorktreeStage(worktreePath) {
31288
31484
  return { status: "failed", error: e instanceof Error ? e.message : String(e) };
31289
31485
  }
31290
31486
  }
31291
- async function removeWorktreeWithReconcile(wtPath, git2, listWorktrees, pathExists) {
31487
+ async function removeWorktreeWithReconcile(wtPath, git3, listWorktrees, pathExists) {
31292
31488
  const attemptRemove = async () => {
31293
31489
  try {
31294
- await git2(["worktree", "remove", "--force", wtPath]);
31490
+ await git3(["worktree", "remove", "--force", wtPath]);
31295
31491
  return void 0;
31296
31492
  } catch (e) {
31297
31493
  return formatGitCommandError(e);
31298
31494
  }
31299
31495
  };
31300
31496
  const pruneAndVerify = async (reason) => {
31301
- await git2(["worktree", "prune"]).catch(() => "");
31497
+ await git3(["worktree", "prune"]).catch(() => "");
31302
31498
  let worktrees2;
31303
31499
  try {
31304
31500
  worktrees2 = await listWorktrees();
@@ -31328,6 +31524,16 @@ async function removeWorktreeWithReconcile(wtPath, git2, listWorktrees, pathExis
31328
31524
  };
31329
31525
  }
31330
31526
  if (!isWorktreePathRegistered(worktrees, wtPath)) return pruneAndVerify("reconciled-unregistered");
31527
+ await git3(["worktree", "prune"]).catch(() => "");
31528
+ try {
31529
+ worktrees = await listWorktrees();
31530
+ } catch (e) {
31531
+ return {
31532
+ status: "failed",
31533
+ error: `could not verify worktree registration after prune: ${formatGitCommandError(e)}; path=${wtPath}`
31534
+ };
31535
+ }
31536
+ if (!isWorktreePathRegistered(worktrees, wtPath)) return pruneAndVerify("reconciled-pruned");
31331
31537
  const retryError = await attemptRemove();
31332
31538
  if (!retryError) return pruneAndVerify("reconciled-after-retry");
31333
31539
  if (isNotAWorkingTreeMessage(retryError) || isNotAWorkingTreeMessage(firstError)) {
@@ -31405,8 +31611,8 @@ async function cleanupPrMergeLocalBranch(branch, options) {
31405
31611
  return report;
31406
31612
  }
31407
31613
  const safeCwd = selectSafeWorktreeCwd([...afterWorktrees, ...beforeWorktrees], wtPath, { pathExists });
31408
- const git2 = (args) => safeCwd ? execGit(["-C", safeCwd, ...args]) : execGit(args);
31409
- const headCheck = await verifyBranchHead(git2, branch, options.expectedHeadOid);
31614
+ const git3 = (args) => safeCwd ? execGit(["-C", safeCwd, ...args]) : execGit(args);
31615
+ const headCheck = await verifyBranchHead(git3, branch, options.expectedHeadOid);
31410
31616
  if (!headCheck.ok) {
31411
31617
  report.localBranch = {
31412
31618
  name: branch,
@@ -31469,8 +31675,8 @@ async function cleanupPrMergeLocalBranch(branch, options) {
31469
31675
  }
31470
31676
  const removal = await removeWorktreeWithReconcile(
31471
31677
  wtPath,
31472
- git2,
31473
- async () => parseGitWorktreePorcelain(await git2(["worktree", "list", "--porcelain"])),
31678
+ git3,
31679
+ async () => parseGitWorktreePorcelain(await git3(["worktree", "list", "--porcelain"])),
31474
31680
  pathExists
31475
31681
  );
31476
31682
  if (removal.status === "failed") {
@@ -31493,11 +31699,20 @@ async function cleanupPrMergeLocalBranch(branch, options) {
31493
31699
  stageTeardown,
31494
31700
  tmpEvidenceCount: tmpEvidence.length
31495
31701
  };
31702
+ if (pathExists(wtPath)) {
31703
+ const residue = options.removeResidueDir?.(wtPath) ?? removeResidueDirectory(wtPath);
31704
+ if (residue.ok) {
31705
+ report.worktree.residue = "swept";
31706
+ } else {
31707
+ report.worktree.residue = "left";
31708
+ report.worktree.residueError = residue.error;
31709
+ }
31710
+ }
31496
31711
  try {
31497
- await git2(["branch", "-D", branch]);
31712
+ await git3(["branch", "-D", branch]);
31498
31713
  report.localBranch = { name: branch, status: "deleted" };
31499
31714
  } catch (e) {
31500
- const remaining = await git2(["branch", "--list", branch]).catch(() => "");
31715
+ const remaining = await git3(["branch", "--list", branch]).catch(() => "");
31501
31716
  report.localBranch = remaining.trim() ? { name: branch, status: "failed", error: e instanceof Error ? e.message : String(e) } : { name: branch, status: "already-gone" };
31502
31717
  }
31503
31718
  return report;
@@ -31520,6 +31735,9 @@ function renderPrMergeCleanupLines(cleanup) {
31520
31735
  } else if (wt.status === "preserved") {
31521
31736
  lines.push(`pr merge: preserved worktree ${wt.path} (--preserve-worktree)`);
31522
31737
  }
31738
+ if (wt.residue === "left") {
31739
+ lines.push(`pr merge: worktree ${wt.path} registration is gone but residue remains \u2014 delete by hand: ${wt.residueError ?? wt.path}`);
31740
+ }
31523
31741
  if (wt.artifactsArchive?.status === "archived" && wt.artifactsArchive.path) {
31524
31742
  lines.push(`pr merge: archived worktree evidence to ${wt.artifactsArchive.path}`);
31525
31743
  } else if (wt.artifactsArchive?.status === "blocked" && wt.artifactsArchive.error) {
@@ -31528,6 +31746,17 @@ function renderPrMergeCleanupLines(cleanup) {
31528
31746
  return lines;
31529
31747
  }
31530
31748
 
31749
+ // src/utf8-receipt.ts
31750
+ var import_node_fs37 = require("node:fs");
31751
+ var import_node_path35 = require("node:path");
31752
+ function writeUtf8Receipt(path2, text) {
31753
+ (0, import_node_fs37.mkdirSync)((0, import_node_path35.dirname)(path2), { recursive: true });
31754
+ const body = text.endsWith("\n") ? text : `${text}
31755
+ `;
31756
+ (0, import_node_fs37.writeFileSync)(path2, body, "utf8");
31757
+ return Buffer.byteLength(body, "utf8");
31758
+ }
31759
+
31531
31760
  // src/board-commands.ts
31532
31761
  function refuseRateLimited(e, json) {
31533
31762
  if (!isRateLimitedError(e)) return false;
@@ -31555,7 +31784,9 @@ function registerBoardCommands(program3) {
31555
31784
  includeAllBodies: o.bodies,
31556
31785
  allowPartial: o.allowPartial
31557
31786
  }, o.direct ? {} : { snapshot: registryClientDeps(config) });
31558
- console.log(o.json ? JSON.stringify(report) : renderBoardReport(report));
31787
+ const output = o.json ? JSON.stringify(report) : renderBoardReport(report);
31788
+ if (!o.out) console.log(output);
31789
+ else console.log(`Wrote board read to ${o.out} (UTF-8, ${writeUtf8Receipt(o.out, output)} bytes)`);
31559
31790
  } catch (e) {
31560
31791
  return failGraceful(`board read failed: ${withDiscoverMissDetail(e.message)}`);
31561
31792
  }
@@ -31564,7 +31795,7 @@ function registerBoardCommands(program3) {
31564
31795
  return alreadyClaimed ? `Check ${ref}: claimed and In Progress, no live contest - claim would renew the lease (nothing written)` : `Check ${ref}: free - claim would proceed (nothing written)`;
31565
31796
  }
31566
31797
  const board = program3.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
31567
- board.command("read", { isDefault: true }).description("read the board and print user-owned, claimable, and taken items").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--direct", "bypass the Hub snapshot and read the live board through direct GitHub GraphQL").option("--bundle-details", "fetch body/comments only for user-owned and claimable issues").option("--bodies", "fetch body/comments for EVERY scoped row, including taken and unowned in-flight ones \u2014 for consumers that scope by Status rather than ownership (#4861); implies --bundle-details and costs one extra read per row").option("--allow-partial", "return partial board results when later page/detail reads fail").addHelpText("after", "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n").action((o) => runBoardRead(o));
31798
+ board.command("read", { isDefault: true }).description("read the board and print user-owned, claimable, and taken items").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--direct", "bypass the Hub snapshot and read the live board through direct GitHub GraphQL").option("--bundle-details", "fetch body/comments only for user-owned and claimable issues").option("--bodies", "fetch body/comments for EVERY scoped row, including taken and unowned in-flight ones \u2014 for consumers that scope by Status rather than ownership (#4861); implies --bundle-details and costs one extra read per row").option("--allow-partial", "return partial board results when later page/detail reads fail").option("--out <path>", "write the output to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5802)").addHelpText("after", "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nNever capture the JSON with a shell redirect on Windows: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n").action((o) => runBoardRead(o));
31568
31799
  withExamples(mutating(
31569
31800
  board.command("claim <issues...>").description("claim issues: assign them and move their Project v2 Status to In Progress \u2014 idempotent, so an item already yours and In Progress succeeds unchanged (one or more refs)").addHelpText("after", "\nevery claim stamps a lane-identity marker comment on the issue (`<!-- mmi-claim: \u2026 -->`,\nsurface/session@host) so other agents can attribute the hold (#3727). The session is the\nhost-exported id when the surface provides one, otherwise a per-process `synth-` fallback \u2014\na claim is never anonymous (#5245). `board show`, doctor and unclaim read the latest marker.\n").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--force", "take an item already claimed by another lane that shows live evidence of active work (#3727)").option("--check", "read-only: run every claim gate and report the verdict, writing nothing \u2014 exits 1 with the same refusal a real claim would raise (#4511)").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails"),
31570
31801
  (_opts, args) => ({ command: "board claim", issues: args[0] ?? [] })
@@ -32227,7 +32458,7 @@ async function checkDocsIndexAtHead(opts, deps) {
32227
32458
  }
32228
32459
 
32229
32460
  // src/issue-commands.ts
32230
- var import_node_fs37 = require("node:fs");
32461
+ var import_node_fs38 = require("node:fs");
32231
32462
  var import_node_crypto11 = require("node:crypto");
32232
32463
 
32233
32464
  // src/learning-closure-rate.ts
@@ -32414,7 +32645,7 @@ async function editIssue(client, options, deps = {}) {
32414
32645
  const url = `https://github.com/${repo}/issues/${parsed.number}`;
32415
32646
  const patch = {};
32416
32647
  let bodyChanged = false;
32417
- const textDeps = () => deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs37.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
32648
+ const textDeps = () => deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs38.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
32418
32649
  if (options.titleFile !== void 0) {
32419
32650
  patch.title = await resolveIssueTitle({ title: options.title, titleFile: options.titleFile }, textDeps());
32420
32651
  } else if (options.title !== void 0) {
@@ -33031,7 +33262,7 @@ function extendCreateCommand(issue2, batchAttach) {
33031
33262
  if (opts.batch) {
33032
33263
  let specs;
33033
33264
  try {
33034
- const raw = (0, import_node_fs37.readFileSync)(opts.batch, "utf8");
33265
+ const raw = (0, import_node_fs38.readFileSync)(opts.batch, "utf8");
33035
33266
  specs = JSON.parse(raw);
33036
33267
  if (!Array.isArray(specs)) throw new Error("batch file must contain a JSON array");
33037
33268
  } catch (e) {
@@ -33106,8 +33337,8 @@ ${lines}`, {
33106
33337
  }
33107
33338
 
33108
33339
  // src/train-commands.ts
33109
- var import_node_fs38 = require("node:fs");
33110
- var import_node_path34 = require("node:path");
33340
+ var import_node_fs39 = require("node:fs");
33341
+ var import_node_path36 = require("node:path");
33111
33342
  var RELEASE_BUMP_INTENTS = ["major", "minor", "patch"];
33112
33343
  function resolveReleaseBumpIntent(raw) {
33113
33344
  const intent = typeof raw === "string" ? raw.trim() : "";
@@ -33118,7 +33349,7 @@ function resolveReleaseBumpIntent(raw) {
33118
33349
  }
33119
33350
  function readRepoVersion() {
33120
33351
  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;
33352
+ return JSON.parse((0, import_node_fs39.readFileSync)((0, import_node_path36.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
33122
33353
  } catch {
33123
33354
  return void 0;
33124
33355
  }
@@ -33275,9 +33506,9 @@ function registerDeployCommands(program3) {
33275
33506
  }
33276
33507
 
33277
33508
  // src/discovery-commands.ts
33278
- var import_node_fs39 = require("node:fs");
33509
+ var import_node_fs40 = require("node:fs");
33279
33510
  var import_node_os17 = require("node:os");
33280
- var import_node_path35 = require("node:path");
33511
+ var import_node_path37 = require("node:path");
33281
33512
  var GC_GH_TIMEOUT_MS2 = 2e4;
33282
33513
  async function collectStatus() {
33283
33514
  const repo = await resolveRepo();
@@ -33467,8 +33698,8 @@ async function collectOnboardStatus(opts = {}) {
33467
33698
  }
33468
33699
  const home = (0, import_node_os17.homedir)();
33469
33700
  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)
33701
+ readKnown: () => readFileSyncSafe((0, import_node_path37.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs40.readFileSync),
33702
+ readSettings: () => readFileSyncSafe((0, import_node_path37.join)(home, ".claude", "settings.json"), import_node_fs40.readFileSync)
33472
33703
  });
33473
33704
  return { track, board, registry: registry2, secrets, plugin, doors: opts.doors ?? [], nextCommand };
33474
33705
  }
@@ -33777,7 +34008,10 @@ function findCommandInManifest(manifest, commandPath3) {
33777
34008
  return house ? houseRootAsParent(manifest, house) : void 0;
33778
34009
  }
33779
34010
  const visit = (command) => {
33780
- if (command.path === commandPath3 || command.flat_path === commandPath3) return command;
34011
+ const aliasMatches = command.aliases?.some(
34012
+ (alias) => [command.path, command.flat_path].some((path2) => `${path2.slice(0, path2.lastIndexOf(" ") + 1)}${alias}` === commandPath3)
34013
+ );
34014
+ if (command.path === commandPath3 || command.flat_path === commandPath3 || aliasMatches) return command;
33781
34015
  for (const child2 of command.subcommands) {
33782
34016
  const found = visit(child2);
33783
34017
  if (found) return found;
@@ -34228,8 +34462,8 @@ function registerPrLifecycleCommands(program3) {
34228
34462
  }
34229
34463
 
34230
34464
  // src/post-merge-recon.ts
34231
- var import_node_fs40 = require("node:fs");
34232
- var import_node_path36 = require("node:path");
34465
+ var import_node_fs41 = require("node:fs");
34466
+ var import_node_path38 = require("node:path");
34233
34467
 
34234
34468
  // src/cross-repo-filing-issue.ts
34235
34469
  function crossRepoFilingRetryCommand(prRepo, prNumber) {
@@ -34395,16 +34629,16 @@ function buildPostMergeReconRecovery(input) {
34395
34629
  }
34396
34630
  function writePostMergeReconRecovery(cwd, recovery) {
34397
34631
  const path2 = postMergeReconStatePath(cwd, recovery.repo, recovery.pr);
34398
- (0, import_node_fs40.mkdirSync)((0, import_node_path36.dirname)(path2), { recursive: true });
34399
- (0, import_node_fs40.writeFileSync)(path2, `${JSON.stringify(recovery, null, 2)}
34632
+ (0, import_node_fs41.mkdirSync)((0, import_node_path38.dirname)(path2), { recursive: true });
34633
+ (0, import_node_fs41.writeFileSync)(path2, `${JSON.stringify(recovery, null, 2)}
34400
34634
  `, "utf8");
34401
34635
  return path2;
34402
34636
  }
34403
34637
  function clearPostMergeReconRecovery(cwd, repo, pr2) {
34404
34638
  const path2 = postMergeReconStatePath(cwd, repo, pr2);
34405
- if (!(0, import_node_fs40.existsSync)(path2)) return;
34639
+ if (!(0, import_node_fs41.existsSync)(path2)) return;
34406
34640
  try {
34407
- (0, import_node_fs40.unlinkSync)(path2);
34641
+ (0, import_node_fs41.unlinkSync)(path2);
34408
34642
  } catch {
34409
34643
  }
34410
34644
  }
@@ -37461,17 +37695,17 @@ function parseOriginRepo(remoteUrl) {
37461
37695
  }
37462
37696
  function ghHostsConfigPath(env, platform2) {
37463
37697
  const sep3 = platform2 === "win32" ? "\\" : "/";
37464
- const join32 = (...parts) => parts.join(sep3);
37698
+ const join33 = (...parts) => parts.join(sep3);
37465
37699
  const explicit = env.GH_CONFIG_DIR?.trim();
37466
- if (explicit) return join32(explicit, "hosts.yml");
37700
+ if (explicit) return join33(explicit, "hosts.yml");
37467
37701
  if (platform2 === "win32") {
37468
37702
  const appData = (env.AppData ?? env.APPDATA)?.trim();
37469
- return appData ? join32(appData, "GitHub CLI", "hosts.yml") : void 0;
37703
+ return appData ? join33(appData, "GitHub CLI", "hosts.yml") : void 0;
37470
37704
  }
37471
37705
  const xdg = env.XDG_CONFIG_HOME?.trim();
37472
- if (xdg) return join32(xdg, "gh", "hosts.yml");
37706
+ if (xdg) return join33(xdg, "gh", "hosts.yml");
37473
37707
  const home = env.HOME?.trim();
37474
- return home ? join32(home, ".config", "gh", "hosts.yml") : void 0;
37708
+ return home ? join33(home, ".config", "gh", "hosts.yml") : void 0;
37475
37709
  }
37476
37710
  function parseGhHostsAccounts(yaml, host = "github.com") {
37477
37711
  let hostIndent = null;
@@ -37521,10 +37755,10 @@ function ghAccountCaveat(announcedLogin, accounts) {
37521
37755
  }
37522
37756
 
37523
37757
  // src/doctor-io.ts
37524
- var import_node_fs41 = require("node:fs");
37758
+ var import_node_fs42 = require("node:fs");
37525
37759
  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");
37760
+ var import_node_path39 = require("node:path");
37761
+ var import_node_child_process18 = require("node:child_process");
37528
37762
  var import_node_util8 = require("node:util");
37529
37763
 
37530
37764
  // src/discard-sink.ts
@@ -37533,27 +37767,27 @@ function nodeDiscardSinkPath(platform2 = process.platform) {
37533
37767
  }
37534
37768
 
37535
37769
  // src/doctor-io.ts
37536
- var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process16.execFile);
37770
+ var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process18.execFile);
37537
37771
  function execFileCapture(file, args, opts = {}) {
37538
37772
  const sink = nodeDiscardSinkPath();
37539
- const inFd = (0, import_node_fs41.openSync)(sink, "r");
37540
- const errFd = (0, import_node_fs41.openSync)(sink, "w");
37773
+ const inFd = (0, import_node_fs42.openSync)(sink, "r");
37774
+ const errFd = (0, import_node_fs42.openSync)(sink, "w");
37541
37775
  try {
37542
- return (0, import_node_child_process16.execFileSync)(file, args, {
37776
+ return (0, import_node_child_process18.execFileSync)(file, args, {
37543
37777
  ...opts,
37544
37778
  encoding: "utf8",
37545
37779
  stdio: [inFd, "pipe", errFd]
37546
37780
  });
37547
37781
  } finally {
37548
- (0, import_node_fs41.closeSync)(inFd);
37549
- (0, import_node_fs41.closeSync)(errFd);
37782
+ (0, import_node_fs42.closeSync)(inFd);
37783
+ (0, import_node_fs42.closeSync)(errFd);
37550
37784
  }
37551
37785
  }
37552
37786
  var MMI_PLUGIN_ID2 = "mmi@mutmutco";
37553
37787
  function installedClaudePluginVersion() {
37554
37788
  try {
37555
37789
  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")
37790
+ (0, import_node_fs42.readFileSync)((0, import_node_path39.join)((0, import_node_os18.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
37557
37791
  );
37558
37792
  const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
37559
37793
  if (versions.length === 0) return void 0;
@@ -37564,7 +37798,7 @@ function installedClaudePluginVersion() {
37564
37798
  }
37565
37799
  function manifestVersion(path2) {
37566
37800
  try {
37567
- const manifest = JSON.parse((0, import_node_fs41.readFileSync)(path2, "utf8"));
37801
+ const manifest = JSON.parse((0, import_node_fs42.readFileSync)(path2, "utf8"));
37568
37802
  return typeof manifest.version === "string" && manifest.version.trim() ? manifest.version.trim() : void 0;
37569
37803
  } catch {
37570
37804
  return void 0;
@@ -37573,12 +37807,12 @@ function manifestVersion(path2) {
37573
37807
  function readHermesPluginEvidence(env = process.env) {
37574
37808
  const host = hermesConfigRoot(env);
37575
37809
  const root = hermesPluginRoot(env);
37576
- const installRecordPresent = (0, import_node_fs41.existsSync)(root);
37577
- const manifestPath = (0, import_node_path37.join)(root, "plugin.yaml");
37810
+ const installRecordPresent = (0, import_node_fs42.existsSync)(root);
37811
+ const manifestPath = (0, import_node_path39.join)(root, "plugin.yaml");
37578
37812
  let installedVersion;
37579
37813
  let manifest = "missing";
37580
37814
  try {
37581
- const text = (0, import_node_fs41.readFileSync)(manifestPath, "utf8");
37815
+ const text = (0, import_node_fs42.readFileSync)(manifestPath, "utf8");
37582
37816
  let version;
37583
37817
  try {
37584
37818
  const parsed = JSON.parse(text).version;
@@ -37592,20 +37826,20 @@ function readHermesPluginEvidence(env = process.env) {
37592
37826
  if (version) {
37593
37827
  installedVersion = version;
37594
37828
  manifest = "valid";
37595
- } else if ((0, import_node_fs41.existsSync)(manifestPath)) manifest = "invalid";
37829
+ } else if ((0, import_node_fs42.existsSync)(manifestPath)) manifest = "invalid";
37596
37830
  } catch {
37597
- if ((0, import_node_fs41.existsSync)(manifestPath)) manifest = "invalid";
37831
+ if ((0, import_node_fs42.existsSync)(manifestPath)) manifest = "invalid";
37598
37832
  }
37599
37833
  let skills = false;
37600
37834
  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();
37835
+ skills = (0, import_node_fs42.existsSync)((0, import_node_path39.join)(root, "skills")) && (0, import_node_fs42.statSync)((0, import_node_path39.join)(root, "skills")).isDirectory();
37602
37836
  } catch {
37603
37837
  }
37604
37838
  return {
37605
- hostPresent: (0, import_node_fs41.existsSync)(host),
37839
+ hostPresent: (0, import_node_fs42.existsSync)(host),
37606
37840
  installRecordPresent,
37607
37841
  manifest,
37608
- payloadPresent: (0, import_node_fs41.existsSync)((0, import_node_path37.join)(root, "__init__.py")) && skills && manifest === "valid",
37842
+ payloadPresent: (0, import_node_fs42.existsSync)((0, import_node_path39.join)(root, "__init__.py")) && skills && manifest === "valid",
37609
37843
  ...installedVersion ? { installedVersion } : {}
37610
37844
  };
37611
37845
  }
@@ -37613,7 +37847,7 @@ function installedSurfacePluginVersion(surface) {
37613
37847
  const token = surfaceToken(surface);
37614
37848
  if (token === "kilo") {
37615
37849
  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();
37850
+ const stamp = (0, import_node_fs42.readFileSync)((0, import_node_path39.join)((0, import_node_os18.homedir)(), ".kilo", ".mmi-kilo-version"), "utf8").trim();
37617
37851
  return stamp || void 0;
37618
37852
  } catch {
37619
37853
  return void 0;
@@ -37621,13 +37855,13 @@ function installedSurfacePluginVersion(surface) {
37621
37855
  }
37622
37856
  if (token === "hermes") return readHermesPluginEvidence().installedVersion;
37623
37857
  if (token === "cursor") {
37624
- return manifestVersion((0, import_node_path37.join)(cursorLocalPluginRoot(), ".cursor-plugin", "plugin.json"));
37858
+ return manifestVersion((0, import_node_path39.join)(cursorLocalPluginRoot(), ".cursor-plugin", "plugin.json"));
37625
37859
  }
37626
37860
  if (token === "jervcode") {
37627
37861
  return installedJervCodePackageVersion();
37628
37862
  }
37629
37863
  if (token === "kimi") {
37630
- return manifestVersion((0, import_node_path37.join)(surfaceConfigRoot(surface), "plugins", "managed", "mmi", ".kimi-plugin", "plugin.json"));
37864
+ return manifestVersion((0, import_node_path39.join)(surfaceConfigRoot(surface), "plugins", "managed", "mmi", ".kimi-plugin", "plugin.json"));
37631
37865
  }
37632
37866
  if (token === "claude") return installedClaudePluginVersion();
37633
37867
  if (token !== "codex") return void 0;
@@ -37661,13 +37895,13 @@ function worktreeRootSync() {
37661
37895
  }
37662
37896
  var gitignorePath = () => {
37663
37897
  const root = worktreeRootSync();
37664
- return root === null ? null : (0, import_node_path37.join)(root, ".gitignore");
37898
+ return root === null ? null : (0, import_node_path39.join)(root, ".gitignore");
37665
37899
  };
37666
37900
  function readGitignore() {
37667
37901
  const path2 = gitignorePath();
37668
37902
  if (path2 === null) return null;
37669
37903
  try {
37670
- return (0, import_node_fs41.readFileSync)(path2, "utf8");
37904
+ return (0, import_node_fs42.readFileSync)(path2, "utf8");
37671
37905
  } catch {
37672
37906
  return null;
37673
37907
  }
@@ -37676,14 +37910,14 @@ function writeGitignore(content) {
37676
37910
  const path2 = gitignorePath();
37677
37911
  if (path2 === null) return false;
37678
37912
  try {
37679
- (0, import_node_fs41.writeFileSync)(path2, content, "utf8");
37913
+ (0, import_node_fs42.writeFileSync)(path2, content, "utf8");
37680
37914
  return true;
37681
37915
  } catch {
37682
37916
  return false;
37683
37917
  }
37684
37918
  }
37685
37919
  function lineEndingState(root) {
37686
- const attributesPresent = (0, import_node_fs41.existsSync)((0, import_node_path37.join)(root, ".gitattributes"));
37920
+ const attributesPresent = (0, import_node_fs42.existsSync)((0, import_node_path39.join)(root, ".gitattributes"));
37687
37921
  try {
37688
37922
  const output = execFileCapture("git", ["-C", root, "ls-files", "--eol", "--", ":(glob)**/*.sh"], {
37689
37923
  windowsHide: true
@@ -37750,8 +37984,8 @@ ${r.stderr ?? ""}`).catch(() => "");
37750
37984
  function ghMultiAccountCaveat(announcedLogin) {
37751
37985
  try {
37752
37986
  const hostsPath = ghHostsConfigPath(process.env, process.platform);
37753
- if (!hostsPath || !(0, import_node_fs42.existsSync)(hostsPath)) return void 0;
37754
- return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0, import_node_fs42.readFileSync)(hostsPath, "utf8")));
37987
+ if (!hostsPath || !(0, import_node_fs43.existsSync)(hostsPath)) return void 0;
37988
+ return ghAccountCaveat(announcedLogin, parseGhHostsAccounts((0, import_node_fs43.readFileSync)(hostsPath, "utf8")));
37755
37989
  } catch {
37756
37990
  return void 0;
37757
37991
  }
@@ -37759,7 +37993,7 @@ function ghMultiAccountCaveat(announcedLogin) {
37759
37993
  var ENV_HEAL_LOCK_STALE_MS = 10 * 6e4;
37760
37994
  var ENV_HEAL_LOCK_MAX_WAIT_MS = 2 * 6e4;
37761
37995
  function envHealLockPath(home) {
37762
- return (0, import_node_path38.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
37996
+ return (0, import_node_path40.join)(home, ".claude", "plugins", ".mmi-env-heal.lock");
37763
37997
  }
37764
37998
  async function withEnvHealLock(what, run) {
37765
37999
  try {
@@ -37896,7 +38130,7 @@ function mmiDoctorDeps(opts = {}) {
37896
38130
  );
37897
38131
  const result = applyPluginCachePlan(
37898
38132
  plan,
37899
- (p) => (0, import_node_fs42.rmSync)(p, { recursive: true }),
38133
+ (p) => (0, import_node_fs43.rmSync)(p, { recursive: true }),
37900
38134
  stagingApplyFsGuard(configRoot)
37901
38135
  );
37902
38136
  return {
@@ -37934,7 +38168,7 @@ function mmiDoctorDeps(opts = {}) {
37934
38168
  // adopted the generated routing index (MMG-Unlive: `docs/Archive/**` only, no index, no gate.yml) would
37935
38169
  // get a permanent — demanding an artifact it never asked for.
37936
38170
  docsIndexState: (root) => {
37937
- if (!(0, import_node_fs42.existsSync)((0, import_node_path38.join)(root, DOCS_INDEX_PATH))) return void 0;
38171
+ if (!(0, import_node_fs43.existsSync)((0, import_node_path40.join)(root, DOCS_INDEX_PATH))) return void 0;
37938
38172
  const real = createDocsIndexDeps(root);
37939
38173
  let docs2;
37940
38174
  const listDocs = () => docs2 ??= real.listDocs();
@@ -37943,7 +38177,7 @@ function mmiDoctorDeps(opts = {}) {
37943
38177
  },
37944
38178
  // #4168: working-tree heal — write if drifted, then re-check. Commit remains the operator's step.
37945
38179
  healDocsIndex: (root) => {
37946
- if (!(0, import_node_fs42.existsSync)((0, import_node_path38.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
38180
+ if (!(0, import_node_fs43.existsSync)((0, import_node_path40.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
37947
38181
  const real = createDocsIndexDeps(root);
37948
38182
  let docs2;
37949
38183
  const listDocs = () => docs2 ??= real.listDocs();
@@ -38040,7 +38274,18 @@ async function requireFreshTrainCli(commandName) {
38040
38274
  releasedVersion
38041
38275
  });
38042
38276
  if (report.ok) return;
38043
- throw new Error(staleTrainCliMessage(report, commandName));
38277
+ if (process.env.MMI_TRAIN_NO_SELF_UPDATE === "1") {
38278
+ throw new Error(staleTrainCliMessage(report, commandName));
38279
+ }
38280
+ const converge = await selfConvergeTrainCli({
38281
+ currentVersion: resolveClientVersion(),
38282
+ releasedVersion: report.releasedVersion,
38283
+ commandName
38284
+ });
38285
+ if (converge.kind === "failed") {
38286
+ throw new Error(`${commandName}: ${converge.error}. ${staleTrainCliMessage(report, commandName)}`);
38287
+ }
38288
+ process.exit(converge.reexecCode);
38044
38289
  }
38045
38290
  var cachedCommandPaths;
38046
38291
  function allCommandPaths() {
@@ -38253,19 +38498,19 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
38253
38498
  });
38254
38499
  var rules = program2.command("rules").description("org-managed .gitignore delivery");
38255
38500
  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");
38257
- const current = (0, import_node_fs42.existsSync)(path2) ? (0, import_node_fs42.readFileSync)(path2, "utf8") : null;
38501
+ const path2 = (0, import_node_path40.join)(process.cwd(), ".gitignore");
38502
+ const current = (0, import_node_fs43.existsSync)(path2) ? (0, import_node_fs43.readFileSync)(path2, "utf8") : null;
38258
38503
  const plan = planManagedGitignore(current);
38259
38504
  const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
38260
38505
  if (opts.json) {
38261
- if (opts.write && plan.changed) (0, import_node_fs42.writeFileSync)(path2, plan.content, "utf8");
38506
+ if (opts.write && plan.changed) (0, import_node_fs43.writeFileSync)(path2, plan.content, "utf8");
38262
38507
  console.log(JSON.stringify(plan, null, 2));
38263
38508
  if (!opts.write && plan.changed) process.exitCode = 1;
38264
38509
  return;
38265
38510
  }
38266
38511
  if (opts.write) {
38267
38512
  if (plan.changed) {
38268
- (0, import_node_fs42.writeFileSync)(path2, plan.content, "utf8");
38513
+ (0, import_node_fs43.writeFileSync)(path2, plan.content, "utf8");
38269
38514
  console.log(`mmi-cli devops org rules gitignore: updated .gitignore (${drift})`);
38270
38515
  } else {
38271
38516
  console.log("mmi-cli devops org rules gitignore: up to date");
@@ -38451,7 +38696,7 @@ function scheduleRelatedDiscovery(o) {
38451
38696
  try {
38452
38697
  const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body, "--fail-soft"];
38453
38698
  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() });
38699
+ spawnDetachedSelf(args, { spawn: import_node_child_process19.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
38455
38700
  } catch {
38456
38701
  }
38457
38702
  }
@@ -38841,7 +39086,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
38841
39086
  if (dupe) return fail(`org project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
38842
39087
  if (o.secretsFile) {
38843
39088
  try {
38844
- vars.push(`secrets=${(0, import_node_fs42.readFileSync)(o.secretsFile, "utf8")}`);
39089
+ vars.push(`secrets=${(0, import_node_fs43.readFileSync)(o.secretsFile, "utf8")}`);
38845
39090
  } catch (e) {
38846
39091
  return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
38847
39092
  }
@@ -39666,11 +39911,11 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
39666
39911
  }
39667
39912
  });
39668
39913
  async function listCiWorkflowPaths(cwd = process.cwd()) {
39669
- const wfDir = (0, import_node_path38.join)(cwd, ".github", "workflows");
39670
- if (!(0, import_node_fs42.existsSync)(wfDir)) return [];
39671
- return (0, import_node_fs42.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
39914
+ const wfDir = (0, import_node_path40.join)(cwd, ".github", "workflows");
39915
+ if (!(0, import_node_fs43.existsSync)(wfDir)) return [];
39916
+ return (0, import_node_fs43.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
39672
39917
  try {
39673
- return workflowReportsPrChecks((0, import_node_fs42.readFileSync)((0, import_node_path38.join)(wfDir, name), "utf8"));
39918
+ return workflowReportsPrChecks((0, import_node_fs43.readFileSync)((0, import_node_path40.join)(wfDir, name), "utf8"));
39674
39919
  } catch {
39675
39920
  return true;
39676
39921
  }
@@ -39736,16 +39981,16 @@ function ciAuditDeps() {
39736
39981
  // gate re-seed step is skipped gracefully rather than failing mid-run.
39737
39982
  readSeedFile: (path2) => {
39738
39983
  if (!root) return null;
39739
- const fullPath = (0, import_node_path38.join)(root, path2);
39740
- return (0, import_node_fs42.existsSync)(fullPath) ? (0, import_node_fs42.readFileSync)(fullPath, "utf8") : null;
39984
+ const fullPath = (0, import_node_path40.join)(root, path2);
39985
+ return (0, import_node_fs43.existsSync)(fullPath) ? (0, import_node_fs43.readFileSync)(fullPath, "utf8") : null;
39741
39986
  }
39742
39987
  };
39743
39988
  }
39744
39989
  function hubRoot() {
39745
- const fromPkg = (0, import_node_path38.join)(__dirname, "..", "..");
39990
+ const fromPkg = (0, import_node_path40.join)(__dirname, "..", "..");
39746
39991
  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();
39992
+ if ((0, import_node_fs43.existsSync)((0, import_node_path40.join)(fromPkg, marker))) return fromPkg;
39993
+ if ((0, import_node_fs43.existsSync)((0, import_node_path40.join)(process.cwd(), marker))) return process.cwd();
39749
39994
  return null;
39750
39995
  }
39751
39996
  async function waitLoopCorePool(label) {
@@ -40017,15 +40262,24 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
40017
40262
  else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
40018
40263
  if (result.status === "failed") process.exitCode = 1;
40019
40264
  });
40020
- jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--squash-body-file <path>", "squash commit body (overrides GitHub COMMIT_MESSAGES); use when a pushed commit mentions close/fix/resolve + #N that must not close (#5723)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal")).action(async (number, o) => {
40265
+ jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").addOption(new Option("--disable-auto", "disable a queued auto-merge without merging").conflicts(["auto", "wait", "squash", "merge", "rebase", "preserveWorktree", "gc", "squashBodyFile", "force"])).option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--squash-body-file <path>", "squash commit body (overrides GitHub COMMIT_MESSAGES); use when a pushed commit mentions close/fix/resolve + #N that must not close (#5723)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal")).action(async (number, o) => {
40021
40266
  const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
40022
40267
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
40268
+ if (o.disableAuto) {
40269
+ await execFileP2("gh", buildPrDisableAutoArgs({ number, repoArgs }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e) => {
40270
+ const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
40271
+ if (note) throw new Error(`gh pr merge ${number} --disable-auto: ${note}`);
40272
+ throw e;
40273
+ });
40274
+ console.log(JSON.stringify({ mergeStatus: "auto-merge-disabled", pr: number, ...o.repo ? { repo: o.repo } : {} }));
40275
+ return;
40276
+ }
40023
40277
  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) => {
40278
+ 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
40279
  if (!isGitHubRateLimitError(e) || !repoForPostCleanup) throw e;
40026
40280
  console.warn(`pr merge: gh GraphQL rate-limited \u2014 reading PR #${number} via REST instead (#4588).`);
40027
40281
  const snapshot = await fetchRestPrSnapshot(number, repoForPostCleanup);
40028
- return { head: snapshot.headRef, base: snapshot.baseRef, oid: snapshot.headSha };
40282
+ return { head: snapshot.headRef, base: snapshot.baseRef, oid: snapshot.headSha, state: snapshot.merged ? "MERGED" : snapshot.state.toUpperCase() };
40029
40283
  });
40030
40284
  const headRef = prMeta.head;
40031
40285
  const baseRef = prMeta.base;
@@ -40046,7 +40300,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
40046
40300
  const mergeSquashBody = squashBodyTextForMerge(
40047
40301
  closingGuardInput,
40048
40302
  method === "--squash",
40049
- o.squashBodyFile ? (0, import_node_fs42.readFileSync)(o.squashBodyFile, "utf8") : void 0
40303
+ o.squashBodyFile ? (0, import_node_fs43.readFileSync)(o.squashBodyFile, "utf8") : void 0
40050
40304
  );
40051
40305
  if (!o.squashBodyFile) warnSquashBodyClosingStrip("pr merge", closingGuardInput, mergeSquashBody);
40052
40306
  const closingGuardVerdict = evaluateClosingGuard(closingGuardInput, {
@@ -40060,6 +40314,11 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
40060
40314
  return;
40061
40315
  }
40062
40316
  if (closingGuardVerdict.message) console.warn(closingGuardVerdict.message);
40317
+ if (prMeta.state !== "MERGED" && !o.preserveWorktree) {
40318
+ if (!repoForPostCleanup) throw new Error("pr merge: repository is unreadable; cannot prove remote branch preservation");
40319
+ const repoSettings = await defaultGitHubClient().rest("GET", `repos/${repoForPostCleanup}`);
40320
+ assertRemoteBranchPreservedOnMerge(repoForPostCleanup, repoSettings.delete_branch_on_merge);
40321
+ }
40063
40322
  const startingPath = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
40064
40323
  const housekeeping = assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr merge", { force: o.force });
40065
40324
  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 +40366,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
40107
40366
  }
40108
40367
  const remoteBefore = await remoteBranchExists2(headRef);
40109
40368
  let upgradedToAuto = false;
40110
- let remoteNotAttemptedReason = "host-owned-lifecycle";
40369
+ let remoteNotAttemptedReason = "preserved-delayed-cleanup";
40111
40370
  const overrideBody = mergeSquashBody ? { ...writeSquashBodyFile(mergeSquashBody), text: mergeSquashBody } : await composeOverrideBodyFile(
40112
40371
  number,
40113
40372
  repoArgs,
@@ -40130,7 +40389,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
40130
40389
  }
40131
40390
  if (!repoForPostCleanup) throw e;
40132
40391
  console.warn(`pr merge: gh GraphQL rate-limited \u2014 merging PR #${number} via REST PUT instead (#4588).`);
40133
- const commitMessage = bodyFile ? (0, import_node_fs42.readFileSync)(bodyFile, "utf8") : void 0;
40392
+ const commitMessage = bodyFile ? (0, import_node_fs43.readFileSync)(bodyFile, "utf8") : void 0;
40134
40393
  await defaultGitHubClient().rest("PUT", `repos/${repoForPostCleanup}/pulls/${number}/merge`, {
40135
40394
  body: { merge_method: method.slice(2), ...commitMessage ? { commit_message: commitMessage } : {} },
40136
40395
  timeoutMs: GH_MUTATION_TIMEOUT_MS
@@ -40219,7 +40478,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
40219
40478
  preserveWorktree: o.preserveWorktree,
40220
40479
  gcAcknowledged: o.gc,
40221
40480
  expectedHeadOid: headRefOid,
40222
- pathExists: (p) => (0, import_node_fs42.existsSync)(p),
40481
+ pathExists: (p) => (0, import_node_fs43.existsSync)(p),
40223
40482
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout
40224
40483
  });
40225
40484
  } catch (e) {
@@ -40326,7 +40585,7 @@ function trainApplyDeps() {
40326
40585
  run: async (file, args) => {
40327
40586
  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
40587
  try {
40329
- return isWin2 && file === "npm" ? (await execFileP2("cmd.exe", ["/c", "npm", ...args], { timeout })).stdout : (await execFileP2(file, args, { timeout })).stdout;
40588
+ return isWin3 && file === "npm" ? (await execFileP2("cmd.exe", ["/c", "npm", ...args], { timeout })).stdout : (await execFileP2(file, args, { timeout })).stdout;
40330
40589
  } catch (e) {
40331
40590
  throw surfaceTrainSubprocessFailure(e, file, args);
40332
40591
  }
@@ -40422,6 +40681,7 @@ function renderReleaseResume(r) {
40422
40681
  if (r.dispatch) lines.push(` deploy: ${r.dispatch.note}`);
40423
40682
  if (r.devRollForward) lines.push(` development: ${r.devRollForward.note}`);
40424
40683
  if (r.rcAlignment) lines.push(` rc: ${r.rcAlignment.note}`);
40684
+ if (r.checkout) lines.push(` checkout: ${r.checkout.note}`);
40425
40685
  return lines.join("\n");
40426
40686
  }
40427
40687
  function renderReleaseAbort(r) {
@@ -40506,7 +40766,7 @@ for (const commandName of ["rcand", "release"]) {
40506
40766
  { flags: "--ack <shas>", description: "comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)" },
40507
40767
  { flags: "--dev", description: "full-track repos release development -> main directly, skipping rc (refuses if rc carries content not in development; no-op on direct-track repos) (#1062)" },
40508
40768
  { flags: "--abort", description: "with --apply, delete only a proven unpublished failed release candidate on any registered deploy model and restore local main for a same-version recut (#3944/#5650/#5657)" },
40509
- { flags: "--retry-publish <run-id>", description: "with --apply, retry failed jobs of one proven Hub publish release run (#3949)" }
40769
+ { flags: "--retry-publish <run-id>", description: "with --apply, retry one proven failed or cancelled Hub publish release run (#3949/#5797)" }
40510
40770
  ];
40511
40771
  for (const f of RELEASE_ONLY_FLAGS) {
40512
40772
  if (commandName === "release") {
@@ -40762,7 +41022,7 @@ function hotfixStatusDeps() {
40762
41022
  run: async (file, args) => {
40763
41023
  const timeout = file === "npm" ? STATUS_NPM_MS : file === "gh" ? STATUS_GH_MS : STATUS_GIT_MS;
40764
41024
  try {
40765
- return isWin2 && file === "npm" ? (await execFileP2("cmd.exe", ["/c", "npm", ...args], { timeout })).stdout : (await execFileP2(file, args, { timeout })).stdout;
41025
+ return isWin3 && file === "npm" ? (await execFileP2("cmd.exe", ["/c", "npm", ...args], { timeout })).stdout : (await execFileP2(file, args, { timeout })).stdout;
40766
41026
  } catch (e) {
40767
41027
  throw surfaceTrainSubprocessFailure(e, file, args);
40768
41028
  }
@@ -40805,8 +41065,7 @@ ${r.repo}: applied=[${r.applied.join("; ")}] skipped=[${r.skipped.join("; ")}]${
40805
41065
  console.log("\nDry-run \u2014 re-run with --apply to patch merge settings and activate product rulesets (master-admin).");
40806
41066
  }
40807
41067
  }
40808
- const applyFailed = applyResults.some((result) => result.errors.length > 0 || result.postApply?.state === "failed");
40809
- if (!audit.ok || applyFailed) process.exitCode = 1;
41068
+ if (ciReconcileExitFailed({ apply: o.apply === true, audit, applyResults })) process.exitCode = 1;
40810
41069
  });
40811
41070
  registerBootstrapCommands(program2);
40812
41071
  var access = program2.command("access").description("org access audit (read-only)");
@@ -40855,18 +41114,18 @@ access.command("audit").description("audit collaborator roles + train-branch pus
40855
41114
  targets = resolution.targets;
40856
41115
  }
40857
41116
  const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
40858
- const fileMatrix = (0, import_node_fs42.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs42.readFileSync)("access-matrix.json", "utf8")) : {};
41117
+ const fileMatrix = (0, import_node_fs43.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs43.readFileSync)("access-matrix.json", "utf8")) : {};
40859
41118
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
40860
41119
  const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
40861
- const fileContracts = (0, import_node_fs42.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs42.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
41120
+ const fileContracts = (0, import_node_fs43.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs43.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
40862
41121
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
40863
- const sanctioned = (0, import_node_fs42.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs42.readFileSync)("access-matrix.json", "utf8")) : {};
41122
+ const sanctioned = (0, import_node_fs43.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs43.readFileSync)("access-matrix.json", "utf8")) : {};
40864
41123
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess, sanctioned);
40865
41124
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
40866
41125
  if (!report.ok) process.exitCode = 1;
40867
41126
  });
40868
41127
  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";
41128
+ var isWin3 = process.platform === "win32";
40870
41129
  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
41130
  if (opts.guide) {
40872
41131
  consoleIo.log(`MMI Agentic Onboarding: ${CANONICAL_ONBOARDING_URL}`);
@@ -40892,16 +41151,16 @@ function directoryBytes(path2) {
40892
41151
  let total = 0;
40893
41152
  let entries;
40894
41153
  try {
40895
- entries = (0, import_node_fs42.readdirSync)(path2, { withFileTypes: true });
41154
+ entries = (0, import_node_fs43.readdirSync)(path2, { withFileTypes: true });
40896
41155
  } catch {
40897
41156
  return 0;
40898
41157
  }
40899
41158
  for (const entry of entries) {
40900
- const child2 = (0, import_node_path38.join)(path2, entry.name);
41159
+ const child2 = (0, import_node_path40.join)(path2, entry.name);
40901
41160
  if (entry.isDirectory()) total += directoryBytes(child2);
40902
41161
  else {
40903
41162
  try {
40904
- total += (0, import_node_fs42.statSync)(child2).size;
41163
+ total += (0, import_node_fs43.statSync)(child2).size;
40905
41164
  } catch {
40906
41165
  }
40907
41166
  }
@@ -40909,25 +41168,25 @@ function directoryBytes(path2) {
40909
41168
  return total;
40910
41169
  }
40911
41170
  function listDirEntries(dir) {
40912
- return (0, import_node_fs42.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
41171
+ return (0, import_node_fs43.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
40913
41172
  }
40914
41173
  function readInstalledPluginRefs(configRoot) {
40915
41174
  const p = installedPluginsPathForConfig(configRoot);
40916
- if (!(0, import_node_fs42.existsSync)(p)) return [];
41175
+ if (!(0, import_node_fs43.existsSync)(p)) return [];
40917
41176
  try {
40918
- return installedPluginPaths((0, import_node_fs42.readFileSync)(p, "utf8"));
41177
+ return installedPluginPaths((0, import_node_fs43.readFileSync)(p, "utf8"));
40919
41178
  } catch {
40920
41179
  return null;
40921
41180
  }
40922
41181
  }
40923
41182
  function pluginCacheFsDeps(configRoot, dirBytes) {
40924
41183
  return {
40925
- exists: (p) => (0, import_node_fs42.existsSync)(p),
40926
- listVersionDirs: (root) => (0, import_node_fs42.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
41184
+ exists: (p) => (0, import_node_fs43.existsSync)(p),
41185
+ listVersionDirs: (root) => (0, import_node_fs43.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
40927
41186
  dirBytes,
40928
- listStagingDirs: (root) => (0, import_node_fs42.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
41187
+ listStagingDirs: (root) => (0, import_node_fs43.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
40929
41188
  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) };
41189
+ return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path40.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs43.statSync)(p).mtimeMs) };
40931
41190
  } catch {
40932
41191
  return { name: d.name, mtimeMs: Date.now() };
40933
41192
  }
@@ -40941,10 +41200,10 @@ function stagingApplyFsGuard(configRoot) {
40941
41200
  return {
40942
41201
  referencedPaths: () => readInstalledPluginRefs(configRoot),
40943
41202
  mtimeMs: (name) => {
40944
- const p = (0, import_node_path38.join)(stagingRoot, name);
40945
- if (!(0, import_node_fs42.existsSync)(p)) return null;
41203
+ const p = (0, import_node_path40.join)(stagingRoot, name);
41204
+ if (!(0, import_node_fs43.existsSync)(p)) return null;
40946
41205
  try {
40947
- return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs42.statSync)(q).mtimeMs);
41206
+ return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs43.statSync)(q).mtimeMs);
40948
41207
  } catch {
40949
41208
  return null;
40950
41209
  }
@@ -40970,7 +41229,7 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
40970
41229
  { withBytes: true, configRoot, includeStaging: surface !== "codex" }
40971
41230
  );
40972
41231
  const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
40973
- const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs42.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
41232
+ const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs43.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
40974
41233
  const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
40975
41234
  if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
40976
41235
  else console.log(renderPluginCachePlan(plan, result));