Pace - Key Files
Pace - 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; the architecture narrative stays in the architecture docs 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. |
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 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 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. |