agreed; Contract Graph source slice audited (2026-07-15)

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 from LumiAPIClient.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.

    • /hq routes (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)
    • 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.”

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.swift is the sole KMP bridge: KMPMacView (NSViewControllerRepresentable) wraps an NSViewController hosting a Compose-for-macOS ComposePanel. See F78 §4 for the full factory pattern.
    • The #if canImport(shared) branch in SharedScreenBridge.swift is the production path once ./gradlew :shared:linkReleaseMacosArm64 succeeds. The #else native SwiftUI fallback is a temporary scaffold (currently active — see §11 audit rows 7–8).
    • LumiCore/LumiUI Swift Package extraction (original §4.1 plan) is superseded by the KMP shared module. Networking, auth, state, and all content screens live in Kotlin commonMain, shared with Android and Windows/Linux. Watch-specific Swift code remains in LumiWatch/.
    • Platform-specific Swift code is shell-only: navigation chrome, window management, macOS notification registration, complication rendering, Handoff, Spotlight. No screen-level business logic.

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 via KMPMacView (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 commonMain screen 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).

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

TargetPRODUCT_BUNDLE_IDENTIFIERSDKROOTTARGETED_DEVICE_FAMILYDeployment Target
Lumi (iOS)com.locuno.lumi.iosiphoneos1,2iOS 18.0
LumiMaccom.locuno.lumi.iosmacosx—macOS 15.0
LumiWatchcom.locuno.lumi.ios.watchkitappwatchos—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 .watchkitapp suffix 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)

PlatformBeforeAfter
iOScom.locuno.lumi.ioscom.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)
Androidcom.lumi.aicom.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 approval
  • POST /api/approvals/{id}/reject/ — reject a pending approval
  • GET /api/pulses/today/ — pulse questions + status
  • POST /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

