Commit Graph

72 Commits

Author SHA1 Message Date
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 f90f2ab39d feat(validation): artifact-emission repair ladder + empty-schema fix
Deterministic repair pipeline (extraction, classification, policy, gated
LLM-repair rung) with ArtifactRepairAttempted/Failed events. Also fix
JsonSchemaValidator: a schema with no declared properties describes 'any
object' — unknown-property enforcement now only applies against a declared
shape (empty-properties schemas were rejecting every real artifact).
2026-07-05 18:04:36 +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 883e93cecb fix(events): AnyMapSerializer drops nulls instead of failing to read them
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.
2026-07-02 13:26:34 +04:00
kami 1a9e1019ec fix(sessions): terminalize session on workflow completion
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.
2026-07-02 12:23:39 +04:00
kami e0c222392c feat(context,kernel): compression pipeline stage 3 — replay-safe tier summarizer
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.
2026-07-01 14:03:47 +04:00
kami d26f20c316 docs(dox): build out the DOX AGENTS.md hierarchy
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.
2026-06-28 20:43:27 +04:00
kami 3e94d7fbff merge: integrate feat/backlog-burndown into master
master had advanced 16 commits past feat/backlog-burndown's base, and the two
branches independently built four of the same features. Resolved 26 conflicts.

Overlap features — kept master's implementation (more complete / production-wired /
more robust), dropped the feature branch's parallel constellation:
- llama-server health probe: kept master's event-store-backed tps probe; dropped the
  branch's LlamaLivenessClient (liveness-only, throughput unwired).
- event-store probe: kept master's EventStoreHealthProbe; dropped EventStoreLatencyProbe.
- brief echo-back gate: kept master's BriefEchoDiff (Jaccard, tolerates rewording);
  dropped the branch's exact-set-diff BriefEchoComparator/Extractor.
- static-first reviewer: kept master's command/exit-code gate (ProcessStaticAnalysisRunner,
  wired); dropped the branch's structured-finding static_check stage (no-op seam).
  Its structured-findings model is filed as a follow-up in BACKLOG.

Feature-branch net-new work brought in and kept (master had none):
- native task tracking (aggregate, agent tools wired into analyst/implementer/reviewer,
  dependency graph + gates, decompose, REST/CLI, TUI task board)
- critique-outcome producer (role-rel §6 — master had deferred it)
- stage-level plan checkpointing (C-A2, folded into runPostStageGates)
- CLAUDE.md/AGENTS.md L0 standing context
- cross-session grants + TUI (grant scopes/revoke, @ picker, session resume browser)

Verified: full Gradle compile (all modules + tests) green; tests pass for core:events,
core:kernel, infrastructure:workflow, apps:server, apps:cli, testing:integration; tui-go
go build + go test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 11:30:41 +00:00
kami 30321e08a5 feat(toolintent): record the session's active task as a session-local fact
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>
2026-06-24 20:35:40 +00:00
kami 9b003d02ea refactor(events): record tool capabilities on the invocation event
Capability is the canonical signal the plane-2 gates dispatch on, but
ToolInvocationRequestedEvent only carried the tool name — so ReadFilesProjection
classified reads by a hardcoded "file_read" string, bypassing the abstraction and
(worse) re-deriving a semantic fact at replay from whatever the name meant. That
violates the event-sourcing invariant that replay reads recorded facts, not live
state.

Now the tool's requiredCapabilities are recorded on the event at emission, and the
projection classifies by ToolCapability.FILE_READ. To let the lowest module carry
it, ToolCapability moves from core:tools into core:events keeping its package, so
every existing import resolves unchanged and core:tools imports it from below. The
field is defaulted, so pre-existing events deserialize as empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 19:46:44 +00:00
kami 0c6ac903b7 feat(tasks): native task tracking — aggregate, agent tools, context bundle
Adds task tracking as a first-class event-sourced aggregate (ADR-0012). The
event log stays the only source of truth; the board is a projection, and all of
a project's task events live in one stream (tasks:<projectId>).

core:
- new core:tasks module mirroring core:sessions: TaskStatus/State/Reducer/
  BoardProjector/CounterProjection/Repository + TaskService write path that
  appends events via the existing EventStore (no new storage)
- task events in core:events (sealed marker TaskEvent), registered in
  Serialization.kt; status is derived by the reducer from lifecycle facts
- work-graph edges: TaskLinkType + typed TaskTargetKind on the link, so bundle
  resolution dispatches on the recorded kind instead of guessing from the id
- delete is a soft tombstone (TaskDeletedEvent); cancel stays a visible outcome

