Session 2.8: A daily brief that wakes up before you do

How to run this session

This guide is built to be handed to an AI assistant, which then walks you through it one step at a time. Claude Code in a terminal, Claude Desktop, and Codex all work. Save this page to your machine, open a fresh assistant session, give it this file, and paste this:

Walk me through Session 2.8 one step at a time.
Give me only the current step. Stop after each step and wait for me to say it's done.
Before each step, check whether it's already done on this machine, and skip it if it is.
Tell me what each command does before I run it.
Start at the beginning.

Then work through it together. Tell the assistant when a step is finished, or paste the error when one isn’t.

Assistant: the rules below govern this whole session. They override any default habit of reading a document and summarizing it.

  1. One step per message. Give the learner the current step and nothing else. Do not list the steps that follow, do not preview what is coming, and do not merge two steps because they look short. An outline of the session counts as revealing the steps, so do not produce one. If the learner asks how long this will take, give a count and a time estimate rather than a list.
  2. Stop and wait. End each step by asking the learner to do it and report back. Do not continue until they answer.
  3. Check before you instruct. Most steps name a way to tell whether the work is already done on this machine. Ask for that check first. When it passes, say the step is already satisfied and move on.
  4. Explain before you instruct. For every command, say in one or two sentences what it actually does and why this step exists, before the learner runs it.
  5. Debug in place. When something fails, fix it with the learner here. Do not send them to a documentation page, and do not skip ahead.
  6. Resume cleanly. If the learner comes back later and says where they stopped, confirm that position and give the next step only. Do not recap what they already did.
  7. Posture. Every example uses public sources, invented scenarios, or the learner’s own personal material. Never ask for anything belonging to their employer or a client. If they offer some, redirect to a public equivalent.
  8. Audience. A capable professional on macOS who has not lived in a terminal day to day. Write peer to peer. Do not explain the learner’s own field back to them.
  9. The Reference Sections below are addressed to you, not to the learner. They are stage directions. Turn them into steps and deliver them one at a time. Never paste them at the learner.
  10. Which tool the learner uses. This curriculum works with Claude Code or Codex. The learner’s CLAUDE.md or AGENTS.md records which one; if it does not, ask once and do not ask again. Where a step below has a ### Claude Code version and a ### Codex version, deliver only theirs and do not mention the other exists. Everywhere else the work is identical, so say “your assistant” rather than assuming Claude. This is about the tool the learner installs and works in, not about which assistant is reading this guide.

Run this from ~/ai-training, the working directory you made in Session 2.1. About 75 minutes.

Posture: public, synthetic, or personal data only. The brief reads from your own calendar, your own personal Gmail, and public sources (Hacker News, FRED, etc.). Nothing client-internal. Nothing from a work account.


Practice task

By the end of this session you will have, in ~/ai-training/:

  1. briefing.py — a Python script that, in parallel, gathers (a) today’s calendar from Calendar MCP, (b) the last 24 hours of unread emails from Gmail MCP, (c) the top 3 stories from Hacker News, and outputs a one-page markdown brief separated into “Must Do Today” and “Strategic 3 (longer-term).”
  2. A scheduled launchd job (~/Library/LaunchAgents/com.<you>.morning-brief.plist) that runs briefing.py every morning at 6:00 AM, even if your laptop was asleep at the time.
  3. The first real brief — ~/ai-training/briefs/2026-05-07.md — produced by the script and reviewed.
  4. A failure-email pattern: if briefing.py errors, an email lands in your inbox with the traceback, so silent failures don’t accumulate.

Three artifacts plus a working schedule. Tomorrow morning a brief lands before you wake up. Check it on your phone in 10 minutes.

A production version of this is a “Good morning” workflow that gathers seven sources in parallel — calendar, both inboxes, action items, current-week priorities, longer-term goals, the Claude Mail alias from 2.2, people/projects in flight — and outputs Must-Do / Strategic-3. A launchd plist (com.<user>.email-briefing.plist) fires at 6 AM daily; if the laptop was asleep, it runs on next wake. The brief is read on a phone over coffee. The discipline that makes it work: the brief is text in an email body — no laptop required to consume.


Why brief-then-schedule

Most “AI-powered productivity tool” demos show the same thing: someone types a prompt, output appears, end of demo. The user goes back to typing the same prompt every morning manually. Three weeks later they stop using the tool.

