Skip to content

ZelAnton/agent-workspace

 
 

Repository files navigation

agent-workspace

npm version License: MIT Backends: git + jj

A fast Git/Jujutsu worktree workflow tool for AI coding agents (CLI: ws). Spin up isolated worktrees in milliseconds, run an agent in each, then merge and clean up — all from one short command. Distributed as a single static binary.

Cover

Why

AI coding agents work best with isolated environments — one agent per worktree, no stepping on each other's files, no half-finished branches polluting your main checkout:

  • Parallel execution — run multiple agents simultaneously without interference; each gets its own working directory and branch.
  • Clean separation — a feature's work-in-progress never touches your main checkout. Throw the worktree away and your repo is pristine.
  • Snap mode — a "use and discard" loop: create a worktree, run an agent in it, and on exit ws walks you through merge-or-keep and tidies up.
  • Near-instant creation — Copy-on-Write (reflink) cloning makes ws new near-instant even on multi-gigabyte monorepos, using almost no extra disk.
  • Native git and Jujutsu (jj) — both backends are first-class; colocated repos default to jj.
  • Terminal-tab aware — on Windows Terminal / iTerm2 / GNOME Terminal / WezTerm (and tmux), ws new and ws cd open the worktree in a fresh tab/window and leave your current shell put.

At a glance

ws new fix-auth -s claude   # new worktree + branch, run Claude in it, merge on exit
ws ls                       # see every worktree and its base branch
ws cd fix-auth              # jump into one (new tab if your terminal supports it)
ws merge -d                 # squash-merge back to the base branch and delete the worktree
ws clean                    # garbage-collect worktrees already merged

Requirements

  • git ≥ 2.x (always required, including for colocated repos). Optionally jj for the Jujutsu backend.
  • A POSIX shell (bash / zsh / fish) or PowerShell — needed for the cd-changing commands (ws setup installs the integration).
  • Copy-on-Write is opportunistic: a reflink-capable filesystem (Windows ReFS / DevDrive, Linux Btrfs / XFS, macOS APFS) enables it; everything else falls back to a plain copy automatically.

Install

Quick install (no Node.js required)

macOS / Linux:

curl -fsSL https://github.com/ZelAnton/agent-workspace/releases/latest/download/install.sh | sh

Windows (PowerShell):

iwr https://github.com/ZelAnton/agent-workspace/releases/latest/download/install.ps1 -UseBasicParsing | iex

Installs ws to ~/.agent-workspace/bin, adds it to your PATH, and runs ws setup for shell integration.

Via npm

npm install -g @zelanton/agent-workspace

Update

ws update

ws update detects how ws was installed (~/.agent-workspace/install_channel):

  • Shell installer — downloads the latest release from GitHub and atomically replaces itself (uses self_replace for the Windows .exe rename-trick).
  • npm — re-runs npm install -g @zelanton/agent-workspace@latest.

Shell integration is installed automatically by both channels. To reinstall manually:

ws setup

Supported shells: bash, zsh, fish, PowerShell

Uninstall

Remove the shell wrapper installed by ws setup:

ws uninstall

If the ws binary is missing or broken, run the standalone script instead — it works without a functioning binary:

# macOS / Linux
curl -fsSL https://github.com/ZelAnton/agent-workspace/releases/latest/download/uninstall.sh | sh

# Windows (PowerShell)
iwr https://github.com/ZelAnton/agent-workspace/releases/latest/download/uninstall.ps1 -UseBasicParsing | iex

The uninstall step does not delete ~/.agent-workspace/ (binary, channel marker, cached config) or any worktrees you created — both scripts print follow-up commands for those. If installed via npm, also run npm uninstall -g @zelanton/agent-workspace.

Quick Start

# Create a worktree and enter it
ws new feature-x

# ... develop, commit ...

# Merge back (merges to the branch you were on when creating)
ws merge            # keeps worktree
ws merge -d         # deletes worktree after merge

Other useful commands:

ws ls              # List all worktrees (with BASE branch info)
ws cd feature-y    # Switch to another worktree
ws cd              # Return to main repository

Snap Mode

