# list_prs — General Forgejo PR Listing Script

**Script name:** `list_prs`

**Script location:** [`scripts/list_prs.ts`](../../../scripts/list_prs.ts)

**Invoke:**
```bash
npx --yes tsx .opencode/skills/auto-agents-system/scripts/list_prs.ts \
  --url URL --pat TOKEN --owner OWNER --repo REPO \
  [--state open|closed|all] [--min-approvals N] [--max-approvals N] \
  [--mergeable true|false] [--stale not_stale|stale_no_conflicts|stale_with_conflicts|stale_unknown] \
  [--ci-status passing|failing|pending|unknown[,...]]
```

**CRITICAL:** Always invoke the script using bash and the format noted above. **Never** try to use the skill as a task, or invoke it through any other method than by directly calling bash with a command of the form listed above under "Invoke:".

## Synopsis

```
list_prs.ts --url URL --pat TOKEN --owner OWNER --repo REPO [OPTIONS]
```

Fetches all pull requests from a Forgejo repository, enriches each one with a
computed approval count, a staleness classification, and a priority rank, then
filters and sorts the results before writing a JSON array to stdout.

`list_prs.ts` is both a standalone CLI tool and the **importable core module**
used by the four thin wrapper scripts
(`list_prs_needs_review`, `list_prs_ready_to_merge`, `list_prs_stale_clean`,
`list_prs_stale_conflicts`). When imported, it exports `listPRs` and
`parseConnectionArgs` for use by those wrappers.


## Arguments

### Required

| Flag | Type | Description |
|------|------|-------------|
| `--url URL` | string | Forgejo base URL, **no trailing slash** (e.g. `https://git.example.com`) |
| `--pat TOKEN` | string | Personal Access Token with read access to the repository |
| `--owner OWNER` | string | Repository owner or organisation name |
| `--repo REPO` | string | Repository name |

All four required flags must be present. Any missing flag causes an immediate
exit with code 1 and an error message to stderr.

### Optional Filters

All filter flags default to "no filter" (i.e. all values pass) when omitted.

| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--state open\|closed\|all` | string | `open` | Filter by PR state |
| `--min-approvals N` | integer ≥ 0 | (none) | Include only PRs with **≥ N** non-dismissed approvals |
| `--max-approvals N` | integer ≥ 0 | (none) | Include only PRs with **≤ N** non-dismissed approvals |
| `--mergeable true\|false` | boolean | (none) | Filter by Forgejo's raw `PR.mergeable` field |
| `--stale VALUE` | string | (none) | Filter by computed `stale_state` (see Stale States below) |
| `--ci-status VALUE[,...]` | string | (none) | Comma-separated whitelist of CI statuses to include (see CI Status below). Omit to include all CI statuses. |
| `-h`, `--help` | — | — | Print usage to stderr and exit 0 |

### Argument Validity Rules

- `--min-approvals` and `--max-approvals` may be used together; both constraints apply simultaneously (logical AND).
- `--min-approvals 1 --max-approvals 0` is logically impossible and will return an empty result set — the script does not reject it, it simply matches nothing.
- `--mergeable` filters on the raw API field, not on the computed `stale_state`. Use `--stale` for the semantically richer classification.
- `--ci-status` accepts a comma-separated list of values from `passing`, `failing`, `pending`, `unknown`. A PR must match **any one** of the listed values (logical OR within the list). Multiple `--ci-status` flags are not supported — put all desired values in one comma-separated argument.
- Any unknown flag causes exit code 1 and a usage message.
- `--state` only accepts the three literal values `open`, `closed`, `all`; any other value is passed to the API which may behave unexpectedly (this is not validated client-side).


## Output

Written to **stdout** as a pretty-printed JSON array. Each element is the full
Forgejo PR object as returned by the API, augmented with four additional fields:

| Field | Type | Description |
|-------|------|-------------|
| `approvals_count` | integer | Count of distinct non-dismissed `APPROVED` reviews. Per-reviewer: only the reviewer's most recent review is counted; if they later submitted `REQUEST_CHANGES`, that supersedes the approval and it is not counted. |
| `stale_state` | string | One of five stale states (see below) |
| `ci_status` | string | One of four CI status values (see CI Status below) |
| `priority_rank` | integer | 0 = `Priority/CI Blocker` (highest), …, 5 = `Priority/Backlog`, 6 = unlabelled |
| `priority_label` | string \| null | The matched `Priority/*` label name, or `null` if none present |

The array is **sorted ascending by `priority_rank`** so the most urgent PRs appear
first within the result set.

### Stale States

| `stale_state` value | Meaning |
|---------------------|---------|
| `not_stale` | `PR.merge_base == base branch HEAD` — PR is fully up-to-date |
| `stale_no_conflicts` | Base has advanced AND `PR.mergeable == true` — clean auto-rebase possible |
| `stale_with_conflicts` | Base has advanced AND `PR.mergeable == false` — manual resolution required |
| `stale_unknown` | Base has advanced AND `PR.mergeable == null` — Forgejo is still computing |
| `compute_error` | `PR.merge_base` or base HEAD SHA is missing; classification impossible |

### CI Status

Derived from `GET /api/v1/repos/{owner}/{repo}/commits/{head_sha}/status` — the Forgejo combined commit-status endpoint. The `state` field of that response is mapped as follows:

| `ci_status` value | Forgejo combined state | Meaning |
|-------------------|----------------------|---------|
| `passing` | `success` | All status checks have reported success |
| `failing` | `failure` \| `error` \| `warning` | At least one check has failed or errored |
| `pending` | `pending` | Checks are queued or still running |
| `unknown` | `""` \| `unknown` | No status checks have been reported for this commit |

Use `--ci-status passing` to restrict to PRs where all CI has passed. Use `--ci-status passing,pending` to include both passing and still-running PRs. Omit `--ci-status` entirely to return PRs regardless of their CI state.

### Priority Rank Table

| `priority_rank` | Label |
|----------------|-------|
| 0 | `Priority/CI Blocker` |
| 1 | `Priority/Critical` |
| 2 | `Priority/High` |
| 3 | `Priority/Medium` |
| 4 | `Priority/Low` |
| 5 | `Priority/Backlog` |
| 6 | (no `Priority/*` label present) |


## Diagnostics / stderr

Progress messages are written to stderr during execution (not to stdout, so
they do not contaminate the JSON output):

```
Fetching PRs from https://git.example.com/org/repo (state=open)...
Fetched 42 PR(s).  Enriching and filtering...
Fetching HEAD SHAs for 3 unique base branch(es)...
Fetching reviews for 42 PR(s) concurrently (limit=20)...
Fetching CI status for 42 PR(s) concurrently (limit=20)...

Result: 12/42 PR(s) matched the filter criteria.
```

Warnings about individual failures (e.g. a branch API error for one PR) are
also written to stderr. The script continues processing the remaining PRs
rather than aborting.


## Exit Codes

| Code | Meaning |
|------|---------|
| 0 | Success — JSON array written to stdout (may be empty `[]`) |
| 1 | Fatal error — message written to stderr |


## Errors the Script May Throw

| Condition | Behaviour |
|-----------|-----------|
| Missing required `--url`, `--pat`, `--owner`, or `--repo` | Exit 1 with descriptive message |
| Unknown CLI flag | Exit 1 with usage message |
| API returns non-2xx for the PR list | Throws; script exits 1 with the HTTP status and body |
| API returns non-2xx for a base branch HEAD | Logs a warning to stderr; marks affected PRs as `compute_error`; continues |
| API returns non-2xx for a PR's reviews | Logs a warning; treats that PR as having 0 approvals; continues |
| Network timeout (30 s per request) | Throws; script exits 1 |


## Performance Notes

The script uses concurrent fetch chains to minimise wall-clock time:

- **PR pages** are fetched in parallel batches of 6 pages at a time.
- **Base-branch HEAD SHAs** are fetched simultaneously for all unique branches (cached: one call per unique branch regardless of how many PRs share it).
- **Reviews** are fetched with up to 20 concurrent in-flight requests at once.

For a repository with ~326 PRs, this reduces review-fetching from ~230 s
(sequential) to ~1.7 s.


## When to Use This Script

Use `list_prs.ts` directly when you need **custom filter combinations** not
served by one of the four wrapper scripts:

- List all closed PRs: `--state closed`
- Find PRs with exactly 2 approvals: `--min-approvals 2 --max-approvals 2`
- Find approved PRs that are still pending Forgejo's conflict check: `--min-approvals 1 --stale stale_unknown`
- List all PRs regardless of state: `--state all`
- Find open PRs ready to merge (approved, not stale, CI passing): `--min-approvals 1 --stale not_stale --ci-status passing`
- Find PRs with broken CI: `--ci-status failing`
- Find PRs with passing or pending CI: `--ci-status passing,pending`

For the four standard operating buckets used by `pr-merge-pool-supervisor`,
prefer the thin wrappers — they are self-documenting and require fewer flags.


## Usage Examples

```bash
# List all open PRs (no filters — returns everything)
npx --yes tsx /app/.opencode/skills/auto-agents-system/scripts/list_prs.ts \
  --url https://git.example.com \
  --pat ghp_XXXX \
  --owner my-org \
  --repo my-repo

# List open PRs with at least 1 approval that are not stale
npx --yes tsx /app/.opencode/skills/auto-agents-system/scripts/list_prs.ts \
  --url https://git.example.com \
  --pat ghp_XXXX \
  --owner my-org \
  --repo my-repo \
  --min-approvals 1 \
  --stale not_stale

# List open PRs awaiting a first review (0 approvals)
npx --yes tsx /app/.opencode/skills/auto-agents-system/scripts/list_prs.ts \
  --url https://git.example.com \
  --pat ghp_XXXX \
  --owner my-org \
  --repo my-repo \
  --max-approvals 0

# List all closed PRs (for audit/history purposes)
npx --yes tsx /app/.opencode/skills/auto-agents-system/scripts/list_prs.ts \
  --url https://git.example.com \
  --pat ghp_XXXX \
  --owner my-org \
  --repo my-repo \
  --state closed

# Extract just PR numbers from the result using jq
npx --yes tsx /app/.opencode/skills/auto-agents-system/scripts/list_prs.ts \
  --url https://git.example.com --pat ghp_XXXX \
  --owner my-org --repo my-repo --min-approvals 1 \
  | jq '[.[].number]'
```


## Agent Usage Examples

### pr-merge-pool-supervisor

The supervisor does **not** call `list_prs.ts` directly. It calls the four
purpose-built wrapper scripts to populate its four work queues each cycle.
It would only call `list_prs.ts` directly if it needed a custom filter
combination not covered by those wrappers (e.g. investigating `stale_unknown`
PRs to determine if Forgejo is lagging).

### pr-merge-worker

The worker does **not** call `list_prs.ts`. It receives a specific PR number
from the supervisor and operates on that single PR. However, a worker
implementing complex decision logic could import `listPRs` programmatically
to inspect the state of all PRs before deciding how to proceed.

### system-watchdog-pool-supervisor

The watchdog could use `list_prs.ts` with `--state all` or custom filter
combinations to audit the full PR pipeline — for example, to detect PRs
that have been open for an unusually long time, or to count approved PRs
that are not progressing through the merge pipeline.

### implementation-pool-supervisor

The implementation supervisor might use `list_prs.ts` with
`--min-approvals 0 --max-approvals 0` (or equivalently `list_prs_needs_review`)
to check whether any of its submitted PRs are awaiting a first review, so it
can decide whether to dispatch more implementation workers or wait for review
capacity to clear.


## Module API (when imported)

```typescript
import { listPRs, parseConnectionArgs } from './list_prs.ts';

// Config object
const config: Config = {
  url: 'https://git.example.com',
  pat: 'ghp_XXXX',
  owner: 'my-org',
  repo: 'my-repo',
};

// Filters object (all fields optional)
const filters: Filters = {
  state: 'open',
  minApprovals: 1,
  stale: 'not_stale',
  ciStatus: ['passing'],       // only PRs with all CI checks passing
};

// ciStatus accepts any combination:
//   ciStatus: ['passing']              — CI must have passed
//   ciStatus: ['pending']              — CI still running
//   ciStatus: ['failing']             — CI has failed
//   ciStatus: ['passing', 'pending']  — CI passed or still running
//   (omit ciStatus entirely)          — any CI state passes through

const prs: AugmentedPR[] = await listPRs(config, filters);
```

The exported `parseConnectionArgs(argv)` function parses only the four
connection flags (`--url`, `--pat`, `--owner`, `--repo`) and is used by the
four wrapper scripts. It rejects any unknown flag with an informative error.


## Dependencies

- Node.js ≥ 18 (native `fetch` API, stable since v21; available unflagged since v18)
- `npx tsx` for TypeScript execution (auto-installed by `npx --yes tsx`)
- Zero external npm packages — uses only Node.js built-ins
