Metadata-Version: 2.4
Name: pr-bot
Version: 0.2.0
Summary: PR lifecycle helpers for a main + release/X.Y + feat|fix|hotfix/slug branching model.
Author-email: Andrew Rothstein <andrew.rothstein@gmail.com>
License: MIT
License-File: LICENSE
Requires-Python: >=3.14
Requires-Dist: cyclopts>=3.0
Description-Content-Type: text/markdown

# pr-bot

PR lifecycle helpers for a **main + `release/X.Y` + `{feat,fix,hotfix}/<slug>`**
branching model. Linear history, squash merges to `main`, fast-forward-only
`main`, semver (`X.Y.Z`) tags on release branches.

Ships as:

- a single Python CLI (`pr-bot`) — one install, works in every repo
- a set of Claude Code slash commands (`/pr-start`, `/release-cut`, …) that are
  thin wrappers over the CLI

## The model

```
main ─────●──●──●──●──●──────●──●──●──────────●────────▶  (FF-only, linear)
            \             \                    \
             feat/login    fix/off-by-one       release/1.4 ──●──●──▶
                                                               │  │
                                                               │  └── tag 1.4.1
                                                               │
                                                               hotfix/npe-on-logout
```

- **Branches off `main`:** `feat/<slug>`, `fix/<slug>`.
  Squash-merged back into `main`; branch is deleted on merge.
- **Release branch:** `release/X.Y` is cut from a commit on `main`.
  Long-lived. Receives only hotfixes.
- **Tags:** `X.Y.Z` (annotated) placed on a commit on the matching
  `release/X.Y` branch.
- **Hotfixes:** `hotfix/<slug>` off `release/X.Y`, PR'd back to
  `release/X.Y`, then forward-ported to `main` as a `fix/<slug>` PR.

## Prerequisites

| Tool  | Minimum | Why                                                         |
| ----- | ------- | ----------------------------------------------------------- |
| `uv`  | 0.4+    | Runs the CLI hermetically via `uvx`; bootstraps Python 3.14 |
| `git` | 2.30+   | `--force-with-lease`, `symbolic-ref --short`                |
| `gh`  | 2.40+   | `pr create/merge`, `repo create`, `release create`          |

`gh` must be authenticated (`gh auth status`). For git pushes, run
`gh auth setup-git` once — pr-bot relies on gh's credential helper.

## Install

### Recommended: no install, hermetic per invocation

Every slash command invokes the CLI through `uvx`. Each call runs in a
cached, isolated environment with deps pinned by `pyproject.toml` — nothing
is installed into any shared venv, and upgrades are atomic (bump the
version, invalidate the cache).

Point `uvx` at a source by setting one env var:

```bash
# a local clone (dev loop)
export PR_BOT_SOURCE=/path/to/pr-bot

# or a git ref (pinned release — most hermetic)
export PR_BOT_SOURCE='git+https://github.com/arothste/pr-bot@1.0.0'
```

The slash commands default to `PR_BOT_SOURCE=pr-bot`, which resolves from
PyPI once the package is published there.

### Convenience: plain CLI on PATH (not hermetic)

If you want `pr-bot` on your PATH instead of going through `uvx` each time:

```bash
uv tool install /path/to/pr-bot
```

This creates a shared user venv — fine for a workstation, but it's one
global install, not per-invocation isolation.

### Slash commands

Claude Code discovers slash commands from `~/.claude/commands/` (user) or
`.claude/commands/` (project). Symlink the directory so updates propagate:

```bash
ln -s /path/to/pr-bot/.claude/commands ~/.claude/commands/pr-bot
# or, for a project-local install:
ln -s /path/to/pr-bot/.claude/commands .claude/commands/pr-bot
```

(Plain `cp` also works if you prefer a detached copy.)

## Commands

Everything is `pr-bot <group> <subcommand>`. Each has a slash-command twin.

### Starting and landing a PR

