Stacked PRs for GitHub — agent-first CLI for parallel development
Project description
ez
Stacked PRs for GitHub.
ez is a fast, lightweight CLI for managing stacked pull requests on GitHub. It shells out to git and gh so there's nothing magical happening under the hood — just the tools you already know, orchestrated intelligently.
Why stacked PRs?
Large pull requests are hard to review. Stacked PRs let you break work into a chain of small, focused branches where each branch builds on the one below it:
main
└── feat/auth-types ← PR #1 (data models)
└── feat/auth-api ← PR #2 (API routes, depends on #1)
└── feat/auth-ui ← PR #3 (frontend, depends on #2)
Reviewers see small diffs. You keep working without waiting. When PR #1 merges, ez rebases the rest of the stack automatically.
The problem is that git doesn't know about stacks. Rebasing, reordering, and keeping GitHub PRs pointed at the right base branch is tedious and error-prone. ez handles all of that for you.
Quick start
# Install
pip install ez-stack
ez setup --yes
# Initialize in any git repo
cd your-repo
ez init
# Start building a stack
ez create feat/parse-config
# ... make changes ...
ez commit -m "add config parser"
ez create feat/use-config
# ... make changes ...
ez commit -m "wire config into app"
# Push the entire stack and open PRs for all branches
ez submit
That's it. Two PRs, correctly chained, with GitHub base branches set automatically.
Using ez with AI Agents
ez is designed to work well in automated and agentic contexts:
- JSON output —
ez status --jsonandez log --jsonemit machine-readable data that agents can parse without screen-scraping. - Non-interactive flags —
ez checkout <name>orez checkout 42bypasses the TUI picker.ez commit --if-changedexits cleanly when there's nothing to commit. - Autostash —
ez sync --autostasheliminates thegit stash && ... && git stash popdance. - Structured exit codes — each failure mode has a distinct exit code (rebase conflict = 3, stale remote = 4, usage error = 5, unstaged changes = 6) so agents can take the right recovery action.
Install the ez skill
If your agent supports Skills, install the repo's ez-workflow skill directly from GitHub:
npx skills add https://github.com/rohoswagger/ez-stack --skill ez-workflow
That installs the instructions from SKILL.md, which teaches the agent when to use ez instead of raw git/gh commands in an ez-managed repo.
Quick install check
pip install ez-stack && ez --version
Key agent patterns
# Parse current branch state
ez status --json
# → {"branch":"feat/x","parent":"main","pr_number":42,"children":[],"needs_restack":false,...}
# Only commit if there are staged changes
ez commit -m "chore: format" --if-changed
# Sync without a manual stash dance
ez sync --autostash
# Get just the PR URL for scripting
open $(ez pr-link)
# Find branches that need restacking
ez log --json | jq '.[] | select(.needs_restack)'
For agents that support local instruction files, AGENTS.md contains a compact install-and-usage guide. For skills-based agents, install ez-workflow with npx skills add and use that skill directly.
Agent setup prompt
Copy-paste this into any AI coding agent to get ez installed and configured:
Install ez-stack, a CLI for stacked PRs on GitHub, and set it up in this repo.
Steps:
- Install:
pip install ez-stack(or update:ez update)- Configure shell:
ez setup --yes- Initialize in this repo:
ez init- Install the skill:
ez skill install(agents in this repo auto-discover it)- Add these rules to the project's CLAUDE.md (or equivalent agent instructions file):
## ez-stack This repo uses `ez` for stacked PRs. When `.git/ez/stack.json` exists, use `ez` instead of raw git/gh commands: - `ez create <name>` instead of `git checkout -b` - `ez commit -m "msg"` instead of `git commit` (auto-restacks children, shows diff stat) - `ez commit -m "msg" -- path1 path2` to stage specific files - `ez diff --stat` instead of `git diff main...HEAD --stat` - `ez parent` instead of parsing `ez status` for the parent branch - `ez push` instead of `git push` / `gh pr create` - `ez submit` to push the entire stack - `ez sync --autostash` to sync with trunk Key flags: `--json` on status/log for machine output, `--if-changed` on commit to no-op when nothing staged, `--from <base>` on create to skip checkout. Output: every command appends `[ok | 45ms]` or `[exit:3 | 120ms]` to stderr. Exit codes: 0=ok, 2=gh error, 3=conflict, 4=stale ref, 5=usage error, 6=unstaged changes.
Commands
Stack creation & editing
| Command | Description |
|---|---|
ez init |
Initialize ez in the current repository |
ez create <name> |
Create a new branch stacked on the current branch |
ez create <name> -m "msg" |
Create branch and commit staged changes in one step |
ez create <name> -am "msg" |
Create branch, stage all tracked changes, and commit |
ez create <name> --from <base> |
Create branch from a specific base without checking it out first |
ez commit -m <msg> |
Commit staged changes, restack children, show diff stat |
ez commit -m "subj" -m "body" |
Multi-paragraph commit (repeated -m, like git) |
ez commit -m <msg> -- <paths> |
Stage specific paths and commit |
ez commit -m <msg> --if-changed |
Commit only if there are staged changes (no-op otherwise) |
ez amend |
Amend the last commit and restack children |
ez delete [<name>] |
Delete a branch from the stack and restack |
ez move --onto <branch> |
Reparent the current branch onto another branch |
Syncing & rebasing
| Command | Description |
|---|---|
ez sync |
Fetch trunk, detect merged PRs, clean up, and restack |
ez sync --dry-run |
Preview what sync would do without making changes |
ez sync --autostash |
Stash uncommitted changes before sync, restore after |
ez restack |
Rebase each branch onto its parent |
Navigation
| Command | Description |
|---|---|
ez up |
Check out the branch above the current one |
ez down |
Check out the branch below the current one |
ez top |
Check out the top of the stack |
ez bottom |
Check out the bottom of the stack |
ez checkout |
Interactively select a branch to check out |
ez checkout <name> |
Switch directly to a branch by name (non-interactive) |
ez checkout <number> |
Switch directly to a branch by PR number (non-interactive) |
GitHub integration
| Command | Description |
|---|---|
ez push |
Push current branch only and create/update its PR |
ez push --title "..." --body "..." |
Push and set/update PR title and body |
ez push --base <branch> |
Push and override the PR base branch |
ez submit |
Push all branches in the stack and create/update all PRs |
ez pr |
Open the current branch's PR in the browser |
ez pr-link |
Print the PR URL to stdout (pipeable) |
ez pr-edit |
Edit the PR body in $EDITOR |
ez pr-edit --title "..." --body "..." |
Edit the PR title/body directly |
ez draft |
Mark the current PR as a draft |
ez ready |
Mark the current PR as ready for review |
ez merge |
Merge the bottom PR of the stack via GitHub |
Inspection & diffing
| Command | Description |
|---|---|
ez log |
Show the full stack with branch names, commit counts, and PR status |
ez log --json |
Show the full stack as a JSON array (machine-readable) |
ez status |
Show the current branch and its position in the stack |
ez status --json |
Show current branch info as JSON (machine-readable) |
ez diff |
Show diff of current branch vs parent (what the PR reviewer sees) |
ez diff --stat |
Show only the diffstat summary |
ez diff --name-only |
Show only changed file names |
ez parent |
Print the parent branch name to stdout (pipeable) |
ez branch |
List all branches with PR numbers and worktree paths |
Updating
| Command | Description |
|---|---|
ez update |
Update ez to the latest version |
ez update --check |
Check for updates without installing |
ez update --version v0.1.12 |
Install a specific version |
ez push vs ez submit
ez push |
ez submit |
|
|---|---|---|
| Scope | Current branch only | All branches from trunk to current |
| PRs | Creates/updates PR for current branch | Creates/updates PRs for all branches |
| When to use | Iterating on a single branch | First push of a stack, or after restacking all branches |
Note: Running
gh pr createafterez pushwill fail —ez pushalready created the PR.
Example workflow
Here's a complete session building a three-branch stack:
# 1. Start from main
git checkout main && git pull
ez init
# 2. Create the first branch in the stack
ez create feat/auth-types
cat > src/auth/types.rs << 'EOF'
pub struct User { pub id: u64, pub email: String }
pub struct Session { pub token: String, pub user_id: u64 }
EOF
ez commit -m "define User and Session types"
# 3. Stack a second branch on top
ez create feat/auth-api
cat > src/auth/api.rs << 'EOF'
pub fn login(email: &str) -> Session { /* ... */ }
pub fn logout(session: &Session) { /* ... */ }
EOF
ez commit -m "add login/logout API"
# 4. Stack a third branch on top
ez create feat/auth-middleware
cat > src/middleware/auth.rs << 'EOF'
pub fn require_auth(req: &Request) -> Result<User, AuthError> { /* ... */ }
EOF
ez commit -m "add auth middleware"
# 5. See the full stack
ez log
# main
# ├── feat/auth-types (1 commit)
# │ ├── feat/auth-api (1 commit)
# │ │ ├── feat/auth-middleware (1 commit) ← you are here
# 6. Push everything and open PRs
ez submit
# Creates 3 PRs:
# feat/auth-types → main
# feat/auth-api → feat/auth-types
# feat/auth-middleware → feat/auth-api
# 7. After feat/auth-types is reviewed and merged on GitHub:
ez sync
# Fetches main (which now includes auth-types),
# rebases auth-api onto main, rebases auth-middleware onto auth-api,
# deletes the merged feat/auth-types branch,
# and updates PR base branches on GitHub.
ez create with a commit message
ez create accepts -m to commit staged changes in one step:
git add src/auth.rs
ez create feat/auth -m "add auth module"
# equivalent to: ez create feat/auth && ez commit -m "add auth module"
Use -a to stage all tracked changes automatically (like git commit -a):
ez create feat/auth -am "add auth module"
# equivalent to: git add -A && ez create feat/auth -m "add auth module"
How it works
ez is intentionally simple in its architecture:
- No custom git internals. Every git operation is a call to the
gitCLI. Every GitHub operation goes throughgh. You can always see exactly what happened by reading your git log. - Stack metadata is stored in
.git/ez/stack.json— a single JSON file tracking branch order, parent relationships, and associated PR numbers. It's local to your repo and ignored by git. - Restacking uses
git rebase --ontoto move each branch in the stack onto its updated parent. This is the same operation you'd do by hand;ezjust does it for every branch in the right order. - PR management calls
gh pr createandgh pr editto set base branches so GitHub shows the correct, minimal diff for each PR in the stack.
Stack metadata format
{
"version": 1,
"trunk": "main",
"branches": [
{ "name": "feat/auth-types", "parent": "main", "pr": 101 },
{ "name": "feat/auth-api", "parent": "feat/auth-types", "pr": 102 },
{ "name": "feat/auth-middleware", "parent": "feat/auth-api", "pr": null }
]
}
Prerequisites
- git 2.38+
- gh (GitHub CLI), authenticated via
gh auth login - A GitHub repository with push access
Installation
pip install ez-stack
ez setup --yes
ez init
That's it. pip install gives you the ez binary. ez setup configures your shell for auto-cd on worktree create/delete/switch. ez init initializes stacking in your repo.
Other install methods
Pre-built binaries are also available on the Releases page, or via the install script:
curl -fsSL https://raw.githubusercontent.com/rohoswagger/ez-stack/main/install.sh | bash
Contributing
Contributions are welcome! Please read CONTRIBUTING.md for development setup, code style, and how to submit changes.
License
MIT. See LICENSE for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ez_stack-0.2.11-py3-none-manylinux_2_17_x86_64.whl.
File metadata
- Download URL: ez_stack-0.2.11-py3-none-manylinux_2_17_x86_64.whl
- Upload date:
- Size: 727.0 kB
- Tags: Python 3, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cdbcd70c93062f6028a6e6ad9fd2102cbf4c286b42f30d0f4a3dc0bc4be7a5ff
|
|
| MD5 |
5b1d2919625f277718c0925e92cdaaf6
|
|
| BLAKE2b-256 |
d21e4da84c6f8a2f73d2971d93ff785cc9fccdcd8f1ea33dafe0364d9c4a6436
|
Provenance
The following attestation bundles were made for ez_stack-0.2.11-py3-none-manylinux_2_17_x86_64.whl:
Publisher:
python-wheel.yml on rohoswagger/ez-stack
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ez_stack-0.2.11-py3-none-manylinux_2_17_x86_64.whl -
Subject digest:
cdbcd70c93062f6028a6e6ad9fd2102cbf4c286b42f30d0f4a3dc0bc4be7a5ff - Sigstore transparency entry: 1201748055
- Sigstore integration time:
-
Permalink:
rohoswagger/ez-stack@d0fba2011e69250bc6f33d66abb64fba4a8b6aa1 -
Branch / Tag:
refs/tags/v0.2.11 - Owner: https://github.com/rohoswagger
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-wheel.yml@d0fba2011e69250bc6f33d66abb64fba4a8b6aa1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file ez_stack-0.2.11-py3-none-manylinux_2_17_aarch64.whl.
File metadata
- Download URL: ez_stack-0.2.11-py3-none-manylinux_2_17_aarch64.whl
- Upload date:
- Size: 666.8 kB
- Tags: Python 3, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8da526fd883ed5504fce35af99e24734dccdd9ea62d5f95155f7314ebd1b43f7
|
|
| MD5 |
27fe0f7a7275eb36a61dd4a462dbc466
|
|
| BLAKE2b-256 |
36f1b5d685366545d538f54be1e3d26831618dd72dbf8967de396958e7f75ce3
|
Provenance
The following attestation bundles were made for ez_stack-0.2.11-py3-none-manylinux_2_17_aarch64.whl:
Publisher:
python-wheel.yml on rohoswagger/ez-stack
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ez_stack-0.2.11-py3-none-manylinux_2_17_aarch64.whl -
Subject digest:
8da526fd883ed5504fce35af99e24734dccdd9ea62d5f95155f7314ebd1b43f7 - Sigstore transparency entry: 1201748003
- Sigstore integration time:
-
Permalink:
rohoswagger/ez-stack@d0fba2011e69250bc6f33d66abb64fba4a8b6aa1 -
Branch / Tag:
refs/tags/v0.2.11 - Owner: https://github.com/rohoswagger
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-wheel.yml@d0fba2011e69250bc6f33d66abb64fba4a8b6aa1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file ez_stack-0.2.11-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: ez_stack-0.2.11-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 648.8 kB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6c1303cbd37c37159ddbb607a741685c319fc8612883fb3582a194042e274472
|
|
| MD5 |
7bdb55d264254aaa72a9b6404a11c675
|
|
| BLAKE2b-256 |
cf524399a5f36c0f3f1a524cb1e021be9f9283fb722bee13396c62f22451a787
|
Provenance
The following attestation bundles were made for ez_stack-0.2.11-py3-none-macosx_11_0_arm64.whl:
Publisher:
python-wheel.yml on rohoswagger/ez-stack
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ez_stack-0.2.11-py3-none-macosx_11_0_arm64.whl -
Subject digest:
6c1303cbd37c37159ddbb607a741685c319fc8612883fb3582a194042e274472 - Sigstore transparency entry: 1201747977
- Sigstore integration time:
-
Permalink:
rohoswagger/ez-stack@d0fba2011e69250bc6f33d66abb64fba4a8b6aa1 -
Branch / Tag:
refs/tags/v0.2.11 - Owner: https://github.com/rohoswagger
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-wheel.yml@d0fba2011e69250bc6f33d66abb64fba4a8b6aa1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file ez_stack-0.2.11-py3-none-macosx_10_12_x86_64.whl.
File metadata
- Download URL: ez_stack-0.2.11-py3-none-macosx_10_12_x86_64.whl
- Upload date:
- Size: 696.4 kB
- Tags: Python 3, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e84d7e2c86419473542de01178db4f7724ed5945687b23e45a028a81853b4041
|
|
| MD5 |
c44d317bc5ae58d0269bc77878ce607f
|
|
| BLAKE2b-256 |
1265ad8a996f3d4afcfda88aec614c5ff1ed734ea5ca13ea63729e3200b86dd6
|
Provenance
The following attestation bundles were made for ez_stack-0.2.11-py3-none-macosx_10_12_x86_64.whl:
Publisher:
python-wheel.yml on rohoswagger/ez-stack
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ez_stack-0.2.11-py3-none-macosx_10_12_x86_64.whl -
Subject digest:
e84d7e2c86419473542de01178db4f7724ed5945687b23e45a028a81853b4041 - Sigstore transparency entry: 1201748089
- Sigstore integration time:
-
Permalink:
rohoswagger/ez-stack@d0fba2011e69250bc6f33d66abb64fba4a8b6aa1 -
Branch / Tag:
refs/tags/v0.2.11 - Owner: https://github.com/rohoswagger
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-wheel.yml@d0fba2011e69250bc6f33d66abb64fba4a8b6aa1 -
Trigger Event:
push
-
Statement type: