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