/find-call — Call Investigation Skill
You are helping the user pull deep, cited context from their past calls. Many calendars — especially those using a notes/transcription bot (e.g. notes.bot, Sembly, Fireflies, Otter) — have an automatically-appended "Meeting Resources" block in the calendar event description. The canonical block has up to 4 named links: Transcription (sometimes with sub-links like This Call and Project Calls), Meeting Notes, Video, Parent Folder.
The investigation flow is read-only — it never modifies Calendar, Drive, or transcripts. The only state this skill ever writes is its own plugin-local preferences file ~/.claude/find-call/config.json, and only through the --config wizard. Accuracy and source attribution dominate speed.
Any person, project, or meeting names anywhere in this document are illustrative placeholders. Real ones resolve against the user's
~/.claude/shared/identity.jsonteammate roster — this skill ships with no real names baked in.
Invocation modes (parse $ARGUMENTS first)
| Flag | Mode | Where |
|---|---|---|
| (none) / seed text | Investigate a past call | Workflow below (Steps 1–8) |
--status |
Read-only health check — providers + preferences + identity inheritance | ## Mode: --status |
--config |
Interactive source-provider wizard (writes config.json) | ## Mode: --config |
Precedence on conflict: --status > --config > default. Positional text with no flag is the call query.
Operating principles
- Calendar is the index. Every call you care about has a calendar event on the user's calendar. Start there.
- Meeting Notes give structured signal cheap. They're typically rotating Markdown/Doc files. Sections usually include
Topic:,Date:,Short Summary,Key Discussion Points,Action Points(per attendee),Meeting Resources. For action-item / decision questions, notes alone usually answer. - Transcripts add tone, verbatim, hesitation, push-back. Read them via parallel sonnet sub-agents only when the query is interpretive or notes are too thin.
- Transcripts are a parallel augment (when available). Not every call is transcript-indexed (1-on-1s often miss). If a notetaker provider is available (Sembly today, via
mcp__sembly-ai__*), query it IN PARALLEL with the calendar query; if none is connected — or the user pinnedtranscripts: off— skip it silently and rely on Meeting Notes. Seereferences/sources.md. - Cite everything. Every claim in your output anchors to a Doc URL, a transcript line, or a Sembly meeting ID. No floating "based on the meeting" claims.
Step 0 — Load identity (every invocation, before searching)
Read ~/.claude/shared/identity.json — the same file /clickup and /g-event write. This skill is a read-only consumer; it never writes this file. Extract:
user.name— used in sub-agent prompts and output ("the user's commitments"). Substitute it wherever this doc says {user.name}.user.email— the organizer; auto-excluded from attendee lists; its domain is the implicit "internal" domain.teammates[](first_name,latin_alias,full_name,email,active) — the attendee-name resolver input. When the user names someone by first name, match against these.trusted_domains[]— internal-org domains; used only to label attendees as internal vs external in output. Has no security gate here (read-only skill).
Optional, soft dependency on /g-event: if ~/.claude/g-event/config.json exists — or, for users who have not yet migrated from the old plugin name, the legacy ~/.claude/gevent/config.json (read the new path first; fall back to the legacy one only when the new path is absent) — read defaults.calendar (use it as the calendar ID instead of primary) and always_include[] (the notes_bot entry's email tells you which bot appends the Meeting Resources block). If neither file is present, default the calendar to primary and treat the notes bot as unknown — neither is required.
Resolve source providers (preference + fallback)
Read ~/.claude/find-call/config.json if it exists, section sources. This is the only file the skill ever writes, and only via the --config wizard — the investigation flow only reads it. It sets, per source, the user's preferred provider order — NOT a hard restriction. No file (or no sources block) → every source is auto. Full model + schema + examples: references/sources.md. Summary:
sources.calendar—auto(default) |cli|mcp.auto= default order (prefernpx @googleworkspace/cli, then a connected Google Calendar MCP).cli= try CLI first, then fall back to MCP.mcp= try MCP first, then fall back to CLI. All values fall back; the difference is only which is tried first.sources.docs—auto(default) |cli|mcp|off. Same preference+fallback as calendar;off= never fetch Meeting Notes (summarize from calendar + transcripts only).sources.transcripts—auto(default) |off|["sembly", …].auto= use every connected notetaker MCP (Sembly today; future ones picked up automatically).off= notes-only, never read a transcript even on interpretive queries. A list sets a preference order — try those first, then fall back to any other connected notetaker.
Guiding principle — get the data. The config is a preference, never a wall. Try the preferred provider first; if it's unavailable, unauthenticated, errors, or returns nothing, fall through to the next available provider for that source. Only report a source as unavailable when every provider for it failed. Detect provider availability from the session tool list — don't assume. The two genuine hard limits: off disables a source on purpose, and WebFetch is NEVER a provider for Google URLs (it can't supply the auth they need).
If ~/.claude/shared/identity.json is missing or has no user: degrade gracefully. Calendar search still works against primary; attendee-name resolution falls back to matching the literal name against event attendee displayName/email. Surface a one-line banner once:
ℹ No ~/.claude/shared/identity.json found — teammate-name resolution is limited. Run `/clickup:onboard identity` or `/g-event:onboard identity` once to set up your profile + roster (shared across all three plugins).
Do not HALT — this skill is useful even with zero config.
Tooling rules (HARD CONSTRAINTS)
Which tool you use for each source is resolved from sources.* (Step 0) as a preference order — try the preferred provider first, fall back to the next available one until the data is retrieved.
- Google Calendar:
- CLI path:
npx @googleworkspace/cli calendar events list --params '<JSON>'. - MCP path: a connected Google Calendar MCP's list-events tool (
mcp__*Google_Calendar*__list_events). - Order by
sources.calendar(auto/cli→ CLI first;mcp→ MCP first). If the first choice is unavailable/unauthenticated/errors, fall back to the other. Only fail if both fail — then tell the user how to enable one. - NEVER
WebFetchfor any Google URL under any provider (Google URLs need auth WebFetch can't supply).
- CLI path:
- Google Doc / Drive text (
sources.docs, ordered the same way; falls back between paths):- CLI path:
npx @googleworkspace/cli drive files export --params '{"fileId":"<id>","mimeType":"text/plain"}' --output ./.tmp/find-call/<id>.txt. The--outputpath MUST be inside the current working directory —/tmp/...is rejected by the CLI. ThenReadthe local file. - MCP path: a connected Drive MCP read tool.
off: skip notes entirely (the one value that does NOT fall back — it's a deliberate disable).
- CLI path:
- Transcripts (
sources.transcripts): try the preferred notetaker(s) first, then fall back to any other connected one. Sembly —mcp__sembly-ai__list_meetingsfor index,mcp__sembly-ai__get_meetingfor content. Read-only methods only. If no notetaker is connected (ortranscripts: off), skip transcripts and say so. - WebFetch: ONLY for non-Google URLs and only as last resort.
- Sub-agents:
subagent_type: general-purpose, model: sonnet. Never opus for transcript reading; never haiku. - NEVER: modify any calendar event, doc, drive folder, or transcript/meeting. NEVER write time logs.
Workflow
Step 1 — Parse query, build search predicate
Extract from the user's message:
- Topic keywords: content nouns/phrases ("retention", "onboarding flow", "the API migration").
- Person references: first names → match against teammate
first_name/latin_alias/full_namefrom identity.json, then against attendeedisplayNameandemailusername. - Time anchor: "yesterday", "last week", "Tuesday", "April 28". If absent, default range = past 7 days.
- Intent verbs: "summarize", "recap", "what did I commit", "what did X say", "tone", "react", "push back", "verbatim". Interpretive verbs flag transcript-depth need (Step 4).
Step 2 — Parallel search: Calendar + transcripts
Run BOTH in parallel via parallel tool calls in the same message, each through its resolved provider (Step 0). Skip the transcript query if transcripts: off or no notetaker MCP is connected.
Calendar (provider cli/auto shown; under mcp, call the Calendar MCP's list-events with the same range):
npx @googleworkspace/cli calendar events list --params '{
"calendarId":"<defaults.calendar or primary>",
"timeMin":"<range_start_minus_1day>T00:00:00Z",
"timeMax":"<range_end_plus_1day>T23:59:59Z",
"singleEvents":true,
"maxResults":50,
"orderBy":"startTime"
}'
Pad ±1 day for local-time/UTC boundary. Filter out eventType in ('workingLocation','focusTime','outOfOffice').
Transcripts (e.g. Sembly, when connected and not off):
mcp__sembly-ai__list_meetings(start_at=range_start, finish_at=range_end)
Step 3 — Score & rank candidates
For each event, compute a relevance score:
score = (3 if exact phrase substring in title else 0)
+ (2 per attendee-name match if user named someone)
+ (1 per significant keyword match in title or description preamble)
+ (recency: this-week=2, this-month=1, older=0)
- (1 if the user's responseStatus == "declined")
# TUNE-ME— these constants are v1 starting points; revisit after a few weeks of real use.
Step 4 — Disambiguation (tiered)
Decision tree:
- 0 matches → expand range to 30 days, retry. If still 0, ask "Searched 30 days, no match for ''. Want me to widen to 90 days or check all-time?"
- 1 match → proceed silently to Step 5.
- 2-3 matches AND
top_score / second_score > 1.5AND top is within last 7 days → silent auto-pick top, but show inline "Picked '' (also considered: , ); say 'no, ' to switch." - 2-3 matches with close scores →
AskUserQuestionwith dated options. - ≥4 matches →
AskUserQuestionwith options grouped by week, plus an "All (parallel investigation)" option capped at 10 events. If N>10, ask user to narrow first.
AskUserQuestion format:
question: "Found <N> matches for '<query>' — which did you mean?"
options:
- "<Apr 28> — <Title> (<duration>)"
- "<Apr 25> — <Title> (<duration>)"
- "All <N> (parallel)"
- Other (user types narrower phrase)
Step 5 — Per-match data fetch (parallel)
For each selected event:
Parse the description HTML. Use this regex set to extract the Meeting Resources links:
- Doc IDs:
https://docs\.google\.com/document/d/([A-Za-z0-9_-]+)— capture all and label by adjacent link text (This Call,Project Calls,Open). - Drive file:
https://drive\.google\.com/file/d/([A-Za-z0-9_-]+) - Drive folder:
https://drive\.google\.com/drive/folders/([A-Za-z0-9_-]+) - Strip query strings (
?usp=drivesdk,?tab=t.0) before passing to the CLI.
- Doc IDs:
If Meeting Resources block present:
- Always pull Meeting Notes (
drive files export→./.tmp/find-call/<docId>.txt). - Skip Video unless the user explicitly asks ("did anyone show their screen"). The video is binary; this skill doesn't transcribe video.
- Always pull Meeting Notes (
If Meeting Resources block ABSENT (common for 1-on-1s and bot-less calls):
- Try Sembly (if connected): search
list_meetingsresults for date+title fuzzy match →get_meetingif hit. - If both miss → return: "Found '' on but no Meeting Notes / Sembly transcript exists. Calendar description: ''. Want me to look at adjacent events?"
- Try Sembly (if connected): search
Sembly augment (when available):
- For each matched event, also pull
get_meetingif its date+title fuzzy-matches any item in the parallellist_meetingsresult. - Sembly's structured fields (decisions, tasks, risks, requirements, issues) are higher-signal than raw transcript and are independent of the notes-bot output.
- For each matched event, also pull
Step 6 — Depth decision (notes-only vs transcript-subagent)
For each matched event, decide whether to spawn a transcript sub-agent:
Spawn transcript sub-agent when ANY of:
- Number of matched events ≤ 5 (uniform-parallel default).
- Query contains an interpretive trigger word:
tone, react, push back, hesitate, lash, rant, verbatim, exact words, mood, defensive, confident, convince, justify, explain, defend, body language(liberal — when in doubt, include). - The Meeting Notes section for this event is < 2K tokens (notes likely truncated/missing).
- The user's message contains "deep dive", "transcript", "full context", "everything they said".
Cap: maximum 5 transcript sub-agents per query. If matches > 5 and the cap is hit, deep-read the top-5-ranked and notes-only the rest. Tell the user explicitly which is which.
Sub-agent prompt template (sonnet, general-purpose):
You are a transcript reader for one call. Read this transcript and answer:
1. What did {user.name} personally say or commit to? (quote verbatim with line refs if available)
2. What decisions were made and by whom? (cite the speaker)
3. What action items exist for {user.name}? (cite the moment)
4. <the user's specific question if any>
Source files (read these and ONLY these):
- Notes: <local path>
- Transcript: <local path or Sembly meeting id>
Sembly meeting id (optional): <id>
RULES:
- Cite every claim with a quote or line reference. Never paraphrase a commitment without a quote.
- If the transcript doesn't contain an answer, say "transcript does not contain this" — do not invent.
- Output ≤ 1500 tokens.
Step 7 — Compose output
Answer the user's actual question first, directly. The output exists to answer what was asked — not to fill a template. Lead with the answer; cite it; stop.
- Targeted question ("what did I commit to?", "did the client push back?", "what was decided?") → answer that, with citations, and omit every section that doesn't bear on it. Don't append a "Key decisions" block to a commitment question, or a generic "Summary" nobody asked for. One matched call + a pointed question can be two sentences.
- Open-ended question ("summarize the X call", "recap my week with Dana") → use the fuller structure below as a starting shape, dropping any section with no grounded content.
Hard rules regardless of shape: every claim carries a citation (Doc URL / transcript line / meeting ID); state your sources read; if a transcript was NOT read for a call, say so explicitly; never pad with boilerplate, hedging, or generic framing.
Fallback structure for open-ended summaries (drop empty sections — do not emit a header with "none"):
## <Date> — <Event Title> (<duration>)
Sources read: [Meeting Notes](<doc url>) [• [Transcript](<doc url>)] [• <provider>: <meeting_id>]
**Summary:** <2-3 sentences — only what's grounded in sources>
**Key decisions:** <cite each> · **{user.name}'s commitments:** <cite verbatim where possible> · **Open questions / blockers:** <only if present>
<If transcript was NOT read for this match:>
> _Notes-only for this call — say "deep dive on this one" if you want me to read the full transcript._
For multiple matched calls, add a 1-paragraph cross-cutting synthesis at the end ONLY if it surfaces a real pattern supported by ≥2 sources — otherwise skip it:
**Across these <N> calls:** <synthesis — only patterns supported by ≥2 sources>
Step 8 — Optional: offer to remember the alias (v1: disabled)
v1 SHIPS STATELESS. Skip this step. The infrastructure below is documented for v1.1.
After successful disambiguation, IF v1.1 has memory enabled, propose:
AskUserQuestion: "Want me to remember '<phrase>' = <project>/<event-pattern> for next time?"
options: ["Yes, remember", "No, ask me each time"]
On Yes → append to references/aliases.md (plugin-local) with learned: <ISO date> stamp.
Anti-slop rules (HARD)
- Answer the question asked, then stop. No generic framing, no unrequested sections, no hedging filler. A pointed question gets a pointed, cited answer — not a full templated report.
- Cite every claim. Every decision, commitment, quote, or fact in output must link to a Doc URL or transcript/meeting ID. If you can't cite it, don't say it.
- Don't summarize what you didn't read. If a transcript subagent was skipped for an event, output explicitly says "Notes-only for this call". Never claim "the team also discussed X" without a source — that's invention.
- Don't infer attendee positions. "Dana probably agreed" is forbidden. "Dana said 'I'm fine with that' [transcript line 142]" is fine.
- No emoji confidence indicators. This skill returns hard sourced claims; emoji confidence (🟢/🟡/🔴) belongs to other workflows, not this one.
- Never invent action items. Only quote action items that appear in the Notes'
Action Pointssection or are spoken verbatim in the transcript. - Never modify any asset. This skill makes zero write calls to Calendar / Drive / Sembly / Slack / Gmail. If a follow-up action is needed, tell the user what to do, don't do it for them.
Failure modes (handlers)
| Code | Symptom | Handler |
|---|---|---|
| F1 | No Meeting Resources block in event description | Try Sembly (if connected) → if miss, return calendar description + offer adjacent-event search |
| F2 | Doc rotated; current Open link is the new active file, doesn't contain this meeting |
Search exported text for Topic: <title> or event date — if not found, fall back to Parent Folder, search archived files by name |
| F3 | Sembly has it, Calendar doesn't | Sembly query runs in parallel; merge by datetime+title fuzzy |
| F4 | Phrase matches >1 events | Tiered disambiguation (Step 4) |
| F5 | Description is HTML, not plain | Use the regex set in Step 5; do not parse as HTML |
| F6 | Timezone boundary misses a borderline event | Pad timeMin/timeMax by ±1 day, filter post-hoc |
| F7 | Title [PFX] Roadmap Review (External) doesn't match "Roadmap Review call" |
Match against title substring AND attendee names AND description content |
| F8 | Drive export 403 | Catch error, fall back to Sembly for that meeting, surface "permission needed" warning |
| F9 | Notes section <500 tokens | Auto-escalate to transcript sub-agent for that single event |
Things to never do
- Never
WebFetcha Google URL under any provider — Google URLs need auth WebFetch can't supply. This is the ONLY hard limit on Google access: a Calendar/Drive MCP is fully in play per the resolved order — tried first whensources.*ismcp, or as the fallback whencli/autoprefers the CLI but the CLI can't deliver. Never refuse a working MCP fallback. - Never write to Calendar/Drive/Sembly/Slack/Gmail. Read-only.
- Never write time logs — that's a different tool's job. If the query is about logging hours, say so and hand off.
- Never spawn an opus sub-agent for transcript reading. Sonnet only.
- Never read full Drive Doc JSON when the goal is text —
drive files export mimeType=text/plainis the path. - Never load a transcript directly into main context. Always go through a sub-agent that returns ≤1500 tokens of cited summary.
- Never invent action items, decisions, or quotes. If the source doesn't say it, you don't say it.
- Never auto-pick a match when scores are close (top/second ratio ≤ 1.5×) — disambiguate via AskUserQuestion.
- Never write to
~/.claude/shared/identity.json(org-event/config.json— including the legacygevent/config.json— orclickup/config.json). This skill is a read-only consumer of shared/other-plugin state;/clickupand/g-eventown those writes. The ONLY file this skill ever writes is its own~/.claude/find-call/config.json, and ONLY via the--configwizard throughscripts/config_io.py.
Examples
Names below are illustrative placeholders. In practice the person references resolve against the user's
~/.claude/shared/identity.jsonteammate roster.
Example 1 — single match, action-item query (notes sufficient):
User: "what did I commit to in the AI Assessment Bot meeting yesterday?"
Skill:
- Calendar list yesterday±1 → matches
AI Assessment Bot(yesterday). - Single match, has Meeting Resources block → pull Meeting Notes only; per Step 6 default for ≤5 matches, also spawn a transcript sub-agent.
- Output cites
Action Points → {user.name}section verbatim, with the Doc URL.
Example 2 — multi-match, "all of them":
User: "summarize my calls with Dana this week"
Skill:
- Calendar list this-week with attendee match
dana.*(resolved via identity.json) → 3 matches. - AskUserQuestion: "Found 3 — which?" with "All 3 (parallel)" option.
- User picks "All 3" → spawn 3 parallel sonnet transcript sub-agents (under cap of 5).
- Output: 3 sections + 1 cross-cutting synthesis paragraph.
Example 3 — no Meeting Resources block:
User: "what did Sam and I talk about in our 1-on-1 last week?"
Skill:
- Match
1on1 - {user.name} / Sam(last week) — no Meeting Resources block (1-on-1, no notes bot). - Sembly parallel search (if connected): hit on the same date+title.
- Use Sembly's structured
summary,decisions,tasksfields. - Output cites
Sembly meeting <id>. If Sembly is not connected, return the calendar description and offer adjacent-event search.
Example 4 — interpretive query forces transcript:
User: "did the client push back when I proposed the limited rollout?"
Skill:
- Match the relevant client call.
- "push back" is an interpretive trigger → spawn transcript sub-agent regardless.
- Sub-agent searches transcript for the client's responses around the rollout-proposal timestamps.
- Output: verbatim quote with line ref OR explicit "transcript shows the client said '' immediately after — not pushback per se."
Sub-skill boundaries
/g-eventowns event creation/modification. If the query is "schedule" / "move" / "cancel", hand off./clickupowns task creation. If the query asks to create a follow-up ticket from a call, return the call context but DO NOT call/clickupdirectly — let the user initiate.- Time-logging is a separate concern. If the query is "what did I do" with hour-counting intent, this skill is the wrong tool — say so.
Mode: --status
Read-only. Writes nothing. Shows which provider each source will resolve to, what's connected, and where identity/calendar defaults come from.
- Run
python <plugin-root>/scripts/config_io.py --show(e.g.python plugins/find-call/scripts/config_io.py --show). It returns JSON: currentsourcespreferences, whether the config file exists, any corruption error, and whether thenpxCLI binary is present (cli_binary_available). Auth is NOT probed here — note that the CLI may be present but unauthenticated. - Detect connected providers from THIS session's tool list: a Google Calendar MCP (
mcp__*Google_Calendar*), a Drive MCP, and notetakers (mcp__sembly-ai__*). The script can't see session MCPs — this step is yours. - Check
~/.claude/shared/identity.jsonexists (identity inherited from/clickupor/g-event) and~/.claude/g-event/config.json— falling back to the legacy~/.claude/gevent/config.jsonwhen the new path is absent — (calendar default). - Print a resolution table — for each source: preference → which provider it resolves to first + what's available as fallback (✓/✗). If the config is unset, show
autoand say "not set — all auto".
/find-call:status
calendar : auto → cli (googleworkspace CLI present ✓; auth not probed)
docs : auto → cli ✓
transcripts: auto → sembly connected ✓
identity : inherited from g-event ✓ (~/.claude/shared/identity.json)
calendar id: primary (no g-event defaults.calendar)
config : ~/.claude/find-call/config.json — not set (all auto)
Mode: --config
Interactive wizard. This is the ONLY write this skill performs — and it writes ONLY ~/.claude/find-call/config.json, via the guarded helper. Never touches identity.json, g-event/config.json (or the legacy gevent/config.json), or clickup/config.json.
- Read current preferences:
python <plugin-root>/scripts/config_io.py --show. Pre-select the current value for each source. - Ask via
AskUserQuestion— one question per source, marking the current value. Make clear these are preferences, not restrictions — the skill always falls back to whatever works (exceptoff):- Calendar source →
auto(default order) /cli(prefer the googleworkspace CLI, fall back to MCP) /mcp(prefer a Calendar MCP, fall back to CLI). - Docs source →
auto/cli(prefer CLI) /mcp(prefer MCP) /off(never fetch Meeting Notes — the only non-fallback value). - Transcripts →
auto(every connected notetaker) /off(notes-only, never read a transcript) /sembly(prefer Sembly, fall back to other connected notetakers).
- Calendar source →
- Persist via the helper (it validates values and writes atomically —
flock+ tmp +os.replace):
Only pass the keys the user changed. The script rejects invalid values rather than writing them.python <plugin-root>/scripts/config_io.py --set calendar=<v> docs=<v> transcripts=<v> - Confirm: print the resulting JSON and "run
/find-call:statusto verify resolution."
Never write any file other than config.json. If the script exits non-zero, surface its stderr and do not retry blindly.
v1 known TUNE-ME items
- Disambiguation scoring constants (Step 3) — invented; revisit after a few weeks of usage.
- Transcript sub-agent cap (5) — revisit if the user routinely picks "All N" with N>5.
- Interpretive-verb list (Step 6) — extend as missed cases surface.
- Memory persistence (Step 8) — disabled in v1; flip in v1.1 if usage justifies it.