Sprint 41 — walk every room


Sprint 40 ended with a confession: the plan had two halves, and the second one — walk every room before guests arrive — never started. So Sprint 41 was the walkthrough, nearly whole: every surface, both languages, empty states and error states, the help site held against the actual app, and the beta gate read line by line until it stopped being a list and became two items. Ninety-six PRs merged in nine days — nearly eleven a day, the highest-velocity sprint of the project — with eleven migrations (0132 through 0142) and eighteen production promotes carrying the build stamp from v1.0.131 to v1.0.142 (the nineteenth — the sprint-close promote — was mid-flight as I wrote this). And the theme was, of all things, tidying.

Walking the rooms

The walkthrough itself was less glamorous and more useful than any feature. What falls out of walking every screen in both languages: a shared error boundary so a crash renders an apology instead of a stack trace, a localized 404, six missing loading skeletons, first-run copy that actually explains the first run. The help site got the same treatment against the running app — six flatly wrong claims fixed on day one, thirteen stale ones the day after, then a coverage batch for pages that had features but no manual. The house rule that documentation must tell the truth is easy to state and expensive to keep; this was the keeping.

The daily digest got caught lying too, in a subtler way: the platform occasionally skips a scheduled tick, and the send gate was written as “is it exactly 10:00?” — so a skipped tick meant a silently skipped digest. The gate is now a catch-up (“is it 10:00 or later and we haven’t sent today?”), and the email itself got a structured brief and a proper unsubscribe footer. And because dogfooding never stops: school fee statements — the Quebec état de compte — now extract their money fields and get a full transcription, because one arrived and didn’t.

A warranty on the sofa

The one big planned feature: Purchases & Warranties. Upload a store invoice and the things on it — a sofa, a TV, a kid’s bike — become assets, each on its own confirm card: who it belongs to, what it lives inside, the vendor as a contact, the invoice linked as provenance. And each one gets a warranty: one year from purchase by default, editable to whatever the paper says (AppleCare’s three years, say), with a reminder thirty days before it expires — because a warranty you remember two weeks after the deadline is a story, not a warranty.

Asset types stopped being a hardcoded list on the way through — there’s a registry now, six new built-in kinds for purchased things, and a Settings page where a household can add its own. And existing assets got warranties backfilled from their earliest known dates — including the roughly ten-year legal warranty Quebec puts on a residence, which most people own without knowing they hold. Expired ones show a quiet badge, never a nag. Later in the sprint the warranties moved out of the Obligations list entirely, into a Protection section on the asset itself — a warranty belongs with the thing, not filed between the mortgage and the phone bill.

The front door, before guests

With real testers weeks away, the front door grew up. Sign-up is now gated by invite codes — you can’t wander in off the internet before the doors officially open — and the operator tooling matured behind the scenes: a durable login ledger, per-session controls, and the ability to properly suspend an account (sessions ended, background jobs paused) without destroying anything. Deliberately boring, deliberately reversible.

The satisfying one was defense-in-depth: row-level security in production is now enforced by the database itself. Every query has always been tenant-scoped in application code, and Postgres has carried RLS policies on every table since the early sprints — but the production connection role was privileged enough to bypass them, so the second lock existed without being locked. The runtime role got swapped for one that cannot bypass anything, verified live. Belt and suspenders, where the suspenders now actually hold weight: if some future query ever forgets its scoping clause, the database refuses it rather than trusting me.

Nine agents, thirteen errands, one day

Then came my favorite day of the sprint. I’d accumulated thirteen dogfood items — the kind of list that usually bleeds across two weeks — and instead ran nine coding agents in parallel, each in its own git worktree, with me as dispatcher and reviewer. Eleven feature PRs merged by evening. Among them:

  • Proposal packages are atomic. An invoice’s asset, warranty, and vendor contact now land together or not at all — no more half-applied cards leaving an asset without its warranty.
  • Company expense tracking, for the tax-season version of me: mark spending as claimable against an expense target, with a defaults ladder so recurring things classify themselves, and a year-end CSV my accountant can actually open.
  • In-app dialogs everywhere. Every native browser confirm() replaced with a real dialog — small, but it’s the difference between an app and a webpage.
  • Real tenant suspension, enforced everywhere — not a flag that hides a button, a status the whole system respects.
  • Model routing moved again, eval-gated as always: four roles promoted to GPT-5.6’s tiers, and a new rule I expect to keep — the planner role goes to the cheapest model that scores a perfect eval, not the fanciest one that clears the bar. If the small model gets 10/10, the small model gets the job.