FeatureiOSmacOSwatchOS
NavigationTabBar (bottom)Sidebar + ⌘ keysPage-based swipe
BriefFull scrollThree-column with detail panelTop-3 vertical stack
PulseFull form, 1 question/screenSingle window with keyboard entryVoice dictation, 1 question/screen
ChatFull threadSide-by-side thread + responseNot available (too small)
Admin HQNot availableFull HQ (F43)Not available
SettingsIn-app tabPreferences window (⌘,)Companion app settings on iPhone
NotificationsBanner + notification centerBanner + notification centerTaptic + actionable buttons
SearchPull-down or tab⌘F anywhereNot 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 (#4f46e5 theme color, #102A43 text, #FAF8F4 background) 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

#CaseTypeWhere
1macOS app launches, authenticates, loads briefe2eLumiMacUITests/
2macOS multi-window: Brief + Today + detached detaile2esame
3macOS keyboard shortcuts: ⌘1–⌘5 navigate correctlye2esame
4iCloud Keychain shares JWT between iOS and macOSunitUnitTests/TestKeychainSharing
5watchOS app loads today’s brief from phoneunitLumiWatchTests/
6watchOS complication updates on push notificationunitsame
7watchOS voice dictation submits pulse answere2eLumiWatchUITests/
8Cross-platform bundle ID verificationciBuild validation in GitHub Actions
9All 4 targets build successfully in CIciXcode Cloud or xcodebuild in CI
10Shared LumiCore builds for iOS + macOS + watchOSunitxcodebuild -scheme LumiCore test
11MenuBarDataStore initial state is zero (no stale counts)unitLumiMacTests/MenuBarDataStoreTests
12MenuBarDataStore.refresh() is no-op when signed outunitLumiMacTests/MenuBarDataStoreTests
13MenuBarDataStore.stopPolling() zeroes counts + clears lastRefreshedunitLumiMacTests/MenuBarDataStoreTests
14MenuBarDataStore.startPolling() is idempotent (no task leaks)unitLumiMacTests/MenuBarDataStoreTests
15Dock badge shows live pending-decision counte2emanual macOS verification
16MacProjectsViewModel initial state is .loadingunitLumiMacTests/ViewModelTests — MacProjectsViewModelTests
17MacProjectsViewModel .loaded([]) is distinct from .loadingunitsame
18MacTasksViewModel initial state is .loading, selectedStatus is nilunitLumiMacTests/ViewModelTests — MacTasksViewModelTests
19Native fallback: Today/Brief/Pulse show real data when KMP not linkede2emanual macOS verification (bridge #else branch)
20LumiNotificationContent.pendingDecisions(1) title is “1 decision waiting” (singular)unitLumiMacTests/NotificationCenterTests — LumiNotificationContentTests
21LumiNotificationContent.shouldNotify(previous:new:) fires on increase, not decrease/no-changeunitsame
22Decision/brief notification category identifiers are stable (navigation depends on them)unitsame
23LumiNotificationCenter.notifyNewDecisions(count: 0) is a no-op and does not crashunitLumiMacTests/NotificationCenterTests — LumiNotificationCenterTests
24Window state: selected tab persists across cold launches (@AppStorage("lumi.mac.selectedTab"))e2emanual macOS verification
25DecisionItem decodes snake_case fields (requestedByName, decisionNeededBy, moneyAmount, confidenceScore)unitLumiMacTests/APIClientTests — test_decisionItem_decodesFromSnakeCase
26DecisionItem with null optional fields (requestedByName, decisionNeededBy, moneyAmount, decidedAt) decodes without crashunitLumiMacTests/APIClientTests — test_decisionItem_nullOptionals_decodesCleanly
27LumiTab.decisions rawValue round-trips correctly (menu-bar navigation uses rawValue)unitLumiMacTests/LumiTabTests — test_decisions_rawValueRoundtrip
28LumiTab.decisions tooltip is non-empty (sidebar hover must show keyboard shortcut hint)unitLumiMacTests/LumiTabTests — test_decisions_tooltipContainsShortcutHint
29MacDecisionsViewModel initial state is .loading, selectedStatus is nilunitLumiMacTests/ViewModelTests — MacDecisionsViewModelTests
30Decisions tab shows live decision list; selecting a row populates the detail panele2emanual macOS verification
31LumiNotificationCenter.tabRoute(forCategory: "lumi.decisions") returns "decisions" (not "today")unitLumiMacTests/NotificationCenterTests — test_tabRoute_decisionsCategory_isDecisions
32LumiNotificationCenter.tabRoute(forCategory: "lumi.brief") returns "brief"unitLumiMacTests/NotificationCenterTests — test_tabRoute_briefCategory_isBrief
33TaskListItem decodes all snake_case fields used by TaskDetailView (ownerName, projectName, dueDate, createdAt, completedAt)unitLumiMacTests/TaskDetailViewTests — test_taskListItem_decodesDetailFields
34TaskListItem null optional fields (ownerName, projectName, dueDate, completedAt) decode without crashingunitLumiMacTests/TaskDetailViewTests — test_taskListItem_nullOptionals_decodesCleanly
35TaskListItem with all four priority values (critical/high/medium/low) round-trips correctlyunitLumiMacTests/TaskDetailViewTests — test_taskListItem_allPriorityValues_roundTrip
36TaskListItem with all four status values (todo/in_progress/done/cancelled) round-trips correctlyunitLumiMacTests/TaskDetailViewTests — test_taskListItem_allStatusValues_roundTrip
37ProjectItem decodes all snake_case fields used by ProjectDetailView (name, description, status, dueDate, createdAt)unitLumiMacTests/ProjectDetailViewTests — test_projectItem_decodesDetailFields
38ProjectItem null optional field (dueDate) decodes without crashingunitLumiMacTests/ProjectDetailViewTests — test_projectItem_nullOptionals_decodesCleanly
39ProjectItem with all four status values (active/on_hold/completed/archived) round-trips correctlyunitLumiMacTests/ProjectDetailViewTests — test_projectItem_allStatusValues_roundTrip
40MacTasksViewModel overdueOnly starts false; can be set to trueunitLumiMacTests/ViewModelTests — MacTasksViewModelOverdueTests.test_overdueOnly_canBeSetToTrue
41MacTasksViewModel switching from overdue back to a status filter clears overdueOnlyunitLumiMacTests/ViewModelTests — MacTasksViewModelOverdueTests.test_clearingOverdueOnly_restoresStatusFilter
42.lumiNavigateTo notification with ["tab": "tasks", "filter": "overdue"] carries both keysunitLumiMacTests/ViewModelTests — MacTasksViewModelOverdueTests.test_overdueTodayNotification_carriesBothKeys
43MacTasksViewModel overdueOnly and selectedStatus are mutually exclusiveunitLumiMacTests/ViewModelTests — MacTasksViewModelOverdueTests.test_overdueOnly_andSelectedStatus_areMutuallyExclusive
44LumiTab.commitments rawValue round-trips correctlyunitLumiMacTests/LumiTabTests — test_commitments_rawValueRoundtrip
45LumiTab.commitments tooltip contains “⌘8” shortcut hintunitLumiMacTests/LumiTabTests — test_commitments_tooltipContainsShortcutHint
46LumiTab.approvals rawValue round-trips correctlyunitLumiMacTests/LumiTabTests — test_approvals_rawValueRoundtrip
47LumiTab.approvals tooltip contains “⌘9” shortcut hintunitLumiMacTests/LumiTabTests — test_approvals_tooltipContainsShortcutHint
48Intelligence section tabs have correct icon names (commitments/approvals/signalInbox/followUps)unitLumiMacTests/LumiTabTests — test_intelligenceTabs_haveExpectedIcons
49⌘N (File → New Task) opens the KMP create-task sheete2eLumiMacUITests/ — test_cmdNOpensNewTaskSheet
50Notification.Name.lumiCreateTask rawValue is stable (“lumi.create.task”)unitLumiMacTests/CreateTaskShortcutTests — test_lumiCreateTask_notificationName_isStable
51SharedScreen.createTask is a valid reachable enum caseunitLumiMacTests/CreateTaskShortcutTests — test_createTask_isValidSharedScreen
52KMPDtoBridge #else stubs compile with all fields LumiAPIClient.saveSession() accessesunitLumiMacTests/CreateTaskShortcutTests — test_kmpDtoBridgeStubs_haveRequiredFields
53AppSession.organizationName is exposed so Preferences window can show the org nameunitLumiMacTests/CreateTaskShortcutTests — test_appSession_organizationName_isInitiallyNil
54LumiTab.search tooltip contains “⌘F” shortcut hint (F47 §2/§6.3 “⌘F anywhere”)unitLumiMacTests/LumiTabTests — test_search_tooltipContainsCmdF
55LumiTab.search rawValue round-trips correctly (LumiCommands.postNavigate uses rawValue)unitLumiMacTests/LumiTabTests — test_search_rawValueRoundtrip
56⌘F navigates to the Search tab (KMP search screen activates)e2eLumiMacUITests/ — test_cmdF_navigatesToSearch
57LumiTab.search icon name is “magnifyingglass”unitLumiMacTests/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_PLATFORMS lists macosx but SUPPORTS_MACCATALYST = NO and SUPPORTS_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

#ScenarioUser-visibleInternalLogRetry
1Mac app launches without networkLast-cached brief with “Offline” badge. Retry button: “Try again”HTTP client returns URLError.notConnectedToInternetWARNING “mac_offline_launch”Auto-retry on network restoration (NWPathMonitor)
2Watch loses connection to phoneLast-cached complication data (up to 30 min old). “Pull to refresh” or “Open on iPhone” CTAWCSession.isReachable = falseINFO “watch_phone_disconnected”Auto-retry on WCSession reactivation
3Voice dictation fails (noisy environment)“Couldn’t hear you. Try again or type your answer.” Scribble keyboard appears as fallbackSFSpeechRecognizer returns errorWARNING “watch_dictation_failed”Manual retry via dictation button or scribble
4iCloud Keychain sync conflict (two devices change org)Silent — local org context re-synced from server on next API callNSUbiquitousKeyValueStore conflict, server overridesDEBUG “icloud_org_sync_conflict”Automatic — server is source of truth
5Mac app receives push while in full-screen presentationNotification follows macOS system preference (Do Not Disturb, Focus mode). No custom overrideUNUserNotificationCenter respects system focusDEBUG “push_suppressed_by_focus”n/a — respects system preference
6User force-quits Mac app mid-syncState is server-truth. Next launch re-fetches. No local corruption possibleProcess killed, no partial writeINFO “mac_force_quit_during_sync”Automatic — cold start on next launch
7Build fails due to missing platform metadata in ASCApp Store Connect submission rejected with: “Missing screenshots for macOS platform.”CI submission step failsERROR “asc_missing_platform_metadata”Fix metadata in App Store Connect, re-submit
8CI simulator pairing timeout for watchOS testsCI fails: “Could not pair iOS and watchOS simulators within 120s.”xcrun simctl pair timeoutERROR “ci_simulator_pair_timeout”CI re-run; investigate simulator state
9Mac app window restoration fails (state corrupted)App opens to blank window or default size. Previous window positions lostNSWindow restoration archive corruptWARNING “mac_window_restore_failed”n/a — app opens with default window config
10Watch complication update exceeds daily budgetComplication shows stale count (last successful update). No user indication of budget exhaustionComplication update rejected by ClockKitERROR “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.

#ClaimStatusEvidence
1ios/LumiMac/ directory structure (11 Swift stubs)✅ matchios/LumiMac/ on main (MC r01)
2ios/LumiWatch/ directory structure (7 Swift stubs)✅ matchios/LumiWatch/ on main (MC r01)
3ios/LumiMacTests/ — 14 files, §7 cases 1–57: assertions + XCTSkip (environment-gated)✅ updatedMC 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) ✅
4ios/LumiWatchTests/ — 4 files, §7 cases 5–7: 16 real assertions + 3 XCTSkip✅ updatedMC 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
5ios/AGENTS.md macOS conventions (12 items)✅ matchios/AGENTS.md (MC r01)
6Xcode targets (LumiMac, LumiWatch in project.pbxproj)✅ doneIO r04 (aa4d6802b) — MC-to-IO-r02 hand-off resolved
7LumiCore/LumiUI shared Swift Package extraction⚠️ gapIO steward in place; content not started — blocked on Xcode + LumiCore architecture
8macOS + watchOS app feature implementation⚠️ partialScaffold 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.
9iCloud Keychain sync (§4.4)⚠️ gapNot started — entitlement + provisioning profile required
10App Store Connect (Mac + Watch platform metadata)⚠️ gapNot started
11CI: all 4 targets build in CI (§7 case 9)⚠️ partialmac-notarize.yml stub added (r05-C) — full CI blocked on macOS 15 self-hosted runner with Xcode

12. Open Questions / Decisions

  1. 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.
  2. 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.
  3. 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.
  4. 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 KMPMacView wrapping an NSViewController containing a ComposePanel — so AppKit NSViewController is 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).
  5. 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.
  6. 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) with loadError: 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 = nil cleared on success.
    • hasContent computed property: !decisions.isEmpty || !risks.isEmpty || !overdues.isEmpty.
    • Comment updated: “Unknown future item_type values are silently skipped (forward-compatible).”
  • ios/LumiWatch/Views/QuickActionsView.swift:
    • Added @State private var actionError: String?.
    • Approve/reject buttons: replaced try? await (silent failure) with try await inside do/catch; on failure sets actionError and does NOT call loadActions() (item stays in list; user can retry).
    • loadActions() clears actionError at start.
    • Action-error amber banner shown at top of List when actionError != nil (above items).
  • ios/LumiWatchTests/WatchBriefTests.swift:
    • Added test_watchAPIErrorDescriptionsAreNonEmpty — verifies every WatchAPIError case produces a non-empty localizedDescription (what the error banners actually show).
    • Added test_httpErrorDescriptionIncludesCode — verifies status code appears in message (diagnostic requirement).
    • Added test_todayBriefCategorisationSkipsUnknownItemTypes — verifies unknown future item_type values are silently ignored, not placed in wrong sections.

