npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ruddr

v0.6.7

Published

Thin control plane for Codex, Claude Code, OpenCode, Pi, and Factory Droid agent sessions: run, steer, observe, and a live TUI and web dashboard with diffs. One native binary.

Readme

Ruddr

A control plane for agents that run other agents.

Ruddr is a single native binary that keeps a live handle on long-running Codex, Claude Code, OpenCode 2, Pi, omp, Factory Droid, Hermes Agent, and OpenClaw sessions. An orchestrating agent launches a turn in the background, reads its progress from plain files, and redirects it over a local socket while it runs, without waiting for it to finish or restarting it. Every command works the same from a human shell, so a person can watch or steer too, but the primary operator is another agent.

ruddr TUI dashboard

ruddr run --prompt-file task.md --state-dir run &   # start a turn
ruddr peek --state-dir run                          # watch it think
ruddr steer --state-dir run "tests first, skip the benchmark"
ruddr tui                                           # every session, live
ruddr web                                           # the same, in a browser

Why it exists

Agent harnesses increasingly delegate: one agent plans, spawns a coding agent for the long turn, and keeps working while it runs. codex exec --json cannot support that loop. Its output is observable, but its stdin closes after the initial prompt, so the orchestrator can only wait for the turn or kill it. Both waste the work already done, and mid-flight discoveries (a wrong assumption, new user input, a better plan) cannot reach the running turn.

Ruddr owns the provider connection and exposes a small local control socket. A later command from the orchestrating agent, a cron job, or a human can steer the turn that is already running.

Ruddr is built for agents first:

  • State is plain files. An orchestrator polls or tails state.json, events.jsonl, and output.md without a TTY.
  • Commands print JSON where metadata matters. Skills and scripts read ruddr thread ... results without scraping text.
  • Steering is a CLI call. It talks to the socket, is safe to issue from a background task, and fails loudly when the turn is gone instead of starting a replacement.
  • The TUI, the web dashboard, and human steering use the same files and socket. They are optional.

Providers:

  • Codex. Ruddr owns a codex app-server connection and calls turn/steer on the running turn.
  • Claude Code. Ruddr drives the claude CLI directly over its stream-json protocol. A persistent streaming-input queue makes steering part of the same live session, and the structured events expose summarized reasoning, assistant updates, and tool lifecycle to the same TUI used for Codex.
  • OpenCode 2. Ruddr runs a private v2 server and uses its durable session inbox. Steering uses delivery: "steer" on the active session.
  • Pi. Ruddr runs Pi in JSONL RPC mode. Pi exposes native steering, interruption, session persistence, streamed tool events, and usage totals.
  • omp (oh-my-pi). omp is a Pi fork. Ruddr drives its RPC mode with the Pi adapter. A turn ends at omp's session_settled event.
  • Factory Droid. Ruddr runs droid exec in stream JSON-RPC mode. A steer is a user message that Droid queues and reads at its next step.
  • Hermes Agent and OpenClaw. Ruddr speaks the Agent Client Protocol (ACP) to hermes acp and to openclaw acp, OpenClaw's bridge to its running Gateway. Hermes steers through its /steer command; OpenClaw runs cannot be steered.

All providers use the same state directory, commands, and TUI. Codex speaks the app-server protocol itself. For the others, Ruddr starts its own binary as a hidden adapter, ruddr app-server --provider NAME, which translates the provider's protocol into the Codex app-server protocol on stdio.

task launcher ──stdio JSON-RPC──> provider app-server or adapter
      │
      ├── state.json / events.jsonl / trace.log / output.md
      │
      └── local control socket <── ruddr steer "focus on the failing test first"

Ruddr does not replace the engine, model, or auth provider. Like a ship's rudder, it changes the heading of a turn that is already running.

Status

Ruddr is an early working prototype. Provider protocols change quickly, so reverify Ruddr after provider upgrades. OpenCode support targets the 2.0 preview CLI through opencode2 or opencode-next. Ruddr does not support OpenCode 1 yet.

Ruddr 0.6.0 is a Rust rewrite. One binary holds the CLI, the run controller, the provider adapters, the TUI, and the web dashboard, and nothing needs Bun or Go at runtime. Changes in 0.6.0 lists the behavior that differs from 0.5.

Native Windows runs work as of 0.4.3 without access control of their own; see the Windows access note. Use WSL, Linux, or macOS when other accounts share the machine. As of 0.4.5, detached runs and TUI-started sessions survive the end of an SSH connection to a Windows host, and --remote drives hosts whose SSH shell is PowerShell.

Adding a provider means implementing one adapter behind the existing --provider flag. The state directory, control socket, steering commands, and TUI are provider-agnostic already.

Changes in 0.6.0

The commands, flags, exit codes, run directory files, registry, config files, and environment variables of 0.5 still work. These behaviors changed:

  • Flags are GNU style only: --flag value or --flag=value. The Go single-dash form (-state-dir DIR) is rejected as bad usage. Flags may follow positional arguments, as in ruddr steer "text" --state-dir DIR.
  • steer, prompt, interrupt, and single-run stop exit 4 when the run's controller is gone, like wait.
  • An unknown command exits 2, and so does run with an invalid --provider.
  • steer without --expected-turn-id pins the turn it observed in state.json, so a steer never lands on a later turn.
  • ruddr thread --provider NAME sends the request to another provider's adapter instead of codex app-server.
  • ruddr stop sends the same control request as 0.5, so a 0.6 binary can stop an idle session that a 0.5 controller runs.
  • ruddr run --help prints to stdout.
  • Ctrl-C during the run --detach startup wait stops the launcher. The detached controller keeps running.
  • During shutdown, Ruddr waits at most 5 seconds for the provider to exit after SIGKILL, then records a warning in trace.log and moves on.
  • Runs that the TUI and web dashboard start use directory names of the form YYYYMMDD-HHMMSS-<hex> (UTC), like ruddr run.
  • The web dashboard answers a stale prompt route with HTTP 409 instead of 500.
  • The dashboards no longer scan ps for running runs that predate the global registry. Point --root or --state-dir at such a run to see it.
  • On Windows the control channel is a named pipe instead of a Unix socket.
  • The Claude adapter drives the claude CLI directly instead of the Claude Agent SDK, so it needs no Node or Bun dependency.
  • RUDDR_TUI_ENTRY, RUDDR_WEB_ENTRY, and the RUDDR_*_ADAPTER_ENTRY variables are gone, along with the tui --rs flag.

Ruddr was previously published as Rudder and, before that, Codex Rudder. Settings, registries, and install locations from both earlier names keep working: RUDDER_* and CODEX_RUDDER_* environment variables are read as fallbacks, earlier ~/.config and ~/.local/state directories are searched after the new ones, and the rudder command stays as an alias.

Install

The quickest route is the npm package. It contains a small Node launcher that downloads the prebuilt ruddr binary for your platform from the matching GitHub release, verifies it, and runs it. The binary holds all of Ruddr, including the TUI, the web dashboard, and the provider adapters:

npm install -g ruddr
# or
bun add -g ruddr
ruddr --help

