“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:

  1. (06ebf85) — patch both daemons to persist full prompts + responses to sibling JSONL files (samples.jsonl, outcomes.jsonl). Earlier rows only stored code_len / response_len; useful for trend graphs, useless for SFT.
  2. (f3b3e55) — werbos.corpus package: readers for both sources, threshold filter on verdict, canonical-key dedup (source + identifier + normalized prompt), Sample dataclass.
  3. (f6d145c) — werbos corpus build / werbos corpus stats CLI. 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:

  1. skill.model set / ≥5 markers / category in {creative, wellness} → COMPLEX
  2. empty input → SIMPLE
  3. short (≤24 chars) + no skill → SIMPLE
  4. SIMPLE trigger word (“hi”, “thanks”) on short input → SIMPLE
  5. COMPLEX trigger word (“explain”, “translate”, “code”) → COMPLEX
  6. 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:

precookableseed_corpusBehavior
FalseanythingSkill 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 (what werbos train lora shells 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:

  1. Ollama 0.23 API change. Old modelfile: "..." field was replaced by from: "<base>". Stub trainer crashed with a 400 on the first SR8 cycle.
  2. eval-adapter writes baseline: null (not {}) on single-model runs. (e8c2632) The daemon assumed dict and crashed at .get(...).get(...). Patched with or {} coalescing + added a bootstrap-promote case so the first cycle establishes an active_version. Two new regression tests pin both edge cases.
  3. systemd unit escapes are unforgiving. First timer fire leaked literal backslash-escapes into the WERBOS_TRAIN_CMD env 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

SprintStatusCommitTests added
SE.7.0shipped34a310e+47
SE.7.2shipped4906c12+37
SE.2shipped98c7a4f+51
SE.4shipped0339f1c+41
SE.5.1shippedbc3933b+36
SE.7.3shippedd15e617+68
hotfixshippedc1c1951+2
Total6 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

  1. 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.
  2. 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.
  3. 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:

  1. Buy werbos.ai domain. Push everything to a new org repo.
  2. First NPMjs push. Depends on A.5 Day 5 PyPI publish landing first (still the load-bearing precondition).
  3. 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.”