@mutmutco/cli 3.33.0 → 3.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.
Files changed (2) hide show
  1. package/dist/main.cjs +554 -288
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -3408,8 +3408,8 @@ function useColor() {
3408
3408
  var program = new Command();
3409
3409
 
3410
3410
  // src/index.ts
3411
- var import_promises6 = require("node:fs/promises");
3412
- var import_node_fs27 = require("node:fs");
3411
+ var import_promises7 = require("node:fs/promises");
3412
+ var import_node_fs28 = require("node:fs");
3413
3413
  var import_node_child_process12 = require("node:child_process");
3414
3414
 
3415
3415
  // src/cli-shared.ts
@@ -4475,7 +4475,7 @@ function trackedPathStatus(c, realPath, repoAnchor) {
4475
4475
  return null;
4476
4476
  }
4477
4477
  }
4478
- function rootScratchDirSnapshot(root, readdir, stat2) {
4478
+ function rootScratchDirSnapshot(root, readdir2, stat2) {
4479
4479
  try {
4480
4480
  const rootStat = stat2(root);
4481
4481
  let mtimeMs = rootStat.mtimeMs;
@@ -4483,7 +4483,7 @@ function rootScratchDirSnapshot(root, readdir, stat2) {
4483
4483
  const stack = [root];
4484
4484
  while (stack.length) {
4485
4485
  const current = stack.pop();
4486
- for (const ent of readdir(current, { withFileTypes: true })) {
4486
+ for (const ent of readdir2(current, { withFileTypes: true })) {
4487
4487
  const child2 = (0, import_node_path4.join)(current, ent.name);
4488
4488
  if (ent.isSymbolicLink?.()) return null;
4489
4489
  try {
@@ -4503,13 +4503,13 @@ function rootScratchDirSnapshot(root, readdir, stat2) {
4503
4503
  }
4504
4504
  }
4505
4505
  function collectScratchSnapshot(repoRoot2, deps = {}) {
4506
- const readdir = deps.readdir ?? import_node_fs6.readdirSync;
4506
+ const readdir2 = deps.readdir ?? import_node_fs6.readdirSync;
4507
4507
  const stat2 = deps.stat ?? import_node_fs6.statSync;
4508
- const readFile5 = deps.readFile ?? import_node_fs6.readFileSync;
4508
+ const readFile6 = deps.readFile ?? import_node_fs6.readFileSync;
4509
4509
  const plansRoot = (0, import_node_path4.join)(repoRoot2, "plans");
4510
4510
  const rootScratchFiles = [];
4511
4511
  try {
4512
- for (const ent of readdir(repoRoot2, { withFileTypes: true })) {
4512
+ for (const ent of readdir2(repoRoot2, { withFileTypes: true })) {
4513
4513
  const isDir = ent.isDirectory();
4514
4514
  const isFile = ent.isFile();
4515
4515
  if (!(isDir && ROOT_SCRATCH_DIRS.has(ent.name)) && !(isFile && ROOT_SCRATCH_FILE_PREFIXES.some((prefix) => ent.name.startsWith(prefix)))) {
@@ -4518,7 +4518,7 @@ function collectScratchSnapshot(repoRoot2, deps = {}) {
4518
4518
  const full = (0, import_node_path4.join)(repoRoot2, ent.name);
4519
4519
  try {
4520
4520
  if (isDir) {
4521
- const snap = rootScratchDirSnapshot(full, readdir, stat2);
4521
+ const snap = rootScratchDirSnapshot(full, readdir2, stat2);
4522
4522
  if (snap) rootScratchFiles.push({ path: full, dir: repoRoot2, name: ent.name, mtimeMs: snap.mtimeMs, bytes: snap.bytes, kind: "dir" });
4523
4523
  } else {
4524
4524
  const st = stat2(full);
@@ -4531,12 +4531,12 @@ function collectScratchSnapshot(repoRoot2, deps = {}) {
4531
4531
  }
4532
4532
  const planMdFiles = [];
4533
4533
  try {
4534
- for (const ent of readdir(plansRoot, { withFileTypes: true })) {
4534
+ for (const ent of readdir2(plansRoot, { withFileTypes: true })) {
4535
4535
  if (!ent.isFile() || !ent.name.endsWith(".md")) continue;
4536
4536
  const full = (0, import_node_path4.join)(plansRoot, ent.name);
4537
4537
  try {
4538
4538
  const st = stat2(full);
4539
- const raw = readFile5(full, "utf8");
4539
+ const raw = readFile6(full, "utf8");
4540
4540
  planMdFiles.push({ path: full, dir: plansRoot, name: ent.name, mtimeMs: st.mtimeMs, bytes: st.size, kind: "file", hash: hashContent(normalizeEol(raw)) });
4541
4541
  } catch {
4542
4542
  }
@@ -4545,7 +4545,7 @@ function collectScratchSnapshot(repoRoot2, deps = {}) {
4545
4545
  }
4546
4546
  let planMeta = {};
4547
4547
  try {
4548
- planMeta = parseMeta(readFile5((0, import_node_path4.join)(plansRoot, ".plan-meta.json"), "utf8"));
4548
+ planMeta = parseMeta(readFile6((0, import_node_path4.join)(plansRoot, ".plan-meta.json"), "utf8"));
4549
4549
  } catch {
4550
4550
  planMeta = {};
4551
4551
  }
@@ -4553,7 +4553,7 @@ function collectScratchSnapshot(repoRoot2, deps = {}) {
4553
4553
  const syncQueueSlugs = (() => {
4554
4554
  let queueRaw;
4555
4555
  try {
4556
- queueRaw = readFile5((0, import_node_path4.join)(plansRoot, ".sync-queue.json"), "utf8");
4556
+ queueRaw = readFile6((0, import_node_path4.join)(plansRoot, ".sync-queue.json"), "utf8");
4557
4557
  } catch (e) {
4558
4558
  const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
4559
4559
  return code === "ENOENT" || code === "ENOTDIR" ? /* @__PURE__ */ new Set() : null;
@@ -5062,7 +5062,7 @@ async function fetchOrgConfig(deps) {
5062
5062
  }
5063
5063
  }
5064
5064
  async function postJson(pathSuffix, payload, deps, method = "POST", opts = {}) {
5065
- if (!deps.baseUrl) return { ok: false, status: 0, body: null, error: "no Hub API URL (set MMI_HUB_URL or use a current MMI CLI/plugin build)" };
5065
+ if (!deps.baseUrl) return { ok: false, status: 0, body: null, error: "no Hub API URL (set MMI_HUB_URL or use a current MMI CLI/plugin build)", unreachable: "no-base-url" };
5066
5066
  const token = await deps.token();
5067
5067
  if (!token) return { ok: false, status: 0, body: null, error: "no Hub session token (run `gh auth login`)" };
5068
5068
  const timeoutMs = opts.timeoutMs ?? deps.timeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS;
@@ -5081,7 +5081,7 @@ async function postJson(pathSuffix, payload, deps, method = "POST", opts = {}) {
5081
5081
  }
5082
5082
  return { ok: res.ok, status: res.status, body };
5083
5083
  } catch (e) {
5084
- return { ok: false, status: 0, body: null, error: e.message };
5084
+ return { ok: false, status: 0, body: null, error: e.message, unreachable: "transport" };
5085
5085
  }
5086
5086
  }
5087
5087
  async function registerProject(payload, deps) {
@@ -5099,6 +5099,11 @@ async function setDeployCoords(slug, payload, deps) {
5099
5099
  async function recordDocsAudit(verdict, deps) {
5100
5100
  return postJson("/docs-audit/record", { ...verdict }, deps);
5101
5101
  }
5102
+ async function postSchedulesLift(repo, schedules, deps) {
5103
+ const res = await postJson("/schedules/lift", { repo, schedules }, deps);
5104
+ if (!res.ok && res.status === 404) return { ...res, unreachable: "route-absent" };
5105
+ return res;
5106
+ }
5102
5107
  async function tenantControl(payload, deps) {
5103
5108
  return postJson("/tenant-control", payload, deps, "POST", { noRetry: true });
5104
5109
  }
@@ -8919,6 +8924,14 @@ function defaultWorktreePath(repoRoot2, branch) {
8919
8924
  const safe = branch.replace(/[/\\]+/g, "-");
8920
8925
  return (0, import_node_path11.join)((0, import_node_path11.dirname)(repoRoot2), "mmi-worktrees", safe);
8921
8926
  }
8927
+ async function primaryCheckoutRootOf(git) {
8928
+ try {
8929
+ const out = (await git(["rev-parse", "--path-format=absolute", "--git-common-dir"])).trim();
8930
+ return out ? (0, import_node_path11.dirname)(out) : void 0;
8931
+ } catch {
8932
+ return void 0;
8933
+ }
8934
+ }
8922
8935
  function resolveWorktreeBase(from, remote) {
8923
8936
  const remotePrefix = `${remote}/`;
8924
8937
  const fetchBranch = from.startsWith(remotePrefix) ? from.slice(remotePrefix.length) : void 0;
@@ -9017,7 +9030,7 @@ function commandLadderHint() {
9017
9030
  }
9018
9031
 
9019
9032
  // src/index.ts
9020
- var import_node_path23 = require("node:path");
9033
+ var import_node_path25 = require("node:path");
9021
9034
 
9022
9035
  // src/merge-ci-policy.ts
9023
9036
  function resolveMergeCiPolicy(input) {
@@ -9307,6 +9320,27 @@ function workflowTriggersPullRequest(yaml) {
9307
9320
  if (!onBlock) return false;
9308
9321
  return /\bpull_request\b/.test(onBlock);
9309
9322
  }
9323
+ function workflowTriggersSchedule(yaml) {
9324
+ const onBlock = extractOnBlock(yaml);
9325
+ if (!onBlock) return false;
9326
+ if (/^(["']?)on\1:/.test(onBlock)) {
9327
+ const value = onBlock.replace(/^(["']?)on\1:\s*/, "");
9328
+ if (/^\s*\[/.test(value)) {
9329
+ return /(^|[,\s\[])["']?schedule["']?([,\s\]]|$)/.test(value);
9330
+ }
9331
+ return /\bschedule["']?\s*:/.test(value);
9332
+ }
9333
+ const content = onBlock.split(/\r?\n/).filter((l) => {
9334
+ const t = l.trim();
9335
+ return t.length > 0 && !t.startsWith("#");
9336
+ });
9337
+ if (!content.length) return false;
9338
+ const minIndent = Math.min(...content.map((l) => l.match(/^(\s*)/)?.[1].length ?? 0));
9339
+ return content.some((line) => {
9340
+ const indent = line.match(/^(\s*)/)?.[1].length ?? 0;
9341
+ return indent === minIndent && /^\s*schedule\s*:/.test(line);
9342
+ });
9343
+ }
9310
9344
  function workflowReportsPrChecks(yaml) {
9311
9345
  const onBlock = extractOnBlock(yaml);
9312
9346
  if (!onBlock) return false;
@@ -9322,6 +9356,9 @@ function extractOnBlock(yaml) {
9322
9356
  if (/^(["']?)on\1:\s*$/.test(line)) {
9323
9357
  inOn = true;
9324
9358
  onIndent = 0;
9359
+ } else if (/^(["']?)on\1:\s+#/.test(line)) {
9360
+ inOn = true;
9361
+ onIndent = 0;
9325
9362
  } else if (/^(["']?)on\1:\s+\S/.test(line)) {
9326
9363
  return line;
9327
9364
  }
@@ -9468,6 +9505,63 @@ function planManagedGitignore(current) {
9468
9505
  return { changed, content, added, removed };
9469
9506
  }
9470
9507
 
9508
+ // src/gate-budget.ts
9509
+ var BLESSED_RUN_WITH_BUDGET_SHA = "b6ec6e4eafcda73b543fc0754a6356551fe762a1";
9510
+ var REMOTE_USE = /^mutmutco\/MMI-Hub\/\.github\/actions\/run-with-budget@(\S+)$/;
9511
+ var LOCAL_USE = "./.github/actions/run-with-budget";
9512
+ var FULL_SHA = /^[0-9a-f]{40}$/;
9513
+ var MAX_SECONDS_CEILING = 1800;
9514
+ function isGateWorkflowPath(path2) {
9515
+ const base = path2.split(/[/\\]/).pop() ?? path2;
9516
+ return /^(?:.+[-_.])?gate\.ya?ml$/i.test(base);
9517
+ }
9518
+ function checkGateBudget(files, opts = {}) {
9519
+ const failures = [];
9520
+ const notes = [];
9521
+ for (const { path: path2, body } of files) {
9522
+ const uses = [...body.matchAll(/^\s*-?\s*uses:\s*(\S+)/gm)].map((m) => m[1].replace(/^['"]|['"]$/g, "")).filter((ref) => ref.includes("run-with-budget"));
9523
+ if (uses.length === 0) {
9524
+ failures.push(`${path2}: check command does not run via run-with-budget \u2014 the gate has no wall-clock ceiling`);
9525
+ continue;
9526
+ }
9527
+ for (const ref of uses) {
9528
+ if (ref === LOCAL_USE) {
9529
+ if (!opts.hubLocalAllowed) {
9530
+ failures.push(`${path2}: local ${LOCAL_USE} reference \u2014 consume the org-shared action pinned by SHA (mutmutco/MMI-Hub/.github/actions/run-with-budget@<full sha>)`);
9531
+ }
9532
+ continue;
9533
+ }
9534
+ const remote = REMOTE_USE.exec(ref);
9535
+ if (!remote) {
9536
+ failures.push(`${path2}: unrecognized run-with-budget reference '${ref}' \u2014 use mutmutco/MMI-Hub/.github/actions/run-with-budget@<full sha>`);
9537
+ continue;
9538
+ }
9539
+ if (!FULL_SHA.test(remote[1])) {
9540
+ failures.push(`${path2}: run-with-budget pinned to '${remote[1]}' \u2014 pin the full 40-hex commit SHA, not a branch or tag`);
9541
+ continue;
9542
+ }
9543
+ if (remote[1] !== BLESSED_RUN_WITH_BUDGET_SHA) {
9544
+ notes.push(`${path2}: run-with-budget pinned to ${remote[1].slice(0, 12)} (blessed ${BLESSED_RUN_WITH_BUDGET_SHA.slice(0, 12)}) \u2014 valid but outdated`);
9545
+ }
9546
+ }
9547
+ const ceilingLines = [...body.matchAll(/^\s*max-seconds:\s*(.+?)\s*$/gm)].map((m) => m[1].replace(/\s+#.*$/, ""));
9548
+ if (ceilingLines.length === 0) {
9549
+ failures.push(`${path2}: run-with-budget has no max-seconds input \u2014 the budget step guards nothing`);
9550
+ continue;
9551
+ }
9552
+ for (const raw of ceilingLines) {
9553
+ const literal = /^['"]?(\d+)['"]?$/.exec(raw);
9554
+ const seconds = literal ? Number(literal[1]) : NaN;
9555
+ if (!literal || seconds < 1) {
9556
+ failures.push(`${path2}: max-seconds '${raw}' is not a literal positive integer`);
9557
+ } else if (seconds > MAX_SECONDS_CEILING) {
9558
+ failures.push(`${path2}: max-seconds ${seconds} exceeds the org ceiling ${MAX_SECONDS_CEILING} \u2014 a budget that generous guards nothing; measure the gate and set a real ceiling`);
9559
+ }
9560
+ }
9561
+ }
9562
+ return { ok: failures.length === 0, failures, notes };
9563
+ }
9564
+
9471
9565
  // src/bootstrap-apply.ts
9472
9566
  function parseOwnerRepo(repo) {
9473
9567
  const trimmed = repo.trim();
@@ -9481,6 +9575,7 @@ function parseOwnerRepo(repo) {
9481
9575
  return { owner, name, slug: name.toLowerCase(), fullName: `${owner}/${name}` };
9482
9576
  }
9483
9577
  var DEFAULT_GATE_CMD = "npm run check";
9578
+ var DEFAULT_GATE_MAX_SECONDS = "300";
9484
9579
  var DEFAULT_GATE_PY_VERSION = "3.11";
9485
9580
  var GATE_RUNTIME_DEFAULTS = {
9486
9581
  node: { cmd: DEFAULT_GATE_CMD, install: "npm ci" },
@@ -9494,7 +9589,10 @@ function gateSeedVars(cls, releaseTrack, runtime = "node") {
9494
9589
  GATE_INSTALL_CMD: rt.install,
9495
9590
  GATE_WORKDIR: ".",
9496
9591
  GATE_CACHE_DEP_PATH: "package-lock.json",
9497
- GATE_PY_VERSION: DEFAULT_GATE_PY_VERSION
9592
+ GATE_PY_VERSION: DEFAULT_GATE_PY_VERSION,
9593
+ GATE_MAX_SECONDS: DEFAULT_GATE_MAX_SECONDS,
9594
+ // The pin is the CLI's blessed SHA, not an operator knob — one central place to bump (#3178).
9595
+ GATE_BUDGET_SHA: BLESSED_RUN_WITH_BUDGET_SHA
9498
9596
  };
9499
9597
  const track = releaseTrack ?? (cls === "content" ? "trunk" : "full");
9500
9598
  if (track === "trunk") {
@@ -9540,6 +9638,8 @@ function gateConfigToVars(gate) {
9540
9638
  if (typeof gate.workdir === "string" && gate.workdir.trim()) out.GATE_WORKDIR = gate.workdir;
9541
9639
  if (typeof gate.cacheDepPath === "string" && gate.cacheDepPath.trim()) out.GATE_CACHE_DEP_PATH = gate.cacheDepPath;
9542
9640
  if (typeof gate.pyVersion === "string" && gate.pyVersion.trim()) out.GATE_PY_VERSION = gate.pyVersion;
9641
+ const seconds = typeof gate.maxSeconds === "number" ? String(gate.maxSeconds) : gate.maxSeconds;
9642
+ if (typeof seconds === "string" && /^\d+$/.test(seconds.trim()) && Number(seconds) > 0) out.GATE_MAX_SECONDS = seconds.trim();
9543
9643
  return out;
9544
9644
  }
9545
9645
  function seedMatchesDeployModel(seed, deployModel) {
@@ -9601,10 +9701,10 @@ function labelsToPrune(orgLabelNames) {
9601
9701
  const org = new Set(orgLabelNames);
9602
9702
  return GITHUB_DEFAULT_LABELS.filter((name) => !org.has(name));
9603
9703
  }
9604
- function resolveSeedContent(seed, vars, readFile5) {
9605
- if (seed.source === "self") return readFile5(seed.target);
9704
+ function resolveSeedContent(seed, vars, readFile6) {
9705
+ if (seed.source === "self") return readFile6(seed.target);
9606
9706
  if (seed.source.startsWith("seed:")) {
9607
- const tmpl = readFile5(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
9707
+ const tmpl = readFile6(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
9608
9708
  return tmpl == null ? null : renderSeed(tmpl, vars);
9609
9709
  }
9610
9710
  return null;
@@ -10020,11 +10120,22 @@ async function auditRepoCi(repo, deps) {
10020
10120
  const allPaths = await listWorkflowPaths(deps, repo, baseBranch) ?? [];
10021
10121
  const prPaths = await filterPullRequestTriggered(deps, repo, baseBranch, allPaths);
10022
10122
  const missing = [];
10123
+ const gateBodies = [];
10023
10124
  for (const path2 of prPaths) {
10024
10125
  const body = await fetchFileContent(deps, repo, baseBranch, path2);
10025
10126
  if (body == null) continue;
10127
+ if (isGateWorkflowPath(path2)) gateBodies.push({ path: path2, body });
10026
10128
  if (!/^\s*concurrency:/m.test(body) || !/cancel-in-progress:\s*true/.test(body)) missing.push(path2);
10027
10129
  }
10130
+ if (gateBodies.length > 0) {
10131
+ const budget = checkGateBudget(gateBodies, { hubLocalAllowed: repoClass === "hub" });
10132
+ checks.push({
10133
+ ok: budget.ok,
10134
+ label: "gate runs under the org wall-clock budget (#3178)",
10135
+ detail: [...budget.failures, ...budget.notes].join("; ") || void 0,
10136
+ remediation: budget.ok ? void 0 : `Wrap the gate's check command in mutmutco/MMI-Hub/.github/actions/run-with-budget@${BLESSED_RUN_WITH_BUDGET_SHA} with a measured max-seconds \u2014 see skills/bootstrap/seeds/gate.template.yml`
10137
+ });
10138
+ }
10028
10139
  if (prPaths.length > 0) {
10029
10140
  checks.push({
10030
10141
  ok: missing.length === 0,
@@ -10378,6 +10489,14 @@ function earlyExitGraceMs() {
10378
10489
  }
10379
10490
  return EARLY_EXIT_GRACE_MS;
10380
10491
  }
10492
+ var HEALTH_POLL_INTERVAL_MS = 1e3;
10493
+ function healthPollIntervalMs() {
10494
+ if (process.env.NODE_ENV === "test") {
10495
+ const testOverride = Number(process.env.MMI_CLI_TEST_HEALTH_POLL_MS);
10496
+ if (Number.isFinite(testOverride) && testOverride >= 10) return testOverride;
10497
+ }
10498
+ return HEALTH_POLL_INTERVAL_MS;
10499
+ }
10381
10500
  function waitForProcessStability(child2, graceMs = earlyExitGraceMs()) {
10382
10501
  return new Promise((resolve5, reject) => {
10383
10502
  let settled = false;
@@ -10793,7 +10912,7 @@ function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
10793
10912
  async function cleanupStageState(state, paths, timeoutMs, fallbackCwd) {
10794
10913
  await killTree(state.pid);
10795
10914
  if (state.teardown?.command.trim()) {
10796
- await shell(state.teardown.command.trim(), state.teardown.cwd || state.cwd || fallbackCwd, timeoutMs);
10915
+ await shell(state.teardown.command.trim(), state.teardown.cwd || state.cwd || fallbackCwd, Math.max(timeoutMs, 1e4));
10797
10916
  }
10798
10917
  for (const path2 of [...new Set(paths.filter((p) => Boolean(p)))]) {
10799
10918
  (0, import_node_fs14.rmSync)(path2, { force: true });
@@ -10834,7 +10953,7 @@ async function waitForHealth(url, timeoutMs, anyStatus = false) {
10834
10953
  } catch (e) {
10835
10954
  last = e.message;
10836
10955
  }
10837
- await new Promise((resolve5) => setTimeout(resolve5, 1e3));
10956
+ await new Promise((resolve5) => setTimeout(resolve5, healthPollIntervalMs()));
10838
10957
  }
10839
10958
  throw new Error(`stage health check timed out for ${url}${last ? ` (${last})` : ""}`);
10840
10959
  }
@@ -11275,29 +11394,6 @@ function resolveIssueTitle(input, deps) {
11275
11394
  noun: "title"
11276
11395
  });
11277
11396
  }
11278
- var NORTH_STAR_BODY_PREFIX = "North Star:";
11279
- var NORTH_STAR_SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
11280
- function normalizeNorthStarSlug(raw) {
11281
- const slug = raw.trim();
11282
- if (!slug || !NORTH_STAR_SLUG_RE.test(slug)) {
11283
- throw new Error(`invalid North Star slug "${raw}" \u2014 expected kebab-case like agent-session-workflow`);
11284
- }
11285
- return slug;
11286
- }
11287
- function northStarLabel(slug) {
11288
- return `northstar:${slug}`;
11289
- }
11290
- function appendNorthStarLine(body, slug) {
11291
- const line = `${NORTH_STAR_BODY_PREFIX} ${slug}`;
11292
- const existing = new RegExp(`^${NORTH_STAR_BODY_PREFIX}\\s+\\S+\\s*$`, "m");
11293
- if (existing.test(body)) return body;
11294
- const trimmed = body.replace(/\s+$/, "");
11295
- return trimmed ? `${trimmed}
11296
-
11297
- ${line}
11298
- ` : `${line}
11299
- `;
11300
- }
11301
11397
 
11302
11398
  // src/issue-view-json.ts
11303
11399
  var DEFAULT_ISSUE_VIEW_FIELDS = "number,title,state,url,labels,author,assignees,milestone,body";
@@ -12345,6 +12441,8 @@ function parseVerifyBroker(stdout) {
12345
12441
  }
12346
12442
 
12347
12443
  // src/train-apply.ts
12444
+ var import_node_fs16 = require("node:fs");
12445
+ var import_node_path14 = require("node:path");
12348
12446
  function resolveDeployModel2(meta, repo) {
12349
12447
  const m = meta?.deployModel;
12350
12448
  if (isDeployModel(m)) return m;
@@ -13188,6 +13286,34 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
13188
13286
  }
13189
13287
  return { note: `no manual dispatch: ${model} repo deploys via its own push-triggered workflow`, deployStatus: "pending" };
13190
13288
  }
13289
+ function readLocalGateWorkflows() {
13290
+ const dir = (0, import_node_path14.join)(".github", "workflows");
13291
+ let names;
13292
+ try {
13293
+ names = (0, import_node_fs16.readdirSync)(dir);
13294
+ } catch {
13295
+ return null;
13296
+ }
13297
+ const files = [];
13298
+ for (const name of names.filter(isGateWorkflowPath)) {
13299
+ try {
13300
+ files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs16.readFileSync)((0, import_node_path14.join)(dir, name), "utf8") });
13301
+ } catch {
13302
+ }
13303
+ }
13304
+ return files;
13305
+ }
13306
+ function enforceGateBudget(deps, repo) {
13307
+ const files = (deps.readGateWorkflows ?? readLocalGateWorkflows)();
13308
+ if (files == null || files.length === 0) return;
13309
+ const budget = checkGateBudget(files, { hubLocalAllowed: repo.toLowerCase() === "mutmutco/mmi-hub" });
13310
+ for (const note of budget.notes) deps.warn?.(`gate budget: ${note}`);
13311
+ if (!budget.ok) {
13312
+ throw new Error(
13313
+ `release refused \u2014 the gate runs without the org wall-clock budget (#3178): ${budget.failures.join("; ")}. Wrap the check command in the pinned run-with-budget step (see skills/bootstrap/seeds/gate.template.yml in MMI-Hub), or run \`mmi-cli ci audit --repo ${repo}\` for the full picture.`
13314
+ );
13315
+ }
13316
+ }
13191
13317
  async function preflight(deps, ctx, stage, meta) {
13192
13318
  const model = resolveDeployModel2(meta, ctx.repo);
13193
13319
  if (model === "content") {
@@ -13197,6 +13323,7 @@ async function preflight(deps, ctx, stage, meta) {
13197
13323
  throw new Error(`${ctx.repo} is not Hub-deployed (deployModel=none) \u2014 the release train does not apply; use the project's own release path`);
13198
13324
  }
13199
13325
  await deps.runSelf(["secrets", "preflight", "--stage", stage, "--repo", ctx.repo]);
13326
+ enforceGateBudget(deps, ctx.repo);
13200
13327
  return model;
13201
13328
  }
13202
13329
  async function preflightMergeToMain(deps, deployModel, remoteRef, blockingPrefix, realignMessage) {
@@ -15037,8 +15164,8 @@ function renderAccessReport(report) {
15037
15164
  }
15038
15165
 
15039
15166
  // src/docs-index-command.ts
15040
- var import_node_fs16 = require("node:fs");
15041
- var import_node_path14 = require("node:path");
15167
+ var import_node_fs17 = require("node:fs");
15168
+ var import_node_path15 = require("node:path");
15042
15169
  var DOCS_INDEX_PATH = "docs/index.md";
15043
15170
  function isRoutableDocsPath(relPath) {
15044
15171
  const normalized = relPath.replace(/\\/g, "/");
@@ -15116,25 +15243,25 @@ function walkMarkdown(dir) {
15116
15243
  const stack = [dir];
15117
15244
  while (stack.length) {
15118
15245
  const current = stack.pop();
15119
- for (const entry of (0, import_node_fs16.readdirSync)(current, { withFileTypes: true })) {
15120
- const full = (0, import_node_path14.join)(current, entry.name);
15246
+ for (const entry of (0, import_node_fs17.readdirSync)(current, { withFileTypes: true })) {
15247
+ const full = (0, import_node_path15.join)(current, entry.name);
15121
15248
  if (entry.isDirectory()) {
15122
15249
  stack.push(full);
15123
15250
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
15124
- out.push((0, import_node_path14.relative)(dir, full).split(import_node_path14.sep).join("/"));
15251
+ out.push((0, import_node_path15.relative)(dir, full).split(import_node_path15.sep).join("/"));
15125
15252
  }
15126
15253
  }
15127
15254
  }
15128
15255
  return out;
15129
15256
  }
15130
15257
  function createDocsIndexDeps(repoRoot2) {
15131
- const docsDir = (0, import_node_path14.join)(repoRoot2, "docs");
15132
- const indexPath = (0, import_node_path14.join)(repoRoot2, DOCS_INDEX_PATH);
15258
+ const docsDir = (0, import_node_path15.join)(repoRoot2, "docs");
15259
+ const indexPath = (0, import_node_path15.join)(repoRoot2, DOCS_INDEX_PATH);
15133
15260
  return {
15134
- listDocs: () => (0, import_node_fs16.existsSync)(docsDir) ? walkMarkdown(docsDir).filter(isRoutableDocsPath).sort() : [],
15135
- readDoc: (relPath) => (0, import_node_fs16.readFileSync)((0, import_node_path14.join)(docsDir, relPath), "utf8"),
15136
- readIndex: () => (0, import_node_fs16.existsSync)(indexPath) ? (0, import_node_fs16.readFileSync)(indexPath, "utf8") : null,
15137
- writeIndex: (content) => (0, import_node_fs16.writeFileSync)(indexPath, content, "utf8")
15261
+ listDocs: () => (0, import_node_fs17.existsSync)(docsDir) ? walkMarkdown(docsDir).filter(isRoutableDocsPath).sort() : [],
15262
+ readDoc: (relPath) => (0, import_node_fs17.readFileSync)((0, import_node_path15.join)(docsDir, relPath), "utf8"),
15263
+ readIndex: () => (0, import_node_fs17.existsSync)(indexPath) ? (0, import_node_fs17.readFileSync)(indexPath, "utf8") : null,
15264
+ writeIndex: (content) => (0, import_node_fs17.writeFileSync)(indexPath, content, "utf8")
15138
15265
  };
15139
15266
  }
15140
15267
 
@@ -16061,8 +16188,8 @@ function writeError(res) {
16061
16188
  }
16062
16189
 
16063
16190
  // src/secrets-commands.ts
16064
- var import_node_fs17 = require("node:fs");
16065
- var import_node_path15 = require("node:path");
16191
+ var import_node_fs18 = require("node:fs");
16192
+ var import_node_path16 = require("node:path");
16066
16193
  var import_node_os5 = require("node:os");
16067
16194
 
16068
16195
  // src/project-runtime.ts
@@ -16186,18 +16313,18 @@ function collectMap(value, previous = []) {
16186
16313
  return [...previous, value];
16187
16314
  }
16188
16315
  async function decryptRailsCredentials(input) {
16189
- const appDir = (0, import_node_path15.resolve)(input.appDir ?? process.cwd());
16316
+ const appDir = (0, import_node_path16.resolve)(input.appDir ?? process.cwd());
16190
16317
  const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
16191
16318
  const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
16192
- const credentialsPath = (0, import_node_path15.resolve)(appDir, credentialsFile);
16193
- const masterKeyPath = (0, import_node_path15.resolve)(appDir, masterKeyFile);
16319
+ const credentialsPath = (0, import_node_path16.resolve)(appDir, credentialsFile);
16320
+ const masterKeyPath = (0, import_node_path16.resolve)(appDir, masterKeyFile);
16194
16321
  const env = {
16195
16322
  ...process.env,
16196
16323
  MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
16197
16324
  MMI_RAILS_MASTER_KEY_FILE: masterKeyPath
16198
16325
  };
16199
- if ((0, import_node_fs17.existsSync)(masterKeyPath)) {
16200
- env.RAILS_MASTER_KEY = (0, import_node_fs17.readFileSync)(masterKeyPath, "utf8").trim();
16326
+ if ((0, import_node_fs18.existsSync)(masterKeyPath)) {
16327
+ env.RAILS_MASTER_KEY = (0, import_node_fs18.readFileSync)(masterKeyPath, "utf8").trim();
16201
16328
  }
16202
16329
  const script = [
16203
16330
  'require "json"',
@@ -16207,9 +16334,9 @@ async function decryptRailsCredentials(input) {
16207
16334
  'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
16208
16335
  "puts JSON.generate(config.config)"
16209
16336
  ].join("\n");
16210
- const scriptDir = (0, import_node_fs17.mkdtempSync)((0, import_node_path15.join)((0, import_node_os5.tmpdir)(), "mmi-rails-decrypt-"));
16211
- const scriptPath = (0, import_node_path15.join)(scriptDir, "decrypt.rb");
16212
- (0, import_node_fs17.writeFileSync)(scriptPath, script, "utf8");
16337
+ const scriptDir = (0, import_node_fs18.mkdtempSync)((0, import_node_path16.join)((0, import_node_os5.tmpdir)(), "mmi-rails-decrypt-"));
16338
+ const scriptPath = (0, import_node_path16.join)(scriptDir, "decrypt.rb");
16339
+ (0, import_node_fs18.writeFileSync)(scriptPath, script, "utf8");
16213
16340
  try {
16214
16341
  const args = ["exec", "ruby", scriptPath];
16215
16342
  const cmd = process.platform === "win32" ? "cmd.exe" : "bundle";
@@ -16221,7 +16348,7 @@ async function decryptRailsCredentials(input) {
16221
16348
  });
16222
16349
  return JSON.parse(stdout);
16223
16350
  } finally {
16224
- (0, import_node_fs17.rmSync)(scriptDir, { recursive: true, force: true });
16351
+ (0, import_node_fs18.rmSync)(scriptDir, { recursive: true, force: true });
16225
16352
  }
16226
16353
  }
16227
16354
  async function readSecretStdin() {
@@ -16296,7 +16423,7 @@ function registerSecretsCommands(program3) {
16296
16423
  secrets.command("org-catalog").description("MASTER-ONLY: set the _org/<provider>/* catalog declarations from a JSON file (#2244)").requiredOption("--file <path>", 'JSON file: { "entries": { "<provider>/<KEY>": {key,purpose,group,owner,provider,stages[],consumers[]} } }').action((o) => withSecrets(async (d) => {
16297
16424
  let body;
16298
16425
  try {
16299
- body = (0, import_node_fs17.readFileSync)((0, import_node_path15.resolve)(o.file), "utf8");
16426
+ body = (0, import_node_fs18.readFileSync)((0, import_node_path16.resolve)(o.file), "utf8");
16300
16427
  } catch (e) {
16301
16428
  return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
16302
16429
  }
@@ -16399,7 +16526,7 @@ function registerSecretsCommands(program3) {
16399
16526
  {
16400
16527
  ...d,
16401
16528
  decryptRailsCredentials,
16402
- removeFile: (path2) => (0, import_node_fs17.unlinkSync)((0, import_node_path15.resolve)(o.appDir ?? process.cwd(), path2))
16529
+ removeFile: (path2) => (0, import_node_fs18.unlinkSync)((0, import_node_path16.resolve)(o.appDir ?? process.cwd(), path2))
16403
16530
  },
16404
16531
  {
16405
16532
  repo: o.repo,
@@ -16812,9 +16939,9 @@ function extractWorkflowCrons(yamlText) {
16812
16939
  function workflowEntry(repo, workflowPath, yamlText) {
16813
16940
  const crons = extractWorkflowCrons(yamlText);
16814
16941
  if (!crons.length) return null;
16815
- const basename4 = workflowPath.split("/").pop() ?? workflowPath;
16942
+ const basename3 = workflowPath.split("/").pop() ?? workflowPath;
16816
16943
  return {
16817
- name: `${repo}/${basename4.replace(/\.ya?ml$/, "")}`,
16944
+ name: `${repo}/${basename3.replace(/\.ya?ml$/, "")}`,
16818
16945
  cadence: crons.join(" + "),
16819
16946
  executor: "github-actions",
16820
16947
  llm: llmFromHeader(yamlText),
@@ -16918,6 +17045,10 @@ function spliceDoc(docText, generatedSection) {
16918
17045
  }
16919
17046
  return docText.slice(0, start) + generatedSection + docText.slice(end + DOC_END_MARKER.length);
16920
17047
  }
17048
+ var HARBOUR_LLM_LAUNCHERS = /* @__PURE__ */ new Set(["cursor-agent"]);
17049
+ function isHarbourLlmLauncher(executor) {
17050
+ return HARBOUR_LLM_LAUNCHERS.has(executor);
17051
+ }
16921
17052
  function splitCadence(cadence) {
16922
17053
  return cadence.split("+").map((c) => c.replace(/\s+/g, " ").trim()).filter(Boolean);
16923
17054
  }
@@ -16979,6 +17110,38 @@ function reconcileGithubActions(liveGithub, registry2, readRepos) {
16979
17110
  function renderDrift(d) {
16980
17111
  return `${d.class}: ${d.name} (${d.executor}) \u2014 ${d.detail}; remedy: ${d.remedy}`;
16981
17112
  }
17113
+ function strayCronDrift(name) {
17114
+ return {
17115
+ class: "stray-cron",
17116
+ name,
17117
+ executor: "github-actions",
17118
+ detail: "workflow still carries on: schedule",
17119
+ remedy: "strip on: schedule \u2192 workflow_dispatch and let the harbour dispatcher fire on the registry cadence (docs/schedules.md rule 1)"
17120
+ };
17121
+ }
17122
+ function strayCronDrifts(workflows) {
17123
+ return workflows.filter((w) => workflowTriggersSchedule(w.yamlText)).map((w) => strayCronDrift(w.name)).sort((a, b) => a.name.localeCompare(b.name));
17124
+ }
17125
+ function unlauncheredLlmDrift(entry) {
17126
+ if (entry.llm !== "yes") return null;
17127
+ if (isHarbourLlmLauncher(entry.executor)) return null;
17128
+ const allowed = [...HARBOUR_LLM_LAUNCHERS].join(", ");
17129
+ return {
17130
+ class: "unlaunchered-llm",
17131
+ name: entry.name,
17132
+ executor: entry.executor,
17133
+ detail: `llm: yes with executor outside the harbour launcher set (${allowed})`,
17134
+ remedy: `move the lane to executor: ${allowed} and run it through the Hub harbour key (docs/schedules.md rule 4)`
17135
+ };
17136
+ }
17137
+ function unlauncheredLlmDrifts(entries) {
17138
+ const drifts = [];
17139
+ for (const e of entries) {
17140
+ const d = unlauncheredLlmDrift(e);
17141
+ if (d) drifts.push(d);
17142
+ }
17143
+ return drifts.sort((a, b) => a.name.localeCompare(b.name));
17144
+ }
16982
17145
  function assembleReconciliation(githubEntries2, readRepos, registry2) {
16983
17146
  if (registry2 === null) {
16984
17147
  return {
@@ -17013,10 +17176,11 @@ async function listWorkflows(client, repo) {
17013
17176
  }
17014
17177
  }
17015
17178
  async function repoWorkflowEntries(client, repo) {
17016
- const workflows = await listWorkflows(client, repo);
17179
+ const workflowsList = await listWorkflows(client, repo);
17017
17180
  const entries = [];
17018
17181
  const failures = [];
17019
- for (const wf of workflows) {
17182
+ const workflows = [];
17183
+ for (const wf of workflowsList) {
17020
17184
  if (wf?.state !== "active" || typeof wf.path !== "string" || !wf.path) continue;
17021
17185
  if (!wf.path.startsWith(".github/workflows/")) continue;
17022
17186
  try {
@@ -17026,6 +17190,9 @@ async function repoWorkflowEntries(client, repo) {
17026
17190
  );
17027
17191
  if (typeof contents?.content !== "string" || contents.encoding !== "base64") continue;
17028
17192
  const text = Buffer.from(contents.content, "base64").toString("utf8");
17193
+ const basename3 = wf.path.split("/").pop() ?? wf.path;
17194
+ const name = `${repo}/${basename3.replace(/\.ya?ml$/, "")}`;
17195
+ workflows.push({ name, yamlText: text });
17029
17196
  const entry = workflowEntry(repo, wf.path, text);
17030
17197
  if (entry) entries.push(entry);
17031
17198
  } catch (e) {
@@ -17034,19 +17201,20 @@ async function repoWorkflowEntries(client, repo) {
17034
17201
  else throw e;
17035
17202
  }
17036
17203
  }
17037
- return { entries, failures };
17204
+ return { entries, failures, workflows };
17038
17205
  }
17039
17206
  async function githubEntries(client) {
17040
17207
  const entries = [];
17041
17208
  const incomplete = [];
17042
17209
  const drift = [];
17043
17210
  const readRepos = [];
17211
+ const workflows = [];
17044
17212
  let repos;
17045
17213
  try {
17046
17214
  const listing = await client.restPaginate(`/orgs/${ORG}/repos?per_page=100`);
17047
17215
  repos = listing.filter((r) => typeof r?.name === "string" && r.archived !== true).map((r) => r.name).sort();
17048
17216
  } catch (e) {
17049
- return { entries, incomplete: [`github: could not list ${ORG} repos \u2014 ${e.message}`], drift, readRepos };
17217
+ return { entries, incomplete: [`github: could not list ${ORG} repos \u2014 ${e.message}`], drift, reconciliation: [], readRepos };
17050
17218
  }
17051
17219
  const results = await Promise.all(
17052
17220
  repos.map(async (repo) => {
@@ -17062,12 +17230,15 @@ async function githubEntries(client) {
17062
17230
  else {
17063
17231
  readRepos.push(r.repo);
17064
17232
  entries.push(...r.entries);
17233
+ workflows.push(...r.workflows);
17065
17234
  if (r.failures.length) {
17066
17235
  drift.push(`${r.repo}: ${r.failures.length} workflow record(s) listed active with no file on the default branch (deleted one-offs?) \u2014 deregister them: ${r.failures.map((f) => f.split(": ")[1]).join(", ")}`);
17067
17236
  }
17068
17237
  }
17069
17238
  }
17070
- return { entries, incomplete, drift, readRepos };
17239
+ const reconciliation = [...strayCronDrifts(workflows), ...unlauncheredLlmDrifts(entries)];
17240
+ drift.push(...reconciliation.map(renderDrift));
17241
+ return { entries, incomplete, drift, reconciliation, readRepos };
17071
17242
  }
17072
17243
  async function awsJson(args) {
17073
17244
  const run = async () => {
@@ -17102,7 +17273,8 @@ async function awsEntries() {
17102
17273
  } catch (e) {
17103
17274
  incomplete.push(`aws: scheduler listing failed \u2014 ${e.message}`);
17104
17275
  }
17105
- return { entries, incomplete, drift: [] };
17276
+ const reconciliation = unlauncheredLlmDrifts(entries);
17277
+ return { entries, incomplete, drift: reconciliation.map(renderDrift), reconciliation };
17106
17278
  }
17107
17279
  async function defaultRegistryRead() {
17108
17280
  return fetchSchedulesList(registryClientDeps(await loadConfig()));
@@ -17118,7 +17290,7 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
17118
17290
  entries: sortEntries([...gh.entries, ...aws.entries, ...DECLARED_ENTRIES]),
17119
17291
  incomplete: [...gh.incomplete, ...aws.incomplete, ...recon.incomplete],
17120
17292
  drift: [...gh.drift, ...aws.drift, ...recon.driftLines],
17121
- reconciliation: recon.reconciliation
17293
+ reconciliation: [...gh.reconciliation, ...aws.reconciliation, ...recon.reconciliation]
17122
17294
  };
17123
17295
  }
17124
17296
  function warnIncomplete2(incomplete) {
@@ -17159,6 +17331,169 @@ function registerSchedulesCommands(program3) {
17159
17331
  });
17160
17332
  }
17161
17333
 
17334
+ // src/schedules-lift-command.ts
17335
+ var import_promises3 = require("node:fs/promises");
17336
+ var import_node_path17 = require("node:path");
17337
+
17338
+ // src/schedules-lift.ts
17339
+ var SCHEDULE_HEADER_FIELDS = ["schedule", "what", "owner", "cadence", "executor", "llm", "output", "breaks", "kill"];
17340
+ function parseScheduleHeader(yamlText) {
17341
+ const out = {};
17342
+ for (const rawLine of yamlText.split("\n")) {
17343
+ const m = /^#\s*(schedule|what|owner|cadence|executor|llm|output|breaks|kill):\s*(.+?)\s*$/.exec(rawLine);
17344
+ if (m && out[m[1]] === void 0) out[m[1]] = m[2].trim();
17345
+ }
17346
+ return out;
17347
+ }
17348
+ function cadenceContainsCron(cadence, cron) {
17349
+ const cadenceTokens = cadence.split(/\s+/).filter(Boolean);
17350
+ const cronTokens = cron.split(/\s+/).filter(Boolean);
17351
+ if (!cronTokens.length || cronTokens.length > cadenceTokens.length) return false;
17352
+ return cadenceTokens.some((_, i) => i + cronTokens.length <= cadenceTokens.length && cronTokens.every((t, j) => cadenceTokens[i + j] === t));
17353
+ }
17354
+ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
17355
+ const crons = extractWorkflowCrons(yamlText);
17356
+ const header = parseScheduleHeader(yamlText);
17357
+ if (!crons.length && !header.schedule) return null;
17358
+ const basename3 = workflowPath.split("/").pop() ?? workflowPath;
17359
+ const expectedId = `${repo}/${basename3.replace(/\.ya?ml$/, "")}`;
17360
+ const missing = SCHEDULE_HEADER_FIELDS.filter((f) => !header[f]);
17361
+ if (missing.length) {
17362
+ throw new Error(`schedules lift: ${expectedId} (${workflowPath}) is a scheduled workflow but is missing the eight-field entry-template header field(s): ${missing.join(", ")} \u2014 see docs/schedules.md "The entry template".`);
17363
+ }
17364
+ const h = header;
17365
+ if (h.schedule !== expectedId) {
17366
+ throw new Error(`schedules lift: ${workflowPath} header \`schedule: ${h.schedule}\` must equal the join key \`${expectedId}\` (<repo>/<workflow-basename>).`);
17367
+ }
17368
+ for (const cron of crons) {
17369
+ if (!cadenceContainsCron(h.cadence, cron)) {
17370
+ throw new Error(`schedules lift: ${expectedId} cadence \`${h.cadence}\` does not contain the workflow cron \`${cron}\` (whole-token comparison) \u2014 the declared cadence disagrees with the actual schedule.`);
17371
+ }
17372
+ }
17373
+ if (llmFromHeader(yamlText) === "unknown") {
17374
+ throw new Error(`schedules lift: ${expectedId} header \`llm: ${h.llm}\` does not classify (yes / no / embeddings) \u2014 an unreadable declaration would register a row whose LLM column lies.`);
17375
+ }
17376
+ return {
17377
+ id: expectedId,
17378
+ what: h.what,
17379
+ owner: h.owner,
17380
+ cadence: h.cadence,
17381
+ executor: h.executor,
17382
+ llm: h.llm,
17383
+ output: h.output,
17384
+ breaks: h.breaks,
17385
+ kill: h.kill,
17386
+ repo,
17387
+ sourcePath: workflowPath
17388
+ };
17389
+ }
17390
+ function scheduleRecordsFromWorkflows(repo, files) {
17391
+ const records = [];
17392
+ const seen = /* @__PURE__ */ new Set();
17393
+ for (const { path: path2, text } of files) {
17394
+ const rec = scheduleRecordFromWorkflow(repo, path2, text);
17395
+ if (!rec) continue;
17396
+ if (seen.has(rec.id)) {
17397
+ throw new Error(`schedules lift: duplicate schedule id \`${rec.id}\` \u2014 two workflow files in ${repo} share one join key (${rec.sourcePath}).`);
17398
+ }
17399
+ seen.add(rec.id);
17400
+ records.push(rec);
17401
+ }
17402
+ return records.sort((a, b) => a.id.localeCompare(b.id));
17403
+ }
17404
+
17405
+ // src/schedules-lift-command.ts
17406
+ var DEFAULT_WORKFLOWS_DIR = ".github/workflows";
17407
+ var SCHEDULE_REPO_RE = /^[A-Za-z0-9_.-]+$/;
17408
+ var SchedulesLiftUsageError = class extends Error {
17409
+ constructor(message) {
17410
+ super(message);
17411
+ this.name = "SchedulesLiftUsageError";
17412
+ }
17413
+ };
17414
+ var SCHEDULES_LIFT_EXIT_UNREACHABLE = 75;
17415
+ var RegistryUnreachableError = class extends Error {
17416
+ exitCode = SCHEDULES_LIFT_EXIT_UNREACHABLE;
17417
+ constructor(message) {
17418
+ super(message);
17419
+ this.name = "RegistryUnreachableError";
17420
+ }
17421
+ };
17422
+ async function readWorkflowFiles(dir) {
17423
+ let names;
17424
+ try {
17425
+ names = await (0, import_promises3.readdir)(dir);
17426
+ } catch {
17427
+ return [];
17428
+ }
17429
+ const files = [];
17430
+ for (const name of names.sort()) {
17431
+ if (!/\.ya?ml$/.test(name)) continue;
17432
+ files.push({ path: `.github/workflows/${name}`, text: await (0, import_promises3.readFile)((0, import_node_path17.join)(dir, name), "utf8") });
17433
+ }
17434
+ return files;
17435
+ }
17436
+ function repoNameFromRemoteUrl(url) {
17437
+ const base = url.trim().replace(/\.git$/, "").split(/[/:]/).filter(Boolean).pop();
17438
+ return base ?? null;
17439
+ }
17440
+ async function defaultPostLift(repo, records) {
17441
+ return postSchedulesLift(repo, records, registryClientDeps(await loadConfig()));
17442
+ }
17443
+ async function runSchedulesLift(opts, deps = {}) {
17444
+ let repo = opts.repo?.trim();
17445
+ if (!repo) {
17446
+ const url = await (deps.originUrl ?? (() => gitOut(["remote", "get-url", "origin"])))();
17447
+ repo = repoNameFromRemoteUrl(url) ?? void 0;
17448
+ if (!repo) {
17449
+ throw new Error("schedules lift: could not resolve the repo name from the origin remote \u2014 pass --repo <name>.");
17450
+ }
17451
+ }
17452
+ if (!SCHEDULE_REPO_RE.test(repo)) {
17453
+ throw new SchedulesLiftUsageError(`schedules lift: repo \`${repo}\` is not a bare repo segment (allowed: A-Z a-z 0-9 _ . -; no \`/\` or \`#\`) \u2014 the schedule id is <repo>/<name>, so a separator would form an invalid multi-segment id.`);
17454
+ }
17455
+ const dir = opts.dir ?? DEFAULT_WORKFLOWS_DIR;
17456
+ const files = await (deps.readFiles ?? readWorkflowFiles)(dir);
17457
+ if (!files.length) {
17458
+ throw new Error(`schedules lift: no workflow files under ${dir} \u2014 refusing to post an empty lift (the route prunes; run from the repo checkout or pass --dir).`);
17459
+ }
17460
+ const records = scheduleRecordsFromWorkflows(repo, files);
17461
+ const payload = { repo, schedules: records };
17462
+ if (opts.dryRun) return { repo, records, payload };
17463
+ const response = await (deps.postLift ?? defaultPostLift)(repo, records);
17464
+ if (!response.ok) {
17465
+ const unreachable = response.unreachable ?? (response.status === 404 ? "route-absent" : void 0);
17466
+ if (unreachable) {
17467
+ throw new RegistryUnreachableError(`schedules lift: registry unreachable (${unreachable}) \u2014 ${response.error ?? `HTTP ${response.status}`}`);
17468
+ }
17469
+ if (response.error) throw new Error(`schedules lift: ${response.error}`);
17470
+ const detail = response.body?.error ?? "";
17471
+ throw new Error(`schedules lift: HTTP ${response.status}${detail ? ` \u2014 ${detail}` : ""}`);
17472
+ }
17473
+ return { repo, records, payload, response };
17474
+ }
17475
+ function registerSchedulesLiftCommand(program3, deps = {}) {
17476
+ const schedules = program3.commands.find((c) => c.name() === "schedules");
17477
+ if (!schedules) throw new Error("schedules lift: registerSchedulesCommands must run first \u2014 the lift attaches to the `schedules` command");
17478
+ schedules.command("lift").description("lift this repo's eight-field workflow headers into SCHEDULE# registry rows (C2, #3186) \u2014 a per-repo replace; aborts the whole lift on any header violation; exits 75 when the registry is unreachable (#3187)").option("--repo <name>", "bare repo name for the join key (defaults to the origin remote basename, case-preserved)").option("--dir <path>", "workflows directory to lift (defaults to .github/workflows)").option("--dry-run", "print the {repo, schedules} body that would be POSTed; never write").action(async (o) => {
17479
+ try {
17480
+ const result = await runSchedulesLift({ repo: o.repo, dir: o.dir, dryRun: Boolean(o.dryRun) }, deps);
17481
+ if (o.dryRun) {
17482
+ console.log(JSON.stringify(result.payload, null, 2));
17483
+ return;
17484
+ }
17485
+ console.log(JSON.stringify(result.response?.body));
17486
+ } catch (e) {
17487
+ const err = e;
17488
+ if (err instanceof RegistryUnreachableError) {
17489
+ console.error(`mmi-cli ${err.message}`);
17490
+ return await cleanExit(err.exitCode);
17491
+ }
17492
+ return await failGraceful(err.message);
17493
+ }
17494
+ });
17495
+ }
17496
+
17162
17497
  // src/edge-tunnel.ts
17163
17498
  var HOSTNAME_RE = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/;
17164
17499
  var UPSTREAM_RE = /^https?:\/\/[^/\s]+(?::\d+)?(?:\/.*)?$/;
@@ -17618,7 +17953,7 @@ function registerQueryCommands(program3) {
17618
17953
  }
17619
17954
 
17620
17955
  // src/bootstrap-commands.ts
17621
- var import_node_fs18 = require("node:fs");
17956
+ var import_node_fs19 = require("node:fs");
17622
17957
 
17623
17958
  // src/bootstrap-verify.ts
17624
17959
  var TRAIN_BRANCHES2 = ["development", "rc", "main"];
@@ -18166,7 +18501,7 @@ function registerBootstrapCommands(program3) {
18166
18501
  client: defaultGitHubClient(),
18167
18502
  projectMeta: meta,
18168
18503
  deployModel: typeof meta?.deployModel === "string" ? meta.deployModel : void 0,
18169
- readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs18.existsSync)(path2) ? (0, import_node_fs18.readFileSync)(path2, "utf8") : null,
18504
+ readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs19.existsSync)(path2) ? (0, import_node_fs19.readFileSync)(path2, "utf8") : null,
18170
18505
  // requiredGcpApis is stored as an array by a JSON write, but `org project set --var KEY=VALUE` stores a raw
18171
18506
  // comma-string — accept either so the seeded value verifies regardless of how it was written.
18172
18507
  requiredGcpApis: (() => {
@@ -18217,12 +18552,12 @@ function registerBootstrapCommands(program3) {
18217
18552
  return fail(`bootstrap apply: ${e.message}`);
18218
18553
  }
18219
18554
  const manifestPath = "skills/bootstrap/seeds/manifest.json";
18220
- if (!(0, import_node_fs18.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
18221
- const manifest = loadBootstrapSeeds((0, import_node_fs18.readFileSync)(manifestPath, "utf8"));
18555
+ if (!(0, import_node_fs19.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
18556
+ const manifest = loadBootstrapSeeds((0, import_node_fs19.readFileSync)(manifestPath, "utf8"));
18222
18557
  const baseBranch = o.class === "content" ? "main" : "development";
18223
18558
  const slug = parsedRepo.slug;
18224
18559
  const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
18225
- const readFile5 = (p) => (0, import_node_fs18.existsSync)(p) ? (0, import_node_fs18.readFileSync)(p, "utf8") : null;
18560
+ const readFile6 = (p) => (0, import_node_fs19.existsSync)(p) ? (0, import_node_fs19.readFileSync)(p, "utf8") : null;
18226
18561
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
18227
18562
  const rawVars = {};
18228
18563
  for (const value of cmdOpts.var ?? []) {
@@ -18300,7 +18635,7 @@ function registerBootstrapCommands(program3) {
18300
18635
  }
18301
18636
  const planned = planSeedAction(resolved, exists);
18302
18637
  const isBlock = resolved.source === "managed-block";
18303
- const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile5) : null;
18638
+ const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile6) : null;
18304
18639
  const action = reconcileSeedAction(planned, content, isBlock, remoteContent);
18305
18640
  actions.push(action);
18306
18641
  if (o.execute && (action.action === "create" || action.action === "update")) {
@@ -18355,7 +18690,7 @@ function registerBootstrapCommands(program3) {
18355
18690
  }
18356
18691
  const rulesetSeed = manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
18357
18692
  if (rulesetSeed) {
18358
- const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile5);
18693
+ const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile6);
18359
18694
  if (rulesetContent) {
18360
18695
  try {
18361
18696
  const activation = await activateProductRuleset(repo, stripRulesetComment(rulesetContent), defaultGitHubClient());
@@ -18418,12 +18753,12 @@ LIVE apply to ${repo}:
18418
18753
  }
18419
18754
 
18420
18755
  // src/stage-commands.ts
18421
- var import_node_fs20 = require("node:fs");
18422
- var import_node_path17 = require("node:path");
18756
+ var import_node_fs21 = require("node:fs");
18757
+ var import_node_path19 = require("node:path");
18423
18758
 
18424
18759
  // src/port-registry.ts
18425
- var import_node_fs19 = require("node:fs");
18426
- var import_node_path16 = require("node:path");
18760
+ var import_node_fs20 = require("node:fs");
18761
+ var import_node_path18 = require("node:path");
18427
18762
 
18428
18763
  // ../infra/port-geometry.mjs
18429
18764
  var PORT_BLOCK = 100;
@@ -18437,8 +18772,8 @@ function nextPortBlock(registry2) {
18437
18772
  return [base, base + PORT_SPAN];
18438
18773
  }
18439
18774
  function loadPortRegistry(path2) {
18440
- if (!(0, import_node_fs19.existsSync)(path2)) return {};
18441
- const raw = JSON.parse((0, import_node_fs19.readFileSync)(path2, "utf8"));
18775
+ if (!(0, import_node_fs20.existsSync)(path2)) return {};
18776
+ const raw = JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
18442
18777
  const out = {};
18443
18778
  for (const [key, value] of Object.entries(raw)) {
18444
18779
  if (Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number")) {
@@ -18452,9 +18787,9 @@ function ensurePortRange(repo, path2) {
18452
18787
  const existing = registry2[repo];
18453
18788
  if (existing) return existing;
18454
18789
  const range = nextPortBlock(registry2);
18455
- const raw = (0, import_node_fs19.existsSync)(path2) ? JSON.parse((0, import_node_fs19.readFileSync)(path2, "utf8")) : {};
18790
+ const raw = (0, import_node_fs20.existsSync)(path2) ? JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8")) : {};
18456
18791
  raw[repo] = range;
18457
- (0, import_node_fs19.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
18792
+ (0, import_node_fs20.writeFileSync)(path2, JSON.stringify(raw, null, 2) + "\n", "utf8");
18458
18793
  return range;
18459
18794
  }
18460
18795
  function portCursorSeed(registry2) {
@@ -18476,22 +18811,22 @@ function existingPortRange(repo, registry2) {
18476
18811
  return registry2[repo] ?? null;
18477
18812
  }
18478
18813
  function portRangeInfraAt(root, source) {
18479
- const registryPath = (0, import_node_path16.join)(root, "infra", "port-ranges.json");
18480
- const ddbScriptPath = (0, import_node_path16.join)(root, "infra", "port-ddb.mjs");
18481
- if (!(0, import_node_fs19.existsSync)(registryPath) || !(0, import_node_fs19.existsSync)(ddbScriptPath)) return null;
18814
+ const registryPath = (0, import_node_path18.join)(root, "infra", "port-ranges.json");
18815
+ const ddbScriptPath = (0, import_node_path18.join)(root, "infra", "port-ddb.mjs");
18816
+ if (!(0, import_node_fs20.existsSync)(registryPath) || !(0, import_node_fs20.existsSync)(ddbScriptPath)) return null;
18482
18817
  return { root, source, registryPath, ddbScriptPath };
18483
18818
  }
18484
18819
  function resolvePortRangeInfra(cwd, packageDir) {
18485
18820
  const direct = portRangeInfraAt(cwd, "cwd");
18486
18821
  if (direct) return direct;
18487
- for (let dir = cwd; ; dir = (0, import_node_path16.dirname)(dir)) {
18488
- const sibling = portRangeInfraAt((0, import_node_path16.join)(dir, "MMI-Hub"), "sibling-hub");
18822
+ for (let dir = cwd; ; dir = (0, import_node_path18.dirname)(dir)) {
18823
+ const sibling = portRangeInfraAt((0, import_node_path18.join)(dir, "MMI-Hub"), "sibling-hub");
18489
18824
  if (sibling) return sibling;
18490
- const parent = (0, import_node_path16.dirname)(dir);
18825
+ const parent = (0, import_node_path18.dirname)(dir);
18491
18826
  if (parent === dir) break;
18492
18827
  }
18493
18828
  if (packageDir) {
18494
- const pkgRoot = (0, import_node_path16.join)(packageDir, "..", "..");
18829
+ const pkgRoot = (0, import_node_path18.join)(packageDir, "..", "..");
18495
18830
  const pkgFrom = portRangeInfraAt(pkgRoot, "pkg-root");
18496
18831
  if (pkgFrom) return pkgFrom;
18497
18832
  }
@@ -18667,8 +19002,8 @@ function registerStageCommands(program3) {
18667
19002
  const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
18668
19003
  return decideStage({
18669
19004
  registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
18670
- hasCompose: (0, import_node_fs20.existsSync)((0, import_node_path17.join)(process.cwd(), "docker-compose.yml")),
18671
- hasEnvExample: (0, import_node_fs20.existsSync)((0, import_node_path17.join)(process.cwd(), ".env.example"))
19005
+ hasCompose: (0, import_node_fs21.existsSync)((0, import_node_path19.join)(process.cwd(), "docker-compose.yml")),
19006
+ hasEnvExample: (0, import_node_fs21.existsSync)((0, import_node_path19.join)(process.cwd(), ".env.example"))
18672
19007
  });
18673
19008
  }
18674
19009
  async function fetchStageVaultEnvMerge() {
@@ -19106,8 +19441,8 @@ function registerBoardCommands(program3) {
19106
19441
  }
19107
19442
 
19108
19443
  // src/merge-cleanup.ts
19109
- var import_node_fs21 = require("node:fs");
19110
- var import_promises4 = require("node:fs/promises");
19444
+ var import_node_fs22 = require("node:fs");
19445
+ var import_promises5 = require("node:fs/promises");
19111
19446
  var import_node_child_process10 = require("node:child_process");
19112
19447
 
19113
19448
  // src/board-advance.ts
@@ -19193,23 +19528,23 @@ function boardAdvanceFailureMessage(result) {
19193
19528
  }
19194
19529
 
19195
19530
  // src/deferred-registry-store.ts
19196
- var import_promises3 = require("node:fs/promises");
19197
- var import_node_path18 = require("node:path");
19531
+ var import_promises4 = require("node:fs/promises");
19532
+ var import_node_path20 = require("node:path");
19198
19533
  var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
19199
19534
  async function atomicWrite(target, contents) {
19200
19535
  const tmp = `${target}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
19201
- await (0, import_promises3.writeFile)(tmp, contents, "utf8");
19536
+ await (0, import_promises4.writeFile)(tmp, contents, "utf8");
19202
19537
  try {
19203
19538
  await renameWithRetry(tmp, target);
19204
19539
  } catch (e) {
19205
- await (0, import_promises3.unlink)(tmp).catch(() => void 0);
19540
+ await (0, import_promises4.unlink)(tmp).catch(() => void 0);
19206
19541
  throw e;
19207
19542
  }
19208
19543
  }
19209
19544
  async function renameWithRetry(from, to, attempts = 5, backoffMs = 20) {
19210
19545
  for (let i = 0; ; i++) {
19211
19546
  try {
19212
- await (0, import_promises3.rename)(from, to);
19547
+ await (0, import_promises4.rename)(from, to);
19213
19548
  return;
19214
19549
  } catch (e) {
19215
19550
  const code = e.code;
@@ -19221,7 +19556,7 @@ async function renameWithRetry(from, to, attempts = 5, backoffMs = 20) {
19221
19556
  async function readStrict(registryPath) {
19222
19557
  let text;
19223
19558
  try {
19224
- text = await (0, import_promises3.readFile)(registryPath, "utf8");
19559
+ text = await (0, import_promises4.readFile)(registryPath, "utf8");
19225
19560
  } catch (e) {
19226
19561
  if (e.code === "ENOENT") return [];
19227
19562
  throw e;
@@ -19232,15 +19567,15 @@ async function acquireLock(lockPath, opts, deadline) {
19232
19567
  for (; ; ) {
19233
19568
  let handle;
19234
19569
  try {
19235
- handle = await (0, import_promises3.open)(lockPath, "wx");
19570
+ handle = await (0, import_promises4.open)(lockPath, "wx");
19236
19571
  } catch (e) {
19237
19572
  const code = e.code;
19238
19573
  if (code !== "EEXIST" && code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") throw e;
19239
19574
  try {
19240
- const age = Date.now() - (await (0, import_promises3.stat)(lockPath)).mtimeMs;
19575
+ const age = Date.now() - (await (0, import_promises4.stat)(lockPath)).mtimeMs;
19241
19576
  if (age > opts.staleMs) {
19242
19577
  try {
19243
- await (0, import_promises3.unlink)(lockPath);
19578
+ await (0, import_promises4.unlink)(lockPath);
19244
19579
  continue;
19245
19580
  } catch (unlinkError) {
19246
19581
  const code2 = unlinkError.code;
@@ -19269,7 +19604,7 @@ async function acquireLock(lockPath, opts, deadline) {
19269
19604
  }
19270
19605
  async function lockHeldBy(lockPath, token) {
19271
19606
  try {
19272
- return await (0, import_promises3.readFile)(lockPath, "utf8") === token;
19607
+ return await (0, import_promises4.readFile)(lockPath, "utf8") === token;
19273
19608
  } catch {
19274
19609
  return false;
19275
19610
  }
@@ -19277,7 +19612,7 @@ async function lockHeldBy(lockPath, token) {
19277
19612
  async function releaseLock(lockPath, guard) {
19278
19613
  await guard.handle.close().catch(() => void 0);
19279
19614
  if (await lockHeldBy(lockPath, guard.token)) {
19280
- await (0, import_promises3.unlink)(lockPath).catch(() => void 0);
19615
+ await (0, import_promises4.unlink)(lockPath).catch(() => void 0);
19281
19616
  }
19282
19617
  }
19283
19618
  function makeDeferredWorktreeStore(registryPath, lockOpts = {}) {
@@ -19291,19 +19626,19 @@ function makeDeferredWorktreeStore(registryPath, lockOpts = {}) {
19291
19626
  // Lenient read for the sweep's initial fetch: any error (missing/corrupt) yields an empty queue.
19292
19627
  read: async () => {
19293
19628
  try {
19294
- return parseDeferredWorktreesFile(await (0, import_promises3.readFile)(registryPath, "utf8"));
19629
+ return parseDeferredWorktreesFile(await (0, import_promises4.readFile)(registryPath, "utf8"));
19295
19630
  } catch {
19296
19631
  return [];
19297
19632
  }
19298
19633
  },
19299
19634
  // Standalone atomic write — THROWS on failure (no best-effort swallow, #2846).
19300
19635
  write: async (entries) => {
19301
- await (0, import_promises3.mkdir)((0, import_node_path18.dirname)(registryPath), { recursive: true });
19636
+ await (0, import_promises4.mkdir)((0, import_node_path20.dirname)(registryPath), { recursive: true });
19302
19637
  await atomicWrite(registryPath, serializeDeferredWorktrees(entries));
19303
19638
  },
19304
19639
  // Serialized read-modify-write under the repo-wide lock (#2846).
19305
19640
  update: async (mutate) => {
19306
- await (0, import_promises3.mkdir)((0, import_node_path18.dirname)(registryPath), { recursive: true });
19641
+ await (0, import_promises4.mkdir)((0, import_node_path20.dirname)(registryPath), { recursive: true });
19307
19642
  const deadline = Date.now() + opts.maxWaitMs;
19308
19643
  for (; ; ) {
19309
19644
  const guard = await acquireLock(lockPath, opts, deadline);
@@ -19430,7 +19765,7 @@ async function applyGcPlan(plan, remote) {
19430
19765
  return cleanupPrMergeLocalBranch(branch.branch, {
19431
19766
  beforeWorktrees,
19432
19767
  startingPath: branch.worktreePath,
19433
- pathExists: (p) => (0, import_node_fs21.existsSync)(p),
19768
+ pathExists: (p) => (0, import_node_fs22.existsSync)(p),
19434
19769
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
19435
19770
  teardownWorktreeStage,
19436
19771
  deferredStore,
@@ -19456,7 +19791,7 @@ async function applyGcPlan(plan, remote) {
19456
19791
  for (const wt of plan.worktreeDirs) {
19457
19792
  try {
19458
19793
  const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, siblingRoot, {
19459
- realpath: (path2) => (0, import_node_fs21.realpathSync)(path2)
19794
+ realpath: (path2) => (0, import_node_fs22.realpathSync)(path2)
19460
19795
  });
19461
19796
  if (!cleanupTarget.ok) {
19462
19797
  result.failed.push(`${wt.path}: ${cleanupTarget.reason}`);
@@ -19583,13 +19918,13 @@ var realWorktreeDirRemover = {
19583
19918
  probe: (p) => {
19584
19919
  let st;
19585
19920
  try {
19586
- st = (0, import_node_fs21.lstatSync)(p);
19921
+ st = (0, import_node_fs22.lstatSync)(p);
19587
19922
  } catch {
19588
19923
  return null;
19589
19924
  }
19590
19925
  if (st.isSymbolicLink()) return "link";
19591
19926
  try {
19592
- (0, import_node_fs21.readlinkSync)(p);
19927
+ (0, import_node_fs22.readlinkSync)(p);
19593
19928
  return "link";
19594
19929
  } catch {
19595
19930
  }
@@ -19597,7 +19932,7 @@ var realWorktreeDirRemover = {
19597
19932
  },
19598
19933
  readdir: (p) => {
19599
19934
  try {
19600
- return (0, import_node_fs21.readdirSync)(p);
19935
+ return (0, import_node_fs22.readdirSync)(p);
19601
19936
  } catch {
19602
19937
  return [];
19603
19938
  }
@@ -19606,12 +19941,12 @@ var realWorktreeDirRemover = {
19606
19941
  // leaving the target); a file symlink with unlink. rmdir first, fall back to unlink.
19607
19942
  detachLink: (p) => {
19608
19943
  try {
19609
- (0, import_node_fs21.rmdirSync)(p);
19944
+ (0, import_node_fs22.rmdirSync)(p);
19610
19945
  } catch {
19611
- (0, import_node_fs21.unlinkSync)(p);
19946
+ (0, import_node_fs22.unlinkSync)(p);
19612
19947
  }
19613
19948
  },
19614
- removeTree: (p) => (0, import_promises4.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
19949
+ removeTree: (p) => (0, import_promises5.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
19615
19950
  };
19616
19951
  async function resolvePrimaryCheckout(execGit) {
19617
19952
  try {
@@ -19641,9 +19976,9 @@ async function worktreeHasStageState(worktreePath) {
19641
19976
  }
19642
19977
  }
19643
19978
  function stageStateFileBelongsToWorktree(statePath, worktreePath) {
19644
- if (!(0, import_node_fs21.existsSync)(statePath)) return false;
19979
+ if (!(0, import_node_fs22.existsSync)(statePath)) return false;
19645
19980
  try {
19646
- const state = JSON.parse((0, import_node_fs21.readFileSync)(statePath, "utf8"));
19981
+ const state = JSON.parse((0, import_node_fs22.readFileSync)(statePath, "utf8"));
19647
19982
  const recordedCwd = typeof state.identity?.cwd === "string" ? state.identity.cwd : typeof state.cwd === "string" ? state.cwd : "";
19648
19983
  return Boolean(recordedCwd && isPathUnderDirectory(recordedCwd, worktreePath));
19649
19984
  } catch {
@@ -19771,8 +20106,8 @@ async function fetchRestCorePool(gh = defaultGhApi) {
19771
20106
  }
19772
20107
 
19773
20108
  // src/worktree-lifecycle-commands.ts
19774
- var import_node_fs22 = require("node:fs");
19775
- var import_node_path19 = require("node:path");
20109
+ var import_node_fs23 = require("node:fs");
20110
+ var import_node_path21 = require("node:path");
19776
20111
  var GH_TIMEOUT_MS = 2e4;
19777
20112
  var DEFAULT_BASE = "origin/development";
19778
20113
  var DEFAULT_REMOTE = "origin";
@@ -19815,7 +20150,6 @@ function classifyStaleLeaks(input) {
19815
20150
  const protectedBranches = input.protectedBranches ?? PROTECTED_BRANCHES2;
19816
20151
  const leaks = [];
19817
20152
  const worktreeBranches2 = new Set(input.worktrees.filter((w) => !w.primary).map((w) => w.branch));
19818
- const worktreeByBranch = new Map(input.worktrees.map((w) => [w.branch, w.path]));
19819
20153
  const stageByPath = new Map(input.stages.map((s) => [s.path, s.port]));
19820
20154
  for (const wt of input.worktrees) {
19821
20155
  if (wt.primary) continue;
@@ -19910,13 +20244,13 @@ function registerWorktreeCommands(program3) {
19910
20244
  if (PROTECTED_BRANCHES2.has(branch)) {
19911
20245
  return fail(`worktree land: refusing to land protected branch '${branch}'`, { code: ERROR_CODES.ERR_BAD_ENUM });
19912
20246
  }
19913
- const gitFile = (0, import_node_path19.join)(wtPath, ".git");
19914
- const isLinked = (0, import_node_fs22.existsSync)(gitFile) && (0, import_node_fs22.statSync)(gitFile).isFile();
20247
+ const gitFile = (0, import_node_path21.join)(wtPath, ".git");
20248
+ const isLinked = (0, import_node_fs23.existsSync)(gitFile) && (0, import_node_fs23.statSync)(gitFile).isFile();
19915
20249
  if (apply && !isLinked) {
19916
20250
  return fail("worktree land: run from inside the linked worktree you want to land (this is the primary checkout)");
19917
20251
  }
19918
20252
  const commonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
19919
- const primaryCheckout = commonDir ? (0, import_node_path19.dirname)(commonDir) : wtPath;
20253
+ const primaryCheckout = commonDir ? (0, import_node_path21.dirname)(commonDir) : wtPath;
19920
20254
  const stageSummary = readStageSummary(wtPath);
19921
20255
  const hasStage = stageSummary != null;
19922
20256
  const steps = landSteps(branch, true, hasStage);
@@ -20028,10 +20362,10 @@ async function gatherWorktreeContext() {
20028
20362
  }
20029
20363
  const orphanDirs = [];
20030
20364
  const wtRoot = siblingMmiWorktreesRoot(repoRoot2);
20031
- if ((0, import_node_fs22.existsSync)(wtRoot)) {
20365
+ if ((0, import_node_fs23.existsSync)(wtRoot)) {
20032
20366
  let entries = [];
20033
20367
  try {
20034
- entries = (0, import_node_fs22.readdirSync)(wtRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path19.join)(wtRoot, e.name));
20368
+ entries = (0, import_node_fs23.readdirSync)(wtRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => (0, import_node_path21.join)(wtRoot, e.name));
20035
20369
  } catch {
20036
20370
  }
20037
20371
  const known = new Set(worktrees.map((w) => w.path));
@@ -20056,7 +20390,7 @@ async function bestEffortGit(args, cwd, step, timeoutMs = GIT_TIMEOUT_MS) {
20056
20390
  }
20057
20391
 
20058
20392
  // src/issue-commands.ts
20059
- var import_node_fs23 = require("node:fs");
20393
+ var import_node_fs24 = require("node:fs");
20060
20394
  var import_node_crypto5 = require("node:crypto");
20061
20395
  var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
20062
20396
  async function editIssue(client, options, deps = {}) {
@@ -20066,39 +20400,23 @@ async function editIssue(client, options, deps = {}) {
20066
20400
  const url = `https://github.com/${repo}/issues/${parsed.number}`;
20067
20401
  const patch = {};
20068
20402
  let bodyChanged = false;
20069
- let northStarSlug;
20070
20403
  if (options.title !== void 0) patch.title = options.title;
20071
20404
  if (options.body !== void 0 || options.bodyFile !== void 0) {
20072
20405
  let body;
20073
20406
  if (options.bodyFile) {
20074
- const td = deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs23.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
20407
+ const td = deps.textDeps ?? { readFile: (p, e) => Promise.resolve((0, import_node_fs24.readFileSync)(p, e)), readStdin: () => Promise.resolve("") };
20075
20408
  body = await resolveIssueBody({ body: options.body, bodyFile: options.bodyFile }, td);
20076
20409
  } else {
20077
20410
  body = options.body ?? "";
20078
20411
  }
20079
- if (options.northStar) {
20080
- northStarSlug = normalizeNorthStarSlug(options.northStar);
20081
- body = appendNorthStarLine(body, northStarSlug);
20082
- }
20083
20412
  patch.body = body;
20084
20413
  bodyChanged = true;
20085
- } else if (options.northStar) {
20086
- northStarSlug = normalizeNorthStarSlug(options.northStar);
20087
- const current = await client.rest("GET", `repos/${repo}/issues/${parsed.number}`);
20088
- patch.body = appendNorthStarLine(current?.body ?? "", northStarSlug);
20089
- bodyChanged = true;
20090
20414
  }
20091
20415
  if (patch.title !== void 0 || patch.body !== void 0) {
20092
20416
  await client.rest("PATCH", `repos/${repo}/issues/${parsed.number}`, { body: patch });
20093
20417
  }
20094
20418
  const labelsAdded = [];
20095
20419
  const labelsRemoved = [];
20096
- if (options.northStar && northStarSlug) {
20097
- const nsLabel = northStarLabel(northStarSlug);
20098
- if (!options.addLabel?.includes(nsLabel)) {
20099
- (options.addLabel ??= []).push(nsLabel);
20100
- }
20101
- }
20102
20420
  if (options.addLabel?.length) {
20103
20421
  await client.rest("POST", `repos/${repo}/issues/${parsed.number}/labels`, { body: { labels: options.addLabel } });
20104
20422
  labelsAdded.push(...options.addLabel);
@@ -20128,8 +20446,7 @@ async function editIssue(client, options, deps = {}) {
20128
20446
  bodyChanged,
20129
20447
  labelsAdded,
20130
20448
  labelsRemoved,
20131
- ...parentResult ? { parent: parentResult } : {},
20132
- ...northStarSlug ? { northStarSlug } : {}
20449
+ ...parentResult ? { parent: parentResult } : {}
20133
20450
  };
20134
20451
  }
20135
20452
  async function closeIssue(client, options, deps = {}) {
@@ -20309,7 +20626,7 @@ ${spec.body ?? ""}`;
20309
20626
  const hash = (0, import_node_crypto5.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
20310
20627
  return `${batchKey}:${hash}`;
20311
20628
  }
20312
- var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "northStar", "parent", "repo"]);
20629
+ var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "parent", "repo"]);
20313
20630
  function validateBatchSpecs(specs) {
20314
20631
  const errors = [];
20315
20632
  const validated = [];
@@ -20349,28 +20666,6 @@ function validateBatchSpecs(specs) {
20349
20666
  }
20350
20667
  return { ok: errors.length === 0, errors, validated };
20351
20668
  }
20352
- var NORTH_STAR_LABEL_COLOR = "5319e7";
20353
- function northStarLabelsForBatch(specs) {
20354
- const labels = /* @__PURE__ */ new Set();
20355
- for (const spec of specs) {
20356
- if (spec.northStar) labels.add(northStarLabel(normalizeNorthStarSlug(spec.northStar)));
20357
- for (const l of spec.labels ?? []) if (l.startsWith("northstar:")) labels.add(l);
20358
- }
20359
- return [...labels].sort((a, b) => a.localeCompare(b));
20360
- }
20361
- async function ensureNorthStarLabels(client, repo, specs) {
20362
- const minted = [];
20363
- for (const name of northStarLabelsForBatch(specs)) {
20364
- try {
20365
- await client.rest("POST", `repos/${repo}/labels`, {
20366
- body: { name, color: NORTH_STAR_LABEL_COLOR, description: `North Star: ${name.slice("northstar:".length)}` }
20367
- });
20368
- minted.push(name);
20369
- } catch {
20370
- }
20371
- }
20372
- return minted;
20373
- }
20374
20669
  async function createIssuesBatch(specs, options, deps = {}) {
20375
20670
  const client = deps.client ?? defaultGitHubClient();
20376
20671
  const validation = validateBatchSpecs(specs);
@@ -20384,14 +20679,6 @@ ${lines}`);
20384
20679
  throw new Error("could not resolve repo \u2014 pass --repo owner/repo or set repo per row");
20385
20680
  }
20386
20681
  const rowRepo = (spec) => spec.repo ?? defaultRepo;
20387
- const byRepo = /* @__PURE__ */ new Map();
20388
- for (const v of validation.validated) {
20389
- const r = rowRepo(v.spec);
20390
- byRepo.set(r, [...byRepo.get(r) ?? [], v.spec]);
20391
- }
20392
- for (const [r, specsForRepo] of byRepo) {
20393
- await ensureNorthStarLabels(client, r, specsForRepo);
20394
- }
20395
20682
  const created = [];
20396
20683
  const failures = [];
20397
20684
  for (const entry of validation.validated) {
@@ -20406,12 +20693,6 @@ ${lines}`);
20406
20693
  }
20407
20694
  }
20408
20695
  let body = spec.body ?? "";
20409
- if (spec.northStar) {
20410
- const slug = normalizeNorthStarSlug(spec.northStar);
20411
- body = appendNorthStarLine(body, slug);
20412
- const nsLabel = northStarLabel(slug);
20413
- if (!spec.labels?.includes(nsLabel)) (spec.labels ??= []).push(nsLabel);
20414
- }
20415
20696
  if (rowKey) {
20416
20697
  body = appendIdempotencyMarker(body, rowKey);
20417
20698
  }
@@ -20465,7 +20746,7 @@ function registerIssueLifecycleCommands(program3) {
20465
20746
  const issue2 = program3.commands.find((c) => c.name() === "issue");
20466
20747
  if (!issue2) return;
20467
20748
  mutating(
20468
- issue2.command("edit <ref>").description("edit an issue title/body/labels, reparent it (--parent), or link a North Star \u2014 reparenting is impossible via raw gh").option("--title <title>", "new title").option("--body <body>", "new body (markdown)").option("--body-file <path|->", "read new body from a UTF-8 file, or stdin with -").option("--add-label <label...>", "label(s) to add (repeatable)").option("--remove-label <label...>", "label(s) to remove (repeatable)").option("--parent <ref>", "reparent: link under this parent (#123, owner/repo#123, or URL)").option("--north-star <slug>", "append a North Star body line + northstar:<slug> label").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)"),
20749
+ issue2.command("edit <ref>").description("edit an issue title/body/labels or reparent it (--parent) \u2014 reparenting is impossible via raw gh").option("--title <title>", "new title").option("--body <body>", "new body (markdown)").option("--body-file <path|->", "read new body from a UTF-8 file, or stdin with -").option("--add-label <label...>", "label(s) to add (repeatable)").option("--remove-label <label...>", "label(s) to remove (repeatable)").option("--parent <ref>", "reparent: link under this parent (#123, owner/repo#123, or URL)").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)"),
20469
20750
  (opts, args) => ({ command: "issue edit", ref: args[0], fields: Object.keys(opts).filter((k) => k !== "repo" && opts[k] !== void 0) })
20470
20751
  ).action(async (ref, o) => {
20471
20752
  try {
@@ -20478,8 +20759,7 @@ function registerIssueLifecycleCommands(program3) {
20478
20759
  bodyFile: o.bodyFile,
20479
20760
  addLabel: o.addLabel,
20480
20761
  removeLabel: o.removeLabel,
20481
- parent: o.parent,
20482
- northStar: o.northStar
20762
+ parent: o.parent
20483
20763
  });
20484
20764
  console.log(JSON.stringify(result));
20485
20765
  } catch (e) {
@@ -20598,7 +20878,7 @@ function extendCreateCommand(issue2) {
20598
20878
  if (opts.batch) {
20599
20879
  let specs;
20600
20880
  try {
20601
- const raw = (0, import_node_fs23.readFileSync)(opts.batch, "utf8");
20881
+ const raw = (0, import_node_fs24.readFileSync)(opts.batch, "utf8");
20602
20882
  specs = JSON.parse(raw);
20603
20883
  if (!Array.isArray(specs)) throw new Error("batch file must contain a JSON array");
20604
20884
  } catch (e) {
@@ -20642,12 +20922,12 @@ ${lines}`);
20642
20922
  }
20643
20923
 
20644
20924
  // src/train-commands.ts
20645
- var import_node_fs25 = require("node:fs");
20646
- var import_node_path21 = require("node:path");
20925
+ var import_node_fs26 = require("node:fs");
20926
+ var import_node_path23 = require("node:path");
20647
20927
 
20648
20928
  // src/plugin-guard-io.ts
20649
- var import_node_fs24 = require("node:fs");
20650
- var import_node_path20 = require("node:path");
20929
+ var import_node_fs25 = require("node:fs");
20930
+ var import_node_path22 = require("node:path");
20651
20931
  var import_node_os6 = require("node:os");
20652
20932
 
20653
20933
  // src/plugin-guard.ts
@@ -20777,11 +21057,11 @@ function runHostBin(bin, args, opts) {
20777
21057
  }
20778
21058
  var installedPluginsPath2 = (surface = detectSurface(process.env)) => {
20779
21059
  const homeDir = surface === "codex" ? ".codex" : ".claude";
20780
- return (0, import_node_path20.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
21060
+ return (0, import_node_path22.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "installed_plugins.json");
20781
21061
  };
20782
21062
  function readInstalledPlugins(surface = detectSurface(process.env)) {
20783
21063
  try {
20784
- return JSON.parse((0, import_node_fs24.readFileSync)(installedPluginsPath2(surface), "utf8"));
21064
+ return JSON.parse((0, import_node_fs25.readFileSync)(installedPluginsPath2(surface), "utf8"));
20785
21065
  } catch {
20786
21066
  return null;
20787
21067
  }
@@ -20789,13 +21069,13 @@ function readInstalledPlugins(surface = detectSurface(process.env)) {
20789
21069
  function marketplaceCloneCandidates(surface, home) {
20790
21070
  if (surface === "codex") {
20791
21071
  return [
20792
- (0, import_node_path20.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
20793
- (0, import_node_path20.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
21072
+ (0, import_node_path22.join)(home, ".codex", ".tmp", "marketplaces", "mutmutco"),
21073
+ (0, import_node_path22.join)(home, ".codex", "plugins", "marketplaces", "mutmutco")
20794
21074
  ];
20795
21075
  }
20796
- return [(0, import_node_path20.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
21076
+ return [(0, import_node_path22.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
20797
21077
  }
20798
- function marketplaceClonePresent(surface, home, exists = import_node_fs24.existsSync) {
21078
+ function marketplaceClonePresent(surface, home, exists = import_node_fs25.existsSync) {
20799
21079
  return marketplaceCloneCandidates(surface, home).some(exists);
20800
21080
  }
20801
21081
  async function fetchNpmReleasedVersion() {
@@ -20813,7 +21093,7 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
20813
21093
  isOrgRepo,
20814
21094
  installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
20815
21095
  marketplaceClonePresent: marketplaceClonePresent(surface, (0, import_node_os6.homedir)()),
20816
- pluginCachePresent: (0, import_node_fs24.existsSync)((0, import_node_path20.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
21096
+ pluginCachePresent: (0, import_node_fs25.existsSync)((0, import_node_path22.join)((0, import_node_os6.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
20817
21097
  };
20818
21098
  }
20819
21099
  function claudePluginGuardState(isOrgRepo) {
@@ -20937,7 +21217,7 @@ function formatTrainStatus(r) {
20937
21217
  // src/train-commands.ts
20938
21218
  function readRepoVersion() {
20939
21219
  try {
20940
- return JSON.parse((0, import_node_fs25.readFileSync)((0, import_node_path21.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
21220
+ return JSON.parse((0, import_node_fs26.readFileSync)((0, import_node_path23.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
20941
21221
  } catch {
20942
21222
  return void 0;
20943
21223
  }
@@ -21437,7 +21717,7 @@ function registerExplainCommand(program3) {
21437
21717
  }
21438
21718
 
21439
21719
  // src/pr-commands.ts
21440
- var import_promises5 = require("node:fs/promises");
21720
+ var import_promises6 = require("node:fs/promises");
21441
21721
  var GC_GH_TIMEOUT_MS4 = 2e4;
21442
21722
  var CHECKS_WATCH_POLL_MS = 15e3;
21443
21723
  var CHECKS_WATCH_TIMEOUT_MS = 10 * 6e4;
@@ -21581,10 +21861,10 @@ function registerPrLifecycleCommands(program3) {
21581
21861
  let body;
21582
21862
  try {
21583
21863
  if (o.title || o.titleFile) {
21584
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises5.readFile, readStdin });
21864
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
21585
21865
  }
21586
21866
  if (o.body || o.bodyFile) {
21587
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises5.readFile, readStdin });
21867
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
21588
21868
  }
21589
21869
  } catch (e) {
21590
21870
  return fail(`pr edit: ${e.message}`);
@@ -21614,7 +21894,7 @@ function registerPrLifecycleCommands(program3) {
21614
21894
  }
21615
21895
  let body;
21616
21896
  try {
21617
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises5.readFile, readStdin });
21897
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
21618
21898
  } catch (e) {
21619
21899
  return fail(`pr comment: ${e.message}`);
21620
21900
  }
@@ -22167,7 +22447,7 @@ function checkSchedules(probe) {
22167
22447
  ok: false,
22168
22448
  label: "schedules",
22169
22449
  detail: `${probe.drift.length} drift finding(s)`,
22170
- fix: "run `mmi-cli org schedules` \u2014 file/registry/live disagree; each drift line names its per-class remedy, applied in the owning repo",
22450
+ fix: "run `mmi-cli org schedules` \u2014 each drift line names its per-class remedy (file/registry/live mismatch or harbour enforcement), applied in the owning repo",
22171
22451
  verbose: evidence
22172
22452
  };
22173
22453
  }
@@ -22339,9 +22619,9 @@ async function runDoctorClean(opts, io, deps) {
22339
22619
  }
22340
22620
 
22341
22621
  // src/doctor-io.ts
22342
- var import_node_fs26 = require("node:fs");
22622
+ var import_node_fs27 = require("node:fs");
22343
22623
  var import_node_os7 = require("node:os");
22344
- var import_node_path22 = require("node:path");
22624
+ var import_node_path24 = require("node:path");
22345
22625
  var import_node_child_process11 = require("node:child_process");
22346
22626
  var import_node_util8 = require("node:util");
22347
22627
  var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process11.execFile);
@@ -22349,7 +22629,7 @@ var MMI_PLUGIN_ID2 = "mmi@mutmutco";
22349
22629
  function installedClaudePluginVersion() {
22350
22630
  try {
22351
22631
  const file = JSON.parse(
22352
- (0, import_node_fs26.readFileSync)((0, import_node_path22.join)((0, import_node_os7.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
22632
+ (0, import_node_fs27.readFileSync)((0, import_node_path24.join)((0, import_node_os7.homedir)(), ".claude", "plugins", "installed_plugins.json"), "utf8")
22353
22633
  );
22354
22634
  const versions = (file.plugins?.[MMI_PLUGIN_ID2] ?? []).map((r) => r.version).filter((v) => Boolean(v));
22355
22635
  if (versions.length === 0) return void 0;
@@ -22370,13 +22650,13 @@ function worktreeRootSync() {
22370
22650
  }
22371
22651
  var gitignorePath = () => {
22372
22652
  const root = worktreeRootSync();
22373
- return root === null ? null : (0, import_node_path22.join)(root, ".gitignore");
22653
+ return root === null ? null : (0, import_node_path24.join)(root, ".gitignore");
22374
22654
  };
22375
22655
  function readGitignore() {
22376
22656
  const path2 = gitignorePath();
22377
22657
  if (path2 === null) return null;
22378
22658
  try {
22379
- return (0, import_node_fs26.readFileSync)(path2, "utf8");
22659
+ return (0, import_node_fs27.readFileSync)(path2, "utf8");
22380
22660
  } catch {
22381
22661
  return null;
22382
22662
  }
@@ -22385,7 +22665,7 @@ function writeGitignore(content) {
22385
22665
  const path2 = gitignorePath();
22386
22666
  if (path2 === null) return false;
22387
22667
  try {
22388
- (0, import_node_fs26.writeFileSync)(path2, content, "utf8");
22668
+ (0, import_node_fs27.writeFileSync)(path2, content, "utf8");
22389
22669
  return true;
22390
22670
  } catch {
22391
22671
  return false;
@@ -22409,7 +22689,7 @@ async function repoRoot() {
22409
22689
  }
22410
22690
  function hasRepoLocalWorktrees() {
22411
22691
  const root = worktreeRootSync();
22412
- return root !== null && (0, import_node_fs26.existsSync)((0, import_node_path22.join)(root, ".worktrees"));
22692
+ return root !== null && (0, import_node_fs27.existsSync)((0, import_node_path24.join)(root, ".worktrees"));
22413
22693
  }
22414
22694
 
22415
22695
  // src/index.ts
@@ -22582,19 +22862,19 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
22582
22862
  });
22583
22863
  var rules = program2.command("rules").description("org-managed .gitignore delivery");
22584
22864
  rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").option("--json", "machine-readable output").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
22585
- const path2 = (0, import_node_path23.join)(process.cwd(), ".gitignore");
22586
- const current = (0, import_node_fs27.existsSync)(path2) ? (0, import_node_fs27.readFileSync)(path2, "utf8") : null;
22865
+ const path2 = (0, import_node_path25.join)(process.cwd(), ".gitignore");
22866
+ const current = (0, import_node_fs28.existsSync)(path2) ? (0, import_node_fs28.readFileSync)(path2, "utf8") : null;
22587
22867
  const plan = planManagedGitignore(current);
22588
22868
  const drift = [...plan.added.map((l) => `+${l}`), ...plan.removed.map((l) => `-${l}`)].join(", ") || "block normalize";
22589
22869
  if (opts.json) {
22590
- if (opts.write && plan.changed) (0, import_node_fs27.writeFileSync)(path2, plan.content, "utf8");
22870
+ if (opts.write && plan.changed) (0, import_node_fs28.writeFileSync)(path2, plan.content, "utf8");
22591
22871
  console.log(JSON.stringify(plan, null, 2));
22592
22872
  if (!opts.write && plan.changed) process.exitCode = 1;
22593
22873
  return;
22594
22874
  }
22595
22875
  if (opts.write) {
22596
22876
  if (plan.changed) {
22597
- (0, import_node_fs27.writeFileSync)(path2, plan.content, "utf8");
22877
+ (0, import_node_fs28.writeFileSync)(path2, plan.content, "utf8");
22598
22878
  console.log(`mmi-cli org rules gitignore: updated .gitignore (${drift})`);
22599
22879
  } else {
22600
22880
  console.log("mmi-cli org rules gitignore: up to date");
@@ -22771,13 +23051,8 @@ function runWorktreeInstall(command, cwd, quiet) {
22771
23051
  });
22772
23052
  });
22773
23053
  }
22774
- async function primaryCheckoutRoot(worktreeRoot) {
22775
- try {
22776
- const out = (await execFileP2("git", ["-C", worktreeRoot, "rev-parse", "--path-format=absolute", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
22777
- return out ? (0, import_node_path23.dirname)(out) : void 0;
22778
- } catch {
22779
- return void 0;
22780
- }
23054
+ async function primaryCheckoutRoot(from) {
23055
+ return primaryCheckoutRootOf(async (args) => (await execFileP2("git", ["-C", from, ...args], { timeout: GIT_TIMEOUT_MS })).stdout);
22781
23056
  }
22782
23057
  function makeProvisionDeps(worktreeRoot, quiet, log) {
22783
23058
  return {
@@ -22789,26 +23064,26 @@ function makeProvisionDeps(worktreeRoot, quiet, log) {
22789
23064
  function acquireWorktreeSetupLock(worktreeRoot) {
22790
23065
  const lockPath = repoRuntimeStatePath(worktreeRoot, "worktree-setup.lock");
22791
23066
  const take = () => {
22792
- const fd = (0, import_node_fs27.openSync)(lockPath, "wx");
23067
+ const fd = (0, import_node_fs28.openSync)(lockPath, "wx");
22793
23068
  try {
22794
- (0, import_node_fs27.writeSync)(fd, String(Date.now()));
23069
+ (0, import_node_fs28.writeSync)(fd, String(Date.now()));
22795
23070
  } finally {
22796
- (0, import_node_fs27.closeSync)(fd);
23071
+ (0, import_node_fs28.closeSync)(fd);
22797
23072
  }
22798
23073
  return () => {
22799
23074
  try {
22800
- (0, import_node_fs27.rmSync)(lockPath, { force: true });
23075
+ (0, import_node_fs28.rmSync)(lockPath, { force: true });
22801
23076
  } catch {
22802
23077
  }
22803
23078
  };
22804
23079
  };
22805
23080
  try {
22806
- (0, import_node_fs27.mkdirSync)((0, import_node_path23.dirname)(lockPath), { recursive: true });
23081
+ (0, import_node_fs28.mkdirSync)((0, import_node_path25.dirname)(lockPath), { recursive: true });
22807
23082
  return take();
22808
23083
  } catch {
22809
23084
  try {
22810
- if (Date.now() - (0, import_node_fs27.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
22811
- (0, import_node_fs27.rmSync)(lockPath, { force: true });
23085
+ if (Date.now() - (0, import_node_fs28.statSync)(lockPath).mtimeMs > WORKTREE_SETUP_LOCK_TTL_MS) {
23086
+ (0, import_node_fs28.rmSync)(lockPath, { force: true });
22812
23087
  return take();
22813
23088
  }
22814
23089
  } catch {
@@ -22841,7 +23116,7 @@ withExamples(mutating(
22841
23116
  } else if (o.claim || o.for) {
22842
23117
  return fail("worktree create: --claim/--for need an issue-ref argument (e.g. worktree create 2687 --claim)");
22843
23118
  }
22844
- const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
23119
+ const repoRoot2 = await primaryCheckoutRoot(process.cwd()) ?? process.cwd();
22845
23120
  const wtPath = o.path ?? defaultWorktreePath(repoRoot2, branch);
22846
23121
  const { base, fetchBranch } = resolveWorktreeBase(fromRef, o.remote);
22847
23122
  if (fetchBranch) {
@@ -22996,6 +23271,7 @@ registerSecretsCommands(program2);
22996
23271
  registerEdgeCommands(program2);
22997
23272
  registerBoxCommands(program2);
22998
23273
  registerSchedulesCommands(program2);
23274
+ registerSchedulesLiftCommand(program2);
22999
23275
  var docs = program2.command("docs").description("generated docs surfaces \u2014 the routing index (org knowledge layer)");
23000
23276
  docs.command("index").description("regenerate docs/index.md from the docs/ tree (--write, the default) or fail on drift (--check) \u2014 the generated routing index, never hand-maintained").option("--check", "compare against the committed docs/index.md and exit 1 on drift; never write").option("--write", "regenerate docs/index.md when it has drifted (the default)").action(async (o) => {
23001
23277
  try {
@@ -23228,7 +23504,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
23228
23504
  if (dupe) return fail(`org project set: KEY "${dupe}" was passed to both --var and --set; --set is an alias of --var, so pass each KEY once`);
23229
23505
  if (o.secretsFile) {
23230
23506
  try {
23231
- vars.push(`secrets=${(0, import_node_fs27.readFileSync)(o.secretsFile, "utf8")}`);
23507
+ vars.push(`secrets=${(0, import_node_fs28.readFileSync)(o.secretsFile, "utf8")}`);
23232
23508
  } catch (e) {
23233
23509
  return fail(`org project set: cannot read --secrets-file ${o.secretsFile}: ${e.message}`);
23234
23510
  }
@@ -23512,7 +23788,7 @@ function resolveCreateType(raw, command) {
23512
23788
  }
23513
23789
  var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
23514
23790
  withExamples(mutating(
23515
- issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").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 -").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; 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("--north-star <slug>", "link to a North Star plan \u2014 appends a body line + northstar:<slug> label (#1922)").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"),
23791
+ issue.command("create").description("create an issue (type \u2192 label) and print {number,url,label} JSON").option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").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 -").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; 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"),
23516
23792
  // --dry-run/--validate-only plan: validate --type + --priority (mirrors the action — a bad enum fails
23517
23793
  // ERR_BAD_ENUM, a missing priority defaults to medium) then echo the resolved create intent. Refs
23518
23794
  // (`--parent`) and title-source are validated by the action on a real run.
@@ -23527,23 +23803,14 @@ withExamples(mutating(
23527
23803
  let body;
23528
23804
  let title;
23529
23805
  let issueType;
23530
- let northStarSlug;
23531
23806
  let extraLabels = [];
23532
23807
  try {
23533
23808
  issueType = resolveCreateType(o.type, "issue create");
23534
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
23535
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
23536
- if (o.northStar !== void 0) {
23537
- northStarSlug = normalizeNorthStarSlug(o.northStar);
23538
- body = appendNorthStarLine(body, northStarSlug);
23539
- }
23809
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
23810
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
23540
23811
  if (o.idempotencyKey) body = appendIdempotencyMarker(body, o.idempotencyKey);
23541
23812
  priority = resolveCreatePriority(o.priority, "issue create");
23542
23813
  extraLabels = [...o.label ?? []];
23543
- if (northStarSlug) {
23544
- const nsLabel = northStarLabel(northStarSlug);
23545
- if (!extraLabels.includes(nsLabel)) extraLabels.push(nsLabel);
23546
- }
23547
23814
  args = buildIssueArgs({
23548
23815
  type: issueType,
23549
23816
  title,
@@ -23585,7 +23852,6 @@ withExamples(mutating(
23585
23852
  priority,
23586
23853
  projectItemId,
23587
23854
  onBoard,
23588
- ...northStarSlug ? { northStarSlug } : {},
23589
23855
  ...parentLinkFields(parent, parentLinkError)
23590
23856
  }));
23591
23857
  }), [
@@ -23677,7 +23943,7 @@ issue.command("comment <ref>").description("post a Markdown comment to an issue
23677
23943
  }
23678
23944
  let body;
23679
23945
  try {
23680
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
23946
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
23681
23947
  } catch (e) {
23682
23948
  return fail(`issue comment: ${e.message}`);
23683
23949
  }
@@ -23737,8 +24003,8 @@ program2.command("report").description("file a friction report on the Hub board
23737
24003
  let title;
23738
24004
  const sourceRepo = o.repo ?? await resolveRepo(void 0);
23739
24005
  try {
23740
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
23741
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
24006
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
24007
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
23742
24008
  priority = resolveCreatePriority(o.priority, "report");
23743
24009
  if (!ISSUE_TYPES.includes(o.type)) {
23744
24010
  throw new Error(`unknown issue type "${o.type}" \u2014 expected one of: ${ISSUE_TYPES.join(", ")}`);
@@ -23787,8 +24053,8 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
23787
24053
  let args;
23788
24054
  try {
23789
24055
  skill = assertSkillName(o.skill);
23790
- rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
23791
- const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
24056
+ rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
24057
+ const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
23792
24058
  title = buildSkillLessonTitle(skill, rawTitle);
23793
24059
  priority = resolveCreatePriority(o.priority, "skill-lesson");
23794
24060
  body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
@@ -23839,8 +24105,8 @@ withExamples(pr.command("create").description("create a PR and print {number,url
23839
24105
  let body;
23840
24106
  let title;
23841
24107
  try {
23842
- title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises6.readFile, readStdin });
23843
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises6.readFile, readStdin });
24108
+ title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises7.readFile, readStdin });
24109
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises7.readFile, readStdin });
23844
24110
  } catch (e) {
23845
24111
  return fail(`pr create: ${e.message}`, e instanceof TextArgError ? { code: e.code, offending_flag: e.offendingFlag } : void 0);
23846
24112
  }
@@ -23875,11 +24141,11 @@ pr.command("view <number>").description("read a PR as structured JSON (merged st
23875
24141
  }
23876
24142
  });
23877
24143
  async function listCiWorkflowPaths(cwd = process.cwd()) {
23878
- const wfDir = (0, import_node_path23.join)(cwd, ".github", "workflows");
23879
- if (!(0, import_node_fs27.existsSync)(wfDir)) return [];
23880
- return (0, import_node_fs27.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
24144
+ const wfDir = (0, import_node_path25.join)(cwd, ".github", "workflows");
24145
+ if (!(0, import_node_fs28.existsSync)(wfDir)) return [];
24146
+ return (0, import_node_fs28.readdirSync)(wfDir).filter((name) => /\.(ya?ml)$/i.test(name)).filter((name) => {
23881
24147
  try {
23882
- return workflowReportsPrChecks((0, import_node_fs27.readFileSync)((0, import_node_path23.join)(wfDir, name), "utf8"));
24148
+ return workflowReportsPrChecks((0, import_node_fs28.readFileSync)((0, import_node_path25.join)(wfDir, name), "utf8"));
23883
24149
  } catch {
23884
24150
  return true;
23885
24151
  }
@@ -23906,16 +24172,16 @@ function ciAuditDeps() {
23906
24172
  // gate re-seed step is skipped gracefully rather than failing mid-run.
23907
24173
  readSeedFile: (path2) => {
23908
24174
  if (!root) return null;
23909
- const fullPath = (0, import_node_path23.join)(root, path2);
23910
- return (0, import_node_fs27.existsSync)(fullPath) ? (0, import_node_fs27.readFileSync)(fullPath, "utf8") : null;
24175
+ const fullPath = (0, import_node_path25.join)(root, path2);
24176
+ return (0, import_node_fs28.existsSync)(fullPath) ? (0, import_node_fs28.readFileSync)(fullPath, "utf8") : null;
23911
24177
  }
23912
24178
  };
23913
24179
  }
23914
24180
  function hubRoot() {
23915
- const fromPkg = (0, import_node_path23.join)(__dirname, "..", "..");
24181
+ const fromPkg = (0, import_node_path25.join)(__dirname, "..", "..");
23916
24182
  const marker = "skills/bootstrap/seeds/manifest.json";
23917
- if ((0, import_node_fs27.existsSync)((0, import_node_path23.join)(fromPkg, marker))) return fromPkg;
23918
- if ((0, import_node_fs27.existsSync)((0, import_node_path23.join)(process.cwd(), marker))) return process.cwd();
24183
+ if ((0, import_node_fs28.existsSync)((0, import_node_path25.join)(fromPkg, marker))) return fromPkg;
24184
+ if ((0, import_node_fs28.existsSync)((0, import_node_path25.join)(process.cwd(), marker))) return process.cwd();
23919
24185
  return null;
23920
24186
  }
23921
24187
  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) => {
@@ -24133,7 +24399,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
24133
24399
  localCleanup = await cleanupPrMergeLocalBranch(headRef, {
24134
24400
  beforeWorktrees,
24135
24401
  startingPath,
24136
- pathExists: (p) => (0, import_node_fs27.existsSync)(p),
24402
+ pathExists: (p) => (0, import_node_fs28.existsSync)(p),
24137
24403
  execGit: async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout,
24138
24404
  teardownWorktreeStage,
24139
24405
  deferredStore,
@@ -24243,8 +24509,8 @@ function trainApplyDeps() {
24243
24509
  // Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
24244
24510
  announce: (args) => announceRelease({
24245
24511
  run: async (file, cmdArgs) => (await execFileP2(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
24246
- readFile: (path2) => (0, import_promises6.readFile)(path2, "utf8"),
24247
- removeFile: (path2) => (0, import_promises6.unlink)(path2)
24512
+ readFile: (path2) => (0, import_promises7.readFile)(path2, "utf8"),
24513
+ removeFile: (path2) => (0, import_promises7.unlink)(path2)
24248
24514
  }, args),
24249
24515
  fetchEdgeDomains: async (slug) => {
24250
24516
  const proj = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
@@ -24473,10 +24739,10 @@ access.command("audit").description("audit collaborator roles + train-branch pus
24473
24739
  targets = resolution.targets;
24474
24740
  }
24475
24741
  const derivedMatrix = registryProjects ? accessMatrixFromProjects(registryProjects) : {};
24476
- const fileMatrix = (0, import_node_fs27.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs27.readFileSync)("access-matrix.json", "utf8")) : {};
24742
+ const fileMatrix = (0, import_node_fs28.existsSync)("access-matrix.json") ? loadAccessMatrix((0, import_node_fs28.readFileSync)("access-matrix.json", "utf8")) : {};
24477
24743
  const matrix = mergeAccessMatrix(fileMatrix, derivedMatrix);
24478
24744
  const derivedContracts = registryProjects ? dataAccessContractsFromProjects(registryProjects) : { consumers: {} };
24479
- const fileContracts = (0, import_node_fs27.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs27.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
24745
+ const fileContracts = (0, import_node_fs28.existsSync)("data-access-contracts.json") ? loadDataAccessContracts((0, import_node_fs28.readFileSync)("data-access-contracts.json", "utf8")) : { consumers: {} };
24480
24746
  const dataAccess = mergeDataAccessContracts(fileContracts, derivedContracts);
24481
24747
  const report = await auditOrgAccess(targets, deps, matrix, dataAccess);
24482
24748
  console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
@@ -24509,16 +24775,16 @@ function directoryBytes(path2) {
24509
24775
  let total = 0;
24510
24776
  let entries;
24511
24777
  try {
24512
- entries = (0, import_node_fs27.readdirSync)(path2, { withFileTypes: true });
24778
+ entries = (0, import_node_fs28.readdirSync)(path2, { withFileTypes: true });
24513
24779
  } catch {
24514
24780
  return 0;
24515
24781
  }
24516
24782
  for (const entry of entries) {
24517
- const child2 = (0, import_node_path23.join)(path2, entry.name);
24783
+ const child2 = (0, import_node_path25.join)(path2, entry.name);
24518
24784
  if (entry.isDirectory()) total += directoryBytes(child2);
24519
24785
  else {
24520
24786
  try {
24521
- total += (0, import_node_fs27.statSync)(child2).size;
24787
+ total += (0, import_node_fs28.statSync)(child2).size;
24522
24788
  } catch {
24523
24789
  }
24524
24790
  }
@@ -24526,25 +24792,25 @@ function directoryBytes(path2) {
24526
24792
  return total;
24527
24793
  }
24528
24794
  function listDirEntries(dir) {
24529
- return (0, import_node_fs27.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
24795
+ return (0, import_node_fs28.readdirSync)(dir, { withFileTypes: true }).map((d) => ({ name: d.name, isDirectory: d.isDirectory() }));
24530
24796
  }
24531
24797
  function readInstalledPluginRefs(home) {
24532
24798
  const p = installedPluginsPath(home);
24533
- if (!(0, import_node_fs27.existsSync)(p)) return [];
24799
+ if (!(0, import_node_fs28.existsSync)(p)) return [];
24534
24800
  try {
24535
- return installedPluginPaths((0, import_node_fs27.readFileSync)(p, "utf8"));
24801
+ return installedPluginPaths((0, import_node_fs28.readFileSync)(p, "utf8"));
24536
24802
  } catch {
24537
24803
  return null;
24538
24804
  }
24539
24805
  }
24540
24806
  function pluginCacheFsDeps(home, dirBytes) {
24541
24807
  return {
24542
- exists: (p) => (0, import_node_fs27.existsSync)(p),
24543
- listVersionDirs: (root) => (0, import_node_fs27.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
24808
+ exists: (p) => (0, import_node_fs28.existsSync)(p),
24809
+ listVersionDirs: (root) => (0, import_node_fs28.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name),
24544
24810
  dirBytes,
24545
- listStagingDirs: (root) => (0, import_node_fs27.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
24811
+ listStagingDirs: (root) => (0, import_node_fs28.readdirSync)(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
24546
24812
  try {
24547
- return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path23.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs27.statSync)(p).mtimeMs) };
24813
+ return { name: d.name, mtimeMs: newestMtimeMs((0, import_node_path25.join)(root, d.name), listDirEntries, (p) => (0, import_node_fs28.statSync)(p).mtimeMs) };
24548
24814
  } catch {
24549
24815
  return { name: d.name, mtimeMs: Date.now() };
24550
24816
  }
@@ -24558,10 +24824,10 @@ function stagingApplyFsGuard(home) {
24558
24824
  return {
24559
24825
  referencedPaths: () => readInstalledPluginRefs(home),
24560
24826
  mtimeMs: (name) => {
24561
- const p = (0, import_node_path23.join)(stagingRoot, name);
24562
- if (!(0, import_node_fs27.existsSync)(p)) return null;
24827
+ const p = (0, import_node_path25.join)(stagingRoot, name);
24828
+ if (!(0, import_node_fs28.existsSync)(p)) return null;
24563
24829
  try {
24564
- return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs27.statSync)(q).mtimeMs);
24830
+ return newestMtimeMs(p, listDirEntries, (q) => (0, import_node_fs28.statSync)(q).mtimeMs);
24565
24831
  } catch {
24566
24832
  return null;
24567
24833
  }
@@ -24577,7 +24843,7 @@ program2.command("plugin-prune").description(`prune stale cached MMI plugin vers
24577
24843
  { withBytes: true }
24578
24844
  );
24579
24845
  const anythingToDelete = plan.prune.length > 0 || plan.staging.length > 0;
24580
- const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs27.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard((0, import_node_os8.homedir)())) : void 0;
24846
+ const result = o.apply && anythingToDelete ? applyPluginCachePlan(plan, (p) => (0, import_node_fs28.rmSync)(p, { recursive: true, force: true }), stagingApplyFsGuard((0, import_node_os8.homedir)())) : void 0;
24581
24847
  const warnings = plan.prune.length > 0 ? [CONCURRENT_SESSION_WARNING] : [];
24582
24848
  if (o.json) console.log(JSON.stringify({ ...plan, warnings, applied: result ?? null }));
24583
24849
  else console.log(renderPluginCachePlan(plan, result));