@mutmutco/cli 4.1.15 → 4.1.17

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 +462 -104
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -13181,10 +13181,10 @@ var rollout_plan_default = {
13181
13181
  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)."
13182
13182
  },
13183
13183
  baseline: {
13184
- version: "4.1.15",
13185
- tag: "v4.1.15",
13186
- commit: "ed45d37a071b",
13187
- npm: "@mutmutco/cli@4.1.15"
13184
+ version: "4.1.17",
13185
+ tag: "v4.1.17",
13186
+ commit: "65f193ccd344",
13187
+ npm: "@mutmutco/cli@4.1.17"
13188
13188
  },
13189
13189
  exitCriterion: "fleet-n-of-n",
13190
13190
  hubOnlyShortcut: "forbidden",
@@ -13201,14 +13201,14 @@ var rollout_plan_default = {
13201
13201
  repo: "mutmutco/mmi-hub",
13202
13202
  role: "canary",
13203
13203
  schedule: "train",
13204
- v3Target: "v4.1.15"
13204
+ v3Target: "v4.1.17"
13205
13205
  }
13206
13206
  ],
13207
13207
  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.",
13208
13208
  rollback: {
13209
13209
  independent: true,
13210
- mechanism: "npm dist-tag latest -> 4.1.15 and redeploy the Hub Lambda from tag v4.1.15 (ed45d37a071b); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
13211
- v3Target: "v4.1.15 (@mutmutco/cli@4.1.15, tag commit ed45d37a071b \u2014 last known-good release carrying the repo-index v4-only contract)"
13210
+ mechanism: "npm dist-tag latest -> 4.1.17 and redeploy the Hub Lambda from tag v4.1.17 (65f193ccd344); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
13211
+ v3Target: "v4.1.17 (@mutmutco/cli@4.1.17, tag commit 65f193ccd344 \u2014 last known-good release carrying the repo-index v4-only contract)"
13212
13212
  }
13213
13213
  },
13214
13214
  {
@@ -14193,7 +14193,8 @@ async function runPrForIssue(deps, issueRef, opts) {
14193
14193
  return extractPrForIssueResponse(resp);
14194
14194
  }
14195
14195
  async function runIssueViewContext(deps, opts) {
14196
- const fields = opts.comments || opts.context ? ensureField(opts.jsonFields, "comments") : opts.jsonFields;
14196
+ const baseFields = opts.context ? opts.jsonFields.split(",").map((field) => field.trim()).filter((field) => field !== "linkedPrs" && field !== "children").join(",") : opts.jsonFields;
14197
+ const fields = opts.comments || opts.context ? ensureField(baseFields, "comments") : baseFields;
14197
14198
  const base = await deps.ghJson(["issue", "view", String(opts.number), "--repo", opts.repo, "--json", fields], GH_LIST_TIMEOUT_MS);
14198
14199
  if (!opts.context) return base;
14199
14200
  const result = { ...base };
@@ -20410,13 +20411,44 @@ function crossRepoAmbiguousRefusalMessage(ambiguous, repo, context = "pr merge")
20410
20411
  const first = `#${ambiguous[0] ?? "N"}`;
20411
20412
  return `${context}: REFUSED \u2014 GitHub will close ${named} on merge (a bare \`#N\` after a closing keyword always means the repo the PR lives in), but this body also mentions ${first} qualified against a DIFFERENT repo elsewhere \u2014 check whether that closing keyword was meant to carry an explicit owner/repo#N qualifier instead. Reword the closing reference with the full owner/repo#N form, or re-run with --force to merge anyway and let ${named} close.`;
20412
20413
  }
20413
- function commitMessagesText(commits) {
20414
- if (!Array.isArray(commits)) return "";
20414
+ function parsedCommits(commits) {
20415
+ if (!Array.isArray(commits)) return [];
20415
20416
  return commits.map((commit) => {
20416
20417
  const { messageHeadline, messageBody } = commit ?? {};
20417
- return `${typeof messageHeadline === "string" ? messageHeadline : ""}
20418
- ${typeof messageBody === "string" ? messageBody : ""}`;
20419
- }).join("\n");
20418
+ return {
20419
+ headline: typeof messageHeadline === "string" ? messageHeadline : "",
20420
+ body: typeof messageBody === "string" ? messageBody : ""
20421
+ };
20422
+ });
20423
+ }
20424
+ function commitMessagesText(commits) {
20425
+ return parsedCommits(commits).map((commit) => `${commit.headline}
20426
+ ${commit.body}`).join("\n");
20427
+ }
20428
+ function defaultSquashBodyFromCommits(commits) {
20429
+ if (commits.length <= 1) {
20430
+ const only = commits[0];
20431
+ if (!only) return "";
20432
+ const body = only.body.trim();
20433
+ return body || only.headline.trim();
20434
+ }
20435
+ return commits.map((commit) => `${commit.headline}
20436
+
20437
+ ${commit.body}`.trim()).filter(Boolean).join("\n\n");
20438
+ }
20439
+ function rewriteUnregisteredClosingKeywords(text, allowedClosing) {
20440
+ if (!text.includes("#")) return text;
20441
+ const allowed = new Set(allowedClosing);
20442
+ CLOSING_MENTION_RE.lastIndex = 0;
20443
+ const segments = text.split(/(```[\s\S]*?```|~~~[\s\S]*?~~~|`[^`\n]*`)/g);
20444
+ return segments.map((seg, i) => {
20445
+ if (i % 2 === 1) return seg;
20446
+ return seg.replace(CLOSING_MENTION_RE, (match, _keyword, plain, linked) => {
20447
+ const n = Number(plain ?? linked);
20448
+ if (!Number.isInteger(n) || n <= 0 || allowed.has(n)) return match;
20449
+ return `part of #${n}`;
20450
+ });
20451
+ }).join("");
20420
20452
  }
20421
20453
  function parseClosingGuardInput(raw, repo) {
20422
20454
  if (!raw || typeof raw !== "object") return void 0;
@@ -20428,17 +20460,25 @@ function parseClosingGuardInput(raw, repo) {
20428
20460
  if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) return void 0;
20429
20461
  closing.push(n);
20430
20462
  }
20463
+ const parsed = Array.isArray(commits) ? parsedCommits(commits) : [];
20431
20464
  const commitClosing = [...new Set(findClosingMentions(commitMessagesText(commits)).map((m) => m.issue))];
20432
20465
  const text = `${typeof title === "string" ? title : ""}
20433
20466
  ${typeof body === "string" ? body : ""}`;
20434
- return { state, text, closing, commitClosing, repo };
20467
+ return { state, text, closing, commitClosing, commits: parsed, repo };
20435
20468
  }
