@mutmutco/cli 4.5.103 → 4.5.105

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 +442 -322
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -4747,7 +4747,7 @@ function useColor() {
4747
4747
  var program = new Command();
4748
4748
 
4749
4749
  // src/command-composition.ts
4750
- var import_node_fs54 = require("node:fs");
4750
+ var import_node_fs55 = require("node:fs");
4751
4751
  init_clean_exit();
4752
4752
  init_cli_shared();
4753
4753
 
@@ -14908,7 +14908,7 @@ async function readDefaultBranchGate(repo, client, baseBranch, now = Date.now(),
14908
14908
  async function gateIsProvenGreen(repo, client, baseBranch, gateFiles = DEFAULT_GATE_FILES) {
14909
14909
  return (await readDefaultBranchGate(repo, client, baseBranch, Date.now(), gateFiles)).state === "green";
14910
14910
  }
14911
- async function activateProductRuleset(repo, rulesetBody, client, enforcement = "active") {
14911
+ async function activateProductRuleset(repo, rulesetBody, client, enforcement = "active", dryRun = false) {
14912
14912
  const want = new Set(rulesetRequiredContexts({ rules: rulesetBody.rules }));
14913
14913
  const wantStrict = rulesetStrictPolicy({ rules: rulesetBody.rules });
14914
14914
  const wantBranches = rulesetBranchIncludes(rulesetBody);
@@ -14927,10 +14927,10 @@ async function activateProductRuleset(repo, rulesetBody, client, enforcement = "
14927
14927
  if (detail.enforcement === effective && branchesMatch && strictMatch && have.size === want.size && [...want].every((c) => have.has(c))) {
14928
14928
  return { action: "skipped", enforcement: effective, detail: `${effective} ruleset already matches required contexts, branches and up-to-date policy${kept}` };
14929
14929
  }
14930
- await client.rest("PUT", `repos/${repo}/rulesets/${existing.id}`, { body: { ...rulesetBody, enforcement: effective }, timeoutMs: 2e4 });
14930
+ if (!dryRun) await client.rest("PUT", `repos/${repo}/rulesets/${existing.id}`, { body: { ...rulesetBody, enforcement: effective }, timeoutMs: 2e4 });
14931
14931
  return { action: "updated", enforcement: effective, detail: `ruleset ${existing.id}${kept}` };
14932
14932
  }
14933
- await client.rest("POST", `repos/${repo}/rulesets`, { body: { ...rulesetBody, enforcement }, timeoutMs: 2e4 });
14933
+ if (!dryRun) await client.rest("POST", `repos/${repo}/rulesets`, { body: { ...rulesetBody, enforcement }, timeoutMs: 2e4 });
14934
14934
  return { action: "created", enforcement };
14935
14935
  }
14936
14936
  async function gateContextEnforced(repo, rest) {
@@ -16383,7 +16383,7 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
16383
16383
  suite,
16384
16384
  ...suite === "mm-work" && deployModel === "tenant-container" ? {
16385
16385
  requiredBuildSecrets: ["NODE_AUTH_TOKEN=@github-packages-token"],
16386
- requiredChecks: ["gate", "screens / screens"]
16386
+ requiredChecks: options.existing?.requiredChecks ?? ["gate"]
16387
16387
  } : {},
16388
16388
  repos: [`${parsedRepo.owner}/${slug}`],
16389
16389
  branch: vars.BRANCH || (cls === "content" ? "main" : "development"),
@@ -17699,10 +17699,10 @@ var rollout_plan_default = {
17699
17699
  note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
17700
17700
  },
17701
17701
  baseline: {
17702
- version: "4.5.103",
17703
- tag: "v4.5.103",
17704
- commit: "c2dd99cf0d72",
17705
- npm: "@mutmutco/cli@4.5.103"
17702
+ version: "4.5.105",
17703
+ tag: "v4.5.105",
17704
+ commit: "2edfdb644608",
17705
+ npm: "@mutmutco/cli@4.5.105"
17706
17706
  },
17707
17707
  exitCriterion: "fleet-n-of-n",
17708
17708
  hubOnlyShortcut: "forbidden",
@@ -17719,14 +17719,14 @@ var rollout_plan_default = {
17719
17719
  repo: "mutmutco/mmi-hub",
17720
17720
  role: "canary",
17721
17721
  schedule: "train",
17722
- v3Target: "v4.5.103"
17722
+ v3Target: "v4.5.105"
17723
17723
  }
17724
17724
  ],
17725
17725
  rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
17726
17726
  rollback: {
17727
17727
  independent: true,
17728
- mechanism: "npm dist-tag latest -> 4.5.103 and redeploy the Hub Lambda from tag v4.5.103 (c2dd99cf0d72); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17729
- v3Target: "v4.5.103 (@mutmutco/cli@4.5.103, tag commit c2dd99cf0d72 \u2014 last known-good release carrying the repo-index v4-only contract)"
17728
+ mechanism: "npm dist-tag latest -> 4.5.105 and redeploy the Hub Lambda from tag v4.5.105 (2edfdb644608); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17729
+ v3Target: "v4.5.105 (@mutmutco/cli@4.5.105, tag commit 2edfdb644608 \u2014 last known-good release carrying the repo-index v4-only contract)"
17730
17730
  }
17731
17731
  },
17732
17732
  {
@@ -21992,7 +21992,7 @@ function renderTenantControl(r) {
21992
21992
  }
21993
21993
 
21994
21994
  // src/train-apply-phases.ts
21995
- var import_node_fs24 = require("node:fs");
21995
+ var import_node_fs25 = require("node:fs");
21996
21996
  var import_promises4 = require("node:fs/promises");
21997
21997
  var import_node_path24 = require("node:path");
21998
21998
 
@@ -25765,6 +25765,7 @@ function formatTrainDoctor(v) {
25765
25765
 
25766
25766
  // src/release-plain-words.ts
25767
25767
  var import_promises3 = require("node:fs/promises");
25768
+ var import_node_fs24 = require("node:fs");
25768
25769
  function releaseCutByNotes(cutBy) {
25769
25770
  return `## Internal
25770
25771
  Cut by ${formatSessionIdentity(cutBy)}
@@ -25779,8 +25780,44 @@ function buildPlainWordsSection(lines2, big, heading = PLAIN_WORDS_HEADING) {
25779
25780
  if (big) body.push(PLAIN_WORDS_MARKER);
25780
25781
  return body.join("\n");
25781
25782
  }
25782
- function noChangePlainWordsLine(suiteTag) {
25783
- return suiteTag ? `No changes to this module in this release; ships with the MM-Work suite ${suiteTag}.` : "No changes to this module in this release.";
25783
+ var SUMMARY_GUIDE = "https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing";
25784
+ function validatePlainWordsSummary(content) {
25785
+ const lines2 = content.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
25786
+ if (lines2[0]?.startsWith("No changes to this module")) throw new Error(`requires a real MM-Work title, not the no-change placeholder \u2014 see ${SUMMARY_GUIDE}`);
25787
+ if (lines2.length < 3 || lines2.length > 6) throw new Error(`requires 3\u20136 curated non-empty lines (title + 2\u20135 notes; found ${lines2.length}) \u2014 see ${SUMMARY_GUIDE}`);
25788
+ }
25789
+ function validateSuiteReleaseSummary(path2) {
25790
+ if (!path2) throw new Error(`requires --announce-summary-file <path> before promoting any member \u2014 see ${SUMMARY_GUIDE}`);
25791
+ let content;
25792
+ try {
25793
+ content = (0, import_node_fs24.readFileSync)(path2, "utf8");
25794
+ } catch (e) {
25795
+ throw new Error(`could not read summary file ${path2}: ${e.message} \u2014 see ${SUMMARY_GUIDE}`);
25796
+ }
25797
+ validatePlainWordsSummary(content);
25798
+ }
25799
+ async function validateReleaseSummaries(releases, files, releaseExists) {
25800
+ const refusals = [];
25801
+ for (const { repo, tag } of releases) {
25802
+ if (await releaseExists(repo, tag)) continue;
25803
+ try {
25804
+ validateSuiteReleaseSummary(files[repo]);
25805
+ } catch (e) {
25806
+ refusals.push(`${repo}: ${e.message}`);
25807
+ }
25808
+ }
25809
+ if (refusals.length) throw new Error(`suite release summaries refused; no member was released:
25810
+ ${refusals.join("\n")}`);
25811
+ }
25812
+ async function requireSuiteReleaseSummary(deps, path2) {
25813
+ if (!path2) throw new Error(`a new mm-work suite Release requires --announce-summary-file <path> with a title + 2\u20135 notes \u2014 see ${SUMMARY_GUIDE}`);
25814
+ let content;
25815
+ try {
25816
+ content = await (deps.readFile ?? ((p) => (0, import_promises3.readFile)(p, "utf8")))(path2);
25817
+ } catch (e) {
25818
+ throw new Error(`could not read --announce-summary-file ${path2}: ${e.message} \u2014 see ${SUMMARY_GUIDE}`);
25819
+ }
25820
+ validatePlainWordsSummary(content);
25784
25821
  }
25785
25822
  function upsertPlainWordsSection(body, section) {
25786
25823
  let rest = body;
@@ -25810,7 +25847,8 @@ function plainWordsSummaryLines(content) {
25810
25847
  async function writePlainWordsIfSuiteMember(deps, args) {
25811
25848
  if (args.meta?.suite !== "mm-work") return;
25812
25849
  try {
25813
- const lines2 = args.summaryLines && args.summaryLines.length > 0 ? args.summaryLines : [noChangePlainWordsLine(args.suiteTag)];
25850
+ if (!args.summaryLines?.length) return;
25851
+ const lines2 = args.summaryLines;
25814
25852
  const section = [
25815
25853
  buildPlainWordsSection(lines2, args.big === true),
25816
25854
  ...args.summaryLinesTr?.length ? [buildPlainWordsSection(args.summaryLinesTr, false, PLAIN_WORDS_HEADING_TR)] : []
@@ -25962,14 +26000,14 @@ function readLocalGateWorkflows() {
25962
26000
  const dir = (0, import_node_path24.join)(".github", "workflows");
25963
26001
  let names;
25964
26002
  try {
25965
- names = (0, import_node_fs24.readdirSync)(dir);
26003
+ names = (0, import_node_fs25.readdirSync)(dir);
25966
26004
  } catch {
25967
26005
  return null;
25968
26006
  }
25969
26007
  const files = [];
25970
26008
  for (const name of names.filter(isGateWorkflowPath)) {
25971
26009
  try {
25972
- files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs24.readFileSync)((0, import_node_path24.join)(dir, name), "utf8") });
26010
+ files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs25.readFileSync)((0, import_node_path24.join)(dir, name), "utf8") });
25973
26011
  } catch {
25974
26012
  }
25975
26013
  }
@@ -25979,14 +26017,14 @@ function readLocalWorkflows() {
25979
26017
  const dir = (0, import_node_path24.join)(".github", "workflows");
25980
26018
  let names;
25981
26019
  try {
25982
- names = (0, import_node_fs24.readdirSync)(dir);
26020
+ names = (0, import_node_fs25.readdirSync)(dir);
25983
26021
  } catch {
25984
26022
  return null;
25985
26023
  }
25986
26024
  const files = [];
25987
26025
  for (const name of names.filter((n) => /\.ya?ml$/i.test(n))) {
25988
26026
  try {
25989
- files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs24.readFileSync)((0, import_node_path24.join)(dir, name), "utf8") });
26027
+ files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs25.readFileSync)((0, import_node_path24.join)(dir, name), "utf8") });
25990
26028
  } catch {
25991
26029
  }
25992
26030
  }
@@ -26868,7 +26906,7 @@ async function activeRcandLedgerCandidate(deps, cwd) {
26868
26906
  }
26869
26907
  let prior;
26870
26908
  try {
26871
- prior = parseReleaseLedger((0, import_node_fs24.readFileSync)(path2, "utf8"));
26909
+ prior = parseReleaseLedger((0, import_node_fs25.readFileSync)(path2, "utf8"));
26872
26910
  } catch {
26873
26911
  return void 0;
26874
26912
  }
@@ -27157,6 +27195,7 @@ Nothing was written. Inspect ${ledger.path} (or clear it only after proving the
27157
27195
  }
27158
27196
  const ledgerMode = persisted ? "strict" : "migration";
27159
27197
  const partialRelease = await isStrayUnreleasedTag(deps, tag, tagSha, ctx.repo);
27198
+ if (partialRelease && meta.suite === "mm-work") await requireSuiteReleaseSummary(deps, options.announceSummaryFile);
27160
27199
  if (!partialRelease) {
27161
27200
  try {
27162
27201
  await verifyPublishedRelease(deps, ctx.repo, tag, "main", tagSha);
@@ -28031,6 +28070,7 @@ async function runTrainApplyLocked(command, deps, options = {}) {
28031
28070
  const bumpIntent = await resolveTrainBumpIntent(deps, process.env, void 0, preOneZeroInForce(meta));
28032
28071
  const branchHints = await loadReleaseTrackBranchHints(deps);
28033
28072
  const directTrack = isHubControlRepo(ctx.repo) || resolveReleaseTrack(meta, branchHints) === "direct";
28073
+ if (command === "release" && meta.suite === "mm-work") await requireSuiteReleaseSummary(deps, options.announceSummaryFile);
28034
28074
  const pipelineInput = { deps, ctx, command, meta, branchHints, watch, options, bumpIntent };
28035
28075
  if (command === "rcand") {
28036
28076
  if (directTrack) {
@@ -28344,7 +28384,7 @@ function planBranchRequiredContexts(input) {
28344
28384
  })).filter((entry) => entry.contexts.length > 0);
28345
28385
  return { common, pending, waiting };
28346
28386
  }
28347
- async function branchContextPlan(deps, repo, authority, meta) {
28387
+ async function branchContextPlan(deps, repo, authority, meta, emittedBranchesOnly = false) {
28348
28388
  const branches = resolveRequiredCheckBranches(meta, repo);
28349
28389
  const emittedByBranch = {};
28350
28390
  const unreadable = [];
@@ -28354,7 +28394,10 @@ async function branchContextPlan(deps, repo, authority, meta) {
28354
28394
  emittedByBranch[branch] = await resolveEmittedPrContexts(deps, repo, branch);
28355
28395
  if (presence === void 0 || emittedByBranch[branch] === void 0) unreadable.push(branch);
28356
28396
  }
28357
- return { ...planBranchRequiredContexts({ contexts: authority.contexts, branches, emittedByBranch }), unreadable };
28397
+ const bindingBranches = emittedBranchesOnly ? branches.filter((branch) => authority.contexts.some((context) => emittedByBranch[branch]?.includes(context))) : branches;
28398
+ const plan = planBranchRequiredContexts({ contexts: authority.contexts, branches: bindingBranches, emittedByBranch });
28399
+ const waiting = planBranchRequiredContexts({ contexts: authority.contexts, branches, emittedByBranch }).waiting;
28400
+ return { ...plan, waiting, unreadable, bindingBranches };
28358
28401
  }
28359
28402
  function projectCommonAuthority(authority, common) {
28360
28403
  const apiPayload = patchRulesetRequiredContexts(authority.apiPayload, common);
@@ -28608,8 +28651,8 @@ async function auditRepoCi(repo, callerDeps) {
28608
28651
  checks.push({
28609
28652
  ok: pendingAligned,
28610
28653
  label: "branch-specific required checks match workflow emitters",
28611
- detail: pendingAligned ? void 0 : "pending ruleset contexts or branch conditions have not converged to the emitted registry requirements",
28612
- remediation: pendingAligned ? void 0 : `mmi-cli devops ci reconcile --repo ${repo} --apply`
28654
+ detail: pendingAligned ? void 0 : `pending ruleset contexts or branch conditions have not converged to the emitted registry requirements \u2014 run mmi-cli devops bootstrap apply ${repo} --only product-ruleset --execute`,
28655
+ remediation: pendingAligned ? void 0 : `mmi-cli devops bootstrap apply ${repo} --only product-ruleset --execute`
28613
28656
  });
28614
28657
  }
28615
28658
  }
@@ -28641,7 +28684,7 @@ async function auditRepoCi(repo, callerDeps) {
28641
28684
  checks.push({
28642
28685
  ok: aligned,
28643
28686
  label: RULESET_REFERENCE_MATCH_LABEL,
28644
- detail: aligned ? `contexts [${authoritativeRuleset.contexts.join(", ")}], branches [${authoritativeRuleset.branchIncludes.join(", ")}] and the up-to-date policy match declared authority` : `${authoritativeRuleset.source} requires contexts [${authoritativeRuleset.contexts.join(", ")}]; ${authoritativeRuleset.branchSource} requires branches [${authoritativeRuleset.branchIncludes.join(", ")}]; ${PRODUCT_RULESET_REF} declares contexts [${authoritativeRuleset.committedContexts.join(", ")}] / branches [${authoritativeRuleset.committedBranchIncludes.join(", ")}] and live ${PRODUCT_RULESET_NAME} has contexts [${liveContexts.join(", ")}] / branches [${liveBranchIncludes.join(", ")}]` + (strictAligned ? "" : `; strict_required_status_checks_policy differs from committed policy \u2014 ${PRODUCT_RULESET_REF} declares ${authoritativeRuleset.committedStrict}, live ${PRODUCT_RULESET_NAME} has ${liveStrict}`),
28687
+ detail: aligned ? `contexts [${authoritativeRuleset.contexts.join(", ")}], branches [${authoritativeRuleset.branchIncludes.join(", ")}] and the up-to-date policy match declared authority` : `${authoritativeRuleset.source} requires contexts [${authoritativeRuleset.contexts.join(", ")}]; ${authoritativeRuleset.branchSource} requires branches [${authoritativeRuleset.branchIncludes.join(", ")}]; ${PRODUCT_RULESET_REF} declares contexts [${authoritativeRuleset.committedContexts.join(", ")}] / branches [${authoritativeRuleset.committedBranchIncludes.join(", ")}] and live ${PRODUCT_RULESET_NAME} has contexts [${liveContexts.join(", ")}] / branches [${liveBranchIncludes.join(", ")}]` + (strictAligned ? "" : `; strict_required_status_checks_policy differs from committed policy \u2014 ${PRODUCT_RULESET_REF} declares ${authoritativeRuleset.committedStrict}, live ${PRODUCT_RULESET_NAME} has ${liveStrict}`) + ` \u2014 live repair: mmi-cli devops bootstrap apply ${repo} --only product-ruleset --execute`,
28645
28688
  remediation: aligned ? void 0 : `mmi-cli devops ci reconcile --repo ${repo} --apply`
28646
28689
  });
28647
28690
  }
@@ -29184,9 +29227,14 @@ async function authoritativeSafetyHold(authority, deps, repo, baseBranch, meta)
29184
29227
  }
29185
29228
  return null;
29186
29229
  }
29187
- async function applyCiReconcileRepo(repo, deps) {
29230
+ async function applyCiReconcileRepo(repo, deps, options = {}) {
29231
+ if (options.dryRun && !options.productRulesetOnly) throw new Error("dry-run requires productRulesetOnly");
29188
29232
  const meta = await deps.getProjectMeta(slugFromRepo(repo));
29233
+ if (options.productRulesetOnly && !meta?.requiredChecks?.length) {
29234
+ return { repo, applied: [], skipped: [], errors: ["product-ruleset requires registry META requiredChecks"] };
29235
+ }
29189
29236
  const report = await auditRepoCi(repo, deps);
29237
+ const finish = (result2, pendingReason) => options.productRulesetOnly ? Promise.resolve(result2) : finalizeCiReconcile(repo, deps, result2, report, pendingReason);
29190
29238
  const baseBranch = "development";
29191
29239
  let raw = report.class === "deployable" && !report.explicitNoCi ? await fetchRulesetSeedBody(deps, repo) : null;
29192
29240
  let authority = null;
@@ -29196,12 +29244,12 @@ async function applyCiReconcileRepo(repo, deps) {
29196
29244
  authority = parseAuthoritativeRuleset(raw, meta, repo);
29197
29245
  } catch (e) {
29198
29246
  empty.errors.push(`${PRODUCT_RULESET_REF} is not parseable: ${e.message}`);
29199
- return finalizeCiReconcile(repo, deps, empty, report);
29247
+ return finish(empty);
29200
29248
  }
29201
29249
  const hold = await authoritativeSafetyHold(authority, deps, repo, baseBranch, meta);
29202
29250
  if (hold) {
29203
29251
  empty.skipped.push(hold);
29204
- return finalizeCiReconcile(repo, deps, empty, report, hold);
29252
+ return finish(empty, hold);
29205
29253
  }
29206
29254
  } else if (report.class === "deployable" && !report.explicitNoCi) {
29207
29255
  const registryAuthority = registryAuthorityWithoutReference(meta, repo);
@@ -29209,59 +29257,64 @@ async function applyCiReconcileRepo(repo, deps) {
29209
29257
  const hold = await authoritativeSafetyHold(registryAuthority, deps, repo, baseBranch, meta);
29210
29258
  if (hold) {
29211
29259
  empty.skipped.push(hold);
29212
- return finalizeCiReconcile(repo, deps, empty, report, hold);
29260
+ return finish(empty, hold);
29213
29261
  }
29214
29262
  }
29215
29263
  }
29216
- const result = await applyCiReconcileMergeSettingsFromReport(repo, deps, report);
29264
+ const result = options.productRulesetOnly ? empty : await applyCiReconcileMergeSettingsFromReport(repo, deps, report);
29217
29265
  if (report.class !== "deployable" || report.explicitNoCi) {
29218
- return finalizeCiReconcile(repo, deps, result, report);
29266
+ return finish(result);
29219
29267
  }
29220
- const refDelivery = await seedGateYml(repo, deps, meta, result);
29268
+ const refDelivery = options.productRulesetOnly ? void 0 : await seedGateYml(repo, deps, meta, result);
29221
29269
  if (refDelivery === "pending") {
29222
29270
  const reason = `${PRODUCT_RULESET_REF} is pending on its protected-base source PR; live ruleset convergence waits for that source to merge`;
29223
29271
  result.skipped.push(reason);
29224
- return finalizeCiReconcile(repo, deps, result, report, reason);
29272
+ return finish(result, reason);
29225
29273
  }
29226
29274
  raw ??= await fetchRulesetSeedBody(deps, repo);
29227
29275
  if (!raw) {
29228
29276
  result.errors.push(`missing ${PRODUCT_RULESET_REF} on development \u2014 run bootstrap apply first`);
29229
- return finalizeCiReconcile(repo, deps, result, report);
29277
+ return finish(result);
29230
29278
  }
29231
29279
  if (authority == null) {
29232
29280
  try {
29233
29281
  authority = parseAuthoritativeRuleset(raw, meta, repo);
29234
29282
  } catch (e) {
29235
29283
  result.errors.push(`${PRODUCT_RULESET_REF} is not parseable: ${e.message}`);
29236
- return finalizeCiReconcile(repo, deps, result, report);
29284
+ return finish(result);
29237
29285
  }
29238
29286
  const hold = await authoritativeSafetyHold(authority, deps, repo, baseBranch, meta);
29239
29287
  if (hold) {
29240
29288
  result.skipped.push(hold);
29241
- return finalizeCiReconcile(repo, deps, result, report, hold);
29289
+ return finish(result, hold);
29242
29290
  }
29243
29291
  }
29244
29292
  if (authority.contexts.length === 0) {
29245
29293
  result.errors.push(`${authority.source} declares no required contexts`);
29246
- return finalizeCiReconcile(repo, deps, result, report);
29294
+ return finish(result);
29247
29295
  }
29248
29296
  let pendingPlan = null;
29249
29297
  const declaredContexts = authority.contexts;
29250
29298
  if (authority.source === "registry META requiredChecks") {
29251
- pendingPlan = await branchContextPlan(deps, repo, authority, meta);
29299
+ pendingPlan = await branchContextPlan(deps, repo, authority, meta, options.productRulesetOnly);
29252
29300
  if (pendingPlan.unreadable.length) {
29253
29301
  const reason = `required branch workflow emitters unreadable on [${pendingPlan.unreadable.join(", ")}] \u2014 ruleset convergence deferred`;
29254
29302
  result.skipped.push(reason);
29255
- return finalizeCiReconcile(repo, deps, result, report, reason);
29303
+ return finish(result, reason);
29256
29304
  }
29257
29305
  if (pendingPlan.common.length === 0) {
29258
29306
  result.errors.push("no common required context is emitted by every required branch \u2014 ruleset convergence deferred");
29259
- return finalizeCiReconcile(repo, deps, result, report);
29307
+ return finish(result);
29260
29308
  }
29261
29309
  authority = projectCommonAuthority(authority, pendingPlan.common);
29310
+ if (options.productRulesetOnly) {
29311
+ const apiPayload = patchRulesetBranchIncludes(authority.apiPayload, pendingPlan.bindingBranches);
29312
+ authority = { ...authority, apiPayload, branchIncludes: rulesetBranchIncludes(apiPayload) };
29313
+ for (const entry of pendingPlan.waiting) result.skipped.push(`${entry.branch} waits for workflow emitter(s) [${entry.contexts.join(", ")}]`);
29314
+ }
29262
29315
  }
29263
29316
  const sourceNeedsConvergence = !sameContexts(authority.committedContexts, authority.contexts) || !sameContexts(authority.committedBranchIncludes, authority.branchIncludes) || !authority.committedStrict;
29264
- if (sourceNeedsConvergence) {
29317
+ if (sourceNeedsConvergence && !options.productRulesetOnly) {
29265
29318
  try {
29266
29319
  const delivery = await deliverSeedFile(
29267
29320
  deps,
@@ -29278,15 +29331,15 @@ async function applyCiReconcileRepo(repo, deps) {
29278
29331
  `${PRODUCT_RULESET_REF} authority \u2192 contexts [${authority.contexts.join(", ")}], branches [${authority.branchIncludes.join(", ")}] delivered on ${delivery.plan.branch} (${delivery.plan.reason})${delivery.prUrl ? `: ${delivery.prUrl}` : ""}`
29279
29332
  );
29280
29333
  const reason = `${PRODUCT_RULESET_REF} authority is pending on its protected-base source PR; live ruleset convergence waits for that source to merge`;
29281
- return finalizeCiReconcile(repo, deps, result, report, reason);
29334
+ return finish(result, reason);
29282
29335
  } else {
29283
29336
  result.applied.push(`reconciled ${PRODUCT_RULESET_REF} authority \u2192 contexts [${authority.contexts.join(", ")}], branches [${authority.branchIncludes.join(", ")}]`);
29284
29337
  }
29285
29338
  } catch (e) {
29286
29339
  result.errors.push(`ruleset reference delivery failed: ${e.message}`);
29287
- return finalizeCiReconcile(repo, deps, result, report);
29340
+ return finish(result);
29288
29341
  }
29289
- } else {
29342
+ } else if (!options.productRulesetOnly) {
29290
29343
  result.skipped.push(`${PRODUCT_RULESET_REF} already matches declared context and branch authority`);
29291
29344
  }
29292
29345
  let live;
@@ -29294,7 +29347,7 @@ async function applyCiReconcileRepo(repo, deps) {
29294
29347
  live = await readLiveProductRuleset(deps, repo);
29295
29348
  } catch (e) {
29296
29349
  result.errors.push(`cannot read the live ${PRODUCT_RULESET_NAME} ruleset: ${e.message}`);
29297
- return finalizeCiReconcile(repo, deps, result, report);
29350
+ return finish(result);
29298
29351
  }
29299
29352
  const liveContexts = live == null ? [] : sortedUnique(rulesetRequiredContexts(live));
29300
29353
  const liveBranchIncludes = live == null ? [] : rulesetBranchIncludes(live);
@@ -29312,7 +29365,7 @@ async function applyCiReconcileRepo(repo, deps) {
29312
29365
  if (pendingPlan?.pending.some((entry) => entry.contexts.some((context) => unsafe.includes(context)))) {
29313
29366
  const reason = `pending required-check rulesets left non-enforcing \u2014 [${unsafe.join(", ")}] emitted only by path-filtered workflows`;
29314
29367
  result.skipped.push(reason);
29315
- return finalizeCiReconcile(repo, deps, result, report, reason);
29368
+ return finish(result, reason);
29316
29369
  }
29317
29370
  const gateProvenGreen = await gateIsProvenGreen(repo, deps.client, baseBranch, gateFiles);
29318
29371
  const plan = resolveProductRulesetReconcilePlan({
@@ -29329,25 +29382,25 @@ async function applyCiReconcileRepo(repo, deps) {
29329
29382
  if (!plan.shouldActivate) {
29330
29383
  if (plan.holdReason) {
29331
29384
  result.skipped.push(plan.holdReason);
29332
- return finalizeCiReconcile(repo, deps, result, report, plan.holdReason);
29385
+ return finish(result, plan.holdReason);
29333
29386
  }
29334
29387
  result.skipped.push(`live ${PRODUCT_RULESET_NAME} contexts, branches and up-to-date policy already match declared authority`);
29335
29388
  }
29336
29389
  if (plan.shouldActivate) {
29337
29390
  try {
29338
- const activation = await activateProductRuleset(repo, authority.apiPayload, deps.client, plan.targetEnforcement);
29391
+ const activation = await activateProductRuleset(repo, authority.apiPayload, deps.client, plan.targetEnforcement, options.dryRun);
29339
29392
  if (activation.action === "skipped") result.skipped.push(activation.detail ?? "product ruleset");
29340
- else result.applied.push(`product ruleset ${activation.action}${activation.detail ? `: ${activation.detail}` : ""}`);
29393
+ else result.applied.push(`product ruleset ${activation.action} (${activation.enforcement}; contexts [${authority.contexts.join(", ")}], branches [${authority.branchIncludes.join(", ")}])${activation.detail ? `: ${activation.detail}` : ""}`);
29341
29394
  } catch (e) {
29342
29395
  result.errors.push(e.message);
29343
- return finalizeCiReconcile(repo, deps, result, report);
29396
+ return finish(result);
29344
29397
  }
29345
29398
  }
29346
29399
  if (pendingPlan) {
29347
29400
  if (pendingPlan.pending.length > 0 && !gateProvenGreen) {
29348
29401
  const reason = "pending required-check rulesets left non-enforcing \u2014 the gate is not green on development";
29349
29402
  result.skipped.push(reason);
29350
- return finalizeCiReconcile(repo, deps, result, report, reason);
29403
+ return finish(result, reason);
29351
29404
  }
29352
29405
  try {
29353
29406
  const desired = /* @__PURE__ */ new Set();
@@ -29363,20 +29416,20 @@ async function applyCiReconcileRepo(repo, deps) {
29363
29416
  patchRulesetRequiredContexts(statusOnly, entry.contexts),
29364
29417
  [entry.branch]
29365
29418
  );
29366
- const activation = await activateProductRuleset(repo, payload, deps.client);
29367
- (activation.action === "skipped" ? result.skipped : result.applied).push(`${name}: ${activation.action}`);
29419
+ const activation = await activateProductRuleset(repo, payload, deps.client, "active", options.dryRun);
29420
+ (activation.action === "skipped" ? result.skipped : result.applied).push(`${name}: ${activation.action} \u2014 contexts [${entry.contexts.join(", ")}]`);
29368
29421
  }
29369
29422
  const list = await deps.client.rest("GET", `repos/${repo}/rulesets`);
29370
29423
  for (const stale of list ?? []) {
29371
29424
  if (!stale.name?.startsWith(PRODUCT_PENDING_RULESET_PREFIX) || desired.has(stale.name) || stale.id == null) continue;
29372
- await deps.client.rest("DELETE", `repos/${repo}/rulesets/${stale.id}`);
29425
+ if (!options.dryRun) await deps.client.rest("DELETE", `repos/${repo}/rulesets/${stale.id}`);
29373
29426
  result.applied.push(`deleted converged ${stale.name}`);
29374
29427
  }
29375
29428
  } catch (e) {
29376
29429
  result.errors.push(`pending ruleset convergence failed: ${e.message}`);
29377
29430
  }
29378
29431
  }
29379
- return finalizeCiReconcile(repo, deps, result, report);
29432
+ return finish(result);
29380
29433
  }
29381
29434
 
29382
29435
  // src/command-composition.ts
@@ -30728,7 +30781,7 @@ function registerBoardCommands(program3) {
30728
30781
  }
30729
30782
 
30730
30783
  // src/bootstrap-commands.ts
30731
- var import_node_fs28 = require("node:fs");
30784
+ var import_node_fs29 = require("node:fs");
30732
30785
  var import_node_os17 = require("node:os");
30733
30786
  var import_node_path28 = require("node:path");
30734
30787
  init_cli_shared();
@@ -30914,7 +30967,7 @@ init_github_client();
30914
30967
  init_cli_shared();
30915
30968
 
30916
30969
  // src/port-registry.ts
30917
- var import_node_fs25 = require("node:fs");
30970
+ var import_node_fs26 = require("node:fs");
30918
30971
  var import_node_path25 = require("node:path");
30919
30972
 
30920
30973
  // ../infra/port-geometry.mjs
@@ -30929,8 +30982,8 @@ function nextPortBlock(registry2) {
30929
30982
  return [base, base + PORT_SPAN];
30930
30983
  }
30931
30984
  function loadPortRegistry(path2) {
30932
- if (!(0, import_node_fs25.existsSync)(path2)) return {};
30933
- const raw = JSON.parse((0, import_node_fs25.readFileSync)(path2, "utf8"));
30985
+ if (!(0, import_node_fs26.existsSync)(path2)) return {};
30986
+ const raw = JSON.parse((0, import_node_fs26.readFileSync)(path2, "utf8"));
30934
30987
  const out = {};
30935
30988
  for (const [key, value] of Object.entries(raw)) {
30936
30989
  if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
@@ -30960,7 +31013,7 @@ function existingPortRange(repo, registry2) {
30960
31013
  function portRangeInfraAt(root, source) {
30961
31014
  const registryPath = (0, import_node_path25.join)(root, "infra", "port-ranges.json");
30962
31015
  const ddbScriptPath = (0, import_node_path25.join)(root, "infra", "port-ddb.mjs");
30963
- if (!(0, import_node_fs25.existsSync)(registryPath) || !(0, import_node_fs25.existsSync)(ddbScriptPath)) return null;
31016
+ if (!(0, import_node_fs26.existsSync)(registryPath) || !(0, import_node_fs26.existsSync)(ddbScriptPath)) return null;
30964
31017
  return { root, source, registryPath, ddbScriptPath };
30965
31018
  }
30966
31019
  function resolvePortRangeInfra(cwd, packageDir) {
@@ -31042,7 +31095,7 @@ async function assignPersistedPortRange(repo, slug, reg, opts) {
31042
31095
  }
31043
31096
 
31044
31097
  // src/box-commands.ts
31045
- var import_node_fs27 = require("node:fs");
31098
+ var import_node_fs28 = require("node:fs");
31046
31099
  var import_node_child_process10 = require("node:child_process");
31047
31100
  var import_node_os16 = require("node:os");
31048
31101
  var import_node_path27 = require("node:path");
@@ -31174,7 +31227,7 @@ function applyRemoteScript(items, date) {
31174
31227
  init_clean_exit();
31175
31228
 
31176
31229
  // src/secrets-commands.ts
31177
- var import_node_fs26 = require("node:fs");
31230
+ var import_node_fs27 = require("node:fs");
31178
31231
  var import_node_path26 = require("node:path");
31179
31232
  var import_node_os15 = require("node:os");
31180
31233
  init_cli_shared();
@@ -32336,8 +32389,8 @@ async function decryptRailsCredentials(input) {
32336
32389
  MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
32337
32390
  MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
32338
32391
  };
32339
- if ((0, import_node_fs26.existsSync)(masterKeyPath)) {
32340
- env.RAILS_MASTER_KEY = (0, import_node_fs26.readFileSync)(masterKeyPath, "utf8").trim();
32392
+ if ((0, import_node_fs27.existsSync)(masterKeyPath)) {
32393
+ env.RAILS_MASTER_KEY = (0, import_node_fs27.readFileSync)(masterKeyPath, "utf8").trim();
32341
32394
  }
32342
32395
  const script = [
32343
32396
  'require "json"',
@@ -32347,9 +32400,9 @@ async function decryptRailsCredentials(input) {
32347
32400
  'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
32348
32401
  "puts JSON.generate(config.config)"
32349
32402
  ].join("\n");
32350
- const scriptDir = (0, import_node_fs26.mkdtempSync)((0, import_node_path26.join)((0, import_node_os15.tmpdir)(), "mmi-rails-decrypt-"));
32403
+ const scriptDir = (0, import_node_fs27.mkdtempSync)((0, import_node_path26.join)((0, import_node_os15.tmpdir)(), "mmi-rails-decrypt-"));
32351
32404
  const scriptPath = (0, import_node_path26.join)(scriptDir, "decrypt.rb");
32352
- (0, import_node_fs26.writeFileSync)(scriptPath, script, "utf8");
32405
+ (0, import_node_fs27.writeFileSync)(scriptPath, script, "utf8");
32353
32406
  try {
32354
32407
  const args = ["exec", "ruby", scriptPath];
32355
32408
  const cmd = process.platform === "win32" ? "cmd.exe" : "bundle";
@@ -32361,7 +32414,7 @@ async function decryptRailsCredentials(input) {
32361
32414
  });
32362
32415
  return JSON.parse(stdout);
32363
32416
  } finally {
32364
- (0, import_node_fs26.rmSync)(scriptDir, { recursive: true, force: true });
32417
+ (0, import_node_fs27.rmSync)(scriptDir, { recursive: true, force: true });
32365
32418
  }
32366
32419
  }
32367
32420
  async function readSecretStdin() {
@@ -32451,7 +32504,7 @@ function registerSecretsCommands(program3) {
32451
32504
  let body;
32452
32505
  if (o.file) {
32453
32506
  try {
32454
- body = (0, import_node_fs26.readFileSync)((0, import_node_path26.resolve)(o.file), "utf8");
32507
+ body = (0, import_node_fs27.readFileSync)((0, import_node_path26.resolve)(o.file), "utf8");
32455
32508
  } catch (e) {
32456
32509
  return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
32457
32510
  }
@@ -32587,7 +32640,7 @@ function registerSecretsCommands(program3) {
32587
32640
  {
32588
32641
  ...d,
32589
32642
  decryptRailsCredentials,
32590
- removeFile: (path2) => (0, import_node_fs26.unlinkSync)((0, import_node_path26.resolve)(o.appDir ?? process.cwd(), path2))
32643
+ removeFile: (path2) => (0, import_node_fs27.unlinkSync)((0, import_node_path26.resolve)(o.appDir ?? process.cwd(), path2))
32591
32644
  },
32592
32645
  {
32593
32646
  repo: o.repo,
@@ -32635,8 +32688,6 @@ function registerSecretsCommands(program3) {
32635
32688
  var BOX_KEYS = {
32636
32689
  /** Opens every MM box INCLUDING the CI runner (`mmi-runner`). The general-purpose MM key. */
32637
32690
  mmMaster: "HETZNER_MASTER_SSH_PRIVATE_KEY",
32638
- /** Opens the OGUZ project's boxes. */
32639
- oguzMaster: "HETZNER_OGUZ_MASTER_SSH_PRIVATE_KEY",
32640
32691
  /** The deploy key. Opens MM prod/tenant boxes but NOT `mmi-runner` — a real trap: reaching for this key
32641
32692
  * to get into the CI runner fails, and the failure reads like a broken box rather than a wrong key. */
32642
32693
  mmDeploy: "HETZNER_SSH_PRIVATE_KEY"
@@ -32645,11 +32696,10 @@ var HETZNER_VAULT_SLUG = "_org";
32645
32696
  var HETZNER_NS = "hetzner";
32646
32697
  var nsKey = (key) => `${HETZNER_NS}/${key}`;
32647
32698
  var PROJECT_API_TOKEN = {
32648
- MM: nsKey("HETZNER_MM_API_TOKEN"),
32649
- OGUZ: nsKey("HETZNER_OGUZ_API_TOKEN")
32699
+ MM: nsKey("HETZNER_MM_API_TOKEN")
32650
32700
  };
32651
- function sshKeyForProject(project2) {
32652
- return project2 === "OGUZ" ? BOX_KEYS.oguzMaster : BOX_KEYS.mmMaster;
32701
+ function sshKeyForProject(_project) {
32702
+ return BOX_KEYS.mmMaster;
32653
32703
  }
32654
32704
  function vaultPath(key) {
32655
32705
  return `/mmi-future/${HETZNER_VAULT_SLUG}/${HETZNER_NS}/${key}`;
@@ -32752,7 +32802,7 @@ var HETZNER_API = "https://api.hetzner.cloud/v1/servers";
32752
32802
  var HETZNER_TIMEOUT_MS = 15e3;
32753
32803
  var PER_PAGE = 50;
32754
32804
  var MAX_PAGES = 20;
32755
- var PROJECTS = ["MM", "OGUZ"];
32805
+ var PROJECTS = ["MM"];
32756
32806
  async function fetchProjectBoxes(project2, token) {
32757
32807
  const boxes = [];
32758
32808
  let page = 1;
@@ -32846,7 +32896,7 @@ function registerBoxCommands(program3) {
32846
32896
  }
32847
32897
  const remote = command && command.length ? command.join(" ") : void 0;
32848
32898
  const wroteScript = o.ssh && o.script ? o.script : null;
32849
- if (wroteScript) (0, import_node_fs27.writeFileSync)(wroteScript, sshRecipeScript(found, remote), "utf8");
32899
+ if (wroteScript) (0, import_node_fs28.writeFileSync)(wroteScript, sshRecipeScript(found, remote), "utf8");
32850
32900
  if (o.json) {
32851
32901
  console.log(JSON.stringify({
32852
32902
  box: found,
@@ -32920,17 +32970,17 @@ function hubGitShow(ref) {
32920
32970
  return (path2) => git3(["show", `${ref}:${path2}`]);
32921
32971
  }
32922
32972
  function runOnBox(found, script) {
32923
- const dir = (0, import_node_fs27.mkdtempSync)((0, import_node_path27.join)((0, import_node_os16.tmpdir)(), "mmi-box-control-"));
32973
+ const dir = (0, import_node_fs28.mkdtempSync)((0, import_node_path27.join)((0, import_node_os16.tmpdir)(), "mmi-box-control-"));
32924
32974
  try {
32925
32975
  const recipe = (0, import_node_path27.join)(dir, "recipe.sh");
32926
- (0, import_node_fs27.writeFileSync)(recipe, sshRecipeScript(found, "sh -s"), "utf8");
32976
+ (0, import_node_fs28.writeFileSync)(recipe, sshRecipeScript(found, "sh -s"), "utf8");
32927
32977
  const r = (0, import_node_child_process10.spawnSync)("bash", [recipe], { input: script, encoding: "utf8", maxBuffer: 16 * 1024 * 1024, timeout: 18e4, windowsHide: true });
32928
32978
  if (r.status !== 0) {
32929
32979
  throw new UnreadableError(`box control: the ssh session to ${found.name} failed (exit ${r.status ?? r.signal}): ${(r.stderr || "").trim().split("\n").slice(-3).join(" | ")}`);
32930
32980
  }
32931
32981
  return r.stdout;
32932
32982
  } finally {
32933
- (0, import_node_fs27.rmSync)(dir, { recursive: true, force: true });
32983
+ (0, import_node_fs28.rmSync)(dir, { recursive: true, force: true });
32934
32984
  }
32935
32985
  }
32936
32986
  function formatControl(mode, box, rows) {
@@ -34433,8 +34483,8 @@ function suiteModulePackageAccessCheck(suite, deployModel) {
34433
34483
  if (suite !== "mm-work" || deployModel !== "tenant-container") return void 0;
34434
34484
  return {
34435
34485
  ok: true,
34436
- label: "mmwork-ds package access (manual org-admin step; unverified)",
34437
- detail: "An org admin must add this repository with Read under @mutmutco/mmwork-ds \u2192 Package settings \u2192 Manage Actions access. No GitHub REST API can verify or grant this; without it CI fails with 403 read_package."
34486
+ label: "mmwork-ds + mmwork-screens package access (manual org-admin step; unverified)",
34487
+ detail: `An org admin must add this repository with Read under BOTH @mutmutco/mmwork-ds and @mutmutco/mmwork-screens \u2192 Package settings \u2192 Manage Actions access (#7984). No GitHub REST API can verify or grant this; without it the image build or the screens job fails with 403 read_package. Once screens is green, require it: mmi-cli oracle org project set <owner/repo> --var 'requiredChecks=["gate","screens / screens"]'.`
34438
34488
  };
34439
34489
  }
34440
34490
  function attachTrainDoctorVerdicts(report, trainDoctors) {
@@ -34565,12 +34615,12 @@ async function verifyTargetedSeedPr(repo, number, plan, target, expected, client
34565
34615
  // src/bootstrap-commands.ts
34566
34616
  var execGitForSeedSource = async (args) => (await execFileP("git", args, { timeout: GIT_TIMEOUT_MS })).stdout;
34567
34617
  async function gateRenderDiff(current, rendered) {
34568
- const dir = (0, import_node_fs28.mkdtempSync)((0, import_node_path28.join)((0, import_node_os17.tmpdir)(), "mmi-gate-diff-"));
34618
+ const dir = (0, import_node_fs29.mkdtempSync)((0, import_node_path28.join)((0, import_node_os17.tmpdir)(), "mmi-gate-diff-"));
34569
34619
  const before = (0, import_node_path28.join)(dir, "committed-gate.yml");
34570
34620
  const after = (0, import_node_path28.join)(dir, "registry-render.yml");
34571
34621
  try {
34572
- (0, import_node_fs28.writeFileSync)(before, current);
34573
- (0, import_node_fs28.writeFileSync)(after, rendered);
34622
+ (0, import_node_fs29.writeFileSync)(before, current);
34623
+ (0, import_node_fs29.writeFileSync)(after, rendered);
34574
34624
  try {
34575
34625
  return (await execFileP("git", ["diff", "--no-index", "--", before, after], { timeout: GIT_TIMEOUT_MS })).stdout;
34576
34626
  } catch (e) {
@@ -34579,7 +34629,7 @@ async function gateRenderDiff(current, rendered) {
34579
34629
  throw e;
34580
34630
  }
34581
34631
  } finally {
34582
- (0, import_node_fs28.rmSync)(dir, { recursive: true, force: true });
34632
+ (0, import_node_fs29.rmSync)(dir, { recursive: true, force: true });
34583
34633
  }
34584
34634
  }
34585
34635
  function bootstrapTrainDoctorDeps() {
@@ -34639,13 +34689,13 @@ function registerBootstrapCommands(program3) {
34639
34689
  })),
34640
34690
  pushAllowlistOwners: verifyOwners,
34641
34691
  pushAllowlistOwnersRead: verifyOwnersRead,
34642
- readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs28.existsSync)(path2) ? (0, import_node_fs28.readFileSync)(path2, "utf8") : null,
34692
+ readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs29.existsSync)(path2) ? (0, import_node_fs29.readFileSync)(path2, "utf8") : null,
34643
34693
  // requiredGcpApis is stored as an array by a JSON write, but `org project set --var KEY=VALUE` stores a raw
34644
34694
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
34645
34695
  // #3689: the same committed map the org access audit reads (#3664), so a sanctioned admin is not a
34646
34696
  // permanent bootstrap failure on one surface and an intended state on the other. Absent file → no
34647
34697
  // sanction, which is the pre-#3664 behaviour.
34648
- sanctionedAdmins: (0, import_node_fs28.existsSync)("access-matrix.json") ? entriesValueByCanonicalRepo(loadSanctionedAdmins((0, import_node_fs28.readFileSync)("access-matrix.json", "utf8")), repo) : void 0,
34698
+ sanctionedAdmins: (0, import_node_fs29.existsSync)("access-matrix.json") ? entriesValueByCanonicalRepo(loadSanctionedAdmins((0, import_node_fs29.readFileSync)("access-matrix.json", "utf8")), repo) : void 0,
34649
34699
  requiredGcpApis: (() => {
34650
34700
  const v = meta?.requiredGcpApis;
34651
34701
  if (Array.isArray(v)) return v;
@@ -34723,14 +34773,14 @@ function registerBootstrapCommands(program3) {
34723
34773
  bootstrap.command("drift").description("#3818: compare every org-owned whole-file seed against MMI-Hub's copy across the registry roster; read-only").option("--repo <owner/repo>", "audit one repo instead of the roster (never a fleet verdict)").option("--json", "machine-readable output").action(async () => {
34724
34774
  const o = { repo: rawValue("--repo", ""), json: rawFlag("--json") };
34725
34775
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
34726
- if (!(0, import_node_fs28.existsSync)(manifestPath)) return fail(`bootstrap drift: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the reference this compares against`);
34776
+ if (!(0, import_node_fs29.existsSync)(manifestPath)) return fail(`bootstrap drift: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the reference this compares against`);
34727
34777
  const seedSource = await resolveHubSeedSource(execGitForSeedSource);
34728
34778
  if (!seedSource.ok) return fail(`bootstrap drift: ${seedSource.reason}`);
34729
- const manifest = loadBootstrapSeeds((0, import_node_fs28.readFileSync)(manifestPath, "utf8"));
34779
+ const manifest = loadBootstrapSeeds((0, import_node_fs29.readFileSync)(manifestPath, "utf8"));
34730
34780
  const hubContents = /* @__PURE__ */ new Map();
34731
34781
  for (const s of manifest.seeds) {
34732
34782
  if (s.ownership !== "org" || s.source !== "self") continue;
34733
- hubContents.set(s.target, (0, import_node_fs28.existsSync)(s.target) ? (0, import_node_fs28.readFileSync)(s.target, "utf8") : null);
34783
+ hubContents.set(s.target, (0, import_node_fs29.existsSync)(s.target) ? (0, import_node_fs29.readFileSync)(s.target, "utf8") : null);
34734
34784
  }
34735
34785
  let targets;
34736
34786
  let classOf = (_repo) => "deployable";
@@ -34830,7 +34880,7 @@ function registerBootstrapCommands(program3) {
34830
34880
  }
34831
34881
  if (findings.some((f) => f.state !== "waived")) process.exitCode = 1;
34832
34882
  });
34833
- bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").addOption(new Option("--class <class>", "deployable | content").default("deployable").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--box <name>", "resolve this live box and plan free registry DEPLOY# ports (container deploy models)").option("--domains <stages>", "optional comma-separated dev=host,rc=host,main=host deploy domains (requires --box)").option("--only <target>", "deliver ONLY this manifest target (#3818) \u2014 one file, no labels/ruleset/registry writes; or the special target environments (#5035) to run the deployment-environment reconcile alone (dry-run classifies, --execute writes)").option("--from-config", "with --only gate.yml: adopt an equal legacy gate with a render stamp, then update from registry config via review PR; refuse hand edits").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
34883
+ bootstrap.command("apply <repo>").description("run from the MMI-Hub repo root: idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").addOption(new Option("--class <class>", "deployable | content").default("deployable").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--box <name>", "resolve this live box and plan free registry DEPLOY# ports (container deploy models)").option("--domains <stages>", "optional comma-separated dev=host,rc=host,main=host deploy domains (requires --box)").option("--only <target>", "deliver ONLY this manifest target (#3818) \u2014 one file, no labels/ruleset/registry writes; or the special targets environments (#5035) / product-ruleset (#7986) to reconcile only that live surface (dry-run classifies, --execute writes)").option("--from-config", "with --only gate.yml: adopt an equal legacy gate with a render stamp, then update from registry config via review PR; refuse hand edits").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo, cmdOpts) => {
34834
34884
  const o = {
34835
34885
  class: rawValue("--class", "deployable"),
34836
34886
  projectType: rawValue("--project-type", ""),
@@ -34917,25 +34967,45 @@ function registerBootstrapCommands(program3) {
34917
34967
  }
34918
34968
  return;
34919
34969
  }
34970
+ if (o.only.trim() === "product-ruleset") {
34971
+ if (cmdOpts.var?.length) return fail("bootstrap apply: --only product-ruleset accepts no --var overrides; set registry META instead");
34972
+ const read = await fetchProjectBySlugChecked(parsedRepo.slug, registryClientDeps(await loadConfig()));
34973
+ if (!read.ok) return failGraceful(`bootstrap apply: registry META unreadable (${read.error}); refusing product ruleset reconciliation`);
34974
+ if (!read.project?.requiredChecks?.length) return failGraceful("bootstrap apply: --only product-ruleset requires registry META requiredChecks");
34975
+ const result = await applyCiReconcileRepo(repo, {
34976
+ client: defaultGitHubClient(),
34977
+ listProjects: async () => [],
34978
+ getProjectMeta: async () => read.project
34979
+ }, { productRulesetOnly: true, dryRun: !o.execute });
34980
+ if (o.json) console.log(JSON.stringify(result, null, 2));
34981
+ else {
34982
+ console.log(`bootstrap apply --only product-ruleset \u2014 ${o.execute ? "LIVE" : "dry-run; no writes"} (${repo}):`);
34983
+ result.applied.forEach((line) => console.log(` ${o.execute ? "APPLY" : "PLAN"} ${line}`));
34984
+ result.skipped.forEach((line) => console.log(` SKIP ${line}`));
34985
+ result.errors.forEach((line) => console.error(` REFUSED ${line}`));
34986
+ }
34987
+ if (result.errors.length) return failGraceful("bootstrap apply: product ruleset reconciliation failed");
34988
+ return;
34989
+ }
34920
34990
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
34921
- if (!(0, import_node_fs28.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
34991
+ if (!(0, import_node_fs29.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
34922
34992
  const seedSource = await resolveHubSeedSource(execGitForSeedSource);
34923
34993
  if (!seedSource.ok) return fail(`bootstrap apply: ${seedSource.reason}`);
34924
- const manifest = loadBootstrapSeeds((0, import_node_fs28.readFileSync)(manifestPath, "utf8"));
34994
+ const manifest = loadBootstrapSeeds((0, import_node_fs29.readFileSync)(manifestPath, "utf8"));
34925
34995
  const baseBranch = o.class === "content" ? "main" : "development";
34926
34996
  const slug = parsedRepo.slug;
34927
34997
  const onlyTarget = o.fromConfig && o.only.trim() === "gate.yml" ? ".github/workflows/gate.yml" : o.only.trim();
34928
34998
  const gh2 = async (args) => execFileP("gh", args, { timeout: 2e4 });
34929
- const readFile10 = (p) => (0, import_node_fs28.existsSync)(p) ? (0, import_node_fs28.readFileSync)(p, "utf8") : null;
34999
+ const readFile10 = (p) => (0, import_node_fs29.existsSync)(p) ? (0, import_node_fs29.readFileSync)(p, "utf8") : null;
34930
35000
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
34931
35001
  const putSeed = async (target, content, ref, sha) => {
34932
35002
  const tmp = (0, import_node_path28.join)((0, import_node_os17.tmpdir)(), `mmi-seed-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
34933
- (0, import_node_fs28.writeFileSync)(tmp, JSON.stringify(contentPutBody(target, content, ref, sha)), "utf8");
35003
+ (0, import_node_fs29.writeFileSync)(tmp, JSON.stringify(contentPutBody(target, content, ref, sha)), "utf8");
34934
35004
  try {
34935
35005
  await gh2(contentPutInputArgs(repo, target, tmp));
34936
35006
  } finally {
34937
35007
  try {
34938
- (0, import_node_fs28.unlinkSync)(tmp);
35008
+ (0, import_node_fs29.unlinkSync)(tmp);
34939
35009
  } catch {
34940
35010
  }
34941
35011
  }
@@ -35003,9 +35073,10 @@ function registerBootstrapCommands(program3) {
35003
35073
  const onlyManagedBlock = onlyTarget ? seedsToApply[0]?.managedBlock != null : false;
35004
35074
  if (vars.SUITE === "mm-work" || registryMeta?.suite === "mm-work") {
35005
35075
  if (applyDeployModel === "tenant-container") {
35006
- const moduleGate = withDerivedRepoVars({}, parsedRepo, o.class, effectiveTrack, registryRequiredCheckBranches, ["gate", "screens / screens"]);
35076
+ const requiredChecks = registryMeta?.requiredChecks ?? ["gate"];
35077
+ const moduleGate = withDerivedRepoVars({}, parsedRepo, o.class, effectiveTrack, registryRequiredCheckBranches, requiredChecks);
35007
35078
  vars.GATE_RULESET_CONTEXTS_JSON = moduleGate.GATE_RULESET_CONTEXTS_JSON;
35008
- if (registryMeta) registryMeta = { ...registryMeta, requiredChecks: ["gate", "screens / screens"] };
35079
+ if (registryMeta) registryMeta = { ...registryMeta, requiredChecks };
35009
35080
  }
35010
35081
  }
35011
35082
  const controlClient = defaultGitHubClient();
@@ -35570,10 +35641,10 @@ LIVE apply to ${repo}:
35570
35641
  bootstrap.command("propagate").description("#4238: re-entrant canary\u2192wave tick \u2014 plan (or, with --execute, open) per-repo PRs fanning an org-owned seed out to the fleet").option("--target <path>", "the manifest target to propagate (an org-owned whole file or declared Hub-managed block)").option("--execute", "LIVE tick via gh (master-gated) \u2014 opens/reuses per-repo seed-propagate PRs; dry-run prints the plan only").option("--json", "machine-readable output").action(async () => {
35571
35642
  const o = { target: rawValue("--target", ""), execute: rawFlag("--execute"), json: rawFlag("--json") };
35572
35643
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
35573
- if (!(0, import_node_fs28.existsSync)(manifestPath)) return fail(`bootstrap propagate: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the desired state this tick propagates`);
35644
+ if (!(0, import_node_fs29.existsSync)(manifestPath)) return fail(`bootstrap propagate: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the desired state this tick propagates`);
35574
35645
  const seedSource = await resolveHubSeedSource(execGitForSeedSource);
35575
35646
  if (!seedSource.ok) return fail(`bootstrap propagate: ${seedSource.reason}`);
35576
- const manifest = loadBootstrapSeeds((0, import_node_fs28.readFileSync)(manifestPath, "utf8"));
35647
+ const manifest = loadBootstrapSeeds((0, import_node_fs29.readFileSync)(manifestPath, "utf8"));
35577
35648
  const propagatable = manifest.seeds.filter(isPropagatableSeed);
35578
35649
  if (!o.target) {
35579
35650
  return fail(`bootstrap propagate: --target <path> is required \u2014 one of:
@@ -35582,9 +35653,9 @@ LIVE apply to ${repo}:
35582
35653
  const seed = propagatable.find((s) => s.target === o.target);
35583
35654
  if (!seed) return fail(`bootstrap propagate: --target '${o.target}' names no centrally propagatable seed in ${manifestPath}. Propagatable targets:
35584
35655
  ${propagatable.map((s) => s.target).join("\n ")}`);
35585
- if (!seed.managedBlock && !(0, import_node_fs28.existsSync)(seed.target)) return fail(`bootstrap propagate: the Hub's own copy of '${seed.target}' is missing \u2014 nothing to propagate`);
35586
- const hubContent = seed.managedBlock ? null : (0, import_node_fs28.readFileSync)(seed.target, "utf8");
35587
- const readSeedFile2 = (path2) => (0, import_node_fs28.existsSync)(path2) ? (0, import_node_fs28.readFileSync)(path2, "utf8") : null;
35656
+ if (!seed.managedBlock && !(0, import_node_fs29.existsSync)(seed.target)) return fail(`bootstrap propagate: the Hub's own copy of '${seed.target}' is missing \u2014 nothing to propagate`);
35657
+ const hubContent = seed.managedBlock ? null : (0, import_node_fs29.readFileSync)(seed.target, "utf8");
35658
+ const readSeedFile2 = (path2) => (0, import_node_fs29.existsSync)(path2) ? (0, import_node_fs29.readFileSync)(path2, "utf8") : null;
35588
35659
  const isWorkflowSeed = seed.target.startsWith(".github/workflows/");
35589
35660
  const cfg = await loadConfig();
35590
35661
  const projects = await fetchProjectsList(registryClientDeps(cfg));
@@ -35593,9 +35664,9 @@ LIVE apply to ${repo}:
35593
35664
  }
35594
35665
  const rosterRepos = collectRegistryRepos(projects).filter((r) => r.toLowerCase() !== "mutmutco/mmi-hub");
35595
35666
  let independentCount = rosterRepos.length;
35596
- if ((0, import_node_fs28.existsSync)("projects.json")) {
35667
+ if ((0, import_node_fs29.existsSync)("projects.json")) {
35597
35668
  try {
35598
- const local = JSON.parse((0, import_node_fs28.readFileSync)("projects.json", "utf8"));
35669
+ const local = JSON.parse((0, import_node_fs29.readFileSync)("projects.json", "utf8"));
35599
35670
  const localRepos = /* @__PURE__ */ new Set();
35600
35671
  for (const p of local.projects ?? []) for (const r of p.repos ?? []) {
35601
35672
  const full = (r.includes("/") ? r : `mutmutco/${r}`).toLowerCase();
@@ -35768,12 +35839,12 @@ LIVE apply to ${repo}:
35768
35839
  const tmp = (0, import_node_path28.join)((0, import_node_os17.tmpdir)(), `mmi-propagate-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
35769
35840
  const desiredContent = desiredByRepo.get(rec.repo);
35770
35841
  if (desiredContent == null) return fail(`bootstrap propagate: no resolved content for ${rec.repo} ${seed.target} \u2014 refusing to write`);
35771
- (0, import_node_fs28.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, desiredContent, branch, existingSha)), "utf8");
35842
+ (0, import_node_fs29.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, desiredContent, branch, existingSha)), "utf8");
35772
35843
  try {
35773
35844
  await gh2(contentPutInputArgs(rec.repo, seed.target, tmp));
35774
35845
  } finally {
35775
35846
  try {
35776
- (0, import_node_fs28.unlinkSync)(tmp);
35847
+ (0, import_node_fs29.unlinkSync)(tmp);
35777
35848
  } catch {
35778
35849
  }
35779
35850
  }
@@ -35842,10 +35913,10 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
35842
35913
  return fail(`bootstrap rollback: ${e.message}`);
35843
35914
  }
35844
35915
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
35845
- if (!(0, import_node_fs28.existsSync)(manifestPath)) return fail(`bootstrap rollback: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the manifest names which targets are org-owned and therefore propagated (and rollback-able)`);
35916
+ if (!(0, import_node_fs29.existsSync)(manifestPath)) return fail(`bootstrap rollback: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the manifest names which targets are org-owned and therefore propagated (and rollback-able)`);
35846
35917
  const seedSource = await resolveHubSeedSource(execGitForSeedSource);
35847
35918
  if (!seedSource.ok) return fail(`bootstrap rollback: ${seedSource.reason}`);
35848
- const manifest = loadBootstrapSeeds((0, import_node_fs28.readFileSync)(manifestPath, "utf8"));
35919
+ const manifest = loadBootstrapSeeds((0, import_node_fs29.readFileSync)(manifestPath, "utf8"));
35849
35920
  const propagatable = manifest.seeds.filter(isPropagatableSeed);
35850
35921
  if (!o.target) {
35851
35922
  return fail(`bootstrap rollback: --target <path> is required \u2014 one of:
@@ -35862,10 +35933,10 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
35862
35933
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
35863
35934
  let candidates;
35864
35935
  if (o.record) {
35865
- if (!(0, import_node_fs28.existsSync)(o.record)) return fail(`bootstrap rollback: --record '${o.record}' not found`);
35936
+ if (!(0, import_node_fs29.existsSync)(o.record)) return fail(`bootstrap rollback: --record '${o.record}' not found`);
35866
35937
  let parsed;
35867
35938
  try {
35868
- parsed = JSON.parse((0, import_node_fs28.readFileSync)(o.record, "utf8"));
35939
+ parsed = JSON.parse((0, import_node_fs29.readFileSync)(o.record, "utf8"));
35869
35940
  } catch (e) {
35870
35941
  return fail(`bootstrap rollback: --record '${o.record}' is not valid JSON: ${e.message}`);
35871
35942
  }
@@ -35943,12 +36014,12 @@ Rollback: \`mmi-cli devops bootstrap rollback ${rec.repo} --target ${seed.target
35943
36014
  existingSha = void 0;
35944
36015
  }
35945
36016
  const tmp = (0, import_node_path28.join)((0, import_node_os17.tmpdir)(), `mmi-rollback-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
35946
- (0, import_node_fs28.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, preSeedContent, plan.branch, existingSha)), "utf8");
36017
+ (0, import_node_fs29.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, preSeedContent, plan.branch, existingSha)), "utf8");
35947
36018
  try {
35948
36019
  await gh2(contentPutInputArgs(repo, seed.target, tmp));
35949
36020
  } finally {
35950
36021
  try {
35951
- (0, import_node_fs28.unlinkSync)(tmp);
36022
+ (0, import_node_fs29.unlinkSync)(tmp);
35952
36023
  } catch {
35953
36024
  }
35954
36025
  }
@@ -36404,7 +36475,7 @@ async function waitSuiteDev(deps, opts) {
36404
36475
  }
36405
36476
 
36406
36477
  // src/suite-train.ts
36407
- var import_node_fs29 = require("node:fs");
36478
+ var import_node_fs30 = require("node:fs");
36408
36479
  var import_node_path29 = require("node:path");
36409
36480
  var import_node_crypto14 = require("node:crypto");
36410
36481
 
@@ -36434,6 +36505,17 @@ var SCHEMA = "mmi-suite-cut/v1";
36434
36505
  var SHA2 = /^[a-f0-9]{40}$/i;
36435
36506
  var RC_TAG = /^v\d+\.\d+\.\d+-rc\.\d+$/;
36436
36507
  var RELEASE_TAG = /^v\d+\.\d+\.\d+$/;
36508
+ async function reapplyRulesets(deps, record, after) {
36509
+ for (const member of record.members) {
36510
+ try {
36511
+ member.ruleset = { after, ok: true, lines: await deps.productRuleset(member.repo) };
36512
+ } catch (e) {
36513
+ member.ruleset = { after, ok: false, lines: [e.message] };
36514
+ console.warn(`${member.repo}: product ruleset not re-applied (${e.message}); run mmi-cli devops bootstrap apply ${member.repo} --only product-ruleset --execute from the MMI-Hub checkout`);
36515
+ }
36516
+ writeSuiteCut(deps.recordPath, record);
36517
+ }
36518
+ }
36437
36519
  async function kitCheck(deps, shellSha, bundles, where) {
36438
36520
  const shell2 = shellSha ? await deps.shellKit(shellSha) : void 0;
36439
36521
  const kits = await Promise.all(bundles.map(async (b) => ({ id: b.id, kit: b.url ? await deps.bundleKit(b.url) : void 0 })));
@@ -36480,7 +36562,7 @@ function suiteReleaseTag(rc) {
36480
36562
  function readSuiteCut(path2) {
36481
36563
  let raw;
36482
36564
  try {
36483
- raw = (0, import_node_fs29.readFileSync)(path2, "utf8");
36565
+ raw = (0, import_node_fs30.readFileSync)(path2, "utf8");
36484
36566
  } catch (e) {
36485
36567
  if (e.code === "ENOENT") return void 0;
36486
36568
  throw e;
@@ -36491,16 +36573,16 @@ function readSuiteCut(path2) {
36491
36573
  } catch {
36492
36574
  throw new Error(`suite cut record unreadable at ${path2}; repair it, never recut`);
36493
36575
  }
36494
- if (record.schema !== SCHEMA || record.lead !== SUITE_REPO || !SHA2.test(record.leadDevelopment) || !SHA2.test(record.leadPreviousRc) || !Array.isArray(record.members) || record.members.length === 0 || record.pinPr !== void 0 && (typeof record.pinPr !== "string" || !record.pinPr.startsWith("https://github.com/mutmutco/MM-Work/pull/")) || record.pinsUnchanged !== void 0 && (record.pinsUnchanged !== true || record.pinPr !== void 0) || record.pinMerged !== void 0 && (record.pinMerged !== true && record.pinMerged !== false || !record.pinPr && !record.pinsUnchanged) || record.leadRcTag !== void 0 && (!RC_TAG.test(record.leadRcTag) || !SHA2.test(record.leadRcSha ?? "") || !record.pinMerged) || record.leadReleased !== void 0 && (record.leadReleased !== true || !record.leadRcTag) || record.announcement !== void 0 && (!["pending", "sent", "skipped"].includes(record.announcement) || !record.leadReleased) || record.heldMembers !== void 0 && (!Array.isArray(record.heldMembers) || record.heldMembers.some((h) => !h.id || !h.repo || typeof h.reason !== "string" || !h.reason.trim() || h.since !== void 0 && typeof h.since !== "string" || record.members.some((m) => m.id === h.id))) || record.members.some((m) => !m.id || !m.repo || !m.backend || !m.host || !SHA2.test(m.development) || !SHA2.test(m.previousRc) || m.rcWasAbsent !== void 0 && m.rcWasAbsent !== true || m.tag !== void 0 && (!RC_TAG.test(m.tag) || !SHA2.test(m.sha ?? "")) || m.published !== void 0 && (m.published !== true || !m.tag) || m.released !== void 0 && (m.released !== true || !m.published) || m.mainPublished !== void 0 && (m.mainPublished !== true || !m.released || !!m.carried) || m.backendAfterRelease !== void 0 && (!m.released || typeof m.backendAfterRelease !== "string") || m.carried !== void 0 && (m.carried !== true || m.tag !== void 0 || m.published !== void 0 || !isPin(m.rcPin) || !isPin(m.mainPin)) || m.carried === void 0 && (m.rcPin !== void 0 || m.mainPin !== void 0)))
36576
+ if (record.schema !== SCHEMA || record.lead !== SUITE_REPO || !SHA2.test(record.leadDevelopment) || !SHA2.test(record.leadPreviousRc) || !Array.isArray(record.members) || record.members.length === 0 || record.pinPr !== void 0 && (typeof record.pinPr !== "string" || !record.pinPr.startsWith("https://github.com/mutmutco/MM-Work/pull/")) || record.pinsUnchanged !== void 0 && (record.pinsUnchanged !== true || record.pinPr !== void 0) || record.pinMerged !== void 0 && (record.pinMerged !== true && record.pinMerged !== false || !record.pinPr && !record.pinsUnchanged) || record.leadRcTag !== void 0 && (!RC_TAG.test(record.leadRcTag) || !SHA2.test(record.leadRcSha ?? "") || !record.pinMerged) || record.leadReleased !== void 0 && (record.leadReleased !== true || !record.leadRcTag) || record.announcement !== void 0 && (!["pending", "sent", "skipped"].includes(record.announcement) || !record.leadReleased) || record.heldMembers !== void 0 && (!Array.isArray(record.heldMembers) || record.heldMembers.some((h) => !h.id || !h.repo || typeof h.reason !== "string" || !h.reason.trim() || h.since !== void 0 && typeof h.since !== "string" || record.members.some((m) => m.id === h.id))) || record.members.some((m) => !m.id || !m.repo || !m.backend || !m.host || !SHA2.test(m.development) || !SHA2.test(m.previousRc) || m.rcWasAbsent !== void 0 && m.rcWasAbsent !== true || m.tag !== void 0 && (!RC_TAG.test(m.tag) || !SHA2.test(m.sha ?? "")) || m.published !== void 0 && (m.published !== true || !m.tag) || m.released !== void 0 && (m.released !== true || !m.published) || m.mainPublished !== void 0 && (m.mainPublished !== true || !m.released || !!m.carried) || m.backendAfterRelease !== void 0 && (!m.released || typeof m.backendAfterRelease !== "string") || m.carried !== void 0 && (m.carried !== true || m.tag !== void 0 || m.published !== void 0 || !isPin(m.rcPin) || !isPin(m.mainPin)) || m.carried === void 0 && (m.rcPin !== void 0 || m.mainPin !== void 0) || m.ruleset !== void 0 && (!["rc", "main"].includes(m.ruleset.after) || typeof m.ruleset.ok !== "boolean" || !Array.isArray(m.ruleset.lines))))
36495
36577
  throw new Error(`suite cut record has unknown or invalid state at ${path2}; repair it, never recut`);
36496
36578
  return record;
36497
36579
  }
36498
36580
  function writeSuiteCut(path2, record) {
36499
- (0, import_node_fs29.mkdirSync)((0, import_node_path29.dirname)(path2), { recursive: true });
36581
+ (0, import_node_fs30.mkdirSync)((0, import_node_path29.dirname)(path2), { recursive: true });
36500
36582
  const temp = `${path2}.${(0, import_node_crypto14.randomBytes)(8).toString("hex")}.tmp`;
36501
36583
  try {
36502
- (0, import_node_fs29.writeFileSync)(temp, JSON.stringify(record, null, 2), { flag: "wx" });
36503
- (0, import_node_fs29.renameSync)(temp, path2);
36584
+ (0, import_node_fs30.writeFileSync)(temp, JSON.stringify(record, null, 2), { flag: "wx" });
36585
+ (0, import_node_fs30.renameSync)(temp, path2);
36504
36586
  } catch (e) {
36505
36587
  throw new Error(`suite cut record write failed at ${path2}: ${e.message}; stop, verify the file before retrying`);
36506
36588
  }
@@ -36549,25 +36631,25 @@ async function suiteMarkAbandoned(deps, ownerGo = false) {
36549
36631
  return tags.map((t) => `${t.repo}@${t.tag}`);
36550
36632
  }
36551
36633
  async function retireFailedLeadRcLedger(deps, ownerGo) {
36552
- if (!(0, import_node_fs29.existsSync)(deps.ledgerPath)) return void 0;
36634
+ if (!(0, import_node_fs30.existsSync)(deps.ledgerPath)) return void 0;
36553
36635
  return withLedgerLock(deps.ledgerPath, async (fence) => {
36554
- const ledger = parseReleaseLedger((0, import_node_fs29.readFileSync)(deps.ledgerPath, "utf8"));
36636
+ const ledger = parseReleaseLedger((0, import_node_fs30.readFileSync)(deps.ledgerPath, "utf8"));
36555
36637
  const states = Object.values(ledger.phases).map((p) => p.state);
36556
36638
  if (ledger.repo !== SUITE_REPO || ledger.lane !== "rcand" || ledger.phases.promotion.state === "complete" || !states.includes("failed")) return void 0;
36557
36639
  if (!ownerGo) throw new Error(`the lead's failed rcand ledger for ${ledger.tag} blocks every train; retire it with suite abandon --owner-go`);
36558
36640
  if ((await deps.rcHead())?.toLowerCase() === ledger.tagSha.toLowerCase()) throw new Error(`origin rc already points at ${ledger.tag}; it was promoted, so its ledger is not retired`);
36559
36641
  const archive = (0, import_node_path29.join)((0, import_node_path29.dirname)(deps.ledgerPath), `phases.archive-${ledger.tag}.json`);
36560
- if ((0, import_node_fs29.existsSync)(archive)) throw new Error(`ledger archive already exists at ${archive}; inspect before retiring again`);
36642
+ if ((0, import_node_fs30.existsSync)(archive)) throw new Error(`ledger archive already exists at ${archive}; inspect before retiring again`);
36561
36643
  await deps.markAbandoned(SUITE_REPO, ledger.tag, ledger.tagSha);
36562
- await fence(() => (0, import_node_fs29.renameSync)(deps.ledgerPath, archive));
36644
+ await fence(() => (0, import_node_fs30.renameSync)(deps.ledgerPath, archive));
36563
36645
  return `${ledger.tag} (ledger archived to ${archive})`;
36564
36646
  });
36565
36647
  }
36566
36648
  function suiteAbandon(recordPath, ownerGo = false, now = /* @__PURE__ */ new Date()) {
36567
36649
  const record = abandonableCut(recordPath, ownerGo);
36568
36650
  const archive = `${recordPath}.abandoned-${record.leadDevelopment.slice(0, 12)}-${now.toISOString().replace(/[:.]/g, "")}.json`;
36569
- if ((0, import_node_fs29.existsSync)(archive)) throw new Error(`abandoned-cut archive already exists at ${archive}; inspect before abandoning again`);
36570
- (0, import_node_fs29.renameSync)(recordPath, archive);
36651
+ if ((0, import_node_fs30.existsSync)(archive)) throw new Error(`abandoned-cut archive already exists at ${archive}; inspect before abandoning again`);
36652
+ (0, import_node_fs30.renameSync)(recordPath, archive);
36571
36653
  return { archive, orphanTags: record.members.filter((m) => m.tag).map((m) => `${m.repo}@${m.tag}`) };
36572
36654
  }
36573
36655
  async function suiteRestoreMainPins(deps) {
@@ -36633,12 +36715,12 @@ async function suiteRcand(deps, opts) {
36633
36715
  }
36634
36716
  if ((record?.announcement === "sent" || record?.announcement === "skipped") && record.leadReleased) {
36635
36717
  const archive = `${deps.recordPath}.${record.leadRcTag}.json`;
36636
- if ((0, import_node_fs29.existsSync)(archive)) throw new Error(`suite cut archive already exists at ${archive}; inspect before a new cut`);
36637
- (0, import_node_fs29.renameSync)(deps.recordPath, archive);
36718
+ if ((0, import_node_fs30.existsSync)(archive)) throw new Error(`suite cut archive already exists at ${archive}; inspect before a new cut`);
36719
+ (0, import_node_fs30.renameSync)(deps.recordPath, archive);
36638
36720
  record = void 0;
36639
36721
  }
36640
36722
  if (record && !record.pinPr && !record.leadRcTag && record.members.every((m) => !m.tag)) {
36641
- (0, import_node_fs29.rmSync)(deps.recordPath, { force: true });
36723
+ (0, import_node_fs30.rmSync)(deps.recordPath, { force: true });
36642
36724
  record = void 0;
36643
36725
  }
36644
36726
  if (!record) {
@@ -36692,7 +36774,7 @@ async function suiteRcand(deps, opts) {
36692
36774
  quiet.push(member);
36693
36775
  }
36694
36776
  if (record.members.every((m) => m.carried || quiet.includes(m)) && await deps.contains(SUITE_REPO, record.leadDevelopment, record.leadPreviousRc)) {
36695
- (0, import_node_fs29.rmSync)(deps.recordPath, { force: true });
36777
+ (0, import_node_fs30.rmSync)(deps.recordPath, { force: true });
36696
36778
  throw new Error("nothing to promote: neither MM-Work nor any module has new development commits since its rc; no suite cut started");
36697
36779
  }
36698
36780
  for (const member of quiet) {
@@ -36710,7 +36792,7 @@ async function suiteRcand(deps, opts) {
36710
36792
  }
36711
36793
  if (refusals.length) {
36712
36794
  const untouched = record.members.every((m) => !m.tag) && !record.pinPr;
36713
- if (untouched) (0, import_node_fs29.rmSync)(deps.recordPath, { force: true });
36795
+ if (untouched) (0, import_node_fs30.rmSync)(deps.recordPath, { force: true });
36714
36796
  throw new Error(`module rc serving is not ready; ${untouched ? "no suite cut started" : "no new tag was cut"}:
36715
36797
  ${refusals.join("\n")}
36716
36798
  Each module must ship a live release that bind-mounts <deployPath>/data read-only and serves <data>/rc/module.js at /rc/module.js (docs/Guides/suite-trains.md).`);
@@ -36773,6 +36855,7 @@ Each module must ship a live release that bind-mounts <deployPath>/data read-onl
36773
36855
  record.leadRcSha = leadCut.sha;
36774
36856
  writeSuiteCut(deps.recordPath, record);
36775
36857
  }
36858
+ await reapplyRulesets(deps, record, "rc");
36776
36859
  return { plan, record };
36777
36860
  }
36778
36861
  async function suiteRelease(deps, ownerGo, big = false) {
@@ -36808,6 +36891,10 @@ async function suiteRelease(deps, ownerGo, big = false) {
36808
36891
  throw new Error(`${member.repo}: live backend changed after the cut (${member.backend} \u2192 ${live.version}); release refused`);
36809
36892
  }
36810
36893
  if ((await deps.tagSha(SUITE_REPO, record.leadRcTag))?.toLowerCase() !== record.leadRcSha?.toLowerCase()) throw new Error("MM-Work rc tag changed after the cut; release refused");
36894
+ await deps.validateReleaseSummaries([
36895
+ ...record.members.filter((m) => !m.carried && !m.released && !m.hotfixedTo).map((m) => ({ repo: m.repo, tag: m.releasedAs ?? suiteReleaseTag(m.tag) })),
36896
+ ...!record.leadReleased ? [{ repo: SUITE_REPO, tag: suiteReleaseTag(record.leadRcTag) }] : []
36897
+ ]);
36811
36898
  const publishesMain = !record.leadReleased;
36812
36899
  const refusals = [];
36813
36900
  for (const member of record.members) {
@@ -36839,6 +36926,7 @@ Each member's live release must serve <data>/main/module.js at its main pin path
36839
36926
  writeSuiteCut(deps.recordPath, record);
36840
36927
  }
36841
36928
  }
36929
+ await reapplyRulesets(deps, record, "main");
36842
36930
  if (!record.leadReleased) {
36843
36931
  await deps.release(SUITE_REPO, suiteReleaseTag(record.leadRcTag), record.leadRcSha, { big });
36844
36932
  record.leadReleased = true;
@@ -36865,7 +36953,7 @@ var hotfixRecordPath = (deps) => (0, import_node_path29.join)((0, import_node_pa
36865
36953
  function readSuiteHotfix(path2) {
36866
36954
  let raw;
36867
36955
  try {
36868
- raw = (0, import_node_fs29.readFileSync)(path2, "utf8");
36956
+ raw = (0, import_node_fs30.readFileSync)(path2, "utf8");
36869
36957
  } catch (e) {
36870
36958
  if (e.code === "ENOENT") return void 0;
36871
36959
  throw e;
@@ -36904,11 +36992,11 @@ async function suiteHotfix(deps, opts) {
36904
36992
  if (cut && !cut.leadReleased) throw new Error(`an unfinished suite cut (${cut.leadRcTag ?? "no lead tag"}) is in flight; finish it with suite release or retire it with suite abandon first`);
36905
36993
  const path2 = hotfixRecordPath(deps);
36906
36994
  let record = readSuiteHotfix(path2);
36907
- if (record?.mapped) {
36908
- (0, import_node_fs29.renameSync)(path2, `${path2}.${record.leadTag}.json`);
36909
- record = void 0;
36910
- }
36995
+ const completed = record?.mapped ? record : void 0;
36996
+ if (completed) record = void 0;
36911
36997
  if (record && record.repo !== found.repo) throw new Error(`an unfinished suite hotfix for ${record.repo} is recorded; finish it (rerun with that member) first`);
36998
+ if (!record?.tag) await deps.validateHotfixSummary();
36999
+ if (completed) (0, import_node_fs30.renameSync)(path2, `${path2}.${completed.leadTag}.json`);
36912
37000
  const rec = record ?? { schema: "mmi-suite-hotfix/v1", member: found.id, repo: found.repo, from: opts.from, ...opts.ack?.length ? { ack: opts.ack } : {} };
36913
37001
  const save = () => writeSuiteHotfix(path2, rec);
36914
37002
  save();
@@ -37175,7 +37263,7 @@ async function runRcDataSwitch(deps, opts) {
37175
37263
  }
37176
37264
 
37177
37265
  // src/suite-train-io.ts
37178
- var import_node_fs43 = require("node:fs");
37266
+ var import_node_fs44 = require("node:fs");
37179
37267
  var import_node_path43 = require("node:path");
37180
37268
  init_cli_shared();
37181
37269
 
@@ -38237,6 +38325,7 @@ async function runHotfixReleaseLocked(deps, versionInput, options, doctor) {
38237
38325
  await assertConsumerBomLineage(deps, mergedSha, tag);
38238
38326
  }
38239
38327
  const releaseExists = await hotfixReleaseExists(deps, ctx, tag);
38328
+ if (!releaseExists && meta.suite === "mm-work") await requireSuiteReleaseSummary(deps, options.announceSummaryFile);
38240
38329
  if (!releaseExists && isHubControlRepo(ctx.repo)) {
38241
38330
  if (!options.announceSummaryFile) {
38242
38331
  throw new Error("a new MMI-Hub hotfix Release requires --announce-summary-file <path> with 3-6 short LLM-curated lines; create the summary file, then rerun the same hotfix release command \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#announce-summary-missing");
@@ -38852,7 +38941,7 @@ function checkHotfixCarries(options) {
38852
38941
  }
38853
38942
 
38854
38943
  // src/merge-cleanup.ts
38855
- var import_node_fs31 = require("node:fs");
38944
+ var import_node_fs32 = require("node:fs");
38856
38945
  var import_node_path31 = require("node:path");
38857
38946
  var import_node_os18 = require("node:os");
38858
38947
  init_cli_shared();
@@ -39168,7 +39257,7 @@ async function runPrLand(prNumber, options, deps) {
39168
39257
 
39169
39258
  // src/test-policy-core.ts
39170
39259
  var import_node_child_process13 = require("node:child_process");
39171
- var import_node_fs30 = require("node:fs");
39260
+ var import_node_fs31 = require("node:fs");
39172
39261
  var import_node_path30 = require("node:path");
39173
39262
 
39174
39263
  // src/test-command-policy-shared.mjs
@@ -39722,7 +39811,7 @@ function loadPolicySource(root, explicitRef, diffBase) {
39722
39811
  }
39723
39812
  function readFileOrNull2(path2) {
39724
39813
  try {
39725
- return (0, import_node_fs30.readFileSync)(path2, "utf8");
39814
+ return (0, import_node_fs31.readFileSync)(path2, "utf8");
39726
39815
  } catch {
39727
39816
  return null;
39728
39817
  }
@@ -39750,10 +39839,10 @@ function classify(changed, policy, present = () => false) {
39750
39839
  const removedProtected = [...removed].filter((p) => protectedBy.has(p)).map((p) => ({ path: p, why: protectedBy.get(p) ?? "" }));
39751
39840
  return { mandatoryHits, untestedHits, testChanges, meaningfulTestChanges, addedTests, removedProtected };
39752
39841
  }
39753
- function unresolvedProtectedEntries(policy, root, exists = (path2) => (0, import_node_fs30.existsSync)(path2)) {
39842
+ function unresolvedProtectedEntries(policy, root, exists = (path2) => (0, import_node_fs31.existsSync)(path2)) {
39754
39843
  return (policy.protected ?? []).map((p) => p.path).filter((p) => !exists((0, import_node_path30.join)(root, p)));
39755
39844
  }
39756
- function unresolvedSatisfiers(policy, root, exists = (path2) => (0, import_node_fs30.existsSync)(path2)) {
39845
+ function unresolvedSatisfiers(policy, root, exists = (path2) => (0, import_node_fs31.existsSync)(path2)) {
39757
39846
  const declared = (policy.mandatory ?? []).flatMap((m) => m.satisfiedBy ?? []);
39758
39847
  return [...new Set(declared)].filter((p) => !exists((0, import_node_path30.join)(root, p)));
39759
39848
  }
@@ -39894,7 +39983,7 @@ function runTestPolicy(root, deps = {}) {
39894
39983
  }
39895
39984
  const loaded = deps.policy ? { policy: deps.policy, source: { ref: null, sha: null, path: POLICY_FILE } } : loadPolicySource(root, deps.policyRef, deps.base);
39896
39985
  const { policy, source: policySource } = loaded;
39897
- const exists = deps.exists ?? ((path2) => (0, import_node_fs30.existsSync)(path2));
39986
+ const exists = deps.exists ?? ((path2) => (0, import_node_fs31.existsSync)(path2));
39898
39987
  const counts = { mandatoryCount: (policy.mandatory ?? []).length, protectedCount: (policy.protected ?? []).length };
39899
39988
  const base = deps.changed ? "(injected)" : deps.policyRef ? resolveHotfixBase(root) : resolveBase(root, deps.base);
39900
39989
  const refusal = deps.changed ? null : untrustworthyRange(root, base);
@@ -40192,13 +40281,13 @@ function resolveSquashMergeBodyText(commits, allowedClosing, cwd) {
40192
40281
  return rewritten === base ? null : rewritten;
40193
40282
  }
40194
40283
  function writeSquashBodyFile(body) {
40195
- const dir = (0, import_node_fs31.mkdtempSync)((0, import_node_path31.join)((0, import_node_os18.tmpdir)(), "mmi-squash-body-"));
40284
+ const dir = (0, import_node_fs32.mkdtempSync)((0, import_node_path31.join)((0, import_node_os18.tmpdir)(), "mmi-squash-body-"));
40196
40285
  const path2 = (0, import_node_path31.join)(dir, "body.txt");
40197
- (0, import_node_fs31.writeFileSync)(path2, body.endsWith("\n") ? body : `${body}
40286
+ (0, import_node_fs32.writeFileSync)(path2, body.endsWith("\n") ? body : `${body}
40198
40287
  `, "utf8");
40199
40288
  return { path: path2, cleanup: () => {
40200
40289
  try {
40201
- (0, import_node_fs31.rmSync)(dir, { recursive: true, force: true });
40290
+ (0, import_node_fs32.rmSync)(dir, { recursive: true, force: true });
40202
40291
  } catch {
40203
40292
  }
40204
40293
  } };
@@ -40405,7 +40494,7 @@ async function deleteMergedRemoteBranch(options) {
40405
40494
  }
40406
40495
 
40407
40496
  // src/project-info-sync.ts
40408
- var import_node_fs32 = require("node:fs");
40497
+ var import_node_fs33 = require("node:fs");
40409
40498
  var import_node_path32 = require("node:path");
40410
40499
  var UPDATE_PROJECT_INFO = `mutation($projectId: ID!, $shortDescription: String!, $readme: String!) {
40411
40500
  updateProjectV2(input: { projectId: $projectId, shortDescription: $shortDescription, readme: $readme }) {
@@ -40454,13 +40543,13 @@ function sharedName(entries, fallback) {
40454
40543
  function buildProjectInfoSyncPlan(targetRepo2, project2, projects, repoRoot2) {
40455
40544
  if (!project2.projectId) throw new Error(`org project sync-info: ${targetRepo2} registry META has no projectId`);
40456
40545
  const readmePath = (0, import_node_path32.join)(repoRoot2, "README.md");
40457
- if (!(0, import_node_fs32.existsSync)(readmePath)) throw new Error(`org project sync-info: ${targetRepo2} has no README.md`);
40546
+ if (!(0, import_node_fs33.existsSync)(readmePath)) throw new Error(`org project sync-info: ${targetRepo2} has no README.md`);
40458
40547
  const entries = entriesFor(project2, projects);
40459
40548
  const memberRepos = [...new Set(entries.flatMap((entry) => entry.repos ?? []))].filter((repo) => /^[^/]+\/[^/]+$/.test(repo)).sort((a, b) => a.localeCompare(b));
40460
40549
  const projectName = sharedName(entries, project2.name?.trim() || targetRepo2.split("/").pop() || targetRepo2);
40461
40550
  if (!memberRepos.length) throw new Error(`org project sync-info: project ${projectName} has no registered member repos`);
40462
40551
  const entryNames = entries.map((entry) => entry.name?.trim()).filter((name) => Boolean(name));
40463
- const shortDescription = memberRepos.length === 1 ? shortDescriptionFromReadme((0, import_node_fs32.readFileSync)(readmePath, "utf8")) : `Shared work across ${new Intl.ListFormat("en", { type: "conjunction" }).format(entryNames)}.`;
40552
+ const shortDescription = memberRepos.length === 1 ? shortDescriptionFromReadme((0, import_node_fs33.readFileSync)(readmePath, "utf8")) : `Shared work across ${new Intl.ListFormat("en", { type: "conjunction" }).format(entryNames)}.`;
40464
40553
  const lines2 = [
40465
40554
  `# ${projectName}`,
40466
40555
  "",
@@ -40479,8 +40568,8 @@ function buildProjectInfoSyncPlan(targetRepo2, project2, projects, repoRoot2) {
40479
40568
  const targetBase = `https://github.com/${targetRepo2}`;
40480
40569
  const targetBranch = branchFor(targetRepo2, projects);
40481
40570
  const orgDocs = [
40482
- (0, import_node_fs32.existsSync)((0, import_node_path32.join)(repoRoot2, "architecture.md")) ? `- [Hub architecture](${targetBase}/blob/${targetBranch}/architecture.md)` : "",
40483
- (0, import_node_fs32.existsSync)((0, import_node_path32.join)(repoRoot2, "docs", "Architecture", "agentic-dev-environment.md")) ? `- [Agentic development environment](${targetBase}/blob/${targetBranch}/docs/Architecture/agentic-dev-environment.md)` : ""
40571
+ (0, import_node_fs33.existsSync)((0, import_node_path32.join)(repoRoot2, "architecture.md")) ? `- [Hub architecture](${targetBase}/blob/${targetBranch}/architecture.md)` : "",
40572
+ (0, import_node_fs33.existsSync)((0, import_node_path32.join)(repoRoot2, "docs", "Architecture", "agentic-dev-environment.md")) ? `- [Agentic development environment](${targetBase}/blob/${targetBranch}/docs/Architecture/agentic-dev-environment.md)` : ""
40484
40573
  ].filter(Boolean);
40485
40574
  if (orgDocs.length) lines2.push("", "## Organisation docs", "", ...orgDocs);
40486
40575
  return { projectId: project2.projectId, projectName, targetRepo: targetRepo2, memberRepos, shortDescription, readme: `${lines2.join("\n")}
@@ -40502,7 +40591,7 @@ async function syncProjectInfo(plan, client, apply) {
40502
40591
  }
40503
40592
 
40504
40593
  // src/train-commands.ts
40505
- var import_node_fs33 = require("node:fs");
40594
+ var import_node_fs34 = require("node:fs");
40506
40595
  var import_node_path33 = require("node:path");
40507
40596
  init_cli_shared();
40508
40597
  init_clean_exit();
@@ -40518,7 +40607,7 @@ function resolveReleaseBumpIntent(raw) {
40518
40607
  }
40519
40608
  function readRepoVersion() {
40520
40609
  try {
40521
- return JSON.parse((0, import_node_fs33.readFileSync)((0, import_node_path33.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
40610
+ return JSON.parse((0, import_node_fs34.readFileSync)((0, import_node_path33.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
40522
40611
  } catch {
40523
40612
  return void 0;
40524
40613
  }
@@ -40883,7 +40972,7 @@ async function openSuitePinPr(deps, input) {
40883
40972
 
40884
40973
  // src/command-register-collaboration.ts
40885
40974
  var import_node_child_process17 = require("node:child_process");
40886
- var import_node_fs42 = require("node:fs");
40975
+ var import_node_fs43 = require("node:fs");
40887
40976
  var import_promises8 = require("node:fs/promises");
40888
40977
  init_clean_exit();
40889
40978
  init_cli_shared();
@@ -41017,7 +41106,7 @@ init_github_client();
41017
41106
  init_github_quota();
41018
41107
 
41019
41108
  // src/issue-commands.ts
41020
- var import_node_fs34 = require("node:fs");
41109
+ var import_node_fs35 = require("node:fs");
41021
41110
  var import_node_crypto15 = require("node:crypto");
41022
41111
  init_cli_shared();
41023
41112
  init_clean_exit();
@@ -41210,7 +41299,7 @@ async function editIssue(client, options, deps = {}) {
41210
41299
  const url = `https://github.com/${repo}/issues/${parsed.number}`;
41211
41300
  const patch = {};
41212
41301
  let bodyChanged = false;
41213
- const textDeps = () => deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs34.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
41302
+ const textDeps = () => deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs35.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
41214
41303
  if (options.titleFile !== void 0) {
41215
41304
  patch.title = await resolveIssueTitle({ title: options.title, titleFile: options.titleFile }, textDeps());
41216
41305
  } else if (options.title !== void 0) {
@@ -41873,7 +41962,7 @@ function extendCreateCommand(issue, batchAttach) {
41873
41962
  if (opts.batch) {
41874
41963
  let specs;
41875
41964
  try {
41876
- const raw = (0, import_node_fs34.readFileSync)(opts.batch, "utf8");
41965
+ const raw = (0, import_node_fs35.readFileSync)(opts.batch, "utf8");
41877
41966
  specs = JSON.parse(raw);
41878
41967
  if (!Array.isArray(specs)) {
41879
41968
  const top = specs === null ? "null" : typeof specs === "object" ? `an object with keys: ${Object.keys(specs).join(", ") || "(none)"}` : `a ${typeof specs}`;
@@ -41950,7 +42039,7 @@ ${lines2}`, {
41950
42039
  }
41951
42040
 
41952
42041
  // src/post-merge-recon.ts
41953
- var import_node_fs35 = require("node:fs");
42042
+ var import_node_fs36 = require("node:fs");
41954
42043
  var import_node_path35 = require("node:path");
41955
42044
 
41956
42045
  // src/cross-repo-filing-issue.ts
@@ -42120,16 +42209,16 @@ function buildPostMergeReconRecovery(input) {
42120
42209
  }
42121
42210
  function writePostMergeReconRecovery(cwd, recovery) {
42122
42211
  const path2 = postMergeReconStatePath(cwd, recovery.repo, recovery.pr);
42123
- (0, import_node_fs35.mkdirSync)((0, import_node_path35.dirname)(path2), { recursive: true });
42124
- (0, import_node_fs35.writeFileSync)(path2, `${JSON.stringify(recovery, null, 2)}
42212
+ (0, import_node_fs36.mkdirSync)((0, import_node_path35.dirname)(path2), { recursive: true });
42213
+ (0, import_node_fs36.writeFileSync)(path2, `${JSON.stringify(recovery, null, 2)}
42125
42214
  `, "utf8");
42126
42215
  return path2;
42127
42216
  }
42128
42217
  function clearPostMergeReconRecovery(cwd, repo, pr) {
42129
42218
  const path2 = postMergeReconStatePath(cwd, repo, pr);
42130
- if (!(0, import_node_fs35.existsSync)(path2)) return;
42219
+ if (!(0, import_node_fs36.existsSync)(path2)) return;
42131
42220
  try {
42132
- (0, import_node_fs35.unlinkSync)(path2);
42221
+ (0, import_node_fs36.unlinkSync)(path2);
42133
42222
  } catch {
42134
42223
  }
42135
42224
  }
@@ -42605,7 +42694,7 @@ function registerPrLifecycleCommands(program3) {
42605
42694
 
42606
42695
  // src/review-verdict.ts
42607
42696
  var import_node_child_process14 = require("node:child_process");
42608
- var import_node_fs36 = require("node:fs");
42697
+ var import_node_fs37 = require("node:fs");
42609
42698
  var import_node_os20 = require("node:os");
42610
42699
  var import_node_path36 = require("node:path");
42611
42700
  init_cli_shared();
@@ -42772,15 +42861,15 @@ async function readPrIssueComments(number, repo) {
42772
42861
  return comments;
42773
42862
  }
42774
42863
  async function postPrCommentFromFile(number, repo, body) {
42775
- const dir = (0, import_node_fs36.mkdtempSync)((0, import_node_path36.join)((0, import_node_os20.tmpdir)(), "mmi-review-verdict-"));
42864
+ const dir = (0, import_node_fs37.mkdtempSync)((0, import_node_path36.join)((0, import_node_os20.tmpdir)(), "mmi-review-verdict-"));
42776
42865
  const path2 = (0, import_node_path36.join)(dir, "body.md");
42777
42866
  try {
42778
- (0, import_node_fs36.writeFileSync)(path2, body, "utf8");
42867
+ (0, import_node_fs37.writeFileSync)(path2, body, "utf8");
42779
42868
  const { stdout } = await execFileP("gh", ["pr", "comment", number, "--repo", repo, "--body-file", path2], { timeout: GH_MUTATION_TIMEOUT_MS });
42780
42869
  return stdout.trim();
42781
42870
  } finally {
42782
42871
  try {
42783
- (0, import_node_fs36.rmSync)(dir, { recursive: true, force: true });
42872
+ (0, import_node_fs37.rmSync)(dir, { recursive: true, force: true });
42784
42873
  } catch {
42785
42874
  }
42786
42875
  }
@@ -43014,17 +43103,17 @@ async function prCreateClaimRefusal(body, repoOption, deps = {}) {
43014
43103
  }
43015
43104
 
43016
43105
  // src/worktree-residue-sweep.ts
43017
- var import_node_fs41 = require("node:fs");
43106
+ var import_node_fs42 = require("node:fs");
43018
43107
  var import_node_path41 = require("node:path");
43019
43108
 
43020
43109
  // src/worktree-merge-cleanup.ts
43021
- var import_node_fs40 = require("node:fs");
43110
+ var import_node_fs41 = require("node:fs");
43022
43111
  var import_node_path40 = require("node:path");
43023
43112
  init_cli_shared();
43024
43113
 
43025
43114
  // src/stage-runner.ts
43026
43115
  var import_node_child_process16 = require("node:child_process");
43027
- var import_node_fs38 = require("node:fs");
43116
+ var import_node_fs39 = require("node:fs");
43028
43117
  var import_node_path38 = require("node:path");
43029
43118
  var import_node_net = require("node:net");
43030
43119
  var import_node_util5 = require("node:util");
@@ -43035,7 +43124,7 @@ function normalizeEol2(s) {
43035
43124
  }
43036
43125
 
43037
43126
  // src/env-detection.ts
43038
- var import_node_fs37 = require("node:fs");
43127
+ var import_node_fs38 = require("node:fs");
43039
43128
  var import_node_path37 = require("node:path");
43040
43129
  var SKIPPED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", ".jerv", ".pi", ".venv", "venv"]);
43041
43130
  function isEnvBaseName(name) {
@@ -43046,7 +43135,7 @@ function findEnvFiles(root) {
43046
43135
  const walk2 = (dir, rel) => {
43047
43136
  let entries;
43048
43137
  try {
43049
- entries = (0, import_node_fs37.readdirSync)(dir, { withFileTypes: true });
43138
+ entries = (0, import_node_fs38.readdirSync)(dir, { withFileTypes: true });
43050
43139
  } catch {
43051
43140
  return;
43052
43141
  }
@@ -43060,7 +43149,7 @@ function findEnvFiles(root) {
43060
43149
  if (isEnvBaseName(entry.name)) {
43061
43150
  let isDir = false;
43062
43151
  try {
43063
- isDir = (0, import_node_fs37.lstatSync)((0, import_node_path37.join)(dir, entry.name)).isDirectory();
43152
+ isDir = (0, import_node_fs38.lstatSync)((0, import_node_path37.join)(dir, entry.name)).isDirectory();
43064
43153
  } catch {
43065
43154
  }
43066
43155
  if (!isDir) found.push(relPath);
@@ -43173,12 +43262,12 @@ var DOCKER_TIMEOUT_MS = 15e3;
43173
43262
  function stageBash() {
43174
43263
  if (process.platform !== "win32") return "bash";
43175
43264
  const configured = process.env.SHELL;
43176
- if (configured && (0, import_node_path38.isAbsolute)(configured) && /[/\\]bash(?:\.exe)?$/i.test(configured) && !/[/\\](?:System32|Sysnative)[/\\]bash(?:\.exe)?$/i.test(configured) && (0, import_node_fs38.existsSync)(configured)) return configured;
43265
+ if (configured && (0, import_node_path38.isAbsolute)(configured) && /[/\\]bash(?:\.exe)?$/i.test(configured) && !/[/\\](?:System32|Sysnative)[/\\]bash(?:\.exe)?$/i.test(configured) && (0, import_node_fs39.existsSync)(configured)) return configured;
43177
43266
  const candidates = [
43178
43267
  (0, import_node_path38.join)(process.env.ProgramFiles || "C:\\Program Files", "Git", "bin", "bash.exe"),
43179
43268
  ...process.env.LOCALAPPDATA ? [(0, import_node_path38.join)(process.env.LOCALAPPDATA, "Programs", "Git", "bin", "bash.exe")] : []
43180
43269
  ];
43181
- const bash = candidates.find((path2) => (0, import_node_fs38.existsSync)(path2));
43270
+ const bash = candidates.find((path2) => (0, import_node_fs39.existsSync)(path2));
43182
43271
  if (!bash) throw new Error("Local stages require Git Bash on Windows. Install Git for Windows or set SHELL to its absolute bash.exe path.");
43183
43272
  return bash;
43184
43273
  }
@@ -43534,7 +43623,7 @@ function stageProcessEnv(stagePort, extraEnv) {
43534
43623
  function composeResolvesPort(cwd) {
43535
43624
  if (process.env.PORT) return true;
43536
43625
  const envFile = (0, import_node_path38.join)(cwd, ".env");
43537
- return (0, import_node_fs38.existsSync)(envFile) && envFileKeys((0, import_node_fs38.readFileSync)(envFile, "utf8")).has("PORT");
43626
+ return (0, import_node_fs39.existsSync)(envFile) && envFileKeys((0, import_node_fs39.readFileSync)(envFile, "utf8")).has("PORT");
43538
43627
  }
43539
43628
  function stageComposeEnv(config, stagePort, vaultEnvMerge, cwd) {
43540
43629
  const composeProject = config.env?.COMPOSE_PROJECT_NAME ?? vaultEnvMerge?.COMPOSE_PROJECT_NAME ?? stageComposeProjectName(cwd);
@@ -43549,13 +43638,13 @@ function stageComposeEnv(config, stagePort, vaultEnvMerge, cwd) {
43549
43638
  function stageComposeFiles(cwd) {
43550
43639
  const files = ["docker-compose.yml"];
43551
43640
  for (const conventional of ["docker-compose.override.yml", "docker-compose.override.yaml"]) {
43552
- if ((0, import_node_fs38.existsSync)((0, import_node_path38.join)(cwd, conventional))) files.push(conventional);
43641
+ if ((0, import_node_fs39.existsSync)((0, import_node_path38.join)(cwd, conventional))) files.push(conventional);
43553
43642
  }
43554
43643
  return [...files, RUNTIME_ENV_OVERRIDE_RELPATH];
43555
43644
  }
43556
43645
  async function prepareRuntimeEnvPassthrough(cwd, composeEnv, vaultEnvMerge) {
43557
43646
  const keys = Object.keys(vaultEnvMerge ?? {});
43558
- if (!keys.length || !(0, import_node_fs38.existsSync)((0, import_node_path38.join)(cwd, "docker-compose.yml"))) return {};
43647
+ if (!keys.length || !(0, import_node_fs39.existsSync)((0, import_node_path38.join)(cwd, "docker-compose.yml"))) return {};
43559
43648
  let configJson;
43560
43649
  try {
43561
43650
  const { stdout } = await execFileP3("docker", ["compose", "config", "--format", "json"], {
@@ -43572,20 +43661,20 @@ async function prepareRuntimeEnvPassthrough(cwd, composeEnv, vaultEnvMerge) {
43572
43661
  const body = renderRuntimeEnvOverride(runtimeEnvMarkedServices(configJson), keys, runtimeEnvExcludes(configJson));
43573
43662
  if (!body) return {};
43574
43663
  const overridePath = (0, import_node_path38.join)(cwd, RUNTIME_ENV_OVERRIDE_RELPATH);
43575
- (0, import_node_fs38.mkdirSync)((0, import_node_path38.join)(cwd, "tmp", "stage"), { recursive: true });
43576
- (0, import_node_fs38.writeFileSync)(overridePath, body, "utf8");
43664
+ (0, import_node_fs39.mkdirSync)((0, import_node_path38.join)(cwd, "tmp", "stage"), { recursive: true });
43665
+ (0, import_node_fs39.writeFileSync)(overridePath, body, "utf8");
43577
43666
  return stageComposeFileEnv(stageComposeFiles(cwd));
43578
43667
  }
43579
43668
  async function ensureStageRuntimeEnv(config, opts, cwd) {
43580
43669
  if (!config.ensureEnv) return;
43581
43670
  const target = (0, import_node_path38.join)(cwd, config.ensureEnv.target);
43582
43671
  const example = (0, import_node_path38.join)(cwd, config.ensureEnv.example);
43583
- if (!(0, import_node_fs38.existsSync)(target) && (0, import_node_fs38.existsSync)(example)) {
43584
- (0, import_node_fs38.copyFileSync)(example, target);
43585
- } else if ((0, import_node_fs38.existsSync)(target) && (0, import_node_fs38.existsSync)(example)) {
43586
- const stale = detectStaleEnvFile((0, import_node_fs38.readFileSync)(example, "utf8"), (0, import_node_fs38.readFileSync)(target, "utf8"), {
43587
- exampleMtimeMs: (0, import_node_fs38.statSync)(example).mtimeMs,
43588
- targetMtimeMs: (0, import_node_fs38.statSync)(target).mtimeMs
43672
+ if (!(0, import_node_fs39.existsSync)(target) && (0, import_node_fs39.existsSync)(example)) {
43673
+ (0, import_node_fs39.copyFileSync)(example, target);
43674
+ } else if ((0, import_node_fs39.existsSync)(target) && (0, import_node_fs39.existsSync)(example)) {
43675
+ const stale = detectStaleEnvFile((0, import_node_fs39.readFileSync)(example, "utf8"), (0, import_node_fs39.readFileSync)(target, "utf8"), {
43676
+ exampleMtimeMs: (0, import_node_fs39.statSync)(example).mtimeMs,
43677
+ targetMtimeMs: (0, import_node_fs39.statSync)(target).mtimeMs
43589
43678
  });
43590
43679
  if (stale) {
43591
43680
  const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
@@ -43593,8 +43682,8 @@ async function ensureStageRuntimeEnv(config, opts, cwd) {
43593
43682
  console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
43594
43683
  }
43595
43684
  }
43596
- if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs38.existsSync)(target)) {
43597
- (0, import_node_fs38.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs38.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
43685
+ if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs39.existsSync)(target)) {
43686
+ (0, import_node_fs39.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs39.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
43598
43687
  }
43599
43688
  }
43600
43689
  async function gitText(cwd, args) {
@@ -43624,20 +43713,20 @@ async function resolveGlobalStatePath(cwd, explicit) {
43624
43713
  return void 0;
43625
43714
  }
43626
43715
  function readState(path2) {
43627
- if (!(0, import_node_fs38.existsSync)(path2)) return null;
43716
+ if (!(0, import_node_fs39.existsSync)(path2)) return null;
43628
43717
  try {
43629
- return JSON.parse((0, import_node_fs38.readFileSync)(path2, "utf8"));
43718
+ return JSON.parse((0, import_node_fs39.readFileSync)(path2, "utf8"));
43630
43719
  } catch {
43631
43720
  return null;
43632
43721
  }
43633
43722
  }
43634
43723
  function mkdirFor(path2) {
43635
43724
  const dir = path2.slice(0, Math.max(path2.lastIndexOf("/"), path2.lastIndexOf("\\")));
43636
- (0, import_node_fs38.mkdirSync)(dir, { recursive: true });
43725
+ (0, import_node_fs39.mkdirSync)(dir, { recursive: true });
43637
43726
  }
43638
43727
  function writeState(path2, state) {
43639
43728
  mkdirFor(path2);
43640
- (0, import_node_fs38.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
43729
+ (0, import_node_fs39.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
43641
43730
  }
43642
43731
  function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
43643
43732
  const reservation = {
@@ -43666,7 +43755,7 @@ async function cleanupStageState(state, paths, timeoutMs, fallbackCwd, currentEn
43666
43755
  );
43667
43756
  }
43668
43757
  for (const path2 of [...new Set(paths.filter((p) => Boolean(p)))]) {
43669
- (0, import_node_fs38.rmSync)(path2, { force: true });
43758
+ (0, import_node_fs39.rmSync)(path2, { force: true });
43670
43759
  }
43671
43760
  }
43672
43761
  async function killTree(pid) {
@@ -43913,8 +44002,8 @@ async function runStage(config = {}, opts = {}) {
43913
44002
  });
43914
44003
  }
43915
44004
  } catch (e) {
43916
- (0, import_node_fs38.rmSync)(statePath, { force: true });
43917
- if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs38.rmSync)(globalStatePath, { force: true });
44005
+ (0, import_node_fs39.rmSync)(statePath, { force: true });
44006
+ if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs39.rmSync)(globalStatePath, { force: true });
43918
44007
  throw e;
43919
44008
  }
43920
44009
  const started = await startStage(config, {
@@ -43929,7 +44018,7 @@ async function runStage(config = {}, opts = {}) {
43929
44018
  }
43930
44019
 
43931
44020
  // src/worktree-evidence-archive.ts
43932
- var import_node_fs39 = require("node:fs");
44021
+ var import_node_fs40 = require("node:fs");
43933
44022
  var import_node_path39 = require("node:path");
43934
44023
  var JERV_ARTIFACT_RUN_SCOPE_ENV_VARS = ["JERV_RUN_ID", ...SESSION_ID_ENV_VARS];
43935
44024
  function sanitizeArchiveSegment(value, max = 80) {
@@ -43945,17 +44034,17 @@ function resolveArtifactRunScope(env = process.env) {
43945
44034
  }
43946
44035
  function defaultIsDirectory(path2) {
43947
44036
  try {
43948
- return (0, import_node_fs39.lstatSync)(path2).isDirectory();
44037
+ return (0, import_node_fs40.lstatSync)(path2).isDirectory();
43949
44038
  } catch {
43950
44039
  return false;
43951
44040
  }
43952
44041
  }
43953
44042
  function archiveWorktreeJervArtifacts(args, deps = {}) {
43954
- const exists = deps.exists ?? import_node_fs39.existsSync;
44043
+ const exists = deps.exists ?? import_node_fs40.existsSync;
43955
44044
  const isDirectory = deps.isDirectory ?? defaultIsDirectory;
43956
- const copyDir = deps.copyDir ?? ((from, to) => (0, import_node_fs39.cpSync)(from, to, { recursive: true, force: true }));
44045
+ const copyDir = deps.copyDir ?? ((from, to) => (0, import_node_fs40.cpSync)(from, to, { recursive: true, force: true }));
43957
44046
  const mkdirp = deps.mkdirp ?? ((path2) => {
43958
- (0, import_node_fs39.mkdirSync)(path2, { recursive: true });
44047
+ (0, import_node_fs40.mkdirSync)(path2, { recursive: true });
43959
44048
  });
43960
44049
  const env = deps.env ?? process.env;
43961
44050
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
@@ -43979,13 +44068,13 @@ function archiveWorktreeJervArtifacts(args, deps = {}) {
43979
44068
  }
43980
44069
  }
43981
44070
  function defaultStat(path2) {
43982
- const st = (0, import_node_fs39.statSync)(path2);
44071
+ const st = (0, import_node_fs40.statSync)(path2);
43983
44072
  return { mtimeMs: st.mtimeMs, size: st.size, isDirectory: () => st.isDirectory() };
43984
44073
  }
43985
44074
  function scanWorktreeTmpEvidence(worktreePath, newerThanMs, deps = {}) {
43986
- const exists = deps.exists ?? import_node_fs39.existsSync;
44075
+ const exists = deps.exists ?? import_node_fs40.existsSync;
43987
44076
  const stat4 = deps.stat ?? defaultStat;
43988
- const readdir2 = deps.readdir ?? import_node_fs39.readdirSync;
44077
+ const readdir2 = deps.readdir ?? import_node_fs40.readdirSync;
43989
44078
  const tmpRoot = (0, import_node_path39.join)(worktreePath, "tmp");
43990
44079
  if (!exists(tmpRoot)) return [];
43991
44080
  const entries = [];
@@ -44018,10 +44107,10 @@ function scanWorktreeTmpEvidence(worktreePath, newerThanMs, deps = {}) {
44018
44107
  return entries;
44019
44108
  }
44020
44109
  function archiveWorktreeTmpArtifacts(args, deps = {}) {
44021
- const exists = deps.exists ?? import_node_fs39.existsSync;
44022
- const copyDir = deps.copyDir ?? ((from, to) => (0, import_node_fs39.cpSync)(from, to, { recursive: true, force: true }));
44110
+ const exists = deps.exists ?? import_node_fs40.existsSync;
44111
+ const copyDir = deps.copyDir ?? ((from, to) => (0, import_node_fs40.cpSync)(from, to, { recursive: true, force: true }));
44023
44112
  const mkdirp = deps.mkdirp ?? ((path2) => {
44024
- (0, import_node_fs39.mkdirSync)(path2, { recursive: true });
44113
+ (0, import_node_fs40.mkdirSync)(path2, { recursive: true });
44025
44114
  });
44026
44115
  const env = deps.env ?? process.env;
44027
44116
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
@@ -44108,7 +44197,7 @@ function normPath2(p) {
44108
44197
  function unlinkNodeModulesJunction(wtPath) {
44109
44198
  const nm = (0, import_node_path40.join)(wtPath, "node_modules");
44110
44199
  try {
44111
- if ((0, import_node_fs40.lstatSync)(nm).isSymbolicLink()) (0, import_node_fs40.rmdirSync)(nm);
44200
+ if ((0, import_node_fs41.lstatSync)(nm).isSymbolicLink()) (0, import_node_fs41.rmdirSync)(nm);
44112
44201
  return { ok: true };
44113
44202
  } catch (e) {
44114
44203
  if (e.code === "ENOENT") return { ok: true };
@@ -44119,8 +44208,8 @@ function defaultSleep2(ms) {
44119
44208
  return new Promise((resolve7) => setTimeout(resolve7, ms));
44120
44209
  }
44121
44210
  async function removeResidueDirectory(wtPath, options = {}) {
44122
- const { sleep: sleep2 = defaultSleep2, removeEmptyDir = import_node_fs40.rmdirSync, exists = import_node_fs40.existsSync } = options;
44123
- const removeRecursive = options.removeRecursive ?? ((path2) => (0, import_node_fs40.rmSync)(path2, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }));
44211
+ const { sleep: sleep2 = defaultSleep2, removeEmptyDir = import_node_fs41.rmdirSync, exists = import_node_fs41.existsSync } = options;
44212
+ const removeRecursive = options.removeRecursive ?? ((path2) => (0, import_node_fs41.rmSync)(path2, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }));
44124
44213
  const junction = unlinkNodeModulesJunction(wtPath);
44125
44214
  if (!junction.ok) return junction;
44126
44215
  try {
@@ -44246,21 +44335,21 @@ async function listNestedIgnoredNodeModules(wtPath, execGit) {
44246
44335
  return out.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.endsWith("node_modules/") && line !== "node_modules/").map((line) => (0, import_node_path40.join)(wtPath, line.slice(0, -1)));
44247
44336
  }
44248
44337
  function safeRemoveTree(path2) {
44249
- const stat4 = (0, import_node_fs40.lstatSync)(path2);
44338
+ const stat4 = (0, import_node_fs41.lstatSync)(path2);
44250
44339
  if (stat4.isSymbolicLink()) {
44251
44340
  try {
44252
- (0, import_node_fs40.rmdirSync)(path2);
44341
+ (0, import_node_fs41.rmdirSync)(path2);
44253
44342
  } catch {
44254
- (0, import_node_fs40.unlinkSync)(path2);
44343
+ (0, import_node_fs41.unlinkSync)(path2);
44255
44344
  }
44256
44345
  return;
44257
44346
  }
44258
44347
  if (stat4.isDirectory()) {
44259
- for (const entry of (0, import_node_fs40.readdirSync)(path2)) safeRemoveTree((0, import_node_path40.join)(path2, entry));
44260
- (0, import_node_fs40.rmdirSync)(path2);
44348
+ for (const entry of (0, import_node_fs41.readdirSync)(path2)) safeRemoveTree((0, import_node_path40.join)(path2, entry));
44349
+ (0, import_node_fs41.rmdirSync)(path2);
44261
44350
  return;
44262
44351
  }
44263
- (0, import_node_fs40.unlinkSync)(path2);
44352
+ (0, import_node_fs41.unlinkSync)(path2);
44264
44353
  }
44265
44354
  async function removeWorktreeNodeModules(wtPath) {
44266
44355
  try {
@@ -44276,7 +44365,7 @@ function errorMessage(e) {
44276
44365
  }
44277
44366
  function resolvedOrRaw(path2) {
44278
44367
  try {
44279
- return normPath2((0, import_node_fs40.realpathSync)(path2));
44368
+ return normPath2((0, import_node_fs41.realpathSync)(path2));
44280
44369
  } catch {
44281
44370
  return normPath2(path2);
44282
44371
  }
@@ -44284,10 +44373,10 @@ function resolvedOrRaw(path2) {
44284
44373
  function unlinkEscapingReparsePoints(root, primaryRoot) {
44285
44374
  let realRoot;
44286
44375
  try {
44287
- if ((0, import_node_fs40.lstatSync)(root).isSymbolicLink()) {
44376
+ if ((0, import_node_fs41.lstatSync)(root).isSymbolicLink()) {
44288
44377
  return { ok: false, error: `delete root ${normPath2(root)} is a reparse point resolving to ${resolvedOrRaw(root)}` };
44289
44378
  }
44290
- realRoot = normPath2((0, import_node_fs40.realpathSync)(root));
44379
+ realRoot = normPath2((0, import_node_fs41.realpathSync)(root));
44291
44380
  } catch (e) {
44292
44381
  if (e.code === "ENOENT") return { ok: true, unlinked: [] };
44293
44382
  return { ok: false, error: `cannot resolve delete root ${normPath2(root)}: ${errorMessage(e)}` };
@@ -44303,7 +44392,7 @@ function unlinkEscapingReparsePoints(root, primaryRoot) {
44303
44392
  const dir = stack.pop();
44304
44393
  let entries;
44305
44394
  try {
44306
- entries = (0, import_node_fs40.readdirSync)(dir, { withFileTypes: true });
44395
+ entries = (0, import_node_fs41.readdirSync)(dir, { withFileTypes: true });
44307
44396
  } catch (e) {
44308
44397
  if (e.code === "ENOENT") continue;
44309
44398
  return { ok: false, error: `cannot scan ${normPath2(dir)} for reparse points: ${errorMessage(e)}` };
@@ -44313,7 +44402,7 @@ function unlinkEscapingReparsePoints(root, primaryRoot) {
44313
44402
  if (entry.isSymbolicLink()) {
44314
44403
  let target = "";
44315
44404
  try {
44316
- target = normPath2((0, import_node_fs40.realpathSync)(child2));
44405
+ target = normPath2((0, import_node_fs41.realpathSync)(child2));
44317
44406
  } catch {
44318
44407
  target = "";
44319
44408
  }
@@ -44496,7 +44585,7 @@ async function cleanupPrMergeLocalBranch(branch, options) {
44496
44585
  return report;
44497
44586
  }
44498
44587
  const execGit = options.execGit ?? (async (args) => (await execFileP("git", args, { timeout: GIT_TIMEOUT_MS })).stdout);
44499
- const pathExists = options.pathExists ?? import_node_fs40.existsSync;
44588
+ const pathExists = options.pathExists ?? import_node_fs41.existsSync;
44500
44589
  let afterWorktrees = [];
44501
44590
  try {
44502
44591
  afterWorktrees = parseGitWorktreePorcelain(await execGit(["worktree", "list", "--porcelain"]));
@@ -44783,7 +44872,7 @@ function residueLedgerPath(gitCommonDir) {
44783
44872
  }
44784
44873
  function readPaths(file, deps) {
44785
44874
  try {
44786
- const parsed = JSON.parse((deps.readLedger ?? ((f) => (0, import_node_fs41.readFileSync)(f, "utf8")))(file));
44875
+ const parsed = JSON.parse((deps.readLedger ?? ((f) => (0, import_node_fs42.readFileSync)(f, "utf8")))(file));
44787
44876
  return Array.isArray(parsed) ? parsed.filter((p) => typeof p === "string") : [];
44788
44877
  } catch {
44789
44878
  return [];
@@ -44792,8 +44881,8 @@ function readPaths(file, deps) {
44792
44881
  function writePaths(file, paths, deps) {
44793
44882
  try {
44794
44883
  const write = deps.writeLedger ?? ((f, t) => {
44795
- (0, import_node_fs41.mkdirSync)((0, import_node_path41.dirname)(f), { recursive: true });
44796
- (0, import_node_fs41.writeFileSync)(f, t, "utf8");
44884
+ (0, import_node_fs42.mkdirSync)((0, import_node_path41.dirname)(f), { recursive: true });
44885
+ (0, import_node_fs42.writeFileSync)(f, t, "utf8");
44797
44886
  });
44798
44887
  write(file, `${JSON.stringify([...new Set(paths)], null, 1)}
44799
44888
  `);
@@ -44807,17 +44896,17 @@ function recordResidue(gitCommonDir, path2, deps = {}) {
44807
44896
  writePaths(file, paths, deps);
44808
44897
  }
44809
44898
  async function sweepWorktreeResidue(gitCommonDir, registered, deps = {}) {
44810
- const exists = deps.exists ?? import_node_fs41.existsSync;
44811
- const readDir = deps.readDir ?? ((p) => (0, import_node_fs41.readdirSync)(p));
44899
+ const exists = deps.exists ?? import_node_fs42.existsSync;
44900
+ const readDir = deps.readDir ?? ((p) => (0, import_node_fs42.readdirSync)(p));
44812
44901
  const isDir = deps.isDir ?? ((p) => {
44813
44902
  try {
44814
- return (0, import_node_fs41.statSync)(p).isDirectory();
44903
+ return (0, import_node_fs42.statSync)(p).isDirectory();
44815
44904
  } catch {
44816
44905
  return false;
44817
44906
  }
44818
44907
  });
44819
44908
  const removeRecorded = deps.removeRecorded ?? ((p) => removeResidueDirectory(p));
44820
- const removeEmpty = deps.removeEmpty ?? ((p) => (0, import_node_fs41.rmdirSync)(p));
44909
+ const removeEmpty = deps.removeEmpty ?? ((p) => (0, import_node_fs42.rmdirSync)(p));
44821
44910
  const report = { swept: [], kept: [], dropped: [], emptyRemoved: [], unrecorded: [] };
44822
44911
  const live = new Set(registered.map(norm));
44823
44912
  const primary = registered[0] ? norm(registered[0]) : void 0;
@@ -44897,8 +44986,8 @@ function argvWantsJson2() {
44897
44986
  function hubRoot() {
44898
44987
  const fromPkg = (0, import_node_path42.join)(__dirname, "..", "..");
44899
44988
  const marker = "skills/bootstrap/seeds/manifest.json";
44900
- if ((0, import_node_fs42.existsSync)((0, import_node_path42.join)(fromPkg, marker))) return fromPkg;
44901
- if ((0, import_node_fs42.existsSync)((0, import_node_path42.join)(process.cwd(), marker))) return process.cwd();
44989
+ if ((0, import_node_fs43.existsSync)((0, import_node_path42.join)(fromPkg, marker))) return fromPkg;
44990
+ if ((0, import_node_fs43.existsSync)((0, import_node_path42.join)(process.cwd(), marker))) return process.cwd();
44902
44991
  return null;
44903
44992
  }
44904
44993
  function ciAuditDeps() {
@@ -44911,7 +45000,7 @@ function ciAuditDeps() {
44911
45000
  readSeedFile: (path2) => {
44912
45001
  if (!root) return null;
44913
45002
  const fullPath = (0, import_node_path42.join)(root, path2);
44914
- return (0, import_node_fs42.existsSync)(fullPath) ? (0, import_node_fs42.readFileSync)(fullPath, "utf8") : null;
45003
+ return (0, import_node_fs43.existsSync)(fullPath) ? (0, import_node_fs43.readFileSync)(fullPath, "utf8") : null;
44915
45004
  }
44916
45005
  };
44917
45006
  }
@@ -45675,10 +45764,10 @@ ${list}`);
45675
45764
  });
45676
45765
  async function listCiWorkflowPaths(cwd = process.cwd()) {
45677
45766
  const wfDir = (0, import_node_path42.join)(cwd, ".github", "workflows");
45678
- if (!(0, import_node_fs42.existsSync)(wfDir)) return [];
45679
- return (0, import_node_fs42.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
45767
+ if (!(0, import_node_fs43.existsSync)(wfDir)) return [];
45768
+ return (0, import_node_fs43.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
45680
45769
  try {
45681
- return workflowReportsPrChecks((0, import_node_fs42.readFileSync)((0, import_node_path42.join)(wfDir, name), "utf8"));
45770
+ return workflowReportsPrChecks((0, import_node_fs43.readFileSync)((0, import_node_path42.join)(wfDir, name), "utf8"));
45682
45771
  } catch {
45683
45772
  return true;
45684
45773
  }
@@ -45883,7 +45972,7 @@ ${list}`);
45883
45972
  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) => {
45884
45973
  if (!isReviewVerdict(o.verdict)) return fail(`pr review-verdict: --verdict must be one of ${REVIEW_VERDICTS.join("|")} (got ${o.verdict})`);
45885
45974
  const verdict = o.verdict;
45886
- const findings = o.findingsFile ? (0, import_node_fs42.readFileSync)(o.findingsFile, "utf8") : void 0;
45975
+ const findings = o.findingsFile ? (0, import_node_fs43.readFileSync)(o.findingsFile, "utf8") : void 0;
45887
45976
  let patch;
45888
45977
  let head;
45889
45978
  try {
@@ -46158,7 +46247,7 @@ ${list}`);
46158
46247
  const mergeSquashBody = squashBodyTextForMerge(
46159
46248
  closingGuardInput,
46160
46249
  method === "--squash",
46161
- o.squashBodyFile ? (0, import_node_fs42.readFileSync)(o.squashBodyFile, "utf8") : void 0
46250
+ o.squashBodyFile ? (0, import_node_fs43.readFileSync)(o.squashBodyFile, "utf8") : void 0
46162
46251
  );
46163
46252
  if (!o.squashBodyFile) warnSquashBodyClosingStrip("pr merge", closingGuardInput, mergeSquashBody);
46164
46253
  const closingGuardVerdict = evaluateClosingGuard(closingGuardInput, {
@@ -46288,7 +46377,7 @@ ${list}`);
46288
46377
  }
46289
46378
  if (!repoForPostCleanup) throw e;
46290
46379
  console.warn(`pr merge: gh GraphQL rate-limited \u2014 merging PR #${number} via REST PUT instead (#4588).`);
46291
- const commitMessage = bodyFile ? (0, import_node_fs42.readFileSync)(bodyFile, "utf8") : void 0;
46380
+ const commitMessage = bodyFile ? (0, import_node_fs43.readFileSync)(bodyFile, "utf8") : void 0;
46292
46381
  await defaultGitHubClient().rest("PUT", `repos/${repoForPostCleanup}/pulls/${number}/merge`, {
46293
46382
  body: { merge_method: method.slice(2), ...commitMessage ? { commit_message: commitMessage } : {} },
46294
46383
  timeoutMs: GH_MUTATION_TIMEOUT_MS
@@ -46420,7 +46509,7 @@ ${list}`);
46420
46509
  preserveWorktree: o.preserveWorktree,
46421
46510
  gcAcknowledged: o.gc,
46422
46511
  expectedHeadOid: headRefOid,
46423
- pathExists: (p) => (0, import_node_fs42.existsSync)(p),
46512
+ pathExists: (p) => (0, import_node_fs43.existsSync)(p),
46424
46513
  // #5899: pin cleanup git calls to the main checkout — the task worktree this process may be
46425
46514
  // standing in is removed mid-cleanup, so a cwd-relative invocation fails with
46426
46515
  // 'fatal: not a git repository' and leaves a spurious partial-cleanup exit.
@@ -47329,6 +47418,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
47329
47418
  const slug = applyingRepo ? slugOf(applyingRepo) : "";
47330
47419
  const isHub = applyingRepo?.toLowerCase() === ANNOUNCE_REPO.toLowerCase();
47331
47420
  let announcesToChannel = isHub;
47421
+ let isWorkSuite = false;
47332
47422
  let releaseLanguage2 = "en";
47333
47423
  if (!isHub && slug) {
47334
47424
  let read;
@@ -47342,9 +47432,17 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
47342
47432
  `release: Hub registry read failed for ${slug} (${read.error}) \u2014 whether ${slug} announces to a project release channel is UNREAD, so the --announce-summary-file requirement is unproven. Likely transient (cold start, network, or auth blip); retry shortly.`
47343
47433
  );
47344
47434
  }
47435
+ isWorkSuite = read.project?.suite === "mm-work";
47345
47436
  announcesToChannel = Boolean(read.project?.releaseChannel?.trim());
47346
47437
  releaseLanguage2 = read.project?.releaseLanguage?.trim() || "en";
47347
47438
  }
47439
+ if (isWorkSuite) {
47440
+ try {
47441
+ await requireSuiteReleaseSummary({}, o.announceSummaryFile);
47442
+ } catch (e) {
47443
+ return fail(`release: ${e.message}`);
47444
+ }
47445
+ }
47348
47446
  if (announcesToChannel) {
47349
47447
  if (!o.announceSummaryFile) {
47350
47448
  return fail(
@@ -47648,11 +47746,11 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}, summaryFileTr,
47648
47746
  const checkout = async (repo) => {
47649
47747
  if (repo === SUITE_REPO) return root;
47650
47748
  const target = (0, import_node_path43.join)(base, repo.split("/")[1]);
47651
- if (!(0, import_node_fs43.existsSync)(target)) {
47652
- (0, import_node_fs43.mkdirSync)(base, { recursive: true });
47749
+ if (!(0, import_node_fs44.existsSync)(target)) {
47750
+ (0, import_node_fs44.mkdirSync)(base, { recursive: true });
47653
47751
  await exec("git", ["clone", "--quiet", "--branch", "development", `https://github.com/${repo}.git`, target], root, WATCH_TIMEOUT);
47654
47752
  }
47655
- if (!(0, import_node_fs43.existsSync)((0, import_node_path43.join)(target, ".git"))) throw new Error(`${repo}: suite checkout has no .git at ${target}; stop`);
47753
+ if (!(0, import_node_fs44.existsSync)((0, import_node_path43.join)(target, ".git"))) throw new Error(`${repo}: suite checkout has no .git at ${target}; stop`);
47656
47754
  const origin = await exec("git", ["remote", "get-url", "origin"], target);
47657
47755
  if (!origin.toLowerCase().endsWith(`/${repo.toLowerCase()}.git`) && !origin.toLowerCase().endsWith(`/${repo.toLowerCase()}`))
47658
47756
  throw new Error(`${repo}: suite checkout origin mismatch at ${target}; stop`);
@@ -47856,6 +47954,20 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}, summaryFileTr,
47856
47954
  };
47857
47955
  return {
47858
47956
  recordPath,
47957
+ validateHotfixSummary: () => requireSuiteReleaseSummary({}, summaryFile),
47958
+ validateReleaseSummaries: (releases) => validateReleaseSummaries(
47959
+ releases,
47960
+ { ...memberSummaryFiles, [SUITE_REPO]: summaryFile },
47961
+ async (repo, tag) => {
47962
+ try {
47963
+ await gh(["release", "view", tag, "--repo", repo, "--json", "tagName"]);
47964
+ return true;
47965
+ } catch (e) {
47966
+ if (/release not found|HTTP 404|Not Found|\(404\)/i.test(e.message)) return false;
47967
+ throw e;
47968
+ }
47969
+ }
47970
+ ),
47859
47971
  registry: registry2,
47860
47972
  backend,
47861
47973
  hold,
@@ -47936,10 +48048,11 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}, summaryFileTr,
47936
48048
  },
47937
48049
  // #7738: a member hotfixes as suite-solo; the lead hotfixes under its suite-lead capability (inTrain sets it).
47938
48050
  hotfixStart: (repo, o) => inTrain(repo, async (deps) => {
48051
+ await requireSuiteReleaseSummary(deps, summaryFile);
47939
48052
  const r = await runHotfixStart(deps, { from: o.from, ack: o.ack, suiteSolo: repo !== SUITE_REPO });
47940
48053
  return { tag: r.tag };
47941
48054
  }, repo !== SUITE_REPO),
47942
- hotfixRelease: (repo, tag) => inTrain(repo, async (deps) => ({ tag: (await runHotfixRelease(deps, tag, { suiteSolo: repo !== SUITE_REPO })).tag }), repo !== SUITE_REPO),
48055
+ hotfixRelease: (repo, tag) => inTrain(repo, async (deps) => ({ tag: (await runHotfixRelease(deps, tag, { suiteSolo: repo !== SUITE_REPO, announceSummaryFile: summaryFile, announceSummaryFileTr: summaryFileTr })).tag }), repo !== SUITE_REPO),
47943
48056
  // #7738: the #7714 solo pin leg, polled until the auto-merge lands (30 s x 40 = 20 min), then the caller decides.
47944
48057
  soloPin: async (repo, tag) => {
47945
48058
  for (let i = 0; ; i++) {
@@ -47960,13 +48073,27 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}, summaryFileTr,
47960
48073
  const body = await gh(["release", "view", leadTag, "--repo", SUITE_REPO, "--json", "body", "--jq", ".body"]);
47961
48074
  if (parseSuiteMapping(body)) return;
47962
48075
  const dir = suiteStateDir(root);
47963
- (0, import_node_fs43.mkdirSync)(dir, { recursive: true });
48076
+ (0, import_node_fs44.mkdirSync)(dir, { recursive: true });
47964
48077
  const file = (0, import_node_path43.join)(dir, `notes-${leadTag}.md`);
47965
- (0, import_node_fs43.writeFileSync)(file, `${body}
48078
+ (0, import_node_fs44.writeFileSync)(file, `${body}
47966
48079
 
47967
48080
  ${suiteMappingBlock(leadTag, mapping)}`);
47968
48081
  await gh(["release", "edit", leadTag, "--repo", SUITE_REPO, "--notes-file", file]);
47969
48082
  },
48083
+ productRuleset: async (repo) => {
48084
+ const read = await fetchProjectBySlugChecked(slugOf(repo), registryClientDeps(await loadConfig()));
48085
+ if (!read.ok) throw new Error(`registry META unreadable (${read.error})`);
48086
+ if (!read.project?.requiredChecks?.length) return ["skip: no registry META requiredChecks; the seeded ruleset stays as it is"];
48087
+ const result = await applyCiReconcileRepo(repo, {
48088
+ client: defaultGitHubClient(),
48089
+ listProjects: async () => [],
48090
+ getProjectMeta: async () => read.project
48091
+ }, { productRulesetOnly: true });
48092
+ const lines2 = [...result.applied, ...result.skipped.map((s) => `skip: ${s}`)];
48093
+ lines2.forEach((line) => console.log(`${repo}: product ruleset ${line}`));
48094
+ if (result.errors.length) throw new Error(result.errors.join("; "));
48095
+ return lines2;
48096
+ },
47970
48097
  release,
47971
48098
  shippedTag,
47972
48099
  sameContent: sameContent2,
@@ -47989,7 +48116,7 @@ ${suiteMappingBlock(leadTag, mapping)}`);
47989
48116
 
47990
48117
  // src/suite-commands.ts
47991
48118
  init_cli_shared();
47992
- var import_node_fs44 = require("node:fs");
48119
+ var import_node_fs45 = require("node:fs");
47993
48120
  function parseMemberSummaryFiles(entries) {
47994
48121
  const memberSummaryFiles = {};
47995
48122
  for (const entry of entries) {
@@ -47999,12 +48126,6 @@ function parseMemberSummaryFiles(entries) {
47999
48126
  }
48000
48127
  return memberSummaryFiles;
48001
48128
  }
48002
- function validateSuiteReleaseSummary(path2) {
48003
- if (!path2) throw new Error("suite release requires --announce-summary-file before promoting any member");
48004
- const lines2 = (0, import_node_fs44.readFileSync)(path2, "utf8").split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
48005
- if (lines2[0]?.startsWith("No changes to this module")) throw new Error("suite release requires a real MM-Work title, not the no-change placeholder");
48006
- if (lines2.length < 3 || lines2.length > 6) throw new Error("suite release announcement needs 3\u20136 curated lines before promoting any member");
48007
- }
48008
48129
  function suiteRcDataProdDeps() {
48009
48130
  return {
48010
48131
  fetch: async (url, init) => {
@@ -48077,7 +48198,7 @@ function registerSuiteCommands(program3) {
48077
48198
  try {
48078
48199
  const io = suiteTrainIo(root);
48079
48200
  retired = await retireFailedLeadRcLedger({ ledgerPath: releaseLedgerPath(root), rcHead: () => io.head(SUITE_REPO, "rc").then((h) => h ?? void 0), markAbandoned: io.markAbandoned }, o.ownerGo === true);
48080
- if (retired && !(0, import_node_fs44.existsSync)(io.recordPath)) {
48201
+ if (retired && !(0, import_node_fs45.existsSync)(io.recordPath)) {
48081
48202
  console.log(o.json ? JSON.stringify({ retiredLeadRc: retired }, null, 2) : `suite abandon: retired the lead's failed, never-promoted rc candidate ${retired}; marked abandoned on origin (#7854)`);
48082
48203
  return;
48083
48204
  }
@@ -48099,10 +48220,9 @@ function registerSuiteCommands(program3) {
48099
48220
  return fail(`suite abandon: ${e.message}`);
48100
48221
  }
48101
48222
  });
48102
- suite.command("release").description("verify the cut and unchanged live backends, ship members then MM-Work, announce once").option("--owner-go", "explicit owner approval to release the tested candidate").option("--announce-summary-file <path>", "curated suite announcement, posted once after the train; also this cut's plain-words summary for MM-Work itself").option("--member-summary-file <owner/repo=path>", "repeatable: a member's OWN plain-words summary (#7510) \u2014 never the suite lead's summary reused; a member with no entry still ships, with the coordinator-ruled no-change line", (val, prev = []) => [...prev, val], []).option("--announce-summary-file-tr <path>", "Turkish plain-words summary for MM-Work itself").option("--member-summary-file-tr <owner/repo=path>", "repeatable: a member's own Turkish plain-words summary", (val, prev = []) => [...prev, val], []).option("--big", "marks MM-Work's own release big for the plain-words section (#7510); a member is never marked big through this flag \u2014 it is big only via the read route's automatic minor/major detection").option("--json", "structured receipt").action(async (o) => {
48223
+ suite.command("release").description("verify the cut and unchanged live backends, ship members then MM-Work, announce once").option("--owner-go", "explicit owner approval to release the tested candidate").option("--announce-summary-file <path>", "curated suite announcement, posted once after the train; also this cut's plain-words summary for MM-Work itself").option("--member-summary-file <owner/repo=path>", "repeatable: required title + 2\u20135 notes for each member being released; carried members need none", (val, prev = []) => [...prev, val], []).option("--announce-summary-file-tr <path>", "Turkish plain-words summary for MM-Work itself").option("--member-summary-file-tr <owner/repo=path>", "repeatable: a member's own Turkish plain-words summary", (val, prev = []) => [...prev, val], []).option("--big", "marks MM-Work's own release big for the plain-words section (#7510); a member is never marked big through this flag \u2014 it is big only via the read route's automatic minor/major detection").option("--json", "structured receipt").action(async (o) => {
48103
48224
  try {
48104
48225
  if (!o.ownerGo) throw new Error("suite release requires --owner-go; nothing was written");
48105
- validateSuiteReleaseSummary(o.announceSummaryFile);
48106
48226
  const memberSummaryFiles = parseMemberSummaryFiles(o.memberSummaryFile ?? []);
48107
48227
  const memberSummaryFilesTr = parseMemberSummaryFiles(o.memberSummaryFileTr ?? []);
48108
48228
  const root = await leadRoot();
@@ -48123,14 +48243,14 @@ function registerSuiteCommands(program3) {
48123
48243
  return fail(`suite release: ${e.message}`);
48124
48244
  }
48125
48245
  });
48126
- suite.command("hotfix <member>").description("member hotfix, main pin PR, MM-Work patch release, and the recorded suite version -> member versions mapping (#7738)").requiredOption("--from <pr#|sha[,pr#|sha...]>", "merged development PR(s) or SHA(s) the member hotfix carries").option("--ack <shas>", "dev shas verified as not needed on main (see hotfix start --ack)").option("--owner-go", "explicit owner approval: ships the member and MM-Work to production (not needed for --dry-run)").option("--dry-run", "read-only: print the ordered plan; writes nothing").option("--json", "structured receipt").action(async (member, o) => {
48246
+ suite.command("hotfix <member>").description("member hotfix, main pin PR, MM-Work patch release, and the recorded suite version -> member versions mapping (#7738)").requiredOption("--from <pr#|sha[,pr#|sha...]>", "merged development PR(s) or SHA(s) the member hotfix carries").option("--ack <shas>", "dev shas verified as not needed on main (see hotfix start --ack)").option("--announce-summary-file <path>", "required title + 2\u20135 notes for the member and lead hotfix Releases").option("--announce-summary-file-tr <path>", "optional Turkish plain-words notes").option("--owner-go", "explicit owner approval: ships the member and MM-Work to production (not needed for --dry-run)").option("--dry-run", "read-only: print the ordered plan; writes nothing").option("--json", "structured receipt").action(async (member, o) => {
48127
48247
  try {
48128
48248
  if (!o.ownerGo && !o.dryRun) throw new Error("suite hotfix requires --owner-go (it ships to production); nothing was written. --dry-run prints the plan");
48129
48249
  const root = await leadRoot();
48130
48250
  const unlock = o.dryRun ? async () => void 0 : await acquireTrainWriterLock(root, "suite hotfix");
48131
48251
  let result;
48132
48252
  try {
48133
- result = await suiteHotfix(suiteTrainIo(root), { member, from: o.from, ack: o.ack?.split(",").map((s) => s.trim()).filter(Boolean), ownerGo: o.ownerGo, dryRun: o.dryRun });
48253
+ result = await suiteHotfix(suiteTrainIo(root, o.announceSummaryFile, {}, o.announceSummaryFileTr), { member, from: o.from, ack: o.ack?.split(",").map((s) => s.trim()).filter(Boolean), ownerGo: o.ownerGo, dryRun: o.dryRun });
48134
48254
  } finally {
48135
48255
  await unlock();
48136
48256
  }
@@ -48267,7 +48387,7 @@ function suiteStatusDeps(github, registry2) {
48267
48387
 
48268
48388
  // src/packages-adopt-export.ts
48269
48389
  var import_node_child_process18 = require("node:child_process");
48270
- var import_node_fs45 = require("node:fs");
48390
+ var import_node_fs46 = require("node:fs");
48271
48391
  var import_node_os21 = require("node:os");
48272
48392
  var import_node_path44 = require("node:path");
48273
48393
  var import_node_util6 = require("node:util");
@@ -48891,36 +49011,36 @@ async function adoptExport(opts, deps) {
48891
49011
  if (!/^sha256:[0-9a-f]{64}$/.test(opts.digest)) throw new Error("--digest must be sha256:<64 hex>, from the export run summary");
48892
49012
  const packagePath = (0, import_node_path44.join)(opts.dir, "package.json");
48893
49013
  const lockPath = (0, import_node_path44.join)(opts.dir, "package-lock.json");
48894
- const packageText = (0, import_node_fs45.readFileSync)(packagePath, "utf8");
48895
- const lockText = (0, import_node_fs45.readFileSync)(lockPath, "utf8");
48896
- const work = (0, import_node_fs45.mkdtempSync)((0, import_node_path44.join)((0, import_node_os21.tmpdir)(), "mmi-adopt-export-"));
49014
+ const packageText = (0, import_node_fs46.readFileSync)(packagePath, "utf8");
49015
+ const lockText = (0, import_node_fs46.readFileSync)(lockPath, "utf8");
49016
+ const work = (0, import_node_fs46.mkdtempSync)((0, import_node_path44.join)((0, import_node_os21.tmpdir)(), "mmi-adopt-export-"));
48897
49017
  try {
48898
49018
  const run = JSON.parse((await deps.ghApi(`repos/${EXPORT_REPO}/actions/runs/${opts.run}`)).toString("utf8"));
48899
- (0, import_node_fs45.writeFileSync)((0, import_node_path44.join)(work, "expected.json"), JSON.stringify(expectedFromRun(run, opts)));
48900
- (0, import_node_fs45.writeFileSync)((0, import_node_path44.join)(work, "provider.json"), await deps.ghApi(`repos/${EXPORT_REPO}/actions/artifacts/${opts.artifact}`));
48901
- (0, import_node_fs45.writeFileSync)((0, import_node_path44.join)(work, "artifact.zip"), await deps.ghApi(`repos/${EXPORT_REPO}/actions/artifacts/${opts.artifact}/zip`));
49019
+ (0, import_node_fs46.writeFileSync)((0, import_node_path44.join)(work, "expected.json"), JSON.stringify(expectedFromRun(run, opts)));
49020
+ (0, import_node_fs46.writeFileSync)((0, import_node_path44.join)(work, "provider.json"), await deps.ghApi(`repos/${EXPORT_REPO}/actions/artifacts/${opts.artifact}`));
49021
+ (0, import_node_fs46.writeFileSync)((0, import_node_path44.join)(work, "artifact.zip"), await deps.ghApi(`repos/${EXPORT_REPO}/actions/artifacts/${opts.artifact}/zip`));
48902
49022
  const script = (0, import_node_path44.join)(work, "package_export.py");
48903
- (0, import_node_fs45.writeFileSync)(script, package_export_default);
49023
+ (0, import_node_fs46.writeFileSync)(script, package_export_default);
48904
49024
  await deps.python(script, ["verify", work, opts.ds, opts.screens]);
48905
49025
  const verified = (0, import_node_path44.join)(work, "verified");
48906
- const metadata = Object.fromEntries(PACKAGES.map(({ name, short: short3 }) => [name, JSON.parse((0, import_node_fs45.readFileSync)((0, import_node_path44.join)(verified, `${short3}.metadata.json`), "utf8"))]));
49026
+ const metadata = Object.fromEntries(PACKAGES.map(({ name, short: short3 }) => [name, JSON.parse((0, import_node_fs46.readFileSync)((0, import_node_path44.join)(verified, `${short3}.metadata.json`), "utf8"))]));
48907
49027
  const plan = planAdoption(packageText, lockText, metadata);
48908
49028
  for (const { name } of plan.result.adopted) {
48909
49029
  const short3 = PACKAGES.find((p) => p.name === name).short;
48910
49030
  await deps.npm(["cache", "add", (0, import_node_path44.join)(verified, `${short3}.tgz`)], opts.dir);
48911
49031
  }
48912
- (0, import_node_fs45.writeFileSync)(packagePath, plan.packageText);
48913
- (0, import_node_fs45.writeFileSync)(lockPath, plan.lockText);
49032
+ (0, import_node_fs46.writeFileSync)(packagePath, plan.packageText);
49033
+ (0, import_node_fs46.writeFileSync)(lockPath, plan.lockText);
48914
49034
  try {
48915
49035
  await deps.npm(["ci", "--offline", "--ignore-scripts", "--no-audit", "--no-fund"], opts.dir);
48916
49036
  } catch (error) {
48917
- (0, import_node_fs45.writeFileSync)(packagePath, packageText);
48918
- (0, import_node_fs45.writeFileSync)(lockPath, lockText);
49037
+ (0, import_node_fs46.writeFileSync)(packagePath, packageText);
49038
+ (0, import_node_fs46.writeFileSync)(lockPath, lockText);
48919
49039
  throw new Error(`npm ci --offline failed, package.json and package-lock.json restored: ${error.message}`);
48920
49040
  }
48921
49041
  return plan.result;
48922
49042
  } finally {
48923
- (0, import_node_fs45.rmSync)(work, { recursive: true, force: true });
49043
+ (0, import_node_fs46.rmSync)(work, { recursive: true, force: true });
48924
49044
  }
48925
49045
  }
48926
49046
  var execFileBuffer = (0, import_node_util6.promisify)(import_node_child_process18.execFile);
@@ -48985,12 +49105,12 @@ function registerPackagesCommands(program3) {
48985
49105
  init_cli_shared();
48986
49106
 
48987
49107
  // src/wave-status.ts
48988
- var import_node_fs46 = require("node:fs");
49108
+ var import_node_fs47 = require("node:fs");
48989
49109
  function readStageSummary(worktreePath) {
48990
49110
  const statePath = stageStatePath(worktreePath);
48991
- if (!(0, import_node_fs46.existsSync)(statePath)) return void 0;
49111
+ if (!(0, import_node_fs47.existsSync)(statePath)) return void 0;
48992
49112
  try {
48993
- const state = JSON.parse((0, import_node_fs46.readFileSync)(statePath, "utf8"));
49113
+ const state = JSON.parse((0, import_node_fs47.readFileSync)(statePath, "utf8"));
48994
49114
  const port = typeof state.port === "number" ? state.port : void 0;
48995
49115
  if (port == null || !Number.isInteger(port) || port <= 0) return void 0;
48996
49116
  return { port, url: typeof state.url === "string" ? state.url : void 0 };
@@ -49047,7 +49167,7 @@ async function collectWaveStatus(deps) {
49047
49167
  // src/discovery-commands.ts
49048
49168
  init_github_client();
49049
49169
  init_cli_shared();
49050
- var import_node_fs47 = require("node:fs");
49170
+ var import_node_fs48 = require("node:fs");
49051
49171
  var import_node_os22 = require("node:os");
49052
49172
  var import_node_path45 = require("node:path");
49053
49173
  var GC_GH_TIMEOUT_MS3 = 2e4;
@@ -49240,8 +49360,8 @@ async function collectOnboardStatus(opts = {}) {
49240
49360
  }
49241
49361
  const home = (0, import_node_os22.homedir)();
49242
49362
  const plugin = onboardPluginGate({
49243
- readKnown: () => readFileSyncSafe((0, import_node_path45.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs47.readFileSync),
49244
- readSettings: () => readFileSyncSafe((0, import_node_path45.join)(home, ".claude", "settings.json"), import_node_fs47.readFileSync)
49363
+ readKnown: () => readFileSyncSafe((0, import_node_path45.join)(home, ...KNOWN_MARKETPLACES_RELATIVE), import_node_fs48.readFileSync),
49364
+ readSettings: () => readFileSyncSafe((0, import_node_path45.join)(home, ".claude", "settings.json"), import_node_fs48.readFileSync)
49245
49365
  });
49246
49366
  return {
49247
49367
  guide: "Read the MMI org-guide skill before project work; load only the relevant topic. Use mmi-help for a short introduction.",
@@ -52345,7 +52465,7 @@ function registerSessionReport(program3) {
52345
52465
  }
52346
52466
 
52347
52467
  // src/stage-commands.ts
52348
- var import_node_fs48 = require("node:fs");
52468
+ var import_node_fs49 = require("node:fs");
52349
52469
  var import_node_path46 = require("node:path");
52350
52470
  init_cli_shared();
52351
52471
  init_clean_exit();
@@ -52594,8 +52714,8 @@ function registerStageCommands(program3) {
52594
52714
  const repo = Array.isArray(project2?.repos) && typeof project2.repos[0] === "string" ? project2.repos[0] : slug;
52595
52715
  const resolution = decideStage({
52596
52716
  registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
52597
- hasCompose: (0, import_node_fs48.existsSync)((0, import_node_path46.join)(process.cwd(), "docker-compose.yml")),
52598
- hasEnvExample: (0, import_node_fs48.existsSync)((0, import_node_path46.join)(process.cwd(), ".env.example")),
52717
+ hasCompose: (0, import_node_fs49.existsSync)((0, import_node_path46.join)(process.cwd(), "docker-compose.yml")),
52718
+ hasEnvExample: (0, import_node_fs49.existsSync)((0, import_node_path46.join)(process.cwd(), ".env.example")),
52599
52719
  repo
52600
52720
  });
52601
52721
  if (resolution.source === "derived" && resolution.derived) {
@@ -52888,14 +53008,14 @@ vercel preview (${preview.branch}): ${d.state}${d.url ? ` \u2014 ${d.url}` : ""}
52888
53008
 
52889
53009
  // src/tenant-artifact.ts
52890
53010
  var import_node_crypto16 = require("node:crypto");
52891
- var import_node_fs49 = require("node:fs");
53011
+ var import_node_fs50 = require("node:fs");
52892
53012
  var import_promises11 = require("node:fs/promises");
52893
53013
  var import_node_path47 = require("node:path");
52894
53014
  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}$/;
52895
53015
  var MAX_BYTES = 5 * 1024 * 1024 * 1024;
52896
53016
  async function sha256File(path2) {
52897
53017
  const hash = (0, import_node_crypto16.createHash)("sha256");
52898
- for await (const chunk of (0, import_node_fs49.createReadStream)(path2)) hash.update(chunk);
53018
+ for await (const chunk of (0, import_node_fs50.createReadStream)(path2)) hash.update(chunk);
52899
53019
  return hash.digest("hex");
52900
53020
  }
52901
53021
  async function putTenantArtifact(repo, stage, inputPath, deps) {
@@ -52918,7 +53038,7 @@ async function putTenantArtifact(repo, stage, inputPath, deps) {
52918
53038
  return [key, value];
52919
53039
  }));
52920
53040
  headers["content-length"] = String(info.size);
52921
- const stream = (0, import_node_fs49.createReadStream)(path2);
53041
+ const stream = (0, import_node_fs50.createReadStream)(path2);
52922
53042
  let uploaded;
52923
53043
  try {
52924
53044
  uploaded = await fetch(body.uploadUrl, {
@@ -53057,7 +53177,7 @@ function renderVerifySecrets(body) {
53057
53177
  }
53058
53178
 
53059
53179
  // src/command-register-developer.ts
53060
- var import_node_fs53 = require("node:fs");
53180
+ var import_node_fs54 = require("node:fs");
53061
53181
  init_clean_exit();
53062
53182
  init_cli_shared();
53063
53183
  init_error_codes();
@@ -53125,13 +53245,13 @@ async function executeWaveLand(plan, deps) {
53125
53245
  // src/dist-drift.ts
53126
53246
  var import_node_child_process19 = require("node:child_process");
53127
53247
  var import_node_crypto18 = require("node:crypto");
53128
- var import_node_fs51 = require("node:fs");
53248
+ var import_node_fs52 = require("node:fs");
53129
53249
  var import_node_os23 = require("node:os");
53130
53250
  var import_node_path49 = require("node:path");
53131
53251
 
53132
53252
  // ../scripts/distribution-digest.mjs
53133
53253
  var import_node_crypto17 = require("node:crypto");
53134
- var import_node_fs50 = require("node:fs");
53254
+ var import_node_fs51 = require("node:fs");
53135
53255
  var import_node_path48 = require("node:path");
53136
53256
  var slash = (value) => value.replaceAll("\\", "/");
53137
53257
  function repoPath(root, declaredPath, label) {
@@ -53145,7 +53265,7 @@ function repoPath(root, declaredPath, label) {
53145
53265
  function digestFiles(files) {
53146
53266
  const hash = (0, import_node_crypto17.createHash)("sha256");
53147
53267
  for (const file of [...files].sort((a, b) => a.relative.localeCompare(b.relative))) {
53148
- const content = file.stat.isSymbolicLink() ? Buffer.from((0, import_node_fs50.readlinkSync)(file.absolute), "utf8") : (0, import_node_fs50.readFileSync)(file.absolute);
53268
+ const content = file.stat.isSymbolicLink() ? Buffer.from((0, import_node_fs51.readlinkSync)(file.absolute), "utf8") : (0, import_node_fs51.readFileSync)(file.absolute);
53149
53269
  hash.update(file.relative, "utf8");
53150
53270
  hash.update("\0");
53151
53271
  hash.update(file.stat.isSymbolicLink() ? "symlink" : "file", "utf8");
@@ -53160,8 +53280,8 @@ function digestFiles(files) {
53160
53280
  function digestPackedFiles(packageRoot, packedFiles) {
53161
53281
  return digestFiles(packedFiles.map((path2) => {
53162
53282
  const absolute = repoPath(packageRoot, path2, "packed artifact identity path");
53163
- if (!(0, import_node_fs50.existsSync)(absolute)) throw new Error(`packed artifact identity path ${path2} does not exist`);
53164
- return { absolute, relative: slash(path2), stat: (0, import_node_fs50.lstatSync)(absolute) };
53283
+ if (!(0, import_node_fs51.existsSync)(absolute)) throw new Error(`packed artifact identity path ${path2} does not exist`);
53284
+ return { absolute, relative: slash(path2), stat: (0, import_node_fs51.lstatSync)(absolute) };
53165
53285
  }));
53166
53286
  }
53167
53287
 
@@ -53245,12 +53365,12 @@ function renderDistDriftReceipt(receipt) {
53245
53365
  return lines2;
53246
53366
  }
53247
53367
  function readOrNull(path2) {
53248
- return (0, import_node_fs51.existsSync)(path2) ? (0, import_node_fs51.readFileSync)(path2) : null;
53368
+ return (0, import_node_fs52.existsSync)(path2) ? (0, import_node_fs52.readFileSync)(path2) : null;
53249
53369
  }
53250
53370
  function walkFiles(root) {
53251
53371
  const files = [];
53252
53372
  const walk2 = (directory) => {
53253
- for (const entry of (0, import_node_fs51.readdirSync)(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
53373
+ for (const entry of (0, import_node_fs52.readdirSync)(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
53254
53374
  const child2 = (0, import_node_path49.join)(directory, entry.name);
53255
53375
  if (entry.isDirectory()) walk2(child2);
53256
53376
  else files.push(child2);
@@ -53261,7 +53381,7 @@ function walkFiles(root) {
53261
53381
  }
53262
53382
  function bomPathFor(root) {
53263
53383
  try {
53264
- const registry2 = JSON.parse((0, import_node_fs51.readFileSync)((0, import_node_path49.join)(root, "surfaces.json"), "utf8"));
53384
+ const registry2 = JSON.parse((0, import_node_fs52.readFileSync)((0, import_node_path49.join)(root, "surfaces.json"), "utf8"));
53265
53385
  return (0, import_node_path49.join)(root, registry2?.sharedAgentCore?.releaseMetadata?.bomPath ?? "distribution-bom.json");
53266
53386
  } catch {
53267
53387
  return (0, import_node_path49.join)(root, "distribution-bom.json");
@@ -53277,7 +53397,7 @@ function rebuildTo(packageRoot, outDir) {
53277
53397
  });
53278
53398
  }
53279
53399
  function runDistStatus(root) {
53280
- const stage = (0, import_node_fs51.mkdtempSync)((0, import_node_path49.join)((0, import_node_os23.tmpdir)(), "mmi-dist-drift-"));
53400
+ const stage = (0, import_node_fs52.mkdtempSync)((0, import_node_path49.join)((0, import_node_os23.tmpdir)(), "mmi-dist-drift-"));
53281
53401
  let overlayCount = 0;
53282
53402
  try {
53283
53403
  const cliOut = (0, import_node_path49.join)(stage, "cli-dist");
@@ -53293,19 +53413,19 @@ function runDistStatus(root) {
53293
53413
  const tree = (path2) => readOrNull((0, import_node_path49.join)(root, path2));
53294
53414
  const distRoot = (0, import_node_path49.join)(root, "cli", "dist");
53295
53415
  const distTree = () => walkFiles(distRoot).map((absolute) => `cli/dist/${(0, import_node_path49.relative)(distRoot, absolute).replaceAll("\\", "/")}`);
53296
- const bom = JSON.parse((0, import_node_fs51.readFileSync)(bomPathFor(root), "utf8"));
53416
+ const bom = JSON.parse((0, import_node_fs52.readFileSync)(bomPathFor(root), "utf8"));
53297
53417
  const digest = (entries) => {
53298
53418
  const overlay = (0, import_node_path49.join)(stage, `overlay-${overlayCount++}`);
53299
53419
  for (const entry of entries) {
53300
53420
  const target = (0, import_node_path49.join)(overlay, entry.path);
53301
- (0, import_node_fs51.mkdirSync)((0, import_node_path49.dirname)(target), { recursive: true });
53302
- (0, import_node_fs51.writeFileSync)(target, entry.bytes);
53421
+ (0, import_node_fs52.mkdirSync)((0, import_node_path49.dirname)(target), { recursive: true });
53422
+ (0, import_node_fs52.writeFileSync)(target, entry.bytes);
53303
53423
  }
53304
53424
  return digestPackedFiles(overlay, entries.map((entry) => entry.path));
53305
53425
  };
53306
53426
  return computeDistDriftReceipt({ committed, tree, rebuilt, distTree, bom, digest });
53307
53427
  } finally {
53308
- (0, import_node_fs51.rmSync)(stage, { recursive: true, force: true });
53428
+ (0, import_node_fs52.rmSync)(stage, { recursive: true, force: true });
53309
53429
  }
53310
53430
  }
53311
53431
 
@@ -53518,7 +53638,7 @@ function registerSchedulesLiftCommand(program3, deps = {}) {
53518
53638
 
53519
53639
  // src/spawn-policy-core.ts
53520
53640
  var import_node_child_process20 = require("node:child_process");
53521
- var import_node_fs52 = require("node:fs");
53641
+ var import_node_fs53 = require("node:fs");
53522
53642
  var import_node_path51 = require("node:path");
53523
53643
  var SPAWNERS = ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync"];
53524
53644
  var CALL_SOURCE = String.raw`(^|[^.\w$])(${SPAWNERS.join("|")})\s*\(`;
@@ -53605,7 +53725,7 @@ function runSpawnPolicy(root) {
53605
53725
  for (const file of files) {
53606
53726
  let raw;
53607
53727
  try {
53608
- raw = (0, import_node_fs52.readFileSync)((0, import_node_path51.join)(root, file), "utf8");
53728
+ raw = (0, import_node_fs53.readFileSync)((0, import_node_path51.join)(root, file), "utf8");
53609
53729
  } catch {
53610
53730
  continue;
53611
53731
  }
@@ -53626,18 +53746,18 @@ function registerDeveloperCommands(program3) {
53626
53746
  const rules = program3.command("rules").description("org-managed .gitignore delivery");
53627
53747
  rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").option("--json", "machine-readable output").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
53628
53748
  const path2 = (0, import_node_path52.join)(process.cwd(), ".gitignore");
53629
- const current = (0, import_node_fs53.existsSync)(path2) ? (0, import_node_fs53.readFileSync)(path2, "utf8") : null;
53749
+ const current = (0, import_node_fs54.existsSync)(path2) ? (0, import_node_fs54.readFileSync)(path2, "utf8") : null;
53630
53750
  const plan = planManagedGitignore(current);
53631
53751
  const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
53632
53752
  if (opts.json) {
53633
- if (opts.write && plan.changed) (0, import_node_fs53.writeFileSync)(path2, plan.content, "utf8");
53753
+ if (opts.write && plan.changed) (0, import_node_fs54.writeFileSync)(path2, plan.content, "utf8");
53634
53754
  console.log(JSON.stringify(plan, null, 2));
53635
53755
  if (!opts.write && plan.changed) process.exitCode = 1;
53636
53756
  return;
53637
53757
  }
53638
53758
  if (opts.write) {
53639
53759
  if (plan.changed) {
53640
- (0, import_node_fs53.writeFileSync)(path2, plan.content, "utf8");
53760
+ (0, import_node_fs54.writeFileSync)(path2, plan.content, "utf8");
53641
53761
  console.log(`mmi-cli devops org rules gitignore: updated .gitignore (${drift})`);
53642
53762
  } else {
53643
53763
  console.log("mmi-cli devops org rules gitignore: up to date");
@@ -54037,7 +54157,7 @@ function mmiDoctorDeps(opts = {}) {
54037
54157
  );
54038
54158
  const result = applyPluginCachePlan(
54039
54159
  plan,
54040
- (p) => (0, import_node_fs54.rmSync)(p, { recursive: true }),
54160
+ (p) => (0, import_node_fs55.rmSync)(p, { recursive: true }),
54041
54161
  stagingApplyFsGuard(configRoot)
54042
54162
  );
54043
54163
  return {
@@ -54072,7 +54192,7 @@ function mmiDoctorDeps(opts = {}) {
54072
54192
  // has no generated routing index would
54073
54193
  // get a permanent — demanding an artifact it never asked for.
54074
54194
  docsIndexState: (root) => {
54075
- if (!(0, import_node_fs54.existsSync)((0, import_node_path53.join)(root, DOCS_INDEX_PATH))) return void 0;
54195
+ if (!(0, import_node_fs55.existsSync)((0, import_node_path53.join)(root, DOCS_INDEX_PATH))) return void 0;
54076
54196
  const real = createDocsIndexDeps(root);
54077
54197
  let docs;
54078
54198
  const listDocs = () => docs ??= real.listDocs();
@@ -54081,7 +54201,7 @@ function mmiDoctorDeps(opts = {}) {
54081
54201
  },
54082
54202
  // #4168: working-tree heal — write if drifted, then re-check. Commit remains the operator's step.
54083
54203
  healDocsIndex: (root) => {
54084
- if (!(0, import_node_fs54.existsSync)((0, import_node_path53.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
54204
+ if (!(0, import_node_fs55.existsSync)((0, import_node_path53.join)(root, DOCS_INDEX_PATH))) return { drift: false, docCount: 0 };
54085
54205
  const real = createDocsIndexDeps(root);
54086
54206
  let docs;
54087
54207
  const listDocs = () => docs ??= real.listDocs();
@@ -54795,7 +54915,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
54795
54915
  if (dupe) return fail(`org project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
54796
54916
  if (o.secretsFile) {
54797
54917
  try {
54798
- vars.push(`secrets=${(0, import_node_fs54.readFileSync)(o.secretsFile, "utf8")}`);
54918
+ vars.push(`secrets=${(0, import_node_fs55.readFileSync)(o.secretsFile, "utf8")}`);
54799
54919
  } catch (e) {
54800
54920
  return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
54801
54921
  }
@@ -55184,15 +55304,15 @@ function ciAuditDeps2() {
55184
55304
  readSeedFile: (path2) => {
55185
55305
  if (!root) return null;
55186
55306
  const fullPath = (0, import_node_path53.join)(root, path2);
55187
- return (0, import_node_fs54.existsSync)(fullPath) ? (0, import_node_fs54.readFileSync)(fullPath, "utf8") : null;
55307
+ return (0, import_node_fs55.existsSync)(fullPath) ? (0, import_node_fs55.readFileSync)(fullPath, "utf8") : null;
55188
55308
  }
55189
55309
  };
55190
55310
  }
55191
55311
  function hubRoot2() {
55192
55312
  const fromPkg = (0, import_node_path53.join)(__dirname, "..", "..");
55193
55313
  const marker = "skills/bootstrap/seeds/manifest.json";
55194
- if ((0, import_node_fs54.existsSync)((0, import_node_path53.join)(fromPkg, marker))) return fromPkg;
55195
- if ((0, import_node_fs54.existsSync)((0, import_node_path53.join)(process.cwd(), marker))) return process.cwd();
55314
+ if ((0, import_node_fs55.existsSync)((0, import_node_path53.join)(fromPkg, marker))) return fromPkg;
55315
+ if ((0, import_node_fs55.existsSync)((0, import_node_path53.join)(process.cwd(), marker))) return process.cwd();
55196
55316
  return null;
55197
55317
  }
55198
55318
  registerQueryCommands(program2);
@@ -55318,10 +55438,10 @@ access.command("audit").description("audit collaborator roles + train-branch pus
55318
55438
  targets = resolution.targets;
55319
55439
  }
55320
55440
  const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
55321
- const fileMatrix = (0, import_node_fs54.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs54.readFileSync)("access-matrix.json", "utf8")) : {};
55441
+ const fileMatrix = (0, import_node_fs55.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs55.readFileSync)("access-matrix.json", "utf8")) : {};
55322
55442
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
55323
55443
  const dataAccess = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
55324
- const sanctioned = (0, import_node_fs54.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs54.readFileSync)("access-matrix.json", "utf8")) : {};
55444
+ const sanctioned = (0, import_node_fs55.existsSync)("access-matrix.json") ? loadSanctionedAdmins((0, import_node_fs55.readFileSync)("access-matrix.json", "utf8")) : {};
55325
55445
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess, sanctioned);
55326
55446
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
55327
55447
  if (!report.ok) process.exitCode = 1;
@@ -55352,7 +55472,7 @@ function directoryBytes(path2) {
55352
55472
  let total = 0;
55353
55473
  let entries;
55354
55474
  try {
55355
- entries = (0, import_node_fs54.readdirSync)(path2, { withFileTypes: true });
55475
+ entries = (0, import_node_fs55.readdirSync)(path2, { withFileTypes: true });
55356
55476
  } catch {
55357
55477
  return 0;
55358
55478
  }
@@ -55361,7 +55481,7 @@ function directoryBytes(path2) {
55361
55481
  if (entry.isDirectory()) total += directoryBytes(child2);
55362
55482
  else {
55363
55483
  try {
55364
- total += (0, import_node_fs54.statSync)(child2).size;
55484
+ total += (0, import_node_fs55.statSync)(child2).size;
55365
55485
  } catch {
55366
55486
  }
55367
55487
  }
@@ -55369,25 +55489,25 @@ function directoryBytes(path2) {
55369
55489
  return total;
55370
55490
  }
55371
55491
  function listDirEntries(dir) {
55372
- return (0, import_node_fs54.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
55492
+ return (0, import_node_fs55.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
55373
55493
  }
55374
55494
  function readInstalledPluginRefs(configRoot) {
55375
55495
  const p = installedPluginsPathForConfig(configRoot);
55376
- if (!(0, import_node_fs54.existsSync)(p)) return [];
55496
+ if (!(0, import_node_fs55.existsSync)(p)) return [];
55377
55497
  try {
55378
- return installedPluginPaths((0, import_node_fs54.readFileSync)(p, "utf8"));
55498
+ return installedPluginPaths((0, import_node_fs55.readFileSync)(p, "utf8"));
55379
55499
  } catch {
55380
55500
  return null;
55381
55501
  }
55382
55502
  }
55383
55503
  function pluginCacheFsDeps(configRoot, dirBytes) {
55384
55504
  return {
55385
- exists: (p) => (0, import_node_fs54.existsSync)(p),
55386
- listVersionDirs: (root) => (0, import_node_fs54.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
55505
+ exists: (p) => (0, import_node_fs55.existsSync)(p),
55506
+ listVersionDirs: (root) => (0, import_node_fs55.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
55387
55507
  dirBytes,
55388
- listStagingDirs: (root) => (0, import_node_fs54.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
55508
+ listStagingDirs: (root) => (0, import_node_fs55.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
55389
55509
  try {
55390
- return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path53.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs54.statSync)(p).mtimeMs) };
55510
+ return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path53.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs55.statSync)(p).mtimeMs) };
55391
55511
  } catch {
55392
55512
  return { name: d.name, mtimeMs: Date.now() };
55393
55513
  }
@@ -55402,9 +55522,9 @@ function stagingApplyFsGuard(configRoot) {
55402
55522
  referencedPaths: () => readInstalledPluginRefs(configRoot),
55403
55523
  mtimeMs: (name) => {
55404
55524
  const p = (0, import_node_path53.join)(stagingRoot, name);
55405
- if (!(0, import_node_fs54.existsSync)(p)) return null;
55525
+ if (!(0, import_node_fs55.existsSync)(p)) return null;
55406
55526
  try {
55407
- return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs54.statSync)(q).mtimeMs);
55527
+ return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs55.statSync)(q).mtimeMs);
55408
55528
  } catch {
55409
55529
  return null;
55410
55530
  }
@@ -55430,7 +55550,7 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
55430
55550
  { withBytes: true, configRoot, includeStaging: surface !== "codex" }
55431
55551
  );
55432
55552
  const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
55433
- const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs54.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
55553
+ const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs55.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard(configRoot)) : void 0;
55434
55554
  const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
55435
55555
  if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
55436
55556
  else console.log(renderPluginCachePlan(plan, result));