Aller au contenu
← BlogIA & agents· 7 min de lecture

Une vraie issue, quatre minutes, une PR mergée et une release, sans intervention humaine. Ce que fait la boucle de Shipyard à chaque étape, les garde-fous qui la tiennent et les limites que j’ai rencontrées.

IAOps
Onglet « Files changed » de la PR : index.html, status.json et deux tests ajoutés

Le 2 août 2026, à 0 h 11, mon script de test crée une issue sur un dépôt GitHub tout neuf. À 0 h 12, une pull request s’ouvre. À 0 h 13, elle est mergée. À 0 h 15, Release Please publie la version 1.0.0. Personne n’a cliqué entre-temps. Le dépôt est public, tout est encore là.

Ce que je veux raconter ici, ce n’est pas la vitesse. C’est ce qui se passe pendant ces quatre minutes, et surtout ce qui empêche la boucle de merger n’importe quoi. Shipyard est le framework que j’ai construit pour faire livrer des issues par des agents Claude Code. Cet article suit une issue de bout en bout, puis regarde les garde-fous et les limites.

Le principe : le script décide, l’agent exécute

Avec agent-setup, mon outil précédent, c’était moi qui lançais chaque sprint et chaque workflow, dans une session où le modèle enchaînait lui-même les étapes. Pour livrer sans moi, il fallait un déroulé qui ne dépende pas de ce que le modèle décide de faire ensuite. Dans shipyard, le flux de contrôle est donc un script bash, agent-loop.sh, et chaque agent exécute une seule étape bornée dans une session claude -p neuve.

L’agent ne pilote rien. Il rend du texte avec quelques marqueurs que le script sait lire : COMMIT: pour le message de commit, PR-BODY pour le corps de PR, NEEDS-HUMAN: quand il est bloqué, VERDICT: pour la review. Tout ce qui change un état sur GitHub, un label, un merge ou une fermeture, est fait par le script. La mémoire vit dans git, sur le board et dans .project/state.json. Jamais dans le contexte d’un modèle.

Un tour de boucle, de l’issue au merge

Le diagramme se lit de haut en bas. Deux choses comptent. L’orchestrateur parle à chaque acteur, mais les acteurs ne se parlent jamais entre eux. Et le reviewer n’arrive qu’après la CI : s’il n’y a pas de vert, il n’y a pas de jugement.

Étape 1 : l’issue, c’est le contrat

L’issue du test demande d’exposer un status.json et de le lier depuis la page d’accueil. Elle a quatre critères d’acceptation cochables et une définition de terminé : node --test et la CI au vert, sans dépendance ajoutée. C’est le seul cahier des charges que reçoivent les agents. Une issue floue donne une PR floue, et aucune quantité de garde-fous ne rattrape ça.

Étape 2 : la session de dev

La boucle prend la prochaine issue « Todo » de l’itération courante du GitHub Project. Elle la passe « In Progress », crée un worktree isolé sur la branche feat/2 et lance Sonnet avec l’issue, le contexte du projet et les règles d’ingénierie de son rôle. Le test d’abord, l’implémentation ensuite.

Onglet « Files changed » de la PR : index.html, status.json et deux tests ajoutés
Le diff produit par la session de dev : trois fichiers, dont les deux tests qui encodent les critères de l’issue.
test/site.test.js
test('status.json is valid JSON reporting ok status', () => {
  const status = JSON.parse(readFileSync('public/status.json', 'utf8'));
  assert.deepEqual(status, { status: 'ok' });
});

test('the Pages app links to the machine-readable status page', () => {
  const html = readFileSync('public/index.html', 'utf8');
  assert.match(html, /<a[^>]+href="status\.json"[^>]*>Service status<\/a>/);
});

Avant de pousser quoi que ce soit, le script rejoue lui-même le lint, les tests et le build du projet, avec les commandes lues dans state.json. Si un gate échoue, une session de correction reçoit la commande exacte et la fin de sa sortie, avec un budget de deux tentatives. Un message de commit hors format est normalisé en Conventional Commits plutôt que rejeté, et c’est écrit dans le log. Puis viennent le push, gh pr create et le passage en « In Review ».

