Day 16 — 'the studio shipped. then the studio couldn't talk to itself. one line later, it could.'
Morning: R.1 preflight decisions locked, R.2 T.3 flip shipped — werbos serve --bind now defaults to operator-approval. Mid-day: SD.1 design-studio MVP (Pollinations.ai image_generate + Playwright screenshot_url + the design-studio agent template) and SD.2 dashboard surface (list + wizard + 3-pane canvas) shipped same day. SD.2 day-2 wired ambient project_context: chats bound to a project read ~/werbos/projects/<name>/intent.md so the operator stops re-typing briefs. Evening: AO.1 Part 1 reframed @studio as an orchestrator, introduced dispatch_to_agent as the primary tool, and installed three specialists (@visionary scope, @front-end HTML/JS bones, @css-wizard restyle). Per-agent model routing finally cashed in — 14b for design judgment, coder:7b for structure. Late night: AO.0 — the bug that had been costing the whole week — diagnosed and fixed. _context_builder_for_agent dropped inline_tool_docs on the per-agent builder, so agent-bound chats double-documented every tool (inline prose AND structured payload) and qwen narrated 'I'll route to @visionary' instead of structurally tool-calling. One line in sessions.py + an env-var-gated payload dumper in pitts (391399f). System prompt fell 19,403 → 11,073 chars; the orchestrator emits clean structured tool_calls on the next replay. 11 commits.
“the studio shipped. then the studio couldn’t talk to itself. one line later, it could.”
Morning — preflight + safety flips
Locked the R.1 preflight decisions (af6f9fe) and shipped R.2
T.3 (7c3477f): werbos serve now defaults to operator-
approval before any listener binds an externally-reachable
port. The legacy --confirm-bind opt-in is gone; you opt OUT
now with --unsafe-immediate-bind. The same flip the security
audit wanted, just expressed the right way around — the safer
path is the path with fewer flags.
Mid-day — SD.1 + SD.2 in the same window
SD.1 (3fe6bd1) built the design-studio agent + two new
tools:
image_generate— uses Pollinations.ai’s free FLUX endpoint. No auth, URL-encode the prompt, get a JPEG back. Saves under~/werbos/images/<ts>-<slug>.jpg. Local-first inference for the LM still wins; visual references can hit a free remote when local Stable Diffusion isn’t worth the disk.screenshot_url— headless Playwright chromium with a hardened allow-list (file:// or localhost only by default, https:// optional). The MVP was three turns: generate a reference image → write a single-file HTML to match → screenshot and self-check.
SD.2 (c11cad6) wrapped a dashboard around it. List +
wizard + 3-pane canvas (file tree, preview iframe, embedded
chat). 25 new endpoint tests + 8 session-level intent.md tests.
2,472 lines of TSX. Same shape as Claude Design — the operator
expected one.
SD.2 day-2 + AO.1 Part 2 (dd1bc8b) was the small fix that
unlocks the rest: chats bound to a project read the project’s
intent.md as ambient context. Operator stops re-typing the
brief that’s already sitting on disk. POST /api/chats
accepts project= (body or query) → SessionRegistry.create()
threads it into ContextBuilder.project_context. The “✦ Chat
about this project” button in StudioDetail uses it.
Evening — orchestration ensemble (AO.1 Part 1)
349ddaa was the bigger reframe. Instead of trying to make one
agent good at every step of building a thing, @studio
became an orchestrator with a single primary tool —
dispatch_to_agent. Three new specialist agents took over the
artifact work:
@visionary(qwen2.5:14b) — vague request → focused product brief in markdown (spec.md).@front-end(qwen2.5-coder:7b) — HTML+JS bones, no styling. Emits one fencedhtmlblock which the artifact extractor lifts to disk.@css-wizard(qwen2.5:14b) — takes the existingindex.htmland rewrites only the<style>block, keeping structure + JS identical.
dispatch_to_agent runs each specialist in a fresh
pitts.Agent sub-session with its own persona, captures the
text content, parses it with extract_artifacts (four
strategies: explicit path=foo fence, language-hint fence
mapped to suffix, unfenced single, fallback warning), and
writes files under ~/werbos/projects/<project>/ on the
specialist’s behalf. Recursion-guarded depth 4. Brief capped
at 16 KB. Per-agent model routing — the V1.8 plumbing that
landed two days ago — finally cashed in here: 14b for design
judgment, coder:7b for structure, all routed by the same
orchestrator with one HTTP host.
The clever bit: specialists emit text content, never
structured tool calls, so the framework writes files for
them. Whatever tool-fidelity issues qwen-class models have, the
specialists dodge them entirely. The orchestrator was still
expected to tool-call structurally to invoke dispatch_to_agent
— which set up the day’s last sprint.
Late night — AO.0, the bug the whole week was paying for
Drove a test through the chat UI. @studio produced a working
todo HTML — but as chat output, zero tool calls. Same
failure family as the qwen2.5-coder:7b observation from
yesterday. The orchestrator was narrating “I’ll use my X tool”
instead of structurally emitting the call.
It looked like a model-fidelity problem on qwen2.5:14b. It wasn’t.
The dumper
Added WERBOS_BELLMAN_DUMP to pitts (391399f on the sibling
repo). When the env var is set, BellmanBackend appends every
outgoing request payload and every incoming response chunk to
<path>.{request,response}.ndjson. ~30 LOC; never raises on
dump failures; no-op when the env var is unset. Set it,
restarted serve, drove one chat turn against @studio +
qwen2.5:14b on SR8, looked at the JSON:
system prompt: 19,403 chars
↳ contains '# Tools' section: True
↳ contains '## dispatch_to_agent' subsection: True
tools count in structured payload: 9 (including dispatch_to_agent)
Two representations of every tool — the prose docs inline in
the system prompt on top of the structured tools list
already in the API payload. Some turns qwen picked the
structured channel; some turns it picked narration. Same model,
same prompt, stochastic.
The one-line fix (8ec97c3)
werbos/cli/serve.py:228 already sets inline_tool_docs=False
on the default ContextBuilder. The fix werbos code shipped
weeks ago. But SessionRegistry._context_builder_for_agent
(sessions.py:566) builds a fresh ContextBuilder for agent-
bound chat sessions and silently dropped that flag. Dataclass
default is True.
return ContextBuilder(
persona=combined_persona,
character=base.character,
mirror_ratio=base.mirror_ratio,
max_tokens=base.max_tokens,
compaction_threshold=base.compaction_threshold,
chars_per_token=base.chars_per_token,
tokenizer=base._tokenizer,
# AO.0 fix (2026-05-16): propagate from the base.
inline_tool_docs=base.inline_tool_docs,
)
After: system prompt dropped to 11,073 chars (-42%). Same
payload replayed against ollama directly — qwen2.5:14b emitted
a clean structured dispatch_to_agent("@visionary", ...)
tool_call with the operator’s brief verbatim. Exactly the
contract the orchestrator persona writes.
Why this matters beyond AO.0
The class of bug: dataclass defaults bite at copy boundaries. Any time you build a “fresh X seeded from existing X,” every field needs an explicit pass-through. Regression tests should pin the flag itself, not a downstream symptom — symptom tests miss it because “the model didn’t tool-call” is a hundred possible causes.
Corollary: two representations of the same affordance → models pick the weaker one. Tools-in-prompt + tools-in- payload meant qwen had a choice between “narrate” and “emit.” Sometimes it picked the wrong one. The fix wasn’t model training; it was deleting the bad option.
The werbos chat REPL (chat.py:2347 + 2361) constructs
ContextBuilder without inline_tool_docs=False — same class
of bug, hasn’t been retested with the AO.0 fix in place. That’s
tomorrow’s sweep.
Numbers worth keeping
- 11 commits on
main. - +58 new tests. 3,802 → 3,859 just from AO.1 Part 1, plus the AO.0 regressions both directions. Full suite green.
- System prompt -42% on agent-bound chats (19,403 → 11,073
chars) the moment
inline_tool_docspropagated. - 4 specialist agents installed:
@studio(orchestrator),@visionary(scope, 14b),@front-end(HTML/JS, coder:7b),@css-wizard(restyle, 14b). - Tool-result preview cap <2KB so the orchestrator’s context budget survives a three-step chain.
AHA
The dumper that cracked AO.0 took 30 minutes to write. The bug
had been costing me visible model-fidelity confusion for the
better part of a week. The dump-on-env-var pattern is cheap
and worth keeping — WERBOS_BELLMAN_DUMP stays in pitts; any
future “why doesn’t this model do X” question gets a payload
diff instead of a model-blame.
Carryover
- AO.1 Part 3 — pipeline indicator UI in StudioDetail.
Reuse the
.host-pillaesthetic. New/api/studio/projects/<name>/pipelineendpoint that walks the project’s chat archives so the operator sees ✓ spec / ✓ bones / ⋯ styled as the chain advances. - AO.1 Part 4 — shared-scratch
pipeline.mdper project, so specialists can see what previous steps decided instead of re-deriving from the brief. - Wider AO.0 sweep — re-test
werbos chatREPL,werbos eval, and the Python SDK surface after propagatinginline_tool_docs=False. Same class of bug, multiple call sites. - EN.1 encryption-at-rest — 6-7 day v1 sprint drafted at
docs/sprints/en1-encryption-at-rest.md. 2SKD architecture ported from the 1Password reverse-engineering. Three decisions to lock before kickoff: Argon2id vs PBKDF2, SQLCipher vs accept-v1-metadata-leak, master-passphrase caching policy.
Quote of the day
“i 100% want that encrypted”
Re: sensitive chats. The seed for EN.1.
Aphorisms
- Dataclass defaults bite at copy boundaries.
- Two representations of the same affordance → models pick the weaker one.
- The dump-on-env-var pattern costs nothing and turns multi-day mysteries into 30-minute diffs.