rowboat/PLAN.md
gagan 08a4077d43
Meeting prep: attendee notes + proactive 6h-ahead prep briefs (#636)
* feat(meetings): inline meeting prep under next meeting

Resolve a meeting's attendees against the knowledge base and render each
attendee's existing person.md inline beneath the next upcoming meeting in
the Meetings view. Deterministic (email-exact match, then unambiguous
name/alias), no LLM in the hot path.

- core: resolveMeetingPrep() + meeting-prep:resolve IPC
- renderer: notes-first rows with expandable person.md; unmatched attendees
  collapse into a 'no notes yet' row with a Create note action that hands
  off to the Copilot
- prep re-resolves when a People note changes

* feat(meetings): per-meeting prep toggle

Add a Prep toggle to every eligible meeting row, not just the next one.
The next meeting still auto-expands; other meetings resolve their attendee
notes lazily when their toggle is opened. All-day and solo events get no
toggle.

* feat(meetings): org matching, cached index, and field-bleed fix

#2 Matching: resolve the meeting's external companies from attendee email
domains (and matched people's Organization field), excluding the user's own
domain, and surface those org notes in a Companies section. Normalize display
names (strip '(via ...)' suffixes) before name/alias matching.

#3 Perf: meeting prep reads a cached knowledge index instead of rescanning the
whole knowledge dir on every resolve; invalidated whenever a file under
knowledge/ changes (wired to the workspace watcher).

Fix: extractField in the knowledge index consumed newlines after the label, so
an empty field (e.g. **Role:**) bled the next line's value. Restrict it to
spaces/tabs so empty fields resolve to undefined. Also strip the folder prefix
from link targets for display (Organizations/Rowboat Labs -> Rowboat Labs).

* feat(meetings): proactive prep notes generated 6h ahead

Generate a meeting prep note ~6h before each meeting and surface its brief
in the prep card.

- Generator (meeting_prep_brief.ts): assembles roster + 'last time' recap
  (extracts the prior instance's Action items) + agenda, ordered by meeting
  type (recurring -> recap first, one-off -> agenda first), then one model
  call for a 'what matters' brief. Writes knowledge/Meetings/prep/<slug>-<date>.md
  with eventId/recurringEventId frontmatter. Reuses the summarizeMeeting LLM
  path (configured model, useCase: meeting_prep).
- Scheduler (meeting_prep_scheduler.ts): calendar-aware tick (5m), generates
  prep within the 6h lead window, state file dedupes, self-heals on changes.
- Card: shows the brief (bulleted, compact) above a People list whose rows
  link out to each person's note instead of rendering it inline. Generated
  prep notes are kept out of the past-notes table.

* chore(meetings): poll prep scheduler every 15m instead of 5m
2026-07-02 00:35:35 +05:30

111 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Meeting Prep — "Next up" prep card
## Goal
When a meeting is imminent, surface who will be there by pulling up each
attendee's existing `person.md` from the knowledge base and showing it — no
click, no LLM in the hot path. An ambient card that's just *ready* when you
look.
This is a deliberately light, deterministic surface. It does **not** replace
the two existing meeting-prep paths:
- the chat skill (`assistant/skills/meeting-prep/`) — LLM brief on demand
- the pre-built automation (`pre_built/meeting-prep.md`) — LLM brief files
It complements them with an instant, no-LLM "who's in this meeting" view.
## Decisions (settled)
| Axis | Choice |
|------|--------|
| Surface | **Ambient card**, no notification. Pinned at top of Meetings view. |
| Activation | Next eligible meeting is **≤ 30 min away**. Outside that → no card. |
| Depth | **Just show `person.md`** — render the note, no LLM synthesis. |
| Who | **Everyone incl. teammates.** Skip self / all-day / declined / cancelled. |
| Sparse meetings | **Notes-first**: attendees with notes shown prominently; the rest collapse into "N others — no notes yet" → expand to create. |
| No-note action | **Offer to create** a note for that attendee. |
## Resolution rules
For each attendee (excluding `self`):
1. **Email exact match** (case-insensitive) against `KnowledgeIndex.people[].email` — reliable.
2. Else **name/alias match** on `displayName``people[].name` / `people[].aliases`,
only when it's a **single unambiguous hit**.
3. Matched → read `person.md`, return `{ name, role, org, path, markdown }`.
4. Unmatched → returned as a "no note" attendee (name + email).
Org notes (`Organizations/{Org}.md`) are **deferred**`person.md` already
carries Org/Role inline, enough for v1.
## Architecture
| Piece | Location | Notes |
|-------|----------|-------|
| Attendee → note resolver | **new IPC** `meeting-prep:resolve` in `apps/main/src/ipc.ts` | Input: attendee list (`{email?, displayName?, self?}[]`). Builds/uses `buildKnowledgeIndex()`, resolves per rules above, reads matched notes, returns structured payload. Deterministic + fast. |
| Shared types | `packages/shared/src/` | `MeetingPrepAttendee`, `MeetingPrepResult` request/response shapes + IPC channel typing. |
| "Next up" card UI | **new component** rendered above `UpcomingEvents` in `meetings-view.tsx` | Picks next event ≤30 min out, calls IPC, renders notes-first + collapsed others. Reuses existing markdown rendering. |
| Create-note action | reuse the note-creation agent (only LLM touch, user-initiated) | v1: open a prefilled Copilot chat to create the note (visible/steerable) rather than firing silently. |
No new background loop — the renderer already polls `calendar_sync/` every
minute, so the card stays current for free.
## Build slices
1. **Resolver IPC + shared types**`meeting-prep:resolve`, wired through
preload. Unit-testable in isolation. ← first slice
2. **"Next up" card UI** — component + 30-min gating + notes-first / collapsed
rendering, reading from the IPC.
3. **Create-note action** — prefilled Copilot chat for an unmatched attendee.
## Out of scope (fast-follow candidates)
- Notification variant (extend `notify_calendar_meetings.ts` tick).
- Org-note surfacing.
- Configurable lead time (hardcode 30 min for v1).
- LLM-synthesized brief button on the card.
## Phase 2 — Proactive, meeting-aware prep (6h ahead)
Shift from a static contact viewer to meeting-aware prep, generated ahead of time.
### Decisions
- **Trigger:** a calendar-driven tick (clone of `notify_calendar_meetings.ts`), NOT the
cron/bg-task engine — fire time is `start 6h`, which varies per event, so a
calendar scan + state file is the right fit (self-healing on reschedule/cancel).
- **Storage:** write a real note → `knowledge/Meetings/prep/<slug>-<date>.md` (openable/editable).
- **Notify:** none — silent background generation; appears in the Meetings view.
- **Adaptive by meeting type:**
- Recurring (`recurringEventId` + prior notes found) → lead with **"Last time" recap**
(prior instance's `## Action items` / summary), then roster.
- One-off / new → lead with **agenda** (event `description`), then roster + company.
- **Roster:** always list every attendee; ones with a note link to it, ones without
get **Create note**.
- **AI brief:** the only LLM step. Reuses the `summarizeMeeting` path exactly —
`generateText` against the user's configured model (`getMeetingNotesModel` /
`getDefaultModelAndProvider`), tagged `useCase: 'meeting_prep'`. Deterministic
parts (roster, recap, agenda) are assembled in code; the model only writes the
"what matters" summary.
### Build slices
- **A. Core generator** (`meeting_prep_brief.ts`): assemble context (roster + recap +
agenda, meeting-type aware) → `generatePrepBrief()` (one model call) → render +
`writePrepNote()` to `knowledge/Meetings/prep/`. Frontmatter stamps `eventId` /
`recurringEventId` / `meetingDate` / `generatedAt`.
- **B. Scheduler** (`meeting_prep_scheduler.ts`): tick (~few min), scan `calendar_sync/`,
for events starting within 6h, not ended, ≥1 non-self attendee, not already prepped
(state file keyed by eventId) → generate + write. `init()` wired in `main.ts` beside
`initCalendarNotifications()`.
- **C. Card integration:** the inline prep card prefers the generated prep note when it
exists (instant), else falls back to the live deterministic resolve.
### Open detail / enabler
- Notes don't store an event id today, so recap linking is a title/date heuristic.
Stamp `eventId`/`recurringEventId` into captured notes' frontmatter going forward so
future links are exact.
## Verification
- Resolver: feed known attendee emails → correct note match; unknown → no-note.
- Card: appears only inside the 30-min window; notes-first ordering; collapse
toggles; create action opens prefilled chat.
- `npm run deps && npm run lint` clean.