@code-migration/wow-migrator 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-migration/wow-migrator",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Install KMP migration skills into Claude Code, Codex, Cursor, Gemini, OpenCode, OpenClaw, and JiuwenSwarm via npm install.",
5
5
  "keywords": [
6
6
  "android",
@@ -63,7 +63,7 @@ Module-first migrator for Legacy Android → KMP target assembly. **Upstream ana
63
63
 
64
64
  ## Protocol Summary
65
65
 
66
- 0. Pre-flight — [dependencies.yaml](dependencies.yaml): `rg` / `git` / `curl`, optional `jetbrains` MCP (`optional_mcp`), upstream analyst **P6** (`upstream_inputs`); record `dependency_preflight` in `run_manifest.json`.
66
+ 0. Pre-flight — [dependencies.yaml](dependencies.yaml): `rg` / `git` / `curl`, optional `jetbrains` MCP (`optional_mcp`), upstream analyst **P6** (`upstream_inputs`); **identify `design_mode` from user input (default `mvi`)**; record `dependency_preflight` and `design_mode` in `run_manifest.json`.
67
67
  1. Verify analyst **P6**; `run_manifest.json`, `upstream_analyst_index.json`.
68
68
  2. Migration inventory + `modules_migration_index.json`.
69
69
  3. Workspace state init.
@@ -73,6 +73,23 @@ Module-first migrator for Legacy Android → KMP target assembly. **Upstream ana
73
73
  7. Global representation + completion-report `report` mode.
74
74
  8. **kmp-test-validator** — **mandatory** when **V0** ready (MG17).
75
75
 
76
+ ## Design Mode (architecture pattern)
77
+
78
+ The migrator targets one presentation architecture per run, selected at **pre-flight (Step 0)** by the Leader from the **user input**, then frozen for the whole run and recorded in `run_manifest.json` → `design_mode`.
79
+
80
+ | `design_mode` | Architecture reference | When chosen |
81
+ |---|---|---|
82
+ | `mvi` **(default)** | [references/kmp-mvi-flowredux.md](references/kmp-mvi-flowredux.md) | Default when user input gives no clear signal; or user mentions MVI, FlowRedux, state machine, reducer, intent, unidirectional, sealed `State`/`Action`, `dispatch`, `inState`, `onEnter` |
83
+ | `mvvm` | [references/kmp-mvvm.md](references/kmp-mvvm.md) | User mentions MVVM, shared `ViewModel`, `StateFlow`/`uiState`, `viewModelScope`, `collectAsStateWithLifecycle`, KMP-ObservableViewModel, SKIE |
84
+
85
+ Both modes also follow [references/kmp-expert.md](references/kmp-expert.md) for base KMP/CMP conventions.
86
+
87
+ **Rules**:
88
+ - **Default is `mvi`** — when the user input contains no explicit or implied architecture signal, the Leader MUST select `mvi`.
89
+ - Record the decision as `design_mode: { value, source: "user_input | default", signals: [] }` in `run_manifest.json` at **MG0**.
90
+ - The Leader passes `design_mode` and the resolved `architecture_reference_path` to every architecture-producing role (planning-gate, prep, module-implementation, module-node-review-fix, global-migration-phase) and to TPA for target-pattern detection.
91
+ - `design_mode` is **fixed for the run**; a mid-run change requires a fresh run, not in-place mutation.
92
+
76
93
  ## Skill Chain (mandatory)
77
94
 
78
95
  ```text
@@ -108,6 +125,7 @@ android-project-analyst (P6) → android-to-kmp-migrator (M0–V0) → kmp-test-
108
125
  | [bind.md](bind.md) | Limits, constraints, failures |
109
126
  | [dependencies.yaml](dependencies.yaml) | CLI + optional MCP per role |
110
127
  | [roles/](roles/) | Role specs |
128
+ | [references/](references/) | Architecture references: `kmp-mvi-flowredux.md` (MVI, default), `kmp-mvvm.md` (MVVM), `kmp-expert.md` (base KMP/CMP) |
111
129
 
112
130
  ## Handoff Gates
113
131
 
@@ -123,6 +141,7 @@ android-project-analyst (P6) → android-to-kmp-migrator (M0–V0) → kmp-test-
123
141
  ## Shared Rules
124
142
 
125
143
  - **Skill chain**: `android-project-analyst` **P6** before migrator; `kmp-test-validator` **after** migrator **V0** — both mandatory.
144
+ - **Design mode**: identified from user input at pre-flight, **default `mvi`**; frozen for the run; architecture-producing roles MUST follow the resolved `architecture_reference_path` (`kmp-mvi-flowredux.md` for `mvi`, `kmp-mvvm.md` for `mvvm`).
126
145
  - **Target KMP edit mandate**: after analyst P6 understanding, migrator roles MUST create or update production files under `kmp_target_project_path`. Planning-only or artifact-only completion is invalid.
127
146
  - **Roles that edit target** (record `changed_files[]` or `integration_changed_files[]`):
128
147
  - `migration-prep` — optional scaffold edits (theme, resources, routes, models) when planning allows
@@ -29,6 +29,7 @@
29
29
  - **Skill chain (mandatory)**:
30
30
  - **Before migrator**: `android-project-analyst` MUST finish and produce package **P6** (`handoff_gates.P6.ready = true`). If P6 is missing or stale, return `blocked` and dispatch analyst — do not start migrator nodes.
31
31
  - **After migrator**: when package **V0** is ready, Leader MUST invoke `kmp-test-validator` (MG17). Migrator completion without validator dispatch is invalid.
32
+ - **Design mode**: at pre-flight the Leader identifies `design_mode` from user input — **default `mvi`** when no architecture signal is present. It is recorded in `run_manifest.json`, **frozen for the run**, and every architecture-producing role MUST follow the resolved `architecture_reference_path` (`references/kmp-mvi-flowredux.md` for `mvi`, `references/kmp-mvvm.md` for `mvvm`). Both modes also follow `references/kmp-expert.md`.
32
33
  - **Role schedule**: dispatch only role IDs listed in [SKILL.md](SKILL.md).
33
34
  - **Mode discipline**:
34
35
  - `module-implementation`: `ui` then `logic` — separate invocations
@@ -44,6 +45,9 @@
44
45
  | Failure | Response |
45
46
  |---|---|
46
47
  | Unknown or invalid role ID | Reject; use role from `SKILL.md` registry |
48
+ | `design_mode` not identified at pre-flight | Default to `mvi`; record `source: default` in `run_manifest.json` |
49
+ | Architecture-producing dispatch missing `design_mode` | Reject; re-dispatch with `design_mode` + `architecture_reference_path` |
50
+ | Code produced against wrong architecture vs `design_mode` | `needs_rerun` owning role with correct reference |
47
51
  | `ui` and `logic` combined | Reject invocation |
48
52
  | `integrate` and `align` combined | Reject invocation |
49
53
  | Verification restoration failed | Rerun `module-implementation` or `migration-prep`; no completion record |
@@ -147,7 +147,7 @@ output_root = <output_dir or ~/.a2c_agents/migration>/android-to-kmp-migrator
147
147
 
148
148
  | Step | Gate | Required artifacts before next step |
149
149
  |---|---|---|
150
- | `MG0` | Run lock | `run_manifest.json`, `upstream-index/upstream_analyst_index.json` |
150
+ | `MG0` | Run lock | `run_manifest.json` (incl. `design_mode`), `upstream-index/upstream_analyst_index.json` |
151
151
  | `MG1` | Workspace init | global `migration_workspace_state.*` |
152
152
  | `MG2` | Migration index | `migration_module_inventory.*`, `modules_migration_index.json`, per-module `module_brief.json` |
153
153
  | `MG3` | Target baseline | global `target-project-assistant/*` (`mode: global_baseline`) + `target_alignment_revision.*` |
@@ -241,6 +241,30 @@ output_root = <output_dir or ~/.a2c_agents/migration>/android-to-kmp-migrator
241
241
 
242
242
  ## Key Artifact Content Requirements
243
243
 
244
+ ### `run_manifest.json` → `design_mode`
245
+
246
+ Records the presentation architecture pattern, identified from **user input** at pre-flight (Step 0a) and **frozen for the run**. Default is `mvi` when user input gives no clear signal.
247
+
248
+ ```json
249
+ {
250
+ "design_mode": {
251
+ "value": "mvi",
252
+ "source": "default",
253
+ "signals": [],
254
+ "architecture_reference_path": "references/kmp-mvi-flowredux.md"
255
+ }
256
+ }
257
+ ```
258
+
259
+ | Field | Meaning |
260
+ |---|---|
261
+ | `value` | `mvi` (default) \| `mvvm` |
262
+ | `source` | `user_input` when a signal was matched; `default` when none |
263
+ | `signals` | matched keywords/phrases from user input (empty when defaulted) |
264
+ | `architecture_reference_path` | `references/kmp-mvi-flowredux.md` for `mvi`, `references/kmp-mvvm.md` for `mvvm` |
265
+
266
+ The Leader MUST pass `design_mode.value` and `design_mode.architecture_reference_path` into every architecture-producing dispatch (`migration-planning-gate`, `migration-prep`, `module-implementation`, `module-node-review-fix`, `global-migration-phase`) and to `target-project-assistant` for target-pattern detection.
267
+
244
268
  ### `upstream_analyst_index.json`
245
269
 
246
270
  ```json
@@ -328,7 +352,7 @@ Human/agent-readable synthesis of align mode; includes `entry_point_alignment_re
328
352
 
329
353
  ## Leader Obligations
330
354
 
331
- 1. Verify analyst package `P6` before `MG0` completes.
355
+ 1. Verify analyst package `P6` before `MG0` completes; identify `design_mode` from user input (default `mvi`) and write it to `run_manifest.json` at `MG0`, then pass `design_mode` + `architecture_reference_path` into every architecture-producing dispatch.
332
356
  2. Dispatch `target-project-assistant` for all target-project questions; other roles MUST reference TPA artifacts instead of re-analyzing target ad hoc.
333
357
  3. Ensure each module produces **target KMP edits** via `module-implementation` (and optional `migration-prep` / `module-node-review-fix` `fix`) before writing `module_completion_record.json`.
334
358
  4. Write `module_completion_record.json` after each module passes `migration-verification`; include aggregated `target_changed_files[]` for the module.
@@ -353,3 +377,6 @@ Human/agent-readable synthesis of align mode; includes `entry_point_alignment_re
353
377
  | Planning complete but `changed_files[]` empty when tasks require edits | `needs_rerun` → `module-implementation` or `migration-prep` |
354
378
  | `changed_files` paths outside `kmp_target_project_path` | `blocked` — reject artifact; rerun owning role |
355
379
  | `target_files_exist` failed | `needs_rerun` → owning edit role |
380
+ | `design_mode` missing from `run_manifest.json` at MG0 | `blocked`, `reason: missing` — Leader must identify (default `mvi`) before module dispatch |
381
+ | Architecture-producing dispatch missing `design_mode` / `architecture_reference_path` | Reject dispatch; re-dispatch with `design_mode` injected |
382
+ | Implementation/review uses the wrong architecture vs `design_mode` | `needs_rerun` → owning role with correct `architecture_reference_path` |
@@ -0,0 +1,260 @@
1
+ ---
2
+ name: kmp-expert
3
+ description: >
4
+ Foundational Kotlin Multiplatform / Compose Multiplatform knowledge for KMP/CMP
5
+ development. Use when working on any KMP/CMP task: project setup, source set
6
+ hierarchy, expect/actual, Gradle/version-catalog config, choosing the library
7
+ stack (Ktor, SQLDelight, Room, Koin, serialization), iOS interop (SKIE,
8
+ KMP-NativeCoroutines), Wasm/web targets, or debugging build/source-set issues.
9
+ Triggers on Kotlin Multiplatform, KMP, CMP, Compose Multiplatform, commonMain,
10
+ expect/actual, shared module, iosMain, klib, Kotlin/Native.
11
+ ---
12
+
13
+ # Kotlin Multiplatform / Compose Multiplatform — expert skill
14
+
15
+ This is the foundational knowledge layer for KMP/CMP work. For presentation
16
+ architecture, see the companion `kmp-mvvm` and `kmp-mvi-flowredux` skills — this
17
+ skill covers everything beneath them: structure, source sets, interop, and the stack.
18
+
19
+ References:
20
+ - https://kotlinlang.org/docs/multiplatform/multiplatform-discover-project.html
21
+ - https://kotlinlang.org/docs/multiplatform/multiplatform-hierarchy.html
22
+ - https://blog.jetbrains.com/kotlin/2026/05/new-kmp-default-structure/
23
+
24
+ ## KMP vs CMP — know which one you mean
25
+
26
+ - **KMP (Kotlin Multiplatform)** — shares non-UI logic (networking, persistence,
27
+ business rules). You build the UI natively per platform (Compose on Android,
28
+ SwiftUI on iOS). Officially supported by Google for Android/iOS logic sharing.
29
+ - **CMP (Compose Multiplatform)** — a declarative UI framework layered on top of KMP
30
+ that lets you share the UI too. On iOS it renders via Skia (Skiko), not native widgets.
31
+
32
+ Decision rule: push as much as possible into `commonMain`. Use KMP-only when you need
33
+ the deepest native iOS fidelity; use CMP when one UI codebase outweighs that.
34
+
35
+ ---
36
+
37
+ ## Compilation targets
38
+
39
+ | Target | Compiler | Output |
40
+ |---|---|---|
41
+ | Android / JVM backend | Kotlin/JVM | JVM bytecode |
42
+ | iOS / macOS / watchOS / tvOS / Linux / Windows | Kotlin/Native (LLVM) | Native binary / `.framework` |
43
+ | Web | Kotlin/Wasm (or legacy Kotlin/JS) | WebAssembly / JS |
44
+
45
+ Kotlin/Native produces standalone binaries with no VM, which is how KMP gets native
46
+ performance on iOS.
47
+
48
+ ---
49
+
50
+ ## Project structure (JetBrains 2026 default)
51
+
52
+ The default structure changed in 2026 to give each module a single responsibility,
53
+ aligning with Android Gradle Plugin 9.0.
54
+
55
+ ```
56
+ project-root/
57
+ ├── shared/ ← KMP library: ALL shared code (the only multiplatform module)
58
+ │ └── src/
59
+ │ ├── commonMain/ ← 80–90% of code lives here
60
+ │ ├── androidMain/ ← Android actuals + JVM-only deps
61
+ │ ├── iosMain/ ← iOS actuals (intermediate; see hierarchy below)
62
+ │ ├── desktopMain/ ← JVM desktop actuals
63
+ │ ├── wasmJsMain/ ← web actuals
64
+ │ └── commonTest/
65
+ ├── androidApp/ ← thin Android entry point, depends on :shared
66
+ ├── iosApp/ ← Xcode project consuming the shared framework
67
+ ├── desktopApp/ ← JVM desktop entry point
68
+ └── webApp/ ← Wasm/JS entry point
69
+ ```
70
+
71
+ The old single `composeApp` module that mixed library + app concerns is replaced by a
72
+ clean `shared` library plus separate `*App` modules. Generate new projects at
73
+ `kmp.jetbrains.com`. Reference samples: `KMP-App-Template`, `kotlinconf-app`, RSS Reader.
74
+
75
+ For large apps, modularize further: feature modules (`:feature-x`) each split into
76
+ `domain`/`data`/`presentation`, plus shared `:core-network`, `:core-db`, `:core-ui`.
77
+
78
+ ---
79
+
80
+ ## Source set hierarchy — the mental model
81
+
82
+ Source sets form a tree. During compilation for a target, Kotlin combines **all** source
83
+ sets on the path from `commonMain` down to the platform leaf.
84
+
85
+ ```
86
+ commonMain
87
+ / \
88
+ appleMain jvmAndroidShared (you create if needed)
89
+ / \ / \
90
+ iosMain macosMain androidMain desktopMain
91
+ / \
92
+ iosArm64 iosSimulatorArm64
93
+ ```
94
+
95
+ Key facts:
96
+ - `commonMain` compiles to every target; code here may use only multiplatform APIs.
97
+ - **Intermediate source sets** (`iosMain`, `appleMain`, `nativeMain`) share code among a
98
+ subset of targets. Put `actual` declarations in the intermediate set (e.g. `iosMain`),
99
+ not in each leaf (`iosArm64Main`, `iosSimulatorArm64Main`).
100
+ - The **default hierarchy template** (modern Kotlin) auto-wires `nativeMain`, `appleMain`,
101
+ `iosMain`, etc. You rarely need manual `dependsOn` anymore.
102
+ - Kotlin does **not** auto-share a JVM+Android source set. If their deps overlap, create a
103
+ custom intermediate set and wire it with `dependsOn` (IDE intellisense may complain but
104
+ it compiles).
105
+
106
+ ---
107
+
108
+ ## expect / actual — the platform contract
109
+
110
+ The cornerstone mechanism. Declare an `expect` in `commonMain`; provide an `actual` per
111
+ target (or per intermediate source set).
112
+
113
+ ```kotlin
114
+ // commonMain
115
+ expect fun getPlatform(): Platform
116
+ interface Platform { val name: String }
117
+
118
+ // androidMain
119
+ actual fun getPlatform(): Platform = object : Platform {
120
+ override val name = "Android ${Build.VERSION.SDK_INT}"
121
+ }
122
+
123
+ // iosMain
124
+ actual fun getPlatform(): Platform = object : Platform {
125
+ override val name = UIDevice.currentDevice.systemName()
126
+ }
127
+ ```
128
+
129
+ Three forms:
130
+ 1. `expect fun` / `actual fun` — functions (most common)
131
+ 2. `expect class` / `actual class` — full classes
132
+ 3. **Interface + expect factory** (recommended) — declare an `interface` in common, an
133
+ `expect fun buildX(): Interface` factory, and platform `actual` factories returning
134
+ platform impls. This keeps common code free of platform types and is easier to test.
135
+
136
+ Prefer the interface+factory form over `expect class`; it avoids the strictness of
137
+ matching every member signature across platforms.
138
+
139
+ When NOT to use expect/actual: if a library already provides a multiplatform API (Ktor,
140
+ SQLDelight, kotlinx-datetime), use it directly in `commonMain` — no expect/actual needed.
141
+
142
+ ---
143
+
144
+ ## The standard 2026 library stack
145
+
146
+ Verify multiplatform support at `klibs.io` before adding any dependency.
147
+
148
+ | Concern | Library | Notes |
149
+ |---|---|---|
150
+ | Networking | **Ktor client** | `commonMain` core + per-platform engine (OkHttp/Android, Darwin/iOS, CIO/desktop) |
151
+ | Serialization | **kotlinx.serialization** | apply the plugin to the module that defines `@Serializable` types |
152
+ | Persistence | **SQLDelight** (typed SQL) or **Room KMP** (Google, DAO-style) | SQLDelight for control; Room for Android-team familiarity |
153
+ | Key-value | **multiplatform-settings** or **DataStore** | small prefs |
154
+ | DI | **Koin** | no codegen → fast multiplatform compile; `koin-compose-viewmodel` for CMP |
155
+ | Dates/time | **kotlinx-datetime** | multiplatform `Instant`, `LocalDate`, time zones |
156
+ | Coroutines | **kotlinx-coroutines-core** | the concurrency backbone |
157
+ | Image loading | **Coil 3** | multiplatform |
158
+ | Navigation | Navigation-Compose (KMP), **Voyager**, or **Decompose** | Decompose pairs well with KMP lifecycle |
159
+ | Testing | **kotlinx-coroutines-test** + **Turbine** | flow testing |
160
+
161
+ ---
162
+
163
+ ## Gradle configuration (modern, terse)
164
+
165
+ ```kotlin
166
+ // shared/build.gradle.kts
167
+ plugins {
168
+ kotlin("multiplatform")
169
+ kotlin("plugin.serialization")
170
+ id("com.android.library")
171
+ }
172
+
173
+ kotlin {
174
+ androidTarget()
175
+ iosX64(); iosArm64(); iosSimulatorArm64()
176
+ jvm("desktop")
177
+ wasmJs { browser() }
178
+
179
+ // Default hierarchy template auto-creates iosMain/appleMain/nativeMain.
180
+ sourceSets {
181
+ commonMain.dependencies {
182
+ implementation(libs.ktor.client.core)
183
+ implementation(libs.kotlinx.serialization.json)
184
+ implementation(libs.koin.core)
185
+ implementation(libs.kotlinx.coroutines.core)
186
+ }
187
+ androidMain.dependencies { implementation(libs.ktor.client.okhttp) }
188
+ iosMain.dependencies { implementation(libs.ktor.client.darwin) }
189
+ getByName("desktopMain").dependencies { implementation(libs.ktor.client.cio) }
190
+ commonTest.dependencies {
191
+ implementation(libs.turbine)
192
+ implementation(libs.kotlinx.coroutines.test)
193
+ }
194
+ }
195
+ }
196
+ ```
197
+
198
+ Use a `gradle/libs.versions.toml` version catalog for all dependency coordinates —
199
+ it is the standard for keeping versions consistent across modules.
200
+
201
+ ---
202
+
203
+ ## iOS interop — the hard part
204
+
205
+ Kotlin/Native exports through an Objective-C bridge, which is lossy: sealed classes lose
206
+ exhaustiveness, `Int?` becomes a boxed `KotlinInt`, and `suspend` functions become
207
+ completion-handler callbacks. Mitigations:
208
+
209
+ | Tool | What it fixes |
210
+ |---|---|
211
+ | **SKIE** | Maps Kotlin `Flow` → Swift `AsyncSequence`, sealed classes → Swift enums with associated values, default args. Easy setup, less verbose. |
212
+ | **KMP-NativeCoroutines** | Maps `suspend`/`Flow` to Swift `async/await`, Combine, or RxSwift with proper cancellation. The more battle-tested option. |
213
+ | **KMP-ObservableViewModel** | Lets SwiftUI observe Kotlin ViewModels and handles the iOS lifecycle/store-owner boilerplate. |
214
+ | **Swift Export (emerging)** | Direct Kotlin→Swift modules (suspend→async/await, sealed→enums) without the ObjC layer. Still experimental — don't build production architecture on it yet. |
215
+
216
+ Practical rules:
217
+ - Reduce the exported surface: mark internal code `internal`/`private` and enable
218
+ `explicitApi()` so you don't export everything by default.
219
+ - Annotate the flows/suspend functions you expose with the chosen tool's annotation
220
+ (`@NativeCoroutines`, `@NativeCoroutinesState`).
221
+ - iOS engineers should never see `KotlinInt` or completion handlers — that's a sign your
222
+ interop layer is missing.
223
+
224
+ ---
225
+
226
+ ## Common gotchas
227
+
228
+ | Symptom | Cause | Fix |
229
+ |---|---|---|
230
+ | `@Serializable` compiles but crashes at runtime | serialization plugin not on the module defining the type | Apply `kotlin("plugin.serialization")` to that module |
231
+ | IDE shows red but it compiles | IntelliSense lagging on intermediate source sets | Sync Gradle; the build is the source of truth |
232
+ | Can't share code across JVM + Android | Kotlin doesn't auto-create that intermediate set | Create a custom source set with `dependsOn` |
233
+ | iOS sees opaque classes, no exhaustive switch | Raw ObjC bridge without SKIE | Add SKIE or KMP-NativeCoroutines |
234
+ | Slow Kotlin/Native builds | Native compilation is inherently slower than JVM | Use `embedAndSign`/`SKIE` caching; iterate on Android/desktop, verify on iOS less often |
235
+ | `expect class` won't compile — member mismatch | strict signature matching across platforms | Switch to interface + `expect` factory function |
236
+ | Coroutine on iOS never cancels | exposed raw `suspend` without interop annotation | Annotate with `@NativeCoroutines` for proper cancellation |
237
+ | Android-only API leaked into commonMain | wrote `android.*` import in common code | Move it behind expect/actual or an interface |
238
+
239
+ ---
240
+
241
+ ## Build/run quick reference
242
+
243
+ ```bash
244
+ ./gradlew :shared:build # compile the shared library, all targets
245
+ ./gradlew :androidApp:installDebug # build + install Android app
246
+ ./gradlew :desktopApp:run # run desktop (JVM) app
247
+ ./gradlew :shared:iosSimulatorArm64Test # run iOS tests on simulator
248
+ # iOS app itself is built/run from Xcode (iosApp project) consuming the framework
249
+ ```
250
+
251
+ ---
252
+
253
+ ## When advising on KMP/CMP work
254
+
255
+ 1. Default to putting code in `commonMain`; only drop to platform source sets when an API
256
+ genuinely differs. Reach for expect/actual last, libraries first.
257
+ 2. Check `klibs.io` for multiplatform support before suggesting any dependency.
258
+ 3. For UI: ask whether they want shared UI (CMP) or native UI (KMP-only) before scaffolding.
259
+ 4. For presentation logic, defer to the `kmp-mvvm` or `kmp-mvi-flowredux` skill.
260
+ 5. Always consider the iOS interop cost of anything exposed across the Swift boundary.
@@ -0,0 +1,344 @@
1
+ ---
2
+ name: kmp-mvi-flowredux
3
+ description: >
4
+ KMP/CMP MVI architecture using FlowRedux state machines. Use when building or
5
+ reviewing Kotlin Multiplatform features that need MVI pattern: state machines,
6
+ sealed State/Action classes, unidirectional data flow, Compose Multiplatform UI
7
+ wiring, or when the user mentions FlowRedux, MVI, state machine, inState, onEnter,
8
+ dispatch, or sealed interface State.
9
+ ---
10
+
11
+ # KMP MVI with FlowRedux — architecture skill
12
+
13
+ Reference: https://freeletics.github.io/FlowRedux/
14
+
15
+ ## Core concepts
16
+
17
+ MVI (Model-View-Intent) enforces **unidirectional data flow**:
18
+
19
+ ```
20
+ Action (Intent) ──► StateMachine ──► State ──► UI
21
+ ▲ │
22
+ └────────────────────────────────────────┘
23
+ dispatch()
24
+ ```
25
+
26
+ FlowRedux models this as an explicit **state machine** with a Coroutines DSL:
27
+ - `State` — sealed interface; each subtype is a discrete screen state
28
+ - `Action` — sealed interface; every user gesture or external event
29
+ - `FlowReduxStateMachineFactory` — holds the `spec { }` DSL and produces instances
30
+ - `FlowReduxStateMachine` — public API: `state: Flow<State>` + `suspend dispatch(action)`
31
+
32
+ ---
33
+
34
+ ## Architecture layers
35
+
36
+ ```
37
+ :shared/commonMain
38
+ ├── feature/
39
+ │ ├── model/
40
+ │ │ ├── FeatureState.kt ← sealed interface State + subtypes
41
+ │ │ └── FeatureAction.kt ← sealed interface Action + subtypes
42
+ │ ├── statemachine/
43
+ │ │ └── FeatureStateMachineFactory.kt ← FlowReduxStateMachineFactory
44
+ │ └── domain/
45
+ │ └── FeatureRepository.kt ← interface; expect/actual for platform impl
46
+ └── di/
47
+ └── FeatureModule.kt ← Koin module wiring factory + repo
48
+
49
+ :composeApp/commonMain
50
+ └── feature/
51
+ ├── FeatureScreen.kt ← @Composable, uses produceStateMachine()
52
+ └── FeatureViewModel.kt ← optional: ViewModel wrapper for lifecycle
53
+ ```
54
+
55
+ **Rule:** All state machine logic lives in `:shared/commonMain`. The UI layer
56
+ (`:composeApp`) only renders state and dispatches actions — zero business logic.
57
+
58
+ ---
59
+
60
+ ## Coding guidance
61
+
62
+ ### 1. Define State and Action as sealed interfaces
63
+
64
+ ```kotlin
65
+ // shared/commonMain/.../model/ItemListState.kt
66
+ sealed interface ItemListState {
67
+ data object Loading : ItemListState
68
+ data class ShowContent(val items: List<Item>) : ItemListState
69
+ data class Error(val message: String, val countdown: Int) : ItemListState
70
+ }
71
+
72
+ // shared/commonMain/.../model/ItemListAction.kt
73
+ sealed interface ItemListAction {
74
+ data object RetryLoading : ItemListAction
75
+ data class ToggleFavorite(val itemId: Int) : ItemListAction
76
+ }
77
+ ```
78
+
79
+ Rules:
80
+ - `State` subtypes are `data object` (no fields) or `data class` (immutable fields)
81
+ - Never put UI logic inside a `State` — it is plain data
82
+ - `Action` subtypes carry only the payload the handler needs — nothing else
83
+
84
+ ### 2. Write the StateMachineFactory in commonMain
85
+
86
+ ```kotlin
87
+ // shared/commonMain/.../statemachine/ItemListStateMachineFactory.kt
88
+ class ItemListStateMachineFactory(
89
+ private val repository: ItemRepository
90
+ ) : FlowReduxStateMachineFactory<ItemListState, ItemListAction>() {
91
+
92
+ init {
93
+ initializeWith { Loading }
94
+
95
+ spec {
96
+ inState<Loading> {
97
+ onEnter { state ->
98
+ try {
99
+ val items = repository.loadItems()
100
+ state.override { ShowContent(items) }
101
+ } catch (t: Throwable) {
102
+ state.override { Error(t.message ?: "Unknown error", countdown = 3) }
103
+ }
104
+ }
105
+ }
106
+
107
+ inState<Error> {
108
+ on<RetryLoading> { _, state ->
109
+ state.override { Loading }
110
+ }
111
+
112
+ collectWhileInState(timerEverySecond()) { _, state ->
113
+ val next = state.snapshot.countdown - 1
114
+ if (next <= 0) state.override { Loading }
115
+ else state.mutate { copy(countdown = next) }
116
+ }
117
+ }
118
+
119
+ inState<ShowContent> {
120
+ on<ToggleFavorite>(executionPolicy = ExecutionPolicy.Unordered) { action, state ->
121
+ // delegate to child state machine — see composing section
122
+ state.mutate { copy(items = items.map { item ->
123
+ if (item.id == action.itemId) item.copy(toggling = true) else item
124
+ }) }
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ ```
131
+
132
+ Rules:
133
+ - One factory per feature/screen — not one global machine
134
+ - `initializeWith { }` always first in `init`
135
+ - Extract large handlers into `private suspend fun` for readability and unit-testability
136
+ - Never call `state.override` or `state.mutate` after the function returns — these are the only mutation points
137
+
138
+ ### 3. DSL blocks reference
139
+
140
+ | Block | Triggers when | Typical use |
141
+ |---|---|---|
142
+ | `onEnter { }` | State is entered | Load data, start timers |
143
+ | `on<Action> { }` | Action arrives while in this state | User interactions |
144
+ | `collectWhileInState(flow) { }` | Flow emits while in this state | Countdown, realtime updates |
145
+ | `onActionEffect<Action> { }` | Like `on<>` but does NOT transition state | Logging, analytics side-effects |
146
+ | `condition { }` | Wraps sub-blocks with a predicate | Feature flags, conditional handlers |
147
+ | `untilIdentityChanged { }` | Re-enters block when identity field changes | Pagination, refresh on id change |
148
+
149
+ ### 4. ExecutionPolicy — choose deliberately
150
+
151
+ ```kotlin
152
+ // Default: CancelPrevious — cancel in-flight handler when same action fires again
153
+ on<SearchQueryChanged> { action, state -> ... }
154
+
155
+ // Unordered — run in parallel, no order guarantee; use for independent async ops
156
+ on<ToggleFavorite>(executionPolicy = ExecutionPolicy.Unordered) { ... }
157
+
158
+ // Ordered — sequential; use when order matters and you must not cancel
159
+ on<PageLoad>(executionPolicy = ExecutionPolicy.Ordered) { ... }
160
+
161
+ // Throttled — ignore re-triggers within duration; use for debounce-like behaviour
162
+ on<ButtonClick>(executionPolicy = ExecutionPolicy.Throttled(500.milliseconds)) { ... }
163
+ ```
164
+
165
+ ### 5. Compose Multiplatform UI wiring
166
+
167
+ ```kotlin
168
+ // composeApp/commonMain/.../FeatureScreen.kt
169
+ @Composable
170
+ fun ItemListScreen(factory: ItemListStateMachineFactory) {
171
+ val stateMachine = factory.produceStateMachine() // FlowRedux compose extension
172
+ val state by stateMachine.state.collectAsState() // or use stateMachine.state.value
173
+
174
+ when (val s = state) {
175
+ is Loading -> LoadingIndicator()
176
+ is ShowContent -> ContentList(s.items, onToggle = {
177
+ stateMachine.dispatch(ToggleFavorite(it)) // dispatch is non-suspending in compose ext
178
+ })
179
+ is Error -> ErrorView(s.message, s.countdown, onRetry = {
180
+ stateMachine.dispatch(RetryLoading)
181
+ })
182
+ }
183
+ }
184
+ ```
185
+
186
+ For Android ViewModel lifecycle:
187
+
188
+ ```kotlin
189
+ class ItemListViewModel @Inject constructor(
190
+ private val factory: ItemListStateMachineFactory
191
+ ) : ViewModel() {
192
+ private val stateMachine = factory.shareIn(viewModelScope)
193
+ val state: StateFlow<ItemListState> = stateMachine.state
194
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), Loading)
195
+
196
+ fun dispatch(action: ItemListAction) {
197
+ viewModelScope.launch { stateMachine.dispatch(action) }
198
+ }
199
+ }
200
+ ```
201
+
202
+ ### 6. Composing state machines (hierarchical)
203
+
204
+ Delegate a sub-problem to a child state machine rather than bloating the parent spec:
205
+
206
+ ```kotlin
207
+ inState<ShowContent> {
208
+ onActionStartStateMachine(
209
+ stateMachineFactoryBuilder = { action: ToggleFavorite ->
210
+ FavoriteStatusStateMachineFactory(
211
+ itemId = action.itemId,
212
+ httpClient = httpClient
213
+ )
214
+ }
215
+ ) { favoriteStatus: FavoriteStatus ->
216
+ // merge child state into parent state
217
+ mutate { copy(items = items.updateFavoriteStatus(favoriteStatus)) }
218
+ }
219
+ }
220
+ ```
221
+
222
+ Use `onEnterStartStateMachine()` when you want a child machine to start on state entry
223
+ rather than on an explicit action.
224
+
225
+ ### 7. Effects (side-effects without state transition)
226
+
227
+ ```kotlin
228
+ inState<ShowContent> {
229
+ onActionEffect<ToggleFavorite> { action, stateSnapshot ->
230
+ analytics.track("toggle_favorite", mapOf("id" to action.itemId))
231
+ // cannot mutate state from an effect block
232
+ }
233
+ }
234
+ ```
235
+
236
+ Use `*Effect` variants whenever you need to react to an action or event but must not
237
+ change state (navigation events, analytics, logging).
238
+
239
+ ---
240
+
241
+ ## Project structure checklist
242
+
243
+ ```
244
+ :shared
245
+ commonMain
246
+ └── feature/itemlist/
247
+ ├── model/
248
+ │ ├── ItemListState.kt ✓ sealed interface, data classes, immutable
249
+ │ └── ItemListAction.kt ✓ sealed interface
250
+ ├── statemachine/
251
+ │ ├── ItemListStateMachineFactory.kt ✓ extends FlowReduxStateMachineFactory
252
+ │ └── FavoriteStatusStateMachineFactory.kt ✓ child machine
253
+ ├── domain/
254
+ │ └── ItemRepository.kt ✓ interface only
255
+ └── data/
256
+ └── ItemRepositoryImpl.kt ✓ actual implementation (or expect/actual)
257
+
258
+ :composeApp
259
+ commonMain
260
+ └── feature/itemlist/
261
+ ├── ItemListScreen.kt ✓ @Composable, pure render + dispatch
262
+ └── ItemListViewModel.kt ✓ only if AndroidX lifecycle needed
263
+ ```
264
+
265
+ ---
266
+
267
+ ## Testing guidance
268
+
269
+ Prefer **functional integration tests** with Turbine over unit tests per handler.
270
+
271
+ ```kotlin
272
+ // Use runTest + Turbine for the full state machine
273
+ @Test
274
+ fun `loading transitions to ShowContent on success`() = runTest {
275
+ val factory = ItemListStateMachineFactory(FakeItemRepository(items = sampleItems))
276
+ val sm = factory.shareIn(backgroundScope)
277
+
278
+ sm.state.test {
279
+ assertEquals(Loading, awaitItem())
280
+ assertEquals(ShowContent(sampleItems), awaitItem())
281
+ }
282
+ }
283
+
284
+ // Override initial state to test mid-flow without replaying all transitions
285
+ @Test
286
+ fun `retry from Error transitions to Loading`() = runTest {
287
+ val factory = ItemListStateMachineFactory(FakeItemRepository())
288
+ factory.initializeWith { Error("oops", countdown = 3) }
289
+ val sm = factory.shareIn(backgroundScope)
290
+
291
+ sm.state.test {
292
+ assertEquals(Error("oops", 3), awaitItem())
293
+ sm.dispatch(RetryLoading)
294
+ assertEquals(Loading, awaitItem())
295
+ }
296
+ }
297
+ ```
298
+
299
+ For **unit testing handlers**, extract them to `private suspend fun` and test with
300
+ `ChangeableState` + `changedState.reduce(snapshot)` directly.
301
+
302
+ ---
303
+
304
+ ## Gradle dependencies
305
+
306
+ ```kotlin
307
+ // shared/build.gradle.kts
308
+ kotlin {
309
+ sourceSets {
310
+ commonMain.dependencies {
311
+ // FlowRedux core (KMP)
312
+ implementation("com.freeletics.flowredux:flowredux:2.0.1")
313
+ // Coroutines
314
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
315
+ }
316
+ }
317
+ }
318
+
319
+ // composeApp/build.gradle.kts
320
+ commonMain.dependencies {
321
+ // FlowRedux Compose extensions (produceStateMachine, dispatch)
322
+ implementation("com.freeletics.flowredux:compose:2.0.1")
323
+ }
324
+
325
+ // Testing
326
+ commonTest.dependencies {
327
+ implementation("app.cash.turbine:turbine:1.1.0")
328
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
329
+ }
330
+ ```
331
+
332
+ ---
333
+
334
+ ## Anti-patterns to avoid
335
+
336
+ | Anti-pattern | Fix |
337
+ |---|---|
338
+ | Business logic inside `@Composable` | Move to `inState { }` handlers |
339
+ | Mutable state inside `State` data class | All fields must be `val`; use `state.mutate { copy(...) }` |
340
+ | One giant state machine for the whole app | One factory per screen/feature |
341
+ | `state.override` called after a `return` | Override is the last expression in the lambda |
342
+ | Catching all `Throwable` silently | Log or encode the error into an `Error` state subtype |
343
+ | Skipping `ExecutionPolicy` on concurrent actions | Explicitly choose `Unordered` or `Ordered` where needed |
344
+ | Navigation logic inside state | Use `onActionEffect` + a navigation callback; state stays pure data |
@@ -0,0 +1,328 @@
1
+ ---
2
+ name: kmp-mvvm
3
+ description: >
4
+ KMP/CMP MVVM architecture with shared ViewModels. Use when building or reviewing
5
+ Kotlin Multiplatform features that follow the MVVM pattern: ViewModel + StateFlow,
6
+ UI state classes, state hoisting, collectAsStateWithLifecycle, Koin viewModel
7
+ injection, or bridging shared ViewModels to SwiftUI on iOS. Triggers on MVVM,
8
+ ViewModel, StateFlow, uiState, viewModelScope, KMP-ObservableViewModel, SKIE.
9
+ ---
10
+
11
+ # KMP MVVM with shared ViewModels — architecture skill
12
+
13
+ References:
14
+ - https://kotlinlang.org/docs/multiplatform/compose-viewmodel.html
15
+ - https://touchlab.co/kmp-view-models
16
+
17
+ ## Core concepts
18
+
19
+ MVVM separates concerns into three groups:
20
+
21
+ ```
22
+ observe (StateFlow) user events (fun calls)
23
+ View ◄───────────────────────── ViewModel ◄──────────────────── View
24
+ │ │
25
+ │ ▼
26
+ │ Model (UseCase / Repository)
27
+ └─ renders UiState │
28
+
29
+ data / network / cache
30
+ ```
31
+
32
+ - **Model** — domain + data layer (UseCases, Repositories) in `commonMain`
33
+ - **ViewModel** — holds and exposes immutable `UiState` as `StateFlow`, handles events
34
+ - **View** — Compose Multiplatform (shared) or native (SwiftUI/Compose), renders state only
35
+
36
+ MVVM vs MVI: MVVM exposes a single immutable `UiState` and *public methods* for events,
37
+ rather than dispatching `Action` objects through a reducer. Use MVVM when the team is
38
+ Android/Compose-native and the per-screen state is simple; reach for MVI/FlowRedux when
39
+ state transitions are complex enough to warrant an explicit state machine.
40
+
41
+ ---
42
+
43
+ ## Architecture layers
44
+
45
+ ```
46
+ :shared/commonMain
47
+ ├── feature/order/
48
+ │ ├── presentation/
49
+ │ │ ├── OrderUiState.kt ← single immutable data class (the "Model" the View sees)
50
+ │ │ └── OrderViewModel.kt ← extends ViewModel, exposes StateFlow<OrderUiState>
51
+ │ ├── domain/
52
+ │ │ ├── OrderRepository.kt ← interface
53
+ │ │ └── PlaceOrderUseCase.kt ← business logic, no Android deps
54
+ │ └── data/
55
+ │ └── OrderRepositoryImpl.kt ← actual impl (Ktor/SQLDelight)
56
+ └── di/
57
+ └── OrderModule.kt ← Koin module: viewModelOf(::OrderViewModel)
58
+
59
+ :composeApp/commonMain
60
+ └── feature/order/
61
+ └── OrderScreen.kt ← @Composable, koinViewModel() + collectAsStateWithLifecycle()
62
+
63
+ iosApp (Swift) ← observes ViewModel via KMP-ObservableViewModel
64
+ ```
65
+
66
+ **Rule:** ViewModel, UiState, and all logic live in `:shared/commonMain`. The View only
67
+ collects state and calls ViewModel methods.
68
+
69
+ ---
70
+
71
+ ## Choosing the ViewModel base class
72
+
73
+ There are three viable approaches. Pick one per project and stay consistent.
74
+
75
+ | Approach | commonMain dependency | iOS story | When to use |
76
+ |---|---|---|---|
77
+ | **AndroidX `lifecycle-viewmodel` (multiplatform)** | `androidx.lifecycle:lifecycle-viewmodel:2.8+` | Manual lifecycle on iOS; brings `viewModelScope` baggage | CMP-first apps where iOS UI is also Compose |
78
+ | **KMP-ObservableViewModel** (rickclephas) | `com.rickclephas.kmp:kmp-observableviewmodel-core` | First-class: SwiftUI observes directly, handles store-owner boilerplate | **Recommended** when iOS uses SwiftUI; this is the official Kotlin-docs recommendation |
79
+ | **Pure Kotlin ViewModel** (your own `interface`/base) | none | Explicit Swift observer wrapper you write | Maximum control, zero androidx in commonMain |
80
+
81
+ The official Kotlin documentation recommends **KMP-ObservableViewModel** for SwiftUI
82
+ because there is no built-in `ViewModelStoreOwner` on iOS — the library ties the
83
+ ViewModel lifecycle to the SwiftUI view automatically.
84
+
85
+ ---
86
+
87
+ ## Coding guidance
88
+
89
+ ### 1. Model UiState as a single immutable data class
90
+
91
+ ```kotlin
92
+ // shared/commonMain/.../presentation/OrderUiState.kt
93
+ data class OrderUiState(
94
+ val quantity: Int = 1,
95
+ val items: List<OrderItem> = emptyList(),
96
+ val isLoading: Boolean = false,
97
+ val errorMessage: String? = null,
98
+ )
99
+ ```
100
+
101
+ Rules:
102
+ - One `UiState` per screen — never expose multiple loose `StateFlow`s for one screen
103
+ - All fields `val`, with sensible defaults
104
+ - Represent loading/error as fields (`isLoading`, `errorMessage`), or use a sealed
105
+ `UiState` if states are mutually exclusive enough to warrant it
106
+ - Keep it free of framework types (no `Composable`, no `Color`, no `Painter`)
107
+
108
+ ### 2. ViewModel exposes StateFlow, never MutableStateFlow
109
+
110
+ ```kotlin
111
+ // shared/commonMain/.../presentation/OrderViewModel.kt
112
+ import com.rickclephas.kmp.observableviewmodel.ViewModel
113
+ import com.rickclephas.kmp.observableviewmodel.MutableStateFlow
114
+ import com.rickclephas.kmp.observableviewmodel.stateIn
115
+ import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesState
116
+
117
+ class OrderViewModel(
118
+ private val placeOrder: PlaceOrderUseCase,
119
+ private val repository: OrderRepository,
120
+ ) : ViewModel() {
121
+
122
+ private val _uiState = MutableStateFlow(viewModelScope, OrderUiState())
123
+
124
+ @NativeCoroutinesState // exposes the flow cleanly to Swift
125
+ val uiState: StateFlow<OrderUiState> = _uiState.asStateFlow()
126
+
127
+ init { loadItems() }
128
+
129
+ fun setQuantity(n: Int) {
130
+ _uiState.update { it.copy(quantity = n) }
131
+ }
132
+
133
+ fun submit() {
134
+ viewModelScope.coroutineScope.launch {
135
+ _uiState.update { it.copy(isLoading = true, errorMessage = null) }
136
+ runCatching { placeOrder(_uiState.value.quantity) }
137
+ .onSuccess { _uiState.update { s -> s.copy(isLoading = false) } }
138
+ .onFailure { t -> _uiState.update { s -> s.copy(isLoading = false, errorMessage = t.message) } }
139
+ }
140
+ }
141
+
142
+ private fun loadItems() { /* launch in viewModelScope, update _uiState */ }
143
+ }
144
+ ```
145
+
146
+ Rules:
147
+ - `_uiState` is private `MutableStateFlow`; expose read-only `StateFlow` via `asStateFlow()`
148
+ - Always update via `update { copy(...) }` — atomic, avoids race conditions
149
+ - Launch coroutines in `viewModelScope` so they cancel when the ViewModel clears
150
+ - Annotate the public flow with `@NativeCoroutinesState` for clean Swift interop
151
+ - Never expose `suspend` functions to the View; the View calls plain `fun`, the VM launches
152
+
153
+ ### 3. Compose Multiplatform View — state hoisting + lifecycle-aware collection
154
+
155
+ ```kotlin
156
+ // composeApp/commonMain/.../OrderScreen.kt
157
+ @Composable
158
+ fun OrderScreen(viewModel: OrderViewModel = koinViewModel()) {
159
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
160
+
161
+ OrderContent(
162
+ state = uiState,
163
+ onQuantityChange = viewModel::setQuantity,
164
+ onSubmit = viewModel::submit,
165
+ )
166
+ }
167
+
168
+ // Stateless, hoisted — fully previewable and testable
169
+ @Composable
170
+ private fun OrderContent(
171
+ state: OrderUiState,
172
+ onQuantityChange: (Int) -> Unit,
173
+ onSubmit: () -> Unit,
174
+ ) {
175
+ Column {
176
+ if (state.isLoading) LinearProgressIndicator()
177
+ QuantityStepper(state.quantity, onQuantityChange)
178
+ state.errorMessage?.let { Text(it, color = MaterialTheme.colorScheme.error) }
179
+ Button(onClick = onSubmit, enabled = !state.isLoading) { Text("Place order") }
180
+ }
181
+ }
182
+ ```
183
+
184
+ Rules:
185
+ - Split into a stateful screen (`koinViewModel()` + collect) and a stateless content
186
+ composable that takes `state` + lambdas — this is **state hoisting**
187
+ - Use `collectAsStateWithLifecycle()` (not bare `collectAsState()`) so collection pauses
188
+ when the UI is not visible
189
+ - Pass method references (`viewModel::setQuantity`), not the whole ViewModel, into content
190
+ - The stateless content composable has zero ViewModel dependency → trivially previewable
191
+
192
+ ### 4. Dependency injection with Koin
193
+
194
+ ```kotlin
195
+ // shared/commonMain/.../di/OrderModule.kt
196
+ val orderModule = module {
197
+ singleOf(::OrderRepositoryImpl) bind OrderRepository::class
198
+ factoryOf(::PlaceOrderUseCase)
199
+ viewModelOf(::OrderViewModel) // koin-compose-viewmodel
200
+ }
201
+ ```
202
+
203
+ Compose retrieves it with `koinViewModel()` (from `koin-compose-viewmodel`), which
204
+ scopes the ViewModel to the navigation entry / composition correctly on all platforms.
205
+
206
+ ### 5. iOS bridge (SwiftUI)
207
+
208
+ With KMP-ObservableViewModel + `@NativeCoroutinesState`, SwiftUI observes directly:
209
+
210
+ ```swift
211
+ struct OrderView: View {
212
+ @StateViewModel var viewModel = OrderViewModel(placeOrder: ..., repository: ...)
213
+
214
+ var body: some View {
215
+ VStack {
216
+ if viewModel.uiState.isLoading { ProgressView() }
217
+ Stepper("Qty: \(viewModel.uiState.quantity)",
218
+ value: Binding(get: { Int(viewModel.uiState.quantity) },
219
+ set: { viewModel.setQuantity(n: Int32($0)) }))
220
+ Button("Place order") { viewModel.submit() }
221
+ }
222
+ }
223
+ }
224
+ ```
225
+
226
+ If you instead use AndroidX ViewModel + SKIE, SKIE maps `StateFlow` to a Swift
227
+ `AsyncSequence`/`@Observable` and you bridge it with an observer wrapper.
228
+
229
+ ---
230
+
231
+ ## Project structure checklist
232
+
233
+ ```
234
+ :shared
235
+ commonMain
236
+ └── feature/order/
237
+ ├── presentation/
238
+ │ ├── OrderUiState.kt ✓ single immutable data class
239
+ │ └── OrderViewModel.kt ✓ private Mutable, public StateFlow
240
+ ├── domain/
241
+ │ ├── OrderRepository.kt ✓ interface
242
+ │ └── PlaceOrderUseCase.kt ✓ pure logic, no androidx
243
+ ├── data/
244
+ │ └── OrderRepositoryImpl.kt ✓ Ktor / SQLDelight
245
+ └── di/OrderModule.kt ✓ viewModelOf(::OrderViewModel)
246
+
247
+ :composeApp
248
+ commonMain
249
+ └── feature/order/OrderScreen.kt ✓ stateful screen + stateless content
250
+
251
+ iosApp ✓ @StateViewModel, observes uiState
252
+ ```
253
+
254
+ ---
255
+
256
+ ## Testing guidance
257
+
258
+ The ViewModel is a plain class — test it with `runTest` + Turbine, injecting fakes.
259
+
260
+ ```kotlin
261
+ @Test
262
+ fun `submit sets loading then clears on success`() = runTest {
263
+ val vm = OrderViewModel(
264
+ placeOrder = FakePlaceOrderUseCase(succeeds = true),
265
+ repository = FakeOrderRepository(),
266
+ )
267
+ vm.uiState.test {
268
+ assertEquals(OrderUiState(), awaitItem()) // initial
269
+ vm.setQuantity(3)
270
+ assertEquals(3, awaitItem().quantity)
271
+ vm.submit()
272
+ assertTrue(awaitItem().isLoading) // loading on
273
+ assertFalse(awaitItem().isLoading) // cleared after success
274
+ }
275
+ }
276
+ ```
277
+
278
+ The stateless content composable is tested separately with Compose UI tests / screenshot
279
+ tests — it takes a `UiState` directly, so no ViewModel is needed.
280
+
281
+ ---
282
+
283
+ ## Gradle dependencies
284
+
285
+ ```kotlin
286
+ // shared/build.gradle.kts
287
+ kotlin {
288
+ sourceSets {
289
+ commonMain.dependencies {
290
+ // Recommended iOS-friendly ViewModel (official Kotlin docs)
291
+ implementation("com.rickclephas.kmp:kmp-observableviewmodel-core:1.0.0-BETA-13")
292
+ // OR the AndroidX multiplatform ViewModel
293
+ // implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.8.4")
294
+
295
+ implementation("io.insert-koin:koin-core:4.0.0")
296
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
297
+ }
298
+ commonTest.dependencies {
299
+ implementation("app.cash.turbine:turbine:1.1.0")
300
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
301
+ }
302
+ }
303
+ }
304
+
305
+ // composeApp/build.gradle.kts
306
+ commonMain.dependencies {
307
+ implementation("io.insert-koin:koin-compose-viewmodel:4.0.0") // koinViewModel()
308
+ implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4")
309
+ }
310
+ ```
311
+
312
+ For SwiftUI interop, add the KMP-ObservableViewModel Swift package, or KMP-NativeCoroutines / SKIE.
313
+
314
+ ---
315
+
316
+ ## Anti-patterns to avoid
317
+
318
+ | Anti-pattern | Fix |
319
+ |---|---|
320
+ | Exposing `MutableStateFlow` publicly | Expose read-only `StateFlow` via `asStateFlow()` |
321
+ | Multiple loose `StateFlow`s for one screen | Combine into one `UiState` data class |
322
+ | Business logic inside the `@Composable` | Move to the ViewModel; the View only renders + calls methods |
323
+ | Passing the whole ViewModel into child composables | Hoist state: pass `UiState` + lambdas |
324
+ | `collectAsState()` instead of `collectAsStateWithLifecycle()` | Use the lifecycle-aware variant to pause off-screen collection |
325
+ | `suspend` functions exposed to the View | Keep them internal; the VM launches in `viewModelScope` |
326
+ | Framework types (`Color`, `Painter`) inside `UiState` | Keep `UiState` platform-agnostic data only |
327
+ | AndroidX ViewModel in commonMain when targeting SwiftUI | Prefer KMP-ObservableViewModel or a pure-Kotlin base |
328
+ | Mutating state with `.value = .value.copy()` under concurrency | Use `update { copy(...) }` for atomic updates |
@@ -49,7 +49,7 @@ You are the `global-migration-phase` node subagent. Your job is **target KMP pro
49
49
  - Integrate mode: no edits outside `kmp_target_project_path` or outside approved integration glue paths from TPA `integration_constraints`.
50
50
 
51
51
  **Mandatory**:
52
- - Integrate: validate `kmp_target_project_path`, package `M4`, all module representations, analyst cross-module globals, and `target_alignment_revision.json` before editing.
52
+ - Integrate: validate `kmp_target_project_path`, `design_mode` + `architecture_reference_path`, package `M4`, all module representations, analyst cross-module globals, and `target_alignment_revision.json` before editing. Cross-module glue, DI, and entry wiring MUST follow the run's `design_mode` (default `mvi`: state-machine wiring per `references/kmp-mvi-flowredux.md`; `mvvm`: `ViewModel`/Koin wiring per `references/kmp-mvvm.md`) and `references/kmp-expert.md` base KMP conventions — shared glue/nav/DI in `commonMain`, platform entry wrappers in `androidMain`/`iosMain`, `expect`/`actual` for platform launch hooks.
53
53
  - Integrate: `output_dir = <global_dir>/node-results/global-migration-phase/integrate`
54
54
  - Align: primary output under `<global_dir>/node-results/global-migration-phase/align`; alignment report under `report_dir`
55
55
  - Include `mode` and `kmp_target_project_path` in JSON return payload.
@@ -190,6 +190,11 @@ Read-only verification after integrate. For each Legacy Android entry in analyst
190
190
  ```text
