### Onboarding View with Setup Rows Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-20-onboarding.md The main onboarding view that utilizes the SetupRow helper to display and manage setup tasks like accessibility permissions and speech model downloads. It includes logic for initial permission checks, model loading, and polling for accessibility status. ```swift // Accessibility SetupRow( icon: "keyboard.fill", title: "Accessibility", subtitle: "For the Control key hotkey & pasting", isComplete: accessibilityGranted ) { if !accessibilityGranted { Button("Grant") { PermissionChecker.promptAccessibility() } .buttonStyle(.borderedProminent) .tint(.orange) .controlSize(.small) } } // Model download SetupRow( icon: "brain", title: "Speech Model", subtitle: modelManager.state == .error ? "Download failed" : modelManager.isReady ? "Ready" : "Downloading…", isComplete: modelManager.isReady ) { if modelManager.state == .loading { ProgressView() .controlSize(.small) } else if modelManager.state == .error { Button("Retry") { Task { await modelManager.loadModel() } } .buttonStyle(.borderedProminent) .tint(.orange) .controlSize(.small) } } } .padding(.horizontal, 24) Spacer() if allComplete { Button(action: { stopAccessibilityPolling() onContinue() }) { Text("Continue") .font(.headline) .frame(maxWidth: .infinity) .padding(.vertical, 8) } .buttonStyle(.borderedProminent) .tint(.orange) .padding(.horizontal, 40) .padding(.bottom, 24) } else { Text("Complete all items above to continue") .font(.caption) .foregroundStyle(.secondary) .padding(.bottom, 24) } } .onAppear { // Check initial permission states micGranted = AVCaptureDevice.authorizationStatus(for: .audio) == .authorized micDenied = AVCaptureDevice.authorizationStatus(for: .audio) == .denied accessibilityGranted = PermissionChecker.checkAccessibility() // Start model download if !modelLoadStarted && !modelManager.isReady { modelLoadStarted = true Task { await modelManager.loadModel() } } // Poll for accessibility startAccessibilityPolling() } .onDisappear { stopAccessibilityPolling() } } private func startAccessibilityPolling() { accessibilityTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { let granted = PermissionChecker.checkAccessibility() if granted { accessibilityGranted = true stopAccessibilityPolling() } } } private func stopAccessibilityPolling() { accessibilityTimer?.invalidate() accessibilityTimer = nil } } ``` -------------------------------- ### TryItController for Onboarding Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-20-onboarding.md Manages hotkey monitoring, audio recording, and transcription for the TryItStep. Initializes with a WhisperTranscriber and handles retries for starting the hotkey monitor. ```swift @MainActor class TryItController: ObservableObject { @Published var isRecording = false @Published var isTranscribing = false @Published var transcribedText: String? @Published var monitorStartFailed = false private var hotkeyMonitor: HotkeyMonitor? private var audioRecorder: AudioRecorder? private var hasAdvanced = false private var retryCount = 0 private let maxRetries = 5 private let transcriber: WhisperTranscriber init(transcriber: WhisperTranscriber) { self.transcriber = transcriber } func start(onAdvance: @escaping () -> Void) { let recorder = AudioRecorder() recorder.prewarm() self.audioRecorder = recorder let monitor = HotkeyMonitor() monitor.onRecordingStart = { [weak self] in Task { @MainActor in guard let self else { return } self.isRecording = true try? recorder.startRecording() } } monitor.onRecordingStop = { [weak self] in Task { @MainActor in guard let self else { return } self.isRecording = false self.isTranscribing = true let buffer = await recorder.stopRecording() let text = await self.transcriber.transcribe(audioBuffer: buffer) self.isTranscribing = false if let text { self.transcribedText = text // Auto-advance after 2 seconds (guarded) DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in self?.advance(onAdvance: onAdvance) } } } } if monitor.start() { self.hotkeyMonitor = monitor } else { retryStartMonitor(monitor: monitor) } } func advance(onAdvance: () -> Void) { guard !hasAdvanced else { return } hasAdvanced = true cleanup() onAdvance() } func cleanup() { hotkeyMonitor?.stop() hotkeyMonitor = nil audioRecorder = nil } private func retryStartMonitor(monitor: HotkeyMonitor) { guard retryCount < maxRetries else { monitorStartFailed = true return } retryCount += 1 DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in if monitor.start() { self?.hotkeyMonitor = monitor } else { self?.retryStartMonitor(monitor: monitor) } } } } ``` -------------------------------- ### Install and Launch Signed App Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-25-settings-sidebar-and-sound-toggle.md Installs the newly built signed Ghost Pepper application to the Applications directory and launches it. Removes any previous installation. ```sh rm -rf /Applications/GhostPepper.app cp -R build/settings-sidebar-signed/Build/Products/Debug/GhostPepper.app /Applications/GhostPepper.app open -na /Applications/GhostPepper.app ``` -------------------------------- ### SetupRow View Implementation Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-20-onboarding.md A reusable SwiftUI view for displaying individual setup tasks. It shows an icon, title, subtitle, and completion status, with a slot for custom actions when the task is not complete. ```swift struct SetupRow: View { let icon: String let title: String let subtitle: String let isComplete: Bool @ViewBuilder let actions: () -> Actions var body: some View { HStack(spacing: 12) { Image(systemName: icon) .font(.title2) .frame(width: 32) .foregroundStyle(isComplete ? .green : .secondary) VStack(alignment: .leading, spacing: 2) { Text(title) .font(.body.weight(.medium)) Text(subtitle) .font(.caption) .foregroundStyle(.secondary) } Spacer() if isComplete { Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) .font(.title3) } else { actions() } } .padding(12) .background( RoundedRectangle(cornerRadius: 10) .fill(Color(nsColor: .controlBackgroundColor)) ) } } ``` -------------------------------- ### Read File Output Format Example Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/specs/2026-04-28-agentic-meeting-qa-design.md Illustrates the output format for the `read_file` tool, including line numbers and metadata about the returned lines. ```text 1 --- 2 title: Dana <> Matt 3 date: 2025-01-29 ... 200 some text ``` -------------------------------- ### Start Recording Audio Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/specs/2026-03-20-onboarding-design.md Begins the audio recording process. This is a direct call to the audio recording component. ```swift AudioRecorder.startRecording() ``` -------------------------------- ### SwiftUI SetupStep View Structure Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-20-onboarding.md Define the structure for the setup step view, which manages microphone and accessibility permissions, and model downloads. It uses observed objects for application state and model management. ```swift struct SetupStep: View { @ObservedObject var appState: AppState @ObservedObject var modelManager: ModelManager let onContinue: () -> Void @State private var micGranted = false @State private var micDenied = false @State private var accessibilityGranted = false @State private var accessibilityTimer: Timer? @State private var modelLoadStarted = false private var allComplete: Bool { micGranted && accessibilityGranted && modelManager.isReady } var body: some View { VStack(spacing: 16) { Text("Setup") .font(.system(size: 24, weight: .bold)) .padding(.top, 24) Text("Grant permissions and download the speech model") .font(.callout) .foregroundStyle(.secondary) VStack(spacing: 10) { // Microphone SetupRow( icon: "mic.fill", title: "Microphone", subtitle: "To hear your voice", isComplete: micGranted ) { if micDenied { Button("Open Settings") { PermissionChecker.openMicrophoneSettings() } .buttonStyle(.borderedProminent) .tint(.orange) .controlSize(.small) } else if !micGranted { Button("Grant") { Task { let granted = await PermissionChecker.checkMicrophone() micGranted = granted if !granted { micDenied = true } } } .buttonStyle(.borderedProminent) .tint(.orange) .controlSize(.small) } } ``` -------------------------------- ### Start Hotkey Monitor Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/specs/2026-03-20-onboarding-design.md Attempts to start the hotkey monitoring service. Returns false if accessibility permissions are not yet fully applied by the system, requiring a retry. ```swift HotkeyMonitor.start() ``` -------------------------------- ### Trace Display Example Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/specs/2026-04-28-agentic-meeting-qa-design.md Illustrates the expandable trace view showing tool activity, input summaries, and result summaries for debugging. ```plaintext ▼ Trace (8 events) [grep] pattern="Quinn", case_insensitive=true → 7 matches in 3 files [read_file] 2025-01-29/dana-matt.md offset=1 limit=200 → returned lines 1–200 of 312 [grep] pattern="Sam Rivers" → 2 matches in 1 file [read_file] 2026-01-07/team-standup.md offset=170 limit=50 → returned lines 170–219 of 580 ... ``` -------------------------------- ### Build and Install Debug Build Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-04-28-agentic-meeting-qa.md Builds the GhostPepper application using xcodebuild, copies the debug build to the Applications directory, and opens it. ```bash xcodebuild build -project GhostPepper.xcodeproj -scheme GhostPepper -derivedDataPath build/derived -skipMacroValidation -quiet cp -R build/derived/Build/Products/Debug/GhostPepper.app /Applications/ open /Applications/GhostPepper.app ``` -------------------------------- ### Configure Agentic Q&A Callback Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-04-28-agentic-meeting-qa.md Sets up the `onAskQuestion` callback for the Q&A interface. It handles backend selection (Claude API or local) and initiates the agent's question-answering process. Requires API key setup for Claude API. ```swift controller.onAskQuestion = { [weak self] question in AsyncThrowingStream { continuation in guard let self else { continuation.finish() return } let backend = QABackendKind(rawValue: self.meetingQABackend) ?? .claudeAPI switch backend { case .claudeAPI: guard let key = KeychainHelper.get(AnthropicProvider.keychainKey), !key.isEmpty else { Task { @MainActor in self.showSettings(section: .meetingTranscript) } continuation.yield(.error("Add your Claude API key to continue — Settings opened.")) continuation.finish() return } let model = ClaudeAPIModel(rawValue: self.claudeAPIModel) ?? .sonnet let provider = AnthropicProvider(model: model, apiKey: key) let archiveRoot = MeetingTranscriptSettings.effectiveSaveDirectory() let agent = MeetingQAAgent(provider: provider, model: model, archiveRoot: archiveRoot, maxIterations: 15) let task = Task { do { for try await event in agent.ask(question) { continuation.yield(event) } continuation.finish() } catch { self.debugLogStore.record(category: .model, message: "Agentic Q&A error: \(error)") continuation.yield(.error("Claude API error: \(error.localizedDescription)")) continuation.finish() } } continuation.onTermination = { _ in task.cancel() } case .local: continuation.yield(.error("Local cross-meeting Q&A isn't supported — switch to Claude API in Settings → Meeting Transcript → Cross-Meeting Q&A.")) continuation.finish() } } } ``` -------------------------------- ### Example CleanupModelProbe One-Shot Mode Usage Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/specs/2026-03-25-cleanup-model-probe-design.md Demonstrates how to use the CleanupModelProbe in one-shot mode with a specific model and input text. This mode is designed to be scriptable for automated testing or quick checks. ```sh CleanupModelProbe --model fast --input "Okay, it's running now." ``` -------------------------------- ### Copy Built App to Applications Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-04-20-speaker-identities.md Copies the compiled release build of the Ghost Pepper application to the standard /Applications directory. This step prepares the app for user installation. ```bash ditto build/Build/Products/Release/GhostPepper.app /Applications/GhostPepper.app ``` -------------------------------- ### Enter Interactive Mode Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/development.md Start the cleanup model probe in interactive mode, keeping the selected model loaded for continuous use. Type ':quit' or send EOF to exit. ```sh ./scripts/cleanup-model-probe.sh --model fast --thinking suppressed ``` -------------------------------- ### TryItStep View Implementation Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-20-onboarding.md A SwiftUI View for the onboarding 'Try It' step. It displays instructions, a visual representation of the control key, and uses TryItController to manage recording and transcription. ```swift struct TryItStep: View { @ObservedObject var appState: AppState let onContinue: () -> Void @StateObject private var controller: TryItController init(appState: AppState, onContinue: @escaping () -> Void) { self.appState = appState self.onContinue = onContinue self._controller = StateObject(wrappedValue: TryItController(transcriber: appState.transcriber)) } var body: some View { VStack(spacing: 20) { Text("Try It") .font(.system(size: 24, weight: .bold)) .padding(.top, 24) Text("Hold the **Control** key and say something") .font(.callout) .foregroundStyle(.secondary) // Keyboard visual HStack(spacing: 6) { KeyCap(label: "fn", highlighted: false) KeyCap(label: "⌃ control", highlighted: true, isActive: controller.isRecording) KeyCap(label: "⌥", highlighted: false) KeyCap(label: "⌘", highlighted: false) } .padding(.vertical, 8) ``` -------------------------------- ### Run Full Test Suite and Build Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-23-transcription-stack.md Execute the complete test suite and then build the application in debug configuration. This verifies the overall stability and build process. ```bash xcodebuild -project GhostPepper.xcodeproj -scheme GhostPepper -derivedDataPath build/test-derived -skipMacroValidation CODE_SIGNING_ALLOWED=NO test ``` ```bash xcodebuild -project GhostPepper.xcodeproj -scheme GhostPepper -configuration Debug -derivedDataPath build/derived -skipMacroValidation CODE_SIGNING_ALLOWED=NO build ``` -------------------------------- ### Git Commit for Welcome Step Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-20-onboarding.md Commit the changes made to the `OnboardingWindow.swift` file after implementing the welcome step. ```bash git add GhostPepper/UI/OnboardingWindow.swift git commit -m "feat: add welcome step with branding and privacy message" ``` -------------------------------- ### SwiftUI WelcomeStep View Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-20-onboarding.md Implement the initial welcome screen for the onboarding flow. This view displays branding, a description of the app, and privacy information. It requires a closure to handle the continue action. ```swift struct WelcomeStep: View { let onContinue: () -> Void var body: some View { VStack(spacing: 20) { Spacer() Image(nsImage: NSApp.applicationIconImage) .resizable() .frame(width: 128, height: 128) .cornerRadius(24) Text("Ghost Pepper") .font(.system(size: 28, weight: .bold)) Text("Hold-to-talk speech-to-text\nfor your Mac") .font(.title3) .foregroundStyle(.secondary) .multilineTextAlignment(.center) HStack(spacing: 8) { Image(systemName: "lock.shield.fill") .foregroundStyle(.green) Text("100% Private — Everything runs locally on your Mac.\nNo cloud, no accounts, no data ever leaves your machine.") .font(.callout) .foregroundStyle(.secondary) } .padding() .background( RoundedRectangle(cornerRadius: 10) .fill(Color.green.opacity(0.08)) .strokeBorder(Color.green.opacity(0.2)) ) .padding(.horizontal, 24) Spacer() Button(action: onContinue) { Text("Get Started") .font(.headline) .frame(maxWidth: .infinity) .padding(.vertical, 8) } .buttonStyle(.borderedProminent) .tint(.orange) .padding(.horizontal, 40) .padding(.bottom, 24) } } } ``` -------------------------------- ### Build Signed Local App Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-25-settings-sidebar-and-sound-toggle.md Builds a locally signed version of the Ghost Pepper app for debugging and installation. Requires manual code signing configuration. ```sh xcodebuild -project GhostPepper.xcodeproj -scheme GhostPepper -configuration Debug -derivedDataPath build/settings-sidebar-signed -clonedSourcePackagesDirPath build/settings-sidebar-signed-source -skipMacroValidation CODE_SIGN_STYLE=Manual CODE_SIGN_IDENTITY='Developer ID Application: Jesse Vincent (87WJ58S66M)' DEVELOPMENT_TEAM=87WJ58S66M build ``` -------------------------------- ### Commit Onboarding Changes Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-20-onboarding.md Stage and commit the Swift file for the onboarding UI changes. ```bash git add GhostPepper/UI/OnboardingWindow.swift git commit -m "feat: add setup step with permissions and model download" ``` -------------------------------- ### Launch Debug Build Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-04-28-agentic-meeting-qa.md Builds the application, copies it to the Applications folder, and launches it for manual verification. Useful for memory checks. ```bash xcodebuild build -project GhostPepper.xcodeproj -scheme GhostPepper -derivedDataPath build/derived -skipMacroValidation cp -R build/derived/Build/Products/Debug/GhostPepper.app /Applications/ open /Applications/GhostPepper.app ``` -------------------------------- ### Granola-imported Meeting File Format Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/specs/2026-04-28-agentic-meeting-qa-design.md Example of the YAML frontmatter and structure for Granola-imported meeting transcripts. This format is common and includes metadata like title, date, and granola_id. ```yaml --- title: "..." date: "2025-01-29T..." granola_id: "..." source_type: meeting imported_from: granola --- ``` -------------------------------- ### Stage and Commit Onboarding Polish Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-20-onboarding.md Stage all modified files in the repository and commit them with a message indicating the polishing and edge case handling of the onboarding flow. ```bash git add -A git commit -m "feat: polish onboarding flow and handle edge cases" ``` -------------------------------- ### Preferred Grep Binary Selection Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-04-28-agentic-meeting-qa.md Determines the preferred grep binary to use, prioritizing ripgrep installations in common locations before falling back to the system's default grep. ```swift private static func preferredGrepBinary() -> String { let candidates = ["/opt/homebrew/bin/rg", "/usr/local/bin/rg"] for c in candidates where FileManager.default.isExecutableFile(atPath: c) { return c } return "/usr/bin/grep" } ``` -------------------------------- ### Update App Entry Point for Onboarding Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-20-onboarding.md Modify the `.onAppear` block in `GhostPepperApp.swift` to conditionally show the onboarding window based on the `onboardingCompleted` state. This snippet also shows the addition of necessary properties. ```swift @AppStorage("onboardingCompleted") private var onboardingCompleted = false private let onboardingController = OnboardingWindowController() ``` ```swift .onAppear { guard !hasInitialized else { return } hasInitialized = true if onboardingCompleted { Task { await appState.initialize() } } else { onboardingController.show(appState: appState) { Task { await appState.initialize() } } } } ``` -------------------------------- ### Build and Check for Errors Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-20-onboarding.md Compiles the project to verify that the newly added onboarding controller and view do not introduce syntax errors. This command is expected to fail until the step views are defined. ```bash xcodegen generate && xcodebuild -project GhostPepper.xcodeproj -scheme GhostPepper -configuration Debug build -skipMacroValidation 2>&1 | grep -E "error:|BUILD" ``` -------------------------------- ### Meeting Q&A System Prompt Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/specs/2026-04-28-agentic-meeting-qa-design.md The system prompt defines the role, tools, archive structure, and answering strategy for the meeting Q&A assistant. It guides the agent on how to process information and cite sources. ```text You are the meeting Q&A assistant for the user's personal meeting archive. You answer questions about their meetings using three tools: grep, read_file, and list_dir. # Archive layout Root: {ARCHIVE_ROOT} Files are markdown meeting transcripts in YYYY-MM-DD/ folders, with one or more .md files per meeting. Two file formats coexist: 1. **Granola-imported** (most files). Starts with YAML frontmatter: ``` --- title: "..." date: "2025-01-29T..." granola_id: "..." source_type: meeting imported_from: granola --- ``` Followed by an H1 title, then `## Summary` (with `### Subsection` headings), sometimes `## Transcript` with **[HH:MM] Speaker:** lines. Transcripts can be 4,000+ lines. 2. **Native Ghost Pepper** (a smaller fraction — quick notes and window snippets). No frontmatter. Starts with an H1 title, then `**Date:**` line, then `## Notes` with free-form content. Generally short. Both formats are valid. When grep matches a file, check for `---` on line 1 to know which format you're dealing with. # How to answer 1. Always cite your sources as `path:line` or `path:start-end`. Every factual claim needs a citation. If you can't cite it, don't claim it. 2. Prefer grep for names, dates, and exact strings. It's much cheaper than read_file. 3. Use read_file with a small offset/limit to confirm context around a grep match. Read more (up to 1000 lines) only when you need the full meeting. 4. Use list_dir to discover meetings on a specific date or to find date-named folders. 5. Stop searching when you have enough to answer. Don't read every file. # Voice-to-text reasoning Transcripts are voice-to-text with frequent artifacts: misheard names, run-on fragments, dropped words. When a phrase looks garbled, reason about the likely intended meaning from surrounding context. Examples of artifacts you should interpret, not take literally: - "He's not a Quinn Adler for 10 years" almost certainly means "He's known Quinn for 10 years." - "Robin" addressed in a "Dana <> Matt" meeting is most likely Dana being addressed informally — note the discrepancy in your answer. - Names with similar phonemes are often the same person across files. When you interpret an artifact, say so explicitly: "The transcript reads X, which I read as Y because [reason]." # Multi-hop questions For "do X and Y know each other" or similar relationship questions: 1. Search for both names independently. 2. Look for direct co-attendance (both names appearing in the same file's attendees field or transcript). 3. Look for one mentioning the other in a third party's meeting (often the strongest signal in this archive). 4. Cite the strongest evidence. Be honest about what you can and can't conclude. # Iteration budget You have at most 15 tool calls per question. Plan accordingly. Front-load grep calls (cheap, narrow the search), then read selectively. ``` -------------------------------- ### Onboarding View Structure Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-20-onboarding.md The main SwiftUI view for the onboarding wizard, handling navigation between different steps. It uses a switch statement to display the appropriate step based on the `currentStep` state. ```swift struct OnboardingView: View { @ObservedObject var appState: AppState let onComplete: () -> Void @State private var currentStep = 1 var body: some View { VStack { switch currentStep { case 1: WelcomeStep(onContinue: { currentStep = 2 }) case 2: SetupStep(appState: appState, modelManager: appState.modelManager, onContinue: { currentStep = 3 }) case 3: TryItStep(appState: appState, onContinue: { currentStep = 4 }) case 4: DoneStep(onComplete: { UserDefaults.standard.set(true, forKey: "onboardingCompleted") onComplete() }) default: EmptyView() } } .frame(width: 480, height: 520) } ``` -------------------------------- ### Onboarding UI with Recording and Transcription Status Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-20-onboarding.md This SwiftUI code displays the status of a recording and transcription process, including success and failure messages. It's used in the onboarding flow to guide the user. ```swift // Status area VStack(spacing: 12) { if controller.isRecording { HStack(spacing: 8) { Circle() .fill(.red) .frame(width: 10, height: 10) Text("Recording...") .foregroundStyle(.secondary) } } else if controller.isTranscribing { HStack(spacing: 8) { ProgressView() .controlSize(.small) Text("Transcribing...") .foregroundStyle(.secondary) } } else if let text = controller.transcribedText { VStack(spacing: 8) { Text("\"\(text)\"") .font(.body) .italic() .padding() .frame(maxWidth: .infinity) .background( RoundedRectangle(cornerRadius: 10) .fill(Color(nsColor: .controlBackgroundColor)) ) .padding(.horizontal, 24) HStack(spacing: 4) { Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) Text("It works! Your words will be pasted wherever your cursor is.") .font(.callout) .foregroundStyle(.green) } } } else if controller.monitorStartFailed { Text("Could not start hotkey monitor.\nPlease verify Accessibility is enabled in System Settings.") .font(.callout) .foregroundStyle(.red) .multilineTextAlignment(.center) } else { Text("Waiting for you to hold Control...") .foregroundStyle(.secondary) } } .frame(minHeight: 100) Spacer() HStack { Button("Skip") { controller.advance(onAdvance: onContinue) } .buttonStyle(.bordered) Spacer() Button(action: { controller.advance(onAdvance: onContinue) }) { Text("Continue") .font(.headline) .padding(.horizontal, 16) .padding(.vertical, 8) } .buttonStyle(.borderedProminent) .tint(.orange) } .padding(.horizontal, 40) .padding(.bottom, 24) } .onAppear { controller.start(onAdvance: onContinue) } .onDisappear { controller.cleanup() } ``` -------------------------------- ### CalendarEvent Struct Extension Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/specs/2026-04-28-meeting-home-calendar-list-design.md Extends the existing CalendarEvent struct to include new properties for start date, end date, all-day status, and attendee count, facilitating better event filtering and rendering. ```swift struct CalendarEvent { let title: String let startTime: String // existing — ISO string for compat let startDate: Date? // new — parsed let endDate: Date? // new let isAllDay: Bool // new let attendees: [String] let attendeeCount: Int // new — total before email-prefix fallback let organizer: String? let meetLink: String? } ``` -------------------------------- ### Stage and Commit Onboarding Changes Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-20-onboarding.md Stage the modified Swift files related to the onboarding flow and commit them with a descriptive message. ```bash git add GhostPepper/GhostPepperApp.swift GhostPepper/UI/OnboardingWindow.swift git commit -m "feat: wire onboarding into app launch flow" ``` -------------------------------- ### MeetingQAAgent Implementation Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-04-28-agentic-meeting-qa.md The core implementation of the MeetingQAAgent, including its initializer and the `ask` method which sets up the agent's run loop. ```swift import Foundation final class MeetingQAAgent { private let provider: LLMProvider private let model: ClaudeAPIModel private let archiveRoot: URL private let maxIterations: Int private let tools: MeetingQATools private let toolDefinitions: [LLMTool] init(provider: LLMProvider, model: ClaudeAPIModel, archiveRoot: URL, maxIterations: Int = 15) { self.provider = provider self.model = model self.archiveRoot = archiveRoot self.maxIterations = maxIterations self.tools = MeetingQATools(root: archiveRoot) self.toolDefinitions = Self.buildToolDefinitions() } func ask(_ question: String) -> AsyncThrowingStream { AsyncThrowingStream { continuation in let task = Task { [weak self] in guard let self else { continuation.finish(); return } await self.runLoop(question: question, continuation: continuation) } continuation.onTermination = { _ in task.cancel() } } } private func runLoop(question: String, continuation: AsyncThrowingStream.Continuation) async { let systemPrompt = MeetingQASystemPrompt.build(archiveRootPath: archiveRoot.path) var messages: [LLMMessage] = [LLMMessage(role: .user, content: [.text(question)])] var cumulativeUsage = ProviderUsage.zero for iteration in 0.. String { return """ You are the meeting Q&A assistant for the user's personal meeting archive. \ You answer questions about their meetings using three tools: grep, read_file, and list_dir. # Archive layout Root: \(archiveRootPath) Files are markdown meeting transcripts in YYYY-MM-DD/ folders, with one or more .md \ files per meeting. Two file formats coexist: 1. Granola-imported (most files). Starts with YAML frontmatter: --- title: "..." date: "2025-01-29T..." granola_id: "..." source_type: meeting imported_from: granola --- Followed by an H1 title, then ## Summary (with ### subsection headings), \ sometimes ## Transcript with **[HH:MM] Speaker:** lines. Transcripts can be \ 4,000+ lines. 2. Native Ghost Pepper (a smaller fraction — quick notes and window snippets). \ No frontmatter. Starts with an H1 title, then **Date:** line, then ## Notes \ with free-form content. Generally short. Both formats are valid. When grep matches a file, check for `---` on line 1 to know \ which format you're dealing with. # How to answer 1. Always cite your sources as `path:line` or `path:start-end`. Every factual claim \ needs a citation. If you can't cite it, don't claim it. 2. Prefer grep for names, dates, and exact strings. It's much cheaper than read_file. 3. Use read_file with a small offset/limit to confirm context around a grep match. \ Read more (up to 1000 lines) only when you need the full meeting. 4. Use list_dir to discover meetings on a specific date or to find date-named folders. 5. Stop searching when you have enough to answer. Don't read every file. # Voice-to-text reasoning Transcripts are voice-to-text with frequent artifacts: misheard names, run-on \ fragments, dropped words. When a phrase looks garbled, reason about the likely \ intended meaning from surrounding context. Examples of artifacts you should interpret, not take literally: - "He's not a Quinn Adler for 10 years" almost certainly means \ "He's known Quinn for 10 years." - "Robin" addressed in a "Dana <> Matt" meeting is most likely Dana being \ addressed informally — note the discrepancy in your answer. - Names with similar phonemes are often the same person across files. When you interpret an artifact, say so explicitly: "The transcript reads X, which I \ read as Y because [reason]." # Multi-hop questions For "do X and Y know each other" or similar relationship questions: 1. Search for both names independently. 2. Look for direct co-attendance (both names appearing in the same file's \ attendees field or transcript). 3. Look for one mentioning the other in a third party's meeting (often the \ strongest signal in this archive). 4. Cite the strongest evidence. Be honest about what you can and can't conclude. # Iteration budget You have at most 15 tool calls per question. Plan accordingly. Front-load grep \ calls (cheap, narrow the search), then read selectively. """) } } ``` -------------------------------- ### Today's Calendar List UI Mockup Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/specs/2026-04-28-meeting-home-calendar-list-design.md Illustrates the proposed UI for the 'Today' calendar list in the new-tab landing view, showing how all-day events, regular events with start buttons, and attendee counts would be displayed. ```text ────────── Today ────────── [All day: PTO Monday] (rendered as a header chip, no action) 9:00 AM Standup 4 people [▶ Start] 10:30 AM 1:1 with Lara 1 person [▶ Start] 2:00 PM Design review 8 people [▶ Start] ``` -------------------------------- ### Build macOS Scheme Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-04-28-agentic-meeting-qa.md Verifies the project builds successfully after implementing QAEvent and QATranscript. This command checks the macOS scheme and skips macro validation. ```bash xcodebuild build -project GhostPepper.xcodeproj -scheme GhostPepper -destination 'platform=macOS' -skipMacroValidation -quiet ``` -------------------------------- ### Onboarding Window Controller Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-20-onboarding.md Manages the lifecycle and display of the onboarding window. It dismisses any existing onboarding window before showing a new one and handles the completion callback. ```swift // GhostPepper/UI/OnboardingWindow.swift import SwiftUI import AppKit import AVFoundation class OnboardingWindowController { private var window: NSWindow? func show(appState: AppState, onComplete: @escaping () -> Void) { dismiss() let onboardingView = OnboardingView(appState: appState, onComplete: { [weak self] in self?.dismiss() onComplete() }) let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 480, height: 520), styleMask: [.titled], backing: .buffered, defer: false ) window.title = "Ghost Pepper" window.contentView = NSHostingView(rootView: onboardingView) window.center() window.isReleasedWhenClosed = false window.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: true) self.window = window } func dismiss() { window?.close() window = nil } } ``` -------------------------------- ### Force Claude API Backend Migration Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-04-28-agentic-meeting-qa.md Ensures that existing installations of the application are migrated to use the Claude API for agentic Q&A if they were previously set to use the local backend. This code should be added to the `AppState.init` method or a similar first-launch migration hook. ```swift // Migration: agentic Q&A doesn't support local backend. Force Claude API. if meetingQABackend == QABackendKind.local.rawValue { meetingQABackend = QABackendKind.claudeAPI.rawValue } ``` -------------------------------- ### PathSandbox Tests Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-04-28-agentic-meeting-qa.md Unit tests for the PathSandbox class in Swift. These tests cover various scenarios including resolving root paths, valid relative paths, and rejecting invalid paths like parent escapes, absolute paths, and symlinks outside the root directory. Setup involves creating a temporary directory with sample files and teardown removes the temporary directory. ```swift import XCTest @testable import GhostPepper final class PathSandboxTests: XCTestCase { private var rootDir: URL! override func setUpWithError() throws { rootDir = FileManager.default.temporaryDirectory.appendingPathComponent("PathSandboxTests-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory(at: rootDir, withIntermediateDirectories: true) try FileManager.default.createDirectory(at: rootDir.appendingPathComponent("2025-01-29"), withIntermediateDirectories: true) try "hello".write(to: rootDir.appendingPathComponent("2025-01-29/note.md"), atomically: true, encoding: .utf8) } override func tearDownWithError() throws { try? FileManager.default.removeItem(at: rootDir) } func testResolvesRootForEmptyAndDot() throws { XCTAssertEqual(try PathSandbox.resolveSafe("", root: rootDir).path, rootDir.resolvingSymlinksInPath().path) XCTAssertEqual(try PathSandbox.resolveSafe(".", root: rootDir).path, rootDir.resolvingSymlinksInPath().path) } func testResolvesValidRelativePath() throws { let url = try PathSandbox.resolveSafe("2025-01-29/note.md", root: rootDir) XCTAssertTrue(url.path.hasSuffix("/2025-01-29/note.md")) XCTAssertTrue(FileManager.default.fileExists(atPath: url.path)) } func testRejectsParentEscape() { XCTAssertThrowsError(try PathSandbox.resolveSafe("../outside.md", root: rootDir)) { error in guard case PathSandboxError.pathOutsideRoot = error else { XCTFail("Expected pathOutsideRoot, got \(error)") return } } } func testRejectsAbsolutePath() { XCTAssertThrowsError(try PathSandbox.resolveSafe("/etc/passwd", root: rootDir)) { error in guard case PathSandboxError.pathOutsideRoot = error else { XCTFail("Expected pathOutsideRoot, got \(error)") return } } } func testRejectsSymlinkOutsideRoot() throws { let outside = FileManager.default.temporaryDirectory.appendingPathComponent("PathSandboxTests-outside-\(UUID().uuidString).md") try "secret".write(to: outside, atomically: true, encoding: .utf8) defer { try? FileManager.default.removeItem(at: outside) } let symlinkPath = rootDir.appendingPathComponent("link.md") try FileManager.default.createSymbolicLink(at: symlinkPath, withDestinationURL: outside) XCTAssertThrowsError(try PathSandbox.resolveSafe("link.md", root: rootDir)) { error in guard case PathSandboxError.pathOutsideRoot = error else { XCTFail("Expected pathOutsideRoot, got \(error)") return } } } func testAllowsSymlinkInsideRoot() throws { let target = rootDir.appendingPathComponent("2025-01-29/note.md") let symlink = rootDir.appendingPathComponent("alias.md") try FileManager.default.createSymbolicLink(at: symlink, withDestinationURL: target) XCTAssertNoThrow(try PathSandbox.resolveSafe("alias.md", root: rootDir)) } } ``` -------------------------------- ### Commit Onboarding Scaffold Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-20-onboarding.md Stages and commits the newly created Swift file for the onboarding window controller and view, using a conventional commit message for feature implementation. ```bash git add GhostPepper/UI/OnboardingWindow.swift git commit -m "feat: scaffold onboarding window controller and view" ``` -------------------------------- ### Run Full Verification Suite Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-25-transcription-lab.md Execute the full test suite for the Transcription Lab. Ensure CODE_SIGNING_ALLOWED is set to NO for this verification step. ```bash xcodebuild -project GhostPepper.xcodeproj -scheme GhostPepper -derivedDataPath build/transcription-lab-full -skipMacroValidation CODE_SIGNING_ALLOWED=NO test ``` -------------------------------- ### Run Settings Window Shell Tests Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-25-settings-sidebar-and-sound-toggle.md Executes multiple tests related to the settings window's content hosting, close button behavior, and window reuse. This is run after implementing the sidebar shell. ```sh xcodebuild -project GhostPepper.xcodeproj -scheme GhostPepper -derivedDataPath build/settings-shell-red -clonedSourcePackagesDirPath build/settings-shell-red-source CODE_SIGNING_ALLOWED=NO -skipMacroValidation -only-testing:GhostPepperTests/GhostPepperTests/testSettingsWindowHostsSwiftUIViaContentViewController -only-testing:GhostPepperTests/GhostPepperTests/testSettingsWindowControllerCloseButtonOrdersWindowOutWithoutClosing -only-testing:GhostPepperTests/GhostPepperTests/testAppStateShowSettingsReusesSingleWindow test ``` -------------------------------- ### Xcode Build Command for Chord Engine and Hotkey Monitor Testing Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-23-transcription-stack.md This command executes unit tests specifically for the ChordEngine and HotkeyMonitor. It's used to verify the failing state before implementation and the passing state afterward. ```bash xcodebuild -project GhostPepper.xcodeproj -scheme GhostPepper -derivedDataPath build/test-derived -skipMacroValidation CODE_SIGNING_ALLOWED=NO test -only-testing:GhostPepperTests/ChordEngineTests -only-testing:GhostPepperTests/HotkeyMonitorTests ``` -------------------------------- ### Git Add and Commit for Chord Bindings Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-23-transcription-stack.md This command stages and commits the newly created Swift files for the chord bindings feature. It's a standard step after implementing a new feature. ```bash git add GhostPepper/Input/PhysicalKey.swift GhostPepper/Input/KeyChord.swift GhostPepper/Input/ChordAction.swift GhostPepper/Input/ChordBindingStore.swift GhostPepperTests/KeyChordTests.swift GhostPepperTests/ChordBindingStoreTests.swift git commit -m "feat: add chord bindings" ``` -------------------------------- ### Run CleanupModelProbe CLI with 'none' thinking Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-25-cleanup-model-probe.md Executes the CleanupModelProbe CLI with the Qwen fast model, providing input and setting the thinking mode to 'none'. This demonstrates one-shot runs and stage-by-stage output. ```sh ./build/cleanup-probe-cli/Build/Products/Debug/CleanupModelProbe --model fast --input "Okay, it's running now." --thinking none ``` -------------------------------- ### Create Source File Stubs Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-04-28-agentic-meeting-qa.md Creates empty stub files for various QA-related Swift source files using shell redirection. ```bash cat > GhostPepper/QA/LLMProvider.swift <<'EOF' import Foundation // Implemented in Task 8. EOF ``` ```bash cat > GhostPepper/QA/AnthropicProvider.swift <<'EOF' import Foundation // Implemented in Tasks 9-10. EOF ``` ```bash cat > GhostPepper/QA/MeetingQAAgent.swift <<'EOF' import Foundation // Implemented in Task 11. EOF ``` ```bash cat > GhostPepper/QA/MeetingQATools.swift <<'EOF' import Foundation // Implemented in Tasks 3-5. EOF ``` ```bash cat > GhostPepper/QA/PathSandbox.swift <<'EOF' import Foundation // Implemented in Task 2. EOF ``` ```bash cat > GhostPepper/QA/QAEvent.swift <<'EOF' import Foundation // Implemented in Task 7. EOF ``` ```bash cat > GhostPepper/QA/QATranscript.swift <<'EOF' import Foundation // Implemented in Task 7. EOF ``` ```bash cat > GhostPepper/QA/MeetingQASystemPrompt.swift <<'EOF' import Foundation // Implemented in Task 6. EOF ``` -------------------------------- ### Build Signed Local App for Live Validation Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-03-25-transcription-lab.md Build a locally signed application for live validation. This command configures manual code signing and specifies the development team. ```bash xcodebuild -project GhostPepper.xcodeproj -scheme GhostPepper -configuration Debug -derivedDataPath build/transcription-lab-signed -skipMacroValidation CODE_SIGN_STYLE=Manual CODE_SIGN_IDENTITY='Developer ID Application: Jesse Vincent (87WJ58S66M)' DEVELOPMENT_TEAM=87WJ58S66M build ``` -------------------------------- ### Search for QABackendKind and related references Source: https://github.com/matthartman/ghost-pepper/blob/main/docs/superpowers/plans/2026-04-28-agentic-meeting-qa.md Use grep to find references to `QABackendKind`, `meetingQABackend`, and `meetingQAModelKind` in `AppState.swift`. This is to identify and remove obsolete code related to the local QA backend. ```bash grep -n "QABackendKind\|meetingQABackend\|meetingQAModelKind" GhostPepper/AppState.swift ```