@sentry/junior-github 0.162.0 → 0.164.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
@@ -730,16 +730,33 @@ import {
730
730
  pluginToolOutputSchema
731
731
  } from "@sentry/junior-plugin-api";
732
732
  import { z } from "zod";
733
- var RESERVED_SANDBOX_DIRECTORIES = /* @__PURE__ */ new Set([".junior", "data", "skills"]);
733
+
734
+ // src/sandbox-paths.ts
735
+ var RESERVED_SANDBOX_DIRECTORIES = /* @__PURE__ */ new Set([
736
+ ".junior",
737
+ "data",
738
+ "skills"
739
+ ]);
740
+ function isReservedSandboxDirectory(path) {
741
+ return RESERVED_SANDBOX_DIRECTORIES.has(path.toLowerCase());
742
+ }
743
+
744
+ // src/tools/clone-repository.ts
734
745
  var inputSchema = z.object({
735
746
  repo: z.string().describe('Repository in "owner/name" format.'),
736
- directory: z.string().regex(/^[A-Za-z0-9._-]+$/).refine((value) => value !== "." && value !== "..", {
737
- message: "Directory must be a single directory name."
738
- }).describe("Optional destination directory under the sandbox root.").optional()
747
+ directory: z.string().regex(/^(?:[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*)$/).refine(
748
+ (value) => !value.split("/").some((part) => part === "." || part === ".."),
749
+ {
750
+ message: "Directory must be a relative path without . or .. segments."
751
+ }
752
+ ).describe(
753
+ "Optional destination directory under the sandbox root. Defaults to repos/{name}."
754
+ ).optional()
739
755
  }).strict();
740
756
  var cloneSchema = z.object({
741
757
  path: z.string(),
742
- repo: z.string()
758
+ repo: z.string(),
759
+ workspaces: z.array(z.string())
743
760
  });
744
761
  var outputSchema = pluginToolOutputSchema.extend({
745
762
  target: z.literal("cloneRepository"),
@@ -753,7 +770,7 @@ function parseRepo(value) {
753
770
  return { owner: parts[0], name: parts[1] };
754
771
  }
755
772
  function defaultDirectory(repoName) {
756
- return RESERVED_SANDBOX_DIRECTORIES.has(repoName) ? `${repoName}-repo` : repoName;
773
+ return `repos/${repoName}`;
757
774
  }
758
775
  function commandSignal(signal, timeoutMs) {
759
776
  const timeout = AbortSignal.timeout(timeoutMs);
@@ -802,7 +819,27 @@ function createGitHubCloneRepositoryTool(ctx) {
802
819
  async execute(input, options) {
803
820
  const repo = parseRepo(input.repo);
804
821
  const directory = input.directory ?? defaultDirectory(repo.name);
822
+ const rootSegment = directory.split("/")[0] ?? directory;
823
+ if (isReservedSandboxDirectory(rootSegment)) {
824
+ throw new PluginToolInputError(
825
+ `Directory conflicts with a reserved sandbox path: ${directory}`
826
+ );
827
+ }
805
828
  const path = `${ctx.sandbox.root}/${directory}`;
829
+ const parentDirectory = directory.includes("/") ? directory.slice(0, directory.lastIndexOf("/")) : void 0;
830
+ if (parentDirectory) {
831
+ const mkdir = await ctx.sandbox.run({
832
+ cmd: "mkdir",
833
+ args: ["-p", "--", `${ctx.sandbox.root}/${parentDirectory}`],
834
+ cwd: ctx.sandbox.root,
835
+ signal: commandSignal(options.signal, 3e4)
836
+ });
837
+ if (mkdir.exitCode !== 0) {
838
+ throw new PluginToolInputError(
839
+ `Failed to create clone parent directory: ${parentDirectory}`
840
+ );
841
+ }
842
+ }
806
843
  const exists = await ctx.sandbox.run({
807
844
  cmd: "bash",
808
845
  args: ["-c", `test -e "$1"`, "bash", path],
@@ -837,7 +874,20 @@ function createGitHubCloneRepositoryTool(ctx) {
837
874
  `GitHub repository clone failed: ${clone.stderr.trim() || `exit ${clone.exitCode}`}`
838
875
  );
839
876
  }
840
- const data = { repo: `${repo.owner}/${repo.name}`, path };
877
+ const repoId = `${repo.owner}/${repo.name}`;
878
+ let workspaces = [];
879
+ try {
880
+ workspaces = await ctx.workspaces.findByRepository({
881
+ provider: "github",
882
+ repo: repoId
883
+ });
884
+ } catch (error) {
885
+ ctx.log.error("github.clone.workspaces_lookup.failed", {
886
+ repo: repoId,
887
+ error: error instanceof Error ? error.message : String(error)
888
+ });
889
+ }
890
+ const data = { repo: repoId, path, workspaces };
841
891
  return { target: "cloneRepository", ...data };
842
892
  }
843
893
  });
@@ -2550,8 +2600,200 @@ function createGitHubUpdatePullRequestTool(ctx) {
2550
2600
  });
2551
2601
  }
2552
2602
 
2603
+ // src/tools/resolve-pull-request-review-thread.ts
2604
+ import {
2605
+ definePluginTool as definePluginTool10,
2606
+ PluginToolInputError as PluginToolInputError11,
2607
+ pluginToolOutputSchema as pluginToolOutputSchema10
2608
+ } from "@sentry/junior-plugin-api";
2609
+ import { z as z10 } from "zod";
2610
+
2611
+ // src/webhooks/ownership.ts
2612
+ var GITHUB_NOREPLY_DOMAIN = "users.noreply.github.com";
2613
+ function botLoginFromEmail(value) {
2614
+ const email = value?.trim();
2615
+ if (!email) return void 0;
2616
+ const separator = email.lastIndexOf("@");
2617
+ if (separator <= 0) return void 0;
2618
+ const domain = email.slice(separator + 1).toLowerCase();
2619
+ if (domain !== GITHUB_NOREPLY_DOMAIN) return void 0;
2620
+ const localPart = email.slice(0, separator);
2621
+ const login = localPart.slice(localPart.indexOf("+") + 1).trim();
2622
+ return login.toLowerCase().endsWith("[bot]") ? login : void 0;
2623
+ }
2624
+
2625
+ // src/tools/resolve-pull-request-review-thread.ts
2626
+ var inputSchema8 = z10.object({
2627
+ repo: z10.string().describe(
2628
+ 'Repository in "owner/name" format. Required for repository-scoped credentials (GraphQL has no repo path).'
2629
+ ),
2630
+ threadId: z10.string().trim().min(1).describe(
2631
+ "GitHub pull request review thread node ID (the same `threadId` / `id` variable used by `gh api graphql` resolveReviewThread)."
2632
+ )
2633
+ }).strict();
2634
+ var outputSchema8 = pluginToolOutputSchema10.extend({
2635
+ target: z10.literal("resolvePullRequestReviewThread"),
2636
+ repo: z10.string(),
2637
+ number: z10.number(),
2638
+ threadId: z10.string(),
2639
+ resolved: z10.boolean()
2640
+ });
2641
+ function parseRepo10(value) {
2642
+ const parts = value.split("/").map((part) => part.trim());
2643
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
2644
+ throw new PluginToolInputError11('repo must use "owner/name" format');
2645
+ }
2646
+ return { owner: parts[0], name: parts[1], ref: `${parts[0]}/${parts[1]}` };
2647
+ }
2648
+ async function readJson7(response) {
2649
+ const text2 = await response.text();
2650
+ if (!text2) return void 0;
2651
+ try {
2652
+ return JSON.parse(text2);
2653
+ } catch {
2654
+ return text2;
2655
+ }
2656
+ }
2657
+ function githubError(payload) {
2658
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
2659
+ const message = payload.message;
2660
+ if (typeof message === "string" && message.trim()) return message.trim();
2661
+ }
2662
+ return "GitHub request failed";
2663
+ }
2664
+ function createGitHubResolvePullRequestReviewThreadTool(ctx, botEmail) {
2665
+ return definePluginTool10({
2666
+ annotations: {
2667
+ destructiveHint: true,
2668
+ idempotentHint: true,
2669
+ openWorldHint: true,
2670
+ readOnlyHint: false
2671
+ },
2672
+ 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 Junior authored.",
2673
+ inputSchema: inputSchema8,
2674
+ outputSchema: outputSchema8,
2675
+ async execute(input) {
2676
+ const parsedInput = inputSchema8.safeParse(input);
2677
+ if (!parsedInput.success) {
2678
+ throw new PluginToolInputError11(
2679
+ "Invalid GitHub resolvePullRequestReviewThread input.",
2680
+ { cause: parsedInput.error }
2681
+ );
2682
+ }
2683
+ const repo = parseRepo10(parsedInput.data.repo);
2684
+ const botLogin = botLoginFromEmail(botEmail)?.toLowerCase();
2685
+ if (!botLogin) {
2686
+ throw new Error("GitHub App bot identity is not configured.");
2687
+ }
2688
+ const query = `query ReviewThreadOwnership($threadId: ID!) {
2689
+ node(id: $threadId) {
2690
+ ... on PullRequestReviewThread {
2691
+ id
2692
+ isResolved
2693
+ pullRequest {
2694
+ number
2695
+ repository { nameWithOwner }
2696
+ author { login }
2697
+ }
2698
+ }
2699
+ }
2700
+ }`;
2701
+ const lookupResponse = await ctx.egress.fetch({
2702
+ provider: "github",
2703
+ operation: "github.pull.review-thread.get",
2704
+ request: new Request("https://api.github.com/graphql", {
2705
+ method: "POST",
2706
+ headers: { "Content-Type": "application/json" },
2707
+ body: JSON.stringify({
2708
+ operationName: "ReviewThreadOwnership",
2709
+ query,
2710
+ variables: { threadId: parsedInput.data.threadId }
2711
+ })
2712
+ })
2713
+ });
2714
+ const lookupPayload = await readJson7(lookupResponse);
2715
+ if (!lookupResponse.ok) {
2716
+ throw new Error(
2717
+ `GitHub review thread lookup failed with HTTP ${lookupResponse.status}: ${githubError(lookupPayload)}`
2718
+ );
2719
+ }
2720
+ const thread = z10.object({
2721
+ data: z10.object({
2722
+ node: z10.object({
2723
+ id: z10.string(),
2724
+ isResolved: z10.boolean(),
2725
+ pullRequest: z10.object({
2726
+ author: z10.object({ login: z10.string() }),
2727
+ number: z10.number(),
2728
+ repository: z10.object({ nameWithOwner: z10.string() })
2729
+ })
2730
+ }).nullable()
2731
+ })
2732
+ }).parse(lookupPayload).data.node;
2733
+ if (!thread) throw new Error("GitHub review thread was not found.");
2734
+ const pullRequest = thread.pullRequest;
2735
+ const ownsPullRequest = pullRequest.repository.nameWithOwner.toLowerCase() === repo.ref.toLowerCase() && pullRequest.author.login.toLowerCase() === botLogin;
2736
+ if (!ownsPullRequest) {
2737
+ throw new Error(
2738
+ "Junior can only resolve review threads on pull requests it authored."
2739
+ );
2740
+ }
2741
+ if (thread.isResolved) {
2742
+ return {
2743
+ target: "resolvePullRequestReviewThread",
2744
+ repo: repo.ref,
2745
+ number: pullRequest.number,
2746
+ threadId: thread.id,
2747
+ resolved: true
2748
+ };
2749
+ }
2750
+ const mutation = `mutation ResolveReviewThread($threadId: ID!) {
2751
+ resolveReviewThread(input: {threadId: $threadId}) {
2752
+ thread { id isResolved }
2753
+ }
2754
+ }`;
2755
+ const resolveResponse = await ctx.egress.fetch({
2756
+ provider: "github",
2757
+ operation: `github.pull.review-thread.resolve:${repo.ref.toLowerCase()}`,
2758
+ request: new Request("https://api.github.com/graphql", {
2759
+ method: "POST",
2760
+ headers: { "Content-Type": "application/json" },
2761
+ body: JSON.stringify({
2762
+ operationName: "ResolveReviewThread",
2763
+ query: mutation,
2764
+ variables: { threadId: thread.id }
2765
+ })
2766
+ })
2767
+ });
2768
+ const resolvePayload = await readJson7(resolveResponse);
2769
+ if (!resolveResponse.ok) {
2770
+ throw new Error(
2771
+ `GitHub review thread resolution failed with HTTP ${resolveResponse.status}: ${githubError(resolvePayload)}`
2772
+ );
2773
+ }
2774
+ const resolved = z10.object({
2775
+ data: z10.object({
2776
+ resolveReviewThread: z10.object({
2777
+ thread: z10.object({ id: z10.string(), isResolved: z10.boolean() })
2778
+ })
2779
+ })
2780
+ }).parse(resolvePayload).data.resolveReviewThread.thread;
2781
+ if (resolved.id !== thread.id || !resolved.isResolved) {
2782
+ throw new Error("GitHub did not resolve the requested review thread.");
2783
+ }
2784
+ return {
2785
+ target: "resolvePullRequestReviewThread",
2786
+ repo: repo.ref,
2787
+ number: pullRequest.number,
2788
+ threadId: resolved.id,
2789
+ resolved: true
2790
+ };
2791
+ }
2792
+ });
2793
+ }
2794
+
2553
2795
  // src/tools.ts
