Adversarial pre-PR reviewer that bolts onto OpenAI Symphony's WORKFLOW.md state machine. Anvil inserts one new Linear state, Adversarial Review, between Symphony's In Progress and Human Review. Symphony's builder finishes, transitions the issue to Adversarial Review, anvil polls, runs a cross-model audit on the diff, and either moves the issue forward to Human Review (pass) or back to Rework with findings (fail). Verdicts append to the same ## Codex Workpad comment Symphony's worker is already using, so the ticket reads as one unbroken thread instead of fragmenting across separate comments.
Symphony orchestrates Codex agents through a documented Linear state machine: Todo -> In Progress -> Human Review -> Merging -> Done (with Rework as the bounce-back). Symphony is the elixir reference implementation at https://github.com/openai/symphony. Anvil does not modify Symphony's code. It runs as a separate Rust daemon, polls Linear independently, only acts on the new Adversarial Review state, and uses a different model from the one the builder used. The persona is a markdown file - swap models / prompts / providers without touching the binary.
- Install elixir + mise (Symphony's prerequisites) and clone Symphony:
git clone https://github.com/openai/symphony ~/projects/symphony-upstream cd ~/projects/symphony-upstream/elixir mise trust && mise exec -- mix deps.get
- Install anvil:
git clone https://github.com/shawnpetros/anvil ~/projects/anvil cd ~/projects/anvil cargo install --path .
- Copy the contents of
WORKFLOW.md.fragment.mdinto your project's SymphonyWORKFLOW.md(instructions in the fragment file). - Add the
Adversarial Reviewworkflow state to each Linear team anvil should review. - Set the
LINEAR_TOKENenv var to your Linear personal API key. Anvil refuses to load if it sees a real token in the config file. - Write
~/.config/anvil/config.toml:anvil init $EDITOR ~/.config/anvil/config.toml
- Verify:
anvil check
- Run:
anvil run
Run: claude -p "Bootstrap this repo for Anvil. Read README at https://github.com/shawnpetros/anvil and follow the setup."
Anvil ships with one persona at templates/reviewer.md. A persona is a markdown file with YAML frontmatter:
---
name: anvil-reviewer
description: Adversarial code reviewer running pre-PR audit on Symphony-built diffs
agent_command: claude # or "codex"
model_hint: sonnet # opus | sonnet | haiku, optional
---
You are reviewing {{identifier}}: {{title}}.
The diff:
{{diff}}
[... your prompt body ...]Available variables: {{identifier}}, {{title}}, {{description}}, {{branch}}, {{workspace_path}}, {{diff}}. Unknown placeholders are left in the rendered prompt verbatim so typos surface visibly.
The agent's job is to write REVIEW.md to the workspace and exit. Anvil owns every Linear write (state moves, comments, labels). The reviewer.md template documents the structured output contract.
New personas are just markdown files. Drop one in templates/, point review.persona_path at it. Same shape as templates/reviewer.md. Future personas (Penny meeting-attender, Argyle code-reviewer, anything else) follow the same pattern: frontmatter for the agent_command and model, body for the prompt.
loop {
issues = linear.fetch_issues_in_state(teams, "Adversarial Review")
for issue in issues {
workspace = workspace_root / sanitize(issue.identifier)
diff = git_diff_against_main(workspace, issue.branch_name)
prompt = persona.render({ identifier, title, description, diff, branch, workspace_path })
output = spawn_subprocess(persona.agent_command, prompt) // claude -p / codex exec
review = parse(workspace / "REVIEW.md")
match review.status {
Pass => transition(issue, "Human Review"); append_workpad("Adversarial Review", "PASS", notes)
Fail => transition(issue, "Rework"); append_workpad("Adversarial Review", "FAIL", findings)
None => leave_state_alone(); append_workpad("Adversarial Review", "BLOCKED", "REVIEW.md missing")
}
}
sleep(poll_interval_seconds)
}
State transitions and comments are anvil's responsibility. The reviewer subprocess does not write to Linear. The Linear API token lives only in the env var named by tracker.api_key_env; the config file refuses to store a real one.
If you'd rather not maintain Symphony plus Anvil as two separate processes, there's Smithy. A fork of Symphony with Anvil's adversarial-review state baked in, plus dual agent runtimes (Codex + Claude Code), Linear OAuth identity, label-gated autonomous merge, model-summarized run logs, and a max-retry circuit breaker. One process, all the opinions.
Anvil stays as the option for users who want vanilla Symphony plus adversarial review without forking.
MIT.
Shawn Petros (shawn.petros@gmail.com).