Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
0b8b4bd
distill: mine "A fails here, B works" repair pitfalls from shell runs
vu1n Oct 27, 2026
894aef6
repairs: confirmed success, same invocation, redact values by default…
vu1n Oct 27, 2026
83cad14
repairs: prove the fix segment ran, plain wrapper words only, skip in…
vu1n Oct 27, 2026
d184772
repairs: attribute module/env errors only in single-command runs; ski…
vu1n Oct 27, 2026
2ae4fa8
repairs: allowlisted wrappers only, install inside the working run, c…
vu1n Oct 27, 2026
e658633
repairs: background jobs prove nothing; probe flags reject with any args
vu1n Oct 27, 2026
0f0ac8c
repairs: read Codex's 'Process exited with code N'
vu1n Oct 27, 2026
f0a7964
repairs: pair a script only with a run from the same directory
vu1n Oct 27, 2026
2f7db34
repairs: split shell operators outside quotes; env flags count as a c…
vu1n Oct 27, 2026
7c416f9
repairs: any setup or install in the working run disqualifies it; com…
vu1n Oct 27, 2026
8e01e90
repairs: only install/add/sync count as installs, not -v
vu1n Oct 27, 2026
86c3a5c
repairs: unchanged means no env change; an env fix sets only the miss…
vu1n Oct 27, 2026
084ba9e
repairs: quoted or escaped operators are arguments; probes don't prov…
vu1n Oct 27, 2026
6c0b90e
repairs: read comments and newlines as the shell does; an env change …
vu1n Oct 27, 2026
19c672d
repairs: a later bare run after export or source isn't unchanged
vu1n Oct 27, 2026
f9f1f57
repairs: compare a script's arguments too; an install in between rule…
vu1n Oct 27, 2026
d5b2f0a
repairs: same invocation means the same arguments; the claim shows on…
vu1n Oct 27, 2026
f04928b
repairs: a cd after the command leaves the place unknown; installs ar…
vu1n Oct 27, 2026
ebc95ed
repairs: a failure is explained only by its own repair
vu1n Oct 27, 2026
2ae9e6c
repairs: a trailing separator ends the command
vu1n Oct 27, 2026
86eb10e
repairs: |& is a pipe; a file edit in between rules out a script repair
vu1n Oct 27, 2026
070f89e
repairs: an empty argument isn't the end of the command; substitution…
vu1n Oct 27, 2026
093e16a
repairs: error text counts only from a failed or status-masked run; r…
vu1n Oct 27, 2026
abf61b7
repairs: shell edits in between count as edits; a URL or user@ in the…
vu1n Oct 27, 2026
480ada7
repairs: skip redirection-only segments; unset is an env change; only…
vu1n Oct 27, 2026
76a32c0
repairs: an in-place edit in the working run counts; quoted < and > a…
vu1n Oct 27, 2026
66d5ca7
repairs: attribute an error across every pipeline stage
vu1n Oct 27, 2026
f6fe03a
repairs: a redirection-only command still sets the chain's status
vu1n Oct 27, 2026
ce35b8e
repairs: writes before the invocation in the working run count; only …
vu1n Oct 27, 2026
65ab06a
repairs: a relative-path fix must run from the same place
vu1n Oct 27, 2026
b5adba2
repairs: keep argv boundaries when joining a command list
vu1n Oct 27, 2026
62a3d66
repairs: exempt only the segment that runs the command from the write…
vu1n Oct 27, 2026
0ad24a8
repairs: a cd made on its own between runs changes the place
vu1n Oct 27, 2026
106f929
repairs: check the wrapped executable's path, not the wrapper's
vu1n Oct 27, 2026
2d94ec7
repairs: reject any absolute path and env changes between the runs
vu1n Oct 27, 2026
1e32b12
repairs: every relative cd moves the persistent shell
vu1n Oct 27, 2026
01146e6
repairs: skip runs fed stdin; scan between runs only for a candidate fix
vu1n Oct 27, 2026
883c3b4
repairs: follow the persistent shell's directory through every cd
vu1n Oct 27, 2026
c298b45
repairs: only Claude's Bash keeps its directory across calls
vu1n Oct 27, 2026
6ca0629
repairs: a piped or backgrounded cd leaves the shell's place unknown
vu1n Oct 27, 2026
080c429
repairs: a bare run after an export doesn't prove the variable unneeded
vu1n Oct 27, 2026
2f03dd5
repairs: follow a cd only when the run proves it ran; export runs not…
vu1n Oct 27, 2026
9bd3a06
repairs: only a persistent shell carries an export to the next call
vu1n Oct 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/features/distill.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ paths:
- "kypp/distill.py"
- "kypp/batch_distill.py"
- "kypp/episodes.py"
- "kypp/repairs.py"
- "scripts/eval_episodes.py"
- "eval/episodes/*"
- "docs/episode-extraction-evaluation.md"
Expand All @@ -12,6 +13,7 @@ entry:
- "kypp/batch_distill.py"
tests:
- "tests/test_episodes.py"
- "tests/test_repairs.py"
---
# Distillation

