# Implementation Status

**Total: ~803 tests**, 0 clippy warnings.

---

### athen-core (22 source files, 51 tests)
**Status**: Complete -- all types, trait contracts, and config loading.
- `traits/reminder.rs`: `SystemReminderBuilder` trait + `ReminderContext` (iteration, tools_called, recent_failed_tools). Drives ephemeral `<system-reminder>` injection in the executor — anti-drift port. Default impl lives in `athen-agent::reminders`.
- `error.rs`: `AthenError` enum (Io, Serialization, TaskNotFound, ToolNotFound, LlmProvider, RiskThresholdExceeded, Timeout, Sandbox, Ipc, Config, Other) + `Result<T>` alias
- `event.rs`: `SenseEvent`, `EventSource`, `EventKind`, `SenderInfo`, `NormalizedContent`, `Attachment`
- `task.rs`: `Task`, `TaskStep`, `TaskPriority` (Background..Critical), `TaskStatus` (7 states), `StepStatus`, `DomainType`
- `risk.rs`: `RiskScore` with `decision()` method, `RiskLevel`, `RiskDecision`, `RiskContext`, `BaseImpact`, `DataSensitivity`
- `contact.rs`: `Contact`, `TrustLevel` (T0..T4) with `risk_multiplier()`, `ContactIdentifier`, `IdentifierKind`
- `llm.rs`: `LlmRequest`, `LlmResponse`, `ChatMessage`, `Role`, `MessageContent`, `ToolCall`, `TokenUsage`, `FinishReason`, `LlmChunk` (includes `is_thinking: bool`, `tool_calls: Vec<ToolCall>`), `BudgetStatus`, `LlmStream`, `ModelProfile`. `LlmResponse` includes `reasoning_content: Option<String>`.
- `tool.rs`: `ToolDefinition`, `ToolBackend` (NativeMcp/Shell/Script/HttpApi), `ToolResult`, `AuthConfig`, `ScriptRuntime`, `HttpMethod`
- `sandbox.rs`: `SandboxLevel` (None/OsNative/Container), `SandboxProfile`, `SandboxCapabilities`, `Mount`
- `ipc.rs`: `IpcMessage`, `IpcPayload` (14 variants), `ProcessId`, `ProcessType`, `ProcessTarget`, `TaskProgressReport`, `TaskControlCommand`, `ControlAction`, `ProcessRegistration`, `ProcessHealthStatus`, `ApprovalRequest`, `ApprovalResponse`
- `notification.rs`: `Notification`, `NotificationUrgency` (Low/Medium/High/Critical), `NotificationOrigin` (RiskSystem/SenseRouter/Agent/System), `DeliveryResult`, `DeliveryStatus`
- `config.rs`: `AthenConfig`, `OperationMode`, `OperationConfig`, `ModelsConfig`, `ProviderConfig`, `AuthType`, `ProfileConfig`, `DomainConfig`, `SecurityConfig`, `SecurityMode`, `PersistenceConfig`, `EmailConfig` (enabled, imap_server, imap_port, username, password, use_tls, folders, poll_interval_secs, lookback_hours -- defaults: disabled, port 993, TLS on, INBOX, 60s poll, 24h lookback), `TelegramConfig` (enabled, bot_token, owner_user_id: Option<i64>, allowed_chat_ids: Vec<i64>, poll_interval_secs -- defaults: disabled, 5s poll), `NotificationConfig` (preferred_channels, escalation_timeout_secs, quiet_hours), `QuietHours` (start_hour/minute, end_hour/minute, allow_critical), `NotificationChannelKind` (InApp, Telegram), `EmbeddingConfig` (mode, provider, model, base_url, api_key), `EmbeddingMode` (Automatic, Cloud, LocalOnly, Specific, Off). Both `NotificationConfig` and `EmbeddingConfig` added to `AthenConfig` with `#[serde(default)]`.
- `config_loader.rs`: `load_config(path)`, `load_config_dir(dir)`, `save_default_config(path)`. Loads TOML files with serde defaults for missing fields. Supports split config: `config.toml` (main) + optional `models.toml` override.
- `traits/`: 11 trait files defining all inter-module contracts
  - `traits/notification.rs`: `NotificationChannel` trait with `channel_kind()` and `send()`
  - `traits/embedding.rs`: `EmbeddingProvider` trait with `provider_id()`, `dimensions()`, `embed()`, `embed_batch()`, `is_available()`
  - `traits/memory.rs`: `VectorIndex` trait: added `list_all()` default method. `KnowledgeGraph` trait: added `list_entities()`, `list_relations()`, `update_entity()`, `delete_entity()`, `delete_relation()` default methods. `EntityExtractor` trait with `extract()` method + `ExtractionResult` struct (entities: Vec<Entity>, relations: Vec<(String, String, String)>)
  - `traits/profile.rs`: `ProfileStore` trait — `list_profiles`, `load_profile(id)`, `save_profile`, `delete_profile`, `list_templates`, `resolve_templates(ids)`. Implemented in `athen-persistence`.
  - `traits/compaction.rs`: `ArcCompactor` trait + `ContextEntry` (persistence-agnostic mirror of `ArcEntry`), `ArcContextView` (summary + tail + tool_cache), `CompactionOutcome`. Methods: `should_compact`, `compact`, `load_context_view` — all parameterised on `model_window_tokens` + `target_tokens` rather than a `ModelId` to stay provider-agnostic. **Phase-1 shipped**: `LlmArcCompactor` implementation lives at `crates/athen-app/src/compaction.rs` and is wired into the executor via `commands.rs::execute_*_task`. See `docs/ARC_COMPACTION.md` §12 for the implementation status map.
  - `config.rs`: `ProviderConfig` carries the compaction budget (`context_window_tokens` default 128k, `compaction_trigger_pct` default 65, `compaction_target_pct` default 30 — read by `LlmArcCompactor` to derive per-arc budgets) and `temperature: Option<f32>` (per-provider sampling override; surfaced in Settings UI as `provider-temperature`, applied at request construction; per-task / per-profile override not yet wired — see `project_per_task_model_selection.md`).
- `agent_profile.rs`: AgentProfile system. `AgentProfile` (id, display_name, description, persona_template_ids, custom_persona_addendum, tool_selection, expertise, model_profile_hint, builtin, timestamps; `DEFAULT_ID = "default"`). `PersonaTemplate` (id, display_name, body, builtin) categorized by `PersonaCategory` (Voice/Mission/Constraints/OutputStyle). `ToolSelection` (All/Groups/Explicit/Deny — defaults to All). `ExpertiseDeclaration` (domains, task_kinds, languages, strengths, avoid). `DomainTag` closed enum (Email, Calendar, Messaging, Coding, Research, Outreach, Marketing, Finance, Scheduling, DataAnalysis, Writing, Translation, Health, Legal, Infrastructure, Architecture, Support, SocialMedia, Other) — additive-only. `TaskKindTag` closed enum (Drafting, Editing, Summarizing, Researching, Scheduling, CodeReview, Coding, Debugging, DataAnalysis, Outreach, Triage, Other). `ResolvedAgentProfile { profile, persona_templates }` is what the executor receives — `has_custom_persona()` returns false for the seeded default so today's hardcoded persona is preserved.
- `wakeup.rs`: Scheduled / recurring / one-shot proactive triggers (see `docs/WAKEUPS.md`). `Wakeup` (id, schedule, instruction, autonomy, preferred_channel, tool_allowlist, contact_allowlist, profile, arc_id, origin, created_at, last_fired_at, next_fire_at, enabled). `Schedule` enum (`OneShot { at }`, `Cron { expr, tz }`, `Interval { every_seconds, anchor }`) — opaque cron string at this layer; the scheduler parses. `AutonomyBand` enum (`Auto`, `SafeOnly`, `NotifyOnly`) — `from_str_lossy` falls back to `SafeOnly` on unknown so a future variant we forgot to handle never silently widens autonomy. `WakeupOrigin` (`User`, `Agent { authoring_arc_id }`) tagged enum so the UI can link agent-authored wake-ups back to their arc. `traits/wakeup.rs`: `WakeupStore` trait — `create`, `update`, `delete`, `get`, `list_all` (newest-first), `list_due(now)` (enabled + `next_fire_at <= now`, ordered by next-fire ascending), `mark_fired(id, fired_at, next_fire_at)` (atomic; `None` next clears the schedule), `set_enabled`. `WakeupFireSink` trait (one method `fire(&Wakeup, fired_at)`) — the dispatch port the scheduler hands fires to; coordinator wires it in Phase 3. 5 tests in core.
- `profile_routing.rs`: Coordinator scoring helpers. `ClassifiedTask { domain, task_kind, language, raw_text }`. `classify_task(source, lower_text, language)` infers domain from `EventSource` first, falls back to `infer_domain_from_keywords(text)` when source is None or `Other` (keyword groups for Infrastructure/SocialMedia/Legal/Health/Architecture/Support/Outreach/Marketing/Coding/Research). `pick_profile(classified, profiles)` keyword-only scoring (domain match, task_kind match, anti-match penalty, builtin tiebreaker). `pick_profile_blended(classified, profiles, query_embedding, profile_embeddings)` adds semantic blend on top: `kw + sem * SEMANTIC_WEIGHT (4.0)`, filter `*kw > 0 || *sem > 0.55` so a weak embedding-only match cannot win over a clean keyword miss. `profile_embedding_text(p)` concatenates display_name + description + strengths + addendum for embedding. `cosine_similarity(a, b)` returns 0.0 on length mismatch / empty / zero norm. Backwards compatible: `None` query embedding falls through to keyword-only.

### athen-ipc (13 unit + 5 integration = 18 tests)
**Status**: Complete -- full IPC transport layer.
- `transport.rs`: `IpcTransport` trait + `UnixTransport` implementation using split `UnixStream` halves with independent `Mutex`es for concurrent send/recv. Length-prefixed framing (4-byte big-endian). 16 MiB message size limit.
- `codec.rs`: `encode()` serializes `IpcMessage` to length-prefixed JSON bytes. `decode()` deserializes. `read_length_prefix()` extracts u32 from 4 bytes.
- `server.rs`: `IpcServer` binds `UnixListener`, accepts connections, spawns per-connection reader tasks, identifies processes by first message's `source` field. Methods: `send_to()`, `broadcast()`, `broadcast_to_type()`, `route()`, `connected_count()`, `shutdown()`. `IpcClient` connects to coordinator, auto-sends Registration message.

### athen-risk (62 unit + 6 integration = 68 tests)
**Status**: Complete -- full risk evaluation engine with natural language intent detection and AuthUser bypass.
- `scorer.rs`: `RiskScorer` implementing the formula `(Ibase * Morigen * Mdatos) + Pincertidumbre`. Confidence penalty: `(1.0 - confidence)^2 * 100`. Maps total to RiskLevel and RiskDecision. Implements `RiskEvaluator` trait.
- `rules.rs`: `RuleEngine` with compiled regex patterns using `LazyLock`. Returns `RuleMatch` with `base_impact`, `data_sensitivity`, `matched_patterns`, and `intent_based` flag. Detects:
  - **Dangerous shell commands**: rm -rf, sudo, dd, mkfs, chmod 777, redirect to /dev/, pipe to sh/bash/zsh
  - **Natural language destructive intent** (`DESTRUCTIVE_INTENT` patterns): delete/remove/erase/wipe/destroy/nuke + file/folder/dir/everything/all (both orderings), format/reset/clear/empty/purge + disk/drive/partition/database/system, kill/terminate + all/every + process/service, modify/change/edit/overwrite + system/config/password/credentials, send/post/upload + data/file/secret/key/token. Intent-based matches set `intent_based = true` and receive `confidence = 0.6` (adds 16-point uncertainty penalty), pushing scores into HumanConfirm range.
  - **Secrets**: OpenAI keys (sk-...), AWS keys (AKIA...), private key headers, passwords in URLs
  - **PII**: email addresses, phone numbers
  - **Financial keywords**: payment, transfer, purchase, buy, invoice, billing, credit card
  - **External URLs**: http/https URLs
  Returns `Option<RiskScore>` -- `Some` if confident, `None` for LLM fallback.
- `llm_fallback.rs`: `LlmRiskEvaluator` takes `Box<dyn LlmRouter>`. 30-second timeout on LLM calls. Constructs structured prompt that considers what the agent would ACTUALLY DO (not just literal text) -- asks LLM to classify impact as system for any delete/remove/wipe/destroy actions. Conservative fallback on failure/timeout: `WritePersist + PersonalInfo + 0.3 confidence` (lands in HumanConfirm range, score ~89).
- `path_eval.rs`: `PathRiskEvaluator<G: GrantLookup>` -- deterministic per-path risk classification without LLM calls. Evaluates path-touching operations (Read/Write) with a fixed hierarchy: allowed system paths (read-only) < user home (read-allowed, write-risk) < granted directories (allow/deny based on arc grant) < sensitive system dirs (always deny write). Falls back to `RiskScorer` for final numeric score landing in the correct decision band. Wired by `FileGate` in athen-app for agent file access control.
- `lib.rs`: `CombinedRiskEvaluator` implementing `RiskEvaluator` -- tries rules first, falls back to LLM if rules return `None`. **AuthUser bypass**: when rules return `None` (no dangerous patterns) and trust is `AuthUser`, returns a safe score (0.5) directly -- skips LLM risk fallback entirely for benign user input, reducing latency.

