Notifications & Nudges
Situation Board
One merged, self-draining board — Mallory's editorial judgement about what genuinely needs Alex, with enough context that he never opens Slack to triage.
Situation
An audit against live prod D1 found the Response-SLA engine working but invisible: channel_pending held 142 ball-in-court items, 130 auto-resolved the moment Alex replied, 12 aging 24–99h — and 0 of 142 had ever been nudged. The signal was excellent; the delivery was nil.
Meanwhile Mallory's pushes were a slower echo of the phone's own notifications — competing on the one axis he loses (speed) instead of his strengths (judgement, memory, cross-source curation). Three overlapping surfaces (Updates feed, WaitingCard, /waiting) fragmented the experience, two producers watched the same Slack/Gmail, calendar nudges had never fired, and all 283 reminders had no feed UI at all.
Key decisions
One merged board
A single self-draining situation board replaces the three-surface split. Two folds: a tiny primary fold (editor's note + *On your court*), and a secondary fold (*Coming up*, *Remember*, *Handled/FYI*) that is context you can check, not work you must do.
Hero = judgement + context
Hard triage plus one line of *why it matters* per item. Visible auto-resolution was explicitly considered and *not* chosen as the hero — the drain stays a quiet background truth.
Live board + editor's note
Hybrid build: a deterministic live layer assembles from the durable stores with no model call (instant, drains live), overlaid by a periodic LLM ranking/voice pass 2–3× a day. Never stale-blank, just sharper at each tick.
Ambient by default, breach escalates
Nothing routine pushes — the native Slack/Gmail notification already did the real-time job. This retires the 2-hourly mention digest. Only a genuine breach fires unscheduled; warnings stay ambient on the board.
One court source of truth
channel_pending absorbs the mention monitor's intelligence via the existing-but-unused triage: 'llm' hook, cached once per item at capture. The parallel nudge producer retires.
Configurable composer model
The composer's model is a slot in the Model Settings surface — default a cheap tier, swappable to OpenRouter later. Explicitly not a hardcoded constant.
Architecture
Two layers. The live layer assembles zones deterministically from D1 on every request; the editorial layer writes a ranked shortlist + voice line to KV a few times a day, which the live board overlays on its rule-based order.
assembleBoard() — zone assembly plus the editorial rank/park overlay. No I/O, fully unit-tested.GET /board aggregator: one call returns note, court, events, reminders, handled.board:editorial (note + rankedIds + parkedIds) to KV.channel_pending.summary + wants_action (the cached triage verdict); new calendar_events cache table for the *Coming up* zone.UpdatesView; WaitingCard + ChatPointer collapse to one pointer; /waiting deep-links into the court zone.Risks & guardrails
Ruthless curation hiding something
*Handled / FYI* keeps the quiet record of what auto-resolved and what Mallory decided didn't need Alex — collapsed by default, but present so the judgement is auditable. Auditability is what earns trust in the curation.
Triage cost runaway
Triage runs once per item — at first capture or when the snippet changes — never on an unchanged 15-minute tick. Cost stays near the old 4×/weekday level, not 96×/day.
Breach-gate regression
The highest-risk behavioural change: most test cases live here. Quiet hours and the runaway hard cap still apply on top.
Nudge history churn
nudges is demoted to a push ledger — no longer a display source — so the 3-day prune and the broken response_sla rendering both become non-issues.
Delivery
Phase 1 — Data foundation
Migration 024, calendar_events + board-reminder db helpers, triage-aware upsertPending, pure zone assembly, GET /board + GET /reminders.
Phase 2 — Judgement + delivery
Composer prompt/parse with a configurable model, the composer cron at briefing/EOD/midday, breach-only push gate, once-per-item LLM triage on the poll.
Phase 3 — Runtime producers
POST /ingest/calendar upserting calendar_events (Access-gated); retire the VM mention-nudge path.
Phase 4 — Web surfaces
loadBoard() + boardAction() API client, SituationBoard replacing UpdatesView, one chat pointer, /waiting redirecting into the board.
Out of scope
docs/superpowers/specs/2026-07-18-situation-board-design.mdplan ·
docs/superpowers/plans/2026-07-18-situation-board.md