stakk 2.1.3

A CLI tool that bridges Jujutsu (jj) bookmarks to GitHub stacked pull requests
stakk-2.1.3 is not a library.

stakk

stakk bridges Jujutsu bookmarks to GitHub stacked pull requests. It reads your change graph, lets you select a stack and name bookmarks for the commits that need them, then pushes and maintains one PR per bookmark: correct base branches, stack comments, no duplicates on re-runs.

It is not a jj wrapper. jj stays in charge of your commits and bookmarks; stakk takes over only where that local state has to exist on GitHub.

Interactive stakk submission flow

Features

  • Automatic stack detection — finds bookmark chains and their topological order in the jj change graph, including unbookmarked heads, which stakk can bookmark for you.
  • Interactive TUI — a graph of every branch stack, then a screen for assigning bookmarks. Each commit cycles through: [x] existing → [~] auto → [>] typed by hand → [+] generated stakk-xxxx[*] custom command → [ ] skip.
  • Auto bookmark naming — the [~]auto state derives names from commit descriptions and file paths via TF-IDF scoring; r cycles alternatives and --auto-prefix brands them (e.g. gb-caching-database).
  • Non-interactive selection--keep/--new/--new-auto/--new-command build the exact same submission the TUI would, without a terminal.
  • Stack-awareness comments — every PR gets the full stack with links, updated in place on re-runs and rendered with customizable minijinja templates. See Stack info placement.
  • Dry-run mode--dry-run prints the submission plan and stops: no bookmark is created, nothing is pushed, and nothing is written to GitHub.
  • No direct git usage — all VCS operations go through jj commands, so workspaces and non-colocated repos work automatically.

Installation

Requirements

stakk shells out to the jj CLI, which must be installed and on your PATH. The minimum supported jj version is 0.39.0. Older versions may work but are untested; stakk prints a warning when it detects one. Raising that floor is not a breaking change — see Stability.

mise (recommended)

mise use -g 'github:glennib/stakk'

Other methods

mise use -g 'cargo:stakk' # from crates.io
cargo binstall stakk # using cargo-binstall
cargo install stakk # install from source

Or pre-built binaries from the latest release.

Quick start

# Submit interactively — pick a stack and assign bookmarks in the TUI
stakk

# See your stacks, and the change ids to name on the command line
# (offline: jj only, never GitHub)
stakk graph

# Submit without the TUI — one mark per PR boundary: keep an existing
# bookmark, name a new one at change qzvs, auto-name one at wmtk
stakk submit --keep feat-auth --new qzvs=feat-api --new-auto wmtk

# Add --dry-run to any submission to see the plan and stop
stakk submit --keep feat-auth --new qzvs=feat-api --new-auto wmtk --dry-run

How stacking works

In jj, bookmarks point at changes. When bookmarks form a linear chain — each building on the previous — they represent a stack. Create the bookmarks yourself, or let stakk discover unbookmarked heads and create them for you:

 ○  feat-ui    ← leaf
 ○  feat-api
 ○  feat-auth
 ◆  main       ← trunk

Picking feat-ui as the tip and keeping all three bookmarks — in the TUI, or as stakk submit --keep feat-auth --keep feat-api --keep feat-ui — pushes every one of them and creates or updates one PR per bookmark, basing each on the bookmark below it:

  • feat-auth → PR targeting main
  • feat-api → PR targeting feat-auth
  • feat-ui → PR targeting feat-api

Each PR shows only its own diff, and a stack comment on every PR links all related PRs together. The comment orders the stack the way stakk graph and the TUI do, leaf at the top and trunk at the bottom, and marks the PR you are looking at. Each row is a bare PR link, which GitHub renders as a reference carrying that PR's live title and merge state:

Stack of 3 PRs merging into main

• Add the search UI #14
• Add the search API #13 👈 this PR
• Add user authentication #12
• main

Stack info placement

--stack-placement decides where the stack overview lives on each PR: a separate PR comment (default), a fenced section in the PR body, none (write nothing and remove what is already there), or ignore (write nothing and touch nothing). Switching between comment and body migrates automatically, and a submission that produces a single PR is not a stack, so no stack info is written.

