Containerized autonomous plan execution for one or more target repositories, deployable on Coolify.
It wraps umputun/ralphex (the "extended Ralph loop") and drives it through umputun/fya so unattended runs stay on the Claude Max plan instead of the Agent-SDK credit pool. Each plan task runs in a fresh Claude session, gets validated, browser-verified with agent-browser, code-reviewed by Claude, and shipped as a GitHub PR.
Status: deployment scaffold. The image + Coolify wiring below are the focus; a couple of items are flagged VERIFY and the voice "ask-human" escalation is not built yet.
| Tool | Role |
|---|---|
| Claude Code + fya | task execution & reviews on the Max plan (fya = PTY wrapper for headless interactive Claude) |
| ralphex | the loop: tasks → validation → review → finalize/PR |
| agent-browser (+ Chrome) | per-task browser verification against the app's dev server |
| pnpm, gh, git, ripgrep | toolchain |
A long-running worker container:
- Clones every repo in
REPOS(comma-separated) into a persistent volume on first boot,pnpm installs each. - Serves the ralphex dashboard on
:8080(Coolify maps a domain). - Every poll it fetches +
git pull --ff-onlyeach repo's base branch, so plans/code pushed to the repo are picked up without a restart and each run starts from the latest base. - Watches each repo's
docs/plans/*.md— drop (or push) a plan in and it executes once per file content: implement → validate → agent-browser check → commit → review → open PR.
REPOS entries are name=URL[#branch] or just URL, e.g.
REPOS="app=https://github.com/your-org/your-repo.git#main,api=https://github.com/your-org/api.git".
Each repo is cloned to /workspace/<name>/ and runs independently. The image is generic — per-repo behavior (dev-server URL, the agent-browser check, whether to open a PR) lives in each repo's own .ralphex/ config, so commit an .ralphex/ into every target repo.
A plan is plain markdown:
# Plan: My Feature
## Validation Commands
- `pnpm test`
### Task 1: Do the thing
- [ ] implement X
- [ ] add testsFiles without a ### Task N: or ### Iteration N: section are treated as non-executable notes and skipped (otherwise ralphex fails them on every poll). Each plan runs once per content — its outcome is recorded under the repo's .ralphex/plan-state/, so it is not re-picked-up on the next poll. To re-run a completed or failed plan, edit the file so its content hash changes.
There's no macOS keychain on the server, so auth is token-based:
# Claude (Max plan, headless): mint a portable OAuth token
claude setup-token # -> CLAUDE_CODE_OAUTH_TOKEN (VERIFY this keeps you on Max, not SDK credits)
# GitHub: a token with 'repo' + 'workflow' scope -> GITHUB_TOKEN- Create the resource. Two methods; B is recommended — Coolify has open bugs where the compose build context arrives empty even for git-based deploys (coolify#6002, #5182), so letting Coolify build is flaky. The prebuilt image avoids building in Coolify entirely.
- B — Prebuilt image (recommended): the
build-imageAction pushesghcr.io/luiskisters/executr:lateston every push tomain. Make that package pullable by Coolify — set it public (GitHub → your profile → Packages → executr → Package settings), or add aread:packagestoken as a registry credential in Coolify. Then deploydocker-compose.yml(it already references the image) via Docker Compose or Empty Docker Compose — no build context needed. - A — Let Coolify build (fallback): New Resource → Docker Compose → Private Repository
luisKisters/executr, branchmain, composedocker-compose.yml, and replace theimage:line withbuild: .. May hit the build-context bug above. - Never use Empty Docker Compose with
build: .— no Dockerfile in context, so it fails withopen Dockerfile: no such file or directory(the original error).
- B — Prebuilt image (recommended): the
- Environment Variables — set these (secrets where sensitive); see
.env.example:CLAUDE_CODE_OAUTH_TOKEN,GITHUB_TOKEN(required)REPOS— comma-separated repos,name=URL[#branch](falls back toREPO_URL/REPO_BRANCHif unset)GIT_AUTHOR_NAME/GIT_AUTHOR_EMAIL- optional:
GROQ_API_KEY,TELEGRAM_BOT_TOKEN,RALPHEX_WEB_HOST(dashboard bind address; defaults to0.0.0.0so Coolify's proxy can reach it)
- Storage — the named volume
executr_repopersists the clone,docs/plans/, and.ralphex/state across redeploys. - Domain — point one at port
8080for the dashboard. - Deploy. First boot is slow (clone +
pnpm install+ Chrome already baked in).
Drop a markdown plan into /workspace/<name>/docs/plans/ (per repo in REPOS) — via Coolify's container terminal, a committed file in the target repo, or the mounted volume. The loop picks it up; watch the dashboard; the PR lands on GitHub.
- MCPs — the target repo's
.mcp.jsonrides along in the clone; Claude-via-fya auto-loads it. MCP servers needing keys → add those as Coolify env vars. - Skills — commit Claude skills into the repo, or bake them into the image (
COPYinto$CLAUDE_CONFIG_DIR). agent-browser skills ship with the CLI. - Envs — the app's
.env.localrides in the clone (or inject values as Coolify secrets, which is cleaner than committing).
- Claude token billing —
claude setup-tokenis subscription-billed (Max) per Anthropic's docs; still worth a trivial smoke-test on first deploy. - Release assets / build — the Dockerfile resolves the latest
fya/ralphexversions at build time and builds green in CI; runs non-root asnode(Claude refuses--dangerously-skip-permissionsas root). - Per-repo
.ralphex/— each target repo needs its own.ralphex/(config + prompts) committed, or it runs with ralphex defaults (no browser gate, no auto-PR). - Dashboard idle — verify
ralphex --serve --watchruns without prompting on your ralphex version. - Voice ask-human — not wired yet;
TELEGRAM_BOT_TOKENhere only powers ralphex's built-in notifications for now.
cp .env.example .env # fill in tokens
docker compose up --build
# dashboard: http://localhost:8080This is the deployment layer of the "executr" idea — adopting ralphex + fya rather than building an orchestrator from scratch (XML/markdown plans, per-phase browser gate, Telegram voice escalation).