Like the title says, could this support JJ workspaces? Had claude write up the following proposal:
Add Jujutsu (jj) backend for worktree operations
Summary
Add optional support for Jujutsu (jj) as a backend for aoe's worktree lifecycle operations. Today aoe is git-only: src/git/worktree.rs shells out to git worktree {add,remove,prune,list} and uses libgit2 for repo introspection. This proposal extracts a WorktreeBackend trait, keeps the existing git implementation unchanged, and adds a JjBackend for users who manage their repos with jj.
Scope for v1: colocated jj repos only (jj git init --colocate / jj git clone --colocate). Non-colocated jj is a follow-up.
Motivation
jj users currently have two options:
- Don't use aoe's auto-worktree features. Pre-create workspaces with
jj workspace add, point aoe at the directory, skip --worktree / --new-branch / --delete-worktree.
- Let aoe run
git worktree add inside a colocated jj repo. Works, but those worktrees aren't known to jj — jj workspace list won't see them, and cleanup is git-only.
Neither is great. Native support means the TUI's "create session" flow and aoe worktree {list,cleanup} work the same way for both crowds.
Detection
Backends are detected per-repo at the root:
pub enum VcsBackend { Jj, Git }
pub fn detect_backend(repo_path: &Path) -> VcsBackend {
if repo_path.join(".jj").is_dir() { return VcsBackend::Jj; }
if let Some(parent) = repo_path.parent() {
if parent.join(".jj").is_dir() { return VcsBackend::Jj; }
}
VcsBackend::Git
}
Rules:
.jj/ present → JjBackend. Colocated repos always also have .git/; we prefer jj when both exist.
.jj/ absent, .git/ present → GitBackend (today's behavior, byte-for-byte).
- Neither → existing
GitError::NotAGitRepo.
Override path: add worktree.backend = "auto" | "git" | "jj" to WorktreeConfig. auto runs the detection above. Per AGENTS.md, this also requires a FieldKey, SettingField, apply_field_to_global / apply_field_to_profile, clear_profile_override, and *ConfigOverride wiring. Standard playbook.
Optional CLI flag for one-offs: aoe add --vcs jj <branch>.
Dispatch
Extract the existing GitWorktree API into a trait. Today's call sites (~10, in src/cli/add.rs, src/cli/remove.rs, src/cli/worktree.rs, src/git/cleanup.rs, src/git/diff.rs, src/server/api/git.rs) become trait calls.
// src/vcs/mod.rs (renamed from src/git/)
pub trait WorktreeBackend {
fn add_worktree(&self, path: &Path, branch: &str, new: bool) -> Result<()>;
fn remove_worktree(&self, path: &Path, force: bool) -> Result<()>;
fn list_worktrees(&self) -> Result<Vec<WorktreeEntry>>;
fn prune(&self) -> Result<()>;
fn current_branch(&self, path: &Path) -> Result<String>;
fn fetch_branch(&self, remote: &str, branch: &str) -> Result<()>;
fn is_dirty(&self, path: &Path) -> Result<bool>;
// ...remaining GitWorktree surface
}
pub struct GitBackend { repo_path: PathBuf } // existing impl, renamed
pub struct JjBackend { repo_path: PathBuf } // new
pub fn open(repo_path: PathBuf) -> Result<Box<dyn WorktreeBackend>> {
match detect_backend(&repo_path) {
VcsBackend::Jj => Ok(Box::new(JjBackend::new(repo_path)?)),
VcsBackend::Git => Ok(Box::new(GitBackend::new(repo_path)?)),
}
}
Call site change is mechanical:
// before
let git_wt = GitWorktree::new(main_repo)?;
git_wt.add_worktree(...);
// after
let wt = vcs::open(main_repo)?;
wt.add_worktree(...);
Command mapping
| Operation |
Git |
Jj |
| add worktree |
git worktree add <path> <branch> |
jj workspace add <path> + jj new <bookmark> in it |
| add new branch |
git worktree add -b <new> <path> |
jj workspace add <path> + jj bookmark create <new> + jj new |
| remove worktree |
git worktree remove <path> |
jj workspace forget <name> (main repo) + remove dir |
| list |
git worktree list --porcelain |
jj workspace list |
| prune |
git worktree prune |
jj workspace forget for missing (or no-op; jj self-cleans) |
| current branch |
libgit2 repo.head() |
libgit2 (colocated) — no jj-specific code needed |
| fetch |
git fetch <remote> <branch> |
jj git fetch --branch <branch> |
| dirty check |
libgit2 status |
libgit2 (colocated) |
| diff |
libgit2 diff |
libgit2 (colocated) |
Key insight: for colocated repos, all read paths stay on libgit2. jj keeps .git/ exported. The JjBackend only needs to override the mutating ops (add/remove/fetch). This keeps the diff/status surface single-pathed.
Edge cases
aoe already handles bare-repo + linked-worktree layouts (find_main_repo_from_linked_worktree_gitfile, find_main_repo_from_worktree_gitdir, src/git/worktree.rs:84-122). The jj equivalents:
- jj workspace from a bare repo.
.jj/repo/store/git/ holds the bare. Detection walks up looking for .jj/ or the existing bare-repo parent pattern.
jj workspace add workspaces. Each has its own .jj/ pointing back to the main repo. jj workspace list from any workspace returns all of them.
- Mixed:
git worktree add inside a colocated jj repo. That worktree has a .git file link but no .jj/. Detection → GitBackend. Correct: git-created worktrees aren't known to jj. aoe treats it as a git worktree. Acceptable.
- Non-colocated jj. Out of scope for v1. Detection error message points users to
jj git init --colocate, or we run it for them on first use behind a config flag.
User-facing behavior
jj user, no config changes needed:
jj git clone --colocate https://github.com/foo/bar
cd bar
aoe add feature-x --new-branch
# detects .jj/, dispatches to JjBackend
# runs: jj workspace add ../bar-feature-x
# cd ../bar-feature-x && jj bookmark create feature-x && jj new feature-x
# session created, TUI shows it like any other worktree session
aoe rm feature-x --delete-worktree
# JjBackend: jj workspace forget bar-feature-x, then remove the directory
git user — zero behavior change:
git clone https://github.com/foo/bar
cd bar
aoe add feature-x --new-branch
# .jj/ absent → GitBackend → today's code path, byte-for-byte
Effort estimate
- Trait extraction: ~half a day. Mechanical, well-bounded.
JjBackend (colocated only): 1–2 days. Mirrors GitBackend's shell-out structure; jj's CLI is machine-readable.
- Tests: 1–2 days. Add jj-fixture variants alongside the existing
src/git/worktree.rs tests.
- Config wiring (
WorktreeConfig field, settings TUI, profile overrides): few hours.
- Docs (
docs/guides/workflow.md, CLI reference, new "Using aoe with Jujutsu" section): few hours.
Total: ~1 week of focused work for a clean v1.
Why this is low-risk
- Existing git users see nothing new. The trait extraction is a refactor with no behavior change.
- The shell-out boundary is already clean — ~18
Command::new("git") calls all in one file.
- Read paths stay on libgit2 for colocated repos, so diff/status/branch-name code is untouched.
- Detection prefers jj only when
.jj/ is actually present, so plain git repos never hit jj code paths.
Out of scope (follow-ups)
- Non-colocated jj support.
- Sapling, fossil, or other VCS backends (the trait makes them possible, but no user demand cited).
- Auto-running
jj git init --colocate on a plain git repo to migrate it. Should be explicit user action.
Happy to take this on if there's interest. Wanted to sanity-check the design before opening a PR.
Like the title says, could this support JJ workspaces? Had claude write up the following proposal:
Add Jujutsu (jj) backend for worktree operations
Summary
Add optional support for Jujutsu (jj) as a backend for aoe's worktree lifecycle operations. Today aoe is git-only:
src/git/worktree.rsshells out togit worktree {add,remove,prune,list}and uses libgit2 for repo introspection. This proposal extracts aWorktreeBackendtrait, keeps the existing git implementation unchanged, and adds aJjBackendfor users who manage their repos with jj.Scope for v1: colocated jj repos only (
jj git init --colocate/jj git clone --colocate). Non-colocated jj is a follow-up.Motivation
jj users currently have two options:
jj workspace add, point aoe at the directory, skip--worktree/--new-branch/--delete-worktree.git worktree addinside a colocated jj repo. Works, but those worktrees aren't known to jj —jj workspace listwon't see them, and cleanup is git-only.Neither is great. Native support means the TUI's "create session" flow and
aoe worktree {list,cleanup}work the same way for both crowds.Detection
Backends are detected per-repo at the root:
Rules:
.jj/present →JjBackend. Colocated repos always also have.git/; we prefer jj when both exist..jj/absent,.git/present →GitBackend(today's behavior, byte-for-byte).GitError::NotAGitRepo.Override path: add
worktree.backend = "auto" | "git" | "jj"toWorktreeConfig.autoruns the detection above. PerAGENTS.md, this also requires aFieldKey,SettingField,apply_field_to_global/apply_field_to_profile,clear_profile_override, and*ConfigOverridewiring. Standard playbook.Optional CLI flag for one-offs:
aoe add --vcs jj <branch>.Dispatch
Extract the existing
GitWorktreeAPI into a trait. Today's call sites (~10, insrc/cli/add.rs,src/cli/remove.rs,src/cli/worktree.rs,src/git/cleanup.rs,src/git/diff.rs,src/server/api/git.rs) become trait calls.Call site change is mechanical:
Command mapping
git worktree add <path> <branch>jj workspace add <path>+jj new <bookmark>in itgit worktree add -b <new> <path>jj workspace add <path>+jj bookmark create <new>+jj newgit worktree remove <path>jj workspace forget <name>(main repo) + remove dirgit worktree list --porcelainjj workspace listgit worktree prunejj workspace forgetfor missing (or no-op; jj self-cleans)repo.head()git fetch <remote> <branch>jj git fetch --branch <branch>Key insight: for colocated repos, all read paths stay on libgit2. jj keeps
.git/exported. TheJjBackendonly needs to override the mutating ops (add/remove/fetch). This keeps the diff/status surface single-pathed.Edge cases
aoe already handles bare-repo + linked-worktree layouts (
find_main_repo_from_linked_worktree_gitfile,find_main_repo_from_worktree_gitdir,src/git/worktree.rs:84-122). The jj equivalents:.jj/repo/store/git/holds the bare. Detection walks up looking for.jj/or the existing bare-repo parent pattern.jj workspace addworkspaces. Each has its own.jj/pointing back to the main repo.jj workspace listfrom any workspace returns all of them.git worktree addinside a colocated jj repo. That worktree has a.gitfile link but no.jj/. Detection →GitBackend. Correct: git-created worktrees aren't known to jj. aoe treats it as a git worktree. Acceptable.jj git init --colocate, or we run it for them on first use behind a config flag.User-facing behavior
jj user, no config changes needed:
git user — zero behavior change:
Effort estimate
JjBackend(colocated only): 1–2 days. MirrorsGitBackend's shell-out structure; jj's CLI is machine-readable.src/git/worktree.rstests.WorktreeConfigfield, settings TUI, profile overrides): few hours.docs/guides/workflow.md, CLI reference, new "Using aoe with Jujutsu" section): few hours.Total: ~1 week of focused work for a clean v1.
Why this is low-risk
Command::new("git")calls all in one file..jj/is actually present, so plain git repos never hit jj code paths.Out of scope (follow-ups)
jj git init --colocateon a plain git repo to migrate it. Should be explicit user action.Happy to take this on if there's interest. Wanted to sanity-check the design before opening a PR.