La phase dev n’attend pas la CI. Elle enchaîne sur l’issue suivante. La review est une autre phase du même run, qui reprend toutes les issues « In Review ».

Étape 3 : la CI d’abord

La phase review commence par attendre les checks. Elle interroge gh pr checks, patiente tant qu’ils ne sont pas enregistrés, puis tant qu’ils tournent. Trois issues sont possibles. Vert : on passe au jugement. Rouge : le reviewer est court-circuité, et la session de correction reçoit le nom du job en échec et la fin de son log. Pas de réponse au bout du délai : c’est un troisième état, qu’on réattend sans consommer de tour, parce qu’un runner bloqué n’est pas un bug dans le code.

Liste des runs GitHub Actions du dépôt de test : ci, promote, release-please
Les runs Actions du même test : la CI de la feature, puis la promotion et Release Please, tous déclenchés sans intervention.

Dans ce test, le job build a réussi en huit secondes. Sur les dépôts scaffoldés par shipyard, la protection de branche exige trois checks : build, trivy et pr-title (commitlint), en mode strict. La branche doit donc être à jour avec sa base.

Étape 4 : une review dans une session fraîche

Sur une CI verte, le script lance une nouvelle session, Opus par défaut. Elle reçoit le contexte de l’issue, le diff de la PR et rien d’autre. Elle n’a pas vu le code s’écrire et ne sait pas ce que le développeur « voulait » faire. Son prompt lui dit ce qu’elle ne juge pas, puisque la CI s’en charge, et ce qu’elle juge : chaque critère est-il satisfait par le diff ? Les tests ont-ils été affaiblis pour passer ? Le diff sort-il du périmètre ? La couverture du code neuf atteint-elle le seuil ?

templates/loop/prompt-review.md.tpl (extrait)
Commence ta réponse par **exactement une** ligne :

VERDICT: approve
ou
VERDICT: request-changes

Si `approve` : arrête-toi là. Rien à ajouter, pas de résumé de ce qui va bien.

Si `request-changes` : **au plus 3 points bloquants**, le plus grave d'abord :

**1. <le défaut en une phrase>**
- Constat : <fichier:ligne> — <ce que fait le code, factuel>
- Conséquence : <ce qui casse, pour qui, dans quel cas>
- Attendu : <l'action précise à faire>

Le format de sortie est strict, et ce n’est pas cosmétique. Les premières reviews faisaient trois écrans, et le correctif suivant traitait le point le moins important. Aujourd’hui, c’est au plus trois points bloquants, avec fichier et ligne, dans un budget de 2 000 caractères. Un verdict autre que approve est publié en commentaire sur la PR : un humain qui arrive après coup voit la discussion. Puis il est réinjecté dans une session de correction, sur la même branche.

Étape 5 : le merge, et la suite

Sur approve, le script merge en rebase, sous un verrou global. Deux PR qui mergeaient en même temps sur la même base se battaient sur « base out of date ». Si le merge échoue, le script vérifie d’abord sur le serveur que la PR n’a pas déjà été mergée, puis cherche un conflit à résoudre. Ensuite il ferme l’issue, met le board sur « Done », met à jour le vault et notifie mon téléphone.

PR Release Please « chore(main): release 1.0.0 » listant la feature et l’issue fermée
La fin du parcours : Release Please lit le commit Conventional de la boucle et en fait l’entrée de changelog de la 1.0.0.

Le reste est de l’outillage GitHub classique, posé par le bootstrap : la promotion develop → main, puis Release Please qui lit le commit Conventional et ouvre la PR de release. Pour l’issue du test, il s’est écoulé moins de quatre minutes entre sa création et la 1.0.0.

Quand ça ne passe pas

Le chemin heureux est le plus court à écrire. L’essentiel du script traite tout le reste. Chaque boucle est bornée : 3 tentatives de dev, 4 tours de review, 2 réintégrations de la base en cas de conflit. Au-delà, l’issue passe « Blocked » et la boucle poste un commentaire qui donne la cause et l’action à faire.

