@mutmutco/codex-plugin 4.5.103 → 4.5.104

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.103",
3
+ "version": "4.5.104",
4
4
  "mmiCompat": "4.x",
5
5
  "description": "MMI workflow skills and org gates delivery.",
6
6
  "author": {
package/cli/dist/main.cjs CHANGED
@@ -14908,7 +14908,7 @@ async function readDefaultBranchGate(repo, client, baseBranch, now = Date.now(),
14908
14908
  async function gateIsProvenGreen(repo, client, baseBranch, gateFiles = DEFAULT_GATE_FILES) {
14909
14909
  return (await readDefaultBranchGate(repo, client, baseBranch, Date.now(), gateFiles)).state === "green";
14910
14910
  }
14911
- async function activateProductRuleset(repo, rulesetBody, client, enforcement = "active") {
14911
+ async function activateProductRuleset(repo, rulesetBody, client, enforcement = "active", dryRun = false) {
14912
14912
  const want = new Set(rulesetRequiredContexts({ rules: rulesetBody.rules }));
14913
14913
  const wantStrict = rulesetStrictPolicy({ rules: rulesetBody.rules });
14914
14914
  const wantBranches = rulesetBranchIncludes(rulesetBody);
@@ -14927,10 +14927,10 @@ async function activateProductRuleset(repo, rulesetBody, client, enforcement = "
14927
14927
  if (detail.enforcement === effective && branchesMatch && strictMatch && have.size === want.size && [...want].every((c) => have.has(c))) {
14928
14928
  return { action: "skipped", enforcement: effective, detail: `${effective} ruleset already matches required contexts, branches and up-to-date policy${kept}` };
14929
14929
  }
14930
- await client.rest("PUT", `repos/${repo}/rulesets/${existing.id}`, { body: { ...rulesetBody, enforcement: effective }, timeoutMs: 2e4 });
14930
+ if (!dryRun) await client.rest("PUT", `repos/${repo}/rulesets/${existing.id}`, { body: { ...rulesetBody, enforcement: effective }, timeoutMs: 2e4 });
14931
14931
  return { action: "updated", enforcement: effective, detail: `ruleset ${existing.id}${kept}` };
14932
14932
  }
14933
- await client.rest("POST", `repos/${repo}/rulesets`, { body: { ...rulesetBody, enforcement }, timeoutMs: 2e4 });
14933
+ if (!dryRun) await client.rest("POST", `repos/${repo}/rulesets`, { body: { ...rulesetBody, enforcement }, timeoutMs: 2e4 });
14934
14934
  return { action: "created", enforcement };
14935
14935
  }
14936
14936
  async function gateContextEnforced(repo, rest) {
@@ -16383,7 +16383,7 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
16383
16383
  suite,
16384
16384
  ...suite === "mm-work" && deployModel === "tenant-container" ? {
16385
16385
  requiredBuildSecrets: ["NODE_AUTH_TOKEN=@github-packages-token"],
16386
- requiredChecks: ["gate", "screens / screens"]
16386
+ requiredChecks: options.existing?.requiredChecks ?? ["gate"]
16387
16387
  } : {},
16388
16388
  repos: [`${parsedRepo.owner}/${slug}`],
16389
16389
  branch: vars.BRANCH || (cls === "content" ? "main" : "development"),
@@ -17699,10 +17699,10 @@ var rollout_plan_default = {
17699
17699
  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)."
17700
17700
  },
17701
17701
  baseline: {
17702
- version: "4.5.103",
17703
- tag: "v4.5.103",
17704
- commit: "c2dd99cf0d72",
17705
- npm: "@mutmutco/cli@4.5.103"
17702
+ version: "4.5.104",
17703
+ tag: "v4.5.104",
17704
+ commit: "6bf765dc986c",
17705
+ npm: "@mutmutco/cli@4.5.104"
17706
17706
  },
17707
17707
  exitCriterion: "fleet-n-of-n",
17708
17708
  hubOnlyShortcut: "forbidden",
@@ -17719,14 +17719,14 @@ var rollout_plan_default = {
17719
17719
  repo: "mutmutco/mmi-hub",
17720
17720
  role: "canary",
17721
17721
  schedule: "train",
17722
- v3Target: "v4.5.103"
17722
+ v3Target: "v4.5.104"
17723
17723
  }
17724
17724
  ],
17725
17725
  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.",
17726
17726
  rollback: {
17727
17727
  independent: true,
17728
- mechanism: "npm dist-tag latest -> 4.5.103 and redeploy the Hub Lambda from tag v4.5.103 (c2dd99cf0d72); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17729
- v3Target: "v4.5.103 (@mutmutco/cli@4.5.103, tag commit c2dd99cf0d72 \u2014 last known-good release carrying the repo-index v4-only contract)"
17728
+ mechanism: "npm dist-tag latest -> 4.5.104 and redeploy the Hub Lambda from tag v4.5.104 (6bf765dc986c); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17729
+ v3Target: "v4.5.104 (@mutmutco/cli@4.5.104, tag commit 6bf765dc986c \u2014 last known-good release carrying the repo-index v4-only contract)"
17730
17730
  }
17731
17731
  },
17732
17732
  {
@@ -28344,7 +28344,7 @@ function planBranchRequiredContexts(input) {
28344
28344
  })).filter((entry) => entry.contexts.length > 0);
