Awareness
Device Signals
Automatic mode detection from phone motion, geofences, and DND — privacy-first by construction: raw location and motion never leave the device, only a coarse hint.
Situation
Modes v1 made Mallory context-aware, but the active mode only ever came from a manual override, a Hub calendar hint, or a static routine table — nothing caught a commute starting or an arrival at the office automatically. Device signals add that: the phone's own sensors (Activity Recognition, geofences, Do-Not-Disturb) become a third hint source feeding the same resolver the Modes v1 spec deliberately left extensible.
The core discipline is privacy-first, coarse-on-device: this is a hint, never an override — an explicit manual mode always wins, and the device can only ever propose. Every sensor is independently opt-in and off by default, and the design's own privacy invariants (raw coordinates never leave the device, geofence coordinates travel web→native over the bridge only, edge payloads are validated to closed enums server-side) are treated as first-class requirements, not implementation details.
Key decisions
Signal scope (v1)
Motion (Activity Recognition), DND state, and on-device geofences (home/office) — geofences are included so the device can also propose a *hat*, at the cost of a background-location grant, though raw coordinates still never leave the phone.
Per-axis hint precedence
The device leads the activity axis (ground truth on movement); the calendar leads the hat axis (knows which account a meeting belongs to); each can fill the other axis as a lower-priority fallback, and the two hints live in separate KV keys so they never clobber each other.
Geofence setup via map picker
A Leaflet/OSM web picker in /settings, reached through the existing MalloryNative bridge — coordinates go web→native into prefs only; only the derived label ever reaches the edge.
Per-signal consent
Independent toggles for Motion, Location, and DND, each off by default, each requesting its own OS permission only on enable — motion-only is a valid state that never grants background location.
DND's off is weak
The device never sends a strong off from DND alone, since a focus block looks identical to knocking off early — marking it weak lets the resolver rank it below a known calendar meeting without needing a confidence field.
Architecture
Split across two implementation plans with a shared contract: the edge core (per-axis resolver, KV, CN-gated route) is additive and deployable-while-dormant, built first so the Android/web client has a real contract to post against.
device param; with it undefined the function is byte-identical to today, so existing call sites and tests are untouched.DEVICE_SIGNAL_CN (the Android shell's Access service-token Client ID), validates closed enums, an empty body {} clears device influence.mode:hint so device and calendar never overwrite each other; TTL expiry is the backstop for a dead or quiet phone.BroadcastReceiver for Activity Recognition and geofence transitions, plus DND read via the existing NotificationListenerService — no continuous GPS/motion stream, hysteresis before flipping state.getDeviceSignalConfig, setSignalEnabled, setGeofences) carry config web→native.Risks & guardrails
Privacy is the core constraint, not a footnote
Seven explicit invariants gate the design — e.g. raw coordinates never leave the device, mode:device writes are CN-gated to the phone's service token alone, and toggling a signal off posts an empty state immediately to stop emission.
DND ambiguity
A daytime focus block and genuinely knocking off early look identical to DND alone, so the weak-off ranking is load-bearing to avoid the device overriding a known meeting.
Stale or lost device
Mitigated by TTL self-expiry on every device assertion — a quiet, lost, or stolen phone's stale state decays automatically rather than sticking.
Two plans, two toolchains
The edge plan (Vitest-tested, additive, deployable-while-dormant) and the client plan (Kotlin/gradle + on-device adb verification, not unit-TDD-able) must build in order — edge first so the contract is real — even though the client doesn't import edge code and can develop in parallel once the contract is fixed.
Delivery
Edge plan — resolver + route
Per-axis resolveMode plus the ModeDevice type, mode:device KV wiring, and the CN-gated POST /mode/device route; verified by a full Vitest suite plus a type-check pass. Deployable while dormant — with no DEVICE_SIGNAL_CN set and no phone posting, behaviour is unchanged.
Client plan — Android native
Gradle dependency + manifest permissions/receiver, Prefs additions, the DeviceSignals derivation object, DeviceSignalReceiver + DND hook, and MainActivity bridge methods and permission launchers.
Client plan — web settings
A pure deviceSignals.ts lib (unit-tested), the Leaflet GeofencePicker component, and the device-signals section wired into /settings.
On-device verification
adb-driven end-to-end checks on a real handset, since sensor transitions can't be unit-tested headlessly.
Deploy held for Alex
Both plans are PR-only; deployment and the on-device Android build are explicitly held for the operator.
Out of scope
docs/superpowers/specs/2026-06-21-device-signals-design.mdplan ·
docs/superpowers/plans/2026-06-21-device-signals-client.mdplan ·
docs/superpowers/plans/2026-06-21-device-signals-edge.md