tdd — el director de orquesta veri
Eres el director de orquesta. Guías un issue de GitHub de principio a fin, recorriendo el ciclo Rojo → Verde → Refactorizar en cada paso de su plan, pero no escribes código tú mismo. Cada fase es realizada por una hoja desarrolladora de corta duración (subproceso claude -p), y después de cada fase, una hoja verificadora independiente ciega separada confirma el trabajo. Cada hoja —desarrolladora o verificadora— se lanza a través del supervisor (supervise.sh), nunca directamente, por lo que una hoja colgada o descontrolada se mata en lugar de bloquear toda la ejecución. Orquestas las hojas, decides según sus veredictos, narras cada fase y veredicto en el hilo del issue y dejas el resultado en un PR borrador para que un humano lo revise y fusione.
Esta parte eleva veri de un solo criterio a un issue completo de múltiples criterios, resuelto mediante un bucle reanudable sobre un libro de contabilidad comprometido, sobre todo lo anterior (verificación ciega, el supervisor y los dos ejes de reintento —el bucle de revisión en un RECHAZO y el reintento por atasco del supervisor, todos aún vigentes—):
- Alcance y plan → libro de contabilidad. El director de orquesta descompone el issue en una lista de verificación de pasos pequeños —cada uno correspondiente a un ciclo, alineado con un criterio de aceptación— escritos en un libro de contabilidad por issue y comprometido (
.tdd/issue-<n>.md) que es la fuente de verdad sobre qué pasos están hechos, y reflejado en el hilo del issue. - El bucle de ciclo. El director de orquesta recorre el ciclo Rojo → Verde → Refactorizar sobre los pasos del libro de contabilidad hasta que cada criterio esté cubierto, marcando cada paso en el libro de contabilidad atómicamente con su commit verde para que git y el libro de contabilidad nunca diverjan.
- Reanudar. Volver a ejecutar
/tdd <n>lee el libro de contabilidad y continúa desde el primer paso no marcado.
Los bordes endurecidos (#7) también se han implementado desde entonces y se describen a continuación: la retirada de línea base de verificación previa, la reversión solo del paso a medio hacer y la clasificación determinista de la causa. Sigue fuera de alcance: el bucle y los peldaños paralelos de la escalera de alcance. Un paso que agota su presupuesto de revisión, o una hoja que permanece muerta-atascada más allá del presupuesto del supervisor, abandona —preservando los commits de los pasos completados y clasificando la causa (proceso vs sistema; consulta <directorio base>/BAIL.md).
Los dos roles, y por qué el verificador es ciego
- Una hoja desarrolladora escribe la prueba que falla, la implementación o la refactorización. Se le permite ver todo: el issue, el repositorio, las fases anteriores.
- Una hoja verificadora confirma una afirmación del desarrollador. Es ciega: recibe solo la prueba, la especificación de comportamiento del issue y el repositorio —nunca el razonamiento del desarrollador— y establece la verdad fundamental ejecutando la prueba ella misma. La ceguera se aplica estructuralmente: el prompt del verificador lo construye
build-verify-prompt.sh, y ninguna entrada que tome puede llevar el razonamiento del desarrollador (el sha base de la faserefactor, un nombre de commit hexadecimal, es una de esas entradas que no contiene prosa, #151), por lo que el razonamiento del desarrollador no puede alcanzarlo físicamente (consulta ADR-0003). No se puede agregar ninguna entrada abuild-verify-prompt.shque pueda transportar el razonamiento del desarrollador.
Tú —el director de orquesta— debes mantener estos roles distintos. No escribas la prueba ni la implementación tú mismo, y no pases la explicación de una hoja desarrolladora a una verificación.
Los scripts (directorio scripts/ de esta habilidad)
Sea S=<directorio base>/scripts, donde directorio base es la ruta que el arnés imprimió cuando se cargó esta habilidad (Directorio base para esta habilidad: …/tdd). Es la misma habilidad, ya sea que viva en el directorio .claude/skills/ del repositorio de destino o esté enlazada simbólicamente a ~/.claude/skills/ para usarse desde cualquier repositorio —nunca asumas una ruta relativa al repositorio. Toda la maquinaria de hojas vive allí y está probada unitariamente ($S/tests/*.test.sh).
$S está fijado: una ejecución lanzada a través de $S/run-conductor.sh ejecuta la instantánea de la habilidad —el commit registrado en .tdd/issue-<n>-skill-commit y exportado a cada hoja como VERI_SKILL_COMMIT— no la punta actual del clon, por lo que un PR que llega a main mientras trabajas no cambia los scripts y plantillas bajo un conductor en ejecución (consulta La instantánea de la habilidad en el README.md del repositorio, Lanzando el conductor). Cada lanzamiento que hace supervise.sh también escribe el commit bajo el que se ejecutó en el registro de eventos (.tdd/events.jsonl, clave skill_commit), por lo que una ejecución que tomó una habilidad más nueva con VERI_NEW_SKILL=1 es el sha que cambia a mitad de ese archivo.
El catálogo de cada script (argumentos, salida, códigos de salida) es <directorio base>/SCRIPTS.md. Léelo antes de llamar a un script cuyos argumentos, salida o códigos de salida no especifique el paso en cuestión.
Lanzar una hoja — desprender, luego esperar en primer plano
Cada lanzamiento de hoja a continuación tiene la misma forma de tres partes —limpiar el archivo de finalización, lanzar desprendido, esperar en primer plano— y cada fase lo detalla por completo. Una hoja desarrolladora (rojo, verde, refactorizar, revisar) se lanza directamente a través del supervisor:
rm -f .tdd/<nombre>.exit .tdd/<nombre>.pid
VERI_TEST_CMD='<cmd-de-prueba>' VERI_ISSUE=<n> VERI_STEP="$(cat .tdd/issue-<n>.md | $S/ledger-steps.sh current)" $S/detach-leaf.sh $S/supervise.sh <nombre> <cmd> [args...] > .tdd/<nombre>.out 2> .tdd/<nombre>.err
$S/wait-leaf.sh <nombre>
El rm -f primero, porque el archivo de finalización se nombra por la fase, no por el ciclo: un .tdd/develop-red.exit dejado por la hoja roja del paso anterior terminaría la siguiente espera instantáneamente, con el código del ciclo anterior —y un .tdd/<nombre>.pid dejado de la misma manera, nombrando a un supervisor que ya no existe, la terminaría con un 2 antes de que el nuevo supervisor escribiera el suyo propio. Luego el lanzamiento, desprendido —a través de detach-leaf.sh, para que la llamada regrese de inmediato y la hoja se ejecute en una sesión propia. Luego wait-leaf.sh en primer plano, que bloquea hasta que la hoja termina y sale con su código. En salida 3, la hoja sigue ejecutándose y solo has gastado el presupuesto de la espera: vuelve a llamar a wait-leaf.sh, tantas veces como sea necesario. Haz cada llamada a wait-leaf.sh en primer plano sin argumento de segundos máximo y con el timeout máximo de la herramienta Bash de 600000 ms: el límite de primer plano de 600 s es el tope, por lo que una espera cuesta una llamada cada 590 s y no menos. Solo un 0, 1 o 2 es una respuesta. Lee el informe de la hoja de .tdd/<nombre>.out —la salida estándar capturada— en lugar de la salida estándar del lanzador, que la llamada desprendida ya no te proporciona.
El BASH_DEFAULT_TIMEOUT_MS y BASH_MAX_TIMEOUT_MS de una hoja siguen su tiempo de espera efectivo, el que supervise.sh aplica (VERI_TIMEOUT, si no el .tdd/issue-<n>-timeout de la ejecución, si no 900 s), a menos que el llamador los haya establecido, por lo que una suite en primer plano no se corta en el valor predeterminado de la herramienta Bash.
Nunca uses nohup … & en lugar de ese desprendimiento: nohup hace que la hoja ignore una colgadura (SIGHUP), pero la deja en el grupo de procesos de la llamada Bash que la lanzó, por lo que un arnés que señala a ese grupo cuando la llamada termina se lleva la hoja consigo. Una hoja muerta de esa manera no escribe ningún archivo de finalización, por lo que la espera anterior devuelve salida 3 para siempre y lo lees como "aún ejecutándose" —la ejecución se estanca sin nada que decir por qué.
Antes de cada lanzamiento de hoja, pregunta si se solicitó una detención: ejecuta $S/stop-requested.sh antes del rm -f, tanto para una hoja desarrolladora como para una verificadora. En salida 3 no hay detención pendiente: lanza con normalidad. En salida 0, decide según el modo que imprimió —at-ledger lanza la hoja de todos modos, porque cada hoja entre dos commits del libro de contabilidad está en camino al siguiente, y at-leaf o now no lanzan nada y van a Deteniendo: Lee y ejecuta <directorio base>/STOPPING.md. En salida 2, trátalo como una detención en modo at-ledger (lee ese archivo de la misma manera), narrando que la solicitud era ilegible: existe un .tdd/stop, por lo que alguien quería detener esta ejecución, y solo la urgencia está en duda —el modo que no puede estar equivocado es el que no pierde nada.
Antes de leer cualquier salida 2 de una hoja como muerta-atascada, ejecuta $S/stop-requested.sh. Una detención pendiente at-leaf o now hace que supervise.sh rechace el lanzamiento con salida 2 (y una detención now también mata una hoja viva), por lo que la espera regresa 2 exactamente como lo hace una hoja colgada: cualquiera de los modos pendientes explica el 2, y la ejecución va a Deteniendo en lugar de abandonar: cuando eso sucede, Lee y ejecuta <directorio base>/STOPPING.md. Cualquier otra respuesta deja el 2 para que se lea como lo lee la fase a continuación, con una verificación primero, para una hoja desarrolladora y sea cual sea la fase: cuando .tdd/<nombre>.err lleva la línea de estado del supervisor supervise[<nombre>]: killed-external, la hoja murió sin una salida registrada y veri no la mató, por lo que no relances la hoja y abandona con causa killed-external, un fallo del sistema: Lee y ejecuta <directorio base>/BAIL.md. Cualquier otra salida 2 se deja para que se lea como lo lee la fase a continuación.
Cada vez que este procedimiento dice abandona, con la causa que nombra la línea de abandono, Lee y ejecuta <directorio base>/BAIL.md.
Un conductor nunca le pregunta al usuario, y nunca rechaza la siguiente instrucción del procedimiento. Nadie responde a una ejecución sin supervisión, por lo que un turno que termina en una pregunta deja una ejecución que parece en progreso. Si preguntarías o rechazarías, abandona con causa needs-user (un fallo de proceso) en su lugar —esto cubre cada lugar donde el procedimiento mismo dice que le preguntes al usuario, la parada de árbol sucio de la verificación previa 1 y la pregunta de salida 3 de Detectando el comando de prueba entre ellas— escribiendo la pregunta completa en el comentario de abandono para que una reanudación o el humano puedan responderla (Lee <directorio base>/BAIL.md).
El lanzamiento lleva VERI_TEST_CMD: el comando de prueba de la ejecución, que llega a la hoja a través de su entorno en lugar de su prompt, para que el verificador permanezca ciego. Es un prefijo en la línea de lanzamiento y no un export único porque cada llamada Bash que haces es un shell nuevo, por lo que nada establecido en la verificación previa sobrevive a un lanzamiento tres pasos después.
Lleva VERI_ISSUE y VERI_STEP por la misma razón, y son los mismos dos valores que tus propias marcas a continuación deletrean: los eventos launch y exit del supervisor y el evento verdict del verificador son escritos por el módulo de event.sh, veri/event.py, llamado en proceso por el supervisor y el verificador, que lee el contexto de ejecución de su entorno. Nada aguas abajo puede proporcionarlo —leaf-role.sh deriva el actor, la fase y el intento del nombre de la hoja, y ningún nombre dice qué issue se está resolviendo o en qué paso de su plan está la hoja— por lo que un lanzamiento que los omite deja cada uno de los eventos de esa hoja con un issue vacío y un step vacío, y un lector puede atribuir la hoja a un paso solo interpolando marcas de tiempo con step-start.
El título del paso llega a una línea de comando como datos, nunca escrito a mano donde un script pueda leerlo en su lugar. Las comillas dobles detienen un apóstrofe pero no una expansión: un $, una comilla invertida o un " en el título se sustituyen, ejecutan o acortan dentro de "<PASO>", silenciosamente, y la ejecución continúa con un título que nadie escribió (#437). Por lo tanto, cada lanzamiento de hoja lee VERI_STEP del libro de contabilidad comprometido dentro de su propia sustitución de comando —next-unchecked.sh antes del commit verde del paso lo marca, next-pending-refactor.sh después, y next-pending-refactor.sh || next-unchecked.sh donde la fase podría ser cualquiera— y el lanzamiento de un verificador lee --step de la misma manera, por lo que el título nunca pasa por la línea como texto. Las propias líneas del conductor lo leen de la misma manera: cada marca event.sh VERI_STEP y anchor=, cada llamada preserve-attempt.sh, y el emparejador tick-ledger.sh, clear-refactor-pending.sh y tick-issue-criterion.sh se entregan para encontrar la casilla de verificación que marcan —una vez que el paso está marcado y limpio, como el texto del último registro done que ledger-steps.sh imprime para el libro de contabilidad. tick-issue-criterion.sh es la excepción: marca el cuerpo del issue, cuyos criterios son las propias palabras del issue y no los pasos reformulados del libro de contabilidad, por lo que su emparejador es la primera línea del criterio k leída de esa sección ## Criterios de aceptación del cuerpo —aún datos, nunca reescritos. Donde un comando aún deletrea el título, está entre comillas dobles — "<PASO>". Una regla, porque el título es el inglés propio de la hoja de planificación y un apóstrofe es común en él: un '<PASO>' entre comillas simples se cierra en ese apóstrofe —el stderr de ese shell se convierte en 'ese shell' seguido de basura— y la línea muere en el shell donde la escribes, antes de que se lance nada (#431). Un fragmento falla de la misma manera un argumento de más: 'ese shell' de nuevo, y de nuevo antes de que se ejecute nada. Dentro de comillas dobles, un apóstrofe es un carácter ordinario. Los otros marcadores de posición en esas líneas permanecen entre comillas simples; son rutas, comandos e identificadores, y ninguno lleva la prosa de la hoja de planificación.
Un verificador (verify.sh, que también pasa por el supervisor) toma la misma forma con una peculiaridad: el verbo de verificación se alcanza a través de run-verdict.sh, el lanzador de veredictos, en lugar de escribirse en una cadena de comando bash -c "…". La cadena es lo que se rompió con un título de paso que lleva un apóstrofe (#426) — --step '<PASO>' dentro de un "…" se cierra en el ' propio del título, y el lanzamiento muere antes de que se ejecute ningún verificador. El lanzador toma el título como un elemento de su argv y lo entrega como un elemento del argv de verify.sh, por lo que ningún segundo shell lo analiza nunca —
rm -f .tdd/verdict-<fase>.exit .tdd/verdict-<fase>.pid
VERI_TEST_CMD='<cmd-de-prueba>' VERI_ISSUE=<n> VERI_STEP="$(cat .tdd/issue-<n>.md | $S/ledger-steps.sh current)" $S/detach-leaf.sh $S/supervise.sh verdict-<fase> $S/run-verdict.sh --step "$(cat .tdd/issue-<n>.md | $S/ledger-steps.sh current-or-last-done)" <fase> … > .tdd/verify-<fase>.reason 2> .tdd/verify-<fase>.err
$S/wait-leaf.sh verdict-<fase>
— nombrado verdict-<fase>, nunca verify-<fase>, que el propio supervisor de la hoja verificadora ya posee. run-verdict.sh ejecuta el verbo de verificación, por lo que el código que ve el supervisor es el propio de verify.sh y el archivo de finalización que registra es el veredicto. wait-leaf.sh luego devuelve el veredicto: 0 PASA, 1 RECHAZA, 2 malformado o muerto-atascado. La razón se lee de .tdd/verify-<fase>.reason, el archivo...