paperclip-github-plugin 0.8.3 → 0.8.5

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/README.md CHANGED
@@ -159,8 +159,8 @@ When the local Paperclip API is available, the plugin also syncs labels by name,
159
159
  | Open issue with no linked pull request, created by a repository maintainer | `todo` on first import |
160
160
  | Open issue with no linked pull request | Configured default status, which defaults to `backlog` |
161
161
  | Open issue with a linked pull request and unfinished CI | `in_progress` |
162
- | Open issue with failing CI, a non-mergeable linked pull request, requested changes, or unresolved review threads | `todo`, or `in_progress` when GitHub Sync can hand the work back to an executor |
163
- | Open issue with green CI, a merge-ready linked pull request, no requested changes, and all review threads resolved | `in_review` |
162
+ | Open issue with failing CI, a non-mergeable linked pull request, or unresolved review threads | `todo`, or `in_progress` when GitHub Sync can hand the work back to an executor |
163
+ | Open issue with green CI, a merge-ready linked pull request, and all review threads resolved | `in_review` |
164
164
  | Closed issue completed as finished work | `done` |
165
165
  | Closed issue closed as `not_planned` or `duplicate` | `cancelled` |
166
166
 
@@ -170,7 +170,8 @@ Additional behavior:
170
170
  - If the Paperclip host initially creates that imported maintainer issue in `backlog`, GitHub Sync promotes it to `todo` without replacing the configured default assignee with the executor handoff assignee, so triage ownership stays intact.
171
171
  - When Paperclip board access is connected for a company, the advanced assignee dropdowns list both company agents and `Me` for the connected board user.
172
172
  - Newly imported issues that finish sync in `todo` and are assigned to an agent enqueue an assignee wakeup so the agent can pick them up promptly.
173
- - For linked pull requests, GitHub Sync treats merge-conflict, behind-branch, blocked, draft, unstable merge states, and requested-changes review decisions as executor work, while merge-ready states such as `CLEAN` and `HAS_HOOKS` can move work into `in_review` when CI is green and there are no requested changes or unresolved review threads.
173
+ - For linked pull requests, GitHub Sync treats merge-conflict, behind-branch, blocked, draft, unstable merge states, and unresolved review threads as executor work, while merge-ready states such as `CLEAN` and `HAS_HOOKS` can move work into `in_review` when CI is green and review threads are resolved. A stale aggregate `CHANGES_REQUESTED` review decision alone does not move that maintainer wait back to active execution. Transient `UNKNOWN` mergeability also does not move an already `in_review` maintainer wait back to active execution when CI is green and review threads are resolved.
174
+ - Imported issues that are already `blocked` stay `blocked` while any first-class `blockedBy` issue is still non-terminal, even if the linked GitHub pull request is otherwise green and review-ready.
174
175
  - When sync moves work into `in_review`, GitHub Sync first follows the Paperclip issue execution policy's current reviewer or approver when that stage is visible on the issue. If Paperclip exposes an internal review or approval stage but not yet the participant, the plugin falls back to the configured reviewer or approver handoff assignee. If the transition is only a healthy linked-PR wait with no visible internal review or approval stage, GitHub Sync leaves the issue unassigned so it can wait on normal maintainer review without waking an internal owner.
175
176
  - When sync moves work back into active execution, GitHub Sync first follows the Paperclip issue execution policy `returnAssignee` when it is available. Otherwise it falls back to the configured executor handoff assignee and then to the default imported assignee.
176
177
  - Sync-driven handoffs to agent assignees best-effort enqueue an explicit wakeup so the next reviewer, approver, or executor can pick the issue up even when their agent is not running heartbeats.