28345
28345
  return { common, pending, waiting };
28346
28346
  }
28347
- async function branchContextPlan(deps, repo, authority, meta) {
28347
+ async function branchContextPlan(deps, repo, authority, meta, emittedBranchesOnly = false) {
28348
28348
  const branches = resolveRequiredCheckBranches(meta, repo);
28349
28349
  const emittedByBranch = {};
28350
28350
  const unreadable = [];
@@ -28354,7 +28354,10 @@ async function branchContextPlan(deps, repo, authority, meta) {
28354
28354
  emittedByBranch[branch] = await resolveEmittedPrContexts(deps, repo, branch);
28355
28355
  if (presence === void 0 || emittedByBranch[branch] === void 0) unreadable.push(branch);
28356
28356
  }
28357
- return { ...planBranchRequiredContexts({ contexts: authority.contexts, branches, emittedByBranch }), unreadable };
28357
+ const bindingBranches = emittedBranchesOnly ? branches.filter((branch) => authority.contexts.some((context) => emittedByBranch[branch]?.includes(context))) : branches;
28358
+ const plan = planBranchRequiredContexts({ contexts: authority.contexts, branches: bindingBranches, emittedByBranch });
28359
+ const waiting = planBranchRequiredContexts({ contexts: authority.contexts, branches, emittedByBranch }).waiting;
28360
+ return { ...plan, waiting, unreadable, bindingBranches };
28358
28361
  }
28359
28362
  function projectCommonAuthority(authority, common) {
28360
28363
  const apiPayload = patchRulesetRequiredContexts(authority.apiPayload, common);
@@ -28608,8 +28611,8 @@ async function auditRepoCi(repo, callerDeps) {
28608
28611
  checks.push({
28609
28612
  ok: pendingAligned,
28610
28613
  label: "branch-specific required checks match workflow emitters",
28611
- detail: pendingAligned ? void 0 : "pending ruleset contexts or branch conditions have not converged to the emitted registry requirements",
28612
- remediation: pendingAligned ? void 0 : `mmi-cli devops ci reconcile --repo ${repo} --apply`
28614
+ detail: pendingAligned ? void 0 : `pending ruleset contexts or branch conditions have not converged to the emitted registry requirements \u2014 run mmi-cli devops bootstrap apply ${repo} --only product-ruleset --execute`,
28615
+ remediation: pendingAligned ? void 0 : `mmi-cli devops bootstrap apply ${repo} --only product-ruleset --execute`
28613
28616
  });
28614
28617
  }
28615
28618
  }
@@ -28641,7 +28644,7 @@ async function auditRepoCi(repo, callerDeps) {
28641
28644
  checks.push({
28642
28645
  ok: aligned,
28643
28646
  label: RULESET_REFERENCE_MATCH_LABEL,
28644
- detail: aligned ? `contexts [${authoritativeRuleset.contexts.join(", ")}], branches [${authoritativeRuleset.branchIncludes.join(", ")}] and the up-to-date policy match declared authority` : `${authoritativeRuleset.source} requires contexts [${authoritativeRuleset.contexts.join(", ")}]; ${authoritativeRuleset.branchSource} requires branches [${authoritativeRuleset.branchIncludes.join(", ")}]; ${PRODUCT_RULESET_REF} declares contexts [${authoritativeRuleset.committedContexts.join(", ")}] / branches [${authoritativeRuleset.committedBranchIncludes.join(", ")}] and live ${PRODUCT_RULESET_NAME} has contexts [${liveContexts.join(", ")}] / branches [${liveBranchIncludes.join(", ")}]` + (strictAligned ? "" : `; strict_required_status_checks_policy differs from committed policy \u2014 ${PRODUCT_RULESET_REF} declares ${authoritativeRuleset.committedStrict}, live ${PRODUCT_RULESET_NAME} has ${liveStrict}`),
28647
+ detail: aligned ? `contexts [${authoritativeRuleset.contexts.join(", ")}], branches [${authoritativeRuleset.branchIncludes.join(", ")}] and the up-to-date policy match declared authority` : `${authoritativeRuleset.source} requires contexts [${authoritativeRuleset.contexts.join(", ")}]; ${authoritativeRuleset.branchSource} requires branches [${authoritativeRuleset.branchIncludes.join(", ")}]; ${PRODUCT_RULESET_REF} declares contexts [${authoritativeRuleset.committedContexts.join(", ")}] / branches [${authoritativeRuleset.committedBranchIncludes.join(", ")}] and live ${PRODUCT_RULESET_NAME} has contexts [${liveContexts.join(", ")}] / branches [${liveBranchIncludes.join(", ")}]` + (strictAligned ? "" : `; strict_required_status_checks_policy differs from committed policy \u2014 ${PRODUCT_RULESET_REF} declares ${authoritativeRuleset.committedStrict}, live ${PRODUCT_RULESET_NAME} has ${liveStrict}`) + ` \u2014 live repair: mmi-cli devops bootstrap apply ${repo} --only product-ruleset --execute`,
28645
28648
  remediation: aligned ? void 0 : `mmi-cli devops ci reconcile --repo ${repo} --apply`
28646
28649
  });
28647
28650
  }
@@ -29184,9 +29187,14 @@ async function authoritativeSafetyHold(authority, deps, repo, baseBranch, meta)
29184
29187
  }