### athen-sandbox (32 tests)
**Status**: Complete -- tiered sandboxing with auto-detection. All three desktop platforms have a real OS-native backend now.
- `detect.rs`: `SandboxDetector::detect()` checks for bwrap, landlock, macOS sandbox, Windows sandbox, Podman, Docker. Platform-specific checks short-circuit to false on wrong OS.
- `container.rs`: `ContainerExecutor` with `ContainerRuntime` enum (Podman/Docker). Auto-detects runtime. `build_run_args()` constructs container run command with --rm, --network=none, -v mounts, --memory, --cpus, --timeout.
- `bwrap.rs` (Linux): `BwrapSandbox` builds bwrap commands per `SandboxProfile` -- ReadOnly (--ro-bind / /), RestrictedWrite (--bind for allowed paths), NoNetwork (--unshare-net), Full (--unshare-all). Always includes --die-with-parent, --new-session.
- `landlock.rs` (Linux): Stub returning "not yet implemented".
- `macos.rs` (macOS): `MacOsSandbox` generates Lisp-syntax Seatbelt profiles per `SandboxProfile`: `(allow default)` + `(deny file-write*)` + per-path `(allow file-write* (subpath "..."))` for `RestrictedWrite` (mirrors bwrap's read-everywhere + write-only-allowed semantics); `(deny network*)` for `NoNetwork`/`Full`. Also re-allows writes to `/dev/null`, `/dev/dtracehelper`, `/dev/tty*`, `/dev/fd/N` so common commands still work. Profile written to a tempfile and passed to `/usr/bin/sandbox-exec -f` (avoids shell escaping for paths with spaces/quotes). `escape_path` handles backslash and double-quote escaping for the Lisp string literal. Tempfile held alive via `_guard` binding across the await. `kill_on_drop(true)` on the spawned command.
- `windows.rs` (Windows): `WindowsSandbox` layers two primitives.
  - **Job Object** (`JobObjectGuard`): `CreateJobObjectW` + `JOBOBJECT_EXTENDED_LIMIT_INFORMATION` with `KILL_ON_JOB_CLOSE` (process tree dies with the parent), `PROCESS_MEMORY` cap (2 GB/proc), `ACTIVE_PROCESS` cap (64). Process is created `CREATE_SUSPENDED`, assigned to job, then resumed.
  - **AppContainer** (`AppContainerProfile`): per-execution unique SID via `CreateAppContainerProfile` (with `DeriveAppContainerSidFromAppContainerName` fallback), grants ACL access to the resolved binary path + each `allowed_paths` entry + `%LOCALAPPDATA%\Athen\runtimes\` (so portable Python/Node loads), skips `C:\Windows\` (already accessible to ALL APPLICATION PACKAGES). `internetClient` capability included only for `ReadOnly`/`RestrictedWrite`; `NoNetwork`/`Full` ship empty capability list (no socket access).
  - Process spawn uses `STARTUPINFOEXW` + `PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES`. Stdout/stderr captured via inheritable pipes (read ends marked non-inheritable). All resources RAII-wrapped (`JobObjectGuard`, `ProcThreadAttributeList`, `StdPipe`, `AppContainerProfile`, `AclGrantGuard`, `LoadedLibrary`). AppContainer APIs reached via dynamic load from `userenv.dll` because `windows` 0.59 doesn't expose them through any feature flag.
  - `quote_arg` implements full CommandLineToArgvW backslash-doubling rule. Live-execution tests are `#[cfg(target_os = "windows")] #[ignore]` since they need a real Windows kernel; cross-typechecked via `cargo check --target x86_64-pc-windows-gnu`.
- `lib.rs`: `UnifiedSandbox` facade -- auto-detects capabilities, selects best sandbox per level (bwrap > landlock > macos > windows for OsNative; podman > docker for Container).

### athen-coordinador (37 unit + 4 integration = 41 tests)
**Status**: Complete -- full coordinator orchestration with persistence, trust, and approval management.
- `router.rs`: `DefaultRouter` implementing `EventRouter`. Maps EventSource->DomainType (Email/Messaging->Communication, Calendar->Agenda, UserInput/System->Base). Priority: UserInput/Calendar=High, Messaging/Email=Normal, System=Low.
- `queue.rs`: `PriorityTaskQueue` implementing `TaskQueue`. Uses `BinaryHeap<PrioritizedTask>` -- higher priority first, FIFO within same priority (oldest first).
- `dispatcher.rs`: `Dispatcher` manages agent availability. `register_agent()`, `unregister_agent()`, `assign_task()`, `release_agent()`, `assigned_agent()`, `force_release_all()` -- force-releases all assigned agents back to the available pool (used in single-user desktop apps where stale assignments can block new tasks).
- `risk.rs`: `CoordinatorRiskEvaluator` wrapping `Box<dyn RiskEvaluator>`. `evaluate_and_decide()` returns `RiskDecision`.
- `lib.rs`: `Coordinator` wiring all components with optional persistence, trust management, and human approval flow:
  - `.with_persistence(Box<dyn PersistentStore>)` -- attaches SQLite store for task durability. `process_event()` saves tasks after creation, `complete_task()` updates status in DB. Persistence errors are logged but never crash the system.
  - `.with_trust_manager(TrustManager)` -- enables contact-aware risk evaluation. `process_event()` resolves sender trust via `TrustManager` and factors it into risk scoring (AuthUser for UserInput, resolved trust for external senders, Neutral fallback). `complete_task()` records approval for implicit trust evolution.
  - `recover_tasks()` -- loads non-terminal tasks from persistent store and re-enqueues them on startup. Terminal statuses (Completed, Failed, Cancelled) are skipped.
  - `process_event()` routes->resolves sender trust->evaluates risk->sets status->persists->enqueues. Returns `Vec<(TaskId, RiskDecision)>` instead of `Vec<TaskId>`, so the app layer can differentiate `NotifyAndProceed` from `SilentApprove` and trigger appropriate notifications. Tasks with `HumanConfirm` risk go to `awaiting_approval` map instead of queue. `dispatch_next()` dequeues and assigns to agent. `complete_task()` releases agent->updates DB->records trust approval.
  - **Approval management**: `awaiting_approval: Mutex<HashMap<TaskId, Task>>` holds tasks pending human decision. `get_awaiting_approval()` returns the first pending task. `approve_task(id)` moves task to Pending and enqueues. `deny_task(id)` sets status to Cancelled and persists.
  - `infer_identifier_kind()` helper -- infers `IdentifierKind` (Email/Phone/Other) from sender identifier strings.
  - `task_contacts: Mutex<HashMap<TaskId, ContactId>>` -- maps task IDs to resolved contact IDs for trust feedback on completion.

### athen-memory (46 unit + 5 integration = 51 tests)
**Status**: Complete -- vector search + knowledge graph with SQLite persistence + real embeddings + LLM entity extraction + hybrid retrieval + UI management methods.
- `vector.rs`: `InMemoryVectorIndex` -- brute-force cosine similarity search. `tokio::sync::RwLock` for concurrent reads.
- `graph.rs`: `InMemoryGraph` -- BFS exploration from entry node. Scoring combines recency (exponential decay, 7-day half-life), frequency, and importance weighted by `ExploreParams`.
- `sqlite.rs`: `SqliteVectorIndex` and `SqliteGraph` -- SQLite-backed persistent versions. Embeddings stored as little-endian f32 blobs. Uses `std::sync::Mutex` (not tokio) since rusqlite is synchronous and locks are never held across `.await`. `SqliteVectorIndex::list_all()` orders by `rowid DESC` (newest first). `SqliteGraph::add_entity()` deduplicates by name (case-insensitive COLLATE NOCASE). `SqliteGraph` implements all new `KnowledgeGraph` methods (`list_entities`, `list_relations`, `update_entity`, `delete_entity`, `delete_relation`).
- `extractor.rs`: `LlmEntityExtractor` implementing `EntityExtractor` -- extracts entities and relations from text via LLM calls (Cheap profile, 30s timeout). Parses structured JSON output into `ExtractionResult`. Filters garbage entities (underscores, short names, "user"/"assistant"/"system").
- `lib.rs`: `Memory` facade implementing `MemoryStore`. `min_relevance_score: f32` field with `with_min_score()` builder (default 0.3). Three-phase intelligence:
  - **Phase 1 (Embeddings)**: `embedder: Option<Box<dyn EmbeddingProvider>>` with `with_embedder()` builder. `remember()` generates real embeddings via provider, stores content in `metadata._content`. `recall()` embeds query for semantic vector search, filters by `min_relevance_score` before returning results.
  - **Phase 2 (Entity Extraction)**: `extractor: Option<Box<dyn EntityExtractor>>` with `with_extractor()` builder. `remember()` auto-extracts entities and relations from content via LLM, adds them to the knowledge graph automatically.
  - **Phase 3 (Hybrid Retrieval)**: `recall()` performs hybrid vector search + graph exploration. Graph-connected results receive a score boost, results are deduplicated and merged for richer context.
  - `forget()` removes from vector index.
  - **UI management methods**: `list_all()`, `update()` (re-embeds on save), `list_entities()`, `list_relations()`, `update_entity()`, `delete_entity()`, `delete_relation()` -- expose full CRUD for the frontend memory management UI.

### athen-sentidos (81 tests)
**Status**: Complete -- user input monitor, full email monitor, full calendar monitor, full telegram monitor, messaging stub, polling runner.
- `user_input.rs`: `UserInputMonitor` using `tokio::sync::Mutex<mpsc::Receiver<String>>` for interior mutability. Converts strings to `SenseEvent` with EventSource::UserInput, EventKind::Command, RiskLevel::Safe. Exposes `sender()` for UI to push messages.
- `email.rs`: Full `EmailMonitor` -- real IMAP polling via `imap` v2.4 (sync, wrapped in `spawn_blocking`) + `rustls-connector` for TLS (no OpenSSL). `mailparse` for MIME body parsing (text, HTML, attachments). Tracks `last_seen_uid` for incremental polling. Uses `BODY.PEEK[]` to avoid marking emails as read. `extract_email_body()` recursively walks MIME parts. Configurable via `EmailConfig` (server, port, TLS, folders, poll interval, lookback). 15 tests.
- `calendar.rs`: Full `CalendarMonitor` -- polls SQLite every 60s, queries events within 7 days using `datetime()` for timezone-safe comparison. 21 tests.
  - `generate_reminder_events()` -- fires reminders when `minutes_until <= reminder_minutes` for each event
  - "Starting now" notification when event within 1 minute, even without explicit reminders
  - Session-level deduplication via `Mutex<HashSet<(event_id, reminder_minutes)>>`
  - Opens fresh rusqlite connection per poll (safe at 60s interval)
  - Graceful: returns empty if DB or table doesn't exist
  - Added `rusqlite` and `tempfile` (dev) dependencies to athen-sentidos
- `telegram.rs`: Full `TelegramMonitor` -- raw HTTP via `reqwest` (no Telegram framework dependency). Uses `getUpdates` long-polling with offset tracking. Handles text messages, photo captions, document captions. Owner messages (matching `owner_user_id`) -> `RiskLevel::Safe` (L1), others -> `RiskLevel::Caution` (L2). Configurable `allowed_chat_ids` filtering. Exports public `send_message(bot_token, chat_id, text)` utility function used by both the Telegram monitor and the notification system. 22 tests.
- `telegram_send.rs`: Outbound Telegram adapter `BotApiTelegramSender` implementing `TelegramSender` trait. Backs the agent's `send_telegram` tool. `sendMessage` for text (auto-chunks at 4096 chars on line/word boundaries, threads only the first chunk against `reply_to_message_id`), `sendPhoto` / `sendDocument` via multipart upload. Caption affinity: a single attachment + short text (≤1024 chars, no explicit caption) gets the text packed as the file's caption (one bubble); multiple attachments → text leads as its own bubble, each file follows. `Auto` kind dispatches by extension (image extensions → photo, everything else → document). `test_connection()` hits `/getMe`. 8 unit tests.
- `messaging.rs`: Stub `MessagingMonitor` -- 30s poll interval.
- `lib.rs`: Generic `SenseRunner<M: SenseMonitor>` -- polling loop with `tokio::select!` for shutdown signal. Sends events through mpsc channel.

### athen-shell (20 tests)
**Status**: Complete -- cross-platform shell execution.
- `native.rs`: `NativeShell` -- uses `sh -c` on Unix, `cmd /C` on Windows via `tokio::process::Command`. 30-second timeout. Captures stdout/stderr/exit code/execution time. `which()` uses system `which`/`where`.
- `nushell.rs`: `NushellShell` -- auto-detects `nu` binary. If available: `nu -c "command"`. If not: falls back to NativeShell with info log.
- `lib.rs`: `Shell` unified facade. `execute()` prefers nushell, `execute_native()` always native. Convenience: `run()` returns stdout, `run_ok()` returns bool, `has_program()` checks existence.

### athen-persistence (150 unit + 5 integration = 155 tests)
**Status**: Complete -- SQLite persistence with atomic checkpoints, arcs, calendar, contacts, notifications, legacy chat history, and per-`chat_id` Telegram transcript log.
- `lib.rs`: `Database` struct with `new(path)` and `in_memory()`. Auto-creates tables on init (including arc, calendar, contacts, notifications, and legacy chat tables). Provides `store()` -> `SqliteStore`, `chat_store()` -> `ChatStore`, `arc_store()` -> `ArcStore`, `calendar_store()` -> `CalendarStore`, `contact_store()` -> `SqliteContactStore`, and `notification_store()` -> `NotificationStore` accessors.
- `store.rs`: `SqliteStore` implementing `PersistentStore`. Full CRUD for tasks (with steps serialized as JSON), checkpoints with SHA-256 integrity verification, pending messages with atomic pop (transaction-based select+update).
- `checkpoint.rs`: `CheckpointManager` -- atomic file-based backup (write temp -> fsync -> rename). Integrity verification with SHA-256 checksums.
- `arcs.rs`: `ArcStore` -- git-branch-like workflow containers replacing sessions. Types:
  - `ArcMeta`: id, name, source (`ArcSource`: UserInput/Email/Calendar/Messaging/System), status (`ArcStatus`: Active/Archived/Merged), parent_arc_id, merged_into_arc_id, created_at, updated_at
  - `ArcEntry`: id, arc_id, entry_type (`EntryType`: Message/ToolCall/EmailEvent/CalendarEvent/SystemEvent/Summary), source, content, metadata (JSON), created_at, turn_id. The `Summary` variant is reserved for arc compaction (`docs/ARC_COMPACTION.md`); written by the compactor, never by the agent loop directly.
  - `ArcMeta` includes `summarized_through_entry_id: Option<i64>` — points at the largest `arc_entries.id` covered by the latest compaction summary; `None` means "never compacted." Set by the compactor in the same SQLite transaction that writes the summary entry, so restart cannot lose compaction state.
  Methods:
  - `create_arc(name, source)` -- creates a new arc
  - `create_arc_with_parent(name, source, parent_id)` -- creates a branched child arc
  - `list_arcs()` -- returns all non-merged arcs ordered by `updated_at` DESC
  - `rename_arc(id, name)` -- renames an arc
  - `delete_arc(id)` -- deletes arc and all its entries
  - `archive_arc(id)` -- sets status to Archived
  - `merge_arc(source_id, target_id)` -- moves all entries from source to target arc, marks source as Merged with `merged_into_arc_id`
  - `add_entry(arc_id, entry_type, source, content, metadata)` -- adds an interaction entry
  - `load_entries(arc_id)` -- returns all entries ordered by creation time
  - `migrate_from_chat_tables()` -- auto-migrates legacy chat_sessions/chat_messages to arcs on first startup
  Schema: `arcs` (id PK, name, source, status, parent_arc_id, merged_into_arc_id, created_at, updated_at) + `arc_entries` (id PK, arc_id FK, entry_type, source, content, metadata, created_at). 10 tests.
- `calendar.rs`: `CalendarStore` (Clone) -- Athen's native internal calendar system. 19 tests. Types:
  - `CalendarEvent`: 15 fields (id, title, description, start_time, end_time, all_day, location, recurrence, reminder_minutes, color, category, created_by, arc_id, created_at, updated_at)
  - `EventCreator` (User/Agent), `Recurrence` (Daily/Weekly/Monthly/Yearly), `FiredReminder`
  Methods: full CRUD -- `create_event`, `update_event`, `delete_event`, `get_event`, `list_events` (overlap range query using `datetime()` for proper timezone handling), `list_all_events`, `get_upcoming_events`, `get_events_by_category`.
  Reminder tracking: `record_fired_reminder`, `is_reminder_fired`, `clear_old_fired_reminders`.
  `normalize_to_utc()` function normalizes any ISO 8601 timestamp (with offset like `+02:00`, bare, or `Z`) to UTC before storage. Applied in both `create_event` and `update_event`. Ensures consistent storage regardless of whether the frontend (sends UTC `Z`) or agent (may send local offset) creates the event.
  Schema: `calendar_events` table + `fired_reminders` table (composite PK).
- `contacts.rs`: `SqliteContactStore` (Clone) implementing `ContactStore` trait. Two tables: `contacts` (10 columns) + `contact_identifiers` (4 columns, UNIQUE(identifier, kind), ON DELETE CASCADE). Full CRUD with UPSERT transactions. 14 tests. Provides production persistence for contacts (previously only `InMemoryContactStore` existed for tests).
- `chat.rs`: `ChatStore` -- legacy SQLite-backed chat history. Still exists for backward compatibility but no longer used by the app. Auto-migrated to arcs on first startup.
  Schema: `chat_messages` (id, session_id, role, content, content_type, created_at) + `chat_sessions` (session_id PK, name, created_at, updated_at).
- `notifications.rs`: `NotificationStore` (Clone) -- SQLite-backed notification persistence. Schema: `notifications` table (id TEXT PK, urgency, title, body, origin, arc_id, task_id, requires_response, is_read, created_at, updated_at). Full CRUD: `save`, `load`, `list_all`, `list_unread`, `mark_read`, `mark_all_read`, `mark_arc_read`, `delete`, `delete_read`, `unread_count`. 9 tests.
- `grants.rs`: `GrantStore` -- SQLite-backed directory grant persistence. Types: `Access` (Read/Write), `GrantScope` (Arc/Global), `DirectoryGrant` (id, scope, path, access, granted_at). Full CRUD: `grant_arc`, `grant_global`, `list_arc_grants`, `list_global_grants`, `revoke`, `check` (lookup grant). System paths never writable by design. Wired by `FileGate` in athen-app for agent path permission checking. Schema: `grants` table (id PK auto, scope, arc_id, path, access, granted_at).
- `mcp.rs`: `McpStore` -- SQLite-backed MCP configuration persistence. Two tables: `mcp_enabled (mcp_id PK, config JSON, enabled_at)` tracks which entries are currently active, and `mcp_custom_entries (id PK, definition JSON, enabled_at)` stores the full user-defined entry definitions for BYO (since they're not in the static catalog). On startup `restore_enabled_mcps` resolves enabled rows against the bundled catalog first, then the custom-entries table. CRUD: `enable`, `disable`, `list_enabled`, `get`, plus `add_custom`, `remove_custom`, `list_custom`, `get_custom`. Wired by `McpRegistry` in athen-mcp.
- `wakeups.rs`: `SqliteWakeupStore` implementing `WakeupStore`. Single `wakeups` table — schedule, origin, allowlists, preferred_channel are JSON columns (none queried in v1). One partial index `idx_wakeups_next_fire` on `(next_fire_at)` filtered by `enabled = 1 AND next_fire_at IS NOT NULL`, used by `list_due`. `create` rejects duplicates by id with a clear message; `update` errors when missing; `delete`, `mark_fired`, `set_enabled` all error on missing id. RFC3339 datetime serialization with second precision. Wired into `Database::run_migrations` via `wakeup_store().init_schema()`. 15 tests covering: full-fields round-trip, duplicate rejection, update-replace + missing, delete + missing, list_all order (created_at desc), list_due filters (enabled + next_fire_at present + due), mark_fired (with and without next), set_enabled toggle + missing, agent-origin round-trip, interval schedule round-trip, allowlists optional + round-trip.
- `telegram_chat_log.rs`: `TelegramChatLogStore` — per-`chat_id` Telegram transcript (inbound + outbound), cross-arc. Single `telegram_chat_log` table (id PK auto, chat_id, direction, text, has_attachments, ts) with index on `(chat_id, ts)`. Inbound writes happen in `state.rs::execute_owner_telegram_message` right after the routing decision; outbound writes happen in `email_gate.rs::ArcAwareTelegramOutboundRecorder` after every successful `send_telegram`. `recent(chat_id, n)` returns up to N rows ordered oldest → newest (chronological transcript). `append` enforces per-`chat_id` retention at 200 rows via a `DELETE WHERE id NOT IN (LIMIT 200)` follow-up — bounded storage, no separate GC task. Read path used by the owner-Telegram handler to inject the last 4 messages as a `<CONTEXT type="telegram-chat-history">` system bubble, **only when arc routing creates a fresh arc** (`!arc_was_reused`); reused arcs already loaded their own history. 4 tests covering chronological order, chat_id scoping, retention cap, and limit cap. **TODO**: gate injection behind prompt-size modes when those ship — small-context-window models shouldn't pay the ~300-500 token cost.
- `profiles.rs`: `SqliteProfileStore` implementing `ProfileStore`. Tables: `agent_profiles` (id PK string, display_name, description, persona_template_ids JSON, custom_persona_addendum, tool_selection JSON, expertise JSON, model_profile_hint, builtin, created_at, updated_at), `persona_templates` (id PK, display_name, category, body, builtin, created_at), and `arc_profiles` (arc_id PK, profile_id) for per-arc profile assignment. `seed_builtins_if_empty(now)` is **per-id idempotent** — iterates `builtin_profiles(now)` (12 entries: default, assistant, coder, devops, systems_architect, technical_support, researcher, marketing, social_media, outreach, lawyer, doctor) and inserts only missing ids, so user edits to a built-in survive future launches. `save_profile` is relaxed to **preserve the existing row's `builtin` flag and `created_at`** instead of rejecting writes to built-ins — caller's flag is ignored when a row already exists. `restore_builtin(id)` looks up `canonical_builtin_profile(id)` and rewrites via the same path, reverting any user edits. Built-in personas are framed for non-experts: doctor + lawyer carry research-oriented disclaimers (LLM safety layer enforces actual limits); social_media is platform-native (LinkedIn/TikTok/Instagram/X) distinct from broader marketing. All built-ins ship with `ToolSelection::All` — persona drives behavior, not tool restriction. Tests cover seed-all-canonical, additive-when-some-exist, edit-preserves-builtin-flag, restore-reverts-edits, restore-rejects-unknown-id.
- Schema: `tasks`, `task_steps`, `checkpoints`, `pending_messages`, `arcs`, `arc_entries`, `calendar_events`, `fired_reminders`, `contacts`, `contact_identifiers`, `notifications`, `grants`, `enabled_mcps`, `agent_profiles`, `persona_templates`, `arc_profiles`, `wakeups`, `chat_messages` (legacy), `chat_sessions` (legacy) tables.

### athen-scheduler (23 tests)
**Status**: Phase 2 complete — pure `compute_next_fire` + tickwise `WakeupScheduler` driver. Wired in `athen-app` later (Phase 3).
- `compute.rs`: `compute_next_fire(schedule, after) -> Option<DateTime<Utc>>` — pure, strict-greater-than-`after` semantics. `OneShot { at }`: `Some(at)` iff `at > after`. `Cron { expr, tz }`: parses `expr` as 5-field cron (auto-prepends `0 ` for seconds when 6/7-field passes through), interprets in `tz` via `chrono-tz::Tz`, returns next slot strictly after `after` projected to UTC. `Interval { every_seconds, anchor }`: future anchor returns the anchor itself; past anchor advances to the next grid point strictly after `after`. Returns `None` for terminal/invalid states (one-shot past, unparseable cron, unknown tz, `every_seconds == 0`). 16 tests covering future/past/exact-match for all three kinds, named-tz, DST spring-forward (Madrid 2026-03-29), strict-inequality at exact grid points, and "skip 100 missed slots" coalescing.
- `scheduler.rs`: `WakeupScheduler<S, K>` generic over `WakeupStore` + `WakeupFireSink`. `tick(now)` drains `list_due(now)`, fires each via the sink, and writes `mark_fired(id, now, compute_next_fire(schedule, now))` — passing `now` (not the missed `next_fire_at`) is what implements per-schedule coalescing of missed slots. `arm_unscheduled(now)` fills in `next_fire_at` for enabled wake-ups whose `next_fire_at` is `None` (freshly created); idempotent. `run(period, shutdown)` async loop using `tokio::time::interval` with `MissedTickBehavior::Delay`. Sink errors are logged and the row is still marked fired so a broken sink can't busy-loop. 7 tests with mock store + recording sink + failing sink covering: fire+advance, interval coalescing across 99 missed slots, skip disabled, mark-fired-on-sink-error, arm fresh rows, skip already-armed, fire ordering by next_fire_at.
- `lib.rs`: re-exports `compute_next_fire`, `WakeupScheduler`, `TickReport`.

### athen-web (23 tests)
**Status**: Complete — search and page-reader providers behind `WebSearchProvider` / `PageReader` traits. Bundled no-key defaults, optional key-gated upgrades, **quota-aware multi-provider failover** for search. Wired into `ShellToolRegistry` as the `web_search` and `web_fetch` tools.
- `lib.rs`: re-exports + `default_http_client()` (User-Agent set, 30s timeout) for adapters that don't bring their own `reqwest::Client`. Re-exports `BraveSearch`, `MultiSearchProvider`, `ProviderSlot` alongside the existing search/reader types.
- `search/mod.rs`: `WebSearchProvider` trait (`search`, `name`, `last_used`) + `SearchResult { title, url, snippet }`. `last_used` defaults to `name`; wrappers like `MultiSearchProvider` override it to report which underlying provider actually answered the most recent call so the agent's tool output (`answered_by` field) and logs can attribute hits without context-window waste.
- `search/duckduckgo.rs`: `DuckDuckGoSearch` — POSTs `html.duckduckgo.com/html/`, parses results with `scraper` CSS selectors, unwraps the `/l/?uddg=...` redirect wrapper so agents see real URLs. HTTP 202 surfaces as a clear "rate-limited" error. Tolerant parser: missing fields fall back to empty strings rather than failing the whole query.
- `search/tavily.rs`: `TavilySearch` — POST `api.tavily.com/search` with `api_key` in body, max_results enforced. ~1k req/month free tier.
- `search/brave.rs`: `BraveSearch` — GET `api.search.brave.com/res/v1/web/search` with `X-Subscription-Token` header, parses `web.results[].{title,url,description}`. 2k queries/month free tier (no card).
- `search/multi.rs`: `MultiSearchProvider` — quota-aware fan-out across a priority-ordered `Vec<ProviderSlot>`. `ProviderSlot::keyed(...)` participates in cooldowns; `ProviderSlot::floor(...)` never cools down (DDG slot, so the chain always has *something* to fall back to). On error, `classify_error` heuristically tags the failure as quota (HTTP 402/403, "quota"/"exceeded"/"subscription" in the message) → 24h cooldown, or rate-limit (HTTP 429, "rate limit"/"too many requests") → 15min cooldown. Other errors (network/JSON/5xx) leave the provider armed. Cooldowns are in-memory only — on restart we retry every provider once and rediscover its state from the response, trading a few wasted calls for not having to track each provider's clock. Tracks the answering provider in an `AtomicUsize` so `last_used()` is lock-free. Per-call attempts logged at `info!` (`trying provider=...`, `provider answered`, `provider failed (no cooldown), trying next`, `skipping (in cooldown)`) so chain order is visible at default `RUST_LOG=info` without enabling debug. 8 unit tests (first-wins, rate-limit fallthrough + cooldown, quota exhaustion, floor never cools, last_used reporting, returns last error when all fail, classifier coverage).
- `reader/mod.rs`: `PageReader` trait (`fetch`, `name`) + `ReadResult { url, title, content, source }`. The `source` string identifies the tier (e.g. `"local-html"`, `"jina"`, `"wayback"`, `"cloudflare"`).
- `reader/local.rs`: `LocalReader` — plain reqwest GET with `Accept: text/markdown, text/html;q=0.9, */*;q=0.5`. Cloudflare-opted-in origins return clean markdown for free; HTML responses go through `strip_noise` (script/style blocks removed via case-insensitive scan) → `html2md::parse_html`. Title extracted via tiny regex-free `<title>` scan. UTF-8 safe `truncate_chars` cap at 40k. 5MB body cap.
- `reader/jina.rs`: `JinaReader` — `https://r.jina.ai/<url>` with `Accept: text/markdown` and `X-Return-Format: markdown` headers. Strips Jina's `Title: ... URL Source: ... Markdown Content:` prelude into structured fields. Optional bearer auth.
- `reader/wayback.rs`: `WaybackReader` — composes `LocalReader` against `https://web.archive.org/web/2id_/<url>`. The `2id_` modifier strips Wayback's nav banner so we get raw archived HTML.
- `reader/cloudflare.rs`: `CloudflareReader` — `POST api.cloudflare.com/.../browser-rendering/markdown` with bearer-auth on a CF API token. Built but not wired by default (paid).
- `reader/hybrid.rs`: `HybridReader` — chains `primary → js_fallback → archive_fallback`. Default chain: `LocalReader → JinaReader → WaybackReader`. `looks_empty` heuristic: hard floor 150 chars, soft band 150–800 only triggers fallback when JS-required markers present. Genuinely small static pages (`example.com` ~190 chars) pass through; SPA stubs and JS-warning pages fall back. Each tier failure logs at debug level so the chain decision is auditable. Tests: 4 unit (split header, SPA detection edge cases).
- Tests: 23 unit (DDG redirect unwrap, jina header split, local title/strip-noise/truncate, hybrid heuristic edge cases, multi-provider failover/cooldown/floor/last_used/classifier). No live-network tests in CI; live smoke runs via `cargo run -p athen-web --example smoke`.

### athen-agent (56 unit + 3 integration = 59 tests)
**Cursor-style per-tool error hints (#216, 2026-05-11)**: `tool_error_hints.rs::hint_for(tool_name)` returns a one-line common-misuse policy for 20+ tools (file primitives, shell, http_request, memory, calendar, send_telegram, email_send, web_*, install_package, contacts_*, identity_add, create_wakeup). `maybe_append_hint(body, tool_name, tool_error)` wraps a failing tool's body with a `<system-reminder>` carrying the hint — episodic anchoring so the model sees the error + the rule in the same context window. Skipped when `tool_error == "loop_guard"` or `"duplicate_in_batch"` (those carry their own purposeful policy text). Wired in the executor's tool-result push (~line 1960); truncation runs BEFORE hint append. 8 unit tests (known-tool coverage, length bounds 40–600 chars, skip behavior).

**Anti-drift system-reminder injection (#216, 2026-05-11)**: `reminders.rs::ProfileSystemReminderBuilder` distills active profile + post-filter tool list + identity excerpt into a stable ~150-token body at construction time. `build()` returns the same pre-baked body on iterations 3, 6, 9, … (`REMINDER_EVERY_N_ITERATIONS`, period clamped to ≥1) and `None` otherwise — skips iteration 0 because the static system prompt is already fresh. Body shape: `Profile: <display_name>. Tools you can call right now: <comma-separated, sorted, capped at 24 with ", …">. Hard rules: <3 generic anti-drift lines>. Persona: <Constraints templates first, Voice fallback, ≤240 chars>. Identity: <non-divider lines from identity_block, ≤240 chars, UTF-8-safe char clip>.` Wrapped by `wrap_reminder()` into `<system-reminder>\n...\n</system-reminder>` before injection. Wired through `DefaultExecutor::reminder_builder` (custom impl, takes precedence) + `auto_reminders: bool` (default `false`; flips on via `enable_default_reminders(true)` on the builder). When enabled, `execute()` constructs the default builder lazily after `available_tools` is filtered, so the reminder reflects the *actual* post-`ToolSelection` surface, not the registry total. Lives in the dynamic suffix — never invalidates the cached static prefix. `build_workspace_rules` adds one sentence declaring the `<system-reminder>` tag contract so the model treats them as authoritative. All 5 athen-app construction sites (`state.rs:2716`, `commands.rs:2091/3129/4112`, `delegation.rs:367`) now opt in. 13 unit tests for the builder. Distinct from `external_system_suffix` (memory/attachment scaffolding inside the leading system message) and the `<CONTEXT>` user-turn wrapper (per-turn volatile state) — reminders are mid-conversation re-anchors, the other two are static-prefix or first-turn bundling.


**Status**: Complete -- LLM-driven task execution with real tool calling, streaming responses (including thinking content forwarding), cancellation, tool-level risk checking, sandbox integration, session memory, LLM completion judge, graceful max-steps handling, streaming tool calls, <think> tag parsing, 2-tier tool discovery, batch tool calls, and loop protection.
- `executor.rs`: `DefaultExecutor` implementing `AgentExecutor`. Fields include optional `stream_sender: Option<mpsc::UnboundedSender<String>>` for progressive streaming and `cancel_flag: Option<Arc<AtomicBool>>` for user-initiated cancellation. Accepts optional `context_messages: Vec<ChatMessage>` prepended to conversation for session-level memory. System prompt ("You are Athen, an AI agent that ACTS first and talks second") is conversation-aware: when context messages are present, it tells the LLM "You are in an ongoing conversation". **System prompt includes current date/time and timezone** (e.g. "Current date and time: Monday, April 6, 2026 at 11:46 (CEST, UTC+02:00)") via `chrono::Local::now()`. **Calendar guidance**: tells the agent to use the local timezone offset (e.g. `2026-04-06T12:15:00+02:00`) instead of UTC `Z`, with the detected offset included dynamically. **2-tier tool discovery**: system prompt includes only one-line group summaries (e.g. "calendar_list, calendar_create, calendar_update, calendar_delete (4 calendar tools)"). Full tool schemas are not in prompt; agent discovers them on-demand by name via `get_tool_details` or reads `~/.athen/tools/*.md` files per group. Includes numbered rules: (1) never say "I'll do X" -- just do it, (2) never ask what to do -- take initiative, (3) call tools immediately, (4) only text when task is complete, (5) be concise, (6) make reasonable choices, (8) ALWAYS respond in natural language, NEVER output raw JSON (with BAD/GOOD examples). **MEMORY & KNOWLEDGE guidance section**: tells the agent to check memory/contacts when people or past interactions are mentioned. BAD/GOOD examples included (English + Spanish variants). **Loop protection**: tracks max consecutive tool-only steps with no text output; if exceeded, injects nudge to produce user-visible progress. Loop: check cancel_flag -> check timeout -> check max_steps -> build LlmRequest with small tool list -> call LlmRouter (streaming or non-streaming) -> **batch tool calls** (execute all tools in parallel, not sequentially) -> check cancel_flag between batches -> record steps via StepAuditor -> repeat until LLM says done. Tool call results fed back as `Role::Tool` messages with `tool_call_id` for OpenAI-compatible APIs. **Tool tracking**: `tools_called: Vec<String>` tracks which specific tools were called during execution.
  - **Streaming**: when `stream_sender` is set, uses `try_streaming_call()` which calls `LlmRouter::route_streaming()`, collects text deltas, and forwards each chunk through the sender. If streaming returns empty content (tool call response), falls back to non-streaming `route()` to get tool call data. **Streaming tool calls**: supports `tool_calls` arriving via streaming chunks. **Thinking content**: forwards `is_thinking` chunks and `<think>` tag content to the stream sender for frontend display. **Graceful fallback**: when streaming succeeds but non-streaming fallback errors, returns empty response instead of propagating the error.
  - **`StreamResult` struct**: returned from `try_streaming_call()`, contains collected content, tool calls, and reasoning content.
  - **`extract_think_tags()`**: parses `<think>...</think>` blocks from model responses (e.g. DeepSeek R1), extracts thinking text separately from content.
  - **`clean_model_response()`**: strips thinking tags from final displayed response, extracts text from JSON wrappers like `{"response": "text"}`, replaces empty responses with default message.
  - **Cancellation**: `cancel_flag` is checked at loop start and between each tool batch. When set to `true`, returns immediately with `{ reason: "cancelled", response: "Task cancelled by user." }`.
  - **LLM Completion Judge**: replaces the regex-based anti-lazy nudge system. `judge_completion()` is a lightweight LLM call (Cheap profile, 5 tokens, 30s timeout) that evaluates whether the agent actually completed the user's request. Checks the user's request, agent's response, and list of tools called. Returns DONE or CONTINUE. Language-agnostic (works for English, Spanish, any language). Fires at most once per execution. Defaults to DONE on failure/timeout.
  - **Graceful max-steps**: when the executor hits the step limit, it makes one final LLM call with `tools: None` asking for a summary of work accomplished, instead of returning raw JSON.
- `tools.rs`: `ShellToolRegistry` implementing `ToolRegistry` with 16 built-in tools. Provides `ShellExtraWritableProvider` trait for per-arc directory grant discovery (wired by `file_gate.rs`) and `ToolboxApprovalGate` trait for gating `install_package`:
  - `shell_execute` -- runs shell commands with **2-layer safety**: (1) pre-execution risk check via `RuleEngine.evaluate()` on the actual command string -- blocks Danger/Critical commands with a descriptive error returned to the LLM, (2) sandboxed execution via bwrap with `SandboxProfile::RestrictedWrite` (writable: `/tmp`, `$HOME`, cwd, + per-arc granted paths; read-only: everything else). Graceful fallback to unsandboxed if bwrap not installed. Tool description forbids using curl/wget/lynx for web content — agent should use `web_fetch`/`web_search` instead.
  - `shell_spawn` / `shell_kill` / `shell_logs` -- detached process management with PID tracking, log-file capture, and SIGTERM-then-SIGKILL teardown. Only kills PIDs in the registry's tracked map.
  - `read` -- file reads with `cat -n`-style line numbers, optional `offset`/`limit`, NUL-byte binary detection. Workspace-relative path resolution via `paths::resolve_in_workspace`.
  - `edit` -- exact-string replace, requires a prior `read` of the file to enforce read-before-modify and detect external changes. Atomic write (sibling tmp + rename, fallback to direct write on cross-FS rename).
  - `write` -- full overwrite. Existing files require a prior `read`; new files don't. Atomic.
  - `grep` -- ripgrep wrapper.
  - `list_directory` -- lists directory entries as JSON array.
  - `memory_store` / `memory_recall` -- in-session `HashMap<String, String>` (overridden to persistent semantic memory by `AppToolRegistry`).
  - `web_search` -- delegates to `Arc<dyn WebSearchProvider>` (production default in `athen-app::state`: `MultiSearchProvider` with Brave → Tavily → DDG-floor when keys are configured, plain `DuckDuckGoSearch` when none are; swap via `with_web_search`). Returns `{ provider, answered_by, query, results: [{title, url, snippet}] }` — `provider` is the wrapper name (`"multi"` or `"duckduckgo"`), `answered_by` is the underlying provider that actually handled the call.
  - `web_fetch` -- delegates to `Arc<dyn PageReader>` (default `HybridReader` chaining Local → Jina → Wayback, swap via `with_page_reader`). Returns `{ url, title, content, source, content_chars }`. Rejects non-http(s) URLs early without network round-trip.
  - `install_package` / `uninstall_package` / `list_installed_packages` -- persistent toolbox at `~/.athen/toolbox/` for pip/npm dependencies the agent decides it needs. `install_package` is approval-gated via `ToolboxApprovalGate` (production binding `RouterToolboxApprovalGate` routes through `ApprovalRouter` -> in-app dialog + Telegram escalation). `uninstall` and `list` are unrestricted (reversible / read). Manifest at `~/.athen/toolbox/manifest.json` tracks `{runtime, package, version_spec, installed_version, reason, installed_at, runtime_version}`. `shell_execute` wraps every command so `PYTHONPATH=<toolbox/python>:<existing>` and `PATH=<toolbox/node/bin>:<PATH>` are exported transparently. See `toolbox.rs` and the Toolbox section in `docs/TOOLS_AND_SENSES.md`.
  Each tool has proper JSON Schema parameter definitions for LLM tool calling. `ShellToolRegistry` holds an optional `UnifiedSandbox` (from `athen-sandbox`), a `RuleEngine` (from `athen-risk`), the spawn-tracking map, the web search/page reader providers, and the optional toolbox approval gate — all auto-initialized on construction.
- `toolbox.rs`: persistent shell-tool memory. `Runtime::{Python, Node}`, `InstalledPackage`, `ToolboxManifest` (atomic save), `RuntimeProbe` (`python`/`pip`/`node`/`npm` detection with 5s timeouts, cached in a process-scoped `Mutex<Option<RuntimeProbe>>` so `invalidate_runtime_probe_cache()` can reset after a wizard install), `ToolboxPromptInfo` (probe + manifest summary injected into the executor system prompt). `install_python_package`/`install_node_package` shell out to `pip install --target=<dir>` / `npm install --prefix=<dir>`. `uninstall_python_package` walks `dist-info/RECORD` (PEP 503-normalized) since `pip uninstall` does not support `--target`; `uninstall_node_package` uses `npm uninstall --prefix`. `clear_toolbox` wipes the directory and manifest. **Windows alias filtering**: `resolve_executable` runs `where.exe` and skips any hit under `\Microsoft\WindowsApps\` (where the App Execution Alias for `python.exe`/`python3.exe` lives — opens the Microsoft Store on activation rather than running anything). Belt-and-suspenders: any `--version` output mentioning "Microsoft Store" or "was not found" is rejected.
- `runtimes.rs`: portable Python/Node installer for the onboarding wizard. Pinned versions (single hardcoded source of truth, no manifest file per the no-config-files rule): Python `3.12.7` from python-build-standalone `20241016` `install_only` archives (uniform tar.gz across all OS, includes pip), Node `22.11.0` from `nodejs.org/dist` (tar.gz Unix, zip Windows). `RuntimeKind` enum, `PortableRuntimeRecord`, `RuntimesManifest`, `InstallProgress` enum (Resolving/Downloading{downloaded,total}/Verifying/Extracting/Done), `ProgressCb = Arc<dyn Fn(InstallProgress) + Send + Sync>`. `install_runtime(kind, progress)` is serialised through a process-wide `INSTALL_LOCK: Mutex<()>` so a fast double-click in the wizard doesn't race two extracts into the same dir. Verifies SHA-256 against the published sidecar (`.sha256` for python-build-standalone, `SHASUMS256.txt` for Node). After successful install: `init_portable_path()` re-runs and `toolbox::invalidate_runtime_probe_cache()` is called. `init_portable_path()` runs once at app startup (`athen-app/src/lib.rs`) and prepends portable bin dirs to the process `PATH` (Windows: `<runtimes>/python/` + `<runtimes>/python/Scripts/` + `<runtimes>/node/`; Unix: `<runtimes>/python/bin/` + `<runtimes>/node/bin/`). Idempotent on repeated calls. Tauri surface: `get_runtime_status` and `install_runtime` (streams `runtime-install-progress` events). 6 unit tests.
- `tool_grouping.rs`: Helpers for 2-tier tool discovery. `group_for(name)` extracts the prefix (e.g. "memory", "calendar", "files", "web" from tool names). `summarize_groups()` returns one-line summary per group for the system prompt. `is_always_revealed` covers memory + shell + file primitives + the web tools (`web_search`, `web_fetch`) so their full schemas are inline every turn — without this, small models default to whatever IS revealed (typically `shell_execute`) and use `curl` instead of the dedicated web tools.
- `tools_doc.rs`: Generates per-group markdown references. Writes to configured directory (typically `~/.athen/tools/`), one `.md` file per group. Agent reads only the group it needs via `read_file`, avoiding context bloat. Replaces monolithic TOOLS.md with on-demand modular loading.
- `tool_truncation.rs`: centralised tool-output capping applied at the executor's tool-result serialisation point (`executor.rs:1691–1694`). `policy_for(name)` returns one of `TruncationPolicy::None | Chars { max } | HeadTail { head, tail }`; UTF-8 boundary-safe slicing; explicit `[TRUNCATED: …]` markers so the model knows to refine. Per-tool table (`shell_execute` HeadTail 8K+4K, `read` 40K, `grep` 20K, `web_fetch` 20K, fallback 20K, etc.) is hardcoded — not user-configurable. The audit trail keeps the full untruncated result; only the model-visible bytes are capped. 8 tests. Independent of arc compaction (different layer). Documented in `docs/TOOLS_AND_SENSES.md` §1.
- `auditor.rs`: `InMemoryAuditor` implementing `StepAuditor` with `tokio::sync::Mutex<HashMap<TaskId, Vec<TaskStep>>>`.
- `timeout.rs`: `DefaultTimeoutGuard` -- sets deadline at Instant::now() + duration.
- `resource.rs`: `DefaultResourceMonitor` -- reads `/proc/self/statm` on Linux for resident memory. `AtomicBool` cache for within-limits state.
- `lib.rs`: `AgentBuilder` with fluent API. `.context_messages(Vec<ChatMessage>)` sets prior conversation history for session memory. `.stream_sender(UnboundedSender<String>)` enables streaming text forwarding. `.cancel_flag(Arc<AtomicBool>)` enables user-initiated cancellation. `.tool_doc_path(PathBuf)` sets the directory where tool markdown files are written. Defaults: 50 max_steps, 5-minute timeout, InMemoryAuditor.
- Integration tests: mock LLM returns tool call -> real `ShellToolRegistry` executes -> result fed back -> LLM completes. Batch tool calls tested (multiple tools in parallel).

### athen-contacts (15 tests)
**Status**: Complete -- trust management with implicit learning. Production persistence via `SqliteContactStore` in athen-persistence (see above).
- `lib.rs`: `ContactStore` trait (save/load/find_by_identifier/list_all/delete) + `InMemoryContactStore` for testing.
- `trust.rs`: `TrustManager` with:
  - `resolve_contact()` -- finds existing or creates T0 Unknown
  - `risk_multiplier()` -- delegates to TrustLevel, returns 5.0x for blocked
  - `record_approval()` -- every 5 approvals upgrades T0->T1->T2 (never past T2, never if manual override)
  - `record_rejection()` -- tracks count in notes JSON, every 3 rejections downgrades T2->T1->T0 (never if manual override)
  - `set_trust_level()` -- sets level + trust_manual_override flag
  - `block_contact()`, `is_blocked()`, `list_contacts()`, `find_by_identifier()`

### athen-llm (110 unit + 0 integration = 110 tests)
**Status**: Complete -- router with streaming + Anthropic/DeepSeek/OpenAI-compatible providers + Ollama/llama.cpp wrappers + embedding providers with auto-detection. Vision Phase 1 shipped: provider trait gained `supports_vision()`; Anthropic + OpenAI proper serialise `MessageContent::Multimodal { text, images }` into provider-native content blocks. Non-vision providers (DeepSeek, Ollama, llama.cpp) call `providers::reject_multimodal()` at the top of `complete`/`complete_streaming` so attached images surface a clear error instead of being silently dropped on the wire.
- `budget.rs`: `BudgetTracker` -- daily USD limit, token counting, midnight UTC reset, `can_afford()`, `record_usage()`, `status()`, `is_warning()`. Zero budget always rejected.
- `router.rs`: `CircuitBreaker` (Closed/Open/HalfOpen state machine, configurable thresholds/timeout). `DefaultLlmRouter` implementing `LlmRouter` -- profile-based routing, failover chains, circuit breakers per provider, budget enforcement. Implements both `route()` and `route_streaming()` with independent failover methods (`route_with_failover`, `route_streaming_with_failover`) -- each tries providers in priority order, respects circuit breakers, and records success/failure.
- `providers/mod.rs`: Provider-shared helpers including `reject_multimodal(provider_id, request)` — returns a clear LlmProvider error if any user message in the request is `MessageContent::Multimodal` with non-empty images. Used by every adapter whose configured model can't accept vision content.
- `providers/openai.rs`: `OpenAiCompatibleProvider` -- fully generic adapter for any OpenAI-compatible API endpoint. Builder pattern: `new(base_url)`, `with_api_key()`, `with_model()`, `with_provider_id()`, `with_client()`, `with_cost_estimator()`, `with_vision(bool)`. Convenience constructor: `openai(api_key)` for OpenAI proper. Features:
  - API key is optional -- `Authorization: Bearer` header only sent when key is present (supports local servers without auth)
  - Full tool calling support with proper wire format conversion (assistant messages with tool_calls, tool result messages with tool_call_id)
  - SSE streaming via `complete_streaming()` -- parses `data:` lines, handles `[DONE]`, extracts content deltas, finish_reason, **`reasoning_content`** (thinking models), and **`tool_calls`** from streaming chunks
  - Non-streaming `complete()` extracts `message.reasoning_content` into `LlmResponse.reasoning_content`
  - `OpenAiRequestOut` has `extra: Option<Value>` field with `#[serde(flatten)]` for provider-specific parameters
  - `CostEstimator` trait for pluggable pricing: `OpenAiCostEstimator` (gpt-4o, gpt-4o-mini, o3, etc.), `ZeroCostEstimator` (for local providers)
  - `parse_sse_chunks()` public function for reuse by wrapper providers
- `providers/deepseek.rs`: Full `DeepSeekProvider` -- OpenAI-compatible API at `api.deepseek.com/v1/chat/completions`. Bearer auth, request/response mapping, SSE streaming, tool call support. Cost estimation: deepseek-chat ($0.14/M input, $0.28/M output), deepseek-reasoner ($0.55/$2.19). Builder pattern: `new(api_key)`, `with_model()`, `with_base_url()`.
- `providers/anthropic.rs`: Full `AnthropicProvider` -- POST to `/v1/messages`, proper headers (x-api-key, anthropic-version), request/response mapping, SSE streaming, cost estimation for opus/sonnet/haiku tiers. `with_vision(bool)` toggles `supports_vision()`; `anthropic_multimodal_blocks()` serialises `MessageContent::Multimodal` into Claude's `[{type:text}, {type:image, source:{type:base64|url, ...}}]` content array.
- `providers/ollama.rs`: `OllamaProvider` -- thin wrapper around `OpenAiCompatibleProvider` for local Ollama inference. Default URL `http://localhost:11434`, zero-cost estimation, delegates all LLM logic to inner provider. Real health check via `GET /api/tags` (returns model count). Builder: `new(model)`, `with_base_url()`, `with_model()`.
- `providers/llamacpp.rs`: `LlamaCppProvider` -- thin wrapper around `OpenAiCompatibleProvider` for llama.cpp's `llama-server`. Default URL `http://localhost:8080`, zero-cost estimation. Real health check via `GET /health`. Constructor: `new(base_url, model)`, `localhost(model)`.
- `providers/google.rs`: Stub returning "not yet implemented".
- `embeddings/`: Full embedding provider system integrated with `athen-memory` for semantic search and knowledge graph grounding.
  - `embeddings/ollama.rs`: `OllamaEmbedding` -- uses Ollama's `/api/embed` endpoint, auto-detects dimensions, batch support, model presence check via `/api/tags`. Builder: `new(model)`, `with_base_url()`. 8 tests.
  - `embeddings/openai.rs`: `OpenAiEmbedding` -- uses `/v1/embeddings` endpoint, optional API key, known dimension lookup for OpenAI models (text-embedding-3-small: 1536, text-embedding-3-large: 3072, text-embedding-ada-002: 1536), works with any OpenAI-compatible endpoint. Constructors: `openai(api_key)`, `compatible(base_url)`. Builder: `with_model()`, `with_api_key()`. 14 tests.
  - `embeddings/keyword.rs`: `KeywordEmbedding` -- TF-IDF hash projection fallback using FNV-1a hashing, 384 dimensions, L2 normalized. Zero external dependencies, always available. 13 tests.
  - `embeddings/router.rs`: `EmbeddingRouter` -- auto-detection waterfall trying providers in priority order with keyword fallback always available. 5 tests.
- `quirks/`: per-model quirks subsystem (~46 tests across the module). 5 axes — `ToolExtractionStrategy` (Structured / InlineXmlQwenStyle / InlineXmlVendorTagged / InlineJsonLlama / InlinePythonicLlama / SpecialTokenBlock), `ReasoningSurface` (None / SeparateField / InlineThinkTags / NativeContentBlock / HiddenServerSide), `TemplateStrictness` (Lenient / SystemMustBeFirst / SystemAbsorbedIntoUser), `ToolArgRepair` (control-char escape, double-encoded JSON arrays), and sub-flags (`echo_reasoning_on_tool_turn`, `system_message_required`).
  - `quirks/mod.rs`: `ModelQuirks` struct, `apply_to_response` (post-parse hook in non-streaming path), `extract_streaming_tail` (end-of-stream extractor for streaming path). Critical fix at lines 213–221: when `content` is empty AND `tool_calls` empty, scan `reasoning_content` for inline tool-call markup — required for Qwen3.5/3.6 under llama.cpp `--jinja --reasoning-format` (see `feedback_quirks_scan_reasoning_content.md`).
  - `quirks/extractors.rs`: parsers for inline strategies — Hermes-JSON `<tool_call>{json}</tool_call>` (primary), Qwen-XML `<tool_call><function=N><parameter=K>V` (fallback), `<think>` strip.
  - `quirks/repair.rs`: `escape_control_chars` (descends into arrays + objects), `unescape_double_encoded_arrays`.
  - `quirks/seed.rs`: 23 family seeds — Claude Opus 4.7 / Sonnet 4.6 / Haiku 4.5, GPT-5 / o-series, Gemini 3 Pro, DeepSeek V4 (Chat / Pro / R1), Qwen 3.5 / 3.6 / Coder Next, Gemma 4, Kimi K2.6, MiniMax M2.5, Llama 3.2 / 3.3 / 4, Mistral Large 3 / Magistral / Codestral, Grok 4, Default. Family selection is user-driven via Settings dropdown (no auto-detect); slug field is independently editable so a same-wire-format model upgrade is a one-line change. Full design + per-vendor SKU registry in `docs/PER_MODEL_QUIRKS.md`.
  - **Streaming caveat**: end-of-stream extraction works, but raw `<tool_call>` markup still appears in the visible streaming deltas (can't retroactively edit chunks already sent). Frontend filter is the planned fix.

### athen-mcp (15 tests)
**Status**: Complete -- MCP runtime + BYO custom servers (#224 shipped 2026-05-12) + per-server/per-tool risk overrides (#225 shipped 2026-05-12). `rmcp` SDK with stdio child-process transport; lazy spawn, `initialize` handshake on first call. Per-tool risk: `McpCatalogEntry.tool_risks: HashMap<String, BaseImpact>` overrides the per-server `base_risk` default for individual tools. `list_tools` / `list_tools_for` stamp `t.tool_risks.get(name).unwrap_or(entry.base_risk)` on each `McpToolDescriptor.base_risk`, which `AppToolRegistry::list_tools` (app_tools.rs:2020) reads instead of hardcoded WritePersist. `McpRegistry::update_risks(mcp_id, default_risk, overrides)` mutates the in-memory entry without dropping the live client. Surfaced in Settings → MCP Servers → expand "Tools" pane: per-server default risk dropdown + per-tool dropdown + Save.
- `lib.rs`: Composition root. Exports `McpRegistry` and `EnabledEntry`.
- `catalog.rs`: `builtin_catalog()` -- hardcoded list of branded MCP entries (currently empty since the original `files` entry was retired in favor of built-in file tools). Stays as the extension point for future Athen-shipped MCPs. `lookup(id)` retrieves entry by ID.
- `registry.rs`: `McpRegistry` -- runtime state of enabled MCPs. Two constructors: `new()` (vault-less, only handles `Bundled` and `Process` entries without `EnvValue::Vault` bindings) and `new_with_vault(Arc<dyn Vault>)` (full BYO support). Lazy-spawns child processes via `TokioChildProcess`; lazy `Drop` kills the subprocess. `enable(id, config)` does the catalog lookup for bundled entries; **`enable_custom(entry, config)`** accepts a self-describing `McpCatalogEntry` (used by BYO). Spawn paths: `Bundled { binary_name }` resolves next-to-exe or PATH; `Process { command, args, env, working_dir }` uses `env_clear() + PATH reinject`, resolves each `EnvBinding` (`Plain` direct, `Vault { scope, key }` via `vault.get`), errors with an actionable message when a vault entry is missing. Windows: `creation_flags(0x0800_0000)` on both spawn paths to avoid the cmd flash. **`test_spawn(entry, config, vault)`** is a static helper used by the UI's "Test connection" button — spawns + handshakes + lists tools + drops, zero persisted state. **`list_tools_for(mcp_id)`** returns the tool list for a single server (for the expanded UI row). Implements `McpClient` trait. 12 tests covering: unknown-id rejection, plain/vault env resolution, missing-vault-entry error message, missing-vault error message, `enable_custom` cleanup on spawn failure, list_tools on empty registry, etc.

### athen-cli (0 tests)
**Status**: Complete -- working agentic CLI with tool execution.
- `main.rs`: REPL loop wiring all components end-to-end. Reads `DEEPSEEK_API_KEY` from env or config. Uses `config_loader` to discover config from `~/.athen/` or `./config/`. Creates `DeepSeekProvider` -> `DefaultLlmRouter` (mapped to all profiles) -> `CombinedRiskEvaluator` (real rule engine + LLM fallback) -> `Coordinator` (real router, queue, dispatcher). Synchronous stdin with clean EOF/Ctrl+D handling. Risk-gated: low-risk auto-approved, high-risk prompts for confirmation, hard-block rejected. Commands: `/quit`, `/exit`.
- Uses full `AgentBuilder` + `DefaultExecutor` with `ShellToolRegistry` -- the agent can execute shell commands (sandboxed when bwrap available), read/write files, list directories, and use in-session memory (store/recall) autonomously via LLM tool calls.
- `SharedRouter`: Wrapper around `Arc<DefaultLlmRouter>` implementing `LlmRouter` trait for sharing between risk evaluator and agent.
- **Verified working**: Full agentic pipeline -- user input -> coordinator -> risk -> dispatch -> agent executor -> LLM -> tool calls -> execution -> result.

### athen-app (85 tests)
**Wake-ups Phase 5 (agent-authored `create_wakeup`, complete)**: New `crates/athen-app/src/wakeup_tool.rs` ships `WakeupAuthoringRegistry` — a `ToolRegistry` wrapper that adds `create_wakeup` on top of any base. Composition: sits between `DelegationToolRegistry` (inner) and `WakeupRestrictedRegistry` (outer) in both the user-driven path (`state::build_tool_registry`) and the wake-up firing paths (`commands::execute_approved_task` + `execute_dispatched_task`). Tool args: `{instruction, schedule:{kind, at?|in?, every_seconds?, anchor?, expr?, tz?}, profile?, autonomy?, tool_allowlist?, contact_allowlist?, inherit_restrictions?, preferred_channel?, arc_id?}`. The `in` field accepts relative durations like `2h`, `90m`, `1d12h` — combinations of `d/h/m/s` units — so the agent doesn't have to do RFC3339 math. **Risk model — dynamic BaseImpact**: the static `ToolDefinition::base_risk` is `Read` so the per-action risk gate stays silent; the *real* risk is computed inside `call_tool` as `max(base_risk for each tool in declared tool_allowlist)` against `inner.list_tools()`. `tool_allowlist=None` defaults to `WritePersist` (conservative middle so naked declarations don't slip through silently). The mechanical rule fits intuition: read-only check-ins are silent, outbound is notify, anything mapping to `BaseImpact::System` (today's "critical" tier) requires explicit user approval at create time. **Autonomy escalation**: agent-authored wake-ups default to `SafeOnly` if the agent doesn't ask. If the agent explicitly requests `Auto`, the call routes through `ApprovalRouter::ask_with_escalation` (same router the file/email gate uses) — Auto is a user-trust setting, not an agent-trust setting. `BaseImpact::System` impact also forces the prompt regardless of autonomy. Approval question carries the instruction summary + reason + tool/contact/autonomy details. Fail-closed: when the call needs approval but no `ApprovalRouter` is wired (CLI / test builds), the tool refuses rather than silently widening trust. **Persistence**: on success, builds `Wakeup{ origin: WakeupOrigin::Agent{authoring_arc_id} }` with `authoring_arc_id` = `file_gate::arc_uuid(parent_arc_id)` (deterministic v5 mapping from the string arc id), computes `next_fire_at` via `athen_scheduler::compute_next_fire`, persists via the existing `WakeupStore`. The running scheduler picks it up on the next tick — no new infrastructure. **Plumbing**: `wakeup_store: Option<Arc<dyn WakeupStore>>` added to `ApprovedTaskCtx` + `ApprovedTaskBgCtx` + threaded from `AppState::wakeup_store` at all three ctx-construction sites. UI surfacing of agent-authored wake-ups is automatic — `WakeupView::origin` already renders `"agent"` for `WakeupOrigin::Agent`, populated by the existing list-wakeups Tauri command. 19 unit tests in `wakeup_tool::tests` covering: dynamic-impact computation across each declared-allowlist shape, approval-required predicate matrix, duration parsing (positive + reject), schedule conversion (one_shot at/in/both/neither, interval zero), end-to-end wrapper integration via in-memory `WakeupStore` (origin agent, autonomy default, fail-closed-without-router, pass-through forwarding).

**Wake-ups Phase 3c wiring (autonomy directive, complete)**: `task_wakeup_map: TaskWakeupMap` (Arc<RwLock<HashMap<TaskId, WakeupId>>>) added to `AppState` alongside `task_arc_map`. `CoordinatorWakeupSink` populates both on every fire that produces a `SilentApprove` / `NotifyAndProceed` / `HumanConfirm` decision; the dispatch loop drains both on completion. The dispatch loop reads `task_wakeup_map`, fetches the full `Wakeup` row from `wakeup_store`, and stashes it on `ApprovedTaskCtx::wakeup` (Option). Both `execute_dispatched_task` and `execute_approved_task` prepend an "Autonomy directive" block to `system_suffix` — last in the suffix so it sits after compaction summary, memory, and surfaced attachments. Three bands: `Auto` ("only Critical pauses"), `SafeOnly` ("Caution-or-above stops; write a stop note"), `NotifyOnly` ("never send anything outward; write what you would have sent into the arc"). UI: `wakeup-autonomy` `<select>` in the wake-up form (defaults to `safe_only`), surfaced as `Autonomy: …` in each row's meta line. `update_wakeup` Tauri command added so existing rows can have their autonomy band edited.

**Wake-ups subagent inheritance (delegation propagation, complete)**: `Wakeup::inherit_restrictions: bool` (default `true`) added to `athen-core::wakeup`. SQLite migration via idempotent `ALTER TABLE wakeups ADD COLUMN inherit_restrictions INTEGER NOT NULL DEFAULT 1` — old rows surface as `true` so the safer default applies on upgrade. Plumbing: `WakeupSubagentRestrictions` (sync, cloneable snapshot of pre-resolved tool allowlist hash + lower-cased identifier hash + autonomy band) lives in `wakeup_registry`; `resolve_wakeup_restrictions` is the single async helper that turns a wake-up's raw fields into a snapshot. Both `execute_approved_task` and `execute_dispatched_task` call it once per firing and clone the result into (a) the parent's `WakeupRestrictedRegistry` wrap (now via the new sync `new_with_resolved` constructor — no second contact-store lookup) and (b) `DelegationContext::wakeup_restrictions` when `Wakeup::inherit_restrictions = true`. `delegation.rs` reads the snapshot at sub-agent spawn (line ~309) and re-wraps the sub-registry with `WakeupRestrictedRegistry::new_with_resolved` so the wake-up's tool/contact allowlist + autonomy band propagate to the child. When the wake-up opted out (`inherit_restrictions = false`) or there is no wake-up firing, the sub-agent uses the bare base registry as before. UI: new `wakeup-inherit-row` checkbox in `index.html` (defaults to checked); `submitWakeup` posts `inherit_restrictions: bool`; `WakeupView` exposes the field so edit-mode shows the saved value. The previously-needed lock that would have hard-blocked `delegate_to_agent` from wake-up tool allowlists is no longer necessary — opting `delegate_to_agent` in is now safe by default because sub-agents inherit the same allowlist. 1 new unit test (`resolve_wakeup_restrictions_normalizes_inputs`) plus the existing 4 in `wakeup_registry::tests`. Resolves the gap originally tracked under #175 for the inheritance axis specifically; the broader delegation deep-pass (depth, cancellation, identity) still pending under #175.

**Wake-ups Phase 3c2 wiring (tool + contact allowlist enforcement, complete)**: `crates/athen-app/src/wakeup_registry.rs` ships `WakeupRestrictedRegistry`, a `ToolRegistry` wrapper that sits *outermost* (after delegation, app tools, MCP, file gate) so it can hide tools any inner layer exposes. Constructor pre-resolves `contact_allowlist` (UUIDs) into the concrete email/phone/Telegram identifier strings via `ContactStore::load`, lower-cased for case-insensitive comparison; a contact-id allowlist that resolves to zero identifiers fails closed (refuses every outbound call). `list_tools` filters by `tool_allowlist`; `call_tool` rejects names outside the list and, for outbound tools (today: `email_send`), checks every recipient in `to`/`cc`/`bcc` against the resolved identifier set. `AutonomyBand::NotifyOnly` strips outbound tools from `list_tools` and rejects calls regardless of allowlist — defense in depth alongside the system-suffix directive. Both `execute_approved_task` and `execute_dispatched_task` wrap their final registry with this when `ctx.wakeup` is `Some`. `wakeup_commands.rs` extends `CreateWakeupReq` (and `WakeupView`) with `tool_allowlist: Option<Vec<String>>` + `contact_allowlist: Option<Vec<String>>` (UUIDs); empty arrays sanitize to `None` so "cleared the multi-select" means "use profile defaults," not "block everything." New Tauri command `list_available_tools` builds the same registry the executor uses (against the active arc) and returns `Vec<{name, description, outbound}>` — `outbound` flagged by name match against `wakeup_registry::OUTBOUND_TOOL_NAMES` so the UI can render a "sends" badge. UI: two new `wakeups-form-row` blocks in `index.html` — Tool allowlist (multi-checkbox list rendered from `list_available_tools`) and Contact allowlist (rendered from `list_contacts`); both populate from `existing.tool_allowlist`/`contact_allowlist` on edit, post back via `tool_allowlist`/`contact_allowlist` keys on save. Inventory caches per-form-open to avoid backend round-trips. 3 wrapper unit tests in `wakeup_registry::tests`.

**Wake-ups Phase 3b wiring (real coordinator dispatch, complete)**: `wakeup_store: Option<Arc<SqliteWakeupStore>>` and `wakeup_scheduler_shutdown: Option<oneshot::Sender<()>>` added to `AppState`. Initialized in `AppState::new()` from `Database::wakeup_store()`. `start_wakeup_scheduler()` method spawns a `WakeupScheduler` background task with a 5-second tick period, calls `arm_unscheduled(now)` first so freshly-created rows that lack a `next_fire_at` get armed before the first tick. Called from `setup()` hook in lib.rs after `start_dispatch_loop`. **Phase 3b sink**: `crates/athen-app/src/wakeup_sink.rs` ships `CoordinatorWakeupSink` (Phase 3a's `LoggingWakeupSink` was removed once the real path landed). Each fire (a) resolves the target arc — declared `arc_id` if it still exists, otherwise a fresh `"Wake-up: <truncated instruction>"` arc; (b) persists a `SystemEvent` "Wake-up fired: …" marker so the user sees the trigger before the agent responds; (c) builds a synthetic `SenseEvent { source: System, kind: NewMessage, content.summary = instruction, content.body = {wakeup_id, fired_at, origin, autonomy, instruction} }` and hands it to `coordinator.process_event(event)`; (d) for each resulting `(task_id, decision)`, registers `task_id → arc_id` in `task_arc_map` for `SilentApprove` / `NotifyAndProceed` / `HumanConfirm` (so the dispatch loop knows where to write replies and so the eventual approval — Phase 3c — has somewhere to land); (e) calls `dispatch_signal.notify_one()` if anything was enqueued. `HardBlock` is logged + surfaced via the marker only. AutonomyBand + tool/contact allowlist enforcement intentionally NOT yet wired — that's Phase 3c. **Phase 3a Tauri commands** (`wakeup_commands.rs`): `create_wakeup` (one-shot / cron / interval, computes initial `next_fire_at` via `compute_next_fire`, errors if schedule produces no fire), `list_wakeups` (returns `WakeupView` JSON-friendly form), `delete_wakeup`, `set_wakeup_enabled`.

**Status**: Complete -- Tauri 2 desktop app with full agentic tool execution (10 tools including calendar), streaming responses (with thinking content display), arc-based workflow management, native calendar system, sense router for email/calendar/messaging/telegram triage, notification orchestrator with multi-channel delivery (InApp + Telegram), settings UI with provider/email/telegram/notification/embedding management, contacts management UI, provider hot-swap, kill switch, real-time progress events, approval UI, config loading, persistence, path-based file gates (directory grants per arc), and Telegram bot integration. Verified working.
- `src/lib.rs`: Tauri composition root. Modules include `pub(crate) mod app_tools`, `pub(crate) mod contacts`, `pub(crate) mod file_gate`, `pub(crate) mod notifier`. Registers 58 command handlers (52 + 6 agent profile commands: `list_agent_profiles`, `set_arc_profile`, `create_agent_profile`, `update_agent_profile`, `delete_agent_profile`, `restore_agent_profile`): `send_message`, `get_status`, `approve_task`, `cancel_task`, `new_arc`, `get_arc_history`, `list_arcs`, `switch_arc`, `rename_arc`, `delete_arc`, `get_current_arc`, `branch_arc`, `merge_arcs`, `get_timeline_data`, `get_settings`, `save_provider`, `delete_provider`, `test_provider`, `save_settings`, `set_active_provider`, `save_email_settings`, `test_email_connection`, `save_telegram_settings`, `test_telegram_connection`, `list_calendar_events`, `create_calendar_event`, `update_calendar_event`, `delete_calendar_event`, `list_contacts`, `get_contact`, `set_contact_trust`, `block_contact`, `unblock_contact`, `delete_contact`, `mark_notification_seen`, `mark_notification_read`, `mark_all_notifications_read`, `list_notifications`, `delete_notification`, `delete_read_notifications`, `get_notification_settings`, `save_notification_settings`, `save_embedding_settings`, `test_embedding_provider`, `list_memories`, `update_memory`, `delete_memory`, `list_entities`, `list_relations`, `update_entity`, `delete_entity`, `delete_relation`. Registers agent in `setup()` hook. Installs `rustls::crypto::aws_lc_rs::default_provider()` at startup. Calls `state.start_calendar_monitor(app.handle().clone())` in setup. **`tracing-subscriber` initialized** for structured logging in the app.
- `src/main.rs`: Entry point with `windows_subsystem = "windows"` for release builds.
- `src/state.rs`: `AppState` -- composition root that loads TOML configuration via `find_config_dir()` (same discovery order as CLI: `~/.athen/` -> `./config/` -> defaults), resolves active provider from config, builds router and coordinator. Contains:
  - `router: Arc<RwLock<Arc<DefaultLlmRouter>>>` -- double-wrapped for runtime hot-swap: inner `Arc` is the router, `RwLock` allows atomic replacement when the user switches providers
  - `active_provider_id: Mutex<String>` -- ID of the currently active LLM provider (e.g. "deepseek", "ollama")
  - `history: Mutex<Vec<ChatMessage>>` -- arc-level conversation memory
  - `arc_id: Mutex<String>` -- current arc identifier
  - `arc_store: Option<ArcStore>` -- persistent arc storage backed by SQLite
  - `calendar_store: Option<CalendarStore>` -- calendar event storage backed by SQLite
  - `pending_message: Mutex<Option<String>>` -- stashes user's message for replay after approval
  - `model_name: Mutex<String>` -- resolved from config, returned by `get_status`
  - `cancel_flag: Arc<AtomicBool>` -- shared cancellation flag for in-progress tasks
  - `memory: Option<Arc<Memory>>` -- persistent memory system built via `build_memory()` helper (SQLite vector index + SQLite graph + keyword embeddings + LLM entity extractor)
  - `profile_store: Option<Arc<dyn ProfileStore>>` -- `SqliteProfileStore` from the database, seeded with 12 built-ins on first launch
  - `profile_embedder: Arc<dyn EmbeddingProvider>` -- `EmbeddingRouter` (keyword fallback when no provider configured) used to embed query text and per-profile descriptions for blended Coordinator routing
  - `profile_embedding_cache: ProfileEmbeddingCache` -- `Arc<RwLock<HashMap<ProfileId, (DateTime<Utc>, Vec<f32>)>>>`, lazy-populated and invalidated by `profile.updated_at`
  - `SharedRouter` wrapper implementing `LlmRouter` via `Arc<RwLock<Arc<DefaultLlmRouter>>>` -- delegates `route()`, `route_streaming()`, and `budget_remaining()` through the double-Arc indirection
  - `build_router_for_provider(id, base_url, model, api_key)` -- factory function that creates the appropriate provider type based on ID: "deepseek" -> `DeepSeekProvider`, "ollama" -> `OllamaProvider`, "llamacpp" -> `LlamaCppProvider`, anything else -> `OpenAiCompatibleProvider`
  - `restore_or_create_arc()` -- on startup, tries to restore the most recent arc's entries from SQLite; creates a new arc if none exist. Auto-migrates legacy chat sessions to arcs on first startup.
  - `start_calendar_monitor(app_handle)` -- spawns background task that polls `CalendarStore` every 60s, fires SenseEvents through sense router for reminder notifications and arc creation. Always starts (no enable flag -- just checks local DB).
  - `start_telegram_monitor(app_handle)` -- spawns `TelegramMonitor` background task. Owner messages (matching `owner_user_id`) skip sense router triage and go straight to agent execution via `execute_owner_telegram_message()`. Non-owner messages go through normal `process_sense_event()` triage. Owner responses are sent back to Telegram via `sendMessage` API (with 4096-char split for long responses). Arc creation/reuse uses 5-minute time-window grouping. Conversation history loaded from arc for context continuity.
  - **SqliteContactStore replaces InMemoryContactStore**: creates `SqliteContactStore` from the database and wires `TrustManager` with it. Trust-aware risk evaluation is now live in production.
  - `build_coordinator_with_persistence()` creates the TrustManager from SqliteContactStore and attaches it via `.with_trust_manager()`.
  - `init_notifier(app_handle)` -- creates `NotificationOrchestrator` with InApp + optional Telegram channels, wires LLM router for humanization, wires `NotificationStore` for persistence, loads persisted notifications on startup
  - Window focus tracking via `on_window_event` -- sets `notifier.set_user_present(focused)`
  - **API key resolution**: config file key takes priority over env var (e.g. saved key via Settings > `DEEPSEEK_API_KEY` env var). Env var format: `{PROVIDER_ID}_API_KEY` (e.g. `DEEPSEEK_API_KEY`, `OPENAI_API_KEY`). Config values like `${DEEPSEEK_API_KEY}` are treated as unresolved placeholders.
- `src/commands.rs`: Tauri IPC commands:
  - `send_message` -- emits `agent-progress` event for risk evaluation phase, processes through coordinator pipeline, checks for awaiting approval (returns `PendingApproval` with task_id/description/risk_score/risk_level), snapshots conversation history, builds full `AgentExecutor` with `AppToolRegistry` (10 tools: 6 shell + 4 calendar) per request, passes history as `context_messages`, wires streaming sender and cancel flag, executes with 25 max_steps and 90s timeout. On failure: friendly error messages via `format_user_error()`. On cancellation: returns "Task cancelled by user." On max-steps: returns "I ran out of steps (N used) before finishing." Appends user+assistant messages to arc history. Persists entries to SQLite via `ArcStore`.
  - `approve_task` -- approves or denies a task flagged by risk system. On approve: retrieves stashed message, builds executor with streaming + cancel flag, dispatches and executes. On deny: cancels task, clears stashed message.
  - `cancel_task` -- sets `cancel_flag` to `true`; executor checks at loop start and between tool calls.
  - `get_status` -- returns actual model name and connection status.
  - `get_arc_history` -- returns current arc's entries for frontend rendering on startup.
  - `new_arc` -- clears in-memory history, creates new arc with ArcSource::UserInput.
  - `get_current_arc` -- returns current arc ID.
  - `switch_arc(arc_id)` -- loads target arc's entries from SQLite into memory, returns display messages.
  - `rename_arc(arc_id, name)` -- renames an arc.
  - `delete_arc(arc_id)` -- deletes arc and entries. If deleting the active arc, switches to next most recent or creates new one.
  - `list_arcs` -- returns `Vec<ArcMeta>` for sidebar rendering.
  - `branch_arc(parent_id, name)` -- creates a child arc branched from parent.
  - `merge_arcs(source_id, target_id)` -- merges source arc entries into target, marks source as Merged.
  - `list_calendar_events(start, end)` -- range query for calendar events.
  - `create_calendar_event(event)` -- insert calendar event and return it.
  - `update_calendar_event(event)` -- update calendar event by id.
  - `delete_calendar_event(id)` -- delete calendar event by id.
  - `mark_notification_seen(id)` -- mark single notification as seen (toast clicks).
  - `mark_notification_read(id)` -- mark single notification as read.
  - `mark_all_notifications_read` -- mark all notifications as read.
  - `list_notifications` -- return all notifications with read status.
  - `delete_notification(id)` -- delete single notification.
  - `delete_read_notifications` -- bulk delete all read notifications.
  - `format_user_error(err)` -- converts technical error strings to friendly messages (Timeout, Connection, Auth/401, rate_limit/429, max_steps, Budget, RiskThresholdExceeded). `simplify_error()` strips Rust enum formatting for the fallback case.
  - `AgentProgress` struct with `detail: Option<String>` field -- carries tool arguments/result summaries (truncated to 200 chars).
  - `TauriAuditor` -- wraps `InMemoryAuditor`, emits `agent-progress` Tauri events on each step. Extracts meaningful `detail` from step output: shell_execute -> stdout, read_file/write_file -> path, list_directory -> path, errors -> error text, completion -> response preview. `truncate_detail()` compacts newlines and truncates (UTF-8 safe via `char_indices`).
  - `extract_key_terms()` -- extracts key terms from user messages for broader memory recall (stop word filtering, Spanish + English).
  - `judge_worth_remembering()` -- LLM judge for smart auto-remember (60s timeout, Cheap profile). Only stores distilled summaries of meaningful interactions.
  - **Auto-inject memory**: uses full message + individual key terms for broader memory coverage before agent execution.
  - **Auto-remember**: after agent completes, LLM judge evaluates whether the interaction is worth remembering, stores distilled summary if so.
  - 8 new memory management commands: `list_memories`, `update_memory`, `delete_memory`, `list_entities`, `list_relations`, `update_entity`, `delete_entity`, `delete_relation`.
  - 6 agent profile commands:
    - `list_agent_profiles` -- returns all profiles + per-arc assignment for the current arc.
    - `set_arc_profile(arc_id, profile_id)` -- assigns a profile to an arc; subsequent `send_message` calls resolve the profile + persona templates and pass `ResolvedAgentProfile` into the executor.
    - `create_agent_profile(input)` / `update_agent_profile(input)` -- accepts `AgentProfileInput` (snake_case serde rename), saves via `ProfileStore`. Update is allowed for built-ins; the store preserves the `builtin` flag and `created_at`. Re-reads after save so the response carries server-side timestamps.
    - `delete_agent_profile(id)` -- user-authored only; built-ins are guarded.
    - `restore_agent_profile(id)` -- reverts a built-in to its canonical seed values via `SqliteProfileStore::restore_builtin`.
  - `send_message` also calls `force_release_all()` on the dispatcher when `dispatch_next()` returns None, then retries. Prevents "No agent available" errors from stale assignments.
  - `spawn_stream_forwarder(app_handle, arc_id)` -- spawns a background task that reads from `mpsc::UnboundedReceiver<String>` and emits `agent-stream` Tauri events with `{ delta, is_final, arc_id, is_thinking }` payload. Takes `arc_id: Option<String>` and includes it in every event so the frontend only renders streaming bubbles for the active arc. Emits `is_final: true` when the channel closes. **`is_thinking` field** forwarded from agent streaming for thinking block UI.
- `src/app_tools.rs`: `AppToolRegistry` -- wraps `ShellToolRegistry` + adds 4 calendar tools backed by `CalendarStore` + 5 contact tools backed by `SqliteContactStore` + 2 attachment tools (`read_attachment_full`, `fetch_attachment`) backed by `AttachmentStore` + optional `memory: Option<Arc<Memory>>` for persistent `memory_store`/`memory_recall` tool overrides + optional `mcp: Option<Arc<dyn McpClient>>` for MCP-exposed tools + optional `file_gate: Option<Arc<FileGate>>` for path-gated file access. Implements `ToolRegistry` trait. Agent gets the union of: 18 shell tools (incl. `send_telegram` for outbound Telegram text + attachments, owner-chat auto-approve, non-owner routed through `ApprovalRouter`) + 4 calendar + 5 contact + 2 attachment + persistent memory overrides + dynamically added MCP tools prefixed with `<mcp_id>__`. `read_attachment_full(id)` returns the full extracted text (sidecar -> lazy PDF extract -> text-MIME local read), no truncation budget — used when the inline surfacing snippet was truncated. `fetch_attachment(id)` reports `already_local` / `purged` / `metadata_only` state with original source coordinates (per-source auto-refetch lands with #147). Each store is optional: tools only appear in `list_tools` when their backing store is wired. MCP tools and file-touching tools routed through `file_gate.rs` for directory-access control. Respects hexagonal architecture: no new deps on athen-agent, tools injected via composition root.
- `src/commands.rs::persist_uploaded_attachments`: Composer-side companion to the email/Telegram persist paths. Decodes base64 from the frontend, writes each file to `~/.athen/sense-attachments/<event_id>/<sanitized_name>`, inserts an `AttachmentStore` row with `source = None` (local upload, no refetch), and eagerly extracts PDF text via `spawn_blocking`. Returns the synthesized `event_id` so the caller can stamp it onto the user-message arc entry's `attachment_event_id` metadata — surfacing (`latest_sense_event_id_in_arc`) finds it the same way it always did, and the frontend uses the same id to hydrate inline thumbnails on arc reload via `list_attachments_for_event`. `send_message` unifies composer images + file uploads through this single path: images are converted to `UploadedAttachment` shape with `mime_type = image/<fmt>`, persisted, then dropped from the live `images` IPC arg so surfacing reads them back without double-feed. 4 unit tests: empty-uploads short-circuit, no-store fallback, text upload returns event_id + writes row, filename sanitization (path traversal escapes). The legacy "📎 N file(s) uploaded" marker arc entry was removed — the user-message bubble now carries the attachments directly.
- `src/commands.rs::list_attachments_for_event`: Tauri command keyed by `event_id`. Returns `Vec<AttachmentThumbnail>` (`{ id, name, mime_type, size_bytes, purged, data_url }`). Image rows under 4 MB ship a base64 data URL inlined; non-image rows ship `data_url: None` and the UI renders a name + icon chip. Purged rows go grayed-out + line-through in the renderer. Wired through `pending_upload_event_id: Mutex<Option<Uuid>>` on `AppState` so reload thumbnails survive the in-app approval round-trip too: `send_message` stashes the upload event_id when entering the approval branch, `approve_task` takes it, `execute_approved_task` stamps the user-message metadata. The Telegram-driven (background) approval path threads `None` — composer uploads can't originate from a phone.
- `src/compaction.rs`: `LlmArcCompactor` — Phase-1 implementation of the `ArcCompactor` trait. `should_compact` estimates arc tokens (`chars/4`) and compares against the per-provider `compaction_trigger_pct` budget (`resolve_compaction_budget`). `compact` runs the §4 hard-coded summarization prompt (ARC GOAL / PARTICIPANTS / DECISIONS / CONSTRAINTS / PENDING APPROVALS / TOOL OUTCOMES / OPEN ACTION) via `ModelProfile::Fast` at temperature 0; burst heuristic keeps the last 25% of entries verbatim and refuses to compact <4-entry arcs. `load_context_view` reads `ArcMeta.summarized_through_entry_id`, picks the latest summary, loads tail entries with `id > cutoff`, builds a latest-per-tool cache from the compacted prefix, and returns an `ArcContextView`. `view_to_messages` converts the view to `(Vec<ChatMessage>, system_suffix)` so the suffix can be appended via `external_system_suffix` to the leading system prompt (preserves prompt-cache prefix). Settings UI surfaces `provider-context-window`, `provider-compaction-trigger`, `provider-compaction-target` (frontend `app.js:3646–3656`); validators in `settings.rs::save_provider_settings` clamp `trigger > target`. Manual compaction via `compact_arc` Tauri command (`commands.rs:3653`, force via `target_tokens = 0`). 8 tests. Status map in `docs/ARC_COMPACTION.md` §12.
- `src/attachment_purger.rs`: Background TTL sweep for attachment bytes. `sweep_once(store, cutoff)` lists rows whose `fetched_at < cutoff` and aren't already purged, deletes the file, then calls `mark_purged(id)` so `local_path` becomes NULL and `purged_at` is stamped. Extracted-text sidecars are deliberately left in place — they're tiny and keep arc continuity working after the bytes are gone. `spawn_loop(store, ttl_days, interval)` runs the sweep on a forever-loop (default hourly cadence, 30-day TTL from `AttachmentPolicy::default`). Wired in `lib.rs::setup` via `state.start_attachment_purger()` immediately after the email/calendar/telegram monitors. 4 unit tests cover skip-recent, purge-old-keep-sidecar, missing-file-still-marks-purged, no-reconsider-already-purged.
- `src/file_gate.rs`: Path-based permission gate for file-touching tools. `FileGate` struct sits between agent tool calls and the built-in `tokio::fs`-backed file primitives (`read`/`edit`/`write`/`grep`/`list_directory`). Every call carrying a path is routed through `PathRiskEvaluator`, which classifies the access into four bands (Allow/GrantPrompt/QueryFirst/Deny). Wires `GrantStore` (directory grants per arc) + `PathRiskEvaluator` (from `athen-risk`). Returns `ToolResult` with guidance or auto-execution based on risk assessment. Integrates system paths (never writable) + arc grants + contextual risk.
- `src/contacts.rs`: Contacts UI and management commands. 6 commands: `list_contacts`, `get_contact`, `set_contact_trust`, `block_contact`, `unblock_contact`, `delete_contact`. `ContactInfo` and `IdentifierInfo` serialization types for Tauri IPC.
- `src/notifier.rs`: Notification orchestrator and channel implementations.
  - `InAppChannel` -- emits `notification` Tauri events to the frontend
  - `TelegramChannel` -- sends notifications via `athen_sentidos::telegram::send_message` to the owner's chat
  - `NotificationOrchestrator` -- manages delivery across channels with:
    - User presence detection (AtomicBool set by Tauri window focus events)
    - Quiet hours suppression (overnight ranges, critical bypass)
    - Channel selection based on presence (InApp when present, external when away)
    - Escalation with `CancellationToken` from `tokio-util` (try next channel after timeout)
    - LLM humanization (Cheap profile, 30s timeout) -- rephrases raw notification text into natural language before delivery
    - SQLite persistence via `NotificationStore` -- survives app restarts
    - `load_persisted()` restores notifications on startup
    - Delete support: `delete_notification()` single, `delete_read_notifications()` bulk
    - `mark_arc_read()` -- auto-marks notifications as read when user switches to the related arc
  - `NotificationInfo` serializable type for frontend
  - 23 tests (channel selection, fallback, escalation, quiet hours, persistence, delete, load_persisted)
- `src/sense_router.rs`: Generic sense-to-arc router that processes any `SenseEvent` through:
  - **LLM triage**: classifies event relevance as ignore/low/medium/high. Only medium+ reach the user. Spam/low-priority events are silently logged.
  - **Arc creation**: LLM generates descriptive arc names (e.g. "Meeting with John", "Server alert") based on event content.
  - **ArcEntry persistence**: stores event as `ArcEntry` with source-specific `EntryType` and metadata JSON.
  - **Context messages**: `build_context_message()` adds a system message to the Arc so the agent has full context when user opens it. Calendar events get "The user may ask you about this event, want to reschedule it, or need help preparing for it."
  - **Calendar formatting**: `format_calendar_body()` builds readable text from calendar event JSON fields (title, times, location, status like "Starting in 3h 6m"). Calendar events default sender to "Calendar". Body extraction handles calendar's structured JSON.
  - **Frontend emission**: emits `sense-event` Tauri event for real-time UI updates.
  - **Fallback**: if LLM fails or times out, defaults to "medium" relevance (better to show than miss).
  - **Time-window grouping**: `find_recent_arc_from_source()` -- when LLM triage wants a new arc, checks for a recent active arc from the same source updated within 5 minutes. If found, merges into it instead of creating a new arc. Prevents rapid-fire messages (e.g. Telegram) from spawning separate arcs. 1 test.
  - Works for email, calendar, messaging, telegram, or any future sense source. Replaces sense-specific triage code.
  - **Profile routing**: `route_new_arc_to_profile()` runs after triage when a new arc is created. Calls `classify_task` to derive a `ClassifiedTask`, embeds the query text via `profile_embedder`, lazy-caches per-profile embeddings keyed by `(id, updated_at)`, and calls `pick_profile_blended` to score profiles. Result is persisted via `ProfileStore::set_arc_profile`. Falls back to keyword-only routing on any embedding error. `process_sense_event` and `route_new_arc_to_profile` carry `#[allow(clippy::too_many_arguments)]` because they thread the embedder + cache through.
- `src/settings.rs`: Settings management commands:
  - `get_settings` -- loads `~/.athen/models.toml`, returns `SettingsResponse` with provider list (sorted: active first), active provider ID, security mode. Shows env var keys with `(env)` hint.
  - `save_provider(id, base_url, model, api_key)` -- saves/updates provider to `~/.athen/models.toml`. API key handling: `None` preserves existing, `Some("")` removes, `Some("sk-...")` updates. **Hot-reloads** when saving the active provider: builds new router and swaps via `RwLock`.
  - `delete_provider(id)` -- removes provider. If deleting the active provider, automatically switches to first remaining or "deepseek" fallback, hot-reloads router.
  - `test_provider(id, base_url, model, api_key)` -- tests connectivity. Provider-specific: Ollama -> `GET /api/tags`, llama.cpp -> `GET /health`, Anthropic -> `POST /v1/messages`, others -> `POST /v1/chat/completions`. 15-second timeout.
  - `set_active_provider(id)` -- switches active provider at runtime. Builds new router, swaps via `RwLock`, persists choice to `~/.athen/models.toml` under `assignments.active_provider`. Cloud providers require API key (checks config then env var).
  - `save_settings(security_mode)` -- saves security mode (bunker/assistant/yolo) to `~/.athen/config.toml`.
  - `save_email_settings(server, port, username, password, use_tls, folders, poll_interval, lookback_hours)` -- saves email/IMAP config to `~/.athen/config.toml`. Restarts email monitor if enabled.
  - `test_email_connection(server, port, username, password, use_tls)` -- tests IMAP connectivity with provided credentials.
  - `save_telegram_settings(enabled, bot_token, owner_user_id, allowed_chat_ids, poll_interval_secs)` -- saves Telegram bot config to `~/.athen/config.toml`. Restarts telegram monitor if enabled.
  - `test_telegram_connection(bot_token)` -- tests Telegram bot API connectivity via `getMe` endpoint.
  - `save_embedding_settings(mode, provider, model, base_url, api_key)` -- saves embedding config to `~/.athen/config.toml`.
  - `test_embedding_provider(provider, model, base_url, api_key)` -- tests embedding provider connectivity.
  - `get_notification_settings` -- returns notification config (preferred channels, escalation timeout, quiet hours).
  - `save_notification_settings(preferred_channels, escalation_timeout_secs, quiet_hours)` -- saves notification config to `~/.athen/config.toml`.
  - `get_attachment_policy_settings` / `save_attachment_policy_settings(...)` -- attachment policy round-trip (#148). UI sees sizes in MB; persisted as bytes. Validates inline-trust >= download-trust, per-event budget >= per-attachment cap, ttl >= 1 day. `byte_ttl_days` is consumed by `state::start_attachment_purger` on next app launch; the other fields persist for sense-side gates that #139 will wire on. 4 unit tests cover trust-level round-trip, unknown-string rejection, full-policy TOML round-trip, and legacy-config-without-policy parses with defaults via `#[serde(default)]`. **MIME bundles (#148 v2):** the UI exposes 5 named bundle checkboxes (Images / PDFs / Text & code / Office documents / Data files) instead of a comma-separated MIME prefix list — bundles expand to the underlying prefix list on save (e.g. "office" → 4 legacy/modern Office prefixes), prefixes collapse back to bundles on load (case-insensitive). 5 additional tests cover default-maps-to-all-bundles, office-expansion, round-trip identity, unknown-bundle-id-silently-dropped, and case-insensitive matching.
  - **Owner Telegram arc matching (#149):** `state::execute_owner_telegram_message` previously reused any `Messaging`-source arc updated within 5 minutes — that randomly hijacked the owner's reply when a third-party Telegram chat was active. New tier order: (1) `/newarc` command forces a fresh arc, (2) the in-memory `TelegramOutboundHint` slot picks the arc whose Telegram notification fired in the last 2 minutes (highest signal — covers cross-channel hops where an Email arc gets its reply via Telegram), (3) any active arc with `primary_reply_channel = "telegram"` updated in the last 5 min, (4) any Messaging arc within 5 min (legacy fallback), (5) create new. The hint slot is written by `notifier::TelegramChannel::send` after a successful `sendMessage` via `stamp_outbound_hint(hint, arc_id)`. Outbound replies that reused an existing arc append a `📍 Arc: "name" (reason). Send /newarc to start fresh.` footer so the user can spot mis-routings and reset inline. `parse_newarc_command(text) -> (force_new, remaining)` is the pure helper — 6 unit tests cover bare /newarc, /newarc + follow-up, leading whitespace, mid-message non-trigger, prefix-only-matches-on-word-boundary (`/newarchaeology`), and tab separator. The hint helper has 2 tests covering single-write and overwrite-by-most-recent semantics. Multi-intent extension (one Telegram message → N arcs) is captured in `docs/MULTI_INTENT_ROUTING.md` as task #152.
  - `EmbeddingSettingsInfo` response type for embedding settings UI.
  - Helper types: `ProviderInfo` (id, name, type, base_url, model, has_api_key, api_key_hint, is_active), `SettingsResponse` (includes `TelegramSettingsInfo` with the actual `bot_token` so fields populate on reload), `TestResult`. `mask_api_key()` shows first 3 + last 4 chars.
- `src/process.rs`: Child process lifecycle management (stub).
- **Email monitor wiring**: On app launch, if `email.enabled = true` in config, starts `EmailMonitor` in background. Polls IMAP at configured interval, feeds `SenseEvent`s through the sense router for LLM triage and arc creation.
- **Calendar monitor wiring**: Always starts on app launch (no enable flag -- it just checks the local DB). `start_calendar_monitor()` background task polls every 60s, fires `SenseEvent`s through the sense router for reminder notifications and arc creation.
- **Telegram monitor wiring**: On app launch, if `telegram.enabled = true` in config, starts `TelegramMonitor` in background. Owner messages skip triage and go directly to agent execution with responses sent back via Telegram `sendMessage` API.
- `tauri.conf.json`: Window 900x700, `frontendDist` points to `../../frontend`. **`"withGlobalTauri": true`** required in `app` section to inject `window.__TAURI__` into the webview.
- `frontend/index.html`: Full app layout with sidebar, chat area, settings page, and calendar view:
  - **Sidebar**: arc list with `+ New Arc` button, Settings, Calendar, and Contacts buttons at bottom, hamburger toggle for mobile. Arc list populated dynamically from `list_arcs`. Arcs show source icons (message/email/calendar/system) and branch indicators for child arcs.
  - **Chat area**: header with logo + "New Arc" button + per-arc agent profile picker (`#arc-profile-picker`, calls `set_arc_profile` on change), message container, input form with send/stop buttons, status bar.
  - **Profile manager**: Settings → Agent Profiles section listing all profiles with edit/clone/delete (user-authored) or edit/clone/restore-default (built-ins). Modal editor for create/edit/clone with: display name, id, description, persona addendum textarea, domain/task-kind/avoid chip grids, free-form strengths input, model profile hint. Saves via `create_agent_profile` / `update_agent_profile`. Restore button calls `restore_agent_profile`.
  - **Stop button**: red square, initially hidden, shown during processing. Calls `cancel_task`.
  - **Settings page**: provider cards area, "Add Provider" button with template dropdown (DeepSeek, OpenAI, Anthropic, Ollama, llama.cpp, Custom), security mode selector (Assistant/Bunker/YOLO), email settings section (IMAP config, test connection, app password hints), telegram settings section (bot token, owner user ID, allowed chat IDs, poll interval, test connection, enable/disable toggle), notification settings section (channel priority, escalation timeout, quiet hours), embedding settings section (simple/advanced toggle), back button.
  - **Notifications**: Notifications button in sidebar with unread count badge, notifications-view container for full notification list.
  - **Calendar view**: full `#calendar-view` container with header (prev/next/today/view-select), calendar grid, event modal with all fields (title, date, time, all-day, location, description, category dropdown, color picker, reminder dropdown, recurrence dropdown).
  - **Memory view**: Memory tab with sidebar button, two sub-tabs (Memories + Knowledge Graph). Memories: search, inline editing (re-embeds on save), delete. Knowledge Graph: expandable entity cards with relations and metadata.
- `frontend/styles.css`: Dark theme (Tokyo Night-inspired). Top of `:root` declares `color-scheme: dark` so WebKitGTK renders native `<select>` popups on dark surfaces (fix for white-on-white dropdowns in agent picker / onboarding / memory mode). Global `select, option` overrides set `var(--surface-2)` background + `var(--text)` color. Profile manager styles: `.profile-list`, `.profile-card`, `.profile-card-actions`, `.profile-modal`, `.profile-chip-grid`, `.profile-chip`. Header profile picker: `.header-right`, `.profile-picker-wrap`, `.profile-picker-label`, `.profile-picker`. `.mcp-card-icon` is now a flex container that holds an inline SVG (folder/globe/terminal/database/mail/calendar) instead of literal text. Sidebar with arc items (source icons, branch indicators, rename/delete on hover, notification dots with pulse animation), tool execution cards with status icons (check/cross/spinner) and fade-in animation, streaming message bubbles, chat bubbles with avatars, risk badges, code blocks with language labels, approval dialog, sense event cards with relevance badges, settings page with provider cards (expand/collapse), email settings section, telegram settings section, notification settings section, and embedding settings section, contacts view with trust badges (color-coded: Unknown=red, Neutral=gray, Known=yellow, Trusted=green, AuthUser=blue), calendar view (month grid, week grid, event pills, day cells, modal, color picker dots -- ~350 lines), toast container and notification-toast styles (urgency variants: low=blue, medium=amber, high=orange, critical=red), notification-item styles (unread blue border, read dimmed), notif-badge (red pill), channel-order-item, time-input, advanced-toggle, api-key-field, auto-growing textarea, stop button (red), **thinking block styles** (collapsible details), mobile responsive.
- `frontend/app.js`: Full chat frontend with:
  - **Streaming**: listens for `agent-stream` Tauri events (now with `arc_id` and `is_thinking` fields). Creates streaming bubble on first chunk only if `arc_id` matches `activeArcId`; background arcs get a pulsing blue notification dot on the sidebar instead. Appends text progressively via `textContent` (safe, fast). On `is_final`, re-renders full text with markdown for proper formatting, resets `streamingBubble` and `streamingText` to prevent multiple rapid streams from merging. Tracks `streamingBubble`, `streamingText`, `didReceiveStreamChunks` state. **Thinking content**: `is_thinking` chunks rendered in a collapsible `<details>` block.
  - **Empty-bubble rescue (Qwen-on-llamacpp)**: when `--jinja` routes a Qwen `<think>...</think>` envelope entirely into `reasoning_content`, the streaming consumer sees only thinking chunks and never creates a `.message-bubble`. The non-streaming fallback in `executor::try_streaming_call` still returns content (promoted from reasoning by `quirks::apply_to_response`), so `sendMessage`'s response handler now creates a regular bubble from `response.content` after `finalizeStreamingMessage` if no bubble exists yet. Provider-agnostic — only fires when the streamed row exists but has no `.message-bubble`, which is mathematically impossible for any provider that streams a real content delta.
  - **Inline message attachments**: `addMessage` accepts `meta.attachments` and renders thumbnail chips below the bubble (above the timestamp). Images become clickable `.message-attachment-thumb` (data URL → opens full-size in new tab); other MIMEs render as `.message-attachment-chip` with name + icon + size. Purged rows go grayed-out + line-through. On live send the FE reuses the composer's local base64 to render thumbnails immediately. On arc reload, `renderHistoryEntry` reads the user-message entry's `attachment_event_id` metadata and calls `list_attachments_for_event` async via `hydrateAttachmentsAsync`, splicing chips in once the bytes return — keeps `loadHistory` from blocking on N file reads.
  - **Tool execution cards**: listens for `agent-progress` events. Creates `tool-steps-container` div, appends `tool-execution-card` elements with status class (completed/failed/in-progress), status icon (checkmark/cross/dot), tool name, and truncated detail text. Cards have fade-in CSS animation.
  - **Arc sidebar**: `loadArcs()` fetches arc list, `renderArcList()` builds sidebar items with name, source icon (message/email/calendar/system), relative date, entry count badge, and branch indicator for child arcs. Double-click or pencil icon to rename (inline contenteditable). Delete button with confirmation. Branch button creates child arc. Merged arcs hidden from sidebar. Auto-names new arcs from first user message (~30 chars). Active arc highlighted. Timeline toggle button in sidebar header.
  - **Arc timeline view**: Full-screen time-aligned multi-lane graph, toggled via button in sidebar. Each arc is a vertical column (most recently active = rightmost). Each entry is a colored node (blue=message, amber=tool, purple=email, green=calendar, gray=system) positioned by timestamp. Vertical rail lines connect nodes within each arc. Sticky column headers show source icon, arc name, entry count -- click to open. Time axis on left margin with relative labels ("Now", "5m ago", "Yesterday"). Hover nodes for content preview tooltips. Entries within 2 minutes are grouped into the same row. Merged arcs dimmed (40% opacity), archived at 60%. "Back" and "+ New Arc" buttons in header. Auto-refreshes every 30 seconds. Backend: `get_timeline_data` command returns all arcs with their entries in a single call. Full-screen overlay (`position: fixed; z-index: 100`). Five-view toggle: chat <-> settings <-> timeline <-> calendar <-> contacts.
  - **Sense event cards**: listens for `sense-event` Tauri events. Renders source-specific icons, LLM relevance badges (Important/Urgent), agent's reasoning text, and context-aware action buttons (Summarize/Draft Reply/Add to Calendar/Open Arc). Calendar events get "What should I prepare?" button instead of "Summarize"; action buttons switch to event's Arc first before sending prompt; prompts are short (agent has context from system message).
  - **Calendar view**: `showCalendar()` / `hideCalendar()` view switching. Month view (7-column grid) and week view (time grid with positioned events). Event modal for create/edit/delete. Category-to-color mapping.
  - **Kill switch**: stop button (red square) replaces send button during processing. Calls `cancel_task` command. Escape key also cancels. `isProcessing` flag controls button visibility.
  - **Error handling**: `format_user_error()` produces friendly messages. Retry button for transient errors (stores `lastMessage`, `retryLastMessage()` re-submits). "Open Settings" link for auth errors.
  - **Markdown renderer** (inline, no dependencies): fenced code blocks with language labels, inline code, headers (h1-h3), ordered/unordered lists, bold, italic, links. Code blocks protected from inline transformations.
  - **XSS protection**: user messages use `textContent` (never innerHTML), assistant messages go through markdown renderer with `escapeHtml()` on code blocks.
  - **Real-time progress**: status bar shows "Step N: tool_name (status)" and "Evaluating risk..." during risk phase.
  - **Approval dialog**: shows risk badge, score, description, approve/deny buttons.
  - **Contacts view**: sidebar button, contact cards with color-coded trust badges (Unknown=red, Neutral=gray, Known=yellow, Trusted=green, AuthUser=blue), expandable details, trust level dropdown, block/unblock/delete actions.
  - **Arc notification dots**: `arcsWithNotifications` Set, `markArcWithNotification()`, CSS pulse animation on sidebar items. Dot cleared when switching to that arc.
  - **`arc-updated` event listener**: refreshes sidebar when background Telegram execution completes.
  - **Toast notification system**: listens for `notification` Tauri events. Slide-in toasts with urgency-based styling (low=blue, medium=amber, high=orange, critical=red). Auto-dismiss for Low/Medium urgency. Click-to-open-arc navigation. Mark-seen on click via `mark_notification_seen` command.
  - **Notifications view**: full notification list with read/unread state. Delete per item, "Clear read" bulk delete, "Mark all read" button. Notification badge on sidebar (unread count, auto-updates via polling). Seven-view toggle: chat <-> settings <-> timeline <-> calendar <-> contacts <-> notifications <-> memory.
  - **Settings page**: loads providers via `get_settings`, renders provider cards with expand/collapse. Edit fields for base URL, model, API key (masked display, show/hide toggle). "Test Connection" and "Save" buttons per provider. "Set Active" button to switch provider. "Delete" with confirmation. Add provider via template selection. Security mode dropdown with contextual hints. Email settings section with IMAP server/port/username/password fields, TLS toggle, test connection button, and app password hints for Gmail/Outlook. Telegram settings section with bot token field (populated on reload), owner user ID, allowed chat IDs, poll interval, test connection, enable/disable toggle. Notification settings section with channel priority list, escalation timeout, quiet hours (start/end time inputs, allow-critical toggle). Embedding settings section with simple mode dropdown (Automatic/Cloud/Local Only/Specific/Off) + advanced toggle revealing provider/model/URL/API key fields with test connection button.
  - **Agent profile picker** (header) and **profile manager** (Settings):
    - `loadAgentProfiles()` fetches all profiles + per-arc assignment; `renderProfilePicker()` populates the header dropdown; `onProfileChange()` calls `set_arc_profile`.
    - `loadProfileManager()` + `renderProfileList()` + `buildProfileCard()` render the manager. Built-ins get a "Restore default" button; Edit is enabled for all (built-in edits are persisted but the `builtin` flag is preserved).
    - `openProfileEditor(mode, profile?)` opens the modal in create/edit/clone mode. Chip grids built from `PROFILE_DOMAINS` / `PROFILE_TASK_KINDS` constants. `saveProfileFromEditor()` builds the snake_case `AgentProfileInput` and calls `create_agent_profile` / `update_agent_profile`.
    - `mcpIconSvg(name)` maps icon name → inline SVG (folder/globe/terminal/database/mail/calendar) so MCP cards render an icon instead of literal text like "Folder".
  - **Auto-growing textarea**: expands with content up to 150px. Enter sends, Shift+Enter for newline.
  - **Smooth scroll**: `requestAnimationFrame` + `scrollTo` on new messages and tool cards.
- **Requires system libraries**: `webkit2gtk4.1-devel gtk3-devel libsoup3-devel libappindicator-gtk3-devel` (Fedora).
- **Verified working**: Full multi-step agentic pipeline with streaming confirmed. Tested: (1) "What tools do you have?" -> LLM correctly lists its 10 tools (6 shell + 4 calendar), (2) "Read https://alejandrogarcia.blog/ and write to HELLO.md" -> LLM uses `shell_execute` (curl) to fetch website, then `write_file` to save formatted markdown. Streaming renders progressively with markdown finalization. Tool execution cards show in real time with status icons. Arc persistence across app restarts. Provider hot-swap works without restart. Settings UI tested for add/edit/delete/test/activate providers. Email monitor tested with real IMAP server. Calendar monitor fires reminders for upcoming events. Telegram monitor: owner messages trigger direct agent execution with responses sent back to Telegram; non-owner messages triaged through sense router. Contacts UI: trust levels, block/unblock, delete working. Memory system: auto-inject context from past interactions, auto-remember meaningful conversations via LLM judge, full memory management UI (search, edit, delete memories and knowledge graph entities).

---

## Distribution Channels

Linux users install through one of three channels; macOS/Windows ship via GitHub Releases. See [docs/PACKAGING.md](PACKAGING.md) for full rationale and per-release checklist.

- **AUR** (`packaging/aur/athen-bin/`): published as `athen-bin`, a binary repackage of the upstream `.deb`. Per-release bump via `packaging/aur/bump.sh <version> <aur-clone>`. Source-build PKGBUILD also lives in `packaging/aur/athen/` but is not published.
- **Fedora COPR** (`packaging/copr/athen.spec`): source-built RPM at `albiol2004/athen`. Auto-rebuilds on every release tag via GitHub webhook. Build runs with network enabled (cargo fetches crates from crates.io); switch to a vendored `Source1` tarball before any push for inclusion in official Fedora repos.
- **GitHub Releases**: the canonical source — `.AppImage`, `.deb`, `.rpm`, `.dmg`, `.msi`, `_setup.exe` plus signed `latest.json` manifest. Built by `.github/workflows/release.yml`.

Auto-updater behaviour splits on install kind via `commands::detect_installer_kind`: AppImage / macOS / Windows self-update through `tauri-plugin-updater`; system-package installs (rpm/deb/AUR) get a banner pointing to the GitHub release page and update through their package manager.

---

## Integration Tests (28 integration tests + 15 app_tools tests + 23 notifier tests + memory management tests)

Integration tests wire real implementations together (no mocks except LLM).

### athen-coordinador/tests/integration_pipeline.rs (4 tests)
- User input flows through coordinator to create task with correct domain/priority/status
- Dangerous commands ("sudo rm -rf") get blocked by real rule engine
- Dispatch assigns tasks to registered agents, release cycle works
- Priority ordering: High (UserInput) dispatched before Normal (Email)

### athen-ipc/tests/integration_ipc.rs (5 tests)
- Monitor sends SenseEvent to coordinator via IPC server/client
- Coordinator broadcasts HealthPing to multiple agent clients
- Coordinator routes TaskAssignment to specific agent only
- Client reconnection after disconnect
- 5 concurrent clients sending simultaneously -- no messages lost

### athen-persistence/tests/integration_persistence.rs (5 tests)
- Full task lifecycle (create->update steps->checkpoint->complete->filter)
- Checkpoint survives simulated crash (file-based DB, drop, reopen)
- Pending message queue ordering (FIFO, pop atomicity, no re-pop)
- 10 concurrent task operations -- no corruption
- CheckpointManager file atomicity (temp->fsync->rename)

### athen-risk/tests/integration_risk.rs (6 tests)
- Same action, different trust levels -> proportional score changes
- Trust evolution (5 approvals upgrades T0->T1->T2) reduces risk over time
- Rule engine: AuthUser vs Unknown sender for dangerous commands
- Data sensitivity escalation (Plain->PersonalInfo->Secrets)
- Uncertainty penalty impact on otherwise safe actions
- CombinedEvaluator chooses rules vs LLM based on pattern match

### athen-memory/tests/integration_memory.rs (5 tests)
- Knowledge graph: build contact network, explore at depth 1 vs 2
- Vector search: cosine similarity ranking with known embeddings
- Memory facade: remember/recall/forget lifecycle
- SQLite persistence across connection drop/reopen
- Graph exploration respects max_nodes, max_depth, relevance_threshold

### athen-agent/tests/integration_agent.rs (3 tests)
- Mock LLM returns shell_execute tool call -> real ShellToolRegistry runs `echo hello` -> result fed back -> LLM completes
- Mock LLM requests read_file -> real tool reads temp file -> correct content returned
- Multi-step: tool call -> result -> another tool call -> result -> final answer

---

## Running the CLI

```bash
# Set API key and run
DEEPSEEK_API_KEY=sk-... cargo run -p athen-cli --release

# Or build first, then run the binary directly
cargo build -p athen-cli --release
DEEPSEEK_API_KEY=sk-... ./target/release/athen-cli
```

The CLI reads from stdin, processes through the full pipeline (coordinator -> risk -> dispatch -> LLM), and prints responses. Exit with Ctrl+D, `/quit`, or `/exit`.
