Skip to content

Jujutsu (`jj`) + stacked PRs on Compass

Use this for every version-control operation in your clone. Your clone is a jj repo (a commit + bookmark model, no staging area). You drive it through the vcs_* tools: they commit, rebase, push, and open or update one PR per bookmark. Raw jj stays available for reads and for local history edits the tools do not cover.

You never push. vcs_submit sends a git bundle of your commits to the Compass Server, and the Server pushes them and writes the PRs. Your forge credential can still write today, but a raw git push or PR write skips every check the tools make, so never use one. The one exception is gh pr close on an older PR that vcs_retire cannot close (see “Moving an older PR onto the tools”). Hard limits: never push or force-push main, never merge — the human operator holds the merge gate.

  • Your clone is yours. Each agent works in its own container with its own colocated clone: its own .jj/ beside its own .git/. You coordinate with other agents at PR/merge time, not on a shared working copy.
  • The working copy is a commit. jj has no staging area. The working copy is a live commit, @; every edit you make is already in @. vcs_commit describes @, sets its bookmark, and starts a fresh @ on top.
  • jj is colocated over git. .jj/ and .git/ share one object store. git sits in detached-HEAD state (jj drives, git follows) — expected.
  • Change ID vs commit ID. Every commit has a stable change ID (survives rewrites) and a git commit ID (changes when the commit is rewritten). Address commits by change ID or by revset (@, @-, main@origin).
  • A bookmark is a branch, and one bookmark is one PR. A stack is a chain of bookmarked commits, each based on the one below. It must be one linear line: vcs_submit refuses a fork.
Tool Does
vcs_status Your stack from the tip down to the base: change, commit, bookmark, subject, conflict, empty.
vcs_commit Describes @ (or <workspace>@) from subject and sections, sets bookmark, then starts a new @ on it.
vcs_workspace add, forget, or update-stale one subagent working copy.
vcs_sync Fetches origin, then rebases your own commits onto trunk or the foreign base they sit on. Reports conflicts.
vcs_submit Sync, checks, the repo’s pre-push hook, then pushes through the Server and creates or updates one draft PR per bookmark.
vcs_pr_status PR, state, draft, checks, and authorship for each bookmark, bottom to top.
vcs_retire Closes your PR for one bookmark, deletes its branch through the Server, then deletes the local bookmark.

Issues, comments, reviews, and full PR reads stay on the forge_* tools (forge_list_issues, forge_create_issue, forge_get_pull_request, forge_submit_review, …).