package/dist/manifest.js CHANGED
@@ -535,7 +535,7 @@ var COMPANY_METRIC_API_ROUTE_URL_PATH = `/api/plugins/${GITHUB_SYNC_PLUGIN_ID}/a
535
535
  var require2 = createRequire(import.meta.url);
536
536
  var packageJson = require2("../package.json");
537
537
  var SCHEDULE_TICK_CRON = "* * * * *";
538
- var MANIFEST_VERSION = "0.8.3"?.trim() || typeof packageJson.version === "string" && packageJson.version.trim() || process.env.npm_package_version?.trim() || "0.0.0-dev";
538
+ var MANIFEST_VERSION = "0.8.5"?.trim() || typeof packageJson.version === "string" && packageJson.version.trim() || process.env.npm_package_version?.trim() || "0.0.0-dev";
539
539
  var manifest = {
540
540
  id: GITHUB_SYNC_PLUGIN_ID,
541
541
  apiVersion: 1,
@@ -559,6 +559,7 @@ var manifest = {
559
559
  "issues.create",
560
560
  "issues.update",
561
561
  "issues.wakeup",
562
+ "issue.relations.read",
562
563
  "issue.comments.read",
563
564
  "issue.comments.create",
564
565
  "agents.read",
package/dist/worker.js CHANGED
@@ -1447,7 +1447,16 @@ function formatGitHubIssueCountLabel(count) {
1447
1447
  return `${normalizedCount} GitHub ${normalizedCount === 1 ? "issue" : "issues"}`;
1448
1448
  }
1449
1449
  function getErrorMessage(error) {
1450
- return error instanceof Error ? error.message : String(error);
1450
+ if (error instanceof Error) {
1451
+ return error.message;
1452
+ }
1453
+ if (error && typeof error === "object" && "message" in error) {
1454
+ const message = error.message;
1455
+ if (typeof message === "string") {
1456
+ return message;
1457
+ }
1458
+ }
1459
+ return String(error);
1451
1460
  }
1452
1461
  function isPaperclipLabelSyncError(error) {
1453
1462
  return error instanceof PaperclipLabelSyncError;
@@ -4889,7 +4898,9 @@ function resolvePaperclipStatusFromLinkedPullRequests(linkedPullRequests, option
4889
4898
  )) {
4890
4899
  return options?.preferInProgress ? "in_progress" : "todo";
4891
4900
  }
4892
- if (linkedPullRequests.length > 0 && linkedPullRequests.every((pullRequest) => isGitHubPullRequestReviewReadyForSync(pullRequest))) {
4901
+ if (linkedPullRequests.length > 0 && linkedPullRequests.every(
4902
+ (pullRequest) => isGitHubPullRequestReviewReadyForSync(pullRequest) || options?.preserveTransientUnknownMergeabilityWait === true && isGitHubPullRequestTransientUnknownMergeabilityWait(pullRequest)
4903
+ )) {
4893
4904
  return "in_review";
4894
4905
  }
4895
4906
  return "in_progress";
@@ -5021,6 +5032,40 @@ function getPaperclipIssueSyncContext(issue) {
5021
5032
  executionState: normalizePaperclipIssueExecutionState(record.executionState)
5022
5033
  };
5023
5034
  }
5035
+ function hasUnresolvedPaperclipIssueBlockerSummary(blockers) {
5036
+ if (!Array.isArray(blockers)) {
5037
+ return false;
5038
+ }
5039
+ return blockers.some((blocker) => {
5040
+ if (!blocker || typeof blocker !== "object") {
5041
+ return true;
5042
+ }
5043
+ const status = blocker.status;
5044
+ return status !== "done" && status !== "cancelled";
5045
+ });
5046
+ }
5047
+ function isMissingIssueRelationsReadCapabilityError(error) {
5048
+ const message = getErrorMessage(error);
5049
+ return /missing required capability/i.test(message) && /issue\.relations\.read/i.test(message) && /issues\.relations\.get/i.test(message);
5050
+ }
5051
+ async function hasUnresolvedPaperclipIssueBlocker(ctx, issue, companyId) {
5052
+ const record = issue;
5053
+ if (hasUnresolvedPaperclipIssueBlockerSummary(record.blockedBy)) {
5054
+ return true;
5055
+ }
5056
+ if (typeof ctx.issues.relations?.get !== "function") {
5057
+ return false;
5058
+ }
5059
+ try {
5060
+ const relations = await ctx.issues.relations.get(issue.id, companyId);
5061
+ return hasUnresolvedPaperclipIssueBlockerSummary(relations.blockedBy);
5062
+ } catch (error) {
5063
+ if (isMissingIssueRelationsReadCapabilityError(error)) {
5064
+ return false;
5065
+ }
5066
+ throw error;
5067
+ }
5068
+ }
5024
5069
  function isSamePaperclipIssueAssigneePrincipal(left, right) {
5025
5070
  if (!left || !right) {
5026
5071
  return !left && !right;
@@ -5392,7 +5437,8 @@ function resolvePaperclipIssueStatus(params) {
5392
5437
  }
5393
5438
  if (snapshot.linkedPullRequests.length > 0) {
5394
5439
  return resolvePaperclipStatusFromLinkedPullRequests(snapshot.linkedPullRequests, {
5395
- preferInProgress: hasExecutorHandoffTarget
5440
+ preferInProgress: hasExecutorHandoffTarget,
5441
+ preserveTransientUnknownMergeabilityWait: currentStatus === "done" || currentStatus === "in_review"
5396
5442
  });
5397
5443
  }
5398
5444
  if (wasImportedThisRun) {
@@ -5409,7 +5455,8 @@ function resolvePaperclipPullRequestIssueStatus(params) {
5409
5455
  return currentStatus;
5410
5456
  }
5411
5457
  return resolvePaperclipStatusFromLinkedPullRequests([pullRequest], {
5412
- preferInProgress: hasExecutorHandoffTarget
5458
+ preferInProgress: hasExecutorHandoffTarget,
5459
+ preserveTransientUnknownMergeabilityWait: currentStatus === "in_review"
5413
5460
  });
5414
5461
  }
5415
5462
  async function listLinkedPullRequestsForIssue(octokit, repository, issueNumber) {
@@ -5556,10 +5603,13 @@ function normalizeGitHubPullRequestReviewDecision(value) {
5556
5603
  }
5557
5604
  }
5558
5605
  function isGitHubPullRequestActionRequiredForSync(pullRequest) {
5559
- return pullRequest.reviewDecision === "changes_requested" || pullRequest.mergeability === "conflicting" || ACTION_REQUIRED_GITHUB_PULL_REQUEST_MERGE_STATE_STATUSES.has(pullRequest.mergeStateStatus);
5606
+ return pullRequest.mergeability === "conflicting" || ACTION_REQUIRED_GITHUB_PULL_REQUEST_MERGE_STATE_STATUSES.has(pullRequest.mergeStateStatus);
5607
+ }
5608
+ function isGitHubPullRequestTransientUnknownMergeabilityWait(pullRequest) {
5609
+ return pullRequest.ciState === "green" && !pullRequest.hasUnresolvedReviewThreads && pullRequest.mergeability !== "conflicting" && pullRequest.mergeStateStatus === "unknown";
5560
5610
  }
5561
5611
  function isGitHubPullRequestReviewReadyForSync(pullRequest) {
5562
- if (pullRequest.ciState !== "green" || pullRequest.hasUnresolvedReviewThreads || pullRequest.reviewDecision === "changes_requested") {
5612
+ if (pullRequest.ciState !== "green" || pullRequest.hasUnresolvedReviewThreads) {
5563
5613
  return false;
5564
5614
  }
5565
5615
  return REVIEW_READY_GITHUB_PULL_REQUEST_MERGE_STATE_STATUSES.has(pullRequest.mergeStateStatus);
@@ -5592,9 +5642,6 @@ function listGitHubPullRequestSyncBlockingConditions(pullRequest) {
5592
5642
  if (pullRequest.hasUnresolvedReviewThreads) {
5593
5643
  conditions.push("unresolved review threads");
5594
5644
  }
5595
- if (pullRequest.reviewDecision === "changes_requested") {
5596
- conditions.push("requested changes");
5597
- }
5598
5645
  return conditions;
5599
5646
  }
5600
5647
  function tryBuildGitHubPullRequestStatusSnapshotFromBatchNode(node, repository) {
@@ -9025,7 +9072,7 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
9025
9072
  paperclipIssueSyncContext,
9026
9073
  advancedSettings
9027
9074
  );
9028
- const nextStatus = resolvePaperclipIssueStatus({
9075
+ let nextStatus = resolvePaperclipIssueStatus({
9029
9076
  currentStatus: paperclipIssue.status,
9030
9077
  snapshot,
9031
9078
  hasTrustedNewComment,
@@ -9034,6 +9081,9 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
9034
9081
  maintainerAuthoredImportedIssue,
9035
9082
  hasExecutorHandoffTarget: Boolean(executorTransitionAssignee)
9036
9083
  });
9084
+ if (paperclipIssue.status === "blocked" && nextStatus !== "blocked" && await hasUnresolvedPaperclipIssueBlocker(ctx, paperclipIssue, mapping.companyId)) {
9085
+ nextStatus = "blocked";
9086
+ }
9037
9087
  const shouldPreserveMaintainerWaitRouting = isHealthyMaintainerWaitTransition({
9038
9088
  currentStatus: paperclipIssue.status,
9039
9089
  nextStatus,
@@ -9236,11 +9286,14 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
9236
9286
  paperclipIssueSyncContext,
9237
9287
  advancedSettings
9238
9288
  );
9239
- const nextStatus = resolvePaperclipPullRequestIssueStatus({
9289
+ let nextStatus = resolvePaperclipPullRequestIssueStatus({
9240
9290
  currentStatus: paperclipIssue.status,
9241
9291
  pullRequest,
9242
9292
  hasExecutorHandoffTarget: Boolean(executorTransitionAssignee)
9243
9293
  });
9294
+ if (paperclipIssue.status === "blocked" && nextStatus !== "blocked" && await hasUnresolvedPaperclipIssueBlocker(ctx, paperclipIssue, mapping.companyId)) {
9295
+ nextStatus = "blocked";
9296
+ }
9244
9297
  const shouldPreserveMaintainerWaitRouting = isHealthyMaintainerWaitTransition({
9245
9298
  currentStatus: paperclipIssue.status,
9246
9299
  nextStatus,
@@ -14327,6 +14380,7 @@ function shouldStartWorkerHost(moduleUrl, entry = process.argv[1]) {
14327
14380
  }
14328
14381
  var __testing = {
14329
14382
  buildSyncFallbackExecutionStatePatch,
14383
+ hasUnresolvedPaperclipIssueBlocker,
14330
14384
  isHealthyMaintainerWaitTransition,
14331
14385
  resolveSyncTransitionAssignee
14332
14386
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperclip-github-plugin",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "Paperclip plugin for synchronizing GitHub issues into Paperclip projects.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",