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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
29520653f21f9eb870ab6a421299303c683d421f99fe0e418daef4514f5daf05
|
|
| MD5 |
fe8994b634b54fbefbb1f3a2368c808c
|
|
| BLAKE2b-256 |
cf958da4a5538c7e813d4e205a73b1dea9473d71833328e83fa4ae91cf2e4db8
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c0e3934a64dde96f3f80494f9f502d5188ebc00aac526d1ba0de58e4e3a2133e
|
|
| MD5 |
f71d1422676c6b1aacf4ca490a0ea8d5
|
|
| BLAKE2b-256 |
36bd106f4284423e857523512ff18c252bf9f0043012af1b4392e2f3e4cc6854
|