Skip to content

Pull requests

When a session’s work needs your team’s review, and not just a local merge, spwn opens the pull request for you. It seeds the PR from the session’s PR doc and then keeps watching it: checks, review state, and whether GitHub will let it merge. Review happens where it already does, on GitHub, with your required checks, CODEOWNERS and approvals. spwn opens the PR, reports on it, and stays out of the way.

Pull requests work with github.com repositories.

Open it from any of these places:

  • the Source control row under a git project in the sidebar (the twisty beside it expands a compact summary in place);
  • Source control · project in the ⌘K palette;
  • the ⇅N chip in the sidebar summary, which counts open PRs and adds a ! when one needs a look;
  • Source control or Create PR… in a session’s Inspector, on the PR tab.

The header is about your project checkout, not a session worktree. It shows:

  • the current branch, with ↓behind and ↑ahead of its upstream, and a ● for uncommitted changes;
  • a link to the repository.

Click the branch name for a menu. It lists local and remote branches (choosing origin/x checks out x) and has Create branch…, which runs git checkout -b.

The buttons run plain git in your project folder:

Button Runs
Fetch git fetch --all --prune
Pull git pull --ff-only. A diverged branch fails instead of making a merge commit.
Push git push, or git push -u origin HEAD when there’s no upstream yet.
Sync Fetch, then pull, then push, stopping at the first error.

A credential prompt fails fast with a readable error instead of hanging. A GitHub auth failure adds a hint pointing at the token.

The list shows every open PR on the repository, whoever opened it (the 100 most recently updated). It also shows PRs your sessions opened after they’ve merged or closed. The header counts N open · M needing a look. A PR needs a look when it’s open and its checks are failing or changes were requested.

Each row shows:

  • State: open, draft, merged or closed.
  • Checks: ✓ N passing, ✗ checks failing, ◷ checks running. No pill means no checks.
  • Review: approved, changes (changes requested) or review (a review is required before it can merge).
  • ⎇ head → base, the author, and linked when the PR belongs to one of your sessions.

Expand a row with ▸ to see each check and its result (linked to the check’s page) and the requested reviewers. Request… adds reviewers by GitHub login.

Below the PRs, spwn lists every agent session with a branch but no open PR. Each shows its branch and base, ↑ahead, ↓behind, changed files and uncommitted changes, plus:

  • no doc when the branch has no PR doc yet;
  • #N? when its PR doc names a PR number the repository doesn’t have.

Create PR… is enabled once the branch has commits its base doesn’t.

Key Action
j / ↓, k / ↑ Next / previous pull request
↵ Open it on GitHub
r Refresh pull requests now

Click Create PR… on a session. The Open a pull request dialog is filled in from the session’s PR doc without calling GitHub:

  • Title: editable.
  • From: the session’s branch.
  • Into: the base. It defaults to the session’s base branch, otherwise the repository’s default branch.
  • Reviewers: GitHub logins, comma separated. Optional.
  • Open as a draft.
  • Body, as it will be sent: read-only. Edit the PR doc to change it.

Push and open (or Open as draft) then:

  1. pushes the session’s branch (-u origin HEAD if it has no upstream);
  2. opens the PR;
  3. requests reviewers. If this fails, you get a warning and the PR is kept;
  4. writes the PR back into the PR doc.

Only committed work goes into the PR. The dialog warns you if the worktree has uncommitted changes, and leaves them where they are.

Every session has a PR doc at .spwn/pr/<branch>.md in its worktree, with the slashes in the branch name turned into dashes. It’s committed on the session’s own branch. Edit it in the Inspector’s PR tab, or have the agent write it. It starts with TOML front matter between +++ fences, followed by markdown:

+++
title = "Retry idempotent requests on 502/503"
risk = "low"
ticket = "SHOP-412"
+++
Wraps the API client's `send` in a retry with jittered backoff…

The front matter is free-form: any keys you like. The pull request is rendered from it:

  • Title: the title key. Failing that, the prose’s first # heading, then the session’s title, then the branch name.
  • Body: the prose, followed by a block spwn owns, fenced by <!-- spwn:begin --> / <!-- spwn:end -->. The block holds:
    • a table of your front-matter keys;
    • the session’s cost, when the doc has one;
    • a footer naming the branch, the session and the doc.

After the PR is opened, spwn writes three keys into the front matter:

pr = 142
pr_url = "https://github.com/acme/shop/pull/142"
pr_repo = "acme/shop"

