Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,12 @@ Resolution order: `--config <path>` → `$RA_CONFIG` → `./ra.toml` →
| `webfetch_fetch` | Fetches one web page as Markdown through `webfetch-cli`, writes `.md/`, and returns bounded JSON. |
| `webfetch_crawl` | Crawls a bounded documentation subtree through `webfetch-cli`, mirrors `.md/`, and returns bounded JSON. |
| `openspec` | Drives the agent-own OpenSpec SDD loop through the `openspec` CLI as structured actions (`status`, `list`, `show`, `instructions`, `validate`, `init`, `update`, `new_change`, `archive`, `workflow_state`); non-interactive, with bounded JSON output. |
| `tmux_run` | Starts or reuses a Ra-owned tmux session/window and runs a command, blocking or non-blocking. |
| `tmux_send` | Sends literal input or tmux key names to a target pane. |
| `tmux_capture` | Captures visible pane content or scrollback from a target pane. |
| `tmux_kill` | Kills a Ra-owned tmux session/window/pane, or all `ra__*` sessions. |
| `tmux_listen` | Polls a pane until output changes or an optional substring/regex appears. |
| `tmux_wait` | Blocks until a tmux event, hook expression, program result, or sleep timeout resolves. |
| `graphify_ensure` / `graphify_impact` / `graphify_update` / `graphify_query` / `graphify_path` / `graphify_explain` | Added when `[graphify]` is enabled; maintains and uses Graphify as Ra's R2A project graph. |

Toggle the catalog via `[tools] builtin = […]`; an empty allow-list
Expand Down Expand Up @@ -247,6 +253,13 @@ interactive prompts), forces `--strict` validation and explicit
`error.kind:"missing_openspec"` with install guidance when the CLI is
absent. Ra consumes the OpenSpec convention; it does not reimplement the CLI.

`tmux_run`, `tmux_send`, `tmux_capture`, `tmux_kill`, `tmux_listen`, and
`tmux_wait` operate on Ra-owned tmux sessions named `ra__{session}`.
`tmux_listen` and `tmux_wait` share event expression semantics for
`event`, `pattern`, `regex`, and `hook`; `tmux_wait` always requires
`timeout_ms`. If `tmux` is missing, they return structured install
guidance instead of an opaque spawn error.

## Protocols & specs

Authoritative schemas live in [`spec/`](spec/) — see
Expand Down
130 changes: 130 additions & 0 deletions docs/prd-tmux-native-tool.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# PRD: Native Tmux Built-In Tools

## Overview / Problem Statement

Ra agents need a persistent terminal surface for long-running dev servers,
watchers, REPLs, and interactive CLIs. The existing `bash` tool is a one-shot
execution path: it runs a command, waits for exit, and returns combined
stdout/stderr. That makes it hard to start a process, inspect output later,
send input across turns, or clean up persistent terminal state.

## Goals & Success Metrics

- Agents can create and reuse named tmux sessions without colliding with user
tmux sessions.
- Agents can run a command in tmux in blocking or non-blocking mode.
- Agents can send input/key names to a running pane.
- Agents can capture visible pane content or scrollback ranges.
- Agents can terminate Ra-owned tmux sessions/windows/panes.
- Agents can wait for new pane output or a pattern without opening a daemonized
listener.
- Agents can actively block on tmux wait events, including program exit,
program output, pane output updates, hook expressions, and bounded sleep.
- Missing `tmux` returns structured install guidance instead of an opaque spawn
error.
- Focused tests cover parameter handling, missing-binary behavior, argv shape,
catalog registration, and a real tmux round trip when tmux is available.

## User Personas & Stories

- As an agent using a dev server, I want to start it once and inspect its output
later so that I do not block a turn while the process stays alive.
- As an agent using a REPL or interactive CLI, I want to send input to an
existing pane so that I can continue the same session across turns.
- As an operator, I want Ra-owned tmux sessions to be namespaced so that agent
tools cannot accidentally target my personal tmux sessions.
- As an operator, I want a cleanup tool so that agent-created terminal state can
be removed deliberately.
- As an agent coordinating a long-running terminal workflow, I want one bounded
wait primitive so that I can wait for completion, output, hooks, or a sleep
interval without guessing with unbounded polling.

## Functional Requirements