These hold on every change, no exceptions.

  • Never push/force-push main; never merge. You open PRs and drive them to merge-ready; the operator merges.
  • Every PR needs an issue. vcs_submit takes issues, a map from bookmark to issue (#12, owner/repo#12, or a Linear key). It is required for every bookmark that has no PR yet. Find an issue that fits with forge_list_issues, or create one with forge_create_issue when none does. The Server checks that the issue exists and is open.
  • The first commit of a bookmark is the PR text. vcs_commit on a new bookmark needs sections, one per heading of the repo’s .github/PULL_REQUEST_TEMPLATE.md. That commit’s subject becomes the PR title, with the issue appended, and its sections become the PR body. Later commits on the bookmark take text instead. The PR text is written once; to change it, call vcs_submit with text and that bookmark’s issue in issues.
  • Review fixes are a NEW commit, never an amend. Land each fix as a new commit on the bookmark with vcs_commit and submit again. An amend loses the link between a review thread and its code and shows no interdiff.
  • Always non-interactive. When you run raw jj, pass -m to describe and commit, never use interactive flags (split -i, squash -i, a bare resolve), and pass --no-pager to read commands.
  • Do not edit a bookmark commit by hand. Editing files while @ sits on a bookmark commit silently amends it. vcs_commit always leaves a fresh @ on top; after any raw jj edit, run jj new <bookmark> before you edit.
  • Stack changes with overlapping edits. If two PRs edit the same lines of a file, put the later bookmark on top of the first PR’s line.
vcs_sync # fetch and rebase your commits onto trunk
# ... you or a subagent edit files ...
vcs_commit { bookmark, subject, sections } # first commit: sections per template heading
vcs_submit { issues: { <bookmark>: "#12" } } # push through the Server, open a draft PR
vcs_pr_status # PR, checks, and authorship per bookmark
vcs_submit { ready: true } # when review and CI are clean: tip leaves draft
  • Start a new line on trunk. vcs_sync rebases your own commits onto the latest trunk. For a fresh change, run jj --no-pager new 'main@origin' first.
  • A stack is several bookmarks in one line. Commit the first slice to bookmark a, then the next slice to bookmark b; vcs_submit with the top bookmark submits the whole line, bottom first, each PR based on the one below.
  • Your commits are pushed as you made them. Nothing is squashed. Each PR shows each of its commits.
  • The pre-push hook runs inside vcs_submit. It is the repo’s own hook runner, with the same arguments and stdin a real push gives it. A failing hook stops the submit. A hook still running after 120 s is stopped and the submit goes on; the result says “pre-push hook timed out after 120 s”. Then find out why the hook is slow and fix it in a change of its own. CI on the PR is the main gate.
  • Read before you submit. vcs_status lists your stack; it must show only your commits. vcs_submit refuses conflicts, merge commits, empty descriptions, two bookmarks on one commit, commits above the tip that no bookmark publishes, and a forked stack.
vcs_commit { bookmark: <bookmark>, subject: "fix(scope): address review", text: "…" }
vcs_submit { bookmark: <top bookmark> }

The new commit re-triggers CI and the review bots on the new head. Full loop — spawn the reviewer, fix the mechanical findings, surface the judgment calls, iterate to merge-ready: skill://review.

A jj workspace is another working copy of the same clone, with its own @. Give each parallel subagent its own workspace so they edit different slices without colliding.

vcs_workspace { action: "add", name: "ws-router", base: "main@origin" }
# ... the subagent edits files only, in ws-router ...
vcs_commit { workspace: "ws-router", bookmark: "router", subject: "…", sections: {…} }
vcs_workspace { action: "forget", name: "ws-router" } # then delete the directory
  • Subagents edit files only. You commit their work with vcs_commit { workspace, bookmark, subject, sections } (or text for a later commit), which runs in that workspace’s root.
  • Bookmarks are shared across workspaces. Give each subagent a distinct bookmark name.
  • A stale workspace (it fell behind a concurrent operation) is repaired with vcs_workspace { action: "update-stale", name: "ws-router" }.

If your line sits on a PR another agent of the same owner authored, the Server manages it: on submit it rebases and pushes that bookmark and tells the owning agent. On a conflict it pushes nothing and asks that agent to resolve. You never rebase, track, or push another agent’s bookmark yourself.

vcs_retire { bookmark } closes your PR and deletes its branch through the Server, then deletes the local bookmark. It only works on a PR you authored, and a retry after a partial run is safe. To replace a mixed or superseded PR, first open its replacement and check that every hunk of the old PR is in it (or dropped on purpose), then retire the old one with a comment on it that links the replacement (forge_comment_on_pull_request).

A PR opened before the vcs_* tools carries a block between <!-- start jj-vine stack --> and <!-- end jj-vine stack --> in its body. The Server has no record that you wrote it, so the tools cannot update or close it. Move each such line once, onto new commits under new bookmark names:

  1. Run vcs_sync. Copy the line with jj duplicate <bottom>::<tip>; the copies sit on the same base with the same content and messages.
  2. Run jj bookmark forget <old> for each old bookmark. This drops the local name and stops tracking <old>@origin, so the tools leave the old branch alone.
  3. Run jj bookmark create <new> -r <copy> for each copy. Use names the old line never used.
  4. Run jj diff --from <old tip>@origin --to <new tip> --stat; it must show no change. Then run vcs_submit with bookmark set to the new tip and each new bookmark’s issue in issues. For each new PR, run vcs_submit again with that bookmark’s issue in issues and text whose body links the PR it replaces; text covers one bookmark per call.
  5. Close each old PR with gh pr close <number> --comment <new PR URL>. The vcs_* tools cannot close a PR the Server has no record of.

Review history stays on the old PR. The move is done when none of your open PRs carries that block.

Task Tool or command
Show your stack vcs_status
Commit (first commit of a bookmark) vcs_commit { bookmark, subject, sections }
Commit (later commit) vcs_commit { bookmark, subject, text }
Rebase onto latest trunk vcs_sync
Push and open or update the PRs vcs_submit { issues }
Take the tip PR out of draft vcs_submit { ready: true }
Replace one PR’s title and body vcs_submit { issues, text: { bookmark, subject, sections } }
PR, checks, authorship vcs_pr_status
Close a PR and delete its branch vcs_retire { bookmark }
Add or remove a subagent workspace vcs_workspace
Start a change off latest main jj --no-pager new 'main@origin'
Review a change’s diff jj --no-pager diff --stat
Split an over-large commit by file jj split <file>...
Undo the last jj operation jj undo
Rewind the repo to an earlier state jj --no-pager op log then jj op restore <op-id>