@sentry/junior-github 0.108.0 → 0.110.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/SETUP.md CHANGED
@@ -133,8 +133,8 @@ Use `additionalUserScopes` only when a human-identity integration flow requires
133
133
  ## 3) Runtime behavior
134
134
 
135
135
  - When either GitHub skill is active, authenticated `gh` and `git` commands cause the runtime to inject GitHub credentials automatically for the current turn.
136
- - The plugin classifies GitHub traffic from the forwarded HTTP request. Reads use `installation-read`, while `GET /user` uses `user-read`. Allowlisted App-owned mutations, Git smart-HTTP pushes, and user-attachment uploads to `uploads.github.com/user-attachments/assets` use repository-scoped `installation-write`. Unknown REST writes and GraphQL mutations are denied.
137
- - `user-read` and explicitly human `user-write` operations require the actor, or an explicitly delegated user subject, to authorize the GitHub App through the private OAuth flow. Junior-owned issue, pull request, branch, and asset-upload operations do not fall back to user OAuth.
136
+ - The plugin classifies GitHub traffic from the forwarded HTTP request. Reads use `installation-read`, while `GET /user` uses `user-read`. Allowlisted App-owned mutations and Git smart-HTTP pushes use repository-scoped `installation-write`. User-attachment uploads to `uploads.github.com/user-attachments/assets` use `user-write`. Unknown REST writes and GraphQL mutations are denied.
137
+ - `user-read` and explicitly human `user-write` operations require the actor, or an explicitly delegated user subject, to authorize the GitHub App through the private OAuth flow. Junior-owned issue, pull request, and branch operations do not fall back to user OAuth.
138
138
  - Headless resource-event turns use the `resource-event` system actor and may receive the same repository-scoped installation grants. This lets Junior respond to subscribed pull request events by committing and pushing fixes without inheriting a subscriber's OAuth credential.
139
139
  - Git commits use Junior as author and committer. Resolvable human run actors are credited once with `Co-Authored-By` trailers.
140
140
  - Issued credentials are reused only within the current turn, credential leases are cached by plugin grant and repository lease scope, and upstream 403 permission denials clear the cached lease before the next retry.
@@ -15,6 +15,13 @@ export declare const githubIssueStateSchema: z.ZodEnum<{
15
15
  closed: "closed";
16
16
  }>;
17
17
  export type GitHubIssueState = z.output<typeof githubIssueStateSchema>;
18
+ export declare const githubIssueStateReasonSchema: z.ZodEnum<{
19
+ completed: "completed";
20
+ duplicate: "duplicate";
21
+ not_planned: "not_planned";
22
+ reopened: "reopened";
23
+ }>;
24
+ export type GitHubIssueStateReason = z.output<typeof githubIssueStateReasonSchema>;
18
25
  /** Current provider projection for issues classified as Junior-owned. */
19
26
  export declare const juniorGitHubIssues: import("drizzle-orm/pg-core").PgTableWithColumns<{
20
27
  name: "junior_github_issues";
@@ -107,6 +114,25 @@ export declare const juniorGitHubIssues: import("drizzle-orm/pg-core").PgTableWi
107
114
  }, {}, {
108
115
  $type: "open" | "closed";
109
116
  }>;
117
+ stateReason: import("drizzle-orm/pg-core").PgColumn<{
118
+ name: "state_reason";
119
+ tableName: "junior_github_issues";
120
+ dataType: "string";
121
+ columnType: "PgText";
122
+ data: "completed" | "duplicate" | "not_planned" | "reopened";
123
+ driverParam: string;
124
+ notNull: false;
125
+ hasDefault: false;
126
+ isPrimaryKey: false;
127
+ isAutoincrement: false;
128
+ hasRuntimeDefault: false;
129
+ enumValues: [string, ...string[]];
130
+ baseColumn: never;
131
+ identity: undefined;
132
+ generated: undefined;
133
+ }, {}, {
134
+ $type: "completed" | "duplicate" | "not_planned" | "reopened";
135
+ }>;
110
136
  openedAt: import("drizzle-orm/pg-core").PgColumn<{
111
137
  name: "opened_at";
112
138
  tableName: "junior_github_issues";
@@ -478,6 +504,25 @@ export declare const githubSqlSchema: {
478
504
  }, {}, {
479
505
  $type: "open" | "closed";
480
506
  }>;
507
+ stateReason: import("drizzle-orm/pg-core").PgColumn<{
508
+ name: "state_reason";
509
+ tableName: "junior_github_issues";
510
+ dataType: "string";
511
+ columnType: "PgText";
512
+ data: "completed" | "duplicate" | "not_planned" | "reopened";
513
+ driverParam: string;
514
+ notNull: false;
515
+ hasDefault: false;
516
+ isPrimaryKey: false;
517
+ isAutoincrement: false;
518
+ hasRuntimeDefault: false;
519
+ enumValues: [string, ...string[]];
520
+ baseColumn: never;
521
+ identity: undefined;
522
+ generated: undefined;
523
+ }, {}, {
524
+ $type: "completed" | "duplicate" | "not_planned" | "reopened";
525
+ }>;
481
526
  openedAt: import("drizzle-orm/pg-core").PgColumn<{
482
527
  name: "opened_at";
483
528
  tableName: "junior_github_issues";
package/dist/index.js CHANGED
@@ -1113,6 +1113,12 @@ var githubPullRequestCommitCompositionSchema = z5.enum([
1113
1113
  "mixed"
1114
1114
  ]);
1115
1115
  var githubIssueStateSchema = z5.enum(["closed", "open"]);
1116
+ var githubIssueStateReasonSchema = z5.enum([
1117
+ "completed",
1118
+ "duplicate",
1119
+ "not_planned",
1120
+ "reopened"
1121
+ ]);
1116
1122
  var juniorGitHubIssues = pgTable(
1117
1123
  "junior_github_issues",
1118
1124
  {
@@ -1121,6 +1127,7 @@ var juniorGitHubIssues = pgTable(
1121
1127
  repositoryFullName: text("repository_full_name").notNull(),
1122
1128
  number: integer("number").notNull(),
1123
1129
  state: text("state").$type().notNull(),
1130
+ stateReason: text("state_reason").$type(),
1124
1131
  openedAt: timestamp("opened_at", { withTimezone: true }).notNull(),
1125
1132
  closedAt: timestamp("closed_at", { withTimezone: true }),
1126
1133
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull()
@@ -1164,6 +1171,7 @@ var githubIssueOutcomeInputSchema = z6.object({
1164
1171
  repositoryFullName: z6.string().min(1),
1165
1172
  repositoryId: z6.string().min(1),
1166
1173
  state: githubIssueStateSchema,
1174
+ stateReason: githubIssueStateReasonSchema.optional(),
1167
1175
  updatedAt: z6.date()
1168
1176
  }).strict();
1169
1177
  function projectionValues(input) {
@@ -1174,6 +1182,7 @@ function projectionValues(input) {
1174
1182
  repositoryFullName: input.repositoryFullName,
1175
1183
  repositoryId: input.repositoryId,
1176
1184
  state: input.state,
1185
+ stateReason: input.stateReason ?? null,
1177
1186
  updatedAt: input.updatedAt
1178
1187
  };
1179
1188
  }
@@ -1305,6 +1314,7 @@ var canonicalIssueOutcomeSchema = z8.object({
1305
1314
  created_at: z8.string(),
1306
1315
  id: z8.number().int().positive(),
1307
1316
  number: z8.number().int().positive(),
1317
+ state_reason: githubIssueStateReasonSchema.nullable().optional(),
1308
1318
  updated_at: z8.string(),
1309
1319
  user: z8.object({ login: z8.string().min(1) }).strict()
1310
1320
  }).strict(),
@@ -1321,6 +1331,7 @@ var issueOutcomeSchema = z8.object({
1321
1331
  created_at: z8.string(),
1322
1332
  id: z8.number().int().positive(),
1323
1333
  number: z8.number().int().positive(),
1334
+ state_reason: githubIssueStateReasonSchema.nullable().optional(),
1324
1335
  updated_at: z8.string(),
1325
1336
  user: z8.object({ login: z8.string().min(1) }).passthrough()
1326
1337
  }).passthrough(),
@@ -1337,6 +1348,7 @@ var issueOutcomeSchema = z8.object({
1337
1348
  created_at: provider.issue.created_at,
1338
1349
  id: provider.issue.id,
1339
1350
  number: provider.issue.number,
1351
+ state_reason: provider.issue.state_reason,
1340
1352
  updated_at: provider.issue.updated_at,
1341
1353
  user: { login: provider.issue.user.login }
1342
1354
  },
@@ -1388,6 +1400,7 @@ function normalizeGitHubIssueOutcome(args) {
1388
1400
  repositoryFullName: parsed.repository.full_name,
1389
1401
  repositoryId: String(parsed.repository.id),
1390
1402
  state: parsed.action === "closed" ? "closed" : "open",
1403
+ stateReason: parsed.action === "closed" ? issue.state_reason ?? void 0 : void 0,
1391
1404
  updatedAt
1392
1405
  };
1393
1406
  }
@@ -1636,8 +1649,7 @@ var pullRequestStatsSchema = z10.object({
1636
1649
  juniorOnly: z10.number().int().nonnegative(),
1637
1650
  medianMergeTimeMs: z10.number().nonnegative().nullable(),
1638
1651
  merged: z10.number().int().nonnegative(),
1639
- mixed: z10.number().int().nonnegative(),
1640
- open: z10.number().int().nonnegative()
1652
+ mixed: z10.number().int().nonnegative()
1641
1653
  }).strict().transform((row) => {
1642
1654
  const terminal = row.merged + row.closed;
1643
1655
  const classified = row.juniorOnly + row.mixed;
@@ -1655,7 +1667,6 @@ var pullRequestRepositoryStatsSchema = z10.object({
1655
1667
  juniorOnly: z10.number().int().nonnegative(),
1656
1668
  merged: z10.number().int().nonnegative(),
1657
1669
  mixed: z10.number().int().nonnegative(),
1658
- open: z10.number().int().nonnegative(),
1659
1670
  repository: z10.string().min(1)
1660
1671
  }).strict().transform((row) => {
1661
1672
  const terminal = row.merged + row.closed;
@@ -1665,19 +1676,37 @@ var pullRequestRepositoryStatsSchema = z10.object({
1665
1676
  };
1666
1677
  });
1667
1678
  var issueStatsSchema = z10.object({
1668
- closed: z10.number().int().nonnegative(),
1679
+ closedCompleted: z10.number().int().nonnegative(),
1680
+ closedDuplicate: z10.number().int().nonnegative(),
1681
+ closedNotPlanned: z10.number().int().nonnegative(),
1682
+ closedUnknown: z10.number().int().nonnegative(),
1669
1683
  created: z10.number().int().nonnegative(),
1670
1684
  days: z10.number().int().positive(),
1671
- medianCloseTimeMs: z10.number().nonnegative().nullable(),
1672
- open: z10.number().int().nonnegative()
1685
+ medianCloseTimeMs: z10.number().nonnegative().nullable()
1673
1686
  }).strict().transform((row) => ({
1674
1687
  ...row,
1675
1688
  medianCloseTimeMs: row.medianCloseTimeMs ?? void 0
1676
1689
  }));
1677
- var issueRepositoryStatsSchema = z10.object({
1690
+ var pullRequestDaySchema = z10.object({
1678
1691
  closed: z10.number().int().nonnegative(),
1679
1692
  created: z10.number().int().nonnegative(),
1680
- open: z10.number().int().nonnegative(),
1693
+ date: z10.string().date(),
1694
+ merged: z10.number().int().nonnegative()
1695
+ }).strict();
1696
+ var issueDaySchema = z10.object({
1697
+ closedCompleted: z10.number().int().nonnegative(),
1698
+ closedDuplicate: z10.number().int().nonnegative(),
1699
+ closedNotPlanned: z10.number().int().nonnegative(),
1700
+ closedUnknown: z10.number().int().nonnegative(),
1701
+ created: z10.number().int().nonnegative(),
1702
+ date: z10.string().date()
1703
+ }).strict();
1704
+ var issueRepositoryStatsSchema = z10.object({
1705
+ closedCompleted: z10.number().int().nonnegative(),
1706
+ closedDuplicate: z10.number().int().nonnegative(),
1707
+ closedNotPlanned: z10.number().int().nonnegative(),
1708
+ closedUnknown: z10.number().int().nonnegative(),
1709
+ created: z10.number().int().nonnegative(),
1681
1710
  repository: z10.string().min(1)
1682
1711
  }).strict();
1683
1712
  function queryRows(result) {
@@ -1707,8 +1736,7 @@ async function aggregatePullRequestWindows(args) {
1707
1736
  ${table.mergedAt},
1708
1737
  ${table.closedAt}
1709
1738
  FROM ${table}
1710
- WHERE ${table.state} = 'open'
1711
- OR ${table.openedAt} >= ${oldestStart}
1739
+ WHERE ${table.openedAt} >= ${oldestStart}
1712
1740
  OR ${table.mergedAt} >= ${oldestStart}
1713
1741
  OR ${table.closedAt} >= ${oldestStart}
1714
1742
  )
@@ -1727,8 +1755,6 @@ async function aggregatePullRequestWindows(args) {
1727
1755
  WHERE recent_pull_requests.state = 'closed_unmerged'
1728
1756
  AND recent_pull_requests.closed_at >= windows.start_at
1729
1757
  )::integer AS "closed",
1730
- count(recent_pull_requests.pull_request_id)
1731
- FILTER (WHERE recent_pull_requests.state = 'open')::integer AS "open",
1732
1758
  count(recent_pull_requests.pull_request_id)
1733
1759
  FILTER (
1734
1760
  WHERE recent_pull_requests.state = 'merged'
@@ -1766,6 +1792,55 @@ async function aggregatePullRequestWindows(args) {
1766
1792
  `);
1767
1793
  return z10.array(pullRequestStatsSchema).parse(queryRows(result));
1768
1794
  }
1795
+ async function aggregatePullRequestDays(args) {
1796
+ const end = new Date(args.nowMs);
1797
+ const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) * DAY_MS);
1798
+ const table = juniorGitHubPullRequests;
1799
+ const result = await args.db.execute(sql3`
1800
+ WITH days AS (
1801
+ SELECT generate_series(
1802
+ date_trunc('day', ${start}::timestamptz AT TIME ZONE 'UTC'),
1803
+ date_trunc('day', ${end}::timestamptz AT TIME ZONE 'UTC'),
1804
+ interval '1 day'
1805
+ ) AS day
1806
+ ), events AS (
1807
+ SELECT
1808
+ date_trunc('day', ${table.openedAt} AT TIME ZONE 'UTC') AS day,
1809
+ 'created' AS kind
1810
+ FROM ${table}
1811
+ WHERE ${table.openedAt} >= ${start}
1812
+ UNION ALL
1813
+ SELECT
1814
+ date_trunc('day', ${table.mergedAt} AT TIME ZONE 'UTC') AS day,
1815
+ 'merged' AS kind
1816
+ FROM ${table}
1817
+ WHERE ${table.state} = 'merged' AND ${table.mergedAt} >= ${start}
1818
+ UNION ALL
1819
+ SELECT
1820
+ date_trunc('day', ${table.closedAt} AT TIME ZONE 'UTC') AS day,
1821
+ 'closed' AS kind
1822
+ FROM ${table}
1823
+ WHERE ${table.state} = 'closed_unmerged' AND ${table.closedAt} >= ${start}
1824
+ ), daily AS (
1825
+ SELECT
1826
+ events.day,
1827
+ count(*) FILTER (WHERE events.kind = 'created')::integer AS created,
1828
+ count(*) FILTER (WHERE events.kind = 'merged')::integer AS merged,
1829
+ count(*) FILTER (WHERE events.kind = 'closed')::integer AS closed
1830
+ FROM events
1831
+ GROUP BY events.day
1832
+ )
1833
+ SELECT
1834
+ to_char(days.day, 'YYYY-MM-DD') AS "date",
1835
+ coalesce(daily.created, 0)::integer AS "created",
1836
+ coalesce(daily.merged, 0)::integer AS "merged",
1837
+ coalesce(daily.closed, 0)::integer AS "closed"
1838
+ FROM days
1839
+ LEFT JOIN daily ON daily.day = days.day
1840
+ ORDER BY days.day
1841
+ `);
1842
+ return z10.array(pullRequestDaySchema).parse(queryRows(result));
1843
+ }
1769
1844
  async function aggregatePullRequestRepositories(args) {
1770
1845
  const start = new Date(args.nowMs - 30 * DAY_MS);
1771
1846
  const table = juniorGitHubPullRequests;
@@ -1781,7 +1856,6 @@ async function aggregatePullRequestRepositories(args) {
1781
1856
  WHERE ${table.state} = 'closed_unmerged'
1782
1857
  AND ${table.closedAt} >= ${start}
1783
1858
  )::integer AS "closed",
1784
- count(*) FILTER (WHERE ${table.state} = 'open')::integer AS "open",
1785
1859
  count(*) FILTER (
1786
1860
  WHERE ${table.state} = 'merged'
1787
1861
  AND ${table.mergedAt} >= ${start}
@@ -1798,8 +1872,7 @@ async function aggregatePullRequestRepositories(args) {
1798
1872
  AND ${table.commitComposition} IS NULL
1799
1873
  )::integer AS "compositionUnknown"
1800
1874
  FROM ${table}
1801
- WHERE ${table.state} = 'open'
1802
- OR ${table.openedAt} >= ${start}
1875
+ WHERE ${table.openedAt} >= ${start}
1803
1876
  OR ${table.mergedAt} >= ${start}
1804
1877
  OR ${table.closedAt} >= ${start}
1805
1878
  GROUP BY ${table.repositoryFullName}
@@ -1824,11 +1897,11 @@ async function aggregateIssueWindows(args) {
1824
1897
  SELECT
1825
1898
  ${table.issueId},
1826
1899
  ${table.state},
1900
+ ${table.stateReason},
1827
1901
  ${table.openedAt},
1828
1902
  ${table.closedAt}
1829
1903
  FROM ${table}
1830
- WHERE ${table.state} = 'open'
1831
- OR ${table.openedAt} >= ${oldestStart}
1904
+ WHERE ${table.openedAt} >= ${oldestStart}
1832
1905
  OR ${table.closedAt} >= ${oldestStart}
1833
1906
  )
1834
1907
  SELECT
@@ -1839,10 +1912,27 @@ async function aggregateIssueWindows(args) {
1839
1912
  count(recent_issues.issue_id)
1840
1913
  FILTER (
1841
1914
  WHERE recent_issues.state = 'closed'
1915
+ AND recent_issues.state_reason = 'completed'
1842
1916
  AND recent_issues.closed_at >= windows.start_at
1843
- )::integer AS "closed",
1917
+ )::integer AS "closedCompleted",
1918
+ count(recent_issues.issue_id)
1919
+ FILTER (
1920
+ WHERE recent_issues.state = 'closed'
1921
+ AND recent_issues.state_reason = 'duplicate'
1922
+ AND recent_issues.closed_at >= windows.start_at
1923
+ )::integer AS "closedDuplicate",
1844
1924
  count(recent_issues.issue_id)
1845
- FILTER (WHERE recent_issues.state = 'open')::integer AS "open",
1925
+ FILTER (
1926
+ WHERE recent_issues.state = 'closed'
1927
+ AND recent_issues.state_reason = 'not_planned'
1928
+ AND recent_issues.closed_at >= windows.start_at
1929
+ )::integer AS "closedNotPlanned",
1930
+ count(recent_issues.issue_id)
1931
+ FILTER (
1932
+ WHERE recent_issues.state = 'closed'
1933
+ AND recent_issues.state_reason IS NULL
1934
+ AND recent_issues.closed_at >= windows.start_at
1935
+ )::integer AS "closedUnknown",
1846
1936
  (
1847
1937
  percentile_cont(0.5) WITHIN GROUP (
1848
1938
  ORDER BY extract(
@@ -1860,6 +1950,57 @@ async function aggregateIssueWindows(args) {
1860
1950
  `);
1861
1951
  return z10.array(issueStatsSchema).parse(queryRows(result));
1862
1952
  }
1953
+ async function aggregateIssueDays(args) {
1954
+ const end = new Date(args.nowMs);
1955
+ const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) * DAY_MS);
1956
+ const table = juniorGitHubIssues;
1957
+ const result = await args.db.execute(sql3`
1958
+ WITH days AS (
1959
+ SELECT generate_series(
1960
+ date_trunc('day', ${start}::timestamptz AT TIME ZONE 'UTC'),
1961
+ date_trunc('day', ${end}::timestamptz AT TIME ZONE 'UTC'),
1962
+ interval '1 day'
1963
+ ) AS day
1964
+ ), events AS (
1965
+ SELECT
1966
+ date_trunc('day', ${table.openedAt} AT TIME ZONE 'UTC') AS day,
1967
+ 'created' AS kind
1968
+ FROM ${table}
1969
+ WHERE ${table.openedAt} >= ${start}
1970
+ UNION ALL
1971
+ SELECT
1972
+ date_trunc('day', ${table.closedAt} AT TIME ZONE 'UTC') AS day,
1973
+ coalesce(${table.stateReason}, 'unknown') AS kind
1974
+ FROM ${table}
1975
+ WHERE ${table.state} = 'closed' AND ${table.closedAt} >= ${start}
1976
+ ), daily AS (
1977
+ SELECT
1978
+ events.day,
1979
+ count(*) FILTER (WHERE events.kind = 'created')::integer AS created,
1980
+ count(*) FILTER (WHERE events.kind = 'completed')::integer
1981
+ AS closed_completed,
1982
+ count(*) FILTER (WHERE events.kind = 'duplicate')::integer
1983
+ AS closed_duplicate,
1984
+ count(*) FILTER (WHERE events.kind = 'not_planned')::integer
1985
+ AS closed_not_planned,
1986
+ count(*) FILTER (WHERE events.kind = 'unknown')::integer
1987
+ AS closed_unknown
1988
+ FROM events
1989
+ GROUP BY events.day
1990
+ )
1991
+ SELECT
1992
+ to_char(days.day, 'YYYY-MM-DD') AS "date",
1993
+ coalesce(daily.created, 0)::integer AS "created",
1994
+ coalesce(daily.closed_completed, 0)::integer AS "closedCompleted",
1995
+ coalesce(daily.closed_duplicate, 0)::integer AS "closedDuplicate",
1996
+ coalesce(daily.closed_not_planned, 0)::integer AS "closedNotPlanned",
1997
+ coalesce(daily.closed_unknown, 0)::integer AS "closedUnknown"
1998
+ FROM days
1999
+ LEFT JOIN daily ON daily.day = days.day
2000
+ ORDER BY days.day
2001
+ `);
2002
+ return z10.array(issueDaySchema).parse(queryRows(result));
2003
+ }
1863
2004
  async function aggregateIssueRepositories(args) {
1864
2005
  const start = new Date(args.nowMs - 30 * DAY_MS);
1865
2006
  const table = juniorGitHubIssues;
@@ -1869,15 +2010,30 @@ async function aggregateIssueRepositories(args) {
1869
2010
  count(*) FILTER (WHERE ${table.openedAt} >= ${start})::integer
1870
2011
  AS "created",
1871
2012
  count(*) FILTER (
1872
- WHERE ${table.state} = 'closed' AND ${table.closedAt} >= ${start}
1873
- )::integer AS "closed",
1874
- count(*) FILTER (WHERE ${table.state} = 'open')::integer AS "open"
2013
+ WHERE ${table.state} = 'closed'
2014
+ AND ${table.stateReason} = 'completed'
2015
+ AND ${table.closedAt} >= ${start}
2016
+ )::integer AS "closedCompleted",
2017
+ count(*) FILTER (
2018
+ WHERE ${table.state} = 'closed'
2019
+ AND ${table.stateReason} = 'duplicate'
2020
+ AND ${table.closedAt} >= ${start}
2021
+ )::integer AS "closedDuplicate",
2022
+ count(*) FILTER (
2023
+ WHERE ${table.state} = 'closed'
2024
+ AND ${table.stateReason} = 'not_planned'
2025
+ AND ${table.closedAt} >= ${start}
2026
+ )::integer AS "closedNotPlanned",
2027
+ count(*) FILTER (
2028
+ WHERE ${table.state} = 'closed'
2029
+ AND ${table.stateReason} IS NULL
2030
+ AND ${table.closedAt} >= ${start}
2031
+ )::integer AS "closedUnknown"
1875
2032
  FROM ${table}
1876
- WHERE ${table.state} = 'open'
1877
- OR ${table.openedAt} >= ${start}
2033
+ WHERE ${table.openedAt} >= ${start}
1878
2034
  OR ${table.closedAt} >= ${start}
1879
2035
  GROUP BY ${table.repositoryFullName}
1880
- ORDER BY "created" DESC, "closed" DESC, "repository" ASC
2036
+ ORDER BY "created" DESC, "closedCompleted" DESC, "repository" ASC
1881
2037
  LIMIT 25
1882
2038
  `);
1883
2039
  return z10.array(issueRepositoryStatsSchema).parse(queryRows(result));
@@ -1895,11 +2051,25 @@ function formatDuration(value) {
1895
2051
  function formatCommitComposition(stats) {
1896
2052
  return `${stats.juniorOnly} Junior-only \xB7 ${stats.mixed} mixed \xB7 ${stats.compositionUnknown} unknown`;
1897
2053
  }
2054
+ function startOfUtcDay(timestampMs) {
2055
+ const date = new Date(timestampMs);
2056
+ date.setUTCHours(0, 0, 0, 0);
2057
+ return date;
2058
+ }
1898
2059
  async function buildGitHubOutcomeReport(args) {
1899
- const [windows, repositories, issueWindows, issueRepositories] = await Promise.all([
2060
+ const [
2061
+ windows,
2062
+ pullRequestDays,
2063
+ repositories,
2064
+ issueWindows,
2065
+ issueDays,
2066
+ issueRepositories
2067
+ ] = await Promise.all([
1900
2068
  aggregatePullRequestWindows(args),
2069
+ aggregatePullRequestDays(args),
1901
2070
  aggregatePullRequestRepositories(args),
1902
2071
  aggregateIssueWindows(args),
2072
+ aggregateIssueDays(args),
1903
2073
  aggregateIssueRepositories(args)
1904
2074
  ]);
1905
2075
  const thirtyDays = windows.find((window) => window.days === 30);
@@ -1908,17 +2078,10 @@ async function buildGitHubOutcomeReport(args) {
1908
2078
  generatedAt: new Date(args.nowMs).toISOString(),
1909
2079
  title: "GitHub work delivered",
1910
2080
  metrics: [
1911
- { label: "PRs created \xB7 30d", value: String(thirtyDays.created) },
1912
- {
1913
- label: "PRs merged \xB7 30d",
1914
- tone: thirtyDays.merged > 0 ? "good" : "neutral",
1915
- value: String(thirtyDays.merged)
1916
- },
1917
2081
  {
1918
2082
  label: "Junior-only merge rate \xB7 30d",
1919
2083
  value: formatPercent(thirtyDays.juniorOnlyRate)
1920
2084
  },
1921
- { label: "PRs open now", value: String(thirtyDays.open) },
1922
2085
  {
1923
2086
  label: "PR merge rate \xB7 30d",
1924
2087
  value: formatPercent(thirtyDays.mergeRate)
@@ -1927,34 +2090,60 @@ async function buildGitHubOutcomeReport(args) {
1927
2090
  label: "median PR merge time \xB7 30d",
1928
2091
  value: formatDuration(thirtyDays.medianMergeTimeMs)
1929
2092
  },
1930
- { label: "issues created \xB7 30d", value: String(issueThirtyDays.created) },
1931
- { label: "issues open now", value: String(issueThirtyDays.open) }
2093
+ {
2094
+ label: "median issue close time \xB7 30d",
2095
+ value: formatDuration(issueThirtyDays.medianCloseTimeMs)
2096
+ }
1932
2097
  ],
1933
- recordSets: [
2098
+ widgets: [
1934
2099
  {
1935
- title: "Pull request outcome windows",
1936
- fields: [
1937
- { key: "window", label: "Window" },
2100
+ id: "pull-request-outcomes",
2101
+ type: "bar_chart",
2102
+ title: "Pull request outcomes",
2103
+ description: "Daily outcomes",
2104
+ timeRangeDays: [...WINDOWS],
2105
+ series: [
1938
2106
  { key: "created", label: "Created" },
1939
- { key: "merged", label: "Merged" },
1940
- { key: "closed", label: "Closed unmerged" },
1941
- { key: "commitComposition", label: "Merged commit composition" },
1942
- { key: "mergeRate", label: "Merge rate" },
1943
- { key: "mergeTime", label: "Median merge time" }
2107
+ { key: "merged", label: "Merged", tone: "good" },
2108
+ { key: "closed", label: "Closed unmerged", tone: "danger" }
1944
2109
  ],
1945
- records: windows.map((stats) => ({
1946
- id: `${stats.days}d`,
2110
+ categories: pullRequestDays.map((stats) => ({
2111
+ id: stats.date,
2112
+ label: stats.date,
1947
2113
  values: {
1948
- window: `${stats.days} days`,
1949
- created: String(stats.created),
1950
- merged: String(stats.merged),
1951
- closed: String(stats.closed),
1952
- commitComposition: formatCommitComposition(stats),
1953
- mergeRate: formatPercent(stats.mergeRate),
1954
- mergeTime: formatDuration(stats.medianMergeTimeMs)
2114
+ created: stats.created,
2115
+ merged: stats.merged,
2116
+ closed: stats.closed
1955
2117
  }
1956
2118
  }))
1957
2119
  },
2120
+ {
2121
+ id: "issue-outcomes",
2122
+ type: "bar_chart",
2123
+ title: "Issue outcomes",
2124
+ description: "Daily outcomes",
2125
+ timeRangeDays: [...WINDOWS],
2126
+ series: [
2127
+ { key: "created", label: "Created" },
2128
+ { key: "completed", label: "Completed", tone: "good" },
2129
+ { key: "duplicate", label: "Duplicate", tone: "warning" },
2130
+ { key: "notPlanned", label: "Not planned", tone: "danger" },
2131
+ { key: "unknown", label: "Unknown" }
2132
+ ],
2133
+ categories: issueDays.map((stats) => ({
2134
+ id: stats.date,
2135
+ label: stats.date,
2136
+ values: {
2137
+ created: stats.created,
2138
+ completed: stats.closedCompleted,
2139
+ duplicate: stats.closedDuplicate,
2140
+ notPlanned: stats.closedNotPlanned,
2141
+ unknown: stats.closedUnknown
2142
+ }
2143
+ }))
2144
+ }
2145
+ ],
2146
+ recordSets: [
1958
2147
  {
1959
2148
  title: "Pull request repositories \xB7 30d",
1960
2149
  emptyText: "No Junior-owned pull request activity yet.",
@@ -1963,7 +2152,6 @@ async function buildGitHubOutcomeReport(args) {
1963
2152
  { key: "created", label: "Created" },
1964
2153
  { key: "merged", label: "Merged" },
1965
2154
  { key: "closed", label: "Closed unmerged" },
1966
- { key: "open", label: "Open now" },
1967
2155
  { key: "commitComposition", label: "Merged commit composition" },
1968
2156
  { key: "mergeRate", label: "Merge rate" }
1969
2157
  ],
@@ -1974,48 +2162,31 @@ async function buildGitHubOutcomeReport(args) {
1974
2162
  created: String(stats.created),
1975
2163
  merged: String(stats.merged),
1976
2164
  closed: String(stats.closed),
1977
- open: String(stats.open),
1978
2165
  commitComposition: formatCommitComposition(stats),
1979
2166
  mergeRate: formatPercent(stats.mergeRate)
1980
2167
  }
1981
2168
  }))
1982
2169
  },
1983
- {
1984
- title: "Issue outcome windows",
1985
- fields: [
1986
- { key: "window", label: "Window" },
1987
- { key: "created", label: "Created" },
1988
- { key: "closed", label: "Closed" },
1989
- { key: "open", label: "Open now" },
1990
- { key: "closeTime", label: "Median close time" }
1991
- ],
1992
- records: issueWindows.map((stats) => ({
1993
- id: `${stats.days}d`,
1994
- values: {
1995
- window: `${stats.days} days`,
1996
- created: String(stats.created),
1997
- closed: String(stats.closed),
1998
- open: String(stats.open),
1999
- closeTime: formatDuration(stats.medianCloseTimeMs)
2000
- }
2001
- }))
2002
- },
2003
2170
  {
2004
2171
  title: "Issue repositories \xB7 30d",
2005
2172
  emptyText: "No Junior-owned issue activity yet.",
2006
2173
  fields: [
2007
2174
  { key: "repository", label: "Repository" },
2008
2175
  { key: "created", label: "Created" },
2009
- { key: "closed", label: "Closed" },
2010
- { key: "open", label: "Open now" }
2176
+ { key: "completed", label: "Completed" },
2177
+ { key: "duplicate", label: "Duplicate" },
2178
+ { key: "notPlanned", label: "Not planned" },
2179
+ { key: "unknown", label: "Unknown reason" }
2011
2180
  ],
2012
2181
  records: issueRepositories.map(({ repository, ...stats }) => ({
2013
2182
  id: repository,
2014
2183
  values: {
2015
2184
  repository,
2016
2185
  created: String(stats.created),
2017
- closed: String(stats.closed),
2018
- open: String(stats.open)
2186
+ completed: String(stats.closedCompleted),
2187
+ duplicate: String(stats.closedDuplicate),
2188
+ notPlanned: String(stats.closedNotPlanned),
2189
+ unknown: String(stats.closedUnknown)
2019
2190
  }
2020
2191
  }))
2021
2192
  }
@@ -2254,6 +2425,52 @@ if [ -n "\${JUNIOR_GIT_ACTOR_COAUTHOR_TRAILERS:-}" ]; then
2254
2425
  done <<< "$JUNIOR_GIT_ACTOR_COAUTHOR_TRAILERS"
2255
2426
  fi
2256
2427
 
2428
+ # The hook owns co-author attribution. Remove every model-supplied co-author
2429
+ # from the final Git trailer block before appending the host-derived actors.
2430
+ tmp_file=$(mktemp)
2431
+ trap 'rm -f "$tmp_file"' EXIT
2432
+ awk '
2433
+ { lines[NR] = $0 }
2434
+ END {
2435
+ line = NR
2436
+ while (line > 0 && lines[line] == "") {
2437
+ line--
2438
+ }
2439
+ start = line
2440
+ while (start > 0 && lines[start] != "") {
2441
+ start--
2442
+ }
2443
+ valid = line > 0
2444
+ for (i = start + 1; valid && i <= line; i++) {
2445
+ if (lines[i] !~ /^[[:alnum:]-]+: .+/) {
2446
+ valid = 0
2447
+ }
2448
+ }
2449
+ if (!valid) {
2450
+ for (i = 1; i <= NR; i++) {
2451
+ print lines[i]
2452
+ }
2453
+ exit 0
2454
+ }
2455
+ kept = 0
2456
+ for (i = start + 1; i <= line; i++) {
2457
+ if (tolower(lines[i]) !~ /^co-authored-by: /) {
2458
+ kept++
2459
+ }
2460
+ }
2461
+ before = kept > 0 ? start : start - 1
2462
+ for (i = 1; i <= before; i++) {
2463
+ print lines[i]
2464
+ }
2465
+ for (i = start + 1; i <= line; i++) {
2466
+ if (tolower(lines[i]) !~ /^co-authored-by: /) {
2467
+ print lines[i]
2468
+ }
2469
+ }
2470
+ }
2471
+ ' "$message_file" > "$tmp_file"
2472
+ cat "$tmp_file" > "$message_file"
2473
+
2257
2474
  final_trailer_block=$(awk '
2258
2475
  { lines[NR] = $0 }
2259
2476
  END {
@@ -2279,57 +2496,6 @@ final_trailer_block=$(awk '
2279
2496
  }
2280
2497
  ' "$message_file")
2281
2498
 
2282
- duplicate_desired_trailer=false
2283
- while IFS= read -r desired_trailer; do
2284
- if [ -n "$desired_trailer" ] && [ "$(printf '%s\\n' "$final_trailer_block" | grep -Fxc -- "$desired_trailer")" -gt 1 ]; then
2285
- duplicate_desired_trailer=true
2286
- break
2287
- fi
2288
- done <<< "$desired_trailers"
2289
-
2290
- if [ "$duplicate_desired_trailer" = true ]; then
2291
- tmp_file=$(mktemp)
2292
- desired_file=$(mktemp)
2293
- trap 'rm -f "$tmp_file" "$desired_file"' EXIT
2294
- printf '%s' "$desired_trailers" > "$desired_file"
2295
- awk -v desired_file="$desired_file" '
2296
- BEGIN {
2297
- while ((getline value < desired_file) > 0) {
2298
- if (value != "") {
2299
- wanted[value] = 1
2300
- }
2301
- }
2302
- close(desired_file)
2303
- }
2304
- { lines[NR] = $0 }
2305
- END {
2306
- line = NR
2307
- while (line > 0 && lines[line] == "") {
2308
- line--
2309
- }
2310
- start = line
2311
- while (start > 0 && lines[start] != "") {
2312
- start--
2313
- }
2314
- valid = line > 0
2315
- for (i = start + 1; valid && i <= line; i++) {
2316
- if (lines[i] !~ /^[[:alnum:]-]+: .+/) {
2317
- valid = 0
2318
- }
2319
- }
2320
- for (i = 1; i <= NR; i++) {
2321
- if (valid && i > start && i <= line && wanted[lines[i]]) {
2322
- if (seen[lines[i]]++) {
2323
- continue
2324
- }
2325
- }
2326
- print lines[i]
2327
- }
2328
- }
2329
- ' "$message_file" > "$tmp_file"
2330
- cat "$tmp_file" > "$message_file"
2331
- fi
2332
-
2333
2499
  missing_trailers=""
2334
2500
  collect_missing_trailer() {
2335
2501
  if ! printf '%s\\n' "$final_trailer_block" | grep -Fqx -- "$1"; then
@@ -2641,28 +2807,6 @@ function githubRepositoryFromLeaseScope(leaseScope) {
2641
2807
  }
2642
2808
  return { owner: match[1], name: match[2] };
2643
2809
  }
2644
- function githubRepositoryIdFromUploadUrl(upstreamUrl) {
2645
- const repositoryId = Number(upstreamUrl.searchParams.get("repository_id"));
2646
- if (!Number.isSafeInteger(repositoryId) || repositoryId <= 0) {
2647
- throw new EgressPolicyDenied(
2648
- "GitHub asset upload request is missing a valid repository_id."
2649
- );
2650
- }
2651
- return repositoryId;
2652
- }
2653
- function githubRepositoryIdLeaseScope(repositoryId) {
2654
- return `repository-id:${repositoryId}`;
2655
- }
2656
- function githubRepositoryIdFromLeaseScope(leaseScope) {
2657
- const match = /^repository-id:(\d+)$/.exec(leaseScope ?? "");
2658
- const repositoryId = Number(match?.[1]);
2659
- if (!Number.isSafeInteger(repositoryId) || repositoryId <= 0) {
2660
- throw new GitHubPluginSetupError(
2661
- "GitHub asset upload grant is missing a repository id lease scope."
2662
- );
2663
- }
2664
- return repositoryId;
2665
- }
2666
2810
  async function resolveUserAccount(tokens) {
2667
2811
  const account = await githubRequest("https://api.github.com", "/user", {
2668
2812
  token: tokens.accessToken
@@ -2843,8 +2987,7 @@ async function issueInstallationToken(options) {
2843
2987
  const permissions = "permissions" in options ? options.permissions : typeof options.loadPermissions === "function" ? await options.loadPermissions({ appJwt, installationId }) : void 0;
2844
2988
  const body = {
2845
2989
  ...permissions ? { permissions } : {},
2846
- ..."repositories" in options ? { repositories: options.repositories } : {},
2847
- ..."repositoryIds" in options ? { repository_ids: options.repositoryIds } : {}
2990
+ ..."repositories" in options ? { repositories: options.repositories } : {}
2848
2991
  };
2849
2992
  const accessTokenResponse = await githubRequest(
2850
2993
  "https://api.github.com",
@@ -2864,7 +3007,6 @@ async function issueInstallationToken(options) {
2864
3007
  async function issueInstallationCredential(options) {
2865
3008
  const token = await issueInstallationToken(options);
2866
3009
  return createCredentialLease({
2867
- ...options.domains ? { domains: options.domains } : {},
2868
3010
  token: token.token,
2869
3011
  expiresAtMs: token.expiresAtMs
2870
3012
  });
@@ -3183,13 +3325,7 @@ async function githubGrantForEgress(ctx) {
3183
3325
  upstreamUrl
3184
3326
  });
3185
3327
  if (isGitHubAssetUploadRequest(method, upstreamUrl)) {
3186
- const repositoryId = githubRepositoryIdFromUploadUrl(upstreamUrl);
3187
- return grantForAccess(
3188
- "write",
3189
- "github.asset-upload",
3190
- "installation-write",
3191
- githubRepositoryIdLeaseScope(repositoryId)
3192
- );
3328
+ return grantForAccess("write", "github.asset-upload", "user-write");
3193
3329
  }
3194
3330
  const smartHttpAccess = githubSmartHttpAccess(upstreamUrl);
3195
3331
  if (smartHttpAccess) {
@@ -3423,18 +3559,6 @@ function githubPlugin(options = {}) {
3423
3559
  });
3424
3560
  }
3425
3561
  if (ctx.grant.name === "installation-write") {
3426
- if (ctx.grant.reason === "github.asset-upload") {
3427
- const repositoryId = githubRepositoryIdFromLeaseScope(
3428
- ctx.grant.leaseScope
3429
- );
3430
- return await issueInstallationCredential({
3431
- appIdEnv,
3432
- domains: GITHUB_ASSET_UPLOAD_CREDENTIAL_DOMAINS,
3433
- privateKeyEnv,
3434
- installationIdEnv,
3435
- repositoryIds: [repositoryId]
3436
- });
3437
- }
3438
3562
  const repository = githubRepositoryFromLeaseScope(
3439
3563
  ctx.grant.leaseScope
3440
3564
  );
@@ -12,6 +12,12 @@ declare const githubIssueOutcomeInputSchema: z.ZodObject<{
12
12
  open: "open";
13
13
  closed: "closed";
14
14
  }>;
15
+ stateReason: z.ZodOptional<z.ZodEnum<{
16
+ completed: "completed";
17
+ duplicate: "duplicate";
18
+ not_planned: "not_planned";
19
+ reopened: "reopened";
20
+ }>>;
15
21
  updatedAt: z.ZodDate;
16
22
  }, z.core.$strict>;
17
23
  export type GitHubIssueOutcomeInput = z.output<typeof githubIssueOutcomeInputSchema>;
@@ -0,0 +1,5 @@
1
+ ALTER TABLE "junior_github_issues" ADD COLUMN "state_reason" text;
2
+ --> statement-breakpoint
3
+ UPDATE "junior_github_issues"
4
+ SET "state_reason" = 'completed'
5
+ WHERE "state" = 'closed';
@@ -0,0 +1,275 @@
1
+ {
2
+ "id": "6e0acdc7-db48-4ebf-846e-0589fc32afb5",
3
+ "prevId": "fb7f4132-d92f-4127-be54-23f93c79034b",
4
+ "version": "7",
5
+ "dialect": "postgresql",
6
+ "tables": {
7
+ "public.junior_github_issues": {
8
+ "name": "junior_github_issues",
9
+ "schema": "",
10
+ "columns": {
11
+ "issue_id": {
12
+ "name": "issue_id",
13
+ "type": "text",
14
+ "primaryKey": true,
15
+ "notNull": true
16
+ },
17
+ "repository_id": {
18
+ "name": "repository_id",
19
+ "type": "text",
20
+ "primaryKey": false,
21
+ "notNull": true
22
+ },
23
+ "repository_full_name": {
24
+ "name": "repository_full_name",
25
+ "type": "text",
26
+ "primaryKey": false,
27
+ "notNull": true
28
+ },
29
+ "number": {
30
+ "name": "number",
31
+ "type": "integer",
32
+ "primaryKey": false,
33
+ "notNull": true
34
+ },
35
+ "state": {
36
+ "name": "state",
37
+ "type": "text",
38
+ "primaryKey": false,
39
+ "notNull": true
40
+ },
41
+ "state_reason": {
42
+ "name": "state_reason",
43
+ "type": "text",
44
+ "primaryKey": false,
45
+ "notNull": false
46
+ },
47
+ "opened_at": {
48
+ "name": "opened_at",
49
+ "type": "timestamp with time zone",
50
+ "primaryKey": false,
51
+ "notNull": true
52
+ },
53
+ "closed_at": {
54
+ "name": "closed_at",
55
+ "type": "timestamp with time zone",
56
+ "primaryKey": false,
57
+ "notNull": false
58
+ },
59
+ "updated_at": {
60
+ "name": "updated_at",
61
+ "type": "timestamp with time zone",
62
+ "primaryKey": false,
63
+ "notNull": true
64
+ }
65
+ },
66
+ "indexes": {
67
+ "junior_github_issues_opened_at_idx": {
68
+ "name": "junior_github_issues_opened_at_idx",
69
+ "columns": [
70
+ {
71
+ "expression": "opened_at",
72
+ "isExpression": false,
73
+ "asc": true,
74
+ "nulls": "last"
75
+ }
76
+ ],
77
+ "isUnique": false,
78
+ "concurrently": false,
79
+ "method": "btree",
80
+ "with": {}
81
+ },
82
+ "junior_github_issues_closed_at_idx": {
83
+ "name": "junior_github_issues_closed_at_idx",
84
+ "columns": [
85
+ {
86
+ "expression": "closed_at",
87
+ "isExpression": false,
88
+ "asc": true,
89
+ "nulls": "last"
90
+ }
91
+ ],
92
+ "isUnique": false,
93
+ "concurrently": false,
94
+ "method": "btree",
95
+ "with": {}
96
+ },
97
+ "junior_github_issues_open_idx": {
98
+ "name": "junior_github_issues_open_idx",
99
+ "columns": [
100
+ {
101
+ "expression": "issue_id",
102
+ "isExpression": false,
103
+ "asc": true,
104
+ "nulls": "last"
105
+ }
106
+ ],
107
+ "isUnique": false,
108
+ "where": "\"junior_github_issues\".\"state\" = 'open'",
109
+ "concurrently": false,
110
+ "method": "btree",
111
+ "with": {}
112
+ }
113
+ },
114
+ "foreignKeys": {},
115
+ "compositePrimaryKeys": {},
116
+ "uniqueConstraints": {},
117
+ "policies": {},
118
+ "checkConstraints": {},
119
+ "isRLSEnabled": false
120
+ },
121
+ "public.junior_github_pull_requests": {
122
+ "name": "junior_github_pull_requests",
123
+ "schema": "",
124
+ "columns": {
125
+ "pull_request_id": {
126
+ "name": "pull_request_id",
127
+ "type": "text",
128
+ "primaryKey": true,
129
+ "notNull": true
130
+ },
131
+ "repository_id": {
132
+ "name": "repository_id",
133
+ "type": "text",
134
+ "primaryKey": false,
135
+ "notNull": true
136
+ },
137
+ "repository_full_name": {
138
+ "name": "repository_full_name",
139
+ "type": "text",
140
+ "primaryKey": false,
141
+ "notNull": true
142
+ },
143
+ "number": {
144
+ "name": "number",
145
+ "type": "integer",
146
+ "primaryKey": false,
147
+ "notNull": true
148
+ },
149
+ "state": {
150
+ "name": "state",
151
+ "type": "text",
152
+ "primaryKey": false,
153
+ "notNull": true
154
+ },
155
+ "commit_composition": {
156
+ "name": "commit_composition",
157
+ "type": "text",
158
+ "primaryKey": false,
159
+ "notNull": false
160
+ },
161
+ "conversation_ids": {
162
+ "name": "conversation_ids",
163
+ "type": "text[]",
164
+ "primaryKey": false,
165
+ "notNull": true,
166
+ "default": "ARRAY[]::text[]"
167
+ },
168
+ "opened_at": {
169
+ "name": "opened_at",
170
+ "type": "timestamp with time zone",
171
+ "primaryKey": false,
172
+ "notNull": true
173
+ },
174
+ "merged_at": {
175
+ "name": "merged_at",
176
+ "type": "timestamp with time zone",
177
+ "primaryKey": false,
178
+ "notNull": false
179
+ },
180
+ "closed_at": {
181
+ "name": "closed_at",
182
+ "type": "timestamp with time zone",
183
+ "primaryKey": false,
184
+ "notNull": false
185
+ },
186
+ "updated_at": {
187
+ "name": "updated_at",
188
+ "type": "timestamp with time zone",
189
+ "primaryKey": false,
190
+ "notNull": true
191
+ }
192
+ },
193
+ "indexes": {
194
+ "junior_github_pull_requests_opened_at_idx": {
195
+ "name": "junior_github_pull_requests_opened_at_idx",
196
+ "columns": [
197
+ {
198
+ "expression": "opened_at",
199
+ "isExpression": false,
200
+ "asc": true,
201
+ "nulls": "last"
202
+ }
203
+ ],
204
+ "isUnique": false,
205
+ "concurrently": false,
206
+ "method": "btree",
207
+ "with": {}
208
+ },
209
+ "junior_github_pull_requests_merged_at_idx": {
210
+ "name": "junior_github_pull_requests_merged_at_idx",
211
+ "columns": [
212
+ {
213
+ "expression": "merged_at",
214
+ "isExpression": false,
215
+ "asc": true,
216
+ "nulls": "last"
217
+ }
218
+ ],
219
+ "isUnique": false,
220
+ "concurrently": false,
221
+ "method": "btree",
222
+ "with": {}
223
+ },
224
+ "junior_github_pull_requests_closed_at_idx": {
225
+ "name": "junior_github_pull_requests_closed_at_idx",
226
+ "columns": [
227
+ {
228
+ "expression": "closed_at",
229
+ "isExpression": false,
230
+ "asc": true,
231
+ "nulls": "last"
232
+ }
233
+ ],
234
+ "isUnique": false,
235
+ "concurrently": false,
236
+ "method": "btree",
237
+ "with": {}
238
+ },
239
+ "junior_github_pull_requests_open_idx": {
240
+ "name": "junior_github_pull_requests_open_idx",
241
+ "columns": [
242
+ {
243
+ "expression": "pull_request_id",
244
+ "isExpression": false,
245
+ "asc": true,
246
+ "nulls": "last"
247
+ }
248
+ ],
249
+ "isUnique": false,
250
+ "where": "\"junior_github_pull_requests\".\"state\" = 'open'",
251
+ "concurrently": false,
252
+ "method": "btree",
253
+ "with": {}
254
+ }
255
+ },
256
+ "foreignKeys": {},
257
+ "compositePrimaryKeys": {},
258
+ "uniqueConstraints": {},
259
+ "policies": {},
260
+ "checkConstraints": {},
261
+ "isRLSEnabled": false
262
+ }
263
+ },
264
+ "enums": {},
265
+ "schemas": {},
266
+ "sequences": {},
267
+ "roles": {},
268
+ "policies": {},
269
+ "views": {},
270
+ "_meta": {
271
+ "columns": {},
272
+ "schemas": {},
273
+ "tables": {}
274
+ }
275
+ }
@@ -29,6 +29,13 @@
29
29
  "when": 1784762053015,
30
30
  "tag": "0003_pull_request_conversations",
31
31
  "breakpoints": true
32
+ },
33
+ {
34
+ "idx": 4,
35
+ "version": "7",
36
+ "when": 1784774242129,
37
+ "tag": "0004_marvelous_toad_men",
38
+ "breakpoints": true
32
39
  }
33
40
  ]
34
41
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-github",
3
- "version": "0.108.0",
3
+ "version": "0.110.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.108.0"
34
+ "@sentry/junior-plugin-api": "0.110.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^25.9.1",
@@ -13,10 +13,10 @@
13
13
 
14
14
  - **Adopted:** preserve the upstream one-file-per-upload behavior, supported formats, markdown output, and upload endpoint.
15
15
  - **Adopted:** preserve the upstream MIT license in the skill directory as explicitly required.
16
- - **Replaced:** direct `gh auth token` access with Junior's host-managed, repository-scoped installation credential injection so credentials never enter script output or files.
16
+ - **Replaced:** direct `gh auth token` access with Junior's host-managed requesting-user OAuth credential injection so credentials never enter script output or files.
17
17
  - **Replaced:** numeric repository-id override with explicit `owner/repo`; the script resolves the id through authenticated `gh api`.
18
18
  - **Narrowed:** automatic activation requires a concrete local path and GitHub destination.
19
- - **Deferred:** live upload verification because it would create an external GitHub asset; deterministic mocked validation is sufficient for implementation checks.
19
+ - **Verified:** a live upload with the repository-scoped installation credential returned HTTP 500; the endpoint's upstream implementation uses a user token.
20
20
 
21
21
  ## Coverage
22
22
 
@@ -23,7 +23,7 @@ Out of scope:
23
23
  - Run `scripts/upload.sh` once per local file.
24
24
  - Return image markdown or a bare video URL.
25
25
  - Surface per-file failures exactly enough for the user to act.
26
- - Use the GitHub plugin's repository-scoped installation credential injection; do not retrieve tokens in the script.
26
+ - Use the GitHub plugin's requesting-user OAuth credential injection; do not retrieve tokens in the script.
27
27
 
28
28
  ## Reference Architecture
29
29
 
@@ -40,7 +40,7 @@ Out of scope:
40
40
  ## Known Limitations
41
41
 
42
42
  - GitHub's user-attachment endpoint is not part of the documented public REST API.
43
- - A real upload requires GitHub to accept the repository-scoped installation credential for the target repository.
43
+ - A real upload requires the requesting user to authorize the GitHub App.
44
44
 
45
45
  ## Maintenance Notes
46
46
 
@@ -21,7 +21,8 @@ Use `git` and `gh` for repository work. Use `github_createPullRequest`, not `gh
21
21
  - Read applicable `AGENTS.md` files before editing. Narrower repo/task instructions win.
22
22
  - Preserve unrelated work. Never force-push, delete refs, or perform destructive merges.
23
23
  - Base conclusions on repository evidence. Do not claim a check ran unless it did.
24
- - Supported App-owned writes use an automatically injected repository-scoped credential. Try the operation before requesting remediation; ask for no user token.
24
+ - For Junior-owned pull requests, push the branch before creating the PR. The runtime supplies repository-scoped GitHub App credentials for both; try the operations before requesting remediation and never ask for a user token.
25
+ - A tool-routing denial requires the named tool; only an upstream denial justifies permission remediation.
25
26
  - Stop for ambiguous targets, missing access, destructive operations, or unresolved upstream permission failures.
26
27
 
27
28
  ## Workflow
@@ -36,9 +37,7 @@ For edits, choose the smallest credible validation path before changing files. C
36
37
 
37
38
  ### 2. Investigate
38
39
 
39
- Establish where the behavior lives, current versus requested behavior, root cause or gap, and the smallest proof of correctness. Read linked issues, PRs, specs, and failing output when provided. If the request is investigation-only, report evidence without editing.
40
-
41
- For non-trivial architecture, API, security, concurrency, migration, or broad cross-file work, use the available advisor after gathering evidence and before editing. Resolve material concerns in the plan.
40
+ Establish where the behavior lives, current versus requested behavior, root cause or gap, and the smallest proof of correctness. Read linked issues, PRs, specs, and failing output when provided. For pull requests, inspect conversation comments, inline review comments, reviews, the diff, and checks. If the request is investigation-only, report evidence without editing.
42
41
 
43
42
  ### 3. Edit
44
43
 
@@ -56,8 +55,6 @@ Do not install or repair the GitHub plugin runtime itself; that is manifest-owne
56
55
 
57
56
  Run targeted changed-file/package checks before broad suites. Separate regressions from baseline failures. For instruction-only changes, run available structural checks and perform a content-consistency review.
58
57
 
59
- For non-trivial work, review the final diff and initial results with the available advisor before packaging. Address material correctness, regression, and testing concerns, then rerun affected checks.
60
-
61
58
  ### 5. Package every completed edit
62
59
 
63
60
  Unless the user explicitly says not to create a PR, every completed repository edit must end in a pushed branch and PR. Default to draft; honor an explicit user or repo instruction to open it ready for review. Do not stop at local changes or a commit.
@@ -77,11 +74,3 @@ If PR creation is blocked, report the exact failed command/tool call and leave t
77
74
  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.
78
75
 
79
76
  Return: repo, branch, PR URL/number, checks and results, pre-existing failures, and anything not run with the reason.
80
-
81
- ## Operation notes
82
-
83
- - **Clone/history:** clone shallowly by default; deepen before any operation that relies on omitted ancestry.
84
- - **PR inspection:** read conversation comments, inline review comments, reviews, diff, and checks.
85
- - **PR mutation:** push before create; use only supported endpoints in the API reference.
86
- - **Workflow dispatch:** `gh workflow run` is supported only for workflows declaring `workflow_dispatch`.
87
- - **Permissions:** a tool-routing denial requires the named tool; only an upstream denial justifies permission remediation.