20436
- function restCommitMessagesText(commits) {
20437
- if (!Array.isArray(commits)) return "";
20469
+ function parsedRestCommits(commits) {
20470
+ if (!Array.isArray(commits)) return [];
20438
20471
  return commits.map((entry) => {
20439
20472
  const message = (entry ?? {}).commit?.message;
20440
- return typeof message === "string" ? message : "";
20441
- }).join("\n");
20473
+ if (typeof message !== "string" || !message) return { headline: "", body: "" };
20474
+ const nl = message.indexOf("\n");
20475
+ if (nl === -1) return { headline: message, body: "" };
20476
+ return { headline: message.slice(0, nl), body: message.slice(nl + 1).replace(/^\n/, "") };
20477
+ });
20478
+ }
20479
+ function restCommitMessagesText(commits) {
20480
+ return parsedRestCommits(commits).map((commit) => `${commit.headline}
20481
+ ${commit.body}`).join("\n");
20442
20482
  }
20443
20483
  function parseRestClosingGuardInput(prRaw, commitsRaw, repo) {
20444
20484
  if (!prRaw || typeof prRaw !== "object") return void 0;
@@ -20446,10 +20486,11 @@ function parseRestClosingGuardInput(prRaw, commitsRaw, repo) {
20446
20486
  if (typeof state !== "string") return void 0;
20447
20487
  const bodyText = typeof body === "string" ? body : "";
20448
20488
  const closing = [...new Set(findClosingMentions(bodyText).map((m) => m.issue))];
20489
+ const parsed = parsedRestCommits(commitsRaw);
20449
20490
  const commitClosing = [...new Set(findClosingMentions(restCommitMessagesText(commitsRaw)).map((m) => m.issue))];
20450
20491
  const text = `${typeof title === "string" ? title : ""}
20451
20492
  ${bodyText}`;
20452
- return { state: merged === true ? "MERGED" : state.toUpperCase(), text, closing, commitClosing, repo };
20493
+ return { state: merged === true ? "MERGED" : state.toUpperCase(), text, closing, commitClosing, commits: parsed, repo };
20453
20494
  }
20454
20495
  function negatedClosingRefusalMessage(negated, context = "pr merge") {
20455
20496
  const named = negated.map((n) => `#${n}`).join(", ");
@@ -20459,7 +20500,7 @@ function negatedClosingRefusalMessage(negated, context = "pr merge") {
20459
20500
  function commitClosingRefusalMessage(closing, context = "pr merge") {
20460
20501
  const named = closing.map((n) => `#${n}`).join(", ");
20461
20502
  const first = `#${closing[0] ?? "N"}`;
20462
- return `${context}: REFUSED \u2014 commit messages will close ${named} through the squash body, but GitHub omitted ${named} from closingIssuesReferences. Remove close/fix/resolve + ${first} from the commit message (if the issue must stay open, use "Part of ${first}" / "Refs ${first}" / "leaves ${first} open" \u2014 never "does not close ${first}"), or re-run with --force to merge anyway and let ${named} close.`;
20503
+ return `${context}: REFUSED \u2014 commit messages will close ${named} through the squash body, but GitHub omitted ${named} from closingIssuesReferences. Remove close/fix/resolve + ${first} from the commit message (if the issue must stay open, use "Part of ${first}" / "Refs ${first}" / "leaves ${first} open" \u2014 never "does not close ${first}"), edit the squash body at merge time (--squash-body-file), or re-run with --force to merge anyway and let ${named} close.`;
20463
20504
  }
20464
20505
  function alreadyClosedCommitClosingMessage(closed, context = "pr merge") {
20465
20506
  const named = closed.map((n) => `#${n}`).join(", ");
@@ -20481,8 +20522,9 @@ function evaluateClosingGuard(input, opts) {
20481
20522
  };
20482
20523
  }
20483
20524
  if (input.state !== "OPEN") return { blocked: false };
20525
+ const squashBodyClosing = opts.squashBodyText ? [...new Set(findClosingMentions(opts.squashBodyText).map((m) => m.issue))] : void 0;
20484
20526
  const { open: commitClosing, skippedClosed } = partitionCommitClosings(
20485
- input.commitClosing ?? [],
20527
+ squashBodyClosing ?? input.commitClosing ?? [],
20486
20528
  input.closing,
20487
20529
  input.alreadyClosed
20488
20530
  );
@@ -25225,9 +25267,35 @@ function repoIndexPathTieBreak(path2) {
25225
25267
  if (/\.(test|spec)\.[^.]+$/i.test(p)) return 1;
25226
25268
  return 0;
25227
25269
  }
25270
+ var LOCAL_REPO_INDEX_REFRESH_BUDGET_MS = 2e3;
25271
+ var LOCAL_REPO_INDEX_DELTA_FILE_LIMIT = 50;
25228
25272
  function repoIndexStorePath(cwd) {
25229
25273
  return repoRuntimeStatePath(cwd, "repo-index", "index.json");
25230
25274
  }
25275
+ function localRepoIndexUsagePath(cwd) {
25276
+ return repoRuntimeStatePath(cwd, "repo-index", "local-usage.json");
25277
+ }
25278
+ function recordLocalRepoIndexQueryUsage(cwd, now = /* @__PURE__ */ new Date()) {
25279
+ const store = localRepoIndexUsagePath(cwd);
25280
+ let previous = 0;
25281
+ try {
25282
+ const raw = JSON.parse((0, import_node_fs24.readFileSync)(store, "utf8"));
25283
+ if (typeof raw.count === "number" && Number.isFinite(raw.count)) previous = raw.count;
25284
+ } catch {
25285
+ }
25286
+ (0, import_node_fs24.mkdirSync)((0, import_node_path21.dirname)(store), { recursive: true });
25287
+ (0, import_node_fs24.writeFileSync)(store, `${JSON.stringify({ count: previous + 1, updatedAt: now.toISOString() }, null, 2)}
25288
+ `, "utf8");
25289
+ }
25290
+ function readLocalRepoIndexQueryUsage(cwd) {
25291
+ try {
25292
+ const raw = JSON.parse((0, import_node_fs24.readFileSync)(localRepoIndexUsagePath(cwd), "utf8"));
25293
+ if (typeof raw.count !== "number" || !Number.isFinite(raw.count)) return null;
25294
+ return { count: raw.count, updatedAt: raw.updatedAt ?? "" };
25295
+ } catch {
25296
+ return null;
25297
+ }
25298
+ }
25231
25299
  function isHardDeniedPath(relPosix) {
25232
25300
  return isHardDeniedRepoIndexPath(relPosix);
25233
25301
  }
@@ -25338,14 +25406,41 @@ function listCandidatePaths(cwd, exec = import_node_child_process12.execFileSync
25338
25406
  return [];
25339
25407
  }
25340
25408
  }
25341
- function rebuildRepoIndex(cwd, repoSlug3) {
25342
- const candidates = listCandidatePaths(cwd).filter(isIndexablePath);
25343
- const ignored = defaultIsIgnored(cwd, candidates);
25344
- const readmeHints = loadReadmeHints(cwd, candidates.filter((p) => !ignored.has(p)));
25345
- const entries = [];
25409
+ var RepoIndexRefreshBudgetError = class extends Error {
25410
+ constructor() {
25411
+ super(`local index refresh exceeded ${LOCAL_REPO_INDEX_REFRESH_BUDGET_MS}ms`);
25412
+ }
25413
+ };
25414
+ function assertRefreshBudget(deadline) {
25415
+ if (deadline !== void 0 && Date.now() >= deadline) throw new RepoIndexRefreshBudgetError();
25416
+ }
25417
+ function deadlineExec(deadline) {
25418
+ return ((file, args, options) => {
25419
+ assertRefreshBudget(deadline);
25420
+ return (0, import_node_child_process12.execFileSync)(file, args, {
25421
+ ...options,
25422
+ timeout: Math.max(1, deadline - Date.now()),
25423
+ windowsHide: true
25424
+ });
25425
+ });
25426
+ }
25427
+ function gitHead(cwd, exec = import_node_child_process12.execFileSync) {
25428
+ try {
25429
+ return String(exec("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8", windowsHide: true })).trim() || void 0;
25430
+ } catch (error) {
25431
+ if (error instanceof RepoIndexRefreshBudgetError) throw error;
25432
+ return void 0;
25433
+ }
25434
+ }
25435
+ function sourceFilesForIndex(cwd, deadline) {
25436
+ const exec = deadline === void 0 ? import_node_child_process12.execFileSync : deadlineExec(deadline);
25437
+ const candidates = listCandidatePaths(cwd, exec).filter(isIndexablePath);
25438
+ assertRefreshBudget(deadline);
25439
+ const ignored = defaultIsIgnored(cwd, candidates, exec);
25440
+ const files = [];
25346
25441
  for (const rel of candidates) {
25347
- if (ignored.has(rel)) continue;
25348
- if (isHardDeniedPath(rel)) continue;
25442
+ assertRefreshBudget(deadline);
25443
+ if (ignored.has(rel) || isHardDeniedPath(rel)) continue;
25349
25444
  const abs = (0, import_node_path21.join)(cwd, ...rel.split("/"));
25350
25445
  if (!(0, import_node_fs24.existsSync)(abs)) continue;
25351
25446
  let text;
@@ -25355,31 +25450,118 @@ function rebuildRepoIndex(cwd, repoSlug3) {
25355
25450
  continue;
25356
25451
  }
25357
25452
  if (text.length > 15e5) continue;
25358
- const hash = (0, import_node_crypto6.createHash)("sha256").update(text).digest("hex").slice(0, 16);
25359
- const symbols = extractSymbols(text);
25360
- const docBlurb = extractModuleBlurb(text);
25361
- const top = rel.includes("/") ? rel.split("/")[0] : "";
25362
- const hint = docBlurb ?? (top ? readmeHints.get(top) : void 0) ?? readmeHints.get("");
25363
- entries.push({
25453
+ files.push({
25364
25454
  path: rel,
25365
- hash,
25366
- symbols,
25367
- ...hint ? { blurb: hint.slice(0, BLURB_CAP) } : {}
25455
+ hash: (0, import_node_crypto6.createHash)("sha256").update(text).digest("hex").slice(0, 16),
25456
+ text
25368
25457
  });
25369
25458
  }
25459
+ return files;
25460
+ }
25461
+ function entryForSource(file, readmeHints) {
25462
+ const docBlurb = extractModuleBlurb(file.text);
25463
+ const top = file.path.includes("/") ? file.path.split("/")[0] : "";
25464
+ const hint = docBlurb ?? (top ? readmeHints.get(top) : void 0) ?? readmeHints.get("");
25465
+ return {
25466
+ path: file.path,
25467
+ hash: file.hash,
25468
+ symbols: extractSymbols(file.text),
25469
+ ...hint ? { blurb: hint.slice(0, BLURB_CAP) } : {}
25470
+ };
25471
+ }
25472
+ function buildRepoIndexProjection(cwd, repoSlug3, deadline) {
25473
+ const files = sourceFilesForIndex(cwd, deadline);
25474
+ assertRefreshBudget(deadline);
25475
+ const readmeHints = loadReadmeHints(cwd, files.map((file) => file.path));
25476
+ const entries = files.map((file) => {
25477
+ assertRefreshBudget(deadline);
25478
+ return entryForSource(file, readmeHints);
25479
+ });
25370
25480
  entries.sort((a, b) => a.path.localeCompare(b.path));
25371
- const projection = {
25481
+ const exec = deadline === void 0 ? import_node_child_process12.execFileSync : deadlineExec(deadline);
25482
+ const indexHead = gitHead(cwd, exec);
25483
+ return {
25372
25484
  schema: REPO_INDEX_SCHEMA,
25373
25485
  repo: repoSlug3,
25374
25486
  builtAt: (/* @__PURE__ */ new Date()).toISOString(),
25487
+ ...indexHead ? { indexHead } : {},
25375
25488
  entries
25376
25489
  };
25490
+ }
25491
+ function writeRepoIndex(cwd, projection) {
25377
25492
  const store = repoIndexStorePath(cwd);
25378
25493
  (0, import_node_fs24.mkdirSync)((0, import_node_path21.dirname)(store), { recursive: true });
25379
25494
  (0, import_node_fs24.writeFileSync)(store, `${JSON.stringify(projection, null, 2)}
25380
25495
  `, "utf8");
25496
+ }
25497
+ function rebuildRepoIndex(cwd, repoSlug3) {
25498
+ const projection = buildRepoIndexProjection(cwd, repoSlug3);
25499
+ writeRepoIndex(cwd, projection);
25381
25500
  return projection;
25382
25501
  }
25502
+ function refreshRepoIndex(cwd, repoSlug3, options = {}) {
25503
+ const budgetMs = options.budgetMs ?? LOCAL_REPO_INDEX_REFRESH_BUDGET_MS;
25504
+ const deadline = Date.now() + Math.max(0, budgetMs);
25505
+ try {
25506
+ const exec = deadlineExec(deadline);
25507
+ const existing = loadRepoIndex(cwd);
25508
+ if (!existing) {
25509
+ const projection2 = buildRepoIndexProjection(cwd, repoSlug3, deadline);
25510
+ writeRepoIndex(cwd, projection2);
25511
+ return { status: "rebuilt", projection: projection2 };
25512
+ }
25513
+ const head = gitHead(cwd, exec);
25514
+ if (!head) return { status: "stale", reason: "checkout HEAD is unavailable" };
25515
+ const worktreeDirty = String(exec("git", ["status", "--porcelain", "--untracked-files=normal"], {
25516
+ cwd,
25517
+ encoding: "utf8",
25518
+ windowsHide: true
25519
+ })).length > 0;
25520
+ if (existing.indexHead === head && !worktreeDirty) return { status: "refreshed", projection: existing };
25521
+ if (!existing.indexHead) {
25522
+ const projection2 = buildRepoIndexProjection(cwd, repoSlug3, deadline);
25523
+ writeRepoIndex(cwd, projection2);
25524
+ return { status: "rebuilt", projection: projection2 };
25525
+ }
25526
+ const files = sourceFilesForIndex(cwd, deadline);
25527
+ const current = new Map(files.map((file) => [file.path, file]));
25528
+ const previous = new Map(existing.entries.map((entry) => [entry.path, entry]));
25529
+ const changed = /* @__PURE__ */ new Set();
25530
+ for (const [path2, file] of current) {
25531
+ if (previous.get(path2)?.hash !== file.hash) changed.add(path2);
25532
+ }
25533
+ for (const path2 of previous.keys()) {
25534
+ if (!current.has(path2)) changed.add(path2);
25535
+ }
25536
+ const readmeChanged = [...changed].some((path2) => path2 === "README.md" || /^[^/]+\/README\.md$/.test(path2));
25537
+ if (changed.size > LOCAL_REPO_INDEX_DELTA_FILE_LIMIT || readmeChanged) {
25538
+ const projection2 = buildRepoIndexProjection(cwd, repoSlug3, deadline);
25539
+ writeRepoIndex(cwd, projection2);
25540
+ return { status: "rebuilt", projection: projection2 };
25541
+ }
25542
+ const changedSources = [...changed].map((path2) => current.get(path2)).filter((file) => file !== void 0);
25543
+ const needsHint = changedSources.some((file) => !extractModuleBlurb(file.text));
25544
+ const readmeHints = needsHint ? loadReadmeHints(cwd, files.map((file) => file.path)) : /* @__PURE__ */ new Map();
25545
+ const entries = files.map((file) => {
25546
+ assertRefreshBudget(deadline);
25547
+ const old = previous.get(file.path);
25548
+ return old?.hash === file.hash ? old : entryForSource(file, readmeHints);
25549
+ });
25550
+ entries.sort((a, b) => a.path.localeCompare(b.path));
25551
+ const projection = {
25552
+ ...existing,
25553
+ repo: repoSlug3,
25554
+ builtAt: (/* @__PURE__ */ new Date()).toISOString(),
25555
+ indexHead: head,
25556
+ entries
25557
+ };
25558
+ writeRepoIndex(cwd, projection);
25559
+ return { status: "refreshed", projection };
25560
+ } catch (error) {
25561
+ const reason = error instanceof RepoIndexRefreshBudgetError ? error.message : `local index refresh failed: ${error.message}`;
25562
+ return { status: "stale", reason };
25563
+ }
25564
+ }
25383
25565
  function loadRepoIndex(cwd) {
25384
25566
  const store = repoIndexStorePath(cwd);
25385
25567
  if (!(0, import_node_fs24.existsSync)(store)) return null;
@@ -25396,6 +25578,7 @@ function repoIndexStatus(cwd) {
25396
25578
  const idx = loadRepoIndex(cwd);
25397
25579
  if (!idx) return { present: false, path: path2 };
25398
25580
  const symbolCount = idx.entries.reduce((n, e) => n + e.symbols.length, 0);
25581
+ const localUsage = readLocalRepoIndexQueryUsage(cwd);
25399
25582
  return {
25400
25583
  present: true,
25401
25584
  path: path2,
@@ -25403,7 +25586,8 @@ function repoIndexStatus(cwd) {
25403
25586
  repo: idx.repo,
25404
25587
  builtAt: idx.builtAt,
25405
25588
  fileCount: idx.entries.length,
25406
- symbolCount
25589
+ symbolCount,
25590
+ ...localUsage ? { localQueryCount: localUsage.count } : {}
25407
25591
  };
25408
25592
  }
25409
25593
  function searchRepoIndex(idx, query, limit = 20) {
@@ -26197,9 +26381,9 @@ async function probeRepoIndexV4ReadinessCloud(queries, deps) {
26197
26381
  if (!token) return { ok: false, error: "no Hub session token (run `gh auth login`)" };
26198
26382
  const baseUrl = deps.baseUrl.replace(/\/$/, "");
26199
26383
  const headers = { ...clientVersionHeaders(), Authorization: ["Bearer", token].join(" "), "content-type": "application/json" };
26200
- const shadowPass = async (readinessProbe) => {
26384
+ const shadowPass = async (readinessProbe, passQueries = queries) => {
26201
26385
  const ms = {};
26202
- for (const query of queries) {
26386
+ for (const query of passQueries) {
26203
26387
  const res = await fetchWithRetry(deps.fetch ?? fetch, `${baseUrl}/repo-index/v4/shadow`, {
26204
26388
  method: "POST",
26205
26389
  headers,
@@ -26221,6 +26405,11 @@ async function probeRepoIndexV4ReadinessCloud(queries, deps) {
26221
26405
  }
26222
26406
  const verdict = await shadowPass(true);
26223
26407
  if (!verdict.ok) return verdict;
26408
+ const latencyRetries = queries.filter((query) => query.latencyMs !== void 0 && verdict.ms[query.id] !== void 0 && verdict.ms[query.id] > query.latencyMs);
26409
+ if (latencyRetries.length) {
26410
+ const retry = await shadowPass(true, latencyRetries);
26411
+ if (!retry.ok) return retry;
26412
+ }
26224
26413
  const res = await fetchWithRetry(deps.fetch ?? fetch, `${baseUrl}/repo-index/status`, {
26225
26414
  method: "GET",
26226
26415
  headers: { ...clientVersionHeaders(), Authorization: ["Bearer", token].join(" ") }
@@ -26750,9 +26939,9 @@ function buildGraphEdges(cwd, repo, commit, rosterRepos2) {
26750
26939
  }
26751
26940
 
26752
26941
  // src/repo-index-sync.ts
26753
- function execFileUtf8(file, args) {
26942
+ function execFileUtf8(file, args, env) {
26754
26943
  return new Promise((resolve6, reject) => {
26755
- (0, import_node_child_process15.execFile)(file, args, { encoding: "utf8", windowsHide: true }, (error, stdout) => {
26944
+ (0, import_node_child_process15.execFile)(file, args, { encoding: "utf8", windowsHide: true, ...env ? { env } : {} }, (error, stdout) => {
26756
26945
  if (error) reject(error);
26757
26946
  else resolve6(String(stdout ?? ""));
26758
26947
  });
@@ -26760,6 +26949,9 @@ function execFileUtf8(file, args) {
26760
26949
  }
26761
26950
  var ESTATE_CLASSIFY_CONCURRENCY = 8;
26762
26951
  var ESTATE_HEALTHY_CLASSIFY_BUDGET_MS = 5 * 601e3;
26952
+ function formatEstateRunFailureSummary(failed) {
26953
+ return failed.map((f) => `${f.repo} (${f.probe?.failureClass ?? "failed"})`).join(", ");
26954
+ }
26763
26955
  var COMMIT3 = /^[a-f0-9]{40}$/;
26764
26956
  var SHA256 = /^[a-f0-9]{64}$/;
26765
26957
  var UTC_MILLIS = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
@@ -26795,14 +26987,57 @@ function checkoutExactCommit(repo, dest, token, commit) {
26795
26987
  const head = git3(["rev-parse", "HEAD"]).trim().toLowerCase();
26796
26988
  if (head !== commit) throw new Error(`checkout of ${repo} resolved ${head}, not the requested commit ${commit}`);
26797
26989
  }
26990
+ var HeadProbeError = class extends Error {
26991
+ constructor(message, exitCode, failureClass) {
26992
+ super(message);
26993
+ this.exitCode = exitCode;
26994
+ this.failureClass = failureClass;
26995
+ }
26996
+ exitCode;
26997
+ failureClass;
26998
+ };
26999
+ function classifyProbeStderr(stderr) {
27000
+ const text = stderr.toLowerCase();
27001
+ if (/(could not read username|could not read password|authentication failed|invalid credentials|permission denied|terminal prompts disabled|returned error: 401|returned error: 403)/.test(text)) return "auth-denied";
27002
+ if (/(repository .* not found|couldn't find remote ref|not found)/.test(text)) return "not-found";
27003
+ if (/(timed out|timeout|operation timed out|connection timed out)/.test(text)) return "timeout";
27004
+ if (/(could not resolve host|network is unreachable|connection (refused|reset)|temporary failure|failed to connect|could not connect)/.test(text)) return "network";
27005
+ return "unreadable";
27006
+ }
27007
+ function headProbeEnv() {
27008
+ const env = { ...process.env };
27009
+ delete env.GIT_ASKPASS;
27010
+ delete env.SSH_ASKPASS;
27011
+ for (const key of Object.keys(env)) {
27012
+ if (key.startsWith("GIT_CONFIG_")) delete env[key];
27013
+ }
27014
+ env.GIT_TERMINAL_PROMPT = "0";
27015
+ return env;
27016
+ }
27017
+ function toHeadProbeError(error, repo) {
27018
+ const e = error;
27019
+ const exitCode = typeof e.code === "number" ? e.code : null;
27020
+ const stderr = typeof e.stderr === "string" ? e.stderr : "";
27021
+ return new HeadProbeError(
27022
+ `remote HEAD probe of ${repo} failed (exit ${exitCode ?? "unknown"}, ${classifyProbeStderr(stderr)})`,
27023
+ exitCode,
27024
+ classifyProbeStderr(stderr)
27025
+ );
27026
+ }
26798
27027
  async function remoteHead(repo, token) {
26799
27028
  const basic = Buffer.from(`x-access-token:${token}`, "utf8").toString("base64");
26800
- const stdout = await execFileUtf8(
26801
- "git",
26802
- ["-c", `http.extraHeader=Authorization: Basic ${basic}`, "ls-remote", "--exit-code", `https://github.com/${repo}.git`, "HEAD"]
26803
- );
27029
+ let stdout;
27030
+ try {
27031
+ stdout = await execFileUtf8(
27032
+ "git",
27033
+ ["-c", "credential.helper=", "-c", `http.extraHeader=Authorization: Basic ${basic}`, "ls-remote", "--exit-code", `https://github.com/${repo}.git`, "HEAD"],
27034
+ headProbeEnv()
27035
+ );
27036
+ } catch (error) {
27037
+ throw toHeadProbeError(error, repo);
27038
+ }
26804
27039
  const match = stdout.match(/^([a-f0-9]{40})\s+HEAD$/m);
26805
- if (!match) throw new Error(`could not resolve remote HEAD for ${repo}`);
27040
+ if (!match) throw new HeadProbeError(`could not resolve remote HEAD for ${repo}`, null, "unreadable");
26806
27041
  return match[1];
26807
27042
  }
26808
27043
  async function mapLimit(items, limit, fn) {
@@ -26871,6 +27106,7 @@ async function syncEstateRepoIndex(opts) {
26871
27106
  const skipped = [];
26872
27107
  const drift = [];
26873
27108
  const needsFullRebuild = [];
27109
+ let rosterSize = 0;
26874
27110
  const answer = (ok) => ({
26875
27111
  ok: ok ?? failed.length === 0,
26876
27112
  published,
@@ -26878,7 +27114,12 @@ async function syncEstateRepoIndex(opts) {
26878
27114
  skipped,
26879
27115
  provenanceToken: CURRENT_REPO_INDEX_PROVENANCE_TOKEN,
26880
27116
  drift,
26881
- needsFullRebuild
27117
+ needsFullRebuild,
27118
+ receipt: {
27119
+ changed: published.length,
27120
+ skipped: Math.max(0, rosterSize - published.length - failed.length),
27121
+ failed: failed.length
27122
+ }
26882
27123
  });
26883
27124
  const fullRebuildToken = (opts.fullRebuild ?? "").trim();
26884
27125
  const forceFull = fullRebuildToken !== "";
@@ -26918,6 +27159,7 @@ async function syncEstateRepoIndex(opts) {
26918
27159
  const busy = new Set(
26919
27160
  (opts.skipRepos ?? []).map((repo) => normalizeRepoIndexRepo(repo)).filter((repo) => repo !== null)
26920
27161
  );
27162
+ rosterSize = repos.length;
26921
27163
  const emit = (progress) => {
26922
27164
  try {
26923
27165
  opts.onClassify?.(progress);
@@ -26950,12 +27192,12 @@ async function syncEstateRepoIndex(opts) {
26950
27192
  }
26951
27193
  let targetCommit = requestedCommit || void 0;
26952
27194
  if (base && !forceFull) {
26953
- let headUnreadable = false;
27195
+ let headProbe = null;
26954
27196
  if (!targetCommit) {
26955
27197
  try {
26956
27198
  targetCommit = await remoteHead(repo, opts.githubToken);
26957
- } catch {
26958
- headUnreadable = true;
27199
+ } catch (error) {
27200
+ headProbe = error instanceof HeadProbeError ? error : new HeadProbeError(error.message, null, "unreadable");
26959
27201
  }
26960
27202
  }
26961
27203
  const provenance = await fetchRepoIndexV4ProvenanceCloud(repo, opts.deps).catch(
@@ -26979,11 +27221,10 @@ async function syncEstateRepoIndex(opts) {
26979
27221
  emit({ row: row2, warning });
26980
27222
  return { repo, row: row2, warning, base, expectedActiveDigest };
26981
27223
  }
26982
- if (headUnreadable) {
26983
- const row2 = { repo, reason: "head-unreadable", action: "skip", activeCommit: base.commit };
26984
- const warning = `${repo}: remote HEAD unreadable; leaving verified-ready authority at ${base.commit}`;
26985
- emit({ row: row2, warning });
26986
- return { repo, row: row2, warning, base, expectedActiveDigest };
27224
+ if (headProbe) {
27225
+ const row2 = { repo, reason: "head-unreadable", action: "failed", activeCommit: base.commit };
27226
+ emit({ row: row2 });
27227
+ return { repo, row: row2, headProbe, base, expectedActiveDigest };
26987
27228
  }
26988
27229
  }
26989
27230
  const row = {
@@ -27000,6 +27241,14 @@ async function syncEstateRepoIndex(opts) {
27000
27241
  drift.push(entry.row);
27001
27242
  if (entry.warning) skipped.push(entry.warning);
27002
27243
  if (entry.row.action === "needs-full-rebuild") needsFullRebuild.push(entry.repo);
27244
+ if (entry.headProbe) {
27245
+ const exit = entry.headProbe.exitCode === null ? "unknown" : String(entry.headProbe.exitCode);
27246
+ failed.push({
27247
+ repo: entry.repo,
27248
+ error: `remote HEAD unreadable (exit ${exit}, ${entry.headProbe.failureClass}) \u2014 verified-ready authority at ${entry.row.activeCommit ?? "unknown"} was not re-probed; reconcile FAILED`,
27249
+ probe: { exitCode: entry.headProbe.exitCode, failureClass: entry.headProbe.failureClass }
27250
+ });
27251
+ }
27003
27252
  }
27004
27253
  if (opts.plan) return answer();
27005
27254
  for (const entry of classified) {
@@ -33451,29 +33700,47 @@ ${err?.stdout ?? ""}`.split("\n").map((line) => line.trim()).find((line) => line
33451
33700
  var defaultGhMergeAutoIo = {
33452
33701
  gh: async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout
33453
33702
  };
33454
- async function composeOverrideBodyFile(prNumber, repoArgs, gh) {
33703
+ function resolveSquashMergeBodyText(commits, allowedClosing, cwd) {
33704
+ const policy = squashBodyWithOverride(commits, cwd);
33705
+ const base = policy ?? defaultSquashBodyFromCommits(commits);
33706
+ if (!base) return null;
33707
+ const rewritten = rewriteUnregisteredClosingKeywords(base, allowedClosing);
33708
+ if (policy) return rewritten;
33709
+ return rewritten === base ? null : rewritten;
33710
+ }
33711
+ function writeSquashBodyFile(body) {
33712
+ const dir = (0, import_node_fs40.mkdtempSync)((0, import_node_path37.join)((0, import_node_os18.tmpdir)(), "mmi-squash-body-"));
33713
+ const path2 = (0, import_node_path37.join)(dir, "body.txt");
33714
+ (0, import_node_fs40.writeFileSync)(path2, body.endsWith("\n") ? body : `${body}
33715
+ `, "utf8");
33716
+ return { path: path2, cleanup: () => {
33717
+ try {
33718
+ (0, import_node_fs40.rmSync)(dir, { recursive: true, force: true });
33719
+ } catch {
33720
+ }
33721
+ } };
33722
+ }
33723
+ async function composeOverrideBodyFile(prNumber, repoArgs, gh, opts) {
33724
+ if (opts?.bodyText != null && opts.bodyText !== "") {
33725
+ return { ...writeSquashBodyFile(opts.bodyText), text: opts.bodyText };
33726
+ }
33455
33727
  try {
33456
33728
  const raw = await gh(["pr", "view", prNumber, ...repoArgs, "--json", "commits"], GC_GH_TIMEOUT_MS);
33457
- const commits = JSON.parse(raw).commits ?? [];
33458
- const body = squashBodyWithOverride(commits.map((c) => ({ headline: c.messageHeadline ?? "", body: c.messageBody ?? "" })), process.cwd());
33729
+ const commits = (JSON.parse(raw).commits ?? []).map((c) => ({ headline: c.messageHeadline ?? "", body: c.messageBody ?? "" }));
33730
+ const body = resolveSquashMergeBodyText(commits, opts?.allowedClosing ?? [], process.cwd());
33459
33731
  if (!body) return void 0;
33460
- const dir = (0, import_node_fs40.mkdtempSync)((0, import_node_path37.join)((0, import_node_os18.tmpdir)(), "mmi-squash-body-"));
33461
- const path2 = (0, import_node_path37.join)(dir, "body.txt");
33462
- (0, import_node_fs40.writeFileSync)(path2, `${body}
33463
- `, "utf8");
33464
- return { path: path2, cleanup: () => {
33465
- try {
33466
- (0, import_node_fs40.rmSync)(dir, { recursive: true, force: true });
33467
- } catch {
33468
- }
33469
- } };
33732
+ return { ...writeSquashBodyFile(body), text: body };
33470
33733
  } catch {
33471
33734
  return void 0;
33472
33735
  }
33473
33736
  }
33474
- async function ghMergeAutoEnqueue(prNumber, repo, method, io = defaultGhMergeAutoIo) {
33737
+ async function ghMergeAutoEnqueue(prNumber, repo, method, extra) {
33738
+ const io = extra?.io ?? defaultGhMergeAutoIo;
33475
33739
  const args = repo ? ["--repo", repo] : [];
33476
- const overrideBody = await composeOverrideBodyFile(prNumber, args, io.gh);
33740
+ const overrideBody = await composeOverrideBodyFile(prNumber, args, io.gh, {
33741
+ allowedClosing: method === "--squash" ? extra?.allowedClosing : void 0,
33742
+ bodyText: method === "--squash" ? extra?.bodyText : void 0
33743
+ });
33477
33744
  try {
33478
33745
  return await mergeAutoEnqueueWithBody(prNumber, args, method, io, overrideBody?.path);
33479
33746
  } finally {
@@ -33831,34 +34098,60 @@ async function removeWorktreeWithReconcile(wtPath, git3, listWorktrees, pathExis
33831
34098
  return formatGitCommandError(e);
33832
34099
  }
33833
34100
  };
33834
- const firstError = await attemptRemove();
33835
- if (!firstError) {
33836
- await git3(["worktree", "prune"]).catch(() => "");
33837
- return { status: "removed" };
33838
- }
33839
- let worktrees = await listWorktrees().catch(() => []);
33840
- if (!isWorktreePathRegistered(worktrees, wtPath)) {
34101
+ const pruneAndVerify = async (reason) => {
33841
34102
  await git3(["worktree", "prune"]).catch(() => "");
33842
- return { status: "removed", reason: "reconciled-unregistered" };
34103
+ let worktrees2;
34104
+ try {
34105
+ worktrees2 = await listWorktrees();
34106
+ } catch (e) {
34107
+ return {
34108
+ status: "failed",
34109
+ error: `could not verify worktree removal after prune: ${formatGitCommandError(e)}; path=${wtPath}`
34110
+ };
34111
+ }
34112
+ if (isWorktreePathRegistered(worktrees2, wtPath)) {
34113
+ return {
34114
+ status: "failed",
34115
+ error: `worktree remains registered after prune; path=${wtPath}`
34116
+ };
34117
+ }
34118
+ return { status: "removed", ...reason ? { reason } : {} };
34119
+ };
34120
+ const firstError = await attemptRemove();
34121
+ if (!firstError) return pruneAndVerify();
34122
+ let worktrees;
34123
+ try {
34124
+ worktrees = await listWorktrees();
34125
+ } catch (e) {
34126
+ return {
34127
+ status: "failed",
34128
+ error: `could not verify worktree registration after remove failure: ${formatGitCommandError(e)}; path=${wtPath}`
34129
+ };
33843
34130
  }
34131
+ if (!isWorktreePathRegistered(worktrees, wtPath)) return pruneAndVerify("reconciled-unregistered");
33844
34132
  const retryError = await attemptRemove();
33845
- if (!retryError) {
33846
- await git3(["worktree", "prune"]).catch(() => "");
33847
- return { status: "removed", reason: "reconciled-after-retry" };
33848
- }
34133
+ if (!retryError) return pruneAndVerify("reconciled-after-retry");
33849
34134
  if (isNotAWorkingTreeMessage(retryError) || isNotAWorkingTreeMessage(firstError)) {
33850
- worktrees = await listWorktrees().catch(() => worktrees);
33851
- if (!isWorktreePathRegistered(worktrees, wtPath)) {
33852
- await git3(["worktree", "prune"]).catch(() => "");
33853
- return { status: "removed", reason: "reconciled-not-a-working-tree" };
34135
+ try {
34136
+ worktrees = await listWorktrees();
34137
+ } catch (e) {
34138
+ return {
34139
+ status: "failed",
34140
+ error: `could not verify worktree registration after remove failure: ${formatGitCommandError(e)}; path=${wtPath}`
34141
+ };
33854
34142
  }
34143
+ if (!isWorktreePathRegistered(worktrees, wtPath)) return pruneAndVerify("reconciled-not-a-working-tree");
33855
34144
  }
33856
- worktrees = await listWorktrees().catch(() => worktrees);
33857
- const registered = isWorktreePathRegistered(worktrees, wtPath);
33858
- if (!registered) {
33859
- await git3(["worktree", "prune"]).catch(() => "");
33860
- return { status: "removed", reason: "reconciled-unregistered" };
34145
+ try {
34146
+ worktrees = await listWorktrees();
34147
+ } catch (e) {
34148
+ return {
34149
+ status: "failed",
34150
+ error: `could not verify worktree registration after remove failure: ${formatGitCommandError(e)}; path=${wtPath}`
34151
+ };
33861
34152
  }
34153
+ const registered = isWorktreePathRegistered(worktrees, wtPath);
34154
+ if (!registered) return pruneAndVerify("reconciled-unregistered");
33862
34155
  return {
33863
34156
  status: "failed",
33864
34157
  error: formatWorktreeRemovalFailureDetail({
@@ -33869,6 +34162,9 @@ async function removeWorktreeWithReconcile(wtPath, git3, listWorktrees, pathExis
33869
34162
  })
33870
34163
  };
33871
34164
  }
34165
+ function prMergeLocalCleanupExitCode(cleanup) {
34166
+ return cleanup?.worktree?.status === "failed" || cleanup?.localBranch?.status === "failed" ? 1 : void 0;
34167
+ }
33872
34168
  async function cleanupPrMergeLocalBranch(branch, options) {
33873
34169
  const report = { branch };
33874
34170
  if (!branch) {
@@ -33975,7 +34271,7 @@ async function cleanupPrMergeLocalBranch(branch, options) {
33975
34271
  const removal = await removeWorktreeWithReconcile(
33976
34272
  wtPath,
33977
34273
  git3,
33978
- async () => parseGitWorktreePorcelain(await execGit(["worktree", "list", "--porcelain"])),
34274
+ async () => parseGitWorktreePorcelain(await git3(["worktree", "list", "--porcelain"])),
33979
34275
  pathExists
33980
34276
  );
33981
34277
  if (removal.status === "failed") {
@@ -41334,11 +41630,29 @@ async function runRepoIndexSearchCommand(query, o, defaultMode) {
41334
41630
  const useLocal = o.local === true && o.cloud !== true;
41335
41631
  if (useLocal) {
41336
41632
  const root = await repoRoot();
41337
- let idx = loadRepoIndex(root);
41338
- if (!idx) idx = rebuildRepoIndex(root, inferRepoSlug(root));
41633
+ const refreshed = refreshRepoIndex(root, inferRepoSlug(root));
41634
+ if (!refreshed.projection) {
41635
+ const receipt = {
41636
+ ok: false,
41637
+ source: "local",
41638
+ repo: inferRepoSlug(root),
41639
+ query,
41640
+ hits: [],
41641
+ status: refreshed.status,
41642
+ reason: refreshed.reason ?? "local index refresh failed"
41643
+ };
41644
+ if (o.json) consoleIo.log(JSON.stringify(receipt, null, 2));
41645
+ else console.error(`repo-index: local index stale \u2014 ${receipt.reason}`);
41646
+ return await cleanExit(1);
41647
+ }
41648
+ const idx = refreshed.projection;
41339
41649
  const hits = searchRepoIndex(idx, query, limit);
41650
+ try {
41651
+ recordLocalRepoIndexQueryUsage(root);
41652
+ } catch {
41653
+ }
41340
41654
  if (o.json) {
41341
- consoleIo.log(JSON.stringify({ ok: true, source: "local", repo: idx.repo, query, hits }, null, 2));
41655
+ consoleIo.log(JSON.stringify({ ok: true, source: "local", repo: idx.repo, query, hits, status: refreshed.status }, null, 2));
41342
41656
  return;
41343
41657
  }
41344
41658
  if (hits.length === 0) {
@@ -41585,7 +41899,10 @@ repoIndex.command("sync-estate").description("Hub indexer: publish a pushed comm
41585
41899
  for (const f of res.failed) {
41586
41900
  console.error(`repo-index: FAILED ${f.repo}: ${f.error}`);
41587
41901
  }
41588
- if (!res.ok) await failGraceful(`sync-estate incomplete (${res.failed.length} failed)`);
41902
+ console.log(`repo-index: estate reconcile receipt \u2014 ${res.receipt.changed} changed, ${res.receipt.skipped} skipped, ${res.receipt.failed} failed`);
41903
+ if (!res.ok) {
41904
+ await failGraceful(`sync-estate incomplete (${res.failed.length} failed): ${formatEstateRunFailureSummary(res.failed)}`);
41905
+ }
41589
41906
  } catch (e) {
41590
41907
  await failGraceful(e.message);
41591
41908
  }
@@ -42839,6 +43156,20 @@ async function readClosingGuardInput(number, repoArgs, repo, context) {
42839
43156
  return input;
42840
43157
  }
42841
43158
  }
43159
+ function squashBodyTextForMerge(input, methodIsSquash, explicitBody) {
43160
+ if (!methodIsSquash) return void 0;
43161
+ if (explicitBody != null && explicitBody !== "") return explicitBody;
43162
+ if (!input) return void 0;
43163
+ return resolveSquashMergeBodyText(input.commits ?? [], input.closing, process.cwd()) ?? void 0;
43164
+ }
43165
+ function warnSquashBodyClosingStrip(context, input, squashBodyText) {
43166
+ if (!input || squashBodyText == null) return;
43167
+ const { open: open2 } = partitionCommitClosings(input.commitClosing ?? [], input.closing, input.alreadyClosed);
43168
+ if (!open2.length) return;
43169
+ console.warn(
43170
+ `${context}: composing a squash body that leaves unregistered ${open2.map((n) => `#${n}`).join(", ")} open \u2014 GitHub's default COMMIT_MESSAGES composition would close them (#5723).`
43171
+ );
43172
+ }
42842
43173
  function ciAuditDeps() {
42843
43174
  const cfgPromise = loadConfig();
42844
43175
  const root = hubRoot();
@@ -42988,7 +43319,13 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
42988
43319
  return typeof viewed.state === "string" ? viewed.state : void 0;
42989
43320
  }
42990
43321
  );
42991
- const landClosingGuardVerdict = evaluateClosingGuard(landClosingGuardInput, { force: o.force, context: "pr land" });
43322
+ const landSquashBody = squashBodyTextForMerge(landClosingGuardInput, true);
43323
+ warnSquashBodyClosingStrip("pr land", landClosingGuardInput, landSquashBody);
43324
+ const landClosingGuardVerdict = evaluateClosingGuard(landClosingGuardInput, {
43325
+ force: o.force,
43326
+ context: "pr land",
43327
+ squashBodyText: landSquashBody
43328
+ });
42992
43329
  if (landClosingGuardVerdict.blocked) {
42993
43330
  console.error(landClosingGuardVerdict.message);
42994
43331
  process.exitCode = 1;
@@ -43063,7 +43400,10 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
43063
43400
  if (!result2.ok) throw new Error(`could not read PR state: ${result2.error}`);
43064
43401
  return JSON.parse(result2.state);
43065
43402
  },
43066
- mergeAuto: (prNumber, repo) => ghMergeAutoEnqueue(prNumber, repo, "--squash"),
43403
+ mergeAuto: (prNumber, repo) => ghMergeAutoEnqueue(prNumber, repo, "--squash", {
43404
+ allowedClosing: landClosingGuardInput?.closing,
43405
+ bodyText: landSquashBody
43406
+ }),
43067
43407
  pollMerged: async (prNumber, repo, deadlineMs) => {
43068
43408
  let lastFailure;
43069
43409
  while (Date.now() < deadlineMs) {
@@ -43124,7 +43464,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
43124
43464
  else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
43125
43465
  if (result.status === "failed") process.exitCode = 1;
43126
43466
  });
43127
- 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)").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("--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) => {
43467
+ 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)").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) => {
43128
43468
  const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
43129
43469
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
43130
43470
  const repoForPostCleanup = await resolveRepo(o.repo) ?? o.repo;
@@ -43147,7 +43487,20 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
43147
43487
  return typeof viewed.state === "string" ? viewed.state : void 0;
43148
43488
  }
43149
43489
  );
43150
- const closingGuardVerdict = evaluateClosingGuard(closingGuardInput, { force: o.force, context: "pr merge" });
43490
+ if (o.squashBodyFile && method !== "--squash") {
43491
+ throw new Error("pr merge: --squash-body-file applies only to squash merges");
43492
+ }
43493
+ const mergeSquashBody = squashBodyTextForMerge(
43494
+ closingGuardInput,
43495
+ method === "--squash",
43496
+ o.squashBodyFile ? (0, import_node_fs48.readFileSync)(o.squashBodyFile, "utf8") : void 0
43497
+ );
43498
+ if (!o.squashBodyFile) warnSquashBodyClosingStrip("pr merge", closingGuardInput, mergeSquashBody);
43499
+ const closingGuardVerdict = evaluateClosingGuard(closingGuardInput, {
43500
+ force: o.force,
43501
+ context: "pr merge",
43502
+ squashBodyText: mergeSquashBody
43503
+ });
43151
43504
  if (closingGuardVerdict.blocked) {
43152
43505
  console.error(closingGuardVerdict.message);
43153
43506
  process.exitCode = 1;
@@ -43202,7 +43555,12 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
43202
43555
  const remoteBefore = await remoteBranchExists2(headRef);
43203
43556
  let upgradedToAuto = false;
43204
43557
  let remoteNotAttemptedReason = "host-owned-lifecycle";
43205
- const overrideBody = await composeOverrideBodyFile(number, repoArgs, async (a, t) => (await execFileP2("gh", a, { timeout: t })).stdout);
43558
+ const overrideBody = mergeSquashBody ? { ...writeSquashBodyFile(mergeSquashBody), text: mergeSquashBody } : await composeOverrideBodyFile(
43559
+ number,
43560
+ repoArgs,
43561
+ async (a, t) => (await execFileP2("gh", a, { timeout: t })).stdout,
43562
+ { allowedClosing: method === "--squash" ? closingGuardInput?.closing : void 0 }
43563
+ );
43206
43564
  const bodyFile = overrideBody?.path;
43207
43565
  try {
43208
43566
  await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: o.auto, deleteBranch: false, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch(async (e) => {
@@ -43387,7 +43745,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
43387
43745
  })) {
43388
43746
  console.error(line);
43389
43747
  }
43390
- process.exitCode = postMergeReconExitCode({ boardAdvance, crossRepoFilingIssue }) ?? process.exitCode;
43748
+ process.exitCode = prMergeLocalCleanupExitCode(localCleanup) ?? postMergeReconExitCode({ boardAdvance, crossRepoFilingIssue }) ?? process.exitCode;
43391
43749
  });
43392
43750
  registerQueryCommands(program2);
43393
43751
  registerIssueLifecycleCommands(program2, { attach: attachToProject });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.1.15",
3
+ "version": "4.1.17",
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",