If a turn is running, the change rides that turn’s commit. Otherwise spwn commits the doc on its own (spwn: pr #142) and pushes it. Checks, reviews and mergeability are never written to the doc. They change too often, and spwn keeps them in memory.

Push update on the Inspector’s PR tab re-renders the title and replaces only the fenced block of the PR body. Anything a reviewer added outside the fence stays.

spwn doesn’t open a second PR for a branch that already has one:

  • If the PR doc already names pr = N and GitHub’s #N has the same head branch, spwn adopts it. The dialog says so beforehand.
  • If GitHub answers that a PR for the branch already exists, spwn finds it and adopts it.

The toast reads Adopted #N. A PR is linked to its session by the doc’s pr number, or else by its head branch. That’s also how a PR an agent opened itself with gh inside its pane shows up as linked.

On a pull request row:

  • #N opens it on GitHub.
  • Ready for review / Convert to draft.
  • Sync from base merges the base into the session’s branch, in its own worktree. It’s offered when GitHub reports the PR conflicting or behind, and the PR is linked to a session. Conflicts go to the session’s agent, as with Sync from base.
  • Merge, with a ▾ method menu: merge, squash or rebase. The default is squash, and spwn remembers your choice per project. The confirm dialog also offers Merge and delete the branch. spwn sends the head commit it showed you, so if the branch moved since you looked, GitHub refuses and you’re asked to refresh.
  • Close, and Reopen for a closed (not merged) PR.
  • Request… reviewers, in the expanded row.

Merge is enabled only when GitHub says it can go. Otherwise the button says why:

  • A draft — mark it ready for review first
  • Conflicts with main
  • Blocked: a required check or review hasn’t passed
  • Behind main — the base requires branches to be up to date

Some checks failing, with none of them required, still allows a merge, with a note.

On a session row: Create PR…, and click the title to open the session.

Add a personal access token in Settings → GitHub, or on the first-run screen. It needs one of:

  • a classic token with the repo scope;
  • a fine-grained token with read and write access to Contents and Pull requests.

The token is saved to github-token in spwn’s app data folder with mode 0600, apart from the other settings, and is never sent back to the browser.

git gets it through a credential helper that spwn passes as environment config (GIT_CONFIG_COUNT / GIT_CONFIG_KEY_n / GIT_CONFIG_VALUE_n):

  • The helper is scoped to https://github.com, so the token is never offered to another host.
  • Your ~/.gitconfig isn’t touched.
  • Every git that spwn starts picks it up: its own fetch/pull/push, hooks, and the shells and agents in its panes.
  • With no token saved, the helper stays silent and your own credential helpers apply.
  • The helper reads the file on each use, so changing or removing the token reaches panes that are already open.

gh inside a session gets a config directory of spwn’s own, gh/hosts.yml in the app data folder, through GH_CONFIG_DIR. That lets an agent run gh pr view or gh pr create as you. spwn uses a config file instead of GH_TOKEN so the token never shows up in ps output.

Every agent and every PR uses the one token, so GitHub attributes all of it to that token’s owner.

The GitHub API allowance is shared. Every gh an agent runs spends from the same budget as spwn’s own requests. spwn keeps its share small:

  • Nothing polls until you look. A repository is polled only while its Source control panel is open. The sidebar chip reads the cache.
  • One request per repository per refresh, however many PRs there are. It’s a single GraphQL query that returns every open PR with its checks, review and mergeability, plus the remaining rate-limit points. Two projects on the same repository share it.
  • Expanding a row costs a few extra requests, for that PR only.

How often it polls:

Situation Every
Panel open, repository has PRs 45 s
Panel open, no PRs 5 min
Just after spwn changed something (opened, merged, closed…) 3 s, 8 s, 20 s, then back to normal
After failures Doubling from 45 s, up to 10 min
Under 200 points left Spread over what’s left of the rate-limit window, up to 15 min
Rate-limited Not until the limit resets

Writes are capped at a quarter of GitHub’s own limits: 20 a minute and 125 an hour per repository. A write that would have to wait more than 10 seconds fails with a message instead.

When GitHub rate-limits spwn, the panel keeps showing the last list, says so, and counts down to the next try. The header’s as of 2m ago tells you how fresh the list is.

spwn reads the origin remote and works out the host from its URL (HTTPS, git@ or ssh://). Pull requests are supported on github.com only. GitHub Enterprise, GitLab and other forges get a banner naming the host, such as spwn only works with pull requests on github.com so far (this remote is on gitlab.com), and the branch header’s Fetch, Pull and Push keep working.

A few more cases:

  • If the token can’t push to the repository, spwn lists PRs but won’t offer to open or merge them.
  • Cross-fork PRs, from a fork to its upstream, aren’t supported yet.

spwn serves every project it manages over git’s smart HTTP protocol, read-only:

Terminal window
git clone http://localhost:4317/git/shop

The last part of the URL is the project’s name or id. Any stock git client works, so a CI runner or an editor on another machine can fetch from spwn without spwn installed. Session branches live in the same repository, so a clone includes them as origin/spwn/<id>:

Terminal window
git log origin/main..origin/spwn/3f9c2a1b

git push is refused with this remote is read-only.

When spwn runs with accounts turned on (anything but a 127.0.0.1 bind, or --auth), git asks for credentials. Use an access token as the password. See Remote access.