This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Before every commit, run make lint and make typecheck and fix any errors. Do not commit code that fails linting or type checking.
Lintel is an open-source AI collaboration infrastructure platform. It orchestrates multi-agent workflows triggered from Slack threads, using event sourcing and CQRS patterns. Built with Python 3.12+, FastAPI, LangGraph, and PostgreSQL.
This is a uv workspace monorepo with 45+ packages under packages/:
Core packages:
| Package | Name | Dependencies | Description |
|---|---|---|---|
packages/contracts/ |
lintel-contracts |
(none) | Pure domain contracts: types, commands, events, Protocol interfaces |
packages/agents/ |
lintel-agents |
contracts | AI agent runtime (roles: planner, coder, reviewer, pm, designer, summarizer) |
packages/workflows/ |
lintel-workflows |
contracts, agents | LangGraph workflow orchestration and graph nodes |
packages/app/ |
lintel |
all below | Thin composition root: lifespan wiring, middleware, router mounting |
Reusable library packages (each independently installable):
| Package | Name | Dependencies | Description |
|---|---|---|---|
packages/event-store/ |
lintel-event-store |
contracts | Append-only event persistence (Postgres + in-memory) |
packages/event-bus/ |
lintel-event-bus |
contracts | In-memory pub/sub event bus |
packages/projections/ |
lintel-projections |
contracts, event-bus | Read-model projection engine and concrete projections |
packages/persistence/ |
lintel-persistence |
contracts | Generic CRUD/dict stores, vault |
packages/sandbox/ |
lintel-sandbox |
contracts | Docker-based code execution sandboxes |
packages/pii/ |
lintel-pii |
contracts | PII detection/anonymization (presidio) |
packages/observability/ |
lintel-observability |
contracts | OpenTelemetry tracing/metrics/logging |
packages/models/ |
lintel-models |
contracts | LLM provider routing (litellm) |
packages/slack/ |
lintel-slack |
contracts | Slack channel adapter |
packages/repos/ |
lintel-repos |
contracts | GitHub repository provider |
packages/coordination/ |
lintel-coordination |
(asyncpg only) | Database advisory locks |
packages/infrastructure/ |
lintel-infrastructure |
(residual) | MCP tool client only — will be dissolved |
API route packages (extracted from app — each independently installable):
| Package | Name | Namespace | Description |
|---|---|---|---|
packages/api-support/ |
lintel-api-support |
lintel.api_support |
StoreProvider, dispatch_event, EntityStore/DictStore protocols |
packages/users/ |
lintel-users |
lintel.users |
User CRUD routes + in-memory store |
packages/teams/ |
lintel-teams |
lintel.teams |
Team CRUD routes + in-memory store |
packages/policies-api/ |
lintel-policies-api |
lintel.policies_api |
Policy CRUD routes + in-memory store |
packages/notifications-api/ |
lintel-notifications-api |
lintel.notifications_api |
Notification rule CRUD routes + in-memory store |
packages/environments-api/ |
lintel-environments-api |
lintel.environments_api |
Environment CRUD routes + in-memory store |
packages/variables-api/ |
lintel-variables-api |
lintel.variables_api |
Variable CRUD routes + in-memory store |
packages/credentials-api/ |
lintel-credentials-api |
lintel.credentials_api |
Credential CRUD routes + in-memory store |
packages/audit-api/ |
lintel-audit-api |
lintel.audit_api |
Audit entry CRUD routes + in-memory store |
packages/approval-requests-api/ |
lintel-approval-requests-api |
lintel.approval_requests_api |
Approval request CRUD routes + in-memory store |
packages/boards/ |
lintel-boards |
lintel.boards |
Board + tag CRUD routes + in-memory stores |
packages/triggers-api/ |
lintel-triggers-api |
lintel.triggers_api |
Trigger CRUD routes + in-memory store |
packages/artifacts-api/ |
lintel-artifacts-api |
lintel.artifacts_api |
Code artifact + test result routes + in-memory stores |
packages/projects-api/ |
lintel-projects-api |
lintel.projects_api |
Project CRUD routes + in-memory store |
packages/work-items-api/ |
lintel-work-items-api |
lintel.work_items_api |
Work item CRUD routes + in-memory store |
packages/skills-api/ |
lintel-skills-api |
lintel.skills_api |
Skill CRUD routes + in-memory store |
packages/agent-definitions-api/ |
lintel-agent-definitions-api |
lintel.agent_definitions_api |
Agent definition CRUD routes + in-memory store |
packages/mcp-servers-api/ |
lintel-mcp-servers-api |
lintel.mcp_servers_api |
MCP server CRUD routes + in-memory store |
packages/models-api/ |
lintel-models-api |
lintel.models_api |
Model CRUD routes + in-memory store |
packages/ai-providers-api/ |
lintel-ai-providers-api |
lintel.ai_providers_api |
AI provider CRUD routes + in-memory store |
packages/repositories-api/ |
lintel-repositories-api |
lintel.repositories_api |
Repository registration routes |
packages/workflow-definitions-api/ |
lintel-workflow-definitions-api |
lintel.workflow_definitions_api |
Workflow definition routes |
packages/settings-api/ |
lintel-settings-api |
lintel.settings_api |
Settings + connection routes |
packages/compliance-api/ |
lintel-compliance-api |
lintel.compliance_api |
Compliance policy/regulation/practice routes + stores |
packages/experimentation-api/ |
lintel-experimentation-api |
lintel.experimentation_api |
KPI + experiment + metric routes |
packages/automations/ |
lintel-automations |
lintel.automations |
Self-contained automation domain: types, events, routes, store, scheduler, hooks |
packages/sandboxes-api/ |
lintel-sandboxes-api |
lintel.sandboxes_api |
Sandbox lifecycle routes (proxies to lintel-sandbox) |
packages/pipelines-api/ |
lintel-pipelines-api |
lintel.pipelines_api |
Pipeline run + stage routes, SSE delivery loop |
packages/chat-api/ |
lintel-chat-api |
lintel.chat_api |
Chat conversation routes + ChatService |
packages/auth-api/ |
lintel-auth-api |
lintel.auth_api |
JWT authentication, login routes, auth middleware |
Each package has src/lintel/<pkg>/ source and colocated tests/ directory. The lintel namespace is shared across packages via implicit namespace packages (no __init__.py in src/lintel/).
Cross-package integration and e2e tests live at the root tests/ directory.
Uses Make with uv for Python dependency management. Run make help to list all targets.
make install # Install all deps (uv sync --all-extras --all-packages)
make test # Run all tests (pass ARGS= for extra pytest flags)
make test-affected # Run tests only for packages changed since BASE_REF
make test-contracts # Run contracts package tests
make test-agents # Run agents package tests
make test-workflows # Run workflows package tests
make test-app # Run app package tests
make test-event-store # Run event-store package tests
make test-event-bus # Run event-bus package tests
make test-persistence # Run persistence package tests
make test-sandbox # Run sandbox package tests
make test-pii # Run PII package tests
make test-observability # Run observability package tests
make test-models # Run models package tests
make test-slack # Run slack package tests
make test-repos # Run repos package tests
make test-coordination # Run coordination package tests
make test-projections # Run projections package tests
make test-api-support # Run api-support package tests
make test-users # Run users package tests
make test-teams # Run teams package tests
# ... pattern: make test-<package-dir> for all 28 extracted API packages
make test-pipelines-api # Run pipelines-api package tests
make test-chat-api # Run chat-api package tests
make test-unit # Run all package tests (parallelised)
make test-integration # Run integration tests only
make test-e2e # Run e2e tests only
make lint # Ruff check + format check
make typecheck # mypy strict mode
make format # Auto-fix formatting and lint
make serve # Dev server on :8000
make migrate # Run event store migrations
make all # lint + typecheck + test + integration + UI buildRun a single test: uv run pytest packages/contracts/tests/test_types.py -v
Run affected tests only: make test-affected BASE_REF=origin/main
CI rule: The CI workflow (.github/workflows/ci.yml) must always use make targets — never run raw uv run, pytest, ruff, or mypy commands directly. This keeps CI and local dev in sync via a single source of truth in the Makefile.
Event-sourced CQRS with clean architecture boundaries enforced by workspace package dependencies:
packages/contracts/— Slim kernel only (EventEnvelope, ThreadRef, ActorType, core protocols). NEVER add new types, events, or commands here — put them in the relevant domain package instead.packages/domain/— Domain types, events, and sub-packages:metrics/(agent/DORA/human/team metrics + engine),hooks/(workflow hook engine + pattern matching),notifications/(multi-channel dispatcher),reviews/(review engine),guardrails/(condition language, approval bridge, cost rules, escalation),auth/(JWT, passwords),git_events.py(git webhook listeners)packages/agents/— AI agent definitions and runtime, agent types/protocols/eventspackages/workflows/— LangGraph workflow orchestration, workflow/pipeline types/events/commandspackages/event-store/— Append-only event persistence (Postgres + in-memory)packages/event-bus/— In-memory pub/sub event bus with subscriptionspackages/projections/— Read-model projection engine and concrete projectionspackages/persistence/— Generic CRUD/dict stores, vaultpackages/sandbox/— Docker-based isolated code executionpackages/pii/— PII detection/anonymization (presidio)packages/observability/— OpenTelemetry tracing/metrics/loggingpackages/models/— LLM provider routing (litellm)packages/slack/— Slack channel adapter (slack-bolt)packages/repos/— GitHub repository providerpackages/coordination/— Database advisory lockspackages/api-support/—StoreProvider,dispatch_event,EntityStore/DictStoreprotocols shared by all API packagespackages/app/— Thin composition root only: lifespan wiring, middleware, mounting extracted routers. Domain logic lives in extracted packages.packages/<name>-api/orpackages/<name>/— 28 extracted API route packages (users, teams, boards, chat, pipelines, etc.)
Key patterns:
ThreadRef(workspace_id, channel_id, thread_ts) is the canonical workflow instance identifier- Commands are frozen dataclasses expressing intent; events record facts
EventEnvelopewraps all events with metadata (correlation_id, causation, timestamps)- Library packages implement Protocol interfaces from contracts — never import app from library packages
- pytest uses
--import-mode=importlib— do NOT add__init__.pyto test directories
Import rules — where things live:
lintel.contracts.types— ONLY ThreadRef, ActorType, CorrelationId, EventIdlintel.contracts.events— ONLY EventEnvelope, register_events, deserialize_event, EVENT_TYPE_MAPlintel.contracts.protocols— ONLY EventHandler, EventBus, CommandDispatcher, EventStorelintel.domain.types— All domain types (Project, WorkItem, Board, User, Team, Policy, etc.)lintel.domain.events— All domain events (ProjectCreated, WorkItemUpdated, etc.)lintel.domain.metrics— Metrics engine, agent/DORA/human/team metric collectorslintel.domain.hooks— HookEngine, pattern matching for workflow hookslintel.domain.notifications— NotificationDispatcher, notification typeslintel.domain.reviews— ReviewEngine, review modelslintel.domain.guardrails— GuardrailEvaluator, condition language, approval bridgelintel.domain.auth— JWT utilities, password hashinglintel.domain.git_events— GitEventListener, git webhook event typeslintel.automations.types— Automation types (AutomationDefinition, AutomationTriggerType, ConcurrencyPolicy)lintel.automations.events— Automation events (AutomationCreated, AutomationFired, etc.)lintel.workflows.types— Workflow types (PipelineRun, Stage, StageStatus, etc.)lintel.workflows.events— Workflow events (PipelineRunStarted, WorkflowStarted, etc.)lintel.workflows.commands— Workflow commands (StartWorkflow)lintel.<pkg>.types/events/protocols— Package-specific contracts (sandbox, models, pii, repos, agents, etc.)- NEVER add new types or events to
lintel-contracts— it is a frozen kernel. New domain items go in their own domain package (e.g.lintel-automations), orlintel-domainfor cross-cutting types.
There are two DI patterns in use, depending on the package:
Each extracted package declares module-level StoreProvider instances. The app wires the real store at startup by calling .override():
# In the package routes module (e.g. lintel/users/routes.py):
from lintel.api_support.provider import StoreProvider
user_store_provider: StoreProvider[UserStore] = StoreProvider()
@router.get("/users")
async def list_users() -> list[User]:
store = user_store_provider.get()
...
# In packages/app lifespan wiring:
from lintel.users.routes import user_store_provider
user_store_provider.override(stores["users"])- No
dependency-injectoror@injectneeded in extracted packages StoreProvider.get()raisesRuntimeErrorif not wired — tests must call.override()in fixtures
packages/app/src/lintel/api/container.py — AppContainer still manages non-extracted services (workflow executor, event bus, projections, etc.) as providers.Object holders. Route handlers for these use @inject + Depends(Provide[...]):
from dependency_injector.wiring import Provide, inject
from lintel.api.container import AppContainer
@router.get("/items")
@inject
async def list_items(
store: ItemStore = Depends(Provide[AppContainer.item_store]), # noqa: B008
) -> list[dict[str, Any]]:
...@injectmust be the decorator directly above the function (after@router.X())- Use
= Depends(Provide[AppContainer.X])default-value syntax with# noqa: B008
Testing with DI: Override providers in tests:
container.some_store.override(providers.Object(fake_store))Domain logic is encapsulated in typed classes, not standalone functions:
| Class | Location | Purpose |
|---|---|---|
StageTracker |
workflows/nodes/_stage_tracking.py |
Pipeline stage lifecycle (mark_running, append_log, mark_completed) |
ChatService |
app/routes/chat.py |
Chat routing, project resolution, workflow dispatch |
SandboxToolDispatcher |
agents/sandbox_tools.py |
Routes tool calls to SandboxManager methods |
BranchNaming |
workflows/nodes/_branch_naming.py |
Branch name generation conventions |
GitOperations |
workflows/nodes/_git_helpers.py |
Sandbox git operations (rebase) |
WorkflowErrorHandler |
workflows/nodes/_error_handling.py |
Standardised node error handling |
AuditEmitter |
workflows/nodes/_event_helpers.py |
Audit entry recording |
NotificationService |
workflows/nodes/_notifications.py |
Phase change notifications |
ReportVersionService |
app/routes/pipelines.py |
Pipeline report versioning |
MetricsEngine |
domain/metrics/engine.py |
Aggregates agent, DORA, human, and team metrics |
HookEngine |
domain/hooks/engine.py |
Workflow hook registration and pattern-matched dispatch |
NotificationDispatcher |
domain/notifications/dispatcher.py |
Multi-channel notification delivery |
ReviewEngine |
domain/reviews/engine.py |
Automated codebase review orchestration |
GuardrailEvaluator |
domain/guardrails/evaluator.py |
Guardrail condition language evaluation |
ApprovalBridge |
domain/guardrails/approval_bridge.py |
Event-driven approval flow for guardrails |
GitEventListener |
domain/git_events.py |
Git webhook event handling and workflow triggers |
WorkflowNode |
workflows/base.py |
Base class for all workflow graph nodes |
ApprovalGateNode |
workflows/nodes/approval_gate.py |
Human approval gate within workflow graphs |
Workflow node pattern — nodes instantiate StageTracker at the top:
async def research_codebase(state, config):
from lintel.workflows.nodes._stage_tracking import StageTracker
tracker = StageTracker(config, state)
await tracker.mark_running("research")
await tracker.append_log("research", "Researching...")
# ... do work ...
await tracker.mark_completed("research", outputs={...})The dev environment runs via make dev (or scripts/dev-tmux.sh), which creates a tmux session lintel with 3 windows:
- prompts — 3 horizontal panes, each running a
claudeinstance - services — 3 panes:
make serve-db(API + DB),make ui-dev(Vite),make ollama-serve - editor — nvim + 2 terminal panes (horizontal split below nvim, split vertically)
Debugging with dev services: When debugging runtime issues, check the services window panes for logs/errors. The API server, UI dev server, and Ollama are already running — use tmux capture-pane to read their output rather than starting new processes.
Running commands in tmux: When the user asks to run a command in a tmux pane, use tmux send-keys -t lintel:<window>.<pane> '<command>' Enter to send it, then tmux capture-pane -t lintel:<window>.<pane> -p | tail -N to read output. This gives the user visibility in their terminal. Prefer this over running commands directly when the user requests it.
Running tests: Run tests directly using the Bash tool when asked. Use make test-unit, make test-affected, make all, or targeted uv run pytest commands as needed.
When doing development work, use the running dev services for integration testing rather than relying solely on unit tests:
- Make HTTP requests to the API at
http://localhost:8000/api/v1/usingrtk proxy curl(bypasses token filtering for raw JSON) - Monitor server logs via
tmux capture-pane -t lintel:1.0 -p | tail -N - Check pipeline status at
/api/v1/pipelines/{run_id}after triggering workflows - Start a chat by POSTing to
/api/v1/chat/conversationswith amessage,project_id, andmodel_id - Follow conversation at
/api/v1/chat/conversations/{conversation_id} - MCP server is available at
/mcpfor tool/resource interaction
This gives visibility into the full request lifecycle: HTTP status, chat routing, workflow dispatch, stage progression, and server-side errors.
- Ruff with strict rule set (E, F, W, I, N, UP, ANN, B, A, SIM, TCH, RUF), line length 100
- mypy strict mode with pydantic plugin
- pytest-asyncio with
asyncio_mode = "auto"— no need for@pytest.mark.asyncio - pytest-testmon available for local dev change detection (
uv run pytest --testmon) - Integration tests use testcontainers (postgres, nats)
- Tech stack manifests live in
docs/tech-stack/*.yaml— see.claude/docs/tech-stack-schema.mdfor the schema - Context7 MCP server is configured in
.mcp.jsonfor fetching framework documentation - Use
mcp__context7__resolve-library-idto find a library's Context7 ID - Use
mcp__context7__get-library-docsto fetch docs by topic - Types reference:
docs/types-reference.md— all dataclasses, enums, protocols, request/response models with file paths and line numbers. Run/update-typesto refresh after adding/moving types. - Entities:
docs/requirements/entities.md— all domain entities, enums, and planned modifications with line numbers. When the user says "entities" or "update entities", this is the file. - Agents:
docs/requirements/agents.md— agent roles, categories, runtime architecture, sandbox tools, and workflow integration. - Related docs:
docs/architecture.md,docs/events.md,docs/local-dev.md
- MCP backend required: Every UI feature must have a corresponding MCP server tool/resource implemented in the backend (
packages/infrastructure/src/lintel/infrastructure/mcp/). Do not ship frontend-only features — the MCP layer is the integration surface and must be kept in sync. - Tests required: Every feature must include tests colocated with the package:
- Unit tests for new domain logic in
packages/<pkg>/tests/ - Integration tests when the feature touches infrastructure in
tests/integration/
- Unit tests for new domain logic in
- Test the package you changed, not everything: During development, run per-package tests for fast feedback:
make test-contracts,make test-agents,make test-workflows,make test-app,make test-event-store,make test-event-bus,make test-persistence,make test-pii,make test-models, etc.- Or use
make test-affected BASE_REF=mainto auto-detect affected packages - Only run
make test-unitormake allas a final check before committing
- Do NOT add
__init__.pyto test directories — pytest uses--import-mode=importliband__init__.pyin tests causes namespace collisions across packages.