Reason:

  • TodayView catch branch set isOffline = 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().”
  • QuickActionsView approve/reject used try? await, silently discarding errors and always calling loadActions() (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 from fetchLatestBrief): path /api/briefs/today/ ✅ (was /api/briefs/latest/ — does not exist → 404 on every load).
    • WatchBriefResponse / WatchBriefItem (replace WatchBrief): match real backend shape { id, status, items: [{ id, item_type, title }] }. WatchBrief had decisions/risks/overdues: [String]? — fields that do not exist on the real API → empty data on every load.
    • fetchPendingApprovals(limit:) (replaces fetchPendingDecisions): path /api/approvals/?queue=me ✅.
    • approveApproval(id:) (replaces approveDecision): path /api/approvals/{id}/approve/ ✅ (was /api/decisions/{id}/approve/ — does not exist).
    • rejectApproval(id:) (replaces rejectDecision): path /api/approvals/{id}/reject/ ✅.
    • WatchApproval / WatchApprovalListResponse (replaces WatchDecision): match real ApprovalResponse.
    • Deleted: snoozeDecision — /api/decisions/{id}/snooze/ does not exist in any spec.
    • WatchAPIClient.Paths enum extracted for unit-testability.
  • ios/LumiWatch/Views/TodayView.swift: calls fetchTodayBrief() (correct path); filters brief.items by item_type to 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 two XCTSkip placeholders 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, WatchBrief had no items field — 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: WatchBriefTests now 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: added makeOrgManagementViewController, makeOrgSetupViewController, and makeSubscriptionViewController — 3 factory functions called by SharedScreenBridge.swift but absent from macosMain. Added matching imports (10 lines) for OrgManagementScreen, OrgManagementTab, OrgSetupScreen, SubscriptionScreen and 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 iosMain as part of F24 (Subscription), F02 (OrgManagement), and F30 (OrgSetup) KMP migrations, but not mirrored to macosMain. SharedScreenBridge.swift .org, .orgSetup, and .subscription switch cases call these missing symbols — linking the rebuilt XCFramework would produce 3 unresolved Swift symbol errors.

