---
phase: 01-project-infrastructure-module-wiring
plan: 04
type: execute
wave: 2
depends_on: [01, 02]
files_modified:
  - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt
  - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt
  - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt
  - composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt
  - composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt
  - composeApp/src/androidMain/AndroidManifest.xml
  - composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt
  - composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt
  - iosApp/iosApp/iOSApp.swift
autonomous: true
requirements: [INFRA-02]
requirements_addressed: [INFRA-02]

must_haves:
  truths:
    - "initKoin() is defined once in commonMain and called exactly once per platform entry point (no double-init — PITFALL #4)"
    - "configureLogging() runs BEFORE initKoin() on every platform (so Koin module loading can use Kermit)"
    - "App.kt (@Composable) never calls startKoin — Koin is started outside composition (anti-pattern guard in Pattern 4)"
    - "appModule is an empty Koin module placeholder; Phase 2+ adds authModule, syncModule, etc."
    - "Kermit tag is 'recipe' (D-15)"
    - "iOS Swift side calls KoinIosKt.doInitKoin() inside iOSApp.init() — one call site"
    - "Android uses MainApplication registered via android:name=\".MainApplication\" in AndroidManifest.xml"
    - "wasmJs main() initializes Koin + logging BEFORE ComposeViewport { App() } (PITFALL #8 future-proof)"
  artifacts:
    - path: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt"
      provides: "initKoin(config: KoinAppDeclaration? = null): KoinApplication helper invoking startKoin { modules(appModule) }"
      exports: ["initKoin"]
    - path: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt"
      provides: "Empty val appModule = module { } placeholder"
      exports: ["appModule"]
    - path: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt"
      provides: "configureLogging() — Logger.setTag(\"recipe\")"
      exports: ["configureLogging"]
    - path: "composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt"
      provides: "fun doInitKoin() { configureLogging(); initKoin() } — exported as Swift symbol KoinIosKt.doInitKoin"
      exports: ["doInitKoin"]
    - path: "composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt"
      provides: "class MainApplication : Application() { onCreate → configureLogging(); initKoin { androidContext(this) } }"
    - path: "composeApp/src/androidMain/AndroidManifest.xml"
      provides: "<application android:name=\".MainApplication\" ...>"
      contains: "android:name=\".MainApplication\""
    - path: "composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt"
      provides: "Desktop main() invoking configureLogging() + initKoin() before application { Window { App() } }"
    - path: "composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt"
      provides: "Wasm main() invoking configureLogging() + initKoin() before ComposeViewport { App() } (PITFALL #8)"
    - path: "iosApp/iosApp/iOSApp.swift"
      provides: "Swift @main struct with init() { KoinIosKt.doInitKoin() } and import ComposeApp"
      contains: "import ComposeApp", "KoinIosKt.doInitKoin()"
  key_links:
    - from: "iosApp/iosApp/iOSApp.swift"
      to: "composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt"
      via: "Kotlin top-level fun doInitKoin → Swift symbol KoinIosKt.doInitKoin()"
      pattern: "KoinIosKt\\.doInitKoin\\(\\)"
    - from: "composeApp/src/androidMain/AndroidManifest.xml"
      to: "composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt"
      via: "android:name=\".MainApplication\" attribute on <application>"
      pattern: "android:name=\"\\.MainApplication\""
    - from: "MainApplication.onCreate / iOSApp.init / jvm main / wasm main"
      to: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt"
      via: "initKoin() call"
      pattern: "initKoin\\("
---

<objective>
Wire the Koin + Kermit bootstrap across every composeApp platform entry point. Create the two commonMain source files (`di/Koin.kt`, `di/AppModule.kt`, `logging/Logging.kt`), the iOS Kotlin bridge (`iosMain/di/KoinIos.kt`), the Android `Application` subclass + manifest registration, modify the JVM + Wasm entry points to call `configureLogging() → initKoin()` before composition, and modify Swift's `iOSApp.swift` to call `KoinIosKt.doInitKoin()` inside `init()`. The Kermit tag is `"recipe"` (D-15); the Koin module is an empty placeholder (D-14) that Phase 2+ extends.

Purpose: Phase 1 proves the DI + logging wiring is correct from day 1 so Phase 2 (Auth) can add `authModule`, Phase 4 can add `syncModule`, etc. without revisiting the bootstrap mechanics. PITFALL #4 (double-init on iOS) is neutralized by concentrating all startup into one `initKoin()` helper with a single call site per platform.

