# Epic 13 — Interfaces (TUI + CLI + API)

**status:** planned
**depends on:** epics 0–12
**goal:** user interaction layer — server owns the orchestrator, TUI and CLI are clients

---

## architecture

```
apps/server   ← owns orchestrator, event store, all infrastructure. Ktor.
apps/tui      ← Mosaic-based interactive terminal. websocket client only.
apps/cli      ← Clikt-based non-interactive entrypoint. http/websocket client.
```

all three are clients of `core:kernel` + `infrastructure` through `apps/server`.
TUI and CLI never instantiate orchestrator directly — they talk to the server.

---

## task 1: websocket protocol definition

**this must be done first.** everything else depends on it.

define the protocol as a sealed class hierarchy, serialized via kotlinx.serialization:

### server → client (push):
```kotlin
sealed class ServerMessage {
    // session lifecycle
    data class SessionStarted(val sessionId: SessionId, val workflowId: String) : ServerMessage()
    data class SessionPaused(val sessionId: SessionId, val reason: PauseReason) : ServerMessage()
    data class SessionCompleted(val sessionId: SessionId) : ServerMessage()
    data class SessionFailed(val sessionId: SessionId, val reason: String) : ServerMessage()

    // stage execution
    data class StageStarted(val sessionId: SessionId, val stageId: StageId) : ServerMessage()
    data class StageCompleted(val sessionId: SessionId, val stageId: StageId) : ServerMessage()
    data class StageFailed(val sessionId: SessionId, val stageId: StageId, val reason: String) : ServerMessage()

    // inference
    data class InferenceStarted(val sessionId: SessionId, val stageId: StageId) : ServerMessage()
    data class InferenceCompleted(val sessionId: SessionId, val stageId: StageId, val outputSummary: String) : ServerMessage()
    data class InferenceTimedOut(val sessionId: SessionId, val stageId: StageId, val elapsedMs: Long) : ServerMessage()

    // tool execution
    data class ToolStarted(val sessionId: SessionId, val toolName: String, val tier: Tier) : ServerMessage()
    data class ToolCompleted(val sessionId: SessionId, val toolName: String, val outputSummary: String) : ServerMessage()
    data class ToolFailed(val sessionId: SessionId, val toolName: String, val reason: String) : ServerMessage()
    data class ToolRejected(val sessionId: SessionId, val toolName: String, val reason: String) : ServerMessage()

    // approval
    data class ApprovalRequired(
        val sessionId: SessionId,
        val requestId: ApprovalRequestId,
        val tier: Tier,
        val riskSummary: RiskSummaryDto,
        val toolName: String?,
        val preview: String?,
    ) : ServerMessage()

    // provider
    data class ProviderStatusChanged(val providerId: String, val status: ProviderHealthDto) : ServerMessage()

    // error
    data class ProtocolError(val message: String) : ServerMessage()
}
```

### client → server (commands):
```kotlin
sealed class ClientMessage {
    data class StartSession(val workflowId: String, val config: SessionConfigDto?) : ClientMessage()
    data class ResumeSession(val sessionId: SessionId) : ClientMessage()
    data class CancelSession(val sessionId: SessionId) : ClientMessage()
    data class ApprovalResponse(
        val requestId: ApprovalRequestId,
        val decision: ApprovalDecision,   // APPROVE, REJECT, STEER
        val steeringNote: String?,
    ) : ClientMessage()
    data class Ping(val timestamp: Long) : ClientMessage()
}
```

**files:**
- `apps/server/src/main/kotlin/.../protocol/ServerMessage.kt`
- `apps/server/src/main/kotlin/.../protocol/ClientMessage.kt`
- `apps/server/src/main/kotlin/.../protocol/ProtocolSerializer.kt` — register both hierarchies

**acceptance criteria:**
- all messages serialize/deserialize correctly
- unknown message type produces `ProtocolError`, not crash
- protocol types are DTOs — no domain types leak into protocol layer

---

## task 2: apps/server — Ktor server + websocket

**dependencies:** task 1

**endpoints:**

