Day 27 — 'the orchestration ships before the trainer.'
Six SE sprints shipped in one session: SE.7.0 corpus extractor, SE.7.2 eval + promotion gate, SE.2 autofix Proposer, SE.4 complexity router, SE.5.1 offline cache warmer, SE.7.3 nightly auto-train daemon. The day-defining moment: SE.7.3 went green in tests but the operator caught that no real trainer was wired ('i wanted to have to train over night... thats why i just set that up'). What we learned trying to fix that: SR8 is AMD CPU + integrated Radeon, no NVIDIA, so mlx-lm won't run there; HF PEFT on CPU is overnight-feasible but produces a degraded adapter on the 11-row corpus skill-smoke can produce; skill-smoke is monitoring data not training data because the dedup key collapses 523 cycle rows to 11 unique. We shipped a stub trainer (Modelfile copy creating a real Ollama-resident name) so the rest of the pipeline could prove itself overnight: corpus build, growth gate, candidate naming, real eval-adapter composite scores, real promotion gate decisions. Timer armed for 2-hour cadence. By morning the journal will carry ~4 verified cycles with real composite-score data.
“the orchestration ships before the trainer.”
Six SE sprints in one session. Brainstorm-none, ship-six. SE.7.0 corpus extractor → SE.7.2 eval + promotion → SE.2 autofix Proposer → SE.4 complexity router → SE.5.1 cache warmer → SE.7.3 nightly auto-train daemon. Then the day-defining moment: I shipped SE.7.3 thinking the work was done, the operator typed “i wanted to have to train over night…. thats why i just set that up” and the real story started.
The answer was not “ship SE.7.1 trainer in a couple hours.” The answer was: stub the trainer, prove the orchestration under real load tonight, name SE.7.1 as a clearly-scoped next sprint. The orchestration ships before the trainer. That’s the architecture thesis the day named itself after.
SE.7.0 — the corpus extractor
The skill-smoke daemon + the curriculum daemon have been firing on SR8 for days, but their outputs (markdown reports + CSV) weren’t shaped to feed a trainer. SE.7.0 closes that gap.
Three commits:
- (
06ebf85) — patch both daemons to persist full prompts + responses to sibling JSONL files (samples.jsonl,outcomes.jsonl). Earlier rows only storedcode_len/response_len; useful for trend graphs, useless for SFT. - (
f3b3e55) —werbos.corpuspackage: readers for both sources, threshold filter on verdict, canonical-key dedup (source + identifier + normalized prompt), Sample dataclass. - (
f6d145c) —werbos corpus build/werbos corpus statsCLI. Cron-friendly. Writes unified SFT JSONL.
Schema marker _schema: "corpus/1" on every row so a future
v2 reader can refuse stale shape. Append-only field policy
locked.
SE.7.2 — eval + promotion gate
(cdf22f2) — composite eval across skill-smoke (format-marker
pass rate) and curriculum (problem-solve pass rate). Run against
any candidate Ollama model. Optional baseline → promotion gate
decides promote/reject based on a delta floor (default 0.05).
werbos eval-adapter run --candidate werbos-0.2 --baseline werbos-0.1 \
--json /tmp/eval.json --delta 0.05
Exit codes mirror the gate: 0 promote, 1 reject, 2 argv error. Cron + train-daemon both depend on this.
SE.2 — the autofix Proposer (v1)
(f4b10da) — walks the SE.1.1 trend CSV + SE.7.0 samples,
classifies skill regressions into one of four diagnoses:
MARKER_TOO_STRICT, MODEL_FLOOR, MODEL_DRIFT, INFRA. Emits
fix-candidate-<skill>-<hash>.md files for operator review.
No code modifications. v2 will layer a PR-drafter on top once v1 proves itself.
A real bug surfaced + fixed during the sprint: the diagnosis order was wrong. MARKER_TOO_STRICT was firing first when responses were too short to contain any markers, masking MODEL_FLOOR which is the actual root cause. Reordered the classifier so MODEL_FLOOR is checked first.
SE.4 — complexity router (v1)
(6675b4e) — pure-function heuristic classifier maps the
operator’s input + skill metadata to one of three tiers
(SIMPLE / MEDIUM / COMPLEX), which then routes to the right
Ollama model:
SIMPLE llama3.2:3b ~3-5s
MEDIUM llama3.1:8b ~10-40s
COMPLEX qwen2.5:14b ~60-150s
Rules locked in order:
- skill.model set / ≥5 markers / category in {creative, wellness} → COMPLEX
- empty input → SIMPLE
- short (≤24 chars) + no skill → SIMPLE
- SIMPLE trigger word (“hi”, “thanks”) on short input → SIMPLE
- COMPLEX trigger word (“explain”, “translate”, “code”) → COMPLEX
- default → MEDIUM
Opt-in via WERBOS_ROUTER_ENABLED=1 so v1 never surprises an
existing flow. Per-tier overrides via env vars.
SE.5.1 — the offline cache warmer
(e5bf5a1) — SE.5 Layer 2. SE.5 v1 shipped lazy-fill caching
(first call pays inference; second serves from SQLite). SE.5.1
adds offline pre-rendering: skills declare a seed_corpus tuple
of common operator inputs, the warmer walks each precookable
skill’s corpus overnight, the chat surface then serves cached
responses on the FIRST live ask, not the second.
Two orthogonal opt-ins:
precookable | seed_corpus | Behavior |
|---|---|---|
False | anything | Skill never touches the cache. |
True | () | Lazy-fill only (SE.5). |
True | ("a", "b", ...) | Lazy-fill + offline warming. |
Seed corpora populated for eli5 (10 concepts: recursion, DNS,
git rebase vs merge, …) and quote (10 topics: perseverance
through long projects, shipping over perfection, …).
summarize left empty — its inputs are unique paste-text, no
sensible seed corpus exists.
Per-skill error isolation in the runner so one skill failing
mid-batch doesn’t abort the whole warming pass. _..._ italic
error responses (run_prompt_skill’s “couldn’t reach model”
convention) are recognized and NOT cached.
SE.7.3 — the train daemon, AND the SE.7.1 reality check
(5375284) — composes the SE.7 arc into one cron-friendly
orchestrator: corpus build → growth gate → train → eval →
promote. Six phases, each observable via the journal. Persistent
state at ~/werbos/train_daemon/state.json (atomic tmp + rename
so a SIGKILL mid-write leaves the prior file intact).
Tests went green: 68 new (+19 state, +15 policy, +16 daemon, +18 CLI). I shipped it. The operator typed:
“i wanted to have to train over night…. thats why i just set that up”
Caught me. I’d shipped the orchestration AND the integration
point (WERBOS_TRAIN_CMD env var), but never wired an actual
trainer that runs on SR8. The honest constraints I should have
surfaced when shipping SE.7.3:
- SR8 is AMD Ryzen 7 + integrated Radeon 780M, no NVIDIA GPU, 27GB RAM, ~3.9GB used.
mlx-lm(whatwerbos train lorashells out to) is Apple Silicon only — won’t run on SR8.- HuggingFace PEFT works on CPU but a 3B QLoRA over our 11-row corpus would take 8-12h wall-clock and overfit to a single example per skill.
SE.7.1 (real Linux trainer) is a real sprint. Not a tonight sprint.
The stub trainer story
The compromise: ship a stub trainer that creates a real
Ollama-resident model name (FROM llama3.2:3b via the
/api/create endpoint), so the rest of the pipeline gets to
prove itself overnight. Each “trained” model is secretly the
base; eval-adapter scores it for real; promotion gate gets a
real decision (mostly REJECT, since delta is ~0).
Three real bugs hit in production that the unit tests didn’t catch — patched live:
- Ollama 0.23 API change. Old
modelfile: "..."field was replaced byfrom: "<base>". Stub trainer crashed with a 400 on the first SR8 cycle. - eval-adapter writes
baseline: null(not{}) on single-model runs. (e8c2632) The daemon assumed dict and crashed at.get(...).get(...). Patched withor {}coalescing + added a bootstrap-promote case so the first cycle establishes an active_version. Two new regression tests pin both edge cases. - systemd unit escapes are unforgiving. First timer fire
leaked literal backslash-escapes into the
WERBOS_TRAIN_CMDenv var:sh: 1: python3\ /data/...: not found. Fix: drop the escapes, write a host wrapper script (run-werbos-train-daemon.sh), point systemd at the wrapper. Quoting lives in bash, not systemd.
After all three fixes: a real end-to-end cycle in 7.5 minutes on SR8. Corpus build → growth gate (passed) → stub trainer (10s) → eval-adapter (real composite 0.455 on 11-row corpus) → bootstrap PROMOTED werbos-0.1. Cycle 2 ran 6 minutes later: werbos-0.2 vs werbos-0.1 baseline → composite 0.500 → delta +0.000 → REJECTED. Exactly what the orchestrator should do.
systemd timer armed: every 2h starting 01:10 PDT. By morning the journal carries ~4 verified cycles with real composite- score data against a real (if synthetic) candidate model.
The skill-smoke-isn’t-training-data discovery
While trying to fill the corpus before the overnight run, I
backfilled /data/skill-smoke/samples.jsonl from 49 cached
markdown reports — +512 rows. Then ran werbos corpus build. Output: 11 rows. Same as before.
The dedup key in SE.7.0 is (source, identifier, normalized prompt). Every skill-smoke cycle for eli5 uses the same
canonical test_input as the prompt. So 49 cycles’ worth of
eli5 outputs all collapse to ONE row.
This is correct dedup behavior. It’s also a structural truth about the data pipeline: skill-smoke is a monitoring signal, not a training signal. Real corpus growth needs varied prompts — operator chat archives, dogfood replay, curriculum (once outcomes.jsonl is flowing).
Logged as SE.7.0.1 for tomorrow. The 100-row growth floor in SE.7.3 was never going to be hit from skill-smoke alone.
Hotfix and the regression-test discipline
The null-baseline crash (e8c2632) went straight to main
with two new regression tests. Found in production at 06:09
UTC, patched at 06:10, container rebuilt at 06:11, first
successful overnight-style cycle ran at 06:11–06:17. Twelve
minutes from observation to fix-deployed.
The test the bug had been hiding from:
async def test_baseline_null_in_eval_json_does_not_crash():
"""eval-adapter writes `baseline: null` for single-model runs."""
_router.eval_payload = {
"candidate": {"composite_score": 0.65},
"baseline": None, # <- real eval-adapter shape
"decision": None,
}
# ...
The unit tests had always passed {"composite_score": ...} as
the baseline payload. The integration test the codebase
needed was “what does eval-adapter actually write when you
don’t pass a –baseline.” Now pinned.
Sprint accounting
| Sprint | Status | Commit | Tests added |
|---|---|---|---|
| SE.7.0 | shipped | 34a310e | +47 |
| SE.7.2 | shipped | 4906c12 | +37 |
| SE.2 | shipped | 98c7a4f | +51 |
| SE.4 | shipped | 0339f1c | +41 |
| SE.5.1 | shipped | bc3933b | +36 |
| SE.7.3 | shipped | d15e617 | +68 |
| hotfix | shipped | c1c1951 | +2 |
| Total | 6 sprints + 1 fix | — | +282 |
Test sweep: 5013 passing, +198 over the day’s start. Eight pre-existing failures unchanged (SSRF + session-registry — known unrelated).
Three AHAs worth keeping
- Skill-smoke is monitoring data, not training data. Same prompt every cycle → dedups to one row → trainer sees nothing varied. The real corpus has to come from operator chat archives or curriculum.
- The orchestration ships before the trainer. Six phases of cron, journal, state, eval, gate, promotion all work without a real LoRA. SE.7.1’s job is just to fill the training slot.
- Unit tests pin the shapes you imagined; integration tests pin the shapes you got. Three bugs tonight (Ollama API, null baseline, systemd escapes). Zero would have shown up in unit tests; all three caught within a minute of running the real pipeline. Cover both.
Tomorrow
Day 30 is the launch deadline (shifted from May 29 to June 2 per the day-counter pause). Three things between here and there:
- Buy werbos.ai domain. Push everything to a new org repo.
- First NPMjs push. Depends on A.5 Day 5 PyPI publish landing first (still the load-bearing precondition).
- Glow up the org. LICENSE, CONTRIBUTING, hero README.
SE.7.1 (real Linux trainer) gets its plan-mode after the org move closes. The overnight cycles will keep running on the stub; the journal will have stories to read by morning.
“the orchestration ships before the trainer.”