AboutOpinionesBlogContactoAuditoria

Software Crafters® 2026 | Creado con 🖤 para elevar el nivel de la conversación sobre programación en español | Legal

Ralph y la primitiva agéntica del loop

Ralph es la primitiva agéntica más simple posible: un bash de una línea que itera un agente de IA sobre un LOOP.md hasta cumplir la spec. Cómo se monta en Claude Code y OpenCode, con demo real lado a lado de la kata String Calculator.

Miguel A. Gómez47 min read

En la newsletter hablo de cómo diseñar mejor software. O lo que es lo mismo: escribir código sostenible.

Al suscribirte comparto contigo los libros que más me han hecho crecer como dev —los que todo desarrollador serio debería leer al menos una vez.

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

Ralph

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.

¿Qué es el loop de Ralph?

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.

El caso CURSED

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.

Lo que sí y lo que no es Ralph

Ralph es:

  • Un bucle externo a la sesión del agente
  • Una técnica de persistencia mecánica
  • Una forma de aprovechar que el modelo se autocorrige si ve su propio fallo
  • Adecuado para tareas con criterio de completitud verificable (tests, typecheck, lint)

Ralph no es:

  • Un agente nuevo
  • Una técnica para tareas creativas o de exploración (donde no hay "done" objetivo)
  • Una sustitución del juicio del operador
  • Magia que arregla un mal prompt

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.

Por qué funciona

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.

Contexto limpio cada vuelta

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.

Memoria distribuida en archivos

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.
  • El propio código: 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.

Backpressure: la pieza más importante del harness

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.

Subagentes en paralelo, build en serie

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.

Anatomía: LOOP.md + TASK.md

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.

La estructura del LOOP.md

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.                                    │
└──────────────────────────────────────────────┘

Phase 0: orientación

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:

  • "study", no "read": estudiar es más activo que leer. El modelo se lo toma más en serio.
  • "don't assume not implemented": no asumir que algo falta solo porque no lo ves en tu contexto. Comprueba antes.
  • "using parallel subagents": explicitar que las búsquedas se paralelizan.
  • "Ultrathink": una palabra reciente del lenguaje Anthropic, que en algunas implementaciones activa más profundidad de razonamiento.

Phase 1-4: ejecución con cinturón

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.

Guardrails 9001+

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:

  • No mentir para escapar el loop. El modelo, viéndose atascado, podría caer en la tentación de emitir el promise para terminar. Hay que decírselo explícito.
  • No placeholders. Nada de 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.
  • One task per loop. El instinto del modelo es resolver todo lo que ve. Ralph requiere lo contrario.
  • Verify, do not assume. Antes de afirmar que un test pasa, córrelo. Antes de afirmar que un archivo existe, haz ls.

Tamaño del prompt: menos es más

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.

Una mejora sobre Huntley: TDD strict por iteración

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.

LOOP.md de ejemplo

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.

TASK.md de ejemplo

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:

  1. El guardrail 9004 es nuevo. No basta con "write tests before code", obligas a "fail test first, see the failure, then write minimum code". Esa disciplina baja la sobreingeniería y aumenta la confianza en cada commit.
  2. El guardrail 9002 está matizado. Huntley dice "no placeholders ni fakes". TDD strict permite la fake-it transformation dentro de un baby step, siempre que el siguiente test del MISMO iteración la generalice. Si no, se queda como fake comprometido, eso sí es trampa.
  3. El guardrail 9007 mete style guide al loop. El LOOP.md no solo dice qué hacer, dice cómo se nombran las cosas. Nombres de tests en lenguaje de dominio (calculates, sums, rejects) en vez de lenguaje técnico (returns, throws). El loop pasa a ser contrato no solo de comportamiento sino de calidad de código.

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.

Ralph en Claude Code

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.

Opción A: Bash puro (la canónica)

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:

  1. Tope duro de iteraciones, porque el bucle es literal while :;. Sin un cap, si el modelo no detecta su propia finalización, sigue ad infinitum gastando tokens.
  2. Detector de finalización. La convención es que el modelo emita <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.

Opción B: Plugin oficial /ralph-loop de Anthropic

En 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:

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

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

Opción C: Comando nativo /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:

  • Bash puro si valoras el principio Huntley (contexto fresco siempre) y quieres control total del bucle desde shell. Es lo que usé en la demo.
  • Plugin oficial /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.

Opción D: Wrappers de comunidad

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.

Ralph en OpenCode

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.

