Commit Graph

239 Commits

Author SHA1 Message Date
kami a8c496e909 fix(toolintent): exempt list_dir from reference-exists + rejection-loop breaker
Greenfield discovery/analyst stages thrashed on plane-2 REFERENCE_EXISTS
rejections: the model listed a not-yet-created dir (e.g. frontend/), got
hard-BLOCKED every round, and burned MAX_TOOL_ROUNDS without progress.

- New ToolCapability.DIRECTORY_LIST; ListDirTool declares it alongside
  FILE_READ. ReferenceExistsRule now exempts directory enumeration (a
  listing of an absent dir truthfully reports empty; only hallucinated
  file *reads* still fail loud). Dispatch stays on capability, not name.
- Stage-agnostic rejection-loop breaker in the tool loop: after 3 rounds
  where every tool call is rejected (BLOCKED/ERROR) with no success,
  force the model to produce its output. Covers JSON-emitting stages the
  read-loop breaker (file_written-only) never protected.
2026-07-07 14:09:03 +04:00
kami efb6eb0334 feat(validation): capability-gap reflection rung (#30 part 2)
Bounded LLM "are you sure?" pass over the capability gaps part 1 detects, run
once at plan-lock before the operator is asked to approve — honest mistakes
self-correct without escalation; only a genuine tool need reaches the human.

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

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

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

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

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

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

Tests: RecoveryRoutingTest (deterministic gate, no static return edge — proves
dynamic return) + GateRetryBudgetExhaustionTest RECOVER case + two compiler
tests. All green; detekt clean.
2026-07-07 12:05:26 +04:00
kami 597a26d551 feat(validation): auto build-gate on terminal freestyle stage (Gate 4 floor)
Closes the COMPLETED-lie where a freestyle stage passes by producing a
schema-valid artifact regardless of whether its build actually succeeded:
a verify stage could run 'npm run build', watch it fail, then self-attest a
build_verified artifact and transition to done. Tool failures never failed
the stage.

The compiler now flags the terminal stage autoBuildGate=true when no stage
declares an explicit build_expectation. Code-ness is decided at runtime, not
compile time (the declared kind is always the generic file_written; only the
written paths reveal code): runExecutionGate folds the FileWritten manifest via
sessionProducedBuildTarget() and promotes to a PROJECT build when a build
manifest (package_json/gradle_module) or an imports_resolve-bearing path was
written, then runs the real command from .correx/project.toml [commands]. A
non-clean exit fails the stage retryably; a docs-only plan is left untouched.

Live-proven (run 3, session 28812b44): gate fired on the real
'npm --prefix frontend run build', exit 2 on a bad tsconfig, RetryAttempted
instead of a false COMPLETED.

Modules green: transitions+workflow 67, kernel 60.
2026-07-07 01:01:12 +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 28e9e698a1 feat(inference): capture reasoning_content on responses (WIP, provider side)
Adds a nullable `reasoning` field to InferenceResponse and maps the
llama.cpp / OpenAI-compat `reasoning_content` field into it, so local models
that emit their chain-of-thought have it captured. Provider/model side only —
the emit-site event field (reasoningArtifactId) + CAS storage in the
orchestrator are still pending (tracked in Vikunja Correx #4).
2026-07-06 01:26:01 +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 33ea44d8cc feat(context): required/optional bucket split, budget ceilings, grounded handoffs
Context rework from the 2026-07-05 context-poisoning diagnosis (session
6ca236c4 read-loop):

- ContextBucket on ContextEntry: REQUIRED (task, constraints, needed
  artifacts, failure feedback) is never pruned/compressed/dropped; the
  build fails fast (RequiredContextOverflowException -> non-retryable
  stage failure) if required alone exceeds the stage budget.
- OptionalCeilings: per-sourceType token caps for the optional block
  (repoMap 1200, relevantFiles 1600, journal 800, profiles 400/600,
  vocabulary 400), tuned for the 9-26B tier at 16k; enforced by
  truncation (journal keeps its tail, others their head).
- Repo map is now a structural 'what exists' listing (alphabetical,
  per-directory, no recency ranking, no symbols); .md/docs paths are
  gated behind an explicit docs mention in the stage prompt — retrieval
  hits remain the only other path for docs to enter context.
- file_written needs render as a manifest of ALL files the producing
  stage wrote (projected from ArtifactContentStored -> ToolInvocation
  -> FileWritten events), fixing the last-write-wins collapse; content
  stays lazy behind file_read.
- Decision journal folds a stage's RETRY records into one summary line
  once a later transition leaves that stage (render-time only).
- Dedupe retrieved-file lines in buildRelevantFilesEntry.
2026-07-05 18:05:02 +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 24f3b843bb fix(talkie): label narration actions 'in this run', not 'this stage'
The actions line is session-wide, but the old 'Actions taken this stage' label
made a literal narrator (Qwen2.5-1.5B) contradict itself ('no files read this
stage'). Relabel to match reality.
2026-07-03 14:33:02 +04:00
kami bc5fedf99b fix(talkie): surface narration actions in the user turn, session-wide
Live run showed the actions were in the prompt but the 1.2B narrator ignored
them: they sat in a system-role entry (small models under-weight system context)
and were stage-scoped to the pausing stage, which usually isn't the one that ran
the tools. Fold the recent actions into the USER trigger instruction instead, and
take the last N tool calls across the whole run rather than per-stage. Narration
now names the concrete actions ("reviewing server directories and reading the
specified file") instead of generic filler.
2026-07-03 14:03:24 +04:00
kami 1c027e8189 feat(talkie): ground narration in the stage's actual tool actions
Narration was generic ("Stage X completed. Summarise.") because nothing about
what the stage actually did reached the narrator — the L2 summary is a
placeholder and StageCompletedEvent carries no output. narrate() now reads the
stage's ToolInvocationRequested events from the log and feeds the narrator a
concrete 'Actions taken this stage: file_read(path), ...' entry, and the system
prompt asks it to prefer those concrete actions over restating the stage name.
2026-07-03 13:40:00 +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 77c28ba313 refactor(config): move embedder/l3 to top-level [embedder]/[l3] sections
They are shared infra (the orchestrator uses them for workflow repo-knowledge,
not just the router chat layer), so [router.embedder]/[router.l3] was misleading.
Loader/writer use the new names; legacy [router.*] still read as a fallback.
Also notes the STEERING LLM-reformulation cost as a ponytail follow-up.
2026-07-03 13:16:20 +04:00
kami a0b3a1865a fix(kernel): deterministic repo-map floor + 0.0-hit filter + closest-path suggestion
RepoMapComputed recorded per session (in-memory scan cache keeps the walk-once
guarantee); zero-score embedder hits filtered before reaching the agent, falling
back to the deterministic repo map; REFERENCE_EXISTS surfaces a closest-path hint
via closestPathByFilename. Breaks the hallucinated-package-path read loop even
with a noop embedder.
2026-07-03 13:15:54 +04:00
kami 2698971082 fix(kernel): de-dup tool events, restore approval diff, break read-before-write deadlock
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.
2026-07-03 01:01:19 +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 beb1d5c3b9 test: dedupe DefaultSessionReducerTest into one file
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.
2026-07-02 12:25:29 +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 18cbd34739 fix(kernel,tools,workflow): session-robustness QA sweep
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.
2026-07-02 00:56:45 +04:00
kami 968cbfa973 fix(context,tools): context hygiene + tool ergonomics from freestyle QA
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.
2026-07-02 00:44:04 +04:00
kami cb4e41a59f feat(context): static-doc compression — prune CLAUDE.md/AGENTS.md doc entries
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.
2026-07-01 14:57:19 +04:00
kami e3743835ec feat(server): wire EmbeddingRelevanceScorer into context builder, default level 4
Move embedder construction ahead of the engines so the context pack builder ranks
freeform turns by query relevance (query-conditioned selection). Default
compression_level 4 (format + static cache + token pruning + age-tiering) for a 12b
on a 16k window.
2026-07-01 14:33:00 +04:00
kami 047e2a4070 feat(context,infra): compression pipeline stages 4-5 — token pruning + relevance + ToMe
- build() is now suspend: pipeline runs all stages in fixed order, each gated by level
- TOKEN_PRUNE (level 3): TokenPruner interface + LLMLingua-2 sidecar (sidecars/llmlingua)
  + HttpTokenPruner adapter (fails open if sidecar down); prunes freeform, preserves
  protected spans, skips tier-0 turns when TIER_SPLIT on
- TOME_MERGE (level 8): ToMeMerger collapses near-duplicate freeform turns (Jaccard)
- Stage 5 selection: RelevanceScorer + EmbeddingRelevanceScorer (cosine over Embedder);
  query-conditioned reorder so least-relevant freeform drops first under budget
- [orchestration] compression_level + token_pruner_url config, wired in Main
- suspend ripple fixed across builder callers/stubs
2026-07-01 14:29:56 +04:00
kami 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 b50688df1d feat(context): compression pipeline stage 2 — fact sheet + strict allocation
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.
2026-07-01 13:56:09 +04:00
kami 61be90070f feat(context): compression pipeline stage 1 — level policy + deterministic stages
Additive CompressionPolicy (levels 1-9), ProtectedSpanTagger (regex
load-bearing spans), FormatCompressor (json→dotted lines), ContextClassifier
(static/structured/freeform). Stage 1 (FORMAT_COMPRESS) wired into
DefaultContextPackBuilder behind the policy; structured entries only, lossless.
PromptRenderer repetition-anchoring of steering directives.
2026-07-01 13:53:19 +04:00
kami 4be8f292ae fix(kernel,server,workflow): per-stage retry budget, sane freestyle budget, tail /events
Three defects surfaced by a live headless freestyle_planning run (all made a
long execution workflow fail or stall in ways invisible to a REST operator):

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

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

3. GET /events returned the OLDEST `limit` events (readFrom(0) then take): for a
   session past `limit` events the tail — including a pending ApprovalRequested —
   was invisible to a headless/REST poller, the exact caller POST /approve serves.
   Default now tails; explicit fromSeq still paginates forward.
2026-07-01 13:41:06 +04:00
kami c8c2521fa2 fix(kernel,inference): reliable artifact emission for local models
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).
2026-07-01 03:08:24 +04:00
kami e72051a9ea feat(kernel): emit_artifact tool + actionable validation feedback
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.
2026-06-30 20:53:40 +04:00
kami fe6e530a75 Merge remote-tracking branch 'origin/feat/session-robustness-and-dox' into feat/session-robustness-and-dox 2026-06-29 20:58:07 +04:00
kami 82674e85c4 feat(inference): capability-aware routing for tool-heavy stages
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>
2026-06-29 11:06:20 +00:00
kami 238d353653 feat(qa): remote NIM provider + headless-QA robustness
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>
2026-06-29 10:50:16 +00:00
kami a00bd4aade feat(tools): propose_scope recalibration valve for the execution loop
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.
2026-06-29 01:04:48 +04:00
kami 3e44c6d107 feat(kernel): deterministic per-task claim stage + per-task write scope
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).
2026-06-29 00:56:55 +04:00
kami c1e4c7b25e feat(transitions): tasks_ready predicate for execution loop
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.
2026-06-29 00:45:33 +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 1cb7fec677 feat(kernel): read-only mode after read-before-write + mandatory task-decompose gate
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).
2026-06-28 20:43:18 +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 f3b3145f36 feat(tasks): surface ready/blockedBy on GET /tasks and a readable decompose preview
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>
2026-06-25 11:54:24 +00:00
kami 5d61cca34c fix(kernel): flatten tool-call array args to List so affected_paths isn't dropped
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>
2026-06-25 10:39:03 +00:00
kami 6a779861c2 feat(toolintent): stale-write gate
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>
2026-06-25 06:15:15 +00:00
kami 4159361690 feat(toolintent): write-scope adherence gate (declarable)
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>
2026-06-24 21:31:08 +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 47a109e49f refactor(toolintent): one SessionContext read-model for the gates
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>
2026-06-24 20:16:15 +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 146f96a6fd feat(toolintent): reference-must-exist gate (anti-hallucination)
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>
2026-06-24 18:27:08 +00:00
kami 693e38ffac feat(toolintent): read-before-write gate (anti-hallucination)
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>
2026-06-24 18:13:06 +00:00