# CLAUDE.md — Minutes

> Your AI remembers every conversation you've had.

## Project Overview

**Minutes** — open-source, privacy-first conversation memory layer for AI assistants. Captures any audio (meetings, voice memos, brain dumps), transcribes locally with whisper.cpp or parakeet.cpp, diarizes speakers, and outputs searchable markdown with structured action items and decisions. Built with Rust + Tauri v2 + Node.js (MCP).

**Four input modes, one pipeline:**
- **Live recording**: `minutes record` / `minutes stop` — for meetings, calls, conversations
- **Live transcript**: `minutes live` / `minutes stop` — real-time transcription with delta reads for AI coaching mid-meeting
- **Notetaking**: `minutes note "important point"` — timestamped annotations during recording
- **Folder watcher**: `minutes watch` — auto-processes voice memos from iPhone/iCloud

## Quick Start

```bash
cd ~/Sites/minutes
cargo build                          # Build Rust workspace
cargo test -p minutes-core --no-default-features  # Fast tests (no whisper model)
cargo run --bin minutes -- setup --model tiny      # Download whisper model
cargo run --bin minutes -- setup --diarization     # Download speaker diarization models (~34MB)
cargo run --bin minutes -- record    # Start recording
cargo run --bin minutes -- stop      # Stop and process
```

## Full Build (CLI + Tauri App)

```bash
./scripts/build.sh                   # Builds everything and installs CLI
./scripts/build.sh --install         # Same + copies .app to /Applications
./scripts/install-dev-app.sh         # Canonical signed dev app install to ~/Applications/Minutes Dev.app
# Or manually:
export CXXFLAGS="-I$(xcrun --show-sdk-path)/usr/include/c++/v1"
cargo build --release -p minutes-cli           # CLI binary
cargo tauri build --bundles app                # Tauri .app bundle
cp target/release/minutes ~/.local/bin/minutes # Install CLI
open target/release/bundle/macos/Minutes.app   # Launch app
```

**Hard rule for macOS desktop packaging and dogfooding:**

- If the work touches TCC-sensitive features, do **not** keep replacing `/Applications/Minutes.app` with local rebuilds.
- Use `./scripts/install-dev-app.sh` and test `~/Applications/Minutes Dev.app`.
- If a stable local codesigning identity exists, export `MINUTES_DEV_SIGNING_IDENTITY` before running the script.
- On this machine, the preferred identity is:
  - `Developer ID Application: Mathieu Silverstein (63TMLKT8HN)`
- Example:

```bash
export MINUTES_DEV_SIGNING_IDENTITY="Developer ID Application: Mathieu Silverstein (63TMLKT8HN)"
./scripts/install-dev-app.sh
```

**IMPORTANT**: After any code change, you must rebuild ALL affected targets:
- CLI changes: `cargo build --release -p minutes-cli && cp target/release/minutes ~/.local/bin/minutes`
- Tauri changes: `cargo tauri build --bundles app` then relaunch the appropriate app bundle
- TCC-sensitive desktop work (hotkeys, Screen Recording, Input Monitoring, Accessibility): `./scripts/install-dev-app.sh`
- MCP server changes: `cd crates/mcp && npm run build` (compiles TS server + builds UI, then restart MCP client sessions)
- MCP App UI only: `cd crates/mcp && npm run build:ui` (rebuild just the dashboard HTML)
- All Rust + app: `./scripts/build.sh` (add `--install` to copy .app to /Applications)
- **Don't forget the MCP server** — it's TypeScript, not Rust. `./scripts/build.sh` does NOT rebuild it. Always run `cd crates/mcp && npm run build` after touching `crates/mcp/src/index.ts` or `crates/mcp/ui/`.

## Desktop Identity Rules

For macOS permission-sensitive development, there are now two distinct desktop app identities:

- Production app:
  - name: `Minutes.app`
  - bundle id: `com.useminutes.desktop`
  - canonical install path: `/Applications/Minutes.app`
- Development app:
  - name: `Minutes Dev.app`
  - bundle id: `com.useminutes.desktop.dev`
  - canonical install path: `~/Applications/Minutes Dev.app`

Use the dev app for any work involving:

- dictation hotkeys / Input Monitoring
- Screen Recording prompts
- AppleScript / Accessibility automation
- any repeated TCC permission prompt investigation

Do not trust results from:

- `./Minutes.app`
- raw `target/debug/minutes-app`
- raw `target/release/minutes-app`
- repo-local bundle outputs launched directly from `target/`

Those identities are not stable enough for TCC debugging.

Native hotkey sanity check:

