The Flow
Everything that happens between you typing into werbos and getting a response back. The diagrams answer: which component is doing what, where does the data go, and when something is wrong where do you look.
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
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.
(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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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):
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/sizeso 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 inintent.select_library, which readsCorpusIndexchunks; it took until Day 104 to reach the CARD path, becauseresolve()had no route to it andlibrary_lookupflattened 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 ownlangjoins 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 citedoperator-goldchunk, 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:
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_gapscoreslie_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:
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):
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_idis 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β15fires, the broken skill lands inweak_at. - Moat-safe by construction. The check is deterministic β no model grades an answer’s quality,
the soft-judge-grading-soft-judge trap
reflection_oracleexists to refuse.--driveonly rewords the input to test robustness; the first run’s adversary already foundsyllogismsurviving 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--drivereworder.
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”:
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:
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.
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:
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:
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 × 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.
| owned | llama3.2:3b | qwen2.5-coder:7b | |
|---|---|---|---|
| params in the answer path | 0 | 3B | 7B |
| answered | 67% | 100% | 100% |
| correct | 53% | 43% | 58% |
| wrong | 81 | 323 | 234 |
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.
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.
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.
(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.
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.
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.
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.
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)
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.
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:
--lmmode 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.--lmis one flat token stream, randomn_ctxwindows, 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.savestreams a multi-GB zip over minutes. Interrupt it and the resume dies onfailed 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.binon 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 HOURSmeasures 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.