Commit Graph

289 Commits

Author SHA1 Message Date
kami 7909357747 fix(cli): format stats with Locale.ROOT so output is locale-independent
renderStats used the default-locale String.format, so under a comma-decimal
locale (e.g. ru_RU) percentages/rates rendered as "58,3" — both producing
locale-dependent CLI output and failing StatsRenderTest. Format all five
numeric rows with Locale.ROOT.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 18:59:38 +00:00
kami 85af2c67b7 tui: real terminal cursor in composer + window title (v2)
Two v2-unlocked polish items now that the TUI is on bubbletea v2:

Native cursor — the composer caret was a frame-animated "▏" that forced the
redraw loop to keep ticking in insert mode. v2's View.Cursor lets the terminal
draw and blink a real cursor, so:
- editorLines/launcherInput drop a width-1 marker rune (U+E123, same cell width
  as "▏") at the caret; View() locates it in the fully-composited frame via
  splitCursor → (X,Y), swaps it for a space, and sets a blinking CursorBar there.
- render() (preview/golden tests) resolves the marker to the static "▏" glyph, so
  screenshots are unchanged and the marker never leaks into output.
- nil cursor in normal mode hides it (vim-correct); gated on overlay==None so an
  open palette/files modal (which keeps editMode==Insert) doesn't draw a cursor
  behind it.
- animating() no longer forces a redraw in insert mode — the terminal blinks the
  cursor itself, so an idle insert screen holds still (native text selection
  survives) and burns no frames.

Window title — View.WindowTitle names the terminal tab after the active session,
falling back to the model name then "correx".

Tests: new cursor_test.go (coord extraction, mode/overlay gating, no marker leak);
updated the animating-gate test for the new insert-mode contract. Build, vet,
gofmt, full suite green; compose preview shows the static caret with no marker leak.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 16:28:22 +00:00
kami d247b19608 tui: migrate Bubble Tea v1 → v2 (charm.land), enable Shift+Enter
Move the Go TUI from github.com/charmbracelet/{bubbletea,lipgloss} to the v2
ecosystem at charm.land/{bubbletea,lipgloss}/v2 (Go 1.25). v2's kitty keyboard
disambiguation is on by default, so Shift+Enter now reliably inserts a newline
in the composer (Ctrl+J / Alt+Enter kept as fallbacks for non-kitty terminals).

Approach: rather than rewrite ~190 key-match sites to v2's (Code, Mod) idiom, a
small shim (internal/app/key.go) converts a v2 KeyPressMsg into the v1-shaped
keyMsg the handlers already expect, at the single Update boundary. The rest is
mechanical:
- key constants tea.Key* → shim consts; tea.KeyMsg → keyMsg.
- lipgloss.Color is now a func returning color.Color, not a type → fields/params
  retyped to image/color.Color (theme/diff/overlays/view).
- v2 View() returns tea.View: render() builds the string, View() wraps it and
  carries AltScreen (alt-screen is a per-frame View field now, not a program opt).
- WithWhitespaceBackground/Foreground → WithWhitespaceStyle(Style).
- preview: drop lipgloss.SetColorProfile (v2 renders truecolor by default).

Build, vet, gofmt, and the full test suite are green; preview renders truecolor
across kinds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 15:59:55 +00:00
kami 6c792b83e2 feat(tui-go): in-session right panel cycles events / changes / off (d)
Press d (or the "panel" palette command) in-session to cycle the right panel:
- events  — the live event stream (default, unchanged)
- changes — a token-usage line (tokens + turns, summed from the transcript's
            TurnMetrics) over a git-status-style list of files the session has
            written, each with summed +/− from its diff. ^x still opens the full
            diff.
- off     — hide the panel; the output transcript takes the full width.

