Dans l'article précédent, j'ai raconté comment KittyClaw est passé de board passif à orchestrateur. Un ticket créé, trente secondes plus tard Claude bosse dessus. Cet article rentre dans le détail : qu'est-ce qui se passe dans l'intervalle ?

Je vais suivre le chemin d'un ticket de sa création à son exécution, en m'arrêtant sur les cinq décisions de design qui rendent le système robuste — ou qui l'ont sauvé quand il ne l'était pas.


1. Le trigger : un poll, pas un webhook

Quand le ticket est créé, rien ne se passe immédiatement. KittyClaw ne « réagit » pas à l'event de création — il regarde le board toutes les 30 secondes et décide quoi faire.

{
  "trigger": {
    "type": "ticketInColumn",
    "columns": ["Todo", "InProgress"],
    "assigneeSlug": "programmer",
    "seconds": 30
  }
}

J'ai hésité pendant une soirée entre polling et events. Les events sont plus « propres » (pas de latence, pas de charge CPU inutile). Mais le polling a trois avantages que j'ai fini par trouver décisifs :

  • Il survit à tout. Le moteur crashe, se restart, rate un event — au prochain tick, il rattrape.
  • Il est naturellement rate-limited. Même si je crée 50 tickets d'un coup, le dispatch se fait par fenêtres de 30s. Pas de thundering herd.
  • C'est le même modèle que l'ancien dispatcher.mjs, que je remplaçais. Du coup, aucune régression possible.

Pour les events rares (un commit git, un passage à Done), j'ai des triggers spécialisés (gitCommit, statusChange) qui polent à 60s ou 30s. Pour les events qui n'ont pas de sens à rater, j'ai boardIdle et agentInactivity qui regardent l'absence d'activité — impossibles à modéliser en events.

Règle empirique : si tu doutes entre poll et webhook dans un système solo-dev, prends le poll. Tu déboguer avec un fichier de log et un sleep 30, pas avec ngrok et une stack de retries.


2. Les conditions : un gate avant le dispatch

Tous les tickets matchés par le trigger ne sont pas forcément dispatchés. Entre le trigger et l'action, il y a une liste de conditions à passer :

{
  "conditions": [
    { "type": "minDescriptionLength", "length": 50 },
    { "type": "labels", "labels": ["release"] }
  ]
}

Sur HolybotsRisingApps par exemple, l'automation publisher ne se déclenche que si le ticket porte le label release et le label app:borne ou app:gamemaster. Le developer, lui, n'accepte que les tickets dont la description fait au moins 50 caractères — pour éviter qu'un ticket balancé à la va-vite du genre « fix le bug » parte en dispatch avant que le producer l'ait groomé.

Les conditions sont le bon endroit pour encoder les politiques de qualité du board. Elles ne remplacent pas le jugement humain, mais elles éliminent 80 % des cas où je lancerais un agent pour rien.


3. Le contexte injecté : minimal, délibérément

Quand le dispatch se déclenche, KittyClaw lance un process claude avec un prompt. Le prompt est beaucoup plus simple que ce à quoi on pourrait s'attendre :

{contenu du fichier skills/programmer.md}

Focus on ticket #42: {titre du ticket}

C'est tout. Pas de description, pas de commentaires, pas de sub-tickets, pas de contexte du repo.

Pourquoi si peu ? Parce que l'agent a déjà les outils pour aller les chercher. Il a accès à l'API de KittyClaw (GET /api/projects/{slug}/tickets/{id}), il peut lire les commentaires, les mentions, les tickets liés, les sub-tickets, à la demande. Et il tourne dans le WorkspacePath du projet — il peut git log, lire les fichiers, faire ses recherches.

Tout contexte que tu injectes dans le prompt est du contexte que tu paies en tokens pour chaque run, y compris quand il n'est pas nécessaire. Laisser l'agent aller chercher à la demande est 10× plus économique et 10× plus à jour.

Le skill file (skills/programmer.md) contient les règles du métier : comment lire un ticket, comment poser des commentaires, comment gérer les sub-tickets, quand renvoyer en review. C'est la persona — stable, versionnée dans le repo, modifiable sans toucher au code de KittyClaw.


4. Les sessions : la mémoire entre deux runs

Si je relance le même agent sur le même ticket dix minutes plus tard, je ne veux pas qu'il reparte de zéro. Il faut qu'il se souvienne de ce qu'il a lu, fait, conclu au run précédent.

Claude Code supporte nativement le --resume <session-id>. KittyClaw s'accroche à ça :

var existingSessionId = _sessions.GetSessionId(workspace, agentName, ticketId);
var sessionId = existingSessionId ?? Guid.NewGuid().ToString();
var isResume = existingSessionId is not null;

La clé est (workspace, agent, ticket). Chaque combinaison a sa propre session persistante, stockée dans .agents/channel/dispatch-state.json. Si l'agent revient sur le ticket, c'est en --resume et le prompt devient :

The owner has posted feedback on ticket #42: {titre}
Read ALL owner comments on this ticket and address them.

Plus besoin de redonner le skill — il est dans la session. On lui dit juste : « reviens, y'a du nouveau, regarde. »

Point qui m'a coûté une matinée : les sessions doivent survivre aux restarts de KittyClaw. J'ai commencé par les garder en mémoire, puis un dotnet watch les a toutes perdues. Depuis, tout est persisté sur disque immédiatement, dans le même fichier JSON que l'ancien dispatcher.mjs. Bonus : mes projets qui tournaient encore sous dispatcher.mjs peuvent migrer sans perdre leurs sessions.


5. La concurrence : empêcher deux agents de marcher sur les mêmes fichiers

Problème réel : si programmer et 3d-artist tournent en même temps sur le même repo, ils se marchent dessus. Deux processus qui éditent des fichiers, un git status qui ne sait plus qui a fait quoi.