191
191
  ROLE: global-migration-phase node in android-to-kmp-migrator. Modes: integrate | align. NEVER combine.
192
192
 
193
+ DESIGN MODE: glue/DI/entry wiring follows design_mode (default mvi → references/kmp-mvi-flowredux.md
194
+ state-machine wiring; mvvm → references/kmp-mvvm.md ViewModel/Koin wiring) and references/kmp-expert.md
195
+ base KMP conventions (shared glue/nav/DI in commonMain, platform entry wrappers + expect/actual launch
196
+ hooks in androidMain/iosMain). Do NOT mix patterns.
197
+
193
198
  INTEGRATE — EDIT THE TARGET KMP PROJECT:
194
199
  - Wire cross-module UI transitions, control logic handoffs, and data calls inside kmp_target_project_path.
195
200
  - Wire entry points: Android launcher/Application/root nav/deep links → KMP app shell (entry_point_wiring[]).
@@ -208,7 +213,7 @@ CONTROL:
208
213
  analyst cross-module globals, target_alignment_revision before editing.
209
214
  - Align: consume global_system_integration output; inspect target files without modifying them.
210
215
 
211
- INPUTS: mode, kmp_target_project_path, analyst cross_module_architecture_path,
216
+ INPUTS: mode, design_mode, architecture_reference_path, kmp_target_project_path, analyst cross_module_architecture_path,
212
217
  cross_module_data_logic_path, module_migration_representation paths,
