paperclip-github-plugin 0.6.1 → 0.7.1
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 +14 -14
- package/dist/manifest.js +14 -10
- package/dist/worker.js +113 -156
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -44,11 +44,11 @@ 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, 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.
|
|
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.
|
|
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
|
|
|
51
|
-
Long-running syncs continue in the background, so quick actions do not have to wait for the whole import to finish. Once a sync has started, the settings page, dashboard widget, and toolbar actions can request cancellation; the worker stops cooperatively after the current repository or issue step finishes. If the worker restarts mid-run, GitHub Sync now recovers that orphaned `running` state on the next read or control action instead of leaving the UI stuck in `running` or silently restarting the old run.
|
|
51
|
+
Long-running syncs continue in the background, so quick actions do not have to wait for the whole import to finish. Once a sync has started, the settings page, dashboard widget, and toolbar actions can request cancellation; the worker stops cooperatively after the current repository or issue step finishes. If the worker restarts mid-run, GitHub Sync now recovers that orphaned `running` state on the next read or control action instead of leaving the UI stuck in `running` or silently restarting the old run. When sync needs to wake an assigned agent, it uses Paperclip's host-owned issue wakeup API first so blocker, liveness, budget, and auth checks stay centralized, then falls back to the local wakeup route only when an older or partial host bridge cannot service the SDK call.
|
|
52
52
|
|
|
53
53
|
## Highlights
|
|
54
54
|
|
|
@@ -68,9 +68,9 @@ The plugin does more than mirror issue text. It looks at linked pull requests, m
|
|
|
68
68
|
|
|
69
69
|
GitHub Sync exposes a dedicated KPI dashboard widget alongside the operational sync widget. During full company syncs, the worker snapshots the current open GitHub backlog and records when already-imported GitHub issues move from open to closed. The KPI widget turns that worker-owned state into backlog, issue-closure, and Paperclip PR-creation cards with recent history and comparisons against older periods.
|
|
70
70
|
|
|
71
|
-
Because GitHub alone cannot tell which pull requests came from a Paperclip company, the plugin uses explicit Paperclip attribution for delivery activity. `create_pull_request` automatically records a Paperclip-created PR event, and agents that use `gh` or another non-plugin GitHub client can post pull-request-created events to the plugin
|
|
71
|
+
Because GitHub alone cannot tell which pull requests came from a Paperclip company, the plugin uses explicit Paperclip attribution for delivery activity. `create_pull_request` automatically records a Paperclip-created PR event, and agents that use `gh` or another non-plugin GitHub client can post pull-request-created events to the plugin API route so the KPI history stays specific to Paperclip work.
|
|
72
72
|
|
|
73
|
-
That
|
|
73
|
+
That API route path matters on authenticated Paperclip deployments today because a current host bug blocks agents from calling plugin tools unless the instance runs in `local_trusted` mode. Those agents can still use `gh` with the propagated `GITHUB_TOKEN`, then call the agent-authenticated plugin API route from the shell after they create a PR. The Paperclip host authenticates `Authorization: Bearer <PAPERCLIP_API_KEY>`, scopes the request to the calling agent's company, and rejects anonymous or non-agent calls before dispatching to the worker.
|
|
74
74
|
|
|
75
75
|
### Project pull request command center
|
|
76
76
|
|
|
@@ -89,7 +89,7 @@ Paperclip agents can search GitHub for duplicates, read and update issues, post
|
|
|
89
89
|
## Requirements
|
|
90
90
|
|
|
91
91
|
- Node.js 20+
|
|
92
|
-
- a Paperclip host
|
|
92
|
+
- a Paperclip host on `2026.427.0` or newer with plugin installation enabled
|
|
93
93
|
- a GitHub token with API access to the repositories you want to sync
|
|
94
94
|
|
|
95
95
|
## Install from npm
|
|
@@ -178,7 +178,7 @@ The plugin is designed to avoid persisting raw credentials in plugin state.
|
|
|
178
178
|
- On authenticated deployments, any selected propagation agents receive `GITHUB_TOKEN` as an agent env secret-ref binding that points at the same saved GitHub token secret instead of a copied raw token.
|
|
179
179
|
- The worker resolves those secret references at runtime instead of storing raw tokens in plugin state.
|
|
180
180
|
- On authenticated Paperclip deployments, sync is blocked until the relevant company has connected Paperclip board access.
|
|
181
|
-
- KPI
|
|
181
|
+
- KPI API route requests must include `Authorization: Bearer <PAPERCLIP_API_KEY>` from an agent run; the Paperclip host authenticates the token and supplies the agent company before the worker records any metric event.
|
|
182
182
|
|
|
183
183
|
### Optional worker-local token file
|
|
184
184
|
|
|
@@ -208,14 +208,14 @@ The plugin exposes GitHub workflow tools to Paperclip agents, including:
|
|
|
208
208
|
|
|
209
209
|
When an agent sends GitHub body content through the plugin, including issue bodies, pull request descriptions, comments, and review-thread replies, the plugin adds a GitHub-flavored Markdown footer with a horizontal rule and compact heading that discloses AI authorship. If the tool caller supplies `llmModel`, the footer also includes the model name, for example `###### ✨ This comment was AI-generated using gpt-5.4`.
|
|
210
210
|
|
|
211
|
-
### KPI attribution
|
|
211
|
+
### KPI attribution API route
|
|
212
212
|
|
|
213
|
-
The `create_pull_request` tool automatically records a company-level Paperclip PR creation metric. For delivery flows that use `gh` or another non-plugin GitHub client, post a JSON payload to `/api/plugins/paperclip-github-plugin/
|
|
213
|
+
The `create_pull_request` tool automatically records a company-level Paperclip PR creation metric. For delivery flows that use `gh` or another non-plugin GitHub client, post a JSON payload to `/api/plugins/paperclip-github-plugin/api/company-metrics/events` after the PR is created.
|
|
214
214
|
|
|
215
215
|
Supported payload fields:
|
|
216
216
|
|
|
217
217
|
- `metric` required: `pull_request_created`
|
|
218
|
-
- `companyId` optional when
|
|
218
|
+
- `companyId` optional; when present it must match the authenticated agent's company
|
|
219
219
|
- `repository` optional: `owner/repo` or `https://github.com/owner/repo`
|
|
220
220
|
- `pullRequestNumber` optional
|
|
221
221
|
- `pullRequestUrl` optional
|
|
@@ -223,18 +223,18 @@ Supported payload fields:
|
|
|
223
223
|
- `eventKey` optional custom dedupe key
|
|
224
224
|
- `count` optional positive integer
|
|
225
225
|
|
|
226
|
-
Each request must include:
|
|
226
|
+
Each request must be made by a Paperclip agent run and include:
|
|
227
227
|
|
|
228
228
|
- `Authorization: Bearer <PAPERCLIP_API_KEY>`
|
|
229
229
|
|
|
230
|
-
The
|
|
230
|
+
The Paperclip host validates that bearer token and passes the authenticated agent company to the plugin worker. Requests are rejected before worker dispatch when the token is missing, invalid, expired, or not an agent token.
|
|
231
231
|
|
|
232
232
|
Example:
|
|
233
233
|
|
|
234
234
|
```bash
|
|
235
235
|
payload='{"metric":"pull_request_created","repository":"paperclipai/example-repo","pullRequestNumber":21}'
|
|
236
236
|
|
|
237
|
-
curl -X POST "${PAPERCLIP_API_URL%/}/api/plugins/paperclip-github-plugin/
|
|
237
|
+
curl -X POST "${PAPERCLIP_API_URL%/}/api/plugins/paperclip-github-plugin/api/company-metrics/events" \
|
|
238
238
|
-H "content-type: application/json" \
|
|
239
239
|
-H "authorization: Bearer ${PAPERCLIP_API_KEY}" \
|
|
240
240
|
-d "${payload}"
|
|
@@ -244,14 +244,14 @@ The worker deduplicates repeated PR events by preferring the pull request URL, t
|
|
|
244
244
|
|
|
245
245
|
Current host caveat: on authenticated Paperclip deployments, the Paperclip host currently guards `GET /api/plugins/tools` and `POST /api/plugins/tools/execute` with board authentication before dispatching to any plugin worker. If an agent run does not have board access for the target company, GitHub Sync tool discovery and execution fail with `403 {"error":"Board access required"}` before this plugin's worker code runs.
|
|
246
246
|
|
|
247
|
-
Because the KPI attribution endpoint is a
|
|
247
|
+
Because the KPI attribution endpoint is a native plugin JSON route rather than a plugin tool, authenticated agent runs can still call it directly with `PAPERCLIP_API_KEY` even while that host bug blocks the GitHub Sync tool surface.
|
|
248
248
|
|
|
249
249
|
## Troubleshooting
|
|
250
250
|
|
|
251
251
|
- If setup is reported as incomplete, confirm that a GitHub token has been saved or that `${PAPERCLIP_HOME:-~/.paperclip}/plugins/github-sync/config.json` contains `githubToken`, and make sure at least one mapping has a created Paperclip project.
|
|
252
252
|
- If Paperclip says board access is required, open plugin settings inside the affected company and complete the Paperclip board access flow before retrying sync.
|
|
253
253
|
- If GitHub Sync agent tools fail with `403 {"error":"Board access required"}` on `/api/plugins/tools` or `/api/plugins/tools/execute`, the current Paperclip host rejected the request before the plugin worker ran. Re-run from a board-authenticated session or agent run that has board access to the target company.
|
|
254
|
-
- If a KPI
|
|
254
|
+
- If a KPI API route call is rejected, make sure the request includes `Authorization: Bearer ${PAPERCLIP_API_KEY}`, that the token is still valid for the current run, and that any `companyId` in the payload matches the calling agent's company.
|
|
255
255
|
- If the worker reaches an authenticated HTML page instead of the Paperclip API JSON responses it expects, connect Paperclip board access for that company or set `PAPERCLIP_API_URL` to a worker-accessible Paperclip API origin.
|
|
256
256
|
- If a sync run finishes with partial failures, open the saved troubleshooting panel in GitHub Sync to inspect the repository, issue number, raw error, and suggested fix for each recorded failure.
|
|
257
257
|
- If sync says the Paperclip API URL is not trusted, reopen the plugin from the current Paperclip host so the settings UI can refresh the saved origin, or set `PAPERCLIP_API_URL` for the worker.
|
package/dist/manifest.js
CHANGED
|
@@ -508,19 +508,20 @@ var GITHUB_AGENT_TOOLS = [
|
|
|
508
508
|
|
|
509
509
|
// src/kpi-contract.ts
|
|
510
510
|
var GITHUB_SYNC_PLUGIN_ID = "paperclip-github-plugin";
|
|
511
|
-
var
|
|
512
|
-
var
|
|
511
|
+
var COMPANY_METRIC_API_ROUTE_KEY = "record-company-metric-event";
|
|
512
|
+
var COMPANY_METRIC_API_ROUTE_PATH = "/company-metrics/events";
|
|
513
|
+
var COMPANY_METRIC_API_ROUTE_URL_PATH = `/api/plugins/${GITHUB_SYNC_PLUGIN_ID}/api${COMPANY_METRIC_API_ROUTE_PATH}`;
|
|
513
514
|
|
|
514
515
|
// src/manifest.ts
|
|
515
516
|
var require2 = createRequire(import.meta.url);
|
|
516
517
|
var packageJson = require2("../package.json");
|
|
517
|
-
var DASHBOARD_WIDGET_CAPABILITY = "ui.dashboardWidget.register";
|
|
518
518
|
var SCHEDULE_TICK_CRON = "* * * * *";
|
|
519
|
-
var MANIFEST_VERSION = "0.
|
|
519
|
+
var MANIFEST_VERSION = "0.7.1"?.trim() || typeof packageJson.version === "string" && packageJson.version.trim() || process.env.npm_package_version?.trim() || "0.0.0-dev";
|
|
520
520
|
var manifest = {
|
|
521
521
|
id: GITHUB_SYNC_PLUGIN_ID,
|
|
522
522
|
apiVersion: 1,
|
|
523
523
|
version: MANIFEST_VERSION,
|
|
524
|
+
minimumHostVersion: "2026.427.0",
|
|
524
525
|
displayName: "GitHub Sync",
|
|
525
526
|
description: "Synchronize GitHub issues into Paperclip projects.",
|
|
526
527
|
author: "\xC1lvaro S\xE1nchez-Mariscal",
|
|
@@ -528,7 +529,7 @@ var manifest = {
|
|
|
528
529
|
capabilities: [
|
|
529
530
|
"ui.sidebar.register",
|
|
530
531
|
"ui.page.register",
|
|
531
|
-
|
|
532
|
+
"ui.dashboardWidget.register",
|
|
532
533
|
"ui.detailTab.register",
|
|
533
534
|
"ui.commentAnnotation.register",
|
|
534
535
|
"ui.action.register",
|
|
@@ -539,11 +540,12 @@ var manifest = {
|
|
|
539
540
|
"issues.read",
|
|
540
541
|
"issues.create",
|
|
541
542
|
"issues.update",
|
|
543
|
+
"issues.wakeup",
|
|
542
544
|
"issue.comments.read",
|
|
543
545
|
"issue.comments.create",
|
|
544
546
|
"agents.read",
|
|
545
547
|
"jobs.schedule",
|
|
546
|
-
"
|
|
548
|
+
"api.routes.register",
|
|
547
549
|
"http.outbound",
|
|
548
550
|
"secrets.read-ref",
|
|
549
551
|
"agent.tools.register"
|
|
@@ -579,11 +581,13 @@ var manifest = {
|
|
|
579
581
|
schedule: SCHEDULE_TICK_CRON
|
|
580
582
|
}
|
|
581
583
|
],
|
|
582
|
-
|
|
584
|
+
apiRoutes: [
|
|
583
585
|
{
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
586
|
+
routeKey: COMPANY_METRIC_API_ROUTE_KEY,
|
|
587
|
+
method: "POST",
|
|
588
|
+
path: COMPANY_METRIC_API_ROUTE_PATH,
|
|
589
|
+
auth: "agent",
|
|
590
|
+
capability: "api.routes.register"
|
|
587
591
|
}
|
|
588
592
|
],
|
|
589
593
|
tools: GITHUB_AGENT_TOOLS,
|
package/dist/worker.js
CHANGED
|
@@ -563,9 +563,9 @@ function parseRepositoryReference(repositoryInput) {
|
|
|
563
563
|
|
|
564
564
|
// src/kpi-contract.ts
|
|
565
565
|
var GITHUB_SYNC_PLUGIN_ID = "paperclip-github-plugin";
|
|
566
|
-
var
|
|
567
|
-
var
|
|
568
|
-
var
|
|
566
|
+
var COMPANY_METRIC_API_ROUTE_KEY = "record-company-metric-event";
|
|
567
|
+
var COMPANY_METRIC_API_ROUTE_PATH = "/company-metrics/events";
|
|
568
|
+
var COMPANY_METRIC_API_ROUTE_URL_PATH = `/api/plugins/${GITHUB_SYNC_PLUGIN_ID}/api${COMPANY_METRIC_API_ROUTE_PATH}`;
|
|
569
569
|
|
|
570
570
|
// src/paperclip-health.ts
|
|
571
571
|
function normalizeOptionalString(value) {
|
|
@@ -594,6 +594,8 @@ function requiresPaperclipBoardAccess(value) {
|
|
|
594
594
|
}
|
|
595
595
|
|
|
596
596
|
// src/worker.ts
|
|
597
|
+
var GITHUB_ISSUE_ORIGIN_KIND = `plugin:${GITHUB_SYNC_PLUGIN_ID}:github-issue`;
|
|
598
|
+
var GITHUB_PULL_REQUEST_ORIGIN_KIND = `plugin:${GITHUB_SYNC_PLUGIN_ID}:github-pull-request`;
|
|
597
599
|
var SETTINGS_SCOPE = {
|
|
598
600
|
scopeKind: "instance",
|
|
599
601
|
stateKey: "paperclip-github-plugin-settings"
|
|
@@ -5024,7 +5026,10 @@ function resolvePaperclipIssueExecutorAssignee(syncContext, advancedSettings) {
|
|
|
5024
5026
|
return null;
|
|
5025
5027
|
}
|
|
5026
5028
|
function resolveSyncTransitionAssignee(params) {
|
|
5027
|
-
const { nextStatus, syncContext, advancedSettings } = params;
|
|
5029
|
+
const { currentStatus, nextStatus, syncContext, advancedSettings } = params;
|
|
5030
|
+
if (isHealthyMaintainerWaitTransition({ currentStatus, nextStatus, syncContext })) {
|
|
5031
|
+
return null;
|
|
5032
|
+
}
|
|
5028
5033
|
if (nextStatus === "in_review") {
|
|
5029
5034
|
return resolvePaperclipIssueReviewAssignee(syncContext, advancedSettings);
|
|
5030
5035
|
}
|
|
@@ -5033,6 +5038,10 @@ function resolveSyncTransitionAssignee(params) {
|
|
|
5033
5038
|
}
|
|
5034
5039
|
return null;
|
|
5035
5040
|
}
|
|
5041
|
+
function isHealthyMaintainerWaitTransition(params) {
|
|
5042
|
+
const { currentStatus, nextStatus, syncContext } = params;
|
|
5043
|
+
return nextStatus === "in_review" && (currentStatus === "done" || currentStatus === "in_review") && syncContext.executionState === null && syncContext.executionPolicy !== null;
|
|
5044
|
+
}
|
|
5036
5045
|
function doesPaperclipIssueAssigneeMatch(currentAssignee, nextAssignee) {
|
|
5037
5046
|
return isSamePaperclipIssueAssigneePrincipal(currentAssignee, nextAssignee);
|
|
5038
5047
|
}
|
|
@@ -6773,9 +6782,6 @@ function getPaperclipIssueEndpoint(baseUrl, issueId) {
|
|
|
6773
6782
|
function getPaperclipHealthEndpoint(baseUrl) {
|
|
6774
6783
|
return new URL("/api/health", baseUrl).toString();
|
|
6775
6784
|
}
|
|
6776
|
-
function getPaperclipCurrentAgentEndpoint(baseUrl) {
|
|
6777
|
-
return new URL("/api/agents/me", baseUrl).toString();
|
|
6778
|
-
}
|
|
6779
6785
|
function getPaperclipAgentWakeupEndpoint(baseUrl, agentId) {
|
|
6780
6786
|
return new URL(`/api/agents/${agentId}/wakeup`, baseUrl).toString();
|
|
6781
6787
|
}
|
|
@@ -6824,7 +6830,38 @@ async function detectPaperclipBoardAccessRequirement(paperclipApiBaseUrl) {
|
|
|
6824
6830
|
}
|
|
6825
6831
|
}
|
|
6826
6832
|
async function wakePaperclipIssueAssignee(ctx, params) {
|
|
6827
|
-
if (!params.assigneeAgentId
|
|
6833
|
+
if (!params.assigneeAgentId) {
|
|
6834
|
+
return;
|
|
6835
|
+
}
|
|
6836
|
+
if (ctx.issues && typeof ctx.issues.requestWakeup === "function" && params.companyId) {
|
|
6837
|
+
try {
|
|
6838
|
+
await ctx.issues.requestWakeup(params.paperclipIssueId, params.companyId, {
|
|
6839
|
+
reason: params.reason,
|
|
6840
|
+
contextSource: `github-sync.${params.mutation}`,
|
|
6841
|
+
...params.mutation === "import" ? { idempotencyKey: ["github-sync", params.mutation, params.paperclipIssueId].join(":") } : {}
|
|
6842
|
+
});
|
|
6843
|
+
return;
|
|
6844
|
+
} catch (error) {
|
|
6845
|
+
if (!params.paperclipApiBaseUrl) {
|
|
6846
|
+
ctx.logger.warn("GitHub sync could not wake the assignee for a Paperclip issue through the SDK.", {
|
|
6847
|
+
issueId: params.paperclipIssueId,
|
|
6848
|
+
agentId: params.assigneeAgentId,
|
|
6849
|
+
companyId: params.companyId,
|
|
6850
|
+
mutation: params.mutation,
|
|
6851
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6852
|
+
});
|
|
6853
|
+
return;
|
|
6854
|
+
}
|
|
6855
|
+
ctx.logger.warn("GitHub sync could not wake the assignee through the SDK. Falling back to the local Paperclip API.", {
|
|
6856
|
+
issueId: params.paperclipIssueId,
|
|
6857
|
+
agentId: params.assigneeAgentId,
|
|
6858
|
+
companyId: params.companyId,
|
|
6859
|
+
mutation: params.mutation,
|
|
6860
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6861
|
+
});
|
|
6862
|
+
}
|
|
6863
|
+
}
|
|
6864
|
+
if (!params.paperclipApiBaseUrl) {
|
|
6828
6865
|
return;
|
|
6829
6866
|
}
|
|
6830
6867
|
try {
|
|
@@ -7569,6 +7606,7 @@ async function updatePaperclipIssueState(ctx, params) {
|
|
|
7569
7606
|
nextStatus,
|
|
7570
7607
|
nextAssignee,
|
|
7571
7608
|
clearAssignee,
|
|
7609
|
+
clearExecutionPolicy,
|
|
7572
7610
|
transitionComment,
|
|
7573
7611
|
transitionCommentAnnotation,
|
|
7574
7612
|
paperclipApiBaseUrl
|
|
@@ -7583,7 +7621,8 @@ async function updatePaperclipIssueState(ctx, params) {
|
|
|
7583
7621
|
});
|
|
7584
7622
|
const issuePatch = {
|
|
7585
7623
|
status: nextStatus,
|
|
7586
|
-
...syncExecutionStatePatch === null ? { executionState: null } : {}
|
|
7624
|
+
...syncExecutionStatePatch === null ? { executionState: null } : {},
|
|
7625
|
+
...clearExecutionPolicy ? { executionPolicy: null, executionState: null } : {}
|
|
7587
7626
|
};
|
|
7588
7627
|
if (nextAssignee) {
|
|
7589
7628
|
if (nextAssignee.kind === "agent") {
|
|
@@ -7773,6 +7812,8 @@ async function createPaperclipIssue(ctx, mapping, advancedSettings, issue, avail
|
|
|
7773
7812
|
projectId: mapping.paperclipProjectId,
|
|
7774
7813
|
title,
|
|
7775
7814
|
...description ? { description } : {},
|
|
7815
|
+
originKind: GITHUB_ISSUE_ORIGIN_KIND,
|
|
7816
|
+
originId: normalizeGitHubIssueHtmlUrl(issue.htmlUrl) ?? issue.htmlUrl,
|
|
7776
7817
|
...defaultAssignee?.kind === "agent" ? { assigneeAgentId: defaultAssignee.id } : defaultAssignee?.kind === "user" ? { assigneeUserId: defaultAssignee.id } : {}
|
|
7777
7818
|
});
|
|
7778
7819
|
const ensuredCreatedIssueId = createdIssue.id;
|
|
@@ -8093,12 +8134,18 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
|
|
|
8093
8134
|
maintainerAuthoredImportedIssue,
|
|
8094
8135
|
hasExecutorHandoffTarget: Boolean(executorTransitionAssignee)
|
|
8095
8136
|
});
|
|
8137
|
+
const shouldPreserveMaintainerWaitRouting = isHealthyMaintainerWaitTransition({
|
|
8138
|
+
currentStatus: paperclipIssue.status,
|
|
8139
|
+
nextStatus,
|
|
8140
|
+
syncContext: paperclipIssueSyncContext
|
|
8141
|
+
});
|
|
8096
8142
|
const nextTransitionAssignee = resolveSyncTransitionAssignee({
|
|
8143
|
+
currentStatus: paperclipIssue.status,
|
|
8097
8144
|
nextStatus,
|
|
8098
8145
|
syncContext: paperclipIssueSyncContext,
|
|
8099
8146
|
advancedSettings
|
|
8100
8147
|
});
|
|
8101
|
-
const shouldClearTransitionAssignee = nextStatus === "in_review" && nextTransitionAssignee === null && paperclipIssueSyncContext.assignee !== null;
|
|
8148
|
+
const shouldClearTransitionAssignee = nextStatus === "in_review" && (nextTransitionAssignee === null || shouldPreserveMaintainerWaitRouting) && paperclipIssueSyncContext.assignee !== null;
|
|
8102
8149
|
const nextAssigneeChanged = nextTransitionAssignee ? !doesPaperclipIssueAssigneeMatch(paperclipIssueSyncContext.assignee, nextTransitionAssignee.principal) : false;
|
|
8103
8150
|
const shouldWakeImportedAssignee = wasImportedThisRun && paperclipIssue.status === nextStatus && nextStatus === "todo" && paperclipIssueSyncContext.assignee?.kind === "agent";
|
|
8104
8151
|
const shouldWakeTransitionAssignee = paperclipIssue.status !== nextStatus && nextTransitionAssignee?.principal.kind === "agent" && isActionablePaperclipIssueStatus(nextStatus) && (nextAssigneeChanged || paperclipIssue.status !== nextStatus);
|
|
@@ -8120,6 +8167,7 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
|
|
|
8120
8167
|
syncContext: paperclipIssueSyncContext,
|
|
8121
8168
|
nextStatus,
|
|
8122
8169
|
clearAssignee: true,
|
|
8170
|
+
...shouldPreserveMaintainerWaitRouting ? { clearExecutionPolicy: true } : {},
|
|
8123
8171
|
transitionComment: "",
|
|
8124
8172
|
paperclipApiBaseUrl
|
|
8125
8173
|
});
|
|
@@ -8155,6 +8203,7 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
|
|
|
8155
8203
|
nextStatus,
|
|
8156
8204
|
...nextTransitionAssignee ? { nextAssignee: nextTransitionAssignee.principal } : {},
|
|
8157
8205
|
...shouldClearTransitionAssignee ? { clearAssignee: true } : {},
|
|
8206
|
+
...shouldPreserveMaintainerWaitRouting ? { clearExecutionPolicy: true } : {},
|
|
8158
8207
|
transitionComment: transitionComment.body,
|
|
8159
8208
|
transitionCommentAnnotation: transitionComment.annotation,
|
|
8160
8209
|
paperclipApiBaseUrl
|
|
@@ -8278,12 +8327,18 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
|
|
|
8278
8327
|
pullRequest,
|
|
8279
8328
|
hasExecutorHandoffTarget: Boolean(executorTransitionAssignee)
|
|
8280
8329
|
});
|
|
8330
|
+
const shouldPreserveMaintainerWaitRouting = isHealthyMaintainerWaitTransition({
|
|
8331
|
+
currentStatus: paperclipIssue.status,
|
|
8332
|
+
nextStatus,
|
|
8333
|
+
syncContext: paperclipIssueSyncContext
|
|
8334
|
+
});
|
|
8281
8335
|
const nextTransitionAssignee = resolveSyncTransitionAssignee({
|
|
8336
|
+
currentStatus: paperclipIssue.status,
|
|
8282
8337
|
nextStatus,
|
|
8283
8338
|
syncContext: paperclipIssueSyncContext,
|
|
8284
8339
|
advancedSettings
|
|
8285
8340
|
});
|
|
8286
|
-
const shouldClearTransitionAssignee = nextStatus === "in_review" && nextTransitionAssignee === null && paperclipIssueSyncContext.assignee !== null;
|
|
8341
|
+
const shouldClearTransitionAssignee = nextStatus === "in_review" && (nextTransitionAssignee === null || shouldPreserveMaintainerWaitRouting) && paperclipIssueSyncContext.assignee !== null;
|
|
8287
8342
|
const nextAssigneeChanged = nextTransitionAssignee ? !doesPaperclipIssueAssigneeMatch(paperclipIssueSyncContext.assignee, nextTransitionAssignee.principal) : false;
|
|
8288
8343
|
const shouldWakeTransitionAssignee = paperclipIssue.status !== nextStatus && nextTransitionAssignee?.principal.kind === "agent" && isActionablePaperclipIssueStatus(nextStatus) && (nextAssigneeChanged || paperclipIssue.status !== nextStatus);
|
|
8289
8344
|
if (paperclipIssue.status === nextStatus) {
|
|
@@ -8300,6 +8355,7 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
|
|
|
8300
8355
|
syncContext: paperclipIssueSyncContext,
|
|
8301
8356
|
nextStatus,
|
|
8302
8357
|
clearAssignee: true,
|
|
8358
|
+
...shouldPreserveMaintainerWaitRouting ? { clearExecutionPolicy: true } : {},
|
|
8303
8359
|
transitionComment: "",
|
|
8304
8360
|
paperclipApiBaseUrl
|
|
8305
8361
|
});
|
|
@@ -8324,6 +8380,7 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
|
|
|
8324
8380
|
nextStatus,
|
|
8325
8381
|
...nextTransitionAssignee ? { nextAssignee: nextTransitionAssignee.principal } : {},
|
|
8326
8382
|
...shouldClearTransitionAssignee ? { clearAssignee: true } : {},
|
|
8383
|
+
...shouldPreserveMaintainerWaitRouting ? { clearExecutionPolicy: true } : {},
|
|
8327
8384
|
transitionComment,
|
|
8328
8385
|
paperclipApiBaseUrl
|
|
8329
8386
|
});
|
|
@@ -8626,156 +8683,38 @@ async function persistCompanyActivityMetricEvent(ctx, params, options = {}) {
|
|
|
8626
8683
|
...result.eventKey ? { eventKey: result.eventKey } : {}
|
|
8627
8684
|
};
|
|
8628
8685
|
}
|
|
8629
|
-
function
|
|
8630
|
-
if (input.
|
|
8631
|
-
|
|
8632
|
-
}
|
|
8633
|
-
const rawBody = input.rawBody.trim();
|
|
8634
|
-
if (!rawBody) {
|
|
8635
|
-
throw new Error("Webhook body must be a JSON object.");
|
|
8636
|
-
}
|
|
8637
|
-
let parsed;
|
|
8638
|
-
try {
|
|
8639
|
-
parsed = JSON.parse(rawBody);
|
|
8640
|
-
} catch {
|
|
8641
|
-
throw new Error("Webhook body must be valid JSON.");
|
|
8642
|
-
}
|
|
8643
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
8644
|
-
throw new Error("Webhook body must be a JSON object.");
|
|
8645
|
-
}
|
|
8646
|
-
return parsed;
|
|
8647
|
-
}
|
|
8648
|
-
async function resolveCompanyIdForCompanyMetricEvent(ctx, params) {
|
|
8649
|
-
const requestedCompanyId = normalizeCompanyId(params.companyId);
|
|
8650
|
-
if (requestedCompanyId) {
|
|
8651
|
-
return requestedCompanyId;
|
|
8686
|
+
function parseCompanyMetricApiRouteBody(input) {
|
|
8687
|
+
if (!input.body || typeof input.body !== "object" || Array.isArray(input.body)) {
|
|
8688
|
+
throw new Error("Company KPI route body must be a JSON object.");
|
|
8652
8689
|
}
|
|
8653
|
-
|
|
8654
|
-
return void 0;
|
|
8655
|
-
}
|
|
8656
|
-
const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|
|
8657
|
-
const matchingCompanyIds = [
|
|
8658
|
-
...new Set(
|
|
8659
|
-
settings.mappings.filter((mapping) => getNormalizedMappingRepositoryUrl(mapping) === params.repositoryUrl).map((mapping) => normalizeCompanyId(mapping.companyId)).filter((companyId) => Boolean(companyId))
|
|
8660
|
-
)
|
|
8661
|
-
];
|
|
8662
|
-
return matchingCompanyIds.length === 1 ? matchingCompanyIds[0] : void 0;
|
|
8690
|
+
return input.body;
|
|
8663
8691
|
}
|
|
8664
|
-
function
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
if (typeof headerValue === "string") {
|
|
8671
|
-
const trimmedValue = headerValue.trim();
|
|
8672
|
-
return trimmedValue || void 0;
|
|
8673
|
-
}
|
|
8674
|
-
if (!Array.isArray(headerValue)) {
|
|
8675
|
-
continue;
|
|
8676
|
-
}
|
|
8677
|
-
for (const entry of headerValue) {
|
|
8678
|
-
if (typeof entry !== "string") {
|
|
8679
|
-
continue;
|
|
8680
|
-
}
|
|
8681
|
-
const trimmedValue = entry.trim();
|
|
8682
|
-
if (trimmedValue) {
|
|
8683
|
-
return trimmedValue;
|
|
8692
|
+
async function handleCompanyMetricApiRoute(ctx, input) {
|
|
8693
|
+
if (input.routeKey !== COMPANY_METRIC_API_ROUTE_KEY) {
|
|
8694
|
+
return {
|
|
8695
|
+
status: 404,
|
|
8696
|
+
body: {
|
|
8697
|
+
error: `Unsupported plugin API route: ${input.routeKey}.`
|
|
8684
8698
|
}
|
|
8685
|
-
}
|
|
8686
|
-
}
|
|
8687
|
-
return void 0;
|
|
8688
|
-
}
|
|
8689
|
-
function normalizeCompanyMetricWebhookBearerToken(value) {
|
|
8690
|
-
if (!value) {
|
|
8691
|
-
return void 0;
|
|
8692
|
-
}
|
|
8693
|
-
const trimmedValue = value.trim();
|
|
8694
|
-
if (!trimmedValue) {
|
|
8695
|
-
return void 0;
|
|
8696
|
-
}
|
|
8697
|
-
const bearerMatch = trimmedValue.match(/^Bearer\s+(.+)$/i);
|
|
8698
|
-
if (!bearerMatch) {
|
|
8699
|
-
return void 0;
|
|
8700
|
-
}
|
|
8701
|
-
const token = bearerMatch[1]?.trim();
|
|
8702
|
-
return token || void 0;
|
|
8703
|
-
}
|
|
8704
|
-
function normalizePaperclipCurrentAgentRecord(value) {
|
|
8705
|
-
if (!value || typeof value !== "object") {
|
|
8706
|
-
return null;
|
|
8707
|
-
}
|
|
8708
|
-
const record = value;
|
|
8709
|
-
const id = normalizeOptionalString2(record.id);
|
|
8710
|
-
const companyId = normalizeCompanyId(record.companyId);
|
|
8711
|
-
return id && companyId ? {
|
|
8712
|
-
id,
|
|
8713
|
-
companyId
|
|
8714
|
-
} : null;
|
|
8715
|
-
}
|
|
8716
|
-
async function readCompanyMetricWebhookCurrentAgent(paperclipApiBaseUrl, bearerToken) {
|
|
8717
|
-
const response = await fetchPaperclipApi(getPaperclipCurrentAgentEndpoint(paperclipApiBaseUrl), {
|
|
8718
|
-
method: "GET",
|
|
8719
|
-
headers: {
|
|
8720
|
-
accept: "application/json",
|
|
8721
|
-
authorization: `Bearer ${bearerToken}`
|
|
8722
|
-
}
|
|
8723
|
-
});
|
|
8724
|
-
const payloadResult = await readPaperclipApiJsonResponse(response, {
|
|
8725
|
-
operationLabel: "current agent"
|
|
8726
|
-
});
|
|
8727
|
-
if (payloadResult.failure) {
|
|
8728
|
-
if (payloadResult.failure.requiresAuthentication) {
|
|
8729
|
-
throw new Error("Company KPI webhook Authorization must be a valid PAPERCLIP_API_KEY bearer token.");
|
|
8730
|
-
}
|
|
8731
|
-
const detail = payloadResult.failure.errorMessage ? ` ${payloadResult.failure.errorMessage}` : "";
|
|
8732
|
-
throw new Error(`Could not validate the KPI webhook Paperclip API key.${detail}`);
|
|
8733
|
-
}
|
|
8734
|
-
const agent = normalizePaperclipCurrentAgentRecord(payloadResult.data);
|
|
8735
|
-
if (!agent) {
|
|
8736
|
-
throw new Error("Paperclip did not return a usable current agent record while validating the KPI webhook caller.");
|
|
8737
|
-
}
|
|
8738
|
-
return agent;
|
|
8739
|
-
}
|
|
8740
|
-
async function assertCompanyMetricWebhookAuthenticated(ctx, input, companyId) {
|
|
8741
|
-
const rawAuthorization = getWebhookHeaderValue(input.headers, COMPANY_METRIC_WEBHOOK_AUTH_HEADER);
|
|
8742
|
-
const bearerToken = normalizeCompanyMetricWebhookBearerToken(rawAuthorization);
|
|
8743
|
-
if (!bearerToken) {
|
|
8744
|
-
throw new Error(
|
|
8745
|
-
`Missing or invalid ${COMPANY_METRIC_WEBHOOK_AUTH_HEADER} header. Use Bearer <PAPERCLIP_API_KEY>.`
|
|
8746
|
-
);
|
|
8699
|
+
};
|
|
8747
8700
|
}
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
const paperclipApiBaseUrl = getConfiguredPaperclipApiBaseUrl(settings, config, companyId);
|
|
8751
|
-
if (!paperclipApiBaseUrl) {
|
|
8752
|
-
throw new Error(
|
|
8753
|
-
"A trusted Paperclip API origin is required to validate PAPERCLIP_API_KEY. Set PAPERCLIP_API_URL or save the Paperclip host origin before sending KPI webhook events."
|
|
8754
|
-
);
|
|
8701
|
+
if (input.actor.actorType !== "agent") {
|
|
8702
|
+
throw new Error("Company KPI metric events must be recorded by an authenticated Paperclip agent.");
|
|
8755
8703
|
}
|
|
8756
|
-
const
|
|
8757
|
-
if (
|
|
8758
|
-
throw new Error("Company KPI
|
|
8704
|
+
const companyId = normalizeCompanyId(input.companyId);
|
|
8705
|
+
if (!companyId) {
|
|
8706
|
+
throw new Error("Company KPI metric events require the host to provide the authenticated agent company.");
|
|
8759
8707
|
}
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
if (
|
|
8763
|
-
throw new Error(
|
|
8708
|
+
const payload = parseCompanyMetricApiRouteBody(input);
|
|
8709
|
+
const requestedCompanyId = normalizeCompanyId(payload.companyId);
|
|
8710
|
+
if (requestedCompanyId && requestedCompanyId !== companyId) {
|
|
8711
|
+
throw new Error("companyId must match the authenticated Paperclip agent company.");
|
|
8764
8712
|
}
|
|
8765
|
-
const payload = parseWebhookPayloadRecord(input);
|
|
8766
8713
|
const repositoryInput = normalizeOptionalString2(payload.repository);
|
|
8767
8714
|
const repository = repositoryInput ? parseRepositoryReference(repositoryInput) : null;
|
|
8768
8715
|
if (repositoryInput && !repository) {
|
|
8769
8716
|
throw new Error("repository must be owner/repo or https://github.com/owner/repo.");
|
|
8770
8717
|
}
|
|
8771
|
-
const companyId = await resolveCompanyIdForCompanyMetricEvent(ctx, {
|
|
8772
|
-
companyId: payload.companyId,
|
|
8773
|
-
repositoryUrl: repository?.url
|
|
8774
|
-
});
|
|
8775
|
-
if (!companyId) {
|
|
8776
|
-
throw new Error("companyId is required unless repository maps to exactly one company.");
|
|
8777
|
-
}
|
|
8778
|
-
await assertCompanyMetricWebhookAuthenticated(ctx, input, companyId);
|
|
8779
8718
|
const metric = normalizeCompanyActivityMetricInputValue(payload.metric);
|
|
8780
8719
|
if (!metric) {
|
|
8781
8720
|
throw new Error('metric must be "pull_request_created".');
|
|
@@ -8792,7 +8731,7 @@ async function handleCompanyMetricWebhook(ctx, input) {
|
|
|
8792
8731
|
});
|
|
8793
8732
|
if (!dedupeKey) {
|
|
8794
8733
|
throw new Error(
|
|
8795
|
-
"Company KPI
|
|
8734
|
+
"Company KPI metric events require pullRequestUrl, repository plus pullRequestNumber, or eventKey so duplicate deliveries can be ignored."
|
|
8796
8735
|
);
|
|
8797
8736
|
}
|
|
8798
8737
|
const recordedMetric = await persistCompanyActivityMetricEvent(
|
|
@@ -8812,17 +8751,27 @@ async function handleCompanyMetricWebhook(ctx, input) {
|
|
|
8812
8751
|
}
|
|
8813
8752
|
);
|
|
8814
8753
|
ctx.logger.info(
|
|
8815
|
-
recordedMetric.recorded ? "GitHub Sync recorded a company KPI
|
|
8754
|
+
recordedMetric.recorded ? "GitHub Sync recorded a company KPI API route event." : "GitHub Sync ignored a duplicate company KPI API route event.",
|
|
8816
8755
|
{
|
|
8817
|
-
|
|
8756
|
+
routeKey: input.routeKey,
|
|
8818
8757
|
companyId,
|
|
8819
8758
|
metric,
|
|
8820
8759
|
repositoryUrl: repository?.url,
|
|
8821
8760
|
pullRequestNumber,
|
|
8822
8761
|
pullRequestUrl,
|
|
8823
|
-
|
|
8762
|
+
agentId: input.actor.agentId ?? null,
|
|
8763
|
+
runId: input.actor.runId ?? null
|
|
8824
8764
|
}
|
|
8825
8765
|
);
|
|
8766
|
+
return {
|
|
8767
|
+
status: recordedMetric.recorded ? 201 : 200,
|
|
8768
|
+
body: {
|
|
8769
|
+
status: recordedMetric.recorded ? "recorded" : "duplicate",
|
|
8770
|
+
recorded: recordedMetric.recorded,
|
|
8771
|
+
companyId,
|
|
8772
|
+
metric: "pull_request_created"
|
|
8773
|
+
}
|
|
8774
|
+
};
|
|
8826
8775
|
}
|
|
8827
8776
|
async function createGitHubToolOctokit(ctx, companyId) {
|
|
8828
8777
|
const token = (await resolveGithubToken(ctx, { companyId })).trim();
|
|
@@ -11149,6 +11098,8 @@ async function createProjectPullRequestPaperclipIssue(ctx, input) {
|
|
|
11149
11098
|
companyId: scope.companyId,
|
|
11150
11099
|
projectId: scope.projectId,
|
|
11151
11100
|
title: requestedTitle,
|
|
11101
|
+
originKind: GITHUB_PULL_REQUEST_ORIGIN_KIND,
|
|
11102
|
+
originId: pullRequestUrl,
|
|
11152
11103
|
description: buildPaperclipIssueDescriptionFromPullRequest({
|
|
11153
11104
|
repository: scope.repository,
|
|
11154
11105
|
pullRequestNumber,
|
|
@@ -13309,6 +13260,11 @@ function shouldStartWorkerHost(moduleUrl, entry = process.argv[1]) {
|
|
|
13309
13260
|
return resolve(entry) === resolve(modulePath);
|
|
13310
13261
|
}
|
|
13311
13262
|
}
|
|
13263
|
+
var __testing = {
|
|
13264
|
+
buildSyncFallbackExecutionStatePatch,
|
|
13265
|
+
isHealthyMaintainerWaitTransition,
|
|
13266
|
+
resolveSyncTransitionAssignee
|
|
13267
|
+
};
|
|
13312
13268
|
var plugin = definePlugin({
|
|
13313
13269
|
async setup(ctx) {
|
|
13314
13270
|
pluginRuntimeContext = ctx;
|
|
@@ -13676,11 +13632,11 @@ var plugin = definePlugin({
|
|
|
13676
13632
|
}
|
|
13677
13633
|
});
|
|
13678
13634
|
},
|
|
13679
|
-
async
|
|
13635
|
+
async onApiRequest(input) {
|
|
13680
13636
|
if (!pluginRuntimeContext) {
|
|
13681
|
-
throw new Error("GitHub Sync worker is not ready to handle
|
|
13637
|
+
throw new Error("GitHub Sync worker is not ready to handle API routes yet.");
|
|
13682
13638
|
}
|
|
13683
|
-
|
|
13639
|
+
return handleCompanyMetricApiRoute(pluginRuntimeContext, input);
|
|
13684
13640
|
},
|
|
13685
13641
|
async onShutdown() {
|
|
13686
13642
|
pluginRuntimeContext = null;
|
|
@@ -13691,6 +13647,7 @@ if (shouldStartWorkerHost(import.meta.url)) {
|
|
|
13691
13647
|
startWorkerRpcHost({ plugin });
|
|
13692
13648
|
}
|
|
13693
13649
|
export {
|
|
13650
|
+
__testing,
|
|
13694
13651
|
worker_default as default,
|
|
13695
13652
|
shouldStartWorkerHost
|
|
13696
13653
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paperclip-github-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Paperclip plugin for synchronizing GitHub issues into Paperclip projects.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@octokit/rest": "^22.0.1",
|
|
44
|
-
"@paperclipai/plugin-sdk": "^2026.
|
|
44
|
+
"@paperclipai/plugin-sdk": "^2026.427.0",
|
|
45
45
|
"react": "^19.2.5",
|
|
46
46
|
"react-markdown": "^10.1.0",
|
|
47
47
|
"rehype-raw": "^7.0.0",
|