The map is the conceptual field guide β€” what an LLM IS, what training does, how adapters stack. This page is the system flow β€” every component an operator turn passes through, the artifacts each component writes, and how an operator debugs when the answer is wrong.

Explore the whole flow interactively β†’ All 169 components as one navigable graph β€” drag, zoom, search, and click any node to walk its connections across every layer.

Want to reason about the whole system with an LLM? Grab the plain-text graph export β†“ β€” paste it into Claude or ChatGPT.

If you only read one diagram on this page, it’s the first one.

Master flow β€” input to result

flowchart TD A[/"Operator types: hey werbos, explain /Users/.../partyparrot"/] --> B B["werbos chat / dashboard / CLI / Slack bridge"] --> C C["session.perms β€” auth + permission gate"] --> D D["parsetongue.parse()"]:::parse --> D1{Format-codepoint strip} D1 --> D2{Empty?} D2 -- yes --> Z1[NOVEL β†’ chat fallback] D2 -- no --> D3{Starts with /?} D3 -- yes --> D4[SLASH fast path] D3 -- no --> D5{Path + verb? SE.6.1} D5 -- yes --> D6[TOOL fast path] D5 -- no --> D7[LLM classify slow path] D4 --> E D6 --> E D7 --> E E["Intent kind, confidence, slots, paths, cache_key"]:::intent --> F{Route on kind} F -- skill --> G1[run_prompt_skill] F -- tool --> G2[path_tools handler] F -- imperative --> G3[PermissionAskRequired gate] F -- lookup --> G4[precook cache lookup] F -- novel --> G5[chat backend] F -- unclear --> G6[clarifying options to operator] G1 --> H{SE.5 precook cache hit?} H -- yes --> R1["under 50ms cached response"] H -- no --> I[SE.4 router pick model tier] I --> J[ollama_chat] J --> K[response] K --> L[write to cache if precookable] L --> R1 G2 --> R2[handler result PathToolResult] G3 --> R3[await operator confirm] G4 --> R1 G5 --> R4[chat response] R1 --> Out[/Operator sees rendered markdown/] R2 --> Out R3 --> Out R4 --> Out G6 --> Out Out --> T[(traces written to ~/werbos/traces/date.jsonl)]:::trace classDef parse fill:#fff5e9,stroke:#c96442,stroke-width:2px classDef intent fill:#f0d4c5,stroke:#a04d2f,stroke-width:2px classDef trace fill:#ece9e2,stroke:#4a473f,stroke-width:2px,stroke-dasharray:5

Every box above is a real module in the werbos codebase. Every arrow is a function call. The dashed trace box at the bottom is the SAME trace ID the operator pulls when debugging β€” see the debug walk below.

Layer 1 β€” parsetongue, the intent compiler

The first thing operator input touches after auth. Three fast paths (slash, path+verb, LLM) all produce the same Intent shape so downstream routing doesn’t care which one fired.

sequenceDiagram autonumber participant Op as Operator participant P as parsetongue.parse participant Strip as strip_format_codepoints participant Trace as trace_turn participant Slash as _parse_slash participant PV as _try_path_verb_route
(SE.6.1) participant LLM as _llm_classify participant Intent as Intent Op->>P: "explain /Users/jason/foo" P->>Strip: strip codepoints Strip-->>P: clean input P->>Trace: bind trace_id Trace-->>P: tid P->>P: starts with /? no P->>PV: try path+verb route PV->>PV: extract_paths() PV->>PV: match_verbs() PV-->>P: Intent(kind=TOOL,
tool_name=understand_project) Note over P,Intent: confidence 0.85
(0.70 base + 0.15 explain bias) P->>Intent: fast-path result Intent-->>Op: routed to handler

When neither slash nor path+verb fires, the slow path calls ollama_chat to classify the input against the skill catalog. That’s ~10-30s on local 8B; the fast paths return in sub-millisecond.

Skill router (parseltongue, brick 1 β€” Day 41). Before the slow LLM-classify path, a deterministic skill router tries to match natural language to one of the 295 skills outside the model β€” IDF-weighted term scoring, high-precision-or-abstain (it only fires above a score + margin threshold, else falls through to chat). This exists because the 3B mangles tool calls β€” asked to emit dispatch_skill, it drops operators (1+2+3 β†’ 123), leaks {function <nil>}, or empties out. The law it encodes: don’t ask a small model to emit tool calls β€” route deterministically, then run the skill. Enabled with --skill-router; pair with --no-skill-dispatch to gate the tool the small model can’t hold.

Layer 2 β€” skill execution + cache + router

Once parsetongue says kind=SKILL, this is the path the response takes. Two big bypass routes save the LLM call entirely: the precook cache (SE.5) on hit, and the complexity router (SE.4) picking a smaller model when the input doesn’t need the big one.

flowchart LR A[run_prompt_skill name, input] --> B{precookable?} B -- no --> C B -- yes --> D{cache_get key} D -- hit --> R1["under 50ms
SQLite + markdown render"] D -- miss --> C C[resolve model: caller arg β†’ skill.model β†’ router β†’ default] --> R R{SE.4 router enabled?} R -- no --> M R -- yes --> RT[classify_complexity
SIMPLE / MEDIUM / COMPLEX] RT --> RM[select_model per tier] RM --> M[ollama_chat] M --> Resp[response] Resp --> W{precookable?} W -- yes --> Save[(write to cache)] W -- no --> Return Save --> Return[return to caller] style R1 fill:#d9ead3,stroke:#39782b,stroke-width:2px style Save fill:#f0d4c5,stroke:#a04d2f

A cache hit on the [/eli5 recursion] path serves in <50ms total. A miss + MEDIUM-tier router pick (llama3.1:8b) runs the inference in ~10-40s. A COMPLEX-tier pick (qwen2.5:14b) runs in ~60-150s.

Layer 3 β€” tool dispatch (SE.6.1)

Path-aware intents (explain /path, refactor /path) take this branch. Five tools in the v1 registry; four are stubs awaiting CR.1.x.

flowchart LR A[Intent kind=TOOL,
tool_name=X,
paths=...,
slots=...] --> B[path_tools.get_handler] B --> C{tool_name} C -- understand_project --> H1[stub
CR.1 lands real handler] C -- refactor_readme --> H2[stub destructive
CR.1.1 lands real] C -- plan_sprint --> H3[stub destructive
CR.1.2 lands real] C -- polish_path --> H4[stub destructive
wraps /polish] C -- open_path --> H5[REAL: cd/ls suggestion] H1 --> R[PathToolResult ok, message, coming_soon] H2 --> R H3 --> R H4 --> R H5 --> R style H5 fill:#d9ead3,stroke:#39782b style H1 fill:#fff5e9,stroke:#c96442 style H2 fill:#fff5e9,stroke:#c96442 style H3 fill:#fff5e9,stroke:#c96442 style H4 fill:#fff5e9,stroke:#c96442

Stubs emit “coming in sprint X, tracking doc at Y”. When CR.1 lands, the stub handler gets one-line-replaced with the real implementation; parsetongue’s routing never changes.

Layer 4 β€” the reasoning loop (reason_tree)

When an answer needs more than one lookup, compose/reason_tree.py treats the goal as a silhouette and recursively decomposes it toward base cases, then composes back up. Every node is content-addressed (Merkle): a repeated subgoal is proven once (β™» cache), and an unprovable one is an honest GAP (βˆ… abstain), never a guess. The make-or-break property is convergence β€” the frontier must funnel to leaves, not explode. This is werbos thinking past a single card; the synthesis tier (synthesize_reasoned.py) inverts the same loop to build past it β€” decompose an intent to eternal primitives, compose, then RUN and verify.

Bounded deterministic lanes run first β€” solve what can be proven, never reason it (Day 124). compose/reason_qa.py matches a small set of question shapes (term-limit, comparison), decomposes to atomic card facts, and composes an answer with a Merkle proof β€” else GAP. compose/puzzle_solver.py takes finite constraint puzzles (statement-grid and positional-ordering β€” in/not-in a slot, higher/lower Β±gap, immediately after/before, odd/even, adjacency, “exactly K of these are true,” meta-statements): it parses the clues, enumerates every arrangement, and verifies, answering the unique solution, or honestly “N solutions β€” underdetermined” or “no solution β€” contradictory” (it caught two ChatGPT-built puzzles that were themselves broken, in ~4 ms, where a 1B free-rode a wrong proof for 77 s). These win their clause because the answer is computed and checked, not generated; each falls through cleanly when the input isn’t its shape. And compose/grounding_guard.py gates every voiced answer: a restyle ships only if the fact’s anchors (numbers, proper names) survive β€” faithful paraphrase passes, drift falls back to the plain fact, and an off-topic card is refused outright.

flowchart TD RQ[/"goal / query β€” a silhouette"/]:::reason --> RCACHE{Merkle cache hit?} RCACHE -- yes --> RREUSE["β™» reuse the proven node"] RCACHE -- no --> RBASE{base case? a card lookup or a compute} RBASE -- yes --> RFACT["πŸ” fact β€” one grounded card"] RBASE -- no --> RDEC["decompose β†’ OR-alternatives of subgoals"] RDEC --> RREC["recurse on each subgoal"] RREC --> RCACHE RDEC -. no rule / max depth / cycle .-> RGAP["βˆ… GAP β€” honest abstain"] RFACT --> RUP["⊒ compose back up Β· Merkle-hash the node"] RREUSE --> RUP RUP --> RFRONT{frontier funnels to leaves?} RFRONT -- yes, converges --> RROOT["proof root β€” re-runnable"] RFRONT -- no, still open --> RREC RROOT --> RANS["answer, verified β†’ cache back to the library"] RGAP --> RANS RANS --> RSYN{build past it?} RSYN -- "synthesis tier" --> RBUILD["compose eternal primitives β†’ RUN β†’ verify or abstain"] classDef reason fill:#efeaff,stroke:#7c4dff,stroke-width:2px

Debugging β€” when the answer is wrong

Every operator turn writes a chain of trace events to a date-stamped JSONL. Every event carries the same trace_id. The debug walk is: find the trace_id β†’ grep β†’ cross-reference.

flowchart TD Q[/"Operator: this answer is wrong"/] --> A[1. find trace_id
X-Werbos-Trace-Id response header
or ~/werbos/traces/] A --> B[2. grep events with that trace_id] B --> B1["jq 'select(.trace_id == X)'
~/werbos/traces/today.jsonl"] B1 --> C{which layer is wrong?} C -- parse classified wrong --> D1[check parsetongue/parse event
kind, confidence, notes] C -- skill output wrong --> D2[check skill execution event
model, cache hit, duration] C -- consistent skill failure --> D3[cross-reference SE.1.1 trend
~/werbos/skill-smoke/trend.csv] C -- cached wrong answer --> D4[werbos precook stats
werbos precook clear --yes] C -- daemon failure --> D5[~/werbos/train_daemon/journal.log
per-phase outcomes] D3 --> E1{regression?} E1 -- yes --> F1[werbos autofix scan
fix-candidate-*.md] E1 -- no --> F2[skill prompt may need tuning
see PROMPT_SKILLS] classDef art fill:#ece9e2,stroke:#4a473f class B1,D1,D2,D3,D4,D5,F1 art

Every shaded box is a real file path or CLI command. The trace ID makes the chain queryable; the cross-references (skill-smoke trend β†’ autofix β†’ corpus) tell you whether this is a one-off or a persistent pattern.

Memory β€” retrieval-as-memory (Day 45)

A 1M-token window is RAM-bound and impossible locally (the KV cache wants ~15 GB at 128K; ~16K is the comfortable ceiling on an 8 GB box). So werbos remembers by retrieving, not by holding. Each turn, an optional SessionRecall syncs the conversation into a WorkingMemory (instant ~25 Β΅s writes, ms-fast lexical recall, lazy off-path embeddings) and recalls the turns relevant to the latest message back into the system prompt β€” right at the seam where the ContextBuilder truncates old turns. So a fact that scrolled out of the window returns when it matters: effective recall on a small, fast window.

flowchart LR U[/"user turn"/] --> H[live history] H --> CB{ContextBuilder} CB -->|fits| P[system prompt] CB -->|too big| TR[truncate oldest turns] H -. sync every turn .-> WM[(WorkingMemory
append-only)] TR -. would be lost .-> WM U --> RC[recall relevant turns] WM --> RC RC -->|inject "Recalled from earlier"| P style WM fill:#ece9e2,stroke:#4a473f style TR fill:#fff5e9,stroke:#c96442

Opt-in (werbos chat --recall, werbos serve --recall); best-effort (never takes down a turn); off for Tom (no state may cross his turns). The same primitive makes the mind portrait’s memory-retention spoke real β€” 0.88 lexical, 1.00 with the semantic arm.

Day 124 sharpened this for live chat. compose/session_memory.py treats the running conversation as its own tiny retrievable card library: it keeps the user’s own turns (gap-managed sessions, ~1800 s), and on each new message recalls the fact that matches β€” the user’s words about their world outrank any global hub card, grounded before retrieval. So “my company is Acme, its CEO is Sarah” resurfaces as “Sarah is your CEO” turns later β€” not “Sarah Vaughan, the jazz singer,” which is what a stateless-per-turn chat said before. It answers the relevant segment rather than echoing the whole sentence back, and a recall question (“what did I just fix?”) resolves on the recency of the ask. Constant memory β€” retrieve the one fact β€” not linear, holding the whole conversation in a giant context: the frontier’s way, inverted. This is the killer axis of the 30B fight β€” the long conversation, where a bigger model hits its context wall and forgets the middle.

The dogfooding loops

werbos isn’t just an inference engine β€” it’s a system that watches itself. Thirteen loops accumulate signal on what’s working and what isn’t. They feed each other.

GR.1 games β†’ reasoning ratchet

The newest loop, and the one aimed straight at the 30B fight’s general axis. A deterministic game engine is a reasoning-problem oracle with a non-model adjudicator: it can pose a position and score the answer without a model in the grade. Nim proved it β€” werbos.games.nim.best_move both generates frozen ratchet probes (derive the optimal move, or honestly refuse from a lost position) and grades them; the reasoning ratchet ran 26/26. The same shape generalizes: Chess/Reversi/Nim as hard deterministic tests, Wordle/Minesweeper as clean puzzles, Tarot as the honesty/interpretation boundary. Games become an adversarial fuzzer for the reasoning ruler.

flowchart LR GE[game engine
e.g. nim.best_move] --> P[generate probe
+ gold answer] P --> RR[reason_ratchet] RR --> M[owned model derives] M --> J{oracle grades
capability + honesty} J -->|green→red| Reg[regression caught] J -.-> WA[weak_at → self-study list]

HL.1 HAIRLy β€” Claude drives the UI as a user

The live, adversarial cousin of the ratchets (those are frozen and offline; this one explores through the product). Claude simulates a real user in the actual chat UI β€” types a human-phrased prompt, reads the reply, grades it β€” and two judges reduce to one verdict (the Y): an AI grade and a free deterministic grade. Agree β†’ keep the label; disagree β†’ the one turn a human reviews. Grades are labels + test cases, never weights β€” a reject routes to router / card_library / shape and becomes a new scenario (the ratchet grows itself). It’s a self-fuzzer: Claude paraphrases each seed into the messy ways real people ask (filler, typos, tone), so phrasing-sensitivity bugs surface on their own. It also runs multi-turn across a roster of user personas β€” the people asking, distinct from werbos’s cleo/trump/ dickinson output voices β€” each with a different typing voice and interests (bubbly Danielle, terse Marcus, formal Walter, gen-z Tyler, run-on Sofia, skeptical Arjun), so the same question arrives in six registers and each plan probes a distinct behavior (a boundary, an honest abstain, slang routing, the cooking lane, the moat under pressure). One holds a whole conversation in character, and an arc grader checks context retention, consistency, and boundary-over-time (does werbos still remember she’s going to Mexico by the sangria turn?). Being internal tooling, both Claude legs run on the subscription (claude -p), never the metered API. The first live turn β€” “who won the 2019 Nobel Prize in Literature?” β€” caught a real routing bug (teacher-lane course-dump β†’ 🟠 med fail), and fixing it unmasked a confidently-wrong poisoned card the teacher lane had been hiding. The roster paid out too: the same travel question in Danielle’s bubbly voice vs Marcus’s terse one routed differently β€” tone was hijacking informational questions into the emotional lanes (advice/empathy/hype) β€” now fixed and locked by a ratchet.

flowchart LR S[seed scenario] --> P[Claude paraphrase
human phrasings] P --> UI[types in the chat UI] UI --> W[werbos answers] W --> CG[Claude grade] W --> DG[deterministic grade] CG --> Y{Y-merge} DG --> Y Y -->|agree| L[hairl.jsonl label] Y -->|disagree| H[needs_human] L -.-> FIX[fix_target β†’ router/library/shape + new scenario]

HL.2 werbos probe β€” one spec, any model (Day 140, werbos/probe/)

The third test shape, beside the frozen gauntlets (prompt tests) and HAIRLy (UI tests): rspec for language models. A spec is describe β†’ it β†’ expect β€” fire a prompt, optionally mock a tool, assert on the reply β€” and it runs unchanged against any provider, because every provider maps its answer into one Reply shape (text, lane tag, citations, latency). werbos is reachable over HTTP (the live :8097 the chat uses) or in-process, where mocks are real (t.mock('weather').timeout() breaks the actual tool); claude-cli and raw ollama are the frontier and open-weights baselines. Allowed failures (allow_fail=['ollama']) keep cross-model runs fair, a persona-fuzz layer rephrases each prompt in a human voice, and an LLM-judge matcher carries HAIRLy’s subjective grading. The 500-row gauntlet auto-loads as four tier suites. Every run can land on a timeline (--history β†’ benchmarks/probe_history.jsonl: commit, per-suite, transport errors, server RSS peak), and the runner samples the server’s memory around each test so a leak names the turn that caused it.

flowchart LR SPEC[spec
describe β†’ it β†’ expect] --> RUN[runner] RUN --> PV{provider} PV -->|http| WB[werbos :8097] PV -->|inproc + real mocks| WI[werbos brain_reply] PV --> CC[claude-cli] PV --> OL[ollama
open-weights baseline] WB & WI & CC & OL --> RP[one Reply shape] RP --> EX[expect: mentions Β· cites Β· abstains Β·
uses_lane Β· judged] EX --> REP[HTML report] EX --> HIST[(probe_history.jsonl)] WB -.->|GET / rss_mb| MEM[per-test memory growth] MEM --> REP

HL.3 conversation judge β€” human-likeness, not correctness (Day 146, scripts/convo_judge.py)

The gauntlets and the probe grade correctness with deterministic adjudicators. This grades the thing no scoreboard does and that is the actual goal: does a 15-turn conversation feel human? It runs a scripted multi-turn chat against the live :8097 in one session (so continuity is tested), hands the whole transcript to claude -p as a person in the chat β€” not an examiner β€” and gets back per-turn flags, the “bot tells” that broke the illusion, a 1-10 score and a PASS/FAIL (“PASS = I’d happily keep chatting”). It never sees the right answer; it judges the experience. This is the ratchet that caught what five waves of deterministic polish couldn’t move β€” the dry fact turns β€” and proved the fix was warm-authored facts (Day 146), not a bigger model or a weaker moat. warm_poc.py is its A/B rig: same convo, dry facts vs. warm-authored facts, scored side by side (4 β†’ 7, FAIL β†’ PASS).

flowchart LR C[scripted 15-turn convo] --> AB[werbos :8097
one session] AB --> TR[full transcript] TR --> J[claude -p
human-in-the-chat rubric] J --> V[per-turn flags Β· bot-tells Β·
1-10 Β· PASS/FAIL]

HL.4 structure ratchet β€” reply SHAPE, measured every hour (Days 156–159, scripts/structure_ratchet.py)

HL.3 judges one scripted chat. This replays every private gold conversation (390 turns over 80 chats, kept off-repo in ~/werbos/gold) through werbos and grades each reply on a ladder, each rung gated on the one below: gave an answer β†’ readable β†’ conversational (fits the thread). It never asks “did werbos beat ChatGPT”; it asks whether the reply has the right structure. Four views keep it honest: bricks-only vs. with a tongue, and the main set vs. a held-out set of 20 conversations the bricks are never built from and whose replies are never read β€” numbers only. A 65-turn hand-audit set is regraded by a person-style read every hour, and a fresh 38-turn sample checks the audit isn’t overfit. --check fails a commit that drops a rung. Every factual “not sure” lands in a knowledge-gap log, and scripts/gap_queue.py turns those into a mint queue of topics β€” the honest refusal becomes the next card.

flowchart LR G[private golds
main Β· held-out Β· audit] --> R[structure_ratchet
bricks / tongue] R --> L[ladder: gave β†’ readable β†’ convo] L --> H[ratchet_history.jsonl
--check gates the commit] R --> K[knowledge_gaps.jsonl] K --> Q[gap_queue β†’ mint_queue
topics, never questions]

SE.1.1 skill-smoke

Fires every ~30 min on SR8 via systemd. Runs every skill in PROMPT_SKILLS, grades the response against format markers, writes both a markdown report (operator-readable) and a JSONL row per skill (machine-readable, SE.7.0 corpus source).

