01The sprint in one paragraph
Fix, in the order a stranger on a phone meets them, the four things the live product does wrong — Deepstate's audio playing over the chess card, the white "Paused" modal that fires on both live cards at 20 s because neither posts {blitz:'touch'}, no way back on a phone, and the confirm-email step — then instrument the loop with attribution the client cannot fake (entry_source from ?via=, ua_class derived inside ingest_batch from the request's user-agent, ui_event for like/share/back/end/loop, boot_ms), put the numbers on a /stats page the founder can open on a phone, and send one hundred people from the founder's network through one tagged link in two waves into a six-to-eight-card deck of sites the founder may frame. The read on Day 14 is D1 with its interval, beside the four numbers that are readable at n≈100 (position-0 hold, reached-end, external idle share, back taps), so the decision forks cleanly: card 0 and the chrome, or the length of the deck. STRATEGY.md's bet 5 (the factory), "Ripple Tap opens for everyone", the card_editorial rewrite, bundled boot of card 0, the wasm idle redesign and composition v0 are obsolete for the product as it is (two linked sites, a hidden catalogue); this sprint is bet 1's floor, bet 2's external-idle rule and bet 3's instrumentation scoped to what is live. No bench, no harvest script, no concierge, no save channel until the read says which of them is the constraint.
Since the plan was drafted (8 September, evening): the web-card sleep work is already on main and live (commits 0c8eec1, 9962fc4): a card BLITZ leaves is unloaded, so Deepstate no longer plays over the chess card. And a linked itch.io game surfaced the mirror problem: itch's game page refuses framing, the mirror stripped that and served it anyway, and itch's own shell answered with its hotlink notice, which is now the third live card. That is item 0 below, and it is Day 1.
The goal this serves. Blitz is for billions of people discovering new AI-generated content. This sprint is the first honest measurement of whether a stranger stays and comes back; the supply under test is pages a model wrote, uploaded by the person who asked for them, next to the two linked sites. So the founder's deck below is mostly AI-generated pages, and the linked sites are the comparison, not the thesis.
Days are engineering days for one engineer with an agent, verification included. Every item has a done-when an agent can check.
Item 0
Mirror only what consents; translate platform links to their embed endpoints; fix the itch card
1 day
Why
A linked itch.io game page sends frame-ancestors 'self' https://itch.io. embed_check marks every such site mirror, and the edge function strips the header and serves the page from our origin; itch's game shell then sees a non-itch parent and shows "the site you loaded the game on tried to steal or hotlink it". That is the card live at position 2 today. Mirror mode was built for the founder's own hosts (docs/EMBEDDING-A-SITE.md says so); applied to a stranger's site it is the StumbleUpon mistake, and it does not even work. itch publishes itch.io/embed-upload/<upload id> for third-party embedding: fetched, it answers 200 with no framing headers and wraps the same game, and framed from a foreign origin it shows itch's player, not the notice (verified in a browser on 8 September).
Done when
Migration 0016 (20260909080000_mirror_consent.sql): a mirror_host(host, added_by, note) table seeded with chess-program-tutor.netlify.app and deepstate-demo.netlify.app; embed_check returns mirror: true only for hosts in it, and for any other frame-refusing site returns ok: false with the reason "This site does not allow itself to be shown inside other sites. If it is yours, add <meta name="blitz-embed" content="allow"> to the page; if it is on itch.io, link its embed address." (the meta-tag check reads the body embed_check already fetches, and a host that carries it is treated as consenting). submit_link translates known platforms before the check: an *.itch.io/<game> page whose body names html-classic.itch.zone/html/<id>/index.html becomes https://itch.io/embed-upload/<id>?color=0b0e14; shadertoy.com/view/<id> becomes /embed/<id>?gui=true&paused=false; openprocessing.org/sketch/<id> becomes /sketch/<id>/embed/; codepen.io/<user>/pen/<id> becomes /<user>/embed/<id>?default-tab=result; scratch.mit.edu/projects/<id> becomes /projects/<id>/embed; editor.p5js.org/<user>/sketches/<id> becomes /<user>/full/<id> (each translation recorded on the row as platform). embed_check refuses a body that carries a known hotlink notice ("You should be using itch.io"). The existing dborta1-itch-io-2a4819 card is updated in the same migration to the embed address with embed_mode = 'frame'. netlify/edge-functions/mirror.js refuses hosts not in mirror_host (read through a new mirror_hosts() RPC or the same table via the publishable key) even when a card exists. MAX_HTML_BYTES rises to 25 MB (MONOLITH is one 5 MB index.html). docs/EMBEDDING-A-SITE.md gets the consent rule and the takedown line. The chain applies three times on a fresh Postgres 15; a unit test covers every translator; a phone opens the itch card playing.
Touches
supabase/migrations/20260909080000_mirror_consent.sql, netlify/edge-functions/mirror.js, src/api/media.ts, src/components/LinkSite.tsx (the refusal text), docs/EMBEDDING-A-SITE.md, tests/unit/.
Item 1
Day-1 baseline before any code, migration 0018 (metric_snapshot), and Deploy 1
0.75 days
Why
No row of position-0 hold, depth, idle share or D1 has ever been written, and the idle-policy deploy on Day 2 destroys the old numbers. The sleep work (a card left behind is unloaded, so Deepstate's audio stops over the chess card) is already on main and live; Deploy 1 carries item 0 and the two migrations. metric_snapshot makes the baseline durable without a migration that reads production data (that would be unreproducible on a fresh chain).
Done when
tools/sql/retention-readout.sql runs in the SQL editor against card_event/card_session as they are today and returns, for Sep 1–8: first-visit actors per day; position-0 hold (feed_position 0, span_ms >= 5000, exit_condition <> 'CRASH') on each actor's first session; median max(visit_ordinal) per visit_id; IDLE_TIMEOUT share of exits by card.source; SWIPE_DOWN share; D1 by first-seen day — and the numbers sit in a new docs/RETENTION.md next to the targets with a Day-10 and Day-14 column left empty. supabase/migrations/20260909090000_metric_snapshot.sql creates metric_snapshot(name, metric, value numeric, n integer, note, taken_at) with client grants revoked, plus metric_snapshot_read() behind is_admin(); the readout script's last block is the insert (name='baseline'). The chain applies three times on a fresh Postgres 15 with tools/pg-verify.sh (the scratchpad's pg-verify.sh committed to the repo, http extension included). CI green (typecheck, vitest, build:web, Playwright including web-card-sleep.spec.ts and focus-steal.spec.ts); Netlify has published; the Migrate workflow shows 0016 and 0018 applied; select * from metric_snapshot_read() returns >= 6 rows each with n; on a phone BLITZ from Deepstate to chess silences Deepstate and back reloads it (state loss on return accepted, already written in DESIGN-NOTES section 3).
Touches
tools/sql/retention-readout.sql (new), docs/RETENTION.md (new), supabase/migrations/20260909090000_metric_snapshot.sql (new), tools/pg-verify.sh (new, from the scratchpad)
Item 2
External cards get no idle timer and never show the Paused modal; uploaded clips keep alive while playing
1 day
Why
Both live cards are external and never post {blitz:'touch'}, so anyone who plays for 20 s gets 'Paused — Tap to keep playing' over the game and a session that ends IDLE_TIMEOUT; every pilot dwell on card 0 would be capped at the timer and the pilot would be blind on its own first card. STRATEGY bet 2's rule: an external card has no timer; its session ends only on BLITZ, back, TAB_HIDDEN or DESTROYED. Hosted cards keep the 20 s sweep (the html viewer's injected probe already posts touches; the badge is untouched this sprint so telemetry.spec.ts's 'Paused' contract stays). Uploaded videos are a primary path and hit the same wall at 20 s while a clip plays, so the media viewer gets a keepalive — the one part of this item that is cut first if the day runs out.
Done when
GameMeta (src/lib/protocol.ts) gains source: 'wasm'|'hosted'|'external'; cardRowToMeta (src/api/cards.ts) sets it from CardRow.source (mirror counts as external), src/api/mockGames.ts sets deepstate-demo external, and when absent it is derived from the page_url origin exactly as sandboxTokensFor does. The idle interval in Carousel.tsx (the block at IDLE_TIMEOUT_MS / ?idle=) skips hosts whose meta.source === 'external'; GameViewer.tsx never renders the Paused state-badge for an external host. BlitzCardMessage gains 'alive' (WebCardHost.onWindowMessage resets lastInputAt, does not increment reportedTouches) and public/cards/media/index.html posts it on timeupdate at most once a second while a video is playing. New tests/e2e/idle-policy.spec.ts with ?idle=1500: an external fixture on a second origin (a card whose page_url is http://127.0.0.1:4173/... against the localhost preview, inserted through __blitz's api.insertGame) runs 5 s with no IDLE_TIMEOUT batch and no .state-badge; a hosted card under ?own=1 with no probe still exits IDLE_TIMEOUT and shows 'Paused' (telemetry.spec.ts's idle case stays green, unchanged); a playing clip fixture stays 'running' past the timeout. docs/DESIGN-NOTES.md section 5 gains the external exception. Post-deploy: select count(*) from card_session s join card c using (card_id) where c.source = 'external' and s.exit_condition = 'IDLE_TIMEOUT' and s.first_ts > '<Deploy 2 ts>' returns 0 (this query is the Day-3 gate).
Touches
src/lib/protocol.ts, src/api/cards.ts, src/api/mockGames.ts, src/components/Carousel.tsx, src/components/GameViewer.tsx, src/lib/WebCardHost.ts, public/cards/media/index.html, tests/e2e/idle-policy.spec.ts (new), docs/DESIGN-NOTES.md
Depends on
Deploy 1
Item 3
A back chevron beside BLITZ, on phones — the one back affordance
0.75 days
Why
The only way back today is .nav-arrows, display:none except under @media (pointer: fine): structurally invisible on every phone, so a person who passes something they liked can never return and SWIPE_DOWN is 0% of exits on touch. Founder rulings: BLITZ is the only way forward, no sentinel history entry, hardware Back untouched; the chevron lives in the parent DOM (the one control content cannot capture) and is the way back and the only one. The e2e default project is already an iPhone SE touch viewport, so the spec needs no extra device.
Done when
TelemetryDisplay.tsx renders a 36 px chevron with a 48 px hit area at the right end of the left column (BLITZ stays dead centre; the columns stay equal), aria-label 'Previous card', disabled and dimmed at index 0, calling a new onBack prop that App.tsx wires to api.prev() (Carousel goTo(i-1), whose leave() already records SWIPE_DOWN); from the end card it returns to the last card. The .nav-arrows block in Carousel.tsx and index.css is deleted so there is exactly one back control (helpers.ts swipe('down') and focus-steal.spec.ts already use __blitz.prev() and are unaffected). New tests/e2e/back-chevron.spec.ts on the default touch project: chevron visible and disabled at 0; from index 1 a tap makes index 0 active and the leaving card's final batch carries exit_condition 'SWIPE_DOWN'; from the end slide it lands on the last card; browser history length is unchanged. Founder confirms on a phone that the chevron and BLITZ hit areas do not overlap. Its tap logs ui_event 'back' once item 4 lands (the logUi call is added there).
Touches
src/components/TelemetryDisplay.tsx, src/App.tsx (onBack -> api()?.prev(), canBack = index > 0), src/components/Carousel.tsx (remove .nav-arrows), src/index.css (.blitz-back; delete .nav-arrows), tests/e2e/back-chevron.spec.ts (new)
Depends on
Deploy 2 and the Day-3 gate (external IDLE_TIMEOUT = 0)
Item 4
Migration 0019: ui_event + log_ui, entry_source and server-derived ua_class on card_event, boot_ms, retention_daily(days, via), internal-actor exclusions in stats_daily, reserved handles — and the client stamps
2.5 days
Why
Like is a local boolean, Share taps vanish, the chevron's use and the end card's reach are invisible, pilot links cannot be attributed, and in-app webviews (Discord, Reddit) would poison a D1 read. ingest_batch ignores payload keys it has no column for, so client stamps land only once the columns exist — and its last body is in 0005 (20260908120000_card_rank.sql), which carries ip_hash, feed_position, visit_ordinal and visit_id; re-creating from any earlier body would lose them. ua_class is derived inside the RPC from the request's user-agent (the pattern request_ip_hash() already uses) so a client cannot claim to be a browser. stats_daily counts the founder's own tours as DAU today. Bet 3's bench/provenance/card_status_log/house/growth_weekly columns are deliberately left out: no supply or composition work ships this sprint, so they would be schema with no reader.
Done when
supabase/migrations/20260914090000_first_minute_instrumentation.sql: (a) ui_event(event_id, server_ts, actor_id from current_actor(), card_id nullable for end/loop, kind check in ('like','unlike','share','back','end','loop','link_open'), visit_id uuid, feed_position int, entry_surface, entry_source, ua_class, ip_hash) with all client grants revoked and the 0006-style partition revoke block; log_ui(p_kind text, p_card_id text, p_visit_id uuid, p_feed_position int) SECURITY DEFINER, granted to authenticated only, charging one ingest_bucket token, stamping ua_class and ip_hash server-side; (b) card_event gains entry_source text, ua_class text check in ('browser','webview','headless'), boot_ms integer; ingest_batch re-created from 0005's body plus those three — entry_source accepted only if ~ '^[a-z0-9_-]{1,32}$', ua_class computed once per call from current_setting('request.headers', true)::json->>'user-agent' ('headless' on HeadlessChrome|Playwright, 'webview' on wv)|FBAN|FBAV|Instagram|Discord|Reddit|Line/|TikTok, else 'browser'), never from the payload; boot_ms stored only where visit_ordinal = 1; (c) card_session re-created with visit_id, visit_ordinal, entry_source, ua_class, boot_ms and card_source (join card.source); card_rank() still ranks deepstate-demo 0 and chess-program-tutor 1 after apply (SQL assertion in tools/pg-verify.sh); (d) retention_daily(days int default 14, via text default null) behind is_admin(), per UTC day: first_visits_browser, pos0_hold (rate + n), reached_end (visits with a ui_event 'end' / visits), median_depth, back_taps_per_visit, share_taps, likes, idle_share_external, idle_share_hosted, webview_share, boot_ms_p50, d1 (rate + n), excluding actor.is_internal, card_session.is_probe and ua_class = 'headless'; stats_daily and stats_totals gain the same exclusions; (e) insert the founder's short reserved-handle list into reserved_handle. Verified by tools/pg-verify.sh (chain x3 on fresh Postgres 15): an anon JWT log_ui inserts with the server's actor; select on ui_event is permission denied for anon and authenticated; a batch with ua_class in the payload does not override the header-derived class; an invalid via is stored null. Client: src/lib/share.ts captureEntry reads ?via= beside ?r= into sessionStorage, entryContext() returns entry_source, syncUrlToCard drops 'via'; shareCard text becomes 'Play this.' (nothing reports a score); TelemetryTransport stamps entry_source on every batch and boot_ms (performance.now() at the first 'running' of the visit, set in App.tsx) on visit_ordinal 1 only; new src/api/ui.ts logUi(kind, cardId) -> rpc('log_ui') with visitId() and the carousel position, fire-and-forget, pushing to __blitz's uiLog when there is no backend; GameViewer logs like/unlike and share on 'shared'|'copied'; Carousel/App log back (chevron), end (end slide becomes active), loop (restart), link_open (end card's Link something). tests/unit/share.test.ts covers via capture and an invalid via dropped; new tests/e2e/ui-events.spec.ts: /c/deepstate-demo?via=e2e&r=u_test, Like, Share, chevron, end, loop -> telemetryLog rows carry entry_source='e2e' and boot_ms on ordinal 1 only, uiLog lists like, share, back, end, loop with card_id and visit_id. Production tour after the Migrate workflow applies: /feed?via=pilot&r=<uid> rows carry entry_source='pilot', ua_class='browser', boot_ms on ordinal 1; a headless tour's rows carry 'headless'; select * from retention_daily(14) returns every column for an admin and 42501 for anyone else. docs/ACCOUNTS.md (reserved handles, confirm email) and docs/CONTENT.md's numbers table updated.
Touches
supabase/migrations/20260914090000_first_minute_instrumentation.sql (new), tools/pg-verify.sh, src/lib/share.ts, src/telemetry/TelemetryTransport.ts, src/telemetry/TelemetryTypes.ts, src/api/ui.ts (new), src/api/stats.ts, src/components/GameViewer.tsx, src/components/Carousel.tsx, src/App.tsx, tests/unit/share.test.ts, tests/e2e/ui-events.spec.ts (new), docs/ACCOUNTS.md, docs/CONTENT.md
Depends on
Item 3 (the chevron exists to log); the founder's reserved-handle list by Day 6
Item 5
/stats: the numbers on a page the founder can open on a phone
0.75 days
Why
DAU lives in a table inside the profile sheet and everything else in the SQL editor; the founder wants to track DAU and read the pilot on a shareable page. Admin-only aggregates (the 0014 pattern); /stats mounts no carousel or transport, so opening it never starts a card session on card 0 — which every read from the feed does today. This replaces the static /retention build proposed earlier.
Done when
public/_redirects and netlify.toml both gain '/stats /index.html 200' and '/stats/ /index.html 200' before the /c/* rule, in the same order (the file's own rule). src/index.tsx renders src/components/StatsPage.tsx instead of App when location.pathname is /stats, so no Carousel, transport or feed resource is constructed. The page shows stats_daily(14) (DAU, new, acct, sessions), retention_daily(14, via) with a via filter (all / pilot / landing / endcard) and the metric_snapshot baseline as a column beside each number, card_rank(14) for the live cards (rank, votes, hold_ucb, demotion — is Deepstate outvoted yet, shown from data), a 'Mark this browser internal' button calling set_internal(true), and 'Admins only' plus the sign-in door for anyone else; AccountSheet's StatsPanel links to /stats. tests/e2e/stats.spec.ts: a non-admin sees 'Admins only' and telemetryLog stays empty after 5 s on /stats; a unit test covers the page's rendering of a fixture JSON from the three RPCs. The founder opens /stats on the phone as @rand and sees the baseline beside live numbers. No chart, no target column — the JSON in tables is the read.
Touches
public/_redirects, netlify.toml, src/index.tsx, src/components/StatsPage.tsx (new), src/api/stats.ts, src/components/AccountSheet.tsx, tests/e2e/stats.spec.ts (new), tests/unit/stats.test.ts (new)
Depends on
Item 4 deployed
Item 6
The end card as the way back: the webview exit line and Share Blitz
0.5 days
Why
The end card arrives on the seventh press and is the last thing a visitor sees; today it offers Link something and Back to the top. A Discord or Reddit webview visitor who later opens Blitz from Safari is a stranger to us (the webview's storage is not Safari's), and nothing on the end card sends anyone. STRATEGY section 3's end-of-deck lines, scoped; add-to-home-screen is deliberately left out (no pilot arrival reaches a second visit inside the window).
Done when
New src/lib/ua.ts classifies the client's own user agent (browser / webview / headless — used for this line only; the recorded class is the server's). When it says 'webview' the end card's first line is 'Open this in your browser to keep your place' with a working Copy address button. A 'Share Blitz' button shares /feed?via=endcard&r=<uid> with text 'Play this.' and logs ui_event 'share' with card_id null. tests/e2e/end-card.spec.ts gains: the copy line appears under a Discord/Instagram user agent and not under the default; Share Blitz calls the share path and a 'share' ui event lands in uiLog; the existing two cases stay green. manifest start_url stays /feed. Founder reads it on a phone.
Touches
src/components/Carousel.tsx (end-slide), src/App.tsx, src/lib/ua.ts (new), src/index.css, tests/e2e/end-card.spec.ts
Depends on
Item 4
Item 7
The landing tells the truth: Open the feed first, ?via=landing on every door, no promises the feed cannot keep
0.5 days
Why
The front door promises 'Three kinds of thing' — Games, Toys & art, Web pages with Cannae, a data explorer and a quiz — '3 card types' and '<1s to first play' over a feed of linked sites and uploads, and its primary CTA is Create account. A stranger who reads a promise the feed cannot keep bounces at card 0. Arrivals by source cannot be read if the landing's own links are untagged. The landing is a static page and pilot links go straight to /feed, so this can ship on the fix day without touching the cohort's build.
Done when
In public/landing/index.html the hero's primary CTA is 'Open the feed' -> /feed?via=landing and 'Create account' moves to the creators section (the data-when-signed-in behaviour kept); every /feed and /c/ href on the page carries ?via=landing (the auth-callback forward keeps its own query); #kinds reads 'Sites people link', 'Pages people upload', 'Photos and clips' with no Cannae / data-explorer / quiz promises; the '3 card types' and '<1s to first play' facts are deleted (the boot_ms p50 replaces the latter in a later sprint, once there is a week of data); the 'Dev tools' footer link is gone (the toolbar stays behind ?dev=1). tests/e2e/pages.spec.ts asserts the hero CTA href, that every /feed link carries via=landing, and the absence of 'Dev tools', 'Three kinds' and '3 card types'. After the deploy, retention_daily shows entry_source='landing' arrivals. The founder reads it on a phone.
Touches
public/landing/index.html, tests/e2e/pages.spec.ts
Depends on
Item 4 (captureEntry reads via)
Item 8
The pilot-build gate: column-check tour, linked-card thumbnails, card_rank assertion
0.5 days
Why
A pilot sent into a build whose stamps do not land is a pilot wasted, and the informal 'production tour' is not a check an agent can fail. The generic /cards/linked/thumb.svg is what every pilot visitor sees in the slide-thumb for the second before a frame paints, on every linked card; a phone-sized screenshot is an hour's work. After every migration the ranking must be unchanged.
Done when
tools/column-check.sql (run in the SQL editor after a fresh headless tour of the live site through /feed?via=e2e-tour&r=<uid> that taps Like, Share, the chevron, reaches the end card and loops) returns zero missing for that actor: entry_source='e2e-tour', ua_class='headless', boot_ms non-null on visit_ordinal 1 and null elsewhere, one ui_event per rail tap, exit_condition SWIPE_DOWN on the chevron session, an 'end' and a 'loop' event; and select rank, card_id from card_rank(14) where rank <= 1 still returns deepstate-demo, chess-program-tutor. For each linked card the founder's 375x667 phone screenshot is committed at public/cards/linked/<slug>.jpg (<= 40 KB, q60) and update public.card set thumbnail_url = '/cards/linked/<slug>.jpg' where card_id = '<slug>' is run in the SQL editor (card is closed to clients); the slide-thumb shows it on the phone. This gate passes before the first pilot link goes out.
Touches
tools/column-check.sql (new), public/cards/linked/<slug>.jpg (new, per card), the SQL editor
Depends on
Items 4, 5, 6 deployed; the founder's screenshots
Item 9
Wave-1 fix day (reserved): whatever the first fifty show, P0 only
1 day
Why
The strategy's own rule is that what wave 1 shows is fixed before more traffic is spent, and the first fifty strangers will surface what no tour did (a blank frame on a linked card, a lost boot_ms, a webview-heavy arrival mix, a chevron nobody finds). A day is reserved for it rather than pretending it will not happen; if nothing is P0, the day goes to the landing (item 7) and the docs.
Done when
docs/RETENTION.md has the Day-10 wave-1 row (position-0 hold, reached-end, back taps per visit, share taps, external idle share, webview share, boot_ms p50, ua_class mix, n) written before any fix starts; each fix has a failing test first, e2e green, is deployed the same day (Deploy 7) and phone-checked within the hour; the wave-2 gate query (external IDLE_TIMEOUT since Deploy 2 = 0; boot_ms present on >= 90% of ordinal-1 rows since Deploy 4; column-check still zero missing) passes before Day 11; anything not P0 is listed under 'carried to sprint 2' in RETENTION.md.
Touches
whatever wave 1 names; docs/RETENTION.md
Depends on
Wave 1 sent
Item 10
Docs and the readout query, during the freeze
0.5 days
Why
The readout is the sprint's deliverable and must not be written on the morning it is read; docs are the only thing that ships during the freeze because they do not change the client bundle.
Done when
tools/sql/retention-readout.sql gains the Day-14 block (retention_daily(14,'pilot') and (14,null) plus the binomial interval for D1 and pos-0 hold at the observed n), rehearsed on Day 13 against real rows; docs/CONTENT.md's numbers table gains entry_source, ua_class, boot_ms, ui_event and retention_daily; docs/RANKING.md 'what the client stamps' gains the three columns and one line 'ui_event never ranks'; docs/DESIGN-NOTES.md section 5 carries the external idle exception; docs/EMBEDDING-A-SITE.md gains 'Platform embed endpoints are fair game; a maker's own page is asked first; mirror mode is only for owners who said yes — never for a harvested card' and a takedown line ('yours and you want it gone? reply remove and it is hidden the same day').
Touches
tools/sql/retention-readout.sql, docs/CONTENT.md, docs/RANKING.md, docs/DESIGN-NOTES.md, docs/EMBEDDING-A-SITE.md
Depends on
Item 4
Total: about 9.75 engineering days across the fourteen, with the weekend days deliberately deploy-free.
The review gate: a phone check of every deploy, and the three dashboard moves (4 h)
Why. Nothing goes live unreviewed on a phone. Confirm email is a project setting only the founder can flip and it costs a step in Create account; the reserved-handle list is the founder's to write; internal-actor marking has no UI until /stats ships, and a tour that votes poisons card_rank.
Done when. Each of the six planned deploys (and Deploy 7 if it happens) has a one-line phone verdict in docs/RETENTION.md within the hour of publishing (rollback is Netlify's previous deploy); on Day 1 update public.actor set is_internal = true where handle = 'rand' is run and the founder signs in as @rand on every phone and browser used for tours (same auth user, same actor) — from Day 8 the /stats button covers anonymous tour browsers; the reserved-handle list (blitz, blitzlabs, admin, rand variants, and whatever else the founder wants held) is given to the agent by Day 6; on Day 7, after 0019 is applied, Authentication -> Providers -> Email -> Confirm email is OFF (mailer_autoconfirm true) and Create account -> handle -> + is confirmed as one screen on a phone.
Founder 1
Six to eight cards, mostly pages a model wrote, each opened on a phone first
9 h
Why
With two cards the end card comes on the second press and there is nothing to come back for. The founder's rule is user-linked sites, not generated toys, and a maker is asked before framing — so in a fortnight the supply is platform embed endpoints built for third parties (fair game per STRATEGY section 9) and the founder's own AI-written pages through the upload path, each judged against the three questions: does it respond to the first touch, does it fit 375x667, would you show it to someone. The deck is what the pilot's depth and reached-end are read against.
Done when
At least 6 live cards by Day 8 (stretch 8: Deepstate 0, chess 1, 3–4 pages a model wrote and the founder uploaded through the + sheet, 1–2 platform embeds), every one linked through the product's own + sheet (submit_link / submit_media 'html'), phone-reviewed with a one-line verdict per card in docs/RETENTION.md's review table, and a 375x667 screenshot handed to the agent for item 8. Hunting order, by touch-fit: OpenProcessing /sketch/<id>/embed and the p5.js editor's full-view first, itch.io embed-upload HTML5 games that take touch second, CodePen /embed third, Shadertoy /embed last (mostly watch-only). A platform embed endpoint that refuses framing is skipped, never mirrored; mirror mode is for the founder's own sites and owners who said yes. Per-platform stop rule: when more than half a platform's candidates fail the three questions, stop drawing from it and record the verdicts, so the next sprint's harvest starts from a read. Each extra card after six is about 45 minutes.
Founder 2
The pilot: 100 first visits from the founder's own network, in two waves with a read between
7 h
Why
Tens of sessions a day cannot read anything. A hundred first visits through one tagged link is the only denominator this sprint gets; it is sent only after the first-minute fixes and the column check are live, and split so wave 1 can be read and fixed before the second fifty are spent. Founder-controlled 1:1 sends only; nothing posted anywhere. STRATEGY's 200–300 seed cohort waits for composition, as it says it should.
Done when
Message text written once (0.5 h) and a list of ~120 people and small groups (1 h) exist by Day 8; the link is /feed?via=pilot&r=<the founder's u_ id> (entry_surface 'share', entry_source 'pilot'; the readout excludes pilot-referred arrivals from share-to-arrival); wave 1 of ~50 on Day 9 (2 h), wave 2 of ~50 on Day 11 (2 h) only after the wave-2 gate passes; replies answered (1 h); on each of Days 9–14 the five-minute check select card_id, developer, created_at from public.card where created_at > now() - interval '1 day' and creator_id is distinct from (select actor_id from public.actor where handle = 'rand') is run and any visitor-linked card is opened on the phone the same day and hidden by hand if it fails the three questions (0.5 h total) — the by-hand bench, because nothing benches a visitor's link yet. At least 100 actors with entry_source='pilot' and ua_class='browser' whose first session is on Days 9–12.
Founder 3
The reads: the Day-1 baseline run, the cold walk, the wave-1 read, the Day-14 readout
3.5 h
Why
Every number in this sprint is read against a row that exists before any code ships, and the readout is written by the person who decides the next fortnight.
Done when
Day 1: tools/sql/retention-readout.sql run in the SQL editor with the agent and the Sep 1–8 row in docs/RETENTION.md before Deploy 2 (0.5 h). Day 8: a cold walk in a fresh browser profile on the phone — landing -> feed -> Deepstate -> BLITZ -> chess -> the new cards -> end card -> loop, the chevron back, a Share — with any blocker fixed before the first link goes out (0.5 h). Day 10: the wave-1 row written (1 h). Day 14: retention_daily(14,'pilot') and (14,null) copied from /stats into docs/RETENTION.md next to the baseline with n and the interval, and the kill criteria answered one by one (1.5 h).
Founder 4
A spare hour: the DM template and concierge.csv shape, and the two or three makers the founder already knows
1 h
Why
If kill criterion 4 fires (people finish and do not return), the next sprint's supply work starts with the ask in the right order; drafting it now costs an hour and builds nothing. Makers the founder knows personally can be asked before their own page is linked — the only concierge this sprint.
Done when
docs/RETENTION.md (or a harvest/ note) carries a three-sentence DM template (here is your thing on a phone; tap to put your name on it — by hand this sprint; reply remove and it is gone today) and the concierge.csv column shape (who, where, card, date sent, reply, claimed, removed); 0–3 makers the founder knows have been asked, and none of their pages is linked before a yes.
Total: about 24.5 hours.
07The readout and the kill criteria
Day 14 (Tue 22 Sep), from /stats — retention_daily(14,'pilot') and retention_daily(14,null) beside the metric_snapshot baseline — and tools/sql/retention-readout.sql's Day-14 block, on pilot arrivals only (entry_source='pilot', ua_class='browser' as the server derived it, first session Days 9–12), written next to the Day-1 baseline row and the Day-10 wave-1 row in docs/RETENTION.md with n and a binomial interval on every rate. Read in this order: (1) IDLE_TIMEOUT share of external-card exits since Deploy 2 — must be exactly 0, it is the measurement; (2) position-0 hold (server span >= 5 s, no CRASH) on first sessions, target >= 50%; (3) share of visits reaching the end card, target >= 50%, and median depth (max visit_ordinal per visit_id) reported with n against the baseline's structural <= 2; (4) back taps per visit on touch, > 0; (5) D1 — the outcome — with its interval (about ±6 points at n≈100); (6) diagnostics, reported never targeted: share taps and share arrivals with entry_source not 'pilot', likes, loops per visit, webview share of arrivals, boot_ms p50 on ordinal 1, stats_daily DAU over the fortnight with the founder's tours excluded, card_rank(14) for the live cards (votes, hold_ucb, demotion — whether Deepstate has 20 votes yet, from data), the per-platform review verdicts, and the pilot's n by wave. Not read: D7, anything per hosted card (the founder's own uploads only), any ranking number as a verdict under 20 votes, any A/B. Demote-only stays exactly as written; the only thing this readout decides is where the next fortnight's engineering days and content hours go.
- Any external-card session since Deploy 2 ended IDLE_TIMEOUT (read Day 3, Day 10, Day 14): the policy is not reaching production and nothing else ships until it does — it is the measurement.
- The wave-2 gate fails on Day 10 (external idle share not 0, boot_ms missing on ordinal-1 rows, column-check not clean, or an open P0): wave 2 is not sent, the sprint reports n≈50 honestly, and the next fortnight fixes measurement before composition or supply.
- Fewer than 80 pilot browser actors by Day 14 even with both waves sent: the D1 number is not reported as a number; only the guard rails are read, and the next sprint starts by finding out why one hundred sends produced fewer than eighty arrivals (webview share, the message, the link).
- Position-0 hold < 40% and reached-end < 50%: the first minute is the problem, not the way back — the next fortnight goes to card 0's phone fit (Deepstate's own hold number, read from card_rank, is the ruling's 'until outvoted') and to the chrome, not to supply and not to composition.
- Reached-end >= 50% but D1 < 5%: people finish and do not return — the deck is too short; the next sprint is supply through the founder's own linking and the concierge with the bench ruling in hand (STRATEGY bet 6), not more chrome.
- D1 between 5% and 10% at n≈100 (the gray zone): one more founder-network wave after the wave-1 fixes, no seed cohort yet, and the day-21 read decides.
- D1 >= 10% at n≈100 with the guard rails met: the bar holds provisionally; the next sprint is composition rescoped for a linked-card deck plus supply (bets 4 and 6), and there is still no launch post until D1 holds on >= 200 browser arrivals at a day-21 read (STRATEGY section 7).
- Back taps per visit = 0 on touch across the pilot with the chevron live: the control is not being found — a placement fix before anything that assumes a way back is measured; a fix, not a kill.
- Webview share of pilot arrivals > 40%: the end card's browser line was needed and, if it was cut, ships before any cohort; retention is read on browser arrivals only either way.
- A card linked by a visitor during the pilot goes a day without being opened on a phone, or a removal request is not honoured the same day: the section-9 rule was broken by hand — the bench is sprint 2's first item regardless of the numbers.