paperclip-github-plugin 0.9.6 → 0.9.8
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 +3 -0
- package/dist/manifest.js +1 -1
- package/dist/worker.js +105 -6
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -94,6 +94,8 @@ 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
|
+
When a Paperclip issue is linked to a GitHub issue and also has older direct pull request links, the GitHub issue remains the status source of truth. Direct pull request links only drive status for PR-only Paperclip issues, which prevents stale merged PR metadata from closing work while the GitHub issue and its current linked PR are still open.
|
|
98
|
+
|
|
97
99
|
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
100
|
|
|
99
101
|
### Agent workflows built in
|
|
@@ -184,6 +186,7 @@ Additional behavior:
|
|
|
184
186
|
- 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.
|
|
185
187
|
- Open imported issues that are already in `backlog` stay in `backlog` until someone changes them in Paperclip.
|
|
186
188
|
- If an imported issue is `done` or `cancelled` and GitHub shows it open again with no linked pull request, sync moves it to `todo` so agents can pick it up again.
|
|
189
|
+
- When an issue has an active Paperclip issue monitor, GitHub Sync lets that monitor own the wait. It still refreshes GitHub metadata, but it does not change the issue's Paperclip status, assignee, execution state, transition comments, or wakeups until the monitor is no longer active.
|
|
187
190
|
- Trusted new GitHub comments from the original issue author or a verified maintainer/admin can move an open imported issue back into active work, whether the new comment lands on the source issue, in a linked pull request's top-level comment stream, or in a linked pull request review thread; GitHub Sync uses `in_progress` when it can route the issue to an executor and otherwise `todo`.
|
|
188
191
|
- When the sync changes a Paperclip issue status, it adds a Paperclip comment explaining what changed and why.
|
|
189
192
|
|
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.9.
|
|
645
|
+
var MANIFEST_VERSION = "0.9.8"?.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
|
@@ -5418,7 +5418,8 @@ function normalizePaperclipIssueExecutionState(value) {
|
|
|
5418
5418
|
const lastDecisionId = normalizeOptionalString2(record.lastDecisionId) ?? null;
|
|
5419
5419
|
const lastDecisionOutcome = normalizeOptionalString2(record.lastDecisionOutcome) ?? null;
|
|
5420
5420
|
const completedStageIds = Array.isArray(record.completedStageIds) ? record.completedStageIds.map((stageId) => normalizeOptionalString2(stageId)).filter((stageId) => Boolean(stageId)) : [];
|
|
5421
|
-
|
|
5421
|
+
const monitor = normalizePaperclipIssueMonitorState(record.monitor);
|
|
5422
|
+
if (!status && currentStageId === null && currentStageIndex === null && currentStageType === null && currentParticipant === null && returnAssignee === null && completedStageIds.length === 0 && lastDecisionId === null && lastDecisionOutcome === null && monitor === null) {
|
|
5422
5423
|
return null;
|
|
5423
5424
|
}
|
|
5424
5425
|
return {
|
|
@@ -5430,7 +5431,25 @@ function normalizePaperclipIssueExecutionState(value) {
|
|
|
5430
5431
|
returnAssignee,
|
|
5431
5432
|
completedStageIds,
|
|
5432
5433
|
...lastDecisionId !== null ? { lastDecisionId } : {},
|
|
5433
|
-
...lastDecisionOutcome !== null ? { lastDecisionOutcome } : {}
|
|
5434
|
+
...lastDecisionOutcome !== null ? { lastDecisionOutcome } : {},
|
|
5435
|
+
...monitor !== null ? { monitor } : {}
|
|
5436
|
+
};
|
|
5437
|
+
}
|
|
5438
|
+
function normalizePaperclipIssueMonitorState(value) {
|
|
5439
|
+
if (!value || typeof value !== "object") {
|
|
5440
|
+
return null;
|
|
5441
|
+
}
|
|
5442
|
+
const record = value;
|
|
5443
|
+
const status = normalizeOptionalString2(record.status);
|
|
5444
|
+
const clearedAt = normalizeOptionalString2(record.clearedAt) ?? null;
|
|
5445
|
+
const clearReason = normalizeOptionalString2(record.clearReason) ?? null;
|
|
5446
|
+
if (!status && clearedAt === null && clearReason === null && Object.keys(record).length === 0) {
|
|
5447
|
+
return null;
|
|
5448
|
+
}
|
|
5449
|
+
return {
|
|
5450
|
+
...status ? { status } : {},
|
|
5451
|
+
clearedAt,
|
|
5452
|
+
clearReason
|
|
5434
5453
|
};
|
|
5435
5454
|
}
|
|
5436
5455
|
function getPaperclipIssueAssigneePrincipal(issue) {
|
|
@@ -5456,6 +5475,17 @@ function getPaperclipIssueSyncContext(issue) {
|
|
|
5456
5475
|
executionState: normalizePaperclipIssueExecutionState(record.executionState)
|
|
5457
5476
|
};
|
|
5458
5477
|
}
|
|
5478
|
+
function hasActivePaperclipIssueMonitor(syncContext) {
|
|
5479
|
+
const monitor = syncContext.executionState?.monitor;
|
|
5480
|
+
if (!monitor) {
|
|
5481
|
+
return false;
|
|
5482
|
+
}
|
|
5483
|
+
if (monitor.clearedAt || monitor.clearReason) {
|
|
5484
|
+
return false;
|
|
5485
|
+
}
|
|
5486
|
+
const normalizedStatus = monitor.status?.trim().toLowerCase();
|
|
5487
|
+
return normalizedStatus !== "cleared" && normalizedStatus !== "completed" && normalizedStatus !== "cancelled" && normalizedStatus !== "canceled";
|
|
5488
|
+
}
|
|
5459
5489
|
function hasUnresolvedPaperclipIssueBlockerSummary(blockers) {
|
|
5460
5490
|
if (!Array.isArray(blockers)) {
|
|
5461
5491
|
return false;
|
|
@@ -5675,6 +5705,13 @@ function isClearableMaintainerWaitExecutionState(executionState) {
|
|
|
5675
5705
|
if (executionState === null) {
|
|
5676
5706
|
return true;
|
|
5677
5707
|
}
|
|
5708
|
+
if (hasActivePaperclipIssueMonitor({
|
|
5709
|
+
assignee: null,
|
|
5710
|
+
executionPolicy: null,
|
|
5711
|
+
executionState
|
|
5712
|
+
})) {
|
|
5713
|
+
return false;
|
|
5714
|
+
}
|
|
5678
5715
|
if (executionState.currentParticipant !== null || executionState.returnAssignee !== null || executionState.currentStageId !== null || executionState.currentStageIndex !== null || executionState.currentStageType !== null || executionState.completedStageIds.length > 0 || executionState.lastDecisionId || executionState.lastDecisionOutcome) {
|
|
5679
5716
|
return false;
|
|
5680
5717
|
}
|
|
@@ -7347,6 +7384,23 @@ async function listGitHubPullRequestIssueLinksForMapping(ctx, mapping, target) {
|
|
|
7347
7384
|
}
|
|
7348
7385
|
return [...recordsByKey.values()];
|
|
7349
7386
|
}
|
|
7387
|
+
async function listGitHubIssueLinkedPaperclipIssueIdsForMapping(ctx, mapping, importedIssueRecords, target) {
|
|
7388
|
+
const issueIds = new Set(
|
|
7389
|
+
importedIssueRecords.map((record) => record.paperclipIssueId).filter(Boolean)
|
|
7390
|
+
);
|
|
7391
|
+
if (target?.kind === "issue" && target.issueId && target.githubIssueNumber) {
|
|
7392
|
+
issueIds.add(target.issueId);
|
|
7393
|
+
}
|
|
7394
|
+
const issueLinks = await listGitHubIssueLinkRecords(ctx, {
|
|
7395
|
+
...target?.kind === "issue" && target.issueId ? { paperclipIssueId: target.issueId } : {}
|
|
7396
|
+
});
|
|
7397
|
+
for (const record of issueLinks) {
|
|
7398
|
+
if (doesGitHubIssueLinkRecordMatchMapping(record, mapping) && doesGitHubIssueLinkRecordMatchTarget(record, target)) {
|
|
7399
|
+
issueIds.add(record.paperclipIssueId);
|
|
7400
|
+
}
|
|
7401
|
+
}
|
|
7402
|
+
return issueIds;
|
|
7403
|
+
}
|
|
7350
7404
|
function doesGitHubIssueLinkRecordMatchMapping(record, mapping) {
|
|
7351
7405
|
if (record.data.repositoryUrl !== getNormalizedMappingRepositoryUrl(mapping)) {
|
|
7352
7406
|
return false;
|
|
@@ -7384,6 +7438,7 @@ async function listExternalGitHubLinkSyncWork(ctx, mappings, target) {
|
|
|
7384
7438
|
const syncableMappings = getSyncableMappingsForTarget(mappings, target);
|
|
7385
7439
|
const issueLinksByKey = /* @__PURE__ */ new Map();
|
|
7386
7440
|
const pullRequestLinksByKey = /* @__PURE__ */ new Map();
|
|
7441
|
+
const issueLinkedPaperclipIssueIds = /* @__PURE__ */ new Set();
|
|
7387
7442
|
const [issueLinks, pullRequestLinks] = await Promise.all([
|
|
7388
7443
|
listGitHubIssueLinkRecords(ctx, {
|
|
7389
7444
|
...target?.kind === "issue" && target.issueId ? { paperclipIssueId: target.issueId } : {}
|
|
@@ -7393,7 +7448,11 @@ async function listExternalGitHubLinkSyncWork(ctx, mappings, target) {
|
|
|
7393
7448
|
})
|
|
7394
7449
|
]);
|
|
7395
7450
|
for (const record of issueLinks) {
|
|
7396
|
-
if (!doesGitHubIssueLinkRecordMatchTarget(record, target)
|
|
7451
|
+
if (!doesGitHubIssueLinkRecordMatchTarget(record, target)) {
|
|
7452
|
+
continue;
|
|
7453
|
+
}
|
|
7454
|
+
issueLinkedPaperclipIssueIds.add(record.paperclipIssueId);
|
|
7455
|
+
if (isGitHubIssueLinkCoveredByMappings(record, syncableMappings)) {
|
|
7397
7456
|
continue;
|
|
7398
7457
|
}
|
|
7399
7458
|
issueLinksByKey.set(
|
|
@@ -7402,7 +7461,7 @@ async function listExternalGitHubLinkSyncWork(ctx, mappings, target) {
|
|
|
7402
7461
|
);
|
|
7403
7462
|
}
|
|
7404
7463
|
for (const record of pullRequestLinks) {
|
|
7405
|
-
if (!doesGitHubPullRequestLinkRecordMatchTarget(record, target) || isGitHubPullRequestLinkCoveredByMappings(record, syncableMappings)) {
|
|
7464
|
+
if (!doesGitHubPullRequestLinkRecordMatchTarget(record, target) || issueLinkedPaperclipIssueIds.has(record.paperclipIssueId) || isGitHubPullRequestLinkCoveredByMappings(record, syncableMappings)) {
|
|
7406
7465
|
continue;
|
|
7407
7466
|
}
|
|
7408
7467
|
pullRequestLinksByKey.set(
|
|
@@ -9496,6 +9555,19 @@ async function cancelUnmappedTransferredGitHubIssue(ctx, params) {
|
|
|
9496
9555
|
updatedStatusesCount: 0
|
|
9497
9556
|
};
|
|
9498
9557
|
}
|
|
9558
|
+
const paperclipIssueSyncContext = getPaperclipIssueSyncContext(paperclipIssue);
|
|
9559
|
+
if (hasActivePaperclipIssueMonitor(paperclipIssueSyncContext)) {
|
|
9560
|
+
ctx.logger.info("GitHub sync skipped transferred issue cancellation because an issue monitor is active.", {
|
|
9561
|
+
companyId,
|
|
9562
|
+
issueId: params.importedIssue.paperclipIssueId,
|
|
9563
|
+
transferredRepositoryUrl: params.transferredRepository.url,
|
|
9564
|
+
currentStatus: paperclipIssue.status,
|
|
9565
|
+
resolvedStatus: "cancelled"
|
|
9566
|
+
});
|
|
9567
|
+
return {
|
|
9568
|
+
updatedStatusesCount: 0
|
|
9569
|
+
};
|
|
9570
|
+
}
|
|
9499
9571
|
await unlinkPaperclipIssueFromGitHub(ctx, {
|
|
9500
9572
|
companyId,
|
|
9501
9573
|
issueId: params.importedIssue.paperclipIssueId
|
|
@@ -9505,7 +9577,7 @@ async function cancelUnmappedTransferredGitHubIssue(ctx, params) {
|
|
|
9505
9577
|
companyId,
|
|
9506
9578
|
issueId: params.importedIssue.paperclipIssueId,
|
|
9507
9579
|
currentStatus: paperclipIssue.status,
|
|
9508
|
-
syncContext:
|
|
9580
|
+
syncContext: paperclipIssueSyncContext,
|
|
9509
9581
|
nextStatus,
|
|
9510
9582
|
transitionComment: buildUnmappedTransferredIssueCancellationComment({
|
|
9511
9583
|
previousStatus: paperclipIssue.status,
|
|
@@ -9824,6 +9896,17 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
|
|
|
9824
9896
|
importedIssue.lastSeenCommentCount = snapshot.commentCount;
|
|
9825
9897
|
importedIssue.lastSeenGitHubState = snapshot.state;
|
|
9826
9898
|
importedIssue.linkedPullRequestCommentCounts = currentLinkedPullRequestCommentCounts;
|
|
9899
|
+
if (hasActivePaperclipIssueMonitor(paperclipIssueSyncContext)) {
|
|
9900
|
+
ctx.logger.info("GitHub sync skipped Paperclip issue state changes because an issue monitor is active.", {
|
|
9901
|
+
companyId: mapping.companyId,
|
|
9902
|
+
issueId: importedIssue.paperclipIssueId,
|
|
9903
|
+
repositoryUrl: repository.url,
|
|
9904
|
+
githubIssueNumber: githubIssue.number,
|
|
9905
|
+
currentStatus: paperclipIssue.status,
|
|
9906
|
+
resolvedStatus: nextStatus
|
|
9907
|
+
});
|
|
9908
|
+
continue;
|
|
9909
|
+
}
|
|
9827
9910
|
if (paperclipIssue.status === nextStatus) {
|
|
9828
9911
|
if (shouldClearTransitionAssignee || shouldClearCompletedExecutionPolicy) {
|
|
9829
9912
|
updateSyncFailureContext(syncFailureContext, {
|
|
@@ -10080,6 +10163,16 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
|
|
|
10080
10163
|
const shouldClearTransitionAssignee = nextStatus === "in_review" && (nextTransitionAssignee === null || shouldPreserveMaintainerWaitRouting) && paperclipIssueSyncContext.assignee !== null;
|
|
10081
10164
|
const nextAssigneeChanged = nextTransitionAssignee ? !doesPaperclipIssueAssigneeMatch(paperclipIssueSyncContext.assignee, nextTransitionAssignee.principal) : false;
|
|
10082
10165
|
const shouldWakeTransitionAssignee = paperclipIssue.status !== nextStatus && nextTransitionAssignee?.principal.kind === "agent" && isActionablePaperclipIssueStatus(nextStatus) && (nextAssigneeChanged || paperclipIssue.status !== nextStatus);
|
|
10166
|
+
if (hasActivePaperclipIssueMonitor(paperclipIssueSyncContext)) {
|
|
10167
|
+
ctx.logger.info("GitHub sync skipped Paperclip pull request issue state changes because an issue monitor is active.", {
|
|
10168
|
+
companyId: mapping.companyId,
|
|
10169
|
+
issueId: paperclipIssueId,
|
|
10170
|
+
repositoryUrl: primaryRepository?.url,
|
|
10171
|
+
currentStatus: paperclipIssue.status,
|
|
10172
|
+
resolvedStatus: nextStatus
|
|
10173
|
+
});
|
|
10174
|
+
continue;
|
|
10175
|
+
}
|
|
10083
10176
|
if (paperclipIssue.status === nextStatus) {
|
|
10084
10177
|
if (shouldClearTransitionAssignee || shouldClearCompletedExecutionPolicy) {
|
|
10085
10178
|
updateSyncFailureContext(syncFailureContext, {
|
|
@@ -14142,7 +14235,13 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
14142
14235
|
const importRegistryByIssueId = new Map(
|
|
14143
14236
|
importedIssueRecords.map((entry) => [entry.githubIssueId, entry])
|
|
14144
14237
|
);
|
|
14145
|
-
const
|
|
14238
|
+
const githubIssueLinkedPaperclipIssueIds = await listGitHubIssueLinkedPaperclipIssueIdsForMapping(
|
|
14239
|
+
ctx,
|
|
14240
|
+
mapping,
|
|
14241
|
+
importedIssueRecords,
|
|
14242
|
+
options.target
|
|
14243
|
+
);
|
|
14244
|
+
const pullRequestLinks = (await listGitHubPullRequestIssueLinksForMapping(ctx, mapping, options.target)).filter((record) => !githubIssueLinkedPaperclipIssueIds.has(record.paperclipIssueId));
|
|
14146
14245
|
const ensuredPaperclipIssueIds = /* @__PURE__ */ new Map();
|
|
14147
14246
|
const trackedIssueIds = /* @__PURE__ */ new Set([
|
|
14148
14247
|
...issues.map((issue) => issue.id),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paperclip-github-plugin",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8",
|
|
4
4
|
"description": "Paperclip plugin for synchronizing GitHub issues into Paperclip projects.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -49,11 +49,11 @@
|
|
|
49
49
|
"remark-gfm": "^4.0.1"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@types/node": "24.12.
|
|
52
|
+
"@types/node": "24.12.4",
|
|
53
53
|
"@types/react": "19.2.14",
|
|
54
54
|
"esbuild": "0.28.0",
|
|
55
|
-
"playwright": "1.
|
|
56
|
-
"tsx": "4.
|
|
55
|
+
"playwright": "1.60.0",
|
|
56
|
+
"tsx": "4.22.0",
|
|
57
57
|
"typescript": "6.0.3"
|
|
58
58
|
}
|
|
59
59
|
}
|