Skip to content

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 │
└─────────────┘ └─────────────┘

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.

openloop service start spawns a single resident process (openloop daemon worker, detached). It:

  1. Ticks every runtime.tickIntervalSeconds (default 5s).
  2. Selects a project using the configured strategy (round-robin, priority, or focus; see scheduling).
  3. Runs one scheduler iteration for that project: pick a task, check policy, run the agent, validate, decide promotion.
  4. Enforces the global budget: when daily spend exceeds budgets.dailyCostUsd, the daemon fires budget-blocked hooks, 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.

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 in tasks.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.

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.

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 decision

The promotion pipeline page covers the decision logic; guardrails covers the failure modes.