Metadata-Version: 2.4
Name: jaiver
Version: 0.2.0
Summary: Drive any TUI/terminal program over tmux, adapter-configured, with optional jai containment.
Keywords: tmux,cli,agent,automation,tui,orchestration
Author: Andrew Phillips
Author-email: Andrew Phillips <skeledrew@duck.com>
License-Expression: AGPL-3.0-only
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: MacOS
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Terminals
Requires-Dist: typer>=0.12
Requires-Dist: tomlkit>=0.13
Requires-Python: >=3.13
Project-URL: Homepage, https://gitlab.com/skeledrew/jaiver
Project-URL: Repository, https://gitlab.com/skeledrew/jaiver
Project-URL: Issues, https://gitlab.com/skeledrew/jaiver/-/issues
Project-URL: Changelog, https://gitlab.com/skeledrew/jaiver/-/blob/main/CHANGELOG.md
Description-Content-Type: text/markdown

# jaiver

**Status: working end-to-end, pre-1.0.** Core lifecycle, comms, adapter
hooks, and sleep/wake are implemented and tested (see [Status](#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](https://jai.scs.stanford.edu/) 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`](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](https://pi.dev/) 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](https://github.com/shellular-org).
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

```bash
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):

```bash
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):

```bash
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):

```bash
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 launch`ed or `designate`d. 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):

```bash
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:

```bash
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:

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

## Contributing

See [`CONTRIBUTING.md`](CONTRIBUTING.md) for dev setup, tests, and PR
guidelines. Released under [AGPLv3](LICENSE).

## Changelog

See [`CHANGELOG.md`](CHANGELOG.md) for release history. Day-to-day
development is tracked against the maintainer's own private tracker;
[GitLab issues](https://gitlab.com/skeledrew/jaiver/-/issues) is where to
file bugs/requests from outside and is checked regularly, it's just not
where the maintainer's own backlog lives.
