Agent-native project state store for AI development loops. Stack: Elixir 1.18 / Phoenix 1.8, PostgreSQL with RLS, Oban, Req, Cloak. Web: Phoenix LiveView (landing page + future admin), Tailwind CSS v4.
Also read AGENTS.md — contains Phoenix 1.8 framework guidelines, Elixir conventions, Ecto patterns.
Refer to docs/design-system.md for full specifications. Key points:
- Dark mode only — no light mode for v1
- Color palette: cool slate grays (
slate-*), deep indigo-blue accent (accent-*) - Typography: Geist (headings + body), Geist Mono (IDs, agent names, code, status badges)
- Cards:
rounded-md(6px), no shadows on inline cards, border-only structure - Terminal aesthetic: monospace for data, cool blue-gray tones, precision over decoration
- Anti-patterns: no rounded-xl, no gradients, no glassmorphism, no warm grays, no centered heroes
YOU MUST load the orchestration protocol and build status from memory-keeper at the start of every conversation:
mcp__memory-keeper__context_get({ key: "CRITICAL_ALWAYS_READ_FIRST_PRINCIPLES", channel: "loopctl" })
mcp__memory-keeper__context_get({ key: "build_status", channel: "loopctl" })
These contain:
- The orchestration loop rules (you are READ-ONLY on code, you dispatch agents)
- Current build progress (which epics/stories are done)
- Architectural decisions made during implementation
- The DI, fixture, and mock patterns to enforce
Do NOT proceed with any implementation work until you have loaded and read both keys.
lib/loopctl/
├── tenants/ # Tenants, multi-tenancy
├── auth/ # API keys, auth pipeline, RBAC
├── audit/ # Immutable audit log
├── agents/ # Agent registry
├── projects/ # Projects CRUD
├── work_breakdown/ # Epics, stories, dependencies
├── progress/ # Two-tier status tracking
├── artifacts/ # Artifact reports, verification results
├── orchestrator/ # Orchestrator state checkpointing
├── webhooks/ # Webhook subscriptions, events, delivery
├── import_export/ # Bulk import/export
├── skills/ # Skill versioning and performance
├── quality_assurance/ # UI test runs and findings (project-level QA)
├── token_usage/ # Token consumption tracking, budgets, cost anomalies
├── schema.ex # Base schema macro
└── repo.ex # Ecto Repo
lib/loopctl_web/
├── controllers/ # JSON API controllers
├── plugs/ # Auth pipeline plugs
├── router.ex # API routes under /api/v1/
└── fallback_controller.ex
- Context modules:
Loopctl.Tenants,Loopctl.Auth,Loopctl.Progress, etc. - Schema modules:
Loopctl.Tenants.Tenant,Loopctl.Auth.ApiKey, etc. - Controllers:
LoopctlWeb.TenantController,LoopctlWeb.StoryController, etc. - Oban workers:
Loopctl.Workers.WebhookDeliveryWorker, etc. - Behaviours:
Loopctl.HealthCheck.Behaviour,Loopctl.Webhooks.DeliveryBehaviour, etc.
CRITICAL: loopctl is multi-tenant. Every tenant's data is isolated via PostgreSQL RLS.
- EVERY table (except
tenantsand global tables) hastenant_id - EVERY context function takes
tenant_idas the first argument - EVERY query is scoped by RLS policies (SET LOCAL per transaction)
tenant_idis NEVER in changesetcast— always set programmatically- EVERY test includes a tenant isolation test case
- Two Repos:
Loopctl.Repo(RLS enforced) andLoopctl.AdminRepo(BYPASSRLS for superadmin)
EVERY change to loopctl must be evaluated against this checklist before merging.
Design spec: the Chain of Custody v2 design lives in docs/chain-of-custody-v2.md.
Sections 2.1 and 9 in particular establish the human-rooted signup ceremony (WebAuthn)
and the chain-of-custody invariants that the rest of the system relies on. Consult it
before changing anything in the auth, signup, or audit pipelines.
superadmin (4) > user (3) > orchestrator (2) > agent (1)
Higher roles can access lower-role endpoints. The hierarchy is enforced by RequireRole plug.
The API enforces that nobody marks their own work as done:
POST /stories/:id/report—409 self_report_blockedif caller == assigned_agent_idPOST /stories/:id/review-complete—409 self_review_blockedif caller == assigned_agent_idPOST /stories/:id/verify—409 self_verify_blockedif caller == assigned_agent_id
The MCP server must NEVER hold both implementer and reviewer keys in the same process. The 409 errors are the system working correctly — do not add workarounds.
Ask these questions:
- Does this weaken chain-of-custody? If a single session could now both implement and verify/report, the change is WRONG.
- Does this give agents destructive capabilities? ALL destructive operations (any DELETE, archive, budget/token corrections, cost anomaly resolution, tenant audit key rotation) must stay at
role: :user. Constructive and metadata work-breakdown operations (create/update epics, stories, dependencies, imports, backfills for pre-loopctl work) are atrole: :orchestratorso an autonomous orchestrator can compose a project and record state without human intervention. Agents (role: :agent) can never write work-breakdown data — only read it. The rule of thumb: if the operation REMOVES data, it requires:user. - Does this collapse trust boundaries? The role hierarchy exists so that agents can't self-promote. Lowering a role requirement is fine for read operations and for operations the role logically needs (orchestrators creating projects). It's wrong for operations that serve as a security gate.
- Does this affect RLS? New tables must use
ENABLE ROW LEVEL SECURITY(notFORCE) since the production role (schema_admin) is the table owner without BYPASSRLS.
The trust model is enforced by six layers:
- L0 Human + hardware anchor (WebAuthn at tenant signup)
- L1 Capability tokens (signed, scoped, non-replayable)
- L2 Database invariants (FK, CHECK, triggers, partial indexes)
- L3 Independent re-execution (SWE-bench-style verification)
- L4 Structural role separation (dispatch lineage, rotating verifier)
- L5 Behavioral detection (lazy-bastard score, CoT sanity)
- L6 Halt on byzantine (divergent STH, custody halt)
Full spec: docs/chain-of-custody-v2.md. Wiki: https://loopctl.com/wiki/chain-of-custody.
Long-lived env-var keys are replaced by per-dispatch ephemeral keys minted via
POST /api/v1/dispatches. Each dispatch carries its lineage path (root → self)
and an ephemeral API key with a bounded TTL. The MCP server v2 tool
mcp__loopctl__dispatch handles minting.
Legacy env-var keys (LOOPCTL_AGENT_KEY, LOOPCTL_ORCH_KEY, etc.) continue to
work during the deprecation window but will be removed at the epic merge.
All external dependencies use behaviours + config-based DI:
# Define the behaviour
defmodule Loopctl.HealthCheck.Behaviour do
@callback check() :: {:ok, map()} | {:error, term()}
end
# Consumer resolves via Application.get_env
defp health_checker do
Application.get_env(:loopctl, :health_checker, Loopctl.HealthCheck.Default)
end
# config/test.exs maps to mock
config :loopctl, :health_checker, Loopctl.MockHealthChecker
# Oban workers use compile-time DI
@delivery_client Application.compile_env(:loopctl, :webhook_delivery, Loopctl.Webhooks.ReqDelivery)NEVER use Application.put_env in test files. NEVER pass dependencies as function opts.
Opts are for query parameters (limit, offset, filters) only.
async: trueon EVERY test file via DataCase/ConnCase- NEVER
Application.put_envin tests — all service swapping via config/test.exs Mox.set_mox_from_context(tags)in DataCase/ConnCase setup for async isolationsetup :verify_on_exit!on EVERY test file using Mox- Default permissive stubs in DataCase/ConnCase setup via
stub_all_defaults/0 - Fixtures:
fixture(:type, attrs)for DB records,build(:type, attrs)for data — defined ONLY intest/support/fixtures.ex - Mocks: defined ONLY in
test/support/mocks.ex— neverMox.defmockin test files - Tenant isolation test in every context module test — tenant A cannot see tenant B's data
mix precommit # Full quality gate (compile, format, credo --strict, dialyzer, test)
mix test # Run all tests
mix test --failed # Re-run failed tests
mix ecto.reset # Drop, create, migrate- Never use
@dialyzermodule attributes to suppress warnings priv/plts/dialyzer_ignore.exsuses regex patterns for known upstream issues- Fix root causes instead of adding suppressions
- PRD:
docs/prd.md— full product requirements - User Stories:
docs/user_stories/epic_N_name/us_N.M.json— 60 stories across 15 epics - Skills:
skills/loopctl-*.md— 6 orchestration skills - Orchestration Guide:
docs/orchestration-guide.md— methodology: loop, trust model, checkpointing - MCP Server:
mcp-server/— 42 typed tools for Claude Code agents (no curl needed), published asloopctl-mcp-serveron npm - Build Status: memory-keeper key
build_status, channelloopctl
Claude Code agents should use the loopctl MCP tools instead of curl. Install via npm install loopctl-mcp-server, then configure in ~/.claude/mcp.json or .mcp.json:
{"mcpServers": {"loopctl": {"command": "npx", "args": ["loopctl-mcp-server"], "env": {"LOOPCTL_SERVER": "https://loopctl.com", "LOOPCTL_ORCH_KEY": "...", "LOOPCTL_AGENT_KEY": "..."}}}}Tools: mcp__loopctl__list_projects, mcp__loopctl__list_stories, mcp__loopctl__verify_story, etc. (41 total). See mcp-server/README.md for the full list.
loopctl supports external observability tooling through its API and data model:
- Two-tier trust model:
agent_status(self-reported) vsverified_status(orchestrator-set) are separate fields on every story. External tools can compare them to detect unverified completions. - Orchestrator state checkpointing:
PUT /orchestrator/state/:project_idpersists orchestrator session state (phase, last verified story, decision context). Enables crash recovery and session handoff. Versioned with optimistic locking. - Audit API:
GET /stories/:id/historyreturns the immutable event log for any story. External observers can replay the decision chain for any story without parsing raw session logs. - Change feed:
GET /changes?since=...lets observer processes poll for all state transitions across a project, enabling external dashboards and alerting. /loopctl:observepattern: Orchestrators can POST structured audit events to loopctl (session start/end, rule violations, review outcomes) and query them back via the audit API. This allows post-run analysis of orchestrator behavior without coupling to any specific AI tool's log format.