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 :

  1. À partir d’une skill v0, génération de plusieurs variants par mutation (reformulations, ajouts d’exemples, suppressions de redondances).
  2. Évaluation de chaque variant sur un jeu de cas réels mémorisés.
  3. Sélection multi-objectifs : qualité de sortie, latence, coût en tokens, taux de retry.
  4. Le meilleur variant devient v1 et 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énementQuandExemples de hooks
SessionStartAu démarrage du podon_session_start.py : boot, RAG context, IPC consumer groups
UserPromptSubmitQuand l’utilisateur soumet un prompton_user_prompt.py : router 3-stage (normalize, split, route)
PreToolUseAvant chaque appel d’outildep_guard.py, task_redirect.py, pod_primitives_guard.py
PostToolUseAprès chaque appel d’outilcontext_checkpoint.py, redis_telemetry.py, heartbeat_repair.py
SubagentStopQuand un pod enfant termineon_subagent_stop.py : entity update, IPC publish, review request
StopQuand le pod termineon_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 2bloquant. Le pod respecte le hook et annule l’action. Utilisé par dep_guard.py (block install torch sans CPU index) et task_redirect.py (block TaskCreate → redirige vers hierarchy entities).

Pourquoi des hooks plutôt qu’un middleware

Trois raisons :

  1. 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.
  2. Composable — plusieurs hooks peuvent s’enchaîner sur le même événement.
  3. Observable — chaque hook log dans logs/{hook_name}.log. L’opérateur peut tail -f n’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 PostToolUse n’envoie pas d’emails, ne crée pas de PR. Il observe et log.