surfaces:
- agent tools (Tier T2 mutators, T1 read): task_create / task_update /
  task_delete / task_context, registered in both the main and per-workspace
  tool registries
- REST GET /tasks/{id}/context for external agents

context bundle (TaskContextAssembler):
- task fields + acceptance criteria + relevant files + resolved dependency
  tasks (live status) + raw related links + notes, with a compact render()
- semantic enrichment via a TaskKnowledgeRetriever port bridged to the existing
  L3 retriever (late-bound holder for composition-root ordering)
- ADR/doc resolution via a TaskDocumentResolver port + filesystem adapter
  (docs/decisions/adr-*.md, padding-agnostic; *.md paths)

Unit-verified across core:tasks / infrastructure:tools / apps:server; full
gradlew check green. Not yet live-QA'd end-to-end against a running server.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 19:00:58 +00:00
kami e46777e29f kernel: deterministic static_check stage (§B-§5) + critique-outcome producer (§B-§6)
Two reviewer-reliability tracks, both deterministic and unit-verified (no model/network).

§B-§5 — static_check stage seam. The StaticAnalysisRunner + StaticFindingsRecordedEvent +
reviewer-context filter already existed; what was missing was a stage that runs the tool and
emits the event. Added:
- StaticCheckStageExecutor: reads stage metadata (static_tool/static_argv), runs the configured
  command via StaticAnalysisRunner, returns a StaticFindingsRecordedEvent. No-op (empty findings)
  when no runner is wired or no command is configured — safe to carry unconfigured.
- A deterministic-stage seam in DefaultSessionOrchestrator.enterStage: any stage with
  metadata["stage_type"] == "static_check" is run by the executor instead of the LLM subagent,
  then advances on its (unconditional) exit edge.
- TomlWorkflowLoader: stage_type/static_tool/static_argv fields → StageConfig.metadata.
- role_pipeline.toml: a static_check stage between implementer and reviewer (no-op until
  static_argv + a CommandRunner are set; activation is live-QA-gated, see StaticAnalysisRunner doc).

§B-§6 — critique-outcome producer. The CritiqueFinding type + CriticCalibrationProjection existed
but nothing fed them. Added:
- CritiqueFindingsRecordedEvent (+ CritiqueVerdict): the producing side — a critic's findings +
  verdict for one review iteration, carrying modelHash for per-model calibration.
- CritiqueOutcomeCorrelator: pure loop-resolution logic deciding UPHELD (fixed between rounds) /
  DISMISSED (persisted into an approved final) / INCONCLUSIVE (open at a non-approved terminal),
  per critic (role + modelHash) and per finding id.
- A hook in completeWorkflow/failWorkflow that correlates recorded findings into
  CritiqueOutcomeCorrelatedEvents at loop resolution — no-op when none recorded, idempotent.
  (LLM-side finding emission stays a separate model-gated activation.)

Tests: StaticCheckStageExecutorTest (4), StaticCheckStageTest integration (1, fake runner →
event + transition), CritiqueOutcomeCorrelatorTest (8), CritiqueCalibrationWiringTest integration
(1, seeded findings → outcomes at completion), updated RolePipelineWorkflowTest. Full suites for
core:events/kernel/critique, infrastructure:workflow, testing:integration green; detekt clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 21:14:00 +00:00
kami c36d41b9d5 feat(approvals): cross-session grant scopes (PROJECT/GLOBAL) + revoke
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>
2026-06-21 16:07:33 +00:00
kami c616982b7b feat(context): inject CLAUDE.md / AGENTS.md as L0 standing context
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>
2026-06-21 10:45:43 +00:00
kami f107ff554c feat(router): idea-board -> project-profile promotion (E)
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>
2026-06-21 07:30:43 +00:00
kami 447fc7a43e feat(kernel): reviewer static-first infra (B§5)
- 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>
2026-06-20 15:36:06 +00:00
kami 027ff1f6ff feat(toolintent): dynamic per-session egress allowlist (D)
- EgressHostsGrantedEvent (registered + serialization test): additive per-session
  egress grants (e.g. an approved research source list)
- EgressAllowlist pure union helper + EgressAllowlistProjection (folds grants per
  session); NetworkHostRule delegates to the union, preserving exact/suffix semantics
- rule not yet fed the session set live (ToolCallAssessmentInput has no sessionId);
  precise TODO(wiring) left. batch fetch-approval skipped (needs approval-flow surgery)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 13:14:48 +00:00
