@mutmutco/cli 4.5.45 → 4.5.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.cjs +132 -44
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -14365,6 +14365,9 @@ async function activateProductRuleset(repo, rulesetBody, client, enforcement = "
14365
14365
  return { action: "created", enforcement };
14366
14366
  }
14367
14367
 
14368
+ // src/bootstrap-apply.ts
14369
+ var import_node_crypto5 = require("node:crypto");
14370
+
14368
14371
  // src/bootstrap-seeds.ts
14369
14372
  var PLACEHOLDER_RE = /\{\{([A-Z0-9_]+)\}\}/g;
14370
14373
  function loadBootstrapSeeds(manifestJson) {
@@ -15593,6 +15596,32 @@ function planSeedAction(seed, exists) {
15593
15596
  }
15594
15597
  return exists ? { target: seed.target, action: "update", ownership: "org", reason: "org-owned, refresh to current" } : { target: seed.target, action: "create", ownership: "org", reason: "org-owned, missing" };
15595
15598
  }
15599
+ var GATE_RENDER_NOTE = "# Rendered from the registry gate config by mmi-cli devops bootstrap apply <repo> --only gate.yml --from-config; edit the config, not this file.\n";
15600
+ var gateHash = (body) => (0, import_node_crypto5.createHash)("sha256").update(body, "utf8").digest("hex");
15601
+ function parseGateRenderStamp(content) {
15602
+ const match = /^# mmi-gate-render: sha256=([a-f0-9]{64})\n/.exec(content);
15603
+ return match ? { hash: match[1], body: content.slice(match[0].length) } : null;
15604
+ }
15605
+ function verifyGateRenderStamp(content) {
15606
+ const stamp = parseGateRenderStamp(content);
15607
+ return stamp !== null && stamp.body.startsWith(GATE_RENDER_NOTE) && gateHash(stamp.body) === stamp.hash;
15608
+ }
15609
+ function stampGateRender(rendered) {
15610
+ const body = GATE_RENDER_NOTE + rendered;
15611
+ return `# mmi-gate-render: sha256=${gateHash(body)}
15612
+ ${body}`;
15613
+ }
15614
+ function planGateFromConfig(current, rendered) {
15615
+ const target = ".github/workflows/gate.yml";
15616
+ const base = { target, ownership: "repo" };
15617
+ if (current === null) return { ...base, action: "create", reason: "registry gate config; review via branch + PR" };
15618
+ const stamp = parseGateRenderStamp(current);
15619
+ if (stamp) {
15620
+ if (!verifyGateRenderStamp(current)) return { ...base, action: "skip", refused: true, reason: "REFUSED: stamped gate was hand-edited since its last render (diff below)" };
15621
+ return stamp.body === GATE_RENDER_NOTE + rendered ? { ...base, action: "skip", reason: "already current from registry gate config" } : { ...base, action: "update", reason: "registry gate config changed; update stamped gate via branch + PR" };
15622
+ }
15623
+ return current === rendered ? { ...base, action: "update", reason: "adopt legacy gate: add registry render stamp via branch + PR" } : { ...base, action: "skip", refused: true, reason: "REFUSED: unstamped gate differs from registry render; make the registry config reproduce the file first, then re-run to adopt (diff below)" };
15624
+ }
15596
15625
  function reconcileSeedAction(action, content, isManagedBlock, remoteContent = null) {
15597
15626
  if (action.action === "skip") return action;
15598
15627
  if (content == null) {
@@ -17014,10 +17043,10 @@ var rollout_plan_default = {
17014
17043
  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)."
17015
17044
  },
17016
17045
  baseline: {
17017
- version: "4.5.45",
17018
- tag: "v4.5.45",
17019
- commit: "f66545d593d2",
17020
- npm: "@mutmutco/cli@4.5.45"
17046
+ version: "4.5.47",
17047
+ tag: "v4.5.47",
17048
+ commit: "d8722f771c7d",
17049
+ npm: "@mutmutco/cli@4.5.47"
17021
17050
  },
17022
17051
  exitCriterion: "fleet-n-of-n",
17023
17052
  hubOnlyShortcut: "forbidden",
@@ -17034,14 +17063,14 @@ var rollout_plan_default = {
17034
17063
  repo: "mutmutco/mmi-hub",
17035
17064
  role: "canary",
17036
17065
  schedule: "train",
17037
- v3Target: "v4.5.45"
17066
+ v3Target: "v4.5.47"
17038
17067
  }
17039
17068
  ],