Prebuilt binaries cover macOS (Apple Silicon and Intel), Linux (x64 and arm64, statically linked against musl), and Windows x64. Every download is verified against the SHA-256 checksums pinned inside the published package. The package contains no sources and never builds anything. On any other platform, or when the download fails, the launcher exits with an error that says how to build Ruddr yourself (see Build). Set RUDDR_BINARY to the path of a binary you built, and the launcher runs it instead of downloading one. RUDDR_SKIP_DOWNLOAD=1 skips the download; without RUDDR_BINARY the launcher then fails with the same error.

The install-time fetch only saves time on the first run. The launcher checks for the binary on every invocation and downloads it when it is missing. When npm blocks install scripts, the launcher provisions the binary on first use and installs the bundled delegate skill then. If that skill installation warns about a failure, run ruddr skill install to retry.

npm install -g needs write access to the global prefix. Install into a user prefix when it does not have that access:

npm install -g --prefix "$HOME/.local" ruddr

The npm launcher needs Node 18 or newer. The binary itself needs nothing else at runtime: no Bun, Go, or Node. Each provider needs its own CLI on PATH (see Build for the versions Ruddr is verified against). On Windows, provider launches resolve .cmd shims through PATHEXT. Arguments remain separate values when Ruddr starts the resolved executable.

Updating

ruddr update --check   # report whether a newer release exists
ruddr update           # install it

Ruddr looks up the latest GitHub release at most once a day, on ruddr version and in the background of ruddr tui and ruddr web, and caches the answer under ~/.local/state/ruddr/update-check.json. When a newer release exists, ruddr version prints a notice, and the TUI shows a status toast, shows an ↑ VERSION badge in its header, and offers Update Ruddr in the command palette. Set RUDDR_NO_UPDATE_CHECK=1 to disable the check. Failed automatic checks also wait a day before retrying and retain the last known release. ruddr update --check always performs a fresh lookup and writes the release-check cache. It does not install a binary or write skills.

ruddr update picks the install channel from where the binary lives:

  • A binary inside a global npm or bun package is reinstalled at the new version through that tool. npm keeps the detected install prefix with npm install -g --prefix PREFIX [email protected]; the user-prefix install above stays under $HOME/.local. Bun uses bun add -g [email protected]. On Windows, Ruddr resolves npm and bun through PATH and PATHEXT, including .cmd shims.
  • A standalone binary is replaced in place after the download is verified against the release's checksums.txt. A binary that scripts/install-local.sh copied into ~/.local/bin is a standalone binary, so ruddr update replaces it with the release build.
  • A binary at the root of a source checkout is left alone. Ruddr tells you to run git pull there and rerun scripts/install-local.sh.

ruddr update without --check also reinstalls the ruddr-delegate skill into the default skill directories, including when Ruddr is already up to date. After an upgrade the new binary writes its own copy, so the skill always matches the installed release even when a package manager skipped its postinstall hook.

Releases are cut by pushing a vX.Y.Z tag that matches the version under [workspace.package] in Cargo.toml and the version in package.json. The workflow builds every platform, attaches the binaries and checksums to a GitHub release, and publishes the npm package.

Companion: dejavu

dejavu searches past Codex, Claude Code, Pi, omp, OpenCode, Factory Droid, OpenClaw, and Hermes transcripts on the same machine. Ruddr uses it in two places: the TUI's and the web dashboard's f key runs deja find to look up an earlier session, open it read-only, or continue it under Ruddr, and agents driving Ruddr use dejavu to recall what earlier runs decided or changed before they delegate new work. The H history browser in the TUI and the web dashboard reads the same stores with parsers that follow dejavu's. dejavu is a separate single binary; install it from its latest release and keep it current with dejavu self-update:

curl -fsSL -o ~/.local/bin/dejavu https://github.com/safzanpirani/dejavu/releases/latest/download/dejavu-darwin-arm64
chmod +x ~/.local/bin/dejavu && ln -sf dejavu ~/.local/bin/deja

Pick the asset for your platform: dejavu-darwin-arm64, dejavu-darwin-x64, dejavu-linux-x64, dejavu-linux-arm64, or dejavu-windows-x64.exe. Ruddr works without it; f then says that deja is not on PATH.

Build

Ruddr builds with Rust stable, 1.88 or newer:

cargo build --release -p ruddr-cli   # target/release/ruddr

scripts/install-local.sh runs the workspace tests, builds the release binary, copies it to ~/.local/bin/ruddr (RUDDR_BIN_DIR overrides the directory), adds the rudder alias, and installs the delegate skill:

./scripts/install-local.sh

The build needs no Bun. The browser client for ruddr web is committed as a prebuilt bundle in crates/ruddr-web/assets and embedded in the binary. Bun 1.4 or newer is needed only to rebuild that bundle after changing web/client (see Development).

Codex runs require a CLI with codex app-server and turn/steer support; that command surface is verified against codex-cli 0.145.0. Claude runs require the claude CLI and use the caller's normal Claude Code authentication. The Claude adapter speaks the stream-json protocol that Claude Agent SDK 0.3.245 used; its verification against a current claude CLI release is pending. OpenCode runs require the opencode2 or opencode-next executable and are verified against OpenCode 2.0.15. The model-switch and variant request schemas were checked against a local OpenCode 2.0.22 server without sending a prompt. The adapter uses the /api/experimental/session wait and export routes that 2.0.15 introduced, and falls back to the older /api/session routes on a 404.

Pi runs require the pi executable with RPC mode. omp runs require the omp executable; the adapter follows the RPC protocol of omp 18.6.1. Droid runs require the droid executable and are verified against droid 0.228.0 and 0.230.0, which speak Factory protocols 1.233.0 and 1.241.0. The adapters inherit each CLI's normal authentication environment.

Run a task

Create a self-contained prompt and a run directory:

mkdir -p .scratch/ruddr-demo
$EDITOR .scratch/ruddr-demo/prompt.md

ruddr run \
  --provider codex \
  --cwd "$PWD" \
  --prompt-file .scratch/ruddr-demo/prompt.md \
  --state-dir .scratch/ruddr-demo/run \
  --model gpt-6-astra \
  --sandbox workspace-write

--provider defaults to codex, so existing commands do not need to change.

Flags are GNU style: --flag value or --flag=value, before or after positional arguments. -- ends the flags; everything after it in run is the app-server command. Every command prints its flags with --help.

--state-dir is optional. Without it, Ruddr creates .scratch/ruddr/<time>-<id> under --cwd, prints the path (run --detach prints it on stdout, a foreground run on stderr), and writes a .gitignore into .scratch/ruddr so run files stay out of git status. Sessions started from ruddr tui or ruddr web live in .scratch/ruddr-tui, which ignores itself the same way. Pass --state-dir when you want to choose the location.

--config KEY=VALUE (repeatable) overrides a ~/.codex/config.toml setting for one Codex run. A run fails at thread/start when that file enables a feature the chosen model rejects:

ruddr run --prompt-file task.md --model gpt-6-sol \
  --config features.token_budget.use_history_notes_extension=false

--config works with the default codex app-server command. With a custom command after --, add -c KEY=VALUE to that command instead.

