Commit Graph

103 Commits

Author SHA1 Message Date
kami 6feef150c9 feat: scaffold cross-session router memory (L3) with turbovec sidecar
Defines the L3MemoryStore contract in core:router (entry, query, hit,
in-memory test default) and a new infrastructure:router:turbovec module
that adapts turbovec via a Python sidecar over stdin/stdout JSON-Lines.

Adapter manages the subprocess via ProcessBuilder with lazy start,
mutex-serialized requests, and shutdown via withTimeout. Wire-format
metadata that turbovec doesn't store (sessionId, turnId, text) is kept
in an in-memory map for now — durable persistence ships with the
upcoming ChatTurnEvent.

Not yet wired into RouterFacade, InfrastructureModule, or DI. Embedding
generation source is a separate concern.
2026-05-30 00:40:27 +04:00
kami d276148e2c fix: store actual rendered prompt artifact instead of contextpack.toString()
Extract message-rendering logic from LlamaCppInferenceProvider into a
provider-agnostic PromptRenderer in core:inference. SessionOrchestrator
now serializes the rendered ChatMessage list as JSON for the prompt
artifact, so the audit trail reflects what the LLM actually saw.

Resolves the TODO at SessionOrchestrator.kt:691.
2026-05-30 00:32:38 +04:00
kami f922b855eb feat: add Go/Bubble Tea TUI rewrite (apps/tui-go)
Reimplement the TUI in Go using Bubble Tea, replacing the Kotlin/Tamboui
app. Same WebSocket protocol, soft-rounded layout with blue accent theme.
2026-05-30 00:00:35 +04:00
kami 1f6680f774 feat: convert router panel to conversation output log with user messages
Replace raw string routerMessages with structured RouterEntry(role, content)
entries. User messages are stored locally on SubmitInput (both IDLE and
IN_SESSION). Router responses arrive as 'router' entries via
RouterResponseMessage. Tool diffs arrive as 'tool' entries.

RouterPanel renders as a styled conversation log with role prefixes:
  ▸ user msgs in accent, router replies in strong body, tool output in dim.
Multi-line content (diffs, long responses) is split on \n for proper display.
Panel title changed from 'router' to 'output' to reflect the log role.
2026-05-29 21:47:58 +04:00
kami 1cad2bbde1 fix: set default capabilities on LlamaCpp provider to unblock router
LlamaCppInferenceProvider was created with empty capabilities, causing
NoEligibleProviderException when the router requested ModelCapability.General.
FirstAvailableRoutingStrategy requires declared capabilities to cover all
requested ones — empty set satisfied nothing.

Add a DEFAULT_LLAMA_CAPABILITIES constant with General, Coding, Reasoning,
Summarization, and ToolCalling scores, and pass it through createLlamaCppProvider()
to ModelDescriptor. Existing callers get sensible defaults automatically.

This is the root cause of the 'router not connected — epic 14' placeholder:
the server-side error (capability mismatch) sent a ProtocolError instead of a
RouterResponseMessage, so the TUI never set routerConnected=true.
2026-05-29 20:59:16 +04:00
kami 955f1a6987 feat: soft-rounded TUI layout with blue accent theme — all 9 phases
Complete TUI visual redesign: Theme.kt with full palette + 10 styles,
3-row grid layout with 1.7:1 horizontal split, redesigned StatusBar
with context meter, EventStreamPanel, FooterBar with contextual
keybinds, WelcomePanel for IDLE state, InputBar as left panel footer.
All existing components updated to use Theme colors. RouterPanel keeps
'not connected — epic 14' placeholder.
2026-05-29 20:53:17 +04:00
kami 35d5c24eae chore: add TODO for unbounded scrollOffset in diff view
The diffScrollOffset in NavUp/NavDown has no upper bound — it can
scroll past the last line. The ApprovalSurface clamps the viewport
with coerceIn/minOf, so it doesn't crash, but the offset keeps
growing. Marked with TODO(diff-scroll) in both handlers.
2026-05-29 17:48:27 +04:00
kami 2127824942 fix: implement stage_complete tool for LLM-driven stage completion
- Add STAGE_COMPLETE_TOOL constant and meta-tool handler in executeStage
  loop: when LLM calls stage_complete, break out of tool dispatch loop
  and proceed to normal validation/transition
- Inject stage_complete tool definition in every inference request so
  the LLM always has the option to signal stage completion
- Replace expand-all diff with scrollable diff window in approval
  surface (ctrl+x toggles, up/down scrolls, shows line range)