213
218
  presentation_resource entry_points paths (per module + launcher module),
214
219
  target_alignment_revision_path (entry_point_anchors[]), global_system_integration path (align mode),
@@ -9,7 +9,7 @@ You are the `migration-planning-gate` node subagent. You merge **migration analy
9
9
  ## Success Criteria
10
10
 
11
11
  - `migration_planning_gate.json` and `migration_planning_gate.md` written under `output_dir`.
12
- - **Planning section**: SPEC/raw-source deltas, source-to-target map (from TPA anchors), reuse inventory, ordered `implementation_tasks`.
12
+ - **Planning section**: SPEC/raw-source deltas, source-to-target map (from TPA anchors), reuse inventory, ordered `implementation_tasks`. The source-to-target map and tasks MUST follow the run's `design_mode` (default `mvi`) layout from `architecture_reference_path` — `mvi` (`references/kmp-mvi-flowredux.md`): `model/` (sealed `State`/`Action`), `statemachine/` (`FlowReduxStateMachineFactory`), `domain/`; `mvvm` (`references/kmp-mvvm.md`): `presentation/` (`ViewModel` + `UiState`), `domain/`, `data/`. Both modes target a KMP project per `references/kmp-expert.md` base conventions — map source to KMP source sets (`commonMain` first, `androidMain`/`iosMain` only for platform actuals) and the 2026 `shared` + `*App` module layout.
13
13
  - **Dependency/platform section**: capability map, minimal-change dependency decisions, platform boundaries, `ready_for_implementation` or `blocked`.
