Orchestrator and scheduler
orch.py contains the CLI, the Orchestrator class (the run loop), the serial merge, the
wizard and the watch agent. All configuration is read from ralph_orchestrator/config.yaml
relative to the kit directory; the target repository is the git toplevel of the current
working directory (git rev-parse --show-toplevel, falling back to the cwd).
The run loop
cmd_run loads config, tickets and state, consumes e-mail replies captured before the run
(best-effort: it never blocks the run), validates the ticket DAG, optionally disables
consensus (--no-consensus) or demands a passing preflight (--preflight), then runs the
loop:
integration worktree, .ralph/ excluded,
plugins wired] --> B[write run report HTML] B --> C{ready tickets
fit the limit?} C -->|yes| D[launch: worktree, worker thread
(contract first if enabled)] C -->|no, none running| E[finish] D --> F[wait poll_seconds
for any worker] F --> G[process_bus:
auto-answers, change requests] G --> H[finish(done worker):
merge or retry/block] H --> I[save state, regenerate report] I --> J{timer due?} J -->|reply_poll_seconds| K[poll IMAP for
operator replies] J -->|every_seconds| L[periodic status mail] K --> C L --> C J -->|no| C E --> M[reason + final report mail
exit 0 / 2 / 1]
-
Ready set — a ticket is ready when its status is
pendingand alldepends_onaremerged. The ready list is sorted by critical length (length of the longest dependent chain below it — the most urgent first), then by id. -
Dynamic agent limit — pending tickets are weighted by complexity (S = 1, M = 2, L = 3); one agent can absorb 2 weight units, so
limit = max(1, total_weight // 2), capped byorchestrator.agents_ceiling.--max-agents Noverrides the cap for the run. -
Scope-overlap serialisation — a ready ticket whose
scopeglobs overlap any currently running ticket’s scopes waits; parallel agents never edit the same files (graph.scopes_overlap: prefix match on the non-wild part +fnmatchboth ways). -
Contract first — when
collaboration.contractsis enabled, a wave of ≥ 2 parallel tickets negotiates its interface contract before launch (collaboration.adoc#contracts). -
Bus — each loop iteration processes new bus messages in the main thread (collaboration.adoc#bus).
-
Mail — a periodic check-in goes out every
report.every_seconds; operator replies are polled from IMAP everyreport.reply_poll_secondswhile the run is active (mail-loop.adoc#in-run). -
Halt — on Ctrl-C the orchestrator kills every child process group (
proc.kill_all), shuts down the pool and recordsINTERRUPTED.
Finishing a ticket
When a worker reports:
-
infrastructure failure (the agent output contains
connection refused/econnrefused/could not resolve): the attempt is not charged to the ticket and the run counters are decremented. 20 consecutive infra failures halt the run withAPI_DOWN(single failures retry afterapi_retry_sleep, default 60 s). -
budget exhausted (
orchestrator.max_total_agent_runsreached): the run halts withBUDGET. -
ok: the orchestrator serially merges the ticket branch.
-
not ok: attempts < max →
pendingwith the failure feedback (next loop); attempts exhausted →blocked(its dependents wait). The feedback’s first line is printed.
Merging
into _integration worktree] --> B{conflict?} B -->|yes| C[merge --abort
ticket back to pending/blocked
with the conflict as feedback] B -->|no| D{project.test_cmd
configured?} D -->|yes| E[run regression suite
on integration worktree] E -->|fail| F[git reset --hard HEAD~1
merge undone; ticket retried
with the test output as feedback] E -->|pass| G[merged ✓] D -->|no| G G --> H{orchestrator.push?} H -->|true| I[git push remote
ralph/integration] H -->|false| J[branch stays local]
Merges are the only writes to ralph/integration, and they happen in the main thread only.
Exit codes and final reason
| Final reason | Exit code | Meaning |
|---|---|---|
|
0 |
every ticket merged |
|
2 |
at least one ticket exhausted its attempts; dependents never ran |
|
1 |
run ended with no ready, no running, and unfinished work that cannot be scheduled |
|
1 |
|
|
1 |
20 consecutive infrastructure failures |
|
1 |
Ctrl-C (children killed, state saved, report written) |
The reason, the counters and the full event list are persisted in state.json
(data-models.adoc#state-json), and the final report mail is sent with the HTML (and,
when present, PDF) run report attached (mail-loop.adoc#mails).
Re-work (rework, used by the watch agent)
A bounded pass that re-attempts pending tickets only — merged and running tickets are never relaunched, their history is preserved, and events are appended, never wiped. It declares no final reason and sends no mail; the caller (watch) owns the loop.
The wizard
orch.py wizard is an interactive setup that hard-stops (WIZARD STOPPED) at the first
real problem, in this order:
-
Prints the banner art (
ralph_orchestrator/templates/ralph_wizard.ans, versioned) and asks Did you already fill config.yaml correctly? — "n" stops with a pointer to the file. -
wizard_problemsconfig sanity:agent.cmdnon-empty,agent.modelsS/M/L present,project.base_branchexists as a git branch,llm.plannerandllm.reviewerspresent, gateway key resolvable (configuration.adoc#gateway). Any missing item stops the wizard with the list of problems. -
Agent CLI check — the first token of
agent.cmdmust be on PATH and answer--version. If it is missing, the wizard asks for youropencode.json(or the binary), parses itsprovidersection (parse_opencode_providers:options.baseURL/options.apiKey,$ENVreferences resolved) and tests each provider endpoint for reachability (see below); a failed endpoint stops the wizard. If the binary itself is found but not on PATH, the wizard offersnpm install -g opencode-aiand stops until the CLI is usable. -
Gateway check —
test_endpointon the planner’s base URL with strict mode: the gateway must answerGET <base>/modelswith 200, or (gateways without/models, HTTP 400/404/405) a 1-tokenchat/completionsprobe with 200. HTTP 401/403 = key rejected → stop. -
Install —
_pip_ensureimportsyaml,langchain_core,langchain_openaiand pip installsrequirements.txtfor whatever is missing; then_install_plugins(below).
test_endpoint semantics (used by the wizard, the --ping preflight and provider checks):
| Response | Verdict |
|---|---|
|
reachable |
401 / 403 on |
reachable but key rejected |
400 / 404 / 405 on |
fall through to the 1-token chat probe |
chat probe → 200 |
reachable (strict) / reachable, key accepted (non-strict) |
chat probe → 401/403 |
key rejected |
chat probe → other error |
reachable in non-strict mode, not in strict |
Agent plugins
The agents the loop spawns are real opencode run sessions, so they load opencode plugins
natively. ensure_opencode_plugins is run on every orch.py run (idempotent,
best-effort — a missing npx or a read-only config never blocks a run) and by
orch.py install:
-
Project npm plugins —
agent.plugins(default["@dietrichgebert/ponytail"]) are merged into the target repo’s.opencode/opencode.jsonpluginlist, preserving existing entries and keys. OpenCode reads that file for everyopencode runin the repo. -
Caveman —
agent.caveman: trueruns the official one-time global installer (npx -y github:JuliusBrussee/caveman — --only opencode, target~/.config/opencode/plugins/caveman) when not already installed; a run without it prints a hint and continues.
The dashboard’s Plugins view shows exactly what OpenCode would load — project config, global config, and local plugin directories — and lists expected-but-missing plugins with an install hint (no fabrication: only what exists on disk is reported active).