paperclip-github-plugin 0.8.12 → 0.9.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/README.md CHANGED
@@ -44,7 +44,7 @@ The plugin adds a full in-host workflow instead of a one-off import script:
44
44
  2. Connect one or more GitHub repositories to Paperclip projects.
45
45
  3. Run a sync manually or let the scheduled job keep things up to date.
46
46
 
47
- During sync, the plugin imports one top-level Paperclip issue per GitHub issue, stamps it with a namespaced GitHub Sync plugin origin, updates already imported issues instead of recreating them, maps GitHub labels into Paperclip labels, and keeps GitHub-specific metadata in dedicated Paperclip surfaces rather than stuffing everything into the issue description. On Paperclip `2026.428.0` and newer, the detail surfaces can also recover GitHub issue and pull request links from Paperclip's own `originKind` / `originId` fields when the plugin registry or legacy hidden marker is missing.
47
+ During sync, the plugin imports one top-level Paperclip issue per GitHub issue, stamps it with a namespaced GitHub Sync plugin origin, updates already imported issues instead of recreating them, maps GitHub labels into Paperclip labels, and keeps GitHub-specific metadata in dedicated Paperclip surfaces rather than stuffing everything into the issue description. On Paperclip `2026.512.0` and newer, the plugin passes the intended initial import status explicitly so Paperclip's assigned-issue creation defaults cannot override GitHub Sync's routing, and the detail surfaces can recover GitHub issue and pull request links from Paperclip's own `originKind` / `originId` fields when the plugin registry or legacy hidden marker is missing.
48
48
 
49
49
  When the host exposes plugin issue creation, imported GitHub issues are created through the Paperclip plugin SDK path so they are not attributed to the connected board user. The worker still uses direct local Paperclip REST calls for label sync and for description, assignee, or status repair paths when those routes are available.
50
50
 
@@ -104,7 +104,7 @@ They can also link a Paperclip issue to a GitHub issue or pull request in any ac
104
104
  ## Requirements
105
105
 
106
106
  - Node.js 20+
107
- - a Paperclip host with plugin installation enabled. GitHub Sync is built and tested against Paperclip `2026.428.0`; the manifest relies on explicit capabilities instead of a strict host-version gate because current latest/development hosts can report `0.0.0` during plugin upgrade.
107
+ - a Paperclip host with plugin installation enabled. GitHub Sync is built and tested against Paperclip `2026.512.0`; the manifest relies on explicit capabilities instead of a strict host-version gate because current latest/development hosts can report `0.0.0` during plugin upgrade.
108
108
  - a GitHub token with API access to the repositories you want to sync
109
109
 
110
110
  ## Install from npm
@@ -219,7 +219,7 @@ Notes:
219
219
 
220
220
  GitHub Sync uses direct worker-side Paperclip REST calls for host paths that are not fully covered by the plugin SDK, such as label reconciliation and some issue repair paths. By default, manual setup and sync actions use the current browser origin. If that origin is not reachable from the plugin worker, set **Worker Paperclip API URL** in GitHub Sync settings; the value is saved internally as plugin config `paperclipApiBaseUrl`.
221
221
 
222
- For private LAN, Docker, Kubernetes, custom DNS, or self-signed-certificate deployments, set that field to a local route the plugin worker can reach, such as `http://localhost:3100`, and port-forward or route that address to the Paperclip API when needed. Do not rely on exporting a process environment variable named `PAPERCLIP_API_URL` to Paperclip; Paperclip `2026.428.0` does not pass that value through to plugin worker runtime.
222
+ For private LAN, Docker, Kubernetes, custom DNS, or self-signed-certificate deployments, set that field to a local route the plugin worker can reach, such as `http://localhost:3100`, and port-forward or route that address to the Paperclip API when needed. Do not rely on exporting a process environment variable named `PAPERCLIP_API_URL` to Paperclip; release-target verification keeps this as explicit plugin configuration because worker runtime environments do not guarantee that process variable.
223
223
 
224
224
  ## GitHub agent tools
225
225
 
@@ -354,8 +354,8 @@ Useful scripts:
354
354
 
355
355
  - `pnpm dev` watches the manifest, worker, and UI bundles and rebuilds them into `dist/`
356
356
  - `pnpm dev:ui` starts a local Paperclip plugin UI dev server from `dist/ui` on port `4177`
