Introduction

Codeloops

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:

  1. The actor (a coding agent) executes your task
  2. Git captures the changes made
  3. The critic (another agent instance) evaluates the output against your original prompt
  4. If the work is incomplete, the critic provides feedback and the actor tries again
  5. 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.md file and run codeloops

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:

AgentInstallation
Claude Codeclaude.ai/code
OpenCodeopencode.ai/docs
Cursorcursor.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:

  1. Read prompt.md from the current directory
  2. Start the actor agent with your prompt
  3. Capture the git diff after the actor completes
  4. Run the critic agent to evaluate the changes
  5. 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:

DecisionMeaning
DONETask is complete, loop ends
CONTINUEMore work needed, actor runs again with feedback
ERRORSomething 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

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:

  1. Set a limit: Use --max-iterations 5 to cap the attempts
  2. Cancel and reformulate: Press Ctrl+C and rewrite your prompt with more detail
  3. Review the feedback: Check what the critic is asking for

Actor Produces Errors

If the actor exits with an error (non-zero exit code):

  1. The critic will suggest recovery steps
  2. The actor tries again with recovery guidance
  3. 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

  1. Be specific: Include file paths, function names, and exact requirements
  2. Define "done": What criteria must be met for the task to be complete?
  3. Provide context: Mention relevant parts of your codebase
  4. Start small: Complex tasks often work better when broken into smaller prompts

Next Steps

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

CommandDescription
runRun the actor-critic loop (default)
sessionsBrowse and inspect sessions
uiStart the web UI
initInteractive configuration setup
helpPrint 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

OptionTypeDefaultDescription
-p, --prompt <PROMPT>String-Task prompt (inline)
--prompt-file <FILE>Pathprompt.mdPath to prompt file

If neither --prompt nor --prompt-file is provided, codeloops looks for prompt.md in the working directory.

Directory Options

OptionTypeDefaultDescription
-d, --working-dir <DIR>PathCurrent directoryWorking directory for the session

Agent Options

OptionTypeDefaultDescription
-a, --agent <AGENT>EnumclaudeAgent 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

OptionTypeDefaultDescription
-n, --max-iterations <N>IntegerUnlimitedMaximum loop iterations

Output Options

OptionTypeDefaultDescription
--log-format <FORMAT>EnumprettyOutput format
--log-file <PATH>Path-Write structured logs to file
--json-outputFlag-Output final result as JSON
--no-colorFlag-Disable colored output

Log format values: pretty, json, compact

Other Options

OptionTypeDefaultDescription
--dry-runFlag-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]
OptionTypeDescription
--outcome <OUTCOME>StringFilter by outcome: success, failed, interrupted, max_iterations_reached
--after <DATE>DateShow sessions after date (YYYY-MM-DD)
--before <DATE>DateShow sessions before date (YYYY-MM-DD)
--search <TEXT>StringSearch in prompt text
--project <NAME>StringFilter 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]
OptionTypeDefaultDescription
--devFlag-Development mode with hot reloading
--api-port <PORT>Integer3100API server port
--ui-port <PORT>Integer3101UI 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:

  1. Prompts for your preferred default agent
  2. Creates ~/.config/codeloops/config.toml
  3. Displays the generated configuration

Run this after installation to set up your defaults.

Global Options

These options work with any command:

OptionDescription
-h, --helpPrint help information
-V, --versionPrint version information

Exit Codes

CodeMeaning
0Success
1Max iterations reached
2Failed (error during execution)
130User interrupted (Ctrl+C)

Environment Variables

VariableDescription
CODELOOPS_UI_DIROverride the UI directory location
NO_COLORDisable 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):

  1. CLI flags (e.g., --agent claude)
  2. Project configuration (codeloops.toml in working directory)
  3. Global configuration (~/.config/codeloops/config.toml)
  4. 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

SectionFieldTypeDescription
[defaults]agentStringDefault agent: claude, opencode, or cursor
[defaults]modelStringDefault model name (optional)
[defaults.actor]agentStringActor-specific agent override
[defaults.actor]modelStringActor-specific model override
[defaults.critic]agentStringCritic-specific agent override
[defaults.critic]modelStringCritic-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

SectionFieldTypeDescription
(root)agentStringDefault agent for this project
(root)modelStringDefault model for this project
[actor]agentStringActor agent override
[actor]modelStringActor model override
[critic]agentStringCritic agent override
[critic]modelStringCritic 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

