Introduction
Codeloops is a command-line tool that orchestrates an actor-critic feedback loop for AI coding agents. It runs your preferred coding agent (Claude Code, OpenCode, or Cursor) as the "actor" to execute tasks, then uses another agent as the "critic" to evaluate the work and provide feedback. This loop continues until the task is complete or a maximum iteration count is reached.
The Problem
AI coding agents are powerful, but they lack a built-in mechanism for self-correction. When an agent makes a mistake or produces incomplete work, it has no way to know unless a human reviews the output. This leads to:
- Incomplete implementations that miss edge cases
- Code that doesn't fully address the original requirements
- Bugs that could have been caught with a second look
How Codeloops Solves It
Codeloops introduces a feedback loop where:
- The actor (a coding agent) executes your task
- Git captures the changes made
- The critic (another agent instance) evaluates the output against your original prompt
- If the work is incomplete, the critic provides feedback and the actor tries again
- The loop continues until the critic approves or max iterations are reached
┌─────────────────────────────────────────────────────────────┐
│ │
│ prompt.md ──▶ Actor ──▶ Git Diff ──▶ Critic ──▶ Done? │
│ ▲ │ │
│ │ feedback │ │
│ └────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Key Benefits
- Self-correcting loop: Mistakes are caught and fixed automatically
- Improved task completion: Multiple iterations ensure requirements are fully met
- Full observability: Every iteration is logged to a session file for review
- Agent flexibility: Mix and match agents for actor and critic roles
- Simple interface: Just write a
prompt.mdfile and runcodeloops
Who Should Use This
Codeloops is for developers who:
- Use AI coding agents for development tasks
- Want higher quality output from their AI tools
- Need a way to review and analyze AI agent behavior
- Want to experiment with different agent configurations
Next Steps
Ready to get started? Head to the Installation guide to set up codeloops, then follow the Quickstart to run your first session.
Installation
This guide covers how to install codeloops and its prerequisites.
Prerequisites
Before installing codeloops, ensure you have:
Rust Toolchain
Codeloops is written in Rust. Install the Rust toolchain via rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
At Least One Supported Agent
You need at least one AI coding agent CLI installed:
| Agent | Installation |
|---|---|
| Claude Code | claude.ai/code |
| OpenCode | opencode.ai/docs |
| Cursor | cursor.com/cli |
The agent binary must be in your PATH. Verify with:
# Check which agents are available
which claude
which opencode
which cursor-agent # or 'agent'
Git
Git is required for capturing diffs between iterations:
git --version
Building from Source
Clone the repository and build:
git clone https://github.com/silvabyte/codeloops
cd codeloops
cargo build --release
The binary will be at ./target/release/codeloops.
Adding to PATH
Option 1: Create a symlink:
sudo ln -s $(pwd)/target/release/codeloops /usr/local/bin/codeloops
Option 2: Copy the binary:
sudo cp ./target/release/codeloops /usr/local/bin/
Option 3: Add the target directory to your PATH in ~/.bashrc or ~/.zshrc:
export PATH="$PATH:/path/to/codeloops/target/release"
Verifying Installation
Check that codeloops is installed correctly:
codeloops --version
You should see output like:
codeloops 0.1.0
First-Time Setup
Run the interactive setup to configure your default agent:
codeloops init
This creates a global configuration file at ~/.config/codeloops/config.toml with your preferred defaults.
Installing the Web UI (Optional)
The web UI is included when you build from source. To install it for standalone use:
cd ui
bun install
bun run build
The built UI will be in ui/dist/. See Web UI Overview for usage.
Next Steps
With codeloops installed, proceed to the Quickstart guide to run your first session.
Quickstart
This guide walks you through running codeloops for the first time.
Step 1: Initialize Configuration
If you haven't already, run the interactive setup:
codeloops init
Select your preferred agent (Claude, OpenCode, or Cursor) when prompted.
Step 2: Create a Prompt File
Navigate to a git repository where you want to make changes:
cd /path/to/your/project
Create a prompt.md file describing your task:
Fix the typo in the greeting message. The word "Helo" should be "Hello".
Keep your first prompt simple and specific. Complex tasks work better once you're familiar with how the loop operates.
Step 3: Run Codeloops
Execute the loop:
codeloops
Codeloops will:
- Read
prompt.mdfrom the current directory - Start the actor agent with your prompt
- Capture the git diff after the actor completes
- Run the critic agent to evaluate the changes
- Either finish (if the critic approves) or loop back with feedback
Step 4: Understand the Output
During execution, you'll see:
[codeloops] Starting actor-critic loop
[codeloops] Prompt: Fix the typo in the greeting message...
[codeloops] Working directory: /path/to/your/project
[codeloops] Actor: Claude Code | Critic: Claude Code
[iteration 1]
[actor] Running Claude Code...
[actor] Completed in 12.3s (exit code: 0)
[git] 1 file changed, 1 insertion(+), 1 deletion(-)
[critic] Evaluating changes...
[critic] Decision: DONE
[critic] Summary: The typo has been fixed. "Helo" is now "Hello".
[codeloops] Session complete: success (1 iteration)
Decision Types
The critic returns one of three decisions:
| Decision | Meaning |
|---|---|
| DONE | Task is complete, loop ends |
| CONTINUE | More work needed, actor runs again with feedback |
| ERROR | Something went wrong, actor gets recovery instructions |
Step 5: View the Result
Check what changed:
git diff
Or use the sessions command:
codeloops sessions show
This opens an interactive viewer to browse the session details.
Alternative: Inline Prompt
Instead of a file, you can pass the prompt directly:
codeloops --prompt "Add a --verbose flag to the CLI"
Alternative: Specify Agent
Override the configured agent:
codeloops --agent opencode
Or use different agents for actor and critic:
codeloops --actor-agent opencode --critic-agent claude
Common First-Run Issues
Agent not found
Error: Agent 'claude' not found in PATH
Install the agent or ensure its binary is in your PATH.
No prompt provided
Error: No prompt provided. Create a prompt.md file or use --prompt
Create a prompt.md file or pass --prompt "your task".
Not a git repository
Error: Not a git repository
Initialize git or navigate to an existing repository:
git init
Next Steps
- Read Your First Session for a detailed walkthrough
- See CLI Reference for all available options
- Learn about Configuration to customize behavior
Your First Session
This guide provides a detailed walkthrough of a real codeloops session, explaining each step of the actor-critic loop.
The Task
Let's say you're working on a web application and need to add input validation to a user registration endpoint. The endpoint currently accepts any input without checking for valid email format or password requirements.
Setting Up
Navigate to your project and create a detailed prompt:
cd ~/projects/myapp
Create prompt.md:
Add input validation to the user registration endpoint in src/api/users.rs:
Requirements:
- Email must be a valid email format
- Password must be at least 8 characters
- Password must contain at least one uppercase letter and one number
- Return appropriate error messages for validation failures
The endpoint is the `register` function that handles POST /api/users/register.
Running the Loop
Start codeloops:
codeloops
Iteration 1: Initial Attempt
Actor Phase
The actor receives your prompt and starts working:
[iteration 1]
[actor] Running Claude Code...
The actor examines your codebase, finds src/api/users.rs, and implements validation. After completion:
[actor] Completed in 45.2s (exit code: 0)
[git] 2 files changed, 47 insertions(+), 3 deletions(-)
The actor made changes to 2 files and the git diff is captured.
Critic Phase
The critic receives:
- Your original prompt
- The actor's stdout output
- The git diff showing all changes
The critic evaluates whether the changes meet your requirements:
[critic] Evaluating changes...
[critic] Decision: CONTINUE
[critic] Feedback: The email validation is implemented but the password
validation only checks length. Missing: uppercase letter check
and number requirement. Also, error messages are generic -
please provide specific feedback for each validation rule.
What Happened
The actor's first attempt was incomplete. It added email validation and password length checking, but missed:
- Uppercase letter requirement
- Number requirement
- Specific error messages
The critic identified these gaps and provided feedback.
Iteration 2: Addressing Feedback
Actor Phase (with Feedback)
The actor now receives the original prompt plus the critic's feedback:
[iteration 2]
[actor] Running Claude Code...
The actor reads the feedback and addresses each point:
[actor] Completed in 32.1s (exit code: 0)
[git] 1 file changed, 28 insertions(+), 12 deletions(-)
Critic Phase
The critic re-evaluates:
[critic] Evaluating changes...
[critic] Decision: DONE
[critic] Summary: Input validation is now complete. Email format is
validated using regex. Password validation checks: minimum
8 characters, at least one uppercase letter (A-Z), at least
one digit (0-9). Error messages are specific and helpful.
[critic] Confidence: 0.95
Session Complete
[codeloops] Session complete: success (2 iterations)
[codeloops] Duration: 89.4s
[codeloops] Session saved: 2025-01-27T15-30-45Z_a3f2c1
The loop ends because the critic returned DONE.
Reviewing the Session
Using the CLI
View the session details:
codeloops sessions show 2025-01-27T15-30-45Z_a3f2c1
Or use the interactive picker:
codeloops sessions show
Viewing the Diff
See the cumulative changes across all iterations:
codeloops sessions diff 2025-01-27T15-30-45Z_a3f2c1
Using the Web UI
For a visual interface:
codeloops ui
Navigate to the session to see:
- Iteration timeline
- Each iteration's diff with syntax highlighting
- Critic feedback trail
Understanding the Session File
The session is stored at ~/.local/share/codeloops/sessions/2025-01-27T15-30-45Z_a3f2c1.jsonl:
{"type":"session_start","timestamp":"2025-01-27T15:30:45Z","prompt":"Add input validation...","working_dir":"/home/user/projects/myapp","actor_agent":"Claude Code","critic_agent":"Claude Code"}
{"type":"iteration","iteration_number":1,"actor_output":"...","git_diff":"...","critic_decision":"CONTINUE","feedback":"The email validation is implemented but..."}
{"type":"iteration","iteration_number":2,"actor_output":"...","git_diff":"...","critic_decision":"DONE","feedback":null}
{"type":"session_end","outcome":"success","iterations":2,"summary":"Input validation is now complete...","confidence":0.95,"duration_secs":89.4}
What If It Doesn't Go Well?
Task Takes Too Many Iterations
If the loop continues for many iterations without completing:
- Set a limit: Use
--max-iterations 5to cap the attempts - Cancel and reformulate: Press Ctrl+C and rewrite your prompt with more detail
- Review the feedback: Check what the critic is asking for
Actor Produces Errors
If the actor exits with an error (non-zero exit code):
- The critic will suggest recovery steps
- The actor tries again with recovery guidance
- If errors persist, the session ends as
failed
Critic Always Says CONTINUE
This usually means your prompt is ambiguous. The critic doesn't know when the task is "done" because the requirements aren't clear. Add explicit acceptance criteria to your prompt.
Tips for Success
- Be specific: Include file paths, function names, and exact requirements
- Define "done": What criteria must be met for the task to be complete?
- Provide context: Mention relevant parts of your codebase
- Start small: Complex tasks often work better when broken into smaller prompts
Next Steps
- Learn about Configuration to customize agent behavior
- See Writing Prompts for prompt best practices
- Explore Sessions for session management
CLI Reference
Complete reference for all codeloops commands and options.
Overview
codeloops [OPTIONS] [COMMAND]
When no command is specified, codeloops runs the actor-critic loop (equivalent to codeloops run).
Commands
| Command | Description |
|---|---|
run | Run the actor-critic loop (default) |
sessions | Browse and inspect sessions |
ui | Start the web UI |
init | Interactive configuration setup |
help | Print help information |
Run Command
Execute the actor-critic loop. This is the default command when none is specified.
codeloops run [OPTIONS]
codeloops [OPTIONS] # Same as above
Prompt Options
| Option | Type | Default | Description |
|---|---|---|---|
-p, --prompt <PROMPT> | String | - | Task prompt (inline) |
--prompt-file <FILE> | Path | prompt.md | Path to prompt file |
If neither --prompt nor --prompt-file is provided, codeloops looks for prompt.md in the working directory.
Directory Options
| Option | Type | Default | Description |
|---|---|---|---|
-d, --working-dir <DIR> | Path | Current directory | Working directory for the session |
Agent Options
| Option | Type | Default | Description |
|---|---|---|---|
-a, --agent <AGENT> | Enum | claude | Agent for both actor and critic |
--actor-agent <AGENT> | Enum | - | Agent specifically for actor role |
--critic-agent <AGENT> | Enum | - | Agent specifically for critic role |
-m, --model <MODEL> | String | - | Model to use (if agent supports it) |
Agent values: claude, opencode, cursor
Loop Control
| Option | Type | Default | Description |
|---|---|---|---|
-n, --max-iterations <N> | Integer | Unlimited | Maximum loop iterations |
Output Options
| Option | Type | Default | Description |
|---|---|---|---|
--log-format <FORMAT> | Enum | pretty | Output format |
--log-file <PATH> | Path | - | Write structured logs to file |
--json-output | Flag | - | Output final result as JSON |
--no-color | Flag | - | Disable colored output |
Log format values: pretty, json, compact
Other Options
| Option | Type | Default | Description |
|---|---|---|---|
--dry-run | Flag | - | Show configuration without executing |
Examples
# Run with default prompt.md
codeloops
# Run with inline prompt
codeloops --prompt "Fix the bug in main.rs"
# Run with custom prompt file
codeloops --prompt-file tasks/feature.md
# Run with specific agent
codeloops --agent opencode
# Run with mixed agents
codeloops --actor-agent opencode --critic-agent claude
# Limit iterations
codeloops --max-iterations 5
# Output as JSON
codeloops --json-output
# Dry run to verify configuration
codeloops --dry-run
Sessions Command
Browse and inspect recorded sessions.
codeloops sessions <SUBCOMMAND>
Subcommands
list
List all sessions with optional filtering.
codeloops sessions list [OPTIONS]
| Option | Type | Description |
|---|---|---|
--outcome <OUTCOME> | String | Filter by outcome: success, failed, interrupted, max_iterations_reached |
--after <DATE> | Date | Show sessions after date (YYYY-MM-DD) |
--before <DATE> | Date | Show sessions before date (YYYY-MM-DD) |
--search <TEXT> | String | Search in prompt text |
--project <NAME> | String | Filter by project name |
Examples:
# List all sessions
codeloops sessions list
# Filter by outcome
codeloops sessions list --outcome success
# Filter by date range
codeloops sessions list --after 2025-01-01 --before 2025-01-31
# Search prompts
codeloops sessions list --search "authentication"
# Filter by project
codeloops sessions list --project myapp
show
Show detailed session information.
codeloops sessions show [ID]
If no ID is provided, opens an interactive picker to select a session.
Examples:
# Interactive picker
codeloops sessions show
# Show specific session
codeloops sessions show 2025-01-27T15-30-45Z_a3f2c1
diff
Show the cumulative git diff from a session.
codeloops sessions diff [ID]
If no ID is provided, opens an interactive picker.
Examples:
# Interactive picker
codeloops sessions diff
# Show diff for specific session
codeloops sessions diff 2025-01-27T15-30-45Z_a3f2c1
stats
Show aggregate statistics across all sessions.
codeloops sessions stats
Output includes:
- Total sessions
- Success rate
- Average iterations
- Average duration
- Sessions by project
UI Command
Start the web UI for visual session browsing.
codeloops ui [OPTIONS]
| Option | Type | Default | Description |
|---|---|---|---|
--dev | Flag | - | Development mode with hot reloading |
--api-port <PORT> | Integer | 3100 | API server port |
--ui-port <PORT> | Integer | 3101 | UI server port |
Examples:
# Start UI with defaults
codeloops ui
# Custom ports
codeloops ui --api-port 4000 --ui-port 4001
# Development mode
codeloops ui --dev
The UI opens automatically in your default browser.
Init Command
Interactive first-time setup.
codeloops init
This command:
- Prompts for your preferred default agent
- Creates
~/.config/codeloops/config.toml - Displays the generated configuration
Run this after installation to set up your defaults.
Global Options
These options work with any command:
| Option | Description |
|---|---|
-h, --help | Print help information |
-V, --version | Print version information |
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Max iterations reached |
| 2 | Failed (error during execution) |
| 130 | User interrupted (Ctrl+C) |
Environment Variables
| Variable | Description |
|---|---|
CODELOOPS_UI_DIR | Override the UI directory location |
NO_COLOR | Disable colored output when set |
Configuration
Codeloops supports configuration at two levels: global (user-wide) and project-level. This guide explains how to configure both.
Configuration Precedence
Settings are resolved in this order (highest to lowest priority):
- CLI flags (e.g.,
--agent claude) - Project configuration (
codeloops.tomlin working directory) - Global configuration (
~/.config/codeloops/config.toml) - Built-in defaults
For example, if you set agent = "opencode" in your global config but run codeloops --agent claude, Claude will be used.
Global Configuration
Location: ~/.config/codeloops/config.toml
The global configuration sets your user-wide defaults. Create it with codeloops init or manually.
Schema
[defaults]
agent = "claude" # Default agent for both roles
model = "sonnet" # Default model (optional)
[defaults.actor]
agent = "opencode" # Override agent for actor role
model = "gpt-4o" # Override model for actor
[defaults.critic]
agent = "claude" # Override agent for critic role
model = "opus" # Override model for critic
Fields
| Section | Field | Type | Description |
|---|---|---|---|
[defaults] | agent | String | Default agent: claude, opencode, or cursor |
[defaults] | model | String | Default model name (optional) |
[defaults.actor] | agent | String | Actor-specific agent override |
[defaults.actor] | model | String | Actor-specific model override |
[defaults.critic] | agent | String | Critic-specific agent override |
[defaults.critic] | model | String | Critic-specific model override |
Example Configurations
Simple setup (same agent for everything):
[defaults]
agent = "claude"
Mixed agents (fast actor, thorough critic):
[defaults]
agent = "claude"
[defaults.actor]
agent = "opencode"
model = "gpt-4o"
[defaults.critic]
model = "opus"
Project Configuration
Location: codeloops.toml in the project root (working directory)
Project configuration overrides global settings for a specific project. This is useful when different projects need different agent configurations.
Schema
agent = "claude" # Default agent for this project
model = "sonnet" # Default model (optional)
[actor]
agent = "opencode" # Actor agent for this project
model = "gpt-4o" # Actor model for this project
[critic]
agent = "claude" # Critic agent for this project
model = "opus" # Critic model for this project
Fields
| Section | Field | Type | Description |
|---|---|---|---|
| (root) | agent | String | Default agent for this project |
| (root) | model | String | Default model for this project |
[actor] | agent | String | Actor agent override |
[actor] | model | String | Actor model override |
[critic] | agent | String | Critic agent override |
[critic] | model | String | Critic model override |
Example Configurations
Simple project config:
agent = "claude"
Project using OpenCode for fast iteration:
[actor]
agent = "opencode"
model = "gpt-4o-mini"
[critic]
agent = "claude"
model = "sonnet"
Resolution Examples
Example 1: No configuration
With no config files and running codeloops:
- Actor: Claude (default)
- Critic: Claude (default)
Example 2: Global config only
~/.config/codeloops/config.toml:
[defaults]
agent = "opencode"
Running codeloops:
- Actor: OpenCode (from global)
- Critic: OpenCode (from global)
Example 3: Global + project config
~/.config/codeloops/config.toml:
[defaults]
agent = "opencode"
codeloops.toml:
agent = "claude"
Running codeloops:
- Actor: Claude (project overrides global)
- Critic: Claude (project overrides global)
Example 4: CLI overrides everything
~/.config/codeloops/config.toml:
[defaults]
agent = "opencode"
codeloops.toml:
agent = "claude"
Running codeloops --agent cursor:
- Actor: Cursor (CLI overrides all)
- Critic: Cursor (CLI overrides all)
Example 5: Per-role configuration
~/.config/codeloops/config.toml:
[defaults]
agent = "claude"
[defaults.actor]
agent = "opencode"
Running codeloops:
- Actor: OpenCode (from defaults.actor)
- Critic: Claude (from defaults)
Example 6: CLI role-specific override
~/.config/codeloops/config.toml:
[defaults]
agent = "opencode"
Running codeloops --critic-agent claude:
- Actor: OpenCode (from global)
- Critic: Claude (CLI override for critic only)
Creating Configuration
Using init
The simplest way to create global configuration:
codeloops init
This runs an interactive setup and creates the config file.
Manual Creation
Create the file manually:
mkdir -p ~/.config/codeloops
cat > ~/.config/codeloops/config.toml << 'EOF'
[defaults]
agent = "claude"
EOF
For project config, create codeloops.toml in your project root.
Validating Configuration
Use --dry-run to see the resolved configuration without executing:
codeloops --dry-run
Output shows the effective agent, model, and other settings that would be used.
Agents
Codeloops works with multiple AI coding agents. This guide covers supported agents and how to choose between them.
Supported Agents
| Agent | CLI Value | Binary | Description |
|---|---|---|---|
| Claude Code | claude | claude | Anthropic's Claude-powered coding agent |
| OpenCode | opencode | opencode | Multi-model coding agent |
| Cursor | cursor-agent | cursor-agent | Cursor IDE's agent CLI |
Agent Details
Claude Code
Claude Code is Anthropic's official coding agent, powered by Claude models.
Binary: claude
Strengths:
- Excellent reasoning and planning
- Strong understanding of complex codebases
- Good at following detailed instructions
- Reliable critic evaluation
Installation: Visit claude.ai/code
Verify installation:
which claude
claude --version
OpenCode
OpenCode is a multi-model coding agent that supports various LLM backends.
Binary: opencode
Strengths:
- Supports multiple models (GPT-4, etc.)
- Fast execution for straightforward tasks
- Good for rapid iteration
Installation: Visit opencode.ai/docs
Verify installation:
which opencode
opencode --version
Cursor
Cursor's CLI provides access to its coding capabilities outside the IDE.
Binary: cursor-agent (or agent)
Strengths:
- Integrates with Cursor IDE workflow
- Familiar for Cursor users
Installation: Visit cursor.com/cli
Verify installation:
which cursor-agent # or 'agent'
Choosing Agents
Same Agent for Both Roles
The simplest configuration uses one agent for both actor and critic:
codeloops --agent claude
This is recommended when:
- You're starting out and want simplicity
- The agent performs well for your use case
- You want consistent behavior
Different Agents for Actor and Critic
You can use different agents for each role:
codeloops --actor-agent opencode --critic-agent claude
This is useful when:
- You want fast iteration with a thorough reviewer
- Different agents have different strengths
- You're experimenting with agent combinations
Configuration Recommendations
For complex tasks (refactoring, architecture changes):
codeloops --agent claude
Use Claude for both roles when the task requires deep understanding.
For fast iteration (simple fixes, small changes):
codeloops --actor-agent opencode --critic-agent claude
Use a fast actor with a thorough critic.
For Cursor users:
codeloops --agent cursor-agent
Use Cursor if you're already in the Cursor ecosystem.
Model Selection
Some agents support model selection:
# Specify model for both roles
codeloops --agent claude --model opus
# Specify model per role (via config file)
Model support depends on the agent:
- Claude Code: Supports Claude models (sonnet, opus, etc.)
- OpenCode: Supports multiple backends (gpt-4o, etc.)
- Cursor: Uses Cursor's configured model
Agent Availability
Codeloops checks agent availability before running. If an agent isn't found:
Error: Agent 'claude' not found in PATH
To fix this:
- Install the agent
- Ensure the binary is in your PATH
- Verify with
which <agent-name>
Checking All Agents
Check which agents are available:
which claude opencode cursor-agent
Agent Configuration
In Global Config
# ~/.config/codeloops/config.toml
[defaults]
agent = "claude"
[defaults.actor]
agent = "opencode"
[defaults.critic]
agent = "claude"
In Project Config
# codeloops.toml
[actor]
agent = "opencode"
model = "gpt-4o"
[critic]
agent = "claude"
model = "sonnet"
How Agents Are Invoked
When codeloops runs an agent, it:
- Spawns the agent binary as a subprocess
- Passes the prompt via stdin or command-line arguments
- Sets the working directory
- Captures stdout, stderr, and exit code
- Waits for completion
The agent runs with full access to the filesystem within the working directory, allowing it to read and modify files as needed.
Agent Output
Agent output is captured and passed to the critic. The output typically includes:
- What the agent did
- Files modified
- Any errors encountered
This output, combined with the git diff, forms the basis for critic evaluation.
Sessions
Every codeloops run is recorded as a session. This guide covers how to view, filter, and analyze your sessions.
Session Storage
Sessions are stored as JSONL files in:
~/.local/share/codeloops/sessions/
Each session is a single file named:
<timestamp>_<hash>.jsonl
For example: 2025-01-27T15-30-45Z_a3f2c1.jsonl
The hash is derived from the prompt, making it easy to identify related sessions.
Listing Sessions
Basic List
codeloops sessions list
Output:
ID Project Outcome Iters Duration Prompt
2025-01-27T15-30-45Z_a3f2c1 myapp success 2 89.4s Add input validation to...
2025-01-27T14-22-10Z_b5d3e2 myapp success 1 45.1s Fix the typo in greeting...
2025-01-26T09-15-33Z_c7f4a9 api-svc failed 5 312.8s Implement OAuth flow...
Filtering by Outcome
# Only successful sessions
codeloops sessions list --outcome success
# Only failed sessions
codeloops sessions list --outcome failed
# Interrupted sessions (Ctrl+C)
codeloops sessions list --outcome interrupted
# Max iterations reached
codeloops sessions list --outcome max_iterations_reached
Filtering by Date
# Sessions after a date
codeloops sessions list --after 2025-01-01
# Sessions before a date
codeloops sessions list --before 2025-01-31
# Date range
codeloops sessions list --after 2025-01-01 --before 2025-01-31
Filtering by Project
codeloops sessions list --project myapp
The project name is the basename of the working directory where the session ran.
Searching Prompts
codeloops sessions list --search "authentication"
This searches the prompt text for the given substring.
Combining Filters
codeloops sessions list --outcome success --project myapp --after 2025-01-01
Viewing Session Details
Interactive Picker
codeloops sessions show
This opens an interactive picker to browse and select a session.
Specific Session
codeloops sessions show 2025-01-27T15-30-45Z_a3f2c1
Output includes:
- Session metadata (timestamp, agents, working directory)
- Full prompt text
- Each iteration with actor output and critic feedback
- Final outcome and summary
Viewing Session Diffs
Interactive Picker
codeloops sessions diff
Specific Session
codeloops sessions diff 2025-01-27T15-30-45Z_a3f2c1
This shows the cumulative git diff from all iterations. The diff is syntax-highlighted in the terminal.
Session Statistics
codeloops sessions stats
Output:
Session Statistics
==================
Total Sessions: 47
Success Rate: 78.7%
Avg Iterations: 2.3
Avg Duration: 94.2s
By Project:
myapp: 23 sessions (82.6% success)
api-svc: 15 sessions (73.3% success)
web-frontend: 9 sessions (77.8% success)
Sessions Over Time:
2025-01-27: 5 sessions
2025-01-26: 8 sessions
2025-01-25: 12 sessions
...
Session Outcomes
| Outcome | Description |
|---|---|
success | Critic approved the work (DONE decision) |
failed | Error during execution |
interrupted | User pressed Ctrl+C |
max_iterations_reached | Hit the iteration limit without completion |
Understanding Session Content
Session Start
Contains initial configuration:
- Timestamp
- Prompt
- Working directory
- Actor and critic agents
- Models (if specified)
- Max iterations (if set)
Iterations
Each iteration records:
- Iteration number
- Actor output (stdout)
- Actor stderr
- Actor exit code
- Actor duration
- Git diff
- Files changed count
- Critic decision (DONE, CONTINUE, or ERROR)
- Critic feedback (if CONTINUE)
- Timestamp
Session End
Final status:
- Outcome
- Total iterations
- Summary (from critic)
- Confidence score (0-1)
- Total duration
Programmatic Access
Using jq
Sessions are JSONL (one JSON object per line), making them easy to parse:
# Get the prompt from a session
head -1 ~/.local/share/codeloops/sessions/2025-01-27T15-30-45Z_a3f2c1.jsonl | jq -r '.prompt'
# Get all critic decisions
cat ~/.local/share/codeloops/sessions/2025-01-27T15-30-45Z_a3f2c1.jsonl | jq -r 'select(.type == "iteration") | .critic_decision'
# Get the final outcome
tail -1 ~/.local/share/codeloops/sessions/2025-01-27T15-30-45Z_a3f2c1.jsonl | jq -r '.outcome'
Using Python
import json
from pathlib import Path
session_file = Path.home() / ".local/share/codeloops/sessions/2025-01-27T15-30-45Z_a3f2c1.jsonl"
with open(session_file) as f:
lines = [json.loads(line) for line in f]
start = lines[0]
iterations = [l for l in lines if l.get("type") == "iteration"]
end = lines[-1] if lines[-1].get("type") == "session_end" else None
print(f"Prompt: {start['prompt']}")
print(f"Iterations: {len(iterations)}")
if end:
print(f"Outcome: {end['outcome']}")
Using Rust
The codeloops-sessions crate provides session parsing:
#![allow(unused)] fn main() { use codeloops_sessions::{SessionStore, SessionFilter}; let store = SessionStore::new()?; let sessions = store.list_sessions(&SessionFilter::default())?; for summary in sessions { println!("{}: {}", summary.id, summary.prompt_preview); } // Load a full session let session = store.load_session("2025-01-27T15-30-45Z_a3f2c1")?; println!("Iterations: {}", session.iterations.len()); }
Managing Sessions
Deleting Sessions
Sessions are plain files. Delete them directly:
# Delete a specific session
rm ~/.local/share/codeloops/sessions/2025-01-27T15-30-45Z_a3f2c1.jsonl
# Delete all sessions older than 30 days
find ~/.local/share/codeloops/sessions -name "*.jsonl" -mtime +30 -delete
Backing Up Sessions
# Copy all sessions to a backup location
cp -r ~/.local/share/codeloops/sessions ~/backups/codeloops-sessions-$(date +%Y%m%d)
Session Size
Sessions can grow large if:
- The actor produces verbose output
- Many iterations occur
- Large diffs are generated
Check session sizes:
ls -lh ~/.local/share/codeloops/sessions/
Web UI for Sessions
For visual session browsing:
codeloops ui
The web UI provides:
- Session list with filters
- Iteration timeline
- Syntax-highlighted diffs
- Critic feedback trail
- Statistics and charts
See Web UI Overview for details.
Web UI Overview
Codeloops includes a web-based interface for browsing and analyzing sessions. This guide provides an overview of what the UI offers.
What the Web UI Provides
Session List
Browse all recorded sessions with:
- Sortable columns (timestamp, project, outcome, iterations, duration)
- Quick filters for outcome and project
- Search functionality for prompts
- Date range filtering
Session Detail View
Examine individual sessions with:
- Full prompt display
- Session metadata (agents, working directory, duration)
- Iteration-by-iteration breakdown
Iteration Timeline
Visualize the actor-critic loop:
- Timeline showing each iteration
- Duration bars for actor execution
- Color-coded critic decisions (DONE, CONTINUE, ERROR)
- Click to expand iteration details
Syntax-Highlighted Diffs
View code changes with:
- Syntax highlighting for common languages
- Side-by-side or unified diff view
- File-by-file navigation
- Line number display
Statistics and Charts
Analyze patterns across sessions:
- Success rate over time
- Average iterations per project
- Session duration trends
- Project-wise breakdown
Real-Time Updates
When a session is in progress:
- Live iteration updates via Server-Sent Events
- Progress indicators
- Auto-refresh of session list
When to Use CLI vs UI
Use the CLI when:
- Doing quick lookups (
codeloops sessions show <id>) - Scripting and automation
- Working in a terminal-only environment
- Piping output to other tools
Use the UI when:
- Browsing multiple sessions
- Comparing iterations visually
- Analyzing patterns and statistics
- Reviewing complex diffs
- Sharing session details with others
Technology Stack
The web UI is built with:
| Technology | Purpose |
|---|---|
| React 19 | UI framework |
| React Router 7 | Client-side routing |
| TypeScript | Type safety |
| Vite | Build tool and dev server |
| Tailwind CSS 4 | Styling |
| Recharts | Charts and visualizations |
| Bun | JavaScript runtime and bundler |
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Web Browser │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ React UI │ │
│ │ Dashboard │ Session Detail │ Stats │ Diff Viewer │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
│ HTTP / SSE
▼
┌─────────────────────────────────────────────────────────────┐
│ API Server (Rust) │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ /sessions │ │ /stats │ │ /sessions/live │ │
│ │ REST API │ │ REST API │ │ SSE │ │
│ └─────────────┘ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Session Store (JSONL Files) │
│ ~/.local/share/codeloops/sessions/*.jsonl │
└─────────────────────────────────────────────────────────────┘
Ports
By default:
- API Server: Port 3100
- UI Server: Port 3101 (development) or served by API (production)
Both are configurable via CLI flags.
Browser Support
The UI works in modern browsers:
- Chrome/Chromium (recommended)
- Firefox
- Safari
- Edge
Next Steps
- Usage Guide - How to use the UI
- Development Guide - Contributing to the UI
Web UI Usage
This guide covers how to use the codeloops web interface.
Starting the UI
Basic Start
codeloops ui
This:
- Starts the API server on port 3100
- Serves the UI on port 3101
- Opens your default browser automatically
Custom Ports
codeloops ui --api-port 4000 --ui-port 4001
Development Mode
For UI development with hot reloading:
codeloops ui --dev
Navigating the Interface
Sidebar
The sidebar provides navigation:
| Section | Description |
|---|---|
| Dashboard | Session list with filters |
| Stats | Statistics and charts |
Dashboard (Session List)
The main view shows all sessions in a table:
| Column | Description |
|---|---|
| Timestamp | When the session started |
| Project | Project name (working directory basename) |
| Outcome | success, failed, interrupted, or max_iterations_reached |
| Iterations | Number of actor-critic loops |
| Duration | Total session time |
| Prompt | First 100 characters of the prompt |
Sorting: Click column headers to sort.
Row click: Opens the session detail view.
Filters
Above the session table:
Outcome Filter: Dropdown to filter by outcome type.
Project Filter: Dropdown to filter by project name.
Search: Text input to search prompts.
Date Range: Start and end date pickers.
Clear Filters: Reset all filters.
Session Detail View
Click a session to open its detail view:
Header Section
- Session ID
- Timestamp
- Working directory
- Actor agent and model
- Critic agent and model
- Total duration
- Final outcome
Prompt Section
The full prompt text, rendered as markdown.
Iteration Timeline
Visual timeline of iterations:
[===========] 45s ──▶ CONTINUE
[=======] 32s ──▶ DONE
Each bar represents an iteration:
- Width proportional to duration
- Color indicates critic decision:
- Green: DONE
- Yellow: CONTINUE
- Red: ERROR
Iteration Details
Click an iteration to expand:
Actor Output: What the agent produced (stdout)
Git Diff: Syntax-highlighted diff of changes
Critic Feedback: Feedback text (for CONTINUE decisions)
Metadata: Duration, exit code, files changed
Diff Viewer
The diff viewer shows code changes:
File Tabs: If multiple files changed, tabs let you switch between them.
Syntax Highlighting: Code is highlighted based on file extension.
Line Numbers: Both old and new line numbers shown.
Change Indicators:
- Green background: Added lines
- Red background: Removed lines
- No background: Unchanged context lines
Stats Page
The statistics page shows:
Summary Cards
- Total Sessions
- Success Rate
- Average Iterations
- Average Duration
Charts
Sessions Over Time: Bar chart showing sessions per day.
Success Rate Trend: Line chart of success rate over time.
Iterations Distribution: Histogram of iteration counts.
Duration Distribution: Histogram of session durations.
By Project
Table breakdown per project:
- Session count
- Success rate
- Average iterations
- Average duration
Real-Time Updates
When sessions are running:
Session List: New sessions appear automatically.
Active Session: If viewing an active session, iterations appear as they complete.
Status Indicator: Shows "Live" badge for in-progress sessions.
The UI uses Server-Sent Events (SSE) for real-time updates without polling.
Keyboard Shortcuts
| Key | Action |
|---|---|
/ | Focus search input |
Esc | Close detail view / clear search |
j | Next session in list |
k | Previous session in list |
Enter | Open selected session |
URL Structure
The UI uses client-side routing:
| URL | View |
|---|---|
/ | Dashboard (session list) |
/sessions/:id | Session detail |
/stats | Statistics page |
You can bookmark or share these URLs directly.
Troubleshooting
UI Won't Start
Port already in use:
Error: Address already in use (port 3100)
Solution: Use different ports:
codeloops ui --api-port 4000 --ui-port 4001
UI directory not found:
Error: UI directory not found
Solution: Build the UI or set CODELOOPS_UI_DIR:
cd ui && bun run build
# or
export CODELOOPS_UI_DIR=/path/to/ui/dist
No Sessions Showing
Check that sessions exist:
ls ~/.local/share/codeloops/sessions/
If empty, run some codeloops sessions first.
Slow Loading
Large sessions (many iterations, large diffs) may load slowly. The API paginates where possible, but individual session detail views load the full session.
Browser Console Errors
Open browser developer tools (F12) and check the console for errors. Common issues:
- CORS errors: Ensure the API server is running
- 404 errors: API endpoint issues
- Network errors: Port/firewall problems
Tips
-
Use filters: Large session lists load faster with filters applied.
-
Bookmark searches: The URL includes filter state, so bookmark filtered views.
-
Compare iterations: Use the iteration timeline to quickly see where changes happened.
-
Export data: Use the CLI for programmatic access to session data.
Prompt Builder
The Prompt Builder is a conversational AI-powered interface for creating high-quality prompt.md files. It guides you through an interview process tailored to your work type, helping you capture all the necessary context for effective coding sessions.
Accessing the Prompt Builder
Start the web UI and navigate to the Prompt Builder:
codeloops ui
Then click "Prompt Builder" in the navigation header, or go directly to /prompt-builder.
Work Types
The Prompt Builder supports five work types, each with a tailored interview flow:
Feature
For new functionality. The AI guides you through:
- Problem Definition - Who is this for? What problem does it solve?
- Technical Approach - What components need to change? What patterns to follow?
- Implementation Details - Specific changes per component, edge cases
- Acceptance Criteria - Definition of done, verification steps
Defect
For bug fixes. The AI helps capture:
- Symptom - What's happening vs. what should happen? Reproduction steps
- Root Cause - Where in the code? Why is it happening?
- Fix Strategy - How to address it? Which files?
- Verification - Tests to add, regression prevention
Risk
For security vulnerabilities, performance issues, or technical concerns:
- Risk Identification - What risk? How discovered? Impact?
- Current State - Where does it exist? Current mitigations?
- Remediation Plan - How to address? Files to change?
- Validation - How to verify the fix?
Debt
For technical debt cleanup:
- Current State - What needs improvement? Why is it problematic?
- Target State - What should it look like? Patterns to follow?
- Refactoring Plan - Files to touch, safe order of operations
- Verification - Tests that must pass, behavior to preserve
Custom
For anything that doesn't fit the above categories. The AI asks general questions about your goal and helps structure the prompt appropriately.
Using the Prompt Builder
1. Select Work Type
Choose a work type by clicking one of the text links: Feature, Defect, Risk, Debt, or Custom.
2. Have a Conversation
The AI asks probing questions based on your work type. Answer naturally - the AI builds your prompt incrementally based on your responses.
Tips:
- Be specific about context (files, functions, behaviors)
- Include reproduction steps for defects
- Mention constraints or non-goals
- Share relevant code snippets if helpful
3. Preview Your Prompt
Press Cmd+P (Mac) or Ctrl+P (Windows/Linux) to toggle the preview panel. The preview shows your prompt.md building in real-time.
You can also:
- Edit the preview directly for fine-tuning
- Continue chatting to add more context
- The preview updates automatically
4. Export
When you're satisfied with your prompt:
| Action | Description |
|---|---|
| Save | Writes prompt.md to your project directory |
| Copy | Copies to clipboard |
| Download | Downloads as a file |
Keyboard Shortcuts
| Shortcut | Action |
|---|---|
Cmd/Ctrl + Enter | Send message |
Cmd/Ctrl + P | Toggle preview panel |
Escape | Close preview panel |
Session Persistence
Your session is automatically saved to browser local storage. If you:
- Refresh the page
- Close and reopen the browser
- Navigate away and return
...your conversation will be restored.
To start fresh, click "Start over" to clear the session.
Best Practices
Be Specific
Instead of:
"Fix the login bug"
Try:
"Users clicking 'Login' on the /auth page get a 500 error. Expected: redirect to /dashboard. The error appears in the console as 'TypeError: user.id is undefined' in auth.service.ts line 42."
Provide Context
Mention:
- Relevant files or functions
- Related features or systems
- Previous attempts and why they failed
- Constraints (performance, backwards compatibility, etc.)
Define Success
Be clear about:
- What "done" looks like
- How to verify the change works
- Tests that should pass
- Edge cases to handle
Example Session
Work Type: Feature
AI: Let's design this feature together. Who is this feature for and what problem does it solve?
You: Our API team needs rate limiting on the /api/v2 endpoints. Currently any client can hammer the API and there's no protection. They should be limited to 100 requests per minute per API key.
AI: Good context. What components will need to change? Are there existing patterns to follow?
You: We already have a RateLimiter class in src/middleware/rate-limiter.ts that's used for auth endpoints. We should extend that. The v2 routes are in src/routes/v2/*.ts.
...and so on. The resulting prompt.md captures all this context in a structured format ready for a coding session.
Web UI Development
This guide covers how to develop and contribute to the codeloops web UI.
Prerequisites
- Bun (recommended) or Node.js 18+
- Rust toolchain (for the API server)
Development Setup
Clone and Install
git clone https://github.com/silvabyte/codeloops
cd codeloops/ui
bun install
Start Development Servers
Option 1: Use the integrated dev mode:
codeloops ui --dev
This starts both the API server and Vite dev server with hot reloading.
Option 2: Run servers separately:
# Terminal 1: API server
cargo run -- ui --dev
# Terminal 2: Vite dev server (if needed separately)
cd ui && bun dev
Development URLs
- UI: http://localhost:3101
- API: http://localhost:3100
In dev mode, the UI proxies API requests to the backend automatically.
Project Structure
ui/
├── src/
│ ├── main.tsx # Application entry point
│ ├── App.tsx # Root component with routing
│ │
│ ├── api/
│ │ ├── types.ts # TypeScript interfaces
│ │ └── client.ts # API client functions
│ │
│ ├── pages/
│ │ ├── Dashboard.tsx # Session list view
│ │ ├── SessionDetail.tsx # Single session view
│ │ ├── Stats.tsx # Statistics page
│ │ └── NotFound.tsx # 404 page
│ │
│ ├── components/
│ │ ├── Layout.tsx # Main layout with sidebar
│ │ ├── Welcome.tsx # Empty state component
│ │ ├── SessionTable.tsx # Sessions list table
│ │ ├── SessionFilters.tsx# Filter controls
│ │ ├── StatsBar.tsx # Statistics summary
│ │ ├── IterationTimeline.tsx # Visual timeline
│ │ ├── CriticTrail.tsx # Critic feedback display
│ │ └── DiffViewer.tsx # Code diff viewer
│ │
│ ├── hooks/
│ │ ├── useSessions.ts # Fetch sessions list
│ │ ├── useSession.ts # Fetch single session
│ │ ├── useStats.ts # Fetch statistics
│ │ └── useSSE.ts # Server-Sent Events
│ │
│ └── lib/
│ └── utils.ts # Utility functions
│
├── package.json
├── vite.config.ts # Vite configuration
├── tsconfig.json # TypeScript configuration
├── tailwind.config.js # Tailwind CSS configuration
└── serve.ts # Standalone server entry
Key Components
API Client (src/api/client.ts)
Functions for fetching data from the backend:
// Fetch all sessions with optional filters
export async function fetchSessions(filters?: SessionFilter): Promise<SessionSummary[]>
// Fetch a single session by ID
export async function fetchSession(id: string): Promise<Session>
// Fetch session diff
export async function fetchSessionDiff(id: string): Promise<string>
// Fetch statistics
export async function fetchStats(): Promise<SessionStats>
Types (src/api/types.ts)
TypeScript interfaces matching the Rust types:
interface SessionSummary {
id: string
timestamp: string
prompt_preview: string
working_dir: string
project: string
outcome: string | null
iterations: number
duration_secs: number | null
confidence: number | null
actor_agent: string
critic_agent: string
}
interface Session {
id: string
start: SessionStart
iterations: Iteration[]
end: SessionEnd | null
}
Hooks
Custom React hooks for data fetching:
// useSessions.ts
export function useSessions(filters?: SessionFilter) {
const [sessions, setSessions] = useState<SessionSummary[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
// ... fetch logic
return { sessions, loading, error, refetch }
}
SSE Hook (src/hooks/useSSE.ts)
Handles real-time updates:
export function useSSE(onEvent: (event: SessionEvent) => void) {
useEffect(() => {
const eventSource = new EventSource('/api/sessions/live')
eventSource.onmessage = (e) => {
const event = JSON.parse(e.data)
onEvent(event)
}
return () => eventSource.close()
}, [onEvent])
}
Adding a New Page
- Create the page component in
src/pages/:
// src/pages/MyPage.tsx
export default function MyPage() {
return (
<div>
<h1>My Page</h1>
</div>
)
}
- Add the route in
src/App.tsx:
import MyPage from './pages/MyPage'
// In the Routes:
<Route path="/my-page" element={<MyPage />} />
- Add navigation in
src/components/Layout.tsx:
<NavLink to="/my-page">My Page</NavLink>
Adding a New Component
- Create the component in
src/components/:
// src/components/MyComponent.tsx
interface MyComponentProps {
title: string
children: React.ReactNode
}
export function MyComponent({ title, children }: MyComponentProps) {
return (
<div className="p-4 border rounded">
<h2 className="text-lg font-bold">{title}</h2>
{children}
</div>
)
}
- Import and use it:
import { MyComponent } from '../components/MyComponent'
<MyComponent title="Hello">
Content here
</MyComponent>
Styling
The UI uses Tailwind CSS. Add classes directly to elements:
<div className="flex items-center gap-4 p-4 bg-gray-100 rounded-lg">
<span className="text-sm text-gray-600">Label</span>
<button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Click me
</button>
</div>
Building for Production
cd ui
bun run build
Output goes to ui/dist/.
Creating a Standalone Binary
cd ui
bun run compile
This creates a codeloops-ui binary that serves the UI without needing Bun installed.
Testing
Type Checking
bun run typecheck
# or
npx tsc --noEmit
Linting
bun run lint
# or
npx eslint src/
API Development
The API server is in Rust at crates/codeloops/src/api/. Key files:
| File | Purpose |
|---|---|
mod.rs | Router setup |
sessions.rs | Session endpoints |
stats.rs | Statistics endpoint |
sse.rs | Server-Sent Events |
To add a new API endpoint:
- Add the handler function in the appropriate file
- Add the route in
mod.rs:
#![allow(unused)] fn main() { .route("/api/my-endpoint", get(my_handler)) }
- Add the corresponding client function in
ui/src/api/client.ts
Common Tasks
Update API Types
When Rust types change:
- Update
ui/src/api/types.tsto match - Update any affected components
Add a New Filter
- Add the filter field to
SessionFilterintypes.ts - Update
SessionFilters.tsxto include the new control - Update
fetchSessions()to send the filter parameter - Update the Rust API to handle the new filter
Add a Chart
- Add the data field to the appropriate type
- Import from Recharts:
import { BarChart, Bar, XAxis, YAxis, Tooltip } from 'recharts'
- Add the chart component:
<BarChart data={data}>
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Bar dataKey="value" fill="#3b82f6" />
</BarChart>
Debugging
Browser DevTools
- Network tab: Check API requests
- Console: View errors and logs
- React DevTools: Inspect component state
API Debugging
# Test API endpoints directly
curl http://localhost:3100/api/sessions
curl http://localhost:3100/api/stats
SSE Debugging
In browser console:
const es = new EventSource('http://localhost:3100/api/sessions/live')
es.onmessage = (e) => console.log(JSON.parse(e.data))
Architecture Overview
This document provides a high-level overview of codeloops' architecture, design philosophy, and component structure.
Design Philosophy
Simplicity
Codeloops embraces simplicity at every level:
- Interface: A
prompt.mdfile is the primary interface. No complex configuration required. - Execution: Run
codeloopsand the loop handles the rest. - Output: JSONL session files are human-readable and tool-friendly.
Composability
The system is designed for flexibility:
- Agent-agnostic: Works with any coding agent that has a CLI.
- Mixed configurations: Use different agents for actor and critic roles.
- Extensible: Adding new agents requires implementing a simple trait.
Observability
Every action is recorded:
- Full session logging: All inputs, outputs, and decisions are captured.
- Iteration history: See exactly what happened at each step.
- Statistics: Analyze patterns across sessions.
System Diagram
┌─────────────────────────────────────────────────────────────────────────────┐
│ User Interface │
│ ┌───────────────┐ ┌────────────────┐ ┌───────────────────────────────┐ │
│ │ prompt.md │ │ CLI (codeloops) │ │ Web UI │ │
│ │ │ │ │ │ (sessions, stats, diffs) │ │
│ └───────┬───────┘ └────────┬─────────┘ └──────────────┬──────────────┘ │
└──────────┼───────────────────┼───────────────────────────┼──────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Core System │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ LoopRunner │ │
│ │ (codeloops-core crate) │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │ │
│ │ │ Actor │ ──────▶ │ Git Diff │ ──────▶ │ Critic │ │ │
│ │ │ Agent │ │ Capture │ │ Agent │ │ │
│ │ └─────────────┘ └─────────────┘ └───────────┘ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────┐ │ │ │
│ │ └──────────────────│ Feedback │◀────────────────┘ │ │
│ │ │ Loop │ │ │
│ │ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Session Writer │ │
│ │ (codeloops-logging crate) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
└────────────────────────────────────┼────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Storage Layer │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ~/.local/share/codeloops/sessions/*.jsonl │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Component Overview
CLI Binary (codeloops crate)
The main entry point. Responsibilities:
- Parse command-line arguments
- Load configuration (global and project)
- Initialize the loop runner
- Handle session commands
- Serve the web UI and API
Loop Runner (codeloops-core crate)
Orchestrates the actor-critic loop. Responsibilities:
- Execute actor agents
- Capture git diffs
- Execute critic agents
- Parse critic decisions
- Manage iteration flow
- Handle interrupts (Ctrl+C)
Agent Abstraction (codeloops-agent crate)
Provides a unified interface for coding agents. Responsibilities:
- Define the
Agenttrait - Implement agents (Claude, OpenCode, Cursor)
- Spawn agent processes
- Capture output (stdout, stderr, exit code)
Critic Evaluation (codeloops-critic crate)
Handles critic logic. Responsibilities:
- Build evaluation prompts
- Parse critic decisions (DONE, CONTINUE, ERROR)
- Extract feedback and confidence scores
Git Operations (codeloops-git crate)
Manages git interactions. Responsibilities:
- Capture diffs between iterations
- Track changed files
- Provide diff summaries
Logging (codeloops-logging crate)
Handles output and session recording. Responsibilities:
- Format log output (pretty, JSON, compact)
- Write session JSONL files
- Handle structured events
Session Management (codeloops-sessions crate)
Reads and queries sessions. Responsibilities:
- Parse JSONL session files
- Provide session summaries (fast)
- Filter and search sessions
- Calculate statistics
- Watch for session changes
Data Flow
-
Input: User provides prompt via
prompt.mdor--prompt -
Configuration: CLI loads global and project config, resolves agent choices
-
Initialization: LoopRunner created with actor/critic agents
-
Session Start: SessionWriter creates JSONL file, writes start line
-
Actor Execution: Actor agent spawned with prompt, output captured
-
Diff Capture: Git diff computed between before/after states
-
Critic Evaluation: Critic agent spawned with actor output + diff
-
Decision Parsing: Critic response parsed for decision and feedback
-
Session Update: Iteration recorded to JSONL file
-
Loop Control: If DONE, end session. If CONTINUE, feed back to actor.
-
Completion: SessionEnd line written, process exits
Why This Architecture?
Separation of Concerns
Each crate has a single responsibility:
codeloops-agent: Knows how to run agentscodeloops-critic: Knows how to evaluatecodeloops-git: Knows how to diffcodeloops-core: Orchestrates everything
This makes testing and modification easier.
Extensibility
Adding a new agent:
- Implement the
Agenttrait - Add it to the agent factory
- No changes to core loop logic
Observability
JSONL session files provide:
- Complete audit trail
- Easy parsing (standard JSON)
- Append-only writes (crash-safe)
- Human readability
Performance
Design choices for performance:
- Fast session summaries (read first/last lines only)
- Streaming output from agents
- Minimal overhead in the loop
Next Steps
- The Actor-Critic Loop - Detailed loop mechanics
- Crate Structure - Deep dive into each crate
- Data Flow - Sequence diagrams and data paths
The Actor-Critic Loop
This document explains the core feedback loop that drives codeloops.
Concept
The actor-critic pattern comes from reinforcement learning, where:
- The actor takes actions (makes code changes)
- The critic evaluates those actions (reviews the changes)
In codeloops:
- The actor is a coding agent executing your task
- The critic is another agent instance evaluating the work
- Feedback flows from critic to actor until the task is complete
State Machine
┌──────────────────┐
│ START │
└────────┬─────────┘
│
▼
┌──────────────────┐
┌────▶│ ACTOR_EXECUTING │◀────┐
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ CAPTURING_DIFF │ │
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │CRITIC_EVALUATING │ │
│ └────────┬─────────┘ │
│ │ │
│ ┌──────────┼──────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────┐ ┌────────┐ ┌─────┐ │
│ │DONE│ │CONTINUE│ │ERROR│ │
│ └──┬─┘ └───┬────┘ └──┬──┘ │
│ │ │ │ │
│ │ │ feedback │ │
│ │ └──────────┼────┘
│ │ │
│ │ recovery │
│ │ suggestion │
│ │ ┌──────────┘
│ │ │
│ ▼ ▼
│ ┌──────┐ ┌──────────────┐
│ │ END │ │ FEED_BACK │
│ └──────┘ └──────┬───────┘
│ │
└──────────────────┘
Decision Types
The critic returns one of three decisions:
DONE
The task is complete. The critic has determined that:
- All requirements in the prompt are met
- The implementation is correct
- No further changes are needed
Response includes:
- Summary of what was accomplished
- Confidence score (0.0 to 1.0)
CONTINUE
More work is needed. The critic has determined that:
- The prompt requirements are not fully met
- There are issues that need addressing
- Additional changes are required
Response includes:
- Feedback explaining what's missing or wrong
- Guidance for the next iteration
ERROR
Something went wrong. This occurs when:
- The actor's exit code indicates failure
- The actor produced error output
- The changes broke something
Response includes:
- Analysis of what went wrong
- Recovery suggestion for the actor
Iteration Flow
1. Actor Execution
The actor receives:
- Original prompt (always)
- Previous feedback (if CONTINUE or ERROR)
┌─────────────────────────────────────────────────┐
│ Actor Input │
├─────────────────────────────────────────────────┤
│ Original Prompt: │
│ Add input validation to the login endpoint. │
│ │
│ Previous Feedback (if any): │
│ The email validation is good, but password │
│ validation is missing. Please add checks for │
│ minimum length and required characters. │
└─────────────────────────────────────────────────┘
The actor executes with full filesystem access in the working directory.
2. Diff Capture
After the actor completes, codeloops captures:
- Git diff (all changes since session start)
- Number of files changed
- Actor stdout and stderr
- Actor exit code
- Execution duration
3. Critic Evaluation
The critic receives:
- Original prompt
- Actor's output (stdout)
- Git diff of changes
- Iteration number
- Previous history (summarized)
┌─────────────────────────────────────────────────┐
│ Critic Input │
├─────────────────────────────────────────────────┤
│ Task Prompt: │
│ Add input validation to the login endpoint. │
│ │
│ Actor Output: │
│ I've added email validation using regex and │
│ password length checking... │
│ │
│ Git Diff: │
│ diff --git a/src/auth.rs b/src/auth.rs │
│ + if !is_valid_email(&email) { ... } │
│ + if password.len() < 8 { ... } │
│ │
│ Iteration: 1 │
└─────────────────────────────────────────────────┘
4. Decision Parsing
The critic's response is parsed to extract:
- Decision (DONE, CONTINUE, or ERROR)
- Feedback or summary text
- Confidence score (for DONE)
Expected critic output format:
DECISION: DONE
SUMMARY: Input validation has been added to the login endpoint.
Email addresses are validated using RFC 5321 compliant regex.
Passwords require minimum 8 characters.
CONFIDENCE: 0.95
Or for CONTINUE:
DECISION: CONTINUE
FEEDBACK: The email validation looks good, but password validation
only checks length. The requirements also specified:
- At least one uppercase letter
- At least one number
Please add these additional password requirements.
5. Loop Control
Based on the decision:
DONE: Session ends successfully
- SessionEnd written with outcome="success"
- Summary and confidence recorded
- Exit code 0
CONTINUE: Actor runs again
- Feedback passed to actor
- New iteration begins
- Same prompt + feedback
ERROR: Recovery attempted
- Recovery suggestion passed to actor
- New iteration begins
- If repeated errors, may fail session
Termination Conditions
The loop ends when:
- Success: Critic returns DONE
- Max iterations: Configured limit reached (exit code 1)
- Error: Unrecoverable error occurs (exit code 2)
- Interrupt: User presses Ctrl+C (exit code 130)
Confidence Scoring
When the critic returns DONE, it provides a confidence score:
| Score | Meaning |
|---|---|
| 0.9 - 1.0 | High confidence, all requirements clearly met |
| 0.7 - 0.9 | Good confidence, requirements met with minor uncertainty |
| 0.5 - 0.7 | Moderate confidence, some requirements unclear |
| < 0.5 | Low confidence, task may be incomplete |
The score is recorded but doesn't affect loop behavior. It's informational for users reviewing sessions.
Feedback Quality
Good critic feedback:
- Specific about what's missing or wrong
- References the original requirements
- Provides actionable guidance
- Prioritizes issues by importance
Example of good feedback:
FEEDBACK: The validation is partially implemented:
DONE:
- Email format validation using regex
MISSING:
1. Password minimum length check (required: 8 characters)
2. Password uppercase letter requirement
3. Password digit requirement
Please implement the missing password validations and return
appropriate error messages for each case.
Actor Recovery
When the actor fails (non-zero exit code), the critic provides recovery guidance:
DECISION: ERROR
ANALYSIS: The actor encountered a compilation error:
error[E0599]: no method named `validate_email` found
RECOVERY: The `validate_email` method doesn't exist. You need to
either:
1. Import it from the `validators` crate, or
2. Implement it in src/utils/validation.rs
Check the project's existing validation patterns in src/utils/.
The actor then receives this recovery suggestion and attempts to fix the issue.
Iteration Limits
Without a limit, loops could run indefinitely. Set limits with:
codeloops --max-iterations 5
Or in configuration:
max_iterations = 5
When the limit is reached:
- Outcome is "max_iterations_reached"
- Exit code is 1
- Session is complete but task may be unfinished
Best Practices
For Prompts
Clear prompts lead to accurate critic evaluation:
- Include acceptance criteria
- Be specific about requirements
- Define what "done" looks like
For Iteration Limits
Choose limits based on task complexity:
- Simple fixes: 2-3 iterations
- Medium features: 5-10 iterations
- Complex tasks: Consider breaking into smaller prompts
For Agent Selection
Consider critic thoroughness:
- More thorough critic = better feedback but slower
- Faster critic = quicker iterations but may miss issues
Implementation Details
The loop is implemented in codeloops-core/src/loop_runner.rs:
#![allow(unused)] fn main() { pub async fn run(&self, context: LoopContext) -> Result<LoopOutcome, LoopError> { loop { // Run actor let actor_output = self.actor.execute(&context.build_prompt()).await?; // Capture diff let diff = self.diff_capture.capture()?; // Run critic let critic_output = self.critic.evaluate(&actor_output, &diff).await?; // Parse decision match critic_output.decision { Decision::Done { summary, confidence } => { return Ok(LoopOutcome::Success { ... }); } Decision::Continue { feedback } => { context.set_feedback(feedback); continue; } Decision::Error { recovery } => { context.set_feedback(recovery); continue; } } } } }
Crate Structure
Codeloops is organized as a Rust workspace with multiple crates. This document details each crate's purpose and key types.
Overview
| Crate | Type | Purpose |
|---|---|---|
codeloops | Binary | CLI, API server, session viewer |
codeloops-core | Library | Loop orchestration |
codeloops-agent | Library | Agent abstraction |
codeloops-critic | Library | Critic evaluation |
codeloops-git | Library | Git diff capture |
codeloops-logging | Library | Logging and session writing |
codeloops-sessions | Library | Session reading and parsing |
Dependency Graph
┌─────────────────┐
│ codeloops │ (binary)
│ │
└────────┬────────┘
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────┐ ┌─────────────────┐
│ codeloops-core │ │codeloops- │ │ codeloops- │
│ │ │sessions │ │ logging │
└────────┬─────────┘ └──────────────┘ └─────────────────┘
│
┌────┴────┬─────────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌──────────────┐
│agent │ │critic │ │ git │
└────────┘ └────────┘ └──────────────┘
codeloops (Binary Crate)
Location: crates/codeloops/
The main CLI binary. Entry point for all user interactions.
Key Files
| File | Purpose |
|---|---|
main.rs | CLI entry, argument parsing, command dispatch |
config.rs | Configuration loading (global + project) |
sessions.rs | Session commands (list, show, diff, stats) |
init.rs | Interactive setup |
ui.rs | Web UI launcher |
api/mod.rs | API server router |
api/sessions.rs | Session API endpoints |
api/stats.rs | Statistics API endpoint |
api/sse.rs | Server-Sent Events for live updates |
Key Types
#![allow(unused)] fn main() { // CLI argument structure struct Cli { prompt: Option<String>, prompt_file: Option<PathBuf>, working_dir: Option<PathBuf>, agent: Option<AgentChoice>, actor_agent: Option<AgentChoice>, critic_agent: Option<AgentChoice>, max_iterations: Option<usize>, // ... } enum Commands { Run, Sessions(SessionsAction), Ui(UiArgs), Init, } // Configuration types struct GlobalConfig { defaults: DefaultsConfig, } struct ProjectConfig { agent: Option<String>, model: Option<String>, actor: Option<RoleConfig>, critic: Option<RoleConfig>, } }
codeloops-core (Library Crate)
Location: crates/codeloops-core/
Core loop orchestration logic.
Key Files
| File | Purpose |
|---|---|
lib.rs | Crate root, re-exports |
loop_runner.rs | Main loop execution |
context.rs | Loop context and iteration records |
outcome.rs | Loop outcomes (Success, Failed, etc.) |
error.rs | Error types |
Key Types
#![allow(unused)] fn main() { // Loop orchestrator pub struct LoopRunner { actor: Arc<dyn Agent>, critic: Arc<dyn Agent>, diff_capture: DiffCapture, logger: Logger, session_writer: SessionWriter, interrupt: Arc<AtomicBool>, } // Shared context across iterations pub struct LoopContext { pub prompt: String, pub working_dir: PathBuf, pub iteration: usize, pub history: Vec<IterationRecord>, pub max_iterations: Option<usize>, pub last_feedback: Option<String>, } // Record of one iteration pub struct IterationRecord { pub iteration_number: usize, pub actor_output: String, pub actor_stderr: String, pub actor_exit_code: i32, pub actor_duration_secs: f64, pub git_diff: String, pub git_files_changed: usize, pub critic_output: String, pub critic_decision: String, pub timestamp: DateTime<Utc>, } // Terminal states pub enum LoopOutcome { Success { iterations, summary, confidence, history, duration }, MaxIterationsReached { iterations, history, duration }, UserInterrupted { iterations, history, duration }, Failed { iterations, error, history, duration }, } }
Main Loop
#![allow(unused)] fn main() { impl LoopRunner { pub async fn run(&self, context: LoopContext) -> Result<LoopOutcome> { // Write session start self.session_writer.write_start(&context)?; loop { // Check interrupt if self.interrupt.load(Ordering::SeqCst) { return Ok(LoopOutcome::UserInterrupted { ... }); } // Check max iterations if let Some(max) = context.max_iterations { if context.iteration >= max { return Ok(LoopOutcome::MaxIterationsReached { ... }); } } // Execute iteration let result = self.run_iteration(&mut context).await?; match result { IterationResult::Done { summary, confidence } => { return Ok(LoopOutcome::Success { ... }); } IterationResult::Continue { feedback } => { context.last_feedback = Some(feedback); context.iteration += 1; } } } } } }
codeloops-agent (Library Crate)
Location: crates/codeloops-agent/
Agent abstraction layer.
Key Files
| File | Purpose |
|---|---|
lib.rs | Crate root, factory function |
traits.rs | Agent trait definition |
claude.rs | Claude Code agent |
opencode.rs | OpenCode agent |
cursor.rs | Cursor agent |
spawner.rs | Process spawning utilities |
output.rs | Output types |
Key Types
#![allow(unused)] fn main() { // Agent interface #[async_trait] pub trait Agent: Send + Sync { fn name(&self) -> &str; fn agent_type(&self) -> AgentType; async fn execute( &self, prompt: &str, config: &AgentConfig, ) -> Result<AgentOutput, AgentError>; async fn is_available(&self) -> bool; fn binary_path(&self) -> &Path; } // Agent types pub enum AgentType { ClaudeCode, OpenCode, Cursor, } // Agent configuration pub struct AgentConfig { pub working_dir: PathBuf, pub timeout: Option<Duration>, pub env_vars: HashMap<String, String>, pub model: Option<String>, } // Agent execution result pub struct AgentOutput { pub stdout: String, pub stderr: String, pub exit_code: i32, pub duration: Duration, } // Factory function pub fn create_agent(agent_type: AgentType) -> Box<dyn Agent> { match agent_type { AgentType::ClaudeCode => Box::new(ClaudeCodeAgent::new()), AgentType::OpenCode => Box::new(OpenCodeAgent::new()), AgentType::Cursor => Box::new(CursorAgent::new()), } } }
codeloops-critic (Library Crate)
Location: crates/codeloops-critic/
Critic evaluation and decision parsing.
Key Files
| File | Purpose |
|---|---|
lib.rs | Crate root, re-exports |
evaluator.rs | Critic evaluation logic |
decision.rs | Decision types and parsing |
prompts.rs | Prompt templates for critic |
Key Types
#![allow(unused)] fn main() { // Critic evaluator pub struct CriticEvaluator { agent: Arc<dyn Agent>, } // Decision types pub enum CriticDecision { Done { summary: String, confidence: f64, }, Continue { feedback: String, }, Error { recovery: String, }, } // Evaluation input pub struct EvaluationInput { pub prompt: String, pub actor_output: String, pub git_diff: String, pub iteration: usize, } }
codeloops-git (Library Crate)
Location: crates/codeloops-git/
Git operations for diff capture.
Key Files
| File | Purpose |
|---|---|
lib.rs | Crate root, re-exports |
diff.rs | Diff capture functionality |
status.rs | Git status utilities |
Key Types
#![allow(unused)] fn main() { // Diff capture utility pub struct DiffCapture { working_dir: PathBuf, baseline_commit: Option<String>, } // Diff result pub struct DiffSummary { pub diff: String, pub files_changed: usize, pub insertions: usize, pub deletions: usize, } impl DiffCapture { pub fn new(working_dir: PathBuf) -> Self; pub fn capture(&self) -> Result<DiffSummary>; pub fn set_baseline(&mut self); } }
codeloops-logging (Library Crate)
Location: crates/codeloops-logging/
Logging and session file writing.
Key Files
| File | Purpose |
|---|---|
lib.rs | Crate root, re-exports |
session.rs | Session JSONL writer |
events.rs | Log event types |
Key Types
#![allow(unused)] fn main() { // Session writer pub struct SessionWriter { file: File, id: String, } // Session line types #[derive(Serialize)] #[serde(tag = "type")] pub enum SessionLine { #[serde(rename = "session_start")] SessionStart { timestamp: DateTime<Utc>, prompt: String, working_dir: PathBuf, actor_agent: String, critic_agent: String, actor_model: Option<String>, critic_model: Option<String>, max_iterations: Option<usize>, }, #[serde(rename = "iteration")] Iteration { iteration_number: usize, actor_output: String, actor_stderr: String, actor_exit_code: i32, actor_duration_secs: f64, git_diff: String, git_files_changed: usize, critic_decision: String, feedback: Option<String>, timestamp: DateTime<Utc>, }, #[serde(rename = "session_end")] SessionEnd { outcome: String, iterations: usize, summary: Option<String>, confidence: Option<f64>, duration_secs: f64, timestamp: DateTime<Utc>, }, } impl SessionWriter { pub fn new(sessions_dir: &Path, prompt: &str) -> Result<Self>; pub fn write_start(&mut self, ...) -> Result<()>; pub fn write_iteration(&mut self, ...) -> Result<()>; pub fn write_end(&mut self, ...) -> Result<()>; } }
codeloops-sessions (Library Crate)
Location: crates/codeloops-sessions/
Session reading, parsing, and querying.
Key Files
| File | Purpose |
|---|---|
lib.rs | Crate root, re-exports |
store.rs | Session storage access |
parser.rs | JSONL parsing |
types.rs | Session types |
watcher.rs | File system watcher for live updates |
Key Types
#![allow(unused)] fn main() { // Session store pub struct SessionStore { sessions_dir: PathBuf, } // Full session pub struct Session { pub id: String, pub start: SessionStart, pub iterations: Vec<Iteration>, pub end: Option<SessionEnd>, } // Session summary (for listings) pub struct SessionSummary { pub id: String, pub timestamp: DateTime<Utc>, pub prompt_preview: String, pub working_dir: PathBuf, pub project: String, pub outcome: Option<String>, pub iterations: usize, pub duration_secs: Option<f64>, pub confidence: Option<f64>, pub actor_agent: String, pub critic_agent: String, } // Filter criteria pub struct SessionFilter { pub outcome: Option<String>, pub after: Option<DateTime<Utc>>, pub before: Option<DateTime<Utc>>, pub search: Option<String>, pub project: Option<String>, } // Statistics pub struct SessionStats { pub total_sessions: usize, pub success_rate: f64, pub avg_iterations: f64, pub avg_duration_secs: f64, pub sessions_over_time: Vec<DayCount>, pub by_project: Vec<ProjectStats>, } impl SessionStore { pub fn new() -> Result<Self>; pub fn list_sessions(&self, filter: &SessionFilter) -> Result<Vec<SessionSummary>>; pub fn load_session(&self, id: &str) -> Result<Session>; pub fn get_stats(&self) -> Result<SessionStats>; } }
Which Crate for Which Change?
| Change Type | Crate |
|---|---|
| New CLI command | codeloops |
| Configuration options | codeloops |
| Loop behavior | codeloops-core |
| New agent support | codeloops-agent |
| Critic evaluation logic | codeloops-critic |
| Git operations | codeloops-git |
| Log output format | codeloops-logging |
| Session parsing | codeloops-sessions |
| API endpoints | codeloops (api/) |
| Web UI | ui/ (separate) |
Data Flow
This document details how data flows through codeloops during execution.
High-Level Flow
User Input Processing Storage Output
─────────── ────────── ─────── ──────
prompt.md ────────▶ CLI Parser ─────────────────────────▶ Console
│
▼
Config Loader ───────────────────────▶ Console
│
▼
Agent Factory
│
▼
LoopRunner ──────────▶ SessionWriter ─▶ JSONL File
│ │
┌─────────┴─────────┐ │
▼ ▼ │
Actor Critic ─────────┤
│ │ │
▼ ▼ │
Git Diff ───────────────────────────┤
│
▼
Session File
Detailed Sequence: Running a Session
Phase 1: Initialization
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ User │ │ CLI │ │ Config │ │ Agents │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
│ codeloops │ │ │
│────────────────▶ │ │
│ │ │ │
│ │ load global │ │
│ │───────────────▶│ │
│ │ │ │
│ │◀───────────────│ │
│ │ │ │
│ │ load project │ │
│ │───────────────▶│ │
│ │ │ │
│ │◀───────────────│ │
│ │ │ │
│ │ merge configs │ │
│ │───────▶ │ │
│ │ │ │
│ │ create agents │ │
│ │────────────────────────────────▶│
│ │ │ │
│ │◀────────────────────────────────│
│ │ │ │
Phase 2: Session Start
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ CLI │ │ Runner │ │ Writer │ │ File │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
│ run(context) │ │ │
│───────────────▶│ │ │
│ │ │ │
│ │ create writer │ │
│ │───────────────▶│ │
│ │ │ │
│ │ │ create file │
│ │ │───────────────▶│
│ │ │ │
│ │ write_start() │ │
│ │───────────────▶│ │
│ │ │ │
│ │ │ append line │
│ │ │───────────────▶│
│ │ │ │
Phase 3: Actor Execution
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Runner │ │ Actor │ │ Process │ │ Git │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
│ build prompt │ │ │
│───────▶ │ │ │
│ │ │ │
│ execute() │ │ │
│───────────────▶│ │ │
│ │ │ │
│ │ spawn process │ │
│ │───────────────▶│ │
│ │ │ │
│ │ │ run agent CLI │
│ │ │ (claude/opencode)
│ │ │ │
│ │◀───────────────│ (stdout/stderr)
│ │ │ │
│◀───────────────│ AgentOutput │ │
│ │ │ │
│ capture diff │ │ │
│─────────────────────────────────────────────────▶│
│ │ │ │
│◀─────────────────────────────────────────────────│
│ │ │ │
Phase 4: Critic Evaluation
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Runner │ │ Critic │ │ Agent │ │ Parser │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
│ evaluate() │ │ │
│───────────────▶│ │ │
│ │ │ │
│ │ build prompt │ │
│ │───────▶ │ │
│ │ │ │
│ │ execute() │ │
│ │───────────────▶│ │
│ │ │ │
│ │◀───────────────│ AgentOutput │
│ │ │ │
│ │ parse response │ │
│ │───────────────────────────────▶│
│ │ │ │
│ │◀───────────────────────────────│
│ │ │ CriticDecision│
│◀───────────────│ │ │
│ │ │ │
Phase 5: Iteration Recording
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Runner │ │ Writer │ │ File │ │ Logger │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
│ write_iteration() │ │
│───────────────▶│ │ │
│ │ │ │
│ │ serialize JSON │ │
│ │───────▶ │ │
│ │ │ │
│ │ append line │ │
│ │───────────────▶│ │
│ │ │ │
│ log iteration │ │ │
│─────────────────────────────────────────────────▶│
│ │ │ │
│ │ │ │ console
│ │ │ │ output
│ │ │ │
Phase 6: Loop Decision
┌──────────┐
│ Runner │
└────┬─────┘
│
│ match decision
│───────▶
│
├─── DONE ───────────▶ write_end() ─▶ return Success
│
├─── CONTINUE ───────▶ set_feedback() ─▶ loop back
│
└─── ERROR ──────────▶ set_feedback() ─▶ loop back
Data Structures at Each Stage
Input Data
prompt: String
working_dir: PathBuf
config: {
actor_agent: AgentType,
critic_agent: AgentType,
actor_model: Option<String>,
critic_model: Option<String>,
max_iterations: Option<usize>,
}
After Actor Execution
actor_output: {
stdout: String,
stderr: String,
exit_code: i32,
duration: Duration,
}
After Diff Capture
diff_summary: {
diff: String,
files_changed: usize,
insertions: usize,
deletions: usize,
}
After Critic Evaluation
decision: {
type: "DONE" | "CONTINUE" | "ERROR",
summary?: String, // for DONE
confidence?: f64, // for DONE
feedback?: String, // for CONTINUE
recovery?: String, // for ERROR
}
Session File (JSONL)
Line 1 (SessionStart):
{
"type": "session_start",
"timestamp": "2025-01-27T15:30:45Z",
"prompt": "...",
"working_dir": "/path/to/project",
"actor_agent": "Claude Code",
"critic_agent": "Claude Code",
"actor_model": "sonnet",
"critic_model": null,
"max_iterations": 10
}
Line 2..N (Iteration):
{
"type": "iteration",
"iteration_number": 1,
"actor_output": "...",
"actor_stderr": "",
"actor_exit_code": 0,
"actor_duration_secs": 45.2,
"git_diff": "...",
"git_files_changed": 3,
"critic_decision": "CONTINUE",
"feedback": "...",
"timestamp": "2025-01-27T15:31:30Z"
}
Final Line (SessionEnd):
{
"type": "session_end",
"outcome": "success",
"iterations": 2,
"summary": "...",
"confidence": 0.95,
"duration_secs": 89.4,
"timestamp": "2025-01-27T15:32:14Z"
}
API Data Flow
List Sessions
Browser API SessionStore
│ │ │
│ GET /api/sessions │ │
│─────────────────────▶│ │
│ │ │
│ │ list_sessions(filter) │
│ │─────────────────────────▶│
│ │ │
│ │ │ read first/last
│ │ │ lines of each file
│ │ │
│ │◀─────────────────────────│
│ │ Vec<SessionSummary> │
│ │ │
│◀─────────────────────│ │
│ JSON response │ │
│ │ │
Get Session Detail
Browser API SessionStore
│ │ │
│ GET /api/sessions/id │ │
│─────────────────────▶│ │
│ │ │
│ │ load_session(id) │
│ │─────────────────────────▶│
│ │ │
│ │ │ read entire
│ │ │ JSONL file
│ │ │
│ │◀─────────────────────────│
│ │ Session │
│ │ │
│◀─────────────────────│ │
│ JSON response │ │
│ │ │
Live Updates (SSE)
Browser API SessionWatcher
│ │ │
│ GET /api/sessions/live (SSE) │
│─────────────────────▶│ │
│ │ │
│ │ subscribe() │
│ │─────────────────────────▶│
│ │ │
│◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ (connection held open) │
│ │ │
│ │ │ file change
│ │ │ detected
│ │◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│
│ │ SessionEvent │
│ │ │
│◀─────────────────────│ │
│ SSE event │ │
│ │ │
File System Layout
~/.config/codeloops/
└── config.toml # Global configuration
~/.local/share/codeloops/
├── sessions/ # Session storage
│ ├── 2025-01-27T15-30-45Z_a3f2c1.jsonl
│ ├── 2025-01-27T14-22-10Z_b5d3e2.jsonl
│ └── ...
└── ui/ # UI assets (optional)
├── index.html
├── assets/
└── ...
/path/to/project/
├── codeloops.toml # Project configuration (optional)
├── prompt.md # Default prompt file
└── ... # Project files
Concurrency Model
- Actor execution: Single-threaded, synchronous process spawn
- Critic execution: Single-threaded, synchronous process spawn
- Session writing: Append-only, no concurrent writers
- API server: Multi-threaded (tokio async runtime)
- SSE: Async broadcast channel for events
The loop itself is sequential (actor → diff → critic → decision → repeat), ensuring consistent state and predictable behavior.
Session Format
This document provides a complete reference for the codeloops session file format.
Overview
Sessions are stored as JSONL (JSON Lines) files. Each line is a valid JSON object representing one event in the session.
Location: ~/.local/share/codeloops/sessions/
Filename format: <timestamp>_<hash>.jsonl
Example: 2025-01-27T15-30-45Z_a3f2c1.jsonl
timestamp: ISO 8601 format with hyphens replacing colons (filesystem-safe)hash: First 6 characters of SHA256(prompt)
Line Types
Every line has a type field indicating its kind:
| Type | Occurrence | Description |
|---|---|---|
session_start | Exactly once | First line, session metadata |
iteration | Zero or more | One per actor-critic cycle |
session_end | Zero or once | Last line, final outcome |
SessionStart
The first line of every session file.
Schema
{
"type": "session_start",
"timestamp": "<ISO 8601 datetime>",
"prompt": "<string>",
"working_dir": "<path>",
"actor_agent": "<string>",
"critic_agent": "<string>",
"actor_model": "<string | null>",
"critic_model": "<string | null>",
"max_iterations": "<integer | null>"
}
Fields
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Always "session_start" |
timestamp | string | Yes | ISO 8601 datetime when session started |
prompt | string | Yes | Full task prompt text |
working_dir | string | Yes | Absolute path to working directory |
actor_agent | string | Yes | Actor agent name (e.g., "Claude Code") |
critic_agent | string | Yes | Critic agent name (e.g., "Claude Code") |
actor_model | string/null | Yes | Actor model name or null if not specified |
critic_model | string/null | Yes | Critic model name or null if not specified |
max_iterations | integer/null | Yes | Iteration limit or null if unlimited |
Example
{
"type": "session_start",
"timestamp": "2025-01-27T15:30:45Z",
"prompt": "Add input validation to the user registration endpoint.\n\nRequirements:\n- Email must be valid format\n- Password must be at least 8 characters",
"working_dir": "/home/user/projects/myapp",
"actor_agent": "Claude Code",
"critic_agent": "Claude Code",
"actor_model": "sonnet",
"critic_model": null,
"max_iterations": 10
}
Iteration
One line per actor-critic cycle. Sessions may have zero iterations (if the actor fails immediately).
Schema
{
"type": "iteration",
"iteration_number": "<integer>",
"actor_output": "<string>",
"actor_stderr": "<string>",
"actor_exit_code": "<integer>",
"actor_duration_secs": "<float>",
"git_diff": "<string>",
"git_files_changed": "<integer>",
"critic_decision": "<string>",
"feedback": "<string | null>",
"timestamp": "<ISO 8601 datetime>"
}
Fields
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Always "iteration" |
iteration_number | integer | Yes | 1-indexed iteration number |
actor_output | string | Yes | Actor's stdout output |
actor_stderr | string | Yes | Actor's stderr output |
actor_exit_code | integer | Yes | Actor's process exit code |
actor_duration_secs | float | Yes | Actor execution time in seconds |
git_diff | string | Yes | Unified diff of changes |
git_files_changed | integer | Yes | Number of files modified |
critic_decision | string | Yes | "DONE", "CONTINUE", or "ERROR" |
feedback | string/null | Yes | Critic feedback (null for DONE) |
timestamp | string | Yes | ISO 8601 datetime when iteration completed |
Critic Decision Values
| Value | Meaning |
|---|---|
DONE | Task complete, no more iterations |
CONTINUE | More work needed, feedback provided |
ERROR | Error occurred, recovery suggestion provided |
Example (CONTINUE)
{
"type": "iteration",
"iteration_number": 1,
"actor_output": "I've added email validation using a regex pattern. The validation is now in place for the registration endpoint.",
"actor_stderr": "",
"actor_exit_code": 0,
"actor_duration_secs": 45.2,
"git_diff": "diff --git a/src/api/users.rs b/src/api/users.rs\nindex 1234567..abcdefg 100644\n--- a/src/api/users.rs\n+++ b/src/api/users.rs\n@@ -10,6 +10,12 @@ pub async fn register(data: Json<RegisterRequest>) {\n+ if !is_valid_email(&data.email) {\n+ return Err(ApiError::BadRequest(\"Invalid email format\"));\n+ }\n",
"git_files_changed": 1,
"critic_decision": "CONTINUE",
"feedback": "Email validation is implemented correctly. However, password validation is missing. Please add:\n1. Minimum 8 character length check\n2. Error message for invalid passwords",
"timestamp": "2025-01-27T15:31:30Z"
}
Example (DONE)
{
"type": "iteration",
"iteration_number": 2,
"actor_output": "I've added password validation with minimum length check. Both email and password are now validated.",
"actor_stderr": "",
"actor_exit_code": 0,
"actor_duration_secs": 32.1,
"git_diff": "diff --git a/src/api/users.rs b/src/api/users.rs\n...",
"git_files_changed": 1,
"critic_decision": "DONE",
"feedback": null,
"timestamp": "2025-01-27T15:32:02Z"
}
Example (ERROR)
{
"type": "iteration",
"iteration_number": 1,
"actor_output": "",
"actor_stderr": "error[E0433]: failed to resolve: use of undeclared crate or module `validator`",
"actor_exit_code": 101,
"actor_duration_secs": 12.5,
"git_diff": "",
"git_files_changed": 0,
"critic_decision": "ERROR",
"feedback": "The actor encountered a compilation error. The `validator` crate is not in Cargo.toml. Please either:\n1. Add `validator = \"0.16\"` to Cargo.toml, or\n2. Implement validation manually without the external crate",
"timestamp": "2025-01-27T15:31:00Z"
}
SessionEnd
The last line of a completed session. May be absent if the session is still in progress or crashed.
Schema
{
"type": "session_end",
"outcome": "<string>",
"iterations": "<integer>",
"summary": "<string | null>",
"confidence": "<float | null>",
"duration_secs": "<float>",
"timestamp": "<ISO 8601 datetime>"
}
Fields
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Always "session_end" |
outcome | string | Yes | Session outcome (see values below) |
iterations | integer | Yes | Total number of iterations completed |
summary | string/null | Yes | Task completion summary (for success) |
confidence | float/null | Yes | Confidence score 0.0-1.0 (for success) |
duration_secs | float | Yes | Total session duration in seconds |
timestamp | string | Yes | ISO 8601 datetime when session ended |
Outcome Values
| Value | Description |
|---|---|
success | Critic returned DONE, task complete |
failed | Unrecoverable error occurred |
interrupted | User pressed Ctrl+C |
max_iterations_reached | Hit iteration limit without completion |
Example (success)
{
"type": "session_end",
"outcome": "success",
"iterations": 2,
"summary": "Input validation has been added to the user registration endpoint. Email addresses are validated using RFC 5321 compliant regex. Passwords require minimum 8 characters. Appropriate error messages are returned for invalid inputs.",
"confidence": 0.95,
"duration_secs": 89.4,
"timestamp": "2025-01-27T15:32:14Z"
}
Example (max_iterations_reached)
{
"type": "session_end",
"outcome": "max_iterations_reached",
"iterations": 10,
"summary": null,
"confidence": null,
"duration_secs": 482.3,
"timestamp": "2025-01-27T15:38:45Z"
}
Complete Example
A full session file:
{"type":"session_start","timestamp":"2025-01-27T15:30:45Z","prompt":"Fix the typo in greeting.rs","working_dir":"/home/user/myapp","actor_agent":"Claude Code","critic_agent":"Claude Code","actor_model":null,"critic_model":null,"max_iterations":null}
{"type":"iteration","iteration_number":1,"actor_output":"I found and fixed the typo. 'Helo' is now 'Hello'.","actor_stderr":"","actor_exit_code":0,"actor_duration_secs":23.4,"git_diff":"diff --git a/src/greeting.rs b/src/greeting.rs\n--- a/src/greeting.rs\n+++ b/src/greeting.rs\n@@ -1 +1 @@\n-println!(\"Helo, World!\");\n+println!(\"Hello, World!\");","git_files_changed":1,"critic_decision":"DONE","feedback":null,"timestamp":"2025-01-27T15:31:08Z"}
{"type":"session_end","outcome":"success","iterations":1,"summary":"Fixed the typo in greeting.rs. Changed 'Helo' to 'Hello'.","confidence":1.0,"duration_secs":23.4,"timestamp":"2025-01-27T15:31:08Z"}
Parsing Sessions
Using jq
# Get session prompt
head -1 session.jsonl | jq -r '.prompt'
# Get all critic decisions
jq -r 'select(.type == "iteration") | .critic_decision' session.jsonl
# Get final outcome
tail -1 session.jsonl | jq -r '.outcome'
# Count iterations
jq -s '[.[] | select(.type == "iteration")] | length' session.jsonl
Using Python
import json
def parse_session(filepath):
with open(filepath) as f:
lines = [json.loads(line) for line in f]
start = lines[0]
iterations = [l for l in lines if l.get("type") == "iteration"]
end = lines[-1] if lines[-1].get("type") == "session_end" else None
return {
"start": start,
"iterations": iterations,
"end": end,
}
Using Rust
#![allow(unused)] fn main() { use codeloops_sessions::{SessionStore, Session}; let store = SessionStore::new()?; let session: Session = store.load_session("2025-01-27T15-30-45Z_a3f2c1")?; println!("Prompt: {}", session.start.prompt); println!("Iterations: {}", session.iterations.len()); if let Some(end) = session.end { println!("Outcome: {}", end.outcome); } }
Session Discovery
Sessions are stored in a flat directory. To discover sessions:
# List all session files
ls ~/.local/share/codeloops/sessions/*.jsonl
# Find sessions by date
ls ~/.local/share/codeloops/sessions/2025-01-27*.jsonl
# Find sessions by prompt hash
ls ~/.local/share/codeloops/sessions/*_a3f2c1.jsonl
File Integrity
Sessions are written in append-only mode:
- Lines are written atomically (full line or nothing)
- No line is ever modified after writing
- Crashes leave the file in a consistent state
If a session file is missing session_end, the session was either:
- Interrupted before completion
- Crashed unexpectedly
- Still in progress
Configuration Schema
This document provides a complete reference for all configuration options.
Configuration Files
| File | Location | Scope |
|---|---|---|
| Global | ~/.config/codeloops/config.toml | All projects |
| Project | <working-dir>/codeloops.toml | Single project |
Precedence
Settings are resolved in order (highest priority first):
- CLI flags
- Project configuration
- Global configuration
- Built-in defaults
Global Configuration
File: ~/.config/codeloops/config.toml
Complete Schema
# Default settings applied to all sessions
[defaults]
# Default agent for both actor and critic roles
# Values: "claude", "opencode", "cursor"
# Default: "claude"
agent = "claude"
# Default model for both roles (optional)
# Value depends on agent (e.g., "sonnet", "opus", "gpt-4o")
# Default: none (uses agent default)
model = "sonnet"
# Actor-specific overrides (optional section)
[defaults.actor]
# Agent for actor role (overrides defaults.agent for actor)
agent = "opencode"
# Model for actor role (overrides defaults.model for actor)
model = "gpt-4o"
# Critic-specific overrides (optional section)
[defaults.critic]
# Agent for critic role (overrides defaults.agent for critic)
agent = "claude"
# Model for critic role (overrides defaults.model for critic)
model = "opus"
Section Reference
[defaults]
Base defaults for all sessions.
| Key | Type | Default | Description |
|---|---|---|---|
agent | string | "claude" | Default agent for both roles |
model | string | none | Default model for both roles |
[defaults.actor]
Override defaults for the actor role.
| Key | Type | Default | Description |
|---|---|---|---|
agent | string | inherit | Agent for actor |
model | string | inherit | Model for actor |
[defaults.critic]
Override defaults for the critic role.
| Key | Type | Default | Description |
|---|---|---|---|
agent | string | inherit | Agent for critic |
model | string | inherit | Model for critic |
Example Configurations
Minimal (use defaults):
[defaults]
agent = "claude"
With model:
[defaults]
agent = "claude"
model = "sonnet"
Different agents per role:
[defaults]
agent = "claude"
[defaults.actor]
agent = "opencode"
model = "gpt-4o"
[defaults.critic]
model = "opus"
Project Configuration
File: <working-dir>/codeloops.toml
Complete Schema
# Default agent for this project
# Values: "claude", "opencode", "cursor"
agent = "claude"
# Default model for this project (optional)
model = "sonnet"
# Actor-specific settings (optional section)
[actor]
agent = "opencode"
model = "gpt-4o"
# Critic-specific settings (optional section)
[critic]
agent = "claude"
model = "opus"
Field Reference
Root Level
| Key | Type | Default | Description |
|---|---|---|---|
agent | string | inherit | Default agent for this project |
model | string | inherit | Default model for this project |
[actor]
| Key | Type | Default | Description |
|---|---|---|---|
agent | string | inherit | Agent for actor |
model | string | inherit | Model for actor |
[critic]
| Key | Type | Default | Description |
|---|---|---|---|
agent | string | inherit | Agent for critic |
model | string | inherit | Model for critic |
Example Configurations
Simple project config:
agent = "claude"
Mixed agents:
[actor]
agent = "opencode"
model = "gpt-4o-mini"
[critic]
agent = "claude"
model = "sonnet"
Valid Values
Agent Values
| Value | Agent |
|---|---|
"claude" | Claude Code |
"opencode" | OpenCode |
"cursor" | Cursor |
Model Values
Model values depend on the agent:
Claude Code:
"sonnet"- Claude Sonnet"opus"- Claude Opus"haiku"- Claude Haiku
OpenCode:
"gpt-4o"- GPT-4o"gpt-4o-mini"- GPT-4o Mini- Other OpenAI models
Cursor:
- Uses Cursor's configured model
Resolution Examples
Example 1: No config files
Running codeloops with no config:
| Setting | Value | Source |
|---|---|---|
| Actor agent | claude | default |
| Critic agent | claude | default |
| Actor model | (none) | default |
| Critic model | (none) | default |
Example 2: Global config only
~/.config/codeloops/config.toml:
[defaults]
agent = "opencode"
model = "gpt-4o"
Running codeloops:
| Setting | Value | Source |
|---|---|---|
| Actor agent | opencode | global |
| Critic agent | opencode | global |
| Actor model | gpt-4o | global |
| Critic model | gpt-4o | global |
Example 3: Global + Project config
~/.config/codeloops/config.toml:
[defaults]
agent = "opencode"
codeloops.toml:
agent = "claude"
Running codeloops:
| Setting | Value | Source |
|---|---|---|
| Actor agent | claude | project |
| Critic agent | claude | project |
Example 4: CLI overrides all
~/.config/codeloops/config.toml:
[defaults]
agent = "opencode"
codeloops.toml:
agent = "claude"
Running codeloops --agent cursor:
| Setting | Value | Source |
|---|---|---|
| Actor agent | cursor | CLI |
| Critic agent | cursor | CLI |
Example 5: Per-role settings
~/.config/codeloops/config.toml:
[defaults]
agent = "claude"
model = "sonnet"
[defaults.actor]
agent = "opencode"
model = "gpt-4o"
Running codeloops:
| Setting | Value | Source |
|---|---|---|
| Actor agent | opencode | global.actor |
| Critic agent | claude | global.defaults |
| Actor model | gpt-4o | global.actor |
| Critic model | sonnet | global.defaults |
Environment Variables
| Variable | Description |
|---|---|
CODELOOPS_UI_DIR | Override UI assets directory |
NO_COLOR | Disable colored output when set |
Creating Configuration
Using init
codeloops init
Interactive prompts create ~/.config/codeloops/config.toml.
Manual creation
mkdir -p ~/.config/codeloops
cat > ~/.config/codeloops/config.toml << 'EOF'
[defaults]
agent = "claude"
EOF
Validating Configuration
Use --dry-run to see resolved configuration:
codeloops --dry-run
Output shows the effective settings without executing.
API Server Reference
This document provides a complete reference for the codeloops REST API.
Overview
The API server provides HTTP endpoints for accessing session data. It's started with codeloops ui and runs alongside the web UI.
Base URL: http://localhost:3100 (default)
Content-Type: application/json (unless noted)
Starting the Server
# Start with defaults
codeloops ui
# Custom API port
codeloops ui --api-port 4000
# Development mode
codeloops ui --dev
Endpoints
List Sessions
List all sessions with optional filtering.
Request
GET /api/sessions
Query Parameters
| Parameter | Type | Description |
|---|---|---|
outcome | string | Filter by outcome: success, failed, interrupted, max_iterations_reached |
after | string | Sessions after date (YYYY-MM-DD) |
before | string | Sessions before date (YYYY-MM-DD) |
search | string | Search in prompt text |
project | string | Filter by project name |
Response
[
{
"id": "2025-01-27T15-30-45Z_a3f2c1",
"timestamp": "2025-01-27T15:30:45Z",
"prompt_preview": "Add input validation to the user registration...",
"working_dir": "/home/user/projects/myapp",
"project": "myapp",
"outcome": "success",
"iterations": 2,
"duration_secs": 89.4,
"confidence": 0.95,
"actor_agent": "Claude Code",
"critic_agent": "Claude Code"
}
]
Response Fields
| Field | Type | Description |
|---|---|---|
id | string | Session identifier |
timestamp | string | ISO 8601 start time |
prompt_preview | string | First 256 chars of prompt |
working_dir | string | Absolute path |
project | string | Basename of working_dir |
outcome | string/null | Outcome or null if active |
iterations | integer | Number of iterations |
duration_secs | float/null | Total duration or null if active |
confidence | float/null | Confidence score (0-1) |
actor_agent | string | Actor agent name |
critic_agent | string | Critic agent name |
Example
# List all sessions
curl http://localhost:3100/api/sessions
# Filter by outcome
curl "http://localhost:3100/api/sessions?outcome=success"
# Filter by date range
curl "http://localhost:3100/api/sessions?after=2025-01-01&before=2025-01-31"
# Search prompts
curl "http://localhost:3100/api/sessions?search=authentication"
# Combine filters
curl "http://localhost:3100/api/sessions?outcome=success&project=myapp"
Get Session
Get detailed information for a single session.
Request
GET /api/sessions/{id}
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | string | Session identifier |
Response
{
"id": "2025-01-27T15-30-45Z_a3f2c1",
"start": {
"timestamp": "2025-01-27T15:30:45Z",
"prompt": "Add input validation to the user registration endpoint...",
"working_dir": "/home/user/projects/myapp",
"actor_agent": "Claude Code",
"critic_agent": "Claude Code",
"actor_model": "sonnet",
"critic_model": null,
"max_iterations": 10
},
"iterations": [
{
"iteration_number": 1,
"actor_output": "I've added email validation...",
"actor_stderr": "",
"actor_exit_code": 0,
"actor_duration_secs": 45.2,
"git_diff": "diff --git a/src/api/users.rs...",
"git_files_changed": 1,
"critic_decision": "CONTINUE",
"feedback": "Email validation looks good, but...",
"timestamp": "2025-01-27T15:31:30Z"
},
{
"iteration_number": 2,
"actor_output": "I've added password validation...",
"actor_stderr": "",
"actor_exit_code": 0,
"actor_duration_secs": 32.1,
"git_diff": "diff --git a/src/api/users.rs...",
"git_files_changed": 1,
"critic_decision": "DONE",
"feedback": null,
"timestamp": "2025-01-27T15:32:02Z"
}
],
"end": {
"outcome": "success",
"iterations": 2,
"summary": "Input validation has been added...",
"confidence": 0.95,
"duration_secs": 89.4,
"timestamp": "2025-01-27T15:32:14Z"
}
}
Example
curl http://localhost:3100/api/sessions/2025-01-27T15-30-45Z_a3f2c1
Get Session Diff
Get the cumulative git diff for a session.
Request
GET /api/sessions/{id}/diff
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | string | Session identifier |
Response
Content-Type: text/plain
diff --git a/src/api/users.rs b/src/api/users.rs
index 1234567..abcdefg 100644
--- a/src/api/users.rs
+++ b/src/api/users.rs
@@ -10,6 +10,18 @@ pub async fn register(data: Json<RegisterRequest>) {
+ // Validate email
+ if !is_valid_email(&data.email) {
+ return Err(ApiError::BadRequest("Invalid email format"));
+ }
+
+ // Validate password
+ if data.password.len() < 8 {
+ return Err(ApiError::BadRequest("Password must be at least 8 characters"));
+ }
Example
curl http://localhost:3100/api/sessions/2025-01-27T15-30-45Z_a3f2c1/diff
Get Statistics
Get aggregate statistics across all sessions.
Request
GET /api/stats
Response
{
"total_sessions": 47,
"success_rate": 0.787,
"avg_iterations": 2.3,
"avg_duration_secs": 94.2,
"sessions_over_time": [
{ "date": "2025-01-27", "count": 5 },
{ "date": "2025-01-26", "count": 8 },
{ "date": "2025-01-25", "count": 12 }
],
"by_project": [
{
"project": "myapp",
"total": 23,
"success_rate": 0.826
},
{
"project": "api-svc",
"total": 15,
"success_rate": 0.733
}
]
}
Response Fields
| Field | Type | Description |
|---|---|---|
total_sessions | integer | Total number of sessions |
success_rate | float | Success rate (0.0-1.0) |
avg_iterations | float | Average iterations per session |
avg_duration_secs | float | Average session duration |
sessions_over_time | array | Sessions grouped by date |
by_project | array | Statistics per project |
Example
curl http://localhost:3100/api/stats
Live Session Events (SSE)
Stream real-time session events using Server-Sent Events.
Request
GET /api/sessions/live
Response
Content-Type: text/event-stream
Events are sent as they occur:
event: session_created
data: {"id":"2025-01-27T16-00-00Z_b5d3e2","timestamp":"2025-01-27T16:00:00Z","prompt_preview":"Fix the bug...","actor_agent":"Claude Code","critic_agent":"Claude Code"}
event: session_updated
data: {"id":"2025-01-27T16-00-00Z_b5d3e2","iteration":1,"critic_decision":"CONTINUE"}
event: session_completed
data: {"id":"2025-01-27T16-00-00Z_b5d3e2","outcome":"success","iterations":2}
Event Types
| Event | Description |
|---|---|
session_created | New session started |
session_updated | Iteration completed |
session_completed | Session finished |
Example (JavaScript)
const eventSource = new EventSource('http://localhost:3100/api/sessions/live');
eventSource.addEventListener('session_created', (e) => {
const data = JSON.parse(e.data);
console.log('New session:', data.id);
});
eventSource.addEventListener('session_updated', (e) => {
const data = JSON.parse(e.data);
console.log('Session updated:', data.id, 'iteration:', data.iteration);
});
eventSource.addEventListener('session_completed', (e) => {
const data = JSON.parse(e.data);
console.log('Session completed:', data.id, 'outcome:', data.outcome);
});
Example (curl)
curl -N http://localhost:3100/api/sessions/live
Prompt Builder Endpoints
The Prompt Builder feature provides endpoints for creating prompt.md files through an AI-guided interview process.
Get Context
Get the current working directory context for the UI.
Request
GET /api/context
Response
{
"workingDir": "/home/user/projects/myapp",
"projectName": "myapp"
}
Response Fields
| Field | Type | Description |
|---|---|---|
workingDir | string | Absolute path to working directory |
projectName | string | Basename of working directory |
Example
curl http://localhost:3100/api/context
Create Prompt Session
Start a new prompt building session.
Request
POST /api/prompt-session
Request Body
{
"workType": "feature",
"workingDir": "/home/user/projects/myapp"
}
Request Fields
| Field | Type | Description |
|---|---|---|
workType | string | Work type: feature, defect, risk, debt, or custom |
workingDir | string | Absolute path to working directory |
Response
{
"sessionId": "prompt-abc123-def456"
}
Example
curl -X POST http://localhost:3100/api/prompt-session \
-H "Content-Type: application/json" \
-d '{"workType": "feature", "workingDir": "/home/user/projects/myapp"}'
Send Message
Send a message to the prompt session and receive AI response via SSE.
Request
POST /api/prompt-session/{sessionId}/message
Path Parameters
| Parameter | Type | Description |
|---|---|---|
sessionId | string | Session identifier from create session |
Request Body
{
"content": "I want to add user authentication"
}
Response
Content-Type: text/event-stream
data: {"content":"Let's"}
data: {"content":" design"}
data: {"content":" this"}
data: {"content":" feature."}
data: {"promptDraft":"# Feature: User Authentication\n\n## Problem\n..."}
data: [DONE]
SSE Events
| Event Data | Description |
|---|---|
{"content": "..."} | Streaming text chunk from AI |
{"promptDraft": "..."} | Updated prompt.md draft |
[DONE] | End of stream marker |
Special Messages
Send __INIT__ as the content to get the initial AI greeting for the selected work type.
Example
# Get initial greeting
curl -X POST "http://localhost:3100/api/prompt-session/prompt-abc123/message" \
-H "Content-Type: application/json" \
-d '{"content": "__INIT__"}'
# Send user message
curl -X POST "http://localhost:3100/api/prompt-session/prompt-abc123/message" \
-H "Content-Type: application/json" \
-d '{"content": "I want to add input validation for the API"}'
Save Prompt
Save the prompt.md content to disk.
Request
POST /api/prompt/save
Request Body
{
"workingDir": "/home/user/projects/myapp",
"content": "# Feature: Input Validation\n\n## Problem\n..."
}
Request Fields
| Field | Type | Description |
|---|---|---|
workingDir | string | Directory to save prompt.md |
content | string | Content to write to prompt.md |
Response
{
"path": "/home/user/projects/myapp/prompt.md"
}
Error Responses
| Status | Description |
|---|---|
| 500 | Write failed (permission denied, disk full, etc.) |
Example
curl -X POST http://localhost:3100/api/prompt/save \
-H "Content-Type: application/json" \
-d '{"workingDir": "/home/user/projects/myapp", "content": "# My Prompt\n\nContent here..."}'
Error Responses
404 Not Found
{
"error": "Session not found",
"id": "invalid-session-id"
}
400 Bad Request
{
"error": "Invalid filter parameter",
"details": "Invalid date format for 'after' parameter"
}
500 Internal Server Error
{
"error": "Internal server error",
"details": "Failed to read session file"
}
CORS
The API server allows cross-origin requests from localhost origins by default, enabling the separate UI dev server to make requests.
Headers included:
Access-Control-Allow-Origin: *(in dev mode)Access-Control-Allow-Methods: GET, OPTIONSAccess-Control-Allow-Headers: Content-Type
Health Check
Check if the API server is running:
curl http://localhost:3100/api/sessions
A successful response (even an empty array []) indicates the server is healthy.
Rate Limiting
There is no built-in rate limiting. The API is designed for local use only.
Authentication
There is no authentication. The API is intended for local development use.
Programmatic Usage
Python
import requests
BASE_URL = "http://localhost:3100"
# List sessions
sessions = requests.get(f"{BASE_URL}/api/sessions").json()
# Get session detail
session = requests.get(f"{BASE_URL}/api/sessions/{sessions[0]['id']}").json()
# Get statistics
stats = requests.get(f"{BASE_URL}/api/stats").json()
JavaScript/TypeScript
const BASE_URL = "http://localhost:3100";
// List sessions
const sessions = await fetch(`${BASE_URL}/api/sessions`).then(r => r.json());
// Get session detail
const session = await fetch(`${BASE_URL}/api/sessions/${sessions[0].id}`).then(r => r.json());
// Get statistics
const stats = await fetch(`${BASE_URL}/api/stats`).then(r => r.json());
curl
# List sessions with jq formatting
curl -s http://localhost:3100/api/sessions | jq
# Get specific session
curl -s http://localhost:3100/api/sessions/2025-01-27T15-30-45Z_a3f2c1 | jq
# Get diff
curl http://localhost:3100/api/sessions/2025-01-27T15-30-45Z_a3f2c1/diff
Development Setup
This guide covers how to set up a development environment for contributing to codeloops.
Prerequisites
Cloning the Repository
git clone https://github.com/silvabyte/codeloops
cd codeloops
Building
Debug Build
cargo build
Binary location: ./target/debug/codeloops
Release Build
cargo build --release
Binary location: ./target/release/codeloops
Running Tests
All Tests
cargo test --workspace
Specific Crate
cargo test -p codeloops-core
cargo test -p codeloops-agent
cargo test -p codeloops-sessions
With Output
cargo test --workspace -- --nocapture
Running the CLI Locally
# Show help
cargo run -- --help
# Run with a prompt
cargo run -- --prompt "Fix the typo"
# List sessions
cargo run -- sessions list
# Start the UI
cargo run -- ui --dev
Frontend Development
Install Dependencies
cd ui
bun install
Development Server
# From project root
cargo run -- ui --dev
# Or from ui directory
cd ui && bun dev
Build
cd ui
bun run build
Type Checking
cd ui
bun run typecheck
Code Style
Rust
The project uses standard Rust formatting:
# Format code
cargo fmt
# Check formatting
cargo fmt --check
# Run clippy
cargo clippy --workspace
TypeScript
cd ui
bun run lint
bun run format
Project Structure
codeloops/
├── crates/
│ ├── codeloops/ # CLI binary
│ ├── codeloops-core/ # Loop orchestration
│ ├── codeloops-agent/ # Agent abstraction
│ ├── codeloops-critic/ # Critic evaluation
│ ├── codeloops-git/ # Git operations
│ ├── codeloops-logging/ # Logging and session writing
│ └── codeloops-sessions/ # Session reading
├── ui/ # Web UI (React)
├── docs/ # Documentation (mdbook)
├── Cargo.toml # Workspace manifest
└── README.md
Issue Tracking
This project uses beads for issue tracking. Issues are stored in .beads/ directory.
Common Commands
# List ready issues (no blockers)
bd ready
# Show all open issues
bd list --status=open
# Create a new issue
bd create --title="Description" --type=task
# Start working on an issue
bd update <id> --status=in_progress
# Close an issue
bd close <id>
# Sync with remote
bd sync
See AGENTS.md for detailed workflow.
Making Changes
1. Find or Create an Issue
bd ready # See available work
bd show <id> # Review details
Or create a new issue:
bd create --title="Add feature X" --type=feature
2. Create a Branch
git checkout -b feature/my-feature
3. Make Changes
Edit files, add tests, update documentation.
4. Test
cargo test --workspace
cargo fmt --check
cargo clippy --workspace
5. Commit
git add <files>
git commit -m "feat: add feature X"
Follow Conventional Commits:
feat:- New featurefix:- Bug fixdocs:- Documentationrefactor:- Code refactoringtest:- Testschore:- Maintenance
6. Push and Create PR
git push -u origin feature/my-feature
gh pr create
Documentation
Building Docs
# Install mdbook
cargo install mdbook
# Build
mdbook build docs
# Serve locally
mdbook serve docs --open
Rustdoc
# Build API documentation
cargo doc --no-deps --open
Debugging
Verbose Logging
RUST_LOG=debug cargo run -- --prompt "test"
Session Files
Sessions are stored in ~/.local/share/codeloops/sessions/. Inspect them with:
cat ~/.local/share/codeloops/sessions/*.jsonl | jq
Testing Without Agents
For testing changes that don't require actual agent execution, you can mock the agent:
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use crate::Agent; struct MockAgent; impl Agent for MockAgent { // ... implement with test data } } }
Common Development Tasks
Adding a New CLI Option
- Edit
crates/codeloops/src/main.rs - Add the option to the
Clistruct with clap attributes - Handle the option in the command logic
- Update CLI reference documentation
Adding a Configuration Option
- Edit
crates/codeloops/src/config.rs - Add field to appropriate struct (GlobalConfig or ProjectConfig)
- Update resolution logic
- Update configuration documentation
Adding a Session Field
- Edit
crates/codeloops-logging/src/session.rs(SessionLine enum) - Edit
crates/codeloops-sessions/src/types.rs(Session structs) - Update parser in
crates/codeloops-sessions/src/parser.rs - Update session format documentation
Adding an API Endpoint
- Create handler in
crates/codeloops/src/api/ - Add route in
crates/codeloops/src/api/mod.rs - Add TypeScript types in
ui/src/api/types.ts - Add client function in
ui/src/api/client.ts - Update API documentation
Getting Help
- Open an issue on GitHub
- Check existing issues for similar problems
- Review the documentation
Contributor License
By contributing, you agree that your contributions will be licensed under the same license as the project.
Adding New Agents
This guide walks through adding support for a new coding agent to codeloops.
Overview
To add a new agent, you need to:
- Implement the
Agenttrait - Add the agent type to the
AgentTypeenum - Update the agent factory function
- Add CLI support
- Update documentation and tests
Step 1: Implement the Agent Trait
Create a new file in crates/codeloops-agent/src/agents/:
#![allow(unused)] fn main() { // crates/codeloops-agent/src/agents/aider.rs use crate::{Agent, AgentConfig, AgentError, AgentOutput, AgentType, OutputCallback}; use async_trait::async_trait; use std::path::{Path, PathBuf}; use std::process::Stdio; use tokio::process::Command; use tokio::io::{AsyncBufReadExt, BufReader}; /// Aider coding agent implementation. pub struct AiderAgent { binary_path: PathBuf, } impl AiderAgent { pub fn new() -> Self { Self { binary_path: PathBuf::from("aider"), } } } #[async_trait] impl Agent for AiderAgent { fn name(&self) -> &str { "Aider" } fn agent_type(&self) -> AgentType { AgentType::Aider } // Note: execute() has a default implementation that calls execute_with_callback(), // so you only need to implement execute_with_callback(). async fn execute_with_callback( &self, prompt: &str, config: &AgentConfig, on_output: Option<OutputCallback>, ) -> Result<AgentOutput, AgentError> { let start = std::time::Instant::now(); // Build command let mut cmd = Command::new(&self.binary_path); cmd.current_dir(&config.working_dir); // Add agent-specific arguments cmd.arg("--message").arg(prompt); cmd.arg("--yes"); // Auto-confirm changes cmd.arg("--no-git"); // Let codeloops handle git // Add model if specified if let Some(model) = &config.model { cmd.arg("--model").arg(model); } // Set up I/O cmd.stdin(Stdio::null()); cmd.stdout(Stdio::piped()); cmd.stderr(Stdio::piped()); // Spawn process let mut child = cmd.spawn().map_err(|e| { AgentError::SpawnError(format!("Failed to spawn aider: {}", e)) })?; // Capture output let stdout = child.stdout.take().unwrap(); let stderr = child.stderr.take().unwrap(); let mut stdout_reader = BufReader::new(stdout).lines(); let mut stderr_reader = BufReader::new(stderr).lines(); let mut stdout_output = String::new(); let mut stderr_output = String::new(); // Read output streams loop { tokio::select! { line = stdout_reader.next_line() => { match line { Ok(Some(line)) => { if let Some(ref callback) = on_output { callback(&line); } stdout_output.push_str(&line); stdout_output.push('\n'); } Ok(None) => break, Err(e) => { stderr_output.push_str(&format!("Read error: {}\n", e)); break; } } } line = stderr_reader.next_line() => { if let Ok(Some(line)) = line { stderr_output.push_str(&line); stderr_output.push('\n'); } } } } // Wait for process let status = child.wait().await.map_err(|e| { AgentError::ExecutionError(format!("Failed to wait for aider: {}", e)) })?; let duration = start.elapsed(); Ok(AgentOutput { stdout: stdout_output, stderr: stderr_output, exit_code: status.code().unwrap_or(-1), duration, }) } async fn is_available(&self) -> bool { Command::new(&self.binary_path) .arg("--version") .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .await .map(|s| s.success()) .unwrap_or(false) } fn binary_path(&self) -> &Path { &self.binary_path } } impl Default for AiderAgent { fn default() -> Self { Self::new() } } }
Step 2: Add the Agent Type
Edit crates/codeloops-agent/src/lib.rs:
#![allow(unused)] fn main() { /// Supported agent types. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AgentType { ClaudeCode, OpenCode, Cursor, Aider, // Add new variant } impl AgentType { pub fn display_name(&self) -> &'static str { match self { AgentType::ClaudeCode => "Claude Code", AgentType::OpenCode => "OpenCode", AgentType::Cursor => "Cursor", AgentType::Aider => "Aider", // Add display name } } } }
Step 3: Update the Factory Function
Edit crates/codeloops-agent/src/lib.rs:
#![allow(unused)] fn main() { mod agents; pub use agents::aider::AiderAgent; // Export new agent pub use agents::claude::ClaudeCodeAgent; pub use agents::cursor::CursorAgent; pub use agents::opencode::OpenCodeAgent; /// Create an agent instance from the agent type. pub fn create_agent(agent_type: AgentType) -> Box<dyn Agent> { match agent_type { AgentType::ClaudeCode => Box::new(ClaudeCodeAgent::new()), AgentType::OpenCode => Box::new(OpenCodeAgent::new()), AgentType::Cursor => Box::new(CursorAgent::new()), AgentType::Aider => Box::new(AiderAgent::new()), // Add factory case } } }
Don't forget to add the module declaration:
#![allow(unused)] fn main() { // crates/codeloops-agent/src/agents/mod.rs pub mod aider; pub mod claude; pub mod cursor; pub mod opencode; }
Step 4: Add CLI Support
Edit crates/codeloops/src/main.rs:
#![allow(unused)] fn main() { /// Agent choice for CLI arguments. #[derive(Debug, Clone, Copy, ValueEnum)] pub enum AgentChoice { Claude, Opencode, Cursor, Aider, // Add new variant } impl From<AgentChoice> for AgentType { fn from(choice: AgentChoice) -> Self { match choice { AgentChoice::Claude => AgentType::ClaudeCode, AgentChoice::Opencode => AgentType::OpenCode, AgentChoice::Cursor => AgentType::Cursor, AgentChoice::Aider => AgentType::Aider, // Add mapping } } } }
Step 5: Update Configuration
Edit crates/codeloops/src/config.rs to recognize the new agent string:
#![allow(unused)] fn main() { fn parse_agent(s: &str) -> Option<AgentType> { match s.to_lowercase().as_str() { "claude" => Some(AgentType::ClaudeCode), "opencode" => Some(AgentType::OpenCode), "cursor" => Some(AgentType::Cursor), "aider" => Some(AgentType::Aider), // Add parsing _ => None, } } }
Step 6: Write Tests
Create test file crates/codeloops-agent/src/agents/aider_test.rs:
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; use std::path::PathBuf; #[test] fn test_agent_name() { let agent = AiderAgent::new(); assert_eq!(agent.name(), "Aider"); } #[test] fn test_agent_type() { let agent = AiderAgent::new(); assert_eq!(agent.agent_type(), AgentType::Aider); } #[test] fn test_binary_path() { let agent = AiderAgent::new(); assert_eq!(agent.binary_path(), Path::new("aider")); } #[tokio::test] async fn test_execute_not_available() { // Test behavior when agent is not installed let agent = AiderAgent { binary_path: PathBuf::from("/nonexistent/aider"), }; assert!(!agent.is_available().await); } } }
Step 7: Update Documentation
CLI Reference
Update docs/src/user-guide/cli-reference.md:
Agent values: `claude`, `opencode`, `cursor`, `aider`
Agents Guide
Update docs/src/user-guide/agents.md:
## Supported Agents
| Agent | CLI Value | Binary | Description |
|-------|-----------|--------|-------------|
| Claude Code | `claude` | `claude` | Anthropic's Claude-powered coding agent |
| OpenCode | `opencode` | `opencode` | Multi-model coding agent |
| Cursor | `cursor` | `cursor` | Cursor IDE's agent CLI |
| Aider | `aider` | `aider` | AI pair programming in your terminal |
### Aider
Aider is an AI pair programming tool that works in your terminal.
**Binary**: `aider`
**Strengths**:
- Works with many LLM providers
- Good for iterative editing
- Supports multiple files
**Installation**: Visit [aider.chat](https://aider.chat/)
Configuration Schema
Update docs/src/reference/config-schema.md:
### Agent Values
| Value | Agent |
|-------|-------|
| `"claude"` | Claude Code |
| `"opencode"` | OpenCode |
| `"cursor"` | Cursor |
| `"aider"` | Aider |
Step 8: Test the Integration
# Build
cargo build
# Run tests
cargo test -p codeloops-agent
# Test CLI
./target/debug/codeloops --agent aider --dry-run
# Test actual execution (requires aider installed)
./target/debug/codeloops --agent aider --prompt "Fix typo"
Agent Implementation Tips
Handle Different Output Formats
Agents may produce output in different formats. Normalize output for the critic:
#![allow(unused)] fn main() { fn normalize_output(&self, raw_output: &str) -> String { // Remove agent-specific noise // Standardize formatting raw_output.to_string() } }
Handle Model Selection
Different agents support models differently:
#![allow(unused)] fn main() { // Some agents use --model cmd.arg("--model").arg(model); // Some use environment variables cmd.env("MODEL_NAME", model); // Some use different flag names cmd.arg("-m").arg(model); }
Handle Working Directory
Agents should run in the specified working directory:
#![allow(unused)] fn main() { cmd.current_dir(&config.working_dir); }
Handle Prompts
Different agents accept prompts differently:
#![allow(unused)] fn main() { // Via argument cmd.arg("--message").arg(prompt); // Via stdin cmd.stdin(Stdio::piped()); // Then write prompt to stdin after spawn // Via file let prompt_file = config.working_dir.join(".prompt"); std::fs::write(&prompt_file, prompt)?; cmd.arg("--prompt-file").arg(&prompt_file); }
Handle Errors Gracefully
#![allow(unused)] fn main() { if !status.success() { // Don't fail immediately - let the critic handle it // The critic can provide recovery suggestions } }
Checklist
Before submitting your PR:
- Agent trait implemented
- AgentType enum updated
- Factory function updated
- CLI AgentChoice added
- Configuration parsing updated
- Tests written
- Documentation updated (agents guide, CLI reference, config schema)
-
All tests pass (
cargo test --workspace) -
Code formatted (
cargo fmt) -
No clippy warnings (
cargo clippy --workspace)
Background & Inspiration
Codeloops has gone through multiple iterations since its inception. This page covers the project's origins, the challenges faced, and the key insight that shaped its current architecture.
Origins (March/April 2025)
Codeloops started in March/April 2025, inspired by the actor-critic model from neuroscience. The idea came from reading Max Bennett's A Brief History of Intelligence, which introduced concepts about temporal difference learning and how the brain uses separate systems for action and evaluation.
The problem was clear: coding agents produce what could be called "code slop"—unstructured, error-prone output. Memory gaps cause agents to forget APIs they've just created. Context lapses lead them to ignore prior configurations. For serious software development, the results were unreliable.
The original architecture had three components:
- The Actor: The coding agent generates code/plans
- The Critic: An LLM evaluates outputs for accuracy and best practices
- The Knowledge Graph: Stores feedback and context for future iterations
Early testing used Anthropic's Haiku 3.5 as the critic, paired with Augment's agent. The approach showed promise—a week of testing cost under $0.40.
The Architectural Challenge
But where should this feedback loop live? The initial assumption was that the loop needed to be embedded within the coding agent layer itself—perhaps as a plugin, extension, or modification to the agent's core behavior.
This approach proved frustrating. Coding agents have their own architectures, extension points, and constraints. Trying to inject a feedback loop at that level meant fighting against each agent's design rather than working with it. The project stalled.
Enter Ralph Wiggum (2026)
The Ralph Wiggum loop changed everything.
Ralph Wiggum is a technique popularized by Geoffrey Huntley that implements a simple but powerful idea: wrap your coding agent in an external while loop that keeps running until a task is complete. The agent doesn't need to know it's in a loop—it just executes, and the external harness decides whether to continue.
The brilliance of Ralph Wiggum was showing that the feedback loop belongs outside the agent, not inside it. The agent is a black box that takes a prompt and produces output. The orchestration layer—the loop, the evaluation, the decision to continue—lives above it.
This insight unlocked codeloops. In January 2026, the project was revived with a new architecture. Instead of modifying agents, codeloops wraps them. Any agent with a CLI becomes a valid actor or critic. The loop is agent-agnostic because it operates at a higher architectural layer.
How Codeloops Differs
While Ralph Wiggum uses naive persistence (loop until a completion promise appears), codeloops adds structure:
| Aspect | Ralph Wiggum | Codeloops |
|---|---|---|
| Evaluation | Self-evaluation (agent decides when done) | Explicit critic agent reviews the actor's work |
| Feedback source | Context window + file state | Git diff + stdout/stderr |
| Roles | Single agent | Separate actor and critic (can be different agents) |
| Decision protocol | Completion promise string | Structured DONE/CONTINUE/ERROR response |
The actor-critic separation means the agent doing the work isn't the same one judging it. This provides a second perspective and catches issues the actor might miss.
Acknowledgments
Thanks to Max Bennett for A Brief History of Intelligence, which introduced the actor-critic concepts from neuroscience that inspired this project.
Thanks to Geoffrey Huntley for the Ralph Wiggum technique. It provided the key architectural insight that made codeloops viable: the feedback loop belongs in the orchestration layer, not the agent layer.
Further Reading
- Improving Coding Agents: An Early Look at Codeloops - Original blog post from May 2025
- Ralph Wiggum - Original Post
- The Actor-Critic Loop - How codeloops implements its feedback loop
- Architecture Overview - System design and component structure
Best Practices
This guide covers best practices for getting the most out of codeloops, based on patterns observed across real sessions.
The Planning Workflow
The most successful codeloops sessions follow a two-phase workflow:
Phase 1: Interactive Exploration
Use your coding agent interactively to explore and plan:
# Start your agent
opencode # or claude
# Explore the problem
> Analyze the authentication flow and identify where rate limiting should be added
# Generate a detailed prompt
> /promptmd
Phase 2: Disciplined Execution
Run codeloops with the generated prompt:
codeloops
This workflow leverages your agent's interactive capabilities for exploration, then uses codeloops' actor-critic loop for structured, self-correcting execution.
What Makes Prompts Succeed in One Iteration
Analysis of successful single-iteration sessions reveals these patterns:
Include Root Cause Analysis (for bugs)
Don't just describe symptoms—explain why it's happening:
Good:
## Problem
The daemon logs spam this error every ~6 seconds:
`Telegram poll error: Ctrl-C signal handler already registered`
## Root Cause
The daemon registers a `ctrlc::set_handler` at `crates/butler/src/commands/daemon.rs:79`.
Then it calls `telegram::poll()` which also calls `ctrlc::set_handler()` at line 40.
The `ctrlc` crate only allows one global handler—the second registration fails.
## Fix
Skip the ctrlc registration in `poll()` when called with `once: true`.
## Files
- `crates/butler/src/commands/telegram.rs` — lines 38-42
- `crates/butler/src/commands/daemon.rs` — lines 79-81, 91
Provide Exact File Paths and Line Numbers
Specificity eliminates guesswork:
Good:
In `run.rs`, guard against empty output before sending to Telegram:
File: `crates/butler/src/commands/run.rs` — lines 100-103
Change:
```rust
OutputTarget::Telegram => {
let msg = output_result.stdout.trim();
if msg.is_empty() {
eprintln!("Warning: playbook produced no output");
} else {
tg.send_message(msg).await?;
}
}
### Include Schema Definitions for Data Changes
When adding database tables or data structures, include the full schema:
**Good:**
```markdown
## Database Schema
```sql
CREATE TABLE tasks (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
trigger_at TEXT,
created_at TEXT NOT NULL
);
CREATE INDEX idx_tasks_status_trigger ON tasks(status, trigger_at);
### Structure Complex Tasks in Phases
Break large features into numbered, dependent tasks:
**Good:**
```markdown
## Phase 1: Foundation Setup
### Task 1.1: Create the database module
Location: `crates/butler-db/`
- Implement `open_db()` with WAL mode
- Add migration helpers
### Task 1.2: Add state helpers
- Implement `get_state(conn, table, key)`
- Implement `set_state(conn, table, key, value)`
## Phase 2: Migration
### Task 2.1: Migrate telegram state
- Replace JSON file reads with database queries
- Update `crates/butler/src/commands/telegram.rs`
What Causes Multiple Iterations
Sessions that require 2-3 iterations typically have these gaps:
Missing Edge Cases
The critic catches issues the prompt didn't mention:
- Double initialization of global state
- Empty output handling
- Error propagation paths
- Clippy warnings in new code
Prevention: Include a "Validation Checklist" in your prompt:
## Validation Checklist
- [ ] No clippy warnings in modified files
- [ ] Error cases return appropriate error types
- [ ] Empty/null inputs are handled
- [ ] New code has test coverage
Ambiguous "Done" Criteria
When the critic can't determine if requirements are met, it requests more changes.
Prevention: Use explicit acceptance criteria with checkboxes:
## Acceptance Criteria
- [ ] `bun scripts/generate-route-config.ts --check` returns exit code 0 when in sync
- [ ] `bun scripts/generate-route-config.ts --check` returns exit code 1 when out of sync
- [ ] Pre-commit hook fails with clear error message when route config is stale
- [ ] All existing tests pass
Writing Effective Prompts
Be Specific
Vague prompts lead to vague results:
Bad:
Improve the code.
Good:
Refactor the `calculate_total` function in src/cart.rs to:
1. Use iterators instead of manual loops
2. Handle the case where the cart is empty
3. Add documentation comments
Include Acceptance Criteria
Tell the critic what "done" looks like:
## Task
Add rate limiting to the API endpoint.
## Acceptance Criteria
- [ ] Limit to 100 requests per minute per IP
- [ ] Return 429 status code when limit exceeded
- [ ] Include Retry-After header in 429 responses
- [ ] Log rate limit violations
Provide Context
Reference specific files and functions:
Add pagination to the `list_users` function in src/api/users.rs.
The function currently returns all users. Change it to:
- Accept `page` and `per_page` query parameters
- Default to page 1 with 20 items per page
- Return total count in the response
Related types are in src/models/user.rs and src/api/types.rs.
One Task Per Prompt
Don't combine unrelated tasks:
Bad:
Fix the login bug and add a new dashboard page and update the README.
Good: Create separate prompts for each:
prompt-login.md: Fix the login bugprompt-dashboard.md: Add dashboard pageprompt-readme.md: Update README
Structuring Complex Tasks
Break Down Large Features
Instead of one large prompt:
Implement complete user authentication with registration, login,
logout, password reset, and email verification.
Create a series of prompts:
- User registration
- User login
- Logout
- Password reset (request)
- Password reset (confirm)
- Email verification
Run them sequentially, each building on the previous.
Use Iteration Limits Wisely
Set limits based on task complexity:
| Task Type | Suggested Limit |
|---|---|
| Simple fix | 2-3 |
| Small feature | 5 |
| Medium feature | 10 |
| Complex task | Consider breaking down |
codeloops --max-iterations 5
Agent Selection
Same Agent for Both Roles
Use the same agent when:
- Starting out (simpler configuration)
- The agent performs well for your use case
- You want predictable behavior
codeloops --agent claude
Different Agents per Role
Mix agents when:
- You want fast iteration with thorough review
- Different agents excel at different aspects
# Fast actor, thorough critic
codeloops --actor-agent opencode --critic-agent claude
Agent-Task Matching
| Task Type | Recommended |
|---|---|
| Complex refactoring | Claude (strong reasoning) |
| Simple fixes | OpenCode (fast) |
| Cursor-centric workflow | Cursor |
Working with the Feedback Loop
Trust the Critic
The critic provides valuable feedback. If the loop continues, review the feedback to understand what's missing.
Know When to Cancel
Cancel and reformulate if:
- Multiple iterations produce similar results
- The critic's feedback seems stuck
- The task scope may be wrong
Press Ctrl+C to cancel, then revise your prompt.
Learn from Sessions
Review completed sessions to improve future prompts:
codeloops sessions show
Look for patterns:
- What feedback was given?
- How many iterations did similar tasks take?
- What made some tasks succeed faster?
CI/CD Integration
JSON Output
Use JSON output for machine parsing:
codeloops --json-output > result.json
Exit Codes
Use exit codes in scripts:
codeloops --max-iterations 3 || echo "Task incomplete"
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Max iterations reached |
| 2 | Failed |
| 130 | Interrupted |
Configuration Files
Use project configuration for CI consistency:
# codeloops.toml
agent = "claude"
max_iterations = 5
log_format = "json"
Pipeline Example
# .github/workflows/autofix.yml
- name: Run codeloops
run: |
codeloops --prompt "${{ github.event.issue.body }}" \
--max-iterations 3 \
--json-output > result.json
continue-on-error: true
- name: Check result
run: |
outcome=$(jq -r '.outcome' result.json)
if [ "$outcome" != "success" ]; then
echo "Task did not complete successfully"
exit 1
fi
Performance Tips
Minimize Iteration Count
Better prompts = fewer iterations = faster completion.
Use Appropriate Agents
Faster agents reduce total time when the task is straightforward.
Limit Scope
Smaller, focused tasks complete faster than large ones.
Common Pitfalls
Ambiguous Requirements
Problem: Critic keeps requesting changes because requirements are unclear.
Solution: Add explicit acceptance criteria to your prompt. Use checkboxes that the critic can verify.
Too Many Unrelated Changes
Problem: Actor makes changes beyond what was asked.
Solution: Be specific about scope in the prompt. Add "Do not modify other files" if needed.
Ignoring Feedback
Problem: Re-running the same prompt hoping for different results.
Solution: Read the critic's feedback and adjust your prompt or approach. The critic often catches legitimate issues like:
- Double initialization of global state
- Missing error handling
- Clippy warnings
Overly Broad Tasks
Problem: Task never completes because scope is too large.
Solution: Break into smaller, manageable tasks. Use the /promptmd workflow to plan interactively, then execute each phase with codeloops.
Missing Context
Problem: Actor can't find the right files or uses wrong patterns.
Solution: Include file paths and line numbers. Reference existing code patterns by showing examples from the codebase.
Describing Symptoms Instead of Causes
Problem: Bug fix prompts describe what's wrong but not why.
Solution: Include root cause analysis. Explain why the bug happens, not just what happens. This dramatically reduces iterations.
Bad:
The daemon crashes on startup.
Good:
The daemon crashes on startup because tracing-subscriber is initialized twice:
1. `main.rs:10` calls `tracing_subscriber::fmt().init()`
2. `daemon.rs:66` calls it again, which panics
Fix: Use `try_init()` instead of `init()` in main.rs.
Not Including Quality Assurance Steps
Problem: Prompt completes but leaves behind clippy warnings, failing tests, or missing documentation.
Solution: Always include QA requirements in your prompt:
## Quality Requirements
- Run `cargo clippy` and fix all warnings in modified files
- Ensure all existing tests pass
- Add tests for new functionality
- Update relevant documentation
Security Considerations
Review Changes
Always review the git diff before committing:
git diff
Sensitive Files
Don't include sensitive information in prompts. The prompt is logged in the session file.
Access Control
Agents have full filesystem access in the working directory. Run codeloops in appropriate environments.
Session Management
Regular Cleanup
Delete old sessions periodically:
# Delete sessions older than 30 days
find ~/.local/share/codeloops/sessions -name "*.jsonl" -mtime +30 -delete
Backup Important Sessions
Copy sessions you want to preserve:
cp ~/.local/share/codeloops/sessions/important-session.jsonl ~/backups/
Analyze Patterns
Use statistics to improve your workflow:
codeloops sessions stats
Look at success rates by project to identify areas for improvement.
FAQ
Frequently asked questions about codeloops.
General
What is codeloops?
Codeloops is a command-line tool that orchestrates an actor-critic feedback loop for AI coding agents. It runs a coding agent to execute tasks, evaluates the results with another agent instance, and loops until the task is complete.
Why use codeloops instead of running an agent directly?
AI coding agents lack built-in self-correction. They may produce incomplete work, miss edge cases, or make mistakes without knowing. Codeloops adds a feedback loop where a critic evaluates the work and provides guidance for improvement, leading to higher quality results.
What agents are supported?
Currently supported:
- Claude Code (
claude) - OpenCode (
opencode) - Cursor (
cursor)
Can I use my own LLM or a custom agent?
Yes, by implementing the Agent trait. See Adding New Agents for a step-by-step guide.
Is codeloops free?
Codeloops itself is open source and free. However, the agents it uses (Claude, OpenCode, Cursor) may have their own pricing. You'll need accounts with those services.
Usage
Why does the loop keep running?
The loop continues when the critic returns CONTINUE, meaning the task isn't complete. Possible reasons:
- Requirements not fully met: Check the critic's feedback to see what's missing
- Vague prompt: Add explicit acceptance criteria
- Error in implementation: Actor may be making mistakes
Review the session to see the feedback:
codeloops sessions show
How do I stop a running session?
Press Ctrl+C. The session will be recorded with outcome interrupted.
How do I reduce iterations?
- Write more specific prompts with clear acceptance criteria
- Provide context (file paths, function names)
- Break complex tasks into smaller ones
- Set a limit:
--max-iterations 5
Why is the critic rejecting my changes?
Common reasons:
- Requirements in the prompt aren't fully implemented
- Edge cases not handled
- Tests failing (if mentioned in prompt)
- Code quality issues
Check the feedback in the session:
codeloops sessions show
Can I run multiple sessions simultaneously?
Yes, in different terminals or directories. Each session writes to its own file.
Configuration
Where is the configuration file?
- Global:
~/.config/codeloops/config.toml - Project:
./codeloops.toml(in working directory)
How do I see what configuration is being used?
Use dry run:
codeloops --dry-run
How do I use different agents for actor and critic?
codeloops --actor-agent opencode --critic-agent claude
Or in configuration:
[actor]
agent = "opencode"
[critic]
agent = "claude"
Sessions
Where are my sessions stored?
~/.local/share/codeloops/sessions/
Can I delete old sessions?
Yes, they're just files:
rm ~/.local/share/codeloops/sessions/session-id.jsonl
How do I export session data?
Sessions are JSONL files. Parse them with jq, Python, or any JSON tool:
cat ~/.local/share/codeloops/sessions/session.jsonl | jq
Why is my session file missing session_end?
The session was either:
- Still in progress when you checked
- Interrupted (Ctrl+C, crash)
- The process was killed
Troubleshooting
Agent not found in PATH
Error: Agent 'claude' not found in PATH
Solution: Install the agent and ensure its binary is in your PATH.
# Check if agent is installed
which claude
# Verify it works
claude --version
No prompt provided
Error: No prompt provided
Solution: Create a prompt.md file or use --prompt:
echo "Fix the bug" > prompt.md
codeloops
Or:
codeloops --prompt "Fix the bug"
Not a git repository
Error: Not a git repository
Solution: Initialize git in your working directory:
git init
Permission denied
Error: Permission denied
Solution: Check file and directory permissions:
ls -la ~/.local/share/codeloops/
Session directory doesn't exist
The session directory is created automatically. If it fails, create it manually:
mkdir -p ~/.local/share/codeloops/sessions
UI won't start
Port in use:
codeloops ui --api-port 4000 --ui-port 4001
UI directory not found: Build the UI:
cd ui && bun install && bun run build
Slow performance
Possible causes:
- Agent response time (depends on the service)
- Large diffs (many files changed)
- Complex prompts
Tips:
- Use faster agents for simple tasks
- Break large tasks into smaller ones
Development
How do I contribute?
See Development Setup for getting started.
How do I report bugs?
Open an issue on GitHub with:
- Steps to reproduce
- Expected vs actual behavior
- Session file (if applicable)
- System information
How do I request features?
Open an issue on GitHub describing:
- The use case
- Proposed solution
- Alternatives considered
Miscellaneous
Does codeloops modify my code?
No, codeloops doesn't modify code directly. The agents do. Codeloops orchestrates the loop and captures the results.
Does codeloops send my code anywhere?
Codeloops itself doesn't send code anywhere. The agents you use may send code to their services (Claude API, OpenAI API, etc.) according to their terms of service.
Can I use codeloops offline?
Codeloops itself runs locally, but the agents typically require internet access to their respective APIs.
How is this different from GitHub Copilot?
Copilot is an inline code completion tool. Codeloops is a task execution framework with feedback loops. They solve different problems and can be used together.
Does codeloops work with any language?
Yes, codeloops is language-agnostic. It works with whatever languages your chosen agent supports.
Can I use codeloops in CI/CD?
Yes. Use --json-output for machine-readable output and exit codes for status:
codeloops --max-iterations 3 --json-output > result.json
Where can I get help?
- Documentation: This site
- Issues: GitHub Issues
- Discussions: GitHub Discussions