epic-13: add cli and tui entry point, finish epic.
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
# 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<ApprovalDecision>` 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=<eventId>&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=<id>`: 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<ApprovalRequestId, Boolean>` — 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 <path> [--session <id>] [--auto-approve]`
|
||||
- `correx session list/resume/cancel/events`
|
||||
- `correx approve <session-id> --decision <approve|reject|steer> [--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.
|
||||
@@ -0,0 +1,326 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user