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.
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.
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.
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>
Add CapabilityAwareRoutingStrategy — it hard-filters to providers that declare every
required capability (like FirstAvailable) but ranks the matches by summed required-capability
score; ties keep list order, so it is a strict superset of first-available. The server now
wires it as the routing policy.
The orchestrator augments a stage's required capabilities with ModelCapability.ToolCalling
when the stage grants tools, so tool-heavy stages route to the best tool-calling model rather
than whichever healthy provider comes first.
Scores come from each provider's declared capabilities(); observed per-model reliability
(GET /metrics/tool-reliability) can feed in later as an additional weight.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enable autonomous QA through a remote OpenAI-compatible provider (NVIDIA NIM)
and harden the tool/approval path so unattended multi-stage runs complete.
- inference: add openai_compat provider (Bearer chat-completions for NIM/OpenAI),
dispatched by provider type "nim"/"openai"; key via api_key/api_key_env.
- server: bind configured [server] host/port instead of a hardcoded 8080;
POST /sessions accepts an optional `intent` (WS parity) for intent-driven workflows.
- kernel: thread the bound operator profile's approval_mode into per-tool gating so
auto/yolo enable unattended approval (engine still consulted; policy/plane-2 BLOCK
stays terminal); on a recoverable tool failure feed the tool's arg-schema back into
context so the model self-corrects instead of repeating a malformed call.
- tools: split deletion out of file_write into a separate, explicitly-named file_delete
tool — a model can no longer delete a file by getting a write-mode parameter wrong.
- server: add GET /metrics/tool-reliability — per-model tool-call validity from the
event log (measurement groundwork for capability-aware routing).
- docs: update AGENTS.md across kernel, tools, server, inference.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a write is blocked for being outside the claimed task's
affected_paths, the implementer can propose widening scope via a new
T2 propose_scope tool. The operator decides (invariant #4 — the agent
can't self-grant). Approve unions the proposed paths into the task's
affected_paths (existing TaskAffectedPathsSetEvent); activeScope
re-derives the wider manifest from events so the same write then passes.
Reject blocks the task (via TaskClaimCoordinator.blockActiveTask, hooked
on both approval-denial branches, scoped to propose_scope) so the loop
advances. No new event types.
A stage flagged claimTask gets the kernel (not the LLM) to claim the
next ready task — or re-surface the one already claimed on a review
bounce — and inject its TaskContextAssembler bundle as L0. While a task
is claimed, plane-2's write manifest narrows to the task's affectedPaths.
Project identity is resolved from the origin-session link the task tools
already record (TaskService.projectForSession), so the loop predicate and
claim stage agree regardless of the free-form project key the agent passed.
Claiming flows through a kernel-declared TaskClaimCoordinator implemented
in apps/server (no cross-core import).
New TasksReady condition driven by EvaluationContext.readyTaskCount,
fed at resolve time via a kernel-declared ReadyTaskCounter interface
implemented in apps/server over TaskService (keeps core:kernel
decoupled from core:tasks). Loops a graph while ready>0, exits at 0.
Root Child DOX Index assembled, plus a per-module AGENTS.md across the tree
(core/*, infrastructure/*, apps/*, testing/*, and docs/examples/frontend/etc),
each following the DOX section shape: Purpose, Ownership, Local Contracts,
Work Guidance, Verification, Child DOX Index.
GET /tasks now reports dependency-graph status per task — ready (TODO with no unmet
deps) and blockedBy (ids of unfinished blockers) — resolved via TaskGraph over each
task's FULL project board, so a status filter can't hide a blocker. Default-valued
fields are omitted (encodeDefaults=false); consumers read absent as zero.
The task_decompose approval card now shows a readable graph (epic + numbered children
with their "after" dependency labels) via renderDecomposePreview wired into
computeToolPreview, instead of the truncated raw JSON the generic fallback gave.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Block writing a file whose on-disk content changed since the session read it — a
concurrent/external edit the agent's view doesn't reflect. file_read records a
content hash on whole-file reads; the orchestrator's completion path now carries
the tool's structuredOutput (it previously dropped it, unlike SandboxedToolExecutor
— the two paths were inconsistent), so SessionContext.readHashes folds it.
StaleWriteRule compares the current hash (WorldProbe.contentHash) against the
read-time hash and BLOCKs a mismatch, telling the agent to re-read first. Files the
session wrote itself are excluded, so its own edits never look stale; partial reads
set no baseline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completing a task that declared affected_paths but saw no matching write this
session is now blocked ("marked done without doing it"), escapable with force.
TaskUpdateTool reads the session's writes through a SessionWrites port whose
adapter folds SessionContext (writes = recorded receipt.affectedEntities), and
matches them against the task's affected_paths globs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claiming a task now emits SessionWorkingTaskEvent{taskId, affectedPaths} into the
session's own stream (via a SessionFactRecorder port + event-store adapter), and
SessionContextProjection folds it into SessionContext.activeTask. So a gate learns
"which task is this session working, and its scope" by reading the session stream
— no cross-stream scan, no kernel->tasks dependency. Re-emitted on an affected_paths
edit by the claimant, so the snapshot scope stays current; latest wins on replay.
This is the data source the write-scope gate needs (next).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A file_read of a path that is inside the workspace but does not exist is now
BLOCKED with "no such path" instead of silently returning empty — so an agent
that hallucinated a filename fails loud and self-corrects (list the directory,
fix the name) rather than proceeding on an absent reference. Out-of-workspace
targets stay PathContainmentRule's concern, so the gates don't double-handle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An LLM that edits or overwrites a file it never read is acting on hallucinated
contents. This plane-2 ToolCallRule blocks it: dispatching on FILE_WRITE (covers
file_write and file_edit), a write to a path that exists on disk but was not
file_read earlier this session is BLOCKED. A brand-new file passes — you can't
read what doesn't exist, and creating is legitimate.
- ReadFilesProjection folds the session's file_read events (request + completion,
so a failed read doesn't count) into the set of paths read, mirroring
EgressAllowlistProjection; threaded into ToolCallAssessmentInput.sessionReadPaths
via SessionOrchestrator.resolveSessionReadPaths.
- Read paths and the write target are both resolved through the probe (toRealPath),
so spelling differences and symlinks can't dodge the gate.
- The orchestrator already feeds a block's rationale back as a tool result, so the
agent reads the file and retries; the rejected-event reason now carries that
rationale too (specific audit trail instead of a generic string).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The doctrine says "search before creating", but that leans on the LLM; this is
the backstop against an agent re-creating a task across runs. TaskService.
findDuplicates matches active (non-terminal) same-title tasks, case- and
whitespace-insensitively — a DONE/CANCELLED look-alike is not a duplicate, since
that work may legitimately recur.
- task_create rejects a duplicate, naming the look-alike and offering force=true.
- POST /tasks → 409 with the duplicates, or force:true to override;
correx task create --force.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The DEPENDS_ON/BLOCKS link types were inert metadata — nothing reasoned about
them. TaskGraph turns them into answerable questions: what a task waits on
(its DEPENDS_ON targets plus any task that BLOCKS it), whether it is ready
(TODO with no unmet blocker — a terminal dependency stops blocking), and what
finishing it would unblock. Readiness surfaces workable tasks to claim; it
never assigns (honouring the rejected /tasks/next scheduler).
Surfaced across the stack:
- TaskService.ready / blockers / blocking.
- Context bundle: a `blocked` flag + `blocked_by` list, rendered as a prominent
"BLOCKED — waiting on ..." line, so an agent calling task_context learns it
should wait before charging at the task.
- GET /tasks?ready=true, GET /tasks/{id}/blockers; correx task ready / blockers.
Cross-project dependencies are scoped to a project for now (documented in
TaskGraph). Tests cover both link directions, terminal-dependency clearing,
the ready filter, the reverse blocking direction, the bundle flag, and REST.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surfaces a task's lifecycle straight from the event log (the source of truth) so
you can see what actually happened to it — claim/submit/complete, notes, links,
git-driven status — rather than only its current state. Useful for debugging a
live agent run, where current state alone doesn't say how it got there.
- TaskHistory.render: pure timeline renderer (split content/lifecycle to stay under
the complexity cap, mirroring DefaultTaskReducer).
- TaskService.history(id): the task's own events, in order; survives soft-delete
(the deletion is part of the trail), empty for an id that never existed.
- GET /tasks/{id}/history (200 text timeline, 404 when the id never existed) and
the `correx task history <id>` CLI command.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
A disposable Markdown view of the board, grouped by status with checkboxes
and claimants — rendered from the event log, never a source of truth.
TaskMarkdown.render (pure) backs GET /tasks/export[?project=].
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
POST /tasks/sync-git advances task status from commit messages: a mention
moves a task to IN_PROGRESS, a closing keyword (fixes/closes/resolves
auth-12) walks it the legal path to DONE. Only ever advances (never
regresses or touches BLOCKED/CANCELLED), so re-running is idempotent; each
advance leaves a [git <sha>] note. Acts on commits in the request body or
reads the repo's recent log (GitCommandCommitReader) — wire it to a git
hook or CI step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ranked exact/substring search: TaskSearch (all terms AND, ranked title >
key > goal > criteria > notes) over one project or the whole board via
TaskService.search. Surfaced as GET /tasks?q=, a read-only task_search tool
for agents, and a `/` filter in the TUI board.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a cross-project task board to the TUI (T / palette): a roster fetched
from GET /tasks with vim-style nav, refresh, and an enter-to-open detail
pane (goal, criteria, paths, links, notes) built from the list payload — no
extra fetch. Server: TaskService.listAll() and a project-optional GET /tasks
(scoped with ?project=, whole board without; recent-first).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The context bundle resolved only TASK and DOC link targets; ARTIFACT and
SESSION stayed raw. Add two ports (TaskArtifactResolver/TaskSessionResolver)
with apps/server adapters: artifacts resolve to producing stage/session +
content excerpt from CAS, sessions to status/intent/workflow. Unresolved
targets still fall back to raw links. Wired through the tool and REST bundle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CRUD, lifecycle transitions, work-graph links and notes over /tasks;
share link-kind inference (TaskTargetKinds) between the REST route and
task_update tool. End-to-end route tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds task tracking as a first-class event-sourced aggregate (ADR-0012). The
event log stays the only source of truth; the board is a projection, and all of
a project's task events live in one stream (tasks:<projectId>).
core:
- new core:tasks module mirroring core:sessions: TaskStatus/State/Reducer/
BoardProjector/CounterProjection/Repository + TaskService write path that
appends events via the existing EventStore (no new storage)
- task events in core:events (sealed marker TaskEvent), registered in
Serialization.kt; status is derived by the reducer from lifecycle facts
- work-graph edges: TaskLinkType + typed TaskTargetKind on the link, so bundle
resolution dispatches on the recorded kind instead of guessing from the id
- delete is a soft tombstone (TaskDeletedEvent); cancel stays a visible outcome
surfaces:
- agent tools (Tier T2 mutators, T1 read): task_create / task_update /
task_delete / task_context, registered in both the main and per-workspace
tool registries
- REST GET /tasks/{id}/context for external agents
context bundle (TaskContextAssembler):
- task fields + acceptance criteria + relevant files + resolved dependency
tasks (live status) + raw related links + notes, with a compact render()
- semantic enrichment via a TaskKnowledgeRetriever port bridged to the existing
L3 retriever (late-bound holder for composition-root ordering)
- ADR/doc resolution via a TaskDocumentResolver port + filesystem adapter
(docs/decisions/adr-*.md, padding-agnostic; *.md paths)
Unit-verified across core:tasks / infrastructure:tools / apps:server; full
gradlew check green. Not yet live-QA'd end-to-end against a running server.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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.
First slice of the plan-generation pipeline epic. A pure-Kotlin PlanLinter runs over the
compiled freestyle ExecutionPlan (WorkflowGraph) before it is locked, complementing
ExecutionPlanCompiler (which throws on unreachable/unknown-kind/bad-edge) with the checks
it does NOT make:
- HARD unproduced_need — a stage needs an artifact no stage produces. Real gap: only the
TOML loader checked this; the freestyle compiler did not, so such plans compiled and
failed at runtime.
- HARD trap_state — a stage with no path to the terminal (inescapable loop / dead end).
Uses terminal-reachability, NOT "any cycle", so legitimate verdict-gated review loops pass.
- SOFT stage_count / fan_out / empty_brief / duplicate_brief — scored, never blocking.
FreestyleDriver lints after compile and records PlanLintCompletedEvent (pure function of
the recorded plan → replay-safe, no env observation in v1); a hard failure rejects the plan
(ExecutionPlanRejectedEvent source="lint") before the operator is asked or the plan locks,
so a broken plan never executes. Pays off now for single-plan freestyle; becomes the
per-candidate filter when multi-candidate generation (Slice 2) lands.
Deferred to later slices: file-path grounding + symbol resolution (needs an index),
token-budget/ADR-bypass checks, and a dedicated :core:planning module.
Run operator-configured static-analysis commands (compiler/detekt/formatters) as a
deterministic harness step on the producing stage, before the LLM reviewer. A stage
declares `static_analysis = [commands]`; after it produces its artifact, each command
runs in the workspace root and the results are recorded in a StaticAnalysisCompletedEvent
(invariant #9 env observation — recorded live, folded on replay, never re-run). A
non-clean command fails the stage retryably with its output fed back verbatim (§2:
compiler/test output is ground truth), so the implementer fixes it and only static-clean
code ever reaches the reviewer. This makes §5's "reviewer context excludes static findings"
mechanical — the findings are resolved upstream, so the reviewer can't waste inference on
what detekt catches for free; no output-parsing, no context plumbing.
- StaticAnalysisCompletedEvent + StaticAnalysisFinding (core:events) + registration.
- StaticAnalysisRunner seam + ProcessStaticAnalysisRunner (whitespace-split argv, stderr
merged, timeout→nonzero exit) in core:kernel; wired (nullable) into OrchestratorEngines
and the server. Null runner / no workspace root → logged no-op, never blocks the run.
- StageConfig.staticAnalysis + `static_analysis` TOML field; runStaticAnalysis folded into
a runPostStageGates sequence (produces → grounding → echo → static analysis).
- Documented `static_analysis` on the role_pipeline implementer stage (commented; commands
are workspace-specific). The reviewer prompt half shipped in 3467826.
Surface the health subsystem (llama/event-store/disk probes) in the TUI,
mirroring the session-stats pane: ClientMessage.GetHealthChecks ->
ServerMessage.HealthChecks (health.checks, reuses HealthReport) ->
StreamQueries.healthChecks via HealthInspectionService; Go OverlayHealth on
key H + palette 'health checks', pull-based fetch, per-subject status with
degraded loud. Empty/disabled -> visible fallback (no blank/lie). Go+Kotlin
golden tests pin the wire contract field-for-field. Observability spec §4/§3.
Third HealthProbe (after disk + llama): times a cheap lastGlobalSequence()
read as an event-store responsiveness heartbeat — DEGRADED on throw or when
latency exceeds eventStoreLatencyWarnMs (default 500). Read-only by design
(no probe writes into the source-of-truth log; Invariant #1). Plugs into the
existing HealthMonitor; no new events. Observability spec §4.
Also wraps a long line in LlamaServerHealthProbeTest to clear a detekt
MaxLineLength finding introduced in aaf896d.
Second HealthProbe (after disk-watermark): liveness via GET /health on the
configured llama base URL + tokens/sec degradation watch derived from recorded
InferenceCompletedEvents (windowed average vs configurable threshold, opt-in via
llamaTpsWarnBelow). Plugs into the existing HealthMonitor edge-detection loop;
no new events. Observability spec §4.
ListIdeas -> idea.list (StreamQueries folds the board from the log, replay-neutral)
and DiscardIdea -> append IdeaDiscardedEvent landed in the capturing session, then
reply with the refreshed board. IdeaDto on the wire.