--image FILE (repeatable, up to 10) attaches a png, jpg, gif, or webp image to the first turn. steer and prompt take the same flag. Ruddr checks that each file exists before it sends anything, and an invalid path exits 2. Codex receives each image as a localImage input. Claude Code, OpenCode, Pi, omp, Droid, Hermes, and OpenClaw get the absolute paths listed under the prompt text, and the agent opens them with its own file-reading tool, so keep images somewhere the provider's sandbox can read:

ruddr run --prompt-file task.md --image .scratch/screens/login-bug.png
ruddr steer --state-dir .scratch/demo/run --image after.png "Now it looks like this."

Commands exit with distinct codes, so scripts can branch without parsing text:

| Code | Meaning | |---|---| | 0 | success | | 1 | a run failed or was interrupted, or any other error | | 2 | bad usage: an unknown command, subcommand, or flag; an invalid provider; missing required arguments; or conflicting flags | | 3 | still running: wait timed out, or result was asked of an unfinished run | | 4 | stale: a controller died without persisting a terminal state; wait, steer, prompt, interrupt, and single-run stop exit 4 for such a run |

Run Claude Code through the same control plane:

ruddr run \
  --provider claude \
  --cwd "$PWD" \
  --prompt-file .scratch/ruddr-demo/prompt.md \
  --state-dir .scratch/ruddr-demo/claude.run \
  --effort high \
  --sandbox workspace-write

Omit --model to use Claude Code's configured default. Ruddr finds the claude executable in this order: --claude-path, then RUDDR_CLAUDE_PATH, then claude on PATH. Set RUDDR_CLAUDE_PATH to make a choice persistent. Point either at a wrapper script when your Claude authentication depends on shell or Keychain setup that a detached process does not inherit. read-only maps to Claude's plan mode. workspace-write uses acceptEdits and enables Claude's command sandbox. It automatically allows Bash only when Claude runs the command inside that sandbox, adds the working directory to the writable paths, and fails if the sandbox is unavailable. danger-full-access maps to bypassPermissions. Ruddr denies any operation that still requires interactive approval.

Run OpenCode 2 or Pi through the same surface:

ruddr run --provider opencode --cwd "$PWD" \
  --prompt-file .scratch/ruddr-demo/prompt.md \
  --state-dir .scratch/ruddr-demo/opencode.run

ruddr run --provider pi --cwd "$PWD" \
  --prompt-file .scratch/ruddr-demo/prompt.md \
  --state-dir .scratch/ruddr-demo/pi.run

The default model for both adapters is openrouter/deepseek/deepseek-v4-flash-vision-exp. Use --opencode-path or RUDDR_OPENCODE_PATH to select an OpenCode 2 executable; without either, Ruddr looks for opencode2, then opencode-next, on PATH. Use --pi-path or RUDDR_PI_PATH to select a Pi executable; without either, Ruddr looks for pi on PATH. OpenCode installs private Ruddr-scoped agents with explicit permission rules for each sandbox value. Pi disables extensions and enables only its read, grep, find, and list tools for read-only. Both adapters use their provider's native permission system. They do not provide Ruddr-enforced filesystem containment for workspace-write.

OpenCode maps --effort to the session model's variant field before each prompt. The built-in DeepSeek model offers none, low, high, and max. OpenCode 2.0.22 reports these variants through its model API. Other models have their own variant names; configure their picker choices with ruddr models add opencode PROVIDER/MODEL --efforts low,high. OpenCode also accepts --model PROVIDER/MODEL#VARIANT; an explicit --effort takes precedence over that suffix.

An OpenCode --resume-thread ID keeps the session's model and variant when both overrides are omitted. --model switches the resumed session's model; --effort sets its variant. A model override without an effort uses the new model's default variant. Ruddr records the effective model and variant in state.json. A rejected model or variant update stops the run before the prompt.

Run these adapters only in trusted workspaces. OpenCode 2 loads project configuration and plugins, and Pi loads project-local resources after approval. Those resources can execute code outside the adapters' tool permission rules.

Run omp the same way:

ruddr run --provider omp --cwd "$PWD" \
  --prompt-file .scratch/ruddr-demo/prompt.md \
  --state-dir .scratch/ruddr-demo/omp.run

The default omp model is anthropic/claude-opus-5-5. omp matches model names fuzzily and also accepts its own roles, so --model opus works. Efforts are omp's thinking levels: off, minimal, low, medium, high, xhigh, max, and auto. Use --omp-path or RUDDR_OMP_PATH to select an omp executable; without either, Ruddr looks for omp on PATH. omp authenticates itself, so a shell wrapper that injects credentials does not run under Ruddr; export the variable or log in with omp instead.

Ruddr starts omp with --auto-approve, because Ruddr has no approval surface, and with --allow-home, so a run in the home directory stays there. omp has no option to choose a new session's ID, so thread/start returns the ID omp picks. --resume-thread accepts that ID or a transcript path, and --ephemeral maps to --no-session. read-only disables extensions and enables only omp's read, grep, find, and glob tools. omp's hashline edits name anchored lines instead of old and new text, so Ruddr reports each edit as a file change with the diff from omp's result once the edit lands. Ruddr rejects --fork-thread for omp. A fresh run resumes the newest session in the directory when omp's autoResume setting is on, so leave that setting off for delegated work. Like Pi, omp loads project-local resources, so run it only in trusted workspaces.

Run Hermes Agent or OpenClaw the same way:

ruddr run --provider hermes --cwd "$PWD" \
  --prompt-file .scratch/ruddr-demo/prompt.md \
  --state-dir .scratch/ruddr-demo/hermes.run

Both speak ACP: a thread is an ACP session (session/new, or session/load for --resume-thread), a turn is one session/prompt request, and an interrupt is session/cancel. The built-in model agent-default keeps the model the agent's own config selects; another --model goes to session/set_model (Hermes model IDs look like custom:gpt-5.6-sol). --effort, --ephemeral, --fork-thread, and --sandbox read-only are rejected: these agents keep every session and cannot confine their shell tools to reads. Ruddr declares no file-system or terminal capability, so the agent uses its own tools, and it answers every permission request as cancelled. Hermes runs workspace-write in its accept_edits mode (workspace and /tmp edits allowed, sensitive paths still asked about, so refused) and danger-full-access in dont_ask.

A Hermes steer is sent as /steer <text> while the turn runs. Hermes injects it into the active turn and confirms it; the confirmation never reaches the transcript, and a steer Hermes does not confirm fails instead of becoming a new turn. OpenClaw runs reject steers; interrupt the turn and send a prompt. openclaw acp needs the OpenClaw Gateway running on the same machine. Use --hermes-path or RUDDR_HERMES_PATH, and --openclaw-path or RUDDR_OPENCLAW_PATH, to select the executables; otherwise Ruddr looks for hermes and openclaw on PATH. Hermes 0.18 needs its ACP extra (pip install -e '.[acp]' in the Hermes checkout); hermes acp --check reports whether it is installed.

Run Factory Droid the same way:

ruddr run --provider droid --cwd "$PWD" \
  --prompt-file .scratch/ruddr-demo/prompt.md \
  --state-dir .scratch/ruddr-demo/droid.run

