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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Six QA plans (docs/qa/QA-*.md) pinning observable evidence for the shipped tracks:
research egress + batch source-approval (D), architect contradiction (B§4), llama health
probe (A§4), idea promotion (E), reviewer static-first filter (B§5, partial), and the
brief-echo gate ARM-IT plan (C-A1) — the latter is the go/no-go for arming the gate in
production (it breaks the planner if the model can't emit a parseable brief_echo).
Plus the env: docs/qa/ENV.md runbook + docs/qa/README.md index, and scripts/qa/
(sync-config.sh, searxng-up/down.sh with the JSON-format gotcha, README). Scripts are
set -euo pipefail, syntax-checked, executable. Authored from the repo build/config
(server :apps:server:run, CLI :apps:cli:run, tui-go GOTOOLCHAIN=auto, sample-config.toml).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
A§4 LLAMA_SERVER probe is now registered (gated on a llamacpp provider URL); EVENT_STORE
latency probe was already wired (266bbf0). Both move to RETRO. Remaining A§4: tokens/sec
throughput feed (health endpoint is reachability-only) + live health TUI pane.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Architect contradiction-check is now live (server subscribes to the architect design
artifact and emits the display-only flag). All cleanly unit-verifiable deterministic
backlog tracks are now landed; what remains is model-dependent activations (live-QA
gated) and deep session-lifecycle work (§H) better done with live testing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
f107ff5 (idea-board -> profile promotion) moves to RETRO. Plan-comparison view marked
BLOCKED: no plan-candidate event exists server-side (only workflow.proposed), so there
is no wire shape to render or unit-verify until a multi-candidate planner lands.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
ab7e4be (egress activation) and 4b0024f (batch source-list approval) move to RETRO;
§D is now fully landed except the standalone web-approval client (needs network/SearXNG).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
ToolCallAssessmentInput gains sessionEgressHosts; SessionOrchestrator resolves it at
the assessment build site by folding EgressHostsGrantedEvents through
EgressAllowlistProjection. NetworkHostRule now enforces session-granted hosts (union
with static), replacing the emptySet() TODO — the allowlist built in 027ff1f is live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PlanStage gains a `writes` field (planner-declared workspace-relative paths/globs);
PlanDerivedManifest.deriveAllowedWrites + combine union it with the static baseline.
ExecutionPlanCompiler wires the union into StageConfig.writeManifest, enforced by the
existing ManifestContainmentRule via the same glob matcher (no parallel matcher).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
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>
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>
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>
`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>
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>
- StaticFinding/StaticFindingSeverity + StaticFindingsRecordedEvent (registered + test)
- StaticAnalysisRunner over an injectable CommandRunner; parses compiler/ktlint/detekt
finding formats (lenient) into StaticFindings
- excludeStaticFindingsFromReview pure filter, live-wired into SessionOrchestrator
context assembly so static-tool findings are kept out of the reviewer's context
- static_check pipeline stage left as a documented TODO (needs an event-emitting
deterministic stage-type seam that doesn't exist yet)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- EgressHostsGrantedEvent (registered + serialization test): additive per-session
egress grants (e.g. an approved research source list)
- EgressAllowlist pure union helper + EgressAllowlistProjection (folds grants per
session); NetworkHostRule delegates to the union, preserving exact/suffix semantics
- rule not yet fed the session set live (ToolCallAssessmentInput has no sessionId);
precise TODO(wiring) left. batch fetch-approval skipped (needs approval-flow surgery)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- promote research fetch quality + content_sha256 from ToolExecutionCompletedEvent
metadata to first-class SourceFetchedEvent / LowQualityExtractionEvent (registered
+ serialization test); existing metadata write kept intact (additive)
- emitted from SandboxedToolExecutor on research-fetch tool completion, guarded on
the url/content_sha256/quality markers; reuses the extractor's LOW_QUALITY marker
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
- StageCheckpointPassed/FailedEvent (registered + serialization test): per-stage
reconciliation of produced artifacts vs the locked plan's produces-slots
- StageCheckpointReconciler (pure) + tests
- emitStageCheckpoint wired in after verifyProduces, runs regardless of its
pass/fail (verifyProduces still owns the halt); gated on an ExecutionPlanLocked
in the session log so non-plan workflows are unaffected
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- BriefEcho model + BriefEchoMismatchEvent (registered + serialization test)
- BriefEchoExtractor: parses the planner's brief_echo block and the original
brief's referenced-files/symbols/acceptance-criteria dimensions
- BriefEchoComparator: conservative diff — a dropped acceptance criterion or an
invented file path halts; dropping a file/symbol alone does not
- checkBriefEcho gate chained into the post-stage flow after groundBriefReferences;
opt-in via stageConfig.metadata["briefEcho"], fails the stage (retryable) on
divergence with re-grounding feedback
- prompt/workflow activation (planner.md emitting brief_echo; role_pipeline
metadata) left as live-QA follow-up
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Standalone SceneValidator(content) -> ValidationReport: header well-formedness,
ExtResource/SubResource id resolution, node parent-path integrity. Not wired into
any pipeline (validation layer only; scene editing is a later epic). (BACKLOG I)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
- 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>
- 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>
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.