kami b098d87075 feat(research): dedicated SourceFetched + LowQualityExtraction events (D)
- promote research fetch quality + content_sha256 from ToolExecutionCompletedEvent
  metadata to first-class SourceFetchedEvent / LowQualityExtractionEvent (registered
  + serialization test); existing metadata write kept intact (additive)
- emitted from SandboxedToolExecutor on research-fetch tool completion, guarded on
  the url/content_sha256/quality markers; reuses the extractor's LOW_QUALITY marker

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 11:49:26 +00:00
kami eae0a0ccf6 feat(memory): architect contradiction-check (B§4, display-only)
- 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>
2026-06-20 11:31:39 +00:00
kami 1a1b5cc5ed feat(kernel): stage-level plan checkpointing (C-A2)
- StageCheckpointPassed/FailedEvent (registered + serialization test): per-stage
  reconciliation of produced artifacts vs the locked plan's produces-slots
- StageCheckpointReconciler (pure) + tests
- emitStageCheckpoint wired in after verifyProduces, runs regardless of its
  pass/fail (verifyProduces still owns the halt); gated on an ExecutionPlanLocked
  in the session log so non-plan workflows are unaffected

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 10:53:41 +00:00
kami 1df7af5b85 feat(kernel): brief echo-back gate (C-A1)
- BriefEcho model + BriefEchoMismatchEvent (registered + serialization test)
- BriefEchoExtractor: parses the planner's brief_echo block and the original
  brief's referenced-files/symbols/acceptance-criteria dimensions
- BriefEchoComparator: conservative diff — a dropped acceptance criterion or an
  invented file path halts; dropping a file/symbol alone does not
- checkBriefEcho gate chained into the post-stage flow after groundBriefReferences;
  opt-in via stageConfig.metadata["briefEcho"], fails the stage (retryable) on
  divergence with re-grounding feedback
- prompt/workflow activation (planner.md emitting brief_echo; role_pipeline
  metadata) left as live-QA follow-up

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 10:42:49 +00:00
kami f783eed4be feat(critique): structured CritiqueFinding + per-model calibration projection
- CritiqueFinding/CritiqueSeverity/CritiqueRole shared type (BACKLOG B6)
- CritiqueOutcomeCorrelatedEvent + serialization registration (BACKLOG C-A3)
- core/critique module: CriticCalibration state/reducer/projector, keyed by (modelHash, role)
- schema+projection only; live reviewer-loop producer wiring deferred

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 07:45:23 +00:00
kami 68136e77d7 feat(workflow): deterministic plan lint (plan-pipeline §5, Slice 1)
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.
2026-06-15 00:26:22 +04:00
kami 365eb8a279 feat(kernel): static-first reviewer gate (role-reliability §5)
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.
2026-06-14 23:51:49 +04:00
kami bcc59d2164 feat(kernel): brief echo-back gate (plan-pipeline-addenda A1)
New brief_echo stage before the planner in role_pipeline: the model restates
the analyst brief as a structured artifact, and a deterministic gate
(checkBriefEcho, opt-in via brief_echo=true) diffs the restatement against the
original brief. On divergence — dropped requirements (Jaccard coverage < 0.34)
or hallucinated files — it emits BriefEchoMismatchEvent and fails the stage
retryably, so a misread brief never reaches plan generation.

The diff (BriefEchoDiff) is a pure function of two recorded artifacts, so it is
replay-safe and records no observation; the event fires only on mismatch, for
audit. Symbols are recorded but non-blocking in v1. Mirrors the groundBrief-
References gate. role_pipeline only; freestyle_planning wiring is a follow-up.

