ralph

agent
Security Audit
Warn
Health Pass
  • License — License: MIT
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Community trust — 25 GitHub stars
Code Warn
  • fs module — File system access in .github/workflows/publish-npm.yml
  • process.env — Environment variable access in .github/workflows/release-please.yml
Permissions Pass
  • Permissions — No dangerous permissions requested

No AI report is available for this listing yet.

SUMMARY

Autonomous coding-agent loop. Runs Claude Code or Codex against your repo in a Docker sandbox, iterating implementer → reviewer until the plan or GitHub issues are done. AFK-friendly: detach, retries, notifications.

README.md

Ralph — Autonomous Coding-Agent Loop

@daonhan/ralph
@daonhan/ralph-core
CI
License: MIT

Ralph drives Claude Code by default, or Codex when selected with
--agent codex, against a target repository in an iterating implementer →
reviewer pipeline isolated inside a custom Docker image.

⚠️ Security: Ralph runs the selected agent without interactive approval inside the sandbox (--permission-mode bypassPermissions for Claude; --dangerously-bypass-approvals-and-sandbox for Codex) and, by default, bind-mounts the host Docker socket — granting root-equivalent access to the host Docker daemon. Point it only at repositories, plans, and GitHub issues you trust. Disable the socket mount with RALPH_DOCKER_SOCK=0. See SECURITY.md for the full threat model.

New here? Start with QUICKSTART.md (zero-to-first-loop). Hacking on Ralph itself → CONTRIBUTING.md. Internals → docs/ARCHITECTURE.md. Background / design walkthrough → The Ralph AFK Stack, Explained (Substack).

  • @daonhan/ralph-core — library: iteration loop, docker runner, template renderer, stage registry. Importable from any Node project.
  • @daonhan/ralph — CLI: exposes ralph-afk and ralph-ghafk bin entries. Depends on @daonhan/ralph-core.

Two AFK entry points (both installed globally after npm i -g @daonhan/ralph):

  • ralph-afk — plan/PRD-driven loop. Hand it a plan + PRD string; iterates until the agent emits the sentinel <promise>NO MORE TASKS</promise>.
  • ralph-ghafk — GitHub-issue-driven loop. Pulls open issues with gh issue list and lets the agent pick the next AFK task.

Convenience shims live at apps/cli/scripts/afk.sh and apps/cli/scripts/ghafk.sh — thin wrappers that fall back to npx @daonhan/ralph if not installed.

Agent playbooks: packages/core/templates/prompt.md (for ralph-afk) and packages/core/templates/ghprompt.md (for ralph-ghafk). Reviewer instructions: packages/core/templates/review.md. All three ship inside @daonhan/ralph-core.


Architecture (AFK loops)

ralph-afk / ralph-ghafk               (bin entries from @daonhan/ralph, on PATH after `npm i -g`)
   │
   ▼
@daonhan/ralph (CLI, apps/cli)        bin: ralph-afk, ralph-ghafk; scripts: afk.sh, ghafk.sh shims
   │ imports
   ▼
@daonhan/ralph-core (packages/core)
   ├── runAfk / runGhAfk              (env-driven entry: argv → runLoop)
   ├── runLoop                        (drives stage chain per iteration; checks sentinel)
   ├── render                         (renderer: @include / @spill / !? / !`cmd` / {{ INPUTS }})
   ├── stages                         (stage registry: implementer, ghafkImplementer, reviewer)
   └── runner                         (docker run → NDJSON stream → live print → final result)
   │
   ▼
docker run ralph-sandbox <selected-agent> …

Each iteration runs the stage chain [implementer, reviewer]. The implementer is the "gate": if it emits <promise>NO MORE TASKS</promise>, the loop exits before the reviewer runs.

