Skip to content

/pre-meeting-brief

Pre-meeting brief generation

Category: infra
Field: general
License: MIT
Updated: 2026-04
Stages:

Pre-Meeting Brief

v1.3 — Granola health-check simplified after the Granola public-API rewrite. Phase 0f no longer probes transcript content (the upcoming meeting has no transcript yet, and probing an arbitrary prior transcript adds latency without a clean health signal — it was producing false HOLLOW). HOLLOW classification removed for Granola entirely; Granola is now OK / DEGRADED / UNAVAILABLE only. Phase 1c fallback clarified to hunt PRIOR project meetings for context, not the upcoming one. Source-confidence table, circuit breaker, and error-handling table aligned. v1.2 — Generates a structured pre-meeting briefing from project communications. Run from a project directory.

Pulls context from WhatsApp, Gmail, Granola, Google Docs, and local files. Surfaces decisions required, open loops, and preparation needs. Optionally emails the brief to attendees.

Prerequisites

Required: - Gmail MCP — for searching and reading message context - Google Calendar MCP — for identifying the target meeting and attendees

Recommended: - Granola MCP — for pulling meeting transcripts - WhatsApp MCP — for picking up project group threads - A project .claude/CLAUDE.md with team roster, group JIDs, dashboard doc ID, and folder paths

Optional: - A send-email helper script if you want one-step send (otherwise the skill creates a Gmail draft)

First-Time Setup

  1. Create a project config at [project-root]/.claude/CLAUDE.md with at minimum:

    Markdown
    # Project Name
    
    ## Team
    - [Name] <email@example.com> — PI
    - [Name] <email@example.com> — Research Manager
    - [Name] <email@example.com> — RA
    
    ## Folders
    - transcripts: `transcripts/`
    - weekly_reviews: `weekly-reviews/`
    
    ## Config
    ```yaml
    project_keywords: [keyword1, keyword2]
    exclude_keywords: [unrelated_term]
    whatsapp_groups: ["Project Group Name"]
    google_doc_id: "YOUR_DOC_ID"
    granola_folder: "Project Folder Name"   # optional
    
    ```

  2. Create a transcripts folder in your project directory:

    Bash
    mkdir -p transcripts/
    

  3. (Optional) Configure a routing file at ~/.claude-assistant/config/granola-routing.json if you have multiple projects pulling Granola transcripts. The skill uses this to list known projects when it can't find a config.

Arguments

$ARGUMENTS can include (combine freely): - (none) — next upcoming meeting with project context - [keyword] — filter by meeting title - nosend — skip the email phase - email — include email draft (prompt to send) - light — force Light depth (overrides auto-detection) - full — force Full depth (overrides auto-detection) - since:YYYY-MM-DD — override lookback start date

CRITICAL: No Permission Prompts

Do NOT use Task agents or ToolSearch. All required tools are pre-approved. Call them directly: - Read, Write, Bash, Glob, Grep - mcp__google_workspace__get_events, mcp__google_workspace__search_gmail_messages, mcp__google_workspace__get_gmail_message_content - mcp__google_workspace__get_doc_content, mcp__google_workspace__draft_gmail_message - mcp__granola-api__search_granola_events, mcp__granola-api__get_granola_document, mcp__granola-api__get_granola_transcript - mcp__whatsapp__list_messages, mcp__whatsapp__search_messages, mcp__whatsapp__list_chats

Risk escalation: - Data gathering (all reads/searches): Auto - Briefing display: Auto - Email draft display: Batch-confirm (show draft + recipients) - Email send: Per-item confirm (user must say "send")

Instructions

Phase 0: Identify Meeting + Pre-flight

0a. Read project config: Read $(pwd)/.claude/CLAUDE.md. Extract: - whatsapp_groups — group names (look up JIDs via sqlite3 ~/whatsapp-mcp-ts/data/whatsapp.db "SELECT jid, name FROM chats WHERE name LIKE '%[group_name]%'") - google_doc_id — project dashboard doc - team roster — all role groups (pis, research_managers, field_team, ras, etc.) - project_keywords, exclude_keywords - folders.transcripts, folders.weekly_reviews - granola_folder or granola_folders — Granola folder name(s) for transcript fetching - pre_meeting_brief section (if present — optional config overrides)

Extract fields from inside markdown code fences if present.

If no .claude/CLAUDE.md found: Stop. Read ~/.claude-assistant/config/granola-routing.json (if present) and list known project directories:

