Poste de pilotage Chapitre 6 / 42

herdr & le multiplexage d'agents

Le poste de pilotage devient une salle de contrôle : un workspace herdr où chaque agent a sa pane, son état sémantique, et où vous pouvez partir sans rien arrêter.

Au chapitre 2, vous avez regardé un agent construire Plume dans votre terminal : une fenêtre, un agent, vos yeux rivés sur le défilement. Projetez maintenant la suite du livre : un scout qui explore, un planner qui rédige, un builder qui implémente, et vous qui voudriez aussi garder un œil sur le serveur de Plume. Quatre choses à surveiller, une seule fenêtre, et cette question qui revient sans cesse : « il en est où, celui-là ? ». Ce chapitre vous équipe avant que le problème n’arrive : à la fin, vous saurez faire tenir toute l’usine dans un seul écran, lire l’état de chaque agent d’un coup d’œil, et quitter votre poste sans rien interrompre. La pièce du jour est le workspace herdr de plume-factory, et fidèle au chapitre 5, vous le monterez d’une seule commande : just fleet.

herdr : tmux pensé pour les agents

L’idée en une phrase

herdr est un multiplexeur de terminal (panes persistantes, découpage d’écran, sessions qui survivent à la déconnexion) augmenté d’une notion que les multiplexeurs classiques n’ont pas : l’état sémantique d’agent (idle, working, blocked, done) porté par chaque pane. Dans l’usine, c’est une pièce du poste de pilotage, entièrement côté déterministe : herdr héberge et observe les agents, il n’en est jamais un.

Points clés

  • Un binaire unique, sans dépendance, qui tourne dans votre terminal actuel. Au moment d’écrire, la série 0.8. Le modèle de panes et le préfixe clavier sont ceux de tmux : si vous venez de tmux, vous êtes chez vous.
  • Chaque pane porte un agent_status : les agents intégrés (pi, Claude Code, entre autres) le rapportent automatiquement via herdr integration install, une pane shell reste unknown.
  • Toute la surface est pilotable en CLI : herdr pane run, herdr pane read, herdr pane list… Chaque commande répond en JSON sur stdout, vos scripts lisent les ids dans la réponse au lieu de les deviner.
  • Les verbes qui changent tout sont les attentes bloquantes : herdr wait output --match … attend qu’un texte apparaisse, herdr wait agent-status --status idle attend qu’un agent ait fini son tour. Fini les boucles sleep + relecture d’écran.
  • Un agent qui passe idle a fini son tour, pas forcément réussi sa tâche : un refus poli rend aussi la main. On relit la pane (ou on vérifie l’artefact) après l’attente. Dans l’usine, ce sont les gates qui jugeront.

Exemple concret

Sans multiplexeur, surveiller deux agents en parallèle ressemble à ceci : deux fenêtres de terminal, l’alt-tab toutes les trente secondes, et le risque bien réel de laisser un agent bloqué sur une question pendant vingt minutes sans le voir : vingt minutes de perdu, plus le contexte que vous avez déchargé de votre tête entre-temps. Avec herdr, les deux agents sont dans un workspace, la barre d’état affiche working / working, puis blocked sur le second, et vous répondez dans la minute. En script, la même vigilance devient une ligne : herdr wait agent-status w1:p2 --status idle --timeout 300000 rend la main à la seconde où l’agent termine. Coût de l’outil : zéro token, c’est du déterminisme pur. Gain : des minutes par run aujourd’hui, et l’infrastructure d’observation dont le best-of-N du module 6 aura besoin, quand ce sont cinq usines qu’il faudra suivre en parallèle.

Multiplexeur classique vs herdr

Capacitétmux et assimilésherdr
Panes persistantes, détachementouioui
État d’un agentlire l’écran soi-mêmeagent_status sémantique par pane
Attendre la fin d’un tourboucle sleep + scrapingwait agent-status --status idle
Pilotage par scriptsend-keys, sortie bruteCLI complète, réponses JSON

Commande — installer herdr et vérifier

