Le squelette ADW Chapitre 13 / 42

La boucle de correction & le SDLC complet

L'échec de gate cesse d'arrêter le run : il repart vers le builder en enveloppe, dans sa session vivante — et adw_sdlc.py déroule plan → build → test → review → document en une seule commande.

Hier, vous avez peut-être vu un run d’adw_build mourir en rouge sur un échec de gate. Regardez ce que l’usine avait entre les mains à cet instant : un motif d’échec déjà rédigé, preuve à l’appui, et la session du builder encore vivante : l’agent qui venait d’écrire le code presque bon attendait, tout son contexte en tête. Et le run s’arrêtait quand même. Aujourd’hui, vous fermez cette boucle : l’échec de gate repart vers le builder, en enveloppe, par la même porte qu’un rapport d’agent, la symétrie que le chapitre 9 vous avait promise. Comme la chaîne sait désormais se corriger, elle peut s’allonger : à la fin du chapitre, une seule commande déroulera le SDLC complet (plan → build → test → review → document) et votre usine atteindra le premier grand jalon du livre, un cycle de développement entier, vert de bout en bout, sur Plume. Deux pièces se posent : adws/adw_sdlc.py, la chaîne maîtresse, et la version chapitre 13 du roster, qui accueille son cinquième agent, le documenter.

La boucle de correction : l’échec revient en enveloppe

L’idée en une phrase

La boucle de correction transforme un échec de gate en travail à refaire : le code met le verdict des gates en forme d’enveloppe et le renvoie au builder dans sa session vivante, par la même porte qu’un rapport d’agent. La boucle elle-même vit côté code déterministe : le runner retente, borné, et les gates re-tranchent à chaque tour.

Points clés

  • La symétrie du chapitre 9, enfin exploitée. Un résultat produit par du code peut être mis en forme d’enveloppe et tendu à un agent par la même porte qu’un rapport d’agent : le verdict des gates devient une Envelope (status: fail, motif dans notes_for_next_agent), injectée par handoff(). Le builder ne fait pas la différence, et c’est voulu.
  • La session vivante fait toute l’économie. Le builder qui reçoit le verdict a encore la spec, le code et ses choix en contexte : la reprise coûte quelques milliers de tokens, contre un redémarrage à froid qui rejouerait tout le build.
  • La boucle est bornée par le code. --fix-loops fixe le nombre de reprises (3 par défaut), ce n’est jamais « jusqu’à ce que ça passe ». Une boucle non bornée est une facture non bornée.
  • Les gates re-tranchent à chaque tour. Elles tournent dans la phase build, après chaque tentative : une phase build verte signifie gates vertes sur le dernier état du code, pas de feu vert périmé.
  • Trois échecs, trois portes, et la boucle n’en ouvre qu’une de plus. Le contrat repart via correction() (chapitre 9), la gate repart en enveloppe via handoff() (aujourd’hui), la brèche de permission ne repart jamais : l’écriture est annulée et la phase meurt (chapitre 10).

Exemple concret

Le builder implémente la spec, déclare ses fichiers, mais bun test rend exit 1 : une assertion casse. Hier : run rouge, ~10 à 20 centimes dépensés pour rien, et c’est vous qui relisiez le motif avant de relancer un run entier. Aujourd’hui, le verdict (gate nommée, exit 1, la queue de la sortie en preuve) repart dans la session vivante du builder. Il voit le test qui casse, corrige les quelques lignes, relance la suite, redéclare ses fichiers : ~2 à 5 centimes, une à deux minutes de plus, et les gates re-tranchent. Le run entier reste vert sans que vous ayez levé les yeux. La reprise coûte un facteur 5 environ de moins que le re-run, et surtout elle ne coûte plus votre attention.

Trois échecs, trois portes

ÉchecQui le détecteCe qui repart vers l’agentSession
contrat — enveloppe invalideparse() + check(), en codele motif exact, via correction()la même, vivante
gate — déclaration fausse, tests rougesles gates, en codele verdict, mis en enveloppe et injecté par handoff()la même, vivante
brèche de permissionl’état des lieux, en coderien — l’écriture est annulée, la phase meurtle run s’arrête

Script — la boucle, au cœur de la phase build

