Taste/Style Profile Learning
Kerux learns portable, confidence-scored coding-style preferences from your trajectory history and injects them into the system prompt — so the agent writes code the way you do, across every project.
Inspired by CommandCode’s taste registry: preferences like
export style: named exports (confidence 0.85) travel with the user, not
the repo. Design reference: research/product-research-t_2be9e216.md
(ide #9).
Data model
Everything lives in crates/kerux-core/src/taste.rs.
| Type | Role |
|---|---|
TasteProfile | Portable unit: versioned bag of preferences + metadata. The JSON document is the profile — push/pull needs no conversion step. |
TastePreference | One learned rule: stable key (export style), preferred value (named exports), evidence counters (positive/negative), denormalized confidence in 0.0..=1.0. |
PreferenceObservation | One extracted signal from trajectory history: “the user’s work exhibited (or contradicted) preference X”, with weight and timestamp. |
PreferenceExtractor | Trait: &[Trajectory] -> Vec<PreferenceObservation>. The extraction engine implements this. |
TasteStore / FileTasteStore | Portable storage contract + default file-backed implementation (one JSON document per profile name). |
Supporting enums: PreferenceCategory (naming, formatting, architecture,
tooling, language, documentation, testing, workflow, other) and
PreferenceSource (extracted, inferred, manual).
Confidence scoring
Every preference counts supporting (positive) and contradicting
(negative) observations. Confidence combines two factors
(compute_confidence):
- Consistency —
positive / (positive + negative). A preference that is contradicted half the time can never exceed0.5. - Saturation —
n / (n + HALF_SATURATION)withHALF_SATURATION = 5. One observation yields~0.17, five yield0.5, twenty yield0.8; confidence grows slowly and never quite reaches1.0.
confidence = (positive / total) * (total / (total + 5))
No evidence scores 0.0 — nothing is injected until something is
actually observed. The score is stored denormalized on the preference and
recomputed whenever evidence changes (recompute_confidence); the raw
counters are the source of truth.
Storage format
The portable JSON document is the profile itself (version field guards
forward compatibility, missing fields get defaults so old documents stay
readable):
{
"version": 1,
"name": "kerux",
"created_at": 1787700000,
"updated_at": 1787766000,
"preferences": [
{
"key": "export style",
"category": "language",
"value": "named exports",
"positive": 20,
"negative": 1,
"confidence": 0.8,
"source": "extracted",
"first_observed_at": 1787700000,
"last_observed_at": 1787766000
}
],
"metadata": { "source_project": "kerux" }
}
Two locations, same format:
- Store (portable registry):
FileTasteStorepersists one pretty-JSON document per profile name under<data_root>/taste/<name>.json(~/.kerux/taste/,KERUX_HOMEoverrides the root). Names are sanitized for the filesystem. - Project-local:
<project_root>/.kerux/taste.json(project_taste_path) — commit it to version control to share a house style with your team via PR.
Writes are atomic (temp file + rename, via the shared persist helpers);
missing or corrupt files read as “start fresh”.
Push/pull semantics
Push = load the project profile, save it into a TasteStore under a
name. Pull = load from the store, TasteProfile::merge into the project
profile. Merge rules, per matching key:
kerux taste push team
kerux taste pull team
Both commands use the current directory as the project root. Push requires
an existing .kerux/taste.json; pull creates it when absent and atomically
writes the merged profile.
- Same value: evidence counters add, the observation window widens (min first / max last), confidence recomputed.
- Conflicting values: the side with more total evidence wins (ties go to the more recently observed one).
- Manual source propagates: an explicitly stated preference
(
source: manual) wins over learned sources. - Keys present on only one side are copied over; metadata keys missing from the target are filled in without overwriting.
Prompt injection
TasteProfile::render_prompt_block(min_confidence, max_items) renders
the top preferences (highest confidence first, at most one entry per key,
capped) as a markdown block:
## Learned Coding Style Preferences
Learned from past sessions. Follow these unless the user instructs otherwise.
- export style: named exports (confidence 0.80)
- naming: snake_case files (confidence 0.75)
Returns None when nothing clears the threshold or max_items == 0 —
callers omit the block entirely rather than injecting an empty section.
retain_confident(min) prunes weak preferences from a profile outright.
At runtime, [taste] enabled = true injects this block from the current
project’s .kerux/taste.json. min_confidence defaults to 0.5 and
max_items defaults to 10; setting enabled = false disables injection.
Extraction engine
crates/kerux-core/src/taste_extraction.rs implements
PreferenceExtractor as TrajectoryPreferenceExtractor: a deterministic,
LLM-free miner over recorded Trajectory steps (the canonical action
record; messages mirror the same tool calls and are skipped to avoid
double-counting). It emits supports = true evidence only — counter-
evidence is reserved for explicit/manual signals.
Signals mined per trajectory step:
| Step | Signal | Key → value |
|---|---|---|
terminal | toolchain commands (cargo test/clippy/fmt/build, pytest, jest, eslint, prettier, go test, make, tsc, …) | test runner, linter, formatter, build tool |
terminal | git add <path> vs git add -A/git commit -am | commit style → staged per-file commits / bulk commits |
terminal after an edit step | tests run right after file_write/patch/edit_block | test discipline → runs tests after edits (once per edit cycle) |
file_write/patch/edit_block | file extension | primary language → Rust, TypeScript, … |
file_write/patch/edit_block | file-stem casing | file naming → snake_case / kebab-case / camelCase / PascalCase |
file_write/patch/edit_block | shallowest space indent or tab dominance of written content | indentation → 2 spaces / 3 spaces / 4 spaces / tabs |
file_write (non-append) vs patch/edit_block | edit granularity | edit style → full file rewrites / targeted patches |
Retry-loop guard: identical (key, value) observations are capped per
trajectory (max_repeats_per_trajectory, default 3), so one stuck session
cannot saturate a preference. Confidence scoring stays in taste.rs —
the extractor only emits evidence, folded via
TasteProfile::apply_observations.
Division of labor
taste.rs(this design): schema, scoring math, storage, merge, prompt rendering.taste_extraction.rs:TrajectoryPreferenceExtractorimplementsPreferenceExtractor; results fold viaTasteProfile::apply_observations.- System-prompt wiring and
kerux taste push/pullCLI: built onTasteStore+render_prompt_block+project_taste_path.