AgentCLI ValueBinaryDescription
Claude CodeclaudeclaudeAnthropic's Claude-powered coding agent
OpenCodeopencodeopencodeMulti-model coding agent
Cursorcursor-agentcursor-agentCursor 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:

  1. Install the agent
  2. Ensure the binary is in your PATH
  3. 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:

  1. Spawns the agent binary as a subprocess
  2. Passes the prompt via stdin or command-line arguments
  3. Sets the working directory
  4. Captures stdout, stderr, and exit code
  5. 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

OutcomeDescription
successCritic approved the work (DONE decision)
failedError during execution
interruptedUser pressed Ctrl+C
max_iterations_reachedHit 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:

TechnologyPurpose
React 19UI framework
React Router 7Client-side routing
TypeScriptType safety
ViteBuild tool and dev server
Tailwind CSS 4Styling
RechartsCharts and visualizations
BunJavaScript 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

Web UI Usage

This guide covers how to use the codeloops web interface.

Starting the UI

Basic Start

codeloops ui

This:

  1. Starts the API server on port 3100
  2. Serves the UI on port 3101
  3. 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

The sidebar provides navigation:

SectionDescription
DashboardSession list with filters
StatsStatistics and charts

Dashboard (Session List)

The main view shows all sessions in a table:

ColumnDescription
TimestampWhen the session started
ProjectProject name (working directory basename)
Outcomesuccess, failed, interrupted, or max_iterations_reached
IterationsNumber of actor-critic loops
DurationTotal session time
PromptFirst 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

KeyAction
/Focus search input
EscClose detail view / clear search
jNext session in list
kPrevious session in list
EnterOpen selected session

URL Structure

The UI uses client-side routing:

URLView
/Dashboard (session list)
/sessions/:idSession detail
/statsStatistics 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

  1. Use filters: Large session lists load faster with filters applied.

  2. Bookmark searches: The URL includes filter state, so bookmark filtered views.

  3. Compare iterations: Use the iteration timeline to quickly see where changes happened.

  4. 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:

  1. Problem Definition - Who is this for? What problem does it solve?
  2. Technical Approach - What components need to change? What patterns to follow?
  3. Implementation Details - Specific changes per component, edge cases
  4. Acceptance Criteria - Definition of done, verification steps

Defect

For bug fixes. The AI helps capture:

  1. Symptom - What's happening vs. what should happen? Reproduction steps
  2. Root Cause - Where in the code? Why is it happening?
  3. Fix Strategy - How to address it? Which files?
  4. Verification - Tests to add, regression prevention

Risk

For security vulnerabilities, performance issues, or technical concerns:

  1. Risk Identification - What risk? How discovered? Impact?
  2. Current State - Where does it exist? Current mitigations?
  3. Remediation Plan - How to address? Files to change?
  4. Validation - How to verify the fix?

Debt

For technical debt cleanup:

  1. Current State - What needs improvement? Why is it problematic?
  2. Target State - What should it look like? Patterns to follow?
  3. Refactoring Plan - Files to touch, safe order of operations
  4. 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:

ActionDescription
SaveWrites prompt.md to your project directory
CopyCopies to clipboard
DownloadDownloads as a file

Keyboard Shortcuts

ShortcutAction
Cmd/Ctrl + EnterSend message
Cmd/Ctrl + PToggle preview panel
EscapeClose 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

  1. Create the page component in src/pages/:
// src/pages/MyPage.tsx
export default function MyPage() {
  return (
    <div>
      <h1>My Page</h1>
    </div>
  )
}
  1. Add the route in src/App.tsx:
import MyPage from './pages/MyPage'

// In the Routes:
<Route path="/my-page" element={<MyPage />} />
  1. Add navigation in src/components/Layout.tsx:
<NavLink to="/my-page">My Page</NavLink>

Adding a New Component

  1. 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>
  )
}
  1. 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:

FilePurpose
mod.rsRouter setup
sessions.rsSession endpoints
stats.rsStatistics endpoint
sse.rsServer-Sent Events

To add a new API endpoint:

  1. Add the handler function in the appropriate file
  2. Add the route in mod.rs:
#![allow(unused)]
fn main() {
.route("/api/my-endpoint", get(my_handler))
}
  1. Add the corresponding client function in ui/src/api/client.ts

Common Tasks

Update API Types

When Rust types change:

  1. Update ui/src/api/types.ts to match
  2. Update any affected components