Cette pièce ne touche pas le harnais : le roster décide qui tourne, le port traduit, une seule version suffit donc. Le cœur d’adw_sdlc.py (fichier complet dans les travaux pratiques) : les gates tournent dans la phase, et leur échec prépare la relance au lieu de tuer le run :

# Les gates, DANS la phase build : leur echec doit revenir au builder
# dans sa session vivante — pas arreter le run comme dans adw_build.
failed = gates.motif(reports)
if failed:
    verdict = Envelope(status="fail", summary="gates en echec sur ton build",
                       notes_for_next_agent=failed)
    # Le verdict du code part par la meme porte qu'un rapport d'agent :
    # une enveloppe, injectee par handoff() — la symetrie du chapitre 9.
    next_ask = (REPAIR_ASK + "\n\n" + envelopes.handoff(verdict) + "\n\n"
                + envelopes.contract(BuildEnvelope))
    raise PhaseFailure(f"gates en echec :\n{failed}")

Piège courant : « la boucle de correction, c’est laisser l’agent réessayer jusqu’à ce que ça passe » décrit une autre machine, une machine sans frein. Ici, chaque tour est cadré trois fois par le code : le verdict qui repart est déterministe (les gates, pas une impression), le nombre de reprises est borné (--fix-loops), et c’est le même juge qui re-tranche à chaque tour. L’agent ne négocie pas la sortie de la boucle, il la mérite.


Review, Document : le SDLC de bout en bout

L’idée en une phrase

adws/adw_sdlc.py enchaîne le SDLC complet (plan → build → test → review → document) en dix-huit phases dont quatre seulement sont des phases agent : le reviewer répond à la seconde question (« est-ce ce qui était demandé ? », que les gates ne savent pas poser), le documenter écrit la mémoire du changement sous app_docs/, et tout le séquencement reste côté code déterministe.

Points clés

  • Deux questions, deux juges. Les gates tranchent « est-ce que ça tourne ? », mécaniquement et pour zéro token. Le reviewer juge « est-ce ce qui était demandé ? » contre la spec, son unique contrat, exigence par exigence. Aucun des deux ne sait répondre à la question de l’autre.
  • status n’est pas le verdict. Dans ReviewEnvelope, status: success dit que la revue a abouti. Le verdict, c’est approved, et un refus doit nommer ses findings, check() l’exige.
  • Le reviewer juge les mains liées : writes: [] au roster depuis le chapitre 10, état des lieux après sa phase. L’exemple du chapitre 10, le reviewer serviable qui « corrige au passage », est désormais un cas que votre usine détecte et annule en vrai.
  • Un refus de review arrête le run en rouge, délibérément. Un échec de gate est mécanique et la machine le renvoie au builder. Un refus de review met en cause la conformité à la spec, et cet arbitrage vous revient : la corriger coûte zéro token (chapitre 11), et personne ne laisse deux agents renégocier la spec entre eux.
  • Le documenter écrit pour l’ingénieur qui arrive après : ce qui a changé, où ça vit, comment le vérifier, traçable aux fichiers du run, jamais un fichier que le run n’a pas touché. Il tourne sur le modèle léger par défaut du roster : rédiger d’après un diff est un travail de rapport, pas de décision. Un compte rendu ne s’écrase jamais.

Exemple concret

Lancez adw_sdlc.py sur « ajoute un bouton pour effacer le texte de l’éditeur de Plume ». Le scout lit Plume sur le léger : moins d’un centime. Le planner pose la spec sur le frontier : ~15 à 25 centimes. Le builder implémente sur le workhorse : ~10 à 20 centimes, et si une gate recale le build, la reprise ajoute ~2 à 5 centimes. Le reviewer confronte le code à la spec sur l’intermédiaire : ~2 à 5 centimes. Le documenter rédige sur le léger : ~1 centime. Total : ~40 à 60 centimes, 10 à 15 minutes, quatre agents, un seul geste de votre part, et à l’arrivée un code retour plus deux artefacts durables : la spec dans specs/, le compte rendu dans app_docs/. La variante éco tient en une ligne : passez le model: du planner sur le workhorse, et le même SDLC tombe à ~20 à 30 centimes.

Le SDLC en une commande

