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.

Metadata

Release files for jaiver 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for jaiver 0.2.0
File Size Uploaded
jaiver-0.2.0.tar.gz 125.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for jaiver 0.2.0
File Interpreter ABI Platform
jaiver-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 260.4 kB

Release files / jaiver-0.2.0.tar.gz

Download URL jaiver-0.2.0.tar.gz
Size 125.0 kB
Tags Source
SHA-256 checksum
How to use checksums
29520653f21f9eb870ab6a421299303c683d421f99fe0e418daef4514f5daf05
BLAKE2b-256 checksum
How to use checksums
cf958da4a5538c7e813d4e205a73b1dea9473d71833328e83fa4ae91cf2e4db8
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release files / jaiver-0.2.0-py3-none-any.whl

Download URL jaiver-0.2.0-py3-none-any.whl
Size 135.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c0e3934a64dde96f3f80494f9f502d5188ebc00aac526d1ba0de58e4e3a2133e
BLAKE2b-256 checksum
How to use checksums
36bd106f4284423e857523512ff18c252bf9f0043012af1b4392e2f3e4cc6854
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page