Accueil / Articles

Arrêter les boucles des agents IA incontrôlables : quatre garde-fous pour la production

Le cap change, les jetons et le temps sont pris en compte, les objectifs sont vérifiés par des tests réels, les appels d’outils de hachage permettent de détecter les boucles ping-pong, et un retrait en toute sécurité a lieu lorsque l’agent autonome est bloqué.

Éditeur : REACTAPP.TOP14 min de lecture3289 mots

Sources: Don’t Let Your AI Agents Loop Forever: An Engineering Guide to Termination Criteria

Une facture API de 3 800 dollars due à un agent autonome bloqué pendant douze heures dans une boucle de correction syntaxique est une épreuve que personne ne devrait devoir affronter. Voici comment mettre en place des règles d’arrêt de qualité professionnelle autour d’un système agent.

Le scénario du vendredi après-midi

C’est vendredi, 17h00. Un nouvel agent de codage entre dans l’environnement de test. Sa mission est d’examiner les demandes de fusion, de corriger les erreurs de types dans TypeScript et de relancer les tests. La commande semble correcte, l’ordinateur se ferme et le week-end commence.

Déjà samedi matin, les alertes de PagerDuty et les notifications de facturation inondent le téléphone. L’agent a corrigé le fichier user.service.ts en raison d’une erreur de type, ce qui a endommagé auth.service.ts. La correction de ce dernier fichier a à son tour endommagé le premier, et les deux fichiers se sont continuellement annulés mutuellement. Après 1 400 appels d’outils, la facture liée aux tokens atteint 3 800 dollars et le jeu de tests est corrompu.

La majeure partie de l’attention en ingénierie se porte sur les prompts, le choix des outils et les graphes d’orchestration. En production, la décision ayant les plus grandes conséquences concerne la manière dont l’agent s’arrête, et non la manière dont il démarre.

Qu’est-ce qu’un cycle agent

Un cycle agent est un cycle de contrôle dans lequel un LLM perçoit l’état, choisit une action, appelle un outil, lit la sortie et décide s’il doit continuer.

                   ┌──────────────────────────────────────┐
                   │        Agent Perception Loop         │
                   │       (Perceive → Plan → Act)        │
                   └──────────────────┬───────────────────┘
                                      │
           ┌──────────────────────────┼──────────────────────────┐
           ▼                          ▼                          ▼
┌────────────────────┐    ┌────────────────────┐    ┌────────────────────┐
│ 1. Success Guard   │    │ 2. Resource Caps   │    │ 3. Progress Guard  │
│ (Programmatic Test)│    │ (Tokens/Turns/Time)│    │ (Loop/Hash Detect) │
└────────────────────┘    └────────────────────┘    └────────────────────┘
                                      │
                                      ▼
                   ┌──────────────────────────────────────┐
                   │  4. Human Handoff / Safe Rollback    │
                   └──────────────────────────────────────┘

Si le système de contrôle qui l’entoure ne dispose pas de règles d’arrêt explicites et structurées, l’agent aboutit finalement à l’un de trois modes d’échec :

  • Exécution infinie : boucle infinie due à des appels d’outils erronés ou répétés.
  • Sortie prématurée : arrêt prématuré avec déclaration de victoire alors que le travail est inachevé ou échoue silencieusement.
  • Epuisement du budget : utilisation totale de la quantité allouée de tokens avant l’achèvement d’une tâche en plusieurs étapes.

Quatre types de critères d’arrêt couvrent ces cas, et chaque agent a besoin des quatre avant de pouvoir être déployé.

1. Critères de succès : vérifier l’objectif de manière déterministe

Le piège. Ne jamais accepter le jugement de l’agent en se contentant de lui demander s’il a terminé. Les modèles ont tendance à privilégier l’idée de complétion et à se montrer flatteurs, et ils rapportent souvent un succès alors que la construction est défectueuse ou que le fichier de sortie est vide.