The default Droid model is glm-5.3-flash, with efforts low, high, and max. droid exec --help lists every model your Factory account offers. Use --droid-path or RUDDR_DROID_PATH to select a Droid executable; without either, Ruddr looks for droid on PATH. The sandbox sets Droid's autonomy level: read-only runs at off, workspace-write at medium, and danger-full-access at high. Droid enforces these levels itself. Ruddr adds no filesystem containment. Ruddr rejects every Droid permission request and question. A tool call above the autonomy level therefore ends the turn as failed, and the error names the autonomy level. Droid sessions always persist, so --ephemeral is refused. --fork-thread copies the whole Droid session. --fork-before-turn and --fork-through-turn are not supported for Droid.

Run it in the background from an agent harness so the harness can continue reading user messages and issue steering commands. --detach does this without harness support. It starts the controller in its own session, writes the child's early stderr to launch.stderr.log, and returns once the run reports active, idle, or completed. On Windows it also leaves the OpenSSH session's job object, which would otherwise kill the run when the SSH connection closes. A controller that fails during startup makes run --detach exit non-zero with that stderr. The launcher waits up to 15 seconds for the run to start. A controller still starting after that keeps running, and the launcher returns without retrying. Ctrl-C during the wait stops only the launcher; the detached controller keeps running. --prompt-file - reads the prompt from stdin and stores it as prompt.md (0600) inside the state directory; steer and prompt accept --message-file - the same way.

--turn-timeout defaults to one hour and stops a silently hung turn and its child process group. Set it to 0 only when an unbounded run is intentional. SIGINT and SIGTERM mark the run interrupted, terminate the app-server process group, and remove the control socket before Ruddr exits. Ruddr sends the provider SIGTERM and closes its stdin, escalates to SIGKILL after 3 seconds, and waits at most 5 more seconds before it records a warning and exits. On Windows, Ctrl-C, Ctrl-Break, and closing the console do the same.

Resume an existing provider thread or session for another steerable turn:

ruddr run \
  --resume-thread THREAD_ID \
  --cwd "$PWD" \
  --prompt-file .scratch/ruddr-demo/prompt.md \
  --state-dir .scratch/ruddr-demo/resumed-run

Fork first when the new work should preserve the source thread:

ruddr run \
  --fork-thread THREAD_ID \
  --fork-before-turn TURN_ID \
  --cwd "$PWD" \
  --prompt-file .scratch/ruddr-demo/prompt.md \
  --state-dir .scratch/ruddr-demo/forked-run

Use --fork-through-turn TURN_ID instead to include the selected turn.

Discover and manage Codex threads

ruddr thread prints the app-server result as JSON so agent skills and shell scripts can consume pagination cursors and complete metadata without scraping human-formatted output. It asks codex app-server by default:

ruddr thread list --limit 20 --cwd-filter "$PWD"
ruddr thread search --limit 10 "parser regression"
ruddr thread read --include-turns THREAD_ID
ruddr thread turns --limit 20 THREAD_ID

ruddr thread fork --before-turn TURN_ID THREAD_ID
ruddr thread name THREAD_ID "Parser regression investigation"
ruddr thread archive THREAD_ID
ruddr thread unarchive THREAD_ID

Every thread subcommand also accepts a stdio-compatible child command after --, using the same private auth-bridge composition as ruddr run.

--provider NAME sends the request to another provider's adapter (ruddr app-server --provider NAME) instead. The adapters implement only the run lifecycle, so they answer most thread actions with a method-not-found error. A command after -- works only with Codex.

Steer the active turn

ruddr steer \
  --state-dir .scratch/ruddr-demo/run \
  "New information: the regression starts in parser.go. Focus there first."

The command fails if the turn is no longer active or the active turn ID does not match. It never silently starts a replacement turn.

For exact multiline input:

ruddr steer --state-dir .scratch/ruddr-demo/run \
  --message-file .scratch/ruddr-demo/steer.md

Automation can pass --expected-turn-id ID to reject a steer when the selected session advances to another active turn before submission. Without the flag, steer reads the active turn ID from state.json and sends that, so the controller rejects the steer if a later turn has started. steer, prompt, and interrupt exit 4 when the run's controller is gone.

Idle sessions: multi-turn without new processes

ruddr run --idle keeps the controller and provider alive after a turn finishes. The run's status becomes idle and the control socket accepts new turns on the same thread:

ruddr run --idle --prompt-file task.md --state-dir .scratch/demo/run &
ruddr wait  --state-dir .scratch/demo/run   # blocks through idle; Ctrl+C to stop watching
ruddr prompt --state-dir .scratch/demo/run "Now add tests for the fix."
ruddr stop   --state-dir .scratch/demo/run  # graceful shutdown while idle

Rules:

  • prompt works only while the session is idle; steer works only while a turn is active. Neither command is ever converted into the other.
  • interrupt during a turn returns an idle session to idle instead of killing the provider; stop ends an idle session gracefully.
  • --idle-timeout (default 4h) exits the session after that long idle; the final persisted status is the last turn's terminal status.
  • --turn-timeout applies per turn.
  • output.md separates turns with ---; each prompt attempt is recorded as a synthetic userMessage item followed by an append-only decision event in events.jsonl. The TUI hides rejected attempts.
  • state.json gains idle, turns, lastTurnStatus, and tokenUsage (cumulative counts, context window, and cost when the provider reports one). lastTurnStatus keeps how the latest turn ended after the session returns to idle. Prompt text still never reaches state.json.
  • wait --turn returns when the current turn ends instead of blocking through idle, and fails when that turn did not complete.
  • The idle field records that the run was started with --idle. It stays true while the session is starting or active. Poll status == "idle" to know when prompt will be accepted.

Models

ruddr models [--json] prints the model catalog: each provider's models and its default. The TUI's picker uses it, and ruddr run without --model uses the default. OpenCode resumes keep their stored model.

The Codex catalog includes gpt-6.1-sol, gpt-6-sol, and gpt-6-luna. All three support low, medium, high, xhigh, and max reasoning; the two Sol models also support ultra. Select one with --model, for example ruddr run --prompt-file task.md --model gpt-6.1-sol --effort high. The delegate skill defaults to that model and effort; the CLI catalog default is gpt-6-astra.

Ruddr ships a short built-in list. Add the models you use, change a default, or hide one you never pick; Ruddr does not import every model a provider knows about. opencode models and similar provider commands list the IDs to choose from.

ruddr models add opencode opencode/deepseek-v4-flash --label "DeepSeek Flash" --default
ruddr models add codex gpt-7-preview --efforts low,medium,high
ruddr models add codex gpt-6-sol --config features.token_budget.use_history_notes_extension=false
ruddr models default claude claude-sonnet-5
ruddr models remove codex gpt-5.6-luna      # hides a built-in model
ruddr models path                           # where the file lives

The changes live in ~/.config/ruddr/models.json ($XDG_CONFIG_HOME is honored; RUDDR_MODELS_FILE overrides the path). You can edit it by hand:

{
  "models": [
    { "provider": "opencode", "id": "opencode/deepseek-v4-flash", "label": "DeepSeek Flash", "default": true },
    { "provider": "codex", "id": "gpt-5.6-luna", "hidden": true },
    { "provider": "codex", "id": "gpt-6-sol",
      "config": { "features.token_budget.use_history_notes_extension": "false" } }
  ]
}

