
context-kernel
Task-induced context normalization for coding agents — a native Claude Code plugin. The task induces a projection; operators preserve the answer, not just sh…
Install with your AI
Paste into Claude Code, Cursor, or any agent — it reads the repo and wires the tool into your project.
Install and set up context-kernel (claude-plugin project) into my current project. Found on https://claudeers.com/context-kernel Repo: https://github.com/Pinperepette/context-kernel Homepage/docs: — Detected install method: claude-plugin → /plugin install context-kernel@Pinperepette/context-kernel Category: rag. Platforms: cli, api, web. Read the repo's README for exact setup and env vars, then install it and wire it into my project. Claudeers Health Verdict: active; community-verified: false. Confirm the source before running anything.
/plugin marketplace add Pinperepette/context-kernel /plugin install context-kernel@Pinperepette/context-kernel
git clone https://github.com/Pinperepette/context-kernel
// compatibility
| Platforms | cli, api, web |
|---|---|
| Operating systems | — |
| AI compatibility | claude |
| License | MIT |
| Pricing | open-source |
| Language | Python |
context-kernel
Via selectionis: of everything the repository affords, only the task-indexed path is lit. The rest is not deleted — it remains as roots, omnia possibilia, one page fault away. Solum quod refert, manet.
Context engineering for Claude Code, Codex and AI coding agents: task-induced context optimization with measured answer preservation.
context-kernel is an open-source context engineering framework for coding agents: repositories and tool outputs get projected onto task-equivalent working sets, cutting token usage — measurably — without changing what the agent can answer.
Modern coding agents waste most of their context window on information that is irrelevant to the current task. context-kernel — a native Claude Code plugin, with a Pi port, a Codex fallback and MCP tools any AI coding assistant can call — reduces that context while preserving the agent's ability to solve the task: the repository is projected onto a representative of its equivalence class with respect to the task. Deterministic, stdlib-only, zero API keys — and every claim below is measured, not estimated, and reproducible from this repository.
- −79% tokens end-to-end on a complete real Claude Code session — every tool output the session produced, not a single-command microbenchmark (details)
- 100% sufficiency on two 60-case fault benchmarks (pandas, 1.4k files; Django, 3k files): token optimization is worthless if the answer dies — here the answer's survival is measured (what we measure)
- 406 tests (402 pure-stdlib Python + 4 Pi bridge), CI on Linux, Windows and macOS
- Zero dependencies, zero API calls — verification runs in-session
"There is no such thing as the context" is not a slogan: there is no task-independent context. A context only exists with respect to a task, and the task is what induces the projection (the model). context-kernel is one implementation; the underlying idea — task-induced context projection — is independent of Claude Code and applies to local or hosted coding agents alike.
This is not compression. gzip makes text smaller and unreadable; normalization maps a context to a canonical, smaller member of the same answer-equivalence class. The output is still code, still readable, still sufficient — by construction where possible, by measurement everywhere else.
Quick example
One real measured case (Django, from the sufficiency benchmark):
Repository: Django — 2,972 source files
│
▼ task-induced projection (the symptom: one traceback)
Task state: 44 files (−98.5% of the repository)
│
▼
Agent solves the same bug — sufficiency verified: the raise site
is in the working set for 60/60 real fault cases
The whole project is this arrow, made deterministic, measured, and safe to rely on (how).
Get started in 60 seconds
Claude Code, native plugin — no API key, no config, nothing to sign up for:
/plugin marketplace add pinperepette/context-kernel
/plugin install context-kernel
The hooks now normalize tool outputs automatically. You'll see a
[context-kernel: …] footer on long outputs and the cumulative savings in the
status line; confirm it's live with /hooks and /plugin.
First real result — project a repo onto one bug. In any repo, paste a
traceback or error into your prompt: the T2 hook detects it and injects the
repository slice for that symptom — the files that can matter (seeds from the
stack frames, their dependency closure, the callers, related tests), with
everything it excluded declared as a recoverable page fault, not a deletion.
Prefer to call it explicitly? The plugin ships the kernel_repo_slice MCP
tool and the /kernel-repo-slice skill; every AI assistant that speaks
MCP can use it.
Full install options — Pi, Codex, manual install.sh, Windows notes — are in
§7 Installation.
Figures regenerate from real data with python3 docs/charts.py — this one
reads your own ~/.context-kernel-savings.log.
Why context engineering matters
Context engineering is becoming one of the main bottlenecks for coding agents: the context window is the scarce resource, and filling it with task-irrelevant tokens degrades cost and answer quality at the same time. context-kernel addresses this problem by projecting repositories and tool outputs onto the smallest task-equivalent representation — context optimization that is verified, not hoped for (how, why it's different).
Features
The core, in five bullets:
- Task-induced context projection — the repository becomes a small task state (the model, the operators)
- Repository slicing from real symptoms — stack traces, error literals, git diffs — via deterministic dependency-graph codebase analysis
- Context normalization of tool outputs (dedup, re-read deltas, grep-aware and JSON-aware MCP projection) — ambient token optimization on every tool call
- Answer-preserving verification — application canary, sampled A/B answer-invariance, an objective sufficiency oracle (the canary)
- Zero dependencies — stdlib-only, no API keys, no index to maintain
Beyond the core, same discipline:
- Symbol-level slicing for monolithic files (−96% below the file-level floor on pandas)
- Automatic token budgeting derived from the live context window
- Task charter extraction with an active guard on edits and shell writes
- Page-fault recovery — every exclusion is a prior, not a prohibition
- Live telemetry — savings ledger, statusline, revealed-relevance mining
- Every language covered — precise import graphs for Python/JS/PHP/Go, a declared generic floor for ~27 more extensions (the gate rule)
What we measure: rate and distortion
Every projection trades rate (how many tokens are removed) against distortion (whether the answer changes). Most context optimization tools report only the first number. context-kernel measures both: rate from the savings ledger and the manifests; distortion through an objective sufficiency oracle on real fault sites, an application canary, and sampled A/B answer-invariance judgments (measured results).
Measured results at a glance
Measured. Not estimated. Reproducible — every row from this repository (full details, charts and commands):
| Corpus | Files | Reduction | Sufficiency |
|---|---|---|---|
| pandas (file-level slice) | 1,415 | −75% … −91% | 100% (60/60 fault sites) |
| pandas (symbol-level, $T_{2b}$) | 1,415 | −96% | method-exact slices |
| Django (file-level slice) | 2,972 | −91% … −98.5% | 100% (60/60 fault sites) |
| lodash (real JS stack) | 1,048 | −97% | all 4 frames kept |
| ripgrep (generic floor, Rust) | 104 | −31% | declared weaker class |
| one full Claude Code session | — | −79% end-to-end | task completed |
| real ephemeral tool outputs, park dividend on | 7,707 outputs | −44.8% (vs −31.9% baseline) | 0 signal lines lost in either arm |
| rich — full pipeline on a real upstream bug, blind protocol | 190 | −99% (2-file slice under auto budget) | fix written from the slice alone: charter 8/8, 0 page faults, suite green |
| gjson (Go) — full pipeline on a real upstream panic (#192), blind protocol | 1 file, 3,650 lines | −58% (symbol slice of a monolith) | Go stack frames → symbol slice squash+3 fns; fix written from the slice alone, byte-identical to upstream f0ee9eb; suite green, upstream TestIssue192 passes |
| celery — dynamic-reference resolver on a real DI framework | 425 | — (completeness, not reduction) | static graph seeded at bin/shell.py misses concurrency.eventlet/gevent (dynamic-only imports); CK_DYNREF recovers both as seeds with their call sites (+ transitive deps); non-literal (registry.py:67) and external (django.db) args declared as blind spots, never guessed |
| Django hard, multi-hop bench (n=5) | 2,972 | −12% tokens, −29% calls, −38% time | correctness tie — 3/5 in both arms: a cost win, not a correctness one, and not monotone (one case where the manifest misled) |
| A/B answer-invariance, sampled on live traffic | 153 elisions | — | 4 invariant / 3 degraded; all 3 degradations are Bash outputs whose repetitive shape hid the signal (a symbol-name list, a diff hunk header, a numeric step sequence) — never a file read; each named shape is now recognized by the Bash signal predicate, with regression tests (§10) |
The −79% is an end-to-end session measurement, not a microbenchmark: it sums every tool output a complete real session produced — repository reads, repeated reads, greps, web fetches, MCP results, slices — before vs after normalization. Conversation messages are never touched by the kernel, so they sit outside both sides of the ratio: the operators act on what the tools inject into the context window, and that is where the redundancy lives.
The last two rows are the honest counterweight, and they say when it is worth
it. The clean wins are on structured code and file reads. On a hard,
multi-hop task the edge is real but it is a cost win — fewer calls, tokens
and time at equal correctness — not a correctness win, and it is not
monotone: the slice can occasionally mislead. The live A/B says the same from
the compression side: where normalization degrades, it degrades on Bash
outputs whose form looks like noise but is the answer — exactly what # ck:raw
and the auto-degrade canary exist to catch. Rule of thumb: lean on it for
code and repo slicing; keep an eye on it for shape-heavy shell output.
What makes it different
| Existing systems | context-kernel |
|---|---|
| retrieve relevant documents | choose a small equivalent context |
| compression | projection |
| heuristic ranking | task-induced projector $\pi_Q$ |
| hope the answer survives | measure answer preservation |
| Approach | Task-aware | Deterministic | Answer preservation | Failure mode |
|---|---|---|---|---|
| Naive truncation | no | yes | none | silent signal loss |
| Summarization | weakly | no | unmeasured | paraphrase drift |
| Embeddings / RAG retrieval | query-aware | no | unmeasured | similarity ≠ relevance for code |
| context-kernel | yes ($\pi_Q$) | yes | measured (oracle + canary + A/B) | declared: visible markers + page fault |
What this is not
- not a RAG system — nothing is retrieved by similarity into the prompt
- not a summarizer — no paraphrase ever replaces source code
- not a prompt optimizer — prompts are untouched; the context is projected
- not an embedding retriever — for code, reachability beats similarity (measured: §4)
It is a task-induced projection system with measured answer preservation.
Use cases for AI coding agents
- Claude Code — native plugin: hooks, skills, agents, MCP server
- Codex and other harnesses — fallback glue, plus a bridge contract to port any AI agent harness in ~100 lines
- Large monorepositories — automatic token budgeting + symbol-level slicing
- AI-assisted debugging — a traceback in the prompt injects the working set by itself
- Code review / pull request review —
--from-diffturns a PR into a working set with blast radius and related tests - Bug fixing against an extracted, actively-guarded task charter
- Repository exploration under a token budget, with page-fault recovery
How task-induced context projection works
flowchart TD
R[Repository + tool outputs] --> P["Projection π_Q — T1 syntactic, T2 semantic"]
P --> TS["Task State TS(Q)"]
TS --> A[Agent works on the task state]
A --> V["Verification — T4: charter check, answer invariance"]
V -. "page fault: one read" .-> R
1. Mathematical foundation of task-induced context projection
(The framing owes a debt to the operator-theoretic language of spectral theory — projections, kernels, decompositions. It is an analogy used with care, not a claim of isomorphism.)
1.1 The answer map induces an equivalence
Let $X$ be the space of possible contexts and $Y$ the space of answers. An agent solving a task $Q$ is a map
$$A_Q : X \longrightarrow Y$$
$A_Q$ induces an equivalence relation on contexts:
$$x \sim_Q x' \iff A_Q(x) = A_Q(x')$$
This is the load-bearing observation: there is no such thing as "the" context. There are infinitely many representations equivalent under the task. The agent does not need the document — it needs any representative of the document's equivalence class $[x]_Q$. Normalization means choosing a small one.
1.2 Task-induced projectors
A task-induced projector is a map $\pi_Q : X \to X$ that is
- idempotent: $\pi_Q(\pi_Q(x)) = \pi_Q(x)$ — normalizing twice changes nothing;
- answer-preserving: $A_Q(\pi_Q(x)) = A_Q(x)$, i.e. $\pi_Q(x) \in [x]_Q$.
The subscript is not decoration. Change the question and the kernel changes: what is invisible to "fix this KeyError" may be load-bearing for "audit the license headers". A projector without a task index is either trivial or wrong.
Everything $\pi_Q$ removes lies in the kernel of the task,
$$\ker Q \ = \ {\ \delta \in X \ : \ A_Q(x + \delta) = A_Q(x)\ \ \forall x \ }$$
— the part of the context that cannot move the answer.
1.3 Two kernels: syntactic and semantic
The kernel splits into two parts of very different nature:
$$\ker_{\mathrm{syn}} \ = \ \bigcap_{Q} \ker Q \qquad\subseteq\qquad \ker Q$$
- Syntactic kernel — invisible to every task: ANSI escapes, progress-bar spam, consecutive duplicate lines, a file re-read that is byte-identical to the copy already in context. Because $\ker_{\mathrm{syn}}$ does not depend on $Q$, it can be projected away ambiently, by a hook that never needs to know what you are working on. That is exactly what $T_1$ is.
- Semantic kernel — invisible to this task: the 1,404 pandas files that a
mergeKeyError cannot see. This is the hard part; it requires knowing $Q$. That is what $T_2 \ldots T_4$ are for.
This dichotomy is why the architecture has the shape it has: task-independent normalization runs everywhere and always; task-induced normalization runs when there is a symptom to induce it.
1.4 The Task State
Define the task state:
$$\mathrm{TS}(Q) \ := \ \pi_Q(C)$$
the canonical small representative of $[C]_Q$. The agent never works on the repository; it works on the task state. Concretely, $\pi_Q$ is factored through four operators:
$$\mathrm{TS}(Q) \ = \ \big(T_4 \circ T_3 \circ T_2 \circ T_1\big)(C)$$
In the continuous (embedding-based) picture, $Q$ spans a subspace $\mathrm{span}(Q)$ generated by relevance probes, and each context unit $u$ is scored by how much of its energy lives inside it:
$$\mathrm{score}(u) \ = \ \frac{\lVert \pi_Q, e(u) \rVert}{\lVert e(u) \rVert} \ \in \ [0,1]$$
The discrete, exactly-computable picture replaces embeddings with reachability on the dependency graph — which is what the shipped operators use, because for code, structure beats similarity (we measured it; see §6).
1.5 Composition of projectors
When is $\pi_2 \circ \pi_1$ still answer-preserving? If both were answer-preserving globally, composition would be trivial: $A_Q(\pi_2(\pi_1 x)) = A_Q(\pi_1 x) = A_Q(x)$. The real content is that each practical operator preserves answers only under premises — $T_2$'s soundness assumes the import graph is computed from the true sources. So the composition law is:
$\pi_2 \circ \pi_1$ is answer-preserving iff the premises of $\pi_2$ hold on $\mathrm{Im}(\pi_1)$.
Order matters through the premises, not through the algebra. Concretely: run $T_1$ (output normalization) before $T_2$ (graph slicing) and nothing breaks, because they act on different factors of the context space,
$$C \ = \ C_{\mathrm{repo}} \times C_{\mathrm{dialogue}}, \qquad T_1 = \mathrm{id} \otimes \tau_1, \quad T_2 = \tau_2 \otimes \mathrm{id}$$
and projectors on different tensor factors commute. Run a $T_1$-style truncation on the sources before $T_2$, and $T_2$'s premise dies — which is why this plugin never does that.
Two structural facts worth stating plainly:
- Answer-preserving projectors (with compatible premises) form a monoid under composition — closed, associative, with identity.
- They do not form a group. There is no $\pi_Q^{-1}$: the inverse of a projection is not an operator — it is an access path. That is the page fault (§1.6).
1.6 The honest weakening: page faults
Perfect answer-preservation cannot be guaranteed by static analysis alone (dynamic imports, dependency injection, config indirection). So the guarantee this plugin actually makes is deliberately weaker and checkable:
$$A_Q(\pi_Q(C)) = A_Q(C) \quad\textbf{or}\quad \text{the miss is detectable and repairable}$$
Every exclusion is a prior, not a prohibition. The manifest declares what was projected away; if the agent needs an excluded piece, recovering it costs one read — a page fault, in the OS sense. The interesting quantity is then not "was the projection perfect?" but "what did the faults cost?" — and that is logged, per run.
1.7 Rate–distortion
Every normalization trades rate (fraction of tokens removed) against distortion (probability the answer moved out of its class). Both are measured:
- rate — from the manifests and the savings log;
- distortion — via an objective oracle: take real fault sites in a repo
(actual
raisestatements), synthesize the partial symptom a user would report (the caller's frame plus the error message, not the raise site itself), and check whether the projection keeps the raise site in the task state. If the file that throws is projected away, the answer changes with certainty.
Distortion is also predicted per run, deterministically — not only measured after the fact by the offline oracle or in production by the fault ledger. The sufficiency oracle compares the returned projection $P$ against the answer-preserving closure $R$ of the seeds (their full backward dependency set on the static graph): $P \supseteq R \Rightarrow$ sufficient; otherwise $R \setminus P$ is exactly the set of expected page faults, declared by name in the manifest. This is the "sufficient context" idea (Yu et al., Sufficient Context, ICLR 2025) but computed rather than judged by a model — and it is the abstention signal made exact: when the slice is insufficient the manifest tells the agent where to page-fault instead of leaving it to guess. It is a measurement (T4), never a change to $\pi$.
Where the sufficiency oracle predicts distortion statically, delta
debugging (hooks/ddmin.py, Zeller & Hildebrandt) proves the minimum by
running: given a reproduction and a pass/fail oracle, it isolates the
1-minimal input that still reproduces — the empirical rate–distortion
optimum on the query side (the tightest $Q$ at zero distortion). A minimized
repro feeds a tighter, more precise slice back into $T_2$.
The curve lives in ~/.context-kernel-pipeline.jsonl, one JSON row per run.
2. The four operators
| Operator | Kernel it targets | What it does | Guarantee | |
|---|---|---|---|---|
| $T_1$ | normalize (impl. compress.py) | syntactic | Signal-preserving normalization of tool outputs (dedup, ANSI/progress strip, head+signal+tail elision). Plus re-read deltas (unchanged re-read → 3-line marker; changed file → unified diff against the copy in context), command deltas (the same Bash command with identical output → marker), output parking for ephemeral results (an elided Bash/MCP/WebFetch output can't be page-faulted by re-reading a file — the command already ran; so the full original is parked on disk at elision time and the footer declares the targeted recovery: recall.py KEY --grep/--lines returns only the lines you ask for, deterministically — the projection's inverse finally exists for ephemeral context too, and recall.py --search REGEX makes that inverse navigable across the whole session (a MemGPT-style recall storage: grep every parked output at once, then page in the one that matched — the access path made searchable, still no ranking, no model); and the inverse pays a dividend: because recovery is guaranteed, the elision rate on exactly the parked tools is more aggressive by construction (CK_EPHEMERAL_SCALE 0.5, auto-off when parking is off) — measured on 7,707 real ephemeral outputs: −44.8% vs −31.9% baseline, ~460k extra tokens, zero signal lines lost in either arm, bench/ephemeral_dividend.py), grep-aware projection (matches grouped per file, first K kept, the rest becomes counts — no file is ever dropped), outline-first giant reads (a Python file above ~20k tokens arrives as signatures with exact line ranges; bodies are fetched per symbol via offset/limit), prose projection for WebFetch (nav/link runs collapse), a JSON projection for MCP tool outputs (long homogeneous object arrays → first K samples + key schema + count; the schema is the syntactic kernel of the structure — paying it N times is redundancy; repeated identical MCP calls get the same delta/page-fault mechanics as Bash commands), an adaptive rate that works from the first token (baseline scale 0.75 — a young session is not an excuse for lazy compression) and tightens further to 0.5 as the context window fills (60%→90%, from the live usage tracker), and learned per-category rates closing the $T_5 \to T_1$ loop: revealed.py --apply-rates (an explicit human command, never silent tuning) writes per-extension rates from recurrent measured page faults — a category that repeatedly cost re-reads gets lighter compression (relax) or untouched pass-through (raw). Relax-only by construction: the absence of faults never tightens anything, because a fault is visible only when the model actually re-read. | Signal lines (errors/warnings) always survive; every elision leaves a visible marker; a canary verifies each replacement was actually applied (§4) |
| $T_2$ | repo slice | semantic | Projects the repository onto the working set induced by the symptom: seeds from stack frames / quoted literals, dependency closure, bounded importers, related tests. Import graphs for Python, JS/TS, PHP and Go (PHP: use/namespace/group-use/require edges via a declaration-derived FQCN map, fatal-error and file.php(N) frames as seeds and ambient strong symptoms; Go: package-level edges from the go.mod module path — importing a package pulls its directory, _test.go files arrive only as related tests, goroutine-dump frames seed, and without a go.mod internal imports are declared unresolvable rather than guessed). Every other source language (~27 extensions: Rust, Java, Ruby, C/C++, C#, Swift, Kotlin, …) gets the generic mention graph — filename-literal + unique-stem references, edges labeled [grafo generico], weaker class declared in the manifest (§2.2: the gate rule) — promoted to precise edges when the repo ships a ctags tags index (CK_CTAGS, default on): a file citing a uniquely defined symbol gets a real symbol→definer edge, recovering dependencies the filename heuristic can't see; the SCIP idea (consume an existing index) with zero dependencies (SCIP is protobuf, ctags is text); ambiguous symbol → skipped, additive only (edges added, never removed). Token budget (auto-derived from the live context window) selects the richest closure that fits; on monolithic repos it descends to symbol level ($T_{2b}$). A slice can also be seeded from a git diff (--from-diff REF, e.g. main... for a PR): the changed source files become seeds and the same graph returns the review working set — dependencies, importers (the blast radius), related tests. Learned priors from $T_5$ (revealed.py --write-priors, recurrence ≥2 only) feed back in: recurrently-read-outside files become extra seeds with a declared why, never-opened slice files get a [freddo T5] flag — additive and declarative only, never an exclusion. A git co-change prior (CK_CHURN, default on) adds the cold-start signal T5 lacks on a fresh repo: files that historically change in the same commits as the seeds (evolutionary coupling, recurrence ≥2) become extra seeds with a declared why — additive, never an exclusion, never a slice on its own. A coverage prior (CK_COV, default on) consumes a test-run artifact if the repo ships one (.coverage SQLite, coverage.xml Cobertura, lcov.info — all parsed with the stdlib, zero deps): files that actually executed but lie outside the seeds' static closure are the dynamic reachability the graph can't see (DI, reflection, config indirection) and become extra seeds — additive, never an exclusion; when the artifact is too broad (whole-suite, not the failing scenario) the count is declared as a page-fault hint rather than seeded blindly. A supervised dynamic-reference resolver (CK_DYNREF, default on) scans the seed files for importlib.import_module/__import__: a literal argument resolvable to a repo file becomes a seed with its call site (recovering a module the static graph can't see), a non-literal or unresolvable one becomes a declared blind spot — additive too, never guessed. An optional --anchor-ends reorder (off by default) fights the lost-in-the-middle attention curve: the seed stays at the head and the importers/callers (where the cause may live) rise to the tail, so both attention-hot extremes carry causal signal while the related tests — repro, not cause — sink into the middle; ordering only, never selection — the identical set of files, permuted, safe for $\pi$. | Sound on the static import graph, with literal dynamic imports recovered and the rest declared; blind spots are declared exclusions + page faults; a deterministic sufficiency verdict flags whether the projection equals the answer-preserving dependency closure or, under budget, names the dropped units as expected page faults; results cached by repo fingerprint and operator hash |
| $T_3$ | task charter | semantic | Extracts the constraints the fix must respect — contracts, invariants, behaviors pinned by tests — each with a mandatory file:line citation, ≤ ~10 items. Saved via charter.py it becomes active — and citations don't rot: at save time each citation captures an anchor (the cited line's content), and charter.py refresh re-resolves drifted file:line numbers deterministically — a unique anchor match updates state and text, zero or ambiguous matches are declared unresolvable, never guessed (the FQCN rule again): a PreToolUse guard injects the relevant constraints right before any Edit/Write of a cited file (the charter goes from post-hoc checklist to live invariant), and the charter survives auto-compaction (§2.1). Guard contract verified live: the harness honors additionalContext on PreToolUse — constraints reach the model before the edit, TTL dedup confirmed. The guard also watches Bash, closing the shell loophole: a command matching a known write pattern (sed -i, perl -i, tee, redirects, mv/cp/rm, truncate, dd of=, git checkout/restore) that names a cited file gets the same constraints injected before it runs — conservative by design (closed pattern list + cited file required; a false negative beats noise on every ls). | Every claim is citable; a stale citation is detectable; the guard indexes only cited constraints — the skill's rule made mechanical |
| $T_4$ | verifier | — (checks, does not project) | Adversarial check of the fix against the charter, constraint by constraint; or answer-invariance judgment $A_Q(x) \overset{?}{=} A_Q(\pi_Q(x))$. Plus empirical minimality (hooks/ddmin.py, delta debugging à la Zeller): given a reproduction and a pass/fail oracle you supply, it isolates the 1-minimal input that still reproduces — the operational twin of the (static) sufficiency oracle: sufficiency predicts distortion, ddmin proves the minimal query by running. A minimized repro is a tighter $Q$ → a more precise slice. Opt-in (needs a runnable oracle), deterministic, stdlib. | Reads ground truth via sed/awk, never through its own (normalized) Read tool |
flowchart LR
C[Repository + tool outputs] --> T1["T1 · syntactic normalize\n(hooks, ambient, task-free)"]
T1 --> T2["T2 · repo slice\n(task-induced, graph + budget)"]
T2 --> T2b["T2b · symbol slice\n(only if budget\nunsatisfiable)"]
T2 --> T3["T3 · task charter\n(constraints, file:line)"]
T2b --> T3
T3 --> FIX["fix / answer"]
FIX --> T4["T4 · adversarial verify"]
T4 -->|"page fault: one read"| C
2.1 Defending the Task State
Three events can silently invalidate $TS(Q)$, and all three are handled:
- Auto-compaction is a projection not indexed by the task — exactly the
"projector without a task index" the formalism warns about. It cannot be
prevented, but $TS(Q)$ can be defended: a PreCompact hook snapshots the
active charter ($T_3$) and the head of the current working-set manifest
($T_2$); the SessionStart hook re-injects them when the session resumes from
a compaction. The post-compact session restarts from the task state, not
from a generic summary. Verified live on a real
/compact: the post-compact brief carried the full active charter. A companion scheduler decides when the cheaper manual/compactis worth pre-empting the automatic one: instead of a fixed occupancy ratio, the advisory threshold is modulated by the measured cost of dropping context.lifetime.pyreads the survival curve straight from the fault ledger (~/.context-kernel-faults.log) — an empirical histogram over the log the plugin already writes, not a clock-TTL table (a stack trace dies when the bug is fixed, not after 30 wall-clock seconds): when recent elisions keep re-entering (context still alive) it advises later, when they don't (context dead) it advises sooner. The shift is bounded to a narrow band around the operator's base (a pathological estimate moves the threshold by at most ±0.12, never to an absurd value), it is timing only — the $T_1$ compression rates are untouched (the $T_5$ invariant: learned signals only relax) — and it is advisory anyway, with the PreCompact snapshot defending $TS(Q)$ regardless.CK_COMPACT_ADAPT=0falls back to the fixed base. - Session restarts are the between-sessions discontinuity (compaction is the within-session one), and by symmetry they get the same defense: a SessionEnd hook snapshots the charter head and working-set head keyed by repo (the next session has a new session id, but the same repo), and the next SessionStart on that repo re-injects them while fresh (default 24h). A charter cleared in the meantime disappears from the restore too — the snapshot never resurrects state the user explicitly dropped.
- Task switches. The whole theory assumes one $Q$ at a time, but real sessions drift: a second symptom arrives and the projection computed for $Q_1$ has no guarantees about $Q_2$. The ambient $T_2$ hooks track the active working set per session (seed set identifies the task); when a new symptom with different seeds arrives, the injected manifest carries an explicit task-switch declaration with the manifest diff (files $Q_2$ needs that the previous working set excluded). This closes the one case of honest weakening that had no marker: the silent change of $Q$.
2.2 Language coverage and the gate rule
Breadth follows the same philosophy as everything else: a declared guarantee class beats an undeclared big number. Two tiers:
| Tier | Languages | Graph | Guarantee |
|---|---|---|---|
Precise (a LANG_PACKS entry) | Python | import graph via module map | fixture + sufficiency bench (pandas 60 + django 60 fault sites, 100%) |
| JS/TS | import/require graph | fixture + real-stack bench (lodash) | |
| PHP | use/namespace/group-use/require via FQCN map | fixture | |
| Go | package-level graph from go.mod | fixture | |
Generic floor (everything else: .rs .java .rb .c .cpp .cs .swift .kt .scala .sh .lua .ex .hs .dart .jl and more) | ~27 extensions | mention graph: filename-literal references (#include "render.h") + whole-word stem references with a uniqueness guard (two config.rs in the repo → no edge; never guess) | declared weaker: every such edge is labeled [grafo generico] in the manifest, and the header names the extensions it covered — upgraded to precise symbol→definer edges when a ctags tags index is present (unique symbols only, additive) |
The gate rule: a language is listed as precise only when it has a
LANG_PACKS entry, a fixture in tests/test_repo_slice.py, and its
guarantee is measured (sufficiency bench where a corpus exists). Until then
it stays on the generic floor — supported, honest about its class, never
silently absent. Adding a language is deliberately cheap: one LANG_PACKS
entry (extensions + an edge-extractor factory), one fixture, and the bench —
the PHP pack came from an external user's report in a day, and that is the
intended path. Precision tiers beyond this (e.g. tree-sitter as an optional
dependency) are bought only when the bench proves the floor insufficient on
a language people actually use — never speculatively.
3. Complexity and guarantee classes
Determinism is not uniform across the pipeline, and it should not be hidden. Guarantee classes: formal (holds by construction), supervised heuristic (heuristic, but every application is checked by an instrument), probabilistic, auditable (LLM-produced, but every claim carries a citation that can be verified deterministically), empirical (LLM judgment).
| Operator | Time | Space | Token effect (measured) | Guarantee class |
|---|---|---|---|---|
| $T_1$ dedup / ANSI / elision | $O(n)$ in output length | $O(n)$ | −45…−93% per output | supervised heuristic (canary) |
| $T_1$ re-read delta | $O(n)$ + SHA-1 | state ≤ ~2 MB | −98% on unchanged re-reads | formal (hash equality) |
| $T_2$ import graph + slice | $O(\text{files} + \text{imports})$; pandas 1,415 files ≈ 12 s | $O(V+E)$ | −75…−97% of repo | formal on the static graph; premises declared |
| $T_2$ cache hit | $O(\text{files})$ stat-only ≈ 0.26 s | 20 entries | — | formal (fingerprint + operator hash) |
| $T_{2b}$ def-use symbol slice | $O(\text{AST})$ | $O(\text{AST})$ | −96% below file-level floor | formal w.r.t. "behavior of symbol S" |
| budget resolution | $O(\text{files})$ stat | $O(1)$ | picks the point on the curve | formal (arithmetic on measured state) |
| $T_3$ charter | 1 LLM pass | — | ~10 constraints replace the diff context | probabilistic, auditable (file:line) |
| $T_4$ verify | 1–2 LLM passes | — | — | empirical, adversarial |
4. Measured results
Measured. Not estimated. Reproducible. All numbers below are from real
runs (July 2026, Claude Code 2.1.x), with the commands shown. Two kinds of
measurement, kept
distinct: microbenchmarks (one operator on a controlled input — the
pip3 list and re-read rows below, the sufficiency bench suites) and
end-to-end (a complete real coding session, summed across everything the
kernel normalized — the −79% row). A microbenchmark shows an operator works;
the end-to-end number shows the whole kernel pays for itself in real work.
4.1 Syntactic normalization ($T_1$, live)
| Case | Kind | Before | After | Rate |
|---|---|---|---|---|
pip3 list, 1053 lines | micro | 14,900 tok | 995 tok | −93% |
| One complete live session (Bash+Read+WebFetch) | end-to-end | 66,832 tok | 14,212 tok | −79% |
| Unchanged file re-read (delta) | micro | ~2,000 tok | ~40 tok | −98% |
Measured, not estimated: the end-to-end row is the session's savings ledger
(~/.context-kernel-savings.log), summed over every tool output the kernel
normalized in that session — the same ledger the cumulative figure at the top
of this README is drawn from. Conversation messages are outside both sides of
the ratio (the kernel never touches them).
4.2 Sufficiency benchmark ($T_2$, objective distortion)
bench/sufficiency_bench.py on pandas (1,415 source files, 60 real raise
sites, partial symptoms — caller frame + message only):
xychart-beta
title "pandas, 60 fault sites: token rate removed (bars) vs sufficiency (line)"
x-axis ["full closure", "deps#8804;2", "deps#8804;1 + importers#8804;1"]
y-axis "percent" 0 --> 100
bar [75, 86, 91]
line [100, 100, 100]
Sufficiency stays at 100% at every depth. The measured lesson: distortion is dominated by seed quality, not closure depth — cutting depth is nearly free. (The benchmark also earned its keep: its first run scored 85% and the misses exposed a real seeding bug — ambiguous in-root absolute paths — which is now fixed and regression-tested.)
On lodash (JavaScript, 1,048 files, a real node stack trace): task state
30/1048 files (−97%) with all four trace frames retained at every depth.
Replicated on a second, unseen repository — Django (2,972 source files, 60 real raise sites, same partial-symptom protocol, 2026-07-17):
xychart-beta
title "django, 60 fault sites: token rate removed (bars) vs sufficiency (line)"
x-axis ["importers#8804;2 only", "deps#8804;2", "deps#8804;1 + importers#8804;1"]
y-axis "percent" 0 --> 100
bar [91, 97, 99]
line [100, 100, 100]
Sufficiency 100% at every depth here too, with a mean task state of 44 files out of 2,972 (−98.5%) at the shallowest config. Two repositories, two ecosystems, one conclusion: the seed mechanism carries the guarantee; depth only buys rate.
Does ambient injection help the agent? (preliminary, N=5 —
bench/exploration_ab.py, Django, degraded symptoms, headless claude
with Haiku, kernel hooks disabled in both arms): localization accuracy is
unchanged (5/5 both arms), while exploration cost drops on average —
−17% tool calls, −12% tokens processed — with high variance: the
injected working set halves the work exactly on the cases where
exploration would have been long (worst case: 10→6 calls, 304k→148k
tokens), and can slightly lengthen the path on cases that were already
trivial. The honest summary: the slice buys exploration economy where
exploration is expensive, not correctness at this difficulty.
The hard tier (--difficulty hard: the symptom keeps only the error
class and the caller module — no message words, so grepping the literal is
impossible and exploration must walk the graph; N=5, same protocol):
correctness drops to 3/5 in both arms — where the symptom is too poor,
the manifest does not rescue correctness either (the two misses are misses
for everyone). What changes is the economy, in both directions: on the cases
both arms solve, the slice cuts −48% and −64% of processed tokens
(166k→87k, 231k→84k); on one miss the control burned 353k tokens over 9
calls while the slice arm stopped at 92k; and on one case the manifest
actively misled the agent into a longer path (5→13 calls, 143k→529k).
Means: calls −29%, tokens −12%, time −38%. Third point on the curve, honest
reading: degrading the symptom lowers the correctness floor for everyone
and widens the economy spread — the slice remains an economy device, and a
prior that can misfire, not a correctness device. (N=5, preliminary,
reproducible.)
4.3 The monolith floor and the symbol descent ($T_{2b}$)
Measuring the budget in tokens (not files) exposed a structural wall:
| Level | Task state for a real pandas KeyError | Cost |
|---|---|---|
| file-level minimum (11 files) | frame.py, generic.py, … | ~372k tok — unsatisfiable |
| $T_{2b}$ symbol level | DataFrame.merge (32 lines), NDFrame._get_label_or_level_values (59 lines), _MergeOperation.__init__, _get_merge_keys, def-use slice of merge | ~15.4k tok (−96%) |
Class-enclosed frames become exact method line-ranges (sed -n 'a,bp');
top-level functions become backward def-use slices (Python exact, Go
conservative — §10). The manifest ships the extraction commands ready to run.
The symbol descent was validated on a real Go monolith, blind: tidwall/gjson
(one file, 3,650 lines) at its parent of the fix for issue #192. The real panic
(slice bounds out of range [:5] with length 4) was reproduced, its Go stack
frames drove $T_{2b}$ to the symbol slice Get/execModifier/parseArray/squash
(~12.9k tok, −58% of the file), and the fix for squash was written from that
slice alone — coming out byte-identical to the upstream commit f0ee9eb; the
full gjson suite stays green and upstream's own TestIssue192 passes. This is
the conservative Go slice (§10) exercised end-to-end on production code.
4.4 Ambient cost operator
The budget needs no human input. The $T_1$ hook snapshots the live context
occupancy from the session transcript on every tool call; a PreToolUse rule
injects --budget auto into any slicer invocation that lacks one:
budget: auto: session 6e2e49dc, model claude-fable-5,
window ~467k, in use ~362k, headroom ~104k -> budget 41k
4.5 Operator cache
$T_2$ is deterministic, so identical inputs are never recomputed. The cache key is (repo fingerprint, symptom, parameters, resolved budget, operator hash):
| pandas (1,415 files) | |
|---|---|
| cold run | 12.0 s |
| identical re-run | 0.26 s (46×) |
Every manifest is stamped operatore: T2@<hash> — change the script and the
cache invalidates itself, and telemetry knows which operator version produced
each data point.
5. The canary: measuring effect, not intent
A savings log proves the hook computed a replacement — not that the harness applied it. The canary closes that gap: each replacement records its exact footer (with the numbers); on the next invocation the hook checks the session transcript for that footer. Present → verified. Absent → alarm, with the model told in-band that savings are being overstated.
And the canary now does more than warn: after N violations in the same
session (CK_CANARY_DEGRADE_N, default 3) the session auto-degrades to
raw pass-through — the hook stops compressing that session entirely. When the
harness is ignoring updatedToolOutput, the full output enters the context
regardless, so continuing to compress only spends work and litters the output
with markers; degrading stops paying a cost that is being thrown away. It is
per-session (a new session, with a unique id, starts clean — the degrade is
never permanent nor contagious) and reversible within a session via
savings.py --reset-canary once the contract is understood to be restored.
This is not theoretical. The canary's first real alarm led to three real bugs in one night: the Read tool's nested response shape had silently never been normalized; canary verification could false-positive on content that merely quoted a footer; stderr-only output was destroyed by the replacement path. All three are fixed and regression-tested — and the fix was itself produced by running this plugin's own pipeline ($T_2 \to T_3 \to \text{fix} \to T_4$, verdict: PASS 12/12).
5.0 The release smoke rite: the canary philosophy, end-to-end
Every release of this plugin that was verified live surfaced bugs the full
test suite could not see — because tests exercise the operators, while a live
session exercises the contract with the real harness. hooks/smoke.py
turns that ritual into a deterministic two-command protocol, run inside a
live session:
python3 hooks/smoke.py generate # 400 lines with a needle computed at
# runtime — the hook compresses this output
python3 hooks/smoke.py check # verifies, on the real transcript, what
# the harness actually did: 8 PASS/FAIL points
check asserts: result present in the transcript; compressed there
(updatedToolOutput honored); needle elided; parking declared with its key;
key in the store; recall.py --grep recovers the needle numbered; no new
canary failures; advisor mechanics (4 points) on the session's real context
state. A release is not green until the smoke passes in a real session.
Declared scope: the Bash leg represents the ephemerals; real /compact,
resume and the guards remain manual rites (they need harness events a script
cannot emit).
5.1 Sampled answer-invariance: the A/B on live traffic
The canary proves the replacement entered the context; it says nothing
about whether the answer stayed in its equivalence class. That is what the
sampled A/B measures. Every Nth elision (CK_AB_RATE, default 20 — elision is
the risky kind of normalization; re-read deltas are formal and exempt)
stores its (original, compressed) pair. Then
python3 hooks/ab_verify.py # judge pending samples (--status, --dry-run, --limit N)
has a model judge each pair — does the compressed version preserve every
actionable signal of the original? — via claude -p (headless, your
subscription: still zero API keys). Verdicts land in the same report as the
savings: rate is what $T_1$ removes, distortion is what the A/B
measures on real traffic, and every DEGRADED verdict names what was lost
(kept in the state file under degradations), so the heuristics get tuned
against real misses instead of anecdotes.
5.2 Revealed relevance: mining what the model actually opened ($T_5$)
Ablating the context to measure each piece's contribution is interventionist (N pieces = N+1 calls) and its signal drowns in run-to-run variance. The honest, free version is observational: the transcript already reveals which files of the working set the model actually opened, which files it opened outside the slice, and which page faults it paid after an elision.
python3 hooks/revealed.py # last 5 transcripts
python3 hooks/revealed.py session.jsonl # or explicit ones; --json for machines
python3 hooks/revealed.py --aggregate --last 30 # longitudinal: recurring
# faults -> config proposal
python3 hooks/revealed.py --aggregate --apply-rates # actuate: write learned
# per-category rates for T1
python3 hooks/revealed.py --aggregate --write-priors # actuate: write learned
# per-repo priors for T2
The report answers "how much did the faults cost?" with numbers — slice files never opened (the prior was wide: consider fewer importers/depth), files read outside the slice (lost seeds: candidates for the next slice), page-fault count with the token cost of the re-reads. Every insight is a suggestion a human applies — the telemetry suggests, determinism stays intact (same rule as everywhere else in the pipeline: no learned operators).
--aggregate [--last N] adds the longitudinal view a single transcript
cannot show: recurring page faults on the same file (→ proposed # ck:raw
or threshold bump, with the accumulated token cost), files repeatedly read
outside the slice (→ seed candidates), slice files never opened across
several manifests (→ the prior is wide). Proposals fire only on
recurrence (≥2 sessions/occurrences) — the single episode is already in
the per-transcript report; and they remain proposals: no auto-tuning.
Since 1.11.0 the proposals can also be actuated — explicitly. The loop
stays human-in-command: nothing is written unless you run the command, and
both writers only move in the fail-safe direction. --apply-rates writes
// faq
What is context-kernel?
Task-induced context normalization for coding agents — a native Claude Code plugin. The task induces a projection; operators preserve the answer, not just shrink tokens. Deterministic, stdlib-only, 117 tests.. It is open-source on GitHub.
Is context-kernel free to use?
context-kernel is open-source under the MIT license, so it is free to use.
What category does context-kernel belong to?
context-kernel is listed under rag in the Claudeers registry of Claude-compatible tools.
// embed badge
[](https://claudeers.com/context-kernel)
// retro hit counter
[](https://claudeers.com/context-kernel)
// reviews
// guestbook
// related in RAG & Knowledge
Persistent Context Across Sessions for Every Agent – Captures everything your agent does during sessions, compresses it with AI, and injects relevant contex…
✨ Light and Fast AI Assistant. Support: Web | iOS | MacOS | Android | Linux | Windows
Compress tool outputs, logs, files, and RAG chunks before they reach the LLM. 60-95% fewer tokens, same answers. Library, proxy, MCP server.
A light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code by TÂCHES.