@triagepilot/provider-github 1.1.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/LICENSE ADDED
@@ -0,0 +1,105 @@
1
+ # Functional Source License, Version 1.1, Apache 2.0 Future License
2
+
3
+ ## Abbreviation
4
+
5
+ FSL-1.1-Apache-2.0
6
+
7
+ ## Notice
8
+
9
+ Copyright 2026 Miroslav Babjak
10
+
11
+ ## Terms and Conditions
12
+
13
+ ### Licensor ("We")
14
+
15
+ The party offering the Software under these Terms and Conditions.
16
+
17
+ ### The Software
18
+
19
+ The "Software" is each version of the software that we make available under
20
+ these Terms and Conditions, as indicated by our inclusion of these Terms and
21
+ Conditions with the Software.
22
+
23
+ ### License Grant
24
+
25
+ Subject to your compliance with this License Grant and the Patents,
26
+ Redistribution and Trademark clauses below, we hereby grant you the right to
27
+ use, copy, modify, create derivative works, publicly perform, publicly display
28
+ and redistribute the Software for any Permitted Purpose identified below.
29
+
30
+ ### Permitted Purpose
31
+
32
+ A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
33
+ means making the Software available to others in a commercial product or
34
+ service that:
35
+
36
+ 1. substitutes for the Software;
37
+
38
+ 2. substitutes for any other product or service we offer using the Software
39
+ that exists as of the date we make the Software available; or
40
+
41
+ 3. offers the same or substantially similar functionality as the Software.
42
+
43
+ Permitted Purposes specifically include using the Software:
44
+
45
+ 1. for your internal use and access;
46
+
47
+ 2. for non-commercial education;
48
+
49
+ 3. for non-commercial research; and
50
+
51
+ 4. in connection with professional services that you provide to a licensee
52
+ using the Software in accordance with these Terms and Conditions.
53
+
54
+ ### Patents
55
+
56
+ To the extent your use for a Permitted Purpose would necessarily infringe our
57
+ patents, the license grant above includes a license under our patents. If you
58
+ make a claim against any party that the Software infringes or contributes to
59
+ the infringement of any patent, then your patent license to the Software ends
60
+ immediately.
61
+
62
+ ### Redistribution
63
+
64
+ The Terms and Conditions apply to all copies, modifications and derivatives of
65
+ the Software.
66
+
67
+ If you redistribute any copies, modifications or derivatives of the Software,
68
+ you must include a copy of or a link to these Terms and Conditions and not
69
+ remove any copyright notices provided in or with the Software.
70
+
71
+ ### Disclaimer
72
+
73
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
74
+ IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
75
+ PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
76
+
77
+ IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
78
+ SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
79
+ EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
80
+
81
+ ### Trademarks
82
+
83
+ Except for displaying the License Details and identifying us as the origin of
84
+ the Software, you have no right under these Terms and Conditions to use our
85
+ trademarks, trade names, service marks or product names.
86
+
87
+ ## Grant of Future License
88
+
89
+ We hereby irrevocably grant you an additional license to use the Software under
90
+ the Apache License, Version 2.0 that is effective on the second anniversary of
91
+ the date we make the Software available. On or after that date, you may use the
92
+ Software under the Apache License, Version 2.0, in which case the following
93
+ will apply:
94
+
95
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
96
+ this file except in compliance with the License.
97
+
98
+ You may obtain a copy of the License at
99
+
100
+ http://www.apache.org/licenses/LICENSE-2.0
101
+
102
+ Unless required by applicable law or agreed to in writing, software distributed
103
+ under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
104
+ CONDITIONS OF ANY KIND, either express or implied. See the License for the
105
+ specific language governing permissions and limitations under the License.
@@ -0,0 +1,142 @@
1
+ import type { ConfigurationDocument, ConfigurationSource, RepositoryRef as ContractRepositoryRef, RiskTier, WorkspaceId } from "@triagepilot/contracts";
2
+ type Requester = {
3
+ request(route: string, parameters: Record<string, unknown>): Promise<{
4
+ data: unknown;
5
+ }>;
6
+ };
7
+ export declare class GitHubConfigurationSource implements ConfigurationSource {
8
+ private readonly octokit;
9
+ constructor(octokit: Requester);
10
+ loadOrganization(_workspaceId: WorkspaceId): Promise<null>;
11
+ loadRepository(input: {
12
+ workspaceId: WorkspaceId;
13
+ repository: ContractRepositoryRef;
14
+ trustedRevision: string;
15
+ }): Promise<ConfigurationDocument | null>;
16
+ }
17
+ export interface RepositoryRef {
18
+ owner: string;
19
+ repo: string;
20
+ }
21
+ export interface PullRequestRef extends RepositoryRef {
22
+ pullNumber: number;
23
+ }
24
+ export interface CheckRunRef extends RepositoryRef {
25
+ headSha: string;
26
+ }
27
+ export interface PullRequestReview {
28
+ userLogin: string;
29
+ userType?: string;
30
+ state: "APPROVED" | "CHANGES_REQUESTED" | "COMMENTED" | "PENDING" | "DISMISSED" | string;
31
+ commitId: string | null;
32
+ submittedAt: string | null;
33
+ }
34
+ export interface GitHubReviewerReplacementState {
35
+ state: string;
36
+ currentHeadRevision: string;
37
+ authorActor: string;
38
+ requestedActors: string[];
39
+ reviews: Array<{
40
+ actor: string;
41
+ actorType: "human" | "bot";
42
+ state: string;
43
+ commitId: string | null;
44
+ submittedAt: string | null;
45
+ }>;
46
+ }
47
+ export interface GitHubRoutingRecoveryState {
48
+ state: string;
49
+ baseRevision: string;
50
+ headRevision: string;
51
+ isDraft: boolean;
52
+ }
53
+ export interface ReviewerReplacementErrorClassification {
54
+ kind: "permanent" | "retryable";
55
+ message: string;
56
+ }
57
+ export declare function createInstallationRequester(input: {
58
+ appId: string;
59
+ privateKey: string;
60
+ installationId: number;
61
+ }): Promise<Requester>;
62
+ export declare class GitHubAdapter {
63
+ private readonly octokit;
64
+ constructor(octokit: Requester);
65
+ fetchDefaultBranchRevision(repository: ContractRepositoryRef): Promise<string>;
66
+ fetchRoutingRecoveryState(input: {
67
+ pullRequest: PullRequestRef;
68
+ }): Promise<GitHubRoutingRecoveryState | null>;
69
+ inspectReviewerReplacement(input: {
70
+ pullRequest: PullRequestRef;
71
+ }): Promise<GitHubReviewerReplacementState>;
72
+ listRequestedReviewers(input: {
73
+ pullRequest: PullRequestRef;
74
+ signal?: AbortSignal;
75
+ assertAuthorized?: () => Promise<void>;
76
+ }): Promise<string[]>;
77
+ reconcileReviewerReplacement(input: {
78
+ pullRequest: PullRequestRef;
79
+ unavailableActor: string;
80
+ replacementActor: string;
81
+ signal?: AbortSignal;
82
+ assertAuthorized?: () => Promise<void>;
83
+ }): Promise<{
84
+ changed: boolean;
85
+ }>;
86
+ classifyReviewerReplacementError(error: unknown): ReviewerReplacementErrorClassification;
87
+ upsertRoutingComment(input: {
88
+ pullRequest: PullRequestRef;
89
+ decisionId: string;
90
+ body: string;
91
+ }): Promise<void>;
92
+ requestHumanReviewers(input: {
93
+ pullRequest: PullRequestRef;
94
+ reviewers: string[];
95
+ }): Promise<void>;
96
+ syncRiskLabel(input: {
97
+ pullRequest: PullRequestRef;
98
+ tier: RiskTier;
99
+ }): Promise<void>;
100
+ submitPolicyApproval(input: {
101
+ pullRequest: PullRequestRef;
102
+ expectedHeadSha: string;
103
+ decisionId: string;
104
+ body: string;
105
+ }): Promise<void>;
106
+ listPullRequestReviews(input: {
107
+ pullRequest: PullRequestRef;
108
+ }): Promise<PullRequestReview[]>;
109
+ private listReviewerReplacementReviews;
110
+ createHumanReviewPolicyCheck(input: {
111
+ checkRun: CheckRunRef;
112
+ decisionId: string;
113
+ state: "in_progress" | "success" | "failure";
114
+ summary: string;
115
+ }): Promise<{
116
+ checkRunId: string;
117
+ }>;
118
+ findHumanReviewPolicyCheck(input: {
119
+ checkRun: CheckRunRef;
120
+ decisionId: string;
121
+ appId: number;
122
+ }): Promise<{
123
+ checkRunId: string;
124
+ state: "in_progress" | "success" | "failure";
125
+ } | null>;
126
+ updateHumanReviewPolicyCheck(input: {
127
+ checkRun: CheckRunRef;
128
+ checkRunId: string;
129
+ state: "success" | "failure";
130
+ summary: string;
131
+ }): Promise<void>;
132
+ writeRoutingCheck(input: {
133
+ checkRun: CheckRunRef;
134
+ decisionId: string;
135
+ conclusion: "success" | "neutral" | "failure";
136
+ summary: string;
137
+ }): Promise<void>;
138
+ }
139
+ export declare function githubRepositoryUrl(repository: Pick<ContractRepositoryRef, "owner" | "name">): string;
140
+ export declare function githubChangeRequestUrl(repository: Pick<ContractRepositoryRef, "owner" | "name">, changeRequestNumber: number): string;
141
+ export declare function parseGitHubPullRequestUrl(value: unknown): PullRequestRef | null;
142
+ export {};
@@ -0,0 +1,632 @@
1
+ import { App } from "@octokit/app";
2
+ const PAGE_SIZE = 100;
3
+ const HUMAN_REVIEW_POLICY_CHECK_NAME = "triagepilot/human-review-policy";
4
+ const RISK_LABELS = {
5
+ low: { name: "triagepilot:risk-low", color: "0e8a16", description: "TriagePilot risk: low" },
6
+ medium: { name: "triagepilot:risk-medium", color: "fbca04", description: "TriagePilot risk: medium" },
7
+ high: { name: "triagepilot:risk-high", color: "b60205", description: "TriagePilot risk: high" },
8
+ };
9
+ const CONFIGURATION_PATHS = [".triagepilot.yml", ".github/triagepilot.yml"];
10
+ export class GitHubConfigurationSource {
11
+ octokit;
12
+ constructor(octokit) {
13
+ this.octokit = octokit;
14
+ }
15
+ async loadOrganization(_workspaceId) {
16
+ return null;
17
+ }
18
+ async loadRepository(input) {
19
+ for (const path of CONFIGURATION_PATHS) {
20
+ try {
21
+ const response = await this.octokit.request("GET /repos/{owner}/{repo}/contents/{path}", {
22
+ owner: input.repository.owner,
23
+ repo: input.repository.name,
24
+ path,
25
+ ref: input.trustedRevision,
26
+ });
27
+ return {
28
+ content: decodeGitHubContent(response.data),
29
+ revision: input.trustedRevision,
30
+ path,
31
+ };
32
+ }
33
+ catch (error) {
34
+ if (!isGitHubStatus(error, 404))
35
+ throw error;
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+ }
41
+ class GitHubReviewerReplacementProtocolError extends Error {
42
+ }
43
+ class GitHubReviewerReplacementInputError extends Error {
44
+ }
45
+ export async function createInstallationRequester(input) {
46
+ const app = new App({ appId: input.appId, privateKey: input.privateKey });
47
+ return app.getInstallationOctokit(input.installationId);
48
+ }
49
+ export class GitHubAdapter {
50
+ octokit;
51
+ constructor(octokit) {
52
+ this.octokit = octokit;
53
+ }
54
+ async fetchDefaultBranchRevision(repository) {
55
+ const repositoryResponse = await this.octokit.request("GET /repos/{owner}/{repo}", {
56
+ owner: repository.owner,
57
+ repo: repository.name,
58
+ });
59
+ const defaultBranch = readRequiredString(repositoryResponse.data, "default_branch", "GitHub repository default branch");
60
+ const commitResponse = await this.octokit.request("GET /repos/{owner}/{repo}/commits/{ref}", {
61
+ owner: repository.owner,
62
+ repo: repository.name,
63
+ ref: defaultBranch,
64
+ });
65
+ return readRequiredString(commitResponse.data, "sha", "GitHub default branch revision");
66
+ }
67
+ async fetchRoutingRecoveryState(input) {
68
+ let response;
69
+ try {
70
+ response = await this.octokit.request("GET /repos/{owner}/{repo}/pulls/{pull_number}", {
71
+ ...toPullParams(input.pullRequest),
72
+ });
73
+ }
74
+ catch (error) {
75
+ if (isGitHubStatus(error, 404))
76
+ return null;
77
+ throw error;
78
+ }
79
+ if (!isRecord(response.data)
80
+ || !isRecord(response.data.base)
81
+ || !isRecord(response.data.head)
82
+ || typeof response.data.draft !== "boolean") {
83
+ throw new GitHubReviewerReplacementProtocolError("GitHub pull request routing state is unavailable");
84
+ }
85
+ try {
86
+ return {
87
+ state: readRequiredString(response.data, "state", "GitHub pull request state").toLowerCase(),
88
+ baseRevision: readRequiredString(response.data.base, "sha", "GitHub pull request base revision"),
89
+ headRevision: readRequiredString(response.data.head, "sha", "GitHub pull request head revision"),
90
+ isDraft: response.data.draft,
91
+ };
92
+ }
93
+ catch {
94
+ throw new GitHubReviewerReplacementProtocolError("GitHub pull request routing state is unavailable");
95
+ }
96
+ }
97
+ async inspectReviewerReplacement(input) {
98
+ const response = await this.octokit.request("GET /repos/{owner}/{repo}/pulls/{pull_number}", {
99
+ ...toPullParams(input.pullRequest),
100
+ });
101
+ if (!isRecord(response.data) || !isRecord(response.data.head) || !isRecord(response.data.user)) {
102
+ throw new GitHubReviewerReplacementProtocolError("GitHub pull request replacement state is unavailable");
103
+ }
104
+ const state = readReviewerReplacementString(response.data, "state", "GitHub pull request state");
105
+ const currentHeadRevision = readReviewerReplacementString(response.data.head, "sha", "GitHub pull request head revision");
106
+ const authorActor = normalizeGitHubActor(readReviewerReplacementString(response.data.user, "login", "GitHub pull request author"));
107
+ const requestedActors = await this.listRequestedReviewers(input);
108
+ const reviews = (await this.listReviewerReplacementReviews(input)).map((review) => ({
109
+ actor: normalizeGitHubActor(review.userLogin),
110
+ actorType: review.userType === "Bot" ? "bot" : "human",
111
+ state: review.state.toLowerCase(),
112
+ commitId: review.commitId,
113
+ submittedAt: review.submittedAt,
114
+ }));
115
+ return { state, currentHeadRevision, authorActor, requestedActors, reviews };
116
+ }
117
+ async listRequestedReviewers(input) {
118
+ await assertProviderMutationAuthorized(input);
119
+ const reviewers = new Set();
120
+ const response = await this.octokit.request("GET /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers", { ...toPullParams(input.pullRequest), ...providerRequestSignal(input.signal) });
121
+ const users = readRequestedReviewerUsers(response.data);
122
+ for (const user of users) {
123
+ if (!isRecord(user) || typeof user.login !== "string" || user.login.trim().length === 0) {
124
+ throw new GitHubReviewerReplacementProtocolError("GitHub requested reviewer user is malformed");
125
+ }
126
+ reviewers.add(normalizeGitHubActor(user.login));
127
+ }
128
+ return [...reviewers];
129
+ }
130
+ async reconcileReviewerReplacement(input) {
131
+ const unavailableActor = normalizeGitHubIndividualActor(input.unavailableActor);
132
+ const replacementActor = normalizeGitHubIndividualActor(input.replacementActor);
133
+ if (unavailableActor === replacementActor) {
134
+ throw new GitHubReviewerReplacementInputError("GitHub unavailable and replacement actors must differ");
135
+ }
136
+ const requestedBeforeRemoval = await this.listRequestedReviewers(input);
137
+ let changed = false;
138
+ if (requestedBeforeRemoval.includes(unavailableActor)) {
139
+ await assertProviderMutationAuthorized(input);
140
+ await this.octokit.request("DELETE /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers", {
141
+ ...toPullParams(input.pullRequest),
142
+ reviewers: [unavailableActor.slice(1)],
143
+ team_reviewers: [],
144
+ ...providerRequestSignal(input.signal),
145
+ });
146
+ changed = true;
147
+ }
148
+ const requestedBeforeAddition = await this.listRequestedReviewers(input);
149
+ if (!requestedBeforeAddition.includes(replacementActor)) {
150
+ await assertProviderMutationAuthorized(input);
151
+ await this.octokit.request("POST /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers", {
152
+ ...toPullParams(input.pullRequest),
153
+ reviewers: [replacementActor.slice(1)],
154
+ team_reviewers: [],
155
+ ...providerRequestSignal(input.signal),
156
+ });
157
+ changed = true;
158
+ }
159
+ return { changed };
160
+ }
161
+ classifyReviewerReplacementError(error) {
162
+ const message = error instanceof Error ? error.message : readOptionalString(error, "message") ?? "provider failed";
163
+ const status = readOptionalNumber(error, "status");
164
+ const retryableLimit = hasRetryableReviewerReplacementSignal(error, message);
165
+ return {
166
+ kind: error instanceof GitHubReviewerReplacementProtocolError
167
+ || error instanceof GitHubReviewerReplacementInputError
168
+ || (status !== null && [400, 401, 403, 404, 410, 422].includes(status) && !retryableLimit)
169
+ ? "permanent"
170
+ : "retryable",
171
+ message,
172
+ };
173
+ }
174
+ async upsertRoutingComment(input) {
175
+ const marker = decisionMarker(input.decisionId);
176
+ const existing = await findPaginated(async (page) => {
177
+ const comments = await this.octokit.request("GET /repos/{owner}/{repo}/issues/{issue_number}/comments", {
178
+ ...toIssueParams(input.pullRequest),
179
+ page,
180
+ per_page: PAGE_SIZE,
181
+ });
182
+ return Array.isArray(comments.data) ? comments.data : [];
183
+ }, (comment) => isCommentWithMarker(comment, marker));
184
+ const body = `${marker}\n${input.body}`;
185
+ if (isCommentWithId(existing)) {
186
+ await this.octokit.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", {
187
+ ...toRepositoryParams(input.pullRequest),
188
+ comment_id: existing.id,
189
+ body,
190
+ });
191
+ return;
192
+ }
193
+ await this.octokit.request("POST /repos/{owner}/{repo}/issues/{issue_number}/comments", {
194
+ ...toIssueParams(input.pullRequest),
195
+ body,
196
+ });
197
+ }
198
+ async requestHumanReviewers(input) {
199
+ const handles = [...new Set(input.reviewers)].slice(0, 2).map((reviewer) => reviewer.replace(/^@/, ""));
200
+ const reviewers = handles.filter((handle) => !handle.includes("/"));
201
+ const teamReviewers = handles
202
+ .filter((handle) => handle.includes("/"))
203
+ .map((handle) => handle.slice(handle.indexOf("/") + 1));
204
+ await this.octokit.request("POST /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers", {
205
+ ...toPullParams(input.pullRequest),
206
+ reviewers,
207
+ team_reviewers: teamReviewers,
208
+ });
209
+ }
210
+ async syncRiskLabel(input) {
211
+ const target = RISK_LABELS[input.tier];
212
+ try {
213
+ await this.octokit.request("POST /repos/{owner}/{repo}/labels", {
214
+ ...toRepositoryParams(input.pullRequest),
215
+ ...target,
216
+ });
217
+ }
218
+ catch (error) {
219
+ if (!isGitHubStatus(error, 422))
220
+ throw error;
221
+ }
222
+ const labels = [];
223
+ for (let page = 1;; page += 1) {
224
+ const response = await this.octokit.request("GET /repos/{owner}/{repo}/issues/{issue_number}/labels", {
225
+ ...toIssueParams(input.pullRequest),
226
+ page,
227
+ per_page: PAGE_SIZE,
228
+ });
229
+ const records = Array.isArray(response.data) ? response.data : [];
230
+ labels.push(...records);
231
+ if (records.length < PAGE_SIZE)
232
+ break;
233
+ }
234
+ const managedNames = new Set(Object.values(RISK_LABELS).map((label) => label.name));
235
+ for (const label of labels) {
236
+ const name = readLabelName(label);
237
+ if (name === undefined || name === target.name || !managedNames.has(name))
238
+ continue;
239
+ await this.octokit.request("DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{name}", {
240
+ ...toIssueParams(input.pullRequest),
241
+ name,
242
+ });
243
+ }
244
+ await this.octokit.request("POST /repos/{owner}/{repo}/issues/{issue_number}/labels", {
245
+ ...toIssueParams(input.pullRequest),
246
+ labels: [target.name],
247
+ });
248
+ }
249
+ async submitPolicyApproval(input) {
250
+ const marker = decisionMarker(input.decisionId);
251
+ const existing = await findPaginated(async (page) => {
252
+ const reviews = await this.octokit.request("GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews", {
253
+ ...toPullParams(input.pullRequest),
254
+ page,
255
+ per_page: PAGE_SIZE,
256
+ });
257
+ return Array.isArray(reviews.data) ? reviews.data : [];
258
+ }, (review) => hasBodyMarker(review, marker));
259
+ if (existing !== undefined)
260
+ return;
261
+ await this.octokit.request("POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews", {
262
+ ...toPullParams(input.pullRequest),
263
+ commit_id: input.expectedHeadSha,
264
+ event: "APPROVE",
265
+ body: `${marker}\n${input.body}`,
266
+ });
267
+ }
268
+ async listPullRequestReviews(input) {
269
+ const reviews = [];
270
+ for (let page = 1;; page += 1) {
271
+ const response = await this.octokit.request("GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews", {
272
+ ...toPullParams(input.pullRequest),
273
+ page,
274
+ per_page: PAGE_SIZE,
275
+ });
276
+ const records = Array.isArray(response.data) ? response.data : [];
277
+ for (const record of records) {
278
+ const review = readPullRequestReview(record, false);
279
+ if (review !== undefined)
280
+ reviews.push(review);
281
+ }
282
+ if (records.length < PAGE_SIZE)
283
+ return reviews;
284
+ }
285
+ }
286
+ async listReviewerReplacementReviews(input) {
287
+ const reviews = [];
288
+ for (let page = 1;; page += 1) {
289
+ const response = await this.octokit.request("GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews", {
290
+ ...toPullParams(input.pullRequest),
291
+ page,
292
+ per_page: PAGE_SIZE,
293
+ });
294
+ if (!Array.isArray(response.data)) {
295
+ throw new GitHubReviewerReplacementProtocolError("GitHub pull request reviews response is malformed");
296
+ }
297
+ for (const record of response.data)
298
+ reviews.push(readPullRequestReview(record, true));
299
+ if (response.data.length < PAGE_SIZE)
300
+ return reviews;
301
+ }
302
+ }
303
+ async createHumanReviewPolicyCheck(input) {
304
+ const response = await this.octokit.request("POST /repos/{owner}/{repo}/check-runs", {
305
+ ...toRepositoryParams(input.checkRun),
306
+ head_sha: input.checkRun.headSha,
307
+ external_id: input.decisionId,
308
+ ...humanReviewPolicyCheckPayload(input),
309
+ });
310
+ if (!isCheckRunWithId(response.data))
311
+ throw new Error("GitHub did not return a check run ID");
312
+ return { checkRunId: String(response.data.id) };
313
+ }
314
+ async findHumanReviewPolicyCheck(input) {
315
+ const matches = [];
316
+ for (let page = 1;; page += 1) {
317
+ const checks = await this.octokit.request("GET /repos/{owner}/{repo}/commits/{ref}/check-runs", {
318
+ ...toRepositoryParams(input.checkRun),
319
+ ref: input.checkRun.headSha,
320
+ check_name: HUMAN_REVIEW_POLICY_CHECK_NAME,
321
+ app_id: input.appId,
322
+ filter: "all",
323
+ page,
324
+ per_page: PAGE_SIZE,
325
+ });
326
+ const records = readCheckRuns(checks.data);
327
+ for (const check of records) {
328
+ if (!isCheckRunWithId(check) || !isHumanReviewPolicyCheckRun(check, input.decisionId, input.appId))
329
+ continue;
330
+ const state = readHumanReviewPolicyCheckState(check);
331
+ if (state !== null)
332
+ matches.push({ id: check.id, state });
333
+ }
334
+ if (records.length < PAGE_SIZE)
335
+ break;
336
+ }
337
+ const existing = matches.sort((left, right) => compareCheckRunIds(right.id, left.id))[0];
338
+ if (!isCheckRunWithId(existing))
339
+ return null;
340
+ return { checkRunId: String(existing.id), state: existing.state };
341
+ }
342
+ async updateHumanReviewPolicyCheck(input) {
343
+ await this.octokit.request("PATCH /repos/{owner}/{repo}/check-runs/{check_run_id}", {
344
+ ...toRepositoryParams(input.checkRun),
345
+ check_run_id: input.checkRunId,
346
+ ...completedHumanReviewPolicyCheckPayload(input),
347
+ });
348
+ }
349
+ async writeRoutingCheck(input) {
350
+ const existing = await findPaginated(async (page) => {
351
+ const checks = await this.octokit.request("GET /repos/{owner}/{repo}/commits/{ref}/check-runs", {
352
+ ...toRepositoryParams(input.checkRun),
353
+ ref: input.checkRun.headSha,
354
+ check_name: "triagepilot/routing",
355
+ filter: "all",
356
+ page,
357
+ per_page: PAGE_SIZE,
358
+ });
359
+ return readCheckRuns(checks.data);
360
+ }, (check) => isDecisionCheckRun(check, input.decisionId));
361
+ const check = {
362
+ name: "triagepilot/routing",
363
+ external_id: input.decisionId,
364
+ status: "completed",
365
+ conclusion: input.conclusion,
366
+ output: {
367
+ title: "TriagePilot routing",
368
+ summary: input.summary,
369
+ },
370
+ };
371
+ if (isCheckRunWithId(existing)) {
372
+ await this.octokit.request("PATCH /repos/{owner}/{repo}/check-runs/{check_run_id}", {
373
+ ...toRepositoryParams(input.checkRun),
374
+ check_run_id: existing.id,
375
+ ...check,
376
+ });
377
+ return;
378
+ }
379
+ await this.octokit.request("POST /repos/{owner}/{repo}/check-runs", {
380
+ ...toRepositoryParams(input.checkRun),
381
+ head_sha: input.checkRun.headSha,
382
+ ...check,
383
+ });
384
+ }
385
+ }
386
+ async function assertProviderMutationAuthorized(input) {
387
+ input.signal?.throwIfAborted();
388
+ await input.assertAuthorized?.();
389
+ input.signal?.throwIfAborted();
390
+ }
391
+ function providerRequestSignal(signal) {
392
+ return signal === undefined ? {} : { request: { signal } };
393
+ }
394
+ export function githubRepositoryUrl(repository) {
395
+ return `https://github.com/${encodeURIComponent(repository.owner)}/${encodeURIComponent(repository.name)}`;
396
+ }
397
+ export function githubChangeRequestUrl(repository, changeRequestNumber) {
398
+ return `${githubRepositoryUrl(repository)}/pull/${changeRequestNumber}`;
399
+ }
400
+ export function parseGitHubPullRequestUrl(value) {
401
+ if (typeof value !== "string" || value.trim() !== value || value.length === 0)
402
+ return null;
403
+ try {
404
+ const url = new URL(value);
405
+ const match = /^\/([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/([A-Za-z0-9._-]+)\/pull\/([1-9][0-9]*)\/?$/.exec(url.pathname);
406
+ if (url.protocol !== "https:"
407
+ || url.hostname !== "github.com"
408
+ || url.port !== ""
409
+ || url.username !== ""
410
+ || url.password !== ""
411
+ || url.search !== ""
412
+ || url.hash !== ""
413
+ || match === null)
414
+ return null;
415
+ const pullNumber = Number(match[3]);
416
+ if (!Number.isSafeInteger(pullNumber) || pullNumber > 2_147_483_647)
417
+ return null;
418
+ return { owner: match[1], repo: match[2], pullNumber };
419
+ }
420
+ catch {
421
+ return null;
422
+ }
423
+ }
424
+ function toRepositoryParams(ref) {
425
+ return { owner: ref.owner, repo: ref.repo };
426
+ }
427
+ function toPullParams(ref) {
428
+ return { ...toRepositoryParams(ref), pull_number: ref.pullNumber };
429
+ }
430
+ function toIssueParams(ref) {
431
+ return { ...toRepositoryParams(ref), issue_number: ref.pullNumber };
432
+ }
433
+ function decisionMarker(decisionId) {
434
+ return `<!-- triagepilot:decision:${decisionId} -->`;
435
+ }
436
+ function isCommentWithMarker(comment, marker) {
437
+ return typeof comment === "object" && comment !== null && "body" in comment && String(comment.body).startsWith(marker);
438
+ }
439
+ function isCommentWithId(comment) {
440
+ return typeof comment === "object" && comment !== null && "id" in comment;
441
+ }
442
+ function readLabelName(label) {
443
+ if (typeof label !== "object" || label === null || !("name" in label))
444
+ return undefined;
445
+ const name = String(label.name).trim();
446
+ return name || undefined;
447
+ }
448
+ function readRequiredString(value, key, label) {
449
+ if (!isRecord(value) || typeof value[key] !== "string" || value[key].trim().length === 0) {
450
+ throw new Error(`${label} is unavailable`);
451
+ }
452
+ return value[key].trim();
453
+ }
454
+ function decodeGitHubContent(data) {
455
+ if (!isRecord(data) || typeof data.content !== "string")
456
+ return "";
457
+ return Buffer.from(data.content.replace(/\s/g, ""), "base64").toString("utf8");
458
+ }
459
+ function isGitHubStatus(error, status) {
460
+ return typeof error === "object" && error !== null && "status" in error && error.status === status;
461
+ }
462
+ function readCheckRuns(data) {
463
+ if (typeof data !== "object" || data === null || !("check_runs" in data))
464
+ return [];
465
+ return Array.isArray(data.check_runs) ? data.check_runs : [];
466
+ }
467
+ function isDecisionCheckRun(check, decisionId) {
468
+ return (typeof check === "object" &&
469
+ check !== null &&
470
+ "name" in check &&
471
+ check.name === "triagepilot/routing" &&
472
+ "external_id" in check &&
473
+ check.external_id === decisionId);
474
+ }
475
+ function isHumanReviewPolicyCheckRun(check, decisionId, appId) {
476
+ return (isRecord(check) &&
477
+ check.name === HUMAN_REVIEW_POLICY_CHECK_NAME &&
478
+ check.external_id === decisionId &&
479
+ isRecord(check.app) &&
480
+ check.app.id === appId);
481
+ }
482
+ function compareCheckRunIds(left, right) {
483
+ const leftId = BigInt(String(left));
484
+ const rightId = BigInt(String(right));
485
+ return leftId < rightId ? -1 : leftId > rightId ? 1 : 0;
486
+ }
487
+ function readHumanReviewPolicyCheckState(check) {
488
+ if (!isRecord(check))
489
+ return null;
490
+ if (check.status !== "completed")
491
+ return "in_progress";
492
+ return check.conclusion === "success" || check.conclusion === "failure" ? check.conclusion : null;
493
+ }
494
+ function isCheckRunWithId(check) {
495
+ return typeof check === "object" && check !== null && "id" in check;
496
+ }
497
+ function humanReviewPolicyCheckPayload(input) {
498
+ const check = {
499
+ name: HUMAN_REVIEW_POLICY_CHECK_NAME,
500
+ output: {
501
+ title: "TriagePilot human review policy",
502
+ summary: input.summary,
503
+ },
504
+ };
505
+ if (input.state === "in_progress")
506
+ return { ...check, status: "in_progress" };
507
+ return { ...check, status: "completed", conclusion: input.state };
508
+ }
509
+ function completedHumanReviewPolicyCheckPayload(input) {
510
+ return {
511
+ name: HUMAN_REVIEW_POLICY_CHECK_NAME,
512
+ status: "completed",
513
+ conclusion: input.state,
514
+ output: {
515
+ title: "TriagePilot human review policy",
516
+ summary: input.summary,
517
+ },
518
+ };
519
+ }
520
+ function readPullRequestReview(value, strict) {
521
+ if (!isRecord(value)
522
+ || !isRecord(value.user)
523
+ || typeof value.user.login !== "string"
524
+ || typeof value.state !== "string"
525
+ || value.user.login.trim().length === 0
526
+ || value.state.trim().length === 0) {
527
+ if (strict)
528
+ throw new GitHubReviewerReplacementProtocolError("GitHub pull request review is malformed");
529
+ return undefined;
530
+ }
531
+ if (strict
532
+ && (typeof value.user.type !== "string"
533
+ || (value.user.type !== "User" && value.user.type !== "Bot")
534
+ || !("commit_id" in value)
535
+ || !("submitted_at" in value)
536
+ || (typeof value.commit_id !== "string" && value.commit_id !== null)
537
+ || (typeof value.submitted_at !== "string" && value.submitted_at !== null)
538
+ || (typeof value.commit_id === "string" && value.commit_id.trim().length === 0)
539
+ || (typeof value.submitted_at === "string" && value.submitted_at.trim().length === 0))) {
540
+ throw new GitHubReviewerReplacementProtocolError("GitHub pull request review is malformed");
541
+ }
542
+ return {
543
+ userLogin: value.user.login,
544
+ ...(typeof value.user.type === "string" ? { userType: value.user.type } : {}),
545
+ state: value.state,
546
+ commitId: typeof value.commit_id === "string" ? value.commit_id : null,
547
+ submittedAt: typeof value.submitted_at === "string" ? value.submitted_at : null,
548
+ };
549
+ }
550
+ function readRequestedReviewerUsers(value) {
551
+ if (!isRecord(value) || !Array.isArray(value.users)) {
552
+ throw new GitHubReviewerReplacementProtocolError("GitHub requested reviewers response is malformed");
553
+ }
554
+ return value.users;
555
+ }
556
+ function readReviewerReplacementString(value, key, label) {
557
+ if (!isRecord(value) || typeof value[key] !== "string" || value[key].trim().length === 0) {
558
+ throw new GitHubReviewerReplacementProtocolError(`${label} is unavailable`);
559
+ }
560
+ return value[key].trim();
561
+ }
562
+ function normalizeGitHubActor(value) {
563
+ const login = value.trim().replace(/^@/, "").toLowerCase();
564
+ if (login.length === 0)
565
+ throw new Error("GitHub actor login is unavailable");
566
+ return `@${login}`;
567
+ }
568
+ function normalizeGitHubIndividualActor(value) {
569
+ let actor;
570
+ try {
571
+ actor = normalizeGitHubActor(value);
572
+ }
573
+ catch {
574
+ throw new GitHubReviewerReplacementInputError("GitHub reviewer actor login is unavailable");
575
+ }
576
+ if (actor.includes("/")) {
577
+ throw new GitHubReviewerReplacementInputError("GitHub reviewer actor must identify an individual user");
578
+ }
579
+ return actor;
580
+ }
581
+ function isRecord(value) {
582
+ return typeof value === "object" && value !== null;
583
+ }
584
+ function readOptionalString(value, key) {
585
+ return isRecord(value) && typeof value[key] === "string" ? value[key] : null;
586
+ }
587
+ function readOptionalNumber(value, key) {
588
+ return isRecord(value) && typeof value[key] === "number" ? value[key] : null;
589
+ }
590
+ function hasRetryableReviewerReplacementSignal(error, message) {
591
+ const status = readOptionalNumber(error, "status");
592
+ if (status === 429)
593
+ return true;
594
+ if (status !== 403 && status !== 422)
595
+ return false;
596
+ const headers = isRecord(error) && isRecord(error.response) && isRecord(error.response.headers)
597
+ ? error.response.headers
598
+ : null;
599
+ const retryAfter = readHeader(headers, "retry-after");
600
+ const remaining = readHeader(headers, "x-ratelimit-remaining");
601
+ const normalizedMessage = message.toLowerCase();
602
+ return retryAfter !== null
603
+ || remaining === "0"
604
+ || normalizedMessage.includes("rate limit")
605
+ || normalizedMessage.includes("secondary limit")
606
+ || normalizedMessage.includes("abuse")
607
+ || normalizedMessage.includes("spam")
608
+ || normalizedMessage.includes("submitted too quickly");
609
+ }
610
+ function readHeader(headers, name) {
611
+ if (headers === null)
612
+ return null;
613
+ for (const [key, value] of Object.entries(headers)) {
614
+ if (key.toLowerCase() === name && (typeof value === "string" || typeof value === "number")) {
615
+ return String(value).trim();
616
+ }
617
+ }
618
+ return null;
619
+ }
620
+ function hasBodyMarker(value, marker) {
621
+ return typeof value === "object" && value !== null && "body" in value && String(value.body).includes(marker);
622
+ }
623
+ async function findPaginated(fetchPage, predicate) {
624
+ for (let page = 1;; page += 1) {
625
+ const values = await fetchPage(page);
626
+ const match = values.find(predicate);
627
+ if (match !== undefined)
628
+ return match;
629
+ if (values.length < PAGE_SIZE)
630
+ return undefined;
631
+ }
632
+ }
@@ -0,0 +1,24 @@
1
+ import type { CredentialProvider, ProviderConnectionId, WorkspaceId } from "@triagepilot/contracts";
2
+ export interface GitHubAppCredentials {
3
+ appId: string;
4
+ privateKey: string;
5
+ }
6
+ export interface GitHubAppCredentialShape extends GitHubAppCredentials {
7
+ webhookSecret: string;
8
+ }
9
+ export declare class GitHubCredentialProvider implements CredentialProvider<GitHubAppCredentials> {
10
+ private readonly credential;
11
+ constructor(credential: GitHubAppCredentials);
12
+ getCredential(_input: {
13
+ workspaceId: WorkspaceId;
14
+ providerConnectionId: ProviderConnectionId;
15
+ }): Promise<GitHubAppCredentials>;
16
+ }
17
+ export declare function validateGitHubAppCredentials(input: GitHubAppCredentials): {
18
+ appId: string;
19
+ };
20
+ export declare function validateGitHubAppCredentialsShape(input: GitHubAppCredentialShape): {
21
+ appId: string;
22
+ };
23
+ export declare function loadGitHubAppCredentials(source: NodeJS.ProcessEnv): Promise<GitHubAppCredentials>;
24
+ export declare function loadGitHubCredentials(source: NodeJS.ProcessEnv): Promise<GitHubAppCredentialShape>;
@@ -0,0 +1,55 @@
1
+ import { readFile } from "node:fs/promises";
2
+ export class GitHubCredentialProvider {
3
+ credential;
4
+ constructor(credential) {
5
+ this.credential = credential;
6
+ validateGitHubAppCredentials(credential);
7
+ }
8
+ async getCredential(_input) {
9
+ return { ...this.credential };
10
+ }
11
+ }
12
+ async function readSecret(source, directName, fileName) {
13
+ const direct = source[directName]?.trim();
14
+ const file = source[fileName]?.trim();
15
+ if (direct && file) {
16
+ throw new Error(`${directName} and ${fileName} cannot both be set`);
17
+ }
18
+ const value = file ? (await readFile(file, "utf8")).trim() : direct;
19
+ if (!value) {
20
+ throw new Error(`${directName} or ${fileName} is required`);
21
+ }
22
+ return value;
23
+ }
24
+ export function validateGitHubAppCredentials(input) {
25
+ if (!input.appId.trim()) {
26
+ throw new Error("GitHub App ID is required");
27
+ }
28
+ if (!input.privateKey.includes("-----BEGIN") || !input.privateKey.includes("PRIVATE KEY-----")) {
29
+ throw new Error("GitHub private key must be PEM formatted");
30
+ }
31
+ return { appId: input.appId.trim() };
32
+ }
33
+ export function validateGitHubAppCredentialsShape(input) {
34
+ const validated = validateGitHubAppCredentials(input);
35
+ if (!input.webhookSecret.trim()) {
36
+ throw new Error("GitHub webhook secret is required");
37
+ }
38
+ return validated;
39
+ }
40
+ export async function loadGitHubAppCredentials(source) {
41
+ const credentials = {
42
+ appId: source.GITHUB_APP_ID?.trim() ?? "",
43
+ privateKey: (await readSecret(source, "GITHUB_PRIVATE_KEY", "GITHUB_PRIVATE_KEY_FILE")).replace(/\\n/g, "\n"),
44
+ };
45
+ validateGitHubAppCredentials(credentials);
46
+ return credentials;
47
+ }
48
+ export async function loadGitHubCredentials(source) {
49
+ const credentials = {
50
+ ...(await loadGitHubAppCredentials(source)),
51
+ webhookSecret: await readSecret(source, "GITHUB_WEBHOOK_SECRET", "GITHUB_WEBHOOK_SECRET_FILE"),
52
+ };
53
+ validateGitHubAppCredentialsShape(credentials);
54
+ return credentials;
55
+ }
@@ -0,0 +1,4 @@
1
+ export { createInstallationRequester, GitHubAdapter, GitHubConfigurationSource, githubChangeRequestUrl, githubRepositoryUrl, parseGitHubPullRequestUrl, type CheckRunRef, type PullRequestRef, type PullRequestReview, type GitHubReviewerReplacementState, type GitHubRoutingRecoveryState, type ReviewerReplacementErrorClassification, type RepositoryRef, } from "./adapter.js";
2
+ export { GitHubCredentialProvider, loadGitHubAppCredentials, loadGitHubCredentials, validateGitHubAppCredentials, validateGitHubAppCredentialsShape, type GitHubAppCredentials, type GitHubAppCredentialShape, } from "./credentials.js";
3
+ export { normalizeGitHubWebhook, type GitHubWebhookInput, type NormalizedGitHubWebhookEvent, } from "./normalization.js";
4
+ export { verifyGitHubSignature } from "./webhook.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { createInstallationRequester, GitHubAdapter, GitHubConfigurationSource, githubChangeRequestUrl, githubRepositoryUrl, parseGitHubPullRequestUrl, } from "./adapter.js";
2
+ export { GitHubCredentialProvider, loadGitHubAppCredentials, loadGitHubCredentials, validateGitHubAppCredentials, validateGitHubAppCredentialsShape, } from "./credentials.js";
3
+ export { normalizeGitHubWebhook, } from "./normalization.js";
4
+ export { verifyGitHubSignature } from "./webhook.js";
@@ -0,0 +1,14 @@
1
+ import type { NormalizedChangeRequestEvent } from "@triagepilot/contracts";
2
+ export interface GitHubWebhookInput {
3
+ deliveryId: string;
4
+ eventName: string;
5
+ payload: unknown;
6
+ }
7
+ export interface NormalizedGitHubWebhookEvent extends NormalizedChangeRequestEvent {
8
+ provider: "github";
9
+ providerAccount: {
10
+ login: string;
11
+ type: string;
12
+ };
13
+ }
14
+ export declare function normalizeGitHubWebhook(input: GitHubWebhookInput): NormalizedGitHubWebhookEvent | null;
@@ -0,0 +1,75 @@
1
+ import { z } from "zod";
2
+ const ROUTING_PULL_REQUEST_ACTIONS = new Set(["opened", "reopened", "synchronize", "ready_for_review"]);
3
+ const REVIEW_PULL_REQUEST_ACTIONS = new Set(["submitted", "edited", "dismissed"]);
4
+ const githubIdSchema = z.union([
5
+ z.number().int().safe().transform(String),
6
+ z.string().trim().regex(/^\d+$/),
7
+ ]);
8
+ const pullRequestWebhookSchema = z.object({
9
+ action: z.string(),
10
+ installation: z.object({ id: githubIdSchema }),
11
+ sender: z.object({
12
+ id: githubIdSchema,
13
+ login: z.string().trim().min(1),
14
+ }),
15
+ repository: z.object({
16
+ id: githubIdSchema,
17
+ name: z.string().trim().min(1),
18
+ owner: z.object({ login: z.string().trim().min(1), type: z.string().trim().min(1) }),
19
+ }),
20
+ pull_request: z.object({
21
+ id: githubIdSchema,
22
+ number: z.number().int().positive(),
23
+ draft: z.boolean(),
24
+ base: z.object({ sha: z.string().trim().min(1) }),
25
+ head: z.object({ sha: z.string().trim().min(1) }),
26
+ }),
27
+ });
28
+ const pullRequestReviewWebhookSchema = pullRequestWebhookSchema.extend({
29
+ review: z.object({ state: z.string().trim().min(1) }),
30
+ });
31
+ export function normalizeGitHubWebhook(input) {
32
+ if (input.eventName === "pull_request") {
33
+ const payload = pullRequestWebhookSchema.parse(input.payload);
34
+ if (!ROUTING_PULL_REQUEST_ACTIONS.has(payload.action))
35
+ return null;
36
+ return toNormalizedEvent(input.deliveryId, "change_request", payload);
37
+ }
38
+ if (input.eventName === "pull_request_review") {
39
+ const payload = pullRequestReviewWebhookSchema.parse(input.payload);
40
+ if (!REVIEW_PULL_REQUEST_ACTIONS.has(payload.action))
41
+ return null;
42
+ return toNormalizedEvent(input.deliveryId, "change_request_review", payload);
43
+ }
44
+ return null;
45
+ }
46
+ function toNormalizedEvent(deliveryId, eventName, payload) {
47
+ return {
48
+ deliveryId,
49
+ eventName,
50
+ eventAction: payload.action,
51
+ provider: "github",
52
+ externalConnectionId: payload.installation.id,
53
+ providerAccount: {
54
+ login: payload.repository.owner.login,
55
+ type: payload.repository.owner.type,
56
+ },
57
+ changeRequest: {
58
+ repository: {
59
+ provider: "github",
60
+ externalId: payload.repository.id,
61
+ owner: payload.repository.owner.login,
62
+ name: payload.repository.name,
63
+ },
64
+ externalId: String(payload.pull_request.number),
65
+ number: payload.pull_request.number,
66
+ baseRevision: payload.pull_request.base.sha,
67
+ headRevision: payload.pull_request.head.sha,
68
+ },
69
+ actor: {
70
+ externalId: payload.sender.id,
71
+ displayName: payload.sender.login,
72
+ },
73
+ isDraft: payload.pull_request.draft,
74
+ };
75
+ }
@@ -0,0 +1,5 @@
1
+ export declare function verifyGitHubSignature(input: {
2
+ body: string;
3
+ secret: string;
4
+ signature: string | null;
5
+ }): Promise<void>;
@@ -0,0 +1,12 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ export async function verifyGitHubSignature(input) {
3
+ if (!input.signature?.startsWith("sha256=")) {
4
+ throw new Error("Invalid GitHub webhook signature");
5
+ }
6
+ const expected = `sha256=${createHmac("sha256", input.secret).update(input.body).digest("hex")}`;
7
+ const expectedBuffer = Buffer.from(expected);
8
+ const actualBuffer = Buffer.from(input.signature);
9
+ if (expectedBuffer.length !== actualBuffer.length || !timingSafeEqual(expectedBuffer, actualBuffer)) {
10
+ throw new Error("Invalid GitHub webhook signature");
11
+ }
12
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@triagepilot/provider-github",
3
+ "version": "1.1.0",
4
+ "license": "FSL-1.1-Apache-2.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/TriagePilot/triage-pilot.git",
8
+ "directory": "packages/provider-github"
9
+ },
10
+ "type": "module",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "LICENSE"
20
+ ],
21
+ "main": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "dependencies": {
24
+ "@types/node": "^22.10.2",
25
+ "@triagepilot/contracts": "1.1.0",
26
+ "@octokit/app": "^15.1.2",
27
+ "@octokit/request": "^9.1.4",
28
+ "zod": "^3.24.1"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "publishedAt": "2026-09-22T09:41:47.000Z",
34
+ "futureLicenseEffectiveAt": "2028-09-22T09:41:47.000Z",
35
+ "scripts": {
36
+ "build": "rm -rf dist && tsc -p tsconfig.json",
37
+ "check": "tsc -p tsconfig.json --noEmit"
38
+ }
39
+ }