```bash
./scripts/diagnose-desktop-hotkey.sh "$HOME/Applications/Minutes Dev.app"
```

See [docs/DESKTOP-DEVELOPMENT.md](/docs/DESKTOP-DEVELOPMENT.md) for the full workflow.

For dictation shortcut work:

- prioritize the `Standard shortcut (recommended)` path first
- treat the raw-key `Caps Lock` / `fn` path as advanced and permission-heavy
- do not call the raw-key path “done” just because the monitor is active; require visible feedback or logged event delivery

### Open-source contributor note

This repo is public, so local scripts must not assume the maintainer's Apple
certificate or local notarization credentials.

- `./scripts/install-dev-app.sh` works without Apple credentials by falling
  back to ad-hoc signing
- for more stable TCC-sensitive testing, contributors can export
  `MINUTES_DEV_SIGNING_IDENTITY` to any consistent local codesigning identity
- release signing / notarization is maintainer-only and should be configured
  explicitly via environment variables, not by hardcoded defaults in scripts

## Pre-Commit Checklist

**Full table lives in [docs/PRE-COMMIT.md](docs/PRE-COMMIT.md).** Read it before any commit that touches Rust, the MCP server, the frontend, or release surfaces — it covers manifest sync, MCPB bundle guards, fmt/clippy/test, Unix-only-API gating, feature-stub parity, site release constants, skill compiler outputs, the toolchain pin, and UI render verification.

Two traps that bite hardest (the rest are in the doc):
- **Toolchain pin.** `command -v cargo` must match `rustup which cargo`. If Homebrew rust shadows the rustup proxy, `rust-toolchain.toml` is silently ignored and your local clippy/rustfmt drift from CI's. Fix once: prepend rustup's bin dir to PATH or `brew uninstall rust`.
- **UI render verification.** Type checks and Rust unit tests don't catch UI render bugs. Any change to `tauri/src/index.html`, any new Tauri `cmd_*`, or any modal/overlay shift requires building the dev app and click-testing in `~/Applications/Minutes Dev.app`.

## Release Checklist

**Full procedure lives in [docs/RELEASE.md](docs/RELEASE.md).** Walk through every step in order when shipping. Highlights:
- All 6 version sources must match (`Cargo.toml`, `crates/cli/Cargo.toml`, `tauri/src-tauri/tauri.conf.json`, `crates/mcp/package.json`, `crates/sdk/package.json`, `manifest.json`) plus the version string in `crates/mcp/src/index.ts`.
- `crates/whisper-guard/` ships on its own cadence — bump + publish independently if it changed since last whisper-guard publish.
- Push to `main` and wait for CI green **before** tagging. Create the GitHub release as a `--draft`, wait for release workflows, then `--draft=false`. Never `git tag` locally.
- After tagging: upload `.mcpb`, publish npm packages (sdk first, then mcp — and `crates/mcp/package.json` must depend on the npm version of `minutes-sdk`, not `file:../sdk`), redeploy site via prebuilt Vercel flow from repo root, update Homebrew tap if CLI changed.

## Design System

Always read `DESIGN.md` before making any visual or UI decisions.
All font choices, colors, spacing, border radius, and aesthetic direction are defined there.
Do not deviate without explicit user approval.

