Aller au contenu
← ProjetsIAOpsDevEn cours

D’un cahier des charges à un repo livré en autonomie par des agents.

Rôle
Concepteur et mainteneur
Période
2025 — aujourd’hui
Contexte
Outil interne, utilisé sur mes produits
Shipyard

1

commande pour un repo complet

~30

incidents encodés en garde-fous

3

couches : framework, projet, delivery

0

merge sans CI verte

Contexte

Les assistants IA codent vite mais sans cadre : pas de tests, pas de review, pas de traçabilité entre le ticket et la mise en production.

Le défi

Rendre une boucle d’agents fiable sur la durée : reprise après quota épuisé, blocages détectés et relancés, garde-fous de qualité non contournables.

  1. 01IA

    Bootstrap depuis une spec

    Un renderer shell + templates génère le code, les tests, la CI (Trivy, commitlint, Release Please, Dependabot), la protection de branche et le Kanban GitHub.

  2. 02Ops

    Boucle de livraison sur VPS

    Ticket → dev TDD → PR → review par une session neuve → auto-merge si CI verte. Back-off quand le quota est épuisé, escalade humaine seulement si nécessaire.

  3. 03IA

    Skill packs verrouillés

    Règles d’ingénierie composées par agent, langage et domaine, figées à un commit pour des runs reproductibles. Serveur MCP CVE local.

  4. 04Ops

    Pilotage mobile

    Sessions tmux + Happy : suivre, mettre en pause et débloquer la boucle depuis le téléphone, notifications push.

Architecture

Stack

  • Claude Code
  • AOAgents & orchestration
  • MCP
  • GitHub Actions
  • RPRelease Please
  • Trivy
  • Linux

Résultats

  • ✦Utilisé pour livrer mes propres produits
  • ✦Une trentaine d’incidents de production encodés en correctifs
  • ✦Repris comme base de koklo-cli

Le récit

Les captures de cette page viennent de dépôts publics jetables que mes tests de bout en bout créent sur GitHub, par exemple shipyard-e2e-full-cycle. Les issues y sont synthétiques, mais tout le reste est réel : vraies sessions Claude Code, vraie CI GitHub Actions, vrais merges.

Comment ça marche

1. Une issue avec des critères vérifiables

Tout part d’une issue. Le bootstrap la crée depuis le cahier des charges, ou je l’écris à la main. Elle porte un objectif, des critères d’acceptation cochables et une définition de terminé. La session de dev ne reçoit rien d’autre que ce texte, le contexte du projet et les règles d’ingénierie de son rôle.

Issue GitHub « expose a machine-readable status » avec objectif, critères d’acceptation et définition de terminé
Le point de départ : une issue avec des critères vérifiables. C’est tout ce que la session de dev reçoit, avec le contexte du projet.

2. La boucle développe et ouvre la PR

agent-loop.sh prend la prochaine issue « Todo » de l’itération courante, crée un worktree isolé sur feat/<n> et lance une session claude -p neuve sur Sonnet. Le test vient d’abord, puis l’implémentation. Avant tout push, le script rejoue lui-même le lint, les tests et le build du projet. Si un gate est rouge, une session de correction reçoit la commande exacte et la fin de la sortie. Ensuite il commit en Conventional Commits, pousse et ouvre la PR. L’issue passe « In Review ». La phase dev n’attend pas la CI : elle enchaîne sur l’issue suivante.

Pull request feat/2 ouverte par la boucle, mergée dans develop
La PR ouverte par la boucle sur la branche `feat/2`, liée à l’issue par `Closes #2`, puis mergée moins d’une minute après son ouverture.

3. Le gate dur, puis le jugement

La phase review reprend chaque issue « In Review ». Elle attend d’abord que les checks requis soient enregistrés puis terminés. Une CI rouge devient directement un retour de correction, avec le nom du job en échec et la fin de son log. Le reviewer n’est appelé que sur une CI verte, dans une session Opus fraîche qui n’a jamais vu le code s’écrire.

Onglet Checks de la PR : job build vert
Le gate dur : tant que les checks requis ne sont pas verts, le reviewer n’est même pas appelé.

4. Merge, release, promotion

Sur approve, le script merge en rebase sous un verrou global, ferme l’issue, met à jour le board et le vault, puis notifie le téléphone. Dans le test complet, le même dépôt enchaîne ensuite une montée de dépendance, la promotion vers main et la release de Release Please. La PR de dépendance y est simulée par le compte de test, car GitHub ne laisse pas un test se faire passer pour Dependabot.

Liste des PR du dépôt de test : feature, dépendance, promotion vers main, release 1.0.0
Tout le cycle visible dans une liste : la feature, une montée de dépendance, la promotion `develop → main` et la release 1.0.0 de Release Please.

5. Quand il faut un humain

Une issue aux critères contradictoires ne doit pas produire du code. La session de dev renvoie NEEDS-HUMAN:, la boucle passe l’issue en « Blocked » et poste un seul commentaire : la cause, et ce qu’il faut faire pour relancer. Au run suivant, une phase de triage relit chaque blocage contre l’état réel du dépôt. Ce qui n’était bloqué que techniquement repart dans la file.

Commentaire « Besoin humain » posté par la boucle sur une issue aux critères contradictoires
Une issue volontairement impossible : la session de dev refuse d’inventer et la boucle pose une seule question, avec la commande pour relancer.