One-liner for AI agent workflows:

ws new -s claude           # Random branch name
ws new fix-bug -s codex    # Specified branch name
ws new -s "claude --dangerously-skip-permissions"  # Command with arguments

Argument quoting-s takes a single token. Use quotes whenever the command has flags or arguments (-s "agent --flag"), otherwise the shell hands the trailing args to ws new instead.

Nested snap is refused — running ws new -s from inside an existing worktree exits with an error. Run ws cd to return to the main repo first.

Flow: Create worktree → Enter → Run agent → [Develop] → Agent exits → Check changes → Merge → Cleanup

After the agent exits — whether normally or with a crash / Ctrl+C — ws checks the worktree state:

  • No changes: Worktree cleaned up automatically
  • Only commits (nothing uncommitted):
    [m] Merge into base branch
    [q] Exit snap mode
    
  • Uncommitted changes:
    [r] Reopen agent (let agent commit)
    [q] Exit snap mode (commit manually)
    

base_branch must still exist — if the worktree's base branch was deleted while the agent ran, [m] errors out. Use ws merge --into <branch> to pick an explicit target instead.

Commands

Worktree Management

Command Description
ws new [branch] Create worktree from current branch (random name if omitted)
ws new --base <branch> Create from specific base branch (default: current branch)
ws new -s <cmd> Create + snap mode
ws new --no-cow Skip Copy-on-Write cloning for this creation
ws new --no-tab / --in-new-tab Override terminal-tab behavior for this creation
ws cd [branch] Switch to worktree (omit branch to return to main repo)
ws ls List worktrees
ws ls -l Show full path for each worktree
ws mv <old> <new> Rename worktree (use . for current)
ws rm <branch> Remove worktree (use . for current)
ws rm -f <branch> Force remove with uncommitted changes
ws clean Remove worktrees with no diff from their base branch (falls back to trunk); dirty worktrees are skipped
ws clean --dry-run Preview which worktrees would be cleaned

Workflow

Command Description
ws merge Merge to base branch (falls back to trunk, default: squash)
ws merge -s <strategy> Merge with strategy (squash/merge)
ws merge --into <branch> Merge to specific branch (overrides base)
ws merge -d Delete worktree after merge (default: keep)
ws merge -H Skip pre-merge hooks
ws sync Sync from base branch (falls back to trunk, default: rebase)
ws sync -s <strategy> Sync with strategy (rebase/merge)
ws sync --from <branch> Sync from specific branch (overrides base)
ws sync --continue Continue after resolving conflicts
ws sync --abort Abort sync

Info

Command Description
ws status Show current worktree info (also reports in-progress ws sync rebase/merge with recovery hints)
ws repo-info Show the cached per-repo metadata (file count, size, origin URL, GitHub slug)
ws repo-info --refresh Force-refresh that cache (otherwise auto-refreshed every 30 days)
ws update Update to the latest version

Per-repo settings

Command Description
ws config list List supported per-repo keys with descriptions
ws config get <key> Read a setting (e.g. workspace.alias, workspace.use_path_hash)
ws config set <key> <value> Write a setting to the repo-level config
ws config unset <key> Remove a setting
ws exclude Open the TUI tree picker for [copy] exclude patterns
ws exclude <pattern>... Add CoW exclude patterns (e.g. target node_modules '**/*.iso')
ws exclude --list Show current exclude patterns
ws exclude --remove <pattern> Drop a pattern
ws exclude --clear Remove all patterns

Both ws config and ws exclude write the local repo-level .workspace.toml and keep it out of git automatically (via .git/info/exclude).

Configuration

Command Description
ws setup Install shell integration (auto-detect)
ws setup --shell zsh Install for specific shell
ws uninstall Remove shell integration (inverse of setup)
ws init Initialize project config
ws init --trunk <branch> Initialize with specific trunk branch
ws init --merge-strategy <strategy> Set default merge strategy (squash/merge)
ws init --sync-strategy <strategy> Set default sync strategy (rebase/merge)
ws init --copy-files <pattern> Files to copy to new worktrees (repeatable)

Machine-readable output (--format json)

ws is built to be driven by AI agents, so every read/result command can emit a stable JSON object instead of scraped text. Add the global --format json flag:

ws ls --format json          # { "worktrees": [ { "branch", "base_branch", "is_current", "commits", ... } ] }
ws status --format json      # { "branch", "merge_target", "commits", "uncommitted", "in_progress", ... }
ws repo-info --format json   # { "repo_name", "origin", "total_files", "total_bytes", "from_cache", ... }
ws new feature-x --format json   # { "branch", "path", "base_branch", "created", "snap" }
ws merge --format json           # { "merged", "branch", "target", "commits", "deleted" }
ws sync --format json            # { "action", "branch", "target", "strategy" }
ws clean --format json           # { "dry_run", "cleaned": [...], "skipped_dirty": [...] }
ws rm . --format json            # { "action": "removed", "branch", "returned_to" }
ws config list --format json     # { "path", "keys": [ { "key", "kind", "value", "description" } ] }

Every object carries a top-level schema_version (currently 1) so an agent can gate on the shape it expects — see docs/json-output.md for the full per-command roster and the versioning policy. mv, cd, exclude --list, and config get emit JSON too.

Discipline: in JSON mode stdout carries only the single JSON object; all progress, notices, and the update nag go to stderr (so ws ls --format json | jq just works). Errors are emitted as { "error", "causes": [...] } on stderr with a non-zero exit. ws new --format json prints the created worktree path on stdout — an agent can read it directly instead of relying on shell integration. Human output (the default, --format human) is unchanged.

Configuration

Base Directory

Defaults to ~/.agent-workspace. Override via AGENT_WORKSPACE_DIR:

export AGENT_WORKSPACE_DIR=/data/agent-workspace

Global Config $AGENT_WORKSPACE_DIR/config.toml (default ~/.agent-workspace/config.toml)

[general]
merge_strategy = "squash"  # squash | merge
sync_strategy = "rebase"   # rebase | merge
copy_files = [".env", ".env.*"]  # Gitignore-style patterns for files to copy

[hooks]
post_create = ["pnpm install"]
pre_merge = ["pnpm test", "pnpm lint"]
post_merge = []

copy_files constraints — patterns are gitignore-style and must stay inside the repo: leading / (absolute paths) and .. traversal are rejected. Symlinks are not followed.

Hook trust boundary — hooks run via sh -c (or cmd /C on Windows) with no sandboxing or timeout. Treat .workspace.toml like any shell script: only run repos whose hooks you would bash directly.

Hook CWDpre_merge and post_merge always run with the worktree root as the working directory. post_create runs in the new worktree.

Project Config .workspace.toml

