<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title>Vagner Bessa — Building Publicly</title><description>Progress updates from the projects I am building in public.</description><link>https://bessavagner.com/</link><item><title>White on white: making dark mode a token decision, and the palette that failed its own contrast test</title><link>https://bessavagner.com/building/turmarium/05-white-on-white-design-system/</link><guid isPermaLink="true">https://bessavagner.com/building/turmarium/05-white-on-white-design-system/</guid><description>Update 5 of the Turmarium build log. Turmarium is a multi-tenant B2B school-management SaaS, English-first with pt-BR first-class. Updates 1-4 proved tenant isolation, switched it on, built the academic catalog behind an import boundary, and fired the first domain event. Update 5 is the detour before the big feature epics: one web-led sprint that turns the working-but-unstyled scaffold into a real design system. Dark mode becomes a three-layer token decision (primitive to semantic to component); the styling engine the plan had locked (shadcn) gets swapped for daisyUI at the mandatory brainstorm; the chosen Azure palette fails its own WCAG contrast math; and the per-tenant accent ships as a seam with the pipeline deferred on purpose. 144 web tests, 130 backend tests, coverage 92.76% on the browser app and 97% on the API, import-linter still two contracts kept and zero broken.</description><pubDate>Tue, 21 Jul 2026 16:45:00 GMT</pubDate></item><item><title>A wall between tenants: RegWatch grows a secured DRF API</title><link>https://bessavagner.com/building/regwatch/05-a-wall-between-tenants/</link><guid isPermaLink="true">https://bessavagner.com/building/regwatch/05-a-wall-between-tenants/</guid><description>Update 5 of the RegWatch build log: the daily pipeline had a database full of matches nobody could reach, so I gave it an HTTP surface. Session auth, invite-only access, and a single workspace-scoping chokepoint that makes one firm&apos;s data 404 for another. Plus the secure-by-default reflex that crashed the batch jobs, and why one image now has to boot two ways.</description><pubDate>Mon, 20 Jul 2026 11:00:00 GMT</pubDate></item><item><title>Every falsy value went missing: adding nodriver to the stealth bench</title><link>https://bessavagner.com/building/stealthbench/03-every-falsy-value-went-missing/</link><guid isPermaLink="true">https://bessavagner.com/building/stealthbench/03-every-falsy-value-went-missing/</guid><description>Update 3 of the stealthbench build log. stealthbench scores browser-automation stealth setups against self-hosted bot detectors on your own localhost and commits every result. This update widens the matrix: two more detectors (a Sannysoft-style panel and rebrowser-bot-detector) and a fifth config, nodriver. nodriver is async and the bench&apos;s BrowserHandle contract is sync, so the new handle drives each call to completion on a private event loop. Wiring it in surfaced a real bug: nodriver&apos;s tab.evaluate(return_by_value=True) drops falsy results (its unwrap tests `if remote_object.value:`, so 0/false/&apos;&apos; vanish) and leaves plain objects wrapped in a RemoteObject, so detector signals came back as None or garbage. The fix is to stop fighting its unwrap: wrap the script in JSON.stringify and json.loads the returned string, which round-trips every value faithfully. With that fixed, the two new panels mostly corroborate the tells ranking (undetected-chromedriver and nodriver ace both, vanilla and selenium-stealth get dinged), and camoufox runs only 9 of rebrowser&apos;s 10 tests because one is Chrome-specific.</description><pubDate>Sat, 18 Jul 2026 12:00:00 GMT</pubDate></item><item><title>Shouting into an empty room: offerings, enrollments, and the first domain event I shipped with no subscriber</title><link>https://bessavagner.com/building/turmarium/04-shouting-into-an-empty-room/</link><guid isPermaLink="true">https://bessavagner.com/building/turmarium/04-shouting-into-an-empty-room/</guid><description>Update 4 of the Turmarium build log. Turmarium is a multi-tenant B2B school-management SaaS, English-first with pt-BR first-class. Updates 1-3 proved tenant isolation at the database, switched it on for real traffic, and built the academic catalog behind a strict import boundary. Update 4 makes the architecture finally do something: offerings (turmas, a discipline taught in a term to a section) and enrollments, two more tenant-scoped tables with forced RLS, plus the first domain event, EnrollmentCreated, emitted onto the event bus that has sat unused since update 1. The honest twist: the event fires into an empty room. No module subscribes yet (that is a later epic); only a test listens. Which is lucky, because writing this update is how I discovered the emitter does not do what I designed it to do: it fires inside the still-open request transaction, not after commit, and the in-process test that covers it cannot see the difference. PROTECT keeps a class you actually ran from being deleted by accident, limit_choices_to turns out to guard the form and not the API, and update 3&apos;s half-built syllabus revisit gets paid off first. 124 backend tests, 80 web tests, import-linter at two contracts kept and zero broken, coverage near 97% on the API and above the gate on the browser app.</description><pubDate>Fri, 17 Jul 2026 08:00:00 GMT</pubDate></item><item><title>Building on a moving runtime: I re-ran the finding my sprint conductor is built on</title><link>https://bessavagner.com/building/supskill/01-re-running-the-finding/</link><guid isPermaLink="true">https://bessavagner.com/building/supskill/01-re-running-the-finding/</guid><description>Update 1 of the supskill build log. supskill is a sprint conductor for Claude Code: it drives one sprint from a markdown backlog to a merged branch, drawing a fresh context boundary at every stage and holding three human gates, with all authoritative state on disk so it survives its own context death. The whole project rests on one finding: a delegated agent cannot ask you anything. Before writing this, I re-ran that finding on Claude Code 2.1.210, and the runtime had moved: AskUserQuestion is now unavailable inside a subagent and absent from a headless session&apos;s 62-tool manifest, and calling it returns a loud error instead of the silently-invented empty answer a cited issue reported. That makes supskill&apos;s core invariant stronger, not weaker: a delegated agent still has no channel to escalate, so nothing forces it to stop-and-ask rather than guess. This update shows the probe, the turn, and the on-disk state spine (280 offline tests) that makes a skipped gate structurally impossible.</description><pubDate>Thu, 16 Jul 2026 11:00:00 GMT</pubDate></item><item><title>Benchmarking stealth: score the range, not the number</title><link>https://bessavagner.com/building/stealthbench/02-scores-that-dont-repeat/</link><guid isPermaLink="true">https://bessavagner.com/building/stealthbench/02-scores-that-dont-repeat/</guid><description>Update 2 of the stealthbench build log. stealthbench scores browser-automation stealth setups against self-hosted bot detectors on your own localhost and commits every result. Update 1 reported one number per config; this update ran each config ten times and drew the spread on the chart. Two of the four scores don&apos;t repeat: stock Selenium and undetected-chromedriver wobble run to run (undetected-chromedriver even hits a clean 100% on some trials), while selenium-stealth and camoufox land on the same number every time. The bars are descriptive spread (min/max/mean/population stdev), deliberately not a confidence interval. Also in this update: the results schema bumped to v2 while still reading every v1 snapshot, a history layer aggregates committed snapshots into a trend, and run metadata records browser and driver versions from the environment (numbers only, no IP or fingerprint).</description><pubDate>Sat, 11 Jul 2026 12:00:00 GMT</pubDate></item><item><title>One seam, a whole new browser: adding a stealth Firefox to the bench with zero detector changes</title><link>https://bessavagner.com/building/stealthbench/01-a-second-browser-zero-detector-changes/</link><guid isPermaLink="true">https://bessavagner.com/building/stealthbench/01-a-second-browser-zero-detector-changes/</guid><description>Update 1 of the stealthbench build log. stealthbench is a reproducible benchmark that puts numbers on browser-automation stealth: it scores stealth configs (how you set up an automated browser) against self-hosted detectors (how a site decides you&apos;re a bot), on your own localhost, and commits every result. This update adds the fleet&apos;s first non-Chrome arm, a stealth Firefox (Camoufox driven through Playwright), and does it the way the architecture was built to allow: a new Config plus a new BrowserHandle adapter, with zero changes under detectors/. The BrowserHandle Protocol is the Ports and Adapters seam; a test (test_seam.py) fails the build if a browser SDK is imported outside configs/, and the sprint&apos;s proof is an empty `git status src/stealthbench/detectors/`. The one real roadblock was Firefox&apos;s Xray wrapper hiding the detector pages&apos; window globals, fixed by reaching through window.wrappedJSObject in the evaluate translation. Camoufox lands at 82% on the tells panel and passes BotD and CreepJS; the three tells it &apos;fails&apos; are Chrome-specific, so a genuine Firefox doesn&apos;t match them.</description><pubDate>Fri, 10 Jul 2026 12:00:00 GMT</pubDate></item><item><title>Good fences: the academic catalog, and the import boundary that broke once a module used it</title><link>https://bessavagner.com/building/turmarium/03-good-fences-import-boundary/</link><guid isPermaLink="true">https://bessavagner.com/building/turmarium/03-good-fences-import-boundary/</guid><description>Update 3 of the Turmarium build log. Turmarium is a multi-tenant B2B school-management SaaS, English-first with pt-BR first-class. Updates 1 and 2 proved and then switched on tenant isolation with Postgres row-level security. Update 3 builds the first real feature on top of it: the academic catalog (disciplines, syllabi with ordered units, terms with one current at a time), each a new tenant-scoped table with forced RLS, a django-ninja CRUD API, and browser screens driven off the generated OpenAPI client. It is also the first module to live behind the import boundary from update 1, and the boundary broke the moment the module imported anything real. The fix is a one-word import-linter flag, plus a probe test to prove the contract still catches a real violation. Along the way, a route that shadowed itself into a 405, and a syllabus you can create but can&apos;t navigate back to, both shipped honestly. 95 backend tests, 62 web tests, import-linter at two contracts kept and zero broken, coverage above the gate on both sides.</description><pubDate>Fri, 10 Jul 2026 08:00:00 GMT</pubDate></item><item><title>Unattended at last: RegWatch on Cloud Run, and the round-trip that nearly killed the job</title><link>https://bessavagner.com/building/regwatch/04-deploying-regwatch-to-cloud-run/</link><guid isPermaLink="true">https://bessavagner.com/building/regwatch/04-deploying-regwatch-to-cloud-run/</guid><description>Update 4 of the RegWatch build log: taking the daily pipeline off my laptop and onto GCP Cloud Run Jobs against Supabase. The plan was clean; the first live deploy was not. Three quick config failures, and then the one that taught me something: a cross-region round-trip that timed the job out at a full hour, until co-locating Cloud Run with the Supabase region dropped a whole gazette day to ten minutes.</description><pubDate>Thu, 09 Jul 2026 11:00:00 GMT</pubDate></item><item><title>The LLM judge that can&apos;t fail your build: an offline evaluator, advisory by default</title><link>https://bessavagner.com/building/replaygate/05-the-llm-judge-that-cant-fail-your-build/</link><guid isPermaLink="true">https://bessavagner.com/building/replaygate/05-the-llm-judge-that-cant-fail-your-build/</guid><description>Update 5 of the ReplayGate build log: adding an LLM-as-judge for the dimensions a deterministic predicate can&apos;t score (goal completion, relevance, tone), and then spending most of the sprint making sure it can never fail a build on its own. The judge is a real Claude call via forced tool_use, but its verdicts are recorded and replayed offline like every other call, keyed by the conversation and dimensions. `--judge` is advisory (prints scores, never touches the exit code); `--judge-gate` is a separate opt-in with its own exit code 4. Writing the post caught a bug in it: with no recorded verdict the gate found nothing below threshold and passed the build without running, so it now refuses with exit 2 rather than failing open. Plus the golden test that pinned the deterministic gate byte-for-byte before any of this was written, and why I flipped my own roadmap to build the judge before the WhatsApp channel.</description><pubDate>Wed, 08 Jul 2026 11:00:00 GMT</pubDate></item><item><title>Green tests, red browser: turning on tenant isolation for real traffic</title><link>https://bessavagner.com/building/turmarium/02-green-tests-red-browser/</link><guid isPermaLink="true">https://bessavagner.com/building/turmarium/02-green-tests-red-browser/</guid><description>Update 2 of the Turmarium build log. Turmarium is a multi-tenant B2B school-management SaaS, English-first with pt-BR first-class. Update 1 proved Postgres row-level security at the database but left the request-path middleware a no-op, so it shipped zero tenant-data endpoints on purpose, behind a written gate. Update 2 closes that gate: the RLS session variable now gets set inside JWT auth, the first tenant-data API (memberships and users) ships behind role guards, and a React + Vite + Tailwind front end drives login, forced password change, and org switching through a client generated off the OpenAPI schema. Hardening the policy turned up a Postgres cast that crashes during planning no matter what guards surround it. Then driving the finished app in a browser turned up two more bugs the green unit suite never saw: switch-org failing closed under its own RLS, and a CORS wall the front end hit on its first request. 66 backend tests, 97.58% coverage, and the isolation gate from update 1 finally closed.</description><pubDate>Wed, 08 Jul 2026 08:00:00 GMT</pubDate></item><item><title>Starting Turmarium: proving one school can&apos;t read another&apos;s data</title><link>https://bessavagner.com/building/turmarium/01-proving-tenant-isolation-at-the-database/</link><guid isPermaLink="true">https://bessavagner.com/building/turmarium/01-proving-tenant-isolation-at-the-database/</guid><description>Update 1 of a new build log. Turmarium is a multi-tenant B2B school-management SaaS, built English-first with pt-BR first-class, where the academic core is a stable spine and every future capability plugs in as a module through contracts and domain events, never by importing the core. This first slice is the foundation: a Django 6 + Ninja modular monolith, JWT auth with org/role claims, and tenant isolation proven at the database with Postgres row-level security. 19 tests, coverage 82%, and zero tenant-data endpoints yet, on purpose.</description><pubDate>Mon, 06 Jul 2026 08:00:00 GMT</pubDate></item><item><title>Pass, fail, or diverged: cross-version regression testing for AI agents</title><link>https://bessavagner.com/building/replaygate/04-cross-version-regress-pass-fail-or-diverged/</link><guid isPermaLink="true">https://bessavagner.com/building/replaygate/04-cross-version-regress-pass-fail-or-diverged/</guid><description>Update 4 of the ReplayGate build log: the regress gate from #3 grows the headline feature, replaying a fixture recorded from one agent against a different candidate. The catch is that a key-miss isn&apos;t a pass or a fail, it&apos;s a third outcome (the candidate left the recorded trajectory), resolved by a pinned/live policy and a distinct exit code 3. Three support-agent candidates demonstrate OK, DIVERGED, and FAILED against one recording. Plus the fixture-mutation bug a live fallback introduced, and finally wiring the OpenTelemetry spans I&apos;d deferred since #1 through the capture loop.</description><pubDate>Sat, 04 Jul 2026 08:00:00 GMT</pubDate></item><item><title>Caught in the act: a regress gate that fails CI when an agent books before you confirm</title><link>https://bessavagner.com/building/replaygate/03-the-regress-gate-catching-cross-turn-bugs/</link><guid isPermaLink="true">https://bessavagner.com/building/replaygate/03-the-regress-gate-catching-cross-turn-bugs/</guid><description>Update 3 of the ReplayGate build log: the record-and-replay from #1 and #2 becomes an actual regression test. A pure-function invariant suite judges a whole replayed conversation (booked-before-confirm, re-asked order id, forgotten dietary constraint), a replaygate regress command turns a violation into an exit code, and a GitHub Actions gate fails the build on a cross-turn bug. Plus the punctuation bug hiding in the flagship invariant&apos;s own happy path, and a review finding that made the CI gate honest.</description><pubDate>Thu, 02 Jul 2026 15:00:00 GMT</pubDate></item><item><title>From mocking to knocking: a real AI that builds the digest and knocks on the inbox</title><link>https://bessavagner.com/building/regwatch/03-the-daily-run-made-real/</link><guid isPermaLink="true">https://bessavagner.com/building/regwatch/03-the-daily-run-made-real/</guid><description>Update 3 of the RegWatch build log: turning a pipeline that was tested against fakes into a real daily run that can run unattended. A single command, real Anthropic and Resend integrations behind the existing interfaces, per-section resilience so one bad article can&apos;t sink the day, a run logbook, and an honest account of a feature I promised in #2 and chose to drop.</description><pubDate>Wed, 01 Jul 2026 09:00:00 GMT</pubDate></item><item><title>Now I can replay it: offline regression testing for multi-turn AI agents</title><link>https://bessavagner.com/building/replaygate/02-offline-regression-testing-for-ai-agents/</link><guid isPermaLink="true">https://bessavagner.com/building/replaygate/02-offline-regression-testing-for-ai-agents/</guid><description>Update 2 of the ReplayGate build log: the record half from #1 now replays. A recorded conversation re-runs offline with zero network (no provider SDK imported, no API key read), and the same capture seam records against five providers (Anthropic, OpenAI, OpenRouter, Ollama, Gemini) through one provider-agnostic client. With an honest note on the cross-turn detector that&apos;s still ahead.</description><pubDate>Tue, 30 Jun 2026 15:00:00 GMT</pubDate></item><item><title>Starting ReplayGate: recording an agent before I can replay it</title><link>https://bessavagner.com/building/replaygate/01-recording-an-agent-before-i-can-replay-it/</link><guid isPermaLink="true">https://bessavagner.com/building/replaygate/01-recording-an-agent-before-i-can-replay-it/</guid><description>Update 1 of a new build log. ReplayGate does conversation-level regression testing for multi-turn, channel-native AI agents — it records an agent&apos;s LLM and tool calls into a deterministic fixture, then replays them offline to catch the cross-turn bugs (like &apos;booked before the user confirmed&apos;) that per-turn tests miss. This first slice is the record half: the trace contract, a DuckDB span store, the record/replay wrappers, and a CLI — 20 tests, no network.</description><pubDate>Tue, 30 Jun 2026 11:00:00 GMT</pubDate></item><item><title>Fetching the gazette: a clean-room INlabs ingestor</title><link>https://bessavagner.com/building/regwatch/02-fetching-the-gazette-inlabs-ingestor/</link><guid isPermaLink="true">https://bessavagner.com/building/regwatch/02-fetching-the-gazette-inlabs-ingestor/</guid><description>Update 2 of the RegWatch build log: turning the validated INlabs path into a tested fetch_editions(date), with an lxml parser, an httpx client tested via MockTransport, and an honest note on the resilience I deferred.</description><pubDate>Sun, 28 Jun 2026 11:00:00 GMT</pubDate></item><item><title>Starting RegWatch: a clean-room core and a research spike</title><link>https://bessavagner.com/building/regwatch/01-starting-regwatch-clean-room-core/</link><guid isPermaLink="true">https://bessavagner.com/building/regwatch/01-starting-regwatch-clean-room-core/</guid><description>Kicking off a build-in-public log for RegWatch, a multi-client DOU watcher, with a TDD Postgres-FTS core and a research spike into how to fetch the gazette.</description><pubDate>Sat, 27 Jun 2026 00:00:00 GMT</pubDate></item></channel></rss>