| name | mobile-app-builder |
| description | Mobile application development discipline covering iOS native (Swift / SwiftUI), Android native (Kotlin / Jetpack Compose), React Native, and Flutter cross-platform approaches. Covers app architecture (MVVM, MVI, Clean, TCA), state management per platform, navigation and deep-link semantics, offline-first local-DB selection and sync conflict resolution, push and local notification compliance (APNs / FCM), accessibility (VoiceOver, TalkBack, WCAG 2.2 AA mobile), and App Store / Play Console store compliance including privacy manifests. Use when designing or reviewing any mobile feature, architecture decision, platform selection, or store-submission readiness. Pair with security-and-auth for token storage and biometric auth flows. |
| owner | Priya Menon (Mobile App Builder, domain persona) |
| tier | domain:mobile |
| scope_tags | ["mobile","ios","android","react-native","flutter","mobile-architecture","offline-first"] |
| paths | ["**/*.swift","**/Package.swift"] |
| inspired_by | [{"source":"msitarzewski/agency-agents/engineering/engineering-mobile-app-builder.md@783f6a72bfd7f3135700ac273c619d92821b419a","license":"MIT","relationship":"structural_inspiration","authored_by":"ceo-orchestration framework","authored_at":"2026-05-07T00:00:00.000Z"},{"source":"affaan-m/ecc/skills/android-clean-architecture@81af40761939056ab3dc54732fd4f562a27309d0","license":"MIT","relationship":"structural_inspiration","authored_by":"ceo-orchestration framework","authored_at":"2026-07-07T00:00:00.000Z"}] |
| domain | mobile |
| priority | 8 |
| risk_class | medium |
| stack | ["swift","kotlin","react-native"] |
| context_budget_tokens | 600 |
| inactive_but_retained | true |
| repo_profile_binding | {"frontend":{"active":false,"priority":10},"engine":{"active":false,"priority":10},"fintech":{"active":false,"priority":10},"trading-readonly":{"active":false,"priority":10},"generic":{"active":false,"priority":10}} |
| activation_triggers | [{"event":"help-me-invoked","regex":"(?i)foundation.?models|apple.?intelligence"}] |
| source | affaan-m/ecc@81af4076 skills/android-clean-architecture/ |
| license | MIT |
Mobile App Builder
Cardinal Rule
Platform behavior is a contract, not a suggestion. Platform-specific memory
pressure, background execution limits, and review policy rules are not edge cases
— they are the invariants the entire runtime assumes. Every architecture decision
must be defensible against App Store Review and Play Console policies before any
line of implementation is written.
Fail-Fast Rule
Stop and surface a structured error when:
- A proposed token or credential storage location is not the platform keychain or
Keystore (see Cross-References: security-and-auth).
- A navigation change breaks the back-stack contract, universal-link routing, or
accessibility traversal order.
- An offline-first sync operation has no conflict-resolution strategy defined.
- A notification payload exceeds the platform limit (APNs: 4 KB; FCM: 4 KB data,
up to 4 KB notification) or lacks the required permission-request trigger.
- A store-submission target uses a deprecated API or missing privacy-manifest
declaration.
- A Claude integration via Apple Foundation Models uses
.apiKey auth in a
release configuration (see Apple Foundation Models: three mandates).
Never silently default, guess, or smooth over these conditions.
When to Apply
Load this skill when:
- Selecting or revisiting the native vs. cross-platform platform strategy.
- Designing or reviewing app architecture, module boundaries, or state-management
approach.
- Implementing navigation, deep links, universal links, or app links.
- Adding or modifying offline persistence, sync, or conflict resolution.
- Integrating push notifications (APNs / FCM) or local notification scheduling.
- Reviewing accessibility compliance (VoiceOver, TalkBack, dynamic type, touch
targets).
- Preparing a build for App Store or Play Console submission.
- Adding biometric authentication or platform keychain/Keystore integration
(always pair with
core/security-and-auth).
- Integrating Claude through Apple's Foundation Models framework
(
ClaudeForFoundationModels), or routing between the on-device model and
Claude.
Platform Strategy
Selection matrix
| Criterion | Native iOS + Android | React Native | Flutter |
|---|
| Team skill fit | iOS (Swift) / Android (Kotlin) specialists | TypeScript / JavaScript teams | Dart-capable or greenfield |
| Platform fidelity | Highest — full API surface, no bridge | High with native modules; bridge overhead | High; own rendering engine, not system widgets |
| Performance ceiling | Best (Metal, Vulkan direct) | Good; JS bridge latency on high-frequency events | Good; Impeller/Skia rendering; no JS bridge |
| Time-to-market (both platforms) | Slowest (two codebases) | Faster; ~60-80% code share realistic | Fastest; ~70-90% code share realistic |
| Third-party SDK availability | All SDKs have native variants | Most SDKs; gaps require native modules | Growing; some SDKs absent or community-only |
| Maintenance burden | Two separate release trains | One JS layer + two native shells | One Dart layer + two shells; Dart FFI for native |
| Store compliance risk | Lowest; no interpretation layer | Low; React Native well-precedented | Low; Flutter well-precedented |
Decision rule: choose native when the feature set requires deep platform API
access (ARKit, Core NFC, on-device ML, Bluetooth LE central mode) or when the
existing team has platform specialization. Choose React Native when a JS/TS web
team is the primary mobile team and third-party SDK coverage is sufficient. Choose
Flutter when both platforms are greenfield, delivery velocity is the constraint, and
Dart adoption is feasible.
Never choose a framework to avoid learning platform conventions. Platform
conventions must be understood regardless of framework — they define the contract
that governs review and runtime behavior.
Apple Foundation Models (Claude integration)
Beta surface: targets the server-side language model API introduced in the
OS 27 betas (iOS / iPadOS / macOS / visionOS / watchOS 27, Xcode 27). APIs
may change before GA.
ClaudeForFoundationModels is Anthropic's Swift package
(https://github.com/anthropics/ClaudeForFoundationModels.git, SPM) that
conforms Claude to Apple's LanguageModel protocol. The same
LanguageModelSession API that drives Apple's on-device model drives Claude —
respond(to:), streaming via streamResponse(to:), guided generation, and
tool calling work unchanged; you switch providers by swapping the model:
argument. Requests go directly from the app to the Claude API (Apple is not in
the request path) and are billed to the adopter's Anthropic account at standard
API pricing.
.package(url: "https://github.com/anthropics/ClaudeForFoundationModels.git", from: "0.1.0")
import FoundationModels
import ClaudeForFoundationModels
let claude = ClaudeLanguageModel(name: .sonnet4_6, auth: auth)
let session = LanguageModelSession(model: claude)
let response = try await session.respond(to: prompt)
On-device <-> Claude routing doctrine
Same tiering logic as the CEO routing table: route every request to the
cheapest tier that is capable, escalate only on demonstrated capability demand.
| Tier | Route here when | Cost profile |
|---|
Apple on-device model (SystemLanguageModel) | Lightweight tasks, privacy-sensitive content, offline paths | Free, private, offline-capable; sized for lightweight tasks |
Claude (ClaudeLanguageModel) | Larger context, frontier reasoning, server-side tools (web search, code execution via serverTools:) | Billed per request to the adopter's account; network required |
- Routing is the
model: argument — both tiers share LanguageModelSession,
so there are no dual code paths to maintain.
- Escalation is a product decision the app makes per session; make it explicit
in code (a named routing function), never implicit in scattered call sites.
- Degrade gracefully downward: catch
LanguageModelError.rateLimited (or
timeout) from Claude and fall back to SystemLanguageModel for that turn,
queue the request, or surface a retry affordance.
Structured output (@Generable)
Annotate a type with @Generable (with @Guide per field) and request it with
generating: — the package maps it onto Claude structured outputs. If the
chosen model's declared capabilities do not include structured output, the
package throws LanguageModelError.unsupportedGenerationGuide rather than
silently degrading — surface it, never swallow it.
Three mandates (fail-fast — do not ship without all three)
.proxied(headers:) is the ONLY production auth. .apiKey is
development-only: a key bundled into a shipping binary is extractable, and
anyone who extracts it can make requests billed to the adopter's account.
Production routes through the adopter's own backend via
auth: .proxied(headers:) plus baseURL: — the relay attaches the Claude
credential (x-api-key) server-side, the app ships no key, and the supplied
headers let the proxy authorize the caller. A release build with .apiKey
is a Fail-Fast condition (see Fail-Fast Rule).
fixedEffort: for .xhigh / .max. The framework's per-request
reasoning hints stop at high. fixedEffort: pins a Claude effort level for
every request, takes precedence over the framework's hints, and is the only
way to request .xhigh or .max. The level must be one the model's
declared capabilities accept; the API defaults to high when no effort is
sent.
- Capability declaration for unknown model IDs. Compiled-in
ClaudeModel
constants (e.g. .opus4_8 = claude-opus-4-8) carry each model's
capabilities — sampling parameters, effort levels, adaptive thinking,
structured output, image input — and the package uses them to decide which
request fields to send, because sending a field a model rejects is a hard
error. For an ID that is not compiled in, declare what it accepts
explicitly via ClaudeModel(id:capabilities:); there is deliberately no
shorthand that guesses. This is the framework-side mirror of ADR-149's
model-id allowlist doctrine: never assume what an unverified model ID
supports.
App Architecture
Architecture pattern selection
| Pattern | When it wins | When to avoid |
|---|
| MVVM (iOS: SwiftUI/Combine; Android: ViewModel/StateFlow) | Declarative UI, clear View/ViewModel boundary, reactive bindings | Deep side-effect chains where unidirectional flow is essential |
| MVI (Model-View-Intent) | Complex state with explicit intent dispatch; Android Compose is well-suited | Simple screens — ceremony overhead is not justified |
| Clean Architecture (Use Cases + Repositories) | Large codebases, testability priority, CI gate coverage requirements | Small apps; over-engineers the data layer unnecessarily |
| TCA (The Composable Architecture — iOS/Swift) | Pure unidirectional state machine; composable feature trees; SwiftUI teams already familiar | Non-Swift targets; Kotlin/RN/Flutter teams lack first-class TCA equivalents |
Module boundaries
- Feature modules are independently compilable. Circular dependencies between
feature modules are a build error, not a warning.
- Core modules (networking, persistence, analytics, auth) have no dependency on
any feature module.
- Platform modules (camera, biometrics, push) expose a protocol/interface
boundary to feature modules; no direct SDK import from feature code.
- No UI code in domain or data layers. Domain layer has zero UIKit / SwiftUI /
Compose import.
Testability
- ViewModels / Presenters are plain objects with no platform import.
- Repository layer takes protocol/interface arguments; real implementations are
injected at the composition root, stubs at test time.
- Snapshot tests (iOS: swift-snapshot-testing; Android: Paparazzi) cover each
distinct UI state, not every permutation.
Clean Architecture on Android / Kotlin Multiplatform
The "Clean Architecture" row in the pattern table and the module-boundary
doctrine above become concrete on Android — and on Kotlin Multiplatform (KMP),
where the domain and data layers are shared Kotlin across Android and iOS — as a
fixed layer stack governed by a single one-directional dependency rule. Reach for
this realization when the codebase is large enough that testability and CI gate
coverage justify the data-layer ceremony; consult the pattern table's "when to
avoid" column before adopting it on a small app.
Layer stack
| Module | Holds | Depends on |
|---|
app | Entry point, DI wiring, Application / composition root | everything below |
presentation | Screens, ViewModels, UI models, navigation | domain, design-system, core |
domain | UseCases, domain models, repository interfaces — pure Kotlin | core only (ideally nothing) |
data | Repository implementations, DataSources, DB, network | domain, core |
core | Shared error types, base utilities | nothing |
design-system | Reusable Compose components, theme, typography | core |
feature/* | Optional per-feature slices for large apps | domain, design-system, core |
The one-directional rule (dependencies point inward, never out):
app -> presentation, domain, data, core
presentation -> domain, design-system, core
data -> domain, core
domain -> core (or nothing)
core -> (nothing)
The load-bearing invariant: domain is pure Kotlin. Zero Android framework
import, zero data or presentation import. This is the concrete form of the
"no UI code in domain or data layers" rule above. A framework import in domain
is a Fail-Fast condition, not a style preference — it silently couples business
rules to a platform and breaks KMP sharing.
Domain layer
- UseCase = one business operation, exposed through a single
operator fun invoke so call sites read as getItemsByCategory(category).
Use suspend for one-shot work and return Flow<T> for reactive streams —
do not fold both concerns into one UseCase.
- Domain models are plain data classes with no persistence or serialization
annotations. An
@Entity, @Serializable, or DTO type in the domain layer is
a leak of an outer concern inward.
- Repository interfaces live in
domain; implementations live in data.
This is dependency inversion: the business layer declares what it needs, the
data layer supplies it, and domain never names a concrete database or client.
class GetItemsByCategory(private val repo: ItemRepository) {
suspend operator fun invoke(category: String): Result<List<Item>> =
repo.getItemsByCategory(category)
}
interface ItemRepository {
suspend fun getItemsByCategory(category: String): Result<List<Item>>
fun observeItems(): Flow<List<Item>>
}
Data layer
- Repository implementations coordinate DataSources — a local source and a
remote source, each with one job — rather than talking to the framework
directly. A repository that accretes query logic, caching policy, and mapping
all at once is the "fat repository" anti-pattern; split it into focused
DataSources.
- Map at the boundary. DTOs (network) and entities (DB) convert to and from
domain models via small mapper functions — idiomatically Kotlin extension
functions kept beside the data models. A DTO or DB entity must never cross into
domain or the UI; the mapper is the seam that keeps the invariant true.
- Storage/network realization for the offline-first strategy: Room on
Android, SQLDelight for KMP, Ktor for the network client. Clean layering is
what lets you swap those without touching
domain.
Composition root and error boundary
- Dependency injection is wired only in
app — the one module that sees
every layer. Koin is the KMP-friendly choice; Hilt is Android-only. This is the
same "inject real implementations at the composition root, stub at test time"
discipline the Testability section states, made concrete: because domain
depends on interfaces, tests substitute fakes with no framework present.
- Errors cross the repository boundary as values, not exceptions. Return
Result<T> or a sealed AppError / Try type from the repository; the
ViewModel maps that value to UI state. An exception thrown from data and
caught (or missed) in a Composable is an uncontrolled boundary — model the
failure explicitly so every call site is forced to handle it.
Layering anti-patterns (build-time and review-time)
- Android framework class imported in
domain — breaks purity and KMP sharing.
- DB entity or DTO exposed to the UI — always map to a domain model first.
- Business logic living in a ViewModel — extract it to a UseCase so it is
testable without the UI.
GlobalScope or unstructured coroutines — use viewModelScope and structured
concurrency (see State Management).
- Circular module dependency — if
A depends on B, B must not depend on
A; this is a build error, matching the feature-module rule in Module
boundaries above.
State Management
State management is platform-specific. Cross-platform frameworks add their own layer.
iOS — SwiftUI + Combine:
@StateObject owns lifecycle; @ObservedObject observes. Mixing them causes
phantom resets.
@Published properties trigger re-render; expensive computations belong in
receive(on:) pipelines, not body closures.
- Global shared state lives in
@EnvironmentObject; over-use creates invisible
coupling — prefer scoped @StateObject trees.
Android — Jetpack Compose + Kotlin Flow:
ViewModel survives configuration changes; rememberSaveable survives process
death for lightweight UI state only.
collectAsStateWithLifecycle (not collectAsState) prevents background
collection and battery drain.
StateFlow for UI state; SharedFlow for events (navigation, one-shot errors).
Never emit UI state through SharedFlow — it has no replay guarantee on
recomposition.
React Native — Redux / MobX / Zustand:
- Zustand preferred for small-to-medium apps; Redux + Redux Toolkit for
complex async flows.
- All async side effects belong in middleware (Redux Thunk / Saga) or Zustand
actions; never inside component effects.
- Component re-render surface must be minimized via selector memoization
(
useSelector, useMemo).
Flutter — Riverpod / Bloc:
- Riverpod
AsyncNotifier for async data; Notifier for synchronous state.
- Bloc pattern for complex event-state machines; avoid it for simple CRUD screens.
StateProvider in Riverpod is a code smell for complex state — migrate to
Notifier before the first code review.
Navigation Discipline
- Route as URL. Every screen has a deterministic path segment. Deep links,
universal links (iOS), and app links (Android) must resolve to the same code
path as in-app navigation — no dual routing tables.
- Back-stack semantics are explicit.
popBackStack, replaceTop, and
clearAndReplace have distinct semantics; the wrong choice corrupts the user's
navigation history.
- Universal links (iOS):
apple-app-site-association file must be hosted at
https://<domain>/.well-known/apple-app-site-association with correct
applinks entries. AASA changes propagate with up to 24-hour CDN delay.
- App links (Android): Digital Asset Links JSON at
https://<domain>/.well-known/assetlinks.json. Verification must pass before
release; test via adb shell pm get-app-links.
- Gesture handling: swipe-back (iOS edge-pan) and predictive-back (Android 13+)
must not trigger unsaved-data loss without a confirmation dialog. Modal sheets
must handle the dismiss gesture identically to the dismiss button.
Offline-First Patterns
Local DB selection
| Database | iOS fit | Android fit | RN fit | Flutter fit | When to choose |
|---|
| SQLite (raw) | Core Data wrapper | Room wrapper | expo-sqlite / react-native-quick-sqlite | sqflite | Full SQL control; complex queries |
| Core Data | Native | — | — | — | iOS-only; tight SwiftUI integration; migration tooling |
| Room | — | Native | — | — | Android-only; Kotlin-first; Flow integration |
| Realm | Native SDK | Native SDK | realm-js | realm-dart | Reactive queries; sync with MongoDB Atlas |
| WatermelonDB | — | — | React Native | — | RN-specific; lazy loading; Observable architecture |
| Drift (formerly Moor) | — | — | — | Native | Flutter-native; type-safe SQL with code generation |
Sync conflict resolution
Conflict resolution strategy must be documented in the architecture before
implementation. Accepted strategies:
- Last-write-wins (LWW): simplest; requires server-authoritative timestamps;
correct only when concurrent writes to the same field are rare and low-stakes.
- Server-wins: client changes are local optimistic UI only; server state
replaces on sync. Correct for externally driven data (e.g. order status).
- CRDT (Conflict-free Replicated Data Types): mathematically merge-safe;
appropriate for collaborative or append-only structures (notes, checklists).
- Three-way merge (ancestor + local + remote): requires ancestor record storage;
appropriate for complex entities where partial merges are valid.
No silent discard. If a conflict is resolved in a way that discards user
input, the resolution must be surfaced to the user.
Optimistic UI
Minimum structure for an optimistic mutation (pseudocode — applies to all platforms):
function submitOptimistic(item):
previousState = store.snapshot()
store.applyOptimistic(item) // local immediate update
try:
result = await api.post(item)
store.commit(result) // replace optimistic with confirmed
catch SyncError as e:
store.rollback(previousState) // restore prior state
retryQueue.enqueue(item, e) // dead-letter; never silent drop
ui.showConflictBanner(e)
Optimistic mutations must be paired with:
- A rollback path on sync failure (restore previous local state).
- A visual diff signal so the user can distinguish committed from optimistic state.
- A retry queue with exponential backoff and dead-letter storage (not silent drop).
Push + Local Notifications
APNs (iOS)
- Register via
UNUserNotificationCenter.requestAuthorization at a justified
moment — not on first app launch. Prompt timing must be validated against
provisioning profile entitlements.
- Payload limit: 4 KB total. Exceed it and the notification is silently dropped.
- Use
mutable-content: 1 for notification service extension processing only when
needed (media attachments, decryption). The extension has 30 seconds.
- Silent push (
content-available: 1, no alert/sound/badge) is rate-limited by
iOS at discretion. Never rely on silent push for time-sensitive delivery.
FCM (Android / cross-platform)
- Notification messages (display by FCM SDK) vs. data messages (handled by app):
choose data messages for all custom handling; notification messages bypass
onMessageReceived when the app is in background.
HIGH vs. NORMAL priority: HIGH wakes the device from Doze; use only for
user-facing alerts. Background data sync must use NORMAL.
- Android 13+ requires
POST_NOTIFICATIONS permission; request at the appropriate
user-action moment, not at app start.
Delivery diagnostics
- Log device token registration, token refresh, and delivery receipt (FCM
delivery receipt API / APNs feedback service) at the analytics plane.
- Silent-push budget exhaustion (iOS) and Doze-mode delivery latency (Android)
must be documented in the system's SLA.
Accessibility Compliance
- VoiceOver (iOS) / TalkBack (Android): every interactive element has a
descriptive
accessibilityLabel (iOS) / contentDescription (Android). Never
expose implementation strings ("ic_btn_close_24") as accessibility labels.
- Dynamic type (iOS) / font scale (Android): layouts must not overflow or
truncate at the system's largest accessibility font size. Test at 200% scale.
- Touch target floor: minimum 44 × 44 pt (iOS HIG) / 48 × 48 dp (Material
Design). Elements below the floor must have a tap-area inset applied without
changing visual size.
- WCAG 2.2 AA mobile profile:
- Contrast ratio ≥ 4.5:1 for body text; ≥ 3:1 for large text (18pt regular /
14pt bold) and UI components.
- Focus indicators visible at ≥ 3:1 contrast against adjacent colors.
- No information conveyed by color alone; use shape or label redundancy.
- Motion: respect
prefers-reduced-motion (iOS) / ANIMATOR_DURATION_SCALE = 0
(Android) — provide non-animated state transitions.
Store Compliance
App Store (iOS)
- Privacy manifest (PrivacyInfo.xcprivacy): required for all third-party SDKs
in the required reasons API list since 2024. Missing or incomplete manifests
cause automated rejection.
- Tracking permission (ATT):
NSUserTrackingUsageDescription required; present
the requestTrackingAuthorization prompt only after a pre-permission dialog
explaining value. Cross-app tracking without permission is a Review guideline 5.1.2
violation.
- Age rating: accurately declare content; incorrect rating triggers removal.
- Export compliance: encryption usage requires annual self-classification or
the
ITSAppUsesNonExemptEncryption = NO plist key when only standard
platform APIs (HTTPS, Keychain) are used.
- Review guideline 4.2 (Minimum Functionality): content-only apps without
native functionality will be rejected.
Play Console (Android)
- Data safety section: every data type collected or shared must be declared;
inconsistency with actual behavior triggers enforcement action.
- Target API level: must target the current year's required API level within
the grace period. Apps targeting older levels are hidden from new installations.
- Background location: requires
ACCESS_BACKGROUND_LOCATION permission with a
policy URL and a specific use-case justification in the declaration.
- Exact alarms (Android 12+):
USE_EXACT_ALARM or SCHEDULE_EXACT_ALARM
requires justification; calendar and reminder apps qualify; background sync
does not.
Anti-patterns
| Anti-pattern | Why it fails |
|---|
| Blocking the main thread (UI thread) with I/O or CPU work | Causes ANR (Android) / watchdog kills (iOS); 5-second main-thread block is observable by App Store Review automated tools |
Ignoring memory pressure signals (applicationDidReceiveMemoryWarning / onTrimMemory) | Image caches grow unbounded; app is killed by the OS in background; users blame "battery drain" |
Storing credentials, tokens, or PII in UserDefaults, SharedPreferences, or unprotected files | Accessible to any other app on jailbroken / rooted devices; violates App Store guideline 5.1.1 |
Shipping .apiKey Claude auth in a release binary (ClaudeForFoundationModels) | The key is extractable from the shipped binary; anyone who extracts it makes requests billed to the adopter's Anthropic account. .proxied(headers:) through the adopter's backend is the only production auth |
| No offline handling — assuming network is always available | Crashes or blank screens on airplane mode; fails Review on unreliable test networks; breaks accessibility (assistive tech users rely on cached content) |
| Requesting sensitive permissions (location, camera, notifications) at app launch with no context | Permission denial rate is highest at launch; iOS 14+ enforces notification permission timing heuristics |
| Using deprecated API without fallback | App is rejected or removed from store on next review cycle; no graceful runtime degradation |
Cross-References
.claude/skills/core/security-and-auth/SKILL.md — Keychain / Keystore token
storage, biometric auth, OAuth / PKCE flows, certificate pinning.
.claude/skills/core/code-review-checklist/SKILL.md — apply mobile-specific
checklist items during PR review: thread safety, memory leaks, permission
handling, test coverage floor.
.claude/skills/frontend/accessibility-and-wcag/SKILL.md — WCAG 2.2 detailed
compliance criteria and testing methodology applicable to web views embedded in
mobile apps.
.claude/skills/core/llm-routing-and-finops/SKILL.md — the CEO tiering table
that the on-device <-> Claude routing doctrine mirrors (cheapest capable tier
first, escalate on capability demand).
ADR Anchors
- ADR-058 (Brainstorm gate + two-pass adversarial review): mobile architecture
and platform-strategy decisions are L3+ and require the brainstorm gate before
plan execution. Two-pass adversarial review applies to any change touching the
offline-sync contract, store compliance declarations, or notification permission
surfaces.
- ADR-149 (model-id allowlist):
ClaudeModel(id:capabilities:) explicit
capability declaration for model IDs not compiled into
ClaudeForFoundationModels is the in-app mirror of the framework's allowlist
doctrine — never guess what an unverified model ID accepts.
Changelog
- PLAN-153 Wave G (SP-037, 2026-07-09): Android clean-architecture layering doctrine folded in (clean-room ADAPT; provenance in frontmatter/NOTICE).
Skill-Import-Attestation: reviewed-by=AE9B236FDAF0462874060C6BCFCFACF00335DC74; sha256=00ff5c83ae52e269b5121ff86de4b1e1003f3d157998053e669bf06d773e128d