@mutmutco/cli 4.3.2 → 4.3.3

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 +133 -35
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -5555,6 +5555,31 @@ function rewriteNegatedClosingPhrases(text) {
5555
5555
  (seg, i) => i % 2 === 1 ? seg : seg.replace(NEGATED_CLOSING_PHRASE_RE, (_m, plain, linked) => `leaves #${plain ?? linked} open`)
5556
5556
  ).join("");
5557
5557
  }
5558
+ var LINE_START_CLOSING_RE = /^\s*(?:[-*+]\s+)?(?:\*\*|__)?\s*(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:#\d+|\[#\d+\]\()/i;
5559
+ var FENCE_LINE_RE = /^\s*(?:```|~~~)/;
5560
+ function rewriteCloseKeywordProse(text) {
5561
+ if (!text.includes("#")) return { text, rewritten: [] };
5562
+ const rewritten = /* @__PURE__ */ new Set();
5563
+ let inFence = false;
5564
+ const lines = text.split("\n").map((line) => {
5565
+ if (FENCE_LINE_RE.test(line)) {
5566
+ inFence = !inFence;
5567
+ return line;
5568
+ }
5569
+ if (inFence || LINE_START_CLOSING_RE.test(line)) return line;
5570
+ CLOSING_MENTION_RE.lastIndex = 0;
5571
+ return line.split(/(`[^`\n]*`)/g).map((seg, i) => {
5572
+ if (i % 2 === 1) return seg;
5573
+ return seg.replace(CLOSING_MENTION_RE, (match, _keyword, plain, linked) => {
5574
+ const n = Number(plain ?? linked);
5575
+ if (!Number.isInteger(n) || n <= 0) return match;
5576
+ rewritten.add(n);
5577
+ return `part of #${n}`;
5578
+ });
5579
+ }).join("");
5580
+ });
5581
+ return rewritten.size ? { text: lines.join("\n"), rewritten: [...rewritten] } : { text, rewritten: [] };
5582
+ }
5558
5583
  function findClosingMentions(text) {
5559
5584
  const mentions = [];
5560
5585
  for (const match of text.matchAll(CLOSING_MENTION_RE)) {
@@ -5588,11 +5613,12 @@ var CROSS_REPO_TOKEN_RE = /\b([\w.-]+\/[\w.-]+)#(\d+)\b/g;
5588
5613
  function findCrossRepoAmbiguousClosings(text, closingNumbers, repo) {
5589
5614
  if (!closingNumbers.length) return [];
5590
5615
  const closing = new Set(closingNumbers);
5616
+ const bareClosing = new Set(findClosingMentions(text).map((m) => m.issue));
5591
5617
  const flagged = /* @__PURE__ */ new Set();
5592
5618
  const repoLower = repo.toLowerCase();
5593
5619
  for (const match of text.matchAll(CROSS_REPO_TOKEN_RE)) {
5594
5620
  const n = Number(match[2]);
5595
- if (!closing.has(n)) continue;
5621
+ if (!closing.has(n) || !bareClosing.has(n)) continue;
5596
5622
  if ((match[1] ?? "").toLowerCase() === repoLower) continue;
5597
5623
  flagged.add(n);
5598
5624
  }
@@ -11708,7 +11734,16 @@ async function gatherClaimLiveness(client, repo, number, fetchOpenPulls) {
11708
11734
  const out = { failed: [] };
11709
11735
  try {
11710
11736
  const comments = await client.restPaginate(`repos/${repo}/issues/${number}/comments`);
11711
- out.marker = latestClaimMarker(comments.map((comment) => ({ body: comment.body ?? "" })));
11737
+ const rows = comments.map((comment) => ({ body: comment.body ?? "", login: comment.user?.login ?? void 0 }));
11738
+ out.marker = latestClaimMarker(rows);
11739
+ if (out.marker) {
11740
+ for (let i = rows.length - 1; i >= 0; i -= 1) {
11741
+ if (latestClaimMarker([rows[i]])) {
11742
+ out.markerBy = rows[i].login;
11743
+ break;
11744
+ }
11745
+ }
11746
+ }
11712
11747
  out.markerAgeMs = out.marker ? Date.now() - Date.parse(out.marker.ts) : void 0;
11713
11748
  if (out.marker) {
11714
11749
  const state = probeLocalClaimSession(out.marker);
@@ -11775,17 +11810,26 @@ function laneOwnership(marker, current) {
11775
11810
  }
11776
11811
  return "unknown";
11777
11812
  }
11778
- async function checkLaneContest(client, item, actor = describeSessionIdentity()) {
11813
+ function laneResumeEvidence(evidence) {
11814
+ if (!evidence.marker || evidence.sessionState !== "dead" || evidence.failed.length > 0) return void 0;
11815
+ if (!isHostSessionId(evidence.marker.session)) return void 0;
11816
+ const artifacts = [evidence.openPr, evidence.branch ? `live branch ${evidence.branch}` : void 0].filter(Boolean);
11817
+ return `prior claim by lane ${describeClaimMarker(evidence.marker)} (@${evidence.markerBy}, ${formatClaimAge(evidence.markerAgeMs)} old) is verifiably not running on this host \u2014 its local session transcript is gone or stale${artifacts.length ? `; resuming over ${artifacts.join("; ")}` : ""}`;
11818
+ }
11819
+ async function checkLaneContest(client, item, actor = describeSessionIdentity(), viewerLogin) {
11779
11820
  const evidence = await gatherClaimLiveness(client, item.repository, item.number, openPullsFetcher(client));
11780
11821
  const ownership = laneOwnership(evidence.marker, actor);
11822
+ const sameOwner = Boolean(viewerLogin && evidence.markerBy && evidence.markerBy.toLowerCase() === viewerLogin.toLowerCase());
11823
+ const resume = ownership !== "mine" && sameOwner ? laneResumeEvidence(evidence) : void 0;
11781
11824
  const live = ownership === "mine" ? [] : liveEvidenceLines(evidence, item.repository);
11782
11825
  const unverifiable = ownership === "mine" ? [] : evidence.failed;
11783
11826
  return {
11784
- contested: ownership !== "mine" && (live.length > 0 || unverifiable.length > 0),
11827
+ contested: ownership !== "mine" && !resume && (live.length > 0 || unverifiable.length > 0),
11785
11828
  live,
11786
11829
  unverifiable,
11787
11830
  ownership,
11788
- marker: evidence.marker
11831
+ marker: evidence.marker,
11832
+ resume
11789
11833
  };
11790
11834
  }
11791
11835
  function laneContestMessage(ref, contest, verb) {
@@ -12084,8 +12128,9 @@ async function claimOneBoardItem(ctx, selector, options) {
12084
12128
  host: ctx.session.host
12085
12129
  };
12086
12130
  let previousHolder;
12087
- const claimedReceipt = () => previousHolder ? { outcome: "took-over", holder, previousHolder } : { outcome: "claimed", holder };
12088
- const heldReceipt = () => previousHolder ? { outcome: "took-over", holder, previousHolder } : { outcome: "held", holder };
12131
+ let resumeEvidence;
12132
+ const claimedReceipt = () => previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : { outcome: "took-over", holder, previousHolder } : { outcome: "claimed", holder };
12133
+ const heldReceipt = () => previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : { outcome: "took-over", holder, previousHolder } : { outcome: "held", holder };
12089
12134
  if (flatItem.contentType !== "Issue") throw new Error(`${flatItem.ref} is not an issue`);
12090
12135
  const pre = evaluateClaim(flatItem, assignedLogin);
12091
12136
  if (!pre.ok) throw new Error(pre.reason);
@@ -12096,17 +12141,25 @@ async function claimOneBoardItem(ctx, selector, options) {
12096
12141
  if (!verdict.ok) throw new Error(verdict.reason);
12097
12142
  item = fresh;
12098
12143
  const refuseIfContested = async () => {
12099
- const contest = await checkLaneContest(client, item, ctx.session);
12100
- if (!contest.contested) return;
12101
- if (!options.force) throw new Error(laneContestMessage(item.ref, contest, "claim"));
12102
- previousHolder = {
12144
+ const contest = await checkLaneContest(client, item, ctx.session, report.viewer);
12145
+ resumeEvidence = void 0;
12146
+ const displaced = () => ({
12103
12147
  login: assignedLogin,
12104
12148
  ...contest.marker ? {
12105
12149
  session: contest.marker.session,
12106
12150
  surface: contest.marker.surface,
12107
12151
  host: contest.marker.host
12108
12152
  } : {}
12109
- };
12153
+ });
12154
+ if (!contest.contested) {
12155
+ if (contest.resume && contest.marker) {
12156
+ resumeEvidence = contest.resume;
12157
+ previousHolder = displaced();
12158
+ }
12159
+ return;
12160
+ }
12161
+ if (!options.force) throw new Error(laneContestMessage(item.ref, contest, "claim"));
12162
+ previousHolder = displaced();
12110
12163
  };
12111
12164
  await refuseIfContested();
12112
12165
  if (verdict.alreadyClaimed) {
@@ -12176,7 +12229,7 @@ async function claimBoardIssues(options, deps = {}) {
12176
12229
  const ref = `${selector.repo}#${selector.number}`;
12177
12230
  try {
12178
12231
  const result = await claimOneBoardItem(ctx, selector, options);
12179
- results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, alreadyClaimed: result.alreadyClaimed, checked: result.checked };
12232
+ results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, resumeEvidence: result.resumeEvidence, alreadyClaimed: result.alreadyClaimed, checked: result.checked };
12180
12233
  } catch (e) {
12181
12234
  results[index] = { ref, claimed: false, reason: e.message };
12182
12235
  }
@@ -15322,10 +15375,10 @@ var rollout_plan_default = {
15322
15375
  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)."
15323
15376
  },
15324
15377
  baseline: {
15325
- version: "4.3.2",
15326
- tag: "v4.3.2",
15327
- commit: "c4aa995c1dfc",
15328
- npm: "@mutmutco/cli@4.3.2"
15378
+ version: "4.3.3",
15379
+ tag: "v4.3.3",
15380
+ commit: "851ec4c56e34",
15381
+ npm: "@mutmutco/cli@4.3.3"
15329
15382
  },
15330
15383
  exitCriterion: "fleet-n-of-n",
15331
15384
  hubOnlyShortcut: "forbidden",
@@ -15342,14 +15395,14 @@ var rollout_plan_default = {
15342
15395
  repo: "mutmutco/mmi-hub",
15343
15396
  role: "canary",
15344
15397
  schedule: "train",
15345
- v3Target: "v4.3.2"
15398
+ v3Target: "v4.3.3"
15346
15399
  }
15347
15400
  ],
15348
15401
  rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
15349
15402
  rollback: {
15350
15403
  independent: true,
15351
- mechanism: "npm dist-tag latest -> 4.3.2 and redeploy the Hub Lambda from tag v4.3.2 (c4aa995c1dfc); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15352
- v3Target: "v4.3.2 (@mutmutco/cli@4.3.2, tag commit c4aa995c1dfc \u2014 last known-good release carrying the repo-index v4-only contract)"
15404
+ mechanism: "npm dist-tag latest -> 4.3.3 and redeploy the Hub Lambda from tag v4.3.3 (851ec4c56e34); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
15405
+ v3Target: "v4.3.3 (@mutmutco/cli@4.3.3, tag commit 851ec4c56e34 \u2014 last known-good release carrying the repo-index v4-only contract)"
15353
15406
  }
15354
15407
  },
15355
15408
  {
@@ -23765,19 +23818,23 @@ function registerBoardCommands(program3) {
23765
23818
  if (result.checked) {
23766
23819
  if (result.outcome === "held") return `Check ${ref}: held by ${holder} - claim would renew the lease (nothing written)`;
23767
23820
  if (result.outcome === "took-over") return `Check ${ref}: held by ${previousHolder} - --force claim would take it over for ${holder} (nothing written)`;
23821
+ if (result.outcome === "resumed") return `Check ${ref}: prior lane ${previousHolder} is verifiably dead - claim would resume its work for ${holder} (nothing written; ${result.resumeEvidence})`;
23768
23822
  return `Check ${ref}: free - claim would proceed for ${holder} (nothing written)`;
23769
23823
  }
23770
23824
  if (result.partial) {
23771
- return result.outcome === "took-over" ? `Partially took over ${ref} from ${previousHolder}: ${result.warning}` : `Partially claimed ${ref} for ${holder}: ${result.warning}`;
23825
+ if (result.outcome === "took-over") return `Partially took over ${ref} from ${previousHolder}: ${result.warning}`;
23826
+ if (result.outcome === "resumed") return `Partially resumed ${ref} from ${previousHolder}: ${result.warning}`;
23827
+ return `Partially claimed ${ref} for ${holder}: ${result.warning}`;
23772
23828
  }
23773
23829
  if (result.outcome === "took-over") return `Took over ${ref} from ${previousHolder} for ${holder} - In Progress`;
23830
+ if (result.outcome === "resumed") return `Resumed ${ref} from ${previousHolder} for ${holder} - In Progress (${result.resumeEvidence})`;
23774
23831
  if (result.outcome === "held") return `${ref} is held by ${holder} - In Progress`;
23775
23832
  return `Claimed ${ref} for ${holder} - In Progress`;
23776
23833
  }
23777
23834
  const board = program3.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
23778
23835
  board.command("read", { isDefault: true }).alias("list").description("read the board and print user-owned, claimable, and taken items").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--direct", "bypass the Hub snapshot and read the live board through direct GitHub GraphQL").option("--bundle-details", "fetch body/comments only for user-owned and claimable issues").option("--bodies", "fetch body/comments for EVERY scoped row, including taken and unowned in-flight ones \u2014 for consumers that scope by Status rather than ownership (#4861); implies --bundle-details and costs one extra read per row").option("--allow-partial", "return partial board results when later page/detail reads fail").option("--out <path>", "write the output to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5802)").addHelpText("after", "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nNever capture the JSON with a shell redirect on Windows: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n").action((o) => runBoardRead(o));
23779
23836
  withExamples(mutating(
23780
- board.command("claim <issues...>").description("claim issues: assign them and move their Project v2 Status to In Progress \u2014 idempotent, so an item already yours and In Progress succeeds unchanged (one or more refs)").addHelpText("after", "\nevery claim stamps a lane-identity marker comment on the issue (`<!-- mmi-claim: \u2026 -->`,\nsurface/session@host) so other agents can attribute the hold (#3727). The session is the\nhost-exported id when the surface provides one, otherwise a per-process `synth-` fallback \u2014\na claim is never anonymous (#5245). `board show`, doctor and unclaim read the latest marker.\n").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--force", "take an item already claimed by another lane that shows live evidence of active work (#3727)").option("--check", "read-only: run every claim gate and report the verdict, writing nothing \u2014 exits 1 with the same refusal a real claim would raise (#4511)").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails"),
23837
+ board.command("claim <issues...>").description("claim issues: assign them and move their Project v2 Status to In Progress \u2014 idempotent, so an item already yours and In Progress succeeds unchanged (one or more refs)").addHelpText("after", "\nevery claim stamps a lane-identity marker comment on the issue (`<!-- mmi-claim: \u2026 -->`,\nsurface/session@host) so other agents can attribute the hold (#3727). The session is the\nhost-exported id when the surface provides one, otherwise a per-process `synth-` fallback \u2014\na claim is never anonymous (#5245). `board show`, doctor and unclaim read the latest marker.\n\nsame-owner resume (#6035): when the prior marker was posted by YOUR login on THIS host and its\nlocal session is verifiably dead (transcript probe), the claim proceeds as a resume without\n--force and names the evidence. A live, foreign-host, or unprobeable prior lane still refuses.\n").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--force", "take an item already claimed by another lane that shows live evidence of active work (#3727)").option("--check", "read-only: run every claim gate and report the verdict, writing nothing \u2014 exits 1 with the same refusal a real claim would raise (#4511)").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails"),
23781
23838
  (_opts, args) => ({ command: "board claim", issues: args[0] ?? [] })
23782
23839
  ).action(async (issueRefs, o) => {
23783
23840
  if (issueRefs.length === 1) {
@@ -35616,7 +35673,8 @@ async function mergeAutoEnqueueWithBody(prNumber, args, method, io, bodyFile) {
35616
35673
  return confirmEnqueueOutcome();
35617
35674
  }
35618
35675
  function cleanupGitArgs(cwd, args) {
35619
- return cwd ? ["-C", cwd, ...args] : args;
35676
+ if (!cwd || args[0] === "-C") return args;
35677
+ return ["-C", cwd, ...args];
35620
35678
  }
35621
35679
  async function remoteBranchExists2(branch, options = {}) {
35622
35680
  if (!branch) return void 0;
@@ -36028,6 +36086,24 @@ async function checkDocsIndexAtHead(opts, deps) {
36028
36086
  };
36029
36087
  }
36030
36088
 
36089
+ // src/pr-create-claim-guard.ts
36090
+ async function prCreateClaimRefusal(body, repoOption, deps = {}) {
36091
+ const issues = [...new Set(findClosingMentions(body).map((mention) => mention.issue))];
36092
+ if (!issues.length) return void 0;
36093
+ const repo = await requireRepo(repoOption);
36094
+ const client = deps.client ?? defaultGitHubClient();
36095
+ const actor = deps.actor ?? describeSessionIdentity();
36096
+ const checkContest = deps.checkContest ?? checkLaneContest;
36097
+ for (const number of issues) {
36098
+ const contest = await checkContest(client, { repository: repo, number }, actor);
36099
+ if (!contest.contested) continue;
36100
+ const ref = `${repo}#${number}`;
36101
+ const holder = contest.marker ? `lane ${describeClaimMarker(contest.marker)}` : "another lane";
36102
+ return `pr create: REFUSED \u2014 ${ref} is held by ${holder} with live or unreadable work evidence; run \`mmi-cli oracle board claim ${ref} --force\` before creating this PR`;
36103
+ }
36104
+ return void 0;
36105
+ }
36106
+
36031
36107
  // src/worktree-merge-cleanup.ts
36032
36108
  var import_node_fs40 = require("node:fs");
36033
36109
  var import_node_path37 = require("node:path");
@@ -36481,6 +36557,10 @@ async function removeWorktreeWithReconcile(wtPath, git3, listWorktrees, pathExis
36481
36557
  })
36482
36558
  };
36483
36559
  }
36560
+ function isPrMergeWorktreePartial(cleanup) {
36561
+ const worktree = cleanup?.worktree;
36562
+ return Boolean(worktree?.path && worktree.status !== "removed" && worktree.status !== "preserved");
36563
+ }
36484
36564
  function prMergeLocalCleanupExitCode(cleanup) {
36485
36565
  return cleanup?.worktree?.status === "failed" || cleanup?.localBranch?.status === "failed" ? 1 : void 0;
36486
36566
  }
@@ -37331,10 +37411,20 @@ ${list}`);
37331
37411
  }
37332
37412
  body = normalizeClosingDirectives(body);
37333
37413
  body = rewriteNegatedClosingPhrases(body);
37414
+ const prose = rewriteCloseKeywordProse(body);
37415
+ if (prose.rewritten.length) {
37416
+ body = prose.text;
37417
+ process.stderr.write(
37418
+ `pr create: WARNING \u2014 rewrote close-keyword prose to "part of #N" for ${prose.rewritten.map((n) => `#${n}`).join(", ")}: GitHub parses close/fix/resolve + #N even mid-sentence (#6039). Keep closing keywords on a dedicated "Closes #N" line.
37419
+ `
37420
+ );
37421
+ }
37334
37422
  const docsCheck = await checkDocsIndexAtHead({ repo: o.repo, head: o.head }, createPrCreateDocsIndexDeps());
37335
37423
  if (docsCheck && !docsCheck.ok) {
37336
37424
  return fail(`pr create: ${docsCheck.detail} \u2014 ${docsCheck.fix}`);
37337
37425
  }
37426
+ const claimRefusal = await prCreateClaimRefusal(body, o.repo);
37427
+ if (claimRefusal) return fail(claimRefusal);
37338
37428
  const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo, draft: o.draft }));
37339
37429
  if (isGhCreateRateLimited(created)) {
37340
37430
  console.log(JSON.stringify(created));
@@ -37349,7 +37439,8 @@ ${list}`);
37349
37439
  ], [
37350
37440
  "--head and --base default to the current branch and the repo default; only pass them to override.",
37351
37441
  "Use --body-file for multiline PR bodies instead of shell-escaped inline markdown.",
37352
- "Write that file under .jerv/ inside the host workspace (#4405); host policy governs untracked files."
37442
+ "Write that file under .jerv/ inside the host workspace (#4405); host policy governs untracked files.",
37443
+ 'Keep closing keywords on a dedicated line ("Closes #N") \u2014 GitHub parses close/fix/resolve + #N even mid-sentence, so pr create rewrites such prose to "part of #N" (#6039).'
37353
37444
  ]);
37354
37445
  pr.command("view <ref>").description("read a PR as structured JSON (merged state, head/base, URL, merge commit) \u2014 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 \u2014 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) \u2014 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 (ref, o) => {
37355
37446
  let parsed;
@@ -37738,7 +37829,7 @@ ${list}`);
37738
37829
  else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
37739
37830
  if (result.status === "failed") process.exitCode = 1;
37740
37831
  });
37741
- jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").addOption(new Option("--disable-auto", "disable a queued auto-merge without merging").conflicts(["auto", "wait", "squash", "merge", "rebase", "preserveWorktree", "gc", "squashBodyFile", "force"])).option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--squash-body-file <path>", "squash commit body (overrides GitHub COMMIT_MESSAGES); use when a pushed commit mentions close/fix/resolve + #N that must not close (#5723)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal")).action(async (number, o) => {
37832
+ jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").addOption(new Option("--disable-auto", "disable a queued auto-merge without merging").conflicts(["auto", "wait", "squash", "merge", "rebase", "preserveWorktree", "gc", "squashBodyFile", "force"])).option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m) \u2014 run as a background/monitor task or under a shell timeout above that budget; a short foreground timeout (e.g. 120s) kills it after checks pass and leaves the PR open (#6027)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--squash-body-file <path>", "squash commit body (overrides GitHub COMMIT_MESSAGES); use when a pushed commit mentions close/fix/resolve + #N that must not close (#5723)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal")).action(async (number, o) => {
37742
37833
  const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
37743
37834
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
37744
37835
  if (o.disableAuto) {
@@ -37802,6 +37893,7 @@ ${list}`);
37802
37893
  const ciHeadRef = repoForPostCleanup ? await prHeadRefForCiProbe(number, repoForPostCleanup) : void 0;
37803
37894
  const ciPolicy = await resolveMergeCiPolicyForCheckout(o.repo, ciHeadRef);
37804
37895
  if (o.wait) {
37896
+ console.warn(`pr merge: --wait can hold the shell up to the full ${PR_CHECKS_TIMEOUT_MS / 6e4}m budget plus the merge \u2014 run under a shell timeout above that budget or as a background/monitor task (#6027)`);
37805
37897
  const repo = await requireRepo(o.repo);
37806
37898
  const budgetMs = PR_CHECKS_TIMEOUT_MS;
37807
37899
  const waitStarted = Date.now();
@@ -37995,12 +38087,13 @@ ${list}`);
37995
38087
  execGit: async (args) => (await execFileP("git", cleanupGitArgs(primaryRoot, args), { timeout: GIT_TIMEOUT_MS })).stdout,
37996
38088
  branchExists: (b) => remoteBranchExists2(b, { cwd: primaryRoot })
37997
38089
  });
37998
- const worktreePartial = localCleanup?.worktree?.path && localCleanup.worktree.status !== "removed" ? {
38090
+ const worktree = localCleanup?.worktree;
38091
+ const worktreePartial = isPrMergeWorktreePartial(localCleanup) && worktree?.path ? {
37999
38092
  kind: "worktree-directory",
38000
- path: localCleanup.worktree.path,
38001
- reason: localCleanup.worktree.reason ?? localCleanup.worktree.status,
38002
- error: localCleanup.worktree.error ?? localCleanup.worktree.residueError,
38003
- remediation: localCleanup.worktree.remediation ?? `Remove-Item -LiteralPath '${localCleanup.worktree.path?.replace(/'/g, "''")}' -Recurse -Force`
38093
+ path: worktree.path,
38094
+ reason: worktree.reason ?? worktree.status,
38095
+ error: worktree.error ?? worktree.residueError,
38096
+ remediation: worktree.remediation ?? `Remove-Item -LiteralPath '${worktree.path.replace(/'/g, "''")}' -Recurse -Force`
38004
38097
  } : void 0;
38005
38098
  const partialCleanup = [
38006
38099
  ...worktreePartial ? [worktreePartial] : [],
@@ -38345,11 +38438,16 @@ function registerBoxCommands(program3) {
38345
38438
  await failGraceful("runtime box get: --script requires --ssh (it writes the ssh connect recipe)");
38346
38439
  return;
38347
38440
  }
38348
- if (o.json) console.log(JSON.stringify({ box: found, incomplete }, null, 2));
38349
- else if (o.ssh && o.script) {
38350
- (0, import_node_fs42.writeFileSync)(o.script, sshRecipeScript(found), "utf8");
38351
- console.log(`wrote ${o.script} \u2014 run: bash "${o.script}"`);
38352
- } else if (o.ssh) console.log(`${formatSshRecipe(found)}
38441
+ const wroteScript = o.ssh && o.script ? o.script : null;
38442
+ if (wroteScript) (0, import_node_fs42.writeFileSync)(wroteScript, sshRecipeScript(found), "utf8");
38443
+ if (o.json) {
38444
+ console.log(JSON.stringify({
38445
+ box: found,
38446
+ incomplete,
38447
+ ...wroteScript ? { script: { path: wroteScript, run: `bash "${wroteScript}"` } } : {}
38448
+ }, null, 2));
38449
+ } else if (wroteScript) console.log(`wrote ${wroteScript} \u2014 run: bash "${wroteScript}"`);
38450
+ else if (o.ssh) console.log(`${formatSshRecipe(found)}
38353
38451
  ${SSH_RECIPE_AGENT_NOTE}`);
38354
38452
  else console.log(formatBoxTable([found]));
38355
38453
  if (!o.json) warnIncomplete2(incomplete);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.3.2",
3
+ "version": "4.3.3",
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",