@h-rig/runtime 0.0.6-alpha.13 → 0.0.6-alpha.15

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.
@@ -4352,7 +4352,7 @@ function stripHtml(input) {
4352
4352
  }
4353
4353
  function containsBlockerText(input) {
4354
4354
  const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
4355
- return /not safe(?: to merge)?|unsafe(?: to merge)?|do not merge|cannot merge|blockers?|must fix|changes requested|please fix|needs? fix|fix this|address this/i.test(text);
4355
+ return /not safe(?: to merge)?|unsafe(?: to merge)?|do not merge|cannot merge|blockers?|must fix|changes requested|please fix|needs? fix|fix this|address this|\breject(?:ed|ion)?\b|\bskip(?:ped)?\b|status\s*:\s*(?:reject(?:ed)?|skip(?:ped)?|failed)/i.test(text);
4356
4356
  }
4357
4357
  function isStrictFiveOfFive(score) {
4358
4358
  return score.value === 5 && score.scale === 5;
@@ -4360,6 +4360,189 @@ function isStrictFiveOfFive(score) {
4360
4360
  function containsConflictingScoreText(input) {
4361
4361
  return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
4362
4362
  }
4363
+ function greptileStatusVerdict(status) {
4364
+ const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
4365
+ if (!normalized)
4366
+ return null;
4367
+ if (["APPROVE", "APPROVED"].includes(normalized))
4368
+ return "approved";
4369
+ if (["REJECT", "REJECTED", "CHANGES_REQUESTED", "CHANGE_REQUESTED"].includes(normalized))
4370
+ return "rejected";
4371
+ if (["SKIP", "SKIPPED"].includes(normalized))
4372
+ return "skipped";
4373
+ if (["FAIL", "FAILED", "FAILURE", "ERROR"].includes(normalized))
4374
+ return "failed";
4375
+ if (["PENDING", "QUEUED", "IN_PROGRESS", "RUNNING", "STARTED", "REQUESTED", "REVIEWING_FILES", "GENERATING_SUMMARY"].includes(normalized))
4376
+ return "pending";
4377
+ if (["COMPLETE", "COMPLETED"].includes(normalized))
4378
+ return "completed";
4379
+ return null;
4380
+ }
4381
+ function isBlockingGreptileVerdict(verdict) {
4382
+ return verdict === "rejected" || verdict === "skipped" || verdict === "failed";
4383
+ }
4384
+ function greptileRequestTimeoutMs(env) {
4385
+ const fallback = 30000;
4386
+ const parsed = Number.parseInt(env.GREPTILE_REQUEST_TIMEOUT_MS || `${fallback}`, 10);
4387
+ return Number.isFinite(parsed) && parsed >= 1000 ? parsed : fallback;
4388
+ }
4389
+ function normalizeGreptileMcpCodeReview(entry, fallbackId) {
4390
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
4391
+ return null;
4392
+ const record = entry;
4393
+ const id = typeof record.id === "string" ? record.id.trim() : fallbackId?.trim() ?? "";
4394
+ if (!id)
4395
+ return null;
4396
+ const metadataRecord = record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? record.metadata : null;
4397
+ return {
4398
+ id,
4399
+ status: typeof record.status === "string" ? record.status : null,
4400
+ createdAt: typeof record.createdAt === "string" ? record.createdAt : null,
4401
+ body: typeof record.body === "string" ? record.body : null,
4402
+ metadata: metadataRecord ? { checkHeadSha: typeof metadataRecord.checkHeadSha === "string" ? metadataRecord.checkHeadSha : null } : null
4403
+ };
4404
+ }
4405
+ function uniqueGreptileCodeReviews(reviews) {
4406
+ const seen = new Set;
4407
+ const unique2 = [];
4408
+ for (const review of reviews) {
4409
+ if (seen.has(review.id))
4410
+ continue;
4411
+ seen.add(review.id);
4412
+ unique2.push(review);
4413
+ }
4414
+ return unique2;
4415
+ }
4416
+ function selectGreptileApiReviewsForGate(reviews, headSha) {
4417
+ const sorted = [...reviews].sort((left, right) => Date.parse(right.createdAt ?? "") - Date.parse(left.createdAt ?? ""));
4418
+ const current = headSha ? sorted.filter((review) => review.metadata?.checkHeadSha === headSha) : [];
4419
+ const untied = sorted.filter((review) => !review.metadata?.checkHeadSha);
4420
+ const latest = sorted.slice(0, 1);
4421
+ return uniqueGreptileCodeReviews([...current, ...untied, ...latest]);
4422
+ }
4423
+ function greptileApiSignalFromCodeReview(review, details) {
4424
+ const selected = details ?? review;
4425
+ return {
4426
+ id: selected.id || review.id,
4427
+ body: selected.body ?? review.body ?? null,
4428
+ reviewedSha: selected.metadata?.checkHeadSha ?? review.metadata?.checkHeadSha ?? null,
4429
+ status: selected.status ?? review.status ?? null
4430
+ };
4431
+ }
4432
+ async function callGreptileMcpToolForGate(input) {
4433
+ const controller = new AbortController;
4434
+ const timeoutId = setTimeout(() => {
4435
+ controller.abort(new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`));
4436
+ }, input.timeoutMs);
4437
+ let response;
4438
+ try {
4439
+ response = await input.fetchFn(input.apiBase, {
4440
+ method: "POST",
4441
+ headers: {
4442
+ Authorization: `Bearer ${input.apiKey}`,
4443
+ "Content-Type": "application/json"
4444
+ },
4445
+ body: JSON.stringify({
4446
+ jsonrpc: "2.0",
4447
+ id: `rig-strict-gate-${input.name}-${Date.now()}`,
4448
+ method: "tools/call",
4449
+ params: { name: input.name, arguments: input.args }
4450
+ }),
4451
+ signal: controller.signal
4452
+ });
4453
+ } catch (error) {
4454
+ if (controller.signal.aborted) {
4455
+ throw controller.signal.reason instanceof Error ? controller.signal.reason : new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`);
4456
+ }
4457
+ throw error;
4458
+ } finally {
4459
+ clearTimeout(timeoutId);
4460
+ }
4461
+ const raw = await response.text();
4462
+ if (!response.ok) {
4463
+ throw new Error(`HTTP ${response.status}: ${raw}`);
4464
+ }
4465
+ let envelope;
4466
+ try {
4467
+ envelope = JSON.parse(raw);
4468
+ } catch {
4469
+ throw new Error(`Malformed MCP response: ${raw}`);
4470
+ }
4471
+ if (envelope.error?.message) {
4472
+ throw new Error(envelope.error.message);
4473
+ }
4474
+ const text = (envelope.result?.content ?? []).filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text ?? "").join(`
4475
+ `).trim();
4476
+ if (!text) {
4477
+ throw new Error(`MCP tool ${input.name} returned no text payload.`);
4478
+ }
4479
+ return text;
4480
+ }
4481
+ async function callGreptileMcpToolJsonForGate(input) {
4482
+ const text = await callGreptileMcpToolForGate(input);
4483
+ try {
4484
+ return JSON.parse(text);
4485
+ } catch {
4486
+ throw new Error(`MCP tool ${input.name} returned malformed JSON: ${text}`);
4487
+ }
4488
+ }
4489
+ async function collectConfiguredGreptileApiSignals(input) {
4490
+ if (!input.enabled || input.options?.enabled === false) {
4491
+ return { signals: [], errors: [] };
4492
+ }
4493
+ const env = input.options?.env ?? process.env;
4494
+ const secrets = resolveRuntimeSecrets(env);
4495
+ const apiKey = secrets.GREPTILE_API_KEY?.trim() ?? "";
4496
+ if (!apiKey) {
4497
+ return { signals: [], errors: [] };
4498
+ }
4499
+ const fetchFn = input.options?.fetch ?? globalThis.fetch;
4500
+ if (typeof fetchFn !== "function") {
4501
+ return { signals: [], errors: ["Greptile API/MCP evidence read failed: fetch is not available."] };
4502
+ }
4503
+ const apiBase = secrets.GREPTILE_API_BASE?.trim() || "https://api.greptile.com/mcp";
4504
+ const remote = secrets.GREPTILE_REMOTE?.trim() || "github";
4505
+ const repository = secrets.GREPTILE_REPOSITORY?.trim() || input.repoName;
4506
+ const defaultBranch = secrets.GREPTILE_DEFAULT_BRANCH?.trim() || input.baseRefName?.trim() || "main";
4507
+ const timeoutMs = greptileRequestTimeoutMs(env);
4508
+ try {
4509
+ const listPayload = await callGreptileMcpToolJsonForGate({
4510
+ apiBase,
4511
+ apiKey,
4512
+ name: "list_code_reviews",
4513
+ args: {
4514
+ name: repository,
4515
+ remote,
4516
+ defaultBranch,
4517
+ prNumber: input.prNumber,
4518
+ limit: 20
4519
+ },
4520
+ timeoutMs,
4521
+ fetchFn
4522
+ });
4523
+ const reviews = (listPayload.codeReviews ?? []).map((entry) => normalizeGreptileMcpCodeReview(entry)).filter((review) => !!review);
4524
+ const selectedReviews = selectGreptileApiReviewsForGate(reviews, input.headSha);
4525
+ const signals = [];
4526
+ for (const review of selectedReviews) {
4527
+ const detailsPayload = await callGreptileMcpToolJsonForGate({
4528
+ apiBase,
4529
+ apiKey,
4530
+ name: "get_code_review",
4531
+ args: { codeReviewId: review.id },
4532
+ timeoutMs,
4533
+ fetchFn
4534
+ });
4535
+ const details = normalizeGreptileMcpCodeReview(detailsPayload.codeReview, review.id) ?? review;
4536
+ signals.push(greptileApiSignalFromCodeReview(review, details));
4537
+ }
4538
+ return { signals, errors: [] };
4539
+ } catch (error) {
4540
+ return {
4541
+ signals: [],
4542
+ errors: [`Greptile API/MCP evidence read failed: ${error instanceof Error ? error.message : String(error)}`]
4543
+ };
4544
+ }
4545
+ }
4363
4546
  function firstString(record, keys) {
4364
4547
  for (const key of keys) {
4365
4548
  const value = record[key];
@@ -4486,7 +4669,7 @@ function normalizeReviewThread(entry) {
4486
4669
  function relevantIssueComment(comment) {
4487
4670
  const login = comment.user?.login ?? comment.author?.login ?? "";
4488
4671
  const body = comment.body ?? "";
4489
- return isGreptileGithubLogin(login) || /greptile|blocker|unsafe|not safe|do not merge|must fix|please fix|changes requested|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
4672
+ return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
4490
4673
  }
4491
4674
  function latestThreadComment(thread) {
4492
4675
  const nodes = thread.comments?.nodes ?? [];
@@ -4522,7 +4705,8 @@ function makeGreptileSignal(input) {
4522
4705
  const scores = parseGreptileScores(input.body);
4523
4706
  const reviewedSha = input.reviewedSha?.trim() || null;
4524
4707
  const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
4525
- const blocker = input.blocker ?? containsBlockerText(input.body);
4708
+ const verdict = input.verdict ?? null;
4709
+ const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
4526
4710
  const explicitApproval = input.explicitApproval ?? false;
4527
4711
  return {
4528
4712
  source: input.source,
@@ -4534,6 +4718,7 @@ function makeGreptileSignal(input) {
4534
4718
  score: scores[0] ?? null,
4535
4719
  scores,
4536
4720
  explicitApproval,
4721
+ verdict,
4537
4722
  blocker,
4538
4723
  actionable: input.actionable ?? blocker,
4539
4724
  bodyExcerpt: bodyExcerpt(input.body),
@@ -4556,9 +4741,9 @@ function collectGreptileSignals(evidence) {
4556
4741
  for (const context of contextSources) {
4557
4742
  if (!context.body.trim())
4558
4743
  continue;
4559
- if (!/greptile|score|confidence|\b\d+\s*\/\s*5\b|blocker|unsafe|not safe|do not merge|changes requested/i.test(context.body))
4560
- continue;
4561
4744
  const contextBlocker = containsBlockerText(context.body);
4745
+ if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
4746
+ continue;
4562
4747
  signals.push(makeGreptileSignal({
4563
4748
  source: context.source,
4564
4749
  body: context.body,
@@ -4571,16 +4756,16 @@ function collectGreptileSignals(evidence) {
4571
4756
  for (const apiSignal of evidence.apiSignals ?? []) {
4572
4757
  const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
4573
4758
 
4574
- `);
4575
- if (!body.trim())
4576
- continue;
4759
+ `) || "Status: UNKNOWN";
4760
+ const verdict = greptileStatusVerdict(apiSignal.status);
4577
4761
  signals.push(makeGreptileSignal({
4578
4762
  source: "api",
4579
4763
  body,
4580
4764
  currentHeadSha: evidence.currentHeadSha,
4581
4765
  trusted: true,
4582
4766
  reviewedSha: apiSignal.reviewedSha ?? null,
4583
- explicitApproval: false
4767
+ explicitApproval: verdict === "approved",
4768
+ verdict
4584
4769
  }));
4585
4770
  }
4586
4771
  for (const review of evidence.reviews) {
@@ -4605,20 +4790,6 @@ function collectGreptileSignals(evidence) {
4605
4790
  blocker: state === "CHANGES_REQUESTED" || undefined
4606
4791
  }));
4607
4792
  }
4608
- for (const comment of evidence.changedFileReviewComments) {
4609
- const login = commentAuthorLogin(comment);
4610
- const body = comment.body ?? "";
4611
- if (!body.trim() || !isGreptileGithubLogin(login))
4612
- continue;
4613
- signals.push(makeGreptileSignal({
4614
- source: "changed-file-comment",
4615
- body,
4616
- currentHeadSha: evidence.currentHeadSha,
4617
- trusted: true,
4618
- authorLogin: login,
4619
- reviewedSha: comment.commit_id ?? comment.original_commit_id ?? null
4620
- }));
4621
- }
4622
4793
  for (const comment of evidence.relevantIssueComments) {
4623
4794
  const login = commentAuthorLogin(comment);
4624
4795
  const body = comment.body ?? "";
@@ -4726,10 +4897,17 @@ function deriveGreptileEvidence(input) {
4726
4897
  const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
4727
4898
  const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
4728
4899
  const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
4729
- const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && isStrictFiveOfFive(entry.score)) ?? null;
4900
+ const currentPendingApiSignals = trustedSignals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && isCurrentOrUntied(signal));
4901
+ const signalCanApproveByScore = (signal) => {
4902
+ if (signal.source === "api")
4903
+ return signal.verdict === "approved" || signal.verdict === "completed";
4904
+ return signal.verdict !== "pending" && !isBlockingGreptileVerdict(signal.verdict);
4905
+ };
4906
+ const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && signalCanApproveByScore(entry.signal) && isStrictFiveOfFive(entry.score)) ?? null;
4907
+ const approvingExplicitSignal = trustedSignals.find((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && signal.explicitApproval === true && !signal.blocker) ?? null;
4730
4908
  const approvedByScore = !!approvingScoreEntry;
4731
- const approvedByExplicitMapping = false;
4732
- const approvingSignal = approvingScoreEntry?.signal ?? null;
4909
+ const approvedByExplicitMapping = !!approvingExplicitSignal;
4910
+ const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
4733
4911
  const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
4734
4912
  const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
4735
4913
  const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
@@ -4758,13 +4936,14 @@ function deriveGreptileEvidence(input) {
4758
4936
  const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
4759
4937
  return completedState && review.commit_id === input.currentHeadSha;
4760
4938
  });
4939
+ const completedGreptileApi = trustedSignals.some((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && (signal.verdict === "approved" || signal.verdict === "rejected" || signal.verdict === "skipped" || signal.verdict === "failed" || signal.verdict === "completed"));
4761
4940
  const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
4762
4941
  const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
4763
4942
  const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
4764
- const completed = completedGreptileCheck || completedGreptileReview || !!approvingSignal || trustedSignals.some((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha);
4943
+ const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
4765
4944
  const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
4766
- const approved = fresh && completed && !blockers.length && !unresolvedComments.length && (approvedByScore || approvedByExplicitMapping);
4767
- const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : "unproven";
4945
+ const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
4946
+ const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
4768
4947
  const source = approvingSignal?.source === "api" ? "api" : approvingSignal?.source === "github-review" ? "github-review" : approvingSignal?.source === "changed-file-comment" || approvingSignal?.source === "issue-comment" || approvingSignal?.source === "review-thread" ? "github-comment" : greptileReviews.length > 0 && greptileChecks.length > 0 ? "combined" : greptileReviews.length > 0 ? "github-review" : greptileChecks.length > 0 ? "github-check" : signals.some((signal) => signal.source === "pr-body" || signal.source === "pr-title") ? "pr-body" : "missing";
4769
4948
  return {
4770
4949
  source,
@@ -4871,6 +5050,7 @@ async function collectPrReviewEvidence(input) {
4871
5050
  readErrors.push("gh pr view did not return required reviews array");
4872
5051
  }
4873
5052
  const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
5053
+ const baseRefName = firstString(view, ["baseRefName"]);
4874
5054
  const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
4875
5055
  const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
4876
5056
  const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
@@ -4908,8 +5088,19 @@ async function collectPrReviewEvidence(input) {
4908
5088
  }
4909
5089
  }
4910
5090
  const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
5091
+ const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
5092
+ const configuredGreptileApiRead = await collectConfiguredGreptileApiSignals({
5093
+ enabled: shouldCollectConfiguredGreptileApi,
5094
+ options: input.greptileApi,
5095
+ repoName: parsed.repoName,
5096
+ prNumber: parsed.prNumber,
5097
+ headSha,
5098
+ baseRefName
5099
+ });
5100
+ readErrors.push(...configuredGreptileApiRead.errors);
5101
+ const apiSignals = [...input.apiSignals ?? [], ...configuredGreptileApiRead.signals];
4911
5102
  const checkFailures = statusCheckRollup.filter((check) => !isGreptileLabel(checkName(check)) && isFailingCheck(check) && !isAllowedFailure(checkName(check), input.allowedFailures ?? [])).map((check) => `Check failed: ${checkName(check)}${check.detailsUrl || check.link ? ` (${check.detailsUrl ?? check.link})` : ""}`);
4912
- const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && !isAllowedFailure(checkName(check), input.allowedFailures ?? [])).map((check) => `Check pending: ${checkName(check)}`);
5103
+ const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
4913
5104
  const evidenceBase = {
4914
5105
  title: firstString(view, ["title"]),
4915
5106
  body: firstString(view, ["body"]),
@@ -4919,7 +5110,7 @@ async function collectPrReviewEvidence(input) {
4919
5110
  reviewThreads,
4920
5111
  checks: checksWithGreptileDetails,
4921
5112
  currentHeadSha: headSha,
4922
- apiSignals: input.apiSignals ?? []
5113
+ apiSignals
4923
5114
  };
4924
5115
  const greptile = deriveGreptileEvidence(evidenceBase);
4925
5116
  return {
@@ -4930,7 +5121,7 @@ async function collectPrReviewEvidence(input) {
4930
5121
  body: evidenceBase.body,
4931
5122
  headSha,
4932
5123
  headRefName: firstString(view, ["headRefName"]),
4933
- baseRefName: firstString(view, ["baseRefName"]),
5124
+ baseRefName,
4934
5125
  state: firstString(view, ["state"]),
4935
5126
  isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
4936
5127
  mergeable: firstString(view, ["mergeable"]),
@@ -4947,71 +5138,251 @@ async function collectPrReviewEvidence(input) {
4947
5138
  greptile
4948
5139
  };
4949
5140
  }
5141
+ function capGateMessage(value, maxChars = 1200) {
5142
+ const normalized = value.trim();
5143
+ return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}
5144
+ [truncated for gate summary; see full evidence artifact]` : normalized;
5145
+ }
4950
5146
  function evaluateEvidence(evidence) {
4951
- const reasons = [];
5147
+ const reasonDetails = [];
4952
5148
  const warnings = [];
4953
- let pending = false;
4954
- if (evidence.readErrors.length > 0) {
4955
- reasons.push(...evidence.readErrors.map((error) => `Required PR evidence surface could not be read completely: ${error}`));
4956
- }
4957
- if (!evidence.headSha)
4958
- reasons.push("PR head SHA could not be read; current-head Greptile approval cannot be proven.");
4959
- if (evidence.checkFailures.length > 0)
4960
- reasons.push(...evidence.checkFailures);
4961
- if (evidence.pendingChecks.length > 0) {
4962
- pending = true;
4963
- reasons.push(...evidence.pendingChecks);
5149
+ const seen = new Set;
5150
+ const addReason = (reason) => {
5151
+ const capped = { ...reason, message: capGateMessage(reason.message) };
5152
+ const key = `${capped.code}:${capped.message}`;
5153
+ if (seen.has(key))
5154
+ return;
5155
+ seen.add(key);
5156
+ reasonDetails.push(capped);
5157
+ };
5158
+ const greptile = evidence.greptile;
5159
+ const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
5160
+ const hasPendingGreptileCheck = evidence.pendingChecks.some((check) => /greptile/i.test(check));
5161
+ const pendingGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
5162
+ const unknownGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && !signal.verdict && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
5163
+ const awaitingFreshGreptileProof = hasPendingGreptileCheck || pendingGreptileApiSignals.length > 0 || !greptile.completed || greptile.mapping === "missing" || greptile.mapping === "stale";
5164
+ for (const error of evidence.readErrors) {
5165
+ addReason({
5166
+ code: "read_error",
5167
+ reasonClass: "reject",
5168
+ surface: error.startsWith("Greptile API/MCP") ? "greptile" : "github",
5169
+ suggestedAction: "needs_attention",
5170
+ message: `Required PR evidence surface could not be read completely: ${error}`,
5171
+ headSha: evidence.headSha || null
5172
+ });
5173
+ }
5174
+ if (!evidence.headSha) {
5175
+ addReason({
5176
+ code: "missing_head_sha",
5177
+ reasonClass: "reject",
5178
+ surface: "github",
5179
+ suggestedAction: "needs_attention",
5180
+ message: "PR head SHA could not be read; current-head Greptile approval cannot be proven.",
5181
+ headSha: null
5182
+ });
5183
+ }
5184
+ for (const failure of evidence.checkFailures) {
5185
+ addReason({
5186
+ code: "ci_failed",
5187
+ reasonClass: "reject",
5188
+ surface: "ci",
5189
+ suggestedAction: "fix",
5190
+ message: failure,
5191
+ headSha: evidence.headSha || null
5192
+ });
5193
+ }
5194
+ for (const pendingCheck of evidence.pendingChecks) {
5195
+ addReason({
5196
+ code: "check_pending",
5197
+ reasonClass: "pending",
5198
+ surface: "ci",
5199
+ suggestedAction: "wait",
5200
+ message: pendingCheck,
5201
+ headSha: evidence.headSha || null
5202
+ });
4964
5203
  }
4965
5204
  const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
4966
5205
  if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
4967
- reasons.push(`Required review is unresolved (${evidence.reviewDecision}).`);
5206
+ addReason({
5207
+ code: "review_decision_blocking",
5208
+ reasonClass: "reject",
5209
+ surface: "review",
5210
+ suggestedAction: "fix",
5211
+ message: `Required review is unresolved (${evidence.reviewDecision}).`,
5212
+ headSha: evidence.headSha || null
5213
+ });
5214
+ }
5215
+ for (const thread of unresolvedThreadSummaries(evidence.reviewThreads)) {
5216
+ addReason({
5217
+ code: "review_thread_unresolved",
5218
+ reasonClass: "reject",
5219
+ surface: "review",
5220
+ suggestedAction: "fix",
5221
+ message: thread,
5222
+ headSha: evidence.headSha || null
5223
+ });
5224
+ }
5225
+ if (greptile.mapping === "missing") {
5226
+ addReason({
5227
+ code: "greptile_missing",
5228
+ reasonClass: "pending",
5229
+ surface: "greptile",
5230
+ suggestedAction: "wait",
5231
+ message: "Missing Greptile check/review evidence for this PR.",
5232
+ headSha: evidence.headSha || null
5233
+ });
4968
5234
  }
4969
- const unresolvedThreads = unresolvedThreadSummaries(evidence.reviewThreads);
4970
- if (unresolvedThreads.length > 0)
4971
- reasons.push(...unresolvedThreads);
4972
- const greptile = evidence.greptile;
4973
- if (greptile.mapping === "missing")
4974
- reasons.push("Missing Greptile check/review evidence for this PR.");
4975
- const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
4976
5235
  if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
4977
- reasons.push(`Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`);
5236
+ addReason({
5237
+ code: "greptile_stale",
5238
+ reasonClass: "pending",
5239
+ surface: "greptile",
5240
+ suggestedAction: "wait",
5241
+ message: `Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`,
5242
+ headSha: evidence.headSha || null,
5243
+ reviewedSha: greptile.reviewedSha ?? staleSignal?.reviewedSha ?? null
5244
+ });
5245
+ }
5246
+ for (const signal of pendingGreptileApiSignals) {
5247
+ addReason({
5248
+ code: "greptile_pending",
5249
+ reasonClass: "pending",
5250
+ surface: "greptile",
5251
+ suggestedAction: "wait",
5252
+ message: `Greptile API/MCP review is pending for the current PR head${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
5253
+ headSha: evidence.headSha || null,
5254
+ reviewedSha: signal.reviewedSha ?? null
5255
+ });
5256
+ }
5257
+ for (const signal of unknownGreptileApiSignals) {
5258
+ addReason({
5259
+ code: "greptile_api_status_unknown",
5260
+ reasonClass: "reject",
5261
+ surface: "greptile",
5262
+ suggestedAction: "needs_attention",
5263
+ message: `Greptile API/MCP review status is unknown; merge requires a known terminal APPROVED/COMPLETED 5/5 result or a known conservative status${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
5264
+ headSha: evidence.headSha || null,
5265
+ reviewedSha: signal.reviewedSha ?? null
5266
+ });
4978
5267
  }
4979
5268
  if (!greptile.completed) {
4980
- pending = true;
4981
- reasons.push("Greptile check/review has not completed for the current PR head.");
5269
+ addReason({
5270
+ code: "greptile_pending",
5271
+ reasonClass: "pending",
5272
+ surface: "greptile",
5273
+ suggestedAction: "wait",
5274
+ message: "Greptile check/review has not completed for the current PR head.",
5275
+ headSha: evidence.headSha || null,
5276
+ reviewedSha: greptile.reviewedSha ?? null
5277
+ });
5278
+ }
5279
+ if (!greptile.fresh) {
5280
+ addReason({
5281
+ code: "greptile_not_current_head",
5282
+ reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
5283
+ surface: "greptile",
5284
+ suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
5285
+ message: "Greptile approval is not tied to the current PR head SHA.",
5286
+ headSha: evidence.headSha || null,
5287
+ reviewedSha: greptile.reviewedSha ?? null
5288
+ });
4982
5289
  }
4983
- if (!greptile.fresh)
4984
- reasons.push("Greptile approval is not tied to the current PR head SHA.");
4985
5290
  if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
4986
- reasons.push(`Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`);
5291
+ addReason({
5292
+ code: "greptile_score_not_5",
5293
+ reasonClass: "reject",
5294
+ surface: "greptile",
5295
+ suggestedAction: "fix",
5296
+ message: `Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`,
5297
+ headSha: evidence.headSha || null,
5298
+ reviewedSha: greptile.reviewedSha ?? null
5299
+ });
4987
5300
  }
4988
- if (!greptile.score && greptile.mapping !== "score-5-of-5") {
4989
- reasons.push("No parseable Greptile 5/5 score or explicit approved mapping was found from trusted current-head evidence; merge is blocked.");
5301
+ const hasApprovedMapping = greptile.mapping === "score-5-of-5" || greptile.mapping === "explicit-approved";
5302
+ if (!greptile.score && !hasApprovedMapping) {
5303
+ addReason({
5304
+ code: "greptile_score_missing",
5305
+ reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
5306
+ surface: "greptile",
5307
+ suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
5308
+ message: "No parseable Greptile 5/5 score or direct current-head Greptile API APPROVED mapping was found from trusted evidence; merge is blocked.",
5309
+ headSha: evidence.headSha || null,
5310
+ reviewedSha: greptile.reviewedSha ?? null
5311
+ });
4990
5312
  }
4991
5313
  if (greptile.mapping === "unproven") {
4992
- reasons.push("Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.");
5314
+ addReason({
5315
+ code: "greptile_mapping_unproven",
5316
+ reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
5317
+ surface: "greptile",
5318
+ suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
5319
+ message: "Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.",
5320
+ headSha: evidence.headSha || null,
5321
+ reviewedSha: greptile.reviewedSha ?? null
5322
+ });
4993
5323
  }
4994
- if (greptile.blockers.length > 0) {
4995
- reasons.push(...greptile.blockers.map((entry) => `Greptile/blocker text: ${entry.trim().slice(0, 500)}`));
5324
+ for (const blocker of greptile.blockers) {
5325
+ addReason({
5326
+ code: "greptile_blocker_text",
5327
+ reasonClass: "reject",
5328
+ surface: "greptile",
5329
+ suggestedAction: "fix",
5330
+ message: `Greptile/blocker text: ${blocker}`,
5331
+ headSha: evidence.headSha || null,
5332
+ reviewedSha: greptile.reviewedSha ?? null
5333
+ });
5334
+ }
5335
+ for (const comment of greptile.unresolvedComments) {
5336
+ addReason({
5337
+ code: "greptile_unresolved_comment",
5338
+ reasonClass: "reject",
5339
+ surface: "greptile",
5340
+ suggestedAction: "fix",
5341
+ message: comment,
5342
+ headSha: evidence.headSha || null,
5343
+ reviewedSha: greptile.reviewedSha ?? null
5344
+ });
4996
5345
  }
4997
- if (greptile.unresolvedComments.length > 0)
4998
- reasons.push(...greptile.unresolvedComments);
4999
5346
  if (!greptile.approved)
5000
5347
  warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
5001
- return { reasons: Array.from(new Set(reasons)), warnings, pending };
5348
+ const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
5349
+ return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
5002
5350
  }
5003
5351
  function evaluateStrictPrMergeGate(evidence) {
5004
5352
  const evaluated = evaluateEvidence(evidence);
5005
- const approved = evaluated.reasons.length === 0 && evidence.greptile.approved;
5353
+ const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
5006
5354
  return {
5007
5355
  approved,
5008
5356
  pending: evaluated.pending,
5009
5357
  reasons: evaluated.reasons,
5358
+ reasonDetails: evaluated.reasonDetails,
5010
5359
  warnings: evaluated.warnings,
5011
5360
  actionableFeedback: evaluated.reasons,
5012
5361
  evidence
5013
5362
  };
5014
5363
  }
5364
+ function strictMergeHeadShaFromGate(result, prUrl) {
5365
+ if (!result.approved) {
5366
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate is not approved.`);
5367
+ }
5368
+ if (result.evidence.prUrl !== prUrl) {
5369
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate evidence belongs to ${result.evidence.prUrl}.`);
5370
+ }
5371
+ const headSha = result.evidence.headSha?.trim();
5372
+ if (!headSha) {
5373
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate did not provide a current head SHA.`);
5374
+ }
5375
+ if (!/^[0-9a-f]{40}$/i.test(headSha)) {
5376
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate head is not a raw 40-character commit SHA.`);
5377
+ }
5378
+ if (!result.evidence.greptile.fresh || result.evidence.greptile.currentHeadSha !== headSha) {
5379
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate approval is not tied to head ${headSha}.`);
5380
+ }
5381
+ if (result.evidence.greptile.mapping !== "score-5-of-5" && result.evidence.greptile.mapping !== "explicit-approved") {
5382
+ throw new Error(`Refusing to merge ${prUrl}: strict merge gate mapping is ${result.evidence.greptile.mapping}.`);
5383
+ }
5384
+ return headSha;
5385
+ }
5015
5386
  function promptExcerpt(value, maxChars = 4000) {
5016
5387
  return value.length > maxChars ? `${value.slice(0, maxChars)}
5017
5388
 
@@ -5023,6 +5394,10 @@ function promptJsonExcerpt(value, maxChars = 6000) {
5023
5394
  function buildStrictPrGateSteeringPrompt(result) {
5024
5395
  const evidence = result.evidence;
5025
5396
  const unresolvedReviewThreads = evidence.reviewThreads.filter((thread) => thread.isResolved !== true && thread.isOutdated !== true);
5397
+ const displayedReasons = result.reasons.slice(0, 20).map((reason) => `- ${promptExcerpt(reason, 1200)}`);
5398
+ if (result.reasons.length > displayedReasons.length) {
5399
+ displayedReasons.push(`- ${result.reasons.length - displayedReasons.length} additional gate reasons omitted from prompt; see merge-gate-result.json.`);
5400
+ }
5026
5401
  const lines = [
5027
5402
  `Strict PR merge gate blocked ${evidence.prUrl}.`,
5028
5403
  `PR title: ${evidence.title || "(empty)"}`,
@@ -5031,10 +5406,13 @@ function buildStrictPrGateSteeringPrompt(result) {
5031
5406
  evidence.greptile.score ? `Greptile score: ${evidence.greptile.score.value}/${evidence.greptile.score.scale}` : "Greptile score: not proven",
5032
5407
  "",
5033
5408
  "Gate reasons:",
5034
- ...result.reasons.length ? result.reasons.map((reason) => `- ${reason}`) : ["- No reasons recorded"],
5409
+ ...displayedReasons.length ? displayedReasons : ["- No reasons recorded"],
5410
+ "",
5411
+ "Structured gate reason details:",
5412
+ result.reasonDetails.length ? promptJsonExcerpt(result.reasonDetails, 4000) : "[]",
5035
5413
  "",
5036
5414
  "Required evidence read status:",
5037
- evidence.readErrors.length ? JSON.stringify(evidence.readErrors, null, 2) : "All required PR evidence surfaces were read completely.",
5415
+ evidence.readErrors.length ? promptJsonExcerpt(evidence.readErrors, 2000) : "All required PR evidence surfaces were read completely.",
5038
5416
  "",
5039
5417
  "Full PR title:",
5040
5418
  evidence.title || "(empty)",
@@ -5098,6 +5476,7 @@ function persistPrReviewCycleArtifacts(input) {
5098
5476
  approved: input.result.approved,
5099
5477
  pending: input.result.pending,
5100
5478
  reasons: input.result.reasons,
5479
+ reasonDetails: input.result.reasonDetails,
5101
5480
  warnings: input.result.warnings,
5102
5481
  actionableFeedback: input.result.actionableFeedback,
5103
5482
  prUrl: input.result.evidence.prUrl,
@@ -5932,7 +6311,7 @@ async function runGreptileReviewForPr(options) {
5932
6311
  };
5933
6312
  }
5934
6313
  const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
5935
- if (/not safe(?: to merge)?|unsafe(?: to merge)?|do not merge|cannot merge|blockers?|must fix|changes requested|please fix|needs? fix|fix this|address this/i.test(blockerScanBody)) {
6314
+ if (/not safe(?: to merge)?|unsafe(?: to merge)?|do not merge|cannot merge|blockers?|must fix|changes requested|please fix|needs? fix|fix this|address this|\breject(?:ed|ion)?\b|\bskip(?:ped)?\b|status\s*:\s*(?:reject(?:ed)?|skip(?:ped)?|failed)/i.test(blockerScanBody)) {
5936
6315
  reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
5937
6316
  return {
5938
6317
  verdict: "REJECT",
@@ -6014,6 +6393,7 @@ async function runGreptileReviewForPr(options) {
6014
6393
  approved: strictGate.approved,
6015
6394
  pending: strictGate.pending,
6016
6395
  reasons: strictGate.reasons,
6396
+ reasonDetails: strictGate.reasonDetails,
6017
6397
  warnings: strictGate.warnings,
6018
6398
  greptile: strictGate.evidence.greptile,
6019
6399
  readErrors: strictGate.evidence.readErrors
@@ -6036,6 +6416,7 @@ async function runGreptileReviewForPr(options) {
6036
6416
  approved: strictGate.approved,
6037
6417
  pending: strictGate.pending,
6038
6418
  reasons: strictGate.reasons,
6419
+ reasonDetails: strictGate.reasonDetails,
6039
6420
  warnings: strictGate.warnings,
6040
6421
  greptile: strictGate.evidence.greptile,
6041
6422
  readErrors: strictGate.evidence.readErrors
@@ -6162,6 +6543,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
6162
6543
  approved: strictGate.approved,
6163
6544
  pending: strictGate.pending,
6164
6545
  reasons: strictGate.reasons,
6546
+ reasonDetails: strictGate.reasonDetails,
6165
6547
  warnings: strictGate.warnings,
6166
6548
  greptile: strictGate.evidence.greptile
6167
6549
  },
@@ -6184,6 +6566,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
6184
6566
  approved: strictGate.approved,
6185
6567
  pending: strictGate.pending,
6186
6568
  reasons: strictGate.reasons,
6569
+ reasonDetails: strictGate.reasonDetails,
6187
6570
  warnings: strictGate.warnings,
6188
6571
  greptile: strictGate.evidence.greptile
6189
6572
  },
@@ -6299,19 +6682,25 @@ function shouldTriggerGreptileReview(existingReview, expectedHeadSha) {
6299
6682
  if ((existingReview.metadata?.checkHeadSha || "") !== expectedHeadSha) {
6300
6683
  return true;
6301
6684
  }
6302
- return isGreptileReviewTerminal(existingReview.status);
6685
+ return false;
6303
6686
  }
6304
6687
  function shouldContinueGreptileMcpPolling(options) {
6305
6688
  if (options.githubCheckState.completed) {
6306
6689
  return false;
6307
6690
  }
6691
+ if (options.attempt + 1 >= options.pollAttempts) {
6692
+ return false;
6693
+ }
6308
6694
  if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
6309
6695
  return true;
6310
6696
  }
6311
- return options.attempt + 1 < options.pollAttempts;
6697
+ return true;
6312
6698
  }
6313
6699
  function shouldContinueGithubGreptileFallbackPolling(options) {
6314
6700
  const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
6701
+ if (options.attempt + 1 >= options.pollAttempts) {
6702
+ return false;
6703
+ }
6315
6704
  if (waitingForVisiblePendingReview) {
6316
6705
  return true;
6317
6706
  }
@@ -7047,7 +7436,7 @@ function gitOpenPr(options) {
7047
7436
  "",
7048
7437
  "## Review",
7049
7438
  "- Completion verification will run validation, verifier review, and PR policy checks.",
7050
- "- When repository policy allows it, Rig enables GitHub auto-merge after approval."
7439
+ "- When repository policy allows it, Rig attempts an immediate strict-gated, head-locked merge after approval."
7051
7440
  ].join(`
7052
7441
  `);
7053
7442
  const preCheck = runCapture2(withGhRepo([gh, "pr", "list", "--state", "merged", "--head", branch, "--json", "url,mergedAt", "--jq", ".[0]"], repoNameWithOwner), repoRoot);
@@ -7243,15 +7632,12 @@ function gitMergePr(options) {
7243
7632
  return { status: "already-merged", url: options.pr.url };
7244
7633
  }
7245
7634
  if (state !== "OPEN") {
7246
- throw new Error(`Cannot auto-merge PR ${options.pr.url}: state is ${state}.`);
7635
+ throw new Error(`Cannot merge PR ${options.pr.url}: state is ${state}.`);
7247
7636
  }
7248
7637
  if (isDraft) {
7249
- throw new Error(`Cannot auto-merge draft PR ${options.pr.url}.`);
7250
- }
7251
- const strictGateHeadSha = options.strictGateHeadSha?.trim();
7252
- if (!strictGateHeadSha) {
7253
- throw new Error(`Refusing to merge PR ${options.pr.url}: strict merge gate did not provide a current head SHA.`);
7638
+ throw new Error(`Cannot merge draft PR ${options.pr.url}.`);
7254
7639
  }
7640
+ const strictGateHeadSha = strictMergeHeadShaFromGate(options.strictGate, options.pr.url);
7255
7641
  const mergeArgs = withGhRepo([gh, "pr", "merge", options.pr.url], repoNameWithOwner);
7256
7642
  const method = options.method || "squash";
7257
7643
  mergeArgs.push(method === "merge" ? "--merge" : method === "rebase" ? "--rebase" : "--squash");
@@ -8242,7 +8628,6 @@ async function main() {
8242
8628
  if (prs.length === 0) {
8243
8629
  console.log("Auto-merge: skipped (no PR metadata found)");
8244
8630
  } else {
8245
- let mergePending = false;
8246
8631
  let cycle = 0;
8247
8632
  for (const pr of prs) {
8248
8633
  cycle += 1;
@@ -8271,16 +8656,13 @@ async function main() {
8271
8656
  pr,
8272
8657
  method: "squash",
8273
8658
  deleteBranch: true,
8274
- strictGateHeadSha: gate.evidence.headSha
8659
+ strictGate: gate
8275
8660
  });
8276
- if (mergeResult.status === "auto-merge-enabled") {
8277
- mergePending = true;
8278
- console.log(`WAIT: Auto-merge enabled but PR is still open (${pr.repoLabel}): ${pr.url}`);
8661
+ if (mergeResult.status === "merged" || mergeResult.status === "already-merged") {
8662
+ console.log(`OK: PR merge confirmed (${pr.repoLabel}): ${pr.url}`);
8279
8663
  }
8280
8664
  }
8281
- if (mergePending) {
8282
- failed = true;
8283
- } else if (!failed) {
8665
+ if (!failed) {
8284
8666
  sourceCloseoutAllowed = true;
8285
8667
  console.log("OK: Auto-merge complete");
8286
8668
  }