Expand All @@ -20,6 +22,22 @@ distiller runs when `KYPP_DISTILL_MODEL` is set (`claude`, `codex` or an ollama

## Gotchas
- Without `KYPP_DISTILL_MODEL` you only get heuristic, failure-mined pitfalls.
- Repair pitfalls (`kypp/repairs.py`) run with or without a model: a shell command that fails
with command-not-found, a missing module or a missing env var, followed in the same session by a
form that works (`uv run pytest`, `.venv/bin/python`, `V=x cmd`). The subject names only what
failed, so a second session's repair lands on the same subject and the K=2 gate accepts it; a
one-session quirk stays a candidate. A repair suppresses the generic "`<tool>` repeatedly
failed" pitfall for the failures it explains.
- A repair needs a later run with a confirmed success (Claude's `completed`, Codex's exit 0) that
runs the same thing: the same script or `-m module` for a missing module or env var (inline
`-c` code never counts). Only a segment followed by nothing but `&&`, outside any `||` or pipe,
counts as having succeeded, since a chain exits with its last command. It is
skipped when the original works later without the fix, when the thing was installed in between,
when the later run only probes (`--version`, `--help`), or when the working form is a host path.
- Values are written only for a short allowlist of env vars (`PYTHONPATH`, `NODE_ENV`, ...), and
never one with `@` in it. A fix runs the command by path or behind a known wrapper (`_WRAPPERS`:
`uv run`, `python -m`, `npx`, ...). Nothing else counts, so `touch pytest` isn't a fix, and flags
and their values (`--token X`, `-e K=V`) never reach a claim.
- If the LLM distiller fails, it falls back to the heuristic distiller rather than raising.
- With `TYPESAFE_API_KEY` or `KYPP_DECIDER_URL` (a local strands-decider) set, and `kypp[s1]`
installed, a System One gate drops drafts scoring
Expand Down
29 changes: 24 additions & 5 deletions kypp/distill.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from typing import TYPE_CHECKING, Protocol

from . import episodes as _episodes
from . import s1
from . import repairs, s1
from .vocab import OLLAMA_DEFAULT_HOST, TYPES # single-sourced vocabulary (turso-free leaf)

if TYPE_CHECKING:
Expand Down Expand Up @@ -169,14 +169,32 @@ def distill(self, trace: Trace) -> list[ClaimDraft]:
f"in this session's observations", 0.6))
if trace.run_failed:
drafts.append(ClaimDraft("pitfall", "run failed", _trunc(trace.run_failed), 0.5))
fixes, explained = repairs.mine(trace)
for name, n in trace.tool_failures.items():
if n >= 2:
sample = next(a for a in trace.actions if a.failed and a.name == name)
unexplained = [a for i, a in enumerate(trace.actions)
if a.failed and a.name == name and i not in explained]
# A failure a repair explains is said better by the repair ("A fails, B works").
if n >= 2 and unexplained:
sample = unexplained[0]
drafts.append(ClaimDraft(
"pitfall", f"{name} repeatedly failed",
_trunc(f"`{name}` errored {n}x this run; e.g. {sample.output}"), 0.4,
code_refs=_refs_from(sample.input)))
return drafts
return drafts + fixes


class WithRepairs:
"""Add the deterministic repair pitfalls to another distiller's drafts. The LLM distiller is told
to skip environment artifacts; a repair is one only until a second session hits it, and then
the K=2 gate accepts it."""

def __init__(self, inner: Distiller):
self.inner = inner

def distill(self, trace: Trace) -> list[ClaimDraft]:
drafts = self.inner.distill(trace)
taken = {d.subject for d in drafts}
return drafts + [d for d in repairs.mine(trace)[0] if d.subject not in taken]


# LLM_DISTILL_PROMPT + _SCHEMA are what the model is ASKED for (model-agnostic, code-grounded claims).
Expand Down Expand Up @@ -404,7 +422,8 @@ def distiller_from_env() -> Distiller:
A configured System One model (see kypp.s1) adds the write gate on top. The seam-config analog
of store.store_from_env."""
model = os.environ.get("KYPP_DISTILL_MODEL")
inner = (FallbackDistiller(LLMDistiller(_complete_from_model(model), episodes=_episodes.extractor_from_env()),
inner = (FallbackDistiller(WithRepairs(LLMDistiller(_complete_from_model(model),
episodes=_episodes.extractor_from_env())),
HeuristicDistiller())
if model else HeuristicDistiller())
via = s1.client()
Expand Down
Loading
Loading