Prompt templates expand five tag forms before each stage runs, in order — @include: (inline a file, no shell), @spill[?]: (run a command, write its output to a side file the agent Reads), !?`cmd|||fallback` (try-shell), !`cmd` (host shell), and {{ INPUTS }} (the entry CLI's input arg — the plan/PRD string for ralph-afk, empty for ralph-ghafk). Full semantics under Change the template syntax; the runtime model lives in docs/ARCHITECTURE.md.


Repo layout

ralph/
├── package.json                 monorepo root (private, shared devDeps, pnpm scripts)
├── pnpm-workspace.yaml
├── tsconfig.base.json           shared TS compiler options
├── .npmrc                       link-workspace-packages, prefer-workspace-packages
├── .dockerignore                shrinks build context (consumed at repo root)
├── apps/
│   └── cli/                     @daonhan/ralph
│       ├── package.json
│       ├── bin/
│       │   ├── ralph-afk.js
│       │   └── ralph-ghafk.js
│       └── scripts/             optional bash shims (ship in npm tarball)
│           ├── afk.sh
│           └── ghafk.sh
├── packages/
│   └── core/                    @daonhan/ralph-core
│       ├── package.json
│       ├── tsconfig.json
│       ├── src/                 main.ts, gh-main.ts, loop.ts, runner.ts, render.ts, stages.ts, index.ts, cli-help.ts, retry.ts, keepalive.ts, detach.ts, notify.ts + __tests__/
│       └── templates/           afk.md, ghafk.md, review.md, prompt.md, ghprompt.md, CHANGELOG.md, Dockerfile (builds ralph-sandbox image)
└── (playbooks live in packages/core/templates/ alongside the prompt templates)

At runtime, the host workspace gets a .ralph-tmp/ directory containing the per-iteration prompt files and logs/*.ndjson. This directory is gitignored.


Prerequisites

  • Docker — Docker Desktop (Windows/macOS) or Docker Engine (Linux). The orchestrator shells out to docker build / docker run.
  • Node.js 20+ + npm 9+ (or pnpm/yarn). For Windows: native nvm-for-windows, nvm-windows, directly from nodejs.org, or Node inside WSL. For macOS/Linux: nvm, asdf, or a distro package.
  • gh authenticated (only required for ralph-ghafk): gh auth login once.
  • Claude Code or Codex authentication for the provider you select. See "First-run setup" below.
  • (Windows, optional but recommended) bash.exe on PATH — comes free with Git for Windows. The renderer prefers it over cmd.exe because POSIX redirects + utilities (git log, gh issue list) are smoother. If absent, the renderer falls back to cmd.exe and uses the built-in try-shell tag (!?\cmd|||fallback``) so commands that fail return their fallback string cleanly — no broken render.

Supported shells / OS combinations

Where you invoke ralph-afk Claude Codex Notes
Linux native (Ubuntu, etc.) /bin/bash is used for shell tags.
macOS native /bin/bash is used.
Windows PowerShell / cmd Native Windows is supported for both providers.
Windows + WSL bash Install Ralph, the selected host CLI, and credentials inside the same WSL distro.
Windows + Git Bash Native Git Bash and its Windows home are supported for both providers.

Windows + WSL: credentials

Credentials live on the host at ~/.claude or ~/.codex (for the selected provider) and ~/.config/gh, then get bind-mounted into the container. The path resolves per the shell that launches ralph-afk:

Launch from $HOME is Mounted into container
Windows PowerShell / cmd C:\Users\<name> The selected provider's credential store under this home is mounted
WSL bash /home/<linuxname> The selected provider's credential store under this home is mounted
Linux / macOS /home/<name> or /Users/<name> The selected provider's credential store under this home is mounted

Codex works from native Windows shells and WSL alike: Ralph mounts ~/.codex
read-only at /mnt/codex-creds and copies auth.json (plus config.toml and
AGENTS.md when present) into a container-local CODEX_HOME before each
stage, so the credential home never sits on an NTFS-backed bind mount. Log in
with the host Codex CLI (codex login) from the same shell environment that
launches Ralph.

If you already logged in via PowerShell claude.exe and want WSL to use those creds too:

# WSL bash — replace <WINUSER>
mkdir -p ~/.claude
cp -r /mnt/c/Users/<WINUSER>/.claude/. ~/.claude/
cp /mnt/c/Users/<WINUSER>/.claude.json ~/.claude.json 2>/dev/null || true
mkdir -p ~/.config/gh
# gh on native Windows stores config in AppData/Roaming/GitHub CLI; fall back to .config/gh
cp -r "/mnt/c/Users/<WINUSER>/AppData/Roaming/GitHub CLI/." ~/.config/gh/ 2>/dev/null || \
cp -r /mnt/c/Users/<WINUSER>/.config/gh/. ~/.config/gh/ 2>/dev/null || true
  • Launching Claude from PowerShell after a global install — just call the bin directly:
    ralph-afk "<plan-and-prd>" 3
    
  • Or from inside WSL bash:
    ralph-afk "<plan-and-prd>" 3
    

Choose the coding agent

Claude remains the default:

ralph-afk "./docs/plans/x.md ./docs/prd/x.md" 5

Select Codex per invocation:

ralph-afk --agent codex "./docs/plans/x.md ./docs/prd/x.md" 5
ralph-ghafk --agent codex 5

For automation, RALPH_AGENT=codex is the fallback when --agent is absent.
The explicit flag always wins.

Codex ignores ~/.codex/config.toml by default while still reusing its login.
Pass --codex-user-config to load that configuration intentionally. This may
start configured MCP servers and hooks, so their commands and paths must work
inside the Linux sandbox.

RALPH_MODEL applies to the selected agent. For Claude the model resolves as
RALPH_MODEL → the model pinned by the host's ~/.claude/settings.json
(env.ANTHROPIC_MODEL, else the model key /model stored; its "(default)"
entry stores no model) → claude-opus-5[1m], Ralph's own default. Ralph passes
--model rather than letting the container choose, because the sandbox image's
CLI is frozen at image build time and its built-in default can lag the host's.
The exception is third-party routing: when the host settings enable
CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, or
CLAUDE_CODE_USE_FOUNDRY, model IDs are provider-specific, so Ralph sends no
--model and the container CLI resolves as before. Isolated Codex defaults to
gpt-5.6-sol with high reasoning when RALPH_MODEL is unset. In inherited
configuration mode, an unset model and reasoning effort come from
~/.codex/config.toml. An explicit invalid model fails; Ralph never reruns the
stage with another model.

First-run setup

1. Get the image

The orchestrator resolves the image in three steps on each run:

  1. docker image inspect $RALPH_IMAGE — short-circuits if the image is already on the host (a floating tag like :latest is re-pulled anyway, so a republished sandbox isn't pinned to a stale local copy).
  2. Otherwise docker pull $RALPH_IMAGE — defaults to docker.io/daonhan/ralph-sandbox:latest.
  3. If pull fails AND $RALPH_DOCKER_CONTEXT/Dockerfile exists, falls back to docker build -t $RALPH_IMAGE $RALPH_DOCKER_CONTEXT.

For most users step 2 is enough — no local Dockerfile needed. To prime the cache:

docker pull docker.io/daonhan/ralph-sandbox:latest

Build locally (offline, custom changes):

cd ralph
docker build -t docker.io/daonhan/ralph-sandbox:latest -f packages/core/templates/Dockerfile .

The image bundles Node 22, Debian Bookworm Python 3.11 as python and python3,
python -m venv, uv/uvx 0.11.28, .NET SDK 10, gh, jq, git, Claude Code,
and the pinned Codex CLI. Basic Python repositories need no extra runtime install. Create a
project-local virtual environment (for example, .venv) or use uv-managed
isolation; do not install project dependencies globally into the Debian system
Python.

This release provides one baked system Python and does not select versions from
.python-version, .tool-versions, .mise.toml, pyproject.toml, or similar
manifests. Repositories pinned to another Python version need a custom
RALPH_IMAGE until future version-detection support is added.

Publishing a new image (maintainers)

The repo ships a GitHub Actions workflow at .github/workflows/publish-image.yml that builds + pushes linux/amd64 images to Docker Hub.

The Python runtime and tooling addition is a ralph-sandbox image release only;
it does not bump @daonhan/ralph-core or @daonhan/ralph.

Triggers:

  • workflow_dispatch — manual run from the Actions tab; pick the tag and whether to also push :latest.
  • Git tag ralph-sandbox-v* — pushing a tag like ralph-sandbox-v0.1.3 (cut by release-please) publishes :v0.1.3 plus :latest, and enriches the matching GitHub Release with the image digest, an SBOM, and a keyless cosign attestation.
  • Git tag image-v* — legacy compatibility shim; publishes :vX.Y.Z plus :latest but does not enrich a GitHub Release. Slated for removal after one release cycle through the new path.

Required repo secrets: DOCKERHUB_USERNAME, DOCKERHUB_TOKEN (a Docker Hub access token with Read & Write scope on the daonhan/ralph-sandbox repository).

2. Authenticate (one-off)

The image is stateless. Provider credentials live on the host at ~/.claude or ~/.codex. If you use ralph-ghafk, its provider-independent GitHub CLI credentials live at ~/.config/gh. The orchestrator mounts only the selected provider's credentials, plus GitHub CLI credentials when present, into each container.

Same-shell rule. ralph-afk / ralph-ghafk read $HOME of the shell that launched them. Auth from the same shell context you intend to run the bins in. PowerShell host (C:\Users\<you>\.config\gh\) and WSL host (\\wsl$\Ubuntu\home\<you>\.config\gh\) are separate stores — don't mix. Native PowerShell and Git Bash homes are valid for both providers.

Choose one provider login path below. Claude and Codex authentication are
mutually exclusive; GitHub authentication is provider-independent and required
only for ralph-ghafk.

Claude login

Linux / macOS / WSL bash
mkdir -p ~/.claude
touch ~/.claude.json

docker run -it --rm \
  -v "$HOME/.claude:/home/agent/.claude" \
  -v "$HOME/.claude.json:/home/agent/.claude.json" \
  docker.io/daonhan/ralph-sandbox:latest bash
Windows PowerShell
New-Item -ItemType Directory -Force "$HOME\.claude" | Out-Null
if (-not (Test-Path "$HOME\.claude.json")) { New-Item -ItemType File "$HOME\.claude.json" | Out-Null }

docker run -it --rm `
  -v "${HOME}\.claude:/home/agent/.claude" `
  -v "${HOME}\.claude.json:/home/agent/.claude.json" `
  docker.io/daonhan/ralph-sandbox:latest bash
Inside the container
claude /login         # browser flow; Claude only
exit

Codex login

Use this path instead when you select Codex. Install the host CLI version
pinned in Ralph's sandbox from the same shell environment that will launch
Ralph:

npm install --global @openai/[email protected]
codex --version

Codex credentials must be file-backed because a host OS keyring is not
available inside Docker. Create ~/.codex/config.toml if needed and set:

cli_auth_credentials_store = "file"

Then authenticate in that same shell:

codex login
codex login status

Ralph mounts ~/.codex read-only at /mnt/codex-creds and copies auth.json
(plus config.toml and AGENTS.md when present) into a container-local
CODEX_HOME=/home/agent/.codex before each stage. Codex therefore never writes
to the host credential store: an OAuth token refreshed inside the container is
not written back, and the host CLI re-refreshes on its next use. Ralph runs
Codex with --ephemeral, so stage session transcripts are not persisted
either.

GitHub login (ralph-ghafk only)

Skip this section when you use only ralph-afk. For ralph-ghafk, authenticate
GitHub regardless of whether you selected Claude or Codex. Ralph renders issue
data with the host gh command, then mounts the same configuration read-only at
/home/agent/.config/gh for the stage.

Linux / macOS / WSL bash
export GH_CONFIG_DIR="$HOME/.config/gh"
mkdir -p "$GH_CONFIG_DIR"
gh auth login
gh auth status
Windows PowerShell
$env:GH_CONFIG_DIR = "$HOME\.config\gh"
New-Item -ItemType Directory -Force $env:GH_CONFIG_DIR | Out-Null
gh auth login
gh auth status

Native Windows gh otherwise defaults to its AppData directory, which Ralph
does not mount. Keep GH_CONFIG_DIR set when you invoke ralph-ghafk from this
PowerShell session; set it again before the invocation if you open a new one.
On Linux, macOS, and WSL, keep the exported GH_CONFIG_DIR in the same shell for
gh auth status and ralph-ghafk; export it again in a new shell before either
command. This pins gh to the configuration directory Ralph mounts even when
XDG_CONFIG_HOME differs.

For gh auth login pick: GitHub.comHTTPSY (authenticate Git) →
Login with web browser. Copy the one-time code, open
https://github.com/login/device in the host browser, paste it, and approve.

Verify back on the host

Verify the credentials for the provider you selected.

Claude credentials

Linux / macOS / WSL:

ls -la ~/.claude/.credentials.json ~/.claude.json

PowerShell:

Get-ChildItem "$HOME\.claude\.credentials.json","$HOME\.claude.json"
Codex credentials
codex login status

Because cli_auth_credentials_store = "file", verify that a successful login
also created the credential file without printing its reusable secret.

ls -la ~/.codex/auth.json
GitHub credentials (ralph-ghafk only)

Linux / macOS / WSL:

export GH_CONFIG_DIR="$HOME/.config/gh"
gh auth status

PowerShell:

$env:GH_CONFIG_DIR = "$HOME\.config\gh"
gh auth status

Run the matching command from the same shell context as Ralph. On PowerShell,
keep GH_CONFIG_DIR set for the subsequent ralph-ghafk invocation. These
commands verify the active GitHub account without displaying the reusable
credential stored in hosts.yml.

Re-login / token expired

Re-run claude /login inside the container, codex login from the matching host
shell, or the host gh auth login flow above as appropriate. Provider login
updates the selected provider's writable host store; host GitHub login updates
~/.config/gh, which Ralph later mounts read-only.


ralph-afk — plan/PRD loop

Usage

ralph-afk "<plan-and-prd>" <iterations>

(Or via the shim: ./node_modules/@daonhan/ralph/scripts/afk.sh "<plan-and-prd>" <iterations>.)

Also supports:

  • ralph-afk --help (or -h) — usage, flags, env vars.

  • ralph-afk --version (or -V) — print bin + core version and exit.

  • ralph-afk --print-config — print resolved workspace / docker context / image / docker-socket status and exit. Use for diagnostics before launching a real loop.

  • <plan-and-prd> — a single string forwarded verbatim as {{ INPUTS }} in the template. Conventionally paths to plan and PRD files.

  • <iterations> — max loop iterations. Exits early if implementer emits the sentinel.

Example

ralph-afk "./docs/plans/inventory.md ./docs/prd/PRD-Inventory.md" 10

From PowerShell on Windows:

wsl bash -c "ralph-afk './docs/plans/inventory.md ./docs/prd/PRD-Inventory.md' 10"

What happens per iteration

  1. Render template packages/core/templates/afk.md:
    • !?`git log -n 5 …|||No commits found` → recent commits (try-shell)
    • {{ INPUTS }} → the plan/PRD string
    • @include:prompt.md → the agent playbook (inlined by the Node renderer, no shell)
  2. Implementer stage (gate) — docker run ralph-sandbox <selected-agent> … with the rendered prompt streamed in via a tempfile under .ralph-tmp/ (avoids Windows 32 KB argv limit). Provider events are normalized and rendered live; the terminal completion is captured.
  3. Sentinel check — if the completion contains <promise>NO MORE TASKS</promise>, print Ralph complete after <N> iterations. and exit 0.
  4. Reviewer stage — runs packages/core/templates/review.md. Reads the HEAD commit (the git show --stat summary inline, the full patch spilled to .ralph-tmp/spill-…/head.diff via @spill?:head.diff), then either commits a fix(review): … patch or emits <review>OK</review> / <review>SKIP</review> and stops. Single pass; never amends the implementer's commit.

ralph-ghafk — GitHub-issue loop

Usage

export GH_CONFIG_DIR="$HOME/.config/gh"
ralph-ghafk <iterations>

No plan/PRD arg — context comes from open GitHub issues.

What happens per iteration

  1. Render template packages/core/templates/ghafk.md:
    • !?`git log -n 5 …|||No commits found` → recent commits (try-shell)
    • !?`gh issue list --state open --limit 50 --json number,title,labels|||[]` → a lean inline index of open issues (number / title / labels)
    • @spill?:issues.json=`gh issue list … --json number,title,body,labels,comments` → full issue bodies + comments written to .ralph-tmp/spill-…/issues.json; the agent Reads that file before picking a task
    • @include:ghprompt.md → the agent playbook (inlined by the Node renderer, no shell)
  2. ghafk-implementer stage (gate) — agent picks one open AFK issue, implements it, commits, closes / comments on the issue.
  3. Sentinel check — same as ralph-afk.
  4. Reviewer stage — same as ralph-afk.

Running AFK

Both bins are designed to chew through long runs unattended. Five AFK flags wire that up:

Flag Default What it does
--no-keep-alive off (wake-lock acquired) Skip the OS wake-lock for the loop's lifetime.
--max-retries <N> 3 Per-stage retry budget on transient failures. 0 restores fail-fast.
--detach off Fork the loop into a background process, print pid + log path, and exit.
--log <path> <workspace>/.ralph-tmp/logs/detached-<parent-pid>.log Override the detached log target. Only meaningful with --detach.
--notify off OS toast + terminal bell on loop completion or unrecoverable failure.

Canonical overnight recipe:

ralph-afk --detach --notify "<plan-and-prd>" 50

This forks into the background, holds an OS wake-lock so the host doesn't sleep, retries transient stage failures up to 3× with exponential backoff (5s / 30s / 2m), and raises a toast + bell when the run finishes (sentinel hit or iteration cap reached) or fails (signal, uncaught exception). Tail the log from any shell:

tail -f <workspace>/.ralph-tmp/logs/detached-*.log

Full per-OS notes (wake-lock mechanism, BurntToast install, WSL2 caveat, etc.) live in docs/keep-alive.md.


Consuming the package in another repo

Global install (recommended — run from anywhere)

npm i -g @daonhan/ralph

After install, both bins are on your $PATH:

cd /path/to/some/workspace
ralph-afk "<plan-and-prd>" 5
ralph-ghafk 5

The bundled Dockerfile (shipped inside @daonhan/ralph-core) is the default RALPH_DOCKER_CONTEXT, so the docker build fallback works even when you invoke from a workspace that has no Dockerfile of its own.

Per-repo install

# in your workspace repo
npm i -D @daonhan/ralph         # or: pnpm add -D @daonhan/ralph
./node_modules/.bin/ralph-afk "<plan-and-prd>" 5

Bootstrap on demand (no install)

npx -y @daonhan/ralph ralph-afk "<plan-and-prd>" 5

Environment variables

Variable Default Purpose
RALPH_WORKSPACE process.cwd() Host path bind-mounted at /home/agent/workspace. Also where .ralph-tmp/ is written.
RALPH_DOCKER_CONTEXT bundled @daonhan/ralph-core dir Build context for the docker build fallback. Only consulted if docker pull fails. Must contain Dockerfile. Defaults to the npm-installed core dir, which ships Dockerfile.
RALPH_IMAGE docker.io/daonhan/ralph-sandbox:latest Full image reference. ensureImage does inspectpullbuild (fallback).
RALPH_IMAGE_TAG (legacy) Deprecated alias for RALPH_IMAGE. Honored if RALPH_IMAGE unset.
RALPH_AGENT claude Agent fallback when --agent is absent: claude or codex.
RALPH_RESULT_GRACE_MS 30000 Milliseconds to wait after the provider completion event before force-killing a docker child that fails to exit on its own. 0 disables the timer (original wait-forever behavior). Invalid values (non-finite, negative) fall back to the default.
RALPH_DOCKER_SOCK (on if a socket is found) Set to 0 to disable bind-mounting the host Docker socket into the sandbox. Mounted by default so Testcontainers inside the container can spawn sibling containers — this grants the sandbox root-equivalent access to the host Docker daemon. Disable when running untrusted prompts.
RALPH_DOCKER_SOCK_PATH (auto-detected) Explicit host docker.sock path. Auto-detection (when unset) tries DOCKER_HOST (unix:// only), then /var/run/docker.sock, Docker Desktop, Colima, Rancher Desktop, and rootless Docker/Podman socket locations.
RALPH_MODEL Claude claude-opus-5[1m]; isolated Codex uses gpt-5.6-sol/high Model override for the selected agent. Claude falls back to the model pinned in host ~/.claude/settings.json, then Ralph's own default instead of the sandbox CLI's frozen one — except under third-party routing (CLAUDE_CODE_USE_BEDROCK/_VERTEX/_FOUNDRY), where the container CLI still resolves.
DOCKER_HOST (unset) A unix:///… value is parsed for the docker-socket bind-mount; tcp:// / npipe:// / ssh:// are not bind-mountable.
XDG_RUNTIME_DIR (unset) Searched for rootless Docker/Podman sockets during auto-detection.
NO_COLOR / TERM=dumb (unset) Disable ANSI color in Ralph's own output. Color is also auto-disabled when stdout/stderr is not a TTY, so piping to a file stays clean.

Local development (this monorepo)

Full contributor guide — dev loop, tests, adding a stage, releasing — lives in CONTRIBUTING.md. The essentials:

pnpm install                          # links workspace, hoists devDeps
pnpm -r build                         # compiles packages/core/dist
pnpm -r typecheck                     # no-emit type check
pnpm -r test                          # packages/core runs `vitest run` (apps/cli has no tests)
pnpm test                             # root: `node --test` over scripts/*.test.mjs

A husky pre-commit hook runs lint-staged (prettier --ignore-unknown --write on staged files) then pnpm typecheck on every commit.

Build artifacts

  • packages/core/dist/ — compiled .js + .d.ts. Required for both pnpm pack and pnpm publish.
  • apps/cli has no build step — bin shims are hand-written JS.

Pack tarballs (smoke-test before publish)

(cd packages/core && pnpm pack --pack-destination /tmp)
(cd apps/cli      && pnpm pack --pack-destination /tmp)

# Install both in a throwaway repo to verify the published artifacts work
mkdir /tmp/ralph-test && cd /tmp/ralph-test
npm init -y
npm i -D /tmp/daonhan-ralph-core-*.tgz /tmp/daonhan-ralph-*.tgz
./node_modules/.bin/ralph-afk           # → prints usage

Global install from local checkout (dev shortcut)

pnpm link --global is brittle inside this workspace (pnpm 9 rewrites the dependent's manifest). Use the pack-then-install path instead:

pnpm -r build
(cd packages/core && pnpm pack --pack-destination /tmp/ralph-packs)
(cd apps/cli      && pnpm pack --pack-destination /tmp/ralph-packs)
npm i -g /tmp/ralph-packs/daonhan-ralph-core-*.tgz \
         /tmp/ralph-packs/daonhan-ralph-*.tgz
ralph-afk          # → Usage: ralph-afk <plan-and-prd> <iterations>

Re-run after each source change. To uninstall: npm uninstall -g @daonhan/ralph @daonhan/ralph-core.

Publish

Publishing is automated — you don't run pnpm publish by hand. Land work on main with Conventional Commits; release-please opens one Release PR per component, and merging that PR cuts the component tag (ralph-core-v* / ralph-v* / ralph-sandbox-v*) that triggers publish-npm.yml / publish-image.yml. See RELEASING.md for the full flow, required secrets, version policy, and rollback runbook.

Escape hatch (only if the pipeline is unavailable):

pnpm -r publish --access public   # topological order; workspace:^ rewritten to semver

Use a local checkout in another repo (no publish)

Use the pack-then-install path above. It exposes ralph-afk / ralph-ghafk globally; no per-workspace step needed.


Customizing the pipeline

Add a stage

  1. Add an entry to STAGES in packages/core/src/stages.ts:
    linter: { name: "linter", template: "lint.md", permissionMode: "bypassPermissions" } satisfies Stage,
    
  2. Create packages/core/templates/lint.md using the same !`cmd` + {{ INPUTS }} syntax.
  3. Wire it into the chain in main.ts / gh-main.ts:
    stages: [STAGES.implementer, STAGES.linter, STAGES.reviewer],
    
  4. pnpm -r build and republish.

Only the first stage is the gate (sentinel-checked). Subsequent stages always run after a non-sentinel gate result. Ralph runs the selected provider without interactive approval (permissionMode: "bypassPermissions" for Claude; --dangerously-bypass-approvals-and-sandbox for Codex). With the Docker socket disabled, persistent host-write exposure still includes the workspace mount and, for Claude, the read-write credential store (Codex credentials are mounted read-only); GitHub CLI config is read-only.

Change the template syntax

Renderer is in packages/core/src/render.ts. Tags supported today:

  • !`<shell cmd>` — executed via bash (Linux/macOS/WSL/Git Bash) or cmd.exe (Windows native fallback) with cwd = workspaceDir. stdout (trailing newline trimmed) replaces the tag. Failures throw and abort the iteration.
  • !?`<shell cmd>|||<fallback>` — try-shell. Same as ! but stderr is suppressed and a non-zero exit returns the literal fallback string. Use this for cross-platform safety — avoids depending on shell-specific 2>/dev/null || echo "…" idioms.
  • @spill[?]:<name>=`<shell cmd>[|||<fallback>]` — run <cmd> and write its stdout to a file <name> in the per-stage spill dir (.ralph-tmp/spill-…/), substituting the container-relative path ./.ralph-tmp/spill-…/<name> into the prompt for the agent to Read. The ? form suppresses stderr and writes <fallback> on non-zero exit; <name> must be a plain filename (no path separators, no ..). Use for large outputs that would bloat the prompt — review.md spills the full HEAD patch, ghafk.md the full issue bodies.
  • @include:<rel-or-abs-path> — inline a file (via Node readFileSync). Path resolved against the template's own directory when relative. No shell. Use this for bundled playbooks, not for live shell output.
  • {{ INPUTS }} — replaced with the inputs field passed into runLoop.

Tags expand in a fixed order: @include@spill!?!{{ INPUTS }}.

On Windows, the renderer prefers bash.exe (Git for Windows / WSL passthrough) over cmd.exe. The !? tag makes commands tolerant either way.

Override the image

Set RALPH_IMAGE=registry.example.com/my-image:tag before invoking the shim, or edit the default in packages/core/src/runner.ts. The runner does inspectpullbuild against whatever ref is set; legacy RALPH_IMAGE_TAG still works for backward compatibility.

Change feedback loops or task priority

The agent playbooks are self-contained: packages/core/templates/prompt.md (plan/PRD source + progress recording, for ralph-afk) and ghprompt.md (issue triage + close/comment, for ralph-ghafk). Each carries its own task-priority ladder, feedback loops, commit rules, and final rules. afk.md / ghafk.md each @include their respective playbook. Edit the playbook for a loop to change its task priority or feedback loops.


Stopping a run

  • Natural stop: implementer emits <promise>NO MORE TASKS</promise>.
  • Manual stop: Ctrl+C. runLoop installs SIGINT / SIGTERM handlers that abort the active stage (via AbortController, killing the docker child), release the OS wake-lock, fire the --notify toast if enabled, and exit 130 (SIGINT) / 143 (SIGTERM). Tempfiles under .ralph-tmp/.run-*.md and the per-stage spill-*/ dir are removed by the finally block in runner.ts; a hard SIGKILL may leave them — safe to delete, gitignored.

Troubleshooting

  • Cannot find module '@daonhan/ralph-core'@daonhan/ralph was installed but its dep didn't resolve. Re-run npm install (or pnpm install) in the workspace, or use npx -y @daonhan/ralph to let npx fetch a clean copy.
  • @esbuild/win32-x64 package is present but this platform needs @esbuild/linux-x64node_modules/ installed from the wrong OS. Delete node_modules/ + lockfile and reinstall under WSL.
  • Not logged in · Please run /login — Claude credentials are missing inside the container. Run the interactive docker run … claude /login step from "First-run setup".
  • Codex reports that login is missing — ensure cli_auth_credentials_store = "file", run codex login from the same shell environment as Ralph (per the same-shell rule), and confirm codex login status succeeds and ~/.codex/auth.json exists in that environment's home.
  • Codex fails with Operation not permitted (os error 1) / EPERM at startup — the container's CODEX_HOME is sitting on a Windows bind mount, which cannot host the unix socket and symlinks Codex creates at startup. Current Ralph avoids this by copying credentials into a container-local CODEX_HOME; upgrade @daonhan/ralph if you see this.
  • Codex config, MCP servers, or hooks are missing — isolated Codex intentionally ignores ~/.codex/config.toml; opt in with --codex-user-config and ensure configured commands and paths work inside Linux Docker.
  • An explicit Codex model fails — fix or remove RALPH_MODEL. Ralph does not silently fall back to gpt-5.6-sol or another model after an explicit model failure.
  • The Claude stage fails on the model itself (unknown model, or one your plan cannot use) — Ralph sent its own default because neither RALPH_MODEL nor your host ~/.claude/settings.json pinned one. Run ralph-afk --print-config to see the model and where it came from, then set RALPH_MODEL=<model you have access to> or pick an explicit (non-"(default)") entry in /model.
  • gh issue list fails with not a git repository — the workspace has no .git. The ghafk.md template uses || echo "[]" fallback so the iteration still proceeds, but gh cannot detect the target repo. Initialize the repo, or push first.
  • MSB3248 during dotnet build / dotnet test — virtiofs/9p quirk on Windows-mounted source. The agent retries automatically per the recipe in packages/core/templates/prompt.md; manual repro:
    dotnet test <path-to-test-csproj> \
      -m:1 \
      /p:UseSharedCompilation=false \
      /p:BuildInParallel=false \
      /p:BaseIntermediateOutputPath=/tmp/ralph-obj/<name>/ \
      /p:BaseOutputPath=/tmp/ralph-bin/<name>/
    
  • docker run exit 1 with no selected-agent output — image stale. Force refresh:
    docker rmi docker.io/daonhan/ralph-sandbox:latest
    docker pull docker.io/daonhan/ralph-sandbox:latest
    
  • docker pull failed … and no Dockerfile at … — the default image ref isn't reachable (offline, registry down, or you set a custom $RALPH_IMAGE that doesn't exist) AND no Dockerfile is at $RALPH_DOCKER_CONTEXT. Fix one of: connectivity, RALPH_IMAGE, or place a Dockerfile at $RALPH_DOCKER_CONTEXT.
  • pull access denied … repository does not exist$RALPH_IMAGE points at a private repo or a typo. Either docker login, switch to a public image, or unset RALPH_IMAGE to use the default.
  • Loop hangs after a stage's final assistant message (no next iteration, no error) — the selected CLI inside the sandbox emitted its completion event but failed to exit. After RALPH_RESULT_GRACE_MS (default 30000ms), the runner kills the lingering docker child, keeps the captured completion, and continues the loop. Bump or disable the timer via the environment when diagnosing. To inspect or stop the container manually before the timer expires:
    docker ps --filter ancestor=docker.io/daonhan/ralph-sandbox:latest
    docker kill <container-id>
    
    The sandbox runs with --rm, so the container is removed after it exits.