The unlock is structural: the brief has to land without you asking for it. Scheduling moves the cost from “remember to run it” to “decide once whether the brief is useful.” If it’s useful, it just shows up. If it’s not, you delete the launchd job. Either way, the daily cost is zero.

Two pieces fall out of this:


Step 1: Verify prerequisites (3 minutes)

You need:

  1. Gmail MCP from 2.2, working. claude mcp list shows it connected.
  2. A Calendar MCP. If you haven’t installed one, do it now — same path as Gmail (Composio / Rube has Calendar bundled with the same OAuth, or self-host).
  3. Python 3 with requests available: pip3 install --user requests.
claude mcp list   # confirm both Gmail and Calendar show connected

What this does. Lists every MCP server Claude Code has configured for this project and shows whether each one is connected right now.


Step 2: Hand Claude this guide

Step 1 was yours. The rest runs through Claude. If you haven’t handed it this guide yet, follow “How to run this session” at the top. From inside ~/ai-training, start claude, give it this file, and add one line so it knows where you are:

Gmail and Calendar MCPs are connected.

Claude takes over from here, walking through the script build, the launchd schedule, and the failure-email pattern one step at a time.


Reference Sections

Claude: walk the user through the script build, the schedule install, and the failure-email pattern. Peer-tone. Two rules:

  1. The script is real Python that reads from real MCPs. Don’t write it as one-shot code in a tool call. Save it as a file; run it as a file. Re-running has to be one command.
  2. The schedule has to actually work overnight. Test it once during the session by setting a 2-minute trigger and watching it fire, then change to 6 AM. Don’t ship an untested schedule.

Design: this session uses every memory + technique pattern from prior sessions — MCP composition (2.2), Python scripts (2.4), exhibits/ and folder discipline (2.4), MEMORY.md preferences (2.5), sub-agents and parallelism (2.6), voice + fact-check (2.7) on the brief output. The brief is the integrative artifact.


Confirm the setup

Before any script work:

  1. Gmail and Calendar MCPs both show connected in claude mcp list.
  2. The user can articulate what would make a useful brief tomorrow morning. Push for specificity. “What’s on my calendar plus what’s in my inbox” is too vague. “What meetings, prepped or unprepped; emails from anyone in [list], or marked urgent; and three things in ” is workable.

Step A — Write briefing.py (25 minutes)

Already done? Run ls ~/ai-training/briefing.py. If it exists, skip to Step B.

Build the script with the user. Skeleton at ~/ai-training/briefing.py:

#!/usr/bin/env python3
"""Daily morning brief — gathers calendar, email, and public news in
parallel, outputs a markdown brief."""

import json
import subprocess
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta
from pathlib import Path

import requests

OUT_DIR = Path.home() / "ai-training" / "briefs"
OUT_DIR.mkdir(parents=True, exist_ok=True)

def fetch_calendar():
    """Today's calendar events. Returns list of {time, title, attendees}."""
    # Call Claude with the Calendar MCP via `claude -p`
    prompt = (
        "Use the Calendar MCP to list every event on my primary calendar "
        "for today, in chronological order. Output as JSON: a list of "
        '{"time": "HH:MM", "title": "...", "attendees": ["..."]}. '
        "JSON only, no commentary."
    )
    out = subprocess.run(
        ["claude", "-p", prompt, "--output-format", "json"],
        capture_output=True, text=True, timeout=120,
    )
    return json.loads(out.stdout)

def fetch_email():
    """Unread emails from the last 24 hours. Returns list of {from, subject, snippet, urgency}."""
    prompt = (
        "Use the Gmail MCP. Search for messages from the last 24 hours "
        "that are unread, NOT in Promotions/Social/Updates, NOT from "
        "no-reply addresses. For each, extract: from, subject, "
        "one-sentence snippet, urgency (1-5). Sort by urgency desc. "
        'Output as JSON list: [{"from": "...", "subject": "...", '
        '"snippet": "...", "urgency": N}]. JSON only.'
    )
    out = subprocess.run(
        ["claude", "-p", prompt, "--output-format", "json"],
        capture_output=True, text=True, timeout=120,
    )
    return json.loads(out.stdout)

