/code-review et /simplify sur un diff dont vous connaissez le contenu~/projets/quizops50 minutes, exercice optionnel compris : 12 min pour l'exercice 1, 15 pour le 2, 13 pour le 3, 10 pour le 4.
Le tag tp6-depart reconstruit l'état de sortie du TP 5 : la validation Pydantic de POST /api/answers et les trois tests du tirage. Il ne contient encore aucun skill de projet ni aucune commande.
cd ~/projets/quizops
outils/depart.sh 6
............................. [100%]
29 passed in 0.21s
Le dossier .claude/ du projet ne contient que les permissions posées au TP 1.
$ ls .claude/
settings.json
Faire relire un diff dont vous connaissez déjà les défauts, pour apprendre à jauger une revue automatique.
outils/saboter.py 4 ajoute deux fonctions à app/quiz.py : une comparaison de chaînes avec is, et un doublon verbeux de la même logique.
$ uv run outils/saboter.py 4
Sabotage 4 appliqué dans app/quiz.py. Retour à la normale : git checkout -- app/
$ uv run pytest -q | tail -1
29 passed, 1 warning in 0.20s
❌ La suite reste verte. Ce diff ne casse rien : il ajoute du code mort et un piège. Aucun test ne le verra jamais.
uvx ruff check .
F632 [*] Use `==` to compare constant literals
--> app/quiz.py:16:12
|
15 | def categorie_est_docker(question: dict) -> bool:
16 | return question["categorie"] is "docker"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: Replace `is` with `==`
SIM103 Return the condition `question["categorie"] == "docker"` directly
--> app/quiz.py:20:5
|
19 | def categorie_docker(question: dict) -> bool:
20 | / if question["categorie"] == "docker":
21 | | return True
22 | | else:
23 | | return False
| |____________________^
help: Replace with `return question["categorie"] == "docker"`
Found 2 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
/code-review est un skill livré avec Claude Code. Il relit le diff du dossier de travail et rend des constats structurés. Il s'exprime en anglais par défaut.
/code-review
Extrait de ce que vous devez voir, deux constats sur quatre :
[
{
"file": "app/quiz.py",
"line": 16,
"summary": "categorie_est_docker compares strings with `is` instead of `==`, so it returns False even for questions whose category actually is \"docker\"."
},
{
"file": "app/quiz.py",
"line": 15,
"summary": "Both new functions are dead code (…) and they duplicate the intent of the already-existing generic filtrer_par_categorie(questions, categorie)."
}
]
Le constat que ruff ne pouvait pas produire est le dernier : ces fonctions ne sont appelées nulle part et doublonnent une fonction existante. La revue a lu les appelants, l'analyse statique a lu le fichier.
/simplify relit les mêmes modifications et applique les nettoyages qu'il juge sûrs, sans chercher les bugs.
/simplify
⏺ Bash(git diff HEAD -- app/quiz.py)
⏺ Read(~/projets/quizops/app/quiz.py)
⏺ Bash(grep -rn "categorie_est_docker\|categorie_docker" --include="*.py" ~/projets/quizops)
⏺ Agent(general-purpose: Reuse review of quiz.py diff)
⏺ Agent(general-purpose: Simplification review of quiz.py diff)
⏺ All four agents agree: `categorie_est_docker` and `categorie_docker` are dead code
(unused anywhere in the app), duplicate each other, and duplicate the existing
general-purpose `filtrer_par_categorie`. Removing both.
⏺ Edit(~/projets/quizops/app/quiz.py)
⏺ Bash(uv run pytest -q)
$ uv run pytest -q | tail -1
29 passed in 0.21s
$ uvx ruff check .
All checks passed!
$ git diff --stat
Le dernier git diff --stat est vide si /simplify a supprimé les deux fonctions ajoutées. Sinon, git checkout -- app/ remet le fichier en état.
ruff ne pouvait-il pas produire ?Q: Pourquoi la suite reste-t-elle verte ?
R: Le sabotage ajoute des fonctions que personne n'appelle. Une suite de tests mesure des comportements observables ; du code mort n'en a aucun. Seule une relecture, humaine ou automatique, voit ce genre d'ajout.
Q: Quel constat ruff ne pouvait-il pas produire ?
R: Le fait que les deux fonctions doublonnent filtrer_par_categorie, qui existe déjà et couvre le besoin. L'analyse statique juge un fichier contre des règles ; la revue a cherché les appelants dans tout le projet.
Écrire un skill de projet complet, avec les trois pièces qui le rendent reproductible : les instructions, un gabarit et un script de vérification.
Un skill de projet vit dans .claude/skills/<nom>/. Le fichier SKILL.md porte le frontmatter et la procédure ; les fichiers voisins sont à sa disposition.
mkdir -p .claude/skills/nouvelle-question
Crée le skill de projet .claude/skills/nouvelle-question/ en trois fichiers.
SKILL.md : frontmatter name, description, argument-hint et allowed-tools ; la description dit quand
l'utiliser (ajouter, créer, insérer une question de quiz). La procédure lit data/questions.json,
rédige la question depuis $ARGUMENTS, l'ajoute avec l'id suivant, puis lance le script de vérification.
modele.json : le gabarit d'une question, quatre propositions, reponse entre 0 et 3.
verifier.py : vérifie champs, catégories, quatre propositions, réponse dans les bornes, ids uniques,
affiche "N questions, M erreur" et sort en code 1 s'il y a une erreur.
$ find .claude/skills -type f | sort
.claude/skills/nouvelle-question/SKILL.md
.claude/skills/nouvelle-question/modele.json
.claude/skills/nouvelle-question/verifier.py
$ uv run python .claude/skills/nouvelle-question/verifier.py
12 questions, 0 erreur
/nouvelle-question Quel port écoute kubelet par défaut ?
L'agent lit data/questions.json, repère le plus grand id, rédige la question sur le gabarit, l'ajoute et relance le script jusqu'à 0 erreur.
$ uv run python .claude/skills/nouvelle-question/verifier.py
13 questions, 0 erreur
$ uv run pytest -q | tail -1
29 passed in 0.21s
SKILL.md
---
name: nouvelle-question
description: Ajoute une question au quiz QuizOps dans data/questions.json puis vérifie le fichier. À utiliser quand on demande d'ajouter, de créer ou d'insérer une question de quiz.
argument-hint: "[énoncé de la question]"
allowed-tools: Read, Edit, Bash(uv run python .claude/skills/nouvelle-question/verifier.py)
---
Ajoute une question au quiz QuizOps.
1. Lis `data/questions.json` et repère le plus grand `id`.
2. Rédige la question à partir de la demande : $ARGUMENTS. Quatre propositions plausibles, une seule juste ; `reponse` est l'index de la bonne réponse, de 0 à 3. Suis le gabarit `modele.json` de ce dossier.
3. Ajoute l'objet à la fin du tableau, avec `id` égal au plus grand id plus un.
4. Lance `uv run python ${CLAUDE_SKILL_DIR}/verifier.py` et corrige jusqu'à obtenir `0 erreur`.
5. Affiche la question ajoutée et le résultat de la vérification. Ne committe pas.
modele.json
{
"id": 99,
"categorie": "kubernetes",
"question": "Énoncé court, qui se termine par un point d'interrogation ?",
"propositions": ["proposition A", "proposition B", "proposition C", "proposition D"],
"reponse": 2
}
verifier.py, en-tête et contrat vérifié :
"""Vérifie data/questions.json : champs, propositions, réponses et identifiants uniques.
Usage : uv run python .claude/skills/nouvelle-question/verifier.py
Sortie : "N questions, M erreurs" ; code de retour 1 s'il y a au moins une erreur.
"""
CHEMIN = Path(__file__).resolve().parents[3] / "data" / "questions.json"
CATEGORIES = {"docker", "kubernetes"}
CHAMPS = ("id", "categorie", "question", "propositions", "reponse")
✅ Les instructions varient d'une exécution à l'autre, le script non. C'est lui qui rend le résultat vérifiable.
Comparer deux skills : l'un que le modèle peut déclencher seul, l'autre réservé à votre appel explicite.
La grille de relecture du projet, celle des six questions vues au module 04, mérite d'être versionnée. Mais une revue déclenchée toute seule au milieu d'une session de code est une gêne : vous la voulez sur demande explicite, et seulement là.
.claude/commands/revue.md : un frontmatter avec description et disable-model-invocation: true, puis la procédure de relecture des modifications non committées. Ce dossier est l'ancien format des commandes personnalisées, fusionné depuis dans les skills ; il sert ici à vérifier que les deux se comportent pareil.uv run outils/saboter.py 4 pour avoir un diff à relire, puis lancez /revue./revue se déclenche./context et regardez la table Skills.Contraintes à imposer dans le fichier
constat | fichier:ligne | gravité, trois gravités.disable-model-invocation: true dans le frontmatter.💡 Indice : les commandes personnalisées ont été fusionnées dans les skills, et .claude/commands/revue.md produit le même /revue que .claude/skills/revue/SKILL.md. C'est le champ disable-model-invocation, pas le dossier, qui décide du déclenchement automatique.
$ ls .claude/commands/
revue.md
$ head -4 .claude/commands/revue.md
---
description: Relit les modifications non committées avec la grille du projet et classe les constats par gravité.
disable-model-invocation: true
---
Dans /context, table Skills :
| Skill | Source | Tokens |
|-------------------|----------|--------|
| nouvelle-question | Project | ~60 |
nouvelle-question y figure et coûte sa description à chaque session ; revue n'y figure pas, puisque le modèle n'a pas le droit de l'appeler.
1. Le fichier
---
description: Relit les modifications non committées avec la grille du projet et classe les constats par gravité.
disable-model-invocation: true
---
Relis les modifications non committées : `git diff` puis `git diff --staged`.
Pour chaque fichier modifié, réponds aux six questions de la grille :
1. Fait-il ce qui a été demandé ?
2. Fait-il autre chose (fichier créé en plus, dépendance ajoutée, fonction renommée) ?
3. Que se passe-t-il aux bords : liste vide, `None`, zéro, entrée invalide ?
4. Les erreurs sont-elles traitées ou avalées ?
5. Y a-t-il un risque de sécurité : entrée non validée, secret en dur, SQL assemblé ?
6. Est-ce maintenable dans six mois : noms explicites, fonctions courtes, cohérence avec le reste ?
Rends un tableau `constat | fichier:ligne | gravité` avec trois gravités : bloquant, à corriger, remarque.
Ne modifie aucun fichier.
2 et 3. Les deux essais
/revue produit le tableau attendu. La demande en langage naturel produit une relecture, elle aussi utile, mais sans la grille : cinq remarques libres au lieu du tableau à trois gravités. Rien dans la transcription ne montre Skill(revue).
4. Le test symétrique
La phrase « Ajoute une question au quiz… » déclenche le skill de l'exercice 2 : la transcription affiche Skill(nouvelle-question) avant toute lecture de fichier. La description a fait le travail, parce qu'elle nomme les verbes de la demande.
✅ La description est le déclencheur, disable-model-invocation: true est l'interrupteur.
Empêcher une revue de charger la conversation en cours en lui donnant son propre contexte.
Une revue lit le diff, plusieurs fichiers et parfois tout le projet. Tout cela reste ensuite dans la fenêtre de la session, alors que seul le tableau final vous intéresse.
/context et la ligne Messages./revue sur le diff du sabotage 4, puis relevez /context de nouveau.context: fork et agent: Explore au frontmatter de .claude/commands/revue.md./revue, relevez /context une troisième fois.Contraintes à imposer
💡 Indice : context: fork exécute la commande dans une fenêtre séparée et ne rend que sa réponse finale.
Trois relevés de la ligne Messages de /context : avant la revue, après la revue sans context: fork, après la revue avec. Le troisième relevé est du même ordre de grandeur que le premier, le deuxième est nettement plus haut.
3. Le frontmatter enrichi
---
description: Relit les modifications non committées avec la grille du projet et classe les constats par gravité.
disable-model-invocation: true
context: fork
agent: Explore
---
4. Ce que vous devez observer
La session principale ne reçoit que le tableau de constats. Les lectures de fichiers, les git diff et les grep de la revue ont eu lieu dans la fenêtre forkée, qui disparaît ensuite.
⚠️ agent: Explore restreint la revue à la lecture. C'est cohérent avec « ne modifie aucun fichier », et c'est plus solide qu'une phrase dans le corps de la commande.
✅ Un contexte isolé ne rend pas la revue plus juste, il en rend le coût borné et répétable dans une longue session.
/code-review et /simplify travaillent sur le diff courant et s'expriment en anglais par défaut ; la preuve reste pytest et ruff.SKILL.md pour la procédure, des fichiers voisins pour les gabarits, un script pour ce qui doit être identique à chaque fois.disable-model-invocation: true : le skill devient réservé à l'humain et cesse de coûter sa description dans le contexte.context: fork : la procédure s'exécute dans une fenêtre séparée et ne rend que sa conclusion.Rendez-vous au TP 7 pour rendre tout cela obligatoire : ce que le skill recommande, le hook l'impose, et la chaîne d'intégration le vérifie à chaque poussée.
➡️ Module associé : Skills, personnalisation et automatisation · TP suivant : TP 7 : Git, CI/CD et Hooks