@mutmutco/codex-plugin 4.5.96 → 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.96",
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
  }
@@ -13143,6 +13191,7 @@ function deriveLoopStaleness(item, loopState, now = Date.now()) {
13143
13191
  };
13144
13192
  }
13145
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;
13146
13195
  var CLAIM_MARKER_LIVE_MS = 24 * 60 * 6e4;
13147
13196
  var CLAIM_STAMP_IDLE_MS = 4 * 60 * 6e4;
13148
13197
  function isValidClaimMarker(parsed) {
@@ -13161,10 +13210,36 @@ function parseClaimMarkers(body) {
13161
13210
  }
13162
13211
  return out;
13163
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
+ }
13164
13234
  function latestClaimMarker(comments) {
13165
13235
  for (let i = comments.length - 1; i >= 0; i -= 1) {
13166
- const markers = parseClaimMarkers(comments[i].body);
13167
- 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];
13168
13243
  }
13169
13244
  return void 0;
13170
13245
  }
@@ -13198,7 +13273,7 @@ function claimFromMarker(item, marker, linked = {}, now = Date.now()) {
13198
13273
  holder: describeClaimMarker(marker),
13199
13274
  ageMs: now - Date.parse(marker.ts),
13200
13275
  currentlyClaimed: item.assignees.length > 0 && item.status === "In Progress",
13201
- ...claimStampStaleness(marker, { activityAt: [item.updatedAt, ...linked.activityAt ?? []], linkedWork: linked.linkedWork }, now)
13276
+ ...claimStampStaleness(marker, { activityAt: [...linked.activityAt ?? []], linkedWork: linked.linkedWork }, now)
13202
13277
  };
13203
13278
  }