14
14
  - No feature UI/logic implementation; build-config changes only when gate justifies them.
15
15
 
@@ -64,9 +64,13 @@ See [output-contract.md](../output-contract.md). Artifact basename: `migration_p
64
64
  ROLE: migration-planning-gate node. Merge planning + dependency/platform gate in ONE invocation.
65
65
 
66
66
  PLANNING: SPEC deltas, source-to-target map from TPA anchors, ordered tasks. No target re-survey.
67
+ Layout follows design_mode (default mvi): mvi → model/statemachine/domain (references/kmp-mvi-flowredux.md);
68
+ mvvm → presentation(ViewModel+UiState)/domain/data (references/kmp-mvvm.md).
69
+ Both target a KMP project per references/kmp-expert.md base conventions: prefer commonMain, drop to
70
+ androidMain/iosMain only for platform actuals, follow the shared + *App module layout.
67
71
  GATE: capability map, minimal-change deps, platform boundaries. ready_for_implementation or blocked.
68
72
 
69
- INPUTS: migration_module_id, module_scope, module_brief_path, target_module_anchors_path,
73
+ INPUTS: design_mode, architecture_reference_path, migration_module_id, module_scope, module_brief_path, target_module_anchors_path,
70
74
  target_alignment_revision_path, upstream module_representation, SPEC paths, target path,
71
75
  allowed_files, allowed_source_sets, output_dir.
72
76
 
@@ -10,7 +10,7 @@ You are the `migration-prep` node subagent. You merge **presentation integration
10
10
 
11
11
  - `migration_prep.json` and `migration_prep.md` written under `output_dir`.
12
12
  - **Presentation section**: token mappings, resource mapping, route mapping, UI handoff, presentation gaps.
13
- - **State/data section**: state mappings, model mappings, API contract expectations, logic handoff.
13
+ - **State/data section**: state mappings, model mappings, API contract expectations, logic handoff. State holder expectations MUST follow the run's `design_mode` (default `mvi`): `mvi` → sealed `State`/`Action` + state-machine handoff (`references/kmp-mvi-flowredux.md`); `mvvm` → immutable `UiState` + `ViewModel` event-method handoff (`references/kmp-mvvm.md`). All scaffold and contracts target a KMP project per `references/kmp-expert.md` base conventions — place shared tokens/resources/models/routes in `commonMain`, reserve `androidMain`/`iosMain` for platform actuals, and prefer the multiplatform stack (Ktor, kotlinx-serialization, kotlinx-datetime) over Android-only types.
14
14
  - Changed files recorded; cross-module impacts noted.
15
15
  - No full UI layouts or repository/API behavior.
16
16
 
@@ -71,8 +71,12 @@ ROLE: migration-prep node. Merge presentation + state/data prep in ONE invocatio
71
71
 
72
72
  PRESENTATION: tokens, resources, media, routes, UI handoff.
73
73
  STATE/DATA: state holders, models, mappers, API expectations, logic handoff.
74
+ State holder shape follows design_mode (default mvi): mvi → sealed State/Action + state machine
75
+ (references/kmp-mvi-flowredux.md); mvvm → UiState + ViewModel methods (references/kmp-mvvm.md).
76
+ Target is a KMP project per references/kmp-expert.md: scaffold in commonMain, platform actuals only in
77
+ androidMain/iosMain, prefer the multiplatform library stack over Android-only types.
74
78
 
75
- INPUTS: migration_module_id, migration_planning_gate_path, analyst dimension paths,
79
+ INPUTS: design_mode, architecture_reference_path, migration_module_id, migration_planning_gate_path, analyst dimension paths,
76
80
  target path, allowed_files, output_dir.
77
81
 
78
82
  OUTPUTS: migration_prep.json, migration_prep.md
@@ -25,6 +25,17 @@ You merge **UI implementation** and **logic implementation** with strict modes.
25
25
 
26
26
  **Gate**: `logic` mode MUST NOT run until latest UI review is `approved`.
27
27
 
28
+ ## Design Mode (architecture pattern)
29
+
30
+ The run's `design_mode` (default `mvi`) is supplied by the Leader together with `architecture_reference_path`. You MUST implement to that pattern; do not mix patterns.
31
+
32
+ | `design_mode` | Reference | Shape you produce |
33
+ |---|---|---|
34
+ | `mvi` **(default)** | `references/kmp-mvi-flowredux.md` | Sealed `State`/`Action`, `FlowReduxStateMachineFactory`, `dispatch()` from UI, unidirectional flow |
35
+ | `mvvm` | `references/kmp-mvvm.md` | `ViewModel` exposing immutable `UiState` as `StateFlow`, public event methods, `collectAsStateWithLifecycle()` |
36
+
37
+ Both modes follow `references/kmp-expert.md` for base KMP/CMP conventions. If `design_mode` or `architecture_reference_path` is missing from the dispatch, return `blocked` — do not guess the pattern.
38
+
28
39
  ## Success Criteria
29
40
 
30
41
  **UI mode**:
@@ -54,7 +65,7 @@ You merge **UI implementation** and **logic implementation** with strict modes.
54
65
  - Do not run full project compile/build — static edits only; build is `kmp-test-validator`.
55
66
 
56
67
  **Mandatory**:
57
- - Validate `kmp_target_project_path`, planning-gate `ready_for_implementation`, prep outputs, `target_module_anchors.json`, `allowed_files`, source sets, and workspace state before editing.
68
+ - Validate `kmp_target_project_path`, `design_mode` + `architecture_reference_path`, planning-gate `ready_for_implementation`, prep outputs, `target_module_anchors.json`, `allowed_files`, source sets, and workspace state before editing.
58
69
  - Map each implementation task to a target path from planning/TPA before writing code.
59
70
  - Include `mode`, `kmp_target_project_path`, and `changed_files` in JSON return payload.
60
71
  - Write migration evidence artifacts under `output_dir`; write **implementation code** under `kmp_target_project_path`.
@@ -70,6 +81,8 @@ You merge **UI implementation** and **logic implementation** with strict modes.
70
81
  "legacy_module_id": "",
71
82
  "module_scope": {},
72
83
  "kmp_target_project_path": "",
84
+ "design_mode": "mvi | mvvm",
85
+ "architecture_reference_path": "",
73
86
  "output_root": "",
74
87
  "output_dir": "",
75
88
  "target_edit_summary": {
@@ -127,6 +140,11 @@ ROLE: module-implementation node in android-to-kmp-migrator. Modes: ui | logic.
127
140
  YOU IMPLEMENT IN THE TARGET KMP PROJECT. Edit/create KMP files under kmp_target_project_path.
128
141
  Legacy Android and analyst artifacts are read-only evidence. Do NOT edit Legacy Android.
129
142
 
143
+ DESIGN MODE: follow design_mode (default mvi). mvi → references/kmp-mvi-flowredux.md
144
+ (sealed State/Action + FlowReduxStateMachineFactory + dispatch); mvvm → references/kmp-mvvm.md
145
+ (ViewModel + immutable UiState StateFlow + public event methods). Both follow references/kmp-expert.md.
146
+ Do NOT mix patterns. If design_mode/architecture_reference_path missing, return blocked.
147
+
130
148
  UI MODE:
131
149
  - Port visible UI from upstream presentation evidence into target Compose/resources/navigation.
132
150
  - changed_files = every target file you created or modified.
@@ -143,7 +161,7 @@ CONTROL:
143
161
  - Map each task to a target path from planning source_to_target_map / TPA anchors first.
144
162
  - If anchor or allowed_files is missing, return blocked — do not guess target paths.
145
163
 
146
- INPUTS: mode, migration_module_id, legacy_module_id, kmp_target_project_path,
164
+ INPUTS: mode, design_mode, architecture_reference_path, migration_module_id, legacy_module_id, kmp_target_project_path,
147
165
  migration_planning_gate_path, migration_prep_path, target_module_anchors_path,
148
166
  upstream module_representation + presentation_resource/behavior_logic paths (read-only),
149
167
  prior module_implementation_ui output (logic mode), allowed_files, output_dir.
@@ -11,6 +11,17 @@ You are the `module-node-review-fix` node subagent. You consolidate review and f
11
11
  - `mode: review`: read-only review of one module/node slice.
12
12
  - `mode: fix`: scoped edit of explicit `must_fix` findings from one review report.
13
13
 
14
+ ## Design Mode (architecture pattern)
15
+
16
+ The run's `design_mode` (default `mvi`) and `architecture_reference_path` are supplied by the Leader. Both modes MUST judge/repair code against that pattern:
17
+
18
+ - `mvi` → `references/kmp-mvi-flowredux.md` (sealed `State`/`Action`, `FlowReduxStateMachineFactory`, `dispatch`).
19
+ - `mvvm` → `references/kmp-mvvm.md` (`ViewModel` + immutable `UiState` `StateFlow` + public event methods).
20
+
21
+ Both modes also judge code against `references/kmp-expert.md` base KMP/CMP conventions — correct source-set placement (`commonMain` vs `androidMain`/`iosMain`), `expect`/`actual` usage, no Android-only APIs leaked into `commonMain`, and multiplatform-stack choices.
22
+
23
+ Flag architecture drift (wrong pattern vs `design_mode`) or base-KMP violations as `must_fix` findings in review; in fix mode, conform to the references. Do not introduce the other pattern.
24
+
14
25
  ## Success Criteria
15
26
 
16
27
  - Review mode writes `module_node_review.json` and `module_node_review.md`.
@@ -68,10 +79,15 @@ Shared return shape applies.
68
79
  ROLE: module-node-review-fix node.
69
80
 
70
81
  Respect mode strictly.
71
- Review mode: read-only; verify one owning node slice for contract, scope, parity, source-set, target convention, dependency discipline, and handoff readiness.
82
+ Review mode: read-only; verify one owning node slice for contract, scope, parity, source-set, target convention, design_mode architecture conformance, dependency discipline, and handoff readiness.
72
83
  Fix mode: consume one review report; fix only assigned must_fix findings inside allowed_files; set requires_re_review=true.
73
84
 
74
- INPUTS: mode, migration_module_id, module_scope, owning_node, owning_node_output_path, changed_files, review_report_path for fix mode, allowed_files, workspace state, output_dir.
85
+ DESIGN MODE: judge/repair against design_mode (default mvi). mvi references/kmp-mvi-flowredux.md;
86
+ mvvm → references/kmp-mvvm.md. Also enforce references/kmp-expert.md base KMP conventions (source-set
87
+ placement, expect/actual, no android.* in commonMain). Wrong-pattern or base-KMP violations are must_fix.
88
+ Never introduce the other pattern.
89
+
90
+ INPUTS: mode, design_mode, architecture_reference_path, migration_module_id, module_scope, owning_node, owning_node_output_path, changed_files, review_report_path for fix mode, allowed_files, workspace state, output_dir.
75
91
 
76
92
  OUTPUTS:
77
93
  - review mode: module_node_review.json/md (read-only reviewed files, findings, approval/needs-fix decision, blockers)
@@ -20,6 +20,8 @@ You are the `target-project-assistant` node subagent. You understand the existin
20
20
  - `target_alignment_revision.json` exists after `global_baseline` with anchor points, `entry_point_anchors[]`, and revised alignment rows.
21
21
  - Per-module `target_module_anchors.json` maps legacy evidence to resolvable target paths.
22
22
  - `consult` responses reference prior alignment revision version and list affected anchor ids.
23
+ - `target_project_layout` notes the target's existing presentation pattern and whether it matches the run's `design_mode` (default `mvi`); a mismatch is surfaced in `integration_constraints[]` for the Leader, not silently resolved.
24
+ - `target_project_layout` is read against `references/kmp-expert.md` base KMP conventions — record the target's source-set hierarchy (`commonMain` / `androidMain` / `iosMain`), `shared` + `*App` module shape, and multiplatform stack so anchors resolve to the correct KMP source set; flag deviations from the base layout in `integration_constraints[]`.
23
25
  - No target code edits (read-only analysis).
24
26
 
25
27
  ## Boundary
@@ -106,7 +108,14 @@ You own target KMP understanding and alignment revision. Modes:
106
108
 
107
109
  You do NOT edit target code or run full builds. Other roles MUST use your artifacts.
108
110
 
109
- INPUTS: kmp_target_project_path, analyst_output_root, upstream_analyst_index_path, modules_index_path,
111
+ DESIGN MODE: detect the target's existing presentation pattern and compare to design_mode (default mvi:
112
+ references/kmp-mvi-flowredux.md; mvvm: references/kmp-mvvm.md). Record the match/mismatch in
113
+ integration_constraints[] — do not resolve it yourself.
114
+ KMP BASE: read the target against references/kmp-expert.md — capture source-set hierarchy
115
+ (commonMain/androidMain/iosMain), shared + *App module shape, and stack so anchors resolve to the right
116
+ source set; flag base-layout deviations in integration_constraints[].
117
+
118
+ INPUTS: design_mode, architecture_reference_path, kmp_target_project_path, analyst_output_root, upstream_analyst_index_path, modules_index_path,
110
119
  migration_assembly_basis_path, cross_module_architecture_path, cross_module_data_logic_path,
111
120
  legacy module_representation paths, migration_module_id, mode, output_dir.
112
121
 
@@ -59,10 +59,19 @@ graph TD
59
59
  ## Step 0 — Pre-flight
60
60
 
61
61
  - **Executor**: Leader
62
- - **Input**: [dependencies.yaml](dependencies.yaml) — `tools[]` (`rg`, `git`, `curl`), `optional_mcp.jetbrains`, `upstream_inputs` analyst **P6**
63
- - **Output**: `run_manifest.json` → `dependency_preflight` (CLI status, MCP availability, P6 readiness pointer)
62
+ - **Input**: [dependencies.yaml](dependencies.yaml) — `tools[]` (`rg`, `git`, `curl`), `optional_mcp.jetbrains`, `upstream_inputs` analyst **P6**; the **user input / migration request**
63
+ - **Output**: `run_manifest.json` → `dependency_preflight` (CLI status, MCP availability, P6 readiness pointer) and `design_mode` (architecture pattern decision)
64
64
  - **Gate**: missing CLI tools → degraded modes per dependencies.yaml; `android-project-analyst` **P6** not ready → **blocked** — invoke analyst first, do not dispatch migrator nodes
65
65
 
66
+ ### Step 0a — Identify design mode (default MVI)
67
+
68
+ - Scan the **user input** for an explicit or implied presentation architecture:
69
+ - **`mvvm`** signals: "MVVM", shared `ViewModel`, `StateFlow` / `uiState`, `viewModelScope`, `collectAsStateWithLifecycle`, KMP-ObservableViewModel, SKIE → `references/kmp-mvvm.md`
70
+ - **`mvi`** signals: "MVI", FlowRedux, state machine, reducer, intent, unidirectional, sealed `State`/`Action`, `dispatch`, `inState`, `onEnter` → `references/kmp-mvi-flowredux.md`
71
+ - **No clear signal → default `mvi`.**
72
+ - Record `design_mode: { value: "mvi | mvvm", source: "user_input | default", signals: [], architecture_reference_path: "" }` in `run_manifest.json`.
73
+ - Freeze `design_mode` for the run; pass `design_mode` + `architecture_reference_path` into every architecture-producing dispatch (planning-gate, prep, module-implementation, module-node-review-fix, global-migration-phase) and to TPA for target-pattern detection.
74
+
66
75
  ## Step 1 — Upstream + output root
67
76
 
68
77
  - Verify analyst package **P6**; write `upstream_analyst_index.json`.
@@ -125,6 +134,7 @@ Any target question → TPA `mode: consult` (append `consultation_log`).
125
134
  ## Acceptance Criteria
126
135
 
127
136
  - `android-project-analyst` **P6** verified before any migrator module dispatch.
137
+ - `design_mode` identified from user input at Step 0 (default `mvi`) and recorded in `run_manifest.json`; architecture-producing dispatches carry `design_mode` + `architecture_reference_path`.
128
138
  - Target KMP files created or updated under `kmp_target_project_path` for every module requiring implementation; `target_changed_files[]` aggregated in `migration_report.json`.
129
139
  - `kmp-test-validator` invoked after **V0** — mandatory MG17 step.
130
140
  - Dispatch only role IDs from `SKILL.md`.