Files in this folder

File / dir Purpose
apps/cli/scripts/afk.sh Optional shim — plan/PRD loop. Falls back to npx @daonhan/ralph ralph-afk. Shipped in the npm tarball.
apps/cli/scripts/ghafk.sh Optional shim — GitHub-issue loop. Calls ralph-ghafk.
packages/core/templates/prompt.md Agent playbook for ralph-afk. Shipped in core tarball.
packages/core/templates/ghprompt.md Agent playbook for ralph-ghafk. Shipped in core tarball.
packages/core/templates/Dockerfile Builds ralph-sandbox image: Node 22 + Python 3.11/venv + uv/uvx + .NET SDK 10 + gh + Claude Code + pinned Codex CLI. Shipped in @daonhan/ralph-core tarball.
.dockerignore Shrinks build context (consumed at repo root for CI builds).
package.json Monorepo root (private). Shared devDeps + pnpm workspace scripts.
pnpm-workspace.yaml Declares apps/* and packages/* as workspace members.
tsconfig.base.json Shared TS compiler options inherited by every package.
apps/cli/ @daonhan/ralph — CLI bin entries (ralph-afk, ralph-ghafk).
packages/core/src/main.ts Exports runAfk(argv).
packages/core/src/gh-main.ts Exports runGhAfk(argv).
packages/core/src/loop.ts Iteration driver. Runs stage chain; first stage is the gate.
packages/core/src/render.ts Template renderer (!`cmd` + {{ INPUTS }}).
packages/core/src/runner.ts docker run wrapper + NDJSON stream + credential mounts. Image lookup: inspect → pull → build. Reads RALPH_IMAGE.
.github/workflows/publish-image.yml CI: build + push linux/amd64 ralph-sandbox to Docker Hub on workflow_dispatch, ralph-sandbox-v* tag (release-please primary), or legacy image-v* tag.
.github/workflows/publish-npm.yml CI: publish @daonhan/ralph-core / @daonhan/ralph to npm on ralph-core-v* / ralph-v* tags; enriches the GitHub Release with the .tgz, SBOM, and cosign attestation.
.github/workflows/release-please.yml CI: on push to main, opens a per-component Release PR; merging it cuts the tag that triggers the publish workflows.
RELEASING.md Single source of truth for releasing all three components (npm packages + image): release-please flow, version policy, secrets, rollback runbook.
CONTRIBUTING.md Maintainer / contributor guide: dev loop, tests, adding a stage, release pipeline.
QUICKSTART.md Zero-to-first-loop getting-started guide for new users.
docs/ARCHITECTURE.md Internals / runtime data-flow reference for library extenders and core contributors.
packages/core/src/cli-help.ts Flag parsing (parseFlags); --help / --version / --print-config output.
packages/core/src/retry.ts withRetries — per-stage retry with exponential backoff (default 3).
packages/core/src/keepalive.ts OS wake-lock acquire/release for the loop's lifetime (--no-keep-alive to skip).
packages/core/src/detach.ts --detach fork-and-exit into a background process.
packages/core/src/notify.ts --notify OS toast + terminal bell on loop terminal events.
packages/core/src/stages.ts Stage registry — implementer, ghafkImplementer, reviewer.
packages/core/src/index.ts Barrel re-export — runAfk, runGhAfk, runLoop, STAGES, renderTemplate, …
packages/core/templates/afk.md ralph-afk prompt template.
packages/core/templates/ghafk.md ralph-ghafk prompt template.
packages/core/templates/review.md Reviewer prompt template.

License

MIT (c) Paul Nguyen.

Reviews (0)

No results found