Repo/worktree config lives in .workspace.toml — a local, per-machine file that ws keeps out of git by adding it to the repo's local exclude file (.git/info/exclude), so there's nothing to commit and your working tree stays clean (existing repos with a committed .agent-workspace.toml keep working: it's read as a fallback when no .workspace.toml is present). It layers in three tiers, later overriding earlier:

  1. global~/.agent-workspace/config.toml
  2. repo-level.workspace.toml at the main repo root
  3. worktree-level.workspace.toml at an individual worktree's root (overrides the repo-level file for commands run inside that worktree)

trunk is project-only; other fields are merged (copy_files / copy.exclude append, hooks replace per-phase when set, the rest are overrides). ws config and ws exclude write the repo-level file; worktree-level files are hand-authored.

[general]
trunk = "main"  # Trunk branch (auto-detected if omitted)
vcs = "git"     # Force a backend for this repo: "git" | "jj" (default: auto-detect)
merge_strategy = "merge"  # Override global merge strategy
sync_strategy = "merge"   # Override global sync strategy
copy_files = ["*.secret.*"]  # Appended to global copy_files

[hooks]
post_create = ["pnpm install"]  # Replaces global hooks if set

[copy]
# Gitignore-style patterns the Copy-on-Write path SKIPS when cloning the source
# repo into a new worktree. Anchor with a leading `/` for top-level only.
exclude = ["/target", "/node_modules", "**/*.iso"]

[create]
use_cow = true          # Copy-on-Write worktree creation (default: true when supported)

[ui]
open_in_new_tab = true  # Open ws new / ws cd in a new terminal tab (default: true)

[workspace]
alias = "myrepo"        # Override the workspace directory name (default: repo basename)
use_path_hash = false   # Append a `-<hash>` suffix to disambiguate same-named repos

VCS backend selection

ws speaks both git and Jujutsu. The backend is resolved once per invocation, in this order:

  1. --vcs <auto|git|jj> global flag
  2. [general] vcs in the repo-level .workspace.toml
  3. [general] vcs in the global config
  4. auto-detection from .git/ / .jj/ presence

Colocated repos (both .git/ and .jj/) default to jj — you installed jj for a reason, so ws respects it. Force git with --vcs=git or vcs = "git". A handful of git-shaped operations have no clean jj analogue and return a clear Error::Unsupported with a hint rather than guessing:

Operation jj guidance
ws mv use ws rm + ws new
ws sync --abort / --continue jj records conflicts in commits — resolve the files and re-run

See AGENTS.md → "VCS backend compatibility" for the full semantic-delta table (no staging area, bookmarks vs branches, atomic operations).

How shell integration works

A child process can't change its parent shell's directory, so the commands that move you (ws cd, ws new, ws rm, ws mv, ws merge -d, ws clean) can't cd you directly. Instead the binary writes the destination to a temp file and a tiny shell wrapper (installed by ws setup) reads it and performs the cd.

Consequences worth knowing:

  • Run ws setup once per shell. Without it, the cd-changing commands still do their work but can't move your shell — and ws refuses to delete or move the current worktree without integration (it would strand you in a deleted directory). cd to the main repo first, or install the wrapper.
  • The wrapper is bracketed by # === agent-workspace BEGIN/END === markers in your rc file; ws uninstall removes exactly that block and nothing else.
  • On terminals with tab support, ws new / ws cd open a new tab instead, and the originating shell stays exactly where it was.

Troubleshooting

  • ws cd doesn't change my directory — shell integration isn't active in this shell. Run ws setup, then open a new shell (or source your rc file).
  • "Refusing to remove/move the current worktree without shell integration" — same cause. Run ws setup, or ws cd back to the main repo and retry.
  • ws new opened a new tab I didn't want — pass --no-tab, or set [ui] open_in_new_tab = false in your config.
  • Creation isn't using Copy-on-Write — the source repo and $AGENT_WORKSPACE_DIR must be on the same reflink-capable volume. ws probes with a real sentinel reflink and falls back to a plain copy silently otherwise. Force a plain copy with --no-cow.
  • Merge says "Base branch '…' no longer exists" — the worktree's base branch was deleted. Pick an explicit target: ws merge --into <branch>.

Storage Layout

~/.agent-workspace/
├── bin/                           # ws binary (shell installer)
├── config.toml                    # Global config
├── install_channel                # 'npm' or 'shell'
└── {project}-{hash}/              # one dir per source repo
    ├── {branch_name}.toml         # Worktree metadata
    ├── {branch_name}/             # Worktree directory
    └── ...

v0.13.6 layout change — earlier versions added an extra workspaces/ directory between the base and the per-project dirs (~/.agent-workspace/workspaces/{project}-{hash}/{branch}/). That intermediate level is gone now. Existing workspaces/ directories from pre-0.13.6 installs are NOT migrated automatically — the worktrees inside have absolute paths baked into git's gitlink / jj's workspace registration that would break on relocation. ws prints a one-line migration nudge on startup whenever it spots a non-empty legacy workspaces/ dir. Run ws rm <branch> from inside each old worktree to clean up, or rm -rf ~/.agent-workspace/workspaces if you don't need them.

License

MIT © Anton Zhelezniakou. Fork of nekocode/agent-worktree.

About

A workspace workflow tool for AI coding agents. Supports Git worktrees and jj workspaces, with fast isolated environment creation using ReFS block cloning on Windows.

Topics

Resources

License

Stars

3 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages

  • Rust 94.6%
  • Shell 3.1%
  • PowerShell 1.7%
  • JavaScript 0.6%