adversarial verification
a worker produces; N verifiers with different lenses try to reject it in parallel; objections loop back until a majority passes.
generalize the single verifier from fix-until-green into a panel. a worker produces; N verifiers, each with a different lens, try hard to reject it; their objections feed back to the worker; the round repeats until a majority passes. this is the phased shape with parallel review inside each phase.
the scenario
you want more than one kind of scrutiny on the same work: correctness, security, edge cases. each concern is a separate reviewer with its own lens, told to find a real problem. the work is accepted only when a majority of lenses pass. failures from the minority feed back so the worker can address them, and the panel re-reviews.
the annotated recipe
import { orcr } from "@orchestratr/sdk";
const LENSES = ["correctness", "security", "edge cases and error handling"];
await orcr.scope("harden", async () => {
const worker = await orcr.agent.run({
agent: "claude", name: "worker", gc: "never", cwd: process.cwd(),
prompt: "Implement the task in TASK.md. Say DONE when finished.",
});
await worker.wait();
for (let round = 1; round <= 5; round++) {
const verdicts = await Promise.all(LENSES.map((lens, i) =>
orcr.ask({
agent: "codex", path: `verify/round_${round}/lens_${i}`,
prompt: `Adversarially review the uncommitted changes in ${process.cwd()}
through the lens of ${lens}. Try hard to find a real problem.
Reply PASS, or FAIL: <the single most important problem>.`,
})));
const failures = verdicts.filter(v => !v.trim().startsWith("PASS"));
if (failures.length <= LENSES.length / 2) break; // majority passed
await worker.send(`Reviewers rejected the work:\n${failures.join("\n")}\nFix these.`);
await worker.wait();
}
await worker.kill();
}, { killOnThrow: true });what each piece is doing:
LENSESis the array of concerns. adding a lens is one string; the rest of the loop adapts.- the worker is long-lived (
gc: "never"), spawned once, and steered across rounds.await worker.wait()after the spawn lets it finish the initial implementation before the first review. - each round fans the panel out with
Promise.alloverLENSES. every verifier is a freshorcr.askon a different provider than the worker. - the per-round path is the bookkeeping.
verify/round_${round}/lens_${i}gives every reviewer a unique, human-readable path:harden/verify/round_1/lens_0,harden/verify/round_2/lens_1, and so on. rounds never collide because the round number is a path segment, so the panel can re-run with the same lenses each round. - the majority test is
failures.length <= LENSES.length / 2. with three lenses, the work passes when at most one fails (1 <= 1.5). the minority's objections are sent to the worker verbatim, thenworker.wait()blocks until the worker's fix turn completes before the next round.
different lenses, one prompt template
each verifier gets the same instructions with its lens substituted in, and is told to "try hard to find a real problem." distinct lenses on distinct providers is how you get genuinely independent objections rather than three copies of the same review.
primitives it exercises
orcr.scopeto root the worker and every round's panel.orcr.agent.runwithgc: "never"for the persistent worker.agent.sendandagent.waitto steer and join on the worker.- parallel
orcr.askfor the lens panel each round.
failure and cleanup
{ killOnThrow: true }on the scope barrier-killsharden/**if any await throws, so a blocked verifier or a bug never leaves the worker or a half-finished panel running. see scopes, lineage & fan-out.- rounds are bounded to 5. a panel that can never reach a majority still exits the loop and hits
worker.kill(). like every loop pattern, the stop condition carries a number, never "until it passes." - each verifier is
gc: "immediate"(viaask), so the panel self-cleans every round; only the worker needs an explicitkill().
run it
orcr scaffold harden-workflow && cd harden-workflow
# paste the recipe into workflow.ts, replacing the generated example
npx tsx workflow.tswith three lenses per round and a live worker, one round can have up to four agents in flight. keep an eye on the concurrency caps if you add many lenses; anything over a free slot queues rather than overloads the machine.
next: generate-and-filter inverts this, fanning many producers and judging once.