Imported from NicolaiSchmid/steno (
AGENTS.md). Install upstream withnpx skills add NicolaiSchmid/steno. Copyright stays with the author.
AGENTS.md — Coding Agent Guidelines for Steno
Steno is a bot-free meeting recorder for the Mac with a dumb iOS companion recorder. Swift core and macOS app, Expo (React Native) mobile app. GitHub repository
NicolaiSchmid/steno; local checkouts may still sit in a directory namedaudaciousfrom before the rename.
Scope and every settled decision live in
.plans/2026-09-24-initial-scope.md.
Read it before proposing architecture. Do not widen or narrow the scope in
code; propose changes as a new plan file first.
Workspace layout
| Area | Path | Description |
|---|---|---|
| Rust core | Cargo.toml, crates/ |
Cargo workspace replacing the Swift package crate by crate (plan .plans/2026-10-02-rust-core-and-tauri-shell.md): steno-bridge first, then steno-core, steno-host (the view models and the bridge host over the store), steno-speech (with steno-speech-coreml, the Mac's CoreML backend, unsafe confined to its coreml module), steno-audio, steno-llm, steno-adapters, steno-handover, steno-cli |
| Desktop app | apps/desktop/ |
Tauri 2 shell for macOS, Linux and Windows hosting the web UI; added by WP3 of the same plan. Holds no logic apart from the fixture host behind its fixture-host feature, which answers the bridge from the recorded fixtures until WP6 wires the real host |
| Swift core | Package.swift, Sources/, Tests/ |
Swift package, shipping until the Rust cutover: StenoCore, StenoAudio, StenoSpeech, StenoLLM, StenoAdapters, StenoHandover and the steno CLI |
| macOS app | apps/macos/ |
The Swift shell: AppController, the view models, the three window hosts and their bridges (Steno/Web/), the services; SwiftUI draws only the menu bar item and the floating panels. Xcode project generated by xcodegen from project.yml (the .xcodeproj is not committed) |
| Web UI | apps/macos/web/ |
The Mac windows (main, Settings, onboarding) as a React and Tailwind app served from the bundle in WKWebView. Own pnpm lockfile and Biome config, own README. Bridge contract in Sources/StenoBridge, fixtures in apps/macos/web/fixtures/bridge/ (served by the mock bridge outside the app only; pnpm build fails if they reach dist/) |
| Landing page | apps/site/ |
steno.nicolaischmid.com: Next.js 16 static export (home page and /recording-law), React and Tailwind, Biome. Lives in the root pnpm workspace (package.json, pnpm-workspace.yaml, pnpm-lock.yaml at the repo root), own README |
| Mobile | mobile/ |
Expo dev-client iOS recorder. Own pnpm lockfile, own Biome config, own README |
| Plans | .plans/ |
Dated decision and implementation plans |
| Research | docs/research/ |
Product teardowns and landscape notes |
| Skills | .agents/skills/ |
Repository-local agent skills (authoritative); .claude/skills/ holds symlinks |
Plans
- Store all implementation plans in the repo root
.plans/directory, namedYYYY-MM-DD-<slug>.md. - Create plan files there instead of using ad-hoc locations elsewhere in the repo.
- When writing paths in plan files, always use paths relative to the repo root, never absolute machine-specific full paths.
- Commit plan files to git with the related work so the planning history is preserved.
- A plan that supersedes an earlier one says so at the top of both files.
Skills
Repository-local skills live in .agents/skills/ (read by Codex).
.claude/skills/ holds symlinks to them so Claude Code and T3 Code discover
the same files. Do not copy skill text between the two. For writing or
rewriting docs, use the docs-writing skill.
Git
- Use Conventional Commits:
type(scope): short summary. Common types:feat,fix,refactor,docs,chore,test,ci. Scopes:core,macos,mobile,adapters,plans,ci. - Enable the repository hooks once per clone:
git config core.hooksPath .githooks. The post-checkout hook bootstraps worktrees viascripts/setup-worktree.sh. - Prefer
ghREST-backed commands and the GitHub REST API. Use GraphQL only when REST cannot provide the data. Never use GraphQL for routine PR creation, status checks or comments. - After opening or updating a PR, wait for CI and address failures before considering the task complete.
Worktrees
scripts/setup-worktree.sh copies mobile/.env from the main checkout and
runs pnpm install in mobile/ when the lockfile changed. If a worktree is
missing local setup, run it. Do not ask first.
Swift (core and macOS app)
- Swift 6 language mode, strict concurrency. Targets macOS 15+, Apple Silicon only.
- Formatting with
swift formatfrom the toolchain, default configuration, until a.swift-formatfile says otherwise. - One Swift package for everything that is not UI. The macOS app target depends on it; no logic in the app target that the iOS or CLI surfaces could need.
- Protocols for every pluggable boundary named in the scope:
SpeechEngine,Diarizer,EchoCanceller,LanguageModel,Destination. Two implementations before generalising further. - Persistence through GRDB. Migrations are append-only and live in one file.
- Audio never leaves the device. Only these code paths open network
connections, and a new one needs a plan first:
- a
Destinationand the LLM client, which send text only; - the model download, which fetches models and sends nothing but the request;
- the updater, which fetches the signed update feed and the update;
- the phone handover server, which listens on the local network for paired phones and only receives their recordings.
- a
- Tests with
swift test. Fixtures underTests/Fixtures/; keep audio fixtures short and synthetic, never recordings of real meetings.
Landing page (apps/site/)
- Next.js 16 App Router with
output: "export"; the site must stay fully static so any host can serveout/. No server components that need a runtime, no API routes. - Installed from the repository root (
pnpm install), run withpnpm dev:site;pnpm check:siteandpnpm build:sitemust pass before a PR (.github/workflows/site-ci.yml). Vercel builds from the rootvercel.json; keep its paths in step with the workspace.mobile/andapps/macos/web/stay outside the root workspace;pnpm-workspace.yamlsays why. - Design and structure follow t3.codes (pingdotgg/t3code
apps/marketing): dark only, DM Sans and JetBrains Mono, hero → feature blocks → closing CTA. Tokens live insrc/app/globals.css; no raw colours at call sites. Hue is reserved for the red live state, the primary action is white on dark. - Copy presents Steno as multi-platform (macOS, Windows, Linux) and does not
mention the vault or Obsidian. The hero mock uses the README's example
meeting, never a real recording. URLs and which desktops have a released
build live in
src/lib/site.ts.
Mobile (mobile/)
- Expo SDK 57 dev client, pnpm 11 (
packageManagerinmobile/package.json), Node 24. Not Expo Go. - Biome is the sole linter and formatter:
pnpm lint/pnpm lint:fix. Tabs, double quotes, sorted imports and Tailwind classes. pnpm check(lint, typecheck, tests) must pass before a PR.- Files kebab-case for modules, PascalCase for React components. Functional
components only. Theme tokens come from
global.css; no hard-coded colours at call sites. Motion tokens come fromsrc/lib/motion.ts. - Runtime config is read from
process.env.EXPO_PUBLIC_*insrc/lib/env.ts, never throughapp.config.tsextra, so it stays out of the native fingerprint.env.tsmust not throw at module load. - Native identifiers (bundle id, slug, EAS project id,
ascAppId) are immutable once a build ships. Change them only in a plan that says why. - Delivery:
.github/workflows/mobile-cd.ymldecides OTA vs TestFlight from the Expo native fingerprint. Seemobile/README.mdbefore touching anything that moves the fingerprint (dependencies, config plugins, permissions).
Rust (core and desktop)
- Edition 2024, stable toolchain pinned in
rust-toolchain.toml.cargo fmt,cargo clippy --workspace --all-targets -- -D warningsandcargo test --workspacemust pass on Linux, macOS and Windows (rust-ci.yml). unsafeonly in platform backends (steno-audio, the CoreML backend) and wrapped in a safe module with a comment on every invariant.- The bridge fixtures in
apps/macos/web/fixtures/bridge/are the contract: a Rust type that cannot round-trip its fixture byte for byte is a failing test, not a formatting difference. - Shared dependency versions live in the root
[workspace.dependencies]; crates inherit them withworkspace = true. A PR that needs a new shared dependency adds it there, not in a crate manifest. - Persistence shares the Swift schema. New migrations are written once in SQL
and mirrored in
Migrations.swiftand the Rust.sqlfiles until cutover; the schema parity test proves them equal. - Same privacy rule as Swift, with the same list of network paths:
destinations (
steno-adapters) and the LLM client (steno-llm), text only; the model downloads (steno-speech'sModelStoreandsteno-diarize's model fetch); the Tauri updater; and the handover server (steno-handover). ONNX Runtime's telemetry stays off in every process that opens a session.
Review guidelines
Prioritise high-signal findings: correctness, real-time audio safety (allocations or locks on the audio thread, buffer underruns), privacy (audio or transcript leaving the device unexpectedly), data integrity in SQLite and in exported files, and fit with the scope. Do not comment on formatting the tools enforce. Give exact file and line references, the failure mode, and the smallest reasonable fix.
