GitHub
Concept

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

FieldSourceNotes
numbersequential per repoStable identity for URL slugs and pfai pr <num>
stateopen | closed | mergedThree states only — draft is a separate flag
draftboolReviewers aren’t auto-notified while draft is true
mergedboolTrue once state=merged; stays true even if the merge commit is reverted
mergeablenullable boolCached merge-conflict status; null means “computing”
headRef / baseRef / headSHAgitSource branch, target branch, source commit
mergeMethodmerge | squash | rebaseSet when the PR is merged
labels, assignees, authorDBStandard 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}:

EndpointUse
GET /pulls?state=open&base=main&author=…List, filterable
POST /pullsCreate
GET /pulls/{number}Detail
PATCH /pulls/{number}Update title/body/labels/assignees, toggle draft, close/reopen
GET /pulls/{number}/diffPatch text
GET /pulls/{number}/commitsCommits between base and head
GET /pulls/{number}/checksCI check status (passing/failing/pending)
GET /pulls/{number}/comments · POSTTop-level conversation
GET /pulls/{number}/reviews · POSTStructured reviews
GET /pulls/{number}/merge-checksAggregated merge eligibility (approvals + checks + protection)
POST /pulls/{number}/mergeMerge — {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 commentsReviews
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?NoYes (see below)
Carries forward across force-pushes?YesYes — 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:

VerdictMeaning
APPROVEDReviewer approves the changes
CHANGES_REQUESTEDReviewer wants changes before merging
COMMENTEDComments without a verdict — neither approves nor blocks
PENDINGDraft 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:

  1. Branch protection (Repositories → Branch protection) — requireApprovals, requiredReviewers, requireStatusChecks, etc. apply per branchPattern glob.
  2. Repo approval rules at /api/v1/forge/{owner}/{repo}/approval-rules — additional rules with custom path filters and approval counts.
  3. 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 | skipped
  • conclusion: 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:

MethodEffect
mergeStandard merge commit; preserves full branch history
squashSingle commit on base, message taken from PR title/body or override
rebaseReplay 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

See also