paperclip-github-plugin 0.8.11 → 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
 
@@ -94,7 +94,7 @@ If a Paperclip issue was created locally or by an agent workflow before GitHub S
94
94
 
95
95
  Manual GitHub issue links are added to the same import registry and issue-link entity used by normal sync, so future syncs update the Paperclip issue from the GitHub issue. Manual pull request links are added to the PR-link entity used by the project Pull Requests page, so future syncs monitor PR status even when there is no closing GitHub issue.
96
96
 
97
- Linked Paperclip issues can also be unlinked from the GitHub detail surface. Unlinking removes the active GitHub Sync link metadata and import-registry tracking for that Paperclip issue without deleting either side, so the issue stays local until it is linked again.
97
+ Operators can unlink a linked Paperclip issue from the GitHub detail surface when they intentionally want GitHub Sync to stop updating it. Agent-facing tools and native agent API routes can create durable issue and pull request links, but they do not expose an unlink operation; internal sync repair may still tombstone a link when GitHub transfers an issue to an unmapped repository.
98
98
 
99
99
  ### Agent workflows built in
100
100
 
@@ -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.11"?.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
@@ -2857,21 +2857,10 @@ async function buildToolbarSyncState(ctx, input) {
2857
2857
  if (entityType === "issue" && entityId && companyId) {
2858
2858
  const link = await resolvePaperclipIssueGitHubLink(ctx, entityId, companyId);
2859
2859
  if (link) {
2860
- const mappings2 = getSyncableMappingsForTarget(settings.mappings, {
2861
- kind: "issue",
2862
- companyId,
2863
- projectId: link.paperclipProjectId,
2864
- issueId: entityId,
2865
- repositoryUrl: link.repositoryUrl,
2866
- githubIssueId: link.githubIssueId,
2867
- githubIssueNumber: link.githubIssueNumber,
2868
- githubIssueUrl: link.githubIssueUrl,
2869
- displayLabel: `issue #${link.githubIssueNumber}`
2870
- });
2871
2860
  return {
2872
2861
  kind: "issue",
2873
2862
  visible: false,
2874
- canRun: githubTokenConfigured && mappings2.length > 0,
2863
+ canRun: githubTokenConfigured,
2875
2864
  label: `Sync #${link.githubIssueNumber}`,
2876
2865
  message: `Sync ${link.repositoryUrl.replace(/^https:\/\/github\.com\//, "")} issue #${link.githubIssueNumber}.`,
2877
2866
  syncState: settings.syncState,
@@ -2880,20 +2869,10 @@ async function buildToolbarSyncState(ctx, input) {
2880
2869
  };
2881
2870
  }
2882
2871
  const pullRequestLink = await resolvePaperclipIssueGitHubPullRequestLink(ctx, entityId, companyId);
2883
- const mappings = pullRequestLink ? getSyncableMappingsForTarget(settings.mappings, {
2884
- kind: "issue",
2885
- companyId,
2886
- projectId: pullRequestLink.data.paperclipProjectId,
2887
- issueId: entityId,
2888
- repositoryUrl: pullRequestLink.data.repositoryUrl,
2889
- githubPullRequestNumber: pullRequestLink.data.githubPullRequestNumber,
2890
- githubPullRequestUrl: pullRequestLink.data.githubPullRequestUrl,
2891
- displayLabel: `pull request #${pullRequestLink.data.githubPullRequestNumber}`
2892
- }) : [];
2893
2872
  return {
2894
2873
  kind: "issue",
2895
2874
  visible: false,
2896
- canRun: githubTokenConfigured && mappings.length > 0,
2875
+ canRun: githubTokenConfigured && Boolean(pullRequestLink),
2897
2876
  label: pullRequestLink?.data.githubPullRequestNumber ? `Sync PR #${pullRequestLink.data.githubPullRequestNumber}` : "Sync issue",
2898
2877
  message: pullRequestLink ? `Sync ${pullRequestLink.data.repositoryUrl.replace(/^https:\/\/github\.com\//, "")} pull request #${pullRequestLink.data.githubPullRequestNumber}.` : "This Paperclip issue is not linked to GitHub yet.",
2899
2878
  syncState: settings.syncState,
@@ -5702,9 +5681,19 @@ function buildPaperclipIssueStatusTransitionComment(params) {
5702
5681
  };
5703
5682
  }
5704
5683
  function buildPaperclipPullRequestIssueStatusTransitionComment(params) {
5705
- const reason = describeGitHubLinkedPullRequestsStatusReason([params.pullRequest]);
5684
+ const reason = describeGitHubDirectPullRequestIssueStatusReason(params.pullRequests);
5706
5685
  return `GitHub Sync updated the status from \`${formatPaperclipIssueStatus(params.previousStatus)}\` to \`${formatPaperclipIssueStatus(params.nextStatus)}\` because ${reason}.`;
5707
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
+ }
5708
5697
  function resolvePaperclipIssueStatus(params) {
5709
5698
  const {
5710
5699
  currentStatus,
@@ -5757,6 +5746,23 @@ function resolvePaperclipPullRequestIssueStatus(params) {
5757
5746
  preserveTransientUnknownMergeabilityWait: currentStatus === "done" || currentStatus === "in_review"
5758
5747
  });
5759
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
+ }
5760
5766
  async function listLinkedPullRequestsForIssue(octokit, repository, issueNumber) {
5761
5767
  const linkedPullRequests = [];
5762
5768
  const seenPullRequestKeys = /* @__PURE__ */ new Set();
@@ -8996,6 +9002,16 @@ async function assignImportedPaperclipIssueToUser(ctx, params) {
8996
9002
  });
8997
9003
  }
8998
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
+ }
8999
9015
  async function createPaperclipIssue(ctx, mapping, advancedSettings, issue, availableLabels, paperclipApiBaseUrl, syncFailureContext) {
9000
9016
  if (!mapping.companyId || !mapping.paperclipProjectId) {
9001
9017
  throw new Error(`Mapping ${mapping.id} is missing resolved Paperclip project identifiers.`);
@@ -9003,15 +9019,20 @@ async function createPaperclipIssue(ctx, mapping, advancedSettings, issue, avail
9003
9019
  const title = issue.title;
9004
9020
  const description = buildPaperclipIssueDescription(issue);
9005
9021
  const defaultAssignee = getConfiguredAdvancedAssigneePrincipal(advancedSettings, "default");
9006
- const createdIssue = await ctx.issues.create({
9007
- companyId: mapping.companyId,
9008
- projectId: mapping.paperclipProjectId,
9009
- title,
9010
- ...description ? { description } : {},
9011
- originKind: GITHUB_ISSUE_ORIGIN_KIND,
9012
- originId: normalizeGitHubIssueHtmlUrl(issue.htmlUrl) ?? issue.htmlUrl,
9013
- ...defaultAssignee?.kind === "agent" ? { assigneeAgentId: defaultAssignee.id } : defaultAssignee?.kind === "user" ? { assigneeUserId: defaultAssignee.id } : {}
9014
- });
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
+ );
9015
9036
  const ensuredCreatedIssueId = createdIssue.id;
9016
9037
  const normalizedCreatedIssueDescription = createdIssue.description ?? void 0;
9017
9038
  const createPath = "sdk";
@@ -9729,6 +9750,18 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
9729
9750
  updatedDescriptionsCount
9730
9751
  };
9731
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
+ }
9732
9765
  async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mapping, advancedSettings, pullRequestLinks, paperclipApiBaseUrl, pullRequestStatusCache, syncFailureContext, failures, assertNotCancelled, onProgress) {
9733
9766
  if (!mapping.companyId || !ctx.issues || typeof ctx.issues.get !== "function" || typeof ctx.issues.update !== "function") {
9734
9767
  return {
@@ -9740,52 +9773,93 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
9740
9773
  const mappingCompanyId = mapping.companyId;
9741
9774
  const mappingProjectId = mapping.paperclipProjectId;
9742
9775
  const totalIssueCount = pullRequestLinks.length;
9776
+ const pullRequestLinkGroups = groupGitHubPullRequestLinksByPaperclipIssue(pullRequestLinks);
9743
9777
  const queuedIssueWakeups = [];
9744
- for (const pullRequestLink of pullRequestLinks) {
9745
- if (assertNotCancelled) {
9746
- await assertNotCancelled();
9747
- }
9748
- try {
9749
- const pullRequestRepository = requireRepositoryReference(pullRequestLink.data.repositoryUrl);
9750
- updateSyncFailureContext(syncFailureContext, {
9751
- phase: "evaluating_github_status",
9752
- repositoryUrl: pullRequestRepository.url,
9753
- githubIssueNumber: void 0
9754
- });
9755
- const pullRequestResponse = await octokit.rest.pulls.get({
9756
- owner: pullRequestRepository.owner,
9757
- repo: pullRequestRepository.repo,
9758
- pull_number: pullRequestLink.data.githubPullRequestNumber,
9759
- headers: {
9760
- "X-GitHub-Api-Version": GITHUB_API_VERSION
9761
- }
9762
- });
9763
- const livePullRequestState = getPullRequestApiState({
9764
- state: pullRequestResponse.data.state,
9765
- merged: pullRequestResponse.data.merged
9766
- }) === "open" ? "open" : "closed";
9767
- if (livePullRequestState !== pullRequestLink.data.githubPullRequestState || pullRequestResponse.data.html_url !== pullRequestLink.data.githubPullRequestUrl || pullRequestResponse.data.title !== pullRequestLink.data.title) {
9768
- await upsertGitHubPullRequestLinkRecord(ctx, {
9769
- companyId: pullRequestLink.data.companyId ?? mappingCompanyId,
9770
- projectId: pullRequestLink.data.paperclipProjectId ?? mappingProjectId,
9771
- 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",
9772
9789
  repositoryUrl: pullRequestRepository.url,
9773
- pullRequestNumber: pullRequestLink.data.githubPullRequestNumber,
9774
- pullRequestUrl: pullRequestResponse.data.html_url ?? pullRequestLink.data.githubPullRequestUrl,
9775
- pullRequestTitle: pullRequestResponse.data.title || pullRequestLink.data.title || `Pull request #${pullRequestLink.data.githubPullRequestNumber}`,
9776
- pullRequestState: livePullRequestState
9790
+ githubIssueNumber: void 0
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
+ }
9777
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
+ }
9778
9852
  }
9779
- if (livePullRequestState !== "open") {
9780
- 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();
9781
9861
  }
9782
- const pullRequest = await getGitHubPullRequestStatusSnapshot(
9783
- octokit,
9784
- pullRequestRepository,
9785
- pullRequestLink.data.githubPullRequestNumber,
9786
- pullRequestStatusCache
9787
- );
9788
- const paperclipIssue = await ctx.issues.get(pullRequestLink.paperclipIssueId, mapping.companyId);
9862
+ const paperclipIssue = await ctx.issues.get(paperclipIssueId, mapping.companyId);
9789
9863
  if (!paperclipIssue) {
9790
9864
  continue;
9791
9865
  }
@@ -9794,12 +9868,12 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
9794
9868
  paperclipIssueSyncContext,
9795
9869
  advancedSettings
9796
9870
  );
9797
- let nextStatus = resolvePaperclipPullRequestIssueStatus({
9871
+ let nextStatus = resolvePaperclipDirectPullRequestIssueStatus({
9798
9872
  currentStatus: paperclipIssue.status,
9799
- pullRequest,
9873
+ pullRequests: pullRequestSnapshots,
9800
9874
  hasExecutorHandoffTarget: Boolean(executorTransitionAssignee)
9801
9875
  });
9802
- 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)) {
9803
9877
  nextStatus = "blocked";
9804
9878
  }
9805
9879
  const shouldPreserveMaintainerWaitRouting = isHealthyMaintainerWaitTransition({
@@ -9807,6 +9881,10 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
9807
9881
  nextStatus,
9808
9882
  syncContext: paperclipIssueSyncContext
9809
9883
  });
9884
+ const shouldClearCompletedExecutionPolicy = shouldClearCompletedSyncExecutionPolicy({
9885
+ nextStatus,
9886
+ syncContext: paperclipIssueSyncContext
9887
+ });
9810
9888
  const nextTransitionAssignee = resolveSyncTransitionAssignee({
9811
9889
  currentStatus: paperclipIssue.status,
9812
9890
  nextStatus,
@@ -9817,20 +9895,20 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
9817
9895
  const nextAssigneeChanged = nextTransitionAssignee ? !doesPaperclipIssueAssigneeMatch(paperclipIssueSyncContext.assignee, nextTransitionAssignee.principal) : false;
9818
9896
  const shouldWakeTransitionAssignee = paperclipIssue.status !== nextStatus && nextTransitionAssignee?.principal.kind === "agent" && isActionablePaperclipIssueStatus(nextStatus) && (nextAssigneeChanged || paperclipIssue.status !== nextStatus);
9819
9897
  if (paperclipIssue.status === nextStatus) {
9820
- if (shouldClearTransitionAssignee) {
9898
+ if (shouldClearTransitionAssignee || shouldClearCompletedExecutionPolicy) {
9821
9899
  updateSyncFailureContext(syncFailureContext, {
9822
9900
  phase: "updating_paperclip_status",
9823
- repositoryUrl: pullRequestRepository.url,
9901
+ repositoryUrl: primaryRepository?.url,
9824
9902
  githubIssueNumber: void 0
9825
9903
  });
9826
9904
  await updatePaperclipIssueState(ctx, {
9827
9905
  companyId: mapping.companyId,
9828
- issueId: pullRequestLink.paperclipIssueId,
9906
+ issueId: paperclipIssueId,
9829
9907
  currentStatus: paperclipIssue.status,
9830
9908
  syncContext: paperclipIssueSyncContext,
9831
9909
  nextStatus,
9832
- clearAssignee: true,
9833
- ...shouldPreserveMaintainerWaitRouting ? { clearExecutionPolicy: true } : {},
9910
+ ...shouldClearTransitionAssignee ? { clearAssignee: true } : {},
9911
+ ...shouldPreserveMaintainerWaitRouting || shouldClearCompletedExecutionPolicy ? { clearExecutionPolicy: true } : {},
9834
9912
  transitionComment: "",
9835
9913
  paperclipApiBaseUrl
9836
9914
  });
@@ -9840,22 +9918,22 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
9840
9918
  const transitionComment = buildPaperclipPullRequestIssueStatusTransitionComment({
9841
9919
  previousStatus: paperclipIssue.status,
9842
9920
  nextStatus,
9843
- pullRequest
9921
+ pullRequests: pullRequestSnapshots
9844
9922
  });
9845
9923
  updateSyncFailureContext(syncFailureContext, {
9846
9924
  phase: "updating_paperclip_status",
9847
- repositoryUrl: pullRequestRepository.url,
9925
+ repositoryUrl: primaryRepository?.url,
9848
9926
  githubIssueNumber: void 0
9849
9927
  });
9850
9928
  await updatePaperclipIssueState(ctx, {
9851
9929
  companyId: mapping.companyId,
9852
- issueId: pullRequestLink.paperclipIssueId,
9930
+ issueId: paperclipIssueId,
9853
9931
  currentStatus: paperclipIssue.status,
9854
9932
  syncContext: paperclipIssueSyncContext,
9855
9933
  nextStatus,
9856
9934
  ...nextTransitionAssignee ? { nextAssignee: nextTransitionAssignee.principal } : {},
9857
9935
  ...shouldClearTransitionAssignee ? { clearAssignee: true } : {},
9858
- ...shouldPreserveMaintainerWaitRouting ? { clearExecutionPolicy: true } : {},
9936
+ ...shouldPreserveMaintainerWaitRouting || shouldClearCompletedExecutionPolicy ? { clearExecutionPolicy: true } : {},
9859
9937
  transitionComment,
9860
9938
  paperclipApiBaseUrl
9861
9939
  });
@@ -9863,7 +9941,7 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
9863
9941
  if (shouldWakeTransitionAssignee && nextTransitionAssignee?.principal.kind === "agent") {
9864
9942
  queuedIssueWakeups.push({
9865
9943
  assigneeAgentId: nextTransitionAssignee.principal.id,
9866
- paperclipIssueId: pullRequestLink.paperclipIssueId,
9944
+ paperclipIssueId,
9867
9945
  reason: STATUS_TRANSITION_WAKE_REASON,
9868
9946
  mutation: "status_transition",
9869
9947
  previousStatus: paperclipIssue.status,
@@ -9875,16 +9953,6 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
9875
9953
  throw error;
9876
9954
  }
9877
9955
  recordRecoverableSyncFailure(ctx, failures, error, syncFailureContext);
9878
- continue;
9879
- } finally {
9880
- completedIssueCount += 1;
9881
- if (onProgress) {
9882
- await onProgress({
9883
- pullRequestLink,
9884
- completedIssueCount,
9885
- totalIssueCount
9886
- });
9887
- }
9888
9956
  }
9889
9957
  }
9890
9958
  await mapWithConcurrency(
@@ -12909,19 +12977,24 @@ async function createProjectPullRequestPaperclipIssue(ctx, input) {
12909
12977
  };
12910
12978
  }
12911
12979
  const requestedTitle = typeof input.title === "string" && input.title.trim() ? input.title.trim() : pullRequest.title.trim();
12912
- const createdIssue = await ctx.issues.create({
12913
- companyId: scope.companyId,
12914
- projectId: scope.projectId,
12915
- title: requestedTitle,
12916
- originKind: GITHUB_PULL_REQUEST_ORIGIN_KIND,
12917
- originId: pullRequestUrl,
12918
- description: buildPaperclipIssueDescriptionFromPullRequest({
12919
- repository: scope.repository,
12920
- pullRequestNumber,
12921
- pullRequestUrl,
12922
- body: pullRequest.body
12923
- })
12924
- });
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
+ );
12925
12998
  const resolvedIssue = await ctx.issues.get(createdIssue.id, scope.companyId) ?? createdIssue;
12926
12999
  await upsertGitHubPullRequestLinkRecord(ctx, {
12927
13000
  companyId: scope.companyId,
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "paperclip-github-plugin",
3
- "version": "0.8.11",
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",