Aucun harnais impliqué dans l’installation. Dès qu’on lance des agents dedans, les deux harnais du livre sont concernés : l’intégration rapporte leur état à herdr.

# macOS / Linux
curl -fsSL https://herdr.dev/install.sh | sh

# Windows (PowerShell)
powershell -ExecutionPolicy Bypass -c "irm https://herdr.dev/install.ps1 | iex"

# vérifier — au moment d'écrire : la série 0.8.x
herdr --version

# faire rapporter leur état aux deux harnais du livre (une intégration par appel)
herdr integration install pi
herdr integration install claude

Piège courant : « herdr remplace mon terminal » est inexact. herdr n’est ni une app graphique ni un nouveau terminal : c’est un binaire qui tourne dans votre terminal actuel, comme tmux. Vous gardez votre émulateur, vos polices, votre config shell. herdr n’ajoute que la couche de multiplexage et d’observation des agents.


Workspaces, états d’agents et accès distant

L’idée en une phrase

herdr organise tout en un arbre, sessionworkspacetabpane, où chaque niveau est un objet adressable (w1, w1:t1, w1:p1) que vos scripts créent, interrogent et ferment par la CLI. Le workspace de l’usine devient ainsi une pièce reproductible du poste de pilotage, montée par une recette just, et accessible depuis n’importe où en SSH.

Points clés

  • Un workspace par projet : le vôtre s’appellera plume-factory, créé avec herdr workspace create --cwd … --label …. La réponse JSON contient deux ids à capturer : celui du workspace ET celui de la pane racine. On ne se fie jamais au focus pour placer une pane.
  • pane run tape la commande et valide, send-text tape sans valider (deux temps utiles pour piloter un agent interactif : le texte, puis send-keys enter). pane read est vos yeux : l’écran visible ou le scrollback, en texte.
  • Fermer la dernière pane d’un workspace ferme le workspace. Dans un script, on ne ferme jamais la pane racine d’un workspace qu’on veut garder : on ajoute d’abord, on retire ensuite.
  • En script, toujours --no-focus : vos commandes ne doivent pas voler le focus de l’humain qui travaille à côté, et un split sans cible explicite s’applique à la pane focalisée, qui peut être dans un tout autre workspace.
  • Détachez-vous, tout continue : les panes vivent dans un serveur, pas dans votre fenêtre. Fermez le terminal, rentrez chez vous, puis herdr --remote votre-machine en SSH : la flotte est toujours là, les agents ont continué à travailler.

Exemple concret

Vendredi, 17 h 40. Vous lancez depuis votre poste fixe un agent sur une tâche longue, le genre de refonte qui prendra une demi-heure et quelques dollars de tokens. Sans serveur de panes, fermer le capot tuerait le processus : run interrompu, tokens déjà facturés, tout à relancer lundi. Avec herdr, vous fermez le capot et l’agent continue dans sa pane côté serveur. Dans le train, un herdr --remote poste-fixe en SSH raccroche votre téléphone ou votre portable à la même session : la barre affiche done, vous lisez la pane, le travail est terminé. Le multiplexeur n’a rien décidé, rien jugé. Il a garanti que la fenêtre et le travail sont deux choses indépendantes. C’est la propriété qu’exigeront les longues chaînes du module 3 : un ADW de dix minutes ne doit pas dépendre de la survie de votre fenêtre.

L’arbre herdr

NiveauId d’exempleRôle dans l’usine
Sessiondefault, ou nomméeun serveur persistant, la « salle »
Workspacew1un projet : plume-factory
Tabw1:t1une page de panes : travail, logs…
Panew1:p1un PTY réel : un shell, un agent, Plume

Commande — lancer un agent dans la flotte

Quand vous lancerez des agents dans des panes, la commande diffère par harnais. Voici les deux, sous leur forme la plus simple (l’usine des modules 3 et suivants les encapsulera) :

# version pi — l'argv après « -- » est la commande de la pane
herdr agent start scout --workspace w1 --split right --cwd "$PWD" --no-focus -- \
  pi "explore ce depot et resume sa structure"

