An open-source, agent-native project state store for AI development loops.
loopctl provides a multi-tenant REST API and CLI for AI coding agents and orchestrators to track project work breakdown, report progress, verify deliverables, and maintain audit trails. It solves the problem of AI agents fabricating results by separating self-reported progress from independently verified progress.
Website: loopctl.com
When AI coding agents implement large projects (25+ epics, 185+ stories), there is no reliable way to:
- Track what work has actually been completed vs. what agents claim is done
- Independently verify that deliverables match specifications
- Coordinate multiple implementation agents working in parallel
- Resume orchestration after session interruptions
- Maintain an audit trail of who did what and when
In practice, implementing agents fabricate review results, skip UI implementation while claiming backend is "complete," and self-report success without producing the required artifacts.
loopctl is a dumb state store with a two-tier trust model:
- Implementation agents write their own status (
agent_status: pending -> contracted -> assigned -> implementing -> reported_done) - An independent orchestrator reads those updates, performs verification, and writes its findings (
verified_status: unverified -> verified -> rejected) - It is structurally impossible for implementing agents to mark their own work as verified
loopctl does not make decisions, execute code, or run tests. It stores state, enforces access control, and serves data.
- Two-tier trust model -- agent_status and verified_status are written by different roles via
exact_roleenforcement - Multi-tenant with PostgreSQL Row Level Security -- every tenant's data is fully isolated
- Sprint contracts -- agents must acknowledge acceptance criteria before claiming stories
- Dependency graph -- epic and story dependencies with cycle detection, ready/blocked queries
- Webhook events -- real-time notifications on status changes, signed with HMAC-SHA256
- Audit trail -- immutable, append-only log of every mutation with partitioning and 90-day retention
- Skill versioning -- store, version, and track performance of orchestrator prompts
- Import/export -- bulk import user stories from JSON, export for round-trip fidelity
- CLI -- escript binary for all operations (
loopctl status,loopctl claim,loopctl verify) - Token cost intelligence -- agents report token usage per story; per-agent efficiency rankings, configurable budgets, and anomaly detection prevent runaway costs across long sprints
- OpenAPI 3.0 -- self-documenting API with Swagger UI for agent discovery
| Term | Definition |
|---|---|
| Tenant | An organization. All data is isolated per tenant via PostgreSQL RLS. |
| Project | A codebase being tracked (e.g., a GitHub repo). |
| Epic | A group of related stories within a project. |
| Story | The atomic unit of work with acceptance criteria. |
| Agent | An AI coding agent that implements features (implementer) or coordinates work (orchestrator). |
| Orchestrator | An AI agent that assigns work, verifies deliverables, and manages the development loop. |
| Two-tier trust | Implementing agents write agent_status; orchestrators write verified_status. Neither can write the other. |
| Contract | An agent acknowledges a story's acceptance criteria before claiming it. |
| Skill | A versioned prompt/instruction set used by orchestrators, with performance tracking. |
| Token Budget | A configurable token-consumption limit scoped to a story, agent, epic, or project. Exceeding the limit can warn or block story reports. |
| Cost Summary | An aggregated view of token usage per agent, with efficiency rankings and model mix breakdown. |
| Cost Anomaly | A story whose token consumption deviates beyond a configurable multiplier from the project baseline, flagged automatically. |
| Component | Technology |
|---|---|
| Language | Elixir 1.18 / Erlang OTP 27 |
| Framework | Phoenix 1.8 (API-only) |
| Database | PostgreSQL 16 with RLS |
| Background Jobs | Oban |
| HTTP Client | Req |
| Encryption | Cloak (AES-256-GCM) |
| CLI | Escript |
| Deployment | Docker Compose (PostgreSQL + App + Nginx) |
# Prerequisites: Elixir 1.18+, PostgreSQL 16+
git clone https://github.com/mkreyman/loopctl.git
cd loopctl
# 1. Create the loopctl_app role (needed for RLS in tests)
# The dev server uses the postgres superuser, but tests switch to
# the loopctl_app role via SET LOCAL ROLE to enforce RLS policies.
psql -U postgres -c "CREATE ROLE loopctl_app LOGIN PASSWORD 'loopctl_app_pass';"
psql -U postgres -c "GRANT ALL ON DATABASE loopctl_dev TO loopctl_app;"
# 2. Setup and run
# mix setup installs deps, creates the database, and runs migrations.
# A default Cloak encryption key is configured in config.exs for dev.
mix setup
mix phx.server # Start server at localhost:4000
# Verify it's working
curl http://localhost:4000/health
# Should return: {"status":"ok",...}
# Token efficiency commands (after registering a tenant + project)
loopctl token-usage --project my-app # Project cost summary
loopctl cost-anomalies --project my-app # Open anomalies
loopctl budget set --project my-app --scope per_story --limit 200000Note: The
CLOAK_KEYandSECRET_KEY_BASEenvironment variables are only required for production/Docker deployments. Dev uses defaults fromconfig/dev.exsandconfig/config.exs.
# Prerequisites: Docker, Docker Compose
cp .env.example .env
# Edit .env with your secrets (see "Generate Secrets" below)
# Generate TLS certificates for nginx (deploy/certs/ must exist)
mkdir -p deploy/certs
openssl req -x509 -newkey rsa:4096 -keyout deploy/certs/selfsigned.key \
-out deploy/certs/selfsigned.crt -days 365 -nodes \
-subj "/CN=loopctl.local"
docker compose build
docker compose up -d
docker compose exec -T app /app/bin/migrate
# Verify it's working
curl -sk https://localhost:8443/health
# Should return: {"status":"ok",...}# SECRET_KEY_BASE
mix phx.gen.secret
# CLOAK_KEY (32 bytes, base64)
elixir -e ':crypto.strong_rand_bytes(32) |> Base.encode64() |> IO.puts()'Ports: Local development runs on
http://localhost:4000. Docker deployment useshttps://localhost:8443(nginx TLS proxy). All examples below use the local dev URL.
Once running, the API is self-documenting:
GET /-- Redirects to/api/v1/GET /api/v1/-- Discovery endpoint with linksGET /api/v1/openapi-- Full OpenAPI 3.0 specification (machine-readable)GET /swaggerui-- Interactive Swagger UI (human-readable)GET /health-- Health check
loopctl uses role-based API keys. Each role has specific permissions in the two-tier trust model.
-
Register as a tenant — visit
https://loopctl.com/signup(requires a hardware authenticator: YubiKey, Touch ID, or Windows Hello). CLI-based registration is no longer supported. -
Create role-specific keys (using your user key):
# Create an agent key (needed to register agents) curl -X POST http://localhost:4000/api/v1/api_keys \ -H "Authorization: Bearer lc_user_key" \ -H "Content-Type: application/json" \ -d '{"name": "agent-bootstrap", "role": "agent"}' # Register an orchestrator agent curl -X POST http://localhost:4000/api/v1/agents/register \ -H "Authorization: Bearer lc_agent_bootstrap_key" \ -H "Content-Type: application/json" \ -d '{"name": "orchestrator-main", "agent_type": "orchestrator"}' # Note the agent ID from the response # Create the orchestrator key linked to the agent curl -X POST http://localhost:4000/api/v1/api_keys \ -H "Authorization: Bearer lc_user_key" \ -H "Content-Type: application/json" \ -d '{"name": "orchestrator-main", "role": "orchestrator", "agent_id": "<agent_id>"}' # Create an implementer agent key curl -X POST http://localhost:4000/api/v1/api_keys \ -H "Authorization: Bearer lc_user_key" \ -H "Content-Type: application/json" \ -d '{"name": "worker-1", "role": "agent"}'
-
Register your implementer agent (using the agent key):
curl -X POST http://localhost:4000/api/v1/agents/register \ -H "Authorization: Bearer lc_agent_key" \ -H "Content-Type: application/json" \ -d '{"name": "worker-1", "agent_type": "implementer"}'
Note: Each agent-role API key can register exactly ONE agent (one-to-one binding). Once an agent key has registered an agent, calling
/agents/registeragain with the same key returns 409. To register multiple agents, create separate agent keys for each:# Create keys for 2 implementation agents + 1 orchestrator bootstrap curl -X POST .../api_keys -d '{"name": "worker-1", "role": "agent"}' curl -X POST .../api_keys -d '{"name": "worker-2", "role": "agent"}' curl -X POST .../api_keys -d '{"name": "orch-bootstrap", "role": "agent"}'
Now the agent key can contract, claim, start, and report stories. The orchestrator key (linked to its agent) can verify and reject stories.
The chain-of-custody rule: nobody marks their own work as done. The implementer requests review; a different agent confirms it.
Setup:
1. Register tenant Visit /signup (WebAuthn required)
2. Create project POST /api/v1/projects
3. Import stories POST /api/v1/projects/:id/import
4. Register agent POST /api/v1/agents/register
Per story (implementer):
5. Get ready stories GET /api/v1/stories/ready?project_id=...
6. Contract story POST /api/v1/stories/:id/contract
7. Claim story POST /api/v1/stories/:id/claim
8. Start implementing POST /api/v1/stories/:id/start-work (or /start)
9. Request review POST /api/v1/stories/:id/request-review
↳ fires story.review_requested webhook
↳ implementer's role ENDS here
Per story (reviewer — must be a DIFFERENT agent):
10. Confirm implementation POST /api/v1/stories/:id/report (409 if caller == implementer)
11. Complete review POST /api/v1/stories/:id/review-complete (409 if caller == implementer)
↳ fires story.review_completed webhook
Per story (orchestrator):
12. Verify or reject POST /api/v1/stories/:id/verify OR /reject
↳ verify returns 409 if orchestrator == implementer
| Role | Can Do |
|---|---|
superadmin |
Everything. Cross-tenant via impersonation. |
user |
Manage tenant settings, API keys, projects, import/export. |
orchestrator |
Verify/reject stories. Write orchestrator state. Force-unclaim. |
agent |
Contract, claim, start, report stories. Submit artifacts. |
Role design note: The
userrole is for tenant administration -- managing settings, API keys, and projects. It does not participate in the development trust model. Theorchestratorandagentroles manage the development loop. This separation is by design: tenant admins provision infrastructure while the trust model governs the implementation/verification cycle.
Superadmin keys are created via the database or by a privileged script -- they cannot be created through the API since they require
tenant_id=NULL.
The two-tier trust model governs who can write which field. The chain-of-custody principle governs who can perform each handoff action: nobody marks their own work as done.
Three endpoints enforce caller identity at the API level. If the caller is the assigned agent, the request is rejected with 409:
| Endpoint | Blocked response | Meaning |
|---|---|---|
POST /stories/:id/report |
409 self_report_blocked |
The implementer cannot mark their own work as done — a different agent (reviewer) must call this |
POST /stories/:id/review-complete |
409 self_review_blocked |
The reviewer cannot declare their own review complete if they were the implementer |
POST /stories/:id/verify |
409 self_verify_blocked |
The orchestrator cannot verify a story they implemented |
The field stories.reported_by_agent_id tracks which agent confirmed the implementation (i.e., called /report). This must differ from the assigned implementer.
When an implementer finishes work, they do not call /report directly. Instead, they signal readiness:
POST /stories/:id/request-reviewThis transitions the story to a "review requested" state without advancing agent_status to reported_done. It fires a story.review_requested webhook. A different agent (the reviewer) then calls /report to confirm the implementation, followed by /review-complete to close the review. Only then can the orchestrator call /verify.
Agent endpoints (exact_role: agent):
POST /stories/:id/contract
POST /stories/:id/claim
POST /stories/:id/start (alias: /start-work)
POST /stories/:id/request-review (NEW — signals readiness, does NOT mark done)
Reviewer endpoints (different agent from implementer):
POST /stories/:id/report (alias: /report-done — blocked if caller == assigned_agent_id)
POST /stories/:id/review-complete (blocked if caller == assigned_agent_id)
Orchestrator endpoints (exact_role: orchestrator):
POST /stories/:id/verify (requires review_type and summary — blocked if caller == assigned_agent_id)
POST /stories/:id/reject
POST /stories/:id/force-unclaim
POST /stories/bulk/mark-complete (mark pre-existing stories complete in one call)
POST /epics/:id/verify-all (verify all reported_done stories in an epic)
An agent key cannot call verify/reject. An orchestrator key cannot call claim/start/request-review. This is enforced at the plug level with strict atom equality -- no role hierarchy bypass. Identity gates are an additional layer enforced regardless of role.
Before claiming a story, agents must contract it -- acknowledging they have read the acceptance criteria by echoing back the story title and AC count. This prevents agents from claiming stories they have not read:
# Agent fetches story, reads ACs, then contracts
curl -X POST http://localhost:4000/api/v1/stories/:id/contract \
-H "Authorization: Bearer lc_agent_key" \
-H "Content-Type: application/json" \
-d '{"story_title": "Implement user auth", "ac_count": 8}'If the title or count does not match, the contract is rejected.
Orchestrators can skip contract validation for bulk operations using skip_contract_check: true
(orchestrator role only):
curl -X POST http://localhost:4000/api/v1/stories/:id/contract \
-H "Authorization: Bearer lc_orch_key" \
-H "Content-Type: application/json" \
-d '{"skip_contract_check": true}'GET /api/v1/stories returns stories with flexible filters. Supports up to 500 results per page
via limit and offset.
# All stories in a project
curl http://localhost:4000/api/v1/stories?project_id=<id>
# Filter by status fields
curl "http://localhost:4000/api/v1/stories?project_id=<id>&agent_status=reported_done&verified_status=unverified"
# Filter to a specific epic
curl "http://localhost:4000/api/v1/stories?project_id=<id>&epic_id=<epic_id>"
# Paginate a large project
curl "http://localhost:4000/api/v1/stories?project_id=<id>&limit=500&offset=0"
curl "http://localhost:4000/api/v1/stories?project_id=<id>&limit=500&offset=500"Available query parameters: project_id (required), agent_status, verified_status, epic_id,
limit (max 500, default 100), offset (default 0).
UI test runs track project-level, ad-hoc QA walkthroughs against a running application. They are not tied to individual stories — a UI test run covers the whole app from a user's perspective.
UI testing is optional. Not all projects need it. Run a UI test pass when you want to verify the full application works end-to-end after a batch of stories has been merged.
Start a UI test run:
curl -X POST http://localhost:4000/api/v1/projects/:id/ui_test_runs \
-H "Authorization: Bearer lc_orch_key" \
-H "Content-Type: application/json" \
-d '{"notes": "Post-epic-37 QA pass"}'
# Returns: {"data": {"id": "<run_uuid>", "status": "running", ...}}Record findings (one call per finding):
curl -X POST http://localhost:4000/api/v1/ui_test_runs/:id/findings \
-H "Authorization: Bearer lc_orch_key" \
-H "Content-Type: application/json" \
-d '{
"severity": "bug",
"title": "Login form does not show error on invalid password",
"steps": "1. Visit /login\n2. Enter wrong password\n3. Submit",
"expected": "Error message displayed below the password field",
"actual": "Page reloads with no feedback"
}'Complete a UI test run:
curl -X POST http://localhost:4000/api/v1/ui_test_runs/:id/complete \
-H "Authorization: Bearer lc_orch_key" \
-H "Content-Type: application/json" \
-d '{"summary": "3 bugs found, 1 enhancement suggestion"}'List runs for a project:
curl "http://localhost:4000/api/v1/projects/:id/ui_test_runs" \
-H "Authorization: Bearer lc_orch_key"Get a single run with its findings:
curl "http://localhost:4000/api/v1/ui_test_runs/:id" \
-H "Authorization: Bearer lc_orch_key"Each finding has the following fields:
| Field | Required | Description |
|---|---|---|
severity |
Yes | One of: bug, enhancement, blocker |
title |
Yes | Short description of the finding |
steps |
No | Reproduction steps |
expected |
No | Expected behavior |
actual |
No | Actual behavior observed |
Mark pre-existing stories as complete in a single request. Useful when bootstrapping a project that already has completed work:
curl -X POST http://localhost:4000/api/v1/stories/bulk/mark-complete \
-H "Authorization: Bearer lc_orch_key" \
-H "Content-Type: application/json" \
-d '{
"stories": [
{"story_id": "<uuid>", "summary": "Pre-existing implementation", "review_type": "pre_existing"},
{"story_id": "<uuid>", "summary": "Carried over from v1", "review_type": "pre_existing"}
]
}'Verify all reported_done stories in an epic at once (orchestrator only). Both review_type and
summary are required:
curl -X POST http://localhost:4000/api/v1/epics/:id/verify-all \
-H "Authorization: Bearer lc_orch_key" \
-H "Content-Type: application/json" \
-d '{"review_type": "enhanced", "summary": "All stories reviewed and AC-compliant"}'The verify endpoint requires both review_type and summary. Omitting either returns 422:
curl -X POST http://localhost:4000/api/v1/stories/:id/verify \
-H "Authorization: Bearer lc_orch_key" \
-H "Content-Type: application/json" \
-d '{"result": "pass", "review_type": "enhanced", "summary": "All acceptance criteria met"}'When a state transition is invalid, 409 responses include structured context:
{
"error": {
"message": "Conflict",
"current_state": "assigned",
"attempted_action": "claim",
"hints": ["Story is already claimed by another agent. Use force-unclaim to reset it."]
}
}The current_state, attempted_action, and hints fields are always present on 409 responses
from story transition endpoints. Read hints to understand the corrective action.
pending --> contracted --> assigned --> implementing --> reported_done
^ |
| +-------------------------------------------+
| v
| +---------+
| | VERIFY |--> verified
| | or |
| | REJECT |--> rejected --> auto-reset to pending
| +---------+
|
+---- unclaim / force-unclaim (back to pending)
The import endpoint accepts a structured JSON payload with epics, stories, and optional dependencies:
curl -X POST http://localhost:4000/api/v1/projects/:id/import \
-H "Authorization: Bearer lc_user_key" \
-H "Content-Type: application/json" \
-d '{
"epics": [
{
"number": 1,
"title": "User Authentication",
"description": "Auth infrastructure",
"stories": [
{
"number": "1.1",
"title": "Implement login endpoint",
"acceptance_criteria": [
{"criterion": "POST /login returns JWT on valid credentials"},
{"criterion": "Invalid credentials return 401"}
]
},
{
"number": "1.2",
"title": "Implement logout endpoint",
"acceptance_criteria": [
{"criterion": "POST /logout invalidates the session"}
]
}
]
}
],
"story_dependencies": [
{"story": "1.1", "depends_on": "1.2"}
],
"epic_dependencies": [
{"epic": 1, "depends_on": 2}
]
}'Each epic requires number (integer) and title (string). Each story requires number (string, e.g. "1.1") and title. Stories are nested under their epic's stories array. Story dependencies use "story" and "depends_on" keys referencing story numbers. Epic dependencies use "epic" and "depends_on" keys referencing epic numbers. All dependencies are validated for cycles.
Stories can be imported with initial status overrides using initial_agent_status and
initial_verified_status. This allows bootstrapping projects where some or all work already exists:
{
"stories": [
{
"number": "1.1",
"title": "Database schema",
"acceptance_criteria": [{"criterion": "Migrations applied"}],
"initial_agent_status": "reported_done",
"initial_verified_status": "pass"
}
]
}Stories imported with initial_verified_status: "pass" are treated as already verified and will
not appear in the ready queue or block dependent stories.
Subscribe to real-time notifications for these event types:
| Event Type | Fired When |
|---|---|
story.status_changed |
Agent transitions story status (contract, claim, start) |
story.review_requested |
Implementer calls /request-review — signals readiness for handoff |
story.review_completed |
Reviewer calls /review-complete — review cycle is closed |
story.verified |
Orchestrator verifies a story |
story.rejected |
Orchestrator rejects a story |
story.auto_reset |
Rejected story auto-resets to pending |
story.force_unclaimed |
Orchestrator force-unclaims a story |
epic.completed |
All stories in an epic are verified |
artifact.reported |
Agent submits an artifact report |
agent.registered |
New agent registers |
project.imported |
Work breakdown imported into a project |
webhook.test |
Manual test ping via POST /webhooks/:id/test |
Every webhook delivery is a JSON POST with the following envelope. The data fields vary by event type.
story.status_changed -- fired on contract, claim, start, report:
{
"event": "story.status_changed",
"story_id": "a1b2c3d4-...",
"project_id": "b2c3d4e5-...",
"epic_id": "c3d4e5f6-...",
"old_status": "pending",
"new_status": "contracted",
"agent_id": "d4e5f6a7-...",
"timestamp": "2026-03-27T12:00:00Z"
}story.verified -- fired when orchestrator verifies a story:
{
"event": "story.verified",
"story_id": "a1b2c3d4-...",
"project_id": "b2c3d4e5-...",
"epic_id": "c3d4e5f6-...",
"orchestrator_agent_id": "e5f6a7b8-...",
"summary": "All acceptance criteria met",
"timestamp": "2026-03-27T14:00:00Z"
}story.rejected -- fired when orchestrator rejects a story:
{
"event": "story.rejected",
"story_id": "a1b2c3d4-...",
"project_id": "b2c3d4e5-...",
"epic_id": "c3d4e5f6-...",
"orchestrator_agent_id": "e5f6a7b8-...",
"reason": "Missing test coverage for edge cases",
"findings": {"missing_tests": ["empty input handling", "error boundary"]},
"timestamp": "2026-03-27T15:00:00Z"
}The findings field is a map (object) matching whatever the orchestrator passed in the reject request body. It defaults to {} if omitted.
Payloads are signed with HMAC-SHA256 using the webhook's secret. Verify the X-Loopctl-Signature header to authenticate delivery.
# Elixir
expected = :crypto.mac(:hmac, :sha256, signing_secret, raw_body)
|> Base.encode16(case: :lower)
signature = "sha256=" <> expected
# Compare with X-Loopctl-Signature header# Bash
echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/.* /sha256=/'API requests are rate-limited per API key and per tenant:
- Per key: 300 requests/minute (configurable via tenant settings)
- Per tenant: 3x the per-key limit (aggregate across all keys)
- Registration: 5 requests/hour per IP address
- Superadmin: exempt from rate limiting
Rate limit headers are included in every authenticated response:
| Header | Description |
|---|---|
X-RateLimit-Limit |
Requests allowed per window |
X-RateLimit-Remaining |
Requests remaining in current window |
X-RateLimit-Reset |
Unix timestamp when the window resets |
429 responses include a Retry-After header and retry_after_seconds in the JSON body:
{"error": {"status": 429, "message": "Too many requests. Retry after 45 seconds.", "retry_after_seconds": 45}}GET /api/v1/routes
Returns all available API endpoints with method, path, and description. Agents can call this first to discover the API without probing blindly.
All list endpoints accept both limit and page_size as query params (bidirectional aliasing). Use whichever you prefer — they are interchangeable.
All list endpoints support page-based pagination:
?page=1&page_size=20(defaults)- Maximum
page_size: 100 (general endpoints) - Maximum
limit: 500 (GET /storiesendpoint)
Responses include metadata:
{
"data": [...],
"meta": {
"page": 1,
"page_size": 20,
"total_count": 42,
"total_pages": 3
}
}The change feed (GET /api/v1/changes) uses cursor-based pagination with ?since=<ISO8601>. Responses include has_more and next_since for continuation.
All errors follow a consistent envelope format:
{"error": {"message": "Not found", "status": 404}}Validation errors include field-level details:
{
"error": {
"message": "Validation failed",
"status": 422,
"details": {
"slug": ["has already been taken"],
"email": ["can't be blank"]
}
}
}The CLI is an escript binary that wraps the REST API:
# Build
mix escript.build
# Configure
./loopctl auth login --server https://loopctl.local:8443 --key lc_your_key
# Use
./loopctl status --project my-project
./loopctl next --project my-project
./loopctl claim US-2.1
./loopctl verify US-2.1 --result pass --summary "All ACs met"
./loopctl skill list
./loopctl admin statsDefault output is JSON (agent-first). Use --format human for tables.
loopctl ships with an MCP (Model Context Protocol) server that gives Claude Code agents direct typed tool access — no curl, no bash, no JSON parsing.
# Install dependencies
cd mcp-server && npm install
# Add to ~/.claude/mcp.json (global) or <project>/.mcp.json (per-project)
{
"mcpServers": {
"loopctl": {
"command": "node",
"args": ["/path/to/loopctl/mcp-server/index.js"],
"env": {
"NODE_TLS_REJECT_UNAUTHORIZED": "0",
"LOOPCTL_SERVER": "https://192.168.86.55:8443",
"LOOPCTL_ORCH_KEY": "lc_your_orchestrator_key",
"LOOPCTL_AGENT_KEY": "lc_your_agent_key",
"LOOPCTL_REVIEWER_KEY": "lc_your_reviewer_key"
}
}
}
}Keys must be in the env block — the MCP server process does not inherit the shell environment.
| Tool | Description | API Key Used |
|---|---|---|
get_tenant |
Verify connectivity (current tenant info) | orchestrator |
list_projects |
List all projects | orchestrator |
create_project |
Create a new project | orchestrator |
get_progress |
Project progress summary (supports include_cost) |
orchestrator |
import_stories |
Import epics and stories | orchestrator |
list_stories |
List stories with filters (supports include_token_totals) |
orchestrator |
list_ready_stories |
Stories ready for work | orchestrator |
get_story |
Get story details | orchestrator |
contract_story |
Contract a story | agent |
claim_story |
Claim a story | agent |
start_story |
Start implementation | agent |
request_review |
Signal readiness for review | agent |
report_story |
Mark implementation done (supports token_usage) |
orchestrator |
review_complete |
Record review completion | orchestrator |
verify_story |
Verify a story | orchestrator |
reject_story |
Reject a story | orchestrator |
bulk_mark_complete |
Bulk mark stories complete | orchestrator |
verify_all_in_epic |
Verify all in an epic | orchestrator |
report_token_usage |
Report token consumption for a story session | agent |
get_cost_summary |
Project cost summary with optional breakdown | orchestrator |
get_story_token_usage |
Token usage records for a story | orchestrator |
get_cost_anomalies |
Cost anomaly alerts | orchestrator |
set_token_budget |
Set token budget for a scope | orchestrator |
knowledge_index |
Load knowledge wiki catalog | agent |
knowledge_search |
Search knowledge wiki by topic | agent |
knowledge_get |
Get full article content by ID | agent |
knowledge_context |
Get relevance-ranked articles for a task query | agent |
knowledge_create |
Create a new knowledge article | agent |
knowledge_publish |
Publish a draft article | orchestrator |
knowledge_drafts |
List draft (unpublished) articles | orchestrator |
knowledge_lint |
Lint check for stale or low-coverage articles | orchestrator |
knowledge_export |
Export all articles as ZIP archive | orchestrator |
list_routes |
Discover all API endpoints | orchestrator |
Agents call tools directly: mcp__loopctl__get_tenant(), mcp__loopctl__list_projects(), mcp__loopctl__create_project({name: "MyApp", slug: "myapp"}). No curl or bash needed.
lib/loopctl/
tenants/ # Multi-tenancy
auth/ # API keys, RBAC
audit/ # Immutable audit log
agents/ # Agent registry
projects/ # Projects CRUD
work_breakdown/ # Epics, stories, dependencies, graph queries
progress/ # Two-tier status tracking
artifacts/ # Artifact reports, verification results
orchestrator/ # State checkpointing
webhooks/ # Subscriptions, events, delivery
import_export/ # Bulk import/export
bulk_operations/ # Bulk claim/verify/reject
skills/ # Skill versioning + performance
token_usage/ # Token consumption tracking, budgets, cost anomalies, analytics
quality_assurance/ # UI test runs and findings
cli/ # Escript CLI commands
lib/loopctl_web/
controllers/ # 26 JSON API controllers
plugs/ # Auth pipeline (7 plugs)
mix precommit # Full quality gate: compile, format, credo, dialyzer, test
mix test # Run 1582 tests
mix test --failed # Re-run failures
mix ecto.reset # Drop, create, migrate
mix escript.build # Build CLI binary- Orchestration Guide -- How to use loopctl to manage AI development projects (methodology, skills, step-by-step walkthrough)
- PRD -- Full product requirements document
- User Stories -- 75 stories across 17 epics
- Skills -- 6 orchestration skill definitions (read by the orchestrator during the loop)
- OpenAPI Spec -- Machine-readable API spec (when running)
loopctl deploys as a 3-container Docker Compose stack:
| Service | Image | Port |
|---|---|---|
| db | postgres:16 | Internal |
| app | Elixir release | 4000 (internal) |
| nginx | nginx:alpine | 8443 (HTTPS), 8080 (HTTP redirect) |
See deploy/ for nginx config, systemd service, backup scripts, and setup guide.
Create the RLS role used by tests:
psql -U postgres -c "CREATE ROLE loopctl_app LOGIN PASSWORD 'loopctl_app_pass';"Run pending migrations and restart the app:
docker compose exec -T app /app/bin/migrate
docker compose restart appThe claim endpoint requires exact_role: :agent. User and orchestrator keys cannot claim stories. Create an agent key:
curl -X POST http://localhost:4000/api/v1/api_keys \
-H "Authorization: Bearer lc_user_key" \
-H "Content-Type: application/json" \
-d '{"name": "worker-1", "role": "agent"}'Story numbers must be plain strings like "1.1" (no "US-" prefix). Epic numbers are integers. Dependencies use number references:
{"story": "1.1", "depends_on": "1.2"}
{"epic": 1, "depends_on": 2}Superadmin keys are tenant-less. Use the X-Impersonate-Tenant header for tenant-scoped endpoints:
curl http://localhost:4000/api/v1/projects \
-H "Authorization: Bearer lc_superadmin_key" \
-H "X-Impersonate-Tenant: <tenant_id>"The orchestrator API key must have agent_id set, linking it to a registered agent with agent_type: orchestrator. Create the agent first via /agents/register, then create the orchestrator key with agent_id:
# 1. Register the orchestrator agent (using an agent-role bootstrap key)
curl -X POST http://localhost:4000/api/v1/agents/register \
-H "Authorization: Bearer lc_agent_bootstrap_key" \
-H "Content-Type: application/json" \
-d '{"name": "orchestrator-main", "agent_type": "orchestrator"}'
# Note the agent ID from the response
# 2. Create the orchestrator key with agent_id
curl -X POST http://localhost:4000/api/v1/api_keys \
-H "Authorization: Bearer lc_user_key" \
-H "Content-Type: application/json" \
-d '{"name": "orchestrator-main", "role": "orchestrator", "agent_id": "<agent_id>"}'This project is licensed under the MIT License. See the LICENSE file for details.
See CONTRIBUTING.md for guidelines and GitHub Issues for open items.