Impact:

  • All SharedScreen bridge call sites now have matching Kotlin factories in macosMain
  • No content drift: OrgManagementScreen, OrgSetupScreen, SubscriptionScreen are pure commonMain Compose, 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. macosMain export 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 — searchText was never forwarded to the KMP SearchScreen (which manages its own query state). Removing it frees ⌘F for the correct ⌘F→Search behavior registered in LumiCommands.
  • ios/LumiMac/App/LumiTab.swift: .search tooltip updated from "Global search" to "Global search (⌘F)".
  • ios/LumiMacTests/LumiTabTests.swift: doc comment fixed (“all 8 tabs” → “all 25 tabs”); test_tabCountIs14 renamed test_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). LumiCommands had 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 .lumiCreateTask notification.
  • ios/LumiMac/Theme/LumiColors.swift: Notification.Name.lumiCreateTask (“lumi.create.task”) added.
  • ios/LumiMac/Views/ContentView.swift: @State showCreateTask; .onReceive(.lumiCreateTask) opens KMPMacView(.createTask) as a sheet (520×680); toolbar ”+” button also sets the flag; old no-op // TODO removed.
  • ios/LumiMac/KMP/KMPDtoBridge.swift: #else compile-only stubs for KMPLoginResponse, KMPUserResponse, KMPOrgResponse — LumiAPIClient and AppSession now 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 shows AppSession.shared.userEmail; Sign Out wired to AppSession.shared.logout(); Org tab shows AppSession.shared.organizationName; Switch Org posts .lumiNavigateTo to Settings tab — all stubs replaced with live data.
  • ios/LumiMacTests/CreateTaskShortcutTests.swift (new): 5 tests — notification name stability, SharedScreen.createTask reachability, KMPDtoBridge stub fields, AppSession.organizationName existence, ⌘N XCUITest skip.
  • ios/LumiMacTests/KeyboardShortcutTests.swift: test_cmdNOpensNewTaskSheet added.
  • §7 cases 49–53 added.