changesRows derives everything from data already in the model (router-turn
metrics + the tool entries' unified diffs), so no new protocol. Footer + ? help
updated; "changes" preview kind added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 14:47:23 +00:00
kami cde0c33031 feat(tui-go): multi-line input — Ctrl+J / Alt+Enter newline, growing box (Phase 2)
Enter still submits; Ctrl+J (always) and Alt+Enter (most terminals) insert a
newline at the cursor. True Shift+Enter isn't distinguishable from Enter in
bubbletea v1.3.10 (terminals send the same byte, no kitty-protocol parsing), so
these are the working stand-ins — noted in the input hint.

- editorLines() renders the buffer as styled rows with the caret at the cursor,
  splitting on \n and capping at maxInputLines (last rows kept). Both the idle
  launcher input and the in-session bottom bar use it and grow to fit; View()
  sizes the bottom bar via inputBarHeight() instead of the fixed inputH.
- Footer gains a "^J newline" hint in insert mode (non-filter); the launcher
  sub-line shows "⌥↵/^J newline". New "compose" preview kind + editor_lines_test
  (split + height cap).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 12:03:06 +00:00
kami f2be46a743 refactor(tui-go): move session dates to the R overlay; drop dead idle panels
The launcher removed the idle session list, so the per-row date moves to the R
resume overlay — now the sole session list. sessionListRow shows an absolute
local date (absDateISO → shortDateTime) alongside the relative age:
"Jun 22 14:30 · 5m ago", with the year shown for older-than-this-year rows.

Delete the now-unused idle-panel renderers (sessionRows / welcomeRows /
workflowRows) and renderMainNarrow's dead idle branch (renderLauncher owns idle,
narrow included). Add a "resume" preview kind to screenshot the R overlay.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 11:57:41 +00:00
kami 9d00612742 feat(tui-go): idle launcher — centered input + status/keys rail (Phase 1)
Replace the idle two-panel layout (session list + welcome, where the welcome
panel duplicated the list) with an opencode-style launcher: a compact, centered
input whose lower-right shows the launch target and model (chat by default), a
hideable right rail of status + quick keys, and no session list — it's a keypress
away via R.

- Tab (or w) cycles the launch target: chat → workflows → chat (launcherWf),
  shown at the input. Enter on chat starts a chat; on a workflow, StartSession
  with the typed text as the brief.
- Right rail (connection, session/active counts, i/Tab/R/?/p keys) hides via the
  new "rail" palette command. Drops automatically on narrow terminals.
- View() suppresses the bottom input bar on idle (the input is the launcher);
  footer + ? help updated to the launcher keys (help-coverage test extended).

The old idle-panel renderers (sessionRows/welcomeRows/workflowRows) are now
unused but kept in the tree pending Phase 2 + a decision on where the session
dates should live now that the idle list is gone. Multi-line input (Ctrl+J /
Alt+Enter newline) is Phase 2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 11:33:19 +00:00
kami 81280c5bd5 feat(tui-go): welcome "recent" list uses the local date, matching the session list
Was the UTC-then-local time-of-day; now the same "Jan 02 15:04" / "Jan 02 2006"
shortDateTime as the session list, so a previous-day session isn't shown as if
it were today.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 10:28:28 +00:00
kami 6bee3a7d31 fix(tui-go): render event/activity times in local time, not UTC
formatTime forced .UTC(), so every event-log timestamp and the welcome "recent"
times read hours off the operator's wall clock — and inconsistent with the local
date just added to the session list. time.UnixMilli is already local, so the
explicit .UTC() was the bug; drop it. No test asserts a clock string (the render
matrix only checks event type/detail), so this is display-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 10:14:51 +00:00
kami 4e4d7c186a fix(tui-go): make the write/edit output-panel number meaningful
The collapsed tool row showed "tool output (N chars)" where N was len() of the
raw unified diff — i.e. the diff blob's *byte* length (headers, @@, +/-/context
all counted), labelled "chars". That number corresponded to nothing the operator
cares about, and len() is bytes not characters.

- Summarise a write/edit by the diff's row count instead — "· diff (7 rows) —
  ^x to view" — matching exactly what the ^x modal reports. Non-diff output (the
  defensive fallback) now counts runes, not bytes, so "N chars" is truthful.
  Retire itoaLen (the byte-count-as-"len" footgun).
- Harden the (+a −b) line counts in diffSummary: guard the file header with the
  trailing space ("+++ "/"--- "), so a changed line whose content starts with
  "++"/"--" is counted instead of mistaken for a header. Apply the same guard in
  parseUnifiedDiff so such a line is rendered (and counted) rather than dropped.

