A Rust CLI that creates per-story workspaces for AI coding agents (pi, claude, codex, opencode) across a polyrepo / microservice codebase.
Each story gets a fresh directory — ~/.agentws/<story>/ — containing git worktrees of only the repos that story touches. The agent is scoped by default (no token waste or context pollution from grepping the whole work/ tree), but it can request access to another repo at any time and a human approves it.
Design philosophy:
agentwsdoes not spawn agents. It owns the workspace. Youactivateit (conda-style), then run any agent natively. Resume,/resume, model switching — all of that just works because the agent sees the workspace as its project directory.For current development status, see
HANDOFF.md.
# 1. install
cargo install --path . --locked
# or build and copy target/release/agentws somewhere on PATH
# 2. configure (one-time)
mkdir -p ~/.config/agentws
printf 'repo_roots = ["~/work"]\n' > ~/.config/agentws/config.toml
# 3. add shell integration (one-time per shell)
eval "$(agentws init-shell zsh)" # or bash / fish
# 4. create a workspace
agentws new auth-payments --repos svc-auth,svc-api
# 5. activate it, then run any agent normally
agentws activate auth-payments
pi # native pi, scoped to the workspace
# or one-shot passthrough
agentws activate auth-payments pi -c
# done for the day
agentws deactivateThink conda activate, not docker run.
When you activate a workspace, agentws does three things in your current shell:
cd ~/.agentws/<story>/export AGENTWS_WORKSPACE=<story>- remembers where you came from for
deactivate
That's it. After that, pi, claude, codex, or opencode run normally with the workspace as their project directory. They see only the repos inside that workspace, and they use their own native session/resume logic.
Because activate/deactivate change your shell state, they are implemented as a sourced shell function, not as a binary. Add this to your shell rc:
# ~/.zshrc or ~/.bashrc
eval "$(agentws init-shell zsh)"For fish:
# ~/.config/fish/config.fish
agentws init-shell fish | sourceAGENTWS_WORKSPACE is per-shell (not global). Two terminal tabs can have two different active workspaces, exactly like conda.
agentws new <story> [--repos a,b,c] [--base main] # create worktrees + activate hint
agentws list # list workspaces, * marks active
agentws status [story] # show repos, branches, requests
agentws open <story> # print workspace root path
agentws use <story> # set the global active pointer
agentws delete [story...] [--dry-run] [--force] [--yes] # remove worktrees + manifest;
# bare `delete` = fuzzy multi-select;
# dirty worktrees kept unless --forceOnce activated (or when run from inside a workspace), these resolve the target workspace automatically:
agentws add <repo> [--story story] # add another repo now
agentws remove <repo> [--story story] # drop a repo from the workspace
agentws archive <story> # remove worktrees, keep manifest
agentws restore <story> # recreate worktrees from manifest
agentws code [--story story] # open in VS Code (multi-root)agentws code opens the workspace in VS Code via a generated multi-root
.code-workspace (one top-level folder per repo worktree), so each worktree
shows as a flat root and gets its own Source Control entry — handy for diffing
a single worktree against main in the GUI. The file is regenerated on
new/add/remove/restore and removed on archive; agentws code also
recreates it if missing. Like the others, story is inferred from
$AGENTWS_WORKSPACE / cwd, so a bare agentws code from inside an activated
workspace just works.
activate/deactivate are shell functions, not binary subcommands — load them
once via agentws init-shell (see The activation model):
agentws activate <story> # cd into the workspace + set $AGENTWS_WORKSPACE
agentws activate <story> pi -c # one-shot: run a command there, then return
agentws deactivate # restore cwd + unset envagentws request <repo> [--story story] [--reason ...] # queue a request
agentws pending # show pending requests
agentws approve <id-or-repo> [--story story] # create worktree + mark approved
agentws deny <id-or-repo> [--story story] # mark deniedagentws mcp # run the MCP server (stdio)
agentws mcp-config claude # print MCP wiring snippet
agentws completions zsh # generate shell completions
agentws init-shell zsh # print activate/deactivate function
agentws config # show resolved config + key paths
agentws discover # list discovered repos~/.config/agentws/config.toml:
# Roots that will be scanned for git repositories.
repo_roots = ["~/work"]
# Default base branch to create story branches from
# (default: each repo's default branch).
# default_base = "main"
# Paths to symlink from each original repo into its worktree.
# symlinks = ["node_modules", ".env*"]
# Shell command run inside each worktree right after creation.
# post_create = "npm ci"The differentiator: an agent can discover it needs another repo mid-session, request it, and a human approves it.
Two ways to add a repo:
- Human-initiated:
agentws add <repo> - Agent-initiated: the agent uses the
agentwsMCP server (tools:list_available_repos,request_repo,check_request).
When the agent calls request_repo, agentws writes a pending request to the manifest and fires a notification. The human runs agentws approve <id> in any terminal. On approval, a worktree is created under the workspace root, so it appears in-scope to the running agent with no restart.
To wire the MCP server into an agent:
agentws mcp-config claude # prints the snippet for ~/.claude.json
agentws mcp-config codex # prints the snippet for ~/.codex/config.tomlFor pi, there is no built-in MCP; use the CLI commands directly or build a pi extension.
- Always activate. Commands like
add,request,statusresolve the workspace in this order:--storyflag →$AGENTWS_WORKSPACEenv var → cwd inside a workspace → the global.currentpointer → single workspace. - Resume is free. Because agents key sessions by project directory,
pi -cor/resumeinside an activated workspace resumes that workspace's conversation automatically. - Piping is safe.
agentws list | headexits quietly instead of panicking on a broken pipe. - Switching stories.
agentws use <story>sets the global pointer for commands run outside an activated shell. Inside an activated shell, the env var wins.
cargo build # build
cargo test # run unit + integration tests (21 tests)
cargo clippy --all-targets # lint
cargo install --path . --locked # install to ~/.cargo/bin/agentwsThe crate exposes both a binary (src/main.rs) and a library (src/lib.rs), so
the modules are testable: in-module unit tests (#[cfg(test)]) cover the pure
functions and the git/worktree/discovery/manifest logic, and an integration test
under tests/ (resolve_priority.rs) runs in its own process so it can
mutate HOME / $AGENTWS_WORKSPACE / cwd without racing other tests.
See HANDOFF.md for the current development state, including known issues and the next steps.