Day 11 — 'the dashboard stops lying.'
v1 release week, compressed into one session. Ten sprints (V1.0–V1.9) shipped end-to-end against the locked v1 baseline: env-var alignment, cosmetic truth pass, `/api/integrations` env-var probe, `/api/settings`, `/api/agents` CRUD, permissions enforcement, models lifecycle, channels routing, the scheduler daemon (largest sub-sprint), `/api/activity` aggregator. Every dashboard surface now does what it appears to do. +96 tests in the scheduler alone; +200ish across the day. The v1 baseline contract from `project_v1_baseline_scope` is satisfied.
“the dashboard stops lying.”
Day 10 shipped the dashboard as a shell — 9 routes, beautifully
rendered, mostly localStorage underneath. Day 11 made every row of
the per-element audit in gap-hunting.md true. The morning
locked the v1 baseline (Phase 14 in the roadmap; 10 sprints across
~25 days planned). The evening compressed all 10 into one session.
By 21:46 PT, every v1-scope surface — agents, perms, channels,
scheduler, integrations, models, settings, Home’s Recent activity
— answered to the server instead of to localStorage.
The through-line: a dashboard that lies on click poisons every demo. Better to delay v1 than ship dishonest. The fastest way to keep that promise was to make today’s session the v1 release.
The lock-in
A short morning of planning + an infrastructure fix before the sprint rush:
8ddfe62— Locked the v1 baseline. Phase 14 of the roadmap collected the 10 sprints (V1.0 through V1.9) into a single cheapest-truth-first ordering. Per-element audit atgap-hunting.mdis the canonical truth-table the sprints attack. Also captured Phase 15 (function-calling parity / skill registry) as the post-v1 horizon.3b4fcfa—nginx /api/proxy was stripping$host, so every state-mutating request to the dashboard’s API looked cross-origin to werbos’s CSRF middleware (which is configured for same-origin only). Forwarding$http_hostmakes the proxy transparent to the middleware. One-line nginx fix, but unblocks every POST/PUT/DELETE the rest of the day will land.
The v1 rush
Ten sprints, one session. Roughly two hours of actual delivery time (17:43 → 21:46 PT). Sequenced cheapest-truth-first per the roadmap plan, with V1.7 the gravity well (~7-day sprint compressed to a long block):
| Sprint | Surface | Headline | Commit |
|---|---|---|---|
| V1.0 | Adapter env-vars | Drop WERBOS_ prefix from dashboard catalog — the names now match what werbos/server/app.py actually reads. Was the highest-ROI bug in the repo: 4-line edit unblocked every adapter | 697bd8d |
| V1.1 | Cosmetic truth pass | Sidebar pill / topbar avatar / Home Card 2 — stop hardcoding values whose real source already exists | 36501e9 |
| V1.2 | /api/integrations | Real env-var-presence probe replaces honor-system “I’ve set this up” mark. Home Card 4 num + subline reads live | 50bf291 + 3684d07 |
| V1.3 | /api/settings | Werbos home / chats dir / default model / mirror surface from the actually-running server | cf9aa31 + 41ce5d6 |
| V1.4 | /api/agents CRUD | AgentStore (one markdown file per agent at ~/werbos/agents/<id>.md) + 6 endpoints + dashboard swaps localStorage cache for /api/agents reads + tags fold into agent payload | ca6536a → 98a64b4 |
| V1.5 | Permissions enforcement | Tri-state verb gating actually fires at tool dispatch. Tool-registry perm-check + pending-action ask flow for the ask middle state | efe5778 + 0195c36 |
| V1.6 | Models lifecycle | Test / Pull (SSE-streamed ollama pull) / Remove for /api/models/{id:path} end-to-end. Backend inferred from id | 9e10c32 |
| V1.8 | Channels routing | social_channel_routing table + in-band /bind @handle slash + AgentDetail Connected-channels list. Inbounds route to the agent toggled for the channel | 59872c1 |
| V1.7 | Scheduler daemon | Largest by far. New werbos/scheduler/ pkg (RunStore + hand-rolled cron + FakeClock + asyncio ticker), POST /api/agents/{id}/run, real ChatSessionRunExecutor, SessionRegistry.create(agent_id=…) consuming the V1.8 deferred persona wrap, per-run timeout + bounded auto-restart (3 crashes / 10-min window). 96 new tests | 67f1555 → 546e2dd |
| V1.9 | /api/activity | Unified-timeline aggregator over chats / tasks / runs. Home’s Recent-activity placeholder replaced with the live feed (kind chip + agent handle + status + timestamp). Last v1 stub | 991d13d + 2c0142b |
V1.7 deserves its own paragraph. The Scheduler page on Day 10 was
the largest gap in the dashboard — toggles flipped a localStorage
status field, Run-now was optimistic UI hints, Recent runs was a
hardcoded “lands Day 4” placeholder. The sprint landed seven days
of scope compressed into one block: data plane (agent_runs SQLite
ledger, UUIDv7 ids), HTTP surface (4 routes + a RunDispatcher
Protocol so day 2 was testable before the daemon existed),
hand-rolled 5-field cron parser (skipped croniter; the dialect
the dashboard’s crontab.guru tooltip implies is small enough to
own), FakeClock with a heap of pending sleepers so daemon tests
advance time deterministically without monkey-patching
asyncio.sleep, the asyncio ticker itself (races sleep against
stop signal for clean teardown; sweeps orphaned running rows on
start). Day 4 consumed V1.8’s deferred persona-wrap by making
SessionRegistry.create(agent_id=…) build a per-session
ContextBuilder seeded with the agent’s identity block + persona
voice. Day 5 wrapped the executor in asyncio.wait_for for hard
timeout and added a bounded auto-restart with a sliding 10-minute
crash budget. Day 6 wired Scheduler.tsx to the live daemon. Day 7
the end-to-end round-trip + gap-hunting.md close.
The coder-models tangent
Pre-sprint, before V1.0 went out, one targeted fix to the self-contamination loop coder-tuned local models fall into:
2dd0405— Two failure modes observed on the L1 benchmark withqwen2.5-coderandqwen2.5:14b. (1) Models emit a fenced```jsonpseudo-tool-call instead of dispatching the structured channel; Pitts’s salvage parser skips fenced JSON on purpose (R2 defense against tool-call injection from retrieved web pages), so the wrong-shape sticks in history and the model copies its own pattern on subsequent turns. Fix: a history sanitizer (strip_fenced_tool_call_json) erases self-authored fenced shapes from prior assistant content before re-feeding to the backend. (2) Models double-escape string content in tool args (\\ninstead of\n); after JSON decode the file has literal backslash-n instead of real newlines. Fix: targeted un-escape (only the 6 chars in_BACKSLASH_ESCAPES; NOTunicode_escape, which would eat\\Wregex backslashes). Six new tests.4f0dc1d— Pinned coder temperature low + named the fence anti-pattern in the persona body so the model is told what NOT to do.
Late polish
After V1.9 close, four small fixes landed before sleep — each one catching a quiet lie I’d missed at first scan:
a1e6205— ROADMAP cleanup. Phase 14 collapsed from 600 lines of per-sprint planning bullets to a closed-sprints summary table. Phase 2 (orchestration suite — observe / watchtower / warden / beacon, all v1.0.0 since Day 7) collapsed too. Net diff: 41 insertions, 723 deletions. Added the “Two known opens (NOT v1 blockers)” block at the bottom for residual cleanups.d8fa7c7— Home Card 2 was filtering tasks bystatus !== "completed", but TaskStore only ever writespending | in_progress | waiting_human | done | failed. The string"completed"literally never appears, so the inequality always evaluatedtrueand “Open tasks” rendered the entire tasks table. Symptom on SR8: showed4with zero tasks actually in flight. Fix: explicit allow-list of non-terminal statuses matches the store’s vocabulary directly.b7140c5— Topbar avatar already pulled fromProfile.identity.name, but operators routinely paste pronouns inline ("Jason (he/him)","(they/them) Sam","Jason he/him"), so the first-codepoint extraction picked"("or"h"by accident. NewstripPronouns()helper drops parenthesized blocks + slash-containing tokens before taking the first character.ce89e2d— Collapsed sidebar toggle was unreachable on mobile. The brand-mark (26px) + the sidebar-head’s 18px horizontal padding overflowed the 60px collapsed column;.sidebar { overflow: hidden }clipped the›button off the right edge. Once collapsed, no way to reopen it. Fix: hide brand-mark + empty col when collapsed, center the toggle, bump it to 44×44 below 640px viewport for WCAG/HIG touch-target minimums.
Numbers worth keeping
- 10 / 10 v1 sprints closed — the v1 baseline contract from
project_v1_baseline_scopeis satisfied. The dashboard’s v1-scope surfaces (agents, perms, channels, scheduler, integrations, models, settings, activity) no longer have anylocalStorage-only / stub / fake / honor-system rows. - Two known opens remain in
gap-hunting.md’s cosmetic-lie table — sidebar host pill hardcoded127.0.0.1:8000, topbar avatarJ. Both< 10-line UI rewires. Neither blocks v1 ship. - 96 new tests in the V1.7 sprint alone (cron 22 + clock 8
- daemon 20 + e2e 6 + lifecycle 8 + store 27 + routes 19 + session-wrap 10 — those last two cross V1.7/V1.9 boundaries but were V1.7’s drive).
- 32 commits between
f65cadf(Day 10 docs roll-up) andce89e2d(final sidebar fix). The v1 sprint commits themselves:697bd8d→2c0142b.
Through-line, restated
Day 10 fortified, then built. Day 11 made what was built honest. The pattern that keeps emerging: every feature has a documented-vs-actual surface, and the documented surface lies for a while because the actual surface is hard. v1 release is the discipline of refusing to ship until those match. Each of today’s ten sprints was a 1-to-5-day plan from the locked baseline; one session compressed that into a continuous block by chasing the cheapest truth at each step (V1.0 — a 4-line rename — first; V1.7 — a 7-day daemon sprint — sequenced after every prerequisite was real).
Tomorrow opens with one of the cosmetic-lie cleanups still
pending, a werbos add Slack operator walkthrough, and whatever
falls out of the morning’s gap-hunting pass against the now-real
dashboard.