loopy-loop runs long-running AI agent workflows inside your repository.
It turns a goal file in your repository into an inspectable sequence of agent
iterations: plan, implement, evaluate, record evidence, and continue until the
goal is met or the loop hits a terminal blocker.
The value is control and durability. Instead of asking one agent to solve a large task in one fragile chat, loopy-loop gives the work a persistent session directory, repeatable workflow prompts, explicit stop conditions, and structured logs. You can pause, resume, audit what happened, adjust the goal, inspect every prompt/result pair, and keep the actual project changes in normal git branches and PRs.
Under the hood, loopy-loop runs a small FastAPI coordinator and one or more
workers. The coordinator owns the loop state and chooses the next workflow. The
workers run assignments through
team-harness, which can delegate
to agent CLIs such as Codex, Claude Code, and Gemini. The packaged
inner_outer_eval template also uses
eval-banana conventions for
session-scoped evaluation checks.
Install the CLI from the official PyPI package.
With uv, install it as a command-line tool:
uv tool install loopy-loopOr with pip:
pip install loopy-loopFor development inside this repository:
uv sync --extra devThis repo also ships an Agent Skill that teaches Claude Code, Codex, and compatible agents how to set up and run loopy-loop in another target repo.
npx skills add https://github.com/writeitai/loopy-loop --skill loopy-loopThe skill source lives under skills/loopy-loop/.
Run this from the repository you want agents to work on:
loopy init --template inner_outer_evalThis is the recommended starting template. It creates:
loopy_loop_config.yamlloopy_loop_goal.txt.loopy_loop/workflow_sets/inner_outer_eval/workflows/outer/.loopy_loop/workflow_sets/inner_outer_eval/workflows/inner/.loopy_loop/workflow_sets/inner_outer_eval/workflows/eval_reviewer/.loopy_loop/workflow_sets/inner_outer_eval/workflows/eval_runner/- a
.gitignoreentry for.loopy_loop/sessions/
loopy init is idempotent. It creates missing files and leaves existing files
alone.
The loop goal lives in loopy_loop_goal.txt. Replace the scaffolded example
with the real target, including constraints and observable completion criteria.
Example:
Implement passwordless email login.
Completion criteria:
- Users can request a one-time login link from the sign-in page.
- The link expires after 15 minutes and cannot be reused.
- Existing password login keeps working.
- Tests cover token expiry, token reuse, and successful login.
- README documents required environment variables.
Keep the goal specific enough that a reviewer or eval workflow can decide
whether the loop is done. For one-off overrides, start the coordinator with
--goal-file PATH; the file is copied into the session as goal.md.
Start the coordinator in one terminal:
loopy coordinator --host 127.0.0.1 --port 8080Start a worker in another terminal:
loopy worker --coordinator http://127.0.0.1:8080Useful control commands:
loopy status
loopy stopIf the coordinator stops while a session is still running, restart it with:
loopy coordinator --host 127.0.0.1 --port 8080 --resumeThe default templates use team_harness_provider: "codex", so the coordinator
uses local Codex authentication. If you switch to an OpenAI-compatible provider,
export the environment variable named in team_harness_api_key_env, usually
OPENROUTER_API_KEY, in both the coordinator and worker shells.
At a high level:
loopy initwrites a root config, a goal file, and workflow files into the target repo.loopy coordinatorloadsloopy_loop_config.yaml, resolves the goal text, creates a session under.loopy_loop/sessions/, and exposes two HTTP endpoints:/registerand/finished.- A worker calls
/register, receives the next workflow assignment, renders the workflow prompt with session paths, and runs it throughteam-harness. team-harnessruns a coordinator model and can spawn external worker CLIs such as Codex, Claude Code, or Gemini.- The worker writes the rendered prompt, normalized result, result text, harness run id, and harness output path into the iteration directory.
- The coordinator records the result, checks session control/eval artifacts, and either dispatches the next workflow or stops.
The inner_outer_eval template is organized around four workflows:
outer: plans, tracks durable project state, reviews evidence, and decides what should happen next.inner: implements the selected work in the target repo.eval_reviewer: creates or refreshes session-scoped eval-banana checks.eval_runner: runs the eval checks and writesgoal_check.json.
The loop does not hide state inside a chat transcript. Continuity comes from
git state plus files in .loopy_loop/sessions/<session_id>/.
After initialization, the target repo has this shape:
target repo/
├── loopy_loop_config.yaml
├── loopy_loop_goal.txt
└── .loopy_loop/
├── workflow_sets/
│ └── <workflow_set>/workflows/<workflow_id>/
│ ├── config.yaml
│ └── prompt.txt
└── sessions/
└── <session_id>/
├── goal.md
├── session.json
├── state.json
├── control.json
├── updates_from_user.md
├── project_state/
├── eval_checks/
├── eval_results/
├── harness_outputs/
├── child_requests/
├── children/
└── iterations/
Workflow definitions are part of the repo and should usually be committed. Session directories are runtime output and are ignored by default.
Root config lives at loopy_loop_config.yaml:
goal_file: loopy_loop_goal.txt
workflow_set: inner_outer_eval
max_turns: 160
goal_check_consecutive_failures_cap: 3
team_harness_provider: "codex"
team_harness_model: "gpt-5.5"
team_harness_agents:
- "codex"
- "claude"
- "gemini"
team_harness_agent_models:
codex: "gpt-5.5"
claude: "claude-opus-4-8"
gemini: "gemini-3.5-flash"
team_harness_agent_reasoning_efforts:
codex: "high"
team_harness_api_base: "https://openrouter.ai/api/v1"
team_harness_api_key_env: "OPENROUTER_API_KEY"Important rules:
workflow_setselects the default workflow set for new sessions.goal_fileis resolved relative toloopy_loop_config.yaml.- Inline
goalvalues in YAML are rejected; the goal should live in a file. max_turnsis the maximum number of completed workflow iterations.team_harness_modelcontrols the team-harness coordinator model.team_harness_agent_modelscontrols default models for worker subprocesses.team_harness_api_baseis normalized by loopy-loop: trailing slash stripped,/v1appended when missing.team_harness_max_retries,team_harness_retry_base_delay_s, andteam_harness_retry_max_delay_sare optional retry controls for transient team-harness API/network errors.
Workflow config lives beside each workflow prompt:
enabled: true
priority: 0
run_every: 1
must_follow: null
not_before_iteration: 0
run_on_start: false
run_after_successes: null
emits_goal_check: false
description: ""Workflow rules:
- The workflow id is the folder name under
.loopy_loop/workflow_sets/<workflow_set>/workflows/. prioritybreaks ties among eligible workflows; higher values run first.run_everyis based on completed iteration count, not wall clock.run_on_start=truemakes a workflow eligible before any successful workflow has run.must_followandrun_after_successes.workflow_idmust reference existing workflow ids.run_after_successescan schedule a workflow after every N successful runs of another workflow:
run_after_successes:
workflow_id: inner
every: 10emits_goal_check=truelets a non-goal_checkworkflow writegoal_check.jsonas an eval artifact. Stopping still requires updating sessioncontrol.json.
Each fresh coordinator run creates one session directory:
.loopy_loop/sessions/<session_id>/
Session ids start with a UTC timestamp and include a deterministic goal hash, so session directories sort chronologically and similar goals are easy to compare.
Important session files:
goal.md: the exact goal text copied into the session.session.json: session metadata.state.json: coordinator-owned dispatch state.events.jsonl: reserved append-only diagnostics log.control.json: workflow-owned stop switch.updates_from_user.md: human-writable inbox for changes after the session starts.project_state/: workflow-owned durable markdown state.eval_checks/: session-scoped eval-banana checks.eval_results/: raw eval-banana reports.harness_outputs/: team-harness coordinator and worker artifacts.iterations/: one directory per loopy-loop assignment.
Each iteration directory contains:
.loopy_loop/sessions/<session_id>/iterations/<NNNN>_<workflow_id>/
├── prompt.txt
├── result.json
├── result_text.txt
├── harness_run_id.txt
├── pending_finished_request.json
└── goal_check.json # only for eval-emitting workflows
prompt.txt is the rendered prompt sent to TeamHarness.run(...).
result.json is loopy-loop's normalized result. result_text.txt is the
plain-text final response. harness_run_id.txt links the iteration to the
corresponding team-harness output under harness_outputs/.
Team-harness outputs are routed here:
.loopy_loop/sessions/<session_id>/harness_outputs/<NNNN>_<workflow_id>/<team_harness_run_id>/
Eval-banana outputs should be routed here:
.loopy_loop/sessions/<session_id>/eval_results/<eval_banana_run_id>/
See docs/session-layout.md for the full session file contract.
control.json is the session-scoped stop switch. It starts as:
{
"state": "running",
"reason": "session active",
"stop_reason": null,
"schema_version": 1
}To stop successfully, a workflow writes:
{
"state": "stopped",
"reason": "evals passed",
"stop_reason": "goal_met",
"schema_version": 1
}To stop because the loop cannot continue:
{
"state": "stopped",
"reason": "specific terminal blocker",
"stop_reason": "unresolvable_error",
"schema_version": 1
}goal_check.json is a per-iteration eval artifact:
{"goal_met": false, "reason": "docs still missing", "schema_version": 1}A valid goal_check.json does not stop the loop by itself. It is evidence.
Stopping is controlled by session control.json. If goal-check output is
missing or invalid repeatedly, the coordinator stops with
stop_reason="goal_check_broken" after the configured failure cap.
Workflow sets are mandatory. Even a single-loop repo uses:
.loopy_loop/workflow_sets/main/workflows/...
The older .loopy_loop/workflows/... layout is not loaded.
A workflow can request one sequential child session by writing a JSON file under
the active session's child_requests/ directory:
{
"workflow_set": "pm_planner_dispatcher",
"goal": "Implement the selected planner item.",
"schema_version": 1
}The coordinator creates the child session under the parent session's
children/ directory, copies the request goal into the child goal.md, runs
the requested workflow set, and resumes the parent after the child reaches a
terminal state. v1 is depth-first and single-child-at-a-time.
The packaged pm_planner_dispatcher workflow set uses this contract for PM
orchestration:
plannermaintains PM state, selects one work item, and reviews terminal child-session evidence.dispatcherwrites one child request for the selected work item or imports terminal child evidence back into PM state.
The coordinator exposes exactly two endpoints:
POST /registerPOST /finished
Both return a TaskResponse with action equal to "run" or "stop".
A run response carries workflow_set, workflow_id, session_id,
iteration, and a config snapshot. A stop response carries stop_reason.
If /finished receives a stale response for a task that is no longer current,
the coordinator does not mutate state. If a worker exits after writing
result.json but before /finished is acknowledged, the next /register
recovers the completed result from the iteration directory instead of marking
the task abandoned.
See docs/http-contract.md for exact JSON payloads.
loopy init [--template default|inner_outer_eval|pm_planner_dispatcher]Scaffolds loopy-loop files. The default template creates only the reserved
goal_check workflow. inner_outer_eval creates the recommended outer/inner/eval
workflow set. pm_planner_dispatcher creates planner/dispatcher workflows for
child-session orchestration.
loopy coordinator --host 0.0.0.0 --port 8080 [--resume] [--workflow-set NAME] [--goal-file PATH]Runs the coordinator. --workflow-set and --goal-file override the root
config for the new session. --resume reuses a non-terminal latest session.
loopy worker --coordinator http://127.0.0.1:8080Runs a blocking worker until the coordinator returns action: "stop".
loopy status
loopy stopstatus prints the latest session state. stop sets stop_requested=true in
the latest session-local state.
team-harness: the model and agent-CLI orchestration layer used by loopy-loop workers.eval-banana: a lightweight YAML evaluation framework used by the packaged eval workflows.