@mutmutco/cli 2.34.1 → 2.35.0

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.
package/dist/main.cjs CHANGED
@@ -3392,7 +3392,7 @@ var program = new Command();
3392
3392
 
3393
3393
  // src/index.ts
3394
3394
  var import_promises5 = require("node:fs/promises");
3395
- var import_node_fs15 = require("node:fs");
3395
+ var import_node_fs16 = require("node:fs");
3396
3396
 
3397
3397
  // src/rules-sync.ts
3398
3398
  function normalizeEol(s) {
@@ -3427,7 +3427,7 @@ var import_node_child_process10 = require("node:child_process");
3427
3427
 
3428
3428
  // src/cli-shared.ts
3429
3429
  var import_promises = require("node:fs/promises");
3430
- var import_node_fs6 = require("node:fs");
3430
+ var import_node_fs7 = require("node:fs");
3431
3431
  var import_node_crypto2 = require("node:crypto");
3432
3432
  var import_node_child_process4 = require("node:child_process");
3433
3433
  var import_node_util4 = require("node:util");
@@ -4109,10 +4109,19 @@ async function hubAuthToken(deps) {
4109
4109
  }
4110
4110
 
4111
4111
  // src/stdin-inject.ts
4112
+ var import_node_fs6 = require("node:fs");
4112
4113
  var injectedStdin;
4114
+ function stdinHasPipedInput(statFd = () => (0, import_node_fs6.fstatSync)(0)) {
4115
+ try {
4116
+ const stat = statFd();
4117
+ return stat.isFIFO() || stat.isFile();
4118
+ } catch {
4119
+ return false;
4120
+ }
4121
+ }
4113
4122
  async function readStdin() {
4114
4123
  if (injectedStdin !== void 0) return injectedStdin;
4115
- if (process.stdin.isTTY !== false) return "";
4124
+ if (!stdinHasPipedInput()) return "";
4116
4125
  const chunks = [];
4117
4126
  for await (const chunk of process.stdin) chunks.push(chunk);
4118
4127
  return Buffer.concat(chunks).toString("utf8");
@@ -4161,7 +4170,7 @@ function sessionDeps() {
4161
4170
  env: process.env,
4162
4171
  readPersisted: () => {
4163
4172
  try {
4164
- return (0, import_node_fs6.readFileSync)(SESSION_FILE, "utf8");
4173
+ return (0, import_node_fs7.readFileSync)(SESSION_FILE, "utf8");
4165
4174
  } catch {
4166
4175
  return null;
4167
4176
  }
@@ -4174,8 +4183,8 @@ function sessionDeps() {
4174
4183
  var resolveSessionId = () => resolveSession(sessionDeps());
4175
4184
  function persistSession(id) {
4176
4185
  try {
4177
- (0, import_node_fs6.mkdirSync)(".mmi", { recursive: true });
4178
- (0, import_node_fs6.writeFileSync)(SESSION_FILE, id, "utf8");
4186
+ (0, import_node_fs7.mkdirSync)(".mmi", { recursive: true });
4187
+ (0, import_node_fs7.writeFileSync)(SESSION_FILE, id, "utf8");
4179
4188
  } catch {
4180
4189
  }
4181
4190
  }
@@ -4267,7 +4276,9 @@ async function resolveTextArg(input, deps, labels) {
4267
4276
  const source = input.file ?? "";
4268
4277
  const text = source === "-" ? await deps.readStdin() : await deps.readFile(source, "utf8");
4269
4278
  if (text.trim().length === 0) {
4270
- throw new Error(`${labels.file} produced an empty ${labels.noun}`);
4279
+ throw new Error(
4280
+ source === "-" ? `${labels.file} - read empty stdin (nothing piped \u2014 pass a heredoc/pipe, or ${labels.file} <path>)` : `${labels.file} produced an empty ${labels.noun}`
4281
+ );
4271
4282
  }
4272
4283
  return text;
4273
4284
  }
@@ -4287,7 +4298,7 @@ function resolveIssueTitle(input, deps) {
4287
4298
  }
4288
4299
 
4289
4300
  // src/saga-capture.ts
4290
- var import_node_fs7 = require("node:fs");
4301
+ var import_node_fs8 = require("node:fs");
4291
4302
  var import_node_os2 = require("node:os");
4292
4303
  var import_node_path6 = require("node:path");
4293
4304
  function parseHookInput(stdin) {
@@ -4314,16 +4325,16 @@ function resolveCursorTranscriptPath(hook, env = process.env) {
4314
4325
  const slug = cursorProjectSlug(workspaceRoot);
4315
4326
  const transcriptsDir = (0, import_node_path6.join)(cursorProjectsRoot(env), slug, "agent-transcripts");
4316
4327
  const nested = (0, import_node_path6.join)(transcriptsDir, conversationId, `${conversationId}.jsonl`);
4317
- if ((0, import_node_fs7.existsSync)(nested)) return nested;
4328
+ if ((0, import_node_fs8.existsSync)(nested)) return nested;
4318
4329
  const flat = (0, import_node_path6.join)(transcriptsDir, `${conversationId}.jsonl`);
4319
- if ((0, import_node_fs7.existsSync)(flat)) return flat;
4330
+ if ((0, import_node_fs8.existsSync)(flat)) return flat;
4320
4331
  return void 0;
4321
4332
  }
4322
4333
  function resolveTranscriptPath(hook, env = process.env) {
4323
4334
  const fromHook = (hook.transcript_path ?? hook.transcriptPath)?.trim();
4324
4335
  if (fromHook) return fromHook;
4325
4336
  const fromEnv = env.CURSOR_TRANSCRIPT_PATH?.trim();
4326
- if (fromEnv && (0, import_node_fs7.existsSync)(fromEnv)) return fromEnv;
4337
+ if (fromEnv && (0, import_node_fs8.existsSync)(fromEnv)) return fromEnv;
4327
4338
  const surface = env.MMI_AGENT_SURFACE?.trim();
4328
4339
  if (surface === "cursor" || env.CURSOR_TRACE_ID || env.CURSOR_SESSION_ID) {
4329
4340
  return resolveCursorTranscriptPath(hook, env);
@@ -5072,24 +5083,24 @@ function registerSagaCommands(program3) {
5072
5083
  // src/honcho-commands.ts
5073
5084
  var import_node_child_process5 = require("node:child_process");
5074
5085
  var import_promises3 = require("node:fs/promises");
5075
- var import_node_fs9 = require("node:fs");
5086
+ var import_node_fs10 = require("node:fs");
5076
5087
  var import_node_path8 = require("node:path");
5077
5088
 
5078
5089
  // src/honcho-ingest-skip.ts
5079
- var import_node_fs8 = require("node:fs");
5090
+ var import_node_fs9 = require("node:fs");
5080
5091
  var import_node_path7 = require("node:path");
5081
5092
  var INGEST_SKIP_FILE = ".mmi/honcho/last-skip.json";
5082
5093
  function recordIngestSkip(record) {
5083
5094
  try {
5084
- (0, import_node_fs8.mkdirSync)((0, import_node_path7.dirname)(INGEST_SKIP_FILE), { recursive: true });
5085
- (0, import_node_fs8.writeFileSync)(INGEST_SKIP_FILE, JSON.stringify(record), "utf8");
5095
+ (0, import_node_fs9.mkdirSync)((0, import_node_path7.dirname)(INGEST_SKIP_FILE), { recursive: true });
5096
+ (0, import_node_fs9.writeFileSync)(INGEST_SKIP_FILE, JSON.stringify(record), "utf8");
5086
5097
  } catch {
5087
5098
  }
5088
5099
  }
5089
5100
  function readIngestSkip() {
5090
5101
  try {
5091
- if (!(0, import_node_fs8.existsSync)(INGEST_SKIP_FILE)) return null;
5092
- const o = JSON.parse((0, import_node_fs8.readFileSync)(INGEST_SKIP_FILE, "utf8"));
5102
+ if (!(0, import_node_fs9.existsSync)(INGEST_SKIP_FILE)) return null;
5103
+ const o = JSON.parse((0, import_node_fs9.readFileSync)(INGEST_SKIP_FILE, "utf8"));
5093
5104
  return o?.reason && o?.surface && o?.ts ? o : null;
5094
5105
  } catch {
5095
5106
  return null;
@@ -5097,7 +5108,7 @@ function readIngestSkip() {
5097
5108
  }
5098
5109
  function clearIngestSkip() {
5099
5110
  try {
5100
- if ((0, import_node_fs8.existsSync)(INGEST_SKIP_FILE)) (0, import_node_fs8.unlinkSync)(INGEST_SKIP_FILE);
5111
+ if ((0, import_node_fs9.existsSync)(INGEST_SKIP_FILE)) (0, import_node_fs9.unlinkSync)(INGEST_SKIP_FILE);
5101
5112
  } catch {
5102
5113
  }
5103
5114
  }
@@ -5159,7 +5170,8 @@ function formatVaultPointer(p) {
5159
5170
  ``,
5160
5171
  `enumerate actual keys: mmi-cli secrets list`,
5161
5172
  `read one: mmi-cli secrets get <stage>/<KEY> (e.g. main/GOOGLE_CLIENT_ID)`,
5162
- `set a project-tier key: mmi-cli secrets set <KEY> (value via stdin; org tier needs a master grant)`
5173
+ `set a project-tier key: mmi-cli secrets set <KEY> (value via stdin; org tier needs a master grant)`,
5174
+ `copy provider keys: mmi-cli secrets copy --from rc --to dev --keys RECALL_API_KEY,GEMINI_API_KEY`
5163
5175
  ];
5164
5176
  return lines.join("\n");
5165
5177
  }
@@ -5503,6 +5515,54 @@ async function secretsRevoke(deps, repo, login, key, _opts) {
5503
5515
  }
5504
5516
  deps.log(`revoked @${login}'s access to ${key} in ${repo}`);
5505
5517
  }
5518
+ var SECRET_COPY_BLOCKED_RE = /(?:ENC_KEY|ENCRYPTION_KEY|SECRET_KEY_BASE)/i;
5519
+ function isSecretCopyBlocked(key) {
5520
+ const slash = key.indexOf("/");
5521
+ const leaf = slash === -1 ? key : key.slice(slash + 1);
5522
+ return SECRET_COPY_BLOCKED_RE.test(leaf);
5523
+ }
5524
+ function copyTierKey(stage2, leaf) {
5525
+ return `${stage2}/${leaf}`;
5526
+ }
5527
+ async function secretsCopy(deps, opts) {
5528
+ if (opts.from === opts.to) {
5529
+ deps.err("secrets copy: --from and --to must differ");
5530
+ return false;
5531
+ }
5532
+ const keys = [...new Set(opts.keys.map((k) => k.trim()).filter(Boolean))];
5533
+ if (!keys.length) {
5534
+ deps.err("secrets copy: --keys required (comma-separated allowlist)");
5535
+ return false;
5536
+ }
5537
+ for (const key of keys) {
5538
+ if (!isValidSecretKey(key)) {
5539
+ deps.err(`invalid secret key ${JSON.stringify(key)}`);
5540
+ return false;
5541
+ }
5542
+ if (isSecretCopyBlocked(key)) {
5543
+ deps.err(`secrets copy: ${key} is not copyable \u2014 generate encryption/stage-distinct keys per stage`);
5544
+ return false;
5545
+ }
5546
+ }
5547
+ for (const key of keys) {
5548
+ const leaf = secretLeafName(key);
5549
+ const srcKey = copyTierKey(opts.from, leaf);
5550
+ const dstKey = copyTierKey(opts.to, leaf);
5551
+ const value = await fetchSecretValue(deps, srcKey, opts);
5552
+ if (!value) {
5553
+ deps.err(`secrets copy: could not read source ${srcKey}`);
5554
+ return false;
5555
+ }
5556
+ if (opts.dryRun) {
5557
+ deps.log(`would copy ${srcKey} \u2192 ${dstKey}`);
5558
+ continue;
5559
+ }
5560
+ const ok = await putSecret(deps, dstKey, value, opts);
5561
+ if (!ok) return false;
5562
+ deps.log(`copied ${srcKey} \u2192 ${dstKey}`);
5563
+ }
5564
+ return true;
5565
+ }
5506
5566
  async function secretsUse(deps, key, opts) {
5507
5567
  const slug = await vaultSlug(deps, opts);
5508
5568
  const tier = classifyTier(slug, key);
@@ -5873,7 +5933,7 @@ function honchoThrottlePath(key) {
5873
5933
  function honchoIngestDue(path2, intervalMs, now = Date.now()) {
5874
5934
  if (intervalMs <= 0) return true;
5875
5935
  try {
5876
- const last = Number((0, import_node_fs9.readFileSync)(path2, "utf8").trim()) || 0;
5936
+ const last = Number((0, import_node_fs10.readFileSync)(path2, "utf8").trim()) || 0;
5877
5937
  return now - last >= intervalMs;
5878
5938
  } catch {
5879
5939
  return true;
@@ -5881,8 +5941,8 @@ function honchoIngestDue(path2, intervalMs, now = Date.now()) {
5881
5941
  }
5882
5942
  function markHonchoIngest(path2, now = Date.now()) {
5883
5943
  try {
5884
- (0, import_node_fs9.mkdirSync)((0, import_node_path8.dirname)(path2), { recursive: true });
5885
- (0, import_node_fs9.writeFileSync)(path2, String(now), "utf8");
5944
+ (0, import_node_fs10.mkdirSync)((0, import_node_path8.dirname)(path2), { recursive: true });
5945
+ (0, import_node_fs10.writeFileSync)(path2, String(now), "utf8");
5886
5946
  } catch {
5887
5947
  }
5888
5948
  }
@@ -6165,7 +6225,7 @@ async function syncDocs(deps, docs2 = SYNCED_DOCS) {
6165
6225
  }
6166
6226
 
6167
6227
  // src/session-start.ts
6168
- var import_node_fs10 = require("node:fs");
6228
+ var import_node_fs11 = require("node:fs");
6169
6229
  var import_node_path9 = require("node:path");
6170
6230
  async function runBufferedStep(step) {
6171
6231
  const lines = [];
@@ -6218,11 +6278,11 @@ function spawnDetachedSelf(args, deps) {
6218
6278
  function planStoreLines(cwd) {
6219
6279
  const mdFiles = (dir, minSize = 0) => {
6220
6280
  const p = (0, import_node_path9.join)(cwd, dir);
6221
- if (!(0, import_node_fs10.existsSync)(p)) return [];
6281
+ if (!(0, import_node_fs11.existsSync)(p)) return [];
6222
6282
  try {
6223
- return (0, import_node_fs10.readdirSync)(p).filter((f) => f.toLowerCase().endsWith(".md")).filter((f) => {
6283
+ return (0, import_node_fs11.readdirSync)(p).filter((f) => f.toLowerCase().endsWith(".md")).filter((f) => {
6224
6284
  try {
6225
- return (0, import_node_fs10.statSync)((0, import_node_path9.join)(p, f)).size >= minSize;
6285
+ return (0, import_node_fs11.statSync)((0, import_node_path9.join)(p, f)).size >= minSize;
6226
6286
  } catch {
6227
6287
  return false;
6228
6288
  }
@@ -6824,6 +6884,73 @@ async function backfillBoardPriorities(options, deps = {}) {
6824
6884
  }
6825
6885
  return result;
6826
6886
  }
6887
+ async function prunePriorityLabels(options, deps = {}) {
6888
+ const cfg = resolveBoardConfig(options.config);
6889
+ const client = deps.client ?? defaultGitHubClient();
6890
+ const collected = await collectBoardItems(cfg, { repo: options.repo }, deps);
6891
+ const issues = collected.items.filter(
6892
+ (item) => item.contentType === "Issue" && item.labels.some((l) => labelToFieldPriority(l))
6893
+ );
6894
+ const concurrency = Math.max(1, options.concurrency ?? 8);
6895
+ const result = {
6896
+ scanned: issues.length,
6897
+ pruned: 0,
6898
+ removedLabels: 0,
6899
+ skippedNoField: 0,
6900
+ failed: 0,
6901
+ details: []
6902
+ };
6903
+ async function work(item) {
6904
+ const priorityLabels = item.labels.filter((l) => labelToFieldPriority(l));
6905
+ if (!priorityLabels.length) return;
6906
+ if (!item.priority) {
6907
+ result.skippedNoField += 1;
6908
+ result.details.push(
6909
+ `${item.ref}: Priority field unset \u2014 kept ${priorityLabels.join(", ")} (run \`mmi-cli board backfill-priority\` first)`
6910
+ );
6911
+ return;
6912
+ }
6913
+ if (options.dryRun) {
6914
+ result.pruned += 1;
6915
+ result.removedLabels += priorityLabels.length;
6916
+ result.details.push(`${item.ref} \u2192 remove ${priorityLabels.join(", ")} (field=${item.priority}) (dry-run)`);
6917
+ return;
6918
+ }
6919
+ try {
6920
+ const removed = [];
6921
+ const alreadyAbsent = [];
6922
+ for (const label of priorityLabels) {
6923
+ try {
6924
+ await client.rest(
6925
+ "DELETE",
6926
+ `repos/${item.repository}/issues/${item.number}/labels/${encodeURIComponent(label)}`
6927
+ );
6928
+ removed.push(label);
6929
+ result.removedLabels += 1;
6930
+ } catch (e) {
6931
+ if (e instanceof GitHubApiError && e.status === 404) {
6932
+ alreadyAbsent.push(label);
6933
+ continue;
6934
+ }
6935
+ throw e;
6936
+ }
6937
+ }
6938
+ result.pruned += 1;
6939
+ const parts = [
6940
+ ...removed.length ? [`removed ${removed.join(", ")}`] : [],
6941
+ ...alreadyAbsent.length ? [`already absent ${alreadyAbsent.join(", ")}`] : []
6942
+ ];
6943
+ result.details.push(`${item.ref} \u2192 ${parts.join("; ")} (field=${item.priority})`);
6944
+ } catch (e) {
6945
+ result.failed += 1;
6946
+ result.details.push(`${item.ref}: ${ghError(e)}`);
6947
+ }
6948
+ }
6949
+ for (let i = 0; i < issues.length; i += concurrency) {
6950
+ await Promise.all(issues.slice(i, i + concurrency).map(work));
6951
+ }
6952
+ return result;
6953
+ }
6827
6954
  async function recoverIssuePriority(client, item) {
6828
6955
  for (const label of item.labels) {
6829
6956
  const fromLabel = labelToFieldPriority(label);
@@ -7278,6 +7405,412 @@ async function runNorthstarContext(io, deps) {
7278
7405
 
7279
7406
  // src/index.ts
7280
7407
  var import_node_path14 = require("node:path");
7408
+
7409
+ // src/merge-ci-policy.ts
7410
+ function resolveMergeCiPolicy(input) {
7411
+ if (input.registryCi === "none") {
7412
+ return { policy: "no-ci", reason: "registry META ci:none" };
7413
+ }
7414
+ if (input.registryRequiredChecks === null) {
7415
+ return { policy: "no-ci", reason: "registry META requiredChecks:null" };
7416
+ }
7417
+ if (Array.isArray(input.registryRequiredChecks) && input.registryRequiredChecks.length === 0) {
7418
+ return { policy: "no-ci", reason: "registry META requiredChecks:[]" };
7419
+ }
7420
+ const ciWorkflows = input.workflowPaths.filter(
7421
+ (p) => /^\.github\/workflows\/[^/]+\.(ya?ml)$/i.test(p.replace(/\\/g, "/"))
7422
+ );
7423
+ if (!ciWorkflows.length) {
7424
+ return { policy: "no-ci", reason: "no .github/workflows CI" };
7425
+ }
7426
+ return { policy: "wait-for-checks", reason: ciWorkflows.join(", ") };
7427
+ }
7428
+ function parseGhPrChecksOutput(stdout) {
7429
+ const text = stdout.trim();
7430
+ if (!text) return "no-checks-reported";
7431
+ if (/^no checks reported/i.test(text)) return "no-checks-reported";
7432
+ const lines = text.split(/\r?\n/).filter((l) => l.trim());
7433
+ if (!lines.length) return "no-checks-reported";
7434
+ let anyPending = false;
7435
+ for (const line of lines) {
7436
+ const parts = line.split(/\s+/);
7437
+ const state = (parts[1] ?? "").toLowerCase();
7438
+ if (state === "fail" || state === "failure" || state === "error") return "failure";
7439
+ if (state === "pass" || state === "success" || state === "skipping" || state === "skipped") continue;
7440
+ anyPending = true;
7441
+ }
7442
+ return anyPending ? "pending" : "success";
7443
+ }
7444
+ var PR_CHECKS_POLL_MS = 3e4;
7445
+ var PR_CHECKS_TIMEOUT_MS = 10 * 6e4;
7446
+ async function waitForPrChecks(deps) {
7447
+ const { policy, reason } = await deps.resolvePolicy();
7448
+ if (policy === "no-ci") {
7449
+ return { policy, status: "skipped", reason };
7450
+ }
7451
+ const now = deps.now ?? (() => Date.now());
7452
+ const deadline = now() + PR_CHECKS_TIMEOUT_MS;
7453
+ let lastDetail = "pending";
7454
+ while (now() < deadline) {
7455
+ const state = await deps.pollChecks();
7456
+ if (state === "success") return { policy, status: "success", detail: lastDetail };
7457
+ if (state === "failure") return { policy, status: "failure", detail: lastDetail };
7458
+ if (state === "no-checks-reported") {
7459
+ lastDetail = "no-checks-reported (waiting for workflow to queue)";
7460
+ await deps.sleep(PR_CHECKS_POLL_MS);
7461
+ continue;
7462
+ }
7463
+ lastDetail = state;
7464
+ await deps.sleep(PR_CHECKS_POLL_MS);
7465
+ }
7466
+ return { policy, status: "timeout", detail: lastDetail };
7467
+ }
7468
+
7469
+ // src/bootstrap-ruleset.ts
7470
+ var PRODUCT_RULESET_NAME = "mmi-product-required-checks";
7471
+ var PRODUCT_GATE_CONTEXT = "gate";
7472
+ function stripRulesetComment(raw) {
7473
+ const parsed = JSON.parse(raw);
7474
+ delete parsed._comment;
7475
+ return parsed;
7476
+ }
7477
+ function rulesetHasGateContext(ruleset, context = PRODUCT_GATE_CONTEXT) {
7478
+ for (const rule of ruleset.rules ?? []) {
7479
+ if (rule.type !== "required_status_checks") continue;
7480
+ for (const check of rule.parameters?.required_status_checks ?? []) {
7481
+ if (check.context === context) return true;
7482
+ }
7483
+ }
7484
+ return false;
7485
+ }
7486
+ function findProductRuleset(rulesets) {
7487
+ return rulesets.find((r) => r.name === PRODUCT_RULESET_NAME);
7488
+ }
7489
+ async function activateProductRuleset(repo, rulesetBody, client) {
7490
+ const list = await client.rest("GET", `repos/${repo}/rulesets`, { timeoutMs: 2e4 });
7491
+ const existing = findProductRuleset(list ?? []);
7492
+ if (existing?.id != null) {
7493
+ const detail = await client.rest("GET", `repos/${repo}/rulesets/${existing.id}`, { timeoutMs: 2e4 });
7494
+ if (detail.enforcement === "active" && rulesetHasGateContext(detail)) {
7495
+ return { action: "skipped", detail: "active ruleset already requires gate" };
7496
+ }
7497
+ await client.rest("PUT", `repos/${repo}/rulesets/${existing.id}`, { body: rulesetBody, timeoutMs: 2e4 });
7498
+ return { action: "updated", detail: `ruleset ${existing.id}` };
7499
+ }
7500
+ await client.rest("POST", `repos/${repo}/rulesets`, { body: rulesetBody, timeoutMs: 2e4 });
7501
+ return { action: "created" };
7502
+ }
7503
+
7504
+ // src/ci-audit.ts
7505
+ var HUB_REPO = "mutmutco/MMI-Hub";
7506
+ var PRODUCT_GATE_CONTEXT2 = "gate";
7507
+ var HUB_GATE_CONTEXTS = ["cli", "infra", "docs"];
7508
+ var PRODUCT_GATE_PATH = ".github/workflows/gate.yml";
7509
+ var PRODUCT_RULESET_REF = ".github/rulesets/mmi-product-required-checks.json";
7510
+ function slugFromRepo(repo) {
7511
+ return (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
7512
+ }
7513
+ function classifyRepo(repo, meta) {
7514
+ if (repo.toLowerCase() === HUB_REPO.toLowerCase()) return "hub";
7515
+ if (meta?.class === "content") return "content";
7516
+ if (meta?.class === "deployable" || meta?.deployModel && meta.deployModel !== "content" && meta.deployModel !== "none") return "deployable";
7517
+ if (meta) return "deployable";
7518
+ return "unknown";
7519
+ }
7520
+ function rulesetStatusChecks(rulesets) {
7521
+ return new Set(rulesets.flatMap((ruleset) => (ruleset.rules || []).filter((rule) => rule.type === "required_status_checks").flatMap((rule) => rule.parameters?.required_status_checks || []).map((check) => check.context).filter((context) => Boolean(context))));
7522
+ }
7523
+ async function restJson(deps, path2, fallback) {
7524
+ try {
7525
+ return await deps.client.rest("GET", path2) ?? fallback;
7526
+ } catch {
7527
+ return fallback;
7528
+ }
7529
+ }
7530
+ async function rulesetDetails(deps, repo, list) {
7531
+ const details = [];
7532
+ for (const ruleset of list) {
7533
+ if (ruleset.id == null || ruleset.rules != null) {
7534
+ details.push(ruleset);
7535
+ continue;
7536
+ }
7537
+ details.push(await restJson(deps, `repos/${repo}/rulesets/${ruleset.id}`, ruleset));
7538
+ }
7539
+ return details;
7540
+ }
7541
+ async function contentExists(deps, repo, branch, path2) {
7542
+ try {
7543
+ const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
7544
+ await deps.client.rest("GET", `repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`);
7545
+ return true;
7546
+ } catch {
7547
+ return false;
7548
+ }
7549
+ }
7550
+ function collectRegistryRepos(projects) {
7551
+ const seen = /* @__PURE__ */ new Set();
7552
+ for (const project2 of projects) {
7553
+ for (const raw of project2.repos ?? []) {
7554
+ const repo = raw.includes("/") ? raw : `mutmutco/${raw}`;
7555
+ seen.add(repo);
7556
+ }
7557
+ }
7558
+ if (!seen.has(HUB_REPO)) seen.add(HUB_REPO);
7559
+ return [...seen].sort((a, b) => a.localeCompare(b));
7560
+ }
7561
+ async function resolveRepoMergeCiPolicy(repo, deps) {
7562
+ const meta = await deps.getProjectMeta(slugFromRepo(repo));
7563
+ const repoClass = classifyRepo(repo, meta);
7564
+ if (repoClass === "content") {
7565
+ return resolveMergeCiPolicy({
7566
+ workflowPaths: [],
7567
+ registryCi: meta?.ci ?? "none",
7568
+ registryRequiredChecks: meta?.requiredChecks ?? []
7569
+ });
7570
+ }
7571
+ const baseBranch = "development";
7572
+ const hasGate = repoClass === "hub" ? true : await contentExists(deps, repo, baseBranch, PRODUCT_GATE_PATH);
7573
+ return resolveMergeCiPolicy({
7574
+ workflowPaths: hasGate ? repoClass === "hub" ? [".github/workflows/gate.yml"] : [PRODUCT_GATE_PATH] : [],
7575
+ registryCi: meta?.ci,
7576
+ registryRequiredChecks: meta?.requiredChecks
7577
+ });
7578
+ }
7579
+ async function auditRepoCi(repo, deps) {
7580
+ const meta = await deps.getProjectMeta(slugFromRepo(repo));
7581
+ const repoClass = classifyRepo(repo, meta);
7582
+ const checks = [];
7583
+ const info = await restJson(deps, `repos/${repo}`, {});
7584
+ const baseBranch = repoClass === "content" ? "main" : "development";
7585
+ checks.push({
7586
+ ok: info.allow_auto_merge === true,
7587
+ label: "allow_auto_merge enabled",
7588
+ detail: info.allow_auto_merge === true ? void 0 : "false or unavailable",
7589
+ remediation: `gh api -X PATCH repos/${repo} -f allow_auto_merge=true -f allow_squash_merge=true -f delete_branch_on_merge=true`
7590
+ });
7591
+ checks.push({
7592
+ ok: info.allow_squash_merge === true,
7593
+ label: "allow_squash_merge enabled",
7594
+ detail: info.allow_squash_merge === true ? void 0 : "false or unavailable"
7595
+ });
7596
+ checks.push({
7597
+ ok: info.delete_branch_on_merge === true,
7598
+ label: "delete_branch_on_merge enabled",
7599
+ detail: info.delete_branch_on_merge === true ? void 0 : "false or unavailable"
7600
+ });
7601
+ const hasGateWorkflow = repoClass === "hub" ? true : repoClass === "content" ? true : await contentExists(deps, repo, baseBranch, PRODUCT_GATE_PATH);
7602
+ if (repoClass === "deployable") {
7603
+ checks.push({
7604
+ ok: hasGateWorkflow,
7605
+ label: "gate workflow committed",
7606
+ detail: hasGateWorkflow ? void 0 : `missing ${PRODUCT_GATE_PATH}`,
7607
+ remediation: `mmi-cli bootstrap apply ${repo} --class deployable --execute (seeds gate.yml)`
7608
+ });
7609
+ checks.push({
7610
+ ok: await contentExists(deps, repo, baseBranch, PRODUCT_RULESET_REF),
7611
+ label: "product ruleset reference committed",
7612
+ detail: `expected ${PRODUCT_RULESET_REF}`,
7613
+ remediation: `mmi-cli bootstrap apply ${repo} --class deployable --execute`
7614
+ });
7615
+ }
7616
+ const rulesetList = await restJson(deps, `repos/${repo}/rulesets?includes_parents=true`, []);
7617
+ const rulesets = await rulesetDetails(deps, repo, rulesetList);
7618
+ const activeBranchRulesets = rulesets.filter((r) => r.target === "branch" && r.enforcement === "active");
7619
+ const statusChecks = rulesetStatusChecks(activeBranchRulesets);
7620
+ if (repoClass === "hub") {
7621
+ const missing = HUB_GATE_CONTEXTS.filter((c) => !statusChecks.has(c));
7622
+ checks.push({
7623
+ ok: missing.length === 0,
7624
+ label: "Hub required status checks active",
7625
+ detail: missing.length ? `missing: ${missing.join(", ")}` : void 0
7626
+ });
7627
+ } else if (repoClass === "deployable") {
7628
+ const missing = [PRODUCT_GATE_CONTEXT2].filter((c) => !statusChecks.has(c));
7629
+ checks.push({
7630
+ ok: missing.length === 0,
7631
+ label: "product gate status check active",
7632
+ detail: missing.length ? `missing context: ${PRODUCT_GATE_CONTEXT2} \u2014 activate ${PRODUCT_RULESET_REF} as a repo ruleset` : void 0,
7633
+ remediation: missing.length ? `Import ${PRODUCT_RULESET_REF} as an active repository ruleset (GitHub \u2192 Settings \u2192 Rules \u2192 Rulesets) \u2014 target: bootstrap --apply automation (#1440)` : void 0
7634
+ });
7635
+ }
7636
+ const registryCi = meta?.ci;
7637
+ const registryRequiredChecks = meta?.requiredChecks;
7638
+ const workflowPaths = hasGateWorkflow && repoClass === "deployable" ? [PRODUCT_GATE_PATH] : [];
7639
+ const { policy, reason } = resolveMergeCiPolicy({
7640
+ workflowPaths,
7641
+ registryCi,
7642
+ registryRequiredChecks
7643
+ });
7644
+ if (repoClass === "content") {
7645
+ const explicitNoCi = registryCi === "none" || registryRequiredChecks === null || Array.isArray(registryRequiredChecks) && registryRequiredChecks.length === 0;
7646
+ checks.push({
7647
+ ok: explicitNoCi,
7648
+ label: "registry META declares intentional no-ci",
7649
+ detail: explicitNoCi ? void 0 : "set ci:none and requiredChecks:[] in registry META",
7650
+ remediation: `mmi-cli project set ${repo} --var ci=none --var requiredChecks=[]`
7651
+ });
7652
+ } else if (repoClass === "deployable") {
7653
+ checks.push({
7654
+ ok: policy === "wait-for-checks",
7655
+ label: "merge CI policy is wait-for-checks",
7656
+ detail: `${policy} (${reason})`
7657
+ });
7658
+ }
7659
+ const ok = checks.every((c) => c.ok);
7660
+ return { repo, class: repoClass, mergePolicy: policy, ok, checks };
7661
+ }
7662
+ async function auditOrgCi(deps, repoFilter) {
7663
+ const projects = await deps.listProjects();
7664
+ if (!projects) {
7665
+ const single = repoFilter ?? HUB_REPO;
7666
+ const report = await auditRepoCi(single, deps);
7667
+ return { ok: report.ok, repos: [report] };
7668
+ }
7669
+ const targets = repoFilter ? [repoFilter] : collectRegistryRepos(projects);
7670
+ const repos = [];
7671
+ for (const repo of targets) {
7672
+ repos.push(await auditRepoCi(repo, deps));
7673
+ }
7674
+ return { ok: repos.every((r) => r.ok), repos };
7675
+ }
7676
+ function renderCiAuditMarkdown(report) {
7677
+ const lines = [
7678
+ `# CI merge-readiness audit`,
7679
+ "",
7680
+ `Fleet: ${report.ok ? "OK" : "GAPS"} (${report.repos.filter((r) => r.ok).length}/${report.repos.length} repos ready)`,
7681
+ "",
7682
+ "| Repo | Class | Policy | OK | Top gap |",
7683
+ "|------|-------|--------|----|---------|"
7684
+ ];
7685
+ for (const r of report.repos) {
7686
+ const gap = r.checks.find((c) => !c.ok);
7687
+ lines.push(`| ${r.repo} | ${r.class} | ${r.mergePolicy} | ${r.ok ? "yes" : "no"} | ${gap?.label ?? "\u2014"} |`);
7688
+ }
7689
+ return lines.join("\n");
7690
+ }
7691
+ function renderCiAuditText(report) {
7692
+ const lines = [`mmi-cli ci audit: ${report.ok ? "OK" : "GAPS"} (${report.repos.length} repos)`];
7693
+ for (const r of report.repos) {
7694
+ lines.push(`
7695
+ ${r.repo} (${r.class}, policy=${r.mergePolicy}) ${r.ok ? "OK" : "GAP"}`);
7696
+ for (const c of r.checks) {
7697
+ lines.push(` ${c.ok ? "OK" : "FAIL"} ${c.label}${c.detail ? ` \u2014 ${c.detail}` : ""}`);
7698
+ }
7699
+ }
7700
+ return lines.join("\n");
7701
+ }
7702
+ async function applyCiReconcileMergeSettings(repo, deps) {
7703
+ const report = await auditRepoCi(repo, deps);
7704
+ const applied = [];
7705
+ const skipped = [];
7706
+ const errors = [];
7707
+ const mergeChecks = report.checks.filter((c) => c.label.startsWith("allow_") || c.label.startsWith("delete_branch"));
7708
+ const needsPatch = mergeChecks.some((c) => !c.ok);
7709
+ if (!needsPatch) {
7710
+ skipped.push("merge settings already canonical");
7711
+ return { repo, applied, skipped, errors };
7712
+ }
7713
+ try {
7714
+ await deps.client.rest("PATCH", `repos/${repo}`, {
7715
+ body: {
7716
+ allow_auto_merge: true,
7717
+ allow_squash_merge: true,
7718
+ delete_branch_on_merge: true
7719
+ }
7720
+ });
7721
+ applied.push("allow_auto_merge, allow_squash_merge, delete_branch_on_merge");
7722
+ } catch (e) {
7723
+ errors.push(e.message);
7724
+ }
7725
+ return { repo, applied, skipped, errors };
7726
+ }
7727
+ async function fetchRulesetSeedBody(deps, repo) {
7728
+ try {
7729
+ const encodedPath = PRODUCT_RULESET_REF.split("/").map(encodeURIComponent).join("/");
7730
+ const file = await deps.client.rest(
7731
+ "GET",
7732
+ `repos/${repo}/contents/${encodedPath}?ref=development`
7733
+ );
7734
+ if (file.encoding !== "base64" || typeof file.content !== "string") return null;
7735
+ return Buffer.from(file.content, "base64").toString("utf8");
7736
+ } catch {
7737
+ return null;
7738
+ }
7739
+ }
7740
+ async function applyCiReconcileRepo(repo, deps) {
7741
+ const merge = await applyCiReconcileMergeSettings(repo, deps);
7742
+ const report = await auditRepoCi(repo, deps);
7743
+ if (report.class !== "deployable") return merge;
7744
+ const gateCheck = report.checks.find((c) => c.label === "product gate status check active");
7745
+ if (gateCheck?.ok) {
7746
+ merge.skipped.push("product ruleset already active");
7747
+ return merge;
7748
+ }
7749
+ const raw = await fetchRulesetSeedBody(deps, repo);
7750
+ if (!raw) {
7751
+ merge.errors.push(`missing ${PRODUCT_RULESET_REF} on development \u2014 run bootstrap apply first`);
7752
+ return merge;
7753
+ }
7754
+ try {
7755
+ const body = stripRulesetComment(raw);
7756
+ const activation = await activateProductRuleset(repo, body, deps.client);
7757
+ if (activation.action === "skipped") merge.skipped.push(activation.detail ?? "product ruleset");
7758
+ else merge.applied.push(`product ruleset ${activation.action}${activation.detail ? `: ${activation.detail}` : ""}`);
7759
+ } catch (e) {
7760
+ merge.errors.push(e.message);
7761
+ }
7762
+ return merge;
7763
+ }
7764
+
7765
+ // src/pr-land.ts
7766
+ var PR_LAND_POLL_MS = 3e4;
7767
+ var PR_LAND_ENQUEUE_TIMEOUT_MS = 10 * 6e4;
7768
+ async function runPrLand(prNumber, options, deps) {
7769
+ const repo = await deps.resolveRepo(prNumber, options.repo);
7770
+ const base2 = { status: "failed", repo, pr: prNumber };
7771
+ if (options.requireTrain !== false) {
7772
+ const verdict = await deps.fetchTrainAuthority(repo);
7773
+ if (!verdict.ok) {
7774
+ return { ...base2, error: `train authority unavailable: ${verdict.error}` };
7775
+ }
7776
+ base2.train = { role: verdict.authority.role, train: verdict.authority.train };
7777
+ if (!verdict.authority.train) {
7778
+ return {
7779
+ ...base2,
7780
+ error: `@${verdict.authority.login ?? "caller"} is ${verdict.authority.role} \u2014 train not authorized on ${repo}; cannot land PR`
7781
+ };
7782
+ }
7783
+ }
7784
+ const ciPolicy = await deps.resolveCiPolicy(repo);
7785
+ base2.ciPolicy = ciPolicy;
7786
+ const checksWait = await deps.waitForChecks(prNumber, repo);
7787
+ base2.checksWait = checksWait;
7788
+ if (checksWait.status === "failure" || checksWait.status === "timeout") {
7789
+ return {
7790
+ ...base2,
7791
+ error: `checks-wait ${checksWait.status}${checksWait.detail ? `: ${checksWait.detail}` : ""}`
7792
+ };
7793
+ }
7794
+ const merge = await deps.mergeAuto(prNumber, repo);
7795
+ base2.mergeStatus = merge.mergeStatus;
7796
+ if (merge.mergeStatus === "failed") {
7797
+ return { ...base2, error: merge.error ?? "merge failed" };
7798
+ }
7799
+ if (merge.mergeStatus === "merged") {
7800
+ return { ...base2, status: "merged" };
7801
+ }
7802
+ const now = deps.now ?? (() => Date.now());
7803
+ const merged = await deps.pollMerged(prNumber, repo, now() + PR_LAND_ENQUEUE_TIMEOUT_MS);
7804
+ if (merged) {
7805
+ return { ...base2, status: "auto-merge-enqueued-then-merged", mergeStatus: "merged" };
7806
+ }
7807
+ return {
7808
+ ...base2,
7809
+ error: "auto-merge enqueued but PR did not reach MERGED within timeout \u2014 poll manually with gh pr view"
7810
+ };
7811
+ }
7812
+
7813
+ // src/index.ts
7281
7814
  var import_node_os5 = require("node:os");
7282
7815
 
7283
7816
  // src/gh-create.ts
@@ -7614,7 +8147,7 @@ function parentLinkFields(result, error) {
7614
8147
  }
7615
8148
 
7616
8149
  // src/report.ts
7617
- var HUB_REPO = "mutmutco/MMI-Hub";
8150
+ var HUB_REPO2 = "mutmutco/MMI-Hub";
7618
8151
  var REPORT_LABEL = "report";
7619
8152
  function findDuplicateReport(source, openReports, threshold = 0.6) {
7620
8153
  const normalizedTitle = normalizeTitle(source.title);
@@ -8298,6 +8831,28 @@ function selectSafeWorktreeCwd(worktrees, targetPath, options) {
8298
8831
  const exists = options?.pathExists ?? (() => true);
8299
8832
  return worktrees.find((w) => !samePath(w.path, targetPath) && exists(w.path))?.path;
8300
8833
  }
8834
+ function isPathUnderDirectory(childPath, parentPath) {
8835
+ const child = normPath(childPath);
8836
+ const parent = normPath(parentPath);
8837
+ if (!child || !parent) return false;
8838
+ if (child === parent) return true;
8839
+ return child.startsWith(`${parent}/`);
8840
+ }
8841
+ function planReleaseCwdBeforeWorktreeRemoval(targetPath, safeCwd, currentCwd) {
8842
+ if (!safeCwd || !isPathUnderDirectory(currentCwd, targetPath)) return void 0;
8843
+ if (samePath(currentCwd, safeCwd)) return void 0;
8844
+ return safeCwd;
8845
+ }
8846
+ function releaseCwdIfUnderWorktree(targetPath, safeCwd, options) {
8847
+ const getCwd = options?.getCwd ?? (() => process.cwd());
8848
+ const chdir = options?.chdir ?? ((path2) => {
8849
+ process.chdir(path2);
8850
+ });
8851
+ const releaseTo = planReleaseCwdBeforeWorktreeRemoval(targetPath, safeCwd, getCwd());
8852
+ if (!releaseTo) return false;
8853
+ chdir(releaseTo);
8854
+ return true;
8855
+ }
8301
8856
  function branchMissingFromList(branch, stdout) {
8302
8857
  const names = stdout.split(/\r?\n/).map((line) => line.replace(/^\*\s*/, "").trim()).filter(Boolean);
8303
8858
  return !names.includes(branch);
@@ -8350,6 +8905,10 @@ async function cleanupPrMergeLocalBranch(branch, options) {
8350
8905
  stageTeardown = { status: "failed", error: errorMessage(e) };
8351
8906
  }
8352
8907
  }
8908
+ releaseCwdIfUnderWorktree(wtPath, safeCwd, {
8909
+ getCwd: options.getCwd,
8910
+ chdir: options.chdir
8911
+ });
8353
8912
  const outcome = await removeWorktreeWithRecovery(wtPath, {
8354
8913
  git,
8355
8914
  sleep: options.sleep ?? defaultSleep,
@@ -8715,7 +9274,7 @@ function decideStage(inputs) {
8715
9274
 
8716
9275
  // src/cursor-plugin-seed.ts
8717
9276
  var import_node_child_process7 = require("node:child_process");
8718
- var import_node_fs11 = require("node:fs");
9277
+ var import_node_fs12 = require("node:fs");
8719
9278
  var import_node_os4 = require("node:os");
8720
9279
  var import_node_path11 = require("node:path");
8721
9280
  var import_node_util6 = require("node:util");
@@ -8744,7 +9303,7 @@ function cursorUserGlobalStatePath() {
8744
9303
  }
8745
9304
  async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
8746
9305
  const dbPath = cursorUserGlobalStatePath();
8747
- if (!(0, import_node_fs11.existsSync)(dbPath)) return void 0;
9306
+ if (!(0, import_node_fs12.existsSync)(dbPath)) return void 0;
8748
9307
  try {
8749
9308
  const { stdout } = await execFileP5("sqlite3", [dbPath, `SELECT value FROM ItemTable WHERE key = '${CURSOR_THIRD_PARTY_STATE_KEY}';`], {
8750
9309
  timeout: 5e3
@@ -8758,11 +9317,11 @@ async function readCursorThirdPartyExtensibilityEnabled(execFileP5) {
8758
9317
  }
8759
9318
  }
8760
9319
  function syncDirContents(src, dest) {
8761
- (0, import_node_fs11.mkdirSync)(dest, { recursive: true });
8762
- for (const name of (0, import_node_fs11.readdirSync)(dest)) {
8763
- (0, import_node_fs11.rmSync)((0, import_node_path11.join)(dest, name), { recursive: true, force: true });
9320
+ (0, import_node_fs12.mkdirSync)(dest, { recursive: true });
9321
+ for (const name of (0, import_node_fs12.readdirSync)(dest)) {
9322
+ (0, import_node_fs12.rmSync)((0, import_node_path11.join)(dest, name), { recursive: true, force: true });
8764
9323
  }
8765
- (0, import_node_fs11.cpSync)(src, dest, { recursive: true });
9324
+ (0, import_node_fs12.cpSync)(src, dest, { recursive: true });
8766
9325
  }
8767
9326
  function releaseTag(releasedVersion) {
8768
9327
  return releasedVersion.startsWith("v") ? releasedVersion : `v${releasedVersion}`;
@@ -8776,7 +9335,7 @@ async function extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFi
8776
9335
  });
8777
9336
  await execFileP5("tar", ["-xf", tarFile, "-C", tmpRoot], { timeout: 6e4 });
8778
9337
  const pluginMmi = (0, import_node_path11.join)(tmpRoot, "plugins", "mmi");
8779
- return (0, import_node_fs11.existsSync)((0, import_node_path11.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
9338
+ return (0, import_node_fs12.existsSync)((0, import_node_path11.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
8780
9339
  } catch {
8781
9340
  return void 0;
8782
9341
  }
@@ -8784,25 +9343,25 @@ async function extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFi
8784
9343
  async function downloadPluginMmiViaGh(tag, tmpRoot) {
8785
9344
  const tarPath = (0, import_node_path11.join)(tmpRoot, "repo.tgz");
8786
9345
  try {
8787
- (0, import_node_fs11.mkdirSync)(tmpRoot, { recursive: true });
9346
+ (0, import_node_fs12.mkdirSync)(tmpRoot, { recursive: true });
8788
9347
  const { stdout } = await execFileBuffer("gh", ghReleaseTarballApiArgs(tag), {
8789
9348
  timeout: 12e4,
8790
9349
  maxBuffer: 100 * 1024 * 1024,
8791
9350
  encoding: "buffer",
8792
9351
  windowsHide: true
8793
9352
  });
8794
- (0, import_node_fs11.writeFileSync)(tarPath, stdout);
9353
+ (0, import_node_fs12.writeFileSync)(tarPath, stdout);
8795
9354
  await execFileBuffer("tar", ["-xzf", tarPath, "-C", tmpRoot], { timeout: 12e4, windowsHide: true });
8796
- const top = (0, import_node_fs11.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
9355
+ const top = (0, import_node_fs12.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
8797
9356
  if (!top) return void 0;
8798
9357
  const pluginMmi = (0, import_node_path11.join)(tmpRoot, top, "plugins", "mmi");
8799
- return (0, import_node_fs11.existsSync)((0, import_node_path11.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
9358
+ return (0, import_node_fs12.existsSync)((0, import_node_path11.join)(pluginMmi, PLUGIN_JSON_REL)) ? pluginMmi : void 0;
8800
9359
  } catch {
8801
9360
  return void 0;
8802
9361
  }
8803
9362
  }
8804
9363
  async function resolvePluginMmiSource(releasedVersion, hubCheckout, tmpRoot, execFileP5) {
8805
- (0, import_node_fs11.mkdirSync)(tmpRoot, { recursive: true });
9364
+ (0, import_node_fs12.mkdirSync)(tmpRoot, { recursive: true });
8806
9365
  const tag = releaseTag(releasedVersion);
8807
9366
  if (hubCheckout) {
8808
9367
  const fromHub = await extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5);
@@ -8829,7 +9388,7 @@ async function applyCursorPluginCacheSeed(input) {
8829
9388
  for (const pin of pinsToSeed) {
8830
9389
  syncDirContents(source, pin.path);
8831
9390
  }
8832
- (0, import_node_fs11.rmSync)(tmpRoot, { recursive: true, force: true });
9391
+ (0, import_node_fs12.rmSync)(tmpRoot, { recursive: true, force: true });
8833
9392
  return true;
8834
9393
  }
8835
9394
 
@@ -9092,6 +9651,12 @@ function cachePathJoin(root, ...parts) {
9092
9651
  const sep = root.includes("\\") ? "\\" : "/";
9093
9652
  return [root.replace(/[\\/]+$/, ""), ...parts].join(sep);
9094
9653
  }
9654
+ function cacheParentDir(root) {
9655
+ const sep = root.includes("\\") ? "\\" : "/";
9656
+ const trimmed = root.replace(/[\\/]+$/, "");
9657
+ const idx = trimmed.lastIndexOf(sep);
9658
+ return idx <= 0 ? root : trimmed.slice(0, idx);
9659
+ }
9095
9660
  function isProtectedCacheDir(name, protectedVersions) {
9096
9661
  const normalized = normalizeVersion(name);
9097
9662
  return name.startsWith(".") || normalized !== void 0 && protectedVersions.has(normalized);
@@ -9122,7 +9687,7 @@ function buildMmiPluginCacheCleanupCheck(input) {
9122
9687
  plannedCount: leftovers.length,
9123
9688
  quarantinePlan: leftovers.map((entry) => ({
9124
9689
  from: entry.path,
9125
- to: cachePathJoin(entry.root, ".mmi-quarantine", stamp, entry.name)
9690
+ to: cachePathJoin(cacheParentDir(entry.root), ".mmi-quarantine", stamp, entry.name)
9126
9691
  }))
9127
9692
  };
9128
9693
  }
@@ -9183,6 +9748,12 @@ var CLAUDE_PLUGIN_HEAL_STEPS = [
9183
9748
  { args: ["plugin", "install", "mmi@mmi"], gated: true },
9184
9749
  { args: ["plugin", "enable", "mmi@mmi"], gated: false }
9185
9750
  ];
9751
+ var CODEX_PLUGIN_RECOVERY = "codex plugin marketplace remove mmi && codex plugin marketplace add mutmutco/MMI-Hub --ref main && codex plugin add mmi@mmi";
9752
+ var CODEX_PLUGIN_HEAL_STEPS = [
9753
+ { args: ["plugin", "marketplace", "remove", "mmi"], gated: false },
9754
+ { args: ["plugin", "marketplace", "add", "mutmutco/MMI-Hub", "--ref", "main"], gated: true },
9755
+ { args: ["plugin", "add", "mmi@mmi"], gated: true }
9756
+ ];
9186
9757
  function healStepAborts(step, ok) {
9187
9758
  return !ok && step.gated;
9188
9759
  }
@@ -9193,7 +9764,7 @@ function pluginRecoveryFix(surface) {
9193
9764
  case "claude-cli":
9194
9765
  return `${claude} # then ${reloadAction(surface)} to reload MMI commands`;
9195
9766
  case "codex":
9196
- return "codex plugin marketplace upgrade mmi && codex plugin add mmi@mmi # then restart Codex";
9767
+ return `${CODEX_PLUGIN_RECOVERY} # then ${reloadAction(surface)}`;
9197
9768
  case "cursor":
9198
9769
  return `in Cursor Dashboard \u2192 Settings \u2192 Plugins, click Update next to the MMI Team Marketplace; then ${reloadAction(surface)} to reload MMI skills + hooks`;
9199
9770
  case "shell":
@@ -9203,7 +9774,7 @@ function pluginRecoveryFix(surface) {
9203
9774
  }
9204
9775
  var PLUGIN_UPDATE_RECIPES = {
9205
9776
  claude: [CLAUDE_PLUGIN_RECOVERY],
9206
- codex: ["codex plugin marketplace upgrade mmi", "codex plugin list # verify mmi@mmi shows the new version"],
9777
+ codex: [CODEX_PLUGIN_RECOVERY, "codex plugin list # verify mmi@mmi shows the released version"],
9207
9778
  cli: ["npm install -g @mutmutco/cli@latest"]
9208
9779
  };
9209
9780
  function highestSemver(versions) {
@@ -9257,7 +9828,7 @@ function isSemverVersion2(v) {
9257
9828
  return typeof v === "string" && /^v?\d+\.\d+\.\d+/.test(v.trim());
9258
9829
  }
9259
9830
  function staleRecordCommand(surface) {
9260
- return surface === "codex" ? "codex plugin marketplace upgrade mmi && codex plugin add mmi@mmi" : CLAUDE_PLUGIN_RECOVERY;
9831
+ return surface === "codex" ? CODEX_PLUGIN_RECOVERY : CLAUDE_PLUGIN_RECOVERY;
9261
9832
  }
9262
9833
  function staleSurfacesFix(stale, releasedVersion) {
9263
9834
  const parts = stale.map((s) => {
@@ -9535,7 +10106,7 @@ async function runStageLiveDown(deps, t) {
9535
10106
 
9536
10107
  // src/stage-runner.ts
9537
10108
  var import_node_child_process8 = require("node:child_process");
9538
- var import_node_fs12 = require("node:fs");
10109
+ var import_node_fs13 = require("node:fs");
9539
10110
  var import_node_path12 = require("node:path");
9540
10111
  var import_node_net2 = require("node:net");
9541
10112
  var import_node_util7 = require("node:util");
@@ -9583,6 +10154,27 @@ function detectStaleEnvFile(exampleContent, targetContent, mtimes) {
9583
10154
  function stageStatePath(cwd = process.cwd()) {
9584
10155
  return (0, import_node_path12.join)(cwd, "tmp", "stage", "state.json");
9585
10156
  }
10157
+ function mergeEnvSecretsIntoFile(content, secrets2) {
10158
+ const lines = content.split(/\r?\n/);
10159
+ const indexByKey = /* @__PURE__ */ new Map();
10160
+ for (let i = 0; i < lines.length; i++) {
10161
+ const trimmed = lines[i].trim();
10162
+ if (!trimmed || trimmed.startsWith("#")) continue;
10163
+ const eq = trimmed.indexOf("=");
10164
+ if (eq === -1) continue;
10165
+ indexByKey.set(trimmed.slice(0, eq).trim(), i);
10166
+ }
10167
+ for (const [key, value] of Object.entries(secrets2)) {
10168
+ const escaped = /[\s#"'\\]/.test(value) ? `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : value;
10169
+ const line = `${key}=${escaped}`;
10170
+ const idx = indexByKey.get(key);
10171
+ if (idx != null) lines[idx] = line;
10172
+ else lines.push(line);
10173
+ }
10174
+ const body = lines.join("\n");
10175
+ return body.endsWith("\n") ? body : `${body}
10176
+ `;
10177
+ }
9586
10178
  var POSIX_ONLY_VERBS = ["cp", "mv", "rm", "ln", "cat", "touch", "chmod", "export"];
9587
10179
  function posixOnlyShellProblems(command, field, platform = process.platform) {
9588
10180
  if (platform !== "win32" || !command?.trim()) return [];
@@ -9643,9 +10235,9 @@ async function shell(command, cwd, timeoutMs) {
9643
10235
  });
9644
10236
  }
9645
10237
  function readState(path2) {
9646
- if (!(0, import_node_fs12.existsSync)(path2)) return null;
10238
+ if (!(0, import_node_fs13.existsSync)(path2)) return null;
9647
10239
  try {
9648
- return JSON.parse((0, import_node_fs12.readFileSync)(path2, "utf8"));
10240
+ return JSON.parse((0, import_node_fs13.readFileSync)(path2, "utf8"));
9649
10241
  } catch {
9650
10242
  return null;
9651
10243
  }
@@ -9697,7 +10289,7 @@ async function stopStage(opts = {}) {
9697
10289
  return { ok: true, action: "stop", statePath, message: "no previous stage state found" };
9698
10290
  }
9699
10291
  await killTree(state.pid);
9700
- (0, import_node_fs12.rmSync)(statePath, { force: true });
10292
+ (0, import_node_fs13.rmSync)(statePath, { force: true });
9701
10293
  return { ok: true, action: "stop", statePath, pid: state.pid, message: `stopped previous stage pid ${state.pid}` };
9702
10294
  }
9703
10295
  async function startStage(config = {}, opts = {}) {
@@ -9706,7 +10298,7 @@ async function startStage(config = {}, opts = {}) {
9706
10298
  const cwd = opts.cwd ?? process.cwd();
9707
10299
  const statePath = opts.statePath ?? stageStatePath(cwd);
9708
10300
  const dir = statePath.slice(0, Math.max(statePath.lastIndexOf("/"), statePath.lastIndexOf("\\")));
9709
- (0, import_node_fs12.mkdirSync)(dir, { recursive: true });
10301
+ (0, import_node_fs13.mkdirSync)(dir, { recursive: true });
9710
10302
  let stagePort;
9711
10303
  if (config.portRange) {
9712
10304
  const [s, e] = config.portRange;
@@ -9718,12 +10310,12 @@ async function startStage(config = {}, opts = {}) {
9718
10310
  if (config.ensureEnv) {
9719
10311
  const target = (0, import_node_path12.join)(cwd, config.ensureEnv.target);
9720
10312
  const example = (0, import_node_path12.join)(cwd, config.ensureEnv.example);
9721
- if (!(0, import_node_fs12.existsSync)(target) && (0, import_node_fs12.existsSync)(example)) {
9722
- (0, import_node_fs12.copyFileSync)(example, target);
9723
- } else if ((0, import_node_fs12.existsSync)(target) && (0, import_node_fs12.existsSync)(example)) {
9724
- const stale = detectStaleEnvFile((0, import_node_fs12.readFileSync)(example, "utf8"), (0, import_node_fs12.readFileSync)(target, "utf8"), {
9725
- exampleMtimeMs: (0, import_node_fs12.statSync)(example).mtimeMs,
9726
- targetMtimeMs: (0, import_node_fs12.statSync)(target).mtimeMs
10313
+ if (!(0, import_node_fs13.existsSync)(target) && (0, import_node_fs13.existsSync)(example)) {
10314
+ (0, import_node_fs13.copyFileSync)(example, target);
10315
+ } else if ((0, import_node_fs13.existsSync)(target) && (0, import_node_fs13.existsSync)(example)) {
10316
+ const stale = detectStaleEnvFile((0, import_node_fs13.readFileSync)(example, "utf8"), (0, import_node_fs13.readFileSync)(target, "utf8"), {
10317
+ exampleMtimeMs: (0, import_node_fs13.statSync)(example).mtimeMs,
10318
+ targetMtimeMs: (0, import_node_fs13.statSync)(target).mtimeMs
9727
10319
  });
9728
10320
  if (stale) {
9729
10321
  const msg = `stale ${config.ensureEnv.target} (${stale}) \u2014 delete it or refresh from ${config.ensureEnv.example} before re-running /stage`;
@@ -9731,6 +10323,9 @@ async function startStage(config = {}, opts = {}) {
9731
10323
  console.error(`mmi-cli stage: ${msg} (allowed via --allow-stale-env)`);
9732
10324
  }
9733
10325
  }
10326
+ if (opts.vaultEnvMerge && Object.keys(opts.vaultEnvMerge).length && (0, import_node_fs13.existsSync)(target)) {
10327
+ (0, import_node_fs13.writeFileSync)(target, mergeEnvSecretsIntoFile((0, import_node_fs13.readFileSync)(target, "utf8"), opts.vaultEnvMerge), "utf8");
10328
+ }
9734
10329
  }
9735
10330
  const extraEnv = {};
9736
10331
  for (const [k, v] of Object.entries(config.env ?? {})) extraEnv[k] = sub(v) ?? v;
@@ -9754,13 +10349,13 @@ async function startStage(config = {}, opts = {}) {
9754
10349
  healthUrl: sub(config.healthUrl?.trim()) || void 0,
9755
10350
  port: stagePort
9756
10351
  };
9757
- (0, import_node_fs12.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf8");
10352
+ (0, import_node_fs13.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf8");
9758
10353
  try {
9759
10354
  if (state.healthUrl) await waitForHealth(state.healthUrl, opts.timeoutMs ?? 6e4, config.healthAnyStatus);
9760
10355
  else await waitForProcessStability(child);
9761
10356
  } catch (e) {
9762
10357
  await killTree(state.pid);
9763
- (0, import_node_fs12.rmSync)(statePath, { force: true });
10358
+ (0, import_node_fs13.rmSync)(statePath, { force: true });
9764
10359
  throw e;
9765
10360
  }
9766
10361
  const result = { ok: true, action: "start", statePath, pid: state.pid, port: stagePort, message: `started stage pid ${state.pid}${stagePort != null ? ` on port ${stagePort}` : ""}` };
@@ -10030,9 +10625,9 @@ async function requireBranch(deps, branch) {
10030
10625
  const current = clean(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
10031
10626
  if (current !== branch) throw new Error(`must run from ${branch}, currently on ${current || "(unknown)"}`);
10032
10627
  }
10033
- var HUB_REPO2 = "mutmutco/MMI-Hub";
10628
+ var HUB_REPO3 = "mutmutco/MMI-Hub";
10034
10629
  function isHubControlRepo(repo) {
10035
- return repo.toLowerCase() === HUB_REPO2.toLowerCase();
10630
+ return repo.toLowerCase() === HUB_REPO3.toLowerCase();
10036
10631
  }
10037
10632
  function projectGetFailureText(e) {
10038
10633
  const err = e;
@@ -10092,7 +10687,7 @@ async function correlateDispatchedRun(deps, workflow, since) {
10092
10687
  "run",
10093
10688
  "list",
10094
10689
  "--repo",
10095
- HUB_REPO2,
10690
+ HUB_REPO3,
10096
10691
  "--workflow",
10097
10692
  workflow,
10098
10693
  "--limit",
@@ -10126,7 +10721,7 @@ async function correlateWorkflowRun(deps, args) {
10126
10721
  "run",
10127
10722
  "list",
10128
10723
  "--repo",
10129
- HUB_REPO2,
10724
+ HUB_REPO3,
10130
10725
  "--workflow",
10131
10726
  args.workflow,
10132
10727
  "--event",
@@ -10154,7 +10749,7 @@ async function correlateWorkflowRun(deps, args) {
10154
10749
  async function watchTenantRun(deps, runId) {
10155
10750
  if (runId == null) return "pending";
10156
10751
  try {
10157
- await deps.run("gh", ["run", "watch", String(runId), "--repo", HUB_REPO2, "--exit-status"]);
10752
+ await deps.run("gh", ["run", "watch", String(runId), "--repo", HUB_REPO3, "--exit-status"]);
10158
10753
  return "success";
10159
10754
  } catch {
10160
10755
  return "failure";
@@ -10375,7 +10970,7 @@ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince
10375
10970
  "run",
10376
10971
  "tenant-publish.yml",
10377
10972
  "--repo",
10378
- HUB_REPO2,
10973
+ HUB_REPO3,
10379
10974
  "-f",
10380
10975
  `slug=${ctx.slug}`,
10381
10976
  "-f",
@@ -11499,7 +12094,7 @@ async function announceRelease(deps, args) {
11499
12094
  }
11500
12095
 
11501
12096
  // src/port-registry.ts
11502
- var import_node_fs13 = require("node:fs");
12097
+ var import_node_fs14 = require("node:fs");
11503
12098
 
11504
12099
  // ../infra/port-geometry.mjs
11505
12100
  var PORT_BLOCK = 100;
@@ -11513,8 +12108,8 @@ function nextPortBlock(registry2) {
11513
12108
  return [base2, base2 + PORT_SPAN];
11514
12109
  }
11515
12110
  function loadPortRegistry(path2) {
11516
- if (!(0, import_node_fs13.existsSync)(path2)) return {};
11517
- const raw = JSON.parse((0, import_node_fs13.readFileSync)(path2, "utf8"));
12111
+ if (!(0, import_node_fs14.existsSync)(path2)) return {};
12112
+ const raw = JSON.parse((0, import_node_fs14.readFileSync)(path2, "utf8"));
11518
12113
  const out = {};
11519
12114
  for (const [key, value] of Object.entries(raw)) {
11520
12115
  if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
@@ -11528,9 +12123,9 @@ function ensurePortRange(repo, path2) {
11528
12123
  const existing = registry2[repo];
11529
12124
  if (existing) return existing;
11530
12125
  const range = nextPortBlock(registry2);
11531
- const raw = (0, import_node_fs13.existsSync)(path2) ? JSON.parse((0, import_node_fs13.readFileSync)(path2, "utf8")) : {};
12126
+ const raw = (0, import_node_fs14.existsSync)(path2) ? JSON.parse((0, import_node_fs14.readFileSync)(path2, "utf8")) : {};
11532
12127
  raw[repo] = range;
11533
- (0, import_node_fs13.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
12128
+ (0, import_node_fs14.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
11534
12129
  return range;
11535
12130
  }
11536
12131
  function portCursorSeed(registry2) {
@@ -11582,7 +12177,7 @@ function safeJson(text, fallback) {
11582
12177
  return fallback;
11583
12178
  }
11584
12179
  }
11585
- async function restJson(deps, path2, fallback) {
12180
+ async function restJson2(deps, path2, fallback) {
11586
12181
  try {
11587
12182
  return await deps.client.rest("GET", path2) ?? fallback;
11588
12183
  } catch {
@@ -11709,7 +12304,7 @@ async function auditRepoAccess(repo, repoClass, owners, deps, projectAdmins = /*
11709
12304
  return { repo, class: repoClass, ok: !findings.some((f) => f.severity === "high"), findings };
11710
12305
  }
11711
12306
  async function auditOrgBasePermission(deps) {
11712
- const org = await restJson(deps, `orgs/${OWNER2}`, {});
12307
+ const org = await restJson2(deps, `orgs/${OWNER2}`, {});
11713
12308
  const perm = org.default_repository_permission;
11714
12309
  if (perm && perm !== "read" && perm !== "none") {
11715
12310
  return [{
@@ -11818,8 +12413,8 @@ var requiredIssueTemplates = [
11818
12413
  var requiredWorkflows = [];
11819
12414
  var requiredProductWorkflows = [".github/workflows/gate.yml"];
11820
12415
  var requiredProductRulesetRef = ".github/rulesets/mmi-product-required-checks.json";
11821
- var HUB_REPO3 = "mutmutco/MMI-Hub";
11822
- var requiredLabels = ["bug", "feature", "task", "priority:urgent", "priority:high", "priority:medium", "priority:low"];
12416
+ var HUB_REPO4 = "mutmutco/MMI-Hub";
12417
+ var requiredLabels = ["bug", "feature", "task"];
11823
12418
  var requiredPriorityOptions = ["Urgent", "High", "Medium", "Low"];
11824
12419
  var strayDefaultLabels = ["documentation", "duplicate", "enhancement", "good first issue", "help wanted", "invalid", "question", "wontfix"];
11825
12420
  var requiredStatusOptions = ["Todo", "In Progress", "In Review", "Done"];
@@ -11858,7 +12453,7 @@ function safeJson2(text, fallback) {
11858
12453
  return fallback;
11859
12454
  }
11860
12455
  }
11861
- async function restJson2(deps, path2, fallback) {
12456
+ async function restJson3(deps, path2, fallback) {
11862
12457
  try {
11863
12458
  return await deps.client.rest("GET", path2) ?? fallback;
11864
12459
  } catch {
@@ -11872,7 +12467,7 @@ async function restPagedJson2(deps, path2, fallback) {
11872
12467
  return fallback;
11873
12468
  }
11874
12469
  }
11875
- async function contentExists(deps, repo, branch, path2) {
12470
+ async function contentExists2(deps, repo, branch, path2) {
11876
12471
  try {
11877
12472
  const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
11878
12473
  await deps.client.rest("GET", `repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`);
@@ -11917,17 +12512,17 @@ function missingRuleTypes(ruleset, required) {
11917
12512
  const types = new Set((ruleset.rules || []).map((rule) => rule.type).filter(Boolean));
11918
12513
  return required.filter((type) => !types.has(type));
11919
12514
  }
11920
- function rulesetStatusChecks(rulesets) {
12515
+ function rulesetStatusChecks2(rulesets) {
11921
12516
  return new Set(rulesets.flatMap((ruleset) => (ruleset.rules || []).filter((rule) => rule.type === "required_status_checks").flatMap((rule) => rule.parameters?.required_status_checks || []).map((check) => check.context).filter((context) => Boolean(context))));
11922
12517
  }
11923
- async function rulesetDetails(deps, repo, list) {
12518
+ async function rulesetDetails2(deps, repo, list) {
11924
12519
  const details = [];
11925
12520
  for (const ruleset of list) {
11926
12521
  if (ruleset.id == null || ruleset.rules != null) {
11927
12522
  details.push(ruleset);
11928
12523
  continue;
11929
12524
  }
11930
- details.push(await restJson2(deps, `repos/${repo}/rulesets/${ruleset.id}`, ruleset));
12525
+ details.push(await restJson3(deps, `repos/${repo}/rulesets/${ruleset.id}`, ruleset));
11931
12526
  }
11932
12527
  return details;
11933
12528
  }
@@ -11946,7 +12541,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
11946
12541
  const branchesWanted = expectedBranches(repoClass, releaseTrack);
11947
12542
  const baseBranch = releaseTrack === "trunk" || repoClass === "content" ? "main" : "development";
11948
12543
  const checks = [];
11949
- const repoInfo = await restJson2(deps, `repos/${repo}`, {});
12544
+ const repoInfo = await restJson3(deps, `repos/${repo}`, {});
11950
12545
  checks.push({ ok: Boolean(repoInfo.default_branch), label: "repo exists" });
11951
12546
  checks.push({ ok: repoInfo.default_branch === baseBranch, label: `default branch is ${baseBranch}`, detail: repoInfo.default_branch || "missing" });
11952
12547
  checks.push({ ok: repoInfo.has_wiki === true, label: "wiki enabled", detail: repoInfo.has_wiki === true ? void 0 : "has_wiki is false or unavailable" });
@@ -11976,29 +12571,29 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
11976
12571
  detail: overgrants.length ? `over-granted: ${overgrants.map((f) => f.actor).join(", ")}` : void 0
11977
12572
  });
11978
12573
  for (const path2 of requiredDocs) {
11979
- checks.push({ ok: await contentExists(deps, repo, baseBranch, path2), label: `bootstrap artifact exists: ${path2}` });
12574
+ checks.push({ ok: await contentExists2(deps, repo, baseBranch, path2), label: `bootstrap artifact exists: ${path2}` });
11980
12575
  }
11981
12576
  for (const path2 of requiredIssueTemplates) {
11982
- checks.push({ ok: await contentExists(deps, repo, baseBranch, path2), label: `issue template exists: ${path2}` });
12577
+ checks.push({ ok: await contentExists2(deps, repo, baseBranch, path2), label: `issue template exists: ${path2}` });
11983
12578
  }
11984
12579
  for (const path2 of requiredWorkflows) {
11985
- checks.push({ ok: await contentExists(deps, repo, baseBranch, path2), label: `automation workflow exists: ${path2}` });
12580
+ checks.push({ ok: await contentExists2(deps, repo, baseBranch, path2), label: `automation workflow exists: ${path2}` });
11986
12581
  }
11987
- if (repo !== HUB_REPO3) {
12582
+ if (repo !== HUB_REPO4 && repoClass === "deployable") {
11988
12583
  for (const path2 of requiredProductWorkflows) {
11989
- checks.push({ ok: await contentExists(deps, repo, baseBranch, path2), label: `gate workflow exists: ${path2}` });
12584
+ checks.push({ ok: await contentExists2(deps, repo, baseBranch, path2), label: `gate workflow exists: ${path2}` });
11990
12585
  }
11991
12586
  checks.push({
11992
- ok: await contentExists(deps, repo, baseBranch, requiredProductRulesetRef),
12587
+ ok: await contentExists2(deps, repo, baseBranch, requiredProductRulesetRef),
11993
12588
  label: "product required-check ruleset reference exists",
11994
12589
  detail: `expected: ${requiredProductRulesetRef} (apply as an active repo ruleset after bootstrap)`
11995
12590
  });
11996
12591
  }
11997
12592
  if (repoClass === "deployable") {
11998
12593
  const trainScript = "scripts/next-version.mjs";
11999
- checks.push({ ok: await contentExists(deps, repo, baseBranch, trainScript), label: `train tooling script exists: ${trainScript}` });
12594
+ checks.push({ ok: await contentExists2(deps, repo, baseBranch, trainScript), label: `train tooling script exists: ${trainScript}` });
12000
12595
  }
12001
- checks.push({ ok: await contentExists(deps, repo, baseBranch, ".cursor/environment.json"), label: "Cursor environment committed" });
12596
+ checks.push({ ok: await contentExists2(deps, repo, baseBranch, ".cursor/environment.json"), label: "Cursor environment committed" });
12002
12597
  const readme = await contentText(deps, repo, baseBranch, "README.md");
12003
12598
  checks.push({
12004
12599
  ok: readme !== null && readme.includes("## Agent context"),
@@ -12006,7 +12601,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
12006
12601
  detail: readme === null ? "README.md not readable via API" : void 0
12007
12602
  });
12008
12603
  const agentRulesPath = `.cursor/rules/${repoSlugFromFullName(repo)}.mdc`;
12009
- const agentRulesOk = await contentExists(deps, repo, baseBranch, agentRulesPath);
12604
+ const agentRulesOk = await contentExists2(deps, repo, baseBranch, agentRulesPath);
12010
12605
  checks.push({
12011
12606
  ok: agentRulesOk,
12012
12607
  label: "Cursor agent rules file exists",
@@ -12019,7 +12614,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
12019
12614
  }
12020
12615
  const strays = strayDefaultLabels.filter((l) => labelNames.has(l));
12021
12616
  checks.push({ ok: strays.length === 0, label: "no stray GitHub-default labels", detail: presentDetail(strays) });
12022
- const actions = await restJson2(deps, `repos/${repo}/actions/permissions`, {});
12617
+ const actions = await restJson3(deps, `repos/${repo}/actions/permissions`, {});
12023
12618
  checks.push({ ok: actions.enabled === true, label: "GitHub Actions enabled" });
12024
12619
  const config = deps.projectMeta ?? null;
12025
12620
  checks.push({
@@ -12124,8 +12719,8 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
12124
12719
  if (fanout != null) checks.push({ ok: fanout, label: `fanout target registered on ${baseBranch}` });
12125
12720
  const projectRegistry = localRegistryCheck(deps, "projects.json", (json) => Array.isArray(json?.projects) && projectRegistryIncludesRepo(json.projects, repo));
12126
12721
  if (projectRegistry != null) checks.push({ ok: projectRegistry, label: "cloud-agent project registry includes repo" });
12127
- const rulesetList = await restJson2(deps, `repos/${repo}/rulesets?includes_parents=true`, []);
12128
- const rulesets = await rulesetDetails(deps, repo, rulesetList);
12722
+ const rulesetList = await restJson3(deps, `repos/${repo}/rulesets?includes_parents=true`, []);
12723
+ const rulesets = await rulesetDetails2(deps, repo, rulesetList);
12129
12724
  const activeOrgRulesets = rulesets.filter(
12130
12725
  (r) => r.source_type === "Organization" && r.target === "branch" && r.enforcement === "active"
12131
12726
  );
@@ -12136,16 +12731,16 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
12136
12731
  label: "covered by an active org ruleset",
12137
12732
  detail: orgRuleset ? void 0 : activeOrgRulesets.length === 0 ? "no active Organization-sourced branch ruleset targets this repo" : `missing rule types: ${missingOrgRuleTypes.join(", ")}`
12138
12733
  });
12139
- if (repo === HUB_REPO3) {
12140
- const statusChecks = rulesetStatusChecks(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
12734
+ if (repo === HUB_REPO4) {
12735
+ const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
12141
12736
  const missing = requiredHubStatusChecks.filter((check) => !statusChecks.has(check));
12142
12737
  checks.push({
12143
12738
  ok: missing.length === 0,
12144
12739
  label: "Hub required status checks configured",
12145
12740
  detail: optionDetail(missing)
12146
12741
  });
12147
- } else {
12148
- const statusChecks = rulesetStatusChecks(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
12742
+ } else if (repoClass === "deployable") {
12743
+ const statusChecks = rulesetStatusChecks2(rulesets.filter((r) => r.target === "branch" && r.enforcement === "active"));
12149
12744
  const missing = requiredProductStatusChecks.filter((check) => !statusChecks.has(check));
12150
12745
  checks.push({
12151
12746
  ok: missing.length === 0,
@@ -13848,11 +14443,11 @@ async function planGraduate(deps, slug, opts = {}) {
13848
14443
  }
13849
14444
 
13850
14445
  // src/atomic-write.ts
13851
- var import_node_fs14 = require("node:fs");
14446
+ var import_node_fs15 = require("node:fs");
13852
14447
  function atomicWriteFileSync(path2, content) {
13853
14448
  const tmp = `${path2}.${process.pid}.tmp`;
13854
- (0, import_node_fs14.writeFileSync)(tmp, content, "utf8");
13855
- (0, import_node_fs14.renameSync)(tmp, path2);
14449
+ (0, import_node_fs15.writeFileSync)(tmp, content, "utf8");
14450
+ (0, import_node_fs15.renameSync)(tmp, path2);
13856
14451
  }
13857
14452
 
13858
14453
  // src/oauth.ts
@@ -14083,7 +14678,7 @@ async function fetchHubVersionInfo(baseUrl) {
14083
14678
  }
14084
14679
  function readRepoVersion() {
14085
14680
  try {
14086
- return JSON.parse((0, import_node_fs15.readFileSync)((0, import_node_path14.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
14681
+ return JSON.parse((0, import_node_fs16.readFileSync)((0, import_node_path14.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
14087
14682
  } catch {
14088
14683
  return void 0;
14089
14684
  }
@@ -14158,6 +14753,22 @@ async function applyClaudePluginHeal(surface, log) {
14158
14753
  }
14159
14754
  return true;
14160
14755
  }
14756
+ async function runCodexPlugin(args) {
14757
+ try {
14758
+ await runHostBin("codex", args, { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
14759
+ return true;
14760
+ } catch {
14761
+ return false;
14762
+ }
14763
+ }
14764
+ async function applyCodexPluginHeal(surface, log) {
14765
+ if (surface !== "codex") return false;
14766
+ log(" \u21BB reinstalling the MMI plugin via `codex plugin` (marketplace remove \u2192 add --ref main \u2192 add)\u2026");
14767
+ for (const step of CODEX_PLUGIN_HEAL_STEPS) {
14768
+ if (healStepAborts(step, await runCodexPlugin([...step.args]))) return false;
14769
+ }
14770
+ return true;
14771
+ }
14161
14772
  var program2 = new Command();
14162
14773
  program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, saga, KB. The engine the plugin SessionStart hook drives.").version(resolveClientVersion());
14163
14774
  async function runRulesSync(opts, io = consoleIo) {
@@ -14193,10 +14804,10 @@ async function runRulesSync(opts, io = consoleIo) {
14193
14804
  for (const entry of fetched) {
14194
14805
  if ("error" in entry) continue;
14195
14806
  const { file, source } = entry;
14196
- const current = (0, import_node_fs15.existsSync)(file) ? await (0, import_promises5.readFile)(file, "utf8") : null;
14807
+ const current = (0, import_node_fs16.existsSync)(file) ? await (0, import_promises5.readFile)(file, "utf8") : null;
14197
14808
  if (needsUpdate(source, current)) {
14198
14809
  const slash = file.lastIndexOf("/");
14199
- if (slash > 0) (0, import_node_fs15.mkdirSync)(file.slice(0, slash), { recursive: true });
14810
+ if (slash > 0) (0, import_node_fs16.mkdirSync)(file.slice(0, slash), { recursive: true });
14200
14811
  await (0, import_promises5.writeFile)(file, normalizeEol(source), "utf8");
14201
14812
  changed++;
14202
14813
  if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
@@ -14222,7 +14833,7 @@ async function runDocsSync(opts, io = consoleIo) {
14222
14833
  return null;
14223
14834
  }
14224
14835
  },
14225
- localContent: async (f) => (0, import_node_fs15.existsSync)(f) ? await (0, import_promises5.readFile)(f, "utf8") : null,
14836
+ localContent: async (f) => (0, import_node_fs16.existsSync)(f) ? await (0, import_promises5.readFile)(f, "utf8") : null,
14226
14837
  writeDoc: async (f, c) => {
14227
14838
  await (0, import_promises5.writeFile)(f, c, "utf8");
14228
14839
  }
@@ -14395,7 +15006,7 @@ function detachPlanSync() {
14395
15006
  }
14396
15007
  }
14397
15008
  function makePlanDeps(cfg, io = consoleIo) {
14398
- const ensureDir = () => (0, import_node_fs15.mkdirSync)(PLANS_DIR, { recursive: true });
15009
+ const ensureDir = () => (0, import_node_fs16.mkdirSync)(PLANS_DIR, { recursive: true });
14399
15010
  return {
14400
15011
  apiUrl: cfg.sagaApiUrl,
14401
15012
  fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
@@ -14403,24 +15014,24 @@ function makePlanDeps(cfg, io = consoleIo) {
14403
15014
  project: async () => (await sagaKey(cfg)).project,
14404
15015
  readLocal: (slug) => {
14405
15016
  try {
14406
- return (0, import_node_fs15.readFileSync)(planPath(slug), "utf8");
15017
+ return (0, import_node_fs16.readFileSync)(planPath(slug), "utf8");
14407
15018
  } catch {
14408
15019
  return null;
14409
15020
  }
14410
15021
  },
14411
15022
  writeLocal: (slug, content) => {
14412
15023
  ensureDir();
14413
- (0, import_node_fs15.writeFileSync)(planPath(slug), content, "utf8");
15024
+ (0, import_node_fs16.writeFileSync)(planPath(slug), content, "utf8");
14414
15025
  },
14415
15026
  removeLocal: (slug) => {
14416
15027
  try {
14417
- (0, import_node_fs15.rmSync)(planPath(slug));
15028
+ (0, import_node_fs16.rmSync)(planPath(slug));
14418
15029
  } catch {
14419
15030
  }
14420
15031
  },
14421
15032
  readMetaRaw: () => {
14422
15033
  try {
14423
- return (0, import_node_fs15.readFileSync)(META_FILE, "utf8");
15034
+ return (0, import_node_fs16.readFileSync)(META_FILE, "utf8");
14424
15035
  } catch {
14425
15036
  return null;
14426
15037
  }
@@ -14431,7 +15042,7 @@ function makePlanDeps(cfg, io = consoleIo) {
14431
15042
  },
14432
15043
  readIndexRaw: () => {
14433
15044
  try {
14434
- return (0, import_node_fs15.readFileSync)(INDEX_FILE, "utf8");
15045
+ return (0, import_node_fs16.readFileSync)(INDEX_FILE, "utf8");
14435
15046
  } catch {
14436
15047
  return null;
14437
15048
  }
@@ -14442,7 +15053,7 @@ function makePlanDeps(cfg, io = consoleIo) {
14442
15053
  },
14443
15054
  readQueueRaw: () => {
14444
15055
  try {
14445
- return (0, import_node_fs15.readFileSync)(QUEUE_FILE, "utf8");
15056
+ return (0, import_node_fs16.readFileSync)(QUEUE_FILE, "utf8");
14446
15057
  } catch {
14447
15058
  return null;
14448
15059
  }
@@ -14632,6 +15243,20 @@ secrets.command("edit <key>").description("alias for set \u2014 replace a secret
14632
15243
  const ok = await secretsEdit(d, key, o);
14633
15244
  if (!ok) process.exitCode = 1;
14634
15245
  }));
15246
+ secrets.command("copy").description("copy provider keys between vault tiers (audit-logged; org-tier source is master-gated)").requiredOption("--from <stage>", "source tier: dev, rc, or main").requiredOption("--to <stage>", "destination tier: dev, rc, or main").requiredOption("--keys <names>", "comma-separated secret names (encryption keys blocked)").option("--dry-run", "report copies without writing").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((o) => withSecrets(async (d) => {
15247
+ const stages = ["dev", "rc", "main"];
15248
+ if (!stages.includes(o.from) || !stages.includes(o.to)) {
15249
+ return fail("secrets copy: --from and --to must be dev, rc, or main");
15250
+ }
15251
+ const ok = await secretsCopy(d, {
15252
+ repo: o.repo,
15253
+ from: o.from,
15254
+ to: o.to,
15255
+ keys: o.keys.split(","),
15256
+ dryRun: o.dryRun
15257
+ });
15258
+ if (!ok) process.exitCode = 1;
15259
+ }));
14635
15260
  secrets.command("rm <key>").description("remove a secret (project tier self-serve; org tier needs a grant)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsRemove(d, key, o)));
14636
15261
  secrets.command("use <key>").description("print guidance on consuming a secret without committing it (no value)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action((key, o) => withSecrets((d) => secretsUse(d, key, o)));
14637
15262
  secrets.command("grant <repo> <login> <key>").description("MASTER-ONLY: grant a project-admin standing access to a specific org-tier secret").action((repo, login, key) => withSecrets((d) => secretsGrant(d, repo, login, key, {})));
@@ -14997,7 +15622,7 @@ oauth.command("verify").description("probe Google authorize with an arbitrary po
14997
15622
  if (mismatch) process.exitCode = 1;
14998
15623
  });
14999
15624
  var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
15000
- issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").requiredOption("--type <type>", "bug | feature | task (sets the matching label)").option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file, or from stdin with -").requiredOption("--priority <priority>", "urgent | high | medium | low (label + board Priority field when configured)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment").action(async (o) => {
15625
+ issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").requiredOption("--type <type>", "bug | feature | task (sets the matching label)").option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file, or from stdin with -").requiredOption("--priority <priority>", "urgent | high | medium | low (sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment").action(async (o) => {
15001
15626
  let args;
15002
15627
  let priority;
15003
15628
  let body;
@@ -15082,12 +15707,12 @@ issue.command("link-child <parent> <child>").description("link an existing issue
15082
15707
  return fail(`issue link-child: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
15083
15708
  }
15084
15709
  });
15085
- program2.command("report").description("file a friction report on the Hub board (GitHub auth, dedups open reports) and print {number,url} JSON").option("--title <title>", "one-line friction summary").option("--title-file <path|->", "read the friction summary from a UTF-8 file, or from stdin with -").option("--body <body>", "report body (markdown)").option("--body-file <path|->", "read report body from a UTF-8 file, or from stdin with -").option("--type <type>", "bug | feature | task (sets the matching label)", "task").option("--priority <priority>", "urgent | high | medium | low (board Priority field when configured)", "medium").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO})`).option("--force", "file a new issue even when an open report looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 report always prints JSON; #682)").action(async (o) => {
15710
+ program2.command("report").description("file a friction report on the Hub board (GitHub auth, dedups open reports) and print {number,url} JSON").option("--title <title>", "one-line friction summary").option("--title-file <path|->", "read the friction summary from a UTF-8 file, or from stdin with -").option("--body <body>", "report body (markdown)").option("--body-file <path|->", "read report body from a UTF-8 file, or from stdin with -").option("--type <type>", "bug | feature | task (sets the matching label)", "task").option("--priority <priority>", "urgent | high | medium | low (sets the board Priority field only, #416)", "medium").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO2})`).option("--force", "file a new issue even when an open report looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 report always prints JSON; #682)").action(async (o) => {
15086
15711
  let body;
15087
15712
  let priority;
15088
15713
  let args;
15089
15714
  let title;
15090
- const targetRepo2 = o.repo ?? HUB_REPO;
15715
+ const targetRepo2 = o.repo ?? HUB_REPO2;
15091
15716
  const sourceRepo = await resolveRepo(void 0);
15092
15717
  try {
15093
15718
  title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises5.readFile, readStdin });
@@ -15282,8 +15907,8 @@ grindCmd.command("estimate").description("Worst-case cost proxy (agent-call unit
15282
15907
  console.log(`ceiling: ${GRIND_COST_CEILING} units \u2014 ${estimate.exceedsCeiling ? "EXCEEDS \u2192 ask human (cap/stuck path)" : "within"}`);
15283
15908
  }
15284
15909
  });
15285
- program2.command("skill-lesson").description("file a skill-lesson on the Hub board (GitHub auth, dedups open lessons) and print {number,url} JSON").requiredOption("--skill <name>", `which skill misfired (${SKILL_NAMES.join(" | ")})`).option("--title <title>", "one-line summary of what misfired").option("--title-file <path|->", "read the one-line summary from a UTF-8 file, or from stdin with -").option("--body <body>", "lesson body: what misfired, the evidence, and the proposed amendment (markdown)").option("--body-file <path|->", "read the lesson body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (board Priority field when configured)", "medium").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO})`).option("--force", "file a new issue even when an open lesson looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 skill-lesson always prints JSON)").action(async (o) => {
15286
- const targetRepo2 = o.repo ?? HUB_REPO;
15910
+ program2.command("skill-lesson").description("file a skill-lesson on the Hub board (GitHub auth, dedups open lessons) and print {number,url} JSON").requiredOption("--skill <name>", `which skill misfired (${SKILL_NAMES.join(" | ")})`).option("--title <title>", "one-line summary of what misfired").option("--title-file <path|->", "read the one-line summary from a UTF-8 file, or from stdin with -").option("--body <body>", "lesson body: what misfired, the evidence, and the proposed amendment (markdown)").option("--body-file <path|->", "read the lesson body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (sets the board Priority field only, #416)", "medium").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO2})`).option("--force", "file a new issue even when an open lesson looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 skill-lesson always prints JSON)").action(async (o) => {
15911
+ const targetRepo2 = o.repo ?? HUB_REPO2;
15287
15912
  const sourceRepo = await resolveRepo(void 0);
15288
15913
  const pluginSha = await resolvePluginSha();
15289
15914
  let skill;
@@ -15354,6 +15979,111 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
15354
15979
  const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo }));
15355
15980
  console.log(JSON.stringify(created));
15356
15981
  });
15982
+ async function listCiWorkflowPaths(cwd = process.cwd()) {
15983
+ const wfDir = (0, import_node_path14.join)(cwd, ".github", "workflows");
15984
+ if (!(0, import_node_fs16.existsSync)(wfDir)) return [];
15985
+ return (0, import_node_fs16.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).map((name) => `.github/workflows/${name}`);
15986
+ }
15987
+ async function resolveMergeCiPolicyForCheckout(repoOpt) {
15988
+ const repo = repoOpt ?? await resolveRepo();
15989
+ if (repo) {
15990
+ return resolveRepoMergeCiPolicy(repo, ciAuditDeps());
15991
+ }
15992
+ const workflowPaths = await listCiWorkflowPaths();
15993
+ return resolveMergeCiPolicy({ workflowPaths });
15994
+ }
15995
+ function ciAuditDeps() {
15996
+ const cfgPromise = loadConfig();
15997
+ return {
15998
+ client: defaultGitHubClient(),
15999
+ listProjects: async () => fetchProjectsList(registryClientDeps(await cfgPromise)),
16000
+ getProjectMeta: async (slug) => fetchProjectBySlug(slug, registryClientDeps(await cfgPromise))
16001
+ };
16002
+ }
16003
+ pr.command("ci-policy").description("report merge CI policy: wait-for-checks vs no-ci (for grind/build agents)").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current checkout)").action(async (o) => {
16004
+ const result = await resolveMergeCiPolicyForCheckout(o.repo);
16005
+ if (o.json) return printLine(JSON.stringify(result));
16006
+ printLine(`merge CI policy: ${result.policy} (${result.reason})`);
16007
+ });
16008
+ pr.command("checks-wait <number>").description("bounded wait for PR checks; skips immediately on no-ci repos (#1432)").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (number, o) => {
16009
+ const repoArgs = o.repo ? ["--repo", o.repo] : [];
16010
+ const result = await waitForPrChecks({
16011
+ resolvePolicy: () => resolveMergeCiPolicyForCheckout(o.repo),
16012
+ pollChecks: async () => {
16013
+ const { stdout } = await execFileP2("gh", ["pr", "checks", number, ...repoArgs], { timeout: GC_GH_TIMEOUT_MS });
16014
+ return parseGhPrChecksOutput(stdout);
16015
+ },
16016
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
16017
+ });
16018
+ if (o.json) printLine(JSON.stringify(result));
16019
+ else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
16020
+ if (result.status === "failure" || result.status === "timeout") process.exitCode = 1;
16021
+ });
16022
+ pr.command("land <number>").description("agent merge path (#1440): train probe \u2192 checks-wait \u2192 merge --auto \u2192 poll enqueued \u2014 development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").action(async (number, o) => {
16023
+ const repoArgs = o.repo ? ["--repo", o.repo] : [];
16024
+ const result = await runPrLand(number, { repo: o.repo, requireTrain: o.requireTrain !== false }, {
16025
+ resolveRepo: async (prNumber, repoOpt) => {
16026
+ if (repoOpt) return repoOpt;
16027
+ const viewed = (await execFileP2("gh", ["pr", "view", prNumber, ...repoArgs, "--json", "headRepository,baseRefName", "--jq", '.headRepository.nameWithOwner + " " + .baseRefName'], { timeout: GC_GH_TIMEOUT_MS })).stdout.trim();
16028
+ const [repo, base2] = viewed.split(/\s+/);
16029
+ if (base2 && base2 !== "development") {
16030
+ throw new Error(`pr land: base branch must be development (got ${base2}) \u2014 promotion merges stay human-only`);
16031
+ }
16032
+ if (!repo) throw new Error("pr land: could not resolve PR repo");
16033
+ return repo;
16034
+ },
16035
+ fetchTrainAuthority: async (repo) => fetchTrainAuthority(repo, registryClientDeps(await loadConfig())),
16036
+ resolveCiPolicy: (repo) => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
16037
+ waitForChecks: (prNumber, repo) => waitForPrChecks({
16038
+ resolvePolicy: () => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
16039
+ pollChecks: async () => {
16040
+ const args = repo ? ["--repo", repo] : [];
16041
+ const { stdout } = await execFileP2("gh", ["pr", "checks", prNumber, ...args], { timeout: GC_GH_TIMEOUT_MS });
16042
+ return parseGhPrChecksOutput(stdout);
16043
+ },
16044
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
16045
+ }),
16046
+ mergeAuto: async (prNumber, repo) => {
16047
+ const args = repo ? ["--repo", repo] : [];
16048
+ try {
16049
+ await execFileP2("gh", buildPrMergeArgs({ number: prNumber, repoArgs: args, method: "--squash", auto: true }), { timeout: GH_MUTATION_TIMEOUT_MS });
16050
+ } catch (e) {
16051
+ const message = String(e.message || "");
16052
+ if (/already been merged/i.test(message)) return { mergeStatus: "merged" };
16053
+ const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
16054
+ if (note) return { mergeStatus: "failed", error: note };
16055
+ if (!basePolicyBlocksImmediateMerge(message)) {
16056
+ return { mergeStatus: "failed", error: message.split("\n")[0] };
16057
+ }
16058
+ return { mergeStatus: "failed", error: `merge blocked: ${message.split("\n")[0]} \u2014 ensure checks are green` };
16059
+ }
16060
+ const state = (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
16061
+ return { mergeStatus: state === "MERGED" ? "merged" : "auto-merge-enqueued" };
16062
+ },
16063
+ pollMerged: async (prNumber, repo, deadlineMs) => {
16064
+ const args = repo ? ["--repo", repo] : [];
16065
+ while (Date.now() < deadlineMs) {
16066
+ const state = (await execFileP2("gh", ["pr", "view", prNumber, ...args, "--json", "state", "--jq", ".state"], { timeout: GC_GH_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
16067
+ if (state === "MERGED") return true;
16068
+ await new Promise((resolve) => setTimeout(resolve, PR_LAND_POLL_MS));
16069
+ }
16070
+ return false;
16071
+ }
16072
+ });
16073
+ if (o.json) printLine(JSON.stringify(result));
16074
+ else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
16075
+ if (result.status === "failed") process.exitCode = 1;
16076
+ else {
16077
+ await execFileP2(process.execPath, [
16078
+ process.argv[1],
16079
+ "pr",
16080
+ "merge",
16081
+ number,
16082
+ ...o.repo ? ["--repo", o.repo] : [],
16083
+ "--squash"
16084
+ ], { timeout: GH_MUTATION_TIMEOUT_MS }).catch(() => void 0);
16085
+ }
16086
+ });
15357
16087
  async function remoteBranchExists2(branch, options = {}) {
15358
16088
  return checkRemoteBranchExists(branch, {
15359
16089
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout
@@ -15362,7 +16092,7 @@ async function remoteBranchExists2(branch, options = {}) {
15362
16092
  var COMPOSE_TIMEOUT_MS = 12e4;
15363
16093
  async function createDeferredWorktreeStore() {
15364
16094
  try {
15365
- const { stdout } = await execFileP2("git", ["rev-parse", "--git-dir"], { timeout: GIT_TIMEOUT_MS });
16095
+ const { stdout } = await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS });
15366
16096
  const registryPath = deferredWorktreesRegistryPath(stdout.trim());
15367
16097
  return {
15368
16098
  read: async () => {
@@ -15393,7 +16123,7 @@ function worktreeRemoveDeps(execGit) {
15393
16123
  }
15394
16124
  function teardownWorktreeStage(worktreePath) {
15395
16125
  return runWorktreeStageTeardown(worktreePath, {
15396
- hasStageState: (wt) => (0, import_node_fs15.existsSync)(stageStatePath(wt)),
16126
+ hasStageState: (wt) => (0, import_node_fs16.existsSync)(stageStatePath(wt)),
15397
16127
  stopRecordedStage: async (wt) => (await stopStage({ cwd: wt })).pid,
15398
16128
  listComposeProjects: async () => {
15399
16129
  const { stdout } = await execFileP2("docker", ["compose", "ls", "--all", "--format", "json"], { timeout: GC_GH_TIMEOUT_MS });
@@ -15404,7 +16134,7 @@ function teardownWorktreeStage(worktreePath) {
15404
16134
  }
15405
16135
  });
15406
16136
  }
15407
- pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").action(async (number, o) => {
16137
+ pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch; on no-ci repos run pr ci-policy / checks-wait first (#1432)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").action(async (number, o) => {
15408
16138
  const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
15409
16139
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
15410
16140
  const headRef = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName", "--jq", ".headRefName"], { timeout: GC_GH_TIMEOUT_MS })).stdout.trim();
@@ -15456,7 +16186,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
15456
16186
  } : await cleanupPrMergeLocalBranch(headRef, {
15457
16187
  beforeWorktrees,
15458
16188
  startingPath,
15459
- pathExists: (p) => (0, import_node_fs15.existsSync)(p),
16189
+ pathExists: (p) => (0, import_node_fs16.existsSync)(p),
15460
16190
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
15461
16191
  teardownWorktreeStage,
15462
16192
  deferredStore,
@@ -15571,6 +16301,25 @@ board.command("backfill-priority").description("set board Priority from priority
15571
16301
  return failGraceful(`board backfill-priority failed: ${e.message}`);
15572
16302
  }
15573
16303
  });
16304
+ board.command("prune-priority-labels").description("remove retired priority:* labels (#416) from issues whose board Priority field is already set").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--dry-run", "report what would be removed without writing").option("--concurrency <n>", "parallel issue edits (default 8)", "8").action(async (o) => {
16305
+ try {
16306
+ const result = await prunePriorityLabels({
16307
+ config: await loadConfigForRepo(o.repo),
16308
+ repo: o.repo,
16309
+ dryRun: o.dryRun,
16310
+ concurrency: Number(o.concurrency) || 8
16311
+ });
16312
+ if (o.json) return console.log(JSON.stringify(result));
16313
+ console.log(
16314
+ `prune-priority-labels: scanned ${result.scanned}, pruned ${result.pruned} (${result.removedLabels} labels), skipped ${result.skippedNoField} (field unset), failed ${result.failed}`
16315
+ );
16316
+ for (const line of result.details.slice(0, 30)) console.log(` ${line}`);
16317
+ if (result.details.length > 30) console.log(` ... +${result.details.length - 30} more`);
16318
+ if (result.failed) process.exitCode = 1;
16319
+ } catch (e) {
16320
+ return failGraceful(`board prune-priority-labels failed: ${e.message}`);
16321
+ }
16322
+ });
15574
16323
  board.command("done <issue>").description("set a board item's Status to Done (does not close the GitHub issue; use `gh issue close`)").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--allow-partial", "return success JSON if the item resolves but the status move fails").action(async (issueRef, o) => {
15575
16324
  try {
15576
16325
  const result = await moveBoardItem({ config: await loadConfigOrDiscover(), selector: issueRef, status: "Done", repo: o.repo, allowPartial: o.allowPartial });
@@ -15609,7 +16358,7 @@ function rawValues(flag) {
15609
16358
  return out;
15610
16359
  }
15611
16360
  function printLine(value) {
15612
- (0, import_node_fs15.writeSync)(1, `${value}
16361
+ (0, import_node_fs16.writeSync)(1, `${value}
15613
16362
  `);
15614
16363
  }
15615
16364
  function stageKeepAlive() {
@@ -15626,10 +16375,26 @@ async function resolveStage() {
15626
16375
  local,
15627
16376
  shell: shellFor(),
15628
16377
  registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
15629
- hasCompose: (0, import_node_fs15.existsSync)((0, import_node_path14.join)(process.cwd(), "docker-compose.yml")),
15630
- hasEnvExample: (0, import_node_fs15.existsSync)((0, import_node_path14.join)(process.cwd(), ".env.example"))
16378
+ hasCompose: (0, import_node_fs16.existsSync)((0, import_node_path14.join)(process.cwd(), "docker-compose.yml")),
16379
+ hasEnvExample: (0, import_node_fs16.existsSync)((0, import_node_path14.join)(process.cwd(), ".env.example"))
15631
16380
  });
15632
16381
  }
16382
+ async function fetchStageVaultEnvMerge() {
16383
+ const cfg = await loadConfig();
16384
+ if (!cfg.sagaApiUrl) return void 0;
16385
+ const read = await fetchProjectBySlugChecked(await repoSlug(), registryClientDeps(cfg)).catch(() => null);
16386
+ if (!read?.ok || !read.project) return void 0;
16387
+ const names = requiredRuntimeSecretNames("dev", read.project.requiredRuntimeSecrets, { includeGoogleOAuth: false });
16388
+ if (!names.length) return void 0;
16389
+ const d = makeSecretsDeps(cfg);
16390
+ const merge = {};
16391
+ for (const name of names) {
16392
+ const key = name.includes("/") ? name : `dev/${name}`;
16393
+ const value = await fetchSecretValue(d, key, {});
16394
+ if (value != null) merge[name.includes("/") ? name.split("/").pop() : name] = value;
16395
+ }
16396
+ return Object.keys(merge).length ? merge : void 0;
16397
+ }
15633
16398
  function stageStepsFor(res, stops = true) {
15634
16399
  if (res.source === "derived" && res.derived) return derivedStagePlan(res.derived, shellFor(), stops);
15635
16400
  if (res.source === "local") return stagePlan(res.config ?? {}, stops);
@@ -15761,6 +16526,7 @@ stage.command("start").description("start the configured local stage process and
15761
16526
  }
15762
16527
  if (res.source === "none") return failGraceful(`stage start: ${res.gap}`);
15763
16528
  const cfg = res.config ?? res.derived.config;
16529
+ const vaultEnvMerge = res.source === "derived" ? await fetchStageVaultEnvMerge() : void 0;
15764
16530
  try {
15765
16531
  const hold = stageKeepAlive();
15766
16532
  let printed = false;
@@ -15768,6 +16534,7 @@ stage.command("start").description("start the configured local stage process and
15768
16534
  const result = await startStage(cfg, {
15769
16535
  timeoutMs: Number(o.timeoutMs || 6e4),
15770
16536
  allowStaleEnv: o.allowStaleEnv,
16537
+ vaultEnvMerge,
15771
16538
  onReady: (ready) => {
15772
16539
  printed = true;
15773
16540
  const reportUrl = reportedStageUrl(res, ready);
@@ -15795,6 +16562,7 @@ stage.command("run").description("force-stop previous stage, build, start, and h
15795
16562
  }
15796
16563
  if (res.source === "none") return failGraceful(`stage run: ${res.gap}`);
15797
16564
  const cfg = res.config ?? res.derived.config;
16565
+ const vaultEnvMerge = res.source === "derived" ? await fetchStageVaultEnvMerge() : void 0;
15798
16566
  try {
15799
16567
  const hold = stageKeepAlive();
15800
16568
  let printed = false;
@@ -15802,6 +16570,7 @@ stage.command("run").description("force-stop previous stage, build, start, and h
15802
16570
  const result = await runStage(cfg, {
15803
16571
  timeoutMs: Number(o.timeoutMs || 6e4),
15804
16572
  allowStaleEnv: o.allowStaleEnv,
16573
+ vaultEnvMerge,
15805
16574
  onReady: (ready) => {
15806
16575
  const reportUrl = reportedStageUrl(res, ready);
15807
16576
  const url = reportUrl ? ` \u2014 ${reportUrl}` : "";
@@ -15981,6 +16750,39 @@ var hotfixCmd = program2.command("hotfix").description("stepwise hotfix orchestr
15981
16750
  hotfixCmd.command("start").description("cherry-pick a merged development PR (or SHA) onto hotfix/vX.Y.Z from origin/main, bump the distribution, open the main-base PR").requiredOption("--from <pr#|sha>", "merged development PR number or commit SHA to cherry-pick").option("--json", "machine-readable output").action(async (o) => runHotfixSub("start", () => runHotfixStart(trainApplyDeps(), { from: o.from }), o.json, renderHotfixStart));
15982
16751
  hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated summary lines for the Hub Slack announcement (#883)").action(async (version, o) => runHotfixSub("release", () => runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile }), o.json, renderHotfixRelease));
15983
16752
  hotfixCmd.command("status [version]").description("derive the full hotfix pipeline state from live git/gh reads and name the exact next subcommand").option("--json", "machine-readable output").action(async (version, o) => runHotfixSub("status", () => runHotfixStatus(trainApplyDeps(), version), o.json, renderHotfixStatus));
16753
+ var ci = program2.command("ci").description("org CI + merge-readiness audit and reconcile");
16754
+ ci.command("audit").description("read-only fleet scan: gate workflow, ruleset contexts, auto-merge, registry META (#1440)").option("--json", "machine-readable output").option("--markdown", "fleet summary table for issue comments").option("--repo <owner/repo>", "audit one repo instead of the full registry").action(async (o) => {
16755
+ const report = await auditOrgCi(ciAuditDeps(), o.repo);
16756
+ if (o.json) console.log(JSON.stringify(report, null, 2));
16757
+ else if (o.markdown) console.log(renderCiAuditMarkdown(report));
16758
+ else console.log(renderCiAuditText(report));
16759
+ if (!report.ok) process.exitCode = 1;
16760
+ });
16761
+ ci.command("reconcile").description("audit + optionally apply merge settings and product ruleset activation (master-admin)").option("--json", "machine-readable output").option("--repo <owner/repo>", "reconcile one repo instead of the full registry").option("--apply", "PATCH merge settings + activate product ruleset when missing (master role required)").action(async (o) => {
16762
+ if (o.apply) {
16763
+ const verdict = await fetchTrainAuthority(HUB_REPO2, registryClientDeps(await loadConfig()));
16764
+ if (!verdict.ok || verdict.authority.role !== "master") {
16765
+ return fail("ci reconcile --apply: master-admin required");
16766
+ }
16767
+ }
16768
+ const deps = ciAuditDeps();
16769
+ const audit = await auditOrgCi(deps, o.repo);
16770
+ const applyResults = o.apply ? await Promise.all(audit.repos.map((r) => applyCiReconcileRepo(r.repo, deps))) : [];
16771
+ const payload = { audit, apply: applyResults };
16772
+ if (o.json) console.log(JSON.stringify(payload, null, 2));
16773
+ else {
16774
+ console.log(renderCiAuditText(audit));
16775
+ if (o.apply) {
16776
+ for (const r of applyResults) {
16777
+ console.log(`
16778
+ ${r.repo}: applied=[${r.applied.join("; ")}] skipped=[${r.skipped.join("; ")}]${r.errors.length ? ` errors=[${r.errors.join("; ")}]` : ""}`);
16779
+ }
16780
+ } else {
16781
+ console.log("\nDry-run \u2014 re-run with --apply to patch merge settings and activate product rulesets (master-admin).");
16782
+ }
16783
+ }
16784
+ if (!audit.ok) process.exitCode = 1;
16785
+ });
15984
16786
  var bootstrap = program2.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").option("--repo <owner/repo>", "target repo").option("--class <class>", "deployable | content", "deployable").option("--json", "machine-readable output").option("--apply", "reserved for future bootstrap execution after explicit master-admin approval").action((o) => {
15985
16787
  if (!o.repo) return fail("bootstrap: required option --repo <owner/repo> not specified");
15986
16788
  if (o.apply) return fail("bootstrap: execution is not implemented yet; use the dry-run plan and the existing /bootstrap skill");
@@ -15999,7 +16801,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
15999
16801
  const report = await verifyBootstrap(repo, o.class, {
16000
16802
  client: defaultGitHubClient(),
16001
16803
  projectMeta: meta,
16002
- readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs15.existsSync)(path2) ? (0, import_node_fs15.readFileSync)(path2, "utf8") : null,
16804
+ readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs16.existsSync)(path2) ? (0, import_node_fs16.readFileSync)(path2, "utf8") : null,
16003
16805
  // requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
16004
16806
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
16005
16807
  requiredGcpApis: (() => {
@@ -16042,12 +16844,12 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
16042
16844
  return fail(`bootstrap apply: ${e.message}`);
16043
16845
  }
16044
16846
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
16045
- if (!(0, import_node_fs15.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
16046
- const manifest = loadBootstrapSeeds((0, import_node_fs15.readFileSync)(manifestPath, "utf8"));
16847
+ if (!(0, import_node_fs16.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; run from the MMI-Hub repo root`);
16848
+ const manifest = loadBootstrapSeeds((0, import_node_fs16.readFileSync)(manifestPath, "utf8"));
16047
16849
  const baseBranch = o.class === "content" ? "main" : "development";
16048
16850
  const slug = parsedRepo.slug;
16049
16851
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
16050
- const readFile5 = (p) => (0, import_node_fs15.existsSync)(p) ? (0, import_node_fs15.readFileSync)(p, "utf8") : null;
16852
+ const readFile5 = (p) => (0, import_node_fs16.existsSync)(p) ? (0, import_node_fs16.readFileSync)(p, "utf8") : null;
16051
16853
  const enc2 = (p) => p.split("/").map(encodeURIComponent).join("/");
16052
16854
  const rawVars = {};
16053
16855
  for (const value of rawValues("--var")) {
@@ -16097,6 +16899,26 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
16097
16899
  applied.push(`${action.action} ${resolved.target}`);
16098
16900
  }
16099
16901
  }
16902
+ if (o.execute && o.class === "deployable") {
16903
+ try {
16904
+ await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-f", "delete_branch_on_merge=true"]);
16905
+ applied.push("merge settings: allow_auto_merge, squash, delete-branch-on-merge");
16906
+ } catch (e) {
16907
+ applied.push(`merge settings (failed: ${e.message})`);
16908
+ }
16909
+ const rulesetSeed = manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
16910
+ if (rulesetSeed) {
16911
+ const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile5);
16912
+ if (rulesetContent) {
16913
+ try {
16914
+ const activation = await activateProductRuleset(repo, stripRulesetComment(rulesetContent), defaultGitHubClient());
16915
+ applied.push(`product ruleset: ${activation.action}${activation.detail ? ` (${activation.detail})` : ""}`);
16916
+ } catch (e) {
16917
+ return failGraceful(`bootstrap apply: product ruleset activation failed: ${e.message}`);
16918
+ }
16919
+ }
16920
+ }
16921
+ }
16100
16922
  if (o.execute) {
16101
16923
  for (const l of manifest.labels) {
16102
16924
  try {
@@ -16150,10 +16972,10 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
16150
16972
  name: vars.NAME || parsedRepo.name
16151
16973
  };
16152
16974
  const readHubFile = async (path2) => {
16153
- const r = await gh(["api", `repos/${HUB_REPO}/contents/${enc2(path2)}?ref=development`]);
16975
+ const r = await gh(["api", `repos/${HUB_REPO2}/contents/${enc2(path2)}?ref=development`]);
16154
16976
  const parsed = JSON.parse(r.stdout);
16155
16977
  if (parsed.encoding !== "base64" || typeof parsed.content !== "string" || !parsed.sha) {
16156
- throw new Error(`could not read ${HUB_REPO}/${path2}`);
16978
+ throw new Error(`could not read ${HUB_REPO2}/${path2}`);
16157
16979
  }
16158
16980
  return { content: Buffer.from(parsed.content, "base64").toString("utf8"), sha: parsed.sha };
16159
16981
  };
@@ -16165,15 +16987,15 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
16165
16987
  applied.push(`fanout: already registered (${parsedRepo.name})`);
16166
16988
  } else {
16167
16989
  const branchName = `bootstrap-register-fanout-${slug}`;
16168
- const headSha = (await gh(["api", `repos/${HUB_REPO}/git/ref/heads/development`, "--jq", ".object.sha"])).stdout.trim();
16990
+ const headSha = (await gh(["api", `repos/${HUB_REPO2}/git/ref/heads/development`, "--jq", ".object.sha"])).stdout.trim();
16169
16991
  try {
16170
- await gh(["api", "-X", "POST", `repos/${HUB_REPO}/git/refs`, "-f", `ref=refs/heads/${branchName}`, "-f", `sha=${headSha}`]);
16992
+ await gh(["api", "-X", "POST", `repos/${HUB_REPO2}/git/refs`, "-f", `ref=refs/heads/${branchName}`, "-f", `sha=${headSha}`]);
16171
16993
  } catch (e) {
16172
16994
  if (!/Reference already exists|already exists/i.test(String(e.message ?? ""))) throw e;
16173
16995
  }
16174
16996
  const branchFileSha = async (path2) => {
16175
16997
  try {
16176
- const r = await gh(["api", `repos/${HUB_REPO}/contents/${enc2(path2)}?ref=${branchName}`, "--jq", ".sha"]);
16998
+ const r = await gh(["api", `repos/${HUB_REPO2}/contents/${enc2(path2)}?ref=${branchName}`, "--jq", ".sha"]);
16177
16999
  return r.stdout.trim() || void 0;
16178
17000
  } catch (e) {
16179
17001
  if (/404|Not Found/i.test(String(e.message ?? ""))) return void 0;
@@ -16182,9 +17004,9 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
16182
17004
  };
16183
17005
  const fanoutBranchSha = await branchFileSha(".github/fanout-targets.json");
16184
17006
  const projectsBranchSha = await branchFileSha("projects.json");
16185
- await gh(contentPutArgs(HUB_REPO, ".github/fanout-targets.json", plan2.fanoutTargets, branchName, fanoutBranchSha));
16186
- await gh(contentPutArgs(HUB_REPO, "projects.json", plan2.projects, branchName, projectsBranchSha));
16187
- const openPrs = await gh(["pr", "list", "--repo", HUB_REPO, "--head", branchName, "--base", "development", "--state", "open", "--json", "number,url"]);
17007
+ await gh(contentPutArgs(HUB_REPO2, ".github/fanout-targets.json", plan2.fanoutTargets, branchName, fanoutBranchSha));
17008
+ await gh(contentPutArgs(HUB_REPO2, "projects.json", plan2.projects, branchName, projectsBranchSha));
17009
+ const openPrs = await gh(["pr", "list", "--repo", HUB_REPO2, "--head", branchName, "--base", "development", "--state", "open", "--json", "number,url"]);
16188
17010
  const prDecision = decideFanoutPrAction(JSON.parse(openPrs.stdout || "[]"));
16189
17011
  if (prDecision.action === "reuse") {
16190
17012
  fanoutPrUrl = prDecision.url;
@@ -16193,7 +17015,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
16193
17015
  "pr",
16194
17016
  "create",
16195
17017
  "--repo",
16196
- HUB_REPO,
17018
+ HUB_REPO2,
16197
17019
  "--base",
16198
17020
  "development",
16199
17021
  "--head",
@@ -16205,7 +17027,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
16205
17027
  ]);
16206
17028
  fanoutPrUrl = created.url;
16207
17029
  }
16208
- await gh(["pr", "merge", fanoutPrUrl, "--repo", HUB_REPO, "--auto", "--squash"]).catch((e) => {
17030
+ await gh(["pr", "merge", fanoutPrUrl, "--repo", HUB_REPO2, "--auto", "--squash"]).catch((e) => {
16209
17031
  if (!/already/i.test(String(e.message ?? ""))) throw e;
16210
17032
  });
16211
17033
  applied.push(`fanout: PR ${fanoutPrUrl} (auto-merge enabled)`);
@@ -16256,16 +17078,16 @@ access.command("audit").description("audit collaborator roles + train-branch pus
16256
17078
  if (o.class !== "deployable" && o.class !== "content") return failGraceful("access audit: --class must be deployable or content");
16257
17079
  targets = [{ repo: o.repo, class: o.class }];
16258
17080
  } else {
16259
- const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs15.existsSync)("projects.json") ? (0, import_node_fs15.readFileSync)("projects.json", "utf8") : null;
17081
+ const projectsJson = registryProjects ? JSON.stringify({ projects: registryProjects }) : (0, import_node_fs16.existsSync)("projects.json") ? (0, import_node_fs16.readFileSync)("projects.json", "utf8") : null;
16260
17082
  if (!projectsJson) return failGraceful("access audit: no project registry \u2014 Hub API unreachable and projects.json not found; run from the MMI-Hub repo root or pass --repo <owner/repo>");
16261
- const fanoutJson = (0, import_node_fs15.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs15.readFileSync)(".github/fanout-targets.json", "utf8") : null;
17083
+ const fanoutJson = (0, import_node_fs16.existsSync)(".github/fanout-targets.json") ? (0, import_node_fs16.readFileSync)(".github/fanout-targets.json", "utf8") : null;
16262
17084
  targets = loadAccessTargets(projectsJson, fanoutJson);
16263
17085
  }
16264
17086
  const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
16265
- const fileMatrix = (0, import_node_fs15.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs15.readFileSync)("access-matrix.json", "utf8")) : {};
17087
+ const fileMatrix = (0, import_node_fs16.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs16.readFileSync)("access-matrix.json", "utf8")) : {};
16266
17088
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
16267
17089
  const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
16268
- const fileContracts = (0, import_node_fs15.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs15.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
17090
+ const fileContracts = (0, import_node_fs16.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs16.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
16269
17091
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
16270
17092
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
16271
17093
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
@@ -16278,7 +17100,7 @@ var installedPluginsPath = (surface = detectSurface(process.env)) => {
16278
17100
  };
16279
17101
  function readInstalledPlugins() {
16280
17102
  try {
16281
- return JSON.parse((0, import_node_fs15.readFileSync)(installedPluginsPath(), "utf8"));
17103
+ return JSON.parse((0, import_node_fs16.readFileSync)(installedPluginsPath(), "utf8"));
16282
17104
  } catch {
16283
17105
  return null;
16284
17106
  }
@@ -16287,7 +17109,7 @@ function installedPluginSources() {
16287
17109
  return ["claude", "codex"].map((surface) => {
16288
17110
  const recordPath = (0, import_node_path14.join)((0, import_node_os5.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
16289
17111
  try {
16290
- return { surface, installed: JSON.parse((0, import_node_fs15.readFileSync)(recordPath, "utf8")), recordPath };
17112
+ return { surface, installed: JSON.parse((0, import_node_fs16.readFileSync)(recordPath, "utf8")), recordPath };
16291
17113
  } catch {
16292
17114
  return { surface, installed: null, recordPath };
16293
17115
  }
@@ -16295,7 +17117,7 @@ function installedPluginSources() {
16295
17117
  }
16296
17118
  function readClaudeSettings() {
16297
17119
  try {
16298
- return JSON.parse((0, import_node_fs15.readFileSync)((0, import_node_path14.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
17120
+ return JSON.parse((0, import_node_fs16.readFileSync)((0, import_node_path14.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
16299
17121
  } catch {
16300
17122
  return null;
16301
17123
  }
@@ -16317,7 +17139,7 @@ function writeProjectInstallRecord(record) {
16317
17139
  const list = file.plugins[MMI_PLUGIN_ID] ?? [];
16318
17140
  list.push(record);
16319
17141
  file.plugins[MMI_PLUGIN_ID] = list;
16320
- (0, import_node_fs15.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
17142
+ (0, import_node_fs16.writeFileSync)(installedPluginsPath(), `${JSON.stringify(file, null, 2)}
16321
17143
  `, "utf8");
16322
17144
  return true;
16323
17145
  } catch {
@@ -16330,9 +17152,9 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
16330
17152
  if (!file) return false;
16331
17153
  if (!file.plugins) file.plugins = {};
16332
17154
  const path2 = installedPluginsPath();
16333
- (0, import_node_fs15.copyFileSync)(path2, `${path2}.bak`);
17155
+ (0, import_node_fs16.copyFileSync)(path2, `${path2}.bak`);
16334
17156
  file.plugins[pluginId] = records;
16335
- (0, import_node_fs15.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
17157
+ (0, import_node_fs16.writeFileSync)(path2, `${JSON.stringify(file, null, 2)}
16336
17158
  `, "utf8");
16337
17159
  return true;
16338
17160
  } catch {
@@ -16345,30 +17167,30 @@ function cursorPluginCacheRoot() {
16345
17167
  function cursorPluginCachePinSnapshots() {
16346
17168
  const root = cursorPluginCacheRoot();
16347
17169
  try {
16348
- return (0, import_node_fs15.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
17170
+ return (0, import_node_fs16.readdirSync)(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => {
16349
17171
  const path2 = (0, import_node_path14.join)(root, entry.name);
16350
17172
  const pluginJson = (0, import_node_path14.join)(path2, ".cursor-plugin", "plugin.json");
16351
17173
  const hooksJson = (0, import_node_path14.join)(path2, "hooks", "hooks.json");
16352
17174
  const cliBundle = (0, import_node_path14.join)(path2, "cli", "dist", "index.cjs");
16353
17175
  let version;
16354
17176
  try {
16355
- const raw = JSON.parse((0, import_node_fs15.readFileSync)(pluginJson, "utf8"));
17177
+ const raw = JSON.parse((0, import_node_fs16.readFileSync)(pluginJson, "utf8"));
16356
17178
  version = typeof raw.version === "string" ? raw.version : void 0;
16357
17179
  } catch {
16358
17180
  version = void 0;
16359
17181
  }
16360
17182
  let isEmpty = true;
16361
17183
  try {
16362
- isEmpty = (0, import_node_fs15.readdirSync)(path2).length === 0;
17184
+ isEmpty = (0, import_node_fs16.readdirSync)(path2).length === 0;
16363
17185
  } catch {
16364
17186
  isEmpty = true;
16365
17187
  }
16366
17188
  return {
16367
17189
  name: entry.name,
16368
17190
  path: path2,
16369
- hasPluginJson: (0, import_node_fs15.existsSync)(pluginJson),
16370
- hasHooksJson: (0, import_node_fs15.existsSync)(hooksJson),
16371
- hasCliBundle: (0, import_node_fs15.existsSync)(cliBundle),
17191
+ hasPluginJson: (0, import_node_fs16.existsSync)(pluginJson),
17192
+ hasHooksJson: (0, import_node_fs16.existsSync)(hooksJson),
17193
+ hasCliBundle: (0, import_node_fs16.existsSync)(cliBundle),
16372
17194
  isEmpty,
16373
17195
  version
16374
17196
  };
@@ -16379,7 +17201,7 @@ function cursorPluginCachePinSnapshots() {
16379
17201
  }
16380
17202
  function hubCheckoutForCursorSeed() {
16381
17203
  const manifest = (0, import_node_path14.join)(process.cwd(), "plugins", "mmi", ".cursor-plugin", "plugin.json");
16382
- return (0, import_node_fs15.existsSync)(manifest) ? process.cwd() : void 0;
17204
+ return (0, import_node_fs16.existsSync)(manifest) ? process.cwd() : void 0;
16383
17205
  }
16384
17206
  function mmiPluginCacheRootSnapshots() {
16385
17207
  const roots = [
@@ -16388,7 +17210,7 @@ function mmiPluginCacheRootSnapshots() {
16388
17210
  ];
16389
17211
  return roots.flatMap(({ surface, root }) => {
16390
17212
  try {
16391
- const entries = (0, import_node_fs15.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
17213
+ const entries = (0, import_node_fs16.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
16392
17214
  name: entry.name,
16393
17215
  path: (0, import_node_path14.join)(root, entry.name),
16394
17216
  isDirectory: entry.isDirectory()
@@ -16401,7 +17223,7 @@ function mmiPluginCacheRootSnapshots() {
16401
17223
  }
16402
17224
  function hasNestedMmiChild(versionDir) {
16403
17225
  try {
16404
- return (0, import_node_fs15.statSync)((0, import_node_path14.join)(versionDir, "mmi")).isDirectory();
17226
+ return (0, import_node_fs16.statSync)((0, import_node_path14.join)(versionDir, "mmi")).isDirectory();
16405
17227
  } catch {
16406
17228
  return false;
16407
17229
  }
@@ -16412,10 +17234,10 @@ function nestedPluginTreeSnapshot() {
16412
17234
  );
16413
17235
  }
16414
17236
  function uniqueQuarantineTarget(path2) {
16415
- if (!(0, import_node_fs15.existsSync)(path2)) return path2;
17237
+ if (!(0, import_node_fs16.existsSync)(path2)) return path2;
16416
17238
  for (let i = 1; i < 100; i += 1) {
16417
17239
  const candidate = `${path2}-${i}`;
16418
- if (!(0, import_node_fs15.existsSync)(candidate)) return candidate;
17240
+ if (!(0, import_node_fs16.existsSync)(candidate)) return candidate;
16419
17241
  }
16420
17242
  return `${path2}-${Date.now()}`;
16421
17243
  }
@@ -16423,10 +17245,10 @@ function quarantinePluginCacheDirs(plan2) {
16423
17245
  let moved = 0;
16424
17246
  for (const move of plan2) {
16425
17247
  try {
16426
- if (!(0, import_node_fs15.existsSync)(move.from)) continue;
17248
+ if (!(0, import_node_fs16.existsSync)(move.from)) continue;
16427
17249
  const target = uniqueQuarantineTarget(move.to);
16428
- (0, import_node_fs15.mkdirSync)((0, import_node_path14.dirname)(target), { recursive: true });
16429
- (0, import_node_fs15.renameSync)(move.from, target);
17250
+ (0, import_node_fs16.mkdirSync)((0, import_node_path14.dirname)(target), { recursive: true });
17251
+ (0, import_node_fs16.renameSync)(move.from, target);
16430
17252
  moved += 1;
16431
17253
  } catch {
16432
17254
  }
@@ -16444,23 +17266,23 @@ async function robocopyMirrorEmpty(emptyDir, target) {
16444
17266
  }
16445
17267
  async function clearNestedPluginTreeDir(targetPath) {
16446
17268
  try {
16447
- if (!(0, import_node_fs15.existsSync)(targetPath)) return true;
17269
+ if (!(0, import_node_fs16.existsSync)(targetPath)) return true;
16448
17270
  if (isWin) {
16449
17271
  const emptyDir = (0, import_node_path14.join)((0, import_node_os5.tmpdir)(), `mmi-empty-${Date.now()}`);
16450
- (0, import_node_fs15.mkdirSync)(emptyDir, { recursive: true });
17272
+ (0, import_node_fs16.mkdirSync)(emptyDir, { recursive: true });
16451
17273
  try {
16452
17274
  await robocopyMirrorEmpty(emptyDir, targetPath);
16453
- (0, import_node_fs15.rmSync)(targetPath, { recursive: true, force: true });
17275
+ (0, import_node_fs16.rmSync)(targetPath, { recursive: true, force: true });
16454
17276
  } finally {
16455
17277
  try {
16456
- (0, import_node_fs15.rmSync)(emptyDir, { recursive: true, force: true });
17278
+ (0, import_node_fs16.rmSync)(emptyDir, { recursive: true, force: true });
16457
17279
  } catch {
16458
17280
  }
16459
17281
  }
16460
- return !(0, import_node_fs15.existsSync)(targetPath);
17282
+ return !(0, import_node_fs16.existsSync)(targetPath);
16461
17283
  }
16462
- (0, import_node_fs15.rmSync)(targetPath, { recursive: true, force: true });
16463
- return !(0, import_node_fs15.existsSync)(targetPath);
17284
+ (0, import_node_fs16.rmSync)(targetPath, { recursive: true, force: true });
17285
+ return !(0, import_node_fs16.existsSync)(targetPath);
16464
17286
  } catch {
16465
17287
  return false;
16466
17288
  }
@@ -16476,8 +17298,8 @@ async function applyNestedPluginTreeCleanup(paths, log) {
16476
17298
  var gitignorePath = () => (0, import_node_path14.join)(process.cwd(), ".gitignore");
16477
17299
  function readTextFile(path2) {
16478
17300
  try {
16479
- if (!(0, import_node_fs15.existsSync)(path2)) return null;
16480
- return (0, import_node_fs15.readFileSync)(path2, "utf8");
17301
+ if (!(0, import_node_fs16.existsSync)(path2)) return null;
17302
+ return (0, import_node_fs16.readFileSync)(path2, "utf8");
16481
17303
  } catch {
16482
17304
  return null;
16483
17305
  }
@@ -16501,7 +17323,7 @@ function strayBrowserArtifactPaths() {
16501
17323
  const cwd = process.cwd();
16502
17324
  return STRAY_BROWSER_ARTIFACT_DIRS.filter((rel) => {
16503
17325
  try {
16504
- return (0, import_node_fs15.existsSync)((0, import_node_path14.join)(cwd, rel));
17326
+ return (0, import_node_fs16.existsSync)((0, import_node_path14.join)(cwd, rel));
16505
17327
  } catch {
16506
17328
  return false;
16507
17329
  }
@@ -16509,14 +17331,14 @@ function strayBrowserArtifactPaths() {
16509
17331
  }
16510
17332
  function readGitignore() {
16511
17333
  try {
16512
- return (0, import_node_fs15.readFileSync)(gitignorePath(), "utf8");
17334
+ return (0, import_node_fs16.readFileSync)(gitignorePath(), "utf8");
16513
17335
  } catch {
16514
17336
  return null;
16515
17337
  }
16516
17338
  }
16517
17339
  function writeGitignore(content) {
16518
17340
  try {
16519
- (0, import_node_fs15.writeFileSync)(gitignorePath(), content, "utf8");
17341
+ (0, import_node_fs16.writeFileSync)(gitignorePath(), content, "utf8");
16520
17342
  return true;
16521
17343
  } catch {
16522
17344
  return false;
@@ -16555,7 +17377,7 @@ async function runDoctor(opts, io = consoleIo) {
16555
17377
  let onPath = pathProbe;
16556
17378
  if (!onPath) {
16557
17379
  const root = process.env.CLAUDE_PLUGIN_ROOT;
16558
- if (root && (0, import_node_fs15.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
17380
+ if (root && (0, import_node_fs16.existsSync)(`${root}/bin/mmi-cli${isWin ? ".cmd" : ""}`)) onPath = true;
16559
17381
  }
16560
17382
  checks.push({ ok: onPath, label: "mmi-cli on PATH", fix: "auto-provisioned at session start \u2014 reopen the session, or install the MMI plugin" });
16561
17383
  const surface = detectSurface(process.env);
@@ -16649,6 +17471,19 @@ async function runDoctor(opts, io = consoleIo) {
16649
17471
  io.err(` \u21BB updated MMI plugin \u2192 ${releasedVersion ?? "latest"} via claude plugin \u2014 ${reloadAction(surface)} to load the new commands`);
16650
17472
  }
16651
17473
  }
17474
+ const codexStale = installedVersionCheck.staleSurfaces?.some((s) => s.surface === "codex") ?? false;
17475
+ if (!installedVersionCheck.ok && codexStale && await applyCodexPluginHeal(surface, (m) => io.err(m))) {
17476
+ const healed = buildInstalledPluginVersionCheck({
17477
+ isOrgRepo: Boolean(cfg.sagaApiUrl),
17478
+ sources: installedPluginSources(),
17479
+ releasedVersion,
17480
+ surface
17481
+ });
17482
+ installedVersionCheck = healed;
17483
+ if (healed.ok) {
17484
+ io.err(` \u21BB updated MMI plugin \u2192 ${releasedVersion ?? "latest"} via codex plugin \u2014 ${reloadAction(surface)} to load the new commands`);
17485
+ }
17486
+ }
16652
17487
  }
16653
17488
  checks.push(installedVersionCheck);
16654
17489
  let cacheCleanupCheck = buildMmiPluginCacheCleanupCheck({
@@ -16709,7 +17544,7 @@ async function runDoctor(opts, io = consoleIo) {
16709
17544
  isOrgRepo: Boolean(cfg.sagaApiUrl),
16710
17545
  surface,
16711
17546
  cacheRoot: cursorCacheRoot,
16712
- cacheRootExists: (0, import_node_fs15.existsSync)(cursorCacheRoot),
17547
+ cacheRootExists: (0, import_node_fs16.existsSync)(cursorCacheRoot),
16713
17548
  pins: cursorPins,
16714
17549
  hubCheckout: hubCheckoutForCursorSeed(),
16715
17550
  releasedVersion
@@ -16729,7 +17564,7 @@ async function runDoctor(opts, io = consoleIo) {
16729
17564
  isOrgRepo: Boolean(cfg.sagaApiUrl),
16730
17565
  surface,
16731
17566
  cacheRoot: cursorCacheRoot,
16732
- cacheRootExists: (0, import_node_fs15.existsSync)(cursorCacheRoot),
17567
+ cacheRootExists: (0, import_node_fs16.existsSync)(cursorCacheRoot),
16733
17568
  pins: cursorPins,
16734
17569
  hubCheckout: hubCheckoutForCursorSeed(),
16735
17570
  releasedVersion