# Ralph Driver Interface Contract ## Overview The Ralph loop loads a platform driver by sourcing `ralph/drivers/${PLATFORM_DRIVER}.sh` inside `load_platform_driver()` (`ralph_loop.sh` line 296). The `PLATFORM_DRIVER` variable defaults to `"claude-code"` and can be overridden via `.ralphrc`. After sourcing, `ralph_loop.sh` immediately calls three functions to populate core globals: 1. `driver_valid_tools` -- populates `VALID_TOOL_PATTERNS` 2. `driver_cli_binary` -- stored in `CLAUDE_CODE_CMD` 3. `driver_display_name` -- stored in `DRIVER_DISPLAY_NAME` **File naming convention:** `${PLATFORM_DRIVER}.sh` (e.g., `claude-code.sh`, `codex.sh`). **Scope:** This documents the sourceable driver contract used by `ralph_loop.sh`. Helper scripts like `cursor-agent-wrapper.sh` are out of scope. **Calling conventions:** - Data is returned via stdout (`echo`). - Booleans are returned via exit status (`0` = true, `1` = false). - Some functions mutate global arrays as side effects. --- ## Required Hooks Called unconditionally by `ralph_loop.sh` with no `declare -F` guard or default stub. Omitting any of these will break the loop at runtime. ### `driver_name()` ```bash driver_name() ``` No arguments. Echo a short lowercase identifier (e.g., `"claude-code"`, `"codex"`). Used at line 2382 to gate platform-specific logic. ### `driver_display_name()` ```bash driver_display_name() ``` No arguments. Echo a human-readable name (e.g., `"Claude Code"`, `"OpenAI Codex"`). Stored in `DRIVER_DISPLAY_NAME`, used in log messages and tmux pane titles. ### `driver_cli_binary()` ```bash driver_cli_binary() ``` No arguments. Echo the CLI executable name or resolved path (e.g., `"claude"`, `"codex"`). Stored in `CLAUDE_CODE_CMD`. Most drivers return a static string; cursor resolves dynamically. ### `driver_valid_tools()` ```bash driver_valid_tools() ``` No arguments. Must populate the global `VALID_TOOL_PATTERNS` array with the platform's recognized tool name patterns. Used by `validate_allowed_tools()`. ### `driver_build_command(prompt_file, loop_context, session_id)` ```bash driver_build_command "$prompt_file" "$loop_context" "$session_id" ``` Three string arguments: | Argument | Description | |----------|-------------| | `$1` prompt_file | Path to the prompt file (e.g., `.ralph/PROMPT.md`) | | `$2` loop_context | Context string for session continuity (may be empty) | | `$3` session_id | Session ID for resume (empty string = new session) | Must populate the global `CLAUDE_CMD_ARGS` array with the complete CLI command and arguments. Return `0` on success, `1` on failure (e.g., prompt file not found). **Reads globals:** `CLAUDE_OUTPUT_FORMAT`, `CLAUDE_PERMISSION_MODE` (claude-code only), `CLAUDE_ALLOWED_TOOLS` (claude-code only), `CLAUDE_USE_CONTINUE`. --- ## Optional Overrides with Loop Defaults `ralph_loop.sh` defines default stubs at lines 284 and 288. All existing drivers override them, but a minimal driver can rely on the defaults. ### `driver_supports_tool_allowlist()` ```bash driver_supports_tool_allowlist() ``` No arguments. Return `0` if the driver supports `--allowedTools` filtering, `1` otherwise. **Default:** returns `1` (false). Currently only `claude-code` returns `0`. ### `driver_permission_denial_help()` ```bash driver_permission_denial_help() ``` No arguments. Print platform-specific troubleshooting guidance when the loop detects a permission denial. **Reads:** `RALPHRC_FILE`, `DRIVER_DISPLAY_NAME`. **Default:** generic guidance text. --- ## Optional Capability Hooks Guarded by `declare -F` checks or wrapper functions in `ralph_loop.sh` (lines 1917-1954, 1576-1583). Safe to omit -- documented fallback behavior applies. ### `driver_supports_sessions()` ```bash driver_supports_sessions() ``` No arguments. Return `0` if the driver supports session resume, `1` otherwise. **If not defined:** assumed true (`0`). Implemented by all 5 drivers; `copilot` returns `1`. ### `driver_supports_live_output()` ```bash driver_supports_live_output() ``` No arguments. Return `0` if the driver supports structured streaming output (stream-json or JSONL), `1` otherwise. **If not defined:** assumed true (`0`). `copilot` returns `1`; all others return `0`. ### `driver_prepare_live_command()` ```bash driver_prepare_live_command() ``` No arguments. Transform `CLAUDE_CMD_ARGS` into `LIVE_CMD_ARGS` for streaming mode. **If not defined:** `LIVE_CMD_ARGS` is copied from `CLAUDE_CMD_ARGS` unchanged. | Driver | Behavior | |--------|----------| | claude-code | Replaces `json` with `stream-json` and adds `--verbose --include-partial-messages` | | codex | Copies as-is (output is already suitable) | | opencode | Copies as-is (output is already suitable) | | cursor | Replaces `json` with `stream-json` | ### `driver_stream_filter()` ```bash driver_stream_filter() ``` No arguments. Echo a `jq` filter expression that transforms raw streaming events into displayable text. **If not defined:** returns `"empty"` (no output). Each driver has a platform-specific filter; `copilot` returns `'.'` (passthrough). ### `driver_extract_session_id_from_output(output_file)` ```bash driver_extract_session_id_from_output "$output_file" ``` One argument: path to the CLI output log file. Echo the extracted session ID. Tried first in the session save chain before the generic `jq` extractor. Only `opencode` implements this (uses `sed` to extract from a `"session"` JSON object). ### `driver_fallback_session_id(output_file)` ```bash driver_fallback_session_id "$output_file" ``` One argument: path to the output file (caller passes it at line 1583; the only implementation in `opencode` ignores it). Last-resort session ID recovery when both driver-specific and generic extractors fail. Only `opencode` implements this (queries `opencode session list --format json`). --- ## Conventional Metadata Hooks Present in every driver but NOT called by `ralph_loop.sh`. Consumed by bmalph's TypeScript doctor/preflight checks in `src/platform/`. A new driver should implement these for `bmalph doctor` compatibility. ### `driver_min_version()` ```bash driver_min_version() ``` No arguments. Echo the minimum required CLI version as a semver string. ### `driver_check_available()` ```bash driver_check_available() ``` No arguments. Return `0` if the CLI binary is installed and reachable, `1` otherwise. --- ## Global Variables ### Written by drivers | Variable | Written by | Type | Description | |----------------------|----------------------------------|-------|------------------------------------------------------| | `VALID_TOOL_PATTERNS`| `driver_valid_tools()` | array | Valid tool name patterns for allowlist validation | | `CLAUDE_CMD_ARGS` | `driver_build_command()` | array | Complete CLI command with all arguments | | `LIVE_CMD_ARGS` | `driver_prepare_live_command()` | array | Modified command for live streaming | ### Read by drivers (set by ralph_loop.sh or .ralphrc) | Variable | Used in | Description | |-------------------------|--------------------------------------------|------------------------------------------------| | `CLAUDE_OUTPUT_FORMAT` | `driver_build_command()` | `"json"` or `"text"` | | `CLAUDE_PERMISSION_MODE`| `driver_build_command()` (claude-code) | Permission mode flag, default `"bypassPermissions"` | | `CLAUDE_ALLOWED_TOOLS` | `driver_build_command()` (claude-code) | Comma-separated tool allowlist | | `CLAUDE_USE_CONTINUE` | `driver_build_command()` | `"true"` or `"false"`, gates session resume | | `RALPHRC_FILE` | `driver_permission_denial_help()` | Path to `.ralphrc` config file | | `DRIVER_DISPLAY_NAME` | `driver_permission_denial_help()` | Human-readable driver name | ### Environment globals (cursor-specific) | Variable | Used in | Description | |---------------|--------------------------------------|-------------------------------------| | `OS`, `OSTYPE`| `driver_running_on_windows()` | OS detection | | `LOCALAPPDATA`| `driver_localappdata_cli_binary()` | Windows local app data path | | `PATH` | `driver_find_windows_path_candidate()`| Manual PATH scanning on Windows | ### Set by ralph_loop.sh from driver output | Variable | Source | Description | |----------------------|-------------------------|-------------------------------| | `CLAUDE_CODE_CMD` | `driver_cli_binary()` | CLI binary name/path | | `DRIVER_DISPLAY_NAME`| `driver_display_name()` | Human-readable display name | --- ## Capability Matrix | Capability | claude-code | codex | opencode | copilot | cursor | |---------------------------------------------------------|:-----------:|:-----------:|:-----------:|:-----------:|:-----------:| | Tool allowlist (`driver_supports_tool_allowlist`) | yes | no | no | no | no | | Session continuity (`driver_supports_sessions`) | yes | yes | yes | no | yes | | Structured live output (`driver_supports_live_output`) | yes | yes | yes | no | yes | | Live command transform (`driver_prepare_live_command`) | transform | passthrough | passthrough | -- | transform | | Stream filter (`driver_stream_filter`) | complex jq | JSONL select| JSONL select| passthrough | complex jq | | Custom session extraction (`driver_extract_session_id_from_output`) | -- | -- | yes | -- | -- | | Fallback session lookup (`driver_fallback_session_id`) | -- | -- | yes | -- | -- | | Dynamic binary resolution (`driver_cli_binary`) | static | static | static | static | dynamic | --- ## Creating a New Driver ### Minimal driver skeleton ```bash #!/usr/bin/env bash # ralph/drivers/my-platform.sh # Driver for My Platform CLI # # Sourced by ralph_loop.sh via load_platform_driver(). # PLATFORM_DRIVER must be set to "my-platform" in .ralphrc. # --------------------------------------------------------------------------- # Required hooks (5) -- omitting any of these breaks the loop # --------------------------------------------------------------------------- # Short lowercase identifier used to gate platform-specific logic. driver_name() { echo "my-platform" } # Human-readable name for log messages and tmux pane titles. driver_display_name() { echo "My Platform" } # CLI executable name or resolved path. driver_cli_binary() { echo "my-platform" } # Populate VALID_TOOL_PATTERNS with recognized tool name patterns. # Used by validate_allowed_tools() to check allowlist entries. driver_valid_tools() { VALID_TOOL_PATTERNS=( "Read" "Write" "Edit" "Bash" # Add your platform's tool patterns here ) } # Build the complete CLI command array. # $1 = prompt_file Path to .ralph/PROMPT.md # $2 = loop_context Context string for session continuity (may be empty) # $3 = session_id Session ID for resume (empty = new session) driver_build_command() { local prompt_file="$1" local loop_context="$2" local session_id="$3" if [[ ! -f "$prompt_file" ]]; then return 1 fi CLAUDE_CMD_ARGS=( "my-platform" "--prompt" "$prompt_file" "--output-format" "${CLAUDE_OUTPUT_FORMAT:-json}" ) # Append session resume flag if continuing a session if [[ "$CLAUDE_USE_CONTINUE" == "true" && -n "$session_id" ]]; then CLAUDE_CMD_ARGS+=("--session" "$session_id") fi # Append context if provided if [[ -n "$loop_context" ]]; then CLAUDE_CMD_ARGS+=("--context" "$loop_context") fi return 0 } # --------------------------------------------------------------------------- # Optional overrides (2) -- loop provides default stubs # --------------------------------------------------------------------------- # Return 0 if the platform supports --allowedTools filtering, 1 otherwise. driver_supports_tool_allowlist() { return 1 } # Print troubleshooting guidance on permission denial. driver_permission_denial_help() { echo "Permission denied. Check that $DRIVER_DISPLAY_NAME has the required permissions." echo "See $RALPHRC_FILE for configuration options." } # --------------------------------------------------------------------------- # Metadata hooks (2) -- used by bmalph doctor, not called by ralph_loop.sh # --------------------------------------------------------------------------- # Minimum required CLI version (semver). driver_min_version() { echo "1.0.0" } # Return 0 if the CLI binary is installed and reachable, 1 otherwise. driver_check_available() { command -v my-platform &>/dev/null } ``` ### Checklist - [ ] All 5 required hooks implemented (`driver_name`, `driver_display_name`, `driver_cli_binary`, `driver_valid_tools`, `driver_build_command`) - [ ] `driver_valid_tools` populates `VALID_TOOL_PATTERNS` with your platform's tool names - [ ] `driver_build_command` handles all three arguments correctly (`prompt_file`, `loop_context`, `session_id`) - [ ] `driver_check_available` returns `0` only when the CLI is installed - [ ] File named `${platform_id}.sh` matching the `PLATFORM_DRIVER` value in `.ralphrc` - [ ] Register corresponding platform definition in `src/platform/` for bmalph CLI integration - [ ] Tested with `bmalph doctor` --- ## Session ID Recovery Chain When the loop needs to persist a session ID for resume, it follows a three-step priority chain (`ralph_loop.sh` lines 1574-1588): 1. **`driver_extract_session_id_from_output($output_file)`** -- Driver-specific extraction. If the function exists (`declare -F` guard) and echoes a non-empty string, that value is used. Only `opencode` implements this (uses `sed` to extract from a `"session"` JSON object). 2. **`extract_session_id_from_output($output_file)`** -- Generic `jq` extractor from `response_analyzer.sh`. Searches the output file for `.sessionId`, `.metadata.session_id`, and `.session_id` in that order. 3. **`driver_fallback_session_id($output_file)`** -- CLI-based last-resort recovery. If the function exists and the previous steps produced nothing, this is called. Only `opencode` implements this (queries `opencode session list --format json`). The first step that returns a non-empty string wins. If all three steps fail, no session ID is saved and the next iteration starts a fresh session.