Skip to content

jus-dispatch v0.8.27

Choose a tag to compare

@caleon caleon released this 08 Oct 06:03
b30bc0c

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/jus

That 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:latest

linux/amd64 and linux/arm64. ⚠️ This page said there was no image, and that was true between #3912 and #4219. The station published one for a --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 build

That 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-setup runs 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_strategy no longer arrives on the wire at all (#3902), and nothing here reads it. There is one behaviour, so there is nothing to select.

⚠️ The sandbox modes ARE back, and this section said they were gone until #4254. 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).

⚠️ The placement is what makes that safe, and on 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.

⚠️ The history matters if you are reading an older note. Between #3900 and #4254 a dispatch was read-only, enforced by the agent's own flags — first 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.

⚠️ Project hooks still run — they are shell commands the harness executes, outside the permission system entirely.

Setup

jus-dispatch init

Press 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 init offers 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-C works.
  • .jus/hooks/orb-setup still works. That was the name before the hook reached docker. Both are found, on both placements, and sandbox-setup wins if you have written both.

⚠️ This is not .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 .dockerignore is 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 found on a file sitting on disk, 0755, after shipping 2.76 GB. The context is a tar built from git ls-files --cached --others --exclude-standard instead: your tracked files plus the untracked ones git is not ignoring, plus the hook whatever git thinks of it. So .gitignore is 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 $HOME set to /home/agent. The build runs as root because a build context is mapped to uid 0, so a project directory that is not world-readable is unreadable to anybody else — and that failure arrives as Permission denied before your hook's first line. sudo is installed and granted to agent, so a hook written for an orb runs unchanged, and anything the hook leaves in that home is handed back to agent afterwards.
  • 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:

  1. libatomic1 is not in the base image, and Node 26 on arm64 needs it. Without it node installs cleanly and then dies on every invocation with error while loading shared libraries: libatomic.so.1, which the shell reports as exit 127 — indistinguishable from node not being installed at all. ⚠️ command -v node succeeds the whole time, so presence is not the check. Running it is.
  2. corepack is no longer in the Node tarball. The usual corepack enable && corepack prepare pnpm@x exits 127 for the same reason. npm install -g pnpm@<pinned> is the working form.

⚠️ Size the image against what your COMMIT needs, not what your INSTALL needs. They are not the same set: a sandbox provisioned well enough for worktree-setup can still be unable to commit, because git hooks run linters that the dependency install never invoked.

Usage

jus-dispatch start

The agent connects to your workspace and listens. When a dispatch arrives it:

  1. claims it, and runs only if the server grants the claim;
  2. cuts a git worktree of its own beside the project, and spawns the CLI in that — never in your checkout (#4223);
  3. streams heartbeats and status back to the workspace in real time;
  4. 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

⚠️ The worktree lock records a pid that means nothing outside the container
(#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/'

⚠️ The Docker volume is the one that costs something to miss. Without it the
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.

⚠️ Copying the config does not make the station work again, and is not meant
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.