En fin de sprint, une rétrospective est ouverte en issue, faits d’abord.

Issue de rétrospective de sprint générée automatiquement
En fin de sprint, la boucle ouvre une rétro : faits d’abord (livrées, bloquées), analyse ensuite.

Sous le capot

Le modèle ne décide jamais de la suite. Le flux de contrôle est un script bash de 2 800 lignes. Chaque agent exécute une étape bornée, dans une session neuve, et rend une sortie que le script analyse : COMMIT:, NEEDS-HUMAN:, VERDICT:. La progression vit dans git, dans le board GitHub et dans state.json, jamais dans le contexte d’un modèle.

Le cycle de vie d’une issue dans la boucle

On lit le diagramme de gauche à droite. Chaque flèche correspond à un changement de statut écrit par le script, jamais par l’agent. Les boucles sont toutes bornées : 3 tentatives de dev par défaut, 4 tours de review, 2 réintégrations de la base en cas de conflit. Au-delà, l’issue passe « Bloquée ». Le retour depuis « Bloquée » est le travail de la phase de triage. Une session Opus y juge le blocage, et le script applique sa décision. Pour cette session, gh est retiré au niveau du CLI avec --disallowedTools.

templates/loop/agent-loop.sh.tpl
while [ "$round" -le "$MAX_REVIEW" ]; do
    round=$((round + 1))
    _review_state_set "$n" "$round" "$conflict_fixes"

    # Hard gate: objective signals. Waits for checks to register + complete.
    local ci_rc=0
    ci_is_green "$pr" || ci_rc=$?
    # … rc 2 = timeout : on réattend sans consommer de tour
    if [ "$ci_rc" -ne 0 ]; then
        verdict="request-changes"
        review_out="$(ci_failure_report "$pr")"
    else
        review_prompt="$(render_prompt "$LOOP_DIR/prompt-review.md" "$review_ctx" qa-reviewer)"
        review_out="$(claude_invoke "$SHIPYARD_REVIEW_MODEL" "$review_prompt")" || break
        verdict="$(grep -m1 '^VERDICT:' <<<"$review_out" | sed 's/^VERDICT:[[:space:]]*//')"
    fi

    [ "$verdict" = "approve" ] || post_review_comment "$pr" "$round" "$MAX_REVIEW" "$verdict" "$review_out"

    if [ "$verdict" = "approve" ]; then
        _with_lock "$REPO_ROOT/.project/.merge.lock" merge_pr "$pr" || { …; }
        finalize_merged_issue "$n" "$pr" "$SHIPYARD_REVIEW_LABEL" "$round"
        return 0
    fi
    # sinon : session de correction (Sonnet) avec le retour réinjecté, push, tour suivant
done

C’est le cœur du gate de merge. La CI est consultée avant le reviewer, et un verdict qui n’est pas approve est publié en commentaire sur la PR. Les verdicts restent ainsi lisibles après coup. Le merge passe par un verrou, parce que deux PR qui mergeaient en même temps se battaient sur « base out of date ».

Ce que la machine dit d’elle-même

Le diagnostic de l’hôte, lancé sur ma machine :

make doctor

Le test de fumée de la boucle simule gh et claude, mais pousse vers un vrai remote git local. Il rejoue 29 scénarios en une minute, sans quota. Le jour où je l’ai lancé pour cette page, il rendait 170 vérifications vertes et une rouge : la passe de doc de la boucle heurtait encore le contrôle de périmètre monorepo dans un scénario. Je le laisse tel quel.

test/loop-smoke.sh (extrait)

D’où ça vient, où ça va

agent-setup → shipyard → koklo-cli

shipyard reprend le socle d’agent-setup : renderer shell, templates, rôles TDD et state.json. Il abandonne Codex pour se concentrer sur Claude Code, et ajoute la boucle autonome. koklo-cli reprend ensuite la boucle et surtout l’ordre de ses gardes, mais la réécrit en Rust sur une base SQLite unique.

Ce que j’en retiens

  • Le script décide, l’agent exécute. Le modèle ne choisit jamais l’étape suivante. Un échec devient alors un état du script, qu’on peut rejouer dans un test de fumée, et non une décision qu’il faut deviner après coup.
  • Chaque incident devient un garde-fou, et un test. Un timeout de CI compté comme un échec, un dernier correctif jamais relu avant l’escalade, un budget de review remis à neuf à chaque reprise : une trentaine de cas sont encodés dans le code, avec leur commentaire et leur scénario de fumée.
  • Trois sources de vérité, c’est deux de trop. state.json, les labels GitHub et le board se désynchronisaient. Une issue dont le travail était déjà poussé est ainsi revenue en dev, a produit un diff vide, puis a été escaladée, en boucle. C’est la raison principale de la réécriture dans koklo-cli.

Galerie

Liste des PR du dépôt de test : feature, dépendance, promotion vers main, release 1.0.0
Issue de rétrospective de sprint générée automatiquement
Onglet Checks de la PR : job build vert
PaaS as code
Projet suivant →PaaS as codeToute ma plateforme d’hébergement décrite en code : une app = un fichier YAML.

Parlons-en. Décrivez votre besoin en quelques lignes, je reviens vers vous sous 48 h.