merge: integrate feat/backlog-burndown into master

master had advanced 16 commits past feat/backlog-burndown's base, and the two
branches independently built four of the same features. Resolved 26 conflicts.

Overlap features — kept master's implementation (more complete / production-wired /
more robust), dropped the feature branch's parallel constellation:
- llama-server health probe: kept master's event-store-backed tps probe; dropped the
  branch's LlamaLivenessClient (liveness-only, throughput unwired).
- event-store probe: kept master's EventStoreHealthProbe; dropped EventStoreLatencyProbe.
- brief echo-back gate: kept master's BriefEchoDiff (Jaccard, tolerates rewording);
  dropped the branch's exact-set-diff BriefEchoComparator/Extractor.
- static-first reviewer: kept master's command/exit-code gate (ProcessStaticAnalysisRunner,
  wired); dropped the branch's structured-finding static_check stage (no-op seam).
  Its structured-findings model is filed as a follow-up in BACKLOG.

Feature-branch net-new work brought in and kept (master had none):
- native task tracking (aggregate, agent tools wired into analyst/implementer/reviewer,
  dependency graph + gates, decompose, REST/CLI, TUI task board)
- critique-outcome producer (role-rel §6 — master had deferred it)
- stage-level plan checkpointing (C-A2, folded into runPostStageGates)
- CLAUDE.md/AGENTS.md L0 standing context
- cross-session grants + TUI (grant scopes/revoke, @ picker, session resume browser)