La solution. Considérez la sortie du modèle comme une hypothèse non vérifiée, et faites en sorte que la boucle compare l’état réel avec quelque chose de déterministe :

  • exécutez npm test ou pytest dans un processus enfant et lisez le code de sortie
  • effectuez des vérifications de type et des analyses de code avec des codes de sortie stricts, tels que tsc --noEmit
  • analysez la sortie à l’aide de schémas Zod ou Pydantic
import { execSync } from 'node:child_process';

export interface VerificationResult {
  success: boolean;
  message: string;
  stepFailed?: string;
}

/** execSync throws on non-zero exit; diagnostics may land on either stream. */
function runOrCapture(command: string, cwd: string): string | null {
  try {
    execSync(command, { cwd, stdio: 'pipe' });
    return null;
  } catch (err: unknown) {
    const e = err as { stdout?: Buffer; stderr?: Buffer };
    const out = e.stdout?.toString() ?? '';
    const errOut = e.stderr?.toString() ?? '';
    return [out, errOut].filter(Boolean).join('\n') || String(err);
  }
}

export class GoalVerifier {
  public static verify(workspacePath: string): VerificationResult {
    const steps: Array<[string, string]> = [
      ['tsc', 'npx tsc --noEmit'],
      ['npm_test', 'npm test'],
    ];

    for (const [stepId, command] of steps) {
      const failure = runOrCapture(command, workspacePath);
      if (failure !== null) {
        return {
          success: false,
          message: `\`${command}\` failed:\n${failure}`,
          stepFailed: stepId,
        };
      }
    }

    return { success: true, message: 'All typechecks and tests passed cleanly.' };
  }
}

La règle de base est simple. Si un éditeur humain exécuterait une commande pour vérifier une demande de fusion, cette même commande devrait servir de condition de sortie du boucle.

2. Plafonds de ressources et de budget : limites strictes

Le piège. Lorsqu’un agent rencontre un cas limite ou une erreur inattendue d’outil, sa réaction par défaut est de réessayer avec de légères variations dans les instructions. En l’absence de plafond au niveau du moteur, un boucle bloquée peut consommer des millions de tokens en quelques minutes à peine.

La solution. Imposer des limites strictes dans trois dimensions indépendantes : les tours, les tokens et le temps réel. Compter les tours et les tokens séparément, car un tour peut impliquer plusieurs appels au modèle, et mélanger les deux augmente discrètement le nombre de tours.

export interface ResourceLimits {
  maxTurns: number;       // e.g. 10 iterations
  maxTotalTokens: number; // e.g. 100_000 input + output
  timeoutMs: number;      // e.g. 120_000 (2 minutes)
}

export class ResourceGuard {
  private readonly startTime = Date.now();
  private totalTokensUsed = 0;
  private currentTurn = 0;

  constructor(private readonly limits: ResourceLimits) {}

  /** Call once per loop iteration, before the model call. */
  public beginTurn(): void {
    this.currentTurn += 1;
  }

  /** Call for every model call, including retries inside a turn. */
  public recordUsage(tokens: number): void {
    this.totalTokensUsed += tokens;
  }

  public getTurnCount(): number {
    return this.currentTurn;
  }

  public checkShouldTerminate(): { terminate: boolean; reason?: string } {
    if (this.currentTurn >= this.limits.maxTurns) {
      return {
        terminate: true,
        reason: `Exceeded turn cap (${this.limits.maxTurns})`,
      };
    }
    if (this.totalTokensUsed >= this.limits.maxTotalTokens) {
      return {
        terminate: true,
        reason: `Exceeded token budget (${this.totalTokensUsed}/${this.limits.maxTotalTokens})`,
      };
    }
    const elapsed = Date.now() - this.startTime;
    if (elapsed >= this.limits.timeoutMs) {
      return {
        terminate: true,
        reason: `Wall-clock timeout reached (${elapsed}ms/${this.limits.timeoutMs}ms)`,
      };
    }
    return { terminate: false };
  }
}