def fetch_hn():
    """Top 3 Hacker News stories. Returns list of {title, url, score}."""
    ids = requests.get(
        "https://hacker-news.firebaseio.com/v0/topstories.json", timeout=10
    ).json()[:3]
    items = []
    for sid in ids:
        item = requests.get(
            f"https://hacker-news.firebaseio.com/v0/item/{sid}.json", timeout=10
        ).json()
        items.append({
            "title": item.get("title"),
            "url": item.get("url"),
            "score": item.get("score"),
        })
    return items

def write_brief(cal, mail, hn, out_path):
    """Compose the markdown brief — Must Do Today vs Strategic 3."""
    today = datetime.now().strftime("%Y-%m-%d %A")
    lines = [f"# Morning brief — {today}", ""]

    lines.append("## Must Do Today")
    lines.append("")
    if cal:
        lines.append("### Calendar")
        for e in cal:
            atts = ", ".join(e.get("attendees", []) or [])
            lines.append(f"- **{e['time']}** {e['title']}" + (f" — {atts}" if atts else ""))
        lines.append("")
    if mail:
        urgent = [m for m in mail if m.get("urgency", 0) >= 4]
        if urgent:
            lines.append("### Urgent email")
            for m in urgent:
                lines.append(f"- **{m['from']}** — {m['subject']}: {m['snippet']}")
            lines.append("")

    lines.append("## Strategic 3")
    lines.append("")
    lines.append("### Top public stories")
    for h in hn:
        lines.append(f"- [{h['title']}]({h['url']}) (score {h['score']})")
    lines.append("")
    if mail:
        rest = [m for m in mail if m.get("urgency", 0) < 4]
        if rest:
            lines.append("### Other email worth knowing about")
            for m in rest[:5]:
                lines.append(f"- **{m['from']}** — {m['subject']}")
    out_path.write_text("\n".join(lines))

def main():
    out_path = OUT_DIR / f"{datetime.now().strftime('%Y-%m-%d')}.md"
    with ThreadPoolExecutor(max_workers=3) as ex:
        cal_f = ex.submit(fetch_calendar)
        mail_f = ex.submit(fetch_email)
        hn_f = ex.submit(fetch_hn)
        cal, mail, hn = cal_f.result(), mail_f.result(), hn_f.result()
    write_brief(cal, mail, hn, out_path)
    print(f"Wrote {out_path}")

if __name__ == "__main__":
    main()

Save and make executable:

chmod +x ~/ai-training/briefing.py

What this does. chmod +x sets the executable bit on briefing.py, marking it as a file the shell is allowed to run directly instead of just read. That pairs with the #!/usr/bin/env python3 line at the top of the file, the shebang, which tells the system which interpreter should run the rest of the file. Together they let you invoke the script as ./briefing.py instead of typing python3 every time. Skip chmod +x and ./briefing.py fails with a permissions error even though the shebang line is correct.

Worth knowing. Both invocations in this guide, the manual test below and the launchd job in Step B, call python3 briefing.py directly, so the executable bit isn’t load-bearing here. It matters once you or a teammate runs ./briefing.py on its own, which is the more common pattern outside this guide.

Run it now to test:

python3 ~/ai-training/briefing.py
cat ~/ai-training/briefs/$(date +%Y-%m-%d).md

What this does. python3 runs the script once, exactly like the launchd job will every morning, and cat prints the brief it just wrote so you can check the output immediately.

The brief lands as a markdown file. Read it together. The structure should be: Must-Do (calendar + urgent email) on top, Strategic-3 (public news + the rest of email) below. Iterate the prompt strings inside fetch_calendar and fetch_email until the output matches what the user wants.

Claude: pay attention to the timeout values. claude -p with MCP calls can take 60–90 seconds; 120-second timeout is the floor. If the user wants the brief to land faster, the bottleneck is rarely the script — it’s the MCP latency.


Step B — Schedule with launchd (15 minutes)

Already done? Run launchctl list | grep morning-brief. If it prints a line, the job is already scheduled. Skip to Step C.

launchd is macOS’s native scheduler. The relevant pieces: a .plist file in ~/Library/LaunchAgents/, a StartCalendarInterval for wall-clock triggers, and load/unload to register/deregister.