Add a New Filter

  1. Add the filter field to SessionFilter in types.ts
  2. Update SessionFilters.tsx to include the new control
  3. Update fetchSessions() to send the filter parameter
  4. Update the Rust API to handle the new filter

Add a Chart

  1. Add the data field to the appropriate type
  2. Import from Recharts:
import { BarChart, Bar, XAxis, YAxis, Tooltip } from 'recharts'
  1. 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.md file is the primary interface. No complex configuration required.
  • Execution: Run codeloops and 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 Agent trait
  • 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

  1. Input: User provides prompt via prompt.md or --prompt

  2. Configuration: CLI loads global and project config, resolves agent choices

  3. Initialization: LoopRunner created with actor/critic agents

  4. Session Start: SessionWriter creates JSONL file, writes start line

  5. Actor Execution: Actor agent spawned with prompt, output captured

  6. Diff Capture: Git diff computed between before/after states

  7. Critic Evaluation: Critic agent spawned with actor output + diff

  8. Decision Parsing: Critic response parsed for decision and feedback

  9. Session Update: Iteration recorded to JSONL file

  10. Loop Control: If DONE, end session. If CONTINUE, feed back to actor.

  11. Completion: SessionEnd line written, process exits

Why This Architecture?

Separation of Concerns

Each crate has a single responsibility:

  • codeloops-agent: Knows how to run agents
  • codeloops-critic: Knows how to evaluate
  • codeloops-git: Knows how to diff
  • codeloops-core: Orchestrates everything

This makes testing and modification easier.

Extensibility

Adding a new agent:

  1. Implement the Agent trait
  2. Add it to the agent factory
  3. 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

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:

  1. Success: Critic returns DONE
  2. Max iterations: Configured limit reached (exit code 1)
  3. Error: Unrecoverable error occurs (exit code 2)
  4. Interrupt: User presses Ctrl+C (exit code 130)

Confidence Scoring

When the critic returns DONE, it provides a confidence score:

ScoreMeaning
0.9 - 1.0High confidence, all requirements clearly met
0.7 - 0.9Good confidence, requirements met with minor uncertainty
0.5 - 0.7Moderate confidence, some requirements unclear
< 0.5Low 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

CrateTypePurpose
codeloopsBinaryCLI, API server, session viewer
codeloops-coreLibraryLoop orchestration
codeloops-agentLibraryAgent abstraction
codeloops-criticLibraryCritic evaluation
codeloops-gitLibraryGit diff capture
codeloops-loggingLibraryLogging and session writing
codeloops-sessionsLibrarySession 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

FilePurpose
main.rsCLI entry, argument parsing, command dispatch
config.rsConfiguration loading (global + project)
sessions.rsSession commands (list, show, diff, stats)
init.rsInteractive setup
ui.rsWeb UI launcher
api/mod.rsAPI server router
api/sessions.rsSession API endpoints
api/stats.rsStatistics API endpoint
api/sse.rsServer-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

FilePurpose
lib.rsCrate root, re-exports
loop_runner.rsMain loop execution
context.rsLoop context and iteration records
outcome.rsLoop outcomes (Success, Failed, etc.)
error.rsError 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

FilePurpose
lib.rsCrate root, factory function
traits.rsAgent trait definition
claude.rsClaude Code agent
opencode.rsOpenCode agent
cursor.rsCursor agent
spawner.rsProcess spawning utilities
output.rsOutput 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

FilePurpose
lib.rsCrate root, re-exports
evaluator.rsCritic evaluation logic
decision.rsDecision types and parsing
prompts.rsPrompt 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

FilePurpose
lib.rsCrate root, re-exports
diff.rsDiff capture functionality
status.rsGit 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

FilePurpose
lib.rsCrate root, re-exports
session.rsSession JSONL writer
events.rsLog 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

