Observed since 20 Oct, 18:39 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
Actor-critic loop for AI coding agents — orchestrate actors and critics via CLI backends
Spike — this is a learning experiment, not production software. The goal is to explore the actor-critic pattern, understand how it can refine output quality, and build practical intuition for using it effectively.
This CLI tool orchestrates an actor and multiple specialised critics by invoking the Claude Code executable as a subprocess. Each role has its own dedicated Claude configuration. Communication uses structured output.
The right orchestration strategy for multiple critics and the best stopping conditions (fixed rounds, critic green-light, consensus, etc.) are open questions this spike aims to answer.
Install from PyPI:
pip install actor-critic-loop
# or with uv
uv add actor-critic-loop
This installs the actor-critic CLI command. You still need at least one supported coding agent CLI (see below).
claude login)npm i -g @openai/codexnpm i -g @mariozechner/pi-coding-agentgit clone https://github.com/ondrasek/spike-claude-code-actor-critics.git
cd spike-claude-code-actor-critics
uv sync
uv run actor-critic-loop init ./my-task \
--actor actor_critic.actors.basic \
--critic correctness=actor_critic.critics.basic \
--critic style=actor_critic.critics.basic \
--orchestrator actor_critic.orchestrators.sequential \
--stop-condition actor_critic.stop_conditions.fixed_rounds
This scaffolds a workspace at ./my-task with dedicated folders for the actor and each critic, containing their Claude Code configurations (CLAUDE.md, settings, skills, subagents). Edit actor-critic.yaml and role configs after init to tune behaviour.
Use --template to clone an existing workspace (e.g. one of the examples) as a starting point:
# Clone an example as your starting point
uv run actor-critic-loop init ./my-task --template ./examples/hello-world/template
# Override specific values while using the template structure
uv run actor-critic-loop init ./my-task --template ./examples/code-review/template --max-rounds 5
# Quick scaffold with defaults (no template needed)
uv run actor-critic-loop init ./my-task
When using --template, the --actor and --critic flags are not allowed (the template defines those). You can still override --orchestrator, --stop-condition, and --max-rounds.
Use wizard to scaffold a workspace and immediately launch Claude Code for interactive setup:
uv run actor-critic-loop wizard ./my-task
The wizard creates the workspace structure, installs the /customize-workspace skill, and opens Claude Code. Claude guides you through configuring the actor, designing critics, writing prompts, and installing plugins — all interactively. If the workspace already exists, the wizard skips scaffolding and launches Claude Code directly.
Place source material in the workspace's input/ directory, then run:
# Basic run (reads everything from actor-critic.yaml)
uv run actor-critic-loop run --workspace ./my-task
# With additional prompt (appended to input/prompt.md)
uv run actor-critic-loop run --workspace ./my-task --prompt "Focus on clarity"
# With verbose output (INFO logging)
uv run actor-critic-loop run --workspace ./my-task --verbose
All configuration — actor, critics, orchestrator, stop condition, and max rounds — lives in actor-critic.yaml at the workspace root.
| Flag | Short | Default | Description |
|---|---|---|---|
--workspace | (required) | Path to workspace directory (must contain actor-critic.yaml) | |
--prompt | Additional prompt (appended to input/prompt.md) | ||
--verbose | -v | Enable INFO-level logging | |
--backend | Override backend for all roles (e.g. claude, codex, opencode, pi) |
Use --verbose when debugging issues. Claude Code is always invoked with streaming mode (--output-format stream-json --verbose), which provides real-time diagnostics.
What verbose mode shows:
All I/O uses workspace directory conventions — no external path flags:
| Directory | Purpose |
|---|---|
<workspace>/input/ | Source material + optional prompt.md (user-provided) |
<workspace>/output/ | Final output (written by orchestrator after last round) |
<workspace>/actor/output/ | Actor's working output area (intermediate, per-round) |
<workspace>/critics/<name>/output/ | Critic's optional output artifacts (e.g. test files, diffs) |
| Code | Meaning |
|---|---|
| 0 | Final verdict is PASS (all critics satisfied) |
| 1 | Final verdict is FAIL (max rounds exhausted without satisfying stop condition) |
| 2 | Config, resolver, or runtime error (malformed YAML, missing directories, Claude Code subprocess failure) |
| 130 | Interrupted by user (Ctrl+C) |
After a run, the CLI displays:
[reconciled: reason])PASS* (yellow) indicates a verdict that was reconciled via re-evaluation. This occurs when the orchestrator detects oscillation (a critic cycling between PASS and FAIL) or goalpost-shifting (consecutive FAILs with different issues each round). The suspect critic is re-invoked with its own verdict history and asked to resolve the contradiction.
Note: The exact CLI interface will evolve as the spike progresses.
The actor-critic loop supports multiple coding agent CLIs as backends. Each role (actor, critic) can use a different backend:
# actor-critic.yaml
actor:
impl: actor_critic.actors.basic
backend: codex # Use Codex for the actor
critics:
- name: quality
impl: actor_critic.critics.basic
backend: claude # Use Claude for the critic
Supported backends:
| Backend | CLI | --add-dir | JSON schema | Thinking control |
|---|---|---|---|---|
claude (default) | claude | Yes | Yes (native) | Yes |
codex | codex | Yes | Yes (native) | No |
opencode | opencode | No (warning) | No (warning) | No |
pi | pi | No (warning) | No (warning) | No |
Override all backends from the CLI: --backend codex
Features not supported by a backend degrade gracefully with logged warnings.
--output-format json, stream-json, --json-schema, or plain text)actor-critic.yaml should be understandable without reading the source code; use fully qualified names over short aliasesRole workspaces can be enhanced with Claude Code capabilities in priority order:
.claude/rules/ (always use output.md to constrain writes).claude/skills/ for specific workflows# Install plugins to actor workspace (project scope)
cd my-task/actor
claude plugin install pyright-lsp --scope project
claude plugin install code-simplifier --scope project
# Install plugins to critic workspace
cd my-task/critics/correctness
claude plugin install code-review --scope project
claude plugin install security-guidance --scope project
| Role Type | Recommended Plugins |
|---|---|
| Python actors | pyright-lsp, code-simplifier |
| TypeScript actors | typescript-lsp, code-simplifier |
| Code critics | code-review, security-guidance |
| Style critics | pr-review-toolkit |
Skills provide custom slash commands:
.claude/skills/
└── generate/
└── SKILL.md # Instructions for /generate command
Rules provide auto-loaded guidance:
.claude/rules/
├── output.md # Required: constrain writes to ./output/
└── style.md # Optional: code style guidelines
See plans/PLUGINS-SKILLS-RULES.md for complete documentation.
Each actor and critic is invoked as a Claude Code subprocess in non-interactive mode (claude -p). The orchestrator runs Claude Code from the role's workspace directory so it picks up the role's CLAUDE.md, settings, skills, and subagents. Roles access shared directories via --add-dir.
Actor invocation:
cd ./my-task/actor && claude -p "the prompt and critic feedback" \
--add-dir ../input \
--add-dir ../critics/correctness/output \
--add-dir ../critics/style/output
Critic invocation:
cd ./my-task/critics/correctness && claude -p "<rendered prompt>" \
--add-dir ../../input --add-dir ../../actor/output
Critic prompts are Jinja2 templates stored in critics/<name>/input/prompt.md. At runtime, the template is rendered with context about the actor's work:
| Variable | Type | Description |
|---|---|---|
actor_output_files | list[str] | Filenames in actor/output/ (excluding .gitkeep) |
actor_prompt_file | str | Absolute resolved path to the actor's input/prompt.md |
actor_prompt | str | Verbatim contents of the actor's input/prompt.md (empty string if missing) |
actor_output_text | str | Actor's stdout text from Claude Code (empty string on first round) |
critic_output_dirs | dict[str, str] | Mapping of critic name to absolute output dir path |
round_num | int | Current round number (1-indexed) |
previous_actor_outputs | list[str] | Actor stdout text from all previous rounds (empty on round 1) |
previous_output_dirs | list[str] | Absolute paths to snapshots/actor/round-{N}/ directories |
Templates use XML sections following Anthropic prompt engineering guidelines (data/context first, instructions in middle, reminder at end):
<actor_task>
The original requirements given to the actor are in `{{ actor_prompt_file }}`.
</actor_task>
<actor_output>
The actor produced the following files:
{% for file in actor_output_files %}
- `{{ file }}`
{% endfor %}
</actor_output>
<review_instructions>
[critic-specific evaluation criteria]
</review_instructions>
<reminder>
- Ground your review in specific evidence.
- Your response MUST be valid JSON with `review`, `passed`, and optionally `issues`.
</reminder>
Every critic must provide its own template — there is no default fallback.
The actor writes its working output to <workspace>/actor/output/. Critics review it there via --add-dir and may produce optional artifacts in their own output/ directories. The actor can access critic artifacts via --add-dir to each critic's output/. After the final round, the orchestrator copies the actor's output to <workspace>/output/.
Each role is constrained to write only to its own output/ subdirectory via permissions in settings.json and a rule in .claude/rules/output.md.
Output format, schema enforcement, and response parsing are implementation-specific — each actor/critic implementation decides which Claude Code flags to use.
Whether a role resumes its Claude Code session across rounds or starts fresh is configurable. Both approaches have trade-offs:
| Strategy | Benefits | Risks |
|---|---|---|
| resume | Full history of prior attempts; no need to re-pass context | Past feedback may anchor or bias; context window fills over many rounds |
| fresh | Clean slate each round; predictable context size | Must re-pass everything explicitly; may repeat failed approaches |
The right strategy depends on the role and the task — this is one of the things the spike aims to explore empirically. Session strategy is configured per role in actor-critic.yaml.
The most significant failure mode in actor-critic loops is critic oscillation — where fixing one critic's feedback breaks another's requirements, or a single critic cycles between PASS and FAIL across rounds. The orchestrator includes automatic oscillation detection and reconciliation:
Two triggers:
| Trigger | Default threshold | What it detects |
|---|---|---|
| Verdict reversal | 2 reversals | Critic cycling PASS→FAIL→PASS→FAIL |
| Consecutive fails | 3 rounds | Same critic failing with different issues each round (goalpost-shifting) |
How reconciliation works:
PASS* (reconciled)Configuration (in actor-critic.yaml):
stop_condition:
config:
max_rounds: 5
max_consecutive_fails: 4 # default: 3
oscillation_reversals: 3 # default: 2
Reconciliation adds zero overhead on clean runs — detection is a pure function over round summaries. Only triggered critics incur an extra invocation.
├── src/
│ └── actor_critic/
│ ├── __init__.py
│ ├── cli.py
│ ├── types.py
│ ├── claude/ # Shared Claude Code invocation
│ │ ├── __init__.py
│ │ ├── invoke.py # Core invoke() function
│ │ ├── events.py # NDJSON event parsing
│ │ └── types.py # ClaudeResult, ClaudeEvent
│ ├── config.py # YAML config loading
│ ├── resolver.py # Dynamic module import
│ ├── scaffold.py # Workspace scaffolding (init command)
│ ├── scaffold_templates/ # Built-in templates for init
│ ├── backends/ # Coding agent CLI backends
│ │ ├── base.py # BackendResult, BackendEvent
│ │ ├── resolve.py # Name → module resolver
│ │ ├── claude.py # Claude Code (wraps claude/invoke.py)
│ │ ├── codex.py # OpenAI Codex CLI
│ │ ├── opencode.py # OpenCode CLI
│ │ └── pi.py # pi-coding-agent CLI
│ ├── actors/
│ │ ├── __init__.py
│ │ ├── basic.py
│ │ └── team.py # Agent teams (parallel work)
│ ├── critics/
│ │ ├── __init__.py
│ │ └── basic.py
│ ├── orchestrators/
│ │ ├── __init__.py
│ │ ├── sequential.py
│ │ ├── oscillation.py # Oscillation detection + reconciliation
│ │ └── feedback.py # Critic feedback serialization
│ └── stop_conditions/
│ ├── __init__.py
│ ├── all_pass.py # Stop when all critics pass
│ ├── any_pass.py # Stop when any critic passes
│ ├── fixed_rounds.py # Stop at max rounds
│ └── quorum.py # Stop when N critics pass
├── tests/
│ ├── system/
│ ├── integration/
│ └── unit/
├── pyproject.toml
└── README.md
Note: The structure above represents the planned/target layout for this spike and not all directories or files may exist yet in the repository.
A workspace is created by actor-critic-loop init and contains the full Claude Code configuration for each role:
my-task/
├── actor-critic.yaml
├── input/ # Source material (user-provided before run)
├── output/ # Final output (written by orchestrator)
├── snapshots/ # Created at runtime — actor output history
│ └── actor/
│ ├── round-1/ # Copy of actor/output/ after round 1
│ └── round-2/ # Copy of actor/output/ after round 2
├── actor/
│ ├── output/ # Actor's working output (intermediate, per-round)
│ ├── CLAUDE.md
│ └── .claude/
│ ├── settings.json # Permissions: writes to ./output/ only
│ ├── rules/
│ │ └── output.md # "Write all output files to ./output/"
│ ├── agents/
│ │ └── researcher.md
│ ├── commands/
│ │ └── generate.md
│ └── skills/
│ └── code-generation/
│ └── SKILL.md
└── critics/
├── correctness/
│ ├── input/
│ │ └── prompt.md # Jinja2 template — critic review prompt
│ ├── output/ # Critic's optional output artifacts
│ ├── CLAUDE.md
│ └── .claude/
│ ├── settings.json
│ ├── rules/
│ │ └── output.md
│ ├── agents/
│ │ └── test-verifier.md
│ └── skills/
│ └── verify-logic/
│ └── SKILL.md
└── style/
├── input/
│ └── prompt.md # Jinja2 template — critic review prompt
├── output/
├── CLAUDE.md
└── .claude/
├── settings.json
├── rules/
│ └── output.md
└── skills/
└── check-style/
└── SKILL.md
actor-critic.yamlTop-level orchestration configuration that ties the workspace together:
actor:
path: ./actor
impl: actor_critic.actors.basic
critics:
- name: correctness
path: ./critics/correctness
impl: actor_critic.critics.basic
- name: style
path: ./critics/style
impl: actor_critic.critics.basic
orchestrator:
impl: actor_critic.orchestrators.sequential
stop_condition:
impl: actor_critic.stop_conditions.all_pass
config:
max_rounds: 3
max_consecutive_fails: 3 # optional: trigger reconciliation after N consecutive FAILs
oscillation_reversals: 2 # optional: trigger reconciliation after N verdict reversals
Tests are run with pytest and organised into:
# Run all tests with coverage
uv run pytest --cov
This project uses Claude Code hooks to enforce code quality standards. When Claude completes work, a quality gate automatically runs and feeds any failures back to Claude for fixing, creating an iterative loop until all checks pass.
┌─────────────────────────────────────────────────────────────────┐
│ ITERATIVE FEEDBACK LOOP │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Claude completes work │
│ │ │
│ ▼ │
│ Stop event fires ──► Quality gate hook runs │
│ │ │
│ ├── Any check fails ──► Exit 2 ──► Claude fixes ──┐ │
│ │ │ │
│ ▼ │ │
│ All checks pass ──► Auto-commit hook runs │ │
│ │ │ │
│ ├── Pre-commit fails ──► Exit 2 ──► Claude fixes ─┤ │
│ │ │ │
│ ▼ │ │
│ Commit + push ──► Session ends cleanly │ │
│ │ │
│ ◄─────────────── Stop event fires again ──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
The hooks are configured in .claude/settings.json and are mandatory for all developers. Exit code 2 feeds stderr back to Claude, prompting automatic fixes.
| Tool | Purpose | Rationale |
|---|---|---|
| pytest | Test execution (fail-fast) | Core TDD requirement |
| pytest-cov | 80% coverage threshold | Enforces test completeness |
| ruff check | Linting | Fast, replaces flake8/pylint |
| ruff format | Formatting verification | Fast, replaces black/isort |
| pyright | Static type checking | Faster than mypy, stricter |
| bandit | Security scanning (medium+ severity) | Catches OWASP vulnerabilities |
| vulture | Dead code detection (80% confidence) | Keeps codebase clean |
Developers can temporarily adjust checks without modifying shared configuration:
| Variable | Default | Purpose |
|---|---|---|
COVERAGE_THRESHOLD | 80 | Minimum coverage percentage |
SKIP_TYPE_CHECK | unset | Set to 1 to skip pyright |
SKIP_SECURITY_SCAN | unset | Set to 1 to skip bandit |
SKIP_DEAD_CODE | unset | Set to 1 to skip vulture |
# Skip slow checks during rapid iteration
SKIP_TYPE_CHECK=1 SKIP_SECURITY_SCAN=1 claude
# Lower coverage threshold temporarily
COVERAGE_THRESHOLD=60 claude
# Run the full quality gate
./.claude/hooks/quality-gate.sh
# Run individual checks
uv run pytest -x --tb=short
uv run ruff check src/ tests/
uv run ruff format --check src/ tests/
uv run pyright src/
uv run bandit -r src/ -ll
uv run vulture src/ --min-confidence 80
PreToolUse (before every Write/Edit/MultiEdit):
tdd-gate.sh — blocks implementation writes without a failing test (60s timeout)PostToolUse (after every Write/Edit/MultiEdit):
per-edit-fix.sh — ruff fix + format + codespell (60s timeout)post-edit-tests.sh — runs pytest for immediate feedback (120s timeout)Stop (when Claude finishes):
quality-gate.sh — full test suite + lint + types (10 min timeout)auto-commit.sh — commit + push if clean (4 min timeout)If any hook returns exit 2, Claude receives stderr and fixes issues. The loop continues until all checks pass.
tdd-gate.sh allows test files)post-edit-tests.sh reports the new test fails (expected)tdd-gate.sh allows (failing test exists)post-edit-tests.sh reports pass/fail immediately| Variable | Default | Purpose |
|---|---|---|
SKIP_TDD_GATE | unset | Set to 1 to disable test-first enforcement |
SKIP_POST_EDIT_TESTS | unset | Set to 1 to disable post-edit test runner |
# Disable TDD gate during non-code tasks
SKIP_TDD_GATE=1 claude
# Disable post-edit tests during rapid iteration
SKIP_POST_EDIT_TESTS=1 claude
# Disable both for non-TDD workflows
SKIP_TDD_GATE=1 SKIP_POST_EDIT_TESTS=1 claude
See docs/FUTURE.md.
Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.2.0 | 22 Feb 2026 | 3 |
This describes our collected data; it is not a security assessment.
Shared repository data · GitHub
Observed since 20 Oct, 18:39 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:ecf835151e9952f860ca4f0da43aafb4908df798d31af9c0f77ffd6a7403fc62