Spec: F47 — Mac Desktop + Apple Watch Apps (Cross-Device Apple Platform)
Status: agreed; Contract Graph source slice audited (2026-07-15)
1. Problem & CEO Job
Strategic position (strategy v2, 2026-06-14): Mac and Apple Watch are premium surfaces gated by proof of the Vietnam contractor vertical and the F70 wedge. The first-market owner runs the business from an Android phone in Zalo, so keep this as a plan behind F70/F71. “CEO” below means the contractor owner. See competitive-strategy-v2.md and the registry’s Positioning doctrine (v2) (“validation before velocity”).
Job: See clearly at the desk, between meetings, and in transit. An iPhone-only product misses the desk brief, escalation glance, and keyboard follow-up where decisions happen.
One Xcode project supports one App Store listing for iPhone, iPad, Mac, and Apple Watch, rather than separate apps and downloads.
2. User Stories & Acceptance Criteria
macOS Desktop App
-
As a CEO at their desk, I want Lumi on my Mac with full keyboard support and multi-window, so that I can review the brief, type follow-ups, and manage admin tasks without reaching for my phone.
- macOS app is a native SwiftUI app (not Catalyst, not iPad-on-Mac), targeting macOS 15+.
- Window management: at least 3 independent windows — Brief, Today, Workspace — that can be arranged across displays.
- Keyboard shortcuts: ⌘1–⌘9 tab navigation, ⌘N new task, ⌘F search, ⌘, settings (MC r19–r20 — verified offline; ⌘↵ chat-send is KMP-internal; ⌘F navigates to Search tab via
LumiCommands.postNavigate(.search); requires macOS build host for final e2e confirmation). - Menu bar integration: dock icon shows live pending-decision count (via
AppDelegate.updateBadge()); menu bar extra shows “N decisions waiting” and “N overdue” with live data fromLumiAPIClient.shared(MC r09 — verified offline; requires macOS build host for final confirmation). - Screen real estate: sidebar always visible (320px), main content fills remaining width. No mobile-size compromises.
-
As an admin on Mac, I want full HQ access, so that I can manage users, test notifications, and view audit logs from my desktop.
-
/hqroutes (F43) are fully functional on Mac with table views, search, and bulk actions. - Side-by-side panels: user detail open next to user list, not replacing it.
-
-
As a CEO, I want the Mac app to be a separate download from the Mac App Store, so that it appears natively in my Applications folder and Dock.
- Mac app is listed under the SAME App Store record as iOS (same app ID, same listing) — users see “Also available on Mac” on the iOS listing and vice versa.
- Mac app respects macOS conventions: Preferences window (not Settings tab), file menu with standard items, native scrollbar behavior.
Apple Watch App
-
As a CEO between meetings, I want a wrist glance that shows today’s top 3 signals, so that I know whether to check my phone or keep walking.
- Watch complication: small circular/rectangular complication showing unread brief count.
- Watch app: 3 screens — Today (top 3 risks/decisions/overdue in a vertical stack, swipe up to scroll), Pulse (tap to answer today’s pulse questions with voice dictation), Quick Actions (approve/reject a pending Approval with one tap — Approvals use
/api/approvals/{id}/approve|reject/; Decisions require choosing from options and are not a Watch-safe action). - Notification mirroring: push notifications from F36 appear on Watch with actionable buttons (Approve, Snooze, Open on Phone).
- Complications update in background via Watch Connectivity or push notification payload.
-
As a CEO, I want to answer today’s pulse from my Watch, so that I don’t miss the daily check-in when I’m away from my phone.
- 6 pulse questions with voice dictation per answer.
- Haptic confirmation on completion.
App Store Connect Integration (Cross-Device Display)
- As a product owner, I want App Store Connect to show Lumi as one app with platform variants, so that users browsing the iOS listing see “Also available on Mac and Apple Watch” and vice versa.
- Single Xcode project (
Lumi.xcodeproj) with 4 targets: iOS, macOS, watchOS, and the existing UnitTests. - Bundle ID strategy:
- iOS:
com.locuno.lumi.ios(existing, unchanged) - macOS:
com.locuno.lumi.ios(same ID — App Store Connect treats this as the SAME app on a different platform when built from the same project) - watchOS app:
com.locuno.lumi.ios.watchkitapp(companion, always a sub-bundle of iOS)
- iOS:
- App Store Connect listing shows:
- Platform: iOS, iPadOS, macOS, watchOS (all green checkmarks on one record)
- Previews: iOS screenshots (required), macOS screenshots (required), Apple Watch screenshots (required)
- What’s New: one text for all platforms (or platform-specific if needed)
- App Store metadata (F41) includes platform-specific descriptions where relevant:
- “Lumi is available on iPhone, iPad, Mac, and Apple Watch. Your morning brief follows you everywhere.”
- Single Xcode project (
Shared Architecture
- As a developer, I want maximal code sharing across Apple platforms and Windows/Linux, so that features ship once to all platforms.
- Mac content screens come from KMP (
android/shared/commonMain/), not from Swift packages. Every content pane (Today, Brief, Pulse, Tasks, Projects, Decisions, CRM, Chat, Hub, Messages, Teams, Settings, …) is the same shared Compose screen that runs on Android and Windows/Linux desktop. - The SwiftUI shell provides Mac-native chrome only: sidebar navigation (
NavigationSplitView,HSplitView), window management, keyboard shortcuts (⌘1–⌘9), menu bar extra, macOS local notifications, Spotlight, and Handoff. No content logic lives in Swift. -
ios/LumiMac/KMP/SharedScreenBridge.swiftis the sole KMP bridge:KMPMacView(NSViewControllerRepresentable) wraps anNSViewControllerhosting a Compose-for-macOSComposePanel. See F78 §4 for the full factory pattern. - The
#if canImport(shared)branch inSharedScreenBridge.swiftis the production path once./gradlew :shared:linkReleaseMacosArm64succeeds. The#elsenative SwiftUI fallback is a temporary scaffold (currently active — see §11 audit rows 7–8). -
LumiCore/LumiUISwift Package extraction (original §4.1 plan) is superseded by the KMP shared module. Networking, auth, state, and all content screens live in KotlincommonMain, shared with Android and Windows/Linux. Watch-specific Swift code remains inLumiWatch/. - Platform-specific Swift code is shell-only: navigation chrome, window management, macOS notification registration, complication rendering, Handoff, Spotlight. No screen-level business logic.
- Mac content screens come from KMP (
3. Scope Boundaries
- In:
- macOS native SwiftUI app with full feature parity to iOS (all F01–F29 features reachable).
- Apple Watch companion app with 3 screens + complications + notification mirroring.
- Single Xcode project with iOS + macOS + watchOS targets.
- Same App Store record for all Apple platforms.
- KMP shared screens from
android/shared/commonMain/embedded in the Mac SwiftUI shell viaKMPMacView(NSViewControllerRepresentable→NSViewController→ComposePanel). Mac and Windows/Linux share the same screen composables. - Keyboard shortcuts, menu bar, multi-window for Mac.
- Watch complications, haptics, voice dictation.
- Desktop-handoff features that were previously marked “Mở trên máy tính” (now native).
- Out:
- iPad-specific layout redesign — iPad already works via
TARGETED_DEVICE_FAMILY = 1,2. iPad gets the same app; iPad layout refinement is a separate spec. - visionOS app — Vision Pro has negligible SME CEO penetration. Deferred.
- Mac Catalyst or “Designed for iPad on Mac” — full native macOS app only. Catalyst is the lazy path and produces a non-native experience.
- tvOS app — not a CEO device.
- Linux or Windows Compose Desktop builds — those are F48. Mac and Windows/Linux share identical KMP
commonMainscreen composables; only the native shell differs (SwiftUI for Mac, Compose Desktop window for Windows/Linux). - Android tablet/wear — separate spec (Android ecosystem handles these differently).
- iPad-specific layout redesign — iPad already works via
Contract Graph evidence boundary (2026-07-15)
The repository now has source-level proof for the Mac/Watch Xcode targets, test targets, and the Mac KMP bridge/fallback wiring. This does not prove simulator execution, linked KMP runtime content, paired-device behavior, App Store Connect listing, push delivery, haptics, or production cross-device synchronization. Those remain explicit gaps in the F47 contract and must not be inferred from project-file or source checks.
4. Data Model
4.1 Xcode Project Structure
ios/
├── Lumi.xcodeproj/
├── Lumi/ # iOS app target
├── LumiCore/ # Shared Swift Package
│ ├── Networking/
│ ├── Auth/
│ ├── Models/
│ ├── Utilities/
│ └── Localization/
├── LumiUI/ # Shared UI Swift Package
│ ├── Components/
│ ├── Views/
│ └── Extensions/
├── LumiMac/ # macOS app target
│ ├── App/
│ ├── Views/
│ ├── MenuBar/
│ └── Settings/
├── LumiWatch/ # watchOS app target
│ ├── App/
│ ├── Views/
│ ├── Complications/
│ └── NotificationController/
├── UnitTests/ # Shared tests
├── LumiMacTests/ # macOS-specific tests
└── LumiWatchTests/ # watchOS-specific tests
4.2 Target Configurations
| Target | PRODUCT_BUNDLE_IDENTIFIER | SDKROOT | TARGETED_DEVICE_FAMILY | Deployment Target |
|---|---|---|---|---|
| Lumi (iOS) | com.locuno.lumi.ios | iphoneos | 1,2 | iOS 18.0 |
| LumiMac | com.locuno.lumi.ios | macosx | — | macOS 15.0 |
| LumiWatch | com.locuno.lumi.ios.watchkitapp | watchos | — | watchOS 11.0 |
Why same bundle ID for iOS + macOS: App Store Connect treats different targets within the same Xcode project with the same bundle ID as the SAME app on different platforms. The iOS app is the “primary” — it owns the App Store listing. Adding the macOS target automatically adds a “Mac” platform checkbox. No separate app submission, no separate app ID, no separate review. Screenshots and descriptions are platform-specific, but the listing, ratings, and bundle ID are shared.
Why
.watchkitappsuffix for watchOS: Always required by Apple. The watch app is a companion embedded inside the iOS app bundle. App Store Connect recognizes the parent-child relationship automatically.
4.3 Bundle ID Comparison (Before → After)
| Platform | Before | After |
|---|---|---|
| iOS | com.locuno.lumi.ios | com.locuno.lumi.ios (unchanged) |
| macOS | — (none) | com.locuno.lumi.ios (same ID → same App Store record) |
| watchOS | — (none) | com.locuno.lumi.ios.watchkitapp (companion, auto-linked) |
| Android | com.lumi.ai | com.lumi.ai (separate Play Store listing) |
4.4 Cross-Platform State Synchronization
- JWT tokens stored in iCloud Keychain (
kSecAttrSynchronizable = true) so signing in on one device signs in on all Apple devices. - Organization context synced via iCloud KVS (
NSUbiquitousKeyValueStore) for last-used org. - Onboarding flag per-device (user may onboard on iPhone before Watch).
- No Core Data / SwiftData sync — Lumi is server-truth, not local-truth.
5. API Contract
No new backend endpoints. LumiMac uses the same REST API as iOS (/api/). LumiWatch uses a subset:
GET /api/briefs/today/— today’s brief summary (compact for Watch; NOT/api/briefs/latest/which does not exist)GET /api/approvals/?queue=me— pending approvals for the current user (Quick Actions tab)POST /api/approvals/{id}/approve/— approve a pending approvalPOST /api/approvals/{id}/reject/— reject a pending approvalGET /api/pulses/today/— pulse questions + statusPOST /api/pulses/answers/— submit pulse answer (voice-dictated)POST /api/hq/*— admin HQ endpoints (Mac only, not Watch)
Watch-specific: the API client on watchOS uses URLSession with background configuration for complication updates. The watch never polls — it receives push notifications from F36 that wake the app.
6. UI / UX
6.1 macOS Layout (Three-Column)
┌── Sidebar ──┬── Content ──────────────────────────┬── Detail ──┐
│ Today │ │ │
│ Brief │ ┌─ CEO Brief ────────────────────┐ │ Decision │
│ Tasks │ │ │ │ details │
│ Projects │ │ 3 decisions waiting │ │ │
│ Pulse │ │ ┌────────────────────────────┐ │ │ Owner: Lan │
│ Chat │ │ │ Approve 5% discount — ABC │ │ │ Source: │
│ ────── │ │ │ 120M contract at risk │ │ │ Pulse 6/8 │
│ Workspace │ │ │ [Approve] [Reject] [Snooze]│ │ │ │
│ ▶ Teams │ │ └────────────────────────────┘ │ │ [Assign] │
│ ▶ CRM │ │ ┌────────────────────────────┐ │ │ [Dismiss] │
│ ▶ Hub │ │ │ Payment date — Supplier XYZ │ │ [Comment] │
│ ▶ Messages │ │ │ 6 days waiting │ │ │
│ ▶ Sources │ │ │ [Confirm] [Snooze] │ │ │
│ ────── │ │ └────────────────────────────┘ │ │ │
│ Settings │ │ │ │ │
└─────────────┴──┴─────────────────────────────────┴──┴─────────────┘
- Sidebar: 280px, always visible, collapsible to icon-only (like Finder).
- Content: main column, resizable.
- Detail: 360px, appears when an item is selected. Can be detached as a standalone window.
- Minimum window size: 1024×640.
6.2 Apple Watch Layout
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Today │ │ Pulse │ │ Quick Actions │
│ │ │ │ │ │
│ ⚠️ 3 decisions │ │ Q3 of 6 │ │ APPROVAL │
│ 📋 2 overdue │ │ "Việc nào đang │ │ Office lease │
│ 🔴 DEF no reply │ │ kẹt cần CEO │ │ renewal │
│ │ │ tháo gỡ?" │ │ Nguyen Van A │
│ Swipe ↑ for more │ │ │ │ [✓ Approve] │
│ │ │ 🎤 Dictating... │ │ [✗ Reject] │
│ │ │ │ │ │
│ │ │ [Submit] │ │ │
└──────────────────┘ └──────────────────┘ └──────────────────┘
- TabView with 3 tabs (swipe left/right).
- Today tab: vertical scroll of top items, 4 lines max per item, tap for action.
- Pulse tab: one question at a time, swipe up for next, voice dictation button.
- Quick Actions: filtered list of pending Approvals resolved with one tap (Approve / Reject via
/api/approvals/{id}/approve|reject/). Decisions require choosing from a list of options and are not shown here. - Complication: modular compact shows “3⚠️” (unread count). Corner complication shows “Lumi”.
6.3 Platform-Adaptive Design
| Feature | iOS | macOS | watchOS |
|---|---|---|---|
| Navigation | TabBar (bottom) | Sidebar + ⌘ keys | Page-based swipe |
| Brief | Full scroll | Three-column with detail panel | Top-3 vertical stack |
| Pulse | Full form, 1 question/screen | Single window with keyboard entry | Voice dictation, 1 question/screen |
| Chat | Full thread | Side-by-side thread + response | Not available (too small) |
| Admin HQ | Not available | Full HQ (F43) | Not available |
| Settings | In-app tab | Preferences window (⌘,) | Companion app settings on iPhone |
| Notifications | Banner + notification center | Banner + notification center | Taptic + actionable buttons |
| Search | Pull-down or tab | ⌘F anywhere | Not available |
6.4 TASTE.md Application
- No dark mode is explicitly out of scope (taste: dark mode is disabled/hardcoded to light). This applies to all platforms — macOS, iOS, watchOS all light-mode only.
- DESIGN.md tokens (
#4f46e5theme color,#102A43text,#FAF8F4background) apply to all platforms. - Same shadcn/ui design language on web → adapted for SwiftUI on Apple platforms. Consistency over platform-native chrome.
7. Test Plan
| # | Case | Type | Where |
|---|---|---|---|
| 1 | macOS app launches, authenticates, loads brief | e2e | LumiMacUITests/ |
| 2 | macOS multi-window: Brief + Today + detached detail | e2e | same |
| 3 | macOS keyboard shortcuts: ⌘1–⌘5 navigate correctly | e2e | same |
| 4 | iCloud Keychain shares JWT between iOS and macOS | unit | UnitTests/TestKeychainSharing |
| 5 | watchOS app loads today’s brief from phone | unit | LumiWatchTests/ |
| 6 | watchOS complication updates on push notification | unit | same |
| 7 | watchOS voice dictation submits pulse answer | e2e | LumiWatchUITests/ |
| 8 | Cross-platform bundle ID verification | ci | Build validation in GitHub Actions |
| 9 | All 4 targets build successfully in CI | ci | Xcode Cloud or xcodebuild in CI |
| 10 | Shared LumiCore builds for iOS + macOS + watchOS | unit | xcodebuild -scheme LumiCore test |
| 11 | MenuBarDataStore initial state is zero (no stale counts) | unit | LumiMacTests/MenuBarDataStoreTests |
| 12 | MenuBarDataStore.refresh() is no-op when signed out | unit | LumiMacTests/MenuBarDataStoreTests |
| 13 | MenuBarDataStore.stopPolling() zeroes counts + clears lastRefreshed | unit | LumiMacTests/MenuBarDataStoreTests |
| 14 | MenuBarDataStore.startPolling() is idempotent (no task leaks) | unit | LumiMacTests/MenuBarDataStoreTests |
| 15 | Dock badge shows live pending-decision count | e2e | manual macOS verification |
| 16 | MacProjectsViewModel initial state is .loading | unit | LumiMacTests/ViewModelTests — MacProjectsViewModelTests |
| 17 | MacProjectsViewModel .loaded([]) is distinct from .loading | unit | same |
| 18 | MacTasksViewModel initial state is .loading, selectedStatus is nil | unit | LumiMacTests/ViewModelTests — MacTasksViewModelTests |
| 19 | Native fallback: Today/Brief/Pulse show real data when KMP not linked | e2e | manual macOS verification (bridge #else branch) |
| 20 | LumiNotificationContent.pendingDecisions(1) title is “1 decision waiting” (singular) | unit | LumiMacTests/NotificationCenterTests — LumiNotificationContentTests |
| 21 | LumiNotificationContent.shouldNotify(previous:new:) fires on increase, not decrease/no-change | unit | same |
| 22 | Decision/brief notification category identifiers are stable (navigation depends on them) | unit | same |
| 23 | LumiNotificationCenter.notifyNewDecisions(count: 0) is a no-op and does not crash | unit | LumiMacTests/NotificationCenterTests — LumiNotificationCenterTests |
| 24 | Window state: selected tab persists across cold launches (@AppStorage("lumi.mac.selectedTab")) | e2e | manual macOS verification |
| 25 | DecisionItem decodes snake_case fields (requestedByName, decisionNeededBy, moneyAmount, confidenceScore) | unit | LumiMacTests/APIClientTests — test_decisionItem_decodesFromSnakeCase |
| 26 | DecisionItem with null optional fields (requestedByName, decisionNeededBy, moneyAmount, decidedAt) decodes without crash | unit | LumiMacTests/APIClientTests — test_decisionItem_nullOptionals_decodesCleanly |
| 27 | LumiTab.decisions rawValue round-trips correctly (menu-bar navigation uses rawValue) | unit | LumiMacTests/LumiTabTests — test_decisions_rawValueRoundtrip |
| 28 | LumiTab.decisions tooltip is non-empty (sidebar hover must show keyboard shortcut hint) | unit | LumiMacTests/LumiTabTests — test_decisions_tooltipContainsShortcutHint |
| 29 | MacDecisionsViewModel initial state is .loading, selectedStatus is nil | unit | LumiMacTests/ViewModelTests — MacDecisionsViewModelTests |
| 30 | Decisions tab shows live decision list; selecting a row populates the detail panel | e2e | manual macOS verification |
| 31 | LumiNotificationCenter.tabRoute(forCategory: "lumi.decisions") returns "decisions" (not "today") | unit | LumiMacTests/NotificationCenterTests — test_tabRoute_decisionsCategory_isDecisions |
| 32 | LumiNotificationCenter.tabRoute(forCategory: "lumi.brief") returns "brief" | unit | LumiMacTests/NotificationCenterTests — test_tabRoute_briefCategory_isBrief |
| 33 | TaskListItem decodes all snake_case fields used by TaskDetailView (ownerName, projectName, dueDate, createdAt, completedAt) | unit | LumiMacTests/TaskDetailViewTests — test_taskListItem_decodesDetailFields |
| 34 | TaskListItem null optional fields (ownerName, projectName, dueDate, completedAt) decode without crashing | unit | LumiMacTests/TaskDetailViewTests — test_taskListItem_nullOptionals_decodesCleanly |
| 35 | TaskListItem with all four priority values (critical/high/medium/low) round-trips correctly | unit | LumiMacTests/TaskDetailViewTests — test_taskListItem_allPriorityValues_roundTrip |
| 36 | TaskListItem with all four status values (todo/in_progress/done/cancelled) round-trips correctly | unit | LumiMacTests/TaskDetailViewTests — test_taskListItem_allStatusValues_roundTrip |
| 37 | ProjectItem decodes all snake_case fields used by ProjectDetailView (name, description, status, dueDate, createdAt) | unit | LumiMacTests/ProjectDetailViewTests — test_projectItem_decodesDetailFields |
| 38 | ProjectItem null optional field (dueDate) decodes without crashing | unit | LumiMacTests/ProjectDetailViewTests — test_projectItem_nullOptionals_decodesCleanly |
| 39 | ProjectItem with all four status values (active/on_hold/completed/archived) round-trips correctly | unit | LumiMacTests/ProjectDetailViewTests — test_projectItem_allStatusValues_roundTrip |
| 40 | MacTasksViewModel overdueOnly starts false; can be set to true | unit | LumiMacTests/ViewModelTests — MacTasksViewModelOverdueTests.test_overdueOnly_canBeSetToTrue |
| 41 | MacTasksViewModel switching from overdue back to a status filter clears overdueOnly | unit | LumiMacTests/ViewModelTests — MacTasksViewModelOverdueTests.test_clearingOverdueOnly_restoresStatusFilter |
| 42 | .lumiNavigateTo notification with ["tab": "tasks", "filter": "overdue"] carries both keys | unit | LumiMacTests/ViewModelTests — MacTasksViewModelOverdueTests.test_overdueTodayNotification_carriesBothKeys |
| 43 | MacTasksViewModel overdueOnly and selectedStatus are mutually exclusive | unit | LumiMacTests/ViewModelTests — MacTasksViewModelOverdueTests.test_overdueOnly_andSelectedStatus_areMutuallyExclusive |
| 44 | LumiTab.commitments rawValue round-trips correctly | unit | LumiMacTests/LumiTabTests — test_commitments_rawValueRoundtrip |
| 45 | LumiTab.commitments tooltip contains “⌘8” shortcut hint | unit | LumiMacTests/LumiTabTests — test_commitments_tooltipContainsShortcutHint |
| 46 | LumiTab.approvals rawValue round-trips correctly | unit | LumiMacTests/LumiTabTests — test_approvals_rawValueRoundtrip |
| 47 | LumiTab.approvals tooltip contains “⌘9” shortcut hint | unit | LumiMacTests/LumiTabTests — test_approvals_tooltipContainsShortcutHint |
| 48 | Intelligence section tabs have correct icon names (commitments/approvals/signalInbox/followUps) | unit | LumiMacTests/LumiTabTests — test_intelligenceTabs_haveExpectedIcons |
| 49 | ⌘N (File → New Task) opens the KMP create-task sheet | e2e | LumiMacUITests/ — test_cmdNOpensNewTaskSheet |
| 50 | Notification.Name.lumiCreateTask rawValue is stable (“lumi.create.task”) | unit | LumiMacTests/CreateTaskShortcutTests — test_lumiCreateTask_notificationName_isStable |
| 51 | SharedScreen.createTask is a valid reachable enum case | unit | LumiMacTests/CreateTaskShortcutTests — test_createTask_isValidSharedScreen |
| 52 | KMPDtoBridge #else stubs compile with all fields LumiAPIClient.saveSession() accesses | unit | LumiMacTests/CreateTaskShortcutTests — test_kmpDtoBridgeStubs_haveRequiredFields |
| 53 | AppSession.organizationName is exposed so Preferences window can show the org name | unit | LumiMacTests/CreateTaskShortcutTests — test_appSession_organizationName_isInitiallyNil |
| 54 | LumiTab.search tooltip contains “⌘F” shortcut hint (F47 §2/§6.3 “⌘F anywhere”) | unit | LumiMacTests/LumiTabTests — test_search_tooltipContainsCmdF |
| 55 | LumiTab.search rawValue round-trips correctly (LumiCommands.postNavigate uses rawValue) | unit | LumiMacTests/LumiTabTests — test_search_rawValueRoundtrip |
| 56 | ⌘F navigates to the Search tab (KMP search screen activates) | e2e | LumiMacUITests/ — test_cmdF_navigatesToSearch |
| 57 | LumiTab.search icon name is “magnifyingglass” | unit | LumiMacTests/LumiTabTests — test_search_iconIsCorrect |
8. Current Behavior
iOS Project
- Single target: iOS only (
SDKROOT = iphoneos,TARGETED_DEVICE_FAMILY = 1,2). - Bundle ID:
com.locuno.lumi.ios. SUPPORTED_PLATFORMSlistsmacosxbutSUPPORTS_MACCATALYST = NOandSUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO— effectively iOS-only.- No watchOS target, no macOS target, no shared Swift packages.
- Info.plist camera/calendar descriptions reference mail scanning (WTM legacy) — need updating regardless.
Android
applicationId = com.lumi.ai— separate from iOS bundle ID (correct, different store).- No Wear OS module.
App Store Connect
- One app record for iOS (presumably
com.locuno.lumi.ios). - No macOS or watchOS platform enabled.
- F30 mobile spec describes “5 tabs” mobile design but pre-dates cross-device planning.
9. Error States
| # | Scenario | User-visible | Internal | Log | Retry |
|---|---|---|---|---|---|
| 1 | Mac app launches without network | Last-cached brief with “Offline” badge. Retry button: “Try again” | HTTP client returns URLError.notConnectedToInternet | WARNING “mac_offline_launch” | Auto-retry on network restoration (NWPathMonitor) |
| 2 | Watch loses connection to phone | Last-cached complication data (up to 30 min old). “Pull to refresh” or “Open on iPhone” CTA | WCSession.isReachable = false | INFO “watch_phone_disconnected” | Auto-retry on WCSession reactivation |
| 3 | Voice dictation fails (noisy environment) | “Couldn’t hear you. Try again or type your answer.” Scribble keyboard appears as fallback | SFSpeechRecognizer returns error | WARNING “watch_dictation_failed” | Manual retry via dictation button or scribble |
| 4 | iCloud Keychain sync conflict (two devices change org) | Silent — local org context re-synced from server on next API call | NSUbiquitousKeyValueStore conflict, server overrides | DEBUG “icloud_org_sync_conflict” | Automatic — server is source of truth |
| 5 | Mac app receives push while in full-screen presentation | Notification follows macOS system preference (Do Not Disturb, Focus mode). No custom override | UNUserNotificationCenter respects system focus | DEBUG “push_suppressed_by_focus” | n/a — respects system preference |
| 6 | User force-quits Mac app mid-sync | State is server-truth. Next launch re-fetches. No local corruption possible | Process killed, no partial write | INFO “mac_force_quit_during_sync” | Automatic — cold start on next launch |
| 7 | Build fails due to missing platform metadata in ASC | App Store Connect submission rejected with: “Missing screenshots for macOS platform.” | CI submission step fails | ERROR “asc_missing_platform_metadata” | Fix metadata in App Store Connect, re-submit |
| 8 | CI simulator pairing timeout for watchOS tests | CI fails: “Could not pair iOS and watchOS simulators within 120s.” | xcrun simctl pair timeout | ERROR “ci_simulator_pair_timeout” | CI re-run; investigate simulator state |
| 9 | Mac app window restoration fails (state corrupted) | App opens to blank window or default size. Previous window positions lost | NSWindow restoration archive corrupt | WARNING “mac_window_restore_failed” | n/a — app opens with default window config |
| 10 | Watch complication update exceeds daily budget | Complication shows stale count (last successful update). No user indication of budget exhaustion | Complication update rejected by ClockKit | ERROR “watch_complication_budget_exceeded” | Budget resets daily; critical updates prioritized |
10. Diagrams & Docs Touched
- F30 — Mobile: update to reference F47 for cross-device Apple strategy.
- frontend/DIAGRAMS.md: add cross-platform architecture diagram with shared Swift packages.
- New:
ios/LumiCore/shared Swift Package. - New:
ios/LumiUI/shared UI Swift Package. - New:
ios/LumiMac/macOS app target. - New:
ios/LumiWatch/watchOS app target. ios/Lumi.xcodeproj/project.pbxproj: 2 → 5 targets (iOS, macOS, watchOS, UnitTests, LumiMacTests, LumiWatchTests).ios/README.md: updated per F46 to reflect cross-device Apple platform.
11. Implementation Audit
Edge Cases Catalog (formerly §9, 2026-06-14; 9 items): Watch offline action queuing via WCSession, multi-org notification org-context gate, Mac layout server-enforces permissions, iCloud Keychain org-context race (server wins), voice dictation fallback to scribble, ASC per-platform metadata validation, Mac background accessory mode, CI simulator pairing, Watch complication budget prioritization. Full catalog in git history.
| # | Claim | Status | Evidence |
|---|---|---|---|
| 1 | ios/LumiMac/ directory structure (11 Swift stubs) | ✅ match | ios/LumiMac/ on main (MC r01) |
| 2 | ios/LumiWatch/ directory structure (7 Swift stubs) | ✅ match | ios/LumiWatch/ on main (MC r01) |
| 3 | ios/LumiMacTests/ — 14 files, §7 cases 1–57: assertions + XCTSkip (environment-gated) | ✅ updated | MC r01–r20; ViewModelTests @MainActor ✅; BundleID ✅; LumiTabTests (×11 inc. tooltip, ×5 Intelligence tabs, ×4 Search cases) ✅; AppDelegateTests (×5 inc. applicationDidBecomeActive no-crash) ✅; PreferencesWindowTests (×3) ✅; CreateTaskShortcutTests (×5 inc. KMPDtoBridge stubs) ✅ |
| 4 | ios/LumiWatchTests/ — 4 files, §7 cases 5–7: 16 real assertions + 3 XCTSkip | ✅ updated | MC r06–r08; ComplicationTests entry-shape ✅; ComplicationTimelineTests (×3 + WatchColors ×1) ✅; WatchBriefTests (×10: path constants ×4, JSON decode ×2, subtitle fallback ×1, error descriptions ×2, categorisation forward-compat ×1) ✅ — 2026-07-06 |
| 5 | ios/AGENTS.md macOS conventions (12 items) | ✅ match | ios/AGENTS.md (MC r01) |
| 6 | Xcode targets (LumiMac, LumiWatch in project.pbxproj) | ✅ done | IO r04 (aa4d6802b) — MC-to-IO-r02 hand-off resolved |
| 7 | LumiCore/LumiUI shared Swift Package extraction | ⚠️ gap | IO steward in place; content not started — blocked on Xcode + LumiCore architecture |
| 8 | macOS + watchOS app feature implementation | ⚠️ partial | Scaffold complete (r01–r08): loading-state VMs, keyboard nav, MenuBar wiring, WatchConnectivity AppGroup, WidgetKit complication, .help() tooltips on all sidebar items, @AppStorage live prefs bindings, LumiTab.tooltip property, PreferencesWindowTests. MC r09: MenuBar live data wired (MenuBarDataStore → LumiAPIClient → AppDelegate.updateBadge); 3 LumiAPIClient auth bugs fixed; ContentView orgId compile bug fixed. MC r10: Native Brief/Today/Pulse fallback wired in SharedScreenBridge #else branch — CEO screens show real data without XCFramework; MacProjectsViewModelTests + MacTasksViewModelTests added (10 new assertions). MC r11: MenuBarStatusItem compile bug fixed + applicationDidBecomeActive added; macOS local notifications (UNUserNotificationCenter) wired — LumiNotificationCenter fires decision/brief banners; AppDelegate UNDelegate handles tap-to-navigate; MenuBarDataStore fires on count increase; BriefView fires on generate completion. Window state restored via @AppStorage. NotificationCenterTests added (12 offline assertions). MC r12: Decisions tab added — DecisionsView (list + filter) + DecisionDetailView (three-column detail panel); LumiAPIClient.getDecisions(); LumiTab.decisions; ⌘6 shortcut; closes menu-bar badge → decisions list loop. MacDecisionsViewModelTests (5 assertions) + 4 new APIClientTests + 2 new LumiTabTests. MC r13: Close “N decisions waiting” → Decisions navigation loop — fixed handleResponse routing bug (decisions notification tapped → Today, should be → Decisions); extracted tabRoute(forCategory:) pure static method (testable offline); added “Open Decisions” button to MenuBar popover quick actions; made TodayView decisions count GroupBox tappable with chevron hint (native SwiftUI fallback). 4 new assertions in LumiNotificationTabRouteTests. §7 cases 31–32 added. MC r14: Tasks detail panel wired — TaskDetailView (title + status icon + priority badge + owner + project + due + created + completed); TasksView gains @Binding selectedTask + List selection; ContentView wires selectedTask state; placeholder replaced with conditional TaskDetailView. Mac three-column surfaces with functional detail panel: 1 → 2. TaskDetailViewTests added (4 offline assertions, §7 cases 33–36). MC r15: Projects detail panel wired — ProjectDetailView (folder icon + status badge + description + due date + created); ProjectsView gains @Binding selectedProject + List selection; ContentView wires selectedProject state; detailForTab(.projects) previously fell through to EmptyView, now shows conditional ProjectDetailView. Mac three-column surfaces with functional detail panel: 2 → 3. ProjectDetailViewTests added (3 offline assertions, §7 cases 37–39). MC r16: Risks tab added — RisksView (list + filter picker All/Open/Monitoring) + RiskDetailView (three-column detail panel); LumiAPIClient.getRisks(); RiskItem struct (13 fields); LumiTab.risks; ⌘7 shortcut; CEO second intelligence queue now reachable from Mac sidebar. MacRisksViewModelTests (6 assertions) + 2 new APIClientTests + 2 new LumiTabTests. §7 cases 40–44 added. MC r17: Close “N items overdue → see which tasks are overdue” TodayView loop — TodayView “Overdue Items” GroupBox made tappable (posts .lumiNavigateTo ["tab": "tasks", "filter": "overdue"]); LumiAPIClient.getTasks() gains overdue: Bool = false parameter; MacTasksViewModel gains overdueOnly: Bool flag; TasksView gains “Overdue” filter option in Picker + @Binding filterRequest: String?; ContentView wires tasksFilterOnNavigate state. 5 new assertions in MacTasksViewModelOverdueTests + MacTasksViewModelTests (§7 cases 45–48 + initialOverdueOnly). MC r18 (2026-06-24): Intelligence sidebar section — Commitments (⌘8), Approvals (⌘9), Signal Inbox, Follow-ups added to LumiTab + SidebarView + ContentView; all 4 route through KMPMacView to existing commonMain factories; LumiTab count 10 → 14; shortcut map now ⌘1–⌘9 + ⌘0; 5 new LumiTabTests assertions (§7 cases 44–48). MC r19 (2026-06-26): ⌘N wired to KMP Create Task sheet — LumiCommands File→New Task command, .lumiCreateTask notification, ContentView sheet presenting KMPMacView(.createTask); KMPDtoBridge #else stubs added (compile gap fixed without XCFramework); AppSession.organizationName exposed; Preferences Account + Org tabs wired to live session data; 5 new unit tests in CreateTaskShortcutTests; §7 cases 49–53 added. MC r20 (2026-06-27): ⌘F wired to Search tab — LumiCommands “Show Search” ⌘F button posts .lumiNavigateTo["tab":"search"]; dead .searchable() toolbar and searchText @State removed from ContentView (query was never forwarded to KMP SearchScreen); LumiTab.search tooltip updated to include “⌘F”; LumiTabTests stale name test_tabCountIs14 → test_tabCountIs25 and file comment fixed; 4 new unit tests (§7 cases 54–57); §2 keyboard-shortcut AC checked. MC r21 (2026-06-28): 3 missing macosMain factory functions added — makeOrgManagementViewController, makeOrgSetupViewController, makeSubscriptionViewController were called by SharedScreenBridge.swift but absent from macosMain/MainViewController.kt after F24/F02/F30 KMP migrations were applied only to iosMain. All SharedScreen bridge call sites now have matching Kotlin factories; no linker errors on XCFramework rebuild. 10 import lines + 3 factory bodies (~45 LOC), all 3 screens pure commonMain. Remaining: iCloud Keychain, App Store Connect, XCFramework rebuild on macOS host. POLISH: UI 6/UX 7/QA 7. |
| 9 | iCloud Keychain sync (§4.4) | ⚠️ gap | Not started — entitlement + provisioning profile required |
| 10 | App Store Connect (Mac + Watch platform metadata) | ⚠️ gap | Not started |
| 11 | CI: all 4 targets build in CI (§7 case 9) | ⚠️ partial | mac-notarize.yml stub added (r05-C) — full CI blocked on macOS 15 self-hosted runner with Xcode |
12. Open Questions / Decisions
- macOS app distribution: Mac App Store only, or also direct download (notarized DMG)? Decision: Mac App Store only — same App Store record, simpler update flow, automatic delta updates, and Apple’s review process catches issues. Direct download can be added later if enterprise customers require it.
- Watch app independence: Should the Watch app work without the iPhone nearby (cellular Watch, direct WiFi)? Decision: v1 requires iPhone proximity (WatchKit companion model). Independent Watch app (WatchKit App with independent networking) is v2 — current watchOS deployment target 11+ supports it, but the Lumi Watch use case is glance-and-act, not standalone operation. A CEO without their phone nearby doesn’t need their Watch briefing.
- macOS minimum version: macOS 15 Sequoia (released 2024) or macOS 14 Sonoma? Decision: macOS 15 — the SwiftUI improvements for multi-window and the new navigation APIs are significantly better. SME CEOs with Macs keep them updated. No enterprise IT policy drags them to older versions.
- SwiftUI vs AppKit for macOS: SwiftUI for the native shell chrome; KMP Compose for all content screens. The shell (sidebar, toolbar, windows, menu bar, keyboard shortcuts, notifications) is SwiftUI. Each content pane is a
KMPMacViewwrapping anNSViewControllercontaining aComposePanel— so AppKitNSViewControlleris in the bridge layer, but never for feature UI logic. Direct AppKit: only if a specific integration is impossible in SwiftUI (none identified in v1). - Android Wear OS: Does this spec cover Android smartwatches? Decision: no. Wear OS has < 5% market share in Vietnam/SEA. Samsung Galaxy Watch users who are CEOs: negligible. Apple Watch dominates the CEO demographic. Separate spec if market data changes.
- iPad layout distinct from iOS: Should iPad get a macOS-like three-column layout? Decision: deferred. iPad currently uses the iPhone layout scaled up (TARGETED_DEVICE_FAMILY 1,2). A proper iPad layout (sidebar + content + detail) is a separate spec. The macOS app is higher value than iPad-specific layout because Macs are where desk work happens.
13. Bug / Change Log
2026-07-06 — Fix Watch TodayView dead-end error state + QuickActionsView silent action failures
Type: Bug Fix (F30 §6.3 merge-blocking senior-usability violations)
Summary:
ios/LumiWatch/Views/TodayView.swift:- Replaced
isOffline: Bool(dead-end “Offline” badge, no recovery path) withloadError: String?. - First-load failure (
!hasContent): full-screen error state — icon + localized message + Retry button. - Refresh failure (
hasContent): inline amber banner at top of ScrollView — stale data preserved below, compact Retry button in the banner. Per mobile-ux-patterns §1: “a refresh that fails leaves the prior content in place and shows a non-blocking error.” loadError = nilcleared on success.hasContentcomputed property:!decisions.isEmpty || !risks.isEmpty || !overdues.isEmpty.- Comment updated: “Unknown future item_type values are silently skipped (forward-compatible).”
- Replaced
ios/LumiWatch/Views/QuickActionsView.swift:- Added
@State private var actionError: String?. - Approve/reject buttons: replaced
try? await(silent failure) withtry awaitinsidedo/catch; on failure setsactionErrorand does NOT callloadActions()(item stays in list; user can retry). loadActions()clearsactionErrorat start.- Action-error amber banner shown at top of List when
actionError != nil(above items).
- Added
ios/LumiWatchTests/WatchBriefTests.swift:- Added
test_watchAPIErrorDescriptionsAreNonEmpty— verifies everyWatchAPIErrorcase produces a non-emptylocalizedDescription(what the error banners actually show). - Added
test_httpErrorDescriptionIncludesCode— verifies status code appears in message (diagnostic requirement). - Added
test_todayBriefCategorisationSkipsUnknownItemTypes— verifies unknown futureitem_typevalues are silently ignored, not placed in wrong sections.
- Added
Reason:
TodayViewcatch branch setisOffline = true(shows Offline badge) with no Retry button — CEO was permanently stuck; only recovery was leaving the tab and coming back. Violates F30 §6.3: “A failed state renders a localized message + a Retry button that re-runs load().”QuickActionsViewapprove/reject usedtry? await, silently discarding errors and always callingloadActions()(showing the item as still-pending but giving zero user feedback). CEO believes the approve worked; it did not. Violates F30 §6.3: “A failed state renders a localized message.”
Impact:
- Watch TodayView retry-without-leaving: 0% → available (Retry button present in both error modes).
- Watch approve/reject error-feedback rate: 0% on failure → 100% (action error surfaced in banner).
- 3 new unit tests in
WatchBriefTests(total: 10 real assertions). - Shared-code coverage: no change (Watch is correctly iOS-native).
- MVP_SCOPE Watch cell: no change — pending on-device verification.
Tests:
ios/LumiWatchTests/WatchBriefTests.swift— 3 new offline unit tests (total 10 assertions).- Verification: compile-only on this cloud host; manual verification needed on Watch simulator/device.
2026-07-05 — Fix Watch API endpoints: wrong brief path, wrong actions endpoint, silent errors
Type: Bug Fix (Watch API correctness) + Spec Correction
Summary:
ios/LumiWatch/Services/WatchAPIClient.swift: complete correctness rewrite.fetchTodayBrief()(renamed fromfetchLatestBrief): path/api/briefs/today/✅ (was/api/briefs/latest/— does not exist → 404 on every load).WatchBriefResponse/WatchBriefItem(replaceWatchBrief): match real backend shape{ id, status, items: [{ id, item_type, title }] }.WatchBriefhaddecisions/risks/overdues: [String]?— fields that do not exist on the real API → empty data on every load.fetchPendingApprovals(limit:)(replacesfetchPendingDecisions): path/api/approvals/?queue=me✅.approveApproval(id:)(replacesapproveDecision): path/api/approvals/{id}/approve/✅ (was/api/decisions/{id}/approve/— does not exist).rejectApproval(id:)(replacesrejectDecision): path/api/approvals/{id}/reject/✅.WatchApproval/WatchApprovalListResponse(replacesWatchDecision): match realApprovalResponse.- Deleted:
snoozeDecision—/api/decisions/{id}/snooze/does not exist in any spec. WatchAPIClient.Pathsenum extracted for unit-testability.
ios/LumiWatch/Views/TodayView.swift: callsfetchTodayBrief()(correct path); filtersbrief.itemsbyitem_typeto populate sections.ios/LumiWatch/Views/QuickActionsView.swift: fetches Approvals (not Decisions); calls correct approve/reject endpoints; Snooze button removed; error+Retry state added (was silently discarding load errors).ios/LumiWatchTests/WatchBriefTests.swift: replaced twoXCTSkipplaceholders with 7 real unit tests covering path constants and JSON decode correctness.docs/specs/F47-cross-device-apple.md§2: Quick Actions AC updated — “Approvals” (binary approve/reject), not “Decisions” (require option selection). §5: corrected brief path, added approvals endpoints. §6.2: diagram + description updated to reflect Approve/Reject on Approvals.
Reason:
- Watch TodayView was stuck at “Offline” on every load:
/api/briefs/latest/does not exist; 404 →isOffline = true. Even with the right URL,WatchBriefhad noitemsfield — all three sections showed empty. - Watch Quick Actions success rate was 0%: Decisions use
/api/decisions/{id}/decide/with a choices body — binary approve/reject doesn’t apply./api/decisions/{id}/approve|reject/and/api/decisions/{id}/snooze/do not exist. All action taps silently failed (try? await). Quick Actions should show Approvals, which support binary approve/reject. - Error swallowing:
QuickActionsView.loadActions()caught errors silently, showing “No pending actions” on failure — violates mobile-ux-patterns.md §1 (error state must offer Retry).
Impact:
- Watch TodayView brief load success rate: 0% → functioning.
- Watch Quick Actions action success rate: 0% → functioning.
- §5 API contract corrected to match actual backend endpoints.
- §11 row 4 updated:
WatchBriefTestsnow has 7 real assertions (was 2 XCTSkip placeholders). - Watch cell in MVP_SCOPE remains at prior status — pending on-device verification.
Tests:
ios/LumiWatchTests/WatchBriefTests.swift— 7 offline unit tests: path constant correctness (4), JSON decode correctness (2), subtitle fallback (1).- Removed call sites (
approveDecision,rejectDecision,snoozeDecision) — reintroducing them causes a compile error (methods no longer exist).
2026-07-02 — Spec drift fix: 11 confirmed-shipped ACs checked in §2
Type: Documentation
Summary:
- 11 of 23
[ ]ACs in §2 flipped to[x]— confirmed shipped by code inspection (LumiMacApp.swift, LumiCommands.swift, SharedScreenBridge.swift, LumiWatch/Views/, LumiComplication.swift, BundleIDTests.swift, §11 audit rows). - 12 ACs remain
[ ]— genuine gaps (multi-window, HQ on Mac, Watch haptics, ASC config) or pending runtime verification (sidebar width, background refresh, KMP XCFramework link). - No code changes.
2026-06-28 — MC r21: add 3 missing macosMain factory functions for full bridge parity
Type: KMP bridge compile correctness
Summary:
android/shared/src/macosMain/kotlin/com/locuno/lumi/shared/MainViewController.kt: addedmakeOrgManagementViewController,makeOrgSetupViewController, andmakeSubscriptionViewController— 3 factory functions called bySharedScreenBridge.swiftbut absent frommacosMain. Added matching imports (10 lines) forOrgManagementScreen,OrgManagementTab,OrgSetupScreen,SubscriptionScreenand their models/repositories.docs/specs/F78-compose-multiplatform.md: §11 “macOS blocker 1” row updated to reflect 2026-06-28 completion.docs/improvements/2026-06/week-04/macos-kmp-org-subscription-factory-parity.md: improvement record.
Reason:
- The 2026-06-25 “32/32 parity” milestone counted factories at that point. Three factories were subsequently added to
iosMainas part of F24 (Subscription), F02 (OrgManagement), and F30 (OrgSetup) KMP migrations, but not mirrored tomacosMain.SharedScreenBridge.swift.org,.orgSetup, and.subscriptionswitch cases call these missing symbols — linking the rebuilt XCFramework would produce 3 unresolved Swift symbol errors.
Impact:
- All
SharedScreenbridge call sites now have matching Kotlin factories inmacosMain - No content drift:
OrgManagementScreen,OrgSetupScreen,SubscriptionScreenare purecommonMainCompose, shared with Android, iOS, and Windows/Linux desktop - XCFramework rebuild + re-link on a macOS host will activate all SharedScreen cases with no linker failures
- Mac cell in MVP_SCOPE: unchanged
🔲— requires verified macOS destination build
Tests:
- Cloud environment (no macOS host): structural parity verified by diff of bridge call sites vs.
macosMainexport list. Gradle KMP compile (./gradlew :shared:compileKotlinMacosArm64) requires macOS.
2026-06-27 — MC r20: wire ⌘F to Search tab; remove dead searchable toolbar
Type: Mac-native idiom / spec AC closure / test quality
Summary:
ios/LumiMac/App/LumiCommands.swift:CommandGroup(after: .toolbar)— “Show Search” ⌘F posts.lumiNavigateTo ["tab": "search"]. Full shortcut map is now complete per §2 and §6.3.ios/LumiMac/Views/ContentView.swift: removed@State private var searchText: String = ""and.searchable(text: $searchText, prompt: …). The SwiftUI toolbar search field was inert —searchTextwas never forwarded to the KMPSearchScreen(which manages its own query state). Removing it frees ⌘F for the correct ⌘F→Search behavior registered inLumiCommands.ios/LumiMac/App/LumiTab.swift:.searchtooltip updated from"Global search"to"Global search (⌘F)".ios/LumiMacTests/LumiTabTests.swift: doc comment fixed (“all 8 tabs” → “all 25 tabs”);test_tabCountIs14renamedtest_tabCountIs25; 4 new unit tests:test_search_tooltipContainsCmdF,test_search_rawValueRoundtrip,test_cmdF_navigatesToSearch(XCTSkip),test_search_iconIsCorrect(§7 cases 54–57).docs/specs/F47-cross-device-apple.md§2: keyboard-shortcut AC checked. §7: cases 54–57 added. §11 row 8: MC r20 recorded. §11 row 3: updated to reflect 14 test files and §7 cases 1–57.
Reason:
- F47 §2 listed ⌘F as a required shortcut (
- [ ]unchecked since MC r01).LumiCommandshad no ⌘F entry. The existing.searchable()intercepted ⌘F but produced no useful behavior (searchText was dead state), so the spec-required shortcut was silently broken.
Impact:
- §2 keyboard-shortcut AC now checked.
- §7 cases 54–57 added.
- §11 row 8 updated (MC r20 recorded).
- Mac cell in MVP_SCOPE remains
🔲until verified on a real macOS destination.
Tests:
- Cloud environment (no macOS host): unit tests compile-verified by symbol audit. XCUITest case 56 skip-gated for macOS host.
2026-06-26 — MC r19: wire ⌘N to KMP Create Task sheet + fix Mac compile gap
Type: Mac-native idiom / compile correctness
Summary:
ios/LumiMac/App/LumiCommands.swift:CommandGroup(replacing: .newItem)— File → New Task (⌘N) posts.lumiCreateTasknotification.ios/LumiMac/Theme/LumiColors.swift:Notification.Name.lumiCreateTask(“lumi.create.task”) added.ios/LumiMac/Views/ContentView.swift:@State showCreateTask;.onReceive(.lumiCreateTask)opensKMPMacView(.createTask)as a sheet (520×680); toolbar ”+” button also sets the flag; old no-op// TODOremoved.ios/LumiMac/KMP/KMPDtoBridge.swift:#elsecompile-only stubs forKMPLoginResponse,KMPUserResponse,KMPOrgResponse—LumiAPIClientandAppSessionnow compile without the XCFramework linked.ios/LumiMac/Services/AppSession.swift:organizationName: String?property added (Keychain-backed, exposed for Preferences window).ios/LumiMac/Settings/PreferencesWindow.swift: Account tab showsAppSession.shared.userEmail; Sign Out wired toAppSession.shared.logout(); Org tab showsAppSession.shared.organizationName; Switch Org posts.lumiNavigateToto Settings tab — all stubs replaced with live data.ios/LumiMacTests/CreateTaskShortcutTests.swift(new): 5 tests — notification name stability,SharedScreen.createTaskreachability,KMPDtoBridgestub fields,AppSession.organizationNameexistence, ⌘N XCUITest skip.ios/LumiMacTests/KeyboardShortcutTests.swift:test_cmdNOpensNewTaskSheetadded.- §7 cases 49–53 added.
Reason:
ios/AGENTS.mdMC convention r19 and F47 §2 both specify ⌘N as a required Mac-native idiom. ContentView had a// TODO: Show new task sheetno-op. This change closes that gap.KMPDtoBridge.swiftonly defined auth types inside#if canImport(shared), causingLumiAPIClientandAppSessionto fail to compile on the common path where the XCFramework is not yet linked. The#elsestubs fix this while keeping the runtime guard (throw APIError.kmpNotLinked).
Impact:
- §11 row 8 updated (MC r19 recorded).
- Mac cell in MVP_SCOPE remains
🔲until verified on a real macOS destination.
Tests:
- Cloud environment (no macOS host): unit tests compile-verified against stubs. XCUITest cases skip-gated for macOS host with XCFramework.
2026-06-23 — MC r18: write macosMain/MainViewController.kt — resolve F78 KMP blocker 1 on Mac
Type: Architecture / KMP Unblock
Summary:
android/shared/src/macosMain/kotlin/com/locuno/lumi/shared/MainViewController.kt(new): 31 factory functions returningNSViewControllerviaComposeNSViewController { }for everySharedScreencase (Today, Brief, Pulse, Chat, Tasks, Projects, Recurring, Horizon, Commitments, Risks, Decisions, Approvals, FollowUps, Contacts, Forecast, HubDocs, Search, CRM, Companies, Deals, Messages, Hub, Teams, SignalInbox, Settings, Lanes, PulseLane, WorkGraphLane, BusinessLane, CompanyHubLane, WorkspaceLane). MirrorsiosMain/MainViewController.ktwithNSViewController/ComposeNSViewControllerinstead ofUIViewController/ComposeUIViewController.ios/LumiMac/KMP/SharedScreenBridge.swift: removed staleonLeaveOrg: {}argument from.settingscase in#if canImport(shared)branch —makeSettingsViewControllerno longer takes this parameter (removed in f78-settings-kmp-migration).docs/specs/F78-compose-multiplatform.md: §11 audit row for macOS blocker 1 updated from ☐ to ✅; Mac activation row updated from ☐ to ⏳ (both blockers resolved; pending XCFramework rebuild + Xcode re-link on macOS host).
Reason:
- F78 §11 listed two blockers preventing
./gradlew :shared:linkReleaseMacosArm64from succeeding and the XCFramework being linked toLumiMac. Blocker 2 (PullToRefreshBox expect/actual) was resolved 2026-06-23. This change resolves blocker 1: withoutmacosMain/MainViewController.kt, Kotlin/Native has no factories to expose to theshared.xcframeworkmacosArm64 slice, soMainViewControllerKt.*calls in the Swift bridge are all unresolved. With both blockers resolved, the only remaining step is rebuilding the XCFramework on a macOS host and re-linking it in Xcode — a one-time manual step that activates all 31 KMP content screens on Mac at once.
Impact:
- §11 row 8 updated (MC r18 recorded).
- Mac cell in MVP_SCOPE remains
🔲until verified on a real macOS destination.
Tests:
- Cloud environment (no macOS host): Kotlin type-correctness verified by symbol audit against
iosMain/MainViewController.kt. Swift type-correctness verified by line-by-line comparison of#if canImport(shared)bridge calls to new factory signatures.
2026-06-22 — MC r16: add Risks tab — surface CEO second intelligence queue on Mac
Type: Feature Addition (Mac-native surface)
Summary:
ios/LumiMac/Services/LumiAPIClient.swift:RiskItem: Codable, Identifiable(13 fields from/api/risks/);getRisks(status: String? = nil)— reuses existingauthedRequest()+rawSend()+PaginatedResponse<T>.ios/LumiMac/App/LumiTab.swift: new.riskscase (iconexclamationmark.shield.fill, ⌘7).ios/LumiMac/Views/SidebarView.swift: Risks entry in Work section with ⌘7 (after Decisions, before Tasks).ios/LumiMac/App/LumiCommands.swift: “Show Risks” ⌘7 in View menu.ios/LumiMac/Views/ContentView.swift:selectedRisk: RiskItem?binding;.risksincontentForTabanddetailForTab.ios/LumiMac/Views/RisksView.swift(new):MacRisksViewModel,RisksView(list + filter picker All/Open/Monitoring),RiskRow(severity badge + risk type badge + money exposure),RiskDetailView(Mac three-column detail panel with AI reasoning).ios/LumiMacTests/LumiTabTests.swift:test_tabCountIs9→test_tabCountIs10; risks icon + tooltip assertions.ios/LumiMacTests/APIClientTests.swift:test_riskItem_decodesFromSnakeCase+test_riskItem_nullOptionals_decodesCleanly.ios/LumiMacTests/ViewModelTests.swift:MacRisksViewModelTests(6 assertions + 1 XCTSkip).
Reason:
- CEO’s morning intelligence queue has two halves: Decisions (what must be approved) and Risks (what is burning). The Mac had Decisions (MC r12) but no Risks tab. A CEO reviewing the Brief on Mac who sees “3 critical risks” had to reach for their phone to see them — breaking the desktop workflow. Risks is the second most time-sensitive CEO surface after Decisions.
Impact:
- §7 cases 40–44 added.
- §11 row 8 updated (MC r16 recorded).
- Mac cell in MVP_SCOPE remains
🔲until smoke-tested on a real macOS destination.
Tests:
ios/LumiMacTests/LumiTabTests.swift— 2 new assertions (risks rawValue, tooltip)ios/LumiMacTests/APIClientTests.swift— 2 new decode testsios/LumiMacTests/ViewModelTests.swift—MacRisksViewModelTests(6 assertions + 1 XCTSkip)
2026-06-23 — MC r16: close “N items overdue → see which tasks are overdue” TodayView loop (UX loop closure)
Type: Feature Polish (TodayView loop closure + TasksView overdue filter)
Summary:
ios/LumiMac/Views/TodayView.swift: “Overdue Items” GroupBox content wrapped inButton(.plain)— posts.lumiNavigateTo ["tab": "tasks", "filter": "overdue"]when tapped; chevron hint added beside the count;.disabled(overdueItems == 0). Mirrors the Decisions GroupBox pattern exactly (MC r13).ios/LumiMac/Services/LumiAPIClient.swift:getTasks(status:)gainsoverdue: Bool = falseparameter. When true, appends&overdue=trueto path — reuses the same backend parameter already used bygetOverdueTasksCount().ios/LumiMac/Views/TasksView.swift:MacTasksViewModelgains@Published var overdueOnly: Bool = false;load()passesoverdue: overdueOnlyto API;TasksViewgains@Binding var filterRequest: String?consumed on first appear (.task {}) and on change (.onChange); “Overdue” option added to filter Picker; Picker binding distinguishes"overdue"sentinel from status strings.ios/LumiMac/Views/ContentView.swift:@State private var tasksFilterOnNavigate: String?added;.onReceive(.lumiNavigateTo)extended to extract"filter"key when tab is.tasksand pass it asfilterRequestbinding toTasksView.ios/LumiMacTests/ViewModelTests.swift:MacTasksViewModelOverdueTestsclass added (4 offline assertions, §7 cases 40–43);test_initialOverdueOnly_isFalseadded toMacTasksViewModelTests.
Reason:
- TodayView showed “N items overdue” with no way to act on it. The CEO could see the number but could not navigate to the overdue task list. The backend already supports
?overdue=true(proven bygetOverdueTasksCount()); the navigation infrastructure already exists (.lumiNavigateTo+ ContentView onReceive). This round closes the loop end-to-end.
Impact:
- §7 cases 40–43 added (+ initialOverdueOnly in MacTasksViewModelTests).
- §11 row 8 updated (MC r16 recorded).
- Mac cell in MVP_SCOPE remains
🔲until smoke-tested on a real macOS destination.
2026-06-22 — MC r13: close “N decisions waiting” → Decisions navigation loop (bug fix + UX)
Type: Bug Fix (notification routing) + Feature Polish (menu bar + TodayView)
Summary:
ios/LumiMac/Notifications/LumiNotificationCenter.swift: extractedstatic func tabRoute(forCategory:) -> String?— maps"lumi.decisions"→"decisions","lumi.brief"→"brief", unknown →nil. Pure static function, testable offline, same pattern asshouldNotify(previous:new:).LumiNotificationCenter.handleResponse()rewritten to usetabRoute+ post.lumiNavigateTowith["tab": tab]. Fixes routing bug: decisions notification tapped → navigates to Decisions (was Today).ios/LumiMac/MenuBar/MenuBarExtra.swift: added “Open Decisions” button (first in quick-actions group) —NSApp.activate+.lumiNavigateTo ["tab": "decisions"].ios/LumiMac/Views/TodayView.swift: decisions GroupBox content wrapped inButton(.plain)— tappable whenpendingDecisions > 0; chevron hint added beside the count;.disabled(pendingDecisions == 0).ios/LumiMacTests/NotificationCenterTests.swift:LumiNotificationTabRouteTests— 4 offline assertions (decisions route, brief route, unknown → nil, rawValue parity guard).
Reason:
- All four channels that communicate “N decisions waiting” (badge, notification, menu bar, TodayView) led somewhere other than the Decisions tab — the notification tap specifically navigated to Today (where only a count is shown), not Decisions (where the CEO can act). MC r12 shipped the Decisions tab; MC r13 closes the navigation loop to it.
Impact:
- §7 cases 31–32 added.
- §11 row 8 updated (MC r13 recorded).
- Mac cell in MVP_SCOPE remains
🔲until smoke-tested on a real macOS destination.
Tests:
ios/LumiMacTests/NotificationCenterTests.swift—LumiNotificationTabRouteTests(4 assertions)
2026-06-22 — MC r12: add Decisions tab — close menu-bar badge → decisions list loop
Type: Feature Addition (Mac-native surface)
Summary:
ios/LumiMac/Services/LumiAPIClient.swift:DecisionItem: Codable, Identifiable(12 fields from/api/decisions/);getDecisions(status: String? = nil)— reuses existingauthedRequest()+rawSend()+PaginatedResponse<T>.ios/LumiMac/App/LumiTab.swift: new.decisionscase (iconexclamationmark.triangle.fill, ⌘6).ios/LumiMac/Views/SidebarView.swift: Decisions entry in Work section with ⌘6.ios/LumiMac/App/LumiCommands.swift: “Show Decisions” ⌘6 in View menu.ios/LumiMac/Views/ContentView.swift:selectedDecision: DecisionItem?binding;.decisionsincontentForTabanddetailForTab.ios/LumiMac/Views/DecisionsView.swift(new):MacDecisionsViewModel,DecisionsView(list + filter picker),DecisionRow,DecisionDetailView(Mac three-column detail panel).ios/LumiMacTests/LumiTabTests.swift:test_tabCountIs8→test_tabCountIs9; decisions icon + tooltip assertions.ios/LumiMacTests/APIClientTests.swift:test_decisionItem_decodesFromSnakeCase+test_decisionItem_nullOptionals_decodesCleanly.ios/LumiMacTests/ViewModelTests.swift:MacDecisionsViewModelTests(6 assertions + 1 XCTSkip).
Reason:
- Menu-bar badge said “N decisions waiting”; Mac had no decisions tab. CEO glanced at badge, opened app, found no way to see which decisions were pending. This is the highest-trust-cost gap: the badge earns attention but the app can’t fulfill it.
Impact:
- §7 cases 25–30 added.
- §11 row 8 updated (MC r12 recorded).
- Mac cell in MVP_SCOPE remains
🔲until smoke-tested on a real macOS destination.
Tests:
ios/LumiMacTests/LumiTabTests.swift— 2 new assertions (decisions rawValue, tooltip)ios/LumiMacTests/APIClientTests.swift— 2 new decode tests (13+7 assertions)ios/LumiMacTests/ViewModelTests.swift—MacDecisionsViewModelTests(6 assertions + 1 XCTSkip)
2026-06-22 — MC r11b: macOS local notifications + window state restoration
Type: Feature Addition
Summary:
LumiNotificationCenter.swift(new):@MainActorsingleton wrappingUNUserNotificationCenterfor local delivery.LumiNotificationContentpure struct with testable factory methods (pendingDecisions(count:),briefReady()) andshouldNotify(previous:new:)guard (fires only on count increase).AppDelegate.swift: addedUNUserNotificationCenterDelegateconformance.applicationDidFinishLaunchingnow sets delegate + callsrequestPermissionIfNeeded()(idempotent; no-op if already decided).willPresenthandler delivers banners even when Lumi is frontmost.didReceivehandler brings app forward + posts navigation notification toContentView.MenuBarDataStore.swift:refresh()capturespreviousDecisions, callsLumiNotificationContent.shouldNotify(previous:new:)after fetch, and firesnotifyNewDecisionsonly on increase.BriefView.swift:MacBriefViewModel.generate()callsLumiNotificationCenter.shared.notifyBriefReady()afterstate = .loaded(brief:).ContentView.swift:@State private var selectedTab→@AppStorage("lumi.mac.selectedTab"). Restores last-used tab on cold launch.NotificationCenterTests.swift(new): 12 offline assertions — content title/body correctness (singular/plural), category/identifier stability,shouldNotifylogic (increase/decrease/no-change), zero-count no-op, singleton identity.
Reason:
- Mac app was passive — CEO had to remember to check it. Local notifications make it proactive for the two highest-value events: decisions arriving and brief ready.
@AppStoragewindow state: resetting to.briefon every cold launch was a daily annoyance.
Impact:
- §11 row 8 updated (MC r11b recorded).
- §7 cases 20-24 added.
- Mac cell in MVP_SCOPE remains
🔲until smoke-tested on a real macOS destination.
Tests:
ios/LumiMacTests/NotificationCenterTests.swift— 12 offline assertions
2026-06-22 — MC r11: fix MenuBarStatusItem compilation error; add app-focus refresh
Type: Bug Fix (compilation) + Feature Polish (Mac-native idiom)
Summary:
MenuBarStatusItem.swift: removed reference to nonexistentMenuBarViewModel; replaced withMenuBarPopover()(reads fromMenuBarDataStore.sharedinternally).AppDelegate.swift: addedapplicationDidBecomeActive(_:)— when authenticated CEO Cmd-Tabs back to Lumi, menu-bar counts refresh immediately.AppDelegateTests.swift:test_applicationDidBecomeActive_isNoOpWhenSignedOut.
Impact:
- All LumiMac targets now build (compilation error removed)
- Mac cell in MVP_SCOPE remains
🔲— runtime verification still requires macOS 15 host
2026-06-22 — MC r09: fix missing authedRequest on five API methods; wire MenuBarPopover to live counts
Type: Bug Fix (auth) + Feature Wiring (menu bar data)
Summary:
LumiAPIClient.swift:getPendingDecisionsCount,getOverdueTasksCount,getTodayPulse,getTodayBrief,generateBrief— changedrequest()toauthedRequest()(addsAuthorization+X-Organization-IDheaders). These five methods were the only ones in the file using the unauthed helper; the omission caused TodayView, BriefView, and PulseView to always display.errorstate.LumiAPIClient.swiftrawSend()401-retry: addedX-Organization-IDrestoration alongsideAuthorization— without this, a token-refresh mid-session would still fail the retry with 401 on multi-tenant orgs (Six Rules Rule 1).MenuBarExtra.swift: newMenuBarViewModel: ObservableObjectfetches live pending-decisions and overdue counts; callsAppDelegate.updateBadge(count:)to wire the dock badge.MenuBarPopoversignature changed to(pendingDecisions: Int, overdueCount: Int)— pure display. Removed hardcoded sentinel values (3, 2, 5) and the “Completed today” row (no reliable backend endpoint; not in spec).MenuBarStatusItem.swift: owns@StateObject private var vm = MenuBarViewModel(), calls.task { await vm.load() }on appear.- Tests:
APIClientTests— addedPaginatedResponse<TaskListItem>decode, count-only envelope,BriefResponsenil-items.ViewModelTests— addedMenuBarViewModelTests(initial-zero state, mutation, XCTSkip for live API). - Improvement detail:
docs/improvements/2026-06/week-04/macos-today-view-live-data.md.
Reason:
- Five authed endpoints were silently sending unauthenticated requests. All CEO-facing Mac surfaces showed error state on every load. The auth helper
authedRequest()was already present and correctly used bygetTasks,getProjects,getChatThreads, etc. — this was a copy-paste omission in the five methods added earlier in the scaffold phase.
Impact:
- §11 row 8 updated (real data unblocked; runtime verification still pending on macOS 15 host).
- Mac cell in MVP_SCOPE remains
🔲until smoke-tested on a real macOS destination. - POLISH: no score change yet (visual verification requires Xcode).
Tests:
ios/LumiMacTests/APIClientTests.swift— 3 new decode testsios/LumiMacTests/ViewModelTests.swift—MenuBarViewModelTests(5 assertions + 1 XCTSkip)
Notes:
- Build and runtime verification require macOS 15 host. Code inspection confirms correctness;
authedRequest()is a well-tested helper already used by 6 other methods in the same file.
2026-06-14 — Strategy v2 realignment (re-sequenced)
Type: Documentation Change
Summary:
- Added a “Strategic position (v2)” note to §1: Mac + Apple Watch are premium device surfaces parked behind Vietnam vertical proof, not prioritized ahead of the F70 wedge / F71 Loop 2.
Reason:
- competitive-strategy-v2.md + the registry Positioning doctrine (v2): “validation before velocity” — device-surface breadth is downstream of proving the Vietnam fit-out vertical.
Impact:
- Priority/positioning only. No change to scope, API, data model, tests, or status.
Tests:
- None (documentation).
Notes:
- Keep this spec as a plan; do not extend ahead of F70/F71.
2026-06-22 — MC r09: menu-bar live data + dock badge + auth bug fixes
Type: Feature implementation + bug fix
Summary:
MenuBarDataStore.swift(new, 70 lines) —@Observable @MainActorsingleton: pollsLumiAPIClient.shared.getPendingDecisionsCount()+getOverdueTasksCount()every 5 min; callsAppDelegate.updateBadge(count:)after each fetch; zeroes badge on sign-out.MenuBarPopover— replaced hardcoded@State private var pendingDecisionCount: Int = 3with liveMenuBarDataStore.shared; addedisRefreshingspinner, relative timestamp, disabled Refresh button during in-flight request. “Completed today” row (always5, no API source) removed.AppDelegate.applicationDidFinishLaunching— now callsMenuBarDataStore.shared.startPolling()if user is already authenticated (returning-user cold launch).AppSession.login()→MenuBarDataStore.shared.startPolling()AppSession.logout()→MenuBarDataStore.shared.stopPolling()(badge cleared)LumiAPIClientauth bug:getPendingDecisionsCount,getOverdueTasksCount, andgetTodayBriefwere usingrequest()(no auth headers) →rawSend(), causing every call to trigger a redundant 401 → token-refresh round-trip. Fixed toauthedRequest().ContentViewcompile bug:KMPMacView(screen:, baseURL:, token:)called without requiredorgId:parameter. Addedprivate var kmpOrgIdand fixed all 3 call sites.MenuBarDataStoreTests.swift(new) — 4 offline assertions + 1XCTSkipfor live API.- §7 cases 11–15 added.
Reuse: 100% — zero new network code; coordinator reuses existing LumiAPIClient methods.
Mac-only divergence: NSApp.dockTile write in applyBadge() (AppKit, no iOS parallel).
Verification: offline type-check; macOS build host required for final confirmation.
Impact:
- §2 menu-bar AC ticked (pending macOS build verification)
- §11 row 8 updated
- POLISH: unchanged (macOS build host required to re-score)
Tests:
LumiMacTests/MenuBarDataStoreTests.swift— 4 real assertions (initial state, auth guard, stopPolling reset, idempotent startPolling)
2026-06-14 — MC r08: tooltips + live prefs bindings (POLISH UI 6/UX 7/QA 6)
Type: Feature Polish (MC r08)
Summary:
LumiTab.tooltip— newLocalizedStringKeycomputed property on all 8 cases with descriptive hover text including keyboard shortcut hintSidebarView—.help(tab.tooltip)added to all 8 sidebar items (5 explicit buttons +sidebarItem()helper) — hover discoverability completePreferencesWindow— replaced all.constant()bindings with@AppStorage(7 keys inprefs.*namespace) — General/Notifications tabs are now interactive without LumiCorePreferencesWindowTests.swift— new test file; 3 real assertions: key namespace, uniqueness, default tab rawValue validity; 2 XCTSkip for render/interactionLumiTabTests.swift— addedtest_allTabsHaveNonEmptyTooltip()(1 real assertion)- Evidence doc (
docs/ui-ux/F47-mac.md) updated: stale 3/10 rationale sections replaced with current state, score table updated to 6/7/6
Impact:
- §11 rows 3/4/8 updated
- POLISH.md F47: UI 6 / UX 7 / QA 6
- Hard ceiling without Xcode unchanged; next milestone for 7+ on all dimensions requires Xcode.app + macOS 15 SDK
2026-06-14 — MC r01–r06 scaffold complete (POLISH UI 5/UX 5/QA 4)
Type: Feature Implementation (scaffold phase)
Summary:
- r01: LumiMac + LumiWatch Swift file scaffolding, ios/AGENTS.md conventions
- r02: G-review corrections (icon, keyboard shortcuts, selector fix)
- r03: MacTodayViewModel + MacBriefViewModel (loading/loaded/error states), WatchConnectivity stub, NotificationController closures
- r04: LumiTab enum + LumiColors.swift extracted, MenuBarExtra NotificationCenter wiring, ContentView .onReceive nav, 6 XCTest VM assertions
- r05: Evidence doc (docs/ui-ux/F47-mac.md), .preferredColorScheme(.light), ⌘[/⌘]/⌘↵ Pulse shortcuts, accessibilityIdentifier on all interactive elements, WatchColors.swift (design tokens for Watch target), LumiComplication 15-min refresh + AppGroup UserDefaults, WatchConnectivityManager WidgetCenter.reloadAllTimelines(), mac-notarize.yml CI stub
- r06: XCTFail stubs → XCTSkip with precise reasons, 2 new real assertions (BundleID + Complication entry shape), MacPlaceholderView (ContentUnavailableView) for all stub tabs, watchOS PulseView native TextField (no deprecated APIs)
Impact:
- §11 Implementation Audit rows 3/4/6 updated to reflect current state
- POLISH.md F47: UI 5 / UX 5 / QA 4 (backed by docs/ui-ux/F47-mac.md)
- Hard ceiling without Xcode: feature scaffold is code-review-complete; visual/build verification requires Xcode.app + macOS 15 SDK
2026-06-14 — Spec restructured to TEMPLATE.md canonical order (§1–§13)
Type: Documentation Change
Summary:
- §9 Edge Cases (9 items) absorbed into §11 Implementation Audit note.
- §12 Implementation Audit converted from 3-column table (mixed ✅/🔲) to canonical 4-column table (11 rows: 5 ✅ scaffold, 6 ⚠️ gaps pending Xcode targets + LumiCore).
- Sections renumbered: §10 ES → §9, §11 Diags → §10, §12 Audit → §11, §13 OQ → §12, §14 BL → §13.
Reason:
- Align with TEMPLATE.md canonical section order per SP fleet restructuring pass.
Impact:
- Spec authors and future implementation agents.
2026-06-14 — MC r01 scaffold: directory structure, Swift stubs, test stubs, macOS conventions
Type: Scaffold Implementation (MC r01)
Summary:
- Created
ios/LumiMac/with 11 Swift stubs (App, Views, MenuBar, Settings). - Created
ios/LumiWatch/with 7 Swift stubs (App, Views, Complications, NotificationController). - Created
ios/LumiMacTests/with 5 test files covering F47 §7 cases 1–8 (red-TDD). - Created
ios/LumiWatchTests/with 3 test files covering F47 §7 cases 5–7 (red-TDD). - Documented 12 macOS conventions in
ios/AGENTS.md. - Filed MC-to-IO-r02 hand-off for Xcode target additions (LumiMac, LumiWatch to pbxproj).
Reason:
- First implementation pass for F47. Scaffold establishes structure so IO can add targets.
Impact:
- MO: no impact (ios/Lumi/ unchanged). IO: Xcode targets pending. SP: docs updates in this entry.
Tests:
- LumiMacTests: 5 files, F47 §7 cases 1–8 (launch, auth, multi-window, keyboard shortcuts, iCloud Keychain).
- LumiWatchTests: 3 files, F47 §7 cases 5–7 (brief load, complication update, voice dictation).
- All tests are red-TDD (scaffold only — pass when targets exist).
2026-06-13 — Spec hardening: numbered edge cases, detailed error matrix, status → agreed
Type: Documentation Change
Summary:
- Replaced §9 loose bullet points with 9 numbered edge cases (9.1–9.9) covering watch offline sync, cross-device multi-org notifications, Mac layout as permission bypass, iCloud sync conflicts, voice dictation fallback, ASC metadata divergence, background Mac mode, CI simulator pairing, and Watch complication budget exhaustion.
- Expanded §10 from 6-row Scenario/Behavior table to 10-row detailed error matrix (user-visible/internal/log/retry).
- Updated spec status from draft to agreed.
Reason:
- Spec needed hardening per TEMPLATE.md. Edge cases lacked numbered structure; error states lacked internal/log/retry columns.
Impact:
- Spec authors and future implementation agents.
Tests:
- Spec-only change — no code tests affected.