@mutmutco/cli 3.105.7 → 3.105.9

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 +384 -263
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -8291,7 +8291,105 @@ function appendHookActivity(cwd, entry) {
8291
8291
 
8292
8292
  // src/worktree.ts
8293
8293
  var import_node_fs12 = require("node:fs");
8294
+ var import_node_path12 = require("node:path");
8295
+
8296
+ // src/file-lock.ts
8297
+ var import_promises2 = require("node:fs/promises");
8294
8298
  var import_node_path11 = require("node:path");
8299
+ var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
8300
+ var IMMEDIATE_RETRY_BUDGET = 3;
8301
+ var FileLockBusyError = class extends Error {
8302
+ lockPath;
8303
+ constructor(label, lockPath, maxWaitMs) {
8304
+ super(`${label} busy: ${lockPath} held longer than ${maxWaitMs}ms`);
8305
+ this.name = "FileLockBusyError";
8306
+ this.lockPath = lockPath;
8307
+ }
8308
+ };
8309
+ function resolveFileLockOpts(opts = {}) {
8310
+ return {
8311
+ staleMs: opts.staleMs ?? 3e4,
8312
+ retryMs: opts.retryMs ?? 50,
8313
+ maxWaitMs: opts.maxWaitMs ?? 5e3,
8314
+ label: opts.label ?? "file lock"
8315
+ };
8316
+ }
8317
+ async function acquireFileLock(lockPath, opts, deadline) {
8318
+ let immediateRetries = 0;
8319
+ for (; ; ) {
8320
+ let handle;
8321
+ try {
8322
+ handle = await (0, import_promises2.open)(lockPath, "wx");
8323
+ } catch (e) {
8324
+ const code = e.code;
8325
+ if (code !== "EEXIST" && code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") throw e;
8326
+ const retryNow = async () => {
8327
+ if (Date.now() >= deadline) throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
8328
+ if (++immediateRetries > IMMEDIATE_RETRY_BUDGET) await sleep(opts.retryMs);
8329
+ };
8330
+ try {
8331
+ const age = Date.now() - (await (0, import_promises2.stat)(lockPath)).mtimeMs;
8332
+ if (age > opts.staleMs) {
8333
+ try {
8334
+ await (0, import_promises2.unlink)(lockPath);
8335
+ await retryNow();
8336
+ continue;
8337
+ } catch (unlinkError) {
8338
+ if (unlinkError instanceof FileLockBusyError) throw unlinkError;
8339
+ const unlinkCode = unlinkError.code;
8340
+ if (unlinkCode === "ENOENT") {
8341
+ await retryNow();
8342
+ continue;
8343
+ }
8344
+ }
8345
+ }
8346
+ } catch (statError) {
8347
+ if (statError instanceof FileLockBusyError) throw statError;
8348
+ if (statError.code !== "ENOENT") throw statError;
8349
+ await retryNow();
8350
+ continue;
8351
+ }
8352
+ if (Date.now() >= deadline) {
8353
+ throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
8354
+ }
8355
+ await sleep(opts.retryMs);
8356
+ continue;
8357
+ }
8358
+ const token = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
8359
+ try {
8360
+ await handle.writeFile(token);
8361
+ } catch (e) {
8362
+ await handle.close().catch(() => void 0);
8363
+ throw e;
8364
+ }
8365
+ return { token, handle };
8366
+ }
8367
+ }
8368
+ async function fileLockHeldBy(lockPath, token) {
8369
+ try {
8370
+ return await (0, import_promises2.readFile)(lockPath, "utf8") === token;
8371
+ } catch {
8372
+ return false;
8373
+ }
8374
+ }
8375
+ async function releaseFileLock(lockPath, guard) {
8376
+ await guard.handle.close().catch(() => void 0);
8377
+ if (await fileLockHeldBy(lockPath, guard.token)) {
8378
+ await (0, import_promises2.unlink)(lockPath).catch(() => void 0);
8379
+ }
8380
+ }
8381
+ async function withFileLock(lockPath, opts, fn) {
8382
+ const resolved = resolveFileLockOpts(opts);
8383
+ await (0, import_promises2.mkdir)((0, import_node_path11.dirname)(lockPath), { recursive: true }).catch(() => void 0);
8384
+ const guard = await acquireFileLock(lockPath, resolved, Date.now() + resolved.maxWaitMs);
8385
+ try {
8386
+ return await fn();
8387
+ } finally {
8388
+ await releaseFileLock(lockPath, guard);
8389
+ }
8390
+ }
8391
+
8392
+ // src/worktree.ts
8295
8393
  var LOCAL_ONLY_FILES = [".claude/settings.local.json"];
8296
8394
  var PKG = "package.json";
8297
8395
  var LOCKFILE = "package-lock.json";
@@ -8337,7 +8435,7 @@ var realFsProbe = {
8337
8435
  }
8338
8436
  };
8339
8437
  function declaredProvision(fs2, abs) {
8340
- const raw = fs2.readFile?.((0, import_node_path11.join)(abs, PKG));
8438
+ const raw = fs2.readFile?.((0, import_node_path12.join)(abs, PKG));
8341
8439
  if (raw === void 0) return void 0;
8342
8440
  try {
8343
8441
  const scripts = JSON.parse(raw).scripts;
@@ -8349,13 +8447,13 @@ function declaredProvision(fs2, abs) {
8349
8447
  }
8350
8448
  function scanInstallDirs(root, fs2 = realFsProbe) {
8351
8449
  const factsFor = (dir) => {
8352
- const abs = dir ? (0, import_node_path11.join)(root, dir) : root;
8353
- const match = LOCKFILE_INSTALLS.find((c) => fs2.isFile((0, import_node_path11.join)(abs, c.lockfile)));
8354
- const hasPackageJson = fs2.isFile((0, import_node_path11.join)(abs, PKG));
8450
+ const abs = dir ? (0, import_node_path12.join)(root, dir) : root;
8451
+ const match = LOCKFILE_INSTALLS.find((c) => fs2.isFile((0, import_node_path12.join)(abs, c.lockfile)));
8452
+ const hasPackageJson = fs2.isFile((0, import_node_path12.join)(abs, PKG));
8355
8453
  return {
8356
8454
  dir,
8357
8455
  hasPackageJson,
8358
- hasNodeModules: fs2.isDir((0, import_node_path11.join)(abs, NODE_MODULES)),
8456
+ hasNodeModules: fs2.isDir((0, import_node_path12.join)(abs, NODE_MODULES)),
8359
8457
  install: match?.command,
8360
8458
  provision: hasPackageJson ? declaredProvision(fs2, abs) : void 0
8361
8459
  };
@@ -8371,7 +8469,7 @@ function npmInstallTargets(dirs) {
8371
8469
  }));
8372
8470
  }
8373
8471
  function isLinkedWorktree(root, fs2 = realFsProbe) {
8374
- return fs2.isFile((0, import_node_path11.join)(root, ".git"));
8472
+ return fs2.isFile((0, import_node_path12.join)(root, ".git"));
8375
8473
  }
8376
8474
  function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
8377
8475
  if (!isLinkedWorktree(root, fs2)) return null;
@@ -8381,7 +8479,7 @@ function worktreeAutoProvisionBanner(root, fs2 = realFsProbe) {
8381
8479
  return `[worktree] provisioning tooling in the background (deps in ${where} + local config) \u2014 \`mmi-cli worktree setup\` to redo`;
8382
8480
  }
8383
8481
  function defaultCopyFile(from, to) {
8384
- (0, import_node_fs12.mkdirSync)((0, import_node_path11.dirname)(to), { recursive: true });
8482
+ (0, import_node_fs12.mkdirSync)((0, import_node_path12.dirname)(to), { recursive: true });
8385
8483
  (0, import_node_fs12.copyFileSync)(from, to);
8386
8484
  }
8387
8485
  async function runDeclaredProvision(target, cwd, runInstall) {
@@ -8412,7 +8510,7 @@ async function provisionWorktree(worktreeRoot, deps) {
8412
8510
  const targets = npmInstallTargets(allDirs);
8413
8511
  if (deps.validateInstall) {
8414
8512
  for (const dir of allDirs.filter((d) => d.hasPackageJson && (d.provision ?? d.install) && d.hasNodeModules)) {
8415
- const cwd = dir.dir ? (0, import_node_path11.join)(worktreeRoot, dir.dir) : worktreeRoot;
8513
+ const cwd = dir.dir ? (0, import_node_path12.join)(worktreeRoot, dir.dir) : worktreeRoot;
8416
8514
  if (!await deps.validateInstall(cwd)) {
8417
8515
  targets.push({
8418
8516
  dir: dir.dir,
@@ -8426,7 +8524,7 @@ async function provisionWorktree(worktreeRoot, deps) {
8426
8524
  const skippedInstall = allDirs.filter((d) => d.hasPackageJson && d.hasNodeModules && !targetDirs.has(d.dir)).map((d) => d.dir);
8427
8525
  const installed = [];
8428
8526
  for (const target of targets) {
8429
- const cwd = target.dir ? (0, import_node_path11.join)(worktreeRoot, target.dir) : worktreeRoot;
8527
+ const cwd = target.dir ? (0, import_node_path12.join)(worktreeRoot, target.dir) : worktreeRoot;
8430
8528
  log(`installing deps: ${target.command} in ${target.dir || "."}`);
8431
8529
  if (target.declared) await runDeclaredProvision(target, cwd, deps.runInstall);
8432
8530
  else await deps.runInstall(target.command, cwd);
@@ -8436,7 +8534,7 @@ async function provisionWorktree(worktreeRoot, deps) {
8436
8534
  const copySkipped = [];
8437
8535
  const primary = await deps.primaryCheckout();
8438
8536
  for (const rel of LOCAL_ONLY_FILES) {
8439
- const dest = (0, import_node_path11.join)(worktreeRoot, rel);
8537
+ const dest = (0, import_node_path12.join)(worktreeRoot, rel);
8440
8538
  if (fs2.isFile(dest)) {
8441
8539
  copySkipped.push({ file: rel, reason: "already-present" });
8442
8540
  continue;
@@ -8445,11 +8543,11 @@ async function provisionWorktree(worktreeRoot, deps) {
8445
8543
  copySkipped.push({ file: rel, reason: "no-primary" });
8446
8544
  continue;
8447
8545
  }
8448
- if (!fs2.isFile((0, import_node_path11.join)(primary, rel))) {
8546
+ if (!fs2.isFile((0, import_node_path12.join)(primary, rel))) {
8449
8547
  copySkipped.push({ file: rel, reason: "absent-in-primary" });
8450
8548
  continue;
8451
8549
  }
8452
- copyFile((0, import_node_path11.join)(primary, rel), dest);
8550
+ copyFile((0, import_node_path12.join)(primary, rel), dest);
8453
8551
  copied.push(rel);
8454
8552
  log(`copied local config: ${rel}`);
8455
8553
  }
@@ -8463,12 +8561,12 @@ function capWorktreeDirName(name, max = 40) {
8463
8561
  }
8464
8562
  function defaultWorktreePath(repoRoot2, branch) {
8465
8563
  const safe = capWorktreeDirName(branch.replace(/[/\\]+/g, "-"));
8466
- return (0, import_node_path11.join)((0, import_node_path11.dirname)(repoRoot2), "mmi-worktrees", (0, import_node_path11.basename)(repoRoot2), safe);
8564
+ return (0, import_node_path12.join)((0, import_node_path12.dirname)(repoRoot2), "mmi-worktrees", (0, import_node_path12.basename)(repoRoot2), safe);
8467
8565
  }
8468
8566
  async function primaryCheckoutRootOf(git2) {
8469
8567
  try {
8470
8568
  const out = (await git2(["rev-parse", "--path-format=absolute", "--git-common-dir"])).trim();
8471
- return out ? (0, import_node_path11.dirname)(out) : void 0;
8569
+ return out ? (0, import_node_path12.dirname)(out) : void 0;
8472
8570
  } catch {
8473
8571
  return void 0;
8474
8572
  }
@@ -8480,10 +8578,26 @@ function resolveWorktreeBase(from, remote) {
8480
8578
  if (SHA_LIKE_RE.test(from)) return { base: from };
8481
8579
  return { base: from, fetchBranch: from, preferRemote: `${remotePrefix}${from}` };
8482
8580
  }
8483
- var GIT_CONFIG_LOCK_RE = /could not lock config file|unable to write upstream branch configuration/i;
8581
+ var GIT_CONFIG_LOCK_RE = /could not lock config file|unable to write upstream branch configuration|unable to access ['"]?\.git\/config['"]?: Permission denied|unknown error occurred while reading the configuration files/i;
8484
8582
  function isGitConfigLockError(error) {
8485
8583
  return GIT_CONFIG_LOCK_RE.test(error instanceof Error ? error.message : String(error));
8486
8584
  }
8585
+ function worktreeAddLockPath(repoRoot2) {
8586
+ return repoRuntimeStatePath(repoRoot2, "worktree-add.lock");
8587
+ }
8588
+ var WORKTREE_ADD_LOCK_STALE_MS = 6e4;
8589
+ var WORKTREE_ADD_LOCK_MAX_WAIT_MS = 12e4;
8590
+ async function withWorktreeAddLock(repoRoot2, fn) {
8591
+ return withFileLock(
8592
+ worktreeAddLockPath(repoRoot2),
8593
+ {
8594
+ staleMs: WORKTREE_ADD_LOCK_STALE_MS,
8595
+ maxWaitMs: WORKTREE_ADD_LOCK_MAX_WAIT_MS,
8596
+ label: "worktree-add lock"
8597
+ },
8598
+ fn
8599
+ );
8600
+ }
8487
8601
  var ADD_WORKTREE_BACKOFF_MS = [100, 250, 500, 750, 1e3];
8488
8602
  var ADD_WORKTREE_MAX_ATTEMPTS = 6;
8489
8603
  async function cleanupOrphanBranch(branch, base, preExistingOid, deps) {
@@ -9251,12 +9365,12 @@ function planManagedGitignore(current) {
9251
9365
 
9252
9366
  // src/docs-index-command.ts
9253
9367
  var import_node_fs14 = require("node:fs");
9254
- var import_node_path13 = require("node:path");
9368
+ var import_node_path14 = require("node:path");
9255
9369
 
9256
9370
  // src/doc-refs-core.ts
9257
9371
  var import_node_child_process5 = require("node:child_process");
9258
9372
  var import_node_fs13 = require("node:fs");
9259
- var import_node_path12 = require("node:path");
9373
+ var import_node_path13 = require("node:path");
9260
9374
  var PIN_RE = /<!--\s*pinned by\s+([^:]+?)\s*:\s*"([^"]+)"[^"]*-->/;
9261
9375
  var PIN_MENTION_RE = /<!--\s*pinned by\b/;
9262
9376
  var FWD_RE = /<!--\s*forward-ref:\s*(\S+?)\s*-->/;
@@ -9315,7 +9429,7 @@ function checkPins(root, readFile9, docs2) {
9315
9429
  findings.push({ kind: "malformed-pin", doc, line: pin.line, detail: pin.text });
9316
9430
  continue;
9317
9431
  }
9318
- const source = readFile9((0, import_node_path12.join)(root, pin.file));
9432
+ const source = readFile9((0, import_node_path13.join)(root, pin.file));
9319
9433
  if (source == null) {
9320
9434
  findings.push({ kind: "missing-test", doc, line: pin.line, detail: pin.file });
9321
9435
  continue;
@@ -9377,11 +9491,11 @@ function checkRefs(root, deps, docs2) {
9377
9491
  for (const { ref } of extractRefs(markdown)) allFirstSegments.add(refFirstSegment(ref));
9378
9492
  }
9379
9493
  const tracked = allFirstSegments.size ? deps.trackedFirstSegments?.([...allFirstSegments]) ?? null : null;
9380
- const firstVerifiable = (first) => tracked ? tracked.has(first) : exists((0, import_node_path12.join)(root, first));
9494
+ const firstVerifiable = (first) => tracked ? tracked.has(first) : exists((0, import_node_path13.join)(root, first));
9381
9495
  const candidates = [];
9382
9496
  const direct = [];
9383
9497
  for (const [doc, markdown] of Object.entries(docs2)) {
9384
- const docDir = import_node_path12.posix.dirname(doc);
9498
+ const docDir = import_node_path13.posix.dirname(doc);
9385
9499
  const base = docDir === "." ? "" : docDir;
9386
9500
  const covered = /* @__PURE__ */ new Set();
9387
9501
  const markers = [];
@@ -9390,21 +9504,21 @@ function checkRefs(root, deps, docs2) {
9390
9504
  direct.push({ kind: "malformed-forward-ref", doc, line: fwd.line, detail: fwd.text });
9391
9505
  continue;
9392
9506
  }
9393
- const docRel = import_node_path12.posix.normalize(import_node_path12.posix.join(base, fwd.target));
9394
- const rootRel = import_node_path12.posix.normalize(fwd.target.replace(/^\/+/, ""));
9507
+ const docRel = import_node_path13.posix.normalize(import_node_path13.posix.join(base, fwd.target));
9508
+ const rootRel = import_node_path13.posix.normalize(fwd.target.replace(/^\/+/, ""));
9395
9509
  markers.push({ target: fwd.target, line: fwd.line, docRel, rootRel });
9396
9510
  covered.add(docRel);
9397
9511
  covered.add(rootRel);
9398
9512
  }
9399
9513
  const links = extractLinks(markdown).map(({ target, line }) => {
9400
- const resolved = import_node_path12.posix.normalize(import_node_path12.posix.join(base, target));
9401
- return { target, line, resolved, missing: !exists((0, import_node_path12.join)(root, resolved)) };
9514
+ const resolved = import_node_path13.posix.normalize(import_node_path13.posix.join(base, target));
9515
+ return { target, line, resolved, missing: !exists((0, import_node_path13.join)(root, resolved)) };
9402
9516
  });
9403
9517
  for (const marker of markers) {
9404
9518
  const coversMissing = links.some(
9405
9519
  (l) => l.missing && (l.resolved === marker.docRel || l.resolved === marker.rootRel)
9406
9520
  );
9407
- if (!coversMissing && (exists((0, import_node_path12.join)(root, marker.docRel)) || exists((0, import_node_path12.join)(root, marker.rootRel)))) {
9521
+ if (!coversMissing && (exists((0, import_node_path13.join)(root, marker.docRel)) || exists((0, import_node_path13.join)(root, marker.rootRel)))) {
9408
9522
  direct.push({
9409
9523
  kind: "stale-forward-ref",
9410
9524
  doc,
@@ -9415,7 +9529,7 @@ function checkRefs(root, deps, docs2) {
9415
9529
  }
9416
9530
  for (const { ref, line } of extractRefs(markdown)) {
9417
9531
  if (!firstVerifiable(refFirstSegment(ref))) continue;
9418
- if (!exists((0, import_node_path12.join)(root, ref))) candidates.push({ kind: "missing-path", doc, line, detail: ref });
9532
+ if (!exists((0, import_node_path13.join)(root, ref))) candidates.push({ kind: "missing-path", doc, line, detail: ref });
9419
9533
  }
9420
9534
  for (const { target, line, resolved, missing } of links) {
9421
9535
  if (resolved.startsWith("..")) {
@@ -9467,18 +9581,18 @@ function readFileOrNull(path2) {
9467
9581
  }
9468
9582
  function walk(dir, root, out) {
9469
9583
  for (const entry of (0, import_node_fs13.readdirSync)(dir)) {
9470
- const full = (0, import_node_path12.join)(dir, entry);
9584
+ const full = (0, import_node_path13.join)(dir, entry);
9471
9585
  if ((0, import_node_fs13.statSync)(full).isDirectory()) walk(full, root, out);
9472
9586
  else if (entry.endsWith(".md")) out.push(full.slice(root.length + 1).replaceAll("\\", "/"));
9473
9587
  }
9474
9588
  return out;
9475
9589
  }
9476
9590
  function defaultListDocs(root) {
9477
- const docsDir = (0, import_node_path12.join)(root, "docs");
9591
+ const docsDir = (0, import_node_path13.join)(root, "docs");
9478
9592
  const docs2 = ((0, import_node_fs13.existsSync)(docsDir) ? walk(docsDir, root, []) : []).filter(
9479
9593
  (rel) => !SKIP_WALK.some((skip) => rel.startsWith(skip))
9480
9594
  );
9481
- return [...ROOT_DOCS.filter((rel) => (0, import_node_fs13.existsSync)((0, import_node_path12.join)(root, rel))), ...docs2];
9595
+ return [...ROOT_DOCS.filter((rel) => (0, import_node_fs13.existsSync)((0, import_node_path13.join)(root, rel))), ...docs2];
9482
9596
  }
9483
9597
  var CHECK_IGNORE_MAX_BUFFER = 32 * 1024 * 1024;
9484
9598
  function defaultIsIgnored(root, relPaths, exec = import_node_child_process5.execFileSync) {
@@ -9538,7 +9652,7 @@ function runDocRefs(root, deps = {}) {
9538
9652
  const walked = listDocs(root);
9539
9653
  const ignoredDocs = walked.length ? isIgnored(walked) : /* @__PURE__ */ new Set();
9540
9654
  const docs2 = Object.fromEntries(
9541
- walked.filter((rel) => !ignoredDocs.has(rel)).map((rel) => [rel, readFile9((0, import_node_path12.join)(root, rel))]).filter(([, body]) => body != null)
9655
+ walked.filter((rel) => !ignoredDocs.has(rel)).map((rel) => [rel, readFile9((0, import_node_path13.join)(root, rel))]).filter(([, body]) => body != null)
9542
9656
  );
9543
9657
  const refResult = checkRefs(root, { exists, isIgnored, trackedFirstSegments }, docs2);
9544
9658
  const findings = [
@@ -9646,19 +9760,19 @@ function walkMarkdown(dir) {
9646
9760
  while (stack.length) {
9647
9761
  const current = stack.pop();
9648
9762
  for (const entry of (0, import_node_fs14.readdirSync)(current, { withFileTypes: true })) {
9649
- const full = (0, import_node_path13.join)(current, entry.name);
9763
+ const full = (0, import_node_path14.join)(current, entry.name);
9650
9764
  if (entry.isDirectory()) {
9651
9765
  stack.push(full);
9652
9766
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
9653
- out.push((0, import_node_path13.relative)(dir, full).split(import_node_path13.sep).join("/"));
9767
+ out.push((0, import_node_path14.relative)(dir, full).split(import_node_path14.sep).join("/"));
9654
9768
  }
9655
9769
  }
9656
9770
  }
9657
9771
  return out;
9658
9772
  }
9659
9773
  function createDocsIndexDeps(repoRoot2) {
9660
- const docsDir = (0, import_node_path13.join)(repoRoot2, "docs");
9661
- const indexPath = (0, import_node_path13.join)(repoRoot2, DOCS_INDEX_PATH);
9774
+ const docsDir = (0, import_node_path14.join)(repoRoot2, "docs");
9775
+ const indexPath = (0, import_node_path14.join)(repoRoot2, DOCS_INDEX_PATH);
9662
9776
  return {
9663
9777
  listDocs: () => {
9664
9778
  if (!(0, import_node_fs14.existsSync)(docsDir)) return [];
@@ -9667,7 +9781,7 @@ function createDocsIndexDeps(repoRoot2) {
9667
9781
  const ignored = defaultIsIgnored(repoRoot2, walked.map((rel) => `docs/${rel}`));
9668
9782
  return walked.filter((rel) => !ignored.has(`docs/${rel}`)).sort();
9669
9783
  },
9670
- readDoc: (relPath) => (0, import_node_fs14.readFileSync)((0, import_node_path13.join)(docsDir, relPath), "utf8"),
9784
+ readDoc: (relPath) => (0, import_node_fs14.readFileSync)((0, import_node_path14.join)(docsDir, relPath), "utf8"),
9671
9785
  readIndex: () => (0, import_node_fs14.existsSync)(indexPath) ? (0, import_node_fs14.readFileSync)(indexPath, "utf8") : null,
9672
9786
  writeIndex: (content) => (0, import_node_fs14.writeFileSync)(indexPath, content, "utf8")
9673
9787
  };
@@ -10317,13 +10431,13 @@ function parseVerifyBroker(stdout) {
10317
10431
 
10318
10432
  // src/train-apply.ts
10319
10433
  var import_node_fs16 = require("node:fs");
10320
- var import_promises2 = require("node:fs/promises");
10321
- var import_node_path15 = require("node:path");
10434
+ var import_promises3 = require("node:fs/promises");
10435
+ var import_node_path16 = require("node:path");
10322
10436
 
10323
10437
  // src/plugin-guard-io.ts
10324
10438
  var import_node_fs15 = require("node:fs");
10325
10439
  var import_node_child_process6 = require("node:child_process");
10326
- var import_node_path14 = require("node:path");
10440
+ var import_node_path15 = require("node:path");
10327
10441
  var import_node_os4 = require("node:os");
10328
10442
  var import_proper_lockfile = __toESM(require_proper_lockfile(), 1);
10329
10443
 
@@ -10614,15 +10728,15 @@ function runHostBin(bin, args, opts) {
10614
10728
  return step ? execFileHard(file, argv, { ...shared, step }) : execFileP2(file, argv, shared);
10615
10729
  }
10616
10730
  function surfaceConfigRoot(surface, env = process.env, home = (0, import_node_os4.homedir)()) {
10617
- if (surface === "codex") return env.CODEX_HOME?.trim() || (0, import_node_path14.join)(home, ".codex");
10618
- if (surface === "kimi") return env.KIMI_CODE_HOME?.trim() || (0, import_node_path14.join)(home, ".kimi-code");
10619
- if (surface === "kilo") return env.KILO_CONFIG_DIR?.trim() || (0, import_node_path14.join)(home, ".config", "kilo");
10620
- if (surface === "cursor") return (0, import_node_path14.join)(home, ".cursor");
10621
- if (surface === "jervcode") return env.PI_CODING_AGENT_DIR?.trim() || (0, import_node_path14.join)(home, ".pi", "agent");
10622
- return (0, import_node_path14.join)(home, ".claude");
10731
+ if (surface === "codex") return env.CODEX_HOME?.trim() || (0, import_node_path15.join)(home, ".codex");
10732
+ if (surface === "kimi") return env.KIMI_CODE_HOME?.trim() || (0, import_node_path15.join)(home, ".kimi-code");
10733
+ if (surface === "kilo") return env.KILO_CONFIG_DIR?.trim() || (0, import_node_path15.join)(home, ".config", "kilo");
10734
+ if (surface === "cursor") return (0, import_node_path15.join)(home, ".cursor");
10735
+ if (surface === "jervcode") return env.PI_CODING_AGENT_DIR?.trim() || (0, import_node_path15.join)(home, ".pi", "agent");
10736
+ return (0, import_node_path15.join)(home, ".claude");
10623
10737
  }
10624
10738
  var installedPluginsPath = (surface = detectSurface(process.env)) => {
10625
- return (0, import_node_path14.join)(surfaceConfigRoot(surface), "plugins", "installed_plugins.json");
10739
+ return (0, import_node_path15.join)(surfaceConfigRoot(surface), "plugins", "installed_plugins.json");
10626
10740
  };
10627
10741
  function readInstalledPlugins(surface = detectSurface(process.env)) {
10628
10742
  try {
@@ -10635,15 +10749,15 @@ function marketplaceCloneCandidates(surface, home, env = process.env) {
10635
10749
  if (surface === "codex") {
10636
10750
  const root = surfaceConfigRoot(surface, env, home);
10637
10751
  return [
10638
- (0, import_node_path14.join)(root, ".tmp", "marketplaces", CODEX_MARKETPLACE),
10639
- (0, import_node_path14.join)(root, "plugins", "marketplaces", CODEX_MARKETPLACE)
10752
+ (0, import_node_path15.join)(root, ".tmp", "marketplaces", CODEX_MARKETPLACE),
10753
+ (0, import_node_path15.join)(root, "plugins", "marketplaces", CODEX_MARKETPLACE)
10640
10754
  ];
10641
10755
  }
10642
10756
  if (surface === "kimi") return [];
10643
10757
  if (surface === "kilo") return [];
10644
10758
  if (surface === "cursor") return [];
10645
10759
  if (surface === "jervcode") return [];
10646
- return [(0, import_node_path14.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
10760
+ return [(0, import_node_path15.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
10647
10761
  }
10648
10762
  function marketplaceClonePresent(surface, home, exists = import_node_fs15.existsSync, env = process.env) {
10649
10763
  return marketplaceCloneCandidates(surface, home, env).some(exists);
@@ -10694,11 +10808,11 @@ function codexHookTrustState(status = codexPluginStatus()) {
10694
10808
  return { applicable: false, trusted: false, trustedCount: 0, requiredCount: 0 };
10695
10809
  }
10696
10810
  const root = surfaceConfigRoot("codex");
10697
- const hooksPath = (0, import_node_path14.join)(root, "plugins", "cache", CODEX_MARKETPLACE, "mmi", status.version, "hooks", "codex-hooks.json");
10811
+ const hooksPath = (0, import_node_path15.join)(root, "plugins", "cache", CODEX_MARKETPLACE, "mmi", status.version, "hooks", "codex-hooks.json");
10698
10812
  const requiredCount = countCodexHookCommands(hooksPath);
10699
10813
  let config = "";
10700
10814
  try {
10701
- config = (0, import_node_fs15.readFileSync)((0, import_node_path14.join)(root, "config.toml"), "utf8");
10815
+ config = (0, import_node_fs15.readFileSync)((0, import_node_path15.join)(root, "config.toml"), "utf8");
10702
10816
  } catch {
10703
10817
  return { applicable: true, trusted: false, trustedCount: 0, requiredCount };
10704
10818
  }
@@ -10734,9 +10848,9 @@ async function npmSelfUpdateCli(target, onStep) {
10734
10848
  }
10735
10849
  function kiloConfigListsPlugin(configRoot, home = (0, import_node_os4.homedir)(), read = (p) => (0, import_node_fs15.readFileSync)(p, "utf8"), exists = import_node_fs15.existsSync) {
10736
10850
  const candidates = ["kilo.json", "kilo.jsonc", "opencode.json", "opencode.jsonc", "config.json"];
10737
- for (const dir of [configRoot, (0, import_node_path14.join)(home, ".kilo")]) {
10851
+ for (const dir of [configRoot, (0, import_node_path15.join)(home, ".kilo")]) {
10738
10852
  for (const file of candidates) {
10739
- const path2 = (0, import_node_path14.join)(dir, file);
10853
+ const path2 = (0, import_node_path15.join)(dir, file);
10740
10854
  if (!exists(path2)) continue;
10741
10855
  try {
10742
10856
  const stripped = read(path2).replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
@@ -10753,7 +10867,7 @@ function kiloConfigListsPlugin(configRoot, home = (0, import_node_os4.homedir)()
10753
10867
  return false;
10754
10868
  }
10755
10869
  function cursorLocalPluginRoot(env = process.env, home = (0, import_node_os4.homedir)()) {
10756
- return (0, import_node_path14.join)(surfaceConfigRoot("cursor", env, home), "plugins", "local", "mmi");
10870
+ return (0, import_node_path15.join)(surfaceConfigRoot("cursor", env, home), "plugins", "local", "mmi");
10757
10871
  }
10758
10872
  function cursorPluginTreeHealthy(root, exists = import_node_fs15.existsSync) {
10759
10873
  return [
@@ -10762,14 +10876,14 @@ function cursorPluginTreeHealthy(root, exists = import_node_fs15.existsSync) {
10762
10876
  "hooks/cursor-hooks.json",
10763
10877
  "scripts/hook-run.mjs",
10764
10878
  "scripts/hook-policy.mjs"
10765
- ].every((path2) => exists((0, import_node_path14.join)(root, ...path2.split("/"))));
10879
+ ].every((path2) => exists((0, import_node_path15.join)(root, ...path2.split("/"))));
10766
10880
  }
10767
10881
  function kimiPluginTreeHealthy(root, exists = import_node_fs15.existsSync) {
10768
10882
  return [
10769
10883
  ".kimi-plugin/plugin.json",
10770
10884
  "skills/mmi/SKILL.md",
10771
10885
  "scripts/hook-run.mjs"
10772
- ].every((path2) => exists((0, import_node_path14.join)(root, ...path2.split("/"))));
10886
+ ].every((path2) => exists((0, import_node_path15.join)(root, ...path2.split("/"))));
10773
10887
  }
10774
10888
  var JERVCODE_WRAPPER_DIR = ".pi-plugin";
10775
10889
  function normalizePiEntry(value) {
@@ -10792,7 +10906,7 @@ function jervcodePackageFamily(entry) {
10792
10906
  function isMmiOwnedPiEntry(entry) {
10793
10907
  if (typeof entry !== "string") return false;
10794
10908
  try {
10795
- const pkg = JSON.parse((0, import_node_fs15.readFileSync)((0, import_node_path14.join)(piEntryFsPath(entry), "package.json"), "utf8"));
10909
+ const pkg = JSON.parse((0, import_node_fs15.readFileSync)((0, import_node_path15.join)(piEntryFsPath(entry), "package.json"), "utf8"));
10796
10910
  return pkg.name === "mmi";
10797
10911
  } catch {
10798
10912
  return false;
@@ -10824,7 +10938,7 @@ function readPiSettings(path2) {
10824
10938
  }
10825
10939
  }
10826
10940
  function mmiPiWrapperEntry(env = process.env, home = (0, import_node_os4.homedir)()) {
10827
- const settings = readPiSettings((0, import_node_path14.join)(surfaceConfigRoot("jervcode", env, home), "settings.json"));
10941
+ const settings = readPiSettings((0, import_node_path15.join)(surfaceConfigRoot("jervcode", env, home), "settings.json"));
10828
10942
  const entries = Array.isArray(settings?.packages) ? settings.packages : [];
10829
10943
  for (const entry of entries) {
10830
10944
  if (typeof entry !== "string") continue;
@@ -10837,17 +10951,17 @@ function mmiPiWrapperEntry(env = process.env, home = (0, import_node_os4.homedir
10837
10951
  function mmiPiWrapperHealthy(entry) {
10838
10952
  if (!entry) return false;
10839
10953
  const wrapper = piEntryFsPath(entry);
10840
- return isMmiOwnedPiEntry(entry) && (0, import_node_fs15.existsSync)((0, import_node_path14.join)((0, import_node_path14.dirname)(wrapper), "skills", "mmi", "SKILL.md"));
10954
+ return isMmiOwnedPiEntry(entry) && (0, import_node_fs15.existsSync)((0, import_node_path15.join)((0, import_node_path15.dirname)(wrapper), "skills", "mmi", "SKILL.md"));
10841
10955
  }
10842
10956
  function findMmiPiSourceClone(home = (0, import_node_os4.homedir)()) {
10843
- const cacheRoot = (0, import_node_path14.join)(home, ".claude", "plugins", "cache", "mutmutco", "mmi");
10957
+ const cacheRoot = (0, import_node_path15.join)(home, ".claude", "plugins", "cache", "mutmutco", "mmi");
10844
10958
  let best = null;
10845
10959
  try {
10846
10960
  for (const entry of (0, import_node_fs15.readdirSync)(cacheRoot, { withFileTypes: true })) {
10847
10961
  if (!entry.isDirectory() || !/^\d+\.\d+\.\d+$/.test(entry.name)) continue;
10848
- if (!(0, import_node_fs15.existsSync)((0, import_node_path14.join)(cacheRoot, entry.name, JERVCODE_WRAPPER_DIR, "package.json"))) continue;
10962
+ if (!(0, import_node_fs15.existsSync)((0, import_node_path15.join)(cacheRoot, entry.name, JERVCODE_WRAPPER_DIR, "package.json"))) continue;
10849
10963
  if (!best || compareVersions(entry.name, best.version) > 0) {
10850
- best = { path: (0, import_node_path14.join)(cacheRoot, entry.name), version: entry.name };
10964
+ best = { path: (0, import_node_path15.join)(cacheRoot, entry.name), version: entry.name };
10851
10965
  }
10852
10966
  }
10853
10967
  } catch {
@@ -10885,7 +10999,7 @@ function healJervCodePackageRegistration(opts = {}) {
10885
10999
  return { available: true, ok: true, changed: false, version: null, detail: "skipped \u2014 no installed MMI clone carries the pi wrapper (install the Claude plugin first)" };
10886
11000
  }
10887
11001
  const packagePath = `${clone.path.replace(/\\/g, "/").replace(/\/+$/, "")}/${JERVCODE_WRAPPER_DIR}`;
10888
- const settingsPath2 = (0, import_node_path14.join)(agentDir, "settings.json");
11002
+ const settingsPath2 = (0, import_node_path15.join)(agentDir, "settings.json");
10889
11003
  const release = (0, import_node_fs15.existsSync)(settingsPath2) ? acquirePiSettingsLock(settingsPath2) : void 0;
10890
11004
  if ((0, import_node_fs15.existsSync)(settingsPath2) && !release) {
10891
11005
  return {
@@ -10915,7 +11029,7 @@ function healJervCodePackageRegistration(opts = {}) {
10915
11029
  }
10916
11030
  current.packages = merged.next;
10917
11031
  try {
10918
- (0, import_node_fs15.mkdirSync)((0, import_node_path14.dirname)(settingsPath2), { recursive: true });
11032
+ (0, import_node_fs15.mkdirSync)((0, import_node_path15.dirname)(settingsPath2), { recursive: true });
10919
11033
  const tmp = `${settingsPath2}.tmp-${process.pid}`;
10920
11034
  (0, import_node_fs15.writeFileSync)(tmp, `${JSON.stringify(current, null, 2)}
10921
11035
  `, "utf8");
@@ -10948,7 +11062,7 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
10948
11062
  return {
10949
11063
  isOrgRepo,
10950
11064
  installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()) || // Kimi's managed plugin directory is its native install record; it has no Claude-style ledger.
10951
- surface === "kimi" && (0, import_node_fs15.existsSync)((0, import_node_path14.join)(root, "plugins", "managed", "mmi")) || // kilo-p1: the install record is the config file itself.
11065
+ surface === "kimi" && (0, import_node_fs15.existsSync)((0, import_node_path15.join)(root, "plugins", "managed", "mmi")) || // kilo-p1: the install record is the config file itself.
10952
11066
  surface === "kilo" && kiloConfigListsPlugin(root) || // #4188: jervcode's install record is the settings-file packages[] entry itself.
10953
11067
  surface === "jervcode" && piEntry !== null || surface === "cursor" && (0, import_node_fs15.existsSync)(cursorLocalPluginRoot()),
10954
11068
  // Kilo has no marketplace to clone — the config file IS the install record, so this dimension of
@@ -10957,9 +11071,9 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
10957
11071
  // Kimi keeps no plugin cache dir — installs are copied to plugins/managed/<id> and run from there.
10958
11072
  // Kilo (kilo-p1) keeps no cache dir either: the plugin's server() provisions ~/.kilo behind the
10959
11073
  // version stamp, so the stamp's presence is the cache signal.
10960
- pluginCachePresent: surface === "jervcode" ? mmiPiWrapperHealthy(piEntry) : surface === "kilo" ? (0, import_node_fs15.existsSync)((0, import_node_path14.join)((0, import_node_os4.homedir)(), ".kilo", ".mmi-kilo-version")) : surface === "kimi" ? kimiPluginTreeHealthy((0, import_node_path14.join)(root, "plugins", "managed", "mmi")) : surface === "cursor" ? cursorPluginTreeHealthy(cursorLocalPluginRoot()) : surface === "codex" ? Boolean(
10961
- codexStatus?.installed && codexStatus.enabled && codexStatus.version && (0, import_node_fs15.existsSync)((0, import_node_path14.join)(root, "plugins", "cache", CODEX_MARKETPLACE, "mmi", codexStatus.version))
10962
- ) : (0, import_node_fs15.existsSync)((0, import_node_path14.join)(root, "plugins", "cache", "mutmutco", "mmi"))
11074
+ pluginCachePresent: surface === "jervcode" ? mmiPiWrapperHealthy(piEntry) : surface === "kilo" ? (0, import_node_fs15.existsSync)((0, import_node_path15.join)((0, import_node_os4.homedir)(), ".kilo", ".mmi-kilo-version")) : surface === "kimi" ? kimiPluginTreeHealthy((0, import_node_path15.join)(root, "plugins", "managed", "mmi")) : surface === "cursor" ? cursorPluginTreeHealthy(cursorLocalPluginRoot()) : surface === "codex" ? Boolean(
11075
+ codexStatus?.installed && codexStatus.enabled && codexStatus.version && (0, import_node_fs15.existsSync)((0, import_node_path15.join)(root, "plugins", "cache", CODEX_MARKETPLACE, "mmi", codexStatus.version))
11076
+ ) : (0, import_node_fs15.existsSync)((0, import_node_path15.join)(root, "plugins", "cache", "mutmutco", "mmi"))
10963
11077
  };
10964
11078
  }
10965
11079
  async function runHostBinLogged(bin, args, opts) {
@@ -10979,9 +11093,9 @@ async function runPluginCli(bin, args, log) {
10979
11093
  function captureCodexHookLauncher() {
10980
11094
  const status = codexPluginStatus();
10981
11095
  if (!status.installed || !status.enabled || !status.version) return void 0;
10982
- const root = (0, import_node_path14.join)(surfaceConfigRoot("codex"), "plugins", "cache", CODEX_MARKETPLACE, "mmi", status.version);
11096
+ const root = (0, import_node_path15.join)(surfaceConfigRoot("codex"), "plugins", "cache", CODEX_MARKETPLACE, "mmi", status.version);
10983
11097
  const files = ["mmi-hook", "mmi-hook.exe"].flatMap((name) => {
10984
- const path2 = (0, import_node_path14.join)(root, "bin", name);
11098
+ const path2 = (0, import_node_path15.join)(root, "bin", name);
10985
11099
  try {
10986
11100
  return [{ name, content: (0, import_node_fs15.readFileSync)(path2) }];
10987
11101
  } catch {
@@ -10991,11 +11105,11 @@ function captureCodexHookLauncher() {
10991
11105
  return files.length === 2 ? { root, files } : void 0;
10992
11106
  }
10993
11107
  function restoreCodexHookLauncher(snapshot) {
10994
- if (!snapshot || (0, import_node_fs15.existsSync)((0, import_node_path14.join)(snapshot.root, "scripts", "hook-run.mjs"))) return false;
10995
- const bin = (0, import_node_path14.join)(snapshot.root, "bin");
11108
+ if (!snapshot || (0, import_node_fs15.existsSync)((0, import_node_path15.join)(snapshot.root, "scripts", "hook-run.mjs"))) return false;
11109
+ const bin = (0, import_node_path15.join)(snapshot.root, "bin");
10996
11110
  (0, import_node_fs15.mkdirSync)(bin, { recursive: true });
10997
11111
  for (const file of snapshot.files) {
10998
- const path2 = (0, import_node_path14.join)(bin, file.name);
11112
+ const path2 = (0, import_node_path15.join)(bin, file.name);
10999
11113
  (0, import_node_fs15.writeFileSync)(path2, file.content);
11000
11114
  if (file.name === "mmi-hook") (0, import_node_fs15.chmodSync)(path2, 493);
11001
11115
  }
@@ -11006,8 +11120,8 @@ function canonicalCursorRemote(remote) {
11006
11120
  }
11007
11121
  async function installCursorPluginCheckout(env = process.env) {
11008
11122
  const configRoot = surfaceConfigRoot("cursor", env);
11009
- const pluginsRoot = (0, import_node_path14.join)(configRoot, "plugins");
11010
- const target = (0, import_node_path14.join)(pluginsRoot, "local", "mmi");
11123
+ const pluginsRoot = (0, import_node_path15.join)(configRoot, "plugins");
11124
+ const target = (0, import_node_path15.join)(pluginsRoot, "local", "mmi");
11011
11125
  const source = env.MMI_CURSOR_PLUGIN_SOURCE?.trim();
11012
11126
  if ((0, import_node_fs15.existsSync)(target) && !source) {
11013
11127
  try {
@@ -11019,12 +11133,12 @@ async function installCursorPluginCheckout(env = process.env) {
11019
11133
  return { ok: false, detail: `refused to replace unmanaged Cursor plugin directory at ${target}` };
11020
11134
  }
11021
11135
  }
11022
- (0, import_node_fs15.mkdirSync)((0, import_node_path14.join)(pluginsRoot, "local"), { recursive: true });
11023
- (0, import_node_fs15.mkdirSync)((0, import_node_path14.join)(pluginsRoot, "staging"), { recursive: true });
11024
- (0, import_node_fs15.mkdirSync)((0, import_node_path14.join)(pluginsRoot, "quarantine"), { recursive: true });
11136
+ (0, import_node_fs15.mkdirSync)((0, import_node_path15.join)(pluginsRoot, "local"), { recursive: true });
11137
+ (0, import_node_fs15.mkdirSync)((0, import_node_path15.join)(pluginsRoot, "staging"), { recursive: true });
11138
+ (0, import_node_fs15.mkdirSync)((0, import_node_path15.join)(pluginsRoot, "quarantine"), { recursive: true });
11025
11139
  const suffix = `${Date.now()}-${process.pid}`;
11026
- const staged = (0, import_node_path14.join)(pluginsRoot, "staging", `mmi-${suffix}`);
11027
- const quarantined = (0, import_node_path14.join)(pluginsRoot, "quarantine", `mmi-${suffix}`);
11140
+ const staged = (0, import_node_path15.join)(pluginsRoot, "staging", `mmi-${suffix}`);
11141
+ const quarantined = (0, import_node_path15.join)(pluginsRoot, "quarantine", `mmi-${suffix}`);
11028
11142
  try {
11029
11143
  if (source) {
11030
11144
  (0, import_node_fs15.cpSync)(source, staged, {
@@ -11107,7 +11221,7 @@ async function applyPluginHeal(surface, log, opts) {
11107
11221
  const refSupported = await marketplaceAddRefSupported(bin);
11108
11222
  const { steps } = adaptHealStepsForRefSupport(tableSteps, refSupported);
11109
11223
  log(healBannerLine(bin, token, refSupported));
11110
- const pinsPath = (0, import_node_path14.join)((0, import_node_os4.homedir)(), ...KNOWN_MARKETPLACES_RELATIVE);
11224
+ const pinsPath = (0, import_node_path15.join)((0, import_node_os4.homedir)(), ...KNOWN_MARKETPLACES_RELATIVE);
11111
11225
  const pins = token === "claude" ? captureMarketplacePins(readKnownMarketplacesFile(pinsPath), [MMI_MARKETPLACE_NAME, JERV_MARKETPLACE_NAME]) : /* @__PURE__ */ new Map();
11112
11226
  try {
11113
11227
  for (const step of steps) {
@@ -11224,7 +11338,7 @@ function restoreMarketplacePinsOnDisk(path2, pins, hostIsRunning = claudeCodeIsR
11224
11338
  return writeMarketplacePinsOnDisk(
11225
11339
  path2,
11226
11340
  pins,
11227
- (restored) => hostIsRunning() ? `${restoredPinsLine(restored)}, but Claude Code is running and can rewrite this file from its own copy \u2014 restart it, then \`mmi-cli doctor --apply\` if the pins did not survive` : restoredPinsLine(restored),
11341
+ (restored) => hostIsRunning() ? `${restoredPinsLine(restored)}, but Claude Code is running and can rewrite this file from its own copy \u2014 restart it, then \`mmi-cli doctor\` if the pins did not survive` : restoredPinsLine(restored),
11228
11342
  "restore"
11229
11343
  );
11230
11344
  }
@@ -11238,13 +11352,13 @@ function applyOrgMarketplacePins(path2, names, hostIsRunning = claudeCodeIsRunni
11238
11352
  return `pinned ${[...pins.keys()].join(", ")} to ${ORG_MARKETPLACE_PINS.ref} with auto-update on`;
11239
11353
  },
11240
11354
  "pin",
11241
- () => hostIsRunning() ? "not pinned \u2014 Claude Code is running and rewrites this registration from its own copy; quit it, then run `mmi-cli doctor --apply`" : void 0
11355
+ () => hostIsRunning() ? "not pinned \u2014 Claude Code is running and rewrites this registration from its own copy; quit it, then run `mmi-cli doctor`" : void 0
11242
11356
  );
11243
11357
  return detail === void 0 ? void 0 : { detail, wrote: landed };
11244
11358
  }
11245
11359
  function writeMarketplacePinPending(path2, names, now = Date.now()) {
11246
11360
  try {
11247
- (0, import_node_fs15.mkdirSync)((0, import_node_path14.dirname)(path2), { recursive: true });
11361
+ (0, import_node_fs15.mkdirSync)((0, import_node_path15.dirname)(path2), { recursive: true });
11248
11362
  (0, import_node_fs15.writeFileSync)(path2, `${JSON.stringify({ v: 1, names: [...names], at: new Date(now).toISOString() })}
11249
11363
  `, "utf8");
11250
11364
  } catch {
@@ -11388,18 +11502,46 @@ async function resolveFoldPaths(deps, model) {
11388
11502
  }
11389
11503
  return ["package.json", "package-lock.json"];
11390
11504
  }
11505
+ async function alignExistingHotfixReleaseMarker(deps, version) {
11506
+ const script = [
11507
+ "const fs = require('fs');",
11508
+ "const p = 'package.json';",
11509
+ "if (!fs.existsSync(p)) { process.stdout.write('absent'); process.exit(0); }",
11510
+ "const j = JSON.parse(fs.readFileSync(p, 'utf8'));",
11511
+ "const marker = j.jervaiseRelease;",
11512
+ "const hasHotfixMarker = !!(marker && typeof marker === 'object' && marker.kind === 'hotfix');",
11513
+ "const requiresMarker = fs.existsSync('scripts/check-release-kind.mjs');",
11514
+ "if (!hasHotfixMarker && !requiresMarker) {",
11515
+ " process.stdout.write('skip');",
11516
+ " process.exit(0);",
11517
+ "}",
11518
+ `const version = ${JSON.stringify(version)};`,
11519
+ "if (hasHotfixMarker && marker.version === version) { process.stdout.write('ok'); process.exit(0); }",
11520
+ "j.jervaiseRelease = { kind: 'hotfix', version };",
11521
+ "fs.writeFileSync(p, JSON.stringify(j, null, 2) + '\\n');",
11522
+ "process.stdout.write(hasHotfixMarker ? 'updated' : 'stamped');"
11523
+ ].join("");
11524
+ const out = (await deps.run("node", ["-e", script])).trim();
11525
+ if (out === "updated") return `jervaiseRelease hotfix marker aligned to ${version}`;
11526
+ if (out === "stamped") return `jervaiseRelease hotfix marker stamped for ${version}`;
11527
+ return null;
11528
+ }
11391
11529
  async function installAppFoldDeps(deps) {
11392
11530
  const hasLock = await deps.run("git", ["cat-file", "-e", "HEAD:package-lock.json"]).then(() => true).catch(() => false);
11393
11531
  await deps.run("npm", hasLock ? ["ci"] : ["install"]);
11394
11532
  }
11395
- async function foldReleaseVersion(deps, model, tag, foldPaths) {
11533
+ async function foldReleaseVersion(deps, model, tag, foldPaths, sourceCommit = "HEAD") {
11396
11534
  if (foldPaths.length === 0) return "no version manifest to fold \u2014 the tag is the version";
11397
11535
  const version = tag.replace(/^v/, "");
11398
11536
  if (model === "hub-serverless") {
11399
- await deps.run("node", ["scripts/release-distribution.mjs", "prepare", version, "--source-commit", "HEAD"]);
11537
+ await deps.run("node", ["scripts/release-distribution.mjs", "prepare", version, "--source-commit", sourceCommit]);
11400
11538
  } else {
11401
11539
  await installAppFoldDeps(deps);
11402
11540
  await deps.run("npm", ["version", version, "--no-git-tag-version", "--allow-same-version"]);
11541
+ const patch = Number(version.split(".")[2] ?? "0");
11542
+ if (model === "registry-publish" && Number.isFinite(patch) && patch > 0) {
11543
+ await alignExistingHotfixReleaseMarker(deps, version);
11544
+ }
11403
11545
  }
11404
11546
  for (const path2 of foldPaths) {
11405
11547
  await deps.run("git", ["add", "--", path2]).catch(() => void 0);
@@ -12356,7 +12498,7 @@ async function dispatchDeploy(deps, ctx, stage, ref, model, watch, autoRunSince,
12356
12498
  return { note: `no manual dispatch: ${model} repo deploys via its own push-triggered workflow`, deployStatus: "pending" };
12357
12499
  }
12358
12500
  function readLocalGateWorkflows() {
12359
- const dir = (0, import_node_path15.join)(".github", "workflows");
12501
+ const dir = (0, import_node_path16.join)(".github", "workflows");
12360
12502
  let names;
12361
12503
  try {
12362
12504
  names = (0, import_node_fs16.readdirSync)(dir);
@@ -12366,7 +12508,7 @@ function readLocalGateWorkflows() {
12366
12508
  const files = [];
12367
12509
  for (const name of names.filter(isGateWorkflowPath)) {
12368
12510
  try {
12369
- files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs16.readFileSync)((0, import_node_path15.join)(dir, name), "utf8") });
12511
+ files.push({ path: `${dir}/${name}`.replace(/\\/g, "/"), body: (0, import_node_fs16.readFileSync)((0, import_node_path16.join)(dir, name), "utf8") });
12370
12512
  } catch {
12371
12513
  }
12372
12514
  }
@@ -12491,7 +12633,7 @@ async function waitForFoldIndexLock(deps, startBranch, resumeCommand) {
12491
12633
  const deadline = now() + GIT_INDEX_LOCK_WAIT_MS;
12492
12634
  for (; ; ) {
12493
12635
  try {
12494
- await (0, import_promises2.stat)(lockPath);
12636
+ await (0, import_promises3.stat)(lockPath);
12495
12637
  } catch (e) {
12496
12638
  if (e.code === "ENOENT") return;
12497
12639
  throw foldFailureGuidance(
@@ -14473,7 +14615,7 @@ var import_node_fs18 = require("node:fs");
14473
14615
  // src/stage-runner.ts
14474
14616
  var import_node_child_process7 = require("node:child_process");
14475
14617
  var import_node_fs17 = require("node:fs");
14476
- var import_node_path16 = require("node:path");
14618
+ var import_node_path17 = require("node:path");
14477
14619
  var import_node_net = require("node:net");
14478
14620
  var import_node_util5 = require("node:util");
14479
14621
 
@@ -14612,11 +14754,11 @@ function appendForceRecreate(up) {
14612
14754
  return `${up.trimEnd()} --force-recreate`;
14613
14755
  }
14614
14756
  function stageStatePath(cwd = process.cwd()) {
14615
- return (0, import_node_path16.join)(cwd, "tmp", "stage", "state.json");
14757
+ return (0, import_node_path17.join)(cwd, "tmp", "stage", "state.json");
14616
14758
  }
14617
14759
  function stageGlobalStatePath(cwd = process.cwd(), gitCommonDir = ".git") {
14618
- const dir = (0, import_node_path16.isAbsolute)(gitCommonDir) ? gitCommonDir : (0, import_node_path16.resolve)(cwd, gitCommonDir);
14619
- return (0, import_node_path16.join)(dir, "mmi", "stage", "state.json");
14760
+ const dir = (0, import_node_path17.isAbsolute)(gitCommonDir) ? gitCommonDir : (0, import_node_path17.resolve)(cwd, gitCommonDir);
14761
+ return (0, import_node_path17.join)(dir, "mmi", "stage", "state.json");
14620
14762
  }
14621
14763
  function normPath2(path2) {
14622
14764
  return path2.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
@@ -14840,8 +14982,8 @@ function stageProcessEnv(stagePort, extraEnv) {
14840
14982
  }
14841
14983
  async function ensureStageRuntimeEnv(config, opts, cwd) {
14842
14984
  if (!config.ensureEnv) return;
14843
- const target = (0, import_node_path16.join)(cwd, config.ensureEnv.target);
14844
- const example = (0, import_node_path16.join)(cwd, config.ensureEnv.example);
14985
+ const target = (0, import_node_path17.join)(cwd, config.ensureEnv.target);
14986
+ const example = (0, import_node_path17.join)(cwd, config.ensureEnv.example);
14845
14987
  if (!(0, import_node_fs17.existsSync)(target) && (0, import_node_fs17.existsSync)(example)) {
14846
14988
  (0, import_node_fs17.copyFileSync)(example, target);
14847
14989
  } else if ((0, import_node_fs17.existsSync)(target) && (0, import_node_fs17.existsSync)(example)) {
@@ -15212,7 +15354,7 @@ var import_node_os17 = require("node:os");
15212
15354
  var import_node_child_process9 = require("node:child_process");
15213
15355
  var import_node_fs21 = require("node:fs");
15214
15356
  var import_node_os7 = require("node:os");
15215
- var import_node_path19 = require("node:path");
15357
+ var import_node_path20 = require("node:path");
15216
15358
  var import_node_util6 = require("node:util");
15217
15359
 
15218
15360
  // src/board-priority.ts
@@ -15567,7 +15709,7 @@ function boardConfigFromProject(meta, floor = {}) {
15567
15709
 
15568
15710
  // src/cli-doctor-shared.ts
15569
15711
  var import_node_fs19 = require("node:fs");
15570
- var import_node_path18 = require("node:path");
15712
+ var import_node_path19 = require("node:path");
15571
15713
  var import_node_fs20 = require("node:fs");
15572
15714
 
15573
15715
  // src/readiness-audit.ts
@@ -15688,9 +15830,9 @@ var import_node_child_process8 = require("node:child_process");
15688
15830
  var import_node_os6 = require("node:os");
15689
15831
 
15690
15832
  // src/gh-create.ts
15691
- var import_promises3 = require("node:fs/promises");
15833
+ var import_promises4 = require("node:fs/promises");
15692
15834
  var import_node_os5 = require("node:os");
15693
- var import_node_path17 = require("node:path");
15835
+ var import_node_path18 = require("node:path");
15694
15836
  var import_node_crypto3 = require("node:crypto");
15695
15837
  var ISSUE_TYPES = ["bug", "feature", "task"];
15696
15838
  var GH_MUTATION_TIMEOUT_MS = 12e4;
@@ -15740,12 +15882,12 @@ async function bodyArgsViaFile(args, deps = {}) {
15740
15882
  const i = args.indexOf("--body");
15741
15883
  if (i === -1 || i + 1 >= args.length) return { args, cleanup: async () => {
15742
15884
  } };
15743
- const write = deps.write ?? import_promises3.writeFile;
15744
- const remove2 = deps.remove ?? import_promises3.unlink;
15745
- const ensureDir = deps.ensureDir ?? import_promises3.mkdir;
15885
+ const write = deps.write ?? import_promises4.writeFile;
15886
+ const remove2 = deps.remove ?? import_promises4.unlink;
15887
+ const ensureDir = deps.ensureDir ?? import_promises4.mkdir;
15746
15888
  const dir = deps.dir ?? (0, import_node_os5.tmpdir)();
15747
- const file = (0, import_node_path17.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
15748
- await ensureDir((0, import_node_path17.dirname)(file), { recursive: true }).catch(() => {
15889
+ const file = (0, import_node_path18.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
15890
+ await ensureDir((0, import_node_path18.dirname)(file), { recursive: true }).catch(() => {
15749
15891
  });
15750
15892
  await write(file, args[i + 1], "utf8");
15751
15893
  return {
@@ -17269,7 +17411,7 @@ async function localBranchHeads() {
17269
17411
  }
17270
17412
  async function currentRepoWorktreeGitRoot(repoRoot2) {
17271
17413
  const gitCommonDir = (await execFileP2("git", ["rev-parse", "--git-common-dir"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
17272
- return gitCommonDir ? (0, import_node_path18.resolve)(repoRoot2, gitCommonDir, "worktrees") : "";
17414
+ return gitCommonDir ? (0, import_node_path19.resolve)(repoRoot2, gitCommonDir, "worktrees") : "";
17273
17415
  }
17274
17416
  async function worktreeBranches() {
17275
17417
  const { stdout } = await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
@@ -17289,7 +17431,7 @@ function resolveGitdirForWorktreeFile(worktreePath, content) {
17289
17431
  const match = /^gitdir:\s*(.+)\s*$/im.exec(content);
17290
17432
  if (!match?.[1]) return void 0;
17291
17433
  const raw = match[1].trim();
17292
- return (0, import_node_path18.isAbsolute)(raw) ? raw : (0, import_node_path18.resolve)(worktreePath, raw);
17434
+ return (0, import_node_path19.isAbsolute)(raw) ? raw : (0, import_node_path19.resolve)(worktreePath, raw);
17293
17435
  }
17294
17436
  function metadataOwnsMissingWorktreeDir(worktreePath, worktreeGitRoot) {
17295
17437
  if (!worktreeGitRoot) return false;
@@ -17298,9 +17440,9 @@ function metadataOwnsMissingWorktreeDir(worktreePath, worktreeGitRoot) {
17298
17440
  for (const ent of entries) {
17299
17441
  if (!ent.isDirectory()) continue;
17300
17442
  try {
17301
- const gitdirPath = (0, import_node_fs19.readFileSync)((0, import_node_path18.join)(worktreeGitRoot, ent.name, "gitdir"), "utf8").trim();
17302
- const resolvedGitdir = (0, import_node_path18.isAbsolute)(gitdirPath) ? gitdirPath : (0, import_node_path18.resolve)(worktreeGitRoot, ent.name, gitdirPath);
17303
- if (sameWorktreeMetadataPath((0, import_node_path18.dirname)(resolvedGitdir), worktreePath)) return true;
17443
+ const gitdirPath = (0, import_node_fs19.readFileSync)((0, import_node_path19.join)(worktreeGitRoot, ent.name, "gitdir"), "utf8").trim();
17444
+ const resolvedGitdir = (0, import_node_path19.isAbsolute)(gitdirPath) ? gitdirPath : (0, import_node_path19.resolve)(worktreeGitRoot, ent.name, gitdirPath);
17445
+ if (sameWorktreeMetadataPath((0, import_node_path19.dirname)(resolvedGitdir), worktreePath)) return true;
17304
17446
  } catch {
17305
17447
  }
17306
17448
  }
@@ -17319,7 +17461,7 @@ function pathExistsKnown(path2) {
17319
17461
  }
17320
17462
  }
17321
17463
  function inspectSiblingWorktreeDir(path2, worktreeGitRoot) {
17322
- const gitPath = (0, import_node_path18.join)(path2, ".git");
17464
+ const gitPath = (0, import_node_path19.join)(path2, ".git");
17323
17465
  let st;
17324
17466
  try {
17325
17467
  st = (0, import_node_fs20.lstatSync)(gitPath);
@@ -17373,7 +17515,7 @@ async function preservedBranches() {
17373
17515
  async function siblingWorktreeDirs(explicitRoot) {
17374
17516
  const repoRoot2 = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
17375
17517
  const worktreeGitRoot = await currentRepoWorktreeGitRoot(repoRoot2);
17376
- const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path18.dirname)((0, import_node_path18.dirname)(worktreeGitRoot)) : repoRoot2;
17518
+ const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path19.dirname)((0, import_node_path19.dirname)(worktreeGitRoot)) : repoRoot2;
17377
17519
  try {
17378
17520
  const dirs = explicitRoot ? listDirsIn(resolveExplicitScanRoot(explicitRoot, primaryRepoRoot)) : worktreeScanDirs(siblingMmiWorktreesRoot(primaryRepoRoot), primaryRepoRoot, listDirsIn, isRepoCheckoutDir);
17379
17521
  return dirs.map((dir) => inspectSiblingWorktreeDir(dir, worktreeGitRoot)).filter((entry) => Boolean(entry));
@@ -17383,13 +17525,13 @@ async function siblingWorktreeDirs(explicitRoot) {
17383
17525
  }
17384
17526
  function listDirsIn(dir) {
17385
17527
  try {
17386
- return (0, import_node_fs20.readdirSync)(dir, { withFileTypes: true }).filter((ent) => ent.isDirectory()).map((ent) => (0, import_node_path18.join)(dir, ent.name));
17528
+ return (0, import_node_fs20.readdirSync)(dir, { withFileTypes: true }).filter((ent) => ent.isDirectory()).map((ent) => (0, import_node_path19.join)(dir, ent.name));
17387
17529
  } catch {
17388
17530
  return [];
17389
17531
  }
17390
17532
  }
17391
17533
  function isRepoCheckoutDir(dir) {
17392
- return (0, import_node_fs20.existsSync)((0, import_node_path18.join)(dir, ".git"));
17534
+ return (0, import_node_fs20.existsSync)((0, import_node_path19.join)(dir, ".git"));
17393
17535
  }
17394
17536
  function resolveExplicitScanRoot(explicitRoot, repoRoot2) {
17395
17537
  let rootDirs;
@@ -18751,14 +18893,14 @@ function probeLocalClaimSession(marker, now = Date.now()) {
18751
18893
  claimSessionProbeCache.set(cacheKey, { checkedAt: now, state });
18752
18894
  return state;
18753
18895
  };
18754
- const root = (0, import_node_path19.join)((0, import_node_os7.homedir)(), ".claude", "projects");
18896
+ const root = (0, import_node_path20.join)((0, import_node_os7.homedir)(), ".claude", "projects");
18755
18897
  try {
18756
18898
  const wanted = `${marker.session}.jsonl`.toLowerCase();
18757
18899
  const pending = [root];
18758
18900
  while (pending.length) {
18759
18901
  const dir = pending.pop();
18760
18902
  for (const entry of (0, import_node_fs21.readdirSync)(dir, { withFileTypes: true })) {
18761
- const path2 = (0, import_node_path19.join)(dir, entry.name);
18903
+ const path2 = (0, import_node_path20.join)(dir, entry.name);
18762
18904
  if (entry.isDirectory()) pending.push(path2);
18763
18905
  else if (entry.isFile() && entry.name.toLowerCase() === wanted) {
18764
18906
  return remember(now - (0, import_node_fs21.statSync)(path2).mtimeMs <= CLAIM_SESSION_ACTIVITY_MS ? "live" : "dead");
@@ -19578,7 +19720,7 @@ function consolidateCommandNamespaces(program3) {
19578
19720
 
19579
19721
  // src/pi-plugin-registration.ts
19580
19722
  var import_node_fs22 = require("node:fs");
19581
- var import_node_path20 = require("node:path");
19723
+ var import_node_path21 = require("node:path");
19582
19724
 
19583
19725
  // src/plugin-cache-prune.ts
19584
19726
  var PLUGIN_CACHE_KEEP = 2;
@@ -19837,26 +19979,26 @@ function newestExistingPiPlugin(home) {
19837
19979
  return void 0;
19838
19980
  }
19839
19981
  for (const version of names.filter(isVersionDirName).sort((a, b) => compareVersions(b, a))) {
19840
- const candidate = (0, import_node_path20.join)(cacheRoot, version, ".pi-plugin");
19982
+ const candidate = (0, import_node_path21.join)(cacheRoot, version, ".pi-plugin");
19841
19983
  if ((0, import_node_fs22.existsSync)(candidate)) return candidate;
19842
19984
  }
19843
19985
  return void 0;
19844
19986
  }
19845
19987
  function expectedPiPluginPath(home, env, installedVersion) {
19846
19988
  const root = env.CLAUDE_PLUGIN_ROOT?.trim();
19847
- if (root && /[\\/]mutmutco[\\/]mmi[\\/]/.test(root)) return (0, import_node_path20.join)(root, ".pi-plugin");
19989
+ if (root && /[\\/]mutmutco[\\/]mmi[\\/]/.test(root)) return (0, import_node_path21.join)(root, ".pi-plugin");
19848
19990
  const version = installedVersion ?? runningPluginVersion(env);
19849
19991
  if (version) {
19850
- const pinned = (0, import_node_path20.join)(home, ".claude", "plugins", "cache", "mutmutco", "mmi", version, ".pi-plugin");
19992
+ const pinned = (0, import_node_path21.join)(home, ".claude", "plugins", "cache", "mutmutco", "mmi", version, ".pi-plugin");
19851
19993
  if ((0, import_node_fs22.existsSync)(pinned)) return pinned;
19852
19994
  }
19853
19995
  return newestExistingPiPlugin(home);
19854
19996
  }
19855
19997
  function settingsPath(home) {
19856
- return (0, import_node_path20.join)(home, ".pi", "agent", "settings.json");
19998
+ return (0, import_node_path21.join)(home, ".pi", "agent", "settings.json");
19857
19999
  }
19858
20000
  function readPiPluginState(home, env, installedVersion) {
19859
- if (!(0, import_node_fs22.existsSync)((0, import_node_path20.join)(home, ".pi", "agent"))) return void 0;
20001
+ if (!(0, import_node_fs22.existsSync)((0, import_node_path21.join)(home, ".pi", "agent"))) return void 0;
19860
20002
  const expectedPath = expectedPiPluginPath(home, env, installedVersion);
19861
20003
  if (!expectedPath) return void 0;
19862
20004
  const file = settingsPath(home);
@@ -20516,7 +20658,14 @@ async function runHotfixStart(deps, options) {
20516
20658
  notes.push(`deleted stale origin/${branch} left by the incomplete train`);
20517
20659
  }
20518
20660
  }
20519
- const { sha, label } = await resolveHotfixSource(deps, ctx, options.from);
20661
+ const specs = splitCarrySpecs([options.from]);
20662
+ if (specs.length === 0) throw new Error("hotfix start: --from named no PR or SHA");
20663
+ const sources = [];
20664
+ for (const spec of specs) {
20665
+ const resolved = await resolveHotfixSource(deps, ctx, spec);
20666
+ if (!sources.some((s) => s.sha === resolved.sha)) sources.push(resolved);
20667
+ }
20668
+ const label = sources.map((s) => s.label).join(", ");
20520
20669
  const foldPaths = deployModel === "hub-serverless" || deployModel === "registry-publish" ? await resolveFoldPaths(deps, deployModel) : [];
20521
20670
  const pickTolerated = deployModel === "hub-serverless" ? [...foldPaths, ...HOTFIX_SKILL_TOLERATED_ROOTS] : foldPaths;
20522
20671
  if (deployModel === "hub-serverless") {
@@ -20532,9 +20681,11 @@ async function runHotfixStart(deps, options) {
20532
20681
  const preexistingLocal = clean3(await deps.run("git", ["branch", "--list", branch]));
20533
20682
  await deps.run("git", ["checkout", "-B", branch, "origin/main"]);
20534
20683
  try {
20535
- const autoResolved = await cherryPickWithToleratedPaths(deps, sha, pickTolerated);
20536
- if (autoResolved.length > 0) {
20537
- notes.push(`auto-resolved regenerable cherry-pick conflict(s): ${autoResolved.join(", ")} (regenerated in bump step)`);
20684
+ for (const source of sources) {
20685
+ const autoResolved = await cherryPickWithToleratedPaths(deps, source.sha, pickTolerated);
20686
+ if (autoResolved.length > 0) {
20687
+ notes.push(`auto-resolved regenerable cherry-pick conflict(s) for ${source.label}: ${autoResolved.join(", ")} (regenerated in bump step)`);
20688
+ }
20538
20689
  }
20539
20690
  } catch (e) {
20540
20691
  const recovery = await restoreAfterFailedPort(deps, { startBranch, branch, created: !preexistingLocal });
@@ -20575,10 +20726,12 @@ async function runHotfixStart(deps, options) {
20575
20726
  branch,
20576
20727
  "--title",
20577
20728
  `[hotfix] ${tag}`,
20729
+ // Every picked sha goes in the marker (#4411) — `hotfix release` reads it when --carries is omitted,
20730
+ // so a multi-fix cycle proves ALL of its targets present before tagging, not just the first.
20578
20731
  "--body",
20579
20732
  `Hotfix ${tag}: cherry-pick of ${label} onto origin/main${bumpNote}.
20580
20733
 
20581
- <!-- mmi-hotfix-carries: ${sha} -->
20734
+ <!-- mmi-hotfix-carries: ${sources.map((s) => s.sha).join(",")} -->
20582
20735
 
20583
20736
  Merge this PR (human-initiated), then run \`mmi-cli hotfix release ${tag}\`.`
20584
20737
  ]));
@@ -20621,6 +20774,61 @@ async function watchReleaseRun(deps, ctx, workflow, sha) {
20621
20774
  }
20622
20775
  return { workflow, conclusion: "not-found" };
20623
20776
  }
20777
+ function devFoldBranch(tag) {
20778
+ return `hotfix-fold/${tag}`;
20779
+ }
20780
+ async function portFoldToDevelopment(deps, ctx, deployModel, tag) {
20781
+ const foldPaths = deployModel === "hub-serverless" || deployModel === "registry-publish" ? await resolveFoldPaths(deps, deployModel) : [];
20782
+ if (foldPaths.length === 0) return `fold port skipped (deployModel=${deployModel} folds no version manifest)`;
20783
+ const branch = devFoldBranch(tag);
20784
+ const listed = await deps.run("gh", [
20785
+ "pr",
20786
+ "list",
20787
+ "--repo",
20788
+ ctx.repo,
20789
+ "--head",
20790
+ branch,
20791
+ "--base",
20792
+ "development",
20793
+ "--state",
20794
+ "all",
20795
+ "--limit",
20796
+ "10",
20797
+ "--json",
20798
+ "number,state,url"
20799
+ ]);
20800
+ const existing = JSON.parse(listed || "[]").filter((r) => r.state === "OPEN" || r.state === "MERGED").sort((a, b) => (b.number ?? 0) - (a.number ?? 0))[0];
20801
+ if (existing) return `development fold PR #${existing.number} for ${tag} is ${existing.state} \u2014 reused`;
20802
+ const previousRef = clean3(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
20803
+ try {
20804
+ await deps.run("git", ["fetch", "origin", "development"]);
20805
+ await deps.run("git", ["checkout", "-B", branch, "origin/development"]);
20806
+ const durableSource = clean3(await deps.run("git", ["merge-base", "HEAD", "origin/development"]));
20807
+ const foldNote = await foldReleaseVersion(deps, deployModel, tag, foldPaths, durableSource);
20808
+ const committed = clean3(await deps.run("git", ["rev-list", "--count", "origin/development..HEAD"]));
20809
+ if (committed === "0") return `development already carries ${tag} \u2014 no fold PR needed (${foldNote})`;
20810
+ await deps.run("git", ["push", "-u", "origin", branch]);
20811
+ const prUrl = clean3(await deps.run("gh", [
20812
+ "pr",
20813
+ "create",
20814
+ "--repo",
20815
+ ctx.repo,
20816
+ "--base",
20817
+ "development",
20818
+ "--head",
20819
+ branch,
20820
+ "--title",
20821
+ `chore(release): port the ${tag} version fold to development`,
20822
+ "--body",
20823
+ `Regenerated version fold for ${tag} (#4410). \`main\` carries the released version; without this development declares the previous one and every PR into it fails catalog-lockstep.
20824
+
20825
+ Generated by \`mmi-cli hotfix release\` \u2014 no main-parented commit is merged in, so the squash parents stay clean (#4365/#4371).`
20826
+ ]));
20827
+ return `opened development fold PR ${prUrl} (${foldNote})`;
20828
+ } finally {
20829
+ if (previousRef && previousRef !== "HEAD") await deps.run("git", ["checkout", previousRef]).catch(() => void 0);
20830
+ }
20831
+ }
20624
20832
  async function runHotfixRelease(deps, versionInput, options = {}) {
20625
20833
  const ctx = await buildTrainApplyContext(deps);
20626
20834
  const deployModel = await resolveHotfixDeployModel(deps, ctx);
@@ -20746,6 +20954,12 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
20746
20954
  } else {
20747
20955
  verifyNote = `distribution verify skipped (deployModel=${deployModel}, Hub-only step)`;
20748
20956
  }
20957
+ let foldNote;
20958
+ try {
20959
+ foldNote = await portFoldToDevelopment(deps, ctx, deployModel, tag);
20960
+ } catch (e) {
20961
+ foldNote = `development fold port FAILED: ${e.message ?? e} \u2014 the release stands; port it by hand: git checkout -B ${devFoldBranch(tag)} origin/development && node scripts/release-distribution.mjs prepare ${version}, then open a development-base PR`;
20962
+ }
20749
20963
  return {
20750
20964
  ...ctx,
20751
20965
  command: "hotfix-release",
@@ -20758,6 +20972,7 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
20758
20972
  runs,
20759
20973
  deployNote,
20760
20974
  verifyNote,
20975
+ foldNote,
20761
20976
  announceNote
20762
20977
  };
20763
20978
  }
@@ -21408,7 +21623,7 @@ function renderAccessReport(report) {
21408
21623
  var import_node_crypto4 = require("node:crypto");
21409
21624
  var import_node_child_process12 = require("node:child_process");
21410
21625
  var import_node_fs23 = require("node:fs");
21411
- var import_node_path21 = require("node:path");
21626
+ var import_node_path22 = require("node:path");
21412
21627
  var REPO_INDEX_SCHEMA = 1;
21413
21628
  var HARD_DENY = [
21414
21629
  /(^|\/)\.env(\.|$)/i,
@@ -21533,7 +21748,7 @@ function loadReadmeHints(cwd, candidatePaths) {
21533
21748
  }
21534
21749
  for (const rel of readmes) {
21535
21750
  if (isHardDeniedPath(rel)) continue;
21536
- const abs = (0, import_node_path21.join)(cwd, ...rel.split("/"));
21751
+ const abs = (0, import_node_path22.join)(cwd, ...rel.split("/"));
21537
21752
  if (!(0, import_node_fs23.existsSync)(abs)) continue;
21538
21753
  let text;
21539
21754
  try {
@@ -21550,7 +21765,7 @@ function loadReadmeHints(cwd, candidatePaths) {
21550
21765
  return hints;
21551
21766
  }
21552
21767
  function toPosix(p) {
21553
- return p.split(import_node_path21.sep).join("/");
21768
+ return p.split(import_node_path22.sep).join("/");
21554
21769
  }
21555
21770
  function listCandidatePaths(cwd, exec = import_node_child_process12.execFileSync) {
21556
21771
  try {
@@ -21572,7 +21787,7 @@ function rebuildRepoIndex(cwd, repoSlug2) {
21572
21787
  for (const rel of candidates) {
21573
21788
  if (ignored.has(rel)) continue;
21574
21789
  if (isHardDeniedPath(rel)) continue;
21575
- const abs = (0, import_node_path21.join)(cwd, ...rel.split("/"));
21790
+ const abs = (0, import_node_path22.join)(cwd, ...rel.split("/"));
21576
21791
  if (!(0, import_node_fs23.existsSync)(abs)) continue;
21577
21792
  let text;
21578
21793
  try {
@@ -21601,7 +21816,7 @@ function rebuildRepoIndex(cwd, repoSlug2) {
21601
21816
  entries
21602
21817
  };
21603
21818
  const store = repoIndexStorePath(cwd);
21604
- (0, import_node_fs23.mkdirSync)((0, import_node_path21.dirname)(store), { recursive: true });
21819
+ (0, import_node_fs23.mkdirSync)((0, import_node_path22.dirname)(store), { recursive: true });
21605
21820
  (0, import_node_fs23.writeFileSync)(store, `${JSON.stringify(projection, null, 2)}
21606
21821
  `, "utf8");
21607
21822
  return projection;
@@ -21682,7 +21897,7 @@ function inferRepoSlug(cwd, exec = import_node_child_process12.execFileSync) {
21682
21897
  if (m?.[1]) return m[1].toLowerCase();
21683
21898
  } catch {
21684
21899
  }
21685
- return ((0, import_node_path21.basename)(cwd) || "local").toLowerCase();
21900
+ return ((0, import_node_path22.basename)(cwd) || "local").toLowerCase();
21686
21901
  }
21687
21902
 
21688
21903
  // src/repo-index-cloud-client.ts
@@ -21824,7 +22039,7 @@ async function gcRepoIndexCloud(deps) {
21824
22039
  // src/repo-index-sync.ts
21825
22040
  var import_node_fs24 = require("node:fs");
21826
22041
  var import_node_os9 = require("node:os");
21827
- var import_node_path22 = require("node:path");
22042
+ var import_node_path23 = require("node:path");
21828
22043
  var import_node_child_process13 = require("node:child_process");
21829
22044
  var MAX_EMBED_BACKFILL_ROUNDS = 40;
21830
22045
  function normalizeRepo(raw) {
@@ -21868,7 +22083,7 @@ async function syncEstateRepoIndex(opts) {
21868
22083
  const failed = [];
21869
22084
  const skipped = [];
21870
22085
  for (const repo of repos) {
21871
- const dir = (0, import_node_fs24.mkdtempSync)((0, import_node_path22.join)((0, import_node_os9.tmpdir)(), "mmi-repo-index-"));
22086
+ const dir = (0, import_node_fs24.mkdtempSync)((0, import_node_path23.join)((0, import_node_os9.tmpdir)(), "mmi-repo-index-"));
21872
22087
  try {
21873
22088
  shallowClone(repo, dir, opts.githubToken);
21874
22089
  const built = rebuildRepoIndex(dir, repo);
@@ -22114,7 +22329,7 @@ async function runRepoIndexHealth(opts) {
22114
22329
  // src/spawn-policy-core.ts
22115
22330
  var import_node_child_process14 = require("node:child_process");
22116
22331
  var import_node_fs26 = require("node:fs");
22117
- var import_node_path23 = require("node:path");
22332
+ var import_node_path24 = require("node:path");
22118
22333
  var SPAWNERS = ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync"];
22119
22334
  var CALL_SOURCE = String.raw`(^|[^.\w$])(${SPAWNERS.join("|")})\s*\(`;
22120
22335
  var SOURCE_EXT = /\.(ts|mts|cts|js|mjs|cjs)$/;
@@ -22200,7 +22415,7 @@ function runSpawnPolicy(root) {
22200
22415
  for (const file of files) {
22201
22416
  let raw;
22202
22417
  try {
22203
- raw = (0, import_node_fs26.readFileSync)((0, import_node_path23.join)(root, file), "utf8");
22418
+ raw = (0, import_node_fs26.readFileSync)((0, import_node_path24.join)(root, file), "utf8");
22204
22419
  } catch {
22205
22420
  continue;
22206
22421
  }
@@ -22219,7 +22434,7 @@ function runSpawnPolicy(root) {
22219
22434
  // src/test-policy-core.ts
22220
22435
  var import_node_child_process15 = require("node:child_process");
22221
22436
  var import_node_fs27 = require("node:fs");
22222
- var import_node_path24 = require("node:path");
22437
+ var import_node_path25 = require("node:path");
22223
22438
  var POLICY_FILE = "test-policy.json";
22224
22439
  var TEST_RE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
22225
22440
  var PY_TEST_RE = /(?:^|\/)test_[^/]*\.py$|_test\.py$/;
@@ -22272,7 +22487,7 @@ function isTestPath(path2) {
22272
22487
  return TEST_RE.test(path2) || PY_TEST_RE.test(path2);
22273
22488
  }
22274
22489
  function loadPolicy(root, readFile9 = readFileOrNull2) {
22275
- const raw = readFile9((0, import_node_path24.join)(root, POLICY_FILE));
22490
+ const raw = readFile9((0, import_node_path25.join)(root, POLICY_FILE));
22276
22491
  if (raw == null) return { mandatory: [], declared: false };
22277
22492
  try {
22278
22493
  return { ...JSON.parse(raw), declared: true };
@@ -22310,11 +22525,11 @@ function classify(changed, policy, present = () => false) {
22310
22525
  return { mandatoryHits, untestedHits, testChanges, addedTests, removedProtected };
22311
22526
  }
22312
22527
  function unresolvedProtectedEntries(policy, root, exists = (path2) => (0, import_node_fs27.existsSync)(path2)) {
22313
- return (policy.protected ?? []).map((p) => p.path).filter((p) => !exists((0, import_node_path24.join)(root, p)));
22528
+ return (policy.protected ?? []).map((p) => p.path).filter((p) => !exists((0, import_node_path25.join)(root, p)));
22314
22529
  }
22315
22530
  function unresolvedSatisfiers(policy, root, exists = (path2) => (0, import_node_fs27.existsSync)(path2)) {
22316
22531
  const declared = (policy.mandatory ?? []).flatMap((m) => m.satisfiedBy ?? []);
22317
- return [...new Set(declared)].filter((p) => !exists((0, import_node_path24.join)(root, p)));
22532
+ return [...new Set(declared)].filter((p) => !exists((0, import_node_path25.join)(root, p)));
22318
22533
  }
22319
22534
  function evaluate(changed, policy, present = () => false) {
22320
22535
  const { mandatoryHits, untestedHits, testChanges, addedTests, removedProtected } = classify(changed, policy, present);
@@ -22502,7 +22717,7 @@ function runTestPolicy(root, deps = {}) {
22502
22717
  const refusal = deps.changed ? null : untrustworthyRange(root, base);
22503
22718
  const changed = deps.changed ?? (refusal ? [] : changedFilesSince(base, root));
22504
22719
  const lookup = deps.override !== void 0 ? { override: deps.override, refusals: [] } : !deps.changed && !refusal ? readOverride(base, root) : { override: null, refusals: [] };
22505
- const present = (path2) => exists((0, import_node_path24.join)(root, path2));
22720
+ const present = (path2) => exists((0, import_node_path25.join)(root, path2));
22506
22721
  const removedByThisDiff = removedPaths(changed);
22507
22722
  const staleFindings = [];
22508
22723
  const unresolved = unresolvedProtectedEntries(policy, root, exists).filter((p) => !removedByThisDiff.has(p));
@@ -22541,7 +22756,7 @@ function runTestPolicy(root, deps = {}) {
22541
22756
 
22542
22757
  // src/project-info-sync.ts
22543
22758
  var import_node_fs28 = require("node:fs");
22544
- var import_node_path25 = require("node:path");
22759
+ var import_node_path26 = require("node:path");
22545
22760
  var UPDATE_PROJECT_INFO = `mutation($projectId: ID!, $shortDescription: String!, $readme: String!) {
22546
22761
  updateProjectV2(input: { projectId: $projectId, shortDescription: $shortDescription, readme: $readme }) {
22547
22762
  projectV2 { id }
@@ -22586,7 +22801,7 @@ function sharedName(entries, fallback) {
22586
22801
  }
22587
22802
  function buildProjectInfoSyncPlan(targetRepo2, project2, projects, repoRoot2) {
22588
22803
  if (!project2.projectId) throw new Error(`org project sync-info: ${targetRepo2} registry META has no projectId`);
22589
- const readmePath = (0, import_node_path25.join)(repoRoot2, "README.md");
22804
+ const readmePath = (0, import_node_path26.join)(repoRoot2, "README.md");
22590
22805
  if (!(0, import_node_fs28.existsSync)(readmePath)) throw new Error(`org project sync-info: ${targetRepo2} has no README.md`);
22591
22806
  const entries = entriesFor(project2, projects);
22592
22807
  const memberRepos = [...new Set(entries.flatMap((entry) => entry.repos ?? []))].filter((repo) => /^[^/]+\/[^/]+$/.test(repo)).sort((a, b) => a.localeCompare(b));
@@ -22612,8 +22827,8 @@ function buildProjectInfoSyncPlan(targetRepo2, project2, projects, repoRoot2) {
22612
22827
  const targetBase = `https://github.com/${targetRepo2}`;
22613
22828
  const targetBranch = branchFor(targetRepo2, projects);
22614
22829
  const orgDocs = [
22615
- (0, import_node_fs28.existsSync)((0, import_node_path25.join)(repoRoot2, "docs", "org-readme.md")) ? `- [Org identity](${targetBase}/blob/${targetBranch}/docs/org-readme.md)` : "",
22616
- (0, import_node_fs28.existsSync)((0, import_node_path25.join)(repoRoot2, "docs", "org-architecture.md")) ? `- [Org architecture](${targetBase}/blob/${targetBranch}/docs/org-architecture.md)` : ""
22830
+ (0, import_node_fs28.existsSync)((0, import_node_path26.join)(repoRoot2, "docs", "org-readme.md")) ? `- [Org identity](${targetBase}/blob/${targetBranch}/docs/org-readme.md)` : "",
22831
+ (0, import_node_fs28.existsSync)((0, import_node_path26.join)(repoRoot2, "docs", "org-architecture.md")) ? `- [Org architecture](${targetBase}/blob/${targetBranch}/docs/org-architecture.md)` : ""
22617
22832
  ].filter(Boolean);
22618
22833
  if (orgDocs.length) lines.push("", "## Organisation docs", "", ...orgDocs);
22619
22834
  return { projectId: project2.projectId, projectName, targetRepo: targetRepo2, memberRepos, shortDescription, readme: `${lines.join("\n")}
@@ -23491,7 +23706,7 @@ function writeError(res) {
23491
23706
 
23492
23707
  // src/secrets-commands.ts
23493
23708
  var import_node_fs29 = require("node:fs");
23494
- var import_node_path26 = require("node:path");
23709
+ var import_node_path27 = require("node:path");
23495
23710
  var import_node_os10 = require("node:os");
23496
23711
 
23497
23712
  // src/project-runtime.ts
@@ -23615,11 +23830,11 @@ function collectMap(value, previous = []) {
23615
23830
  return [...previous, value];
23616
23831
  }
23617
23832
  async function decryptRailsCredentials(input) {
23618
- const appDir = (0, import_node_path26.resolve)(input.appDir ?? process.cwd());
23833
+ const appDir = (0, import_node_path27.resolve)(input.appDir ?? process.cwd());
23619
23834
  const credentialsFile = input.credentialsFile ?? DEFAULT_RAILS_CREDENTIALS_FILE;
23620
23835
  const masterKeyFile = input.masterKeyFile ?? DEFAULT_RAILS_MASTER_KEY_FILE;
23621
- const credentialsPath = (0, import_node_path26.resolve)(appDir, credentialsFile);
23622
- const masterKeyPath = (0, import_node_path26.resolve)(appDir, masterKeyFile);
23836
+ const credentialsPath = (0, import_node_path27.resolve)(appDir, credentialsFile);
23837
+ const masterKeyPath = (0, import_node_path27.resolve)(appDir, masterKeyFile);
23623
23838
  const env = {
23624
23839
  ...process.env,
23625
23840
  MMI_RAILS_CREDENTIALS_FILE: credentialsPath,
@@ -23636,8 +23851,8 @@ async function decryptRailsCredentials(input) {
23636
23851
  'config = ActiveSupport::EncryptedConfiguration.new(config_path: config_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true)',
23637
23852
  "puts JSON.generate(config.config)"
23638
23853
  ].join("\n");
23639
- const scriptDir = (0, import_node_fs29.mkdtempSync)((0, import_node_path26.join)((0, import_node_os10.tmpdir)(), "mmi-rails-decrypt-"));
23640
- const scriptPath = (0, import_node_path26.join)(scriptDir, "decrypt.rb");
23854
+ const scriptDir = (0, import_node_fs29.mkdtempSync)((0, import_node_path27.join)((0, import_node_os10.tmpdir)(), "mmi-rails-decrypt-"));
23855
+ const scriptPath = (0, import_node_path27.join)(scriptDir, "decrypt.rb");
23641
23856
  (0, import_node_fs29.writeFileSync)(scriptPath, script, "utf8");
23642
23857
  try {
23643
23858
  const args = ["exec", "ruby", scriptPath];
@@ -23740,7 +23955,7 @@ function registerSecretsCommands(program3) {
23740
23955
  let body;
23741
23956
  if (o.file) {
23742
23957
  try {
23743
- body = (0, import_node_fs29.readFileSync)((0, import_node_path26.resolve)(o.file), "utf8");
23958
+ body = (0, import_node_fs29.readFileSync)((0, import_node_path27.resolve)(o.file), "utf8");
23744
23959
  } catch (e) {
23745
23960
  return fail(`secrets org-catalog: cannot read --file ${o.file}: ${e.message}`);
23746
23961
  }
@@ -23845,7 +24060,7 @@ function registerSecretsCommands(program3) {
23845
24060
  {
23846
24061
  ...d,
23847
24062
  decryptRailsCredentials,
23848
- removeFile: (path2) => (0, import_node_fs29.unlinkSync)((0, import_node_path26.resolve)(o.appDir ?? process.cwd(), path2))
24063
+ removeFile: (path2) => (0, import_node_fs29.unlinkSync)((0, import_node_path27.resolve)(o.appDir ?? process.cwd(), path2))
23849
24064
  },
23850
24065
  {
23851
24066
  repo: o.repo,
@@ -24226,7 +24441,7 @@ ${SSH_RECIPE_AGENT_NOTE}`);
24226
24441
  }
24227
24442
 
24228
24443
  // src/schedules-commands.ts
24229
- var import_promises4 = require("node:fs/promises");
24444
+ var import_promises5 = require("node:fs/promises");
24230
24445
  var import_node_child_process16 = require("node:child_process");
24231
24446
  var import_node_util7 = require("node:util");
24232
24447
 
@@ -24891,9 +25106,9 @@ function registerSchedulesCommands(program3) {
24891
25106
  await failGraceful("org schedules --doc: refusing to regenerate the doc from an incomplete read (see warnings above).");
24892
25107
  return;
24893
25108
  }
24894
- const docText = await (0, import_promises4.readFile)(o.doc, "utf8");
25109
+ const docText = await (0, import_promises5.readFile)(o.doc, "utf8");
24895
25110
  const spliced = spliceDoc(docText, renderDocSection(entries, (/* @__PURE__ */ new Date()).toISOString()));
24896
- await (0, import_promises4.writeFile)(o.doc, spliced, "utf8");
25111
+ await (0, import_promises5.writeFile)(o.doc, spliced, "utf8");
24897
25112
  reportDrift(drift);
24898
25113
  console.log(`org schedules: wrote ${entries.length} entries into ${o.doc}`);
24899
25114
  return;
@@ -24976,102 +25191,6 @@ function registerSchedulesCommands(program3) {
24976
25191
  });
24977
25192
  }
24978
25193
 
24979
- // src/file-lock.ts
24980
- var import_promises5 = require("node:fs/promises");
24981
- var import_node_path27 = require("node:path");
24982
- var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
24983
- var IMMEDIATE_RETRY_BUDGET = 3;
24984
- var FileLockBusyError = class extends Error {
24985
- lockPath;
24986
- constructor(label, lockPath, maxWaitMs) {
24987
- super(`${label} busy: ${lockPath} held longer than ${maxWaitMs}ms`);
24988
- this.name = "FileLockBusyError";
24989
- this.lockPath = lockPath;
24990
- }
24991
- };
24992
- function resolveFileLockOpts(opts = {}) {
24993
- return {
24994
- staleMs: opts.staleMs ?? 3e4,
24995
- retryMs: opts.retryMs ?? 50,
24996
- maxWaitMs: opts.maxWaitMs ?? 5e3,
24997
- label: opts.label ?? "file lock"
24998
- };
24999
- }
25000
- async function acquireFileLock(lockPath, opts, deadline) {
25001
- let immediateRetries = 0;
25002
- for (; ; ) {
25003
- let handle;
25004
- try {
25005
- handle = await (0, import_promises5.open)(lockPath, "wx");
25006
- } catch (e) {
25007
- const code = e.code;
25008
- if (code !== "EEXIST" && code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") throw e;
25009
- const retryNow = async () => {
25010
- if (Date.now() >= deadline) throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
25011
- if (++immediateRetries > IMMEDIATE_RETRY_BUDGET) await sleep(opts.retryMs);
25012
- };
25013
- try {
25014
- const age = Date.now() - (await (0, import_promises5.stat)(lockPath)).mtimeMs;
25015
- if (age > opts.staleMs) {
25016
- try {
25017
- await (0, import_promises5.unlink)(lockPath);
25018
- await retryNow();
25019
- continue;
25020
- } catch (unlinkError) {
25021
- if (unlinkError instanceof FileLockBusyError) throw unlinkError;
25022
- const unlinkCode = unlinkError.code;
25023
- if (unlinkCode === "ENOENT") {
25024
- await retryNow();
25025
- continue;
25026
- }
25027
- }
25028
- }
25029
- } catch (statError) {
25030
- if (statError instanceof FileLockBusyError) throw statError;
25031
- if (statError.code !== "ENOENT") throw statError;
25032
- await retryNow();
25033
- continue;
25034
- }
25035
- if (Date.now() >= deadline) {
25036
- throw new FileLockBusyError(opts.label, lockPath, opts.maxWaitMs);
25037
- }
25038
- await sleep(opts.retryMs);
25039
- continue;
25040
- }
25041
- const token = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
25042
- try {
25043
- await handle.writeFile(token);
25044
- } catch (e) {
25045
- await handle.close().catch(() => void 0);
25046
- throw e;
25047
- }
25048
- return { token, handle };
25049
- }
25050
- }
25051
- async function fileLockHeldBy(lockPath, token) {
25052
- try {
25053
- return await (0, import_promises5.readFile)(lockPath, "utf8") === token;
25054
- } catch {
25055
- return false;
25056
- }
25057
- }
25058
- async function releaseFileLock(lockPath, guard) {
25059
- await guard.handle.close().catch(() => void 0);
25060
- if (await fileLockHeldBy(lockPath, guard.token)) {
25061
- await (0, import_promises5.unlink)(lockPath).catch(() => void 0);
25062
- }
25063
- }
25064
- async function withFileLock(lockPath, opts, fn) {
25065
- const resolved = resolveFileLockOpts(opts);
25066
- await (0, import_promises5.mkdir)((0, import_node_path27.dirname)(lockPath), { recursive: true }).catch(() => void 0);
25067
- const guard = await acquireFileLock(lockPath, resolved, Date.now() + resolved.maxWaitMs);
25068
- try {
25069
- return await fn();
25070
- } finally {
25071
- await releaseFileLock(lockPath, guard);
25072
- }
25073
- }
25074
-
25075
25194
  // src/schedules-lift-command.ts
25076
25195
  var import_promises6 = require("node:fs/promises");
25077
25196
  var import_node_path28 = require("node:path");
@@ -31099,8 +31218,8 @@ var LOOP_PLAYBOOKS = {
31099
31218
  { label: "Read the next board item", command: "mmi-cli board read" },
31100
31219
  { label: "Claim it and create an isolated worktree", command: "mmi-cli worktree create <issue-number> --claim --from origin/development" },
31101
31220
  { label: "Build and test in the touched package", command: "npm test && npm run build" },
31102
- { label: "Publish the branch", command: "git push -u origin HEAD" },
31103
- { label: "Open the development-base PR", command: 'mmi-cli pr create --title "<title>" --body-file PR_BODY.md --base development' },
31221
+ { label: "Publish the branch", command: "git push origin <branch>:<branch>" },
31222
+ { label: "Open the development-base PR", command: 'mmi-cli pr create --title "<title>" --body-file .jerv/PR_BODY.md --base development' },
31104
31223
  { label: "Wait for checks and land to development", command: "mmi-cli pr checks-wait <PR-number> && mmi-cli pr land <PR-number>" },
31105
31224
  { label: "Release only after the gated train is authorized", command: "mmi-cli release --apply" }
31106
31225
  ]
@@ -31117,8 +31236,8 @@ var LOOP_PLAYBOOKS = {
31117
31236
  "ship-pr": {
31118
31237
  title: "Ship PR",
31119
31238
  steps: [
31120
- { label: "Publish the branch", command: "git push -u origin HEAD" },
31121
- { label: "Open the development-base PR", command: 'mmi-cli pr create --title "<title>" --body-file PR_BODY.md --base development' },
31239
+ { label: "Publish the branch", command: "git push origin <branch>:<branch>" },
31240
+ { label: "Open the development-base PR", command: 'mmi-cli pr create --title "<title>" --body-file .jerv/PR_BODY.md --base development' },
31122
31241
  { label: "Wait for CI checks", command: "mmi-cli pr checks-wait <PR-number>" },
31123
31242
  { label: "Land the PR (merge to development)", command: "mmi-cli pr land <PR-number>" }
31124
31243
  ]
@@ -31126,9 +31245,9 @@ var LOOP_PLAYBOOKS = {
31126
31245
  "hotfix": {
31127
31246
  title: "Hotfix",
31128
31247
  steps: [
31129
- { label: "Create the main-base hotfix PR from an already-merged fix", command: "mmi-cli hotfix start --from <pr#|sha>" },
31248
+ { label: "Create the main-base hotfix PR from every already-merged fix this cycle carries", command: "mmi-cli hotfix start --from <pr#|sha>[,<pr#|sha>...]" },
31130
31249
  { label: "Wait for the hotfix PR checks", command: "mmi-cli pr checks-wait <PR-number>" },
31131
- { label: "After the PR is merged, run the gated release", command: "mmi-cli hotfix release <vX.Y.Z> --carries <pr#|sha>" }
31250
+ { label: "After the PR is merged, run the gated release", command: "mmi-cli hotfix release <vX.Y.Z> --carries <pr#|sha>[,<pr#|sha>...]" }
31132
31251
  ]
31133
31252
  }
31134
31253
  };
@@ -35257,7 +35376,7 @@ withExamples(mutating(
35257
35376
  }
35258
35377
  if (!resumed) {
35259
35378
  step = `git worktree add ${wtPath}`;
35260
- await addWorktreeRobust(wtPath, branch, base, {
35379
+ await withWorktreeAddLock(repoRoot2, () => addWorktreeRobust(wtPath, branch, base, {
35261
35380
  git: async (args) => (await execFileP2("git", args, { timeout: GH_MUTATION_TIMEOUT_MS })).stdout,
35262
35381
  revParse: async (ref) => {
35263
35382
  try {
@@ -35271,7 +35390,7 @@ withExamples(mutating(
35271
35390
  log: (m) => {
35272
35391
  if (!o.json) console.error(` ${m}`);
35273
35392
  }
35274
- });
35393
+ }));
35275
35394
  }
35276
35395
  step = "install deps + copy local-only config";
35277
35396
  const report = await provisionWorktree(wtPath, makeProvisionDeps(wtPath, Boolean(o.json), (m) => {
@@ -36747,10 +36866,11 @@ withExamples(pr.command("create").description("create a PR and print {number,url
36747
36866
  console.log(JSON.stringify(created));
36748
36867
  }), [
36749
36868
  'mmi-cli pr create --title "Add the schema" --body "Closes #2680"',
36750
- 'mmi-cli pr create --title "Add the schema" --body-file PR_BODY.md --draft'
36869
+ 'mmi-cli pr create --title "Add the schema" --body-file .jerv/PR_BODY.md --draft'
36751
36870
  ], [
36752
36871
  "--head and --base default to the current branch and the repo default; only pass them to override.",
36753
- "Use --body-file for multiline PR bodies instead of shell-escaped inline markdown."
36872
+ "Use --body-file for multiline PR bodies instead of shell-escaped inline markdown.",
36873
+ "Write that file under .jerv/ inside a worktree (#4405): any other untracked path makes `worktree land` refuse cleanup as untracked-files."
36754
36874
  ]);
36755
36875
  pr.command("view <number>").description("read a PR as structured JSON (merged state, head/base, URL, merge commit) ? the mmi-cli read path (#2347). --comments folds in every comment; --context also adds linkedIssues (the issues it closes/references, #2894)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). Accepts commas, spaces, or repeated --json flags ? in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "state,baseRefName,mergeCommit"').option("--comments", "include every comment (body + comments in one call) ? read the whole PR before landing it (#2894)").option("--context", "full working context in one call: implies --comments and also adds linkedIssues (the issues the PR closes/references) (#2894)").action(async (number, o) => {
36756
36876
  const n = Number(number);
@@ -37515,7 +37635,8 @@ function renderHotfixRelease(r) {
37515
37635
  ...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
37516
37636
  ` - ${r.verifyNote}`,
37517
37637
  ...r.announceNote ? [` - announce: ${r.announceNote}`] : [],
37518
- ` - next: mmi-cli hotfix status ${r.tag} (no back-merge ? development already has the fix; the next /release back-merge aligns the version manifests)`
37638
+ ` - fold: ${r.foldNote}`,
37639
+ ` - next: mmi-cli hotfix status ${r.tag} (no back-merge of the FIX ? development already has it; the version fold above is ported for you, #4410)`
37519
37640
  ].join("\n");
37520
37641
  }
37521
37642
  function renderHotfixStatus(r) {
@@ -37547,7 +37668,7 @@ var hotfixCmd = program2.command("hotfix").description("stepwise hotfix orchestr
37547
37668
  const steps = trainPlan("hotfix");
37548
37669
  console.log(o.json ? JSON.stringify({ command: "hotfix", steps }, null, 2) : renderSteps("mmi-cli hotfix: dry-run plan", steps));
37549
37670
  });
37550
- hotfixCmd.command("start").description("cherry-pick a merged development PR (or SHA) onto hotfix/vX.Y.Z from origin/main, bump the distribution, open the main-base PR").requiredOption("--from <pr#|sha>", "merged development PR number or commit SHA to cherry-pick").option("--json", "machine-readable output").action(async (o) => runHotfixSub("start", () => runHotfixStart(trainApplyDeps(), { from: o.from }), o.json, renderHotfixStart));
37671
+ hotfixCmd.command("start").description("cherry-pick one or more merged development PRs (or SHAs) onto hotfix/vX.Y.Z from origin/main, bump the distribution, open the main-base PR").requiredOption("--from <pr#|sha[,pr#|sha...]>", "merged development PR number(s) or commit SHA(s) to cherry-pick, in pick order \u2014 one hotfix cycle carries as many fixes as you name (#4411)").option("--json", "machine-readable output").action(async (o) => runHotfixSub("start", () => runHotfixStart(trainApplyDeps(), { from: o.from }), o.json, renderHotfixStart));
37551
37672
  hotfixCmd.command("release <version>").description("after the hotfix PR is merged + checks green: tag, GitHub Release, watch deploy/publish, verify distribution (idempotent)").option("--json", "machine-readable output").option("--announce-summary-file <path>", "agent-curated summary lines for the Hub Slack announcement (#883)").option("--carries <pr#|sha[,pr#|sha...]>", "declared fix target(s) this hotfix must carry; each must be proven present before tagging (#3056)").action(async (version, o) => runHotfixSub("release", () => runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, carries: o.carries ? [o.carries] : [] }), o.json, renderHotfixRelease));
37552
37673
  hotfixCmd.command("status [version]").description("derive the full hotfix pipeline state from live git/gh reads and name the exact next subcommand").option("--json", "machine-readable output").action(async (version, o) => runHotfixSub("status", () => runHotfixStatus(trainApplyDeps(), version), o.json, renderHotfixStatus));
37553
37674
  var ci = program2.command("ci").description("org CI + merge-readiness audit and reconcile");