paperclip-github-plugin 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/worker.js CHANGED
@@ -3,7 +3,10 @@ import { readFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { Octokit } from "@octokit/rest";
6
- import { definePlugin, runWorker } from "@paperclipai/plugin-sdk";
6
+ import {
7
+ definePlugin,
8
+ runWorker
9
+ } from "@paperclipai/plugin-sdk";
7
10
 
8
11
  // src/github-agent-tools.ts
9
12
  var repositoryProperty = {
@@ -529,6 +532,16 @@ var DEFAULT_IGNORED_GITHUB_ISSUE_USERNAMES = ["renovate"];
529
532
  var GITHUB_API_VERSION = "2026-03-10";
530
533
  var DEFAULT_PAPERCLIP_LABEL_COLOR = "#6366f1";
531
534
  var PAPERCLIP_LABEL_PAGE_SIZE = 100;
535
+ var PROJECT_PULL_REQUEST_SUMMARY_CONCURRENCY = 8;
536
+ var PROJECT_PULL_REQUEST_PAGE_SIZE = 10;
537
+ var PROJECT_PULL_REQUEST_METRICS_BATCH_SIZE = 100;
538
+ var PROJECT_PULL_REQUEST_PAGE_CACHE_TTL_MS = 30 * 6e4;
539
+ var PROJECT_PULL_REQUEST_SUMMARY_CACHE_TTL_MS = 60 * 6e4;
540
+ var PROJECT_PULL_REQUEST_DETAIL_CACHE_TTL_MS = 30 * 6e4;
541
+ var PROJECT_PULL_REQUEST_ISSUE_LOOKUP_CACHE_TTL_MS = 60 * 6e4;
542
+ var PROJECT_PULL_REQUEST_GITHUB_INSIGHT_CACHE_TTL_MS = 60 * 6e4;
543
+ var PROJECT_PULL_REQUEST_BRANCH_COMPARE_CACHE_TTL_MS = 30 * 6e4;
544
+ var GITHUB_TOKEN_PERMISSION_AUDIT_CACHE_TTL_MS = 5 * 6e4;
532
545
  var MANUAL_SYNC_RESPONSE_GRACE_PERIOD_MS = 500;
533
546
  var RUNNING_SYNC_MESSAGE = "GitHub sync is running in the background. This page will update when it finishes.";
534
547
  var CANCELLING_SYNC_MESSAGE = "Cancellation requested. GitHub sync will stop after the current step finishes.";
@@ -542,6 +555,7 @@ var MISSING_MAPPING_SYNC_ACTION = "Open settings, add a repository mapping, let
542
555
  var MISSING_BOARD_ACCESS_SYNC_MESSAGE = "Connect Paperclip board access before running sync on this authenticated deployment.";
543
556
  var MISSING_BOARD_ACCESS_SYNC_ACTION = "Open plugin settings for each mapped company that sync will touch, connect Paperclip board access, approve the flow, and then run sync again.";
544
557
  var ISSUE_LINK_ENTITY_TYPE = "paperclip-github-plugin.issue-link";
558
+ var PULL_REQUEST_LINK_ENTITY_TYPE = "paperclip-github-plugin.pull-request-link";
545
559
  var COMMENT_ANNOTATION_ENTITY_TYPE = "paperclip-github-plugin.comment-annotation";
546
560
  var EXTERNAL_CONFIG_FILE_PATH_SEGMENTS = [".paperclip", "plugins", "github-sync", "config.json"];
547
561
  var AI_AUTHORED_COMMENT_FOOTER_PREFIX = "Created by a Paperclip AI agent using ";
@@ -552,6 +566,26 @@ var activeSyncPromise = null;
552
566
  var activeRunningSyncState = null;
553
567
  var activePaperclipApiAuthTokensByCompanyId = null;
554
568
  var activeExternalConfigWarningKey = null;
569
+ var activeProjectPullRequestPageCache = /* @__PURE__ */ new Map();
570
+ var activeProjectPullRequestCountCache = /* @__PURE__ */ new Map();
571
+ var activeProjectPullRequestCountPromiseCache = /* @__PURE__ */ new Map();
572
+ var activeProjectPullRequestMetricsCache = /* @__PURE__ */ new Map();
573
+ var activeProjectPullRequestMetricsPromiseCache = /* @__PURE__ */ new Map();
574
+ var activeProjectPullRequestSummaryCache = /* @__PURE__ */ new Map();
575
+ var activeProjectPullRequestSummaryPromiseCache = /* @__PURE__ */ new Map();
576
+ var activeProjectPullRequestSummaryRecordCache = /* @__PURE__ */ new Map();
577
+ var activeProjectPullRequestDetailCache = /* @__PURE__ */ new Map();
578
+ var activeProjectPullRequestIssueLookupCache = /* @__PURE__ */ new Map();
579
+ var activeGitHubPullRequestStatusSnapshotCache = /* @__PURE__ */ new Map();
580
+ var activeGitHubPullRequestStatusSnapshotPromiseCache = /* @__PURE__ */ new Map();
581
+ var activeGitHubPullRequestReviewSummaryCache = /* @__PURE__ */ new Map();
582
+ var activeGitHubPullRequestReviewSummaryPromiseCache = /* @__PURE__ */ new Map();
583
+ var activeGitHubPullRequestReviewThreadSummaryCache = /* @__PURE__ */ new Map();
584
+ var activeGitHubPullRequestReviewThreadSummaryPromiseCache = /* @__PURE__ */ new Map();
585
+ var activeGitHubPullRequestBehindCountCache = /* @__PURE__ */ new Map();
586
+ var activeGitHubPullRequestBehindCountPromiseCache = /* @__PURE__ */ new Map();
587
+ var activeGitHubRepositoryTokenCapabilityAuditCache = /* @__PURE__ */ new Map();
588
+ var activeGitHubRepositoryTokenCapabilityAuditPromiseCache = /* @__PURE__ */ new Map();
555
589
  var PaperclipLabelSyncError = class extends Error {
556
590
  name = "PaperclipLabelSyncError";
557
591
  status;
@@ -634,23 +668,6 @@ var GITHUB_ISSUE_STATUS_SNAPSHOT_QUERY = `
634
668
  }
635
669
  }
636
670
  `;
637
- var GITHUB_PULL_REQUEST_REVIEW_THREADS_QUERY = `
638
- query GitHubPullRequestReviewThreads($owner: String!, $repo: String!, $pullRequestNumber: Int!, $after: String) {
639
- repository(owner: $owner, name: $repo) {
640
- pullRequest(number: $pullRequestNumber) {
641
- reviewThreads(first: 100, after: $after) {
642
- pageInfo {
643
- hasNextPage
644
- endCursor
645
- }
646
- nodes {
647
- isResolved
648
- }
649
- }
650
- }
651
- }
652
- }
653
- `;
654
671
  var GITHUB_PULL_REQUEST_CI_CONTEXTS_QUERY = `
655
672
  query GitHubPullRequestCiContexts($owner: String!, $repo: String!, $pullRequestNumber: Int!, $after: String) {
656
673
  repository(owner: $owner, name: $repo) {
@@ -750,6 +767,235 @@ var GITHUB_REPOSITORY_OPEN_PULL_REQUEST_STATUSES_QUERY = `
750
767
  }
751
768
  }
752
769
  `;
770
+ var GITHUB_PROJECT_PULL_REQUEST_BASE_FIELDS = `
771
+ id
772
+ number
773
+ title
774
+ url
775
+ state
776
+ mergeable
777
+ mergeStateStatus
778
+ createdAt
779
+ updatedAt
780
+ baseRefName
781
+ headRefName
782
+ headRepositoryOwner {
783
+ login
784
+ }
785
+ changedFiles
786
+ commits {
787
+ totalCount
788
+ }
789
+ author {
790
+ login
791
+ url
792
+ avatarUrl
793
+ }
794
+ labels(first: 20) {
795
+ nodes {
796
+ name
797
+ color
798
+ }
799
+ }
800
+ comments {
801
+ totalCount
802
+ }
803
+ closingIssuesReferences(first: 10) {
804
+ nodes {
805
+ number
806
+ url
807
+ }
808
+ }
809
+ `;
810
+ var GITHUB_PROJECT_PULL_REQUEST_INSIGHT_FIELDS = `
811
+ reviews(first: 100) {
812
+ pageInfo {
813
+ hasNextPage
814
+ endCursor
815
+ }
816
+ nodes {
817
+ state
818
+ author {
819
+ login
820
+ }
821
+ }
822
+ }
823
+ reviewThreads(first: 100) {
824
+ totalCount
825
+ pageInfo {
826
+ hasNextPage
827
+ endCursor
828
+ }
829
+ nodes {
830
+ isResolved
831
+ comments(first: 1) {
832
+ nodes {
833
+ author {
834
+ login
835
+ }
836
+ }
837
+ }
838
+ }
839
+ }
840
+ statusCheckRollup {
841
+ contexts(first: 100) {
842
+ pageInfo {
843
+ hasNextPage
844
+ endCursor
845
+ }
846
+ nodes {
847
+ __typename
848
+ ... on CheckRun {
849
+ status
850
+ conclusion
851
+ }
852
+ ... on StatusContext {
853
+ state
854
+ }
855
+ }
856
+ }
857
+ }
858
+ `;
859
+ var GITHUB_PROJECT_PULL_REQUEST_SUMMARY_FIELDS = `${GITHUB_PROJECT_PULL_REQUEST_BASE_FIELDS}${GITHUB_PROJECT_PULL_REQUEST_INSIGHT_FIELDS}`;
860
+ var GITHUB_PROJECT_PULL_REQUEST_METRICS_FIELDS = `
861
+ number
862
+ mergeable
863
+ reviews(first: 100) {
864
+ pageInfo {
865
+ hasNextPage
866
+ endCursor
867
+ }
868
+ nodes {
869
+ state
870
+ author {
871
+ login
872
+ }
873
+ }
874
+ }
875
+ reviewThreads(first: 100) {
876
+ pageInfo {
877
+ hasNextPage
878
+ endCursor
879
+ }
880
+ nodes {
881
+ isResolved
882
+ comments(first: 1) {
883
+ nodes {
884
+ author {
885
+ login
886
+ }
887
+ }
888
+ }
889
+ }
890
+ }
891
+ statusCheckRollup {
892
+ contexts(first: 100) {
893
+ pageInfo {
894
+ hasNextPage
895
+ endCursor
896
+ }
897
+ nodes {
898
+ __typename
899
+ ... on CheckRun {
900
+ status
901
+ conclusion
902
+ }
903
+ ... on StatusContext {
904
+ state
905
+ }
906
+ }
907
+ }
908
+ }
909
+ `;
910
+ var GITHUB_PROJECT_PULL_REQUESTS_QUERY = `
911
+ query GitHubProjectPullRequests($owner: String!, $repo: String!, $after: String, $first: Int!) {
912
+ repository(owner: $owner, name: $repo) {
913
+ nameWithOwner
914
+ url
915
+ defaultBranchRef {
916
+ name
917
+ }
918
+ pullRequests(first: $first, after: $after, states: [OPEN], orderBy: { field: UPDATED_AT, direction: DESC }) {
919
+ totalCount
920
+ pageInfo {
921
+ hasNextPage
922
+ endCursor
923
+ }
924
+ nodes {
925
+ ${GITHUB_PROJECT_PULL_REQUEST_SUMMARY_FIELDS}
926
+ }
927
+ }
928
+ }
929
+ }
930
+ `;
931
+ var GITHUB_PROJECT_PULL_REQUEST_METRICS_QUERY = `
932
+ query GitHubProjectPullRequestMetrics($owner: String!, $repo: String!, $after: String, $first: Int!) {
933
+ repository(owner: $owner, name: $repo) {
934
+ defaultBranchRef {
935
+ name
936
+ }
937
+ pullRequests(first: $first, after: $after, states: [OPEN], orderBy: { field: UPDATED_AT, direction: DESC }) {
938
+ totalCount
939
+ pageInfo {
940
+ hasNextPage
941
+ endCursor
942
+ }
943
+ nodes {
944
+ ${GITHUB_PROJECT_PULL_REQUEST_METRICS_FIELDS}
945
+ }
946
+ }
947
+ }
948
+ }
949
+ `;
950
+ function buildGitHubProjectPullRequestByNumberAlias(pullRequestNumber) {
951
+ return `pr_${Math.max(1, Math.floor(pullRequestNumber))}`;
952
+ }
953
+ function buildGitHubProjectPullRequestsByNumberQuery(pullRequestNumbers) {
954
+ const normalizedNumbers = [
955
+ ...new Set(
956
+ pullRequestNumbers.map((value) => Math.floor(value)).filter((value) => Number.isFinite(value) && value > 0)
957
+ )
958
+ ];
959
+ if (normalizedNumbers.length === 0) {
960
+ throw new Error("At least one pull request number is required.");
961
+ }
962
+ const selections = normalizedNumbers.map(
963
+ (pullRequestNumber) => `
964
+ ${buildGitHubProjectPullRequestByNumberAlias(pullRequestNumber)}: pullRequest(number: ${pullRequestNumber}) {
965
+ ${GITHUB_PROJECT_PULL_REQUEST_BASE_FIELDS}
966
+ }`
967
+ ).join("\n");
968
+ return `
969
+ query GitHubProjectPullRequestsByNumber($owner: String!, $repo: String!) {
970
+ repository(owner: $owner, name: $repo) {
971
+ ${selections}
972
+ }
973
+ }
974
+ `;
975
+ }
976
+ var GITHUB_PROJECT_OPEN_PULL_REQUEST_COUNT_QUERY = `
977
+ query GitHubProjectOpenPullRequestCount($owner: String!, $repo: String!) {
978
+ repository(owner: $owner, name: $repo) {
979
+ pullRequests(first: 1, states: [OPEN]) {
980
+ totalCount
981
+ }
982
+ }
983
+ }
984
+ `;
985
+ var GITHUB_PULL_REQUEST_CLOSING_ISSUES_QUERY = `
986
+ query GitHubPullRequestClosingIssues($owner: String!, $repo: String!, $pullRequestNumber: Int!) {
987
+ repository(owner: $owner, name: $repo) {
988
+ pullRequest(number: $pullRequestNumber) {
989
+ closingIssuesReferences(first: 10) {
990
+ nodes {
991
+ number
992
+ url
993
+ }
994
+ }
995
+ }
996
+ }
997
+ }
998
+ `;
753
999
  var GITHUB_PULL_REQUEST_REVIEW_THREADS_DETAILED_QUERY = `
754
1000
  query GitHubPullRequestReviewThreadsDetailed($owner: String!, $repo: String!, $pullRequestNumber: Int!, $after: String) {
755
1001
  repository(owner: $owner, name: $repo) {
@@ -860,6 +1106,30 @@ var GITHUB_MARK_PULL_REQUEST_READY_FOR_REVIEW_MUTATION = `
860
1106
  }
861
1107
  }
862
1108
  `;
1109
+ var GITHUB_REQUEST_PULL_REQUEST_COPILOT_REVIEW_MUTATION = `
1110
+ mutation GitHubRequestPullRequestCopilotReview($pullRequestId: ID!, $botLogins: [String!]!) {
1111
+ requestReviews(input: {
1112
+ pullRequestId: $pullRequestId
1113
+ botLogins: $botLogins
1114
+ }) {
1115
+ pullRequest {
1116
+ id
1117
+ number
1118
+ url
1119
+ }
1120
+ requestedReviewers(first: 10) {
1121
+ edges {
1122
+ node {
1123
+ __typename
1124
+ ... on Bot {
1125
+ login
1126
+ }
1127
+ }
1128
+ }
1129
+ }
1130
+ }
1131
+ }
1132
+ `;
863
1133
  var DEFAULT_SETTINGS = {
864
1134
  mappings: [],
865
1135
  syncState: {
@@ -959,6 +1229,118 @@ function getErrorResponseDataMessage(error) {
959
1229
  const message = data.message;
960
1230
  return typeof message === "string" && message.trim() ? message.trim() : void 0;
961
1231
  }
1232
+ function getErrorResponseDataErrors(error) {
1233
+ if (!error || typeof error !== "object" || !("response" in error)) {
1234
+ return [];
1235
+ }
1236
+ const response = error.response;
1237
+ if (!response || typeof response !== "object" || !("data" in response)) {
1238
+ return [];
1239
+ }
1240
+ const data = response.data;
1241
+ if (!data || typeof data !== "object" || !("errors" in data)) {
1242
+ return [];
1243
+ }
1244
+ const errors = data.errors;
1245
+ return Array.isArray(errors) ? errors : [];
1246
+ }
1247
+ function getGitHubValidationErrorSummary(error) {
1248
+ const entries = getErrorResponseDataErrors(error);
1249
+ if (entries.length === 0) {
1250
+ return void 0;
1251
+ }
1252
+ const summaries = /* @__PURE__ */ new Set();
1253
+ for (const entry of entries) {
1254
+ if (typeof entry === "string" && entry.trim()) {
1255
+ summaries.add(entry.trim());
1256
+ continue;
1257
+ }
1258
+ if (!entry || typeof entry !== "object") {
1259
+ continue;
1260
+ }
1261
+ const explicitMessage = "message" in entry && typeof entry.message === "string" ? entry.message.trim() : "";
1262
+ if (explicitMessage) {
1263
+ summaries.add(explicitMessage);
1264
+ continue;
1265
+ }
1266
+ const resource = "resource" in entry && typeof entry.resource === "string" ? entry.resource.trim() : "";
1267
+ const field = "field" in entry && typeof entry.field === "string" ? entry.field.trim() : "";
1268
+ const code = "code" in entry && typeof entry.code === "string" ? entry.code.trim().replace(/_/g, " ") : "";
1269
+ const parts = [
1270
+ resource ? resource.replace(/([a-z])([A-Z])/g, "$1 $2") : "",
1271
+ field ? `field "${field}"` : "",
1272
+ code
1273
+ ].filter(Boolean);
1274
+ if (parts.length > 0) {
1275
+ summaries.add(parts.join(" "));
1276
+ }
1277
+ }
1278
+ return summaries.size > 0 ? [...summaries].join("; ") : void 0;
1279
+ }
1280
+ function formatGitHubPermissionsHeader(value) {
1281
+ if (!value?.trim()) {
1282
+ return void 0;
1283
+ }
1284
+ const parts = value.replace(/;/g, ",").split(",").map((entry) => entry.trim()).filter(Boolean).map((entry) => {
1285
+ const [name, level] = entry.split("=").map((part) => part.trim());
1286
+ if (!name) {
1287
+ return "";
1288
+ }
1289
+ const normalizedName = name.replace(/_/g, " ");
1290
+ return level ? `${normalizedName}: ${level}` : normalizedName;
1291
+ }).filter(Boolean);
1292
+ return parts.length > 0 ? parts.join(", ") : void 0;
1293
+ }
1294
+ function getAcceptedGitHubPermissionsSummary(error) {
1295
+ const headers = getErrorResponseHeaders(error);
1296
+ return formatGitHubPermissionsHeader(headers["x-accepted-github-permissions"]) ?? formatGitHubPermissionsHeader(headers["x-accepted-oauth-scopes"]);
1297
+ }
1298
+ function buildGitHubPullRequestWriteActionError(params) {
1299
+ const rateLimitPause = getGitHubRateLimitPauseDetails(params.error);
1300
+ if (rateLimitPause) {
1301
+ const resourceLabel = formatGitHubRateLimitResource(rateLimitPause.resource) ?? "GitHub API";
1302
+ return new Error(`${resourceLabel} rate limit reached. Wait until ${formatUtcTimestamp(rateLimitPause.resetAt)} before retrying.`);
1303
+ }
1304
+ const actionLabel = params.action === "comment" ? "comment" : params.action === "review" ? "review" : "branch update";
1305
+ const rawMessage = getErrorMessage(params.error).trim();
1306
+ const responseMessage = getErrorResponseDataMessage(params.error);
1307
+ const validationSummary = getGitHubValidationErrorSummary(params.error);
1308
+ const permissionsSummary = getAcceptedGitHubPermissionsSummary(params.error);
1309
+ const status = getErrorStatus(params.error);
1310
+ const combinedMessage = [rawMessage, responseMessage].filter((value) => Boolean(value?.trim())).join(" ").toLowerCase();
1311
+ if (params.action === "review" && params.reviewType === "REQUEST_CHANGES" && !params.body?.trim() && status === 422) {
1312
+ return new Error("Add a review summary before requesting changes. GitHub requires a comment for this action.");
1313
+ }
1314
+ if ((status === 403 || status === 404) && (combinedMessage.includes("resource not accessible") || combinedMessage.includes("not accessible by"))) {
1315
+ const requiredAccess = params.action === "comment" ? "Issues: write" : "Pull requests: write";
1316
+ const permissionSuffix = permissionsSummary ? ` GitHub reported required permissions: ${permissionsSummary}.` : "";
1317
+ return new Error(
1318
+ `GitHub rejected this ${actionLabel} because the configured token cannot write to ${params.repositoryLabel}. Reconnect a token with ${requiredAccess} access and repository visibility for this repo, then retry.${permissionSuffix}`
1319
+ );
1320
+ }
1321
+ if (params.action === "update_branch" && status === 422) {
1322
+ if (combinedMessage.includes("expected head sha") || validationSummary?.toLowerCase().includes("expected_head_sha")) {
1323
+ return new Error("This pull request changed while the branch update was being requested. Refresh the queue and try again.");
1324
+ }
1325
+ if (combinedMessage.includes("merge conflict") || combinedMessage.includes("conflict")) {
1326
+ return new Error("This pull request needs conflict resolution before it can be updated with the base branch.");
1327
+ }
1328
+ }
1329
+ if (responseMessage === "Validation Failed" && validationSummary) {
1330
+ return new Error(`GitHub rejected this ${actionLabel}: ${validationSummary}.`);
1331
+ }
1332
+ if (responseMessage && responseMessage !== rawMessage) {
1333
+ const validationSuffix = validationSummary ? ` ${validationSummary}.` : "";
1334
+ return new Error(`GitHub rejected this ${actionLabel}: ${responseMessage}.${validationSuffix}`);
1335
+ }
1336
+ if (validationSummary) {
1337
+ return new Error(`GitHub rejected this ${actionLabel}: ${validationSummary}.`);
1338
+ }
1339
+ if (rawMessage) {
1340
+ return new Error(rawMessage);
1341
+ }
1342
+ return new Error(`GitHub rejected this ${actionLabel}.`);
1343
+ }
962
1344
  function parsePositiveInteger(value) {
963
1345
  if (!value?.trim()) {
964
1346
  return void 0;
@@ -980,6 +1362,58 @@ function parseRetryAfterTimestamp(value, now = Date.now()) {
980
1362
  const timestamp = Date.parse(value);
981
1363
  return Number.isFinite(timestamp) ? timestamp : void 0;
982
1364
  }
1365
+ function buildGitHubRepositoryTokenCapabilityAuditCacheKey(repository, samplePullRequestNumber) {
1366
+ return `${repository.url.toLowerCase()}::${typeof samplePullRequestNumber === "number" ? samplePullRequestNumber : "none"}`;
1367
+ }
1368
+ function clearGitHubRepositoryTokenCapabilityAudits() {
1369
+ activeGitHubRepositoryTokenCapabilityAuditCache.clear();
1370
+ activeGitHubRepositoryTokenCapabilityAuditPromiseCache.clear();
1371
+ }
1372
+ function getGitHubCapabilityMissingPermissionLabel(capability) {
1373
+ switch (capability) {
1374
+ case "comment":
1375
+ return "Issues: write or Pull requests: write";
1376
+ case "review":
1377
+ case "close":
1378
+ case "update_branch":
1379
+ return "Pull requests: write";
1380
+ case "merge":
1381
+ return "Contents: write";
1382
+ case "rerun_ci":
1383
+ return "Checks: write";
1384
+ }
1385
+ }
1386
+ function classifyGitHubCapabilityProbeError(error, options = {}) {
1387
+ const status = getErrorStatus(error);
1388
+ if (status && options.grantedStatuses?.includes(status)) {
1389
+ return "granted";
1390
+ }
1391
+ if (status === 404 && options.allowNotFoundAsGranted) {
1392
+ return "granted";
1393
+ }
1394
+ if (status === 401 || status === 403 || status === 404) {
1395
+ return "missing";
1396
+ }
1397
+ return "unknown";
1398
+ }
1399
+ function buildGitHubRepositoryTokenCapabilityAudit(params) {
1400
+ const warnings = [...new Set((params.warnings ?? []).map((warning) => warning.trim()).filter(Boolean))];
1401
+ return {
1402
+ repositoryUrl: params.repository.url,
1403
+ repositoryLabel: formatRepositoryLabel(params.repository),
1404
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
1405
+ status: params.missingPermissions.length > 0 ? "missing_permissions" : warnings.length > 0 ? "unverifiable" : "verified",
1406
+ ...typeof params.samplePullRequestNumber === "number" ? { samplePullRequestNumber: params.samplePullRequestNumber } : {},
1407
+ canComment: params.canComment,
1408
+ canReview: params.canReview,
1409
+ canClose: params.canClose,
1410
+ canUpdateBranch: params.canUpdateBranch,
1411
+ canMerge: params.canMerge,
1412
+ canRerunCi: params.canRerunCi,
1413
+ missingPermissions: [...new Set(params.missingPermissions)].sort((left, right) => left.localeCompare(right)),
1414
+ warnings
1415
+ };
1416
+ }
983
1417
  function normalizeSecretRef(value) {
984
1418
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
985
1419
  }
@@ -1025,7 +1459,10 @@ function getGitHubRateLimitPauseDetails(error, now = Date.now()) {
1025
1459
  const retryAfterTimestamp = parseRetryAfterTimestamp(headers["retry-after"], now);
1026
1460
  const responseMessage = getErrorResponseDataMessage(error);
1027
1461
  const rawMessage = [getErrorMessage(error), responseMessage].filter((value) => Boolean(value?.trim())).join(" ").toLowerCase();
1028
- const looksRateLimited = remaining === "0" || resetAtSeconds !== void 0 || retryAfterTimestamp !== void 0 || rawMessage.includes("rate limit");
1462
+ const hasPrimaryLimitHeaders = remaining === "0" && resetAtSeconds !== void 0;
1463
+ const hasRetryAfterLimitHint = retryAfterTimestamp !== void 0;
1464
+ const hasRateLimitMessage = rawMessage.includes("rate limit");
1465
+ const looksRateLimited = status === 429 || hasPrimaryLimitHeaders || hasRetryAfterLimitHint || hasRateLimitMessage;
1029
1466
  if (!looksRateLimited) {
1030
1467
  return null;
1031
1468
  }
@@ -1442,27 +1879,117 @@ function getSyncableMappingsForTarget(mappings, target) {
1442
1879
  return syncableMappings;
1443
1880
  }
1444
1881
  }
1445
- function doesGitHubIssueMatchTarget(issue, target) {
1446
- if (!target || target.kind !== "issue") {
1447
- return true;
1882
+ function normalizeProjectNameForComparison(value) {
1883
+ if (typeof value !== "string") {
1884
+ return "";
1448
1885
  }
1449
- const normalizedIssueUrl = normalizeGitHubIssueHtmlUrl(issue.htmlUrl) ?? issue.htmlUrl;
1450
- return target.githubIssueId !== void 0 && issue.id === target.githubIssueId || target.githubIssueNumber !== void 0 && issue.number === target.githubIssueNumber || target.githubIssueUrl !== void 0 && normalizedIssueUrl === target.githubIssueUrl;
1886
+ return value.trim().replace(/\s+/g, " ").toLowerCase();
1451
1887
  }
1452
- function doesImportedIssueMatchTarget(issue, target) {
1453
- if (!target || target.kind !== "issue") {
1454
- return true;
1888
+ function hydrateResolvedProjectMapping(mapping, context) {
1889
+ const resolvedProjectName = mapping.paperclipProjectName.trim() || context.projectName?.trim() || mapping.paperclipProjectName;
1890
+ return {
1891
+ ...mapping,
1892
+ companyId: mapping.companyId ?? context.companyId,
1893
+ paperclipProjectId: mapping.paperclipProjectId ?? context.projectId,
1894
+ paperclipProjectName: resolvedProjectName
1895
+ };
1896
+ }
1897
+ function getProjectRepositoryUrlFromProjectRecord(project) {
1898
+ if (!project) {
1899
+ return void 0;
1455
1900
  }
1456
- return target.issueId !== void 0 && issue.paperclipIssueId === target.issueId || target.githubIssueId !== void 0 && issue.githubIssueId === target.githubIssueId || target.githubIssueNumber !== void 0 && issue.githubIssueNumber === target.githubIssueNumber;
1901
+ const repositoryCandidates = [
1902
+ project.codebase?.repoUrl,
1903
+ project.primaryWorkspace?.repoUrl,
1904
+ ...Array.isArray(project.workspaces) ? project.workspaces.map((workspace) => workspace?.repoUrl) : []
1905
+ ];
1906
+ for (const repositoryUrl of repositoryCandidates) {
1907
+ if (typeof repositoryUrl !== "string" || !repositoryUrl.trim()) {
1908
+ continue;
1909
+ }
1910
+ const normalizedRepositoryUrl = parseRepositoryReference(repositoryUrl)?.url ?? repositoryUrl.trim();
1911
+ if (parseRepositoryReference(normalizedRepositoryUrl)) {
1912
+ return normalizedRepositoryUrl;
1913
+ }
1914
+ }
1915
+ return void 0;
1457
1916
  }
1458
- async function resolvePaperclipIssueGitHubLink(ctx, issueId, companyId) {
1459
- const linkRecords = await listGitHubIssueLinkRecords(ctx, {
1460
- paperclipIssueId: issueId
1461
- });
1462
- const entityMatch = linkRecords.find((record) => !record.data.companyId || record.data.companyId === companyId);
1463
- if (entityMatch) {
1464
- return {
1465
- source: "entity",
1917
+ async function resolveProjectScopedMappings(ctx, mappings, params) {
1918
+ const companyId = normalizeCompanyId(params.companyId);
1919
+ const projectId = typeof params.projectId === "string" && params.projectId.trim() ? params.projectId.trim() : void 0;
1920
+ if (!companyId || !projectId) {
1921
+ return [];
1922
+ }
1923
+ const candidateMappings = mappings.filter((mapping) => mapping.repositoryUrl.trim());
1924
+ const exactMatches = candidateMappings.filter(
1925
+ (mapping) => mapping.paperclipProjectId === projectId && (!mapping.companyId || mapping.companyId === companyId)
1926
+ ).map((mapping) => hydrateResolvedProjectMapping(mapping, {
1927
+ companyId,
1928
+ projectId
1929
+ }));
1930
+ if (exactMatches.length > 0) {
1931
+ return exactMatches;
1932
+ }
1933
+ const namedFallbackCandidates = candidateMappings.filter(
1934
+ (mapping) => !mapping.paperclipProjectId && mapping.companyId === companyId && Boolean(normalizeProjectNameForComparison(mapping.paperclipProjectName))
1935
+ );
1936
+ let projectName = "";
1937
+ let projectRepositoryUrl;
1938
+ try {
1939
+ const project = await ctx.projects.get(projectId, companyId);
1940
+ projectName = typeof project?.name === "string" ? project.name.trim() : "";
1941
+ projectRepositoryUrl = getProjectRepositoryUrlFromProjectRecord(project);
1942
+ } catch (error) {
1943
+ ctx.logger.warn("Unable to resolve Paperclip project metadata for GitHub project mapping fallback.", {
1944
+ companyId,
1945
+ projectId,
1946
+ error: getErrorMessage(error)
1947
+ });
1948
+ return [];
1949
+ }
1950
+ const normalizedProjectName = normalizeProjectNameForComparison(projectName);
1951
+ if (normalizedProjectName) {
1952
+ const namedFallbackMatches = namedFallbackCandidates.filter((mapping) => normalizeProjectNameForComparison(mapping.paperclipProjectName) === normalizedProjectName).map((mapping) => hydrateResolvedProjectMapping(mapping, {
1953
+ companyId,
1954
+ projectId,
1955
+ projectName
1956
+ }));
1957
+ if (namedFallbackMatches.length > 0) {
1958
+ return namedFallbackMatches;
1959
+ }
1960
+ }
1961
+ if (!projectRepositoryUrl) {
1962
+ return [];
1963
+ }
1964
+ return [{
1965
+ id: `project-repo:${companyId}:${projectId}`,
1966
+ repositoryUrl: projectRepositoryUrl,
1967
+ paperclipProjectName: projectName || "Project",
1968
+ paperclipProjectId: projectId,
1969
+ companyId
1970
+ }];
1971
+ }
1972
+ function doesGitHubIssueMatchTarget(issue, target) {
1973
+ if (!target || target.kind !== "issue") {
1974
+ return true;
1975
+ }
1976
+ const normalizedIssueUrl = normalizeGitHubIssueHtmlUrl(issue.htmlUrl) ?? issue.htmlUrl;
1977
+ return target.githubIssueId !== void 0 && issue.id === target.githubIssueId || target.githubIssueNumber !== void 0 && issue.number === target.githubIssueNumber || target.githubIssueUrl !== void 0 && normalizedIssueUrl === target.githubIssueUrl;
1978
+ }
1979
+ function doesImportedIssueMatchTarget(issue, target) {
1980
+ if (!target || target.kind !== "issue") {
1981
+ return true;
1982
+ }
1983
+ return target.issueId !== void 0 && issue.paperclipIssueId === target.issueId || target.githubIssueId !== void 0 && issue.githubIssueId === target.githubIssueId || target.githubIssueNumber !== void 0 && issue.githubIssueNumber === target.githubIssueNumber;
1984
+ }
1985
+ async function resolvePaperclipIssueGitHubLink(ctx, issueId, companyId) {
1986
+ const linkRecords = await listGitHubIssueLinkRecords(ctx, {
1987
+ paperclipIssueId: issueId
1988
+ });
1989
+ const entityMatch = linkRecords.find((record) => !record.data.companyId || record.data.companyId === companyId);
1990
+ if (entityMatch) {
1991
+ return {
1992
+ source: "entity",
1466
1993
  companyId: entityMatch.data.companyId,
1467
1994
  paperclipProjectId: entityMatch.data.paperclipProjectId,
1468
1995
  repositoryUrl: entityMatch.data.repositoryUrl,
@@ -1624,7 +2151,7 @@ function extractGitHubLinksFromCommentBody(body) {
1624
2151
  return [...links.values()];
1625
2152
  }
1626
2153
  async function buildToolbarSyncState(ctx, input) {
1627
- const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
2154
+ const settings = await getActiveOrCurrentSyncState(ctx);
1628
2155
  const config = await getResolvedConfig(ctx);
1629
2156
  const githubTokenConfigured = hasConfiguredGithubToken(settings, config);
1630
2157
  const companyId = typeof input.companyId === "string" && input.companyId.trim() ? input.companyId.trim() : void 0;
@@ -1872,6 +2399,93 @@ async function listAvailableAssignees(ctx, companyId) {
1872
2399
  return [];
1873
2400
  }
1874
2401
  }
2402
+ async function resolvePaperclipIssueDrawerAgents(ctx, companyId, agentIds) {
2403
+ const normalizedAgentIds = [
2404
+ ...new Set(
2405
+ agentIds.filter((agentId) => typeof agentId === "string" && agentId.trim().length > 0).map((agentId) => agentId.trim())
2406
+ )
2407
+ ];
2408
+ const agentsById = /* @__PURE__ */ new Map();
2409
+ if (normalizedAgentIds.length === 0 || !ctx.agents || typeof ctx.agents.get !== "function") {
2410
+ return agentsById;
2411
+ }
2412
+ await Promise.all(
2413
+ normalizedAgentIds.map(async (agentId) => {
2414
+ try {
2415
+ const agent = await ctx.agents.get(agentId, companyId);
2416
+ if (!agent) {
2417
+ return;
2418
+ }
2419
+ agentsById.set(agent.id, {
2420
+ id: agent.id,
2421
+ name: agent.name,
2422
+ ...agent.title?.trim() ? { title: agent.title.trim() } : {}
2423
+ });
2424
+ } catch (error) {
2425
+ ctx.logger.warn("Unable to load Paperclip agent for pull request issue drawer.", {
2426
+ companyId,
2427
+ agentId,
2428
+ error: getErrorMessage(error)
2429
+ });
2430
+ }
2431
+ })
2432
+ );
2433
+ return agentsById;
2434
+ }
2435
+ async function buildProjectPullRequestPaperclipIssueData(ctx, input) {
2436
+ const issueId = typeof input.issueId === "string" && input.issueId.trim() ? input.issueId.trim() : void 0;
2437
+ const companyId = typeof input.companyId === "string" && input.companyId.trim() ? input.companyId.trim() : void 0;
2438
+ if (!issueId || !companyId) {
2439
+ return null;
2440
+ }
2441
+ const issue = await ctx.issues.get(issueId, companyId);
2442
+ if (!issue) {
2443
+ return null;
2444
+ }
2445
+ const comments = ctx.issues && typeof ctx.issues.listComments === "function" ? await ctx.issues.listComments(issue.id, companyId) : [];
2446
+ const agentsById = await resolvePaperclipIssueDrawerAgents(ctx, companyId, [
2447
+ issue.assigneeAgentId,
2448
+ issue.createdByAgentId,
2449
+ ...comments.map((comment) => comment.authorAgentId)
2450
+ ]);
2451
+ const assignee = issue.assigneeAgentId ? agentsById.get(issue.assigneeAgentId) ?? null : null;
2452
+ const orderedComments = [...comments].sort(
2453
+ (left, right) => coerceDate(left.createdAt).getTime() - coerceDate(right.createdAt).getTime()
2454
+ );
2455
+ return {
2456
+ issueId: issue.id,
2457
+ ...issue.identifier?.trim() ? { issueIdentifier: issue.identifier.trim() } : {},
2458
+ title: issue.title,
2459
+ description: issue.description ?? "",
2460
+ status: issue.status,
2461
+ priority: issue.priority,
2462
+ projectName: issue.project?.name ?? void 0,
2463
+ createdAt: coerceDate(issue.createdAt).toISOString(),
2464
+ updatedAt: coerceDate(issue.updatedAt).toISOString(),
2465
+ labels: Array.isArray(issue.labels) ? issue.labels.map((label) => ({
2466
+ name: label.name,
2467
+ color: label.color
2468
+ })).filter((label) => label.name.trim().length > 0) : [],
2469
+ assignee: assignee ? {
2470
+ id: assignee.id,
2471
+ name: assignee.name,
2472
+ ...assignee.title ? { title: assignee.title } : {}
2473
+ } : null,
2474
+ commentCount: orderedComments.length,
2475
+ comments: orderedComments.map((comment) => {
2476
+ const author = comment.authorAgentId ? agentsById.get(comment.authorAgentId) : null;
2477
+ return {
2478
+ id: comment.id,
2479
+ body: comment.body ?? "",
2480
+ createdAt: coerceDate(comment.createdAt).toISOString(),
2481
+ updatedAt: coerceDate(comment.updatedAt).toISOString(),
2482
+ authorLabel: author?.name ?? (comment.authorUserId ? "Team member" : "Paperclip"),
2483
+ authorKind: author ? "agent" : comment.authorUserId ? "user" : "system",
2484
+ ...author?.title ? { authorTitle: author.title } : {}
2485
+ };
2486
+ })
2487
+ };
2488
+ }
1875
2489
  function createSetupConfigurationErrorSyncState(issue, trigger) {
1876
2490
  switch (issue) {
1877
2491
  case "missing_token":
@@ -1955,7 +2569,11 @@ async function saveSettingsSyncState(ctx, settings, syncState) {
1955
2569
  return next;
1956
2570
  }
1957
2571
  async function setSyncCancellationRequest(ctx, request) {
1958
- await ctx.state.set(SYNC_CANCELLATION_SCOPE, request);
2572
+ if (request) {
2573
+ await ctx.state.set(SYNC_CANCELLATION_SCOPE, request);
2574
+ return;
2575
+ }
2576
+ await ctx.state.delete(SYNC_CANCELLATION_SCOPE);
1959
2577
  }
1960
2578
  async function getSyncCancellationRequest(ctx) {
1961
2579
  const activeRequestedAt = activeRunningSyncState?.syncState.cancelRequestedAt?.trim();
@@ -2018,11 +2636,14 @@ async function waitForSyncResultWithinGracePeriod(promise, timeoutMs) {
2018
2636
  }
2019
2637
  }
2020
2638
  async function getActiveOrCurrentSyncState(ctx) {
2639
+ if (activeRunningSyncState?.syncState.status === "running") {
2640
+ return activeRunningSyncState;
2641
+ }
2021
2642
  const current = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
2022
2643
  if (current.syncState.status === "running") {
2023
2644
  return current;
2024
2645
  }
2025
- return activeRunningSyncState?.syncState.status === "running" ? activeRunningSyncState : current;
2646
+ return current;
2026
2647
  }
2027
2648
  function updateSyncFailureContext(current, next) {
2028
2649
  if ("phase" in next) {
@@ -2216,6 +2837,22 @@ function normalizeGitHubUsername(value) {
2216
2837
  const trimmed = value.trim().replace(/^@+/, "");
2217
2838
  return trimmed ? trimmed.toLowerCase() : void 0;
2218
2839
  }
2840
+ function buildGitHubUsernameAliases(value) {
2841
+ const normalized = normalizeGitHubUsername(value);
2842
+ if (!normalized) {
2843
+ return [];
2844
+ }
2845
+ const aliases = /* @__PURE__ */ new Set([normalized]);
2846
+ if (normalized.endsWith("[bot]")) {
2847
+ const withoutBotSuffix = normalized.slice(0, -"[bot]".length);
2848
+ if (withoutBotSuffix) {
2849
+ aliases.add(withoutBotSuffix);
2850
+ }
2851
+ } else {
2852
+ aliases.add(`${normalized}[bot]`);
2853
+ }
2854
+ return [...aliases];
2855
+ }
2219
2856
  function parseIgnoredIssueAuthorUsernames(value) {
2220
2857
  return value.split(/[\s,]+/g).map((entry) => normalizeGitHubUsername(entry)).filter((entry) => Boolean(entry));
2221
2858
  }
@@ -2535,6 +3172,286 @@ function getPageCursor(pageInfo) {
2535
3172
  }
2536
3173
  return pageInfo.endCursor;
2537
3174
  }
3175
+ async function mapWithConcurrency(items, concurrency, mapper) {
3176
+ if (items.length === 0) {
3177
+ return [];
3178
+ }
3179
+ const limit = Math.max(1, Math.floor(concurrency));
3180
+ const results = new Array(items.length);
3181
+ let nextIndex = 0;
3182
+ async function worker() {
3183
+ while (nextIndex < items.length) {
3184
+ const currentIndex = nextIndex;
3185
+ nextIndex += 1;
3186
+ results[currentIndex] = await mapper(items[currentIndex], currentIndex);
3187
+ }
3188
+ }
3189
+ await Promise.all(
3190
+ Array.from(
3191
+ { length: Math.min(limit, items.length) },
3192
+ async () => worker()
3193
+ )
3194
+ );
3195
+ return results;
3196
+ }
3197
+ function getFreshCacheValue(cache, key, now = Date.now()) {
3198
+ const entry = cache.get(key);
3199
+ if (!entry) {
3200
+ return null;
3201
+ }
3202
+ if (entry.expiresAt <= now) {
3203
+ cache.delete(key);
3204
+ return null;
3205
+ }
3206
+ return entry.value;
3207
+ }
3208
+ function getFreshCacheEntry(cache, key, now = Date.now()) {
3209
+ const entry = cache.get(key);
3210
+ if (!entry) {
3211
+ return null;
3212
+ }
3213
+ if (entry.expiresAt <= now) {
3214
+ cache.delete(key);
3215
+ return null;
3216
+ }
3217
+ return entry;
3218
+ }
3219
+ function setCacheValue(cache, key, value, ttlMs, now = Date.now()) {
3220
+ cache.set(key, {
3221
+ expiresAt: now + ttlMs,
3222
+ value
3223
+ });
3224
+ return value;
3225
+ }
3226
+ function normalizeProjectPullRequestFilter(value) {
3227
+ switch (value) {
3228
+ case "mergeable":
3229
+ case "reviewable":
3230
+ case "failing":
3231
+ return value;
3232
+ default:
3233
+ return "all";
3234
+ }
3235
+ }
3236
+ function normalizeProjectPullRequestPageIndex(value) {
3237
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
3238
+ return Math.floor(value);
3239
+ }
3240
+ if (typeof value === "string" && value.trim()) {
3241
+ const parsed = Number(value.trim());
3242
+ if (Number.isFinite(parsed) && parsed >= 0) {
3243
+ return Math.floor(parsed);
3244
+ }
3245
+ }
3246
+ return 0;
3247
+ }
3248
+ function isCopilotActorLogin(value) {
3249
+ const normalizedLogin = normalizeGitHubUsername(value);
3250
+ return Boolean(normalizedLogin && normalizedLogin.includes("copilot"));
3251
+ }
3252
+ function getProjectPullRequestReviewThreadStarterLogin(node) {
3253
+ return normalizeGitHubUsername(
3254
+ node?.comments?.nodes?.find((comment) => comment?.author?.login)?.author?.login
3255
+ );
3256
+ }
3257
+ function getDetailedPullRequestReviewThreadStarterLogin(thread) {
3258
+ const rootComment = thread.comments.find((comment) => !comment.replyToId) ?? thread.comments[0];
3259
+ return normalizeGitHubUsername(rootComment?.authorLogin);
3260
+ }
3261
+ function summarizeProjectPullRequestReviewThreadsFromConnection(connection) {
3262
+ let unresolvedReviewThreads = 0;
3263
+ let copilotUnresolvedReviewThreads = 0;
3264
+ for (const thread of connection?.nodes ?? []) {
3265
+ if (thread?.isResolved !== false) {
3266
+ continue;
3267
+ }
3268
+ unresolvedReviewThreads += 1;
3269
+ if (isCopilotActorLogin(getProjectPullRequestReviewThreadStarterLogin(thread))) {
3270
+ copilotUnresolvedReviewThreads += 1;
3271
+ }
3272
+ }
3273
+ return {
3274
+ unresolvedReviewThreads,
3275
+ copilotUnresolvedReviewThreads
3276
+ };
3277
+ }
3278
+ function summarizeDetailedPullRequestReviewThreads(threads) {
3279
+ let unresolvedReviewThreads = 0;
3280
+ let copilotUnresolvedReviewThreads = 0;
3281
+ for (const thread of threads) {
3282
+ if (thread.isResolved) {
3283
+ continue;
3284
+ }
3285
+ unresolvedReviewThreads += 1;
3286
+ if (isCopilotActorLogin(getDetailedPullRequestReviewThreadStarterLogin(thread))) {
3287
+ copilotUnresolvedReviewThreads += 1;
3288
+ }
3289
+ }
3290
+ return {
3291
+ unresolvedReviewThreads,
3292
+ copilotUnresolvedReviewThreads
3293
+ };
3294
+ }
3295
+ function normalizeProjectPullRequestClosingIssues(repository, nodes) {
3296
+ const seen = /* @__PURE__ */ new Set();
3297
+ const issues = [];
3298
+ for (const node of nodes ?? []) {
3299
+ const issueNumber = typeof node?.number === "number" && node.number > 0 ? Math.floor(node.number) : void 0;
3300
+ const issueUrl = typeof node?.url === "string" && node.url.trim() ? normalizeGitHubIssueHtmlUrl(node.url) : issueNumber !== void 0 ? buildGitHubIssueUrlFromRepository(repository.url, issueNumber) : void 0;
3301
+ if (issueNumber === void 0 || !issueUrl || seen.has(issueUrl)) {
3302
+ continue;
3303
+ }
3304
+ seen.add(issueUrl);
3305
+ issues.push({
3306
+ number: issueNumber,
3307
+ url: issueUrl
3308
+ });
3309
+ }
3310
+ return issues;
3311
+ }
3312
+ function resolveProjectPullRequestReviewable(record) {
3313
+ return record.githubMergeable === true && record.checksStatus === "passed" && typeof record.copilotUnresolvedReviewThreads === "number" && record.copilotUnresolvedReviewThreads === 0;
3314
+ }
3315
+ function resolveProjectPullRequestMergeable(record) {
3316
+ return record.githubMergeable === true && record.checksStatus === "passed" && typeof record.reviewApprovals === "number" && record.reviewApprovals > 0 && typeof record.unresolvedReviewThreads === "number" && record.unresolvedReviewThreads === 0;
3317
+ }
3318
+ function resolveProjectPullRequestUpToDateStatus(record) {
3319
+ const mergeStateStatus = typeof record.mergeStateStatus === "string" ? record.mergeStateStatus : null;
3320
+ if (mergeStateStatus === "DIRTY" || record.mergeable === "CONFLICTING") {
3321
+ return "conflicts";
3322
+ }
3323
+ if (typeof record.behindBy === "number" && Number.isFinite(record.behindBy) && record.behindBy > 0) {
3324
+ return "can_update";
3325
+ }
3326
+ if (typeof record.behindBy === "number" && Number.isFinite(record.behindBy) && record.behindBy === 0) {
3327
+ return "up_to_date";
3328
+ }
3329
+ if (mergeStateStatus === "BEHIND") {
3330
+ return "can_update";
3331
+ }
3332
+ return "unknown";
3333
+ }
3334
+ function normalizeProjectPullRequestCopilotAction(value) {
3335
+ switch (typeof value === "string" ? value.trim().toLowerCase() : "") {
3336
+ case "fix_ci":
3337
+ return "fix_ci";
3338
+ case "rebase":
3339
+ return "rebase";
3340
+ case "address_review_feedback":
3341
+ return "address_review_feedback";
3342
+ case "review":
3343
+ return "review";
3344
+ default:
3345
+ return null;
3346
+ }
3347
+ }
3348
+ function getProjectPullRequestCopilotActionLabel(action) {
3349
+ switch (action) {
3350
+ case "fix_ci":
3351
+ return "Fix CI";
3352
+ case "rebase":
3353
+ return "Rebase";
3354
+ case "address_review_feedback":
3355
+ return "Address review feedback";
3356
+ case "review":
3357
+ return "Review";
3358
+ }
3359
+ }
3360
+ function getProjectPullRequestNumber(record) {
3361
+ const number = record.number;
3362
+ if (typeof number !== "number" || !Number.isFinite(number) || number <= 0) {
3363
+ return null;
3364
+ }
3365
+ return Math.floor(number);
3366
+ }
3367
+ function getProjectPullRequestUpdatedAtTimestamp(record) {
3368
+ const updatedAt = typeof record.updatedAt === "string" ? Date.parse(record.updatedAt) : Number.NaN;
3369
+ return Number.isFinite(updatedAt) ? updatedAt : 0;
3370
+ }
3371
+ function sortProjectPullRequestRecordsByUpdatedAt(records) {
3372
+ return [...records].sort((left, right) => {
3373
+ const timestampDelta = getProjectPullRequestUpdatedAtTimestamp(right) - getProjectPullRequestUpdatedAtTimestamp(left);
3374
+ if (timestampDelta !== 0) {
3375
+ return timestampDelta;
3376
+ }
3377
+ return (getProjectPullRequestNumber(right) ?? 0) - (getProjectPullRequestNumber(left) ?? 0);
3378
+ });
3379
+ }
3380
+ function getLinkedPaperclipIssueFromProjectPullRequestRecord(record) {
3381
+ const paperclipIssueId = typeof record.paperclipIssueId === "string" && record.paperclipIssueId.trim() ? record.paperclipIssueId.trim() : void 0;
3382
+ if (!paperclipIssueId) {
3383
+ return void 0;
3384
+ }
3385
+ return {
3386
+ paperclipIssueId,
3387
+ ...typeof record.paperclipIssueKey === "string" && record.paperclipIssueKey.trim() ? { paperclipIssueKey: record.paperclipIssueKey.trim() } : {}
3388
+ };
3389
+ }
3390
+ function buildProjectPullRequestMetrics(pullRequests, totalOpenPullRequests, defaultBranchName) {
3391
+ const mergeablePullRequestNumbers = [];
3392
+ const reviewablePullRequestNumbers = [];
3393
+ const failingPullRequestNumbers = [];
3394
+ for (const pullRequest of pullRequests) {
3395
+ const number = getProjectPullRequestNumber(pullRequest);
3396
+ if (number === null) {
3397
+ continue;
3398
+ }
3399
+ if (pullRequest.mergeable === true) {
3400
+ mergeablePullRequestNumbers.push(number);
3401
+ }
3402
+ if (pullRequest.reviewable === true) {
3403
+ reviewablePullRequestNumbers.push(number);
3404
+ }
3405
+ if (pullRequest.checksStatus === "failed") {
3406
+ failingPullRequestNumbers.push(number);
3407
+ }
3408
+ }
3409
+ return {
3410
+ totalOpenPullRequests,
3411
+ ...defaultBranchName ? { defaultBranchName } : {},
3412
+ mergeablePullRequests: mergeablePullRequestNumbers.length,
3413
+ reviewablePullRequests: reviewablePullRequestNumbers.length,
3414
+ failingPullRequests: failingPullRequestNumbers.length,
3415
+ mergeablePullRequestNumbers,
3416
+ reviewablePullRequestNumbers,
3417
+ failingPullRequestNumbers
3418
+ };
3419
+ }
3420
+ function getPublicProjectPullRequestMetrics(metrics) {
3421
+ return {
3422
+ totalOpenPullRequests: metrics.totalOpenPullRequests,
3423
+ ...metrics.defaultBranchName ? { defaultBranchName: metrics.defaultBranchName } : {},
3424
+ mergeablePullRequests: metrics.mergeablePullRequests,
3425
+ reviewablePullRequests: metrics.reviewablePullRequests,
3426
+ failingPullRequests: metrics.failingPullRequests
3427
+ };
3428
+ }
3429
+ function sliceProjectPullRequestRecords(pullRequests, pageIndex, pageSize = PROJECT_PULL_REQUEST_PAGE_SIZE) {
3430
+ const effectivePageSize = Math.max(1, Math.floor(pageSize));
3431
+ const maxPageIndex = Math.max(0, Math.ceil(pullRequests.length / effectivePageSize) - 1);
3432
+ const normalizedPageIndex = Math.min(Math.max(0, Math.floor(pageIndex)), maxPageIndex);
3433
+ const start = normalizedPageIndex * effectivePageSize;
3434
+ const end = start + effectivePageSize;
3435
+ return {
3436
+ pullRequests: pullRequests.slice(start, end),
3437
+ pageIndex: normalizedPageIndex,
3438
+ hasNextPage: end < pullRequests.length,
3439
+ hasPreviousPage: normalizedPageIndex > 0
3440
+ };
3441
+ }
3442
+ function sliceProjectPullRequestNumbers(pullRequestNumbers, pageIndex, pageSize = PROJECT_PULL_REQUEST_PAGE_SIZE) {
3443
+ const effectivePageSize = Math.max(1, Math.floor(pageSize));
3444
+ const maxPageIndex = Math.max(0, Math.ceil(pullRequestNumbers.length / effectivePageSize) - 1);
3445
+ const normalizedPageIndex = Math.min(Math.max(0, Math.floor(pageIndex)), maxPageIndex);
3446
+ const start = normalizedPageIndex * effectivePageSize;
3447
+ const end = start + effectivePageSize;
3448
+ return {
3449
+ pullRequestNumbers: pullRequestNumbers.slice(start, end),
3450
+ pageIndex: normalizedPageIndex,
3451
+ hasNextPage: end < pullRequestNumbers.length,
3452
+ hasPreviousPage: normalizedPageIndex > 0
3453
+ };
3454
+ }
2538
3455
  function classifyGitHubPullRequestCiState(contexts) {
2539
3456
  if (contexts.length === 0) {
2540
3457
  return "unfinished";
@@ -2814,17 +3731,24 @@ function tryBuildGitHubPullRequestStatusSnapshotFromBatchNode(node) {
2814
3731
  if (typeof node.number !== "number") {
2815
3732
  return null;
2816
3733
  }
2817
- const reviewThreads = node.reviewThreads;
2818
- const ciContexts = node.statusCheckRollup?.contexts;
2819
- if (reviewThreads?.pageInfo?.hasNextPage || ciContexts?.pageInfo?.hasNextPage) {
3734
+ const reviewThreadSummary = node.reviewThreads?.pageInfo?.hasNextPage ? null : summarizeProjectPullRequestReviewThreadsFromConnection(node.reviewThreads);
3735
+ const ciState = tryBuildGitHubPullRequestCiStateFromBatchNode(node);
3736
+ if (!reviewThreadSummary || !ciState) {
2820
3737
  return null;
2821
3738
  }
2822
3739
  return {
2823
3740
  number: node.number,
2824
- hasUnresolvedReviewThreads: (reviewThreads?.nodes ?? []).some((reviewThread) => reviewThread?.isResolved === false),
2825
- ciState: classifyGitHubPullRequestCiState(extractGitHubCiContextRecords(ciContexts?.nodes ?? []))
3741
+ hasUnresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads > 0,
3742
+ ciState
2826
3743
  };
2827
3744
  }
3745
+ function tryBuildGitHubPullRequestCiStateFromBatchNode(node) {
3746
+ const ciContexts = node.statusCheckRollup?.contexts;
3747
+ if (ciContexts?.pageInfo?.hasNextPage) {
3748
+ return null;
3749
+ }
3750
+ return classifyGitHubPullRequestCiState(extractGitHubCiContextRecords(ciContexts?.nodes ?? []));
3751
+ }
2828
3752
  async function warmGitHubPullRequestStatusCache(octokit, repository, targetPullRequestNumbers, pullRequestStatusCache) {
2829
3753
  if (targetPullRequestNumbers.size === 0) {
2830
3754
  return;
@@ -2854,6 +3778,7 @@ async function warmGitHubPullRequestStatusCache(octokit, repository, targetPullR
2854
3778
  const snapshot = tryBuildGitHubPullRequestStatusSnapshotFromBatchNode(node);
2855
3779
  if (snapshot) {
2856
3780
  pullRequestStatusCache.set(node.number, snapshot);
3781
+ cacheGitHubPullRequestStatusSnapshot(repository, snapshot);
2857
3782
  }
2858
3783
  }
2859
3784
  if (remainingNumbers.size === 0) {
@@ -2862,27 +3787,6 @@ async function warmGitHubPullRequestStatusCache(octokit, repository, targetPullR
2862
3787
  after = getPageCursor(pullRequests?.pageInfo);
2863
3788
  } while (after);
2864
3789
  }
2865
- async function hasGitHubPullRequestUnresolvedReviewThreads(octokit, repository, pullRequestNumber) {
2866
- let after;
2867
- do {
2868
- const response = await octokit.graphql(
2869
- GITHUB_PULL_REQUEST_REVIEW_THREADS_QUERY,
2870
- {
2871
- owner: repository.owner,
2872
- repo: repository.repo,
2873
- pullRequestNumber,
2874
- after
2875
- }
2876
- );
2877
- const reviewThreads = response.repository?.pullRequest?.reviewThreads;
2878
- const nodes = reviewThreads?.nodes ?? [];
2879
- if (nodes.some((node) => node?.isResolved === false)) {
2880
- return true;
2881
- }
2882
- after = getPageCursor(reviewThreads?.pageInfo);
2883
- } while (after);
2884
- return false;
2885
- }
2886
3790
  async function getGitHubPullRequestCiState(octokit, repository, pullRequestNumber) {
2887
3791
  const contexts = [];
2888
3792
  let after;
@@ -2918,22 +3822,53 @@ async function getGitHubPullRequestCiState(octokit, repository, pullRequestNumbe
2918
3822
  } while (after);
2919
3823
  return classifyGitHubPullRequestCiState(contexts);
2920
3824
  }
2921
- async function getGitHubPullRequestStatusSnapshot(octokit, repository, pullRequestNumber, pullRequestStatusCache) {
3825
+ async function getGitHubPullRequestStatusSnapshot(octokit, repository, pullRequestNumber, pullRequestStatusCache, options) {
2922
3826
  const cached = pullRequestStatusCache.get(pullRequestNumber);
2923
3827
  if (cached) {
2924
3828
  return cached;
2925
3829
  }
2926
- const [hasUnresolvedReviewThreads, ciState] = await Promise.all([
2927
- hasGitHubPullRequestUnresolvedReviewThreads(octokit, repository, pullRequestNumber),
2928
- getGitHubPullRequestCiState(octokit, repository, pullRequestNumber)
2929
- ]);
2930
- const snapshot = {
2931
- number: pullRequestNumber,
2932
- hasUnresolvedReviewThreads,
2933
- ciState
2934
- };
2935
- pullRequestStatusCache.set(pullRequestNumber, snapshot);
2936
- return snapshot;
3830
+ const cacheKey = buildRepositoryPullRequestRecordCacheKey(repository, pullRequestNumber, "status");
3831
+ const cachedSnapshot = getFreshCacheValue(activeGitHubPullRequestStatusSnapshotCache, cacheKey);
3832
+ if (cachedSnapshot) {
3833
+ pullRequestStatusCache.set(pullRequestNumber, cachedSnapshot);
3834
+ return cachedSnapshot;
3835
+ }
3836
+ if (options?.reviewThreadSummary && options.ciState) {
3837
+ const snapshot = cacheGitHubPullRequestStatusSnapshot(repository, {
3838
+ number: pullRequestNumber,
3839
+ hasUnresolvedReviewThreads: options.reviewThreadSummary.unresolvedReviewThreads > 0,
3840
+ ciState: options.ciState
3841
+ });
3842
+ pullRequestStatusCache.set(pullRequestNumber, snapshot);
3843
+ return snapshot;
3844
+ }
3845
+ const inFlightSnapshot = activeGitHubPullRequestStatusSnapshotPromiseCache.get(cacheKey);
3846
+ if (inFlightSnapshot) {
3847
+ const snapshot = await inFlightSnapshot;
3848
+ pullRequestStatusCache.set(pullRequestNumber, snapshot);
3849
+ return snapshot;
3850
+ }
3851
+ const loadSnapshotPromise = (async () => {
3852
+ const [reviewThreadSummary, ciState] = await Promise.all([
3853
+ options?.reviewThreadSummary ?? getOrLoadCachedGitHubPullRequestReviewThreadSummary(octokit, repository, pullRequestNumber),
3854
+ options?.ciState ?? getGitHubPullRequestCiState(octokit, repository, pullRequestNumber)
3855
+ ]);
3856
+ return cacheGitHubPullRequestStatusSnapshot(repository, {
3857
+ number: pullRequestNumber,
3858
+ hasUnresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads > 0,
3859
+ ciState
3860
+ });
3861
+ })();
3862
+ activeGitHubPullRequestStatusSnapshotPromiseCache.set(cacheKey, loadSnapshotPromise);
3863
+ try {
3864
+ const snapshot = await loadSnapshotPromise;
3865
+ pullRequestStatusCache.set(pullRequestNumber, snapshot);
3866
+ return snapshot;
3867
+ } finally {
3868
+ if (activeGitHubPullRequestStatusSnapshotPromiseCache.get(cacheKey) === loadSnapshotPromise) {
3869
+ activeGitHubPullRequestStatusSnapshotPromiseCache.delete(cacheKey);
3870
+ }
3871
+ }
2937
3872
  }
2938
3873
  async function getGitHubIssueStatusSnapshot(octokit, repository, issueNumber, githubIssue, linkedPullRequestsByIssueNumber, issueStatusSnapshotCache, pullRequestStatusCache) {
2939
3874
  if (issueStatusSnapshotCache.has(issueNumber)) {
@@ -3340,6 +4275,33 @@ function normalizeGitHubIssueLinkEntityData(value) {
3340
4275
  syncedAt
3341
4276
  };
3342
4277
  }
4278
+ function normalizeGitHubPullRequestLinkEntityData(value) {
4279
+ if (!value || typeof value !== "object") {
4280
+ return null;
4281
+ }
4282
+ const record = value;
4283
+ const repositoryUrl = typeof record.repositoryUrl === "string" && record.repositoryUrl.trim() ? getNormalizedMappingRepositoryUrl({
4284
+ repositoryUrl: record.repositoryUrl
4285
+ }) : void 0;
4286
+ const githubPullRequestNumber = typeof record.githubPullRequestNumber === "number" && record.githubPullRequestNumber > 0 ? Math.floor(record.githubPullRequestNumber) : void 0;
4287
+ const githubPullRequestUrl = typeof record.githubPullRequestUrl === "string" && record.githubPullRequestUrl.trim() ? record.githubPullRequestUrl.trim() : void 0;
4288
+ const githubPullRequestState = record.githubPullRequestState === "closed" ? "closed" : record.githubPullRequestState === "open" ? "open" : void 0;
4289
+ const title = typeof record.title === "string" && record.title.trim() ? record.title.trim() : void 0;
4290
+ const syncedAt = typeof record.syncedAt === "string" && record.syncedAt.trim() ? record.syncedAt.trim() : void 0;
4291
+ if (!repositoryUrl || githubPullRequestNumber === void 0 || !githubPullRequestUrl || !githubPullRequestState || !syncedAt) {
4292
+ return null;
4293
+ }
4294
+ return {
4295
+ ...typeof record.companyId === "string" && record.companyId.trim() ? { companyId: record.companyId.trim() } : {},
4296
+ ...typeof record.paperclipProjectId === "string" && record.paperclipProjectId.trim() ? { paperclipProjectId: record.paperclipProjectId.trim() } : {},
4297
+ repositoryUrl,
4298
+ githubPullRequestNumber,
4299
+ githubPullRequestUrl,
4300
+ githubPullRequestState,
4301
+ ...title ? { title } : {},
4302
+ syncedAt
4303
+ };
4304
+ }
3343
4305
  function normalizeStoredStatusTransitionCommentAnnotation(value) {
3344
4306
  if (!value || typeof value !== "object") {
3345
4307
  return null;
@@ -3414,34 +4376,80 @@ async function listGitHubIssueLinkRecords(ctx, query = {}) {
3414
4376
  }
3415
4377
  return records;
3416
4378
  }
3417
- async function findStoredStatusTransitionCommentAnnotation(ctx, params) {
3418
- const issueId = params.issueId.trim();
3419
- const commentId = params.commentId.trim();
4379
+ async function listGitHubPullRequestLinkRecords(ctx, query = {}) {
4380
+ const records = [];
4381
+ const requestedIssueId = query.paperclipIssueId?.trim() || void 0;
4382
+ const requestedExternalId = query.externalId?.trim() || void 0;
3420
4383
  for (let offset = 0; ; ) {
3421
4384
  const page = await ctx.entities.list({
3422
- entityType: COMMENT_ANNOTATION_ENTITY_TYPE,
4385
+ entityType: PULL_REQUEST_LINK_ENTITY_TYPE,
3423
4386
  scopeKind: "issue",
3424
- scopeId: issueId,
3425
- externalId: commentId,
4387
+ ...requestedIssueId ? { scopeId: requestedIssueId } : {},
4388
+ ...requestedExternalId ? { externalId: requestedExternalId } : {},
3426
4389
  limit: PAPERCLIP_LABEL_PAGE_SIZE,
3427
4390
  offset
3428
4391
  });
3429
4392
  if (page.length === 0) {
3430
4393
  break;
3431
4394
  }
3432
- const match = page.find((entry) => {
3433
- if (entry.scopeKind !== "issue" || entry.scopeId !== issueId) {
3434
- return false;
4395
+ for (const entry of page) {
4396
+ if (entry.scopeKind !== "issue" || !entry.scopeId) {
4397
+ continue;
3435
4398
  }
3436
- const externalId = "externalId" in entry && typeof entry.externalId === "string" ? entry.externalId : void 0;
3437
- return externalId === commentId;
3438
- });
3439
- const annotation = match ? normalizeStoredStatusTransitionCommentAnnotation(match.data) : null;
3440
- if (annotation) {
3441
- return annotation;
3442
- }
3443
- if (page.length < PAPERCLIP_LABEL_PAGE_SIZE) {
3444
- break;
4399
+ if (requestedIssueId && entry.scopeId !== requestedIssueId) {
4400
+ continue;
4401
+ }
4402
+ if (requestedExternalId && entry.externalId !== requestedExternalId) {
4403
+ continue;
4404
+ }
4405
+ const data = normalizeGitHubPullRequestLinkEntityData(entry.data);
4406
+ if (!data) {
4407
+ continue;
4408
+ }
4409
+ records.push({
4410
+ paperclipIssueId: entry.scopeId,
4411
+ ...typeof entry.createdAt === "string" ? { createdAt: entry.createdAt } : {},
4412
+ ...typeof entry.updatedAt === "string" ? { updatedAt: entry.updatedAt } : {},
4413
+ ...typeof entry.title === "string" && entry.title.trim() ? { title: entry.title.trim() } : {},
4414
+ ...typeof entry.status === "string" && entry.status.trim() ? { status: entry.status.trim() } : {},
4415
+ data
4416
+ });
4417
+ }
4418
+ if (page.length < PAPERCLIP_LABEL_PAGE_SIZE || requestedIssueId && records.length > 0 || requestedExternalId && page.length < PAPERCLIP_LABEL_PAGE_SIZE) {
4419
+ break;
4420
+ }
4421
+ offset += page.length;
4422
+ }
4423
+ return records;
4424
+ }
4425
+ async function findStoredStatusTransitionCommentAnnotation(ctx, params) {
4426
+ const issueId = params.issueId.trim();
4427
+ const commentId = params.commentId.trim();
4428
+ for (let offset = 0; ; ) {
4429
+ const page = await ctx.entities.list({
4430
+ entityType: COMMENT_ANNOTATION_ENTITY_TYPE,
4431
+ scopeKind: "issue",
4432
+ scopeId: issueId,
4433
+ externalId: commentId,
4434
+ limit: PAPERCLIP_LABEL_PAGE_SIZE,
4435
+ offset
4436
+ });
4437
+ if (page.length === 0) {
4438
+ break;
4439
+ }
4440
+ const match = page.find((entry) => {
4441
+ if (entry.scopeKind !== "issue" || entry.scopeId !== issueId) {
4442
+ return false;
4443
+ }
4444
+ const externalId = "externalId" in entry && typeof entry.externalId === "string" ? entry.externalId : void 0;
4445
+ return externalId === commentId;
4446
+ });
4447
+ const annotation = match ? normalizeStoredStatusTransitionCommentAnnotation(match.data) : null;
4448
+ if (annotation) {
4449
+ return annotation;
4450
+ }
4451
+ if (page.length < PAPERCLIP_LABEL_PAGE_SIZE) {
4452
+ break;
3445
4453
  }
3446
4454
  offset += page.length;
3447
4455
  }
@@ -3472,6 +4480,28 @@ async function upsertGitHubIssueLinkRecord(ctx, mapping, issueId, githubIssue, l
3472
4480
  }
3473
4481
  });
3474
4482
  }
4483
+ async function upsertGitHubPullRequestLinkRecord(ctx, params) {
4484
+ await ctx.entities.upsert({
4485
+ entityType: PULL_REQUEST_LINK_ENTITY_TYPE,
4486
+ scopeKind: "issue",
4487
+ scopeId: params.issueId,
4488
+ externalId: params.pullRequestUrl,
4489
+ title: `GitHub pull request #${params.pullRequestNumber}`,
4490
+ status: params.pullRequestState,
4491
+ data: {
4492
+ companyId: params.companyId,
4493
+ paperclipProjectId: params.projectId,
4494
+ repositoryUrl: getNormalizedMappingRepositoryUrl({
4495
+ repositoryUrl: params.repositoryUrl
4496
+ }),
4497
+ githubPullRequestNumber: params.pullRequestNumber,
4498
+ githubPullRequestUrl: params.pullRequestUrl,
4499
+ githubPullRequestState: params.pullRequestState,
4500
+ title: params.pullRequestTitle,
4501
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString()
4502
+ }
4503
+ });
4504
+ }
3475
4505
  async function upsertStatusTransitionCommentAnnotation(ctx, params) {
3476
4506
  const { issueId, commentId, annotation } = params;
3477
4507
  await ctx.entities.upsert({
@@ -4412,7 +5442,13 @@ async function listRepositoryIssuesForImport(allIssues) {
4412
5442
  return sortIssuesForImport(allIssues.filter((issue) => issue.state === "open"));
4413
5443
  }
4414
5444
  function shouldIgnoreGitHubIssue(issue, advancedSettings) {
4415
- return Boolean(issue.authorLogin && advancedSettings.ignoredIssueAuthorUsernames.includes(issue.authorLogin));
5445
+ if (!issue.authorLogin || advancedSettings.ignoredIssueAuthorUsernames.length === 0) {
5446
+ return false;
5447
+ }
5448
+ const issueAuthorAliases = new Set(buildGitHubUsernameAliases(issue.authorLogin));
5449
+ return advancedSettings.ignoredIssueAuthorUsernames.some(
5450
+ (ignoredUsername) => buildGitHubUsernameAliases(ignoredUsername).some((alias) => issueAuthorAliases.has(alias))
5451
+ );
4416
5452
  }
4417
5453
  async function applyDefaultAssigneeToPaperclipIssue(ctx, params) {
4418
5454
  const { companyId, issueId, defaultAssigneeAgentId } = params;
@@ -4965,190 +6001,2495 @@ function assertExplicitRepositoryMatchesLinkedRepository(explicitRepositoryInput
4965
6001
  function sanitizeRepositoryScopedSearchQuery(query) {
4966
6002
  return query.replace(/(^|\s)(?:repo|org|user):(?:"[^"]+"|\S+)/gi, " ").replace(/\s+/g, " ").trim();
4967
6003
  }
4968
- function buildToolSuccessResult(content, data) {
6004
+ function buildToolSuccessResult(content, data) {
6005
+ return {
6006
+ content,
6007
+ data
6008
+ };
6009
+ }
6010
+ function buildToolErrorResult(error) {
6011
+ const rateLimitPause = getGitHubRateLimitPauseDetails(error);
6012
+ if (rateLimitPause) {
6013
+ const resourceLabel = formatGitHubRateLimitResource(rateLimitPause.resource) ?? "GitHub API";
6014
+ return {
6015
+ error: `${resourceLabel} rate limit reached. Wait until ${formatUtcTimestamp(rateLimitPause.resetAt)} before retrying.`
6016
+ };
6017
+ }
6018
+ return {
6019
+ error: getErrorMessage(error)
6020
+ };
6021
+ }
6022
+ async function executeGitHubTool(fn) {
6023
+ try {
6024
+ return await fn();
6025
+ } catch (error) {
6026
+ return buildToolErrorResult(error);
6027
+ }
6028
+ }
6029
+ async function createGitHubToolOctokit(ctx) {
6030
+ const token = (await resolveGithubToken(ctx)).trim();
6031
+ if (!token) {
6032
+ throw new Error(MISSING_GITHUB_TOKEN_SYNC_MESSAGE);
6033
+ }
6034
+ return new Octokit({ auth: token });
6035
+ }
6036
+ async function listGitHubRepositoryOpenPullRequestNumbers(octokit, repository) {
6037
+ const response = await octokit.rest.pulls.list({
6038
+ owner: repository.owner,
6039
+ repo: repository.repo,
6040
+ state: "open",
6041
+ per_page: 1,
6042
+ headers: {
6043
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
6044
+ }
6045
+ });
6046
+ return (response.data ?? []).map((pullRequest) => typeof pullRequest.number === "number" && pullRequest.number > 0 ? Math.floor(pullRequest.number) : null).filter((number) => number !== null);
6047
+ }
6048
+ async function probeGitHubRepositoryTokenCapability(fn, options = {}) {
6049
+ try {
6050
+ await fn();
6051
+ return "granted";
6052
+ } catch (error) {
6053
+ return classifyGitHubCapabilityProbeError(error, options);
6054
+ }
6055
+ }
6056
+ async function loadGitHubRepositoryTokenCapabilityAudit(octokit, repository, options) {
6057
+ try {
6058
+ await octokit.rest.repos.get({
6059
+ owner: repository.owner,
6060
+ repo: repository.repo,
6061
+ headers: {
6062
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
6063
+ }
6064
+ });
6065
+ } catch (error) {
6066
+ const status = getErrorStatus(error);
6067
+ return buildGitHubRepositoryTokenCapabilityAudit({
6068
+ repository,
6069
+ canComment: false,
6070
+ canReview: false,
6071
+ canClose: false,
6072
+ canUpdateBranch: false,
6073
+ canMerge: false,
6074
+ canRerunCi: false,
6075
+ missingPermissions: status === 401 || status === 403 || status === 404 ? [
6076
+ "Metadata: read",
6077
+ "Issues: write or Pull requests: write",
6078
+ "Pull requests: write",
6079
+ "Contents: write",
6080
+ "Checks: write"
6081
+ ] : [],
6082
+ warnings: [
6083
+ status === 401 || status === 403 || status === 404 ? `GitHub Sync could not confirm repository access for ${formatRepositoryLabel(repository)} with the configured token.` : `GitHub Sync could not verify repository permissions for ${formatRepositoryLabel(repository)} because GitHub returned an unexpected error.`
6084
+ ]
6085
+ });
6086
+ }
6087
+ let samplePullRequestNumber = typeof options?.samplePullRequestNumber === "number" && options.samplePullRequestNumber > 0 ? Math.floor(options.samplePullRequestNumber) : void 0;
6088
+ if (!samplePullRequestNumber) {
6089
+ try {
6090
+ samplePullRequestNumber = (await listGitHubRepositoryOpenPullRequestNumbers(octokit, repository))[0];
6091
+ } catch {
6092
+ return buildGitHubRepositoryTokenCapabilityAudit({
6093
+ repository,
6094
+ canComment: false,
6095
+ canReview: false,
6096
+ canClose: false,
6097
+ canUpdateBranch: false,
6098
+ canMerge: false,
6099
+ canRerunCi: false,
6100
+ missingPermissions: [],
6101
+ warnings: [
6102
+ `GitHub Sync could not verify write permissions for ${formatRepositoryLabel(repository)} because GitHub did not return a sample pull request.`
6103
+ ]
6104
+ });
6105
+ }
6106
+ }
6107
+ if (!samplePullRequestNumber) {
6108
+ return buildGitHubRepositoryTokenCapabilityAudit({
6109
+ repository,
6110
+ canComment: false,
6111
+ canReview: false,
6112
+ canClose: false,
6113
+ canUpdateBranch: false,
6114
+ canMerge: false,
6115
+ canRerunCi: false,
6116
+ missingPermissions: [],
6117
+ warnings: [
6118
+ `GitHub Sync could not verify write permissions for ${formatRepositoryLabel(repository)} because the repository has no open pull requests to probe safely.`
6119
+ ]
6120
+ });
6121
+ }
6122
+ const impossibleSha = "0000000000000000000000000000000000000000";
6123
+ const [
6124
+ commentProbe,
6125
+ reviewProbe,
6126
+ closeProbe,
6127
+ updateBranchProbe,
6128
+ mergeProbe,
6129
+ rerunCiProbe
6130
+ ] = await Promise.all([
6131
+ probeGitHubRepositoryTokenCapability(
6132
+ () => octokit.rest.issues.createComment({
6133
+ owner: repository.owner,
6134
+ repo: repository.repo,
6135
+ issue_number: samplePullRequestNumber,
6136
+ body: "",
6137
+ headers: {
6138
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
6139
+ }
6140
+ }),
6141
+ { grantedStatuses: [422] }
6142
+ ),
6143
+ probeGitHubRepositoryTokenCapability(
6144
+ () => octokit.rest.pulls.createReview({
6145
+ owner: repository.owner,
6146
+ repo: repository.repo,
6147
+ pull_number: samplePullRequestNumber,
6148
+ event: "REQUEST_CHANGES",
6149
+ headers: {
6150
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
6151
+ }
6152
+ }),
6153
+ { grantedStatuses: [422] }
6154
+ ),
6155
+ probeGitHubRepositoryTokenCapability(
6156
+ () => octokit.request("PATCH /repos/{owner}/{repo}/pulls/{pull_number}", {
6157
+ owner: repository.owner,
6158
+ repo: repository.repo,
6159
+ pull_number: samplePullRequestNumber,
6160
+ state: "__paperclip_invalid_state__",
6161
+ headers: {
6162
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
6163
+ }
6164
+ }),
6165
+ { grantedStatuses: [422] }
6166
+ ),
6167
+ probeGitHubRepositoryTokenCapability(
6168
+ () => octokit.request("PUT /repos/{owner}/{repo}/pulls/{pull_number}/update-branch", {
6169
+ owner: repository.owner,
6170
+ repo: repository.repo,
6171
+ pull_number: samplePullRequestNumber,
6172
+ expected_head_sha: impossibleSha,
6173
+ headers: {
6174
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
6175
+ }
6176
+ }),
6177
+ { grantedStatuses: [409, 422] }
6178
+ ),
6179
+ probeGitHubRepositoryTokenCapability(
6180
+ () => octokit.rest.pulls.merge({
6181
+ owner: repository.owner,
6182
+ repo: repository.repo,
6183
+ pull_number: samplePullRequestNumber,
6184
+ sha: impossibleSha,
6185
+ headers: {
6186
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
6187
+ }
6188
+ }),
6189
+ { grantedStatuses: [405, 409, 422] }
6190
+ ),
6191
+ probeGitHubRepositoryTokenCapability(
6192
+ () => octokit.rest.checks.rerequestSuite({
6193
+ owner: repository.owner,
6194
+ repo: repository.repo,
6195
+ check_suite_id: 0,
6196
+ headers: {
6197
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
6198
+ }
6199
+ }),
6200
+ { allowNotFoundAsGranted: true }
6201
+ )
6202
+ ]);
6203
+ const missingPermissions = [
6204
+ ...commentProbe === "missing" ? [getGitHubCapabilityMissingPermissionLabel("comment")] : [],
6205
+ ...reviewProbe === "missing" ? [getGitHubCapabilityMissingPermissionLabel("review")] : [],
6206
+ ...closeProbe === "missing" ? [getGitHubCapabilityMissingPermissionLabel("close")] : [],
6207
+ ...updateBranchProbe === "missing" ? [getGitHubCapabilityMissingPermissionLabel("update_branch")] : [],
6208
+ ...mergeProbe === "missing" ? [getGitHubCapabilityMissingPermissionLabel("merge")] : [],
6209
+ ...rerunCiProbe === "missing" ? [getGitHubCapabilityMissingPermissionLabel("rerun_ci")] : []
6210
+ ];
6211
+ const warnings = [
6212
+ ...commentProbe === "unknown" ? ["GitHub Sync could not verify comment permissions."] : [],
6213
+ ...reviewProbe === "unknown" ? ["GitHub Sync could not verify review permissions."] : [],
6214
+ ...closeProbe === "unknown" ? ["GitHub Sync could not verify close permissions."] : [],
6215
+ ...updateBranchProbe === "unknown" ? ["GitHub Sync could not verify update-branch permissions."] : [],
6216
+ ...mergeProbe === "unknown" ? ["GitHub Sync could not verify merge permissions."] : [],
6217
+ ...rerunCiProbe === "unknown" ? ["GitHub Sync could not verify re-run CI permissions."] : []
6218
+ ];
6219
+ return buildGitHubRepositoryTokenCapabilityAudit({
6220
+ repository,
6221
+ samplePullRequestNumber,
6222
+ canComment: commentProbe === "granted",
6223
+ canReview: reviewProbe === "granted",
6224
+ canClose: closeProbe === "granted",
6225
+ canUpdateBranch: updateBranchProbe === "granted",
6226
+ canMerge: mergeProbe === "granted",
6227
+ canRerunCi: rerunCiProbe === "granted",
6228
+ missingPermissions,
6229
+ warnings
6230
+ });
6231
+ }
6232
+ async function getOrLoadGitHubRepositoryTokenCapabilityAudit(octokit, repository, options) {
6233
+ const cacheKey = buildGitHubRepositoryTokenCapabilityAuditCacheKey(repository, options?.samplePullRequestNumber);
6234
+ const cachedAudit = getFreshCacheValue(activeGitHubRepositoryTokenCapabilityAuditCache, cacheKey);
6235
+ if (cachedAudit) {
6236
+ return cachedAudit;
6237
+ }
6238
+ const inFlightAudit = activeGitHubRepositoryTokenCapabilityAuditPromiseCache.get(cacheKey);
6239
+ if (inFlightAudit) {
6240
+ return inFlightAudit;
6241
+ }
6242
+ const loadAuditPromise = (async () => {
6243
+ const audit = await loadGitHubRepositoryTokenCapabilityAudit(octokit, repository, options);
6244
+ return setCacheValue(
6245
+ activeGitHubRepositoryTokenCapabilityAuditCache,
6246
+ cacheKey,
6247
+ audit,
6248
+ GITHUB_TOKEN_PERMISSION_AUDIT_CACHE_TTL_MS
6249
+ );
6250
+ })();
6251
+ activeGitHubRepositoryTokenCapabilityAuditPromiseCache.set(cacheKey, loadAuditPromise);
6252
+ try {
6253
+ return await loadAuditPromise;
6254
+ } finally {
6255
+ if (activeGitHubRepositoryTokenCapabilityAuditPromiseCache.get(cacheKey) === loadAuditPromise) {
6256
+ activeGitHubRepositoryTokenCapabilityAuditPromiseCache.delete(cacheKey);
6257
+ }
6258
+ }
6259
+ }
6260
+ async function resolveRepositoryFromRunContext(ctx, runCtx) {
6261
+ const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
6262
+ const mappings = getSyncableMappingsForTarget(settings.mappings, {
6263
+ kind: "project",
6264
+ companyId: runCtx.companyId,
6265
+ projectId: runCtx.projectId,
6266
+ displayLabel: "project"
6267
+ });
6268
+ const repositories = [
6269
+ ...new Map(
6270
+ mappings.map((mapping) => {
6271
+ const repository = parseRepositoryReference(mapping.repositoryUrl);
6272
+ return repository ? [repository.url, repository] : null;
6273
+ }).filter((entry) => entry !== null)
6274
+ ).values()
6275
+ ];
6276
+ if (repositories.length === 1) {
6277
+ return repositories[0];
6278
+ }
6279
+ if (repositories.length === 0) {
6280
+ throw new Error("No GitHub repository is mapped to the current Paperclip project. Pass repository explicitly.");
6281
+ }
6282
+ throw new Error("Multiple GitHub repositories are mapped to the current Paperclip project. Pass repository explicitly.");
6283
+ }
6284
+ async function resolveGitHubToolRepository(ctx, runCtx, input) {
6285
+ const explicitRepository = normalizeOptionalToolString(input.repository);
6286
+ if (explicitRepository) {
6287
+ return requireRepositoryReference(explicitRepository);
6288
+ }
6289
+ const paperclipIssueId = normalizeOptionalToolString(input.paperclipIssueId);
6290
+ if (paperclipIssueId) {
6291
+ const link = await resolvePaperclipIssueGitHubLink(ctx, paperclipIssueId, runCtx.companyId);
6292
+ if (!link) {
6293
+ throw new Error("This Paperclip issue is not linked to a GitHub issue yet. Pass repository explicitly.");
6294
+ }
6295
+ return requireRepositoryReference(link.repositoryUrl);
6296
+ }
6297
+ return resolveRepositoryFromRunContext(ctx, runCtx);
6298
+ }
6299
+ async function resolveGitHubIssueToolTarget(ctx, runCtx, input) {
6300
+ const paperclipIssueId = normalizeOptionalToolString(input.paperclipIssueId);
6301
+ if (paperclipIssueId) {
6302
+ const link = await resolvePaperclipIssueGitHubLink(ctx, paperclipIssueId, runCtx.companyId);
6303
+ if (!link) {
6304
+ throw new Error("This Paperclip issue is not linked to a GitHub issue yet.");
6305
+ }
6306
+ const repository2 = assertExplicitRepositoryMatchesLinkedRepository(
6307
+ input.repository,
6308
+ link.repositoryUrl,
6309
+ "The provided repository does not match the linked GitHub repository for this Paperclip issue."
6310
+ );
6311
+ const explicitIssueNumber = normalizeToolPositiveInteger(input.issueNumber);
6312
+ if (explicitIssueNumber !== void 0 && explicitIssueNumber !== link.githubIssueNumber) {
6313
+ throw new Error("The provided issue number does not match the linked GitHub issue for this Paperclip issue.");
6314
+ }
6315
+ return {
6316
+ repository: repository2,
6317
+ issueNumber: link.githubIssueNumber,
6318
+ paperclipIssueId,
6319
+ githubIssueId: link.githubIssueId,
6320
+ githubIssueUrl: link.githubIssueUrl
6321
+ };
6322
+ }
6323
+ const repository = await resolveGitHubToolRepository(ctx, runCtx, input);
6324
+ const issueNumber = normalizeToolPositiveInteger(input.issueNumber);
6325
+ if (issueNumber === void 0) {
6326
+ throw new Error("issueNumber is required when paperclipIssueId is not provided.");
6327
+ }
6328
+ return {
6329
+ repository,
6330
+ issueNumber
6331
+ };
6332
+ }
6333
+ async function resolveGitHubPullRequestToolTarget(ctx, runCtx, input) {
6334
+ const paperclipIssueId = normalizeOptionalToolString(input.paperclipIssueId);
6335
+ if (paperclipIssueId) {
6336
+ const link = await resolvePaperclipIssueGitHubLink(ctx, paperclipIssueId, runCtx.companyId);
6337
+ if (!link) {
6338
+ throw new Error("This Paperclip issue is not linked to GitHub yet.");
6339
+ }
6340
+ const repository2 = assertExplicitRepositoryMatchesLinkedRepository(
6341
+ input.repository,
6342
+ link.repositoryUrl,
6343
+ "repository must match the GitHub repository linked to the provided Paperclip issue."
6344
+ );
6345
+ const explicitPullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
6346
+ if (explicitPullRequestNumber !== void 0) {
6347
+ return {
6348
+ repository: repository2,
6349
+ pullRequestNumber: explicitPullRequestNumber,
6350
+ paperclipIssueId
6351
+ };
6352
+ }
6353
+ if (link.linkedPullRequestNumbers.length === 1) {
6354
+ return {
6355
+ repository: repository2,
6356
+ pullRequestNumber: link.linkedPullRequestNumbers[0],
6357
+ paperclipIssueId
6358
+ };
6359
+ }
6360
+ throw new Error("pullRequestNumber is required unless the linked Paperclip issue has exactly one linked pull request.");
6361
+ }
6362
+ const repository = await resolveGitHubToolRepository(ctx, runCtx, input);
6363
+ const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
6364
+ if (pullRequestNumber === void 0) {
6365
+ throw new Error("pullRequestNumber is required when paperclipIssueId is not provided.");
6366
+ }
6367
+ return {
6368
+ repository,
6369
+ pullRequestNumber
6370
+ };
6371
+ }
6372
+ function formatAiAuthorshipFooter(llmModel) {
6373
+ return `
6374
+
6375
+ ---
6376
+ ${AI_AUTHORED_COMMENT_FOOTER_PREFIX}${llmModel.trim()}.`;
6377
+ }
6378
+ function appendAiAuthorshipFooter(body, llmModel) {
6379
+ const trimmedBody = body.trim();
6380
+ if (!trimmedBody) {
6381
+ throw new Error("Comment body cannot be empty.");
6382
+ }
6383
+ const trimmedModel = llmModel.trim();
6384
+ if (!trimmedModel) {
6385
+ throw new Error("llmModel is required when posting a GitHub comment.");
6386
+ }
6387
+ return `${trimmedBody}${formatAiAuthorshipFooter(trimmedModel)}`;
6388
+ }
6389
+ async function listAllGitHubIssueComments(octokit, repository, issueNumber) {
6390
+ const comments = [];
6391
+ for await (const response of octokit.paginate.iterator(octokit.rest.issues.listComments, {
6392
+ owner: repository.owner,
6393
+ repo: repository.repo,
6394
+ issue_number: issueNumber,
6395
+ per_page: 100,
6396
+ headers: {
6397
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
6398
+ }
6399
+ })) {
6400
+ for (const comment of response.data) {
6401
+ comments.push({
6402
+ id: comment.id,
6403
+ body: comment.body ?? "",
6404
+ url: comment.html_url ?? void 0,
6405
+ authorLogin: normalizeGitHubUserLogin(comment.user?.login),
6406
+ authorUrl: comment.user?.html_url ?? void 0,
6407
+ authorAvatarUrl: comment.user?.avatar_url ?? void 0,
6408
+ createdAt: comment.created_at ?? void 0,
6409
+ updatedAt: comment.updated_at ?? void 0
6410
+ });
6411
+ }
6412
+ }
6413
+ return comments;
6414
+ }
6415
+ async function listPaperclipIssuesForProject(ctx, companyId, projectId) {
6416
+ const issues = [];
6417
+ for (let offset = 0; ; ) {
6418
+ const page = await ctx.issues.list({
6419
+ companyId,
6420
+ projectId,
6421
+ limit: PAPERCLIP_LABEL_PAGE_SIZE,
6422
+ offset
6423
+ });
6424
+ if (page.length === 0) {
6425
+ break;
6426
+ }
6427
+ issues.push(...page);
6428
+ if (page.length < PAPERCLIP_LABEL_PAGE_SIZE) {
6429
+ break;
6430
+ }
6431
+ offset += page.length;
6432
+ }
6433
+ return issues;
6434
+ }
6435
+ function buildProjectPullRequestPerson(input) {
6436
+ const normalizedLogin = normalizeGitHubUsername(input.login) ?? "unknown";
6437
+ const displayName = typeof input.name === "string" && input.name.trim() ? input.name.trim() : normalizedLogin;
6438
+ const profileUrl = typeof input.url === "string" && input.url.trim() ? input.url.trim() : `https://github.com/${normalizedLogin}`;
6439
+ const avatarUrl = typeof input.avatarUrl === "string" && input.avatarUrl.trim() ? input.avatarUrl.trim() : void 0;
6440
+ return {
6441
+ name: displayName,
6442
+ handle: `@${normalizedLogin}`,
6443
+ profileUrl,
6444
+ ...avatarUrl ? { avatarUrl } : {}
6445
+ };
6446
+ }
6447
+ function normalizeProjectPullRequestLabels(nodes) {
6448
+ if (!Array.isArray(nodes)) {
6449
+ return [];
6450
+ }
6451
+ const seen = /* @__PURE__ */ new Set();
6452
+ const labels = [];
6453
+ for (const entry of nodes) {
6454
+ const name = typeof entry?.name === "string" ? entry.name.trim() : "";
6455
+ if (!name) {
6456
+ continue;
6457
+ }
6458
+ const key = normalizeLabelName(name);
6459
+ if (seen.has(key)) {
6460
+ continue;
6461
+ }
6462
+ seen.add(key);
6463
+ labels.push({
6464
+ name,
6465
+ ...normalizeHexColor(entry?.color) ? { color: normalizeHexColor(entry?.color) } : {}
6466
+ });
6467
+ }
6468
+ return labels;
6469
+ }
6470
+ function summarizeGitHubPullRequestReviewsFromEntries(entries) {
6471
+ const latestStateByReviewer = /* @__PURE__ */ new Map();
6472
+ let anonymousApprovals = 0;
6473
+ let anonymousChangesRequested = 0;
6474
+ for (const entry of entries) {
6475
+ const state = typeof entry.state === "string" ? entry.state.trim().toUpperCase() : "";
6476
+ const authorLogin = normalizeGitHubUsername(entry.authorLogin);
6477
+ if (state === "APPROVED") {
6478
+ if (authorLogin) {
6479
+ latestStateByReviewer.set(authorLogin, "APPROVED");
6480
+ } else {
6481
+ anonymousApprovals += 1;
6482
+ }
6483
+ continue;
6484
+ }
6485
+ if (state === "CHANGES_REQUESTED") {
6486
+ if (authorLogin) {
6487
+ latestStateByReviewer.set(authorLogin, "CHANGES_REQUESTED");
6488
+ } else {
6489
+ anonymousChangesRequested += 1;
6490
+ }
6491
+ continue;
6492
+ }
6493
+ if (state === "DISMISSED" && authorLogin) {
6494
+ latestStateByReviewer.delete(authorLogin);
6495
+ }
6496
+ }
6497
+ let approvals = anonymousApprovals;
6498
+ let changesRequested = anonymousChangesRequested;
6499
+ for (const state of latestStateByReviewer.values()) {
6500
+ if (state === "APPROVED") {
6501
+ approvals += 1;
6502
+ } else if (state === "CHANGES_REQUESTED") {
6503
+ changesRequested += 1;
6504
+ }
6505
+ }
6506
+ return {
6507
+ approvals,
6508
+ changesRequested
6509
+ };
6510
+ }
6511
+ function summarizeGitHubPullRequestReviewNodes(nodes) {
6512
+ const entries = (nodes ?? []).map((node) => ({
6513
+ state: node?.state ?? void 0,
6514
+ authorLogin: node?.author?.login ?? void 0
6515
+ }));
6516
+ return summarizeGitHubPullRequestReviewsFromEntries(entries);
6517
+ }
6518
+ async function listGitHubPullRequestReviewSummary(octokit, repository, pullRequestNumber) {
6519
+ const entries = [];
6520
+ for await (const response of octokit.paginate.iterator(octokit.rest.pulls.listReviews, {
6521
+ owner: repository.owner,
6522
+ repo: repository.repo,
6523
+ pull_number: pullRequestNumber,
6524
+ per_page: 100,
6525
+ headers: {
6526
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
6527
+ }
6528
+ })) {
6529
+ for (const review of response.data) {
6530
+ entries.push({
6531
+ state: review.state ?? void 0,
6532
+ authorLogin: review.user?.login ?? void 0
6533
+ });
6534
+ }
6535
+ }
6536
+ return summarizeGitHubPullRequestReviewsFromEntries(entries);
6537
+ }
6538
+ function buildProjectPullRequestScopeCacheKey(params) {
6539
+ return `${params.companyId}:${params.projectId}:${getNormalizedMappingRepositoryUrl({
6540
+ repositoryUrl: params.repositoryUrl
6541
+ })}`;
6542
+ }
6543
+ function buildRepositoryPullRequestCacheScopeKey(repository) {
6544
+ return `${repository.owner.toLowerCase()}/${repository.repo.toLowerCase()}`;
6545
+ }
6546
+ function buildRepositoryPullRequestCollectionCacheKey(repository, suffix) {
6547
+ return `${buildRepositoryPullRequestCacheScopeKey(repository)}:${suffix}`;
6548
+ }
6549
+ function buildRepositoryPullRequestRecordCacheKey(repository, pullRequestNumber, suffix) {
6550
+ return `${buildRepositoryPullRequestCollectionCacheKey(repository, "pull-request")}:${Math.max(1, Math.floor(pullRequestNumber))}:${suffix}`;
6551
+ }
6552
+ function buildRepositoryPullRequestCompareCacheKey(repository, baseBranch, headBranch, headRepositoryOwner) {
6553
+ const normalizedBaseBranch = baseBranch.trim();
6554
+ const normalizedHeadBranch = headBranch.trim();
6555
+ const normalizedHeadRepositoryOwner = typeof headRepositoryOwner === "string" && headRepositoryOwner.trim() ? headRepositoryOwner.trim() : repository.owner;
6556
+ const compareHeadRef = normalizedHeadRepositoryOwner.toLowerCase() === repository.owner.toLowerCase() ? normalizedHeadBranch : `${normalizedHeadRepositoryOwner}:${normalizedHeadBranch}`;
6557
+ return `${buildRepositoryPullRequestCollectionCacheKey(repository, "compare")}:${encodeURIComponent(`${normalizedBaseBranch}...${compareHeadRef}`)}`;
6558
+ }
6559
+ function buildProjectPullRequestPageCacheKey(scope, filter, pageIndex, cursor) {
6560
+ return `${buildProjectPullRequestScopeCacheKey({
6561
+ companyId: scope.companyId,
6562
+ projectId: scope.projectId,
6563
+ repositoryUrl: scope.repository.url
6564
+ })}:page:${filter}:${pageIndex}:${cursor ?? ""}`;
6565
+ }
6566
+ function buildProjectPullRequestDetailCacheKey(scope, pullRequestNumber) {
6567
+ return `${buildProjectPullRequestScopeCacheKey({
6568
+ companyId: scope.companyId,
6569
+ projectId: scope.projectId,
6570
+ repositoryUrl: scope.repository.url
6571
+ })}:detail:${pullRequestNumber}`;
6572
+ }
6573
+ function buildProjectPullRequestSummaryRecordCacheKey(scope, pullRequestNumber) {
6574
+ return `${buildProjectPullRequestScopeCacheKey({
6575
+ companyId: scope.companyId,
6576
+ projectId: scope.projectId,
6577
+ repositoryUrl: scope.repository.url
6578
+ })}:summary-record:${Math.max(1, Math.floor(pullRequestNumber))}`;
6579
+ }
6580
+ function invalidateProjectPullRequestCaches(scope) {
6581
+ const cacheKeyPrefix = `${buildProjectPullRequestScopeCacheKey({
6582
+ companyId: scope.companyId,
6583
+ projectId: scope.projectId,
6584
+ repositoryUrl: scope.repository.url
6585
+ })}:`;
6586
+ const repositoryCacheKeyPrefix = `${buildRepositoryPullRequestCacheScopeKey(scope.repository)}:`;
6587
+ for (const cache of [
6588
+ activeProjectPullRequestPageCache,
6589
+ activeProjectPullRequestSummaryCache,
6590
+ activeProjectPullRequestSummaryRecordCache,
6591
+ activeProjectPullRequestDetailCache,
6592
+ activeProjectPullRequestIssueLookupCache
6593
+ ]) {
6594
+ for (const key of cache.keys()) {
6595
+ if (key.startsWith(cacheKeyPrefix)) {
6596
+ cache.delete(key);
6597
+ }
6598
+ }
6599
+ }
6600
+ for (const cache of [
6601
+ activeProjectPullRequestCountCache,
6602
+ activeProjectPullRequestMetricsCache,
6603
+ activeGitHubPullRequestBehindCountCache,
6604
+ activeGitHubPullRequestStatusSnapshotCache,
6605
+ activeGitHubPullRequestReviewSummaryCache,
6606
+ activeGitHubPullRequestReviewThreadSummaryCache
6607
+ ]) {
6608
+ for (const key of cache.keys()) {
6609
+ if (key.startsWith(repositoryCacheKeyPrefix)) {
6610
+ cache.delete(key);
6611
+ }
6612
+ }
6613
+ }
6614
+ for (const key of activeProjectPullRequestSummaryPromiseCache.keys()) {
6615
+ if (key.startsWith(cacheKeyPrefix)) {
6616
+ activeProjectPullRequestSummaryPromiseCache.delete(key);
6617
+ }
6618
+ }
6619
+ for (const promiseCache of [
6620
+ activeProjectPullRequestCountPromiseCache,
6621
+ activeProjectPullRequestMetricsPromiseCache,
6622
+ activeGitHubPullRequestBehindCountPromiseCache,
6623
+ activeGitHubPullRequestStatusSnapshotPromiseCache,
6624
+ activeGitHubPullRequestReviewSummaryPromiseCache,
6625
+ activeGitHubPullRequestReviewThreadSummaryPromiseCache
6626
+ ]) {
6627
+ for (const key of promiseCache.keys()) {
6628
+ if (key.startsWith(repositoryCacheKeyPrefix)) {
6629
+ promiseCache.delete(key);
6630
+ }
6631
+ }
6632
+ }
6633
+ }
6634
+ async function getGitHubPullRequestBehindCount(octokit, repository, options) {
6635
+ const baseBranch = typeof options.baseBranch === "string" ? options.baseBranch.trim() : "";
6636
+ const headBranch = typeof options.headBranch === "string" ? options.headBranch.trim() : "";
6637
+ if (!baseBranch || !headBranch) {
6638
+ return null;
6639
+ }
6640
+ const headRepositoryOwner = typeof options.headRepositoryOwner === "string" && options.headRepositoryOwner.trim() ? options.headRepositoryOwner.trim() : repository.owner;
6641
+ const compareHeadRef = headRepositoryOwner.toLowerCase() === repository.owner.toLowerCase() ? headBranch : `${headRepositoryOwner}:${headBranch}`;
6642
+ const cacheKey = buildRepositoryPullRequestCompareCacheKey(repository, baseBranch, headBranch, headRepositoryOwner);
6643
+ const cachedBehindCountEntry = getFreshCacheEntry(activeGitHubPullRequestBehindCountCache, cacheKey);
6644
+ if (cachedBehindCountEntry) {
6645
+ return cachedBehindCountEntry.value;
6646
+ }
6647
+ const inFlightBehindCount = activeGitHubPullRequestBehindCountPromiseCache.get(cacheKey);
6648
+ if (inFlightBehindCount) {
6649
+ return inFlightBehindCount;
6650
+ }
6651
+ const loadBehindCountPromise = (async () => {
6652
+ try {
6653
+ const response = await octokit.request("GET /repos/{owner}/{repo}/compare/{basehead}", {
6654
+ owner: repository.owner,
6655
+ repo: repository.repo,
6656
+ basehead: `${baseBranch}...${compareHeadRef}`
6657
+ });
6658
+ const behindBy = response.data?.behind_by;
6659
+ return setCacheValue(
6660
+ activeGitHubPullRequestBehindCountCache,
6661
+ cacheKey,
6662
+ typeof behindBy === "number" && behindBy >= 0 ? Math.floor(behindBy) : null,
6663
+ PROJECT_PULL_REQUEST_BRANCH_COMPARE_CACHE_TTL_MS
6664
+ );
6665
+ } catch {
6666
+ return setCacheValue(
6667
+ activeGitHubPullRequestBehindCountCache,
6668
+ cacheKey,
6669
+ null,
6670
+ PROJECT_PULL_REQUEST_BRANCH_COMPARE_CACHE_TTL_MS
6671
+ );
6672
+ }
6673
+ })();
6674
+ activeGitHubPullRequestBehindCountPromiseCache.set(cacheKey, loadBehindCountPromise);
6675
+ try {
6676
+ return await loadBehindCountPromise;
6677
+ } finally {
6678
+ if (activeGitHubPullRequestBehindCountPromiseCache.get(cacheKey) === loadBehindCountPromise) {
6679
+ activeGitHubPullRequestBehindCountPromiseCache.delete(cacheKey);
6680
+ }
6681
+ }
6682
+ }
6683
+ function cacheGitHubPullRequestReviewSummary(repository, pullRequestNumber, summary) {
6684
+ return setCacheValue(
6685
+ activeGitHubPullRequestReviewSummaryCache,
6686
+ buildRepositoryPullRequestRecordCacheKey(repository, pullRequestNumber, "review-summary"),
6687
+ summary,
6688
+ PROJECT_PULL_REQUEST_GITHUB_INSIGHT_CACHE_TTL_MS
6689
+ );
6690
+ }
6691
+ function cacheGitHubPullRequestReviewThreadSummary(repository, pullRequestNumber, summary) {
6692
+ return setCacheValue(
6693
+ activeGitHubPullRequestReviewThreadSummaryCache,
6694
+ buildRepositoryPullRequestRecordCacheKey(repository, pullRequestNumber, "review-threads"),
6695
+ summary,
6696
+ PROJECT_PULL_REQUEST_GITHUB_INSIGHT_CACHE_TTL_MS
6697
+ );
6698
+ }
6699
+ function cacheGitHubPullRequestStatusSnapshot(repository, snapshot) {
6700
+ return setCacheValue(
6701
+ activeGitHubPullRequestStatusSnapshotCache,
6702
+ buildRepositoryPullRequestRecordCacheKey(repository, snapshot.number, "status"),
6703
+ snapshot,
6704
+ PROJECT_PULL_REQUEST_GITHUB_INSIGHT_CACHE_TTL_MS
6705
+ );
6706
+ }
6707
+ async function getOrLoadCachedGitHubPullRequestReviewSummary(octokit, repository, pullRequestNumber, inlineSummary) {
6708
+ const cacheKey = buildRepositoryPullRequestRecordCacheKey(repository, pullRequestNumber, "review-summary");
6709
+ const cachedSummary = getFreshCacheValue(activeGitHubPullRequestReviewSummaryCache, cacheKey);
6710
+ if (cachedSummary) {
6711
+ return cachedSummary;
6712
+ }
6713
+ if (inlineSummary) {
6714
+ return cacheGitHubPullRequestReviewSummary(repository, pullRequestNumber, inlineSummary);
6715
+ }
6716
+ const inFlightSummary = activeGitHubPullRequestReviewSummaryPromiseCache.get(cacheKey);
6717
+ if (inFlightSummary) {
6718
+ return inFlightSummary;
6719
+ }
6720
+ const loadSummaryPromise = (async () => cacheGitHubPullRequestReviewSummary(
6721
+ repository,
6722
+ pullRequestNumber,
6723
+ await listGitHubPullRequestReviewSummary(octokit, repository, pullRequestNumber)
6724
+ ))();
6725
+ activeGitHubPullRequestReviewSummaryPromiseCache.set(cacheKey, loadSummaryPromise);
6726
+ try {
6727
+ return await loadSummaryPromise;
6728
+ } finally {
6729
+ if (activeGitHubPullRequestReviewSummaryPromiseCache.get(cacheKey) === loadSummaryPromise) {
6730
+ activeGitHubPullRequestReviewSummaryPromiseCache.delete(cacheKey);
6731
+ }
6732
+ }
6733
+ }
6734
+ async function getOrLoadCachedGitHubPullRequestReviewThreadSummary(octokit, repository, pullRequestNumber, inlineSummary) {
6735
+ const cacheKey = buildRepositoryPullRequestRecordCacheKey(repository, pullRequestNumber, "review-threads");
6736
+ const cachedSummary = getFreshCacheValue(activeGitHubPullRequestReviewThreadSummaryCache, cacheKey);
6737
+ if (cachedSummary) {
6738
+ return cachedSummary;
6739
+ }
6740
+ if (inlineSummary) {
6741
+ return cacheGitHubPullRequestReviewThreadSummary(repository, pullRequestNumber, inlineSummary);
6742
+ }
6743
+ const inFlightSummary = activeGitHubPullRequestReviewThreadSummaryPromiseCache.get(cacheKey);
6744
+ if (inFlightSummary) {
6745
+ return inFlightSummary;
6746
+ }
6747
+ const loadSummaryPromise = (async () => cacheGitHubPullRequestReviewThreadSummary(
6748
+ repository,
6749
+ pullRequestNumber,
6750
+ summarizeDetailedPullRequestReviewThreads(
6751
+ await listDetailedPullRequestReviewThreads(octokit, repository, pullRequestNumber)
6752
+ )
6753
+ ))();
6754
+ activeGitHubPullRequestReviewThreadSummaryPromiseCache.set(cacheKey, loadSummaryPromise);
6755
+ try {
6756
+ return await loadSummaryPromise;
6757
+ } finally {
6758
+ if (activeGitHubPullRequestReviewThreadSummaryPromiseCache.get(cacheKey) === loadSummaryPromise) {
6759
+ activeGitHubPullRequestReviewThreadSummaryPromiseCache.delete(cacheKey);
6760
+ }
6761
+ }
6762
+ }
6763
+ function getProjectPullRequestNumbersForFilter(metrics, filter) {
6764
+ switch (filter) {
6765
+ case "mergeable":
6766
+ return metrics.mergeablePullRequestNumbers;
6767
+ case "reviewable":
6768
+ return metrics.reviewablePullRequestNumbers;
6769
+ case "failing":
6770
+ return metrics.failingPullRequestNumbers;
6771
+ }
6772
+ }
6773
+ function cacheProjectPullRequestSummaryRecords(scope, pullRequests, ttlMs = PROJECT_PULL_REQUEST_SUMMARY_CACHE_TTL_MS) {
6774
+ const now = Date.now();
6775
+ for (const pullRequest of pullRequests) {
6776
+ const pullRequestNumber = getProjectPullRequestNumber(pullRequest);
6777
+ if (pullRequestNumber === null) {
6778
+ continue;
6779
+ }
6780
+ setCacheValue(
6781
+ activeProjectPullRequestSummaryRecordCache,
6782
+ buildProjectPullRequestSummaryRecordCacheKey(scope, pullRequestNumber),
6783
+ pullRequest,
6784
+ ttlMs,
6785
+ now
6786
+ );
6787
+ }
6788
+ }
6789
+ function selectImportedPaperclipIssueReference(current, next) {
6790
+ if (!current) {
6791
+ return next;
6792
+ }
6793
+ return compareImportedPaperclipIssueCreatedAt(
6794
+ {
6795
+ id: next.paperclipIssueId,
6796
+ createdAt: next.createdAt
6797
+ },
6798
+ {
6799
+ id: current.paperclipIssueId,
6800
+ createdAt: current.createdAt
6801
+ }
6802
+ ) < 0 ? next : current;
6803
+ }
6804
+ async function buildProjectPullRequestIssueLookup(ctx, scope) {
6805
+ const cacheKey = `${buildProjectPullRequestScopeCacheKey({
6806
+ companyId: scope.companyId,
6807
+ projectId: scope.projectId,
6808
+ repositoryUrl: scope.repository.url
6809
+ })}:issue-lookup`;
6810
+ const cachedLookup = getFreshCacheValue(activeProjectPullRequestIssueLookupCache, cacheKey);
6811
+ if (cachedLookup) {
6812
+ return cachedLookup;
6813
+ }
6814
+ const [projectIssues, issueLinks, pullRequestLinks] = await Promise.all([
6815
+ listPaperclipIssuesForProject(ctx, scope.companyId, scope.projectId),
6816
+ listGitHubIssueLinkRecords(ctx),
6817
+ listGitHubPullRequestLinkRecords(ctx)
6818
+ ]);
6819
+ const issuesById = new Map(projectIssues.map((issue) => [issue.id, issue]));
6820
+ const normalizedRepositoryUrl = scope.repository.url;
6821
+ const linkedIssuesByGitHubIssueUrl = /* @__PURE__ */ new Map();
6822
+ for (const record of issueLinks) {
6823
+ if (record.data.repositoryUrl !== normalizedRepositoryUrl) {
6824
+ continue;
6825
+ }
6826
+ if (record.data.companyId && record.data.companyId !== scope.companyId) {
6827
+ continue;
6828
+ }
6829
+ if (record.data.paperclipProjectId && record.data.paperclipProjectId !== scope.projectId) {
6830
+ continue;
6831
+ }
6832
+ const linkedIssue = issuesById.get(record.paperclipIssueId);
6833
+ linkedIssuesByGitHubIssueUrl.set(
6834
+ record.data.githubIssueUrl,
6835
+ selectImportedPaperclipIssueReference(
6836
+ linkedIssuesByGitHubIssueUrl.get(record.data.githubIssueUrl),
6837
+ {
6838
+ paperclipIssueId: record.paperclipIssueId,
6839
+ ...linkedIssue?.identifier ? { paperclipIssueKey: linkedIssue.identifier } : {},
6840
+ createdAt: record.createdAt
6841
+ }
6842
+ )
6843
+ );
6844
+ }
6845
+ for (const issue of projectIssues) {
6846
+ const githubIssueUrl = extractImportedGitHubIssueUrlFromDescription(issue.description);
6847
+ if (!githubIssueUrl) {
6848
+ continue;
6849
+ }
6850
+ linkedIssuesByGitHubIssueUrl.set(
6851
+ githubIssueUrl,
6852
+ selectImportedPaperclipIssueReference(
6853
+ linkedIssuesByGitHubIssueUrl.get(githubIssueUrl),
6854
+ {
6855
+ paperclipIssueId: issue.id,
6856
+ ...issue.identifier ? { paperclipIssueKey: issue.identifier } : {},
6857
+ createdAt: issue.createdAt
6858
+ }
6859
+ )
6860
+ );
6861
+ }
6862
+ const fallbackIssuesByPullRequestNumber = /* @__PURE__ */ new Map();
6863
+ const sortedLinks = [...pullRequestLinks].sort((left, right) => {
6864
+ const rightTimestamp = Date.parse(right.updatedAt ?? right.createdAt ?? "");
6865
+ const leftTimestamp = Date.parse(left.updatedAt ?? left.createdAt ?? "");
6866
+ const safeRightTimestamp = Number.isFinite(rightTimestamp) ? rightTimestamp : 0;
6867
+ const safeLeftTimestamp = Number.isFinite(leftTimestamp) ? leftTimestamp : 0;
6868
+ return safeRightTimestamp - safeLeftTimestamp;
6869
+ });
6870
+ for (const record of sortedLinks) {
6871
+ if (record.data.repositoryUrl !== normalizedRepositoryUrl) {
6872
+ continue;
6873
+ }
6874
+ if (record.data.companyId && record.data.companyId !== scope.companyId) {
6875
+ continue;
6876
+ }
6877
+ if (record.data.paperclipProjectId && record.data.paperclipProjectId !== scope.projectId) {
6878
+ continue;
6879
+ }
6880
+ if (fallbackIssuesByPullRequestNumber.has(record.data.githubPullRequestNumber)) {
6881
+ continue;
6882
+ }
6883
+ const linkedIssue = issuesById.get(record.paperclipIssueId);
6884
+ fallbackIssuesByPullRequestNumber.set(record.data.githubPullRequestNumber, {
6885
+ paperclipIssueId: record.paperclipIssueId,
6886
+ ...linkedIssue?.identifier ? { paperclipIssueKey: linkedIssue.identifier } : {}
6887
+ });
6888
+ }
6889
+ return setCacheValue(
6890
+ activeProjectPullRequestIssueLookupCache,
6891
+ cacheKey,
6892
+ {
6893
+ linkedIssuesByGitHubIssueUrl: new Map(
6894
+ [...linkedIssuesByGitHubIssueUrl.entries()].map(([githubIssueUrl, value]) => [
6895
+ githubIssueUrl,
6896
+ {
6897
+ paperclipIssueId: value.paperclipIssueId,
6898
+ ...value.paperclipIssueKey ? { paperclipIssueKey: value.paperclipIssueKey } : {}
6899
+ }
6900
+ ])
6901
+ ),
6902
+ fallbackIssuesByPullRequestNumber
6903
+ },
6904
+ PROJECT_PULL_REQUEST_ISSUE_LOOKUP_CACHE_TTL_MS
6905
+ );
6906
+ }
6907
+ function resolveLinkedPaperclipIssueForPullRequest(pullRequestNumber, closingIssues, issueLookup) {
6908
+ for (const closingIssue of closingIssues) {
6909
+ const linkedIssue = issueLookup.linkedIssuesByGitHubIssueUrl.get(closingIssue.url);
6910
+ if (linkedIssue) {
6911
+ return linkedIssue;
6912
+ }
6913
+ }
6914
+ return issueLookup.fallbackIssuesByPullRequestNumber.get(pullRequestNumber);
6915
+ }
6916
+ function getProjectPullRequestStatus(state) {
6917
+ switch (state) {
6918
+ case "MERGED":
6919
+ return "merged";
6920
+ case "CLOSED":
6921
+ return "closed";
6922
+ default:
6923
+ return "open";
6924
+ }
6925
+ }
6926
+ async function buildProjectPullRequestSummaryRecord(octokit, repository, node, issueLookup, pullRequestStatusCache) {
6927
+ if (!node || typeof node.number !== "number" || !node.url || !node.title?.trim()) {
6928
+ return null;
6929
+ }
6930
+ const inlineReviewThreadSummary = node.reviewThreads?.pageInfo?.hasNextPage ? null : summarizeProjectPullRequestReviewThreadsFromConnection(node.reviewThreads);
6931
+ const inlineReviewSummary = node.reviews?.pageInfo?.hasNextPage ? null : summarizeGitHubPullRequestReviewNodes(node.reviews?.nodes);
6932
+ const inlineCiState = tryBuildGitHubPullRequestCiStateFromBatchNode({
6933
+ statusCheckRollup: node.statusCheckRollup
6934
+ });
6935
+ const [reviewThreadSummary, reviewSummary, statusSnapshot, behindBy] = await Promise.all([
6936
+ getOrLoadCachedGitHubPullRequestReviewThreadSummary(
6937
+ octokit,
6938
+ repository,
6939
+ node.number,
6940
+ inlineReviewThreadSummary
6941
+ ),
6942
+ getOrLoadCachedGitHubPullRequestReviewSummary(
6943
+ octokit,
6944
+ repository,
6945
+ node.number,
6946
+ inlineReviewSummary
6947
+ ),
6948
+ getGitHubPullRequestStatusSnapshot(octokit, repository, node.number, pullRequestStatusCache, {
6949
+ reviewThreadSummary: inlineReviewThreadSummary,
6950
+ ciState: inlineCiState
6951
+ }),
6952
+ getGitHubPullRequestBehindCount(octokit, repository, {
6953
+ baseBranch: node.baseRefName,
6954
+ headBranch: node.headRefName,
6955
+ headRepositoryOwner: node.headRepositoryOwner?.login
6956
+ })
6957
+ ]);
6958
+ const closingIssues = normalizeProjectPullRequestClosingIssues(repository, node.closingIssuesReferences?.nodes);
6959
+ const linkedIssue = resolveLinkedPaperclipIssueForPullRequest(node.number, closingIssues, issueLookup);
6960
+ const author = buildProjectPullRequestPerson({
6961
+ login: node.author?.login,
6962
+ url: node.author?.url,
6963
+ avatarUrl: node.author?.avatarUrl
6964
+ });
6965
+ const checksStatus = statusSnapshot.ciState === "green" ? "passed" : statusSnapshot.ciState === "red" ? "failed" : "pending";
6966
+ const githubMergeable = node.mergeable === "MERGEABLE";
6967
+ const reviewable = resolveProjectPullRequestReviewable({
6968
+ checksStatus,
6969
+ copilotUnresolvedReviewThreads: reviewThreadSummary.copilotUnresolvedReviewThreads,
6970
+ githubMergeable
6971
+ });
6972
+ const mergeable = resolveProjectPullRequestMergeable({
6973
+ checksStatus,
6974
+ reviewApprovals: reviewSummary.approvals,
6975
+ unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
6976
+ githubMergeable
6977
+ });
6978
+ const upToDateStatus = resolveProjectPullRequestUpToDateStatus({
6979
+ mergeStateStatus: node.mergeStateStatus,
6980
+ mergeable: node.mergeable,
6981
+ behindBy
6982
+ });
6983
+ return {
6984
+ id: node.id ?? `github-pull-request-${repository.owner}-${repository.repo}-${node.number}`,
6985
+ number: node.number,
6986
+ title: node.title.trim(),
6987
+ labels: normalizeProjectPullRequestLabels(node.labels?.nodes),
6988
+ author,
6989
+ assignees: [],
6990
+ checksStatus,
6991
+ upToDateStatus,
6992
+ githubMergeable,
6993
+ reviewable,
6994
+ reviewApprovals: reviewSummary.approvals,
6995
+ reviewChangesRequested: reviewSummary.changesRequested,
6996
+ reviewCommentCount: 0,
6997
+ unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
6998
+ copilotUnresolvedReviewThreads: reviewThreadSummary.copilotUnresolvedReviewThreads,
6999
+ commentsCount: typeof node.comments?.totalCount === "number" && node.comments.totalCount >= 0 ? Math.floor(node.comments.totalCount) : 0,
7000
+ createdAt: node.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
7001
+ updatedAt: node.updatedAt ?? node.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
7002
+ ...linkedIssue?.paperclipIssueId ? { paperclipIssueId: linkedIssue.paperclipIssueId } : {},
7003
+ ...linkedIssue?.paperclipIssueKey ? { paperclipIssueKey: linkedIssue.paperclipIssueKey } : {},
7004
+ mergeable,
7005
+ status: getProjectPullRequestStatus(node.state),
7006
+ githubUrl: node.url,
7007
+ checksUrl: `${node.url}/checks`,
7008
+ reviewsUrl: `${node.url}/files`,
7009
+ reviewThreadsUrl: `${node.url}/files`,
7010
+ commentsUrl: node.url,
7011
+ baseBranch: node.baseRefName ?? "",
7012
+ headBranch: node.headRefName ?? "",
7013
+ commits: typeof node.commits?.totalCount === "number" && node.commits.totalCount >= 0 ? Math.floor(node.commits.totalCount) : 0,
7014
+ changedFiles: typeof node.changedFiles === "number" && node.changedFiles >= 0 ? Math.floor(node.changedFiles) : 0
7015
+ };
7016
+ }
7017
+ async function listProjectPullRequestSummaryRecords(ctx, octokit, scope, options) {
7018
+ const issueLookup = await buildProjectPullRequestIssueLookup(ctx, scope);
7019
+ const pullRequestStatusCache = /* @__PURE__ */ new Map();
7020
+ const pullRequests = [];
7021
+ const first = Math.max(1, Math.floor(options?.first ?? PROJECT_PULL_REQUEST_PAGE_SIZE));
7022
+ let after = typeof options?.after === "string" && options.after.trim() ? options.after.trim() : void 0;
7023
+ let totalOpenPullRequests = 0;
7024
+ let defaultBranchName;
7025
+ let hasNextPage = false;
7026
+ let nextCursor;
7027
+ do {
7028
+ const response = await octokit.graphql(
7029
+ GITHUB_PROJECT_PULL_REQUESTS_QUERY,
7030
+ {
7031
+ owner: scope.repository.owner,
7032
+ repo: scope.repository.repo,
7033
+ first,
7034
+ after
7035
+ }
7036
+ );
7037
+ const connection = response.repository?.pullRequests;
7038
+ if (typeof connection?.totalCount === "number" && connection.totalCount >= 0) {
7039
+ totalOpenPullRequests = Math.floor(connection.totalCount);
7040
+ }
7041
+ defaultBranchName ??= normalizeOptionalString2(response.repository?.defaultBranchRef?.name);
7042
+ const pageNodes = (connection?.nodes ?? []).filter((node) => node !== null);
7043
+ const pageRecords = await mapWithConcurrency(
7044
+ pageNodes,
7045
+ PROJECT_PULL_REQUEST_SUMMARY_CONCURRENCY,
7046
+ async (node) => buildProjectPullRequestSummaryRecord(
7047
+ octokit,
7048
+ scope.repository,
7049
+ node,
7050
+ issueLookup,
7051
+ pullRequestStatusCache
7052
+ )
7053
+ );
7054
+ pullRequests.push(...pageRecords.filter((record) => Boolean(record)));
7055
+ nextCursor = getPageCursor(connection?.pageInfo);
7056
+ hasNextPage = Boolean(connection?.pageInfo?.hasNextPage && nextCursor);
7057
+ if (!options?.collectAll) {
7058
+ break;
7059
+ }
7060
+ after = nextCursor;
7061
+ } while (after);
7062
+ return {
7063
+ pullRequests: sortProjectPullRequestRecordsByUpdatedAt(pullRequests),
7064
+ totalOpenPullRequests,
7065
+ ...defaultBranchName ? { defaultBranchName } : {},
7066
+ hasNextPage,
7067
+ ...nextCursor ? { nextCursor } : {}
7068
+ };
7069
+ }
7070
+ async function buildProjectPullRequestMetricCounts(octokit, repository, node, pullRequestStatusCache) {
7071
+ if (!node || typeof node.number !== "number") {
7072
+ return {
7073
+ pullRequestNumber: null,
7074
+ mergeablePullRequests: 0,
7075
+ reviewablePullRequests: 0,
7076
+ failingPullRequests: 0
7077
+ };
7078
+ }
7079
+ const inlineReviewThreadSummary = node.reviewThreads?.pageInfo?.hasNextPage ? null : summarizeProjectPullRequestReviewThreadsFromConnection(node.reviewThreads);
7080
+ const inlineReviewSummary = node.reviews?.pageInfo?.hasNextPage ? null : summarizeGitHubPullRequestReviewNodes(node.reviews?.nodes);
7081
+ const inlineCiState = tryBuildGitHubPullRequestCiStateFromBatchNode({
7082
+ statusCheckRollup: node.statusCheckRollup
7083
+ });
7084
+ const [reviewThreadSummary, reviewSummary, statusSnapshot] = await Promise.all([
7085
+ getOrLoadCachedGitHubPullRequestReviewThreadSummary(
7086
+ octokit,
7087
+ repository,
7088
+ node.number,
7089
+ inlineReviewThreadSummary
7090
+ ),
7091
+ getOrLoadCachedGitHubPullRequestReviewSummary(
7092
+ octokit,
7093
+ repository,
7094
+ node.number,
7095
+ inlineReviewSummary
7096
+ ),
7097
+ getGitHubPullRequestStatusSnapshot(octokit, repository, node.number, pullRequestStatusCache, {
7098
+ reviewThreadSummary: inlineReviewThreadSummary,
7099
+ ciState: inlineCiState
7100
+ })
7101
+ ]);
7102
+ const checksStatus = statusSnapshot.ciState === "green" ? "passed" : statusSnapshot.ciState === "red" ? "failed" : "pending";
7103
+ const githubMergeable = node.mergeable === "MERGEABLE";
7104
+ const reviewable = resolveProjectPullRequestReviewable({
7105
+ checksStatus,
7106
+ copilotUnresolvedReviewThreads: reviewThreadSummary.copilotUnresolvedReviewThreads,
7107
+ githubMergeable
7108
+ });
7109
+ const mergeable = resolveProjectPullRequestMergeable({
7110
+ checksStatus,
7111
+ reviewApprovals: reviewSummary.approvals,
7112
+ unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
7113
+ githubMergeable
7114
+ });
7115
+ return {
7116
+ pullRequestNumber: Math.floor(node.number),
7117
+ mergeablePullRequests: mergeable ? 1 : 0,
7118
+ reviewablePullRequests: reviewable ? 1 : 0,
7119
+ failingPullRequests: checksStatus === "failed" ? 1 : 0
7120
+ };
7121
+ }
7122
+ async function listProjectPullRequestCount(octokit, scope) {
7123
+ const response = await octokit.graphql(
7124
+ GITHUB_PROJECT_OPEN_PULL_REQUEST_COUNT_QUERY,
7125
+ {
7126
+ owner: scope.repository.owner,
7127
+ repo: scope.repository.repo
7128
+ }
7129
+ );
7130
+ const totalCount = response.repository?.pullRequests?.totalCount;
7131
+ return typeof totalCount === "number" && totalCount >= 0 ? Math.floor(totalCount) : 0;
7132
+ }
7133
+ async function listProjectPullRequestMetrics(octokit, scope) {
7134
+ const pullRequestStatusCache = /* @__PURE__ */ new Map();
7135
+ let totalOpenPullRequests = 0;
7136
+ let defaultBranchName;
7137
+ let mergeablePullRequests = 0;
7138
+ let reviewablePullRequests = 0;
7139
+ let failingPullRequests = 0;
7140
+ const mergeablePullRequestNumbers = [];
7141
+ const reviewablePullRequestNumbers = [];
7142
+ const failingPullRequestNumbers = [];
7143
+ let after;
7144
+ do {
7145
+ const response = await octokit.graphql(
7146
+ GITHUB_PROJECT_PULL_REQUEST_METRICS_QUERY,
7147
+ {
7148
+ owner: scope.repository.owner,
7149
+ repo: scope.repository.repo,
7150
+ first: PROJECT_PULL_REQUEST_METRICS_BATCH_SIZE,
7151
+ after
7152
+ }
7153
+ );
7154
+ const connection = response.repository?.pullRequests;
7155
+ if (typeof connection?.totalCount === "number" && connection.totalCount >= 0) {
7156
+ totalOpenPullRequests = Math.floor(connection.totalCount);
7157
+ }
7158
+ defaultBranchName ??= normalizeOptionalString2(response.repository?.defaultBranchRef?.name);
7159
+ const pageNodes = (connection?.nodes ?? []).filter((node) => node !== null);
7160
+ const pageMetrics = await mapWithConcurrency(
7161
+ pageNodes,
7162
+ PROJECT_PULL_REQUEST_SUMMARY_CONCURRENCY,
7163
+ async (node) => buildProjectPullRequestMetricCounts(octokit, scope.repository, node, pullRequestStatusCache)
7164
+ );
7165
+ for (const pageMetric of pageMetrics) {
7166
+ mergeablePullRequests += pageMetric.mergeablePullRequests;
7167
+ reviewablePullRequests += pageMetric.reviewablePullRequests;
7168
+ failingPullRequests += pageMetric.failingPullRequests;
7169
+ if (pageMetric.pullRequestNumber !== null && pageMetric.mergeablePullRequests > 0) {
7170
+ mergeablePullRequestNumbers.push(pageMetric.pullRequestNumber);
7171
+ }
7172
+ if (pageMetric.pullRequestNumber !== null && pageMetric.reviewablePullRequests > 0) {
7173
+ reviewablePullRequestNumbers.push(pageMetric.pullRequestNumber);
7174
+ }
7175
+ if (pageMetric.pullRequestNumber !== null && pageMetric.failingPullRequests > 0) {
7176
+ failingPullRequestNumbers.push(pageMetric.pullRequestNumber);
7177
+ }
7178
+ }
7179
+ after = getPageCursor(connection?.pageInfo);
7180
+ } while (after);
7181
+ return {
7182
+ totalOpenPullRequests,
7183
+ ...defaultBranchName ? { defaultBranchName } : {},
7184
+ mergeablePullRequests,
7185
+ reviewablePullRequests,
7186
+ failingPullRequests,
7187
+ mergeablePullRequestNumbers,
7188
+ reviewablePullRequestNumbers,
7189
+ failingPullRequestNumbers
7190
+ };
7191
+ }
7192
+ function buildProjectPullRequestMetricsCacheKey(scope) {
7193
+ return buildRepositoryPullRequestCollectionCacheKey(scope.repository, "metrics");
7194
+ }
7195
+ function buildProjectPullRequestSummaryCacheKey(scope) {
7196
+ return `${buildProjectPullRequestScopeCacheKey({
7197
+ companyId: scope.companyId,
7198
+ projectId: scope.projectId,
7199
+ repositoryUrl: scope.repository.url
7200
+ })}:summary`;
7201
+ }
7202
+ function buildProjectPullRequestCountCacheKey(scope) {
7203
+ return buildRepositoryPullRequestCollectionCacheKey(scope.repository, "count");
7204
+ }
7205
+ function getCachedProjectPullRequestSummarySeed(scope) {
7206
+ const cachedPage = getFreshCacheValue(
7207
+ activeProjectPullRequestPageCache,
7208
+ buildProjectPullRequestPageCacheKey(scope, "all", 0)
7209
+ );
7210
+ if (!cachedPage || cachedPage.status !== "ready") {
7211
+ return null;
7212
+ }
7213
+ const totalOpenPullRequests = typeof cachedPage.totalOpenPullRequests === "number" && cachedPage.totalOpenPullRequests >= 0 ? Math.floor(cachedPage.totalOpenPullRequests) : null;
7214
+ const pullRequests = Array.isArray(cachedPage.pullRequests) ? cachedPage.pullRequests.filter(
7215
+ (record) => Boolean(record) && typeof record === "object" && !Array.isArray(record)
7216
+ ) : null;
7217
+ if (totalOpenPullRequests === null || !pullRequests) {
7218
+ return null;
7219
+ }
7220
+ const hasNextPage = cachedPage.hasNextPage === true;
7221
+ const nextCursor = typeof cachedPage.nextCursor === "string" && cachedPage.nextCursor.trim() ? cachedPage.nextCursor.trim() : void 0;
7222
+ if (hasNextPage && !nextCursor) {
7223
+ return null;
7224
+ }
7225
+ const defaultBranchName = normalizeOptionalString2(cachedPage.defaultBranchName);
7226
+ return {
7227
+ totalOpenPullRequests,
7228
+ ...defaultBranchName ? { defaultBranchName } : {},
7229
+ pullRequests: sortProjectPullRequestRecordsByUpdatedAt(pullRequests),
7230
+ hasNextPage,
7231
+ ...nextCursor ? { nextCursor } : {}
7232
+ };
7233
+ }
7234
+ function cacheProjectPullRequestSummary(scope, summary) {
7235
+ const pullRequests = sortProjectPullRequestRecordsByUpdatedAt(summary.pullRequests);
7236
+ const metrics = buildProjectPullRequestMetrics(
7237
+ pullRequests,
7238
+ summary.totalOpenPullRequests,
7239
+ summary.defaultBranchName
7240
+ );
7241
+ cacheProjectPullRequestSummaryRecords(scope, pullRequests);
7242
+ cacheProjectPullRequestMetricsEntry(scope, metrics);
7243
+ return setCacheValue(
7244
+ activeProjectPullRequestSummaryCache,
7245
+ buildProjectPullRequestSummaryCacheKey(scope),
7246
+ {
7247
+ totalOpenPullRequests: summary.totalOpenPullRequests,
7248
+ ...summary.defaultBranchName ? { defaultBranchName: summary.defaultBranchName } : {},
7249
+ pullRequests,
7250
+ metrics
7251
+ },
7252
+ PROJECT_PULL_REQUEST_SUMMARY_CACHE_TTL_MS
7253
+ );
7254
+ }
7255
+ function cacheProjectPullRequestCount(scope, totalOpenPullRequests, ttlMs = PROJECT_PULL_REQUEST_SUMMARY_CACHE_TTL_MS) {
7256
+ return setCacheValue(
7257
+ activeProjectPullRequestCountCache,
7258
+ buildProjectPullRequestCountCacheKey(scope),
7259
+ Math.max(0, Math.floor(totalOpenPullRequests)),
7260
+ ttlMs
7261
+ );
7262
+ }
7263
+ function cacheProjectPullRequestMetricsEntry(scope, metrics, ttlMs = PROJECT_PULL_REQUEST_SUMMARY_CACHE_TTL_MS) {
7264
+ cacheProjectPullRequestCount(scope, metrics.totalOpenPullRequests, ttlMs);
7265
+ return setCacheValue(
7266
+ activeProjectPullRequestMetricsCache,
7267
+ buildProjectPullRequestMetricsCacheKey(scope),
7268
+ metrics,
7269
+ ttlMs
7270
+ );
7271
+ }
7272
+ async function getOrLoadCachedProjectPullRequestMetricsEntry(ctx, scope, octokit) {
7273
+ const summaryCacheKey = buildProjectPullRequestSummaryCacheKey(scope);
7274
+ const cachedSummary = getFreshCacheValue(activeProjectPullRequestSummaryCache, summaryCacheKey);
7275
+ if (cachedSummary) {
7276
+ return cachedSummary.metrics;
7277
+ }
7278
+ const metricsCacheKey = buildProjectPullRequestMetricsCacheKey(scope);
7279
+ const cachedMetrics = getFreshCacheValue(activeProjectPullRequestMetricsCache, metricsCacheKey);
7280
+ if (cachedMetrics) {
7281
+ return cachedMetrics;
7282
+ }
7283
+ const inFlightMetrics = activeProjectPullRequestMetricsPromiseCache.get(metricsCacheKey);
7284
+ if (inFlightMetrics) {
7285
+ return inFlightMetrics;
7286
+ }
7287
+ const loadMetricsPromise = (async () => {
7288
+ const resolvedOctokit = octokit ?? await createGitHubToolOctokit(ctx);
7289
+ const metrics = await listProjectPullRequestMetrics(resolvedOctokit, scope);
7290
+ return cacheProjectPullRequestMetricsEntry(scope, metrics);
7291
+ })();
7292
+ activeProjectPullRequestMetricsPromiseCache.set(metricsCacheKey, loadMetricsPromise);
7293
+ try {
7294
+ return await loadMetricsPromise;
7295
+ } finally {
7296
+ if (activeProjectPullRequestMetricsPromiseCache.get(metricsCacheKey) === loadMetricsPromise) {
7297
+ activeProjectPullRequestMetricsPromiseCache.delete(metricsCacheKey);
7298
+ }
7299
+ }
7300
+ }
7301
+ async function getOrLoadCachedProjectPullRequestCount(ctx, scope, octokit) {
7302
+ const summaryCacheKey = buildProjectPullRequestSummaryCacheKey(scope);
7303
+ const cachedSummary = getFreshCacheValue(activeProjectPullRequestSummaryCache, summaryCacheKey);
7304
+ if (cachedSummary) {
7305
+ return cachedSummary.totalOpenPullRequests;
7306
+ }
7307
+ const metricsCacheKey = buildProjectPullRequestMetricsCacheKey(scope);
7308
+ const cachedMetrics = getFreshCacheValue(activeProjectPullRequestMetricsCache, metricsCacheKey);
7309
+ if (cachedMetrics) {
7310
+ return cachedMetrics.totalOpenPullRequests;
7311
+ }
7312
+ const countCacheKey = buildProjectPullRequestCountCacheKey(scope);
7313
+ const cachedCount = getFreshCacheValue(activeProjectPullRequestCountCache, countCacheKey);
7314
+ if (cachedCount !== null) {
7315
+ return cachedCount;
7316
+ }
7317
+ const cachedSummarySeed = getCachedProjectPullRequestSummarySeed(scope);
7318
+ if (cachedSummarySeed) {
7319
+ return cacheProjectPullRequestCount(scope, cachedSummarySeed.totalOpenPullRequests);
7320
+ }
7321
+ const inFlightCount = activeProjectPullRequestCountPromiseCache.get(countCacheKey);
7322
+ if (inFlightCount) {
7323
+ return inFlightCount;
7324
+ }
7325
+ const loadCountPromise = (async () => {
7326
+ const resolvedOctokit = octokit ?? await createGitHubToolOctokit(ctx);
7327
+ const totalOpenPullRequests = await listProjectPullRequestCount(resolvedOctokit, scope);
7328
+ return setCacheValue(
7329
+ activeProjectPullRequestCountCache,
7330
+ countCacheKey,
7331
+ totalOpenPullRequests,
7332
+ PROJECT_PULL_REQUEST_SUMMARY_CACHE_TTL_MS
7333
+ );
7334
+ })();
7335
+ activeProjectPullRequestCountPromiseCache.set(countCacheKey, loadCountPromise);
7336
+ try {
7337
+ return await loadCountPromise;
7338
+ } finally {
7339
+ if (activeProjectPullRequestCountPromiseCache.get(countCacheKey) === loadCountPromise) {
7340
+ activeProjectPullRequestCountPromiseCache.delete(countCacheKey);
7341
+ }
7342
+ }
7343
+ }
7344
+ async function getOrLoadCachedProjectPullRequestMetrics(ctx, scope, octokit) {
7345
+ return getPublicProjectPullRequestMetrics(
7346
+ await getOrLoadCachedProjectPullRequestMetricsEntry(ctx, scope, octokit)
7347
+ );
7348
+ }
7349
+ async function listProjectPullRequestSummaryRecordsByNumbers(ctx, octokit, scope, pullRequestNumbers) {
7350
+ const normalizedPullRequestNumbers = [
7351
+ ...new Set(
7352
+ pullRequestNumbers.map((value) => Math.floor(value)).filter((value) => Number.isFinite(value) && value > 0)
7353
+ )
7354
+ ];
7355
+ if (normalizedPullRequestNumbers.length === 0) {
7356
+ return [];
7357
+ }
7358
+ const query = buildGitHubProjectPullRequestsByNumberQuery(normalizedPullRequestNumbers);
7359
+ const response = await octokit.graphql(
7360
+ query,
7361
+ {
7362
+ owner: scope.repository.owner,
7363
+ repo: scope.repository.repo
7364
+ }
7365
+ );
7366
+ const repository = response.repository && typeof response.repository === "object" ? response.repository : {};
7367
+ const issueLookup = await buildProjectPullRequestIssueLookup(ctx, scope);
7368
+ const pullRequestStatusCache = /* @__PURE__ */ new Map();
7369
+ const recordsByNumber = /* @__PURE__ */ new Map();
7370
+ const builtRecords = await mapWithConcurrency(
7371
+ normalizedPullRequestNumbers,
7372
+ PROJECT_PULL_REQUEST_SUMMARY_CONCURRENCY,
7373
+ async (pullRequestNumber) => {
7374
+ const node = repository[buildGitHubProjectPullRequestByNumberAlias(pullRequestNumber)];
7375
+ if (!node) {
7376
+ return null;
7377
+ }
7378
+ const record = await buildProjectPullRequestSummaryRecord(
7379
+ octokit,
7380
+ scope.repository,
7381
+ node,
7382
+ issueLookup,
7383
+ pullRequestStatusCache
7384
+ );
7385
+ if (!record) {
7386
+ return null;
7387
+ }
7388
+ return {
7389
+ pullRequestNumber,
7390
+ record
7391
+ };
7392
+ }
7393
+ );
7394
+ for (const builtRecord of builtRecords) {
7395
+ if (!builtRecord) {
7396
+ continue;
7397
+ }
7398
+ recordsByNumber.set(builtRecord.pullRequestNumber, builtRecord.record);
7399
+ }
7400
+ return normalizedPullRequestNumbers.map((pullRequestNumber) => recordsByNumber.get(pullRequestNumber)).filter((record) => Boolean(record));
7401
+ }
7402
+ async function getOrLoadProjectPullRequestSummaryRecordsForNumbers(ctx, scope, pullRequestNumbers, octokit) {
7403
+ const normalizedPullRequestNumbers = [
7404
+ ...new Set(
7405
+ pullRequestNumbers.map((value) => Math.floor(value)).filter((value) => Number.isFinite(value) && value > 0)
7406
+ )
7407
+ ];
7408
+ if (normalizedPullRequestNumbers.length === 0) {
7409
+ return [];
7410
+ }
7411
+ const cachedSummary = getFreshCacheValue(
7412
+ activeProjectPullRequestSummaryCache,
7413
+ buildProjectPullRequestSummaryCacheKey(scope)
7414
+ );
7415
+ const summaryRecordsByNumber = cachedSummary ? new Map(
7416
+ cachedSummary.pullRequests.map((pullRequest) => {
7417
+ const pullRequestNumber = getProjectPullRequestNumber(pullRequest);
7418
+ return pullRequestNumber === null ? null : [pullRequestNumber, pullRequest];
7419
+ }).filter((entry) => Boolean(entry))
7420
+ ) : null;
7421
+ const recordsByNumber = /* @__PURE__ */ new Map();
7422
+ for (const pullRequestNumber of normalizedPullRequestNumbers) {
7423
+ const cachedSummaryRecord = summaryRecordsByNumber?.get(pullRequestNumber);
7424
+ if (cachedSummaryRecord) {
7425
+ recordsByNumber.set(pullRequestNumber, cachedSummaryRecord);
7426
+ continue;
7427
+ }
7428
+ const cachedRecord = getFreshCacheValue(
7429
+ activeProjectPullRequestSummaryRecordCache,
7430
+ buildProjectPullRequestSummaryRecordCacheKey(scope, pullRequestNumber)
7431
+ );
7432
+ if (cachedRecord) {
7433
+ recordsByNumber.set(pullRequestNumber, cachedRecord);
7434
+ }
7435
+ }
7436
+ const missingPullRequestNumbers = normalizedPullRequestNumbers.filter((pullRequestNumber) => !recordsByNumber.has(pullRequestNumber));
7437
+ if (missingPullRequestNumbers.length > 0) {
7438
+ const resolvedOctokit = octokit ?? await createGitHubToolOctokit(ctx);
7439
+ const loadedRecords = await listProjectPullRequestSummaryRecordsByNumbers(
7440
+ ctx,
7441
+ resolvedOctokit,
7442
+ scope,
7443
+ missingPullRequestNumbers
7444
+ );
7445
+ cacheProjectPullRequestSummaryRecords(scope, loadedRecords);
7446
+ for (const loadedRecord of loadedRecords) {
7447
+ const pullRequestNumber = getProjectPullRequestNumber(loadedRecord);
7448
+ if (pullRequestNumber === null) {
7449
+ continue;
7450
+ }
7451
+ recordsByNumber.set(pullRequestNumber, loadedRecord);
7452
+ }
7453
+ }
7454
+ return normalizedPullRequestNumbers.map((pullRequestNumber) => recordsByNumber.get(pullRequestNumber)).filter((record) => Boolean(record));
7455
+ }
7456
+ function buildPaperclipIssueDescriptionFromPullRequest(params) {
7457
+ const importLine = `Imported from GitHub pull request [#${params.pullRequestNumber}](${params.pullRequestUrl}) in ${formatRepositoryLabel(params.repository)}.`;
7458
+ const body = typeof params.body === "string" ? params.body.trim() : "";
7459
+ return body ? `${importLine}
7460
+
7461
+ ${body}` : importLine;
7462
+ }
7463
+ async function requireProjectPullRequestScope(ctx, input, resolvedProjectMappings) {
7464
+ const companyId = normalizeCompanyId(input.companyId);
7465
+ const projectId = typeof input.projectId === "string" && input.projectId.trim() ? input.projectId.trim() : void 0;
7466
+ if (!companyId || !projectId) {
7467
+ throw new Error("A company id and project id are required to load project pull requests.");
7468
+ }
7469
+ const mappings = resolvedProjectMappings ?? await resolveProjectScopedMappings(
7470
+ ctx,
7471
+ normalizeSettings(await ctx.state.get(SETTINGS_SCOPE)).mappings,
7472
+ {
7473
+ companyId,
7474
+ projectId
7475
+ }
7476
+ );
7477
+ if (mappings.length === 0) {
7478
+ throw new Error("No saved GitHub repository mapping matches this Paperclip project.");
7479
+ }
7480
+ const requestedRepositoryUrl = typeof input.repositoryUrl === "string" && input.repositoryUrl.trim() ? getNormalizedMappingRepositoryUrl({
7481
+ repositoryUrl: input.repositoryUrl
7482
+ }) : void 0;
7483
+ const sortedMappings = [...mappings].sort(
7484
+ (left, right) => getNormalizedMappingRepositoryUrl(left).localeCompare(getNormalizedMappingRepositoryUrl(right))
7485
+ );
7486
+ const mapping = requestedRepositoryUrl ? sortedMappings.find((entry) => getNormalizedMappingRepositoryUrl(entry) === requestedRepositoryUrl) : sortedMappings[0];
7487
+ if (!mapping) {
7488
+ throw new Error("This Paperclip project is not mapped to the requested GitHub repository.");
7489
+ }
7490
+ return {
7491
+ companyId,
7492
+ projectId,
7493
+ projectLabel: mapping.paperclipProjectName.trim() || "Project",
7494
+ mapping,
7495
+ repository: requireRepositoryReference(mapping.repositoryUrl),
7496
+ mappingCount: sortedMappings.length
7497
+ };
7498
+ }
7499
+ async function buildProjectPullRequestsPageData(ctx, input) {
7500
+ const filter = normalizeProjectPullRequestFilter(input.filter);
7501
+ const pageIndex = normalizeProjectPullRequestPageIndex(input.pageIndex);
7502
+ const cursor = typeof input.cursor === "string" && input.cursor.trim() ? input.cursor.trim() : void 0;
7503
+ const companyId = normalizeCompanyId(input.companyId);
7504
+ const projectId = typeof input.projectId === "string" && input.projectId.trim() ? input.projectId.trim() : null;
7505
+ if (!companyId || !projectId) {
7506
+ return {
7507
+ status: "missing_project",
7508
+ projectId,
7509
+ projectLabel: "Project",
7510
+ repositoryLabel: "",
7511
+ repositoryUrl: "",
7512
+ repositoryDescription: "",
7513
+ filter,
7514
+ pageIndex: 0,
7515
+ pageSize: PROJECT_PULL_REQUEST_PAGE_SIZE,
7516
+ hasNextPage: false,
7517
+ hasPreviousPage: false,
7518
+ totalFilteredPullRequests: 0,
7519
+ pullRequests: [],
7520
+ message: "Open this page from a mapped Paperclip project."
7521
+ };
7522
+ }
7523
+ const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
7524
+ const projectMappings = await resolveProjectScopedMappings(ctx, settings.mappings, {
7525
+ companyId,
7526
+ projectId
7527
+ });
7528
+ if (projectMappings.length === 0) {
7529
+ return {
7530
+ status: "unmapped",
7531
+ projectId,
7532
+ projectLabel: "Project",
7533
+ repositoryLabel: "",
7534
+ repositoryUrl: "",
7535
+ repositoryDescription: "",
7536
+ filter,
7537
+ pageIndex: 0,
7538
+ pageSize: PROJECT_PULL_REQUEST_PAGE_SIZE,
7539
+ hasNextPage: false,
7540
+ hasPreviousPage: false,
7541
+ totalFilteredPullRequests: 0,
7542
+ pullRequests: [],
7543
+ message: "No GitHub repository is mapped to this project yet."
7544
+ };
7545
+ }
7546
+ const scope = await requireProjectPullRequestScope(ctx, input, projectMappings);
7547
+ const config = await getResolvedConfig(ctx);
7548
+ if (!hasConfiguredGithubToken(settings, config)) {
7549
+ return {
7550
+ status: "missing_token",
7551
+ projectId,
7552
+ projectLabel: scope.projectLabel,
7553
+ repositoryLabel: formatRepositoryLabel(scope.repository),
7554
+ repositoryUrl: scope.repository.url,
7555
+ repositoryDescription: "",
7556
+ filter,
7557
+ pageIndex: 0,
7558
+ pageSize: PROJECT_PULL_REQUEST_PAGE_SIZE,
7559
+ hasNextPage: false,
7560
+ hasPreviousPage: false,
7561
+ totalFilteredPullRequests: 0,
7562
+ pullRequests: [],
7563
+ message: "Configure a GitHub token before opening pull requests."
7564
+ };
7565
+ }
7566
+ try {
7567
+ const octokit = await createGitHubToolOctokit(ctx);
7568
+ const pageCacheKey = buildProjectPullRequestPageCacheKey(scope, filter, pageIndex, cursor);
7569
+ const cachedPage = getFreshCacheValue(activeProjectPullRequestPageCache, pageCacheKey);
7570
+ if (cachedPage) {
7571
+ return cachedPage;
7572
+ }
7573
+ if (filter !== "all") {
7574
+ const metrics = await getOrLoadCachedProjectPullRequestMetricsEntry(ctx, scope, octokit);
7575
+ const filteredPullRequestNumbers = getProjectPullRequestNumbersForFilter(metrics, filter);
7576
+ const page = sliceProjectPullRequestNumbers(filteredPullRequestNumbers, pageIndex, PROJECT_PULL_REQUEST_PAGE_SIZE);
7577
+ const pullRequests = sortProjectPullRequestRecordsByUpdatedAt(
7578
+ await getOrLoadProjectPullRequestSummaryRecordsForNumbers(
7579
+ ctx,
7580
+ scope,
7581
+ page.pullRequestNumbers,
7582
+ octokit
7583
+ )
7584
+ );
7585
+ const tokenPermissionAudit2 = await getOrLoadGitHubRepositoryTokenCapabilityAudit(octokit, scope.repository, {
7586
+ samplePullRequestNumber: pullRequests[0] ? getProjectPullRequestNumber(pullRequests[0]) ?? void 0 : void 0
7587
+ });
7588
+ cacheProjectPullRequestCount(scope, metrics.totalOpenPullRequests);
7589
+ return setCacheValue(
7590
+ activeProjectPullRequestPageCache,
7591
+ pageCacheKey,
7592
+ {
7593
+ status: "ready",
7594
+ projectId,
7595
+ projectLabel: scope.projectLabel,
7596
+ repositoryLabel: formatRepositoryLabel(scope.repository),
7597
+ repositoryUrl: scope.repository.url,
7598
+ repositoryDescription: "",
7599
+ ...metrics.defaultBranchName ? { defaultBranchName: metrics.defaultBranchName } : {},
7600
+ filter,
7601
+ pageIndex: page.pageIndex,
7602
+ pageSize: PROJECT_PULL_REQUEST_PAGE_SIZE,
7603
+ hasNextPage: page.hasNextPage,
7604
+ hasPreviousPage: page.hasPreviousPage,
7605
+ totalFilteredPullRequests: filteredPullRequestNumbers.length,
7606
+ totalOpenPullRequests: metrics.totalOpenPullRequests,
7607
+ pullRequests,
7608
+ tokenPermissionAudit: tokenPermissionAudit2
7609
+ },
7610
+ PROJECT_PULL_REQUEST_PAGE_CACHE_TTL_MS
7611
+ );
7612
+ }
7613
+ const cachedFullSummary = getFreshCacheValue(
7614
+ activeProjectPullRequestSummaryCache,
7615
+ buildProjectPullRequestSummaryCacheKey(scope)
7616
+ );
7617
+ if (cachedFullSummary) {
7618
+ cacheProjectPullRequestCount(scope, cachedFullSummary.totalOpenPullRequests);
7619
+ const page = sliceProjectPullRequestRecords(
7620
+ cachedFullSummary.pullRequests,
7621
+ pageIndex,
7622
+ PROJECT_PULL_REQUEST_PAGE_SIZE
7623
+ );
7624
+ const tokenPermissionAudit2 = await getOrLoadGitHubRepositoryTokenCapabilityAudit(octokit, scope.repository, {
7625
+ samplePullRequestNumber: page.pullRequests[0] ? getProjectPullRequestNumber(page.pullRequests[0]) ?? void 0 : void 0
7626
+ });
7627
+ return setCacheValue(
7628
+ activeProjectPullRequestPageCache,
7629
+ pageCacheKey,
7630
+ {
7631
+ status: "ready",
7632
+ projectId,
7633
+ projectLabel: scope.projectLabel,
7634
+ repositoryLabel: formatRepositoryLabel(scope.repository),
7635
+ repositoryUrl: scope.repository.url,
7636
+ repositoryDescription: "",
7637
+ ...cachedFullSummary.defaultBranchName ? { defaultBranchName: cachedFullSummary.defaultBranchName } : {},
7638
+ filter,
7639
+ pageIndex: page.pageIndex,
7640
+ pageSize: PROJECT_PULL_REQUEST_PAGE_SIZE,
7641
+ hasNextPage: page.hasNextPage,
7642
+ hasPreviousPage: page.hasPreviousPage,
7643
+ totalFilteredPullRequests: cachedFullSummary.totalOpenPullRequests,
7644
+ totalOpenPullRequests: cachedFullSummary.totalOpenPullRequests,
7645
+ pullRequests: page.pullRequests,
7646
+ tokenPermissionAudit: tokenPermissionAudit2
7647
+ },
7648
+ PROJECT_PULL_REQUEST_PAGE_CACHE_TTL_MS
7649
+ );
7650
+ }
7651
+ const summary = await listProjectPullRequestSummaryRecords(ctx, octokit, scope, {
7652
+ after: cursor,
7653
+ first: PROJECT_PULL_REQUEST_PAGE_SIZE
7654
+ });
7655
+ cacheProjectPullRequestCount(scope, summary.totalOpenPullRequests);
7656
+ cacheProjectPullRequestSummaryRecords(scope, summary.pullRequests, PROJECT_PULL_REQUEST_PAGE_CACHE_TTL_MS);
7657
+ if (pageIndex === 0 && !summary.hasNextPage) {
7658
+ cacheProjectPullRequestSummary(scope, {
7659
+ totalOpenPullRequests: summary.totalOpenPullRequests,
7660
+ ...summary.defaultBranchName ? { defaultBranchName: summary.defaultBranchName } : {},
7661
+ pullRequests: summary.pullRequests
7662
+ });
7663
+ }
7664
+ const tokenPermissionAudit = await getOrLoadGitHubRepositoryTokenCapabilityAudit(octokit, scope.repository, {
7665
+ samplePullRequestNumber: summary.pullRequests[0] ? getProjectPullRequestNumber(summary.pullRequests[0]) ?? void 0 : void 0
7666
+ });
7667
+ return setCacheValue(
7668
+ activeProjectPullRequestPageCache,
7669
+ pageCacheKey,
7670
+ {
7671
+ status: "ready",
7672
+ projectId,
7673
+ projectLabel: scope.projectLabel,
7674
+ repositoryLabel: formatRepositoryLabel(scope.repository),
7675
+ repositoryUrl: scope.repository.url,
7676
+ repositoryDescription: "",
7677
+ ...summary.defaultBranchName ? { defaultBranchName: summary.defaultBranchName } : {},
7678
+ filter,
7679
+ pageIndex,
7680
+ pageSize: PROJECT_PULL_REQUEST_PAGE_SIZE,
7681
+ hasNextPage: summary.hasNextPage,
7682
+ hasPreviousPage: pageIndex > 0,
7683
+ totalFilteredPullRequests: summary.totalOpenPullRequests,
7684
+ totalOpenPullRequests: summary.totalOpenPullRequests,
7685
+ ...summary.nextCursor ? { nextCursor: summary.nextCursor } : {},
7686
+ pullRequests: summary.pullRequests,
7687
+ tokenPermissionAudit
7688
+ },
7689
+ PROJECT_PULL_REQUEST_PAGE_CACHE_TTL_MS
7690
+ );
7691
+ } catch (error) {
7692
+ return {
7693
+ status: "error",
7694
+ projectId,
7695
+ projectLabel: scope.projectLabel,
7696
+ repositoryLabel: formatRepositoryLabel(scope.repository),
7697
+ repositoryUrl: scope.repository.url,
7698
+ repositoryDescription: "",
7699
+ filter,
7700
+ pageIndex,
7701
+ pageSize: PROJECT_PULL_REQUEST_PAGE_SIZE,
7702
+ hasNextPage: false,
7703
+ hasPreviousPage: pageIndex > 0,
7704
+ totalFilteredPullRequests: 0,
7705
+ pullRequests: [],
7706
+ message: getErrorMessage(error)
7707
+ };
7708
+ }
7709
+ }
7710
+ async function buildProjectPullRequestMetricsData(ctx, input) {
7711
+ const companyId = normalizeCompanyId(input.companyId);
7712
+ const projectId = typeof input.projectId === "string" && input.projectId.trim() ? input.projectId.trim() : null;
7713
+ if (!companyId || !projectId) {
7714
+ return {
7715
+ status: "missing_project",
7716
+ projectId,
7717
+ totalOpenPullRequests: 0,
7718
+ mergeablePullRequests: 0,
7719
+ reviewablePullRequests: 0,
7720
+ failingPullRequests: 0
7721
+ };
7722
+ }
7723
+ const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
7724
+ const projectMappings = await resolveProjectScopedMappings(ctx, settings.mappings, {
7725
+ companyId,
7726
+ projectId
7727
+ });
7728
+ if (projectMappings.length === 0) {
7729
+ return {
7730
+ status: "unmapped",
7731
+ projectId,
7732
+ totalOpenPullRequests: 0,
7733
+ mergeablePullRequests: 0,
7734
+ reviewablePullRequests: 0,
7735
+ failingPullRequests: 0
7736
+ };
7737
+ }
7738
+ const config = await getResolvedConfig(ctx);
7739
+ if (!hasConfiguredGithubToken(settings, config)) {
7740
+ return {
7741
+ status: "missing_token",
7742
+ projectId,
7743
+ totalOpenPullRequests: 0,
7744
+ mergeablePullRequests: 0,
7745
+ reviewablePullRequests: 0,
7746
+ failingPullRequests: 0
7747
+ };
7748
+ }
7749
+ try {
7750
+ const scope = await requireProjectPullRequestScope(ctx, input, projectMappings);
7751
+ const metrics = await getOrLoadCachedProjectPullRequestMetrics(ctx, scope);
7752
+ return {
7753
+ status: "ready",
7754
+ projectId,
7755
+ ...metrics
7756
+ };
7757
+ } catch (error) {
7758
+ return {
7759
+ status: "error",
7760
+ projectId,
7761
+ message: getErrorMessage(error)
7762
+ };
7763
+ }
7764
+ }
7765
+ async function buildProjectPullRequestCountData(ctx, input) {
7766
+ const companyId = normalizeCompanyId(input.companyId);
7767
+ const projectId = typeof input.projectId === "string" && input.projectId.trim() ? input.projectId.trim() : null;
7768
+ if (!companyId || !projectId) {
7769
+ return {
7770
+ status: "missing_project",
7771
+ projectId,
7772
+ totalOpenPullRequests: 0
7773
+ };
7774
+ }
7775
+ const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
7776
+ const projectMappings = await resolveProjectScopedMappings(ctx, settings.mappings, {
7777
+ companyId,
7778
+ projectId
7779
+ });
7780
+ if (projectMappings.length === 0) {
7781
+ return {
7782
+ status: "unmapped",
7783
+ projectId,
7784
+ totalOpenPullRequests: 0
7785
+ };
7786
+ }
7787
+ const config = await getResolvedConfig(ctx);
7788
+ if (!hasConfiguredGithubToken(settings, config)) {
7789
+ return {
7790
+ status: "missing_token",
7791
+ projectId,
7792
+ totalOpenPullRequests: 0
7793
+ };
7794
+ }
7795
+ try {
7796
+ const scope = await requireProjectPullRequestScope(ctx, input, projectMappings);
7797
+ const totalOpenPullRequests = await getOrLoadCachedProjectPullRequestCount(ctx, scope);
7798
+ return {
7799
+ status: "ready",
7800
+ projectId,
7801
+ totalOpenPullRequests
7802
+ };
7803
+ } catch (error) {
7804
+ return {
7805
+ status: "error",
7806
+ projectId,
7807
+ totalOpenPullRequests: 0,
7808
+ message: getErrorMessage(error)
7809
+ };
7810
+ }
7811
+ }
7812
+ async function buildSettingsTokenPermissionAuditData(ctx, input) {
7813
+ const requestedCompanyId = normalizeCompanyId(input.companyId);
7814
+ if (!requestedCompanyId) {
7815
+ return {
7816
+ status: "ready",
7817
+ allRequiredPermissionsGranted: true,
7818
+ repositories: [],
7819
+ missingPermissions: [],
7820
+ warnings: ["Open a company to audit token permissions for its mapped repositories."]
7821
+ };
7822
+ }
7823
+ const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
7824
+ const config = await getResolvedConfig(ctx);
7825
+ if (!hasConfiguredGithubToken(settings, config)) {
7826
+ return {
7827
+ status: "missing_token",
7828
+ allRequiredPermissionsGranted: false,
7829
+ repositories: [],
7830
+ missingPermissions: [],
7831
+ warnings: [],
7832
+ message: "Save a GitHub token before auditing repository permissions."
7833
+ };
7834
+ }
7835
+ const scopedMappings = getSyncableMappings(filterMappingsByCompany(settings.mappings, requestedCompanyId));
7836
+ if (scopedMappings.length === 0) {
7837
+ return {
7838
+ status: "ready",
7839
+ allRequiredPermissionsGranted: true,
7840
+ repositories: [],
7841
+ missingPermissions: [],
7842
+ warnings: ["Add at least one mapped repository in this company to audit token permissions."]
7843
+ };
7844
+ }
7845
+ try {
7846
+ const octokit = await createGitHubToolOctokit(ctx);
7847
+ const repositories = await Promise.all(
7848
+ [
7849
+ ...new Map(
7850
+ scopedMappings.map((mapping) => {
7851
+ const repository = parseRepositoryReference(mapping.repositoryUrl);
7852
+ return repository ? [repository.url, repository] : null;
7853
+ }).filter((entry) => entry !== null)
7854
+ ).values()
7855
+ ].map((repository) => getOrLoadGitHubRepositoryTokenCapabilityAudit(octokit, repository))
7856
+ );
7857
+ const missingPermissions = [
7858
+ ...new Set(repositories.flatMap((repository) => repository.missingPermissions))
7859
+ ].sort((left, right) => left.localeCompare(right));
7860
+ const warnings = repositories.flatMap((repository) => repository.warnings);
7861
+ return {
7862
+ status: "ready",
7863
+ allRequiredPermissionsGranted: repositories.length > 0 && repositories.every((repository) => repository.status === "verified"),
7864
+ repositories,
7865
+ missingPermissions,
7866
+ warnings
7867
+ };
7868
+ } catch (error) {
7869
+ return {
7870
+ status: "error",
7871
+ allRequiredPermissionsGranted: false,
7872
+ repositories: [],
7873
+ missingPermissions: [],
7874
+ warnings: [],
7875
+ message: getErrorMessage(error)
7876
+ };
7877
+ }
7878
+ }
7879
+ async function listProjectPullRequestClosingIssues(octokit, repository, pullRequestNumber) {
7880
+ const response = await octokit.graphql(
7881
+ GITHUB_PULL_REQUEST_CLOSING_ISSUES_QUERY,
7882
+ {
7883
+ owner: repository.owner,
7884
+ repo: repository.repo,
7885
+ pullRequestNumber
7886
+ }
7887
+ );
7888
+ return normalizeProjectPullRequestClosingIssues(
7889
+ repository,
7890
+ response.repository?.pullRequest?.closingIssuesReferences?.nodes
7891
+ );
7892
+ }
7893
+ function getPullRequestApiState(value) {
7894
+ if (value.merged === true) {
7895
+ return "merged";
7896
+ }
7897
+ return value.state === "closed" ? "closed" : "open";
7898
+ }
7899
+ async function buildProjectPullRequestDetailData(ctx, input) {
7900
+ const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
7901
+ if (!pullRequestNumber) {
7902
+ return null;
7903
+ }
7904
+ const scope = await requireProjectPullRequestScope(ctx, input);
7905
+ const detailCacheKey = buildProjectPullRequestDetailCacheKey(scope, pullRequestNumber);
7906
+ const cachedDetail = getFreshCacheValue(activeProjectPullRequestDetailCache, detailCacheKey);
7907
+ if (cachedDetail !== null) {
7908
+ return cachedDetail;
7909
+ }
7910
+ const cachedSummaryRecord = getFreshCacheValue(
7911
+ activeProjectPullRequestSummaryRecordCache,
7912
+ buildProjectPullRequestSummaryRecordCacheKey(scope, pullRequestNumber)
7913
+ );
7914
+ const cachedLinkedIssue = cachedSummaryRecord ? getLinkedPaperclipIssueFromProjectPullRequestRecord(cachedSummaryRecord) : void 0;
7915
+ const octokit = await createGitHubToolOctokit(ctx);
7916
+ const response = await octokit.rest.pulls.get({
7917
+ owner: scope.repository.owner,
7918
+ repo: scope.repository.repo,
7919
+ pull_number: pullRequestNumber,
7920
+ headers: {
7921
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
7922
+ }
7923
+ });
7924
+ const pullRequest = response.data;
7925
+ const reviewSummaryPromise = getOrLoadCachedGitHubPullRequestReviewSummary(octokit, scope.repository, pullRequestNumber);
7926
+ const reviewThreadSummaryPromise = getOrLoadCachedGitHubPullRequestReviewThreadSummary(octokit, scope.repository, pullRequestNumber);
7927
+ const statusSnapshotPromise = reviewThreadSummaryPromise.then(
7928
+ (reviewThreadSummary2) => getGitHubPullRequestStatusSnapshot(octokit, scope.repository, pullRequestNumber, /* @__PURE__ */ new Map(), {
7929
+ reviewThreadSummary: reviewThreadSummary2
7930
+ })
7931
+ );
7932
+ const [reviewSummary, reviewThreadSummary, comments, linkedIssue, statusSnapshot] = await Promise.all([
7933
+ reviewSummaryPromise,
7934
+ reviewThreadSummaryPromise,
7935
+ listAllGitHubIssueComments(octokit, scope.repository, pullRequestNumber),
7936
+ cachedLinkedIssue ? Promise.resolve(cachedLinkedIssue) : (async () => {
7937
+ const issueLookup = await buildProjectPullRequestIssueLookup(ctx, scope);
7938
+ return resolveLinkedPaperclipIssueForPullRequest(
7939
+ pullRequestNumber,
7940
+ await listProjectPullRequestClosingIssues(octokit, scope.repository, pullRequestNumber),
7941
+ issueLookup
7942
+ );
7943
+ })(),
7944
+ statusSnapshotPromise
7945
+ ]);
7946
+ const author = buildProjectPullRequestPerson({
7947
+ login: pullRequest.user?.login,
7948
+ url: pullRequest.user?.html_url,
7949
+ avatarUrl: pullRequest.user?.avatar_url
7950
+ });
7951
+ const timeline = [];
7952
+ const trimmedBody = pullRequest.body?.trim() ?? "";
7953
+ if (trimmedBody) {
7954
+ timeline.push({
7955
+ id: `github-pull-request-${pullRequestNumber}-description`,
7956
+ kind: "description",
7957
+ author,
7958
+ createdAt: pullRequest.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
7959
+ body: trimmedBody
7960
+ });
7961
+ }
7962
+ for (const comment of comments) {
7963
+ timeline.push({
7964
+ id: `github-pull-request-${pullRequestNumber}-comment-${comment.id}`,
7965
+ kind: "comment",
7966
+ author: buildProjectPullRequestPerson({
7967
+ login: comment.authorLogin,
7968
+ url: comment.authorUrl,
7969
+ avatarUrl: comment.authorAvatarUrl
7970
+ }),
7971
+ createdAt: comment.createdAt ?? comment.updatedAt ?? pullRequest.updated_at ?? (/* @__PURE__ */ new Date()).toISOString(),
7972
+ body: comment.body
7973
+ });
7974
+ }
7975
+ timeline.sort(
7976
+ (left, right) => Date.parse(String(left.createdAt ?? "")) - Date.parse(String(right.createdAt ?? ""))
7977
+ );
7978
+ const checksStatus = statusSnapshot.ciState === "green" ? "passed" : statusSnapshot.ciState === "red" ? "failed" : "pending";
7979
+ const githubMergeable = pullRequest.mergeable === true;
7980
+ const reviewable = resolveProjectPullRequestReviewable({
7981
+ checksStatus,
7982
+ copilotUnresolvedReviewThreads: reviewThreadSummary.copilotUnresolvedReviewThreads,
7983
+ githubMergeable
7984
+ });
7985
+ const mergeable = resolveProjectPullRequestMergeable({
7986
+ checksStatus,
7987
+ reviewApprovals: reviewSummary.approvals,
7988
+ unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
7989
+ githubMergeable
7990
+ });
7991
+ return setCacheValue(
7992
+ activeProjectPullRequestDetailCache,
7993
+ detailCacheKey,
7994
+ {
7995
+ id: `github-pull-request-${scope.repository.owner}-${scope.repository.repo}-${pullRequestNumber}`,
7996
+ number: pullRequest.number,
7997
+ title: pullRequest.title,
7998
+ labels: normalizeGitHubIssueLabels(pullRequest.labels),
7999
+ author,
8000
+ assignees: (pullRequest.assignees ?? []).map((assignee) => buildProjectPullRequestPerson({
8001
+ login: assignee?.login,
8002
+ url: assignee?.html_url,
8003
+ avatarUrl: assignee?.avatar_url
8004
+ })),
8005
+ checksStatus,
8006
+ githubMergeable,
8007
+ reviewable,
8008
+ reviewApprovals: reviewSummary.approvals,
8009
+ reviewChangesRequested: reviewSummary.changesRequested,
8010
+ reviewCommentCount: typeof pullRequest.review_comments === "number" && pullRequest.review_comments >= 0 ? Math.floor(pullRequest.review_comments) : 0,
8011
+ unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
8012
+ copilotUnresolvedReviewThreads: reviewThreadSummary.copilotUnresolvedReviewThreads,
8013
+ commentsCount: typeof pullRequest.comments === "number" && pullRequest.comments >= 0 ? Math.floor(pullRequest.comments) : comments.length,
8014
+ createdAt: pullRequest.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
8015
+ updatedAt: pullRequest.updated_at ?? pullRequest.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
8016
+ ...linkedIssue?.paperclipIssueId ? { paperclipIssueId: linkedIssue.paperclipIssueId } : {},
8017
+ ...linkedIssue?.paperclipIssueKey ? { paperclipIssueKey: linkedIssue.paperclipIssueKey } : {},
8018
+ mergeable,
8019
+ status: getPullRequestApiState({
8020
+ state: pullRequest.state,
8021
+ merged: pullRequest.merged
8022
+ }),
8023
+ githubUrl: pullRequest.html_url,
8024
+ checksUrl: `${pullRequest.html_url}/checks`,
8025
+ reviewsUrl: `${pullRequest.html_url}/files`,
8026
+ reviewThreadsUrl: `${pullRequest.html_url}/files`,
8027
+ commentsUrl: pullRequest.html_url,
8028
+ baseBranch: pullRequest.base.ref,
8029
+ headBranch: pullRequest.head.ref,
8030
+ commits: typeof pullRequest.commits === "number" && pullRequest.commits >= 0 ? pullRequest.commits : 0,
8031
+ changedFiles: typeof pullRequest.changed_files === "number" && pullRequest.changed_files >= 0 ? pullRequest.changed_files : 0,
8032
+ timeline
8033
+ },
8034
+ PROJECT_PULL_REQUEST_DETAIL_CACHE_TTL_MS
8035
+ );
8036
+ }
8037
+ async function createProjectPullRequestPaperclipIssue(ctx, input) {
8038
+ const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
8039
+ if (!pullRequestNumber) {
8040
+ throw new Error("pullRequestNumber is required.");
8041
+ }
8042
+ if (!ctx.issues || typeof ctx.issues.create !== "function") {
8043
+ throw new Error("This Paperclip runtime does not expose plugin issue creation yet.");
8044
+ }
8045
+ const scope = await requireProjectPullRequestScope(ctx, input);
8046
+ const octokit = await createGitHubToolOctokit(ctx);
8047
+ const pullRequestResponse = await octokit.rest.pulls.get({
8048
+ owner: scope.repository.owner,
8049
+ repo: scope.repository.repo,
8050
+ pull_number: pullRequestNumber,
8051
+ headers: {
8052
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
8053
+ }
8054
+ });
8055
+ const pullRequest = pullRequestResponse.data;
8056
+ const pullRequestUrl = pullRequest.html_url;
8057
+ const existingLinks = await listGitHubPullRequestLinkRecords(ctx, {
8058
+ externalId: pullRequestUrl
8059
+ });
8060
+ const existingLink = existingLinks.find(
8061
+ (record) => record.data.githubPullRequestNumber === pullRequestNumber && record.data.repositoryUrl === scope.repository.url && (!record.data.companyId || record.data.companyId === scope.companyId) && (!record.data.paperclipProjectId || record.data.paperclipProjectId === scope.projectId)
8062
+ );
8063
+ const existingIssue = existingLink ? await ctx.issues.get(existingLink.paperclipIssueId, scope.companyId) : null;
8064
+ if (existingLink && existingIssue) {
8065
+ return {
8066
+ paperclipIssueId: existingIssue.id,
8067
+ ...existingIssue.identifier ? { paperclipIssueKey: existingIssue.identifier } : {},
8068
+ alreadyLinked: true
8069
+ };
8070
+ }
8071
+ const requestedTitle = typeof input.title === "string" && input.title.trim() ? input.title.trim() : pullRequest.title.trim();
8072
+ const createdIssue = await ctx.issues.create({
8073
+ companyId: scope.companyId,
8074
+ projectId: scope.projectId,
8075
+ title: requestedTitle,
8076
+ description: buildPaperclipIssueDescriptionFromPullRequest({
8077
+ repository: scope.repository,
8078
+ pullRequestNumber,
8079
+ pullRequestUrl,
8080
+ body: pullRequest.body
8081
+ })
8082
+ });
8083
+ const resolvedIssue = await ctx.issues.get(createdIssue.id, scope.companyId) ?? createdIssue;
8084
+ await upsertGitHubPullRequestLinkRecord(ctx, {
8085
+ companyId: scope.companyId,
8086
+ projectId: scope.projectId,
8087
+ issueId: resolvedIssue.id,
8088
+ repositoryUrl: scope.repository.url,
8089
+ pullRequestNumber,
8090
+ pullRequestUrl,
8091
+ pullRequestTitle: pullRequest.title,
8092
+ pullRequestState: getPullRequestApiState({
8093
+ state: pullRequest.state,
8094
+ merged: pullRequest.merged
8095
+ }) === "open" ? "open" : "closed"
8096
+ });
8097
+ invalidateProjectPullRequestCaches(scope);
8098
+ return {
8099
+ paperclipIssueId: resolvedIssue.id,
8100
+ ...resolvedIssue.identifier ? { paperclipIssueKey: resolvedIssue.identifier } : {},
8101
+ alreadyLinked: false
8102
+ };
8103
+ }
8104
+ async function refreshProjectPullRequests(ctx, input) {
8105
+ const scope = await requireProjectPullRequestScope(ctx, input);
8106
+ invalidateProjectPullRequestCaches(scope);
8107
+ return {
8108
+ status: "refreshed",
8109
+ projectId: scope.projectId,
8110
+ repositoryUrl: scope.repository.url,
8111
+ refreshedAt: (/* @__PURE__ */ new Date()).toISOString()
8112
+ };
8113
+ }
8114
+ async function updateProjectPullRequestBranch(ctx, input) {
8115
+ const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
8116
+ if (!pullRequestNumber) {
8117
+ throw new Error("pullRequestNumber is required.");
8118
+ }
8119
+ const scope = await requireProjectPullRequestScope(ctx, input);
8120
+ const octokit = await createGitHubToolOctokit(ctx);
8121
+ const pullRequestResponse = await octokit.rest.pulls.get({
8122
+ owner: scope.repository.owner,
8123
+ repo: scope.repository.repo,
8124
+ pull_number: pullRequestNumber,
8125
+ headers: {
8126
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
8127
+ }
8128
+ });
8129
+ const pullRequest = pullRequestResponse.data;
8130
+ const githubUrl = pullRequest.html_url ?? `${scope.repository.url}/pull/${pullRequestNumber}`;
8131
+ const pullRequestState = getPullRequestApiState({
8132
+ state: pullRequest.state,
8133
+ merged: pullRequest.merged
8134
+ });
8135
+ if (pullRequestState !== "open") {
8136
+ throw new Error("Only open pull requests can be updated with the base branch.");
8137
+ }
8138
+ const behindBy = await getGitHubPullRequestBehindCount(octokit, scope.repository, {
8139
+ baseBranch: pullRequest.base.ref,
8140
+ headBranch: pullRequest.head.ref,
8141
+ headRepositoryOwner: pullRequest.head.repo?.owner?.login
8142
+ });
8143
+ if (typeof behindBy === "number" && behindBy <= 0) {
8144
+ invalidateProjectPullRequestCaches(scope);
8145
+ return {
8146
+ githubUrl,
8147
+ status: "already_up_to_date"
8148
+ };
8149
+ }
8150
+ const mergeableState = typeof pullRequest.mergeable_state === "string" ? pullRequest.mergeable_state.trim().toLowerCase() : "";
8151
+ if ((mergeableState === "dirty" || pullRequest.mergeable === false) && (behindBy === null || behindBy > 0)) {
8152
+ throw new Error("This pull request needs conflict resolution before it can be updated with the base branch.");
8153
+ }
8154
+ try {
8155
+ await octokit.request("PUT /repos/{owner}/{repo}/pulls/{pull_number}/update-branch", {
8156
+ owner: scope.repository.owner,
8157
+ repo: scope.repository.repo,
8158
+ pull_number: pullRequestNumber,
8159
+ ...typeof pullRequest.head.sha === "string" && pullRequest.head.sha.trim() ? { expected_head_sha: pullRequest.head.sha.trim() } : {},
8160
+ headers: {
8161
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
8162
+ }
8163
+ });
8164
+ } catch (error) {
8165
+ throw buildGitHubPullRequestWriteActionError({
8166
+ action: "update_branch",
8167
+ error,
8168
+ repositoryLabel: `${scope.repository.owner}/${scope.repository.repo}`
8169
+ });
8170
+ }
8171
+ invalidateProjectPullRequestCaches(scope);
8172
+ return {
8173
+ githubUrl,
8174
+ status: "update_requested"
8175
+ };
8176
+ }
8177
+ async function mergeProjectPullRequest(ctx, input) {
8178
+ const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
8179
+ if (!pullRequestNumber) {
8180
+ throw new Error("pullRequestNumber is required.");
8181
+ }
8182
+ const scope = await requireProjectPullRequestScope(ctx, input);
8183
+ const octokit = await createGitHubToolOctokit(ctx);
8184
+ const response = await octokit.rest.pulls.merge({
8185
+ owner: scope.repository.owner,
8186
+ repo: scope.repository.repo,
8187
+ pull_number: pullRequestNumber,
8188
+ headers: {
8189
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
8190
+ }
8191
+ });
8192
+ if (response.data.merged !== true) {
8193
+ throw new Error(response.data.message ?? `GitHub did not merge pull request #${pullRequestNumber}.`);
8194
+ }
8195
+ invalidateProjectPullRequestCaches(scope);
8196
+ return {
8197
+ githubUrl: `${scope.repository.url}/pull/${pullRequestNumber}`,
8198
+ status: "merged"
8199
+ };
8200
+ }
8201
+ async function closeProjectPullRequest(ctx, input) {
8202
+ const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
8203
+ if (!pullRequestNumber) {
8204
+ throw new Error("pullRequestNumber is required.");
8205
+ }
8206
+ const scope = await requireProjectPullRequestScope(ctx, input);
8207
+ const octokit = await createGitHubToolOctokit(ctx);
8208
+ const response = await octokit.rest.pulls.update({
8209
+ owner: scope.repository.owner,
8210
+ repo: scope.repository.repo,
8211
+ pull_number: pullRequestNumber,
8212
+ state: "closed",
8213
+ headers: {
8214
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
8215
+ }
8216
+ });
8217
+ invalidateProjectPullRequestCaches(scope);
4969
8218
  return {
4970
- content,
4971
- data
8219
+ githubUrl: response.data.html_url ?? `${scope.repository.url}/pull/${pullRequestNumber}`,
8220
+ status: getPullRequestApiState({
8221
+ state: response.data.state,
8222
+ merged: response.data.merged
8223
+ })
4972
8224
  };
4973
8225
  }
4974
- function buildToolErrorResult(error) {
4975
- const rateLimitPause = getGitHubRateLimitPauseDetails(error);
4976
- if (rateLimitPause) {
4977
- const resourceLabel = formatGitHubRateLimitResource(rateLimitPause.resource) ?? "GitHub API";
4978
- return {
4979
- error: `${resourceLabel} rate limit reached. Wait until ${formatUtcTimestamp(rateLimitPause.resetAt)} before retrying.`
4980
- };
8226
+ async function addProjectPullRequestComment(ctx, input) {
8227
+ const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
8228
+ if (!pullRequestNumber) {
8229
+ throw new Error("pullRequestNumber is required.");
8230
+ }
8231
+ const body = typeof input.body === "string" ? input.body.trim() : "";
8232
+ if (!body) {
8233
+ throw new Error("Comment body cannot be empty.");
4981
8234
  }
8235
+ const scope = await requireProjectPullRequestScope(ctx, input);
8236
+ const octokit = await createGitHubToolOctokit(ctx);
8237
+ const response = await createProjectPullRequestGitHubComment(octokit, scope, pullRequestNumber, body);
8238
+ invalidateProjectPullRequestCaches(scope);
4982
8239
  return {
4983
- error: getErrorMessage(error)
8240
+ commentId: response.id,
8241
+ commentUrl: response.htmlUrl ?? `${scope.repository.url}/pull/${pullRequestNumber}`
4984
8242
  };
4985
8243
  }
4986
- async function executeGitHubTool(fn) {
8244
+ async function createProjectPullRequestGitHubComment(octokit, scope, pullRequestNumber, body) {
8245
+ let response;
4987
8246
  try {
4988
- return await fn();
8247
+ response = await octokit.rest.issues.createComment({
8248
+ owner: scope.repository.owner,
8249
+ repo: scope.repository.repo,
8250
+ issue_number: pullRequestNumber,
8251
+ body,
8252
+ headers: {
8253
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
8254
+ }
8255
+ });
4989
8256
  } catch (error) {
4990
- return buildToolErrorResult(error);
4991
- }
4992
- }
4993
- async function createGitHubToolOctokit(ctx) {
4994
- const token = (await resolveGithubToken(ctx)).trim();
4995
- if (!token) {
4996
- throw new Error(MISSING_GITHUB_TOKEN_SYNC_MESSAGE);
8257
+ throw buildGitHubPullRequestWriteActionError({
8258
+ action: "comment",
8259
+ error,
8260
+ repositoryLabel: `${scope.repository.owner}/${scope.repository.repo}`
8261
+ });
4997
8262
  }
4998
- return new Octokit({ auth: token });
8263
+ return {
8264
+ id: response.data.id,
8265
+ ...response.data.html_url ? { htmlUrl: response.data.html_url } : {}
8266
+ };
4999
8267
  }
5000
- async function resolveRepositoryFromRunContext(ctx, runCtx) {
5001
- const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
5002
- const mappings = getSyncableMappingsForTarget(settings.mappings, {
5003
- kind: "project",
5004
- companyId: runCtx.companyId,
5005
- projectId: runCtx.projectId,
5006
- displayLabel: "project"
8268
+ function buildProjectPullRequestCopilotComment(action, options) {
8269
+ const baseBranch = typeof options?.baseBranch === "string" ? options.baseBranch.trim() : "";
8270
+ const baseBranchLabel = baseBranch ? `\`${baseBranch}\`` : "the base branch";
8271
+ switch (action) {
8272
+ case "fix_ci":
8273
+ return "@copilot Please investigate the failing CI on this pull request, push the smallest fix needed to this branch, and summarize the root cause and changes.";
8274
+ case "rebase":
8275
+ return `@copilot This pull request is behind ${baseBranchLabel} and needs conflict resolution. Please bring this branch up to date with ${baseBranchLabel}, resolve the conflicts, push the updated branch, and summarize any non-trivial conflict decisions.`;
8276
+ case "address_review_feedback":
8277
+ return "@copilot Please address the unresolved review feedback on this pull request, push the necessary updates to this branch, and summarize what you changed.";
8278
+ case "review":
8279
+ return "@copilot Please review this pull request and leave feedback as GitHub review comments. Focus on correctness, regressions, and missing tests.";
8280
+ }
8281
+ }
8282
+ var COPILOT_PULL_REQUEST_REVIEWER_LOGIN = "copilot-pull-request-reviewer[bot]";
8283
+ async function requestProjectPullRequestCopilotAction(ctx, input) {
8284
+ const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
8285
+ if (!pullRequestNumber) {
8286
+ throw new Error("pullRequestNumber is required.");
8287
+ }
8288
+ const action = normalizeProjectPullRequestCopilotAction(input.action);
8289
+ if (!action) {
8290
+ throw new Error('action must be one of "fix_ci", "rebase", "address_review_feedback", or "review".');
8291
+ }
8292
+ const scope = await requireProjectPullRequestScope(ctx, input);
8293
+ const octokit = await createGitHubToolOctokit(ctx);
8294
+ const pullRequestResponse = await octokit.rest.pulls.get({
8295
+ owner: scope.repository.owner,
8296
+ repo: scope.repository.repo,
8297
+ pull_number: pullRequestNumber,
8298
+ headers: {
8299
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
8300
+ }
5007
8301
  });
5008
- const repositories = [
5009
- ...new Map(
5010
- mappings.map((mapping) => {
5011
- const repository = parseRepositoryReference(mapping.repositoryUrl);
5012
- return repository ? [repository.url, repository] : null;
5013
- }).filter((entry) => entry !== null)
5014
- ).values()
5015
- ];
5016
- if (repositories.length === 1) {
5017
- return repositories[0];
5018
- }
5019
- if (repositories.length === 0) {
5020
- throw new Error("No GitHub repository is mapped to the current Paperclip project. Pass repository explicitly.");
5021
- }
5022
- throw new Error("Multiple GitHub repositories are mapped to the current Paperclip project. Pass repository explicitly.");
5023
- }
5024
- async function resolveGitHubToolRepository(ctx, runCtx, input) {
5025
- const explicitRepository = normalizeOptionalToolString(input.repository);
5026
- if (explicitRepository) {
5027
- return requireRepositoryReference(explicitRepository);
5028
- }
5029
- const paperclipIssueId = normalizeOptionalToolString(input.paperclipIssueId);
5030
- if (paperclipIssueId) {
5031
- const link = await resolvePaperclipIssueGitHubLink(ctx, paperclipIssueId, runCtx.companyId);
5032
- if (!link) {
5033
- throw new Error("This Paperclip issue is not linked to a GitHub issue yet. Pass repository explicitly.");
8302
+ const pullRequest = pullRequestResponse.data;
8303
+ const githubUrl = pullRequest.html_url ?? `${scope.repository.url}/pull/${pullRequestNumber}`;
8304
+ const pullRequestState = getPullRequestApiState({
8305
+ state: pullRequest.state,
8306
+ merged: pullRequest.merged
8307
+ });
8308
+ if (pullRequestState !== "open") {
8309
+ throw new Error("Only open pull requests can request Copilot actions.");
8310
+ }
8311
+ switch (action) {
8312
+ case "fix_ci": {
8313
+ const ciState = await getGitHubPullRequestCiState(octokit, scope.repository, pullRequestNumber);
8314
+ if (ciState !== "red") {
8315
+ throw new Error("This pull request does not currently have failing checks.");
8316
+ }
8317
+ break;
5034
8318
  }
5035
- return requireRepositoryReference(link.repositoryUrl);
8319
+ case "rebase": {
8320
+ const behindBy = await getGitHubPullRequestBehindCount(octokit, scope.repository, {
8321
+ baseBranch: pullRequest.base.ref,
8322
+ headBranch: pullRequest.head.ref,
8323
+ headRepositoryOwner: pullRequest.head.repo?.owner?.login
8324
+ });
8325
+ if (typeof behindBy === "number" && behindBy <= 0) {
8326
+ throw new Error("This pull request is already up to date with the base branch.");
8327
+ }
8328
+ const mergeableState = typeof pullRequest.mergeable_state === "string" ? pullRequest.mergeable_state.trim().toLowerCase() : "";
8329
+ const needsConflictResolution = (mergeableState === "dirty" || pullRequest.mergeable === false) && (behindBy === null || behindBy > 0);
8330
+ if (!needsConflictResolution) {
8331
+ throw new Error("This pull request can be updated without Copilot. Use Update branch instead.");
8332
+ }
8333
+ break;
8334
+ }
8335
+ case "address_review_feedback": {
8336
+ const reviewThreadSummary = await getOrLoadCachedGitHubPullRequestReviewThreadSummary(
8337
+ octokit,
8338
+ scope.repository,
8339
+ pullRequestNumber
8340
+ );
8341
+ if (reviewThreadSummary.unresolvedReviewThreads <= 0) {
8342
+ throw new Error("This pull request does not currently have unresolved review threads.");
8343
+ }
8344
+ break;
8345
+ }
8346
+ case "review":
8347
+ break;
5036
8348
  }
5037
- return resolveRepositoryFromRunContext(ctx, runCtx);
5038
- }
5039
- async function resolveGitHubIssueToolTarget(ctx, runCtx, input) {
5040
- const paperclipIssueId = normalizeOptionalToolString(input.paperclipIssueId);
5041
- if (paperclipIssueId) {
5042
- const link = await resolvePaperclipIssueGitHubLink(ctx, paperclipIssueId, runCtx.companyId);
5043
- if (!link) {
5044
- throw new Error("This Paperclip issue is not linked to a GitHub issue yet.");
8349
+ if (action === "review") {
8350
+ const pullRequestId = typeof pullRequest.node_id === "string" && pullRequest.node_id.trim() ? pullRequest.node_id.trim() : null;
8351
+ if (!pullRequestId) {
8352
+ throw new Error("GitHub did not return a pull request node id for this review request.");
5045
8353
  }
5046
- const repository2 = assertExplicitRepositoryMatchesLinkedRepository(
5047
- input.repository,
5048
- link.repositoryUrl,
5049
- "The provided repository does not match the linked GitHub repository for this Paperclip issue."
5050
- );
5051
- const explicitIssueNumber = normalizeToolPositiveInteger(input.issueNumber);
5052
- if (explicitIssueNumber !== void 0 && explicitIssueNumber !== link.githubIssueNumber) {
5053
- throw new Error("The provided issue number does not match the linked GitHub issue for this Paperclip issue.");
8354
+ let response;
8355
+ try {
8356
+ response = await octokit.graphql(
8357
+ GITHUB_REQUEST_PULL_REQUEST_COPILOT_REVIEW_MUTATION,
8358
+ {
8359
+ pullRequestId,
8360
+ botLogins: [COPILOT_PULL_REQUEST_REVIEWER_LOGIN]
8361
+ }
8362
+ );
8363
+ } catch (error) {
8364
+ throw buildGitHubPullRequestWriteActionError({
8365
+ action: "review",
8366
+ error,
8367
+ repositoryLabel: `${scope.repository.owner}/${scope.repository.repo}`
8368
+ });
5054
8369
  }
8370
+ const requestedReviewers = (response.requestReviews?.requestedReviewers?.edges ?? []).map((edge) => edge?.node?.login?.trim() ?? "").filter(Boolean);
8371
+ invalidateProjectPullRequestCaches(scope);
5055
8372
  return {
5056
- repository: repository2,
5057
- issueNumber: link.githubIssueNumber,
5058
- paperclipIssueId,
5059
- githubIssueId: link.githubIssueId,
5060
- githubIssueUrl: link.githubIssueUrl
8373
+ action,
8374
+ actionLabel: getProjectPullRequestCopilotActionLabel(action),
8375
+ requestedReviewer: requestedReviewers[0] ?? COPILOT_PULL_REQUEST_REVIEWER_LOGIN,
8376
+ githubUrl: response.requestReviews?.pullRequest?.url ?? githubUrl
5061
8377
  };
5062
8378
  }
5063
- const repository = await resolveGitHubToolRepository(ctx, runCtx, input);
5064
- const issueNumber = normalizeToolPositiveInteger(input.issueNumber);
5065
- if (issueNumber === void 0) {
5066
- throw new Error("issueNumber is required when paperclipIssueId is not provided.");
5067
- }
8379
+ const comment = await createProjectPullRequestGitHubComment(
8380
+ octokit,
8381
+ scope,
8382
+ pullRequestNumber,
8383
+ buildProjectPullRequestCopilotComment(action, {
8384
+ baseBranch: pullRequest.base.ref
8385
+ })
8386
+ );
8387
+ invalidateProjectPullRequestCaches(scope);
5068
8388
  return {
5069
- repository,
5070
- issueNumber
8389
+ action,
8390
+ actionLabel: getProjectPullRequestCopilotActionLabel(action),
8391
+ commentId: comment.id,
8392
+ commentUrl: comment.htmlUrl ?? githubUrl,
8393
+ githubUrl
5071
8394
  };
5072
8395
  }
5073
- async function resolveGitHubPullRequestToolTarget(ctx, runCtx, input) {
5074
- const paperclipIssueId = normalizeOptionalToolString(input.paperclipIssueId);
5075
- if (paperclipIssueId) {
5076
- const link = await resolvePaperclipIssueGitHubLink(ctx, paperclipIssueId, runCtx.companyId);
5077
- if (!link) {
5078
- throw new Error("This Paperclip issue is not linked to GitHub yet.");
5079
- }
5080
- const repository2 = assertExplicitRepositoryMatchesLinkedRepository(
5081
- input.repository,
5082
- link.repositoryUrl,
5083
- "repository must match the GitHub repository linked to the provided Paperclip issue."
5084
- );
5085
- const explicitPullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
5086
- if (explicitPullRequestNumber !== void 0) {
5087
- return {
5088
- repository: repository2,
5089
- pullRequestNumber: explicitPullRequestNumber,
5090
- paperclipIssueId
5091
- };
5092
- }
5093
- if (link.linkedPullRequestNumbers.length === 1) {
5094
- return {
5095
- repository: repository2,
5096
- pullRequestNumber: link.linkedPullRequestNumbers[0],
5097
- paperclipIssueId
5098
- };
5099
- }
5100
- throw new Error("pullRequestNumber is required unless the linked Paperclip issue has exactly one linked pull request.");
5101
- }
5102
- const repository = await resolveGitHubToolRepository(ctx, runCtx, input);
8396
+ async function reviewProjectPullRequest(ctx, input) {
5103
8397
  const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
5104
- if (pullRequestNumber === void 0) {
5105
- throw new Error("pullRequestNumber is required when paperclipIssueId is not provided.");
8398
+ if (!pullRequestNumber) {
8399
+ throw new Error("pullRequestNumber is required.");
8400
+ }
8401
+ const reviewType = input.review === "approve" ? "APPROVE" : input.review === "request_changes" ? "REQUEST_CHANGES" : void 0;
8402
+ if (!reviewType) {
8403
+ throw new Error('review must be "approve" or "request_changes".');
8404
+ }
8405
+ const body = typeof input.body === "string" ? input.body.trim() : "";
8406
+ const scope = await requireProjectPullRequestScope(ctx, input);
8407
+ const octokit = await createGitHubToolOctokit(ctx);
8408
+ let response;
8409
+ try {
8410
+ response = await octokit.rest.pulls.createReview({
8411
+ owner: scope.repository.owner,
8412
+ repo: scope.repository.repo,
8413
+ pull_number: pullRequestNumber,
8414
+ event: reviewType,
8415
+ ...body ? { body } : {},
8416
+ headers: {
8417
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
8418
+ }
8419
+ });
8420
+ } catch (error) {
8421
+ throw buildGitHubPullRequestWriteActionError({
8422
+ action: "review",
8423
+ error,
8424
+ repositoryLabel: `${scope.repository.owner}/${scope.repository.repo}`,
8425
+ reviewType,
8426
+ body
8427
+ });
5106
8428
  }
8429
+ invalidateProjectPullRequestCaches(scope);
5107
8430
  return {
5108
- repository,
5109
- pullRequestNumber
8431
+ reviewId: response.data.id,
8432
+ review: reviewType === "APPROVE" ? "approved" : "changes_requested",
8433
+ reviewUrl: response.data.html_url ?? `${scope.repository.url}/pull/${pullRequestNumber}`
5110
8434
  };
5111
8435
  }
5112
- function formatAiAuthorshipFooter(llmModel) {
5113
- return `
5114
-
5115
- ---
5116
- ${AI_AUTHORED_COMMENT_FOOTER_PREFIX}${llmModel.trim()}.`;
5117
- }
5118
- function appendAiAuthorshipFooter(body, llmModel) {
5119
- const trimmedBody = body.trim();
5120
- if (!trimmedBody) {
5121
- throw new Error("Comment body cannot be empty.");
5122
- }
5123
- const trimmedModel = llmModel.trim();
5124
- if (!trimmedModel) {
5125
- throw new Error("llmModel is required when posting a GitHub comment.");
8436
+ function isFailedCheckSuiteConclusion(value) {
8437
+ switch (value?.trim().toLowerCase()) {
8438
+ case "action_required":
8439
+ case "cancelled":
8440
+ case "failure":
8441
+ case "stale":
8442
+ case "timed_out":
8443
+ return true;
8444
+ default:
8445
+ return false;
5126
8446
  }
5127
- return `${trimmedBody}${formatAiAuthorshipFooter(trimmedModel)}`;
5128
8447
  }
5129
- async function listAllGitHubIssueComments(octokit, repository, issueNumber) {
5130
- const comments = [];
5131
- for await (const response of octokit.paginate.iterator(octokit.rest.issues.listComments, {
5132
- owner: repository.owner,
5133
- repo: repository.repo,
5134
- issue_number: issueNumber,
5135
- per_page: 100,
8448
+ async function rerunProjectPullRequestCi(ctx, input) {
8449
+ const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
8450
+ if (!pullRequestNumber) {
8451
+ throw new Error("pullRequestNumber is required.");
8452
+ }
8453
+ const scope = await requireProjectPullRequestScope(ctx, input);
8454
+ const octokit = await createGitHubToolOctokit(ctx);
8455
+ const pullRequestResponse = await octokit.rest.pulls.get({
8456
+ owner: scope.repository.owner,
8457
+ repo: scope.repository.repo,
8458
+ pull_number: pullRequestNumber,
5136
8459
  headers: {
5137
8460
  "X-GitHub-Api-Version": GITHUB_API_VERSION
5138
8461
  }
5139
- })) {
5140
- for (const comment of response.data) {
5141
- comments.push({
5142
- id: comment.id,
5143
- body: comment.body ?? "",
5144
- url: comment.html_url ?? void 0,
5145
- authorLogin: normalizeGitHubUserLogin(comment.user?.login),
5146
- createdAt: comment.created_at ?? void 0,
5147
- updatedAt: comment.updated_at ?? void 0
5148
- });
8462
+ });
8463
+ const checkSuitesResponse = await octokit.rest.checks.listSuitesForRef({
8464
+ owner: scope.repository.owner,
8465
+ repo: scope.repository.repo,
8466
+ ref: pullRequestResponse.data.head.sha,
8467
+ per_page: 100,
8468
+ headers: {
8469
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
5149
8470
  }
8471
+ });
8472
+ const rerunnableSuites = checkSuitesResponse.data.check_suites.filter(
8473
+ (suite) => suite.status === "completed" && isFailedCheckSuiteConclusion(suite.conclusion)
8474
+ );
8475
+ if (rerunnableSuites.length === 0) {
8476
+ throw new Error("No failed GitHub check suites are available to re-run for this pull request.");
8477
+ }
8478
+ for (const suite of rerunnableSuites) {
8479
+ await octokit.rest.checks.rerequestSuite({
8480
+ owner: scope.repository.owner,
8481
+ repo: scope.repository.repo,
8482
+ check_suite_id: suite.id,
8483
+ headers: {
8484
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
8485
+ }
8486
+ });
5150
8487
  }
5151
- return comments;
8488
+ invalidateProjectPullRequestCaches(scope);
8489
+ return {
8490
+ rerunCheckSuiteCount: rerunnableSuites.length,
8491
+ githubUrl: `${scope.repository.url}/pull/${pullRequestNumber}/checks`
8492
+ };
5152
8493
  }
5153
8494
  async function listAllPullRequestFiles(octokit, repository, pullRequestNumber) {
5154
8495
  const files = [];
@@ -6619,6 +9960,30 @@ var plugin = definePlugin({
6619
9960
  const record = input && typeof input === "object" ? input : {};
6620
9961
  return buildToolbarSyncState(ctx, record);
6621
9962
  });
9963
+ ctx.data.register("settings.tokenPermissionAudit", async (input) => {
9964
+ const record = input && typeof input === "object" ? input : {};
9965
+ return buildSettingsTokenPermissionAuditData(ctx, record);
9966
+ });
9967
+ ctx.data.register("project.pullRequests.page", async (input) => {
9968
+ const record = input && typeof input === "object" ? input : {};
9969
+ return buildProjectPullRequestsPageData(ctx, record);
9970
+ });
9971
+ ctx.data.register("project.pullRequests.metrics", async (input) => {
9972
+ const record = input && typeof input === "object" ? input : {};
9973
+ return buildProjectPullRequestMetricsData(ctx, record);
9974
+ });
9975
+ ctx.data.register("project.pullRequests.count", async (input) => {
9976
+ const record = input && typeof input === "object" ? input : {};
9977
+ return buildProjectPullRequestCountData(ctx, record);
9978
+ });
9979
+ ctx.data.register("project.pullRequests.detail", async (input) => {
9980
+ const record = input && typeof input === "object" ? input : {};
9981
+ return buildProjectPullRequestDetailData(ctx, record);
9982
+ });
9983
+ ctx.data.register("project.pullRequests.paperclipIssue", async (input) => {
9984
+ const record = input && typeof input === "object" ? input : {};
9985
+ return buildProjectPullRequestPaperclipIssueData(ctx, record);
9986
+ });
6622
9987
  ctx.data.register("issue.githubDetails", async (input) => {
6623
9988
  const record = input && typeof input === "object" ? input : {};
6624
9989
  return buildIssueGitHubDetails(ctx, record);
@@ -6684,6 +10049,7 @@ var plugin = definePlugin({
6684
10049
  });
6685
10050
  await ctx.state.set(SETTINGS_SCOPE, next);
6686
10051
  await ctx.state.set(SYNC_STATE_SCOPE, next.syncState);
10052
+ clearGitHubRepositoryTokenCapabilityAudits();
6687
10053
  return {
6688
10054
  ...getPublicSettingsForScope(next, requestedCompanyId),
6689
10055
  availableAssignees: requestedCompanyId ? await listAvailableAssignees(ctx, requestedCompanyId) : []
@@ -6733,6 +10099,42 @@ var plugin = definePlugin({
6733
10099
  }
6734
10100
  return validateGithubToken(trimmedToken);
6735
10101
  });
10102
+ ctx.actions.register("project.pullRequests.createIssue", async (input) => {
10103
+ const record = input && typeof input === "object" ? input : {};
10104
+ return createProjectPullRequestPaperclipIssue(ctx, record);
10105
+ });
10106
+ ctx.actions.register("project.pullRequests.refresh", async (input) => {
10107
+ const record = input && typeof input === "object" ? input : {};
10108
+ return refreshProjectPullRequests(ctx, record);
10109
+ });
10110
+ ctx.actions.register("project.pullRequests.updateBranch", async (input) => {
10111
+ const record = input && typeof input === "object" ? input : {};
10112
+ return updateProjectPullRequestBranch(ctx, record);
10113
+ });
10114
+ ctx.actions.register("project.pullRequests.requestCopilotAction", async (input) => {
10115
+ const record = input && typeof input === "object" ? input : {};
10116
+ return requestProjectPullRequestCopilotAction(ctx, record);
10117
+ });
10118
+ ctx.actions.register("project.pullRequests.merge", async (input) => {
10119
+ const record = input && typeof input === "object" ? input : {};
10120
+ return mergeProjectPullRequest(ctx, record);
10121
+ });
10122
+ ctx.actions.register("project.pullRequests.close", async (input) => {
10123
+ const record = input && typeof input === "object" ? input : {};
10124
+ return closeProjectPullRequest(ctx, record);
10125
+ });
10126
+ ctx.actions.register("project.pullRequests.addComment", async (input) => {
10127
+ const record = input && typeof input === "object" ? input : {};
10128
+ return addProjectPullRequestComment(ctx, record);
10129
+ });
10130
+ ctx.actions.register("project.pullRequests.review", async (input) => {
10131
+ const record = input && typeof input === "object" ? input : {};
10132
+ return reviewProjectPullRequest(ctx, record);
10133
+ });
10134
+ ctx.actions.register("project.pullRequests.rerunCi", async (input) => {
10135
+ const record = input && typeof input === "object" ? input : {};
10136
+ return rerunProjectPullRequestCi(ctx, record);
10137
+ });
6736
10138
  ctx.actions.register("sync.runNow", async (input) => {
6737
10139
  const waitForCompletion = input && typeof input === "object" && "waitForCompletion" in input ? Boolean(input.waitForCompletion) : false;
6738
10140
  const paperclipApiBaseUrl = input && typeof input === "object" && "paperclipApiBaseUrl" in input ? input.paperclipApiBaseUrl : void 0;