Commentaire d’escalade « review-exhausted » posté par la boucle sur l’issue
Un run précédent du même test : la boucle escalade « review-exhausted » alors que le commit est déjà sur `develop`. Un vrai bug, attrapé par le test.

Cette capture vient d’un run du même test, une demi-heure plus tôt, avec un budget de review réglé à deux tours. Le commentaire est clair et donne l’action à faire. Mais regardez la chronologie : la PR avait été mergée par GitHub 24 secondes avant l’escalade. Côté serveur, le merge était passé. Côté script, la commande avait renvoyé une erreur, et la boucle avait continué comme si rien n’avait été mergé. Depuis, chaque tentative de merge est vérifiée contre l’état réel de la PR avant d’être déclarée en échec. C’est ce qui permet au run suivant de passer.

Ce n’est pas le seul cas. Sur un de mes projets, une issue avait été escaladée 54 secondes après un commit qui réglait les trois points de la review précédente : le dernier correctif n’était jamais relu. Il l’est maintenant, avant toute escalade.

Escalader vite a un coût : les blocages périment. La PR attendue est mergée, l’humain a répondu en commentaire. Chaque run commence donc par une phase de triage. Une session Opus relit chaque issue bloquée contre l’état réel du dépôt et rend une décision : on la remet en dev, on l’envoie en review, on la ferme comme doublon, on attend une dépendance, ou il faut vraiment un humain. Cette session ne peut pas toucher GitHub : gh lui est retiré au niveau du CLI. C’est le script qui applique.

Le test de fumée hors ligne, lancé pour cet article

Les garde-fous, en une liste

  • Gates locaux avant le push, CI requise avant le jugement. Le jugement d’un modèle n’est jamais le seul gate.
  • Review dans une session fraîche, sur un autre modèle que le dev, avec une grille explicite contre les tests affaiblis.
  • Protection de branche sur develop : build, trivy et pr-title requis, branche à jour exigée.
  • Outils retirés au niveau du CLI, pas seulement déconseillés dans le prompt.
  • Budgets persistés par issue : une review qui survit à un crash reprend son compteur au lieu d’en recevoir un neuf.
  • Périmètre monorepo : avec un label pkg:*, un diff qui touche un autre package est refusé avant la PR.

Les limites, honnêtement

Ce test de bout en bout est réel, mais il est petit. L’issue est synthétique et le reviewer y tournait sur Haiku pour économiser le quota. La PR de dépendance est simulée par le compte de test, car GitHub ne laisse pas un test se faire passer pour Dependabot. Le jour où j’ai relancé le test de fumée pour cet article, une vérification sur 171 était rouge.

  • Le reviewer peut se tromper. Il juge des critères, il ne prouve rien. Si l’issue est mal écrite, il approuvera une PR qui la respecte à la lettre.
  • Trois sources de vérité. state.json, les labels et le board GitHub se désynchronisaient. Une issue déjà poussée est revenue en dev, a produit un diff vide et a été escaladée en boucle. J’ai empilé des réconciliations, mais le vrai correctif était une source unique. C’est ce que fait koklo-cli.
  • Séquentiel. La review et les merges passent un par un. C’est sûr, mais lent sur un gros sprint.
  • Dépendant de GitHub et d’un forfait. Sans CI distante, pas de merge. Et sur un forfait Claude, la boucle tourne par rafales : elle s’arrête quand le quota est atteint, puis reprend quand il revient.
  • La protection de branche n’est pas imposée aux admins, et la boucle tourne avec mon token. Le garde-fou, c’est que le script n’appelle jamais gh pr merge --admin. C’est une convention de code, pas une contrainte du serveur.

Pour finir

Quatre minutes entre une issue et une release, c’est une démo. Ce qui m’intéresse, c’est la boucle qui s’arrête au bon moment, avec une phrase claire et une commande à lancer. C’est elle qui me permet de lancer un sprint depuis mon téléphone, puis de ne regarder que ce qui a vraiment besoin de moi.

#agents#claude-code#ci#code-review#github#automation