Tzeusy

eod-tomorrow-prep

Daily tomorrow-prep briefing, review tomorrow's calendar and fold in today's cross-butler specialist highlights into a multi-domain preparation summary sent via Telegram (sent at the end of the day, 23:00 SGT, to prepare for the day ahead)

Tzeusy 0 Updated 2mo ago
GitHub

Install

npx skillscat add tzeusy/butlers/eod-tomorrow-prep

Install via the SkillsCat registry.

SKILL.md

Tomorrow Prep

This skill runs every day at 23:00 SGT (UTC+8) as a scheduled task. It reviews
tomorrow's calendar, folds in today's cross-butler specialist highlights, and sends
a structured multi-domain preparation summary to the user via Telegram. The message
is sent at the end of the day to prepare the user for the day ahead. Frame it as
"Tomorrow Prep" and title it with tomorrow's date so the future date is expected.
Do not call it an "end-of-day briefing".

The specialist highlights come from the collect_briefing_contributions job (cron
58 6 * * *, which is 14:58 SGT), which aggregates each specialist butler's daily
contribution into a combined payload under briefing/combined/<today-SGT> earlier
the same day. This skill reads that payload and surfaces the domains that have
updates. When no payload exists, it degrades gracefully to a calendar-only summary.

When to Use

Triggered automatically by the eod-tomorrow-prep scheduled task (cron: 0 15 * * *,
which is 23:00 SGT).

Workflow

Step 1: Determine Today's and Tomorrow's Dates (SGT, UTC+8)

Compute both today's and tomorrow's dates in Singapore Time (UTC+8). Today's date
keys the specialist payload lookup; tomorrow's date frames the calendar timeline.
Date string format is YYYY-MM-DD. All event times throughout this skill are
expressed in SGT.

from datetime import datetime, timedelta, timezone

SGT = timezone(timedelta(hours=8))
now_sgt = datetime.now(SGT)
today_sgt = now_sgt.strftime("%Y-%m-%d")
tomorrow_sgt = now_sgt + timedelta(days=1)

start = tomorrow_sgt.replace(hour=0, minute=0, second=0, microsecond=0)
end   = tomorrow_sgt.replace(hour=23, minute=59, second=59, microsecond=0)

Step 2: Fetch Specialist Highlights (Briefing Contributions)

Call state_get('briefing/combined/<today-SGT>'), where <today-SGT> is today's
date in YYYY-MM-DD format (SGT). This returns a JSON payload with a
contributions list. Each entry has these fields:

  • butler: the specialist butler that produced the contribution
  • has_updates: boolean, whether this specialist has anything to report today
  • highlights: structured highlight data
  • summary: a pre-rendered one-line summary string for direct inclusion
combined = state_get(f"briefing/combined/{today_sgt}")
contributions = (combined or {}).get("contributions", [])

If state_get returns null or empty, proceed in calendar-only mode: skip the
"Today's Highlights" section entirely and build the summary from the calendar alone.

Step 3: Fetch Tomorrow's Events

Call calendar_list_events with the computed start and end bounds (SGT, 00:00 to
23:59 tomorrow):

events = calendar_list_events(
    start=start.isoformat(),
    end=end.isoformat(),
)

For each event, extract title, start time (SGT), duration, location, and any
description notes. Sort events by start time (ascending) to build a chronological
timeline.

Step 4: Compose the Multi-Domain Summary

Open with a title line that makes clear this previews the day ahead, using
TOMORROW's weekday and date so the future date reads as intentional:

**🌅 Tomorrow Prep — [Tomorrow's Weekday, Date]**

Do not title this an "end-of-day briefing"; it is sent at the end of the day,
23:00 SGT, to prepare for tomorrow.

Then assemble the body sections:

Calendar — [Day, Date]
List tomorrow's events in chronological order (times in SGT). For each event:
title, time, duration, location. Include prep notes (documents to review, travel
time, items to bring), inferred from the event title, description, and location.
Note any free blocks between events. If there are no events:
"No events scheduled for tomorrow."

Today's Highlights
Include this section only if at least one specialist contribution has
has_updates=true. Omit the section entirely if no contributions are present or all
have has_updates=false (calendar-only mode).

For each specialist domain with has_updates=true, include one concise line using
the pre-rendered summary field. Group lines under these fixed domain labels (one
label per specialist butler, in the order specialists are declared in
src/butlers/jobs/briefing.py::SPECIALIST_BUTLERS):

Learning (education) · Finance · Health · Home · Lifestyle · Relationships · Travel

Heads-up (optional)
Include only when two or more high-priority highlights across different domains
suggest a cross-domain conflict or compounded risk (for example, a travel departure
that overlaps a medical appointment). Keep it to one or two sentences maximum.

Step 5: Length Check

Ensure the full message is under 500 words for mobile readability. Trim event prep
notes or specialist summaries if needed to stay within the limit. The calendar
timeline takes priority over the Highlights section when trimming.

Step 6: Send via notify()

Send the composed message using intent="send" (a proactive outbound notification,
not a reply). Scheduled prompt sessions have no interactive user, so notify() is
the only way to reach the user, and request_context is not required for intent="send":

notify(
    channel="telegram",
    intent="send",
    message=<composed_message>,
)

Do not wait for a user reply.

Exit Criteria

  • Today's and tomorrow's dates determined in SGT (UTC+8)
  • state_get('briefing/combined/<today-SGT>') attempted; specialist contributions
    used when present, calendar-only fallback when absent or empty
  • calendar_list_events called with correct SGT start/end bounds for tomorrow
  • Calendar timeline composed with chronological events, prep notes, and free blocks
  • "Today's Highlights" included only when at least one contribution has
    has_updates=true, grouped under the fixed domain labels
  • Optional "Heads-up" included only for cross-domain conflicts
  • Full message under 500 words; calendar prioritized over Highlights when trimming
  • notify(channel="telegram", intent="send", ...) called once
  • Session exits after delivery