Les plafonds sont une mesure de sécurité, pas une partie intégrante de la conception. Si votre agent s’arrête régulièrement parce qu’il a atteint maxTurns, le problème ne réside pas dans le plafond, mais plutôt dans la manière dont la tâche est décomposée.

3. Garde-fous du progrès : repérer les agents bloqués ou déviés

Trois schémas se reproduisent sans cesse :

  • Ping-pong. L’agent utilise l’Outil A, rencontre une erreur X, utilise ensuite l’Outil B pour la corriger, rencontre une erreur Y, puis réutilise l’Outil A avec les mêmes paramètres.
  • Agitation inutile. Modifier le fichier A endommage le fichier B, et modifier le fichier B endommage le fichier A. L’espace de travail continue de changer tandis que le progrès global reste nul.
  • Dérive sémantique. L’agent se heurte à une alerte inattendue de vérificateur de syntaxe, passe cinq tours à travailler sur le style et oublie l’objectif initial.

Afin de détecter les blocages avant que maxTurns ne soit épuisé, conservez une fenêtre glissante de hachages des appels d’outils ainsi que des hachages des différences de l’espace de travail. La répétition de hachages identiques indique que l’agent tourne en rond.

import { createHash } from 'node:crypto';

export interface ToolCall {
  name: string;
  args: Record<string, unknown>;
}

export class ProgressGuard {
  private readonly recentActionHashes: string[] = [];

  constructor(
    private readonly windowSize = 5,
    private readonly repeatThreshold = 3,
  ) {}

  /** Stable stringify: key order must not change the hash. */
  private hashToolCall(call: ToolCall): string {
    const args = JSON.stringify(call.args, Object.keys(call.args).sort());
    return createHash('sha256').update(`${call.name}:${args}`).digest('hex');
  }

  /** Returns true when the same call has appeared `repeatThreshold` times in the window. */
  public trackAndCheckStuck(call: ToolCall): boolean {
    const actionHash = this.hashToolCall(call);
    const priorOccurrences = this.recentActionHashes.filter((h) => h === actionHash).length;

    this.recentActionHashes.push(actionHash);
    if (this.recentActionHashes.length > this.windowSize) {
      this.recentActionHashes.shift();
    }

    return priorOccurrences + 1 >= this.repeatThreshold;
  }
}

Toute répétition ne mérite pas la même suspicion. Une requête en lecture seule effectuée trois fois est généralement inoffensive, tandis qu’une opération d’écriture répétée trois fois ne l’est presque jamais. Lorsque les métadonnées de l’outil sont disponibles, ajustez le seuil en conséquence plutôt que de traiter toutes les requêtes de la même manière.

4. Escalade humaine et annulation sécurisée

Lorsqu’un boucle se termine en raison d’une limite de budget ou d’un blocage, le pire résultat est un système partiellement endommagé. Un disjoncteur électrique doit effectuer trois actions dans l’ordre : recueillir des diagnostics (quel outil a échoué et quels sont les derniers erreurs observées), ramener l’espace de travail à un état connu et escalader la situation à une personne munie d’un résumé clair.

Faites attention à la deuxième étape. Exécuter git reset --hard suivi de git clean -fd est irréversible. git stash --include-untracked est plus sûr, car il conserve le travail effectué par l’agent pour inspection et peut encore être supprimé ultérieurement. Réservez le reset complet aux environnements temporaires.

import { execSync } from 'node:child_process';
import { writeFileSync } from 'node:fs';
import { join } from 'node:path';

export class AgentEscalationRequiredError extends Error {
  constructor(message: string, public readonly reportPath?: string) {
    super(message);
    this.name = 'AgentEscalationRequiredError';
  }
}

export class AgentCircuitBreaker {
  constructor(
    private readonly workspaceDir: string,
    private readonly reportDir: string, // keep reports OUTSIDE the workspace
  ) {}