| Priority | Requirement |
| --- | --- |
| Must | Provide a `tmux_run` built-in tool that creates or reuses a named session/window and runs a command. |
| Must | Support `tmux_run.wait=false` for non-blocking persistent commands and return the target pane metadata. |
| Must | Support `tmux_run.wait=true` for blocking execution with captured pane output and command exit status. |
| Must | Provide a `tmux_send` built-in tool that sends literal input or tmux key names to a target pane. |
| Must | Provide a `tmux_capture` built-in tool that captures pane content with optional `start_line` / `end_line` bounds. |
| Must | Provide a `tmux_kill` built-in tool that terminates Ra-owned sessions, windows, panes, or all `ra__*` sessions. |
| Must | Provide a `tmux_listen` built-in tool that polls until a shared tmux event expression is observed. |
| Must | Provide a `tmux_wait` built-in tool that blocks until `output_update`, `output_match`, `program_exit`, `program_output`, `hook`, or `sleep` resolves or `timeout_ms` expires. |
| Must | Require `tmux_wait.timeout_ms` so active waits are always bounded. |
| Must | Keep `tmux_listen` and `tmux_wait` on the same event expression semantics for `event`, `pattern`, `regex`, and `hook`. |
| Must | Namespace logical session names as `ra__{session}`. |
| Must | Register all six tools in `default_builtins` and respect `[tools].builtin` allow-list filtering. |
| Must | Return structured JSON for tool output, tmux failures, truncation state, and missing-`tmux` guidance. |
| Must | Document JSON schemas and behavior in `spec/tools.md`, README, and sample config. |
| Should | Keep `tmux_listen` bounded by timeout and polling parameters rather than creating a lifecycle daemon. |
| Won't | Add a `[tmux]` config section in this change. |
| Won't | Replace `bash` for one-shot commands. |
| Won't | Inject tmux plugins or custom `tmux.conf` state. |

## Non-Functional Requirements

- Use `tokio::process::Command` and explicit argv arrays for tmux invocations.
- Avoid shell string composition for tmux argv; the user command is a shell
command only inside the tmux pane.
- Keep outputs bounded through `max_output_bytes` where pane content is
returned.
- Preserve existing built-in tool behavior and tool allow-list semantics.
- Keep tests deterministic by using fake tmux binaries where possible and
skipping or isolating real tmux integration behavior when tmux is absent.

## Design Considerations

Ra should treat tmux session state as local host state. The tools therefore run
tmux locally instead of routing through ACP `terminal/*` reverse calls. Logical
session names are validated and mapped to `ra__{session}` targets so all cleanup
and capture operations remain scoped to Ra-owned sessions.

`tmux_listen` is a bounded polling primitive, not a background stream.
`tmux_wait` is the active blocking companion. Both tools use the same event
expression model:

- `output_update`: the pane capture changes after the initial snapshot.
- `output_match`: the pane capture matches `pattern`.
- `program_exit`: a supplied command exits.
- `program_output`: a supplied command produces matching output, or any output
when no pattern is supplied.
- `hook`: the pane capture matches the same substring/regex expression with an
optional hook label.
- `sleep`: wait for `duration_ms` bounded by `timeout_ms`.

## Technical Considerations

Implementation lives in `src/tools/tmux.rs` and follows the existing built-in
`Tool` trait pattern. Registration and exports are in `src/tools/mod.rs` and
`src/lib.rs`; UI title/kind hints are in `src/session_runner.rs`.

OpenSpec source of truth is archived under
`openspec/changes/archive/2026-06-01-add-native-tmux-tools/`, and the promoted
capability spec is `openspec/specs/tmux-tools/spec.md`.

## Timeline & Milestones

| Milestone | Owner | Target |
| --- | --- | --- |
| PRD and GitHub issue scope update | Agent | Before implementation handoff completion |
| OpenSpec propose/apply/archive | Agent | Same change |
| Implementation and tests | Agent | Same PR |
| PR and other top model review | Agent / reviewer | After validation |

## Open Questions & Risks

- `tmux_run.wait=true` needs deterministic completion. Ra uses a wrapper script
and `tmux wait-for`; this intentionally respawns the target pane for blocking
runs. `tmux_wait` reuses this behavior for program waits.
- `tmux_listen` uses polling rather than tmux control mode. This keeps the tool
simple and bounded but is not a live event stream.
- Hook waits are expression-based labels over pane output, not tmux-native
`set-hook` integration.
- Persistent tmux sessions remain after non-blocking runs until an operator or
agent calls `tmux_kill`.