Solution : les groupes de concurrence.

{
  "actions": [{
    "type": "runClaudeSkill",
    "skillFile": "skills/programmer.md",
    "concurrencyGroup": "code",
    "mutuallyExclusiveWith": ["producer-commit"]
  }]
}

Les règles sont simples :

  • concurrencyGroup : au plus un run actif par groupe. Tous les agents qui touchent au code sont dans le groupe "code"programmer, 3d-artist, technical-artist, qa-tester, documentalist, code-janitor. Un seul à la fois.
  • mutuallyExclusiveWith : pendant qu'un run tourne, il bloque les groupes listés. Le producer-commit de HolybotsRisingApps, qui fait le commit/PR d'un ticket terminé, bloque code, producer, et publisher — parce qu'il a besoin d'un repo stable pour committer proprement.
  • Dédup implicite : pas deux runs actifs sur (agent, ticket). Si tu poste un commentaire pendant qu'un run tourne, il sera pris en compte au prochain poll, pas en parallèle.

Ces trois règles éliminent 100 % des conflits de fichiers que je voyais avec l'ancien dispatcher en JavaScript. Pas parce que le JS était mauvais — parce que les règles de concurrence étaient encodées dans du code if/else au lieu d'être déclaratives.


6. Le budget : le fusible qu'on espère ne jamais griller

Dernier morceau, optionnel mais précieux :

{
  "dailyBudgetUsd": 70
}

KittyClaw track le coût des runs (via le cost-log.jsonl que Claude Code produit nativement). Dès que la somme du jour dépasse le seuil, tous les dispatches non-CEO sont bloqués. Seul un agent de supervision, s'il y en a un, peut encore tourner pour décider quoi faire.

Je n'ai jamais atteint ce seuil en conditions normales. Mais je l'ai atteint une fois à cause d'une boucle infinie entre deux agents qui se commentaient en ping-pong. Le budget a coupé à 70 $, je me suis réveillé le lendemain avec une facture gérable et une leçon apprise. Sans le budget, ça aurait pu être bien pire.


Récap : le cycle complet

Ce qu'il se passe, dans l'ordre, quand je crée un ticket :

  1. t = 0s : je crée le ticket via l'UI ou l'API. Statut Todo, assignee programmer.
  2. t ≈ 15s : le prochain tick du trigger ticketInColumn (poll toutes les 30s) détecte le ticket.
  3. t ≈ 15s : les conditions sont évaluées (longueur de description, labels). Tout passe.
  4. t ≈ 15s : l'action moveTicketStatus passe le ticket à InProgress.
  5. t ≈ 15s : le moteur vérifie les groupes de concurrence. Personne ne tourne sur code — go.
  6. t ≈ 15s : lookup session. Pas de session existante sur (workspace, programmer, #42) — nouvelle session créée.
  7. t ≈ 16s : process claude lancé avec --print --verbose --output-format stream-json --session-id <uuid>, prompt = skill + focus.
  8. t ≈ 17s : les events stream (assistant, tool_use, tool_result) remontent dans le panneau de run, visibles depuis le ticket.
  9. t ≈ N minutes : Claude a fini. Il a posté des commentaires, peut-être créé des sub-tickets, peut-être passé le ticket à OwnerReview. Le run est logué avec son coût.
  10. t = N + 30 min silence sur Done : si le ticket passe à Done et reste silencieux 30 min, l'evaluator se réveille et relit.

Ce pipeline est, dans les grandes lignes, ce que j'avais avant en JavaScript avec dispatcher.mjs. Mais :

  • Il est déclaratif (automations.json) au lieu d'impératif (du code).
  • Il est intégré au board (les runs sont visibles depuis les tickets).
  • Il survit aux crashes (sessions et state persistés).
  • Il s'applique à tous les projets d'un coup (un moteur, N configs).

Ce que ça m'apprend sur l'automatisation

Plus j'ajoute d'automations, plus je me rends compte que la bonne granularité n'est ni "tout coder" ni "tout configurer". C'est une collaboration entre :

  • Un moteur simple, générique, en code. Quelques types de triggers, quelques types d'actions, la concurrence et les sessions.
  • Des configs déclaratives par projet. C'est là que vit la politique : qui dispatch quand, qui a quel budget, qui bloque qui.
  • Des skills en Markdown, versionnés dans le repo du projet. C'est là que vit la persona et le métier.

Le moteur ne sait rien des jeux vidéo, de la doc, ou du CI. Les skills ne savent rien du polling ou des groupes de concurrence. Les configs relient les deux.

Cette séparation rend l'ajout d'un nouvel agent trivial : écrire un skill, ajouter 10 lignes dans automations.json, et c'est en prod. Pas de déploiement, pas de redémarrage, l'endpoint POST /api/projects/{slug}/automations/reload le fait à chaud.


La suite

Sur Aekan, 13 agents tournent avec ce système. Sur HolybotsRisingApps, 6 dont un workflow spécifique de release (publisher gated par label, producer-commit exclusif avec le reste). Sur Lain, 15 avec un budget journalier actif et un CEO qui wake up sur idle.

Chaque projet a ses propres règles. Le moteur, lui, ne change pas.

Et c'est peut-être ça, la bonne définition d'un harness : une infra générique au-dessus de laquelle chaque projet écrit sa propre chorégraphie.


Le code

Tout est open source :

github.com/Ekioo/KittyClaw

Le moteur est dans KittyClaw.Core/Automation/. Le doc de migration depuis un dispatcher.mjs est dans docs/automation-migration.md.

Pour discuter, poser des questions, ou partager tes propres patterns d'orchestration d'agents — le Discord :

Rejoindre le Discord