Skills et hooks
Deux primitives mutables qui étendent l'agent CLI : les skills (capacités déclaratives), les hooks (signaux lifecycle). Toutes deux sous tournoi GEPA.
Skills : des capacités déclaratives
Une skill est une capacité réutilisable que le pod peut invoquer comme on appelle une fonction. Mais contrairement à un outil ou un MCP server, une skill est définie par un fichier markdown avec frontmatter :
---
name: dataviz
description: |
Décision skill pour choisir le bon type de graphique ET le traitement
data-science adapté, en fonction du dataset et de l'intention analytique.
trigger: "user requests a chart, plot, dashboard, or data visualization"
---
# Comment cette skill décide
[corps markdown de la skill...]
Le pod charge la liste des skills disponibles à chaque session. Quand le
trigger sémantique correspond à la requête utilisateur, la skill est
proposée au pod, qui peut l’invoquer via Skill(skill="dataviz", args=...).
Pourquoi des skills plutôt que des prompts
- Composables — une skill peut en appeler une autre.
- Versionnées — chaque skill vit dans
~/.claude/skills/{name}/SKILL.md. - Triggérables sémantiquement — pas besoin de matching exact.
- Mutables par GEPA — le harnais de prompt évolue sous tournoi.
- Observables — chaque invocation est tracée.
Le tournoi GEPA
GEPA (Genetic-Pareto, recherche académique 2025) est l’algorithme que nous utilisons pour faire évoluer les formulations de prompt de chaque skill. Le principe :
- À partir d’une skill
v0, génération de plusieurs variants par mutation (reformulations, ajouts d’exemples, suppressions de redondances). - Évaluation de chaque variant sur un jeu de cas réels mémorisés.
- Sélection multi-objectifs : qualité de sortie, latence, coût en tokens, taux de retry.
- Le meilleur variant devient
v1et remplace l’ancien. Les autres sont retirés.
Le kernel n’est jamais muté par GEPA. Seul le harnais l’est. Cette séparation garantit que les invariants de sécurité, les contrats de sortie, et les politiques métier restent gravés.
Hooks : des signaux POSIX pour l’agent CLI
Les hooks sont des scripts Python attachés à des événements de cycle de vie.
Le pod déclenche le hook, le hook lit stdin, écrit stdout/stderr, et
retourne un exit code que le pod respecte.
Les six événements actifs
| Événement | Quand | Exemples de hooks |
|---|---|---|
SessionStart | Au démarrage du pod | on_session_start.py : boot, RAG context, IPC consumer groups |
UserPromptSubmit | Quand l’utilisateur soumet un prompt | on_user_prompt.py : router 3-stage (normalize, split, route) |
PreToolUse | Avant chaque appel d’outil | dep_guard.py, task_redirect.py, pod_primitives_guard.py |
PostToolUse | Après chaque appel d’outil | context_checkpoint.py, redis_telemetry.py, heartbeat_repair.py |
SubagentStop | Quand un pod enfant termine | on_subagent_stop.py : entity update, IPC publish, review request |
Stop | Quand le pod termine | on_stop.py : session summary, karma scoring |
Exit codes et leur sémantique
- exit 0 — succès, aucune action côté pod. Le tool call continue normalement.
- exit 1 — erreur, le hook a échoué. Le pod log et continue (fail-open).
- exit 2 — bloquant. Le pod respecte le hook et annule l’action.
Utilisé par
dep_guard.py(block install torch sans CPU index) ettask_redirect.py(blockTaskCreate→ redirige vers hierarchy entities).
Pourquoi des hooks plutôt qu’un middleware
Trois raisons :
- Langage indépendant — un hook en Python peut être réécrit en Rust ou en Go sans toucher au pod. C’est juste un binaire qui lit stdin et écrit stdout.
- Composable — plusieurs hooks peuvent s’enchaîner sur le même événement.
- Observable — chaque hook log dans
logs/{hook_name}.log. L’opérateur peuttail -fn’importe quel hook pour voir ce qu’il fait.
Utilitaires partagés
Les hooks partagent une bibliothèque utilitaire pour éviter la duplication :
scripts/hooks/hook_base.py
├── read_hook_input() — parse stdin JSON
├── emit(data) — write stdout JSON
├── write_bus(message) — append au bus JSONL
├── load_state(name) — lit .claude/cache/{name}_state.json
├── save_state(name, data) — écrit atomique tmp-rename
├── setup_logging(name) — handler vers logs/{name}.log
└── check_cooldown(name) — empêche les hooks trop fréquents
scripts/hooks/rag_utils.py
├── search_qdrant(query, k, filter)
└── ingest_to_qdrant(text, meta)
scripts/hooks/ipc.py
├── publish_result(...)
├── publish_signal(...)
├── publish_entity_event(...)
├── consume_entity_events(group, consumer)
├── ack_entity_event(...)
└── ensure_consumer_groups()
Cette factorisation rend chaque hook lisible en moins de 100 lignes. Si un hook dépasse, c’est qu’il fait trop de choses — il doit être splitté ou refactorisé dans la lib.
Garde-fous sur les hooks
- Pas d’I/O bloquante — un hook qui hang bloque le pod. Timeout strict.
- Logs locaux uniquement — ne pas écrire dans Qdrant depuis un hook critique, utiliser le bus JSONL (asynchrone par design).
- Idempotence — un hook peut être déclenché deux fois sur le même événement en cas de retry. Il doit être no-op à la seconde invocation.
- Pas de side-effect surprenant — un hook
PostToolUsen’envoie pas d’emails, ne crée pas de PR. Il observe et log.