# Pre-Commit Checklist

**Run this mental checklist before every commit from this repo.** Not every item applies to every commit — check which areas your changes touch and verify those.

**CI parity note.** CI runs on `ubuntu-latest`, `macos-latest`, and `windows-latest`, and it builds `minutes-core` with `--features parakeet` on all three. The Pre-Commit Checklist is designed to catch CI failures *before* pushing. The common traps that don't surface on a local macOS build:
- `cfg(unix)`-gated code paths: macOS has `unix=true`, so unix-only imports/methods appear to work locally even when they'd break on Windows. Any `use std::os::unix::...`, `PermissionsExt::set_mode`, `UnixStream`, or similar must be inside `#[cfg(unix)]`.
- `cfg(all(feature = "parakeet", unix))` stubs: when a module has both a real unix impl and a non-unix stub, any struct or function the caller accesses must exist in *both* variants with compatible shape. Callers that do `result.some_field` need `some_field` on the stub too.
- Generated/sync'd files: `site/lib/release.ts` is generated from `manifest.json` by `scripts/sync_site_release_version.mjs`. Version bumps in `manifest.json` require running that script — CI's `Site Release Link Consistency` job will fail otherwise.
- Rust toolchain pin: `rust-toolchain.toml` at repo root pins the rustc/clippy/rustfmt version (currently `1.95.0`). CI's `dtolnay/rust-toolchain@stable` action installs latest stable as the system default, but rustup's cargo proxy reads the pin file and routes invocations through the pinned toolchain. **Locally this works only if cargo runs through the rustup proxy** — verify with `command -v cargo` matching `rustup which cargo` (typically `~/.cargo/bin/cargo`; `CARGO_HOME` overrides relocate it). If you have Homebrew rust installed and `which cargo` resolves to `/opt/homebrew/bin/cargo`, the pin is ignored and you'll hit the same lints CI does — the exact failure mode that caused PR #206's tag delay (commits 4954de2, 21cd699 are both clippy-fix-only commits from this drift). Fix: prepend rustup's bin dir to PATH in your shell (`export PATH="$(dirname "$(rustup which cargo)"):$PATH"` in your shell rc), or `brew uninstall rust`. The build scripts (`scripts/build.sh`, `scripts/install-dev-app.sh`) detect via `rustup which cargo` so script-driven builds are immune; only interactive `cargo` calls need the shell PATH fix.

