Poste de pilotage Chapitre 4 / 42

uv & le script Python autonome

L'outillage qui rend chaque pièce de l'usine exécutable en une commande, partout — et le préflight qui vérifie votre poste de pilotage sans dépenser un token.

Depuis deux chapitres, vous tapez uv run sans que le livre vous ait expliqué ce qui se passe derrière : au chapitre 3, hello_factory.py s’est exécuté sans virtualenv à créer, sans pip install, sans même vous demander quelle version de Python était installée. Ce n’était pas un détail de confort, c’était une promesse. Ce chapitre la tient : vous allez comprendre pourquoi n’importe quel script de l’usine se lance en une seule commande, sur n’importe quelle machine, y compris une machine vierge que personne n’a préparée. C’est la première pièce du module « Poste de pilotage » : l’outillage qui fait que l’usine s’exécute au lieu de s’installer. Vous posez aujourd’hui un outil qui vous servira jusqu’au dernier chapitre : adws/doctor.py, le préflight qui vérifie votre poste en deux secondes, sans dépenser un token.

uv, l’outillage Python moderne

L’idée en une phrase

uv est un binaire unique qui remplace toute la chaîne d’outillage Python (pip, venv, pyenv) et qui gère lui-même les interpréteurs, les environnements et les dépendances. Dans l’usine, il vit entièrement du côté déterministe de la couture : c’est le moteur qui lance chaque script du poste de pilotage, gratuitement et sans état à préparer.

Points clés

  • Un seul binaire, aucune dépendance : uv s’installe sans Python préalable, il sait télécharger et gérer les interpréteurs lui-même (uv python install, uv python list).
  • uv run script.py résout les dépendances, construit un environnement isolé et exécute, le tout mis en cache : la première exécution prend quelques secondes, les suivantes démarrent quasi instantanément.
  • uv est rapide : sur l’installation de paquets, comptez un facteur 10 à 100 par rapport à pip, selon l’état du cache. À l’échelle d’une usine qui lance des dizaines de scripts par jour, cette friction disparue change le rapport à l’outillage.
  • Il reste compatible avec l’écosystème : mêmes paquets, même PyPI, et une interface uv pip pour les habitudes existantes. Aucun format propriétaire : la règle « pas de DSL » du chapitre 3 s’applique aussi à l’outillage.

Exemple concret

Installez le poste sur une machine neuve et mesurez. L’installation d’uv : une commande, quelques secondes. Premier uv run adws/hello_factory.py : uv lit l’en-tête du script, constate qu’il lui faut un Python récent, le télécharge si besoin, construit l’environnement. Comptez quelques secondes à quelques dizaines de secondes selon le réseau. Deuxième exécution : tout est en cache, le surcoût d’uv se mesure en millisecondes. Comparez au rituel classique, installer la bonne version de Python, créer le virtualenv, l’activer, installer les dépendances, documenter tout cela dans un README que personne ne relit : comptez des minutes par machine, et une source d’écarts entre les postes. L’usine multipliera les machines (vos collègues, les sandboxes jetables du module 6). Ce rituel-là ne passe pas à l’échelle, uv run si.

La chaîne classique vs uv

BesoinChaîne classiqueAvec uv
Installer Pythonpyenv, installeur systèmeuv python install
Créer l’environnementpython -m venv + activationimplicite dans uv run
Installer les dépendancespip install -r requirements.txtrésolues depuis le script
Lancer un script3 étapes préalablesuv run script.py
Reproduire ailleursREADME + disciplinela commande suffit

Commande — installer et vérifier le poste

Aucun harnais n’est impliqué ici : cette pièce est 100 % déterministe, la même commande vaut pour tous. L’installation officielle tient en une ligne (macOS/Linux, puis Windows) :

# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

# vérifier le poste
uv --version        # au moment d'écrire : la série 0.12.x
uv python list      # les interpréteurs que uv connaît (et peut installer)

Piège courant : « uv, c’est juste un pip plus rapide » est inexact. La vitesse est l’argument le plus visible, mais le gain structurel est ailleurs : uv possède aussi les environnements et les interpréteurs. C’est ce qui autorise l’usine à ne documenter aucune procédure d’installation : il n’y a plus d’état à préparer, seulement des commandes à lancer.


Le script autonome : dépendances inline (PEP 723)

L’idée en une phrase