## Appendix

- GitHub issue: `https://github.com/trotsky1997/ra/issues/24`
- Pull request: `https://github.com/trotsky1997/ra/pull/26`
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-01
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Design

## Tool Model

The tmux tools live in `src/tools/tmux.rs` and implement the existing `Tool`
trait. Ra owns typed parameters, session/window target construction,
dependency detection, JSON envelopes, and output bounding. tmux remains the
terminal multiplexer and command runner.

All spawned `tmux` invocations use `tokio::process::Command` with explicit argv
arrays. The user-provided `tmux_run.command` is intentionally a shell command
executed inside tmux, because tmux panes model interactive shells rather than
direct process argv.

## Session Namespacing

Callers pass logical session names such as `dev`. Ra maps those to tmux session
names as `ra__dev`. To keep tmux target strings unambiguous, logical session,
window, and pane identifiers are non-empty and limited to ASCII
letters/digits/`_`/`-`/`.`. This avoids accidental targeting of operator-owned
sessions and keeps `session:window.pane` construction deterministic.

## Command Execution

`tmux_run` ensures the namespaced session/window exists before running a
command.

- `wait=false` creates a detached session/window with the command when the
target does not exist. When the target already exists, it sends the command
plus Enter to the pane, matching terminal interaction semantics.
- `wait=true` respawns the target pane with a temporary shell script that runs
the caller command and signals completion via `tmux wait-for -S <token>`.
Ra waits for the signal with the caller's timeout, captures the pane output,
and returns a bounded JSON envelope. The pane remains available for later
capture.

This design chooses deterministic blocking behavior for `wait=true` rather
than attempting to infer whether an existing prompt is idle.

## Capture, Listen, and Kill

`tmux_capture` maps directly to `tmux capture-pane -p -t <target>` with
optional `-S`/`-E` line bounds and a Ra-side `max_output_bytes` cap.

`tmux_listen` is a bounded polling helper. It takes an initial capture and then
polls until the pane output changes or an optional substring/regex pattern is
found. It returns the latest capture, a best-effort delta when available, and a
timeout flag instead of opening a daemonized stream.

`tmux_kill` only targets Ra-owned namespaced sessions. It can kill a named
session, window, pane, or all `ra__*` sessions.

## Error Handling

If `tmux` is missing, every tool returns JSON with `ok:false`,
`error.kind:"missing_tmux"`, and installation guidance. tmux command failures
also return structured JSON with exit status, stdout, and stderr so callers can
distinguish missing targets from process errors without parsing anyhow strings.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
## Why

Ra agents need a persistent terminal surface for long-running dev servers,
watchers, REPLs, and interactive CLIs. The existing `bash` tool is a
one-shot shell execution path, so agents cannot reliably start a process,
inspect later output, or send input across turns.

## What Changes

- Add native tmux-backed tools:
- `tmux_run` starts or reuses a namespaced tmux session/window and runs a
command in blocking or non-blocking mode.
- `tmux_send` sends input or tmux key names to a pane.
- `tmux_capture` captures visible pane content or scrollback ranges.
- `tmux_kill` terminates a Ra-owned session/window/pane.
- `tmux_listen` polls a pane until output changes or an optional pattern is
observed.
- Namespace user session names as `ra__{session}` to avoid collisions with
operator-owned tmux sessions.
- Return structured JSON envelopes for tmux command results and structured
install guidance when `tmux` is not on `PATH`.
- Register the tools in the default built-in catalog while respecting the
`[tools].builtin` allow-list.
- Document the tool schemas in `spec/tools.md`, README, and the sample config.

## Capabilities

### New Capabilities
- `tmux-tools`: Native tmux tool support for persistent terminal sessions.

### Modified Capabilities

None.

## Impact

- Adds `src/tools/tmux.rs` and new exports/registrations in `src/tools/mod.rs`
and `src/lib.rs`.
- Updates tool UI hints in `src/session_runner.rs`.
- Updates public tool documentation in `spec/tools.md`, README, and
`spec/ra.toml.example`.
- Adds unit and integration tests for parameter handling, missing binary
guidance, default catalog registration, argv-safe tmux invocation, and a
tmux round-trip when `tmux` is installed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Tmux Tools Delta

## ADDED Requirements

### Requirement: Native Tmux Tool Catalog

