Ralph is an autonomous serial ticket executor for Linear + Pi.
It lets you queue implementation work in Linear, then have a loop of fresh local Pi agents pick up tickets one at a time, implement them with TDD, validate the result, update Linear, and move on to the next ticket.
Long-running local agent sessions can run out of useful memory or drift across tasks. Ralph avoids that by treating every Linear ticket as a fresh unit of work:
- Ralph selects one eligible Linear ticket.
- Ralph creates a branch and a resolved execution packet.
- Ralph spawns a brand-new local
pisubprocess with--no-session. - Pi reads the packet, works AFK, and follows a TDD/red-green-refactor loop.
- Pi writes
worker-result.json. - Ralph validates the repo mechanically.
- Ralph moves the Linear ticket forward, then starts a fresh Pi process for the next ticket.
Ralph does not call model APIs directly. The worker is your local pi CLI, so it uses whatever provider, model, auth, and tools you already configured in Pi.
Ralph is usable as a local TypeScript CLI for a Linear-backed repo. The main loop, Linear client, config loading, queue discovery, packet generation, worker spawning, validation hooks, and workflow-signal writing exist.
The intended normal path is:
ralph executeThe smoke-test path is:
ralph execute --stub--stub uses a fake worker and does not code. Normal execute spawns real fresh Pi workers.
- Node.js 20+
- A working local
piCLI - A Linear personal API key exported as
LINEAR_API_KEY - A target repo with
.pi/executor.yaml - Linear tickets prepared for Ralph's queue
Example shell setup:
export LINEAR_API_KEY="lin_api_..."Pi should already be logged in/configured locally. Ralph relies on Pi for model/provider selection.
From this repo:
npm install
npm run build
npm linkThen from any managed project repo:
ralph --helpIf you do not want to link globally, run the built CLI directly:
node /path/to/Ralph/dist/cli/main.js --helpYou do not copy the Ralph repo into every project. Ralph is the reusable runner. Each project only needs a small repo-local config.
In the project you want Ralph to work on:
.pi/
executor.yaml
executor.lock.yaml # generated by ralph setup
Minimal shape:
schemaVersion: 1
repo:
canonical_remote: origin
slug: "github.com/your-org/your-repo"
runner:
branch_prefix: afk
default_mode: dry-run
serial_only: true
artifacts:
runner_dir: .pi/runner
executions_dir: .pi/executions
linear:
workspace_id: ""
team:
id: "<linear-team-id>"
name: "<linear-team-name>"
signals:
needs_info:
mode: label
name: "needs-info"
in_progress:
mode: state
name: "In Progress"
in_review:
mode: state
name: "In Review"
blocked:
mode: label
name: "blocked"
constants:
labels:
autonomous_ok:
name: "autonomous-ok"
blocked:
name: "blocked"
needs_info:
name: "needs-info"
states:
ready_for_agent:
name: "Ready for Agent"
default_project: default
projects:
default:
id: "<linear-project-id>"
name: "<linear-project-name>"
queues:
default:
required_labels: [autonomous_ok]
allowed_states: [ready_for_agent]
excluded_labels: [blocked, needs_info]
advisory:
comments_on_failures: false
validation:
defaults:
typecheck:
command: npm run typecheck
overridable: false
tests:
command: npm test
overridable: true
lint:
command: npm run lint
overridable: true
build:
command: npm run build
overridable: true
skippable: true
retry_budget:
default: 3
overridable: true
cap: 5
retriable_failure_classes:
- command_failure
- scope_failure
- artifact_failure
worker:
mode: headless
turn_budget:
default: 12
overridable: true
cap: 20
max_tool_calls:
default: 40
overridable: true
cap: 60
scope:
max_files_changed:
default: 8
overridable: true
cap: 12
warn_on_empty_context_refs: true
commit:
final_template:
default: "feat({ticketId}): {title}"
overridable: false
packet:
include_branch_status: true
include_raw_validator_output_appendix: true
truncate_large_output_chars: 12000Then resolve names/IDs:
ralph setup
ralph dry-runRalph only picks tickets from the configured queue. A typical queue requires:
- Linear project matches the configured project
autonomous-oklabel is present- state is
Ready for Agent blockedlabel is absentneeds-infolabel is absent
Each Ralph-bound issue should contain normal human-readable sections plus a machine-owned executor block.
Recommended issue body:
## What to build
<short description of the vertical slice>
## Acceptance criteria
- [ ] observable behavior 1
- [ ] observable behavior 2
## Out of scope
- thing not to touch
## Notes for worker
- optional implementation hints
<!-- EXECUTOR:START -->
schemaVersion: 1
repo: github.com/your-org/your-repo
context_refs: []
adr_refs: []
prd_refs: []
validator_commands: []
overrides: {}
notes_for_worker: []
<!-- EXECUTOR:END -->The Linear identifier, such as THO-123, comes from Linear and should not be duplicated inside the executor block.
ralph setup [--repo <path>]Resolve configured Linear names/IDs and write .pi/executor.lock.yaml.
ralph dry-run [--repo <path>]Validate config and print a preview summary. This does not spawn Pi.
ralph execute [--repo <path>] [--queue <name>] [--project <key>]Run the queue. For each eligible ticket, Ralph spawns a fresh local Pi worker with no previous session memory.
ralph execute --stubUse the fake worker for smoke testing the Ralph loop without asking Pi to code.
ralph resume [--repo <path>]
ralph reset [--repo <path>] [--confirm]
ralph takeover <TICKET-ID> [--mode autonomous|human_steered]Inspect resumability, clear stale run state, or adopt a blocked/in-progress ticket.
For each ticket, Ralph launches Pi roughly as:
pi --mode json --no-session -p "<Ralph worker prompt>"Ralph passes these environment variables:
RALPH_PACKET_PATH— packet JSON to readRALPH_RESULT_PATH—worker-result.jsonto writeRALPH_TURN_BUDGET— advisory turn budgetRALPH_TOOL_BUDGET— advisory tool-call budget
The worker prompt instructs Pi to:
- work fully AFK
- read repo context and the packet
- use TDD/red-green-refactor
- keep changes scoped to the ticket
- run validator commands
- always write a schema-valid result file
npm install
npm run typecheck
npm run build
npm test
npm run lintThe repository intentionally ignores build output, dependencies, runtime execution state, and historical design notes.
Ralph is intentionally conservative:
- serial execution only
- one fresh Pi process per ticket
- no model API calls from Ralph itself
- no push before validation/canonical commit
- dirty worktree checks before execution
- queue recalculation between tickets
- Linear workflow signals are the source of execution truth
Private/internal unless a license is added.