```
GET  /health                          → server + provider status
GET  /sessions                        → list recent sessions (last 20)
GET  /sessions/{id}                   → session detail + current state
POST /sessions                        → start new session (body: StartSessionRequest)
POST /sessions/{id}/cancel            → cancel session
GET  /sessions/{id}/events            → full event log (paginated)
WS   /sessions/{id}/stream            → live event stream for session
WS   /stream                          → global event stream (all sessions)
GET  /workflows                       → list available workflow definitions
GET  /providers                       → provider registry + health
```

**websocket behavior:**
- on connect: send snapshot of current session state, then stream live events
- on disconnect: clean up subscription, no state lost
- on reconnect: client sends last received event id, server replays missed events from that point
- heartbeat: server sends `Ping` every 30s, client must respond within 10s or connection dropped

**files:**
- `apps/server/src/main/kotlin/.../Application.kt`
- `apps/server/src/main/kotlin/.../routes/SessionRoutes.kt`
- `apps/server/src/main/kotlin/.../routes/WorkflowRoutes.kt`
- `apps/server/src/main/kotlin/.../routes/ProviderRoutes.kt`
- `apps/server/src/main/kotlin/.../ws/SessionStreamHandler.kt`
- `apps/server/src/main/kotlin/.../ws/GlobalStreamHandler.kt`
- `apps/server/src/main/kotlin/.../ServerModule.kt` — wires InfrastructureModule + Ktor

**acceptance criteria:**
- `/health` returns 200 with provider status
- websocket stream delivers events within 100ms of emission
- reconnect with last event id replays missed events correctly
- unknown WS message type → `ProtocolError` sent, connection kept open

---

## task 3: apps/server — approval interaction

**dependencies:** task 2

approval flow:
1. orchestrator emits `ApprovalRequiredEvent` → `OrchestrationPausedEvent`
2. server catches pause, sends `ApprovalRequired` message to all connected clients for that session
3. client sends `ApprovalResponse`
4. server calls `approvalEngine.grant()` or `approvalEngine.deny()` accordingly
5. orchestrator resumes

**steering:** `ApprovalDecision.STEER` + `steeringNote` injects a steering event into the session event stream before resuming. the note becomes L0 context for the next stage.

**timeout:** if no response within `ApprovalConfig.timeoutMs`, server auto-denies and emits `ApprovalTimedOutEvent`. configurable, default 5 minutes.

**files:**
- `apps/server/src/main/kotlin/.../approval/ApprovalCoordinator.kt`
- `apps/server/src/main/kotlin/.../routes/ApprovalRoutes.kt` — REST fallback for non-WS clients

**acceptance criteria:**
- approval required → session pauses → client receives message
- approve → session resumes
- reject → session fails with reason
- steer → steering note injected → session resumes with note in context
- timeout → auto-deny, event emitted

---

## task 4: apps/tui — Mosaic dashboard

**dependencies:** task 2, task 3

**layout:**
```
┌─ status bar ──────────────────────────────────────────────────────┐
│ server: ● connected │ provider: gemma-4 (loaded) │ gpu: 74% │ cwd │
├─ session list ────────────────────────────────────────────────────┤
│ ▶ [abc123] "fix auth bug"          ACTIVE   stage 3/5  2m ago     │
│   [def456] "refactor context"      PAUSED   awaiting approval     │
│   [ghi789] "add tool registry"     DONE     5m ago                │
│   [jkl012] "write tests for..."    FAILED   12m ago               │
│   [mno345] "epic 11 wiring"        DONE     1h ago                │
├─ active session ──────────────────────────────────────────────────┤
│ stage: execute_plan (3/5)                                          │
│ last output: "FileEditTool applied patch to src/main/..."          │
│ tool: FileEditTool ████████░░ T2                                   │
├─ approval prompt (when pending) ──────────────────────────────────┤
│ ⚠ APPROVAL REQUIRED — Tier T3                                      │
│ tool: ShellTool  argv: ["rm", "-rf", "build/"]                     │
│ risk: HIGH — DestructiveOperation, RepeatedFailure(3/3)            │
│ [A] approve  [R] reject  [S] steer  [D] details                    │
├─ input ────────────────────────────────────────────────────────────┤
│ > _                                                                │
├─ keybinds ────────────────────────────────────────────────────────┤
│ n new  r resume  c cancel  a approve  q quit  ? help  , config    │
└───────────────────────────────────────────────────────────────────┘
```