2026-05-29 17:40:51 +04:00
kami 7c55a53a9f fix: wire routerConnected flag, expandable diffs, stage in session list, ArtifactCreated mapping 2026-05-29 17:04:33 +04:00
kami fb3ab9b344 feat: wire ChatMode.STEERING toggle in TUI input bar
- Add ChatMode.STEERING toggle via ctrl+s keybinding
- Add KeyEvent.ToggleSteeringMode, Action.CycleChatMode
- TambouiKeyMapper: map ctrl+s to ToggleSteeringMode
- KeyResolver: route ToggleSteeringMode to CycleChatMode (global)
- InputReducer: toggle chatMode between CHAT and STEERING
- TuiState: add chatMode field (default CHAT)
- SessionsReducer: use ctx.chatMode instead of hardcoded CHAT
- RootReducer: pass afterInput.chatMode to SessionsReducerContext
- InputBar: show blue 'chat' or magenta 'steer' label in status line

Activation: ctrl+s toggles mode, 'steer' label appears, submitted
ChatInput uses ChatMode.STEERING → router emits SteeringNoteAddedEvent
→ context builder folds into next stage's context pack
2026-05-29 02:27:26 +04:00
kami 3981d8443d fix: complete P4 audit findings — steering wiring, generation config
P4-1: Wire steering to actually work (advisory-only):
  - RouterFacade emits SteeringNoteAddedEvent via event store in STEERING mode
  - SessionOrchestrator reads steering notes from event store and includes them
    as ContextEntry objects (sourceType=steeringNote) in stage context packs
  - Delete dead SteeringNote data class; single vocabulary = SteeringNoteAddedEvent
  - Add optional validateSteering callback to DefaultRouterFacade for future
    ValidationPipeline wiring (avoids cross-core dependency)
  - Update RouterFacadeTest to assert event emission

P4-2: Router FACE — move hardcoded generation config:
  - Add GenerationConfig field to RouterConfig with same defaults
  - RouterFacade uses config.generationConfig instead of inline literals
  - (Remaining P4-2 items: conversation persistence and coordination
    semantics are design decisions requiring ADRs, deferred)