A Codex model's config map is passed to codex app-server as -c KEY=VALUE for every run on that model, including runs the TUI starts. Use it for a ~/.codex/config.toml setting that model rejects. --unset-config KEY removes an entry, and run --config adds overrides after the model's own.

An invalid file is an error rather than being ignored, so a typo cannot silently run a different default model.

Observe and control

ruddr status --state-dir .scratch/ruddr-demo/run
ruddr status --state-dir .scratch/ruddr-demo/run --json
ruddr peek --state-dir .scratch/ruddr-demo/run -n 40
ruddr wait --state-dir .scratch/ruddr-demo/run --timeout 20m --progress 1m
ruddr interrupt --state-dir .scratch/ruddr-demo/run

Use interrupt --expected-turn-id TURN_ID to stop only the turn you observed. If that turn has changed, Ruddr rejects the command. Without the flag, the CLI captures the current turn ID before sending the control request. Delayed interrupt failures also leave later turns running.

wait --progress DURATION prints one progress line per waiting run to stderr at each interval. It also reports status changes on the next state poll. Each line includes the run name, status, turn count, elapsed time, time since observed log activity, and the last trace line (up to 120 characters). Activity comes from trace.log or events.jsonl modification times and size changes. Missing activity timestamps show as unknown; missing traces show as unavailable. Reads use a fixed 4 KiB tail. Progress reports never diagnose a hang or control the run. Stdout and exit codes keep their existing behavior.

Use a positive Go duration such as 30s or 1m; bare integers are invalid. Use --progress 1m for background waits so the harness's job band shows liveness.

Prune missing run references

ruddr prune                 # list missing references without changing the registry
ruddr prune --json          # structured dry-run report
ruddr prune --apply         # remove the listed missing references

prune checks the current and legacy run registries. It removes an entry only when the state directory lookup returns NotFound. It keeps unreadable targets and malformed entries and reports them separately. It never deletes run directories or their contents. A shared registration lock protects each registry update. The JSON report contains applied, entries, count, kept, and unreadable. RUDDR_REGISTRY_DIR selects one registry for both discovery and pruning. Dry runs do not write files.

Several runs at once

status, peek, wait, result, stop, and interrupt accept --state-dir more than once, and --root DIR selects every run below DIR (up to four levels deep). Give a swarm one directory, such as .scratch/swarm/<agent>/run, and address it as a group:

ruddr status --root .scratch/swarm            # one row per run
ruddr status --root .scratch/swarm --json     # JSON array of state.json
ruddr peek   --root .scratch/swarm            # last 5 trace lines of each run
ruddr wait   --root .scratch/swarm --timeout 30m --progress 1m
ruddr wait   --root .scratch/swarm --any      # return when the next run finishes
ruddr result --root .scratch/swarm            # each run's final answer
ruddr interrupt --root .scratch/swarm         # stop every active turn
ruddr stop   --root .scratch/swarm            # end every idle session
ruddr tui    --root .scratch/swarm            # watch the swarm live
NAME      STATUS     PROVIDER  MODEL            TURNS  TOKENS  ELAPSED  ERROR
api/run   completed  codex     gpt-6-astra      1      84.2K   6m12s
tests/run failed     codex     gpt-6-astra      1      12.1K   2m3s     turn failed; see trace.log
ui/run    active     claude    claude-opus-5-5  -      -       9m40s

The group wait prints that table and exits zero only when every run completed. A run whose controller died shows as stale.

wait --any returns when a run that was still running finishes, prints finished: NAME, and judges only that run. Runs that had already finished do not count, so a loop of wait --any hands back runs one at a time. When nothing is running, it returns at once.

wait --turn also counts an idle session as done, which a swarm of --idle sessions needs after each round of prompt. An idle session keeps its latest turn's outcome in lastTurnStatus, and --turn fails when that turn did not complete. It works for a single run too.

result prints the last agent message of each run's latest turn under a == NAME: STATUS == header, or the error for a run that did not complete, and exits non-zero if any run failed. With one --state-dir it prints only the message; --json prints an array.

interrupt acts only on active runs, and stop only on idle ones; the rest are reported as skipped. --expected-turn-id needs a single run. With one --state-dir and no --root, every command keeps its single-run output.

Ruddr does not isolate a swarm's workspaces. Give each writing agent its own Git worktree as its --cwd, or split the files so no two agents edit the same one.

For a live fullscreen view of several runs, launch the dashboard from any directory:

ruddr tui

The TUI is part of the ruddr binary and is built with ratatui. It needs no other runtime. The dashboard shows live runs first, followed by every finished run from Ruddr's private global registry, newest first. It also discovers state.json files below .scratch in the directory where it was launched. New ruddr run commands register themselves automatically. Scroll the list with the mouse wheel or filter it with /. Point it at extra locations with repeatable --root DIR and --state-dir DIR arguments (--all is still accepted and has no effect):

ruddr tui --root /path/to/project/.scratch
ruddr tui --state-dir /path/to/one/run --state-dir /path/to/another/run
ruddr tui --all
ruddr tui --theme tokyonight
ruddr tui --beta
ruddr tui --mobile

The default TUI uses an at-a-glance dashboard. A persistent sessions pane shows live and recent runs on the left. The right column shows session details, the Chat, Activity, Output, and Diff tabs, and the selected artifact. A header bar shows the project and Git branch, the selected session's provider, model, and effort, a spinner and elapsed time while it works, a context meter, and a count of live sessions. Press Tab to switch focus between the sessions pane and the selected artifact. Press Esc in the sessions pane to return focus to the artifact; it first clears an active cursor, search, or filter.

Use --beta for the chat-first layout. RUDDR_TUI_BETA=1 enables the same layout. Beta mode shows one session's conversation and keeps the sessions list behind a Tab overlay. Enter or Esc closes that overlay.

On narrow terminals the TUI switches to a mobile layout on its own: a single column with the sessions list as an overlay, no details panel, and a tappable action bar below the key hints, so a phone SSH client such as Blink or Termius can drive every action by touch. The bar has four buttons: ≡ N opens the sessions list (N is the number of sessions shown), ✎ prompt opens the prompt (✎ new when the selected session cannot take one), ■ stop stops the selected session, and ⋯ more opens the command palette. Each button is three rows tall and a quarter of the width, and a tap anywhere on it counts. The stop button dims when the selected session cannot be stopped and, like x x, needs a second tap. The switch happens at or below 64 columns and reverses when the window grows. Set mobileWidthThreshold in tui.json to change the width, or pass --mobile (or RUDDR_TUI_MOBILE=1) to force it at any size.

s or Enter in the sessions pane opens the prompt, which routes by session status. An active turn gets a steer. An idle --idle session gets a new turn over the control channel. A finished session continues its thread in a fresh run. The TUI sends steers, prompts, interrupts, and stops to the controller directly. The prompt accepts multiple lines: Enter sends, and Shift+Enter, Alt+Enter, or Ctrl+J insert a newline. It supports the usual line-editing keys, such as Ctrl+A, Ctrl+E, Ctrl+W, and Ctrl+U, and bracketed paste. A failed steer or idle prompt brings the draft back for editing. A failed new-session or continuation launch restores the draft text, images, model, effort, and destination. If you opened a newer draft, the TUI keeps it and reports the private draft.json recovery path for the failed launch. The recovery file contains the prompt text and launch settings. The TUI never retries a failed launch automatically.

