Story-3099

First-run nudges derived from live workspace state

The app can now ask the server which onboarding action is relevant for the current counsel. Completion is never copied into onboarding storage: every read derives it from the real matter, document, WhatsApp, and Vani state.

App API contract

GET /app/onboarding/nudges
200 {
  "nudges": [
    {"key": "create_first_matter", "order": 1},
    {"key": "connect_whatsapp", "order": 3},
    {"key": "personalise_vani", "order": 4}
  ]
}

POST /app/onboarding/nudges/{key}/dismiss
POST body (optional): {"permanent": true}
204 No Content

Responses retain stable registry positions even when an earlier nudge is ineligible. The consumer can therefore show the first returned item while preserving a deterministic sequence.

One-query decision

create_first_matter  = no firm matter exists
add_first_document  = a firm matter exists and no firm document exists
connect_whatsapp    = no active caller Meta binding exists
personalise_vani    = no agents.counsel_preference row exists
                       for firm + caller + agent_key='mira'

All four facts and the caller's current suppression state are selected in one PostgreSQL round trip. The code registry maps those facts to ordered nudges; adding a future fifth ask is one registry entry plus one EXISTS projection.

Dismissal lifecycle

dismissal 1 → suppressed for 1 day
dismissal 2 → suppressed for 3 days
dismissal 3 → suppressed for 7 days
dismissal 4 → suppressed until PostgreSQL 'infinity'

{"permanent": true} → 'infinity' immediately

The only durable onboarding state is the dismissal row in the identity schema. Its primary key includes firm, caller, and nudge key; row-level security enforces the firm boundary. Firm and user identifiers come only from the authenticated database session.

Scope boundary

Drive is intentionally absent from v1 because neither a persistent Drive connection nor document import origin exists today. This path performs deterministic database work only: no generative model call, manual trace span, UI, or analytics event was added.