Output: 9 files created or modified (6 new Kotlin files, 1 manifest edit, 2 existing entry-point rewrites, 1 Swift rewrite). No ViewModels yet — Phase 1 has no screens beyond the template.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-project-infrastructure-module-wiring/01-CONTEXT.md
@.planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md
@.planning/phases/01-project-infrastructure-module-wiring/01-PATTERNS.md
@.planning/phases/01-project-infrastructure-module-wiring/01-VALIDATION.md
@composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt
@composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainActivity.kt
@composeApp/src/androidMain/AndroidManifest.xml
@composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/MainViewController.kt
@composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt
@composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt
@iosApp/iosApp/iOSApp.swift
@CLAUDE.md

<interfaces>
<!-- These come from the Koin library (already wired via recipe.kotlin.multiplatform in Plan 02) -->

From io.insert-koin:koin-core:
```kotlin
fun startKoin(appDeclaration: KoinAppDeclaration): KoinApplication  // top-level
interface KoinApplication
typealias KoinAppDeclaration = KoinApplication.() -> Unit
```

From io.insert-koin:koin-dsl:
```kotlin
fun module(createdAtStart: Boolean = false, moduleDeclaration: ModuleDeclaration): Module
```

From io.insert-koin:koin-android (androidMain only):
```kotlin
// package org.koin.android.ext.koin
fun KoinApplication.androidContext(context: Context): KoinApplication
```

From co.touchlab:kermit:
```kotlin
object Logger {
    fun setTag(tag: String)
    // plus .i { }, .d { }, .e { }, .w { } methods on Logger companion
}
```

From existing composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt (do NOT modify):
```kotlin
@Composable
@Preview
fun App() { /* template body — stays as-is */ }
```

From existing composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainActivity.kt (do NOT modify — sibling reference):
```kotlin
package dev.ulfrx.recipe
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
// class MainActivity : ComponentActivity() { ... setContent { App() } }
```

From existing composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/MainViewController.kt (do NOT modify):
```kotlin
package dev.ulfrx.recipe
import androidx.compose.ui.window.ComposeUIViewController
fun MainViewController() = ComposeUIViewController { App() }
```

Current Android manifest shape (attributes to preserve when adding android:name):
```xml
<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@android:style/Theme.Material.Light.NoActionBar">
```