2554
- function createGitHubTools(ctx) {
2796
+ function createGitHubTools(ctx, botEmail) {
2555
2797
  return {
2556
2798
  cloneRepository: createGitHubCloneRepositoryTool(ctx),
2557
2799
  createIssue: createGitHubIssueTool(ctx),
@@ -2560,6 +2802,7 @@ function createGitHubTools(ctx) {
2560
2802
  getPullRequest: createGitHubGetPullRequestTool(ctx),
2561
2803
  getRelease: createGitHubGetReleaseTool(ctx),
2562
2804
  getRepository: createGitHubGetRepositoryTool(ctx),
2805
+ resolvePullRequestReviewThread: createGitHubResolvePullRequestReviewThreadTool(ctx, botEmail),
2563
2806
  updateIssue: createGitHubUpdateIssueTool(ctx),
2564
2807
  updatePullRequest: createGitHubUpdatePullRequestTool(ctx)
2565
2808
  };
@@ -2570,7 +2813,7 @@ import { createHmac, timingSafeEqual } from "crypto";
2570
2813
 
2571
2814
  // src/issue-outcomes/store.ts
2572
2815
  import { and, eq, lte, sql as sql2 } from "drizzle-orm";
2573
- import { z as z11 } from "zod";
2816
+ import { z as z12 } from "zod";
2574
2817
 
2575
2818
  // src/db/schema.ts
2576
2819
  import { sql } from "drizzle-orm";
@@ -2582,18 +2825,18 @@ import {
2582
2825
  text,
2583
2826
  timestamp
2584
2827
  } from "drizzle-orm/pg-core";
2585
- import { z as z10 } from "zod";
2586
- var githubPullRequestStateSchema = z10.enum([
2828
+ import { z as z11 } from "zod";
2829
+ var githubPullRequestStateSchema = z11.enum([
2587
2830
  "closed_unmerged",
2588
2831
  "merged",
2589
2832
  "open"
2590
2833
  ]);
2591
- var githubPullRequestCommitCompositionSchema = z10.enum([
2834
+ var githubPullRequestCommitCompositionSchema = z11.enum([
2592
2835
  "junior_only",
2593
2836
  "mixed"
2594
2837
  ]);
2595
- var githubIssueStateSchema = z10.enum(["closed", "open"]);
2596
- var githubIssueStateReasonSchema = z10.enum([
2838
+ var githubIssueStateSchema = z11.enum(["closed", "open"]);
2839
+ var githubIssueStateReasonSchema = z11.enum([
2597
2840
  "completed",
2598
2841
  "duplicate",
2599
2842
  "not_planned",
@@ -2663,21 +2906,21 @@ var juniorGitHubPullRequestIssues = pgTable(
2663
2906
  );
2664
2907
 
2665
2908
  // src/issue-outcomes/store.ts
2666
- var githubIssueOutcomeInputSchema = z11.object({
2667
- candidateOwned: z11.boolean(),
2668
- closedAt: z11.date().optional(),
2669
- issueId: z11.string().min(1),
2670
- number: z11.number().int().positive(),
2671
- openedAt: z11.date(),
2672
- repositoryFullName: z11.string().min(1),
2673
- repositoryId: z11.string().min(1),
2909
+ var githubIssueOutcomeInputSchema = z12.object({
2910
+ candidateOwned: z12.boolean(),
2911
+ closedAt: z12.date().optional(),
2912
+ issueId: z12.string().min(1),
2913
+ number: z12.number().int().positive(),
2914
+ openedAt: z12.date(),
2915
+ repositoryFullName: z12.string().min(1),
2916
+ repositoryId: z12.string().min(1),
2674
2917
  state: githubIssueStateSchema,
2675
2918
  stateReason: githubIssueStateReasonSchema.optional(),
2676
- updatedAt: z11.date()
2919
+ updatedAt: z12.date()
2677
2920
  }).strict();
2678
- var githubIssueConversationsInputSchema = z11.object({
2679
- conversationIds: z11.array(z11.string().min(1)).min(1),
2680
- issueId: z11.string().min(1)
2921
+ var githubIssueConversationsInputSchema = z12.object({
2922
+ conversationIds: z12.array(z12.string().min(1)).min(1),
2923
+ issueId: z12.string().min(1)
2681
2924
  }).strict();
2682
2925
  function projectionValues(input) {
2683
2926
  return {
@@ -2741,32 +2984,32 @@ async function recordGitHubIssueConversations(db, input) {
2741
2984
 
2742
2985
  // src/pull-request-outcomes/store.ts
2743
2986
  import { and as and2, eq as eq2, lte as lte2, ne, sql as sql3 } from "drizzle-orm";
2744
- import { z as z12 } from "zod";
2745
- var githubPullRequestOutcomeInputSchema = z12.object({
2746
- candidateOwned: z12.boolean(),
2747
- closedAt: z12.date().optional(),
2987
+ import { z as z13 } from "zod";
2988
+ var githubPullRequestOutcomeInputSchema = z13.object({
2989
+ candidateOwned: z13.boolean(),
2990
+ closedAt: z13.date().optional(),
2748
2991
  commitComposition: githubPullRequestCommitCompositionSchema.optional(),
2749
- mergedAt: z12.date().optional(),
2750
- number: z12.number().int().positive(),
2751
- openedAt: z12.date(),
2752
- pullRequestId: z12.string().min(1),
2753
- repositoryFullName: z12.string().min(1),
2754
- repositoryId: z12.string().min(1),
2992
+ mergedAt: z13.date().optional(),
2993
+ number: z13.number().int().positive(),
2994
+ openedAt: z13.date(),
2995
+ pullRequestId: z13.string().min(1),
2996
+ repositoryFullName: z13.string().min(1),
2997
+ repositoryId: z13.string().min(1),
2755
2998
  state: githubPullRequestStateSchema,
2756
- updatedAt: z12.date()
2999
+ updatedAt: z13.date()
2757
3000
  }).strict();
2758
- var githubPullRequestConversationsInputSchema = z12.object({
2759
- conversationIds: z12.array(z12.string().min(1)).min(1),
2760
- pullRequestId: z12.string().min(1)
3001
+ var githubPullRequestConversationsInputSchema = z13.object({
3002
+ conversationIds: z13.array(z13.string().min(1)).min(1),
3003
+ pullRequestId: z13.string().min(1)
2761
3004
  }).strict();
2762
- var githubPullRequestLinkedIssuesInputSchema = z12.object({
2763
- linkedIssues: z12.array(
2764
- z12.object({
2765
- number: z12.number().int().positive(),
2766
- repositoryFullName: z12.string().min(1)
3005
+ var githubPullRequestLinkedIssuesInputSchema = z13.object({
3006
+ linkedIssues: z13.array(
3007
+ z13.object({
3008
+ number: z13.number().int().positive(),
3009
+ repositoryFullName: z13.string().min(1)
2767
3010
  }).strict()
2768
3011
  ).min(1),
2769
- pullRequestId: z12.string().min(1)
3012
+ pullRequestId: z13.string().min(1)
2770
3013
  }).strict();
2771
3014
  function projectionValues2(input) {
2772
3015
  return {
@@ -2931,55 +3174,39 @@ async function recordGitHubPullRequestLinkedIssues(db, input) {
2931
3174
  }
2932
3175
 
2933
3176
  // src/webhooks/issue-outcome.ts
2934
- import { z as z13 } from "zod";
2935
-
2936
- // src/webhooks/ownership.ts
2937
- var GITHUB_NOREPLY_DOMAIN = "users.noreply.github.com";
2938
- function botLoginFromEmail(value) {
2939
- const email = value?.trim();
2940
- if (!email) return void 0;
2941
- const separator = email.lastIndexOf("@");
2942
- if (separator <= 0) return void 0;
2943
- const domain = email.slice(separator + 1).toLowerCase();
2944
- if (domain !== GITHUB_NOREPLY_DOMAIN) return void 0;
2945
- const localPart = email.slice(0, separator);
2946
- const login = localPart.slice(localPart.indexOf("+") + 1).trim();
2947
- return login.toLowerCase().endsWith("[bot]") ? login : void 0;
2948
- }
2949
-
2950
- // src/webhooks/issue-outcome.ts
2951
- var canonicalIssueOutcomeSchema = z13.object({
2952
- action: z13.enum(["opened", "closed", "reopened"]),
2953
- issue: z13.object({
2954
- body: z13.string().nullable().optional(),
2955
- closed_at: z13.string().nullable().optional(),
2956
- created_at: z13.string(),
2957
- id: z13.number().int().positive(),
2958
- number: z13.number().int().positive(),
3177
+ import { z as z14 } from "zod";
3178
+ var canonicalIssueOutcomeSchema = z14.object({
3179
+ action: z14.enum(["opened", "closed", "reopened"]),
3180
+ issue: z14.object({
3181
+ body: z14.string().nullable().optional(),
3182
+ closed_at: z14.string().nullable().optional(),
3183
+ created_at: z14.string(),
3184
+ id: z14.number().int().positive(),
3185
+ number: z14.number().int().positive(),
2959
3186
  state_reason: githubIssueStateReasonSchema.nullable().optional(),
2960
- updated_at: z13.string(),
2961
- user: z13.object({ login: z13.string().min(1) }).strict()
3187
+ updated_at: z14.string(),
3188
+ user: z14.object({ login: z14.string().min(1) }).strict()
2962
3189
  }).strict(),
2963
- repository: z13.object({
2964
- full_name: z13.string().min(1),
2965
- id: z13.number().int().positive()
3190
+ repository: z14.object({
3191
+ full_name: z14.string().min(1),
3192
+ id: z14.number().int().positive()
2966
3193
  }).strict()
2967
3194
  }).strict();
2968
- var issueOutcomeSchema = z13.object({
2969
- action: z13.enum(["opened", "closed", "reopened"]),
2970
- issue: z13.object({
2971
- body: z13.string().nullable().optional(),
2972
- closed_at: z13.string().nullable().optional(),
2973
- created_at: z13.string(),
2974
- id: z13.number().int().positive(),
2975
- number: z13.number().int().positive(),
3195
+ var issueOutcomeSchema = z14.object({
3196
+ action: z14.enum(["opened", "closed", "reopened"]),
3197
+ issue: z14.object({
3198
+ body: z14.string().nullable().optional(),
3199
+ closed_at: z14.string().nullable().optional(),
3200
+ created_at: z14.string(),
3201
+ id: z14.number().int().positive(),
3202
+ number: z14.number().int().positive(),
2976
3203
  state_reason: githubIssueStateReasonSchema.nullable().optional(),
2977
- updated_at: z13.string(),
2978
- user: z13.object({ login: z13.string().min(1) }).passthrough()
3204
+ updated_at: z14.string(),
3205
+ user: z14.object({ login: z14.string().min(1) }).passthrough()
2979
3206
  }).passthrough(),
2980
- repository: z13.object({
2981
- full_name: z13.string().min(1),
2982
- id: z13.number().int().positive()
3207
+ repository: z14.object({
3208
+ full_name: z14.string().min(1),
3209
+ id: z14.number().int().positive()
2983
3210
  }).passthrough()
2984
3211
  }).passthrough().transform(
2985
3212
  (provider) => canonicalIssueOutcomeSchema.parse({
@@ -3000,7 +3227,7 @@ var issueOutcomeSchema = z13.object({
3000
3227
  }
3001
3228
  })
3002
3229
  );
3003
- var issueLifecycleActionSchema = z13.object({ action: z13.string() }).passthrough();
3230
+ var issueLifecycleActionSchema = z14.object({ action: z14.string() }).passthrough();
3004
3231
  function timestamp2(value) {
3005
3232
  if (!value) return void 0;
3006
3233
  const parsed = new Date(value);
@@ -3046,21 +3273,21 @@ function normalizeGitHubIssueOutcome(args) {
3046
3273
  updatedAt
3047
3274
  };
3048
3275
  }
3049
- var canonicalIssueConversationSchema = z13.object({
3050
- issue: z13.object({
3051
- body: z13.string().nullable().optional(),
3052
- id: z13.number().int().positive(),
3053
- user: z13.object({ login: z13.string().min(1) }).strict()
3276
+ var canonicalIssueConversationSchema = z14.object({
3277
+ issue: z14.object({
3278
+ body: z14.string().nullable().optional(),
3279
+ id: z14.number().int().positive(),
3280
+ user: z14.object({ login: z14.string().min(1) }).strict()
3054
3281
  }).strict(),
3055
- sender: z13.object({ login: z13.string().min(1) }).strict().optional()
3282
+ sender: z14.object({ login: z14.string().min(1) }).strict().optional()
3056
3283
  }).strict();
3057
- var issueConversationSchema = z13.object({
3058
- issue: z13.object({
3059
- body: z13.string().nullable().optional(),
3060
- id: z13.number().int().positive(),
3061
- user: z13.object({ login: z13.string().min(1) }).passthrough()
3284
+ var issueConversationSchema = z14.object({
3285
+ issue: z14.object({
3286
+ body: z14.string().nullable().optional(),
3287
+ id: z14.number().int().positive(),
3288
+ user: z14.object({ login: z14.string().min(1) }).passthrough()
3062
3289
  }).passthrough(),
3063
- sender: z13.object({ login: z13.string().min(1) }).passthrough().optional()
3290
+ sender: z14.object({ login: z14.string().min(1) }).passthrough().optional()
3064
3291
  }).passthrough().transform(
3065
3292
  (provider) => canonicalIssueConversationSchema.parse({
3066
3293
  issue: {
@@ -3087,41 +3314,41 @@ function normalizeGitHubIssueConversations(args) {
3087
3314
  }
3088
3315
 
3089
3316
  // src/webhooks/pull-request-outcome.ts
3090
- import { z as z14 } from "zod";
3091
- var canonicalPullRequestOutcomeSchema = z14.object({
3092
- action: z14.enum(["opened", "closed", "reopened"]),
3093
- pull_request: z14.object({
3094
- body: z14.string().nullable().optional(),
3095
- closed_at: z14.string().nullable().optional(),
3096
- created_at: z14.string(),
3097
- id: z14.number().int().positive(),
3098
- merged: z14.boolean(),
3099
- merged_at: z14.string().nullable().optional(),
3100
- number: z14.number().int().positive(),
3101
- updated_at: z14.string(),
3102
- user: z14.object({ login: z14.string().min(1) }).strict()
3317
+ import { z as z15 } from "zod";
3318
+ var canonicalPullRequestOutcomeSchema = z15.object({
3319
+ action: z15.enum(["opened", "closed", "reopened"]),
3320
+ pull_request: z15.object({
3321
+ body: z15.string().nullable().optional(),
3322
+ closed_at: z15.string().nullable().optional(),
3323
+ created_at: z15.string(),
3324
+ id: z15.number().int().positive(),
3325
+ merged: z15.boolean(),
3326
+ merged_at: z15.string().nullable().optional(),
3327
+ number: z15.number().int().positive(),
3328
+ updated_at: z15.string(),
3329
+ user: z15.object({ login: z15.string().min(1) }).strict()
3103
3330
  }).strict(),
3104
- repository: z14.object({
3105
- full_name: z14.string().min(1),
3106
- id: z14.number().int().positive()
3331
+ repository: z15.object({
3332
+ full_name: z15.string().min(1),
3333
+ id: z15.number().int().positive()
3107
3334
  }).strict()
3108
3335
  }).strict();
3109
- var pullRequestOutcomeSchema = z14.object({
3110
- action: z14.enum(["opened", "closed", "reopened"]),
3111
- pull_request: z14.object({
3112
- body: z14.string().nullable().optional(),
3113
- closed_at: z14.string().nullable().optional(),
3114
- created_at: z14.string(),
3115
- id: z14.number().int().positive(),
3116
- merged: z14.boolean(),
3117
- merged_at: z14.string().nullable().optional(),
3118
- number: z14.number().int().positive(),
3119
- updated_at: z14.string(),
3120
- user: z14.object({ login: z14.string().min(1) }).passthrough()
3336
+ var pullRequestOutcomeSchema = z15.object({
3337
+ action: z15.enum(["opened", "closed", "reopened"]),
3338
+ pull_request: z15.object({
3339
+ body: z15.string().nullable().optional(),
3340
+ closed_at: z15.string().nullable().optional(),
3341
+ created_at: z15.string(),
3342
+ id: z15.number().int().positive(),
3343
+ merged: z15.boolean(),
3344
+ merged_at: z15.string().nullable().optional(),
3345
+ number: z15.number().int().positive(),
3346
+ updated_at: z15.string(),
3347
+ user: z15.object({ login: z15.string().min(1) }).passthrough()
3121
3348
  }).passthrough(),
3122
- repository: z14.object({
3123
- full_name: z14.string().min(1),
3124
- id: z14.number().int().positive()
3349
+ repository: z15.object({
3350
+ full_name: z15.string().min(1),
3351
+ id: z15.number().int().positive()
3125
3352
  }).passthrough()
3126
3353
  }).passthrough().transform(
3127
3354
  (provider) => canonicalPullRequestOutcomeSchema.parse({
@@ -3143,24 +3370,24 @@ var pullRequestOutcomeSchema = z14.object({
3143
3370
  }
3144
3371
  })
3145
3372
  );
3146
- var pullRequestLifecycleActionSchema = z14.object({ action: z14.string() }).passthrough();
3147
- var canonicalPullRequestConversationSchema = z14.object({
3148
- pull_request: z14.object({
3149
- body: z14.string().nullable().optional(),
3150
- id: z14.number().int().positive(),
3151
- user: z14.object({ login: z14.string().min(1) }).strict()
3373
+ var pullRequestLifecycleActionSchema = z15.object({ action: z15.string() }).passthrough();
3374
+ var canonicalPullRequestConversationSchema = z15.object({
3375
+ pull_request: z15.object({
3376
+ body: z15.string().nullable().optional(),
3377
+ id: z15.number().int().positive(),
3378
+ user: z15.object({ login: z15.string().min(1) }).strict()
3152
3379
  }).strict(),
3153
- repository: z14.object({ full_name: z14.string().min(1) }).strict(),
3154
- sender: z14.object({ login: z14.string().min(1) }).strict()
3380
+ repository: z15.object({ full_name: z15.string().min(1) }).strict(),
3381
+ sender: z15.object({ login: z15.string().min(1) }).strict()
3155
3382
  }).strict();
3156
- var pullRequestConversationSchema = z14.object({
3157
- pull_request: z14.object({
3158
- body: z14.string().nullable().optional(),
3159
- id: z14.number().int().positive(),
3160
- user: z14.object({ login: z14.string().min(1) }).passthrough()
3383
+ var pullRequestConversationSchema = z15.object({
3384
+ pull_request: z15.object({
3385
+ body: z15.string().nullable().optional(),
3386
+ id: z15.number().int().positive(),
3387
+ user: z15.object({ login: z15.string().min(1) }).passthrough()
3161
3388
  }).passthrough(),
3162
- repository: z14.object({ full_name: z14.string().min(1) }).passthrough(),
3163
- sender: z14.object({ login: z14.string().min(1) }).passthrough()
3389
+ repository: z15.object({ full_name: z15.string().min(1) }).passthrough(),
3390
+ sender: z15.object({ login: z15.string().min(1) }).passthrough()
3164
3391
  }).passthrough().transform(
3165
3392
  (provider) => canonicalPullRequestConversationSchema.parse({
3166
3393
  pull_request: {
@@ -3400,14 +3627,14 @@ function createGitHubWebhookRoute(args) {
3400
3627
 
3401
3628
  // src/outcomes/profile-report.ts
3402
3629
  import { sql as sql4 } from "drizzle-orm";
3403
- import { z as z15 } from "zod";
3630
+ import { z as z16 } from "zod";
3404
3631
  var DAY_MS = 24 * 60 * 60 * 1e3;
3405
3632
  var WINDOWS = [7, 30, 90];
3406
- var pullRequestStatsSchema = z15.object({
3407
- closed: z15.number().int().nonnegative(),
3408
- created: z15.number().int().nonnegative(),
3409
- days: z15.number().int().positive(),
3410
- merged: z15.number().int().nonnegative()
3633
+ var pullRequestStatsSchema = z16.object({
3634
+ closed: z16.number().int().nonnegative(),
3635
+ created: z16.number().int().nonnegative(),
3636
+ days: z16.number().int().positive(),
3637
+ merged: z16.number().int().nonnegative()
3411
3638
  }).strict().transform((row) => {
3412
3639
  const terminal = row.merged + row.closed;
3413
3640
  return {
@@ -3415,13 +3642,13 @@ var pullRequestStatsSchema = z15.object({
3415
3642
  mergeRate: terminal > 0 ? row.merged / terminal : void 0
3416
3643
  };
3417
3644
  });
3418
- var issueStatsSchema = z15.object({
3419
- created: z15.number().int().nonnegative(),
3420
- days: z15.number().int().positive()
3645
+ var issueStatsSchema = z16.object({
3646
+ created: z16.number().int().nonnegative(),
3647
+ days: z16.number().int().positive()
3421
3648
  }).strict();
3422
- var daySchema = z15.object({
3423
- created: z15.number().int().nonnegative(),
3424
- date: z15.string().date()
3649
+ var daySchema = z16.object({
3650
+ created: z16.number().int().nonnegative(),
3651
+ date: z16.string().date()
3425
3652
  }).strict();
3426
3653
  function queryRows(result) {
3427
3654
  if (typeof result !== "object" || result === null || !("rows" in result) || !Array.isArray(result.rows)) {
@@ -3494,7 +3721,7 @@ async function aggregatePullRequestWindows(args) {
3494
3721
  GROUP BY windows.days
3495
3722
  ORDER BY windows.days
3496
3723
  `);
3497
- return z15.array(pullRequestStatsSchema).parse(queryRows(result));
3724
+ return z16.array(pullRequestStatsSchema).parse(queryRows(result));
3498
3725
  }
3499
3726
  async function aggregateIssueWindows(args) {
3500
3727
  const starts = WINDOWS.map(
@@ -3527,7 +3754,7 @@ async function aggregateIssueWindows(args) {
3527
3754
  GROUP BY windows.days
3528
3755
  ORDER BY windows.days
3529
3756
  `);
3530
- return z15.array(issueStatsSchema).parse(queryRows(result));
3757
+ return z16.array(issueStatsSchema).parse(queryRows(result));
3531
3758
  }
3532
3759
  async function aggregateOpenedDays(args) {
3533
3760
  const end = new Date(args.nowMs);
@@ -3557,7 +3784,7 @@ async function aggregateOpenedDays(args) {
3557
3784
  LEFT JOIN daily ON daily.day = days.day
3558
3785
  ORDER BY days.day
3559
3786
  `);
3560
- return z15.array(daySchema).parse(queryRows(result));
3787
+ return z16.array(daySchema).parse(queryRows(result));
3561
3788
  }
3562
3789
  async function buildGitHubProfileReport(args) {
3563
3790
  const [windows, pullRequestDays, issueWindows, issueDays] = await Promise.all(
@@ -3638,18 +3865,18 @@ async function buildGitHubProfileReport(args) {
3638
3865
 
3639
3866
  // src/outcomes/report.ts
3640
3867
  import { sql as sql6 } from "drizzle-orm";
3641
- import { z as z17 } from "zod";
3868
+ import { z as z18 } from "zod";
3642
3869
 
3643
3870
  // src/outcomes/cost.ts
3644
3871
  import { sql as sql5 } from "drizzle-orm";
3645
- import { z as z16 } from "zod";
3872
+ import { z as z17 } from "zod";
3646
3873
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
3647
- var costWindowSchema = z16.object({
3648
- days: z16.number().int().positive(),
3649
- issueCostUsd: z16.number().nonnegative().nullable(),
3650
- medianIssueCostUsd: z16.number().nonnegative().nullable(),
3651
- medianPullRequestCostUsd: z16.number().nonnegative().nullable(),
3652
- pullRequestCostUsd: z16.number().nonnegative().nullable()
3874
+ var costWindowSchema = z17.object({
3875
+ days: z17.number().int().positive(),
3876
+ issueCostUsd: z17.number().nonnegative().nullable(),
3877
+ medianIssueCostUsd: z17.number().nonnegative().nullable(),
3878
+ medianPullRequestCostUsd: z17.number().nonnegative().nullable(),
3879
+ pullRequestCostUsd: z17.number().nonnegative().nullable()
3653
3880
  }).strict().transform((row) => ({
3654
3881
  days: row.days,
3655
3882
  issueCostUsd: row.issueCostUsd ?? void 0,
@@ -3657,12 +3884,12 @@ var costWindowSchema = z16.object({
3657
3884
  medianPullRequestCostUsd: row.medianPullRequestCostUsd ?? void 0,
3658
3885
  pullRequestCostUsd: row.pullRequestCostUsd ?? void 0
3659
3886
  }));
3660
- var repositoryCostSchema = z16.object({
3661
- issueCostUsd: z16.number().nonnegative().nullable(),
3662
- medianIssueCostUsd: z16.number().nonnegative().nullable(),
3663
- medianPullRequestCostUsd: z16.number().nonnegative().nullable(),
3664
- pullRequestCostUsd: z16.number().nonnegative().nullable(),
3665
- repository: z16.string().min(1)
3887
+ var repositoryCostSchema = z17.object({
3888
+ issueCostUsd: z17.number().nonnegative().nullable(),
3889
+ medianIssueCostUsd: z17.number().nonnegative().nullable(),
3890
+ medianPullRequestCostUsd: z17.number().nonnegative().nullable(),
3891
+ pullRequestCostUsd: z17.number().nonnegative().nullable(),
3892
+ repository: z17.string().min(1)
3666
3893
  }).strict().transform((row) => ({
3667
3894
  issueCostUsd: row.issueCostUsd ?? void 0,
3668
3895
  medianIssueCostUsd: row.medianIssueCostUsd ?? void 0,
@@ -3867,7 +4094,7 @@ async function aggregateGitHubCostWindows(args) {
3867
4094
  INNER JOIN issue_window ON issue_window.days = pull_request_window.days
3868
4095
  ORDER BY pull_request_window.days
3869
4096
  `);
3870
- return z16.array(costWindowSchema).parse(queryRows2(result));
4097
+ return z17.array(costWindowSchema).parse(queryRows2(result));
3871
4098
  }
3872
4099
  async function aggregateGitHubRepositoryCosts(args) {
3873
4100
  if (!await hasConversationUsageTable(args.db)) {
@@ -3965,7 +4192,7 @@ async function aggregateGitHubRepositoryCosts(args) {
3965
4192
  ON issue_totals.repository = repositories.repository
3966
4193
  ORDER BY "repository" ASC
3967
4194
  `);
3968
- return z16.array(repositoryCostSchema).parse(queryRows2(result));
4195
+ return z17.array(repositoryCostSchema).parse(queryRows2(result));
3969
4196
  }
3970
4197
  function formatCostUsd(value) {
3971
4198
  if (value === void 0) return "\u2014";
@@ -3980,12 +4207,12 @@ function formatCostUsd(value) {
3980
4207
  // src/outcomes/report.ts
3981
4208
  var DAY_MS3 = 24 * 60 * 60 * 1e3;
3982
4209
  var WINDOWS2 = [7, 30, 90];
3983
- var pullRequestStatsSchema2 = z17.object({
3984
- closed: z17.number().int().nonnegative(),
3985
- created: z17.number().int().nonnegative(),
3986
- days: z17.number().int().positive(),
3987
- medianMergeTimeMs: z17.number().nonnegative().nullable(),
3988
- merged: z17.number().int().nonnegative()
4210
+ var pullRequestStatsSchema2 = z18.object({
4211
+ closed: z18.number().int().nonnegative(),
4212
+ created: z18.number().int().nonnegative(),
4213
+ days: z18.number().int().positive(),
4214
+ medianMergeTimeMs: z18.number().nonnegative().nullable(),
4215
+ merged: z18.number().int().nonnegative()
3989
4216
  }).strict().transform((row) => {
3990
4217
  const terminal = row.merged + row.closed;
3991
4218
  return {
@@ -3994,12 +4221,12 @@ var pullRequestStatsSchema2 = z17.object({
3994
4221
  mergeRate: terminal > 0 ? row.merged / terminal : void 0
3995
4222
  };
3996
4223
  });
3997
- var pullRequestRepositoryStatsSchema = z17.object({
3998
- closed: z17.number().int().nonnegative(),
3999
- created: z17.number().int().nonnegative(),
4000
- juniorOnly: z17.number().int().nonnegative(),
4001
- merged: z17.number().int().nonnegative(),
4002
- repository: z17.string().min(1)
4224
+ var pullRequestRepositoryStatsSchema = z18.object({
4225
+ closed: z18.number().int().nonnegative(),
4226
+ created: z18.number().int().nonnegative(),
4227
+ juniorOnly: z18.number().int().nonnegative(),
4228
+ merged: z18.number().int().nonnegative(),
4229
+ repository: z18.string().min(1)
4003
4230
  }).strict().transform((row) => {
4004
4231
  const terminal = row.merged + row.closed;
4005
4232
  return {
@@ -4007,33 +4234,33 @@ var pullRequestRepositoryStatsSchema = z17.object({
4007
4234
  mergeRate: terminal > 0 ? row.merged / terminal : void 0
4008
4235
  };
4009
4236
  });
4010
- var issueStatsSchema2 = z17.object({
4011
- closedCompleted: z17.number().int().nonnegative(),
4012
- closedDuplicate: z17.number().int().nonnegative(),
4013
- closedNotPlanned: z17.number().int().nonnegative(),
4014
- closedUnknown: z17.number().int().nonnegative(),
4015
- created: z17.number().int().nonnegative(),
4016
- days: z17.number().int().positive(),
4017
- medianCloseTimeMs: z17.number().nonnegative().nullable()
4237
+ var issueStatsSchema2 = z18.object({
4238
+ closedCompleted: z18.number().int().nonnegative(),
4239
+ closedDuplicate: z18.number().int().nonnegative(),
4240
+ closedNotPlanned: z18.number().int().nonnegative(),
4241
+ closedUnknown: z18.number().int().nonnegative(),
4242
+ created: z18.number().int().nonnegative(),
4243
+ days: z18.number().int().positive(),
4244
+ medianCloseTimeMs: z18.number().nonnegative().nullable()
4018
4245
  }).strict().transform((row) => ({
4019
4246
  ...row,
4020
4247
  medianCloseTimeMs: row.medianCloseTimeMs ?? void 0
4021
4248
  }));
4022
- var pullRequestDaySchema = z17.object({
4023
- created: z17.number().int().nonnegative(),
4024
- date: z17.string().date()
4249
+ var pullRequestDaySchema = z18.object({
4250
+ created: z18.number().int().nonnegative(),
4251
+ date: z18.string().date()
4025
4252
  }).strict();
4026
- var issueDaySchema = z17.object({
4027
- created: z17.number().int().nonnegative(),
4028
- date: z17.string().date()
4253
+ var issueDaySchema = z18.object({
4254
+ created: z18.number().int().nonnegative(),
4255
+ date: z18.string().date()
4029
4256
  }).strict();
4030
- var issueRepositoryStatsSchema = z17.object({
4031
- closedCompleted: z17.number().int().nonnegative(),
4032
- closedDuplicate: z17.number().int().nonnegative(),
4033
- closedNotPlanned: z17.number().int().nonnegative(),
4034
- closedUnknown: z17.number().int().nonnegative(),
4035
- created: z17.number().int().nonnegative(),
4036
- repository: z17.string().min(1)
4257
+ var issueRepositoryStatsSchema = z18.object({
4258
+ closedCompleted: z18.number().int().nonnegative(),
4259
+ closedDuplicate: z18.number().int().nonnegative(),
4260
+ closedNotPlanned: z18.number().int().nonnegative(),
4261
+ closedUnknown: z18.number().int().nonnegative(),
4262
+ created: z18.number().int().nonnegative(),
4263
+ repository: z18.string().min(1)
4037
4264
  }).strict();
4038
4265
  function queryRows3(result) {
4039
4266
  if (typeof result !== "object" || result === null || !("rows" in result) || !Array.isArray(result.rows)) {
@@ -4097,7 +4324,7 @@ async function aggregatePullRequestWindows2(args) {
4097
4324
  GROUP BY windows.days
4098
4325
  ORDER BY windows.days
4099
4326
  `);
4100
- return z17.array(pullRequestStatsSchema2).parse(queryRows3(result));
4327
+ return z18.array(pullRequestStatsSchema2).parse(queryRows3(result));
4101
4328
  }
4102
4329
  async function aggregatePullRequestDays(args) {
4103
4330
  const end = new Date(args.nowMs);
@@ -4125,7 +4352,7 @@ async function aggregatePullRequestDays(args) {
4125
4352
  LEFT JOIN daily ON daily.day = days.day
4126
4353
  ORDER BY days.day
4127
4354
  `);
4128
- return z17.array(pullRequestDaySchema).parse(queryRows3(result));
4355
+ return z18.array(pullRequestDaySchema).parse(queryRows3(result));
4129
4356
  }
4130
4357
  async function aggregatePullRequestRepositories(args) {
4131
4358
  const start = new Date(args.nowMs - 30 * DAY_MS3);
@@ -4155,7 +4382,7 @@ async function aggregatePullRequestRepositories(args) {
4155
4382
  ORDER BY "merged" DESC, "created" DESC, "repository" ASC
4156
4383
  LIMIT 25
4157
4384
  `);
4158
- return z17.array(pullRequestRepositoryStatsSchema).parse(queryRows3(result));
4385
+ return z18.array(pullRequestRepositoryStatsSchema).parse(queryRows3(result));
4159
4386
  }
4160
4387
  async function aggregateIssueWindows2(args) {
4161
4388
  const starts = WINDOWS2.map(
@@ -4224,7 +4451,7 @@ async function aggregateIssueWindows2(args) {
4224
4451
  GROUP BY windows.days
4225
4452
  ORDER BY windows.days
4226
4453
  `);
4227
- return z17.array(issueStatsSchema2).parse(queryRows3(result));
4454
+ return z18.array(issueStatsSchema2).parse(queryRows3(result));
4228
4455
  }
4229
4456
  async function aggregateIssueDays(args) {
4230
4457
  const end = new Date(args.nowMs);
@@ -4252,7 +4479,7 @@ async function aggregateIssueDays(args) {
4252
4479
  LEFT JOIN daily ON daily.day = days.day
4253
4480
  ORDER BY days.day
4254
4481
  `);
4255
- return z17.array(issueDaySchema).parse(queryRows3(result));
4482
+ return z18.array(issueDaySchema).parse(queryRows3(result));
4256
4483
  }
4257
4484
  async function aggregateIssueRepositories(args) {
4258
4485
  const start = new Date(args.nowMs - 30 * DAY_MS3);
@@ -4289,7 +4516,7 @@ async function aggregateIssueRepositories(args) {
4289
4516
  ORDER BY "created" DESC, "closedCompleted" DESC, "repository" ASC
4290
4517
  LIMIT 25
4291
4518
  `);
4292
- return z17.array(issueRepositoryStatsSchema).parse(queryRows3(result));
4519
+ return z18.array(issueRepositoryStatsSchema).parse(queryRows3(result));
4293
4520
  }
4294
4521
  function formatPercent2(value) {
4295
4522
  return value === void 0 ? "\u2014" : `${Math.round(value * 100)}%`;
@@ -4453,18 +4680,18 @@ async function buildGitHubOutcomeReport(args) {
4453
4680
  }
4454
4681
 
4455
4682
  // src/pull-request-outcomes/commit-composition.ts
4456
- import { z as z18 } from "zod";
4457
- var canonicalCommitSchema = z18.object({
4458
- authorEmail: z18.string().nullable(),
4459
- authorLogin: z18.string().nullable()
4683
+ import { z as z19 } from "zod";
4684
+ var canonicalCommitSchema = z19.object({
4685
+ authorEmail: z19.string().nullable(),
4686
+ authorLogin: z19.string().nullable()
4460
4687
  }).strict();
4461
- var providerCommitSchema = z18.object({
4462
- author: z18.object({ login: z18.string() }).passthrough().nullable(),
4463
- commit: z18.object({
4464
- author: z18.object({ email: z18.string() }).passthrough().nullable()
4688
+ var providerCommitSchema = z19.object({
4689
+ author: z19.object({ login: z19.string() }).passthrough().nullable(),
4690
+ commit: z19.object({
4691
+ author: z19.object({ email: z19.string() }).passthrough().nullable()
4465
4692
  }).passthrough()
4466
4693
  }).passthrough();
4467
- var commitPageSchema = z18.array(providerCommitSchema).transform(
4694
+ var commitPageSchema = z19.array(providerCommitSchema).transform(
4468
4695
  (commits) => commits.map(
4469
4696
  (commit) => canonicalCommitSchema.parse({
4470
4697
  authorEmail: commit.commit.author?.email ?? null,
@@ -4911,6 +5138,63 @@ function linkifyGitHubReferences(text2) {
4911
5138
  }).join("\n");
4912
5139
  }
4913
5140
 
5141
+ // src/workspace-prepare.ts
5142
+ async function prepareWorkspace(ctx) {
5143
+ const repos = ctx.repos.map((entry) => {
5144
+ const [owner, name, ...rest] = entry.repo.split("/");
5145
+ if (!owner || !name || rest.length > 0) {
5146
+ throw new Error(`Invalid GitHub repository: ${entry.repo}`);
5147
+ }
5148
+ const segments = entry.path.split("/");
5149
+ if (segments.length === 0 || segments.some(
5150
+ (part) => !part || part === "." || part === ".." || !/^[A-Za-z0-9._-]+$/.test(part)
5151
+ ) || isReservedSandboxDirectory(segments[0])) {
5152
+ throw new Error(`Invalid workspace checkout path: ${entry.path}`);
5153
+ }
5154
+ return { owner, name, path: entry.path, repo: entry.repo };
5155
+ });
5156
+ const paths = /* @__PURE__ */ new Set();
5157
+ for (const entry of repos) {
5158
+ const key = entry.path.toLowerCase();
5159
+ if (paths.has(key)) {
5160
+ throw new Error(`Workspace checkout path collision: ${entry.path}`);
5161
+ }
5162
+ paths.add(key);
5163
+ }
5164
+ for (const { owner, name, path, repo } of repos) {
5165
+ const parent = path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : void 0;
5166
+ if (parent) {
5167
+ const mkdir = await ctx.sandbox.run({
5168
+ cmd: "mkdir",
5169
+ args: ["-p", "--", parent],
5170
+ cwd: ctx.sandbox.root
5171
+ });
5172
+ if (mkdir.exitCode !== 0) {
5173
+ throw new Error(
5174
+ `GitHub workspace checkout parent failed for ${repo}: ${mkdir.stderr.trim() || `exit ${mkdir.exitCode}`}`
5175
+ );
5176
+ }
5177
+ }
5178
+ const result = await ctx.sandbox.run({
5179
+ cmd: "git",
5180
+ args: [
5181
+ "clone",
5182
+ "--quiet",
5183
+ "--depth=1",
5184
+ "--",
5185
+ `https://github.com/${owner}/${name}.git`,
5186
+ path
5187
+ ],
5188
+ cwd: ctx.sandbox.root
5189
+ });
5190
+ if (result.exitCode !== 0) {
5191
+ throw new Error(
5192
+ `GitHub workspace clone failed for ${repo}: ${result.stderr.trim() || `exit ${result.exitCode}`}`
5193
+ );
5194
+ }
5195
+ }
5196
+ }
5197
+
4914
5198
  // src/plugin.ts
4915
5199
  function githubSmartHttpAccess(upstreamUrl) {
4916
5200
  const pathname = upstreamUrl.pathname.toLowerCase();
@@ -5142,6 +5426,28 @@ function githubApiWriteGrantName(method, upstreamUrl) {
5142
5426
  }
5143
5427
  return void 0;
5144
5428
  }
5429
+ function reviewThreadResolveRepository(operation, method, upstreamUrl, bodyText) {
5430
+ const prefix = "github.pull.review-thread.resolve:";
5431
+ if (method !== "POST" || !isGitHubGraphqlUrl(upstreamUrl) || !operation?.startsWith(prefix)) {
5432
+ return void 0;
5433
+ }
5434
+ const repository = operation.slice(prefix.length);
5435
+ if (!/^[^/]+\/[^/]+$/.test(repository)) return void 0;
5436
+ const parsed = parseGitHubGraphqlRequest(bodyText);
5437
+ if (parsed?.operationName !== "ResolveReviewThread" || !/\bmutation\s+ResolveReviewThread\b/.test(parsed.normalized) || !/\bresolveReviewThread\b/.test(parsed.normalized)) {
5438
+ return void 0;
5439
+ }
5440
+ return repository;
5441
+ }
5442
+ function repositoryLeaseScopeFromRef(repository) {
5443
+ const [owner, name] = repository.split("/");
5444
+ if (!owner || !name) {
5445
+ throw new EgressPolicyDenied2(
5446
+ "GitHub review thread resolution does not identify a target repository."
5447
+ );
5448
+ }
5449
+ return githubRepositoryLeaseScope({ owner, name });
5450
+ }
5145
5451
  function isGitHubGraphqlMutation(method, upstreamUrl, bodyText, field) {
5146
5452
  if (method !== "POST" || !isGitHubGraphqlUrl(upstreamUrl)) return false;
5147
5453
  const parsed = parseGitHubGraphqlRequest(bodyText);
@@ -5233,6 +5539,20 @@ async function githubGrantForEgress(ctx) {
5233
5539
  repositoryLeaseScope(upstreamUrl)
5234
5540
  );
5235
5541
  }
5542
+ const reviewThreadRepository = reviewThreadResolveRepository(
5543
+ ctx.request.operation,
5544
+ method,
5545
+ upstreamUrl,
5546
+ ctx.request.bodyText
5547
+ );
5548
+ if (reviewThreadRepository) {
5549
+ return grantForAccess(
5550
+ "write",
5551
+ "github.installation-write",
5552
+ "installation-write",
5553
+ repositoryLeaseScopeFromRef(reviewThreadRepository)
5554
+ );
5555
+ }
5236
5556
  const graphqlAccess = githubGraphqlAccess(
5237
5557
  method,
5238
5558
  upstreamUrl,
@@ -5451,8 +5771,9 @@ function githubPlugin(options = {}) {
5451
5771
  });
5452
5772
  },
5453
5773
  tools(ctx) {
5454
- return createGitHubTools(ctx);
5774
+ return createGitHubTools(ctx, readEnv(botEmailEnv));
5455
5775
  },
5776
+ workspacePrepare: prepareWorkspace,
5456
5777
  async sandboxPrepare(ctx) {
5457
5778
  const hooksPath = `${ctx.sandbox.juniorRoot}/git-hooks`;
5458
5779
  await ctx.sandbox.writeFile({
@@ -5469,11 +5790,8 @@ function githubPlugin(options = {}) {
5469
5790
  if (ctx.tool.name !== "bash") {
5470
5791
  return;
5471
5792
  }
5472
- const botName = readEnv(botNameEnv);
5473
- const botEmail = readEnv(botEmailEnv);
5474
- if (!botName || !botEmail) {
5475
- return;
5476
- }
5793
+ const botName = requireEnv(botNameEnv);
5794
+ const botEmail = requireEnv(botEmailEnv);
5477
5795
  ctx.env.set("GIT_AUTHOR_NAME", botName);
5478
5796
  ctx.env.set("GIT_AUTHOR_EMAIL", botEmail);
5479
5797
  ctx.env.set("JUNIOR_GIT_AUTHOR_NAME", botName);
@@ -0,0 +1,4 @@
1
+ /** Sandbox root directories reserved for Junior runtime material. */
2
+ export declare const RESERVED_SANDBOX_DIRECTORIES: Set<string>;
3
+ /** True when a checkout path collides with a reserved sandbox root (case-insensitive). */
4
+ export declare function isReservedSandboxDirectory(path: string): boolean;
@@ -7,6 +7,7 @@ export declare function createGitHubCloneRepositoryTool(ctx: ToolRegistrationHoo
7
7
  [x: string]: unknown;
8
8
  path: string;
9
9
  repo: string;
10
+ workspaces: string[];
10
11
  target: "cloneRepository";
11
12
  truncated?: boolean | undefined;
12
13
  continuation?: {
@@ -17,6 +18,7 @@ export declare function createGitHubCloneRepositoryTool(ctx: ToolRegistrationHoo
17
18
  [x: string]: unknown;
18
19
  path: string;
19
20
  repo: string;
21
+ workspaces: string[];
20
22
  target: "cloneRepository";
21
23
  truncated?: boolean | undefined;
22
24
  continuation?: {
@@ -0,0 +1,30 @@
1
+ import { type ToolRegistrationHookContext } from "@sentry/junior-plugin-api";
2
+ /** Resolve one review thread after GitHub proves it belongs to a Junior-authored PR. */
3
+ export declare function createGitHubResolvePullRequestReviewThreadTool(ctx: ToolRegistrationHookContext, botEmail: string | undefined): import("@sentry/junior-plugin-api").PluginToolDefinition<{
4
+ repo: string;
5
+ threadId: string;
6
+ }, {
7
+ [x: string]: unknown;
8
+ target: "resolvePullRequestReviewThread";
9
+ repo: string;
10
+ number: number;
11
+ threadId: string;
12
+ resolved: boolean;
13
+ truncated?: boolean | undefined;
14
+ continuation?: {
15
+ arguments: Record<string, unknown>;
16
+ reason?: string | undefined;
17
+ } | undefined;
18
+ }, {
19
+ [x: string]: unknown;
20
+ target: "resolvePullRequestReviewThread";
21
+ repo: string;
22
+ number: number;
23
+ threadId: string;
24
+ resolved: boolean;
25
+ truncated?: boolean | undefined;
26
+ continuation?: {
27
+ arguments: Record<string, unknown>;
28
+ reason?: string | undefined;
29
+ } | undefined;
30
+ }>;
package/dist/tools.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import type { PluginToolDefinition, ToolRegistrationHookContext } from "@sentry/junior-plugin-api";
2
2
  /** Build the GitHub plugin's runtime tools from their per-tool modules. */
3
- export declare function createGitHubTools(ctx: ToolRegistrationHookContext): Record<string, PluginToolDefinition>;
3
+ export declare function createGitHubTools(ctx: ToolRegistrationHookContext, botEmail?: string): Record<string, PluginToolDefinition>;
@@ -0,0 +1,3 @@
1
+ import type { WorkspacePrepareHookContext } from "@sentry/junior-plugin-api";
2
+ /** Clone GitHub repositories into their host-selected Workspace paths. */
3
+ export declare function prepareWorkspace(ctx: WorkspacePrepareHookContext): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-github",
3
- "version": "0.162.0",
3
+ "version": "0.164.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -31,7 +31,7 @@
31
31
  "@sinclair/typebox": "^0.34.49",
32
32
  "drizzle-orm": "^0.45.2",
33
33
  "zod": "^4.4.3",
34
- "@sentry/junior-plugin-api": "0.162.0"
34
+ "@sentry/junior-plugin-api": "0.164.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^25.9.1",
@@ -5,7 +5,7 @@ description: Work with GitHub repositories, source code, branches, commits, pull
5
5
 
6
6
  # GitHub Code Operations
7
7
 
8
- Use `git` and `gh` for repository work. Use `github_createPullRequest`, not `gh pr create`, for new PRs. Use `github_updatePullRequest`, not raw `gh api`/`gh pr edit`, when changing PR title, body, base, or open/closed state.
8
+ Use `git` and `gh` for repository work. Use `github_createPullRequest`, not `gh pr create`, for new PRs. Use `github_updatePullRequest`, not raw `gh api`/`gh pr edit`, when changing PR title, body, base, or open/closed state. Use `github_resolvePullRequestReviewThread`, not raw `gh api graphql` `resolveReviewThread`, when resolving review threads on Junior-authored PRs.
9
9
 
10
10
  ## References
11
11
 
@@ -1,6 +1,6 @@
1
1
  # GitHub API Surface — code & pull requests
2
2
 
3
- PR creation uses Junior's `github_createPullRequest` tool. PR title, body, base, and open/closed state updates use `github_updatePullRequest` so Junior keeps requester attribution and the conversation footer. Other supported mutations use allowlisted REST endpoints through `gh api`; generic GraphQL-backed `gh pr` mutations are not supported.
3
+ PR creation uses Junior's `github_createPullRequest` tool. PR title, body, base, and open/closed state updates use `github_updatePullRequest` so Junior keeps requester attribution and the conversation footer. Review-thread resolve uses `github_resolvePullRequestReviewThread` because GitHub exposes only the GraphQL `resolveReviewThread` mutation (no REST endpoint and no first-class `gh pr` subcommand). Other supported mutations use allowlisted REST endpoints through `gh api`; generic GraphQL-backed `gh pr` mutations are not supported.
4
4
 
5
5
  ## Repo scoping
6
6
 
@@ -50,6 +50,7 @@ Treat explicit repo flags as command-targeting safety rails, not as a credential
50
50
  | Submit pull request review | `gh api repos/owner/repo/pulls/NUMBER/reviews --method POST --input review.json` |
51
51
  | Post inline review comment | `gh api repos/owner/repo/pulls/NUMBER/comments --method POST --input comment.json` |
52
52
  | Reply to inline review comment | `gh api repos/owner/repo/pulls/NUMBER/comments/COMMENT_ID/replies --method POST --input reply.json` |
53
+ | Resolve review thread | `github_resolvePullRequestReviewThread({ repo: "owner/repo", threadId: "PRRT_..." })` (GraphQL `resolveReviewThread` substitute; Junior-authored PRs only) |
53
54
  | View pull request | `gh pr view NUMBER --repo owner/repo [--json ...]` |
54
55
  | List pull requests | `gh pr list --repo owner/repo [--state open \| closed \| merged]` |
55
56
  | Diff pull request | `gh pr diff NUMBER --repo owner/repo` |
@@ -78,6 +79,7 @@ jr-rpc config set github.repo owner/repo
78
79
  - Use `github_updatePullRequest` for title, body, base, or open/closed state changes. Do not raw-`PATCH` `/repos/.../pulls/NUMBER`; that path is denied so Junior can keep the conversation footer.
79
80
  - Merge, fork creation, REST contents/Git database writes, and repository administration are outside the current write allowlist.
80
81
  - Pull request reviews and inline review comments use the same repository-scoped `installation-write` credential as other bot-owned PR writes, so they post as Junior even on headless turns. Merge remains denied.
82
+ - Resolve review threads with `github_resolvePullRequestReviewThread`. That tool is the Junior equivalent of `gh api graphql` `resolveReviewThread`; raw GraphQL mutations stay denied, and the tool only succeeds on Junior-authored PRs.
81
83
  - If the explicit `git push` fails with 401/403 or another access/permission error, verify the repo context and retry once. If it still fails, load troubleshooting guidance and report the exact command failure.
82
84
  - PR comments, labels, and assignees use GitHub's issue endpoints; use the `github-issues` REST guidance for those operations. All allowlisted bot writes share the same repository-scoped `installation-write` credential.
83
85
  - To embed a local image in a GitHub issue, pull request, review, or comment, call `publishImage` first. That tool returns a durable public URL. The published image is public to anyone on the internet who has the URL. Embed the URL with normal GitHub Markdown. Do not use private Slack file links or conversation attachment URLs.
@@ -19,6 +19,7 @@ Use this table to recover quickly while keeping operations deterministic.
19
19
  | `github_createPullRequest` returns 422 for `head` | The head branch was not pushed or the explicit head ref is wrong. | Push the branch, then retry with explicit `repo`, `head`, and `base` values. |
20
20
  | `github_createPullRequest` fails with 422 validation on `base` | The `base` branch does not exist in the target repo. | Resolve the default branch with `gh repo view owner/repo --json defaultBranchRef --jq .defaultBranchRef.name`, then retry with that value as `base`. |
21
21
  | `403` names `github_updatePullRequest` | Raw PR title/body/base/state PATCH was blocked so Junior can own the footer. | Retry with `github_updatePullRequest`; do not use `gh api .../pulls/NUMBER --method PATCH` or `gh pr edit`. |
22
+ | `GraphQL mutations are not enabled` / resolve thread denied | Raw `gh api graphql` `resolveReviewThread` is blocked. | Retry with `github_resolvePullRequestReviewThread({ repo, threadId })`. Only Junior-authored PRs are allowed. |
22
23
  | `github_updatePullRequest` returns upstream 401/403 | The App installation or target repository does not permit the operation. | Use the structured upstream denial to verify installation scope and accepted permissions; do not request user OAuth for this bot-owned operation. |
23
24
  | `git blame`, long log history, or old commits are missing after clone | Repo was cloned shallow by design. | Fetch the required ref and deepen it incrementally; use `--unshallow` only when bounded deepening is insufficient. |
24
25
  | Rebase, merge-base, or `origin/BASE...HEAD` comparison fails or gives odd ancestry | Required ancestry or the remote-tracking base ref is absent from the shallow clone. | Fetch a bounded depth into `BASE:refs/remotes/origin/BASE`, deepen that base ref until the merge base exists, and compare or rebase against `origin/BASE`. Never force-push around incomplete history. |