29185
29188
  return null;
29186
29189
  }
29187
- async function applyCiReconcileRepo(repo, deps) {
29190
+ async function applyCiReconcileRepo(repo, deps, options = {}) {
29191
+ if (options.dryRun && !options.productRulesetOnly) throw new Error("dry-run requires productRulesetOnly");
29188
29192
  const meta = await deps.getProjectMeta(slugFromRepo(repo));
29193
+ if (options.productRulesetOnly && !meta?.requiredChecks?.length) {
29194
+ return { repo, applied: [], skipped: [], errors: ["product-ruleset requires registry META requiredChecks"] };
29195
+ }
29189
29196
  const report = await auditRepoCi(repo, deps);
29197
+ const finish = (result2, pendingReason) => options.productRulesetOnly ? Promise.resolve(result2) : finalizeCiReconcile(repo, deps, result2, report, pendingReason);
29190
29198
  const baseBranch = "development";
29191
29199
  let raw = report.class === "deployable" && !report.explicitNoCi ? await fetchRulesetSeedBody(deps, repo) : null;
29192
29200
  let authority = null;
@@ -29196,12 +29204,12 @@ async function applyCiReconcileRepo(repo, deps) {
29196
29204
  authority = parseAuthoritativeRuleset(raw, meta, repo);
29197
29205
  } catch (e) {
29198
29206
  empty.errors.push(`${PRODUCT_RULESET_REF} is not parseable: ${e.message}`);
29199
- return finalizeCiReconcile(repo, deps, empty, report);
29207
+ return finish(empty);
29200
29208
  }
29201
29209
  const hold = await authoritativeSafetyHold(authority, deps, repo, baseBranch, meta);
29202
29210
  if (hold) {
29203
29211
  empty.skipped.push(hold);
29204
- return finalizeCiReconcile(repo, deps, empty, report, hold);
29212
+ return finish(empty, hold);
29205
29213
  }
29206
29214
  } else if (report.class === "deployable" && !report.explicitNoCi) {
29207
29215
  const registryAuthority = registryAuthorityWithoutReference(meta, repo);
@@ -29209,59 +29217,64 @@ async function applyCiReconcileRepo(repo, deps) {
29209
29217
  const hold = await authoritativeSafetyHold(registryAuthority, deps, repo, baseBranch, meta);
29210
29218
  if (hold) {
29211
29219
  empty.skipped.push(hold);
29212
- return finalizeCiReconcile(repo, deps, empty, report, hold);
29220
+ return finish(empty, hold);
29213
29221
  }
29214
29222
  }
29215
29223
  }
29216
- const result = await applyCiReconcileMergeSettingsFromReport(repo, deps, report);
29224
+ const result = options.productRulesetOnly ? empty : await applyCiReconcileMergeSettingsFromReport(repo, deps, report);
29217
29225
  if (report.class !== "deployable" || report.explicitNoCi) {
29218
- return finalizeCiReconcile(repo, deps, result, report);
29226
+ return finish(result);
29219
29227
  }
29220
- const refDelivery = await seedGateYml(repo, deps, meta, result);
29228
+ const refDelivery = options.productRulesetOnly ? void 0 : await seedGateYml(repo, deps, meta, result);
29221
29229
  if (refDelivery === "pending") {
29222
29230
  const reason = `${PRODUCT_RULESET_REF} is pending on its protected-base source PR; live ruleset convergence waits for that source to merge`;
29223
29231
  result.skipped.push(reason);
29224
- return finalizeCiReconcile(repo, deps, result, report, reason);
29232
+ return finish(result, reason);
29225
29233
  }
29226
29234
  raw ??= await fetchRulesetSeedBody(deps, repo);
29227
29235
  if (!raw) {
29228
29236
  result.errors.push(`missing ${PRODUCT_RULESET_REF} on development \u2014 run bootstrap apply first`);
29229
- return finalizeCiReconcile(repo, deps, result, report);
29237
+ return finish(result);
29230
29238
  }
29231
29239
  if (authority == null) {
29232
29240
  try {
29233
29241
  authority = parseAuthoritativeRuleset(raw, meta, repo);
29234
29242
  } catch (e) {
29235
29243
  result.errors.push(`${PRODUCT_RULESET_REF} is not parseable: ${e.message}`);
29236
- return finalizeCiReconcile(repo, deps, result, report);
29244
+ return finish(result);
29237
29245
  }
29238
29246
  const hold = await authoritativeSafetyHold(authority, deps, repo, baseBranch, meta);
29239
29247
  if (hold) {
29240
29248
  result.skipped.push(hold);
29241
- return finalizeCiReconcile(repo, deps, result, report, hold);
29249
+ return finish(result, hold);
29242
29250
  }
29243
29251
  }
29244
29252
  if (authority.contexts.length === 0) {
29245
29253
  result.errors.push(`${authority.source} declares no required contexts`);
29246
- return finalizeCiReconcile(repo, deps, result, report);
29254
+ return finish(result);
29247
29255
  }
29248
29256
  let pendingPlan = null;
29249
29257
  const declaredContexts = authority.contexts;
29250
29258
  if (authority.source === "registry META requiredChecks") {
29251
- pendingPlan = await branchContextPlan(deps, repo, authority, meta);
29259
+ pendingPlan = await branchContextPlan(deps, repo, authority, meta, options.productRulesetOnly);
29252
29260
  if (pendingPlan.unreadable.length) {
29253
29261
  const reason = `required branch workflow emitters unreadable on [${pendingPlan.unreadable.join(", ")}] \u2014 ruleset convergence deferred`;
29254
29262
  result.skipped.push(reason);
29255
- return finalizeCiReconcile(repo, deps, result, report, reason);
29263
+ return finish(result, reason);
29256
29264
  }
29257
29265
  if (pendingPlan.common.length === 0) {
29258
29266
  result.errors.push("no common required context is emitted by every required branch \u2014 ruleset convergence deferred");
29259
- return finalizeCiReconcile(repo, deps, result, report);
29267
+ return finish(result);
29260
29268
  }
29261
29269
  authority = projectCommonAuthority(authority, pendingPlan.common);
29270
+ if (options.productRulesetOnly) {
29271
+ const apiPayload = patchRulesetBranchIncludes(authority.apiPayload, pendingPlan.bindingBranches);
29272
+ authority = { ...authority, apiPayload, branchIncludes: rulesetBranchIncludes(apiPayload) };
29273
+ for (const entry of pendingPlan.waiting) result.skipped.push(`${entry.branch} waits for workflow emitter(s) [${entry.contexts.join(", ")}]`);
29274
+ }
29262
29275
  }
29263
29276
  const sourceNeedsConvergence = !sameContexts(authority.committedContexts, authority.contexts) || !sameContexts(authority.committedBranchIncludes, authority.branchIncludes) || !authority.committedStrict;
29264
- if (sourceNeedsConvergence) {
29277
+ if (sourceNeedsConvergence && !options.productRulesetOnly) {
29265
29278
  try {
29266
29279
  const delivery = await deliverSeedFile(
29267
29280
  deps,
@@ -29278,15 +29291,15 @@ async function applyCiReconcileRepo(repo, deps) {
29278
29291
  `${PRODUCT_RULESET_REF} authority \u2192 contexts [${authority.contexts.join(", ")}], branches [${authority.branchIncludes.join(", ")}] delivered on ${delivery.plan.branch} (${delivery.plan.reason})${delivery.prUrl ? `: ${delivery.prUrl}` : ""}`
29279
29292
  );
29280
29293
  const reason = `${PRODUCT_RULESET_REF} authority is pending on its protected-base source PR; live ruleset convergence waits for that source to merge`;
29281
- return finalizeCiReconcile(repo, deps, result, report, reason);
29294
+ return finish(result, reason);
29282
29295
  } else {
29283
29296
  result.applied.push(`reconciled ${PRODUCT_RULESET_REF} authority \u2192 contexts [${authority.contexts.join(", ")}], branches [${authority.branchIncludes.join(", ")}]`);
29284
29297
  }
29285
29298
  } catch (e) {
29286
29299
  result.errors.push(`ruleset reference delivery failed: ${e.message}`);
29287
- return finalizeCiReconcile(repo, deps, result, report);
29300
+ return finish(result);
29288
29301
  }
29289
- } else {
29302
+ } else if (!options.productRulesetOnly) {
29290
29303
  result.skipped.push(`${PRODUCT_RULESET_REF} already matches declared context and branch authority`);
29291
29304
  }
29292
29305
  let live;
@@ -29294,7 +29307,7 @@ async function applyCiReconcileRepo(repo, deps) {
29294
29307
  live = await readLiveProductRuleset(deps, repo);
29295
29308
  } catch (e) {
29296
29309
  result.errors.push(`cannot read the live ${PRODUCT_RULESET_NAME} ruleset: ${e.message}`);
29297
- return finalizeCiReconcile(repo, deps, result, report);
29310
+ return finish(result);
29298
29311
  }
29299
29312
  const liveContexts = live == null ? [] : sortedUnique(rulesetRequiredContexts(live));
29300
29313
  const liveBranchIncludes = live == null ? [] : rulesetBranchIncludes(live);
@@ -29312,7 +29325,7 @@ async function applyCiReconcileRepo(repo, deps) {
29312
29325
  if (pendingPlan?.pending.some((entry) => entry.contexts.some((context) => unsafe.includes(context)))) {
29313
29326
  const reason = `pending required-check rulesets left non-enforcing \u2014 [${unsafe.join(", ")}] emitted only by path-filtered workflows`;
29314
29327
  result.skipped.push(reason);
29315
- return finalizeCiReconcile(repo, deps, result, report, reason);
29328
+ return finish(result, reason);
29316
29329
  }
29317
29330
  const gateProvenGreen = await gateIsProvenGreen(repo, deps.client, baseBranch, gateFiles);
29318
29331
  const plan = resolveProductRulesetReconcilePlan({
@@ -29329,25 +29342,25 @@ async function applyCiReconcileRepo(repo, deps) {
29329
29342
  if (!plan.shouldActivate) {
29330
29343
  if (plan.holdReason) {
29331
29344
  result.skipped.push(plan.holdReason);
29332
- return finalizeCiReconcile(repo, deps, result, report, plan.holdReason);
29345
+ return finish(result, plan.holdReason);
29333
29346
  }
29334
29347
  result.skipped.push(`live ${PRODUCT_RULESET_NAME} contexts, branches and up-to-date policy already match declared authority`);
29335
29348
  }
29336
29349
  if (plan.shouldActivate) {
29337
29350
  try {
29338
- const activation = await activateProductRuleset(repo, authority.apiPayload, deps.client, plan.targetEnforcement);
29351
+ const activation = await activateProductRuleset(repo, authority.apiPayload, deps.client, plan.targetEnforcement, options.dryRun);
29339
29352
  if (activation.action === "skipped") result.skipped.push(activation.detail ?? "product ruleset");
29340
- else result.applied.push(`product ruleset ${activation.action}${activation.detail ? `: ${activation.detail}` : ""}`);
29353
+ else result.applied.push(`product ruleset ${activation.action} (${activation.enforcement}; contexts [${authority.contexts.join(", ")}], branches [${authority.branchIncludes.join(", ")}])${activation.detail ? `: ${activation.detail}` : ""}`);
29341
29354
  } catch (e) {
29342
29355
  result.errors.push(e.message);
29343
- return finalizeCiReconcile(repo, deps, result, report);
29356
+ return finish(result);
29344
29357
  }
29345
29358
  }
29346
29359
  if (pendingPlan) {
29347
29360
  if (pendingPlan.pending.length > 0 && !gateProvenGreen) {
29348
29361
  const reason = "pending required-check rulesets left non-enforcing \u2014 the gate is not green on development";
29349
29362
  result.skipped.push(reason);
29350
- return finalizeCiReconcile(repo, deps, result, report, reason);
29363
+ return finish(result, reason);
29351
29364
  }
29352
29365
  try {
29353
29366
  const desired = /* @__PURE__ */ new Set();
@@ -29363,20 +29376,20 @@ async function applyCiReconcileRepo(repo, deps) {
29363
29376
  patchRulesetRequiredContexts(statusOnly, entry.contexts),
29364
29377
  [entry.branch]
29365
29378
  );
29366
- const activation = await activateProductRuleset(repo, payload, deps.client);
29367
- (activation.action === "skipped" ? result.skipped : result.applied).push(`${name}: ${activation.action}`);
29379
+ const activation = await activateProductRuleset(repo, payload, deps.client, "active", options.dryRun);
29380
+ (activation.action === "skipped" ? result.skipped : result.applied).push(`${name}: ${activation.action} \u2014 contexts [${entry.contexts.join(", ")}]`);
29368
29381
  }
29369
29382
  const list = await deps.client.rest("GET", `repos/${repo}/rulesets`);
29370
29383
  for (const stale of list ?? []) {
29371
29384
  if (!stale.name?.startsWith(PRODUCT_PENDING_RULESET_PREFIX) || desired.has(stale.name) || stale.id == null) continue;
29372
- await deps.client.rest("DELETE", `repos/${repo}/rulesets/${stale.id}`);
29385
+ if (!options.dryRun) await deps.client.rest("DELETE", `repos/${repo}/rulesets/${stale.id}`);
29373
29386
  result.applied.push(`deleted converged ${stale.name}`);
29374
29387
  }
29375
29388
  } catch (e) {
29376
29389
  result.errors.push(`pending ruleset convergence failed: ${e.message}`);
29377
29390
  }
29378
29391
  }
29379
- return finalizeCiReconcile(repo, deps, result, report);
29392
+ return finish(result);
29380
29393
  }
29381
29394
 
29382
29395
  // src/command-composition.ts
@@ -34433,8 +34446,8 @@ function suiteModulePackageAccessCheck(suite, deployModel) {
34433
34446
  if (suite !== "mm-work" || deployModel !== "tenant-container") return void 0;
34434
34447
  return {
34435
34448
  ok: true,
34436
- label: "mmwork-ds package access (manual org-admin step; unverified)",
34437
- detail: "An org admin must add this repository with Read under @mutmutco/mmwork-ds \u2192 Package settings \u2192 Manage Actions access. No GitHub REST API can verify or grant this; without it CI fails with 403 read_package."
34449
+ label: "mmwork-ds + mmwork-screens package access (manual org-admin step; unverified)",
34450
+ detail: `An org admin must add this repository with Read under BOTH @mutmutco/mmwork-ds and @mutmutco/mmwork-screens \u2192 Package settings \u2192 Manage Actions access (#7984). No GitHub REST API can verify or grant this; without it the image build or the screens job fails with 403 read_package. Once screens is green, require it: mmi-cli oracle org project set <owner/repo> --var 'requiredChecks=["gate","screens / screens"]'.`
34438
34451
  };
34439
34452
  }
34440
34453
  function attachTrainDoctorVerdicts(report, trainDoctors) {
@@ -34830,7 +34843,7 @@ function registerBootstrapCommands(program3) {
34830
34843
  }
34831
34844
  if (findings.some((f) => f.state !== "waived")) process.exitCode = 1;
34832
34845
  });
34833
- bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").addOption(new Option("--class <class>", "deployable | content").default("deployable").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--box <name>", "resolve this live box and plan free registry DEPLOY# ports (container deploy models)").option("--domains <stages>", "optional comma-separated dev=host,rc=host,main=host deploy domains (requires --box)").option("--only <target>", "deliver ONLY this manifest target (#3818) \u2014 one file, no labels/ruleset/registry writes; or the special target environments (#5035) to run the deployment-environment reconcile alone (dry-run classifies, --execute writes)").option("--from-config", "with --only gate.yml: adopt an equal legacy gate with a render stamp, then update from registry config via review PR; refuse hand edits").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
34846
+ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").addOption(new Option("--class <class>", "deployable | content").default("deployable").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--box <name>", "resolve this live box and plan free registry DEPLOY# ports (container deploy models)").option("--domains <stages>", "optional comma-separated dev=host,rc=host,main=host deploy domains (requires --box)").option("--only <target>", "deliver ONLY this manifest target (#3818) \u2014 one file, no labels/ruleset/registry writes; or the special targets environments (#5035) / product-ruleset (#7986) to reconcile only that live surface (dry-run classifies, --execute writes)").option("--from-config", "with --only gate.yml: adopt an equal legacy gate with a render stamp, then update from registry config via review PR; refuse hand edits").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
34834
34847
  const o = {
34835
34848
  class: rawValue("--class", "deployable"),
34836
34849
  projectType: rawValue("--project-type", ""),
@@ -34917,6 +34930,26 @@ function registerBootstrapCommands(program3) {
34917
34930
  }
34918
34931
  return;
34919
34932
  }
34933
+ if (o.only.trim() === "product-ruleset") {
34934
+ if (cmdOpts.var?.length) return fail("bootstrap apply: --only product-ruleset accepts no --var overrides; set registry META instead");
34935
+ const read = await fetchProjectBySlugChecked(parsedRepo.slug, registryClientDeps(await loadConfig()));
34936
+ if (!read.ok) return failGraceful(`bootstrap apply: registry META unreadable (${read.error}); refusing product ruleset reconciliation`);
34937
+ if (!read.project?.requiredChecks?.length) return failGraceful("bootstrap apply: --only product-ruleset requires registry META requiredChecks");
34938
+ const result = await applyCiReconcileRepo(repo, {
34939
+ client: defaultGitHubClient(),
34940
+ listProjects: async () => [],
34941
+ getProjectMeta: async () => read.project
34942
+ }, { productRulesetOnly: true, dryRun: !o.execute });
34943
+ if (o.json) console.log(JSON.stringify(result, null, 2));
34944
+ else {
34945
+ console.log(`bootstrap apply --only product-ruleset \u2014 ${o.execute ? "LIVE" : "dry-run; no writes"} (${repo}):`);
34946
+ result.applied.forEach((line) => console.log(` ${o.execute ? "APPLY" : "PLAN"} ${line}`));
34947
+ result.skipped.forEach((line) => console.log(` SKIP ${line}`));
34948
+ result.errors.forEach((line) => console.error(` REFUSED ${line}`));
34949
+ }
34950
+ if (result.errors.length) return failGraceful("bootstrap apply: product ruleset reconciliation failed");
34951
+ return;
34952
+ }
34920
34953
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
34921
34954
  if (!(0, import_node_fs28.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
34922
34955
  const seedSource = await resolveHubSeedSource(execGitForSeedSource);
@@ -35003,9 +35036,10 @@ function registerBootstrapCommands(program3) {
35003
35036
  const onlyManagedBlock = onlyTarget ? seedsToApply[0]?.managedBlock != null : false;
35004
35037
  if (vars.SUITE === "mm-work" || registryMeta?.suite === "mm-work") {
35005
35038
  if (applyDeployModel === "tenant-container") {
35006
- const moduleGate = withDerivedRepoVars({}, parsedRepo, o.class, effectiveTrack, registryRequiredCheckBranches, ["gate", "screens / screens"]);
35039
+ const requiredChecks = registryMeta?.requiredChecks ?? ["gate"];
35040
+ const moduleGate = withDerivedRepoVars({}, parsedRepo, o.class, effectiveTrack, registryRequiredCheckBranches, requiredChecks);
35007
35041
  vars.GATE_RULESET_CONTEXTS_JSON = moduleGate.GATE_RULESET_CONTEXTS_JSON;
35008
- if (registryMeta) registryMeta = { ...registryMeta, requiredChecks: ["gate", "screens / screens"] };
35042
+ if (registryMeta) registryMeta = { ...registryMeta, requiredChecks };
35009
35043
  }
35010
35044
  }
35011
35045
  const controlClient = defaultGitHubClient();
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.103",
3
+ "version": "4.5.104",
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.103",
3
+ "version": "4.5.104",
4
4
  "description": "MMI workflow skills and org gates delivery.",
5
5
  "author": {
6
6
  "name": "MMI Future",
@@ -507,7 +507,7 @@ the bootstrap seed Dockerfile and compose templates). The seeded compose is file
507
507
  `labels: { mmi.runtime-env: "true" }`, no `env_file`, and a loopback-only port; the deploy injects the stage's
508
508
  vault values at runtime (`DEPLOY#<stage>.noEnvFile: true`) and the app reads plain env vars — it must **not** self-load SSM and must **not** ship a committed `.env`.
509
509
 
510
- For a `suite: mm-work` tenant-container module, bootstrap apply selects the MM-Work module seed instead: a live `/health` server and `/module.js`, the example module scaffold, secret-backed image build, and `gate` plus `screens / screens`. It sets `requiredBuildSecrets: ["NODE_AUTH_TOKEN=@github-packages-token"]` and those required checks. Before the first CI/deploy, an org admin must grant the repo Read on `@mutmutco/mmwork-ds` under Package settings → Manage Actions access (no GitHub REST API; verify names the manual step).
510
+ For a `suite: mm-work` tenant-container module, bootstrap apply selects the MM-Work module seed instead: a live `/health` server and `/module.js`, the example module scaffold, secret-backed image build, and `gate` plus `screens / screens`. It sets `requiredBuildSecrets: ["NODE_AUTH_TOKEN=@github-packages-token"]` and requires only `gate`. Before the first CI/deploy, an org admin must grant the repo Read on both `@mutmutco/mmwork-ds` and `@mutmutco/mmwork-screens` under Package settings → Manage Actions access (no GitHub REST API; verify names the manual step). Add `screens / screens` to `requiredChecks` only after the screens job is green, or every PR blocks.
511
511
 
512
512
  **Never provision a Google OAuth client for an MM-Work suite module, even if old META declares `oauth`.** Modules sign in through the MM-Work pass and reach Google via `ctx.act`; read the package's GUIDE. The suite seed omits the Google-login guide.
513
513