13204
13279
  function classifyClaimableMarker(comments, actor, now = Date.now(), activityAt = []) {
@@ -13280,7 +13355,7 @@ async function demoteLiveClaimedClaimables(scope, client, actor = describeSessio
13280
13355
  warnings.push(`${item.ref} left claimable: claim-marker evidence could not be read`);
13281
13356
  continue;
13282
13357
  }
13283
- const verdict = classifyClaimableMarker(comments, actor, deps.now, [item.updatedAt]);
13358
+ const verdict = classifyClaimableMarker(comments, actor, deps.now);
13284
13359
  if (verdict.verdict === "claimable") {
13285
13360
  stillClaimable.push(item);
13286
13361
  } else if (verdict.verdict === "owned") {
@@ -14201,6 +14276,20 @@ async function unclaimBoardIssue(options, deps = {}) {
14201
14276
  if (!options.allowPartial) throw new Error(warning);
14202
14277
  return { item, viewer, repo: currentRepo, status: item.status, partial: true, warning };
14203
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
+ }
14204
14293
  const optionId = cfg.statusOptions[toStatus];
14205
14294
  let unclaimCredential = "user";
14206
14295
  try {
@@ -15224,7 +15313,7 @@ function createDocsIndexDeps(repoRoot2) {
15224
15313
 
15225
15314
  // src/gate-budget.ts
15226
15315
  var BLESSED_RUN_WITH_BUDGET_SHA = "5ce541add569e54e7ca96de67a768577d3d604e4";
15227
- var BLESSED_RUNNER_GATE_SHA = "a6ee756d94fd95884451381e94408f0348eb11db";
15316
+ var BLESSED_RUNNER_GATE_SHA = "a19e9392e5ad9b0675f02e4304d68dd933fad60f";
15228
15317
  var REMOTE_USE = /^mutmutco\/MMI-Hub\/\.github\/actions\/run-with-budget@(\S+)$/;
15229
15318
  var LOCAL_USE = "./.github/actions/run-with-budget";
15230
15319
  var FULL_SHA = /^[0-9a-f]{40}$/;
@@ -17256,10 +17345,10 @@ var rollout_plan_default = {
17256
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)."
17257
17346
  },
17258
17347
  baseline: {
17259
- version: "4.5.96",
17260
- tag: "v4.5.96",
17261
- commit: "191fc3cf3396",
17262
- npm: "@mutmutco/cli@4.5.96"
17348
+ version: "4.5.97",
17349
+ tag: "v4.5.97",
17350
+ commit: "72af9cdfc1b7",
17351
+ npm: "@mutmutco/cli@4.5.97"
17263
17352
  },
17264
17353
  exitCriterion: "fleet-n-of-n",
17265
17354
  hubOnlyShortcut: "forbidden",
@@ -17276,14 +17365,14 @@ var rollout_plan_default = {
17276
17365
  repo: "mutmutco/mmi-hub",
17277
17366
  role: "canary",
17278
17367
  schedule: "train",
17279
- v3Target: "v4.5.96"
17368
+ v3Target: "v4.5.97"
17280
17369
  }
17281
17370
  ],
17282
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.",
17283
17372
  rollback: {
17284
17373
  independent: true,
17285
- mechanism: "npm dist-tag latest -> 4.5.96 and redeploy the Hub Lambda from tag v4.5.96 (191fc3cf3396); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17286
- v3Target: "v4.5.96 (@mutmutco/cli@4.5.96, tag commit 191fc3cf3396 \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)"
17287
17376
  }
17288
17377
  },
17289
17378
  {
@@ -30465,8 +30554,10 @@ function trainPlanTargetsFromTags(tags, explicitRaw) {
30465
30554
  }
30466
30555
  function derivedStagePlan(derived, shell2, stops = true) {
30467
30556
  const { port } = derived;
30468
- const envOrder = ["MMI_STAGE", "MMI_PORT", "COMPOSE_PROFILES"];
30469
- 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 } : {} };
30470
30561
  const ensureEnv = shell2 === "powershell" ? `if (-not (Test-Path -LiteralPath '.env')) { Copy-Item -LiteralPath '.env.example' -Destination '.env' }` : `[ -f .env ] || cp .env.example .env`;
30471
30562
  const envPrefix = shell2 === "powershell" ? envOrder.map((k) => `$env:${k}='${resolved[k]}'`).join("; ") : envOrder.map((k) => `${k}=${resolved[k]}`).join(" ");
30472
30563
  const build = shell2 === "powershell" ? `${envPrefix}; ${derived.config.build ?? "docker compose build --no-cache"}` : `${envPrefix} ${derived.config.build ?? "docker compose build --no-cache"}`;
@@ -39572,11 +39663,6 @@ var PROTECTED_BRANCHES = /* @__PURE__ */ new Set(["development", "main", "master
39572
39663
  function isProtectedBranch(branch) {
39573
39664
  return PROTECTED_BRANCHES.has(branch.trim());
39574
39665
  }
39575
- function deriveComposeProjectName(worktreePath) {
39576
- const base = worktreePath.replace(/\\/g, "/").replace(/\/+$/, "").split("/").pop()?.toLowerCase() ?? "";
39577
- const name = base.replace(/[^a-z0-9_-]+/g, "_").replace(/^[^a-z0-9]+/, "");
39578
- return name || void 0;
39579
- }
39580
39666
 
39581
39667
  // src/pr-merge-helpers.ts
39582
39668
  function buildPrMergeArgs(input) {
@@ -41789,6 +41875,53 @@ function parsePrChecksTable(stdout) {
41789
41875
  }
41790
41876
  return rows;
41791
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
+ }
41792
41925
  function parseLinkedIssues(body) {
41793
41926
  const refs = /* @__PURE__ */ new Set();
41794
41927
  const re = /\b(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved|ref|refs|re|references)\s+#(\d+)\b/gi;
@@ -42049,6 +42182,7 @@ function registerPrLifecycleCommands(program3) {
42049
42182
  return stdout;
42050
42183
  };
42051
42184
  const withLogs = async (rows) => o.logs ? attachFailureLogs(rows, fetchLog) : rows;
42185
+ let expected = [];
42052
42186
  const snap = async () => {
42053
42187
  let stdout = "";
42054
42188
  let stderr = "";
@@ -42063,14 +42197,15 @@ function registerPrLifecycleCommands(program3) {
42063
42197
  const parsed = parseGhPrChecksResult(stdout, stderr);
42064
42198
  const state = pollStateFromParsed(parsed);
42065
42199
  const rows = parsePrChecksTable(stdout);
42066
- return { state, rows };
42200
+ return checksStateWithExpected(state, rows, expected);
42067
42201
  };
42068
42202
  try {
42203
+ expected = await requiredCheckContexts(repo, n);
42069
42204
  if (!o.watch) {
42070
42205
  const { state, rows } = await snap();
42071
42206
  const output = { state, checks: await withLogs(rows) };
42072
42207
  console.log(JSON.stringify(output));
42073
- if (state === "failure" || state === "failing") process.exitCode = 1;
42208
+ if (state === "failure" || state === "failing" || state === "incomplete") process.exitCode = 1;
42074
42209
  return;
42075
42210
  }
42076
42211
  const deadline = Date.now() + CHECKS_WATCH_TIMEOUT_MS;
@@ -42801,11 +42936,18 @@ function parseDockerContainerLs(stdout) {
42801
42936
  function containersPublishingPort(containers, port) {
42802
42937
  return containers.filter((c) => c.publishedHostPorts.includes(port));
42803
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
+ }
42804
42947
  function isOwnComposeContainer(container, cwd) {
42805
42948
  if (container.composeWorkingDir && normPath(container.composeWorkingDir) === normPath(cwd)) return true;
42806
42949
  if (container.composeWorkingDir && pathUnder(container.composeWorkingDir, cwd)) return true;
42807
- const derived = deriveComposeProjectName(cwd);
42808
- return Boolean(derived && container.composeProject === derived);
42950
+ return container.composeProject === stageComposeProjectName(cwd);
42809
42951
  }
42810
42952
  function planStagePortTeardown(containers, port, cwd) {
42811
42953
  const ownProjects = [];
@@ -43047,11 +43189,13 @@ function composeResolvesPort(cwd) {
43047
43189
  return (0, import_node_fs38.existsSync)(envFile) && envFileKeys((0, import_node_fs38.readFileSync)(envFile, "utf8")).has("PORT");
43048
43190
  }
43049
43191
  function stageComposeEnv(config, stagePort, vaultEnvMerge, cwd) {
43192
+ const composeProject = config.env?.COMPOSE_PROJECT_NAME ?? vaultEnvMerge?.COMPOSE_PROJECT_NAME ?? stageComposeProjectName(cwd);
43050
43193
  return {
43051
43194
  // Vault secrets first so the stage-selection contract (MMI_STAGE/MMI_PORT/…) always wins on any collision.
43052
43195
  ...vaultEnvMerge ?? {},
43053
43196
  ...stageProcessEnv(stagePort, stageExtraEnv(config, stagePort)),
43054
- ...stagePort != null && !composeResolvesPort(cwd) ? { PORT: String(stagePort) } : {}
43197
+ ...stagePort != null && !composeResolvesPort(cwd) ? { PORT: String(stagePort) } : {},
43198
+ COMPOSE_PROJECT_NAME: composeProject
43055
43199
  };
43056
43200
  }
43057
43201
  function stageComposeFiles(cwd) {
@@ -43201,6 +43345,58 @@ async function killTree(pid) {
43201
43345
  }
43202
43346
  }
43203
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
+ }
43204
43400
  async function waitForHealth(url, timeoutMs, anyStatus = false) {
43205
43401
  const deadline = Date.now() + timeoutMs;
43206
43402
  let last = "";
@@ -43301,7 +43497,15 @@ async function startStage(config = {}, opts = {}) {
43301
43497
  if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4, config.healthAnyStatus);
43302
43498
  else await waitForProcessStability(child2);
43303
43499
  } catch (e) {
43500
+ let detail;
43501
+ if (state.healthUrl) {
43502
+ try {
43503
+ detail = await describeExitedComposeServices(cwd, upEnv);
43504
+ } catch {
43505
+ }
43506
+ }
43304
43507
  await cleanupStageState(state, [statePath, globalStatePath], opts.timeoutMs ?? 6e4, cwd, upEnv);
43508
+ if (detail) throw new Error(`${e.message}${detail}`, { cause: e });
43305
43509
  throw e;
43306
43510
  }
43307
43511
  const result = {
@@ -45242,6 +45446,11 @@ ${list}`);
45242
45446
  } else {
45243
45447
  printLine(`${label}: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
45244
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
+ }
45245
45454
  }
45246
45455
  if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
45247
45456
  if (result.status === "timeout" || result.status === "rate-limited") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
@@ -45294,6 +45503,7 @@ ${list}`);
45294
45503
  // pending must hold the wait open, and a non-required red must fail it; #4396's scoping is the
45295
45504
  // merge paths' contract (`pr land`, `pr merge --wait`), which mirror the GitHub merge button.
45296
45505
  pollChecks: () => pollRestPrChecks(number, repo),
45506
+ pollChecksWithEvidence: () => pollRestPrChecksWithWorkflowEvidence(number, repo),
45297
45507
  pollMergeable: () => pollRestPrMergeable(number, repo),
45298
45508
  pollRateLimit: () => waitLoopCorePool("pr checks-wait"),
45299
45509
  // #3388: on a confirmed red, read the failing runs' annotations so a wall-clock budget kill stops
@@ -51812,10 +52022,13 @@ function deriveStage(inputs) {
51812
52022
  // #2655: only bootstrap a `.env` when the repo ships `.env.example` (legacy). Without it, the stage is
51813
52023
  // vault-native — the runner injects the declared dev secrets into the compose process env (no disk file).
51814
52024
  ...inputs.hasEnvExample ? { ensureEnv: { example: ".env.example", target: ".env" } } : {},
51815
- // MMI_PORT is the host-published port (the free STAGE_PORT); PORT is intentionally absent so compose
51816
- // 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).
51817
52030
  env: {
51818
- MMI_STAGE: "development",
52031
+ MMI_STAGE: "dev",
51819
52032
  MMI_PORT: "$STAGE_PORT",
51820
52033
  COMPOSE_PROFILES: "local"
51821
52034
  }
@@ -52014,13 +52227,20 @@ function registerStageCommands(program3) {
52014
52227
  const portRangeMeta = project2?.portRange ?? void 0;
52015
52228
  const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
52016
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
+ }
52017
52242
  return {
52018
- resolution: decideStage({
52019
- registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
52020
- hasCompose: (0, import_node_fs48.existsSync)((0, import_node_path46.join)(process.cwd(), "docker-compose.yml")),
52021
- hasEnvExample: (0, import_node_fs48.existsSync)((0, import_node_path46.join)(process.cwd(), ".env.example")),
52022
- repo
52023
- }),
52243
+ resolution,
52024
52244
  project: project2,
52025
52245
  cfg
52026
52246
  };
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.96",
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.96",
3
+ "version": "4.5.97",
4
4
  "description": "MMI workflow skills and org gates delivery.",
5
5
  "author": {
6
6
  "name": "MMI Future",