| Command                          | Slash             | What it does                                                      |
| -------------------------------- | ----------------- | ----------------------------------------------------------------- |
| `pr-bot pr start feat <slug>` | `/pr-start`       | Fetch + FF main, create `feat/<slug>` off it.                     |
| `pr-bot pr start fix <slug>`  | `/pr-start`       | Same, but `fix/<slug>`.                                           |
| `pr-bot pr open [--draft]`       | `/pr-open`        | Push current branch, `gh pr create` against main.                 |
| `pr-bot pr sync`                 | `/pr-sync`        | Rebase onto `origin/main`, force-push with lease.                 |
| `pr-bot pr ready`                | `/pr-ready`       | Mark draft PR ready for review.                                   |
| `pr-bot pr merge [--auto]`       | `/pr-merge`       | Squash-merge, delete remote branch, fetch + FF main, prune local. |
| `pr-bot pr cleanup`              | `/pr-cleanup`     | Finish the merge dance after a GitHub-UI merge (FF main + delete local branch). |
| `pr-bot pr status`               | `/pr-status`      | Ahead/behind main + open PR state.                                |

### Releases

| Command                          | Slash             | What it does                                                      |
| -------------------------------- | ----------------- | ----------------------------------------------------------------- |
| `pr-bot release cut X.Y`         | `/release-cut`     | Create `release/X.Y` off main (or `--from <ref>`), push.          |
| `pr-bot release prepare X.Y.Z`   | `/release-prepare` | Bump `pyproject.toml`, regenerate `CHANGELOG.md`, draft missing notes, commit, push. |
| `pr-bot release tag X.Y.Z`       | `/release-tag`     | Tag `X.Y.Z` on `release/X.Y`, push, create GitHub release. Accepts `--notes-file`. |
| `pr-bot release notes-draft X.Y.Z` | `/release-notes-draft` | Auto-draft `docs/releases/X.Y.Z.md` from PRs merged since the last tag, grouped by conventional-commit prefix. |
| `pr-bot release changelog`       | `/release-changelog` | Regenerate `CHANGELOG.md` from `docs/releases/*.md` (sorted desc). |
| `pr-bot release list`            | `/release-list`    | List release branches + their highest tag.                        |
| `pr-bot release next-patch`      | —                  | Print the next unused patch version for the current series.       |

### Hotfixes

| Command                                        | Slash             | What it does                                                      |
| ---------------------------------------------- | ----------------- | ----------------------------------------------------------------- |
| `pr-bot hotfix start <slug> --on X.Y`          | `/hotfix-start`   | `hotfix/<slug>` off `release/X.Y`.                                |
| `pr-bot hotfix open --on X.Y`                  | `/hotfix-open`    | Push + `gh pr create` against `release/X.Y`.                      |
| `pr-bot hotfix forward-port <sha>`             | `/hotfix-forward` | New `fix/<slug>` off main, cherry-picks the hotfix commit.        |

### Sanity

| Command       | Slash       | What it does                                                        |
| ------------- | ----------- | ------------------------------------------------------------------- |
| `pr-bot doctor`        | `/pr-doctor`     | Check main is linear, branches/tags match the model, remote wired. |
| `pr-bot policy apply`  | `/policy-apply`  | Push `.pr-bot.toml` policy to GitHub branch protection.            |
| `pr-bot policy check`  | `/policy-check`  | Diff GitHub's actual branch protection vs. declared policy.        |

## Typical workflows

### Feature PR

```bash
pr-bot pr start feat add-login-button
# …hack hack hack; commit as usual…
pr-bot pr open --draft
# …more commits…
pr-bot pr sync          # keep up with main
pr-bot pr ready
pr-bot pr merge         # squashes, deletes branch, fast-forwards main
```

### Cut and ship 1.4.0

```bash
pr-bot release cut 1.4            # creates release/1.4
pr-bot release tag 1.4.0          # annotated tag + gh release
pr-bot release list               # confirm
```

### Hotfix for a released version

```bash
pr-bot hotfix start fix-npe-on-logout --on 1.4
# …fix, commit…
pr-bot hotfix open --on 1.4       # PR into release/1.4
# …merge via GitHub UI or gh pr merge, whichever your repo uses…
pr-bot release tag 1.4.1          # ship it
pr-bot hotfix forward-port <sha>  # carry the fix to main as fix/<slug>
pr-bot pr open
pr-bot pr merge
```

## Why these rules