Verified: full Gradle compile (all modules + tests) green; tests pass for core:events,
core:kernel, infrastructure:workflow, apps:server, apps:cli, testing:integration; tui-go
go build + go test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 11:30:41 +00:00
259 changed files with 17681 additions and 674 deletions
+202 -11
View File
@@ -1,6 +1,9 @@
package app
import (
"strconv"
"strings"
"github.com/correx/tui-go/internal/protocol"
"github.com/correx/tui-go/internal/ws"
)
@@ -54,12 +57,20 @@ const (
OverlayStats
OverlayIdeas
OverlayHealth
OverlaySessions
OverlayTasks
OverlayFiles
OverlayStatusbar
OverlayGrants
OverlayGrantScope
OverlayHelp
)
// RouterEntry is one line in a session's conversation transcript.
type RouterEntry struct {
Role string // user | router | tool | narration | narration_llm
Role string // user | router | tool | narration | narration_llm | action
Content string
Icon string // action role only: the gutter glyph (✓ ✎ ✗ ⌘ ✕ ⊞ ⊟)
Metrics *TurnMetrics
}
@@ -111,6 +122,27 @@ type Approval struct {
Rationale []string
}
// tierNum parses the numeric part of a tier label ("T3" → 3). Unparseable or
// empty tiers return -1, which is treated as low-risk (never requires confirm).
func tierNum(tier string) int {
t := strings.TrimSpace(strings.ToUpper(tier))
t = strings.TrimPrefix(t, "T")
if t == "" {
return -1
}
n, err := strconv.Atoi(t)
if err != nil {
return -1
}
return n
}
// HighTier reports whether this gate is destructive/high-risk (T3+), so an approve
// must be confirmed with a second keypress rather than acting on one keystroke.
func (a *Approval) HighTier() bool {
return tierNum(a.Tier) >= 3
}
// ClarQuestion is one open question a stage raised (mirrors the wire DTO).
type ClarQuestion struct {
ID string
@@ -159,10 +191,15 @@ type Session struct {
Tools []ToolRecord
ToolsByStage map[string][]ManifestTool
Events []EventEntry
Pending *Approval
Clar *Clarification // open questions awaiting answers (clarification view)
Propose *Proposal // router workflow suggestion awaiting a pick (propose view)
Active bool // an inference/tool is in flight (drives the spinner)
// Pending is the *current* approval gate shown in the band. It always mirrors
// PendingQueue[PendingIdx] (kept in sync by syncPending) so the render code can
// keep reading a single pointer while multiple gates queue up behind it.
Pending *Approval
PendingQueue []*Approval // all pending gates, oldest first; navigated with ↑/↓
PendingIdx int // selected index into PendingQueue
Clar *Clarification // open questions awaiting answers (clarification view)
Propose *Proposal // router workflow suggestion awaiting a pick (propose view)
Active bool // an inference/tool is in flight (drives the spinner)
}
// Workflow is a launchable workflow advertised by the server.
@@ -192,16 +229,21 @@ type Model struct {
wfVisible bool
wfPendingID string // workflow chosen, awaiting an intent line before StartSession
wfPendingName string
bgUpdates int
// launcher (idle screen): the active "what to launch" selection — 0 = chat, 1..N =
// workflows[i-1] — cycled with Tab and shown at the input's lower-right. railHidden
// folds away the idle status/quick-keys rail.
launcherWf int
railHidden bool
bgUpdates int
// input
editMode EditMode
inputMode InputMode
inputBuffer string
inputCursor int
history map[string][]string
historyIndex int
savedBuffer string
inputHistory []string // submitted lines across all contexts (chat, intent, in-session), newest last
historyIndex int // -1 = editing the live buffer; else an index into inputHistory
savedBuffer string // live buffer stashed while walking history
// flow flags
sessionEntered bool
@@ -212,6 +254,11 @@ type Model struct {
routerMessages map[string][]RouterEntry
routerConnected bool
chatMode string
// transcriptSel is the index (into the selected session's transcript) of the message
// the operator has navigated to with ctrl+↑/↓; -1 = no selection (tail-follow). A
// selected message is highlighted, scrolled into view, and is what `y` copies.
transcriptSel int
copiedFlash int // frame at which a copy happened, for a brief "copied" footer note
// provider
currentModel string
@@ -231,7 +278,11 @@ type Model struct {
overlay OverlayKind
overlayEventIdx int
diffScrollOffset int
modalScroll int // body scroll for tall fixed-content modals (help, stats)
eventStripShown bool
// in-session right panel: 0 = events, 1 = changes (token usage + changed files),
// 2 = off (output full-width). Cycled with `d`.
rightPanel int
// artifact viewer (OverlayArtifacts) — populated by the artifact.list response
artifacts []protocol.ArtifactDto
@@ -264,20 +315,85 @@ type Model struct {
health *protocol.HealthDto
healthLoading bool
// session browser (OverlaySessions) — recent sessions fetched over HTTP from
// GET /sessions, so prior runs are reachable after a server restart + TUI reopen
// (the WS stream only carries live sessions, not the historical roster).
sessionList []SessionSummary
sessionListIndex int
sessionListLoading bool
sessionListErr string
// task board (OverlayTasks) — all tasks across projects, fetched over HTTP from
// GET /tasks. taskDetail toggles the per-task detail pane (built from the list payload).
taskList []TaskSummary
taskListIndex int
taskListLoading bool
taskListErr string
taskDetail bool
taskDetailScroll int
taskFilter string // `/` substring narrow over id/title/goal
taskFilterTyping bool
// command palette
paletteFilter string
paletteIndex int
// @ file picker (OverlayFiles) — workspace paths from the file.list reply
files []string // all workspace-relative paths for filesFor
filesFor string // sessionId the file list belongs to
filesLoading bool
fileFilter string // the query typed after @ (narrows the list)
fileIndex int
// status-bar segment visibility (OverlayStatusbar) — persisted TUI-local in tui-prefs.json.
// A segment id present in sbHidden is hidden; absent = shown. sbIndex is the toggle cursor.
sbHidden map[string]bool
sbIndex int
// actionsHidden mutes the inline action rows (tool calls / writes / approvals / grants) in
// the OUTPUT transcript; they always stay in the EVENTS panel. Persisted in tui-prefs.json.
actionsHidden bool
// outputScroll is how many rows up from the bottom the OUTPUT transcript is scrolled
// (0 = tail-follow the newest output). PgUp/PgDn + ctrl+u/d move it; esc snaps back.
outputScroll int
// event-inspector filter (OverlayEventInspector): narrows the event list by a substring
// of type/detail. eventFilterTyping is true while the operator is editing the query after /.
eventFilter string
eventFilterTyping bool
// standing grants (OverlayGrants) — active PROJECT/GLOBAL grants from the grant.list reply.
grants []protocol.GrantDto
grantsLoading bool
grantIndex int
// grant scope picker (OverlayGrantScope) — chosen when the operator presses A on an approval.
// grantScopeIndex selects session/project/global; grantFor holds the approval being widened.
grantScopeIndex int
grantFor *Approval
// animation
frame int // tick counter; drives spinner + caret blink
frame int // tick counter; drives spinner + caret blink
ticking bool // true while a tick loop is scheduled. The loop is gated on animating()
// so an idle screen stops redrawing — which lets native terminal selection survive
// (the 120ms redraw used to wipe a mouse drag every frame).
// snapshot phase
snapshotPhase bool
pendingEvents []protocol.ServerMessage
// lastBase is the full-screen render behind the active modal, stashed by View each
// frame so center() can composite a modal over a dimmed copy of it (transparent
// backdrop) instead of an opaque scrim. Transient render scratch — not real state.
lastBase string
// approval steering input buffer
steerBuffer string
steering bool
// approvalArmed is set while a high-tier (T3+) approve is awaiting its second
// confirming keypress — the destructive-action safety. Any non-confirm key
// disarms it; a confirming `y`/`a`/enter sends the decision.
approvalArmed bool
// clarification view (the interactive question form)
clarFocus int // focused question index
@@ -301,7 +417,6 @@ func NewModel(client *ws.Client) Model {
theme: NewTheme(SoftBlue),
wfIndex: -1,
inputMode: ModeRouter,
history: map[string][]string{},
historyIndex: -1,
routerMessages: map[string][]RouterEntry{},
configStaged: map[string]string{},
@@ -309,6 +424,9 @@ func NewModel(client *ws.Client) Model {
providerType: "LOCAL",
snapshotPhase: true,
eventStripShown: true,
transcriptSel: -1,
sbHidden: loadStatusbarHidden(),
actionsHidden: loadPrefs().InlineActionsHidden,
}
}
@@ -348,6 +466,79 @@ func (m Model) session(id string) *Session {
return nil
}
// syncPending re-points Pending at the selected queue entry, clamping the index
// so it stays in range as the queue grows and shrinks. Called after any queue
// mutation; Pending is nil exactly when the queue is empty.
func (s *Session) syncPending() {
if len(s.PendingQueue) == 0 {
s.PendingQueue = nil
s.PendingIdx = 0
s.Pending = nil
return
}
if s.PendingIdx < 0 {
s.PendingIdx = 0
}
if s.PendingIdx >= len(s.PendingQueue) {
s.PendingIdx = len(s.PendingQueue) - 1
}
s.Pending = s.PendingQueue[s.PendingIdx]
}
// enqueueApproval adds a gate to the queue (replacing one with the same request
// id, so a re-sent gate doesn't duplicate). The freshly-added gate is left where
// it is in the order; the current selection is preserved.
func (s *Session) enqueueApproval(a *Approval) {
for i, p := range s.PendingQueue {
if p.RequestID == a.RequestID {
s.PendingQueue[i] = a
s.syncPending()
return
}
}
s.PendingQueue = append(s.PendingQueue, a)
s.syncPending()
}
// removeApproval drops the gate with requestID and advances the selection. If the
// removed entry was before the cursor the index shifts down to stay on the same
// gate; if it was the selected one the cursor stays put (now showing the next gate).
func (s *Session) removeApproval(requestID string) {
idx := -1
for i, p := range s.PendingQueue {
if p.RequestID == requestID {
idx = i
break
}
}
if idx < 0 {
return
}
s.PendingQueue = append(s.PendingQueue[:idx], s.PendingQueue[idx+1:]...)
if idx < s.PendingIdx {
s.PendingIdx--
}
s.syncPending()
}
// clearApprovals empties the queue (used on resume/completion, where the server
// invalidates every gate at once).
func (s *Session) clearApprovals() {
s.PendingQueue = nil
s.PendingIdx = 0
s.Pending = nil
}
// navApproval moves the queue selection by dir (wrapping) and re-syncs Pending.
func (s *Session) navApproval(dir int) {
n := len(s.PendingQueue)
if n <= 1 {
return
}
s.PendingIdx = (s.PendingIdx + dir + n) % n
s.syncPending()
}
// ensureSession returns the session with id, creating an ACTIVE entry if absent.
// Session existence is derived from the event stream (any event for an unknown
// session vivifies it) rather than a dedicated control frame.