  public handleAbort(reason: string, history: unknown[] = []): never {
    console.error(`[CIRCUIT BREAKER] Terminating agent loop: ${reason}`);

    // 1. Park workspace changes recoverably.
    try {
      execSync('git stash push --include-untracked -m "agent-abort"', {
        cwd: this.workspaceDir,
        stdio: 'pipe',
      });
    } catch (err) {
      console.error('git stash failed during abort; workspace left as-is:', err);
    }

    // 2. Write a diagnostic trace for human review.
    const reportPath = this.writeFailureReport(reason, history);

    // 3. Signal the orchestrator.
    throw new AgentEscalationRequiredError(`Agent failed safely. Reason: ${reason}`, reportPath);
  }

  private writeFailureReport(reason: string, history: unknown[]): string {
    const reportPath = join(this.reportDir, `agent_failure_${Date.now()}.json`);
    writeFileSync(
      reportPath,
      JSON.stringify(
        {
          timestamp: new Date().toISOString(),
          workspace: this.workspaceDir,
          reason,
          historyLength: history.length,
          history: history.slice(-10),
        },
        null,
        2,
      ),
      'utf-8',
    );
    return reportPath;
  }
}

Étude de cas : un générateur d’applications ouvertes

Pour corriger une erreur, on dispose d’un ensemble de tests prédéfini à vérifier. Le cas plus difficile concerne des instructions ouvertes et subjectives, par exemple : créer une application web moderne localement qui gère des tâches, fonctionne de manière autonome jusqu’à ce que le travail soit terminé, soit parfaitement finalisée et pleinement fonctionnelle, et décider soi-même du moment où elle doit s’arrêter.

Des termes tels que « parfaitement finalisée » et « terminé » sont extrêmement flous. Sans guidance, l’agent s’arrête soit après avoir généré un simple index.html, soit il passe des heures à ajuster les marges CSS.

 ┌──────────────────────────────────────────────────────────┐
 │ Vague prompt ("build a modern web app locally")          │
 └────────────────────────────┬─────────────────────────────┘
                              ▼
 ┌──────────────────────────────────────────────────────────┐
 │ Phase 1: Dynamic spec synthesis (`ac-matrix.json`)       │
 └────────────────────────────┬─────────────────────────────┘
                              ▼
 ┌──────────────────────────────────────────────────────────┐
 │ Phase 2: Multi-agent execution loop                      │
 │ (Coder agent + design critic + headless E2E verifier)    │
 └────────────────────────────┬─────────────────────────────┘
                              │
    ┌─────────────────────────┼─────────────────────────┐
    ▼                         ▼                         ▼
┌──────────────────┐    ┌──────────────────┐    ┌──────────────────┐
│ Gate 1: Goal     │    │ Gate 2: Resource │    │ Gate 3: Progress │
│ verification     │    │ caps (turns/     │    │ guard (deadlock/ │
│ (build/E2E/ACs)  │    │ token budget)    │    │ repetition)      │
└─────────┬────────┘    └─────────┬────────┘    └─────────┬────────┘
          └───────────────────────┼───────────────────────┘
                                  ▼
 ┌──────────────────────────────────────────────────────────┐
 │ Gate 4: Safe exit OR circuit-breaker rollback            │
 └──────────────────────────────────────────────────────────┘

Synthétiser une spécification en premier

Dès le premier tour, avant d’écrire le moindre code, l’agent crée une matrice explicite de critères d’acceptation et la sauvegarde sous le nom ac-matrix.json. Il regroupe ces critères en trois catégories :

  • Critères fonctionnels, tels que la persistance locale via SQLite ou JSON, les opérations CRUD et un interrupteur pour le mode sombre.
  • Critères techniques, tels qu’aucune alerte TypeScript ni lint, ainsi qu’un serveur de développement qui démarre sans erreur de console.
  • Critères de conception, tels qu’un layout responsive, des tokens de design cohérents, des états vides et de petites interactions.

Le même modèle qui crée la matrice sera utilisé plus tard pour évaluer son propre travail, il convient donc de la considérer comme un contrat de périmètre et non comme une norme objective. Sa véritable valeur réside dans le fait qu’elle est fixée tôt, avant que l’agent n’ait la moindre raison de modifier les objectifs.

