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 eachsendbuilds on the last. - a verifier spawned fresh each green build, on a different provider, that reviews the uncommitted changes and replies
PASSorFAIL: <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 subtreewhat each piece is doing:
orcr.scope("fix_build", ...)roots everything underfix_build. the fixer lands atfix_build/fixer; each verifier atfix_build/verify/iter_1,fix_build/verify/iter_2, and so on.gc: "never"keeps the fixer alive between iterations sosendsteers the same agent instead of a new one. you clean it up explicitly withkill().- 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 agc: "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'sagent: "claude". the reviewer is not grading its own work.- the verdict is parsed with
verdict.trim().startsWith("PASS"). onFAIL, the objection is fed back to the fixer verbatim, thenfixer.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.scopeto root and namespace the whole workflow.orcr.agent.runwithgc: "never"for the persistent worker.agent.sendto steer the worker with each round's feedback.agent.waitto block on the worker's turn completing.orcr.askfor the fresh verifier each round.
failure and cleanup
two mechanisms keep this from leaking agents:
{ killOnThrow: true }on the scope. if anyawaitinside throws (a build command that never returns, aBlockedverifier that needs a human, a bug in your code), the scope performs a barrier kill offix_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
forloop. always give an unattended loop a numeric ceiling; here it is ten iterations. a build that never goes green exits the loop and hitsfixer.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.tsboth 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.