Ra SHALL include `tmux_run`, `tmux_send`, `tmux_capture`, `tmux_kill`, and
`tmux_listen` in the default built-in catalog when `[tools].builtin` is empty.

#### Scenario: Empty allow-list exposes tmux tools

- **GIVEN** `[tools].builtin` is empty
- **WHEN** Ra builds the default built-in tool catalog
- **THEN** the catalog includes `tmux_run`, `tmux_send`, `tmux_capture`,
`tmux_kill`, and `tmux_listen`

#### Scenario: Non-empty allow-list remains exact

- **GIVEN** `[tools].builtin` contains only `tmux_capture`
- **WHEN** Ra builds the default built-in tool catalog
- **THEN** the catalog contains `tmux_capture` and omits the other tmux tools

### Requirement: Namespaced Tmux Sessions

Ra SHALL map logical tmux session names to Ra-owned tmux sessions using the
`ra__{session}` namespace.

#### Scenario: User session name is namespaced

- **GIVEN** a caller uses session `dev`
- **WHEN** any tmux tool builds a tmux target
- **THEN** the target session name is `ra__dev`

### Requirement: Tmux Run Tool

Ra SHALL provide a `tmux_run` tool that creates or reuses a named tmux
session/window and runs a command.

#### Scenario: Non-blocking run returns target

- **GIVEN** the target session/window does not exist
- **WHEN** `tmux_run` is called with `wait:false`
- **THEN** Ra creates the detached target and returns JSON containing the pane
target without waiting for the process to exit

#### Scenario: Blocking run returns captured output

- **GIVEN** `tmux_run` is called with `wait:true`
- **WHEN** the command exits before the timeout
- **THEN** Ra returns JSON containing `ok:true`, the exit/capture status, and
the captured pane output

### Requirement: Tmux Send Tool

Ra SHALL provide a `tmux_send` tool that sends input or tmux key names to a
target pane.

#### Scenario: Send input with Enter

- **GIVEN** a running tmux pane
- **WHEN** `tmux_send` is called with `keys:"q"` and `enter:true`
- **THEN** Ra invokes `tmux send-keys` for the target and appends Enter

### Requirement: Tmux Capture Tool

Ra SHALL provide a `tmux_capture` tool that captures target pane content with
optional scrollback line bounds.

#### Scenario: Capture recent history

- **GIVEN** a target pane has output in scrollback
- **WHEN** `tmux_capture` is called with `start_line:-50`
- **THEN** Ra returns a bounded JSON envelope containing the captured text

### Requirement: Tmux Kill Tool

Ra SHALL provide a `tmux_kill` tool that terminates Ra-owned tmux targets.

#### Scenario: Kill a session

- **GIVEN** a namespaced tmux session exists
- **WHEN** `tmux_kill` is called for its logical session name
- **THEN** Ra kills the corresponding `ra__*` tmux session

### Requirement: Tmux Listen Tool

Ra SHALL provide a `tmux_listen` tool that polls a pane until output changes or
an optional pattern is observed.

#### Scenario: Listen sees new output

- **GIVEN** a pane later emits new output
- **WHEN** `tmux_listen` is called without a pattern
- **THEN** Ra returns after the capture changes and includes the latest content

#### Scenario: Listen pattern timeout

- **GIVEN** a pane does not emit the requested pattern
- **WHEN** `tmux_listen` reaches its timeout
- **THEN** Ra returns JSON with `timed_out:true` instead of blocking
indefinitely

### Requirement: Missing Tmux Guidance

Ra SHALL return a structured, actionable response when `tmux` is not available
instead of surfacing an opaque spawn failure.

#### Scenario: tmux is missing

- **GIVEN** `tmux` is not found on `PATH`
- **WHEN** any tmux tool executes
- **THEN** the tool returns JSON with `ok:false`, `error.kind:"missing_tmux"`,
and installation guidance
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Tasks

- [x] Add tmux tool implementations with argv-safe tmux process execution.
- [x] Register `tmux_run`, `tmux_send`, `tmux_capture`, `tmux_kill`, and `tmux_listen` in the default built-in catalog.
- [x] Update README, `spec/tools.md`, and init/config examples.
- [x] Add unit/integration tests for registration, params/target handling, missing-tmux structured errors, argv mapping, and tmux round-trip behavior.
- [x] Run focused and full verification.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-01
Loading
Loading