Aller au contenu
English

Réparer une CI en échec

Laissez un agent corriger le code et réessayer jusqu’à ce que votre commande de test réussisse.

Enregistrez les fichiers présentés dans les onglets à côté de la configuration de la page Installation. Le script principal garde une sandbox ouverte pour que les modifications de l’agent et votre commande de test utilisent les mêmes fichiers.

Séparez la création de la sandbox, la tentative de l’agent et la vérification des commits.

import { createSandbox } from "@elie-laloum/outpost";
import { repository, sandboxProvider, coder } from "./outpost.config.ts";

export function openFixSandbox() {
  return createSandbox({
    repository,
    sandboxProvider,
    agent: coder,
    branch: { mode: "named", name: "outpost/fix-ci" },
    hooks: { sandboxReady: [{ executable: "npm", arguments: ["ci"] }] },
  });
}
export function fixRequest(feedback?: string) {
  return {
    brief: {
      text: [
        "`npm test` fails. Fix the code so that it passes, then commit the fix.",
        feedback ? `The last check failed:\n${feedback}` : "",
      ].join("\n\n"),
    },
  };
}
import type { Sandbox, LoopTaskContext } from "@elie-laloum/outpost";
import { defineAgentTask } from "@elie-laloum/outpost";
import { fixRequest } from "./fix-request.ts";

export function createAttempt(sandbox: Sandbox) {
  return async (context: LoopTaskContext, feedback: string | undefined) => {
    const agent = defineAgentTask({
      key: "coder",
      sandbox,
      request: () => fixRequest(feedback),
    });
    const result = await agent.perform(context);
    return { summary: result.text, commits: result.commits.length };
  };
}
import type { Sandbox, LoopTaskContext } from "@elie-laloum/outpost";

export async function testCommand(sandbox: Sandbox, context: LoopTaskContext) {
  return sandbox.command({
    executable: "npm",
    arguments: ["test"],
    retain: 20_000,
    signal: context.signal,
  });
}
import type {
  Sandbox,
  LoopTaskContext,
  LoopCheckResult,
} from "@elie-laloum/outpost";

export async function checkCommit(
  sandbox: Sandbox,
  context: LoopTaskContext,
): Promise<LoopCheckResult> {
  const status = await sandbox.command({
    executable: "git",
    arguments: ["status", "--porcelain"],
    signal: context.signal,
  });
  const feedback = `Tests pass. Commit these changes:\n${status.stdout}`;
  return status.stdout.trim() ? { done: false, feedback } : { done: true };
}

Assemblez la boucle et lancez fix-ci.ts, qui possède et ferme la sandbox.

import type {
  Sandbox,
  LoopTaskContext,
  LoopCheckResult,
} from "@elie-laloum/outpost";
import { testCommand } from "./test-command.ts";
import { checkCommit } from "./check-commit.ts";

export function createCheck(sandbox: Sandbox) {
  return async (context: LoopTaskContext): Promise<LoopCheckResult> => {
    const tests = await testCommand(sandbox, context);
    if (tests.status !== 0)
      return { done: false, feedback: `${tests.stdout}\n${tests.stderr}` };
    return checkCommit(sandbox, context);
  };
}
import type { Sandbox } from "@elie-laloum/outpost";
import { defineLoopTask } from "@elie-laloum/outpost";
import { createAttempt } from "./fix-attempt.ts";
import { createCheck } from "./fix-check.ts";

export function defineFix(sandbox: Sandbox) {
  return defineLoopTask({
    key: "fix",
    maxRounds: 4,
    timeoutMs: 1_200_000,
    attempt: createAttempt(sandbox),
    check: createCheck(sandbox),
  });
}
import { reportValue } from "./reporter.ts";
import { openFixSandbox } from "./fix-sandbox.ts";
import { defineFix } from "./fix-task.ts";
import { defineWorkflow } from "@elie-laloum/outpost";
await using sandbox = await openFixSandbox();
export const fix = defineFix(sandbox);
export const result = await defineWorkflow("fix-ci", [fix]).start({
  budget: { attempts: 4, usage: { input: 5_000_000, output: 200_000 } },
});
reportValue(`status: ${result.status}`);
// Example output: status: done
reportValue(`branch: ${sandbox.workspace.branch}`);
// Example output: branch: outpost/fix-ci
reportValue(`rounds: ${result.tasks[0]?.rounds?.length ?? 0}`);
// Example output: rounds: 2
reportValue(`tokens: ${JSON.stringify(result.usage.tokens)}`);
// Example output: tokens: {"input":1200,"cached":0,"output":320}
if (result.status === "done") reportValue(result.value(fix).summary);
// Example output: Fixed the parser and verified the tests.
result.unwrap();

Lancez la boucle de vérification, puis examinez les commits de sa branche. Le script affiche le statut, le nombre de tours et la consommation de tokens ; il se termine en erreur si le workflow échoue.

node fix-ci.ts
git log --oneline HEAD..outpost/fix-ci

Chaque lien indique qui transmet quoi à qui, dans le sens de la flèche.

Glissez pour vous déplacer · Ctrl + molette pour zoomer
100 %
  • Votre scriptfix-ci.ts ouvre la sandbox, démarre le workflow avec son budget et fixe le code de sortie. (hôte)
    • → Sandbox : createSandbox()
    • → Tentative : workflow.start()
  • BoucledefineLoopTask(), 4 tours au plus, chacun limité par timeoutMs.
    1. Tentativele brief, plus le dernier échec
      • → Agent : perform(context)
    2. Vérificationnpm test doit sortir avec 0, puis l’arbre doit être propre ; les 20 000 derniers caractères de sortie deviennent le retour
      • → Commandes : sandbox.command()
    (workflow)
  • SandboxReste ouverte d’un tour à l’autre : dépendances et modifications sont conservées.
    1. Commandesnpm ci une fois, avant le premier tour ; npm test à chaque tour
    2. Agentmodifie et commite ; ses tokens comptent dans budget
    (sandbox)
    • → Branche : commits
  • Brancheoutpost/fix-ci dans .outpost/workspaces ; await using ferme la sandbox et la conserve. Rien n’est fusionné ni poussé. (hôte)

perform(context) relie l’usage et l’annulation de l’agent au workflow : ses tokens comptent dans budget. timeoutMs borne chaque tour.

Issueresult.statusresult.errorsCode de sortie
Une vérification accepte le tour"done"Vide0
La quatrième vérification refuse"failed"LoopTaskExhausted, avec feedback1
Les tokens ou les tentatives s’épuisent"failed"WorkflowBudgetExceeded1
L’agent ou une commande lève une erreur"failed"L’erreur levée1

result.unwrap() lève une WorkflowFailure pour toutes les lignes sauf la première. Une limite de tokens arrête aussi l’agent en cours.

VarianteChangement
Un autre agentPassez un agent de secours comme agent : createFallbackAgent([claude, codex], { on: ["quota", "unavailable"] }) change d’agent sur une limite d’usage ou une panne.
L’exécuter en CIUtilisez un identifiant pour l’exécution sans surveillance, nommez la branche par exécution (`outpost/fix-ci-${process.env.GITHUB_RUN_ID}`), puis git push origin cette branche.
Vérification plus stricteAjoutez npm run lint ou une vérification de types après les tests, ou demandez l’avis d’un agent relecteur depuis check (Boucles de vérification).
Sandbox cloudRemplacez sandboxProvider par un fournisseur Vercel ou Daytona. Les commits reviennent sur la branche de l’hôte.

API : createSandbox · defineLoopTask · defineAgentTask · WorkflowBudget · LoopTaskExhausted · WorkflowFailure.