ÉtapeQuiCôté coutureCe qui traverse
planscout léger, puis planner frontieragentla demande → PlanEnvelope (la spec)
buildbuilder workhorseagentla spec → BuildEnvelope
testles gatescodele verdict — en enveloppe s’il repart
reviewreviewer intermédiaire, writes: []agentReviewEnvelope : approved + findings
documentdocumenter légeragentDocumentEnvelope : le compte rendu sous app_docs/

Config — le cinquième agent du roster

Le documenter rejoint la feuille de distribution, seule évolution du roster aujourd’hui, et le fichier complet est dans les travaux pratiques. Il hérite du modèle léger et du thinking des défauts : il rapporte, il ne décide pas.

# Le documenter : rediger la memoire du changement, rien d'autre.
# Modele et thinking herites des defauts (leger, medium) — rediger d'apres
# un diff est un travail de rapport, pas de decision.
- name: documenter
  purpose: Rediger apres coup ce qui a change et pourquoi ; n'ecrire que sous app_docs/.
  writes:
    - app_docs/                # la memoire de l'usine — et rien d'autre
  tools: [read, bash, grep, find, ls, write]

Piège courant : « un refus de review devrait repartir vers le builder, comme un échec de gate » semble cohérent, c’est pourtant confondre deux natures d’échec. La gate est un juge déterministe : son verdict est un fait, la machine peut le renvoyer sans arbitrage. Un refus de review dit que le code et la spec divergent, et la bonne correction est parfois dans la spec, pas dans le code. Renvoyer le refus en boucle, c’est laisser deux agents converger entre eux vers autre chose que ce que vous avez demandé. Le run rouge vous rend l’arbitrage : relire les findings, corriger la spec à coût nul, relancer.


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

Deux pièces closent la zone « squelette ADW » du plan : adws/adw_sdlc.py, la chaîne maîtresse qui s’articule sur tout ce que le module a posé (runner, enveloppes, roster, permissions, scout, plan, build, gates) et la version chapitre 13 de factory.config.yaml, qui distribue le cinquième rôle. La loi du livre tourne désormais en circuit fermé : l’agent propose, le code dispose, et quand le code refuse, son verdict repart vers l’agent en enveloppe, par la même porte qu’un rapport, dans la session qui a produit le presque-bon. Quatorze phases code, quatre phases agent, et le contexte ne traverse qu’en enveloppes typées : ScoutEnvelope, PlanEnvelope, BuildEnvelope, ReviewEnvelope, DocumentEnvelope, plus celle, fabriquée par le code, qui transporte les verdicts de gates. À l’usage : ~40 à 60 centimes et 10 à 15 minutes le cycle complet (~20 à 30 en variante éco). Ce que la pièce économise, c’est le re-run entier à chaque accroc (la reprise en session vivante coûte un facteur 5 de moins) et l’après-midi d’ingénieur qu’aurait coûté le même cycle mené à la main. Le jalon du module est atteint : votre usine déroule un SDLC entier, vert, en une commande. Le module 4 s’attaque à ses moteurs : quel modèle pour quelle phase, et comment le mesurer.


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, deux fichiers : le roster version chapitre 13 (le documenter rejoint la distribution) et la chaîne maîtresse adw_sdlc.py. Les ADW des chapitres précédents tournent tels quels : adw_build.py garde son comportement d’hier (l’échec de gate y arrête le run), la boucle de correction étant le propre de la chaîne complète.

Pièce — adws/adw_config/factory.config.yaml

Cette version remplace celle du chapitre 10. Une seule évolution : l’entrée documenter en fin de roster, rien d’autre ne bouge. Identifiants et prix relevés sur openrouter.ai/models le 23 août 2026, des exemples datés à vérifier le jour où vous poserez la pièce.

