Migrate all callables off functions.https.onCall to firebase-functions/v2/https onCall:
createInvite, acceptInvite, leaveCouple, submitOutcome, sendGentleReminder,
sendThinkingOfYou, checkDeviceIntegrity, syncEntitlement, assignDailyQuestionCallable,
wrapReleaseKey. context.auth/app → request.auth/app, data arg → request.data. The 8
client-hardcoded callable names are preserved verbatim (verified via emulator discovery).
The manual `if (!request.app)` App Check check is a 1:1 port (no enforceAppCheck switch).
Hardening folded in:
- acceptInviteCallable: await the previously fire-and-forget partner_joined push (gen 2
freezes the instance after the response) — still swallows push errors so a failed push
never fails the accept.
- checkDeviceIntegrity: 10s timeout on the Play Integrity client.request so a hung upstream
can't pin the instance (fail-closed catch already handles the throw); memory 512MiB.
- wrapReleaseKey: memory 512MiB (tink); HttpsError swapped to v2; lazy tink require + graceful
failure preserved.
- Error mapping with `if (e instanceof HttpsError) throw e` re-throw guard around the risky
DB sections in acceptInvite, leaveCouple, submitOutcome, sendGentleReminder,
sendThinkingOfYou — raw errors map to a clean 'internal' without masking intentional codes
(resource-exhausted rate limits, permission-denied, etc.). leaveCouple's best-effort
recursiveDelete sweep now swallows errors (the transactional leave already succeeded).
- Adopt shared sendPushToUser()/logger; remove copied token readers + plaintext token logging.
Delete dead placeholder callables notifications/reminders.ts (sendDailyQuestionReminder,
sendPartnerAnsweredNotification) — no client caller; wrote sent:false rows nothing consumed.
Build clean; 70 tests green; discovery loads all callables as v2 in us-central1. dist rebuilt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrate all scheduled jobs off functions.pubsub.schedule().onRun() to
firebase-functions/v2/scheduler onSchedule({ schedule, timeZone, ...opts }, handler):
sendChallengeDayReminders, unlockDueMemoryCapsules, sendDailyQuestionProactiveReminder,
sendStreakReminder, sendReengagementReminder, assignDailyQuestion (scheduled export),
aggregateOutcomeStats, scheduledOutcomesReminder.
Hardening folded in:
- Fan-out isolation: Promise.all → Promise.allSettled in dailyQuestionReminder (outer+inner),
reengagement, gameRetention (both jobs), scheduledOutcomesReminder — one bad couple can no
longer abort a whole run. streakReminder / assignDailyQuestion already isolated.
- Resource options: assignDailyQuestion + aggregateOutcomeStats memory 512MiB + timeout 300s
(they iterate all couples); the four fan-out reminders get timeout 180s.
- Adopt shared sendPushToUser()/logger everywhere; remove five copied getUserTokens() and the
copied send/prune blocks (no plaintext token logging remains here).
- Consolidate duplicated date/time helpers into notifications/time.ts (chicagoDateKey, toMillis),
replacing streakReminder's + scheduledOutcomesReminder's per-file copies.
assignDailyQuestion.ts callable export stays v1 for now (migrates in B3); its tested CST helpers
are untouched. Scanner pagination for assignDailyQuestion/aggregateOutcomes is deferred to B6.
Build clean; 70 tests green (tested pure helpers preserved). dist rebuilt. Still on v5.1.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrate all Firestore triggers off the v1 API to firebase-functions/v2/firestore
(onDocumentCreated/Updated/Written); context.params→event.params, snap→event.data,
change.before/after→event.data.before/after. Region stays us-central1 (global option).
Hardening folded in (all reuse in-repo patterns):
- Adopt the shared sendPushToUser()/getUserTokens() helper in every trigger, removing
~7 copied token readers and the copied send/prune blocks. FCM tokens are no longer
logged in plaintext anywhere here (redacted inside push.ts).
- console.* → firebase-functions/logger (structured).
- Idempotency: new claimOnce() (atomic create-if-absent marker under
couples/{id}/notif_marks) dedupes at-least-once redelivery on the non-idempotent
senders (onAnswerWritten/Revealed, onMessageWritten, onCoupleLeave, onEntitlementChanged,
onDateReflectionWritten/Revealed, onDateHistoryCreated). Fail-open. onGameSessionUpdate/
onGamePartFinished/notifyOnDateMatch already had transactional claim-flags — preserved.
- onRestoreRequested: the plan's "claim" is implemented as the existing 60s time-WINDOW
(lastRestorePartnerAlertAt), not a permanent recipientUid marker — a permanent marker
would wrongly block legitimate re-requests (restore docs are deleted+recreated by design).
Faithful port of onGameSessionUpdate/onGamePartFinished (broad wildcard + allowlist kept);
the trigger split and read reorder are deferred to B6 as separate commits.
Build clean; 70 tests green (+ new idempotency.test.ts, push token/prune tests). Emulator
discovery loads all triggers as v2 in us-central1. firebase-functions still v5.1.1 (v6 bump
deferred to B6). dist rebuilt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Groundwork for the v1→v2 Cloud Functions migration; no function is migrated yet
(all 35 still load as v1, verified via emulator discovery).
- options.ts: setGlobalOptions({ region: 'us-central1', maxInstances: 20 }), imported
first in index.ts so it applies before any v2 function is defined. Region pin is
load-bearing — the Android client uses the default region.
- notifications/push.ts: single canonical getUserTokens() + sendPushToUser() that
batches via messaging.sendEachForMulticast() and prunes dead tokens, to replace the
~10 copied token readers and ~19 copied send/prune blocks in later batches.
- log.ts: firebase-functions/logger re-export + redactToken() (FCM tokens are secrets).
- push.test.ts: 9 unit tests (token merge/dedupe, BatchResponse→dead-token mapping,
send/prune/no-op/whole-batch-failure paths). 67 tests green.
firebase-functions stays at v5.1.1 for the migration (supports both the root v1 API and
the /v2 subpaths); bump to v6 is deferred to the final batch once nothing references the
root namespace, so the build stays green at every step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Test-account emails/uids/passwords now live in qa/fixtures.local.md (gitignored);
qa/README.md carries only a pointer. Credentials must never be committed or pushed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The daily reveal chip used displayCategoryName() (a different surface than the
Home pill fixed earlier), so it still showed 'Daily Fun Mc'. Fixed centrally in
the shared helper. Verified live: reveal now shows 'Daily Fun'.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace pickRandomQuestionId (empty pool → unresolvable q_default_daily fallback)
with pickDailyQuestionId(date): computes today's weekday mode (mirrors the client
DailyModeResolver.DOW_DEFAULTS, FROZEN) and deterministically indexes the free
daily pool by epochDay % poolSize — the SAME question the free client would pick,
now assigned server-side.
Fixes two things at the root:
- The daily 'questions' pool was empty, so every couple got q_default_daily, which
the client can't resolve → each device fell back to its own local selection
(the DQ-MISMATCH-001 class; the client half was pinned to the free pool in C1).
- Partners in different time zones computed different device-local weekdays and got
different questions; a single server-assigned id makes both devices identical.
Firestore 'questions' seeded with the 75 free daily_fun_mc weekday questions (ids
match the client asset DB so getQuestionById resolves them). Pool fetched once and
filtered in memory — no composite index needed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Waiting-on-partner screen: 'Send a little nudge 💜' button (shown while the
partner is still playing, not once it's your turn) reusing the generic
thinking-of-you callable (10/day, quiet-hours-safe server-side); one-shot
Toast on result incl. friendly rate-limit copy. Verified live: nudge →
partner_activity push landed on the partner.
- Per-game banner copy: YOUR_TURN/RESULTS in-app banner now branches on gameType
('Your turn — guess their answers' for How Well, 'only mutual yeses ever show'
for Desire Sync, etc.) instead of one generic line; mirrored in the Cloud
Function's partner_completed_part push (yourTurnBody). Verified live.
- Accessibility: merged contentDescription on the This or That MatchScoreBadge
('You matched on N of M') and the How Well score ring Canvas ('You guessed N
of M correctly') — both were split/Canvas visuals invisible to TalkBack.
Unit + functions typecheck green; assembleDebug clean. Server copy change needs
a functions deploy (bundled with the C1 finish-guard).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- NextBeatCard: a self-contained 'what's next' card under every game's results
CTAs so finishing a game doesn't dead-end — shows 'Tonight's question is ready
→' when today's daily is unanswered, else a 'Day N together' streak note. Own
@HiltViewModel (DailyQuestionResolver + LocalAnswerRepository + couple), so the
four game screens only drop in the composable. Verified live on How Well results.
- How Well role swap: the guesser's results now lead with 'Your turn — let {name}
guess you', starting a new round where they become the subject (Newlywed-style
reversal; starter == subject). Verified live.
- Date Match: deterministic per-couple deck shuffle (kills 'every couple sees the
same first card') + skip already-swiped ideas so the deck resumes past them
instead of restarting at card 1. Verified live (swiped card no longer reappears).
- Desire Sync: remember served question ids per couple (SeenDesireQuestionStore on
the settings DataStore) so back-to-back rounds don't repeat prompts; clears the
record to cycle the pool when the unseen remainder can't fill a round.
- How Well pool purity: exclude daily_fun_mc novelty MCs from getQuestionsForPrediction
(1869 real prediction questions remain); DAO method has no other caller.
Unit suite green; assembleDebug clean; live-verified on the emulator pair.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- DOB picker (sign-up + profile): swap the View-based android.app.DatePickerDialog
(platform teal) for a shared Compose M3 DobPickerDialog that renders in the app
palette in both themes; future dates unselectable. Verified live dark = purple.
- SignupHandoff: back the pending DOB with the app's Preferences DataStore keyed
per-uid, instead of an in-memory singleton that died on process death and made
CreateProfile re-ask for the birth date after any restart mid-onboarding
(verified: am kill mid-profile → relaunch → no DOB re-ask). Local-only write,
so the auth-token/Firestore-rules race the old comment guarded against doesn't
apply; per-uid key prevents cross-account leakage.
- Connection Challenges: expose statusDay (calendar-actionable day) on
ChallengeState; the day card shows a 'Day N unlocks tomorrow 🌙' teaser instead
of spoiling the next prompt the moment today's step is marked done. +2
ChallengeStateMachine tests.
Unit + functions suites green; verified live on the emulator pair.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Challenges catalog: hide the 🔒 Premium badge once the couple has premium
(A-003b; matches the Play hub's showPremiumBadge pattern). Verified live
both directions under an authorized grant/revoke cycle.
- Game banner lifecycle (BANNER-LIFE-001): entering a session's screen now
consumes any banner pointing at it (GamePromptController.consumeForSession
wired into ActiveGameSessionMonitor.enter), and activity from a DIFFERENT
session may replace a stale persistent banner. Verified live: no banner on
reveal; stale RESULTS banner replaced by a new session's prompt.
- Waiting/join screen: says 'Your turn — {name} already played their part'
for the non-starter once a first part landed (new partHasFinished mapped
from partFinishNotifiedAt; completedByUsers only fills at reveal). Session
observe mapping now also carries completedByUsers/joinedByUsers.
- How Well results: matched-row colors are now a theme-aware container+content
pair (dark mode was near-invisible: fixed pale-green container under
onSurfaceVariant text).
- Date Match: top card fully opaque — next card's text no longer bleeds
through (was alpha 0.96).
- functions: don't send 'X finished — see your results!' for abandoned/quit
sessions (status flips to completed with empty completedByUsers; a real
completion always has both uids). Found live when a quit triggered a false
banner. Needs deploy (bundled with C4).
Unit + functions suites green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Delete unused ui/theme/Color.kt (zero refs; live palette is Theme.kt).
- Remove orphaned EditProfileScreen wrapper composable (no route references it;
AccountScreen embeds EditProfileContent, which stays).
- Debug-gate ArtPreviewScreen + PairedHomePreviewScreen out of the release nav
graph (BuildConfig.DEBUG).
- Collapse duplicate wheel_history route into game_history (same screen; wheel
surfaces now navigate to Past Games directly).
- CouplePremiumChecker: inject AuthRepository instead of raw
FirebaseAuth.getInstance() (finishes the in-flight auth-DI refactor for
non-data-layer call sites).
- Copy/UI polish: 'It's a match!' (matches the push copy), internal 'mc' token
never renders in the Home daily pill ('Daily Fun Mc' → 'Daily Fun'),
welcome/splash privacy tagline no longer clips mid-sentence (maxLines 4 +
ellipsis, AUTH-TRUNC-001).
Unit + functions suites green; assembleDebug clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Game answer listeners (ToT/HowWell/DesireSync): close(err) instead of
swallowing snapshot errors, + .catch in each VM's observeReveal surfacing a
retryable ERROR (GameCopy.SYNC_ERROR + retrySync re-attach) — games no longer
hang on WAITING forever on listener failure (GAME-HANG-001; matches the
Wheel/Capsule sources' established pattern).
- Daily question: paired fallback pool is now premium-INDEPENDENT so both
partners always resolve the same deterministic question; viewer-premium pools
broke the couple contract when entitlement state differed or flipped mid-day
(DQ-MISMATCH-001, reproduced live: partners answered different questions).
- Daily reveal: humanize raw option-id fallbacks so slugs never render
(DQ-SLUG-001, e.g. 'fake_awards_should_be_mandatory').
- assignDailyQuestion.ts: replace hardcoded CST_OFFSET_HOURS=-6 with DST-safe
Intl America/Chicago helpers (DST-001) + 5 regression tests (CDT/CST labeling,
6PM reveal instant, spring-forward round-trip).
Verified live 2-device: identical daily question on both partners post-fix;
ToT full loop 5/5 reveal regression clean. Unit 244 + functions 58 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records the applied standardizations (state collection, whole-data-layer
Tasks→await, version catalog, TS payload typing, 7/8 ViewModel-Firebase
extractions) and the four deliberately-scoped follow-ons (HomeViewModel metadata
listeners, color tokens, Functions v2, catch-unknown) with the reason each needs
its own verification loop rather than a broad sweep.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DailyQuestionViewModel and PartnerHomeViewModel each held a raw
db.collection(...).addSnapshotListener on the partner's answer doc (just reading
snapshot.exists()). Replace with the existing, proven
FirestoreAnswerDataSource.observeAnswerForUser(...) Flow collected in a
viewModelScope Job (auto-cancelled; previous run cancelled on restart) — dropping
the FirebaseFirestore injection from both. Same behavior (exists → non-null),
now through the data layer.
Verified live: daily question shows the correct "waiting for your partner" state
via the new Flow; compiles + unit suite green.
HomeViewModel's two listeners use MetadataChanges.INCLUDE + inline analytics and
drive the core reveal — left as a scoped follow-on (needs a metadata-aware
data-source Flow + two-device real-time verification). See docs/standardization-review.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five ViewModels reached into the FirebaseAuth.getInstance() static singleton for
the current user id; switch them to the injected AuthRepository.currentUserId
(BucketList, QuestionThread, DateBuilder, MessagesInbox, Conversation). Removes
the Firebase-in-presentation-layer coupling and makes them testable. No behavior
change; compiles + unit suite green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Change the 5 https.onCall handlers from `data: any` to
`data: Record<string, unknown>`, so payload fields are `unknown` and must go
through the existing validators rather than being implicitly-typed. No behavior
change (every field was already validated); tsc + 53 function tests green.
Left as-is deliberately: `catch (err: unknown)` narrowing (churn, marginal) and
the untyped Tink handles in wrapReleaseKeyCallable (the crypto lib ships no types).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Centralize all plugin + dependency versions in gradle/libs.versions.toml (the
modern Gradle standard) and switch the root + app build scripts to the generated
libs.* / alias(libs.plugins.*) accessors. No version changes — a 1:1 migration of
the 57 hardcoded coordinates. Full assembleDebug + unit suite green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Convert the remaining 10 data sources (Bucket, Conversation, QuestionThread,
DatePlan, DateMemory, DateReflection, Answer, User, Auth, DateMatch) from
verbose suspendCancellableCoroutine{cont-> ...addOnSuccess/addOnFailure} wrappers
(and the per-file getDoc/voidAwait/queryAwait/refAwait helper duplicates) to the
kotlinx-coroutines-play-services .await() extension. The data layer now has ZERO
Task-callback wrappers.
Per-block review preserved non-trivial semantics: callbackFlow snapshot listeners
and the DateMatch transaction are untouched; failure→null/false best-effort blocks
became runCatching{ ...await() }.getOrNull()/getOrDefault() (e.g. markRevealed,
hasProfile, PlayIntegrity.verifyWithServer); no-signed-in-user guards became
`?: throw`.
Verified live (real Firebase, paired account): auth + Home reads (couple/user/
answer/challenge), daily-question answer WRITE (secure payload + metadata sets),
and the streak update all succeed — no crash, no PERMISSION_DENIED. Unit suite green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Grounded review of the Android app / functions / build with measured
inconsistencies, what was standardized this session (collectAsStateWithLifecycle;
Firebase Tasks→await subset), and risk-assessed recommendations for the larger
items (finish Tasks→await, version catalog, ViewModel Firebase extraction,
Functions v2, color tokens, TS types).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace verbose suspendCancellableCoroutine{ cont -> task.addOnSuccessListener
{cont.resume}.addOnFailureListener{cont.resumeWithException} } wrappers with the
kotlinx-coroutines-play-services .await() extension in the files whose blocks are
all one-shot Tasks (no snapshot listeners / transactions to endanger):
FirestoreOutcomeDataSource, FirestoreCoupleDataSource, PlayIntegrityChecker.
Notably fixes FirestoreOutcomeDataSource, which mixed both styles in one file.
PlayIntegrityChecker.verifyWithServer keeps its failure→null semantics via
runCatching{ ... .await() }.getOrNull() (a bare await would throw instead).
The remaining ~11 data sources also use the callback pattern but mix in
callbackFlow snapshot listeners / transactions, so they need per-block review —
tracked in Future.md rather than bulk-rewritten on untested I/O.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Migrate all 56 Compose screens from collectAsState() to the lifecycle-aware
collectAsStateWithLifecycle() (added androidx.lifecycle:lifecycle-runtime-compose).
Upstream flow collection now pauses when the UI isn't visible instead of running
in the background — the modern, recommended idiom, and it unifies the two files
already using it with the rest of the app.
No behavior change for visible screens; compiles + unit suite green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tracked dist output rebuilt to match src (aggregateOutcomeStats export +
aggregateOutcomes module), keeping the deployed bundle in sync with source.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design for moving off the deprecated androidx.security:security-crypto to
Tink Android-Keystore-backed storage, without ever losing the couple key.
Covers: the load-bearing hazard (SecurePreferencesFactory.reset() silently
WIPES the couple key on any read failure) which must be removed first;
lazy dual-read + re-wrap + verify-then-clean migration; fail-closed (recover,
never delete) failure matrix; consumer ordering (couple key last); staged RC
rollout + content-free telemetry; test plan; open questions for the owner.
No implementation ships until the owner approves this design (Batch 5.1 gate).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
aggregateOutcomeStats (scheduled) rolls up per-couple check-in deltas
(couples/{id}/outcomes/day_30|60|90) into the "X% feel closer in N weeks" stat
WITHOUT reading any E2EE content — only the self-reported deltas.
Privacy: counts/percentages only (no couple id or individual scores);
minimum-cohort suppression (N<50 → window omitted, no percentages); EXPORT-ONLY
via the top-level aggregate_stats collection, now explicitly deny-all in
firestore.rules (owner reads via console).
Pure aggregate()/extractCoupleOutcome() unit-tested incl. below-threshold
suppression + a no-PII assertion (6 tests); rules deny client read/write of
aggregate_stats (2 tests). Full functions 53/53, rules 121/121.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- DateIdea gains sponsored/sponsorName/externalUrl (native, clearly-labeled ads
— no tracking SDK).
- FirestoreDateIdeaDataSource reads the server-curated date_ideas collection
(defensive: malformed docs skipped, https-only externalUrl); DateMatchRepo
serves remote first, falling back to the shipped static seed when empty/failed
so the feature always has content. Fallback verified live (Date Match renders
the seed) + unit-tested (remote-present / empty / failure).
- Date card shows a "Sponsored · <name>" pill and a "Learn more" link (opened via
the system browser, https-only, no click IDs).
- Local admin seeder (functions/scripts/seedDateIdeas.ts + date_ideas_seed.json,
generated from the Kotlin seed + 1 sponsored example) — idempotent by id, has a
--dry-run; NOT a deployed endpoint. Verified: 38 docs upserted to the emulator,
1 sponsored, tsc builds.
Owner: run the seeder against prod (GOOGLE_APPLICATION_CREDENTIALS) to populate
date_ideas; add real sponsor content.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- PaywallPricing turns a RevenueCat Package into a headline price + an offer line
(free trial, intro price, or one-time), so a dashboard-configured trial or a
lifetime SKU renders without an app release; unknown shapes degrade to the
plain price. Pure formatting is unit-tested (PaywallPricingTest); the SDK
extraction is type-checked against purchases 8.20.
- Gift framing on the plan chooser ("One subscription covers you both — treat
your partner"): couple-shared premium already unlocks both partners, so this
is copy only, no billing change.
Live paywall (real offerings) still needs a real RC key + sandbox per
docs/SUBSCRIPTION_GO_LIVE.md (owner).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Foreground notification taps carried only app_route, so their
push_notification_opened event logged the generic "app_route" category. Also
put the notification type on the intent so the event records the specific kind
(partner_answered, reveal_ready, ...). Backgrounded taps already carry type via
the FCM data payload. Completes push-opened coverage across both tap paths.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The live couple streak used a raw 48h rule that reset on any 48h gap AND
inflated the count by incrementing on every answer (several a day counted
several times). Replace with StreakCalculator.nextCount: once per calendar day,
one forgiving grace day (a single missed day still continues), reset only after
2+ missed days; skip the redundant write on a same-day answer.
Celebration milestones now include the habit-building early tiers
(1/3/7/14/30/100/365), single-sourced from StreakCalculator so the count and the
celebration can't drift; the milestone dialog handles day 1 grammatically.
Tested (StreakCountTest): same-day idempotency, consecutive, grace, reset, and
partner timezone-skew (never breaks a same-boundary streak). Existing 24
StreakCalculator tests still pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Content-free daily-question state + streak, mirrored from HomeViewModel via a
dedicated DataStore (TodayWidgetUpdater -> TodayWidgetState). App-lock drops it
to a generic card so partner-activity state doesn't sit on the protected home
screen; signed-out/unpaired show neutral prompts. Tapping opens the daily
question via MainActivity's app_route extra.
Verified: provider registers, state bridge writes, headline logic unit-tested
(TodayWidgetTest), no crash on launch. On-home-screen render + a Remote Config
kill switch are tracked in Future.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>