Durable process substrate with a TUI for managing long-lived jobs, scopes, and schedules.
⚠️ Pre-1.0 — core JOB / CRON flows,.cuescripts, real:fgPTY attach, client target resolution, and the official command set are implemented. Public contracts may still change before 1.0. Agent runtime concerns live above cue-shell.
cue-shell (cue) is a terminal-native runtime for durable async processes. It is not a traditional shell — it's a structured environment where jobs, scopes, chains, and crons are first-class primitives.
- Three-layer architecture: Process substrate (
cueddaemon) → Core model → Frontends (TUI/MCP/API) - Primary interaction modes: JOB ⚡ · CRON ⏰ — switch with
Shift+Tab :prefix commands: Vim-style builtin access (:run,:kill,:jobs,:cron, ...).cuefile scripts:cue run <file.cue>submits oneR<n>script with fail-fast execution- Foreground PTY attach:
:fg J<n>proxies a real terminal session with input, paste, and resize support - Display tabs with clean semantics:
:out J<n>snapshots stdout,:tail J<n>follows live stdout,:err J<n>opens stderr - Scope persistence: Environment snapshots with delta storage and lifecycle management
- Chain syntax:
->serial ·~>ignore-failure ·|||parallel ·|?|any-success;&&/||stay inside one job - Daemon durability: job history, cron definitions, and per-session cursor reconnects
┌─────────────────────────────────────────┐
│ L3 Frontend: TUI / MCP / REST API │
├─────────────────────────────────────────┤
│ L2 Core model (cue-core) │
│ Job · Scope · Chain · Cron │
├─────────────────────────────────────────┤
│ L1 Process substrate (cued daemon) │
│ Unix socket · SQLite · Process mgmt │
└─────────────────────────────────────────┘
crates/
├── cue-core/ — Core types and logic: Job, Scope, Chain, Cron
├── cue-client/ — Client connection stack and `cue-client` CLI for target/run commands
├── cue-daemon/ — Background daemon library plus `cue-daemon` / `cued` CLIs
├── cue-tui/ — Interactive TUI frontend plus `cue-tui` CLI
├── cue-cli/ — `cue` aggregator entrypoint for explicit namespaces and extensions
# Install the official command set from PyPI:
# `cue`, `cue-client`, `cue-tui`, `cue-daemon`, and `cued`.
uv tool install cue-shell# Prerequisites: Rust 1.95+, just
# Build
cargo build
# Start daemon in foreground
cued -f
# Show the top-level aggregator help
cargo run -p cue-cli --
# Start TUI (auto-connect / auto-reconnect)
cargo run -p cue-tui --bin cue-tui
# or through the aggregator
cargo run -p cue-cli -- tui
# Restart the daemon directly
cargo run -p cue-daemon --bin cue-daemon -- restart
# `cued` remains as a daemon alias
cargo run -p cue-daemon --bin cued -- restart
# Restart from inside the TUI
:restart
# Run checks
just check
# Run tests
just test
# Full CI locally
just ci
# Install pre-commit hooks
just pre-commit-installSee docs/design/README.md for the design index:
- Design overview — Three-layer architecture, crates, primitives, IPC summary
- conceptual-model.md — Jobs/scopes indexing, sequential composition, atomic tool surface
- commands-and-modes.md — Command reference, mode system,
:cronsyntax - cue-script.md —
.cuefile script contract forcue run <file.cue>
cue-shell uses split config files in the platform config dir:
client.toml— client-side transport/profile selection used bycue-client,cue-tui, andcue client ...daemon.toml— daemon-side runtime defaults used bycue-daemon/cued
Scripts are .cue files executed through the client CLI, with a top-level shortcut retained by the aggregator:
cue-client run examples/hello.cue
cue client run examples/hello.cue
cue run examples/hello.cue
A script run gets an id such as R12. Top-level items run in file order with
fail-fast semantics: if one item exits non-zero, later items are not submitted
and cue run exits with that code. Bare non-: items default to :run, while
explicit : commands remain available for builtins and :run(...) mode params.
Output still belongs canonically to jobs, while the daemon delivers script job
output and terminal script status directly to the cue run process so
stdout/stderr and the final exit code do not depend on event-channel
subscriptions.
Interactive JOB multiline input is not a script entry point; put multi-item
workflows in a .cue file. See docs/design/cue-script.md
and examples/.
cue-client and cue-tui default to a local Unix socket profile, so local users do not need any config for the current flow. The top-level cue command is an explicit aggregator: bare cue prints help, cue client ... forwards to cue-client ..., cue tui ... forwards to cue-tui, and cue daemon ... forwards to cue-daemon. Target/profile commands are intentionally namespaced under the client surface; use cue-client target ... or cue client target ... rather than cue target ....
To make the split explicit:
[transport]
default_profile = "local"
[transport.profiles.local]
transport = "unix"
# socket = "/custom/path/to/cued.sock"
[transport.profiles.remote-dev]
transport = "ssh"
destination = "user@example.com"
gateway_command = "cued gateway --stdio"
start_command = "cued start"The local profile name is reserved for Unix socket transport; use another
profile name for SSH targets.
Phase 1 uses the system OpenSSH client and runs the configured gateway command
over SSH, so the client speaks the same IPC through cued gateway --stdio.
Remote daemon startup still stays explicit: cue will not run
start_command for you. cue-client owns client-side transport parsing and
resolution, so CLI/TUI frontends share the same client.toml behavior.
When auto_detect_ssh is enabled, cue-shell adds implicit SSH profiles from
~/.ssh/config. Additional cluster inventory can be configured generically
without hardcoding scheduler-specific names in cue-shell:
[transport.discovery]
# Values are host lists separated by comma, semicolon, or whitespace.
env_hosts = ["CLUSTER_HOSTS"]
# Values are endpoint lists; cue-shell extracts hosts from host:port or URI values.
env_endpoints = ["CLUSTER_ENDPOINTS"]
# Values point to files containing one host per line or host plus extra columns.
env_hostfiles = ["CLUSTER_HOSTFILE"]
# Values use bracket range syntax such as gpu-[01-03,08].
env_bracket_ranges = ["CLUSTER_NODELIST"]Site-specific schedulers should be wired through this config or through external
cue-* extensions rather than hardcoded in the cue-shell core. Other frontends,
including Pi cue integrations, should use the cue-client resolver if they need to honor client.toml:
cue-client target resolve --json
cue client target resolve --json
cue-client target list --json
Direct daemon/socket integrations only see the server side and will not apply client profile selection by themselves.
cue can also dispatch external CLI extensions from client.toml after checking aggregator namespaces and direct shortcuts:
[extensions.commands.foo]
program = "cue-foo"
description = "Foo extension"program is the executable path/name; extension arguments come from the cue foo ...
invocation. Then cue foo arg runs cue-foo arg. Aggregator namespaces and shortcuts such as client, tui, daemon, run, target, help, and version take precedence, and extension names must be kebab-case without colliding with built-in or first-party subcommands. Optional PATH lookup for unknown cue-<name> binaries can be enabled explicitly:
[extensions]
path_lookup = truedaemon.toml can block unsafe commands or command arguments and attach remediation hints.
Block rules run before advisory warnings, so a command can both warn generally
and fail fast for specific arguments.
[block.commands]
sh = "Avoid shell wrappers. Use cue-shell direct-exec, cwd=..., or cue operators."
[block.commands.git]
"--no-verify" = "Run the commit normally; if hooks fail, inspect and fix the hook/check."
[block.commands.npm]
"--force" = "Use the lockfile and normal install path."
[warn.commands]
cd = "Prefer cwd=... over cd in command strings."Matching is literal, not glob or regex based:
[block.commands] sh = "..."blocks a command whoseargv[0]basename is exactlysh. It matchesshand/bin/sh; it does not matchzsh,/bin/zsh, orshellcheck.- Each
[block.commands.<name>]entry maps one blocked argument pattern to its remediation hint. The command name is also matched by exactargv[0]basename. - Argument patterns are checked against each argv token independently, not against the joined command line.
"--no-verify"matches an argument token--no-verify; it also matches--no-verify=...via the--flag=valueconvention. [warn.commands]maps an exact command basename to an advisory hint.
daemon.toml can cap persisted job/script history:
[retention]
max_job_history = 200
max_script_runs = 100It can also enable a runtime wrapper such as rtk. Wrapping is allowlist-only:
commands not listed under [wrapper.allowlist] are never wrapped, and an empty
allowlist wraps nothing.
[wrapper]
enabled = true
binary = "rtk"
[wrapper.allowlist]
commands = ["cargo", "git", "pnpm", "node"]:wrap on/off/status overrides only the session-level enablement. Per-command
mode params such as :run(wrapper=false) cargo test and cron params such as
:cron(wrapper=true) every 5m cargo test override enablement for that
invocation, but still must match the allowlist.
daemon.toml configures the overlay workspace sandbox used by
:run(sandbox=overlay):
[sandbox]
# Root under which each sandboxed job gets its own <root>/<job-id>/{upper,work}.
# Defaults to shared memory so writes stay off disk; point it at a disk-backed
# path if /dev/shm is too small for your builds.
default_upper_root = "/dev/shm/cue-shell-sandbox"
# Refuse to start a sandbox when the upper-root filesystem has less than this
# fraction free (0.0 disables the guard). Protects /dev/shm from runaway writes.
min_free_ratio = 0.1Each sandboxed job gets an independent per-job upper/work pair, so concurrent
jobs never share an overlay layer, and the writes are discarded when the job
finishes. An explicit :run(sandbox=overlay, sandbox.upper=<dir>) treats <dir>
as an upper root (the daemon still appends /<job-id>/{upper,work}), while
sandbox.upper=tmpfs forces a fresh in-memory upper for that job. Overlay
sandboxing is a workspace view, not a security boundary — it does not isolate
absolute paths outside the working tree, network access, process credentials, or
environment variables. See docs/design/sandbox-threat-model.md for the full
trust model.
Typical remote flow:
# Step 1: start the remote daemon explicitly
ssh user@example.com "cued start"
# Step 2: connect with the SSH transport profile
cueIf the remote daemon is not running (or its socket is missing), cue starts the
TUI offline and keeps retrying the configured gateway. cue run <file> still
fails immediately because file-script execution needs a live daemon.
| Component | Status |
|---|---|
| Design docs | ✅ Active |
| Cargo workspace | ✅ Multi-crate workspace |
| CI/CD | ✅ Tests, package smokes, PyPI/GitHub release path |
| cue-core | ✅ Core types / IPC / parser in place |
| cue-client | ✅ Transport profiles / target JSON / script runner |
| cue-daemon | ✅ Durable jobs / crons / scopes / PTY attach |
| cue-tui | ✅ Interactive job+cron frontend / reconnect view |
| cue-cli | ✅ Aggregator / extension dispatch / PyPI command wrappers |
Licensed under the MIT License.