When a session has claimed a task with declared affected_paths, an in-workspace
write outside that scope is BLOCKED — but declarably: the message tells the agent
to widen the task's affected_paths via task_update (which re-records the scope)
and retry. Legitimate, unforeseen changes (a dependency, a caller) are never
trapped, only forced to become a recorded, auditable scope change. Reads the
session's activeTask scope (recorded at claim) — no cross-stream lookup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completing a task that declared affected_paths but saw no matching write this
session is now blocked ("marked done without doing it"), escapable with force.
TaskUpdateTool reads the session's writes through a SessionWrites port whose
adapter folds SessionContext (writes = recorded receipt.affectedEntities), and
matches them against the task's affected_paths globs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claiming a task now emits SessionWorkingTaskEvent{taskId, affectedPaths} into the
session's own stream (via a SessionFactRecorder port + event-store adapter), and
SessionContextProjection folds it into SessionContext.activeTask. So a gate learns
"which task is this session working, and its scope" by reading the session stream
— no cross-stream scan, no kernel->tasks dependency. Re-emitted on an affected_paths
edit by the claimant, so the snapshot scope stays current; latest wins on replay.
This is the data source the write-scope gate needs (next).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the single-purpose sessionReadPaths plumbing with a SessionContext
{reads, writes, activeTask} folded by SessionContextProjection over the session
stream — the read-model every anti-hallucination gate consumes (plane-2 rules via
ToolCallAssessmentInput.session; tool gates via a port, next).
- reads: completions whose recorded capability includes FILE_READ.
- writes: the resolved paths the orchestrator already records on each completion's
receipt.affectedEntities (FileAffectingTool) — no param-parsing reinvention.
- activeTask: null for now; populated once claim records a session fact.
ReadBeforeWriteRule now reads input.session.reads; ReadFilesProjection is retired.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Capability is the canonical signal the plane-2 gates dispatch on, but
ToolInvocationRequestedEvent only carried the tool name — so ReadFilesProjection
classified reads by a hardcoded "file_read" string, bypassing the abstraction and
(worse) re-deriving a semantic fact at replay from whatever the name meant. That
violates the event-sourcing invariant that replay reads recorded facts, not live
state.
Now the tool's requiredCapabilities are recorded on the event at emission, and the
projection classifies by ToolCapability.FILE_READ. To let the lowest module carry
it, ToolCapability moves from core:tools into core:events keeping its package, so
every existing import resolves unchanged and core:tools imports it from below. The
field is defaulted, so pre-existing events deserialize as empty.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Harden the claim warning into a block: task_update action=claim on a task with
unmet blockers is now rejected (no mutation) with the blocker list, instead of
claiming-with-a-warning. Escapable with force=true, which claims anyway and keeps
the "claimed despite unmet blockers" warning for the audit trail.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A file_read of a path that is inside the workspace but does not exist is now
BLOCKED with "no such path" instead of silently returning empty — so an agent
that hallucinated a filename fails loud and self-corrects (list the directory,
fix the name) rather than proceeding on an absent reference. Out-of-workspace
targets stay PathContainmentRule's concern, so the gates don't double-handle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An LLM that edits or overwrites a file it never read is acting on hallucinated
contents. This plane-2 ToolCallRule blocks it: dispatching on FILE_WRITE (covers
file_write and file_edit), a write to a path that exists on disk but was not
file_read earlier this session is BLOCKED. A brand-new file passes — you can't
read what doesn't exist, and creating is legitimate.
- ReadFilesProjection folds the session's file_read events (request + completion,
so a failed read doesn't count) into the set of paths read, mirroring
EgressAllowlistProjection; threaded into ToolCallAssessmentInput.sessionReadPaths
via SessionOrchestrator.resolveSessionReadPaths.
- Read paths and the write target are both resolved through the probe (toRealPath),
so spelling differences and symlinks can't dodge the gate.
- The orchestrator already feeds a block's rationale back as a tool result, so the
agent reads the file and retries; the rejected-event reason now carries that
rationale too (specific audit trail instead of a generic string).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The doctrine says "search before creating", but that leans on the LLM; this is
the backstop against an agent re-creating a task across runs. TaskService.
findDuplicates matches active (non-terminal) same-title tasks, case- and
whitespace-insensitively — a DONE/CANCELLED look-alike is not a duplicate, since
that work may legitimately recur.
- task_create rejects a duplicate, naming the look-alike and offering force=true.
- POST /tasks → 409 with the duplicates, or force:true to override;
correx task create --force.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bring the work graph into the agent loop:
- task_ready (T1): lists tasks ready to work now (TODO, all dependencies
satisfied) so an agent looking for work can pick one and claim it. Surfaces
work; never assigns (honouring the rejected /tasks/next). Registered in
TaskTools — left unwired in the request-driven example workflows, where it
would be noise; it belongs in an autonomous work-pull workflow.
- claim guard: task_update action=claim appends a WARNING naming any unmet
blockers, so an agent knows when it has jumped a dependency. Surfaced, not
hard-blocked — status is derived, so we don't fight the reducer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The DEPENDS_ON/BLOCKS link types were inert metadata — nothing reasoned about
them. TaskGraph turns them into answerable questions: what a task waits on
(its DEPENDS_ON targets plus any task that BLOCKS it), whether it is ready
(TODO with no unmet blocker — a terminal dependency stops blocking), and what
finishing it would unblock. Readiness surfaces workable tasks to claim; it
never assigns (honouring the rejected /tasks/next scheduler).
Surfaced across the stack:
- TaskService.ready / blockers / blocking.
- Context bundle: a `blocked` flag + `blocked_by` list, rendered as a prominent
"BLOCKED — waiting on ..." line, so an agent calling task_context learns it
should wait before charging at the task.
- GET /tasks?ready=true, GET /tasks/{id}/blockers; correx task ready / blockers.
Cross-project dependencies are scoped to a project for now (documented in
TaskGraph). Tests cover both link directions, terminal-dependency clearing,
the ready filter, the reverse blocking direction, the bundle flag, and REST.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surfaces a task's lifecycle straight from the event log (the source of truth) so
you can see what actually happened to it — claim/submit/complete, notes, links,
git-driven status — rather than only its current state. Useful for debugging a
live agent run, where current state alone doesn't say how it got there.
- TaskHistory.render: pure timeline renderer (split content/lifecycle to stay under
the complexity cap, mirroring DefaultTaskReducer).
- TaskService.history(id): the task's own events, in order; survives soft-delete
(the deletion is part of the trail), empty for an id that never existed.
- GET /tasks/{id}/history (200 text timeline, 404 when the id never existed) and
the `correx task history <id>` CLI command.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Nothing asserted that the example workflows actually grant the task tools, so a
future edit could silently drop one and stay green. Pin allowed_tools per stage for
role_pipeline (analyst/implementer/reviewer), freestyle_planning (analyst), and the
shipped review_loop (implement/review). The existing review_loop test loaded an inline,
now-stale toml rather than the shipped file, so add one that loads the real file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A freestyle stage's tools are LLM-authored in the execution_plan. At run time an
unresolvable tool name is silently dropped (resolve -> null -> mapNotNull), so the
stage runs toolless — e.g. a misspelled task_update means the implementation stage
quietly loses task tracking, invisible without a live run.
ExecutionPlanCompiler now takes the registered-tool universe and rejects a plan that
names an unknown tool, with a message identifying the tool and stage. FreestyleDriver
already turns a compile failure into a rejection the architect retries. The param
defaults to empty (validation skipped) so existing callers are unaffected; Main feeds
it toolRegistry.all().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two coupled gaps from tracing a real run:
1. Freestyle implements in phase 2 via stages compiled from the architect's
execution_plan (ExecutionPlanCompiler sets allowedTools = stage.tools), so the
static allow-lists never reach it and architect_freestyle.md banned every tool
but the file four. Teach the architect to thread an analysis-referenced task
through the plan: implementing stages get task_context/task_update and claim +
submit_for_review; the final/review stage completes it. No task referenced → no
task tools, and the plan never creates one.
2. Give the analyst task_create so the work is framed as a tracked item up front
(role_pipeline + freestyle). "Read-only" for the analyst means it writes no
files; a task is an event-log entry, not a file write — task_create is T2, so
opening one is approval-gated. The analyst names the new id in the analysis so
the implementer claims it and the reviewer completes it; the implementer now
creates only as a fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend the role_pipeline wiring to the other workflows that act on a code work
item, matching each stage's tier:
- freestyle_planning: analyst (read-only) gets task_search/task_context to find
related work and ground the analysis; prompt updated to match.
- review_loop: implement gets the full set (claim, submit_for_review, notes),
review gets task_context/task_update to complete on an approved verdict. Its
prompt files don't ship, so the doctrine rides the L0 policy + tool descriptions.
Left untouched: research (external-research flow producing a report, not a code
work item), qa_ping (smoke test), and healthcheck (diagnostic) — none track work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tool availability is a strict per-stage allowlist, so the task doctrine was
inert in role_pipeline — no stage granted the tools. Add them where they fit
each stage's tier and role, and point the stage prompts at the lifecycle:
- analyst (read-only): task_search/task_context to find related or duplicate
work and ground the analysis.
- implementer: full set — claim before working, submit_for_review once
verification passes, create one if the work warrants tracking.
- reviewer: task_context to judge against the task's own criteria, task_update
to complete it on an approved verdict.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The task tools were registered but had only mechanical descriptions and no
standing policy, so agents knew the tools existed but not when to reach for
them. Lead each of the five descriptions with its trigger (when to use, when
not to) and add a task-tracking convention to .correx/project.toml, which is
rendered into every stage's L0 context.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
correx task list/search/show/create/claim/complete/export over the REST
surface, with --json passthrough and pure render helpers. Completes the
human entry points alongside the TUI board.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A disposable Markdown view of the board, grouped by status with checkboxes
and claimants — rendered from the event log, never a source of truth.
TaskMarkdown.render (pure) backs GET /tasks/export[?project=].
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
POST /tasks/sync-git advances task status from commit messages: a mention
moves a task to IN_PROGRESS, a closing keyword (fixes/closes/resolves
auth-12) walks it the legal path to DONE. Only ever advances (never
regresses or touches BLOCKED/CANCELLED), so re-running is idempotent; each
advance leaves a [git <sha>] note. Acts on commits in the request body or
reads the repo's recent log (GitCommandCommitReader) — wire it to a git
hook or CI step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ranked exact/substring search: TaskSearch (all terms AND, ranked title >
key > goal > criteria > notes) over one project or the whole board via
TaskService.search. Surfaced as GET /tasks?q=, a read-only task_search tool
for agents, and a `/` filter in the TUI board.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a cross-project task board to the TUI (T / palette): a roster fetched
from GET /tasks with vim-style nav, refresh, and an enter-to-open detail
pane (goal, criteria, paths, links, notes) built from the list payload — no
extra fetch. Server: TaskService.listAll() and a project-optional GET /tasks
(scoped with ?project=, whole board without; recent-first).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When an agent creates or claims a task via the tool, record its session as
a CONTEXT/SESSION link so the context bundle's session resolver has real
edges to inline (status/intent/workflow). Shared linkOriginSession helper;
reducer dedupes so create+claim from one session is a single edge. The REST
create path stays unlinked (no agent session there).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The context bundle resolved only TASK and DOC link targets; ARTIFACT and
SESSION stayed raw. Add two ports (TaskArtifactResolver/TaskSessionResolver)
with apps/server adapters: artifacts resolve to producing stage/session +
content excerpt from CAS, sessions to status/intent/workflow. Unresolved
targets still fall back to raw links. Wired through the tool and REST bundle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CRUD, lifecycle transitions, work-graph links and notes over /tasks;
share link-kind inference (TaskTargetKinds) between the REST route and
task_update tool. End-to-end route tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds task tracking as a first-class event-sourced aggregate (ADR-0012). The
event log stays the only source of truth; the board is a projection, and all of
a project's task events live in one stream (tasks:<projectId>).
core:
- new core:tasks module mirroring core:sessions: TaskStatus/State/Reducer/
BoardProjector/CounterProjection/Repository + TaskService write path that
appends events via the existing EventStore (no new storage)
- task events in core:events (sealed marker TaskEvent), registered in
Serialization.kt; status is derived by the reducer from lifecycle facts
- work-graph edges: TaskLinkType + typed TaskTargetKind on the link, so bundle
resolution dispatches on the recorded kind instead of guessing from the id
- delete is a soft tombstone (TaskDeletedEvent); cancel stays a visible outcome
surfaces:
- agent tools (Tier T2 mutators, T1 read): task_create / task_update /
task_delete / task_context, registered in both the main and per-workspace
tool registries
- REST GET /tasks/{id}/context for external agents
context bundle (TaskContextAssembler):
- task fields + acceptance criteria + relevant files + resolved dependency
tasks (live status) + raw related links + notes, with a compact render()
- semantic enrichment via a TaskKnowledgeRetriever port bridged to the existing
L3 retriever (late-bound holder for composition-root ordering)
- ADR/doc resolution via a TaskDocumentResolver port + filesystem adapter
(docs/decisions/adr-*.md, padding-agnostic; *.md paths)
Unit-verified across core:tasks / infrastructure:tools / apps:server; full
gradlew check green. Not yet live-QA'd end-to-end against a running server.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Per the hygiene rule: the §B-§5 static_check stage seam + emitter and the §B-§6/A3
critique-outcome producer are built and unit-verified, so move them out of the live
"missing" list. Both narrowed to their remaining live-QA-gated *activations* (a sandboxed
production CommandRunner for §B-§5; model-emitted CritiqueFindingsRecordedEvents for §6),
with a dated RETRO entry + commit map. Updated QA-reviewer-static-first (the producing
stage now exists — check 4 reframed from "blocked on unbuilt seam" to an activation step)
and its README index line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two reviewer-reliability tracks, both deterministic and unit-verified (no model/network).
§B-§5 — static_check stage seam. The StaticAnalysisRunner + StaticFindingsRecordedEvent +
reviewer-context filter already existed; what was missing was a stage that runs the tool and
emits the event. Added:
- StaticCheckStageExecutor: reads stage metadata (static_tool/static_argv), runs the configured
command via StaticAnalysisRunner, returns a StaticFindingsRecordedEvent. No-op (empty findings)
when no runner is wired or no command is configured — safe to carry unconfigured.
- A deterministic-stage seam in DefaultSessionOrchestrator.enterStage: any stage with
metadata["stage_type"] == "static_check" is run by the executor instead of the LLM subagent,
then advances on its (unconditional) exit edge.
- TomlWorkflowLoader: stage_type/static_tool/static_argv fields → StageConfig.metadata.
- role_pipeline.toml: a static_check stage between implementer and reviewer (no-op until
static_argv + a CommandRunner are set; activation is live-QA-gated, see StaticAnalysisRunner doc).
§B-§6 — critique-outcome producer. The CritiqueFinding type + CriticCalibrationProjection existed
but nothing fed them. Added:
- CritiqueFindingsRecordedEvent (+ CritiqueVerdict): the producing side — a critic's findings +
verdict for one review iteration, carrying modelHash for per-model calibration.
- CritiqueOutcomeCorrelator: pure loop-resolution logic deciding UPHELD (fixed between rounds) /
DISMISSED (persisted into an approved final) / INCONCLUSIVE (open at a non-approved terminal),
per critic (role + modelHash) and per finding id.
- A hook in completeWorkflow/failWorkflow that correlates recorded findings into
CritiqueOutcomeCorrelatedEvents at loop resolution — no-op when none recorded, idempotent.
(LLM-side finding emission stays a separate model-gated activation.)
Tests: StaticCheckStageExecutorTest (4), StaticCheckStageTest integration (1, fake runner →
event + transition), CritiqueOutcomeCorrelatorTest (8), CritiqueCalibrationWiringTest integration
(1, seeded findings → outcomes at completion), updated RolePipelineWorkflowTest. Full suites for
core:events/kernel/critique, infrastructure:workflow, testing:integration green; detekt clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per the repo's own hygiene + QA rules: record the operator-requested v2 work in
RETRO.md (2026-06-22 TUI wave table: d247b19 migration, 85af2c6 cursor/title) and
draft docs/qa/QA-tui-v2.md — the kitty-box checklist for the behavioral claims
go test can't prove (Shift+Enter newline, the real blinking cursor + its
normal-mode/modal gating, window title, and a render-regression sweep across the
new v2 layer). Indexed in docs/qa/README.md as the cheapest gate (no model/network/GPU).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
apps/tui (the pre-Go-rewrite Kotlin TUI) is already gone — not on disk, not
tracked, and not in settings.gradle — so its runtime-logs ignore rule pointed
at a path that no longer exists. The apps/tui mentions in docs/epics/epic-13*
are left as historical record.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
The re-audit passed and its one gap — line-only diff-viewer scroll — was
fixed in db27b34. Cut the BACKLOG §E item and record the result + the
diff-viewer paging commit in RETRO 2026-06-22.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>