tool_summary_test.go covers the diff-row summary, the rune (not byte) count, and
the ++/-- content-line edge case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 10:09:56 +00:00
kami 035f7df704 feat(tui-go): make the ? overlay list every keybind
Audited handleNormalKey / handleApprovalKey / the OverlayDiff handler against the
help cheat-sheet and found gaps: enter, ↑↓/jk, /, s, c, a, w, q and ? itself were
bound but undocumented. Reorganise helpBody into navigate / session / overlays /
approval band / diff-viewer groups covering all of them. help_complete_test.go
asserts a row exists for every binding so the help can't silently drift from the
handlers. The longer sheet scrolls fine via the modal-scroll support.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 10:04:41 +00:00
kami 454f24fc3a feat(tui-go): show last-activity date in the starting-view session list
The idle "sessions" panel listed id/status/name/stage but no time at all. Add a
right-aligned last-activity timestamp (Session.LastEventAt) per row — "Jan 02
15:04" within the current year, "Jan 02 2006" for older — so old sessions are
distinguishable at a glance. The name is truncated first so the box's width
clamp never eats the date column. (The R resume overlay already shows a relative
age; this is the live list on the starting screen.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 10:04:41 +00:00
kami 85121cec9d fix(tui-go): event-stream badge tracks session status, not just the socket
The EVENTS panel showed "● live" off m.connected alone, so a completed/failed/
paused session kept reading "live" as long as the websocket was up — stale, since
a terminal session emits no more events. Derive the badge from the selected
session's Status (the same vocabulary the status bar uses): ✓ done, ✗ failed,
‖ paused, ● live while active, ○ idle when disconnected. event_badge_test.go
covers all five.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:52:37 +00:00
kami a547bc4342 chore: remove dead apps/worker and apps/desktop stub modules
Both were scaffolding placeholders from Epic 14 (2c459da) that were never
built out: each was just an `application` build.gradle (clikt only, no
internal deps) and a one-line `println` Main.kt. Nothing depended on them —
no project(...) reference anywhere, only the settings.gradle includes, and no
script/doc/CI launched their MainKt — so they only cost build time configuring
two inert modules.

Delete both module dirs and drop their include lines from settings.gradle.
`./gradlew projects` now lists only :apps:cli and :apps:server (configures
clean).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:47:15 +00:00
kami 7114bb7658 feat(tui-go): scroll tall modals instead of clipping them off-screen
The ? help and S stats modals rendered at fixed height; compositeOverlay
drops any rows past the screen, so on a 24-30 row terminal their bottom
sections — including the close hint — vanished with no way to reach them
(and the backdrop border corrupted). Recent help additions worsened it.

- Add a shared scrollable-modal renderer: helpBody()/statsBody() produce
  discrete lines, renderScrollModal() windows them by m.modalScroll (clamped
  to the viewport via modalBodyRows/scrollableModalMax) and pins the hint, so
  the box never exceeds the screen. A "(off-end/total)" indicator shows in the
  title when scrolled. Keys: ↑↓/jk line, PgUp/PgDn page, ^u/^d half, g/G ends;
  any other key still dismisses help. modal_scroll_test.go (2): clamp + no-op.
- Page the artifacts viewer (v) too: PgUp/PgDn were one line at a time — now a
  full body, with ^u/^d half and g/G ends via scrollArtifact().

Verified via cmd/preview: help/stats close hints now survive at h=20-28 (were
clipped below ~32/40) and show the scroll indicator; full at tall heights.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:26:21 +00:00
kami db27b3436b feat(tui-go): page-scroll the diff/preview/command fullscreen viewer
The ^x fullscreen viewer (OverlayDiff) backs every tool preview — split diff
for write/edit, the command card for shell, plain lines otherwise, plus any
transcript tool-output — but only scrolled one line per keypress, making a
long diff tedious to read.

- Add paging to the viewer, matching the OUTPUT free-scroll contract: ↑↓/jk
  by line, PgUp/PgDn by a full body, ^u/^d by half, g/G to the ends. New
  scrollDiff() clamps to [0, diffMaxScroll] so paging past either edge lands
  exactly (no dead presses). diff_scroll_test.go (2): clamp + no-op-when-fits.
- Update the modal hint to advertise paging (line / page / ends / close) and
  add a "diff / preview viewer (^x)" section to the ? help overlay. Also note
  the T3+ confirm-again behaviour in the approval-band help.

Approval ergonomics re-audit (BACKLOG §E): the spec'd band still holds —
single-key y/n/e for T0-T2, mandatory second-key confirm for T3+ (HighTier),
navigable queue with an i/n indicator, docked band never modal-stacked; all
covered green by approval_test.go. The scroll granularity above was the only
gap, now closed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:09:54 +00:00
kami 08b5c63a0f docs: reconcile BACKLOG against shipped work + gitignore tui preview binary
Backlog hygiene per the file's own maintenance rule — resolved items get
moved out, not left ticked-in-place.

- Cut resolved body bullets from BACKLOG §A/B/C/D/E/G/I (health probes,
  static-first infra, CritiqueFinding, architect contradiction-check, A1/A2
  gates, batch fetch-approval, source/egress events, render matrix, markdown,
  session list, idea promotion, turnId/probe/narration, .cs symbols, scene
  validator). All already archived in RETRO's burndown table.
- Narrow partially-done items to their live follow-up rather than delete:
  B5 -> static_check stage emitter only; B6/C-A3 -> CritiqueOutcomeCorrelated
  producer only; C-A1 -> planner-prompt activation only.
- Drop the stale "§E UNBLOCKED" index line (plan-comparison is BLOCKED, no
  PlanCandidate event; idea-promotion shipped f107ff5).
- Archive the operator-requested TUI wave (palette, @ picker, CLAUDE.md L0
  injection, inline action rows, free-scroll/help/filter) in RETRO 2026-06-22
  — never BACKLOG items, would otherwise be orphaned in neither file.
- gitignore apps/tui-go/preview (cmd/preview build artifact).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 08:57:12 +00:00
kami 531910d38a feat(tui-go): transcript free-scroll + help overlay + event filter
Three TUI-polish items.

- OUTPUT free-scroll: PgUp/PgDn (and ^u/^d half-page) scroll the transcript
  back through history; esc snaps to tail-follow. outputScroll is clamped
  against outputViewport(), which mirrors View/renderMain geometry, so the
  edges land exactly (no dead presses unwinding an overshoot). routerRows
  split into buildTranscriptRows (full render) + windowing so the handler
  measures the same content. Tests cover the clamp + the fits-in-panel no-op.
- Help overlay: `?` (or the "help" palette command) opens a keybinding
  cheat-sheet grouped by context (session / overlays / approval band) — the
  non-palette keys especially. Any key dismisses it.
- Event-inspector filter: `/` filters the event list by a case-insensitive
  substring of type/detail (currentEvents applies it); `c` clears, esc stops
  editing then closes. Fresh per open. EVENTS side panel stays unfiltered.

go build / vet / test green; rendered help + filtered inspector via
cmd/preview (help, events-filter kinds).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 05:41:38 +00:00
kami f14a83c026 feat(tui-go): drop inline tool-start rows (keep results only)
The ⏵ tool-start row doubled up with the ✓/✎ result row in OUTPUT. Drop it
inline — tool starts still show in the tool list + EVENTS panel. The inline
transcript now carries only outcomes: ✎ writes, ✓/✗ tool results, ⌘/✕
approvals, ⊞/⊟ grants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 23:24:10 +00:00
kami 6a06f2ead5 feat(tui-go): inline action rows in the OUTPUT transcript
Surface side-effecting ("external feedback") events in the conversation
flow, opencode-style, instead of only in the EVENTS side log.

- New RouterEntry role "action" (glyph + text) appended at arrival so it
  interleaves with chat turns by order. Rendered dim with an accent gutter
  glyph; the EVENTS panel still carries the full stream.
- Surfaced: tool start (⏵), tool done (✓ / ✎ wrote <path> (+a −b) for a
  diff), tool failed (✗), rejected (✕ blocked), approval resolved
  (⌘ approved / ✕ rejected, noting "· via grant" on a grant auto-approve),
  and grant create/revoke (⊞ / ⊟ · scope). ApprovalResolved names no tool
  on the wire, so the tool is recovered from the pending queue before the
  gate is dropped.
- Toggle: "inline actions" palette command flips actionsHidden (rows are
  always recorded, gated at render, so toggling reveals history); persisted
  in tui-prefs.json. prefs.go refactored to load/mutate/save so the new
  field and statusbarHidden no longer clobber each other.
- demo.go: "actions" preview kind.

go build / vet / test green; rendered the flow via cmd/preview.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 23:00:18 +00:00
kami 8df0ec750c feat(tui-go): grant scope picker (A) + standing-grants viewer
Surface the new cross-session grant scopes in the TUI.

- Approve-always (A) now opens a scope picker instead of immediately
  creating a SESSION grant: choose this session / this project / everywhere.
  SESSION stays the default (A then enter/s = old behaviour); p and g
  create PROJECT / GLOBAL grants. The grant + approval are sent on confirm
  (esc cancels, leaving the call pending).
- New "grants" palette command + G shortcut open a standing-grants viewer
  (OverlayGrants) listing the active PROJECT/GLOBAL grants — scope, tool,
  permitted tiers, project path — with x/enter to revoke. The server's
  RevokeGrant reply (a fresh grant.list) refreshes it in place.
- protocol.go: TypeGrantList + GrantDto; RevokeGrant/ListGrants encoders;
  CreateGrant now documents the wider scopes. server.go decodes grant.list
  into m.grants and treats it as a non-session global reply.
- render-matrix coverage: grant.list classified as a non-rendering query
  reply (populates the overlay, not the transcript).
- demo.go: grants + grant-scope preview kinds.

go build / vet / test green; rendered both overlays via cmd/preview.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 22:35:01 +00:00
kami c36d41b9d5 feat(approvals): cross-session grant scopes (PROJECT/GLOBAL) + revoke
Make the grant system's wider scopes real and add a revoke producer.

Before: grants were loaded per-session (the gate folds only the running
session's own stream), so GrantScope.PROJECT — though declared and matched
in the engine — was dead in practice, and there was no GLOBAL scope or any
way to revoke a grant (ApprovalGrantExpiredEvent had no producer).

- GrantScope: add GLOBAL(toolName); make PROJECT tool-bound. Every
  operator-creatable scope is now tool-bound so a grant can never be a
  blanket "approve everything" (that's YOLO mode).
- DefaultApprovalEngine.scopeMatches: GLOBAL matches the bound tool in any
  context; PROJECT matches when the session's projectId AND tool match.
- Cross-session ledger: PROJECT/GLOBAL grants are appended to a reserved
  GRANT_LEDGER_SESSION_ID stream instead of a session's. The approval gate
  now unions the ledger's grants with the session's, and derives projectId
  from the bound workspace root (ProjectIdentity.of) so PROJECT grants match
  later sessions on the same repo. SESSION/STAGE grants still live in (and
  die with) their session.
- Revoke: ClientMessage.RevokeGrant appends ApprovalGrantExpiredEvent to the
  ledger (reducer already drops it); ListGrants/GrantList expose the active
  standing grants for the TUI viewer.
- Drop the server-side T2 grant ceiling per operator request: a grant may
  now authorize any tier (incl. destructive T3/T4). Tool-binding is retained
  as the remaining guard.

Backend only; the TUI scope picker + grants viewer follow. core:events,
core:approvals, core:kernel, apps:server compile; approvals/events/kernel/
server suites green; new GrantScopeMatchingTest (5) covers GLOBAL/PROJECT
match, project isolation, and the no-cap T4 path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 16:07:33 +00:00
kami 54f94a549f feat(tui-go): command-palette keybinds + configurable status bar
Palette: each command now shows its bare-key shortcut in a key column (and the filter
matches on it), so the palette teaches the shortcuts instead of hiding them.

Status bar: a new "status bar" palette command opens a toggle overlay to show/hide the
non-essential segments (current stage, session status, workspace path, model, last-event
clock, resource gauge); correx/connection/session-name/spinner stay. Choices persist
TUI-local in tui-prefs.json (JSON in the config dir — display state, kept out of the
shared/replayed server config) and reload on launch.

Also trims the in-session footer (drops stats/model — both reachable via `p cmds`) so it
no longer overflows + truncates `q quit` at ~100 cols after the transcript-nav additions.
Tests cover toggle→render gating and prefs persistence; verified via cmd/preview renders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 10:50:28 +00:00
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