Division des rôles

Pour empêcher un agent d’évaluer son propre travail comme étant parfait, séparez les tâches :

  • Un agent sous-chargeur de code crée les fonctionnalités, gère les routages et l’affichage.
  • Un vérificateur de navigateur sans interface lance le serveur de développement, prend des captures d’écran avec Playwright et vérifie les erreurs dans la console.
  • Un agent critiqueur de design examine l’interface utilisateur selon les normes du système de design en ce qui concerne la hiérarchie, l’espacement et le contraste.

L’orchestrateur

Le code ci-dessous suppose l’existence de dépendances injectées (llmAgent, executeTool, runHeadlessBrowserCheck et runDesignCriticAgent), que vous remplacerez par votre propre couche d’orchestration.

import { execSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { ResourceGuard, ProgressGuard, AgentCircuitBreaker } from './guards';

interface AcceptanceCriterion {
  id: string;
  description: string;
  status: 'PENDING' | 'IN_PROGRESS' | 'DONE';
}

interface AcMatrix {
  items: AcceptanceCriterion[];
}

export interface RunResult {
  success: true;
  turns: number;
  summary: string;
}

export async function runOpenEndedWebAppGenerator(
  userPrompt: string,
  workspacePath: string,
  reportDir: string,
  devServerUrl = 'http://localhost:5173',
  options = { maxTurns: 15, maxTotalTokens: 200_000, timeoutMs: 300_000 },
): Promise<RunResult> {
  const resources = new ResourceGuard(options);
  const progress = new ProgressGuard();
  const circuitBreaker = new AgentCircuitBreaker(workspacePath, reportDir);
  const acMatrixPath = join(workspacePath, 'ac-matrix.json');

  let currentPrompt = userPrompt;
  let designRetries = 0;
  const maxDesignRetries = 3;

  while (true) {
    // GUARD 1: resource ceilings
    const resourceCheck = resources.checkShouldTerminate();
    if (resourceCheck.terminate) {
      circuitBreaker.handleAbort(resourceCheck.reason!);
    }
    resources.beginTurn();

    const turnResult = await llmAgent.step(currentPrompt);
    resources.recordUsage(turnResult.tokensUsed);

    // GUARD 2: deadlock detection (only meaningful when a tool was called)
    let toolOutput = '(no tool call this turn)';
    if (turnResult.toolCall) {
      if (progress.trackAndCheckStuck(turnResult.toolCall)) {
        circuitBreaker.handleAbort('Repeating tool call detected (stuck agent)');
      }
      toolOutput = await executeTool(turnResult.toolCall);
    }

    // GUARD 3: deterministic convergence check
    const verification = await evaluateConvergence(workspacePath, acMatrixPath, devServerUrl);

    if (verification.gateFailed === 'design') {
      designRetries += 1;
      if (designRetries > maxDesignRetries) {
        circuitBreaker.handleAbort(
          `Design gate never converged after ${maxDesignRetries} refinement passes`,
        );
      }
    }

    if (verification.isConverged) {
      return {
        success: true,
        turns: resources.getTurnCount(),
        summary: 'Web app built, tested, and design-reviewed cleanly.',
      };
    }

    currentPrompt = `Tool output:\n${toolOutput}\n\nConvergence status:\n${verification.statusMessage}`;
  }
}

interface ConvergenceResult {
  isConverged: boolean;
  statusMessage: string;
  gateFailed?: 'build' | 'runtime' | 'acs' | 'design';
}

async function evaluateConvergence(
  workspacePath: string,
  acMatrixPath: string,
  devServerUrl: string,
): Promise<ConvergenceResult> {
  // Gate 1: build and typecheck
  try {
    execSync('npx tsc --noEmit && npm run build', { cwd: workspacePath, stdio: 'pipe' });
  } catch (err: unknown) {
    const e = err as { stdout?: Buffer; stderr?: Buffer };
    const output = [e.stdout?.toString(), e.stderr?.toString()].filter(Boolean).join('\n');
    return {
      isConverged: false,
      gateFailed: 'build',
      statusMessage: `Gate 1 failed (build/typecheck):\n${output || String(err)}`,
    };
  }

  // Gate 2: dev server and runtime health
  const e2eResult = await runHeadlessBrowserCheck(devServerUrl);
  if (!e2eResult.noConsoleErrors) {
    return {
      isConverged: false,
      gateFailed: 'runtime',
      statusMessage: `Gate 2 failed (console errors): ${e2eResult.errors.join(', ')}`,
    };
  }

  // Gate 3: acceptance criteria fully complete
  let acMatrix: AcMatrix;
  try {
    acMatrix = JSON.parse(readFileSync(acMatrixPath, 'utf-8')) as AcMatrix;
  } catch {
    return {
      isConverged: false,
      gateFailed: 'acs',
      statusMessage: 'Gate 3 incomplete: `ac-matrix.json` missing or unparseable.',
    };
  }

  const pending = acMatrix.items.filter((ac) => ac.status !== 'DONE');
  if (pending.length > 0) {
    return {
      isConverged: false,
      gateFailed: 'acs',
      statusMessage: `Gate 3 incomplete: ${pending.length} ACs remaining (${pending
        .map((a) => a.id)
        .join(', ')})`,
    };
  }

  // Gate 4: design audit (soft gate — see retry cap in the caller)
  const criticVerdict = await runDesignCriticAgent(e2eResult.screenshots);
  if (criticVerdict.score < 8.5) {
    return {
      isConverged: false,
      gateFailed: 'design',
      statusMessage: `Gate 4 incomplete (design ${criticVerdict.score}/10): ${criticVerdict.feedback}`,
    };
  }

  return { isConverged: true, statusMessage: 'All four convergence gates passed.' };
}

Remarquez que les étapes ne sont pas égales. Les deux premières, le compilateur et le navigateur, sont déterministes, car aucun d’eux n’a d’opinion personnelle. Les deux dernières sont évaluées par des modèles, ce qui signifie qu’elles peuvent osciller indéfiniment d’un côté à l’autre. C’est pourquoi l’étape de conception nécessite sa propre limite de tentatives. Un seuil subjectif sans limite de tentatives n’est rien d’autre qu’une boucle infinie plus lente.

Un prompt principal réutilisable

Pour exécuter ce flux dans Claude Code, Copilot CLI, Cursor ou Aider, le modèle ci-dessous indique à l’agent d’écrire sa propre matrice d’acceptation, d’itérer au cours de cycles de développement, de vérifier les résultats localement, d’évaluer la qualité du design et de s’arrêter uniquement une fois l’application terminée.

You are an autonomous lead software engineer, UX designer, and QA verifier. Your goal
is to build a production-quality web application locally, from scratch.

You must operate in a self-terminating agentic loop, running iteratively until the
application is complete, polished, functional, and verified.

================================================================================
1. TARGET APPLICATION SPECIFICATION
================================================================================

[DESCRIBE YOUR APP IDEA HERE — e.g. "A task management web app with local SQLite
persistence, a kanban board with drag-and-drop, priority tags, search/filter
controls, and a dark mode theme."]

================================================================================
2. EXECUTION PROTOCOL
================================================================================

PHASE 1 — DYNAMIC SPEC SYNTHESIS (TURN 1)
Before writing application code or installing dependencies:
  1. Initialize the local project structure (e.g. Vite + React, Next.js, or Node).
  2. Create `ac-matrix.json` in the workspace root defining explicit acceptance
     criteria:
       - Feature ACs: persistence, full CRUD, interactive components, error
         handling, edge cases.
       - Engineering ACs: strict TypeScript (`npx tsc --noEmit`), zero build
         errors, zero linter warnings, dev server boots cleanly.
       - Design ACs: visual hierarchy, responsive layout, dark/light toggle,
         empty states, micro-interactions.

Format:
  {
    "project": "<app-name>",
    "items": [
      { "id": "FEAT-1", "category": "feature",     "description": "Local database persistence for tasks", "status": "PENDING" },
      { "id": "FEAT-2", "category": "feature",     "description": "Drag-and-drop kanban re-ordering",      "status": "PENDING" },
      { "id": "ENG-1",  "category": "engineering", "description": "Clean TypeScript build, zero errors",   "status": "PENDING" },
      { "id": "ENG-2",  "category": "engineering", "description": "Dev server starts with 0 console errors","status": "PENDING" },
      { "id": "DSGN-1", "category": "design",      "description": "Responsive UI with dark/light mode",    "status": "PENDING" }
    ]
  }

Once written, treat `ac-matrix.json` as frozen scope. Do not delete or weaken an
AC to make a gate pass. If an AC turns out to be genuinely infeasible, mark it
BLOCKED with a reason and surface it in the final summary.

PHASE 2 — AUTONOMOUS DEVELOPMENT LOOP
In each turn:
  - Implement features, components, schemas, and routes incrementally.
  - Run local validation after code changes (`npx tsc --noEmit`, `npm run build`).
  - Update AC statuses (PENDING → IN_PROGRESS → DONE) as work is verified.
  - Self-correction rule: if a command fails, read the exact error, fix the root
    cause, and re-verify. Do not repeat the same failing command or edit more
    than twice — change approach instead.

PHASE 3 — THE 4-GATE CONVERGENCE CHECK (MANDATORY)
Do not end execution or declare the project finished until all four gates pass
in the same turn:

Gate 1 — Compiler and build
    `npx tsc --noEmit` and `npm run build` both exit 0 with zero errors.

Gate 2 — Dev server and runtime health
    `npm run dev` boots cleanly with zero unhandled console or network errors.

Gate 3 — Acceptance criteria complete
    Every item in `ac-matrix.json` has "status": "DONE".

Gate 4 — Design audit score >= 8.5/10
    Audit visual hierarchy, color consistency, typography scale, spacing,
    transitions, responsive behavior, and empty states. Score out of 10. If
    below 8.5, refine and re-audit — but no more than 3 design passes total.
    After 3 passes, stop and report the final score as-is.

================================================================================
3. FINAL COMPLETION OUTPUT
================================================================================

Only when all four gates pass, output:
  - Final status: PROJECT COMPLETE & VERIFIED
  - App summary and architecture overview
  - Build and test commands executed, with exit codes
  - Completed acceptance-criteria summary (including any BLOCKED items)
  - Final design score (X/10) and UX highlights
  - Instructions for running the app locally

Then stop.

Checklist de pré-production

Avant de mettre en ligne un cycle d’agent, vérifiez-le en fonction des critères suivants :

  1. Vérification de l’objectif par le biais d’un test déterministe ou d’un compilateur, ainsi que d’une vérification du code de sortie externe. Obligatoire.
  2. Limite de tours, une limite fixe d’itérations telle que currentTurn >= maxTurns. Obligatoire.
  3. Budget de tokens, une limite cumulative telle que totalTokens >= tokenLimit. Obligatoire.
  4. Délai de timeout, une limite temporelle absolue telle que elapsed >= timeoutMs. Obligatoire.
  5. Protection des progrès, un détecteur à fenêtre glissante des signatures d’outils dupliquées. Recommandé.
  6. Protection de l’état, un hash de différence de l’espace de travail permettant de détecter aucune modification. Recommandé.
  7. Solution de secours fiable : un mécanisme de protection avec possibilité de revenir en arrière via git stash en cas d’annulation. Obligatoire.

Les limites plutôt que l’intelligence

Les agents autonomes fiables proviennent moins du fait de rendre le modèle plus intelligent que de la définition stricte de limites opérationnelles. Quatre éléments font la différence entre une expérience fragile et un système que l’on peut laisser fonctionner tout le week-end : des vérificateurs indépendants du modèle, des plafonds stricts en ressources, un hachage des progrès et des mécanismes de réversion permettant de se remettre en état.