# version Claude Code — même mécanique, binaire différent
herdr agent start scout --workspace w1 --split right --cwd "$PWD" --no-focus -- \
  claude -p "explore ce depot et resume sa structure"

# puis : attendre la fin du tour, et LIRE avant de conclure
herdr wait agent-status w1:p2 --status idle --timeout 300000
herdr pane read w1:p2 --source visible --lines 40

Piège courant : « le workspace herdr, c’est de la configuration jetable » sous-estime la pièce. L’arbre est adressable et reproductible : monté par une recette, il se recrée à l’identique sur n’importe quelle machine, et c’est lui qui accueillera les agents des modules suivants. Le jeter et le recréer à la main à chaque session, c’est réintroduire la friction que le chapitre 5 venait d’éliminer.


Fil rouge — la pièce posée aujourd’hui

La pièce du jour est le workspace herdr de l’usine, la case « workspace herdr » de la zone « Poste de pilotage », entre le justfile du chapitre 5 qui la monte et le harness.py du chapitre 7 qui donnera aux ADW leur porte d’entrée vers les agents. La couture ne traverse pas encore cette pièce : herdr est du déterminisme pur, il crée des panes, y tape des commandes, lit des écrans, rapporte des états. Les agents qu’il hébergera restent des nœuds bornés. herdr est le bâtiment, pas un ouvrier. Coût à l’usage : zéro token, une à deux secondes pour monter le workspace. Ce qu’elle économise : les minutes d’un agent blocked que personne ne voit et les runs tués par une fenêtre fermée. Elle pose aussi l’infrastructure d’observation sans laquelle le best-of-N du module 6 serait ingérable.


Travaux pratiques — la pièce du jour

Une pièce complète à poser dans le repo compagnon plume-factory, qui devient, chapitre après chapitre, votre usine logicielle agentique. Aujourd’hui : le workspace de la flotte, monté par une recette. Le poste de pilotage prend forme.

Pièce — justfile

Cette version remplace celle du chapitre 5. Les cinq recettes d’hier sont inchangées, trois recettes de flotte s’ajoutent. Vous pourriez écrire la logique de fleet en bash, directement dans la recette, mais un script shell multi-lignes est fragile hors macOS/Linux (sous Windows, il exige des outils absents du PATH par défaut). La règle du livre est plus simple : dès qu’une logique dépasse la ligne, elle va en Python. La recette reste donc une ligne et la logique vit dans adws/fleet.py, portable partout où uv tourne, sans dépendance (Python lit le JSON nativement). Prérequis : herdr, installé dans la fiche.

# justfile — la surface de commandes de plume-factory.
# `just` seul liste tout : c'est le panneau de commandes de l'usine.

# .env est chargé dans l'environnement des recettes (OPENROUTER_API_KEY au module 4).
set dotenv-load

# Les arguments de la ligne de commande deviennent $1, $2, "$@" dans les recettes.
set positional-arguments

# Windows : les recettes s'exécutent dans le bash de Git (installé avec git) — pas de WSL.
# Ce réglage est ignoré sur macOS/Linux ; adaptez le chemin si Git est installé ailleurs.
set windows-shell := ["C:/Program Files/Git/bin/bash.exe", "-cu"]

# liste les recettes — sans elle, `just` nu exécuterait la première recette du fichier
default:
    @just --list

# le préflight du poste de pilotage (ch. 4)
doctor:
    uv run adws/doctor.py

# l'embryon du runner (ch. 3) — choisir le harnais : just hello claude
hello HARNESS="pi":
    uv run adws/hello_factory.py --harness {{ HARNESS }}

# les tests du payload Plume (ch. 2) — cd et commande sur UNE ligne : chaque ligne a son shell
test:
    cd apps/plume && bun test

# servir Plume en local sur le port 4500 (ch. 2)
serve:
    cd apps/plume && bun run server.ts

