engsys 1.0.0

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.
Files changed (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -0
  3. package/core/agents/aaron.md +152 -0
  4. package/core/agents/bert.md +115 -0
  5. package/core/agents/isabelle.md +136 -0
  6. package/core/agents/jody.md +150 -0
  7. package/core/agents/leith.md +111 -0
  8. package/core/agents/marcelo.md +282 -0
  9. package/core/agents/melvin.md +101 -0
  10. package/core/agents/nyx.md +152 -0
  11. package/core/agents/otto.md +168 -0
  12. package/core/agents/patricia.md +283 -0
  13. package/core/commands/design-audit-local.md +155 -0
  14. package/core/commands/design-audit.md +235 -0
  15. package/core/commands/design-critique.md +96 -0
  16. package/core/commands/file-issue.md +22 -0
  17. package/core/commands/generate-project.md +45 -0
  18. package/core/commands/implement-issue.md +37 -0
  19. package/core/commands/implement-project.md +40 -0
  20. package/core/commands/naturalize.md +61 -0
  21. package/core/commands/pre-push.md +29 -0
  22. package/core/commands/prep-review-collect.md +130 -0
  23. package/core/commands/prep-review-finalize.md +121 -0
  24. package/core/commands/prep-review-publish.md +113 -0
  25. package/core/commands/prep-review.md +65 -0
  26. package/core/commands/project-closeout.md +25 -0
  27. package/core/skills/agentic-eval/SKILL.md +195 -0
  28. package/core/skills/chrome-devtools/SKILL.md +97 -0
  29. package/core/skills/code-review/SKILL.md +26 -0
  30. package/core/skills/gh-cli/SKILL.md +2202 -0
  31. package/core/skills/git-commit/SKILL.md +124 -0
  32. package/core/skills/git-workflow-agents/SKILL.md +462 -0
  33. package/core/skills/git-workflow-agents/reference.md +220 -0
  34. package/core/skills/github-actions/SKILL.md +190 -0
  35. package/core/skills/github-issues/SKILL.md +154 -0
  36. package/core/skills/llm-structured-outputs/SKILL.md +323 -0
  37. package/core/skills/llm-structured-outputs/references/provider-details.md +392 -0
  38. package/core/skills/pre-push/SKILL.md +115 -0
  39. package/core/skills/refactor/SKILL.md +645 -0
  40. package/core/skills/web-design-reviewer/SKILL.md +371 -0
  41. package/core/skills/webapp-testing/SKILL.md +127 -0
  42. package/core/skills/webapp-testing/test-helper.js +56 -0
  43. package/core/templates/CLAUDE.md.tmpl +98 -0
  44. package/core/templates/adr-template.md +67 -0
  45. package/core/templates/gh-issue-templates/bug.md +39 -0
  46. package/core/templates/gh-issue-templates/content.md +42 -0
  47. package/core/templates/gh-issue-templates/enhancement.md +36 -0
  48. package/core/templates/gh-issue-templates/feature.md +39 -0
  49. package/core/templates/gh-issue-templates/infrastructure.md +41 -0
  50. package/core/templates/post-edit-reminders.sh.tmpl +19 -0
  51. package/core/templates/settings.json.tmpl +90 -0
  52. package/core/templates/settings.local.json.tmpl +3 -0
  53. package/core/workflows/agent-implementation-workflow.md +346 -0
  54. package/core/workflows/generate-project.md +258 -0
  55. package/core/workflows/implement-project-workflow.md +190 -0
  56. package/core/workflows/issue-tracking.md +89 -0
  57. package/core/workflows/project-closeout-ceremony.md +77 -0
  58. package/core/workflows/review-workflow.md +266 -0
  59. package/engsys.config.example.yaml +46 -0
  60. package/install +202 -0
  61. package/lessons-library/README.md +80 -0
  62. package/lessons-library/async-callbacks-verify-liveness.md +15 -0
  63. package/lessons-library/change-isnt-done-until-every-surface-updated.md +15 -0
  64. package/lessons-library/claim-then-act-for-irreversible-ops.md +16 -0
  65. package/lessons-library/co-commit-entangled-work.md +15 -0
  66. package/lessons-library/dependabot-triage-playbook.md +17 -0
  67. package/lessons-library/deploy-by-digest-and-verify-the-running-revision.md +15 -0
  68. package/lessons-library/enforce-your-guarantee-at-your-boundary.md +16 -0
  69. package/lessons-library/gate-changes-on-measurement-not-vibes.md +15 -0
  70. package/lessons-library/iac-first-no-console-changes.md +15 -0
  71. package/lessons-library/independent-objective-review-gate.md +15 -0
  72. package/lessons-library/keep-an-immutable-source-of-truth.md +15 -0
  73. package/lessons-library/long-agent-runs-checkpoint-not-poll.md +15 -0
  74. package/lessons-library/model-identity-with-stable-ids-and-provenance.md +15 -0
  75. package/lessons-library/operator-choices-are-first-class.md +15 -0
  76. package/lessons-library/prefer-tool-enforced-structured-output.md +15 -0
  77. package/lessons-library/prove-causation-before-acting.md +15 -0
  78. package/lessons-library/re-read-state-before-acting.md +14 -0
  79. package/lessons-library/read-layer-tolerates-unbackfilled-rows.md +15 -0
  80. package/lessons-library/shell-safety-pipefail-and-validate-before-teardown.md +14 -0
  81. package/lessons-library/shift-correctness-left-and-distrust-false-greens.md +15 -0
  82. package/lessons-library/stray-control-bytes-hide-changes.md +14 -0
  83. package/lessons-library/tests-can-assert-the-bug.md +15 -0
  84. package/lessons-library/verify-ground-truth-not-reports.md +15 -0
  85. package/lessons-library/worktrees-need-bootstrap-from-origin-main.md +15 -0
  86. package/lib/commands.js +356 -0
  87. package/lib/generate-team-avatars.mjs +251 -0
  88. package/lib/manifest.js +155 -0
  89. package/lib/render.js +135 -0
  90. package/lib/selftest.js +90 -0
  91. package/lib/util.js +89 -0
  92. package/lib/yaml.js +156 -0
  93. package/optional-agents/gary.md +86 -0
  94. package/optional-agents/jos.md +136 -0
  95. package/optional-agents/sandy.md +101 -0
  96. package/optional-agents/steve.md +161 -0
  97. package/package.json +43 -0
  98. package/stacks/cloud/aws/claude.fragment.md +17 -0
  99. package/stacks/cloud/aws/settings.fragment.json +39 -0
  100. package/stacks/cloud/aws/skills/aws-deployment-preflight/SKILL.md +165 -0
  101. package/stacks/cloud/aws/skills/cloud-architecture-aws/SKILL.md +265 -0
  102. package/stacks/cloud/azure/claude.fragment.md +17 -0
  103. package/stacks/cloud/azure/settings.fragment.json +45 -0
  104. package/stacks/cloud/azure/skills/azure-deployment-preflight/SKILL.md +175 -0
  105. package/stacks/cloud/azure/skills/cloud-architecture-azure/SKILL.md +211 -0
  106. package/stacks/cloud/cloudflare/claude.fragment.md +21 -0
  107. package/stacks/cloud/cloudflare/settings.fragment.json +31 -0
  108. package/stacks/cloud/cloudflare/skills/cloud-architecture-cloudflare/SKILL.md +294 -0
  109. package/stacks/cloud/cloudflare/skills/cloudflare-deployment-preflight/SKILL.md +175 -0
  110. package/stacks/cloud/gcp/claude.fragment.md +17 -0
  111. package/stacks/cloud/gcp/settings.fragment.json +40 -0
  112. package/stacks/cloud/gcp/skills/cloud-architecture-gcp/SKILL.md +208 -0
  113. package/stacks/cloud/gcp/skills/gcp-deployment-preflight/SKILL.md +137 -0
  114. package/stacks/db/mongo/skills/mongo-conventions/SKILL.md +96 -0
  115. package/stacks/db/prisma/claude.fragment.md +49 -0
  116. package/stacks/db/prisma/skills/docker-database-package-copy/SKILL.md +44 -0
  117. package/stacks/db/prisma/skills/prisma-conventions/SKILL.md +37 -0
  118. package/stacks/domain/mobile-growth/skills/apple-ads/SKILL.md +184 -0
  119. package/stacks/domain/mobile-growth/skills/apple-ads/references/benchmark-notes.md +47 -0
  120. package/stacks/domain/mobile-growth/skills/apple-ads/references/official-links.md +53 -0
  121. package/stacks/domain/mobile-growth/skills/google-play-growth/SKILL.md +197 -0
  122. package/stacks/domain/mobile-growth/skills/google-play-growth/references/benchmark-notes.md +47 -0
  123. package/stacks/domain/mobile-growth/skills/google-play-growth/references/official-links.md +45 -0
  124. package/stacks/iac/bicep/claude.fragment.md +14 -0
  125. package/stacks/iac/bicep/settings.fragment.json +20 -0
  126. package/stacks/iac/bicep/skills/iac-bicep/SKILL.md +113 -0
  127. package/stacks/iac/cdk/claude.fragment.md +14 -0
  128. package/stacks/iac/cdk/settings.fragment.json +23 -0
  129. package/stacks/iac/cdk/skills/iac-cdk/SKILL.md +104 -0
  130. package/stacks/iac/terraform/claude.fragment.md +13 -0
  131. package/stacks/iac/terraform/settings.fragment.json +25 -0
  132. package/stacks/iac/terraform/skills/iac-terraform/SKILL.md +93 -0
  133. package/stacks/iac/terraform/skills/terraform-conventions/SKILL.md +87 -0
  134. package/stacks/lang/kotlin/skills/android-testing/SKILL.md +263 -0
  135. package/stacks/lang/kotlin/skills/jetpack-compose/SKILL.md +264 -0
  136. package/stacks/lang/kotlin/skills/kotlin-coroutines/SKILL.md +329 -0
  137. package/stacks/lang/python/skills/python-conventions/SKILL.md +61 -0
  138. package/stacks/lang/shell/skills/shell-scripting/SKILL.md +110 -0
  139. package/stacks/lang/swift/skills/swift-concurrency/SKILL.md +423 -0
  140. package/stacks/lang/swift/skills/swift-concurrency/references/approachable-concurrency.md +80 -0
  141. package/stacks/lang/swift/skills/swift-concurrency/references/concurrency-patterns.md +233 -0
  142. package/stacks/lang/swift/skills/swift-concurrency/references/swiftui-concurrency.md +187 -0
  143. package/stacks/lang/swift/skills/swift-concurrency/references/synchronization-primitives.md +341 -0
  144. package/stacks/lang/swift/skills/swift-testing/SKILL.md +497 -0
  145. package/stacks/lang/swift/skills/swift-testing/references/testing-advanced.md +106 -0
  146. package/stacks/lang/swift/skills/swift-testing/references/testing-patterns.md +504 -0
  147. package/stacks/lang/swift/skills/swiftdata/SKILL.md +334 -0
  148. package/stacks/lang/swift/skills/swiftdata/references/core-data-coexistence.md +504 -0
  149. package/stacks/lang/swift/skills/swiftdata/references/swiftdata-advanced.md +975 -0
  150. package/stacks/lang/swift/skills/swiftdata/references/swiftdata-queries.md +675 -0
  151. package/stacks/lang/swift/skills/swiftui-patterns/SKILL.md +371 -0
  152. package/stacks/lang/swift/skills/swiftui-patterns/references/architecture-patterns.md +486 -0
  153. package/stacks/lang/swift/skills/swiftui-patterns/references/deprecated-migration.md +1097 -0
  154. package/stacks/lang/swift/skills/swiftui-patterns/references/design-polish.md +780 -0
  155. package/stacks/lang/swift/skills/swiftui-patterns/references/platform-and-sharing.md +696 -0
  156. package/stacks/lang/typescript/skills/typescript-conventions/SKILL.md +91 -0
  157. package/stacks/platform/android/claude.fragment.md +40 -0
  158. package/stacks/platform/android/hooks/pre-push-gradle.sh +70 -0
  159. package/stacks/platform/android/settings.fragment.json +13 -0
  160. package/stacks/platform/android/skills/android-build-conventions/SKILL.md +247 -0
  161. package/stacks/platform/ios/claude.fragment.md +24 -0
  162. package/stacks/platform/ios/hooks/pre-push-xcodebuild.sh +82 -0
  163. package/stacks/platform/ios/settings.fragment.json +21 -0
  164. package/stacks/platform/ios/skills/xcodebuildmcp-simulator-logs/SKILL.md +76 -0
  165. package/stacks/platform/web/skills/frontend-testing/SKILL.md +246 -0
  166. package/stacks/platform/web/skills/react-conventions/SKILL.md +261 -0
  167. package/stacks/platform/web/skills/web-platform-conventions/SKILL.md +55 -0
  168. package/stacks/tooling/issue-tracker-github/claude.fragment.md +10 -0
  169. package/stacks/tooling/issue-tracker-github/settings.fragment.json +24 -0
  170. package/stacks/tooling/issue-tracker-github/skills/issue-tracker-github/SKILL.md +278 -0
  171. package/stacks/tooling/issue-tracker-linear/claude.fragment.md +17 -0
  172. package/stacks/tooling/issue-tracker-linear/settings.fragment.json +9 -0
  173. package/stacks/tooling/issue-tracker-linear/skills/issue-tracker-linear/SKILL.md +183 -0
@@ -0,0 +1,264 @@
1
+ ---
2
+ name: jetpack-compose
3
+ description: "Build Android UIs with Jetpack Compose: composition, state hoisting, remember/rememberSaveable/derivedStateOf, recomposition correctness and performance, side-effect APIs (LaunchedEffect/DisposableEffect/rememberCoroutineScope/produceState/snapshotFlow), Material 3 theming, and Compose navigation. Use when writing or reviewing @Composable functions, fixing recomposition or state bugs, hoisting state, choosing a side-effect API, applying Material 3, or improving Compose performance and stability."
4
+ ---
5
+
6
+ # Jetpack Compose
7
+
8
+ Build and review Android UIs with Jetpack Compose targeting the current Compose
9
+ BOM (2025.x) and Material 3. Composables are pure descriptions of UI for a given
10
+ state; the runtime re-invokes them (recomposes) when the state they read
11
+ changes. Treat composition as a function of state, keep composables side-effect
12
+ free except through the official effect APIs, and hoist state so UI is testable
13
+ and reusable. This is Android's analogue of declarative SwiftUI.
14
+
15
+ ## Contents
16
+
17
+ - [Composition Model](#composition-model)
18
+ - [State and remember](#state-and-remember)
19
+ - [State Hoisting](#state-hoisting)
20
+ - [derivedStateOf](#derivedstateof)
21
+ - [Recomposition Pitfalls](#recomposition-pitfalls)
22
+ - [Side-Effect APIs](#side-effect-apis)
23
+ - [Collecting Flows](#collecting-flows)
24
+ - [Stability and Performance](#stability-and-performance)
25
+ - [Material 3](#material-3)
26
+ - [ViewModel Integration](#viewmodel-integration)
27
+ - [Common Mistakes](#common-mistakes)
28
+ - [Review Checklist](#review-checklist)
29
+
30
+ ## Composition Model
31
+
32
+ - A `@Composable` function emits UI. It may run often, in any order, in parallel,
33
+ and be skipped. **It must be idempotent and free of side effects.**
34
+ - Never mutate external state, perform IO, or launch work directly in the body of
35
+ a composable. Use the effect APIs for anything that must happen *as a result*
36
+ of composition.
37
+ - Recomposition is driven by reads of *snapshot state* (`State<T>` /
38
+ `MutableState<T>`). Reading a state value subscribes that composition scope to
39
+ it; when it changes, only scopes that read it recompose.
40
+
41
+ ## State and remember
42
+
43
+ | API | Use for |
44
+ |---|---|
45
+ | `remember { }` | Keep an object across recompositions (not config changes). |
46
+ | `rememberSaveable { }` | Survive config changes and process death (Bundle-able). |
47
+ | `mutableStateOf(x)` | Observable state that triggers recomposition on change. |
48
+ | `derivedStateOf { }` | Computed state derived from other state (see below). |
49
+ | `produceState` | Convert non-Compose async source into `State`. |
50
+
51
+ ```kotlin
52
+ @Composable
53
+ fun Counter() {
54
+ var count by rememberSaveable { mutableStateOf(0) } // survives rotation
55
+ Button(onClick = { count++ }) { Text("Count: $count") }
56
+ }
57
+ ```
58
+
59
+ - Use `by` delegation (`var x by remember { mutableStateOf(...) }`) for ergonomic
60
+ reads/writes.
61
+ - `remember(key1, key2) { ... }` recomputes when a key changes — use keys to
62
+ reset remembered state when inputs change.
63
+ - Prefer `rememberSaveable` for anything the user would be annoyed to lose on
64
+ rotation (text input, scroll selection, expanded state).
65
+
66
+ ## State Hoisting
67
+
68
+ Make composables **stateless** by lifting state up to the caller. A hoisted
69
+ composable takes the value as a parameter and exposes changes via a callback —
70
+ the "state down, events up" pattern.
71
+
72
+ ```kotlin
73
+ // Stateless, reusable, testable
74
+ @Composable
75
+ fun SearchField(query: String, onQueryChange: (String) -> Unit) {
76
+ TextField(value = query, onValueChange = onQueryChange)
77
+ }
78
+
79
+ // Stateful caller owns the state
80
+ @Composable
81
+ fun SearchScreen(viewModel: SearchViewModel) {
82
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
83
+ SearchField(query = state.query, onQueryChange = viewModel::onQueryChange)
84
+ }
85
+ ```
86
+
87
+ Hoist to the lowest common ancestor that needs the state. State that belongs to
88
+ the screen's logic lives in the `ViewModel`; ephemeral UI state (animations,
89
+ focus, scroll) can stay local with `remember`.
90
+
91
+ ## derivedStateOf
92
+
93
+ Use `derivedStateOf` when state should be *computed* from other state and you
94
+ want to recompute only when the **result** changes, not on every read of the
95
+ inputs. The classic case: deriving a boolean from a frequently changing value.
96
+
97
+ ```kotlin
98
+ val listState = rememberLazyListState()
99
+ // Recomposes only when the boolean flips, not on every scroll pixel.
100
+ val showButton by remember {
101
+ derivedStateOf { listState.firstVisibleItemIndex > 0 }
102
+ }
103
+ ```
104
+
105
+ Do **not** use `derivedStateOf` for a plain transformation of a single state that
106
+ changes at the same rate as the result (e.g. `val upper = text.uppercase()`) —
107
+ that is just a normal calculation and the extra machinery is wasteful.
108
+
109
+ ## Recomposition Pitfalls
110
+
111
+ - **Reading state too high** hurts performance; read it as deep as possible so
112
+ fewer composables recompose. Defer reads with lambdas (e.g. pass
113
+ `{ offset }` rather than `offset`) for high-frequency values like scroll/drag.
114
+ - **Unstable lambdas** allocated in the body cause child recomposition — prefer
115
+ stable method references (`viewModel::onClick`) where possible.
116
+ - **Backwards writes** — writing to state you already read in the same
117
+ composition causes an infinite recomposition loop. Never do it.
118
+ - **Side effects in the body** (logging, mutation, IO) run on every recomposition
119
+ and in undefined order. Move them into effects.
120
+ - **`Modifier` ordering matters** — order changes layout and draw; it is not
121
+ commutative.
122
+ - Always supply a stable `key` in `LazyColumn`/`LazyRow` `items(list, key = {...})`
123
+ so items keep identity across data changes (analogous to a stable
124
+ `Identifiable` id).
125
+
126
+ ## Side-Effect APIs
127
+
128
+ Choose the narrowest effect that fits:
129
+
130
+ | API | When |
131
+ |---|---|
132
+ | `LaunchedEffect(key)` | Run a suspend function when entering composition / when `key` changes; cancelled on leave. |
133
+ | `rememberCoroutineScope()` | Launch coroutines from **event callbacks** (button clicks), not composition. |
134
+ | `DisposableEffect(key)` | Register/unregister non-suspend resources; `onDispose { }` cleans up. |
135
+ | `SideEffect { }` | Publish Compose state to non-Compose code after every successful recomposition. |
136
+ | `produceState` | Turn a callback/Flow source into observable `State`. |
137
+ | `snapshotFlow { }` | Convert reads of snapshot state into a cold `Flow`. |
138
+ | `rememberUpdatedState` | Capture the latest value inside a long-lived effect without restarting it. |
139
+
140
+ ```kotlin
141
+ // Run once on enter (key = Unit), re-run when userId changes:
142
+ LaunchedEffect(userId) {
143
+ viewModel.load(userId) // suspend; auto-cancelled on leave/key change
144
+ }
145
+
146
+ // Observe a lifecycle/listener and clean it up:
147
+ DisposableEffect(lifecycleOwner) {
148
+ val observer = LifecycleEventObserver { _, e -> handle(e) }
149
+ lifecycleOwner.lifecycle.addObserver(observer)
150
+ onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
151
+ }
152
+
153
+ // React to snapshot state as a flow:
154
+ LaunchedEffect(listState) {
155
+ snapshotFlow { listState.firstVisibleItemIndex }
156
+ .distinctUntilChanged()
157
+ .collect { analytics.logScroll(it) }
158
+ }
159
+ ```
160
+
161
+ Rules:
162
+ - The `key` of `LaunchedEffect`/`DisposableEffect` controls restart. Use a stable,
163
+ meaningful key — never `Unit` if the work depends on a changing input.
164
+ - Don't launch coroutines in the composable body; use `LaunchedEffect` (tied to
165
+ composition) or `rememberCoroutineScope` (tied to events).
166
+ - Use `rememberUpdatedState` to read the freshest callback inside a
167
+ `LaunchedEffect(Unit)` that should not restart.
168
+
169
+ ## Collecting Flows
170
+
171
+ Always collect with lifecycle awareness so collection stops in the background:
172
+
173
+ ```kotlin
174
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
175
+ ```
176
+
177
+ `collectAsStateWithLifecycle()` (from `lifecycle-runtime-compose`) is preferred
178
+ over `collectAsState()` on Android — it stops collecting when the lifecycle drops
179
+ below `STARTED`, avoiding wasted work and leaks.
180
+
181
+ ## Stability and Performance
182
+
183
+ - The compiler skips recomposition of a composable when all its parameters are
184
+ **stable** and unchanged. Prefer stable types: immutable `data class`es,
185
+ primitives, and `@Immutable`/`@Stable`-annotated types.
186
+ - `List<T>` is treated as unstable. Prefer `kotlinx.collections.immutable`
187
+ (`ImmutableList`/`PersistentList`) for list parameters, or wrap in a `@Immutable`
188
+ holder.
189
+ - The **Compose Compiler reports / strong-skipping** help find unstable params.
190
+ Strong skipping (default in recent compilers) skips even some unstable params by
191
+ comparing instances, but stable types remain the reliable path.
192
+ - Use `LazyColumn`/`LazyRow`/`LazyVerticalGrid` for large/scrolling collections;
193
+ never render large lists with a `Column` inside a `verticalScroll`.
194
+ - Defer high-frequency state reads (scroll offset, drag) into draw/layout lambdas
195
+ to avoid recomposing on every frame.
196
+ - Avoid `Modifier.composed { }` for new code; prefer `Modifier.Node` based
197
+ modifiers for custom modifiers.
198
+
199
+ ## Material 3
200
+
201
+ Use `androidx.compose.material3` (Material 3 / Material You) for new UIs.
202
+
203
+ ```kotlin
204
+ @Composable
205
+ fun AppTheme(content: @Composable () -> Unit) {
206
+ val dark = isSystemInDarkTheme()
207
+ val colors = if (supportsDynamicColor() && dark) {
208
+ dynamicDarkColorScheme(LocalContext.current) // Material You, Android 12+
209
+ } else if (dark) darkColorScheme() else lightColorScheme()
210
+
211
+ MaterialTheme(colorScheme = colors, typography = AppTypography, content = content)
212
+ }
213
+ ```
214
+
215
+ - Theme through `MaterialTheme` (`colorScheme`, `typography`, `shapes`); read
216
+ values via `MaterialTheme.colorScheme.*` rather than hardcoding colors.
217
+ - Support **dynamic color** on Android 12+ via `dynamicLightColorScheme` /
218
+ `dynamicDarkColorScheme` where the product wants Material You.
219
+ - Use Material 3 components (`Scaffold`, `TopAppBar`, `NavigationBar`,
220
+ `FilledTonalButton`, etc.) and honor `WindowInsets` / edge-to-edge.
221
+ - Respect the platform: this is Material, not iOS — don't port HIG idioms wholesale.
222
+
223
+ ## ViewModel Integration
224
+
225
+ - Obtain the screen `ViewModel` with `viewModel()` (or Hilt's `hiltViewModel()`).
226
+ - Expose a single immutable UI-state object as `StateFlow`; collect with
227
+ `collectAsStateWithLifecycle()`.
228
+ - Pass state down and events up (method references to the `ViewModel`). Keep
229
+ composables free of business logic.
230
+ - Use Compose Navigation (`NavHost`/`NavController`) or Navigation 3 if the
231
+ project has adopted it; hoist the `NavController` to the app root.
232
+
233
+ ## Common Mistakes
234
+
235
+ 1. **Side effects in the composable body** — IO, mutation, logging. Use effects.
236
+ 2. **`collectAsState()` instead of `collectAsStateWithLifecycle()`** on Android.
237
+ 3. **No `key` in lazy list `items`** — breaks item identity and animations.
238
+ 4. **`derivedStateOf` for a 1:1 transform** — unnecessary; just compute it.
239
+ 5. **`remember` where `rememberSaveable` is needed** — state lost on rotation.
240
+ 6. **Launching coroutines in the body** — use `LaunchedEffect`/event scope.
241
+ 7. **`LaunchedEffect(Unit)` that depends on a changing value** — stale captures;
242
+ key it properly or use `rememberUpdatedState`.
243
+ 8. **Unstable parameters** (`List`, lambdas allocated inline) defeating skipping.
244
+ 9. **Reading high-frequency state too high** in the tree — recomposes too much.
245
+ 10. **Backwards writes** — writing state already read this composition (infinite
246
+ loop).
247
+ 11. **Big scrollable `Column`** instead of `LazyColumn`.
248
+ 12. **Hoisting nothing** — monolithic stateful composables that can't be tested
249
+ or previewed.
250
+
251
+ ## Review Checklist
252
+
253
+ - [ ] Composables are side-effect free; effects use the proper effect API
254
+ - [ ] State hoisted appropriately (stateless leaf composables, state in ViewModel)
255
+ - [ ] `rememberSaveable` used for state that must survive config changes
256
+ - [ ] `derivedStateOf` used only for derived state that changes less than inputs
257
+ - [ ] Flows collected with `collectAsStateWithLifecycle()`
258
+ - [ ] `LaunchedEffect`/`DisposableEffect` keyed correctly; resources disposed
259
+ - [ ] Coroutines launched from events via `rememberCoroutineScope`, not body
260
+ - [ ] Lazy lists use stable `key`s
261
+ - [ ] Parameters are stable (immutable data classes / ImmutableList)
262
+ - [ ] Material 3 theming via `MaterialTheme`; dynamic color where wanted
263
+ - [ ] No backwards writes; no IO/mutation in composition
264
+ - [ ] High-frequency reads deferred (lambdas) to limit recomposition
@@ -0,0 +1,329 @@
1
+ ---
2
+ name: kotlin-coroutines
3
+ description: "Write data-safe asynchronous Kotlin with coroutines, Flow, and structured concurrency on Android. Use when fixing coroutine cancellation or leak bugs, choosing dispatchers (Default/IO/Main), designing suspend functions, modeling streams with Flow/StateFlow/SharedFlow, scoping work to viewModelScope/lifecycleScope, handling exceptions with CoroutineExceptionHandler/supervisorScope, or converting callback/RxJava APIs to coroutines. Covers Kotlin 2.x, kotlinx.coroutines structured concurrency discipline, and the Android main-safety contract."
4
+ ---
5
+
6
+ # Kotlin Coroutines
7
+
8
+ Review, fix, and write concurrent Kotlin targeting Kotlin 2.1+ and
9
+ kotlinx.coroutines 1.9+. Apply structured concurrency, correct dispatcher
10
+ choice, and cooperative cancellation with minimal behavior changes. This is the
11
+ Kotlin equivalent of Swift's structured-concurrency discipline: scopes are the
12
+ unit of lifetime, cancellation is cooperative, and main-safety is non-negotiable.
13
+
14
+ ## Contents
15
+
16
+ - [Triage Workflow](#triage-workflow)
17
+ - [Structured Concurrency](#structured-concurrency)
18
+ - [Coroutine Scopes](#coroutine-scopes)
19
+ - [Dispatchers and Main-Safety](#dispatchers-and-main-safety)
20
+ - [Cancellation](#cancellation)
21
+ - [Exception Handling](#exception-handling)
22
+ - [Flow](#flow)
23
+ - [StateFlow and SharedFlow](#stateflow-and-sharedflow)
24
+ - [Bridging Callback APIs](#bridging-callback-apis)
25
+ - [Common Mistakes](#common-mistakes)
26
+ - [Review Checklist](#review-checklist)
27
+
28
+ ## Triage Workflow
29
+
30
+ When diagnosing a coroutine issue, follow this sequence:
31
+
32
+ ### Step 1: Capture context
33
+
34
+ - Identify the scope the coroutine runs in (`viewModelScope`, `lifecycleScope`,
35
+ a custom `CoroutineScope`, or an unscoped `GlobalScope` — a red flag).
36
+ - Identify the dispatcher in effect and whether the work is CPU-bound,
37
+ IO-bound, or UI-bound.
38
+ - Determine whether the work must survive configuration changes / navigation, or
39
+ should be cancelled with the screen.
40
+ - Copy any stack trace; note whether it is a `CancellationException` (normal) or
41
+ a real failure.
42
+
43
+ ### Step 2: Apply the smallest safe fix
44
+
45
+ | Situation | Recommended fix |
46
+ |---|---|
47
+ | Work tied to a screen | Launch in `viewModelScope`; never `GlobalScope`. |
48
+ | Blocking IO (disk, network, DB) | `withContext(Dispatchers.IO) { ... }`. |
49
+ | Heavy CPU work (parse, sort, image) | `withContext(Dispatchers.Default) { ... }`. |
50
+ | UI / state update | Stay on `Dispatchers.Main` (the default for `viewModelScope`). |
51
+ | One child failure must not kill siblings | Use `supervisorScope` / `SupervisorJob`. |
52
+ | Long loop never cancels | Add `ensureActive()` / `yield()` or use cancellable APIs. |
53
+ | Callback API | Wrap with `suspendCancellableCoroutine` or `callbackFlow`. |
54
+
55
+ ### Step 3: Verify
56
+
57
+ - Confirm cancellation propagates (the work stops when the scope is cancelled).
58
+ - Confirm no work runs on the main thread that blocks it.
59
+ - Confirm exceptions surface (not silently swallowed) and `CancellationException`
60
+ is never caught-and-ignored.
61
+
62
+ ## Structured Concurrency
63
+
64
+ Every coroutine has a parent. A parent does not complete until all children
65
+ complete; cancelling a parent cancels all children. This is the core safety
66
+ guarantee — never break out of it.
67
+
68
+ - Prefer `coroutineScope { }` to launch concurrent children that all must finish
69
+ before the function returns.
70
+ - Use `async`/`await` for concurrent work that produces values.
71
+ - Use `launch` for fire-and-forget work *within a scope* (not detached).
72
+
73
+ ```kotlin
74
+ suspend fun loadDashboard(): Dashboard = coroutineScope {
75
+ val profile = async { repo.profile() } // runs concurrently
76
+ val feed = async { repo.feed() } // runs concurrently
77
+ Dashboard(profile.await(), feed.await()) // both joined here
78
+ }
79
+ ```
80
+
81
+ If either child throws, the scope cancels the sibling and rethrows — no leaks.
82
+
83
+ **Never** use `GlobalScope.launch`. It has no parent, no lifecycle, no
84
+ cancellation. It is the coroutine equivalent of a detached, unowned thread.
85
+
86
+ ## Coroutine Scopes
87
+
88
+ | Scope | Lifecycle | Use for |
89
+ |---|---|---|
90
+ | `viewModelScope` | Cleared when the `ViewModel` clears | All ViewModel async work |
91
+ | `lifecycleScope` | Tied to a `Lifecycle` (Activity/Fragment) | UI-layer work bound to a screen |
92
+ | `rememberCoroutineScope()` | Tied to a composable's lifetime | Launching from Compose event callbacks |
93
+ | custom `CoroutineScope` | You manage `cancel()` | Long-lived components (cancel explicitly) |
94
+
95
+ For a custom scope, always pair a `SupervisorJob` with a dispatcher and cancel it
96
+ when the owner is destroyed:
97
+
98
+ ```kotlin
99
+ class Connection {
100
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
101
+ fun close() = scope.cancel() // cancels all children — no leaks
102
+ }
103
+ ```
104
+
105
+ In Compose, collect with `collectAsStateWithLifecycle()` (not `collectAsState()`)
106
+ so collection stops when the app is in the background.
107
+
108
+ ## Dispatchers and Main-Safety
109
+
110
+ A suspend function should be **main-safe**: callable from the main thread without
111
+ blocking it. Push the thread switch *down* into the function, not up to callers.
112
+
113
+ - `Dispatchers.Main` — UI updates. The default for `viewModelScope`.
114
+ - `Dispatchers.Default` — CPU-bound work (parsing, sorting, image processing).
115
+ Backed by a pool sized to CPU cores.
116
+ - `Dispatchers.IO` — blocking IO (disk, network sockets, JDBC, file). Backed by
117
+ a large elastic pool.
118
+ - `Dispatchers.Main.immediate` — runs inline if already on the main thread,
119
+ avoiding an unnecessary re-dispatch (use for state emission).
120
+
121
+ ```kotlin
122
+ // Main-safe: caller can invoke from the main thread.
123
+ suspend fun parseLargeJson(raw: String): Model = withContext(Dispatchers.Default) {
124
+ json.decodeFromString(raw)
125
+ }
126
+ ```
127
+
128
+ Rules:
129
+ 1. Never block the main thread (`Thread.sleep`, blocking network, `runBlocking`).
130
+ 2. Use `withContext`, not `launch`, to switch dispatchers for a value-returning
131
+ step — `withContext` returns to the original dispatcher when done.
132
+ 3. Don't hardcode dispatchers in the call site you want to test; inject a
133
+ dispatcher (or a `CoroutineDispatcher` provider) so tests can substitute
134
+ `StandardTestDispatcher`.
135
+
136
+ ## Cancellation
137
+
138
+ Cancellation is cooperative. A coroutine only stops at a suspension point or when
139
+ it checks cooperatively.
140
+
141
+ ```kotlin
142
+ suspend fun process(items: List<Item>) {
143
+ for (item in items) {
144
+ ensureActive() // throws CancellationException if cancelled
145
+ heavyWork(item) // CPU loops won't cancel on their own
146
+ }
147
+ }
148
+ ```
149
+
150
+ - Suspending functions from kotlinx.coroutines (`delay`, `withContext`, Flow
151
+ operators) are cancellable automatically.
152
+ - Tight CPU loops are **not** — insert `ensureActive()` or `yield()`.
153
+ - `CancellationException` is special: it signals normal cancellation. Never catch
154
+ it and swallow it. If you catch `Exception` broadly, rethrow it:
155
+
156
+ ```kotlin
157
+ try {
158
+ risky()
159
+ } catch (e: CancellationException) {
160
+ throw e // must rethrow — never swallow
161
+ } catch (e: Exception) {
162
+ handle(e)
163
+ }
164
+ ```
165
+
166
+ - For cleanup that must run even after cancellation, use
167
+ `withContext(NonCancellable) { ... }` inside a `finally`.
168
+
169
+ ## Exception Handling
170
+
171
+ - In `coroutineScope` / `async`, exceptions propagate to the awaiting caller —
172
+ handle with `try/catch` around `await()`.
173
+ - In `launch`, an uncaught exception propagates up and cancels the scope. Install
174
+ a `CoroutineExceptionHandler` on the **root** scope for last-resort handling.
175
+ - `supervisorScope` / `SupervisorJob` isolate child failures so one failing child
176
+ does not cancel its siblings:
177
+
178
+ ```kotlin
179
+ supervisorScope {
180
+ launch { mightFail() } // failure here does NOT cancel the sibling
181
+ launch { mustSucceed() }
182
+ }
183
+ ```
184
+
185
+ - A `CoroutineExceptionHandler` only works on root `launch` coroutines, never on
186
+ `async` (whose exception surfaces at `await`).
187
+
188
+ ## Flow
189
+
190
+ `Flow<T>` is a cold asynchronous stream — it does nothing until collected, and
191
+ re-runs its producer for each collector. It is the Kotlin analogue of an
192
+ `AsyncSequence`.
193
+
194
+ ```kotlin
195
+ fun searchResults(query: String): Flow<List<Result>> = flow {
196
+ emit(emptyList()) // initial
197
+ emit(repo.search(query)) // suspends, emits result
198
+ }
199
+ ```
200
+
201
+ Key operators:
202
+ - Transform: `map`, `filter`, `transform`, `flatMapLatest` (cancel-previous —
203
+ ideal for search-as-you-type), `flatMapConcat`, `flatMapMerge`.
204
+ - Combine: `combine`, `zip`, `merge`.
205
+ - Lifecycle: `onStart`, `onEach`, `onCompletion`, `catch` (upstream errors only),
206
+ `retry`.
207
+ - Backpressure/threading: `flowOn(Dispatchers.IO)` changes the **upstream**
208
+ dispatcher; `buffer`, `conflate`, `debounce`, `sample`.
209
+
210
+ ```kotlin
211
+ fun results(query: Flow<String>): Flow<UiState> = query
212
+ .debounce(300)
213
+ .distinctUntilChanged()
214
+ .flatMapLatest { q -> repo.search(q) } // cancels stale searches
215
+ .map(UiState::Loaded)
216
+ .catch { emit(UiState.Error(it)) }
217
+ .flowOn(Dispatchers.Default)
218
+ ```
219
+
220
+ Rules:
221
+ - Use `flowOn` to set the producer dispatcher; never call `withContext` inside a
222
+ `flow { }` builder (it breaks context preservation — the builder enforces this).
223
+ - Put `catch` *downstream* of the operators whose errors you want to handle;
224
+ collector-side errors are not caught by `catch`.
225
+
226
+ ## StateFlow and SharedFlow
227
+
228
+ These are **hot** flows for UI state and events.
229
+
230
+ - `StateFlow<T>` — holds a single current value, conflates, always has a value.
231
+ The standard holder for screen UI state. Emit via `MutableStateFlow`.
232
+ - `SharedFlow<T>` — broadcasts events to multiple collectors; configurable
233
+ replay. Use for one-shot events (navigation, snackbars) with `replay = 0`.
234
+
235
+ ```kotlin
236
+ class FeedViewModel(private val repo: FeedRepository) : ViewModel() {
237
+ private val _state = MutableStateFlow<FeedUiState>(FeedUiState.Loading)
238
+ val state: StateFlow<FeedUiState> = _state.asStateFlow()
239
+
240
+ init {
241
+ viewModelScope.launch {
242
+ _state.value = try {
243
+ FeedUiState.Loaded(repo.feed())
244
+ } catch (e: CancellationException) {
245
+ throw e
246
+ } catch (e: Exception) {
247
+ FeedUiState.Error(e.message)
248
+ }
249
+ }
250
+ }
251
+ }
252
+ ```
253
+
254
+ Convert a cold flow to a lifecycle-aware hot `StateFlow` with `stateIn`:
255
+
256
+ ```kotlin
257
+ val items: StateFlow<List<Item>> = repo.observeItems()
258
+ .stateIn(
259
+ scope = viewModelScope,
260
+ started = SharingStarted.WhileSubscribed(5_000), // stop 5s after last collector
261
+ initialValue = emptyList()
262
+ )
263
+ ```
264
+
265
+ `WhileSubscribed(5_000)` keeps the upstream alive briefly across configuration
266
+ changes while still stopping when nothing observes — the standard choice.
267
+
268
+ For one-shot events prefer a `Channel` consumed as a flow, or a `SharedFlow` with
269
+ `replay = 0`; do **not** model events as `StateFlow` (state is re-delivered on
270
+ recomposition and replays stale events).
271
+
272
+ ## Bridging Callback APIs
273
+
274
+ Single value:
275
+
276
+ ```kotlin
277
+ suspend fun awaitLocation(): Location = suspendCancellableCoroutine { cont ->
278
+ val cb = LocationCallback { loc -> cont.resume(loc) }
279
+ locationManager.request(cb)
280
+ cont.invokeOnCancellation { locationManager.remove(cb) } // clean up
281
+ }
282
+ ```
283
+
284
+ Stream of values:
285
+
286
+ ```kotlin
287
+ fun locationUpdates(): Flow<Location> = callbackFlow {
288
+ val cb = LocationCallback { loc -> trySend(loc) }
289
+ locationManager.request(cb)
290
+ awaitClose { locationManager.remove(cb) } // required — cleans up on cancel
291
+ }
292
+ ```
293
+
294
+ ## Common Mistakes
295
+
296
+ 1. **`GlobalScope.launch`** — no lifecycle, leaks. Use a real scope.
297
+ 2. **`runBlocking` in production code** — blocks the thread; it is for `main()`
298
+ and tests only.
299
+ 3. **Swallowing `CancellationException`** — breaks structured cancellation;
300
+ always rethrow it.
301
+ 4. **Blocking the main thread** — `Thread.sleep`, blocking IO, or heavy CPU on
302
+ `Dispatchers.Main`.
303
+ 5. **`withContext` inside a `flow { }` builder** — use `flowOn` instead.
304
+ 6. **Collecting flows with `collectAsState()` in Compose** — use
305
+ `collectAsStateWithLifecycle()` so collection pauses in the background.
306
+ 7. **Hardcoded dispatchers** — inject them so tests can swap in test
307
+ dispatchers.
308
+ 8. **Modeling one-shot events as `StateFlow`** — they replay stale events; use a
309
+ `Channel`/`SharedFlow(replay = 0)`.
310
+ 9. **Launching independent work that should be concurrent sequentially** — use
311
+ `async`/`await` in a `coroutineScope`.
312
+ 10. **Catching exceptions on `async` with a `CoroutineExceptionHandler`** — it
313
+ does not fire for `async`; catch at `await`.
314
+ 11. **Forgetting `awaitClose` in `callbackFlow`** — leaks the callback.
315
+
316
+ ## Review Checklist
317
+
318
+ - [ ] No `GlobalScope`; work runs in `viewModelScope`/`lifecycleScope`/owned scope
319
+ - [ ] No `runBlocking` outside `main`/tests
320
+ - [ ] Suspend functions are main-safe (`withContext` pushes the switch down)
321
+ - [ ] CPU loops check cancellation (`ensureActive`/`yield`)
322
+ - [ ] `CancellationException` is rethrown, never swallowed
323
+ - [ ] Child-failure isolation uses `supervisorScope`/`SupervisorJob` where needed
324
+ - [ ] `flowOn` sets upstream dispatcher; no `withContext` inside `flow {}`
325
+ - [ ] UI state exposed as immutable `StateFlow` (backed by `MutableStateFlow`)
326
+ - [ ] `stateIn` uses `WhileSubscribed(5_000)` for screen state
327
+ - [ ] One-shot events are not modeled as `StateFlow`
328
+ - [ ] `callbackFlow`/`suspendCancellableCoroutine` clean up on cancellation
329
+ - [ ] Dispatchers injected for testability
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: python-conventions
3
+ description: Python coding conventions for this repo (Python 3.12+, type hints, PEP 257/8). Use when writing, reviewing, or editing any *.py file. Covers type hints, docstrings, formatting, exception handling, async I/O, virtualenv discipline, and test expectations.
4
+ ---
5
+
6
+ # Python Coding Conventions
7
+
8
+ Applies to: any `*.py` file in this repo. Target: Python 3.12+.
9
+
10
+ > Naturalize: confirm the venv location, package manager (uv / pip / poetry), and test runner in `CLAUDE.md`. The defaults below assume a `uv`-managed `.venv/` and `pytest`.
11
+
12
+ ## Function-level expectations
13
+
14
+ - Descriptive function names; type hints on parameters and returns (use builtin generics on modern targets, e.g. `list[str]`, `dict[str, int]`; fall back to `typing.List` / `typing.Dict` only on older runtimes).
15
+ - PEP 257 docstrings placed immediately after `def` / `class`.
16
+ - Break complex functions into smaller, single-purpose helpers.
17
+ - Handle edge cases explicitly; raise or return meaningful errors rather than silently swallowing.
18
+
19
+ ## General
20
+
21
+ - Readability and clarity over cleverness.
22
+ - For non-obvious algorithms, include a short comment explaining the approach.
23
+ - Mention third-party library usage and purpose at the import site if non-obvious.
24
+ - Consistent naming; follow language idioms.
25
+
26
+ ## Style
27
+
28
+ - **PEP 8.** 4-space indents. Keep lines reasonable (the classic guideline is 79; many repos tolerate longer for clarity — match surrounding code and any configured formatter, e.g. `ruff`/`black`).
29
+ - Blank lines to separate functions, classes, and logical blocks.
30
+
31
+ ## Async I/O
32
+
33
+ - For async code, use `async def` / `await` end-to-end; don't block the event loop with sync I/O.
34
+ - Prefer async-native drivers (e.g. an async DB/HTTP client) over wrapping sync calls in threads.
35
+ - Use `asyncio.Lock` / structured task management for shared mutable state; guard caches against concurrent rebuilds.
36
+
37
+ ## Environment & dependencies
38
+
39
+ - Use the project venv (commonly `.venv/`, often `uv`-managed). If a stale top-level `venv/` exists, prefer the project-documented one — check `CLAUDE.md`.
40
+ - Load config from the environment (e.g. `python-dotenv`); quote `.env` values containing spaces so `source .env` doesn't break in zsh.
41
+
42
+ ## Edge cases and testing
43
+
44
+ - Cover critical paths with tests; include empty inputs, invalid types, and large-input cases where relevant.
45
+ - Prefer `pytest`. Run from the project venv.
46
+
47
+ ## Example
48
+
49
+ ```python
50
+ def calculate_area(radius: float) -> float:
51
+ """Calculate the area of a circle given the radius.
52
+
53
+ Parameters:
54
+ radius: The radius of the circle.
55
+
56
+ Returns:
57
+ The area, computed as pi * radius**2.
58
+ """
59
+ import math
60
+ return math.pi * radius ** 2
61
+ ```