FilePurpose
lib.rsCrate root, re-exports
store.rsSession storage access
parser.rsJSONL parsing
types.rsSession types
watcher.rsFile 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 TypeCrate
New CLI commandcodeloops
Configuration optionscodeloops
Loop behaviorcodeloops-core
New agent supportcodeloops-agent
Critic evaluation logiccodeloops-critic
Git operationscodeloops-git
Log output formatcodeloops-logging
Session parsingcodeloops-sessions
API endpointscodeloops (api/)
Web UIui/ (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:

TypeOccurrenceDescription
session_startExactly onceFirst line, session metadata
iterationZero or moreOne per actor-critic cycle
session_endZero or onceLast 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

FieldTypeRequiredDescription
typestringYesAlways "session_start"
timestampstringYesISO 8601 datetime when session started
promptstringYesFull task prompt text
working_dirstringYesAbsolute path to working directory
actor_agentstringYesActor agent name (e.g., "Claude Code")
critic_agentstringYesCritic agent name (e.g., "Claude Code")
actor_modelstring/nullYesActor model name or null if not specified
critic_modelstring/nullYesCritic model name or null if not specified
max_iterationsinteger/nullYesIteration 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

FieldTypeRequiredDescription
typestringYesAlways "iteration"
iteration_numberintegerYes1-indexed iteration number
actor_outputstringYesActor's stdout output
actor_stderrstringYesActor's stderr output
actor_exit_codeintegerYesActor's process exit code
actor_duration_secsfloatYesActor execution time in seconds
git_diffstringYesUnified diff of changes
git_files_changedintegerYesNumber of files modified
critic_decisionstringYes"DONE", "CONTINUE", or "ERROR"
feedbackstring/nullYesCritic feedback (null for DONE)
timestampstringYesISO 8601 datetime when iteration completed

Critic Decision Values

ValueMeaning
DONETask complete, no more iterations
CONTINUEMore work needed, feedback provided
ERRORError 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

FieldTypeRequiredDescription
typestringYesAlways "session_end"
outcomestringYesSession outcome (see values below)
iterationsintegerYesTotal number of iterations completed
summarystring/nullYesTask completion summary (for success)
confidencefloat/nullYesConfidence score 0.0-1.0 (for success)
duration_secsfloatYesTotal session duration in seconds
timestampstringYesISO 8601 datetime when session ended

Outcome Values

ValueDescription
successCritic returned DONE, task complete
failedUnrecoverable error occurred
interruptedUser pressed Ctrl+C
max_iterations_reachedHit 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

FileLocationScope
Global~/.config/codeloops/config.tomlAll projects
Project<working-dir>/codeloops.tomlSingle project

Precedence

Settings are resolved in order (highest priority first):

  1. CLI flags
  2. Project configuration
  3. Global configuration
  4. 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.

KeyTypeDefaultDescription
agentstring"claude"Default agent for both roles
modelstringnoneDefault model for both roles

[defaults.actor]

Override defaults for the actor role.

KeyTypeDefaultDescription
agentstringinheritAgent for actor
modelstringinheritModel for actor

[defaults.critic]

Override defaults for the critic role.

KeyTypeDefaultDescription
agentstringinheritAgent for critic
modelstringinheritModel 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

KeyTypeDefaultDescription
agentstringinheritDefault agent for this project
modelstringinheritDefault model for this project

[actor]

KeyTypeDefaultDescription
agentstringinheritAgent for actor
modelstringinheritModel for actor

[critic]

KeyTypeDefaultDescription
agentstringinheritAgent for critic
modelstringinheritModel 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

ValueAgent
"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:

SettingValueSource
Actor agentclaudedefault
Critic agentclaudedefault
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:

SettingValueSource
Actor agentopencodeglobal
Critic agentopencodeglobal
Actor modelgpt-4oglobal
Critic modelgpt-4oglobal

Example 3: Global + Project config

~/.config/codeloops/config.toml:

[defaults]
agent = "opencode"

codeloops.toml:

agent = "claude"

Running codeloops:

SettingValueSource
Actor agentclaudeproject
Critic agentclaudeproject

Example 4: CLI overrides all

~/.config/codeloops/config.toml:

[defaults]
agent = "opencode"

codeloops.toml:

agent = "claude"

Running codeloops --agent cursor:

SettingValueSource
Actor agentcursorCLI
Critic agentcursorCLI

Example 5: Per-role settings

~/.config/codeloops/config.toml:

[defaults]
agent = "claude"
model = "sonnet"

[defaults.actor]
agent = "opencode"
model = "gpt-4o"

Running codeloops:

SettingValueSource
Actor agentopencodeglobal.actor
Critic agentclaudeglobal.defaults
Actor modelgpt-4oglobal.actor
Critic modelsonnetglobal.defaults

Environment Variables

VariableDescription
CODELOOPS_UI_DIROverride UI assets directory
NO_COLORDisable 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

ParameterTypeDescription
outcomestringFilter by outcome: success, failed, interrupted, max_iterations_reached
afterstringSessions after date (YYYY-MM-DD)
beforestringSessions before date (YYYY-MM-DD)
searchstringSearch in prompt text
projectstringFilter 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

FieldTypeDescription
idstringSession identifier
timestampstringISO 8601 start time
prompt_previewstringFirst 256 chars of prompt
working_dirstringAbsolute path
projectstringBasename of working_dir
outcomestring/nullOutcome or null if active
iterationsintegerNumber of iterations
duration_secsfloat/nullTotal duration or null if active
confidencefloat/nullConfidence score (0-1)
actor_agentstringActor agent name
critic_agentstringCritic 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

ParameterTypeDescription
idstringSession 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

ParameterTypeDescription
idstringSession 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

FieldTypeDescription
total_sessionsintegerTotal number of sessions
success_ratefloatSuccess rate (0.0-1.0)
avg_iterationsfloatAverage iterations per session
avg_duration_secsfloatAverage session duration
sessions_over_timearraySessions grouped by date
by_projectarrayStatistics 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

EventDescription
session_createdNew session started
session_updatedIteration completed
session_completedSession 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

FieldTypeDescription
workingDirstringAbsolute path to working directory
projectNamestringBasename 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

FieldTypeDescription
workTypestringWork type: feature, defect, risk, debt, or custom
workingDirstringAbsolute 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

ParameterTypeDescription
sessionIdstringSession 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 DataDescription
{"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

FieldTypeDescription
workingDirstringDirectory to save prompt.md
contentstringContent to write to prompt.md

Response

{
  "path": "/home/user/projects/myapp/prompt.md"
}

Error Responses

StatusDescription
500Write 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, OPTIONS
  • Access-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 feature
  • fix: - Bug fix
  • docs: - Documentation
  • refactor: - Code refactoring
  • test: - Tests
  • chore: - 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

  1. Edit crates/codeloops/src/main.rs
  2. Add the option to the Cli struct with clap attributes
  3. Handle the option in the command logic
  4. Update CLI reference documentation

Adding a Configuration Option

  1. Edit crates/codeloops/src/config.rs
  2. Add field to appropriate struct (GlobalConfig or ProjectConfig)
  3. Update resolution logic
  4. Update configuration documentation

Adding a Session Field

  1. Edit crates/codeloops-logging/src/session.rs (SessionLine enum)
  2. Edit crates/codeloops-sessions/src/types.rs (Session structs)
  3. Update parser in crates/codeloops-sessions/src/parser.rs
  4. Update session format documentation

Adding an API Endpoint

  1. Create handler in crates/codeloops/src/api/
  2. Add route in crates/codeloops/src/api/mod.rs
  3. Add TypeScript types in ui/src/api/types.ts
  4. Add client function in ui/src/api/client.ts
  5. 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:

  1. Implement the Agent trait
  2. Add the agent type to the AgentType enum
  3. Update the agent factory function
  4. Add CLI support
  5. 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:

  1. The Actor: The coding agent generates code/plans
  2. The Critic: An LLM evaluates outputs for accuracy and best practices
  3. 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:

AspectRalph WiggumCodeloops
EvaluationSelf-evaluation (agent decides when done)Explicit critic agent reviews the actor's work
Feedback sourceContext window + file stateGit diff + stdout/stderr
RolesSingle agentSeparate actor and critic (can be different agents)
Decision protocolCompletion promise stringStructured 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

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:

  1. prompt-login.md: Fix the login bug
  2. prompt-dashboard.md: Add dashboard page
  3. prompt-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:

  1. User registration
  2. User login
  3. Logout
  4. Password reset (request)
  5. Password reset (confirm)
  6. Email verification

Run them sequentially, each building on the previous.

Use Iteration Limits Wisely

Set limits based on task complexity:

Task TypeSuggested Limit
Simple fix2-3
Small feature5
Medium feature10
Complex taskConsider 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 TypeRecommended
Complex refactoringClaude (strong reasoning)
Simple fixesOpenCode (fast)
Cursor-centric workflowCursor

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"
CodeMeaning
0Success
1Max iterations reached
2Failed
130Interrupted

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:

  1. Requirements not fully met: Check the critic's feedback to see what's missing
  2. Vague prompt: Add explicit acceptance criteria
  3. 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?

  1. Write more specific prompts with clear acceptance criteria
  2. Provide context (file paths, function names)
  3. Break complex tasks into smaller ones
  4. 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?