@cvr/stacked
v0.10.0
Published
Branch-based stacked PR manager. Tracks parent-child branch relationships, propagates upstream changes via merges, and creates/updates GitHub PRs via `gh`.
Readme
stacked
Branch-based stacked PR manager. Tracks parent-child branch relationships, propagates upstream changes via merges, and creates/updates GitHub PRs via gh.
Install
bun run build # compiles binary to bin/stacked + symlinks to ~/.bun/bin/Setup
# Install the Claude skill (optional):
stacked init
# Trunk is auto-detected on first use from origin/HEAD when available
# (fallback: main > master > develop). Override only if needed:
stacked trunk <name>Usage
# Create a stack: branch from trunk
stacked create feat-auth
# Stack another branch on top
stacked create feat-auth-ui
# See the stack
stacked list
stacked list --json # machine-readable output
# See a specific stack by name
stacked list feat-auth
# Quick orientation
stacked status
stacked status --json
# List all stacks in the repo
stacked stacks
stacked stacks --json
# Navigate
stacked up # move up one branch
stacked down # move down one branch
stacked top # jump to top of stack
stacked bottom # jump to bottom of stack
stacked checkout feat-auth
stacked checkout any-branch # falls through to git for non-stacked branches
# Sync entire stack with latest trunk
stacked sync
# After editing mid-stack, merge parent into children only
stacked sync --from feat-auth
# Force-include merged branches (rare — they're skipped by default)
stacked sync --include-merged
# Sync, push, and create/update PRs in one step
stacked submit
stacked submit --no-sync # skip the sync step
stacked submit --draft
stacked submit --title "Add auth" --body "Implements OAuth2 flow"
stacked submit --title "Auth,Validation,Tests" # per-branch titles (comma-delimited)
stacked submit --only # process only the current branch
stacked submit --dry-run
# Adopt an existing branch into the stack
stacked adopt existing-branch --after feat-auth
# Move a branch and everything above it onto a new parent
stacked reparent feat-auth-ui --onto feat-api
stacked reparent feat-auth-ui --onto main --dry-run
stacked reparent feat-auth-ui --onto feat-api --sync
# View commits per branch
stacked log
stacked log --json
# Detect existing branch chains and register as stacks
stacked detect
stacked detect --dry-run
# Remove merged branches from stacks (prompts for confirmation)
stacked clean
stacked clean --dry-run
stacked clean --yes # skip confirmation
# Remove a branch from the stack (prompts for confirmation)
stacked delete feat-auth-ui
stacked delete feat-auth-ui --keep-remote # keep the remote branch
stacked delete feat-auth-ui --yes # skip confirmation
# Global flags (work with any command)
stacked --verbose list # debug output
stacked --quiet sync # suppress non-essential output
stacked --no-color list # disable colored output
stacked --yes clean # skip confirmation promptsCommands
| Command | Description |
| ------------------- | ------------------------------------------------------------------------------------------------- |
| trunk [name] | Get/set trunk branch (--json) |
| create <name> | Create branch on top of current (--from, --json) |
| list [stack] | Show stack branches (--json) |
| stacks | List all stacks (--json) |
| status | Show current branch, stack position, working tree state, per-branch push state (--json) |
| checkout <name> | Switch to branch (falls through to git for non-stacked branches) |
| up | Move up one branch in the stack |
| down | Move down one branch in the stack |
| top | Jump to top of stack |
| bottom | Jump to bottom of stack |
| sync | Fetch + merge parent into each child (--from, --include-merged, --continue, --abort) |
| detect | Detect branch chains and register as stacks (--dry-run, --json) |
| clean | Remove merged branches + remote branches (--dry-run, --json) |
| delete <name> | Remove branch from stack + git + remote (--keep-remote, --force, --json) |
| submit | Sync + push + create/update PRs (--no-sync, --title, --body, --only, --draft, --json) |
| adopt <branch> | Add existing branch to stack (--after, --json) |
| reparent <branch> | Move a branch subtree onto another branch or trunk (--onto, --sync, --dry-run, --json) |
| log | Show commits grouped by branch (--json) |
| init | Install the stacked Claude skill to ~/.claude/skills |
Global Flags
| Flag | Description |
| -------------- | ----------------------------- |
| --verbose | Enable debug output |
| --quiet/-q | Suppress non-essential output |
| --no-color | Disable colored output |
| --yes/-y | Skip confirmation prompts |
Merged Branches
Once a PR merges, its branch is detected on the next sync (via gh pr view) and added to the shared repository metadata. From that point on:
syncandsubmittreat the branch as invisible — no merge, no push, no PR mutation.- Children of a merged branch reparent to the next non-merged ancestor (or trunk).
statusshows them with a dim✓ (merged)so they don't look stuck.- The branch still renders in PR-body metadata tables (with ✅) for bookkeeping.
Use stacked sync --include-merged to force them back into the loop, or stacked clean to remove them entirely.
Data Model
Repository-wide stack topology lives under $XDG_STATE_HOME/stacked (normally ~/.local/state/stacked) and is keyed by a normalized origin identity. Clones and linked worktrees of the same remote therefore see the same stacks. Set STACKED_STATE_HOME to override the directory.
Clone-local synchronization markers are stored separately because clones can have different branch tips. Repositories without an origin fall back to their common Git directory and do not share metadata across clones. Existing .git/stacked.json files are migrated automatically after validation; conflicting legacy files stop with an error instead of silently choosing one.
Active stacks are stored as explicit linear parent links plus a per-stack root. mergedBranches is a skip-list of branches whose PRs have merged — they remain in metadata for bookkeeping but are skipped by sync/submit/detect.
reparent moves the selected Branch and every descendant above it as one subtree. Cross-Stack moves remove an emptied source Stack; targeting Trunk creates a new Stack. Because Stacks remain linear, inserting after a Branch places that Branch's former descendants above the moved subtree. Only checkout-local syncedOnto markers whose Parent changed are invalidated. Use --sync to validate the checkout first, then immediately merge and push the moved Branch's destination Lineage without rewriting history. --sync and --dry-run are mutually exclusive.
Trunk is auto-detected on first use from origin/HEAD when available, then falls back to local main, master, or develop. Override with stacked trunk <name>.
Output Conventions
- stdout: data output only (JSON, branch names, tree views) — safe for piping
- stderr: progress messages, spinners, success/warning/error messages
- All commands support
--jsonfor structured output
Development
bun run dev -- --help # run from source
bun run gate # typecheck + lint + fmt + test + build
bun test # tests only