diff --git a/src/sweep/prs.ts b/src/sweep/prs.ts index 1146d76..77980ad 100644 --- a/src/sweep/prs.ts +++ b/src/sweep/prs.ts @@ -26,9 +26,23 @@ export type Pr = z.infer // exhaustive on any realistically-sized backlog; per-head dedup keeps the extra listings cheap. const PR_LIST_LIMIT = 500 +const ListedPr = Pr.omit({ baseRefOid: true }) +const RestPull = z.object({ number: z.number(), base: z.object({ sha: z.string() }) }) + +// `gh pr list --json` on Ubuntu's 2.45 gh has headRefOid but not baseRefOid — unknown field → empty +// stdout, which the sweep used to log as "auth/network down". Pull base SHAs from REST instead. +function pullBaseOids(slug: string): Map | null { + const r = exec('gh', ['api', `repos/${slug}/pulls?state=open&per_page=100`, '--paginate']) + if (!r.ok) { + return null + } + const pulls = z.array(RestPull).parse(JSON.parse(r.stdout)) + return new Map(pulls.map((p) => [p.number, p.base.sha])) +} + export function listPrs(cfg: Config): Pr[] | null { // Filter the PR list directly rather than `gh pr list --label` — that search index lags behind labelling. - const fields = 'number,headRefOid,baseRefOid,baseRefName,isDraft,author,labels,title,body' + const fields = 'number,headRefOid,baseRefName,isDraft,author,labels,title,body' const r = exec('gh', [ 'pr', 'list', @@ -42,10 +56,24 @@ export function listPrs(cfg: Config): Pr[] | null { fields, ]) if (!r.ok) { - log('gh pr list failed (auth/network down?) — aborting sweep') + log(`gh pr list failed — aborting sweep: ${r.combined.trim().split('\n')[0] ?? 'unknown error'}`) + return null + } + const listed = z.array(ListedPr).parse(JSON.parse(r.stdout)) + const bases = pullBaseOids(cfg.slug) + if (bases === null) { + log('gh api pulls failed (auth/network down?) — aborting sweep') return null } - return z.array(Pr).parse(JSON.parse(r.stdout)) + const out: Pr[] = [] + for (const pr of listed) { + const baseRefOid = bases.get(pr.number) + if (!baseRefOid) { + throw new Error(`open PR #${pr.number} missing from REST pulls list`) + } + out.push({ ...pr, baseRefOid }) + } + return out } export function hasReviewLabel(pr: Pr, cfg: Config): boolean { diff --git a/src/sweep/review-one.ts b/src/sweep/review-one.ts index e5f5815..7076ce1 100644 --- a/src/sweep/review-one.ts +++ b/src/sweep/review-one.ts @@ -4,6 +4,7 @@ import { existsSync } from 'node:fs' import { join } from 'node:path' import { detectRepo, exec } from '@bevyl-ai/agent-tools' +import { z } from 'zod' import { runReview } from './codex' import { type Config } from './config' @@ -34,22 +35,27 @@ export async function reviewOne(cfg: Config, ref: string, post: boolean): Promis // Ad-hoc review runs in cwd. When cwd is the target repo we spin a head worktree for file context; otherwise codex // reviews from the inlined diff alone (cross-repo refs can't fetch into a foreign checkout). cfg.repoDir = process.cwd() - const head = exec('gh', [ - 'pr', - 'view', - String(number), - '--repo', - slug, - '--json', - 'headRefOid,baseRefOid,baseRefName,title,body', - ]) + // REST — `gh pr view --json baseRefOid` is missing on Ubuntu 2.45 gh. + const head = exec('gh', ['api', `repos/${slug}/pulls/${number}`]) if (!head.ok) { console.error(`stupify review: couldn't read ${slug}#${number} via gh (auth? does it exist?).`) process.exit(1) } - const meta = Pr.pick({ headRefOid: true, baseRefOid: true, baseRefName: true, title: true, body: true }).parse( - JSON.parse(head.stdout), - ) + const raw = z + .object({ + head: z.object({ sha: z.string() }), + base: z.object({ sha: z.string(), ref: z.string() }), + title: z.string(), + body: z.string().nullable(), + }) + .parse(JSON.parse(head.stdout)) + const meta = Pr.pick({ headRefOid: true, baseRefOid: true, baseRefName: true, title: true, body: true }).parse({ + headRefOid: raw.head.sha, + baseRefOid: raw.base.sha, + baseRefName: raw.base.ref, + title: raw.title, + body: raw.body ?? '', + }) const pr = { number, ...meta,