357
- - `pnpm test:e2e` builds the plugin, boots an isolated Paperclip `2026.428.0` instance, installs the plugin, and verifies the hosted settings page renders
358
- - `pnpm verify:manual` builds the plugin, boots a local-trusted Paperclip `2026.428.0` instance for manual inspection, seeds a `Dummy Company` with a mapped review project and a `CEO` agent on the Codex local adapter using model `gpt-5.4`, installs the plugin, and opens the company dashboard without seeding KPI history.
357
+ - `pnpm test:e2e` builds the plugin, boots an isolated Paperclip `2026.512.0` instance, installs the plugin, and verifies the hosted settings page renders
358
+ - `pnpm verify:manual` builds the plugin, boots a local-trusted Paperclip `2026.512.0` instance for manual inspection, seeds a `Dummy Company` with a mapped review project and a `CEO` agent on the Codex local adapter using model `gpt-5.4`, installs the plugin, and opens the company dashboard without seeding KPI history.
359
359
 
360
360
  For fast hosted UI iteration, run `pnpm dev` in one terminal and `pnpm dev:ui` in another.
361
361
 
package/dist/manifest.js CHANGED
@@ -642,7 +642,7 @@ var PULL_REQUEST_ASSET_API_ROUTE_URL_PATH = `/api/plugins/${GITHUB_SYNC_PLUGIN_I
642
642
  var require2 = createRequire(import.meta.url);
643
643
  var packageJson = require2("../package.json");
644
644
  var SCHEDULE_TICK_CRON = "* * * * *";
645
- var MANIFEST_VERSION = "0.8.12"?.trim() || typeof packageJson.version === "string" && packageJson.version.trim() || process.env.npm_package_version?.trim() || "0.0.0-dev";
645
+ var MANIFEST_VERSION = "0.9.0"?.trim() || typeof packageJson.version === "string" && packageJson.version.trim() || process.env.npm_package_version?.trim() || "0.0.0-dev";
646
646
  var manifest = {
647
647
  id: GITHUB_SYNC_PLUGIN_ID,
648
648
  apiVersion: 1,
package/dist/worker.js CHANGED
@@ -5681,9 +5681,19 @@ function buildPaperclipIssueStatusTransitionComment(params) {
5681
5681
  };
5682
5682
  }
5683
5683
  function buildPaperclipPullRequestIssueStatusTransitionComment(params) {
5684
- const reason = describeGitHubLinkedPullRequestsStatusReason([params.pullRequest]);
5684
+ const reason = describeGitHubDirectPullRequestIssueStatusReason(params.pullRequests);
5685
5685
  return `GitHub Sync updated the status from \`${formatPaperclipIssueStatus(params.previousStatus)}\` to \`${formatPaperclipIssueStatus(params.nextStatus)}\` because ${reason}.`;
5686
5686
  }
5687
+ function describeGitHubDirectPullRequestIssueStatusReason(pullRequests) {
5688
+ const openPullRequests = pullRequests.map((entry) => entry.pullRequest).filter((pullRequest) => Boolean(pullRequest));
5689
+ if (openPullRequests.length > 0) {
5690
+ return describeGitHubLinkedPullRequestsStatusReason(openPullRequests);
5691
+ }
5692
+ if (pullRequests.some((entry) => entry.lifecycleState === "merged")) {
5693
+ return pullRequests.length === 1 ? "the linked pull request was merged" : "at least one linked pull request was merged";
5694
+ }
5695
+ return pullRequests.length === 1 ? "the linked pull request was closed without merge" : "all linked pull requests were closed without merge";
5696
+ }
5687
5697
  function resolvePaperclipIssueStatus(params) {
5688
5698
  const {
5689
5699
  currentStatus,
@@ -5736,6 +5746,23 @@ function resolvePaperclipPullRequestIssueStatus(params) {
5736
5746
  preserveTransientUnknownMergeabilityWait: currentStatus === "done" || currentStatus === "in_review"
5737
5747
  });
5738
5748
  }
