@mutmutco/cli 4.3.19 → 4.3.21

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 +528 -271
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -12902,6 +12902,29 @@ async function waitForPrChecks(deps) {
12902
12902
 
12903
12903
  // src/bootstrap-ruleset.ts
12904
12904
  var PRODUCT_RULESET_NAME = "mmi-product-required-checks";
12905
+ var PRODUCT_RULESET_PATH = ".github/rulesets/mmi-product-required-checks.json";
12906
+ function reseedProductRulesetStrictness(current, seed) {
12907
+ const parse = (raw) => {
12908
+ const policy2 = JSON.parse(raw);
12909
+ if (!policy2 || typeof policy2 !== "object" || Array.isArray(policy2) || policy2.name !== PRODUCT_RULESET_NAME || policy2.target !== "branch" || !Array.isArray(policy2.rules) || policy2.rules.some((r) => !r || typeof r !== "object" || Array.isArray(r) || typeof r.type !== "string")) {
12910
+ throw new Error("product ruleset must be a branch ruleset object with well-formed rules");
12911
+ }
12912
+ const required = policy2.rules.filter((r) => r.type === "required_status_checks");
12913
+ const parameters2 = required[0]?.parameters;
12914
+ if (required.length !== 1 || !parameters2 || typeof parameters2 !== "object" || Array.isArray(parameters2) || !Array.isArray(parameters2.required_status_checks) || !parameters2.required_status_checks.length || parameters2.required_status_checks.some((c) => !c || typeof c !== "object" || Array.isArray(c) || typeof c.context !== "string" || !c.context.trim()) || parameters2.strict_required_status_checks_policy !== void 0 && typeof parameters2.strict_required_status_checks_policy !== "boolean") {
12915
+ throw new Error("product ruleset must carry exactly one well-formed required_status_checks rule");
12916
+ }
12917
+ return { policy: policy2, parameters: parameters2 };
12918
+ };
12919
+ const strict = parse(seed).parameters.strict_required_status_checks_policy;
12920
+ if (typeof strict !== "boolean") throw new Error("canonical product ruleset must declare boolean strictness");
12921
+ if (current === null) return seed;
12922
+ const { policy, parameters } = parse(current);
12923
+ if (parameters.strict_required_status_checks_policy === strict) return current;
12924
+ parameters.strict_required_status_checks_policy = strict;
12925
+ return `${JSON.stringify(policy, null, 2)}
12926
+ `;
12927
+ }
12905
12928
  function stripRulesetComment(raw) {
12906
12929
  const parsed = JSON.parse(raw);
12907
12930
  delete parsed._comment;
@@ -12921,14 +12944,6 @@ function rulesetStrictPolicy(ruleset) {
12921
12944
  const rules = (ruleset.rules ?? []).filter((rule) => rule.type === "required_status_checks");
12922
12945
  return rules.length > 0 && rules.every((rule) => rule.parameters?.strict_required_status_checks_policy === true);
12923
12946
  }
12924
- function patchRulesetStrictPolicy(body) {
12925
- const rules = (body.rules ?? []).map((rule) => {
12926
- const r = rule;
12927
- if (r.type !== "required_status_checks") return rule;
12928
- return { ...r, parameters: { ...r.parameters, strict_required_status_checks_policy: true } };
12929
- });
12930
- return { ...body, rules };
12931
- }
12932
12947
  function rulesetBranchIncludes(ruleset) {
12933
12948
  const raw = ruleset.conditions?.ref_name?.include;
12934
12949
  return Array.isArray(raw) ? [...new Set(raw.filter((ref) => typeof ref === "string" && ref.length > 0))].sort((a, b) => a.localeCompare(b)) : [];
@@ -12948,8 +12963,7 @@ function patchRulesetRequiredContexts(body, contexts) {
12948
12963
  ...r,
12949
12964
  parameters: {
12950
12965
  ...r.parameters,
12951
- strict_required_status_checks_policy: true,
12952
- // #6263: org policy — see patchRulesetStrictPolicy
12966
+ strict_required_status_checks_policy: r.parameters?.strict_required_status_checks_policy ?? false,
12953
12967
  required_status_checks: sorted.map((context) => ({ context }))
12954
12968
  }
12955
12969
  };
@@ -15502,10 +15516,10 @@ var rollout_plan_default = {
15502
15516
  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)."
15503
15517
  },
15504
15518
  baseline: {
15505
- version: "4.3.19",
15506
- tag: "v4.3.19",
15507
- commit: "5776052e13af",
15508
- npm: "@mutmutco/cli@4.3.19"
15519
+ version: "4.3.21",
15520
+ tag: "v4.3.21",
15521
+ commit: "f55fc2217cd3",
15522
+ npm: "@mutmutco/cli@4.3.21"
15509
15523
  },
15510
15524
  exitCriterion: "fleet-n-of-n",
15511
15525
  hubOnlyShortcut: "forbidden",
@@ -15522,14 +15536,14 @@ var rollout_plan_default = {
15522
15536
  repo: "mutmutco/mmi-hub",
15523
15537
  role: "canary",
15524
15538
  schedule: "train",
15525
- v3Target: "v4.3.19"
15539
+ v3Target: "v4.3.21"
15526
15540
  }
15527
15541
  ],
15528
15542
  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.",
15529
15543
  rollback: {
15530
15544
  independent: true,
15531
- mechanism: "npm dist-tag latest -> 4.3.19 and redeploy the Hub Lambda from tag v4.3.19 (5776052e13af); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15532
- v3Target: "v4.3.19 (@mutmutco/cli@4.3.19, tag commit 5776052e13af \u2014 last known-good release carrying the repo-index v4-only contract)"
15545
+ mechanism: "npm dist-tag latest -> 4.3.21 and redeploy the Hub Lambda from tag v4.3.21 (f55fc2217cd3); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15546
+ v3Target: "v4.3.21 (@mutmutco/cli@4.3.21, tag commit f55fc2217cd3 \u2014 last known-good release carrying the repo-index v4-only contract)"
15533
15547
  }
15534
15548
  },
