# Pace - Key Files

<!-- Canonical home for the per-file reference table. Linked from AGENTS.md → Key Files. -->

This is the per-file reference for Pace's source, scripts, and bundled resources. It is the canonical home for the table that previously lived inline in the [repository AGENTS.md](https://github.com/HeyPace/pace/blob/main/AGENTS.md); the architecture narrative stays in [the architecture docs](../architecture/overview.md) and links here.

| File | Lines | Purpose |
|------|-------|---------|
| `leanring_buddyApp.swift` | ~340 | Menu bar app entry point. Uses `@NSApplicationDelegateAdaptor` with `CompanionAppDelegate` which validates the local tool registry at startup, warms LM Studio models, non-activating prewarms Mail for fast drafts when enabled, creates `MenuBarPanelManager`, `PaceMenuBarOverlayManager`, starts `CompanionManager`, handles `pace://` deeplinks via `application(_:open:)` with pre-launch buffering, and optionally installs runtime smoke hooks when `PACE_ENABLE_SMOKE_HOOKS=1`. No main window — the app lives entirely in menu-bar/notch surfaces. |
| `PaceDeepLinkParser.swift` | ~77 | Pure parser for the `pace://` URL scheme (listen, chat, watch, panel). Reject-on-ambiguity rules, percent-decoding via URLComponents, and a hard 500-char chat-text cap because deeplinks are an external input surface. |
| `CompanionManager.swift` | ~1260 | Class declaration and stored properties only — the god-class shell after Phase A decomposition. |
| `CompanionManager+AgentLoop.swift` | ~2660 | AI response pipeline (plan-act-observe loop, fast paths, clarification, planner dispatch). |
| `CompanionManager+PrivateBindings.swift` | ~680 | Permission polling, LM Studio reachability, barge-in VAD, wake-word, shortcut bindings. |
| `CompanionManager+Lifecycle.swift` | ~860 | `start()`/`stop()`, permission requests, avatar/deeplink/chat entry points. |
| `CompanionManager+LocalRetrieval.swift` | ~470 | Local retrieval context, connector refresh, source management. |
| `CompanionManager+ConversationMemory.swift` | ~330 | Conversation turn recording, thread memory, episodic facts. |
| `CompanionManager+TrustSurfacesRuntime.swift` | ~280 | Undo banner, reply replay, failure narration. |
| `CompanionManager+MorningTriage.swift` | ~270 | Morning triage daily brief and watch-mode toggle. |
| `CompanionManager+PostureWatch.swift` | ~340 | Posture watch, watch-mode events, approval, tool-debug helpers. |
| `CompanionManager+LocalMemoryCommands.swift` | ~205 | Remember-site, local memory, recipe voice commands, MCP context. |
| `CompanionManager+DemonstrationFlow.swift` | ~170 | Demonstration flow record/stop/replay. |
| `CompanionManager+PlannerTierPicker.swift` | ~210 | Planner tier picker and Direct-API keys. |
| `CompanionManager+UnifiedMemoryDualWrite.swift` | ~115 | Unified memory Phase 2 dual-write. |
| `CompanionManager+UnifiedMemoryUIAccessors.swift` | ~100 | Unified memory Phase 4 accessors and resync. |
| `CompanionManager+CloudBridge.swift` | ~75 | Cloud bridge setters and consent. |
| `CompanionManager+ProactivitySettings.swift` | ~50 | Proactive nudge toggles and profile setter. |
| `CompanionManager+ProactivePipeline.swift` | ~35 | `PaceProactivityPipeline` forwarders. |
| `PaceScreenContextService.swift` | ~820 | `@MainActor` coordinator extracted from `CompanionManager` during Wave 7b. Owns the per-screen VLM analysis cache (keyed by `PaceScreenAnalysisCacheIdentity`, value `PaceCachedScreenAnalysis`), the PTT-press prewarm `Task<PaceScreenContextPrewarmedSnapshot?, Never>`, and the AX + OCR + VLM merge logic. Public surface: `prewarmScreenContext(reason:)`, `consumeInFlightPrewarmedSnapshot()`, `hasInFlightPrewarmedTask`, `buildUserPromptWithLocalVLMContextIfEnabled(...)`, `cachedDescriptionIfFresh(screenLabel:maxAgeSeconds:referenceDate:)`, `invalidateAllCachedAnalysis(reason:)`. Reads the live `Read My Screen` toggle through an injected `() -> Bool` closure so the user-facing preference still lives on `CompanionManager`. Cache-invalidation rule unchanged from the pre-extraction code: analyzer-identity or display-frame change → MISS; pixel-hash match → free HIT; pixel-hash mismatch but visual-fingerprint diff under threshold → cheap HIT with timestamp refresh. |
| `CompanionSystemPrompt.swift` | ~460 | The system prompt sent to the local planner on every turn. Extracted to its own file because it's a behavior contract — small wording changes here change end-to-end behavior, so it deserves its own diff-able artifact. Generates the available local-tool docs from `PaceToolRegistry` and documents v10 dictate/edit/action JSON contracts. |
| `PaceTagParsers.swift` | ~175 | Pure isolation-free parsers for the inline tag dialect the planner emits: `[POINT:x,y]`, `[DONE]`, the `transcriptIsLikelyScreenReferential` keyword heuristic, and `readMaxAgentStepCount`. Extracted from `CompanionManager` so each parser is unit-testable in isolation. The `PointingParseResult` struct also lives here. |
| `PaceUserPreferencesStore.swift` | ~370 | Typed key namespace + load/save helpers for boolean user preferences (`useLocalVLMForScreenContext`, `isWalkingAvatarEnabled`, `isPaceCursorEnabled`, `areCursorAnnotationsEnabled`, `requiresActionApproval`). Replaces hand-rolled `UserDefaults` patterns with stringly-typed keys. `@Published` properties still live on `CompanionManager`; this owns only the storage layer. |
| `PaceCursorShape.swift` | ~50 | `CodexArrowShape` — the SwiftUI `Shape` Pace renders as its on-screen cursor. Extracted from `OverlayWindow.swift` so the shape can be reused without dragging the whole overlay machinery along. |
| `PaceOverlayPillViews.swift` | ~155 | Reusable SwiftUI voice-state views (`WhisperFlowVoicePillView`, `BlueCursorSpinnerView`) retained for future cursor modes. The active conversation indicator now lives in `PaceMenuBarOverlay.swift`. |
| `MenuBarPanelManager.swift` | ~317 | Custom NSPanel lifecycle for the floating companion panel. Manages show/hide/position, anchors to the menu-bar overlay frame, exposes gated smoke-test show/hide hooks, and installs click-outside-to-dismiss monitor. Does not create a visible status item. `createPanel()` picks the panel content by the `useChatPanelAsPrimarySurface` preference: ON → `PaceChatPanelView` (premium chat panel phase 1), OFF (default) → the shipped `PacePanelChatView`. |
| `PacePanelChatView.swift` | ~356 | The shipped notch-panel conversation surface (flag OFF default): compact header (status + gear + close), live transcript backed by `PaceChatSession` with inline tool chips from `recentActionResults`, live speech / streaming-reply bubbles, and a sticky input. Replaced the prior `CompanionPanelView` dashboard as the panel's content; `CompanionPanelView` is kept in the tree for a one-line revert. |
| `PaceChatPanelView.swift` | ~676 | Phase 1 of the premium chat panel (PRD `docs/product/prds/premium-chat-panel.md`), rendered only when `useChatPanelAsPrimarySurface` is ON. Header strip (voice state + gear → Settings), transcript column merging chat bubbles with inline tool-activity rows (running/done/failed), morning-brief and meeting cards as inline transcript cards, live speech + streaming-reply bubbles, and a permanently docked input that submits through `PaceChatSession.submitUserMessage` → `submitChatTranscriptFromChatSession`. All pure rendering decisions live in `PaceChatTranscriptModel`; auto-scroll respects Reduce Motion. |
| `PaceChatTranscriptModel.swift` | ~351 | Pure, isolation-free (`nonisolated`) mapping layer behind `PaceChatPanelView`. Maps `PaceChatMessage` → transcript row models (alignment + tint decisions), `PaceActionExecutionObservation` / `PaceActionRunRecord` → tool-activity row models (three-state running/done/failed contract, superseded-planned dedupe, one-line summaries, SF Symbol names), merges both into a deterministic chronological thread, and owns the empty-state decision. Unit-tested off the main actor in `PaceChatTranscriptModelTests`. |
| `PaceMenuBarOverlay.swift` | ~590 | Top menu-bar/notch `NSPanel` capsule that visually extends the MacBook notch. Uses a flat top edge with rounded lower corners, shows one fixed-width generic icon slot on each side, runs a right-side sound animation for voice turns, switches to static active-state bars/avatar glyphs when Reduce Motion is enabled, and toggles the existing companion panel when tapped. |
| `CompanionPanelView.swift` | ~850 | SwiftUI panel content for the menu bar dropdown. Shows companion status, turn HUD state including unsupported local-only requests and clickable clarification options, push-to-talk instructions, main planner plus answer-planner status, local model status, ASR provider readiness, TTS voice quality, read-screen toggle, cursor annotation toggle, action approval toggle, watch-mode toggle, core permission rows, local-tool permission preflight rows, recent action results, local memory and retrieval summaries, settings-window entry point, and quit button. Dark aesthetic using `DS` design system. |
| `PaceSettingsWindow.swift` | ~220 | Normal macOS settings window for management surfaces too large for the notch panel. Provides tabs for General preferences, MCP server config, permissions, voice/ASR status, local memory/retrieval status with reset, explicit file-root picker controls, per-source enable/disable and clear controls, and recent action history. Uses the same `CompanionManager` state and preference setters as the panel. |
| `OverlayWindow.swift` | ~1180 | Full-screen transparent overlay hosting the blue cursor and response placement. Handles cursor animation, element pointing with bezier arcs, multi-monitor coordinate mapping, fade-out transitions, and Reduce Motion fallback that snaps pointing/return movement and suppresses nonessential bubble/fade animation. |
| `CompanionResponseOverlay.swift` | ~380 | SwiftUI view for the response text bubble displayed next to the cursor in the overlay. |
| `CompanionScreenCaptureUtility.swift` | ~132 | Multi-monitor screenshot capture using ScreenCaptureKit. Returns labeled image data for each connected display. |
| `PaceScreenImageDiffer.swift` | ~105 | Cheap image-diff gate for screen captures. Downsamples screenshots into grayscale fingerprints and reports mean pixel delta + changed-pixel ratio so Pace can reuse cached screen analysis when only trivial visual noise changed. |
| `PaceScreenWatchMode.swift` | ~173 | Explicit watch-mode primitive. Samples all screens at an interval, compares fingerprints, throttles repeated changes, classifies meaningful changes as major/content/focused-region events, and emits them for panel/voice watch mode. |
| `PaceWatchModeCommandParser.swift` | ~71 | Pure parser for explicit watch-mode voice commands. Routes start/stop phrases before the planner so mode changes stay local and cheap. |
| `PaceVisualFindCommandParser.swift` | ~235 | Pure parser for "find `<text>` on screen" / "show me where `<text>` is" voice commands. Requires an explicit screen marker ("on screen"/"on the screen"/"on my screen") OR the "show me where … is" form so plain "find my keys" chitchat never matches; extracts the query (strips wrapping quotes + a leading article + trailing locator words). Routed AFTER the skill parser in the agent loop so "run the find skill" stays a skill run. Returns `PaceVisualFindCommand` (nil on empty query). |
| `PaceVisualFindService.swift` | ~250 | `@MainActor` deterministic OCR-grounded screen-text search — no LLM. `findAndMark(query:overlayController:visionOCRClient:)` captures screens via `CompanionScreenCaptureUtility`, OCRs each via `PaceVisionOCRClient.recognizeText(...)` (same call `PaceScreenContextService` makes), substring-matches the query, converts each padded pixel box to an AppKit-global rect with the same pattern as `PaceAnnotationActionDrainer`, and draws red rectangles via `PaceAnnotationOverlayController.setAnnotations(...)` (cap 20, cursor-screen-first, first match labelled). Pure nonisolated helpers (`matchBoxes`, `paddedPixelBoundingBox`, `truncatedMatchLabel`, `convertPixelBoxToAppKitGlobalRect`, `orderCapturesCursorScreenFirst`, `spokenConfirmation`) are unit-tested without capture/OCR. |
| `PaceIntentClassifier.swift` | ~470 | Rule-based local classifier for routing turns into chitchat, pure knowledge, screen description, screen action, phone-large-model, or unknown. Pure-knowledge turns skip screen capture and use the text-only planner path. |
| `PaceTurnHUDState.swift` | ~349 | Small user-visible turn status model plus first-pass clarification/unsupported helpers. Tracks listening, understanding, acting, clarification, done, failed, and unsupported local-only states for the panel/cursor surfaces without involving the planner. Clarifies ambiguous edit/destructive commands, resolves clicked clarification options by rewriting ambiguous pronouns into explicit targets, and blocks cloud/large-model escalation requests with local-only copy. Also defines the visual-target ambiguity clarification model (`PaceClickTargetOption`, `PacePendingClickTargetClarification`, `PaceClickTargetClarificationBuilder`) that reuses the same `.clarification` HUD shape so near-tied click candidates render as option chips and resolve to the chosen candidate's exact target. |
| `PacePushToTalkManager.swift` | ~1063 | Push-to-talk voice pipeline (previously `BuddyDictationManager`). Handles microphone capture via `AVAudioEngine`, provider-aware permission checks, keyboard/button dictation sessions, provider partial hypotheses, LocalAgreement stable partial publication, transcript finalization, shortcut parsing, contextual transcription phrases, and live audio-level reporting for waveform feedback. Also owns the lazy engine close (engine kept warm 10s after release so back-to-back PTT skips prepare+start) and the speculative fast-action trigger (stable partial matching a deterministic fast-action fires before PTT release; final-transcript dedup is punctuation-insensitive). |
| `PaceLatencyBudget.swift` | ~220 | Per-turn latency budget tracker. `startTurn`/`mark`/`finishTurn` capture every pipeline stage (PTT press → STT → intent → VLM → planner first-token/total → tool exec → TTS) and emit one structured `BUDGET=` log line per turn, with a rolling 50-turn p50/p90 window for the HUD and benchmark scripts. |
| `PaceSubagentCommandParser.swift` | ~189 | Deterministic parser for parallel-subagent voice commands ("research X, Y, and Z"). Trigger keyword must start the command (politeness prefixes stripped); pronoun-only fragments are rejected so conversational phrases never spawn agents. Returns subtasks + merge strategy for the coordinator. |
| `PaceSubagentCoordinator.swift` | ~377 | Runs a parsed subagent batch: spawns up to 4 concurrent headless planner turns via an `AsyncSemaphore`, tracks per-subagent state, merges results (concatenate / Apple-FM summarize / first-wins), retains at most 20 completed batches. |
| `PaceDictationFastPath.swift` | ~115 | Zero-latency dictation lane: "type …" / "dictate …" transcripts skip planner/VLM/TTS entirely — deterministic `PaceDictationPostProcessor` cleanup, optional Apple FM disfluency pass, then CGEvent typing into the focused field. "write" deliberately does not trigger (compose intents belong to the planner). |
| `PaceAmbientContextStore.swift` | ~309 | Always-on ambient context snapshot (frontmost app, AX window title + top-level role counts, clipboard change metadata, display count, time-of-day), polled every 3s on a main-runloop timer and injected into the planner system prompt as `<ambient_context>`, replacing a VLM round-trip for "what app am I in"-class context. |
| `PaceCompanionWorldModel.swift` | ~434 | Pure typed evidence layer for the default-off Always-On Companion Mode: validated non-identifying observations, coarse zones, provenance, bounded atomic JSON persistence, confidence-decayed derived hypotheses, correction supersession, and exact last-seen/change/presence queries. |
| `PaceCompanionModePolicy.swift` | ~326 | Default-off companion preferences, deterministic lifecycle state machine, intervention scoring, silence-first show/queue/ask/speak/discard policy, deduplication, cooldowns, and negative-feedback thresholds. |
| `PacePerceptionCoordinator.swift` | ~218 | Injected perception-source adapter boundary and event-driven actor coordinator. Enforces one expensive analysis per source, equivalent-burst coalescing, stale-candidate dropping, lifecycle-generation invalidation, and immediate task/source cancellation. |
| `PaceCompanionMemoryPolicy.swift` | ~499 | Deterministic episodic/semantic/spatial/routine promotion over typed companion evidence, confidence reinforcement/decay/contradiction, bounded episode compaction, time/entity/place queries, dedicated local-retrieval documents, uncertainty-aware last-seen/usual wording, and coordinated evidence/index/pending-candidate clear operations. Routine promotion requires three unique supporting observations by default. |
| `PaceCompanionPerceptionSources.swift` | ~361 | Dependency-injected, separately permissioned camera and ambient-voice adapters. Camera sampling is capped at 2 FPS and motion/object-gated into named zones; ambient audio uses local VAD/wake gating and bounded post-wake on-device STT sessions. Optional diarization emits session-only speaker labels. Also maps non-identifying person presence and user-taught objects into expiring typed observations without retaining raw frames/audio. |
| `PaceCompanionCameraCaptureClient.swift` | ~500 | Production observe-only AVFoundation camera client: permission/device handling, capped low-rate sampling, 32×24 luma motion estimation, Vision person rectangles, session-local non-identifying tracks, and region-scoped Vision feature-print matching for explicitly taught objects. Full frames never leave the capture callback or reach persistence. |
| `PaceTaughtObjectStore.swift` | ~170 | Atomic local store for explicitly taught object labels and secure Vision feature-print archives, plus the pure fail-closed distance threshold used by production camera matching. No source photo is persisted. |
| `PaceCompanionPrivacyAndResources.swift` | ~193 | Fail-closed companion privacy and resource invariants: loopback validation, privacy-pinned planner factory, sensitive-app deny list, pre-persistence redaction, bounded raw-data buffers, sampling/concurrency/battery/thermal/storage budgets, and observable accepted/dropped/model-call/raw-buffer/idle metrics. |
| `PaceExistingPerceptionAdapters.swift` | ~207 | Zero-duplicate-loop adapters over the existing ambient-context publisher and explicit Watch Mode event publisher, a bounded take-once ephemeral screen-frame store, fail-closed privacy-pinned local screen-client construction, and targeted post-change screen interpretation with sensitive-app denial/redaction. |
| `PaceCompanionWakeGate.swift` | ~620 | Production local pre-STT wake path using AVAudioEngine and direct Core ML inference over a bounded 2-second PCM ring. It expects `PaceWakeWordClassifier` labels `hey_pace` and `background`, requires consecutive high-confidence windows, releases the mic before bounded PTT handoff, and fails closed on permission/model/label/audio errors without invoking Speech.framework. |
| `Resources/PaceWakeWordClassifier.mlpackage` | 2.5 MB | Bundled raw-PCM Core ML wake model: normalized mono 16 kHz `[1, 32000]` input, `background`/`hey_pace` output, Apache speech-embedding backbone plus Pace MIT temporal head. See `docs/product/pace-wake-word-classifier.md`. |
| `PaceCompanionObservationPresenter.swift` | ~360 | Deterministic typed-observation-to-card/speech/clarification mapping with provenance, non-identifying person copy, deduplication, queueing, and independent output preferences. Delegates decisions to `PaceCompanionInterventionPolicy`. |
| `PaceCompanionObservationCardView.swift` | ~90 | Shared silent companion card rendered in the panel/chat surfaces with provenance, confidence, observation time, and explicit dismissal. |
| `PaceCompanionControlCenter.swift` | ~136 | Main-actor control/presentation model for default-off enable, independent default-off source/card/speech opt-ins, pause, retention, storage usage, source/all clearing, model readiness, runtime state, active sources, and last-observation time. |
| `PaceCompanionRuntime.swift` | ~600 | Default-off lifecycle composition wired into `CompanionManager.start()`/`stop()`. Starts only explicitly enabled sources, prepares the fail-closed pre-STT wake gate, derives structured observations into memory/retrieval, and routes optional card/speech presentations through intervention policy and live restraint. |
| `PaceCompanionSettingsTab.swift` | ~240 | Settings → Companion surface for enable/pause, camera/voice/screen/Mac-context consent, independent silent/spoken output gates, runtime/readiness status, retention/storage, and per-source/all clear controls. Every interactive control has explicit pointer hover behavior. |
| `BuddyTranscriptionProvider.swift` | ~110 | Protocol surface and provider factory for voice transcription backends. Sessions receive per-turn contextual phrase hints. Factory reads `TranscriptionProvider` Info.plist key, and when unset AUTO-PREFERS WhisperKit if the model is on disk (the install-time presence is the user's opt-in signal). Explicit `appleSpeech` / `apple` always wins over an installed WhisperKit; explicit `whisperKit` falls back to Apple Speech with a "WhisperKit fallback" display-name suffix if the model is missing. `isWhisperKitModelInstalledOnDisk()` is a cheap FileManager probe — the pipeline itself still loads lazily on first session. |
| `AppleSpeechTranscriptionProvider.swift` | ~152 | Default on-device transcription provider backed by Apple's Speech framework (`SFSpeechRecognizer` with `requiresOnDeviceRecognition=true`). Applies Pace's local contextual phrases through `contextualStrings`; also carries the explicit WhisperKit fallback display name when needed. |
| `WhisperKitTranscriptionProvider.swift` | ~260 | Production WhisperKit ASR backend (qualified 2026-06-08). Pushes audio buffers through `WhisperKitStreamingSession` — accumulates 16 kHz mono samples and re-transcribes the WHOLE buffer on a 1.2s cadence for live partials, runs one final pass on PTT release. ~9x realtime on ANE, ~1s warm load. `WhisperKitRuntimeCache` serialises pipeline construction so the 1.5 GB CoreML assets load exactly once per process; concurrent first-callers await the same load. `BuddyTranscriptionProviderFactory` auto-prefers this provider when the model file is on disk. |
| `PaceTranscriptionContextualPhraseBuilder.swift` | ~80 | Local vocabulary-hint builder for ASR. Combines the frontmost app name, Pace product/runtime terms, and `PaceToolRegistry` names/aliases so Apple Speech and future WhisperKit sessions get the same biasing input. |
| `PaceLocalAgreementStabilizer.swift` | ~66 | Provider-agnostic LocalAgreement-style partial transcript stabilizer. Emits only word prefixes that agree across consecutive hypotheses, never retracts stable text, and is wired into `PacePushToTalkManager` for current provider partials while remaining reusable by the real WhisperKit streaming bridge. |
| `GlobalPushToTalkShortcutMonitor.swift` | ~132 | System-wide push-to-talk monitor. Owns the listen-only `CGEvent` tap and publishes press/release transitions. |
| `GlobalChatShortcutMonitor.swift` | ~200 | Sibling of `GlobalPushToTalkShortcutMonitor` for the notch chat-input shortcut. Reads `NotchChatShortcut` from Info.plist (default `commandShiftP`, accepts `commandShiftK` / `commandShiftSpace` / `controlShiftP` / `controlShiftSpace`), runs a listen-only `CGEvent` tap, and emits `chatShortcutPressed` on every match. `CompanionManager` flips `isNotchChatInputFocused = true` and posts `.paceShowPanel` so the notch panel surfaces and focuses the TextField. |
| `LocalVLMClient.swift` | ~590 | Screen-analysis client abstraction plus LM Studio loopback-HTTP implementation and in-process runtime placeholder. `PaceScreenAnalysisClientFactory` reads `ScreenAnalysisProvider`, returns the HTTP client by default, and falls back to HTTP when `inProcess` / `coreML` / `mlx` is requested before the runtime bridge exists. The HTTP client sends one screenshot + structured prompt, parses `LocalVLMScreenAnalysis`, synthesizes a compact description from element labels/text when the model omits one, and falls back to regex JSON extraction when the model strays from strict JSON. |
| `PaceVisionOCRClient.swift` | ~240 | Apple Vision `VNRecognizeTextRequest` wrapper. Returns `[RecognizedTextBox]` in screenshot pixel space. `PaceScreenContextMerger.enrich` fuses VLM elements with OCR text by bbox overlap (>50%), appending up to 30 orphan OCR boxes as `static_text` elements. Lets the 2B VLM stay fast while the OCR layer guarantees verbatim text fidelity. Wires `PaceVisionOCRLanguageResolver` at init so Vision biases by `Locale.preferredLanguages` (with `automaticallyDetectsLanguage=true` on macOS 13+), and pipes both per-box text and the cross-line merger join through `PaceOCRPostProcessor` for NFC normalisation + soft-wrap dehyphenation. |
| `PaceVisionOCRLanguageResolver.swift` | ~80 | Pure helper that intersects `Locale.preferredLanguages` with `VNRecognizeTextRequest.supportedRecognitionLanguages()` and falls back to `["en-US"]` when empty. Stolen from auge — gives non-English UIs first-class OCR fidelity instead of mis-recognising umlauts/CJK as garbled ASCII. Vision-free + actor-free so it's trivially unit-testable. |
| `PaceOCRPostProcessor.swift` | ~100 | Pure deterministic OCR cleanup. `joinSingleLineTextsDehyphenated([...])` joins per-line OCR boxes with single-space separator, dropping soft-wrap hyphens at line breaks (current line ends with `-` after a lowercase letter AND next line starts lowercase). `normalizeUnicodeForOCRComparison(_:)` precomposes diacritics to NFC so the click-candidate scorer's string match compares like with like. Stolen from auge's `--clean` flag idea, but pure-deterministic so it doesn't pay the FM round-trip on every screenshot. |
| `PaceOCRDataDetector.swift` | ~160 | Foundation `NSDataDetector` over OCR'd screen text — extracts typed entities (phone, email, URL, date, postal address) deterministically with zero LLM cost. Returned via the value type `PaceDetectedEntity`. `renderEntitiesForPlannerPrompt(_:)` emits a labelled `PHONE: …\nEMAIL: …` block; called from `PaceScreenContextService.formatScreenAnalysisForPrompt` so every per-screen prompt section carries a deduped, capped-at-10 typed-entity list. Stolen from macOS's built-in. |
| `PaceAppIntents.swift` | ~240 | App Intents surface for Pace. Four intents — `PaceConversationIntent`, `PaceStartListeningIntent`, `PaceShowPanelIntent`, `PaceSetWatchModeIntent` — mirror the `pace://` deeplink command set 1:1 and route through the new public `CompanionAppDelegate.executePaceExternalCommand(_:)` shim. `PaceAppShortcuts: AppShortcutsProvider` registers voice phrases ("Ask Pace", "Show Pace", "Toggle watch mode in Pace") for Siri / Spotlight / Shortcuts discoverability. Free-form String params can't be interpolated into Siri phrases (metadata processor limitation), so the conversation intent uses Siri's own parameter-request flow. |
| `PaceAppleNLEmbeddingClient.swift` | ~95 | `PaceTextEmbedding` conformer backed by Apple's NaturalLanguage `NLEmbedding.sentenceEmbedding(for:)`. 300-dim, on-device, ships with macOS 12+, no Apple Intelligence dependency. Lazy-loaded per language (caches `[NLLanguage: NLEmbedding]` behind an `NSLock` since the cache map is not thread-safe). Lower quality than nomic via LM Studio, but free + always available so semantic recall keeps working on a clean Mac. |
| `PaceChainedTextEmbeddingClient.swift` | ~90 | Tries a primary `PaceTextEmbedding` first; on throw, wrong cardinality, or all-zero vectors falls back to a secondary. `makePaceDefault()` wires LM Studio primary + Apple NL fallback. Treats all-zero primary output as a soft-failure too, because a primary that "succeeds" by returning zeros would silently break recall without flipping us to the fallback. Used at the two memory-embedding sites in `CompanionManager`. |
| `PaceSpotlightMemoryIndexer.swift` | ~125 | One-way mirror from `PaceMemoryStore` into the system `CSSearchableIndex` (CoreSpotlight) under the domain identifier `com.pace.app.memory`. Hooked into `persistUnifiedMemory()` so every save dual-writes the JSON + the Spotlight mirror. Tombstoned entries dropped from the mirror but retained on disk for the 30-day window. Title bounded at 60 chars; keywords = `["pace", kind.rawValue] + topicTags`. Best-effort: framework failures log and skip rather than blocking a turn. Pure mapping helpers (`buildSearchableItem(forEntry:domainIdentifier:)`) testable in isolation. |
| `PaceFocusModeMonitor.swift` | ~100 | `@MainActor ObservableObject` wrapping `Intents.INFocusStatusCenter`. `start()` calls `requestAuthorization` exactly once and observes `INFocusStatusCenterFocusStatusDidChangeNotification`. Publishes `isCurrentlyInUserFocus`. Denied-permission users silently report "not focused" so missing permission can never lock Pace out of talking. Fed into `PaceRestraintContext.isInUserFocusMode`, which `PaceRestraintGate.decide` treats as queue-until-idle. Requires `NSFocusStatusUsageDescription` in Info.plist. |
| `PaceMessageShareService.swift` | ~50 | `@MainActor` wrapper over `NSSharingServicePicker(items:).show(...)` for sharing a single message body. Used by the contextMenu in `PaceConversationsView.messageRow(_:)` so right-click on any chat message → Copy / Share… menu. Whitespace-only input short-circuits so the share sheet never opens with nothing to send. macOS enumerates every install share destination (Messages, Mail, Notes, AirDrop, third-party share extensions) — no per-target integrations to maintain. |
| `PaceTranslationAvailabilityChecker.swift` | ~35 | Tiny wrapper over `Translation.LanguageAvailability` (macOS 15+). Returns `.installed` / `.supportedButNotDownloaded` / `.unsupportedLanguagePair` / `.translationFrameworkUnavailable` so callers know whether on-device translation can run offline for a (sourceLanguage, targetLanguage) pair right now. The full `TranslationSession.translate` API requires a SwiftUI `.translationTask` view modifier — a session is created by the modifier and handed to a closure — so headless translation needs a hidden host view + actor channel (deferred). Availability probe ships standalone because it's a clean async API. `resolveUserTargetLanguageCode(preferredLanguagesFromLocale:)` normalises BCP-47 tags ("en-US" → "en", "zh-Hans-CN" → "zh") for the probe. |
| `PaceThermalStateAdvisor.swift` | ~155 | `@MainActor ObservableObject` subscribing to `ProcessInfo.thermalStateDidChangeNotification`. Maps the four thermal levels to a typed `PaceThermalRecommendation` (`.unrestricted` / `.dampenSpeculativeRace` / `.dampenBackgroundLoops` / `.suspendBackground`) and exposes four pure per-surface gates (`shouldRunSpeculativeRace`, `shouldRunProactiveSurfaces`, `shouldRunWatchModeAtFullCadence`, `shouldRunScreenContextPrewarm`). Currently gates the speculative-race site at `CompanionManager` line ~5462; remaining surfaces (watch mode, proactive scheduler, prewarm) still need to opt in. |
| `PaceIDEContextDetector.swift` | ~250 | Recognises eight IDE bundle identifiers (Xcode, VS Code, Cursor, Sublime Text, JetBrains family, Zed, Nova, TextMate) and parses their window titles into a typed `PaceIDEContext` (IDE name, focused file name, optional absolute path). Window title comes from a `CGWindowListCopyWindowInfo` reader at `PaceScreenContextService.frontmostWindowTitleForFrontmostApp()` — no AX permission needed. When a recognised IDE is frontmost, `buildPromptFromEnrichedAnalyses` prepends `Editor context:\nide: …\nfocused file: …\n` to the planner prompt so "summarize this function" / "what does this file do" turns get filename context for free. Pure parsing per IDE; not a real LSP (semantic file tree + type info is a separate future product surface). |
| `PaceMemoryEntryEnricher.swift` | ~135 | Pure write-time enrichment for `PaceMemoryEntry.structured`. Combines NLTagger (`.nameType`, `joinNames`) for persons/places/organizations with `PaceOCRDataDetector` for phones/emails/urls/dates. Result keys are stable (`persons` / `places` / `organizations` / `phones` / `emails` / `urls` / `dates`); values are sorted + comma-joined so two enrichments of the same text produce byte-identical JSON. Wired into `CompanionManager.recordConversationTurn` — every turn now carries entity metadata at the first save. Returns nil for entries with no extractable entities (saves a few JSON bytes per entry vs writing an empty dict). |
| `PaceAudioFileTranscriber.swift` | ~380 | On-device audio file transcription. Decodes any AVAudioFile-supported format → mono 16 kHz Float32 via `AVAudioConverter`, then runs WhisperKit (auto-detect-language) when the model is installed, falling back to Apple `SFSpeechURLRecognitionRequest` with `requiresOnDeviceRecognition=true`. Throws typed `PaceAudioFileTranscriberError` naming which backends were tried. Surfaced through `PaceTranscribeAudioFileIntent` (Shortcuts action, Spotlight phrase "Transcribe audio using Pace", drag-drop via Quick Actions) — full tool-registry wiring deferred since voice-first "transcribe /Users/me/audio.m4a" turns are rare. |
| `PaceLazyEmbeddingScheduler.swift` | ~80 | `@MainActor` extraction of the off-hot-path embedding scheduler that used to live inline in `CompanionManager.scheduleLazyEmbedding`. Explicit init dependencies (`memoryIndex`, `embeddingClientFactory`, `onEmbeddingsPersisted` callback) — same shape as the Wave 7b `PaceScreenContextService` split. Cardinality-mismatched / throwing-client / empty-input paths all leave the index nil-embedded rather than write partial state; semantic recall falls through to BM25 in that case. The CompanionManager method is now a one-line delegate. |
| `BuddyPlannerClient.swift` | ~500 | Protocol the active planner conforms to (`generateResponseStreaming`, `displayName`, `supportsImageInput`) + factory that returns the configured main planner and the fast text-only answer planner. The factory dispatches on `PacePlannerTier` first (Local / CLI bridge / CLI direct / Direct API / Apple FM), with `.local` continuing to honor the legacy cloud-bridge UserDefaults for pre-tier-picker upgraders. **Bundled MLX trumps the local LM Studio path** when `PaceBundledModelsSettings.isUsingMLXInProcessPlanner()` is true AND the SPM runtime is linked — power users keep LM Studio's larger model by leaving the toggle off. `LocalPlannerClient` handles the main LM Studio path; `AppleFoundationModelsPlannerClient` handles short pure-knowledge turns and the appleFoundationModels tier; `HybridPlannerClient` wraps local + bridge and routes per `routingHintForNextCall`; `CloudBridgePlannerClient` is the CLI-bridge tier; `PaceLocalCLIPlannerClient` is the `.cliDirect` direct-spawn tier, gated by `makeCLIDirectPlannerOrLocalFallback` on the pure `cliDirectDispatchDecision(...)` (consent + soak → spawn CLI, else fall back to local with a logged reason); `DirectAPIPlannerClient` is the BYO-key Direct-API tier. Direct API with no stored key falls back to local so Pace stays usable, surfaced as a yellow "no key set" status row in Settings. |
| `PaceMLXPlannerClient.swift` | ~370 | In-process MLX planner. Release-target default: `mlx-community/Qwen3-4B-Instruct-2507-bf16` (override-able via `PaceBundledModelsSettings.plannerModelIdentifier()` → Info.plist → compile-time fallback). bf16 trades ~8 GB download + RAM for materially better accuracy than the 4-bit variant; the **plan-then-execute scaffold** (`CompanionSystemPrompt.wrapWithPlanThenExecuteScaffoldForBundledMLX`) is auto-applied to every system prompt this client sees, forcing the 4B model into explicit intent → plan → action structuring before responding. Compiles with OR without the SPM module via `#if canImport(MLXLLM)`. Model container is shared per-process behind an `NSLock`. Prefetch helper surfaces `Progress` so the Settings UI shows real download %. |
| `PaceMLXEmbeddingClient.swift` | ~180 | In-process MLX embedder — runs `mlx-community/nomic-embed-text-v1.5` (default) via `mlx-swift-examples` MLXEmbedders. Conforms to `PaceTextEmbedding` so it slots into `PaceChainedTextEmbeddingClient.makePaceDefault()` without changing the chain shape. Same `#if canImport(MLXEmbedders)` guard pattern as the planner client; `isRuntimeAvailable` flips true once the SPM dep is linked. Activated when `PaceBundledModelsSettings.isUsingMLXInProcessEmbedder()` is true; falls through to LM Studio → Apple NL via the existing chained-fallback. |
| `PaceBundledModelsSettings.swift` | ~180 | UserDefaults-backed toggle + model-identifier configuration for the in-process MLX runtime. `isUsingMLXInProcessPlanner()` / `isUsingMLXInProcessEmbedder()` short-circuit to false when the respective client's `isRuntimeAvailable == false`, so a missing SPM dependency can't accidentally route turns to a runtime that isn't linked. `runtimeStatusSummary(...)` renders the Settings → Models status row. Empty/whitespace model identifiers are refused so a slip in Settings can't brick the next inference call. **Default state is OFF** — existing users see byte-identical behaviour until they opt in. |
| `PaceBundledModelsSettingsTab.swift` | ~940 | Settings → Models tab. Surfaces the runtime-status row (green check vs yellow warning), four toggles (in-process MLX planner / embedder / VLM / Qwen3 TTS), per-model HuggingFace identifier text fields with Apply buttons, a "Download now" prefetch button with real NSProgress %, and a quality-caveat block explaining the 3-4 point gap vs qwen3-30b-a3b on the FM-fixture set. Toggles disable when the respective client's `isRuntimeAvailable == false`. Also hosts the RAM-aware `memoryBudgetSection` (backed by `PaceModelMemoryBudget`) with a planner-brain picker that routes through `CompanionManager.selectPlannerTierWithConsent` and a VLM size picker whose fit math updates live. |
| `PaceModelMemoryBudget.swift` | ~490 | Pure, testable RAM-budget model — the one source of truth for each on-device model role's estimated resident-RAM cost (planner / vision / STT / TTS / embedder variants). `evaluate(configuration:usableBudgetGB:)` returns the per-model breakdown + fits/over-budget verdict; `largestVLMThatFits(...)` picks the biggest vision tier a budget affords; `plannerMemoryVariant(forTier:...)` maps a `PacePlannerTier` (cloud/Apple-FM → ≈0 local RAM) to its memory variant. No UI/persistence/network — only reads `ProcessInfo.physicalMemory`. |
| `PaceMLXScreenAnalysisClient.swift` | ~250 | Phase C: in-process VLM via `mlx-swift-examples` MLXVLM. Conforms to `PaceScreenAnalysisClient` so the existing `PaceScreenContextService` consumes it without changes. Default model is `mlx-community/Qwen3-VL-4B-Instruct-4bit` (MLXVLM 2.29.1 ships full Qwen3-VL support natively, no port from Qwen2-VL needed). Image bytes flow ScreenCaptureKit → NSImage → CGImage → CIImage into `UserInput.Image`. Prompt + system instruction are byte-identical to the LM Studio HTTP client so the same `LocalVLMScreenAnalysis` JSON parser handles the output. Regex-fallback extraction for "model emitted stray prose preamble". Activated when `PaceBundledModelsSettings.isUsingMLXInProcessVLM()` returns true; trumps the LM Studio HTTP path. Same model → same quality; the win is dropping the HTTP loopback and the `max-loaded-models=2` brittleness. |
| `PaceQwen3TTSClient.swift` | ~200 | Phase D: in-process TTS via WhisperKit's `TTSKit` module (default Qwen3 TTS). Conforms to `BuddyTTSClient` with full barge-in / `manualStop` / `naturalCompletion` stop-reason propagation matching `LocalTTSClient`'s contract. `isCurrentlySpeakingOrPending` flips true synchronously on `speakText` so the CompanionManager poll loop sees us as speaking from t=0. Activated when `PaceBundledModelsSettings.isUsingQwen3TTSInProcess()` returns true; trumps `LocalServerTTSClient` (Kokoro sidecar) and `LocalTTSClient` (Apple AVSpeechSynthesizer). No Kokoro Swift port required — WhisperKit ships a finished TTSKit product (the Argmax team did the StyleTTS2-equivalent work) so Phase D collapsed from a multi-week port to a single integration commit. |
| `LocalPlannerClient.swift` | ~470 | Text-only OpenAI-compatible chat-completions client for a local reasoner. SSE streaming, parses `choices[0].delta.content`. Discards images (logs a notice) — relies on upstream VLM element map being prepended to `userPrompt`. Defensive `stripThinkingBlocks` helper removes `<think>…</think>` from streamed content for thinking-mode models. Retries empty HTTP-200 user-facing streams with prompt caching disabled, then falls back to a non-streaming local completion before surfacing failure. Endpoint construction is guarded by `PaceLocalEndpointGuard` so bad plist values cannot route prompts to remote hosts. |
| `CloudBridgePlannerClient.swift` | ~220 | Opt-in `BuddyPlannerClient` conformer that routes turns through the sibling local-ai Node bridge (`http://localhost:3456`), which spawns the user's already-authenticated Claude Code, Codex, or Gemini CLI. Parses the bridge's `data: {"text":"..."}` / `data: [DONE]` SSE shape (different from OpenAI's format). Discards images; surfaces upstream errors as `PaceCloudBridgeError.upstream`. This is the ONLY intentional break of the no-cloud-LLM principle — consent-gated and default-off. |
| `PaceCloudBridgeConsent.swift` | ~290 | Pure state module for the cloud-bridge feature. Defines `PaceCloudBridgeMode` (off/hybrid/alwaysBridge), `PaceCloudBridgeUpstream` (claude/codex/gemini), `PaceCloudBridgeConfiguration`, and `PaceCloudBridgeConsent` with UserDefaults persistence under the `pace.cloudBridge.` prefix. Enforces the 24-hour soak period before `alwaysBridge` is available via `canEnableAlwaysBridge(now:)`. **Transport-aware for `.cliDirect`**: a SEPARATE direct-spawn consent record (`hasAcceptedDirectSpawnConsent()` / `acceptDirectSpawnConsent()`) + its own soak clock (`markDirectSpawnFirstUsedIfUnset` / `canRunDirectSpawnTurn(now:)`) so accepting the Node-bridge path does NOT auto-grant the direct-spawn path (different data path). `revokeConsentAndResetAllBridgeState()` clears both. |
| `PaceLocalCLIPlannerClient.swift` | ~590 | `BuddyPlannerClient` conformer that direct-spawns the `claude`/`codex` CLI (`PaceLocalCLIUpstream`) per turn — no Node bridge, only the CLI on PATH. Powers both the `.research` lane override and the `.cliDirect` general tier. Ports CodeVetter's stream-json parsing (`PaceLocalCLIStreamJSONParser`), `claude --resume <session_id>` prefix-cache reuse, and `--bare` when `ANTHROPIC_API_KEY` is set. Records a `PaceAPIAuditLog` `planner.cliDirect` entry (target = upstream executable, char-counts only) on every turn — success and failure — so off-device egress is always logged. Preflight helpers `isUpstreamBinaryOnPath(_:)` / `missingBinaryPreflightMessage(for:)` surface a plain-language "is `codex` on PATH?" hint before a turn instead of a silent hang. |
| `PaceLocalEndpointGuard.swift` | ~220 | Shared fail-closed validator for local model HTTP roots. Allows only `http`/`https` loopback hosts (`localhost`, `127.0.0.0/8`, `::1`) and falls back to the default LM Studio localhost URL when planner/VLM plist configuration points at a remote or LAN endpoint. Separate `validatedCloudBridgeURL(from:)` function for the bridge endpoint so future guard tightening on the planner path doesn't accidentally affect the bridge. Separate `validatedDirectAPIURL(from:)` function for the BYO-key Direct API entry point — allows https to any host (consented cloud egress), allows http only for loopback hosts (so local OpenAI-compatible proxies still work for testing), rejects plaintext to non-loopback hosts so a pasted key can never leave Pace in the clear. |
| `DirectAPIPlannerClient.swift` | ~350 | `BuddyPlannerClient` conformer for BYO-key cloud endpoints (Anthropic / OpenAI / OpenRouter / Custom). OpenAI-compatible SSE streaming over a per-call URLSession; loads the API key from `PaceKeychainStore` at request time only (never held on the client). Sets `x-api-key` + `anthropic-version` for Anthropic, `Authorization: Bearer` for the others. Same `<think>…</think>` stripper as `LocalPlannerClient`. Surfaces 401 as `.invalidAPIKey`, every other non-2xx as `.httpError(statusCode, bodyExcerpt)` so users see the verbatim upstream error. Writes one `PaceAPIAuditLog` line per call: provider + model + char-counts + turn-id + outcome — NEVER message content. |
| `PacePlannerTierStore.swift` | ~310 | Pure state module for the planner tier picker. Defines `PacePlannerTier` (local/cliBridge/cliDirect/directAPI/appleFoundationModels — additive; legacy values decode unchanged), `PaceDirectAPIProvider` (anthropic/openai/openrouter/custom) with built-in endpoint URLs + default model identifiers, the `.cliDirect` upstream (`cliDirectUpstream: PaceLocalCLIUpstream`, key `pace.planner.tier.cliDirect.upstream`, default `.codex`), `PacePlannerTierConfiguration` snapshot, and UserDefaults persistence under `pace.planner.tier.` prefix. Does NOT touch Keychain — keys live exclusively in `PaceKeychainStore`. **First-launch tier resolution**: `loadConfiguration()` calls `hasAnyPlannerTierUserDefaultsState()` and only applies the new Apple-FM-first default to clean installs; existing users keep their pinned tier byte-identically. Apple Intelligence availability resolves via `SystemLanguageModel.default.availability` (cached per process) and selects between `.appleFoundationModels` (when available — zero external install needed) and `.local` (fall back to LM Studio). The pure helper `defaultTierForFirstLaunch(appleIntelligenceAvailable:)` is unit-testable. |
| `PaceKeychainStore.swift` | ~145 | Minimal `kSecClassGenericPassword` wrapper. Service `com.pace.app.plannerAPIKeys`, account names `directAPI.<provider>.apiKey`. `kSecAttrSynchronizable=false` (never syncs via iCloud), `kSecAttrAccessibleWhenUnlocked`. Never logs key material. Only legal entry point for API keys anywhere in the codebase — nothing else reads or writes API key bytes. |
| `BuddyTTSClient.swift` | ~80 | Protocol the active TTS conforms to (`speakText`, `isPlaying`, `stopPlayback`) + trivial factory returning `LocalTTSClient`. Protocol kept so a future on-device runtime (Kokoro/Piper-MLX) can plug in without touching `CompanionManager`. |
| `LocalServerTTSClient.swift` | ~400 | High-quality TTS via the loopback Kokoro sidecar (`/v1/audio/speech`). Per-sentence synthesis queue with strictly ordered playback, prefetch of the next sentence during playback, per-utterance Apple-voice fallback, and a 30 s outage memo. `LocalServerTTSConfiguration` is the pure, testable plist view. |
| `LocalTTSClient.swift` | ~226 | On-device TTS via `AVSpeechSynthesizer`. The sole `BuddyTTSClient` conformer. Uses `PaceTTSVoiceResolver`, applies Info.plist voice/prosody tuning for rate/pitch/volume/delays, and maintains its own `isCurrentlySpeakingOrPending` flag so the CompanionManager poll-loop sees playback as active from the moment `speak()` is invoked. Hops to MainActor inside the delegate callback. |
| `PaceTTSVoiceResolver.swift` | ~91 | Shared TTS voice selection and panel summary. Premium/Enhanced Apple voices win even when a compact fallback voice is configured, so installing a better voice automatically upgrades playback on restart. |
| `StreamingSentenceTTSPipeline.swift` | ~510 | Consumes planner streamed text and dispatches complete sentences to TTS as they arrive, instead of waiting for the full response. Cuts perceived time-to-first-spoken-word from ~3s to ~500ms. Strips `<think>` blocks + `<tool_calls>` + action tags + `[POINT]` before sentence segmentation. Owns `markIntentCommitted()` + TTFSW logging — called from `CompanionManager` at PTT-release. Now also publishes the speakable-safe prefix via `@Published inFlightStreamedText` so the in-window chat surface can render "Pace is typing…" without re-routing the planner stream, and exposes `setMutedForCurrentTurn(_:)` so chat-mode mute drops audio for one turn without affecting the live text stream. Also owns `prepareForSupersedingStreamWithinTurn()` (speculative-race supersede reset — stops playback + clears the dispatch cursor so a different winning stream replays cleanly without re-logging TTFSW) and the `firstSpokenWordCharacterCount` probe the race reads for its supersede window. |
| `PaceSpeculativePlannerRace.swift` | ~390 | Wave 4 speed lever, wired into the agent-loop first step (see Plan-act-observe). `raceSpeculative(...)` runs the Apple FM lite planner against the full VLM-fed planner concurrently and returns `PaceSpeculativeRaceResult` (outcome + BOTH planners' final text, so the caller always parses actions from the full text). `SpeculativeRaceCoordinator` routes the winner's accumulated chunks through `onToken`, handles the 500 ms / 60-char supersede, and streams every chunk once a path is the established winner. `PaceSpeculativeRaceWinnerBox` (@MainActor) backs the winner-flip + stale-lite-dispatch guard in `CompanionManager.performFirstStepSpeculativePlannerRace`. Pure coordinator logic unit-tested in `PaceSpeculativePlannerRaceTests`. |
| `PaceChatSession.swift` | ~300 | Backing store for the in-window chat surface. `@MainActor` `ObservableObject` that owns the ordered `[PaceChatMessage]` transcript SwiftUI renders, plus the per-session ephemeral `isChatTTSMuted` flag (never persisted). Persistence is the existing `paceHistory` retrieval index — `PaceLocalChatHistoryReader` re-uses the same JSON the legacy static reader used. `submitUserMessage` forwards through a tiny `PaceChatTranscriptSubmitting` protocol (production conformer routes into `CompanionManager.submitChatTranscriptFromChatSession`); `appendCompletedTurn` is called from `recordConversationTurn` so voice turns and chat turns share one transcript. |
| `PaceConversationsView.swift` | ~290 | Chat surface for the Conversations tab in `PaceMainWindow`. Restructured from the prior read-only list into a transcript + sticky text input + Send button; Enter submits, Shift+Enter inserts a newline. Auto-scrolls to the bottom on new messages and on every `inFlightStreamedText` tick so the streaming "Pace typing…" row stays anchored. Preserves the search field as an in-line filter via `PaceChatSession.filteredMessages(matching:)`. Header includes a per-session TTS mute toggle. Notch panel stays voice-first; THIS surface is the text-fallback PRD deliverable. |
| `PaceSkillsView.swift` | ~465 | "Skills" sidebar tab in `PaceMainWindow`. Top section is **"Your skills"** — the user-taught `.skill.md` skills from `PaceSkillLoader.listUserSkills()` (name, step count, trigger, delete button) plus an inline **"Teach a skill"** form (name + one-step-per-line `TextEditor` + optional trigger → `PaceSkillLoader.skillFromForm` → `save`), and a read-only "Built-in skills" subsection for bundled skills. Below that, the built-in tool catalog auto-generated from `PaceToolRegistry.localTools` (canonical name, description, `exampleUtterance`, risk badge, copy button) + `PaceMCPServerRegistry.loadConfiguredServers()`. Searchable across all three. Drift-proof: startup validation in `PaceToolRegistry` rejects any tool definition with an empty `exampleUtterance`. See PRD: [`docs/product/prds/teachable-skills.md`](../product/prds/teachable-skills.md). |
| `PaceSkillLoader.swift` | ~640 | Loads, parses, **serializes, and writes** `.skill.md` skills (Claude-Code-compatible NL-step format) from `Resources/skills/` (bundled) and `~/Library/Application Support/Pace/skills/` (user). Read side: `parse`, `loadAllSkills`, `toPlannerPrompt` (a step with a non-nil `toolCall` renders `instruction (use tool: <json>)` in the registry's tool-call dialect; steps without one render byte-identically to before). Run-time enforcement: `preflightRequiredPreferences(for:memoryStore:)` — deterministic, no-LLM gate mirroring `PaceRecipeLibrary`'s `requiredPreferences` check through the SAME `PaceLocalMemoryStoreReadable` seam, called by the `.run` case before dispatch. Teach side (teachable-skills PRD): `serialize` (inverse of `parse`, round-trips), atomic `save` / `deleteUserSkill` / `listUserSkills` (same temp-file+rename pattern as `PaceFlowStore`), `skillStructuringSystemPrompt` + `skillFromStructuredJSON` (local-planner NL→skill), `structureSkillDeterministically` (no-model fallback splitter), and `skillFromForm` (Settings typed authoring). Slug reuses `PaceFlowStore.slug(for:)`. Foundation-only + unit-testable — the one model call lives in `CompanionManager.handleTeachSkillCommand`. |
| `PaceSkillRunJournal.swift` | ~200 | Local-only, fire-and-forget success-rate telemetry for taught-skill runs. Append-only JSONL at `~/Library/Application Support/Pace/skill-runs.jsonl`, mirroring `PaceAPIAuditLog`'s pattern (utility-queue append, 5 MB rotation, malformed-line-tolerant read). One `{at, runId, skillSlug, phase, stepsPlanned, failureReason?}` line per lifecycle point: `recordStarted` (returns the `runId`) at dispatch, `recordCompleted`/`recordFailed` at turn end — wired from the `.run` case + agent-loop success/catch paths in `CompanionManager+AgentLoop`. Chosen over a `PaceRetrievalSource` journal because this is telemetry, not recall (no enum/store/rehydrate machinery). No UI yet — telemetry first. |
| `PaceAutomationCommandParser.swift` | ~236 | Deterministic pre-planner voice parsers (no model, no screen) for cron scheduling, background agents, meeting mode, and skills. `PaceSkillCommandParser` handles list/run/install plus **`.create(rawDescription:)`** — matches "teach/learn/create a skill …" (anchored, case-insensitive) and captures the rest as the raw description, routed to `CompanionManager.handleTeachSkillCommand`. Checked before list/install/run so a create utterance isn't swallowed. |
| `PaceStarterPromptCatalog.swift` | ~225 | Pure state + content module for the first-run "Try these" card pinned to the top of the notch panel. Defines `PaceStarterPrompt`, the deterministic 6-prompt `PaceStarterPromptCatalog.all` list (calendar / timer / open Safari / what's on my screen / remember preferred browser / what did I do today), and `PaceStarterPromptStore` — UserDefaults-backed first-seen + tried-set + dismissed-at state under the `pace.firstRun.` prefix. `isVisible(now:)` is the single visibility decision: hide after explicit dismissal, hide after 4-of-6 tried, hide after 24h from first seen. `resetVisibility()` brings the card back without wiping the tried set. No telemetry, no LLM call — fully local state. |
| `PaceTelemetryLog.swift` | ~52 | Single `os.Logger` (subsystem `com.pace.app`, category `metrics`) for performance metrics. Emits `TTFSW=NNNms`, `TTFT=NNNms`, and privacy-safe `RAG=NNNms` count-only retrieval metrics to the macOS unified log alongside the existing `print(…)` calls, so `scripts/benchmark_ttfsw.sh` can aggregate per-turn latency without scraping the Xcode console. |
| `PaceActionApproval.swift` | ~220 | Pure approval-gate helper for action execution. Applies the destructive-only approval policy, decides when routine local actions can suppress initial spoken feedback from parsed plans or planner text, builds popup copy from the risk-labelled action summary, and makes allow-once vs cancel behavior unit-testable without posting real Mac actions. |
| `PaceRuntimeSmokeTestHooks.swift` | ~124 | Disabled-by-default DistributedNotificationCenter hooks for app-level smoke tests. Activated only by `PACE_ENABLE_SMOKE_HOOKS=1`; verifies panel show/hide, cursor annotation state, synthetic clarification show/resolve state, and real approval-popup cancellation without fragile coordinate clicks. |
| `PaceToolRegistry.swift` | ~890 | Typed local tool catalog. Defines canonical names, aliases, schema examples, descriptions, risk levels, execution summaries, observation summaries, planner prompt generation, and startup validation. Validation checks enum coverage, duplicate names/aliases, required metadata, valid JSON schema examples, canonical `tool` fields, drift against the bundled v10 registry artifact, and presence/shape of the bundled v10 planner response schema artifact. Notes supports create/append/search actions through the same registry entry. MCP actions use a generic external-tool risk label because their exact semantics live in the configured server. |
| `Resources/v10-actions/registry.json` | ~29 | Bundled v10 action registry artifact. Mirrors `PaceToolRegistry` canonical kinds, tool names, aliases, risk labels, and schema examples so startup validation can catch prompt/parser/executor drift before the app handles a user action. |
| `Resources/v10-actions/pace-fm-response-v10.schema.json` | ~94 | Bundled v10 planner response schema artifact. Documents the typed `spokenText`/`intent`/`payload` envelope, single-action `{name,args}` calls, multi-call payloads, and dictate/edit fields; startup validation checks it is present and structurally recognizable. |
| `PaceMailDraftStreaming.swift` | ~283 | Incremental v10 Mail.draft detector for incomplete nested planner JSON plus action-plan helper for removing the already-streamed compose action. Lets the manager stream body text into Mail while the planner is still generating when action approval is off, then finalize without creating a duplicate draft. |
| `PaceStreamingPlannerFieldDetector.swift` | ~200 | Incremental v10 detector for dictate/edit/`AX.setValue` text fields during planner streaming (v0.3.14). |
| `PaceRemoteModelManifest.swift` | ~90 | Optional remote JSON manifest cache (`RemoteModelManifestURL`) that overrides bundled MLX model identifiers between Sparkle releases. |
| `PaceDictationPostProcessor.swift` | ~115 | Rule-backed local cleanup scaffold for v10 dictate intents. Handles spoken punctuation, basic prose capitalization, and first-pass code-mode function-call cleanup before typed dictation reaches the executor. |
| `PaceVoiceEditProcessor.swift` | ~192 | Rule-backed selected-text edit scaffold. Parses common voice-edit commands and transforms selected text for shorten, make direct, grammar cleanup, replace X with Y, delete last sentence, and bullets before the executor writes through AX. |
| `PaceActionExecutor.swift` | ~770 | Class declaration, stored properties, init/callbacks, and top-level helper types (`PaceActionTagParser`, `PaceFastActionCommandParser`, grouped plans, click-candidate scoring/ambiguity) after Phase B decomposition. |
| `PaceActionExecutor+EntryPoint.swift` | ~225 | `executeActionPlan`, single-action dispatch, streaming Mail draft entry. |
| `PaceActionExecutor+Mouse.swift` | ~435 | Clicks, clipboard, window snap, scroll. |
| `PaceActionExecutor+SystemToolsAppsMedia.swift` | ~370 | Open app/URL, timers, flows, download, Music, volume, brightness. |
| `PaceActionExecutor+SystemToolsCalendarReminders.swift` | ~185 | Calendar reads/event creation and Reminders. |
| `PaceActionExecutor+SystemToolsNotesMail.swift` | ~800 | Finder, Notes, Mail compose/streaming, Contacts recipient resolution. |
| `PaceActionExecutor+SystemToolsIntegrations.swift` | ~225 | Things, Shortcuts, Messages, AppleScript helpers, auxiliary key events. |
| `PaceActionExecutor+Keyboard.swift` | ~390 | Type, edit, undo, key press, MCP tool calls. |
| `PaceActionExecutorCoordinateConversion.swift` | ~65 | Screenshot-pixel → CG-global coordinate conversion (Wave 6a precedent). |
| `PaceActionResultCenter.swift` | ~94 | UI-friendly action run records for planned/completed/failed/denied/skipped local tool runs. CompanionManager stores the recent list and CompanionPanelView renders the latest entries. |
| `PaceToolCallDebugLog.swift` | ~190 | Plain DTO (`PaceToolCallDebugRecord`) for the Settings → Debug "Tool calls" view. Captures, per turn: transcript, routing lane (fast path / text-only / planner), which planner produced the audio (incl. speculative-race lite-won-with-no-screen), screen element count, the planner's RAW output (pre-strip), parsed tool calls, and dispatch outcome — so a turn that *spoke* but *did nothing* becomes legible. CompanionManager owns the capped list (`recentToolCallDebugRecords`, newest first, ≤25, session-only); captures are written at the fast-path, text-only, and agent-loop-post-execution sites. |
| `PaceDebugSettingsTab.swift` | ~210 | Settings → Debug tab view. Renders `recentToolCallDebugRecords` as per-turn cards (lane badge, routing, planner path, element count, what-you-heard-vs-screen, raw planner output, parsed tool calls, dispatch). Read-only diagnostics; a Clear button calls `CompanionManager.clearToolCallDebugRecords()`. |
| `PaceToolPreflight.swift` | ~182 | Pure preflight checks for local tool plans. Reports disabled actions, missing Accessibility/Calendar/Reminders permissions, Automation prompt warnings, and missing configured MCP server names before approval/execution. |
| `PaceMCPClient.swift` | ~540 | Minimal stdio MCP bridge. Loads `mcpServers`/`servers` JSON config from the user's home directory, launches a server process, performs MCP `initialize` and `tools/call` over newline-delimited JSON-RPC, summarizes text content into Pace observations, and avoids keeping external server processes alive between calls. Validated end-to-end by `PaceMCPClientIntegrationTests` against `scripts/mcp-fixture-server.py`. |
| `PaceMCPServerCatalog.swift` | ~280 | Bundled, one-tap MCP server catalog surfaced in Settings → MCP. Four installable curated servers (filesystem, fetch, applescript, composio); github/slack/linear route through the composio bridge via `supersededBySlug` rather than shipping as separate rows — no remote fetch, list ships with each Pace release. `PaceMCPCatalogInstaller.install/uninstall(_:into:)` performs an atomic JSON merge (temp file + rename) into `~/.config/pace/mcp-servers.json`, preserving every user-added entry. Legacy top-level `servers` keys are migrated forward into the canonical `mcpServers` shape on first install. |
| `PacePrivacyDashboardView.swift` | ~590 | Privacy sidebar entry for PaceMainWindow. Reads-only from `PaceAPIAuditLog` — no new tracking added. Renders a headline card (`0 bytes` when no off-device calls, `X KB to <target>` otherwise), an off-device audit table with target/byte/outcome columns, per-tier turn counts (Local planner / Apple FM / CLI bridge / Direct API), a permissions-vs-last-used cross-reference, and a fixed data-residency claim paragraph. `PacePrivacyDashboardAggregator` and `PacePrivacyByteFormatter` are the pure helpers unit-tested in isolation. |
| `PaceLocalMemoryStore.swift` | ~127 | UserDefaults-backed local memory for durable preferences such as preferred browser, preferred notes app, and default reminder list. Includes a small voice-command parser; preferred browser affects `open_url`. |
| `PaceThreadMemory.swift` | ~300 | Pure two-tier in-context memory: verbatim window of the last K turn pairs (default 4) plus a rolling summary of everything older. Owns no I/O — exposes `record(...)`, `injectionPrefix()`, `verbatimWindow()`, `applySummaryUpdate(...)`, `sessionDidIdle(...)`, `resetSession(...)`, and a monotonic `reserveNextSummaryVersion()` counter so detached summarizer calls finishing out of order are dropped at update time. Session-scoped in memory; persisted across relaunch by `PaceThreadMemoryStore`. |
| `PaceThreadSummarizer.swift` | ~210 | Detached FM call that rolls the displaced verbatim turn into the summary. `PaceThreadFoundationModelSummarizer` uses Apple FM with a single-field `@Generable` envelope (`PaceThreadSummaryResponse`); `PaceThreadLMStudioSummarizer` is the loopback-guarded OpenAI-compatible fallback. Prompt constants live in `PaceThreadSummarizerPrompt` so behavior-changing edits diff cleanly. Fire-and-forget — the user-facing planner turn never awaits this. |
| `PaceThreadMemoryStore.swift` | ~75 | On-device persistence so the conversation survives quit/relaunch. Reads/writes a single atomic JSON (`~/Library/Application Support/Pace/thread-memory.json`) holding a `PaceThreadMemorySnapshot` (session id, verbatim window, rolling summary, version counters). `load` / `save` / `clear`. `PaceThreadMemory` stays I/O-free — it only produces/consumes the snapshot; `CompanionManager` wires restore-at-`start()` + save-after-every-turn/summary/reset, and `clear()` on explicit reset or when thread memory is disabled. Policy: resume always, until reset (no staleness expiry). |
| `PaceMemoryEntry.swift` | ~40 | Unified-memory value type. One `Codable` `PaceMemoryEntry` per memory — `kind` (conversationTurn/fact/preference/journalEvent/summary), text, structured fields, `source` (reuses `PaceRetrievalSource`), timestamps, `[Float]?` embedding, confidence, topicTags, tombstone. Plus `PaceMemoryEntryKind`. Pure data. |
| `PaceMemoryIndex.swift` | ~320 | `@MainActor` in-memory CRUD + brute-force cosine ranking over `PaceMemoryEntry`. `upsert` / `activeEntries` / `tombstone` / `purgeTombstonesOlderThan` / `setEmbedding` / `rankBySemanticSimilarity(topN:)` / `replaceAll`. Skips no-embedding / dimension-mismatch / zero-vector entries; deterministic tie-break by id. No I/O. |
| `PaceMemoryStore.swift` | ~75 | The only disk-touching part of unified memory. Atomic JSON at `~/Library/Application Support/Pace/memory-index.json` (corrupt/missing → `[]`, never blocks launch). Mirrors `PaceThreadMemoryStore`. `CompanionManager` restores at `start()`, dual-writes every turn + extracted fact (with lazy detached embeddings), and persists after each mutation. |
| `PaceMemoryRetriever.swift` | ~200 | THE single recall path (unified-memory Phases 3+5). `@MainActor` class: embeds the query (`PaceTextEmbedding` / LM Studio) and ranks the unified index by cosine; when embeddings are unavailable it falls back to `PaceMemoryIndex.rankByKeywordSimilarity` (BM25) over the SAME index — two ranking modes, one store. Drops excluded + sensitive-topic entries, renders one `LOCAL CONTEXT (memory)` block. `CompanionManager.appendLocalRetrievalContext` calls ONLY this (the legacy parallel lexical-injection path is retired); gated by `useUnifiedMemoryRecall` (default on). |
| `PaceLocalRetrieval.swift` | ~1440 | Local-only RAG store, now the connector INGESTION layer (no longer a recall path — recall moved to `PaceMemoryRetriever` over the unified index, which is resynced from here via `CompanionManager.syncConnectorsIntoUnifiedMemory`). Defines `PaceRetrievalStore` / `PaceRetriever`, retrieval document/status types, file-root preferences, secret-path exclusion, route-aware context policy, persisted source enablement, JSON persistence under Application Support, BM25-style lexical ranking (`rerankedLocalContextBlock`, retained + tested but no longer called in production), source clearing/status, local preference, recent Pace-history, screen-watch + app-usage journals, competitive-research seeds, and an explicit-root file connector that refuses sensitive paths. Still read by the morning-brief composer. INTENTIONALLY RETAINED as the structured operational layer — full deletion (rewriting connectors to emit `PaceMemoryEntry` directly) was scoped and decided against: it re-platforms working non-recall infra (enablement, statuses, journals, morning-brief reads, chat-history backing) for zero user-facing gain. |
| `PaceScreenWatchJournal.swift` | ~239 | Pure day-bucketed journal of watch-mode screen events for the `screenWatchHistory` retrieval source. One rolling document per (day, screen label) with `HH:mm \| category \| app \| description` lines, 90s same-category+app dedup, 40-line bucket cap, 7-day retention, and restart rehydration that parses its own line format back. |
| `PaceAppUsageJournal.swift` | ~161 | Pure day-bucketed journal of foreground app usage for the `appUsageHistory` retrieval source. Accumulates per-app foreground minutes and switch counts from activation events, renders duration-sorted day documents, keeps 7 days, and rehydrates across restarts. Needs no screenshots or permissions. |
| `PaceAppUsageTracker.swift` | ~91 | Thin `@MainActor` AppKit glue between `NSWorkspace.didActivateApplicationNotification` and `PaceAppUsageJournal`. Flushes into retrieval on every app switch plus a tolerant 5-minute timer; started/stopped by the `appUsageHistory` source toggle and app lifecycle. |
| `PaceResearchJournal.swift` | ~270 | Pure day-bucketed journal of research-lane turns for the `researchHistory` retrieval source. One rolling document per day of `{question, answer}` entries, same-day question dedupe, 100-entry cap, 30-day retention, restart rehydration parsing its own line format. Recorded by the `isResearchTurn`-guarded fire-once hook in `CompanionManager+AgentLoop`; exposes `entriesReverseChronological()` / `removeEntry(withId:)` for the Memory-tab list. Mirrors `PaceScreenWatchJournal`'s shape. |
| `PaceTasksSettingsTab.swift` | ~110 | Settings → Tasks tab. Observes `PaceCronScheduler.shared` and lists recurring tasks with a humanized interval (pure `humanizedInterval(_:)`), a "Skips weekends" note, "Last ran …"/"Hasn't run yet" from `PaceCronTask.lastRunAt`, and a delete button; voice-hint empty state. |
| `PaceSystemAudioCapture.swift` | ~636 | Meeting mode controller + SCStream delegate. `PaceMeetingModeController` (singleton, `@MainActor`) starts an SCStream for system audio + a `PaceMeetingAudioRecorder` for mic audio, then on stop runs the full pipeline: segment → transcribe → build notes → journal. State machine: `inactive` → `starting` → `active` → `transcribing` → `synthesizing` → `inactive`. Retriever + a privacy-pinned LOCAL planner are injected via setters to avoid a singleton init cycle; start/stop are guarded by a lifecycle-generation token against MainActor reentrancy, the transcription backend follows the Settings preference, and a mid-meeting SCStream death is surfaced via `systemAudioDroppedMidMeeting` + a notes-summary caveat. The delegate downmixes multi-channel SCStream audio to mono; RMS levels hop to the main actor for the panel meter (`updateSystemAudioLevel`), while raw samples are pushed straight to the recorder's off-main serial writer via the `@Sendable` sink from `makeSystemSampleSink()` (no per-buffer main-actor hop). |
| `PaceMeetingAudioRecorder.swift` | ~674 | `@MainActor` two-track meeting audio recorder. Owns mic via `AVAudioEngine` input tap (resampled to a true 16 kHz mono through one continuous `AVAudioConverter`, boxed in `MicSampleConverter` so conversion runs on the tap thread) + `makeSystemSampleSink()` / `appendSystemSamples(_:)` for the SCStream delegate. Each track drains through a `MeetingTrackWriter` — an `AsyncStream` fed by the capture callbacks and consumed by a single detached task that does Float32→PCM16 conversion + `FileHandle` writes **off the main actor and in FIFO order** (replacing per-buffer `Task { @MainActor }` hops that had no ordering guarantee). Per-track first-sample anchors (from the writer's final state) feed the segmenter's cross-track alignment; recording keeps nothing in RAM (tracks read back from the finalized WAVs at stop). Writes two RIFF WAV files (16-bit PCM) with `.part` suffix; `stop()` finishes+drains both writers, patches the RIFF header with real chunk sizes, and atomically renames to final. `crashRepairAllMeetingRecordings()` statically sweeps every meeting directory at launch (and from Settings), patching only the RIFF size fields so the recorded sample rate is preserved; `pruneMeetingRecordings(olderThanDays:)` applies the retention preference to the audio itself. Recording dir: `~/Library/Application Support/Pace/meetings/<id>/`. |
| `PaceMeetingTurnSegmenter.swift` | ~336 | Pure `nonisolated enum` — energy-based turn segmentation over two meeting audio tracks. Accelerate `vDSP_rmsqv` over 20 ms windows with hysteresis (silence 0.01, speech 0.04, min turn 600 ms, max silence gap 400 ms). Echo trimming: when both tracks exceed the speech threshold in the same window, the louder track wins — compared on an aligned global timeline via per-track `startOffsetSeconds` window padding. Turn sample ranges are clamped to each track's own buffer (the final RMS window is zero-padded and the window count is the cross-track max). No I/O, no async — fully unit-testable. |
| `PaceMeetingNotesBuilder.swift` | ~370 | `@MainActor` structured-notes synthesis. Drives a `BuddyPlannerClient` conformer with a profile-rendered JSON-only prompt to produce `{summary, actionItems, decisions}`. Takes a `profile: PaceMeetingNoteProfile = .general` (default reproduces the legacy `PaceMeetingNotesPrompt` output). Resolves optional action-item transcript grounding (`resolveSource`): a planner-supplied `quote` matched case/space-insensitively against captured turns (longest match wins) → `PaceMeetingActionItemSource{timestamp, quote}`. Lenient decoder; on planner failure/malformed JSON → `synthesisFailed: true` with transcript preserved. Empty transcript → empty notes, no planner call. Defines `PaceMeetingNotes`, `PaceMeetingTurnRecord`, `PaceMeetingActionItem` (+ optional `source`), `PaceMeetingActionItemSource`. |
| `PaceMeetingNoteProfile.swift` | ~200 | Pure `PaceMeetingNoteProfile` value type (slug/name/description/sections/emit + grounding flags + optional `voiceAliases`) shaping how notes are organized per meeting type. `renderSystemPrompt()` builds the JSON-only synthesis prompt; the built-in `.general` profile renders byte-for-byte identical to the legacy `PaceMeetingNotesPrompt` (the compatibility anchor). Lenient `init(from:)` defaults `voiceAliases` to `[]` so pre-existing/user profiles decode. Meetily-informed but Pace-native — keeps the stable `{summary, actionItems, decisions}` output contract. |
| `PaceMeetingNoteProfileLibrary.swift` | ~290 | Pure loader/validator mirroring `PaceRecipeLibrary` + `PaceSkillLoader`: bundled `Resources/meeting-note-profiles/<slug>.json` (general, standup, one-on-one) + user overrides from `~/Library/Application Support/Pace/meeting-note-profiles/` (override-by-slug, malformed user files soft-skipped). Startup validation via `PaceToolRegistry.validateForAppStartup` (loud for bundled). Pure precedence resolver `resolveProfile(explicit → non-general default → inferred → general)` + `shouldInfer(...)` predicate. |
| `PaceMeetingNotesJournal.swift` | ~320 | Pure per-meeting journal for the `meetingNotes` retrieval source. One document per meeting (not day-bucketed), natural-language rendering of summary + action items + decisions for BM25 recall ("what did we decide in standup?"). Action items include transcript grounding (`[ref: "quote"]`) for who-agreed-what recall; robust right-to-left parse-back round-trips text/owner/due/quote. 30-day retention, restart rehydration, mirrors `PaceScreenWatchJournal`'s shape. |
| `PaceFileDownload.swift` | ~98 | Request type and pure helpers for the `download_file` tool: http(s)-only URL validation that refuses embedded credentials, filename sanitization that reduces path-like names to their final component, and Finder-style collision suffixing. |
| `PaceTimerService.swift` | ~210 | Timer skill: `PaceTimerStore` JSON-persists active timers under Application Support, `PaceTimerScheduler` (@MainActor) owns the live `Timer` objects + fires the spoken nudge through `CompanionManager`'s TTS callback, and `PaceTimerDurationParser` turns "3 minutes" / "30s" / "2 hours" / plain seconds into a `TimeInterval`. Past-due timers fire on rehydrate so a quit/restart doesn't silently swallow an egg-timer. |
| `PaceMorningBriefBuilder.swift` | ~220 | Pure deterministic composer for the daily morning brief. Takes typed `PaceMorningBriefInputs` (today's calendar events, unread mail count + top sender/subject, open reminders due today, yesterday's top app + minutes, yesterday's watch highlight) and returns one spoken-ready paragraph. Each clause is omitted when its source is empty; the all-empty fallback is "your morning's clear." No LLM call — keeps the brief cheap, predictable, and unit-testable. |
| `PaceFailureNarrator.swift` | ~150 | Pure deterministic composer for the plain-language failure surface. Takes a typed `PaceFailureKind` (planner offline, missing permission, click missed, sidecar TTS offline, MCP server not configured, cloud-bridge upstream error) and returns a spoken-ready string plus an optional `PaceFailureSuggestion` panel hint (open Settings, open specific permission, run the TTS sidecar script, configure MCP server, open the local-ai bridge folder). No LLM call — mirrors the table-driven shape of `PaceMorningBriefBuilder`. `CompanionManager.speakPlainLanguageFailure(_:context:)` routes the composed string through `PaceRestraintGate.decide(...)` so failure speech stays silent during a Zoom/active call. |
| `PaceMorningTriageScheduler.swift` | ~250 | `@MainActor` proactive scheduler for the daily weekday morning brief. Owns a single `Timer` that fires at the user-configured local time, skips weekends via `Calendar.nextDate(after:matching:matchingPolicy:)`, gates speech through `PaceRestraintGate` (active-call → park brief on `pendingMorningBriefCard` panel surface instead), records the brief in `paceHistory` retrieval, and exposes `deliverNowForTesting()` for the Settings "Send it now" preview button. Source fetching is injected as a closure so the scheduler stays testable without EventKit, Mail AppleScript, or live retrieval state. Default OFF — `CompanionManager.start()` only calls `start()` when `isMorningTriageEnabled` is true. |
| `PaceFlowReplay.swift` | ~165 | Pure model + voice-command parser + replay heuristic. Defines `PaceRecordedFlow` / `PaceRecordedStep`, `PaceFlowCommandParser` ("remember this flow as ...", "do <name>"), and `PaceFlowReplayPlanner` (pause-before-send heuristic + replay observation strings). Schema-identical to the bundled recipe shape so a recipe install is just a `PaceFlowStore.save(...)` call. Wave 3a split out the persistent store (`PaceFlowStore.swift`) and the live event recorder (`PaceFlowRecorder.swift`); this file no longer owns either runtime layer. |
| `PaceFlowStore.swift` | ~245 | JSON-backed persistence for `PaceRecordedFlow` — one file per flow under `~/Library/Application Support/Pace/flows/<slug>.json`. Atomic temp-file + rename writes (same shape as `PaceMCPCatalogInstaller`). Public API is `save / load / delete / listAll / rename` plus `slug(for:)` and a one-shot `migrateLegacyUserDefaultsFlowsIfNeeded(...)`. `listAll()` is sorted by `createdAt` desc. Slug normalization lowercases, collapses non-alphanumerics into `-`, and caps at 64 characters. Schema matches the bundled recipe JSON shape byte-for-byte so `PaceRecipeLibrary.install(...)` writes through the same code path. |
| `PaceFlowRecorder.swift` | ~820 | `@MainActor final class` that turns live user input into `PaceRecordedStep` values via a listen-only `CGEventTap` for `.leftMouseDown / .rightMouseDown / .keyDown / .flagsChanged`. State machine: `idle → recording → stopped(reason:)`. Auto-stops after 60s idle to release the tap. Per-focus typing buffers capped at 256 chars defend against runaway typing OOM. Secure (`AXSecureTextField`) focus suppresses keystroke capture entirely and emits a `typeText(secure: true)` placeholder on flush. Mouse-down rebuilds an AX role-path via `AXUIElementCopyElementAtPosition` + parent climb; clicks with no pressable AX ancestor are dropped (no pixel-only steps). Modifier + key emits `keyShortcut(key:)`; bare return/escape/tab/arrows/Fn-keys also emit as shortcuts so the replayer doesn't try to type them. `NSWorkspace.didActivateApplicationNotification` emits `activateApp(bundleIdentifier:)` with consecutive-dup suppression. `recordEventForTesting / recordAppActivationForTesting / fireIdleTimeoutForTesting` are test seams. |
| `PaceRecipeLibrary.swift` | ~406 | Bundled Poke-style recipe library. Loads `Resources/recipes/<slug>.json`, validates them at app startup via `PaceToolRegistry.validateForAppStartup`, and installs them into `PaceFlowStore` so the existing `run_flow` tool can execute each one with no new tool kind. Refuses install when a `requiredPreferences` key (raw `PaceLocalMemoryKey` value) is unset, when a flow with the same name is already saved, or when the required preference key is unknown. v1 ships morning-standup-setup, weekly-review-draft, email-zero, focus-mode-on, end-of-day-shutdown. |
| `PaceRecipeCommandParser.swift` | ~126 | Pure prefix-based parser for "install/uninstall/list the <name> recipe" voice commands. Routed BEFORE the planner in `CompanionManager` so installing a recipe doesn't burn a planner round-trip. Mirrors the parsing shape of `PaceFlowCommandParser` + `PaceLocalMemoryCommandParser`. |
| `Resources/recipes/<slug>.json` | — | Five bundled Pace flow recipes shipped in the app bundle. JSON schema matches `PaceRecordedFlow` plus a small metadata layer (slug, description, displayCategory, requiredPreferences, secureFieldDefaults). Validated at app startup; malformed JSON fails launch. |
| `PaceEmbeddingReranker.swift` | ~165 | Embedding-backed re-ranking over the lexical retrieval store via the OpenAI-compatible `/v1/embeddings` endpoint (LM Studio). Best-effort: any endpoint/decode failure returns lexical order unchanged; blended min-max scores with semantic tie-breaks. |
| `PacePostureAnalyzer.swift` | ~145 | Pure posture-classification logic for the camera posture watch. Median-calibrated baseline from face-rectangle geometry, sinking/leaning thresholds, consecutive-bad-sample hysteresis, and an alert cooldown — all unit-testable without a camera. |
| `PacePostureMonitor.swift` | ~190 | Thin AVFoundation/Vision glue for the posture watch. Low-res capture session, lock-gated one-frame-per-10s throttle on the video queue, `VNDetectFaceRectanglesRequest`, feeds `PacePostureAnalyzer`; frames never leave memory. Off by default behind the `isPostureWatchEnabled` preference; nudges speak through the existing TTS only when no turn is in flight. |
| `PaceCompetitiveResearchSeeds.swift` | ~90 | Built-in local retrieval seed documents for competitor/product research that should be available without fetching the web at runtime. Seeds cover Project Minimi (ambient memory for Claude), Dayflow (private work journal + LM Studio), and the local private-voice-assistant category (Dottie/OpenFelix patterns vs Pace's on-device agent loop). |
| `PaceSpotlightRetrievalConnector.swift` | ~202 | Spotlight/NSMetadataQuery file discovery source for local retrieval. Searches only explicit configured roots, filters secret paths and unsupported extensions, loads allowed text files through the existing file connector, and exposes an injected candidate provider for tests without relying on the machine Spotlight index. |
| `PaceCalendarRetrievalConnector.swift` | ~220 | Permission-aware Calendar source for local retrieval. Reads EventKit events only when full Calendar access already exists, maps compact event snapshots into retrieval documents, reports denied/restricted/write-only permission as skipped source status, and never requests permission from the retrieval path. |
| `PaceRemindersRetrievalConnector.swift` | ~264 | Permission-aware Reminders source for local retrieval. Reads open and recently completed EventKit reminders only when full Reminders access already exists, maps compact snapshots into retrieval documents, reports denied/restricted/write-only permission as skipped source status, and never requests permission from the retrieval path. |
| `PaceContactsRetrievalConnector.swift` | ~198 | Permission-aware Contacts source for local retrieval. Reads contact names, nicknames, organizations, titles, and email addresses only when Contacts access already exists, maps compact snapshots into retrieval documents, reports denied/restricted permission as skipped source status, and never requests permission from the retrieval path. |
| `PaceNotesRetrievalConnector.swift` | ~184 | Read-only Apple Notes source for local retrieval. Uses AppleScript only on user-triggered source refresh/reset, maps compact note snapshots into retrieval documents, strips simple Notes HTML from indexed text, and reports Automation denial/read failure as skipped source status. |
| `PaceMailRetrievalConnector.swift` | ~209 | Read-only Apple Mail inbox source for local retrieval. Uses AppleScript only on user-triggered source refresh/reset, maps compact message snapshots into retrieval documents, caps indexed message bodies, and reports Automation denial/read failure as skipped source status. |
| `PaceAXTargeter.swift` | ~135 | Accessibility-tree pre-pass for single clicks. Given a CG global point, calls `AXUIElementCopyElementAtPosition`, climbs up to a pressable role (AXButton, AXLink, AXMenuItem, etc.), and fires `AXUIElementPerformAction(kAXPressAction)`. Returns false on miss so the executor falls back to CGEvent. |
| `PaceAvatarOverlay.swift` | ~470 | Small walking-character SwiftUI overlay in its own `NSPanel`. `PaceAvatarOverlayManager` owns lifecycle + position; `PaceAvatarWalkController` drives horizontal movement + idle pauses + mouth-open state based on `CompanionVoiceState`. Click triggers `paceAvatarTapped` which opens the menu-bar panel. |
| `DesignSystem.swift` | ~138 | Design system tokens — colors, corner radii, animations, state layers. All UI references `DS.Colors`, `DS.CornerRadius`, etc. |
| `PaceAnalytics.swift` | ~73 | Local-only analytics shim. Keeps existing call sites stable while intentionally avoiding network telemetry, transcript upload, response upload, or cloud SDK initialization. |
| `WindowPositionManager.swift` | ~151 | Window placement helpers plus System Settings deep links for Accessibility, Screen Recording, Speech Recognition, Calendar, Reminders, and Automation permissions. |
| `AppBundleConfiguration.swift` | ~28 | Runtime configuration reader for keys stored in the app bundle Info.plist. |
| `PaceAXScreenReader.swift` | ~529 | Accessibility-tree screen reader. Walks the frontmost window's AX hierarchy into a compact element list as a screenshot-free screen-context source. |
| `PaceLMStudioModelLoader.swift` | ~353 | LM Studio model lifecycle helper. Warm-loads the configured planner + VLM at launch via the `lms` CLI, runs a keepalive loop, and unloads model RAM synchronously at quit. |
| `AppleFoundationModelsPlannerClient.swift` | ~340 | Apple on-device Foundation Models planner conformer using the `@Generable` typed-output path (`PaceFMTurnResponse`). Preferred pure-knowledge answer planner when Apple Intelligence is available. |
| `PacePlannerModelResolver.swift` | ~206 | Resolves which loaded LM Studio model should serve the planner role, extracting parameter sizes from common model-name patterns and excluding VLMs/embedding models. |
| `PaceScreenContextScaler.swift` | ~95 | Maps VLM element coordinates between downsampled-screenshot space and full screenshot/display space, including multi-monitor origin handling. |
| `PaceFMTurnResponse.swift` | ~110 | `@Generable` typed response envelope for the Apple Foundation Models planner path. |
| `scripts/test-pace.sh` | ~200 | Runs the unit-test suite TCC-safely: `xcodebuild test` into an isolated `/tmp` DerivedData path with code signing disabled, so the interactive Pace.app's permissions stay untouched. Prints a structured pass/fail summary via xcresulttool. The standard way for agents to run tests. |
| `scripts/verify.sh` | ~67 | Pre-commit gate: runs `test-pace.sh`, optionally followed by `diag-pace.py --quick --eval`. Non-zero exit on any failure for safe `&& git commit` chaining. |
| `scripts/start-tts-server.sh` | ~38 | Launches the Kokoro TTS sidecar via uvx/mlx-audio on port 8880. Pace hot-swaps to it without restart; first call downloads the model. |
| `scripts/tts-fixture-server.py` | ~58 | Stdlib HTTP fixture returning a silent WAV from `/v1/audio/speech`, so `LocalServerTTSClient`'s synth→decode→play→drain loop is integration-tested without a model. |
| `scripts/mcp-fixture-server.py` | ~109 | Stdlib-only stdio MCP fixture server speaking Pace's newline-delimited JSON-RPC dialect (echo/fail/sleep tools). Used by `PaceMCPClientIntegrationTests` and the SETUP_LOCAL.md "verify your MCP setup" recipe. |
| `scripts/cloud-bridge-fixture-server.py` | ~70 | Stdlib HTTP fixture for `CloudBridgePlannerClientTests`. Serves the local-ai bridge SSE shape (`data: {"text":"..."}` / `data: [DONE]`) on a configurable port, with a `trigger_error` mode for error-path tests. |
| `scripts/benchmark_ttfsw.sh` | ~140 | Aggregates per-turn TTFSW + TTFT samples from the macOS unified log. Three modes: `--last 10m` (default 30m), `--live` (stream until Ctrl-C), `--file path` (parse a saved log). Outputs a markdown stats table — paste into PRs / landing page. |
| `scripts/smoke-runtime-hooks.sh` | ~122 | Launches the Debug app with `PACE_ENABLE_SMOKE_HOOKS=1` and verifies panel show/hide, cursor annotation off/on state, synthetic clarification show/resolve state, click-target clarification, all-fail observation breadcrumb, and approval-popup cancellation. |
| `scripts/smoke-real-apps.sh` | ~95 | Real-app smoke: Notes/Safari activate + Mail mailto compose timing + executor dry-run unit tests. No Pace UI driving required. |
| `scripts/smoke-executor-surface.sh` | ~55 | Executor PRD runner: dry-run tests, v10 schema fixtures, runtime hooks (when Debug Pace.app exists), real-app smoke. |
| `scripts/eval-v10-gate.sh` | ~55 | Pre-ship v10 gate: unit tests + schema fixtures + FM sweep when Apple Intelligence is ready. |
| `scripts/generate-og-image.sh` | ~30 | Rasterize `website/public/og-image.svg` → `og-image.png` via qlmanage or rsvg-convert. |
| `scripts/train-pace-tuned-model.sh` | ~70 | Pace-tuned LoRA training scaffold (check prerequisites, print MLX command, emit remote manifest JSON). |
| `docs/current/plans/autonomous-companion-consolidation.md` | ~170 | Canonical product-level handoff for turning the implemented perception, memory, proactivity, background-work, and action primitives into one dependable companion. Records the remaining integration and self-learning gaps, five live acceptance stories, validation gates, and recommended resume order. |
| `scripts/export-pace-tuned-turns.sh` | ~45 | Copy opt-in `~/Library/Application Support/Pace/pace-tuned-turns.jsonl` into `evals/pace-tuned-export/`. |
| `leanring-buddy/PaceTunedTurnExporter.swift` | ~170 | Opt-in anonymized local planner turn export (Settings → Models). |
| `scripts/eval-v10-schema-fixtures.py` | ~151 | Deterministic v10 planner-response schema fixture gate. Reads the bundled `pace-fm-response-v10.schema.json` artifact and validates every fixture under `evals/v10-schema-fixtures/` without calling LM Studio or testing a model. |
| `scripts/eval-fm.sh` | ~245 | Runs the Apple Foundation Models planner against every `evals/fm-fixtures/*.txt` fixture with the @Generable typed-output path. Compiles a Swift program once, then iterates fixtures so the FM state stays warm. Prints `spokenText`, `pointAtElementId`, `clickElementId` per fixture. Used as the FM baseline for `eval-planners.py`. |
| `scripts/eval-planners.py` | ~1090 | Head-to-head planner comparison. Takes a list of model identifiers ("fm" delegates to eval-fm.sh; any other name is treated as an LM Studio model id loaded via `lms load`). Sends each through the same fixture set with the same JSON schema, scores against EXPECT_* fields, and emits a markdown scorecard. Also supports `V10_MODE` fixtures (see `evals/pace_v10.py`): sends the real production v10 decode-constrained envelope and scores the DECODED action list. Pinning multiple LM Studio models needs LM Studio Settings → max-loaded-models ≥2. |
| `evals/pace_v10.py` | ~400 | Shared helpers for the v10 action-quality eval (`V10_MODE` fixtures). Provides `V10_RESPONSE_FORMAT` (mirror of `LocalPlannerClient.v10ResponseFormat` — payload free except a typed `calls` sub-schema), `build_agent_mode_system_prompt()` (verbatim `CompanionSystemPrompt` prose blocks + a tool list auto-derived from `PaceToolRegistry.swift`), and `decode_v10_actions()` (mirror of `PaceActionTagParser.parsePlannerActions` — turns an envelope into the executable action list). Keep in sync with those three Swift files. |
| `scripts/eval-memory-recall.py` | ~230 | Recall-QUALITY eval for unified memory. Scores two rankers over inline fixtures whose queries use DIFFERENT words than the expected memory: a keyword/BM25 baseline (stopwords mirror `PaceMemoryIndex`, runs with no model — expected to MISS the lexically-divergent rows) vs. semantic cosine over LM Studio `/v1/embeddings` (the production path — should rescue them). Markdown scorecard; semantic auto-skips with a clear message when LM Studio is unreachable. Exits non-zero only when a reachable semantic run fails to rescue every no-overlap case, so it can gate CI once the embedding model is a fixture. |
| `scripts/eval-locomo-recall.py` | ~190 | Runs Pace's embedding recall against the public **LoCoMo** long-term-memory benchmark (`raw.githubusercontent.com/snap-research/locomo/main/data/locomo10.json`, fetched to /tmp), and doubles as a prototyping bench for recall improvements before porting to `PaceMemoryRetriever`. Indexes dialog content, embeds each question with the production embedding model, ranks, reports **retrieval recall@k** (gold-evidence turn in top-k) overall + by LoCoMo category. Flags: `--window N` (group N turns/unit), `--date` (session-date prefix), `--hybrid` (RRF-fuse BM25 + semantic). Default reproduces the naive per-turn semantic baseline. Measured lift (2 convs, 231 Qs, nomic): baseline recall@1/5 = 33/63%; +windowing+date+hybrid = **49/74%** (temporal @5 76→87%; hybrid slightly regresses multi-hop@1). |
| `scripts/eval-locomo-qa.py` | ~190 | Full LOCAL answer-accuracy pipeline on LoCoMo — the apples-to-apples metric vs. market memory tools. retrieve (nomic + window3 + date + hybrid RRF) → answer (`google/gemma-3-12b` from retrieved context, may use world knowledge) → LLM-judge (same local model, lenient semantic match matching how Mem0/Zep/ByteRover score). Retry-hardened HTTP. Firmest result (**190 Qs / 3 convs**): **~70%** (single-hop 83, open-domain 74, multi-hop 67, temporal 58) — above Mem0 (62–67), just under Zep/LangMem (75–79), fully on-device. (A 56-Q sample read 77%; the bigger run settled it to a stabler 70%. A too-strict judge + memory-only prompt earlier understated it to 54%.) Still a local gemma judge — a GPT-4o judge could move it a few points. |
| `scripts/diag-pace.py` | ~1060 | Pace runtime self-diagnostic. Exercises the exact LM Studio call pattern Pace uses every turn (VLM + planner alternating), measures TTFT, flags model thrashing, asserts VLM JSON decodes into LocalVLMScreenAnalysis. With `--eval` also runs the full FM-fixture set through the configured planner and folds the pass count + mean latency into the same PASS/FAIL board. Read Info.plist directly so it tests exactly what Pace will use at runtime. |
| `evals/fm-fixtures/*.txt` | — | Plain-text fixtures consumed by eval-fm.sh + eval-planners.py + diag-pace.py. Each declares USER:, ELEMENT: lines, plus optional EXPECT_POINT_ID / EXPECT_CLICK_ID / EXPECT_POINT_ID_ONE_OF / SPOKEN_MUST_CONTAIN / SPOKEN_MUST_NOT_CONTAIN / SPOKEN_MAX_WORDS scoring metadata. See `evals/fm-fixtures/README.md` for the full schema. |
| `evals/v10-schema-fixtures/*.json` | — | Deterministic v10 planner-response examples labelled schema-valid or schema-invalid. Used by `scripts/eval-v10-schema-fixtures.py` to catch envelope/schema drift before model evals or runtime-default switches. |
| `evals/fm-fixtures-actions/*.txt` | — | V10 action-quality fixtures (draw / open_app-vs-open_url / multi-step skill). Set `V10_MODE: true`; scored against the DECODED action list via `evals/pace_v10.py`. Cover the three live action-quality failures the typed/free-text fixtures never exercised. See `evals/README.md`. |
