Software Crafters® 2026 | Creado con 🖤 para elevar el nivel de la conversación sobre programación en español | Legal
"Ralph es deterministically bad in an indeterministic world." (Geoffrey Huntley)
En julio de 2025, un ingeniero australiano llamado Geoffrey Huntley publicó un post titulado simplemente "Ralph". Dentro había siete palabras de bash:
while :; do cat PROMPT.md | claude ; done
Eso era todo. Un bucle infinito que le pasaba el mismo prompt a Claude una y otra vez. Sin orquestador, sin estado en memoria, sin lógica condicional. El loop más estúpido que se te puede ocurrir. Huntley le puso el nombre de Ralph Wiggum, el personaje de los Simpsons famoso por darse cabezazos contra los marcos de las puertas mientras grita "I'm helping!".
Y resulta que funcionaba.

Con esa misma técnica, Huntley construyó un lenguaje de programación entero llamado CURSED en tres meses, autonomously, mientras él dormía. Un cliente le había encargado un trabajo de $50.000 que terminó costando $297 en tokens. Un equipo del hackathon de Y Combinator soltó seis repositorios funcionales en una noche por algo menos de 800 dólares en total, a unos 10,50 dólares por hora por cada agente Sonnet corriendo en loop.
A finales de 2025, Ralph se había convertido en uno de los patrones más comentados del año. Anthropic publicó un plugin oficial llamado ralph-loop en diciembre. OpenAI metió un comando /goal en Codex CLI 0.128.0 en abril de 2026 y Anthropic respondió en mayo metiendo su propio /goal directamente en Claude Code 2.1.139, esta vez con una supervisor architecture que separa el agente que trabaja del que decide si está hecho. La comunidad sacó wrappers para Claude Code, OpenCode, Codex, Copilot CLI, Cursor Agent y Qwen Code.
Lo que entonces parecía una broma se convirtió en una primitiva agéntica.
Este artículo es una guía técnica densa sobre Ralph. Quién lo inventó, por qué funciona, cómo se montan los dos archivos del prompt (un LOOP.md reutilizable y un TASK.md con la spec del proyecto), qué cuatro formas hay de correrlo en Claude Code (incluyendo el comando /goal que Anthropic acaba de meter de forma nativa), cómo lo hacemos en OpenCode sin atarnos a Anthropic, y una demo real lado a lado donde Opus 4.7 y GPT-5.5 atacan la misma kata desde cero. Al final, un bonus sobre cómo encajar Ralph encima de un setup multi-modelo en OpenCode.
Sin agencias intermedias. Sin promesas vagas. Bash, archivos, modelos y resultado.
Empezamos.
Lo más fácil de explicar es lo que NO es.
Ralph no es un agente con memoria. No es un agente con planificación interna. No es un framework. Es un bucle externo, escrito en bash, que ejecuta el mismo binario una y otra vez con el mismo input. La inteligencia vive dentro del modelo. Lo único que aporta Ralph es persistencia mecánica.
Lo importante está en lo que persiste entre vueltas: archivos en disco y commits en git. Cada iteración arranca con el contexto del modelo en blanco, pero ve el TASK.md con los checkboxes que la iteración anterior dejó marcados, el código que ya hay en src/, los tests que pasan o fallan, y el git log con los commits previos. El modelo no recuerda nada, pero el sistema sí.
Huntley lo resume con una frase que se ha convertido en mantra del movimiento:
"That's the beauty of Ralph, the technique is deterministically bad in an indeterministic world."
La idea es contraintuitiva. Los modelos de lenguaje son indeterministas: la misma pregunta puede dar respuestas distintas. Los humanos intentamos compensar eso con orquestadores complejos, validaciones cruzadas, multi-agente y retries condicionales. Ralph va en la dirección opuesta. Hazlo igual de mal todas las veces. Si el modelo falla, falla de la misma manera, y el siguiente intento ve el fallo del anterior en los archivos. Es un pressure cooker contextual. La presión es la realidad escrita en disco.
CURSED es el lenguaje de programación que Huntley se inventó para demostrar la técnica. No es un toy language. Tiene compilador a LLVM, librería estándar, sistema de tipos y se aspira a self-hosting. Está construido casi enteramente por Ralph, ejecutándose mientras Huntley duerme. El truco está en que CURSED no está en los datos de entrenamiento de ningún modelo, así que es imposible que el LLM esté "recordando" código que ya vio. Todo lo que hay ahí lo razonó iterando.
Y los costes son los que son. Un contrato de $50.000 USD que se cumplió por $297 en tokens. Una noche de hackathon de Y Combinator con seis repositorios funcionales (más de 1.100 commits entre todos) por unos $800 en total, a aproximadamente $10,50/hora por agente Sonnet corriendo en loop. Lo cuento porque las cifras son las que sostienen el debate. Sin ellas, Ralph sería una curiosidad.
Ralph es:
Ralph no es:
Lo último es lo más importante. Como veremos en la sección de LOOP.md + TASK.md, el operador sigue siendo crítico. La diferencia es que ahora la responsabilidad se traslada del prompt único a la estructura del prompt y del entorno de trabajo.
Antes de meternos en el cómo, vale la pena entender por qué un bucle bash de una línea consigue lo que muchos frameworks de agentes con grafos y orquestadores no consiguen.
La razón número uno: cada iteración arranca con la ventana de contexto vacía. No hay context rot, ese deterioro progresivo que sufren los agentes cuando llevan dos horas conversando consigo mismos y empiezan a contradecirse o a alucinar referencias que se inventaron tres mil tokens atrás.
Los modelos modernos llegan al millón de tokens de ventana de contexto: Claude Opus 4.7 y Sonnet 4.6 desde marzo de 2026, GPT-5.5 desde abril, Gemini 2.5 lleva más. Pero el tamaño es engañoso. Investigaciones recientes han descrito que la "smart zone", donde el modelo realmente razona bien, sigue rondando los 40k a 60k tokens. Más allá, el modelo acepta lo que le metas pero la calidad del razonamiento cae. Ralph evita el problema por construcción: cada vuelta es una sesión nueva, así que nunca llega a la zona caliente.
El estado no vive en la memoria del modelo, vive en disco. Tres ficheros canónicos hacen el trabajo:
TASK.md: la spec del proyecto con sus checkboxes. Es lo que hay que hacer y lo que ya está hecho, el modelo va marcando - [ ] como - [x] a medida que cierra requirements.git log: la cronología de qué se ha hecho. Cada iteración es idealmente un commit.src/, tests/, bin/. Es la prueba viva de qué funciona y qué no.El modelo, al arrancar cada vuelta, lee estos tres ficheros y reconstruye en su cabeza el estado del proyecto. No necesita recordar la conversación anterior porque la conversación anterior está escrita.
Esta es la parte que mucha gente subestima. Ralph funciona porque las herramientas dicen la verdad. El typecheck no opina. Los tests no se contagian del entusiasmo del modelo. El linter no se siente mal por marcar 14 errores.
Si el modelo cree que ha implementado el feature pero npm test devuelve fallo, la siguiente iteración va a leer ese fallo y se va a corregir. Es lo que Huntley llama contextual pressure cooker: el modelo está forzado a confrontar su propio desastre. No hay forma de escapar por ser elocuente.
Esto significa que Ralph no es adecuado para entornos sin verificación automática. Si no tienes tests, no tienes typecheck, no tienes lint, Ralph se convierte en un escupidor de tokens optimistas. Backpressure es el guardarrail.
Lo que estamos describiendo cae dentro de lo que en 2026 se ha empezado a llamar harness engineering: la disciplina de diseñar todo el entorno alrededor del modelo, herramientas, permisos, sandboxing, hooks, persistencia de estado, observabilidad, en vez de obsesionarse con el modelo en sí. Dos papers de Anthropic (noviembre de 2025 y marzo de 2026) lo resumieron con una frase: "not a smarter model but a smarter environment around the model". Ralph es un harness mínimo. Backpressure es la pieza más importante de ese harness, pero hay más: el --permission-mode auto, los stop hooks, los circuit breakers de los wrappers community, la persistencia en archivos. Todo eso, junto, es el harness.
Una sutileza importante. Cuando el modelo necesita leer (buscar en el repo, mapear arquitectura, identificar archivos relevantes), puede lanzar muchos subagentes en paralelo. Esto es seguro porque lectura no causa contención. Huntley llega a hablar de 500 subagentes paralelos en operaciones de búsqueda.
Pero cuando toca construir (correr npm test, ejecutar tsc, llamar a un linter), tiene que ser secuencial. Esas herramientas compiten por el TypeScript server, por el filesystem, por puertos. Si las lanzas en paralelo se pisan entre ellas y dan ruido en vez de señal.
Es un detalle que parece técnico, pero condiciona todo el LOOP.md: hay que decirle al modelo explícitamente qué puede paralelizar y qué no.
Aquí entra el currazo de Ralph. El bucle de bash es trivial, lo que importa es lo que va dentro del prompt. Y en cuanto te pones a estructurarlo en serio descubres que hay dos planos distintos que no conviene mezclar.
Uno es la disciplina del loop: cómo se trabaja, qué se lee al empezar la vuelta, cuándo se commitea, qué guardrails son no-negociables. Eso vale para CUALQUIER proyecto y se reescribe una vez para el resto de tu vida.
El otro es la spec del proyecto: qué tienes que construir, en qué orden, cuándo se considera terminado. Eso es específico de cada cosa.
Si los mezclas en un único PROMPT.md (que es lo que hacía el Huntley original), pierdes la oportunidad de reutilizar el harness y ensucias la spec con referencias a "Phase 0a" y "guardrail 9004" que el dueño del proyecto no debería tener que ver. Si los separas en dos archivos, cada uno cumple su rol:
┌───────────────────────────────────┐ ┌──────────────────────────────┐ │ LOOP.md (reutilizable) │ │ TASK.md (por proyecto) │ │ ───────────────────── │ │ ────────────────── │ │ Phase 0a - Orientation │ │ ## What │ │ Phase 1 - Pick next req │ │ Lo que hay que construir │ │ Phase 2 - Implement TDD strict │ │ │ │ Phase 3 - Commit + update TASK │ │ ## Requirements │ │ Phase 4 - Completion + promise │ │ - [ ] R1: ... │ │ │ │ - [ ] R2: ... │ │ 9001 Do not lie │◀───│ - [ ] R3: ... │ │ 9002 No placeholders │ │ (el modelo va │ │ 9003 One task per loop │ │ marcando estos │ │ 9004 Strict TDD │ │ cada Phase 3) │ │ 9005 Parallel reads │ │ │ │ 9006 Verify, do not assume │ │ ## Acceptance │ │ 9007 Test naming style guide │ │ - [ ] npm test green │ │ │ │ - [ ] typecheck clean │ │ Sirve para cualquier proyecto. │ │ - [ ] README con 3 ej. │ │ No menciona la kata, no │ │ │ │ menciona String Calculator, │ │ Vive en el repo del │ │ no menciona TypeScript. │ │ proyecto, se versiona │ │ │ │ con git como cualquier │ │ Vive en `~/loops/LOOP.md` o │ │ README. │ │ donde te dé la gana. │ │ │ └───────────────────────────────────┘ └──────────────────────────────┘
El wrapper de bash sigue siendo de una línea, solo cambia QUÉ archivo le mete al modelo. Le pasamos LOOP.md (el harness), y desde Phase 0 el modelo lee TASK.md como cualquier otro archivo del repo:
while :; do cat LOOP.md | claude -p --model opus --permission-mode auto done
La analogía mental que mejor funciona: LOOP.md es a TASK.md lo que CLAUDE.md (instrucciones permanentes) es a un README.md o PRD.md (qué se hace en ESTE proyecto). El primero define el cómo y se reutiliza. El segundo define el qué y vive con el código.
LOOP.md se organiza en tres bloques:
┌──────────────────────────────────────────────┐ │ Phase 0a-0e - Orientación │ │ ──────────────── │ │ Qué leer antes de tocar nada. │ │ TASK.md, git log, ls, npm test. │ │ Reconstruir el estado del proyecto. │ ├──────────────────────────────────────────────┤ │ Phase 1-4 - Ejecución │ │ ───────────── │ │ Phase 1: pick the next unchecked req │ │ Phase 2: implement (TDD strict baby steps) │ │ Phase 3: commit + flip checkbox en TASK.md │ │ Phase 4: completion check + promise │ ├──────────────────────────────────────────────┤ │ 9001+ - Guardrails │ │ ────────── │ │ No mentir para escapar, no placeholders, │ │ no hacer más de una cosa por vuelta, │ │ TDD strict, verificar, style guide tests. │ │ │ │ Convención Huntley: más número = más │ │ prioridad, para que el modelo entienda │ │ que estos son invariantes que no │ │ negocia. │ └──────────────────────────────────────────────┘
Las phases 0a-0e existen para que el modelo no parta de cero cada vez. Le obligan a leer TASK.md (para saber qué hace falta y qué ya está hecho), los commits y el estado de los tests antes de hacer nada. Si saltas este paso, el modelo se pone a implementar features que ya están hechas, o reescribe lo que ya funciona.
Vocabulario clave que Huntley repite y que parece marcar diferencia:
El cuerpo del LOOP.md obliga al modelo a hacer una sola cosa por vuelta. Esto es contraintuitivo (parece que pierdes tiempo) pero es la clave. Si dejas que el modelo intente todo a la vez, gasta su contexto bueno en pensar la arquitectura general y termina implementando los detalles a medio gas. Si lo fuerzas a una cosa por vuelta, dedica los 40-60k tokens útiles a hacer ESE item bien.
La novedad de tener checkboxes vivos en TASK.md: cada Phase 3 el modelo abre el archivo, flipa el - [ ] que acaba de cerrar a - [x] y commitea. La siguiente vuelta lee TASK.md actualizado, identifica el primer - [ ] y le toca trabajar en ese. El plan se mantiene él solo.
La Phase 4 incluye la completion check. El modelo solo puede emitir el promise (<promise>DONE</promise> o el tag que hayas definido) cuando todos los - [ ] están en - [x]. Eso es lo que detecta el wrapper para parar el loop.
La convención de Huntley es brillante: numerar las reglas más críticas con números altos (9001, 9002, 9003) para que el modelo las trate como invariantes. Las más importantes:
throw new Error("not implemented"), nada de // TODO. Si no puedes terminar un item, lo dejas en TASK.md y se hará en la próxima vuelta.ls.Un hallazgo curioso: prompts de 1.500 palabras hicieron al agente "más lento y más tonto" que prompts de 103 palabras, según las pruebas que hicieron en el equipo de ZeroSync. La economía es real. Cada token del prompt es un token menos para razonar. La estructura por phases ayuda porque permite densidad: poca palabra, mucha consecuencia. Y separar LOOP.md de TASK.md te permite mantener LOOP.md compacto sin sacrificar la spec.
El PROMPT.md canónico de Huntley dice "una tarea por loop" y suficiente. Es buen punto de partida pero deja al modelo demasiada libertad dentro de cada iteración. Sin más, lo típico es que el modelo escriba TODOS los tests del componente de golpe y luego la implementación entera. Eso no es TDD. Es test-first-massive, que tiene sus propios problemas (tests sobreespecifican, implementación sobreingenieriza para cubrirlos todos).
La mejora que propongo y que vas a ver en la demo es esta: TDD strict baby steps dentro de cada iteración. Una iteración del loop no es "implementa la feature X", es "haz uno o varios ciclos red-green-refactor para el requirement X". Cada uno de esos ciclos es UN test fallando primero, la mínima impl para hacerlo pasar, y refactor.
Este es el LOOP.md que usamos en la demo de este artículo. Es genérico, la kata aparece SOLO en TASK.md. Este harness vale igual para construir una calculadora, un parser, una API o un compilador.
# Ralph Loop Harness You are working in a loop. Each iteration starts fresh. The git history and the files on disk are your memory. This file (`LOOP.md`) defines HOW you work. `TASK.md` defines WHAT you are building. ## Phase 0a: Orientation - Read `TASK.md`. The `- [ ]` items are unfinished, `- [x]` are done. - Run `git log --oneline -20` - Run `ls -R src/ tests/ 2>/dev/null` - Run `npm test 2>&1 | tail -30` ## Phase 1: Pick the next requirement Open `TASK.md`. Pick the lowest-numbered unchecked `- [ ]` item. Do ONLY that one. One requirement per loop. ## Phase 2: Implement with strict TDD baby steps For each behavioural slice of the requirement: RED: one failing test (style: 9007). Run, see it fail. GREEN: minimum production code to pass. Faking allowed if next test in same iteration generalises it. REFACTOR: clean up with green tests. Re-run. ## Phase 3: Commit and update TASK.md - Flip the `- [ ]` you just finished to `- [x]` in `TASK.md`. - `git commit -m "ralph: R<n> <short summary>"` ## Phase 4: Completion check Only when EVERY requirement AND EVERY acceptance criterion in `TASK.md` is `- [x]`. Verify each. If all green: <promise>DONE</promise> ## 9001: DO NOT lie to escape the loop ## 9002: Do NOT implement placeholders (except inside one baby step) ## 9003: One requirement per loop ## 9004. Strict TDD: failing test BEFORE production code, always ## 9005: Parallel subagents for reads, sequential for builds ## 9006: Verify, do not assume ## 9007: Test naming style guide (NON-NEGOTIABLE) - Describe: `describe("The [Subject]", ...)`, domain concept, not function name. - Test cases: DOMAIN verbs (calculates, sums, accepts, rejects, ignores, lists, allows). NOT technical verbs (returns, throws, calls, includes, splits). - AAA with blank lines for tests with setup. - In Phase 4, verify every test name follows this.
Y este es el TASK.md correspondiente. Es la kata de String Calculator de Roy Osherove, perfecta para ilustrar TDD strict porque sus diez requirements están diseñados para forzar baby steps.
# Task: String Calculator kata (Roy Osherove) ## What Build a function `add(input: string): number` in TypeScript. ## Requirements Mark each one `- [x]` when done. Do them in order. ONE per loop. - [ ] R1: empty string returns 0 - [ ] R2: a single number returns the number - [ ] R3: two numbers comma-separated - [ ] R4: arbitrary amount of numbers - [ ] R5: newlines as separator too - [ ] R6: custom single-char delimiter "//;\n1;2" - [ ] R7: negatives throw Error("negatives not allowed: <list>") - [ ] R8: numbers greater than 1000 are ignored - [ ] R9: delimiters of arbitrary length "//[***]\n1***2***3" - [ ] R10: multiple custom delimiters "//[*][%]\n1*2%3" ## Acceptance The loop ends only when ALL items below are `- [x]`. - [ ] `npm test` green with 12+ tests - [ ] `npm run typecheck` zero errors - [ ] `src/calculator.ts` exports `add(input: string): number` - [ ] `README.md` with 3 real examples - [ ] Every test name follows the 9007 style guide in `LOOP.md`
Ese es el contrato entero. Dos archivos. Uno define cómo se trabaja (reutilizable), el otro define qué se construye (con checkboxes que se van marcando).
Tres detalles rompen con el Huntley canónico:
Y, por encima de los tres, la separación de archivos en sí misma. El Huntley original mete todo en un PROMPT.md único. Separarlo en LOOP.md + TASK.md es un cambio arquitectónico: el harness queda reutilizable, y la spec del proyecto vive con el código como cualquier README o PRD.
Lo verás en la demo: cada commit del log es un requirement, dentro de cada requirement hay uno o dos ciclos TDD limpios, los nombres de los tests cuentan lo que la calculadora HACE en lenguaje de dominio, y el TASK.md final tiene todos los checkboxes a [x], el plan se ha ido marcando solo.
Hay cuatro formas de correr Ralph dentro de Claude Code. Las dos primeras son las que llevan más de un año en el ecosistema. La tercera es nueva, Anthropic ha sacado un comando nativo /goal en mayo de 2026 que es Ralph como primitiva oficial, con un giro inteligente que ataca el problema del modelo que miente para salir. La cuarta son los wrappers de comunidad. Vamos por orden.
Es lo que publicó Huntley, adaptado a la separación LOOP.md + TASK.md. Un bucle infinito que le pasa LOOP.md al binario claude; desde Phase 0 el modelo lee TASK.md como otro fichero más del repo.
while :; do cat LOOP.md | claude -p --model opus --permission-mode auto done
Atención al flag --permission-mode auto. Hasta marzo de 2026 lo habitual aquí era --dangerously-skip-permissions, que desactiva toda comprobación: Claude ejecuta lo que sea sin preguntar. Funciona, pero te queda Ralph corriendo en tu máquina con permisos absolutos. En marzo de 2026, Anthropic publicó auto mode como reemplazo más sensato: un clasificador independiente revisa cada acción antes de ejecutarla y bloquea lo destructivo (force push, deploys a producción, curl | bash, exfiltración) mientras deja pasar lo cotidiano (ediciones en tu working dir, instalar dependencias del lockfile, push a la rama en la que estás). Si el clasificador bloquea tres veces seguidas o veinte totales, el loop aborta. Para un Ralph headless en -p, ese aborto es exactamente la red de seguridad que faltaba. Bypass sigue siendo válido para sandboxes aislados como contenedores efímeros, pero auto debería ser tu default.
Cada vuelta es una sesión nueva de Claude Code. El contexto del modelo arranca de cero. Lee LOOP.md por stdin, lee TASK.md desde Phase 0, mira el git log, hace su tarea, commitea (marcando el - [ ] correspondiente en TASK.md), sale. La siguiente vuelta repite. No hay estado en sesión. Todo el estado vive en archivos.
Para que esto sirva en la práctica, hay que añadirle dos cosas:
while :;. Sin un cap, si el modelo no detecta su propia finalización, sigue ad infinitum gastando tokens.<promise>DONE</promise> cuando todos los criterios se cumplen, y el wrapper detecta esa cadena con grep.Eso lleva al script real que usamos en la demo:
#!/bin/bash # ralph.sh: Huntley-style loop for Claude Code MAX_ITERS=${1:-20} COMPLETION="<promise>DONE</promise>" for iter in $(seq 1 $MAX_ITERS); do echo "[ralph] iter $iter/$MAX_ITERS" LOG="logs/iter-$(printf '%02d' $iter).log" cat LOOP.md | claude -p \ --model opus \ --permission-mode auto \ --max-budget-usd 5 \ > "$LOG" 2>&1 if grep -qE "^[[:space:]]*$COMPLETION[[:space:]]*$" "$LOG"; then echo "[ralph] DONE detected, exit" break fi done
Aviso del que aprendí preparando este artículo. Mi primera versión usaba grep -q "$COMPLETION", es decir un match por substring. Funciona el 99% del tiempo pero falla cuando el modelo emite frases del tipo "R4-R10 remain unfinished, so not emitting <promise>DONE</promise>". El modelo está siendo honesto, dice explícitamente que NO emite el promise, pero el grep encuentra la cadena dentro de la frase y corta el loop. Resultado: el modelo no mintió, el wrapper sí. La versión correcta es la de arriba con -E "^[[:space:]]*$COMPLETION[[:space:]]*$": solo cuenta si la línea CONTIENE exclusivamente el tag (con espacios opcionales). El plugin oficial de Anthropic resuelve lo mismo con perl multilínea extrayendo el contenido dentro de las tags y comparándolo exactamente con el promise configurado. Misma idea, distinta sintaxis.
Lo que pasa en realidad cuando llamas claude -p:
┌──────────────────────────────────────────────┐ │ Iteración N │ │ ┌────────────────────────────────────────┐ │ │ │ Claude Code arranca con contexto │ │ │ │ limpio │ │ │ │ │ │ │ │ 1. Recibe LOOP.md por stdin │ │ │ │ 2. Lee TASK.md y CLAUDE.md/AGENTS.md │ │ │ │ 3. Phase 0: ls, git log, npm test │ │ │ │ 4. Phase 1: picks next item │ │ │ │ 5. Phase 2: implements │ │ │ │ 6. Phase 3: git commit │ │ │ │ 7. (opcional) Phase 4: <promise>DONE │ │ │ │ 8. Sale │ │ │ └────────────────────────────────────────┘ │ │ Wrapper detecta promise → para │ │ No detectado → siguiente vuelta │ └──────────────────────────────────────────────┘
Este modelo de ejecución es lo que Huntley quería: contexto fresco siempre, persistencia mecánica vía bash, control total del operador. La pega es que cada arranque de claude -p tarda unos segundos en levantar la sesión, así que hay un overhead fijo por vuelta.
/ralph-loop de AnthropicEn diciembre de 2025, Anthropic publicó un plugin oficial llamado ralph-loop en su marketplace. Se instala con /plugin install ralph-loop@anthropic y añade tres comandos: /ralph-loop, /cancel-ralph y /help.
La sintaxis es prácticamente la misma:
/ralph-loop "Build a REST API for todos. Output <promise>COMPLETE</promise> when done." --completion-promise "COMPLETE" --max-iterations 50
Y aquí viene la sorpresa filosófica. El plugin oficial NO arranca una sesión nueva cada vuelta. Lo que hace es engancharse a un Stop hook, un mecanismo de Claude Code que se dispara cuando el agente intenta salir de la sesión. El hook intercepta la salida, detecta si hay un loop activo, y le manda al agente el mismo prompt de vuelta. Todo dentro de la misma sesión.
┌──────────────────────────────────────────────────────┐ │ SESIÓN INTERACTIVA DE CLAUDE CODE │ │ │ │ /ralph-loop "task" --completion-promise "DONE" │ │ │ │ │ ├─ Crea .claude/ralph-loop.local.md (state file) │ │ │ con frontmatter YAML: iteration, max, promise │ │ │ │ │ ├─ Claude empieza a trabajar │ │ │ │ │ ├─ Claude termina la tarea, intenta salir │ │ │ │ │ ├─ Stop hook intercepta │ │ │ - Lee el state file │ │ │ - Si no se cumplió el promise: incrementa │ │ │ iter, mete el mismo prompt como next user msg │ │ │ - Si se cumplió: borra el state file y permite │ │ │ salir │ │ │ │ │ └─ Loop sigue dentro de la misma sesión │ └──────────────────────────────────────────────────────┘
Es una decisión de ingeniería razonable: aprovecha la sesión y evita el overhead de arrancar claude cada vuelta. Pero rompe el principio fundamental de Huntley, que era contexto fresco cada vuelta. En el plugin oficial, el modelo arrastra la conversación entera, todas las llamadas a herramientas que ha hecho, todos los mensajes del sistema. Acumula. Eso significa que en tareas largas vuelves a tener el problema del context rot que Ralph original eliminaba por diseño.
El plugin compensa con dos detalles inteligentes:
Anti-cheat explícito. En la spec del /ralph-loop figura literalmente: "If a completion promise is set, you may ONLY output it when the statement is completely and unequivocally TRUE. Do not output false promises to escape the loop, even if you think you're stuck or should exit for other reasons. The loop is designed to continue until genuine completion." El modelo recibe el aviso de que no puede mentir para salir. Es una defensa débil, los modelos a veces mienten igual, pero estadísticamente ayuda.
Aislamiento por sesión. El state file vive en .claude/ralph-loop.local.md y guarda el session_id. Si abres una segunda sesión de Claude Code en el mismo proyecto, el hook detecta que es otra sesión y la deja salir normalmente. No te bloquea todas las sesiones que tengas abiertas.
¿Cuándo conviene cada uno? Bash puro de Huntley si quieres pureza filosófica y tareas largas (más de 5-10 iteraciones). Plugin oficial si quieres comodidad y tu tarea es corta y bien acotada. Yo personalmente uso el bash puro para todo, porque me da más control y porque el overhead de arrancar la sesión es despreciable comparado con el coste de razonar bien.
/goal (Claude Code 2.1.139, mayo 2026)En mayo de 2026 Anthropic dio el salto del plugin al comando nativo. Claude Code 2.1.139 trae /goal integrado en el binario, sin instalar nada:
/goal "Implement the String Calculator kata in src/calculator.ts following TDD. The goal is reached when npm test passes with 12+ tests covering R1-R10 from the spec in TASK.md and npm run typecheck is clean."
Y arranca un loop autónomo donde Claude planea, escribe tests, refactoriza, verifica y vuelve a iterar hasta cumplir la condición. Funciona en modo interactivo, con -p (headless) y en Remote Control. Trackea elapsed time, turns y tokens, así que sabes exactamente cuánto te ha costado.
El giro inteligente, y la razón por la que /goal no es simplemente "el plugin pero mejor", está en su supervisor architecture:
┌──────────────────────────────────────────────────────┐ │ AGENTE PRINCIPAL │ │ (sesión 1, opus 4.7) │ │ │ │ Trabaja en la tarea, itera, cree que ha │ │ terminado, marca el goal como cumplido │ │ │ │ │ │ │ ▼ │ └──────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────┐ │ AGENTE SUPERVISOR │ │ (sesión 2 INDEPENDIENTE, modelo separado) │ │ │ │ Lee el estado final del repo desde cero. Sin │ │ contexto del agente principal. Su único trabajo │ │ es verificar si el goal está realmente cumplido │ │ o si el otro se está engañando. │ │ │ │ - Si SÍ: notifica al usuario, cierra. │ │ - Si NO: devuelve el control al agente principal │ │ con las pegas concretas. │ └──────────────────────────────────────────────────────┘
Esto separa el agente que trabaja del que decide cuando está terminado. Lo que ataca de frente el problema que mencionamos en la opción B, el modelo a veces emite el promise para escapar del loop. Con /goal, no hay forma de hacer trampa: otro agente, sin contexto contaminado, audita el repo.
¿Cuándo conviene cada opción de las tres anteriores? Resumen rápido:
/ralph-loop si prefieres quedarte dentro de la sesión interactiva y no quieres orquestar nada por fuera./goal nativo si quieres lo más reciente y la garantía del supervisor. Es la apuesta de Anthropic a futuro.Hay al menos tres wrappers en GitHub que merece la pena conocer aunque no los uses:
frankbria/ralph-claude-code: añade circuit breakers (corta el loop si detecta tres errores iguales, edits-and-revert, lecturas repetidas sin progreso) y un cap configurable de llamadas por hora.mikeyobrien/ralph-orchestrator: orquestador con dashboard, métricas y reporting.snarktank/ralph: wrapper alrededor de un PRD en JSON con seguimiento de stories.Lo interesante de estos no es usarlos tal cual. Es lo que añaden encima del bash puro: detección de patologías. Si tu loop va a correr media noche, los circuit breakers son tu seguro. Pero, ojo, son código de terceros corriendo con --dangerously-skip-permissions. Lee lo que instalas.
OpenCode es la herramienta que materializa el principio de no depender de un solo proveedor. Es un agente de terminal open source, agnóstico al modelo, autenticable por OAuth contra suscripciones que ya tienes (ChatGPT Plus/Pro, GitHub Copilot, OpenCode Go) o contra modelos abiertos corriendo en local. No te ata a nadie.
OpenCode no tiene un plugin oficial de Ralph. Y lo bueno es que no lo necesita. La filosofía del proyecto es minimalismo y composición, así que Ralph en OpenCode es exactamente lo que Huntley publicó originalmente: un bash de una línea.
while :; do opencode run --model openai/gpt-5.5 "$(cat LOOP.md)" done
Y exactamente lo mismo con un cap de iteraciones y detector de promesa:
#!/bin/bash # ralph.sh: Huntley-style loop for OpenCode MAX_ITERS=${1:-20} MODEL=${2:-openai/gpt-5.5} COMPLETION="<promise>DONE</promise>" for iter in $(seq 1 $MAX_ITERS); do echo "[ralph] iter $iter/$MAX_ITERS model=$MODEL" LOG="logs/iter-$(printf '%02d' $iter).log" opencode run --model "$MODEL" "$(cat LOOP.md)" > "$LOG" 2>&1 if grep -qE "^[[:space:]]*$COMPLETION[[:space:]]*$" "$LOG"; then echo "[ralph] DONE detected" break fi done
Cuatro detalles importantes:
--prompt. No es estrictamente stdin como Claude Code. Para archivos largos uso "$(cat LOOP.md)".provider/modelo. La lista la sacas con opencode models. Para esta demo usé openai/gpt-5.5, que en mi caso está conectado vía OAuth con mi suscripción ChatGPT Pro (sin API key, sin pay-per-token).--dangerously-skip-permissions porque OpenCode no exige confirmación interactiva en modo run. Es directamente headless.agentes, no solo con modelos. Un agente en OpenCode (build, plan, general, los que tú definas en ~/.config/opencode/agent/ o en opencode.json) empaqueta un system prompt, un modelo, una lista de herramientas permitidas y un perfil de permisos. Cuando ejecutas opencode run --model X sin más, OpenCode usa el agente primario (por defecto build) y le sobrescribe el modelo con X. Si quieres usar otro agente entero, pasas --agent NAME. Los listas con opencode agent list. En esta demo simple usé el agente build con el modelo openai/gpt-5.5, los dos están entre los defaults de OpenCode.Esto es importante. La decisión de no usar API keys es deliberada. Cuando dependes de una API key estás pagando por tokens al margen de cualquier suscripción que ya tengas. Cuando dependes de OAuth contra tu suscripción de ChatGPT Plus, Pro o Copilot, estás aprovechando lo que ya pagas mensualmente.
En OpenCode esto se configura con:
opencode auth # o, equivalente: opencode providers
Y dentro del menú te puedes loguear con OAuth contra OpenAI (ChatGPT) o contra GitHub Copilot. Para modelos open weights de pago, OpenCode tiene su propio gateway de suscripción flat llamado OpenCode Go (5-10 dólares al mes) que da acceso a Kimi K2.6, GLM-5.1, DeepSeek V4, Qwen y compañía sin manejar API keys.
La lista que ves cuando ejecutas opencode models en mi máquina contiene:
openai/gpt-5.4-mini-fast openai/gpt-5.5 openai/gpt-5.5-pro ← via OAuth ChatGPT Pro opencode-go/kimi-k2.6 ← via OpenCode Go opencode-go/glm-5.1 opencode-go/qwen3.7-max lmstudio/qwen/qwen3-coder-30b ← local lmstudio/openai/gpt-oss-20b ← local ...
Cualquiera de ellos puede ir dentro del bash de Ralph. Sin tocar una API key.
Anthropic decidió en enero de 2026 cerrar el OAuth a OpenCode con una política nueva en sus términos de servicio sobre uso de credenciales. El resultado práctico es que no puedes usar modelos Claude desde OpenCode sin pagar API tokens, y la política oficial es que esos tokens están reservados para Claude Code.
Por eso este artículo separa explícitamente: Claude Code lleva modelos Claude (Opus, Sonnet, Haiku), OpenCode lleva todo lo demás. Es una decisión estratégica: no quieres que tu productividad dependa de un proveedor que puede cambiar las reglas un martes cualquiera.
Para que esto no quede en pura teoría, monté la kata desde cero en los dos agentes. Mismo LOOP.md (el harness con TDD strict + style guide que viste arriba), mismo TASK.md (los diez requirements de la kata con sus checkboxes en - [ ]), mismo scaffolding mínimo (un package.json con TypeScript y vitest, un tsconfig.json con strict: true, un vitest.config.ts), misma carpeta de partida. Y dejé correr ralph.sh con un cap de 14 iteraciones por seguridad.
/tmp/ralph-demo/claude-code/ /tmp/ralph-demo/opencode/ ├── LOOP.md ← idéntico ├── LOOP.md ← idéntico ├── TASK.md ← idéntico ├── TASK.md ← idéntico ├── package.json ├── package.json ├── tsconfig.json ├── tsconfig.json ├── vitest.config.ts ├── vitest.config.ts └── .gitignore └── .gitignore
Y el wrapper, también idéntico para los dos. El bash le pasa al modelo solo LOOP.md; desde Phase 0 el modelo lee TASK.md como otro archivo más:
bash ralph.sh claude opus 14 /tmp/ralph-demo/claude-code bash ralph.sh opencode openai/gpt-5.5 14 /tmp/ralph-demo/opencode
En Claude Code, Opus 4.7 con --permission-mode auto. En OpenCode, gpt-5.5 vía OAuth contra ChatGPT Pro. Cero API keys. Cero pago por tokens al margen de las suscripciones.
Claude Code (Opus 4.7), 11 iteraciones, 832 s (~14 min)
iter 1 60s R1: empty expression calculates zero iter 2 51s R2: single operand calculates itself iter 3 56s R3: two comma-separated operands sum iter 4 65s R4: arbitrary amount of operands sum iter 5 45s R5: newlines as separators iter 6 60s R6: custom single-char delimiter iter 7 74s R7: negatives are rejected with full list iter 8 51s R8: operands above 1000 are ignored iter 9 63s R9: arbitrary-length bracketed delimiter iter 10 121s R10: multiple custom delimiters iter 11 186s R11 (autoañadido): README with three usage examples + DONE
Doce commits, uno por requirement (más el inicial). Iter 11 es interesante: Opus se encontró con que el acceptance pedía README y los requirements R1-R10 no incluían "escribir el README". En vez de mentir o tachar el acceptance, añadió R11 al TASK.md y lo trabajó en la siguiente iteración. Es exactamente el comportamiento que pide Phase 3 del LOOP.md: "if you discover follow-up work, add it to the bottom of the requirement list with the next R number". El modelo siguió el protocolo en vez de improvisar.
OpenCode (gpt-5.5 vía OAuth), 11 iteraciones, 915 s (~15 min)
iter 1 116s R1: empty string returns zero iter 2 62s R2: single number expression iter 3 72s R3: comma-separated pair iter 4 65s R4: arbitrary amount of numbers iter 5 93s R5: newline separators iter 6 92s R6: custom delimiter declaration iter 7 86s R7: reject negative operands iter 8 91s R8: ignore operands greater than 1000 iter 9 78s R9: arbitrary-length delimiters iter 10 81s R10: multiple custom delimiters iter 11 79s acceptance completion (README + checks finales) + DONE
Mismo número de iteraciones, mismo número de commits. Diferencia con Claude: en vez de añadir un R11 explícito al plan, OpenCode hizo una iteración final cuyo commit es "ralph: acceptance completion". Llegó al mismo resultado por un camino sutilmente distinto. Es información: cuando el LOOP.md sugiere pero no fuerza un comportamiento, modelos distintos lo interpretan distinto.
Las dos demos terminaron con un TASK.md 100% marcado. Aquí el de Claude Code (el de OpenCode es virtualmente idéntico, sin R11):
## Requirements - [x] R1: empty string returns 0 - [x] R2: a single number returns the number - [x] R3: two numbers comma-separated - [x] R4: arbitrary amount of numbers - [x] R5: newlines as separator too - [x] R6: custom single-char delimiter "//;\n1;2" - [x] R7: negatives throw Error("negatives not allowed: <list>") - [x] R8: numbers greater than 1000 are ignored - [x] R9: delimiters of arbitrary length "//[***]\n1***2***3" - [x] R10: multiple custom delimiters "//[*][%]\n1*2%3" - [x] R11: write README.md with 3 real usage examples ## Acceptance - [x] npm test green with 12+ tests - [x] npm run typecheck zero errors - [x] src/calculator.ts exports add() - [x] README.md with 3 real usage examples - [x] Every test name follows the 9007 style guide in LOOP.md
Cero - [ ] sin marcar. El modelo fue cerrando los checkboxes uno a uno, vuelta a vuelta. Cuando llegó a Phase 4, abrió el archivo, vio que todo estaba en [x], ejecutó los comandos de verificación y emitió el promise.
Este es el detalle que merece subrayar: el TASK.md final NO es un archivo extra que el modelo escribe al acabar. ES el plan vivo del proyecto, marcado en tiempo real, commit a commit. Si abres el repo a mitad del loop (iter 5, por ejemplo) y haces cat TASK.md, ves exactamente qué hay hecho y qué falta. Y el git log ya tiene los commits cuyos mensajes corresponden a los [x] de arriba. El estado del proyecto es transparente sin necesidad de un dashboard.
Los dos archivos tests/calculator.test.ts siguen al pie de la letra el style guide del 9007. Cero verbos técnicos.
Claude Code:
describe("The Calculator", () => { it("calculates zero for an empty expression", () => { ... }); it("calculates the number itself for a single operand", () => { ... }); it("sums two comma-separated operands", () => { ... }); it("sums an arbitrary amount of comma-separated operands", () => { ... }); it("accepts newlines as separators between operands", () => { ... }); it("accepts a custom single-character delimiter declared in the header", () => { ... }); it("rejects expressions containing a negative operand", () => { ... }); it("lists every negative operand in the rejection message", () => { ... }); it("ignores operands greater than 1000", () => { ... }); it("accepts a custom delimiter of arbitrary length declared in brackets", () => { ... }); it("accepts multiple single-character delimiters declared in brackets", () => { ... }); it("accepts multiple arbitrary-length delimiters declared in brackets", () => { ... }); });
OpenCode:
describe("The Calculator", () => { it("calculates zero for an empty expression", () => { ... }); it("calculates the operand for a single-number expression", () => { ... }); it("sums two comma-separated operands", () => { ... }); it("sums an arbitrary amount of comma-separated operands", () => { ... }); it("sums operands separated by commas and line breaks", () => { ... }); it("accepts a custom delimiter declaration", () => { ... }); it("accepts an arbitrary-length custom delimiter declaration", () => { ... }); it("accepts multiple custom delimiter declarations", () => { ... }); it("accepts multiple arbitrary-length custom delimiter declarations", () => { ... }); it("rejects expressions containing a negative operand", () => { ... }); it("rejects expressions containing every negative operand", () => { ... }); it("ignores operands greater than 1000", () => { ... }); });
Ni un solo returns, throws, calls, includes o splits. Solo verbos de dominio: calculates, sums, accepts, rejects, lists, ignores. Y los dos describe empiezan con "The Calculator", el sujeto de dominio, no con el nombre de la función.
| Métrica | Claude Code (Opus 4.7) | OpenCode (gpt-5.5) |
|---|---|---|
Iteraciones hasta <promise>DONE</promise> | 11 | 11 |
| Tiempo total | 832 s (14 min) | 915 s (15 min) |
| Iter más rápida | 45 s (R5) | 62 s (R2) |
| Iter más lenta | 186 s (R11 + completion) | 116 s (R1, primera) |
Commits ralph: | 12 | 12 |
| Tests creados | 12 | 12 |
TASK.md [x] final | 16/16 (incluye R11 autoañadido) | 15/15 |
| Style guide 9007 | 100% compliance | 100% compliance |
| TDD strict verbalizado | sí, en cada iter | sí, visible en cada diff |
| Coste API | $0 (suscripción Claude Max) | $0 (ChatGPT Pro OAuth) |
| Permisos | --permission-mode auto | nativo OpenCode run |
| Fallos durante el loop | 0 | 0 |
La separación LOOP.md + TASK.md funciona. Ningún modelo se enredó leyendo dos archivos en vez de uno. La Phase 0 abre TASK.md, el modelo identifica el primer - [ ], va a por él, y en Phase 3 lo flipa a - [x]. Cero confusión. Y como bonus, el LOOP.md queda como verdadero contrato genérico, el mismo serviría mañana para una kata de Bowling Game o para un refactor de un módulo legacy.
El TASK.md como plan vivo es más cómodo que un IMPLEMENTATION_PLAN.md generado. En la versión anterior del prompt único, el modelo creaba un IMPLEMENTATION_PLAN.md en la primera iteración a partir de los requirements del prompt. Tener los checkboxes ya en TASK.md desde el día cero ahorra esa iteración inicial y mantiene la spec y el plan como la misma cosa. La spec no se queda obsoleta porque cada iteración la actualiza.
Style guide en el prompt = style guide en el output. Igual que en la versión anterior, los dos modelos siguieron el 9007 al pie de la letra. Cero verbos técnicos.
Los modelos respetaron one-requirement-per-loop. Ni R4+R5 juntos, ni intentos de saltar adelante.
Los dos terminaron prácticamente al mismo tiempo. 14 min vs 15 min, con el mismo número de iteraciones. El factor que más pesa no es el modelo, es la disciplina del prompt y los criterios de aceptación.
Iniciativa diferenciada. Claude se encontró con un hueco en el plan (faltaba R para README) y lo añadió explícitamente como R11, siguiendo Phase 3. OpenCode prefirió cerrar el hueco en una iter de "acceptance completion" sin tocar el plan. Mismo resultado, dos formas de interpretar el mismo LOOP.md. Información útil sobre cómo cada modelo se relaciona con las reglas cuando hay margen.
Coste real: cero por los dos lados. Claude Code consumió cuota de la suscripción Claude Max, no API tokens. OpenCode con ChatGPT Pro OAuth no marcó nada en la factura de OpenAI, entra dentro de la cuota Pro. Las dos demos se pagaron con suscripciones que ya estaban ahí. La factura marginal del experimento entero: 0,00 dólares.
Los git log quedaron así. El TASK.md final (con todos los [x]) es el plano del recorrido, los commits son la cronología:
Claude Code Opus 4.7 OpenCode gpt-5.5 OAuth ───────────────────── ────────────────────── R11 README with three usage examples acceptance completion R10 multiple custom delimiters R10 multiple custom delimiters R9 arbitrary-length bracketed delimiter R9 arbitrary-length delimiters R8 operands above 1000 are ignored R8 ignore operands greater than 1000 R7 negatives are rejected with full list R7 reject negative operands R6 custom single-char delimiter R6 custom delimiter declaration R5 newlines as separators R5 newline separators R4 arbitrary amount of operands sum R4 arbitrary amount of numbers R3 two comma-separated operands sum R3 comma-separated pair R2 single operand calculates itself R2 single number expression R1 empty expression calculates zero R1 empty string returns zero initial scaffold initial scaffold
Dos modelos distintos, dos suscripciones distintas, dos proveedores distintos. Misma kata, mismo LOOP.md, mismo TASK.md, misma disciplina TDD strict, mismo style guide, mismo resultado.
Eso es Ralph. No el modelo. El loop.
Hasta aquí hemos visto Ralph como un loop con un solo agente y un solo modelo. OpenCode tiene otra primitiva interesante: en una misma sesión, el agente primario puede delegar partes del trabajo en sub-agentes distintos, cada uno con su propio modelo, su system prompt y sus permisos. La fase de razonamiento puede ir a un agente que usa un modelo caro y bueno razonando. La de implementación a uno rápido y barato. La de commit a uno local que no cuesta nada.
Antes de seguir conviene aclarar el vocabulario, porque en OpenCode las tres palabras agente, modo y modelo significan cosas distintas:
MODELO El LLM concreto (provider/nombre). openai/gpt-5.5, opencode-go/kimi-k2.6, lmstudio/qwen3.6-35b-a3b. AGENTE Un perfil de trabajo. Empaqueta: system prompt, modelo asignado, herramientas permitidas, permisos. Los listas con `opencode agent list`. Por defecto hay uno llamado `build` que es el primary, el que ejecuta tu instrucción cuando lanzas `opencode run`. Puedes crear los tuyos en `~/.config/opencode/ agent/<nombre>.md` o declararlos en `opencode.json`. MODO Una "skin" más ligera que sobrescribe modelo y permisos del agente actual sin cambiar de agente. Útil dentro de la TUI (Tab para ciclar modos), menos relevante para Ralph headless.
Para Ralph multi-agente lo que importa son los AGENTES. El loop arranca con el agente primario (build por defecto), y desde el LOOP.md ese agente puede invocar a otros usando la herramienta task("nombre del agente", ...). Cada llamada arranca el sub-agente con SU modelo configurado, hace lo que le pides, y devuelve el resultado al primario.
┌──────────────────────────────────────────────────────────────┐ │ BUCLE RALPH (externo) │ │ │ │ while no promise; do │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ Agente PRIMARIO en OpenCode (lo arranca el wrapper) │ │ │ │ │ │ │ │ $ opencode run --agent build "$(cat LOOP.md)" │ │ │ │ (build es el primary, podrías usar el que quieras)│ │ │ │ │ │ │ │ El agente primario delega a sub-agentes según fase: │ │ │ │ │ │ │ │ task("planner") → GPT-5.5 vía OAuth ChatGPT Pro │ │ │ │ task("coder") → Kimi K2.6 vía OpenCode Go │ │ │ │ task("reviewer") → GPT-5.5 vía OAuth ChatGPT Pro │ │ │ │ task("committer")→ Qwen3.6 A3B local (LM Studio) │ │ │ │ │ │ │ │ Todo dentro de UNA vuelta de Ralph. │ │ │ │ Ralph no se entera. Solo ve el output final. │ │ │ └───────────────────────────────────────────────────────┘ │ │ │ │ end │ └──────────────────────────────────────────────────────────────┘
¿Por qué montar esto? Porque te permite ajustar coste y calidad por fase, no por iteración. La fase de razonamiento usa el modelo más capaz. La de implementación usa el de mejor relación coste/calidad. La de commit usa un modelo local que no cuesta nada y es perfectamente capaz de redactar un mensaje de commit razonable. Y todo eso ocurre dentro de una vuelta de Ralph, no entre vueltas.
Configurarlo requiere declarar los agentes en opencode.json:
{ "agent": { "planner": { "description": "Razona sobre arquitectura y decisiones de diseño", "model": "openai/gpt-5.5", "temperature": 0.2, "tools": { "edit": false, "write": false, "bash": false } }, "coder": { "description": "Implementa código siguiendo el plan", "model": "opencode-go/kimi-k2.6", "temperature": 0.0 }, "reviewer": { "description": "Revisa diffs y propone mejoras antes de commit", "model": "openai/gpt-5.5", "temperature": 0.0, "tools": { "edit": false, "write": false } }, "committer": { "description": "Redacta mensaje de commit y commitea", "model": "lmstudio/qwen/qwen3.6-35b-a3b", "temperature": 0.0 } } }
El LOOP.md instruye al agente primario a invocar cada sub-agente en la fase correspondiente: "In Phase 2, before writing code, invoke task('planner') with the chosen requirement. Then invoke task('coder') with the plan. Then task('reviewer') with the diff. Then task('committer') with the verified change."
Y el bash que orquesta todo sigue siendo el mismo, sin cambios. Solo se invoca al agente primario:
while :; do opencode run --agent build "$(cat LOOP.md)" done
(El --agent build es opcional porque build ya es el primary; lo escribo explícito para que se vea en el script qué agente arranca el loop.)
OpenCode internamente lee el opencode.json, resuelve los sub-agentes que el primario invoca y rutea cada uno a su modelo. Ralph no se entera. No tiene que enterarse.
Detalle pequeño que cambia bastante: el sub-agente committer con Qwen local cuesta exactamente cero. Cero tokens al proveedor. La factura del loop entero baja proporcionalmente. Y si tienes planner en GPT-5.5 OAuth y coder en Kimi vía OpenCode Go (suscripción flat), tampoco hay coste marginal por iteración. Todo el loop puede salir gratis.
Es la combinación que casi nadie está cubriendo en los artículos en inglés sobre Ralph. La mayoría se queda en el bucle con un solo agente y un solo modelo. Pero la frontera real está aquí: usar el loop externo de Ralph como wrapper de persistencia y la composición de agentes de OpenCode como router de coste y especialidad. La primitiva mecánica abrazando la primitiva económica.
Tras todo lo anterior es fácil confundirse y pensar que Ralph es la solución a cualquier problema de desarrollo. No lo es. Hay tareas en las que un loop de Ralph es la mejor opción que tienes, y hay tareas en las que es la peor.
La regla que mejor me ha funcionado: si no puedes escribir un script que verifique automáticamente "esto está hecho", no uses Ralph. Si puedes escribir ese script, Ralph probablemente sea la mejor opción.
Si tu loop empieza a quemar cuota a un ritmo raro para una tarea bien acotada (varias horas de Claude Max en una sola kata, o agotar la cuota Pro de ChatGPT en cuestión de iteraciones), algo va mal. O el LOOP.md o el TASK.md está pidiendo demasiado por vuelta, o falta backpressure, o el modelo está atascado en un patológico que necesita un circuit breaker. Vigila.
Lo más importante para cerrar. Ralph no es un sustituto del juicio del operador. Es un amplificador del prompt y del entorno. Si escribes un buen LOOP.md con un TASK.md afinado, montas tests sólidos y eliges la tarea adecuada, Ralph hace muchísimo trabajo por ti. Si haces lo contrario, te quema dinero.
Como dice Huntley en su post original:
"Ralph will test you. Every time Ralph takes a wrong direction, I haven't blamed the tools; instead, I've looked inside."
Esto no va de tener un agente que programe por ti mientras tomas café. Va de tener una primitiva nueva, el loop con persistencia mecánica, que añadir a tu caja de herramientas. Igual que el while se añadió al if, igual que git rebase se añadió a git merge. No reemplaza nada. Suma.
Hay una idea importante detrás de todo esto: tu productividad no la debería decidir un proveedor.
Ralph encaja exactamente en esa idea.
Es la primitiva más simple posible, un bucle bash, y a la vez es la más libre. No depende de Anthropic. No depende de OpenAI. No depende de un framework de agentes. Depende del lenguaje universal de los desarrolladores desde 1989: shell, archivos, git. Eso lo hace portable a cualquier modelo que tenga un binario CLI: Claude Code, OpenCode, Codex CLI, Copilot CLI, Cursor Agent, Qwen Code. Sin lock-in.
Y precisamente por eso me parece importante. No tanto porque Ralph sea revolucionario en sí. Es un bash de una línea, después de todo. Lo importante es lo que demuestra: que las primitivas pequeñas, combinadas con archivos persistentes y un buen sistema de verificación, escalan hasta resolver tareas que parecían exigir orquestadores complejos.
El loop más estúpido funcionó porque era estúpido.
Es el viejo principio KISS, Keep It Simple, Stupid, aplicado a la era de los agentes. Mientras todos los demás añadían capas, Huntley quitó capas. Un bash de una línea, dos archivos en disco, git. Y demostró que era suficiente.
La sencillez es una complejidad resuelta.
¿Quiéres leer más artículos como éste? Pues suscríbete a la newsletter