Le standard PEP 723 permet à un script Python de déclarer sa version de Python et ses dépendances dans son propre en-tête, un bloc de commentaires # /// script. uv run lit ce bloc et fournit l’environnement exact, ce qui fait de chaque pièce de l’usine un fichier unique, autosuffisant et portable, exécutable côté déterministe sans aucune préparation.

Points clés

  • Le bloc est du TOML dans des commentaires, borné par # /// script et # /// : deux champs suffisent, requires-python et dependencies. C’est un standard Python, pas une invention d’uv.
  • Chaque script a son environnement, résolu et mis en cache séparément : deux ADW peuvent épingler des versions différentes d’une même bibliothèque sans se marcher dessus.
  • Le shebang #!/usr/bin/env -S uv run --script rend le fichier directement exécutable (./adws/doctor.py après chmod +x) : la commande et le script ne font plus qu’un.
  • uv init --script crée l’en-tête, et uv add --script fichier.py 'rich>=13' ajoute une dépendance sans éditer le TOML à la main, utile quand c’est un agent qui fait évoluer la pièce.
  • C’est la clé de la portabilité de l’usine : les sandboxes jetables que vous monterez au module 6 recevront vos scripts et les exécuteront tels quels. uv run est tout le contrat.

Exemple concret

Projetez-vous au module 6 : un orchestrateur doit exécuter un ADW dans une machine vierge, créée trente secondes plus tôt. Sans script autonome, il faudrait installer Python, créer un environnement, jouer un requirements.txt. Chaque étape peut échouer, et si c’est un agent qui s’en charge, il brûlera des tokens à déboguer l’installation : comptez quelques dizaines de centimes et de longues minutes, pour un travail sans aucune valeur. Avec PEP 723, le contrat tient en une commande : uv run adws/doctor.py. Résolution, environnement et exécution en quelques secondes, zéro token. La différence entre les deux n’est pas du confort : c’est ce qui rend le passage à l’échelle du module 6 possible.

Script classique vs script autonome

CritèreScript + requirements.txtScript PEP 723
Ce qui voyage2 fichiers et un README1 fichier
Machine neuvepréparer l’environnementuv run suffit
Exécution par un agentrisque de déboguer l’installationune commande, zéro token
Isolationun venv partagé qui dériveun environnement par script

Config — l’en-tête qui rend un script autonome

Collez cet en-tête tel quel en tête de fichier, c’est celui de la pièce du jour. Le bloc déclare tout ce que la machine doit fournir, uv fait le reste :

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = ["rich>=13"]
# ///

Le hello_factory.py du chapitre 3 portait déjà ce bloc, réduit à requires-python : la bibliothèque standard n’a rien à déclarer. Dès qu’une pièce a besoin d’un paquet, ici rich pour un rapport lisible, une ligne suffit.

Piège courant : « les dépendances inline vont désynchroniser mes scripts » est le réflexe hérité des projets applicatifs. C’est l’inverse : c’est l’environnement partagé qui crée le couplage et les dérives silencieuses. Ici, chaque pièce épingle ce dont elle a besoin et uv le lui fournit à l’identique partout. Deux scripts ne peuvent pas se casser l’un l’autre en montant de version.


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

La pièce du jour ouvre la zone « Poste de pilotage » du plan : adws/doctor.py, le préflight de l’usine, voisin de l’embryon hello_factory.py posé au chapitre 3. La couture ne traverse pas cette pièce : tout y est déterministe, shutil.which, des sous-processus --version et des tests d’existence de fichiers, et aucun agent n’est convoqué. C’est pourtant bien une gate : une commande, un code retour, qui définit « poste en état » comme les gates du module 3 définiront « travail accepté ». Coût à l’usage : zéro token, environ deux secondes (quelques secondes de plus à la première exécution, le temps que uv résolve rich). Ce qu’elle économise : chaque futur échec d’ADW causé par un outil manquant, le genre de panne qu’un agent met de longues minutes facturées à diagnostiquer et que ce script attrape gratuitement avant le lancement.


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 préflight du poste de pilotage, et une mise à jour du plan pour l’y inscrire.

Pièce — adws/doctor.py

