# Architecture

> Folder structure: `docs/repo-map.md`.

## Apps / services (facts)

Three Docker services behind nginx gateway:

| Service | Port | Role |
|---------|------|------|
| `backend` | 3140 | Rust/axum API server |
| `frontend` | 80 | Vite dev server (React) |
| `gateway` | 3456 | nginx reverse proxy (routes `/api/*` to backend, rest to frontend) |

## Key patterns (facts)

### API pattern
- All endpoints return `ApiResponse<T>` wrapper: `{ success: bool, data: T|null, error: string|null }`.
- Routes registered in `backend/src/lib.rs` (`build_router()`), handlers in `backend/src/api/<domain>.rs`.
- Axum 0.7 method chaining: same path, multiple methods → `.route("/path", get(h1).post(h2))`.

### SSE streaming
- Agent responses stream via Server-Sent Events (not WebSocket).
- Events: `chunk` (text delta — raw concatenation, no separator), `done`, `error`.
- Orchestration adds: `system`, `round`, `agent_start`, `agent_done`.
- Workflow runs use SSE for real-time progress: `RunEvent` enum with `StepStart`, `StepDone`, `RunDone`, `RunError`. Frontend shows a live progress panel with step indicators (pulse animation for current, check/X for completed).
- Frontend uses `ReadableStream` reader + manual SSE parsing in `api.ts:_streamSSE()`.
- `AbortController` + `signal` for cancellation. `finished` boolean guard prevents double `onDone`.

### Agent execution
- `agents/runner.rs` spawns CLI processes (`claude`, `codex`, `vibe`, `gemini`, `kiro-cli`, `copilot`) with `--print` / non-interactive flags.
- **Two output modes**: `Text` (line-by-line stdout, default for Codex/Vibe/Gemini/Copilot) and `StreamJson` (Claude Code with `--output-format stream-json --verbose --include-partial-messages`). In StreamJson mode, each line is a JSON event parsed by `parse_claude_stream_line()` — text deltas from `stream_event` events, token usage from `result` event.
- Agents run in the project's directory context (or temp dir for global discussions).
- **Runtime probe**: if no local binary is found, `probe_runtime()` tests npx availability (15s timeout, 5min cache). `AgentDetection.runtime_available` distinguishes "installed locally" from "runnable via npx". Frontend uses `isUsable(agent) = (installed || runtime_available) && enabled`.
- MCPs work with all 6 agents: Claude Code (`.mcp.json`), Kiro (`.kiro/settings/mcp.json`), Gemini CLI (`.gemini/settings.json`), Vibe (`.vibe/config.toml`), Codex (`~/.codex/config.toml`), Copilot (prompt-injected). Disk sync writes all formats simultaneously. Claude/Kiro/Gemini use identical JSON format (`mcpServers`). Kiro auth via AWS Builder ID.
- **Prompt injection order**: profiles → skills → directives → MCP context. All injected via `extra_context` parameter to `agent_command()`.
- `AgentConfig` has `full_access: bool` field (persisted in config.toml). When enabled, runner adds `--dangerously-skip-permissions` (Claude), `--full-auto` (Codex), `--trust-all-tools` (Kiro), `--allow-all-tools` (Copilot).
- API: `GET/POST /api/config/agent-access` to read/set the full_access flag. UI toggle in Config > Agents card.
- **Agent lifecycle**: agents can be uninstalled (`POST /api/agents/uninstall`) or toggled on/off (`POST /api/agents/toggle`). Disabled agents tracked in `AppConfig.disabled_agents: Vec<AgentType>`. `AgentDetection` includes `enabled: bool`. Uninstall uses platform-specific commands (npm for Claude/Codex/Copilot, uv/pipx/pip3 for Vibe).
- **Cross-platform HOME resolution**: in Docker, `KRONN_HOST_HOME` overrides `HOME` for all spawned agents so they find their auth config (`~/.claude/`, `~/.codex/`, `~/.copilot/`, etc.). On native (Tauri desktop), HOME is already correct. `COPILOT_HOME` is also set explicitly for Copilot CLI.
- **Windows binary detection**: `find_binary()` matches `.cmd`, `.exe`, `.ps1` extensions in addition to exact names (npm installs create `.cmd` wrappers on Windows).
- **WSL detection**: uses `WSL_DISTRO_NAME` env var first (most reliable), then `/proc/version` fallback.
- **File permissions (fixed)**: agents run as `root` inside Docker. `fix_ownership()` called after all agent executions (discussions, workflows, AI audit) to chown files back to `KRONN_HOST_UID:KRONN_HOST_GID` on host-mounted volumes.
- **Path resolution**: `resolve_host_path` uses Docker mount priority (prefers /host-home over /home/priol).
- **macOS Docker agent bootstrap (0.3.5)**: on macOS hosts, host-mounted binaries are Darwin (macOS) executables that cannot run in the Linux container. `entrypoint.sh` detects `KRONN_HOST_OS=macOS` and installs Linux versions of Claude Code (npm), Codex (npm), and Kiro (curl) inside the container. `find_binary()` skips host-mounted Darwin binaries for `claude`, `codex`, `copilot`, `kiro-cli` when `host_is_macos()`. `~/.npm/bin` is mounted at `/host-bin/npm` via `KRONN_NPM_BIN` env var (auto-detected by Makefile `npm bin -g`).

- **Ollama local LLM (0.4.0)**: unlike other agents (CLI spawn), Ollama uses HTTP API streaming (`POST OLLAMA_HOST/api/chat`). System context (MCP, skills, profiles, directives) is sent as `role: system`, user prompt as `role: user` — the model doesn't confuse context with question. Token tracking from `prompt_eval_count` + `eval_count` in the `done: true` response chunk. Cost: always $0. Docker: `OLLAMA_HOST` env var resolves to `host.docker.internal:11434` (macOS/Windows) or requires `OLLAMA_HOST=0.0.0.0 ollama serve` on WSL/Linux. Health endpoint returns contextual hints per environment. Setup wizard in Settings with 4 states (install → launch → pull models → model picker). `api/ollama.rs` for health+models, execution in `runner.rs:start_ollama_http()`.