17040
17069
  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.",
17041
17070
  rollback: {
17042
17071
  independent: true,
17043
- mechanism: "npm dist-tag latest -> 4.5.45 and redeploy the Hub Lambda from tag v4.5.45 (f66545d593d2); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17044
- v3Target: "v4.5.45 (@mutmutco/cli@4.5.45, tag commit f66545d593d2 \u2014 last known-good release carrying the repo-index v4-only contract)"
17072
+ mechanism: "npm dist-tag latest -> 4.5.47 and redeploy the Hub Lambda from tag v4.5.47 (d8722f771c7d); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17073
+ v3Target: "v4.5.47 (@mutmutco/cli@4.5.47, tag commit d8722f771c7d \u2014 last known-good release carrying the repo-index v4-only contract)"
17045
17074
  }
17046
17075
  },
17047
17076
  {
@@ -22319,7 +22348,7 @@ function checkLineEndings(probe) {
22319
22348
  // src/release-ledger.ts
22320
22349
  var import_node_fs22 = require("node:fs");
22321
22350
  var import_node_path22 = require("node:path");
22322
- var import_node_crypto5 = require("node:crypto");
22351
+ var import_node_crypto6 = require("node:crypto");
22323
22352
 
22324
22353
  // src/file-lock.ts
22325
22354
  var import_promises2 = require("node:fs/promises");
@@ -22677,7 +22706,7 @@ function releaseLedgerPath(cwd) {
22677
22706
  }
22678
22707
  function atomicWriteJson(path2, ledger) {
22679
22708
  (0, import_node_fs22.mkdirSync)((0, import_node_path22.dirname)(path2), { recursive: true });
22680
- const tmp = `${path2}.tmp-${process.pid}-${(0, import_node_crypto5.randomBytes)(4).toString("hex")}`;
22709
+ const tmp = `${path2}.tmp-${process.pid}-${(0, import_node_crypto6.randomBytes)(4).toString("hex")}`;
22681
22710
  (0, import_node_fs22.writeFileSync)(tmp, `${JSON.stringify(ledger, null, 2)}
22682
22711
  `, "utf8");
22683
22712
  (0, import_node_fs22.renameSync)(tmp, path2);
@@ -29092,13 +29121,13 @@ function renderPluginCachePlan(plan, applied) {
29092
29121
  }
29093
29122
 
29094
29123
  // ../shared/github-quota-scope.ts
29095
- var import_node_crypto6 = require("node:crypto");
29124
+ var import_node_crypto7 = require("node:crypto");
29096
29125
  function appQuotaScope(appId, installationId, origin = "https://api.github.com", resource = "graphql") {
29097
- return `github-app-v1:${(0, import_node_crypto6.createHash)("sha256").update(JSON.stringify([new URL(origin).origin, appId.trim(), installationId.trim(), resource])).digest("hex")}`;
29126
+ return `github-app-v1:${(0, import_node_crypto7.createHash)("sha256").update(JSON.stringify([new URL(origin).origin, appId.trim(), installationId.trim(), resource])).digest("hex")}`;
29098
29127
  }
29099
29128
 
29100
29129
  // src/app-actor.ts
29101
- var import_node_crypto7 = require("node:crypto");
29130
+ var import_node_crypto8 = require("node:crypto");
29102
29131
  var APP_ACTOR_ENV = "MMI_ACTOR";
29103
29132
  var APP_VAULT_REPO = "mutmutco/MMI-Hub";
29104
29133
  var APP_VAULT_KEYS = ["GITHUB_APP_ID", "GITHUB_APP_INSTALLATION_ID", "GITHUB_APP_PRIVATE_KEY"];
@@ -29149,7 +29178,7 @@ function mintAppJwt(appId, privateKeyPem, nowSec) {
29149
29178
  exp: now + APP_JWT_TTL_S,
29150
29179
  iss: appId
29151
29180
  }));
29152
- const signer = (0, import_node_crypto7.createSign)("RSA-SHA256");
29181
+ const signer = (0, import_node_crypto8.createSign)("RSA-SHA256");
29153
29182
  signer.update(`${header}.${payload}`);
29154
29183
  return `${header}.${payload}.${signer.sign(privateKeyPem, "base64url")}`;
29155
29184
  }