Le préflight vérifie l’outillage (uv, git, bun, les deux harnais) et la présence des pièces déjà posées aux chapitres 1 à 3. Entièrement côté déterministe, c’est votre premier script PEP 723 avec une vraie dépendance : rich, pour un rapport en tableau. Les outils des chapitres à venir (just, herdr) sont sondés mais simplement signalés : leur absence est normale à ce stade.

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = ["rich>=13"]
# ///
"""doctor — le préflight du poste de pilotage.

Vérifie, sans dépenser un token, que l'outillage et les pièces de l'usine
sont en place. Déterministe de bout en bout : une gate sur votre environnement.
"""
import shutil
import subprocess
import sys
from pathlib import Path

from rich.console import Console
from rich.table import Table

# (nom, requis dès aujourd'hui, rôle dans l'usine)
TOOLS = [
    ("uv", True, "scripts autonomes — ce chapitre"),
    ("git", True, "le socle du dépôt — ch. 1"),
    ("bun", True, "sert Plume et lance ses tests — ch. 2"),
    ("pi", False, "harnais n° 1 — ch. 2-3"),
    ("claude", False, "harnais n° 2 — ch. 2-3"),
    ("just", False, "recettes de l'usine — ch. 5, à venir"),
    ("herdr", False, "multiplexeur d'agents — ch. 6, à venir"),
]

# Les pièces posées aux chapitres 1 à 3 : leur absence signale un dépôt incomplet.
PIECES = ["PLAN.md", ".gitignore", "specs/plume-baseline.md", "apps/plume",
          "adws/hello_factory.py"]


def version_of(tool: str) -> str | None:
    """Sonde un outil sans jamais planter : absent ou muet, c'est None."""
    if shutil.which(tool) is None:
        return None
    try:
        out = subprocess.run([tool, "--version"], capture_output=True, text=True,
                             timeout=15)
        first = (out.stdout or out.stderr).strip().splitlines()
        return first[0][:40] if first else "présente"
    except (OSError, subprocess.TimeoutExpired):
        return None


def main() -> int:
    console = Console()
    problems: list[str] = []

    table = Table(title="plume-factory — poste de pilotage")
    table.add_column("Outil")
    table.add_column("Version")
    table.add_column("Rôle")

    versions: dict[str, str | None] = {}
    for name, required, role in TOOLS:
        versions[name] = version_of(name)
        if versions[name]:
            statut = versions[name]
        elif required:
            statut = "[red]MANQUANT[/red]"
            problems.append(f"outil requis absent : {name}")
        else:
            statut = "[yellow]absent[/yellow]"
        table.add_row(name, statut, role)

    # Un seul harnais suffit — mais il en faut un.
    if not (versions["pi"] or versions["claude"]):
        problems.append("aucun harnais disponible : installez pi ou claude")

    for piece in PIECES:
        if not Path(piece).exists():
            problems.append(f"pièce manquante : {piece}")

    console.print(table)
    if problems:
        for p in problems:
            console.print(f"[red]✗[/red] {p}")
        return 1
    console.print("[green]poste de pilotage : OK[/green]")
    return 0


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

Pièce — PLAN.md

Le préflight ne figurait pas sur le plan initial : conformément à la règle de continuité n° 1, la déviation s’annonce et le plan se met à jour. Cette version remplace celle du chapitre 1. Seuls changements : la ligne doctor.py dans l’arbre, et les jalons des chapitres 2 et 3 cochés (vous les aviez cochés à la main, ils le sont désormais d’origine).

# plume-factory — le plan de l'usine

Ce dépôt contient une **usine logicielle agentique** et le payload qu'elle travaille.

Loi fondamentale : **l'agent propose, le code dispose.** Le code déterministe possède le
séquencement, les reprises et l'acceptation. Un agent n'est qu'un nœud borné à l'intérieur d'une
phase nommée. Le contexte ne traverse une frontière que dans une enveloppe JSON typée. Les gates
définissent « fini » ; un échec de gate revient à l'agent par la même porte qu'un rapport d'agent.

## Arbre cible

