orchestratr
patterns

fix-until-green

a worker agent fixes build errors while a verifier on a different provider judges the result, looping until the verifier says PASS.

fetch the compiler errors, fix them with one agent, verify with a different provider, and repeat until the verifier says PASS. this is the goal-style loop: a long-lived worker you keep steering, and a fresh independent reviewer each round.

the scenario

you have a repository that does not compile. you want an agent to fix it, but you do not want that same agent to certify its own work. so the loop has two roles:

  • a fixer that stays alive across iterations (gc: "never"), holding its context so each send builds on the last.
  • a verifier spawned fresh each green build, on a different provider, that reviews the uncommitted changes and replies PASS or FAIL: <reason>. its objection feeds back to the fixer.

the loop is bounded to 10 iterations so a stubborn build cannot run forever.

the annotated recipe

import { orcr } from "@orchestratr/sdk";
import { execSync } from "node:child_process";

const build = () => {
  try { execSync("npx tsc --noEmit", { stdio: "pipe" }); return { ok: true, errors: "" }; }
  catch (e: any) { return { ok: false, errors: String(e.stdout) }; }
};

await orcr.scope("fix_build", async () => {
  const fixer = await orcr.agent.run({
    agent: "claude", name: "fixer", gc: "never", cwd: process.cwd(),
    prompt: "You fix TypeScript build errors in this repo. Wait for my input.",
  });

  for (let iter = 1; iter <= 10; iter++) {
    const { ok, errors } = build();
    if (ok) {
      // independent eyes: a codex verifier judges the changes, not the author
      const verdict = await orcr.ask({
        agent: "codex", path: `verify/iter_${iter}`,
        prompt: `The build is green. Review the uncommitted changes in ${process.cwd()}
                 for correctness and unintended edits. Reply exactly PASS or FAIL: <reason>.`,
      });
      if (verdict.trim().startsWith("PASS")) break;
      await fixer.send(`A reviewer rejected the changes: ${verdict}. Address this.`);
    } else {
      await fixer.send(`Build errors (iteration ${iter}):\n${errors}\nFix all of them.`);
    }
    await fixer.wait();
  }
  await fixer.kill();
}, { killOnThrow: true });   // any crash cleans up the whole subtree

what each piece is doing:

  • orcr.scope("fix_build", ...) roots everything under fix_build. the fixer lands at fix_build/fixer; each verifier at fix_build/verify/iter_1, fix_build/verify/iter_2, and so on.
  • gc: "never" keeps the fixer alive between iterations so send steers the same agent instead of a new one. you clean it up explicitly with kill().
  • the build check is your code, not orcr's. orcr never infers success from output; you run tsc, read the exit status, and decide.
  • orcr.ask(...) for the verifier is a one-liner: it spawns a gc: "immediate" agent, waits for the answer, returns the final response as a string, and closes the pane. a new verifier each iteration means no stale context.
  • agent: "codex" for the verifier deliberately differs from the fixer's agent: "claude". the reviewer is not grading its own work.
  • the verdict is parsed with verdict.trim().startsWith("PASS"). on FAIL, the objection is fed back to the fixer verbatim, then fixer.wait() blocks until the fixer's turn completes before the next build check.

prime the worker first

in the CI fixture the recipe adds await fixer.wait() right after spawning the fixer, before the loop, so the fixer finishes processing its priming prompt before the first send steers it. do the same for any long-lived worker you spawn with a setup prompt and then steer.

primitives it exercises

  • orcr.scope to root and namespace the whole workflow.
  • orcr.agent.run with gc: "never" for the persistent worker.
  • agent.send to steer the worker with each round's feedback.
  • agent.wait to block on the worker's turn completing.
  • orcr.ask for the fresh verifier each round.

failure and cleanup

two mechanisms keep this from leaking agents:

  • { killOnThrow: true } on the scope. if any await inside throws (a build command that never returns, a Blocked verifier that needs a human, a bug in your code), the scope performs a barrier kill of fix_build/**, cancelling in-flight spawns and killing every agent under the root before the error propagates. see agent lifecycle and GC and scopes, lineage & fan-out.
  • the bounded for loop. always give an unattended loop a numeric ceiling; here it is ten iterations. a build that never goes green exits the loop and hits fixer.kill() regardless.

if the verifier comes back blocked (a usage limit, a login prompt), orcr.ask throws Blocked, which killOnThrow catches and cleans up. see error codes.

run it

orcr scaffold fix-until-green && cd fix-until-green
# paste the recipe into workflow.ts, replacing the generated example
npx tsx workflow.ts

both providers need their integrations installed on both layers (herdr integration install claude and herdr integration install codex); otherwise the run fails fast with integration_missing. see providers & integrations.

next: adversarial verification generalizes the single verifier into N lenses that must reach a majority.

On this page