Skip to content

Scheduling & self-healing

The daemon’s scheduler decides what to work on next at every tick. Its logic is deliberately simple and inspectable.

Each iteration starts by picking a mode for the selected project:

Mode When What happens
implement Tasks in ready Pick a task, run the agent, validate, promote.
plan Tasks in proposed, none ready Generate a spec (.openloop/specs/) so the task can move to ready.
idle Nothing queued Look for improvements to propose; see below.

Preview the choice without running anything:

Terminal window
openloop run-once -p api --dry-run
{ "mode": "implement", "role": "implementer", "taskId": "harden-webhook-retries", "reason": "" }

The prompt given to the agent depends on the task and mode:

  • sdd-planner, spec-driven planning of proposed tasks.
  • implementer, implements ready tasks.
  • repo-improver, idle-time discovery of missing tests, docs, refactors.
  • ci-healer, self-healing fix tasks.

With multiple linked projects the daemon picks one per tick:

  • round-robin (default), fair turns across all projects.
  • priority, prefer the active project (openloop project activate <alias>) whenever it has work.
  • focus, work the active project exclusively until it’s drained.
Terminal window
openloop config set runtime.projectSelectionStrategy priority

One active worker per project at a time, repos are never processed concurrently against themselves.

When validations fail after a run (or pre-existing breakage is detected), the daemon can triage a bounded fix task:

  • Allowed kinds come from policy.yamlselfHealing.allowedTaskKinds (default: lint-fix, type-fix, localized-test-fix).
  • Anything else (broken builds, broad test failures) is not run unattended; it blocks and waits for a human.
  • Self-healing respects the same attempt ceilings and timeouts as normal work.

When a project has no queued work, the daemon uses the idle window to look for gaps (missing tests, stale docs, small refactors) and proposes tasks (discovery, scope-proposal). Proposals land in proposed; nothing runs until they’re promoted to ready (by you, or by plan mode on the next pass).

Per-project schedules in project.json create tasks on a cron cadence:

{
"schedule": [
{ "id": "weekly-deps-audit", "cron": "0 9 * * 1", "title": "Audit dependency updates" },
{ "id": "nightly-tests", "cron": "0 2 * * *", "title": "Run flaky-test sweep", "prompt": "Re-run and stabilize recently flaky tests" }
]
}
  • Five-field cron, evaluated in UTC.
  • Fires create proposed tasks (medium-risk by default).
  • After downtime, missed schedules catch up for up to 7 days.

With runtime.useWorktree: true, each run executes in a dedicated git worktree on an openloop/<taskId> branch; your working tree is never touched. The scheduler refuses to fall back to the main tree; if worktree setup fails, the run is blocked. Customize the branch prefix with runtime.branchPrefix.