@mutmutco/codex-plugin 4.5.95 → 4.5.97

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mmi",
3
- "version": "4.5.95",
3
+ "version": "4.5.97",
4
4
  "mmiCompat": "4.x",
5
5
  "description": "MMI workflow skills and org gates delivery.",
6
6
  "author": {
package/cli/dist/main.cjs CHANGED
@@ -5927,7 +5927,7 @@ async function waitForPrChecks(deps) {
5927
5927
  reason = NO_CI_LIVE_CHECKS_CONTRADICTION_REASON;
5928
5928
  deps.log?.(`merge CI policy contradiction: ${reason}; waiting for live PR checks`);
5929
5929
  policy = "wait-for-checks";
5930
- queuedStates.push(firstState);
5930
+ if (!deps.pollChecksWithEvidence) queuedStates.push(firstState);
5931
5931
  } catch (e) {
5932
5932
  if (!isRetryableGitHubWaitReadError(e)) {
5933
5933
  return { policy, status: "failure", reason: waitReadFailureMessage(e), detail: "github-read-failed" };
@@ -5955,6 +5955,8 @@ async function waitForPrChecks(deps) {
5955
5955
  let successStreak = 0;
5956
5956
  let failureStreak = 0;
5957
5957
  let zeroRunsStreak = 0;
5958
+ let evidenceHeadSha;
5959
+ let lastEvidence;
5958
5960
  report("starting");
5959
5961
  while (now() < deadline) {
5960
5962
  if (deps.pollRateLimit) {
@@ -5986,7 +5988,17 @@ async function waitForPrChecks(deps) {
5986
5988
  }
5987
5989
  let state;
5988
5990
  try {
5989
- state = queuedStates.shift() ?? await deps.pollChecks();
5991
+ if (queuedStates.length) state = queuedStates.shift();
5992
+ else if (deps.pollChecksWithEvidence) {
5993
+ lastEvidence = await deps.pollChecksWithEvidence();
5994
+ if (evidenceHeadSha !== void 0 && evidenceHeadSha !== lastEvidence.headSha) {
5995
+ successStreak = 0;
5996
+ failureStreak = 0;
5997
+ zeroRunsStreak = 0;
5998
+ }
5999
+ evidenceHeadSha = lastEvidence.headSha;
6000
+ state = lastEvidence.state;
6001
+ } else state = await deps.pollChecks();
5990
6002
  } catch (e) {
5991
6003
  if (!isRetryableGitHubWaitReadError(e)) {
5992
6004
  return { policy, status: "failure", reason: waitReadFailureMessage(e), detail: "github-read-failed", waitedMs: now() - started };
@@ -6032,7 +6044,13 @@ async function waitForPrChecks(deps) {
6032
6044
  if (state === "success") {
6033
6045
  successStreak += 1;
6034
6046
  if (successStreak >= PR_CHECKS_SUCCESS_CONFIRMATIONS) {
6035
- return { policy, status: "success", reason, detail: "checks-success" };
6047
+ return {
6048
+ policy,
6049
+ status: "success",
6050
+ reason,
6051
+ detail: "checks-success",
6052
+ ...lastEvidence ? { headSha: lastEvidence.headSha, checks: lastEvidence.checks, workflowRuns: lastEvidence.workflows } : {}
6053
+ };
6036
6054
  }
6037
6055
  lastDetail = "confirming-success";
6038
6056
  await deps.sleep(PR_CHECKS_CONFIRM_MS);
@@ -6347,6 +6365,36 @@ async function pollRestPrChecks(prNumber, repo, gh2 = defaultGhApi, requiredCont
6347
6365
  const { relevant } = partitionByRequired(entries, requiredContexts);
6348
6366
  return derivePollState(relevant);
6349
6367
  }
6368
+ async function pollRestPrChecksWithWorkflowEvidence(prNumber, repo, gh2 = defaultGhApi) {
6369
+ const snapshot = await fetchRestPrSnapshot(prNumber, repo, gh2);
6370
+ const [entries, rawRuns] = await Promise.all([
6371
+ fetchHeadCheckEntries(snapshot.headSha, repo, gh2),
6372
+ gh2(["--paginate", `repos/${repo}/actions/runs?head_sha=${encodeURIComponent(snapshot.headSha)}&per_page=100`, "--jq", ".workflow_runs[] | {id, name, head_sha, status, conclusion, html_url}"])
6373
+ ]);
6374
+ const checks = entries.filter((entry) => !AUXILIARY_RELEASE_CHECK_CONTEXTS.has(entry.name)).map((entry) => ({ name: entry.name, state: entry.bucket }));
6375
+ const workflows = parseNdjsonLines(rawRuns).flatMap((run) => {
6376
+ if (run.head_sha !== snapshot.headSha || typeof run.id !== "number" || typeof run.name !== "string" || typeof run.status !== "string") return [];
6377
+ if (AUXILIARY_RELEASE_CHECK_CONTEXTS.has(run.name)) return [];
6378
+ return [{
6379
+ id: run.id,
6380
+ name: run.name,
6381
+ headSha: snapshot.headSha,
6382
+ status: run.status,
6383
+ conclusion: typeof run.conclusion === "string" ? run.conclusion : null,
6384
+ ...typeof run.html_url === "string" ? { url: run.html_url } : {}
6385
+ }];
6386
+ });
6387
+ const workflowBuckets = workflows.map((run) => {
6388
+ if (run.status !== "completed") return "pending";
6389
+ return run.conclusion === "success" || run.conclusion === "neutral" || run.conclusion === "skipped" ? "pass" : "fail";
6390
+ });
6391
+ return {
6392
+ state: derivePollState([...checks.map((check) => check.state), ...workflowBuckets]),
6393
+ headSha: snapshot.headSha,
6394
+ checks,
6395
+ workflows
6396
+ };
6397
+ }
6350
6398
  async function pollRestCommitChecks(sha, repo, gh2 = defaultGhApi) {
6351
6399
  return derivePollState(partitionByRequired(await fetchHeadCheckEntries(sha, repo, gh2), null).relevant);
6352
6400
  }
@@ -7460,11 +7508,11 @@ function workflowEntry(repo, workflowPath, yamlText) {
7460
7508
  };
7461
7509
  }
7462
7510
  var DECLARED_ENTRIES = [
7463
- { name: "mmi-backup (mm-central, mm-work, mmi-oguz)", cadence: "17 2 * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-backup \u2192 /opt/mmi-control/nightly-backup.sh; every Docker volume is tarred and every running Postgres container is pg_dumpall'd beside it (#7147; source scripts/nightly-backup.sh); zuber (46.225.58.31) is gone: deleted 2026-09-18 by the owner-approved ZuberShade decommission (MMC-ZuberShade#1838, snapshot kept); Hetzner has since reassigned that IP to another machine (#7351)" },
7511
+ { name: "mmi-backup (mm-central, mm-work)", cadence: "17 2 * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-backup \u2192 /opt/mmi-control/nightly-backup.sh; every Docker volume is tarred and every running Postgres container is pg_dumpall'd beside it (#7147; source scripts/nightly-backup.sh); owner-approved mmi-oguz rebuild as CI-only mmi-runner2 with full erasure and no backups (#7859); zuber (46.225.58.31) is gone: deleted 2026-09-18 by the owner-approved ZuberShade decommission (MMC-ZuberShade#1838, snapshot kept); Hetzner has since reassigned that IP to another machine (#7351)" },
7464
7512
  // #7148: a backup nobody restored is a hope. Monthly, restore the latest artifact of every
7465
7513
  // archived stage (including a stage whose containers are down) into scratch Postgres and compare
7466
7514
  // row counts, and read-verify every storage archive against its listing; per-stage PASS/FAIL.
7467
- { name: "mmi-backup-restore-check (mm-central, mm-work, mmi-oguz)", cadence: "0 3 1 * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-backup-restore-check \u2192 /opt/mmi-control/nightly-backup-restore-check.sh; monthly restore proof for every archived stage (#7148; source scripts/nightly-backup-restore-check.sh); zuber (46.225.58.31) is gone: deleted 2026-09-18 by the owner-approved ZuberShade decommission (MMC-ZuberShade#1838, snapshot kept); Hetzner has since reassigned that IP to another machine (#7351)" },
7515
+ { name: "mmi-backup-restore-check (mm-central, mm-work)", cadence: "0 3 1 * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-backup-restore-check \u2192 /opt/mmi-control/nightly-backup-restore-check.sh; monthly restore proof for every archived stage (#7148; source scripts/nightly-backup-restore-check.sh); owner-approved mmi-oguz rebuild as CI-only mmi-runner2 with full erasure and no backups (#7859); zuber (46.225.58.31) is gone: deleted 2026-09-18 by the owner-approved ZuberShade decommission (MMC-ZuberShade#1838, snapshot kept); Hetzner has since reassigned that IP to another machine (#7351)" },
7468
7516
  // #7662 (MM-Work#1520): Vercel Cron's 15-minute central mail call, moved to the box with MM-Work. The app
7469
7517
  // calls its own endpoint with its own CRON_SECRET via docker exec, so the secret stays in the container.
7470
7518
  { name: "mm-work-mail-tick (mm-work)", cadence: "*/15 * * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mm-work-mail-tick \u2192 /opt/mmi-control/mm-work-mail-tick.sh; GET /api/internal/mail per stage listed in MM_WORK_MAIL_STAGES (source scripts/mm-work-mail-tick.sh; log /var/log/mm-work-mail-tick.log)" },
@@ -7472,10 +7520,11 @@ var DECLARED_ENTRIES = [
7472
7520
  // setup-node before it can prune, so at 100% it dies at checkout and never prunes — the cleanup is
7473
7521
  // unavailable exactly when it is needed. This one is root cron on the box: no checkout, no node, and
7474
7522
  // a no-op below 90% disk, which is why 15 minutes is affordable.
7475
- { name: "mmi-runner-hygiene-oob (mmi-runner)", cadence: "*/15 * * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-runner-hygiene \u2192 /opt/mmi-control/runner-hygiene-oob.sh; routine docker/cache prunes every tick, _work relief only at >=90% disk (verify: mmi-cli devops runtime box get mmi-runner --ssh)" },
7523
+ { name: "mmi-runner-hygiene-oob (mmi-runner, mmi-runner2)", cadence: "*/15 * * * * + 5-59/15,10-59/15 * * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-runner-hygiene \u2192 /opt/mmi-control/runner-hygiene-oob.sh; routine docker/cache prunes every 15 min, relief-only pressure checks between, _work relief at >=90% disk or <8 GiB free; second-host rollout #7859 (verify each box via mmi-cli devops runtime box get <box> --ssh)" },
7476
7524
  // #4118: Listening + systemd-active ≠ GitHub online after acquirejob 503. restart.conf cannot see
7477
7525
  // it; an Actions workflow on mmi-live cannot heal it when half the fleet is offline. Root cron.
7478
7526
  { name: "mmi-runner-online-reconcile (mmi-runner)", cadence: "*/5 * * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-runner-online \u2192 /opt/mmi-control/runner-online-reconcile.sh; API-offline + _diag acquirejob, max 2 restarts/tick (verify: mmi-cli devops runtime box get mmi-runner --ssh)" },
7527
+ { name: "mmi-runner-online-reconcile (mmi-runner2)", cadence: "*/5 * * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-runner-online \u2192 /opt/mmi-control/runner-online-reconcile.sh; scripts/runner2-online-reconcile.cron selects lanes 13-16, disables credential files, _diag acquirejob only, max 2 restarts/tick; rollout #7859 (verify: mmi-cli devops runtime box get mmi-runner2 --ssh)" },
7479
7528
  // #5292: a lane that holds a dispatched JOB but produces zero output past the budget honestly
7480
7529
  // reports "busy", so the online reconcile never fires and GitHub will not re-dispatch while the
7481
7530
  // run holds the assignment. This tick cancels + re-triggers those runs (max 1/tick, debounced).
@@ -7483,7 +7532,7 @@ var DECLARED_ENTRIES = [
7483
7532
  // #3852: the runner /tmp sweep — daily inode hygiene (03:41 UTC), deliberately the one clutter path the
7484
7533
  // out-of-band disk-relief script must never touch. Verified installed 2026-08-18: the cron.d file and
7485
7534
  // flock line match scripts/runner-tmp-sweep.cron exactly.
7486
- { name: "mmi-runner-tmp-sweep (mmi-runner)", cadence: "41 * * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-runner-tmp-sweep \u2192 /opt/mmi-control/runner-tmp-sweep.sh; hourly inode hygiene, 6h age (#6274), flock -n (verify: mmi-cli devops runtime box get mmi-runner --ssh)" }
7535
+ { name: "mmi-runner-tmp-sweep (mmi-runner, mmi-runner2)", cadence: "41 * * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-runner-tmp-sweep \u2192 /opt/mmi-control/runner-tmp-sweep.sh; hourly inode hygiene, 6h age (#6274), flock -n; second-host rollout #7859 (verify each box via mmi-cli devops runtime box get <box> --ssh)" }
7487
7536
  // RETIRED 2026-08-18 (MMI-Hub#5234): `zuber-bake` (every 30 min) and `zuber-bake-full` (daily 00:30 UTC)
7488
7537
  // were declared as zuber systemd timers. The ZuberShade full park (#1774, 2026-08-06) stopped them: on
7489
7538
  // the box (46.225.58.31) zuber-bake.timer / zuber-bake-full.timer answer not-found and
@@ -13142,6 +13191,7 @@ function deriveLoopStaleness(item, loopState, now = Date.now()) {
13142
13191
  };
13143
13192
  }
13144
13193
  var CLAIM_MARKER_PATTERN = /<!--\s*mmi-claim:\s*(\{[\s\S]*?\})\s*-->/g;
13194
+ var CLAIM_RELEASE_PATTERN = /<!--\s*mmi-claim-release:\s*(\{[\s\S]*?\})\s*-->/g;
13145
13195
  var CLAIM_MARKER_LIVE_MS = 24 * 60 * 6e4;
13146
13196
  var CLAIM_STAMP_IDLE_MS = 4 * 60 * 6e4;
13147
13197
  function isValidClaimMarker(parsed) {
@@ -13160,10 +13210,36 @@ function parseClaimMarkers(body) {
13160
13210
  }
13161
13211
  return out;
13162
13212
  }
13213
+ function isValidClaimRelease(parsed) {
13214
+ if (!parsed || typeof parsed !== "object") return false;
13215
+ const release = parsed;
13216
+ return typeof release.v === "number" && typeof release.ts === "string" && Number.isFinite(Date.parse(release.ts)) && (release.session === void 0 || typeof release.session === "string") && (release.surface === void 0 || typeof release.surface === "string") && (release.host === void 0 || typeof release.host === "string");
13217
+ }
13218
+ function parseClaimReleases(body) {
13219
+ const out = [];
13220
+ for (const match of body.matchAll(CLAIM_RELEASE_PATTERN)) {
13221
+ try {
13222
+ const parsed = JSON.parse(match[1]);
13223
+ if (isValidClaimRelease(parsed)) out.push(parsed);
13224
+ } catch {
13225
+ }
13226
+ }
13227
+ return out;
13228
+ }
13229
+ function renderClaimReleaseBody(release) {
13230
+ return `Released by \`${formatSessionIdentity(release)}\` at ${release.ts}.
13231
+
13232
+ <!-- mmi-claim-release: ${JSON.stringify(release)} -->`;
13233
+ }
13163
13234
  function latestClaimMarker(comments) {
13164
13235
  for (let i = comments.length - 1; i >= 0; i -= 1) {
13165
- const markers = parseClaimMarkers(comments[i].body);
13166
- if (markers.length) return markers[markers.length - 1];
13236
+ const body = comments[i].body;
13237
+ const markers = parseClaimMarkers(body);
13238
+ const releases = parseClaimReleases(body);
13239
+ if (!markers.length && !releases.length) continue;
13240
+ if (!releases.length) return markers[markers.length - 1];
13241
+ if (!markers.length) return void 0;
13242
+ return body.lastIndexOf("mmi-claim-release:") > body.lastIndexOf("mmi-claim:") ? void 0 : markers[markers.length - 1];
13167
13243
  }
13168
13244
  return void 0;
13169
13245
  }
@@ -13197,7 +13273,7 @@ function claimFromMarker(item, marker, linked = {}, now = Date.now()) {
13197
13273
  holder: describeClaimMarker(marker),
13198
13274
  ageMs: now - Date.parse(marker.ts),
13199
13275
  currentlyClaimed: item.assignees.length > 0 && item.status === "In Progress",
13200
- ...claimStampStaleness(marker, { activityAt: [item.updatedAt, ...linked.activityAt ?? []], linkedWork: linked.linkedWork }, now)
13276
+ ...claimStampStaleness(marker, { activityAt: [...linked.activityAt ?? []], linkedWork: linked.linkedWork }, now)
13201
13277
  };
13202
13278
  }
13203
13279
  function classifyClaimableMarker(comments, actor, now = Date.now(), activityAt = []) {
@@ -13279,7 +13355,7 @@ async function demoteLiveClaimedClaimables(scope, client, actor = describeSessio
13279
13355
  warnings.push(`${item.ref} left claimable: claim-marker evidence could not be read`);
13280
13356
  continue;
13281
13357
  }
13282
- const verdict = classifyClaimableMarker(comments, actor, deps.now, [item.updatedAt]);
13358
+ const verdict = classifyClaimableMarker(comments, actor, deps.now);
13283
13359
  if (verdict.verdict === "claimable") {
13284
13360
  stillClaimable.push(item);
13285
13361
  } else if (verdict.verdict === "owned") {
@@ -14200,6 +14276,20 @@ async function unclaimBoardIssue(options, deps = {}) {
14200
14276
  if (!options.allowPartial) throw new Error(warning);
14201
14277
  return { item, viewer, repo: currentRepo, status: item.status, partial: true, warning };
14202
14278
  }
14279
+ try {
14280
+ const actor = deps.session ?? describeSessionIdentity();
14281
+ const release = {
14282
+ v: 1,
14283
+ session: actor.session,
14284
+ surface: actor.surface,
14285
+ host: actor.host,
14286
+ ts: (/* @__PURE__ */ new Date()).toISOString()
14287
+ };
14288
+ await postIssueComment(client, { ref: item.ref, body: renderClaimReleaseBody(release) });
14289
+ } catch (e) {
14290
+ process.stderr.write(`warning: ${item.ref} was unclaimed, but recording the release marker comment failed: ${ghError(e)}
14291
+ `);
14292
+ }
14203
14293
  const optionId = cfg.statusOptions[toStatus];
14204
14294
  let unclaimCredential = "user";
14205
14295
  try {
@@ -15223,7 +15313,7 @@ function createDocsIndexDeps(repoRoot2) {
15223
15313
 
15224
15314
  // src/gate-budget.ts
15225
15315
  var BLESSED_RUN_WITH_BUDGET_SHA = "5ce541add569e54e7ca96de67a768577d3d604e4";
15226
- var BLESSED_RUNNER_GATE_SHA = "a6ee756d94fd95884451381e94408f0348eb11db";
15316
+ var BLESSED_RUNNER_GATE_SHA = "a19e9392e5ad9b0675f02e4304d68dd933fad60f";
15227
15317
  var REMOTE_USE = /^mutmutco\/MMI-Hub\/\.github\/actions\/run-with-budget@(\S+)$/;
15228
15318
  var LOCAL_USE = "./.github/actions/run-with-budget";
15229
15319
  var FULL_SHA = /^[0-9a-f]{40}$/;
@@ -17255,10 +17345,10 @@ var rollout_plan_default = {
17255
17345
  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)."
17256
17346
  },
17257
17347
  baseline: {
17258
- version: "4.5.95",
17259
- tag: "v4.5.95",
17260
- commit: "d6d4a135bce9",
17261
- npm: "@mutmutco/cli@4.5.95"
17348
+ version: "4.5.97",
17349
+ tag: "v4.5.97",
17350
+ commit: "72af9cdfc1b7",
17351
+ npm: "@mutmutco/cli@4.5.97"
17262
17352
  },
17263
17353
  exitCriterion: "fleet-n-of-n",
17264
17354
  hubOnlyShortcut: "forbidden",
@@ -17275,14 +17365,14 @@ var rollout_plan_default = {
17275
17365
  repo: "mutmutco/mmi-hub",
17276
17366
  role: "canary",
17277
17367
  schedule: "train",
17278
- v3Target: "v4.5.95"
17368
+ v3Target: "v4.5.97"
17279
17369
  }
17280
17370
  ],
17281
17371
  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.",
17282
17372
  rollback: {
17283
17373
  independent: true,
17284
- mechanism: "npm dist-tag latest -> 4.5.95 and redeploy the Hub Lambda from tag v4.5.95 (d6d4a135bce9); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17285
- v3Target: "v4.5.95 (@mutmutco/cli@4.5.95, tag commit d6d4a135bce9 \u2014 last known-good release carrying the repo-index v4-only contract)"
17374
+ mechanism: "npm dist-tag latest -> 4.5.97 and redeploy the Hub Lambda from tag v4.5.97 (72af9cdfc1b7); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17375
+ v3Target: "v4.5.97 (@mutmutco/cli@4.5.97, tag commit 72af9cdfc1b7 \u2014 last known-good release carrying the repo-index v4-only contract)"
17286
17376
  }
17287
17377
  },
17288
17378
  {
@@ -30464,8 +30554,10 @@ function trainPlanTargetsFromTags(tags, explicitRaw) {
30464
30554
  }
30465
30555
  function derivedStagePlan(derived, shell2, stops = true) {
30466
30556
  const { port } = derived;
30467
- const envOrder = ["MMI_STAGE", "MMI_PORT", "COMPOSE_PROFILES"];
30468
- const resolved = { MMI_STAGE: "development", MMI_PORT: String(port), COMPOSE_PROFILES: "local" };
30557
+ const baseOrder = ["MMI_STAGE", "MMI_PORT", "COMPOSE_PROFILES"];
30558
+ const projectName = derived.config.env?.COMPOSE_PROJECT_NAME?.trim();
30559
+ const envOrder = projectName ? [...baseOrder, "COMPOSE_PROJECT_NAME"] : baseOrder;
30560
+ const resolved = { MMI_STAGE: "dev", MMI_PORT: String(port), COMPOSE_PROFILES: "local", ...projectName ? { COMPOSE_PROJECT_NAME: projectName } : {} };
30469
30561
  const ensureEnv = shell2 === "powershell" ? `if (-not (Test-Path -LiteralPath '.env')) { Copy-Item -LiteralPath '.env.example' -Destination '.env' }` : `[ -f .env ] || cp .env.example .env`;
30470
30562
  const envPrefix = shell2 === "powershell" ? envOrder.map((k) => `$env:${k}='${resolved[k]}'`).join("; ") : envOrder.map((k) => `${k}=${resolved[k]}`).join(" ");
30471
30563
  const build = shell2 === "powershell" ? `${envPrefix}; ${derived.config.build ?? "docker compose build --no-cache"}` : `${envPrefix} ${derived.config.build ?? "docker compose build --no-cache"}`;
@@ -37015,8 +37107,8 @@ function bothSidesText(portFailures, portLaneShas) {
37015
37107
  if (rows.length === 0) return "";
37016
37108
  return `BEFORE that --ack: ${rows.map((r) => `${r.label} also conflicts on ${r.paths.join(", ")}`).join("; ")}, which exist on both branches \u2014 the port resolves them by hand for main, so land the SAME resolution on development first (the --ack covers the whole commit, and the next release would otherwise overwrite or clash with the main-only content). `;
37017
37109
  }
37018
- function formatHotfixBatchPreflightRefusal(failures, fromSpecs, portLaneShas = /* @__PURE__ */ new Map()) {
37019
- const lines2 = failures.map((f) => ` - ${f.label}: ${f.blocking.join(", ")}`);
37110
+ function formatHotfixBatchPreflightRefusal(failures, fromSpecs, portLaneShas = /* @__PURE__ */ new Map(), context = { sources: failures, ackedShas: [] }) {
37111
+ const lines2 = failures.map((f) => ` - ${f.label}: ${f.blocking.join(", ")}` + (f.afterAbortedPicks?.length ? ` (observed after aborted pick(s): ${f.afterAbortedPicks.join(", ")}; may depend on those missing changes)` : ""));
37020
37112
  const portFailures = failures.filter((f) => portLaneShas.has(f.sha));
37021
37113
  const synthFailures = failures.filter((f) => !portLaneShas.has(f.sha));
37022
37114
  const head = `hotfix start batch preflight refused: ${failures.length} pick(s) would hard-stop on origin/main:
@@ -37024,15 +37116,15 @@ ${lines2.join("\n")}
37024
37116
  `;
37025
37117
  const batchFrom = fromSpecs.join(",");
37026
37118
  const guide = "https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#hotfix-pick-refused";
37027
- if (portFailures.length > 0) {
37028
- const originals = portFailures.map((f) => f.sha.slice(0, 7)).join(",");
37029
- const portText = `${portFailures.length === failures.length ? "Every failing pick" : `Pick(s) ${portFailures.map((f) => f.label).join(", ")}`} fail on path(s) ABSENT on origin/main but present on origin/development, so a development-based cite-only synthesis PR cannot carry them (a new development commit cannot add files development already has \u2014 its cherry-pick onto main carries nothing, #7669). Use the main-based PORT-COMMIT lane: cut a branch FROM origin/main, add ONE port commit that brings the listed path(s) and the fix onto main-lineage (e.g. \`git checkout origin/development -- <paths>\` then the fix), push it and keep it pushed until the release \u2014 never merge it into development \u2014 then run \`mmi-cli devops hotfix start --from <portSHA> --ack ${originals}\` (a bare SHA is accepted; the originals are the acked development fixes the port replaces), and record the port SHA in the hotfix PR body. The port commit never reaches development and will not patch-id match, so the NEXT rc\u2192main release must pass \`--ack <the hotfix's main commit>\` (hotfix-coverage). ` + bothSidesText(portFailures, portLaneShas);
37030
- if (synthFailures.length === 0) return `${head}${portText}Never hand-resolve onto main; see ${guide}`;
37031
- return `${head}Mixed batch \u2014 two lanes. ${portText}Pick(s) ${synthFailures.map((f) => f.label).join(", ")} fit the development-based cite-only synthesis lane instead: cut the branch FROM origin/development, regenerate the conflict content so it cherry-picks cleanly onto current origin/main, and land that development-parented PR with the cite-only marker (the #5096 flow). Then rerun \`mmi-cli devops hotfix start --from <synthesis merge SHA(s)>,<portSHA> --ack ${originals}\` (the original --from was ${batchFrom}) \u2014 never hand-resolve onto main; never a main-parented branch into development; see ${guide}`;
37032
- }
37033
- return `hotfix start batch preflight refused: ${failures.length} pick(s) would hard-stop on origin/main:
37034
- ${lines2.join("\n")}
37035
- Land ONE development-based cite-only synthesis PR for the whole batch (#4643 supersedes the old cut-from-origin/main wording, which produced main-lineage PRs into development \u2014 the shape portFoldToDevelopment forbids, #4365/#4371): cut the branch FROM origin/development, regenerate the listed conflict content so it cherry-picks cleanly onto current origin/main, and land that development-parented PR with the cite-only marker (the proven #5096 flow), then rerun \`mmi-cli devops hotfix start --from ${batchFrom}\` with the port merge SHA(s) \u2014 never hand-resolve onto main; never a main-parented branch into development; never N serial ports (#3167) \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#hotfix-pick-refused`;
37119
+ const replaceNow = failures.filter((f) => !f.afterAbortedPicks?.length);
37120
+ const replacements = new Map(replaceNow.map((f, i) => [f.sha, `<replacementSHA${i + 1}>`]));
37121
+ const picks = context.sources.map((s) => replacements.get(s.sha) ?? s.sha).join(",");
37122
+ const exclusions = [.../* @__PURE__ */ new Set([...context.ackedShas, ...replaceNow.map((f) => f.sha)])].join(",");
37123
+ const mapping = replaceNow.map((f) => `${replacements.get(f.sha)} replaces ${f.label} (${f.sha})`).join("; ");
37124
+ const dependencyNote = failures.some((f) => f.afterAbortedPicks?.length) ? "Later failures were observed after earlier picks were aborted. Keep those original SHAs in the retry. Re-preflight the complete repaired batch before treating a later path absent on static origin/main as an independent port requirement. If a later pick still fails, replacing it requires separate approval for its exact original SHA. " : "";
37125
+ const portText = portFailures.length > 0 ? `Pick(s) ${portFailures.map((f) => f.label).join(", ")} have path(s) ABSENT on origin/main but present on origin/development. If they still fail after prerequisite repairs, use the main-based PORT-COMMIT lane: cut a branch FROM origin/main, add the missing paths and fix, push it and keep it pushed until the release \u2014 never merge it into development. Record the port SHA in the hotfix PR body. The NEXT rc\u2192main release must pass \`--ack <the hotfix's main commit>\` (hotfix-coverage). ` + bothSidesText(portFailures, portLaneShas) : "";
37126
+ const repairText = synthFailures.length > 0 ? `Pick(s) ${synthFailures.map((f) => f.label).join(", ")} need a development repair PR: cut FROM origin/development and land the conflict correction through normal CI. Its picked commit must carry the required main-clean delta; merely citing the original fixes carries no content and grants no coverage exemption. If that delta cannot be produced on development, stop and ask the owner for a scoped port plan. ` : "";
37127
+ return `${head}${portFailures.length > 0 && synthFailures.length > 0 ? "Mixed batch. " : ""}${dependencyNote}${portText}${repairText}Original ordered --from: ${batchFrom}. Replacement mapping: ${mapping}. Keep every passing pick in its original position and retain existing approved exclusions. Additional owner approval required for replacing/excluding original development commit(s): ${replaceNow.map((f) => f.sha).join(",")}. No cite-only marker is implemented; mmi-hotfix-carries is release verification only. Only after that explicit approval, replace the placeholders and rerun \`mmi-cli devops hotfix start --from ${picks} --ack ${exclusions}\`. Re-preflight the whole batch. Never hand-resolve onto main or merge a main-parented branch into development; see ${guide}`;
37036
37128
  }
37037
37129
  var CHERRY_PICKED_FROM = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
37038
37130
  async function scanHotfixPickGaps(deps, sources, tolerated, rcRef) {
@@ -37122,7 +37214,8 @@ async function preflightHotfixBatchPicks(deps, sources, pickTolerated) {
37122
37214
  failures.push({
37123
37215
  label: source.label,
37124
37216
  sha: source.sha,
37125
- blocking: blocking.length > 0 ? blocking : [message2.split("\n")[0] ?? message2]
37217
+ blocking: blocking.length > 0 ? blocking : [message2.split("\n")[0] ?? message2],
37218
+ ...failures.length > 0 ? { afterAbortedPicks: failures.map((f) => f.label) } : {}
37126
37219
  });
37127
37220
  }
37128
37221
  }
@@ -37337,7 +37430,7 @@ async function runHotfixStartLocked(deps, options, doctor) {
37337
37430
  const preflight2 = await preflightHotfixBatchPicks(deps, sources, pickTolerated);
37338
37431
  if (preflight2.failures.length > 0) {
37339
37432
  const portLaneShas = await classifyHotfixPortLanePicks(deps, preflight2.failures);
37340
- throw new Error(formatHotfixBatchPreflightRefusal(preflight2.failures, specs, portLaneShas));
37433
+ throw new Error(formatHotfixBatchPreflightRefusal(preflight2.failures, specs, portLaneShas, { sources, ackedShas }));
37341
37434
  }
37342
37435
  if (preflight2.skipped.length > 0) {
37343
37436
  const skippedShas = new Set(preflight2.skipped.map((s) => s.sha));
@@ -39570,11 +39663,6 @@ var PROTECTED_BRANCHES = /* @__PURE__ */ new Set(["development", "main", "master
39570
39663
  function isProtectedBranch(branch) {
39571
39664
  return PROTECTED_BRANCHES.has(branch.trim());
39572
39665
  }
39573
- function deriveComposeProjectName(worktreePath) {
39574
- const base = worktreePath.replace(/\\/g, "/").replace(/\/+$/, "").split("/").pop()?.toLowerCase() ?? "";
39575
- const name = base.replace(/[^a-z0-9_-]+/g, "_").replace(/^[^a-z0-9]+/, "");
39576
- return name || void 0;
39577
- }
39578
39666
 
39579
39667
  // src/pr-merge-helpers.ts
39580
39668
  function buildPrMergeArgs(input) {
@@ -41787,6 +41875,53 @@ function parsePrChecksTable(stdout) {
41787
41875
  }
41788
41876
  return rows;
41789
41877
  }
41878
+ function includeMissingRequiredChecks(rows, required) {
41879
+ const reported = new Set(rows.map((row) => row.name));
41880
+ const missing = [...new Set(required)].filter((name) => !reported.has(name)).map((name) => ({
41881
+ name,
41882
+ status: "missing",
41883
+ details: "expected required check has not reported"
41884
+ }));
41885
+ return [...rows, ...missing];
41886
+ }
41887
+ async function requiredCheckContexts(repo, prNumber) {
41888
+ const { stdout: prOutput } = await execFileP("gh", ["pr", "view", String(prNumber), "--repo", repo, "--json", "baseRefName"], { timeout: GC_GH_TIMEOUT_MS2 });
41889
+ const baseRef = JSON.parse(prOutput).baseRefName;
41890
+ if (typeof baseRef !== "string" || !baseRef) throw new Error("pull request response had no base branch");
41891
+ const read = async (path2) => {
41892
+ try {
41893
+ const { stdout } = await execFileP("gh", ["api", path2], { timeout: GC_GH_TIMEOUT_MS2 });
41894
+ return JSON.parse(stdout);
41895
+ } catch (e) {
41896
+ const err = e;
41897
+ if (/\b404\b|Not Found/i.test(`${err.stderr ?? ""} ${err.message ?? ""}`)) return null;
41898
+ throw e;
41899
+ }
41900
+ };
41901
+ const [classic, rules] = await Promise.all([
41902
+ read(`repos/${repo}/branches/${encodeURIComponent(baseRef)}/protection/required_status_checks`),
41903
+ read(`repos/${repo}/rules/branches/${encodeURIComponent(baseRef)}`)
41904
+ ]);
41905
+ const contexts = /* @__PURE__ */ new Set();
41906
+ const oldContexts = classic?.contexts;
41907
+ if (Array.isArray(oldContexts)) {
41908
+ for (const name of oldContexts) if (typeof name === "string") contexts.add(name);
41909
+ }
41910
+ if (Array.isArray(rules)) {
41911
+ for (const rule of rules) {
41912
+ const required = rule.type === "required_status_checks" ? rule.parameters?.required_status_checks : void 0;
41913
+ if (Array.isArray(required)) for (const check of required) {
41914
+ const name = check?.context;
41915
+ if (typeof name === "string") contexts.add(name);
41916
+ }
41917
+ }
41918
+ }
41919
+ return [...contexts];
41920
+ }
41921
+ function checksStateWithExpected(state, rows, required) {
41922
+ const checks = includeMissingRequiredChecks(rows, required);
41923
+ return checks.some((row) => row.status === "missing") && (state === "success" || state === "no-checks-reported") ? { state: "incomplete", rows: checks } : { state, rows: checks };
41924
+ }
41790
41925
  function parseLinkedIssues(body) {
41791
41926
  const refs = /* @__PURE__ */ new Set();
41792
41927
  const re = /\b(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved|ref|refs|re|references)\s+#(\d+)\b/gi;
@@ -42047,6 +42182,7 @@ function registerPrLifecycleCommands(program3) {
42047
42182
  return stdout;
42048
42183
  };
42049
42184
  const withLogs = async (rows) => o.logs ? attachFailureLogs(rows, fetchLog) : rows;
42185
+ let expected = [];
42050
42186
  const snap = async () => {
42051
42187
  let stdout = "";
42052
42188
  let stderr = "";
@@ -42061,14 +42197,15 @@ function registerPrLifecycleCommands(program3) {
42061
42197
  const parsed = parseGhPrChecksResult(stdout, stderr);
42062
42198
  const state = pollStateFromParsed(parsed);
42063
42199
  const rows = parsePrChecksTable(stdout);
42064
- return { state, rows };
42200
+ return checksStateWithExpected(state, rows, expected);
42065
42201
  };
42066
42202
  try {
42203
+ expected = await requiredCheckContexts(repo, n);
42067
42204
  if (!o.watch) {
42068
42205
  const { state, rows } = await snap();
42069
42206
  const output = { state, checks: await withLogs(rows) };
42070
42207
  console.log(JSON.stringify(output));
42071
- if (state === "failure" || state === "failing") process.exitCode = 1;
42208
+ if (state === "failure" || state === "failing" || state === "incomplete") process.exitCode = 1;
42072
42209
  return;
42073
42210
  }
42074
42211
  const deadline = Date.now() + CHECKS_WATCH_TIMEOUT_MS;
@@ -42799,11 +42936,18 @@ function parseDockerContainerLs(stdout) {
42799
42936
  function containersPublishingPort(containers, port) {
42800
42937
  return containers.filter((c) => c.publishedHostPorts.includes(port));
42801
42938
  }
42939
+ function stageComposeProjectName(cwd) {
42940
+ const parts = cwd.replace(/\\/g, "/").replace(/\/+$/, "").split("/").filter(Boolean);
42941
+ const base = parts.pop() ?? "";
42942
+ const parent = parts.pop();
42943
+ const raw = parent ? `${parent}-${base}` : base;
42944
+ const name = raw.toLowerCase().replace(/[^a-z0-9_-]+/g, "_").replace(/^[^a-z0-9]+/, "");
42945
+ return name || "mmi-stage";
42946
+ }
42802
42947
  function isOwnComposeContainer(container, cwd) {
42803
42948
  if (container.composeWorkingDir && normPath(container.composeWorkingDir) === normPath(cwd)) return true;
42804
42949
  if (container.composeWorkingDir && pathUnder(container.composeWorkingDir, cwd)) return true;
42805
- const derived = deriveComposeProjectName(cwd);
42806
- return Boolean(derived && container.composeProject === derived);
42950
+ return container.composeProject === stageComposeProjectName(cwd);
42807
42951
  }
42808
42952
  function planStagePortTeardown(containers, port, cwd) {
42809
42953
  const ownProjects = [];
@@ -43045,11 +43189,13 @@ function composeResolvesPort(cwd) {
43045
43189
  return (0, import_node_fs38.existsSync)(envFile) && envFileKeys((0, import_node_fs38.readFileSync)(envFile, "utf8")).has("PORT");
43046
43190
  }
43047
43191
  function stageComposeEnv(config, stagePort, vaultEnvMerge, cwd) {
43192
+ const composeProject = config.env?.COMPOSE_PROJECT_NAME ?? vaultEnvMerge?.COMPOSE_PROJECT_NAME ?? stageComposeProjectName(cwd);
43048
43193
  return {
43049
43194
  // Vault secrets first so the stage-selection contract (MMI_STAGE/MMI_PORT/…) always wins on any collision.
43050
43195
  ...vaultEnvMerge ?? {},
43051
43196
  ...stageProcessEnv(stagePort, stageExtraEnv(config, stagePort)),
43052
- ...stagePort != null && !composeResolvesPort(cwd) ? { PORT: String(stagePort) } : {}
43197
+ ...stagePort != null && !composeResolvesPort(cwd) ? { PORT: String(stagePort) } : {},
43198
+ COMPOSE_PROJECT_NAME: composeProject
43053
43199
  };
43054
43200
  }
43055
43201
  function stageComposeFiles(cwd) {
@@ -43199,6 +43345,58 @@ async function killTree(pid) {
43199
43345
  }
43200
43346
  }
43201
43347
  }
43348
+ async function describeExitedComposeServices(cwd, env) {
43349
+ const dockerEnv = { ...process.env, ...env };
43350
+ const run = (args) => execFileP3("docker", args, {
43351
+ cwd,
43352
+ windowsHide: true,
43353
+ timeout: DOCKER_TIMEOUT_MS,
43354
+ maxBuffer: 1024 * 1024 * 4,
43355
+ env: dockerEnv
43356
+ }).then(({ stdout }) => stdout);
43357
+ let rows;
43358
+ try {
43359
+ const text = (await run(["compose", "ps", "--all", "--format", "json"])).trim();
43360
+ if (!text) return void 0;
43361
+ try {
43362
+ const parsed = JSON.parse(text);
43363
+ rows = Array.isArray(parsed) ? parsed : [parsed];
43364
+ } catch {
43365
+ rows = text.split(/\r?\n/).flatMap((line) => {
43366
+ try {
43367
+ return [JSON.parse(line)];
43368
+ } catch {
43369
+ return [];
43370
+ }
43371
+ });
43372
+ }
43373
+ } catch {
43374
+ return void 0;
43375
+ }
43376
+ const exited = rows.filter((row) => typeof row.State === "string" && /^(exited|dead)$/i.test(row.State));
43377
+ if (!exited.length) return void 0;
43378
+ const parts = [];
43379
+ for (const row of exited.slice(0, 3)) {
43380
+ const service = String(row.Service ?? row.Name ?? "unknown");
43381
+ let exit = row.ExitCode ?? row.exitCode;
43382
+ if (exit == null && row.ID) {
43383
+ try {
43384
+ exit = (await run(["inspect", "--format", "{{.State.ExitCode}}", String(row.ID)])).trim() || void 0;
43385
+ } catch {
43386
+ }
43387
+ }
43388
+ let tail = "";
43389
+ try {
43390
+ tail = (await run(["compose", "logs", "--no-color", "--tail", "50", service])).trim();
43391
+ } catch {
43392
+ }
43393
+ parts.push(`service '${service}' exited before health (exit ${exit ?? "unknown"})${tail ? ` \u2014 log tail:
43394
+ ${tail}` : ""}`);
43395
+ }
43396
+ return `
43397
+ compose services exited before health:
43398
+ ${parts.join("\n")}`;
43399
+ }
43202
43400
  async function waitForHealth(url, timeoutMs, anyStatus = false) {
43203
43401
  const deadline = Date.now() + timeoutMs;
43204
43402
  let last = "";
@@ -43299,7 +43497,15 @@ async function startStage(config = {}, opts = {}) {
43299
43497
  if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4, config.healthAnyStatus);
43300
43498
  else await waitForProcessStability(child2);
43301
43499
  } catch (e) {
43500
+ let detail;
43501
+ if (state.healthUrl) {
43502
+ try {
43503
+ detail = await describeExitedComposeServices(cwd, upEnv);
43504
+ } catch {
43505
+ }
43506
+ }
43302
43507
  await cleanupStageState(state, [statePath, globalStatePath], opts.timeoutMs ?? 6e4, cwd, upEnv);
43508
+ if (detail) throw new Error(`${e.message}${detail}`, { cause: e });
43303
43509
  throw e;
43304
43510
  }
43305
43511
  const result = {
@@ -45240,6 +45446,11 @@ ${list}`);
45240
45446
  } else {
45241
45447
  printLine(`${label}: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
45242
45448
  for (const line of failedChecksReceiptLines(label, result)) printLine(line);
45449
+ if (result.status === "success" && result.headSha) {
45450
+ const checks = result.checks?.map((check) => `${check.name}=${check.state}`).join(", ") || "none";
45451
+ const workflows = result.workflowRuns?.map((run) => `${run.name}#${run.id}=${run.status}/${run.conclusion ?? "pending"}`).join(", ") || "none";
45452
+ printLine(`${label}: verified head ${result.headSha}; checks: ${checks}; workflows: ${workflows}`);
45453
+ }
45243
45454
  }
45244
45455
  if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
45245
45456
  if (result.status === "timeout" || result.status === "rate-limited") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
@@ -45292,6 +45503,7 @@ ${list}`);
45292
45503
  // pending must hold the wait open, and a non-required red must fail it; #4396's scoping is the
45293
45504
  // merge paths' contract (`pr land`, `pr merge --wait`), which mirror the GitHub merge button.
45294
45505
  pollChecks: () => pollRestPrChecks(number, repo),
45506
+ pollChecksWithEvidence: () => pollRestPrChecksWithWorkflowEvidence(number, repo),
45295
45507
  pollMergeable: () => pollRestPrMergeable(number, repo),
45296
45508
  pollRateLimit: () => waitLoopCorePool("pr checks-wait"),
45297
45509
  // #3388: on a confirmed red, read the failing runs' annotations so a wall-clock budget kill stops
@@ -51810,10 +52022,13 @@ function deriveStage(inputs) {
51810
52022
  // #2655: only bootstrap a `.env` when the repo ships `.env.example` (legacy). Without it, the stage is
51811
52023
  // vault-native — the runner injects the declared dev secrets into the compose process env (no disk file).
51812
52024
  ...inputs.hasEnvExample ? { ensureEnv: { example: ".env.example", target: ".env" } } : {},
51813
- // MMI_PORT is the host-published port (the free STAGE_PORT); PORT is intentionally absent so compose
51814
- // reads the container's bind port from .env rather than this overriding it to the publish port (#1505).
52025
+ // MMI_STAGE is the local dev stage name the estate compose contract selects on (`${MMI_STAGE:-dev}`
52026
+ // in the seeded compose template): `development` never matched it, so a derived stage booted with
52027
+ // the wrong stage identity (#7870). MMI_PORT is the host-published port (the free STAGE_PORT); PORT
52028
+ // is intentionally absent so compose reads the container's bind port from .env rather than this
52029
+ // overriding it to the publish port (#1505).
51815
52030
  env: {
51816
- MMI_STAGE: "development",
52031
+ MMI_STAGE: "dev",
51817
52032
  MMI_PORT: "$STAGE_PORT",
51818
52033
  COMPOSE_PROFILES: "local"
51819
52034
  }
@@ -52012,13 +52227,20 @@ function registerStageCommands(program3) {
52012
52227
  const portRangeMeta = project2?.portRange ?? void 0;
52013
52228
  const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
52014
52229
  const repo = Array.isArray(project2?.repos) && typeof project2.repos[0] === "string" ? project2.repos[0] : slug;
52230
+ const resolution = decideStage({
52231
+ registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
52232
+ hasCompose: (0, import_node_fs48.existsSync)((0, import_node_path46.join)(process.cwd(), "docker-compose.yml")),
52233
+ hasEnvExample: (0, import_node_fs48.existsSync)((0, import_node_path46.join)(process.cwd(), ".env.example")),
52234
+ repo
52235
+ });
52236
+ if (resolution.source === "derived" && resolution.derived) {
52237
+ resolution.derived.config.env = {
52238
+ ...resolution.derived.config.env,
52239
+ COMPOSE_PROJECT_NAME: stageComposeProjectName(process.cwd())
52240
+ };
52241
+ }
52015
52242
  return {
52016
- resolution: decideStage({
52017
- registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
52018
- hasCompose: (0, import_node_fs48.existsSync)((0, import_node_path46.join)(process.cwd(), "docker-compose.yml")),
52019
- hasEnvExample: (0, import_node_fs48.existsSync)((0, import_node_path46.join)(process.cwd(), ".env.example")),
52020
- repo
52021
- }),
52243
+ resolution,
52022
52244
  project: project2,
52023
52245
  cfg
52024
52246
  };
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.95",
3
+ "version": "4.5.97",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/codex-plugin",
3
- "version": "4.5.95",
3
+ "version": "4.5.97",
4
4
  "description": "MMI workflow skills and org gates delivery.",
5
5
  "author": {
6
6
  "name": "MMI Future",