@mutmutco/cli 3.119.0 → 3.120.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.cjs +232 -21
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -7411,6 +7411,7 @@ async function removeWorktreeWithRecovery(wtPath, deps) {
7411
7411
  const backoff = deps.backoffMs ?? [250, 1e3];
7412
7412
  let attempts = 0;
7413
7413
  let lastError;
7414
+ const stillOnDisk = () => deps.pathExists?.(wtPath) ?? false;
7414
7415
  let safeForGitPrimary = true;
7415
7416
  if (deps.detachReparsePoints) {
7416
7417
  try {
@@ -7424,7 +7425,9 @@ async function removeWorktreeWithRecovery(wtPath, deps) {
7424
7425
  attempts++;
7425
7426
  try {
7426
7427
  await deps.git(["worktree", "remove", "--force", wtPath]);
7427
- return { status: "removed", attempts, recovery: i > 0 ? "retry" : void 0 };
7428
+ if (!stillOnDisk()) return { status: "removed", attempts, recovery: i > 0 ? "retry" : void 0 };
7429
+ lastError = new Error(`worktree directory still present after 'git worktree remove --force': ${wtPath}`);
7430
+ break;
7428
7431
  } catch (e) {
7429
7432
  lastError = e;
7430
7433
  const retriesLeft = i < maxAttempts - 1;
@@ -7439,12 +7442,18 @@ async function removeWorktreeWithRecovery(wtPath, deps) {
7439
7442
  try {
7440
7443
  await deps.removeWorktreeDir(wtPath);
7441
7444
  await deps.git(["worktree", "prune"]).catch(() => "");
7442
- return { status: "removed", attempts, recovery: "fallback" };
7445
+ if (!stillOnDisk()) return { status: "removed", attempts, recovery: "fallback" };
7446
+ lastError = new Error(`worktree directory still present after on-disk removal: ${wtPath}`);
7443
7447
  } catch (fallbackError) {
7444
7448
  lastError = fallbackError;
7445
7449
  }
7446
7450
  }
7447
- return { status: "failed", attempts, error: errorMessage(lastError) };
7451
+ return {
7452
+ status: "failed",
7453
+ attempts,
7454
+ error: errorMessage(lastError),
7455
+ ...stillOnDisk() ? { remainsOnDisk: true } : {}
7456
+ };
7448
7457
  }
7449
7458
  var NO_NESTED_LINKS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
7450
7459
  function baseName(p) {
@@ -8282,7 +8291,9 @@ async function cleanupPrMergeLocalBranch(branch, options) {
8282
8291
  sleep: options.sleep ?? defaultSleep,
8283
8292
  detachReparsePoints: options.detachReparsePoints,
8284
8293
  // #3064: junction-safe deferred sweep too
8285
- removeWorktreeDir: options.removeWorktreeDir
8294
+ removeWorktreeDir: options.removeWorktreeDir,
8295
+ pathExists: options.pathExists
8296
+ // #4850: a swept entry is only cleared once its directory is gone
8286
8297
  };
8287
8298
  await sweepDeferredWorktrees(options.deferredStore, removeDeps, options.removalContext).catch(() => void 0);
8288
8299
  let stageTeardown;
@@ -8326,7 +8337,9 @@ async function cleanupPrMergeLocalBranch(branch, options) {
8326
8337
  sleep: options.sleep ?? defaultSleep,
8327
8338
  detachReparsePoints: options.detachReparsePoints,
8328
8339
  // #3064: reach the actual pr-merge teardown path
8329
- removeWorktreeDir: options.removeWorktreeDir
8340
+ removeWorktreeDir: options.removeWorktreeDir,
8341
+ pathExists: options.pathExists
8342
+ // #4850: never report `removed` while the directory survives
8330
8343
  });
8331
8344
  if (options.removalContext) {
8332
8345
  recordWorktreeRemoval(options.removalContext.primaryRoot, {
@@ -9263,6 +9276,9 @@ function resolveWorktreeBase(from, remote) {
9263
9276
  if (SHA_LIKE_RE.test(from)) return { base: from };
9264
9277
  return { base: from, fetchBranch: from, preferRemote: `${remotePrefix}${from}` };
9265
9278
  }
9279
+ function worktreeAddGitPrefix() {
9280
+ return process.platform === "win32" ? ["-c", "core.longpaths=true"] : [];
9281
+ }
9266
9282
  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;
9267
9283
  function isGitConfigLockError(error) {
9268
9284
  return GIT_CONFIG_LOCK_RE.test(error instanceof Error ? error.message : String(error));
@@ -9304,6 +9320,8 @@ async function addWorktreeRobust(wtPath, branch, base, deps) {
9304
9320
  await deps.git(["worktree", "add", wtPath, "-b", branch, base]);
9305
9321
  return;
9306
9322
  } catch (e) {
9323
+ await deps.cleanupPartial?.().catch(() => {
9324
+ });
9307
9325
  await cleanupOrphanBranch(branch, base, preExistingOid, deps).catch(() => {
9308
9326
  });
9309
9327
  const retriesLeft = i < maxAttempts - 1;
@@ -9967,6 +9985,13 @@ var MANAGED_GITIGNORE_LINES = [
9967
9985
  "docs/superpowers/",
9968
9986
  ".playwright-mcp/",
9969
9987
  ".claude/worktrees/",
9988
+ // `.mmi/` is org session scratch (session id, throttle traces, saga queue — AGENTS.md "No committed
9989
+ // `.mmi`"). #2319 retired this rule believing repo-local `.mmi` state was gone; #4833 proved repos
9990
+ // still get `.mmi/` written (older tooling on the machine, product-repo scripts), and dropping the
9991
+ // rule left that scratch untracked-visible one `git add .` away from a commit. Ignore the dir's
9992
+ // contents at ANY depth; an already-tracked `.mmi/config.json` keeps tracking regardless (git never
9993
+ // untracks a tracked file), so no re-inclusion line is needed.
9994
+ "**/.mmi/*",
9970
9995
  // #3425: local agent config the org push wall (`mmi-no-agent-files-org`) forbids committing, at any
9971
9996
  // depth — leaving it untracked is the ONLY legal state, so it is pure `git status` noise. Only the two
9972
9997
  // dirs no repo has cause to track: `.claude/` is excluded because MMI-Hub tracks `.claude/settings.json`,
@@ -15820,10 +15845,10 @@ var rollout_plan_default = {
15820
15845
  note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
15821
15846
  },
15822
15847
  baseline: {
15823
- version: "3.119.0",
15824
- tag: "v3.119.0",
15825
- commit: "32fef9b214ee",
15826
- npm: "@mutmutco/cli@3.119.0"
15848
+ version: "3.120.0",
15849
+ tag: "v3.120.0",
15850
+ commit: "5e8fd4eaab15",
15851
+ npm: "@mutmutco/cli@3.120.0"
15827
15852
  },
15828
15853
  exitCriterion: "fleet-n-of-n",
15829
15854
  hubOnlyShortcut: "forbidden",
@@ -15840,14 +15865,14 @@ var rollout_plan_default = {
15840
15865
  repo: "mutmutco/mmi-hub",
15841
15866
  role: "canary",
15842
15867
  schedule: "train",
15843
- v3Target: "v3.119.0"
15868
+ v3Target: "v3.120.0"
15844
15869
  }
15845
15870
  ],
15846
15871
  rollbackTrigger: "Any red inside the post-cut soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a v3 client refused while the compat window must still admit it (SUPPORTED_MINOR_WINDOW=2, MIN_CLIENT_VERSION 0.0.0 \u2014 D6a), or npm consumer install/doctor failure on the v4 dist.",
15847
15872
  rollback: {
15848
15873
  independent: true,
15849
- mechanism: "npm dist-tag latest -> 3.119.0 and redeploy the Hub Lambda from tag v3.119.0 (32fef9b214ee); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15850
- v3Target: "v3.119.0 (@mutmutco/cli@3.119.0, tag commit 32fef9b214ee \u2014 the preserved latest-v3 distribution, D6b)"
15874
+ mechanism: "npm dist-tag latest -> 3.120.0 and redeploy the Hub Lambda from tag v3.120.0 (5e8fd4eaab15); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15875
+ v3Target: "v3.120.0 (@mutmutco/cli@3.120.0, tag commit 5e8fd4eaab15 \u2014 the preserved latest-v3 distribution, D6b)"
15851
15876
  }
15852
15877
  },
15853
15878
  {
@@ -24696,7 +24721,7 @@ function collectLeaves(node, acc) {
24696
24721
  function buildHouses(tree) {
24697
24722
  const collect = (node, parentHouse, root, out) => {
24698
24723
  if (node.house === root && parentHouse !== root) {
24699
- const entry = { path: node.path };
24724
+ const entry = { path: node.path, canonical: node.canonical };
24700
24725
  if (node.description) entry.description = node.description;
24701
24726
  out.push(entry);
24702
24727
  }
@@ -33256,7 +33281,9 @@ function worktreeRemoveDeps(execGit) {
33256
33281
  // #3064: unlink any reparse point (esp. a `node_modules` junction to base) before the git-native
33257
33282
  // `worktree remove --force`, which would otherwise recurse through it and empty the base checkout.
33258
33283
  detachReparsePoints: (worktreePath) => detachReparsePoints(worktreePath, realWorktreeDirRemover),
33259
- removeWorktreeDir: async (worktreePath) => removeWorktreeTree(worktreePath, await resolvePrimaryCheckout(execGit), realWorktreeDirRemover)
33284
+ removeWorktreeDir: async (worktreePath) => removeWorktreeTree(worktreePath, await resolvePrimaryCheckout(execGit), realWorktreeDirRemover),
33285
+ // #4850: verify the directory is actually gone before any caller reports completion.
33286
+ pathExists: (worktreePath) => (0, import_node_fs39.existsSync)(worktreePath)
33260
33287
  };
33261
33288
  }
33262
33289
  async function worktreeHasStageState(worktreePath) {
@@ -34393,7 +34420,7 @@ function registerWorktreeCommands(program3) {
34393
34420
  );
34394
34421
  report.push({
34395
34422
  step: "remove worktree",
34396
- status: removeOutcome.status === "removed" ? removeOutcome.recovery ? `done (${removeOutcome.recovery})` : "done" : `failed: ${removeOutcome.error ?? "lock held"} \u2014 run: git -C "${primaryCheckout}" worktree remove --force "${wtPath}"`
34423
+ status: removeOutcome.status === "removed" ? removeOutcome.recovery ? `done (${removeOutcome.recovery})` : "done" : removeOutcome.remainsOnDisk ? `failed: ${removeOutcome.error ?? "lock held"} \u2014 the directory "${wtPath}" is still on disk; cd every shell out of it (a cwd inside holds it open on Windows), then delete that directory` : `failed: ${removeOutcome.error ?? "lock held"} \u2014 run: git -C "${primaryCheckout}" worktree remove --force "${wtPath}"`
34397
34424
  });
34398
34425
  if (!shouldContinueLandCleanup(removeOutcome.status)) {
34399
34426
  const result2 = {
@@ -35865,6 +35892,31 @@ function markerCommentBody(body, markerId) {
35865
35892
  return `${prCommentMarker(markerId)}
35866
35893
  ${body}`;
35867
35894
  }
35895
+ var CHECK_LOG_TAIL_LINES = 60;
35896
+ var CHECK_LOG_MAX_BUFFER = 32 * 1024 * 1024;
35897
+ function parseCheckJobId(details) {
35898
+ const match = /\/job\/(\d+)\b/.exec(details ?? "");
35899
+ return match ? match[1] : null;
35900
+ }
35901
+ function failingCheckRows(rows) {
35902
+ return rows.filter((row) => row.status === "fail" || row.status === "failure");
35903
+ }
35904
+ function tailLines(text, max = CHECK_LOG_TAIL_LINES) {
35905
+ const lines = text.split(/\r?\n/).filter((line) => line.trim());
35906
+ return { tail: lines.slice(-max).join("\n"), truncated: lines.length > max };
35907
+ }
35908
+ async function attachFailureLogs(rows, fetchLog) {
35909
+ for (const row of failingCheckRows(rows)) {
35910
+ const jobId = parseCheckJobId(row.details);
35911
+ if (!jobId) continue;
35912
+ try {
35913
+ const { tail, truncated } = tailLines(await fetchLog(jobId));
35914
+ if (tail) row.log = { jobId, tail, truncated };
35915
+ } catch {
35916
+ }
35917
+ }
35918
+ return rows;
35919
+ }
35868
35920
  function parsePrChecksTable(stdout) {
35869
35921
  const lines = stdout.trim().split(/\r?\n/).filter((l) => l.trim());
35870
35922
  if (!lines.length) return [];
@@ -36190,7 +36242,7 @@ function registerPrLifecycleCommands(program3) {
36190
36242
  return failGraceful(`pr update-branch: ${(err.stderr || err.message || String(e)).trim()}`);
36191
36243
  }
36192
36244
  });
36193
- pr2.command("checks <number>").description("one-shot non-blocking check snapshot; --watch streams state with per-check failure log pointers").option("--json", "machine-readable output").option("--watch", "poll until success/failure, streaming one JSON line per poll").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (number, o) => {
36245
+ pr2.command("checks <number>").description("one-shot non-blocking check snapshot; --watch streams state with per-check failure log pointers").option("--json", "machine-readable output").option("--watch", "poll until success/failure, streaming one JSON line per poll").option("--logs", `fold the last ${CHECK_LOG_TAIL_LINES} log lines of every FAILING check into its row, so reading why CI is red needs no second tool (#4853)`).option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (number, o) => {
36194
36246
  const n = assertPositiveInt("pr checks", number);
36195
36247
  let repo;
36196
36248
  try {
@@ -36199,6 +36251,15 @@ function registerPrLifecycleCommands(program3) {
36199
36251
  return fail(`pr checks: ${e.message}`);
36200
36252
  }
36201
36253
  const repoArgs = ["--repo", repo];
36254
+ const fetchLog = async (jobId) => {
36255
+ const { stdout } = await execFileP2(
36256
+ "gh",
36257
+ ["run", "view", ...repoArgs, "--job", jobId, "--log-failed"],
36258
+ { timeout: GC_GH_TIMEOUT_MS4, maxBuffer: CHECK_LOG_MAX_BUFFER }
36259
+ );
36260
+ return stdout;
36261
+ };
36262
+ const withLogs = async (rows) => o.logs ? attachFailureLogs(rows, fetchLog) : rows;
36202
36263
  const snap = async () => {
36203
36264
  let stdout = "";
36204
36265
  let stderr = "";
@@ -36218,7 +36279,7 @@ function registerPrLifecycleCommands(program3) {
36218
36279
  try {
36219
36280
  if (!o.watch) {
36220
36281
  const { state, rows } = await snap();
36221
- const output = { state, checks: rows };
36282
+ const output = { state, checks: await withLogs(rows) };
36222
36283
  console.log(JSON.stringify(output));
36223
36284
  if (state === "failure" || state === "failing") process.exitCode = 1;
36224
36285
  return;
@@ -36227,7 +36288,8 @@ function registerPrLifecycleCommands(program3) {
36227
36288
  let lastState = "";
36228
36289
  while (Date.now() < deadline) {
36229
36290
  const { state, rows } = await snap();
36230
- const payload = { state, timestamp: (/* @__PURE__ */ new Date()).toISOString(), checks: rows };
36291
+ const checks = state === "failure" ? await withLogs(rows) : rows;
36292
+ const payload = { state, timestamp: (/* @__PURE__ */ new Date()).toISOString(), checks };
36231
36293
  console.log(JSON.stringify(payload));
36232
36294
  if (state === "success") return;
36233
36295
  if (state === "failure") {
@@ -39227,6 +39289,93 @@ function hasRepoLocalWorktrees() {
39227
39289
  return root !== null && (0, import_node_fs46.existsSync)((0, import_node_path43.join)(root, ".worktrees"));
39228
39290
  }
39229
39291
 
39292
+ // src/cross-repo-filing-issue.ts
39293
+ async function discoverSameOrgIssueNumber(client, currentRepo, number) {
39294
+ const [owner] = currentRepo.split("/");
39295
+ if (!owner) throw new Error(`cannot derive an organization from ${currentRepo}`);
39296
+ const repositories = await client.restPaginate(`orgs/${owner}/repos?type=all`);
39297
+ const hits = [];
39298
+ let next = 0;
39299
+ const worker = async () => {
39300
+ while (next < repositories.length) {
39301
+ const repo = repositories[next++]?.full_name;
39302
+ if (!repo || repo.toLowerCase() === currentRepo.toLowerCase()) continue;
39303
+ const issue2 = await readExactIssue(client, repo, number);
39304
+ if (issue2) hits.push(issue2);
39305
+ }
39306
+ };
39307
+ await Promise.all(Array.from({ length: Math.min(8, repositories.length) }, () => worker()));
39308
+ return hits.sort((left, right) => left.repo.localeCompare(right.repo));
39309
+ }
39310
+ async function readExactIssue(client, repo, number) {
39311
+ try {
39312
+ const issue2 = await client.rest("GET", `repos/${repo}/issues/${number}`);
39313
+ if (issue2.pull_request || issue2.number !== number || !issue2.title || !issue2.html_url) return void 0;
39314
+ const state = issue2.state?.toUpperCase();
39315
+ if (state !== "OPEN" && state !== "CLOSED") return void 0;
39316
+ return { repo, number, title: issue2.title, url: issue2.html_url, state };
39317
+ } catch (e) {
39318
+ if (e instanceof GitHubApiError && e.status === 404) return void 0;
39319
+ throw e;
39320
+ }
39321
+ }
39322
+ function explicitSameOrgIssueUrls(text, prRepo, closingNumbers) {
39323
+ const [owner] = prRepo.split("/");
39324
+ const found = /* @__PURE__ */ new Map();
39325
+ for (const match of text.matchAll(/https:\/\/github\.com\/([\w.-]+\/[\w.-]+)\/issues\/(\d+)\b/gi)) {
39326
+ const repo = match[1];
39327
+ const number = Number(match[2]);
39328
+ if (!repo || !Number.isInteger(number) || !closingNumbers.has(number)) continue;
39329
+ if (repo.split("/")[0]?.toLowerCase() !== owner.toLowerCase() || repo.toLowerCase() === prRepo.toLowerCase()) continue;
39330
+ found.set(`${repo.toLowerCase()}#${number}`, { repo, number });
39331
+ }
39332
+ return [...found.values()];
39333
+ }
39334
+ async function reconcileCrossRepoFilingIssue(client, prRepo, prNumber) {
39335
+ try {
39336
+ const pr2 = await client.rest("GET", `repos/${prRepo}/pulls/${prNumber}`);
39337
+ if (!pr2.merged_at) return { status: "not-applicable" };
39338
+ const text = `${pr2.title ?? ""}
39339
+ ${pr2.body ?? ""}`;
39340
+ const closingNumbers = new Set(findClosingMentions(text).filter((mention) => !mention.negated).map((mention) => mention.issue));
39341
+ if (closingNumbers.size === 0) return { status: "not-applicable" };
39342
+ const explicit = explicitSameOrgIssueUrls(text, prRepo, closingNumbers);
39343
+ let target;
39344
+ if (explicit.length > 1) {
39345
+ return { status: "failed", error: `multiple same-org issue URLs match bare closing references (${explicit.map((x) => `${x.repo}#${x.number}`).join(", ")})` };
39346
+ }
39347
+ if (explicit.length === 1) {
39348
+ target = await readExactIssue(client, explicit[0].repo, explicit[0].number);
39349
+ if (!target) return { status: "failed", issue: `${explicit[0].repo}#${explicit[0].number}`, error: "the explicit foreign issue URL did not resolve to an issue" };
39350
+ } else {
39351
+ const branchNumber = Number(/^#?(\d+)(?:-|$)/.exec(pr2.head?.ref ?? "")?.[1]);
39352
+ if (!Number.isInteger(branchNumber) || !closingNumbers.has(branchNumber)) return { status: "not-applicable" };
39353
+ if (await readExactIssue(client, prRepo, branchNumber)) return { status: "not-applicable" };
39354
+ const matches = await discoverSameOrgIssueNumber(client, prRepo, branchNumber);
39355
+ if (matches.length !== 1) {
39356
+ return {
39357
+ status: "failed",
39358
+ error: matches.length === 0 ? `branch #${branchNumber} matches a bare closing reference, but no same-org filing issue was found` : `branch #${branchNumber} is ambiguous across ${matches.map((match) => match.repo).join(", ")}`
39359
+ };
39360
+ }
39361
+ target = matches[0];
39362
+ }
39363
+ const issue2 = `${target.repo}#${target.number}`;
39364
+ if (target.state === "CLOSED") return { status: "already-closed", issue: issue2, url: target.url };
39365
+ const prUrl = pr2.html_url ?? `https://github.com/${prRepo}/pull/${prNumber}`;
39366
+ const comment = await client.rest("POST", `repos/${target.repo}/issues/${target.number}/comments`, {
39367
+ body: { body: `Resolved by merged PR ${prUrl}.
39368
+
39369
+ <!-- mmi-cross-repo-close:${prRepo}#${prNumber} -->` }
39370
+ });
39371
+ if (!comment.html_url) throw new Error(`GitHub created no evidence URL on ${issue2}`);
39372
+ await closeIssue(client, { ref: issue2, reason: "completed", evidence: comment.html_url });
39373
+ return { status: "closed", issue: issue2, url: target.url, evidence: comment.html_url };
39374
+ } catch (e) {
39375
+ return { status: "failed", error: e.message };
39376
+ }
39377
+ }
39378
+
39230
39379
  // src/index.ts
39231
39380
  var execFileGitRun = async (file, args) => (await execFileP2(file, args, { timeout: GIT_TIMEOUT_MS })).stdout;
39232
39381
  async function githubRepoReachProbe() {
@@ -40220,7 +40369,31 @@ withExamples(mutating(
40220
40369
  `worktree create: issue ${selector.repo}#${selector.number} belongs to ${selector.repo}, but this checkout is ${resolvedRepo} \u2014 re-run from ${selector.repo}'s primary checkout. Refusing to attach a foreign issue branch here (MMI-Hub#4351).`
40221
40370
  );
40222
40371
  }
40223
- const slug = o.slug ?? await fetchIssueTitle(selector.repo, selector.number).then((t) => t ? slugifyIssueTitle(t) : "");
40372
+ const issueTitle = await fetchIssueTitle(selector.repo, selector.number);
40373
+ if (!o.slug && !issueTitle && o.claim && /^#?\d+$/.test(target)) {
40374
+ try {
40375
+ const client = defaultGitHubClient();
40376
+ const local = await readExactIssue(client, selector.repo, selector.number);
40377
+ if (!local) {
40378
+ const matches = await discoverSameOrgIssueNumber(client, selector.repo, selector.number);
40379
+ if (matches.length === 1) {
40380
+ const foreign = matches[0];
40381
+ let board = "its registered board";
40382
+ try {
40383
+ const cfg = await loadConfigForBoardSelector2(`${foreign.repo}#${foreign.number}`, foreign.repo);
40384
+ if (cfg.projectOwner && cfg.projectNumber) board = `${cfg.projectOwner} project #${cfg.projectNumber}`;
40385
+ } catch {
40386
+ }
40387
+ return fail(
40388
+ `worktree create: issue #${selector.number} is not in ${selector.repo}; it exists as ${foreign.repo}#${foreign.number} on ${board}. Re-run from ${foreign.repo}'s primary checkout with \`mmi-cli worktree create ${foreign.repo}#${foreign.number} --claim\`.`
40389
+ );
40390
+ }
40391
+ }
40392
+ } catch (e) {
40393
+ console.warn(` warning: could not discover whether #${selector.number} belongs to another same-org repo (${e.message})`);
40394
+ }
40395
+ }
40396
+ const slug = o.slug ?? (issueTitle ? slugifyIssueTitle(issueTitle) : "");
40224
40397
  branch = buildNewBranchName(selector.number, slug ?? "");
40225
40398
  if (PROTECTED_BRANCHES2.has(branch)) {
40226
40399
  return fail(`worktree create: generated branch name '${branch}' collides with a protected train branch \u2014 pass a branch name instead`);
@@ -40281,8 +40454,13 @@ withExamples(mutating(
40281
40454
  }
40282
40455
  if (!resumed) {
40283
40456
  step = `git worktree add ${wtPath}`;
40457
+ const wtPathPreExisted = (0, import_node_fs47.existsSync)(wtPath);
40458
+ const partialRemove = worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout);
40284
40459
  await withWorktreeAddLock(repoRoot2, () => addWorktreeRobust(wtPath, branch, base, {
40285
- git: async (args) => (await execFileP2("git", args, { timeout: GH_MUTATION_TIMEOUT_MS })).stdout,
40460
+ // #4834: `-c core.longpaths=true` rides the add command itself a Windows worktree path
40461
+ // pushes long repos past MAX_PATH while the main checkout still fits. Command-scoped, so
40462
+ // the shared `.git/config` this lock serializes is never touched.
40463
+ git: async (args) => (await execFileP2("git", [...worktreeAddGitPrefix(), ...args], { timeout: GH_MUTATION_TIMEOUT_MS })).stdout,
40286
40464
  revParse: async (ref) => {
40287
40465
  try {
40288
40466
  return (await execFileP2("git", ["rev-parse", "--verify", ref], { timeout: GIT_TIMEOUT_MS })).stdout.trim() || void 0;
@@ -40291,6 +40469,13 @@ withExamples(mutating(
40291
40469
  }
40292
40470
  },
40293
40471
  deleteBranch: (b) => execFileP2("git", ["branch", "-D", b], { timeout: GIT_TIMEOUT_MS }).then(() => void 0),
40472
+ cleanupPartial: async () => {
40473
+ if (wtPathPreExisted || !(0, import_node_fs47.existsSync)(wtPath)) return;
40474
+ partialRemove.detachReparsePoints(wtPath);
40475
+ await execFileP2("git", ["worktree", "remove", "--force", wtPath], { timeout: GIT_TIMEOUT_MS }).catch(() => partialRemove.removeWorktreeDir(wtPath).then(() => void 0));
40476
+ await execFileP2("git", ["worktree", "prune"], { timeout: GIT_TIMEOUT_MS }).catch(() => {
40477
+ });
40478
+ },
40294
40479
  sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
40295
40480
  log: (m) => {
40296
40481
  if (!o.json) console.error(` ${m}`);
@@ -42318,6 +42503,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
42318
42503
  };
42319
42504
  }
42320
42505
  const boardAdvance = await advanceClosedIssuesToDone2(number, repoForPostCleanup);
42506
+ const crossRepoFilingIssue = repoForPostCleanup ? await reconcileCrossRepoFilingIssue(defaultGitHubClient(), repoForPostCleanup, Number(number)) : { status: "failed", error: "could not resolve the PR repo for cross-repo filing-issue reconciliation" };
42321
42507
  invalidateStatuslineBoardCache();
42322
42508
  console.log(JSON.stringify({
42323
42509
  ...buildPrMergeResultPayload({
@@ -42331,11 +42517,16 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
42331
42517
  // `boardAdvance` keeps its published array shape; `boardAdvanceStatus` is the field a caller reads to
42332
42518
  // tell "merged clean" from "merged, but the board did not follow" without guessing from the exit code.
42333
42519
  ...boardAdvance.entries.length ? { boardAdvance: boardAdvance.entries } : {},
42334
- boardAdvanceStatus: boardAdvance.status
42520
+ boardAdvanceStatus: boardAdvance.status,
42521
+ ...crossRepoFilingIssue.status !== "not-applicable" ? { crossRepoFilingIssue } : {}
42335
42522
  }));
42336
42523
  const boardAdvanceMessage = boardAdvanceFailureMessage(boardAdvance);
42337
42524
  if (boardAdvanceMessage) console.error(boardAdvanceMessage);
42525
+ if (crossRepoFilingIssue.status === "failed") {
42526
+ console.error(`pr merge: cross-repo filing-issue reconciliation failed (${crossRepoFilingIssue.error}) \u2014 the PR MERGED, but a foreign filing issue may remain open.`);
42527
+ }
42338
42528
  process.exitCode = boardAdvanceExitCode(boardAdvance) ?? process.exitCode;
42529
+ if (crossRepoFilingIssue.status === "failed") process.exitCode = 1;
42339
42530
  });
42340
42531
  registerQueryCommands(program2);
42341
42532
  registerWorktreeCommands(program2);
@@ -42475,6 +42666,21 @@ function renderAlignment(label, alignment) {
42475
42666
  }
42476
42667
  return `${label}: ALIGNMENT PR PENDING \u2014 land it with \`mmi-cli devops pr merge ${alignment.prNumber ?? "<number>"} --auto --merge\`${alignment.prUrl ? ` (${alignment.prUrl})` : ""}`;
42477
42668
  }
42669
+ var JERV_POWERTOOLS_REPO = "mutmutco/Jerv-PowerTools";
42670
+ async function runPostReleaseJervDoctor(repo) {
42671
+ if (repo.toLowerCase() !== JERV_POWERTOOLS_REPO.toLowerCase()) return void 0;
42672
+ try {
42673
+ await execJervCli(["doctor"], { timeout: 10 * 6e4 });
42674
+ return { status: "completed", note: "jerv-cli doctor completed on this owner machine" };
42675
+ } catch (e) {
42676
+ const err = e;
42677
+ const detail = (err.stderr?.trim() || err.message || "unknown doctor failure").split(/\r?\n/)[0];
42678
+ return {
42679
+ status: "reported-issues",
42680
+ note: `jerv-cli doctor ran but reported issues (${detail})`
42681
+ };
42682
+ }
42683
+ }
42478
42684
  function renderTrainApply(commandName, r) {
42479
42685
  let base = `mmi-cli ${commandName}: promoted ${r.repo} \u2014 ${r.stage} at ${r.tag} [${r.deployModel}]; ${renderDeployLine(r)}`;
42480
42686
  if (r.versionFold) base = `${base}; ${r.versionFold}`;
@@ -42493,6 +42699,9 @@ function renderTrainApply(commandName, r) {
42493
42699
  if (r.projectInfoSync) {
42494
42700
  base = `${base}; project info: ${r.projectInfoSync.note}`;
42495
42701
  }
42702
+ if (r.postReleaseJervDoctor) {
42703
+ base = `${base}; Jerv convergence: ${r.postReleaseJervDoctor.note}`;
42704
+ }
42496
42705
  if (r.announceNote) base = `${base}; announce: ${r.announceNote}`;
42497
42706
  if (r.releaseVerdict) {
42498
42707
  const v = r.releaseVerdict;
@@ -42646,6 +42855,7 @@ for (const commandName of ["rcand", "release"]) {
42646
42855
  const ack = (o.ack ?? "").split(",").map((s) => s.trim()).filter(Boolean);
42647
42856
  const result = await runTrainApply(commandName, trainApplyDeps(), { watch: o.watch, announceSummaryFile: o.announceSummaryFile, ack, dev: o.dev });
42648
42857
  let projectInfoSync;
42858
+ const postReleaseJervDoctor = commandName === "release" ? await runPostReleaseJervDoctor(result.repo) : void 0;
42649
42859
  if (commandName === "release") {
42650
42860
  try {
42651
42861
  projectInfoSync = await runProjectInfoSync(result.repo, true);
@@ -42672,6 +42882,7 @@ for (const commandName of ["rcand", "release"]) {
42672
42882
  const reported = {
42673
42883
  ...result,
42674
42884
  ...projectInfoSync ? { projectInfoSync } : {},
42885
+ ...postReleaseJervDoctor ? { postReleaseJervDoctor } : {},
42675
42886
  ...releaseVerdict ? { releaseVerdict } : {}
42676
42887
  };
42677
42888
  printLine(o.json ? JSON.stringify(reported, null, 2) : renderTrainApply(commandName, reported));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "3.119.0",
3
+ "version": "3.120.0",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",