# factory.config.yaml — le roster de l'usine : un agent, un role, un modele.
# Les ADW nomment des agents, jamais des modeles : changer de moteur, c'est
# changer UNE ligne ici, aucun script. Identifiants et prix releves sur
# openrouter.ai/models le 2026-08-23 — des exemples dates, pas des verites.
defaults:
  harness: pi                    # pi | claude — les deux adaptateurs du port (ch. 7)
  model: deepseek/deepseek-v4-flash-0731   # leger : ~0,07 $/M entree, ~0,18 $/M sortie
  thinking: medium               # off | minimal | low | medium | high | xhigh | max
  tools: [read, bash, grep, find, ls]      # lecture + execution ; ecrire se merite
  # Interdit a tout agent qui ne le nomme pas lui-meme dans son `writes` :
  # un agent ne doit pas pouvoir editer la machinerie qui juge son travail.
  protected_files:
    - adws/adw_modules/
    - adws/adw_config/
    - adws/adw_*.py
    - PLAN.md
  data_dir: adws/adw_data        # le runtime : TOUJOURS ouvert, jamais commite

agents:
  - name: scout
    purpose: Reperer ou vivent les choses dans le repo ; ne rien changer.
    thinking: low                # il rapporte, il ne decide pas
    writes: []                   # lecture seule vis-a-vis du repo — pas muet :
                                 # son rapport atterrit sous data_dir
    # write : indispensable pour poser ce rapport — l'allowlist regle
    # l'interface, c'est writes: [] (verifie apres coup) qui protege le repo.
    tools: [read, bash, grep, find, ls, write]

  - name: planner
    purpose: Transformer une demande en plan que le builder implemente sans questions.
    model: anthropic/claude-opus-5        # frontier : ~5 $/M entree, ~25 $/M sortie
    thinking: high               # le plan porte tout le run — c'est ici qu'on paie
    writes:
      - specs/                   # le plan est la seule trace qu'il laisse au repo
    tools: [read, bash, grep, find, ls, write]

  - name: builder
    purpose: Implementer le plan, exactement ; declarer chaque fichier modifie.
    model: z-ai/glm-5.3                   # workhorse : ~1,40 $/M entree, ~4,40 $/M sortie
    thinking: high
    # pas de cle writes : libre dans le repo — mais protected_files tient toujours.
    tools: [read, bash, grep, find, ls, edit, write]

  - name: reviewer
    purpose: Confirmer que ce qui est construit est ce qui etait demande ; ne rien changer.
    model: google/gemini-3.7-flash        # intermediaire rapide : ~0,38 $/M entree
    thinking: high               # juger demande de reflechir, pas d'ecrire
    writes: []                   # un reviewer qui ne peut pas corriger ne peut pas
                                 # corriger en douce — regle, plus promesse

  - name: documenter
    purpose: Rediger apres coup ce qui a change et pourquoi ; n'ecrire que sous app_docs/.
    # modele et thinking herites des defauts (leger, medium) : rediger
    # d'apres un diff est un travail de rapport, pas de decision.
    writes:
      - app_docs/                # la memoire de l'usine — et rien d'autre
    tools: [read, bash, grep, find, ls, write]

Pièce — adws/adw_sdlc.py

La chaîne maîtresse. Elle importe la mécanique des chapitres 11 et 12 (agent_action, constat, perimetre, BuildEnvelope, les gates) et n’ajoute que ce qui lui est propre : la boucle de correction dans la phase build, les deux nouveaux sous-types d’enveloppe, et les briefs du reviewer et du documenter. Rien de posé n’est modifié.

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = ["pyyaml"]
# ///
"""adw_sdlc — le cycle complet : plan -> build -> test -> review -> document.

Usage :
    uv run adws/adw_sdlc.py "Votre demande" [--config ...] [--retries 2] [--fix-loops 3]

La chaine maitresse de l'usine. Dix-huit phases, quatre agent. La nouveaute
du chapitre : un echec de gate ne tue plus le run — son verdict repart vers
le builder, en enveloppe, dans sa session vivante, borne par --fix-loops.
Un refus de review, lui, arrete le run en rouge : cet arbitrage vous revient.
"""
import argparse
import json
import sys
import uuid
from dataclasses import asdict, dataclass, field
from pathlib import Path

from adw_modules import envelopes, gates, harness, roster
from adw_modules.envelopes import Envelope, EnvelopeError
from adw_modules.runner import PhaseFailure, PhaseSpec, Run
from adw_scout import ScoutEnvelope, agent_action, constat, perimetre, scout_ask
from adw_plan import PlanEnvelope, plan_ask
from adw_build import BuildEnvelope, TRUTH_COMMANDS, build_ask

REQUIRED_AGENTS = ("scout", "planner", "builder", "reviewer", "documenter")