Current iOS Swift entry (to replace):
```swift
import SwiftUI
@main
struct iOSApp: App {
    var body: some Scene { WindowGroup { ContentView() } }
}
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create commonMain DI + logging files and iOS Kotlin bridge</name>
  <files>composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt, composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt, composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt, composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt</files>
  <read_first>
    - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 840-870 (Koin bootstrap canonical excerpts: initKoin + appModule + doInitKoin)
    - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 933-948 (Kermit bootstrap: Logger.setTag + init order)
    - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 675-690 (PITFALL #4 — single call site per platform, never from inside @Composable)
    - .planning/phases/01-project-infrastructure-module-wiring/01-PATTERNS.md lines 710-796 (pattern assignments for all 4 files)
    - .planning/phases/01-project-infrastructure-module-wiring/01-CONTEXT.md D-14 (Koin empty appModule), D-15 (Kermit tag "recipe")
  </read_first>
  <action>
  Create 4 new files.

  **File 1: `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt`**:

  ```kotlin
  package dev.ulfrx.recipe.di

  import org.koin.core.KoinApplication
  import org.koin.core.context.startKoin
  import org.koin.dsl.KoinAppDeclaration

  fun initKoin(config: KoinAppDeclaration? = null): KoinApplication = startKoin {
      config?.invoke(this)
      modules(appModule)
  }
  ```

  **File 2: `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt`**:

  ```kotlin
  package dev.ulfrx.recipe.di

  import org.koin.dsl.module

  // Phase 2 adds authModule; Phase 4 adds syncModule; Phase 5 adds catalogModule; etc.
  val appModule = module {
      // intentionally empty in Phase 1
  }
  ```

  **File 3: `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt`**:

  ```kotlin
  package dev.ulfrx.recipe.logging

  import co.touchlab.kermit.Logger

  fun configureLogging() {
      Logger.setTag("recipe")
      // Platform log writers (OSLog iOS, LogCat Android, System.out JVM/Wasm) install by default.
  }
  ```

  **File 4: `composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt`**:

  ```kotlin
  package dev.ulfrx.recipe.di

  import dev.ulfrx.recipe.logging.configureLogging

  fun doInitKoin() {
      configureLogging()
      initKoin()
  }
  ```

  CRITICAL notes (PITFALL #4 / #10):
  - The top-level `fun doInitKoin()` in file `KoinIos.kt` becomes the Swift-accessible symbol `KoinIosKt.doInitKoin()` (Kotlin generates `<FileName>Kt` for top-level declarations).
  - `doInitKoin()` is the SINGLE iOS entry point. `MainViewController()` (the `ComposeUIViewController` factory) must NOT call `startKoin` or `initKoin` — it assumes Koin is already started.
  - `configureLogging()` runs BEFORE `initKoin()` so Koin module loading can use Kermit.

  Do NOT add any expect/actual declarations — the iOS bridge is a plain top-level function, and Kermit's multiplatform Logger handles the platform-specific writer selection internally.
  </action>
  <verify>
    <automated>test -f composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt && test -f composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt && test -f composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt && test -f composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt && grep -q 'package dev.ulfrx.recipe.di' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt && grep -q 'fun initKoin(config: KoinAppDeclaration? = null)' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt && grep -q 'startKoin' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt && grep -q 'modules(appModule)' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt && grep -q 'val appModule = module' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt && grep -q 'Logger.setTag("recipe")' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/logging/Logging.kt && grep -q 'fun doInitKoin' composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt && grep -q 'configureLogging()' composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt && grep -q 'initKoin()' composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/di/KoinIos.kt</automated>
  </verify>
  <acceptance_criteria>
    - `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt` exists and contains package declaration `package dev.ulfrx.recipe.di`
    - `Koin.kt` imports `org.koin.core.KoinApplication`, `org.koin.core.context.startKoin`, `org.koin.dsl.KoinAppDeclaration`
    - `Koin.kt` defines exactly one top-level function `fun initKoin(config: KoinAppDeclaration? = null): KoinApplication` whose body is `startKoin { config?.invoke(this); modules(appModule) }`
    - `AppModule.kt` exists and contains package declaration `package dev.ulfrx.recipe.di`
    - `AppModule.kt` imports `org.koin.dsl.module`
    - `AppModule.kt` declares `val appModule = module { }` (empty — D-14)
    - `Logging.kt` exists and contains package declaration `package dev.ulfrx.recipe.logging`
    - `Logging.kt` imports `co.touchlab.kermit.Logger`
    - `Logging.kt` defines `fun configureLogging()` whose body calls `Logger.setTag("recipe")` (D-15 — exact string)
    - `KoinIos.kt` exists and contains package declaration `package dev.ulfrx.recipe.di`
    - `KoinIos.kt` imports `dev.ulfrx.recipe.logging.configureLogging`
    - `KoinIos.kt` defines `fun doInitKoin()` whose body is `configureLogging(); initKoin()` in that exact order
    - No file references `startKoin` directly outside `Koin.kt` (grep `startKoin` across composeApp/src returns only Koin.kt)
    - `App.kt` is NOT modified (anti-pattern guard — startKoin never called from inside @Composable)
  </acceptance_criteria>
  <done>Koin + Kermit commonMain wiring is in place; iOS bridge exposes a Swift-callable `KoinIosKt.doInitKoin()`.</done>
</task>

<task type="auto">
  <name>Task 2: Create MainApplication.kt + register in AndroidManifest.xml</name>
  <files>composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt, composeApp/src/androidMain/AndroidManifest.xml</files>
  <read_first>
    - composeApp/src/androidMain/AndroidManifest.xml (current 22-line content — target of edit)
    - composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainActivity.kt (sibling reference for androidMain package + imports)
    - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 895-911 (canonical MainApplication.kt)
    - .planning/phases/01-project-infrastructure-module-wiring/01-PATTERNS.md lines 800-849 (MainApplication + manifest deltas)
    - composeApp/build.gradle.kts (verify `libs.koin.android` was added to androidMain.dependencies in Plan 03)
  </read_first>
  <action>
  Create one new file and edit one existing file.

  **Create: `composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt`**:

  ```kotlin
  package dev.ulfrx.recipe

  import android.app.Application
  import dev.ulfrx.recipe.di.initKoin
  import dev.ulfrx.recipe.logging.configureLogging
  import org.koin.android.ext.koin.androidContext

  class MainApplication : Application() {
      override fun onCreate() {
          super.onCreate()
          configureLogging()
          initKoin {
              androidContext(this@MainApplication)
          }
      }
  }
  ```

  CRITICAL:
  - `package dev.ulfrx.recipe` (not `dev.ulfrx.recipe.android` — matches the existing `MainActivity.kt` sibling).
  - `androidContext(this@MainApplication)` — the qualified `this` is required because the `initKoin { ... }` lambda's `this` is a `KoinApplication`, not the Application.
  - `configureLogging()` runs FIRST, then `initKoin { ... }` — establishes the required order (PATTERNS.md "Init order on every platform entry").
  - `org.koin.android.ext.koin.androidContext` comes from `io.insert-koin:koin-android` (catalog alias `libs.koin.android`, added to `composeApp/build.gradle.kts` androidMain deps in Plan 03).

  **Edit: `composeApp/src/androidMain/AndroidManifest.xml`** — add `android:name=".MainApplication"` as the first attribute on the `<application>` element. Do NOT modify any other attribute or element.

  Resulting `<application>` tag:

  ```xml
  <application
          android:name=".MainApplication"
          android:allowBackup="true"
          android:icon="@mipmap/ic_launcher"
          android:label="@string/app_name"
          android:roundIcon="@mipmap/ic_launcher_round"
          android:supportsRtl="true"
          android:theme="@android:style/Theme.Material.Light.NoActionBar">
  ```

  The `<activity>` child element (with `android:name=".MainActivity"`) stays unchanged. The full XML structure (declarations, `<manifest>`, `<intent-filter>`) is preserved — only the single `android:name=".MainApplication"` attribute is added.
  </action>
  <verify>
    <automated>test -f composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt && grep -q '^package dev.ulfrx.recipe$' composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt && grep -q 'class MainApplication : Application()' composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt && grep -q 'override fun onCreate()' composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt && grep -q 'configureLogging()' composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt && grep -q 'androidContext(this@MainApplication)' composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt && grep -q 'android:name="\.MainApplication"' composeApp/src/androidMain/AndroidManifest.xml && grep -q 'android:name="\.MainActivity"' composeApp/src/androidMain/AndroidManifest.xml</automated>
  </verify>
  <acceptance_criteria>
    - `composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainApplication.kt` exists
    - Package declaration is exactly `package dev.ulfrx.recipe` (matches sibling `MainActivity.kt`)
    - Imports include `android.app.Application`, `dev.ulfrx.recipe.di.initKoin`, `dev.ulfrx.recipe.logging.configureLogging`, `org.koin.android.ext.koin.androidContext`
    - Class declaration is `class MainApplication : Application()`
    - `onCreate()` body calls `super.onCreate()` first, then `configureLogging()`, then `initKoin { androidContext(this@MainApplication) }` — in exactly that order
    - `composeApp/src/androidMain/AndroidManifest.xml` contains literal `android:name=".MainApplication"` attribute on the `<application>` element
    - `composeApp/src/androidMain/AndroidManifest.xml` still contains `android:name=".MainActivity"` on the `<activity>` element (unchanged)
    - `composeApp/src/androidMain/AndroidManifest.xml` still contains `<intent-filter>` with MAIN action + LAUNCHER category (unchanged)
    - `composeApp/src/androidMain/AndroidManifest.xml` top-level `<manifest>` declaration unchanged
  </acceptance_criteria>
  <done>Android Application subclass starts Koin + Kermit on process boot; manifest registers the subclass.</done>
</task>

<task type="auto">
  <name>Task 3: Wire JVM + Wasm main() entries and Swift iOSApp.swift</name>
  <files>composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt, composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt, iosApp/iosApp/iOSApp.swift</files>
  <read_first>
    - composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt (current content — target of rewrite)
    - composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt (current content — target of rewrite)
    - iosApp/iosApp/iOSApp.swift (current 11-line content — target of rewrite)
    - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 874-931 (Swift + Desktop + Wasm bootstrap)
    - .planning/phases/01-project-infrastructure-module-wiring/01-RESEARCH.md lines 733-747 (PITFALL #8 — Wasm init order)
    - .planning/phases/01-project-infrastructure-module-wiring/01-PATTERNS.md lines 852-937 (per-file deltas for these three files)
  </read_first>
  <action>
  Replace three file contents.

  **Replace: `composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt`**:

  ```kotlin
  package dev.ulfrx.recipe

  import androidx.compose.ui.window.Window
  import androidx.compose.ui.window.application
  import dev.ulfrx.recipe.di.initKoin
  import dev.ulfrx.recipe.logging.configureLogging

  fun main() {
      configureLogging()
      initKoin()
      application {
          Window(
              onCloseRequest = ::exitApplication,
              title = "recipe",
          ) {
              App()
          }
      }
  }
  ```

  **Replace: `composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt`**:

  ```kotlin
  package dev.ulfrx.recipe

  import androidx.compose.ui.ExperimentalComposeUiApi
  import androidx.compose.ui.window.ComposeViewport
  import dev.ulfrx.recipe.di.initKoin
  import dev.ulfrx.recipe.logging.configureLogging

  @OptIn(ExperimentalComposeUiApi::class)
  fun main() {
      configureLogging()
      initKoin()
      ComposeViewport {
          App()
      }
  }
  ```

  CRITICAL (PITFALL #8): `configureLogging()` and `initKoin()` MUST run BEFORE `ComposeViewport { }` — otherwise the first `koinViewModel<X>()` inside composition throws. Phase 1 has no ViewModels, so this is defensive — but the shape must be correct from day 1.

  **Replace: `iosApp/iosApp/iOSApp.swift`** (Swift file, not Kotlin):

  ```swift
  import SwiftUI
  import ComposeApp

  @main
  struct iOSApp: App {
      init() {
          KoinIosKt.doInitKoin()
      }

      var body: some Scene {
          WindowGroup {
              ContentView()
          }
      }
  }
  ```

  CRITICAL:
  - `import ComposeApp` — matches the framework basename set in `recipe.kotlin.multiplatform` (D-20 / PITFALL #10). The existing file does NOT import ComposeApp; add it.
  - `init() { KoinIosKt.doInitKoin() }` — the Swift symbol `KoinIosKt` is auto-generated from Kotlin file `KoinIos.kt` in package `dev.ulfrx.recipe.di` (created in Task 1).
  - `ContentView()` invocation stays unchanged; `ContentView.swift` already calls `MainViewControllerKt.MainViewController()` which returns a `ComposeUIViewController` — do NOT modify `ContentView.swift`.
  - Do NOT call `startKoin` from `MainViewController()` — iOS init is centralized in `iOSApp.init()` to avoid PITFALL #4.
  </action>
  <verify>
    <automated>grep -q '^package dev.ulfrx.recipe$' composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt && grep -q 'configureLogging()' composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt && grep -q 'initKoin()' composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt && grep -q 'Window(' composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt && grep -q '^package dev.ulfrx.recipe$' composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt && grep -q 'configureLogging()' composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt && grep -q 'initKoin()' composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt && grep -q 'ComposeViewport' composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt && grep -q 'import ComposeApp' iosApp/iosApp/iOSApp.swift && grep -q 'KoinIosKt.doInitKoin()' iosApp/iosApp/iOSApp.swift && grep -q 'init() {' iosApp/iosApp/iOSApp.swift</automated>
  </verify>
  <acceptance_criteria>
    - `composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt` has `configureLogging()` on a line preceding `initKoin()`, and both precede `application {` (init order invariant)
    - JVM main imports include `dev.ulfrx.recipe.di.initKoin` AND `dev.ulfrx.recipe.logging.configureLogging`
    - JVM main preserves `Window(onCloseRequest = ::exitApplication, title = "recipe") { App() }`
    - `composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt` has `configureLogging()` on a line preceding `initKoin()`, and both precede `ComposeViewport {` (PITFALL #8)
    - Web main imports include `dev.ulfrx.recipe.di.initKoin` AND `dev.ulfrx.recipe.logging.configureLogging`
    - Web main still has `@OptIn(ExperimentalComposeUiApi::class)` on `fun main()`
    - `iosApp/iosApp/iOSApp.swift` contains exactly `import SwiftUI` AND `import ComposeApp` (both imports required)
    - `iosApp/iosApp/iOSApp.swift` contains `init() {` followed by `KoinIosKt.doInitKoin()` — exactly one call
    - `iosApp/iosApp/iOSApp.swift` preserves `@main struct iOSApp: App { ... body: some Scene { WindowGroup { ContentView() } } }`
    - `MainViewController.kt` is NOT modified (the existing file returns `ComposeUIViewController { App() }` — Koin bootstrapped outside, PITFALL #4)
    - `App.kt` is NOT modified (anti-pattern guard)
  </acceptance_criteria>
  <done>All four platform entry points call `configureLogging()` then `initKoin()` before composition; iOS Swift wires `KoinIosKt.doInitKoin()` exactly once in `init()`.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Platform process start → DI container initialization | Each platform (Android onCreate, iOS App.init, JVM main, Wasm main) is a trusted bootstrap context; `initKoin()` is called once, from code we control. |
| Kotlin top-level fun → Swift generated symbol | `KoinIos.kt` in package `dev.ulfrx.recipe.di` is compiled into the `ComposeApp.framework` Swift binary as `KoinIosKt.doInitKoin()`. No runtime risk — compile-time symbol mapping. |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-01-04-01 | Denial of Service | Koin double-init on iOS second cold launch (PITFALL #4) | mitigate | Only `iOSApp.init()` calls `KoinIosKt.doInitKoin()`. `MainViewController.kt` does NOT call `startKoin`. Task 3 acceptance criteria explicitly prohibits `startKoin` in `MainViewController.kt`. If Koin is accidentally started twice, `KoinApplicationAlreadyStartedException` fires on launch — visible and easy to diagnose. |
| T-01-04-02 | Denial of Service | Wasm composition runs before Koin init (PITFALL #8) | mitigate | Task 3 explicitly orders `configureLogging() → initKoin() → ComposeViewport { }`. Phase 1 has no ViewModels so the symptom would not surface until Phase 5+, but the order is correct from day 1. |
| T-01-04-03 | Tampering | `App.kt` calling `startKoin` from inside @Composable | mitigate | Task 1 + Task 3 acceptance criteria prohibit modification of `App.kt`. `App.kt` template preserves the anti-pattern-free shape. |
| T-01-04-04 | Information Disclosure | Kermit logs leaking sensitive data | accept | Phase 1 has no sensitive data in the codebase (no auth, no user records, no PII). Kermit tag `"recipe"` is a build identifier, not a secret. Revisit when Phase 2 (Auth) introduces tokens — at that point, Kermit's `.i { }` lambda evaluation prevents accidental string concat of secrets if authors follow the lambda idiom. |
| T-01-04-05 | Elevation of Privilege | Android manifest `android:name=".MainApplication"` registers custom Application subclass | accept | This is the standard Android lifecycle — `MainApplication.onCreate()` runs in the app's own process, same privilege as `MainActivity`. No escalation. |
</threat_model>

<verification>
Phase-level verification for this plan:

- Task 1, 2, 3 `<automated>` blocks pass (grep-based).
- `tools/verify-no-version-literals.sh` continues to exit 0 (no build files modified).
- `tools/verify-shared-pure.sh` continues to exit 0 (shared/ not touched).
- Plan 07 runs `./gradlew build` and `./gradlew :composeApp:jvmTest` — those will exercise `initKoin()` via composition and catch any Koin config error.

No `./gradlew` invocation is in this plan's `<automated>` blocks — Plan 05 + Plan 07 run the compile gates. Keep this plan's verification grep-fast (<5s total).
</verification>

<success_criteria>
- 6 new commonMain/iosMain/androidMain Kotlin files created (Koin.kt, AppModule.kt, Logging.kt, KoinIos.kt, MainApplication.kt — and the init order is correct in each)
- AndroidManifest.xml has `android:name=".MainApplication"` attribute added
- JVM + Wasm main() entries call `configureLogging()` THEN `initKoin()` BEFORE composition
- `iOSApp.swift` imports `ComposeApp` and calls `KoinIosKt.doInitKoin()` in `init()`
- `App.kt` unmodified (anti-pattern guard)
- `MainViewController.kt` unmodified (PITFALL #4 guard)
</success_criteria>

<output>
After completion, create `.planning/phases/01-project-infrastructure-module-wiring/01-04-SUMMARY.md` recording: 6 files created + 3 files modified paths, Kermit tag set to `"recipe"`, Koin appModule content (empty), and confirmation that `App.kt` / `MainViewController.kt` / `ContentView.swift` were NOT modified.
</output>