Runtime install (like research): copy brief_echo.json + brief_echo.md and the
[[artifacts]] block into ~/.config/correx.
2026-06-14 19:43:15 +04:00
kami 3ac0a140ec feat(events): idea capture/discard events
IdeaCapturedEvent(ideaId, sessionId, text, timestampMs) + IdeaDiscardedEvent
(ideaId, sessionId) tombstone, registered in eventModule. The cross-session idea
board is rebuilt from these via eventStore.allEvents(); discard hides without
deleting (invariant #1).
2026-06-14 14:21:40 +04:00
kami 8d4bc0b2dd feat(events): workflow-proposal event model
ProposedWorkflow + WorkflowProposedEvent(sessionId, proposalId, prompt,
candidates, originalRequest), registered in the polymorphic eventModule.
Records the router's triage suggestion of candidate workflows as a
non-authoritative fact the operator acts on.
2026-06-14 13:28:41 +04:00
kami df0144582a feat(events): clarification questions model + events
Adds the vocabulary for a stage raising open questions to the operator:
ClarificationQuestion/ClarificationAnswer value types, the
ClarificationRequested/ClarificationAnswered events (registered in the
event module), a ClarificationRequestId, and ClarificationQuestions.parse
— a lenient, fence-tolerant reader that pulls an optional questions array
out of an LLM artifact (shared by orchestrator and server).
2026-06-14 03:31:30 +04:00
kami f8fd2601a8 feat(server,cli): event-sourced health checks — disk watermark probe (observability §4)
Health states are events. A HealthMonitor polls registered HealthProbes on an
interval and appends HealthDegraded/HealthRestored only on a status transition
(hysteresis), so health is observable, replayable, and renderable like every
other fact in the log. The probe reads nondeterministic environment state and
records it at observe-time (Invariant #9); HealthProjection rebuilds current
health by replay, never re-probing (Invariant #8). Monitor is live-only, started
in ServerModule.start alongside narration; seeded from the projection so a
restart does not re-emit a degraded the log already records.

First concrete probe: DiskWatermarkProbe — event-log + CAS size and growth rate,
warning before the 2am surprise. Read surface mirrors `correx stats`:
GET /health/checks replays the system session and `correx health` renders it.
Health events ride the reserved __system__ session, filtered from operator
session listings.

LLAMA_SERVER (liveness + tokens/sec trend) and EVENT_STORE (append/fsync latency)
probes plug into the same HealthProbe framework as follow-ups.
2026-06-13 22:19:33 +04:00
kami b050dc4e83 feat(kernel): analyst entity grounding (role-reliability §3)
Local analysts hallucinate file paths the same as planners. A stage can now
opt in with `ground_references = true`: after it produces its brief, every
workspace-relative file path the brief named is checked for existence, and a
path that doesn't exist fails the stage (retryable) with the misses fed back
("Reference only files you have read") so the model re-grounds.

Grounding probes the filesystem directly rather than the repo map — the map
is recency-ranked and depth/extension-limited, not a complete existence
oracle, so "absent from the map" can't be a hard failure. Existence is a
nondeterministic observation, so the result is recorded as a
BriefGroundingCheckedEvent (invariant #9); replay reads it back and never
re-probes.

- BriefGroundingCheckedEvent + GroundingReference, registered in eventModule
- BriefReferenceExtractor: pure, deterministic pull of relative file-path
  references from a brief artifact JSON (needs a '/' and a dotted extension;
  skips modules, bare names, URLs, absolute/parent paths — so coarse
  "subsystem" mentions the brief is allowed to make aren't false-flagged)
- SessionOrchestrator.groundBriefReferences on the post-verifyProduces path,
  gated by stage metadata; uses the existing worldProbe
- TomlWorkflowLoader `ground_references` → metadata; role_pipeline analyst
  opts in

Scoped to file references with a '/' + extension (not bare basenames, which
a depth-limited probe can't resolve), so a flagged miss is high-confidence.
The symbol-grounding half of §3 is left for a real symbol index.
2026-06-13 16:20:30 +04:00
kami 823c2e0ade feat(events): WorkspaceStateObservedEvent, RepoKnowledgeRetrievedEvent, stateKey on RepoMapComputedEvent 2026-06-11 13:11:12 +04:00
kami 629b910e7f feat(kernel): retried stages receive explicit failure feedback; default retry backoff 1s 2026-06-10 21:53:45 +04:00
kami 4389163c5c feat(events,sessions): ProjectProfileBoundEvent reduced into session state 2026-06-10 21:47:30 +04:00
kami 0553b1b6ee feat(events,server): ExecutionPlanRejectedEvent — freestyle compile failure visible in log
Both silent-failure branches in FreestyleDriver.lockAndRun now emit
ExecutionPlanRejectedEvent (source=compile or missing_content) before
returning, satisfying invariant #1. Event registered in eventModule for
correct polymorphic serialization.
2026-06-10 21:26:18 +04:00
kami cc6b402a23 fix(server,kernel,events): harden artifact read + restore workspace on reopen + cleanups
Follow-ups spotted while fixing the artifact-content bug:

- #1 robustness: listArtifacts resolves content via runCatching, degrading to null
  instead of failing the whole listing on one bad CAS read (resolveContent helper)
- #2 eviction visibility: WARN when a referenced artifact hash resolves to null
  (most likely evicted/compacted while still referenced by the event log)
- #3 ordering trap: document on ArtifactCreated/ArtifactContentStored that 'Created'
  fires at validation, AFTER ContentStored at inference — the latent fold hazard
- #4 reopen gap: SessionSnapshot now carries workspaceRoot (derived from
  SessionWorkspaceBound), so a reopened session restores its workspace label
- #5 orchestrator headroom: isBackEdge externalised to a top-level function so
  DefaultSessionOrchestrator drops below the TooManyFunctions threshold (was at it)

#6 (orphaned empty F-010-era artifacts) needs no code change — empty content is
already normalised to null ('(no content stored)'); emission is prevented going
forward by the shipped F-010/F-018/F-020 fixes.
2026-06-10 12:58:28 +04:00
kami bb70c94a99 feat(kernel,events): freestyle graph re-routing — deterministic override core
Implements the replay-safe half of preemptive redirect (graph re-routing). An
operator-confirmed PreemptRedirectEvent overrides the resolver's edge at the next
transition boundary; the pure resolver stays untouched.

- PreemptRedirectEvent / PreemptRedirectBlockedEvent (+ serialization registration)
- TransitionExecutedEvent + TransitionDecision.Move gain optional redirectId — the
  durable 'consumed once' marker
- PreemptRedirect.decide: pure decision over the event log (override / block / none),
  so replay reaches the identical outcome without re-classifying (invariant #8)
- override wired in DefaultSessionOrchestrator.step above resolveTransition; back-edge
  jumps count against the existing maxRetries cap via executeMove
- blocks jumps to unknown stages or targets with unsatisfied needs
- DomainEventMapper: redirect events mapped to null (operator surface ships with the
  LLM-proposal + approval-confirm front-half)
- tests: override+consume-once, block-on-needs, block-on-unknown, ignore-consumed

Front-half (LLM proposal -> approval-gate confirm -> emit PreemptRedirectEvent) is
deferred; needs live LLM verification. Until it ships no redirect event is emitted in
production, so the override is inert. Spec: docs/plans/2026-06-10-freestyle-graph-rerouting.md
2026-06-10 11:51:47 +04:00
kami d518400b5f fix(validation): resolve F-007 artifact validator CAS-hash conflation
ArtifactPayloadValidator passed the logical slot name into the
content-hash-keyed CAS store, crashing every workflow with a typed
produces slot. Validator now reads payloads from a content map threaded
through ValidationContext (populated from the orchestrator's per-session
artifact cache across all stages) and no longer depends on ArtifactStore.

Also records the slot->CAS-hash mapping as a new ArtifactContentStoredEvent
(registered for serialization), emitted at both CAS write sites, so
artifact content is recoverable from CAS by hash after a cold start.
2026-06-08 17:26:34 +04:00
kami 0c7fa70f8d feat(personalization): OperatorProfileBoundEvent + bind-at-start + inject about context
Records operator profile as a snapshot event at session start (invariants #8/#9),
populates SessionState.boundProfile via reducer, and injects the about text as a
pinned L0 SYSTEM context entry in every stage. Gated by PersonalizationConfig.enabled.
2026-06-08 10:38:47 +04:00
kami 459c7cd9f5 feat(events): add JournalCompactedEvent + serialization registration 2026-06-08 09:57:54 +04:00
kami 6c8c5e2ad9 feat(workflow): ExecutionPlanCompiler + ExecutionPlanLockedEvent (Slice 3)
- ExecutionPlanLockedEvent: registered, replay-deterministic record of the
  locked plan (CAS ref + compiled stage/start ids).
- ExecutionPlanCompiler: validated execution_plan JSON -> WorkflowGraph,
  reusing ConditionSpec.toCondition(); inline prompts via metadata[promptInline];
  malformed plans throw WorkflowValidationException.
- Replay test: locked event + plan JSON recompiles to the identical graph,
  no architect re-run, no FS reads.
2026-06-08 02:33:57 +04:00
kami fe561ada09 feat(server): intent input channel — StartSession.input seeds decision journal 2026-06-04 02:16:23 +04:00
kami 440c96659d feat(events): RepoMapComputedEvent as recorded environment observation 2026-06-04 00:59:27 +04:00
kami 9d1fe397f1 feat(events): RefinementIterationEvent + orchestration state counter 2026-06-04 00:50:16 +04:00
kami 432226c544 feat(events): add RouterNarrationEvent with polymorphic registration and round-trip test 2026-06-03 15:43:48 +04:00
kami 8d8b2914e0 feat(router): capture latency + tokens on ROUTER ChatTurnEvent
ChatTurnEvent gains nullable latencyMs and tokensUsed fields (defaults preserve
backward-compat; legacy JSON without them deserializes cleanly). emitChatTurn
accepts optional metrics; the ROUTER emit site passes inferenceResponse values
through; the USER emit site leaves both null.
2026-06-03 15:11:37 +04:00