Day 37 — 'close the loop.'
Two arcs. Morning: harden reality. The one real bind footgun — `werbos serve --bind 0.0.0.0` exposed an unauthenticated agent to the LAN, so serve now refuses a non-loopback bind without --allow-remote-bind (fail-closed). And the uncomfortable truth that 5,500 green tests tested logic, not reality: every invisible-failure bug this month lived in deploy config or real backend behavior nothing exercised — so a deploy-config contract (mutation-verified to catch all three recent production breaks) + a real-Ollama live-smoke layer, and a latent bug surfaced on the way (the committed .dockerignore was NEVER applied — context:.. means Docker looks one level up). Afternoon: a daydream about a two-wheeled robot ('PAL, not HAL') converged on the real insight — werbos already runs a reactive loop (the inbox-janitor); what's missing is a DELIBERATIVE one. Built the autopilot: werbos running its own skills in a predict→act→observe→error→correct loop, fail-closed allowlist + append-only revert, then closed the slow loop (a learned routing policy promoted only if it beats the champion on held-out data = the build-up-not-down stability gate), proved it end-to-end (corrections drop to zero after learning), gave it self-reflection ('what I'm missing'), and three ways to watch it (CLI watch + dashboard panel). The AHA: the prediction error is the whole difference between a loop that runs and one that improves.
“close the loop.”
Two arcs today, and they rhyme. The morning was about making sure werbos tells the truth about itself. The afternoon was about teaching it to learn from its own mistakes.
Morning — harden reality
The one real bind footgun (50eafc7)
The core /api/chats API has no auth. So werbos serve --bind 0.0.0.0
— the exact thing a stranger reaches for to use werbos from their
phone — exposed an unauthenticated agent (drive the model, read/write
conversations, trigger tools) to the whole LAN. serve now refuses to
start (exit 2) on any non-loopback bind unless you explicitly pass
--allow-remote-bind, with a friendly message pointing at the
reverse-proxy/Tailscale fix. _bind_is_loopback fails closed —
garbage and hostnames are treated as remote. The docker stack binds
127.0.0.1, so it’s unaffected.
The suite tested logic, not reality (957af4f)
Here’s the uncomfortable one. 5,500 green tests — and yet every invisible-failure bug this month was caught in production, not by the suite. Because they all lived in the one place nothing tested: deploy config and real backend behavior. Everything ran against MockBackend, which never 503s, never evicts a model, never returns an empty reply.
So two new layers. tests/deploy parses the real compose file and
asserts the invariants that were previously enforced only by comments
and operator memory — and I mutation-tested them: re-introduced
each of the three recent production breaks and confirmed the guard
trips. Model-flag drift (the 176s latency regression), keepwarm pinning
a deleted model (the silent 404 every 60s), a non-loopback bind. All
caught. tests/live is a real-Ollama smoke gated on WERBOS_LIVE=1
(skipped offline so the suite stays green) for the bugs the mock can’t
model: empty completion, missing-model-as-clear-error, num_ctx eviction.
The .dockerignore that never ran (921e739)
Closing the build leg surfaced a genuinely latent bug. The committed
orchestration-deploy/.dockerignore was never being applied — the
builds use context: .., so Docker looks for ../.dockerignore, which
doesn’t exist. The whole node_modules-leak protection was decorative;
the suite-ui build break could recur any time. Fixed with per-Dockerfile
<Dockerfile>.dockerignore files (which BuildKit honors regardless of
context root) + a static guard test so a future Dockerfile that forgets
one fails a test, not a deploy.
Afternoon — the autopilot
This one started as a daydream. A two-wheeled robot, a Raspberry Pi, a hand-rolled model — “PAL, not HAL.” HAL was a frontier model in a sealed box that decided it knew better than you; PAL is the inversion: small, local, yours. And the brainstorm kept converging on one real idea: the brain isn’t a router, it’s a predictive loop. Your eyes track a moving object by predicting where it’ll be and correcting on the error.
Then the realization that made it buildable: werbos already runs a reactive loop — the inbox-janitor sorts files. What’s missing isn’t the robot. It’s the deliberative loop sitting beside it.
The loop (ea1372d)
Predictive coding, exact. PREDICT — a forward model reads a request
and writes down, before acting, which skill it’ll use and what it
EXPECTS to happen (that’s the part that makes it a forward model and not
a router). ACT — it dispatches that skill on its own. OBSERVE —
captures the result. ERROR — a judge compares expected vs actual.
CORRECT — on a miss, retry the next-best skill. Every tick appends
{request, prediction, action, outcome, error} to an experience log.
That log is the keystone, because it’s the training data for the slow
loop.
Safety, before it runs (3bed801 · b43249c · 9dc3f58)
An autonomous thing that runs skills on its own needs a leash before
it runs automatically — that’s the right order of operations.
Defense in depth, all fail-closed: a safe-skill allowlist (it
literally cannot reach a destructive skill unattended — the catalog is
filtered before the model sees it AND enforced at act-time), a
dry-run mode, append-only revert (werbos autopilot revert),
and full tracing. Wired into serve opt-in, off by default — you have
to ask for it.
Close the slow loop (1c8a783 · 1e46851)
This is the payoff. The fast loop’s prediction error becomes the slow
loop’s training signal. A learned routing policy is distilled from the
satisfied experiences — and promoted only if it beats the champion on
a held-out split. Never ship a worse policy. That gate is the
champion/challenger pattern, and it’s the operator’s own framing of the
real prize made concrete: “build or destroy itself is the 100x problem”
— stability of self-improvement. A PolicyTrainer runs it on an
interval so the policy improves on its own.
And then I proved it. An end-to-end test runs the whole circuit deterministically: a naive predictor that always guesses “joke” misses and corrects on every stock request; the slow loop trains a policy from that experience; the policy plugged back in routes stock requests straight to the stock skill — and corrections drop to zero. The loop measurably improved from its own mistakes.
It can also reflect now. “What I know about myself, what I’m missing, what I’d like to improve.” The richest signal turned out to be the refused actions — the skills it reached for but wasn’t allowed to run. werbos literally names the capabilities it wishes it had.
Watch it (c2da45c · 0cd632d)
Three ways, revert from any: werbos autopilot watch (a live,
self-refreshing terminal screen — counts, policy version, last training
decision, recent actions), a dashboard panel (/api/autopilot + an
Autopilot route — the reflection cards, a recent-actions table with
per-row revert), and glasshouse trace <id> for the per-action chain.
The AHA
A router goes request → skill and forgets. A forward model goes request → prediction → skill → error, and the error is the thing that both corrects it now and teaches it for next time. No error, no learning. That one extra arrow is the whole difference between a loop that runs and a loop that gets better.
5614 green across the day (+80).
Evening — deployed it, dogfooded it, doubled the library
The afternoon built the autopilot. The evening turned it on — and it immediately taught me things the test suite couldn’t.
Live, with a box you can type into
Wired --autopilot into the homelab deploy and shipped a “drop a
request” box on the dashboard so I could feed it from the browser
instead of SSH-ing a file into the root-owned inbox.
Dogfooding earned its keep (5f540d4)
Watching real requests land on the panel surfaced two bugs no test would
have. One: seven names on the autopilot’s safe-skill allowlist
(news, stocks, weather, …) weren’t actually prompt skills, so the
runner returned “Unknown prompt skill” and they all failed. Two: the
judge compared each result to its own predicted text — so a real joke
that wasn’t the same joke it imagined, a perfectly good tarot spread, and
even correct driving directions all got marked FAIL. For generative
skills the prediction is a shape, not a target.
Fixed both: the allowlist is now the prompt-skill catalog itself (guaranteed runnable, still fail-closed against destructive skills), and the judge grades whether the result satisfies the request, leniently. The joke that failed at 5pm passed at 6.
The library nearly doubled — 137 → 294
This is the headline. First a hand-built developer block — twelve
terminal-tab skills (error-decode, git-undo, regex-build, jq-build,
rubber-duck, explain-code, …). Then I let a multi-agent workflow
loose on the rest: 18 category agents authored in parallel, each followed
by an adversarial verifier — 36 agents, ~912K tokens — producing 157
more skills across dev, career, learning, decision-frameworks, writing,
comms, relationships, parenting, pets, home, health, and events.
A deterministic gate on integration caught what the LLM agents missed:
dupes of the hand-built set, and any format_marker that wasn’t actually a
required label in its own prompt (so the grader can never false-fail it).
“A new capability every Tuesday” became a hundred-and-sixty-nine in an
afternoon. The library is the product, and it just grew a lot.
Debug surfaces, and a silent bug they exposed (f918354)
Two new dashboard pages: Tracing (glasshouse in the browser — paste a trace id, see the whole chain as a timeline) and Memory (what werbos remembers across conversations). Every autopilot action’s trace_id is now a one-click link to its chain.
And building those surfaced a bug that had been quietly sabotaging every UI
change all day: nginx sent no cache header on the SPA shell, so the browser
kept serving a stale index.html pointing at a deleted bundle — “I
deployed but nothing changed.” Now the shell revalidates every load and
the content-hashed assets cache forever. Deploys are instant again.
5627 green by end of day. A long one — and the kind where the system you built starts handing you your next tickets.