Pull Requests
Open, review, approve, and merge changes — three states plus a draft flag, four review verdicts, three merge methods, and a full REST surface.
Pull requests live in the same forge as your repositories. Diff viewing, threaded comments, structured reviews, CI status, branch-protection enforcement, and merge are all served by the same proxifai binary and the same pfai CLI. The runtime path is internal/forge/native/pullstore.go (DB-backed PR state) plus native.go for git operations.
Anatomy of a PR
| Field | Source | Notes |
|---|---|---|
number | sequential per repo | Stable identity for URL slugs and pfai pr <num> |
state | open | closed | merged | Three states only — draft is a separate flag |
draft | bool | Reviewers aren’t auto-notified while draft is true |
merged | bool | True once state=merged; stays true even if the merge commit is reverted |
mergeable | nullable bool | Cached merge-conflict status; null means “computing” |
headRef / baseRef / headSHA | git | Source branch, target branch, source commit |
mergeMethod | merge | squash | rebase | Set when the PR is merged |
labels, assignees, author | DB | Standard issue-style metadata |
The state and draft distinction matters: a draft PR is state=open, draft=true. Marking it ready clears the draft flag without changing the state.
Creating a pull request
Push your branch
git push origin feat/auth Open the PR
From the UI: Code → Pull Requests → New PR. Pick head and base branches.
From the CLI:
pfai pr create --head feat/auth --base main \
--title "Add OIDC login flow" \
--body "Implements PKCE login per RFC 7636. Closes #42."From the API:
curl -X POST http://localhost:3000/api/v1/forge/{owner}/{repo}/pulls \
-H "Authorization: Bearer $PFAI_TOKEN" \
-d '{"title":"Add OIDC login flow","head":"feat/auth","base":"main","draft":false}' Reference issues
Drop Closes #42 or Fixes #17 in the description — when the PR merges, the linked issues auto-close.
REST surface
Every action on a PR has a stable endpoint under /api/v1/forge/{owner}/{repo}/pulls/{number}:
| Endpoint | Use |
|---|---|
GET /pulls?state=open&base=main&author=… | List, filterable |
POST /pulls | Create |
GET /pulls/{number} | Detail |
PATCH /pulls/{number} | Update title/body/labels/assignees, toggle draft, close/reopen |
GET /pulls/{number}/diff | Patch text |
GET /pulls/{number}/commits | Commits between base and head |
GET /pulls/{number}/checks | CI check status (passing/failing/pending) |
GET /pulls/{number}/comments · POST | Top-level conversation |
GET /pulls/{number}/reviews · POST | Structured reviews |
GET /pulls/{number}/merge-checks | Aggregated merge eligibility (approvals + checks + protection) |
POST /pulls/{number}/merge | Merge — {method, message} |
pfai pr wraps each of these (pfai pr list/view/create/merge/diff/comment/review).
Diff viewing
The diff endpoint streams a unified patch; the web UI parses it and renders side-by-side or unified, with:
- File-by-file expand/collapse and a sidebar tree of changed files
- Binary-file changes shown as a summary (image dimensions, line counts in detected text formats)
- Syntax highlighting via Shiki, matching the repo browser
- A Files Changed filter that respects the active reviewer’s review history
Comments and reviews
Two distinct surfaces:
| Issue-style comments | Reviews | |
|---|---|---|
| Endpoint | /pulls/{number}/comments | /pulls/{number}/reviews |
| Inline on a line? | No (top-level only) | Yes — each Review carries a comments[] of inline {path, line, body} |
| Has a verdict? | No | Yes (see below) |
| Carries forward across force-pushes? | Yes | Yes — comments are threaded by stable IDs |
A review captures the reviewer’s overall verdict plus optional inline comments. The state field takes one of four values from forge/types.go:
| Verdict | Meaning |
|---|---|
APPROVED | Reviewer approves the changes |
CHANGES_REQUESTED | Reviewer wants changes before merging |
COMMENTED | Comments without a verdict — neither approves nor blocks |
PENDING | Draft review (the reviewer is still composing) |
@-mentions in any comment notify the mentioned user (and trigger an @agent if the mention targets an agent).
Approval requirements
The merge button enforces both org rules and per-branch rules:
- Branch protection (Repositories → Branch protection) —
requireApprovals,requiredReviewers,requireStatusChecks, etc. apply perbranchPatternglob. - Repo approval rules at
/api/v1/forge/{owner}/{repo}/approval-rules— additional rules with custom path filters and approval counts. - Org push rules — apply to every repo’s pushes.
GET /pulls/{number}/merge-checks aggregates everything into a single response so the UI can render the merge button with a clear reason when it’s disabled.
CI checks
Every commit on the PR’s head branch can have any number of pipeline runs attached. GET /pulls/{number}/checks returns the latest run per workflow with:
status:waiting|running|success|failure|cancelled|skippedconclusion: final result, empty while active- A link into the run for streaming logs without leaving the PR page
Configure required checks under Repository Settings → Branch Protection → Required Status Checks.
Merging
When all requirements are met, three merge methods are available — pick the one that matches your branch policy:
| Method | Effect |
|---|---|
merge | Standard merge commit; preserves full branch history |
squash | Single commit on base, message taken from PR title/body or override |
rebase | Replay each head commit on top of base for a linear history |
Branch protection’s requireLinearHistory rejects merge and forces one of squash or rebase.
pfai pr merge 42 --method squash --message "feat(auth): add OIDC login flow"
curl -X POST http://localhost:3000/api/v1/forge/{owner}/{repo}/pulls/42/merge \
-H "Authorization: Bearer $PFAI_TOKEN" \
-d '{"method":"squash","message":"feat(auth): add OIDC login flow"}'
Merge-conflict detection
mergeable is computed in the background by the forge — when null, the UI shows “checking”. When false, the merge button is disabled and the PR overview lists the conflicting files. Resolve locally and push — the next push triggers a re-check.
State transitions
open ──────────────► merged
│ ▲
close │ │ reopen
▼ │
closed
- Draft is a flag that flips independently — open + draft, or open + ready
- Merged is terminal — you can’t reopen a merged PR
- Closed PRs can be reopened if the head branch still exists (the forge re-resolves the SHA)
pfai pr update 42 --draft=false # mark draft PR as ready
pfai pr update 42 --state=closed # close without merging
pfai pr update 42 --state=open # reopen