REPAIR_ASK = """Les gates ont rejete ton build — le verdict est dans l'enveloppe ci-dessous,
champ 'notes_for_next_agent'. Corrige ce qu'elle nomme, rien d'autre, relance
la suite de tests, et declare a nouveau CHAQUE fichier modifie."""


def builder_action(builder):
    """La phase build du SDLC : la boucle de correction vit DANS la phase.

    Tentative 1 : la spec. Ensuite : ce que la derniere verification a
    rejete — contrat OU gates — repart dans la MEME session. Le verdict des
    gates voyage en enveloppe, par la meme porte qu'un rapport d'agent :
    le builder ne fait pas la difference, et c'est voulu (chapitre 9).
    """
    next_ask = envelopes.correction("enveloppe invalide", BuildEnvelope)

    def action(run: Run, attempt: int) -> BuildEnvelope:
        nonlocal next_ask
        ask = build_ask(run.results["plan"].spec_path)(run) if attempt == 0 else next_ask
        request = harness.HarnessRequest(
            prompt=ask, session_id=run.sessions.get("build"),
            model=builder.model, thinking=builder.thinking, tools=builder.tools)
        try:
            result = harness.run(builder.harness, request)
        except harness.HarnessError as error:
            raise PhaseFailure(str(error)) from None
        # Memoriser la session AVANT de valider : un retry doit la poursuivre.
        run.sessions["build"] = result.session_id
        run.cost_usd += result.cost_usd
        try:
            envelope = envelopes.parse(result.text, BuildEnvelope)
        except EnvelopeError as error:
            next_ask = envelopes.correction(str(error), BuildEnvelope)
            raise PhaseFailure(f"enveloppe invalide : {error}") from None

        # Les gates, DANS la phase : re-trancher apres CHAQUE tentative —
        # une phase build verte signifie gates vertes sur le dernier etat.
        reports = [gates.changed_files_exist(envelope), gates.artifacts_exist(envelope)]
        reports += [gates.command(cmd, cwd)(envelope) for cmd, cwd in TRUTH_COMMANDS]
        failed = gates.motif(reports)
        if failed:
            verdict = Envelope(status="fail", summary="gates en echec sur ton build",
                               notes_for_next_agent=failed)
            next_ask = (REPAIR_ASK + "\n\n" + envelopes.handoff(verdict) + "\n\n"
                        + envelopes.contract(BuildEnvelope))
            raise PhaseFailure(f"gates en echec :\n{failed}")
        return envelope
    return action


@dataclass(frozen=True)
class ReviewEnvelope(Envelope):
    """Ce que le reviewer doit au code : un verdict motive, jamais une retouche."""
    approved: bool = field(default=False, metadata={
        "ask": "true si le build correspond a la spec, false sinon"})
    findings: list = field(default_factory=list, metadata={
        "ask": "liste d'objets {'requirement': l'exigence, 'met': true/false, "
               "'evidence': la preuve ou le manque}, [] si rien a signaler"})

    def check(self) -> None:
        super().check()
        for finding in self.findings:
            if not isinstance(finding, dict) or not finding.get("requirement"):
                raise EnvelopeError(
                    "chaque finding doit etre un objet avec au moins 'requirement'")
        if self.status == "success" and not self.approved and not self.findings:
            raise EnvelopeError(
                "un refus sans findings n'aide personne : nommer chaque manque")


REVIEWER_BRIEF = """Tu es le reviewer de l'usine : confirme que ce qui est construit est ce
qui etait demande. Ce n'est PAS une seance de tests — les gates s'en chargent.
- La spec est ton unique contrat : lis-la en entier, decoupe-la en exigences.
- Juge le code sur le disque, jamais le resume du builder : pars de
  'changed_files', lis les fichiers, statue sur chaque exigence.
- 'status' dit si TA revue a abouti ; le verdict, c'est 'approved' — true
  seulement si CHAQUE exigence est satisfaite.
- Ne change RIEN : tes findings repartent vers l'operateur, c'est l'unique
  voie de reparation."""


