About Loopctl MCP Server
Loopctl MCP Server is an MCP server published by mkreyman in the AI category: mCP server for loopctl — structural trust for AI development loops.
The repository has 3 stars and 1 forks, with the last commit yesterday. Recent commit activity is the strongest signal that reported issues will get attention.
Install
claude mcp add loopctl-mcp-server -- npx -y loopctl-mcp-servernpx -y loopctl-mcp-serverThis server has no ConduID identity, so agent calls to it are not receipted. Pin the version you install and review the source before granting it credentials.
Tools (79)
backfill_storyMark a story verified for work done outside loopctl
Parameters not published.
bulk_mark_completeBulk mark stories complete
Parameters not published.
claim_storyClaim a story
Parameters not published.
contract_storyContract a story
Parameters not published.
create_projectCreate a new project
Parameters not published.
create_storyCreate a single story in an existing epic
Parameters not published.
delete_projectDelete a project and all dependents (irreversible)
Parameters not published.
dispatchMint an ephemeral key for a sub-agent dispatch (Chain of Custody v2)
Parameters not published.
get_acceptance_criteriaStory acceptance criteria with verification status
Parameters not published.
get_cost_anomaliesCost anomaly alerts
Parameters not published.
Show all 79 tools
get_cost_summaryProject cost summary with optional breakdown
Parameters not published.
get_progressProject progress summary (supports `include_cost`)
Parameters not published.
get_sthGet the latest Signed Tree Head for the audit chain (public)
Parameters not published.
get_storyGet story details
Parameters not published.
get_story_token_usageToken usage records for a story
Parameters not published.
get_system_articlesList/fetch system-scoped wiki articles (public)
Parameters not published.
get_tenantVerify connectivity (current tenant info)
Parameters not published.
import_storiesImport epics and stories (supports `merge`)
Parameters not published.
knowledge_agent_usagePer-agent knowledge usage
Parameters not published.
knowledge_analytics_topTop accessed articles
Parameters not published.
knowledge_archiveSoft-delete an article (retained for audit) — TERMINAL, not undoable by any agent call
Parameters not published.
knowledge_article_statsPer-article usage stats
Parameters not published.
knowledge_bulk_deleteBulk soft-delete (archive), partial-success — by id-list, source, or tag+confirm
Parameters not published.
knowledge_bulk_publishPublish drafts, partial-success (per-id results, idempotent, no 100-cap)
Parameters not published.
knowledge_bulk_unpublishBulk revert published articles to draft (archive), partial-success
Parameters not published.
knowledge_conflictsList potential-conflict article pairs the auto-linker/nightly lint flagged as "too similar to coexist" (you judge redundancy vs contradiction).
Parameters not published.
knowledge_contextGet relevance-ranked articles for a task query, scoped to agent's visible articles (with linked references).
Parameters not published.
knowledge_countCount articles matching filters within agent's visible set.
Parameters not published.
knowledge_createCreate an article (published by default; `draft: true` to stage; `idempotency_key` for idempotent capture). Agent-authored articles are `visibility: owner` by default; use `metadata: {visibility: "shared"}` to make visible to other agents.
Parameters not published.
knowledge_curation_logHuman-readable feed of KB curation adjustments (gate/supersede/merge/dismiss); recorded only while tenant `settings.kb_curation_log` is on
Parameters not published.
knowledge_deleteAlias for archive (DELETE verb archives)
Parameters not published.
knowledge_distant_pairsDistant-but-bridgeable article pairs in optimal-novelty band, sampled from agent's visible published articles.
Parameters not published.
knowledge_draftsList draft (unpublished) articles
Parameters not published.
knowledge_exportExport all articles as a ZIP archive
Parameters not published.
knowledge_facetsTag facet counts over agent's visible filtered article set (drives faceted browse).
Parameters not published.
knowledge_getGet full article content by ID
Parameters not published.
knowledge_graphMulti-hop link-graph traversal from a seed article, bounded to agent's visible articles (typed edges, depth/fan-out caps).
Parameters not published.
knowledge_hybrid_searchResolve a query to a governed **curated** answer when one genuinely answers it, else falls back to semantic/keyword **retrieval** — one shape with `meta.provenance` (`curated`/`retrieved`), `confidence`, `curated_article_id`. See [`docs/knowledge-hybrid-retrieval.md`](docs/knowledge-hybrid-retrieval.md).
Parameters not published.
knowledge_indexLoad knowledge wiki catalog (per-category, `fields` projection). Agent callers see only articles they own or marked `shared`.
Parameters not published.
knowledge_ingestSubmit a URL/content for knowledge extraction (draft by default; `publish: true` to publish)
Parameters not published.
knowledge_ingest_batchSubmit up to 50 ingestion items (per-item / batch `publish`)
Parameters not published.
knowledge_ingestion_jobsList recent ingestion jobs
Parameters not published.
knowledge_lintLint check for stale or low-coverage articles
Parameters not published.
knowledge_listList articles (full fields except `idempotency_key`, which is filter-only, lag-free, all-status) — filter by tag/source_type/source_id/idempotency_key for dedup/enumerate/repair. Agent visibility filtering applies.
Parameters not published.
knowledge_noveltyScore ideas (`texts:[string]` or `ideas:[string\
Parameters not published.
knowledge_okf_exportExport the wiki as an OKF v0.1 bundle
Parameters not published.
knowledge_okf_importImport an OKF v0.1 bundle from a directory
Parameters not published.
knowledge_progressive_drillFetch the full body of one stub returned by `knowledge_progressive_index`.
Parameters not published.
knowledge_progressive_indexCompact, top-K-capped topic stubs (id/title/category/summary, no bodies), curated-preferred, hub-linked one hop via `:relates_to`. Follow up with `knowledge_progressive_drill`.
Parameters not published.
knowledge_publishPublish an existing draft article
Parameters not published.
knowledge_random_walkRandom walk through link graph from start article, traversing only agent's visible published articles.
Parameters not published.
knowledge_resolve_conflictRecord a verdict on a conflict pair (`dismiss`/`supersede`/`merge`); the nightly executor acts on `supersede`/`merge` only at `confidence: high`.
Parameters not published.
knowledge_retrieval_metricsDaily retrieval-precision time series (search → open follow-through); `precision` is per surfaced RESULT, `search_follow_through` is per search CALL
Parameters not published.
knowledge_searchSearch knowledge wiki by topic
Parameters not published.
knowledge_statsAggregate article counts (total, by_category, by_status) scoped to agent's visible articles.
Parameters not published.
knowledge_suggest_linksRanked typed-link candidates for an article by embedding similarity among visible articles (read-only).
Parameters not published.
knowledge_suppressTake an article out of RETRIEVAL, reversibly, without changing its status; still readable by id. Requires a `reason`
Parameters not published.
knowledge_unpublishRevert a published article back to draft
Parameters not published.
knowledge_unsuppressLift a retrieval suppression, restoring the article to every read path
Parameters not published.
knowledge_unused_articlesPublished articles with zero accesses
Parameters not published.
list_projectsList all projects
Parameters not published.
list_ready_storiesStories ready for work
Parameters not published.
list_routesDiscover all API endpoints
Parameters not published.
list_storiesList stories with filters (supports `include_token_totals`)
Parameters not published.
memory_forgetDelete one of the caller's OWN memories by id (404 cross-scope, no existence leak).
Parameters not published.
memory_listList the caller's OWN long-term memories (paginated); superadmin `all_subjects=true` lists a tenant's every subject.
Parameters not published.
memory_promoteCompile a session's short-term memory into durable long-term memory (async job; caller's own sessions only)
Parameters not published.
memory_recallSemantically recall the caller's OWN long-term memories by `query` (degrades to a scoped text match, never a silent empty). Private memory, not `knowledge_search`.
Parameters not published.
memory_rememberWrite agent MEMORY (private to the caller's `(tenant, subject)` scope) — a `long_term` fact (embedded, semantically recalled) or a `session` turn. NOT the shared Knowledge Wiki — use `knowledge_create` for curated tenant knowledge.
Parameters not published.
recover_capRe-mint a capability token after a session crash
Parameters not published.
reject_storyReject a story
Parameters not published.
report_storyMark implementation done (supports `token_usage`)
Parameters not published.
report_token_usageReport token consumption for a story session
Parameters not published.
request_reviewSignal readiness for review
Parameters not published.
review_completeRecord review completion
Parameters not published.
set_token_budgetSet token budget for a scope
Parameters not published.
start_storyStart implementation
Parameters not published.
verify_all_in_epicVerify all in an epic
Parameters not published.
verify_storyVerify a story
Parameters not published.
Ask AI
Ask AI about Loopctl MCP Server
Powered by Claude · Grounded in docs
Security checks
- ✓README presentThe repository ships documentation.
5 checks have not been run yet.
README
loopctl
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
The Problem
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.
The Solution
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.
Key Features
- 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
Concepts
| 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. |
Tech Stack
| 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) |
Quick Start
Local Development
# 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 200000
Note: The
CLOAK_KEYandSECRET_KEY_BASEenvironment variables are only required for production/Docker deployments. Dev uses defaults fromconfig/dev.exsandconfig/config.exs.
Docker Deployment
# 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",...}
Generate Secrets
# 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.
API Overview
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
Authentication Flow
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.
Typical Agent Workflow
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
Roles
| 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.
Two-Tier Trust Model and Chain-of-Custody Enforcement
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.
Identity Gates
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.
Request-Review Endpoint
When an implementer finishes work, they do not call /report directly. Instead, they signal readiness:
POST /stories/:id/request-review
This 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.
Endpoint Reference
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.
Sprint Contracts
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}'
Listing Stories
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
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"
Finding Format
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 |
Bulk and Admin Endpoints
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"}'
Verify Endpoint Requirements
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"}'
409 Conflict Responses
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.
Story Lifecycle
pending --> contracted --> assigned --> implementing --> reported_done
^ |
| +-------------------------------------------+
| v
| +---------+
| | VERIFY |--> verified
| | or |
| | REJECT |--> rejected --> auto-reset to pending
| +---------+
|
+---- unclaim / force-unclaim (back to pending)
Import JSON Format
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.
Importing Pre-existing Work
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.
Webhook Event Types
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 |
Webhook Payloads
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.
Verifying Webhook Signatures
# 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=/'
Rate Limiting
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}}
Route Discovery
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.
Parameter Aliasing
All list endpoints accept both limit and page_size as query params (bidirectional aliasing). Use whichever you prefer — they are interchangeable.
Pagination
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.
Error Responses
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"]
}
}
}
CLI
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 stats
Default output is JSON (agent-first). Use --format human for tables.
MCP Server (for Claude Code Agents)
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.
Setup
# 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.
Available Tools (33)
| 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.
Project Structure
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)
Development
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
Documentation
- 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)
Deployment
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.
Troubleshooting
mix setup fails with "role loopctl_app does not exist"
Create the RLS role used by tests:
psql -U postgres -c "CREATE ROLE loopctl_app LOGIN PASSWORD 'loopctl_app_pass';"
Health check returns {"oban":"error"}
Run pending migrations and restart the app:
docker compose exec -T app /app/bin/migrate
docker compose restart app
Agent gets 403 on /stories/:id/claim
The 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"}'
Import returns 422 with validation errors
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 gets 403 on tenant-scoped endpoints
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>"
Orchestrator verify returns 500
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>"}'
License
This project is licensed under the MIT License. See the LICENSE file for details.
Contributing
See CONTRIBUTING.md for guidelines and GitHub Issues for open items.
README copy captured when this server was indexed; it may lag the repository. The original is authoritative.