### Discussions
- `Discussion.project_id` is `Option<String>` (Rust) / `string | null` (TS).
- Discussions without a project are "global" — shown under "Général" group in the sidebar.
- Agent runs in a temp directory for global discussions (no project context).
- `CreateDiscussionRequest.project_id` is also optional; frontend offers "Aucun projet" option.
- **Discussion search/filter**: sidebar includes a search input (`discSearchFilter` state) that filters discussions by title (case-insensitive substring match). When a search term is active, collapsed groups are auto-expanded and only matching discussions are shown. A clear button resets the filter.
- **Archive/unarchive**: `Discussion.archived: bool` (default false). Swipe right on sidebar item to archive, swipe left to delete. Archived discussions shown in a collapsible "Archives" section at the bottom of the sidebar. `PATCH /api/discussions/:id` with `UpdateDiscussionRequest { title?, archived? }`.
- **Title editing**: double-click or pencil icon in chat header for inline rename.
- **Disabled agent detection**: if a discussion's agent is uninstalled or disabled, the text input is grayed out with a warning banner linking to agent config.
- **Agent switch**: the primary agent (`Discussion.agent`) can be changed mid-conversation via `PATCH /api/discussions/:id` with `{ agent: AgentType }`. On switch: agent is updated, `summary_cache` is invalidated (different agent = different budget), a User message is inserted prompting the new agent to summarize and continue, and `runAgent` is auto-triggered. UI: clickable agent name in chat header with dropdown of installed agents.
- **Disc-first refactor (0.8.6 phase 2)**: a discussion is now a TOPIC, decoupled from agent sessions. Schema: `discussion_sessions(disc_id, agent_type, session_id, role 'owner'|'peer', status 'active'|'paused'|'left', joined_at, left_at)` lies N CLI sessions to 1 disc. The form for creating a discussion exposes a checkbox "Launch an agent right away" (default ON, legacy 80% case unchanged). Unchecked → disc created empty, the user invites agents later from the header's `[+ Inviter]` button. Migration 060 backfills existing discs with `source_agent` set into one `'owner'` row each. The legacy `Discussion.agent` column stays for back-compat but `discussion_sessions` is now the source of truth for "who is in this room".
- **Cross-agent collab (0.8.6 phase 2)**: N CLI agents (Claude Code, Codex, Gemini, Kiro, Vibe, Copilot…) can share one Kronn discussion in real time. Flow:
  1. User clicks `[+ Inviter]` once → backend mints a `kr-join-…` token (TTL 10 min, **multi-use** : same token works for N peers).
  2. Modal displays a copy-paste instruction with `disc_join({token: "kr-join-…"})`.
  3. User launches each CLI host-launched (in its own terminal) and pastes the instruction.
  4. Each CLI's MCP bridge (`disc-introspection-mcp.py`) calls `POST /api/discussions/peer-join` → atomic transaction validates token, creates `discussion_sessions` row, returns `{disc_id, peer_count, disc_title, recent_messages, next_steps}`.
  5. The bridge stashes the disc_id in a module-level `_CURRENT_DISC_ID` (runtime binding, no env-var required for host-launched).
  6. Agent's `agent_type` is **auto-derived** from the MCP `clientInfo.name` handshake (Claude Code → ClaudeCode, codex-cli → Codex, vibe → Vibe), with `/proc/<ppid>/cmdline` fallback for CLIs that send a useless clientInfo. No need to set `KRONN_AGENT_TYPE` env.
  7. Agents talk via `disc_append({content: "..."})` (simple mode auto-fills disc_id, source_msg_id, agent_type, role=Agent). Anything they reply only in their local terminal is invisible to peers — the `next_steps` directive makes that explicit and imperative.
  8. Agents block on `disc_wait_for_peer({timeout_secs: 60})` for new peer messages — long-poll endpoint that filters out the caller's own `agent_type`. Default 60 s, clamped 1–90 s server-side.
  9. Agents leave via `disc_leave()` — marks `status='left'`, idempotent. The bridge clears its local `_CURRENT_DISC_ID`. `(agent_type, session_id)` pair is stable per bridge lifetime (`_BRIDGE_SESSION_ID` module-level, 2026-05-21 fix — was regenerated per call, broke disc_leave).
  10. UI header shows live participant chips per agent_type (🤖 Claude, 💠 Codex, ✨ Gemini, 🐙 Kiro, 💻 Copilot, 🐱 Vibe, 🦙 Ollama, ⚙️ Custom, 👤 Unknown), polled every 5 s. Messages auto-refresh every 5 s on the active disc.
  - Validated live 2026-05-21 on a tennis-roleplay smoke test (3 agents, 14 min, 59 messages, 1 point played over a 20-stroke rally). Pattern marketing-friendly : *"le cerveau partagé de tes CLI IA"*. Foundation for future delegation pattern (`agent_delegate({to, task, context})`, cf. `project_multi_agent_orchestrator`).
  - Routes: `POST /api/discussions/:id/invite-peer`, `GET /api/discussions/:id/participants`, `POST /api/discussions/peer-join`, `POST /api/discussions/peer-leave`, `GET /api/discussions/:id/wait`.
  - New MCP tools: `disc_join`, `disc_wait_for_peer`, `disc_leave`. `disc_append` extended with simple-mode `{content: "..."}` ; bulk-mode `{disc_id, messages: […]}` still works for cross-agent-memory transcript replay.
  - Doc fiche: `docs/operations/mcp-servers/kronn-internal.md`. `docs/AGENTS.md` rule relaxed: read the MCP context file IF it exists, otherwise proceed (was blocking on Codex's strict reading of the rule).
- **Isolated workspace (worktree)**: discussions can run in `workspace_mode: "Isolated"`, creating a git worktree in `<repo>/.kronn-worktrees/` with a `kronn/<slug>` branch. The worktree isolates agent changes from the main working tree.
  - **Lock/Unlock**: the worktree "locks" the branch (git forbids checkout elsewhere). User can **unlock** (`POST /discussions/:id/worktree-unlock`) to free the branch for testing in the main repo. **Lock** (`POST /discussions/:id/worktree-lock`) re-creates the worktree.
  - **Auto re-lock**: when sending a message to an unlocked Isolated discussion, the backend auto-attempts `reattach_worktree`. If the branch is still checked out in the main repo, an SSE error is returned and a persistent red banner is shown above the input (with a Retry button).
  - **Relative gitdir**: worktree cross-references use relative paths (`../../.git/worktrees/<name>`) so they work both inside Docker and on the host.
  - UI: badge next to branch name (blue = locked, yellow = unlocked) with lock/unlock toggle button.
- **User identity**: `pseudo` and `avatar_email` in `ServerConfig`. Messages store `author_pseudo`/`author_avatar_email` (migration 021). Gravatar avatar (SHA-256) displayed in user message bubbles + Settings UI.
- **Copy button**: on every agent message (header + footer) and on markdown tables/code blocks via `CopyableBlock` wrapper. Tables copied as TSV.
- **Response time**: per agent message, computed frontend-side as `timestamp(agent) - timestamp(previous user)`.
- **Performance**: `MessageBubble` extracted as `memo` component, streaming uses `<pre>` instead of markdown, auto-scroll throttled, `chatInput` non-controlled.
- **Component split (2026-03-28)**: DiscussionsPage split into 7 files: orchestrator (1218L) + ChatHeader, ChatInput, DiscussionSidebar, NewDiscussionForm, MessageBubble, SwipeableDiscItem.
- **CSS system (2026-03-28)**: token-based CSS (`src/styles/tokens.css`, `utilities.css`, `components.css`) + per-page CSS. No CSS framework.
- **Backend split (2026-03-28)**: `projects.rs` split into projects (1396L) + audit (1848L) + ai_docs (184L) + discover (426L).
- **Vibe agent**: uses `vibe-runner.py` (calls `run_programmatic()` SDK directly, bypasses CLI stdin hang). Falls back to Mistral API if vibe not installed.
- **Multi-line input**: `<textarea>` with auto-resize (Shift+Enter for newlines, Enter to send).
- **Full access badge**: "Full access" indicator on agent messages when `full_access: true`.
- **Multi-user (Phase 1 — contacts + Phase 2 — WebSocket sync)**: `contacts` table (migration 022) with CRUD API (`/api/contacts`). Invite code format `kronn:pseudo@host:port`. Sidebar section shows contacts with online/offline status via real-time WebSocket. Settings > Identity displays the invite code.
  - **WebSocket server**: `GET /api/ws` — accepts connections from local frontend and remote Kronn peers. Auth via `?token=` query param. Messages dispatched through `AppState.ws_broadcast` (tokio broadcast channel).
  - **WebSocket client manager**: background task (`core/ws_client.rs`) maintains outbound WS connections to all contacts. Reconnects with exponential backoff (1s → 60s). Emits presence online/offline events.
  - **Frontend hook**: `useWebSocket.ts` — single WS connection to own backend, auto-reconnect, 30s heartbeat ping. Replaces HTTP polling for contact presence.
  - **Protocol**: JSON tagged union `WsMessage` — variants: `presence`, `ping`, `pong`. Phase 3 (discussion sharing + pause IA) planned.
- **Cross-platform**: `core/env.rs` provides `is_docker()` and `host_os_label()`. Docker-specific logic (chown, safe.directory, /host-home) is conditional. Agent uninstall commands use `#[cfg(unix)]`/`#[cfg(windows)]`. Tauri desktop app embeds the backend with `get_backend_url` command.

### Security & auth

**Bearer token authentication** (opt-in):
- Middleware in `lib.rs` checks `Authorization: Bearer <token>` on all routes except `/api/health`.
- Token is **opt-in**: `ServerConfig.auth_token: Option<String>` + `auth_enabled: bool`. If no token configured or `auth_enabled=false`, all requests pass through.
- User activates auth from Settings UI → `POST /api/config/auth-token/regenerate` generates a UUID token, sets `auth_enabled=true`, returns the token **once**. Frontend stores it in `localStorage`.
- Migration safety: if `auth_token` exists but `auth_enabled=false` (legacy auto-generated artifact), the token is cleared on startup.

**CORS**: restricted to configured `ServerConfig.domain` or `localhost:3140`/`localhost:3141`. Built via `build_cors()` in `lib.rs`.

**Docker host binding**: `main.rs` detects `KRONN_DATA_DIR` env var → binds to `0.0.0.0` (needed for nginx container). Otherwise uses `config.server.host` (default `127.0.0.1`).

**Agent concurrency**: `Semaphore` in `AppState.agent_semaphore` limits concurrent agent processes. Configurable via `ServerConfig.max_concurrent_agents` (1–20, default 5). UI slider in Settings.

**Agent timeouts**: `AGENT_GLOBAL_TIMEOUT` (30 min) + `AGENT_STALL_TIMEOUT` (5 min no output). Agent process killed on timeout or client disconnect. Partial responses saved with `⚠️ [Réponse partielle — agent interrompu]` marker.

**Input validation**: title ≤ 500 chars, content ≤ 100KB, workflow ≤ 20 steps, workflow name ≤ 200 chars.

**Graceful shutdown**: `axum::serve().with_graceful_shutdown()` handles SIGTERM (Unix) + Ctrl+C. In-flight requests finish before exit.

**Server config API**:
- `GET /api/config/server` → `ServerConfigPublic { host, port, domain, max_concurrent_agents, auth_enabled }`
- `POST /api/config/server` → update domain, max_concurrent_agents
- `POST /api/config/auth-token/regenerate` → generate new token (sets `auth_enabled=true`)

### State management
- Backend: `AppState` holds `db: Arc<Database>` (SQLite), `config: Arc<RwLock<AppConfig>>`, `agent_semaphore: Arc<Semaphore>`, and `workflow_engine: Arc<WorkflowEngine>`.
- `Database` struct wraps `Mutex<Connection>` with `with_conn()` async accessor.
- Data persisted in `kronn.db` with WAL mode and foreign keys enabled.
- Migrations run via `backend/src/db/migrations.rs` (versioned SQL files, executed before Mutex wrap to avoid blocking_lock panic).
- Frontend: `useApi` hook for data fetching. Dashboard.tsx (~750 lines) is the main shell with accordion sections per project, bootstrap modal, and smart section defaults based on audit status; sub-pages (SettingsPage.tsx, DiscussionsPage.tsx, McpPage.tsx, WorkflowsPage.tsx) receive data as props. UI state managed locally with `useState`.
- `useMemo` for computed values (agent mentions filtering, unread counts). Conditional polling (only active tab).
- `ErrorBoundary` class component wraps lazy-loaded routes. `React.lazy` + `Suspense` for code splitting (SetupWizard, Dashboard).
- `AbortController` cleanup on component unmount for SSE streams.
- Shared constants extracted to `lib/constants.ts` (AGENT_COLORS, AGENT_LABELS, ALL_AGENT_TYPES).
- Unread badges persisted in localStorage.
- No global state library (no Redux, Zustand, etc.).

### Type bridge
- Rust models annotated with `#[derive(TS)]` from `ts-rs` crate.
- `make typegen` generates `frontend/src/types/generated.ts`.
- Rust is source of truth; TypeScript follows.

### i18n (internationalization)
- Lightweight custom translation system (no external lib).
- 3 UI locales: `fr`, `en`, `es` — defined in `frontend/src/lib/i18n.ts`.
- UI language stored in `localStorage` (`kronn:ui-locale`), separate from agent output language (backend config).
- `I18nContext.tsx` provides `useT()` hook returning `t(key, ...args)` for components.
- Translation keys use dot notation: `nav.projects`, `projects.search`, `config.tokens.title`, etc.
- String interpolation with `{0}`, `{1}` positional placeholders.
- Default locale: `fr`.

### Multi-agent orchestration
- Discussion can involve multiple agents debating in configurable rounds (1–3, default 2 in UI).
- Primary agent (discussion owner) speaks last each round and produces final synthesis.
- Anti-repetition prompts keep rounds concise.
- Language configurable globally (fr/en/zh/br), injected into all agent prompts.

### 3-axis agent configuration (Profiles / Skills / Directives)

Agents are configured along three independent axes, each multi-selectable:

| Axis | Purpose | Selection | Storage |
|------|---------|-----------|---------|
| **Profiles** (WHO) | Agent persona — identity, expertise, personality | Multi-select | Builtin `.md` in `backend/src/profiles/`, custom in `~/.config/kronn/profiles/` |
| **Skills** (WHAT) | Domain expertise — technical knowledge injected into prompts | Multi-select | Builtin `.md` in `backend/src/skills/`, custom in `~/.config/kronn/skills/` |
| **Directives** (HOW) | Output behavior — formatting, language, verbosity constraints | Multi-select | Builtin `.md` in `backend/src/directives/`, custom in `~/.config/kronn/directives/` |

**Profiles** define WHO the agent is: persona name (editable, even on builtins via override file), avatar, color, role, and a detailed persona prompt. 8 builtin profiles (Kai/Architect, Mia/Tech Lead, Sam/QA Engineer, Noa/Product Owner, Kim/Scrum Master, Eve/Technical Writer, Max/Devil's Advocate, Zia/Mentor). Persona names are short (3-4 chars) for token efficiency.

When multiple profiles are selected, the prompt builder generates a **multi-agent collaboration** instruction: each profile's perspective must be considered, trade-offs identified, and assumptions challenged.

**Persona name overrides**: stored in `~/.config/kronn/persona_overrides.json`. Applies to both builtin and custom profiles. API: `PUT /api/profiles/:id/persona-name`.

**Skills guardrails**: each skill includes "Apply when" and "Do NOT apply when" sections in its frontmatter — scoping when the skill's knowledge is relevant vs. when it should be ignored. Prevents cross-domain noise (e.g. Rust skill on a frontend-only change).

**Directives output format specifications**: each directive includes an "Output format" field specifying the expected format (plain text, numbered steps, JSON, diff, markdown report, etc.). This ensures agents produce consistent, predictable output structures.

**Best practices links**: Settings > Agents section includes external links to official prompt engineering best practices (Anthropic, OpenAI) for reference.

**Token-saver directive**: encourages concise output (2-3 sentences per point, no unnecessary headers), reducing token consumption while preserving information density.

All three axes are available in:
- Discussions (profile/skill/directive selectors in new discussion form)
- Workflow steps (per-step selectors)
- AI audit pipeline (default profiles: Architect + Tech Lead + Mentor)

**Native file sync (2026-03-27)**: Skills and profiles are also written as native agent files for progressive discovery:
- Skills → `SKILL.md` in `.claude/skills/`, `.agents/skills/` (Codex), `.gemini/skills/` — agents load name+description at startup, full content on demand (~95% token savings vs prompt injection)
- Profiles → agent files in `.claude/agents/`, `.gemini/agents/`, `.codex/agents/` — agents discover them as sub-agents
- Vibe/Kiro: fallback to prompt injection (Vibe uses custom runner, Kiro headless unconfirmed)
- Sync: additive per discussion (no cleanup), full cleanup at startup + project config change
- Module: `core/native_files.rs`

### Workflow engine

Unified automation system: `Trigger → Steps`. Superset of OpenAI Symphony's WORKFLOW.md format. Post-step operations (create PR, comment on issue, etc.) are handled directly by agents using MCP tools within steps, not as a separate "actions" phase.

**Triggers:**
- **Cron** — scheduled execution (simple recurring task). 1 tick = 1 run.
- **Tracker** — polls an issue tracker API (GitHub, Linear...) at a cron interval. Each new matching issue = 1 separate run with issue context injected via Liquid templates. Pull-based (polling), not push (webhooks). Tracks processed issue IDs for reconciliation (no duplicate runs).
- **Manual** — triggered from dashboard UI or CLI.

**Steps:**
- Sequential execution, each step runs an agent with optional per-step MCPs (resolved and synced before execution).
- Steps can use `mode: debate` for multi-agent discussion at any point.
- Context flows between steps via Liquid-compatible template variables: `{{issue.title}}`, `{{issue.body}}`, `{{issue.number}}`, `{{issue.url}}`, `{{issue.labels}}`, `{{previous_step.output}}`, `{{steps.<name>.output}}`.
- **Conditional branching**: `on_result` rules per step — e.g. `{ contains: "NO_RESULTS", action: stop }`. Actions: `Stop`, `Skip`, `Goto(step_name)`.
- **Per-step agent config**: optional `AgentSettings { model, reasoning_effort, max_tokens }` override.
- **Stall detection**: configurable timeout — kill step if no agent output for N seconds.
- **Retry**: exponential backoff for failed steps (`max_retries`, `backoff: exponential`).

**Workspace:**
- Isolated git worktree per run (`git worktree add`), branch: `kronn/<workflow>/<run-id>`.
- Lifecycle hooks (shell commands): `after_create`, `before_run`, `after_run`, `before_remove`.
- Cleanup on completion/failure.

**MCP injection:**
- `read_all_mcp_contexts()` reads `.mcp.json` and per-project MCP context files (`docs/operations/mcp-servers/*.md`).
- Available MCP servers are listed in agent prompts with instruction to use `mcp__<server>__<tool>` tools instead of Bash workarounds.
- Applied to both workflow steps and discussions.

**Safety:**
- Sandbox mode (Docker), max files/lines changed, approval gates.
- Concurrency limit per workflow (max simultaneous runs).

**Token accounting:**
- Per-step and per-run token totals tracked in `StepResult` and `WorkflowRun`.

**Key design decisions:**
- Workflows are created step-by-step via the dashboard UI (wizard), not just WORKFLOW.md files.
- WORKFLOW.md files can be imported/detected from repos (Symphony format → single-step Kronn workflow).
- Import auto-detects missing MCPs and suggests installation from registry.
- Storage format in DB is JSON (not YAML).
- Reuses existing agent runner and multi-agent debate system.
- Symphony is a strict subset: single-agent, single-prompt, tracker-driven. Kronn adds multi-step, multi-agent, conditional branching, per-step MCPs.

**0.6.0 engine extensions** (full detail in `docs/AGENTS.md § Workflow Engine 0.7.x features`):
- `WorkflowGuards` (timeout / max-llm-calls / loop-revisits) → `RunStatus::StoppedByGuard`.
- `StepOutputFormat::TypedSchema` (JSON-schema-validated step output).
- `Workflow.artifacts` + `---ARTIFACT:name---` envelope persisted to workspace files.
- `StepType::Gate` (`WaitingApproval` + `POST /api/workflows/.../decide` + optional webhook).
- `StepType::Exec` (allowlisted binaries, argv literal, never `sh -c`).
- `ConditionAction::Goto { max_iterations }` loops + `WorkflowRun.state` (`---STATE:k=v---`, `{{state.X}}`, `{{iter.X}}`).
- `Workflow.on_failure` rollback steps (only on `Failed`, not on `Cancelled`/`StoppedByGuard`/Gate-reject).
- Per-item Export/Import for Workflows + Quick Prompts (workflow export bundles referenced QPs).

**0.8.2 engine extensions:**

- **`WsMessage::WorkflowRunUpdated`** (`models::multiuser.rs`) — broadcast à chaque flip de status + chaque transition d'étape (`StepStart`, `StepDone`, `RunDone`, terminal `Reject` dans `resume_run`). Payload : `{run_id, workflow_id, status, step_index: i32 (-1 avant le premier step), total_steps, current_step: Option<String>}`. La SSE reste tab-local (chunk streaming par `RunEvent::StepProgress`) ; le WS sert au cross-tab : la transition vers un Gate devient visible live sur tous les onglets. Frontend handler dans `pages/WorkflowsPage.tsx` : refetch `detailRuns` quand le `workflow_id` match, skip quand un `liveRun` local non-fini est déjà driver autoritaire.
- **`exec_setup_command` + `exec_setup_args`** (`models::workflows::WorkflowStep`) — phase setup avant la commande principale d'un step `Exec`. Mêmes contraintes que `exec_command` (allowlist, pas de shell, argv literal). Échec du setup short-circuit la commande principale et marque le step `Failed`. Preset dropdown dans le wizard pour `composer install` / `npm ci` / `pnpm install --frozen-lockfile` / `yarn install` / `poetry install` / `pip install -r requirements.txt`. Indispensable pour qu'un step `make test` trouve ses deps dans un worktree fraîchement créé.
- **`StepResult.started_at: Option<DateTime<Utc>>`** — wall-clock authoritative capturé par le runner avant `execute_step`. Remplace l'estimate frontend (`runStart + sum(completed_durations)`) qui inflait quand un Gate restait pendant longtemps. Pour les Gates, `resume_run_from_gate` réécrit `duration_ms = now - started_at` pour refléter la vraie durée de la pause humaine (avant : ~0ms = temps de rendu).
- **Per-step token badge** — `StepResult.tokens_used` rendu en chip à côté du nom de step dans `WorkflowDetail` + `RunDetail`. Le compteur run-level reste affiché séparément (badge `wf.tokensTotal`).
- **`effectiveLiveRun` cross-tab synthesis** (`WorkflowDetail.tsx`) — quand `liveRun` est null (autre onglet, ou reload sur un run Gate-paused), synthétise un état pseudo-live à partir du dernier `WorkflowRun` non-fini dans `runs[]`. Sans ça la page collapsait à la vue config statique → l'user pensait que le run était bloqué.
- **Worktree volume parity in Docker** — `docker-compose.yml` ajoute un self-mount `${KRONN_REPOS_DIR}:${KRONN_REPOS_DIR}:rw` pour que les paths host (`/home/<user>/Repositories/...`) restent valides dans le container. `exec_step.rs` traduit `/host-home/` → `${KRONN_HOST_HOME}/` quand le `work_dir` cible un worktree créé côté host. Plus un mount `${KRONN_RUSTUP_HOME}` + `RUSTUP_HOME` env propagation pour que les shims cargo trouvent leur toolchain. `Makefile` auto-détecte `~/.cargo/bin`, `~/.rustup`, `~/.bun/bin` et écrit dans `.env`.

### MCP system (3-tier architecture)

Kronn manages MCPs with a 3-tier model:

```
mcp_servers (type)  →  mcp_configs (configured instance)  →  mcp_config_projects (N:N linkage)
```

**Servers** represent an MCP type (e.g. "GitHub"). Can come from the built-in registry (48 official servers), be detected from `.mcp.json` files, or be added manually.

**Configs** are configured instances of a server with encrypted env vars (AES-256-GCM), a label, and optional args override. One server can have multiple configs (e.g. two GitHub configs with different tokens). Deduplication via FNV-1a hash of (transport + args + env values).

**Project linkage** — N:N relationship. A config can be linked to multiple projects. The `is_global` flag means "applies to all projects" without explicit per-project linkage.

**Registry** — 34 built-in official MCP servers in `core/registry.rs`, grouped by category: Git & Code (GitHub, GitLab, Git), Databases (PostgreSQL, SQLite, Redis), BaaS (Supabase), Cloud & Infra (Cloudflare, AWS CloudWatch, Docker, Azure), Search & Web (Brave Search, Fetch, Exa), Browser (Puppeteer, Chrome DevTools, Playwright, Browserbase), Scraping (Firecrawl), Monitoring (Sentry, Grafana), Communication (Slack), Email (Resend), Project Management (Linear, Atlassian), Design (Figma), Knowledge & Docs (Notion, Context7), Payments (Stripe), AI & Reasoning (Memory, Sequential Thinking), SEO (Ahrefs), Files (Filesystem), Sandbox (E2B). Each has `env_keys` listing required environment variables, plus optional `token_url` (link to provider's token generation page) and `token_help` (short guidance text).

**Disk sync** — When linkages or config values change, Kronn writes agent-specific config files. The full mapping (Claude / Vibe / Kiro / Gemini / Codex / Copilot, per-project and global) plus the 0.6.0 outbound host sync (scope-aware Claude routing, `_kronn` marker, host_sync field, Phase 1+2+3 architecture) is documented in `docs/AGENTS.md § Host MCP sync` — single source of truth, do not duplicate here.

Quick recap (per-project files):
- Claude Code → `<project>/.mcp.json`
- Vibe → `<project>/.vibe/config.toml`
- Kiro → `<project>/.kiro/settings/mcp.json` + `<project>/.docs/mcp/mcp.json`
- Gemini → `<project>/.gemini/settings.json`

Quick recap (host-level / global files, written when `host_sync ≠ None`):
- Claude Code → `~/.claude.json` (top-level OR `projects[<host-path>].mcpServers` based on Kronn scope)
- Gemini → `~/.gemini/settings.json` (top-level)
- Codex → `~/.codex/config.toml` (top-level, stdio only)
- Copilot → `~/.copilot/mcp-config.json` (top-level, stdio only)

Sync triggers: toggle project, toggle global, toggle host_sync, create/update/delete config. Key naming: single config for a server → `server.name.to_lowercase()`, multiple configs of same server → `config.label`. Files are added to `.gitignore`. Codex/Copilot only support stdio (SSE/streamable skipped). All host-level files preserve non-Kronn entries (`_kronn` marker distinguishes ownership).

**MCP context files** — Per-MCP per-project instruction files at `docs/operations/mcp-servers/<slug>.md`. Auto-created with a default template on first disk sync. Customized files are injected into agent system prompts via `--append-system-prompt`. The `McpOverview` response includes `customized_contexts` (list of `"slug:projectId"` pairs) so the frontend can show colored icons for customized vs default context files.

**Detection & matching** — `POST /api/mcps/refresh` scans all projects' `.mcp.json` files, matches detected entries against the registry by command + package name (with version stripping), migrates `detected:*` server IDs to registry IDs, and cleans up orphan servers.

**Secret editing** — Inline editing of encrypted env vars directly in the MCP page. Per-field visibility toggle (eye icon) to show/hide individual values. On save, secrets are re-encrypted, config hash recomputed, and `.mcp.json` re-synced to all linked projects. **0.8.6**: for Custom plugins, the legacy edit-secrets drawer is replaced by a unified "Modifier le plugin" form (spec + values in one place) ; the card still shows a read-only env view with eye reveal for visibility. Registry plugins keep the legacy editable drawer (their spec is immutable so secrets are the only thing to edit).

**Agent API broker (0.8.6)** — `POST /api/agent-api/call` + `api_call` MCP tool let an agent invoke any Kronn-configured plugin (registry OR custom) **without ever seeing the credentials**. Reuses the workflow `ApiCall` executor byte-for-byte. Project scope resolved server-side from 3 fallback sources (in priority) : (1) explicit `project_id` arg, (2) `disc_id` → `disc.project_id` (Kronn-spawned agents), (3) `api_config_id` → `config.project_ids[0]` (host-CLI sessions outside Kronn). Auth happens server-side per `ApiSpec.auth` ; the agent provides plugin_slug + endpoint + (optional) query/headers/body/path_params. Non-secret identifiers (organization_id, account_id, workspace_slug) are referenceable via **`${ENV.KEY}` placeholders** that Kronn substitutes from the encrypted env at request build time — works in `endpoint_path`, `query`, `headers`, `body` (string leaves) and `path_params`. The substitution is case-insensitive on the `ENV.` prefix and normalises keys to UPPER_SNAKE for the lookup. The `mcp_list` MCP tool surfaces each plugin's `config_keys[]` with an `auth_managed: bool` flag so the agent distinguishes credentials (don't reference) from free-form identifiers (free to use as `${ENV.X}`). Deferred to 0.8.7+ : side_effect opt-in gate, per-disc rate-limit, persistent audit log (cf. `project_api_call_logs_0_8_6`).

**Custom API plugins (0.8.1, extended 0.8.6)** — Sentinel `api-custom` server id in the registry surfaces a "Custom API" tile pinned at the top of the Add-plugin drawer. Picking it swaps the panel to a freeform editor (name, base URL, free-form description, optional docs URL, N {label, value} fields, **endpoints[] declared inline + auth picker (0.8.6)**). On submit, the backend materializes a fresh `McpServer` with `source = McpSource::Manual`, transport `ApiOnly`, id `custom-{slug}-{nano}`. The `ApiSpec.config_keys` are derived by uppercasing/snake-casing the field labels (helper `slug_env_key`). **0.8.6** : `ApiSpec.auth` is no longer hardcoded to `ApiAuthKind::None` — the form exposes a picker for `None` / `Bearer` / `TokenExchange` (Didomi-shape : `POST /sessions` JSON body with `${ENV.KEY}` placeholders → `$.access_token` extracted via JSONPath → Bearer header injected on all subsequent calls, cached per-config with same `oauth2_cache` Mutex+TTL pattern as `OAuth2ClientCredentials`). Endpoints declared on the form land in `ApiSpec.endpoints[]` so the `api_call` MCP broker's allowlist allows them. `PUT /api/mcps/custom/:server_id` (0.8.6) lets the user edit the spec post-creation without delete-and-recreate ; the `server_id` is preserved across edits so existing references (configs, workflow ApiCall steps) stay valid. The unified form ALSO patches encrypted env values in the same submit so structure + credentials are managed in one place — the legacy "Edit secrets" drawer is hidden on Custom plugins (kept for registry plugins as their only env path). The AI helper (cf. § below) learns to fill endpoints + auth from the plugin's docs URL.

**AI helper bubble (shared, 0.8.1 UX refactor)** — Single-phase chat used by both `ApiCallAiHelper` (workflow steps) and `CustomApiAiHelper` (custom API form). Phase `picking-agent` removed; agent selection is a dropdown in the bubble header. Welcome state with 3 starter chips (curl / docs link / freeform) when no message has been sent yet — avoids auto-firing the agent for a generic intro. CSS lives in `frontend/src/components/aiHelper.css` (extracted from `WorkflowsPage.css` so styles load on McpPage too). Both helpers share `parseApplyBlocks` from `ApiCallAiHelper.tsx` (the `KRONN:APPLY` wire contract). TD-helpers-unify: a future refactor should extract a shared `<AiChatHelperShell>` so the two helpers stop duplicating ~60% of lifecycle code.

**API endpoints:**
- `GET /api/mcps` — overview (servers + configs with masked secrets + customized_contexts)
- `GET /api/mcps/registry` — built-in registry (searchable, includes token_url/token_help)
- `POST /api/mcps/refresh` — scan & detect
- `POST /api/mcps/configs` — create config (auto-creates server from registry if needed)
- `PUT /api/mcps/configs/:id` — update config (label, env, global, args)
- `DELETE /api/mcps/configs/:id` — delete config
- `PUT /api/mcps/configs/:id/projects` — set project linkages
- `POST /api/mcps/configs/:id/reveal` — decrypt and reveal secrets
- `GET /api/mcps/context/:project_id` — list MCP context files for a project
- `GET /api/mcps/context/:project_id/:slug` — read a single MCP context file
- `PUT /api/mcps/context/:project_id/:slug` — update a MCP context file

### AI audit pipeline

4-state system computed from filesystem (not DB):

```
NoTemplate → TemplateInstalled → Audited → Validated
```

- **Detection**: `scanner::detect_audit_status()` checks `docs/AGENTS.md` existence, `KRONN:BOOTSTRAP`/`{{` markers, and `KRONN:VALIDATED` marker.
- **TODO counting**: `scanner::count_ai_todos()` walks `docs/*.md` files and counts `<!-- TODO` occurrences. Exposed as `Project.ai_todo_count` (computed on-the-fly by `enrich_audit_status()`).
- **Template install** (`POST /api/projects/:id/install-template`): copies `docs/` skeleton + redirectors (CLAUDE.md, .cursorrules, etc.) non-destructively, injects bootstrap prompt block (`KRONN:BOOTSTRAP:START` to `KRONN:BOOTSTRAP:END`).
- **AI audit** (`POST /api/projects/:id/ai-audit`): SSE-streamed 10-step analysis. Each step runs an agent call with `full_access: true` and default profiles (Architect + Tech Lead + Mentor) for multi-perspective analysis. Bootstrap block removed before audit starts. Steps defined in `ANALYSIS_STEPS` constant.
- **Validation**: creates a Discussion with title "Validation audit AI" and a locked prompt. The AI asks questions about ambiguities/TODOs, updates `docs/` files after each answer. Frontend detects validation-in-progress by matching discussion title + project_id. Project page only shows "validation en cours" + link (no validate button).
- **Completion detection**: the prompt instructs the AI to include `KRONN:VALIDATION_COMPLETE` in its final message. Frontend detects this in the last agent message and shows a green banner with "Marquer l'audit comme valide" button — only in the discussion view.
- **Mark validated** (`POST /api/projects/:id/validate-audit`): injects `<!-- KRONN:VALIDATED:YYYY-MM-DD -->` at end of `docs/AGENTS.md`.

**Skill auto-detection**: between audit Phase 2 and Phase 3, `detect_project_skills()` scans project filesystem for config files (Cargo.toml, tsconfig.json, go.mod, etc.) and maps them to skill IDs. Detected skills are saved to DB and used for the validation discussion.

**API endpoints:**
- `POST /api/projects/bootstrap` — create project from scratch (dir + git init + template + discussion)
- `POST /api/projects/:id/install-template` — copy template, inject bootstrap
- `POST /api/projects/:id/ai-audit` — SSE streaming 10-step audit
- `POST /api/projects/:id/validate-audit` — mark audit as validated

**0.8.2 hardenings (`AuditKind` + anti-repetition + `audit_runs`):**

- **`AuditKind` enum** (`models::projects::AuditKind`) — `Full` reste la base (10 steps), plus `Security`, `Docker`, `Performance`, `Accessibility`, `Database`, `ApiDesign`, `Custom`. Chaque kind dispatche son prompt système dédié dans `api/audit/mod.rs`, avec son set de baseline checks. Un audit Security n'est plus un audit Full avec du focus sécu en plus.
- **Mandatory baseline checklist (Step 9)** — 4 checks non-skippables (auth, data persistence, external input handling, secrets in source) qui émettent une TD baseline même quand le scan dimensionnel n'a rien trouvé. Empêche les audits "vides" sur du code qui mérite au moins un signalement. Le cap par run est passé à 30 TDs (avant : 15–20), avec Critical/High exempts du cap.
- **Anti-repetition triplet** — (1) slug-matching sur les TDs existantes : un nouveau scan qui trouve une TD avec un slug légèrement différent REUSE l'ID existant et APPEND une entrée `audit_history` au lieu de créer un doublon ; (2) reconciliation pass : les TDs disparues entre deux audits sont marquées `Resolved` (pas orphelines) ; (3) two-tier `Status` (`Active`/`Reopened`) pour distinguer une vraie régression d'un faux positif. Le slug-churn (le pire anti-pattern d'audit) est désormais bloqué par construction. Le format `audit_history` YAML est documenté dans le prompt audit (`api/audit/mod.rs:283-308`).
- **`audit_runs` table** (migration 050) — une row par invocation : `id, project_id, kind, agent_type, started_at, ended_at, duration_ms, status, td_critical/high/medium/low/total, td_resolved_since_last, td_new_since_last, td_carried_over, health_score, report_path, recommendations_json`. Source de vérité pour la sparkline du badge santé et la chip "delta depuis dernier audit". Accessible via `db::audit_runs::{list_recent, latest_completed}`.
- **Health score** — `compute_health_score(critical, high, medium, low) -> u8` (0–100). Calibration : Critical 12 pts, High 4 pts, Medium 1.5 pt, Low 0.3 pt. Calé sur l'audit DOCROMS_WEB de mai (1C/6H/6M/5L → 53 = "significant debt"). Clampé `[0, 100]`.
- **Cluster detector + recommendations** — Step 10 du Full audit inspecte la distribution des TDs et émet des `AuditRecommendation` (kind à recommander, raison textuelle, cluster_size). Persisté dans `audit_runs.recommendations_json`. Surface en chip cluster dans le health badge ("4+ TDs Security → lance un audit Security"). Cap : top 3 recos par run.
- **Audit elapsed counter** — frontend lit `AuditProgress.started_at` (RFC3339), parse en `auditStartedAt: number | null`, et drive un ticker `setInterval(1s)` qui re-render uniquement quand `auditActive`. Aucun nouvel event SSE — purement client-side.

### Token usage & cost tracking

- **Per-message cost**: `messages.cost_usd` column (migration 024). Real cost from Claude Code `result` stream event (`cost_usd` field). Fallback: static estimation via `core/pricing.rs` (per-provider pricing table: Anthropic, OpenAI, Google, Mistral, Amazon).
- **`StreamJsonEvent::Usage`**: captures `input_tokens`, `output_tokens`, and `cost_usd: Option<f64>` from Claude Code's stream-json output. `cost_usd` is `Some` on the `result` line, `None` on intermediate `message_delta` events.
- **Stats API**: `GET /api/stats/tokens` → `TokenUsageSummary` (total tokens/cost, by_provider, by_project, top_discussions, top_workflows, daily_history 30 days). `GET /api/stats/agent-usage` → per-agent breakdown with project detail.
- **Frontend**: `UsageSection` component in Settings. Summary cards, provider bar, project bars, daily stacked chart (CSS-only), collapsible top-5 lists. Toggle tokens/cost view. Filter discussions/workflows. Discussion names are clickable (navigates to discussion page).
- **Pricing fallback**: when `cost_usd` is null in DB, `pricing::estimate_cost(agent_type, tokens)` applies a 60/40 input/output split with provider-specific per-1M-token rates.

### DB management API
- `GET /api/config/db-info` — returns DB size and record counts per table.
- `GET /api/config/export` — full JSON dump of all data.
- `POST /api/config/import` — restore from JSON dump.
- UI in Config page with counters, export button, import button.

### Agent colors (consistent everywhere)
- Claude Code: `#D4714E` (terracotta)
- Codex: `#10a37f` (OpenAI green)
- Vibe: `#FF7000` (Mistral orange)
- GitHub Copilot: `#238636` (GitHub green)
- Gemini CLI: `#4285f4` (Google blue)
- Kiro: `#7B61FF` (Kiro purple)

## Separation of concerns

- `models/` — Pure data structures, no logic.
- `api/` — HTTP handlers, request validation, SSE streaming.
- `core/` — Business logic (config, scanning, registry).
- `db/` — SQLite persistence, migrations, CRUD operations.
- `agents/` — External CLI process management.
- `workflows/` — Workflow engine: triggers, steps, template rendering, workspaces, tracker adapters (GitHub).

## Data flow

```
User → nginx (gateway:3456)
  → /api/* → backend (axum:3140)
    → [auth middleware] → handlers → state (SQLite via Database struct) / agent runner → SSE response
  → /* → frontend (nginx:80)
    → React SPA → fetch /api/* via api.ts (with Bearer token if auth enabled)
```

### Workflow execution flow (implemented)

```
WorkflowEngine (polling loop, ticks every 30s)
  → check cron triggers → spawn run (respect concurrency_limit)
  → check tracker triggers → poll API → reconcile (skip already-processed) → spawn run per new issue
  → manual trigger via API → spawn run immediately

WorkflowRunner (per run)
  → create workspace (git worktree add -b kronn/<workflow>/<run-id>)
  → run workspace hooks: after_create
  → run workspace hooks: before_run
  → for each step:
    → resolve per-step MCPs → sync to disk
    → render prompt template (Liquid: issue context, previous_step.output, steps.<name>.output)
    → if mode=normal: call agent runner (with optional AgentSettings override)
    → if mode=debate: call multi-agent orchestration
    → monitor stall timeout (kill if no output for N seconds)
    → on failure: retry with exponential backoff (if configured)
    → evaluate on_result conditions (stop/skip/goto)
    → record StepResult (output, tokens, duration)
  → run workspace hooks: after_run
  → cleanup workspace (git worktree remove)
  → run workspace hooks: before_remove
  → emit SSE events throughout for real-time UI updates
```
