Architecture
OpenLoop has three layers: a global registry and daemon, per-project control planes, and the agent processes it drives. Understanding the split explains most of its behavior.
┌────────────────────────────────────────────────────────┐│ Global (~/.openloop/) ││ config.json · projects.json · run/{pid,log,events} ││ ││ ┌──────────────────────────────────────────────────┐ ││ │ Daemon (one per machine) │ ││ │ scheduler · budget ledger · issue sync · hooks │ ││ └──────────────┬───────────────┬───────────────────┘ │└─────────────────┼───────────────┼──────────────────────┘ │ │ ┌─────────▼───┐ ┌───────▼─────┐ │ repo: api │ │ repo: web │ linked repositories │ .openloop/ │ │ .openloop/ │ each with its own control │ ├ project │ │ ├ project │ plane, ledger, and policy │ ├ policy │ │ ├ policy │ │ └ tasks │ │ └ tasks │ └─────────────┘ └─────────────┘The global layer
Section titled “The global layer”Everything machine-wide lives under ~/.openloop/ (relocatable via OPENLOOP_HOME):
| Path | Purpose |
|---|---|
config.json |
Global defaults: budget, runtime limits, selection strategy, hooks, notification channels. |
projects.json |
The registry of linked projects (alias → path). |
run/daemon.pid |
PID file for the daemon (stale PIDs are detected and cleared). |
run/daemon-state.json |
Live daemon state: paused flag, budget spend, per-project summaries. |
run/daemon.log |
Daemon log (rotates at 10 MB, keeps 3). |
run/events.jsonl |
Append-only audit log behind openloop events (rotates at 10 MiB). |
Global state is deliberately separate from repository state, unlinking a project never touches global config, and vice versa.
The daemon
Section titled “The daemon”openloop service start spawns a single resident process (openloop daemon worker, detached). It:
- Ticks every
runtime.tickIntervalSeconds(default 5s). - Selects a project using the configured strategy (
round-robin,priority, orfocus; see scheduling). - Runs one scheduler iteration for that project: pick a task, check policy, run the agent, validate, decide promotion.
- Enforces the global budget: when daily spend exceeds
budgets.dailyCostUsd, the daemon firesbudget-blockedhooks, notifies, and pauses itself.
There is at most one active worker per project. The daemon self-pauses after five consecutive tick errors, recovers stuck in_progress tasks on start, and reclaims stale worktrees.
service run runs the same loop in the foreground, useful under systemd, launchd, or a process supervisor.
The per-project control plane
Section titled “The per-project control plane”Each linked repo gets a .openloop/ directory materialized from templates:
project.json: agent, model, validation commands, worktree settings.policy.yaml: scope globs, risk classes, self-healing allowlist, promotion modes.tasks.json: the task ledger (schema intasks.schema.json).specs/,runs/,promotions/,promotion-results/,verifications/,approvals/,worktrees/, artifacts produced along the way.
Full layout: project files. The control plane is meant to be committed with the repo, so policy and task history travel with the code.
The agents
Section titled “The agents”OpenLoop does not talk to model APIs. Providers wrap coding-agent CLIs (pi, claude, aider, codex, opencode, or a custom command) and run them as subprocesses with a task-specific prompt. Usage and cost are parsed best-effort from each provider’s output; anything unparseable falls back to the configured per-run estimate.
The pipeline
Section titled “The pipeline”Every scheduled run walks the same path:
select task → scope-policy check → self-healing gate → attempt gate → (optional worktree) → agent run → validations → (optional verifier) → (optional review) → promotion decisionThe promotion pipeline page covers the decision logic; guardrails covers the failure modes.