# monter le poste : workspace herdr « plume-factory », Plume servie dans sa pane (ch. 6)
# La logique vit dans adws/fleet.py — la recette reste une ligne, comme la loi l'exige.
fleet:
    uv run adws/fleet.py

# l'état de la flotte : chaque pane et son agent_status
fleet-status:
    herdr pane list

# fermer le workspace de l'usine : just fleet-down w1
fleet-down WS:
    herdr workspace close {{ WS }}

Pièce — adws/fleet.py

Le montage du poste, en Python et bibliothèque standard uniquement. Le script vit entièrement du côté déterministe : il crée le workspace, capture les ids dans les réponses JSON de herdr et jamais depuis le focus, sert Plume dans sa pane, puis vérifie que l’application répond avant d’annoncer le poste prêt.

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# ///
"""fleet — monte le poste de pilotage : workspace herdr, Plume servie dans sa pane.

La logique multi-lignes vit en Python (stdlib) ; la recette just reste une ligne.
Les ids se lisent dans les réponses JSON de herdr — jamais depuis le focus.
"""
import json
import subprocess
import sys
import time
import urllib.request
from pathlib import Path


def herdr(*args: str) -> dict:
    """Appelle herdr et rend sa réponse JSON (dict vide si la commande n'en produit pas)."""
    result = subprocess.run(["herdr", *args], capture_output=True, text=True)
    if result.returncode != 0:
        sys.exit(f"herdr {' '.join(args)} : {result.stderr.strip()[:200]}")
    try:
        return json.loads(result.stdout)
    except ValueError:
        return {}


def main() -> int:
    # le serveur herdr, s'il ne tourne pas déjà — détaché du processus courant
    status = subprocess.run(["herdr", "status", "server"], capture_output=True, text=True)
    if not status.stdout.strip() or "not running" in status.stdout:
        subprocess.Popen(["herdr", "server"],
                         stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        time.sleep(1)

    # capturer les DEUX ids depuis le JSON — jamais depuis le focus
    created = herdr("workspace", "create", "--cwd", str(Path.cwd()),
                    "--label", "plume-factory", "--no-focus")["result"]
    ws = created["workspace"]["workspace_id"]
    root = created["root_pane"]["pane_id"]

    # une pane à droite pour servir Plume ; la racine reste un shell libre
    serve = herdr("pane", "split", root, "--direction", "right",
                  "--no-focus")["result"]["pane"]["pane_id"]
    herdr("pane", "run", serve, "just serve")

    # vérification déterministe : Plume répond — pas « le log a l'air bon »
    for _ in range(20):
        try:
            with urllib.request.urlopen("http://localhost:4500/api/docs", timeout=1) as reply:
                print(f"plume répond : {reply.read().decode().strip()}")
                break
        except OSError:
            time.sleep(0.5)
    else:
        sys.exit(f"Plume ne répond pas — lisez la pane serveur : herdr pane read {serve}")

    print(f"workspace {ws} prêt — shell : {root}, plume : {serve} "
          f"(just fleet-down {ws} pour fermer)")
    return 0


if __name__ == "__main__":
    sys.exit(main())

La gate du TP

just fleet
just fleet-status

Attendu : plume répond : [] (base vide), la ligne workspace w1 prêt… avec les trois ids, puis la liste JSON des panes, deux panes dans le workspace plume-factory, états unknown puisque ce sont un shell et un serveur, pas des agents. Ouvrez herdr dans un autre terminal pour voir la flotte, puis fermez proprement : just fleet-down w1 (l’id affiché par la gate). Coût : zéro token, quelques secondes. Si le script s’arrête sur « Plume ne répond pas », lisez la pane serveur avec la commande qu’il affiche : le message d’erreur de Bun s’y trouve.


Quiz — teste tes connaissances
Poste de pilotage 7 questions Objectif : 5/7 minimum
0/7
bonnes reponses
Objectif non atteint (minimum 5/7 requis).
Remonte relire la fiche memo en pretant attention aux points manques, puis cliquer sur « Recommencer » pour retenter.