Commit Graph

266 Commits

Author SHA1 Message Date
kami c616982b7b feat(context): inject CLAUDE.md / AGENTS.md as L0 standing context
On session start, discover CLAUDE.md and AGENTS.md at the bound workspace root and inject
their (concatenated, header-labeled) contents as an L0 system context entry for every stage —
mirroring the .correx/project.toml ProjectProfile path end to end. Recorded as
AgentInstructionsBoundEvent (invariant #9) so replay reads the recorded fact, not the live
file; folded into SessionState.boundAgentInstructions and rendered by buildAgentInstructionsEntry
right after the project profile. AgentInstructionsLoader reads root-only, both files if present.
Bound via ServerModule.bindAgentInstructions alongside bindProjectProfile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 10:45:43 +00:00
kami 04d7e26482 feat(tui-go): @ file-reference picker
Typing `@` at a word boundary in the chat input (mid-word `@` stays literal, e.g. emails)
opens a workspace file picker: it requests file.list for the session (cached per session),
filters as you type, and enter inserts `@path ` at the cursor — back to typing. Renders as
a transparent overlay, windowed around the selection. file.list is classified non-rendering
in the render-matrix guard. Tests cover token-boundary detection, ref insertion, and filtering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 10:24:57 +00:00
kami 65e5d88ed7 feat(server): ListFiles/FileList protocol for the TUI @ file picker
New WS query: ClientMessage.ListFiles{sessionId} -> ServerMessage.FileList{paths}
(@SerialName "file.list"). StreamQueries.listFiles resolves the session's bound
workspace root (latest SessionWorkspaceBoundEvent) and lists it via WorkspaceFiles —
a path-only walk that reuses RepoMapIndexer's directory-ignore convention but, unlike
the indexer, never reads file contents and includes every file type (configs/assets,
not just source+markdown), sorted and capped. Tests: WorkspaceFiles (types/ignore/cap/
missing) + file.list wire round-trip. Go @ autocomplete consumes this next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 10:19:21 +00:00
kami ba7ac06c16 feat(tui-go): transcript message-jump + OSC52 copy
ctrl+↑/↓ jumps between your sent (user) messages in the transcript: ctrl+↑ from the
bottom selects the most recent, steps to older ones (clamping); ctrl+↓ steps back and
drops to tail-follow past the last. The selected message is highlighted and scrolled
into view (routerRows tracks per-message row offsets and windows to the selection).

`y` yanks the selected message — or the latest turn when none is selected — to the
system clipboard over OSC52 (go-osc52, tmux-wrapped under $TMUX), which is SSH/Termius-
safe unlike a mouse drag. A brief "✓ copied" footer note animates out (and self-stops
the tick). esc drops the selection; switching sessions clears it. Tests cover the
user-message jump/clamp/tail and the copy-text selection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 09:59:29 +00:00
kami 26621567ac feat(tui-go): transparent modal backdrops
Modals now composite over a dimmed copy of the screen behind them instead of an opaque
scrim, so the transcript stays faintly visible around a modal (opencode-style). center()
stashes the frame's base (View sets m.lastBase), strips+dims it to a faint scrim, and
splices the modal box over it (ANSI-aware via ansi.Truncate/TruncateLeft so the side
margins keep the dimmed content). An idempotence guard passes an already-full-screen
modal through unchanged, so the existing double-center builders don't get re-dimmed.
Geometry tests assert the composite still fills the screen exactly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 09:40:52 +00:00
kami 85bfddf1c4 feat(tui-go): slash command menu in the chat input
Typing `/` as the first char of a chat line opens the command palette inline
(opencode-style), instead of typing a literal slash; mid-line `/` stays literal.
Factors openPalette() (shared with the `p` key) and advertises `/ cmds` in the
insert-mode footer when the line is empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 09:35:34 +00:00
kami 9143d554f1 feat(tui-go): global input history + stop idle redraws (copy-safe)
Input history is now a single global ring (recordInput) shared by every input surface —
free-chat, the workflow-intent line, and in-session chat — so ↑/↓ recalls anything you've
sent, including outside a session (previously per-session, so idle/intent had none).

Copy fix: the 120ms animation tick now only runs while something actually animates
(animating(): active session spinner, blinking caret, reconnect). Init no longer starts it
unconditionally; tickKick (re)starts it on demand and tickMsg stops it when idle. An idle
screen no longer auto-redraws, so a native terminal text selection survives instead of being
wiped every frame. Tests cover the ring (dedup/cap/cross-context walk) and the idle gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 09:33:16 +00:00
kami 9eef936073 fix(health): llama probe also covers the managed [[models]] path (A§4)
64a90e7 gated the probe on a static [[providers]] llamacpp url, missing the primary
managed path (sample-config.toml's [[models]], where correx spawns llama-server at
[models].host:port and registers no provider entry). Fall back to that host:port when
[[models]] is configured so the LLAMA_SERVER subject is monitored on the common setup.
Surfaced while drafting the A§4 live-QA plan. Compile green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 08:36:11 +00:00
kami 64a90e74a9 feat(health): register LlamaServerHealthProbe (A§4)
Wire the built-but-unregistered LlamaServerHealthProbe (266bbf0) into the health monitor:
when a `llamacpp` provider URL is configured, add an HttpLlamaLivenessClient-backed probe
(dedicated CIO client, 3s per-ping timeout, shutdown-hook close) to the probe list. Gated
on a configured provider URL so a model-less box never reports a spurious LLAMA_SERVER
DEGRADED. Replaces the Main.kt TODO(wiring). main() is not unit-tested; the probe + client
are already covered by 266bbf0 — compile + :apps:server:test + detekt green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 08:25:25 +00:00
kami 4e08510045 feat(server): activate architect contradiction-check hook (B§4)
Wire the display-only ArchitectContradictionChecker (eae0a0c) live: ServerModule.start()
subscribes to the architect stage's `design` ArtifactCreatedEvent, resolves the decision
text (prefers the design schema's `approach` field; falls back to the capped artifact
text), runs checker.check(...), and appends the non-null PossibleContradictionFlaggedEvent.
Live-only and never halts a stage (runCatching). Checker built in Main.kt gated on
project.enabled (same L3 "project:" namespace ProjectMemoryService.persist populates) and
threaded into ServerModule as a nullable param. Replaces the standing TODO(wiring) in Main.kt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 08:07:33 +00:00
kami f107ff554c feat(router): idea-board -> project-profile promotion (E)
Promote an auto-captured idea from the cross-session board into the curated per-repo
profile at <workspaceRoot>/.correx/project.toml as a `conventions` entry. New
IdeaPromotedEvent tombstones the idea off the board (IdeaReader treats it like a
discard) and records the promotion as a fact; the server PromoteIdea handler loads
the profile, appends the idea text (deduped via ProjectProfile.withConvention), and
writes it back. Adds ProjectProfileWriter (escape-aware TOML serialize/write) and
makes SimpleToml's reader unescape \\ and \" symmetrically with the writers
(fixes a pre-existing read/write asymmetry surfaced by the round-trip test).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 07:30:43 +00:00
kami 4b0024f7d9 feat(research): batch source-list fetch-approval (D)
POST /sessions/{id}/approve-sources extracts distinct hosts from a source list
(SourceListHosts.extract) and emits one EgressHostsGrantedEvent for the session, so
the live per-session egress allowlist auto-clears subsequent web_fetches to those
hosts — approve the list once instead of per-URL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 07:07:23 +00:00
kami d5afcd345a feat(tui-go): approval ergonomics — queue + tier-gated confirm (E §3)
- pending approvals are a navigable queue (PendingQueue/PendingIdx; ↑↓/jk, count
  shown), no modal stacking; resolve removes the specific gate by request id
- y/n/e keys (approve/reject/steer); T0–T2 act on one key, T3+ requires arm+confirm
  ("press y again"), any other key disarms; reject/steer/auto never gated
- snapshot restore now rehydrates the full pending list, not just the first

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 16:40:49 +00:00
kami 2a99e15383 test(tui-go): per-event render-matrix golden tests (E §2)
Table-driven render assertions (37 cases over 42 protocol Type* constants), driven
through applyServer and asserted ANSI-stripped on stable defining text. Coverage
guard parses every Type* from protocol.go and fails if a kind is neither covered
nor explicitly classified non-rendering. Pins width/theme/timestamp; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 16:30:25 +00:00
kami 1f7a5d96a7 feat(tui-go): session list / resume view (E)
R opens a navigable overlay of recent sessions (GET /sessions: short id, status,
workflow/stage, relative age); enter resumes via POST /sessions/{id}/resume, which
replays onto the existing global /stream (no WS re-dial). Fetch/resume errors surface
in the overlay, never panic. stdlib-only (ws.Client.HTTPBase derives the base URL).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 16:22:27 +00:00
kami da72ee5b80 feat(tui-go): render markdown in chat turns (E)
renderMarkdown wraps charmbracelet/glamour (dark style, per-width memoized) and is
hooked into the router chat-turn render path; falls back to plain content on any
glamour error so the TUI never crashes or loses text. Other roles/UI untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 16:12:16 +00:00
kami 1e6699a360 fix(tui-go): go.mod directive 1.26 -> 1.24.2 (1.26 toolchain unreleased)
`go 1.26` is not a fetchable/released toolchain, so released Go cannot build the
module ("toolchain not available"). 1.24.2 is the minimum the deps require
(charmbracelet/x/ansi needs >= 1.24.2) and is a real toolchain; the directive is a
floor, so newer Go still works. Bump if a specific newer release is intended.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 16:07:36 +00:00
kami eff8f8dbdf fix(narration): drop stale pause narrations once approval resolves (G)
Pause narrations are tagged with a pauseKey; a per-session pending-pause set gates
the lane worker so a resolved/resumed pause is skipped instead of blending with the
current status. ApprovalDecisionResolved drops the specific request's pause;
OrchestrationResumed clears all session pauses. Replay-safe (no recorded events change).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 16:02:09 +00:00
kami eae0a0ccf6 feat(memory): architect contradiction-check (B§4, display-only)
- PossibleContradictionFlaggedEvent + RelatedDecision (registered + serialization test)
- ArchitectContradictionChecker: L3 similarity retrieval over prior decisions,
  threshold-gated, fake-testable; non-blocking (surfaces candidates only)
- live wiring left as documented TODO (needs an architect-decision emit hook +
  in-session decision embedding into L3)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 11:31:39 +00:00
kami f715ffa540 feat(repomap): index C#/Godot-Mono (.cs) type symbols
Conservative type-declaration pattern (class/interface/struct/enum/record);
no method capture to avoid false positives. (BACKLOG I)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 09:32:02 +00:00
kami 266bbf0269 feat(health): EventStore latency + llama-server liveness probes
- EventStoreLatencyProbe: times a side-effect-free lastGlobalSequence read,
  wired into HealthMonitor; HealthConfig.eventStoreWarnLatencyMs (BACKLOG A §4)
- LlamaServerHealthProbe: liveness + tokens/sec degradation trend, injectable
  client (HttpLlamaLivenessClient over Ktor); deterministic unit tests
- llama probe left unregistered (TODO) — HttpClient not in monitor scope yet

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 08:39:26 +00:00
kami 784accb08d fix(memory): trailing-delimiter turnId namespace + newline fingerprint
- repomap: tag prefix match requires trailing ':' so /repo can't match /repo2
  (L3RepoKnowledgeRetriever filter + ProjectMemoryService write; the existing
  existsByTurnIdPrefix check already included the delimiter)
- WorkspaceStateProbe fingerprint joins entries with newline to match docstring
- regression test for /repo vs /repo2 non-collision

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 08:08:49 +00:00
kami df76e7f2fb fix(memory): turnId repoRoot prefix collision (/repo vs /repo2)
The repo-map turnId tag and the L3 retriever filter used 'repomap:$repoRoot' with no
delimiter, so a retriever for /repo also matched /repo2 entries (startsWith). Add a trailing
':' to the tag (no-stateKey branch) and the retriever filter so the repoRoot boundary is
explicit. Test: /repo retrieval no longer leaks /repo2 entries, still matches its own
with/without a stateKey suffix.
2026-06-15 00:50:04 +04:00
kami 68136e77d7 feat(workflow): deterministic plan lint (plan-pipeline §5, Slice 1)
First slice of the plan-generation pipeline epic. A pure-Kotlin PlanLinter runs over the
compiled freestyle ExecutionPlan (WorkflowGraph) before it is locked, complementing
ExecutionPlanCompiler (which throws on unreachable/unknown-kind/bad-edge) with the checks
it does NOT make:

- HARD unproduced_need — a stage needs an artifact no stage produces. Real gap: only the
  TOML loader checked this; the freestyle compiler did not, so such plans compiled and
  failed at runtime.
- HARD trap_state — a stage with no path to the terminal (inescapable loop / dead end).
  Uses terminal-reachability, NOT "any cycle", so legitimate verdict-gated review loops pass.
- SOFT stage_count / fan_out / empty_brief / duplicate_brief — scored, never blocking.

FreestyleDriver lints after compile and records PlanLintCompletedEvent (pure function of
the recorded plan → replay-safe, no env observation in v1); a hard failure rejects the plan
(ExecutionPlanRejectedEvent source="lint") before the operator is asked or the plan locks,
so a broken plan never executes. Pays off now for single-plan freestyle; becomes the
per-candidate filter when multi-candidate generation (Slice 2) lands.

Deferred to later slices: file-path grounding + symbol resolution (needs an index),
token-budget/ADR-bypass checks, and a dedicated :core:planning module.
2026-06-15 00:26:22 +04:00
kami 365eb8a279 feat(kernel): static-first reviewer gate (role-reliability §5)
Run operator-configured static-analysis commands (compiler/detekt/formatters) as a
deterministic harness step on the producing stage, before the LLM reviewer. A stage
declares `static_analysis = [commands]`; after it produces its artifact, each command
runs in the workspace root and the results are recorded in a StaticAnalysisCompletedEvent
(invariant #9 env observation — recorded live, folded on replay, never re-run). A
non-clean command fails the stage retryably with its output fed back verbatim (§2:
compiler/test output is ground truth), so the implementer fixes it and only static-clean
code ever reaches the reviewer. This makes §5's "reviewer context excludes static findings"
mechanical — the findings are resolved upstream, so the reviewer can't waste inference on
what detekt catches for free; no output-parsing, no context plumbing.

- StaticAnalysisCompletedEvent + StaticAnalysisFinding (core:events) + registration.
- StaticAnalysisRunner seam + ProcessStaticAnalysisRunner (whitespace-split argv, stderr
  merged, timeout→nonzero exit) in core:kernel; wired (nullable) into OrchestratorEngines
  and the server. Null runner / no workspace root → logged no-op, never blocks the run.
- StageConfig.staticAnalysis + `static_analysis` TOML field; runStaticAnalysis folded into
  a runPostStageGates sequence (produces → grounding → echo → static analysis).
- Documented `static_analysis` on the role_pipeline implementer stage (commented; commands
  are workspace-specific). The reviewer prompt half shipped in 3467826.
2026-06-14 23:51:49 +04:00
kami fd67a6c68d feat(tui-go): health-checks pane
Surface the health subsystem (llama/event-store/disk probes) in the TUI,
mirroring the session-stats pane: ClientMessage.GetHealthChecks ->
ServerMessage.HealthChecks (health.checks, reuses HealthReport) ->
StreamQueries.healthChecks via HealthInspectionService; Go OverlayHealth on
key H + palette 'health checks', pull-based fetch, per-subject status with
degraded loud. Empty/disabled -> visible fallback (no blank/lie). Go+Kotlin
golden tests pin the wire contract field-for-field. Observability spec §4/§3.
2026-06-14 18:57:51 +04:00
kami 7a1362c973 feat(server): event-store health probe
Third HealthProbe (after disk + llama): times a cheap lastGlobalSequence()
read as an event-store responsiveness heartbeat — DEGRADED on throw or when
latency exceeds eventStoreLatencyWarnMs (default 500). Read-only by design
(no probe writes into the source-of-truth log; Invariant #1). Plugs into the
existing HealthMonitor; no new events. Observability spec §4.

Also wraps a long line in LlamaServerHealthProbeTest to clear a detekt
MaxLineLength finding introduced in aaf896d.
2026-06-14 18:40:29 +04:00
kami aaf896d8de feat(server): llama-server health probe
Second HealthProbe (after disk-watermark): liveness via GET /health on the
configured llama base URL + tokens/sec degradation watch derived from recorded
InferenceCompletedEvents (windowed average vs configurable threshold, opt-in via
llamaTpsWarnBelow). Plugs into the existing HealthMonitor edge-detection loop;
no new events. Observability spec §4.
2026-06-14 18:26:09 +04:00
kami 02e963d6cc feat(tui-go): idea board viewer
OverlayIdeas (opened with I, global/cross-session) lists the idea board in the
artifact-viewer shell: ↑↓ to move, x/d to remove (sends DiscardIdea, optimistic
drop), esc to close. Pull-based via ListIdeas -> idea.list.
2026-06-14 14:36:13 +04:00
kami 04e931c860 feat(server): idea board list + discard
ListIdeas -> idea.list (StreamQueries folds the board from the log, replay-neutral)
and DiscardIdea -> append IdeaDiscardedEvent landed in the capturing session, then
reply with the refreshed board. IdeaDto on the wire.
2026-06-14 14:33:11 +04:00
kami b563d9841d feat(tui-go): workflow-propose choice panel
Render a workflow.proposed frame as a single-select picker with a manual-answer
slot: ↑↓/jk to choose, enter to launch, e for custom, esc to peek away. Picking
a candidate sends StartSession (and focuses the launched session); the custom
slot continues the conversation via ChatInput. Reuses the modal helpers from the
clarification view.
2026-06-14 13:29:34 +04:00
kami b62b6e09a1 feat(server): workflow.proposed WS frame
Map WorkflowProposedEvent to a workflow.proposed ServerMessage carrying the
candidates + original request. No new ClientMessage: a pick reuses StartSession
(launch the chosen workflow seeded with the request) and the manual-answer slot
reuses ChatInput.
2026-06-14 13:29:01 +04:00
kami 218a98034e feat(tui-go): interactive clarification question view
Render stage clarification questions as an AskUserQuestion-style form:
header chip, prompt, selectable option chips, and a free-text custom slot.
Arrow/hjkl nav, space to select, e for custom, enter to submit. Submit
guards on all-answered and sends ClarificationResponse; the parked stage
re-runs with the answers in context.
2026-06-14 13:00:59 +04:00
kami 6242b590e4 feat(server): clarification WS protocol + wiring
Surfaces the clarification loop over the wire: ServerMessage.ClarificationRequired
(clarification.required, carrying the structured questions) is mapped from
ClarificationRequestedEvent; ClientMessage.ClarificationResponse carries the
operator's answers and is routed to orchestrator.submitClarification on both
the global and session streams. Reuses the core ClarificationQuestion/Answer
types as the wire shape. Round-trip tests for both directions.
2026-06-14 03:35:59 +04:00
kami 7a0d4d0ee2 feat(research): wire tools + research workflow graph (research-workflow §2/§3)
Makes the research feature runnable end-to-end, off by default.

- config: [tools.research] (enabled, searxng_url, max_results, max_fetch_bytes).
- registration: web_search/web_fetch are built into BOTH the default and per-workspace
  tool registries when research.enabled, sharing one HTTP client threaded from Main
  (none built on the static path). Egress stays harness-enforced: web_fetch is T2
  (operator-approved) and the existing NetworkHostRule still applies.
- workflow: examples/workflows/research.toml — decompose → gather → report, with the
  three artifact schemas and prompts. Fan-out (search per sub-question, fetch per source)
  runs as repeated tool calls inside the gather stage (Correx has no parallel agents);
  per-source synthesis into the dossier is the compression step, so the report stage
  consumes summaries, never raw pages. ResearchWorkflowTest validates the graph contract.

To run: set [tools.research].enabled, register the 3 [[artifacts]], copy research.toml +
prompts + schemas into the workflows dir, start SearXNG. Launch like any workflow (the
T2 fetch approval surfaces as an approval card; the report opens in the artifact viewer).

Follow-ups (noted, not blocking): batch fetch-approval at the source-list level (§3),
a dedicated SourceFetched/LowQualityExtraction event (quality + content hash are already
in tool-result metadata), dynamic per-session egress allowlist, and the web approval client (§6).
2026-06-13 23:18:19 +04:00
kami f8fd2601a8 feat(server,cli): event-sourced health checks — disk watermark probe (observability §4)
Health states are events. A HealthMonitor polls registered HealthProbes on an
interval and appends HealthDegraded/HealthRestored only on a status transition
(hysteresis), so health is observable, replayable, and renderable like every
other fact in the log. The probe reads nondeterministic environment state and
records it at observe-time (Invariant #9); HealthProjection rebuilds current
health by replay, never re-probing (Invariant #8). Monitor is live-only, started
in ServerModule.start alongside narration; seeded from the projection so a
restart does not re-emit a degraded the log already records.

First concrete probe: DiskWatermarkProbe — event-log + CAS size and growth rate,
warning before the 2am surprise. Read surface mirrors `correx stats`:
GET /health/checks replays the system session and `correx health` renders it.
Health events ride the reserved __system__ session, filtered from operator
session listings.

LLAMA_SERVER (liveness + tokens/sec trend) and EVENT_STORE (append/fsync latency)
probes plug into the same HealthProbe framework as follow-ups.
2026-06-13 22:19:33 +04:00
kami 4e5e4e56ea feat(toolintent): stage write-manifest enforcement (role-reliability §2)
Scope creep — an implementer writing files it never declared — is the
dominant local-model failure that compiler/tests don't catch. A stage can
now declare a `writes` manifest (workspace-relative globs); a FILE_WRITE
that resolves inside the workspace but outside the declared set is raised
as PATH_OUTSIDE_MANIFEST → BLOCK, so the model gets the violation as tool
feedback and retries within scope (the §2 bounded-retry signal).

Implemented as a plane-2 rule beside PathContainmentRule (same effect-based
dispatch on FILE_WRITE, same symlink-safe WorldProbe resolution, same
recorded observations so replay reads them back — invariant #9). An empty
manifest is unrestricted; out-of-workspace targets stay PathContainmentRule's
concern, so the two rules don't double-flag.

- StageConfig.writeManifest + TomlWorkflowLoader `writes` parsing
- ToolCallAssessmentInput.writeManifest, threaded from the active stage via
  SessionOrchestrator.runPlane2Assessment
- new ManifestContainmentRule, registered in the server assessor
- shared candidatePathStrings extracted so both path rules judge the same
  target set
- role_pipeline.toml documents the option on the implementer stage

The dedicated ManifestViolationEvent the spec names is not added: the
violation is already recorded replayably in ToolCallAssessedEvent (issue +
observations + BLOCK) and surfaced in the TUI rationale band. A first-class
event is worth adding only once reconciliation consumes it.
2026-06-13 16:05:39 +04:00
kami 65df3f4fad feat(tui,kernel): deterministic command-approval parse layer (§5.1)
Layer 1 of tui-requirements §5: a no-LLM analyzer that shell-lexes a
proposed command, classifies binaries against a known-command table, and
mechanically flags the constructs that widen blast radius — sudo/doas,
recursive delete, pipe-to-shell (skipping wrappers so `| sudo sh` counts),
redirects outside the workspace, network binaries, base64/eval, and
unparseable input (itself a red flag). Pure + replay-stable: same preview
+ workspaceRoot → same analysis, no environment reads.

The approval band renders command approvals as a card — worst-first flag
chips over the reconstructed command line, each token colored by severity
(T0–T4 ramp). ^x opens a scrollable fullscreen view so a long command is
never truncated. Diff and plain-preview paths are unchanged.

Server: computeToolPreview now renders the shell tool's argv as a full,
shell-quoted command line instead of the 200-char JSON-args fallback,
honouring §3's "full untruncated command". The TUI parser accepts both
the rendered line and the older `{"argv":[...]}` shape for replay.

Layer 2 (model annotation) deliberately deferred per the spec.
2026-06-13 15:18:27 +04:00
kami b4ca17c85f fix(tui): responsive layout for slim terminals
The TUI overflowed and became unreadable on narrow terminals: the status bar and
footer spilled far past the edge (justify never truncated), the two-column main
split shrank the events panel to ~20 cols, and the stats overlay wrapped mid-word.

- justify is now width-safe: shrinks the left segment first (the right holds the
  high-signal connection clock / active spinner), clamps the right only as a last
  resort, ANSI-aware so it never slices escape codes. No line can exceed the width.
- Main area collapses below narrowWidth (96): single full-width session list when
  idle; output stacked over the live event strip in-session (the strip is too
  valuable to drop — the constraint is width, not height).
- Compact chrome when slim: status drops workspace/gauge/provider-suffix and uses a
  short clock ("◷ 3s") + bare spinner; footer uses a reduced hint set and drops the
  "Soft·blue" badge; approval action row tightens its gaps so decision keys stay
  visible.
- Overlays get near-full-width on slim terminals; stats pane uses compact formats +
  per-line clipping so it never wraps.

Tests: no rendered line exceeds the terminal width at 40–160 cols (incl. the stats
overlay); narrow in-session main stacks output+events.
2026-06-13 11:01:16 +04:00
kami 8b896b519a feat(tui): truthful-state §2 — last-event clock + unknown-event raw fallback
Implements the load-bearing TUI-requirements §2 hard requirements:

- Last-event clock: "last event Ns ago" always visible in the status bar while
  in-session, updating every frame tick. Turns warn-colored when an active session
  has gone quiet past the stale threshold — the "thinking vs hung" tell. The
  WebSocket connection indicator (●/◌/○) already covers immediate drop visibility.
- Unknown-event raw fallback: applyServer now surfaces any unrecognized,
  session-scoped event type as a raw row in the event stream instead of silently
  dropping it (the frontend must not lie by omission). Recognized-but-unrendered
  types (router.response, protocol_error) get an explicit no-op so the default
  branch means genuinely unknown.

Tests: agoLabel formatting, unknown-event surfaces-as-row, known-ignored
no-spurious-row. Preview "session" frame exercises the clock.

Deferred: the full Go↔Kotlin per-event-type render matrix (§2 last bullet) — a
larger fixture effort than the core behaviors landed here.
2026-06-13 10:46:06 +04:00
kami 4f6bfa85f7 feat(server,tui): session stats pane — metrics over the WS bus + Bubble Tea overlay
Surfaces the observability metrics (observability-spec §3-tier-2) in the Go TUI:
a session-summary pane showing duration, token throughput per provider, tool
time, approval latency per tier, failure counts, and the session wall-time
accounting split. Press `S` (or palette → "session stats").

Mirrors the artifacts/config fetch pattern exactly — a WS request/response, not a
new transport. Server-side reuses the already-tested MetricsInspectionService
(one MetricsReport definition, two wires: REST `correx stats` + WS).

Server:
- ClientMessage.GetSessionStats + ServerMessage.SessionStats(@SerialName
  session.stats), reusing MetricsReport as the nested payload.
- StreamQueries.sessionStats replays via MetricsInspectionService (pure read,
  replay-neutral); routed in GlobalStreamHandler.
- ServerMessageSerializationTest golden pins the session.stats wire format.

TUI (Go/Bubble Tea):
- protocol: TypeSessionStats + StatsDto/nested structs + GetSessionStats encoder
  + golden decode test (cross-language contract).
- OverlayStats pane (statsModal), `S` key + palette entry + footer hint,
  loading/cache-by-session state, bounded breakdown rows.
- demo `stats` preview case for serverless visual verification.
2026-06-13 10:39:52 +04:00
kami fe941408a7 feat(server,cli): observability metrics as a replayable projection + correx stats
Implements observability-spec §2 + §3-tier-1: metrics as a pure fold over the
event log, no third pillar, no OTel. Every metric is replayable over the full
historical log because it derives from events other contexts already emit — no
new EventPayload subclasses, so no serialization registry change.

- MetricsProjection: Projection<MetricsState> folding inference/tool/approval/
  stage/workflow events into raw sums; approval request→decision latency is
  correlated in-fold via a transient pending-request map.
- MetricsInspectionService: rebuilds via DefaultEventReplayer, derives read-side
  ratios (token throughput, avg approval wait, session wall-time accounting split
  into inference / tool / approval-wait / other) into MetricsReport.
- GET /sessions/{id}/metrics route, mirroring the replay endpoint.
- `correx stats <sessionId>` CLI client (--json passthrough), mirroring replay.
- Tests: seeded-event fold correctness with controlled timestamps (7) + CLI
  render smoke tests (4).
2026-06-13 10:23:40 +04:00
kami 75703ec0c8 refactor(server,cli): shared payload-discriminator helper, single-read digest reuse, DOT label escaping 2026-06-12 19:23:05 +04:00
kami 4422aa8b7a feat(cli): causation-graph DOT export for event inspector 2026-06-12 19:17:26 +04:00
kami 64b58e3b05 feat(server,cli): session replay inspection — timeline + determinism digest 2026-06-12 14:39:54 +04:00
kami a04cd1fa24 feat(server,cli): event stream inspector — /sessions/{id}/events + correx events 2026-06-12 14:28:19 +04:00
kami a7d5211f3f feat(server,logging): correlation-structured MDC logging at seams
Implement T1 of observability epic: every server log line carries sessionId
(and stageId where known) via SLF4J MDC, propagated across coroutine
suspension points.

Changes:
- Create com.correx.apps.server.logging.Correlation: withSessionContext()
  helper that sets MDC (sessionId, stageId), wraps block in MDCContext(),
  and restores prior values on exit; supports safe nesting
- Wrap launchSessionRun() orchestrator loop in withSessionContext(sessionId)
  so all session logs carry the sessionId
- Wrap GlobalStreamHandler ChatInput and StartChatSession onUserInput call
  sites in withSessionContext(sessionId)
- Wrap SessionStreamHandler ChatInput onUserInput in withSessionContext(sessionId)
- Update log4j2.properties patterns: append
  %notEmpty{ [%X{sessionId}]}%notEmpty{ [%X{stageId}]} before %msg
  on both console and file appenders
- Add kotlinx-coroutines-slf4j:1.9.0 dependency for MDCContext

Test (6 tests):
- withSessionContext sets/restores sessionId ✓
- withSessionContext sets/restores stageId ✓
- Nested contexts stack and restore correctly ✓
- MDC cleared when stageId not provided ✓
- Propagation across coroutine boundaries via MDCContext ✓
- Cleanup after block exit ✓

Acceptance:
- Zero behavior change; no core module touched ✓
- All imports verified as used ✓
- No bare try-catch; try/finally used correctly ✓
- Detekt-compliant ✓
- Full test suite passes (120 tests, 119 passed, 1 skipped) ✓
2026-06-12 14:17:54 +04:00
kami e195c79510 fix(server): record boot-time parking of stale sessions as OrchestrationPaused
A stale RUNNING session skipped by resumeAbandoned stayed RUNNING in
every projection — the TUI showed a phantom running workflow that no
process was executing, and each boot re-logged the same skip. Parking
now emits OrchestrationPaused(reason=ABANDONED_STALE): status becomes
PAUSED (truthful — not executing, resumable via POST /resume) and
subsequent boots skip it on status alone.
2026-06-12 13:54:08 +04:00
kami a455762dd5 fix(router,server,config,tui-go): close QA findings #2 #3 #9 #10 #12
#2: approval pauses narrate from ApprovalRequestedEvent (carries
tool/tier/preview/stage directly) instead of OrchestrationPaused +
a pending-approvals map the next event had not yet populated — the
narrator always lost that race and produced generic text.

#3: NarrationTrigger now carries the triggering event's stageId; the
narration status line and RouterNarrationEvent.stageId use it instead
of the router projection's currentStageId, which terminal events have
already cleared ('Stage: none').

#9: ExecutionPlanLocked -> plan.locked and ArtifactValidated ->
artifact.validated mapped to the TUI (Go: feed lines, plan shows the
compiled stage chain); ArtifactValidating explicitly dropped.

#10: blank router turn guard — a length-finish with empty text now
emits an explanatory turn naming the max_tokens budget instead of a
silent blank; blank steering responses no longer emit steering notes.

#12: resumeAbandoned only auto-resumes sessions whose last event is
within orchestration.resume_abandoned_max_age_minutes (default 24h,
0 disables); staler sessions stay parked for POST /resume. Knob is
TUI-editable and persisted.
2026-06-12 13:20:04 +04:00
kami 4107a595db refactor(server): single guarded freestyleHandoff shared by run and resume launchers
launchSessionRun and launchSessionResumeWithRehydrate each inlined their
own copy of the freestyle phase-2 handoff with different guard sets — the
run path had none, so a failed planning run still called lockAndRun and
emitted a spurious ExecutionPlanRejected. Both now call one handoff that
checks graph id, planning result, and an existing plan lock before
locking and running phase 2.
2026-06-12 12:32:57 +04:00