@@ -30045,7 +30074,7 @@ var import_node_os16 = require("node:os");
30045
30074
  var import_node_path27 = require("node:path");
30046
30075
 
30047
30076
  // src/box-control.ts
30048
- var import_node_crypto8 = require("node:crypto");
30077
+ var import_node_crypto9 = require("node:crypto");
30049
30078
  var MANIFEST_PATH = "infra/box-control/manifest.json";
30050
30079
  var ALLOWED_POST = /* @__PURE__ */ new Set(["systemctl daemon-reload"]);
30051
30080
  function parseManifest(text) {
@@ -30092,7 +30121,7 @@ function filesForBox(files, box) {
30092
30121
  return files.flatMap((file) => file.targets.filter((t) => t.box === box).map((t) => ({ file, dest: t.dest })));
30093
30122
  }
30094
30123
  function md5(content) {
30095
- return (0, import_node_crypto8.createHash)("md5").update(content).digest("hex");
30124
+ return (0, import_node_crypto9.createHash)("md5").update(content).digest("hex");
30096
30125
  }
30097
30126
  function planRemoteScript(dests) {
30098
30127
  const lines2 = ["set -u"];
@@ -31958,7 +31987,7 @@ function renderDeployPortDoctor(report) {
31958
31987
  }
31959
31988
 
31960
31989
  // src/bootstrap-drift.ts
31961
- var import_node_crypto9 = require("node:crypto");
31990
+ var import_node_crypto10 = require("node:crypto");
31962
31991
  function byteComparableSeeds(manifest, cls) {
31963
31992
  return manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self" && s.classes.includes(cls));
31964
31993
  }
@@ -31978,7 +32007,7 @@ function compareSeedBytes(hubContent, repoContent) {
31978
32007
  return normalize(hubContent) === normalize(repoContent) ? "match" : "drift";
31979
32008
  }
31980
32009
  function seedContentHash(content) {
31981
- return (0, import_node_crypto9.createHash)("sha256").update(content.replace(/\r\n/g, "\n"), "utf8").digest("hex");
32010
+ return (0, import_node_crypto10.createHash)("sha256").update(content.replace(/\r\n/g, "\n"), "utf8").digest("hex");
31982
32011
  }
31983
32012
  function auditRepoSeedDrift(repo, seeds, hubContents, repoReads) {
31984
32013
  const byTarget = new Map(repoReads.map((r) => [r.target, r.content]));
@@ -32140,7 +32169,7 @@ function renderPropagationReport(plan) {
32140
32169
  }
32141
32170
 
32142
32171
  // src/bootstrap-propagation-identity.ts
32143
- var import_node_crypto10 = require("node:crypto");
32172
+ var import_node_crypto11 = require("node:crypto");
32144
32173
  var PROPAGATION_BRANCH_PREFIX = "seed-propagate-";
32145
32174
  var TARGET_MARKER_NAME = "mmi-bootstrap-propagation-target";
32146
32175
  function safeBranchPart(value, maxLength, fallback) {
@@ -32153,7 +32182,7 @@ function repoSlug2(repo) {
32153
32182
  function propagationBranch(repo, target) {
32154
32183
  const repoPart = safeBranchPart(repoSlug2(repo), 32, "repo");
32155
32184
  const targetPart = safeBranchPart(target, 48, "target");
32156
- const hash = (0, import_node_crypto10.createHash)("sha256").update(repo.trim().toLowerCase()).update("\0").update(target).digest("hex").slice(0, 12);
32185
+ const hash = (0, import_node_crypto11.createHash)("sha256").update(repo.trim().toLowerCase()).update("\0").update(target).digest("hex").slice(0, 12);
32157
32186
  return `${PROPAGATION_BRANCH_PREFIX}${repoPart}-${targetPart}-${hash}`;
32158
32187
  }
32159
32188
  function legacyPropagationBranch(repo) {
@@ -33309,7 +33338,7 @@ function renderBootstrapVerifyReport(report) {
33309
33338
  }
33310
33339
 
33311
33340
  // src/bootstrap-seed-delivery.ts
33312
- var import_node_crypto11 = require("node:crypto");
33341
+ var import_node_crypto12 = require("node:crypto");
33313
33342
  init_github_client();
33314
33343
  var SHA = /^[a-f0-9]{40}$/;
33315
33344
  async function readSeedFile(repo, target, ref, client) {
@@ -33332,7 +33361,7 @@ async function readTargetedSeedFiles(repo, plan, target, ref, client) {
33332
33361
  const candidate = ref === plan.baseSha ? base : await readSeedFile(repo, target, ref, client);
33333
33362
  return { base, candidate };
33334
33363
  }
33335
- async function targetedSeedPlan(repo, slug, target, baseBranch, sourceSha, client) {
33364
+ async function targetedSeedPlan(repo, slug, target, baseBranch, sourceSha, client, forcePr = false) {
33336
33365
  const [rules, base] = await Promise.all([
33337
33366
  client.rest("GET", `repos/${repo}/rules/branches/${encodeURIComponent(baseBranch)}`),
33338
33367
  client.rest("GET", `repos/${repo}/git/ref/heads/${encodeURIComponent(baseBranch)}`)
@@ -33342,8 +33371,10 @@ async function targetedSeedPlan(repo, slug, target, baseBranch, sourceSha, clien
33342
33371
  }
33343
33372
  const baseSha = base?.object?.sha;
33344
33373
  if (!baseSha || !SHA.test(baseSha) || !SHA.test(sourceSha)) throw new Error("bootstrap apply: seed identity is unreadable");
33345
- const identity = (0, import_node_crypto11.createHash)("sha256").update(JSON.stringify([repo.toLowerCase(), target, baseSha, sourceSha])).digest("hex").slice(0, 24);
33346
- return { ...planSeedDelivery(rules, slug, baseBranch, `bootstrap-seed-${identity}`), baseSha, baseBranch };
33374
+ const identity = (0, import_node_crypto12.createHash)("sha256").update(JSON.stringify([repo.toLowerCase(), target, baseSha, sourceSha])).digest("hex").slice(0, 24);
33375
+ const delivery = planSeedDelivery(rules, slug, baseBranch, `bootstrap-seed-${identity}`);
33376
+ const scoped = forcePr && delivery.mode === "direct" ? { mode: "pr", ref: `bootstrap-seed-${identity}-${slug}`, branch: `bootstrap-seed-${identity}-${slug}`, reason: "registry gate render requires review" } : delivery;
33377
+ return { ...scoped, baseSha, baseBranch };
33347
33378
  }
33348
33379
  async function verifyTargetedSeedBranch(repo, plan, target, client, allowMissing = false) {
33349
33380
  let ref;
@@ -33405,6 +33436,24 @@ async function enableTargetedSeedAutoMerge(repo, number, plan, target, expected,
33405
33436
 
33406
33437
  // src/bootstrap-commands.ts
33407
33438
  var execGitForSeedSource = async (args) => (await execFileP("git", args, { timeout: GIT_TIMEOUT_MS })).stdout;
33439
+ async function gateRenderDiff(current, rendered) {
33440
+ const dir = (0, import_node_fs28.mkdtempSync)((0, import_node_path28.join)((0, import_node_os17.tmpdir)(), "mmi-gate-diff-"));
33441
+ const before = (0, import_node_path28.join)(dir, "committed-gate.yml");
33442
+ const after = (0, import_node_path28.join)(dir, "registry-render.yml");
33443
+ try {
33444
+ (0, import_node_fs28.writeFileSync)(before, current);
33445
+ (0, import_node_fs28.writeFileSync)(after, rendered);
33446
+ try {
33447
+ return (await execFileP("git", ["diff", "--no-index", "--", before, after], { timeout: GIT_TIMEOUT_MS })).stdout;
33448
+ } catch (e) {
33449
+ const result = e;
33450
+ if (result.code === 1 && result.stdout) return result.stdout;
33451
+ throw e;
33452
+ }
33453
+ } finally {
33454
+ (0, import_node_fs28.rmSync)(dir, { recursive: true, force: true });
33455
+ }
33456
+ }
33408
33457
  function bootstrapTrainDoctorDeps() {
33409
33458
  return {
33410
33459
  run: async (file, args) => (process.platform === "win32" && file === "npm" ? await execFileP("cmd.exe", ["/c", "npm", ...args], { timeout: 3e4 }) : await execFileP(file, args, { timeout: 3e4 })).stdout,
@@ -33645,18 +33694,21 @@ function registerBootstrapCommands(program3) {
33645
33694
  }
33646
33695
  if (findings.some((f) => f.state !== "waived")) process.exitCode = 1;
33647
33696
  });
33648
- 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("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
33697
+ 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) => {
33649
33698
  const o = {
33650
33699
  class: rawValue("--class", "deployable"),
33651
33700
  projectType: rawValue("--project-type", ""),
33652
33701
  deployModel: rawValue("--deploy-model", ""),
33653
33702
  releaseTrack: rawValue("--release-track", ""),
33654
33703
  only: rawValue("--only", ""),
33704
+ fromConfig: rawFlag("--from-config"),
33655
33705
  execute: rawFlag("--execute"),
33656
33706
  json: rawFlag("--json"),
33657
33707
  box: rawValue("--box", ""),
33658
33708
  domains: rawValue("--domains", "")
33659
33709
  };
33710
+ if (o.fromConfig && (o.only.trim() !== ".github/workflows/gate.yml" && o.only.trim() !== "gate.yml" || o.class !== "deployable" || cmdOpts.var?.length))
33711
+ return fail("bootstrap apply: --from-config requires --only gate.yml, --class deployable, and no --var overrides");
33660
33712
  if (o.domains && !o.box) return fail("bootstrap apply: --domains requires --box");
33661
33713
  if (o.box && o.only) return fail("bootstrap apply: --box cannot be combined with --only");
33662
33714
  let boxDomains;
@@ -33725,7 +33777,7 @@ function registerBootstrapCommands(program3) {
33725
33777
  const manifest = loadBootstrapSeeds((0, import_node_fs28.readFileSync)(manifestPath, "utf8"));
33726
33778
  const baseBranch = o.class === "content" ? "main" : "development";
33727
33779
  const slug = parsedRepo.slug;
33728
- const onlyTarget = o.only.trim();
33780
+ const onlyTarget = o.fromConfig && o.only.trim() === "gate.yml" ? ".github/workflows/gate.yml" : o.only.trim();
33729
33781
  const seedsToApply = onlyTarget ? manifest.seeds.filter((s) => s.target === onlyTarget || s.target.replace("{{REPO_SLUG}}", slug) === onlyTarget) : manifest.seeds;
33730
33782
  if (onlyTarget && !seedsToApply.length) {
33731
33783
  const known = manifest.seeds.map((s) => s.target).join("\n ");
@@ -33761,6 +33813,8 @@ function registerBootstrapCommands(program3) {
33761
33813
  registryMeta = meta;
33762
33814
  registryRequiredCheckBranches = meta?.requiredCheckBranches;
33763
33815
  for (const [k, v] of Object.entries(gateConfigToVars(meta?.gate))) if (rawVars[k] == null) rawVars[k] = v;
33816
+ } else if (o.fromConfig) {
33817
+ return failGraceful(`bootstrap apply: registry META unreadable (${metaRead.error}); --from-config requires the committed registry gate config`);
33764
33818
  } else if (o.execute && !onlyTarget) {
33765
33819
  return failGraceful(`bootstrap apply: registry META unreadable (${metaRead.error}); refusing the live registry write, which would reset a registered project's releaseTrack and edgeDomains to class defaults (#7138)`);
33766
33820
  } else {
@@ -33867,10 +33921,42 @@ function registerBootstrapCommands(program3) {
33867
33921
  let scopedPlan;
33868
33922
  let scopedContent;
33869
33923
  let seededToBranch = 0;
33924
+ let configGateAction;
33925
+ let configGateContent;
33870
33926
  if (onlyTarget) {
33871
- scopedPlan = await targetedSeedPlan(repo, slug, onlyTarget, baseBranch, seedSource.sha, controlClient);
33927
+ scopedPlan = await targetedSeedPlan(repo, slug, onlyTarget, baseBranch, seedSource.sha, controlClient, o.fromConfig);
33872
33928
  seedPlan = scopedPlan;
33873
- if (o.execute && scopedPlan.branch) {
33929
+ if (o.fromConfig) {
33930
+ const gate = registryMeta?.gate;
33931
+ if (!gate || !Object.keys(gate).length) return failGraceful("bootstrap apply: --from-config requires a non-empty registry gate config; set it with oracle org project set --var gate={...} first");
33932
+ const gateSeed = seedsToApply[0];
33933
+ if (gateSeed.waivers?.[slug]) return failGraceful(`bootstrap apply: gate.yml is waived for ${slug}: ${gateSeed.waivers[slug]}`);
33934
+ if (gateSeed.source !== "seed:gate.template.yml") return failGraceful("bootstrap apply: gate template is not the ci-reconcile seed");
33935
+ try {
33936
+ const gateVars = withDerivedRepoVars(
33937
+ { ...gateConfigToVars(gate), REPO_SLUG: slug },
33938
+ parsedRepo,
33939
+ "deployable",
33940
+ effectiveTrack,
33941
+ registryMeta?.requiredCheckBranches,
33942
+ registryMeta?.requiredChecks
33943
+ );
33944
+ const rendered = resolveSeedContent(gateSeed, gateVars, readFile9);
33945
+ if (rendered === null || missingPlaceholders(rendered).length) return failGraceful("bootstrap apply: registry gate template is unreadable or has unfilled placeholders");
33946
+ const base = (await readTargetedSeedFiles(repo, scopedPlan, onlyTarget, scopedPlan.baseSha, controlClient)).base?.content ?? null;
33947
+ configGateAction = planGateFromConfig(base, rendered);
33948
+ if (configGateAction.refused && base !== null) {
33949
+ const stamped = parseGateRenderStamp(base);
33950
+ const committedBody = stamped?.body.startsWith(GATE_RENDER_NOTE) ? stamped.body.slice(GATE_RENDER_NOTE.length) : stamped?.body ?? base;
33951
+ console.error(await gateRenderDiff(committedBody, rendered));
33952
+ return failGraceful(`bootstrap apply: ${configGateAction.reason}`);
33953
+ }
33954
+ configGateContent = stampGateRender(rendered);
33955
+ } catch (e) {
33956
+ return failGraceful(`bootstrap apply --from-config: ${e.message}`);
33957
+ }
33958
+ }
33959
+ if (o.execute && scopedPlan.branch && configGateAction?.action !== "skip") {
33874
33960
  await prepareTargetedSeedBranch(repo, scopedPlan, onlyTarget, controlClient, gh2);
33875
33961
  }
33876
33962
  } else if (o.execute) {
@@ -33896,7 +33982,7 @@ function registerBootstrapCommands(program3) {
33896
33982
  }
33897
33983
  }
33898
33984
  const docsForIndex = [];
33899
- const seedReadRef = !o.execute && scopedPlan ? scopedPlan.baseSha : seedPlan.ref;
33985
+ const seedReadRef = scopedPlan && (!o.execute || configGateAction?.action === "skip") ? scopedPlan.baseSha : seedPlan.ref;
33900
33986
  for (const seed of seedsToApply) {
33901
33987
  if (!seed.classes.includes(o.class)) continue;
33902
33988
  if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
@@ -33936,14 +34022,16 @@ function registerBootstrapCommands(program3) {
33936
34022
  } catch {
33937
34023
  exists = false;
33938
34024
  }
33939
- const planned = planSeedAction(resolved, scopedPlan ? baseContent !== null : exists);
34025
+ const planned = configGateAction ?? planSeedAction(resolved, scopedPlan ? baseContent !== null : exists);
33940
34026
  if (scopedPlan && planned.action !== "skip") planned.action = exists ? "update" : "create";
33941
34027
  const isLegacyBlock = resolved.source === "managed-block";
33942
34028
  let content = null;
33943
34029
  let isManaged = isLegacyBlock || resolved.managedBlock != null;
33944
34030
  const preservedContent = scopedPlan ? baseContent : remoteContent;
33945
34031
  if (planned.action === "create" || planned.action === "update") {
33946
- if (isLegacyBlock) {
34032
+ if (o.fromConfig) {
34033
+ content = configGateContent;
34034
+ } else if (isLegacyBlock) {
33947
34035
  content = upsertManagedGitignoreBlock(preservedContent).content;
33948
34036
  } else {
33949
34037
  const rulesetVars = preserveRuleset ? gateSeedVars(
@@ -34012,8 +34100,8 @@ function registerBootstrapCommands(program3) {
34012
34100
  }
34013
34101
  }
34014
34102
  let seedPrUrl;
34015
- const scopedHead = o.execute && scopedPlan?.branch ? await verifyTargetedSeedBranch(repo, scopedPlan, onlyTarget, controlClient) : null;
34016
- if (o.execute && seedPlan.mode === "pr" && seedPlan.branch && (seededToBranch > 0 || scopedContent !== void 0 && scopedHead !== null && scopedHead !== scopedPlan?.baseSha)) {
34103
+ const scopedHead = o.execute && scopedPlan?.branch && configGateAction?.action !== "skip" ? await verifyTargetedSeedBranch(repo, scopedPlan, onlyTarget, controlClient) : null;
34104
+ if (o.execute && seedPlan.mode === "pr" && seedPlan.branch && configGateAction?.action !== "skip" && (seededToBranch > 0 || scopedContent !== void 0 && scopedHead !== null && scopedHead !== scopedPlan?.baseSha)) {
34017
34105
  await gh2(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-F", `delete_branch_on_merge=${MANAGED_DELETE_BRANCH_ON_MERGE}`]).catch(() => {
34018
34106
  });
34019
34107
  const openPrs = await gh2(["pr", "list", "--repo", repo, "--head", seedPlan.branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
@@ -34031,9 +34119,9 @@ function registerBootstrapCommands(program3) {
34031
34119
  "--head",
34032
34120
  seedPlan.branch,
34033
34121
  "--title",
34034
- onlyTarget ? `chore: propagate ${onlyManagedBlock ? "Hub-managed block in" : "org-owned"} ${onlyTarget} from MMI-Hub` : `bootstrap: seed ${parsedRepo.name} (${baseBranch} is protected)`,
34122
+ o.fromConfig ? `chore: render ${onlyTarget} from registry gate config` : onlyTarget ? `chore: propagate ${onlyManagedBlock ? "Hub-managed block in" : "org-owned"} ${onlyTarget} from MMI-Hub` : `bootstrap: seed ${parsedRepo.name} (${baseBranch} is protected)`,
34035
34123
  "--body",
34036
- onlyTarget ? `Auto-opened by \`mmi-cli devops bootstrap apply ${repo} --only ${onlyTarget} --execute\` (#3818).
34124
+ o.fromConfig ? `Auto-opened by \`mmi-cli devops bootstrap apply ${repo} --only gate.yml --from-config --execute\`. The gate is rendered from the repo's registry gate config through the ci-reconcile seed renderer at MMI-Hub@${seedSource.sha}. This PR carries only \`${onlyTarget}\` for review; no registry META or other controls were changed.` : onlyTarget ? `Auto-opened by \`mmi-cli devops bootstrap apply ${repo} --only ${onlyTarget} --execute\` (#3818).
34037
34125
 
34038
34126
  ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owned \`${onlyTarget}\`` : `The org-owned \`${onlyTarget}\` seed`} is declared by MMI-Hub's \`skills/bootstrap/seeds/manifest.json\` at MMI-Hub@${seedSource.sha}; this PR reconciles that declared scope and preserves everything outside it. It carries that file and nothing else \u2014 no labels, ruleset, merge settings or registry META were touched.
34039
34127
 
@@ -34946,7 +35034,7 @@ function formatSuiteStatus(report) {
34946
35034
  // src/suite-train.ts
34947
35035
  var import_node_fs29 = require("node:fs");
34948
35036
  var import_node_path29 = require("node:path");
34949
- var import_node_crypto12 = require("node:crypto");
35037
+ var import_node_crypto13 = require("node:crypto");
34950
35038
  var SCHEMA = "mmi-suite-cut/v1";
34951
35039
  var SHA2 = /^[a-f0-9]{40}$/i;
34952
35040
  var RC_TAG = /^v\d+\.\d+\.\d+-rc\.\d+$/;
@@ -34982,7 +35070,7 @@ function readSuiteCut(path2) {
34982
35070
  }
34983
35071
  function writeSuiteCut(path2, record) {
34984
35072
  (0, import_node_fs29.mkdirSync)((0, import_node_path29.dirname)(path2), { recursive: true });
34985
- const temp = `${path2}.${(0, import_node_crypto12.randomBytes)(8).toString("hex")}.tmp`;
35073
+ const temp = `${path2}.${(0, import_node_crypto13.randomBytes)(8).toString("hex")}.tmp`;
34986
35074
  try {
34987
35075
  (0, import_node_fs29.writeFileSync)(temp, JSON.stringify(record, null, 2), { flag: "wx" });
34988
35076
  (0, import_node_fs29.renameSync)(temp, path2);
@@ -38761,7 +38849,7 @@ init_github_client();
38761
38849
 
38762
38850
  // src/issue-commands.ts
38763
38851
  var import_node_fs34 = require("node:fs");
38764
- var import_node_crypto13 = require("node:crypto");
38852
+ var import_node_crypto14 = require("node:crypto");
38765
38853
  init_cli_shared();
38766
38854
  init_clean_exit();
38767
38855
  init_error_codes();
@@ -39234,7 +39322,7 @@ function rowIdempotencyKey(batchKey, spec) {
39234
39322
  const identity = `${spec.type}
39235
39323
  ${spec.title.trim()}
39236
39324
  ${spec.body ?? ""}`;
39237
- const hash = (0, import_node_crypto13.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
39325
+ const hash = (0, import_node_crypto14.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
39238
39326
  return `${batchKey}:${hash}`;
39239
39327
  }
39240
39328
  var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo", "surface"]);
@@ -49303,14 +49391,14 @@ vercel preview (${preview.branch}): ${d.state}${d.url ? ` \u2014 ${d.url}` : ""}
49303
49391
  }
49304
49392
 
49305
49393
  // src/tenant-artifact.ts
49306
- var import_node_crypto14 = require("node:crypto");
49394
+ var import_node_crypto15 = require("node:crypto");
49307
49395
  var import_node_fs48 = require("node:fs");
49308
49396
  var import_promises10 = require("node:fs/promises");
49309
49397
  var import_node_path46 = require("node:path");
49310
49398
  var ARTIFACT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
49311
49399
  var MAX_BYTES = 5 * 1024 * 1024 * 1024;
49312
49400
  async function sha256File(path2) {
49313
- const hash = (0, import_node_crypto14.createHash)("sha256");
49401
+ const hash = (0, import_node_crypto15.createHash)("sha256");
49314
49402
  for await (const chunk of (0, import_node_fs48.createReadStream)(path2)) hash.update(chunk);
49315
49403
  return hash.digest("hex");
49316
49404
  }
@@ -49540,13 +49628,13 @@ async function executeWaveLand(plan, deps) {
49540
49628
 
49541
49629
  // src/dist-drift.ts
49542
49630
  var import_node_child_process18 = require("node:child_process");
49543
- var import_node_crypto16 = require("node:crypto");
49631
+ var import_node_crypto17 = require("node:crypto");
49544
49632
  var import_node_fs50 = require("node:fs");
49545
49633
  var import_node_os22 = require("node:os");
49546
49634
  var import_node_path48 = require("node:path");
49547
49635
 
49548
49636
  // ../scripts/distribution-digest.mjs
49549
- var import_node_crypto15 = require("node:crypto");
49637
+ var import_node_crypto16 = require("node:crypto");
49550
49638
  var import_node_fs49 = require("node:fs");
49551
49639
  var import_node_path47 = require("node:path");
49552
49640
  var slash = (value) => value.replaceAll("\\", "/");
@@ -49559,7 +49647,7 @@ function repoPath(root, declaredPath, label) {
49559
49647
  return target;
49560
49648
  }
49561
49649
  function digestFiles(files) {
49562
- const hash = (0, import_node_crypto15.createHash)("sha256");
49650
+ const hash = (0, import_node_crypto16.createHash)("sha256");
49563
49651
  for (const file of [...files].sort((a, b) => a.relative.localeCompare(b.relative))) {
49564
49652
  const content = file.stat.isSymbolicLink() ? Buffer.from((0, import_node_fs49.readlinkSync)(file.absolute), "utf8") : (0, import_node_fs49.readFileSync)(file.absolute);
49565
49653
  hash.update(file.relative, "utf8");
@@ -49589,7 +49677,7 @@ var DIST_ARTIFACTS = [
49589
49677
  ];
49590
49678
  var BOM_DIST_TREE_ID = "mmi-cli-dist";
49591
49679
  var ABSENT = "absent";
49592
- var sha256 = (bytes) => `sha256:${(0, import_node_crypto16.createHash)("sha256").update(bytes).digest("hex")}`;
49680
+ var sha256 = (bytes) => `sha256:${(0, import_node_crypto17.createHash)("sha256").update(bytes).digest("hex")}`;
49593
49681
  function artifactDrift(path2, committedBytes, rebuiltBytes) {
49594
49682
  const committed = committedBytes ? sha256(committedBytes) : ABSENT;
49595
49683
  const rebuiltExpected = rebuiltBytes ? sha256(rebuiltBytes) : ABSENT;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.45",
3
+ "version": "4.5.47",
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",