orchestratr
patterns

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:

  • LENSES is 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.all over LENSES. every verifier is a fresh orcr.ask on 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, then worker.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

failure and cleanup

  • { killOnThrow: true } on the scope barrier-kills harden/** 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" (via ask), so the panel self-cleans every round; only the worker needs an explicit kill().

run it

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

with 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.

On this page