Skip to content

Repository files navigation

cradle

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.

What v0.2 ships

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`

Subcommands

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

Checkpoint schema (cradle.checkpoint.v1)

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.

Known limitation: redirect's 1.0 test accuracy is inflated

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.

Acceptance criteria (intent-card)

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.

Why a Python shellout for cradle train?

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.

Build / run

cargo build --workspace
cargo test --workspace
cargo run --bin cradle -- status
cargo run --bin cradle -- harvest redirect --models-dir models --transcripts-dir ~/.claude/projects

Workspace lint posture

  • unsafe_code = "deny" at the workspace level.
  • clippy pedantic + nursery as warn; BAD_RUST patterns (unwrap, expect, panic, todo, unimplemented, dbg!) as deny in production code.
  • cargo deny check bans licenses sources is the supported subset (the full cargo deny check errors against cargo-deny 0.18.3 on a CVSS 4.0 advisory entry — fixed upstream in 0.18.4+).

Origin

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/.

Recent

  • 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 train gates metrics.json against spec.toml thresholds (exit 3 on fail); cradle bake reads checkpoint.json and writes real score/predict const-array Rust, no more skeleton; new crates/cradle-baked workspace member compiles the baked model in via build.rs; new cradle classify/cradle eval subcommands; cradle build runs harvest → train → bake → eval end-to-end.
  • v0.1.1 (2026-05-30): cradle bake skeleton — parsed [bake] from spec.toml, gated on receipt 7 (test_accuracy >= threshold), generated an output/morsel-<model>/ crate placeholder (superseded by v0.2.0).

License

Dual-licensed under MIT OR Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.

About

Under construction: harvest labeled data from Claude transcripts, orchestrate train, bake toward a Rust crate

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages