“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 at gap-hunting.md is 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_host makes 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):

SprintSurfaceHeadlineCommit
V1.0Adapter env-varsDrop 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 adapter697bd8d
V1.1Cosmetic truth passSidebar pill / topbar avatar / Home Card 2 — stop hardcoding values whose real source already exists36501e9
V1.2/api/integrationsReal env-var-presence probe replaces honor-system “I’ve set this up” mark. Home Card 4 num + subline reads live50bf291 + 3684d07
V1.3/api/settingsWerbos home / chats dir / default model / mirror surface from the actually-running servercf9aa31 + 41ce5d6
V1.4/api/agents CRUDAgentStore (one markdown file per agent at ~/werbos/agents/<id>.md) + 6 endpoints + dashboard swaps localStorage cache for /api/agents reads + tags fold into agent payloadca6536a → 98a64b4
V1.5Permissions enforcementTri-state verb gating actually fires at tool dispatch. Tool-registry perm-check + pending-action ask flow for the ask middle stateefe5778 + 0195c36
V1.6Models lifecycleTest / Pull (SSE-streamed ollama pull) / Remove for /api/models/{id:path} end-to-end. Backend inferred from id9e10c32
V1.8Channels routingsocial_channel_routing table + in-band /bind @handle slash + AgentDetail Connected-channels list. Inbounds route to the agent toggled for the channel59872c1
V1.7Scheduler daemonLargest 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 tests67f1555 → 546e2dd
V1.9/api/activityUnified-timeline aggregator over chats / tasks / runs. Home’s Recent-activity placeholder replaced with the live feed (kind chip + agent handle + status + timestamp). Last v1 stub991d13d + 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 with qwen2.5-coder and qwen2.5:14b. (1) Models emit a fenced ```json pseudo-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 (\\n instead 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; NOT unicode_escape, which would eat \\W regex 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 by status !== "completed", but TaskStore only ever writes pending | in_progress | waiting_human | done | failed. The string "completed" literally never appears, so the inequality always evaluated true and “Open tasks” rendered the entire tasks table. Symptom on SR8: showed 4 with zero tasks actually in flight. Fix: explicit allow-list of non-terminal statuses matches the store’s vocabulary directly.
  • b7140c5 — Topbar avatar already pulled from Profile.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. New stripPronouns() 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_scope is satisfied. The dashboard’s v1-scope surfaces (agents, perms, channels, scheduler, integrations, models, settings, activity) no longer have any localStorage-only / stub / fake / honor-system rows.
  • Two known opens remain in gap-hunting.md’s cosmetic-lie table — sidebar host pill hardcoded 127.0.0.1:8000, topbar avatar J. 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) and ce89e2d (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.