Commit Graph

570 Commits

Author SHA1 Message Date
kami 47a109e49f refactor(toolintent): one SessionContext read-model for the gates
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>
2026-06-24 20:16:15 +00:00
kami 9b003d02ea refactor(events): record tool capabilities on the invocation event
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>
2026-06-24 19:46:44 +00:00
kami 78e7a4fefb feat(tasks): dependency hard gate on claim
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>
2026-06-24 18:43:17 +00:00
kami 146f96a6fd feat(toolintent): reference-must-exist gate (anti-hallucination)
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>
2026-06-24 18:27:08 +00:00
kami 693e38ffac feat(toolintent): read-before-write gate (anti-hallucination)
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>
2026-06-24 18:13:06 +00:00
kami 5d48c26ec8 feat(tasks): duplicate-title guard on task creation
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>
2026-06-24 17:34:24 +00:00
kami 215a951b52 feat(tasks): agent-loop integration — task_ready tool + claim guard
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>
2026-06-24 17:34:24 +00:00
kami e77b2960f9 feat(tasks): dependency-aware work graph (blockers, ready, blocking)
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>
2026-06-24 17:02:03 +00:00
kami bce62c080c feat(tasks): per-task history / audit timeline
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>
2026-06-24 16:37:50 +00:00
kami a96c05e228 test(tasks): pin the task-tool wiring in the shipped workflows
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>
2026-06-24 16:16:03 +00:00
kami d12d64e4fe feat(workflow): reject execution plans that reference unknown tools
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>
2026-06-24 16:16:03 +00:00
kami 12d5b9d7dc feat(tasks): analyst opens the task; freestyle threads it into implementation
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>
2026-06-24 15:13:52 +00:00
kami de54be7ecb feat(tasks): wire the task tools into the remaining code-work workflows
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>
2026-06-24 10:26:38 +00:00
kami afb536f21f feat(tasks): wire the task tools into role_pipeline
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>
2026-06-24 10:04:01 +00:00
kami 06c225e3bd feat(tasks): teach agents when to use the task tools
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>
2026-06-24 09:40:44 +00:00
kami f490968dc4 feat(tasks): correx task CLI
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>
2026-06-24 07:17:06 +00:00
kami 8a6678f171 feat(tasks): markdown export projection
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>
2026-06-24 07:17:06 +00:00
kami 8d84ccf92f feat(tasks): git-driven task status
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>
2026-06-24 06:37:34 +00:00
kami 99f781687f feat(tasks): task search across REST, tool, and TUI
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>
2026-06-23 22:08:11 +00:00
kami 1d7fab4ee4 feat(tasks): TUI task board
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>
2026-06-23 20:32:48 +00:00
kami 70b4357316 feat(tasks): auto-link the originating agent session
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>
2026-06-23 20:15:54 +00:00
kami 6b00d89c2c feat(tasks): inline ARTIFACT/SESSION links in the context bundle
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>
2026-06-23 19:59:18 +00:00
kami 6e044d0ab9 feat(tasks): full REST surface for tasks
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>
2026-06-23 19:27:19 +00:00
kami 0c6ac903b7 feat(tasks): native task tracking — aggregate, agent tools, context bundle
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>
2026-06-23 19:00:58 +00:00
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 33baaed903 docs: reconcile BACKLOG→RETRO for static_check stage + critique producer (e46777e)
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>
2026-06-22 21:52:59 +00:00
kami e46777e29f kernel: deterministic static_check stage (§B-§5) + critique-outcome producer (§B-§6)
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>
2026-06-22 21:14:00 +00:00
kami c2a1e6d76d docs: archive v2/cursor TUI work in RETRO + add QA-tui-v2 live-QA plan
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>
2026-06-22 16:32:07 +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 0139225cc4 chore: drop stale .gitignore entry for the removed Kotlin TUI
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>
2026-06-22 09:42:58 +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 f043640373 docs: close §E approval-ergonomics re-audit (move to RETRO)
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>
2026-06-22 09:10:39 +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 3f4c45a7b0 docs(qa): live-QA plan for cross-session grants + revoke
QA-grants.md (10 checks): the headline is grant-in-session-A →
auto-clear-in-session-B, keyed on ApprovalDecisionResolved(AUTO_APPROVED,
reason="grant:<id>") with no APPROVAL_PENDING; plus tool-binding, no-cap
T3/T4, PROJECT same-repo vs different-repo isolation, restart persistence,
revoke-re-prompts, session-grant-no-leak regression, and replay determinism.
Indexed in README; BACKLOG drafted-plans pointer updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 22:41:02 +00:00