Translations: ุงูุนุฑุจูุฉ ยท Espaรฑol ยท Franรงais ยท เคนเคฟเคจเฅเคฆเฅ ยท ๆฅๆฌ่ช ยท ํ๊ตญ์ด ยท Portuguรชs (BR) ยท ะ ัััะบะธะน ยท ็ฎไฝไธญๆ
Each is a translation of this file, which is authoritative where they disagree.
A Ruby CLI tool and library designed from the ground up to be human & AI agent first-class.
rune serves as a universal pseudo-terminal (PTY) runner and structured data bridge for any CLI command or interactive TUI application.
Every command produces formatted, colored terminal output for humans and structured JSON for AI
agents. rune watch additionally writes a live NDJSON event stream while the human drives the
session. Same tool, same commands, dual interface.
rune session goes one step further: it holds an agent CLI โ claude, grok, codex โ open
across separate invocations, so one agent can drive another conversationally and a human can attach
to the same session and take over.
๐ New here? Start with the Getting Started guide.
- Dual Output (Human TTY / Agent JSON & NDJSON)
- Terminal mode: formatted colored output (
rune version) - Agent JSON mode:
--jsonor automatic pipe detection (rune version | cat) - Agent NDJSON mode:
--ndjsonfor a consistent result envelope (rune version --ndjson)
- Terminal mode: formatted colored output (
- Universal PTY Process Runner (
rune run)- Spawns any CLI tool or TUI inside a pseudo-terminal session
- Strips ANSI escape codes, cursor movements, and control sequences automatically
- Disables terminal pagers (
PAGER=cat) so queries return immediately without hanging - Measures process execution duration in milliseconds and detects interactive prompts
- Structured Auto-Parsers (
Rune::Parsers)TableParser: Parses space or pipe-delimited terminal tables into arrays of hashesKeyValueParser: Parses key-value output (key: val) into typed hashesTextSanitizer: Normalizes line endings and cleans ANSI escape codes
- Interactive Script DSL (
Rune::Script)- Step-by-step TUI script automation DSL for driving interactive terminal prompts and TUI menus
- Live Interactive Passthrough (
rune watch)- Puts your terminal in raw mode and forwards keystrokes to the child live, byte-for-byte
- Streams the child's output to your screen as it happens (unlike
rune run, which buffers and returns everything at the end) - Simultaneously logs every chunk as an NDJSON event to a temp file (path announced once, or
--log=PATH) so an AI agent can tail the session live while a human drives it
- Persistent Named Sessions (
rune session)- Holds a REPL-shaped child โ
claude,grok,codex, a shell โ open across separateruneinvocations, which neitherrun(buffers and returns once) norwatch(dies with its child) can do - Send-and-settle: write input, wait for the child to go quiet, get back exactly the output that send produced, turning an async TTY into a synchronous request/response call
--screenreturns the rendered terminal rather than the raw byte stream, which matters because a full-screen agent interleaves its answer with its own repaints โ one measured transcript went from 361KB of repaint traffic to a 1.1KB screenattachhands the live session to a human terminal and Ctrl-] gives it back, still running- Sessions are named, project-scoped, and archivable; transcripts are bounded on disk and in memory, so a session left running for a day does not grow without limit
- Holds a REPL-shaped child โ
The unqualified rune gem name is already taken on the public RubyGems.org registry by an
unrelated package, so gem install rune there installs the wrong thing. Install the maintained,
checksum-pinned formula from the CorvidLabs Homebrew tap:
brew install corvidlabs/tap/rune
rune version --jsonUpgrade later releases through the same channel:
brew upgrade corvidlabs/tap/runeFor source development:
git clone https://github.com/CorvidLabs/rune.git
cd rune
bundle install
ruby bin/rune versionrune --help # every command, plus the global flags
rune run --help # one command's usage and its own flags
rune help watch # same thing, spelled the other wayHelp is structured too, so an agent can discover the surface without scraping text:
rune run --help --json | jq '.data | {usage, flags}'{
"usage": "rune run [--timeout=SECONDS] [--max-output=BYTES] [--tail=N] [--separate-streams] [--] <command...>",
"flags": [
{
"flag": "--timeout=SECONDS",
"description": "Kill the wrapped command after N seconds (default 30). Before `--` only."
},
{
"flag": "--max-output=BYTES",
"description": "Bound clean_output/raw_output to BYTES each, keeping head+tail and marking the join with a `[rune] ==== N bytes omitted by --max-output ====` line. Mutually exclusive with --tail. Before `--` only."
},
{
"flag": "--tail=N",
"description": "Keep only the last N lines of clean_output/raw_output. Mutually exclusive with --max-output. Before `--` only."
},
{
"flag": "--separate-streams",
"description": "Adds clean_stdout/clean_stderr (stderr on a pipe, not the pty) alongside the merged view. Before `--` only."
}
]
}Use
--before the wrapped command. Every rune flag โ--json,--ndjson,--help,--timeout,--logโ is recognized only before the first--. That is what letsrune run -- gh pr list --json numberpass--jsontoghinstead of consuming it. Without the separator, rune takes the flag for itself and the wrapped command silently never sees it.
rune run --json -- git status{
"status": "ok",
"data": {
"command": "git status",
"exit_code": 0,
"clean_output": "On branch main\nnothing to commit, working tree clean\n",
"raw_output": "On branch main\r\nnothing to commit, working tree clean\r\n",
"prompt_detected": false,
"duration_ms": 21.05
}
}rune run --ndjson -- fledge lanes run check{"event":"result","status":"ok","data":{"command":"fledge lanes run check","exit_code":0,"clean_output":"...","duration_ms":1652.8}}rune run --ndjson emits that single envelope when the command finishes. Use rune watch for a
live stream of output events.
require 'rune'
text = <<~TABLE
NAME STATUS VERSION
fledge-plugin active 1.0.0
rust-cli ready 2.1.0
TABLE
parsed = Rune::Parsers::TableParser.parse(text)
# => [{ name: 'fledge-plugin', status: 'active', version: '1.0.0' }, ...]require 'rune'
# Harness an interactive TUI program with input keystrokes
runner = Rune::PTYRunner.new("fledge plugins search --interactive", input: "\x03")
result = runner.run
# => Result with exit_code 130, clean_output, duration_ms# Puts your terminal in raw mode, forwards your keystrokes live โ including
# raw escape sequences like arrow keys, not just whole lines โ and streams
# output to your screen as it happens. Logs an NDJSON event per chunk to a
# temp file (announced once, up front) so an agent can `tail -f` it live
# without any JSON noise landing in your own terminal. The demo's top-level
# menu is a real arrow-key selector (โ/โ + Enter, or q to quit).
rune watch -- ruby examples/humans/demo_tui.rb
# Or point the log somewhere specific:
rune watch --log=/tmp/session.ndjson -- ruby examples/humans/demo_tui.rbIn agent mode โ --json, --ndjson, or any time stdout isn't a terminal โ the live passthrough
moves to stderr so stdout carries nothing but the result envelope. The human keeps their live
view; the calling program gets clean JSON:
rune watch --json -- ruby examples/humans/demo_tui.rb 2>/dev/null | jq .data.log_pathrun buffers and returns once; watch needs a human at a terminal and ends with its child. Neither
can hold an agent REPL open across calls. session can:
# Start a named session. The child outlives this command.
rune session start --name reviewer -- grok
# Send a prompt and wait for the answer. --screen returns the rendered
# terminal, which is where the answer is actually legible.
rune session send --name reviewer --screen -- "Review lib/rune/session/supervisor.rb for races"
# Come back later โ from another process, another agent, another hour.
rune session send --name reviewer --screen -- "Now just the highest-severity one, in one line"
rune session list # what is running, how idle, what it last printed
rune session stop --name reviewerWhy --screen rather than the raw output. A full-screen agent repaints continuously, so the
byte stream contains every frame of every repaint with the answer split across them. Measured
against grok: a 361KB transcript rendered to a 1.1KB screen, and an answer the agent had plainly
displayed was absent from the byte stream in 3 of 3 turns and present in the rendered screen in 3 of
3. If you are matching on content, match on screen.
Take the wheel yourself, then give it back without stopping anything:
rune session attach --name reviewer # Ctrl-] detaches; the session keeps runningSessions are scoped to the enclosing git working tree, so reviewer in two checkouts is two
sessions. That is deliberate, and it is also the most common surprise โ if list shows nothing,
check the directory you are in and RUNE_HOME:
rune session list --all-projectsFinding one thing in a long transcript. A day's work with a driven agent reached 379KB, and
neither --since nor --tail helps when what you want is in the middle:
rune session read --name reviewer --grep 'THE BOARD' --context 2๐ Full guide, including settle tuning and the known limitations: docs/sessions.md.
rune integrates with the CorvidLabs trust toolchain:
- fledge โ Task runner & project lifecycle.
runeis a nativefledgeplugin defined viaplugin.toml. Install directly via:fledge plugins install CorvidLabs/rune fledge rune run --json -- git status
- spec-sync โ Contract enforcement (
specs/) - augur โ Change risk scoring
- ๐ Getting Started guide โ Output modes,
rune runusage, timeouts, and parsers with real command output. - ๐ Persistent sessions guide โ
rune session: named PTY sessions that outlive a single invocation, and send-and-settle for driving one agent CLI from another. - ๐ Pseudo-TTY (PTY) Architecture Guide โ How pseudo-terminals, non-blocking stream reading, ANSI sanitization, prompt detection, script execution, and
rune watch's live bidirectional passthrough work under the hood in Ruby. - ๐ Release guide โ Version synchronization, verification, provenance, tagging, and package publication.
fledge run test # Run RSpec test suite (405 examples, 87% line coverage)
fledge run lint # Run RuboCop linter (0 offenses)
fledge lanes run verify # Full CI gate (lint + tests + strict 100%-coverage spec-sync)
fledge lanes run release # Verify, smoke-test, and build the release gem
fledge run smoke-test # Runnable, assertion-based tour of real behavior (examples/smoke_test.rb)
COVERAGE=1 bundle exec rspec # Same suite, plus an HTML coverage report at coverage/index.htmlexamples/smoke_test.rb is a standalone, dependency-free script (no bundler/rspec required) that
exercises rune run, --timeout, TableParser/KeyValueParser, Script, signal forwarding, and
prompt detection against the real CLI binary, with pass/fail output and a non-zero exit on failure.
Useful as a quick manual sanity check, or on a machine without the dev dependencies installed.
MIT