Skill cloturer-etape¶
Référence technique des choix internes du skill Claude Code cloturer-etape, dernier maillon de la chaîne analyser-etape → implementer-lot → cloturer-etape. Là où analyser-etape cadre l'étape (issue parente, work-items, branches) et implementer-lot exécute chaque lot (code + tests + MR mergée sur la branche parente), cloturer-etape ne se déclenche qu'une fois tout le travail mergé côté GitLab : il garde-fou la complétude, vérifie que les tests E2E couvrent réellement le périmètre fonctionnel de l'étape, produit trois livrables validés indépendamment, puis prépare (sans la déclencher) la fusion finale <branche-parente> → main. Cette page documente le comment : récupération du state d'un work-item via GraphQL, asymétrie de code retour de glab mr list, wrapper gl(), résolution de la branche parente, le double gate complétude / cohérence E2E, et le choix de ne jamais merger ni fermer le ticket. Le quoi vit dans .claude/skills/cloturer-etape/SKILL.md. Le pourquoi de la délégation Claude est décrit dans l'explication Workflow des agents Claude.
Pour qui¶
- Le développeur qui maintient les scripts du skill (corrections, évolutions).
- Le main Claude lorsqu'il diagnostique un échec de préflight, un gate de complétude KO ou un trou de cohérence E2E.
Pour quoi¶
- Comprendre pourquoi
check-etape-complete.shinterroge le state d'un work-item enfant via GraphQL plutôt que via l'IID seul, et pourquoioutput.jsonstocke ungidpar tâche. - Retrouver le pattern qui neutralise l'asymétrie de code retour de
glab mr list(exit 1 quand aucune MR n'existe) sousset -o pipefail. - Savoir comment la branche parente est résolue sans slug dans
output.json(motif<parent_iid>-*). - Comprendre l'articulation des deux gates (complétude scriptée + cohérence E2E par le main Claude) et lequel bloque, lequel escalade.
- Comprendre pourquoi le skill ne ferme jamais le ticket ni ne merge la MR parente — et pourquoi c'est cohérent avec
implementer-lotqui interdit tout merge versmain.
Scripts livrés¶
Tous vivent dans .claude/skills/cloturer-etape/bin/ et sont invoqués depuis le SKILL.md. Les deux derniers ont un effet remote et sont lancés par l'utilisateur après relecture, jamais par le main Claude (cf. CLAUDE.md « Actions à effet remote = script obligatoire »).
| Script | Args | Rôle | Effet remote ? | Stdout / codes retour |
|---|---|---|---|---|
preflight.sh |
<n> |
Vérifie Étapes/étape<n>.md, le front-matter ticket_gitlab:, var/claude/etape<n>/output.json (≥ 1 lot), la branche parente locale <parent_iid>-*, la branche courante (main ou parente, working tree propre, untracked OK), et la disponibilité de glab. |
Non | JSON {parent_iid, parent_url, parent_branch} sur stdout / 0 OK, 1 KO |
check-etape-complete.sh |
<n> |
Gate d'entrée : pour chaque tâche de output.json, vérifie que le work-item enfant est CLOSED et que sa MR de lot est merged. |
Non (lecture) | Tableau lisible + ligne JSON {complete, blockers[]} / 0 complet, 1 incomplet |
publish-livrables.sh |
<n> <livrable...> |
Publie un ou plusieurs livrables (désignés par leur numéro) en commentaires du ticket parent, en une seule commande (glab issue note). Déduit seul l'IID du ticket (front-matter ticket_gitlab:) et le chemin de chaque livrable (var/claude/etape<n>/livrable-<k>-*.md) ; échoue tôt si un livrable manque, avant toute publication. |
Oui | Confirmation par livrable + total / 0 OK, 1 usage/résolution KO |
ensure-parent-mr.sh |
<n> [body_md] |
Crée la MR <parent_branch> → main si absente (no-op sinon), assignée à cpereira, avec Closes #<parent_iid> dans le body. Ne merge jamais. |
Oui | Confirmation (MR existante ou créée) / 0 OK, 1 usage/résolution KO |
Les livrables eux-mêmes sont écrits par le main Claude dans var/claude/etape<n>/ : livrable-0-coherence-e2e.md, livrable-1-resume.md, livrable-2-interactions.md, et optionnellement mr-parente-body.md. Aucun script ne les génère.
Récupération du state d'un work-item via GraphQL¶
check-etape-complete.sh doit savoir, pour chaque tâche enfant, si le work-item GitLab est ouvert ou fermé. L'IID seul ne suffit pas : l'API REST des work-items est expérimentale et le skill préfère la query GraphQL singulière par gid, qui est stable :
La réponse expose .data.workItem.state ∈ OPEN | CLOSED. Le script l'extrait avec un fallback :
WI_STATE="$( { gl api graphql -f query="query{workItem(id:\"${GID}\"){state}}" 2>/dev/null || echo '{}'; } \
| jq -r '.data.workItem.state // "unknown"')"
Pourquoi output.json stocke un gid par tâche
La query workItem(id:) attend un identifiant global au format gid://gitlab/WorkItem/<id>, pas un IID numérique. C'est précisément pour cette requête que analyser-etape enregistre gid à côté de iid et slug dans output.json (.tasks[] = {iid, gid, slug}). Sans le gid mémorisé en amont, le gate devrait reconstruire l'identifiant global via un aller-retour GraphQL supplémentaire (workItems(iids:), cf. la query pluriel documentée pour analyser-etape).
Le singulier workItem(id:) fonctionne ici parce qu'on l'interroge par gid global, contrairement au workItem(iid:) sur le type Project qui, lui, n'existe pas (cf. la note de la référence analyser-etape).
Asymétrie de code retour de glab mr list¶
Pour l'état de la MR de lot, check-etape-complete.sh (et ensure-parent-mr.sh) listent les MR par branche source :
MR_STATE="$( { gl mr list --source-branch "$BRANCH" --state all --output json 2>/dev/null || echo '[]'; } \
| jq -r '.[0].state // "none"')"
Le || echo '[]' n'est pas cosmétique. glab mr list retourne exit 1 quand aucune MR ne correspond à la branche source — ce n'est pas une erreur fonctionnelle, juste « zéro résultat ». Or les scripts tournent sous set -euo pipefail : sans le fallback, ce exit 1 tuerait le script à la première tâche dont la MR n'existe pas encore (cas nominal d'un lot non démarré), alors que le gate doit précisément la rapporter comme bloqueur.
Le pattern { gl mr list ... || echo '[]'; } | jq transforme l'absence de MR en tableau vide, que jq -r '.[0].state // "none"' réduit à l'état none. Le verdict du lot devient alors ✗ proprement, au lieu d'un crash.
Piège déjà corrigé — ne pas réintroduire
Retirer le || echo '[]' (ou sortir le gl mr list du sous-shell { ... }) ferait échouer le gate dès qu'un seul lot n'a pas de MR, transformant un état « incomplet, à rapporter » en « script planté ». Le sous-shell groupé est volontaire : il capture le code retour avant que pipefail ne le propage.
Wrapper gl() (glab natif vs docker compose exec)¶
Comme les autres skills de la chaîne, chaque script qui appelle glab définit en tête le même wrapper :
gl() {
if command -v glab >/dev/null 2>&1; then
glab "$@"
else
docker compose exec -T php glab "$@"
fi
}
glab est installé dans le conteneur php (prérequis CLAUDE.md), pas garanti sur l'hôte. Le wrapper rend les scripts portables hôte ↔ devcontainer. Le -T désactive l'allocation TTY pour rester compatible avec une exécution depuis Claude Code (sans terminal interactif).
preflight.sh applique la même logique pour son test de disponibilité : il considère glab accessible s'il existe en natif ou s'il répond via docker compose exec -T php glab --version.
Résolution de la branche parente¶
output.json ne stocke pas le slug de l'issue parente — seulement les tâches enfants et parent.iid. La branche parente est donc résolue par motif sur l'IID, en prenant la première branche locale qui matche :
PARENT_BRANCH="$(git for-each-ref --format='%(refname:short)' "refs/heads/${PARENT_IID}-*" | head -1)"
C'est la même convention de nommage que celle posée par analyser-etape à la création des branches (<iid>-<slug>). preflight.sh et ensure-parent-mr.sh utilisent tous deux ce motif ; si aucune branche ne matche, ils s'arrêtent avec un diagnostic invitant à git fetch origin && git checkout ${PARENT_IID}-<slug>.
À noter : preflight.sh déduit PARENT_IID du front-matter ticket_gitlab: (dernier segment de l'URL du ticket), tandis que ensure-parent-mr.sh le relit depuis .parent.iid dans output.json. Les deux sources doivent désigner le même ticket parent — c'est le cas par construction puisque analyser-etape écrit les deux.
Le double gate : complétude puis cohérence E2E¶
Le skill applique deux gates successifs et de natures différentes. Les confondre serait une erreur : ils ne mesurent pas la même chose et n'ont pas le même mode d'échec.
Gate 1 — complétude (scripté, bloquant, exit code)¶
check-etape-complete.sh est la condition d'existence du skill : il garantit que tout le travail de l'étape est effectivement mergé côté GitLab. Pour chaque tâche, il croise deux états :
- le work-item enfant doit être
CLOSED(via GraphQL, cf. plus haut) ; - sa MR de lot doit être
merged(viaglab mr list, cf. plus haut).
Un seul manquement (wi_state != CLOSED ou mr_state != merged) fait passer complete: false et le script sort en 1. Le main Claude s'arrête alors et renvoie l'utilisateur vers /implementer-lot <n> <k> pour terminer le ou les lots restants. Ce gate est purement mécanique : il lit des états distants, ne juge rien, et son verdict est un code retour.
Gate 2 — cohérence fonctionnelle ↔ E2E (main Claude, bloquant, escalade)¶
L'étape B du SKILL.md est un gate non scripté, conduit par le main Claude. Objectif : garantir que chaque parcours fonctionnel décrit dans Étapes/étape<n>.md (pages, routes, critères perceptibles depuis un navigateur) est couvert par un test E2E Panther. La démarche croise trois sources : les parcours attendus extraits de l'étape, les routes réellement exposées (castor console debug:router --format=json), et les fichiers tests/E2E/*.php (convention app_foo_bar → AppFooBarTest.php).
Ce gate est lui aussi bloquant, mais son mode d'échec diffère : il n'y a pas de code retour, et surtout le skill n'écrit jamais de tests. Un parcours sans E2E déclenche une escalade au user avec trois options : combler le trou via /implementer-lot <n> <k> (où la délégation test-writer écrira les E2E), accepter le trou explicitement (déconseillé, consigné dans le livrable et apprentissage.md), ou annuler. Tant que l'utilisateur n'a pas tranché, le livrable 1 n'est pas produit.
Pourquoi deux gates et pas un seul¶
Le gate 1 vérifie que tout est mergé ; il ne dit rien de la qualité fonctionnelle de ce qui est mergé. Le gate 2 vérifie que le périmètre fonctionnel est testé E2E ; il suppose que le code est déjà là (donc que le gate 1 est passé). Fusionner les deux serait impossible : le premier est une lecture d'états GitLab automatisable, le second est une analyse de correspondance étape ↔ tests qui demande de comprendre l'intention fonctionnelle et qui, en cas de trou, ne se résout pas par un code retour mais par une décision humaine.
Publication des livrables et apprentissage.md¶
publish-livrables.sh <n> <livrable...> poste un ou plusieurs livrables en commentaires du ticket via glab issue note <iid> -m "$(cat ...)". L'utilisateur ne passe que le numéro d'étape et les numéros de livrables (ex. publish-livrables.sh 6 0 1 à l'étape E, publish-livrables.sh 6 2 à l'étape H) : le script déduit seul l'IID du ticket (front-matter ticket_gitlab:) et le chemin de chaque livrable (glob var/claude/etape<n>/livrable-<k>-*.md), et résout tous les fichiers avant de publier quoi que ce soit (échec atomique si l'un manque). Effet remote irréversible (un commentaire reste visible même supprimé), d'où le passage obligatoire par un script lancé par l'utilisateur après relecture des fichiers. Il sert pour les trois livrables (cohérence E2E, résumé, évaluation).
L'append dans apprentissage.md (étape H du SKILL.md) est la seule écriture locale directe du main Claude dans ce flux. CLAUDE.md interdit normalement de toucher ce fichier ; ce skill constitue la demande explicite qui lève la restriction, exclusivement en mode append, exclusivement avec le format défini. Aucune lecture du contenu existant. Le skill ne commit pas lui-même, mais apprentissage.md fait partie du commit de clôture de l'étape (script séparé lancé par l'utilisateur) — il n'est plus laissé non commité.
Pas de fermeture du ticket ni de merge¶
ensure-parent-mr.sh crée la MR <parent_branch> → main uniquement si elle n'existe pas encore (sinon il affiche la MR existante et sort en no-op). Le body porte systématiquement Closes #<parent_iid> — qu'il provienne du fichier mr-parente-body.md fourni ou du body minimal généré par le script.
Assignation de la MR parente¶
La MR parente est assignée systématiquement, par défaut à cpereira, surchargeable par GITLAB_ASSIGNEE :
glab mr create accepte le flag court -a (alias de --assignee). C'est la même convention que celle appliquée par analyser-etape (ticket parent et tâches) et implementer-lot (MR de lot) : tout objet GitLab créé par la chaîne de skills atterrit assigné.
Le skill ne merge jamais (gl mr create sans --auto-merge, aucun gl mr merge) et ne ferme jamais le ticket explicitement. C'est le merge manuel de la MR parente par l'utilisateur qui ferme automatiquement le ticket, via le Closes #<parent_iid>. Ce merge est le « done » de l'étape.
Pourquoi ce choix de conception¶
- Le merge vers
mainest une décision humaine. C'est l'invariant déjà porté parimplementer-lot, dont les MR de lot ciblent toujours la branche parente, jamaismain.cloturer-etapeest le seul skill autorisé à préparer la fusion versmain, mais il s'arrête à la création de la MR : aucun skill de la chaîne ne fusionne surmainautomatiquement. Côté permissions,.claude/settings.jsondénie déjàgit push origin mainetgit mergeau main Claude (cf.analyser-etape). - La fermeture découle du merge, pas l'inverse. Lier la fermeture du ticket au
Closes #Ngarantit qu'un ticket n'est jamais marqué « done » sans que son travail soit réellement intégré dansmain. Fermer le ticket par un appel séparé (glab issue close) avant le merge créerait un état incohérent : ticket fermé, code pas dansmain.
Si l'utilisateur préfère malgré tout fermer le ticket sans merger, il peut lancer manuellement glab issue close <parent_iid> — mais ce n'est pas le flux nominal.
Place dans la chaîne des skills¶
cloturer-etape est le troisième et dernier maillon : /analyser-etape <n> cadre l'étape et crée les artefacts GitLab, /implementer-lot <n> <k> exécute chaque lot jusqu'au merge sur la branche parente, /cloturer-etape <n> vérifie que tout est terminé et prépare la fusion finale. Le output.json produit par analyser-etape (avec parent.iid et les gid des tâches) est la source de vérité partagée par les trois skills.
Voir aussi¶
.claude/skills/cloturer-etape/SKILL.md— description fonctionnelle du skill (invocation, déroulé étape par étape, livrables, erreurs à signaler).- Skill
analyser-etape— le premier maillon : cadrage d'étape, création des work-items et des branches, asymétriesglab work-items↔glab issue, query GraphQLworkItems(iids:). - Skill
implementer-lot— le maillon intermédiaire : exécution d'un lot, délégationsreviewer/test-writer/doc-writer, MR de lot ciblant la branche parente (jamaismain). - Hooks Claude Code —
guard-delegation.sh(qui peut écrire où), garde-fous du main Claude et des sous-agents. - Workflow des agents Claude — pourquoi le main Claude délègue, et pourquoi le merge vers
mainreste une décision humaine.