15535
15549
  {
@@ -23527,12 +23541,12 @@ function parseAuthoritativeRuleset(raw, meta, repo) {
23527
23541
  const contexts = sortedUnique(registryContexts ?? committedContexts);
23528
23542
  const explicitBranches = Array.isArray(meta?.requiredCheckBranches) && meta.requiredCheckBranches.length > 0 ? resolveRequiredCheckBranches(meta, repo) : null;
23529
23543
  const committedStrict = rulesetStrictPolicy(committedPayload);
23530
- let apiPayload = patchRulesetStrictPolicy(registryContexts == null ? committedPayload : patchRulesetRequiredContexts(committedPayload, contexts));
23544
+ let apiPayload = registryContexts == null ? committedPayload : patchRulesetRequiredContexts(committedPayload, contexts);
23531
23545
  if (explicitBranches) apiPayload = patchRulesetBranchIncludes(apiPayload, explicitBranches);
23532
23546
  const branchIncludes = rulesetBranchIncludes(apiPayload);
23533
23547
  const authoritativeFilePayload = {
23534
23548
  ...filePayload,
23535
- ...registryContexts == null && committedStrict ? {} : { rules: apiPayload.rules },
23549
+ ...registryContexts == null ? {} : { rules: apiPayload.rules },
23536
23550
  ...explicitBranches == null ? {} : { conditions: apiPayload.conditions }
23537
23551
  };
23538
23552
  return {
@@ -23554,7 +23568,7 @@ function sameContexts(left, right) {
23554
23568
  function resolveProductRulesetReconcilePlan(input) {
23555
23569
  const liveNeedsContextConvergence = !sameContexts(input.liveContexts, input.authorityContexts);
23556
23570
  const liveNeedsBranchConvergence = input.liveBranchIncludes !== void 0 && input.authorityBranchIncludes !== void 0 && !sameContexts(input.liveBranchIncludes, input.authorityBranchIncludes);
23557
- const liveNeedsStrictConvergence = input.liveStrict === false;
23571
+ const liveNeedsStrictConvergence = input.liveStrict !== void 0 && input.authorityStrict !== void 0 && input.liveStrict !== input.authorityStrict;
23558
23572
  const liveIsActive = input.liveEnforcement === "active";
23559
23573
  if (liveIsActive && !liveNeedsContextConvergence && !liveNeedsBranchConvergence && !liveNeedsStrictConvergence) {
23560
23574
  return { shouldActivate: false, targetEnforcement: "active" };
@@ -23765,12 +23779,12 @@ async function auditRepoCi(repo, deps) {
23765
23779
  const liveContextsAligned = sameContexts(liveContexts, authoritativeRuleset.contexts);
23766
23780
  const fileBranchesAligned = sameContexts(authoritativeRuleset.committedBranchIncludes, authoritativeRuleset.branchIncludes);
23767
23781
  const liveBranchesAligned = sameContexts(liveBranchIncludes, authoritativeRuleset.branchIncludes);
23768
- const strictAligned = authoritativeRuleset.committedStrict && liveStrict;
23782
+ const strictAligned = authoritativeRuleset.committedStrict === liveStrict;
23769
23783
  const aligned = fileContextsAligned && liveContextsAligned && fileBranchesAligned && liveBranchesAligned && strictAligned;
23770
23784
  checks.push({
23771
23785
  ok: aligned,
23772
23786
  label: RULESET_REFERENCE_MATCH_LABEL,
23773
- 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 must be true (require branches to be up to date before merging, #6263) \u2014 ${PRODUCT_RULESET_REF} declares ${authoritativeRuleset.committedStrict}, live ${PRODUCT_RULESET_NAME} has ${liveStrict}`),
23787
+ 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}`),
23774
23788
  remediation: aligned ? void 0 : `mmi-cli devops ci reconcile --repo ${repo} --apply`
23775
23789
  });
23776
23790
  }
@@ -24360,6 +24374,7 @@ async function applyCiReconcileRepo(repo, deps) {
24360
24374
  liveBranchIncludes,
24361
24375
  authorityBranchIncludes: authority.branchIncludes,
24362
24376
  liveStrict: live == null ? void 0 : rulesetStrictPolicy(live),
24377
+ authorityStrict: authority.committedStrict,
24363
24378
  gateProvenGreen: await gateIsProvenGreen(repo, deps.client, baseBranch, gateFiles),
24364
24379
  unsafeContexts: unsafe
24365
24380
  });
@@ -27167,11 +27182,6 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
27167
27182
  label: "product required-check ruleset enforcement active",
27168
27183
  detail: productRuleset?.enforcement !== "active" ? `${PRODUCT_RULESET_NAME} is ${productRuleset?.enforcement ?? "missing"} \u2014 run mmi-cli devops ci reconcile --apply --repo ${repo} once the gate is green` : void 0
27169
27184
  });
27170
- checks.push({
27171
- ok: productRuleset != null && rulesetStrictPolicy(productRuleset),
27172
- label: "product required-check ruleset requires an up-to-date branch",
27173
- detail: productRuleset != null && rulesetStrictPolicy(productRuleset) ? void 0 : `${PRODUCT_RULESET_NAME} ${productRuleset == null ? "is missing" : "has strict_required_status_checks_policy=false"} \u2014 run mmi-cli devops ci reconcile --apply --repo ${repo} (#6263)`
27174
- });
27175
27185
  const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
27176
27186
  const missing = requiredProductStatusChecks.filter((check) => !statusChecks.has(check));
27177
27187
  checks.push({
@@ -27225,6 +27235,100 @@ function renderBootstrapVerifyReport(report) {
27225
27235
  return lines2.join("\n");
27226
27236
  }
27227
27237
 
27238
+ // src/bootstrap-seed-delivery.ts
27239
+ var import_node_crypto9 = require("node:crypto");
27240
+ var SHA = /^[a-f0-9]{40}$/;
27241
+ async function readSeedFile(repo, target, ref, client) {
27242
+ try {
27243
+ const file = await client.rest(
27244
+ "GET",
27245
+ `repos/${repo}/contents/${target.split("/").map(encodeURIComponent).join("/")}?ref=${encodeURIComponent(ref)}`
27246
+ );
27247
+ if (!file?.sha || !SHA.test(file.sha) || file.encoding !== "base64" || typeof file.content !== "string") {
27248
+ throw new Error("bootstrap apply: scoped seed file is unreadable");
27249
+ }
27250
+ return { sha: file.sha, content: decodeGitHubContents(file.content) };
27251
+ } catch (e) {
27252
+ if (e instanceof GitHubApiError && e.status === 404) return null;
27253
+ throw e;
27254
+ }
27255
+ }
27256
+ async function readTargetedSeedFiles(repo, plan, target, ref, client) {
27257
+ const base = await readSeedFile(repo, target, plan.baseSha, client);
27258
+ const candidate = ref === plan.baseSha ? base : await readSeedFile(repo, target, ref, client);
27259
+ return { base, candidate };
27260
+ }
27261
+ async function targetedSeedPlan(repo, slug, target, baseBranch, sourceSha, client) {
27262
+ const [rules, base] = await Promise.all([
27263
+ client.rest("GET", `repos/${repo}/rules/branches/${encodeURIComponent(baseBranch)}`),
27264
+ client.rest("GET", `repos/${repo}/git/ref/heads/${encodeURIComponent(baseBranch)}`)
27265
+ ]);
27266
+ if (!Array.isArray(rules) || rules.some((rule) => !rule || typeof rule.type !== "string" || !rule.type)) {
27267
+ throw new Error("bootstrap apply: base branch protection is unreadable");
27268
+ }
27269
+ const baseSha = base?.object?.sha;
27270
+ if (!baseSha || !SHA.test(baseSha) || !SHA.test(sourceSha)) throw new Error("bootstrap apply: seed identity is unreadable");
27271
+ const identity = (0, import_node_crypto9.createHash)("sha256").update(JSON.stringify([repo.toLowerCase(), target, baseSha, sourceSha])).digest("hex").slice(0, 24);
27272
+ return { ...planSeedDelivery(rules, slug, baseBranch, `bootstrap-seed-${identity}`), baseSha, baseBranch };
27273
+ }
27274
+ async function verifyTargetedSeedBranch(repo, plan, target, client, allowMissing = false) {
27275
+ let ref;
27276
+ try {
27277
+ ref = await client.rest("GET", `repos/${repo}/git/ref/heads/${encodeURIComponent(plan.ref)}`);
27278
+ } catch (e) {
27279
+ if (allowMissing && e instanceof GitHubApiError && e.status === 404) return null;
27280
+ throw e;
27281
+ }
27282
+ const head = ref?.object?.sha;
27283
+ if (!head || !SHA.test(head)) throw new Error("bootstrap apply: scoped seed head is unreadable");
27284
+ if (head === plan.baseSha) return head;
27285
+ const diff = await client.rest("GET", `repos/${repo}/compare/${plan.baseSha}...${head}`);
27286
+ if (diff?.status !== "ahead" || diff.merge_base_commit?.sha !== plan.baseSha || !Array.isArray(diff.files) || diff.files.length !== 1 || diff.files[0].filename !== target || diff.files[0].previous_filename !== void 0 || !["added", "modified"].includes(diff.files[0].status ?? "")) {
27287
+ throw new Error("bootstrap apply: scoped seed branch contains changes outside the requested target or base");
27288
+ }
27289
+ return head;
27290
+ }
27291
+ async function prepareTargetedSeedBranch(repo, plan, target, client, gh) {
27292
+ const candidates = await client.rest(
27293
+ "GET",
27294
+ `repos/${repo}/pulls?state=open&head=${encodeURIComponent(`${repo.split("/")[0]}:${plan.branch}`)}&base=${encodeURIComponent(plan.baseBranch)}&per_page=2`
27295
+ );
27296
+ if (!Array.isArray(candidates) || candidates.length > 1) throw new Error("bootstrap apply: scoped seed PR identity is unreadable");
27297
+ for (const pr of candidates) {
27298
+ if (!Number.isInteger(pr?.number) || pr.number <= 0 || pr.head?.ref !== plan.branch || pr.base?.ref !== plan.baseBranch) {
27299
+ throw new Error("bootstrap apply: scoped seed PR identity is unreadable");
27300
+ }
27301
+ await gh(["pr", "merge", String(pr.number), "--repo", repo, "--disable-auto"]);
27302
+ const after = await client.rest("GET", `repos/${repo}/pulls/${pr.number}`);
27303
+ if (after?.auto_merge !== null) throw new Error("bootstrap apply: scoped seed PR auto-merge was not disabled");
27304
+ }
27305
+ const existing = await verifyTargetedSeedBranch(repo, plan, target, client, true);
27306
+ if (existing) return;
27307
+ try {
27308
+ await client.rest("POST", `repos/${repo}/git/refs`, { body: { ref: `refs/heads/${plan.branch}`, sha: plan.baseSha } });
27309
+ } catch (e) {
27310
+ if (!(e instanceof GitHubApiError) || e.status !== 422) throw e;
27311
+ }
27312
+ await verifyTargetedSeedBranch(repo, plan, target, client);
27313
+ }
27314
+ async function enableTargetedSeedAutoMerge(repo, number, plan, target, expected, client, gh) {
27315
+ const head = await verifyTargetedSeedBranch(repo, plan, target, client);
27316
+ const pr = await client.rest("GET", `repos/${repo}/pulls/${number}`);
27317
+ if (pr?.state !== "open" || pr.head?.ref !== plan.branch || pr.head?.sha !== head || pr.head.repo?.full_name?.toLowerCase() !== repo.toLowerCase() || pr.base?.ref !== plan.baseBranch || pr.base.sha !== plan.baseSha || pr.base.repo?.full_name?.toLowerCase() !== repo.toLowerCase() || pr.changed_files !== 1) throw new Error("bootstrap apply: scoped seed PR identity or diff changed");
27318
+ const files = await client.rest(
27319
+ "GET",
27320
+ `repos/${repo}/pulls/${number}/files?per_page=2`
27321
+ );
27322
+ if (!Array.isArray(files) || files.length !== 1 || files[0]?.filename !== target || files[0].previous_filename !== void 0) {
27323
+ throw new Error("bootstrap apply: scoped seed PR contains changes outside the requested target");
27324
+ }
27325
+ const file = await readSeedFile(repo, target, head, client);
27326
+ if (file?.content !== expected) {
27327
+ throw new Error("bootstrap apply: scoped seed content differs from this invocation");
27328
+ }
27329
+ await gh(["pr", "merge", String(number), "--repo", repo, "--auto", "--squash", "--match-head-commit", head]);
27330
+ }
27331
+
27228
27332
  // src/bootstrap-commands.ts
27229
27333
  var execGitForSeedSource = async (args) => (await execFileP("git", args, { timeout: GIT_TIMEOUT_MS })).stdout;
27230
27334
  function bootstrapTrainDoctorDeps() {
@@ -27632,14 +27736,23 @@ function registerBootstrapCommands(program3) {
27632
27736
  });
27633
27737
  }
27634
27738
  let seedPlan = { mode: "direct", ref: baseBranch, reason: "dry-run (no protection probe)" };
27739
+ let scopedPlan;
27740
+ let scopedContent;
27635
27741
  let seededToBranch = 0;
27636
- if (o.execute) {
27742
+ if (onlyTarget) {
27743
+ scopedPlan = await targetedSeedPlan(repo, slug, onlyTarget, baseBranch, seedSource.sha, controlClient);
27744
+ seedPlan = scopedPlan;
27745
+ if (o.execute && scopedPlan.branch) {
27746
+ await prepareTargetedSeedBranch(repo, scopedPlan, onlyTarget, controlClient, gh);
27747
+ }
27748
+ } else if (o.execute) {
27637
27749
  let branchRules = [];
27638
27750
  try {
27639
27751
  branchRules = JSON.parse((await gh(["api", `repos/${repo}/rules/branches/${baseBranch}`])).stdout || "[]");
27640
27752
  } catch {
27641
- branchRules = [];
27753
+ throw new Error("bootstrap apply: base branch protection is unreadable");
27642
27754
  }
27755
+ if (!Array.isArray(branchRules)) throw new Error("bootstrap apply: base branch protection is unreadable");
27643
27756
  seedPlan = planSeedDelivery(branchRules, slug, baseBranch);
27644
27757
  if (seedPlan.branch) {
27645
27758
  const headSha = (await gh(["api", `repos/${repo}/git/ref/heads/${baseBranch}`, "--jq", ".object.sha"])).stdout.trim();
@@ -27651,6 +27764,7 @@ function registerBootstrapCommands(program3) {
27651
27764
  }
27652
27765
  }
27653
27766
  const docsForIndex = [];
27767
+ const seedReadRef = !o.execute && scopedPlan ? scopedPlan.baseSha : seedPlan.ref;
27654
27768
  for (const seed of seedsToApply) {
27655
27769
  if (!seed.classes.includes(o.class)) continue;
27656
27770
  if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
@@ -27664,8 +27778,20 @@ function registerBootstrapCommands(program3) {
27664
27778
  let exists = false;
27665
27779
  let sha;
27666
27780
  let remoteContent = null;
27667
- try {
27668
- const r = await gh(["api", `repos/${repo}/contents/${enc(resolved.target)}?ref=${seedPlan.ref}`]);
27781
+ let baseContent = null;
27782
+ const preserveRuleset = onlyTarget === PRODUCT_RULESET_PATH;
27783
+ if (scopedPlan) {
27784
+ try {
27785
+ const files = await readTargetedSeedFiles(repo, scopedPlan, resolved.target, seedReadRef, controlClient);
27786
+ baseContent = files.base?.content ?? null;
27787
+ exists = files.candidate !== null;
27788
+ sha = files.candidate?.sha;
27789
+ remoteContent = files.candidate?.content ?? null;
27790
+ } catch (e) {
27791
+ return failGraceful(`bootstrap apply: cannot read scoped seed file; no replacement made: ${e.message}`);
27792
+ }
27793
+ } else try {
27794
+ const r = await gh(["api", `repos/${repo}/contents/${enc(resolved.target)}?ref=${seedReadRef}`]);
27669
27795
  exists = true;
27670
27796
  try {
27671
27797
  const parsed = JSON.parse(r.stdout);
@@ -27678,23 +27804,34 @@ function registerBootstrapCommands(program3) {
27678
27804
  } catch {
27679
27805
  exists = false;
27680
27806
  }
27681
- const planned = planSeedAction(resolved, exists);
27807
+ const planned = planSeedAction(resolved, scopedPlan ? baseContent !== null : exists);
27808
+ if (scopedPlan && planned.action !== "skip") planned.action = exists ? "update" : "create";
27682
27809
  const isLegacyBlock = resolved.source === "managed-block";
27683
27810
  let content = null;
27684
27811
  let isManaged = isLegacyBlock || resolved.managedBlock != null;
27812
+ const preservedContent = scopedPlan ? baseContent : remoteContent;
27685
27813
  if (planned.action === "create" || planned.action === "update") {
27686
27814
  if (isLegacyBlock) {
27687
- content = upsertManagedGitignoreBlock(remoteContent).content;
27815
+ content = upsertManagedGitignoreBlock(preservedContent).content;
27688
27816
  } else {
27689
- const writeContent = resolveSeedWriteContent(resolved, vars, readFile9, remoteContent);
27817
+ const writeContent = resolveSeedWriteContent(resolved, vars, readFile9, preservedContent);
27690
27818
  if (!writeContent.ok) {
27691
27819
  return fail(`bootstrap apply: ${resolved.target}: ${writeContent.reason} \u2014 refusing to overwrite repo-owned content`);
27692
27820
  }
27693
27821
  content = writeContent.content;
27822
+ if (preserveRuleset) {
27823
+ if (content === null) return fail("bootstrap apply: canonical product ruleset seed is unreadable");
27824
+ try {
27825
+ content = reseedProductRulesetStrictness(baseContent, content);
27826
+ } catch (e) {
27827
+ return fail(`bootstrap apply: product ruleset preservation refused: ${e.message}`);
27828
+ }
27829
+ }
27694
27830
  isManaged = writeContent.managed;
27695
27831
  }
27696
27832
  }
27697
27833
  const action = reconcileSeedAction(planned, content, isManaged, remoteContent);
27834
+ if (onlyTarget && content !== null) scopedContent = content;
27698
27835
  actions.push(action);
27699
27836
  const docBody = content ?? remoteContent;
27700
27837
  if (resolved.target.startsWith("docs/") && resolved.target.endsWith(".md") && docBody !== null) {
@@ -27729,7 +27866,8 @@ function registerBootstrapCommands(program3) {
27729
27866
  }
27730
27867
  }
27731
27868
  let seedPrUrl;
27732
- if (o.execute && seedPlan.mode === "pr" && seedPlan.branch && seededToBranch > 0) {
27869
+ const scopedHead = o.execute && scopedPlan?.branch ? await verifyTargetedSeedBranch(repo, scopedPlan, onlyTarget, controlClient) : null;
27870
+ if (o.execute && seedPlan.mode === "pr" && seedPlan.branch && (seededToBranch > 0 || scopedContent !== void 0 && scopedHead !== null && scopedHead !== scopedPlan?.baseSha)) {
27733
27871
  await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-F", `delete_branch_on_merge=${MANAGED_DELETE_BRANCH_ON_MERGE}`]).catch(() => {
27734
27872
  });
27735
27873
  const openPrs = await gh(["pr", "list", "--repo", repo, "--head", seedPlan.branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
@@ -27758,7 +27896,12 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
27758
27896
  seedPrUrl = created.url;
27759
27897
  }
27760
27898
  let autoMergeEnabled = true;
27761
- await gh(["pr", "merge", seedPrUrl, "--repo", repo, "--auto", "--squash"]).catch((e) => {
27899
+ if (scopedPlan) {
27900
+ if (scopedContent === void 0) throw new Error("bootstrap apply: scoped seed content is unresolved");
27901
+ const number = Number(seedPrUrl.split("/").pop());
27902
+ if (!Number.isInteger(number) || number <= 0) throw new Error("bootstrap apply: scoped seed PR number is unreadable");
27903
+ await enableTargetedSeedAutoMerge(repo, number, scopedPlan, onlyTarget, scopedContent, controlClient, gh);
27904
+ } else await gh(["pr", "merge", seedPrUrl, "--repo", repo, "--auto", "--squash"]).catch((e) => {
27762
27905
  const message2 = String(e.message ?? "");
27763
27906
  if (/already/i.test(message2)) return;
27764
27907
  if (/clean status|enablePullRequestAutoMerge/i.test(message2)) {
@@ -27941,7 +28084,7 @@ ${onlyManagedBlock ? `Only the marker-bounded Hub-managed block inside repo-owne
27941
28084
  deployFactsRead: controlDeployFacts !== null
27942
28085
  });
27943
28086
  }
27944
- if (o.json) console.log(JSON.stringify({ repo, class: o.class, sourceSha: seedSource.sha, only: onlyTarget || null, execute: o.execute, seedDelivery: seedPlan.mode, seedPrUrl, actions, controlPlane: controlRows, applied, ddbWrites }, null, 2));
28087
+ if (o.json) console.log(JSON.stringify({ repo, class: o.class, sourceSha: seedSource.sha, only: onlyTarget || null, execute: o.execute, seedDelivery: seedPlan.mode, seedBranch: seedPlan.branch, seedBaseSha: scopedPlan?.baseSha, seedPrUrl, actions, controlPlane: controlRows, applied, ddbWrites }, null, 2));
27945
28088
  else {
27946
28089
  console.log(renderSeedPlan(actions));
27947
28090
  if (controlRows.length) console.log(`
@@ -27968,7 +28111,7 @@ LIVE apply to ${repo}:
27968
28111
  ${propagatable.map((s) => s.target).join("\n ")}`);
27969
28112
  if (!seed.managedBlock && !(0, import_node_fs26.existsSync)(seed.target)) return fail(`bootstrap propagate: the Hub's own copy of '${seed.target}' is missing \u2014 nothing to propagate`);
27970
28113
  const hubContent = seed.managedBlock ? null : (0, import_node_fs26.readFileSync)(seed.target, "utf8");
27971
- const readSeedFile = (path2) => (0, import_node_fs26.existsSync)(path2) ? (0, import_node_fs26.readFileSync)(path2, "utf8") : null;
28114
+ const readSeedFile2 = (path2) => (0, import_node_fs26.existsSync)(path2) ? (0, import_node_fs26.readFileSync)(path2, "utf8") : null;
27972
28115
  const isWorkflowSeed = seed.target.startsWith(".github/workflows/");
27973
28116
  const cfg = await loadConfig();
27974
28117
  const projects = await fetchProjectsList(registryClientDeps(cfg));
@@ -28045,14 +28188,14 @@ LIVE apply to ${repo}:
28045
28188
  const project2 = projects.find((p) => (p.repos ?? []).some((repo) => repo.toLowerCase() === r.repo.toLowerCase()));
28046
28189
  const track = resolveReleaseTrack(project2, void 0, r.repo);
28047
28190
  const vars = withDerivedRepoVars({}, parseOwnerRepo(r.repo), repoClass, track, project2?.requiredCheckBranches);
28048
- const resolved = resolveSeedWriteContent(seed, vars, readSeedFile, content);
28191
+ const resolved = resolveSeedWriteContent(seed, vars, readSeedFile2, content);
28049
28192
  if (!resolved.ok || resolved.content == null) {
28050
28193
  return fail(`bootstrap propagate: ${r.repo} ${seed.target}: ${resolved.ok ? "rendered no content" : resolved.reason} \u2014 refusing an incomplete per-repo render`);
28051
28194
  }
28052
28195
  desired = resolved.content;
28053
28196
  } else if (seed.managedBlock) {
28054
28197
  const vars = withDerivedRepoVars({}, parseOwnerRepo(r.repo), repoClass);
28055
- const resolved = resolveSeedWriteContent(seed, vars, readSeedFile, content);
28198
+ const resolved = resolveSeedWriteContent(seed, vars, readSeedFile2, content);
28056
28199
  if (!resolved.ok) {
28057
28200
  return fail(`bootstrap propagate: ${r.repo} ${seed.target}: ${resolved.reason} \u2014 refusing to overwrite repo-owned content`);
28058
28201
  }
@@ -31891,8 +32034,7 @@ var LOOP_PLAYBOOKS = {
31891
32034
  { label: "Apply the repository test policy, then build the touched package", command: "mmi-cli tests policy --base origin/development && npm run build" },
31892
32035
  { label: "Publish the branch", command: "git push origin <branch>:<branch>" },
31893
32036
  { label: "Open the development-base PR", command: 'mmi-cli devops pr create --title "<title>" --body-file .jerv/PR_BODY.md --base development' },
31894
- { label: "Post the review verdict (pr merge / pr land refuse an unreviewed PR, #6255)", command: 'mmi-cli devops pr review-verdict <PR-number> --repo <owner/repo> --verdict PROCEED --scope "<one line>" --risk "<one line>" --reviewer <seat-or-model-id>' },
31895
- { label: "Land to development (waits for checks itself and demands the review verdict)", command: "mmi-cli devops pr land <PR-number>" },
32037
+ { label: "Land to development (waits for checks itself)", command: "mmi-cli devops pr land <PR-number>" },
31896
32038
  { label: "Release only after the gated train is authorized", command: "mmi-cli devops release --apply" },
31897
32039
  // #5552: learning-tagged filings are cloud-agent owned — file and return to the current task.
31898
32040
  { label: "Learning reports are fire-and-forget (file, then return to the current task \u2014 never claim/poll/duplicate the learning issue)", command: 'mmi-cli learning report --title "<one-line>" --body "<what hurt>"' }
@@ -31913,14 +32055,14 @@ var LOOP_PLAYBOOKS = {
31913
32055
  steps: [
31914
32056
  { label: "Publish the branch", command: "git push origin <branch>:<branch>" },
31915
32057
  { label: "Open the development-base PR", command: 'mmi-cli devops pr create --title "<title>" --body-file .jerv/PR_BODY.md --base development' },
31916
- { label: "Land the PR (merge to development \u2014 waits for checks itself and demands the review verdict)", command: "mmi-cli devops pr land <PR-number>" }
32058
+ { label: "Land the PR (merge to development \u2014 waits for checks itself)", command: "mmi-cli devops pr land <PR-number>" }
31917
32059
  ]
31918
32060
  },
31919
32061
  "hotfix": {
31920
32062
  title: "Hotfix",
31921
32063
  steps: [
31922
32064
  { label: "Create the main-base hotfix PR from every already-merged fix this cycle carries", command: "mmi-cli devops hotfix start --from <pr#|sha>[,<pr#|sha>...]" },
31923
- { label: "Merge the main-base PR (waits for checks itself and demands the review verdict)", command: "mmi-cli devops pr merge <PR-number> --squash" },
32065
+ { label: "Merge the main-base PR (waits for checks itself)", command: "mmi-cli devops pr merge <PR-number> --squash" },
31924
32066
  { label: "After the PR is merged, run the gated release", command: "mmi-cli devops hotfix release <vX.Y.Z> --carries <pr#|sha>[,<pr#|sha>...]" }
31925
32067
  ]
31926
32068
  }
@@ -32193,7 +32335,7 @@ function parseOriginRepo(remoteUrl) {
32193
32335
 
32194
32336
  // src/issue-commands.ts
32195
32337
  var import_node_fs31 = require("node:fs");
32196
- var import_node_crypto9 = require("node:crypto");
32338
+ var import_node_crypto10 = require("node:crypto");
32197
32339
 
32198
32340
  // src/issue-body.ts
32199
32341
  var import_node_os16 = require("node:os");
@@ -32723,7 +32865,7 @@ function rowIdempotencyKey(batchKey, spec) {
32723
32865
  const identity = `${spec.type}
32724
32866
  ${spec.title.trim()}
32725
32867
  ${spec.body ?? ""}`;
32726
- const hash = (0, import_node_crypto9.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
32868
+ const hash = (0, import_node_crypto10.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
32727
32869
  return `${batchKey}:${hash}`;
32728
32870
  }
32729
32871
  var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo", "surface"]);
@@ -35768,14 +35910,14 @@ function registerStageCommands(program3) {
35768
35910
  }
35769
35911
 
35770
35912
  // src/tenant-artifact.ts
35771
- var import_node_crypto10 = require("node:crypto");
35913
+ var import_node_crypto11 = require("node:crypto");
35772
35914
  var import_node_fs35 = require("node:fs");
35773
35915
  var import_promises6 = require("node:fs/promises");
35774
35916
  var import_node_path32 = require("node:path");
35775
35917
  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}$/;
35776
35918
  var MAX_BYTES = 5 * 1024 * 1024 * 1024;
35777
35919
  async function sha256File(path2) {
35778
- const hash = (0, import_node_crypto10.createHash)("sha256");
35920
+ const hash = (0, import_node_crypto11.createHash)("sha256");
35779
35921
  for await (const chunk of (0, import_node_fs35.createReadStream)(path2)) hash.update(chunk);
35780
35922
  return hash.digest("hex");
35781
35923
  }
@@ -36145,6 +36287,14 @@ async function runPrLand(prNumber, options, deps) {
36145
36287
  };
36146
36288
  }
36147
36289
  }
36290
+ const queue = await deps.queueMerge?.(prNumber, repo);
36291
+ if (queue) return {
36292
+ ...base,
36293
+ queue,
36294
+ status: queue.state === "merged" ? "merged" : "failed",
36295
+ mergeStatus: queue.state === "merged" ? "merged" : "failed",
36296
+ ...queue.state === "merged" ? {} : { error: queue.detail }
36297
+ };
36148
36298
  const ciPolicy = await deps.resolveCiPolicy(repo);
36149
36299
  base.ciPolicy = ciPolicy;
36150
36300
  const checksWaitError = (checksWait) => {
@@ -37454,9 +37604,287 @@ async function deleteMergedRemoteBranch(options) {
37454
37604
  };
37455
37605
  }
37456
37606
 
37457
- // src/post-merge-recon.ts
37607
+ // src/review-verdict.ts
37608
+ var import_node_child_process14 = require("node:child_process");
37458
37609
  var import_node_fs38 = require("node:fs");
37610
+ var import_node_os19 = require("node:os");
37459
37611
  var import_node_path35 = require("node:path");
37612
+ var REVIEW_VERDICT_MARKER = "<!-- zeroci-review v1 -->";
37613
+ var REVIEW_VERDICTS = ["PROCEED", "CORRECT", "ESCALATE"];
37614
+ function isReviewVerdict(value) {
37615
+ return typeof value === "string" && REVIEW_VERDICTS.includes(value);
37616
+ }
37617
+ function renderReviewVerdictComment(input) {
37618
+ const payload = {
37619
+ v: 1,
37620
+ patch: input.patch,
37621
+ head: input.head,
37622
+ verdict: input.verdict,
37623
+ scope: input.scope,
37624
+ risk: input.risk,
37625
+ unverified: input.unverified,
37626
+ reviewer: input.reviewer
37627
+ };
37628
+ const findings = input.findings?.trim();
37629
+ return `${REVIEW_VERDICT_MARKER}
37630
+ \`\`\`json
37631
+ ${JSON.stringify(payload, null, 2)}
37632
+ \`\`\`
37633
+ ${findings ? `
37634
+ ${findings}
37635
+ ` : ""}`;
37636
+ }
37637
+ function isReviewVerdictComment(body) {
37638
+ return body.trimStart().startsWith(REVIEW_VERDICT_MARKER);
37639
+ }
37640
+ var FENCE_RE = /^(?:`{3,}|~{3,})[^\n]*\n([\s\S]*?)\n(?:`{3,}|~{3,})\s*$/m;
37641
+ function parseReviewVerdictComment(body) {
37642
+ if (!isReviewVerdictComment(body)) return void 0;
37643
+ const rest = body.trimStart().slice(REVIEW_VERDICT_MARKER.length);
37644
+ const fence = FENCE_RE.exec(rest);
37645
+ if (!fence) return void 0;
37646
+ let parsed;
37647
+ try {
37648
+ parsed = JSON.parse(fence[1]);
37649
+ } catch {
37650
+ return void 0;
37651
+ }
37652
+ if (!parsed || typeof parsed !== "object") return void 0;
37653
+ const p = parsed;
37654
+ if (p.v !== 1 || !isReviewVerdict(p.verdict)) return void 0;
37655
+ if (typeof p.patch !== "string" || !/^[0-9a-f]{40}$/.test(p.patch)) return void 0;
37656
+ if (typeof p.head !== "string" || typeof p.scope !== "string" || typeof p.risk !== "string" || typeof p.reviewer !== "string") return void 0;
37657
+ const unverified = Array.isArray(p.unverified) ? p.unverified.filter((u) => typeof u === "string") : [];
37658
+ return { v: 1, patch: p.patch, head: p.head, verdict: p.verdict, scope: p.scope, risk: p.risk, unverified, reviewer: p.reviewer };
37659
+ }
37660
+ function latestReviewComment(comments) {
37661
+ let latest;
37662
+ for (const c of comments) {
37663
+ if (!isReviewVerdictComment(c.body)) continue;
37664
+ if (!latest || c.createdAt > latest.createdAt || c.createdAt === latest.createdAt && (c.id ?? 0) >= (latest.id ?? 0)) latest = c;
37665
+ }
37666
+ return latest;
37667
+ }
37668
+ function evaluateReviewVerdict(comments, currentPatchId) {
37669
+ const latest = latestReviewComment(comments);
37670
+ if (!latest) return { ok: false, reason: "none" };
37671
+ const payload = parseReviewVerdictComment(latest.body);
37672
+ if (!payload) return { ok: false, reason: "malformed" };
37673
+ if (payload.verdict !== "PROCEED") return { ok: false, reason: "not-proceed", verdict: payload.verdict, patch: payload.patch };
37674
+ if (payload.patch !== currentPatchId) return { ok: false, reason: "stale", verdict: payload.verdict, patch: payload.patch };
37675
+ return { ok: true, reason: "proceed", verdict: payload.verdict, patch: payload.patch };
37676
+ }
37677
+ function evaluateTrustedReviewVerdict(comments, patch, head) {
37678
+ const latest = latestReviewComment(comments);
37679
+ if (!latest) return { ok: false, reason: "none" };
37680
+ const identity = { commentId: latest.id, commentCreatedAt: latest.createdAt };
37681
+ if (latest.author?.toLowerCase() !== "jervaise") return { ...identity, ok: false, reason: "untrusted-author" };
37682
+ const evaluation = evaluateReviewVerdict([latest], patch);
37683
+ if (!evaluation.ok) return { ...identity, ok: false, reason: evaluation.reason };
37684
+ if (parseReviewVerdictComment(latest.body)?.head !== head) return { ...identity, ok: false, reason: "stale-head" };
37685
+ return { ...identity, ok: true, reason: "proceed" };
37686
+ }
37687
+ async function checkPrReview(number, repo, head, deps = {
37688
+ readHead: readPrHeadSha,
37689
+ readComments: readPrIssueComments,
37690
+ computePatch: computePrPatchId
37691
+ }) {
37692
+ const base = { repo, number, head };
37693
+ if (!/^[1-9][0-9]*$/.test(number) || !/^[\w.-]+\/[\w.-]+$/.test(repo) || !/^[0-9a-f]{40}$/.test(head)) {
37694
+ return { ...base, ok: false, reason: "invalid-input", detail: "expected a PR number, owner/repo and exact lowercase 40-character head SHA" };
37695
+ }
37696
+ try {
37697
+ if (await deps.readHead(number, repo) !== head) return { ...base, ok: false, reason: "stale-head" };
37698
+ const [comments, patch] = await Promise.all([deps.readComments(number, repo), deps.computePatch(number, repo)]);
37699
+ if (await deps.readHead(number, repo) !== head) return { ...base, ok: false, reason: "stale-head" };
37700
+ return { ...base, patch, ...evaluateTrustedReviewVerdict(comments, patch, head) };
37701
+ } catch (e) {
37702
+ return { ...base, ok: false, reason: "unreadable", detail: e.message };
37703
+ }
37704
+ }
37705
+ function computePrPatchId(number, repo) {
37706
+ return new Promise((resolve7, reject) => {
37707
+ const gh = (0, import_node_child_process14.spawn)("gh", ["pr", "diff", number, "--repo", repo], { windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
37708
+ const git3 = (0, import_node_child_process14.spawn)("git", ["patch-id", "--stable"], { windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
37709
+ let out = "";
37710
+ let ghErr = "";
37711
+ let gitErr = "";
37712
+ const timer = setTimeout(() => {
37713
+ gh.kill();
37714
+ git3.kill();
37715
+ reject(new Error(`patch-id: timed out after ${GC_GH_TIMEOUT_MS4}ms`));
37716
+ }, GC_GH_TIMEOUT_MS4);
37717
+ gh.stdout.pipe(git3.stdin);
37718
+ gh.stderr.on("data", (d) => {
37719
+ ghErr += d.toString();
37720
+ });
37721
+ git3.stderr.on("data", (d) => {
37722
+ gitErr += d.toString();
37723
+ });
37724
+ git3.stdout.on("data", (d) => {
37725
+ out += d.toString();
37726
+ });
37727
+ gh.on("error", (e) => {
37728
+ clearTimeout(timer);
37729
+ reject(e);
37730
+ });
37731
+ git3.on("error", (e) => {
37732
+ clearTimeout(timer);
37733
+ reject(e);
37734
+ });
37735
+ let ghCode;
37736
+ let gitCode;
37737
+ const finish = () => {
37738
+ if (ghCode === void 0 || gitCode === void 0) return;
37739
+ clearTimeout(timer);
37740
+ if (ghCode !== 0) return reject(Object.assign(new Error(`gh pr diff ${number} --repo ${repo} exited ${ghCode}: ${ghErr.trim()}`), { stderr: ghErr }));
37741
+ if (gitCode !== 0) return reject(new Error(`git patch-id --stable exited ${gitCode}: ${gitErr.trim()}`));
37742
+ const id = out.trim().split(/\s+/)[0];
37743
+ if (!/^[0-9a-f]{40}$/.test(id ?? "")) return reject(new Error(`patch-id: PR #${number} diff is empty or unreadable (gh: ${ghErr.trim() || "no stderr"})`));
37744
+ resolve7(id);
37745
+ };
37746
+ gh.on("close", (code) => {
37747
+ ghCode = code;
37748
+ finish();
37749
+ });
37750
+ git3.on("close", (code) => {
37751
+ gitCode = code;
37752
+ finish();
37753
+ });
37754
+ git3.stdin.on("error", (e) => {
37755
+ clearTimeout(timer);
37756
+ gh.kill();
37757
+ git3.kill();
37758
+ reject(e);
37759
+ });
37760
+ });
37761
+ }
37762
+ async function readPrHeadSha(number, repo) {
37763
+ const { stdout } = await execFileP("gh", ["api", `repos/${repo}/pulls/${number}`, "--jq", ".head.sha"], { timeout: GC_GH_TIMEOUT_MS4 });
37764
+ const sha = stdout.trim();
37765
+ if (!/^[0-9a-f]{40}$/.test(sha)) throw new Error(`could not read PR #${number} head sha`);
37766
+ return sha;
37767
+ }
37768
+ async function readPrIssueComments(number, repo) {
37769
+ const { stdout } = await execFileP("gh", ["api", "--paginate", `repos/${repo}/issues/${number}/comments?per_page=100`, "--jq", ".[] | {id, body, createdAt: .created_at, author: .user.login}"], { timeout: GC_GH_TIMEOUT_MS4 });
37770
+ const comments = parseNdjsonLines(stdout);
37771
+ if (comments.some((c) => !c || !Number.isSafeInteger(c.id) || c.id <= 0 || typeof c.body !== "string" || typeof c.createdAt !== "string" || !Number.isFinite(Date.parse(c.createdAt)) || c.author !== null && typeof c.author !== "string")) throw new Error("unreadable PR comment identity");
37772
+ return comments;
37773
+ }
37774
+ async function postPrCommentFromFile(number, repo, body) {
37775
+ const dir = (0, import_node_fs38.mkdtempSync)((0, import_node_path35.join)((0, import_node_os19.tmpdir)(), "mmi-review-verdict-"));
37776
+ const path2 = (0, import_node_path35.join)(dir, "body.md");
37777
+ try {
37778
+ (0, import_node_fs38.writeFileSync)(path2, body, "utf8");
37779
+ const { stdout } = await execFileP("gh", ["pr", "comment", number, "--repo", repo, "--body-file", path2], { timeout: GH_MUTATION_TIMEOUT_MS });
37780
+ return stdout.trim();
37781
+ } finally {
37782
+ try {
37783
+ (0, import_node_fs38.rmSync)(dir, { recursive: true, force: true });
37784
+ } catch {
37785
+ }
37786
+ }
37787
+ }
37788
+
37789
+ // src/pr-mergify-queue.ts
37790
+ var MERGIFY_PILOT_REPO = "mutmutco/Jerv-JervCode";
37791
+ var MERGIFY_PILOT_VARIABLE = "JERV_BATCH_QUEUE_PILOT";
37792
+ var MERGIFY_READY_LABEL = "ready-to-merge";
37793
+ async function ghJson(args) {
37794
+ return JSON.parse((await execFileP("gh", ["api", ...args], { timeout: GC_GH_TIMEOUT_MS4 })).stdout);
37795
+ }
37796
+ async function usesMergifyPilot(repo, base, readVariable = async () => ghJson([`repos/${repo}/actions/variables/${MERGIFY_PILOT_VARIABLE}`])) {
37797
+ if (repo.toLowerCase() !== MERGIFY_PILOT_REPO.toLowerCase() || base !== "development") return false;
37798
+ const variable = await readVariable();
37799
+ if (variable?.value === "true") return true;
37800
+ if (variable?.value === "false") return false;
37801
+ throw new Error(`Mergify pilot activation is missing or malformed on ${repo}; refusing native merge`);
37802
+ }
37803
+ function queueCheckState(checks, head) {
37804
+ const check = checks.filter((c) => c.name === "Mergify Merge Queue" && c.head_sha === head && c.app?.id === 10562 && c.app.slug === "mergify").sort((a, b) => b.id - a.id)[0];
37805
+ if (!check) return "refused";
37806
+ if (check.conclusion && !["success", "neutral"].includes(check.conclusion)) return "refused";
37807
+ if (["In merge queue", "Running merge queue checks"].includes(check.output?.title ?? "")) return "pending";
37808
+ return "requested";
37809
+ }
37810
+ async function requestMergifyMerge(number, repo, guardedText, expectedHead, deps = {
37811
+ readPull: () => ghJson([`repos/${repo}/pulls/${number}`]),
37812
+ readChecks: async (head) => parseNdjsonLines((await execFileP("gh", [
37813
+ "api",
37814
+ "--paginate",
37815
+ `repos/${repo}/commits/${head}/check-runs?per_page=100`,
37816
+ "--jq",
37817
+ ".check_runs[]"
37818
+ ], { timeout: GC_GH_TIMEOUT_MS4 })).stdout),
37819
+ addLabel: async () => {
37820
+ await ghJson([`repos/${repo}/issues/${number}/labels`, "--method", "POST", "-f", `labels[]=${MERGIFY_READY_LABEL}`]);
37821
+ },
37822
+ review: (head) => checkPrReview(number, repo, head),
37823
+ now: () => Date.now(),
37824
+ sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms))
37825
+ }, timeoutMs = 6e5) {
37826
+ let head = expectedHead ?? "";
37827
+ let state = "refused";
37828
+ const receipt = (detail) => ({ provider: "mergify", state, head, detail });
37829
+ try {
37830
+ if (repo.toLowerCase() !== MERGIFY_PILOT_REPO.toLowerCase()) return receipt("repository is outside the Mergify pilot");
37831
+ const initial = await deps.readPull();
37832
+ head = expectedHead ?? initial.head.sha;
37833
+ const sameTarget = (pr) => /^[0-9a-f]{40}$/.test(head) && pr.head.sha === head && pr.base.ref === "development";
37834
+ if (!sameTarget(initial)) return receipt("PR head or target base changed");
37835
+ if (initial.merged === true) {
37836
+ state = "merged";
37837
+ return receipt("GitHub confirms the PR merged");
37838
+ }
37839
+ const validate = (pr) => pr.state === "open" && sameTarget(pr) && `${pr.title}
37840
+ ${pr.body ?? ""}` === guardedText;
37841
+ if (!validate(initial)) return receipt("PR state, head, base or guarded title/body changed");
37842
+ const initialState = queueCheckState(await deps.readChecks(head), head);
37843
+ if (initialState === "refused") return receipt("trusted current-head Mergify check is absent or failed");
37844
+ if (!(await deps.review(head)).ok) return receipt("current trusted review verdict does not permit queue admission");
37845
+ if (!validate(await deps.readPull())) return receipt("PR changed before queue request");
37846
+ let mutationDetail = "";
37847
+ if (!initial.labels.some((label) => label.name === MERGIFY_READY_LABEL)) {
37848
+ try {
37849
+ await deps.addLabel();
37850
+ } catch {
37851
+ mutationDetail = "Label response was ambiguous; no replay was attempted. ";
37852
+ }
37853
+ }
37854
+ state = "requested";
37855
+ const deadline = deps.now() + timeoutMs;
37856
+ while (true) {
37857
+ const pr = await deps.readPull();
37858
+ if (!sameTarget(pr)) {
37859
+ state = "refused";
37860
+ return receipt("PR head or target base changed while queued");
37861
+ }
37862
+ if (pr.merged === true) {
37863
+ state = "merged";
37864
+ return receipt("GitHub confirms the PR merged");
37865
+ }
37866
+ if (!validate(pr)) {
37867
+ state = "refused";
37868
+ return receipt("PR state, head, base or guarded title/body changed while queued");
37869
+ }
37870
+ state = queueCheckState(await deps.readChecks(head), head);
37871
+ if (state === "refused") return receipt("trusted current-head Mergify check is absent or failed");
37872
+ if (!pr.labels.some((label) => label.name === MERGIFY_READY_LABEL)) {
37873
+ state = "refused";
37874
+ return receipt(`${mutationDetail}Queue request label is absent`);
37875
+ }
37876
+ if (deps.now() >= deadline) return receipt(`${mutationDetail}${state === "pending" ? "Mergify confirms queue admission" : "Queue requested; Mergify has not confirmed admission"}; PR has not merged within the wait window`);
37877
+ await deps.sleep(3e4);
37878
+ }
37879
+ } catch {
37880
+ state = "refused";
37881
+ return receipt("Mergify or GitHub state is unreadable; no native merge was attempted");
37882
+ }
37883
+ }
37884
+
37885
+ // src/post-merge-recon.ts
37886
+ var import_node_fs39 = require("node:fs");
37887
+ var import_node_path36 = require("node:path");
37460
37888
 
37461
37889
  // src/cross-repo-filing-issue.ts
37462
37890
  function crossRepoFilingRetryCommand(prRepo, prNumber) {
@@ -37622,16 +38050,16 @@ function buildPostMergeReconRecovery(input) {
37622
38050
  }
37623
38051
  function writePostMergeReconRecovery(cwd, recovery) {
37624
38052
  const path2 = postMergeReconStatePath(cwd, recovery.repo, recovery.pr);
37625
- (0, import_node_fs38.mkdirSync)((0, import_node_path35.dirname)(path2), { recursive: true });
37626
- (0, import_node_fs38.writeFileSync)(path2, `${JSON.stringify(recovery, null, 2)}
38053
+ (0, import_node_fs39.mkdirSync)((0, import_node_path36.dirname)(path2), { recursive: true });
38054
+ (0, import_node_fs39.writeFileSync)(path2, `${JSON.stringify(recovery, null, 2)}
37627
38055
  `, "utf8");
37628
38056
  return path2;
37629
38057
  }
37630
38058
  function clearPostMergeReconRecovery(cwd, repo, pr) {
37631
38059
  const path2 = postMergeReconStatePath(cwd, repo, pr);
37632
- if (!(0, import_node_fs38.existsSync)(path2)) return;
38060
+ if (!(0, import_node_fs39.existsSync)(path2)) return;
37633
38061
  try {
37634
- (0, import_node_fs38.unlinkSync)(path2);
38062
+ (0, import_node_fs39.unlinkSync)(path2);
37635
38063
  } catch {
37636
38064
  }
37637
38065
  }
@@ -37661,184 +38089,6 @@ function postMergeReconWarnings(input) {
37661
38089
  return lines2;
37662
38090
  }
37663
38091
 
37664
- // src/review-verdict.ts
37665
- var import_node_child_process14 = require("node:child_process");
37666
- var import_node_fs39 = require("node:fs");
37667
- var import_node_os19 = require("node:os");
37668
- var import_node_path36 = require("node:path");
37669
- var REVIEW_VERDICT_MARKER = "<!-- zeroci-review v1 -->";
37670
- var REVIEW_WAIVER_MARKER = "<!-- zeroci-review-waived v1 -->";
37671
- var REVIEW_VERDICTS = ["PROCEED", "CORRECT", "ESCALATE"];
37672
- function isReviewVerdict(value) {
37673
- return typeof value === "string" && REVIEW_VERDICTS.includes(value);
37674
- }
37675
- function renderReviewVerdictComment(input) {
37676
- const payload = {
37677
- v: 1,
37678
- patch: input.patch,
37679
- head: input.head,
37680
- verdict: input.verdict,
37681
- scope: input.scope,
37682
- risk: input.risk,
37683
- unverified: input.unverified,
37684
- reviewer: input.reviewer
37685
- };
37686
- const findings = input.findings?.trim();
37687
- return `${REVIEW_VERDICT_MARKER}
37688
- \`\`\`json
37689
- ${JSON.stringify(payload, null, 2)}
37690
- \`\`\`
37691
- ${findings ? `
37692
- ${findings}
37693
- ` : ""}`;
37694
- }
37695
- function isReviewVerdictComment(body) {
37696
- return body.trimStart().startsWith(REVIEW_VERDICT_MARKER);
37697
- }
37698
- var FENCE_RE = /^(?:`{3,}|~{3,})[^\n]*\n([\s\S]*?)\n(?:`{3,}|~{3,})\s*$/m;
37699
- function parseReviewVerdictComment(body) {
37700
- if (!isReviewVerdictComment(body)) return void 0;
37701
- const rest = body.trimStart().slice(REVIEW_VERDICT_MARKER.length);
37702
- const fence = FENCE_RE.exec(rest);
37703
- if (!fence) return void 0;
37704
- let parsed;
37705
- try {
37706
- parsed = JSON.parse(fence[1]);
37707
- } catch {
37708
- return void 0;
37709
- }
37710
- if (!parsed || typeof parsed !== "object") return void 0;
37711
- const p = parsed;
37712
- if (p.v !== 1 || !isReviewVerdict(p.verdict)) return void 0;
37713
- if (typeof p.patch !== "string" || !/^[0-9a-f]{40}$/.test(p.patch)) return void 0;
37714
- if (typeof p.head !== "string" || typeof p.scope !== "string" || typeof p.risk !== "string" || typeof p.reviewer !== "string") return void 0;
37715
- const unverified = Array.isArray(p.unverified) ? p.unverified.filter((u) => typeof u === "string") : [];
37716
- return { v: 1, patch: p.patch, head: p.head, verdict: p.verdict, scope: p.scope, risk: p.risk, unverified, reviewer: p.reviewer };
37717
- }
37718
- function evaluateReviewVerdict(comments, currentPatchId) {
37719
- let latest;
37720
- for (const c of comments) {
37721
- if (!isReviewVerdictComment(c.body)) continue;
37722
- if (!latest || c.createdAt >= latest.createdAt) latest = c;
37723
- }
37724
- if (!latest) return { ok: false, reason: "none" };
37725
- const payload = parseReviewVerdictComment(latest.body);
37726
- if (!payload) return { ok: false, reason: "malformed" };
37727
- if (payload.verdict !== "PROCEED") return { ok: false, reason: "not-proceed", verdict: payload.verdict, patch: payload.patch };
37728
- if (payload.patch !== currentPatchId) return { ok: false, reason: "stale", verdict: payload.verdict, patch: payload.patch };
37729
- return { ok: true, reason: "proceed", verdict: payload.verdict, patch: payload.patch };
37730
- }
37731
- function reviewVerdictRemedyCommand(number, repo) {
37732
- return `mmi-cli devops pr review-verdict ${number} --repo ${repo} --verdict PROCEED --scope "<one line>" --risk "<one line>" --reviewer <seat-or-model-id>`;
37733
- }
37734
- function reviewVerdictRefusalMessage(context, number, repo, evaluation, currentPatchId) {
37735
- const why = {
37736
- none: "no review verdict comment on the PR",
37737
- stale: `the latest verdict is bound to patch ${evaluation.patch} but the PR's current diff is ${currentPatchId} \u2014 the content changed since the review`,
37738
- "not-proceed": `the latest verdict is ${evaluation.verdict}, not PROCEED`,
37739
- malformed: "the latest verdict comment is malformed (marker present, JSON block unreadable or wrong shape)",
37740
- proceed: "proceed"
37741
- }[evaluation.reason];
37742
- return `${context}: REVIEW VERDICT REQUIRED (#6255) \u2014 ${why}. Review PR #${number} and post the verdict, then rerun:
37743
- ${reviewVerdictRemedyCommand(number, repo)}
37744
- To land without a review, pass --without-review "<reason>" \u2014 it posts a waiver comment on the PR.`;
37745
- }
37746
- function renderReviewWaiverComment(reason, actor) {
37747
- return `${REVIEW_WAIVER_MARKER}
37748
- Review gate waived by @${actor}: ${reason.trim()}
37749
- `;
37750
- }
37751
- function computePrPatchId(number, repo) {
37752
- return new Promise((resolve7, reject) => {
37753
- const gh = (0, import_node_child_process14.spawn)("gh", ["pr", "diff", number, "--repo", repo], { windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
37754
- const git3 = (0, import_node_child_process14.spawn)("git", ["patch-id", "--stable"], { windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
37755
- let out = "";
37756
- let ghErr = "";
37757
- let gitErr = "";
37758
- const timer = setTimeout(() => {
37759
- gh.kill();
37760
- git3.kill();
37761
- reject(new Error(`patch-id: timed out after ${GC_GH_TIMEOUT_MS4}ms`));
37762
- }, GC_GH_TIMEOUT_MS4);
37763
- gh.stdout.pipe(git3.stdin);
37764
- gh.stderr.on("data", (d) => {
37765
- ghErr += d.toString();
37766
- });
37767
- git3.stderr.on("data", (d) => {
37768
- gitErr += d.toString();
37769
- });
37770
- git3.stdout.on("data", (d) => {
37771
- out += d.toString();
37772
- });
37773
- gh.on("error", (e) => {
37774
- clearTimeout(timer);
37775
- reject(e);
37776
- });
37777
- git3.on("error", (e) => {
37778
- clearTimeout(timer);
37779
- reject(e);
37780
- });
37781
- let ghCode;
37782
- gh.on("close", (code) => {
37783
- ghCode = code;
37784
- });
37785
- git3.on("close", (code) => {
37786
- clearTimeout(timer);
37787
- if (ghCode) return reject(Object.assign(new Error(`gh pr diff ${number} --repo ${repo} exited ${ghCode}: ${ghErr.trim()}`), { stderr: ghErr }));
37788
- if (code) return reject(new Error(`git patch-id --stable exited ${code}: ${gitErr.trim()}`));
37789
- const id = out.trim().split(/\s+/)[0];
37790
- if (!/^[0-9a-f]{40}$/.test(id ?? "")) return reject(new Error(`patch-id: PR #${number} diff is empty or unreadable (gh: ${ghErr.trim() || "no stderr"})`));
37791
- resolve7(id);
37792
- });
37793
- });
37794
- }
37795
- async function readPrHeadSha(number, repo) {
37796
- const { stdout } = await execFileP("gh", ["api", `repos/${repo}/pulls/${number}`, "--jq", ".head.sha"], { timeout: GC_GH_TIMEOUT_MS4 });
37797
- const sha = stdout.trim();
37798
- if (!/^[0-9a-f]{40}$/.test(sha)) throw new Error(`could not read PR #${number} head sha`);
37799
- return sha;
37800
- }
37801
- async function readPrIssueComments(number, repo) {
37802
- const { stdout } = await execFileP("gh", ["api", "--paginate", `repos/${repo}/issues/${number}/comments?per_page=100`, "--jq", ".[] | {body, createdAt: .created_at}"], { timeout: GC_GH_TIMEOUT_MS4 });
37803
- return parseNdjsonLines(stdout);
37804
- }
37805
- async function postPrCommentFromFile(number, repo, body) {
37806
- const dir = (0, import_node_fs39.mkdtempSync)((0, import_node_path36.join)((0, import_node_os19.tmpdir)(), "mmi-review-verdict-"));
37807
- const path2 = (0, import_node_path36.join)(dir, "body.md");
37808
- try {
37809
- (0, import_node_fs39.writeFileSync)(path2, body, "utf8");
37810
- const { stdout } = await execFileP("gh", ["pr", "comment", number, "--repo", repo, "--body-file", path2], { timeout: GH_MUTATION_TIMEOUT_MS });
37811
- return stdout.trim();
37812
- } finally {
37813
- try {
37814
- (0, import_node_fs39.rmSync)(dir, { recursive: true, force: true });
37815
- } catch {
37816
- }
37817
- }
37818
- }
37819
- async function requireReviewVerdict(context, number, repo, opts) {
37820
- if (opts.withoutReview !== void 0) {
37821
- const reason = opts.withoutReview.trim();
37822
- if (!reason) throw new Error(`${context}: --without-review needs a non-empty reason`);
37823
- const actor = await execFileP("gh", ["api", "user", "--jq", ".login"], { timeout: GC_GH_TIMEOUT_MS4 }).then((r) => r.stdout.trim()).catch(() => "") || "unknown";
37824
- await postPrCommentFromFile(number, repo, renderReviewWaiverComment(reason, actor));
37825
- console.warn(`${context}: review gate WAIVED by @${actor} \u2014 ${reason} (waiver comment posted on PR #${number})`);
37826
- return void 0;
37827
- }
37828
- let comments;
37829
- let currentPatchId;
37830
- try {
37831
- [comments, currentPatchId] = await Promise.all([readPrIssueComments(number, repo), computePrPatchId(number, repo)]);
37832
- } catch (e) {
37833
- const detail = e.message || String(e);
37834
- const limited = isGitHubRateLimitError(e) ? " (gh rate-limited \u2014 retry after the pool resets)" : "";
37835
- throw new Error(`${context}: cannot read PR #${number}'s review verdict${limited} \u2014 ${detail}. Refusing to merge WITHOUT the review gate (#6255).`);
37836
- }
37837
- const evaluation = evaluateReviewVerdict(comments, currentPatchId);
37838
- if (evaluation.ok) return void 0;
37839
- return reviewVerdictRefusalMessage(context, number, repo, evaluation, currentPatchId);
37840
- }
37841
-
37842
38092
  // src/pr-create-docs-check.ts
37843
38093
  var import_node_child_process15 = require("node:child_process");
37844
38094
  var GIT_TIMEOUT_MS2 = 15e3;
@@ -38963,7 +39213,7 @@ function ciAuditDeps() {
38963
39213
  }
38964
39214
  };
38965
39215
  }
38966
- async function ghJson(args, timeout = 1e4) {
39216
+ async function ghJson2(args, timeout = 1e4) {
38967
39217
  const { stdout } = await execFileP("gh", args, { timeout });
38968
39218
  return JSON.parse(stdout);
38969
39219
  }
@@ -39250,7 +39500,7 @@ function registerCollaborationCommands(program3) {
39250
39500
  async function readParentField(number, repo) {
39251
39501
  let payload;
39252
39502
  try {
39253
- payload = await ghJson(["api", `repos/${repo}/issues/${number}`]);
39503
+ payload = await ghJson2(["api", `repos/${repo}/issues/${number}`]);
39254
39504
  } catch (e) {
39255
39505
  const err = e;
39256
39506
  return { parentReadError: (err.stderr || err.message || String(e)).trim() };
@@ -39282,7 +39532,7 @@ function registerCollaborationCommands(program3) {
39282
39532
  emit(data2, await readParentField(n, repo));
39283
39533
  return;
39284
39534
  }
39285
- const data = await ghJson(["issue", "view", String(n), "--repo", repo, "--json", gh.ghFields]);
39535
+ const data = await ghJson2(["issue", "view", String(n), "--repo", repo, "--json", gh.ghFields]);
39286
39536
  emit(data, await readParentField(n, repo));
39287
39537
  } catch (e) {
39288
39538
  const err = e;
@@ -39297,7 +39547,7 @@ function registerCollaborationCommands(program3) {
39297
39547
  const repo = await resolveRepo(o.repo);
39298
39548
  if (!repo) return fail("issue discover-related: could not resolve repo");
39299
39549
  try {
39300
- const issues = await ghJson([
39550
+ const issues = await ghJson2([
39301
39551
  "issue",
39302
39552
  "list",
39303
39553
  "--repo",
@@ -39312,7 +39562,7 @@ function registerCollaborationCommands(program3) {
39312
39562
  const candidates = findRelatedIssues({ number, title: o.title, body: o.body }, issues);
39313
39563
  if (o.json) return console.log(JSON.stringify({ number, repo, candidates }, null, 2));
39314
39564
  if (!candidates.length) return;
39315
- const viewed = await ghJson([
39565
+ const viewed = await ghJson2([
39316
39566
  "issue",
39317
39567
  "view",
39318
39568
  String(number),
@@ -39386,7 +39636,7 @@ function registerCollaborationCommands(program3) {
39386
39636
  const checked = o.off !== true;
39387
39637
  let body;
39388
39638
  try {
39389
- const viewed = await ghJson(["issue", "view", String(parsed.number), "--repo", repo, "--json", "body"]);
39639
+ const viewed = await ghJson2(["issue", "view", String(parsed.number), "--repo", repo, "--json", "body"]);
39390
39640
  body = viewed.body ?? "";
39391
39641
  } catch (e) {
39392
39642
  return fail(`issue check: could not read ${repo}#${parsed.number}: ${e.message}`);
@@ -39488,7 +39738,7 @@ ${list}`);
39488
39738
  if (!o.force) {
39489
39739
  let openLessons = [];
39490
39740
  try {
39491
- openLessons = await ghJson([
39741
+ openLessons = await ghJson2([
39492
39742
  "issue",
39493
39743
  "list",
39494
39744
  "--repo",
@@ -39534,7 +39784,7 @@ ${list}`);
39534
39784
  console.log(JSON.stringify({ ...created, projectItemId, onBoard }));
39535
39785
  });
39536
39786
  const pr = program3.command("pr").description("pull requests \u2014 reliable create with structured output");
39537
- withExamples(pr.command("create").description("create a PR and print {number,url} JSON").option("--title <title>", "PR title").option("--title-file <path|->", "read the PR title from a UTF-8 file, or from stdin with -").option("--body <body>", "PR body (markdown)").option("--body-file <path|->", "read PR body from a UTF-8 file, or from stdin with -").option("--base <branch>", "base branch (defaults to the repo default)").option("--head <branch>", "head branch (defaults to the current branch)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--draft", "open the PR in draft state (#2667)").option("--json", "machine-readable output (default; accepted for parity)").action(async (o) => {
39787
+ withExamples(pr.command("create").description("create a PR and print {number,url} JSON").option("--title <title>", "PR title").option("--title-file <path|->", "read the PR title from a UTF-8 file, or from stdin with -").option("--body <body>", "PR body (markdown)").option("--body-file <path|->", "read PR body from a UTF-8 file, or from stdin with -").option("--base <branch>", "base branch (defaults to the repo default)").option("--head <branch>", "head branch (defaults to the current branch)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--draft", "open the PR in draft state (#2667)").option("--no-zeroci", "compatibility option; pr create does not mint receipts").option("--json", "machine-readable output (default; accepted for parity)").action(async (o) => {
39538
39788
  let body;
39539
39789
  let title;
39540
39790
  try {
@@ -39594,7 +39844,7 @@ ${list}`);
39594
39844
  console.log(JSON.stringify(data2));
39595
39845
  return;
39596
39846
  }
39597
- const data = await ghJson(["pr", "view", String(n), "--repo", repo, "--json", effective]);
39847
+ const data = await ghJson2(["pr", "view", String(n), "--repo", repo, "--json", effective]);
39598
39848
  console.log(JSON.stringify(data));
39599
39849
  } catch (e) {
39600
39850
  const err = e;
@@ -39794,7 +40044,12 @@ ${list}`);
39794
40044
  if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
39795
40045
  if (result.status === "timeout" || result.status === "rate-limited") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
39796
40046
  });
39797
- jsonParity(pr.command("review-verdict <number>").description("post the ZeroCI review verdict comment the merge helper requires (#6255): computes the PR diff patch-id and head sha itself, renders the v1 contract body, posts it as a PR comment. Only a PROCEED bound to the CURRENT diff lets `pr merge` / `pr land` merge").requiredOption("--repo <owner/repo>", "target repo").requiredOption("--verdict <verdict>", `one of ${REVIEW_VERDICTS.join("|")}`).requiredOption("--scope <text>", "one line: what was reviewed").requiredOption("--risk <text>", "one line: the residual risk").option("--unverified <text>", "a claim the reviewer could not verify (repeatable)", (v, acc) => [...acc, v], []).requiredOption("--reviewer <id>", "reviewer seat or model id").option("--findings-file <path>", "Markdown findings appended after the JSON block")).action(async (number, o) => {
40047
+ pr.command("review-check <number>").description("read-only trusted review verdict check for an exact PR head").requiredOption("--repo <owner/repo>", "target repository").requiredOption("--head <sha>", "exact expected head SHA").option("--json", "machine-readable output").action(async (number, o) => {
40048
+ const result = await checkPrReview(number, o.repo, o.head);
40049
+ console.log(o.json ? JSON.stringify(result) : `pr review-check: ${result.reason}`);
40050
+ if (!result.ok) process.exitCode = 1;
40051
+ });
40052
+ jsonParity(pr.command("review-verdict <number>").description("post an explicit review verdict comment: computes the PR diff patch-id and head sha, renders the v1 contract body, and posts it as a PR comment").requiredOption("--repo <owner/repo>", "target repo").requiredOption("--verdict <verdict>", `one of ${REVIEW_VERDICTS.join("|")}`).requiredOption("--scope <text>", "one line: what was reviewed").requiredOption("--risk <text>", "one line: the residual risk").option("--unverified <text>", "a claim the reviewer could not verify (repeatable)", (v, acc) => [...acc, v], []).requiredOption("--reviewer <id>", "reviewer seat or model id").option("--findings-file <path>", "Markdown findings appended after the JSON block")).action(async (number, o) => {
39798
40053
  if (!isReviewVerdict(o.verdict)) return fail(`pr review-verdict: --verdict must be one of ${REVIEW_VERDICTS.join("|")} (got ${o.verdict})`);
39799
40054
  const verdict = o.verdict;
39800
40055
  const findings = o.findingsFile ? (0, import_node_fs43.readFileSync)(o.findingsFile, "utf8") : void 0;
@@ -39836,7 +40091,7 @@ ${list}`);
39836
40091
  }
39837
40092
  class PrHeadBehindBaseError extends Error {
39838
40093
  }
39839
- pr.command("land <number>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "acknowledge and land past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "land without a review verdict (#6255): posts a `zeroci-review-waived` comment carrying the reason and actor, then proceeds").action(async (number, o) => {
40094
+ pr.command("land <number>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--force", "acknowledge and land past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict").action(async (number, o) => {
39840
40095
  if (/^(?:[^/]+\/[^/]+)?#\d+$/.test(number.trim())) {
39841
40096
  try {
39842
40097
  const parsed = parseIssueRef(number, o.repo);
@@ -39854,7 +40109,7 @@ ${list}`);
39854
40109
  await readClosingGuardInput(number, repoArgs, landRepoForGuard, "pr land"),
39855
40110
  async (n) => {
39856
40111
  if (!landRepoForGuard) return void 0;
39857
- const viewed = await ghJson(["issue", "view", String(n), "--repo", landRepoForGuard, "--json", "state"]);
40112
+ const viewed = await ghJson2(["issue", "view", String(n), "--repo", landRepoForGuard, "--json", "state"]);
39858
40113
  return typeof viewed.state === "string" ? viewed.state : void 0;
39859
40114
  }
39860
40115
  );
@@ -39871,16 +40126,15 @@ ${list}`);
39871
40126
  return;
39872
40127
  }
39873
40128
  if (landClosingGuardVerdict.message) console.warn(landClosingGuardVerdict.message);
39874
- {
39875
- const gateRepo = landRepoForGuard ?? await requireRepo(o.repo);
39876
- const refusal = await requireReviewVerdict("pr land", number, gateRepo, { withoutReview: o.withoutReview });
39877
- if (refusal) {
39878
- console.error(refusal);
39879
- process.exitCode = 1;
39880
- return;
39881
- }
39882
- }
39883
40129
  const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false }, {
40130
+ queueMerge: async (prNumber, repo) => {
40131
+ const meta = await ghJson2(["pr", "view", prNumber, "--repo", repo, "--json", "baseRefName,headRefOid"]);
40132
+ if (!await usesMergifyPilot(repo, meta.baseRefName)) return void 0;
40133
+ if (!landClosingGuardInput) throw new Error("pr land: Mergify requires a readable closing-keyword guard");
40134
+ const guard = evaluateClosingGuard(landClosingGuardInput, { force: o.force, context: "pr land", squashBodyText: landClosingGuardInput.text });
40135
+ if (guard.blocked) throw new Error(guard.message);
40136
+ return requestMergifyMerge(prNumber, repo, landClosingGuardInput.text, meta.headRefOid);
40137
+ },
39884
40138
  resolveRepo: async (prNumber, repoOpt) => {
39885
40139
  const args = repoOpt ? ["--repo", repoOpt] : repoArgs;
39886
40140
  const viewed = (await execFileP("gh", ["pr", "view", prNumber, ...args, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS4 })).stdout.trim();
@@ -40015,7 +40269,7 @@ ${list}`);
40015
40269
  else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
40016
40270
  if (result.status === "failed") process.exitCode = 1;
40017
40271
  });
40018
- jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo); from a foreign checkout the remote probe/delete address this repo and local cleanup runs only in the verified sibling checkout ../<repo>, else localBranch reports skipped-foreign-cwd and the receipt carries foreignCwd: true (#6148)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").addOption(new Option("--disable-auto", "disable a queued auto-merge without merging").conflicts(["auto", "wait", "squash", "merge", "rebase", "preserveWorktree", "gc", "squashBodyFile", "force"])).option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m) \u2014 run as a background/monitor task or under a shell timeout above that budget; a short foreground timeout (e.g. 120s) kills it after checks pass and leaves the PR open (#6027)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888); also the merge settings-proof bypass when delete_branch_on_merge cannot be proven (#6210)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--squash-body-file <path>", "squash commit body (overrides GitHub COMMIT_MESSAGES); use when a pushed commit mentions close/fix/resolve + #N that must not close (#5723)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "merge without a review verdict (#6255): posts a `zeroci-review-waived` comment carrying the reason and actor, then proceeds")).action(async (number, o) => {
40272
+ jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo); from a foreign checkout the remote probe/delete address this repo and local cleanup runs only in the verified sibling checkout ../<repo>, else localBranch reports skipped-foreign-cwd and the receipt carries foreignCwd: true (#6148)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").addOption(new Option("--disable-auto", "disable a queued auto-merge without merging").conflicts(["auto", "wait", "squash", "merge", "rebase", "preserveWorktree", "gc", "squashBodyFile", "force"])).option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m) \u2014 run as a background/monitor task or under a shell timeout above that budget; a short foreground timeout (e.g. 120s) kills it after checks pass and leaves the PR open (#6027)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888); also the merge settings-proof bypass when delete_branch_on_merge cannot be proven (#6210)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--squash-body-file <path>", "squash commit body (overrides GitHub COMMIT_MESSAGES); use when a pushed commit mentions close/fix/resolve + #N that must not close (#5723)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").option("--without-review <reason>", "compatibility option; shared merge helpers do not require a review verdict")).action(async (number, o) => {
40019
40273
  const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
40020
40274
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
40021
40275
  if (o.disableAuto) {
@@ -40037,20 +40291,24 @@ ${list}`);
40037
40291
  const headRef = prMeta.head;
40038
40292
  const baseRef = prMeta.base;
40039
40293
  const headRefOid = (prMeta.oid ?? "").trim() || void 0;
40294
+ if (!repoForPostCleanup) throw new Error("pr merge: cannot resolve target repository");
40295
+ const mergifyPilot = await usesMergifyPilot(repoForPostCleanup, baseRef);
40296
+ if (mergifyPilot && (method !== "--squash" || o.squashBodyFile)) throw new Error("pr merge: Mergify pilot uses the protected squash title/body policy; custom merge methods and body files are unsupported");
40040
40297
  const devDeployDeps = repoForPostCleanup ? registryClientDeps(await loadConfig()) : void 0;
40041
40298
  const devDeployPlan = repoForPostCleanup && devDeployDeps ? await planDevDeployOnDevelopmentMerge(repoForPostCleanup, baseRef, devDeployDeps).catch(() => ({ applicable: false, reason: "unreadable" })) : { applicable: false, reason: "unreadable" };
40042
40299
  const closingGuardInput = await withAlreadyClosedCommitTargets(
40043
40300
  await readClosingGuardInput(number, repoArgs, repoForPostCleanup, "pr merge"),
40044
40301
  async (n) => {
40045
40302
  if (!repoForPostCleanup) return void 0;
40046
- const viewed = await ghJson(["issue", "view", String(n), "--repo", repoForPostCleanup, "--json", "state"]);
40303
+ const viewed = await ghJson2(["issue", "view", String(n), "--repo", repoForPostCleanup, "--json", "state"]);
40047
40304
  return typeof viewed.state === "string" ? viewed.state : void 0;
40048
40305
  }
40049
40306
  );
40050
40307
  if (o.squashBodyFile && method !== "--squash") {
40051
40308
  throw new Error("pr merge: --squash-body-file applies only to squash merges");
40052
40309
  }
40053
- const mergeSquashBody = squashBodyTextForMerge(
40310
+ if (mergifyPilot && !closingGuardInput) throw new Error("pr merge: Mergify requires a readable closing-keyword guard");
40311
+ const mergeSquashBody = mergifyPilot ? closingGuardInput.text : squashBodyTextForMerge(
40054
40312
  closingGuardInput,
40055
40313
  method === "--squash",
40056
40314
  o.squashBodyFile ? (0, import_node_fs43.readFileSync)(o.squashBodyFile, "utf8") : void 0
@@ -40067,15 +40325,6 @@ ${list}`);
40067
40325
  return;
40068
40326
  }
40069
40327
  if (closingGuardVerdict.message) console.warn(closingGuardVerdict.message);
40070
- if (prMeta.state !== "MERGED") {
40071
- const gateRepo = repoForPostCleanup ?? await requireRepo(o.repo);
40072
- const refusal = await requireReviewVerdict("pr merge", number, gateRepo, { withoutReview: o.withoutReview });
40073
- if (refusal) {
40074
- console.error(refusal);
40075
- process.exitCode = 1;
40076
- return;
40077
- }
40078
- }
40079
40328
  if (prMeta.state !== "MERGED" && !o.preserveWorktree) {
40080
40329
  if (!repoForPostCleanup) throw new Error("pr merge: repository is unreadable; cannot prove remote branch preservation");
40081
40330
  const repoSettings = await fetchRestRepoMergeSettings(repoForPostCleanup);
@@ -40157,7 +40406,7 @@ ${list}`);
40157
40406
  }
40158
40407
  return true;
40159
40408
  };
40160
- if (o.wait && !await runWaitGate()) return;
40409
+ if (!mergifyPilot && o.wait && !await runWaitGate()) return;
40161
40410
  if (ciPolicy.policy === "no-ci") {
40162
40411
  const guard = decidePrMergeNoCiGuard(await pollGhPrChecks(number, repoArgs), ciPolicy.reason);
40163
40412
  if (guard.action === "refuse") throw new Error(`gh pr merge ${number}: ${guard.message}`);
@@ -40166,7 +40415,7 @@ ${list}`);
40166
40415
  const remoteBefore = await remoteBranchExists2(headRef, { remote });
40167
40416
  let upgradedToAuto = false;
40168
40417
  let remoteNotAttemptedReason = "preserved-delayed-cleanup";
40169
- const overrideBody = mergeSquashBody ? { ...writeSquashBodyFile(mergeSquashBody), text: mergeSquashBody } : await composeOverrideBodyFile(
40418
+ const overrideBody = mergifyPilot ? void 0 : mergeSquashBody ? { ...writeSquashBodyFile(mergeSquashBody), text: mergeSquashBody } : await composeOverrideBodyFile(
40170
40419
  number,
40171
40420
  repoArgs,
40172
40421
  async (a, t) => (await execFileP("gh", a, { timeout: t })).stdout,
@@ -40246,7 +40495,15 @@ ${list}`);
40246
40495
  });
40247
40496
  try {
40248
40497
  try {
40249
- await mergeOnce();
40498
+ if (mergifyPilot) {
40499
+ const queue = await requestMergifyMerge(number, repoForPostCleanup, closingGuardInput.text, headRefOid);
40500
+ if (queue.state !== "merged") {
40501
+ console.log(JSON.stringify({ mergeStatus: "failed", pr: number, repo: repoForPostCleanup, queue }));
40502
+ process.exitCode = queue.state === "refused" ? 1 : PR_CHECKS_TIMEOUT_EXIT_CODE;
40503
+ return;
40504
+ }
40505
+ remoteNotAttemptedReason = "pr-already-merged";
40506
+ } else await mergeOnce();
40250
40507
  } catch (e) {
40251
40508
  if (!(e instanceof PrHeadBehindBaseError)) throw e;
40252
40509
  const localCheckedOut = !foreignCwd && await prHeadCheckedOutHere(headRef, targetRepo2, Boolean(o.repo));
@@ -40718,13 +40975,13 @@ ${SSH_RECIPE_AGENT_NOTE}`);
40718
40975
 
40719
40976
  // src/dist-drift.ts
40720
40977
  var import_node_child_process17 = require("node:child_process");
40721
- var import_node_crypto12 = require("node:crypto");
40978
+ var import_node_crypto13 = require("node:crypto");
40722
40979
  var import_node_fs46 = require("node:fs");
40723
40980
  var import_node_os21 = require("node:os");
40724
40981
  var import_node_path42 = require("node:path");
40725
40982
 
40726
40983
  // ../scripts/distribution-digest.mjs
40727
- var import_node_crypto11 = require("node:crypto");
40984
+ var import_node_crypto12 = require("node:crypto");
40728
40985
  var import_node_fs45 = require("node:fs");
40729
40986
  var import_node_path41 = require("node:path");
40730
40987
  var slash = (value) => value.replaceAll("\\", "/");
@@ -40737,7 +40994,7 @@ function repoPath(root, declaredPath, label) {
40737
40994
  return target;
40738
40995
  }
40739
40996
  function digestFiles(files) {
40740
- const hash = (0, import_node_crypto11.createHash)("sha256");
40997
+ const hash = (0, import_node_crypto12.createHash)("sha256");
40741
40998
  for (const file of [...files].sort((a, b) => a.relative.localeCompare(b.relative))) {
40742
40999
  const content = file.stat.isSymbolicLink() ? Buffer.from((0, import_node_fs45.readlinkSync)(file.absolute), "utf8") : (0, import_node_fs45.readFileSync)(file.absolute);
40743
41000
  hash.update(file.relative, "utf8");
@@ -40767,7 +41024,7 @@ var DIST_ARTIFACTS = [
40767
41024
  ];
40768
41025
  var BOM_DIST_TREE_ID = "mmi-cli-dist";
40769
41026
  var ABSENT = "absent";
40770
- var sha256 = (bytes) => `sha256:${(0, import_node_crypto12.createHash)("sha256").update(bytes).digest("hex")}`;
41027
+ var sha256 = (bytes) => `sha256:${(0, import_node_crypto13.createHash)("sha256").update(bytes).digest("hex")}`;
40771
41028
  function artifactDrift(path2, committedBytes, rebuiltBytes) {
40772
41029
  const committed = committedBytes ? sha256(committedBytes) : ABSENT;
40773
41030
  const rebuiltExpected = rebuiltBytes ? sha256(rebuiltBytes) : ABSENT;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.3.19",
3
+ "version": "4.3.21",
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",