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.
Three tool-path correctness fixes:
- Tool-execution events were emitted twice — SandboxedToolExecutor and
SessionOrchestrator.recordToolExecution both emitted Completed/Failed/
FileWritten per call (double TUI rows, double-counted metrics). Split
ownership: orchestrator is the sole recorder of Completed/Failed (truncates
output, drives the read-before-write gate); executor owns Started +
FileWritten (pre/post-image hashes it alone can capture).
- computeToolPreview still gated on operation == "write", but file_write lost
its operation param when delete was split out, so it bailed to raw-args for
every write (approval card showed raw JSON). Drop the dead guard.
- A file_read blocked by REFERENCE_EXISTS can never complete, so it could never
lift read-only mode: a stage writing a NEW file deadlocked (writes filtered,
every read of the not-yet-created target blocked) until MAX_TOOL_ROUNDS.
Lift read-only on a REFERENCE_EXISTS block, and reword the block message to
tell the model to file_write directly. Regression test added.
toJsonElement wrote a null map value as JSON null, but toAny threw on reading it back — asymmetric, so a single null-valued tool-arg made the whole session's events undecodable: 500 on /events AND a runtime WorkflowFailed when the projection rebuilt mid-run. A Map<String, Any> can't hold a null anyway, so drop null-valued keys on both serialize and deserialize, keeping the log round-trippable.
Two copies had drifted — the core:sessions one and the testing:projections one — and one had rotted into asserting the ACTIVE-forever bug as correct. Port the unique boundProfile + freestyle-planning cases into the comprehensive projections copy and delete the core:sessions duplicate.
DefaultSessionReducer skipped WorkflowCompletedEvent to avoid terminalizing on freestyle planning's mid-session completion, but that left execution/normal completions stuck ACTIVE forever — headless watchers and approval loops never saw a terminal state. Add workflowId to WorkflowCompletedEvent (defaulted for replay compat), pass graph.id at all completeWorkflow sites, and terminalize to COMPLETED unless it's the freestyle_planning handoff. Correct stale tests that pinned the ACTIVE-forever behavior.
Uncommitted work from the session-robustness-and-dox branch sweep
(docs/qa/QA-session-robustness-and-dox.md), verified alongside the
compression/context fixes:
- SandboxedToolExecutor: validate tool args centrally before dispatch. A
malformed/missing-arg call becomes a recoverable ERROR: (surfaced with the
tool's arg schema so the model can correct + retry) instead of stranding
the stage with no artifact. + validation test.
- PlanLinter: seed artifacts (analysis) produced by the planning phase count
as available producers, so a plan stage that `needs` them isn't flagged as
an unproduced-need; H1 unproduced-needs + trap-state checks. + tests.
- DefaultSessionOrchestrator: live-QA robustness fixes (event-tail /
per-stage budget + retry handling).
- workflow prompts/configs: DOX AGENTS.md alignment + freestyle/task/role
prompt tweaks.
- SessionOrchestratorIntegrationTest: coverage for the above.
- FreestylePlanningWorkflowTest: allow list_dir in analyst tools (follows the
list_dir wiring in 968cbfa).
- QA plan doc for the branch sweep.
Found while live-QAing freestyle_planning on a 12B local model:
- list_dir tool: recursive, .gitignore-aware listing so weak models stop
flooding context with `ls -R` over node_modules/build/dist. Wired into
the fileRead toggle + advertised to the planner (architect_freestyle).
- ContextClassifier: assistantToolCall turns are STRUCTURED, so the token
pruner never shreds the model's own tool-call history — that was causing
amnesia loops (re-issuing calls it had already made).
- Retire instruction-doc LLMLingua pruning (DOC_SOURCE_TYPES emptied): it
fused load-bearing procedural text into unparseable soup. The static
block stays small by dropping CLAUDE.md at the loader instead.
- AgentInstructionsLoader: load only AGENTS.md, not CLAUDE.md — the latter
targets the outer assistant and polluted the agent's stage context.
- DefaultSessionReducer: WorkflowFailed flips session status to FAILED (was
stuck ACTIVE forever, so clients/approval loops never saw a terminal).
- ShellTool: run shell command lines (cd/&&/pipes) via `sh -c`; unrunnable
program is recoverable instead of an uncaught IOException killing the
stage; malformed argv (non-string/collapsed-array) rejected with guidance.
- llmlingua sidecar: cap force_tokens to max_force_token (big docs blew the
assert and 500'd, so doc pruning silently failed open).
Tests added/updated across all of the above.
The big win the pipeline missed: projectProfile + agentInstructions doc dumps
(~14k chars) are L0-SYSTEM -> STATIC -> never pruned, re-sent verbatim every turn.
Now pruned via the same TokenPruner at a gentler ratio (keep 60% vs 45% freeform),
protected spans (code fences/paths/keys) kept so directives survive. Gated by
TOKEN_PRUNE (level 3+); base systemPrompt stays verbatim.
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.
ContextSummarizedEvent (registered) keyed on stable session sequence, not the
ephemeral pack ordinal, so replay reuses the recorded summary artifact and never
re-calls the LLM (invariants #6/#8). TierContextSummarizer mirrors
JournalCompactionService for freeform context (tool outputs/docs/retrieved text)
the journal path doesn't cover. ContextState + DefaultContextReducer fold the
event; reducer replay-safety unit-tested.
FactSheet derives load-bearing spans (newest-first, capped) and pins them as an
L0 SYSTEM entry never dropped by the compressor (level 7). Event-derived each
assembly, not a persisted file, per invariant #1. Strict budget allocation
(level 9) compresses structured groups before freeform so conversation can't
starve tool logs / artifacts.
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.
Two fixes that together let the freestyle analyst/architect stages actually
produce valid artifacts on a local model (verified end-to-end: analyst passed
first try, architect produced a validated plan):
1. Tools-less final emission (SessionOrchestrator): producing the artifact
while tools are in the request is unreliable — llama.cpp's Gemma template
switches to a channel format and leaks <|channel> markers into the text, and
models keep tool-calling until the round cap without ever emitting. After the
tool loop, fire ONE tools-less inference to get clean JSON (also re-enables
the JSON grammar, since grammar+tools is rejected). Gated on the artifact not
already being produced via emit_artifact.
2. GBNF grammar handles arrays/objects (GbnfGrammarConverter): array-typed
properties fell through to a 'string' rule, so the grammar forced a string
where the schema demanded an array — an unwinnable grammar-vs-validator
disagreement. Add generic value/object/array rules and map types correctly.
Note: also needs schema 'items' on array props (runtime schemas updated; mirror
to repo schemas/*.json).
Two robustness fixes for LLM-emitted artifact stages (e.g. freestyle analyst/
architect) that were exhausting their retry budget on weak models:
1. emit_artifact meta-tool: stages with an LLM-emitted slot now expose an
emit_artifact tool whose parameters ARE the artifact's JSON schema, giving
the model a structured tool-call channel to produce the artifact instead of
a free-form final message. Sidesteps llama.cpp's grammar+tools
incompatibility (the artifact can't be grammar-constrained while tools are
present). The call's arguments are captured as the artifact content.
2. Actionable retry feedback: validation rejection now surfaces the concrete
error messages (e.g. 'required property summary missing') as the stage
failure reason instead of a bare 'validation failed', so the retry-feedback
entry tells the model what to fix.
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.
Two orchestrator tool-gating behaviors (both touch SessionOrchestrator):
- Read-only switch: when a tool call is BLOCKed with READ_BEFORE_WRITE, derive a
read-only mode from the event log (block until a later FILE_READ completes) and
withhold FILE_WRITE tools from the next inference turn(s), so a weak model is
pushed down the read-then-write path instead of looping on the blocked write.
- require_task_decompose stage flag (TomlWorkflowLoader) + owesTaskDecompose guard:
a stage so marked cannot stage_complete until a task_decompose/task_create has
succeeded this stage. New task_planning workflow + prompt are the first consumer
(decompose-only: swoop codebase, emit a parent + DEPENDS_ON task graph, stop).
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>
The orchestrator stringified every non-primitive tool argument, so a JSON array
arrived as its toString() and listParam (as? List<*>) read it empty — silently
dropping a task's affected_paths/acceptance_criteria. That left WriteScopeRule
(activeTask.scope empty -> no-op) and cite-before-claim with nothing to enforce
for agent-created tasks.
Extract parseToolArguments: a primitive array becomes List<String>; objects and
arrays-of-objects keep their JSON text for tools that re-parse them
(task_decompose). Unit-tested in ParseToolArgumentsTest.
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>
When a session has claimed a task with declared affected_paths, an in-workspace
write outside that scope is BLOCKED — but declarably: the message tells the agent
to widen the task's affected_paths via task_update (which re-records the scope)
and retry. Legitimate, unforeseen changes (a dependency, a caller) are never
trapped, only forced to become a recorded, auditable scope change. Reads the
session's activeTask scope (recorded at claim) — no cross-stream lookup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claiming a task now emits SessionWorkingTaskEvent{taskId, affectedPaths} into the
session's own stream (via a SessionFactRecorder port + event-store adapter), and
SessionContextProjection folds it into SessionContext.activeTask. So a gate learns
"which task is this session working, and its scope" by reading the session stream
— no cross-stream scan, no kernel->tasks dependency. Re-emitted on an affected_paths
edit by the claimant, so the snapshot scope stays current; latest wins on replay.
This is the data source the write-scope gate needs (next).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the single-purpose sessionReadPaths plumbing with a SessionContext
{reads, writes, activeTask} folded by SessionContextProjection over the session
stream — the read-model every anti-hallucination gate consumes (plane-2 rules via
ToolCallAssessmentInput.session; tool gates via a port, next).
- reads: completions whose recorded capability includes FILE_READ.
- writes: the resolved paths the orchestrator already records on each completion's
receipt.affectedEntities (FileAffectingTool) — no param-parsing reinvention.
- activeTask: null for now; populated once claim records a session fact.
ReadBeforeWriteRule now reads input.session.reads; ReadFilesProjection is retired.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Capability is the canonical signal the plane-2 gates dispatch on, but
ToolInvocationRequestedEvent only carried the tool name — so ReadFilesProjection
classified reads by a hardcoded "file_read" string, bypassing the abstraction and
(worse) re-deriving a semantic fact at replay from whatever the name meant. That
violates the event-sourcing invariant that replay reads recorded facts, not live
state.
Now the tool's requiredCapabilities are recorded on the event at emission, and the
projection classifies by ToolCapability.FILE_READ. To let the lowest module carry
it, ToolCapability moves from core:tools into core:events keeping its package, so
every existing import resolves unchanged and core:tools imports it from below. The
field is defaulted, so pre-existing events deserialize as empty.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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 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>
Two reviewer-reliability tracks, both deterministic and unit-verified (no model/network).
§B-§5 — static_check stage seam. The StaticAnalysisRunner + StaticFindingsRecordedEvent +
reviewer-context filter already existed; what was missing was a stage that runs the tool and
emits the event. Added:
- StaticCheckStageExecutor: reads stage metadata (static_tool/static_argv), runs the configured
command via StaticAnalysisRunner, returns a StaticFindingsRecordedEvent. No-op (empty findings)
when no runner is wired or no command is configured — safe to carry unconfigured.
- A deterministic-stage seam in DefaultSessionOrchestrator.enterStage: any stage with
metadata["stage_type"] == "static_check" is run by the executor instead of the LLM subagent,
then advances on its (unconditional) exit edge.
- TomlWorkflowLoader: stage_type/static_tool/static_argv fields → StageConfig.metadata.
- role_pipeline.toml: a static_check stage between implementer and reviewer (no-op until
static_argv + a CommandRunner are set; activation is live-QA-gated, see StaticAnalysisRunner doc).
§B-§6 — critique-outcome producer. The CritiqueFinding type + CriticCalibrationProjection existed
but nothing fed them. Added:
- CritiqueFindingsRecordedEvent (+ CritiqueVerdict): the producing side — a critic's findings +
verdict for one review iteration, carrying modelHash for per-model calibration.
- CritiqueOutcomeCorrelator: pure loop-resolution logic deciding UPHELD (fixed between rounds) /
DISMISSED (persisted into an approved final) / INCONCLUSIVE (open at a non-approved terminal),
per critic (role + modelHash) and per finding id.
- A hook in completeWorkflow/failWorkflow that correlates recorded findings into
CritiqueOutcomeCorrelatedEvents at loop resolution — no-op when none recorded, idempotent.
(LLM-side finding emission stays a separate model-gated activation.)
Tests: StaticCheckStageExecutorTest (4), StaticCheckStageTest integration (1, fake runner →
event + transition), CritiqueOutcomeCorrelatorTest (8), CritiqueCalibrationWiringTest integration
(1, seeded findings → outcomes at completion), updated RolePipelineWorkflowTest. Full suites for
core:events/kernel/critique, infrastructure:workflow, testing:integration green; detekt clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
- 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>