def review_ask(run: Run) -> str:
    """La mission du reviewer : le brief, la spec, le build a juger, le contrat."""
    plan_env: PlanEnvelope = run.results["plan"]
    return (REVIEWER_BRIEF
            + f"\n\n### spec\n\nLa spec a confronter au code : {plan_env.spec_path}\n\n"
            + envelopes.handoff(run.results["build"]) + "\n\n"
            + envelopes.contract(ReviewEnvelope))


@dataclass(frozen=True)
class DocumentEnvelope(Envelope):
    """Ce que le documenter doit au code : ou vit le compte rendu."""
    doc_path: str = field(default="", metadata={
        "ask": "chemin du compte rendu ecrit sous app_docs/, '' si status=fail"})

    def check(self) -> None:
        super().check()
        if self.status == "success" and not self.doc_path.startswith("app_docs/"):
            raise EnvelopeError("doc_path doit pointer un fichier sous app_docs/")


DOCUMENTER_BRIEF = """Tu es le documenter de l'usine : redige, pour l'ingenieur qui arrive
apres, ce que ce run a change.
- Lis la spec et les fichiers de 'changed_files' : tout ce que tu ecris doit
  etre tracable au code — ne nomme JAMAIS un fichier que le run n'a pas touche.
- Ecris le compte rendu dans app_docs/{adw_id}-<deux-a-quatre-mots-kebab>.md :
  ce qui a change, ou ca vit, comment le verifier. Si le nom existe deja,
  suffixe -v2 : un compte rendu ne s'ecrase JAMAIS.
- Court : un lecteur doit comprendre le changement en moins de deux minutes.
- N'ecris que de la documentation — jamais dans le code."""


def document_ask(run: Run) -> str:
    """La mission du documenter : le brief, la spec, le build a decrire, le contrat."""
    plan_env: PlanEnvelope = run.results["plan"]
    return (DOCUMENTER_BRIEF.format(adw_id=run.adw_id)
            + f"\n\n### spec\n\nLa demande d'origine, planifiee : {plan_env.spec_path}\n\n"
            + envelopes.handoff(run.results["build"]) + "\n\n"
            + envelopes.contract(DocumentEnvelope))


def verite_plan(run: Run, attempt: int) -> PlanEnvelope:
    """Phase code : la spec declaree existe — sinon inutile de payer le build."""
    envelope: PlanEnvelope = run.results["plan"]
    if envelope.status != "success":
        raise PhaseFailure(f"le planner declare lui-meme un echec : {envelope.summary!r}")
    spec = Path(envelope.spec_path)
    if not spec.is_file() or spec.stat().st_size == 0:
        raise PhaseFailure(f"spec declaree mais introuvable ou vide : {envelope.spec_path}")
    print(f"spec posee : {envelope.spec_path}", file=sys.stderr)
    return envelope


def verdict_review(run: Run, attempt: int) -> ReviewEnvelope:
    """Phase code : appliquer le verdict — un refus arrete le run, findings nommes.

    Deliberement PAS une boucle : un refus de review met en cause la
    conformite a la spec, et cet arbitrage revient a l'operateur — la spec
    se corrige a zero token, c'est tout le levier du chapitre 11.
    """
    review: ReviewEnvelope = run.results["review"]
    if review.status != "success":
        raise PhaseFailure(f"le reviewer declare lui-meme un echec : {review.summary!r}")
    if not review.approved:
        unmet = [f for f in review.findings if not f.get("met", False)]
        detail = "\n".join(f"  - {f.get('requirement')} : {f.get('evidence', 'non satisfait')}"
                           for f in unmet) or f"  - {review.summary}"
        raise PhaseFailure(
            "review refusee — le build ne correspond pas a la spec :\n" + detail)
    return review