Text Only
No .claude/CLAUDE.md found in this directory.
Known project directories:
[list from granola-routing.json routes]
cd into a project directory and re-run /pre-meeting-brief.

0b. Identify target meeting: 1. Fetch today + tomorrow events via mcp__google_workspace__get_events (all calendars) 2. If [keyword] argument: filter by title contains keyword (case-insensitive) 3. Else: match events where attendee emails overlap project team roster, OR title contains any project_keyword 4. If 0 matches: "No matching meetings found today or tomorrow. Specify a keyword or check your calendar." 5. If 2+ matches: show numbered list, ask user to pick 6. If 1 match: proceed

0c. Determine depth + funder flag: - Auto-Light if: meeting ≤30 min AND calendar title contains "standup", "check-in", "weekly sync", "1:1", "one-on-one" AND no full argument - Auto-Full otherwise - Override with light or full argument - Funder flag: set if any attendee matches known funder contacts from a configured donor profiles folder (e.g., ~/Documents/donors/) or if the project config lists them as funders. If set, donor profile lookup is included in Phase 1.

0d. Determine lookback window: Priority order: 1. since:YYYY-MM-DD argument (explicit override) 2. Most recent weekly-review-*.md in folders.weekly_reviews 3. Fallback: 14 days ago

Display in output header: "Lookback since [date] ([anchor: weekly review / 14-day default])"

0e. Populate local transcripts: If granola_folder or granola_folders is configured in .claude/CLAUDE.md and you have a transcript fetcher (e.g., granola-fetch): Run the fetcher once per configured folder for the lookback window. This ensures local transcript files are up-to-date before Phase 1. If neither field exists: skip. If the fetcher fails: note the error, continue (Phase 1 will use existing local files + MCP fallback).

0f. Pre-flight health checks:

Run all checks silently. Results feed into the Phase 2 banner. No user interaction.

