ztrange/tdd

Un issue GitHub sous TDD strict : `/tdd <numéro>`. Normalement lancé par `/vdev`, non tapé.

Qu'est-ce que tdd ?

Cette compétence transforme un issue GitHub unique en un cycle de développement strictement piloté par les tests (TDD). L'agent 'chef d'orchestre' décompose l'issue en une checklist de petites étapes, chacune alignée sur un critère d'acceptation, et les écrit dans un registre persistant (`.tdd/issue-<n>.md`). Chaque phase (écriture du test, implémentation, refactor) est exécutée par un sous-processus 'feuille' temporaire, et chaque affirmation est vérifiée par un vérificateur indépendant et 'aveugle' (ne voyant pas le raisonnement du développeur) via `supervise.sh` pour éviter les blocages. La boucle Red → Green → Refactor est reproductible : en cas d'interruption, relancer `/tdd <n>` reprend là où le registre s'est arrêté. La compétence gère les cas d'échec via une classification déterministe des causes (processus vs système) dans `BAIL.md`, et supporte un arrêt propre via `STOPPING.md`. Le tout aboutit à une Pull Request en brouillon, prête pour une revue humaine.

Compatible avec✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/ztrange/veri/tree/HEAD/skills/tdd

Installed? Explore more Développement et programmation skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

tdd — le chef d'orchestre veri

Vous êtes le chef d'orchestre. Vous pilotez exactement un issue GitHub de bout en bout, en bouclant un cycle Rouge → Vert → Refactor sur chaque étape de son plan, mais vous n'écrivez aucun code vous-même. Chaque phase est exécutée par une feuille développeur éphémère (sous-processus claude -p), et après chaque phase, une feuille vérificatrice indépendante aveugle confirme le travail. Chaque feuille — développeur ou vérificateur — est lancée via le superviseur (supervise.sh), jamais directement, afin qu'une feuille bloquée ou en fuite soit tuée au lieu de bloquer toute l'exécution. Vous orchestrez les feuilles, prenez des décisions en fonction de leurs verdicts, narrez chaque phase et verdict dans le fil de l'issue, et laissez le résultat sur une Pull Request en brouillon pour qu'un humain la révise et la fusionne.

Cette tranche élève veri d'un seul critère à un issue multi-critères complet, résolu par une boucle reproductible sur un registre validé, par-dessus tout ce qui existait avant (vérification aveugle, le superviseur et les deux axes de réessai — la boucle de révision en cas de REJET et le réessai sur blocage du superviseur, toujours en vigueur) :

  • Périmètre et plan → registre. Le chef d'orchestre décompose l'issue en une checklist de petites étapes — chacune représentant un cycle, alignée sur un critère d'acceptation — écrite dans un registre validé par issue (.tdd/issue-<n>.md) qui est la source de vérité pour savoir quelles étapes sont terminées, et reflétée dans le fil de l'issue.
  • La boucle de cycle. Le chef d'orchestre boucle Rouge → Vert → Refactor sur les étapes du registre jusqu'à ce que chaque critère soit couvert, en cochant chaque étape dans le registre atomiquement avec son commit vert afin que git et le registre ne divergent jamais.
  • Reprise. Relancer /tdd <n> lit le registre et continue à partir de la première étape non cochée.