| Area | When to check | How to verify |
|------|---------------|---------------|
| **Manifest tools sync** | Any new/renamed/removed MCP tool | Compare `manifest.json` tools array against `server.tool()` and `registerAppTool()` calls in `crates/mcp/src/index.ts` |
| **Manifest description** | New user-facing features | Read `long_description` in `manifest.json` — does it mention the new capability? |
| **Manifest version** | Version bumps | `manifest.json` version must match all other version sources |
| **MCP server rebuild** | Any change to `crates/mcp/src/` or `crates/mcp/ui/` | `cd crates/mcp && npm run build` |
| **MCPB bundle guard** | Any change to `.mcpbignore`, `crates/mcp/**`, `crates/sdk/**`, `manifest.json`, or anything else that affects what lands in the packed extension | `mcpb pack . minutes.mcpb && ./scripts/check_mcpb_bundle.sh minutes.mcpb && ./scripts/smoke_mcpb_handshake.sh minutes.mcpb`. The `MCP Server` CI job runs this on every push since issue #149; running it locally first saves a round-trip. |
| **Toolchain matches the pin** | Any Rust change (precondition for fmt/clippy/test) | `command -v cargo` should print the same path as `rustup which cargo` (rustup proxy), and `cargo --version` should match the `channel` in `rust-toolchain.toml`. If cargo resolves to `/opt/homebrew/bin/cargo` or another non-rustup path, the pin is ignored — your local clippy/rustfmt versions silently drift from CI. Fix once: prepend rustup's bin dir to PATH in your shell config (typically `~/.cargo/bin`, or `dirname "$(rustup which cargo)"` if `CARGO_HOME` is overridden), or `brew uninstall rust`. |
| **cargo fmt** | Any Rust change | `cargo fmt --all -- --check` |
| **cargo clippy** | Any Rust change | `cargo clippy --all --no-default-features -- -D warnings` |
| **cargo test** | Any Rust change | `cargo test -p minutes-core --no-default-features --lib` — CI runs the full suite; the local no-default-features run catches most regressions without needing the whisper model. |
| **UI render verification** | Any change to `tauri/src/index.html`, any new Tauri `cmd_*` exposed to the frontend, or any modal/overlay/panel layout shift | Build the dev app with `./scripts/install-dev-app.sh` and click-test the affected surface in `~/Applications/Minutes Dev.app`. Type checks and Rust unit tests don't catch UI render bugs — PR #206 surfaced 4 such bugs only via click-testing (path candidates duplicated, build-artifact bundles polluting the picker, default selection wrong, ad-hoc detection always returning true). None of the four would have failed any test or CI job. If the UI change is non-trivial, also run the gstack `/qa` skill against the running dev app. |
| **Unix-only APIs behind cfg** | Any new `use std::os::unix::*`, `PermissionsExt`, `UnixStream`, `set_mode(0o...)`, or similar | Wrap the import AND every call site in `#[cfg(unix)]`. Also gate any test that shells out to a POSIX script or uses chmod semantics. CI's `Test (windows-latest)` fails otherwise; `cargo check --no-default-features` on macOS won't catch this because `cfg(unix)` is true locally. |
| **Feature-gated stub struct parity** | Any change to `parakeet_sidecar.rs` or similar file with an `imp` + `imp_stub` split | When the unix imp exposes a struct with fields that callers read (`result.transcript`, `result.elapsed_ms`, etc.), the stub struct in the `not(unix)` branch must declare the same fields (even though the stub function always returns `Err` and the match arm is unreachable at runtime). Without this, Windows + `--features parakeet` fails with E0609 "no field" errors. |
| **Site release constants** | Any bump to `manifest.json` version | `node scripts/sync_site_release_version.mjs` regenerates `site/lib/release.ts` to match. CI's `Site Release Link Consistency` job fails otherwise. Treat this like the other version-sync sites in the Release Checklist. |
| **SDK rebuild** | Any change to `crates/sdk/src/` | `cd crates/sdk && npm run build` |
| **Mutual exclusion** | Any change to recording/dictation/live transcript start paths | Verify all three modes check each other's PID/state: `live_transcript::run` checks recording+dictation PIDs, `cmd_record`/`capture::record_to_wav` checks live PID, `dictation::run` checks live PID, Tauri `cmd_start_*` checks `live_transcript_active`+`recording`+`dictation_active` |
| **Tauri command duplication** | Changes to live transcript start/stop logic | Both `cmd_start_live_transcript` and `handle_live_shortcut_event` must use the shared `try_acquire_live` + `run_live_session` functions. Do NOT duplicate logic. |
| **Desktop app identity** | Any Tauri packaging, dogfooding, Screen Recording, Input Monitoring, Accessibility, call capture, hotkey, or repeated-permission work | Use `./scripts/install-dev-app.sh`, not `rm -rf /Applications/Minutes.app && cp ...`. If a local signing identity exists, export `MINUTES_DEV_SIGNING_IDENTITY` first. Test `~/Applications/Minutes Dev.app`, not `/Applications/Minutes.app`. |
| **README accuracy** | New/removed tools, features, crates, or CLI commands | Tool/resource counts, crate list in Architecture, feature sections, and CLI examples in README.md must reflect the current state. Check: tool count matches `manifest.json`, crate list matches `ls crates/*/`, module count matches `ls crates/core/src/*.rs` |
| **Website accuracy** | New/removed tools, CLI commands, features, version bumps, or skill changes | The hero stat line (version, MCP tool count, CLI command count, test count) is **generated** from `site/lib/release.ts` — run `node scripts/sync_site_release_version.mjs` after any change that affects those counts (the `Site Release Link Consistency` CI job fails if it drifts, and the script now also scans `manifest.json`, `crates/cli/src/main.rs`, and Rust + TS test files). Hand-written copy in `site/app/page.tsx` (release banner, feature descriptions, pipeline blurb) must still be updated manually — the script cannot reason about prose. The `/for-agents` skill catalog (rendered from `site/lib/skills-catalog.json`) is **generated** — never hand-edit that file or the skill cards in `site/app/for-agents/page.tsx`; run the skill compiler instead (see **Skill compiler outputs sync**). Redeploy the landing page with the prebuilt Vercel flow from the repo root: `npx vercel@50.38.2 build --prod && npx vercel@50.38.2 deploy --prebuilt --yes --prod --scope evil-genius-laboratory` |
| **npm dep versions** | Version bumps | `crates/mcp/package.json` `minutes-sdk` dep must reference a version that's actually published on npm. Check with `npm view minutes-sdk versions --json` |
| **Release notes drafted** | Version bumps / releases | Every release is a visibility moment in followers' GitHub feeds. Draft compelling release notes BEFORE creating the release. No empty releases — ever. See [Release Checklist](RELEASE.md) step 5. |
| **Release warranted?** | New/removed MCP tools, new CLI commands, user-facing features | Manifest changes (new tools, updated description) don't reach Claude Desktop users until a release is cut and `.mcpb` is uploaded. If the change is user-visible, plan a release. |
| **marketplace.json plugin version bumped** | Any change to `.claude/plugins/minutes/` that should reach users | `.claude-plugin/marketplace.json` → `plugins[0].version` is the **single field Claude Code reads** to decide what "latest" means for plugin-only releases. Bumping `plugin.json` alone is not enough — if `marketplace.json` still advertises the old version, `/plugin update minutes@minutes` reports "already at latest" and users silently miss everything shipped. Keep all three version surfaces in lockstep: `marketplace.json` `plugins[0].version`, `.claude/plugins/minutes/plugin.json`, `.claude/plugins/minutes/.claude-plugin/plugin.json`. |
| **Plugin release upgrade snippet in notes** | Any plugin-only release (push-to-main, no tag) | Plugin releases don't auto-deliver to existing users. Every release-note body must include the two-command refresh sequence: `/plugin marketplace update minutes` then `/plugin update minutes@minutes`, then restart Claude Code. Without this, pre-release adopters stay silently stuck on the version they installed originally — their local marketplace mirror is a git clone that only pulls when explicitly asked. See `notes-release-plugin-v0.8.0.md` for the template. |
| **Skill compiler outputs sync** | Any change to `tooling/skills/sources/`, `.claude/plugins/minutes/skills/`, or `.claude/plugins/minutes/hooks/lib/`; any new, renamed, or removed skill | The compiler in `tooling/skills/` writes **four** targets from canonical sources: `.claude/plugins/minutes/skills/` (plugin, now includes per-skill `scripts/` and `references/` assets — Claude host's `assetPolicy` is `copy` mode), `.agents/skills/minutes/` (Codex/Gemini/AGENTS-aware), `.opencode/skills/` + `.opencode/commands/` (OpenCode), and `site/lib/skills-catalog.json` (consumed by `/for-agents`, its JSON-LD, and the llms.txt generator). Rebuild with `cd tooling/skills && npm run build && npm run compile`, then verify with `npm run test && npm run check && npm run compile:dry`. `check` fails if any of the four outputs is stale. Runtime helpers must stay byte-identical across `.claude/plugins/minutes/hooks/lib/`, `.agents/skills/minutes/_runtime/hooks/lib/`, and `.opencode/skills/_runtime/hooks/lib/`. **After the skill compiler, also regenerate llms.txt** because the skill catalog feeds it: `npm --prefix site run generate:llms` to write, `npm --prefix site run check:llms` to verify — this is now enforced in CI. **New skills require three site-facing frontmatter fields** under `metadata`: `site_category`, `site_example`, `site_best_for` — the compiler throws with a clear error on any missing field. |
