Repository navigation
Installing jus-dispatch
jus-dispatch is the remote dispatch agent for Juscribe. It connects to the Juscribe server over a WebSocket, runs a coding-agent CLI against your project when a dispatch arrives, and reports the result back to the board.
Platform support: macOS and Linux, amd64 and arm64. Requires the CLI you dispatch to — Claude Code by default.
Install
brew install juscribe/tap/jusThat installs the jus CLI and jus-dispatch together, and brew upgrade jus moves both. Binaries for macOS and Linux, amd64 and arm64, are attached to every release at github.com/juscribe/jus-dispatch/releases and download without authentication.
A container image ships on the same cut, for the docker placement:
docker pull ghcr.io/juscribe/jus-dispatch:latestlinux/amd64 and linux/arm64. --sandbox docker mode that left with it; p188 restored the placement layer and the image came back with it.
To build from a checkout instead:
cd dispatch && make buildThat writes dispatch/bin/jus-dispatch. make build-all cross-compiles all four platforms into the same directory, and make install copies the host build into $GOPATH/bin.
What it does NOT do
The station created a git worktree per dispatch, ran the CLI inside it, and then merged, pushed or left the branch. jus-dispatch leaves the branch and stops there (#4223).
- It creates a worktree, runs the CLI in it, removes the worktree and keeps the branch. The board is told the branch name and how many commits are on it.
- No merge, no push, no pull request, on any path. A person reviews and lands it. A run that committed nothing gets its branch deleted and the board says so rather than naming an empty one.
⚠️ .jus/hooks/worktree-setupruns inside the new worktree first, if your project has one — a git checkout is missing everything you gitignore. A hook that fails or is not executable fails the dispatch rather than handing an agent a broken tree.branch_strategyno longer arrives on the wire at all (#3902), and nothing here reads it. There is one behaviour, so there is nothing to select.
raw / orbstack / docker and the setup-sandbox, setup-orb, setup-docker and auth commands were restored across #4222, #4221, #4220, #4243 and #4245. jus-dispatch init is what chooses between them — the Setup section below records the choice; it does not make it, and this sentence said otherwise until #4292.
What a dispatch is allowed to do
It runs under --permission-mode auto — it can edit files (#4254).
raw there is nothing. Under orbstack and docker only the project directory is mounted, so a write outside it is refused by the kernel; raw is your real machine with an autonomous agent on it. jus-dispatch init prints that difference and takes a typed agree before it will select raw.
dontAsk, then plan after #3970 found that a project's own .claude/settings.json outranked the narrower mode in a trusted checkout. That approach is retired: the boundary is the container now, not the model's own restraint.
One rule still binds under auto, and it is the project's own. permissions.deny in .claude/settings.json is honoured and recorded. Measured on claude 2.1.273: asked for a deny-listed file, the CLI reached for it through cat — not the Read tool — and the rule stopped it anyway. --setting-sources is deliberately never passed, because it would discard exactly that.
--dangerously-skip-permissions is never passed and must not be. An autonomous mode answers the permission system; that flag switches it off, and takes the deny rules with it.
Setup
jus-dispatch initPress Enter to accept a default (shown in brackets):
| Prompt | Default | What it means |
|---|---|---|
| API token | (none — required) | Your Juscribe API token, from Settings → API Tokens. It authenticates the agent with the server. |
| Server URL | wss://app.juscribe.ai/cable |
The WebSocket endpoint for your Juscribe instance. Accept the default unless you have a designated subdomain. |
| Project path | Current directory | Absolute path to the git repository the agent runs in. |
| Sandbox | (none — you are asked) | Where the CLI runs: raw, orbstack or docker. raw is your real machine and requires a typed agree. |
| CLI provider | claude |
Only shown when multi-LLM is enabled for your workspace. |
| Thinking effort | medium |
low, medium, high or max. Claude only. |
| Log level | info |
debug, info, warn or error. Use debug when troubleshooting a connection. |
| Log file | ~/.local/share/jus-dispatch/logs/jus-dispatch.log |
Where the structured log is written, in addition to stderr. |
Config is saved to ~/.config/jus-dispatch/config.toml, or to <project>/.jus/dispatch.toml when the directory has been through jus init.
~/.config/jus-station/ is not read, at any precedence. A frozen station and a new agent can sit on one machine, and sharing a config would have each writing over the other's scope. The station cannot connect either way — the server's protocol floor is past it — but the file is left alone rather than migrated. See Upgrading from jus-station.
Every value can be overridden at runtime by a flag (--token, --server, --project, --log-level, --log-file) or an environment variable (JUSCRIBE_API_TOKEN, JUS_SERVER_URL, JUS_PROJECT_PATH, JUS_LOG_LEVEL, JUS_LOG_FILE, JUS_EFFORT_LEVEL).
Your project's toolchain is yours to install
jus-dispatch initoffers to file this as a chore on your board, with the commands below already filled in for your sandbox — you can decline, and nothing is written to your tracker unless you say yes.
A sandbox gets git, the AI CLI and the jus toolset, and nothing else. It does not get your language runtime, your package manager or your database client — jus-dispatch has no way to know what those are.
Put an executable .jus/hooks/sandbox-setup in your repository. Both setup-orb and setup-docker run it, with your project as the working directory:
#!/usr/bin/env bash
set -euo pipefail
sudo apt-get update
sudo apt-get install -y nodejs npm postgresql-client- No hook is not an error. Most projects need no provisioning and none is invented for them.
- A hook that fails, or exists without an executable bit, fails setup — a sandbox missing the tools it was meant to install is one every dispatch dies in, somewhere that names neither the hook nor its permissions.
- It is not bounded by a timeout. A toolchain install can legitimately run for half an hour; the output streams to your terminal and
Ctrl-Cworks. .jus/hooks/orb-setupstill works. That was the name before the hook reached docker. Both are found, on both placements, andsandbox-setupwins if you have written both.
.jus/hooks/worktree-setup, and the two are not interchangeable. That one runs per dispatch, in a fresh worktree, and restores what you gitignore — typically starting with a dependency install. This one runs once per sandbox and installs the thing that dependency install invokes.
| Placement | What the hook becomes |
|---|---|
orbstack |
A provisioning step inside the orb, run once by setup-orb after git, the CLI and the jus toolset are installed. |
docker |
A build layer. setup-docker builds an image from ghcr.io/juscribe/jus-dispatch with your hook in it, tags it jus-dispatch-<project>-<hash>:latest and points [sandbox] target at that. Every container is docker run --rm, so nothing installed at start-up would survive the run that needed it. |
raw |
Nothing — it is your own machine, which already has your tools. The hook is not run. |
What the docker build gives your hook
- Your project as git sees it, bind-mounted at the path it has on your machine, as the working directory — so a hook that calls a script in the repository works the same on both placements. Writes to it are discarded rather than reaching your checkout.
⚠️ Your.dockerignoreis NOT applied, deliberately (#4533). It describes your production image, and the build context here is a different thing — one project excluded/.jus/from its image, correctly, and the build then died at./.jus/hooks/orb-setup: not foundon a file sitting on disk, 0755, after shipping 2.76 GB. The context is a tar built fromgit ls-files --cached --others --exclude-standardinstead: your tracked files plus the untracked ones git is not ignoring, plus the hook whatever git thinks of it. So.gitignoreis what bounds it — the same 2.76 GB project came to 37.5 MB — and a hook that needs a file git ignores will not find it.- Root, with
$HOMEset to/home/agent. The build runs as root because a build context is mapped touid 0, so a project directory that is not world-readable is unreadable to anybody else — and that failure arrives asPermission deniedbefore your hook's first line.sudois installed and granted toagent, so a hook written for an orb runs unchanged, and anything the hook leaves in that home is handed back toagentafterwards. - A rebuild when the hook changes, and docker's cache when it has not.
If you build your own image by hand instead
That still works — FROM ghcr.io/juscribe/jus-dispatch, add your toolchain, point [sandbox] target at it. Two traps, both measured on the published image (Debian 12, arm64), and both surface as the same opaque exit 127:
libatomic1is not in the base image, and Node 26 on arm64 needs it. Without itnodeinstalls cleanly and then dies on every invocation witherror while loading shared libraries: libatomic.so.1, which the shell reports as exit 127 — indistinguishable from node not being installed at all.⚠️ command -v nodesucceeds the whole time, so presence is not the check. Running it is.corepackis no longer in the Node tarball. The usualcorepack enable && corepack prepare pnpm@xexits 127 for the same reason.npm install -g pnpm@<pinned>is the working form.
worktree-setup can still be unable to commit, because git hooks run linters that the dependency install never invoked.
Usage
jus-dispatch startThe agent connects to your workspace and listens. When a dispatch arrives it:
- claims it, and runs only if the server grants the claim;
- cuts a git worktree of its own beside the project, and spawns the CLI in that — never in your checkout (#4223);
- streams heartbeats and status back to the workspace in real time;
- reports the result when the session ends.
Leave it running in a terminal tab, a tmux session or behind a process manager. Ctrl+C shuts it down gracefully — it holds the connection open until active dispatches finish, and a second Ctrl+C quits immediately.
A result nothing could deliver is spooled under ~/.local/share/jus-dispatch/spool and re-sent at the next start.
Other commands
| Command | Description |
|---|---|
jus-dispatch version |
Print agent and protocol versions |
jus-dispatch init |
Re-run the setup wizard (overwrites the existing config) |
jus-dispatch setup-sandbox |
Choose a placement and provision it — the one init calls |
jus-dispatch setup-orb |
Provision the OrbStack orb directly, when you already know you want one |
jus-dispatch setup-docker |
Provision the Docker image and the CLI auth volume directly |
jus-dispatch auth |
Sign the CLI in inside the sandbox — orb or container. Interactive, and the only command here that is |
Each is also reachable as jus dispatch <command> through the wrapper (#4291).
Running the agent itself inside a container
Everything above puts the CLI in a sandbox and leaves the agent on your
machine. The other arrangement — the agent in a container too — is what the
station's page documented, and it still works:
docker run --rm -it -e JUSCRIBE_API_TOKEN -e ANTHROPIC_API_KEY -v /path/to/your/project:/workspace ghcr.io/juscribe/jus-dispatch:latest start --sandbox raw --project /workspace--sandbox raw IS REQUIRED HERE, AND NOTHING SAID SO UNTIL #4292.
jus-dispatch start refuses when no placement is declared, and its message
tells you to run jus-dispatch init — which is the wrong advice inside a
container: there is no orb to reach and no docker socket, and the agent is
already contained. raw means "run the CLI here", and here is the container
you chose. The refusal cannot tell the two situations apart, so this line is
the record that it is expected.
For a persistent config instead of flags, mount one:
docker run --rm -it -e ANTHROPIC_API_KEY -v ~/.config/jus-dispatch:/home/agent/.config/jus-dispatch -v /path/to/your/project:/workspace ghcr.io/juscribe/jus-dispatch:latest start --sandbox raw
(#4285). The reaper's liveness test reads it as dead, so the content checks —
a dirty tree, or a branch with commits — are what protect a running dispatch's
worktree in this arrangement. They are unchanged and sufficient; it is the lock
that is not.
Verify
Check the Juscribe workspace — the header shows a green agent indicator. With one agent connected it is a dot; with several it becomes a count badge, and hovering names them. Dispatches route to your agent when the workspace dispatch mode is set to "agent".
Configuration file
[server]
url = "wss://app.juscribe.ai/cable"
token = "your-bot-api-token"
organization_id = "org-uuid"
workspace_id = "1"
[project]
path = "/path/to/your/project"
[sandbox]
mode = "orbstack"
target = "jus-agent"
[execution]
cli_type = "claude"
cli_path = ""
max_turns = 200
heartbeat_interval = "30s"
effort_level = "medium"
[logging]
level = "info"
file = "/Users/you/.local/share/jus-dispatch/logs/jus-dispatch.log"The file is written 0600 — it holds the API token.
cli_path is normally empty. The binary follows the provider: cli_type = "codex"
runs codex. Set cli_path only to point at a binary PATH will not find under that
name. It replaced claude_path, which is still read when the provider is claude (#4329).
[sandbox] is not optional in practice. With no mode in the file and no --sandbox flag, jus-dispatch start refuses rather than defaulting — an agent that quietly fell back to raw would be running an autonomous CLI on your machine because a config was incomplete. target is the orb name under orbstack and unused under raw; docker records the image and the auth volume here too, written by setup-docker.
Upgrading from jus-station
jus-station is retired and the server refuses it at connect. jus-dispatch is a
different binary rather than a rename, and nothing is migrated for you — four
things an old install left behind are in places nothing here reads. jus-dispatch start names whichever of them exist, at warn level, and changes none of them.
| What | Where jus-station put it | What to do |
|---|---|---|
| Agent config | ~/.config/jus-station/config.toml |
cp ~/.config/jus-station/config.toml ~/.config/jus-dispatch/config.toml, then re-read it — the [sandbox] block is new |
| Project config | <project>/.jus/station.toml |
cp .jus/station.toml .jus/dispatch.toml |
| Undelivered results | ~/.local/share/jus-station/spool |
Read them if you want to know what was lost. They cannot be replayed: the agent that produced them is refused at subscribe |
| Docker CLI login | volume jus-station-claude-config |
docker run --rm -v jus-station-claude-config:/from:ro -v jus-dispatch-claude-config:/to alpine sh -c 'cp -a /from/. /to/' |
CLI has to be signed in again, with nothing anywhere saying why the old login
stopped being used. That is jus-dispatch auth — since #4531 it drives docker
as well as the orb, so it is no longer a reason to re-run the whole of
setup-docker.
to. Both installs can sit on one machine; only this one connects.
Troubleshooting
"subscription rejected by server" — the protocol version is too old (brew upgrade jus) or the token does not belong to a bot user. Both are deterministic: the agent gives up rather than retrying.
"registered with NO workspace" — the agent is connected but invisible. Scope its API token to a workspace, or set organization_id / workspace_id under [server].
"registration rejected" — the workspace may have moved to an organization you are not a member of. Ask an owner to invite you, then set the ids above and restart.
"dispatch.toml holds a token that is NOT in use" — the injected JUSCRIBE_API_TOKEN overrides the file's. Clear the stored one so it cannot misdirect the next debugging session.
Dispatches fail in ~250 ms having burned no tokens — the CLI cannot authenticate. Run jus-dispatch auth, which signs in to whichever sandbox your config records; the log says so at error level with the CLI's own reason.