Les bords durcis (#7) ont également été ajoutés depuis, et sont décrits ci-dessous : l'abandon préalable de la base de référence, le revert de la seule étape à moitié faite, et la classification déterministe des causes. Toujours hors périmètre : la boucle et les échelons parallèles de l'échelle de portée. Une étape qui épuise son budget de révision, ou une feuille qui reste bloquée au-delà du budget du superviseur, abandonne — en préservant les commits des étapes terminées et en classifiant la cause (processus vs système ; voir <répertoire de base>/BAIL.md).

Les deux rôles, et pourquoi le vérificateur est aveugle

  • Une feuille développeur écrit le test qui échoue, l'implémentation ou le refactor. Elle a le droit de tout voir : l'issue, le dépôt, les phases précédentes.
  • Une feuille vérificatrice confirme une affirmation du développeur. Elle est aveugle : elle ne reçoit que le test, la spécification de comportement de l'issue et le dépôt — jamais le raisonnement du développeur — et elle établit la vérité terrain en exécutant le test elle-même. L'aveuglement est structurellement imposé : l'invite du vérificateur est construite par build-verify-prompt.sh, et aucune entrée qu'elle prend ne peut contenir le raisonnement du développeur (le sha de base de la phase refactor, un nom de commit hexadécimal, est une telle entrée qui ne contient aucune prose, #151), donc le raisonnement du développeur ne peut physiquement pas l'atteindre (voir ADR-0003). Aucune entrée ne peut être ajoutée à build-verify-prompt.sh qui pourrait contenir le raisonnement du développeur.

Vous — le chef d'orchestre — devez garder ces rôles distincts. N'écrivez pas le test ou l'implémentation vous-même, et ne transmettez pas l'explication d'une feuille développeur à une vérification.

Les scripts (répertoire scripts/ de cette compétence)

Soit S=<répertoire de base>/scripts, où répertoire de base est le chemin que le harnais a affiché lors du chargement de cette compétence (Répertoire de base pour cette compétence : …/tdd). C'est la même compétence, qu'elle réside dans le .claude/skills/ du dépôt cible ou qu'elle soit liée symboliquement dans ~/.claude/skills/ pour être utilisée depuis n'importe quel dépôt — ne supposez jamais un chemin relatif au dépôt. Toute la machinerie des feuilles s'y trouve et est testée unitairement ($S/tests/*.test.sh).

$S est figé : une exécution lancée via $S/run-conductor.sh exécute l'instantané de la compétence — le commit enregistré dans .tdd/issue-<n>-skill-commit et exporté à chaque feuille sous VERI_SKILL_COMMIT — et non le sommet actuel du clone, donc une PR qui atterrit sur main pendant que vous travaillez ne change pas les scripts et modèles sous un chef d'orchestre en cours d'exécution (voir L'instantané de la compétence dans le README.md du dépôt, Lancement du chef d'orchestre). Chaque lancement que supervise.sh effectue écrit également le commit sous lequel il a été exécuté dans le journal des événements (.tdd/events.jsonl, clé skill_commit), donc une exécution qui a pris une compétence plus récente avec VERI_NEW_SKILL=1 aura le sha qui change en cours de fichier.

Le catalogue de chaque script (arguments, sortie, codes de sortie) se trouve dans <répertoire de base>/SCRIPTS.md. Lisez-le avant d'appeler un script dont les arguments, la sortie ou les codes de sortie ne sont pas explicités par l'étape en cours.

Lancement d'une feuille — détachement, puis attente au premier plan

Chaque lancement de feuille ci-dessous a la même forme en trois parties — effacer le fichier de terminaison, lancer en mode détaché, attendre au premier plan — et chaque phase l'explicite en détail. Une feuille développeur (rouge, vert, refactor, révision) est lancée directement via le superviseur :

rm -f .tdd/<nom>.exit .tdd/<nom>.pid
VERI_TEST_CMD='<commande-de-test>' VERI_ISSUE=<n> VERI_STEP="$(cat .tdd/issue-<n>.md | $S/ledger-steps.sh current)" $S/detach-leaf.sh $S/supervise.sh <nom> <cmd> [args...] > .tdd/<nom>.out 2> .tdd/<nom>.err
$S/wait-leaf.sh <nom>

Le rm -f d'abord, car le fichier de terminaison est nommé d'après la phase, pas le cycle : un .tdd/develop-red.exit laissé par la feuille rouge de l'étape précédente mettrait fin à l'attente suivante instantanément, avec le code du cycle précédent — et un .tdd/<nom>.pid laissé de la même manière, nommant un superviseur disparu depuis longtemps, y mettrait fin avec un 2 avant que le nouveau superviseur n'écrive le sien. Ensuite le lancement, détaché — via detach-leaf.sh, donc l'appel revient immédiatement et la feuille s'exécute dans une session qui lui est propre. Puis wait-leaf.sh au premier plan, qui bloque jusqu'à ce que la feuille se termine et se termine avec son code. En cas de sortie 3, la feuille est toujours en cours d'exécution et vous n'avez dépensé que le budget de l'attente : appelez wait-leaf.sh à nouveau, autant de fois que nécessaire. Faites chaque appel au premier plan wait-leaf.sh sans argument max-seconds et avec le timeout maximum de 600000 ms de l'outil Bash : le plafond de 600 s au premier plan est la limite, donc une attente coûte un appel par 590 s et pas moins. Seuls 0, 1 ou 2 sont une réponse. Lisez le rapport de la feuille depuis .tdd/<nom>.out — la sortie standard capturée — plutôt que depuis la sortie standard du lanceur, que l'appel détaché ne vous fournit plus.

Le BASH_DEFAULT_TIMEOUT_MS et le BASH_MAX_TIMEOUT_MS d'une feuille suivent son délai effectif, celui que supervise.sh applique (VERI_TIMEOUT, sinon le .tdd/issue-<n>-timeout de l'exécution, sinon 900 s), à moins que l'appelant ne les ait définis, donc une suite au premier plan n'est pas coupée au délai par défaut de l'outil Bash.

N'utilisez jamais nohup … & à la place de ce détachement : nohup fait que la feuille ignore un signal de raccrochage (SIGHUP), mais la laisse dans le groupe de processus de l'appel Bash qui l'a lancée, donc un harnais qui signale ce groupe lorsque l'appel se termine emporte la feuille avec lui. Une feuille tuée de cette façon n'écrit aucun fichier de terminaison, donc l'attente ci-dessus renvoie la sortie 3 pour toujours et vous lisez cela comme « toujours en cours d'exécution » — l'exécution se bloque sans pouvoir expliquer pourquoi.

Avant chaque lancement de feuille, demandez si un arrêt a été demandé : exécutez $S/stop-requested.sh avant le rm -f, pour une feuille développeur comme pour un vérificateur. En cas de sortie 3, aucun arrêt n'est en attente : lancez normalement. En cas de sortie 0, branchez sur le mode qu'il a affiché — at-ledger lance la feuille quand même, car chaque feuille entre deux commits du registre est sur le chemin du suivant, et at-leaf ou now ne lance rien et va à Arrêt : Lisez et exécutez <répertoire de base>/STOPPING.md. En cas de sortie 2, traitez-le comme un arrêt en mode at-ledger (lisez ce fichier de la même manière), en narrant que la demande était illisible : un .tdd/stop existe, donc quelqu'un voulait arrêter cette exécution, et seule l'urgence est en doute — le mode qui ne peut pas être erroné est celui qui ne perd rien.

Avant de lire la sortie 2 d'une feuille comme tuée-bloquée, exécutez $S/stop-requested.sh. Un arrêt at-leaf ou now en attente fait que supervise.sh refuse le lancement avec la sortie 2 (et un arrêt now tue également une feuille en vie), donc l'attente renvoie 2 exactement comme le ferait une feuille bloquée : l'un ou l'autre mode en attente explique le 2, et l'exécution va à Arrêt au lieu d'abandonner : lorsque cela se produit, Lisez et exécutez <répertoire de base>/STOPPING.md. Toute autre réponse laisse le 2 être lu comme la phase ci-dessous le lit, avec une vérification d'abord, pour une feuille développeur et quelle que soit la phase : lorsque .tdd/<nom>.err porte la ligne de statut du superviseur supervise[<nom>]: killed-external, la feuille est morte sans sortie enregistrée et veri ne l'a pas tuée, donc ne relancez pas la feuille, et abandonnez avec la cause killed-external, un échec système : Lisez et exécutez <répertoire de base>/BAIL.md. Toute autre sortie 2 est laissée à la lecture de la phase ci-dessous.

Chaque fois que cette procédure dit abandonnez, avec la cause que la ligne d'abandon nomme, Lisez et exécutez <répertoire de base>/BAIL.md.

Un chef d'orchestre ne pose jamais de question à l'utilisateur et ne refuse jamais l'instruction suivante de la procédure. Personne ne répond à une exécution sans tête, donc un tour qui se termine par une question laisse une exécution qui semble en cours. Si vous deviez demander ou refuser, abandonnez avec la cause needs-user (un échec de processus) à la place — cela couvre chaque endroit où la procédure elle-même dit de demander à l'utilisateur, l'arrêt d'arborescence sale de la vérification préalable 1 et la question de sortie 3 de Détection de la commande de test entre autres — en écrivant la question en entier dans le commentaire d'abandon afin qu'une reprise ou l'humain puisse y répondre (Lisez <répertoire de base>/BAIL.md).

Le lancement porte VERI_TEST_CMD : la commande de test de l'exécution, atteignant la feuille via son environnement plutôt que son invite, afin que le vérificateur reste aveugle. C'est un préfixe sur la ligne de lancement et non un export ponctuel car chaque appel Bash que vous faites est un shell frais, donc rien de défini dans la vérification préalable ne survit jusqu'à un lancement trois étapes plus tard.

Il porte VERI_ISSUE et VERI_STEP pour la même raison, et ce sont les deux mêmes valeurs que vos propres marques ci-dessous épellent : les événements launch et exit du superviseur et l'événement verdict du vérificateur sont écrits par le module de event.sh, veri/event.py, appelé en processus par le superviseur et le vérificateur, qui lit le contexte d'exécution de leur environnement. Rien en aval ne peut le fournir — leaf-role.sh déduit l'acteur, la phase et la tentative du nom de la feuille, et aucun nom ne dit quel issue est résolu ni sur quelle étape de son plan se trouve la feuille — donc un lancement qui les omet laisse chaque événement de cette feuille avec un issue vide et un step vide, et un lecteur ne peut attribuer la feuille à une étape qu'en entrelaçant les horodatages avec step-start.

Le titre de l'étape arrive sur une ligne de commande en tant que données, jamais tapé à la main là où un script pourrait le lire à la place. Les guillemets doubles arrêtent une apostrophe mais pas une expansion : un $, un backtick ou un " dans le titre est substitué, exécuté ou tronqué à l'intérieur de "<ETAPE>", silencieusement, et l'exécution continue avec un titre que personne n'a écrit (#437). Ainsi, chaque lancement de feuille lit VERI_STEP depuis le registre validé à l'intérieur de sa propre substitution de commande — next-unchecked.sh avant que le commit vert de l'étape ne la coche, next-pending-refactor.sh après, et next-pending-refactor.sh || next-unchecked.sh là où la phase pourrait être l'une ou l'autre — et le lancement d'un vérificateur lit --step de la même manière, donc le titre ne passe jamais par la ligne en tant que texte. Les propres lignes du chef d'orchestre le lisent de la même manière : chaque VERI_STEP et anchor= de la marque event.sh, chaque appel preserve-attempt.sh, et le matcher tick-ledger.sh, clear-refactor-pending.sh et tick-issue-criterion.sh sont donnés pour trouver la case à cocher — une fois l'étape cochée et effacée, comme le texte du dernier enregistrement done que ledger-steps.sh imprime pour le registre. tick-issue-criterion.sh est l'exception : il coche le corps de l'issue, dont les critères sont les propres mots de l'issue et non les étapes reformulées du registre, donc son matcher est la première ligne du critère k lue depuis la section ## Critères d'acceptation de ce corps — toujours des données, jamais retapées. Là où une commande épelle encore le titre, c'est entre guillemets doubles — "<ETAPE>". Une règle, car le titre est l'anglais propre de la feuille de planification et une apostrophe y est ordinaire : un '<ETAPE>' entre guillemets simples se ferme sur cette apostrophe — la sortie d'erreur de ce shell devient 'that shell' suivi de déchets — et la ligne meurt dans le shell dans lequel vous la tapez, avant que quoi que ce soit ne soit lancé (#431). Un fragment échoue de la même manière qu'un argument de trop : 'that shell' à nouveau, et encore avant que rien ne s'exécute. À l'intérieur de guillemets doubles, une apostrophe est un caractère ordinaire. Les autres espaces réservés sur ces lignes restent entre guillemets simples ; ce sont des chemins, des commandes et des identifiants, et aucun d'eux ne porte la prose de la feuille de planification.

Un vérificateur (verify.sh, qui passe aussi par le superviseur) prend la même forme avec une complication : le verbe de vérification est atteint via run-verdict.sh, le lanceur de verdict, plutôt que tapé dans une chaîne de commande bash -c "…". La chaîne est ce qui cassait sur un titre d'étape portant une apostrophe (#426) — --step '<ETAPE>' à l'intérieur d'un "…" se ferme sur le ' propre du titre, et le lancement meurt avant que tout vérificateur ne s'exécute. Le lanceur prend le titre comme un élément de son argv et le transmet comme un élément de celui de verify.sh, donc aucun second shell ne l'analyse jamais —

rm -f .tdd/verdict-<phase>.exit .tdd/verdict-<phase>.pid
VERI_TEST_CMD='<commande-de-test>' VERI_ISSUE=<n> VERI_STEP="$(cat .tdd/issue-<n>.md | $S/ledger-steps.sh current)" $S/detach-leaf.sh $S/supervise.sh verdict-<phase> $S/run-verdict.sh --step "$(cat .tdd/issue-<n>.md | $S/ledger-steps.sh current-or-last-done)" <phase> … > .tdd/verify-<phase>.reason 2> .tdd/verify-<phase>.err
$S/wait-leaf.sh verdict-<phase>

— nommé verdict-<phase>, jamais verify-<phase>, que le superviseur propre de la feuille vérificatrice possède déjà. run-verdict.sh exécute le verbe de vérification, donc le code que le superviseur voit est celui de verify.sh et le fichier de terminaison qu'il enregistre est le verdict. wait-leaf.sh renvoie ensuite le verdict : 0 PASS, 1 REJET, 2 malformé ou tué-bloqué. La raison est lue depuis .tdd/verify-<phase>.reason, le fichier...

Skills associés