@sentry/junior-github 0.159.0 → 0.161.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/README.md CHANGED
@@ -29,3 +29,5 @@ The plugin owns its signed webhook route, deployment, pull request, and release
29
29
  resource events, normalized pull request and issue outcome projections, and
30
30
  dashboard operational report. Core only owns delivery from plugin-published
31
31
  resource events into matching conversation subscriptions.
32
+
33
+ Person profiles call `hooks.profileReport` for Junior-owned GitHub work attributed through conversation actors.
@@ -0,0 +1,3 @@
1
+ import type { ConversationAnnotation, ConversationSidebarAnnotation } from "@sentry/junior-plugin-api";
2
+ /** Select the one GitHub annotation summary shown in a conversation row. */
3
+ export declare function githubSidebarAnnotation(annotations: ConversationAnnotation[]): ConversationSidebarAnnotation | undefined;
package/dist/index.js CHANGED
@@ -2695,19 +2695,30 @@ async function recordGitHubIssueOutcome(db, input) {
2695
2695
  const outcome = githubIssueOutcomeInputSchema.parse(input);
2696
2696
  const values = projectionValues(outcome);
2697
2697
  if (!outcome.candidateOwned) {
2698
- await db.update(juniorGitHubIssues).set(values).where(
2698
+ const updated = await db.update(juniorGitHubIssues).set(values).where(
2699
2699
  and(
2700
2700
  eq(juniorGitHubIssues.issueId, outcome.issueId),
2701
2701
  lte(juniorGitHubIssues.updatedAt, outcome.updatedAt)
2702
2702
  )
2703
- );
2704
- return;
2703
+ ).returning({
2704
+ conversationIds: juniorGitHubIssues.conversationIds
2705
+ });
2706
+ return {
2707
+ applied: updated.length > 0,
2708
+ conversationIds: updated[0]?.conversationIds ?? []
2709
+ };
2705
2710
  }
2706
- await db.insert(juniorGitHubIssues).values({ issueId: outcome.issueId, ...values }).onConflictDoUpdate({
2711
+ const inserted = await db.insert(juniorGitHubIssues).values({ issueId: outcome.issueId, ...values }).onConflictDoUpdate({
2707
2712
  target: juniorGitHubIssues.issueId,
2708
2713
  set: values,
2709
2714
  where: lte(juniorGitHubIssues.updatedAt, outcome.updatedAt)
2715
+ }).returning({
2716
+ conversationIds: juniorGitHubIssues.conversationIds
2710
2717
  });
2718
+ return {
2719
+ applied: inserted.length > 0,
2720
+ conversationIds: inserted[0]?.conversationIds ?? []
2721
+ };
2711
2722
  }
2712
2723
  async function recordGitHubIssueConversations(db, input) {
2713
2724
  const association = githubIssueConversationsInputSchema.parse(input);
@@ -3342,7 +3353,23 @@ function createGitHubWebhookRoute(args) {
3342
3353
  }
3343
3354
  }
3344
3355
  if (issueOutcome) {
3345
- await recordGitHubIssueOutcome(args.db, issueOutcome);
3356
+ const recordedOutcome = await recordGitHubIssueOutcome(
3357
+ args.db,
3358
+ issueOutcome
3359
+ );
3360
+ if (recordedOutcome.applied && issueOutcome.state === "closed") {
3361
+ await Promise.all(
3362
+ recordedOutcome.conversationIds.map(
3363
+ (conversationId) => args.annotations.forConversation(conversationId).upsert({
3364
+ kind: "resource_link",
3365
+ key: `${issueOutcome.repositoryFullName.toLowerCase()}#${issueOutcome.number}`,
3366
+ label: `${issueOutcome.repositoryFullName}#${issueOutcome.number}`,
3367
+ url: `https://github.com/${issueOutcome.repositoryFullName}/issues/${issueOutcome.number}`,
3368
+ status: "closed"
3369
+ })
3370
+ )
3371
+ );
3372
+ }
3346
3373
  }
3347
3374
  const recordedIssueConversations = issueConversations ? await recordGitHubIssueConversations(args.db, issueConversations) : false;
3348
3375
  const recordedPullRequestConversations = pullRequestConversations ? await recordGitHubPullRequestConversations(
@@ -3371,20 +3398,258 @@ function createGitHubWebhookRoute(args) {
3371
3398
  };
3372
3399
  }
3373
3400
 
3374
- // src/outcomes/report.ts
3375
- import { sql as sql5 } from "drizzle-orm";
3376
- import { z as z16 } from "zod";
3377
-
3378
- // src/outcomes/cost.ts
3401
+ // src/outcomes/profile-report.ts
3379
3402
  import { sql as sql4 } from "drizzle-orm";
3380
3403
  import { z as z15 } from "zod";
3381
3404
  var DAY_MS = 24 * 60 * 60 * 1e3;
3382
- var costWindowSchema = z15.object({
3405
+ var WINDOWS = [7, 30, 90];
3406
+ var pullRequestStatsSchema = z15.object({
3407
+ closed: z15.number().int().nonnegative(),
3408
+ created: z15.number().int().nonnegative(),
3383
3409
  days: z15.number().int().positive(),
3384
- issueCostUsd: z15.number().nonnegative().nullable(),
3385
- medianIssueCostUsd: z15.number().nonnegative().nullable(),
3386
- medianPullRequestCostUsd: z15.number().nonnegative().nullable(),
3387
- pullRequestCostUsd: z15.number().nonnegative().nullable()
3410
+ merged: z15.number().int().nonnegative()
3411
+ }).strict().transform((row) => {
3412
+ const terminal = row.merged + row.closed;
3413
+ return {
3414
+ ...row,
3415
+ mergeRate: terminal > 0 ? row.merged / terminal : void 0
3416
+ };
3417
+ });
3418
+ var issueStatsSchema = z15.object({
3419
+ created: z15.number().int().nonnegative(),
3420
+ days: z15.number().int().positive()
3421
+ }).strict();
3422
+ var daySchema = z15.object({
3423
+ created: z15.number().int().nonnegative(),
3424
+ date: z15.string().date()
3425
+ }).strict();
3426
+ function queryRows(result) {
3427
+ if (typeof result !== "object" || result === null || !("rows" in result) || !Array.isArray(result.rows)) {
3428
+ throw new TypeError("GitHub profile report query did not return rows");
3429
+ }
3430
+ return result.rows;
3431
+ }
3432
+ function ownedByUserSql(conversationIds, userId) {
3433
+ return sql4`EXISTS (
3434
+ SELECT 1
3435
+ FROM unnest(${conversationIds}) AS linked(conversation_id)
3436
+ INNER JOIN junior_conversations AS conversations
3437
+ ON conversations.conversation_id = linked.conversation_id
3438
+ INNER JOIN junior_identities AS identities
3439
+ ON identities.id = conversations.actor_identity_id
3440
+ WHERE identities.user_id = ${userId}
3441
+ )`;
3442
+ }
3443
+ function formatPercent(value) {
3444
+ return value === void 0 ? "\u2014" : `${Math.round(value * 100)}%`;
3445
+ }
3446
+ function startOfUtcDay(timestampMs) {
3447
+ const date = new Date(timestampMs);
3448
+ date.setUTCHours(0, 0, 0, 0);
3449
+ return date;
3450
+ }
3451
+ async function aggregatePullRequestWindows(args) {
3452
+ const starts = WINDOWS.map(
3453
+ (days) => [days, new Date(args.nowMs - days * DAY_MS)]
3454
+ );
3455
+ const oldestStart = starts.at(-1)[1];
3456
+ const table = juniorGitHubPullRequests;
3457
+ const owned = ownedByUserSql(sql4`${table.conversationIds}`, args.userId);
3458
+ const result = await args.db.execute(sql4`
3459
+ WITH windows(days, start_at) AS (
3460
+ VALUES
3461
+ (${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
3462
+ (${starts[1][0]}::integer, ${starts[1][1]}::timestamptz),
3463
+ (${starts[2][0]}::integer, ${starts[2][1]}::timestamptz)
3464
+ ), recent_pull_requests AS MATERIALIZED (
3465
+ SELECT
3466
+ ${table.pullRequestId},
3467
+ ${table.state},
3468
+ ${table.openedAt},
3469
+ ${table.mergedAt},
3470
+ ${table.closedAt}
3471
+ FROM ${table}
3472
+ WHERE (${table.openedAt} >= ${oldestStart}
3473
+ OR ${table.mergedAt} >= ${oldestStart}
3474
+ OR ${table.closedAt} >= ${oldestStart})
3475
+ AND ${owned}
3476
+ )
3477
+ SELECT
3478
+ windows.days AS "days",
3479
+ count(recent_pull_requests.pull_request_id)
3480
+ FILTER (WHERE recent_pull_requests.opened_at >= windows.start_at)::integer
3481
+ AS "created",
3482
+ count(recent_pull_requests.pull_request_id)
3483
+ FILTER (
3484
+ WHERE recent_pull_requests.state = 'merged'
3485
+ AND recent_pull_requests.merged_at >= windows.start_at
3486
+ )::integer AS "merged",
3487
+ count(recent_pull_requests.pull_request_id)
3488
+ FILTER (
3489
+ WHERE recent_pull_requests.state = 'closed_unmerged'
3490
+ AND recent_pull_requests.closed_at >= windows.start_at
3491
+ )::integer AS "closed"
3492
+ FROM windows
3493
+ LEFT JOIN recent_pull_requests ON true
3494
+ GROUP BY windows.days
3495
+ ORDER BY windows.days
3496
+ `);
3497
+ return z15.array(pullRequestStatsSchema).parse(queryRows(result));
3498
+ }
3499
+ async function aggregateIssueWindows(args) {
3500
+ const starts = WINDOWS.map(
3501
+ (days) => [days, new Date(args.nowMs - days * DAY_MS)]
3502
+ );
3503
+ const oldestStart = starts.at(-1)[1];
3504
+ const table = juniorGitHubIssues;
3505
+ const owned = ownedByUserSql(sql4`${table.conversationIds}`, args.userId);
3506
+ const result = await args.db.execute(sql4`
3507
+ WITH windows(days, start_at) AS (
3508
+ VALUES
3509
+ (${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
3510
+ (${starts[1][0]}::integer, ${starts[1][1]}::timestamptz),
3511
+ (${starts[2][0]}::integer, ${starts[2][1]}::timestamptz)
3512
+ ), recent_issues AS MATERIALIZED (
3513
+ SELECT
3514
+ ${table.issueId},
3515
+ ${table.openedAt}
3516
+ FROM ${table}
3517
+ WHERE ${table.openedAt} >= ${oldestStart}
3518
+ AND ${owned}
3519
+ )
3520
+ SELECT
3521
+ windows.days AS "days",
3522
+ count(recent_issues.issue_id)
3523
+ FILTER (WHERE recent_issues.opened_at >= windows.start_at)::integer
3524
+ AS "created"
3525
+ FROM windows
3526
+ LEFT JOIN recent_issues ON true
3527
+ GROUP BY windows.days
3528
+ ORDER BY windows.days
3529
+ `);
3530
+ return z15.array(issueStatsSchema).parse(queryRows(result));
3531
+ }
3532
+ async function aggregateOpenedDays(args) {
3533
+ const end = new Date(args.nowMs);
3534
+ const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) * DAY_MS);
3535
+ const table = args.table;
3536
+ const owned = ownedByUserSql(sql4`${table.conversationIds}`, args.userId);
3537
+ const result = await args.db.execute(sql4`
3538
+ WITH days AS (
3539
+ SELECT generate_series(
3540
+ date_trunc('day', ${start}::timestamptz AT TIME ZONE 'UTC'),
3541
+ date_trunc('day', ${end}::timestamptz AT TIME ZONE 'UTC'),
3542
+ interval '1 day'
3543
+ ) AS day
3544
+ ), daily AS (
3545
+ SELECT
3546
+ date_trunc('day', ${table.openedAt} AT TIME ZONE 'UTC') AS day,
3547
+ count(*)::integer AS created
3548
+ FROM ${table}
3549
+ WHERE ${table.openedAt} >= ${start}
3550
+ AND ${owned}
3551
+ GROUP BY date_trunc('day', ${table.openedAt} AT TIME ZONE 'UTC')
3552
+ )
3553
+ SELECT
3554
+ to_char(days.day, 'YYYY-MM-DD') AS "date",
3555
+ coalesce(daily.created, 0)::integer AS "created"
3556
+ FROM days
3557
+ LEFT JOIN daily ON daily.day = days.day
3558
+ ORDER BY days.day
3559
+ `);
3560
+ return z15.array(daySchema).parse(queryRows(result));
3561
+ }
3562
+ async function buildGitHubProfileReport(args) {
3563
+ const [windows, pullRequestDays, issueWindows, issueDays] = await Promise.all(
3564
+ [
3565
+ aggregatePullRequestWindows(args),
3566
+ aggregateOpenedDays({
3567
+ db: args.db,
3568
+ nowMs: args.nowMs,
3569
+ table: juniorGitHubPullRequests,
3570
+ userId: args.userId
3571
+ }),
3572
+ aggregateIssueWindows(args),
3573
+ aggregateOpenedDays({
3574
+ db: args.db,
3575
+ nowMs: args.nowMs,
3576
+ table: juniorGitHubIssues,
3577
+ userId: args.userId
3578
+ })
3579
+ ]
3580
+ );
3581
+ const thirtyDays = windows.find((window) => window.days === 30);
3582
+ const issueThirtyDays = issueWindows.find((window) => window.days === 30);
3583
+ const hasActivity = windows.some((window) => window.created + window.merged + window.closed > 0) || issueWindows.some((window) => window.created > 0);
3584
+ if (!hasActivity) {
3585
+ return void 0;
3586
+ }
3587
+ return {
3588
+ generatedAt: new Date(args.nowMs).toISOString(),
3589
+ title: "GitHub",
3590
+ metrics: [
3591
+ {
3592
+ label: "PRs opened \xB7 30d",
3593
+ value: String(thirtyDays.created)
3594
+ },
3595
+ {
3596
+ label: "PRs merged \xB7 30d",
3597
+ value: String(thirtyDays.merged)
3598
+ },
3599
+ {
3600
+ label: "Issues opened \xB7 30d",
3601
+ value: String(issueThirtyDays.created)
3602
+ },
3603
+ {
3604
+ label: "PR merge rate \xB7 30d",
3605
+ value: formatPercent(thirtyDays.mergeRate)
3606
+ }
3607
+ ],
3608
+ widgets: [
3609
+ {
3610
+ id: "pull-requests-created",
3611
+ type: "bar_chart",
3612
+ title: "Pull requests opened",
3613
+ description: "Junior-owned pull requests opened for this person per day",
3614
+ timeRangeDays: [...WINDOWS],
3615
+ series: [{ key: "created", label: "Opened" }],
3616
+ categories: pullRequestDays.map((stats) => ({
3617
+ id: stats.date,
3618
+ label: stats.date,
3619
+ values: { created: stats.created }
3620
+ }))
3621
+ },
3622
+ {
3623
+ id: "issues-created",
3624
+ type: "bar_chart",
3625
+ title: "Issues opened",
3626
+ description: "Junior-owned issues opened for this person per day",
3627
+ timeRangeDays: [...WINDOWS],
3628
+ series: [{ key: "created", label: "Opened" }],
3629
+ categories: issueDays.map((stats) => ({
3630
+ id: stats.date,
3631
+ label: stats.date,
3632
+ values: { created: stats.created }
3633
+ }))
3634
+ }
3635
+ ]
3636
+ };
3637
+ }
3638
+
3639
+ // src/outcomes/report.ts
3640
+ import { sql as sql6 } from "drizzle-orm";
3641
+ import { z as z17 } from "zod";
3642
+
3643
+ // src/outcomes/cost.ts
3644
+ import { sql as sql5 } from "drizzle-orm";
3645
+ import { z as z16 } from "zod";
3646
+ 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()
3388
3653
  }).strict().transform((row) => ({
3389
3654
  days: row.days,
3390
3655
  issueCostUsd: row.issueCostUsd ?? void 0,
@@ -3392,12 +3657,12 @@ var costWindowSchema = z15.object({
3392
3657
  medianPullRequestCostUsd: row.medianPullRequestCostUsd ?? void 0,
3393
3658
  pullRequestCostUsd: row.pullRequestCostUsd ?? void 0
3394
3659
  }));
3395
- var repositoryCostSchema = z15.object({
3396
- issueCostUsd: z15.number().nonnegative().nullable(),
3397
- medianIssueCostUsd: z15.number().nonnegative().nullable(),
3398
- medianPullRequestCostUsd: z15.number().nonnegative().nullable(),
3399
- pullRequestCostUsd: z15.number().nonnegative().nullable(),
3400
- repository: z15.string().min(1)
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)
3401
3666
  }).strict().transform((row) => ({
3402
3667
  issueCostUsd: row.issueCostUsd ?? void 0,
3403
3668
  medianIssueCostUsd: row.medianIssueCostUsd ?? void 0,
@@ -3405,17 +3670,17 @@ var repositoryCostSchema = z15.object({
3405
3670
  pullRequestCostUsd: row.pullRequestCostUsd ?? void 0,
3406
3671
  repository: row.repository
3407
3672
  }));
3408
- function queryRows(result) {
3673
+ function queryRows2(result) {
3409
3674
  if (typeof result !== "object" || result === null || !("rows" in result) || !Array.isArray(result.rows)) {
3410
3675
  throw new TypeError("GitHub cost query did not return rows");
3411
3676
  }
3412
3677
  return result.rows;
3413
3678
  }
3414
3679
  async function hasConversationUsageTable(db) {
3415
- const result = await db.execute(sql4`
3680
+ const result = await db.execute(sql5`
3416
3681
  SELECT to_regclass('public.junior_conversations') IS NOT NULL AS "present"
3417
3682
  `);
3418
- const row = queryRows(result)[0];
3683
+ const row = queryRows2(result)[0];
3419
3684
  return typeof row === "object" && row !== null && "present" in row && row.present === true;
3420
3685
  }
3421
3686
  function emptyCostWindows(windows) {
@@ -3428,7 +3693,7 @@ function emptyCostWindows(windows) {
3428
3693
  }));
3429
3694
  }
3430
3695
  function conversationTreeCostExpr() {
3431
- return sql4`
3696
+ return sql5`
3432
3697
  coalesce((
3433
3698
  SELECT sum(
3434
3699
  CASE
@@ -3459,7 +3724,7 @@ function conversationTreeCostExpr() {
3459
3724
  function pullRequestConversationIdsExpr() {
3460
3725
  const pullRequests = juniorGitHubPullRequests;
3461
3726
  const issues = juniorGitHubIssues;
3462
- return sql4`
3727
+ return sql5`
3463
3728
  ARRAY(
3464
3729
  SELECT DISTINCT unnest(
3465
3730
  ${pullRequests.conversationIds}
@@ -3481,7 +3746,7 @@ function pullRequestConversationIdsExpr() {
3481
3746
  function issueConversationIdsExpr() {
3482
3747
  const pullRequests = juniorGitHubPullRequests;
3483
3748
  const issues = juniorGitHubIssues;
3484
- return sql4`
3749
+ return sql5`
3485
3750
  ARRAY(
3486
3751
  SELECT DISTINCT unnest(
3487
3752
  ${issues.conversationIds}
@@ -3505,21 +3770,21 @@ async function aggregateGitHubCostWindows(args) {
3505
3770
  return emptyCostWindows(args.windows);
3506
3771
  }
3507
3772
  const starts = args.windows.map(
3508
- (days) => [days, new Date(args.nowMs - days * DAY_MS)]
3773
+ (days) => [days, new Date(args.nowMs - days * DAY_MS2)]
3509
3774
  );
3510
3775
  const oldestStart = starts.at(-1)[1];
3511
3776
  const pullRequests = juniorGitHubPullRequests;
3512
3777
  const issues = juniorGitHubIssues;
3513
- const windowValues = sql4.join(
3778
+ const windowValues = sql5.join(
3514
3779
  starts.map(
3515
- ([days, start]) => sql4`(${days}::integer, ${start}::timestamptz)`
3780
+ ([days, start]) => sql5`(${days}::integer, ${start}::timestamptz)`
3516
3781
  ),
3517
- sql4`, `
3782
+ sql5`, `
3518
3783
  );
3519
3784
  const conversationTreeCost = conversationTreeCostExpr();
3520
3785
  const pullRequestConversationIds = pullRequestConversationIdsExpr();
3521
3786
  const issueConversationIds = issueConversationIdsExpr();
3522
- const result = await args.db.execute(sql4`
3787
+ const result = await args.db.execute(sql5`
3523
3788
  WITH windows(days, start_at) AS (
3524
3789
  VALUES ${windowValues}
3525
3790
  ), pull_request_entities AS (
@@ -3602,19 +3867,19 @@ async function aggregateGitHubCostWindows(args) {
3602
3867
  INNER JOIN issue_window ON issue_window.days = pull_request_window.days
3603
3868
  ORDER BY pull_request_window.days
3604
3869
  `);
3605
- return z15.array(costWindowSchema).parse(queryRows(result));
3870
+ return z16.array(costWindowSchema).parse(queryRows2(result));
3606
3871
  }
3607
3872
  async function aggregateGitHubRepositoryCosts(args) {
3608
3873
  if (!await hasConversationUsageTable(args.db)) {
3609
3874
  return [];
3610
3875
  }
3611
- const start = new Date(args.nowMs - 30 * DAY_MS);
3876
+ const start = new Date(args.nowMs - 30 * DAY_MS2);
3612
3877
  const pullRequests = juniorGitHubPullRequests;
3613
3878
  const issues = juniorGitHubIssues;
3614
3879
  const conversationTreeCost = conversationTreeCostExpr();
3615
3880
  const pullRequestConversationIds = pullRequestConversationIdsExpr();
3616
3881
  const issueConversationIds = issueConversationIdsExpr();
3617
- const result = await args.db.execute(sql4`
3882
+ const result = await args.db.execute(sql5`
3618
3883
  WITH pull_request_entities AS (
3619
3884
  SELECT
3620
3885
  ${pullRequests.repositoryFullName} AS repository,
@@ -3700,7 +3965,7 @@ async function aggregateGitHubRepositoryCosts(args) {
3700
3965
  ON issue_totals.repository = repositories.repository
3701
3966
  ORDER BY "repository" ASC
3702
3967
  `);
3703
- return z15.array(repositoryCostSchema).parse(queryRows(result));
3968
+ return z16.array(repositoryCostSchema).parse(queryRows2(result));
3704
3969
  }
3705
3970
  function formatCostUsd(value) {
3706
3971
  if (value === void 0) return "\u2014";
@@ -3713,14 +3978,14 @@ function formatCostUsd(value) {
3713
3978
  }
3714
3979
 
3715
3980
  // src/outcomes/report.ts
3716
- var DAY_MS2 = 24 * 60 * 60 * 1e3;
3717
- var WINDOWS = [7, 30, 90];
3718
- var pullRequestStatsSchema = z16.object({
3719
- closed: z16.number().int().nonnegative(),
3720
- created: z16.number().int().nonnegative(),
3721
- days: z16.number().int().positive(),
3722
- medianMergeTimeMs: z16.number().nonnegative().nullable(),
3723
- merged: z16.number().int().nonnegative()
3981
+ var DAY_MS3 = 24 * 60 * 60 * 1e3;
3982
+ 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()
3724
3989
  }).strict().transform((row) => {
3725
3990
  const terminal = row.merged + row.closed;
3726
3991
  return {
@@ -3729,12 +3994,12 @@ var pullRequestStatsSchema = z16.object({
3729
3994
  mergeRate: terminal > 0 ? row.merged / terminal : void 0
3730
3995
  };
3731
3996
  });
3732
- var pullRequestRepositoryStatsSchema = z16.object({
3733
- closed: z16.number().int().nonnegative(),
3734
- created: z16.number().int().nonnegative(),
3735
- juniorOnly: z16.number().int().nonnegative(),
3736
- merged: z16.number().int().nonnegative(),
3737
- repository: z16.string().min(1)
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)
3738
4003
  }).strict().transform((row) => {
3739
4004
  const terminal = row.merged + row.closed;
3740
4005
  return {
@@ -3742,47 +4007,47 @@ var pullRequestRepositoryStatsSchema = z16.object({
3742
4007
  mergeRate: terminal > 0 ? row.merged / terminal : void 0
3743
4008
  };
3744
4009
  });
3745
- var issueStatsSchema = z16.object({
3746
- closedCompleted: z16.number().int().nonnegative(),
3747
- closedDuplicate: z16.number().int().nonnegative(),
3748
- closedNotPlanned: z16.number().int().nonnegative(),
3749
- closedUnknown: z16.number().int().nonnegative(),
3750
- created: z16.number().int().nonnegative(),
3751
- days: z16.number().int().positive(),
3752
- medianCloseTimeMs: z16.number().nonnegative().nullable()
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()
3753
4018
  }).strict().transform((row) => ({
3754
4019
  ...row,
3755
4020
  medianCloseTimeMs: row.medianCloseTimeMs ?? void 0
3756
4021
  }));
3757
- var pullRequestDaySchema = z16.object({
3758
- created: z16.number().int().nonnegative(),
3759
- date: z16.string().date()
4022
+ var pullRequestDaySchema = z17.object({
4023
+ created: z17.number().int().nonnegative(),
4024
+ date: z17.string().date()
3760
4025
  }).strict();
3761
- var issueDaySchema = z16.object({
3762
- created: z16.number().int().nonnegative(),
3763
- date: z16.string().date()
4026
+ var issueDaySchema = z17.object({
4027
+ created: z17.number().int().nonnegative(),
4028
+ date: z17.string().date()
3764
4029
  }).strict();
3765
- var issueRepositoryStatsSchema = z16.object({
3766
- closedCompleted: z16.number().int().nonnegative(),
3767
- closedDuplicate: z16.number().int().nonnegative(),
3768
- closedNotPlanned: z16.number().int().nonnegative(),
3769
- closedUnknown: z16.number().int().nonnegative(),
3770
- created: z16.number().int().nonnegative(),
3771
- repository: z16.string().min(1)
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)
3772
4037
  }).strict();
3773
- function queryRows2(result) {
4038
+ function queryRows3(result) {
3774
4039
  if (typeof result !== "object" || result === null || !("rows" in result) || !Array.isArray(result.rows)) {
3775
4040
  throw new TypeError("GitHub outcome query did not return rows");
3776
4041
  }
3777
4042
  return result.rows;
3778
4043
  }
3779
- async function aggregatePullRequestWindows(args) {
3780
- const starts = WINDOWS.map(
3781
- (days) => [days, new Date(args.nowMs - days * DAY_MS2)]
4044
+ async function aggregatePullRequestWindows2(args) {
4045
+ const starts = WINDOWS2.map(
4046
+ (days) => [days, new Date(args.nowMs - days * DAY_MS3)]
3782
4047
  );
3783
4048
  const oldestStart = starts.at(-1)[1];
3784
4049
  const table = juniorGitHubPullRequests;
3785
- const result = await args.db.execute(sql5`
4050
+ const result = await args.db.execute(sql6`
3786
4051
  WITH windows(days, start_at) AS (
3787
4052
  VALUES
3788
4053
  (${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
@@ -3832,13 +4097,13 @@ async function aggregatePullRequestWindows(args) {
3832
4097
  GROUP BY windows.days
3833
4098
  ORDER BY windows.days
3834
4099
  `);
3835
- return z16.array(pullRequestStatsSchema).parse(queryRows2(result));
4100
+ return z17.array(pullRequestStatsSchema2).parse(queryRows3(result));
3836
4101
  }
3837
4102
  async function aggregatePullRequestDays(args) {
3838
4103
  const end = new Date(args.nowMs);
3839
- const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) * DAY_MS2);
4104
+ const start = startOfUtcDay2(args.nowMs - (WINDOWS2.at(-1) - 1) * DAY_MS3);
3840
4105
  const table = juniorGitHubPullRequests;
3841
- const result = await args.db.execute(sql5`
4106
+ const result = await args.db.execute(sql6`
3842
4107
  WITH days AS (
3843
4108
  SELECT generate_series(
3844
4109
  date_trunc('day', ${start}::timestamptz AT TIME ZONE 'UTC'),
@@ -3860,12 +4125,12 @@ async function aggregatePullRequestDays(args) {
3860
4125
  LEFT JOIN daily ON daily.day = days.day
3861
4126
  ORDER BY days.day
3862
4127
  `);
3863
- return z16.array(pullRequestDaySchema).parse(queryRows2(result));
4128
+ return z17.array(pullRequestDaySchema).parse(queryRows3(result));
3864
4129
  }
3865
4130
  async function aggregatePullRequestRepositories(args) {
3866
- const start = new Date(args.nowMs - 30 * DAY_MS2);
4131
+ const start = new Date(args.nowMs - 30 * DAY_MS3);
3867
4132
  const table = juniorGitHubPullRequests;
3868
- const result = await args.db.execute(sql5`
4133
+ const result = await args.db.execute(sql6`
3869
4134
  SELECT
3870
4135
  ${table.repositoryFullName} AS "repository",
3871
4136
  count(*) FILTER (WHERE ${table.openedAt} >= ${start})::integer
@@ -3890,15 +4155,15 @@ async function aggregatePullRequestRepositories(args) {
3890
4155
  ORDER BY "merged" DESC, "created" DESC, "repository" ASC
3891
4156
  LIMIT 25
3892
4157
  `);
3893
- return z16.array(pullRequestRepositoryStatsSchema).parse(queryRows2(result));
4158
+ return z17.array(pullRequestRepositoryStatsSchema).parse(queryRows3(result));
3894
4159
  }
3895
- async function aggregateIssueWindows(args) {
3896
- const starts = WINDOWS.map(
3897
- (days) => [days, new Date(args.nowMs - days * DAY_MS2)]
4160
+ async function aggregateIssueWindows2(args) {
4161
+ const starts = WINDOWS2.map(
4162
+ (days) => [days, new Date(args.nowMs - days * DAY_MS3)]
3898
4163
  );
3899
4164
  const oldestStart = starts.at(-1)[1];
3900
4165
  const table = juniorGitHubIssues;
3901
- const result = await args.db.execute(sql5`
4166
+ const result = await args.db.execute(sql6`
3902
4167
  WITH windows(days, start_at) AS (
3903
4168
  VALUES
3904
4169
  (${starts[0][0]}::integer, ${starts[0][1]}::timestamptz),
@@ -3959,13 +4224,13 @@ async function aggregateIssueWindows(args) {
3959
4224
  GROUP BY windows.days
3960
4225
  ORDER BY windows.days
3961
4226
  `);
3962
- return z16.array(issueStatsSchema).parse(queryRows2(result));
4227
+ return z17.array(issueStatsSchema2).parse(queryRows3(result));
3963
4228
  }
3964
4229
  async function aggregateIssueDays(args) {
3965
4230
  const end = new Date(args.nowMs);
3966
- const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) * DAY_MS2);
4231
+ const start = startOfUtcDay2(args.nowMs - (WINDOWS2.at(-1) - 1) * DAY_MS3);
3967
4232
  const table = juniorGitHubIssues;
3968
- const result = await args.db.execute(sql5`
4233
+ const result = await args.db.execute(sql6`
3969
4234
  WITH days AS (
3970
4235
  SELECT generate_series(
3971
4236
  date_trunc('day', ${start}::timestamptz AT TIME ZONE 'UTC'),
@@ -3987,12 +4252,12 @@ async function aggregateIssueDays(args) {
3987
4252
  LEFT JOIN daily ON daily.day = days.day
3988
4253
  ORDER BY days.day
3989
4254
  `);
3990
- return z16.array(issueDaySchema).parse(queryRows2(result));
4255
+ return z17.array(issueDaySchema).parse(queryRows3(result));
3991
4256
  }
3992
4257
  async function aggregateIssueRepositories(args) {
3993
- const start = new Date(args.nowMs - 30 * DAY_MS2);
4258
+ const start = new Date(args.nowMs - 30 * DAY_MS3);
3994
4259
  const table = juniorGitHubIssues;
3995
- const result = await args.db.execute(sql5`
4260
+ const result = await args.db.execute(sql6`
3996
4261
  SELECT
3997
4262
  ${table.repositoryFullName} AS "repository",
3998
4263
  count(*) FILTER (WHERE ${table.openedAt} >= ${start})::integer
@@ -4024,9 +4289,9 @@ async function aggregateIssueRepositories(args) {
4024
4289
  ORDER BY "created" DESC, "closedCompleted" DESC, "repository" ASC
4025
4290
  LIMIT 25
4026
4291
  `);
4027
- return z16.array(issueRepositoryStatsSchema).parse(queryRows2(result));
4292
+ return z17.array(issueRepositoryStatsSchema).parse(queryRows3(result));
4028
4293
  }
4029
- function formatPercent(value) {
4294
+ function formatPercent2(value) {
4030
4295
  return value === void 0 ? "\u2014" : `${Math.round(value * 100)}%`;
4031
4296
  }
4032
4297
  function formatDuration(value) {
@@ -4036,7 +4301,7 @@ function formatDuration(value) {
4036
4301
  if (hours < 24) return `${Math.round(hours * 10) / 10}h`;
4037
4302
  return `${Math.round(hours / 24 * 10) / 10}d`;
4038
4303
  }
4039
- function startOfUtcDay(timestampMs) {
4304
+ function startOfUtcDay2(timestampMs) {
4040
4305
  const date = new Date(timestampMs);
4041
4306
  date.setUTCHours(0, 0, 0, 0);
4042
4307
  return date;
@@ -4052,13 +4317,13 @@ async function buildGitHubOutcomeReport(args) {
4052
4317
  costWindows,
4053
4318
  repositoryCosts
4054
4319
  ] = await Promise.all([
4055
- aggregatePullRequestWindows(args),
4320
+ aggregatePullRequestWindows2(args),
4056
4321
  aggregatePullRequestDays(args),
4057
4322
  aggregatePullRequestRepositories(args),
4058
- aggregateIssueWindows(args),
4323
+ aggregateIssueWindows2(args),
4059
4324
  aggregateIssueDays(args),
4060
4325
  aggregateIssueRepositories(args),
4061
- aggregateGitHubCostWindows({ ...args, windows: WINDOWS }),
4326
+ aggregateGitHubCostWindows({ ...args, windows: WINDOWS2 }),
4062
4327
  aggregateGitHubRepositoryCosts(args)
4063
4328
  ]);
4064
4329
  const thirtyDays = windows.find((window) => window.days === 30);
@@ -4073,7 +4338,7 @@ async function buildGitHubOutcomeReport(args) {
4073
4338
  metrics: [
4074
4339
  {
4075
4340
  label: "PR closure merge rate \xB7 30d",
4076
- value: formatPercent(thirtyDays.mergeRate)
4341
+ value: formatPercent2(thirtyDays.mergeRate)
4077
4342
  },
4078
4343
  {
4079
4344
  label: "Median PR merge time \xB7 merged in 30d",
@@ -4106,7 +4371,7 @@ async function buildGitHubOutcomeReport(args) {
4106
4371
  type: "bar_chart",
4107
4372
  title: "Pull requests created",
4108
4373
  description: "Junior-owned pull requests opened per day",
4109
- timeRangeDays: [...WINDOWS],
4374
+ timeRangeDays: [...WINDOWS2],
4110
4375
  series: [{ key: "created", label: "Created" }],
4111
4376
  categories: pullRequestDays.map((stats) => ({
4112
4377
  id: stats.date,
@@ -4119,7 +4384,7 @@ async function buildGitHubOutcomeReport(args) {
4119
4384
  type: "bar_chart",
4120
4385
  title: "Issues created",
4121
4386
  description: "Junior-owned issues opened per day",
4122
- timeRangeDays: [...WINDOWS],
4387
+ timeRangeDays: [...WINDOWS2],
4123
4388
  series: [{ key: "created", label: "Created" }],
4124
4389
  categories: issueDays.map((stats) => ({
4125
4390
  id: stats.date,
@@ -4149,7 +4414,7 @@ async function buildGitHubOutcomeReport(args) {
4149
4414
  merged: String(stats.merged),
4150
4415
  closed: String(stats.closed),
4151
4416
  juniorOnly: String(stats.juniorOnly),
4152
- mergeRate: formatPercent(stats.mergeRate),
4417
+ mergeRate: formatPercent2(stats.mergeRate),
4153
4418
  medianCost: formatCostUsd(
4154
4419
  repositoryCostByName.get(repository)?.medianPullRequestCostUsd
4155
4420
  )
@@ -4188,18 +4453,18 @@ async function buildGitHubOutcomeReport(args) {
4188
4453
  }
4189
4454
 
4190
4455
  // src/pull-request-outcomes/commit-composition.ts
4191
- import { z as z17 } from "zod";
4192
- var canonicalCommitSchema = z17.object({
4193
- authorEmail: z17.string().nullable(),
4194
- authorLogin: z17.string().nullable()
4456
+ import { z as z18 } from "zod";
4457
+ var canonicalCommitSchema = z18.object({
4458
+ authorEmail: z18.string().nullable(),
4459
+ authorLogin: z18.string().nullable()
4195
4460
  }).strict();
4196
- var providerCommitSchema = z17.object({
4197
- author: z17.object({ login: z17.string() }).passthrough().nullable(),
4198
- commit: z17.object({
4199
- author: z17.object({ email: z17.string() }).passthrough().nullable()
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()
4200
4465
  }).passthrough()
4201
4466
  }).passthrough();
4202
- var commitPageSchema = z17.array(providerCommitSchema).transform(
4467
+ var commitPageSchema = z18.array(providerCommitSchema).transform(
4203
4468
  (commits) => commits.map(
4204
4469
  (commit) => canonicalCommitSchema.parse({
4205
4470
  authorEmail: commit.commit.author?.email ?? null,
@@ -4240,6 +4505,48 @@ async function classifyGitHubPullRequestCommitComposition(args) {
4240
4505
  return foundCommit ? "junior_only" : void 0;
4241
4506
  }
4242
4507
 
4508
+ // src/annotations.ts
4509
+ var STATUS_RANK = {
4510
+ warning: 5,
4511
+ open: 4,
4512
+ draft: 3,
4513
+ merged: 2,
4514
+ closed: 1
4515
+ };
4516
+ var STATUS_ICON = {
4517
+ warning: "triangle-alert",
4518
+ open: "circle-dot",
4519
+ draft: "circle-dashed",
4520
+ merged: "git-merge",
4521
+ closed: "circle-x"
4522
+ };
4523
+ function repositoryScope(annotation) {
4524
+ try {
4525
+ const [, owner, repo] = new URL(annotation.url).pathname.split("/");
4526
+ return owner && repo ? { key: `${owner}/${repo}`, label: repo } : void 0;
4527
+ } catch {
4528
+ return void 0;
4529
+ }
4530
+ }
4531
+ function githubSidebarAnnotation(annotations) {
4532
+ const links = annotations.flatMap((annotation) => {
4533
+ const repo = repositoryScope(annotation);
4534
+ const status2 = annotation.status;
4535
+ return repo && status2 ? [{ repo, status: status2 }] : [];
4536
+ });
4537
+ if (links.length === 0) return void 0;
4538
+ const repos = new Map(links.map((link) => [link.repo.key, link.repo.label]));
4539
+ const status = links.reduce(
4540
+ (current, link) => STATUS_RANK[link.status] > STATUS_RANK[current] ? link.status : current,
4541
+ "closed"
4542
+ );
4543
+ return {
4544
+ icon: STATUS_ICON[status],
4545
+ key: "github",
4546
+ label: repos.size === 1 ? [...repos.values()][0] : `${repos.size} repos`
4547
+ };
4548
+ }
4549
+
4243
4550
  // src/webhooks/check-suite-enrichment.ts
4244
4551
  function checkRunsFromResponse(value) {
4245
4552
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -4487,6 +4794,121 @@ async function configureGit(ctx, key, value) {
4487
4794
  }
4488
4795
  }
4489
4796
 
4797
+ // src/reply-markdown.ts
4798
+ var GITHUB_OWNER_PATTERN = "[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?";
4799
+ var GITHUB_REPOSITORY_PATTERN = "[A-Za-z0-9._-]+";
4800
+ var GITHUB_ISSUE_REFERENCE_PATTERN = new RegExp(
4801
+ `^(${GITHUB_OWNER_PATTERN})\\/(${GITHUB_REPOSITORY_PATTERN})#(\\d+)\\b`
4802
+ );
4803
+ function isReferenceBoundary(char) {
4804
+ return char === void 0 || !/[A-Za-z0-9._-]/.test(char);
4805
+ }
4806
+ function readInlineCode(text2, start) {
4807
+ if (text2[start] !== "`") {
4808
+ return void 0;
4809
+ }
4810
+ let markerLength = 1;
4811
+ while (text2[start + markerLength] === "`") {
4812
+ markerLength++;
4813
+ }
4814
+ const marker = "`".repeat(markerLength);
4815
+ const end = text2.indexOf(marker, start + markerLength);
4816
+ return end === -1 ? void 0 : end + markerLength;
4817
+ }
4818
+ function readFenceLength(line) {
4819
+ const trimmed = line.trimStart();
4820
+ if (!trimmed.startsWith("```")) {
4821
+ return void 0;
4822
+ }
4823
+ let length = 0;
4824
+ while (trimmed[length] === "`") {
4825
+ length++;
4826
+ }
4827
+ return length >= 3 ? length : void 0;
4828
+ }
4829
+ function readMarkdownLink(text2, start) {
4830
+ if (text2[start] !== "[") {
4831
+ return void 0;
4832
+ }
4833
+ const labelEnd = text2.indexOf("](", start + 1);
4834
+ if (labelEnd === -1) {
4835
+ return void 0;
4836
+ }
4837
+ const label = text2.slice(start + 1, labelEnd);
4838
+ if (label.includes("[") || label.includes("]") || label.includes("\n")) {
4839
+ return void 0;
4840
+ }
4841
+ const destStart = labelEnd + 2;
4842
+ const closeParens = text2.indexOf(")", destStart);
4843
+ return closeParens === -1 ? void 0 : closeParens + 1;
4844
+ }
4845
+ function readAngleToken(text2, start) {
4846
+ if (text2[start] !== "<") {
4847
+ return void 0;
4848
+ }
4849
+ const end = text2.indexOf(">", start + 1);
4850
+ return end === -1 ? void 0 : end + 1;
4851
+ }
4852
+ function linkifyLine(line) {
4853
+ let output = "";
4854
+ let index2 = 0;
4855
+ while (index2 < line.length) {
4856
+ const codeEnd = readInlineCode(line, index2);
4857
+ if (codeEnd !== void 0) {
4858
+ output += line.slice(index2, codeEnd);
4859
+ index2 = codeEnd;
4860
+ continue;
4861
+ }
4862
+ const markdownLinkEnd = readMarkdownLink(line, index2);
4863
+ if (markdownLinkEnd !== void 0) {
4864
+ output += line.slice(index2, markdownLinkEnd);
4865
+ index2 = markdownLinkEnd;
4866
+ continue;
4867
+ }
4868
+ const angleTokenEnd = readAngleToken(line, index2);
4869
+ if (angleTokenEnd !== void 0) {
4870
+ output += line.slice(index2, angleTokenEnd);
4871
+ index2 = angleTokenEnd;
4872
+ continue;
4873
+ }
4874
+ if (line.startsWith("https://", index2) || line.startsWith("http://", index2)) {
4875
+ const match = /^https?:\/\/\S+/.exec(line.slice(index2));
4876
+ if (match) {
4877
+ output += match[0];
4878
+ index2 += match[0].length;
4879
+ continue;
4880
+ }
4881
+ }
4882
+ if (isReferenceBoundary(index2 === 0 ? void 0 : line[index2 - 1])) {
4883
+ const match = GITHUB_ISSUE_REFERENCE_PATTERN.exec(line.slice(index2));
4884
+ if (match) {
4885
+ const [reference, owner, repository, number] = match;
4886
+ output += `[${reference}](https://github.com/${owner}/${repository}/issues/${number})`;
4887
+ index2 += reference.length;
4888
+ continue;
4889
+ }
4890
+ }
4891
+ output += line[index2];
4892
+ index2++;
4893
+ }
4894
+ return output;
4895
+ }
4896
+ function linkifyGitHubReferences(text2) {
4897
+ let openFenceLength = 0;
4898
+ return text2.split("\n").map((line) => {
4899
+ const fenceLength = readFenceLength(line);
4900
+ if (fenceLength !== void 0) {
4901
+ if (openFenceLength === 0) {
4902
+ openFenceLength = fenceLength;
4903
+ } else if (fenceLength >= openFenceLength) {
4904
+ openFenceLength = 0;
4905
+ }
4906
+ return line;
4907
+ }
4908
+ return openFenceLength > 0 ? line : linkifyLine(line);
4909
+ }).join("\n");
4910
+ }
4911
+
4490
4912
  // src/plugin.ts
4491
4913
  function githubSmartHttpAccess(upstreamUrl) {
4492
4914
  const pathname = upstreamUrl.pathname.toLowerCase();
@@ -4935,6 +5357,21 @@ function githubPlugin(options = {}) {
4935
5357
  ]
4936
5358
  },
4937
5359
  hooks: {
5360
+ conversationSidebar(ctx) {
5361
+ return {
5362
+ annotationsByConversationId: Object.fromEntries(
5363
+ ctx.conversationIds.flatMap((conversationId) => {
5364
+ const annotation = githubSidebarAnnotation(
5365
+ ctx.annotationsByConversationId[conversationId] ?? []
5366
+ );
5367
+ return annotation ? [[conversationId, [annotation]]] : [];
5368
+ })
5369
+ )
5370
+ };
5371
+ },
5372
+ formatMarkdown({ text: text2 }) {
5373
+ return linkifyGitHubReferences(text2);
5374
+ },
4938
5375
  async unfinishedWork(ctx) {
4939
5376
  const db = ctx.db;
4940
5377
  const [
@@ -5004,6 +5441,13 @@ function githubPlugin(options = {}) {
5004
5441
  nowMs: ctx.nowMs
5005
5442
  });
5006
5443
  },
5444
+ async profileReport(ctx) {
5445
+ return await buildGitHubProfileReport({
5446
+ db: ctx.db,
5447
+ nowMs: ctx.nowMs,
5448
+ userId: ctx.subject.id
5449
+ });
5450
+ },
5007
5451
  tools(ctx) {
5008
5452
  return createGitHubTools(ctx);
5009
5453
  },
@@ -30,8 +30,13 @@ export type GitHubIssueConversationsInput = z.output<typeof githubIssueConversat
30
30
  * Record the newest lifecycle projection for one Junior-owned issue.
31
31
  * Ownership-qualified opening or closing events insert; later events update
32
32
  * existing rows, and older provider timestamps cannot regress them.
33
+ * Returns the written row state so follow-up annotation updates stay scoped to
34
+ * associated conversations.
33
35
  */
34
- export declare function recordGitHubIssueOutcome(db: GitHubDb, input: GitHubIssueOutcomeInput): Promise<void>;
36
+ export declare function recordGitHubIssueOutcome(db: GitHubDb, input: GitHubIssueOutcomeInput): Promise<{
37
+ applied: boolean;
38
+ conversationIds: string[];
39
+ }>;
35
40
  /** Append native conversation ids to an existing Junior-owned issue projection. */
36
41
  export declare function recordGitHubIssueConversations(db: GitHubDb, input: GitHubIssueConversationsInput): Promise<boolean>;
37
42
  export {};
@@ -0,0 +1,11 @@
1
+ import type { PluginOperationalReportContent } from "@sentry/junior-plugin-api";
2
+ import type { GitHubDb } from "../db/database.js";
3
+ /**
4
+ * Build one person-scoped GitHub report for Junior-owned work linked to the
5
+ * subject's conversations.
6
+ */
7
+ export declare function buildGitHubProfileReport(args: {
8
+ db: GitHubDb;
9
+ nowMs: number;
10
+ userId: string;
11
+ }): Promise<PluginOperationalReportContent | undefined>;
@@ -0,0 +1,2 @@
1
+ /** Linkify GitHub issue and pull request shorthand outside Markdown code. */
2
+ export declare function linkifyGitHubReferences(text: string): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-github",
3
- "version": "0.159.0",
3
+ "version": "0.161.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.159.0"
34
+ "@sentry/junior-plugin-api": "0.161.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^25.9.1",
@@ -81,5 +81,3 @@ If PR creation or update is blocked, report the exact failed command/tool call a
81
81
  When PR creation returns a subscribable resource hint, subscribe to suggested review/CI events. Report only actionable feedback addressed, build failures fixed, fully green/ready state, or merge.
82
82
 
83
83
  Return: repo, branch, PR URL/number, checks and results, pre-existing failures, and anything not run with the reason.
84
-
85
- When you mention a pull request in a user-facing reply, always include a direct link. Prefer the full PR URL or a Markdown link such as `[#123](https://github.com/owner/repo/pull/123)`. `owner/repo#number` is also fine. Do not leave bare `PR #123` text without a URL or repo.
@@ -80,4 +80,5 @@ jr-rpc config set github.repo owner/repo
80
80
  - 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.
81
81
  - 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
82
  - 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
+ - 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.
83
84
  - Return actionable errors for access, permission, not-found, and validation failures.
@@ -76,7 +76,7 @@ Follow [references/research-rules.md](references/research-rules.md) for cross-ty
76
76
 
77
77
  - The runtime adds the verified `Requested by` block. Do not add or rewrite requester attribution in model-authored body text.
78
78
  - If the person who originally reported or observed the problem differs from the issue creator, capture that with durable body text such as `Reported by Alice.` or `Raised by Alice during incident triage.`
79
- - Attach screenshots from the thread as image links when present.
79
+ - Attach screenshots from the thread when present. For GitHub Markdown, first publish each local image with `publishImage` (the image becomes public to anyone with the URL), then embed the returned URL in the issue body or comment. Do not use private Slack file links or conversation attachment URLs.
80
80
  - Include code snippets, related issues, and related PRs only when they materially improve the issue.
81
81
 
82
82
  ### 4. Verify draft