- **Linear main** makes `git bisect`, `git log`, and blame actually useful.
- **Squash merges** give one commit per PR, keeping PR == unit of change.
- **FF-only main** means main never absorbs stale state; rebasing is the only
  way to integrate.
- **`release/X.Y` + `X.Y.Z` tags** cleanly separate "what's next" from "what
  shipped", and let hotfixes target exactly one released line.

## toolbar — workspace-admin helpers

Ships alongside `pr-bot` from this same repo. Scope: things you do *around*
a repo (create it, wire up a remote), as opposed to what `pr-bot` does
*inside* one once it exists.

| Command                                | Slash                    | What it does                                                              |
| -------------------------------------- | ------------------------ | ------------------------------------------------------------------------- |
| `toolbar repo info`                       | `/toolbar-repo-info`      | Dry-run: show the resolved target (`org/name`, URL, host).                |
| `toolbar repo create [--public] [--push]` | `/toolbar-repo-create`    | `gh repo create` (private by default) + set `origin`; `--push` optional.  |
| `toolbar repo template <name>`            | `/toolbar-repo-template`  | Bootstrap a new project: render skeleton + git init + cut release/0.1 + push to GitHub. |
| `toolbar repo push-main`                  | —                         | Push local main/master after a clean-tree sanity check.                   |

Honors two env vars for defaults (flags override):

```
GH_MIRROR   (default: https://github.com)
GH_ORG      (default: andrewrothstein)
```

`GH_HOST` (from the `gh` CLI) takes precedence over the host parsed from
`GH_MIRROR` — useful when the browser URL differs from the API host.

### Bootstrap a brand-new repo

```bash
cd /path/to/newproj
git init -b main && git commit --allow-empty -m "initial"

toolbar repo info                 # confirm target
toolbar repo create --push        # private repo + push main
```

## Troubleshooting

**`git push` fails with "Repository not found" right after `toolbar repo create`.**
Run `gh auth setup-git` once. `gh` wires itself in as a git credential helper
for `github.com` (and any `GH_HOST` you've set). Without it, `git push` has
no token and gets a 401, which GitHub surfaces as "not found".

**`pr-bot pr sync` stops on a rebase conflict.**
That's the expected behavior — pr-bot does not auto-resolve conflicts.
Resolve with the usual `git add` / `git rebase --continue` flow, then
`git push --force-with-lease origin <branch>`.

**`uvx --from pr-bot pr-bot ...` fails with "package not found".**
pr-bot isn't on PyPI yet. Set `PR_BOT_SOURCE` to a local clone or a git
ref:

```bash
export PR_BOT_SOURCE='git+https://github.com/andrewrothstein/pr-bot@0.1.0'
# or
export PR_BOT_SOURCE=/path/to/your/clone
```

## Contributing

This repo dogfoods its own model. For every change:

```bash
pr-bot pr start feat <slug>
# …edit, commit…
uv run pytest -q && uv run ruff check src tests
pr-bot pr open --draft
pr-bot pr ready
pr-bot pr merge
```

Pure logic goes in `src/pr_bot/names.py` or `src/toolbar/env.py` — unit
tests cover any changes there. Subprocess wrappers live in `pr_bot.git`
(integration-tested in `tests/test_git_integration.py` against a real
git repo in `tmp_path`) and `pr_bot.gh` (tested manually — they talk
to the GitHub API).

Error messages should start with `pr-bot:` or `toolbar:` so callers (and
agents) can grep them.

## Versioning

SemVer on the matching release line.

- **0.1.x** — initial preview. APIs and command names may change.
- **1.0** — ships after a couple weeks of dogfooding and a PyPI publish.

## Publishing

Releases to PyPI happen automatically on tag push (`X.Y.Z`) via a
Trusted Publisher — no API tokens in repo secrets. See
[`docs/publishing.md`](docs/publishing.md) for the one-time PyPI setup
and the per-release flow.

## See also

- [`AGENTS.md`](AGENTS.md) — how AI agents should drive `pr-bot` in any repo.
- [`CLAUDE.md`](CLAUDE.md) — project-specific instructions for Claude Code
  agents working on this repo.
- [`docs/releases/`](docs/releases/) — release notes.
