@sentry/junior-github 0.199.0 → 0.201.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/dist/index.js CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  credentialUnavailable,
24
24
  gitHubDeploymentSourceSubscribable,
25
25
  gitHubIssueSubscribable,
26
+ gitHubPullRequestEventGuidance,
26
27
  gitHubPullRequestSubscribable,
27
28
  gitHubReleaseSourceSubscribable,
28
29
  gitHubRepositorySubscribable,
@@ -42,7 +43,7 @@ import {
42
43
  readGrantPermissions,
43
44
  requireEnv,
44
45
  resolveUserAccount
45
- } from "./chunk-6OPIFILN.js";
46
+ } from "./chunk-F2PJG6AF.js";
46
47
 
47
48
  // src/plugin.ts
48
49
  import {
@@ -1001,7 +1002,9 @@ var createPullRequestToolInputSchema = z4.object({
1001
1002
  title: z4.string().describe("Pull request title."),
1002
1003
  head: z4.string().describe("Head branch or owner:branch ref."),
1003
1004
  base: z4.string().describe("Base branch."),
1004
- body: z4.string().describe("Pull request body. The runtime appends the conversation footer.").optional(),
1005
+ body: z4.string().describe(
1006
+ "Pull request body. The runtime appends the conversation footer."
1007
+ ).optional(),
1005
1008
  draft: z4.boolean().describe("Whether to open the pull request as a draft.").optional()
1006
1009
  }).strict();
1007
1010
  var createPullRequestStateSchema = Type2.Union([
@@ -1252,6 +1255,7 @@ function createGitHubPullRequestTool(ctx, subscriptionConfig) {
1252
1255
  readOnlyHint: false
1253
1256
  },
1254
1257
  description: "Create a GitHub pull request with a runtime-owned conversation footer. Use this instead of shelling out to gh pr create when creating pull requests.",
1258
+ exposure: "direct",
1255
1259
  inputSchema: createPullRequestToolInputSchema,
1256
1260
  outputSchema: gitHubPullRequestOutputSchema,
1257
1261
  async execute(input, options) {
@@ -1994,7 +1998,7 @@ function createGitHubUpdatePullRequestTool(ctx) {
1994
1998
  });
1995
1999
  }
1996
2000
 
1997
- // src/tools/resolve-pull-request-review-thread.ts
2001
+ // src/tools/update-pull-request-feedback.ts
1998
2002
  import {
1999
2003
  definePluginTool as definePluginTool10,
2000
2004
  PluginToolInputError as PluginToolInputError11,
@@ -2019,21 +2023,33 @@ function botUserIdFromEmail(value) {
2019
2023
  return parseGitHubBotIdentity(value)?.userId;
2020
2024
  }
2021
2025
 
2022
- // src/tools/resolve-pull-request-review-thread.ts
2026
+ // src/tools/update-pull-request-feedback.ts
2027
+ var STATUS_TO_REACTION_CONTENT = {
2028
+ reviewing: "eyes",
2029
+ addressed: "+1",
2030
+ declined: "-1"
2031
+ };
2032
+ var FEEDBACK_REACTION_CONTENTS = new Set(
2033
+ Object.values(STATUS_TO_REACTION_CONTENT)
2034
+ );
2023
2035
  var inputSchema8 = z10.object({
2024
- repo: z10.string().describe(
2025
- 'Repository in "owner/name" format. Required because GraphQL has no repo path.'
2036
+ repo: z10.string().describe('Repository in "owner/name" format.'),
2037
+ commentKind: z10.enum(["conversation", "review"]).describe(
2038
+ 'Comment kind: "conversation" for a pull request conversation comment, "review" for an inline review comment.'
2026
2039
  ),
2027
- threadId: z10.string().trim().min(1).describe(
2028
- "GitHub pull request review thread node ID (the same `threadId` / `id` variable used by `gh api graphql` resolveReviewThread)."
2040
+ commentId: z10.number().int().positive().describe(
2041
+ "GitHub comment id (conversation comment id or inline review comment id)."
2042
+ ),
2043
+ status: z10.enum(["reviewing", "addressed", "declined"]).describe(
2044
+ "Feedback status. Maps to a reaction: reviewing -> eyes, addressed -> +1, declined -> -1."
2029
2045
  )
2030
2046
  }).strict();
2031
2047
  var outputSchema8 = pluginToolOutputSchema10.extend({
2032
- target: z10.literal("resolvePullRequestReviewThread"),
2048
+ target: z10.literal("updatePullRequestFeedback"),
2033
2049
  repo: z10.string(),
2034
- number: z10.number(),
2035
- threadId: z10.string(),
2036
- resolved: z10.boolean()
2050
+ commentId: z10.number(),
2051
+ status: z10.enum(["reviewing", "addressed", "declined"]),
2052
+ reactionId: z10.number()
2037
2053
  });
2038
2054
  function parseRepo10(value) {
2039
2055
  const parts = value.split("/").map((part) => part.trim());
@@ -2042,6 +2058,13 @@ function parseRepo10(value) {
2042
2058
  }
2043
2059
  return { owner: parts[0], name: parts[1], ref: `${parts[0]}/${parts[1]}` };
2044
2060
  }
2061
+ function pullRequestCommentReactionsPath(input) {
2062
+ const segment = input.commentKind === "conversation" ? "issues" : "pulls";
2063
+ return `/repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(input.name)}/${segment}/comments/${input.commentId}/reactions`;
2064
+ }
2065
+ function pullRequestFeedbackReactionContent(status) {
2066
+ return STATUS_TO_REACTION_CONTENT[status];
2067
+ }
2045
2068
  async function readJson7(response) {
2046
2069
  const text2 = await response.text();
2047
2070
  if (!text2) return void 0;
@@ -2058,7 +2081,12 @@ function githubError(payload) {
2058
2081
  }
2059
2082
  return "GitHub request failed";
2060
2083
  }
2061
- function createGitHubResolvePullRequestReviewThreadTool(ctx, botEmail) {
2084
+ var reactionSchema = z10.object({
2085
+ id: z10.number(),
2086
+ content: z10.string(),
2087
+ user: z10.object({ id: z10.number() }).nullable()
2088
+ });
2089
+ function createGitHubUpdatePullRequestFeedbackTool(ctx, botEmail) {
2062
2090
  return definePluginTool10({
2063
2091
  annotations: {
2064
2092
  destructiveHint: true,
@@ -2066,22 +2094,175 @@ function createGitHubResolvePullRequestReviewThreadTool(ctx, botEmail) {
2066
2094
  openWorldHint: true,
2067
2095
  readOnlyHint: false
2068
2096
  },
2069
- description: "Resolve a GitHub pull request review thread. Use this instead of shelling out to `gh api graphql` for resolveReviewThread (GraphQL-only; no REST or `gh pr` equivalent). Only works on pull requests the bot authored.",
2097
+ description: "Set Junior's status reaction on GitHub pull request feedback: reviewing (eyes), addressed (+1), or declined (-1). Use the commentId and commentKind from the resource event. Replaces only Junior's prior status reaction on that comment.",
2098
+ exposure: "direct",
2070
2099
  inputSchema: inputSchema8,
2071
2100
  outputSchema: outputSchema8,
2072
2101
  async execute(input) {
2073
2102
  const parsedInput = inputSchema8.safeParse(input);
2074
2103
  if (!parsedInput.success) {
2075
2104
  throw new PluginToolInputError11(
2076
- "Invalid GitHub resolvePullRequestReviewThread input.",
2105
+ "Invalid GitHub updatePullRequestFeedback input.",
2077
2106
  { cause: parsedInput.error }
2078
2107
  );
2079
2108
  }
2109
+ const { commentId, commentKind, status } = parsedInput.data;
2080
2110
  const repo = parseRepo10(parsedInput.data.repo);
2081
2111
  const botUserId = botUserIdFromEmail(botEmail);
2082
2112
  if (botUserId === void 0) {
2083
2113
  throw new Error("GitHub App bot identity is not configured.");
2084
2114
  }
2115
+ const targetContent = pullRequestFeedbackReactionContent(status);
2116
+ const baseUrl = `https://api.github.com${pullRequestCommentReactionsPath({
2117
+ owner: repo.owner,
2118
+ name: repo.name,
2119
+ commentKind,
2120
+ commentId
2121
+ })}`;
2122
+ const listResponse = await ctx.egress.fetch({
2123
+ provider: "github",
2124
+ operation: "github.pull.comment-reaction.list",
2125
+ request: new Request(`${baseUrl}?per_page=100`, {
2126
+ headers: { Accept: "application/vnd.github+json" }
2127
+ })
2128
+ });
2129
+ const listPayload = await readJson7(listResponse);
2130
+ if (!listResponse.ok) {
2131
+ throw new Error(
2132
+ `GitHub reaction lookup failed with HTTP ${listResponse.status}: ${githubError(listPayload)}`
2133
+ );
2134
+ }
2135
+ const botReactions = z10.array(reactionSchema).parse(listPayload).filter(
2136
+ (reaction) => reaction.user?.id === botUserId && FEEDBACK_REACTION_CONTENTS.has(reaction.content)
2137
+ );
2138
+ const existing = botReactions.find(
2139
+ (reaction) => reaction.content === targetContent
2140
+ );
2141
+ for (const stale of botReactions) {
2142
+ if (stale.content === targetContent) continue;
2143
+ const deleteResponse = await ctx.egress.fetch({
2144
+ provider: "github",
2145
+ operation: "github.pull.comment-reaction.delete",
2146
+ request: new Request(`${baseUrl}/${stale.id}`, {
2147
+ method: "DELETE"
2148
+ })
2149
+ });
2150
+ if (!deleteResponse.ok && deleteResponse.status !== 404) {
2151
+ const deletePayload = await readJson7(deleteResponse);
2152
+ throw new Error(
2153
+ `GitHub reaction removal failed with HTTP ${deleteResponse.status}: ${githubError(deletePayload)}`
2154
+ );
2155
+ }
2156
+ }
2157
+ if (existing) {
2158
+ return {
2159
+ target: "updatePullRequestFeedback",
2160
+ repo: repo.ref,
2161
+ commentId,
2162
+ status,
2163
+ reactionId: existing.id
2164
+ };
2165
+ }
2166
+ const createResponse = await ctx.egress.fetch({
2167
+ provider: "github",
2168
+ operation: "github.pull.comment-reaction.create",
2169
+ request: new Request(baseUrl, {
2170
+ method: "POST",
2171
+ headers: {
2172
+ Accept: "application/vnd.github+json",
2173
+ "Content-Type": "application/json"
2174
+ },
2175
+ body: JSON.stringify({ content: targetContent })
2176
+ })
2177
+ });
2178
+ const createPayload = await readJson7(createResponse);
2179
+ if (!createResponse.ok) {
2180
+ throw new Error(
2181
+ `GitHub reaction creation failed with HTTP ${createResponse.status}: ${githubError(createPayload)}`
2182
+ );
2183
+ }
2184
+ const created = reactionSchema.parse(createPayload);
2185
+ return {
2186
+ target: "updatePullRequestFeedback",
2187
+ repo: repo.ref,
2188
+ commentId,
2189
+ status,
2190
+ reactionId: created.id
2191
+ };
2192
+ }
2193
+ });
2194
+ }
2195
+
2196
+ // src/tools/resolve-pull-request-review-thread.ts
2197
+ import {
2198
+ definePluginTool as definePluginTool11,
2199
+ PluginToolInputError as PluginToolInputError12,
2200
+ pluginToolOutputSchema as pluginToolOutputSchema11
2201
+ } from "@sentry/junior-plugin-api";
2202
+ import { z as z11 } from "zod";
2203
+ var inputSchema9 = z11.object({
2204
+ repo: z11.string().describe(
2205
+ 'Repository in "owner/name" format. Required because GraphQL has no repo path.'
2206
+ ),
2207
+ threadId: z11.string().trim().min(1).describe(
2208
+ "GitHub pull request review thread node ID (the same `threadId` / `id` variable used by `gh api graphql` resolveReviewThread)."
2209
+ )
2210
+ }).strict();
2211
+ var outputSchema9 = pluginToolOutputSchema11.extend({
2212
+ target: z11.literal("resolvePullRequestReviewThread"),
2213
+ repo: z11.string(),
2214
+ number: z11.number(),
2215
+ threadId: z11.string(),
2216
+ resolved: z11.boolean()
2217
+ });
2218
+ function parseRepo11(value) {
2219
+ const parts = value.split("/").map((part) => part.trim());
2220
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
2221
+ throw new PluginToolInputError12('repo must use "owner/name" format');
2222
+ }
2223
+ return { owner: parts[0], name: parts[1], ref: `${parts[0]}/${parts[1]}` };
2224
+ }
2225
+ async function readJson8(response) {
2226
+ const text2 = await response.text();
2227
+ if (!text2) return void 0;
2228
+ try {
2229
+ return JSON.parse(text2);
2230
+ } catch {
2231
+ return text2;
2232
+ }
2233
+ }
2234
+ function githubError2(payload) {
2235
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
2236
+ const message = payload.message;
2237
+ if (typeof message === "string" && message.trim()) return message.trim();
2238
+ }
2239
+ return "GitHub request failed";
2240
+ }
2241
+ function createGitHubResolvePullRequestReviewThreadTool(ctx, botEmail) {
2242
+ return definePluginTool11({
2243
+ annotations: {
2244
+ destructiveHint: true,
2245
+ idempotentHint: true,
2246
+ openWorldHint: true,
2247
+ readOnlyHint: false
2248
+ },
2249
+ description: "Resolve a GitHub pull request review thread after acting on its feedback. Only works on pull requests the bot authored.",
2250
+ exposure: "direct",
2251
+ inputSchema: inputSchema9,
2252
+ outputSchema: outputSchema9,
2253
+ async execute(input) {
2254
+ const parsedInput = inputSchema9.safeParse(input);
2255
+ if (!parsedInput.success) {
2256
+ throw new PluginToolInputError12(
2257
+ "Invalid GitHub resolvePullRequestReviewThread input.",
2258
+ { cause: parsedInput.error }
2259
+ );
2260
+ }
2261
+ const repo = parseRepo11(parsedInput.data.repo);
2262
+ const botUserId = botUserIdFromEmail(botEmail);
2263
+ if (botUserId === void 0) {
2264
+ throw new Error("GitHub App bot identity is not configured.");
2265
+ }
2085
2266
  const query = `query ReviewThreadOwnership($threadId: ID!) {
2086
2267
  node(id: $threadId) {
2087
2268
  ... on PullRequestReviewThread {
@@ -2108,32 +2289,32 @@ function createGitHubResolvePullRequestReviewThreadTool(ctx, botEmail) {
2108
2289
  })
2109
2290
  })
2110
2291
  });
2111
- const lookupPayload = await readJson7(lookupResponse);
2292
+ const lookupPayload = await readJson8(lookupResponse);
2112
2293
  if (!lookupResponse.ok) {
2113
2294
  throw new Error(
2114
- `GitHub review thread lookup failed with HTTP ${lookupResponse.status}: ${githubError(lookupPayload)}`
2295
+ `GitHub review thread lookup failed with HTTP ${lookupResponse.status}: ${githubError2(lookupPayload)}`
2115
2296
  );
2116
2297
  }
2117
- const thread = z10.object({
2118
- data: z10.object({
2119
- node: z10.object({
2120
- id: z10.string(),
2121
- isResolved: z10.boolean(),
2122
- pullRequest: z10.object({
2123
- author: z10.object({ databaseId: z10.number().optional() }).nullable(),
2124
- number: z10.number(),
2125
- repository: z10.object({ nameWithOwner: z10.string() })
2298
+ const thread = z11.object({
2299
+ data: z11.object({
2300
+ node: z11.object({
2301
+ id: z11.string(),
2302
+ isResolved: z11.boolean(),
2303
+ pullRequest: z11.object({
2304
+ author: z11.object({ databaseId: z11.number().optional() }).nullable(),
2305
+ number: z11.number(),
2306
+ repository: z11.object({ nameWithOwner: z11.string() })
2126
2307
  })
2127
2308
  }).nullable()
2128
2309
  })
2129
2310
  }).parse(lookupPayload).data.node;
2130
2311
  if (!thread) {
2131
- throw new PluginToolInputError11("GitHub review thread was not found.");
2312
+ throw new PluginToolInputError12("GitHub review thread was not found.");
2132
2313
  }
2133
2314
  const pullRequest = thread.pullRequest;
2134
2315
  const ownsPullRequest = pullRequest.repository.nameWithOwner.toLowerCase() === repo.ref.toLowerCase() && pullRequest.author?.databaseId === botUserId;
2135
2316
  if (!ownsPullRequest) {
2136
- throw new PluginToolInputError11(
2317
+ throw new PluginToolInputError12(
2137
2318
  "This bot can only resolve review threads on pull requests it authored."
2138
2319
  );
2139
2320
  }
@@ -2164,16 +2345,16 @@ function createGitHubResolvePullRequestReviewThreadTool(ctx, botEmail) {
2164
2345
  })
2165
2346
  })
2166
2347
  });
2167
- const resolvePayload = await readJson7(resolveResponse);
2348
+ const resolvePayload = await readJson8(resolveResponse);
2168
2349
  if (!resolveResponse.ok) {
2169
2350
  throw new Error(
2170
- `GitHub review thread resolution failed with HTTP ${resolveResponse.status}: ${githubError(resolvePayload)}`
2351
+ `GitHub review thread resolution failed with HTTP ${resolveResponse.status}: ${githubError2(resolvePayload)}`
2171
2352
  );
2172
2353
  }
2173
- const resolved = z10.object({
2174
- data: z10.object({
2175
- resolveReviewThread: z10.object({
2176
- thread: z10.object({ id: z10.string(), isResolved: z10.boolean() })
2354
+ const resolved = z11.object({
2355
+ data: z11.object({
2356
+ resolveReviewThread: z11.object({
2357
+ thread: z11.object({ id: z11.string(), isResolved: z11.boolean() })
2177
2358
  })
2178
2359
  })
2179
2360
  }).parse(resolvePayload).data.resolveReviewThread.thread;
@@ -2191,6 +2372,129 @@ function createGitHubResolvePullRequestReviewThreadTool(ctx, botEmail) {
2191
2372
  });
2192
2373
  }
2193
2374
 
2375
+ // src/tools/submit-pull-request-review.ts
2376
+ import {
2377
+ definePluginTool as definePluginTool12,
2378
+ PluginToolInputError as PluginToolInputError13,
2379
+ pluginToolOutputSchema as pluginToolOutputSchema12
2380
+ } from "@sentry/junior-plugin-api";
2381
+ import { z as z12 } from "zod";
2382
+ var reviewCommentSchema = z12.object({
2383
+ path: z12.string().trim().min(1).describe("File path in the pull request."),
2384
+ body: z12.string().trim().min(1).describe("Inline review comment."),
2385
+ line: z12.number().int().positive().describe("Line in the pull request diff."),
2386
+ side: z12.enum(["LEFT", "RIGHT"]).describe("Side of the pull request diff."),
2387
+ startLine: z12.number().int().positive().optional().describe("First line of a multi-line comment."),
2388
+ startSide: z12.enum(["LEFT", "RIGHT"]).optional().describe("First side of a multi-line comment.")
2389
+ }).strict().refine(
2390
+ ({ startLine, startSide }) => startLine === void 0 === (startSide === void 0),
2391
+ { message: "startLine and startSide must be provided together." }
2392
+ );
2393
+ var inputSchema10 = z12.object({
2394
+ repo: z12.string().describe('Repository in "owner/name" format.'),
2395
+ number: z12.number().int().positive().describe("Pull request number."),
2396
+ event: z12.enum(["COMMENT", "REQUEST_CHANGES"]).describe("Review result. Junior cannot approve pull requests."),
2397
+ body: z12.string().trim().min(1).describe("Review summary."),
2398
+ comments: z12.array(reviewCommentSchema).optional().describe("Optional inline review comments.")
2399
+ }).strict();
2400
+ var outputSchema10 = pluginToolOutputSchema12.extend({
2401
+ target: z12.literal("submitPullRequestReview"),
2402
+ repo: z12.string(),
2403
+ number: z12.number(),
2404
+ reviewId: z12.number(),
2405
+ state: z12.string(),
2406
+ url: z12.string()
2407
+ });
2408
+ function parseRepo12(value) {
2409
+ const parts = value.split("/").map((part) => part.trim());
2410
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
2411
+ throw new PluginToolInputError13('repo must use "owner/name" format');
2412
+ }
2413
+ return { owner: parts[0], name: parts[1], ref: `${parts[0]}/${parts[1]}` };
2414
+ }
2415
+ async function readJson9(response) {
2416
+ const text2 = await response.text();
2417
+ if (!text2) return void 0;
2418
+ try {
2419
+ return JSON.parse(text2);
2420
+ } catch {
2421
+ return text2;
2422
+ }
2423
+ }
2424
+ function githubError3(body) {
2425
+ if (body && typeof body === "object" && !Array.isArray(body)) {
2426
+ const message = body.message;
2427
+ if (typeof message === "string" && message.trim()) return message.trim();
2428
+ }
2429
+ if (typeof body === "string" && body.trim()) return body.trim();
2430
+ return "GitHub request failed";
2431
+ }
2432
+ function throwReviewError(status, body) {
2433
+ const message = `GitHub pull request review failed with HTTP ${status}: ${githubError3(body)}`;
2434
+ if (status === 404 || status === 422) {
2435
+ throw new PluginToolInputError13(message);
2436
+ }
2437
+ throw new Error(message);
2438
+ }
2439
+ function createGitHubSubmitPullRequestReviewTool(egress) {
2440
+ return definePluginTool12({
2441
+ annotations: {
2442
+ destructiveHint: true,
2443
+ idempotentHint: false,
2444
+ openWorldHint: true,
2445
+ readOnlyHint: false
2446
+ },
2447
+ description: "Submit a GitHub pull request review as a comment or change request. Use this instead of `gh pr review`, GraphQL, or direct REST calls. Junior cannot approve pull requests.",
2448
+ exposure: "direct",
2449
+ inputSchema: inputSchema10,
2450
+ outputSchema: outputSchema10,
2451
+ async execute(review) {
2452
+ const repo = parseRepo12(review.repo);
2453
+ const comments = review.comments?.map((comment) => ({
2454
+ path: comment.path,
2455
+ body: comment.body,
2456
+ line: comment.line,
2457
+ side: comment.side,
2458
+ ...comment.startLine !== void 0 ? { start_line: comment.startLine } : void 0,
2459
+ ...comment.startSide !== void 0 ? { start_side: comment.startSide } : void 0
2460
+ }));
2461
+ const response = await egress.fetch({
2462
+ provider: "github",
2463
+ operation: "github.pull.review.create",
2464
+ request: new Request(
2465
+ `https://api.github.com/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.name)}/pulls/${review.number}/reviews`,
2466
+ {
2467
+ method: "POST",
2468
+ headers: {
2469
+ Accept: "application/vnd.github+json",
2470
+ "Content-Type": "application/json",
2471
+ "X-GitHub-Api-Version": "2022-11-28"
2472
+ },
2473
+ body: JSON.stringify({
2474
+ body: review.body,
2475
+ event: review.event,
2476
+ ...comments ? { comments } : void 0
2477
+ })
2478
+ }
2479
+ )
2480
+ });
2481
+ const parsed = await readJson9(response);
2482
+ if (!response.ok) {
2483
+ throwReviewError(response.status, parsed);
2484
+ }
2485
+ const providerResult = z12.object({ id: z12.number(), html_url: z12.string(), state: z12.string() }).parse(parsed);
2486
+ return {
2487
+ target: "submitPullRequestReview",
2488
+ repo: repo.ref,
2489
+ number: review.number,
2490
+ reviewId: providerResult.id,
2491
+ state: providerResult.state,
2492
+ url: providerResult.html_url
2493
+ };
2494
+ }
2495
+ });
2496
+ }
2497
+
2194
2498
  // src/tools.ts
2195
2499
  function createGitHubTools(ctx, botEmail, pullRequestSubscription) {
2196
2500
  return {
@@ -2205,8 +2509,15 @@ function createGitHubTools(ctx, botEmail, pullRequestSubscription) {
2205
2509
  getRelease: createGitHubGetReleaseTool(ctx),
2206
2510
  getRepository: createGitHubGetRepositoryTool(ctx),
2207
2511
  resolvePullRequestReviewThread: createGitHubResolvePullRequestReviewThreadTool(ctx, botEmail),
2512
+ submitPullRequestReview: createGitHubSubmitPullRequestReviewTool(
2513
+ ctx.egress
2514
+ ),
2208
2515
  updateIssue: createGitHubUpdateIssueTool(ctx),
2209
- updatePullRequest: createGitHubUpdatePullRequestTool(ctx)
2516
+ updatePullRequest: createGitHubUpdatePullRequestTool(ctx),
2517
+ updatePullRequestFeedback: createGitHubUpdatePullRequestFeedbackTool(
2518
+ ctx,
2519
+ botEmail
2520
+ )
2210
2521
  };
2211
2522
  }
2212
2523
 
@@ -2215,7 +2526,7 @@ import { createHmac, timingSafeEqual } from "crypto";
2215
2526
 
2216
2527
  // src/issue-outcomes/store.ts
2217
2528
  import { and, eq, lte, sql as sql2 } from "drizzle-orm";
2218
- import { z as z12 } from "zod";
2529
+ import { z as z14 } from "zod";
2219
2530
 
2220
2531
  // src/db/schema.ts
2221
2532
  import { sql } from "drizzle-orm";
@@ -2227,18 +2538,18 @@ import {
2227
2538
  text,
2228
2539
  timestamp
2229
2540
  } from "drizzle-orm/pg-core";
2230
- import { z as z11 } from "zod";
2231
- var githubPullRequestStateSchema = z11.enum([
2541
+ import { z as z13 } from "zod";
2542
+ var githubPullRequestStateSchema = z13.enum([
2232
2543
  "closed_unmerged",
2233
2544
  "merged",
2234
2545
  "open"
2235
2546
  ]);
2236
- var githubPullRequestCommitCompositionSchema = z11.enum([
2547
+ var githubPullRequestCommitCompositionSchema = z13.enum([
2237
2548
  "junior_only",
2238
2549
  "mixed"
2239
2550
  ]);
2240
- var githubIssueStateSchema = z11.enum(["closed", "open"]);
2241
- var githubIssueStateReasonSchema = z11.enum([
2551
+ var githubIssueStateSchema = z13.enum(["closed", "open"]);
2552
+ var githubIssueStateReasonSchema = z13.enum([
2242
2553
  "completed",
2243
2554
  "duplicate",
2244
2555
  "not_planned",
@@ -2308,21 +2619,21 @@ var juniorGitHubPullRequestIssues = pgTable(
2308
2619
  );
2309
2620
 
2310
2621
  // src/issue-outcomes/store.ts
2311
- var githubIssueOutcomeInputSchema = z12.object({
2312
- candidateOwned: z12.boolean(),
2313
- closedAt: z12.date().optional(),
2314
- issueId: z12.string().min(1),
2315
- number: z12.number().int().positive(),
2316
- openedAt: z12.date(),
2317
- repositoryFullName: z12.string().min(1),
2318
- repositoryId: z12.string().min(1),
2622
+ var githubIssueOutcomeInputSchema = z14.object({
2623
+ candidateOwned: z14.boolean(),
2624
+ closedAt: z14.date().optional(),
2625
+ issueId: z14.string().min(1),
2626
+ number: z14.number().int().positive(),
2627
+ openedAt: z14.date(),
2628
+ repositoryFullName: z14.string().min(1),
2629
+ repositoryId: z14.string().min(1),
2319
2630
  state: githubIssueStateSchema,
2320
2631
  stateReason: githubIssueStateReasonSchema.optional(),
2321
- updatedAt: z12.date()
2632
+ updatedAt: z14.date()
2322
2633
  }).strict();
2323
- var githubIssueConversationsInputSchema = z12.object({
2324
- conversationIds: z12.array(z12.string().min(1)).min(1),
2325
- issueId: z12.string().min(1)
2634
+ var githubIssueConversationsInputSchema = z14.object({
2635
+ conversationIds: z14.array(z14.string().min(1)).min(1),
2636
+ issueId: z14.string().min(1)
2326
2637
  }).strict();
2327
2638
  function projectionValues(input) {
2328
2639
  return {
@@ -2386,33 +2697,33 @@ async function recordGitHubIssueConversations(db, input) {
2386
2697
 
2387
2698
  // src/pull-request-outcomes/store.ts
2388
2699
  import { and as and2, eq as eq2, lte as lte2, ne, sql as sql3 } from "drizzle-orm";
2389
- import { z as z13 } from "zod";
2390
- var githubPullRequestOutcomeInputSchema = z13.object({
2391
- candidateOwned: z13.boolean(),
2392
- closedAt: z13.date().optional(),
2700
+ import { z as z15 } from "zod";
2701
+ var githubPullRequestOutcomeInputSchema = z15.object({
2702
+ candidateOwned: z15.boolean(),
2703
+ closedAt: z15.date().optional(),
2393
2704
  commitComposition: githubPullRequestCommitCompositionSchema.optional(),
2394
- mergedAt: z13.date().optional(),
2395
- number: z13.number().int().positive(),
2396
- openedAt: z13.date(),
2397
- pullRequestId: z13.string().min(1),
2398
- repositoryFullName: z13.string().min(1),
2399
- repositoryId: z13.string().min(1),
2705
+ mergedAt: z15.date().optional(),
2706
+ number: z15.number().int().positive(),
2707
+ openedAt: z15.date(),
2708
+ pullRequestId: z15.string().min(1),
2709
+ repositoryFullName: z15.string().min(1),
2710
+ repositoryId: z15.string().min(1),
2400
2711
  state: githubPullRequestStateSchema,
2401
- title: z13.string().min(1).optional(),
2402
- updatedAt: z13.date()
2712
+ title: z15.string().min(1).optional(),
2713
+ updatedAt: z15.date()
2403
2714
  }).strict();
2404
- var githubPullRequestConversationsInputSchema = z13.object({
2405
- conversationIds: z13.array(z13.string().min(1)).min(1),
2406
- pullRequestId: z13.string().min(1)
2715
+ var githubPullRequestConversationsInputSchema = z15.object({
2716
+ conversationIds: z15.array(z15.string().min(1)).min(1),
2717
+ pullRequestId: z15.string().min(1)
2407
2718
  }).strict();
2408
- var githubPullRequestLinkedIssuesInputSchema = z13.object({
2409
- linkedIssues: z13.array(
2410
- z13.object({
2411
- number: z13.number().int().positive(),
2412
- repositoryFullName: z13.string().min(1)
2719
+ var githubPullRequestLinkedIssuesInputSchema = z15.object({
2720
+ linkedIssues: z15.array(
2721
+ z15.object({
2722
+ number: z15.number().int().positive(),
2723
+ repositoryFullName: z15.string().min(1)
2413
2724
  }).strict()
2414
2725
  ).min(1),
2415
- pullRequestId: z13.string().min(1)
2726
+ pullRequestId: z15.string().min(1)
2416
2727
  }).strict();
2417
2728
  function projectionValues2(input) {
2418
2729
  return {
@@ -2577,39 +2888,39 @@ async function recordGitHubPullRequestLinkedIssues(db, input) {
2577
2888
  }
2578
2889
 
2579
2890
  // src/webhooks/issue-outcome.ts
2580
- import { z as z14 } from "zod";
2581
- var canonicalIssueOutcomeSchema = z14.object({
2582
- action: z14.enum(["opened", "closed", "reopened"]),
2583
- issue: z14.object({
2584
- body: z14.string().nullable().optional(),
2585
- closed_at: z14.string().nullable().optional(),
2586
- created_at: z14.string(),
2587
- id: z14.number().int().positive(),
2588
- number: z14.number().int().positive(),
2891
+ import { z as z16 } from "zod";
2892
+ var canonicalIssueOutcomeSchema = z16.object({
2893
+ action: z16.enum(["opened", "closed", "reopened"]),
2894
+ issue: z16.object({
2895
+ body: z16.string().nullable().optional(),
2896
+ closed_at: z16.string().nullable().optional(),
2897
+ created_at: z16.string(),
2898
+ id: z16.number().int().positive(),
2899
+ number: z16.number().int().positive(),
2589
2900
  state_reason: githubIssueStateReasonSchema.nullable().optional(),
2590
- updated_at: z14.string(),
2591
- user: z14.object({ login: z14.string().min(1) }).strict()
2901
+ updated_at: z16.string(),
2902
+ user: z16.object({ login: z16.string().min(1) }).strict()
2592
2903
  }).strict(),
2593
- repository: z14.object({
2594
- full_name: z14.string().min(1),
2595
- id: z14.number().int().positive()
2904
+ repository: z16.object({
2905
+ full_name: z16.string().min(1),
2906
+ id: z16.number().int().positive()
2596
2907
  }).strict()
2597
2908
  }).strict();
2598
- var issueOutcomeSchema = z14.object({
2599
- action: z14.enum(["opened", "closed", "reopened"]),
2600
- issue: z14.object({
2601
- body: z14.string().nullable().optional(),
2602
- closed_at: z14.string().nullable().optional(),
2603
- created_at: z14.string(),
2604
- id: z14.number().int().positive(),
2605
- number: z14.number().int().positive(),
2909
+ var issueOutcomeSchema = z16.object({
2910
+ action: z16.enum(["opened", "closed", "reopened"]),
2911
+ issue: z16.object({
2912
+ body: z16.string().nullable().optional(),
2913
+ closed_at: z16.string().nullable().optional(),
2914
+ created_at: z16.string(),
2915
+ id: z16.number().int().positive(),
2916
+ number: z16.number().int().positive(),
2606
2917
  state_reason: githubIssueStateReasonSchema.nullable().optional(),
2607
- updated_at: z14.string(),
2608
- user: z14.object({ login: z14.string().min(1) }).passthrough()
2918
+ updated_at: z16.string(),
2919
+ user: z16.object({ login: z16.string().min(1) }).passthrough()
2609
2920
  }).passthrough(),
2610
- repository: z14.object({
2611
- full_name: z14.string().min(1),
2612
- id: z14.number().int().positive()
2921
+ repository: z16.object({
2922
+ full_name: z16.string().min(1),
2923
+ id: z16.number().int().positive()
2613
2924
  }).passthrough()
2614
2925
  }).passthrough().transform(
2615
2926
  (provider) => canonicalIssueOutcomeSchema.parse({
@@ -2630,7 +2941,7 @@ var issueOutcomeSchema = z14.object({
2630
2941
  }
2631
2942
  })
2632
2943
  );
2633
- var issueLifecycleActionSchema = z14.object({ action: z14.string() }).passthrough();
2944
+ var issueLifecycleActionSchema = z16.object({ action: z16.string() }).passthrough();
2634
2945
  function timestamp2(value) {
2635
2946
  if (!value) return void 0;
2636
2947
  const parsed = new Date(value);
@@ -2676,21 +2987,21 @@ function normalizeGitHubIssueOutcome(args) {
2676
2987
  updatedAt
2677
2988
  };
2678
2989
  }
2679
- var canonicalIssueConversationSchema = z14.object({
2680
- issue: z14.object({
2681
- body: z14.string().nullable().optional(),
2682
- id: z14.number().int().positive(),
2683
- user: z14.object({ login: z14.string().min(1) }).strict()
2990
+ var canonicalIssueConversationSchema = z16.object({
2991
+ issue: z16.object({
2992
+ body: z16.string().nullable().optional(),
2993
+ id: z16.number().int().positive(),
2994
+ user: z16.object({ login: z16.string().min(1) }).strict()
2684
2995
  }).strict(),
2685
- sender: z14.object({ login: z14.string().min(1) }).strict().optional()
2996
+ sender: z16.object({ login: z16.string().min(1) }).strict().optional()
2686
2997
  }).strict();
2687
- var issueConversationSchema = z14.object({
2688
- issue: z14.object({
2689
- body: z14.string().nullable().optional(),
2690
- id: z14.number().int().positive(),
2691
- user: z14.object({ login: z14.string().min(1) }).passthrough()
2998
+ var issueConversationSchema = z16.object({
2999
+ issue: z16.object({
3000
+ body: z16.string().nullable().optional(),
3001
+ id: z16.number().int().positive(),
3002
+ user: z16.object({ login: z16.string().min(1) }).passthrough()
2692
3003
  }).passthrough(),
2693
- sender: z14.object({ login: z14.string().min(1) }).passthrough().optional()
3004
+ sender: z16.object({ login: z16.string().min(1) }).passthrough().optional()
2694
3005
  }).passthrough().transform(
2695
3006
  (provider) => canonicalIssueConversationSchema.parse({
2696
3007
  issue: {
@@ -2717,43 +3028,43 @@ function normalizeGitHubIssueConversations(args) {
2717
3028
  }
2718
3029
 
2719
3030
  // src/webhooks/pull-request-outcome.ts
2720
- import { z as z15 } from "zod";
2721
- var canonicalPullRequestOutcomeSchema = z15.object({
2722
- action: z15.enum(["opened", "closed", "reopened"]),
2723
- pull_request: z15.object({
2724
- body: z15.string().nullable().optional(),
2725
- closed_at: z15.string().nullable().optional(),
2726
- created_at: z15.string(),
2727
- id: z15.number().int().positive(),
2728
- merged: z15.boolean(),
2729
- merged_at: z15.string().nullable().optional(),
2730
- number: z15.number().int().positive(),
2731
- title: z15.string().min(1).optional(),
2732
- updated_at: z15.string(),
2733
- user: z15.object({ login: z15.string().min(1) }).strict()
3031
+ import { z as z17 } from "zod";
3032
+ var canonicalPullRequestOutcomeSchema = z17.object({
3033
+ action: z17.enum(["opened", "closed", "reopened"]),
3034
+ pull_request: z17.object({
3035
+ body: z17.string().nullable().optional(),
3036
+ closed_at: z17.string().nullable().optional(),
3037
+ created_at: z17.string(),
3038
+ id: z17.number().int().positive(),
3039
+ merged: z17.boolean(),
3040
+ merged_at: z17.string().nullable().optional(),
3041
+ number: z17.number().int().positive(),
3042
+ title: z17.string().min(1).optional(),
3043
+ updated_at: z17.string(),
3044
+ user: z17.object({ login: z17.string().min(1) }).strict()
2734
3045
  }).strict(),
2735
- repository: z15.object({
2736
- full_name: z15.string().min(1),
2737
- id: z15.number().int().positive()
3046
+ repository: z17.object({
3047
+ full_name: z17.string().min(1),
3048
+ id: z17.number().int().positive()
2738
3049
  }).strict()
2739
3050
  }).strict();
2740
- var pullRequestOutcomeSchema = z15.object({
2741
- action: z15.enum(["opened", "closed", "reopened"]),
2742
- pull_request: z15.object({
2743
- body: z15.string().nullable().optional(),
2744
- closed_at: z15.string().nullable().optional(),
2745
- created_at: z15.string(),
2746
- id: z15.number().int().positive(),
2747
- merged: z15.boolean(),
2748
- merged_at: z15.string().nullable().optional(),
2749
- number: z15.number().int().positive(),
2750
- title: z15.string().min(1).optional(),
2751
- updated_at: z15.string(),
2752
- user: z15.object({ login: z15.string().min(1) }).passthrough()
3051
+ var pullRequestOutcomeSchema = z17.object({
3052
+ action: z17.enum(["opened", "closed", "reopened"]),
3053
+ pull_request: z17.object({
3054
+ body: z17.string().nullable().optional(),
3055
+ closed_at: z17.string().nullable().optional(),
3056
+ created_at: z17.string(),
3057
+ id: z17.number().int().positive(),
3058
+ merged: z17.boolean(),
3059
+ merged_at: z17.string().nullable().optional(),
3060
+ number: z17.number().int().positive(),
3061
+ title: z17.string().min(1).optional(),
3062
+ updated_at: z17.string(),
3063
+ user: z17.object({ login: z17.string().min(1) }).passthrough()
2753
3064
  }).passthrough(),
2754
- repository: z15.object({
2755
- full_name: z15.string().min(1),
2756
- id: z15.number().int().positive()
3065
+ repository: z17.object({
3066
+ full_name: z17.string().min(1),
3067
+ id: z17.number().int().positive()
2757
3068
  }).passthrough()
2758
3069
  }).passthrough().transform(
2759
3070
  (provider) => canonicalPullRequestOutcomeSchema.parse({
@@ -2776,24 +3087,24 @@ var pullRequestOutcomeSchema = z15.object({
2776
3087
  }
2777
3088
  })
2778
3089
  );
2779
- var pullRequestLifecycleActionSchema = z15.object({ action: z15.string() }).passthrough();
2780
- var canonicalPullRequestConversationSchema = z15.object({
2781
- pull_request: z15.object({
2782
- body: z15.string().nullable().optional(),
2783
- id: z15.number().int().positive(),
2784
- user: z15.object({ login: z15.string().min(1) }).strict()
3090
+ var pullRequestLifecycleActionSchema = z17.object({ action: z17.string() }).passthrough();
3091
+ var canonicalPullRequestConversationSchema = z17.object({
3092
+ pull_request: z17.object({
3093
+ body: z17.string().nullable().optional(),
3094
+ id: z17.number().int().positive(),
3095
+ user: z17.object({ login: z17.string().min(1) }).strict()
2785
3096
  }).strict(),
2786
- repository: z15.object({ full_name: z15.string().min(1) }).strict(),
2787
- sender: z15.object({ login: z15.string().min(1) }).strict()
3097
+ repository: z17.object({ full_name: z17.string().min(1) }).strict(),
3098
+ sender: z17.object({ login: z17.string().min(1) }).strict()
2788
3099
  }).strict();
2789
- var pullRequestConversationSchema = z15.object({
2790
- pull_request: z15.object({
2791
- body: z15.string().nullable().optional(),
2792
- id: z15.number().int().positive(),
2793
- user: z15.object({ login: z15.string().min(1) }).passthrough()
3100
+ var pullRequestConversationSchema = z17.object({
3101
+ pull_request: z17.object({
3102
+ body: z17.string().nullable().optional(),
3103
+ id: z17.number().int().positive(),
3104
+ user: z17.object({ login: z17.string().min(1) }).passthrough()
2794
3105
  }).passthrough(),
2795
- repository: z15.object({ full_name: z15.string().min(1) }).passthrough(),
2796
- sender: z15.object({ login: z15.string().min(1) }).passthrough()
3106
+ repository: z17.object({ full_name: z17.string().min(1) }).passthrough(),
3107
+ sender: z17.object({ login: z17.string().min(1) }).passthrough()
2797
3108
  }).passthrough().transform(
2798
3109
  (provider) => canonicalPullRequestConversationSchema.parse({
2799
3110
  pull_request: {
@@ -2913,6 +3224,20 @@ function webhookInstallationId(body) {
2913
3224
  }
2914
3225
  return parseInstallationId(installation.id);
2915
3226
  }
3227
+ function pullRequestFeedbackTarget(events) {
3228
+ const event = events.find(
3229
+ (candidate) => candidate.eventType === "pull_request.comment.created" || candidate.eventType === "pull_request.review_comment.created"
3230
+ );
3231
+ const data = event?.data;
3232
+ if (!data || typeof data.repo !== "string" || typeof data.commentId !== "number" || data.commentKind !== "conversation" && data.commentKind !== "review") {
3233
+ return void 0;
3234
+ }
3235
+ return {
3236
+ commentId: data.commentId,
3237
+ commentKind: data.commentKind,
3238
+ repo: data.repo
3239
+ };
3240
+ }
2916
3241
  function githubCodeChange(outcome, conversationIds) {
2917
3242
  return {
2918
3243
  closedAt: outcome.closedAt,
@@ -3063,6 +3388,21 @@ function createGitHubWebhookRoute(args) {
3063
3388
  deliveryId,
3064
3389
  eventName
3065
3390
  });
3391
+ const feedbackTarget = pullRequestFeedbackTarget(resourceEvents);
3392
+ const hasMatch = args.resourceEvents.hasMatch;
3393
+ const feedbackHasMatch = feedbackTarget && hasMatch ? (await Promise.all(resourceEvents.map((event) => hasMatch(event)))).some(Boolean) : false;
3394
+ if (feedbackTarget && feedbackHasMatch) {
3395
+ try {
3396
+ await args.markFeedbackReviewing(feedbackTarget);
3397
+ } catch (error) {
3398
+ args.log?.error("GitHub pull request feedback reaction failed", {
3399
+ commentId: feedbackTarget.commentId,
3400
+ deliveryId,
3401
+ errorType: error instanceof Error ? error.name : "UnknownError",
3402
+ repository: feedbackTarget.repo
3403
+ });
3404
+ }
3405
+ }
3066
3406
  for (const event of resourceEvents) {
3067
3407
  await args.resourceEvents.publish(event);
3068
3408
  }
@@ -3075,18 +3415,18 @@ function createGitHubWebhookRoute(args) {
3075
3415
  }
3076
3416
 
3077
3417
  // src/pull-request-outcomes/commit-composition.ts
3078
- import { z as z16 } from "zod";
3079
- var canonicalCommitSchema = z16.object({
3080
- authorEmail: z16.string().nullable(),
3081
- authorLogin: z16.string().nullable()
3418
+ import { z as z18 } from "zod";
3419
+ var canonicalCommitSchema = z18.object({
3420
+ authorEmail: z18.string().nullable(),
3421
+ authorLogin: z18.string().nullable()
3082
3422
  }).strict();
3083
- var providerCommitSchema = z16.object({
3084
- author: z16.object({ login: z16.string() }).passthrough().nullable(),
3085
- commit: z16.object({
3086
- author: z16.object({ email: z16.string() }).passthrough().nullable()
3423
+ var providerCommitSchema = z18.object({
3424
+ author: z18.object({ login: z18.string() }).passthrough().nullable(),
3425
+ commit: z18.object({
3426
+ author: z18.object({ email: z18.string() }).passthrough().nullable()
3087
3427
  }).passthrough()
3088
3428
  }).passthrough();
3089
- var commitPageSchema = z16.array(providerCommitSchema).transform(
3429
+ var commitPageSchema = z18.array(providerCommitSchema).transform(
3090
3430
  (commits) => commits.map(
3091
3431
  (commit) => canonicalCommitSchema.parse({
3092
3432
  authorEmail: commit.commit.author?.email ?? null,
@@ -3950,6 +4290,7 @@ function shouldInspectGitHubGraphqlResponse(ctx) {
3950
4290
  const contentType = ctx.response.headers.get("content-type");
3951
4291
  return contentType ? /\bjson\b/i.test(contentType) : false;
3952
4292
  }
4293
+ var PULL_REQUEST_COMMENT_REACTIONS_PATH = /^\/repos\/[^/]+\/[^/]+\/(issues|pulls)\/comments\/[^/]+\/reactions(?:\/[^/]+)?$/;
3953
4294
  var GITHUB_BODY_WRITES = [
3954
4295
  {
3955
4296
  denialMessage: `GitHub issue creation must use the github_createIssue tool so Junior can own idempotency and the conversation footer. ${CREATE_TOOL_ROUTING_GUIDANCE}`,
@@ -3965,6 +4306,13 @@ var GITHUB_BODY_WRITES = [
3965
4306
  operation: "github.pull.create",
3966
4307
  restPath: /^\/repos\/[^/]+\/[^/]+\/pulls$/
3967
4308
  },
4309
+ {
4310
+ denialMessage: `GitHub pull request reviews must use the github_submitPullRequestReview tool. The tool posts to /repos/{owner}/{repo}/pulls/{number}/reviews. ${CREATE_TOOL_ROUTING_GUIDANCE}`,
4311
+ graphqlField: "addPullRequestReview",
4312
+ method: "POST",
4313
+ operation: "github.pull.review.create",
4314
+ restPath: /^\/repos\/[^/]+\/[^/]+\/pulls\/[^/]+\/reviews$/
4315
+ },
3968
4316
  {
3969
4317
  denialMessage: `GitHub issue updates must use the github_updateIssue tool so Junior can own requester attribution and the conversation footer. ${CREATE_TOOL_ROUTING_GUIDANCE}`,
3970
4318
  graphqlField: "updateIssue",
@@ -4027,6 +4375,9 @@ function githubApiWriteGrantName(method, upstreamUrl) {
4027
4375
  ) && !HTTP_READ_METHODS.has(method)) {
4028
4376
  return "installation-write";
4029
4377
  }
4378
+ if ((method === "POST" || method === "DELETE") && PULL_REQUEST_COMMENT_REACTIONS_PATH.test(pathname)) {
4379
+ return "installation-write";
4380
+ }
4030
4381
  return void 0;
4031
4382
  }
4032
4383
  function reviewThreadResolveRepository(operation, method, upstreamUrl, bodyText) {
@@ -4054,6 +4405,16 @@ function isGitHubGraphqlMutation(method, upstreamUrl, bodyText, field) {
4054
4405
  ).test(parsed.normalized);
4055
4406
  }
4056
4407
  function applyGitHubEgressPolicy(input) {
4408
+ const reactionWrite = isGitHubApiUrl(input.upstreamUrl) && (input.method === "POST" || input.method === "DELETE") && PULL_REQUEST_COMMENT_REACTIONS_PATH.test(
4409
+ input.upstreamUrl.pathname.toLowerCase()
4410
+ );
4411
+ if (reactionWrite) {
4412
+ const expectedOperation = input.method === "POST" ? "github.pull.comment-reaction.create" : "github.pull.comment-reaction.delete";
4413
+ enforceEgressPolicy({
4414
+ allowed: input.operation === expectedOperation,
4415
+ denialMessage: "GitHub pull request feedback reactions must use the github_updatePullRequestFeedback tool."
4416
+ });
4417
+ }
4057
4418
  assertGitHubPullRequestApprovalDenied({
4058
4419
  ...input.bodyText !== void 0 ? { bodyText: input.bodyText } : void 0,
4059
4420
  method: input.method,
@@ -4087,7 +4448,7 @@ function requireRepositoryTarget(upstreamUrl) {
4087
4448
  return;
4088
4449
  }
4089
4450
  throw new EgressPolicyDenied2(
4090
- "GitHub write request does not identify a target repository."
4451
+ "GitHub write request has no repository in its URL. Use a /repos/{owner}/{repo}/... URL or a GitHub tool for the operation."
4091
4452
  );
4092
4453
  }
4093
4454
  async function githubGrantForEgress(ctx) {
@@ -4154,7 +4515,7 @@ async function githubGrantForEgress(ctx) {
4154
4515
  if (graphqlAccess) {
4155
4516
  if (graphqlAccess === "write") {
4156
4517
  throw new EgressPolicyDenied2(
4157
- "GitHub GraphQL mutations are not enabled for runtime credentials."
4518
+ "GitHub GraphQL mutations are not enabled for runtime credentials. Use the matching GitHub tool or a supported REST endpoint. For pull request reviews, use github_submitPullRequestReview. It posts to /repos/{owner}/{repo}/pulls/{number}/reviews."
4158
4519
  );
4159
4520
  }
4160
4521
  return grantForAccess(
@@ -4166,7 +4527,7 @@ async function githubGrantForEgress(ctx) {
4166
4527
  const access = HTTP_READ_METHODS.has(method) ? "read" : "write";
4167
4528
  if (access === "write") {
4168
4529
  throw new EgressPolicyDenied2(
4169
- "GitHub write request is not an explicitly allowed Junior operation."
4530
+ "Junior does not support this GitHub write request. Use the matching GitHub tool or a supported REST endpoint."
4170
4531
  );
4171
4532
  }
4172
4533
  return grantForAccess(access, "github.api-read", "installation-read");
@@ -4205,7 +4566,9 @@ function githubPlugin(options = {}) {
4205
4566
  supportedEvents: [...GITHUB_PULL_REQUEST_EVENTS],
4206
4567
  suggestedEvents: [...GITHUB_PULL_REQUEST_SUGGESTED_EVENTS],
4207
4568
  matchFields: GITHUB_PULL_REQUEST_MATCH_FIELDS,
4208
- ...options.pullRequestEvents?.guidance ? { guidance: options.pullRequestEvents.guidance } : void 0
4569
+ guidance: gitHubPullRequestEventGuidance(
4570
+ options.pullRequestEvents?.guidance
4571
+ )
4209
4572
  },
4210
4573
  {
4211
4574
  type: "release_source",
@@ -4214,14 +4577,20 @@ function githubPlugin(options = {}) {
4214
4577
  },
4215
4578
  {
4216
4579
  type: "repository",
4217
- supportedEvents: [...GITHUB_ISSUE_EVENTS, ...GITHUB_PULL_REQUEST_EVENTS],
4580
+ supportedEvents: [
4581
+ ...GITHUB_ISSUE_EVENTS,
4582
+ ...GITHUB_PULL_REQUEST_EVENTS
4583
+ ],
4218
4584
  suggestedEvents: [
4219
4585
  "issue.opened",
4220
4586
  "pull_request.opened",
4221
4587
  ...GITHUB_ISSUE_SUGGESTED_EVENTS,
4222
4588
  ...GITHUB_PULL_REQUEST_SUGGESTED_EVENTS
4223
4589
  ],
4224
- matchFields: GITHUB_PULL_REQUEST_MATCH_FIELDS
4590
+ matchFields: GITHUB_PULL_REQUEST_MATCH_FIELDS,
4591
+ guidance: gitHubPullRequestEventGuidance(
4592
+ options.pullRequestEvents?.guidance
4593
+ )
4225
4594
  }
4226
4595
  ],
4227
4596
  isEnabled: () => Boolean(readEnv("GITHUB_WEBHOOK_SECRET")),
@@ -4343,6 +4712,36 @@ function githubPlugin(options = {}) {
4343
4712
  installationId: () => readEnv(installationIdEnv),
4344
4713
  installationIdEnv,
4345
4714
  log: ctx.log,
4715
+ markFeedbackReviewing: async ({ commentId, commentKind, repo }) => {
4716
+ const [owner, name, ...extra] = repo.split("/");
4717
+ if (!owner || !name || extra.length > 0) {
4718
+ throw new Error(
4719
+ "GitHub pull request feedback received an invalid repository name"
4720
+ );
4721
+ }
4722
+ const token = await issueInstallationToken({
4723
+ appIdEnv,
4724
+ privateKeyEnv,
4725
+ installationIdEnv,
4726
+ repositories: [name]
4727
+ });
4728
+ await githubRequest(
4729
+ "https://api.github.com",
4730
+ pullRequestCommentReactionsPath({
4731
+ owner,
4732
+ name,
4733
+ commentKind,
4734
+ commentId
4735
+ }),
4736
+ {
4737
+ body: {
4738
+ content: pullRequestFeedbackReactionContent("reviewing")
4739
+ },
4740
+ method: "POST",
4741
+ token: token.token
4742
+ }
4743
+ );
4744
+ },
4346
4745
  privateKeyEnv,
4347
4746
  resourceEvents: ctx.resourceEvents,
4348
4747
  webhookSecret: () => readEnv("GITHUB_WEBHOOK_SECRET")