Full mode table, migration details, and stack comment templating: docs/template.md, or run stakk docs template.

Configuration

Settings come from CLI flags, then STAKK_-prefixed environment variables, then a repository stakk.toml (found by walking up to the jj workspace root), then the user config (~/.config/stakk/config.toml on Linux), then built-in defaults. All fields are optional, and unknown fields cause a parse error.

# stakk.toml
remote = "origin"
pr_mode = "draft"
stack_placement = "body"
auto_prefix = "gb-"

Every config key and environment variable, the inherit field, worked examples, and what a repo-supplied stakk.toml is able to do: docs/config.md, or run stakk docs config.

GitHub Enterprise Server

github.com works out of the box. For a GitHub Enterprise Server host, name the host with --github-host, STAKK_GITHUB_HOST, github_host in stakk.toml, or GH_HOST — stakk then accepts remotes on that host and uses its API at https://<host>/api/v3. Tokens are resolved per host, mirroring the GitHub CLI, so an Enterprise token is never sent to github.com.

Host resolution order and the API base: docs/config.md, or run stakk docs config. The gh commands that set the host up, the per-host token order, what to do when it fails, and how to check the setup: docs/auth.md, or run stakk docs auth.

Usage

stakk --help and stakk <subcommand> --help are the flag reference: every flag, its default, and its environment variable.

submit, graph and docs each answer to their initial letter, so stakk s, stakk g and stakk d work too. completions has no short form.

stakk, stakk submit

Submit a stack of bookmarks as stacked PRs. The two spellings are one command: with no selection flags, both launch the interactive flow — a TUI graph of all branch stacks where you pick a leaf, then a screen for toggling bookmarks onto the commits that need them. Repos with no pre-existing bookmarks work too; unmarked commits get stakk-<change_id>. The selection flags below replace the TUI with an explicit, scriptable selection.

Non-interactive selection

--keep, --new, --new-auto and --new-command replace the TUI with a fully explicit, scriptable selection: every PR boundary is named on the command line, all marks must lie on one trunk-to-tip path, the topmost mark is the tip, unmarked commits below it fold into the PR above them, and anything above it — an unbookmarked work-in-progress head, typically — is not submitted at all. rev is a change id or commit id prefix as printed by stakk graph, which makes submission a two-command loop:

stakk graph --format=json
stakk submit --keep base --new qzvs=my-feature --new-auto wmtk

Every selection rule and the machine-readable stakk::selection::* diagnostic codes: docs/agents.md, or run stakk docs agents. Pointing a coding agent at stakk docs agents is the fastest way to bring it up to speed. For a program rather than an agent, docs/scripting.md (stakk docs scripting) adds exit codes and a worked Python example.

PR titles and bodies

PR titles come from the first line of the jj change description and bodies from everything after it; segments with multiple commits join their descriptions with --- separators. Both are written only on PR creation, so manually edited PR descriptions are never overwritten — --sync-pr-content (title, body, all) opts into updating existing PRs, and only changed fields are sent.

Descriptions are reflowed on the way into the PR body: hard-wrapped prose lines are joined into soft-wrapped paragraphs, so a commit message wrapped at 72 columns does not render as ragged lines on GitHub. Structural Markdown — headers, lists, tables, block quotes, fenced and indented code, thematic breaks — is passed through verbatim. --trailers strip removes the trailing key/value block (Signed-off-by, Co-authored-by, Refs, …) from the generated body; the default keep passes it through.

Custom bookmark names

--bookmark-command names bookmarks with an external program. The command is run through sh -c (Unix) or cmd /C (Windows), receives a JSON description of one segment of commits on stdin, and must print a single bookmark name on stdout. It powers the [*] state in the TUI and the --new-command selection flag. The full JSON schema, with a worked example, is in stakk submit --help; the surrounding context is in docs/template.md, or run stakk docs template.

Immutable commits

