Skip to main content

jaiver

Status: working end-to-end, pre-1.0. Core lifecycle, comms, adapter hooks, and sleep/wake are implemented and tested (see Status below); the config/CLI surface may still shift before a 1.0.

Drive any TUI/terminal program over tmux — so an orchestrating agent can delegate work to it like a subagent and a human can attach to the same live instance to co-steer it. No cooperation from the target program is required: jaiver types into the pane (send-keys) and reads the screen (capture-pane), inferring idle/busy/error state from adapter-supplied regex patterns (or a state_hook script, for programs a regex can't describe — the hook gets JAIVER_HANDLE set in its environment, so it can look up per-instance context instead of relying only on pane text). Optional jai containment isolates an adapter's filesystem writes when configured.

jaiver runs everything on its own dedicated tmux socket (tmux -L jaiver ...), separate from any tmux you use day-to-day — that's what keeps launch/attach/stop/peek finding the right session regardless of what tmux you happen to be inside.

See docs/PRD.md for the full design.

Motivation

Disclosure: This section of the README file is the only thing in this entire project that was human-written, and it will very likely remain that way.

This project began as pi-delegate, a way to run Pi as a subagent within Claude Code and maintain visibility into what was going on, as well as steerability in case the model needed further guidance. Then it evolved, as I also dislike how little visibility there is when Claude Code is running subagents of itself, and as a way to give agents control of a shell with SSH or general sudo access.

Today its use case has expanded to being the ACP-compatible agent interaction layer for my personal fork of the Shellular project. Instead of being restricted to EITHER controlling an agent instance from my mobile OR controlling it from desktop, doing BOTH at once is a trivial [t]ask, similar to what the Claude mobile app's remote control offers (but with a whole lot more value-added features, and any agent that works in the terminal can be trivially supported).

Status

Working end-to-end: lifecycle (launch/grant/stop/sleep/wake/list/gc/ designate/attach), comms (send/steer/peek/await/handoff), adapter hooks (state_hook/resume_hook/before_run_hook/after_run_hook/ after_exit_hook, --extras-json), and sleep/wake are all implemented and tested per docs/PRD.md (§16/§17 cover the two most recently added feature sets). CONTEXT.md is the glossary reference for the hooks and sleep/wake domain language.

Develop

uv sync
uv run pytest
uv run jaiver --help

Install the CLI on $PATH

From PyPI (recommended for actually using jaiver, as opposed to hacking on it):

uv tool install jaiver     # or: pipx install jaiver / pip install jaiver
jaiver setup                # scaffolds config, symlinks the skill, checks prereqs

From a checkout (for development, or to track dev ahead of a release):

git clone https://gitlab.com/skeledrew/jaiver
cd jaiver
uv tool install .
jaiver setup

Either way, jaiver setup scaffolds config.toml and symlinks the skill from files bundled inside the installed package itself — this works identically whichever install method you used, no checkout required at runtime.

Layout

src/jaiver/         cli.py · config.py · comms.py · launcher.py · registry.py
                    · paths.py · lifecycle.py · hooks.py · sleeptimer.py
                    · metadata.py · containment.py · jail_export.py
                    · workdir_check.py · setup.py · adoption.py · procfiles.py
                    · recording.py · selection.py · skill/SKILL.md -- the
                    Claude Code skill `jaiver setup` symlinks into place,
                    bundled into the package itself (not the repo root)
                    so setup works from a real install, not just a checkout
tests/              pytest
config.example.toml
LICENSE             AGPLv3
CONTRIBUTING.md
CHANGELOG.md
.gitlab-ci.yml      lint/test/coverage on every push; PyPI publish on a
                    tagged push to main (see CONTRIBUTING.md's Versioning
                    and releases section)
CONTEXT.md          domain glossary (hooks, envelope, extras-json, sleep/wake)
AGENTS.md           deep-dive reference for an agent working in this repo
docs/PRD.md
docs/adr/           architecture decision records
contrib/adapters/   optional per-agent adapter kits (not installed by pip —
                    see "Using a contrib adapter kit" below)

Using a contrib adapter kit

contrib/adapters/* (worked examples: a full Claude Code kit, a Pi cooperating-extension kit, ACP bridge/agent-server scripts — see jaiver docs for the list) is not bundled into the installed package — jaiver's own core stays adapter-agnostic on purpose (nothing in src/jaiver/ knows any specific agent's name or quirks; see CONTEXT.md's "Hooks" section for the generic, pluggable contract every adapter kit implements against). jaiver docs <adapter-name> works from any install and shows that adapter's README even without its scripts on disk — useful for deciding whether you want it before fetching anything.

To actually use one, clone the matching tagged version of the repo alongside your PyPI-installed jaiver (adapter kits are versioned together with core, so mismatched versions can drift):

git clone --branch v<jaiver-version> --depth 1 https://gitlab.com/skeledrew/jaiver /path/to/jaiver-adapters

then point your config.toml's hook fields (and any install_artifacts source) at /path/to/jaiver-adapters/contrib/adapters/<name>/... — see config.example.toml's [adapters.pi] entry for a fully-worked example, and each adapter kit's own README for what it needs.

Command surface

  • lifecycle: setup · doctor · launch · grant · stop · sleep · wake · list · describe · export · gc · designate · attach
  • comms: send · steer · peek · await · handoff · call
  • docs: docs [topic]
  • utility: file-open-by <path>

file-open-by <path> reports whether any process currently has path open — agent- and adapter-agnostic (unlike call's capabilities, this isn't dispatched through any adapter's call_hook, since the answer is identical no matter which adapter, if any, is involved), and independent of jaiver's own registry, so it also catches a bare/foreign process never jaiver launched or designated. Linux scans /proc/<pid>/fd directly; macOS falls back to lsof (no /proc there). Prints one JSON object, {"path": <str>, "open": <bool>, "processes": [{"pid", "comm", "cmdline"}, ...]} — process info, not bare pids, so a caller can tell "another agent process" apart from e.g. less/tail -f without a second pid→process lookup of its own — always exiting 0 (a successful check either way, not an error state).

docs (no args) lists jaiver's shipped doc topics (readme, context, plus one per contrib adapter kit with a README); docs <topic> pages the corresponding file through $PAGER (falling back to less, then more). Works the same whether jaiver was installed from a git checkout or via pip/uv tool install — no browser or knowledge of where the repo lives required. The shipped files are the same markdown already maintained in the repo (symlinked in a checkout, a real build-time copy in an installed wheel), so there's no second copy to drift out of sync.

export <handle> [path...] shows (no paths) or copies out (with paths) a containment-using handle's jailed/diverged files versus the host, from the per-project jai overlay (docs/PRD.md §4). Called with no paths, it's a dry-run: every diverged file tagged new / differs / would-skip: host newer, no filesystem changes. Called with paths, those files are copied out (attributes preserved) — except any whose host destination has a newer mtime than the jailed copy, which are skipped and reported rather than overwritten. After a successful copy, the jailed copy is deleted only if no currently-live instance still shares that jail (editing/deleting a jail's overlay while another process has it mounted is unsafe per jai's own docs) — otherwise it's left in place and reported as still in use.

describe <handle> prints everything jaiver knows about a handle — full instance state (status, controller, granted dirs, timestamps, etc.) plus the resolved adapter's hook wiring and containment config — the one place to check without cross-referencing config.toml by hand. list's per-instance row draws from the same underlying assembly (metadata.py), so the two commands never silently disagree about what's true for a handle.

call invokes a structured, adapter-defined capability (e.g. a model switch) through the instance's call_hook, streaming its JSONL response as it arrives — for requests that don't fit send's "type this text" shape. Only meaningful for adapters that configure one; there's no raw- keystroke fallback. See CONTEXT.md's "Hooks" section.

call_hook is also how ACP (Agent Client Protocol) support is wired up — no separate config field. jaiver call <handle> '{"capability":"acp"}' execs straight into the same call_hook script with --acp appended to argv, turning jaiver into a real ACP agent for that one handle over the caller's own stdio. Any adapter's call_hook that already dispatches its normal capabilities (see contrib/adapters/pi/pi_call_hook.py or contrib/adapters/claude-code-full/claude_code_call_hook.py) can support this by also handling --acp. See CONTEXT.md's "ACP" section.

peek displays a peek_hook's output (a cooperating adapter's own authoritative "what's currently showing", e.g. straight from a sidecar's on-disk transcript) in place of the raw capture-pane scrape, when one's configured — closes a real race where a TUI's own rendering can lag behind state a cooperating adapter's own transcript already reflects as done. On a peek_hook failure, peek reports it and falls back to the raw pane capture rather than showing nothing. peek --raw forces the raw pane view even when a peek_hook is configured and would otherwise succeed — a no-op when no peek_hook is configured. See CONTEXT.md's "Hooks" section.

sleep/wake (and a per-instance sleep_after_minutes timer) let you kill an idle instance for resource management without losing it — a later send/peek/steer/await/attach/grant on a sleeping handle wakes it back up automatically. See docs/PRD.md §17 / CONTEXT.md's "Sleep / wake" section.

launch warns (non-fatally) when the resolved workdir has no recognizable project marker (.git, pyproject.toml, package.json, etc.) and very few files — a lightweight signal to catch a workdir disconnected from the repo a task actually needs (e.g. an umbrella/docs-only checkout with the real source in a sibling repo) before it causes a long silent stall. It's a heuristic, not validation, so set check_workdir = false on an adapter (e.g. shell, ssh) whose workdir isn't meaningfully "a project" to begin with.

Per-invocation spawn args

launch/grant --extras-json '{"args": [...]}' appends tokens to the adapter's extra_args for just that one spawn/respawn — for a flag that has to vary per instance (e.g. pi --session-id <id>) and so can't live in a static [adapters.*] extra_args list. See CONTEXT.md's "extras-json" section for the full envelope contract.

Bring your own session (e.g. SSH)

Two ways to get an SSH session under jaiver's control:

A. Let jaiver launch it, with an adapter that knows the remote prompt (see the [adapters.ssh] example in config.example.toml — copy it into your config.toml and adjust command/idle_pattern for your host):

jaiver launch my-box --adapter ssh
jaiver send my-box "uptime"      # drive it like any other adapter
jaiver handoff my-box human      # hand the keyboard to yourself
jaiver attach my-box             # tmux attach; type directly
# ... work interactively ...
jaiver handoff my-box agent      # detach (Ctrl-b d), hand back to the agent

B. Start and connect yourself first, hand it to jaiver after. This is the "I already ssh'd in, now let the agent take over" flow. Because jaiver only looks at sessions on its own dedicated tmux socket, the session has to be created on that socket for designate to find it — a plain tmux new or a bare ssh in your regular terminal is invisible to jaiver:

tmux -L jaiver new -s mybox      # session on jaiver's socket
ssh user@mybox                    # connect however you like, inside it
# Ctrl-b d to detach once connected (session keeps running)

jaiver designate mybox            # jaiver adopts it as the active instance
jaiver handoff mybox agent        # let the agent start driving it
jaiver send mybox "ls /var/log"
jaiver handoff mybox human        # take the keyboard back any time
jaiver attach mybox               # (or: tmux -L jaiver attach -t mybox)

A designated session with no matching [adapters.*] entry falls back to a generic bash-style idle_pattern (\$\s*$), which is good enough for a lot of shells/remote prompts but not guaranteed — configure a real adapter (path A) if you need reliable idle/busy detection.

If the session is actually running a program with its own [adapters.*] entry (e.g. you spawned pi --session-id <id> yourself, to pass a per- instance flag launch's static extra_args can't express — see "Per- invocation spawn args" below), pass --adapter so designate wires up that adapter's state_hook/send_hook/submit_key etc. instead of falling back to raw, adapter-unaware keystroke injection:

tmux -L jaiver new -s mybox -- pi --session-id abc123
jaiver designate mybox --adapter pi

Contributing

See CONTRIBUTING.md for dev setup, tests, and PR guidelines. Released under AGPLv3.

Changelog

See CHANGELOG.md for release history. Day-to-day development is tracked against the maintainer's own private tracker; GitLab issues is where to file bugs/requests from outside and is checked regularly, it's just not where the maintainer's own backlog lives.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

jaiver-0.2.0.tar.gz (125.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

jaiver-0.2.0-py3-none-any.whl (135.4 kB view details)

Uploaded Python 3

File details

Details for the file jaiver-0.2.0.tar.gz.

File metadata

  • Download URL: jaiver-0.2.0.tar.gz
  • Upload date:
  • Size: 125.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for jaiver-0.2.0.tar.gz
Algorithm Hash digest
SHA256 29520653f21f9eb870ab6a421299303c683d421f99fe0e418daef4514f5daf05
MD5 fe8994b634b54fbefbb1f3a2368c808c
BLAKE2b-256 cf958da4a5538c7e813d4e205a73b1dea9473d71833328e83fa4ae91cf2e4db8

See more details on using hashes here.

File details

Details for the file jaiver-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: jaiver-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 135.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for jaiver-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c0e3934a64dde96f3f80494f9f502d5188ebc00aac526d1ba0de58e4e3a2133e
MD5 f71d1422676c6b1aacf4ca490a0ea8d5
BLAKE2b-256 36bd106f4284423e857523512ff18c252bf9f0043012af1b4392e2f3e4cc6854

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page