# Epic 13 — Interfaces (TUI + CLI + API) ## completed deliverables ### task 0 — orchestrator resume mechanism fixed the approval gate in `SessionOrchestrator` to truly suspend and resume rather than immediately failing. changes: - `handleApproval` is now `suspend` — holds a `CompletableDeferred` per pending request, keyed by `ApprovalRequestId` - emits `ApprovalRequestedEvent` before suspending so the server layer can surface the request id to clients - `DefaultSessionOrchestrator.submitApprovalDecision(requestId, decision)` completes the deferred and resumes the workflow coroutine - `ReplayOrchestrator.mapValidationOutcome` updated to `suspend` to match the parent signature - `approvalEngine` removed from `SessionOrchestrator` (was evaluated synchronously; now decisions arrive externally) files changed: - `core/kernel/.../orchestration/SessionOrchestrator.kt` - `core/kernel/.../orchestration/DefaultSessionOrchestrator.kt` - `core/kernel/.../orchestration/ReplayOrchestrator.kt` --- ### task 1 — websocket protocol defined the wire protocol as sealed class hierarchies with kotlinx.serialization in `apps/server/protocol/`. final structures: * `ServerMessage` — 16 subtypes across session lifecycle, stage execution, inference, tool execution, approval, provider status, and error * `ClientMessage` — 5 subtypes: StartSession, ResumeSession, CancelSession, ApprovalResponse, Ping * `ApprovalDecision` (protocol enum) — `APPROVE`, `REJECT`, `STEER` — separate from the domain `ApprovalDecision` data class * `RiskSummaryDto`, `ProviderHealthDto`, `SessionConfigDto`, `PauseReason` — protocol-only DTOs * `ProtocolSerializer` — encodes `ServerMessage` to JSON, decodes `ClientMessage` from JSON; unknown/malformed input throws `ProtocolException` (never crashes) key properties: - `Json { classDiscriminator = "type"; ignoreUnknownKeys = true }` - no domain types leak into the protocol layer except pure value aliases (`SessionId`, `StageId`, `ApprovalRequestId`, `Tier`) files created: - `apps/server/src/main/kotlin/com/correx/apps/server/protocol/ServerMessage.kt` - `apps/server/src/main/kotlin/com/correx/apps/server/protocol/ClientMessage.kt` - `apps/server/src/main/kotlin/com/correx/apps/server/protocol/Dtos.kt` - `apps/server/src/main/kotlin/com/correx/apps/server/protocol/ProtocolSerializer.kt` --- ### task 2 — ktor server + websocket implemented the full Ktor 3.x server with REST and WebSocket endpoints. rest endpoints: - `GET /health` — provider status, 200 in degraded mode - `GET/POST /sessions` — list and start sessions - `GET /sessions/{id}` — single session state - `POST /sessions/{id}/cancel` - `GET /sessions/{id}/events?from=&limit=50` — paginated event replay - `GET /workflows` — available workflows - `GET /providers` — provider health websocket endpoints: - `WS /sessions/{id}/stream` — session-scoped stream - `WS /stream` — global stream across all sessions websocket behavior: - on connect: snapshot of current state, then live events - on reconnect with `?lastEventId=`: replays missed events from that point - 30s server heartbeat; client must respond within 10s or connection is dropped - unknown client message → `ProtocolError` sent, connection kept open `ServerModule` holds all dependencies as interfaces; no concrete instantiation inside it. files created: - `apps/server/src/main/kotlin/com/correx/apps/server/Application.kt` - `apps/server/src/main/kotlin/com/correx/apps/server/ServerModule.kt` - `apps/server/src/main/kotlin/com/correx/apps/server/routes/SessionRoutes.kt` - `apps/server/src/main/kotlin/com/correx/apps/server/routes/WorkflowRoutes.kt` - `apps/server/src/main/kotlin/com/correx/apps/server/routes/ProviderRoutes.kt` - `apps/server/src/main/kotlin/com/correx/apps/server/ws/SessionStreamHandler.kt` - `apps/server/src/main/kotlin/com/correx/apps/server/ws/GlobalStreamHandler.kt` - `apps/server/src/main/kotlin/com/correx/apps/server/registry/WorkflowRegistry.kt` - `apps/server/src/main/kotlin/com/correx/apps/server/registry/ProviderRegistry.kt` --- ### task 3 — approval interaction implemented the full approval lifecycle bridging the orchestrator suspension and the WebSocket clients. `ApprovalCoordinator` responsibilities: - tracks connected WS clients per session - on `ApprovalRequestedEvent`: broadcasts `ServerMessage.ApprovalRequired` to all clients for that session - schedules a timeout job (`coroutineScope.launch { delay(timeoutMs) }`) that auto-denies if no response within `ApprovalConfig.timeoutMs` (default 5 minutes) - on `ClientMessage.ApprovalResponse`: cancels timeout job, translates protocol enum to domain `ApprovalDecision`, calls `orchestrator.submitApprovalDecision()` - double-response guard via `ConcurrentHashMap` — second response returns `ProtocolError`, no double-resume protocol → domain mapping: - `APPROVE` → `ApprovalOutcome.APPROVED` - `REJECT` → `ApprovalOutcome.REJECTED` - `STEER` → `ApprovalOutcome.APPROVED` + `UserSteering(text = steeringNote, ...)` rest fallback: `POST /sessions/{id}/approve` for non-WS clients. files created: - `apps/server/src/main/kotlin/com/correx/apps/server/approval/ApprovalCoordinator.kt` - `apps/server/src/main/kotlin/com/correx/apps/server/approval/ApprovalConfig.kt` - `apps/server/src/main/kotlin/com/correx/apps/server/routes/ApprovalRoutes.kt` --- ### task 4 — mosaic tui implemented a new `apps/tui` Gradle module with a Mosaic-based interactive terminal UI. architecture: - single WS listener coroutine updates `mutableStateOf(TuiState)` — no polling - separate stdin reader coroutine sends `KeyEvent` to a `Channel` - key handler coroutine dispatches protocol messages to the server - exponential-backoff reconnect (1s → 30s max) on server disconnect - `applyServerMessage` pure reducer maps `ServerMessage` to state updates layout: status bar / session list (last 5) / active session panel / approval panel (collapsed when no pending approval) / input bar keybinds: `n` new, `c` cancel, `a/r/s` approve/reject/steer (only active when approval pending), `↑↓` navigate, `q` quit — inactive binds are no-ops files created: - `apps/tui/build.gradle` - `apps/tui/src/main/kotlin/com/correx/apps/tui/TuiApp.kt` - `apps/tui/src/main/kotlin/com/correx/apps/tui/StateReducer.kt` - `apps/tui/src/main/kotlin/com/correx/apps/tui/KeyEvent.kt` - `apps/tui/src/main/kotlin/com/correx/apps/tui/state/TuiState.kt` - `apps/tui/src/main/kotlin/com/correx/apps/tui/ws/TuiWsClient.kt` - `apps/tui/src/main/kotlin/com/correx/apps/tui/components/StatusBar.kt` - `apps/tui/src/main/kotlin/com/correx/apps/tui/components/SessionList.kt` - `apps/tui/src/main/kotlin/com/correx/apps/tui/components/ActiveSession.kt` - `apps/tui/src/main/kotlin/com/correx/apps/tui/components/ApprovalPanel.kt` - `apps/tui/src/main/kotlin/com/correx/apps/tui/components/InputBar.kt` --- ### task 5 — clikt cli implemented the full `apps/cli` command suite using Clikt. commands: - `correx run --workflow [--session ] [--auto-approve]` - `correx session list/resume/cancel/events` - `correx approve --decision [--note]` - `correx status` - `correx provider list` output modes: `--json` (valid parseable JSON), `--quiet` (errors only), default (human-readable with ANSI color). `correx run` behavior: - starts session via `POST /sessions`, subscribes to WS stream - prints stage transitions and tool calls to stdout - on `ApprovalRequired`: interactive TTY prompt if stdin is a terminal; auto-deny with warning if no TTY and `--auto-approve` absent - exit codes: `0` complete, `1` failed, `2` approval denied `CliWsClient` wraps the Ktor WebSocket client for stream subscriptions. files created: - `apps/cli/src/main/kotlin/com/correx/apps/cli/CorrexCli.kt` - `apps/cli/src/main/kotlin/com/correx/apps/cli/CliConstants.kt` - `apps/cli/src/main/kotlin/com/correx/apps/cli/ws/CliWsClient.kt` - `apps/cli/src/main/kotlin/com/correx/apps/cli/commands/RunCommand.kt` - `apps/cli/src/main/kotlin/com/correx/apps/cli/commands/SessionCommand.kt` - `apps/cli/src/main/kotlin/com/correx/apps/cli/commands/ApproveCommand.kt` - `apps/cli/src/main/kotlin/com/correx/apps/cli/commands/StatusCommand.kt` - `apps/cli/src/main/kotlin/com/correx/apps/cli/commands/ProviderCommand.kt` --- ### task 6 — shared config implemented configuration loading in `core/config`. schema (`~/.config/correx/config.toml`, override via `$CORREX_CONFIG`): ```toml [server] host = "localhost" port = 8080 [tui] theme = "dark" session_list_limit = 5 [cli] default_output = "human" [approval] timeout_ms = 300000 ``` `ConfigLoader.load()` — reads from file, returns `CorrexConfig()` defaults on missing file, logs warning and returns defaults on malformed input. no new library dependencies — manual TOML parser for the required subset (sections + key=value pairs). files created: - `core/config/src/main/kotlin/com/correx/core/config/CorrexConfig.kt` - `core/config/src/main/kotlin/com/correx/core/config/ConfigLoader.kt` --- # final architecture after Epic 13 ```text apps/ ├── server ← Ktor 3.x, REST + WebSocket, approval coordinator ├── tui ← Mosaic reactive terminal (new module) ├── cli ← Clikt commands, WS stream client └── shared ← (config lives in core/config) core/ └── config ← CorrexConfig, ConfigLoader (new module) ``` --- # major architectural outcomes Epic 13 established: * real approval pause/resume — orchestrator now truly suspends and resumes externally * a wire protocol separating transport from domain (no domain types in protocol layer) * a Ktor server as the single point of access — TUI and CLI are pure clients * WebSocket event streaming with snapshot-on-connect and missed-event replay * approval timeout and double-response safety * a Mosaic TUI with reactive state and context-aware keybinds * a full Clikt CLI with machine-readable JSON output and correct exit codes * shared config via TOML with graceful fallback to defaults --- # what Epic 13 intentionally does NOT include not implemented: * router context isolation (explicitly deferred — see CLAUDE.md) * streaming inference responses (explicitly deferred) * parallel agent execution (explicitly deferred) * GPU residency scheduling (explicitly deferred) * TUI config editor (`,` keybind present, editor not implemented) * `correx config --edit` command (not in server surface) --- # final state Correx now has: > a complete interface layer — server, TUI, and CLI — all backed by a single Ktor WebSocket bus, with approval decisions flowing from any client through to the orchestrator's suspended coroutine and resuming execution cleanly.