Idle sessions show idle · failed or idle · interrupted after an unsuccessful turn. The list glyph and details use the outcome's error or warning color in both desktop and mobile layouts. These sessions still accept idle prompts.

The context meter in the header uses the latest context usage reported by the provider. It takes the theme's accent color, then turns to the warning color above 60% and the danger color above 85%. Session token totals remain available in the details panel and are labeled total; they never determine the context percentage. When the current context usage is unknown, the header shows no meter and the details panel shows the labeled session total. The TUI recovers the last context reading from the thread/tokenUsage/updated events in older Codex event logs.

n starts a brand-new session. Pick a provider and model in the picker, type the first prompt, and the TUI starts a detached ruddr run --idle in the current directory, in .scratch/ruddr-tui/<YYYYMMDD-HHMMSS>-<hex>. To start it somewhere else, send /cd DIR as the whole prompt: the TUI checks that the directory exists, keeps the prompt open, and starts this and later new sessions there, so that repository's project skills and instructions load. DIR may be absolute, relative to the current choice, or start with ~; a bare /cd returns to the directory the TUI started in. "Change new-session directory" in the palette opens the prompt with /cd typed. New sessions stay pinned at the top of the list. Any prompt box takes images: Ctrl+V (or a paste that carries no text) attaches the clipboard image, or the image file copied in Finder or Explorer, and dropping image files on the terminal attaches them instead of typing their paths. Clipboard images are saved as private PNGs under the session's .scratch/ruddr-tui/images. On Linux this needs wl-paste or xclip. Attached images show above the draft, and Backspace at the start of the draft removes the last one. m opens the same picker to override the model for continuations, and ←/→ change the effort. When the deja CLI from dejavu is installed, f searches past agent transcripts as you type. Input settles for 250 ms before a search starts. The panel keeps dejavu's ranking and highlights matching terms in excerpts. One request runs at a time; newer input replaces the queued search and stale results are discarded. ↑/↓ selects a hit. Enter opens that session read-only in the history list, including sessions older than its 400-session limit. Ctrl+R opens a continuation prompt for the hit. Esc closes the panel. Results and footer buttons also accept mouse clicks and taps.

L ("Continue where I left off" in the palette) runs deja last --json in the TUI's launch directory and opens the newest eligible session in that repo or cwd. Dejavu excludes the active session. The action opens history without starting a provider run. /cd DIR also changes the directory used by L.

M searches Claude project memories across projects through deja memory search --json. Type a literal phrase, select a file, and press Enter to preview its text. Q asks a question about the selected session through deja query. Enter reviews the question and the fixed session target. Press y to authorize the paid model call, or n/Esc to cancel. The confirmation shows no price estimate because dejavu does not provide one. A confirmed query runs in the background and displays its answer. Esc closes the view but does not cancel a paid call or refund its cost. No query retries run automatically. Free calls have a 30-second deadline; queries have a three-minute deadline. Use ↑/↓ or PgUp/PgDn to scroll a memory preview or answer. All four actions also appear in the command palette. They require deja on PATH.

H (or "Browse every agent's sessions" in the palette) switches the sessions list to every agent's local history, whether or not Ruddr started the session: Codex ($CODEX_HOME/sessions), Claude Code ($CLAUDE_CONFIG_DIR/projects), Pi (~/.pi/*/sessions or $PI_CODING_AGENT_DIR/sessions), omp (~/.omp/agent/sessions, ~/.omp/profiles/*/agent/sessions, and $XDG_DATA_HOME/omp/sessions), OpenCode (its SQLite databases under $XDG_DATA_HOME/opencode, or $OPENCODE_DB), Factory Droid (~/.factory/sessions, or $FACTORY_HOME_OVERRIDE/.factory/sessions), OpenClaw (each agent's agents/<agent>/agent/openclaw-agent.sqlite under $OPENCLAW_STATE_DIR or ~/.openclaw), and Hermes Agent ($HERMES_HOME/state.db or ~/.hermes/state.db). OpenClaw and Hermes open read-only, and only their transcript tables are queried; OpenClaw keeps auth profiles in the same file, and Ruddr never reads them. The newest 400 sessions are listed with their titles. Selecting one shows the whole conversation in Chat, its assistant messages in Output, and every file edit it made in Diff, with the same file tree. Claude, Pi, omp, and OpenClaw follow the active branch of their transcript trees. Codex and Claude diffs come from the patches they recorded, and omp diffs from the numbered diff in each edit result, all with real line numbers. Other providers' diffs are rebuilt from their edit-tool inputs, so their hunk line numbers count from the edited fragment. Each row shows the lines a session added and removed once a background scan has read it, and e (or "Only sessions that edited files" in the palette) hides the sessions with an empty diff. History sessions are read-only: prompts, stops, and deletes are disabled for them. Ruddr reads only transcripts, never the auth files stored beside them. Press H again to return to Ruddr's runs.

/ filters by project, thread, status, model, provider, working directory, or turn when the sessions pane has focus. / searches the selected artifact when the artifact has focus, and n/N move between matches. Chat, Activity, Output, and Diff are clickable tabs; o, h/l, ←/→, or 1 to 4 switch them. j/k, the arrow keys, PgUp/PgDn, Ctrl+D/Ctrl+U, g/Home, and the mouse wheel scroll the pane. c copies the selected row to the clipboard over OSC 52. The TUI captures the mouse, so a click selects a row instead of starting a terminal text selection. Scrolling up pauses follow mode; press End or G to return to live output.

Diff shows the selected session's tracked staged and unstaged working-tree changes against HEAD (in a repository without commits, the index and the working tree). When the session's working directory is not in a Git repository, or git diff fails, Diff shows the edits the run recorded in its own events.jsonl instead, under a one-line note that says why: Codex's file changes, and the edit and write tools of the other providers. Changes a shell command made are not recorded, so they appear only in a Git diff. It refreshes while the Diff tab is open and polls less often while the tree is quiet. Panes at least 80 columns wide also show a changed-file tree with per-file line counts; selecting a file jumps to its patch, and clicking a directory collapses it. Drag the divider beside the tree to reveal long paths or give the patch more room; Ruddr remembers that width across launches. File status letters distinguish modified, added, deleted, and renamed paths. The patch shows old and new line numbers in a gutter, tints added and deleted lines, and renders each file as a banner with its status and line counts. Patch lines get light syntax coloring by file extension: comments, strings, numbers, keywords, and capitalized names. Files modified since the selected session started carry a ● marker in the banner and the tree. Use ]c/[c (or ]h/[h) to move between hunks, and ]f/[f to move between files. Enter or a click on a file banner folds and unfolds that file, and Z folds or unfolds every file. The Diff tab label carries the current +added −deleted totals. Ruddr caps the diff at 2 MiB.

