This is a living document. Update it whenever the product mission evolves, a significant design decision is made, a pattern is established, or a new version ships. It is the source of truth for anyone (human or AI) working on this codebase.
ez-stack makes stacked PRs on GitHub effortless, primarily for AI coding agents.
Agents are the primary audience. Humans benefit too, but when there's a design tradeoff, optimize for agents. Every command should be simpler, more intuitive, and more efficient than the raw git/gh equivalent — the goal is to make version control easier for agents, not to expose git's complexity through a different interface.
Every feature should be inherently more useful than the git commands it replaces.
-
Shell out, don't reimplement.
ezorchestratesgitandgh. It doesn't reimplement git logic. When in doubt, run the real command. -
Auto-restack on every mutation. Any command that moves a branch tip (
commit,amend,move) automatically restacks children. Users should never have to think about rebasing. -
State is append-only metadata. Stack state lives in
.git/ez/stack.json— just branch names, parents, parent SHAs, and PR numbers. It is never the source of truth for git state; git is. -
Fail loudly with hints. Every error message tells the user what went wrong AND what to do next. No silent failures.
-
Human output on stderr, machine output on stdout. Colored tree output and spinners go to stderr. JSON output (
--json), URLs (ez pr-link), and anything meant to be piped goes to stdout. -
Structured exit codes. Agents branch on failure type. Exit codes are documented and stable:
- 0 = success
- 1 = unexpected error
- 2 = GitHub API /
ghCLI error - 3 = rebase conflict (manual resolution required)
- 4 = stale remote ref
- 5 = usage error (on trunk, branch not tracked, etc.)
- 6 = unstaged changes
-
Progressive help discovery. The CLI is the agent's documentation. Three levels:
- Level 0:
ez(no args) → full command list with one-line descriptions (exit 0) - Level 1:
ez worktree(no subcommand) → subcommand list (exit 0) - Level 2:
ez create --help→ full parameter details Discovery commands always exit 0. Agents drill down on-demand instead of loading all docs upfront.
- Level 0:
-
Errors are navigation. Every error message contains both "what went wrong" AND "what to do instead." Agents can't Google — the error itself must point to the fix. One-step correction, not blind guessing.
-
Consistent output metadata. Every command appends
[ok | 45ms]or[exit:3 | 120ms]to stderr. Agents learn command cost over time and can branch on exit status without parsing. This is always on — no--agentflag. -
Show work by default.
ez commitprints the diff stat after committing. Agents need to verify what happened without running a separate command. Default to more information, not less.
src/
main.rs — CLI entry point, dispatch, structured exit codes
cli.rs — clap 4 derive definitions (all commands + flags)
error.rs — EzError enum (thiserror)
stack.rs — StackState: load/save .git/ez/stack.json, all stack queries
git.rs — git shell helpers (run_git, run_git_with_status)
github.rs — gh shell helpers (run_gh, PrInfo)
ui.rs — terminal output helpers (all to stderr)
stack_body.rs — pure module: build PR bodies with ancestor stack section
cmd/
init.rs — ez init
create.rs — ez create
commit.rs — ez commit
amend.rs — ez amend
push.rs — ez push (+ shared push_or_update_pr)
submit.rs — ez submit
sync.rs — ez sync
restack.rs — ez restack
navigate.rs — ez up/down/top/bottom
checkout.rs — ez checkout
list.rs — ez list (replaces ez branch)
log.rs — ez log
status.rs — ez status
delete.rs — ez delete
move_branch.rs — ez move
merge.rs — ez merge
pr_edit.rs — ez pr-edit
draft.rs — ez draft / ez ready
pr_link.rs — ez pr-link
Key invariant: cmd/ files contain business logic. git.rs and github.rs are pure I/O wrappers. stack.rs is pure data — no I/O except load/save.
These features exist specifically to make ez useable by AI agents:
| Feature | Flag/Command | Why |
|---|---|---|
| Machine-readable output | ez status --json, ez log --json |
Parse stack state without scraping colored text |
| Non-interactive checkout | ez checkout <name> or ez checkout <pr-number> |
No TTY required |
| Autostash on sync | ez sync --autostash |
Don't fail when there are dirty files |
| Conditional commit | ez commit --if-changed |
No-op exit 0 when nothing staged |
| Create from base | ez create --from <branch> |
Create branch without checking it out |
| PR URL to stdout | ez pr-link |
Pipeable: open $(ez pr-link) |
| Structured exit codes | (all commands) | Agent can branch on failure type |
- Crate:
ez-stackon crates.io - Binary:
ez - Version: set in
Cargo.toml, tagged asv<semver> - Release process: push tag
v*→ GitHub Actions builds binaries +cargo publish - Publish requires
CARGO_REGISTRY_TOKENrepository secret - NEVER reuse a version tag. crates.io permanently rejects republished versions. If you need to add changes after a tag was pushed, bump to the next version. If a tag was accidentally deleted, restore it on the original commit — don't retag a different commit with the same version.
| Version | Key changes |
|---|---|
| 0.1.1 | Initial release |
| 0.1.2 | Bug fixes |
| 0.1.3 | --title/--body on push/submit, ez pr-edit, ez sync --dry-run, autofetch before push, StaleRemoteRef error |
| 0.1.4 | Stack links in PR bodies, ez push --stack, filter_map in stack_body |
| 0.1.5 | --autostash, --json, non-interactive checkout, --from, --if-changed, ez draft/ez ready, ez pr-link, structured exit codes, SKILL.md |
| 0.1.6 | Worktree support: meta_dir() uses --git-common-dir, ez sync worktree-safe, restack/commit/amend skip branches in other worktrees, ez log shows [wt: <dirname>] |
| 0.1.7 | ez sync prunes merged worktrees; fix trunk fast-forward when current branch is trunk |
| 0.1.8 | Fix ez push clobbering manual gh pr edit --base changes; only update PR base if stack parent is a git ancestor of the branch |
| 0.1.9 | Fix ez sync non-fast-forward trunk warning (skip update when local trunk is equal/ahead/diverged); auto-clean stack entries for branches deleted outside ez |
| 0.1.10 | ez worktree create/delete/list; ez sync --force to force-remove worktrees with uncommitted changes |
| 0.1.11 | Agent CLI UX: ez diff/ez diff --stat/ez diff --name-only, ez parent, ez commit -m "a" -m "b" multi-line, ez commit -- <paths> path-scoped staging, diff stat after commit/amend, timing metadata on all output ([ok | 45ms]), progressive help discovery (bare commands exit 0), actionable error hints on all errors, worktree filter fix (only .worktrees/) |
| 0.1.12 | ez update — self-update command, auto-detects install method (cargo vs script), --check for version check without install, --version for specific version |
| 0.1.13 | Examples in every --help, commit SHA in output, ez worktree delete --yes from inside worktree, worktree_path fix for linked worktrees |
| 0.1.14 | Phased worktree create/delete: validate→mutate→output with rollback on failure; chdir before worktree remove; prune stale entries; recover from broken worktree state |
| 0.1.15 | ez branch — list all branches with PR numbers and worktree paths |
| 0.1.16 | (skipped) |
| 0.1.17 | ez shell-init — shell integration for auto-cd on worktree create/delete (zoxide pattern); remove redundant sync summary |
| 0.1.18 | ez setup — one-command shell configuration (PATH + shell-init); first-run hint prompts users to run it; ez setup --yes for agents |
| 0.1.19 | ez checkout auto-cd's into worktree if branch is checked out there; shell wrapper intercepts checkout for cd |
| 0.1.20 | Auto-drop redundant commits during sync/restack via git cherry + git rebase; remove redundant restack summary |
| 0.1.21 | Fix ez sync not switching off a branch before deleting it (stayed on cleaned-up branch) |
| 0.1.22 | Mutation receipts: every mutating command (commit, amend, sync, restack, push, create) emits structured JSON receipt to stderr; git::diff_stat_numbers() helper; gh abstraction + scope-aware stacking documented as deferred |
| 0.1.23 | Fix ez sync not cleaning up merged branches in worktrees; add git-level merge detection (branch tip is ancestor of trunk) for branches without PR numbers |
| 0.1.24 | Agent audit fixes: receipts on delete/move/merge/submit/worktree; fix delete.rs state corruption (git delete before state removal); worktree guard on delete; exit code 5 for all usage errors; branch/worktree list to stdout; amend hint fix |
| 0.1.25 | ez skill install — bundles SKILL.md into the binary, installs canonically to .agents/skills/ez-workflow/SKILL.md, and symlinks agent-specific skill roots in the current repo. |
| 0.1.26 | Fix ez branch to show trunk, all managed branches, and current branch even if untracked; hint when current branch was created outside ez |
| 0.1.27 | Fix -a flag to use git add -u (tracked only, not untracked); hook failure detection (shows which files pre-commit hooks modified); push error messages wrapped with context; ez status shows working tree (staged/modified/untracked counts); SKILL.md discoverability improvements |
| 0.2.0 | Flagship command redesign: ez create defaults to worktree (--no-worktree for old behavior); ez list replaces ez branch (adds --json, working tree state, worktree paths); ez delete auto-detects and removes worktrees; ez push -am "msg" for stage+commit+push; ez worktree create/delete/list become aliases |
| 0.2.1 | Declarative hooks: .ez/hooks/<event>/<name>.md are markdown instructions printed to agents (not executable scripts). --hook <name> selects a specific hook. Events: post-create, pre-push, post-sync, post-delete. Multiple named hooks per event. |
| 0.2.2 | Version bump (v0.2.1 already published) |
| 0.2.3 | Progressive hook discovery: --hook with no value lists available hooks. Agent flow: --help → sees --hook, tries --hook → gets list, picks one → gets instructions. |
| 0.2.4 | Fix squash-merge detection in ez sync: adds diff-level check (empty diff against trunk) so squash-merged branches are cleaned up even when is_ancestor fails |
| 0.2.14 | Scope Guard: ez create --scope ..., `ez scope show |
| 0.2.15 | Make focused commits the intended workflow: CLI/help/docs/skill now default to ez commit -m "..." -- <paths>, keep ez commit -am "..." for bulk updates, and reserve git add -p for partial hunks |
| 0.2.16 | Sync/list hardening: list all local branches with metadata overlay, paginate PR lookups, clean merged/closed branches safely (including current worktree cleanup via shell auto-cd), improve conflict receipts, and stop dev-port listeners on delete |
| 0.2.17 | Stream git fetch progress during ez sync and surface ez worktree delete --yes as the non-interactive path when deleting the current worktree |
| 0.2.18 | Add -A/--all-files for commit and push, preserve stale-base metadata until real restacks happen across sync/delete/merge, refresh trunk during ez restack, and warn after switching to a branch that is not restacked on latest main |
| 0.2.19 | Canonicalize ez skill install under .agents/skills with safe link-or-copy compatibility targets, improve worktree-awareness messaging and status output, and teach agents when to use -A/-Am for untracked files |
| 0.2.20 | Add non-interactive ez merge --yes, support ez merge --stack for linear stacks, and restore remote branch cleanup after REST-based merges |
| 0.2.24 | ez sync/ez list PR-status lookup uses one GraphQL request with aliased fields per branch instead of paginating every PR in the repo (122s → 1.8s on a 10k-PR repo); add local git-remote URL parser to skip the gh repo view round-trip when deriving owner/repo; ez adopt still uses the global PR scan because it needs the full PR graph. Adds ez track [branch] [--parent <name>] to bring a raw-git branch under ez management without rebasing — parent defaults to the closest tracked ancestor by merge-base, else trunk; updates BranchNotInStack error to hint at ez track. |
| 0.2.25 | ez adopt scopes to local branches by default — one GraphQL call for local-branch PRs instead of paginating every PR in the repo (122s → 1.4s adopting 12 PRs in a 10k-PR repo). --branches <names> and --pr <N> walk the ancestor chain on demand, fetching only the parents that show up in the chain (bounded by stack depth, not repo size). Adds github::get_pr_by_number for single-PR GraphQL lookup. Local PRs whose base isn't local-with-PR or trunk are warned about and skipped — users get a Run \ez adopt --pr `` hint to walk the full chain via remote PR ancestors when needed. |
| 0.2.27 | Restack branches in their worktree instead of skipping: ez sync, ez restack, and all auto-restack paths run git -C <worktree> when a branch is checked out elsewhere (including external worktrees like Superconductor). |
| 0.2.28 | ez create -m commits on the new branch without advancing the parent (stashes uncommitted state and transfers it into the new worktree; rejects --no-worktree). Fix stale-parent_head auto-restack bug: ez commit/ez amend/ez move now cascade the restack across the full descendant subtree (StackState::descendants_topo + shared restack::cascade_restack) instead of only direct children, which left grandchildren detached from the stack until a manual ez restack. |
Every commit must pass:
cargo fmt --all -- --check(formatter, enforced by CI + pre-commit hook)cargo clippy -- -D warnings(zero warnings)cargo test(all tests pass)
Pre-commit hook at .git/hooks/pre-commit runs cargo fmt --all -- --check automatically.
- Unit tests live in
#[cfg(test)] mod tests { ... }inside the source file they test - Pure logic functions (JSON serialization, error mapping, exit codes, stack queries) get direct unit tests
- Commands that shell out to
git/ghare not unit tested at the command level — the wrappers (git.rs,github.rs) are also hard to unit test without a real git repo - Test the data transformations, not the I/O
stack_body.rsis the model for pure, fully testable modules
These have been discussed and intentionally deferred:
ez rename— GitHub ties PRs to branch names; renaming requires deleting the old remote branch which may close the PR. Complex to do correctly.ez split— Multi-commit splitting is high-complexity, high-risk.ez absorb— Requires semantic commit analysis.ez land—ez merge+ez syncalready covers this flow.- CI status in
ez log— Requiresgh run listper branch (slow, extra API calls). Separate feature. - Interactive reorder — Complex TUI.
ez co <pr-number>— Superseded byez checkout <pr-number>(v0.1.5).- Remove
ghdependency — Allghusage ingithub.rscould be replaced with direct GitHub API calls viareqwest+ token. Benefits: smaller binary, works in environments withoutgh, enables GitLab/Bitbucket support. Costs: ~500 lines of HTTP client code, auth management. Keepghfor now (auth handling is worth it); abstract when multi-platform support is needed. - Scope routing / auto-splitting — Now that branch-level Scope Guard exists, the still-deferred version is anything that auto-moves files between branches, suggests sibling branches, or semantically splits work. Keep the shipped feature as pre-mutation intent checking, not ownership automation.
- Python wheel distribution —
pip install ez-stackfor the Python-heavy AI agent ecosystem. Requires building a wheel that bundles the Rust binary.
- Use
ez(not raw git) for all branching and PR operations — see SKILL.md - Rust edition 2024 —
let-elseandlet-chainsare available - All human-facing output to stderr; machine output to stdout
cargo fmt --allbefore every commit, no exceptions