Notifications & Nudges
Notification Voice & Source Handoff
Haiku-composed push teasers plus a server-supplied native deep-link so "Open in Slack/Gmail/Calendar" scales without an Android release.
Situation
Two related fixes to Mallory's push notifications: (a) make every teaser sound like him, composed Worker-side by Haiku instead of using the raw title/body, and (b) hand off correctly to the native app (Slack/Gmail/Calendar) when a source is tapped from Updates, in a way that scales as integrations grow — new sources add a deep-link server-side, never an Android release.
This builds directly on already-shipped dependencies: the Updates surface, notification-tap-to-Updates routing, data-only FCM (a hard prerequisite for any tap-through), and the priority soft-cap — none of which are rebuilt here.
Key decisions
Haiku composes every teaser
composeTeaser(env, {...}) makes one small, low-temp Haiku call at nudge creation (using MALLORY_SYSTEM); on error/timeout it falls back to the existing title/body, never blocking the nudge or its Updates record.
Server-supplied deep-link + JS bridge
Chosen over App-Links auto-routing or a client-side host-map as the most reliable, server-scalable mechanism — the Hub emits native_url + app per item, Android's bridge just opens it.
Label: "Open in {app}"
Falls back to "Go to source" when app is absent but a url exists; no button at all when neither is present (e.g. briefing/EOD).
Notification tap still goes to Updates, not direct-to-source
Already shipped and verified — this design only changes what happens after the user taps "Open in {app}" inside Updates, not the push tap itself.
Fallback to browser on ActivityNotFoundException
Android's openSource bridge tries the native scheme first and catches the failure to open the https URL instead, so an app that isn't installed never dead-ends the tap.
Architecture
A Worker-side Haiku pass writes a voiced teaser at nudge creation; the Hub attaches a native deep-link per source; the web and Android layer only decide whether to hand off natively or fall back to the browser.
handleNudge and by briefing/EOD; writes the teaser column, falls back to title/body on failure.nativeUrl + app per item to the web client.nativeUrl is set, else opens url in a new tab.startActivity(ACTION_VIEW, nativeUrl) with an ActivityNotFoundException catch that falls back to the web URL; mirrors the existing openSettings/openNotificationAccess bridge pattern.post_nudge gains native_url/app/priority params; mention nudges emit a slack:// deep-link; calendar-sync emits Calendar + high priority for imminent (≤2h) meetings.Risks & guardrails
Haiku cost/latency per nudge
One cheap Haiku call per nudge is accepted as fine for now; if batch mention runs make it noticeable, lazy generation at flush/send time is an explicitly deferred optimisation.
App-not-installed dead end
Guarded by the ActivityNotFoundException-to-browser fallback in the Android bridge, verified manually on-device (only Alex can confirm real app handoff).
Bridge-detection must degrade cleanly
The web checks for JS-bridge presence before calling openSource, so the identical UpdatesView code works unchanged in a plain browser and in the Android shell.
Delivery
Worker: migration, data layer, teaser composition
Migration 012 adds native_url/app; db.ts, parseNudgePayload, and handleNudge gain the fields and the Haiku compose step; briefing/EOD callers fixed and /updates serves the new fields.
Web + Android handoff
UpdatesView gets the "Open in {app}" label and bridge-vs-browser branching; Android gets the openSource bridge method.
Hub wiring
post_nudge threads native_url/app/priority through; slack-email-monitor and calendar-sync emit them for their sources.
Verify + deploy
Held for Alex — Worker/web/Hub PRs plus an on-device Android verification of the real app handoff.
Out of scope
docs/superpowers/specs/2026-06-14-notification-voice-and-source-handoff-design.mdplan ·
docs/superpowers/plans/2026-06-14-notification-voice-and-source-handoff.md