paperclip-github-plugin 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/worker.js CHANGED
@@ -728,6 +728,17 @@ var PENDING_STATUS_CONTEXT_STATES = /* @__PURE__ */ new Set(["EXPECTED", "PENDIN
728
728
  var GITHUB_REPOSITORY_MAINTAINER_WARMUP_CONCURRENCY = 4;
729
729
  var GITHUB_REPOSITORY_MAINTAINER_ROLE_NAMES = /* @__PURE__ */ new Set(["admin", "maintain"]);
730
730
  var GITHUB_REPOSITORY_TRUSTED_AUTHOR_ASSOCIATIONS = /* @__PURE__ */ new Set(["collaborator", "member", "owner"]);
731
+ var ACTION_REQUIRED_GITHUB_PULL_REQUEST_MERGE_STATE_STATUSES = /* @__PURE__ */ new Set([
732
+ "behind",
733
+ "blocked",
734
+ "dirty",
735
+ "draft",
736
+ "unstable"
737
+ ]);
738
+ var REVIEW_READY_GITHUB_PULL_REQUEST_MERGE_STATE_STATUSES = /* @__PURE__ */ new Set([
739
+ "clean",
740
+ "has_hooks"
741
+ ]);
731
742
  var GITHUB_ISSUE_STATUS_SNAPSHOT_QUERY = `
732
743
  query GitHubIssueStatusSnapshot($owner: String!, $repo: String!, $issueNumber: Int!, $after: String) {
733
744
  repository(owner: $owner, name: $repo) {
@@ -762,6 +773,8 @@ var GITHUB_PULL_REQUEST_CI_CONTEXTS_QUERY = `
762
773
  query GitHubPullRequestCiContexts($owner: String!, $repo: String!, $pullRequestNumber: Int!, $after: String) {
763
774
  repository(owner: $owner, name: $repo) {
764
775
  pullRequest(number: $pullRequestNumber) {
776
+ mergeable
777
+ mergeStateStatus
765
778
  statusCheckRollup {
766
779
  contexts(first: 100, after: $after) {
767
780
  pageInfo {
@@ -825,6 +838,8 @@ var GITHUB_REPOSITORY_OPEN_PULL_REQUEST_STATUSES_QUERY = `
825
838
  }
826
839
  nodes {
827
840
  number
841
+ mergeable
842
+ mergeStateStatus
828
843
  reviewThreads(first: 100) {
829
844
  pageInfo {
830
845
  hasNextPage
@@ -950,6 +965,8 @@ var GITHUB_PROJECT_PULL_REQUEST_SUMMARY_FIELDS = `${GITHUB_PROJECT_PULL_REQUEST_
950
965
  var GITHUB_PROJECT_PULL_REQUEST_METRICS_FIELDS = `
951
966
  number
952
967
  mergeable
968
+ mergeStateStatus
969
+ baseRefName
953
970
  reviews(first: 100) {
954
971
  pageInfo {
955
972
  hasNextPage
@@ -2219,6 +2236,7 @@ async function resolvePaperclipIssueGitHubLink(ctx, issueId, companyId, options
2219
2236
  githubIssueNumber: entityMatch.data.githubIssueNumber,
2220
2237
  githubIssueUrl: entityMatch.data.githubIssueUrl,
2221
2238
  linkedPullRequestNumbers: entityMatch.data.linkedPullRequestNumbers,
2239
+ linkedPullRequests: entityMatch.data.linkedPullRequests,
2222
2240
  entityRecord: entityMatch
2223
2241
  };
2224
2242
  }
@@ -2240,7 +2258,8 @@ async function resolvePaperclipIssueGitHubLink(ctx, issueId, companyId, options
2240
2258
  githubIssueId: registryMatch.githubIssueId,
2241
2259
  githubIssueNumber: registryMatch.githubIssueNumber,
2242
2260
  githubIssueUrl: githubIssueUrl2,
2243
- linkedPullRequestNumbers: []
2261
+ linkedPullRequestNumbers: [],
2262
+ linkedPullRequests: []
2244
2263
  };
2245
2264
  return await hydrateRecoveredPaperclipIssueGitHubLink(ctx, issueId, fallbackLink2) ?? fallbackLink2;
2246
2265
  }
@@ -2258,7 +2277,8 @@ async function resolvePaperclipIssueGitHubLink(ctx, issueId, companyId, options
2258
2277
  repositoryUrl: githubIssueReference.repositoryUrl,
2259
2278
  githubIssueNumber: githubIssueReference.issueNumber,
2260
2279
  githubIssueUrl: githubIssueReference.issueUrl,
2261
- linkedPullRequestNumbers: []
2280
+ linkedPullRequestNumbers: [],
2281
+ linkedPullRequests: []
2262
2282
  };
2263
2283
  return await hydrateRecoveredPaperclipIssueGitHubLink(ctx, issueId, fallbackLink) ?? fallbackLink;
2264
2284
  }
@@ -2293,7 +2313,7 @@ async function hydrateRecoveredPaperclipIssueGitHubLink(ctx, issueId, fallbackLi
2293
2313
  },
2294
2314
  issueId,
2295
2315
  githubIssue,
2296
- linkedPullRequestNumbers
2316
+ linkedPullRequests
2297
2317
  );
2298
2318
  await upsertGitHubIssueLinkRecord(
2299
2319
  ctx,
@@ -2304,7 +2324,7 @@ async function hydrateRecoveredPaperclipIssueGitHubLink(ctx, issueId, fallbackLi
2304
2324
  },
2305
2325
  issueId,
2306
2326
  githubIssue,
2307
- linkedPullRequestNumbers
2327
+ linkedPullRequests
2308
2328
  );
2309
2329
  return {
2310
2330
  source: "entity",
@@ -2315,6 +2335,7 @@ async function hydrateRecoveredPaperclipIssueGitHubLink(ctx, issueId, fallbackLi
2315
2335
  githubIssueNumber: githubIssue.number,
2316
2336
  githubIssueUrl: normalizeGitHubIssueHtmlUrl(githubIssue.htmlUrl) ?? githubIssue.htmlUrl,
2317
2337
  linkedPullRequestNumbers,
2338
+ linkedPullRequests: normalizeLinkedPullRequestReferences(linkedPullRequests, fallbackLink.repositoryUrl),
2318
2339
  entityRecord
2319
2340
  };
2320
2341
  } catch (error) {
@@ -2418,11 +2439,12 @@ function buildCommentAnnotationLinksFromStoredData(annotation) {
2418
2439
  href: annotation.githubIssueUrl
2419
2440
  }
2420
2441
  ];
2421
- for (const pullRequestNumber of annotation.linkedPullRequestNumbers) {
2442
+ const linkedPullRequests = annotation.linkedPullRequests.length > 0 ? annotation.linkedPullRequests : normalizeLinkedPullRequestReferences(annotation.linkedPullRequestNumbers, annotation.repositoryUrl);
2443
+ for (const pullRequest of linkedPullRequests) {
2422
2444
  links.push({
2423
2445
  type: "pull_request",
2424
- label: `PR #${pullRequestNumber}`,
2425
- href: `${annotation.repositoryUrl}/pull/${pullRequestNumber}`
2446
+ label: formatLinkedPullRequestReferenceLabel(pullRequest, annotation.repositoryUrl),
2447
+ href: `${pullRequest.repositoryUrl}/pull/${pullRequest.number}`
2426
2448
  });
2427
2449
  }
2428
2450
  return links;
@@ -2540,6 +2562,7 @@ async function buildIssueGitHubDetails(ctx, input) {
2540
2562
  githubIssueStateReason: entityMatch.data.githubIssueStateReason,
2541
2563
  commentsCount: entityMatch.data.commentsCount,
2542
2564
  linkedPullRequestNumbers: entityMatch.data.linkedPullRequestNumbers,
2565
+ linkedPullRequests: entityMatch.data.linkedPullRequests,
2543
2566
  labels: entityMatch.data.labels,
2544
2567
  syncedAt: entityMatch.data.syncedAt
2545
2568
  };
@@ -2550,7 +2573,8 @@ async function buildIssueGitHubDetails(ctx, input) {
2550
2573
  githubIssueNumber: link.githubIssueNumber,
2551
2574
  githubIssueUrl: link.githubIssueUrl,
2552
2575
  repositoryUrl: link.repositoryUrl,
2553
- linkedPullRequestNumbers: link.linkedPullRequestNumbers
2576
+ linkedPullRequestNumbers: link.linkedPullRequestNumbers,
2577
+ linkedPullRequests: link.linkedPullRequests
2554
2578
  };
2555
2579
  }
2556
2580
  async function resolveIssueByIdentifier(ctx, input) {
@@ -3698,6 +3722,9 @@ function normalizeImportRegistry(value) {
3698
3722
  const paperclipProjectId = typeof record.paperclipProjectId === "string" && record.paperclipProjectId.trim() ? record.paperclipProjectId.trim() : void 0;
3699
3723
  const companyId = typeof record.companyId === "string" && record.companyId.trim() ? record.companyId.trim() : void 0;
3700
3724
  const lastSeenCommentCount = typeof record.lastSeenCommentCount === "number" && record.lastSeenCommentCount >= 0 ? Math.floor(record.lastSeenCommentCount) : void 0;
3725
+ const linkedPullRequestCommentCounts = normalizeGitHubPullRequestCommentCountRecords(
3726
+ record.linkedPullRequestCommentCounts
3727
+ );
3701
3728
  if (!mappingId || Number.isNaN(githubIssueId) || !paperclipIssueId || !importedAt) {
3702
3729
  return null;
3703
3730
  }
@@ -3710,7 +3737,8 @@ function normalizeImportRegistry(value) {
3710
3737
  ...repositoryUrl ? { repositoryUrl } : {},
3711
3738
  ...paperclipProjectId ? { paperclipProjectId } : {},
3712
3739
  ...companyId ? { companyId } : {},
3713
- ...lastSeenCommentCount !== void 0 ? { lastSeenCommentCount } : {}
3740
+ ...lastSeenCommentCount !== void 0 ? { lastSeenCommentCount } : {},
3741
+ ...linkedPullRequestCommentCounts.length > 0 ? { linkedPullRequestCommentCounts } : {}
3714
3742
  };
3715
3743
  }).filter((entry) => entry !== null);
3716
3744
  }
@@ -3756,6 +3784,7 @@ function buildImportedIssueRecord(mapping, issue, paperclipIssueId, importedAt)
3756
3784
  paperclipIssueId,
3757
3785
  importedAt,
3758
3786
  lastSeenCommentCount: issue.commentsCount,
3787
+ linkedPullRequestCommentCounts: [],
3759
3788
  repositoryUrl: getNormalizedMappingRepositoryUrl(mapping),
3760
3789
  paperclipProjectId: mapping.paperclipProjectId,
3761
3790
  companyId: mapping.companyId
@@ -4010,8 +4039,13 @@ function normalizeProjectPullRequestClosingIssues(repository, nodes) {
4010
4039
  function resolveProjectPullRequestReviewable(record) {
4011
4040
  return record.githubMergeable === true && record.checksStatus === "passed" && typeof record.copilotUnresolvedReviewThreads === "number" && record.copilotUnresolvedReviewThreads === 0;
4012
4041
  }
4042
+ function resolveProjectPullRequestTargetsDefaultBranch(record) {
4043
+ const baseBranch = normalizeOptionalString2(record.baseBranch);
4044
+ const defaultBranchName = normalizeOptionalString2(record.defaultBranchName);
4045
+ return Boolean(baseBranch && defaultBranchName && baseBranch === defaultBranchName);
4046
+ }
4013
4047
  function resolveProjectPullRequestMergeable(record) {
4014
- return record.githubMergeable === true && record.checksStatus === "passed" && typeof record.reviewApprovals === "number" && record.reviewApprovals > 0 && typeof record.unresolvedReviewThreads === "number" && record.unresolvedReviewThreads === 0;
4048
+ return record.githubMergeable === true && record.checksStatus === "passed" && typeof record.reviewApprovals === "number" && record.reviewApprovals > 0 && typeof record.reviewChangesRequested === "number" && record.reviewChangesRequested === 0 && typeof record.unresolvedReviewThreads === "number" && record.unresolvedReviewThreads === 0 && resolveProjectPullRequestTargetsDefaultBranch(record);
4015
4049
  }
4016
4050
  function resolveProjectPullRequestUpToDateStatus(record) {
4017
4051
  const mergeStateStatus = typeof record.mergeStateStatus === "string" ? record.mergeStateStatus : null;
@@ -4194,12 +4228,12 @@ function classifyGitHubPullRequestCiState(contexts) {
4194
4228
  return hasPendingContext ? "unfinished" : "green";
4195
4229
  }
4196
4230
  function resolvePaperclipStatusFromLinkedPullRequests(linkedPullRequests, options) {
4197
- if (linkedPullRequests.some((pullRequest) => pullRequest.hasUnresolvedReviewThreads || pullRequest.ciState === "red")) {
4231
+ if (linkedPullRequests.some(
4232
+ (pullRequest) => pullRequest.hasUnresolvedReviewThreads || pullRequest.ciState === "red" || isGitHubPullRequestActionRequiredForSync(pullRequest)
4233
+ )) {
4198
4234
  return options?.preferInProgress ? "in_progress" : "todo";
4199
4235
  }
4200
- if (linkedPullRequests.length > 0 && linkedPullRequests.every(
4201
- (pullRequest) => pullRequest.ciState === "green" && pullRequest.hasUnresolvedReviewThreads === false
4202
- )) {
4236
+ if (linkedPullRequests.length > 0 && linkedPullRequests.every((pullRequest) => isGitHubPullRequestReviewReadyForSync(pullRequest))) {
4203
4237
  return "in_review";
4204
4238
  }
4205
4239
  return "in_progress";
@@ -4445,15 +4479,19 @@ function resolvePaperclipIssueReviewAssignee(syncContext, advancedSettings) {
4445
4479
  role: nextStage.type === "approval" ? "approver" : "reviewer"
4446
4480
  };
4447
4481
  }
4482
+ const reviewStageType = nextStage?.type ?? currentStageType;
4483
+ if (!reviewStageType && !syncContext.executionPolicy) {
4484
+ return null;
4485
+ }
4448
4486
  const approverOverride = getConfiguredAdvancedAssigneePrincipal(advancedSettings, "approver");
4449
- if (nextStage?.type === "approval" && approverOverride) {
4487
+ if (reviewStageType === "approval" && approverOverride) {
4450
4488
  return {
4451
4489
  principal: approverOverride,
4452
4490
  role: "approver"
4453
4491
  };
4454
4492
  }
4455
4493
  const reviewerOverride = getConfiguredAdvancedAssigneePrincipal(advancedSettings, "reviewer");
4456
- if (reviewerOverride) {
4494
+ if (reviewStageType !== "approval" && reviewerOverride) {
4457
4495
  return {
4458
4496
  principal: reviewerOverride,
4459
4497
  role: "reviewer"
@@ -4541,6 +4579,9 @@ function buildSyncFallbackExecutionStatePatch(params) {
4541
4579
  if ((currentStatus === "done" || currentStatus === "cancelled") && nextStatus !== "done" && nextStatus !== "cancelled") {
4542
4580
  return previousState ? null : void 0;
4543
4581
  }
4582
+ if (nextStatus === "done" || nextStatus === "cancelled") {
4583
+ return previousState ? null : void 0;
4584
+ }
4544
4585
  if (nextStatus === "in_review" && syncContext.executionPolicy) {
4545
4586
  const nextStageMatch = findPaperclipIssueExecutionStageMatch(syncContext.executionPolicy, previousState);
4546
4587
  if (!nextStageMatch) {
@@ -4576,7 +4617,7 @@ function buildSyncFallbackExecutionStatePatch(params) {
4576
4617
  return void 0;
4577
4618
  }
4578
4619
  function describeGitHubStatusTransitionReason(params) {
4579
- const { snapshot, previousCommentCount, hasTrustedNewComment, maintainerAuthoredImportedIssue } = params;
4620
+ const { snapshot, hasTrustedNewComment, maintainerAuthoredImportedIssue } = params;
4580
4621
  if (snapshot.state === "closed") {
4581
4622
  switch (snapshot.stateReason) {
4582
4623
  case "duplicate":
@@ -4587,8 +4628,7 @@ function describeGitHubStatusTransitionReason(params) {
4587
4628
  return "the GitHub issue was closed as completed work";
4588
4629
  }
4589
4630
  }
4590
- const baselineCommentCount = previousCommentCount ?? snapshot.commentCount;
4591
- if (snapshot.commentCount > baselineCommentCount && hasTrustedNewComment) {
4631
+ if (hasTrustedNewComment) {
4592
4632
  return "a new GitHub comment from the issue author or a repository maintainer was added";
4593
4633
  }
4594
4634
  if (snapshot.linkedPullRequests.length === 0) {
@@ -4599,32 +4639,33 @@ function describeGitHubStatusTransitionReason(params) {
4599
4639
  }
4600
4640
  const linkedPullRequestSubject = snapshot.linkedPullRequests.length === 1 ? "the linked pull request" : "linked pull requests";
4601
4641
  const linkedPullRequestVerb = snapshot.linkedPullRequests.length === 1 ? "has" : "have";
4602
- const hasRedCi = snapshot.linkedPullRequests.some((pullRequest) => pullRequest.ciState === "red");
4603
- const hasUnresolvedReviewThreads = snapshot.linkedPullRequests.some((pullRequest) => pullRequest.hasUnresolvedReviewThreads);
4642
+ const blockingConditions = [...new Set(
4643
+ snapshot.linkedPullRequests.flatMap((pullRequest) => listGitHubPullRequestSyncBlockingConditions(pullRequest))
4644
+ )];
4604
4645
  const hasUnfinishedCi = snapshot.linkedPullRequests.some((pullRequest) => pullRequest.ciState === "unfinished");
4605
- if (hasRedCi && hasUnresolvedReviewThreads) {
4606
- return `${linkedPullRequestSubject} ${linkedPullRequestVerb} failing CI with unresolved review threads`;
4607
- }
4608
- if (hasRedCi) {
4609
- return `${linkedPullRequestSubject} ${linkedPullRequestVerb} failing CI`;
4610
- }
4611
- if (hasUnresolvedReviewThreads) {
4612
- return `${linkedPullRequestSubject} ${linkedPullRequestVerb} unresolved review threads`;
4646
+ const hasUnknownMergeability = snapshot.linkedPullRequests.some(
4647
+ (pullRequest) => pullRequest.mergeStateStatus === "unknown"
4648
+ );
4649
+ if (blockingConditions.length > 0) {
4650
+ return `${linkedPullRequestSubject} ${linkedPullRequestVerb} ${formatPlainTextList(blockingConditions)}`;
4613
4651
  }
4614
4652
  if (hasUnfinishedCi) {
4615
4653
  return `${linkedPullRequestSubject} still ${linkedPullRequestVerb} unfinished CI jobs`;
4616
4654
  }
4655
+ if (hasUnknownMergeability) {
4656
+ return `${linkedPullRequestSubject} ${linkedPullRequestVerb} unknown mergeability`;
4657
+ }
4617
4658
  return `${linkedPullRequestSubject} ${linkedPullRequestVerb} green CI with all review threads resolved`;
4618
4659
  }
4619
4660
  function buildStatusTransitionCommentAnnotation(params) {
4620
4661
  const { repository, snapshot, previousStatus, nextStatus, reason } = params;
4662
+ const linkedPullRequests = normalizeLinkedPullRequestReferences(snapshot.linkedPullRequests);
4621
4663
  return {
4622
4664
  repositoryUrl: repository.url,
4623
4665
  githubIssueNumber: snapshot.issueNumber,
4624
4666
  githubIssueUrl: `${repository.url}/issues/${snapshot.issueNumber}`,
4625
- linkedPullRequestNumbers: normalizeLinkedPullRequestNumbers(
4626
- snapshot.linkedPullRequests.map((pullRequest) => pullRequest.number)
4627
- ),
4667
+ linkedPullRequestNumbers: normalizeLinkedPullRequestNumbers(linkedPullRequests.map((pullRequest) => pullRequest.number)),
4668
+ linkedPullRequests,
4628
4669
  previousStatus,
4629
4670
  nextStatus,
4630
4671
  reason,
@@ -4638,13 +4679,11 @@ function buildPaperclipIssueStatusTransitionComment(params) {
4638
4679
  nextStatus,
4639
4680
  repository,
4640
4681
  snapshot,
4641
- previousCommentCount,
4642
4682
  hasTrustedNewComment,
4643
4683
  maintainerAuthoredImportedIssue
4644
4684
  } = params;
4645
4685
  const reason = describeGitHubStatusTransitionReason({
4646
4686
  snapshot,
4647
- previousCommentCount,
4648
4687
  hasTrustedNewComment,
4649
4688
  maintainerAuthoredImportedIssue
4650
4689
  });
@@ -4663,7 +4702,6 @@ function resolvePaperclipIssueStatus(params) {
4663
4702
  const {
4664
4703
  currentStatus,
4665
4704
  snapshot,
4666
- previousCommentCount,
4667
4705
  hasTrustedNewComment,
4668
4706
  wasImportedThisRun,
4669
4707
  defaultImportedStatus,
@@ -4676,8 +4714,7 @@ function resolvePaperclipIssueStatus(params) {
4676
4714
  if (currentStatus === "backlog" && !wasImportedThisRun) {
4677
4715
  return "backlog";
4678
4716
  }
4679
- const baselineCommentCount = previousCommentCount ?? snapshot.commentCount;
4680
- if (snapshot.commentCount > baselineCommentCount && hasTrustedNewComment) {
4717
+ if (hasTrustedNewComment) {
4681
4718
  return hasExecutorHandoffTarget ? "in_progress" : "todo";
4682
4719
  }
4683
4720
  if (snapshot.linkedPullRequests.length > 0) {
@@ -4695,7 +4732,7 @@ function resolvePaperclipIssueStatus(params) {
4695
4732
  }
4696
4733
  async function listLinkedPullRequestsForIssue(octokit, repository, issueNumber) {
4697
4734
  const linkedPullRequests = [];
4698
- const seenPullRequestNumbers = /* @__PURE__ */ new Set();
4735
+ const seenPullRequestKeys = /* @__PURE__ */ new Set();
4699
4736
  let after;
4700
4737
  do {
4701
4738
  const response = await octokit.graphql(GITHUB_ISSUE_STATUS_SNAPSHOT_QUERY, {
@@ -4707,31 +4744,37 @@ async function listLinkedPullRequestsForIssue(octokit, repository, issueNumber)
4707
4744
  const nextLinkedPullRequests = collectGitHubLinkedPullRequests(
4708
4745
  response.repository?.issue?.closedByPullRequestsReferences?.nodes ?? [],
4709
4746
  repository,
4710
- seenPullRequestNumbers
4747
+ seenPullRequestKeys
4711
4748
  );
4712
4749
  linkedPullRequests.push(...nextLinkedPullRequests);
4713
4750
  after = getPageCursor(response.repository?.issue?.closedByPullRequestsReferences?.pageInfo);
4714
4751
  } while (after);
4715
4752
  return linkedPullRequests;
4716
4753
  }
4717
- function collectGitHubLinkedPullRequests(nodes, repository, seenPullRequestNumbers = /* @__PURE__ */ new Set()) {
4754
+ function collectGitHubLinkedPullRequests(nodes, repository, seenPullRequestKeys = /* @__PURE__ */ new Set()) {
4718
4755
  const linkedPullRequests = [];
4719
4756
  for (const node of nodes) {
4720
- if (!node || typeof node.number !== "number" || !node.state || seenPullRequestNumbers.has(node.number)) {
4757
+ if (!node || typeof node.number !== "number" || !node.state) {
4721
4758
  continue;
4722
4759
  }
4723
4760
  const pullRequestOwner = node.repository?.owner?.login?.trim();
4724
4761
  const pullRequestRepo = node.repository?.name?.trim();
4725
- if (pullRequestOwner && pullRequestRepo && !areRepositoriesEqual(repository, {
4726
- owner: pullRequestOwner,
4727
- repo: pullRequestRepo
4728
- })) {
4762
+ const pullRequestRepository = pullRequestOwner && pullRequestRepo ? parseRepositoryReference(`${pullRequestOwner}/${pullRequestRepo}`) : repository;
4763
+ if (!pullRequestRepository) {
4729
4764
  continue;
4730
4765
  }
4731
- seenPullRequestNumbers.add(node.number);
4766
+ const pullRequestKey = buildGitHubPullRequestReferenceKey({
4767
+ number: node.number,
4768
+ repositoryUrl: pullRequestRepository.url
4769
+ });
4770
+ if (seenPullRequestKeys.has(pullRequestKey)) {
4771
+ continue;
4772
+ }
4773
+ seenPullRequestKeys.add(pullRequestKey);
4732
4774
  linkedPullRequests.push({
4733
4775
  number: node.number,
4734
- state: node.state
4776
+ state: node.state,
4777
+ repositoryUrl: pullRequestRepository.url
4735
4778
  });
4736
4779
  }
4737
4780
  return linkedPullRequests;
@@ -4789,7 +4832,75 @@ function extractGitHubCiContextRecords(nodes) {
4789
4832
  }
4790
4833
  return contexts;
4791
4834
  }
4792
- function tryBuildGitHubPullRequestStatusSnapshotFromBatchNode(node) {
4835
+ function normalizeGitHubPullRequestMergeability(value) {
4836
+ if (value === "MERGEABLE") {
4837
+ return "mergeable";
4838
+ }
4839
+ if (value === "CONFLICTING") {
4840
+ return "conflicting";
4841
+ }
4842
+ return "unknown";
4843
+ }
4844
+ function normalizeGitHubPullRequestMergeStateStatus(value) {
4845
+ switch (typeof value === "string" ? value.trim().toLowerCase() : "") {
4846
+ case "behind":
4847
+ return "behind";
4848
+ case "blocked":
4849
+ return "blocked";
4850
+ case "clean":
4851
+ return "clean";
4852
+ case "dirty":
4853
+ return "dirty";
4854
+ case "draft":
4855
+ return "draft";
4856
+ case "has_hooks":
4857
+ return "has_hooks";
4858
+ case "unstable":
4859
+ return "unstable";
4860
+ default:
4861
+ return "unknown";
4862
+ }
4863
+ }
4864
+ function isGitHubPullRequestActionRequiredForSync(pullRequest) {
4865
+ return pullRequest.mergeability === "conflicting" || ACTION_REQUIRED_GITHUB_PULL_REQUEST_MERGE_STATE_STATUSES.has(pullRequest.mergeStateStatus);
4866
+ }
4867
+ function isGitHubPullRequestReviewReadyForSync(pullRequest) {
4868
+ if (pullRequest.ciState !== "green" || pullRequest.hasUnresolvedReviewThreads) {
4869
+ return false;
4870
+ }
4871
+ return REVIEW_READY_GITHUB_PULL_REQUEST_MERGE_STATE_STATUSES.has(pullRequest.mergeStateStatus);
4872
+ }
4873
+ function listGitHubPullRequestSyncBlockingConditions(pullRequest) {
4874
+ const conditions = [];
4875
+ if (pullRequest.ciState === "red") {
4876
+ conditions.push("failing CI");
4877
+ }
4878
+ if (pullRequest.mergeability === "conflicting" || pullRequest.mergeStateStatus === "dirty") {
4879
+ conditions.push("merge conflicts");
4880
+ } else {
4881
+ switch (pullRequest.mergeStateStatus) {
4882
+ case "behind":
4883
+ conditions.push("out-of-date branch state");
4884
+ break;
4885
+ case "blocked":
4886
+ conditions.push("blocked merge requirements");
4887
+ break;
4888
+ case "draft":
4889
+ conditions.push("draft status");
4890
+ break;
4891
+ case "unstable":
4892
+ conditions.push("unstable merge requirements");
4893
+ break;
4894
+ default:
4895
+ break;
4896
+ }
4897
+ }
4898
+ if (pullRequest.hasUnresolvedReviewThreads) {
4899
+ conditions.push("unresolved review threads");
4900
+ }
4901
+ return conditions;
4902
+ }
4903
+ function tryBuildGitHubPullRequestStatusSnapshotFromBatchNode(node, repository) {
4793
4904
  if (typeof node.number !== "number") {
4794
4905
  return null;
4795
4906
  }
@@ -4800,8 +4911,11 @@ function tryBuildGitHubPullRequestStatusSnapshotFromBatchNode(node) {
4800
4911
  }
4801
4912
  return {
4802
4913
  number: node.number,
4914
+ repositoryUrl: repository.url,
4803
4915
  hasUnresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads > 0,
4804
- ciState
4916
+ ciState,
4917
+ mergeability: normalizeGitHubPullRequestMergeability(node.mergeable),
4918
+ mergeStateStatus: normalizeGitHubPullRequestMergeStateStatus(node.mergeStateStatus)
4805
4919
  };
4806
4920
  }
4807
4921
  function tryBuildGitHubPullRequestCiStateFromBatchNode(node) {
@@ -4816,7 +4930,9 @@ async function warmGitHubPullRequestStatusCache(octokit, repository, targetPullR
4816
4930
  return;
4817
4931
  }
4818
4932
  const remainingNumbers = new Set(
4819
- [...targetPullRequestNumbers].filter((pullRequestNumber) => !pullRequestStatusCache.has(pullRequestNumber))
4933
+ [...targetPullRequestNumbers].filter(
4934
+ (pullRequestNumber) => !getCachedGitHubPullRequestStatusSnapshot(pullRequestStatusCache, repository, pullRequestNumber)
4935
+ )
4820
4936
  );
4821
4937
  if (remainingNumbers.size === 0) {
4822
4938
  return;
@@ -4837,9 +4953,9 @@ async function warmGitHubPullRequestStatusCache(octokit, repository, targetPullR
4837
4953
  continue;
4838
4954
  }
4839
4955
  remainingNumbers.delete(node.number);
4840
- const snapshot = tryBuildGitHubPullRequestStatusSnapshotFromBatchNode(node);
4956
+ const snapshot = tryBuildGitHubPullRequestStatusSnapshotFromBatchNode(node, repository);
4841
4957
  if (snapshot) {
4842
- pullRequestStatusCache.set(node.number, snapshot);
4958
+ setCachedGitHubPullRequestStatusSnapshot(pullRequestStatusCache, snapshot);
4843
4959
  cacheGitHubPullRequestStatusSnapshot(repository, snapshot);
4844
4960
  }
4845
4961
  }
@@ -4850,7 +4966,12 @@ async function warmGitHubPullRequestStatusCache(octokit, repository, targetPullR
4850
4966
  } while (after);
4851
4967
  }
4852
4968
  async function getGitHubPullRequestCiState(octokit, repository, pullRequestNumber) {
4969
+ return (await getGitHubPullRequestCiSnapshot(octokit, repository, pullRequestNumber)).ciState;
4970
+ }
4971
+ async function getGitHubPullRequestCiSnapshot(octokit, repository, pullRequestNumber) {
4853
4972
  const contexts = [];
4973
+ let mergeability = "unknown";
4974
+ let mergeStateStatus = "unknown";
4854
4975
  let after;
4855
4976
  do {
4856
4977
  const response = await octokit.graphql(GITHUB_PULL_REQUEST_CI_CONTEXTS_QUERY, {
@@ -4859,6 +4980,8 @@ async function getGitHubPullRequestCiState(octokit, repository, pullRequestNumbe
4859
4980
  pullRequestNumber,
4860
4981
  after
4861
4982
  });
4983
+ mergeability = normalizeGitHubPullRequestMergeability(response.repository?.pullRequest?.mergeable);
4984
+ mergeStateStatus = normalizeGitHubPullRequestMergeStateStatus(response.repository?.pullRequest?.mergeStateStatus);
4862
4985
  const connection = response.repository?.pullRequest?.statusCheckRollup?.contexts;
4863
4986
  const nodes = connection?.nodes ?? [];
4864
4987
  for (const node of nodes) {
@@ -4882,49 +5005,63 @@ async function getGitHubPullRequestCiState(octokit, repository, pullRequestNumbe
4882
5005
  }
4883
5006
  after = getPageCursor(connection?.pageInfo);
4884
5007
  } while (after);
4885
- return classifyGitHubPullRequestCiState(contexts);
5008
+ return {
5009
+ ciState: classifyGitHubPullRequestCiState(contexts),
5010
+ mergeability,
5011
+ mergeStateStatus
5012
+ };
4886
5013
  }
4887
5014
  async function getGitHubPullRequestStatusSnapshot(octokit, repository, pullRequestNumber, pullRequestStatusCache, options) {
4888
- const cached = pullRequestStatusCache.get(pullRequestNumber);
5015
+ const cached = getCachedGitHubPullRequestStatusSnapshot(pullRequestStatusCache, repository, pullRequestNumber);
4889
5016
  if (cached) {
4890
5017
  return cached;
4891
5018
  }
4892
5019
  const cacheKey = buildRepositoryPullRequestRecordCacheKey(repository, pullRequestNumber, "status");
4893
5020
  const cachedSnapshot = getFreshCacheValue(activeGitHubPullRequestStatusSnapshotCache, cacheKey);
4894
5021
  if (cachedSnapshot) {
4895
- pullRequestStatusCache.set(pullRequestNumber, cachedSnapshot);
5022
+ setCachedGitHubPullRequestStatusSnapshot(pullRequestStatusCache, cachedSnapshot);
4896
5023
  return cachedSnapshot;
4897
5024
  }
4898
- if (options?.reviewThreadSummary && options.ciState) {
5025
+ if (options?.reviewThreadSummary && options.ciState && options.mergeability !== void 0 && options.mergeStateStatus !== void 0) {
4899
5026
  const snapshot = cacheGitHubPullRequestStatusSnapshot(repository, {
4900
5027
  number: pullRequestNumber,
5028
+ repositoryUrl: repository.url,
4901
5029
  hasUnresolvedReviewThreads: options.reviewThreadSummary.unresolvedReviewThreads > 0,
4902
- ciState: options.ciState
5030
+ ciState: options.ciState,
5031
+ mergeability: options.mergeability,
5032
+ mergeStateStatus: options.mergeStateStatus
4903
5033
  });
4904
- pullRequestStatusCache.set(pullRequestNumber, snapshot);
5034
+ setCachedGitHubPullRequestStatusSnapshot(pullRequestStatusCache, snapshot);
4905
5035
  return snapshot;
4906
5036
  }
4907
5037
  const inFlightSnapshot = activeGitHubPullRequestStatusSnapshotPromiseCache.get(cacheKey);
4908
5038
  if (inFlightSnapshot) {
4909
5039
  const snapshot = await inFlightSnapshot;
4910
- pullRequestStatusCache.set(pullRequestNumber, snapshot);
5040
+ setCachedGitHubPullRequestStatusSnapshot(pullRequestStatusCache, snapshot);
4911
5041
  return snapshot;
4912
5042
  }
4913
5043
  const loadSnapshotPromise = (async () => {
4914
- const [reviewThreadSummary, ciState] = await Promise.all([
5044
+ const [reviewThreadSummary, ciSnapshot] = await Promise.all([
4915
5045
  options?.reviewThreadSummary ?? getOrLoadCachedGitHubPullRequestReviewThreadSummary(octokit, repository, pullRequestNumber),
4916
- options?.ciState ?? getGitHubPullRequestCiState(octokit, repository, pullRequestNumber)
5046
+ options?.ciState && options.mergeability !== void 0 && options.mergeStateStatus !== void 0 ? {
5047
+ ciState: options.ciState,
5048
+ mergeability: options.mergeability,
5049
+ mergeStateStatus: options.mergeStateStatus
5050
+ } : getGitHubPullRequestCiSnapshot(octokit, repository, pullRequestNumber)
4917
5051
  ]);
4918
5052
  return cacheGitHubPullRequestStatusSnapshot(repository, {
4919
5053
  number: pullRequestNumber,
5054
+ repositoryUrl: repository.url,
4920
5055
  hasUnresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads > 0,
4921
- ciState
5056
+ ciState: ciSnapshot.ciState,
5057
+ mergeability: ciSnapshot.mergeability,
5058
+ mergeStateStatus: ciSnapshot.mergeStateStatus
4922
5059
  });
4923
5060
  })();
4924
5061
  activeGitHubPullRequestStatusSnapshotPromiseCache.set(cacheKey, loadSnapshotPromise);
4925
5062
  try {
4926
5063
  const snapshot = await loadSnapshotPromise;
4927
- pullRequestStatusCache.set(pullRequestNumber, snapshot);
5064
+ setCachedGitHubPullRequestStatusSnapshot(pullRequestStatusCache, snapshot);
4928
5065
  return snapshot;
4929
5066
  } finally {
4930
5067
  if (activeGitHubPullRequestStatusSnapshotPromiseCache.get(cacheKey) === loadSnapshotPromise) {
@@ -4957,8 +5094,14 @@ async function getGitHubIssueStatusSnapshot(octokit, repository, issueNumber, gi
4957
5094
  if (pullRequest.state !== "OPEN") {
4958
5095
  continue;
4959
5096
  }
5097
+ const linkedPullRequestRepository = requireRepositoryReference(pullRequest.repositoryUrl);
4960
5098
  linkedPullRequestSnapshots.push(
4961
- await getGitHubPullRequestStatusSnapshot(octokit, repository, pullRequest.number, pullRequestStatusCache)
5099
+ await getGitHubPullRequestStatusSnapshot(
5100
+ octokit,
5101
+ linkedPullRequestRepository,
5102
+ pullRequest.number,
5103
+ pullRequestStatusCache
5104
+ )
4962
5105
  );
4963
5106
  }
4964
5107
  const snapshot = {
@@ -5071,25 +5214,58 @@ async function listNewGitHubIssueCommentsSinceCount(octokit, repository, issueNu
5071
5214
  }
5072
5215
  return comments;
5073
5216
  }
5074
- async function hasTrustedNewGitHubIssueComment(params) {
5075
- const normalizedPreviousCommentCount = typeof params.previousCommentCount === "number" && params.previousCommentCount >= 0 ? Math.floor(params.previousCommentCount) : params.currentCommentCount;
5076
- const normalizedCurrentCommentCount = Math.max(0, Math.floor(params.currentCommentCount));
5217
+ async function listNewGitHubPullRequestReviewCommentsSinceCount(octokit, repository, pullRequestNumber, previousCommentCount, currentCommentCount) {
5218
+ const normalizedPreviousCommentCount = Math.max(0, Math.floor(previousCommentCount));
5219
+ const normalizedCurrentCommentCount = Math.max(0, Math.floor(currentCommentCount));
5077
5220
  if (normalizedCurrentCommentCount <= normalizedPreviousCommentCount) {
5078
- return false;
5221
+ return [];
5079
5222
  }
5080
- const newComments = await listNewGitHubIssueCommentsSinceCount(
5081
- params.octokit,
5082
- params.repository,
5083
- params.githubIssue.number,
5084
- normalizedPreviousCommentCount,
5085
- normalizedCurrentCommentCount
5086
- );
5087
- if (newComments.length === 0) {
5088
- return false;
5223
+ const newCommentCount = normalizedCurrentCommentCount - normalizedPreviousCommentCount;
5224
+ const comments = [];
5225
+ const perPage = 100;
5226
+ let page = Math.floor(normalizedPreviousCommentCount / perPage) + 1;
5227
+ let remainingOffset = normalizedPreviousCommentCount % perPage;
5228
+ while (comments.length < newCommentCount) {
5229
+ const response = await octokit.rest.pulls.listReviewComments({
5230
+ owner: repository.owner,
5231
+ repo: repository.repo,
5232
+ pull_number: pullRequestNumber,
5233
+ page,
5234
+ per_page: perPage,
5235
+ headers: {
5236
+ accept: "application/vnd.github+json",
5237
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
5238
+ }
5239
+ });
5240
+ if (response.data.length === 0) {
5241
+ break;
5242
+ }
5243
+ for (const comment of response.data.slice(remainingOffset)) {
5244
+ comments.push({
5245
+ id: comment.id,
5246
+ body: typeof comment.body === "string" ? stripNullBytes(comment.body) : "",
5247
+ url: comment.html_url ?? void 0,
5248
+ authorLogin: normalizeGitHubUserLogin(comment.user?.login),
5249
+ ...normalizeGitHubLowercaseString(comment.author_association) ? { authorAssociation: normalizeGitHubLowercaseString(comment.author_association) } : {},
5250
+ createdAt: comment.created_at ?? void 0,
5251
+ updatedAt: comment.updated_at ?? void 0
5252
+ });
5253
+ if (comments.length >= newCommentCount) {
5254
+ break;
5255
+ }
5256
+ }
5257
+ if (response.data.length < perPage) {
5258
+ break;
5259
+ }
5260
+ page += 1;
5261
+ remainingOffset = 0;
5089
5262
  }
5090
- const originalPosterLogin = normalizeGitHubUserLogin(params.githubIssue.authorLogin);
5263
+ return comments;
5264
+ }
5265
+ async function hasTrustedGitHubCommentRecords(params) {
5266
+ const originalPosterLogin = normalizeGitHubUserLogin(params.originalPosterLogin);
5091
5267
  const unseenAuthors = /* @__PURE__ */ new Set();
5092
- for (const comment of newComments) {
5268
+ for (const comment of params.comments) {
5093
5269
  const authorLogin = normalizeGitHubUserLogin(comment.authorLogin);
5094
5270
  if (!authorLogin) {
5095
5271
  continue;
@@ -5123,6 +5299,122 @@ async function hasTrustedNewGitHubIssueComment(params) {
5123
5299
  }
5124
5300
  return false;
5125
5301
  }
5302
+ async function hasTrustedNewGitHubIssueComment(params) {
5303
+ const normalizedPreviousCommentCount = typeof params.previousCommentCount === "number" && params.previousCommentCount >= 0 ? Math.floor(params.previousCommentCount) : params.currentCommentCount;
5304
+ const normalizedCurrentCommentCount = Math.max(0, Math.floor(params.currentCommentCount));
5305
+ if (normalizedCurrentCommentCount <= normalizedPreviousCommentCount) {
5306
+ return false;
5307
+ }
5308
+ const newComments = await listNewGitHubIssueCommentsSinceCount(
5309
+ params.octokit,
5310
+ params.repository,
5311
+ params.githubIssue.number,
5312
+ normalizedPreviousCommentCount,
5313
+ normalizedCurrentCommentCount
5314
+ );
5315
+ if (newComments.length === 0) {
5316
+ return false;
5317
+ }
5318
+ return hasTrustedGitHubCommentRecords({
5319
+ octokit: params.octokit,
5320
+ repository: params.repository,
5321
+ comments: newComments,
5322
+ originalPosterLogin: params.githubIssue.authorLogin,
5323
+ maintainerCache: params.maintainerCache
5324
+ });
5325
+ }
5326
+ async function getGitHubPullRequestCommentCountRecord(octokit, repository, pullRequestNumber, cache) {
5327
+ const pullRequestReference = {
5328
+ number: pullRequestNumber,
5329
+ repositoryUrl: repository.url
5330
+ };
5331
+ const cacheKey = buildGitHubPullRequestReferenceKey(pullRequestReference);
5332
+ const cachedRecord = cache.get(cacheKey);
5333
+ if (cachedRecord) {
5334
+ return cachedRecord;
5335
+ }
5336
+ const response = await octokit.rest.pulls.get({
5337
+ owner: repository.owner,
5338
+ repo: repository.repo,
5339
+ pull_number: pullRequestNumber,
5340
+ headers: {
5341
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
5342
+ }
5343
+ });
5344
+ const record = {
5345
+ ...pullRequestReference,
5346
+ topLevelCommentCount: typeof response.data.comments === "number" && response.data.comments >= 0 ? Math.floor(response.data.comments) : 0,
5347
+ reviewCommentCount: typeof response.data.review_comments === "number" && response.data.review_comments >= 0 ? Math.floor(response.data.review_comments) : 0
5348
+ };
5349
+ cache.set(cacheKey, record);
5350
+ return record;
5351
+ }
5352
+ async function listGitHubPullRequestCommentCountRecords(octokit, linkedPullRequests, cache) {
5353
+ const commentCounts = [];
5354
+ for (const pullRequest of linkedPullRequests) {
5355
+ commentCounts.push(
5356
+ await getGitHubPullRequestCommentCountRecord(
5357
+ octokit,
5358
+ requireRepositoryReference(pullRequest.repositoryUrl),
5359
+ pullRequest.number,
5360
+ cache
5361
+ )
5362
+ );
5363
+ }
5364
+ return normalizeGitHubPullRequestCommentCountRecords(commentCounts);
5365
+ }
5366
+ async function hasTrustedNewLinkedPullRequestComments(params) {
5367
+ if ((params.currentCommentCounts?.length ?? 0) === 0 || (params.previousCommentCounts?.length ?? 0) === 0) {
5368
+ return false;
5369
+ }
5370
+ const previousCommentCountsByKey = new Map(
5371
+ (params.previousCommentCounts ?? []).map((record) => [buildGitHubPullRequestReferenceKey(record), record])
5372
+ );
5373
+ for (const currentCommentCount of params.currentCommentCounts) {
5374
+ const previousCommentCount = previousCommentCountsByKey.get(buildGitHubPullRequestReferenceKey(currentCommentCount));
5375
+ if (!previousCommentCount) {
5376
+ continue;
5377
+ }
5378
+ const pullRequestRepository = requireRepositoryReference(currentCommentCount.repositoryUrl);
5379
+ if (currentCommentCount.topLevelCommentCount > previousCommentCount.topLevelCommentCount) {
5380
+ const newTopLevelComments = await listNewGitHubIssueCommentsSinceCount(
5381
+ params.octokit,
5382
+ pullRequestRepository,
5383
+ currentCommentCount.number,
5384
+ previousCommentCount.topLevelCommentCount,
5385
+ currentCommentCount.topLevelCommentCount
5386
+ );
5387
+ if (await hasTrustedGitHubCommentRecords({
5388
+ octokit: params.octokit,
5389
+ repository: pullRequestRepository,
5390
+ comments: newTopLevelComments,
5391
+ originalPosterLogin: params.githubIssue.authorLogin,
5392
+ maintainerCache: params.maintainerCache
5393
+ })) {
5394
+ return true;
5395
+ }
5396
+ }
5397
+ if (currentCommentCount.reviewCommentCount > previousCommentCount.reviewCommentCount) {
5398
+ const newReviewComments = await listNewGitHubPullRequestReviewCommentsSinceCount(
5399
+ params.octokit,
5400
+ pullRequestRepository,
5401
+ currentCommentCount.number,
5402
+ previousCommentCount.reviewCommentCount,
5403
+ currentCommentCount.reviewCommentCount
5404
+ );
5405
+ if (await hasTrustedGitHubCommentRecords({
5406
+ octokit: params.octokit,
5407
+ repository: pullRequestRepository,
5408
+ comments: newReviewComments,
5409
+ originalPosterLogin: params.githubIssue.authorLogin,
5410
+ maintainerCache: params.maintainerCache
5411
+ })) {
5412
+ return true;
5413
+ }
5414
+ }
5415
+ }
5416
+ return false;
5417
+ }
5126
5418
  async function isMaintainerAuthoredGitHubIssue(params) {
5127
5419
  const authorLogin = normalizeGitHubUserLogin(params.githubIssue.authorLogin);
5128
5420
  if (!authorLogin) {
@@ -5212,11 +5504,121 @@ function getHiddenGitHubImportMarkerPattern() {
5212
5504
  "i"
5213
5505
  );
5214
5506
  }
5507
+ function formatPlainTextList(items) {
5508
+ if (items.length === 0) {
5509
+ return "";
5510
+ }
5511
+ if (items.length === 1) {
5512
+ return items[0];
5513
+ }
5514
+ if (items.length === 2) {
5515
+ return `${items[0]} and ${items[1]}`;
5516
+ }
5517
+ return `${items.slice(0, -1).join(", ")}, and ${items.at(-1)}`;
5518
+ }
5215
5519
  function normalizeLinkedPullRequestNumbers(values) {
5216
5520
  return [...new Set(
5217
5521
  values.filter((pullRequestNumber) => Number.isInteger(pullRequestNumber) && pullRequestNumber > 0)
5218
5522
  )].sort((left, right) => left - right);
5219
5523
  }
5524
+ function buildGitHubPullRequestReferenceKey(pullRequest) {
5525
+ return `${getNormalizedMappingRepositoryUrl({ repositoryUrl: pullRequest.repositoryUrl }).toLowerCase()}#${Math.max(1, Math.floor(pullRequest.number))}`;
5526
+ }
5527
+ function normalizeGitHubPullRequestCommentCountRecords(value) {
5528
+ if (!Array.isArray(value)) {
5529
+ return [];
5530
+ }
5531
+ const recordsByKey = /* @__PURE__ */ new Map();
5532
+ for (const entry of value) {
5533
+ if (!entry || typeof entry !== "object") {
5534
+ continue;
5535
+ }
5536
+ const record = entry;
5537
+ const pullRequestNumber = typeof record.number === "number" && Number.isInteger(record.number) && record.number > 0 ? Math.floor(record.number) : void 0;
5538
+ const repositoryUrl = typeof record.repositoryUrl === "string" && record.repositoryUrl.trim() ? getNormalizedMappingRepositoryUrl({
5539
+ repositoryUrl: record.repositoryUrl
5540
+ }) : void 0;
5541
+ const topLevelCommentCount = typeof record.topLevelCommentCount === "number" && record.topLevelCommentCount >= 0 ? Math.floor(record.topLevelCommentCount) : void 0;
5542
+ const reviewCommentCount = typeof record.reviewCommentCount === "number" && record.reviewCommentCount >= 0 ? Math.floor(record.reviewCommentCount) : void 0;
5543
+ if (pullRequestNumber === void 0 || !repositoryUrl || topLevelCommentCount === void 0 || reviewCommentCount === void 0) {
5544
+ continue;
5545
+ }
5546
+ const normalizedRecord = {
5547
+ number: pullRequestNumber,
5548
+ repositoryUrl,
5549
+ topLevelCommentCount,
5550
+ reviewCommentCount
5551
+ };
5552
+ recordsByKey.set(buildGitHubPullRequestReferenceKey(normalizedRecord), normalizedRecord);
5553
+ }
5554
+ return [...recordsByKey.values()].sort((left, right) => {
5555
+ const repositoryComparison = left.repositoryUrl.toLowerCase().localeCompare(right.repositoryUrl.toLowerCase());
5556
+ if (repositoryComparison !== 0) {
5557
+ return repositoryComparison;
5558
+ }
5559
+ return left.number - right.number;
5560
+ });
5561
+ }
5562
+ function normalizeLinkedPullRequestReferences(values, fallbackRepositoryUrl) {
5563
+ const references = [];
5564
+ const seenKeys = /* @__PURE__ */ new Set();
5565
+ const normalizedFallbackRepositoryUrl = typeof fallbackRepositoryUrl === "string" && fallbackRepositoryUrl.trim() ? getNormalizedMappingRepositoryUrl({
5566
+ repositoryUrl: fallbackRepositoryUrl
5567
+ }) : void 0;
5568
+ for (const value of values) {
5569
+ const number = typeof value === "number" ? Math.floor(value) : value && typeof value === "object" && typeof value.number === "number" ? Math.floor(value.number) : void 0;
5570
+ const repositoryUrl = value && typeof value === "object" && typeof value.repositoryUrl === "string" && value.repositoryUrl.trim() ? getNormalizedMappingRepositoryUrl({
5571
+ repositoryUrl: value.repositoryUrl
5572
+ }) : normalizedFallbackRepositoryUrl;
5573
+ if (!number || number < 1 || !repositoryUrl) {
5574
+ continue;
5575
+ }
5576
+ const referenceKey = buildGitHubPullRequestReferenceKey({
5577
+ number,
5578
+ repositoryUrl
5579
+ });
5580
+ if (seenKeys.has(referenceKey)) {
5581
+ continue;
5582
+ }
5583
+ seenKeys.add(referenceKey);
5584
+ references.push({
5585
+ number,
5586
+ repositoryUrl
5587
+ });
5588
+ }
5589
+ references.sort((left, right) => {
5590
+ const repositoryUrlComparison = left.repositoryUrl.toLowerCase().localeCompare(right.repositoryUrl.toLowerCase());
5591
+ if (repositoryUrlComparison !== 0) {
5592
+ return repositoryUrlComparison;
5593
+ }
5594
+ return left.number - right.number;
5595
+ });
5596
+ return references;
5597
+ }
5598
+ function formatLinkedPullRequestReferenceLabel(pullRequest, issueRepositoryUrl) {
5599
+ const pullRequestRepository = parseRepositoryReference(pullRequest.repositoryUrl);
5600
+ if (!pullRequestRepository) {
5601
+ return `PR #${pullRequest.number}`;
5602
+ }
5603
+ if (issueRepositoryUrl) {
5604
+ const issueRepository = parseRepositoryReference(issueRepositoryUrl);
5605
+ if (issueRepository && areRepositoriesEqual(issueRepository, pullRequestRepository)) {
5606
+ return `PR #${pullRequest.number}`;
5607
+ }
5608
+ }
5609
+ return `${formatRepositoryLabel(pullRequestRepository)}#${pullRequest.number}`;
5610
+ }
5611
+ function getCachedGitHubPullRequestStatusSnapshot(pullRequestStatusCache, repository, pullRequestNumber) {
5612
+ return pullRequestStatusCache.get(
5613
+ buildGitHubPullRequestReferenceKey({
5614
+ number: pullRequestNumber,
5615
+ repositoryUrl: repository.url
5616
+ })
5617
+ );
5618
+ }
5619
+ function setCachedGitHubPullRequestStatusSnapshot(pullRequestStatusCache, snapshot) {
5620
+ pullRequestStatusCache.set(buildGitHubPullRequestReferenceKey(snapshot), snapshot);
5621
+ }
5220
5622
  function extractImportedGitHubIssueUrlFromDescription(description) {
5221
5623
  if (typeof description !== "string") {
5222
5624
  return void 0;
@@ -5442,6 +5844,14 @@ function normalizeGitHubIssueLinkEntityData(value) {
5442
5844
  if (!repositoryUrl || githubIssueId === void 0 || githubIssueNumber === void 0 || !githubIssueUrl || !githubIssueState || !syncedAt) {
5443
5845
  return null;
5444
5846
  }
5847
+ const linkedPullRequestNumbers = normalizeLinkedPullRequestNumbers(
5848
+ Array.isArray(record.linkedPullRequestNumbers) ? record.linkedPullRequestNumbers.filter((entry) => typeof entry === "number") : []
5849
+ );
5850
+ const rawLinkedPullRequests = Array.isArray(record.linkedPullRequests) ? record.linkedPullRequests : [];
5851
+ const linkedPullRequests = normalizeLinkedPullRequestReferences(
5852
+ rawLinkedPullRequests.length > 0 ? rawLinkedPullRequests : linkedPullRequestNumbers,
5853
+ repositoryUrl
5854
+ );
5445
5855
  return {
5446
5856
  ...typeof record.companyId === "string" && record.companyId.trim() ? { companyId: record.companyId.trim() } : {},
5447
5857
  ...typeof record.paperclipProjectId === "string" && record.paperclipProjectId.trim() ? { paperclipProjectId: record.paperclipProjectId.trim() } : {},
@@ -5455,9 +5865,8 @@ function normalizeGitHubIssueLinkEntityData(value) {
5455
5865
  githubIssueState,
5456
5866
  ...githubIssueStateReason ? { githubIssueStateReason } : {},
5457
5867
  commentsCount,
5458
- linkedPullRequestNumbers: normalizeLinkedPullRequestNumbers(
5459
- Array.isArray(record.linkedPullRequestNumbers) ? record.linkedPullRequestNumbers.filter((entry) => typeof entry === "number") : []
5460
- ),
5868
+ linkedPullRequestNumbers,
5869
+ linkedPullRequests,
5461
5870
  labels: normalizeStoredGitHubIssueLabels(record.labels),
5462
5871
  syncedAt
5463
5872
  };
@@ -5507,15 +5916,22 @@ function normalizeStoredStatusTransitionCommentAnnotation(value) {
5507
5916
  if (!repositoryUrl || githubIssueNumber === void 0 || !githubIssueUrl || !previousStatus || !nextStatus || !reason || !createdAt || !paperclipIssueId) {
5508
5917
  return null;
5509
5918
  }
5919
+ const linkedPullRequestNumbers = normalizeLinkedPullRequestNumbers(
5920
+ Array.isArray(record.linkedPullRequestNumbers) ? record.linkedPullRequestNumbers.filter((entry) => typeof entry === "number") : []
5921
+ );
5922
+ const rawLinkedPullRequests = Array.isArray(record.linkedPullRequests) ? record.linkedPullRequests : [];
5923
+ const linkedPullRequests = normalizeLinkedPullRequestReferences(
5924
+ rawLinkedPullRequests.length > 0 ? rawLinkedPullRequests : linkedPullRequestNumbers,
5925
+ repositoryUrl
5926
+ );
5510
5927
  return {
5511
5928
  ...typeof record.companyId === "string" && record.companyId.trim() ? { companyId: record.companyId.trim() } : {},
5512
5929
  paperclipIssueId,
5513
5930
  repositoryUrl,
5514
5931
  githubIssueNumber,
5515
5932
  githubIssueUrl,
5516
- linkedPullRequestNumbers: normalizeLinkedPullRequestNumbers(
5517
- Array.isArray(record.linkedPullRequestNumbers) ? record.linkedPullRequestNumbers.filter((entry) => typeof entry === "number") : []
5518
- ),
5933
+ linkedPullRequestNumbers,
5934
+ linkedPullRequests,
5519
5935
  previousStatus,
5520
5936
  nextStatus,
5521
5937
  reason,
@@ -5642,9 +6058,10 @@ async function findStoredStatusTransitionCommentAnnotation(ctx, params) {
5642
6058
  }
5643
6059
  return null;
5644
6060
  }
5645
- function buildGitHubIssueLinkRecord(target, issueId, githubIssue, linkedPullRequestNumbers) {
6061
+ function buildGitHubIssueLinkRecord(target, issueId, githubIssue, linkedPullRequests) {
5646
6062
  const githubIssueUrl = normalizeGitHubIssueHtmlUrl(githubIssue.htmlUrl) ?? githubIssue.htmlUrl;
5647
6063
  const repositoryUrl = parseRepositoryReference(target.repositoryUrl)?.url ?? target.repositoryUrl.trim();
6064
+ const normalizedLinkedPullRequests = normalizeLinkedPullRequestReferences(linkedPullRequests, repositoryUrl);
5648
6065
  return {
5649
6066
  paperclipIssueId: issueId,
5650
6067
  title: `GitHub issue #${githubIssue.number}`,
@@ -5662,14 +6079,17 @@ function buildGitHubIssueLinkRecord(target, issueId, githubIssue, linkedPullRequ
5662
6079
  githubIssueState: githubIssue.state,
5663
6080
  ...githubIssue.stateReason ? { githubIssueStateReason: githubIssue.stateReason } : {},
5664
6081
  commentsCount: githubIssue.commentsCount,
5665
- linkedPullRequestNumbers: normalizeLinkedPullRequestNumbers(linkedPullRequestNumbers),
6082
+ linkedPullRequestNumbers: normalizeLinkedPullRequestNumbers(
6083
+ normalizedLinkedPullRequests.map((pullRequest) => pullRequest.number)
6084
+ ),
6085
+ linkedPullRequests: normalizedLinkedPullRequests,
5666
6086
  labels: githubIssue.labels,
5667
6087
  syncedAt: (/* @__PURE__ */ new Date()).toISOString()
5668
6088
  }
5669
6089
  };
5670
6090
  }
5671
- async function upsertGitHubIssueLinkRecord(ctx, target, issueId, githubIssue, linkedPullRequestNumbers) {
5672
- const record = buildGitHubIssueLinkRecord(target, issueId, githubIssue, linkedPullRequestNumbers);
6091
+ async function upsertGitHubIssueLinkRecord(ctx, target, issueId, githubIssue, linkedPullRequests) {
6092
+ const record = buildGitHubIssueLinkRecord(target, issueId, githubIssue, linkedPullRequests);
5673
6093
  await ctx.entities.upsert({
5674
6094
  entityType: ISSUE_LINK_ENTITY_TYPE,
5675
6095
  scopeKind: "issue",
@@ -6580,14 +7000,22 @@ async function updatePaperclipIssueState(ctx, params) {
6580
7000
  syncContext,
6581
7001
  nextStatus,
6582
7002
  nextAssignee,
7003
+ clearAssignee,
6583
7004
  transitionComment,
6584
7005
  transitionCommentAnnotation,
6585
7006
  paperclipApiBaseUrl
6586
7007
  } = params;
6587
7008
  const trimmedTransitionComment = transitionComment.trim();
6588
7009
  let issueUpdated = false;
7010
+ const syncExecutionStatePatch = buildSyncFallbackExecutionStatePatch({
7011
+ currentStatus,
7012
+ nextStatus,
7013
+ syncContext,
7014
+ nextAssignee
7015
+ });
6589
7016
  const issuePatch = {
6590
- status: nextStatus
7017
+ status: nextStatus,
7018
+ ...syncExecutionStatePatch === null ? { executionState: null } : {}
6591
7019
  };
6592
7020
  if (nextAssignee) {
6593
7021
  if (nextAssignee.kind === "agent") {
@@ -6597,6 +7025,9 @@ async function updatePaperclipIssueState(ctx, params) {
6597
7025
  issuePatch.assigneeAgentId = null;
6598
7026
  issuePatch.assigneeUserId = nextAssignee.id;
6599
7027
  }
7028
+ } else if (clearAssignee) {
7029
+ issuePatch.assigneeAgentId = null;
7030
+ issuePatch.assigneeUserId = null;
6600
7031
  }
6601
7032
  if (paperclipApiBaseUrl) {
6602
7033
  try {
@@ -6642,16 +7073,10 @@ async function updatePaperclipIssueState(ctx, params) {
6642
7073
  }
6643
7074
  }
6644
7075
  if (!issueUpdated) {
6645
- const fallbackExecutionStatePatch = buildSyncFallbackExecutionStatePatch({
6646
- currentStatus,
6647
- nextStatus,
6648
- syncContext,
6649
- nextAssignee
6650
- });
6651
7076
  const preserveExistingUserAssigneeWithoutLocalApi = nextAssignee?.kind === "user" && !paperclipApiBaseUrl;
6652
7077
  const sdkIssuePatch = {
6653
7078
  ...issuePatch,
6654
- ...fallbackExecutionStatePatch !== void 0 ? { executionState: fallbackExecutionStatePatch } : {}
7079
+ ...syncExecutionStatePatch !== void 0 ? { executionState: syncExecutionStatePatch } : {}
6655
7080
  };
6656
7081
  if (preserveExistingUserAssigneeWithoutLocalApi) {
6657
7082
  delete sdkIssuePatch.assigneeAgentId;
@@ -6931,6 +7356,7 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
6931
7356
  let completedIssueCount = 0;
6932
7357
  const totalIssueCount = importedIssues.length;
6933
7358
  const queuedIssueWakeups = [];
7359
+ const pullRequestCommentCountCache = /* @__PURE__ */ new Map();
6934
7360
  for (const importedIssue of importedIssues) {
6935
7361
  if (assertNotCancelled) {
6936
7362
  await assertNotCancelled();
@@ -7034,11 +7460,11 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
7034
7460
  stateReason: snapshot.stateReason,
7035
7461
  commentsCount: snapshot.commentCount
7036
7462
  },
7037
- snapshotLinkedPullRequestNumbers
7463
+ snapshot.linkedPullRequests
7038
7464
  );
7039
7465
  const previousCommentCount = importedIssue.lastSeenCommentCount;
7040
- const hasNewComments = snapshot.commentCount > (previousCommentCount ?? snapshot.commentCount);
7041
- const hasTrustedNewComment = paperclipIssue.status === "backlog" || !hasNewComments ? false : await hasTrustedNewGitHubIssueComment({
7466
+ const hasNewIssueComments = snapshot.commentCount > (previousCommentCount ?? snapshot.commentCount);
7467
+ const hasTrustedNewIssueComment = paperclipIssue.status === "backlog" || !hasNewIssueComments ? false : await hasTrustedNewGitHubIssueComment({
7042
7468
  octokit,
7043
7469
  repository,
7044
7470
  githubIssue,
@@ -7046,6 +7472,28 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
7046
7472
  currentCommentCount: snapshot.commentCount,
7047
7473
  maintainerCache: repositoryMaintainerCache
7048
7474
  });
7475
+ let currentLinkedPullRequestCommentCounts = snapshot.linkedPullRequests.length === 0 ? [] : importedIssue.linkedPullRequestCommentCounts ?? [];
7476
+ if (snapshot.linkedPullRequests.length > 0) {
7477
+ try {
7478
+ currentLinkedPullRequestCommentCounts = await listGitHubPullRequestCommentCountRecords(
7479
+ octokit,
7480
+ snapshot.linkedPullRequests,
7481
+ pullRequestCommentCountCache
7482
+ );
7483
+ } catch (error) {
7484
+ if (isGitHubRateLimitError(error)) {
7485
+ throw error;
7486
+ }
7487
+ }
7488
+ }
7489
+ const hasTrustedNewLinkedPullRequestComment = paperclipIssue.status === "backlog" || currentLinkedPullRequestCommentCounts.length === 0 ? false : await hasTrustedNewLinkedPullRequestComments({
7490
+ octokit,
7491
+ githubIssue,
7492
+ previousCommentCounts: importedIssue.linkedPullRequestCommentCounts,
7493
+ currentCommentCounts: currentLinkedPullRequestCommentCounts,
7494
+ maintainerCache: repositoryMaintainerCache
7495
+ });
7496
+ const hasTrustedNewComment = hasTrustedNewIssueComment || hasTrustedNewLinkedPullRequestComment;
7049
7497
  const wasImportedThisRun = createdIssueIds.has(importedIssue.githubIssueId);
7050
7498
  const maintainerAuthoredImportedIssue = wasImportedThisRun && advancedSettings.defaultStatus !== "todo" && snapshot.state === "open" && snapshot.linkedPullRequests.length === 0 ? await isMaintainerAuthoredGitHubIssue({
7051
7499
  octokit,
@@ -7061,7 +7509,6 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
7061
7509
  const nextStatus = resolvePaperclipIssueStatus({
7062
7510
  currentStatus: paperclipIssue.status,
7063
7511
  snapshot,
7064
- previousCommentCount,
7065
7512
  hasTrustedNewComment,
7066
7513
  wasImportedThisRun,
7067
7514
  defaultImportedStatus: advancedSettings.defaultStatus,
@@ -7073,12 +7520,31 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
7073
7520
  syncContext: paperclipIssueSyncContext,
7074
7521
  advancedSettings
7075
7522
  });
7523
+ const shouldClearTransitionAssignee = nextStatus === "in_review" && nextTransitionAssignee === null && paperclipIssueSyncContext.assignee !== null;
7076
7524
  const nextAssigneeChanged = nextTransitionAssignee ? !doesPaperclipIssueAssigneeMatch(paperclipIssueSyncContext.assignee, nextTransitionAssignee.principal) : false;
7077
7525
  const shouldWakeImportedAssignee = wasImportedThisRun && paperclipIssue.status === nextStatus && nextStatus === "todo" && paperclipIssueSyncContext.assignee?.kind === "agent";
7078
7526
  const shouldWakeTransitionAssignee = paperclipIssue.status !== nextStatus && nextTransitionAssignee?.principal.kind === "agent" && isActionablePaperclipIssueStatus(nextStatus) && (nextAssigneeChanged || paperclipIssue.status !== nextStatus);
7079
7527
  importedIssue.githubIssueNumber = githubIssue.number;
7080
7528
  importedIssue.lastSeenCommentCount = snapshot.commentCount;
7529
+ importedIssue.linkedPullRequestCommentCounts = currentLinkedPullRequestCommentCounts;
7081
7530
  if (paperclipIssue.status === nextStatus) {
7531
+ if (shouldClearTransitionAssignee) {
7532
+ updateSyncFailureContext(syncFailureContext, {
7533
+ phase: "updating_paperclip_status",
7534
+ repositoryUrl: repository.url,
7535
+ githubIssueNumber: githubIssue.number
7536
+ });
7537
+ await updatePaperclipIssueState(ctx, {
7538
+ companyId: mapping.companyId,
7539
+ issueId: importedIssue.paperclipIssueId,
7540
+ currentStatus: paperclipIssue.status,
7541
+ syncContext: paperclipIssueSyncContext,
7542
+ nextStatus,
7543
+ clearAssignee: true,
7544
+ transitionComment: "",
7545
+ paperclipApiBaseUrl
7546
+ });
7547
+ }
7082
7548
  if (shouldWakeImportedAssignee) {
7083
7549
  queuedIssueWakeups.push({
7084
7550
  assigneeAgentId: paperclipIssueSyncContext.assignee?.kind === "agent" ? paperclipIssueSyncContext.assignee.id : null,
@@ -7094,7 +7560,6 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
7094
7560
  nextStatus,
7095
7561
  repository,
7096
7562
  snapshot,
7097
- previousCommentCount,
7098
7563
  hasTrustedNewComment,
7099
7564
  maintainerAuthoredImportedIssue
7100
7565
  });
@@ -7110,6 +7575,7 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
7110
7575
  syncContext: paperclipIssueSyncContext,
7111
7576
  nextStatus,
7112
7577
  ...nextTransitionAssignee ? { nextAssignee: nextTransitionAssignee.principal } : {},
7578
+ ...shouldClearTransitionAssignee ? { clearAssignee: true } : {},
7113
7579
  transitionComment: transitionComment.body,
7114
7580
  transitionCommentAnnotation: transitionComment.annotation,
7115
7581
  paperclipApiBaseUrl
@@ -7672,23 +8138,73 @@ async function resolveGitHubPullRequestToolTarget(ctx, runCtx, input) {
7672
8138
  if (!link) {
7673
8139
  throw new Error("This Paperclip issue is not linked to GitHub yet.");
7674
8140
  }
7675
- const repository2 = assertExplicitRepositoryMatchesLinkedRepository(
7676
- input.repository,
7677
- link.repositoryUrl,
7678
- "repository must match the GitHub repository linked to the provided Paperclip issue."
7679
- );
7680
8141
  const explicitPullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
8142
+ const linkedPullRequests = link.linkedPullRequests.length > 0 ? link.linkedPullRequests : normalizeLinkedPullRequestReferences(link.linkedPullRequestNumbers, link.repositoryUrl);
7681
8143
  if (explicitPullRequestNumber !== void 0) {
8144
+ const explicitRepository = normalizeOptionalToolString(input.repository);
8145
+ const matchingLinkedPullRequests = linkedPullRequests.filter(
8146
+ (pullRequest) => pullRequest.number === explicitPullRequestNumber
8147
+ );
8148
+ if (explicitRepository) {
8149
+ const requestedRepository = requireRepositoryReference(explicitRepository);
8150
+ if (matchingLinkedPullRequests.length > 0) {
8151
+ const matchingLinkedPullRequest = matchingLinkedPullRequests.find(
8152
+ (pullRequest) => areRepositoriesEqual(requestedRepository, requireRepositoryReference(pullRequest.repositoryUrl))
8153
+ );
8154
+ if (!matchingLinkedPullRequest) {
8155
+ const linkedIssueRepository = requireRepositoryReference(link.repositoryUrl);
8156
+ const allMatchingPullRequestsUseIssueRepository = matchingLinkedPullRequests.every(
8157
+ (pullRequest) => areRepositoriesEqual(linkedIssueRepository, requireRepositoryReference(pullRequest.repositoryUrl))
8158
+ );
8159
+ throw new Error(
8160
+ allMatchingPullRequestsUseIssueRepository ? "repository must match the GitHub repository linked to the provided Paperclip issue." : "repository must match the GitHub repository for the selected linked pull request."
8161
+ );
8162
+ }
8163
+ return {
8164
+ repository: requestedRepository,
8165
+ pullRequestNumber: explicitPullRequestNumber,
8166
+ paperclipIssueId
8167
+ };
8168
+ }
8169
+ const repository2 = assertExplicitRepositoryMatchesLinkedRepository(
8170
+ input.repository,
8171
+ link.repositoryUrl,
8172
+ "repository must match the GitHub repository linked to the provided Paperclip issue."
8173
+ );
8174
+ return {
8175
+ repository: repository2,
8176
+ pullRequestNumber: explicitPullRequestNumber,
8177
+ paperclipIssueId
8178
+ };
8179
+ }
8180
+ if (matchingLinkedPullRequests.length === 1) {
8181
+ return {
8182
+ repository: requireRepositoryReference(matchingLinkedPullRequests[0].repositoryUrl),
8183
+ pullRequestNumber: explicitPullRequestNumber,
8184
+ paperclipIssueId
8185
+ };
8186
+ }
8187
+ if (matchingLinkedPullRequests.length > 1) {
8188
+ throw new Error("repository is required because the linked Paperclip issue has matching pull request numbers in multiple repositories.");
8189
+ }
7682
8190
  return {
7683
- repository: repository2,
8191
+ repository: requireRepositoryReference(link.repositoryUrl),
7684
8192
  pullRequestNumber: explicitPullRequestNumber,
7685
8193
  paperclipIssueId
7686
8194
  };
7687
8195
  }
7688
- if (link.linkedPullRequestNumbers.length === 1) {
8196
+ if (linkedPullRequests.length === 1) {
8197
+ const inferredPullRequest = linkedPullRequests[0];
8198
+ const explicitRepository = normalizeOptionalToolString(input.repository);
8199
+ if (explicitRepository) {
8200
+ const requestedRepository = requireRepositoryReference(explicitRepository);
8201
+ if (!areRepositoriesEqual(requestedRepository, requireRepositoryReference(inferredPullRequest.repositoryUrl))) {
8202
+ throw new Error("repository must match the GitHub repository for the selected linked pull request.");
8203
+ }
8204
+ }
7689
8205
  return {
7690
- repository: repository2,
7691
- pullRequestNumber: link.linkedPullRequestNumbers[0],
8206
+ repository: requireRepositoryReference(inferredPullRequest.repositoryUrl),
8207
+ pullRequestNumber: inferredPullRequest.number,
7692
8208
  paperclipIssueId
7693
8209
  };
7694
8210
  }
@@ -8437,7 +8953,7 @@ function getProjectPullRequestStatus(state) {
8437
8953
  return "open";
8438
8954
  }
8439
8955
  }
8440
- async function buildProjectPullRequestSummaryRecord(octokit, repository, node, issueLookup, pullRequestStatusCache) {
8956
+ async function buildProjectPullRequestSummaryRecord(octokit, repository, node, issueLookup, pullRequestStatusCache, defaultBranchName) {
8441
8957
  if (!node || typeof node.number !== "number" || !node.url || !node.title?.trim()) {
8442
8958
  return null;
8443
8959
  }
@@ -8446,6 +8962,8 @@ async function buildProjectPullRequestSummaryRecord(octokit, repository, node, i
8446
8962
  const inlineCiState = tryBuildGitHubPullRequestCiStateFromBatchNode({
8447
8963
  statusCheckRollup: node.statusCheckRollup
8448
8964
  });
8965
+ const inlineMergeability = normalizeGitHubPullRequestMergeability(node.mergeable);
8966
+ const inlineMergeStateStatus = normalizeGitHubPullRequestMergeStateStatus(node.mergeStateStatus);
8449
8967
  const [reviewThreadSummary, reviewSummary, statusSnapshot, behindBy] = await Promise.all([
8450
8968
  getOrLoadCachedGitHubPullRequestReviewThreadSummary(
8451
8969
  octokit,
@@ -8461,7 +8979,9 @@ async function buildProjectPullRequestSummaryRecord(octokit, repository, node, i
8461
8979
  ),
8462
8980
  getGitHubPullRequestStatusSnapshot(octokit, repository, node.number, pullRequestStatusCache, {
8463
8981
  reviewThreadSummary: inlineReviewThreadSummary,
8464
- ciState: inlineCiState
8982
+ ciState: inlineCiState,
8983
+ mergeability: inlineMergeability,
8984
+ mergeStateStatus: inlineMergeStateStatus
8465
8985
  }),
8466
8986
  getGitHubPullRequestBehindCount(octokit, repository, {
8467
8987
  baseBranch: node.baseRefName,
@@ -8486,8 +9006,11 @@ async function buildProjectPullRequestSummaryRecord(octokit, repository, node, i
8486
9006
  const mergeable = resolveProjectPullRequestMergeable({
8487
9007
  checksStatus,
8488
9008
  reviewApprovals: reviewSummary.approvals,
9009
+ reviewChangesRequested: reviewSummary.changesRequested,
8489
9010
  unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
8490
- githubMergeable
9011
+ githubMergeable,
9012
+ baseBranch: node.baseRefName,
9013
+ defaultBranchName
8491
9014
  });
8492
9015
  const upToDateStatus = resolveProjectPullRequestUpToDateStatus({
8493
9016
  mergeStateStatus: node.mergeStateStatus,
@@ -8562,7 +9085,8 @@ async function listProjectPullRequestSummaryRecords(ctx, octokit, scope, options
8562
9085
  scope.repository,
8563
9086
  node,
8564
9087
  issueLookup,
8565
- pullRequestStatusCache
9088
+ pullRequestStatusCache,
9089
+ defaultBranchName
8566
9090
  )
8567
9091
  );
8568
9092
  pullRequests.push(...pageRecords.filter((record) => Boolean(record)));
@@ -8581,7 +9105,7 @@ async function listProjectPullRequestSummaryRecords(ctx, octokit, scope, options
8581
9105
  ...nextCursor ? { nextCursor } : {}
8582
9106
  };
8583
9107
  }
8584
- async function buildProjectPullRequestMetricCounts(octokit, repository, node, pullRequestStatusCache) {
9108
+ async function buildProjectPullRequestMetricCounts(octokit, repository, node, pullRequestStatusCache, defaultBranchName) {
8585
9109
  if (!node || typeof node.number !== "number") {
8586
9110
  return {
8587
9111
  pullRequestNumber: null,
@@ -8595,6 +9119,8 @@ async function buildProjectPullRequestMetricCounts(octokit, repository, node, pu
8595
9119
  const inlineCiState = tryBuildGitHubPullRequestCiStateFromBatchNode({
8596
9120
  statusCheckRollup: node.statusCheckRollup
8597
9121
  });
9122
+ const inlineMergeability = normalizeGitHubPullRequestMergeability(node.mergeable);
9123
+ const inlineMergeStateStatus = normalizeGitHubPullRequestMergeStateStatus(node.mergeStateStatus);
8598
9124
  const [reviewThreadSummary, reviewSummary, statusSnapshot] = await Promise.all([
8599
9125
  getOrLoadCachedGitHubPullRequestReviewThreadSummary(
8600
9126
  octokit,
@@ -8610,7 +9136,9 @@ async function buildProjectPullRequestMetricCounts(octokit, repository, node, pu
8610
9136
  ),
8611
9137
  getGitHubPullRequestStatusSnapshot(octokit, repository, node.number, pullRequestStatusCache, {
8612
9138
  reviewThreadSummary: inlineReviewThreadSummary,
8613
- ciState: inlineCiState
9139
+ ciState: inlineCiState,
9140
+ mergeability: inlineMergeability,
9141
+ mergeStateStatus: inlineMergeStateStatus
8614
9142
  })
8615
9143
  ]);
8616
9144
  const checksStatus = statusSnapshot.ciState === "green" ? "passed" : statusSnapshot.ciState === "red" ? "failed" : "pending";
@@ -8623,8 +9151,11 @@ async function buildProjectPullRequestMetricCounts(octokit, repository, node, pu
8623
9151
  const mergeable = resolveProjectPullRequestMergeable({
8624
9152
  checksStatus,
8625
9153
  reviewApprovals: reviewSummary.approvals,
9154
+ reviewChangesRequested: reviewSummary.changesRequested,
8626
9155
  unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
8627
- githubMergeable
9156
+ githubMergeable,
9157
+ baseBranch: node.baseRefName,
9158
+ defaultBranchName
8628
9159
  });
8629
9160
  return {
8630
9161
  pullRequestNumber: Math.floor(node.number),
@@ -8674,7 +9205,13 @@ async function listProjectPullRequestMetrics(octokit, scope) {
8674
9205
  const pageMetrics = await mapWithConcurrency(
8675
9206
  pageNodes,
8676
9207
  PROJECT_PULL_REQUEST_SUMMARY_CONCURRENCY,
8677
- async (node) => buildProjectPullRequestMetricCounts(octokit, scope.repository, node, pullRequestStatusCache)
9208
+ async (node) => buildProjectPullRequestMetricCounts(
9209
+ octokit,
9210
+ scope.repository,
9211
+ node,
9212
+ pullRequestStatusCache,
9213
+ defaultBranchName
9214
+ )
8678
9215
  );
8679
9216
  for (const pageMetric of pageMetrics) {
8680
9217
  mergeablePullRequests += pageMetric.mergeablePullRequests;
@@ -9410,6 +9947,20 @@ function getPullRequestApiState(value) {
9410
9947
  }
9411
9948
  return value.state === "closed" ? "closed" : "open";
9412
9949
  }
9950
+ async function getGitHubRepositoryDefaultBranchName(octokit, repository) {
9951
+ try {
9952
+ const response = await octokit.rest.repos.get({
9953
+ owner: repository.owner,
9954
+ repo: repository.repo,
9955
+ headers: {
9956
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
9957
+ }
9958
+ });
9959
+ return normalizeOptionalString2(response.data.default_branch);
9960
+ } catch {
9961
+ return void 0;
9962
+ }
9963
+ }
9413
9964
  async function buildProjectPullRequestDetailData(ctx, input) {
9414
9965
  const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
9415
9966
  if (!pullRequestNumber) {
@@ -9425,6 +9976,15 @@ async function buildProjectPullRequestDetailData(ctx, input) {
9425
9976
  activeProjectPullRequestSummaryRecordCache,
9426
9977
  buildProjectPullRequestSummaryRecordCacheKey(scope, pullRequestNumber)
9427
9978
  );
9979
+ const cachedSummary = getFreshCacheValue(
9980
+ activeProjectPullRequestSummaryCache,
9981
+ buildProjectPullRequestSummaryCacheKey(scope)
9982
+ );
9983
+ const cachedMetrics = getFreshCacheValue(
9984
+ activeProjectPullRequestMetricsCache,
9985
+ buildProjectPullRequestMetricsCacheKey(scope)
9986
+ );
9987
+ const cachedDefaultBranchName = normalizeOptionalString2(cachedSummary?.defaultBranchName) ?? normalizeOptionalString2(cachedMetrics?.defaultBranchName);
9428
9988
  const cachedLinkedIssue = cachedSummaryRecord ? getLinkedPaperclipIssueFromProjectPullRequestRecord(cachedSummaryRecord) : void 0;
9429
9989
  const octokit = await createGitHubToolOctokit(ctx, scope.companyId);
9430
9990
  const response = await octokit.rest.pulls.get({
@@ -9443,7 +10003,8 @@ async function buildProjectPullRequestDetailData(ctx, input) {
9443
10003
  reviewThreadSummary: reviewThreadSummary2
9444
10004
  })
9445
10005
  );
9446
- const [reviewSummary, reviewThreadSummary, comments, linkedIssue, statusSnapshot] = await Promise.all([
10006
+ const defaultBranchNamePromise = cachedDefaultBranchName ? Promise.resolve(cachedDefaultBranchName) : getGitHubRepositoryDefaultBranchName(octokit, scope.repository);
10007
+ const [reviewSummary, reviewThreadSummary, comments, linkedIssue, statusSnapshot, defaultBranchName] = await Promise.all([
9447
10008
  reviewSummaryPromise,
9448
10009
  reviewThreadSummaryPromise,
9449
10010
  listAllGitHubIssueComments(octokit, scope.repository, pullRequestNumber),
@@ -9455,7 +10016,8 @@ async function buildProjectPullRequestDetailData(ctx, input) {
9455
10016
  issueLookup
9456
10017
  );
9457
10018
  })(),
9458
- statusSnapshotPromise
10019
+ statusSnapshotPromise,
10020
+ defaultBranchNamePromise
9459
10021
  ]);
9460
10022
  const author = buildProjectPullRequestPerson({
9461
10023
  login: pullRequest.user?.login,
@@ -9499,8 +10061,11 @@ async function buildProjectPullRequestDetailData(ctx, input) {
9499
10061
  const mergeable = resolveProjectPullRequestMergeable({
9500
10062
  checksStatus,
9501
10063
  reviewApprovals: reviewSummary.approvals,
10064
+ reviewChangesRequested: reviewSummary.changesRequested,
9502
10065
  unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
9503
- githubMergeable
10066
+ githubMergeable,
10067
+ baseBranch: pullRequest.base.ref,
10068
+ defaultBranchName
9504
10069
  });
9505
10070
  return setCacheValue(
9506
10071
  activeProjectPullRequestDetailCache,
@@ -9695,6 +10260,49 @@ async function mergeProjectPullRequest(ctx, input) {
9695
10260
  }
9696
10261
  const scope = await requireProjectPullRequestScope(ctx, input);
9697
10262
  const octokit = await createGitHubToolOctokit(ctx, scope.companyId);
10263
+ const pullRequestResponse = await octokit.rest.pulls.get({
10264
+ owner: scope.repository.owner,
10265
+ repo: scope.repository.repo,
10266
+ pull_number: pullRequestNumber,
10267
+ headers: {
10268
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
10269
+ }
10270
+ });
10271
+ const pullRequest = pullRequestResponse.data;
10272
+ const reviewSummaryPromise = getOrLoadCachedGitHubPullRequestReviewSummary(octokit, scope.repository, pullRequestNumber);
10273
+ const reviewThreadSummaryPromise = getOrLoadCachedGitHubPullRequestReviewThreadSummary(octokit, scope.repository, pullRequestNumber);
10274
+ const statusSnapshotPromise = reviewThreadSummaryPromise.then(
10275
+ (reviewThreadSummary2) => getGitHubPullRequestStatusSnapshot(octokit, scope.repository, pullRequestNumber, /* @__PURE__ */ new Map(), {
10276
+ reviewThreadSummary: reviewThreadSummary2
10277
+ })
10278
+ );
10279
+ const defaultBranchNamePromise = getGitHubRepositoryDefaultBranchName(octokit, scope.repository);
10280
+ const [reviewSummary, reviewThreadSummary, statusSnapshot, defaultBranchName] = await Promise.all([
10281
+ reviewSummaryPromise,
10282
+ reviewThreadSummaryPromise,
10283
+ statusSnapshotPromise,
10284
+ defaultBranchNamePromise
10285
+ ]);
10286
+ if (!defaultBranchName) {
10287
+ throw new Error(
10288
+ "Could not determine the repository default branch before merging this pull request. Retry the merge, and if it keeps failing check GitHub token permissions and connectivity."
10289
+ );
10290
+ }
10291
+ const checksStatus = statusSnapshot.ciState === "green" ? "passed" : statusSnapshot.ciState === "red" ? "failed" : "pending";
10292
+ const mergeable = resolveProjectPullRequestMergeable({
10293
+ checksStatus,
10294
+ reviewApprovals: reviewSummary.approvals,
10295
+ reviewChangesRequested: reviewSummary.changesRequested,
10296
+ unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
10297
+ githubMergeable: pullRequest.mergeable === true,
10298
+ baseBranch: pullRequest.base.ref,
10299
+ defaultBranchName
10300
+ });
10301
+ if (!mergeable) {
10302
+ throw new Error(
10303
+ "This pull request is not mergeable yet. It must target the current default branch, have passing checks, at least one approval, no outstanding changes requests, and no unresolved review threads."
10304
+ );
10305
+ }
9698
10306
  const response = await octokit.rest.pulls.merge({
9699
10307
  owner: scope.repository.owner,
9700
10308
  repo: scope.repository.repo,
@@ -10472,20 +11080,28 @@ async function performSync(ctx, trigger, options = {}) {
10472
11080
  for (const [issueNumber, linkedPullRequests] of warmedLinkedPullRequests.entries()) {
10473
11081
  linkedPullRequestsByIssueNumber.set(issueNumber, linkedPullRequests);
10474
11082
  }
10475
- const openLinkedPullRequestNumbers = /* @__PURE__ */ new Set();
11083
+ const openLinkedPullRequestNumbersByRepository = /* @__PURE__ */ new Map();
10476
11084
  for (const linkedPullRequests of warmedLinkedPullRequests.values()) {
10477
11085
  for (const pullRequest of linkedPullRequests) {
10478
11086
  if (pullRequest.state === "OPEN") {
10479
- openLinkedPullRequestNumbers.add(pullRequest.number);
11087
+ const pullRequestRepository = requireRepositoryReference(pullRequest.repositoryUrl);
11088
+ const entry = openLinkedPullRequestNumbersByRepository.get(pullRequestRepository.url) ?? {
11089
+ repository: pullRequestRepository,
11090
+ numbers: /* @__PURE__ */ new Set()
11091
+ };
11092
+ entry.numbers.add(pullRequest.number);
11093
+ openLinkedPullRequestNumbersByRepository.set(pullRequestRepository.url, entry);
10480
11094
  }
10481
11095
  }
10482
11096
  }
10483
- await warmGitHubPullRequestStatusCache(
10484
- octokit,
10485
- repository,
10486
- openLinkedPullRequestNumbers,
10487
- pullRequestStatusCache
10488
- );
11097
+ for (const entry of openLinkedPullRequestNumbersByRepository.values()) {
11098
+ await warmGitHubPullRequestStatusCache(
11099
+ octokit,
11100
+ entry.repository,
11101
+ entry.numbers,
11102
+ pullRequestStatusCache
11103
+ );
11104
+ }
10489
11105
  await throwIfSyncCancelled();
10490
11106
  } catch (error) {
10491
11107
  if (error instanceof SyncCancellationError || isGitHubRateLimitError(error)) {
@@ -11274,6 +11890,7 @@ function registerGitHubAgentTools(ctx) {
11274
11890
  ].filter((warning) => warning !== null);
11275
11891
  const snapshot = snapshotResult.status === "fulfilled" ? snapshotResult.value : {
11276
11892
  number: target.pullRequestNumber,
11893
+ repositoryUrl: target.repository.url,
11277
11894
  hasUnresolvedReviewThreads: false,
11278
11895
  ciState: "unfinished"
11279
11896
  };