Bash puro estilo Huntley, adaptado

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:

  1. OpenCode acepta el prompt como argumento posicional o por flag --prompt. No es estrictamente stdin como Claude Code. Para archivos largos uso "$(cat LOOP.md)".
  2. El modelo se especifica con formato 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).
  3. No hay --dangerously-skip-permissions porque OpenCode no exige confirmación interactiva en modo run. Es directamente headless.
  4. OpenCode trabaja con 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.

Autenticación OAuth, no API keys

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.

¿Y Anthropic?

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.

Demo comparada: la misma kata en Claude Code y OpenCode

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.

Setup idéntico

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

Lo que pasó iteración a iteración

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.

El TASK.md final, en los dos casos

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 tests que escribieron, lado a lado

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.

Cuadro comparativo

MétricaClaude Code (Opus 4.7)OpenCode (gpt-5.5)
Iteraciones hasta <promise>DONE</promise>1111
Tiempo total832 s (14 min)915 s (15 min)
Iter más rápida45 s (R5)62 s (R2)
Iter más lenta186 s (R11 + completion)116 s (R1, primera)
Commits ralph:1212
Tests creados1212
TASK.md [x] final16/16 (incluye R11 autoañadido)15/15
Style guide 9007100% compliance100% compliance
TDD strict verbalizadosí, en cada itersí, visible en cada diff
Coste API$0 (suscripción Claude Max)$0 (ChatGPT Pro OAuth)
Permisos--permission-mode autonativo OpenCode run
Fallos durante el loop00

Lo que se aprende del lado a lado

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 de las dos demos

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.

Bonus: Ralph encima de un setup multi-agente en OpenCode

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.

Cuándo usar Ralph y cuándo no

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.

Cuándo brilla Ralph

  • Refactorizaciones masivas: renombrar una función en 200 archivos, migrar de una librería a otra, actualizar la firma de una API en todo un módulo. Cada iteración hace un fragmento. El test suite valida. Si un cambio rompe algo, la iteración siguiente lo detecta y lo corrige.
  • Migraciones de versión: subir TypeScript de 4 a 5, migrar de Node 18 a Node 22, cambiar una versión major de un framework. Los criterios de éxito son objetivos (typecheck verde, tests verdes, app arranca).
  • Coverage de tests: "tested coverage por encima del 80% en el módulo X". Ralph empieza por las funciones sin tests, escribe casos, corre la suite, lee la cobertura, repite hasta el target.
  • Documentación masiva: generar JSDoc para todas las funciones públicas, escribir un README, actualizar comentarios de un módulo. Criterio: que existan y sean coherentes.
  • Proyectos pequeños de cero: una CLI, un script, una librería sencilla con interfaz pública pequeña, tests y CI. Si la spec es nítida y los criterios mecánicos, Ralph llega solo.

Cuándo Ralph se rompe

  • Decisiones de diseño: elegir entre arquitectura hexagonal o capas, decidir qué patrón de datos usar, decidir si un módulo merece estar en su propio paquete. Ralph no sabe juzgar, sabe ejecutar.
  • Tareas creativas o exploratorias: prototipar una UI sin specs claras, descubrir qué quiere el cliente, escribir una demo "que enseñe el concepto" sin criterio cerrado de qué es éxito.
  • Debugging de producción: cuando el bug es sutil y el fallo es probabilístico, Ralph entra en un bucle de "lo mismo no funciona" porque no tiene cómo verificar fix.
  • Tareas sin backpressure automática: sin tests, sin typecheck, sin linter, sin nada que diga "lo has hecho mal". Ralph se convierte en un escupidor de tokens optimistas.

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.

Algunas cifras honestas

  • Una iteración de Opus 4.7 en una tarea de scaffold + tests dura entre 60 y 270 segundos. Si pagas por API directamente, cuesta entre 0,30 y 1 dólar por iter (3-7 dólares la kata entera). Si vas vía suscripción Claude Pro o Max, la factura marginal es cero, entra dentro de la cuota.
  • GPT-5.5 vía OAuth contra ChatGPT Pro no cobra extra. Es parte de la suscripción mensual.
  • Kimi K2.6 vía OpenCode Go son 5-10 dólares al mes flat, sin importar cuántas vueltas dé el loop.
  • Tareas más grandes (un toy language al estilo CURSED) requieren típicamente 30-60 iteraciones. Si pagas por API directamente con Opus 4.7, serían 50-150 dólares. Con suscripción Claude Max sigue siendo cuota mensual fija, sin coste marginal por iteración. Con setup multi-agente en OpenCode (planner GPT-5.5 OAuth, coder Kimi vía OpenCode Go, committer Qwen local), tampoco hay coste marginal.

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.

El operador sigue importando

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.

Cierre

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

Quizás también te interese

El CLAUDE.md de Karpathy

Si tus tests nunca fallan, están rotos