Sprint M5 — the checklist that found things
For four sprints I have been writing some version of the same sentence: the release gates are still waiting for me, an hour, and a phone in airplane mode. This sprint I finally did it, and it took about twenty minutes to find two real bugs. I want to lead with that, because the lesson is not “testing is good” — everybody knows that. The lesson is that the hour I kept not spending was the cheapest hour available, and I kept not spending it because there was always something more obviously productive to do instead.
Fifteen PRs merged over the window (August 22–24), 379 tests up from 283, one new binary on TestFlight, seven production over-the-air updates. Weather shipped whole. And the list standing between Domi and the App Store is now four items long, all of them specific.
Weather, in an afternoon
The spec was mine — 1128 lines of it, written weeks ago — and the design agent had layered a visual pass on top. What I did not expect was how much of the sprint’s value came from the two places where the agents disagreed with the brief.
The spec said add Weather as a sixth item in the bottom navigation. The design agent refused, and made the better argument: iOS caps a tab bar at five, and we had just spent a day measuring French labels into the five we have. Weather belongs on the Today screen — a strip you see every morning rather than a destination you navigate to. That is not just cheaper; it is the product. Weather in Domi is context for the day, not a dashboard.
The spec also read as though the phone should call the weather provider directly. The mobile agent pushed back on that one before writing any code: it would send my household’s location to a third party outside our own privacy posture, turn a provider outage into a Domi outage, and put the provider’s rate limits on every device rather than on one server. The forecast now comes from Domi’s own backend. My family’s coordinates never leave.
Then, halfway through, I changed weather providers — MET Norway to Open-Meteo. The backend agent announced the change as four precise deltas before merging anything, and one of them caught a bug the mobile side would never have found alone: the app’s fallback attribution line named MET Norway by hand, so on switchover day it would have printed the wrong company’s name under someone else’s data. Under a Creative Commons licence that is not a cosmetic problem.
The nicest detail is one nobody would notice: precipitation percentages now exist for Canada at all. MET only produces them for the Nordics, so that column had been permanently empty for every Canadian user — and would have shown a page of dashes forever if we had rendered it as specified.
The icons stopped being punctuation
Until this sprint, Domi’s tab bar was five typographic characters: ☀︎ ▦ ≣ ✎ ⚙︎. It looked, at a glance, like an app that had not been finished, which is exactly what it was.
Once weather became part of version 1, the vector library that had been an optional preference became necessary — twenty condition icons at three sizes cannot be maintained as bitmaps, and the design contract calls for a stroke that stays 1.5 pixels at any size, which a scaled bitmap physically cannot do. So the whole set went vector at once: the five tab icons and the Domi mark drawn from the design system’s own path data, the weather glyphs from a library, because a sun should look like a sun and recognisability beats distinctiveness at 24 pixels.
Then I ran the gates
Ten offline release gates. Airplane-mode cold start, offline capture, process-death recovery, reconnect convergence, conflict handling, revocation, encryption, reinstall, reminders. Written weeks ago. Never once run.
Six passed cleanly, and watching them pass was its own reward — force-quitting the app in airplane mode after capturing three things and finding the pending count unchanged is a nice feeling when you have only ever asserted it in a test.
Gate 1 found chat empty offline. Today, Calendar and Lists all rendered instantly; chat showed nothing. It turned out the transcript had been cached correctly the whole time — but the app resolves the conversation’s id over the network, and the screen gave up when that failed, so a cold start offline never reached the cache it already had. The in-process shortcut that hides this does not survive a force-quit, which is precisely what the gate does.
The better finding was about the checklist itself. Its instructions named Today, Calendar and Lists and never mentioned Chat. A tester following the gate exactly would have passed this. I found it by poking at the app instead of reading my own list.
Gate 9 was worse, and more instructive. Delete the app. Reinstall from TestFlight. Domi asked for Face ID, accepted it, and let me straight into my household. The gate requires a reinstall to land on the sign-in screen with nothing left over.
The cause is iOS working exactly as documented: deleting an app removes its container — so the encrypted database goes — but Keychain items survive. The reinstalled app came up holding valid tokens, the old database key, and my app-lock preference. It read a session, saw the lock enabled, asked for Face ID, and rebuilt its cache from the server. Every individual step was correct. The sequence was a hole.
Gate 7 was not ours at all. Revoking my phone’s session from the web left the phone working — and I could still create a shopping list, which reached the server. It turns out revocation kills the ability to refresh a token but not the token itself, which is a bounded and defensible design; what was not defensible is that the mobile routes never checked revocation at all, so a revoked device could keep writing for the token’s remaining life. The backend fix is written and merged. It ships before version 1.
Eleven for eleven
The other thing that arrived this sprint is an AI reviewer on every pull request — deliberately a different model family from the agents writing the code, because a reviewer that shares the author’s blind spots is decoration.
On its first day it produced eleven findings and all eleven were real. No false positives, and none of them stylistic.
Three of them were bugs introduced while fixing its previous finding — an await added for correctness opening a gap somewhere else, three times in a row. Two were tests that would have passed with the production code deleted, written by the same agent that had, hours earlier, put “a test that self-skips the path it claims to cover” into the reviewer’s own instructions.
And the single best one: after the gate 9 fix was written, the reviewer pointed out that it would not have fired on the gate 9 re-run. The marker it relies on does not exist in the build currently on my phone, so deleting that build would leave credentials with nothing to compare them against — and the guard would wave them through. It had been tested against the world it creates rather than the world it inherits. I would have re-run the gate, watched it fail, and drawn entirely the wrong conclusion.
That is the argument for a second reader, and it is not about catching typos.
Very close now
The list between here and the App Store is four items, and I can read it out loud without checking notes.
The backend revocation fix needs a promote to production. Then the 1.0.0 build gets cut — and three of the gates want that specific binary, because the reinstall guard has to ship inside the app rather than arriving afterwards. Then an accessibility and French pass on a real device, at the largest text sizes. Then the mechanical run: demo account for the reviewer, six screenshots in each language, paste the listing into App Store Connect, submit.
Everything else is done. The app has its face, its weather, its icons, its French, and a test suite that has grown from 38 to 379 in a fortnight.
Next sprint should be the one. I have said versions of that before and been wrong, so here is what is different: the remaining work is no longer discovery. There is nothing left on the list that might turn out to be three days instead of an hour. It is a promote, a build, a device pass, and a form.