plume-factory/
├── PLAN.md                          # ce fichier — ch. 1
├── justfile                         # recettes racine — ch. 5
├── just/
│   ├── adws.just                    # lancer les workflows
│   ├── obs.just                     # observer les runs
│   └── sandbox/                     # lifecycle, keys, orch — module 6
├── apps/plume/                      # le payload : app d'écriture Bun + TS — ch. 2
├── specs/                           # les specs : la vôtre (ch. 2), puis celles d'adw_plan (ch. 11+)
├── adws/
│   ├── hello_factory.py             # l'embryon du runner — ch. 3
│   ├── doctor.py                    # le préflight du poste de pilotage — ch. 4
│   ├── adw_prompt.py                # une phase agent, une enveloppe — ch. 8
│   ├── adw_scout.py / adw_plan.py   # reconnaissance et plan — ch. 11
│   ├── adw_build.py                 # implémentation + gates — ch. 12
│   ├── adw_sdlc.py                  # la chaîne complète — ch. 13
│   ├── adw_bench.py                 # mini-benchs par roster — ch. 17
│   ├── adw_modules/
│   │   ├── harness.py               # LE port harnais + adaptateurs pi / claude_code — ch. 7
│   │   ├── runner.py                # phases, séquencement, reprises — ch. 8
│   │   ├── envelopes.py             # enveloppes JSON typées — ch. 9
│   │   ├── roster.py                # chargement de factory.config.yaml — ch. 10
│   │   ├── permissions.py           # tools, writes, fichiers protégés — ch. 10
│   │   ├── gates.py                 # lint / tests / typecheck — ch. 12-13
│   │   └── tracer.py                # événements de phase vers SQLite — ch. 18-20
│   ├── adw_config/                  # factory, frontier, open-weights, eco — ch. 10, 15-17
│   ├── prompts/                     # system.md + user.md par agent
│   └── adw_data/                    # runtime : sessions, enveloppes, factory.db — jamais commité
├── app_docs/                        # sorties du documenter — ch. 13
├── .claude/skills/factory/          # l'usine empaquetée en skill — ch. 26
├── .env.sample                      # OPENROUTER_API_KEY ; provisioning côté hôte seulement
└── .gitignore                       # posé ch. 1

## Zones de l'usine

| Module | Zone | Pièces principales |
|---|---|---|
| 1 — Fondations | le plan et la baseline | PLAN.md, apps/plume, hello_factory.py |
| 2 — Poste de pilotage | l'outillage | uv, justfile, herdr, harness.py |
| 3 — Le squelette ADW | le coeur | runner, envelopes, roster, gates, adw_sdlc |
| 4 — Model stack | les moteurs | 4 rosters, adw_bench |
| 5 — Observabilité | la salle de contrôle | tracer.py, obs.just, export OTel |
| 6 — Sandboxes & scale | le hors-site | just/sandbox, clés provisionnées, best-of-N |
| 7 — Distribution | l'emballage | skill factory, /install, capstone |

## Jalons

- [x] ch. 1 — dépôt initialisé, PLAN.md et .gitignore posés
- [x] ch. 2 — Plume générée en un shot, `bun test` vert : la baseline « sans usine »
- [x] ch. 3 — hello_factory.py : un subprocess, une sortie JSON validée
- [ ] ch. 7 — plus aucun appel direct au harnais hors adw_modules/harness.py
- [ ] ch. 13 — premier adw_sdlc.py vert de bout en bout sur Plume
- [ ] ch. 17 — quatre rosters interchangeables, adw_bench les compare
- [ ] ch. 20 — chaque run laisse une trace SQLite exploitable
- [ ] ch. 25 — un best-of-N de 3 à 5 rosters lancé, moissonné, comparé
- [ ] ch. 27 — l'usine s'installe dans un repo vierge via son skill

## Règles de continuité

1. Une pièce posée ne se casse pas. Toute évolution redonne le fichier entier et l'annonce.
2. `adws/adw_data/` et `.env` ne sont jamais commités.
3. Chaque pièce a sa gate : la commande qui prouve qu'elle fonctionne, coût et durée à l'appui.
4. Le payload Plume reste petit (5 à 8 fichiers) : les runs restent lisibles et bon marché.
5. Les ADW nomment des agents, jamais des modèles. Le roster décide du modèle.

La gate du TP

uv run adws/doctor.py && echo "gate : OK"

Attendu : le tableau du poste de pilotage, la ligne verte poste de pilotage : OK, puis gate : OK. just et herdr apparaissent absent en jaune, c’est normal, ils arrivent aux chapitres 5 et 6. Coût : zéro token, ~2 s (quelques secondes de plus à la première exécution, le temps de résoudre rich). Si la gate échoue, chaque problème est listé et le code retour vaut 1 : corrigez, relancez. Quand elle passe, commitez les deux fichiers.


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.