name: request-radar description: >- Track inbound requests and follow-ups directed at Leo across Outlook, Teams, ServiceNow, and Azure DevOps — who asked what, when, and whether it's still open. Use this skill whenever Leo asks to "track down requests", "what do I owe people", "follow-ups", "what's in my inbox", "what am I being asked for", "who's waiting on me", "did I reply to X", or wants a triage board of pending asks across email + chat + tickets + work items. Also use it to refresh the radar, render the task board, or mark an item resolved/snoozed/promoted. Triggers on: requests, follow-ups, follow ups, inbox triage, outstanding asks, who's waiting on me, /radar. compatibility: >- Requires the bb-azure-ops operational layer (cloudpc SOCKS tunnel + Graph O365 token). Outlook/Teams need ~/.graph-token.json (Chat.Read + Mail.Read). ADO needs az --as-o365. SNOW needs /tmp/snow-cookies.txt. Each source degrades independently if its auth is down. allowed-tools: Read, Glob, Grep, Bash
request-radar
Inbound asks scatter across four channels — email, Teams chat, ServiceNow tickets, and ADO work items/PRs. They have no shared home, so things you owe people fall through the cracks. This skill keeps a persistent ledger of every request/follow-up, suggests a status for each, and renders a triage board so you can see at a glance what needs you vs what's parked elsewhere.
The design is scripts-as-data-producers + a JSON ledger as system of record. The heavy network scan runs fire-and-forget in the background; the board is the payoff at the end.
When to reach for it
- "Track down the requests / follow-ups that came in" → run a full refresh + board.
- "What's still open / who's waiting on me?" →
statusorboardoff the cached ledger. - "I replied to Tim / that's done" →
resolvethe item so it stops surfacing. - "That's real work now" →
promoteit into beads. - Session start → the nudge already printed the open/waiting counts (see hook below).
Architecture
scripts/radar.py one CLI: scan | reconcile | board | nudge | status
| resolve | open | snooze | promote | link
| comment | serve
scripts/refresh.sh background driver: token refresh -> 4 scans (parallel)
-> reconcile -> render board
state/request-radar/
ledger.json SYSTEM OF RECORD, keyed by source+thread
scans/<source>.json latest raw scan per source (data producer output)
board.html the canvas
last-run.json heartbeat for the nudge
Story key = outlook:<conversationId> | teams:<chatId> | snow:<number> |
ado:wi:<id> | ado:pr:<id>. Stable across runs, so reconcile dedups cleanly.
How to run a full refresh (the /radar path)
Run it in the background — token refresh alone is ~30s and the Teams scan walks ~145 chats. Do NOT block the session on it.
# fire-and-forget; you'll be notified on completion
~/.claude/skills/request-radar/scripts/refresh.sh # refreshes O365 token first
# or, if ~/.graph-token.json is already fresh:
~/.claude/skills/request-radar/scripts/refresh.sh --no-token
On completion the driver launches the interactive server (radar.py serve) and prints its
http://<tailscale-ip>:8899/ URL as the last line. Hand Leo that :8899 URL — it is
read-write, so card comments + resolve/archive persist. Do NOT default to ropen: that
serves the board as a read-only file server, the /api/* POSTs silently 404, and edits never
stick (the recurring "board updates don't persist" footgun — fixed 2026-06-02 by serve-by-default).
# refresh.sh now echoes the :8899 interactive URL; just relay it to Leo.
# Fallback only if serve is unreachable (headless host): ropen (read-only)
ropen ~/.claude/state/request-radar/board.html
Then summarize the triage in chat: lead with the open column (what needs Leo), then
waiting, then what newly resolved. Pull the list with radar.py status.
The board is live (client-side)
board.html is a static shell that polls ledger.json every 12s (co-served by ropen from
the same dir) and re-renders client-side. Consequences:
- A
resolve/snooze/open/promotemutation shows on an already-open tab within ~12s — you do NOT need to re-runboardor re-ropenafter a mutation. - You only re-
ropenwhen the shell changed (a renderer edit), not when data changed. - The board view is real-time; the data is only as fresh as the last scan. For continuously
fresh data, pair with
/loop 10m /radar(periodic background re-scan) — but that re-refreshes the O365 token + walks ~145 Teams chats each cycle, so reserve it for active triage windows. - Source-filter chips (Outlook/Teams/SNOW/ADO) toggle visibility client-side; degraded sources show an amber dot + the error.
Interactive mode (comments + write-back)
ropen board.html is read-only (file server — comment inputs show "read-only"). To make the
board interactive, run the bundled server:
~/.claude/skills/request-radar/scripts/radar.py serve # binds 0.0.0.0:8899, prints the Tailscale URL
Then open the printed http://<tailscale-ip>:8899/ on Leo's Mac (NOT the ropen URL). That server:
- serves
board.html+ledger.json(so polling/dates/cards all work), and - accepts
POST /api/comment {id,text}andPOST /api/status {id,action}— comments typed on a card persist straight intoledger.json. Cards show a per-note date; existing comments render above an "add a note…" input that appears on hover.
CLI equivalents: radar.py comment <id> "text" and the existing resolve/open/snooze. The server
is session-scoped (launched fire-and-forget); for a durable always-on board, wrap it in a
systemd-user unit.
Status model
Each story carries a suggested_status (heuristic) and a confirmed status (what the
board shows). On first sight confirmed = suggested. When Leo runs resolve/open/snooze,
manual=true and the override persists — unless genuinely-new activity arrives after the
override (a resolved item with a new reply re-surfaces automatically).
| Status | Meaning | Heuristic |
|---|---|---|
open |
Needs Leo | someone else spoke last with a "?" / request verb / @mention |
waiting |
Ball elsewhere | Leo spoke last, or informational with no ask |
inbox |
Read, not act | triaged as a notification/FYI — own purple lane, kept visible, never auto-hidden |
resolved |
Done | close-out language, or Leo's last msg reads as a wrap-up |
archived |
Dismissed | not an action item; hidden from the board (manual archive) |
snoozed |
Hidden until a date | snooze --days N |
Triage gate (run by /radar after reconcile): triage-queue lists new, untouched items;
classify each as a real request/Leo-or-DevOps responsibility → keep, or a notification → inbox.
The board has four lanes — Open / Waiting / Inbox / Resolved. Per-card hover actions: resolve
(✓) and archive. The inbox/keep/archive CLI + /api/status mirror these.
The heuristic is deliberately loose — it's a suggestion. Expect false-opens (standup agendas, meeting invites, OOO replies); Leo resolves them once and they stay resolved.
Dormancy rule: an Outlook/Teams ask that ages out of the 7-day window but is still open stays on the board — an unanswered ask is more urgent at day 8, not less. ADO/SNOW rows that drop out of their query vanish (the work item moved on / ticket closed).
Mutations
radar.py status # triage list (open + waiting), cached, no scan
radar.py resolve <id> --note "..." # mark done
radar.py open <id> # force back to open
radar.py snooze <id> --days 7 # hide for a week
radar.py promote <id> # prints a `bd create` line; then `link <id> --bd-id <id>`
Source-specific notes
- Outlook — inbox + sent in-window, collapsed per conversation. Sent items decide whether Leo replied last. Automated senders (Azure, alerts, ServiceDesk) are filtered as noise.
- Teams — enumerate chats, scan messages per chat client-side (there is no usable
server-side Teams search with our token — see
bb-azure-opsgraph-endpoints). 1:1s and @mentions rank high; large meeting chats rank low. - SNOW —
sc_task+incidentassigned to Leo, active. Needs live cookies; degrades to empty + a clear error when stale. Re-runsnow-refreshfirst if you want SNOW coverage. - ADO — work items assigned to
@Me(org-wide WIQL, filtered to changed-in-window so the standing backlog doesn't flood the board) + active PRs authored by Leo (reviewer-of- others PRs are intentionally excluded — those aren't Leo's task).az --as-o365token.
Failure handling
Every scanner exits 0 and records {"ok": false, "error": ...} so one dead source never
aborts the pipeline. The board's source line shows per-source health (ok / degraded: ...).
If everything is empty, check the SOCKS tunnel (ss -tlnp | grep 1080) and token freshness
(bb-azure-ops § smoke test).