Commit Graph

318 Commits

Author SHA1 Message Date
kami 119f59d637 test(server): bind workspace in contradiction hook fixture (#266) 2026-07-21 02:13:05 +04:00
kami a2bf976a13 fix(server): unify session workspace root (#266) 2026-07-21 01:48:45 +04:00
kami 918f2f5652 feat(stage-workingset): durable, replay-safe stage working set (slices 1-4)
Stage agents were spending most of their turn budget re-discovering files
already known from prior stages/attempts. Root cause: nothing durable carries
acquired knowledge across handoffs and retries. First four slices of the fix:

- core:sourcedesc — new dependency-free module: describe(path, bytes) derives
  comment-free structural navigation metadata (module, bounded symbols, bounded
  imports, versioned format). Deliberately non-prose: descriptors are derived
  from agent-writable files and rendered into successor-stage context, so
  comments/docstrings/literals are excluded to close a prompt-injection channel.
  CAS post-image hash stays authoritative; descriptor is disposable navigation.

- kernel retry-repair state (ContextFeedback): on retry, name the authoritative
  CAS images of files this stage already wrote so the agent patches them instead
  of re-reading to rediscover them.

- kernel file-written manifest (SessionOrchestratorArtifacts): each produced
  file surfaced with its authoritative CAS image plus a comment-free structural
  descriptor (via core:sourcedesc, over recorded CAS bytes — replay-safe).

- apps/server RepoMapIndexer: route the injected repo-map descriptor through the
  comment-free describe(). Previously scraped leading comments, which were
  embedded into L3 and surfaced verbatim to successor stages — an injection
  channel from one stage into the next. Structural facts (module + imports +
  symbols) remain as the retrieval signal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 14:22:12 +04:00
kami b43ab77eee feat(server): REST /clarify route for headless clarification answers (#42)
Discovery-stage clarifications could only be answered over WebSocket
(ClientMessage.ClarificationResponse), so the curl-based headless QA
driver parked forever at discovery.

Add POST /sessions/{id}/clarify mirroring approveStageRoute. The server
resolves the live (stageId, requestId) from the session id via
SessionOrchestrator.pendingClarificationFor() — the newest still-live,
unanswered ClarificationRequestedEvent from the event log — so a curl
caller need not know the requestId. Empty answers = free-text skip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 23:18:47 +04:00
kami 68c56b6af6 feat(freestyle): return grounding-rejected plan to architect for a bounded re-run
A plan that failed grounding used to dead-end — every gate rejection in
FreestyleDriver.lockAndRun was terminal, so a legitimate grounding catch
(e.g. a stage declaring a PROJECT build with no manifest) left the run stuck
with no retry.

lockAndRun is now a gate loop: on a grounding rejection with retries left it
re-runs the planning workflow from the architect stage (rerunArchitect), which
emits a corrected plan, then re-gates. Other gate failures — and grounding once
maxGroundingRetries is spent — stay terminal.

- FreestyleDriver: gate loop + rerunArchitect/maxGroundingRetries seams;
  groundPlan returns findings (String?) instead of Boolean; post-grounding
  tail extracted to lockAndRunGrounded.
- DefaultSessionOrchestrator.runFrom(startStage) + emitWorkflowStarted(startStage);
  run() delegates to it. Lets the re-run enter directly at architect.
- buildGroundingFeedbackEntry (ContextFeedback) injects the already-recorded
  PlanGroundingEvaluatedEvent findings into the architect's L1 context on re-run;
  wired in SessionOrchestratorExecution.
- Main: rerunArchitect lambda (rehydrate -> runFrom(architect) -> rehydrate).

The architect stage-entry approval gate already reuses a prior APPROVED decision
(alreadyApproved), so the re-run does not re-prompt the operator — added a
FreestyleApprovalGateTest regression guard proving runFrom(architect) with a
seeded approval emits no second request and runs straight through.

Tests: FreestyleDriverTest retry-then-lock + exhaustion->reject(source=grounding);
FreestyleApprovalGateTest reuse-approval guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 03:00:35 +04:00
kami 1b58bc325e wip(freestyle/acr): grounding & edit-tool fixes + ACR-compiler experiment
This branch's uncommitted WIP, committed together (entangled at file level).
Distinct pieces of work:

Freestyle QA fixes (this session):
- FileEditTool: pre-validate replace anchor in validateRequest — reject a
  missing/ambiguous target BEFORE the approval gate, mirroring read/write's
  file-not-found / read-before-write pre-checks. Shared not-found/ambiguous
  messages between validate and execute so they can't drift.
- PlanGrounder: add `scanned` flag; when no RepoMapComputedEvent was recorded,
  repoMapPaths is "unknown" not "empty workspace" — skip scope grounding
  (which proves a path ABSENT) so real paths (apps/server/**) aren't falsely
  rejected. Build-manifest check still runs.
- FreestyleDriver: wire scanned=(repoMap!=null); on plan rejection emit a
  session-terminal WorkflowFailedEvent so a rejected run reads FAILED, not the
  COMPLETED-lie (last verdict was the planning-phase WorkflowCompleted).
- ServerModule: resolve project-memory workspace root from the session's bound
  workspace (sessionWorkspaceRoot) instead of boot-static pm.repoRoot(), fixing
  the workspace-binding divergence (correx vs empty scratch dir). Retire tracked
  in Vikunja #266.
- LaunchRegistrationRaceTest: join registered jobs before asserting launchCount
  — computeIfAbsent returns the Job immediately but the fire-and-forget launch
  body lagged awaitAll (the 49-vs-50 flake).

ACR concept-compiler experiment (pre-existing WIP on this branch):
- ExecutionPlanCompiler/Model/PlanLinter, #264 needs-seam (sessionArtifacts),
  LSP diagnostics subsystem (LspDiagnosticEvents/Runner/Lsp4j), BootWorkspace,
  config surface, workflow prompts/schemas, orchestrator advance-don't-rerun.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 01:20:37 +04:00
kami 6010f6b6c0 feat(server): bootstrap codebase-memory index at workspace open (#242)
codebase-memory search tools failed 9/9 ("project not found or not indexed")
because index_repository was never called for the repo. After MCP servers
mount, if one advertises index_repository, invoke it once with the workspace
root (repo_path) through the normal ToolExecutor path, on a background daemon
so a slow index doesn't block startup. "fast" mode to reach usable quickest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 14:17:00 +04:00
kami 9e62097c97 test(server): verify WS session stream forwards live ApprovalRequired events (#190)
SessionStreamHandler.handle() already registers the connected socket into
ApprovalCoordinator's per-session client map before entering the inbound
read loop, and ApprovalCoordinator.broadcast() (fed by ServerModule.start()'s
live subscribeAll().filter{ApprovalRequestedEvent} subscription) already
targets that map — so a gate firing after connect does reach the socket,
and CLI --auto-approve is not actually blind. Add integration coverage
(none previously existed for SessionStreamHandler) proving: (1) a live
ApprovalRequired fired post-connect reaches the socket, and (2) a
disconnected client is cleanly deregistered without disrupting delivery
to a later client on the same session — covering the 8d7c827e non-blocking
emit and 3559ea67 cleanup-on-termination invariants.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 12:18:51 +04:00
kami 63e8b4f5d6 feat(tools): native MCP client — mount stdio MCP servers as Correx tools
Adds an MCP host layer so external MCP servers (AST/LSP code-intelligence,
package resolvers, etc.) can be mounted at startup and surface their tools/list
as first-class Correx tools named mcp__<server>__<tool>. Each MCP tool is a
Tool+ToolExecutor, so it rides the normal ToolExecutor path and inherits tier
gating, receipts, and event-recorded execution (#5) with zero special-casing;
replay reads the recorded receipt rather than re-calling the server (#8/#9).

- McpProtocol/McpStdioClient/McpTool/McpMounter in infrastructure:tools
  (stdio JSON-RPC 2.0, tools/* slice only). Capabilities empty; safety via
  default T2 tier since external side effects are opaque.
- [[mcp]] config (id/command/env/tier) + array-of-tables parsing.
- Main wires mounted servers into extraTools (main + per-workspace paths) with
  a shutdown hook; a server that fails to start is logged and skipped.
- Tests: in-process fake transport (handshake/list/call/error + tool mapping),
  [[mcp]] parser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 19:26:01 +04:00
kami 53d8108189 feat(orchestration): plan grounding, positive-pattern mining, stage-prompt audition rig
Vikunja #167/#168/#169.

#167 PlanGrounder (infrastructure/workflow): build-prereq + touches-scope
existence grounding, emits PlanGroundingEvaluatedEvent. Deterministic fold
over recorded workspace/plan events (invariants #8/#9).

#168 positive-pattern mining (SessionOrchestratorPlanPatterns): mine
SuccessfulPlanShape from cross-session log — a locked plan whose own
workflow later completed — and inject the closest-resembling plan shape as
L0/SYSTEM advisory context for the planning stage. Keyword-Jaccard
resemblance, no LLM/embedder, replays identically. Advisory only (#3).

#169 stage-prompt audit + CORREX_STAGE_GUIDANCE ON/OFF toggle in
SessionOrchestratorExecution. RunCommand gains --intent to seed a
freestyle session over REST.

Also: workspace verification events + concept-compiler wiring. ./gradlew
check green.
2026-07-16 14:55:56 +04:00
kami ed7efb6072 Implement closed-loop workspace follow-ups 2026-07-15 23:48:12 +04:00
kami 3a48ecd24f feat(concept): heuristic concept compiler (ACR fold-in)
Promotes recurring validated failure->fix patterns into L3 as retrieval-on-demand
concepts. Deterministic core: ConceptCompilerProjection clusters
RetryAttempted->StageCompleted pairs by fingerprint (gate-agnostic), promotes at
N=3 cross-session validated fixes, never-contradicted. ConceptPromotedEvent is the
sole authoritative write (idempotent under replay); ConceptCompilerService appends
it + best-effort injects to L3 (non-authoritative, inv #6). Wired live in
ServerModule.start() on StageCompleted.

Design: docs/plans/2026-07-12-acr-concept-compiler.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DhFXmKe4WisSSPf9LrmmTg
2026-07-15 13:35:17 +04:00
kami d69cb12ce9 refactor: decomposition WIP (orchestrator/server/execution-plan)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DhFXmKe4WisSSPf9LrmmTg
2026-07-15 13:17:01 +04:00
kami aee2e67c66 refactor(detekt): extract magic-number constants, wrap long lines, drop legit suppressions
Mechanical detekt cleanup across apps/core/infrastructure (behavior-preserving):
- MagicNumber 62->10: named constants + removed 'legit' MagicNumber suppressions
  (Tier level from ordinal, ReplayInferenceProvider CHARS_PER_TOKEN, ConfigLoader
  DEFAULT_* consts, capability scores, HTTP ranges, column widths, etc.)
- MaxLineLength cut to ~0 in production modules (line wraps, no logic change)
- Dropped one dead parameter (ConfigLoader.parseArray lineNum)

Structural findings (ReturnCount/LongMethod/Complexity/LargeClass) left for a
deliberate decomposition pass. Full build + detekt gate green.
2026-07-12 23:12:28 +04:00
kami 2912799fe0 feat(tools): bound+frame tool results, spill full output to CAS, tool_output retrieval
Tool results injected into model context are now consistently framed and
globally bounded. Success output over the floor (TOOL_RESULT_MAX_CHARS=8000)
is head/tail-truncated with a marker naming a retrieval ref; the full raw
output spills to the artifact store (CAS) and its hash is recorded on the
ToolReceipt (fullOutputHash) in the event log. Agents recover the full text
via the new read-only tool_output(ref=...) tool.

- SessionOrchestrator: frameTruncatedToolResult + renderToolResult; char-cap
  head/tail so a single pathological long line can't defeat the bound.
- ToolReceipt.fullOutputHash (additive, nullable).
- ToolOutputTool (Tier T1, no fs capability) resolves ref -> full bytes.
- Wired via extraTools in Main.kt; tool_output added to ALWAYS_AVAILABLE_READ_TOOLS.
- Failure path keeps ERROR:/FATAL: prefixes (all-rejected breaker dependency).

Tests: FrameTruncatedToolResultTest, ToolOutputToolTest.
2026-07-12 19:46:02 +04:00
kami 3a4e577b5b refactor(server): decompose DomainEventMapper god-when into per-domain files
DomainEventMapper.kt had grown into a single ~280-line `when` over every
domain event type with a ~40-line flat import list ('god mapper' smell).
Split the when body by domain area into four files, each a suspend fun
returning a MapOutcome sentinel:

- SessionEventMappers.kt   — session/workflow lifecycle + chat turns
- StageInferenceEventMappers.kt — stage transitions + inference lifecycle
- ToolEventMappers.kt      — tool exec/assessment + review findings (+ prettyToolParams)
- LifecycleEventMappers.kt — approval/clarification/proposal/narration/artifact/model/preempt

The dispatcher chains them; MapOutcome.Emit(msg?) vs MapOutcome.Skip
preserves the null-vs-unhandled distinction (a suppressed event like
ArtifactValidating returns Emit(null), not Skip). Public entry points
(DomainEventMapper class, domainEventToServerMessage, prettyToolParams)
unchanged — no behavior change, pure maintainability. Each per-domain
import list is now local to its file. DomainEventMapperTest green.

Vikunja #31.
2026-07-12 18:39:33 +04:00
kami d89d4e32a9 feat(inference): operator-tunable sampling knobs (top_k/min_p/repeat_penalty) for stage requests
GenerationConfig only carried temperature/top_p/max_tokens/stop/seed. Added nullable
topK/minP/repeatPenalty, serialized to the llama.cpp and OpenAI-compat request bodies
via @EncodeDefault(NEVER) so an unset knob is omitted (the model keeps its own default)
and behavior is unchanged unless the operator opts in.

Surfaced as a new [sampling] config section feeding the default stage GenerationConfig
(the main agentic loop) through TomlWorkflowLoader + ExecutionPlanCompiler; the former
hardcoded temperature=0.7/topP=1.0 stage defaults now come from config. Talkie
chat/narration keep their own generation settings.

Vikunja #46 (task 76) — sampling half.
2026-07-12 17:54:25 +04:00
kami b2c7bbe401 feat(config): relocate orchestration loop/threshold/budget constants to [orchestration] config
The kernel's ReAct-loop tuning constants (max tool rounds, read/rejection-loop
nudge thresholds, feedback issue cap, repo-map top-k/files-per-dir, docs catalog
cap, clarification-round cap, review-block confidence/retry cap, default
refinement, recovery/intent route budgets) were hardcoded in SessionOrchestrator/
DefaultSessionOrchestrator. Relocated to a new OrchestrationTuning value threaded
through the orchestrator constructor, mapped from CorrexConfig.orchestration in
Main.kt, parsed in ConfigLoader, written by CorrexConfigWriter. Defaults equal the
former constants so an absent [orchestration] section reproduces prior behavior.

Startup-load (not hot-reload); pure output-truncation caps left as constants.

Vikunja #46 (task 76).
2026-07-12 17:45:54 +04:00
kami 8d7c827ebb fix(server,persistence): stop slow WS client stalling kernel; log dropped emits (#53)
Two event-delivery back-pressure faults from the core audit (#7).

(a) streamGlobal forwarded globalFlow → a per-connection Channel with
buffer.send (SUSPEND). globalFlow is also SUSPEND-overflow, so one wedged
WebSocket client back-pressured globalFlow → append() suspended → the whole
kernel stalled. Now the collector uses trySend; on overflow it closes the
buffer with SlowClientException, tearing down that one connection (client
reconnects and re-syncs via replaySnapshot) instead of blocking append().

(b) Per-session subscriptions (extraBufferCapacity 64) used tryEmit with the
return ignored: a lagging in-process collector (e.g. LiveArtifactRepository)
silently diverged from the durable log. Both SqliteEventStore and
InMemoryEventStore now warnIfDropped() — the event is still persisted; only the
live SharedFlow drops, and we surface it. ponytail: log-only; bounded
rebuild-on-lag if divergence ever bites.

Added slf4j-api to :infrastructure:persistence (already the standard logging
dep across modules) for the warn.

Green: :infrastructure:persistence + :apps:server compile+detekt+test (the one
pre-existing ModelLifecycleWiringTest failure needs a real llama-server, fails
identically on clean checkout).
2026-07-12 17:11:41 +04:00
kami 3559ea67ef fix(kernel,server): evict per-session caches on workflow termination (#54)
Two unbounded per-session leaks that never released after a workflow ended:

- SessionOrchestrator.artifactContentCache (full file contents, keyed
  "<sessionId>:<path>") grew for the process lifetime. Added
  evictArtifactContentCache(sessionId) — session-prefix key removal,
  rehydrate-safe — called on both completeWorkflow and failWorkflow.
  cancellations was already evicted on both terminal paths.

- NarrationSubscriber leaked a Channel + worker coroutine + lanes map entry
  per session forever. closeLane() now closes the lane channel on
  WorkflowCompleted/WorkflowFailed (draining the terminal narration first);
  the worker self-removes from lanes when its for-loop exits on close.

Deferred: SqliteEventStore.subscriptions eviction — removing the SharedFlow
mid-life would strand LiveArtifactRepository's downstream collector (a
suspended-coroutine leak worse than the tiny empty-SharedFlow entry).

Green: :core:kernel compile+detekt, :apps:server *NarrationSubscriber* (8).
2026-07-12 17:05:34 +04:00
kami 25ab4faac9 server: S5 memoize listSessionSummaries + S6 Main.kt hygiene
S5 — GET /sessions re-read + re-projected every session's full log on every
call. Cache keyed on eventStore.lastGlobalSequence(); volatile pair-swap,
no lock — any append bumps the seq so a stale read is impossible.

S6 — close research + health-probe HttpClients via shutdown hook; log the
real configured host:port (was hardcoded 8080) and bind to it; System.err
-> SLF4J; WARN at startup on non-loopback bind (unauthenticated surface).
resolveApiKey precedence item N/A — no such code exists.

Both verified green in isolated worktrees (226 pass). Full local build
currently blocked by an unrelated concurrent core:config edit.
2026-07-12 12:28:19 +04:00
kami df4dd5b155 fix(server): move stuck-approval-pause repair from WS connect to boot (S3)
SessionEventBridge.replaySnapshot ran per WS connection and appended a repair
OrchestrationResumedEvent — a write side effect on a pure read path that two
concurrent clients could double-append (and 'open TUI' mutated replay history).
Moved to a one-shot ServerModule.repairStuckApprovalPauses() at boot; replaySnapshot
is read-only again. The state it repairs comes from a fixed pre-Feb-13 bug, so no
session newly enters it at runtime — boot coverage is sufficient.

Note: :apps:server:test not run (core:kernel broken by concurrent work); edits are
isolated and mirror the existing preRegisterPendingApprovals shape.
2026-07-12 12:22:04 +04:00
kami 979e2c3a16 fix(approval): log + evict on failed approval broadcast (S4)
broadcast() wrapped each client.send in a bare runCatching with no logging or
dead-client eviction, so an approval prompt could vanish silently and the session
hang until reconnect. Now log+evict failed clients from both registries and error
when a prompt reaches 0 live clients.

Note: :apps:server:test not run (core:kernel temporarily broken by concurrent work);
change is isolated, compiled clean before the breakage, detekt passes.
2026-07-12 12:19:16 +04:00
kami caeb9f0868 fix(server): correct pause-reason labels (S7) + bind workspace on REST launch (S8)
S7: DomainEventMapper mapped every non-APPROVAL_PENDING pause (CLARIFICATION_PENDING,
ABANDONED_STALE) to USER_REQUESTED, so clients rendered the wrong pause state. Added
the two PauseReason enum values, mapped the real reason string, and gave the Go TUI
distinct labels.

S8: REST POST /sessions launched a run without resolving/recording a workspace, so the
session had no SessionWorkspaceBoundEvent (path containment, project profile, grants had
nothing to anchor to). Extracted the WS path's resolve+emit into ServerModule.bindWorkspace
and called it from both launchers.
2026-07-12 12:16:34 +04:00
kami e2f44387e1 fix(approval,stream): resolve-after-submit + real tier (S1), persisted sessionSequence on live frames (S2)
S1: ApprovalCoordinator marked a request resolved before submitApprovalDecision,
so a throwing submit left it permanently unanswerable; now resolve only on success
and clear the flag on failure so the client can retry. Recorded decision also used
a hardcoded Tier.T2 regardless of the real tier — now tracked per request.

S2: streamGlobal numbered live frames from a per-connection counter starting at 1,
colliding with the snapshot's lastSessionSequence (MAX(session_sequence)) after
reconnect and causing the TUI dedup filter to drop/misorder frames; now uses each
event's own persisted sessionSequence. Approval broadcast no longer sends 0.
2026-07-12 12:11:49 +04:00
kami 15248cae8a feat(recovery,context): tier-2 intent-holder arbiter + remaining-delta pinning + surfaced signal events
- recovery: two-tier repair ladder — owner then arbiter (recovery stage recast
  as intent-holder, holds initial intent, reconciles cross-file contract disputes)
  with independent INTENT_ROUTE_BUDGET; RecoveryRoutingTest coverage
- context: remaining-delta checklist pinning (RemainingDelta{Pinning,Entry}Test)
- surface write-only events (session name, quality signals) into stats/browse
- parseToolArguments lenient-Json fallback for bare-key drift
- tools: ChildProcess seam
2026-07-11 23:56:52 +04:00
kami 3d5e05c1fb feat(context): reasoning-thread continuity, L3 doc filtering, and gate hardening
Threads reasoning_content across inference calls and journal renders, filters
markdown noise out of L3 repo-knowledge retrieval, adds PlanLinter H3 checks,
and tightens filesystem tool output/dir-listing behavior surfaced by prior
live QA (see project_readloop_campaign memory).
2026-07-10 11:13:43 +04:00
kami f51a8dada4 feat(recovery): route failed write-less stages to recovery instead of futile retry
Adds the failure-ticket + recovery-routing mechanism: a deterministic
gate->capability table gates whether a stage has agency to fix its own
failure, opens a FailureTicketOpenedEvent when it doesn't, and routes to
a metadata role=recovery stage (per-stage budget, cap 2, not reset by
TransitionExecuted) instead of retrying in place. Extends salvage
decisions with a RECOVER option so the review-gate judge can also route
to recovery, unifies deterministic-gate and review-gate routing through
routeToRecovery(), and has ExecutionPlanCompiler synthesize a
write-capable recovery stage + edge for freestyle plans. Read-only tools
(file_read, list_dir) are now always available on any tool-granting
stage so a recovery stage can inspect the write-less stage's failure
without flooding context via shell ls -R.
2026-07-08 10:28:53 +04:00
kami d6bada6f10 feat(clarification): rehydrate pending questions on reconnect + restart
A stage parked on operator clarification lost its modal whenever the
client disconnected or the server restarted — the questions were stranded
in the event log with no way to re-surface them (unlike approvals, which
already rehydrate).

Two composing mechanisms, mirroring the approval path:

- On reconnect: SessionEventBridge.replaySnapshot() now re-pushes the last
  unanswered ClarificationRequestedEvent for a PAUSED/CLARIFICATION_PENDING
  session as a ClarificationRequired message. Gated on the orchestrator
  still holding a live deferred (SessionOrchestrator.liveClarificationRequestIds)
  so a client never sees a modal whose answer could not be delivered.

- On restart: ServerModule.resumeAbandonedSessions() eagerly relaunches
  clarification-parked sessions (approval-parked stay lazy). rehydrate+resume
  re-runs the parked stage, which re-emits a fresh, live
  ClarificationRequestedEvent (round budget MAX=3, 1 prior round recorded, so
  it re-parks rather than silently proceeding), re-arming the deferred that
  the reconnect path gates on.

pauseReason == "CLARIFICATION_PENDING" is the discriminator because the
reducer sets pendingApproval=true for every pause.
2026-07-07 18:30:09 +04:00
kami 8cfc590c7e fix(tui): soft-wrap clarification question prompts
The clarification modal hard-truncated each question's head line with
clip(), so long prompts (e.g. an API question and an Architecture
question) were cut at the modal edge and unreadable. Wrap the prompt
across rows instead: wrapPrompt() greedily word-wraps with a narrower
first line (marker + header chip eat into it) and full-width
continuation lines, indented to align under the prompt.
2026-07-07 18:29:56 +04:00
kami 82c55ec9c7 feat(context): doc catalog — frontmatter/H1 descriptor as read-on-demand index
Docs were indexed as up to 40 h1-h3 headings per .md (a TOC flood) and
force-fed or excluded wholesale by a binary docs gate. Replace with a
compact, always-on catalog the agent reads on demand.

- RepoMapIndexer: .md now yields ONE descriptor (frontmatter
  description/summary/title -> first # H1 -> first prose line), not a
  heading dump. Replay-safe (still List<String> symbols).
- SessionOrchestrator: always-on '## Docs available (file_read to open)'
  catalog — top-N docs by recency as 'path — descriptor', hard-capped at
  DOCS_CATALOG_MAX. One line each, so it stays present without the
  poisoning the docs gate suppressed. Source layout still excludes doc
  content for non-doc prompts.
2026-07-07 14:52:49 +04:00
kami efb6eb0334 feat(validation): capability-gap reflection rung (#30 part 2)
Bounded LLM "are you sure?" pass over the capability gaps part 1 detects, run
once at plan-lock before the operator is asked to approve — honest mistakes
self-correct without escalation; only a genuine tool need reaches the human.

- CapabilityGapReflector: fun-interface seam in core:kernel (plain-data in/out,
  no infrastructure:workflow dep), mirroring SemanticReviewer/SalvageJudge.
- CapabilityGapReflectorImpl (apps/server): one InferenceRouter call for the
  whole gap batch, no per-gap loop, no retry; runCatching degrades to a safe
  RESOLVED-advisory default on any failure (unroutable/timeout/malformed JSON) —
  a broken pass can never manufacture a NEEDS_TOOL escalation or a grant.
- CapabilityGapReflectedEvent(verdict RESOLVED|NEEDS_TOOL) recorded per gap and
  registered in eventModule; replay reads the event, never re-invokes inference
  (invariants #7/#8/#9).
- FreestyleDriver.lockAndRun: RESOLVED is advisory (recorded, plan untouched —
  invariant #3); NEEDS_TOOL is appended to the existing requestPlanApproval
  preview so the operator decides — no auto-grant anywhere (#4/#5).
- reflector nullable -> degrades to part-1 behavior when unwired.

Verified: ./gradlew :core:events:test :core:kernel:test :infrastructure:workflow:test :apps:server:test green.
2026-07-07 13:45:00 +04:00
kami 41ed6414c6 feat(guardrails): steering channel + shell-in-file rule + capability-gap detector
Bundles three operator-reliability guardrails (Vikunja #28/#29/#30) plus the
in-flight branch WIP they were built on top of (reasoning_content capture,
operator/project profile editor, write-jail workspaceRoot fix) — the tree is
interdependent (SessionOrchestrator references reasoningArtifactId from the WIP)
and does not compile as separable subsets, so it lands as one commit.

Guardrails:
- #28 mid-stage steering: ClientMessage.SteerSession -> GlobalStreamHandler ->
  orchestrator.submitSteering, reusing SteeringNoteAddedEvent + existing context
  fold (advisory, non-authoritative; invariants #3/#7). Closes the gap where
  steering typed off an approval gate was silently dropped.
- #29 shell-in-file guardrail: ShellInFileContentRule (core:toolintent) blocks a
  file_write whose content is a bare shell command (e.g. "mkdir -p ..."); FileWriteTool
  description now advertises auto-mkdir of parent dirs. Basename-allowlist so the
  extensionless case is caught; scripts/Makefiles/multiline exempt.
- #30 pt1 capability-gap detector: deterministic CapabilityGapDetector maps stage
  intent -> implied ToolCapability, compares to granted tools, emits advisory
  CapabilityGapDetectedEvent in FreestyleDriver.lockAndRun. Recorded, never fails
  the gate and never auto-grants (invariants #3/#4/#5). Reflection rung is pt2.

Verified: ./gradlew check green (whole tree).
2026-07-07 13:27:59 +04:00
kami 879672a47d feat(recovery): failure-ticket routing + review-gate RECOVER + return-to-sender
Retry-agency invariant: a stage may only retry a gate it has the capability
to change. A write-less stage failing a build/contract/static gate (e.g.
freestyle final_verification with allowedTools=[shell]) can never fix it, so
retrying in place is futile until the budget drains (Vikunja #41).

Instead: open a FailureTicketOpenedEvent (category + requiredCapability derived
deterministically from the gate id, no LLM) and route to a recovery stage that
holds the capability, bounded by a small per-stage route budget.

- Slice 2: SalvageDecision.RECOVER (ternary CONTINUE/RECOVER/FAIL) lets the
  review-gate salvage judge hand off to recovery. Shared routeToRecovery()
  unifies the deterministic agency guard and the judge on one destination;
  decideGateExhaustion now returns StepResult?.
- Return-to-sender: recovery's exit is dynamic (recoveryReturnMove) — it goes
  back to the exact ticket-origin stage to re-run its gate, so a write-less
  gate anywhere in the graph is handled without skipping intervening stages.
  The synthesized recovery->terminal edge is now only a no-ticket fallback.
- Freestyle wiring: ExecutionPlanCompiler injectRecovery flag (Main passes
  true) synthesizes a write-capable recovery stage; ticket evidence reaches it
  via buildRecoveryTicketEntry.
- Read-only tools always present: StageConfig.effectiveAllowedTools adds
  file_read/list_dir to any tool-granting stage (fixes the verifier reaching
  for `shell ls -R` to inspect the tree).

Tests: RecoveryRoutingTest (deterministic gate, no static return edge — proves
dynamic return) + GateRetryBudgetExhaustionTest RECOVER case + two compiler
tests. All green; detekt clean.
2026-07-07 12:05:26 +04:00
kami 79e2c38a88 feat(retry): per-gate retry budgets + progress-aware charging + hybrid salvage
Replaces the single shared per-stage retryCount (reset on TransitionExecuted,
shared across all post-stage gates) with a per-gate budget. Motivated by run
2e468f9f, where a promising freestyle run was terminally FAILED because the
contract gate burned all 3 shared retries on one trivial miss before the
semantic-review gate ever ran.

- StageExecutionResult.Failure gains a `gate` id; each gate tags its failures.
- OrchestrationState gains gateRetryBudgets / gateFailureFingerprints /
  gateSalvageUsed (all rebuilt from events, reset per-stage on TransitionExecuted).
- FailureFingerprint normalizes+hashes the failure reason; RetryCoordinator.decide
  charges a gate's budget only when the fingerprint is unchanged (no progress),
  so a moving run is never penalised. Returns Retry | Exhausted.
- Hybrid exhaustion: deterministic gates fail; the review gate consults a
  SalvageJudge (LLM, or a deterministic allow-one-reset fallback), recorded as
  RetrySalvageDecidedEvent (invariant #9). CONTINUE resets the gate budget once;
  a second exhaustion is terminal.
- Review gate is now the sole authority on review-retry termination; the old
  REVIEW_BLOCK_RETRY_CAP is repurposed as a high absolute backstop only.
- ServerModule escaped-exception path now records a truthful terminal
  WorkflowFailed (real stage/reason/retryExhausted) via recordUnhandledFailure.

Tests: DefaultRetryCoordinatorTest (fingerprint charging) +
GateRetryBudgetExhaustionTest (deterministic exhaust / review CONTINUE-then-
terminal / review FAIL / null-judge fallback) + reducer coverage.
2026-07-06 19:20:46 +04:00
kami 9f12c87bb1 feat(validation): plan-compile gate + semantic-review gate (staged verification #19/#20)
Plan-compile gate (#19): PlanCompilationCheck seam (null-on-replay),
runPlanCompileGate guards a produced execution_plan slot and compiles its
cached content via ExecutionPlanCompiler, emitting PlanCompileCheckedEvent.
On failure the architect gets a retryable hand-back and self-corrects;
exhaustion terminates the dead-end workflow instead of shipping an
uncompilable plan.

Semantic-review gate (#20): SemanticReviewer seam + StageConfig.semanticReview
opt-in (TOML semantic_review + freestyle plan JSON). runReviewGate reads the
FileWritten manifest (never blind context), asks a review-capable model for
PR-comment findings, and is advisory — blocks retryably only on FAIL plus a
correctness finding >=0.7 confidence, capped at 2 blocks so a stubborn stage
completes advisory-only rather than trapping. SemanticReviewerImpl talks to
the provider directly; any reviewer error degrades to PASS. REVIEW_MAX_TOKENS
is 8192: gemma is a reasoning model and 1024 truncated it mid-reasoning before
it reached the JSON verdict (empty content -> parse degraded to PASS).

Both gates live-QA'd on the real repo: plan-compile passed on a freestyle
web-UI scaffold; semantic review caught all 3 planted bugs in a maxOf() and
exercised the block->retry->cap safety valve end-to-end.
2026-07-06 13:11:16 +04:00
kami ff1a0ffdad feat(validation): staged-verification funnel — contract + execution gates
Gate 2 (contract) and Gate 4 (execution) of the 4-gate staged-verification
design, plus the two gate-quality fixes found in live QA.

- Gate 2: KindContractTable (kind→assertions registry, universal floor,
  additive project overrides) + KindInference (path→kind, pure/replay-safe)
  + ContractAssertion model; ContractGateEvaluatedEvent (registered);
  ContractAssertionEvaluator seam + FileSystemContractEvaluator (FS/TEXT,
  COMPILER skipped); runContractGate in runPostStageGates — a stage that
  produces file_written artifacts must have every declared+written file
  exist/non-empty/parse, else retryable hand-back with per-assertion feedback.
- Option A: ExecutionPlanCompiler puts concrete (non-glob) plan `writes` into
  StageConfig.expectedFiles, so a declared-but-unwritten file fails file_exists.
- Gate 4: BuildExpectation enum (none|module|project|tests) + plan field +
  closed-set compiler validation; runExecutionGate resolves the alias against
  the bound project profile commands and runs it as a deterministic gate.
- Live-QA fixes: react_entry kind so main.tsx/index.tsx aren't required to
  export a default component (was an unwinnable false-positive); JSONC
  tolerance (allowComments/allowTrailingComma) so tsconfig.json passes valid_json.

Seams are null on the replay harness (no-op) — recorded events are truth (#9).
2026-07-06 01:25:51 +04:00
kami 595ec187bc refactor: rename the router subsystem to Talkie
The 'router' name was misleading — it's the always-on conversational front-end
(CHAT triage + STEERING into a running workflow), not a routing layer, and it's
distinct from InferenceRouter. Rename core:router -> core:talkie, package
com.correx.core.router -> com.correx.core.talkie, and RouterFacade/Config/State/
Repository/ContextBuilder/Projector/Reducer/Response -> Talkie*. Config section
[router] -> [talkie] (legacy [router] still read as fallback).

Persisted wire formats are preserved: ChatTurnRole.ROUTER and the
RouterNarrationEvent @SerialName("RouterNarration") stay, so existing event
logs still replay. RouterNarrationEvent the class is now TalkieNarrationEvent.
2026-07-03 13:26:00 +04:00
kami a0b3a1865a fix(kernel): deterministic repo-map floor + 0.0-hit filter + closest-path suggestion
RepoMapComputed recorded per session (in-memory scan cache keeps the walk-once
guarantee); zero-score embedder hits filtered before reaching the agent, falling
back to the deterministic repo map; REFERENCE_EXISTS surfaces a closest-path hint
via closestPathByFilename. Breaks the hallucinated-package-path read loop even
with a noop embedder.
2026-07-03 13:15:54 +04:00
kami ca3fd7971e fix(server): skip an unmappable event on replay instead of tearing down the stream
Wrap the per-event mapper.map in runCatching in the replay loop so a single
event the mapper can't handle no longer kills the whole WebSocket stream.
Defense-in-depth beyond the AnyMapSerializer fix.
2026-07-03 01:01:04 +04:00
kami 527490e5df fix(tui): surface late clarifications and stop bg bleed on input + action rows
- clarDismissed was model-global but only reset when the clarification's
  session was selected on arrival, so a clarification for an unselected
  session stayed silently hidden. Reset on every arrival.
- input bar (in-session + launcher) only truncated lines, never padded to
  width, so the terminal backdrop bled through the right. Pad with padTo.
- action/narration/user rows prefixed plain (unstyled) spacers around their
  icons, leaving transparent gaps near the symbols. Use bg-styled spacers.
2026-07-03 01:00:55 +04:00
kami 5e98f6661e feat(tui): render structured artifacts as a summary, not raw JSON
Approval-card and preview JSON artifacts (the freestyle execution_plan, the analyst's analysis) rendered as a raw JSON dump. Add a JSON-aware preview: a scannable goal + numbered stage list for execution_plan, indented key/value lines for other objects, with light highlighting. Non-JSON previews fall back to the raw line view unchanged.
2026-07-02 13:26:42 +04:00
kami 5deb2f6425 fix(tui): fill the frame background so the terminal backdrop can't bleed through
The main render path never applied the Screen backdrop, so any sub-view that left a line short or a row empty showed the transparent terminal background. Pad every frame to full width×height with the theme backdrop before display; content stays top-left anchored so caret coords are unaffected.
2026-07-02 13:26:42 +04:00
kami 9eb05fc0dd test: align events-limit test with tail semantics
The /events limit test still asserted the oldest event (seq 1) after 18cbd34 deliberately switched the no-fromSeq path to return the tail (so a pending approval at the tail stays visible to a headless operator). Expect the tail (seq 3) and add a post-filter tail assertion (seq 2) so the test actually verifies truncate-after-filter.
2026-07-02 12:33:52 +04:00
kami e3743835ec feat(server): wire EmbeddingRelevanceScorer into context builder, default level 4
Move embedder construction ahead of the engines so the context pack builder ranks
freeform turns by query relevance (query-conditioned selection). Default
compression_level 4 (format + static cache + token pruning + age-tiering) for a 12b
on a 16k window.
2026-07-01 14:33:00 +04:00
kami 047e2a4070 feat(context,infra): compression pipeline stages 4-5 — token pruning + relevance + ToMe
- build() is now suspend: pipeline runs all stages in fixed order, each gated by level
- TOKEN_PRUNE (level 3): TokenPruner interface + LLMLingua-2 sidecar (sidecars/llmlingua)
  + HttpTokenPruner adapter (fails open if sidecar down); prunes freeform, preserves
  protected spans, skips tier-0 turns when TIER_SPLIT on
- TOME_MERGE (level 8): ToMeMerger collapses near-duplicate freeform turns (Jaccard)
- Stage 5 selection: RelevanceScorer + EmbeddingRelevanceScorer (cosine over Embedder);
  query-conditioned reorder so least-relevant freeform drops first under budget
- [orchestration] compression_level + token_pruner_url config, wired in Main
- suspend ripple fixed across builder callers/stubs
2026-07-01 14:29:56 +04:00
kami 4be8f292ae fix(kernel,server,workflow): per-stage retry budget, sane freestyle budget, tail /events
Three defects surfaced by a live headless freestyle_planning run (all made a
long execution workflow fail or stall in ways invisible to a REST operator):

1. retryCount was session-global (DefaultOrchestrationReducer): TransitionExecuted
   updated currentStageId but never reset retryCount, while shouldRetry gates on
   it per-stage. An early stage that burned its 3 retries starved every later
   stage of its own — the next stage's first *retryable* failure went terminal.
   Reset retryCount on stage entry; back-edge loops stay bounded by the separate
   refinementIterations counter. Regression test added.

2. Freestyle stages ran at StageConfig's 4096-token default (ExecutionPlanCompiler
   never set a budget) vs 16384-32768 for static role_pipeline stages. A tool-heavy
   stage truncated its context every round, evicting the file_read results it just
   gathered, so it re-read forever and never accumulated enough to write. Set 16384.

3. GET /events returned the OLDEST `limit` events (readFrom(0) then take): for a
   session past `limit` events the tail — including a pending ApprovalRequested —
   was invisible to a headless/REST poller, the exact caller POST /approve serves.
   Default now tails; explicit fromSeq still paginates forward.
2026-07-01 13:41:06 +04:00
kami 5e0c7dfed5 feat(server): REST stage-approval route POST /sessions/{id}/approve
Stage approvals were WS-only (ClientMessage.ApprovalResponse), so a headless
run had no way to clear an approval gate — and apps/cli ApproveCommand already
POSTs to this path (it 404'd). The route is keyed by session: a session
suspends on at most one approval at a time, so ApprovalCoordinator resolves the
pending requestId server-side (new pendingRequestFor reverse lookup). Decision
maps approve/steer->APPROVE, reject->REJECT; routes through the same
ApprovalCoordinator.handleResponse the WS path uses.
2026-06-30 21:17:10 +04:00
kami fe6e530a75 Merge remote-tracking branch 'origin/feat/session-robustness-and-dox' into feat/session-robustness-and-dox 2026-06-29 20:58:07 +04:00
kami 473730060e fix(server): anchor static-registry file_read to the workspace working dir
buildToolConfig built FileReadConfig without workingDir, unlike the sibling
fileWrite/fileEdit configs (and buildToolConfigForWorkspace), so the default
tool registry's file_read resolved relative paths against the JVM process cwd
instead of the configured workspace. When the server is launched from outside
the workspace (e.g. `./gradlew :apps:server:run`, whose cwd is apps/server),
a `file_read .` from a curl-started session resolved to the launch dir, which
is not in the allowed paths, and was rejected with "Path '.' is not in the
allowed list" — while it worked on machines where the launch cwd happens to
coincide with an allowed path. Pass workingDir so relative paths anchor to the
workspace, matching the other file tools.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 16:40:57 +00:00