Self-trained models for my personal-tools stack. Harvest labels from Claude transcripts; orchestrate train and bake into Rust crates.
cradle is a workspace of four Rust crates that turn ~/.claude/projects/**/*.jsonl
transcripts into labeled training data, train a real model, and bake the
trained weights into a dependency-free Rust crate via morsel
— so a consumer can classify without Python, a model file, or a network
round-trip.
This is the v0.2 release: harvest, train, bake, classify, and eval
all ship end-to-end for the redirect model. v0.1 shipped the harvest +
orchestration core only; v0.2 (PRD-cradle-bake-integration) closes the
loop with a real numpy trainer and native Rust codegen.
cradle/
├── crates/
│ ├── cradle-harvest/ transcript JSONL → labeled examples + split
│ ├── cradle-features/ shared featurization registry (turn_pair_v1)
│ └── cradle-baked/ compiled-in baked models (build.rs resolves
│ output/morsel-<model>/src/lib.rs -> the
│ committed tests/fixtures/<model>_fixture/
│ lib.rs -> a harmless stub)
├── src/ cradle binary (cli)
│ ├── cli.rs clap-based subcommands
│ └── orchestrator.rs harvest -> train -> bake -> classify/eval
├── models/
│ ├── redirect/ ← the only model with a real trainer + bake
│ ├── session-productivity/ spec only; extractor deferred
│ └── playbook-match/ spec only; extractor deferred
└── output/
└── morsel-redirect/ generated by `cradle bake redirect`;
standalone crate depending on `morsel`
| Command | What it does | status |
|---|---|---|
cradle harvest <model> |
Walk transcripts, apply the named label extractor, write data/<model>/{train,val,test}.jsonl |
shipped (redirect model only) |
cradle train <model> |
Shell out to uv run python models/<model>/train.py; gate metrics.json against spec.toml's threshold/auc_threshold (exit 3 on fail, or --allow-below-threshold) |
shipped |
cradle bake <model> |
Read checkpoint.json and write output/morsel-<model>/ — const weight arrays over morsel::linear/activation, score/predict, no allocation |
shipped |
cradle classify <model> |
Classify --features <8 floats> or --turn-pair <json> with the compiled-in baked model; prints {"model","p","label"} |
shipped (redirect only) |
cradle eval <model> |
Checks the Python predictions against a Rust re-execution of checkpoint.json (the same morsel::linear_flat/activation numerics cradle bake compiles into const arrays) over data/test.jsonl; exits 0 only if max error < 1e-4 and agreement is 100% |
shipped (redirect only) |
cradle build <model> |
harvest → train → bake → eval | shipped |
cradle status |
Print per-model on-disk status. --json for machine-readable |
shipped |
train.py writes $CRADLE_OUTPUT_DIR/checkpoint.json in this shape (a
2-layer MLP: input_dim -> hidden_dim tanh, hidden_dim -> 1 sigmoid):
{
"schema": "cradle.checkpoint.v1",
"model": "redirect",
"input_dim": 8,
"layers": [
{"w": [[...8 floats], ...8 rows], "b": [...8 floats], "act": "tanh"},
{"w": [[...8 floats]], "b": [...1 float], "act": "sigmoid"}
],
"threshold": 0.5
}cradle bake reads this directly (no safetensors, no external morsel bake binary — morsel is inference-only by design, so the codegen
lives in cradle) and turns layers[i].w/layers[i].b into const
arrays, wiring them together with morsel::linear::linear and
morsel::activation::{tanh,sigmoid}. Output is deterministic: the same
checkpoint always produces byte-identical src/lib.rs (floats formatted
with {:?}, no timestamps).
train.py also writes metrics.json (cradle.metrics.v1: real
test_accuracy, test_auc, n_train, n_val, n_test, epochs) and
predictions.jsonl ({"source_session","source_turn","p"} per test-split
row) — the reference both cradle bake's generated unit test (checks the
compiled const arrays) and cradle eval (checks a dynamic
re-execution of checkpoint.json, before any bake — see the cradle eval row above) compare against.
turn_pair_v1 features 2 and 3 (user_turn contains a redirect keyword
/ user_turn starts with a redirect keyword) are computed from the same
positive_keywords list redirect_v1 uses to assign the label in the
first place (see crates/cradle-features/src/lib.rs and
crates/cradle-harvest/src/lib.rs). That makes the label close to a
direct function of two of the eight input features, which is why
redirect's test_accuracy/test_auc land at 1.0: the harvested
transcripts don't (yet) contain a redirect that avoids every keyword in
the list.
Verified by retraining the same architecture on the same split with
features 2 and 3 zeroed throughout (not just at inference on the
already-trained model, which instead collapses to chance — the current
model puts effectively all of its decision weight on those two
features): test accuracy drops from 1.0 to 0.9444 (34/36). That's a
real signal from the other six features (turn-length, token count,
Jaccard overlap, trailing ?, tool-use marker), just a much weaker one
than the reported 1.0 suggests. Treat redirect's current metrics as
"can the model recite the keyword list back", not "can it recognize a
redirect it hasn't seen worded that way before" — a real generalization
read needs held-out examples that are redirects without being
keyword-literal ones.
12 MUST-level + 1 SHOULD-level criteria, all green at gate time. The
intent-card lives at agent/intent-card.json; the acceptance tests
that prove each AC live at tests/acceptance_acN.rs.
numpy's array math is mature enough for an 8→8→1 MLP and the
train.py shellout runs once, offline, per (re-)bake cycle. The trained
weights leave the Python world entirely at checkpoint.json and never
reach the consumer — cradle bake turns them into const arrays, and
everything downstream (classify, eval, and any future daemon) is
pure Rust. Switching to a pure-Rust trainer (candle, burn) remains an
option later; the harvest / features / bake / classify surfaces don't
change either way.
cargo build --workspace
cargo test --workspace
cargo run --bin cradle -- status
cargo run --bin cradle -- harvest redirect --models-dir models --transcripts-dir ~/.claude/projects
unsafe_code = "deny"at the workspace level.- clippy
pedantic + nurseryas warn; BAD_RUST patterns (unwrap,expect,panic,todo,unimplemented,dbg!) as deny in production code. cargo deny check bans licenses sourcesis the supported subset (the fullcargo deny checkerrors against cargo-deny 0.18.3 on a CVSS 4.0 advisory entry — fixed upstream in 0.18.4+).
Built via /autobuilder from
PRD-cradle.md on 2026-05-27. The
hand-built prototype it replaces is preserved at
~/wintermute/cradle-2026-05-27-handbuilt-bak/.
- v0.2.0 (PRD-cradle-bake-integration): real numpy trainer for
redirect(2-layer MLP, full-batch GD, fixed seed, early-stopped on val loss);cradle traingatesmetrics.jsonagainst spec.toml thresholds (exit 3 on fail);cradle bakereadscheckpoint.jsonand writes realscore/predictconst-array Rust, no more skeleton; newcrates/cradle-bakedworkspace member compiles the baked model in viabuild.rs; newcradle classify/cradle evalsubcommands;cradle buildruns harvest → train → bake → eval end-to-end. - v0.1.1 (2026-05-30):
cradle bakeskeleton — parsed[bake]from spec.toml, gated on receipt 7 (test_accuracy >= threshold), generated anoutput/morsel-<model>/crate placeholder (superseded by v0.2.0).
Dual-licensed under MIT OR Apache-2.0. See LICENSE-MIT
and LICENSE-APACHE.