Metadata-Version: 2.4
Name: pr-bot
Version: 0.3.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
Requires-Dist: textual>=0.80
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`) on PyPI — one install, works in every repo
- a sibling CLI (`toolbar`) for repo bootstrap + workspace ops
- a Claude Code plugin shipping ~28 slash commands (`/pr-*`, `/release-*`,
  `/hotfix-*`, `/stack-*`, `/policy-*`, `/toolbar-*`) that thinly wrap the CLI
- a Textual terminal dashboard (`pr-bot ui`)

## 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: PyPI via uvx

Every slash command invokes the CLI through `uvx`, which fetches `pr-bot`
from PyPI into a cached isolated env per invocation. No shared venv to
manage; upgrades are atomic (bump the version, invalidate the cache).

```bash
# verify
uvx pr-bot --version
```

Pin to a specific release for reproducibility:

```bash
export PR_BOT_SOURCE='pr-bot==0.2.0'              # exact version
export PR_BOT_SOURCE='git+https://github.com/andrewrothstein/pr-bot@0.2.0'
export PR_BOT_SOURCE=/path/to/clone                # dev loop
```

The slash commands default to `PR_BOT_SOURCE=pr-bot` (= latest on PyPI).

### Convenience: plain CLI on PATH

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

```bash
uv tool install pr-bot
```

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

### Slash commands

#### Recommended: install as a Claude Code plugin

```bash
# inside Claude Code:
/plugin marketplace add andrewrothstein/pr-bot
/plugin install pr-bot@pr-bot
```

That's it. All `/pr-*`, `/release-*`, `/hotfix-*`, `/stack-*`,
`/policy-*`, `/toolbar-*` slash commands are available in any project
once the plugin is installed.

#### Alternative: symlink locally

If you'd rather use a clone of this repo directly:

```bash
ln -s /path/to/pr-bot/commands ~/.claude/commands/pr-bot
# or, for a project-local install:
ln -s /path/to/pr-bot/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. See [`docs/policy.md`](docs/policy.md). |
| `pr-bot policy check`  | `/policy-check`  | Diff GitHub's actual branch protection vs. declared policy.        |
| `pr-bot ui`            | —                | Launch the Textual terminal dashboard. See [`docs/ui.md`](docs/ui.md). |

## 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 prepare 1.4.0                            # bumps pyproject + auto-drafts notes + commits + pushes
pr-bot release tag 1.4.0 --notes-file docs/releases/1.4.0.md
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.

## Stacked PRs

For changes too large for one PR but logically a unit, build a chain of
dependent PRs and land them in order:

| Command                          | Slash             | What it does                                                              |
| -------------------------------- | ----------------- | ------------------------------------------------------------------------- |
| `pr-bot stack new <slug-1> <slug-2> [...]`  | `/stack-new`      | Create a chain of PR branches (each branched off the previous).           |
| `pr-bot stack push`              | `/stack-push`     | Push every branch + open/update PRs with cross-link `<!-- pr-bot:stack -->` block. |
| `pr-bot stack sync`              | `/stack-sync`     | Rebase the chain onto `origin/main`; force-push each branch.              |
| `pr-bot stack land [--all]`      | `/stack-land`     | Squash-merge the bottom branch + pop from state. `--all` lands the whole stack. |
| `pr-bot stack status`            | `/stack-status`   | Show branches, PR numbers, draft state.                                   |
| `pr-bot stack abandon`           | `/stack-abandon`  | Delete the stack state file (branches/PRs kept).                          |

State lives in `.git/pr-bot/stack.json` (per-clone, never committed). See
[`docs/design-pr-stack.md`](docs/design-pr-stack.md) for the design.

## 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

If you want the full pr-bot model wired up from scratch (pyproject + ruff
+ ratchet-pinned CI + CLAUDE.md + AGENTS.md + LICENSE + a cut `release/0.1`):

```bash
toolbar repo template my-thing --description "What it does"
# → creates ./my-thing/, git init, scaffolds, gh repo create --private --push
```

Or, if you already have a repo and just need it pushed up:

```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>`.

**Want a pinned version of `pr-bot` instead of always-latest?**
Override `PR_BOT_SOURCE`:

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

## 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 (`src/pr_bot/names.py`, `pr_bot/policy.py`, `pr_bot/stack.py`,
`pr_bot/changelog.py`, `pr_bot/release_notes.py`, `pr_bot/pyproject.py`,
`toolbar/env.py`, `toolbar/template.py`) is unit-tested in `tests/`.
Subprocess wrappers (`pr_bot.git`, `pr_bot.gh`) and the higher-level
command modules (`pr_bot.commands.{pr,release,hotfix,stack}`) are
integration-tested against a real git repo in `tmp_path` plus a `gh`
shim on `PATH`. Total: 148 tests.

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

## Versioning

SemVer on the matching release line.

- **0.x.y** — preview. CLI surface, file formats, and the importable
  Python API may change between minor releases.
- **1.0+** — stable surfaces locked. See [`docs/stability.md`](docs/stability.md)
  for the full list of what's stable, what's experimental, and the
  deprecation policy.

## 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/stability.md`](docs/stability.md) — 1.0 stability commitment.
- [`docs/release-checklist.md`](docs/release-checklist.md) — manual smoke-test steps run against the live GitHub API before tagging.
- [`docs/releases/`](docs/releases/) — release notes.
