2026-08-28 · Updated 2026-08-28 · 11 min read
Migrate a Ralph loop to an auditable YYLO workflow
The migration path from a running prompt loop to an auditable YYLO workflow — inventory the loop's parts, apply three renames (the prompt becomes typed steps, the session becomes receipt-bound evidence, the iteration becomes a named attempt), give the plan file a durable destination, and stage every move so it can roll back.
By Juno AI INC · ralph-loop · workflow-runner · yylo
Migration day starts after the decision, not with it. The loop ran honestly for weeks — the prompt tuned against observed failures, the plan file rewritten more than once, the specs folder doing the steering — and then a morning arrived where the trade was no longer worth it: unreviewed commits outpacing your reading, a breakage you could not attribute to a pass, a stakeholder asking for the record. Naming that moment is not this page's job; the observed trigger list is the comparison's, and this page begins where two or more of those triggers have already fired.
What remains is mechanical care: moving the running thing into YYLO without losing the work it already did or the disciplines that made it productive. Three renames carry the technique across — the standing prompt becomes typed workflow steps, the anonymous session becomes receipt-bound evidence, the endless iteration becomes a named, recoverable attempt — and every remaining part of the loop has a destination too. The neighbors keep their own ground: the definitive reference owns the vocabulary, hardening a loop in place is the stay-put alternative for loops worth keeping, and the artifact contracts this migration lands on are owned end to end by the auditable-workflows guide. Every YYLO claim below was verified against committed templates, CLI source, and the README, evidence dated 2026-08-28; the mapping itself is this site's original work.
Rollback safety is the property that makes the move possible at all. At every stage below, the old loop still exists as files, the new workflow has destroyed nothing, and retreat is a command you already know. A migration with no way back is a bet, not a migration.
Inventory the loop before touching it
A loop is fewer parts than it feels like from inside the tunnel. Write them down first, because each part has a different destination:
- The prompt file. Standing instructions re-read by a fresh process every pass, tuned by observation rather than designed once. Destination: typed steps in one workflow YAML.
- The plan file. The remaining work in priority order, maintained by the same agent that works it down. Destination: task truth on the ledger.
- The specs folder. Declarative inputs, written before the loop runs. Destination: none — specs were always verifiable inputs; they carry over unchanged and the step prompts keep pointing at them.
- The signs. Guardrail lines appended to the prompt after watching a specific failure. Destination: gate steps whose exit codes, not whose phrasing, decide.
- The loop command.
while :; do cat PROMPT.md | claude-code ; done, indefinite by construction. Destination: bounded attempts under a run contract. - The doneness judgment. Watching the stream until it looks baked, then interrupting. Destination: terminal status your review reads from the manifest.
Two honest notes travel with the inventory. First, the prompt file doubles as a standing injection surface — whoever can write it steers every pass — so lifting its content into YAML is also the moment to apply the boundaries the prompt-safety guide sets. Second, the inventory is the migration plan: once every part on the list has a destination, nothing about the move is left to improvisation.
Rename one: the prompt becomes steps
The prompt file mixes two kinds of content that the loop must share one file: standing rules for how to work, and per-pass steering for what to work on. The loop re-reads one file, so the two interleave; the workflow separates them. Standing rules become the step's command and prompt text, parsed and rendered once by machinery. Per-pass steering becomes which task the step is pointed at. A migrated slice of loop work reads like this:
The structural change is in the last step: downstream work receives the prior answer through a template, {{ steps.do_work.response }}, not through a file the worker itself maintains. In the loop, pass N+1 finds pass N's reasoning by re-reading files the same non-deterministic process wrote; in the workflow, the runner renders the substitution before dispatch. And because the YAML is parsed rather than re-interpreted, mistakes surface before anything runs: lint catches the known template anti-patterns in responses and logs, and a dry run renders every artifact while executing nothing.
The signs get the sharpest upgrade. A sign works only while the agent still reads it — the pass that skims past the sign loses the guardrail entirely — and the loop's answer is to add another sign. A gate step moves the same intent out of prose: the check runs as a command, every run, and with fail_workflow: true its failure is the run's failure. The default is deliberate and worth knowing before you mark everything: when a step fails, the failure is written to the manifest, the overall process exit stays zero, and the remaining steps still run — so mark only the steps whose failure must stop everything.
Rename two: the session becomes evidence
Every pass through the raw loop opens a brand-new agent session around an empty context window, and the sole account of why any pass behaved the way it did lives in the terminal you watched it in. Scrollback is not storage. The migration renames the session into two artifacts, one for each thing a session was silently carrying.
Memory — the ability to continue — becomes the session id. Steps that invoke yy, yylo, or ypl get their sessions captured automatically, the session id lands in the manifest as a template value, {{ steps.do_work.session_id }}, a later step can resume that exact conversation, and the run's final successful agent session persists so yy cc continues it from the producing shell. Proof — the ability to verify — becomes the run directory's files: each step's streams land as numbered artifacts, stdout and stderr and the response file for agent steps, hash-bound into the manifest beside the step's status and session id. And where an output is a claim a reviewer or a downstream step must rely on, the step declares a typed receipt whose required fields include the producing step's digest, binding the output to its producer, with the path surfacing downstream as {{ receipts.<id>.path }}.
One contract detail deserves its own sentence because it reverses a loop habit: an agent step that exits zero without an answer fails by rule — silence cannot count as a completed pass. The full receipt and capture contracts, including what recovery may rely on, are the auditable-workflows guide's ground; the migration-level point is that both halves of the session now live in files a script can read.
Rename three: the iteration becomes an attempt
The while-loop keeps no ledger of its own passes; the fortieth pass differs from the thirty-ninth only in the commit log. The workflow names every execution of itself: a run id and its own run directory (by default under .juno_task/specs/workflows/, addressed by workflow and run ids), plus a run contract holding the checkpoint and attempt index, with prior attempts archived under attempts/ rather than overwritten.
That naming is what turns interruption from a lost evening into a recorded state. If the producer dies mid-run — a crash, a kill signal, a dropped connection — verify what survived before touching anything:
Recovery reads only complete, hash-bound step checkpoints. It refuses active, partial, non-contiguous, cross-run, and drifted evidence; on success it appends an interrupted attempt that names the verified prefix and marks the first step the evidence cannot vouch for, and a verified prefix is never promoted into a claim of completion. Resume exactly where that report points: --from-step takes a step id, its name, a zero-based position, or -1 for the last step, and before dispatch the runner re-checks that the workflow, its variables, the rendered commands, the frozen inputs, every producer digest, and each receipt hash are unchanged — predecessors become reused only after that check passes.
The loop's indefinite repetition has two bounded destinations, and choosing between them is not this page's decision to make. If the remaining work is a queue of independent units, repetition stays board-shaped: Run Until Completion keeps issuing bounded iterations while the board still lists open work. If the steps exchange results, the ordered chain above is the destination. The runner-choice guide owns that fork.
Where the plan file goes
The plan file's flaw is structural, not cosmetic: the record of remaining work is maintained by the same non-deterministic process doing the work, so the record keeper and the failure source are one. The destination is task truth. Items become tasks whose status, responses, dependencies, and commit evidence live in the ledger, written by explicit commands and read by scripts; the contract — readiness as a computed answer, completion bound to a commit — is the task-truth guide's ground:
Two loop disciplines survive this move with new addresses. One item per unit becomes one task per step, and close reading of the remaining work becomes reading the board. The workflow does not weaken either habit; it gives each one a place that cannot be edited by the process being managed.
Stage the move so every step can roll back
The whole migration is five stages, each with its own retreat:
- Stop and bank. Interrupt the loop on purpose, at a moment you choose rather than one a crash chooses. Commit or rescue whatever the tree holds, and copy the plan file's remaining items into the migration backlog — that list is the input to everything below. Retreat: a loop is one shell command plus its prompt file, plan file, and specs folder, and stopping changes none of them. If the stop comes too late and the tree is already broken, the failure catalog turns the evidence at hand into the failure that happened.
- Pilot one item bounded. Before any YAML exists, take the top item and run it as one bounded cycle with your review after; the starter walkthrough owns that first evening. Retreat: one cycle's diff is one review decision, and reverting it costs exactly one cycle.
- Write, lint, dry-run. Draft the workflow from the inventory, lint the templates, and render the whole run dry. Retreat: the dry run executes nothing, and the YAML is a file under version control like any other change.
- Run once, review from artifacts. The first real run is reviewed through the manifest and the response files, never through the console that happened to be attached. If the workflow itself was wrong — the harness, not the work — correct it in a fresh output directory using
amendment_mode: harness_only_validationwith--amends-run, adding--from-stepso the runner revalidates and imports the earlier successful prefix and then executes only the corrected suffix. Never modify a finished run so its evidence becomes reusable; that integrity is what every later recovery leans on. - Interrupt cleanly. When a run dies, dry-run the recovery first and resume at the reported step. An interrupted attempt is a recorded state with a verified prefix and a named next action, not a failed migration.
Rollback safety is the property you are buying
The loop's rollback story is the bad morning the comparison names: an unreviewed diff, a judgment call with nothing to lean on, and a hard reset in which the night's work is simply gone. The migrated system replaces that single cliff with a ladder:
- Per cycle, the commit is the unit. You read a bounded pass's diff before deciding anything, and a revert is local to that pass's work.
- Per run, attempts are append-only. Recovery adds an
interruptedattempt, amendments live in fresh directories, and nothing that already happened gets rewritten. - Per workspace, the pilot can live where a bad run cannot touch the main checkout. The worktree topology gives parallel lanes a checkout each, and the managed task path —
yy task start TASK_ID— hands implementation a dedicated worktree to work and commit in.
What survives the migration is the part of the loop worth keeping: one item per unit became one task per step, backpressure inside the pass became gate steps, the plan became the board, and the interrupt reflex became a terminal status you read. What disappears is everything the triggers were measuring — guesswork stops, scrollback narratives, mornings that end in resets. Install YYLO with npm and migrate a single item this week; when your first interruption comes back as a verified prefix and a named step instead of a mystery, the loop has finished becoming a workflow.