**keybinds:**
- `n` — new session (prompts for workflow selection)
- `r` — resume selected session
- `c` — cancel selected session
- `a/r/s` — approve/reject/steer (only active when approval pending)
- `↑↓` — navigate session list
- `enter` — select/expand session
- `q` — quit
- `,` — open config
- `?` — help overlay

**state management:** Mosaic reactive state. server events update state via websocket listener coroutine. no polling.

**files:**
- `apps/tui/src/main/kotlin/.../TuiApp.kt` — Mosaic entry point
- `apps/tui/src/main/kotlin/.../components/StatusBar.kt`
- `apps/tui/src/main/kotlin/.../components/SessionList.kt`
- `apps/tui/src/main/kotlin/.../components/ActiveSession.kt`
- `apps/tui/src/main/kotlin/.../components/ApprovalPrompt.kt`
- `apps/tui/src/main/kotlin/.../components/InputBar.kt`
- `apps/tui/src/main/kotlin/.../ws/TuiWsClient.kt` — websocket client
- `apps/tui/src/main/kotlin/.../state/TuiState.kt`

**acceptance criteria:**
- status bar updates within 1s of provider state change
- session list shows last 5 sessions with correct status
- approval prompt appears immediately on `ApprovalRequired` message
- approve/reject/steer sends correct `ApprovalResponse` to server
- disconnect from server → status bar shows reconnecting state, no crash

---

## task 5: apps/cli — Clikt entrypoint

**dependencies:** task 2, task 3

**commands:**
```
correx run --workflow <path> [--session <id>] [--auto-approve]
correx session list [--limit 20]
correx session resume <id>
correx session cancel <id>
correx session events <id> [--from <eventId>]
correx approve <session-id> --decision <approve|reject|steer> [--note "..."]
correx status
correx provider list
correx config [--edit]
```

**output format:**
- default: human-readable, colored
- `--json` flag: machine-readable JSON on stdout for all commands
- `--quiet` flag: minimal output, only errors

**`correx run` behavior:**
- starts session, subscribes to websocket stream
- prints stage transitions and tool calls to stdout
- on approval required: prompts interactively if TTY, auto-denies if `--auto-approve` not set and no TTY
- exits 0 on session complete, 1 on failure, 2 on approval denied

**files:**
- `apps/cli/src/main/kotlin/.../CorrexCli.kt` — root Clikt command
- `apps/cli/src/main/kotlin/.../commands/RunCommand.kt`
- `apps/cli/src/main/kotlin/.../commands/SessionCommand.kt`
- `apps/cli/src/main/kotlin/.../commands/ApproveCommand.kt`
- `apps/cli/src/main/kotlin/.../commands/StatusCommand.kt`
- `apps/cli/src/main/kotlin/.../commands/ProviderCommand.kt`
- `apps/cli/src/main/kotlin/.../ws/CliWsClient.kt`

**acceptance criteria:**
- `correx run` exits with correct codes
- `--json` produces valid parseable JSON for all commands
- approval prompt works in TTY, auto-denies without TTY when flag absent
- `correx session list` output matches server `/sessions` response

---

## task 6: configuration

**shared config file:** `~/.config/correx/config.toml` (or `$CORREX_CONFIG`)

```toml
[server]
host = "localhost"
port = 8080

[tui]
theme = "dark"
session_list_limit = 5

[cli]
default_output = "human"

[approval]
timeout_ms = 300000  # 5 minutes
```

config accessible from both TUI (`,` keybind) and CLI (`correx config --edit`).

**files:**
- `apps/shared/src/main/kotlin/.../config/CorrexConfig.kt`
- `apps/shared/src/main/kotlin/.../config/ConfigLoader.kt`

---

## sequencing

```
task 1 (protocol)
    ↓
task 2 (server + websocket)
    ↓
task 3 (approval interaction)     ← depends on task 2
    ↓
task 4 (TUI)    task 5 (CLI)      ← both depend on tasks 2+3, parallel
    ↓
task 6 (config)                   ← shared, can be done alongside 4+5
```

---

## deferred to epic 14+

- router layer (L2/L3 memory bridge)
- workflow definition editor in TUI
- session diff viewer
- replay viewer (epic 15)
- GPU memory pressure indicator beyond simple % usage
- multi-server TUI (connecting to remote correx instances)
