Setup
Locate this skill's directory (the folder containing this SKILL.md), then run the resolver script from there:
python <skill-dir>/scripts/skillctx-resolve.py resolve p-meeting-prep
The resolver outputs each binding as key: value (one per line). Substitute each {binding_key} placeholder below with the resolved value.
If any values are missing or the user requests changes, use:
python <skill-dir>/scripts/skillctx-resolve.py set p-meeting-prep <key> <value>
Generate per-meeting prep briefs for today's calendar (one markdown file per meeting + an index), writing to {output_dir}/YYYY-MM-DD/.
When to Use
- User says "meeting prep", "prep my meetings", "prep brief"
- User invokes
/p-meeting-prep - A scheduled cron / launchd job triggers
/p-meeting-prep(typically early morning)
Prerequisites
None are hard requirements except a reachable calendar. Phase 1 falls back rather than aborting, and records every substitution in 00-index.md.
| Want | Used for | If missing |
|---|---|---|
gws CLI (authenticated) |
Google Calendar | Google Calendar MCP |
gh CLI (gh auth status) |
attendee activity | GitHub sections render _(unavailable)_ |
| Slack MCP | DMs, mentions, topic search | Slack sections render _(unavailable)_ |
| Fireflies MCP | prior recordings | Fireflies sections render _(unavailable)_ |
Arguments
--date YYYY-MM-DD— Target date. Default: today.--lang ja|en— Output language. Default:{default_lang}.--dry-run— Print briefs to stdout instead of writing files.--no-open— Skip auto-opening the index after generation.--non-interactive— Never prompt. Fall back silently and render gaps as_(no relevant context found)_. Required for cron / launchd runs, which have no one to answer.
Workflow
Phase 1: Initialize
-
Parse arguments:
--date YYYY-MM-DD(default: today, local time)--lang ja|en(default:{default_lang})--dry-run(default: false)--no-open(default: false)--non-interactive(default: false; treat as true when the invocation came from a scheduler)
-
Compute:
TARGET_DATE: the date to process,YYYY-MM-DDWEEKDAY: short weekday name (e.g.Tue)OUTPUT_DAY_DIR:{output_dir}/{TARGET_DATE}(expand~and env vars)
-
Resolve prerequisites. Never abort and never ask — substitute, then report. Run these probes in one parallel batch.
Probe Missing / failing → fallback Record as command -v gwsGoogle Calendar MCP list_events(startTime/endTime/orderBy: startTime/timeZone).calendar: Google Calendar MCP (gws not installed)gh auth statusnone — GitHub sections render _(unavailable)_github: unavailableslack_read_user_profilenone — Slack sections render _(unavailable)_slack: unavailablefireflies_get_usernone — Fireflies sections render _(unavailable)_fireflies: unavailableAbort only when
gwsand the Calendar MCP both fail; there is then no calendar at all. -
Record
SOURCES_AVAILABLE = {calendar, github, slack, fireflies}for Phase 3, andFALLBACKS— the list of "Record as" strings that fired — for Phase 5.
Phase 2: Calendar fetch + classify
- Fetch the day's events.
→ See
references/agent-gather-calendar.md - Filter out junk:
- Declined events (the user's response is
declined). - All-day events.
- Events with only one attendee = the user (focus blocks, holds).
- Declined events (the user's response is
- Classify everything else in a single LLM call.
→ See
references/agent-classify-meetings.md - Cluster meetings by
topic_key. A cluster is any set of 2+ meetings sharing the sametopic_key. Singletons are clusters of one and need no cross-links. - Save the classified list as
MEETINGS = [{event, category, topic_key, cluster_id, …}]for Phase 3.
Phase 3: Per-meeting fetch + confirm (interactive)
Read the attendee cache once — ~/.cache/p-meeting-prep/attendees.json (see references/attendee-cache.md) — before the loop, and pass relevant entries into every fetch so cached attendees skip live resolution.
Then walk the meetings one at a time, in chronological order. Do not fetch the whole day up front: the user sees and confirms each meeting before you move to the next one. other-category meetings are skipped entirely — no fetch, no prompt.
For each meeting:
1. Fetch. Pick the source set from this table and fan out that meeting's calls in a single parallel tool-use batch.
| Category | Fireflies | Slack DMs/mentions | GitHub (attendees) | Slack topic search | Web (candidate) |
|---|---|---|---|---|---|
| 1:1 | yes | yes | yes | no | no |
| team-sync | yes | no | yes (self) | yes | no |
| external | yes | yes | no | yes | no |
| interview | yes | no | no | no | yes |
| other | no | no | no | no | no |
Per source, follow the matching reference:
- Fireflies →
references/agent-gather-fireflies-meeting-context.md - Slack →
references/agent-gather-slack-meeting-context.md - GitHub →
references/agent-gather-github-attendee-context.md - Web (candidate) →
references/agent-gather-web-candidate-context.md
Skip every call to a source whose SOURCES_AVAILABLE flag is false. Soft budget 15 s per source; on timeout treat it as unavailable for this meeting only, no retry. Reuse across a cluster: a Fireflies or Slack topic search keyed off topic_key runs once per cluster, not once per meeting.
2. Report. Print at most 5 lines — one per source — each either a one-line finding or the concrete reason it is empty. Name the gaps; do not let them slide silently into _(unavailable)_.
HH:MM <title> [category]
Fireflies <one-line finding, or why it is empty>
GitHub <one-line finding, or why it is empty>
Slack <one-line finding, or why it is empty>
<other> <one-line finding, or why it is empty>
Never put a real name, handle, email, or identifier into this skill file — the shape above is the contract, the values belong only in the generated brief.
3. Ask. One AskUserQuestion call (max 4 questions), covering only gaps the user can actually close. Skip this step when there are none — never ask a question whose answer changes nothing.
- Closable: things that live outside the wired sources — a link to the counterpart's public profile or résumé, notes from a meeting that was never recorded, a doc or ticket the fetch could not reach, the real subject when the calendar title is opaque, which of several same-name people the attendee is.
- Not closable: a source marked
UNAVAILABLEback in Phase 1. State it once and move on. - Every question offers both a paste path and a
なし・このまま進めるpath. Never block: "no answer" is a valid answer and means render the gap as_(no relevant context found)_.
4. Fold in whatever the user pasted. If they gave a URL, fetch it before drafting. Store it as user_supplied.
5. Draft the brief for this meeting using the Phase 4 rendering rules, and store it as BRIEFS[event_id].
Under --non-interactive, run steps 1, 2 and 5 only.
Save results as MEETING_CONTEXT[event_id] = {fireflies, slack_attendees, slack_topic, github, web_candidate, user_supplied}.
Phase 4: Rendering rules + cross-scan
Pass 1 runs inside the Phase 3 loop (step 5). These are the rules that step follows, for the one meeting being drafted:
-
Pick the template by
category:1:1→references/template-1on1.mdteam-sync→references/template-team-sync.mdexternal→references/template-external.mdinterview→references/template-interview.mdother→references/template-other.md
-
Render the template using
MEETING_CONTEXT[event_id], in language{lang}. -
If the meeting belongs to a cluster of size ≥ 2, add the
Related today:line referencing the sibling filenames computed in Phase 5. -
Empty data sections render as
_(no relevant context found)_rather than being omitted, so the user can tell the skill looked. -
Anything the user pasted in Phase 3 step 3 outranks a fetched source that contradicts it, and is cited as
(ユーザー提供)/(user-supplied)so the brief stays auditable.
Pass 2 — cross-scan (after the Phase 3 loop has drafted every brief; no user interaction):
Filenames are deterministic from Phase 2 data (chronological order + start time + title slug) — compute them before Pass 2 so both cluster links and overlap links can reference sibling files.
- Detect overlaps across the day's drafts:
- (a) shared non-self attendees appearing in 2+ meetings;
- (b) shared salient topics/entities — proper nouns, repo names, project names appearing in 2+ briefs' bodies (e.g.
changelog-management).
- For each overlap, inject into the header block of every affected brief:
Related today: [<sibling file>](<sibling file>) — <shared attendee or topic, one short clause>Multiple siblings → comma-separated. Merge with any existing cluster-basedRelated today:line rather than duplicating it. other-category meetings are excluded from the cross-scan (they have no body and no fetches).- Record the overlap list as
OVERLAPSfor the index (Phase 5).
Phase 5: Write files
- Compute
OUTPUT_DAY_DIR={output_dir}/<TARGET_DATE>(expand~). - If
OUTPUT_DAY_DIRalready exists:- Remove any existing
<OUTPUT_DAY_DIR>.bak/. mvthe currentOUTPUT_DAY_DIRto<OUTPUT_DAY_DIR>.bak/.
- Remove any existing
- Create
OUTPUT_DAY_DIR. - Compute filenames in chronological order:
NN= zero-padded sequence starting at 01 (the index uses00).HHMM= local start time, 24-hour, no colon.<slug>= ASCII-folded, lowercase, hyphenated title; collapse runs of non-[a-z0-9]to a single-; trim to 40 chars; strip leading/trailing-.- If ASCII-folding leaves fewer than 4 useful characters (typical for Japanese titles), romanize instead: kanji/kana → romaji words, katakana loanwords → their English source word. Example:
採用:グッズブレストMTG→saiyo-goods-brainstorm-mtg. Same 40-char cap and collision rule. - Final name:
NN-HHMM-<slug>.md.
- Write
00-index.mdfirst:
# Meeting prep — <TARGET_DATE> (<WEEKDAY>)
<N> meetings · <K> categories · <C> clusters
<`> ` + each FALLBACKS entry, one line — omit the block when FALLBACKS is empty>
## Today
- HH:MM **<title>** `[category]` → [NN-HHMM-slug.md](NN-HHMM-slug.md)<` · cluster: <topic_key>` if cluster size ≥ 2>
... (one bullet per meeting, chronological)
## Clusters
- `<topic_key>` — <count> meetings (HH:MM, HH:MM, ...)
... (only clusters with size ≥ 2)
## Overlaps
- <shared attendee or topic> — <count> meetings (HH:MM, HH:MM, ...)
... (from Pass 2 cross-scan; omit the section when `OVERLAPS` is empty)
## Sources
- Calendar: <✅ + how it was fetched | ❌>
- GitHub / Slack / Fireflies / Web: <✅ | ❌ reason>
- User-supplied: <what the user pasted in Phase 3, per meeting — omit the line when nothing was pasted>
If N == 0, the index body is No meetings today. and no per-meeting files are written.
- Write one file per meeting using
BRIEFS[event_id]. - Merge any live attendee resolutions from this run back into
~/.cache/p-meeting-prep/attendees.json(read-modify-write; seereferences/attendee-cache.md). - Print the absolute path to
00-index.mdand a one-line summary (N meetings · K categories · C clusters). - On macOS (
uname -s=Darwin), unless--no-openor--dry-run, runopen "<00-index.md>". - If
--dry-run, do NOT touch the filesystem at all. Instead, print to stdout: the index body, then each brief separated by---.
Error Handling
| Error | Action |
|---|---|
gws missing or unauthenticated |
Fall back to Google Calendar MCP; note it in FALLBACKS. Abort only if that also fails. |
gh not authenticated |
Mark GitHub UNAVAILABLE; continue. Add a warning to the top of 00-index.md. |
| Slack MCP unavailable | Mark Slack UNAVAILABLE; continue. Slack sections render as _(unavailable)_. |
| Fireflies MCP unavailable | Mark Fireflies UNAVAILABLE; continue. Fireflies sections render as _(unavailable)_. |
| Classification LLM call fails / bad JSON | Treat every meeting as other. Add a warning to the top of 00-index.md. |
| Per-meeting source fetch timeout (15s) | Section renders as _(unavailable)_. No retry within this run. |
OUTPUT_DAY_DIR exists from a prior run |
Rename to <dir>.bak/ (replacing any prior backup) before writing. |
| Attendee cache missing / corrupt JSON | Treat as empty cache; resolve live; rewrite the file at end of run. |
| Calendar empty for the target date | Write 00-index.md with body No meetings today. Exit 0. |
--dry-run set |
Print to stdout only. Do not touch the filesystem. Do not call open. |
| Filename slug collision (rare) | Append -2, -3, … to the slug until unique within the day's directory. |
| User declines to fill a gap (Phase 3) | Render that section as _(no relevant context found)_; move to the next meeting. |
| User pastes an unreachable URL | Say so in one line, re-ask once, then continue without it. |
--non-interactive set |
Never call AskUserQuestion. Every gap renders as _(no relevant context found)_. |
Scheduling
Scheduled runs must pass --non-interactive — nothing is there to answer Phase 3's questions, and an unanswered prompt stalls the whole run.
This skill is user-triggered. Two recipes for running it automatically each morning are documented in references/cron-recipes.md. Whichever you pick, ensure the scheduled environment inherits credentials for gh, gws, the Slack MCP server, and the Fireflies MCP server.