The worktree pattern is the story here: nine agents sharing one repo without stepping on each other, migrations serialized through me, every PR still passing the full gate. Solo development stopped being singular a while ago; this was the day it felt industrial.

The gate, read again

Beta-gate accounting, start of sprint: six open items. End of sprint: two, and both are “submit the connector to a directory and wait for review.”

What closed: the first real second user joined the household — not a test account, a person I live with, holding their own sign-in, their own role, their own daily digest, on production. The invite-to-digest path that Sprint 39 debugged in theory got validated by the only test that counts. The measurement chapter closed — the numbers stand as the record, four of five bars met and the fifth accepted with its asterisk documented. And the definition-of-done paperwork is signed, which brings me to the weekend.

The paperwork weekend

The DoD required actually reading the threat model and privacy assessment end to end, and reading found things — corrections, stale claims, and one genuinely pleasant surprise: the entire at-rest encryption stack is symmetric-only. No RSA, no elliptic curves anywhere in the key hierarchy — which means the algorithms a future quantum computer breaks aren’t in that path at all. Domi’s data at rest is honestly quantum-resistant, and the docs now say so with the caveats attached: transport encryption isn’t post-quantum yet, and search embeddings remain the one disclosed plaintext residual. Claims with asterisks beat claims without.

The rest of the weekend: an incident-response runbook — who gets notified, in what order, with the register Quebec’s Law 25 requires kept as a living document rather than a scramble. The privacy reference now covers every Canadian province, the US and California, the EU with a French annex, and the UK, with a transfer assessment for the data that crosses borders. The privacy policy was rewritten to be read by an actual human — the accuracy pass happened sprints ago; this was the readability pass. The terms of service got truth-synced to what the system actually does today. And HSTS preload is submitted, so browsers will eventually refuse to ever speak plain HTTP to Domi. None of this is a feature. All of it is what “ready for guests” turns out to mean.

Ops interlude

Three small stories from the engine room. First: mid-sprint, every CI run started failing instantly — zero steps executed, no logs, just red. Not a broken workflow, not a bad merge: the GitHub Actions spending cap had run out, mid-dependabot-marathon. The most alarming-looking failure of the sprint was a billing page. Fixed with a budget bump and, more usefully, a CI fast path so bot dependency PRs skip the gates they can’t affect. Second, supply-chain hardening: pnpm 10, with dependency install scripts blocked by default behind an allowlist and a seven-day cooling-off period on newly published releases — freshly compromised packages mostly get caught in their first week, so Domi simply doesn’t install anything younger than that. Third, the accidental save: while sweeping database grants I noticed the audit and telemetry tables — partitioned by month — had partitions running out on December 1st, after which every insert would have failed. Found by accident, defused with a longer runway and a catch-all default partition. Some bugs you earn; some you trip over five months early.

The Gmail saga

And then the finale, which cost an evening and paid for itself in comedy. Real-time Gmail push — the thing where Google notifies Domi the moment a message arrives, instead of Domi checking on a schedule — had, it turns out, silently never worked. Not degraded: never. The scheduled sync was quietly covering for it the whole time, which is exactly why nobody noticed.

Excavated live from production logs, three failures stacked like sediment. Two were IAM grants, including my favorite absurdity of the project so far: the service account needed explicit permission to act as itself. Not as another account — as itself. Granted, obviously, once discovered. And then the killer, the one under the other two: the webhook was reading the tenant hint from the push message’s attributes — attributes I had dutifully configured on the subscription’s push config — and Google never delivers pushConfig attributes with the messages. They configure the endpoint; they do not ride along. So every push that ever arrived was dropped as unidentifiable, for months, while the fallback sync tidied up behind it.

Fixed the resolution to use what the message actually carries, and then the moment that made the evening worth it: sent myself an email, watched it land in Domi minutes later, no schedule involved. The whole excavation is now a five-piece runbook — topic, subscription, both grants, webhook — so it never costs an evening again.

Next

Sprint 42 opens the doors: the two directory submissions go out, and the first testers who don’t share my last name get their invite codes. In parallel, the mobile app scaffolding starts — the offline-first architecture got its design pass this sprint, and the phone is where a household app actually lives. The rooms are walked, the paperwork is signed, the push notifications push. Time to see who shows up.