Data models and file formats
tickets.yaml
The plan: a list of tickets under a top-level tickets: key
(ralph_orchestrator/tickets.yaml, generated by orch.py plan or
hand-written; example in templates/tickets.example.yaml).
| Field | Required | Type | Meaning |
|---|---|---|---|
|
yes |
string |
stable identifier, unique in the file ( |
|
yes |
string |
one-line imperative description |
|
yes |
string (shell) |
executable acceptance command; exits 0 only when the work is done; run by the orchestrator in the ticket’s worktree |
|
yes |
list of globs |
the only paths the ticket may change; |
|
no |
string |
what to change and why (default |
|
no |
list of ids |
upstream tickets that must be merged first (default |
|
no |
|
picks the agent model ( |
|
no |
int |
per-ticket attempt budget; |
|
no |
list of strings |
checkable acceptance criteria injected into the agent prompt and the reviewer prompt (default |
Example (real file, templates/tickets.example.yaml):
tickets:
- id: T02
title: Backend change
description: Extend src/service.py (do not rewrite it) following the design note.
criteria: [new behaviour covered by an integration test that persists and reloads data]
depends_on: [T01]
complexity: M
scope: [src/service.py, tests/new/test_service.py]
accept: python3 -m pytest -q tests/new/test_service.py
max_attempts: 4
Validation (graph.validate, run by orch.py check, plan, run, watch): no empty
file, no duplicate ids, non-empty accept and scope, known dependency ids, no
self-dependency, no dependency cycle, complexity ∈ {S, M, L}.
state.json
.ralph/state.json — the single source of truth (Architecture). Written
atomically (tmp + rename) under a lock; the heartbeat refreshes on every save.
{
"started": 1761012000.0, // epoch, run start
"heartbeat": 1761012345.6, // epoch, last save
"pid": 50368, // orchestrator pid
"runs": 12, // total agent invocations (decremented on infra failures)
"peak_agents": 2, // max simultaneous workers observed
"reason": "", // final reason, "" while running (DONE/BLOCKED/STUCK/BUDGET/API_DOWN/INTERRUPTED)
"finished": false,
"bus_seen": 1761012300000000000, // last bus message id processed by the orchestrator
"replies": [ // consumed operator replies (cap 50)
{"id": "<mail-id>", "from": "me@example.com", "date": "...", "subject": "Re: [ralph] ...",
"body": "T4: the key is ...", "at": 1761012300.0, "report_delivered": false}
],
"events": [ ... ], // global event list (cap 500) - see below
"tickets": {
"T1": {
"status": "merged", // pending | running | merged | blocked | failed
"attempts": 1,
"model": "tef-qwen3-8/qwen3.8-27b",
"feedback": "",
"branch": "ralph/T1",
"started_at": 1761012010.0, // first "running" of this cycle
"finished_at": 1761012090.0, // terminal status
"last_attempt_at": 1761012080.0,
"updated_at": 1761012090.0,
"events": [ ... ] // per-ticket events (cap 500)
}
}
}
Events
Every state transition and attempt is recorded (per ticket and globally; last 500 kept):
type |
Fields | When |
|---|---|---|
|
|
any status change, including crash recovery ( |
|
|
a new attempt starts |
|
|
an operator reply consumed for the ticket |
|
|
the run’s final reason |
merged, blocked, failed are the terminal statuses (state.TERMINAL). At load time, a
ticket still marked running (crashed run) is reset to pending with a recorded event —
this is the resume mechanism.
Ticket lifecycle
Note: a change request against a merged ticket never re-opens it — it creates a separate
<id>-fixN ticket that depends on it (collaboration.adoc#change-requests).
inbox.json
.ralph/inbox.json — captured operator replies (list, cap 50, deduped by id):
{
"id": "<message-id>", // or "file-<sha1[:12]>" for --file imports
"from": "me@example.com",
"date": "Wed, 21 Oct 2026 15:06:00 +0000",
"subject": "Re: [ralph] DONE: ...",
"body": "T4: the key is 'exchange_rate', not 'rate'.",
"captured_at": 1761012345.6,
"seen": false, // flipped to true by consume_replies
"consumed_at": null // set when consumed
}
See consumption semantics (ticket mentions, feedback append, blocked-ticket unlock).
dynamic.json
.ralph/dynamic.json — change-request follow-ups created mid-run (collaboration.adoc#change-requests):
{
"tickets": [
{"id": "T2-fix1", "title": "Fix requested by T5 in T2",
"description": "<the request>\n\nOriginal ticket: ...",
"criteria": ["<the request>"], "depends_on": ["T2"], "complexity": "M",
"max_attempts": 4, "scope": ["src/service.py"], "accept": "python3 -m pytest -q ..."}
],
"extra_deps": { "T7": ["T2-fix1"] } // dependencies added to existing pending tickets
}
Applied on every load() and by the dashboard’s snapshot builder, so fix tickets and their
new edges survive restarts and appear in the graph.
Bus messages
.ralph/bus/messages.jsonl — one JSON object per line, flock-protected appends
(format and commands). Per-agent read cursors in
.ralph/bus/cursor/<id>; the orchestrator’s own position in state.bus_seen.
Handoffs
.ralph/handoff/<id>.md — written by the worker after a successful attempt:
Ticket T1: Add a greeting function
Files: greet.py
Agent notes:
<last 2000 chars of the agent's output>
Injected (first 2500 chars each) into the prompts of dependent tickets under Work already merged that you build on, and used by the orchestrator’s automatic bus answers.
Contracts
.ralph/contracts/group<k>.json (k = wave index) — the agreed interface contract of one
parallel group:
{
"group": 1,
"tickets": ["T2", "T3"],
"drafts": {
"T2": {
"provides": [{"name": "greet.farewell", "signature": "farewell(name: str) -> str", "notes": ""}],
"requires": [{"name": "greet.greet", "from": "T1", "signature": "greet(name: str) -> str"}]
}
},
"resolved": ["T2 adapts 'greet.greet' to T1's signature 'greet(name: str) -> str'"],
"decided_by": "consensus", // or "owner (no agreement after rounds)"
"panel": "gpt-oss: APPROVE - ..." // the panel's report
}
plus a rendered group<k>.md. for_ticket extracts the binding part per ticket for its
prompt.