jjpr
jjpr manages stacked pull requests for Jujutsu repositories. It pushes bookmarks, creates and updates PRs/MRs, merges them, and syncs the stack on GitHub, GitLab, and Forgejo.
See Installation to get started.
Stacked PRs are chains of small pull requests that branch off of each other. They are better structured and easier to review than one large PR, and they let the developer focus on one feature by working ahead of the reviewer.
Commands
jjpr submit and jjpr watch are what most users want. submit
pushes up the current stack of bookmarks as a stack of PRs to the
forge. watch runs in a loop, creating PRs for new bookmarks,
promoting drafts when CI passes, and merging from the bottom up once
each PR is approved.
Other lower-level commands are there for manual control and debugging.
- merge: merge a stack from the bottom up, one-shot.
- status: show the current stack and PR state.
- auth: test or set up forge authentication.
- config: manage config files.
Reference
Installation
Requires Rust 1.91+ when building from source. Runtime requires jj 0.36+ and a colocated jj/git repository with a supported remote.
Homebrew
brew tap michaeldhopkins/tap
brew install jjpr
cargo-binstall
cargo binstall jjpr
Pulls a pre-built binary if one is published for your platform; falls back to building from source.
crates.io
cargo install jjpr
From source
git clone https://github.com/michaeldhopkins/jjpr
cargo install --path jjpr
Verifying
jjpr --version
Confirms the binary is on your $PATH and reports the installed
version.
Next: authentication
jjpr needs an API token (or gh / glab credentials) to talk to the
forge. See Forge support for token env vars and
self-hosted setup, and auth for verifying that
jjpr can authenticate from the current repo.
watch
jjpr watch runs in a loop and manages the full lifecycle of your
stack. It creates draft PRs, promotes them when CI passes, merges
them when approved, and syncs the rest of the stack after each
merge.
jjpr watch # auto-detect the stack from working copy
jjpr watch <bookmark> # watch the stack ending at <bookmark>
jjpr watch --timeout 60 # stop after 60 minutes
jjpr watch --no-ci-check # merge without waiting for CI
What it does
Each cycle:
- Creates draft PRs for bookmarks in the stack that don’t have one yet.
- Marks drafts as ready when their CI checks pass. Reviewers are not added automatically.
- Merges PRs from the bottom up once they’re approved and mergeable.
- Syncs the stack after each merge: rebases downstream, pushes updated bookmarks, retargets PR bases.
- Reports blockers. When a PR needs approval but has no reviewers, the loop says so and continues.
Press Ctrl+C to exit. The next run resumes from wherever the stack
is now.
Flags
| Flag | Effect |
|---|---|
--timeout <MINUTES> | Exit after this many minutes regardless of state |
--no-ci-check | Treat PRs with non-passing CI as mergeable |
--merge-method <method> | squash, merge, or rebase (overrides config) |
--required-approvals <N> | Override the config’s approval threshold |
--reconcile-strategy <strategy> | rebase or merge for post-merge stack syncing |
--base <branch> | Override the auto-detected stack base |
--remote <name> | Override the git remote name |
--no-fetch | Skip git fetch before starting |
Sample session
Two bookmarks set up as a stack, then a single jjpr watch
invocation handles the rest:
$ jj bookmark set auth
$ jj bookmark set profile
$ jjpr watch
Watching stack for 'profile'...
Creating PR (draft) for 'auth'...
https://github.com/o/r/pull/42
Creating PR (draft) for 'profile'...
https://github.com/o/r/pull/43
Marked 'auth' as ready (CI passing)
'profile' (#43): needs review approval but has no reviewers
hint: run `jjpr submit --reviewer <username>` to request reviewers
Merging 'auth' (PR #42, squash)...
https://github.com/o/r/pull/42
Waiting for 'profile':
- Insufficient approvals (0/1)
profile: Approval received (1/1)
Merging 'profile' (PR #43, squash)...
Done. 2 PRs merged.
When no bookmark exists
If you run jjpr watch before setting any bookmark in the working
copy’s ancestry, it waits for one to appear:
Waiting for a bookmark in the working copy's ancestry...
hint: jj bookmark set <name>
Run jj bookmark set <name> in another shell and the loop picks it up
within a few seconds.
submit
jjpr submit is the manual control point. It pushes bookmarks and
creates or updates PRs on the forge. Use it when you don’t want the
watch loop, or when you’re debugging.
jjpr submit # push and update everything in the stack
jjpr submit --reviewer alice,bob # request reviewers on every PR
jjpr submit --draft # create new PRs as drafts
jjpr submit --ready # mark existing draft PRs as ready
jjpr submit --base coworker-feat # override auto-detected base branch
What it does
- Pushes all bookmarks in the stack to the remote.
- Creates PRs for bookmarks that don’t have one yet.
- Updates PR base branches to maintain the stack structure.
- Updates PR bodies when commit descriptions have changed.
- Adds or updates the stack-awareness comment on multi-PR stacks. Single-PR stacks don’t get a comment, so they look like a normal PR to reviewers.
Submit is idempotent. Run it as often as you want. After rebasing,
editing commit messages, or restacking with jj rebase, re-run jjpr submit. It pushes the new commits, fixes PR base branches, and syncs
descriptions. If everything is already up to date, it reports “Stack
is up to date.”
When no bookmark is specified, jjpr infers the target from the working
copy’s position. It finds which stack overlaps trunk()..@ and
submits up to the topmost bookmark.
Flags
| Flag | Effect |
|---|---|
--reviewer <users> | Comma-separated list of reviewers to request |
--draft | Create new PRs as drafts |
--ready | Mark existing draft PRs as ready |
--base <branch> | Override auto-detected base branch |
--remote <name> | Override the git remote name |
--no-fetch | Skip git fetch before starting |
--dry-run | Print what would happen without doing it |
PR titles and bodies
Title and body come from the first commit’s description in each bookmark’s segment.
The body is wrapped in HTML comment markers. When you re-submit after changing a commit message, only the managed section is updated. Text you add above or below the markers (screenshots, notes, test plans) is preserved.
If you remove the markers from the PR body, jjpr stops updating that PR’s description.
The PR title is not automatically updated after creation. If you change the commit’s first line, jjpr warns you about the drift.
Drafts
--draft creates new PRs as drafts. Existing PRs are unaffected.
--ready converts every draft PR in the stack to ready-for-review.
The two flags are mutually exclusive.
Reviewers
--reviewer alice,bob requests reviewers on every PR in the stack,
new and existing. Idempotent: a reviewer who’s already on a PR isn’t
re-requested.
Stacking on someone else’s branch
If a commit in your stack’s ancestry has a remote bookmark that isn’t
one of your own, jjpr treats it as a foreign base and targets your
first PR at that branch instead of the default branch (e.g., main).
The status output reflects this:
auth (1 change, #42 open, synced)
profile (1 change, needs push)
(based on coworker-feat)
Use --base <branch> to override auto-detection. Useful when the
coworker hasn’t pushed yet.
Conflict check
Before pushing, jjpr checks for unresolved conflicts in the stack. Conflicts halt the operation:
Error: cannot push — some commits have unresolved conflicts:
pnnmmvmu (feat/deferment-roles): add Billings::DueDatePolicy specs
To resolve: jj edit pnnmmvmu, fix the conflicts, then re-run jjpr submit.
Stack-awareness comment
Multi-PR stacks get a comment on every PR linking the others:
This PR is part of a stack:
1. [`feat/auth`](https://github.com/o/r/pull/41)
1. **`feat/profile` <-- this PR**
1. [`feat/settings`](https://github.com/o/r/pull/43)
The list always reflects the current local stack in base-to-top order. As you rebase, split, or reorder commits, the order is recomputed every submit so each PR’s comment stays in sync with the others.
When a PR in the stack is merged or closed and its bookmark is no longer in the local graph, it moves into a collapsible history block at the bottom of the comment, rendered with strikethrough:
1. **`feat/profile` <-- this PR**
1. [`feat/settings`](https://github.com/o/r/pull/43)
<details><summary>2 earlier closed/merged PRs</summary>
1. ~~[`feat/foundation`](https://github.com/o/r/pull/39)~~
1. ~~[`feat/migration`](https://github.com/o/r/pull/40)~~
</details>
Up to seven historical entries are shown, sorted most-recent first by the forge’s merge timestamp. Older entries past the cap stay in the embedded data (so future submits can reconstruct full history) but aren’t displayed:
<summary>7 earlier closed/merged PRs (+3 older entries hidden)</summary>
The comment also embeds a base64-encoded JSON payload of the stack state inside an HTML comment marker. jjpr reads this on the next submit to inherit fossil metadata (PR numbers, merge timestamps) for PRs whose local bookmarks have been cleaned up. Don’t edit it; jjpr rewrites the whole comment on every submit.
Single-PR stacks don’t get a comment.
merge
jjpr merge is the one-shot alternative to jjpr watch. It merges
whatever it can right now and exits.
jjpr merge # merge the inferred stack
jjpr merge <bookmark> # merge the stack ending at <bookmark>
jjpr merge --merge-method rebase # use rebase merge method
jjpr merge --no-ci-check # merge even if CI hasn't passed
jjpr merge --dry-run # preview without merging
What it does
For each PR in the stack, starting from the bottom, jjpr checks:
- PR is not a draft.
- CI checks pass (configurable).
- Required number of approvals reached (configurable).
- No changes requested.
- No merge conflicts.
If the bottommost PR is mergeable, jjpr merges it, fetches the updated default branch, syncs the remaining stack, pushes all remaining bookmarks, and retargets the next PR’s base. Then it checks the next PR and continues until blocked or done.
By default, the remaining stack is synced by rebasing downstream
commits onto the new base. Switch to merge-based reconciliation with
reconcile_strategy = "merge" (see
Configuration). That creates merge commits
instead, avoiding force pushes.
Flags
| Flag | Effect |
|---|---|
--merge-method <method> | squash, merge, or rebase (overrides config) |
--required-approvals <N> | Override the config’s approval threshold |
--no-ci-check | Treat PRs with non-passing CI as mergeable |
--reconcile-strategy <strategy> | rebase or merge for post-merge stack syncing |
--base <branch> | Override the auto-detected stack base |
--remote <name> | Override the git remote name |
--no-fetch | Skip git fetch before starting |
--dry-run | Print what would happen without merging |
CLI flags override the config file.
Retry on transient errors
Merge API calls retry automatically on transient HTTP errors (502, 503). If the forge returns a 405 “merge already in progress”, jjpr polls the PR state for up to 30 seconds to confirm completion. No user action needed; this is transparent in normal operation.
Reconcile failures
After each merge, jjpr reconciles two things: local state (fetch, rebase, push the remaining stack) and forge state (refresh PR list, retarget the next base, update stack-info comments). If either fails, jjpr stops at the next PR rather than merging it. Merging without a local rebase would mix in the previous PR’s changes; merging against stale forge state can target the wrong base.
Local sync failed
Failed fetch, failed rebase, conflicted push, or a divergent change ID.
Merging 'auth' (#42, squash)...
Fetching remotes...
Rebasing remaining stack onto main...
Pushing 'profile'...
Warning: failed to push 'profile': conflicted commits
Updating #43 base to 'main'...
Blocked at 'profile' (#43):
- Local sync failed
Run `jjpr merge` again once the issue is resolved.
Note: local state is out of sync with the forge:
Failed to push 'profile': conflicted commits
To accept the forge state (discard local divergence):
jj git fetch
jj bookmark set profile -r profile@origin
Or to fix local state and push it to the forge:
jj git fetch && jj rebase -s <root-change-id> -d main
# resolve any conflicts, then:
jjpr submit
<root-change-id> is the oldest commit in the next segment (jjpr fills
this in for you). Using the bookmark tip’s change ID rebases only the
tip and strands earlier commits under the old base.
The forge-side parts of reconcile (next PR’s base retarget, stack-info comment update) still run before jjpr stops. Only the merge itself waits.
Forge reconcile failed
The forge merge succeeded but a follow-up API call (list_open_prs, update_pr_base, comment update) returned an error. Recovery is usually to retry; persistent failures point at network or permission issues.
Blocked at 'profile' (#43):
- Forge reconcile failed
Note: forge reconcile failed:
Failed to retarget #43 base to 'main': 502 bad gateway
Retry with `jjpr merge` (or wait for `jjpr watch` to retry).
jjpr watch keeps polling through both kinds of failure and resumes
automatically once the next reconcile succeeds, so persistent watch
sessions self-heal.
status
jjpr status (and bare jjpr) shows the stack containing your
working copy and its PR/MR state. It’s read-only. It fetches the
latest state but doesn’t push or modify anything.
jjpr # current stack (inferred from working copy)
jjpr status # same
jjpr status profile # scope to the stack containing 'profile'
jjpr status --all # show every local stack
The default scope matches submit, merge, and watch: the stack
inferred from the working copy. Pass a bookmark to scope to a specific
stack, or --all to see every local stack at once.
Flags
| Flag | Effect |
|---|---|
--all | Show every local stack instead of only the current one. Mutually exclusive with a positional bookmark. |
--no-fetch | Skip git fetch before reporting |
Output
Each PR shows mergeability, CI status, and review state:
auth (1 change, #42 open, synced)
✓ mergeable ✓ CI passing ✓ 1 approval
profile (2 changes, #43 open, needs push)
✗ CI failing ✗ 0/1 approvals ⚠ changes requested
Draft PRs show a simplified status:
payments (1 change, #44 draft, synced)
— draft
With --all, multiple independent stacks are labeled:
Stack 1:
auth (1 change, #42 open, synced)
✓ mergeable ✓ CI passing ✓ 1 approval
profile (2 changes, #43 open, synced)
✓ mergeable ✓ CI passing ✓ 1 approval
Stack 2:
payments (1 change, #44 draft, needs push)
— draft
checkout (3 changes, #45 open, synced)
✗ CI pending ✗ 0/1 approvals
Glossary
| Field | Meaning |
|---|---|
synced / needs push | Whether the local bookmark matches the forge |
#NN open / #NN draft | PR number and state on the forge |
✓ mergeable | The forge reports the PR can merge without conflicts |
✓ CI passing / ✗ CI pending / ✗ CI failing | Aggregate check status for the head commit |
✓ N/M approvals | Approving reviews vs. required approvals |
⚠ changes requested | At least one reviewer has requested changes |
auth
jjpr auth checks or explains forge authentication. Use it when a
forge call returns 401 or 403, or when you’re setting up a new
machine.
jjpr auth test # test forge authentication for the current repo
jjpr auth setup # show auth setup instructions
test
Detects the forge from your remote URL, resolves the token (env var or CLI fallback), makes an authenticated API call, and reports the result. On success, prints the authenticated user. On failure, prints the error and a hint about which env var to set.
setup
Prints setup instructions for the detected forge: which env var the
token reads from, how to scope it, and how stored credentials from
gh or glab are picked up automatically. If no forge is detected
(running outside a jj repo, for instance), prints instructions for
all supported forges.
config
jjpr config manages config files. Use the field reference in
Configuration for what to put in them.
jjpr config init # create global config at ~/.config/jjpr/config.toml
jjpr config init --repo # create repo-local config at .jj/jjpr.toml
init
Writes a config file populated with defaults. Without --repo, writes
the global config at ~/.config/jjpr/config.toml (or
$XDG_CONFIG_HOME/jjpr/config.toml if that’s set). With --repo,
writes .jj/jjpr.toml in the current repo.
If the target file already exists, init refuses to overwrite it.
Configuration
jjpr reads configuration from two optional TOML files.
| Location | Created by | Purpose |
|---|---|---|
~/.config/jjpr/config.toml (or $XDG_CONFIG_HOME/jjpr/config.toml) | jjpr config init | Global defaults |
.jj/jjpr.toml (inside the repo’s .jj/ directory) | jjpr config init --repo | Repo-local overrides |
If neither file exists, jjpr uses built-in defaults. CLI flags override config files. Repo-local config overrides global config.
Global config
merge_method = "squash"
required_approvals = 1
require_ci_pass = true
reconcile_strategy = "rebase"
stack_nav = "comment"
Repo-local config
Repo-local config goes in .jj/jjpr.toml. Because .jj/ is gitignored,
the file is per-clone.
forge = "forgejo"
forge_token_env = "FORGEJO_TOKEN"
stack_nav = "description"
Field reference
merge_method
How the forge combines the PR when it lands.
squash(default): all commits in the PR collapse into one commit on the target branch. Linear history.merge: a merge commit is created. The individual commits from the PR branch are preserved.rebase: commits are rebased onto the target branch individually with no merge commit. Linear history, each commit kept separately.
required_approvals
Number of approving reviews required before merging. Default 1.
require_ci_pass
If true (default), CI checks must pass before merging. Override with
--no-ci-check on a single invocation.
reconcile_strategy
How the remaining stack is synced after a PR is merged.
rebase(default): rebases downstream commits onto the new base. Rewrites history. Pushes become force-pushes.merge: creates merge commits on downstream branches that incorporate the updated base. Pushes stay fast-forward (no force-push events on GitHub) but the history grows merge commits.
stack_nav
Where to show the stack navigation block.
comment(default): a separate comment on each PR.description: embedded in the PR body. More visible to reviewers. Updates the body on eachsubmit.
forge
Forge type. One of github, gitlab, or forgejo. When set,
auto-detection is skipped. Use this for self-hosted instances that
auto-detection can’t recognize. Repo-local only.
forge_token_env
Name of the environment variable that holds the API token. When
unset, jjpr falls back to the forge’s default (GITHUB_TOKEN,
GITLAB_TOKEN, or FORGEJO_TOKEN). Repo-local only.
Configuring forges
Forge-specific authentication and self-hosted instance setup live in Forge support.
Forge support
jjpr auto-detects the forge from your remote URL and talks directly to
the forge’s API over HTTP. Neither gh nor glab is required, but
jjpr picks up their stored credentials when they’re present.
| Forge | Token env var | CLI fallback |
|---|---|---|
| GitHub | GITHUB_TOKEN or GH_TOKEN | gh auth login (reads stored credentials) |
| GitLab | GITLAB_TOKEN | glab auth login (reads stored credentials) |
| Forgejo / Codeberg | FORGEJO_TOKEN | none |
Auto-detection recognizes github.com, gitlab.com, and
codeberg.org, plus Enterprise subdomains for GitHub and GitLab. For
self-hosted instances, set forge in .jj/jjpr.toml. See
Repo-local config.
GitHub
If you already use gh, jjpr finds your credentials automatically.
Otherwise, export GITHUB_TOKEN (or GH_TOKEN) with at least repo
scope.
GitHub Enterprise is auto-detected from the host
(*.github.enterprise.example.com). Credentials come from gh if it’s
configured for that host.
GitLab
If you use glab, jjpr picks up your credentials. Otherwise, export
GITLAB_TOKEN with api scope.
Self-hosted GitLab is auto-detected from the host. No extra config is needed for gitlab.com or for any GitLab instance with a recognizable URL pattern.
Forgejo and Codeberg
Generate an API token with repo scope from your instance’s settings.
For Codeberg, that’s at
https://codeberg.org/user/settings/applications. Export it:
export FORGEJO_TOKEN=your_token_here
For self-hosted Forgejo, set the forge type in .jj/jjpr.toml:
forge = "forgejo"
Auto-detection only recognizes codeberg.org. Other Forgejo hosts
need the explicit forge setting.
Verifying
jjpr auth test
Reports the detected forge, the token source, and the authenticated user. See the auth command for details.
How it works
jjpr is a coordination layer over jj and your forge’s API. It does
no version control of its own. Every VCS step shells out to the jj
binary, and PR state comes from forge APIs over HTTP.
Stack discovery
jjpr discovers stacks by walking each bookmark toward trunk. Starting from a bookmarked commit, it follows parent commits (taking the first parent through merges) until it reaches the trunk branch or a foreign remote bookmark (see Foreign bases below). Each walk produces a path. Bookmarks on that path become segments of a stack.
The result is an adjacency graph keyed by jj change IDs.
- Segment: a contiguous run of commits between two bookmarked commits, or between trunk and the lowest bookmark.
- Stack: a chain of segments rooted at trunk and ending at a bookmark with no bookmarked descendants.
Multiple bookmarks at the same commit collapse into one segment. Bookmarks that don’t connect to your other stacks (a fork from trunk that isn’t an ancestor of any current bookmark, for example) form their own stacks.
Submission planning
When you run submit, merge, or watch, jjpr:
- Builds the change graph from
jj logoutput. - Identifies the target stack (explicit bookmark argument, or
inferred from
trunk()..@). - Compares each bookmark against forge state. Does a PR exist? Does it point at the right base? Has the title or body drifted?
- Plans the minimum set of pushes, PR creations, base retargets, and body updates needed to reach a consistent state.
- Executes the plan.
Step 4 is what makes the command idempotent. If the plan is empty, jjpr prints “Stack is up to date” and exits.
Merge orchestration
merge and watch walk segments from the bottom up. A segment is
mergeable when its PR is non-draft, has the required approvals, has no
requested changes, has no merge conflicts, and (when CI is required)
has passing CI.
After a merge succeeds, jjpr:
- Fetches the updated default branch.
- Reconciles the rest of the stack onto the new base. Rebase by
default, or merge commits if
reconcile_strategy = "merge". - Pushes the updated bookmarks.
- Retargets the next PR’s base if needed.
- Re-evaluates the next segment.
This continues until the stack is empty or the next segment is blocked.
Merge commits
jj new A B produces a commit with two parents. jjpr follows the
first parent through the merge, so the merge commit and its
first-parent ancestry stay in the current stack. The other parent (or
parents) form independent stacks. PRs for merge bookmarks include a
note saying which branches were merged and that the diff may include
their changes until those PRs land.
Diamond-shaped stacks (two branches that fork from a common ancestor and re-merge) are tracked as two separate stacks for submission. The merge commit’s PR carries the explanatory note about the combined diff.
Foreign bases
If a commit in your stack’s ancestry has a remote bookmark that isn’t
one of your local bookmarks, jjpr treats it as a foreign base. Your
first PR targets that branch instead of the default branch, and the
status output annotates the stack with (based on <branch>).
This handles the common case of stacking on a coworker’s PR branch.
Override with --base <branch> when needed.
What jjpr never does
- Modify the working copy without an explicit user-driven command.
- Reimplement jj operations. Every VCS step is
jj <subcommand>. - Cache forge state across invocations. Each run re-fetches.
Code map
For contributors, the source is laid out as follows.
src/jj/:Jjtrait and theJjRunnerimplementation that shells out to thejjbinary.src/forge/:Forgetrait and per-forge backends (GitHub, GitLab, Forgejo) over a sharedureqHTTP client.src/graph/: change graph construction and traversal.src/submit/: analysis, planning, and execution forsubmit.src/merge/: merge planning, execution, and the watch loop.src/watch.rs: thewatchcommand’s outer loop and helpers.
Troubleshooting
“No bookmark in working copy ancestry”
jjpr (or jjpr status) defaults to scoping output to the stack
containing your working copy. If no bookmark is in trunk()..@,
nothing matches.
Two fixes:
jj bookmark set <name> # mark the current change
or to see every stack regardless of working-copy position:
jjpr status --all
watch is the exception. It waits for a bookmark to appear, polling
every few seconds, instead of exiting.
“skipping ‘<name>’ (points to a missing or conflicted commit)”
A local bookmark points at a commit that no longer exists, usually because the corresponding PR was squash-merged on the forge and the commit was rewritten. The warning includes the cleanup command:
jj bookmark forget <name>
jj git push --deleted
After cleanup, re-run jjpr.
“cannot push — some commits have unresolved conflicts”
A commit in your stack has unresolved merge conflicts (often from a rebase that couldn’t auto-resolve). jjpr won’t push conflicted commits.
jj edit <change-id> # the change ID is in the error message
# resolve the conflicts
jjpr submit
“Local sync failed”
jjpr couldn’t push or rebase locally after a merge, so it stopped
before merging the next PR. Common causes: a jj rebase while jjpr
was running, divergent change IDs from concurrent editing, or a
conflicted push.
The just-merged PR is fine on the forge. The remaining open PRs are still open with their bases retargeted; only their local branches are out of date.
To accept the forge state:
jj git fetch
jj bookmark set <bookmark> -r <bookmark>@origin
To fix local state and push it:
jj git fetch
jj rebase -s <change-id> -d main
# resolve any conflicts
jjpr submit
Then re-run jjpr merge to continue. jjpr watch retries
automatically on the next poll once you fix things.
“Forge reconcile failed”
The forge merge succeeded but a follow-up API call (refresh PR list, retarget the next base, update stack-info comments) returned an error. Local state is fine; only the post-merge bookkeeping didn’t complete.
Retry with jjpr merge. If it keeps failing, check jjpr auth test
for token issues, then forge status / network connectivity.
Authentication errors
jjpr auth test
Reports the detected forge, where the token came from, and what the forge said. Common cases:
- No token found: set
GITHUB_TOKEN,GITLAB_TOKEN, orFORGEJO_TOKEN, or rungh auth login/glab auth login. - 403 / insufficient scope: regenerate the token with
reposcope (GitHub/Forgejo) orapiscope (GitLab). - Self-hosted instance: set
forge = "..."in.jj/jjpr.toml. See Forge support.
“PR title not updated after creation”
By design. jjpr creates the PR title from the commit’s first line but doesn’t rewrite it on subsequent submits. If the first line changes, jjpr warns about the drift and leaves the title alone so your manual edits are preserved.
To re-sync, edit the PR title on the forge directly.
“merge already in progress” warnings
A previous merge call returned a transient 502 or 503 right after
GitHub started processing the merge. jjpr polls the PR state for up to
30 seconds to confirm. If the merge actually completed, jjpr
continues. If not, it reports the failure and exits. Re-run to retry.
You only see the polling output when the network round-trip takes a while. Otherwise it’s silent.