def dispose(run: Run, attempt: int) -> DocumentEnvelope:
    """Phase code : la verite du compte rendu, puis le bilan du run."""
    envelope: DocumentEnvelope = run.results["document"]
    if envelope.status != "success":
        raise PhaseFailure(f"le documenter declare lui-meme un echec : {envelope.summary!r}")
    doc = Path(envelope.doc_path)
    if not doc.is_file() or doc.stat().st_size == 0:
        raise PhaseFailure(
            f"compte rendu declare mais introuvable ou vide : {envelope.doc_path}")
    print(json.dumps(asdict(envelope), indent=2, ensure_ascii=False))
    print(f"SDLC complet : spec={run.results['plan'].spec_path} "
          f"doc={envelope.doc_path}", file=sys.stderr)
    return envelope


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Le SDLC complet : plan -> build -> test -> review -> document.")
    parser.add_argument("prompt", help="la demande, en langage naturel")
    parser.add_argument("--config", default=str(roster.DEFAULT_PATH))
    parser.add_argument("--retries", type=int, default=2,
                        help="reprises de contrat des phases agent, en session vivante")
    parser.add_argument("--fix-loops", type=int, default=3,
                        help="reprises du builder quand les gates echouent")
    args = parser.parse_args()

    factory = roster.load(args.config)      # zero token : tout echec est gratuit
    missing = [name for name in REQUIRED_AGENTS if name not in factory.agents]
    if missing:
        print(f"roster incomplet : agents manquants {missing} — "
              "posez la version chapitre 13 de factory.config.yaml", file=sys.stderr)
        return 1

    scout, planner = factory.agents["scout"], factory.agents["planner"]
    builder = factory.agents["builder"]
    reviewer, documenter = factory.agents["reviewer"], factory.agents["documenter"]
    run = Run(adw_id=uuid.uuid4().hex[:8])
    return run.execute([
        PhaseSpec(name="constat_scout", kind="code", action=constat("scout", factory)),
        PhaseSpec(name="scout", kind="agent",
                  action=agent_action("scout", scout, scout_ask(args.prompt, factory),
                                      ScoutEnvelope),
                  retries=args.retries),
        PhaseSpec(name="perimetre_scout", kind="code",
                  action=perimetre("scout", scout, factory)),
        PhaseSpec(name="constat_plan", kind="code", action=constat("plan", factory)),
        PhaseSpec(name="plan", kind="agent",
                  action=agent_action("plan", planner, plan_ask(args.prompt, factory),
                                      PlanEnvelope),
                  retries=args.retries),
        PhaseSpec(name="perimetre_plan", kind="code",
                  action=perimetre("plan", planner, factory)),
        PhaseSpec(name="verite_plan", kind="code", action=verite_plan),
        PhaseSpec(name="constat_build", kind="code", action=constat("build", factory)),
        PhaseSpec(name="build", kind="agent", action=builder_action(builder),
                  retries=args.fix_loops),
        PhaseSpec(name="perimetre_build", kind="code",
                  action=perimetre("build", builder, factory)),
        PhaseSpec(name="constat_review", kind="code", action=constat("review", factory)),
        PhaseSpec(name="review", kind="agent",
                  action=agent_action("review", reviewer, review_ask, ReviewEnvelope),
                  retries=args.retries),
        PhaseSpec(name="perimetre_review", kind="code",
                  action=perimetre("review", reviewer, factory)),
        PhaseSpec(name="verdict_review", kind="code", action=verdict_review),
        PhaseSpec(name="constat_document", kind="code", action=constat("document", factory)),
        PhaseSpec(name="document", kind="agent",
                  action=agent_action("document", documenter, document_ask,
                                      DocumentEnvelope),
                  retries=args.retries),
        PhaseSpec(name="perimetre_document", kind="code",
                  action=perimetre("document", documenter, factory)),
        PhaseSpec(name="dispose", kind="code", action=dispose),
    ])


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

La gate du TP

# 1) le roster v2 se charge et se valide — cinq agents, zero token
uv run --with pyyaml python -m adws.adw_modules.roster

# 2) le jalon du module : le premier SDLC complet de l'usine, en une commande
uv run adws/adw_sdlc.py "Ajoute un bouton pour effacer le texte de l'editeur de Plume"

Attendu : la première commande liste désormais cinq agents, documenter compris. La seconde déroule dix-huit phases : spec posee, le build (avec, le cas échéant, une reprise gates en echec suivie d’une tentative verte), la review approuvée, puis l’enveloppe du documenter et SDLC complet : ~40 à 60 centimes, 10 à 15 minutes, ou ~20 à 30 centimes en variante éco (planner sur le workhorse). Vérifiez les deux artefacts (ls specs/ app_docs/), puis commitez : votre usine vient de dérouler son premier cycle de développement entier, verte de bout en bout.


Quiz — teste tes connaissances
Le squelette ADW 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.