11 KiB
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:
handleApprovalis nowsuspend— holds aCompletableDeferred<ApprovalDecision>per pending request, keyed byApprovalRequestId- emits
ApprovalRequestedEventbefore suspending so the server layer can surface the request id to clients DefaultSessionOrchestrator.submitApprovalDecision(requestId, decision)completes the deferred and resumes the workflow coroutineReplayOrchestrator.mapValidationOutcomeupdated tosuspendto match the parent signatureapprovalEngineremoved fromSessionOrchestrator(was evaluated synchronously; now decisions arrive externally)
files changed:
core/kernel/.../orchestration/SessionOrchestrator.ktcore/kernel/.../orchestration/DefaultSessionOrchestrator.ktcore/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 errorClientMessage— 5 subtypes: StartSession, ResumeSession, CancelSession, ApprovalResponse, PingApprovalDecision(protocol enum) —APPROVE,REJECT,STEER— separate from the domainApprovalDecisiondata classRiskSummaryDto,ProviderHealthDto,SessionConfigDto,PauseReason— protocol-only DTOsProtocolSerializer— encodesServerMessageto JSON, decodesClientMessagefrom JSON; unknown/malformed input throwsProtocolException(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.ktapps/server/src/main/kotlin/com/correx/apps/server/protocol/ClientMessage.ktapps/server/src/main/kotlin/com/correx/apps/server/protocol/Dtos.ktapps/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 modeGET/POST /sessions— list and start sessionsGET /sessions/{id}— single session statePOST /sessions/{id}/cancelGET /sessions/{id}/events?from=<eventId>&limit=50— paginated event replayGET /workflows— available workflowsGET /providers— provider health
websocket endpoints:
WS /sessions/{id}/stream— session-scoped streamWS /stream— global stream across all sessions
websocket behavior:
- on connect: snapshot of current state, then live events
- on reconnect with
?lastEventId=<id>: replays missed events from that point - 30s server heartbeat; client must respond within 10s or connection is dropped
- unknown client message →
ProtocolErrorsent, 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.ktapps/server/src/main/kotlin/com/correx/apps/server/ServerModule.ktapps/server/src/main/kotlin/com/correx/apps/server/routes/SessionRoutes.ktapps/server/src/main/kotlin/com/correx/apps/server/routes/WorkflowRoutes.ktapps/server/src/main/kotlin/com/correx/apps/server/routes/ProviderRoutes.ktapps/server/src/main/kotlin/com/correx/apps/server/ws/SessionStreamHandler.ktapps/server/src/main/kotlin/com/correx/apps/server/ws/GlobalStreamHandler.ktapps/server/src/main/kotlin/com/correx/apps/server/registry/WorkflowRegistry.ktapps/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: broadcastsServerMessage.ApprovalRequiredto all clients for that session - schedules a timeout job (
coroutineScope.launch { delay(timeoutMs) }) that auto-denies if no response withinApprovalConfig.timeoutMs(default 5 minutes) - on
ClientMessage.ApprovalResponse: cancels timeout job, translates protocol enum to domainApprovalDecision, callsorchestrator.submitApprovalDecision() - double-response guard via
ConcurrentHashMap<ApprovalRequestId, Boolean>— second response returnsProtocolError, no double-resume
protocol → domain mapping:
APPROVE→ApprovalOutcome.APPROVEDREJECT→ApprovalOutcome.REJECTEDSTEER→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.ktapps/server/src/main/kotlin/com/correx/apps/server/approval/ApprovalConfig.ktapps/server/src/main/kotlin/com/correx/apps/server/routes/ApprovalRoutes.kt
task 4 — tui (mosaic → tamboui)
implemented a new apps/tui Gradle module with an interactive terminal UI.
originally built on Mosaic 0.18; migrated to tamboui 0.2.1-SNAPSHOT after the
event-loop refactor (b95135e) split renderer-agnostic subsystems out. the
migration replaced the hand-assembled string frames with a real cell buffer,
widget catalog, and layout engine; domain core (state/, reducer/, ws/,
input/) was untouched. layout now matches the epic-13 reference mockup:
each panel sits in a titled ROUNDED Block, status row shows ●/○ +
provider + session count, session rows uppercase status with an "awaiting
approval" override, and the keybinds row pins to the bottom. see spec at
docs/specs/2026-05-17-tamboui-migration.md; commits b267982
(migration) and 46951ea (visual polish).
architecture:
- single WS listener coroutine updates
mutableStateOf(TuiState)— no polling - separate stdin reader coroutine sends
KeyEventto aChannel - key handler coroutine dispatches protocol messages to the server
- exponential-backoff reconnect (1s → 30s max) on server disconnect
applyServerMessagepure reducer mapsServerMessageto 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.gradleapps/tui/src/main/kotlin/com/correx/apps/tui/TuiApp.ktapps/tui/src/main/kotlin/com/correx/apps/tui/StateReducer.ktapps/tui/src/main/kotlin/com/correx/apps/tui/KeyEvent.ktapps/tui/src/main/kotlin/com/correx/apps/tui/state/TuiState.ktapps/tui/src/main/kotlin/com/correx/apps/tui/ws/TuiWsClient.ktapps/tui/src/main/kotlin/com/correx/apps/tui/components/StatusBar.ktapps/tui/src/main/kotlin/com/correx/apps/tui/components/SessionList.ktapps/tui/src/main/kotlin/com/correx/apps/tui/components/ActiveSession.ktapps/tui/src/main/kotlin/com/correx/apps/tui/components/ApprovalPanel.ktapps/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 <path> [--session <id>] [--auto-approve]correx session list/resume/cancel/eventscorrex approve <session-id> --decision <approve|reject|steer> [--note]correx statuscorrex 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-approveabsent - exit codes:
0complete,1failed,2approval denied
CliWsClient wraps the Ktor WebSocket client for stream subscriptions.
files created:
apps/cli/src/main/kotlin/com/correx/apps/cli/CorrexCli.ktapps/cli/src/main/kotlin/com/correx/apps/cli/CliConstants.ktapps/cli/src/main/kotlin/com/correx/apps/cli/ws/CliWsClient.ktapps/cli/src/main/kotlin/com/correx/apps/cli/commands/RunCommand.ktapps/cli/src/main/kotlin/com/correx/apps/cli/commands/SessionCommand.ktapps/cli/src/main/kotlin/com/correx/apps/cli/commands/ApproveCommand.ktapps/cli/src/main/kotlin/com/correx/apps/cli/commands/StatusCommand.ktapps/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):
[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.ktcore/config/src/main/kotlin/com/correx/core/config/ConfigLoader.kt
final architecture after Epic 13
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 --editcommand (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.