Create ~/Library/LaunchAgents/com.<you>.morning-brief.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.<you>.morning-brief</string>

    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>-lc</string>
        <string>unset CLAUDECODE; cd /Users/<you>/ai-training && /usr/bin/python3 briefing.py >> briefs/launchd.log 2>&1</string>
    </array>

    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key>
        <integer>6</integer>
        <key>Minute</key>
        <integer>0</integer>
    </dict>

    <key>RunAtLoad</key>
    <false/>

    <key>StandardOutPath</key>
    <string>/Users/<you>/ai-training/briefs/launchd.out</string>

    <key>StandardErrorPath</key>
    <string>/Users/<you>/ai-training/briefs/launchd.err</string>
</dict>
</plist>

Replace <you> with the user’s actual username. Then load:

launchctl load ~/Library/LaunchAgents/com.<you>.morning-brief.plist
launchctl list | grep morning-brief

What this does. launchctl load reads the plist and registers it with launchd, the background process that manages scheduled jobs on macOS. From that point on, launchd owns triggering the script at 6:00 AM, even after you close the terminal or reboot the machine. launchctl list | grep morning-brief confirms the job registered; a matching line means launchd knows about it. launchctl unload, used later to test the schedule, reverses this and removes the job.

Three traps to flag explicitly with the user:

  1. unset CLAUDECODE. When launchd invokes a shell that calls claude, an inherited CLAUDECODE environment variable can confuse Claude Code into thinking it’s running inside another Claude session. Always unset it in the wrapper script.
  2. StartCalendarInterval vs StartInterval. StartCalendarInterval fires at wall-clock times (6:00 AM). StartInterval fires every N seconds since boot. If the user picks StartInterval thinking they want “every morning,” the brief fires 6 hours after every boot, which is not “every morning.”
  3. Asleep at 6 AM? launchd handles this — when the laptop wakes, it runs the missed job. But the brief will be 30 minutes late on a wake-from-sleep day. Acceptable; just so the user isn’t surprised.

To test the schedule without waiting until 6 AM tomorrow, change the StartCalendarInterval to a time 2 minutes from now, unload + reload:

launchctl unload ~/Library/LaunchAgents/com.<you>.morning-brief.plist
# edit the plist to a time 2 min from now
launchctl load ~/Library/LaunchAgents/com.<you>.morning-brief.plist

What this does. Same launchctl unload/load cycle as above: unloading drops the current schedule, the manual edit swaps in a near-term trigger, and loading registers it again so you can watch it fire without waiting until tomorrow.

Wait. A new brief should land in briefs/. If it doesn’t, check briefs/launchd.err for the failure. After the test fires, change back to 6:00 AM and reload.


Step C — Failure-email pattern (10 minutes)

Already done? Run grep email_failure ~/ai-training/briefing.py. If it prints a matching line, this step is already done. Skip ahead to Micro-skills introduced.

Scheduled jobs fail silently more often than they succeed silently. The discipline: if the script errors, send yourself an email. Add to the bottom of briefing.py (replace USER_EMAIL with your actual address before running):

USER_EMAIL = "you@example.com"  # replace with your address

def email_failure(traceback_text):
    """Send a failure email via Gmail MCP. Best-effort."""
    prompt = (
        f"Use the Gmail MCP to send a single email to {USER_EMAIL} with "
        f"subject 'briefing.py failed {datetime.now().isoformat()}' and "
        f"body:\n\n{traceback_text}\n\n"
        "Just send it. No drafts, no confirmations."
    )
    subprocess.run(
        ["claude", "-p", prompt],
        capture_output=True, text=True, timeout=120,
    )

if __name__ == "__main__":
    import traceback
    try:
        main()
    except Exception:
        email_failure(traceback.format_exc())
        raise

Claude: send-on-failure is a deliberate exception to the drafts-only default from Session 2.2. The reasoning: a silent failure that accumulates for a week is worse than a single auto-sent email to the user themselves. The send target is the user’s own personal email; no third-party recipient.

Test the failure path: temporarily break something in fetch_calendar (mistype the prompt). Run the script. An email lands. Fix the script.


Micro-skills introduced

Name these out loud:


Wrapping up Session 2.8