Commits that jj considers immutable cannot get a new bookmark: the default bookmarks revset excludes immutable(), so stakk would create a PR it could never see again on the next run. The TUI locks such rows to [ ] and explains why, --new <rev> on one fails with stakk::selection::rev_immutable, and the commits are annotated in stakk graph. Move the work onto mutable commits — or, if you really need a PR there, create the bookmark yourself and drop ~ immutable() from --bookmarks-revset. Details, including the non-interactive side: docs/agents.md, or run stakk docs agents.

stakk graph

Display repository status and all bookmark stacks without submitting. Fully offline: only jj is queried, never GitHub — PR state is stakk submit --dry-run's job. stakk show is an alias for the same command, deprecated and due for removal in a future major release. graph and submit build the graph the same way, so --bookmarks-revset/--heads-revset apply to both: what graph prints is what submit would work on.

The default pretty format renders a jj-log-style graph of all stacks, always fully expanded. Every commit row carries its short change id, bookmarks, and description summary; immutable commits and bookmarks excluded by the bookmarks revset are annotated:

Default branch: main
Remote: origin git@github.com:you/repo.git (you/repo)

 ○  wmtk  feat-a  (no description set)
 ○  wmtl  "feat a work"
 │ ○  rlkv  feat-b  "feat b work"  (immutable — bookmark old-mark excluded by --bookmarks-revset)
 ├─╯
 ○  qzvs  base  "extend base"
 ○  qzvt  "add base"
 ◆  trunk

Bookmarks whose history contains a merge commit cannot be stacked and are left out of the graph; when that happens, pretty names them in a footer, and the JSON reports them in excluded_bookmarks.

--format=json emits a schema-versioned document for machine consumption (scripts, agents); its change id prefixes and bookmark names can be passed directly to stakk submit. It is a sparse projection — identifiers, commit titles, bookmarks and stack position — and --format=json-full is a strict superset that adds each commit's commit_id, full description, author and files[]. Bookmarks carry their push state (unpushed, diverged, synced), derived from jj alone, so a consumer can tell new work from an update without a network round trip. Field-by-field schema: docs/graph.md, or run stakk docs graph.

stakk docs [topic]

Print the documentation bundled into the binary. Because it ships inside the binary, it always describes the version you are actually running. Run stakk docs with no arguments for the list of topics. Each topic is one of the Markdown files in docs/, which is also where to read them on GitHub.

At a terminal the prose is re-flowed to your terminal width. Redirected, the source is emitted verbatim — so stakk docs agents >> AGENTS.md writes exactly the Markdown in docs/agents.md, below the HTML comment that names the topic, which makes it a one-line way to give a coding agent the full non-interactive workflow.

stakk completions <shell>

Generate shell completions. Supported shells: bash, zsh, fish, elvish, powershell.

# Zsh — add to your fpath
stakk completions zsh > ~/.zfunc/_stakk

# Bash
stakk completions bash > ~/.local/share/bash-completion/completions/stakk

# Fish
stakk completions fish > ~/.config/fish/completions/stakk.fish

Design

stakk never calls git directly. All git operations go through jj subcommands (jj git push, jj git remote list, etc.). This means stakk works automatically in jj workspaces and non-colocated repositories — two cases where calling git directly fails.

All forge interaction goes through a Forge trait. GitHub is the first (and currently only) implementation, but the core submission logic is forge-agnostic. This opens the door to Forgejo, GitLab, or other platforms in the future.

Submission runs as analyze → plan → execute, with every repository and GitHub mutation confined to the last phase. That is why --dry-run can stop after the plan and be guaranteed to have written nothing.

Stability

stakk follows semantic versioning. Stable surface: subcommands, their aliases and their flags, STAKK_ environment variables, config keys and their defaults, the two schema_version-ed JSON documents (stakk graph and --bookmark-command's stdin), diagnostic codes, and exit codes. Free to change in any release: rendered --help and stakk docs text, error and progress wording, the pretty output of stakk graph, the order of stacks[] in its JSON, and the TUI layout and keybindings. Raising the minimum supported jj version is not a breaking change.

The contract itself — every entry, with the reasoning: docs/stability.md, or run stakk docs stability.

License

MIT OR Apache-2.0