Skip to content

Repository files navigation

Ralph

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.

Why Ralph exists

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:

  1. Ralph selects one eligible Linear ticket.
  2. Ralph creates a branch and a resolved execution packet.
  3. Ralph spawns a brand-new local pi subprocess with --no-session.
  4. Pi reads the packet, works AFK, and follows a TDD/red-green-refactor loop.
  5. Pi writes worker-result.json.
  6. Ralph validates the repo mechanically.
  7. 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.

Current status

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 execute

The smoke-test path is:

ralph execute --stub

--stub uses a fake worker and does not code. Normal execute spawns real fresh Pi workers.

Requirements

  • Node.js 20+
  • A working local pi CLI
  • 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.

Install for local use

From this repo:

npm install
npm run build
npm link

Then from any managed project repo:

ralph --help

If you do not want to link globally, run the built CLI directly:

node /path/to/Ralph/dist/cli/main.js --help

Setting up a managed repo

You 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: 12000

Then resolve names/IDs:

ralph setup
ralph dry-run

Linear ticket requirements

Ralph only picks tickets from the configured queue. A typical queue requires:

  • Linear project matches the configured project
  • autonomous-ok label is present
  • state is Ready for Agent
  • blocked label is absent
  • needs-info label 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.

Commands

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 --stub

Use 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.

What the Pi worker does

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 read
  • RALPH_RESULT_PATHworker-result.json to write
  • RALPH_TURN_BUDGET — advisory turn budget
  • RALPH_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

Development

npm install
npm run typecheck
npm run build
npm test
npm run lint

The repository intentionally ignores build output, dependencies, runtime execution state, and historical design notes.

Safety model

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

License

Private/internal unless a license is added.

About

Autonomous serial Linear ticket executor for fresh local Pi agents

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages