# Shoal
Shoal gives each coding task its own Git worktree and coordinates shared ports,
Xcode simulators, and other resources. Run agents or commands in a workspace,
switch between tasks, and clean up when finished.
## Install
```sh
brew install hn05/tap/shoal
shoal daemon install
shoal skill install
```
Use `brew install --HEAD hn05/tap/shoal` to track `main` instead of releases, or
download a prebuilt macOS or Linux binary from a [release](https://github.com/HN05/shoal/releases);
the macOS binaries are unsigned, so clear the download quarantine first with
`xattr -d com.apple.quarantine shoal`.
Homebrew installs the additional tools for workspace management and interactive
menus. For a downloaded binary, install
the [runtime dependencies](#runtime-dependencies) before running `shoal daemon install`.
`shoal daemon install` starts the per-user
daemon and writes missing config and chat template defaults in `~/.config/shoal/`;
`shoal skill install` installs the worker and orchestrator skills into each
[AI tool's existing skill directory](docs/reference.md#agent-skill-outside-project-repositories);
`shoal skill install <tool>` creates one. Later `shoal` commands keep installed
skills current after upgrades.
For directory navigation and tab completion, add this to your `.zshrc` or
`.bashrc`, then run it in your current shell:
```sh
source <(shoal shell init)
```
### Runtime dependencies
Put these tools on `PATH` before running `shoal daemon install`, which captures it for
the daemon. Prebuilt binaries do not require Rust.
| Tool | Needed for |
| --- | --- |
| Git 2.43+ (`git`) | Repository and branch operations |
| [Worktrunk](https://worktrunk.dev/) (`wt`, tested with 0.81.0) | Creating and removing worktrees |
| `lsof` | Checking whether a workspace is in use before cleanup |
| [zmx](https://github.com/neurosnap/zmx) | Agent sessions that keep running after you detach |
| `ps` (included with macOS; typically `procps` or `procps-ng` on Linux) | Tracking and stopping processes |
| [fzf](https://github.com/junegunn/fzf) | Interactive menus and pickers |
Feature-specific tools are installed separately: an authenticated `gh` (GitHub),
`glab` (GitLab), or `fj` or `tea` (Forgejo, Gitea) for issues and PR watches, or
`curl` and [Bitbucket credentials](docs/reference.md#forges); the agent or
command you want to run;
and `happy` plus `curl` for Happy sessions. Simulator use requires macOS with
Xcode, `xcrun simctl`, and an installed simulator runtime.
Service installation uses launchd on macOS or a systemd user session on Linux;
without one, [run the daemon in the foreground](docs/reference.md#daemon).
## Create a workspace
Register a local repository or Git clone URL once, then create a task. Each
repository's workspaces (and its clone, for URLs) live under `~/shoal/<name>/`;
`shoal repo migrate <dir>` moves them all to a new root:
```sh
shoal repo add /path/to/project --name my-project
shoal repo setup my-project # Have an agent write and commit its .shoal.toml
shoal add my-project fix-login
shoal codex fix-login --cli
```
Use `shoal add my-project` to pick a new or existing branch, an open issue or an open PR, or pass it explicitly:
```sh
shoal add my-project fix-api --base feature/api # Branch from feature/api
shoal add my-project fix-api --base origin/feature/api # Fetch the pushed branch first
shoal work base set feature-api --workspace fix-api # Record the workspace fix-api builds on
shoal split fix-typo --workspace fix-api # Create a child workspace for work in its own PR
shoal add my-project --existing origin/feature/api --agent codex
shoal add my-project quick-fix --path ../quick-fix
shoal work adopt ../existing-worktree # Take ownership, including automatic cleanup
shoal work adopt --copy ../existing-worktree # Copy work into Shoal's default directory
shoal work rename fix-login fix/login # Rename its branch and workspace together
shoal add https://github.com/owner/repo/issues/68 # Create from an issue
shoal add https://github.com/owner/repo/issues/34 # Add an issue, PR, or branch link
shoal contribute https://github.com/owner/repo/issues/68 # Fork when you cannot push; remove the clone afterwards
shoal link https://github.com/owner/repo/issues/34 assigned # Assign an issue to the current workspace
shoal unlink issue assigned # Remove the assigned issue
shoal watch pr 505 # Watch an explicit PR
```
Inside Herdr, `add` opens a new tab after your choices; use `--here`
to run in the current pane, `--focus` to switch to the new tab, or
`herdr.enabled = false` to treat Herdr as a plain terminal. Elsewhere, an agent that starts with an issue or arguments runs in a
detached zmx session that `shoal attach` reaches. See
[Herdr settings](docs/reference.md#agents).
Use `--agent claude` for Claude Code, `opencode`, `pi` or `grok` for those
tools, or `--agent happy-claude`/`happy-codex` for
a detached [Happy](https://github.com/slopus/happy) session that appears in the
Happy app. Interactive `shoal add` uses `--agent` or picks an installed agent.
Issue additions also use `default_agent = "codex"` from the repository or global
config. Edit `~/.config/shoal/worker-template.md` to add instructions for every
workspace agent and `chat-template.md` for `shoal chat`. Agents read a
repository's own rules from its `AGENTS.md` or `CLAUDE.md`; the same template
files in a repository root add instructions only Shoal's agents need.
New workspaces branch from the repository's default branch unless `--base REF`
is supplied to `add`. With shell integration, `shoal add` enters the
workspace unless its agent runs in a zmx session.
Agent admission defaults to starting agents one at a time while measured CPU,
memory and disk load leaves room; `load = "light"`, `"balanced"` or `"full"`
says how hard to push the machine. Set `max_agents` globally or for one
repository to a fixed limit
(`shoal config set max_agents 3`, `--repo my-project`), `dynamic`, or `none` to
disable queuing for that layer. `shoal add` queues agents past the effective
limit and starts them as slots free; `--now` starts one anyway, and `shoal rm
<name>` drops a queued one. `--after <workspace|issue>` holds an agent until
that work is done and starts it on the finished branch; an issue's agent also
waits for the issues blocking it. See the [agent queue](docs/reference.md#agent-queue).
## Everyday commands
After [adding an AI tool](docs/reference.md#agents), run
`shoal add my-project fix-api --agent droid` or `shoal add <issue-url>`.
```sh
shoal # Inbox when something needs you, else workspaces; Tab switches
shoal repo # Interactive repository menu
shoal inbox # Questions, approvals, agents needing input, failures, ready work, demos; --print to list
shoal work # Workspaces, chats and queued agents; --print to list
shoal work --queue # Agents waiting for a slot under max_agents
shoal work --chats # Running chats
shoal work --repo my-project # Only one repository's workspaces, chats and queued agents
shoal work --status needs-input,failed # Only rows with these statuses
shoal status fix-login # Workspace activity, changes, resources, issue and PR state
shoal status pr 12 # Workspaces that link PR 12; also issue or resource
shoal config show fix-login # Effective settings and the source of each value
shoal config set default_agent codex
shoal config set cleanup.auto.enabled true cleanup.auto.idle_minutes 30 # Saved together
shoal config set default_agent claude --repo my-project
shoal config unset default_agent # Restore the default
shoal cd fix-login # Enter a workspace; omit the name for a picker
shoal run --workspace fix-login -- cargo test
shoal edit fix-login # Open in $VISUAL/$EDITOR, or [commands] edit = ["zed", "{path}"]
shoal run check --workspace fix-login # With [commands] check = ["cargo", "test"] in config
shoal run # List configured commands, arguments, and source layers
shoal claude fix-login # Run Claude Code
shoal codex fix-login --app # Open in the Codex desktop app
shoal attach fix-login # Reattach to the workspace's agent session; Ctrl-\ detaches
shoal demo # Pick a running demo an agent started and open it
shoal demo start web # Start the configured demo `web` for the user to try
shoal happy claude fix-login # Detached Happy session, visible in the Happy app
shoal chat my-project # Discuss the repository with an agent in its checkout
shoal plan --oldest # Settle the oldest unplanned issue's plan in a chat
shoal diff fix-login # Changes since the branch's fork point
shoal conflicts --workspace fix-login # Whether the branch merges cleanly into main
shoal alerts # Conflicts, finished agents, and removals you missed
shoal alerts send "PR #12 is ready to merge" # Notify the user from a workspace
shoal messages send "Stop the dev server" --workspace fix-login # Message the workspace's agents
shoal questions # Pick a question and answer it
shoal questions 3 --answer 2 # Answer question 3 with its second choice
shoal stop fix-login # Stop agents and commands; save them for shoal resume
shoal stop fix-login api # Stop several; the picker selects them with Tab or Ctrl-A
shoal stop --all # Stop every workspace; a reboot saves agents too
shoal resume --all # Restore stopped agents; report stopped commands
shoal done fix-login # Mark finished and request safe cleanup
shoal done --cleanup fix-login # Request cleanup despite [done] cleanup = false
shoal done fix-login --undo # Withdraw a recorded done
shoal rm fix-login # Remove the workspace
shoal rm fix-login old-spike # Remove several; the picker selects them with Tab or Ctrl-A
shoal cleanup # Pick leftover workspaces, kept branches and caches to remove
shoal work checkpoint fix-login # Keep HEAD and unsaved files to return to later
shoal work restore fix-login # Pick a checkpoint, or open a removed workspace again
shoal work restore fix-login 3 # Return the workspace to checkpoint 3
shoal export fix-login -o - | ssh other shoal import - # Move the workspace to another machine
shoal export --repo app --all -o moving # Export every workspace of app, a file each
shoal import moving # Import every workspace exported to a directory
shoal pr open # Push, then link the branch's open PR or open one
shoal pr edit --draft # Change the linked PR's text, labels, reviewers or draft
shoal pr comment --body Done # Comment on the linked PR; close and reopen work too
shoal pr comment --attach demo.mp4 # Upload a file and show or link it in a comment
shoal pr resolve 812 --body-file why.md # Reply in a review thread, then resolve it
shoal pr merge 42 --method rebase # Merge a PR yourself; agents cannot
shoal issue list --label bug flaky # Search issues by text, label, author and state; pr list too
shoal issue close # Close the linked issue; edit, comment, reopen too
shoal issue open --title Bug # Open an issue, recorded as opened; --link also assigns it
shoal issue open --repo team/tool --title Bug # Open it in another repository
shoal repo labels # The repository's labels and what each one means
shoal forge capabilities # What the repository's forge supports, such as resolving threads
shoal forge api 'repos/{owner}/{repo}/branches' # Any path of the forge's API; agents only read
shoal forge ci run deps.yml --input level=full # Start a workflow run on the workspace's branch
shoal forge ci view 2765 # A run's jobs and failed jobs' logs; list shows the newest runs
shoal link 42 assigned # Assign an issue or PR by number (or paste its URL)
shoal link pr 43 assigned # Assign another PR; all must merge before cleanup after done
shoal link 50 related # Record an issue or PR the work relates to
shoal watch # Wake on all linked item comments, checks, or closure
shoal view # Linked items with state, failed CI logs, comments, and reviews
shoal view pr 44 --no-comments # One PR, linked or not, without its discussion
shoal view pr 44 --check rust # Show only this failed CI check's log
shoal pr ci rerun --check rust # Rerun a failed check on the linked PR; cancel stops running ones
shoal ready # Mark linked items ready for review at HEAD
shoal unlink pr 43 assigned # Cancel one assigned PR
shoal unlink pr assigned # Cancel assigned PRs
```
For workspace actions, omit the name to use your scoped or current workspace;
outside one, choose from the interactive picker or pass a target for `--json`
and noninteractive calls. Bare `shoal cd` always opens a picker. `shoal cd -`,
`shoal attach -` and other commands that open, show or run something in a
workspace take `-` for the workspace they last acted on.
Use `shoal <command> --help` for options.
To move changes between your workspace and the default branch:
```sh
shoal repo sync # Fetch and fast-forward the default branch
git rebase main # Or git merge; use your repo's branch name
shoal land # Merge your branch into the default branch; no remote needed
shoal land --push # Then push the default branch instead of opening a PR
```
For local code review, configure `[commands] review = ["tuicr", "-r",
"{diff_base}..HEAD"]` and run `shoal review fix-login` or `shoal review <pr-url>`,
then choose manual or agent review; `shoal review <issue-url>` has an agent refine
an issue before implementation. See the
[command and review configuration](docs/reference.md#configured-commands) for uncommitted
review and exporting feedback to an agent.
CLI agents run in [zmx](https://github.com/neurosnap/zmx) sessions that keep
running when you detach with Ctrl-\ or close the terminal; `shoal attach` returns
to one. Happy sessions run detached, log
to a file Shoal names, and stop with `shoal stop`. The Codex shortcut disables
Codex's sandbox and approval prompts; use `shoal run --workspace fix-login -- codex` for a
custom invocation. Shoal's resource scope is cooperative, not a filesystem sandbox.
Apps that launch their own processes can request `shoal work env fix-login --json`
and add the returned variables to each child's environment. Revoke its scope
with `shoal work env fix-login --revoke <token>`. See
[workspace scope](docs/reference.md#scoped-workspace-commands) for token lifetime
and cleanup behavior.
The agents Shoal starts get JSON from `shoal` without `--json`; give an agent
you start yourself `SHOAL_OUTPUT=json` for the same, and pass `--text` for
human output.
For a separate agent account, configure forge executable wrappers and a
[Git profile](docs/reference.md#git-profiles) for the agent's commits:
```toml
[agent_auth]
fj = "~/bin/fj-agent"
fj_home = "~/.local/share/shoal-auth/fj" # the HOME fj-agent runs fj with
gh = "~/bin/gh-agent"
git_profile = "agent"
```
See [agent accounts](docs/reference.md#agent-accounts)
for wrapper setup.
Memory overload protection stops tracked agents by default while retaining their
workspaces and leases; before that, Shoal messages running agents to reduce load. To opt out, run `shoal config set overload.memory.enabled
false`. See [overload protection](docs/reference.md#overload-protection)
for thresholds and timing. When disk space runs low, the daemon removes
workspaces idle cleanup would remove; if space stays critical, it stops tracked
executions. Agents restore once space returns; `shoal resume` restores the rest. Stop agents manually with `shoal stop`, then restore
a stopped session with `shoal resume`. `shoal resume` also continues an agent that
exited on its own, such as one you quit. Codex and Claude automatically continue
the workspace session after load recovers; `[agent_resume]` overrides their restore
command or enables recovery for other agents.
## Share resources
From a managed workspace:
```sh
shoal acquire port web --reason "Development server"
shoal catalog # What each kind offers: configuration, devices, runtimes
shoal leases # Every kind's leases and usage
shoal release port web
shoal acquire sim --queue # macOS; requires configured simulator profiles
shoal release sim
shoal acquire resource signing --queue
shoal release resource signing
```
Use the returned port or simulator UDID. Reservations and leases belong to the
workspace and survive command exit; release them when finished.
Install a packaged global config with `shoal config install <name>`;
`shoal config install --help` lists the available names.
Put project defaults in `.shoal.toml` or `.shoal/config.toml`. For example:
```toml
[ports.web]
port = 3000
env = "PORT"
[resources.signing]
capacity = 1
requires_approval = true
approval_lifetime = "lease" # Or "workspace"
[resources.server]
kind = "repo"
repo = "saldoir-server" # Registered repository name or remote URL
```
Acquire a related repository with `shoal acquire repo server`. Use its
checkout path in a project override, for example:
```sh
server=$(shoal --json acquire repo server | jq -r '.repository.path')
SALDOIR_SERVER_REPO="$server" Scripts/sync-contract.sh
shoal release repo server
```
Agents request protected resources with `acquire --reason "purpose"`. Review
requests with `shoal approvals`, then use `shoal approvals --approve <number>` or
`shoal approvals --deny <number>` from an unscoped terminal.
Select a named Git identity with `git_profile = "work"` in that file, or use
`shoal add my-project feature --git-profile work`. Define the profile
in global config; see [Git profiles](docs/reference.md#git-profiles).
New workspaces get the repository checkout's ignored local files, such as `.env`;
set `copy_ignored = false` in that file to turn this off. The same file can name a
`setup_cmd`, run automatically on workspace creation or explicitly with
`shoal work setup [workspace]`, and hooks around setup, assignment completion,
agent exits, removal, and resource acquisition/release, for example to open a tmux session or prepare an
external device. See [setup and hooks](docs/reference.md#workspace-setup-and-hooks)
and [resource hooks](docs/reference.md#resource-hooks) for configuration and failure behavior.
A `[seed]` table keeps a build of the default branch that new workspaces clone, so
they rebuild only what their branch changed; see [seed builds](docs/reference.md#seed-builds).
`presets = ["cargo", "bun"]` sets that up for the tools a repository uses; see
[build tool presets](docs/reference.md#build-tool-presets).
Use `[env]` to give tracked executions and hooks workspace-specific variables. Values
substitute `{workspace}`, `{workspace_path}`, `{repo}`, `{branch}`, reserved
`{port.NAME}` values and [shared cache](docs/reference.md#shared-caches) directories
`{cache.NAME}` once; inserted text is not evaluated. For example:
```toml
[env]
AGENT_BROWSER_SESSION = "{workspace}"
AGENT_BROWSER_PROFILE = "{workspace_path}/.agent-browser"
GOCACHE = "{cache.go}"
[caches.go]
scope = "shared"
```
Use `[secrets]` in global or saved repository config to give agents, or only
tools such as `agent-browser`, values fetched from your secret store; see
[agent secrets](docs/reference.md#agent-secrets).
## Cleanup
`shoal rm` removes workspaces and their resources, prompting once, and only before
it deletes uncommitted work. It deletes the branch too unless the branch has commits that are
on no remote and not on the default branch; Shoal deletes such a kept branch once
they are, and `shoal cleanup branches` deletes kept branches, several at a time, with
their commits. `shoal stop` keeps the workspace.
Automatic cleanup removes idle, clean, pushed or landed workspaces after 10 minutes,
merged workspaces after 4 idle hours whether or not their agent ran `shoal done`,
and forgets workspaces whose directory you deleted yourself, keeping the branch.
`shoal cleanup` lists what Shoal left behind: those workspaces, kept branches, and
shared caches nothing is using, with why it keeps the other workspaces. On a terminal
it opens a picker, where Tab selects several, and removes what you pick.
`shoal cleanup workspaces`, `branches` or `caches` acts on one kind, and `shoal status`
shows why it keeps one workspace.
Every removal can be undone for 30 days: `shoal work restore` picks a removed workspace,
and `shoal work restore <workspace>` opens one again with its uncommitted files and links;
`shoal resume` then continues the agents the removal stopped. Removal keeps the
workspace's HEAD and unsaved files in a checkpoint, as `shoal work checkpoint` does and
Shoal does when an agent exits or is stopped; `shoal work restore <workspace> <number>`
returns an existing workspace to one of them. `shoal export <workspace> -o <file>`
removes a workspace and writes that checkpoint and record to a file, and `shoal
import <file>` opens it on another machine, where its agents start new sessions, or
on the same one, where `shoal resume` continues their sessions; `--keep-sessions`
and `--new-sessions` choose either.
An app hosting an external session can hold its workspace while the session is open:
```sh
shoal acquire hold --workspace fix-login --name quay-thread-42 --reason "Quay thread"
shoal leases hold --workspace fix-login
shoal release hold --workspace fix-login --name quay-thread-42
```
Configure idle cleanup in a repository's `.shoal.toml`, or for every repository in
`~/.config/shoal/config.toml` followed by `shoal daemon reload`:
```toml
[cleanup.auto]
enabled = false
```
When something looks wrong, run `shoal doctor --all` for diagnostics or
`shoal doctor <name>` for a workspace; see
[cleanup](docs/reference.md#automatic-cleanup) and [recovery](docs/reference.md#recovery) for details.
## Update
```sh
brew update
brew upgrade hn05/tap/shoal
```
Update a downloaded binary with `shoal update`. Managed daemons apply the upgrade once their operations are idle. For
a main-channel install, use `brew upgrade --fetch-HEAD hn05/tap/shoal`; run
`shoal daemon restart` to apply an upgrade immediately; running agents and
commands reattach to the new daemon. Wrappers from releases before reattachment
are stopped instead; restore them with `shoal resume --all`. The next `shoal`
command updates installed skills; reload `source <(shoal shell init)` in open
terminals.
## Development
See [docs/design.md](docs/design.md) for decisions and the [command reference](docs/reference.md) for behavior.
Create releases through **Actions → release** ([setup](docs/releases.md)).
Requires Rust at least as new as `package.rust-version` in [Cargo.toml](Cargo.toml)
and the [runtime dependencies](#runtime-dependencies).
Integration tests also use Bash, Zsh, and Python 3. Debug builds leave
user-level files outside their state directory, such as installed skills, to the
installed Shoal; tests in a temporary `HOME` set `SHOAL_DEV_USER_FILES=1` to
change them.
```sh
cargo build
cargo test
cargo clippy --all-targets -- -D warnings
cargo fmt --check
```