rafa pages
- rafa
- rafa loop
rafa loop
Run a plan task by task in Claude Code sessions, then follow, pause, resume, stop, wait on and list the running loops.
rafa loop runs plans. start walks a plan one task at a time, one Claude Code session and one commit per task, then wraps up, opens the pull request and waits for CI. The other actions follow and steer a loop that is already running, from another terminal.
Spends: 🪙 start: one session per task, the wrap-up and its retries, repair sessions while CI is red, and the sessions of --retry and --continue. resume starts no session itself but lets a paused run go on spending. Changes: commits on the plan's branch, the plan's tracker, the run records under .rafa/runs/, a pushed branch and a pull request. Needs: Claude Code, git with a commit identity, and gh to push and open the pull request; see Prerequisites.
Things to know before you use it:
- Read Before you run it: every session runs with
--dangerously-skip-permissions. - A loop guards its checkout: it halts when the branch or HEAD moves from outside. Use
--as-worktreeto keep working in your checkout while it runs (The cycle). - A task that reports
blockedstops the run with the reason; start it again with the same line and the blocked task goes first. - The tests a loop runs at each task, stage and before the wrap-up are the ones
checks.task,checks.stageandchecks.loopname. In 0.47.0 all three are empty by default, so a loop runs no tests and says so; lint and type checks still run.templates: [escalation]restores the checks earlier versions ran (Configuration).
Used by the workflows The cycle, rafa next (start, resume) and Stretches (stretch item).
In the target layout this code lives in the private package rafa-run, bundled into @open-tomato/rafa (Repository layout). In 0.47.0 it is already a package, @open-tomato/rafa-run in packages/rafa-run, but only as a facade: it re-exports this code from the root package's src/, where the files still live until their move. You install and type the same either way. [verify: the release the move lands in]
Actions
| Action | Does |
|---|---|
start | run a plan task by task, then wrap up, open the PR and wait for CI 🪙 |
stop | stop a running session now, the task it is running marked [BLOCKED] |
pause | pause a running session once the task it is running ends |
resume | resume a paused session from its next task |
status | show a session: its state, tasks done over total and a rough ETA |
list | list the running sessions, with the tasks done in each plan |
wait | wait on a session until a reason --until names happens |
rafa loop start
Run a plan task by task, then wrap up, open the PR and wait for CI.
rafa loop start [--plan=<string>] [--start-at=<string>] [--inject=<string>]
[--skills-resolver=<string>] [--runtime=<string>] [--no-ci-wait]
[--ci-timeout=<number>] [--ci-attempts=<number>] [--retry=<number>]
[--continue] [--decide=<string>] [--approach=<string>] [--after=<number>]
[--force-wrap-up] [--create-branch] [--as-worktree] [--any-branch]
[--roadmap] [-d|--detached] [--no-hint]
Walks the tracker of a plan one task at a time, each in a Claude Code session whose work is committed once it exits, and retries blocked tasks first. After the last task a wrap-up session promotes findings, syncs with main, pushes and opens or updates the PR, and the loop then waits on its checks, spending repair sessions on a red or conflicting PR. With no --plan it runs PLAN.md in plan.dir, .rafa/plans unless the config names another, or PLAN.md at the project root when that one does not exist. Before any session it checks the prerequisites the config and the plan's PREREQUISITES-<stub>.md name, halting when a required one fails and naming each failed optional one known-missing in every task prompt, and warns once for each migration the effort store holds that this rafa does not know and that is additive. It refuses to start while RAFA_EFFORT_DIR is set, since a loop records to the project's own store. Started on main or master, or on a detached HEAD, it offers to create the plan's feat/<stub> from the latest origin/<base> and run there, --create-branch answering that without asking, and refuses the run when the offer is not taken. On a detached HEAD the base is pr.base, else the branch origin/HEAD names. --as-worktree runs the plan in a worktree of its own on that branch instead, leaving the main checkout as it is, and is refused beside --create-branch. Each run writes its session record to .rafa/runs/<session-id>.json: the plan, the branch, the pid, the start, the state and the running task, and under --roadmap the hop away, when one is. It refuses a plan whose record names another branch, and a plan a session is still running. rafa loop stop, pause, resume, status and list reach the run through that record. With --retry or loop.retries it goes back into its loop instead of stopping, at most that many times in a row, after a red suite step, a task session that exited nonzero, or one that left neither a report nor a commit; a later task done, neither the stopped one nor a repair, starts the count over. With --continue a stop that would end the run is handed to a decision instead: retry the task with a new approach, stop with exit code 20, jump over it or defer it until a later task is done; a run left with only passed-over tasks ends with exit code 22 and no wrap-up unless --force-wrap-up, and under --output=json a decision the line does not name ends the run with exit code 21 and the decision prompt. A run holds its terminal: until phase 6 it refuses --detached. With --runtime the whole run goes on in that installed rafa, never in a src/ directory.
Spends: 🪙 one session per task, one for the wrap-up and up to loop.wrapUp.retries more when it opens no pull request, repair sessions while CI is red, the sessions of up to --retry (loop.retries) more passes of the loop in a row after the stops it retries, and under --continue one read-only decision session per decision
| Flag | Type and default | What it does |
|---|---|---|
--plan | string | The plan to run, relative to the project root. PLAN-<stub>.md is tracked in PLAN_TRACKER-<stub>.md beside it. |
--start-at | string | Waits until this local time of day, as HH:MM, before the first task. |
--inject | string | How much of the plan each task session is handed: full, stage or task. Outranks plan.inject in .rafa/config.yaml. The wrap-up is handed the whole plan. |
--skills-resolver | string | The resolver that picks the skills each task session is handed: planner, tag or none. Outranks task.skills in .rafa/config.yaml for this run only. |
--runtime | string | The installed rafa the run goes on in: a version under ~/.rafa/runtime/, or a path against the working directory to a cli.js or the directory holding it. Refused inside the src/ of the working directory or the project root. Typed after loop start. |
--ci-wait | boolean, default true | Waits on the checks of the PR once it is pushed; --no-ci-wait finishes at the push. |
--ci-timeout | number, default 20 | Minutes to wait for the checks to settle. |
--ci-attempts | number, default 2 | Repair sessions to spend on a red or conflicting PR before escalating; 0 spends none and still reports the verdict. |
--retry | number | Goes back into the loop instead of stopping, at most this many times in a row, 1 to 3, after a red suite step, a task session that exited nonzero but not on its budget, or one that left neither a report nor a commit. The count starts over once a later task is done, neither the stopped one nor a repair. A report that blocks its task, a refused commit, a moved checkout and an interrupt still stop the run. Outranks loop.retries in .rafa/config.yaml, false unless the config names a count; any other value is refused before the run starts. |
--continue | boolean | Hands a stop that would end the run to a decision instead: a report that holds its task at once, and a retry-safe stop once its retries are spent. One read-only decision session picks retry (with a new approach), stop, jump (pass the task over this run) or defer (pass it over until a later task is done), by the bundled criteria and loop.continue.criteria. The retries are loop.retriesOnContinue unless --retry names a count. |
--decide | string | Names the decision on the line, retry, stop, jump or defer, applied once to the first decision point the run meets, so no decision session is spawned for it. Needs --continue. |
--approach | string | The new approach a --decide=retry hands the task's next session; required by it and read beside nothing else. |
--after | number | The tracker line, counted from 1, of the task a --decide=defer waits on; required by it and read beside nothing else. |
--force-wrap-up | boolean | Wraps up a --continue run left with only passed-over tasks anyway, as a draft pull request listing them, once the pre-wrap-up suite step finds no more new failures than loop.forceWrapUp.maxNewFailures tolerates. Needs --continue. |
--create-branch | boolean | On main or master, or on a detached HEAD, creates feat/<plan-stub> from the latest origin/<base> and runs the plan there without asking, or switches to that branch when it already exists. On a detached HEAD the base is pr.base, else the branch origin/HEAD names. Refuses rather than move when a tracked file is modified, the fetch fails or, on main or master, the base has diverged from its remote. |
--as-worktree | boolean | Runs the plan in a linked worktree of its own: feat/<plan-stub> from the latest origin/<base>, added at <loop.worktreeDir>/<plan-stub>, .rafa/worktrees unless the config names another. The main checkout keeps its branch and working tree. Refused beside --create-branch, which makes the same branch by switching the main checkout. |
--any-branch | boolean | Runs on main or master, which the loop otherwise refuses. Waives nothing on a detached HEAD: the run is offered feat/<plan-stub> from the latest origin/<base>, the base being pr.base, else the branch origin/HEAD names, and refused when the offer is not taken. |
--roadmap | boolean | Stamps the hop rafa next --roadmap is away on, when one is, on the session record as its hop: the hop record .rafa/hop.json holds, still away and its home still the position's. What rafa next --roadmap passes to the loop it starts. |
-d, --detached | boolean | Runs the loop in the background. Refused until phase 6, before anything is read: until then a run holds the terminal it starts in. |
--hint | boolean, default true | Ends by naming the one step that follows, asking it where there is a terminal to answer on; --no-hint reads nothing and prints nothing. |
Examples:
rafa loop start --plan=.rafa/plans/PLAN-my-feature.md: Runs the plan from its first open task, then opens the PR and waits on its checks.rafa loop start --plan=.rafa/plans/PLAN-my-feature.md --start-at=23:00 --inject=task --no-ci-wait: Starts at 23:00, hands each task session the plan context and its own task line, and finishes at the push.rafa loop start --plan=.rafa/plans/PLAN-my-feature.md --create-branch: Run frommain, createsfeat/my-featurefrom the latestorigin/mainwithout asking, and runs the plan there.
Output formats: text, json, events.
rafa loop stop
Stop a running session now, the task it is running marked [BLOCKED].
rafa loop stop [-s|--session-id=<string>]
Sends SIGINT to the loop running a session. The loop passes it on to the task session it is running, marks that task [BLOCKED], stores its report and ends, its record then reading stopped; a paused run ends with no task marked. The command waits up to 30 seconds for the record to say so, then names the task the run ended at and what its tracker line holds. A run still in its wrap-up or CI wait after that is warned about, since it ends once that step returns. Running the plan again with rafa loop start retries the blocked task first. With --output=json the session, whether it ended and its task are the data of the terminal result event.
| Flag | Type and default | What it does |
|---|---|---|
-s, --session-id | string | The session to stop, by the id its record under .rafa/runs/ is named with. Without it, the session running on the branch checked out at the project root. |
Examples:
rafa loop stop: Stops the session running on the branch checked out here, marking its running task [BLOCKED].rafa loop stop --session-id=9185b41c-65f7-4dd6-a0c1-6494c4028f0f --output=json: Stops that session, and writes the record it ended with as the data of the result event.
Output formats: text, json.
rafa loop pause
Pause a running session once the task it is running ends.
rafa loop pause [-s|--session-id=<string>]
Writes paused to the record of a running session. The loop reads its record between tasks, so the running task ends, is committed and is marked as it would have been, and the run then holds before its next task, marking nothing, until rafa loop resume goes on or rafa loop stop ends the run. A session already paused is left as it is. With --output=json the session record and whether it changed are the data of the terminal result event.
| Flag | Type and default | What it does |
|---|---|---|
-s, --session-id | string | The session to pause, by the id its record under .rafa/runs/ is named with. Without it, the session running on the branch checked out at the project root. |
Examples:
rafa loop pause: Pauses the session running on the branch checked out here once its running task ends.rafa loop pause -s 9185b41c-65f7-4dd6-a0c1-6494c4028f0f: Pauses that session, whatever branch is checked out.
Output formats: text, json.
rafa loop resume
Resume a paused session from its next task.
rafa loop resume [-s|--session-id=<string>]
Writes running to the record of a paused session, so a run holding between tasks goes on with its next one, and a run whose running task has not ended yet carries on past it as if never paused. A session already running is left as it is. A session that has ended is not resumed: rafa loop start runs its plan again as a new session. With --output=json the session record and whether it changed are the data of the terminal result event.
| Flag | Type and default | What it does |
|---|---|---|
-s, --session-id | string | The session to resume, by the id its record under .rafa/runs/ is named with. Without it, the session running on the branch checked out at the project root. |
Examples:
rafa loop resume: Resumes the session paused on the branch checked out here.rafa loop resume --session-id=9185b41c-65f7-4dd6-a0c1-6494c4028f0f: Resumes that session, whatever branch is checked out.
Output formats: text, json.
rafa loop status
Show a session: its state, tasks done over total and a rough ETA.
rafa loop status [-s|--session-id=<string>]
Reads a session record and its plan's tracker: the state, the pid, the start, the task it names, the tasks done, blocked and open over the whole plan, and beside them the phase the run is in: task, wrap-up, pull-request, ci or repair. A running or paused session also gets a rough ETA from the effort store: the time from its start to the last task it finished, per task finished, times the open and blocked tasks left, and none before its first task finishes. Every blocked task of the checklist is shown with the blocker comment its line trails, which is what the run wrote when that task ended blocked. Without --session-id it shows the session running on the branch checked out at the project root, or the newest one that ran there. With --output=json the record, the counts and the ETA are the data of the terminal result event.
| Flag | Type and default | What it does |
|---|---|---|
-s, --session-id | string | The session to show, by the id its record under .rafa/runs/ is named with. Without it, the session running on the branch checked out at the project root. |
Examples:
rafa loop status: Shows the session on the branch checked out here: its state, tasks done over total, phase and ETA.rafa loop status --session-id=9185b41c-65f7-4dd6-a0c1-6494c4028f0f --output=json: Writes that session's record, task counts and ETA as the data of the result event.
Output formats: text, json.
rafa loop list
List the running sessions, with the tasks done in each plan.
rafa loop list
Lists every session record under .rafa/runs/ that reads running or paused, oldest first: its id, plan, branch, state, pid and start, the tasks of its plan done, blocked and open with the phase its run is in beside them, and the worktree it runs in, or the main checkout. A record whose pid is gone reads stopped and is not listed. Until phase 6 a plan runs in one session at a time, so each row is a plan running. With --output=json the sessions are the data of the terminal result event.
Examples:
rafa loop list: Prints one row per running session: its id, plan, branch, state, tasks done and worktree.rafa loop list --output=json: Writes a start event, then a result event whose data is the sessions.
Output formats: text, json.
rafa loop wait
Wait on a session until a reason --until names happens.
rafa loop wait [-s|--session-id=<string>] [--until=<string>]
[--timeout=<string>]
Follows a loop run until a reason --until asks for happens, then prints one rafa· line naming it and exits with that reason's code. It reads the run's events file, .rafa/runs/<session-id>.events.ndjson, from its first line, so a run that has already ended still answers, and reads the run's record and probes its pid every 5 seconds. The reasons: pr, event pr, exits 0; no-pr, event no-pr, exits 10; halt, event halt, exits 11; error, event error, exits 12; blocked, event task-blocked, exits 13; exit, the run's pid not alive, or its record reading stopped or done, exits 14; quiet:<minutes>, no event for <minutes> awake minutes, exits 15. --timeout reached exits 16, and a session it cannot find exits 2. Without --until it waits for pr,no-pr,halt,error,exit; blocked and quiet are never in that set. Quiet minutes and the timeout count time the machine is awake, so a suspended laptop never reads as a stuck loop; on macOS the clock keeps counting through sleep, so there a long sleep can raise a false quiet or an early timeout. A run with no events file, from an older rafa, is noted once, and only exit, quiet and the timeout can answer for it. Without --session-id it waits on the session running on the branch checked out at the project root, or the newest one that ran there. With --output=json the reason, the event, the session's state and the minutes are the data of a wait event, and of the terminal result when it exits 0.
| Flag | Type and default | What it does |
|---|---|---|
-s, --session-id | string | The session to wait on, by the id its record under .rafa/runs/ is named with. Without it, the session running on the branch checked out at the project root. |
--until | string | The reasons to wait for, a comma list of pr, no-pr, halt, error, blocked, exit, quiet:<minutes>. pr,no-pr,halt,error,exit when it is left out. |
--timeout | string | How many awake minutes to wait before giving up with exit code 16. No limit when it is left out. |
Examples:
rafa loop wait: Waits on the session of the branch checked out here until it opens a PR, ends without one, halts, fails or exits.rafa loop wait --session-id=9185b41c-65f7-4dd6-a0c1-6494c4028f0f --until=blocked,exit,quiet:30: Wakes on a blocked task, the run's end, or 30 awake minutes with no event.rafa loop wait --timeout=60 --output=json: Waits up to an hour awake, then writes the reason, the event and the session's state as a wait event.
Output formats: text, json.