Key rules:
- Landing site defaults to **light cream** (#F8F4ED). Dark mode is opt-in via system preference.
- Accent: **coral** (#C96B4E) in light, **green** (#30D158) in dark. Used sparingly — 5 specific spots only.
- Headings: **Instrument Serif**. Body: **Instrument Sans**. UI/labels/transcript: **Geist Mono**.
- No gradients, no decorative elements, no illustrations. Information density is the aesthetic.
- The transcript output card (diarized speaker labels + action items in Geist Mono) is the primary product demo.
- In QA or design review: flag any code that introduces fonts, colors, or radius values not defined in DESIGN.md.

## GitHub Discussions

Discussions are enabled at `silverstein/minutes` as the community Q&A surface. Issues are for bugs and feature requests; Discussions are for usage questions, setup help, and show-and-tell.

**When to check Discussions:**
- Before closing an issue that's really a question — convert it to a Discussion instead (`gh issue transfer` or manually)
- When a bug report smells like a usage question (wrong device, config confusion, platform quirk) — answer and suggest reposting as a Discussion
- After shipping a release — scan Q&A for questions the release may have answered, and reply with the fix/upgrade path

**When to point users to Discussions:**
- README and error messages that suggest "ask for help" should link to Discussions, not Issues
- Issue templates should nudge Q&A to Discussions

**Quick commands:**
```bash
gh api repos/silverstein/minutes/discussions --jq '.[].title'   # List recent
gh issue list --label question                                    # Find issues that should be discussions
```

## Contributor PRs

**Always merge contributor PRs through GitHub's merge flow.** Use `gh pr merge <number>` or the GitHub merge button. Never cherry-pick/rebase to main manually and then close the PR via API. That makes GitHub display the PR as "Closed" (red) instead of "Merged" (purple), which looks like a rejection to the contributor.

If there are merge conflicts, resolve them on the PR branch and merge through GitHub. If you need to rebase, push to the PR branch first, then merge.

**Never rewrite a contributor's PR without communicating first.** If their approach needs changes, iterate via review comments on their PR. Don't ship a competing implementation silently.

## Project Structure

```
minutes/
├── PLAN.md                    # Master plan (survives compaction — read this first)
├── CLAUDE.md                  # This file
├── BUILD-STATUS.md            # Build progress tracker
├── Cargo.toml                 # Workspace root
├── crates/
│   ├── core/src/              # 34 Rust modules — the engine
│   │   ├── capture.rs         # Audio capture (cpal), device categorization, loopback detection
│   │   ├── resample.rs        # Shared mono-downmix + 16kHz decimation resampler (used by capture + streaming)
│   │   ├── transcribe.rs      # Transcription: whisper.cpp (default) or parakeet.cpp (opt-in). Delegates to whisper-guard for anti-hallucination, optional nnnoiseless denoise
│   │   ├── diarize.rs         # Speaker diarization + attribution types (pyannote-rs, or energy-based from per-source stems)
│   │   ├── summarize.rs       # LLM summarization + speaker mapping (ureq HTTP client)
│   │   ├── voice.rs           # Voice profile storage and matching (voices.db, enrollment, cosine similarity)
│   │   ├── pipeline.rs        # Orchestrates the full flow + structured extraction
│   │   ├── notes.rs           # Timestamped notetaking during/after recordings
│   │   ├── watch.rs           # Folder watcher (settle delay, dedup, lock)
│   │   ├── markdown.rs        # YAML frontmatter + shared parsing utilities
│   │   ├── search.rs          # Walk-dir search + action item queries
│   │   ├── config.rs          # TOML config with compiled defaults
│   │   ├── pid.rs             # PID file lifecycle (flock atomic)
│   │   ├── events.rs          # Append-only JSONL event log for agent reactivity
│   │   ├── device_monitor.rs  # Audio device change detection (CoreAudio listener + auto-reconnect)
│   │   ├── streaming_whisper.rs # Progressive transcription (partial results every 2s)
│   │   ├── streaming.rs       # Streaming audio capture (AudioStream, MultiAudioStream for multi-source)
│   │   ├── logging.rs         # Structured JSON logging
│   │   ├── error.rs           # Per-module error types (thiserror)
│   │   ├── calendar.rs        # Calendar integration (upcoming meetings)
│   │   ├── daily_notes.rs     # Daily note append for dictation/memos
│   │   ├── dictation.rs       # Dictation mode (speak → clipboard + daily note)
│   │   ├── live_transcript.rs # Live transcript mode (real-time JSONL + WAV, delta reads, AI coaching)
│   │   ├── health.rs          # System health checks (model, mic, disk, watcher)
│   │   ├── hotkey_macos.rs    # macOS global hotkey registration
│   │   ├── screen.rs          # Screen context capture (screenshots)
│   │   ├── vad.rs             # Voice activity detection
│   │   ├── vault.rs           # Obsidian/Logseq vault sync
│   │   ├── knowledge.rs       # Knowledge base adapters (wiki/PARA/Obsidian) + fact writing
│   │   ├── knowledge_extract.rs # Structured fact extraction from meeting frontmatter
│   │   ├── desktop_control.rs # Desktop automation (AppleScript, tray interactions)
│   │   ├── graph.rs           # Conversation knowledge graph (people, decisions, commitments)
│   │   ├── jobs.rs            # Background job queue for async processing
│   │   └── palette.rs         # Command palette definitions and matching
│   ├── whisper-guard/          # Standalone anti-hallucination toolkit (segment dedup, silence strip, whisper params)
│   ├── cli/                   # CLI binary — 45 commands
│   ├── reader/                # Lightweight read-only meeting parser (no audio deps)
│   ├── assets/                # Bundled assets (demo.wav)
│   └── mcp/                   # MCP server — 29 tools + 7 resources + MCP App dashboard
│       └── ui/                # Interactive dashboard (vanilla TS, builds to single-file HTML)
├── site/                      # Landing page (Next.js + Remotion demo player)
├── tauri/                     # Tauri v2 menu bar app + singleton AI Assistant
├── .claude/plugins/minutes/   # Claude Code plugin — 18 skills + 1 agent + 2 hooks
├── .agents/skills/minutes/    # Portable skills mirror (Codex, Gemini, other AGENTS-aware agents)
├── .opencode/skills/          # OpenCode-native flattened skills mirror + runtime helpers
├── .opencode/commands/        # OpenCode /minutes-* slash-command wrappers
└── tests/integration/         # Integration tests (including real whisper tests)
```

## Development Commands

```bash
# Build (macOS 26 needs C++ include path for whisper.cpp)
export CXXFLAGS="-I$(xcrun --show-sdk-path)/usr/include/c++/v1"
cargo build

# Test
cargo test -p minutes-core --no-default-features   # Fast (no whisper model)
cargo test -p minutes-core                          # Full (needs tiny model)

# Lint
cargo clippy --all --no-default-features -- -D warnings
cargo fmt --all -- --check

# MCP server (TS server + interactive dashboard UI)
cd crates/mcp && npm install && npm run build       # tsc + vite single-file build
npx vitest run                                      # 30 reader.ts unit tests
node test/mcp_tools_test.mjs                        # 8 MCP integration tests
```

## Key Architecture Decisions

- **Rust** for the engine — single 6.7MB binary, cross-platform, fast
- **whisper-rs** (whisper.cpp) for transcription (default) — local, Apple Silicon optimized, params match whisper-cli defaults (best_of=5, entropy/logprob thresholds)
- **parakeet.cpp** for transcription (opt-in) — NVIDIA FastConformer via subprocess, Metal GPU acceleration on Apple Silicon. Lower WER than Whisper at equivalent model sizes. Requires `--features parakeet` at build time. See `docs/PARAKEET.md` for setup
- **ffmpeg preferred for audio decoding** — shells out to ffmpeg for m4a/mp3/ogg when available (identical to whisper-cli's pipeline). Falls back to symphonia (pure Rust) when ffmpeg isn't installed. This matters for non-English audio — symphonia's AAC decoder produces subtly different samples that trigger whisper hallucination loops (issue #21).
- **Silero VAD** (via whisper-rs) — ML-based voice activity detection integrated directly into whisper's transcription params. Prevents hallucination loops by skipping silence segments. Auto-downloaded during `minutes setup`.
- **whisper-guard** crate — standalone anti-hallucination toolkit extracted from minutes-core. 6-layer defense: Silero VAD gating, no_speech probability filtering (>80% = skip), consecutive segment dedup (3+ similar collapsed), interleaved A/B/A/B pattern detection, foreign-script hallucination detection, language-agnostic noise marker collapse (`[Śmiech]`, `[music]`, `[risas]`, etc.), trailing noise trimming. Publishable to crates.io independently.
- **nnnoiseless** (optional) — pure Rust RNNoise port for noise reduction. Behind `denoise` feature flag, controlled by `config.transcription.noise_reduction`. Processes at 48kHz with first-frame priming. Batch path only (not streaming).
- **pyannote-rs** for speaker diarization — native Rust, ONNX models (~34MB), no Python. Works in CLI, Tauri desktop app, and via MCP. Behind the `diarize` Cargo feature flag.
- **Speaker attribution** — confidence-aware system mapping SPEAKER_X labels to real names. Four levels: L0 (deterministic 1-on-1 via calendar+identity), L1 (LLM suggestions capped at Medium confidence), L2 (voice enrollment in `voices.db`), L3 (confirmed-only learning). Wrong names are worse than anonymous — only High-confidence attributions rewrite transcript labels. `speaker_map` in YAML frontmatter is the canonical attribution data. Voice profiles stored in `~/.minutes/voices.db` (separate from `graph.db` which wipes on rebuild).
- **symphonia** for audio format conversion — m4a/mp3/ogg → WAV, pure Rust (fallback when ffmpeg unavailable)
- **Windowed-sinc resampler** (32-tap Hann) — alias-free 44100→16000 downsampling for WAV inputs
- **ureq** for HTTP — pure Rust, no secrets in process args (replaced curl)
- **fs2 flock** for PID files — atomic check-and-write, prevents TOCTOU races
- **Tauri v2** for desktop app — shares `minutes-core` with CLI, ~10MB
- **Markdown + YAML frontmatter** for storage — universal, works with everything
- **Structured extraction** — action items + decisions in frontmatter as queryable YAML
- **No API keys needed** — Claude summarizes conversationally via MCP tools
- **Live transcript** — per-utterance whisper → JSONL append with PidGuard flock for session exclusivity. Delta reads via line cursor or wall-clock duration. Optional WAV preservation for post-meeting reprocessing. Agent-agnostic: JSONL readable by any agent, MCP tools for Claude, CLAUDE.md context injection for Codex/Gemini/OpenCode.

## Key Patterns

- All audio processing is local (whisper.cpp or parakeet.cpp + pyannote-rs + Silero VAD). ffmpeg recommended but optional.
- Claude summarizes via MCP when the user asks (no API key needed)
- Optional automated summarization via Ollama (local), Mistral, or cloud LLMs
- Config at `~/.config/minutes/config.toml` (optional, compiled defaults work)
- Tauri assistant uses a singleton workspace at `~/.minutes/assistant/`
- `CLAUDE.md` and `AGENTS.md` hold matching general assistant instructions; `CURRENT_MEETING.md` is the active meeting focus for "Discuss with AI"
- Meetings: `~/meetings/` | Voice memos: `~/meetings/memos/`
- `0600` permissions on all output (sensitive content)
- PID file + flock for recording state (`~/.minutes/recording.pid`)
- Watcher: settle delay, move to `processed/`/`failed/`, lock file
- JSON structured logging: `~/.minutes/logs/minutes.log`
- 100% doc comment coverage on all pub functions

## Test Coverage

~290 tests total:
- 85 whisper-guard tests (72 unit + 7 integration + 6 doctest) covering resample, normalize, strip_silence, dedup_segments, dedup_interleaved, collapse_noise_markers, strip_foreign_script, trim_trailing_noise, clean_transcript, clean_segments, CleanOptions toggles + keep_dedup_annotations, CleanStats summary, fork-user integration path, idempotency, pathological inputs, and known-limitation regression guards
- 130 core unit tests (all modules including screen, calendar, config, watch, streaming whisper, vault, dictation, live_transcript, health, vad, hotkey, device_monitor, diarize)
- 10 integration tests (pipeline, permissions, collisions, search filters)
- 33 Tauri unit tests (commands, call detection, call capture)
- 11 CLI tests
- 6 reader crate tests (search, parse)
- 30 reader.ts unit tests (vitest — frontmatter parsing, listing, search, actions, profiles; reader lives in crates/sdk/src/reader.ts)
- 8 MCP integration tests (CLI JSON output, TypeScript compilation)
- 5 hook unit tests (post-record hook: 4 guard tests + 1 nudge test)

## Claude Ecosystem Integration

- **MCP Server**: 29 tools + 7 resources for Claude Desktop / Cowork / Dispatch (`npx minutes-mcp` for zero-install)
- **Claude Code Plugin**: 18 skills (7 capture + 1 search + 4 lifecycle + 2 coaching + 3 knowledge + 1 intelligence) + meeting-analyst agent + SessionStart + PostToolUse hooks
- **Interactive meeting lifecycle**: `/minutes-brief` → `/minutes-prep` → record → `/minutes-tag` → `/minutes-debrief` → `/minutes-mirror` → `/minutes-weekly` with skill chaining via `.brief.md` / `.prep.md` files; `/minutes-graph` for cross-meeting entity queries
- **Conversational summarization**: Claude reads transcripts via MCP, no API key needed
- **Auto-tagging + alerts**: PostToolUse hook tags meetings with git repo, checks for decision conflicts, surfaces overdue action items, nudges `/minutes-debrief` + `/minutes-tag`
- **Proactive reminders**: SessionStart hook checks calendar for upcoming meetings and recommends `/minutes-brief` (fast) or `/minutes-prep` (goal-setting) based on time-to-meeting
- **Portable skills**: `.agents/skills/minutes/` mirrors all 18 skills for Codex, Gemini, and other AGENTS-aware agents, while `.opencode/skills/` + `.opencode/commands/` gives OpenCode native skill discovery and `/minutes-*` slash commands. Both use `$MINUTES_SKILLS_ROOT` instead of `${CLAUDE_PLUGIN_ROOT}` and keep CLI-only speaker confirmation (no desktop app references). Runtime helpers at `_runtime/hooks/lib/` must stay in sync with `.claude/plugins/minutes/hooks/lib/`.
- **Desktop assistant**: Tauri AI Assistant is a singleton session that can switch focus into a selected meeting without spawning parallel assistant workspaces
- **Live coaching**: Tauri Live Mode toggle starts real-time transcription; the assistant workspace `CLAUDE.md` and `AGENTS.md` auto-update so the connected Recall session, Claude Desktop/Code, or any other agent can read the live JSONL file and coach mid-meeting. There is no dedicated transcript/coaching panel in Tauri v1; the coaching happens through the assistant chat surface.