5749
+ function resolvePaperclipDirectPullRequestIssueStatus(params) {
5750
+ const { currentStatus, pullRequests, hasExecutorHandoffTarget } = params;
5751
+ const openPullRequests = pullRequests.map((entry) => entry.pullRequest).filter((pullRequest) => Boolean(pullRequest));
5752
+ if (openPullRequests.length > 0) {
5753
+ if (shouldPreserveBlockedExternalPullRequestWait({
5754
+ currentStatus,
5755
+ linkedPullRequests: openPullRequests
5756
+ })) {
5757
+ return "blocked";
5758
+ }
5759
+ return resolvePaperclipStatusFromLinkedPullRequests(openPullRequests, {
5760
+ preferInProgress: hasExecutorHandoffTarget,
5761
+ preserveTransientUnknownMergeabilityWait: currentStatus === "done" || currentStatus === "in_review"
5762
+ });
5763
+ }
5764
+ return pullRequests.some((entry) => entry.lifecycleState === "merged") ? "done" : "cancelled";
5765
+ }
5739
5766
  async function listLinkedPullRequestsForIssue(octokit, repository, issueNumber) {
5740
5767
  const linkedPullRequests = [];
5741
5768
  const seenPullRequestKeys = /* @__PURE__ */ new Set();
@@ -8975,6 +9002,16 @@ async function assignImportedPaperclipIssueToUser(ctx, params) {
8975
9002
  });
8976
9003
  }
8977
9004
  }