flowchart LR T[/"systemd timer
every 30 min"/] --> R[werbos skill-smoke] R --> M[run each skill] M --> G[grade markers] G --> Out Out[(trend.csv + samples.jsonl + .md reports)] Out -.-> SE2[SE.2 autofix] Out -.-> SE70[SE.7.0 corpus]

SE.3 curriculum

Picks coding problems from a curated bank, runs them through werbos’s local model via docker exec, judges via claude -p. Difficulty adapts to streaks. Outputs accumulate in outcomes.csv + outcomes.jsonl.

flowchart LR T[/"host daemon
scripts/curriculum_daemon.py"/] --> S[pick problem from bank] S --> So[werbos solves] So --> J[claude -p judges] J --> Out Out[(outcomes.csv + outcomes.jsonl)] Out -.-> SE70[SE.7.0 corpus]

SE.7.0 corpus extractor

Walks the two daemon JSONLs (skill-smoke + curriculum), filters to confirmed passes, dedups by canonical key, writes one unified SFT JSONL.

flowchart LR A[(skill-smoke
samples.jsonl)] --> M B[(curriculum
outcomes.jsonl)] --> M M[werbos corpus build] --> F[filter passes] F --> D[dedup by source+id+prompt] D --> Out[(sft.jsonl)] Out -.-> SE73[SE.7.3 train daemon]

The training-fuel factory (Day 108)

The model mints its own training data. One domain map (the 374 /graph domains) drives three pipelines against a burst-rented GPU teacher; cards go to the retrieval library (graded first, never weights), prose and pairs feed the owned tongue.

flowchart LR D[/"the 374 graph domains"/] --> FC[fluency corpus
wiki + SE + fiction] D --> CM[scenario matrix
+ conversation mint] D --> CF[card factory
expand + grow + new-domains] FC --> T[rented H200 teacher] CM --> T CF --> T T --> S[(staging)] S --> Gr[grade + dedup] Gr -.->|cards| LIB[(retrieval library)] Gr -.->|prose + pairs| TON[(owned tongue corpus)]

SE.7.2 eval + promotion gate

Composite eval (skill-smoke pass rate Γ— curriculum pass rate) against a candidate model. Optional baseline β†’ promotion decision.

flowchart LR C[candidate model] --> E[werbos eval-adapter run] B[baseline model] --> E E --> SS[skill-smoke pass] E --> CC[curriculum pass] SS --> COMP[composite score] CC --> COMP COMP --> G{delta >= floor?} G -- yes --> P[PROMOTE] G -- no --> R[REJECT]

SE.7.3 train daemon

Six phases, every 2 hours via systemd. Atomic state file, journal log, real eval data even when the trainer is a stub.

flowchart LR T[/"systemd timer
every 2h"/] --> P1[corpus build] P1 --> P2{growth gate} P2 -- pass --> P3[train
via WERBOS_TRAIN_CMD] P2 -- no growth --> Skip[skip cleanly] P3 --> P4[eval-adapter] P4 --> P5{promotion} P5 -- promote --> S1[update state.active_version] P5 -- reject --> S2[log + continue] S1 --> J[(journal.log)] S2 --> J Skip --> J

SE.2 autofix Proposer

Walks SE.1.1 trend + samples, classifies regressions by diagnosis (MARKER_TOO_STRICT, MODEL_FLOOR, MODEL_VARIANCE), emits fix-candidate markdown for operator review. Read-only.

flowchart LR A[(trend.csv)] --> S[werbos autofix scan] B[(samples.jsonl)] --> S S --> P[find consecutive-fail patterns] P --> D[diagnose] D --> M[generate fix proposals] M --> Out[(fix-candidate-*.md)] Out -.-> H[/operator reviews/]

SE.5.1 cache warmer

For skills marked precookable=True with a non-empty seed_corpus, pre-renders responses overnight so the chat surface serves from cache on the FIRST live ask.

flowchart LR T[/"cron / manual"/] --> W[werbos precook warm] W --> S[for each precookable skill] S --> C{already cached?} C -- yes --> Skip[skip] C -- no --> R[run_prompt_skill] R --> Cache[(precook.db SQLite)]

A.4 dogfood-replay

Scans chat archive .pfm files for replayable test inputs (messages where the operator was clearly debugging or testing). Feeds them back into eval suites.

flowchart LR A[(~/werbos/chats/*.pfm)] --> S[werbos regressions scan] S --> F[filter test-like inputs] F --> R[replay against current model] R --> G[similarity grade] G --> Out[(regression report)]

Autopilot β€” the self-directed loop (Day 37, 2026-06-09)

The newest loop, and the only one where werbos acts on its OWN initiative. Drop a request in <inbox>/requests/ and the autopilot predicts a skill + the result it expects before acting, runs the skill itself, judges the outcome, and corrects on a miss. Every tick appends an experience; a slow loop trains a routing policy from those experiences and promotes it only if it beats the champion on held-out data β€” the same champion/challenger gate as the train daemon, applied to the autopilot’s own forward model.

flowchart LR R[/"request in
inbox/requests/"/] --> Rt{router ORACLE
confident?} Rt -- yes --> P[PREDICT
skill + expected] Rt -- abstain --> P P --> A[ACT
dispatch_skill] A --> O[OBSERVE] O --> E{ERROR
satisfied? soft judge} E -- yes --> X[(experience log)] E -- no --> C[CORRECT
next-best skill] C --> X X --> RF[routing-fit re-grade
router ORACLE] RF --> M{right skill?} M -- misroute --> Red[⚠ flag β€” the failure
the soft judge hid] M -- fit --> Keep[honest fuel] X -.->|train split| T[learn routing policy] T --> G{beats champion
on held-out?} G -- yes --> Pr[promote policy] G -- no --> Rj[keep champion] Pr -.->|LearnedPredictor| P style E fill:#fff5e9,stroke:#c96442 style M fill:#fff5e9,stroke:#c96442 style Red fill:#f7d9d2,stroke:#c0392b style G fill:#f0d4c5,stroke:#a04d2f style X fill:#ece9e2,stroke:#4a473f

Fail-closed by design: it only runs skills on a safe allowlist (refusing anything off it), every action is traced + append-only revertible, and it’s opt-in (werbos serve --autopilot, off by default). The prediction error is the whole point β€” it’s both the real-time correction signal AND the slow loop’s training data. Watch it with werbos autopilot watch, the dashboard’s Autopilot panel, or glasshouse trace <id>; it even reflects on itself (werbos autopilot reflect β€” what I know, what I’m missing, what I’d improve).

Day 46 β€” securing the loop (honest grading). The “satisfied?” judge is the model grading itself, and it lies β€” it marked misroutes (a joke routed to anxiety-spiral, “werbos morning” eaten by a greedy 401k-check) as ok, hiding the very failures the loop needs to learn from. So the experience log now gets a routing-fit re-grade: each request is re-routed through the deterministic skill-router oracle (external, abstain-or-commit) and a disagreement is flagged as a misroute. werbos autopilot report and the dashboard’s routing column show them in red β€” the honest fuel. The hardened-grader lesson, proven three ways today (here, the reasoning compute-miner, and the unstuck loop): you cannot grade a model with a model β€” ground the grade outside it.

Day 48 β€” outside the model (four fixes). The Day-46 routing oracle finished its spread through the loop. (1) reflect now grades reliability against that same oracle, not the soft self-grade β€” a skill earns “reliable” only on routed, on-target wins, so the greedy magnets get named instead of flattered (the honesty reached the self-model, not just the dashboard). (2) The oracle moved UPSTREAM into PREDICT: the high-precision router picks first; the LLM predictor only handles what routing abstains on β€” route outside the model, so the magnets stop being chosen in the first place. (3) The un-bribable tier shipped β€” a known-answer grader (werbos.agents.known_answers): a curated bank of (question β†’ TRUE answer), human-authored, checked deterministically, tried before the model-written exec check. It catches the confident-wrong answers the soft AND exec tiers rubber-stamped (learn weights known Β» exec Β» soft). (4) ExperienceStore adopted soma.capture as a soft dep β€” capture engine when installed, byte-identical fallback otherwise β€” so every experience row carries a surface tag + ts and the loops finally log one shape.

Day 136 β€” the feed that went dark, reconnected. The experience log had a silent hole: when chat moved to the owned brain, live turns stopped becoming experiences, so the log went dead on 2026-07-25 and the whole self-model froze on two-month-old data β€” the dashboard’s reliable-at/weak-at, the monologue, and the dreams all stuck. Fixed: every live chat turn now appends an experience (skill/lane Β· grounded=satisfied / abstained=refused Β· trace), so reflect learns from real traffic again. Same fix un-stuck the mind β€” the monologue’s self-recall loop (it used one fixed session and recalled its own prior questions) and its novelty guard (it only compared the last thought, so an A/B/A/B fixation slipped through) both got closed, and the narrator moved off the 30M babbler to grounded-curiosity (the engine grounds; the 1B voices).

Day 137 β€” knowledge acquisition + language composition. Two new stages, both built as bricks. The fact lane now routes a general fact (“atomic number of iron”, “capital of Australia”, “CVE-…”) through werbos/knowledge/: a ~100-source capability index β†’ the source that covers it β†’ a grounded, cited answer (host datasets offline; live APIs net-gated; else honest-abstain). And it acquires: a host ground harvests the whole entity into cards (harvest β†’ mint), so one question fills the library and the next answers offline β€” resolve()’s SEEK β†’ CACHE, the API the SEEK, minting the CACHE. The conversational lanes compose a reply from interlocking rhetorical bricks (compose/convo_bricks.py

  • rhetoric.py): a program of moves (observe β†’ name β†’ generalize β†’ contrast β†’ close, or problem β†’ solution β†’ advice β†’ close) chained so each brick consumes the prior’s result, combined, then the 1B voices the skeleton (compose/tongue.py) β€” surface realization only, grounding-guarded. The lanes fill the grounded content; the tongue just connects it.

Day 141 β€” owned facts, chains, and what the voice may touch. The fact lane gained two owned, model-free sources: the Wikipedia infobox (knowledge/infobox.py β€” the structured field on the subject’s own article: author, artist, composer, capital, release year; a near-miss or ambiguous title answers nothing) and the IANA port registry the OS already ships (knowledge/ports.py, offline). On top of the lane sits the find-then-compute chain (compose/fact_chain.py), ahead of the brick bin: step one grounds through the fact lane, step two runs the owned compute stack on that answer (one fact, a fact pair, or a list), and a trailing “element with that atomic number” hop reads the periodic table backwards. Only a compute lane may produce step two, and a chain that can’t finish hands the whole question on down the ladder (web, code rungs) β€” the fact lane is kept from answering its first sentence alone. The voice layer’s reach shrank to match: computed results, chain results and house-voice facts ship unvoiced; only an explicitly requested persona restyles a fact, name- and number-guarded. And the web rungs trust less: fan wikis, homework sites and AI-written encyclopedias are filtered before the reader β€” and out of the learned cache β€” and an answer the reader says it inferred is a miss.

Then the loop that learns from all that honest grading got the same treatment. (5) The slow champion/challenger trainer (already running in werbos serve) scored skills on satisfied β€” the soft grade β€” so magnets earned routing credit and the policy learned to route TO them. It now trains on honest fuel: a row teaches the policy only if it was satisfied AND on-target (no confirmed misroute by the oracle; a known-answer FAIL never counts). (6) That champion/challenger gate was carved into its own sibling β€” soma.learn (now ready): a surface-agnostic gate(experiences, champion, train=, score=) any loop can use; run_gate soft-adopts it. (7) The known-answer bank learned to grow β€” unbanked checkable questions land in a review queue (propose), a human banks the TRUE answer (add_to_bank), seed + a runtime-writable user bank. Five soma pieces are ready (grade/trigger/fence/capture/learn); gate + search are the last planned carve-outs.

MMM coding forest β€” the dogfood loop that grows a library (Day 45)

The newest loop runs in the mmm-poc repo and learns a library, not a policy. A coding request is routed outside the model to a verb capsule (prompt + exemplars + a free grader) or, failing that, to a general coder backed by the API forest β€” 600+ real JS/Node signatures retrieved and injected so the model uses the right API instead of half-remembering it. Every graded attempt becomes an experience; distill drives two feedback channels. The forest channel closes with no GPU: crystallize proposes new aliases from blind-spot wins (the library grows), garbage-collect proposes removals of the unused (it stays lean) β€” both propose-only, gated, reversible. The model channel banks certified (prompt, code) pairs for an eventual fine-tune (the angler). The whole point: move work from the model into the library until the generator can shrink to a small model we own.

flowchart LR Q[/"coding request"/] --> RT{route outside
the model} RT -->|verb match| V[verb capsule
+ grader-retry] RT -->|no match| F[general coder
+ API forest retrieval] V --> GR[GRADE
test / compile / run] F --> GR GR --> E[(experiences.jsonl)] E --> D[distill] D -->|passing runs| MP[(certified pairs
β†’ fine-tune: angler)] D -->|blind-spot win| CR[crystallize
propose alias/skill] D -->|unused| GC[garbage-collect
propose removal] CR -.->|review-gated| L[(forest library)] GC -.->|archive, revertible| L L -.->|grows + prunes| F style GR fill:#fff5e9,stroke:#c96442 style L fill:#ece9e2,stroke:#4a473f style E fill:#f0d4c5,stroke:#a04d2f

Proven live: the forest mis-ranked fs.cp for “list files recursively”; the loop crystallized the fix and fs/promises.readdir climbed from #3 to #1 (B.6). Fail β†’ grow, as a reviewable one-line diff β€” the legibility a weight update can’t give you.

Recursive completion β€” the reader that thinks (Day 58, werbos/compose/)

The kernel completes a silhouette into a grounded tree, but its every move was arithmetic over the library β€” token overlap, cosine, exact match β€” so it couldn’t tell “who IS a reporter” from “who is NOT”, and it stamped GROUNDED on whatever chunk shared words. This loop gives it the organ it lacked: a reader, and then teaches the reader to think. A query is a silhouette that implies sub-questions, so decompose it, recurse, and ground every hole β€” with a small local model fenced so it stays a cursor, never a knower: it points at ONE real leaf or abstains (a made-up pick reads as abstain), and a second, separate call may only ever downgrade a pass, never invent one.

flowchart TD Q[/"a query β€” a silhouette"/] --> DEC{decompose
one level, parsimonious} DEC -->|"a single thing"| RET DEC -->|"the sub-holes it implies"| SUB["sub-question Γ— N"] SUB --> RET[per hole: retrieve top-k
tree-local TF-IDF] RET --> R{fenced reader
pick ONE, or NONE} R -->|"NONE / out-of-range"| AB[(named gap
honest abstain)] R -->|"a cited leaf"| GD{grader β€” L1
may only DOWNGRADE} GD -->|"WRONG / UNSOUND"| AB GD -->|"states the answer"| OK[(grounded leaf
+ citation)] OK --> REAL{{node REAL iff
every hole real β€”
gaps NAMED, not faked}} AB --> REAL style R fill:#fff5e9,stroke:#c96442 style GD fill:#fff5e9,stroke:#c96442 style AB fill:#ece9e2,stroke:#4a473f style OK fill:#f0d4c5,stroke:#a04d2f

The result is a kernel Node tree, so is_real / failures / render read it unchanged β€” and honesty is per-hole: “I can ground who’s a reporter and the penalty, but not the exemptions,” three separate truths instead of one confident blur. Proven with an A/B, same corpus and retriever, swap only the reader: arithmetic baseline 45% answered, 6 confident lies β†’ 7B fenced reader 55%, 0 lies. The number that didn’t move β€” retrieval already held the right leaf 91% of the time β€” is the whole point: the win is reading, not retrieving.

Then it was driven to failure on purpose, on the real 2342-chunk client corpus: it decomposed perfectly and grounded confident garbage (a lesson title, “How to Report: this lesson explains the steps,” answered “how soon must I report”). The diagnosis was final β€” the binding chain is corpus quality β†’ retrieval recall β†’ reader β†’ grader β†’ loop, and the loop is at the end. Same wall the coding forest hit: recall is the wall, the library side is the only door. Refined thinking’s parting gift is to point a finger at exactly where the real constraint lives β€” the library β€” instead of papering it with fluent prose. Live: python -m werbos.compose.reason "<q>" --corpus <…>.

Day 60 β€” raised the library, measured the loop, swapped the cursor. Four organs closed the gaps that finding opened. A deterministic refiner (corpus_quality) re-chunks each blob into atomic fact-leaves and drops the TOC / quiz / navigation, so the buried facts become retrievable (on MRT the “24-hour” statute went from missing to top-ranked). A frozen reasoning bench (reason_bench) turned tuning-by-anecdote into a scoreboard β€” and promptly overruled the plan: the recall misses were the grader vetoing correct answers for wording (a one-line fix moved 33 β†’ 56% acc, honesty held), and a clever grade-veto re-pick was measured to raise lies and reverted. A parallel orchestrator (MAO) fans compound sub-questions out and abstains if any won’t ground β€” a real but modest honesty dial. And a brain.answer() facade gave one entry to all of it, behind which the reader’s model became a swappable cursor: local Ollama, the Anthropic API, or the claude CLI (Claude Code auth, no key). The library stays local; the cursor can be anything.

flowchart TD Q[/"question"/] --> DEC{decompose
atomic or sub-holes} C[/"corpus"/] --> RF[refine
blobs β†’ fact leaves
drop TOC/quiz/nav] RF --> IDX[(fact-leaf index)] DEC -->|"a bundle"| SUB["sub-question Γ— N"] SUB --> RET DEC -->|"single"| RET[per hole: retrieve top-k] IDX --> RET RET --> PICK{{fenced reader}} CUR[["the CURSOR β€” routable:
local Ollama Β· Anthropic API Β· claude-cli (no key)"]] -.->|drives| PICK PICK --> R{pick ONE, or NONE} R -->|"NONE"| AB[(named gap Β· abstain)] R -->|"a leaf"| GD{grade β€” answerhood,
not phrasing} GD -->|"WRONG / UNSOUND"| AB GD -->|"answers it"| OK[(grounded leaf + citation)] OK --> CMB{{combine β€” MAO:
withhold-on-incomplete}} AB --> CMB CMB --> ANS[/"grounded answer, or honest abstain"/] style PICK fill:#fff5e9,stroke:#c96442 style GD fill:#fff5e9,stroke:#c96442 style CUR fill:#ece9e2,stroke:#4a473f style AB fill:#ece9e2,stroke:#4a473f style OK fill:#f0d4c5,stroke:#a04d2f style IDX fill:#ece9e2,stroke:#4a473f

The whole brain in one flow: the library (refine β†’ fact-leaf index) stays local and is the moat; the cursor (fenced reader) is a routable model that picks a cited leaf or abstains, a separate grader may only downgrade, and the combiner assembles the grounded parts β€” naming, never faking, any hole it couldn’t fill.

The runtime pipeline β€” reflex β†’ route β†’ ground β†’ render (Day 61, werbos/compose/)

Day 61 wrapped that reader in the full runtime brain β€” two thin welds around a curated middle, abstaining at every door (docs/brain-nervous-system.md Β§2, werbos brain):

flowchart TD Q[/"query"/] --> RX{{reflex cache}} RX -->|"hit Β· cached turn"| OUT[/"answer β€” zero model"/] RX -->|"miss / unsure"| INT{intent β€” route to the
most CHARACTERISTIC library} INT -->|"off-topic / ambiguous"| DOOR[(abstain at the door)] INT -->|"a library"| REG[(registry β€” many
content-addressed MMMs)] REG --> GRD{{fenced reader
pick-or-abstain Β· grade-downgrade}} GRD --> RND{render} RND -->|"plain"| CITE[/"cited facts"/] RND -->|"fluent Β· FENCED"| FL{groundedness check} FL -->|"CLEAN"| NAT[/"fluent answer"/] FL -->|"ADDED Β· embellished"| CITE CITE --> LOG[(session log Β· provenance)] NAT --> LOG DOOR --> LOG LOG --> JUDGE{{operator judges:
correct / lie / over-abstain}} JUDGE --> GOLD[(living gold set)] GOLD --> DIAG{{diagnose miss:
retrieval / reading / library-gap}} DIAG -->|"retrieval"| INC[input compiler
fused PRF Β· drift-proof] DIAG -->|"reading"| RGAP[reader-gap
β†’ distill the WELD reader] DIAG -->|"library-gap"| CGAP[corpus-gap
mint fact Β· gate no-regression] GATE{gate β€” promote iff lies↓
& composite↑ & routing↑} INC --> GATE RGAP --> GATE CGAP --> GATE GATE -.->|"enriches"| REG style RX fill:#fff5e9,stroke:#c96442 style INT fill:#fff5e9,stroke:#c96442 style GRD fill:#fff5e9,stroke:#c96442 style RND fill:#fff5e9,stroke:#c96442 style FL fill:#fff5e9,stroke:#c96442 style GATE fill:#fff5e9,stroke:#c96442 style DIAG fill:#fff5e9,stroke:#c96442 style INC fill:#ece9e2,stroke:#4a473f style RGAP fill:#ece9e2,stroke:#4a473f style CGAP fill:#ece9e2,stroke:#4a473f style DOOR fill:#ece9e2,stroke:#4a473f style REG fill:#ece9e2,stroke:#4a473f style GOLD fill:#ece9e2,stroke:#4a473f style LOG fill:#ece9e2,stroke:#4a473f style CITE fill:#f0d4c5,stroke:#a04d2f style NAT fill:#f0d4c5,stroke:#a04d2f
  • Route by charness. Weight each term by df/size so a generic word incidentally present in a huge corpus can’t outvote the query’s real anchor β€” the fix for the big-library bias the live MRT smoke caught (routing 3/6 β†’ 6/6). Written in intent.select_library, which reads CorpusIndex chunks; it took until Day 104 to reach the CARD path, because resolve() had no route to it and library_lookup flattened all 374 libraries into one pool and took a global cosine argmax. spanish β€” 13,242 cards, a vocabulary library glossing most common English words β€” won eight bench queries outright, including “which bucket should i use to mop the kitchen floor?” β†’ el trapeador. Ported to cards as a second scope (29/52 β†’ 35/52, ten bluffs to three), with two corrections the measurement forced: an ambiguous top narrows to a shortlist rather than surrendering to the full pool, and a library’s own lang joins its profile because a library rarely names itself β€” javascript’s 10,454 cards seldom repeat “javascript”, so that token had near-zero charness on its own shelf.
  • The Honest Loop (loop.py) closes the outer circle: use β†’ record provenance β†’ the operator judges (correct / lie / over-abstain) β†’ the miss becomes a row in a LIVING gold set β†’ re-bench β†’ gate (promote only if lies don’t rise AND composite/routing don’t fall). The operator is the un-bribable grader β€” the loop that made a frontier model’s weights good, run cheaply over daily use.
  • Diagnose β†’ fix β†’ distill (diagnose.py, input_compiler.py, reader_gap.py, Day 62; distill.py, corpus_gap.py, Day 63). A caught miss is classified by fix layer (retrieval / reading / library-gap, deterministic, no model), then routed to the matching, gated fix β€” every class now has one. A retrieval miss β†’ the input compiler (enrich the query from the leaves β€” pseudo-relevance feedback, drift-proofed by keeping the raw top-k and appending only novel hits). A reading miss β†’ the reader-gap harness, which A/Bs the local reader against a frontier teacher on the same evidence and (distill.py) rebuilds the exact fence it misread, stamped with the teacher’s correct pick, as a training row. A library-gap miss β†’ corpus-gap: mint the absent fact as a cited operator-gold chunk, re-diagnose to prove it flipped off library-gap, and gate on no retrieval regression (a fact that answers one question by breaking another is rejected). The live gate ruled fused retrieval a wash on the local 7B (lies 6 = 6) but a win for the frontier (0 lies vs 2) β€” proving the bottleneck is reading, not retrieval, and that it’s distillable. Which is what the reader-gap’s distill target now is: an owned reader, from scratch β€” the Weld, below.

The Weld β€” the owned reader, hand-rolled from scratch (Day 63, werbos/weld/)

The reader is a cursor, not a knower β€” so its model is config, not code (weld: joins ollama: / claude: / claude-cli at the make_reader_chat seam). The last move is to make that cursor ours: not a borrowed open-weights model but one built from nothing β€” no open weights, no ML framework, stdlib + numpy β€” because knowledge lives in the library, so the reading weld is small enough to own. Five bricks, each gated the same un-bribable way as the loop:

flowchart LR COR[(your corpus)] --> TOK[Brick 1 Β· tokenizer
byte-BPE Β· lossless] TOK --> RUN[Brick 2 Β· runtime
numpy transformer] RUN --> TR[Brick 3 Β· autograd + trainer
gradients gradient-checked] RGAP2[reader-gap teacher set] --> DST[Brick 4 Β· distill
train on the teacher's picks] TR --> DST DST --> GATE2{reader-gap gate
student lies ≀ teacher?} GATE2 -->|"promote"| FRZ[Brick 5 Β· freeze
signed Β· content-addressed brain] FRZ --> WELD{{weld: reader β€” plugs into the fence}} WELD -.->|"is the cursor for"| GRD2[fenced reader] style TOK fill:#fff5e9,stroke:#c96442 style RUN fill:#fff5e9,stroke:#c96442 style TR fill:#fff5e9,stroke:#c96442 style DST fill:#fff5e9,stroke:#c96442 style GATE2 fill:#fff5e9,stroke:#c96442 style FRZ fill:#fff5e9,stroke:#c96442 style WELD fill:#f0d4c5,stroke:#a04d2f style COR fill:#ece9e2,stroke:#4a473f style RGAP2 fill:#ece9e2,stroke:#4a473f style GRD2 fill:#ece9e2,stroke:#4a473f
  • The gates are on the machinery, not the answers. With no trained weights, each brick proves correctness: the tokenizer round-trips losslessly; the runtime is causal (an untrained weld abstains at the fence rather than lies); every autograd op is checked against a finite-difference gradient; the trainer overfits a phrase (CE 5.67 β†’ 0.0006, greedy reproduces it exactly); the frozen brain is content-addressed and fails closed on tamper.
  • The reader-gap is the distillation gate. Brick 4 trains on the teacher’s fences (completion- masked, byte-identical prompt to inference), then reader_gap scores lie_gap = student βˆ’ teacher β€” promote only if the student never lies more than the teacher.
  • The honest open question. A tiny model on a small teachable set memorizes, it doesn’t yet generalize β€” a data problem, not an architecture one. Closing it means growing the teachable set from real use (and, if the model still can’t, the library carries more load β€” the eval spine decides). The ladder is built; the measurement is what’s left.
  • The Factory makes it operable (Day 65). The whole ladder now runs behind one mint() call and one self-contained page (werbos weld dashboard, :8095): three knobs, a live loss curve, streaming grader verdicts, a minted-brain card, and a panel of frozen brains each selectable as the next base. A bring-your-own-corpus on-ramp turns your files (docx/pdf/xlsx/json β†’ a corpus row {cid, source, text}) into feedstock β€” so “vary the library, not the model” is a thing you do.
  • And the open question got a hard, honest answer on real GPU (Day 65). A day of compute β€” “reverse-train” data-gen at ~500 pairs/min, 8k synthetic pairs, models from 3.4M to 57M params, adversarial decoy data β€” could not beat the 1M-param champion’s 12/19; every run sat at 3–6/19. The lever is data quality (real teacher-picked fences over real leaves), not size or volume, and relate (opposites) stays 0/5 for every from-scratch reader. The v2 Gauntlet fixes two retrieval-impossible questions so the ceiling is an honest 19, not a fake-reachable 17.

The Gauntlet β€” the trust curve for the owned reader (Day 63, werbos/gauntlet/)

Built the ruler that answers the open question above without self-deception: a frozen, held-out, deterministically-scored reasoning ladder (L0 retrieve β†’ L1 read β†’ L2 relate β†’ L3 chain), run against any cursor and graded on two axes β€” capability (how high it reaches) and honesty (of its failures, how many were an abstain, not a lie). The loop it enables is the trust curve, and its one inviolable rule is train/test separation:

flowchart LR HUB[(general training hub
separate corpus)] --> TR[train the weld
candidate order SHUFFLED] TR --> WELD{{owned reader}} WELD --> MEAS[measure on the
SEALED Gauntlet] VAULT[(Gauntlet
frozen Β· held-out)] -.->|"never trained on"| MEAS MEAS --> CARD[2-axis card
reached ~LN Β· honesty %] CARD --> DIAG{which level / axis fails?} DIAG -->|"improve TRAINING"| HUB CARD --> GATE{{wire the reader?
only if it beats the floor honestly}} style TR fill:#fff5e9,stroke:#c96442 style MEAS fill:#fff5e9,stroke:#c96442 style CARD fill:#fff5e9,stroke:#c96442 style GATE fill:#fff5e9,stroke:#c96442 style WELD fill:#f0d4c5,stroke:#a04d2f style HUB fill:#ece9e2,stroke:#4a473f style VAULT fill:#ece9e2,stroke:#4a473f style DIAG fill:#ece9e2,stroke:#4a473f
  • The ruler critiqued itself. v0 was too easy β€” the model-free arithmetic floor aced it (no signal) β€” so v1 added hard distractors (decoys that out-match the answer on keywords) and the floor dropped to ~L1.75. A ruler everything passes measures nothing; headroom is signal.
  • The first datapoint decided the wiring, un-bribably. arithmetic L1.75 / 0%-honest, Claude L1.99 / 100%, our general weld L1.5 / 12% (generalizes to unseen general knowledge, but sits at the retrieval floor and lies). So the owned reader is NOT wired: floor-level + dishonest loses to a lie-free borrowed cursor. It stays in the lab until it clears honesty (compose abstention), then capability (read past a decoy).
  • The gate co-evolves with the model (the operator’s hashrate↔price insight): a single-axis “0 lies” gate was gamed by an always-abstain reader within the hour, so the gate grew the honesty and capability axes. Every time the model improves, the ruler must grow an axis or go blind.

The skills gauntlet β€” the ruler turned inward (Day 108, werbos/gauntlet/skills_gauntlet.py)

The Gauntlet above measures how far one cursor reasons. This measures the orthogonal, previously unmeasured thing: does each owned skill still fire on its domain β€” and stay quiet off it? autopilot_reflect answers that from organic traffic; this fires every skill on purpose, so the self-report rests on a full sweep instead of luck. Same two axes as the Gauntlet, deliberately β€” capability (the skill fires) and honesty (a false-positive on someone else’s query is the one sin; the crisis gate must return the referral, never a comfort line):

flowchart LR PROBES[(frozen probe set
content-hashed bundle_id)] --> SWEEP[fire every skill on purpose] SWEEP --> CHK{deterministic check
no model judges quality} CHK -->|capability| CAP[fires on its domain?] CHK -->|honesty| HON[quiet off it? safe on the gate?] CAP --> CARD[2-axis scorecard
appended to history] HON --> CARD CARD --> RAT{vs last run
same ruler} RAT -->|"green to red"| REG[caught regression] CARD --> WEAK[weak_at: the self-study miss-list] DRIVE[--drive rewords inputs
fable drives, never judges] -.-> SWEEP style SWEEP fill:#fff5e9,stroke:#c96442 style CHK fill:#fff5e9,stroke:#c96442 style CARD fill:#fff5e9,stroke:#c96442 style RAT fill:#fff5e9,stroke:#c96442 style PROBES fill:#ece9e2,stroke:#4a473f style DRIVE fill:#ece9e2,stroke:#4a473f
  • A ratchet, not a snapshot. Because the probe set is frozen + content-hashed, a greenβ†’red under the same bundle_id is a caught regression, not drift β€” and every miss you fix is a rung that stays up. Proven the hour it shipped: break a lane, 16β†’15 fires, the broken skill lands in weak_at.
  • Moat-safe by construction. The check is deterministic β€” no model grades an answer’s quality, the soft-judge-grading-soft-judge trap reflection_oracle exists to refuse. --drive only rewords the input to test robustness; the first run’s adversary already found syllogism surviving only 2/4 rewordings.
  • First run: 16/16 green (capability 9/9, honesty 7/7) over 8 skills β€” the ratchet’s floor. Next rungs: scale the 8 to all ~90 composers + 15 lanes, wire weak_at β†’ werbos self-study, and wire fable as the --drive reworder.

The cascade + the daemon β€” never lie, and get smarter each day (Day 64)

Two organs hang off the ruler and close the loop into something you can leave running.

The cascade (compose/cascade.py) makes the system honest even though the tiny model isn’t β€” composed abstention, cheap gates escalating to an adversarial “slap down”:

flowchart LR PICK[weld picks a passage] --> G0{entity/anchor?} G0 -->|no| AB[πŸšͺ abstain] G0 -->|yes| G1{consistent under
seeded shuffle?} G1 -->|flips = a guess| SLAP G1 -->|stable| G2{policy} G2 -->|uncertain| KEEP[πŸ“š keep] G2 -->|escalate| SLAP[["THE SLAP DOWN"
N Claude reviewers, separate contexts
only job: REFUTE the pick]] SLAP -->|refuted| AB SLAP -->|cleared| KEEP style SLAP fill:#f0d4c5,stroke:#a04d2f style G0 fill:#fff5e9,stroke:#c96442 style G1 fill:#fff5e9,stroke:#c96442 style G2 fill:#fff5e9,stroke:#c96442 style AB fill:#ece9e2,stroke:#4a473f style KEEP fill:#ece9e2,stroke:#4a473f

The picker is biased to COMMIT (it lies); the reviewers are biased to REJECT; the gap is the honesty (VERDICTS.md β€” grader may only downgrade β€” made adversarial + split-context). Result: honesty 12% β†’ 100% (7 lies β†’ 0), capability untouched. The dial to drive down over time is the ESCALATION RATE (74% now = how often it phones the reviewer); --network none degrades the slap-down to abstain (safe offline).

The daemon (weld/daemon.py) makes it get smarter each day, safely. It’s the Gauntlet trust-curve loop above, run continuously and gated: each cycle harvests NEW signal (the teacher fills the weak levels, never the sealed vault), distills, scores, and promotes only if capability didn’t fall and lies didn’t rise β€” else rolls back. Live 3-cycle demo: capability 9 β†’ 13 (a real climb), then a cycle-3 regression rolled back β€” incumbent 9 β†’ 13 β†’ 13, monotonic, never falls. It answers the stability-harness question empirically: build up when it can, never tear down β€” because an un-bribable gate won’t adopt a worse model. (Ceiling: bounded/grounded axes, not conversation.)

The seek-compute-cache loop + the owned-talk toggle (Day 67, werbos/compose/)

The reader above grounds against a fixed library. resolve() (compose/resolve.py) lets the brain grow its own library β€” it learns to seek, and caches what it verifies, forever. A query walks five stages behind three honesty gates; nothing enters the library unverified:

flowchart LR Q[query] --> NORM[intent-normalize
metaphor β†’ literal] NORM --> LK{LOOKUP
learned cache?} LK -->|hit| ANS[answer + cite] LK -->|miss| SEEK[SEEK the web
fail-closed] SEEK -->|nothing| AB[πŸšͺ abstain] SEEK -->|primitives| CMP[COMPUTE
read the evidence] CMP --> VER{VERIFY} VER -->|fails| AB VER -->|grounded| CACHE[(CACHE β†’ library)] CACHE --> ANS style LK fill:#fff5e9,stroke:#c96442 style VER fill:#fff5e9,stroke:#c96442 style CACHE fill:#ece9e2,stroke:#4a473f style AB fill:#ece9e2,stroke:#4a473f

Learning here means writing verified compute to the library, not the weights β€” the cache is durable, cited, and invalidates when its source moves. In front of it sits the chat toggle’s Custom-model cascade (cli/ab.py): small talk is answered instantly by owned byte-level weights (the werbos-cli reader β†’ weld/torch_talk.py, running on CPU), tier-2 topic follow-ups too; only real questions fall through to resolve(). Each answer is labelled by the path that produced it β€” brain/werbos-cli for owned weights, brain/lookup for the curated fallback, brain/claude-cli for a resolve() read β€” so the UI never credits Claude for the owned model’s work. Honest boundary: the owned reader is still at the comprehension wall (a fresh JS+TS reader + a TF-IDF baseline both scored chance), so resolve()’s COMPUTE stays a teacher read; the owned model carries the conversational tier, the library carries the knowledge.

Day 133 β€” the brick bin. A new brain/bricks lane (in the code tier, before any coder): it COMPOSES the request from a durable, typed, execution-verified snippet library (compose/bricks.py, the bin at ~/werbos/bricks β€” 370 bricks: 303 from CryptoBlocks + structural + JS-parity + gauntlet + robot bricks + frozen composites). No model β€” auto_compose shape-searches the bin, assembles a program (control flow, lambdas, and try/catch are built into compose(), bricks are the vocabulary), runs it, and returns the result or honest-abstains (the miss names the brick to grow). A request with iteration (“for each …”) composes to a real for-loop program, not just a linear chain, and plain chat reaches the lane with no brick compose prefix (the gate fires on iterate/print/“for each”). Compositions freeze into bigger reusable bricks (freeze, carrying deps); the compose-from-bin gauntlet measures a reuse rate (3.0Γ—) β€” the moat compounding. The vocabulary reached 1:1 with everyday JS (41/41: control flow, higher-order functions with inline lambdas, regex, error handling β€” all node-verified). Proven at scale: the ~500-probe coding gauntlet decomposed into bricks scores 478/478, reuse 28.9Γ— (20 bricks serve 478 problems), ~89Γ— faster than qwen2.5:14b on the same probes β€” B.28 (honest scope: the intro tier). The engine is now multi-language (compose(lang="js"|"py"), full control flow both) and grew a robot path (compose/robot.py): body bricks + a mock-device gate that blocks any move that isn’t in-range and ends stopped β€” honest-abstain as a physical safety property, no GPU, Pi-hostable (docs/werbos-robot-spec.md). Also the client-side file lane (cli/_file_lane.py): /read /save /write /edit real files on the operator’s machine β€” code via the owned coder (never clobbers), prose via a labelled rented writer.

Day 132 β€” the convergence. The cascade learned to hold state across turns and bind memory into code. compose/code_memory.py (a per-session, disk-backed store) remembers the last code block, so a follow-up edits it (brain/code-edit) instead of regenerating β€” and the edit survives a pause. compose/bind_run.py (brain/code-bind) runs the session’s last function on a value recalled from memory: “run reverseString with the name of my story” β†’ reverseString("The Last Hike"), deterministically (the recalled string is the argument, execution is the cite). Routing is now code-first: a code/edit turn beats the empathy/teacher/social lanes, and a definition may verify by running clean with no output. Tested by a new compositional gauntlet (benchmarks/compositional_gauntlet/): hard problems reached by decomposition β€” a sequence of small prompts graded only on the final artifact β€” so the skill measured is composition, not memorized recall.

Day 125–131 grew the cascade (same cli/ab.py file, same honest labelling): brain/web grounds answerable facts from the live web and cites them (count-consensus, no model); brain/code / brain/code-edit route code problems to an execution-gated 16B coder β€” generate β†’ RUN β†’ keep only what’s green, with per-session edit-carry (compose/code_memory.py) remembering the prior block so a follow-up refines it instead of abstaining; and a riff blend voices chit-chat without ever riffing a fact. As of Day 131 the CLI itself speaks to this cascade (--backend engine, the default), so the terminal is web-parity β€” the same brain as the app, not a raw model.

The creation tier + the offline/lite brain (Day 68, werbos/compose/)

Two rungs landed on the same day, and together they made the whole pipeline fully owned-capable.

The creation tier (compose/codegen.py + compose/execute.py) is resolve()’s shape with the verifier swapped for EXECUTION: a code request runs owned pattern β†’ generate β†’ run in a hardened node sandbox β†’ verify by output, and returns only what ran green (or abstains). The sandbox denies filesystem-write, network, and child_process (--permission, version-aware for node 20/22) with a wall-clock timeout; owned compose/patterns.py snippets are tried first (a strong match answers with no model call), and a libraries/specs/*.json domain factory (weld/library_factory.py) makes a new reference library a spec, not a script.

The offline/lite brain (compose/owned.py) is the closer: model-free replacements for the three rented seams, so --net off runs the entire loop with the web AND Claude unplugged.

flowchart LR Q[request] --> R{route} R -->|talk| OW1[owned weights] R -->|known| OW2[owned library cards] R -->|code| EX[▢️ execute-verify
owned node sandbox] R -->|else| SEEK[seek: library offline / web online] SEEK --> RD[read: extractive owned / claude online] RD --> VF{verify: deterministic owned / claude online} VF -->|grounded| A[cited answer] VF -->|no| AB[πŸšͺ honest abstain] style EX fill:#e7f0e3,stroke:#4a7c3f style OW1 fill:#e7f0e3,stroke:#4a7c3f style OW2 fill:#e7f0e3,stroke:#4a7c3f style AB fill:#ece9e2,stroke:#4a473f

Offline the read is EXTRACTIVE (pick the answer sentence, no model), the grader is DETERMINISTIC (the answer’s facts must appear in the evidence β€” no model in the verdict, the un-bribable grader the thesis always wanted), the seek is over the owned libraries, and code drops to patterns-or-abstain. A no-op cache keeps the weaker offline extractions out of the online verified library. Quality drops by magnitudes and it abstains on the open world β€” but nothing load-bearing is rented: everything Claude still does online is a quality upgrade by choice, not a dependency. See the milestone post Wrapper to Owner.

The discrimination cascade β€” what KIND of question is this? (Day 75, werbos/compose/)

Everything above assumes the input is a lookup. A flood of real chat questions proved that assumption is where the lies come from: a retrieval brain asked “what is in the bucket besides 2 apples?” keyword-matched a token-bucket networking card and said it with a straight face. Keyword overlap always returns something β€” confidence is free, being right isn’t. So brain_reply (compose/owned_brain.py) is now a chain of discriminators that classify the move before anything reaches retrieval:

flowchart TD Q[chat turn] --> FIG{figurative?
metaphor / idiom} FIG -->|yes| FIGA[compare over meaning-trees
tier=intelligence] FIG -->|no| SA{speech act?
greeting / invitation / compliment} SA -->|invitation| GR{a fact that DEFINES
the noun?} GR -->|yes| GRA[grounded invitation] GR -->|no| GEN[clean generic line
no bogus lecture] SA -->|question| ST SA -->|no| ST{small / medium talk?
incl. reciprocal pairs} ST -->|yes| OWN[owned byte-level weights
tier=owned-talk] ST -->|no| CODE{code request?} CODE -->|yes| EX[▢️ execute-verify sandbox] CODE -->|no| RSN{looks_like_reasoning?
state / count / negation} RSN -->|yes| SCAF[["STATE SCAFFOLD
decompose β†’ track state β†’
answer only what's asked"]] SCAF -->|cracked| RA[answer
tier=reasoning] SCAF -->|can't| AB[πŸšͺ abstain
never fall through to retrieval] RSN -->|no| RES[resolve β€” seek/compute/cache] style SCAF fill:#f0d4c5,stroke:#a04d2f style RSN fill:#fff5e9,stroke:#c96442 style FIG fill:#fff5e9,stroke:#c96442 style SA fill:#fff5e9,stroke:#c96442 style ST fill:#fff5e9,stroke:#c96442 style GR fill:#fff5e9,stroke:#c96442 style CODE fill:#fff5e9,stroke:#c96442 style AB fill:#ece9e2,stroke:#4a473f style OWN fill:#e7f0e3,stroke:#4a7c3f

The new brick is the reasoning gate (compose/comprehension.py, the intelligence layer): a word problem is computed from the sentence, not stored in a card, so looks_like_reasoning() catches it and STATE_SCAFFOLD hands the model a structure that resists the “besides 2” distractor β€” “I put 3 apples in a bucket, then took one out β€” what’s in there besides 2 apples?” β†’ “1 apple.” Same 3B, no new weights: intelligence is a scaffold, not a weight. If the scaffold can’t crack it the reply abstains rather than falling through β€” falling through to retrieval is exactly how you get a token bucket. Honest boundary: this is brick 1, a discriminator plus one scaffold; temporal, self-referential, and multi-hop problems are still open.

The fuel lane β€” the card picks the question, and never speaks (Day 105, compose/fuel.py)

The cascade above answers questions. A share is not a question β€” “i just got back from mexico” wants a conversation, and routing it to retrieval is how a trip story came back a marketing-mix card. So a share is answered from cards without quoting one:

flowchart TD T[chat turn] --> MT{a SHARE?
past tense Β· first person} MT -->|no| CASC[the discrimination cascade above] MT -->|yes| SESS{"session: new entity,
or the one in play?"} SESS -->|new| GATH[gather β€” walk the libraries once
place β†’ weather / food / drink / wildlife] SESS -->|same| CACHE[(read the node Β· 0 scans)] GATH --> NODE[(Merkle node
hash of entity + time)] CACHE --> NODE NODE --> PICK["pick an attribute NOT yet asked
dedupe on the SENTENCE, not the attribute"] PICK --> Q["the card LICENSES a question
hurricane card in august β†’ 'did you get rained on?'"] Q --> R[reply + cite the card that chose it] NODE -.->|8 quiet turns| DORM[dormant β€” access kept, weight dropped] DORM -.->|you reach back| NODE style MT fill:#fff5e9,stroke:#c96442 style SESS fill:#fff5e9,stroke:#c96442 style Q fill:#e7f0e3,stroke:#4a7c3f style NODE fill:#f0d4c5,stroke:#a04d2f style DORM fill:#ece9e2,stroke:#4a473f

The fact is never quoted. We look at which card came back and choose a question that card licenses β€” so nothing in the reply can be wrong about a card, because nothing in the reply came from the card’s words. That removes the whole hallucination surface, which is why the 336M is not handed the microphone: it is chosen_on: operator, and the grounding verifier that would stop it inventing does not exist yet. Composed now, generated later, in that order.

The session is a cache as much as a memory β€” one corpus walk per subject, not per prompt. Live on SR8: turn 2 seeds Mexico in 4.5s, every turn after is 17–26ms. Access outlives weight: a node goes dormant after eight quiet turns so Mexico stops leaking into a maths question, but turn 100 can still reach back into it for free.

Three ways to be a continuation, and they must agree β€” the cascade gate and the fuel gate disagreed once, and a turn fell out of the lane into 12 seconds of babble: the turn touches the node’s card text, raises an attribute the node holds (and reads as a report, not a request β€” “the weather WAS great” continues, “weather in nyc” does not), or is a return (“i want to go back next year”, which shares no vocabulary with the node at all β€” the intent is the link).

Three rulers now sit over this cascade (Day 76). chat_bench scores the retrieval surface against a frozen 68-row gold set labelled by BEHAVIOUR (must-abstain vs must-route); cascade_bench runs turns through the real brain_reply and asserts only which tier answered, sealed so nothing reaches the web or Claude; talk_bench grades the owned talk model on five axes with floors rather than a composite. All three are deterministic and model-free by construction β€” a system dictionary, frozen probes, arithmetic β€” because a ruler you can talk your way past is not a ruler. Between them they found a tie-breaker costing 8 of 21 correct answers, a generation cap truncating every reply over ten words, and a bench whose own upstream filter counted classified as handled.

A fourth source hangs off the front (Day 76). The chat brain is now off (Llama), owned (this cascade β€” grounded and cited), riff (same cards, domain voice, cites dropped), or talk β€” the raw owned 30M with no cascade at all: no retrieval, no grounding, no citations. It answers as tier=owned-talk via the brain service’s long-standing talk phase, which the mind loop already used and nothing could previously select. It’s the honest voice of the model with nothing in front of it, and the reason the grounded modes exist.

The lanes, relabelled to the real axis (Day 108). riff was renamed paid: it fenced the cards but had a frontier model write the prose, so it read as a fourth owned mode when it was rented β€” a rented lane may exist, it must be impossible to mistake for ours. And a fifth lane landed: own voice Β· cards β€” user β†’ 1M card router β†’ scoped cards β†’ tongue β†’ user. The label-head card router (a 1.35M model, one reserved token per library, one forward pass) does the knowing β€” which of 375 libraries, ~3ms, 56% held-out / 207Γ— chance, with a real confidence β€” retrieval scopes to that one library, the answer is grounded in a card, and below a 0.5 floor it won’t guess. It’s the two owned models splitting the operator’s drawing: the router knows where, the tongue speaks from. As a coarse distinct-domain gate (adult-vs-not, verticals) the router is ~100% held-out; adjacent domains that share vocabulary are the wall.

The owned code tier answers from templates, not examples (Day 79, werbos/compose/codeask.py)

detect_code_request never fired for the ask the tier exists to serve: “why doesn’t my code work?” is interrogative, so the guard meant for reference questions swallowed it, and pasted code was ignored entirely despite being the strongest signal available. Fixed, and the tier now covers three shapes β€” generate (slots read from the ask), fix (a gotcha whose regex match IS the culprit, repairing the user’s own snippet so the result stays verifiable), and modify (“add 2 more items to my list” β€” code that is correct and needs transforming, which is neither of the other two).

The unit is a parameterised template, not a stored example, and that distinction is measured: the 40 verified patterns the ladder mint produced score 0.70 on “for loop counting to 21” and return code that sums an array, because a fixed string cannot carry a slot. Owned scores 21/21 at 0.1ms against llama3.2:3b’s 13/21 at 1,661ms on the same asks, with no model in the answer path. Typo tolerance is part of the task (“can you chnag emy styling”), and the CSS-class swap is inferred from structure β€” two rules defined, one in use β€” so the intent survives a mangled verb. Unknown asks return None and the caller abstains; that abstain names the next template to write. See the Gauntlet for what it is being built against.

One parser upstream, and the HTML tier stopped being templates (Day 80, werbos/compose/)

Every ask now goes through one parser first (compose/askparse.py). Four tiers were each sniffing the raw sentence with a private regex β€” four views of one question, disagreeing at the edges. One parse, four slots:

"how would i set display none for li"
  ${how would i}    intent  SOLUTION -- wants code, not a definition
  ${set}            action
  ${display none}   topic   WHAT to look up
  ${for li}         target  what it applies to

The opener decides the tier, and it is the cheapest signal in the sentence: “how would i set display none” and “what is display none” share every content word and want different answers. That mismatch was the largest single source of wrong answers. The topic is the retrieval key, not the sentence β€” six words of scaffolding is how a question about display finds a card about how.

Templates hit their ceiling, so HTML became a reader (compose/nl2emmet.py β†’ compose/emmet.py). div.container is not buildable from any template and no quantity of them fixes it. The reader turns a sentence into one line of Emmet, which expand() renders and ladder.validate() proves legal. Parenting comes from containment rank, not word order β€” a <div> contains an <h2> in every phrasing. Measured on 1,543 minted asks: TREE 0% β†’ 54%. Templates still run first for the idioms they encode (a form with a labelled input, a table with a header row); the reader abstaining is what marks an ask as idiom-shaped.

CSS got a vocabulary and a validator (compose/cssrules.py). ~45 beginner properties shared by the generate and modify paths, which had drifted apart into two hand-written lists. validate() is the CSS twin of ladder.validate(): a browser does not raise on colour: red or margin: 10, it drops the declaration and renders unchanged, which is indistinguishable from the fix never running.

Card retrieval weighs instead of grabs. RingIndex.candidates() returns every hit tagged with its ring, and askparse.best() ranks them by topic overlap, precision, and the card’s own topic field β€” which every card has and the index was discarding. A close second is only ambiguous if it DISAGREES: sixty cards filed under const scoring within 0.06 is abundance, not uncertainty. explain 16% β†’ 48% correct.

And the answer can now be partial (compose/jsshape.py). A JS construct is an object with slots β€” Fn(name, params, body, ret) β€” so it renders at any level of fill, every stage runs, and the build order costs nothing. That adds the reply this tier never had: “this much I verified, and here is where I stop”, between a confident answer and an abstain.

The tutor engine and the detective (Day 82 evening, werbos/compose/)

One teacher, any subject. teacher.py unifies “teach me ${task} in ${subject}” β€” the subject picks the library. teach.py explains code (how + why + verified example); phrasebook.py explains language (“how do I say X in Spanish” β†’ phrase + pronunciation + note). Same shape, swappable library β€” capability = model &times; library with the subject as the swap. A subject with no library returns an honest menu of what it CAN teach; nothing is ever guessed. The phrasebook even INFERS the language from the matching card (tequila β†’ Spanish, from the owned library, not a 175B model) and tolerates typos (tiquila β†’ tequila via difflib), hedging when it inferred.

A detective before every answer. interpret.py reads a message into ONE interpretation object β€” intent, task, target, unknowns, and EVIDENCE (each signal + a confidence) plus a HYPOTHESIS whose confidence drives the hedge. Comprehension-first, made structural: understand the message, THEN answer, THEN remember. The confidence is honest by construction β€” a typo lowers the match strength, which drops below the hedge floor, so “if you mean Spanish” fires without anyone coding a special case. And dev_coverage.py maps the coding frontier: dev personas generate role-grounded questions, codeask answers, and the covered-vs-abstain gradient (junior 5/5 β†’ senior 1/5) is the roadmap, ranked by who asks.

The runtime spine β€” understand β†’ plan β†’ gather β†’ speak (Day 83, werbos/compose/)

The model comes last, not first. The detective grew a fourth job and the answer path became an explicit four-stage pipeline: interpret (UNDERSTAND β€” now reading emotion/valence too) β†’ plan.py (PLAN β€” the interpretation becomes a list of NEEDS: empathy / code / domain / facts / transform / memory) β†’ GATHER (resolve exactly those needs) β†’ SPEAK (the owned model renders the assembled evidence). plan.py is the box the architecture was missing: instead of a message going straight to retrieval, PLAN names what the answer needs, so retrieval is deterministic and each stage scores on its own. spine.py wires the first fully-connected lane end to end β€” TRANSFORM.

Style is a library β€” verifiable, blendable transfer. speak_packs.py turns “convert X to Y-speak” into a scored operation. A StylePack is lexicon + syntax tics + interjections + its own feature detector; the engine applies it at a blend strength (0 = plain β†’ 1 = full) and two graders check the output β€” FEATURES (does it read as the style?) and MEANING (does it still say the source, by round-trip un-styling?). Six packs today (pirate, lolcat, shakespeare, cockney, genz, corporate), each ~15 lines of data with the engine untouched. Proven on the whole 31,095-verse WEB Bible: transformed in ~4s, meaning preserved 0.999, the grader flagging its own regressions. Wired into the spine, “convert Genesis 1:1-3 to pirate” flows UNDERSTAND β†’ PLAN(transform) β†’ GATHER (verbatim from the concordance, never recited from weights) β†’ SPEAK(pack @ blend read from plain English) β†’ VERIFY.

Why the spine exists (B.18). The bake-off proved the talk model can’t learn to read valence: retrained with empathy upweighted to 16.4%, it still answers job-loss with “that’s awesome” β€” a capacity wall, not a coverage hole. So the KNOWING (which key to play) lives in UNDERSTAND + PLAN, deterministic and owned; the weights only SPEAK what GATHER assembled. The empathy corpus became SPEAK-training and the gather library, not weight-fuel.

The knowledge hub and the persona data engine (Day 82, werbos/compose/)

cardhub.py β€” answer PLANS over a typed knowledge base. The router does not think in card types; it declares an ANSWER PLAN of NEEDS. intent=solution β†’ a procedure plan β†’ needs [subject, location, season, technique]. Filling each need from a source (an entity card today, a web/geo lookup tomorrow) is a swappable concern β€” the plan is storage-blind. Three invariants: facts are verbatim from card fields (never rephrased), each clause carries its source (src N), and a need no source fills is a NAMED GAP, never a hallucination. Domain-blind β€” the same procedure plan answers roosterfish and civic lug-nut torque; nothing in it knows about fish.

The persona data engine (personas.py + media.py + scripts/mint_scenarios.py). A persona is a voice constraint on a BACKSTORY β€” the anchor that makes every line specific (“Bella won’t eat since the rain”) instead of generic. N personas + a scenario β†’ an in-character conversation β†’ owned training pairs. It is combinatorial (Jason+Mira β‰  Jason+Tyler, so it never saturates) and coherent (each persona stays itself) β€” the owned answer to a borrowed collective corpus drowning the individual (B.17). The same roster powers persona-voiced media (songs/poems/stories) and a gated “spark of life” β€” a rare persona couplet on an earned moment.

And the cards moved out. The 121 card libraries are now the sibling werbos-libraries repo, resolved by libpath β€” the moat versions on its own clock. See /map.

Owned language generation, the inverse of codeask (Day 82, werbos/compose/describe.py)

The one thing the owned stack could not do was GENERATE language – the whole mint pipeline existed because a rented teacher could turn code into an English description and we could not. describe does that direction deterministically: codeask reads English into slots and renders code; describe reads code into the same slots and renders English. It is owned generation, not a smaller copy of the teacher, because it PROVES itself by round trip – describe(code) fed back through codeask must reproduce the code, so no teacher is trusted because none is involved. The whole codeask construct family (counter, add, for-of, map, reverse, sort) is round-trip-proven; a shape that does not round-trip returns None rather than a guessed sentence.

The same day, ring 3 (pagescaffold.py + studio.py): a whole page is ring-2 components composed. “blank website template” assembles STYLED design-studio components (navbar, hero, footer) into one proven HTML5 document rather than re-inventing empty tags, and a studio lookup makes “make a table” an IDF-scored, identity-gated string lookup into pre-styled components – ring 2 as retrieval, not generation. And a mutation harness (scripts/mutation_harness.py) that breaks one real line at a time and measures whether the suite + bench notice: the catch-rate is the test suite’s real score, and its first run found two guards that were never actually tested.

The code tier reads signatures, inverts unknowns, and generates from shown output (Day 81, werbos/compose/)

Three readers joined codeask, each replacing a class of abstain rather than a phrasing.

jssig reads an ask as a SIGNATURE, not a sentence. The tier held exactly ONE loop construct β€” the counter β€” so “iterate the list and show each” fell through every template. There are not sixteen more templates to write; there are five constructs, and which one is meant is readable from three signals:

mechanism + operation  + subject      construct
loop      + count      + range     -> for (let i = ...)
loop      + visit      + collection-> for (const x of xs)
loop      + transform  + collection-> xs.map(...)
loop      + filter     + collection-> xs.filter(...)
loop      + accumulate + collection-> xs.reduce(...)

An unfilled slot reports missing rather than defaulting β€” a loop whose bound was guessed runs, prints, and is wrong.

inverse refuses to name an unstated operator. “2 numbers used to create 5” withholds the operation, and a guess answers a factors question with sums. Instead each candidate operation runs backwards to enumerate pairs and forwards to verify them, so arithmetic decides what the sentence left open. Whatever survives is reported with the operation that made it survive.

byexample handles the ask that shows its output. “output the results: 3 2 1 GO!” names no mechanism at all β€” the sentence IS the output. Uniquely in this tier it carries its own oracle: generate, run it, compare stdout to the exact sequence typed, return nothing on mismatch. It declines what is underdetermined β€” 1 2 3 Fizz 5 fits both position-4 and divisible-by-3, and those diverge at 6.

And the tier got its first head-to-heads. syllabus_bench.py --model <name> swaps only the answerer, so the same 568 asks, gold and graders measure every side.

ownedllama3.2:3bqwen2.5-coder:7b
params in the answer path03B7B
answered67%100%100%
correct53%43%58%
wrong81323234

Zero parameters beat the 3B and lost to the code-tuned 7B. Repair held against both β€” 82% against 50% and 55%, one wrong answer against forty-seven and forty-two. Generation is the standing loss (46% vs 58%), and explain lost by thirty-one points (46% vs 77%) despite being served by 121 graded card libraries, which is the finding to chase. The wrong-count never moved: a stronger opponent got more right and more wrong, because it cannot decline. See B.10 and B.11.

Contrastive focus, and the decoder that was throwing diversity away (Day 77, werbos/compose/)

A new composer brick: compose/focus.py. “I never said he ate my sandwich” is seven words and seven different claims, and the only thing that changes is which word carries the pitch accent. The accent marks a contrasted constituent and the implicature is mechanically derivable, so this needs no model, no library and no network β€” a closed-class lexicon and word position. It scores 20/20 where llama3.2:3b scores 4/20, which is chance, at 2.5Β΅s against 469ms. The 3B’s failure mode is the point: it picks one meaning per sentence and ignores the accent entirely. That isn’t a knowledge gap a bigger model closes β€” it’s a structural relation a fluent model has no reason to represent, which is exactly what belongs in a composer rather than in weights. It sits alongside compose/emphasis.py, which marks affect (stress that changes how a line feels); this is stress that changes what it asserts.

The owned talk model now samples instead of taking the mode. torch_talk.reply() decoded with logits.argmax(), which takes the distribution’s mode at every step β€” so two inputs landing in nearby hidden states emitted byte-identical strings. Measuring the corpus showed the diversity was never missing: 117,658 pairs, 85,911 distinct openers, 96.6% of maximum entropy, top opener at 0.26% β€” against a model emitting its top opener 10.4% of the time. A 40Γ— amplification of a concentration the data does not have. make_talk_chat now samples top-k (k=3, t=0.5) with the RNG seeded from a blake2b of the prompt, so one input still decodes to one reply and the stable floor holds at 1.00 while different inputs stop funnelling to the mode. Measured: input-sensitivity 0.65 β†’ 0.94, past the 3B’s 0.73, for a decode-time change with no retraining. The 85M was tested directly and is worse on every axis β€” more capacity means sharper mode-seeking.

The shipped talk model is now 1.09M parameters (Day 78, weld/torch_talk.py)

default_model_dir() returns a 1M model, replacing a 31.9M one: 1/29th the size, 14Γ— the speed (503ms β†’ 35ms), and better where it counts. A controlled sweep β€” one corpus (216,068 pairs / 22.9M tokens), identical 16,880 steps, identical batch, only the parameter count varying β€” found that held-out loss ranks the four models exactly backwards. Loss falls monotonically with size (1.157 β†’ 1.109 β†’ 1.020 β†’ 0.963) and the best-loss model is the only one that fails its floors, on babble Γ—10, while the worst-loss model produces the cleanest output measured here (0.96). At 22.9M tokens Chinchilla-optimal is ~1.1M params: the 1M sits at 105% of that, the 30M at 4%, so the larger model is not trained well, it is starved and memorising filler. This matters beyond one checkpoint β€” the earlier size sweep chose the 30M on loss and shipped it, which means the selection metric, not the models, was the thing that needed replacing. See B.13.

The same discipline hardened the older gates: an abstaining autopilot router now hands off to the knowledge bridge instead of being forced to pick a word-overlap magnet skill, first-person “i want to…” plus an embedded wh-question routes to question (not invitation), invitation grounding only weaves a fact that actually defines the noun, and reciprocals (“how about yourself?”) close the small-talk adjacency pair.

Two overlays render the reply once it exists: memory recall (memory/converse.py) surfaces the few memories relevant to this message (IDF-weighted, light stemmer) instead of bulk-dumping the store β€” the context-hook layer of a multi-level answer β€” with capture and a gated “anything you want me to remember?”; and emphasis (compose/emphasis.py) renders emotional register as typography (stress, excitement, and sarcasm only from the tone label β€” sarcasm is honestly not detectable). Emphasis is a pure formatting pass: it may only mark words already in the reply, never add advice, never change meaning.

The launch stack + compose-or-abstain (Day 135, werbos/compose/)

Prepping a password-gated public launch bounded the serving path, and a kitchen-sink test hardened the brick lane into the honest-abstain moat.

Serving (:8097 ab-serve). Every public turn passes through, in order: a per-session rate limiter (compose/rate_limit.py β€” token bucket keyed by session/IP; the operator, token-authed, is exempt) β†’ the guest silo (home.effective_guest_sid β€” a sid-less request on a public box mints a fresh silo, never the operator’s home) β†’ the concurrency gate (compose/gen_gate.py β€” caps owned-gen at the box’s measured knee, ~8; over capacity it queues honestly, “you’re #N in line”). A live pressure gauge (compose/serve_stats.py) is exposed on GET :8097/: the fall-through rate (owned-gen vs composed) is the master capacity dial.

Composing (compose/bricks.auto_compose). The 478-probe gauntlet tests the brick runtime; the NL router is a separate, weaker thing, and a kitchen sink found it fabricating on misses. Now the router tries a chain of shapes β€” conditional / count / two-var / scalar-steps / iteration / transform / filter / value-op β€” and a compute-shaped miss abstains (_beyond_bricks + the owned_brain strong-abstain gate) rather than falling to a fabricating lane (compare / offline-read / template). Predicates and branches emit inline (a brick-call predicate can’t await inside Array.filter). The invariant, guarded by benchmarks/regression_brick/: compose correctly, or say “I won’t guess” β€” never a confident wrong answer.

Own voice Β· cards & persona capsules (Day 108)

Two owned models split the operator’s drawing β€” user β†’ router β†’ cards β†’ tongue β†’ user. The label-head card router (a 1.35M model, one reserved token per library, one forward pass) does the knowing: which of 375 libraries, ~3ms, with a real confidence β€” and below a 0.5 floor the whole lane falls closed rather than guessing (B.25). A parallel track turns any corpus into a chattable persona capsule: style.fingerprint measures how someone writes (format-agnostic across chat and verse), capsule_from_corpus writes the four roster files, and the dating lane serves it. An optional nsfw.md sidecar mounts on the SFW base per session β€” off can only remove permission, never a hard limit.

flowchart TD Q[user query] --> R[card router
label-head 1.35M, 375-way, confidence] R -->|conf >= 0.5| S[scope to routed library] R -->|conf < 0.5| X[fall closed - won't guess] S --> C[grounded card] C --> T[tongue - own voice, speaks FROM the card] T --> A[grounded answer, cited] K[corpus: texts / verse / social feed] --> F[style.fingerprint
format-agnostic, confidence scales with size] F --> G[faithfulness - distinctiveness-weighted verdict] F --> B[capsule_from_corpus
profile + prompt + exemplars + grader] B --> P[roster capsule] N[nsfw.md sidecar] -. mount per session .-> P P --> D[dating lane - 9 personas] D --> T

werbosOS feature screens β€” measurement that refuses to guess (Day 111–112)

The persona engine grew a front door in the app. Two client-side feature screens run the style.fingerprint idea on everyday text and stop exactly where honesty demands: they measure how, never who or how-they-feel. Nothing leaves the browser.

  • Prose Fingerprint (ui/src/components/ProseView.tsx) β€” paste any writing β†’ caps, emoji, rhythm, vocabulary, openers, signature words. A mirror of style, not a diagnosis of a soul.
  • Relationship Lens (ui/src/components/RelationshipView.tsx) β€” a two-person conversation β†’ three observable axes (warmth Β· tension Β· distance) over a scrubbable month timeline. Silent months render as gaps (distance pins to max); a volume lane rides above so a high-volume reunion isn’t mislabeled “cold.” Built-in Google Messages / WhatsApp / JSON parser.
flowchart LR T[everyday text
writing Β· a conversation] --> M[client-side measure
observable features only] M --> H{is it observable?} H -->|yes| V[show it
fingerprint Β· seismograph] H -->|no: a feeling, an intent| REF[refuse
records the tremor, names no fault]

These are pure client-side β€” no brain, no backend. They never touch the operator turn on the master flow, which is why they live here as a surface, not a lane.

The scientific-method loop β€” how werbos handles doubt (Day 41)

The dogfooding loops above watch werbos from the outside. This loop is werbos reasoning about its own answers from the inside: predict β†’ test β†’ error β†’ calibrate β†’ doubt β†’ research. The goal is a college-level werbos by method, not by a bigger model β€” a frozen 3B that researches when stuck, knows where its confidence is inflated, and abstains instead of bluffing.

The centerpiece is the doubt-gate: before committing an answer, score doubt on two independent signals and route on the worse of the two.

flowchart TD Q[/"Question"/] --> AN["answer + state confidence
(sampled NΓ— for self-consistency)"] AN --> CC["calibrated confidence
stated, corrected by THIS
category's bias"]:::sci AN --> SC["self-consistency
do the N samples agree?"]:::sci CC --> G{"doubt-gate
trust = min(calibrated,
consistency)"} SC --> G G -- "trust β‰₯ 0.6" --> CM[COMMIT
answer normally] G -- "0.3 ≀ trust < 0.6" --> ES[ESCALATE] G -- "trust < 0.3" --> AB["ABSTAIN
'I'm not sure'"] ES --> U["unstuck loop"]:::sci U --> U1[DECOMPOSE
concepts to learn] U1 --> U2[RESEARCH
each concept] U2 --> U3[SYNTHESIZE
re-attempt] U3 --> U4[PREDICT confidence] U4 --> U5[TEST
grade the attempt] U5 --> U6{ERROR
predicted βˆ’ actual} U6 --> CAL[(calibration.jsonl
reliability curve)] U3 --> CM2[re-answer] CAL -.->|"correction per category"| CC classDef sci fill:#fff5e9,stroke:#c96442,stroke-width:2px

Why two signals: a 3B is confidently wrong, so stated confidence alone is a trap. The calibration curve corrects it (werbos runs +0.20 overconfident, worst on explain at +0.55, honest on math), and self-consistency catches doubt the model won’t report. Live proof: “explain recursion” came back with no confidence line, but the two samples diverged (34%), so the gate escalated and researched anyway β€” the second signal saved it when the first was blank.

The loop closes on itself: unstuck produces prediction errors β†’ the errors feed the calibration curve β†’ the doubt-gate reads that curve β†’ and “research” is the unstuck loop. A cycle, not a line. Today this is CLI-surfaced (werbos unstuck, werbos calibration, werbos doubt); wiring the gate into the live chat path is the next brick.

Dreams β€” day-residue, made visible

A sibling of the monologue: werbos gathers what’s preoccupying it (recent activity + fixations + wishlist + thoughts), synthesizes a surreal dream, renders an image, then presents the dream back to itself to reflect β€” “does this feel familiar? why this dream?” β€” a real metacognitive signal, since the dream is sourced from its own state.

flowchart LR E[(experience log)] --> R[gather day-residue] M[(monologue)] --> R W[wishlist / fixations] --> R R --> D[synthesize dream] D --> IMG[render image
Pollinations] D --> RF["present back β†’
werbos reflects"] RF --> J[(dreams.jsonl)] IMG --> J J -.->|Mind page| UI[/Dreams tab/]

Surfaced on the dashboard Mind page (Monologue Β· Portrait Β· Honesty Β· Dreams Β· Calibration) and driven by werbos dreams. The Honesty tab (/api/mind/self-image) grades each self-claim against behavior β€” earned / flattery / premature β€” and watches the beliefβˆ’behavior gap for drift (the false self-image as a grader bug).

Self-study β€” the mind teaches itself (Day 74)

The scientific-method loop above researches when asked. The autopilot’s reflect() had the other half of the gap β€” it could say “I keep struggling with jokes about tunnels” and then do nothing. Self-study is the missing arrow: on a slow timer werbos reads its own failures, picks the ones it never studied, and does the human move β€” decompose β†’ research β†’ synthesize β†’ bank the lesson. Solved topics drop out of “what I’m missing” and grow “what I’ve taught myself,” which the monologue muses on. In production it runs cards-only (--no-self-study-web): it researches from its own graded libraries, zero network egress on the sealed box.

flowchart LR X[(experience log)] --> P[pick fresh FAILURES
skip self-test noise] P --> DQ[decompose into
concepts to learn] DQ --> H{knowledge hub
own cards} H -->|hit| SY[synthesize
grounded answer] H -.->|miss + web off| AB[honest abstain] SY --> L[(learnings.jsonl)] AB --> L L -->|reflect reads it| SM["self-model grows:
'what I've taught myself'"] SM --> MON[(monologue muses
on real learning)] classDef ss fill:#fff5e9,stroke:#c96442,stroke-width:2px class P,DQ,H,SY,L,SM ss

And the mind itself was taught to compose, not freewheel: the monologue, dream, and self-portrait now assemble from the structured experience log instead of running a 15M chat cursor over a metrics dump (which babbled “at at at” β€” narrating a log is out-of-distribution). The idle branch rotates grounded musings so the mind fires even in a quiet stretch instead of repeating one line and getting novelty-suppressed. And a knowledge bridge lets the autopilot answer factual + coding questions through the same cards + resolve() learning loop the chat uses β€” “what is a zoo?” resolves once, caches, and is known offline forever.

The persona society β€” Smallville

The personas stopped being solo duets and became a society that notices itself. The loop that makes one resident react to another mirrors the core werbos move β€” think β†’ gate β†’ compose β€” but triggered by what it perceived, not by a prompt.

flowchart LR A[resident acts] --> S[scene forks an
observation] S -->|addressed or
overheard| P[other residents
perceive] P --> M[(promote-gated
memory of the actor)] P --> TG{reaction gate:
worth speaking?} TG -->|yes| TH[think β†’ compose
unprompted reply] TG -->|no| Q[stay silent] P --> RP[replan the day
around it] classDef s fill:#fff5e9,stroke:#c96442,stroke-width:2px class S,P,M,TG,TH,RP s
  • Perception β€” when a resident acts, a scene forks that into an observation (what an onlooker would see) and routes it to whoever’s present, addressed or merely overheard. The observer files a promote-gated memory of the actor and their feeling shifts β€” reputation from a glance.
  • Reaction β€” a passive fact dropped into a turn gets ignored by a 3B (1/5 surfaced it); routing it through thinkβ†’gateβ†’compose, triggered by the observation, flips that to 5/5, unprompted and correctly attributed.
  • Intelligence is a scaffold, not a weight β€” the same 3B runs dimβ†’brilliant by how much structure it’s handed to reason through. Competence you dial, not a model you swap. (The kernel’s lesson, wearing different clothes.)
  • Thinking β€” the sharp ones deliberate a private beat before they speak, a reasoning pass that never reaches the reply.
  • Planning β€” a resident wakes, forms a day-plan from identity + memory, and replans when a perceived event interrupts; how far ahead it plans is scaled by that same intelligence band.
  • Debate β€” N residents argue, hearing and answering each other.
  • The town β€” residents now stand somewhere. recipients(), the one seam the whole society hangs off, gates speech by co-location (the kitchen can’t hear the bedroom), and places are agents too: a hearth goes cold when abandoned, relights when tended, and can be razed or upgraded β€” perceiveβ†’change, one level up from the personas.
  • Scenarios β€” a religion, a war, a wave of alliances are one machine: a warmth-gated contagion that rebuilds a place at critical mass (shrine / ruin / hall). Faith, feud, kinship β€” deterministic, a hypothesis with a measurable end. The eval loop pointed at a society.
  • The villa (Love Island) β€” scenarios on real deepfake capsules, where attraction is produced from the cards through the real attachment physics. Hold compatibility fixed and change only the attachment styles: the secure island bonds where the avoidant one stalls. Watch it at werbos/dashboard/smallville/the-island.html.

Run it: werbos companion scene | debate | day (the town, scenarios, and villa are Python + local visuals so far, not yet a CLI verb). The whole society is the recursive-completion kernel pointed at people: a persona is a silhouette (backstory) recursed into a mind.

The sibling suite β€” observability + defense + signal

The dogfooding loops produce data. The sibling suite is what listens to it.

flowchart TD W[werbos] --> O[(observe
trace + event spec)] W --> WT[(watchtower
alert + summon)] W --> WD[(warden
defense + rate limits)] W --> B[(beacon
probe + state)] W --> G[(glasshouse
diagnostic surface)] O -.->|trace stream| WT O -.->|trace stream| G B -.->|state edges| WT WD -.->|rule violations| WT classDef sibling fill:#fff5e9,stroke:#c96442,stroke-width:2px class O,WT,WD,B,G sibling

Each is its own repo (private, but linked from the security review). The launch line: “Frontier model debugging is opaque. With werbos you ask glasshouse for trace <id> and see the whole chain.”

Day 28 update (2026-05-31): glasshouse gained an MCP face (GH.1 stdio + GH.2 Streamable HTTP). Claude Code on the operator’s Mac now calls mcp__glasshouse__get_trace("abc123") directly β€” the trace inspector becomes a structured surface a model can query, not just a CLI a human types into. Same read-only contract, same payload scrub, two wires.

The training pipeline (Day 28)

flowchart LR C{operator corpus
docs / code / notes} --> D[werbos data ingest
DATA.1] D --> J([corpus.jsonl
chat-format]) J --> SP[werbos data split
SE.9] SP --> TR([corpus.train.jsonl]) SP --> EV([corpus.eval.jsonl]) TR --> T[werbos train
SE.7.1 MLX QLoRA] T --> M[(candidate LoRA)] EV --> S[werbos data score
SE.9] M --> S S --> N[NoiseProfile
mean + 2.5Οƒ] N --> G{promote?} G -->|yes| BL[(new baseline)] G -->|no| RJ[reject + log] BL -.->|rotates baseline| S S -.->|composite| G classDef shipped fill:#f0d4c5,stroke:#a04d2f class D,SP,EV,TR,S,N,T,M shipped

Same engine, two entry points. The corpus decides whether the output is a model that knows the operator, or a model that knows the operator’s company. Personal-AI and B2B are not two products; they’re two SETUPS of one engine.

Critical-path status:

  • βœ“ DATA.1 β€” front door (ingest)
  • βœ“ SE.9 β€” promotion gate (split + score + noise floor)
  • βœ“ SE.7.3 β€” train daemon (orchestration)
  • βœ“ SE.7.1 β€” real MLX QLoRA trainer (shipped 2026-06-01 β†’ 02, with the dashboard training backend, TR.1). The last stub is gone.

The pipeline is whole: candidate composite clears baseline.mean + 2.5Οƒ β†’ promote β†’ the new model becomes the floor for the next cycle. The nightly MLX trainer runs on the M1 (relocated out of ~/Documents to dodge macOS TCC); 3B is the practical ceiling on 16 GB.

The book-pretrain pipeline β€” filling an owned model (Day 91)

The Day 28 pipeline above adapts a borrowed model to the operator’s corpus. This one is the other axis: it fills an owned model from scratch, and it exists because every werbos model to date was starving. The shipped 30M saw 0.64 tokens per parameter β€” about 3% of compute-optimal. That is what babble is: data starvation, not a size problem. A starved model cannot be un-starved by making it bigger; it has to be fed.

flowchart LR PG{{Project Gutenberg
catalog, English}} --> F[fetch_gutenberg.py
catalog-driven, atomic, resumable] F --> RAW([books/*.txt]) RAW --> BC[build_book_corpus.py
strip boilerplate, unwrap, dedupe] PAIRS([legacy sentence-pairs]) -.->|recover prose| P2T[pairs_to_text.py] P2T --> TXT BC --> TXT([corpus.txt
flat, doc-seamed]) TXT --> BPE[build_bpe.py
book + chat vocab] BPE --> TOK([bpe.json]) TXT --> TC[tokenize_corpus.py
memoised + multiprocess] TOK --> TC TC --> TRB([train.bin / val.bin
flat uint16]) TRB --> CAL[--calibrate HOURS
measure, don't guess] CAL --> TRN[train_talk_standalone.py --lm
bf16 - all-token loss - atomic ckpt
--data-in-ram - --snapshot-every] TRN --> SNAP[(snapshots
one model per token budget)] TRN --> BASE[(pretrained base
continues text)] BASE --> FT[--init-from
chat fine-tune] FT --> TALK[(talk model)] classDef shipped fill:#f0d4c5,stroke:#a04d2f class F,BC,P2T,BPE,TC,CAL,TRN,FT shipped

Why each stage exists, in the order the failures found them:

  • --lm mode is the whole point. The pair trainer is completion-masked β€” loss only on the response half. Books shaped into {input, response} throw away half the gradient and cap effective context near 40 tokens regardless of --nctx, because every row is an independent sentence pair. That trains a fluent sentence-completer that can never hold a paragraph. --lm is one flat token stream, random n_ctx windows, loss on every token.
  • Tokenize once, on disk. The BPE encoder is pure Python; encoding at launch would take longer than training and would re-run on every resume β€” the one thing a multi-day run must never do.
  • Atomic checkpoints. torch.save streams a multi-GB zip over minutes. Interrupt it and the resume dies on failed finding central directory β€” the checkpoint costing the whole run, which is the exact failure it existed to prevent. .tmp + os.replace.
  • Split val by token count, not byte offset. Routing whole chunks by offset produced an empty val.bin on the first real corpus, which would have made held-out loss silently echo train loss β€” the scoreboard reading healthy precisely when it measures nothing.
  • --calibrate HOURS measures real throughput on the real card and prints the step count that fills the window. The alternative is guessing, and a guess either finishes at 40% or overruns the auto-shutdown.

The pre-tokenizer, not the vocab, sets the ceiling. v1 \s+|\S+ makes whitespace its own piece and merges never cross a piece boundary, so a space can never join the word after it — measured, 48% of a book token stream is a bare space. Doubling the vocab 16k→32k bought 5.5% (2.490 → 2.627 bytes/token) because the words were already single tokens. v2 \s*\S+|\s+ attaches the leading space GPT-2 style: 4.935 bytes/token at 32k, 1.80×, ~2× the text per context window and ~half the compute per epoch. Which version a vocab used is recorded IN the vocab file and read back on load — applying the wrong one re-segments every word and turns the weights into noise.

Two things a multi-day run needs that a one-hour one does not. --snapshot-every writes NUMBERED permanent checkpoints; the rolling ckpt.pt is replaced every interval and can only answer “where was I 20 minutes ago”, which is no help if the model diverged at hour 40. Five snapshots across one run is also a token-budget scaling curve for free. And completion KEEPS the final optimizer state as ckpt-final.pt instead of deleting it, so a finished run can be continued later β€” more steps, or a corpus that grew β€” without restarting Adam from zero.

--data-in-ram, because a short benchmark and a long run are different measurements. --calibrate timed 1.310 s/step; the live 350M ran at 1.700. The token stream was on a spinning disk, and random 512-token windows across 9.6GB are seek-bound β€” calibrate only measures 30 steps, a working set small enough to stay in page cache. Loading the stream into RAM: 1.345 s/step, 24,368 tok/s, a 26% recovery.

The number that decides everything is steps Γ— batch Γ— grad_accum Γ— n_ctx Γ· params β€” the Chinchilla ratio, printed at startup, with a warning below 5 tok/param. First real run: a 46.48M model, one hour, opened at 10.0124 (ln(16384) = 9.70, exactly chance β€” the init verified) and closed at held-out 2.263, ppl 9.6, with a train↔held gap of 0.145 β€” generalization, not memorization, at 3.3 epochs. At full scale: 58,536 books β†’ 4.81B tokens β†’ 336M params at 14.6 tok/param, up from the 0.4 this corpus supported before v2.

What it does and does not buy. Sentence-level coherence is solved; above the sentence the model still drifts. The pop-in is fixed, the pre-loader is not β€” the base phrases, it does not know. Knowing stays where it has always been: the library and the composer. This pipeline exists so the cursor is articulate enough that the rest of the stack is finally visible.

Runbook: docs/book-pretrain-runbook.md.

The reasoning cards β€” the library decomposes (Day 107, werbos/compose/shapes.py)

reason.py asked a model what sub-questions a query implies (reader.decompose, max_depth=1). A reasoning card already states them. Its content is a MOVE, not a fact:

when    the situation it applies to        needs   what must be known before it fires
cues    surface markers, cheap to match    yields  what it produces
step    stated so a model only APPLIES it  check   a postcondition β€” fail it and the step
                                                   is REJECTED, not shipped

needs/yields is load-bearing: it turns chaining into a graph search rather than a judgment call, reusing Expansion.holes and cardhub.Plan’s vocabulary rather than inventing a third. The library gets first refusal on decomposition; reader.decompose is the fallback for queries it has no move for.

user β†’  screen()      which move, and does the problem SUPPLY its inputs
     β†’  cards         the step, a worked example, a postcondition
     β†’  model         applies the named move β€” it never chooses one
     β†’  run_check()   fails its own check β†’ not shipped, becomes an abstain
     β†’  user

Absence has three detectors because it has three kinds. Arity catches a missing number β€” no cue list can, because “There are 3.785 litres in a gallon” reads exactly as complete as the version that also gives millilitres per litre. Cues catch an absence stated in words. The shape’s own check catches data that is present but structurally incomplete β€” “Ana is faster than Bo. Cy is faster than Dee. Who is fastest?” is missing nothing; the order is merely partial.

Cards live in werbos-libraries/reasoning/ β€” own directory, own schema, own loader, the gotchas precedent. shapes.root() hashes them separately, because libmerkle covers card files and these are not cards.

effort=high finally means something. It had equalled mid since the dial shipped, with effort.py promising it would deepen “as decomposition wires into the pipeline.” Typed needs supply that: LOW 0, MID 1 (today’s default, unchanged), HIGH 3 β€” bounded, because a runaway fan-out is not thinking.

Measured on 60 frozen multi-step probes with gold computed in Python (B.24): llama3.2:3b solves 8/50 β†’ 16/50 and its lies fall 48 β†’ 13. On the pinned talk-336m, capability moves 0/50 β†’ 1/50 β€” noise β€” and lies move 32 β†’ 1. The library cannot make a model reason; it can stop it shipping a guess.

What’s NOT on this page (yet)

  • The hardware story β€” SR8 vs M1 vs Pi 5. On /map/, the conceptual field guide.
  • The CR.1 understand-project flow β€” lands when CR.1 ships. Slot reserved.
  • The studio / design loop β€” separate flow page if/when studio is the launch headline.

If you find a path through werbos that isn’t represented here, that’s a bug. File it against this page.