In Activity, Enter or a click expands a tool row into a card with the command, status, duration, working directory, input, and the last lines of output. Chat renders agent Markdown (headings, lists, quotes, inline code, and fenced code) and shows reasoning and tool calls inline. The text is live: Codex reports partial assistant text as item/agentMessage/delta, and the Claude, OpenCode, Pi, omp, Droid, Hermes, and OpenClaw adapters emit the same notification. Chat renders each completed line as Markdown and shows the line still arriving as plain text, so a message appears while the model writes it. Your steers appear in the transcript too. While the selected session works, a spinner shows in the sessions list and at the bottom of Chat and Activity. An empty Chat tab is clickable and opens the prompt. Status messages lead with a ✓, ›, !, or ✗ glyph and clear themselves after a few seconds.

Right-click a session for a context menu: prompt or steer it, continue it, stop it, open its chat or diff, copy its thread ID, state directory, or working directory, and delete it. With a / filter active the menu also offers to delete every finished session that matches. D deletes the selected session, and the command palette can clear all failed or stale sessions in one step. Deletion asks for confirmation, removes the run's state directory and its registry entries, and never touches a live session. Right-click a patch or activity row to copy it or its path, fold its file, expand the tool, or resume follow. Press : or Ctrl+K for the command palette, which lists every action with its key and filters as you type. Press ? for the key help.

i cycles the session details through full, hidden, and compact. Both layouts start with full details.

TUI launches retain prompt.md and launch.stderr.log in the private run directory and surface startup errors. A controller that is still starting after the brief launch check stays registered; Ruddr does not retry it.

Press t to open the theme picker. Moving through the list previews each palette immediately; Enter saves the choice globally and Escape restores the previous palette. Ruddr includes the 33 built-in OpenCode themes (using their dark variants) alongside its original theme. --theme NAME or RUDDR_TUI_THEME=NAME overrides the saved choice for one launch.

For an active run, s opens the prompt to steer and x x interrupts (an idle session returns to idle; x x on an idle session ends it). For a finished run, s or R continues the same provider thread or session in a fresh private run while preserving its working directory, model, effort, and sandbox. r refreshes, and q exits. Ctrl+C clears a non-empty draft and otherwise exits. State and artifacts otherwise refresh every 500ms; override that with a duration such as --interval 2s (whole milliseconds or seconds, at least 100ms).

The TUI keeps its settings in ~/.config/ruddr/tui.json ($XDG_CONFIG_HOME is honored): theme, mobileWidthThreshold, the diff tree's diffTreeWidth and diffTreeRatio, and the session list's sessionsWidth and sessionsRatio. Drag the right border of the session list or of the diff tree to resize it; < and > also narrow and widen the session list. The web dashboard shares the theme key, and both front ends keep keys they do not use. RUDDR_TUI_FRAMES=1 adds a count of drawn frames to the header, for debugging redraws.

Web dashboard

ruddr web serves the same dashboard to a browser:

ruddr web                         # http://127.0.0.1:4519, printed with its token
ruddr web --host 100.64.0.7       # a Tailscale address, for a phone
ruddr web --port 8080 --root ~/work/.scratch --open

It has the TUI's sessions list, Chat, Activity, Output, and Diff tabs, prompt routing, new sessions, deja search, themes, and keyboard shortcuts. H, the "history" button above the sessions list, or "Browse every agent's sessions" in the palette switches the list to every agent's local history, as in the TUI: the newest 400 sessions, read-only, with each session's chat, output, and file edits. In the f dialog, Enter or a click opens a hit there, and Ctrl+R or its "resume" button continues it in a new run. The Chat tab streams agent messages, reasoning, and command output as they arrive. Command output keeps its ANSI colors. A finished run of three or more commands, searches, or tool calls folds into one row; click it to expand the run. A session row flashes once in its new color when its status changes. The Chat tab renders available file edits as syntax-highlighted diffs from Codex patches and adapter edit-tool inputs. Providers can omit patches. Adapter replacement snippets omit file line numbers and EOF markers because the snippets do not identify their position in the file. Unnumbered apply_patch hunks render as fragments with accurate line counts and no source line numbers. Writes without known prior content show the supplied content as "write/overwrite" without added or deleted line counts. Only confirmed file creation receives an added-file label. The Diff tab shows the working tree against HEAD with a file tree, split or unified layout, and a filter for files edited since the session started. Outside a Git repository it shows the edits the run recorded, labeled "recorded edits", as the TUI does. A refresh re-highlights only files whose patch changed, and small files highlight in the background, so returning to the tab is instant. Diffs and the file tree use Pierre's @pierre/diffs and @pierre/trees. Model and other pickers follow the dashboard theme instead of the browser's native menu. Narrow screens get a single-column phone layout with a bottom tab bar. Press ? for the shortcut list or Cmd/Ctrl+K for the command palette. The theme is shared with ruddr tui.

A nonempty draft saves its text, session, prompt route, and steering turn ID across browser reloads. Restore arms the destination only when the original session and route still match. A steer also requires the same active turn. A changed or missing target leaves the text unarmed and displays a note. Legacy text-only drafts also restore unarmed. Choose the current target with the note’s button, or clear the draft to choose the current route. Restore never sends text. During live updates, the draft retains its original target. The server rejects a stale route or turn ID with HTTP 409 and sends nothing.

The page can steer and start agents, so every API call needs the token stored in ~/.config/ruddr/web-token (created 0600 on first use). Open the printed link once and the server swaps the token for an HttpOnly, SameSite=Strict cookie. Mutations also require a same-origin custom header. The server listens on 127.0.0.1 unless --host says otherwise. Bind it only to loopback or a private network such as Tailscale, never to a public interface. It reads run files only for verified sessions it discovered. The authenticated working-directory picker can list directory names across the host filesystem. New sessions can use any existing working directory that the server account can write to. The stream loads the last 6 MiB of events and skips records larger than 6 MiB. RUDDR_WEB_HOST, RUDDR_WEB_PORT, and --token-file override the defaults, and --interval sets the refresh interval (default 1s).

The server is part of the ruddr binary and needs no other runtime. The browser client is bundled at development time and embedded in the binary, so ruddr web serves it without Bun or Node. The dashboard sends steers, prompts, interrupts, and stops to the controller directly, and starts new sessions and continuations as detached ruddr run --idle processes in .scratch/ruddr-tui/<YYYYMMDD-HHMMSS>-<hex>.

Run artifacts:

  • .ruddr.claim is an atomic ownership marker that prevents state-directory reuse.
  • state.json holds IDs, status, paths, and timestamps, and no prompt or output text.
  • events.jsonl holds the raw provider protocol events and Ruddr's prompt decisions.
  • trace.log is compact human-readable progress.
  • output.md has every completed agentMessage item, appended in order.
  • provider.stderr.log holds child diagnostics. Legacy runs keep their persisted app-server.stderr.log path.

The run directory and all files are owner-only (0700 / 0600). On Unix the control channel is a 0600 Unix socket. It lives inside the run directory when the Unix path limit permits; otherwise Ruddr creates a random owner-only temporary parent and records it in state. On Windows the control channel is a named pipe, and state.json records its name (\\.\pipe\ruddr-<hex>) as socketPath. The raw events, trace, and output can contain prompt, command, and completion content. Persisted errors in state.json are generic; details remain in the private trace and stderr logs.

On native Windows, access to these files depends on NTFS ACLs, and Ruddr does not set them. Run artifacts inherit the ACL of the state directory's parent, and the control pipe uses Windows' default named-pipe security. Other software can widen the parent's ACL; the Codex Windows sandbox, for example, grants its own groups access to %TEMP%. Inspect the parent with icacls before storing sensitive runs there. Issue #6 tracks explicit ACLs.

Output appends avoid rewriting long transcripts. A reported partial write is rolled back to the previous file length; an abrupt process or machine crash can leave a partial final message, as with the other append-only logs. state.json continues to use atomic replacement.

The global run registry stores only private state-directory references under ~/.local/state/ruddr/runs (or XDG_STATE_HOME). It does not duplicate prompt, trace, output, or authentication content. The TUI and web dashboard also read the legacy rudder/runs and codex-rudder/runs registries so existing history does not disappear. RUDDR_REGISTRY_DIR replaces all of them with one directory.

If a process is killed without cleanup, status renders a non-terminal state as stale, while wait, steer, prompt, interrupt, and stop fail promptly with exit 4 instead of polling forever or returning an opaque socket error.

Run on another machine

--remote SSH_TARGET runs any Ruddr command on another machine over SSH. The target is anything ssh accepts, such as a host alias from ~/.ssh/config. RUDDR_SSH names a different ssh executable. The remote machine needs Ruddr on PATH (a POSIX host also finds it in ~/.local/bin); set RUDDR_REMOTE_RUDDR to its path otherwise. Remote run needs the same Ruddr release on both ends, because it relies on --detach.

Ruddr works with a POSIX login shell or with PowerShell, the usual OpenSSH default shell on Windows. The first command to a target runs one probe to tell them apart and caches the answer in ~/.local/state/ruddr/remote-shells.json. Set RUDDR_REMOTE_SHELL=posix or powershell to skip the probe. A Windows host whose default shell is still cmd.exe is refused with a pointer to the OpenSSH DefaultShell setting.

ruddr --remote ampere run --provider codex --cwd '~/src/app' \
  --prompt-file brief.md --state-dir '~/runs/fix-login'
ruddr --remote ampere peek --state-dir '~/runs/fix-login' -n 25
ruddr --remote ampere steer --state-dir '~/runs/fix-login' "keep the API stable"
ruddr --remote ampere wait --state-dir '~/runs/fix-login' --timeout 10m
ruddr --remote ampere tui
  • Paths are remote paths. Quote ~/… so your local shell leaves it alone; Ruddr passes a leading ~/ through unquoted so the remote shell expands it. Relative paths resolve against the remote home directory.
  • --prompt-file and --message-file name local files. Ruddr sends their contents over stdin and the remote side reads them with -. --image names a remote file; copy the image to the remote machine first.
  • run always starts detached and requires --cwd, so the session outlives the SSH connection. Follow it with peek, wait, or the TUI.
  • tui runs on the remote machine under ssh -t. From a phone, its width selects the mobile layout. Sessions it starts run detached, so they keep going when the connection drops.
  • Output and the exit status come back unchanged. Authentication stays on the remote machine, with that machine's provider login.

Layer over codex-auth-broker

Ruddr accepts any stdio-compatible app-server command after --. This lets the existing auth bridge keep ownership of OAuth refresh while Ruddr adds lifecycle and steering:

ruddr run \
  --cwd "$PWD" \
  --prompt-file .scratch/ruddr-demo/prompt.md \
  --state-dir .scratch/ruddr-demo/run \
  --model gpt-6-astra \
  --sandbox workspace-write \
  -- \
  /path/to/codex-auth-broker \
    app-server-bridge \
    -broker-auth-url https://broker.example.com/v1/codex/auth \
    -secret-file /path/to/broker.secret

The process chain is:

ruddr
  └─ codex-auth-broker app-server-bridge
       └─ codex app-server --listen stdio://

Ruddr never reads or persists the broker secret or Codex OAuth tokens. The bridge consumes those and presents the same app-server JSON-RPC stream.

The same mechanism overrides ~/.codex/config.toml for one run. A setting the chosen model does not support fails the run at thread/start; pass the default command with a -c override to turn it off:

ruddr run --model gpt-6-astra --cwd "$PWD" --prompt-file task.md \
  -- codex app-server --listen stdio:// -c features.SOME_FEATURE=false

Why not codex exec resume?

Resume adds a later turn after the current one completes. turn/steer appends input to the currently in-flight regular turn, so Codex can change direction after the current tool call and before it commits to a final answer.

Review and manual compaction turns can reject steering. review-codex-auto should continue using a normal prompt-driven turn (not review/start) when it needs the result to remain steerable.

Protocol compatibility

The installed CLI can emit exact schemas for its version:

schema_dir=$(mktemp -d /tmp/codex-app-server-schema.XXXXXX)
codex app-server generate-json-schema --experimental --out "$schema_dir"

Treat those schemas as candidates, not a runtime capability guarantee. In codex-cli 0.145.0, for example, the experimental schema advertises thread/items/list while the initialized app-server returns JSON-RPC -32601 for that method. Probe a method against the installed runtime before depending on it; Ruddr intentionally does not expose thread/items/list.

Ruddr currently depends on:

  • initialize then initialized
  • thread/start
  • thread/list, thread/search, and thread/read
  • thread/turns/list
  • thread/resume and thread/fork
  • thread/name/set, thread/archive, and thread/unarchive
  • turn/start
  • turn/steer
  • turn/interrupt
  • turn/started, item/*, and turn/completed notifications

The Claude, OpenCode 2, Pi, omp, Droid, Hermes, and OpenClaw adapters implement the lifecycle subset needed by ruddr run, steer, prompt, and interrupt. They do not implement the general Codex app-server surface. Sessions launched by another process do not become live-observable through Ruddr. Each adapter forwards summarized or provider-exposed reasoning. Ruddr does not expose hidden raw chain of thought.

Agent setup guide

Paste this verbatim into Claude Code, Codex, Cursor, or any agent with shell access. Part 1 installs and verifies Ruddr. Part 2 teaches the agent to run and steer provider sessions afterwards.

Set up Ruddr (https://github.com/safzanpirani/ruddr) on this machine, verify
it works, and learn how to operate it. Ruddr runs Codex, Claude Code,
OpenCode 2, Pi, omp, Factory Droid, Hermes Agent, or OpenClaw as an observable,
steerable child process. It writes every
event to
disk and exposes a control socket so you can redirect or stop a turn while it
runs. Follow Part 1 in order and stop at the first failure; keep Part 2 as your
operating manual.

PART 1: INSTALL AND VERIFY

1. Check prerequisites. Report the version of each and stop if any is missing:
   - At least one provider CLI: `codex --version`, `claude --version`,
     `opencode2 --version`, `pi --version`, `omp --version`,
     `droid --version`, `hermes --version`, or `openclaw --version`.
   - For the npm route: Node 18 or newer (`node --version`).
   - For a source build: Rust stable 1.88 or newer (`cargo --version`).
   Ruddr