Fire in parallel: 1. WhatsApp SQLite: sqlite3 ~/whatsapp-mcp-ts/data/whatsapp.db "SELECT max(timestamp), count(*) FROM messages WHERE chat_jid IN ([configured JIDs]) AND timestamp > [lookback_epoch]" 2. Gmail: one test search with project keywords — check auth + result count 3. Granola: search_granola_events with project keywords to check that the API + key are working AND that the project has prior meetings indexed. Do NOT probe transcript content here — the upcoming meeting has no transcript yet, and probing an arbitrary prior-meeting transcript adds latency without a clean health signal. - Events found → OK - No events found → DEGRADED ("no prior project meetings indexed in Granola") - Auth error / unreachable / 401 → UNAVAILABLE (re-auth Granola per your provider's instructions) 4. Google Doc: Read first 500 chars via mcp__google_workspace__get_doc_content (if google_doc_id configured). If readable but body < 100 chars → HOLLOW. 5. Local files: glob transcripts folder — check existence

Source confidence thresholds:

Source OK HOLLOW DEGRADED UNAVAILABLE
WhatsApp Recent messages in lookback via SQLite SQLite has data but no messages since last meeting; OR no messages in lookback but group normally active DB locked, query fails
Gmail Search returns results Search returns 0 (may be normal for some projects) Auth error after retry, timeout, or API exception
Granola Events found for project keywords 0 events found (new project, keyword mismatch, or no prior meetings) 401 / unreachable; re-auth Granola
Google Doc Readable with content Readable but < 100 chars body → note "Google Doc nearly empty — may not be up to date" 403 (permissions changed), 404, timeout
Local files Transcripts folder has files in lookback window Folder exists but empty (normal for new projects) Folder not found

HOLLOW counts as DEGRADED for the minimum-signal gate.

Minimum-signal gate: Require ≥2 sources at OK or DEGRADED/HOLLOW. If only 1 source is functional → show a raw data dump with: "Insufficient data for structured brief. Only [source] available. Raw findings below." Do not present as a structured briefing.


Phase 1: Gather Data (inline parallel MCP calls)

Per-source call limits (Full depth):

Source Max calls Includes Circuit breaker
WhatsApp 5 MCP queries + SQLite fallback Stop after 2 consecutive empty responses
Gmail 8 All searches + content reads combined Stop if auth fails after 1 retry
Granola 5 Pre-flight doc check + Phase 1 reads Stop immediately on first error (401/timeout) in Phase 1
Google Doc 2 Read calls Stop after first error
Local files 3 Glob + reads Silent skip on any error

When a source hits its cap: stop immediately. Note "[SOURCE] capped" in the source banner. Proceed to synthesis with what was gathered. Do NOT debug or retry.

Granola Phase 1 check: before making any Phase 1 Granola calls, check if Phase 0f flagged Granola as DEGRADED or UNAVAILABLE. If UNAVAILABLE, skip Granola in Phase 1 entirely. If DEGRADED (no prior project meetings), skip the Phase 1c MCP fallback but still attempt local-file reads.

Full depth (target 50, warn at 80 tool calls):

Fire these in parallel where possible:

  • 1a. WhatsApp (if OK/DEGRADED):
  • MCP path (if OK): Query via mcp__whatsapp__list_messages. Filter received messages in-context: discard any message where timestamp (ISO 8601 string, format: YYYY-MM-DDTHH:mm:ss.sssZ) is earlier than lookback_start. The column is already ISO 8601 text — compare as strings: timestamp >= lookback_start_iso.
  • SQLite path (if DEGRADED): Query with WHERE timestamp >= '[lookback_start_iso]' (ISO string comparison works correctly on the messages table schema).
  • Post-process (both paths): Group messages by calendar day. Insert date headers between day groups when passing to synthesis:

    Text Only
    --- March 18, 2026 ---
    [messages from that day]
    --- March 17, 2026 ---
    [messages from that day]
    
    Date headers appear in synthesizer context only — not in the output sections. Do not include messages earlier than lookback_start in the context passed to synthesis.

  • 1b. Gmail — 4-search priority stack, fire in parallel:

  • Search 1 (PI priority, 2d): from:({pi_emails}) newer_than:2d — read ALL results. These are highest-priority input regardless of recency within the lookback window.
  • Search 2 (attendee recent, 3d): (from:{attendee_emails}) newer_than:3d
  • Search 3 (team + keywords, full lookback): (from:{team_emails}) ({keywords}) newer_than:{lookback_days}d
  • Search 4 (WhatsApp compensation — only if WhatsApp DEGRADED/UNAVAILABLE): (from:{team_emails}) newer_than:{2*lookback_days}d
  • Read content for top 10-15 relevant messages. Prioritize Search 1-2 results.
  • Gmail cap: 8 total calls (4 searches + up to 4 content reads) max. Agenda detection (Phase 1g) reuses gathered content — does not fire new searches.

Source ranking in synthesis: When multiple sources cover the same topic, weight: recent PI email (Search 1-2) > posted agenda > recent WhatsApp > older transcripts/email. When sources conflict: note the conflict rather than silently preferring one.

  • 1c. Granola transcripts: Primary: Read local .txt files from the transcripts folder (populated by Phase 0e). Apply hollow-transcript check: FULL (≥5,000 chars + conversational speaker content) vs HOLLOW (<5,000 chars, starts with ###, or bullet-only without speaker attribution). Exclude hollow transcripts from substantive use. Fallback (only if no local files AND prior project meetings exist — for upcoming meetings the current meeting has no transcript yet, so this fallback fetches PRIOR project meetings for context, not the upcoming one): Call list_granola_documents to get Granola UUIDs, matching to prior project meetings by title/date. Use those UUIDs with get_granola_transcript — never use Calendar event IDs from search_granola_events directly. (Note: Granola transcript reads are the most likely source of budget overruns — cap at 3 transcripts.)

  • 1d. Google Doc dashboard (if configured + OK): Read project dashboard tab for status, metrics, action items via mcp__google_workspace__get_doc_content.

  • 1e. Local files: Glob transcripts folder for new files since lookback. Read HANDOFF.md, TODO.md, PROJECT_STATE*.md if present (silent skip if not found).

  • 1f. Donor profile (if funder flag set): Read the relevant donor profile from your configured donor folder (e.g., ~/Documents/donors/[slug].md).

  • 1g. Agenda detection (runs on already-gathered data — no new tool calls): Scan retrieved WhatsApp messages and Gmail content for agenda signals:

  • Keywords: "agenda", "topics to cover", "topics for today", "order of business"
  • Structure signals: numbered or bulleted list of meeting topics
  • Look within 48h of meeting time and from meeting participants
  • If found: extract ordered list of agenda items. Note poster and timestamp. Store as detected_agenda: [items].
  • If not found: detected_agenda: null.

Light depth (8-15 tool calls, 15-30 sec): - Last Granola transcript only (if exists) - Gmail: 1 search (team roster emails, last 14 days), read top 5 messages - WhatsApp: messages in project group since last meeting - Skip Google Doc, local files, donor profile


Phase 2: Synthesize

2a. Build one-line source banner (degraded sources only): - If all OK: Sources: all healthy - If any degraded/unavailable: ⚠ Sources: WhatsApp DEGRADED (MCP filter issue, using SQLite) | Granola UNAVAILABLE (auth expired) - Suppress healthy sources from the banner.

2b. Cross-reference action items: If HANDOFF.md, weekly review, or previous Granola transcript found, extract action items. For each, search ALL gathered data for evidence of completion: - DONE: Explicitly marked complete in 2+ independent sources - LIKELY DONE: Appears resolved in 1 source, no contradicting evidence - NO EVIDENCE: Appears open in any source, or only in 1 source as open - CONTESTED: Different sources give conflicting status

Surface only NO EVIDENCE and CONTESTED items in the brief. Suppress DONE and LIKELY DONE.

2c. Organize and classify: - If detected_agenda is not null: Use BY AGENDA ITEM format. For each agenda item, find relevant gathered context and tag each claim as DECIDE/UPDATE/OPEN LOOP. Note: the agenda item itself has no confidence tag — the claims within it get confidence tags. Append "Items not on agenda" section for any DECIDE/OPEN LOOP items not covered by the agenda. - If detected_agenda is null: Use flat classification: - DECIDE: Unresolved thread with "what do you think?" / "can we proceed?" / "need your sign-off"; overdue action item assigned to you; conflicting proposals; budget/timeline questions without answers - UPDATE: Status reports, completed action items, FYI threads, meeting logistics, confirmations - OPEN LOOP: Items others owe you that are overdue or unacknowledged

2d. Per-attendee framing (Full only): For each attendee: compile "What I owe them" and "What they owe me" from cross-referenced action items.

2e. Identify preparation needs (Full only): From gathered data: documents to share, slides to review, draft decisions you need to have ready.

2f. Claim confidence tags (Full depth only): For each item in DECIDE and OPEN LOOPS, assess and tag: - VERIFIED: Corroborated by 2+ independent channels (independent = different communication channels, e.g., email + WhatsApp; NOT two retrieval paths for the same event, e.g., Granola + WhatsApp transcript of the same meeting). - SINGLE-SOURCE [channel, date]: Explicitly stated in exactly one channel. - INFERRED: Derived from absence, pattern, or combination — not explicitly stated in any source.

Rules: - DECIDE items tagged INFERRED only → suppress the item. Write "No decisions identified" if DECIDE section would otherwise be empty. - OPEN LOOP items tagged INFERRED → include but mark visibly. - Cross-check any claim about "[you] said/did X" against your sent emails before including it. If not found in sent email, downgrade to SINGLE-SOURCE or INFERRED. - Temporal attribution: when attributing a claim, use the timestamp of the original event described, not the message timestamp. If a message references a past decision without a dated original event, tag INFERRED with note: "date of original event unknown."


Phase 3: Present Briefing

Full depth: Full terminal template with all sections (DECISIONS REQUIRED, OPEN LOOPS, PER ATTENDEE, PREPARE, KEY UPDATES, SUGGESTED FOCUS, KNOWN BLIND SPOTS).

Light depth: 3-4 bullet format (last met, I owe them, they owe me, open question).

After display (Full only): - If ≥1 DECIDE item or ≥1 "I owe them" item: Send to attendees? (Send / Edit / Save / Skip) - Otherwise: Save locally? (Save / Skip) — don't offer email for meetings with no actionable content.

If nosend argument: skip email prompt entirely. If email argument: always prompt for email regardless of content.


Phase 4: Distribute (if email requested)

4a. Draft email: The email is a document delivery to attendees, not a summary for yourself.

KEEP (include in full, reframe as noted): - DECISIONS / BY AGENDA ITEM content → reframe as "Topics to cover" or by agenda item - KEY UPDATES → keep as "Recent updates" - SUGGESTED FOCUS → keep as "Suggested focus" - Any materials circulated since last meeting → list under "Materials shared" - Do NOT include INFERRED items in the email even if tagged — external recipients should only see VERIFIED or SINGLE-SOURCE claims.

REMOVE (internal to you only): - Source banner (⚠ Sources: ...) - OPEN LOOPS section (items others owe you — not the attendees' problem) - PREPARE section - PER ATTENDEE framing - KNOWN BLIND SPOTS - Any claim tagged INFERRED

Length: The email should contain all the same substantive content as the terminal brief minus the removed sections. Do not compress or summarize — present the full content. A 40-line terminal brief produces a ~25-line email, not a 10-line email.

Use a direct, professional tone. Avoid AI-sounding language ("delve," "leverage," "multifaceted," etc.). Recipients come from the calendar invite attendee list.

4b. Show draft + recipients for confirmation:

Text Only
────────────────────
PRE-MEETING EMAIL
────────────────────

To: [attendee names + emails from calendar invite]
Subject: Ahead of [meeting title] — [date]

[draft]

→ Send / Edit / Skip

4c. Send:

Option A: Gmail MCP draft (simple) Create a draft via mcp__google_workspace__draft_gmail_message. Report: "Draft created in Gmail — open Gmail to review and send."

Option B: Custom send script (advanced) If you have a send-email script at ~/.claude-assistant/scripts/send-email.py:

Bash
cat > /tmp/pre-meeting-body.txt << 'BODY'
[email body]
BODY

python3 ~/.claude-assistant/scripts/send-email.py \
  --to "recipient1@email.com" --to "recipient2@email.com" \
  --subject "Ahead of [meeting title] — [date]" \
  --body-file /tmp/pre-meeting-body.txt

rm -f /tmp/pre-meeting-body.txt

4d. Fallback: If send fails, fall back to mcp__google_workspace__draft_gmail_message. Note "Saved as Gmail draft."

4e. Save locally (if requested or save argument): Write to [transcripts_folder]/pre-meeting-brief-YYYY-MM-DD.md.


Phase 5: Log Performance

Bash
echo "$(date +%Y-%m-%d),pre-meeting-brief,TOOL_CALLS,NOTES" >> ~/.claude-assistant/logs/skill-performance.csv

Replace TOOL_CALLS with the exact count (each MCP call, Read, Write, Glob, Grep, Bash = 1). Replace NOTES with a summary (e.g., team-sync-full-3-decide).

After logging: "Run /post-meeting after this call to capture outcomes."

Customization Points

  • Transcript source: Swap Granola for Otter, Fireflies, or any tool that exports .txt transcripts to your transcripts folder.
  • WhatsApp DB path: The default ~/whatsapp-mcp-ts/data/whatsapp.db reflects one MCP install. Update the SQLite paths if your bridge stores data elsewhere.
  • Donor profile folder: The funder-flag lookup expects a [slug].md file per funder in a configured folder (e.g., ~/Documents/donors/). Skip the funder branch entirely if you don't track funders this way.
  • Email sending: Default is Gmail draft (Option A). Replace with a send script (Option B) if you have one configured.
  • Routing config: ~/.claude-assistant/config/granola-routing.json is optional — used only to suggest known project directories when the skill is run outside one.

Error Handling

Condition Action
No .claude/CLAUDE.md Stop. List known project directories from routing config (if present).
No calendar match Ask for meeting details manually
WhatsApp DEGRADED (MCP filter) Switch to SQLite direct; note in banner
WhatsApp UNAVAILABLE (DB locked) Skip; expand Gmail lookback to 2x; note in banner
Gmail auth error Retry once; if fails, note in banner
Gmail rate limit (429) Backoff and retry; if fails, note in banner
Granola empty (DEGRADED) Check for local transcripts; note in banner (normal for new projects or upcoming meetings without prior project history)
Granola 401 / auth expired Surface: re-auth Granola per your provider's instructions; skip Granola
Granola 429 (rate limit) Backoff and skip if persistent
Google Doc HOLLOW Note in banner: "Google Doc nearly empty — may not be up to date"; use what content exists
Google Doc 403 Note "Doc permissions may have changed" in banner; skip
Google Doc timeout Note "Doc temporarily unavailable" in banner; skip
Local files not found Silent skip (normal for some projects)
<2 sources functional Show raw data dump, not structured brief
All sources UNAVAILABLE Stop — don't produce a brief from zero data
Send fails Fall back to Gmail draft

Performance Budget

Scenario Tool Calls Time
Light 8-15 15-30 sec
Full target 50 1-3 min
Full + email target 55 2-4 min

Advisory threshold: 80 tool calls. Per-source caps bound tool call distribution: Gmail 8, Granola 5, WhatsApp 5, Doc 2, Local 3, plus Phase 0 overhead (~8) = ~31 calls minimum floor. Target 50 total.

Examples

Text Only
/pre-meeting-brief                    # Next meeting matching project context
/pre-meeting-brief Carnegie           # Meeting with "Carnegie" in title
/pre-meeting-brief light              # Force light depth
/pre-meeting-brief full email         # Force full + prompt email
/pre-meeting-brief nosend             # Show brief only, no email
/pre-meeting-brief since:2026-03-01   # Override lookback start