2026-05-29 01:19:21 +04:00
kami f7f237e29f fix: complete all P3 audit findings — empty modules, docs drift, detekt, worktree
P3-1: Delete 17 empty Gradle modules (core:agents/observability/policies/stages,
      infrastructure:scheduler/security/telemetry, all 7 plugins/*, all 3
      interfaces/*). Zero source files, zero dependents, identical build.gradle
      stubs. Remove from settings.gradle. Keep .adoc spec docs as roadmap refs.
P3-2: Fix docs drift — rename :core:orchestration to :core:kernel in
      core-orchestration-submodule-spec.md, rewrite stale chat-transcript
      modules-and-spec.md as proper doc.
      (CLAUDE.md Router context isolation clarification is local-only,
       file is gitignored.)
P3-3: Reduce detekt maxIssues from 999999 to 120 (actual current count ~107).
      (CLAUDE.md detekt description updated locally; file is gitignored.)
P3-4: Remove stale locked worktree agent-a98d45277ce4b0040 (contained only
      cosmetic test style changes and an unused kotlinx-datetime dep).
2026-05-29 01:09:22 +04:00
kami 7936251d6b fix: complete all P2 audit findings — dead code, NPE guard, hygiene
P2-1: Guard SessionId NPE with safe-call in SessionsReducer
P2-2: Remove 16 dead event classes + reducer branches; replace with live orchestration events in tests
P2-3: Standardize ChatInput divergences — guard CancelSession, single-arg ProtocolError
P2-4: Remove dead TuiToolRecord.diff and ToolDisplayStatus.REQUESTED
P2-5: Remove dead streamLive from SessionEventBridge
P2-6: Extract SessionsReducerContext data class for 6+ param method
P2-7: Replace StageId("none") sentinel with TypeId.NONE
P2-9: Add TypeId.random() factory on all type-alias IDs
P2-10: Add KDoc on schemaVersion documenting reserved-for-migration
P2-8: Verified RouterReducer string-template already correct (TypeId value class toString)
2026-05-29 01:01:45 +04:00
kami 8ea3c381ef fix: P1 TUI UX batch — race root cause, diff content, per-session routerMessages, optimistic IDLE submit
P1-1: Replace check-then-act with computeIfAbsent for activeSessionJobs to
prevent double-launch. Register pendingApprovals[requestId] BEFORE emitting
ApprovalRequestedEvent to close the approve-before-register window.
P1-2: ToolCompleted with a diff now appends msg.diff to routerMessages
instead of the static "--- diff from $toolName ---" marker.
P1-3: Change routerMessages from List<String> to Map<String, List<String>>
keyed by sessionId.value. RootReducer appends to the correct session's list;
RouterPanel renders only routerMessages[selectedId].
P1-4: On IDLE chat submit, SessionsReducer creates an optimistic
SessionSummary(status="STARTING") and sets selectedId.
processSessionStartedMessage removes any STARTING session and inserts the
real one on echo. RootReducer sets sessionEntered=true for the optimistic
session.
Tests: LaunchRegistrationRaceTest, ApprovalRegisterBeforeEmitTest,
RootReducerTest (diff/per-session/isolation/optimistic-submit),
SessionsReducerTest (optimistic/reconciliation). All P0/P1 tests pass.
./gradlew check green.
2026-05-29 00:40:33 +04:00
kami eed557fe9c fix: address P0-4 chat zombie, P0-5 double SessionStarted, P0-6 steering wrong content 2026-05-28 23:10:29 +04:00
kami cdee5f2245 fix: harden event store, serialization, and grant engine; wire router chat
Event log integrity (sole source of truth):
- SqliteEventStore: serialize append/appendAll under a Mutex, enable WAL +
  busy_timeout + synchronous=NORMAL, roll back on any Throwable. Replaces the
  app-computed `SELECT MAX(seq)+1` read-modify-write that dropped events under
  concurrent appends (race was invisible to CI: only the in-memory store and
  single-threaded :memory: tests were ever exercised).
- Serialization: encodeDefaults + ignoreUnknownKeys, explicit @SerialName on all
  46 EventPayload subclasses and event-referenced enum constants, @Serializable on
  RiskLevel/RiskAction. Guards the append-only log against silent corruption from
  future class renames, field removals, or default-value changes.

Approval/grant security (Invariant: approvals cannot widen authority):
- Tier authorization is now a ceiling (request.tier.level <= max granted) instead
  of set membership; SESSION grants bind to a specific toolName instead of matching
  everything; handleCreateGrant rejects empty/over-tier/scopeless grants.

Router chat wiring (Phase 0-2): ChatInput round-trip, StartChatSession from IDLE,
router-panel layout, workflows behind Ctrl+W.

Tests: SqliteEventStore concurrency, serialization round-trip + discriminator
stability + unknown-key tolerance, adversarial grant scope/tier; updated existing
approval and reducer tests for the new grant semantics and StartChatSession effect.
2026-05-28 22:39:15 +04:00
kami 57d2237ba0 fix: orchestrator race conditions, diff preview, session snapshot tools, and test fixes
- Fix duplicate inference requests after approval resume by registering orchestrator
  jobs in activeSessionJobs (ServerModule.launchSessionRun), so the guard in the
  OrchestrationResumedEvent subscription prevents spurious duplicate resume.
- Replace blocking Files.readString in computeToolPreview with withContext(Dispatchers.IO)
  by making the function suspend — no blocking I/O on the coroutine dispatcher.
- Guard orderedIds.add() in rebuildTools against duplicate invocation IDs on replayed
  ToolInvocationRequestedEvent to prevent duplicate tool records in snapshots.
- Add unified-diff preview for file_write tools: computeToolPreview reads existing
  file and generates ---/+++ diff before emitting ApprovalRequestedEvent.
- Render diff preview with color coding in ApprovalSurface (@@ yellow, +/- green/red).
- Include current-stage tool records in SessionSnapshot via rebuildTools event replay.
- Fix RouterContextBuilderTest: recursive buildPack helper and 2 tests using class-level
  builder instead of their local builder instances (conversationKeepLast mismatch).
- Add suspend keyword to RouterFacadeTest mock, fix buildPack recursion.
2026-05-28 15:37:16 +04:00
kami e05532e7b2 feat: implement CreateGrant handler and grant-aware approval engine
- GlobalStreamHandler.handleCreateGrant validates scope (SESSION/STAGE)
  and sends ProtocolError to client on validation failure instead of
  silent log.warn + drop.
- Derive event stageId directly from GrantScope type, removing
  redundant stageIdForEvent tuple.
- SessionOrchestrator integrates ApprovalEngine to check active grants
  before requesting user approval for T2+ tool calls.
- ContextEntry gains EntryRole (SYSTEM/USER/ASSISTANT/TOOL) field for
  proper chat message role mapping.
- RouterContextBuilder.build is now suspend; uses Tokenizer for
  accurate token estimation with fallback to character-based estimate.
- LlamaCppInferenceProvider maps EntryRole to ChatMessage role instead
  of heuristic layer-based inference.
2026-05-28 14:06:58 +04:00
kami 92bea6c2c4 fix: PAUSED resume after server restart, diff viewer only shows last tool 2026-05-26 22:26:56 +04:00
kami 1af42befca feat: resume abandoned sessions on server restart
Three coordinated changes:

1. DefaultOrchestrationReducer now handles TransitionExecutedEvent,
   updating currentStageId so the projected state always reflects the
   actual current stage rather than staying stuck at the start stage.

2. DefaultSessionOrchestrator.resume() re-enters the execution loop at
   the current stage without re-emitting WorkflowStartedEvent. It reads
   currentStageId from the projected state, creates an ExecutionContext
   there, and calls enterStage → step exactly as run() does after its
   first stage.

3. ServerModule.start() calls resumeAbandonedSessions(), which scans all
   RUNNING/PAUSED sessions on startup and launches resume() for each one
   whose orchestrator is no longer in memory. Sessions that have a live
   deferred are unaffected; only sessions with no coroutine (e.g. the
   server was restarted while a tool was executing or approval was pending)
   are picked up and re-executed from their current stage.
2026-05-26 21:54:22 +04:00
kami bedd6e020c fix: guard stuck-session fix against OrchestrationPaused/ApprovalRequested race
When a TUI client connects at the exact moment between an
OrchestrationPausedEvent being stored and its corresponding
ApprovalRequestedEvent being stored, the approval projector sees empty
pendingApprovalRequests while orchState.pendingApproval=true. The
stuck-session fix would then append a spurious OrchestrationResumedEvent,
clearing the TUI's pendingApproval display and hiding the approval dialog.

Fix: count OrchestrationPausedEvents vs ApprovalRequestedEvents in the
event log. If there is an unpaired pause (more pauses than requests),
the session just entered the gate — skip the fix entirely.

Also remove spurious ToolExecutionCompleted/Failed emissions from the
kernel orchestrator; those events are emitted by SandboxedToolExecutor
in the infrastructure layer. Keep ToolExecutionRejectedEvent which the
orchestrator does own (rejection happens before executor.execute is
called).
2026-05-26 21:41:56 +04:00
kami 32d4cfa5dc fix: emit ToolExecutionCompleted/Failed/Rejected events after tool dispatch
ToolInvocationRequestedEvent was emitted but the corresponding outcome
events were never emitted, so the TUI permanently showed tools as
RUNNING. Now the orchestrator closes every tool invocation with the
appropriate event:
- ToolExecutionCompletedEvent (Success path)
- ToolExecutionFailedEvent (Failure path)
- ToolExecutionRejectedEvent (approval denied path)
2026-05-26 21:17:07 +04:00
kami 766b91081a fix: unblock sessions stuck in PAUSED state after approval resolved
Sessions that had an approval resolved (or never needed one) but never
received OrchestrationResumedEvent would show the last tool as RUNNING
forever with no subsequent events. Three coordinated fixes:

1. SessionOrchestrator now emits OrchestrationResumedEvent in both the
   approved and rejected approval branches, so the paused flag clears
   correctly going forward.

2. SessionEventBridge.replaySnapshot() detects the stuck pattern
   (pendingApproval=true with no unresolved requests) and appends a
   synthetic OrchestrationResumedEvent so historical sessions self-heal
   on next TUI connect.

3. DomainEventMapper maps OrchestrationResumedEvent → SessionResumed;
   TUI SessionsReducer/SnapshotPhaseReducer handle the new message,
   clearing pendingApproval and setting status back to ACTIVE.

Also adds server-restart recovery path in DefaultSessionOrchestrator
(emit decision + resume events directly when no live deferred exists)
and pre-registers pending approvals in ServerModule.start() to close
the race between reconnect and ApprovalResponse routing.
2026-05-26 21:12:26 +04:00
kami e32bd121e5 fix: store ProcessResult artifact for every shell tool outcome, not only FATAL
Move ProcessResult artifact content storage into dispatchToolCalls so it
fires for all outcomes (success, ERROR, FATAL) — the spec mandates the
artifact MUST always be produced.

- dispatchToolCalls now stores ProcessResult JSON in CAS + cache for
  every tool call result, using the current toolCall's function.arguments
  as the command field (not firstOrNull)
- Replace storeFailedProcessResult with emitProcessResultEvents — event
  emission only, content already stored by dispatchToolCalls
- FATAL and ERROR branches both now emit artifact lifecycle events,
  fixing the gap where retryable ToolResult.Failure produced no artifact
2026-05-26 17:33:07 +04:00
kami 3b24c7e91a feat: shell execution correctness + artifact_field_equals condition
- Add  field to ProcessResultArtifact ("success"/"failed")
- Materialize ToolResult.Failure as a failed ProcessResult artifact
  in the orchestrator, with artifact lifecycle events emitted
- Non-recoverable ToolResult.Failure now fails the stage ("FATAL:")
  instead of being silently fed back as LLM context
- Fix enterStage to return Continue on non-retryable failures,
  enabling back-edge transitions (fix/retry loops)
- Add ArtifactFieldEquals transition condition with flat and
  nested dot-notation field access + FieldOperator enum
  (eq, neq, gt, gte, lt, lte)
- Wire artifact_field_equals through ConditionFactory and
  TomlWorkflowLoader (condition_field, condition_operator)
- Cache ProcessResult artifact content for evaluation context
- Add 16 tests covering all condition operators and error cases
- DefaultTransitionResolver catches condition evaluation errors
  and returns Blocked instead of crashing
2026-05-26 17:28:51 +04:00
kami 9734eec63c docs: add module documentation in AsciiDoc with PlantUML diagrams
Generated by per-group subagents covering all 52 modules across:
core (18), infrastructure (10), apps (5), interfaces (3),
plugins (7), testing (9)

Documentation format:
- AsciiDoc (.adoc) files in docs/modules/<group>/
- PlantUML (.puml) files in docs/diagrams/
- .adoc files reference diagrams via include:: directives

Each doc covers: purpose, responsibilities, non-responsibilities,
key types, event flow, integration points, invariants, PlantUML
diagram, known issues, and open questions.
2026-05-26 16:59:21 +04:00
kami eadf23c945 fix(server): send SessionStarted/StageToolManifest before launching orchestrator
Reordered handleStartSession so protocol messages are sent before the
orchestrator launches in module.moduleScope. If the WS is already closed,
session.send() throws and the orchestrator never launches — avoiding silent
data loss where the session runs but the client never learns it exists.
2026-05-26 16:03:13 +04:00
kami e3812330d2 fix: resolve four findings from code review
- Stop silently booting user from session view on completion
  (SessionsReducer.processSessionCompletedMessage no longer deselects)
- Wrap StageToolManifest send in runCatching to survive WS disconnect
  after orchestrator launch in module scope
- Fix navigateUp dead state: wrap to last workflow instead of -1
- Consolidate event store reads in SessionEventBridge (single read
  for both recentEvents and approval state), removing
  DefaultApprovalRepository dependency
2026-05-26 16:01:57 +04:00
kami 3d95a946a8 fix(server): run orchestrator in module scope to survive WS disconnect 2026-05-26 14:51:09 +04:00
kami 2165d1b899 feat(tui): diff viewer, workflow picker, cursor support, and approval reconnect fix
- Add unified diff viewer for file_write/file_edit tools across all layers:
  ToolReceipt.diff → DiffUtil (LCS-based) → SandboxedToolExecutor → ToolCompleted
  → TuiToolRecord.diff → DiffViewer widget (colored +/- lines)
- Add workflow selection panel: WorkflowList server message, WorkflowListPanel
  widget, ↑↓ seamlessly extends into workflow picker from session list
- Add cursor/caret support: inputCursor in TuiState, CursorLeft/CursorRight
  actions and key events, AppendChar inserts at cursor, Backspace deletes before
- Colorize event history strip (green/red/yellow/blue by type) and input bar
  (blue/yellow prompt, cyan session name, blue keybindings)
- Show 5 recent events instead of 3; increase event strip height to 6
- Remove lastEventAt sort so display and navigation use consistent order
- Fix approval reconnect: register pending approvals with ApprovalCoordinator
  during snapshot replay so lookupSession works after reconnect
- 167 tests pass (126 TUI + 41 server)
2026-05-26 14:44:27 +04:00
kami d701e3cbf3 fix(tui): use takeLast(3) for events, add bridge/TUI tests for snapshot recentEvents
- Change EventHistoryStrip take(3) to takeLast(3) so it shows the 3 most
  recent events instead of the 3 oldest (chronological order is oldest-first)
- Add bridge test confirming replaySnapshot populates recentEvents from the
  event store (with 3 different event types mapped correctly)
- Add TUI SessionsReducer test confirming processSessionSnapshotMessage maps
  EventEntryDto entries from the snapshot into TuiEventEntry on the summary
2026-05-26 13:39:11 +04:00
kami 450b492937 fix(tui): status bar model, snapshot events, session-start selection
Three fixes:

1. Status bar now shows model/provider from ProviderStatusChanged:
   - RootReducer sets currentModel and providerType when the message arrives
   - providerType is LOCAL for llama/local providers, REMOTE otherwise
   - Removes the stale '(no model) (local)' display

2. SessionSnapshot now carries recent event history:
   - New EventEntryDto in the protocol with timestamp/type/detail
   - SessionSnapshot.recentEvents populated by bridge from event store
   - Bridge reads last 7 display-relevant events per session
   - TUI processSessionSnapshotMessage maps them to TuiEventEntry
   - Completed sessions finally show their event history

3. SessionStarted now switches selectedId to the new session:
   - RootReducer cross-field weave updates selectedId alongside sessionEntered
   - Previously kept the old selectedId, navigating user into the wrong session
   - Works whether starting a session from the list or during replay
2026-05-26 13:22:48 +04:00
kami 6770e3bf0a fix(tui): Enter on selected session now enters session view
KeyResolver was swallowing Enter in IDLE state when input text was blank,
so SubmitInput never reached RootReducer to set sessionEntered=true.
Now Enter always dispatches SubmitInput in IDLE — RootReducer handles
blank text by entering the selected session, non-blank by starting a
new session (as before). IN_SESSION and FILTER mode unchanged (blank
Enter still returns null there).
2026-05-26 13:13:57 +04:00
kami c142f146d4 fix(approval): resolve requestId→sessionId lookup in global socket handler
Fix the remaining TODO from 2e3b8e5: replace the TypeId(requestId.value) hack
with a proper requestId→sessionId mapping stored in ApprovalCoordinator and
populated when onApprovalRequested fires from the event store subscription.

Changes across the full bugfix batch (all 9 bugs from TUI rework):
- Bug 1: ApprovalCoordinator broadcasts non-null toolName/preview
- Bug 2: ApprovalDto carries toolName/preview for late-connected clients
- Bug 3: SessionEventBridge.replaySnapshot() preserves toolName/preview
- Bug 4: TUI SessionsReducer processes enriched ApprovalDto fields
- Bug 5: DomainEventMapper passes sessionSequence; GlobalStreamHandler
  tracks per-session counters so live events aren't dropped
- Bug 6: KeyResolver restores Ctrl+L/E/C/N bindings
- Bug 7: SessionSnapshot carries workflowId for display names
- Bug 8: ↑↓ navigates selection only; Enter/sessionEntered gates IN_SESSION
- Bug 9: EventHistoryStrip capped at 4 lines to prevent overflow
- Bonus: ApprovalCoordinator.lookupSession() with proper requestId→sessionId mapping
2026-05-26 13:09:10 +04:00
kami f6ef028883 feat(tui): session display model with DisplayState, input history nav, and StageToolManifest
Restructure the TUI around a DisplayState enum (IDLE, IN_SESSION, APPROVAL) replacing
the previous InputMode-based navigation (NAVIGATE, STEER removed). Key changes:

- **DisplayState model**: New `DisplayState` enum governs per-mode key resolution and UI
  rendering. Removes `ROUTER`/`NAVIGATE`/`STEER`/`FILTER` InputMode complexity.
- **Input history navigation**: Up/Down arrows in IN_SESSION cycle through per-session
  input history (ARCH-HISTORY-2/3). `savedInputBuffer` preserves in-flight text.
- **Input history filter**: `Tab` toggles between ROUTER and FILTER modes; history nav
  disabled while filtering.
- **Ctrl-C → Cancel**: Rebind Ctrl-C from Quit to Cancel across all states.
- **Approval flow**: `ApproveActive`/`RejectActive` moved from SessionsReducer to
  RootReducer via `pendingDecision`/`SubmitApprovalDecision`. Approval overlay
  replaced by `ApprovalSurface` component.
- **Event history strip**: Replaces old overlay with a persistent `EventHistoryStrip`
  component, toggled via Ctrl-E.
- **Background update badge**: New `backgroundUpdateCount` on SessionsState tracks
  events arriving for non-selected sessions (ARCH-BADGE-1).
- **StageToolManifest**: New `ServerMessage.StageToolManifest` and `ToolManifestEntry`
  model for pre-declared tool shapes per stage (RF-3).
- **Cleanup**: Removed `ActiveSession`, `ApprovalPanel`, `ToolsPanel` components.
  Removed `InputMode.NAVIGATE`, `InputMode.STEER`, approval overlay state fields.
  Various reducer simplifications (no more overlaid effects for approval responses).
2026-05-26 01:46:14 +04:00
kami 2e3b8e599c chore(approval): TODO markers for overlay UX and requestId→sessionId lookup
Two follow-ups for the approval refactor:
- InputReducer STEER input mode hardcodes APPROVE; needs the future
  approve/reject overlay UX to let the user pick.
- GlobalStreamHandler derives scopeSessionId from requestId as a
  placeholder; needs a real requestId → sessionId lookup once exposed.
2026-05-25 19:56:07 +04:00
kami 5abf7d1253 refactor(approval): wire ApprovalCoordinator end-to-end
ApprovalCoordinator (added in epic-13) was never instantiated. Its four
capabilities — multi-client fan-out, single-resolver dedupe, request
timeout, and a unified domain mapper — were duplicated piecemeal across
GlobalStreamHandler and SessionStreamHandler, with a STEER-outcome
divergence between them (coordinator mapped STEER → APPROVED while
GlobalStreamHandler mapped STEER → REJECTED). This refactor wires the
coordinator and removes the duplication.

Protocol: drop ApprovalDecision.STEER. STEER is no longer a discriminator;
it's expressed as (APPROVE | REJECT) + non-null steeringNote, so the user
can steer alongside either choice. TUI's current single-keybind STEER
input flow now sends APPROVE + note (matching the original epic-13
intent); a future overlay UX will let the user pick APPROVE or REJECT
explicitly.

Mapper: ClientMessage.ApprovalResponse → DomainApprovalDecision lives in
a single shared `toDomain` helper (apps/server/approval/
ApprovalResponseMapper.kt). Both stream handlers and the coordinator
use it.

Config: delete the duplicate apps/server/approval/ApprovalConfig.kt;
ApprovalCoordinator now takes core.config.ApprovalConfig (already had
timeoutMs=300_000L). ConfigLoader already populates it.

Wiring:
- ServerModule owns a SupervisorJob-backed moduleScope, constructs
  ApprovalCoordinator, and on start() subscribes eventStore.subscribeAll()
  to route ApprovalRequestedEvent payloads to onApprovalRequested.
- GlobalStreamHandler delegates ApprovalResponse to
  approvalCoordinator.handleResponse and registers/unregisters via the
  new registerGlobalClient/unregisterGlobalClient (global sockets aren't
  bound to one SessionId, so broadcasts union session and global sets).
- SessionStreamHandler delegates ApprovalResponse and registers per-session
  via registerClient/unregisterClient.
- Timeout is on by default at 5 minutes via config.timeoutMs.

Tests: ApprovalCoordinatorWiringTest covers delegate path, dedupe
producing ProtocolError, and event-stream subscription routing.
2026-05-25 19:02:51 +04:00
kami 09f47bf8e3 fix(cleanup-04): wire ApproveActive/RejectActive and clear pendingApproval atomically
The cleanup-04 spec's write side was missing: Action.ApproveActive and
Action.RejectActive were dispatched by KeyResolver but had no reducer
branch, making the approve/reject keybindings silent no-ops. STEER
SubmitInput emitted an ApprovalResponse effect without clearing
`pendingApproval` on the selected session, violating the spec invariant
that the effect emission and the clear happen in the same tick.

SessionsReducer now handles both actions via a private
respondToSelectedApproval(sessions, decision, note) helper that reads
state.selectedPendingApproval(), clears the field, and emits
Effect.SendWs(ApprovalResponse(...)). InputReducer's STEER SubmitInput
branch clears `pendingApproval` on the selected session in the same
returned TuiState as the response effect.

Adds four tests covering APPROVE, REJECT, no-pending no-op, and
STEER atomicity.
2026-05-25 17:18:10 +04:00
kami c8a599c64f fix(tui-02): route ApprovalResponse through global socket
Complete the tui-02 spec acceptance criteria: delete the orphan
`connectSession`, `disconnectSession`, `sessionStreamJob`, and
`sessionWsSession` members from TuiWsClient, and remove the `send()`
fallback that previously routed ApprovalResponse over a per-session
socket. All ClientMessage types now travel the single global socket.

Server side: GlobalStreamHandler.handleClientMessage now actually
handles ApprovalResponse (was returning a protocol error), converting
to a domain ApprovalDecision and dispatching to the orchestrator —
mirrors SessionStreamHandler's prior logic.

Adds ApprovalResponseGlobalSocketRoundTripTest covering APPROVE and
STEER encode/decode through ProtocolSerializer.
2026-05-25 17:18:01 +04:00
kami 8f20f6aa24 fix(server-05): synchronize subscription before snapshot read
Close the narrow race where `streamGlobal` launched the subscribeAll
collector and immediately read `lastGlobalSequence()` — events appended
in that window were dropped by the replay=0 SharedFlow before the
collector actually subscribed. Now use a CompletableDeferred barrier
signaled via `onSubscription` (SharedFlow) or `onStart` (cold fallback),
awaited before `bridge.replaySnapshot()`.

Closes the remaining gap on finding #1 from
docs/specs/2026-05-24-review-fixes.
2026-05-25 17:17:32 +04:00
kami cdc03b8809 feat(effects-02): route Action.Quit through sub-reducers; Effect.Quit last
Effect.Quit is appended at the tail of the effect list rather than
prepended, so all sub-reducer cleanup effects dispatch before runner
teardown. Combined with the sequential dispatch from effects-01, list
position is a runtime guarantee.

Closes finding #9.
2026-05-25 16:58:33 +04:00
kami 5effe287d4 feat(effects-01): sequential effect dispatch in TuiApp
Replace per-effect launch with a single launch that iterates effects
sequentially via dispatchAll, closing the ordering race (I-E1 invariant).
Add EffectDispatchOrderTest proving sequential preserves order while
fire-and-forget does not.
2026-05-25 16:49:10 +04:00
kami 9ff58b36ad refactor(cleanup-05): remove flat approval/status fields from SessionSnapshot 2026-05-25 16:30:21 +04:00
kami 9dcf6f4c04 refactor(tui): extract selectedPendingApproval() extension and unify call sites
Add TuiState.selectedPendingApproval() extension and replace the three
inline session-lookup expressions in InputReducer and RouterPanel with it.
2026-05-25 16:26:03 +04:00
kami 7d4468f292 refactor(cleanup-03): delete ApprovalReducer and simplify RootReducer
Remove the approvalAction/NoOp wrong-scope filter and the ApprovalReducer
call site from RootReducer. approvalJustArrived is now derived from the
selected session's pendingApproval delta. Delete ApprovalReducer.kt,
ApprovalState.kt, and ApprovalReducerTest.kt; approval truth is
structurally owned by SessionSummary.
2026-05-25 16:18:12 +04:00
kami 1c67993f5d refactor(cleanup-02): move ApprovalRequired write-side into SessionsReducer
SessionsReducer now handles ServerMessage.ApprovalRequired and populates
SessionSummary.pendingApproval directly on the matched session. The
SessionSnapshot handler also propagates approval fields to pendingApproval.
ApprovalReducer's ServerEventReceived branch is removed; approval truth is
now strictly scoped to a single SessionSummary.
2026-05-25 14:52:26 +04:00
kami d2c6789c4d refactor(cleanup-01): move approval truth to SessionSummary.pendingApproval
Add pendingApproval: ApprovalInfo? = null to SessionSummary and remove
the global approval: ApprovalState slot from TuiState. All production
code derives active approval from the selected session; RootReducer
bridges ApprovalState in/out of the selected session for the duration
of the existing ApprovalReducer (removed in Cleanup 03).
2026-05-25 14:24:20 +04:00
kami cba0314197 refactor(tui-04): remove ConnectSession and DisconnectSession from Effect hierarchy 2026-05-25 12:39:50 +04:00
kami 8b8f9c9379 test(tui-04): assert single-consumer contract for ws.messages and ws.connection 2026-05-25 12:33:49 +04:00
kami de069dbf6a refactor(tui-03): remove ConnectSession from SessionStarted, keep on SessionSnapshot
SessionSnapshot is the correct trigger for connectSession() — the client
has enough state to stream live events at that point. SessionStarted fires
pre-snapshot and no longer needs to emit ConnectSession.
2026-05-25 12:25:58 +04:00