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:

flowchart TD A[ensure_integration:
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 pending and all depends_on are merged. 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 by orchestrator.agents_ceiling. --max-agents N overrides the cap for the run.

  • Scope-overlap serialisation — a ready ticket whose scope globs 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 + fnmatch both ways).

  • Contract first — when collaboration.contracts is 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 every report.reply_poll_seconds while 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 records INTERRUPTED.

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 with API_DOWN (single failures retry after api_retry_sleep, default 60 s).

  • budget exhausted (orchestrator.max_total_agent_runs reached): the run halts with BUDGET.

  • ok: the orchestrator serially merges the ticket branch.

  • not ok: attempts < max → pending with the failure feedback (next loop); attempts exhausted → blocked (its dependents wait). The feedback’s first line is printed.

Merging

flowchart LR A[merge --no-ff ralph/<id>
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

DONE

0

every ticket merged

BLOCKED

2

at least one ticket exhausted its attempts; dependents never ran

STUCK

1

run ended with no ready, no running, and unfinished work that cannot be scheduled

BUDGET

1

max_total_agent_runs reached

API_DOWN

1

20 consecutive infrastructure failures

INTERRUPTED

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:

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

  2. wizard_problems config sanity: agent.cmd non-empty, agent.models S/M/L present, project.base_branch exists as a git branch, llm.planner and llm.reviewers present, gateway key resolvable (configuration.adoc#gateway). Any missing item stops the wizard with the list of problems.

  3. Agent CLI check — the first token of agent.cmd must be on PATH and answer --version. If it is missing, the wizard asks for your opencode.json (or the binary), parses its provider section (parse_opencode_providers: options.baseURL / options.apiKey, $ENV references 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 offers npm install -g opencode-ai and stops until the CLI is usable.

  4. Gateway check — test_endpoint on the planner’s base URL with strict mode: the gateway must answer GET <base>/models with 200, or (gateways without /models, HTTP 400/404/405) a 1-token chat/completions probe with 200. HTTP 401/403 = key rejected → stop.

  5. Install — _pip_ensure imports yaml, langchain_core, langchain_openai and pip installs requirements.txt for whatever is missing; then _install_plugins (below).

test_endpoint semantics (used by the wizard, the --ping preflight and provider checks):

Response Verdict

GET <base>/models → 200

reachable

401 / 403 on /models

reachable but key rejected

400 / 404 / 405 on /models

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.json plugin list, preserving existing entries and keys. OpenCode reads that file for every opencode run in the repo.

  • Caveman — agent.caveman: true runs 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).

Reset

orch.py reset removes every .ralph/wt/ worktree (worktree remove --force + prune), deletes all ralph/ branches except the integration branch (also delete it with --branches), and removes .ralph/ entirely.