Three things to try this week:

  1. Read tomorrow morning’s brief on your phone. No laptop. Notice what’s useful, what’s noise. The brief should be readable in 10 minutes; if it isn’t, the prompts inside fetch_* are over-summarizing or under-filtering.
  2. Tighten the prompts after the first three briefs. Common adjustments: tighten the urgency rubric on email; add a “skip if matches these senders” exclusion; change Hacker News for a more domain-specific feed.
  3. Add one source. A FRED indicator, a public RSS feed, a regulations.gov check on a docket you care about. The script is parametric — adding a fourth fetch_* and a fourth section is 10 lines.

Feedback.

The user submits feedback at https://docs.google.com/forms/d/e/1FAIpQLSd-uHATmygW4H0jMLWvVBWrliVkGYBkNC5XI_bOdB1wq3bDvg/viewform.

Claude: paste the URL into chat. The form mirrors the questions below. Collect answers conversationally first, then have the user click through and submit.

  1. On a 1–5 scale, how useful did this session feel?
  2. Did landing the schedule feel like a real move, or did the launchd ceremony eat too much time?
  3. The Must-Do / Strategic-3 split — does it match how you’d consume the brief, or does it need a different shape?
  4. Failure-email — did wrapping the try/except feel paranoid, or like the right default for any scheduled job?
  5. Will you actually read tomorrow’s brief tomorrow morning? Honestly?
  6. What confused you most this session?
  7. Anything you want covered in Session 2.9 that you didn’t see here?

Tell the user: “Your instructor uses these to tailor next week’s session.”


Good to know

The brief generalizes. The same pattern produces a weekly review (every Sunday 6 PM), a Friday wrap (every Friday 5 PM), a monthly metric scan (first of every month). One script, different prompts, different schedules. Multiple variants on different cadences are common once the first one is wired up.

Don’t over-decorate the brief. Markdown only; no fancy formatting. The brief reads on a phone in 10 minutes; ASCII boxes and emoji break that. The aesthetic is “newspaper headlines,” not “design system.”

MCP latency dominates. A single MCP call is 5–30 seconds. Three in parallel is the same as three in serial because most of the time is in the slowest one. ThreadPoolExecutor with max_workers=3 is the right call.

The brief is the deliverable. The launchd plist, briefing.py, the failure-email — all infrastructure. The one thing the user actually consumes is the markdown file. Optimize for the consumer, not the producer.

Briefs accumulate. A year of daily briefs is 365 markdown files. Future-you can grep “Q3 2026 calendar” for context. Don’t delete them.


Taking it further: loops and cloud scheduling

The launchd pattern you just built works. These two tools extend it — one for in-session iteration, one for getting off your laptop entirely.

/loop — repeat a task inside an active session

/loop runs a task on a repeating interval within the current Claude Code session:

/loop <interval> <task>

What this does. /loop re-runs the given task on a repeating interval for as long as the session stays open, with <interval> setting the wait between runs (30m, 1h). Omit the interval and Claude paces itself based on the task instead of a fixed clock. Each run is a fresh pass over the task rather than a resumed one, so keep the task description self-contained. This is the tool used below to validate briefing.py before trusting it to launchd.

Examples:

Good for:

Practical use here: after briefing.py passes its manual test, open a Claude session and run /loop 5m python3 ~/ai-training/briefing.py three times before loading the launchd plist. If three runs are clean, the schedule is safe.

/schedule — cloud agent on a cron schedule

/schedule creates a scheduled cloud agent that runs on Anthropic’s infrastructure — no launchd plist, no laptop required to be on.

In a Claude Code session, type /schedule and Claude walks you through:

  1. What to do — e.g., run the briefing pipeline and email the output.
  2. When — a cron expression. 0 7 * * 1-5 = 7 AM every weekday.
  3. Where to send output — email, a Drive file, a Slack message.

The agent fires in the cloud on schedule. You get a notification on each run — success and failure alike. The laptop can be closed, off, or traveling.

Practical use here: once /loop validates the pipeline, use /schedule to turn briefing.py into a permanent cloud job. The brief pipeline you built in Step A becomes a cloud agent that fires at 7 AM every weekday without you doing anything.

The three automation approaches, side by side

Method What it does Requires laptop?
launchd / cron Runs a local shell command on schedule Yes
/loop Runs a task repeatedly in an active Claude session Yes (session must be open)
/schedule Creates a cloud agent on a cron schedule No

For a morning brief: launchd is fine if your laptop is always on your desk. /schedule is the upgrade if you travel or close the lid overnight.