Agent Loops is a system that receives coding tasks as JSON specs, decomposes them into subtasks via LLM when needed, assigns each subtask to a specialized agent running inside an isolated Docker container, tracks progress on an SQLite-backed Kanban board, and consolidates results into a shared workspace.
The system is designed for a single VPS (4 cores / 8 GB RAM) and targets a maximum of 3 parallel agent containers.
┌─────────────────────────────────────────┐
│ VPS (4 cores / 8GB) │
│ │
Input (JSON) ──────►│ ┌─────────────────────────────────┐ │
queue/ │ │ Orchestrator (Node.js) │ │
│ │ ├── dispatcher.js (loop 30s) │ │
│ │ ├── decomposer.js (LLM split) │ │
│ │ ├── docker-runner.js │ │
│ │ ├── git-manager.js │ │
│ │ ├── board.js (SQLite) │ │
│ │ └── status-renderer.js │ │
│ └──────────┬───────────────────────┘ │
│ │ dockerode │
│ ┌────────┼────────┬──────────┐ │
│ ▼ ▼ ▼ ▼ │
│ ┌──────┐┌──────┐┌──────┐┌──────┐ │
│ │backend││front ││ qa ││devops│ │
│ │iteratr││iteratr││iteratr││iteratr│ │
│ │KIT SDK││KIT SDK││KIT SDK││KIT SDK│ │
│ │NATS ││NATS ││NATS ││NATS │ │
│ └──┬───┘└──┬───┘└──┬───┘└──┬───┘ │
│ └───────┴───────┴───────┘ │
│ │ results │
│ ▼ │
│ results/ + BOARD.md │
└─────────────────────────────────────────┘
| Module | File | Responsibility |
|---|---|---|
| Dispatcher loop | dispatcher.js |
Polls board every 30 s, picks ready tasks, hands to runner |
| Decomposer | decomposer.js |
Calls Claude to split a triage task into subtasks |
| Docker runner | docker-runner.js |
Creates, starts, watches containers via dockerode |
| Git manager | git-manager.js |
Clones repo, creates branches, pushes results |
| Board | board.js |
SQLite CRUD for tasks, transitions, history |
| Status renderer | status-renderer.js |
Regenerates BOARD.md from current board state |
| State | Meaning |
|---|---|
triage |
Received, awaiting decomposition decision |
todo |
Decomposed; waiting to be picked up |
ready |
Dependencies met, eligible for dispatch |
running |
Agent container is active |
done |
Agent finished, results committed |
blocked |
Agent needs human input |
crashed |
Container exited non-zero |
gave_up |
Exceeded max retries (circuit breaker) |
┌─────────────────────┐
│ triage │
└────────┬────────────┘
│ decomposer.js (LLM)
▼
┌─────────────────────┐
│ todo │◄─────────────────────┐
└────────┬────────────┘ │
│ deps met │
▼ │
┌─────────────────────┐ │
│ ready │◄──────┐ │
└────────┬────────────┘ │ │
│ dispatcher picks │ unblock │
▼ │ │
┌─────────────────────┐ │ │
│ running │───────┘ │
└──┬──────┬──────┬────┘ blocked │
│ │ │ │
done │ │ │ crashed │
▼ │ ▼ │
┌──────────┐ │ ┌──────────────┐ │
│ done │ │ │ crashed │─── retry ──────┘
└──────────┘ │ └──────────────┘ (back to ready)
│ │
│ │ max retries exceeded
│ ▼
│ ┌──────────────┐
│ │ gave_up │
│ └──────────────┘
│
▼
┌──────────────┐
│ blocked │─── human unblocks ──► ready
└──────────────┘
crashed is automatically moved back to ready up to 3 times. After the 3rd crash it transitions to gave_up.blocked for more than 24 h without human action, the dispatcher sends a notification and keeps the task in blocked.running if the current count of running tasks is < 3.Tasks are placed as JSON files in queue/. Two modes are supported.
The file contains a single high-level task. The decomposer splits it into subtasks.
{
"mode": "auto",
"title": "Add user registration flow",
"body": "Implement sign-up with email verification for the REST API. Includes POST /register, email sending via SES, and verification endpoint.",
"repo": "https://github.com/org/api-service.git",
"base_branch": "main",
"metadata": {
"priority": "high",
"acceptance_criteria": [
"Endpoint returns 201 on valid registration",
"Verification email sent within 5 seconds",
"Unverified accounts cannot authenticate"
]
}
}
The file contains an array of tasks already broken down. The orchestrator creates each task directly in todo and resolves dependency ordering.
{
"mode": "pre",
"parent_title": "Add user registration flow",
"repo": "https://github.com/org/api-service.git",
"base_branch": "main",
"tasks": [
{
"title": "Create User model + migration",
"body": "Add users table with email, password_hash, verified_at columns.",
"assignee": "backend",
"priority": "high",
"depends_on": []
},
{
"title": "Implement POST /register endpoint",
"body": "Validate input, hash password, create user, queue verification email.",
"assignee": "backend",
"priority": "high",
"depends_on": ["Create User model + migration"]
},
{
"title": "Send verification email via SES",
"body": "Lambda or worker that sends a templated email with a verification link.",
"assignee": "backend",
"priority": "medium",
"depends_on": ["Create User model + migration"]
},
{
"title": "E2E tests for registration flow",
"body": "Cover happy path, duplicate email, invalid input, and verification expiry.",
"assignee": "qa",
"priority": "medium",
"depends_on": [
"Implement POST /register endpoint",
"Send verification email via SES"
]
},
{
"title": "Add Terraform for SES resources",
"body": "SES domain identity, DKIM, and IAM policy for sending.",
"assignee": "devops",
"priority": "low",
"depends_on": []
}
]
}
When a container is spawned for a task, the agent receives the following context as a structured JSON payload (mounted at /task/context.json inside the container):
{
"task": {
"id": "abc-123",
"title": "Implement POST /register endpoint",
"body": "Validate input, hash password, create user, queue verification email.",
"assignee": "backend",
"priority": "high",
"acceptance_criteria": [
"Endpoint returns 201 on valid registration",
"Verification email sent within 5 seconds"
]
},
"parent": {
"id": "parent-456",
"title": "Add user registration flow",
"result_summary": "User model and migration created in branch feat/user-model.",
"metadata": {
"branch": "feat/user-model",
"commit": "a1b2c3d"
}
},
"prior_attempts": [
{
"attempt": 1,
"exit_code": 1,
"stderr_last_lines": [
"Error: test POST /register returned 500"
],
"started_at": "2026-06-08T14:00:00Z",
"finished_at": "2026-06-08T14:12:00Z"
}
],
"comments": [
{
"author": "human",
"body": "Make sure to use bcrypt with cost factor 12.",
"at": "2026-06-08T14:15:00Z"
}
],
"workspace": {
"repo": "https://github.com/org/api-service.git",
"branch": "task/abc-123",
"base_branch": "main"
}
}
| Field | Present when | Purpose |
|---|---|---|
task |
Always | Title, body, assignee, priority, criteria |
parent |
Task has a parent | Summary and metadata from parent result |
prior_attempts |
Task is a retry | Stderr tail + timestamps from failed runs |
comments |
Humans have commented | Thread of human-supplied guidance |
workspace |
Always | Git repo, branch name, base branch |
| Constraint | Value | Rationale |
|---|---|---|
| CPU | 4 cores total | VPS limit |
| RAM | 8 GB total | VPS limit |
| Max parallel containers | 3 | Leaves 1 core + headroom for orchestrator |
| RAM per container | 1.5 GB | --memory=1536m |
| CPU per container | 1 core | --cpus=1 |
| Container timeout | 30 min | Killed after 30 min, marked crashed |
| Disk per container | 5 GB | --storage-opt size=5g |
| Network | Outbound only | Only LLM APIs (api.anthropic.com, openai.com) |
| and git repos (github.com, gitlab.com) | ||
| Inbound network | None | Containers cannot receive external connections |
| Orchestrator dispatch loop | 30 s | dispatcher.js interval |
Max retries before gave_up |
3 | Circuit breaker |
| Role | Description | Default Model | Maker / Checker |
|---|---|---|---|
backend |
API routes, DB migrations, business logic, services | claude-sonnet-4-20250514 | Maker |
frontend |
UI components, styles, client-side logic, accessibility | claude-sonnet-4-20250514 | Maker |
qa |
Unit tests, integration tests, E2E, coverage enforcement | claude-sonnet-4-20250514 | Checker |
devops |
Dockerfiles, CI/CD config, Terraform, monitoring | claude-sonnet-4-20250514 | Maker |
sre |
Alerts, runbooks, on-call tooling, incident analysis | claude-sonnet-4-20250514 | Checker |
security |
Dependency audits, SAST, secrets scanning, threat models | claude-sonnet-4-20250514 | Checker |
When the decomposer generates subtasks, it assigns roles based on:
assignee if provided in the input.*.tf → devops, *.test.* → qa), keywords in the title/body.backend.