Reason:

  • ios/AGENTS.md MC convention r19 and F47 §2 both specify ⌘N as a required Mac-native idiom. ContentView had a // TODO: Show new task sheet no-op. This change closes that gap.
  • KMPDtoBridge.swift only defined auth types inside #if canImport(shared), causing LumiAPIClient and AppSession to fail to compile on the common path where the XCFramework is not yet linked. The #else stubs 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 returning NSViewController via ComposeNSViewController { } for every SharedScreen case (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). Mirrors iosMain/MainViewController.kt with NSViewController/ComposeNSViewController instead of UIViewController/ComposeUIViewController.
  • ios/LumiMac/KMP/SharedScreenBridge.swift: removed stale onLeaveOrg: {} argument from .settings case in #if canImport(shared) branch — makeSettingsViewController no 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:linkReleaseMacosArm64 from succeeding and the XCFramework being linked to LumiMac. Blocker 2 (PullToRefreshBox expect/actual) was resolved 2026-06-23. This change resolves blocker 1: without macosMain/MainViewController.kt, Kotlin/Native has no factories to expose to the shared.xcframework macosArm64 slice, so MainViewControllerKt.* 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 existing authedRequest() + rawSend() + PaginatedResponse<T>.
  • ios/LumiMac/App/LumiTab.swift: new .risks case (icon exclamationmark.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; .risks in contentForTab and detailForTab.
  • 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 tests
  • ios/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 in Button(.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:) gains overdue: Bool = false parameter. When true, appends &overdue=true to path — reuses the same backend parameter already used by getOverdueTasksCount().
  • ios/LumiMac/Views/TasksView.swift: MacTasksViewModel gains @Published var overdueOnly: Bool = false; load() passes overdue: overdueOnly to API; TasksView gains @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 .tasks and pass it as filterRequest binding to TasksView.
  • ios/LumiMacTests/ViewModelTests.swift: MacTasksViewModelOverdueTests class added (4 offline assertions, §7 cases 40–43); test_initialOverdueOnly_isFalse added to MacTasksViewModelTests.

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 by getOverdueTasksCount()); 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: extracted static func tabRoute(forCategory:) -> String? — maps "lumi.decisions" → "decisions", "lumi.brief" → "brief", unknown → nil. Pure static function, testable offline, same pattern as shouldNotify(previous:new:).
  • LumiNotificationCenter.handleResponse() rewritten to use tabRoute + post .lumiNavigateTo with ["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 in Button(.plain) — tappable when pendingDecisions > 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 existing authedRequest() + rawSend() + PaginatedResponse<T>.
  • ios/LumiMac/App/LumiTab.swift: new .decisions case (icon exclamationmark.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; .decisions in contentForTab and detailForTab.
  • 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): @MainActor singleton wrapping UNUserNotificationCenter for local delivery. LumiNotificationContent pure struct with testable factory methods (pendingDecisions(count:), briefReady()) and shouldNotify(previous:new:) guard (fires only on count increase).
  • AppDelegate.swift: added UNUserNotificationCenterDelegate conformance. applicationDidFinishLaunching now sets delegate + calls requestPermissionIfNeeded() (idempotent; no-op if already decided). willPresent handler delivers banners even when Lumi is frontmost. didReceive handler brings app forward + posts navigation notification to ContentView.
  • MenuBarDataStore.swift: refresh() captures previousDecisions, calls LumiNotificationContent.shouldNotify(previous:new:) after fetch, and fires notifyNewDecisions only on increase.
  • BriefView.swift: MacBriefViewModel.generate() calls LumiNotificationCenter.shared.notifyBriefReady() after state = .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, shouldNotify logic (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.
  • @AppStorage window state: resetting to .brief on 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 nonexistent MenuBarViewModel; replaced with MenuBarPopover() (reads from MenuBarDataStore.shared internally).
  • AppDelegate.swift: added applicationDidBecomeActive(_:) — 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 — changed request() to authedRequest() (adds Authorization + X-Organization-ID headers). These five methods were the only ones in the file using the unauthed helper; the omission caused TodayView, BriefView, and PulseView to always display .error state.
  • LumiAPIClient.swift rawSend() 401-retry: added X-Organization-ID restoration alongside Authorization — without this, a token-refresh mid-session would still fail the retry with 401 on multi-tenant orgs (Six Rules Rule 1).
  • MenuBarExtra.swift: new MenuBarViewModel: ObservableObject fetches live pending-decisions and overdue counts; calls AppDelegate.updateBadge(count:) to wire the dock badge. MenuBarPopover signature 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 — added PaginatedResponse<TaskListItem> decode, count-only envelope, BriefResponse nil-items. ViewModelTests — added MenuBarViewModelTests (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 by getTasks, 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 tests
  • ios/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 @MainActor singleton: polls LumiAPIClient.shared.getPendingDecisionsCount() + getOverdueTasksCount() every 5 min; calls AppDelegate.updateBadge(count:) after each fetch; zeroes badge on sign-out.
  • MenuBarPopover — replaced hardcoded @State private var pendingDecisionCount: Int = 3 with live MenuBarDataStore.shared; added isRefreshing spinner, relative timestamp, disabled Refresh button during in-flight request. “Completed today” row (always 5, no API source) removed.
  • AppDelegate.applicationDidFinishLaunching — now calls MenuBarDataStore.shared.startPolling() if user is already authenticated (returning-user cold launch).
  • AppSession.login() → MenuBarDataStore.shared.startPolling()
  • AppSession.logout() → MenuBarDataStore.shared.stopPolling() (badge cleared)
  • LumiAPIClient auth bug: getPendingDecisionsCount, getOverdueTasksCount, and getTodayBrief were using request() (no auth headers) → rawSend(), causing every call to trigger a redundant 401 → token-refresh round-trip. Fixed to authedRequest().
  • ContentView compile bug: KMPMacView(screen:, baseURL:, token:) called without required orgId: parameter. Added private var kmpOrgId and fixed all 3 call sites.
  • MenuBarDataStoreTests.swift (new) — 4 offline assertions + 1 XCTSkip for 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 — new LocalizedStringKey computed property on all 8 cases with descriptive hover text including keyboard shortcut hint
  • SidebarView — .help(tab.tooltip) added to all 8 sidebar items (5 explicit buttons + sidebarItem() helper) — hover discoverability complete
  • PreferencesWindow — replaced all .constant() bindings with @AppStorage (7 keys in prefs.* namespace) — General/Notifications tabs are now interactive without LumiCore
  • PreferencesWindowTests.swift — new test file; 3 real assertions: key namespace, uniqueness, default tab rawValue validity; 2 XCTSkip for render/interaction
  • LumiTabTests.swift — added test_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.