9005
+ async function ensurePaperclipIssueStandardWorkMode(ctx, issue, companyId) {
9006
+ if (!("workMode" in issue) || issue.workMode === "standard") {
9007
+ return issue;
9008
+ }
9009
+ return ctx.issues.update(
9010
+ issue.id,
9011
+ { workMode: "standard" },
9012
+ companyId
9013
+ );
9014
+ }
8978
9015
  async function createPaperclipIssue(ctx, mapping, advancedSettings, issue, availableLabels, paperclipApiBaseUrl, syncFailureContext) {
8979
9016
  if (!mapping.companyId || !mapping.paperclipProjectId) {
8980
9017
  throw new Error(`Mapping ${mapping.id} is missing resolved Paperclip project identifiers.`);
@@ -8982,15 +9019,20 @@ async function createPaperclipIssue(ctx, mapping, advancedSettings, issue, avail
8982
9019
  const title = issue.title;
8983
9020
  const description = buildPaperclipIssueDescription(issue);
8984
9021
  const defaultAssignee = getConfiguredAdvancedAssigneePrincipal(advancedSettings, "default");
8985
- const createdIssue = await ctx.issues.create({
8986
- companyId: mapping.companyId,
8987
- projectId: mapping.paperclipProjectId,
8988
- title,
8989
- ...description ? { description } : {},
8990
- originKind: GITHUB_ISSUE_ORIGIN_KIND,
8991
- originId: normalizeGitHubIssueHtmlUrl(issue.htmlUrl) ?? issue.htmlUrl,
8992
- ...defaultAssignee?.kind === "agent" ? { assigneeAgentId: defaultAssignee.id } : defaultAssignee?.kind === "user" ? { assigneeUserId: defaultAssignee.id } : {}
8993
- });
9022
+ const createdIssue = await ensurePaperclipIssueStandardWorkMode(
9023
+ ctx,
9024
+ await ctx.issues.create({
9025
+ companyId: mapping.companyId,
9026
+ projectId: mapping.paperclipProjectId,
9027
+ title,
9028
+ ...description ? { description } : {},
9029
+ status: advancedSettings.defaultStatus,
9030
+ originKind: GITHUB_ISSUE_ORIGIN_KIND,
9031
+ originId: normalizeGitHubIssueHtmlUrl(issue.htmlUrl) ?? issue.htmlUrl,
9032
+ ...defaultAssignee?.kind === "agent" ? { assigneeAgentId: defaultAssignee.id } : defaultAssignee?.kind === "user" ? { assigneeUserId: defaultAssignee.id } : {}
9033
+ }),
9034
+ mapping.companyId
9035
+ );
8994
9036
  const ensuredCreatedIssueId = createdIssue.id;
8995
9037
  const normalizedCreatedIssueDescription = createdIssue.description ?? void 0;
8996
9038
  const createPath = "sdk";
@@ -9708,6 +9750,18 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
9708
9750
  updatedDescriptionsCount
9709
9751
  };
9710
9752
  }
9753
+ function groupGitHubPullRequestLinksByPaperclipIssue(pullRequestLinks) {
9754
+ const groups = /* @__PURE__ */ new Map();
9755
+ const sortedLinks = [...pullRequestLinks].sort(
9756
+ (left, right) => left.paperclipIssueId.localeCompare(right.paperclipIssueId) || left.data.repositoryUrl.localeCompare(right.data.repositoryUrl) || left.data.githubPullRequestNumber - right.data.githubPullRequestNumber
9757
+ );
9758
+ for (const pullRequestLink of sortedLinks) {
9759
+ const group = groups.get(pullRequestLink.paperclipIssueId) ?? [];
9760
+ group.push(pullRequestLink);
9761
+ groups.set(pullRequestLink.paperclipIssueId, group);
9762
+ }
9763
+ return groups;
9764
+ }
9711
9765
  async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mapping, advancedSettings, pullRequestLinks, paperclipApiBaseUrl, pullRequestStatusCache, syncFailureContext, failures, assertNotCancelled, onProgress) {
9712
9766
  if (!mapping.companyId || !ctx.issues || typeof ctx.issues.get !== "function" || typeof ctx.issues.update !== "function") {
9713
9767
  return {
@@ -9719,52 +9773,93 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
9719
9773
  const mappingCompanyId = mapping.companyId;
9720
9774
  const mappingProjectId = mapping.paperclipProjectId;
9721
9775
  const totalIssueCount = pullRequestLinks.length;
9776
+ const pullRequestLinkGroups = groupGitHubPullRequestLinksByPaperclipIssue(pullRequestLinks);
9722
9777
  const queuedIssueWakeups = [];
9723
- for (const pullRequestLink of pullRequestLinks) {
9724
- if (assertNotCancelled) {
9725
- await assertNotCancelled();
9726
- }
9727
- try {
9728
- const pullRequestRepository = requireRepositoryReference(pullRequestLink.data.repositoryUrl);
9729
- updateSyncFailureContext(syncFailureContext, {
9730
- phase: "evaluating_github_status",
9731
- repositoryUrl: pullRequestRepository.url,
9732
- githubIssueNumber: void 0
9733
- });
9734
- const pullRequestResponse = await octokit.rest.pulls.get({
9735
- owner: pullRequestRepository.owner,
9736
- repo: pullRequestRepository.repo,
9737
- pull_number: pullRequestLink.data.githubPullRequestNumber,
9738
- headers: {
9739
- "X-GitHub-Api-Version": GITHUB_API_VERSION
9740
- }
9741
- });
9742
- const livePullRequestState = getPullRequestApiState({
9743
- state: pullRequestResponse.data.state,
9744
- merged: pullRequestResponse.data.merged
9745
- }) === "open" ? "open" : "closed";
9746
- if (livePullRequestState !== pullRequestLink.data.githubPullRequestState || pullRequestResponse.data.html_url !== pullRequestLink.data.githubPullRequestUrl || pullRequestResponse.data.title !== pullRequestLink.data.title) {
9747
- await upsertGitHubPullRequestLinkRecord(ctx, {
9748
- companyId: pullRequestLink.data.companyId ?? mappingCompanyId,
9749
- projectId: pullRequestLink.data.paperclipProjectId ?? mappingProjectId,
9750
- issueId: pullRequestLink.paperclipIssueId,
9778
+ for (const [paperclipIssueId, issuePullRequestLinks] of pullRequestLinkGroups.entries()) {
9779
+ const pullRequestSnapshots = [];
9780
+ let groupHadFailure = false;
9781
+ for (const pullRequestLink of issuePullRequestLinks) {
9782
+ if (assertNotCancelled) {
9783
+ await assertNotCancelled();
9784
+ }
9785
+ try {
9786
+ const pullRequestRepository = requireRepositoryReference(pullRequestLink.data.repositoryUrl);
9787
+ updateSyncFailureContext(syncFailureContext, {
9788
+ phase: "evaluating_github_status",
9751
9789
  repositoryUrl: pullRequestRepository.url,
9752
- pullRequestNumber: pullRequestLink.data.githubPullRequestNumber,
9753
- pullRequestUrl: pullRequestResponse.data.html_url ?? pullRequestLink.data.githubPullRequestUrl,
9754
- pullRequestTitle: pullRequestResponse.data.title || pullRequestLink.data.title || `Pull request #${pullRequestLink.data.githubPullRequestNumber}`,
9755
- pullRequestState: livePullRequestState
9790
+ githubIssueNumber: void 0
9756
9791
  });
9792
+ const pullRequestResponse = await octokit.rest.pulls.get({
9793
+ owner: pullRequestRepository.owner,
9794
+ repo: pullRequestRepository.repo,
9795
+ pull_number: pullRequestLink.data.githubPullRequestNumber,
9796
+ headers: {
9797
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
9798
+ }
9799
+ });
9800
+ const livePullRequestLifecycleState = getPullRequestApiState({
9801
+ state: pullRequestResponse.data.state,
9802
+ merged: pullRequestResponse.data.merged
9803
+ });
9804
+ const livePullRequestLinkState = livePullRequestLifecycleState === "open" ? "open" : "closed";
9805
+ if (livePullRequestLinkState !== pullRequestLink.data.githubPullRequestState || pullRequestResponse.data.html_url !== pullRequestLink.data.githubPullRequestUrl || pullRequestResponse.data.title !== pullRequestLink.data.title) {
9806
+ await upsertGitHubPullRequestLinkRecord(ctx, {
9807
+ companyId: pullRequestLink.data.companyId ?? mappingCompanyId,
9808
+ projectId: pullRequestLink.data.paperclipProjectId ?? mappingProjectId,
9809
+ issueId: pullRequestLink.paperclipIssueId,
9810
+ repositoryUrl: pullRequestRepository.url,
9811
+ pullRequestNumber: pullRequestLink.data.githubPullRequestNumber,
9812
+ pullRequestUrl: pullRequestResponse.data.html_url ?? pullRequestLink.data.githubPullRequestUrl,
9813
+ pullRequestTitle: pullRequestResponse.data.title || pullRequestLink.data.title || `Pull request #${pullRequestLink.data.githubPullRequestNumber}`,
9814
+ pullRequestState: livePullRequestLinkState
9815
+ });
9816
+ }
9817
+ if (livePullRequestLifecycleState === "open") {
9818
+ const pullRequest = await getGitHubPullRequestStatusSnapshot(
9819
+ octokit,
9820
+ pullRequestRepository,
9821
+ pullRequestLink.data.githubPullRequestNumber,
9822
+ pullRequestStatusCache
9823
+ );
9824
+ pullRequestSnapshots.push({
9825
+ pullRequestLink,
9826
+ repository: pullRequestRepository,
9827
+ lifecycleState: "open",
9828
+ pullRequest
9829
+ });
9830
+ } else {
9831
+ pullRequestSnapshots.push({
9832
+ pullRequestLink,
9833
+ repository: pullRequestRepository,
9834
+ lifecycleState: livePullRequestLifecycleState
9835
+ });
9836
+ }
9837
+ } catch (error) {
9838
+ if (isGitHubRateLimitError(error)) {
9839
+ throw error;
9840
+ }
9841
+ groupHadFailure = true;
9842
+ recordRecoverableSyncFailure(ctx, failures, error, syncFailureContext);
9843
+ } finally {
9844
+ completedIssueCount += 1;
9845
+ if (onProgress) {
9846
+ await onProgress({
9847
+ pullRequestLink,
9848
+ completedIssueCount,
9849
+ totalIssueCount
9850
+ });
9851
+ }
9757
9852
  }
9758
- if (livePullRequestState !== "open") {
9759
- continue;
9853
+ }
9854
+ if (groupHadFailure || pullRequestSnapshots.length === 0) {
9855
+ continue;
9856
+ }
9857
+ const primaryRepository = pullRequestSnapshots[0]?.repository;
9858
+ try {
9859
+ if (assertNotCancelled) {
9860
+ await assertNotCancelled();
9760
9861
  }
9761
- const pullRequest = await getGitHubPullRequestStatusSnapshot(
9762
- octokit,
9763
- pullRequestRepository,
9764
- pullRequestLink.data.githubPullRequestNumber,
9765
- pullRequestStatusCache
9766
- );
9767
- const paperclipIssue = await ctx.issues.get(pullRequestLink.paperclipIssueId, mapping.companyId);
9862
+ const paperclipIssue = await ctx.issues.get(paperclipIssueId, mapping.companyId);
9768
9863
  if (!paperclipIssue) {
9769
9864
  continue;
9770
9865
  }
@@ -9773,12 +9868,12 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
9773
9868
  paperclipIssueSyncContext,
9774
9869
  advancedSettings
9775
9870
  );
9776
- let nextStatus = resolvePaperclipPullRequestIssueStatus({
9871
+ let nextStatus = resolvePaperclipDirectPullRequestIssueStatus({
9777
9872
  currentStatus: paperclipIssue.status,
9778
- pullRequest,
9873
+ pullRequests: pullRequestSnapshots,
9779
9874
  hasExecutorHandoffTarget: Boolean(executorTransitionAssignee)
9780
9875
  });
9781
- if (paperclipIssue.status === "blocked" && nextStatus !== "blocked" && await hasUnresolvedPaperclipIssueBlocker(ctx, paperclipIssue, mapping.companyId)) {
9876
+ if (paperclipIssue.status === "blocked" && nextStatus !== "blocked" && pullRequestSnapshots.some((entry) => entry.lifecycleState === "open") && await hasUnresolvedPaperclipIssueBlocker(ctx, paperclipIssue, mapping.companyId)) {
9782
9877
  nextStatus = "blocked";
9783
9878
  }
9784
9879
  const shouldPreserveMaintainerWaitRouting = isHealthyMaintainerWaitTransition({
@@ -9786,6 +9881,10 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
9786
9881
  nextStatus,
9787
9882
  syncContext: paperclipIssueSyncContext
9788
9883
  });
9884
+ const shouldClearCompletedExecutionPolicy = shouldClearCompletedSyncExecutionPolicy({
9885
+ nextStatus,
9886
+ syncContext: paperclipIssueSyncContext
9887
+ });
9789
9888
  const nextTransitionAssignee = resolveSyncTransitionAssignee({
9790
9889
  currentStatus: paperclipIssue.status,
9791
9890
  nextStatus,
@@ -9796,20 +9895,20 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
9796
9895
  const nextAssigneeChanged = nextTransitionAssignee ? !doesPaperclipIssueAssigneeMatch(paperclipIssueSyncContext.assignee, nextTransitionAssignee.principal) : false;
9797
9896
  const shouldWakeTransitionAssignee = paperclipIssue.status !== nextStatus && nextTransitionAssignee?.principal.kind === "agent" && isActionablePaperclipIssueStatus(nextStatus) && (nextAssigneeChanged || paperclipIssue.status !== nextStatus);
9798
9897
  if (paperclipIssue.status === nextStatus) {
9799
- if (shouldClearTransitionAssignee) {
9898
+ if (shouldClearTransitionAssignee || shouldClearCompletedExecutionPolicy) {
9800
9899
  updateSyncFailureContext(syncFailureContext, {
9801
9900
  phase: "updating_paperclip_status",
9802
- repositoryUrl: pullRequestRepository.url,
9901
+ repositoryUrl: primaryRepository?.url,
9803
9902
  githubIssueNumber: void 0
9804
9903
  });
9805
9904
  await updatePaperclipIssueState(ctx, {
9806
9905
  companyId: mapping.companyId,
9807
- issueId: pullRequestLink.paperclipIssueId,
9906
+ issueId: paperclipIssueId,
9808
9907
  currentStatus: paperclipIssue.status,
9809
9908
  syncContext: paperclipIssueSyncContext,
9810
9909
  nextStatus,
9811
- clearAssignee: true,
9812
- ...shouldPreserveMaintainerWaitRouting ? { clearExecutionPolicy: true } : {},
9910
+ ...shouldClearTransitionAssignee ? { clearAssignee: true } : {},
9911
+ ...shouldPreserveMaintainerWaitRouting || shouldClearCompletedExecutionPolicy ? { clearExecutionPolicy: true } : {},
9813
9912
  transitionComment: "",
9814
9913
  paperclipApiBaseUrl
9815
9914
  });
@@ -9819,22 +9918,22 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
9819
9918
  const transitionComment = buildPaperclipPullRequestIssueStatusTransitionComment({
9820
9919
  previousStatus: paperclipIssue.status,
9821
9920
  nextStatus,
9822
- pullRequest
9921
+ pullRequests: pullRequestSnapshots
9823
9922
  });
9824
9923
  updateSyncFailureContext(syncFailureContext, {
9825
9924
  phase: "updating_paperclip_status",
9826
- repositoryUrl: pullRequestRepository.url,
9925
+ repositoryUrl: primaryRepository?.url,
9827
9926
  githubIssueNumber: void 0
9828
9927
  });
9829
9928
  await updatePaperclipIssueState(ctx, {
9830
9929
  companyId: mapping.companyId,
9831
- issueId: pullRequestLink.paperclipIssueId,
9930
+ issueId: paperclipIssueId,
9832
9931
  currentStatus: paperclipIssue.status,
9833
9932
  syncContext: paperclipIssueSyncContext,
9834
9933
  nextStatus,
9835
9934
  ...nextTransitionAssignee ? { nextAssignee: nextTransitionAssignee.principal } : {},
9836
9935
  ...shouldClearTransitionAssignee ? { clearAssignee: true } : {},
9837
- ...shouldPreserveMaintainerWaitRouting ? { clearExecutionPolicy: true } : {},
9936
+ ...shouldPreserveMaintainerWaitRouting || shouldClearCompletedExecutionPolicy ? { clearExecutionPolicy: true } : {},
9838
9937
  transitionComment,
9839
9938
  paperclipApiBaseUrl
9840
9939
  });
@@ -9842,7 +9941,7 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
9842
9941
  if (shouldWakeTransitionAssignee && nextTransitionAssignee?.principal.kind === "agent") {
9843
9942
  queuedIssueWakeups.push({
9844
9943
  assigneeAgentId: nextTransitionAssignee.principal.id,
9845
- paperclipIssueId: pullRequestLink.paperclipIssueId,
9944
+ paperclipIssueId,
9846
9945
  reason: STATUS_TRANSITION_WAKE_REASON,
9847
9946
  mutation: "status_transition",
9848
9947
  previousStatus: paperclipIssue.status,
@@ -9854,16 +9953,6 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
9854
9953
  throw error;
9855
9954
  }
9856
9955
  recordRecoverableSyncFailure(ctx, failures, error, syncFailureContext);
9857
- continue;
9858
- } finally {
9859
- completedIssueCount += 1;
9860
- if (onProgress) {
9861
- await onProgress({
9862
- pullRequestLink,
9863
- completedIssueCount,
9864
- totalIssueCount
9865
- });
9866
- }
9867
9956
  }
9868
9957
  }
9869
9958
  await mapWithConcurrency(
@@ -12888,19 +12977,24 @@ async function createProjectPullRequestPaperclipIssue(ctx, input) {
12888
12977
  };
12889
12978
  }
12890
12979
  const requestedTitle = typeof input.title === "string" && input.title.trim() ? input.title.trim() : pullRequest.title.trim();
12891
- const createdIssue = await ctx.issues.create({
12892
- companyId: scope.companyId,
12893
- projectId: scope.projectId,
12894
- title: requestedTitle,
12895
- originKind: GITHUB_PULL_REQUEST_ORIGIN_KIND,
12896
- originId: pullRequestUrl,
12897
- description: buildPaperclipIssueDescriptionFromPullRequest({
12898
- repository: scope.repository,
12899
- pullRequestNumber,
12900
- pullRequestUrl,
12901
- body: pullRequest.body
12902
- })
12903
- });
12980
+ const createdIssue = await ensurePaperclipIssueStandardWorkMode(
12981
+ ctx,
12982
+ await ctx.issues.create({
12983
+ companyId: scope.companyId,
12984
+ projectId: scope.projectId,
12985
+ title: requestedTitle,
12986
+ status: "todo",
12987
+ originKind: GITHUB_PULL_REQUEST_ORIGIN_KIND,
12988
+ originId: pullRequestUrl,
12989
+ description: buildPaperclipIssueDescriptionFromPullRequest({
12990
+ repository: scope.repository,
12991
+ pullRequestNumber,
12992
+ pullRequestUrl,
12993
+ body: pullRequest.body
12994
+ })
12995
+ }),
12996
+ scope.companyId
12997
+ );
12904
12998
  const resolvedIssue = await ctx.issues.get(createdIssue.id, scope.companyId) ?? createdIssue;
12905
12999
  await upsertGitHubPullRequestLinkRecord(ctx, {
12906
13000
  companyId: scope.companyId,
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "paperclip-github-plugin",
3
- "version": "0.8.12",
3
+ "version": "0.9.0",
4
4
  "description": "Paperclip plugin for synchronizing GitHub issues into Paperclip projects.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
- "packageManager": "pnpm@10.33.2",
7
+ "packageManager": "pnpm@11.0.9",
8
8
  "engines": {
9
9
  "node": ">=20"
10
10
  },
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@octokit/rest": "^22.0.1",
44
- "@paperclipai/plugin-sdk": "^2026.428.0",
44
+ "@paperclipai/plugin-sdk": "^2026.512.0",
45
45
  "react": "^19.2.6",
46
46
  "react-markdown": "^10.1.0",
47
47
  "rehype-raw": "^7.0.0",