@mrciphersmith/keryx 0.3.3 → 0.3.5

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 (67) hide show
  1. package/dist/cli.js +125 -51
  2. package/package.json +1 -1
  3. package/src/gdskills/bundled/install-manifest.json +231 -2
  4. package/src/gdskills/bundled/stacks/csharp-dotnet/agent-refs.json +4 -0
  5. package/src/gdskills/bundled/stacks/csharp-dotnet/governance/eval.json +1881 -0
  6. package/src/gdskills/bundled/stacks/csharp-dotnet/governance/scout.json +33 -0
  7. package/src/gdskills/bundled/stacks/csharp-dotnet/pack.json +38 -0
  8. package/src/gdskills/bundled/stacks/csharp-dotnet/rules/coding-style.mdc +100 -0
  9. package/src/gdskills/bundled/stacks/csharp-dotnet/rules/patterns.mdc +107 -0
  10. package/src/gdskills/bundled/stacks/csharp-dotnet/rules/security.mdc +86 -0
  11. package/src/gdskills/bundled/stacks/csharp-dotnet/rules/testing.mdc +89 -0
  12. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-build-fix/SKILL.md +143 -0
  13. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-build-fix/evals.json +77 -0
  14. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-code-review/SKILL.md +121 -0
  15. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-code-review/evals.json +77 -0
  16. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-implementation/SKILL.md +134 -0
  17. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-implementation/evals.json +76 -0
  18. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-testing/SKILL.md +130 -0
  19. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-testing/evals.json +77 -0
  20. package/src/gdskills/bundled/stacks/flutter-dart/agent-refs.json +4 -0
  21. package/src/gdskills/bundled/stacks/flutter-dart/governance/eval.json +1849 -0
  22. package/src/gdskills/bundled/stacks/flutter-dart/governance/scout.json +33 -0
  23. package/src/gdskills/bundled/stacks/flutter-dart/pack.json +41 -0
  24. package/src/gdskills/bundled/stacks/flutter-dart/rules/coding-style.mdc +98 -0
  25. package/src/gdskills/bundled/stacks/flutter-dart/rules/patterns.mdc +88 -0
  26. package/src/gdskills/bundled/stacks/flutter-dart/rules/security.mdc +91 -0
  27. package/src/gdskills/bundled/stacks/flutter-dart/rules/testing.mdc +101 -0
  28. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-build-fix/SKILL.md +134 -0
  29. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-build-fix/evals.json +79 -0
  30. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-code-review/SKILL.md +124 -0
  31. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-code-review/evals.json +74 -0
  32. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-implementation/SKILL.md +139 -0
  33. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-implementation/evals.json +77 -0
  34. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-testing/SKILL.md +134 -0
  35. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-testing/evals.json +74 -0
  36. package/src/gdskills/bundled/stacks/kotlin-android/agent-refs.json +4 -0
  37. package/src/gdskills/bundled/stacks/kotlin-android/governance/eval.json +1889 -0
  38. package/src/gdskills/bundled/stacks/kotlin-android/governance/scout.json +34 -0
  39. package/src/gdskills/bundled/stacks/kotlin-android/pack.json +38 -0
  40. package/src/gdskills/bundled/stacks/kotlin-android/rules/coding-style.mdc +89 -0
  41. package/src/gdskills/bundled/stacks/kotlin-android/rules/patterns.mdc +96 -0
  42. package/src/gdskills/bundled/stacks/kotlin-android/rules/security.mdc +90 -0
  43. package/src/gdskills/bundled/stacks/kotlin-android/rules/testing.mdc +89 -0
  44. package/src/gdskills/bundled/stacks/kotlin-android/skills/compose-implementation/SKILL.md +150 -0
  45. package/src/gdskills/bundled/stacks/kotlin-android/skills/compose-implementation/evals.json +77 -0
  46. package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-build-fix/SKILL.md +151 -0
  47. package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-build-fix/evals.json +76 -0
  48. package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-code-review/SKILL.md +139 -0
  49. package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-code-review/evals.json +78 -0
  50. package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-testing/SKILL.md +131 -0
  51. package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-testing/evals.json +77 -0
  52. package/src/gdskills/bundled/stacks/swift-ios/agent-refs.json +4 -0
  53. package/src/gdskills/bundled/stacks/swift-ios/governance/eval.json +1803 -0
  54. package/src/gdskills/bundled/stacks/swift-ios/governance/scout.json +32 -0
  55. package/src/gdskills/bundled/stacks/swift-ios/pack.json +38 -0
  56. package/src/gdskills/bundled/stacks/swift-ios/rules/coding-style.mdc +92 -0
  57. package/src/gdskills/bundled/stacks/swift-ios/rules/patterns.mdc +112 -0
  58. package/src/gdskills/bundled/stacks/swift-ios/rules/security.mdc +78 -0
  59. package/src/gdskills/bundled/stacks/swift-ios/rules/testing.mdc +90 -0
  60. package/src/gdskills/bundled/stacks/swift-ios/skills/swift-build-fix/SKILL.md +144 -0
  61. package/src/gdskills/bundled/stacks/swift-ios/skills/swift-build-fix/evals.json +75 -0
  62. package/src/gdskills/bundled/stacks/swift-ios/skills/swift-code-review/SKILL.md +122 -0
  63. package/src/gdskills/bundled/stacks/swift-ios/skills/swift-code-review/evals.json +75 -0
  64. package/src/gdskills/bundled/stacks/swift-ios/skills/swift-testing/SKILL.md +131 -0
  65. package/src/gdskills/bundled/stacks/swift-ios/skills/swift-testing/evals.json +75 -0
  66. package/src/gdskills/bundled/stacks/swift-ios/skills/swiftui-implementation/SKILL.md +149 -0
  67. package/src/gdskills/bundled/stacks/swift-ios/skills/swiftui-implementation/evals.json +76 -0
@@ -0,0 +1,34 @@
1
+ [
2
+ {
3
+ "query": "Use when implementing or extending a feature in a Kotlin/Android app with Jetpack Compose -- composable state hoisting, coroutine scopes (viewModelScope/lifecycleScope), StateFlow exposure from a ViewModel, and recomposition-safe side effects.",
4
+ "decision": "fork",
5
+ "topMatch": "kotlin-android/kotlin-android-code-review",
6
+ "recordedAt": "2026-09-25T15:35:02.929Z",
7
+ "skillName": "compose-implementation",
8
+ "justification": "Top match kotlin-android/kotlin-android-code-review is this same pack's read-only review skill (different intent, action verb, workflow), not a real implementation duplicate; no existing implement skill covers Compose state hoisting, coroutine scopes, or StateFlow exposure."
9
+ },
10
+ {
11
+ "query": "Use when a Kotlin/Android module's test suite needs writing, extending, or fixing -- runTest/TestDispatcher for suspend functions and ViewModels, ComposeTestRule semantics-based finders for UI tests, and MockK at the repository/data-source interface boundary.",
12
+ "decision": "fork",
13
+ "topMatch": "swift-ios/swift-testing",
14
+ "recordedAt": "2026-09-25T15:35:03.232Z",
15
+ "skillName": "kotlin-android-testing",
16
+ "justification": "Nearest match swift-ios/swift-testing tests Swift/XCTest/Swift Testing code with a different runtime and mocking library entirely; no existing test skill covers Kotlin coroutine test dispatchers, ComposeTestRule, or MockK."
17
+ },
18
+ {
19
+ "query": "Use when reviewing a Kotlin/Android change for coroutine, Compose, and null-safety risks -- GlobalScope usage, side effects fired directly in a composable body, forced-unwrap on external data, mutable state leaked from a ViewModel, and exported manifest components. Read-only, no edits.",
20
+ "decision": "fork",
21
+ "topMatch": "kotlin-android/compose-implementation",
22
+ "recordedAt": "2026-09-25T15:35:03.502Z",
23
+ "skillName": "kotlin-android-code-review",
24
+ "justification": "Top match kotlin-android/compose-implementation is this same pack's implementation skill (different intent: write code vs read-only review), not a real review duplicate; no existing review skill covers Kotlin GlobalScope, Compose recomposition, or Android manifest exposure risks."
25
+ },
26
+ {
27
+ "query": "Use when a Gradle Android build, ktlintCheck, detekt, or lint task fails, or a Kotlin compile/type error blocks the build -- resolves Gradle/AGP/Kotlin version mismatches, Compose Compiler mismatches, unresolved dependencies, and lint/detekt findings with the smallest root-cause fix.",
28
+ "decision": "fork",
29
+ "topMatch": "csharp-dotnet/dotnet-build-fix",
30
+ "recordedAt": "2026-09-25T15:35:03.780Z",
31
+ "skillName": "kotlin-android-build-fix",
32
+ "justification": "Nearest match csharp-dotnet/dotnet-build-fix fixes dotnet/NuGet/StyleCop errors, not Gradle/AGP/Kotlin/Compose Compiler/detekt findings; each build-fix skill is scoped to its own toolchain's specific failure surface."
33
+ }
34
+ ]
@@ -0,0 +1,38 @@
1
+ {
2
+ "id": "kotlin-android",
3
+ "family": "framework",
4
+ "modules": ["kotlin-android-rules", "kotlin-android-skills"],
5
+ "detectionMarkers": ["android"],
6
+ "provenance": { "origin": "authored", "sourceRef": "flow 336, Wave 4 batch 4" },
7
+ "stability": "experimental",
8
+ "skills": {
9
+ "implement": ["compose-implementation"],
10
+ "test": ["kotlin-android-testing"],
11
+ "review": ["kotlin-android-code-review"],
12
+ "build-fix": ["kotlin-android-build-fix"],
13
+ "migrate": []
14
+ },
15
+ "agentProfile": {
16
+ "displayName": "Kotlin/Android",
17
+ "auditFocus": [
18
+ "`!!` forced-unwrap on a value from an external source (network response, Intent extra, savedInstanceState) instead of a safe call (`?.`) or explicit null handling",
19
+ "`GlobalScope.launch`/`GlobalScope.async` inside a ViewModel or composable instead of `viewModelScope`/`lifecycleScope`, losing structured cancellation",
20
+ "state read or mutated directly inside a composable body instead of via `remember`/`rememberSaveable`/hoisted state, causing recomposition to re-run side effects",
21
+ "a suspend call, `Toast`, navigation, or analytics event fired directly in a composable body instead of inside `LaunchedEffect`/`DisposableEffect`/a callback",
22
+ "a ViewModel exposing `MutableStateFlow`/`MutableLiveData` directly to the UI layer instead of a read-only `StateFlow`/`LiveData` view",
23
+ "secrets or API keys stored in plain `SharedPreferences`, `BuildConfig` fields committed to source, or a manifest component left `exported=\"true\"` with no permission"
24
+ ],
25
+ "buildCommands": [
26
+ "./gradlew assembleDebug",
27
+ "./gradlew testDebugUnitTest",
28
+ "./gradlew lint",
29
+ "./gradlew ktlintCheck || ./gradlew detekt"
30
+ ],
31
+ "fixGuardrails": [
32
+ "never add `!!` or a blanket `@Suppress` annotation to silence a null-safety or lint warning instead of fixing the underlying nullability or logic",
33
+ "never widen a Gradle/AGP/Kotlin version pin or disable a lint/detekt rule repo-wide just to make one failure disappear without saying so in the report",
34
+ "never move a `LaunchedEffect`/`DisposableEffect` body directly into the composable function to work around a recomposition bug -- fix the key or hoist the state instead",
35
+ "never delete or weaken a failing test's assertion to reach a green build; fix the root cause or say the test caught a real bug"
36
+ ]
37
+ }
38
+ }
@@ -0,0 +1,89 @@
1
+ ---
2
+ extends: common
3
+ paths: ["**/*.kt", "**/*.kts"]
4
+ metadata:
5
+ origin: authored
6
+ ---
7
+
8
+ # Kotlin/Android coding style
9
+
10
+ Narrows `core-common-rules`' stack-agnostic style rules to modern
11
+ Kotlin/Android idiom. Applies only to `*.kt`/`*.kts` files — everything
12
+ not Kotlin-specific still comes from the common rules this file `extends`.
13
+
14
+ ## Naming and structure
15
+
16
+ - `PascalCase` for classes, objects, and composables (a `@Composable`
17
+ function that emits UI is named as a noun, `UserProfileCard`, not a verb);
18
+ `camelCase` for functions, properties, and local variables; `SCREAMING_SNAKE_CASE`
19
+ only for `const val` compile-time constants.
20
+ - A `@Composable` function that returns a value instead of emitting UI
21
+ (e.g. `rememberX`) still reads as a normal function name, not a noun —
22
+ reserve the noun-style name for functions that actually emit UI.
23
+ - Keep a file's top-level surface small: one primary class/object per
24
+ file named after it, with small private helpers colocated rather than
25
+ scattered into a shared `Utils.kt`.
26
+ - Package names are all lowercase, no underscores, matching the directory
27
+ layout (`com.example.feature.profile`, not `com.example.Feature.Profile`).
28
+
29
+ ## Data classes vs regular classes
30
+
31
+ - Use `data class` for a type whose identity is its held values (a DTO, a
32
+ UI state holder, a value object needing `equals`/`hashCode`/`copy`) —
33
+ never a `data class` for something with real identity or mutable
34
+ reference semantics (an Android `Activity`, a repository, a singleton
35
+ manager).
36
+ - Prefer `copy()` with named parameters to produce a modified instance
37
+ over building a fresh constructor call by hand; it keeps every unrelated
38
+ field unchanged and the diff at a call site small when a new field is
39
+ added later.
40
+ - A `data class` holding UI state should default every field to a sane
41
+ initial value so `State()` (or `UiState()`) alone is a valid starting
42
+ point.
43
+
44
+ ## Null-safety idiom
45
+
46
+ - Prefer a safe call (`?.`) or the Elvis operator (`?:`) over `!!`; a
47
+ forced-unwrap should be reserved for a value the type system cannot
48
+ express as non-null but that a prior check (or the language, e.g. inside
49
+ a `requireNotNull` block) has already proven non-null at that point —
50
+ never as a shortcut past a compiler nullability warning.
51
+ - Use `let` for a null-checked scope over a nullable receiver
52
+ (`value?.let { ... }`) when the block needs the unwrapped value once;
53
+ avoid chaining several `?.let` calls where a single `if (value != null)`
54
+ smart-cast reads more clearly.
55
+ - Prefer `requireNotNull(x) { "why x must be non-null here" }` over `x!!`
56
+ at a boundary (constructor, public function entry) — it fails with a
57
+ message that explains the invariant instead of a bare
58
+ `NullPointerException`.
59
+ - Model "value or absent" with a nullable type or a sealed result type,
60
+ not a sentinel value (`-1`, `""`) that callers have to remember to check
61
+ for.
62
+
63
+ ## Sealed classes/interfaces for state modeling
64
+
65
+ - Model a UI or domain state with a `sealed interface`/`sealed class`
66
+ hierarchy (`Loading`, `Success(data)`, `Error(throwable)`) rather than a
67
+ single class with several nullable fields and a separate `isLoading`
68
+ boolean — the compiler then forces an exhaustive `when` at every call
69
+ site, so a new state can't be silently unhandled.
70
+ - Prefer a `sealed interface` over `sealed class` when a variant needs no
71
+ shared state or behavior; reach for `sealed class` only when the
72
+ variants share a common property or method implementation.
73
+ - Keep a sealed hierarchy's variants in the same file (or a `sealed`-file
74
+ group) as the parent — Kotlin does not require this, but scattering
75
+ variants across files makes the exhaustiveness check hard to audit by
76
+ reading.
77
+
78
+ ## Extension functions
79
+
80
+ - Add an extension function to attach behavior to a type this module does
81
+ not own (or to keep a small, focused helper out of a growing class) —
82
+ not to route around giving a type a proper method when the type is
83
+ already yours to edit.
84
+ - Keep an extension's receiver type narrow and its name unambiguous
85
+ (`String.toDisplayDate()`, not a generic `Any.format()`); an extension
86
+ with a vague name invites collisions and surprises at the call site.
87
+ - Avoid extension-function sprawl on core types (`String`, `Int`, `List`)
88
+ for one-off, feature-specific logic — a feature-scoped extension on a
89
+ feature-scoped type keeps the change's blast radius visible.
@@ -0,0 +1,96 @@
1
+ ---
2
+ extends: common
3
+ paths: ["**/*.kt", "**/*.kts"]
4
+ metadata:
5
+ origin: authored
6
+ ---
7
+
8
+ # Kotlin/Android patterns
9
+
10
+ Narrows `core-common-rules`' stack-agnostic design guidance to idiomatic
11
+ Kotlin coroutines and Jetpack Compose design. Applies only to
12
+ `*.kt`/`*.kts` files.
13
+
14
+ ## Coroutines: structured concurrency
15
+
16
+ - Launch a coroutine from a scope tied to the lifecycle that owns it —
17
+ `viewModelScope` inside a `ViewModel`, `lifecycleScope` inside an
18
+ `Activity`/`Fragment`, or a plain `coroutineScope { }`/`suspend`
19
+ function for work with no lifecycle owner. Never `GlobalScope.launch`/
20
+ `GlobalScope.async`: `GlobalScope` is unstructured — nothing cancels it
21
+ when the screen or the ViewModel goes away, so it outlives the UI it was
22
+ started for and can update state, navigate, or write to a destroyed
23
+ view.
24
+ - When a function needs to run several suspend calls concurrently, wrap
25
+ them in `coroutineScope { }` and use `async`/`await`, not a `launch`
26
+ fired without an `await` point — `coroutineScope` waits for every child
27
+ to finish (or cancels the rest if one fails) before returning, so a
28
+ caller downstream never observes a half-finished fan-out.
29
+ - Cancellation is cooperative: a long-running loop or CPU-bound block
30
+ inside a coroutine should call `ensureActive()`/check
31
+ `coroutineContext.isActive`, or use a cancellable suspending call
32
+ (`delay`, `yield`), so cancelling the parent scope actually stops it
33
+ instead of running to completion regardless.
34
+ - Switch dispatcher with `withContext(Dispatchers.IO)` around a blocking
35
+ call (network client without its own suspend API, disk I/O), not by
36
+ launching a whole new coroutine on that dispatcher — `withContext`
37
+ keeps the call sequential and propagates cancellation and the result
38
+ back to the caller.
39
+
40
+ ## Compose: recomposition correctness
41
+
42
+ - Strong skipping mode is the Compose compiler default since Kotlin 2.0.20:
43
+ every restartable composable is skippable even with an unstable
44
+ parameter (a plain `List`/`MutableList`, a mutable var-holding class with
45
+ no `@Stable`/`@Immutable` annotation) — the compiler compares unstable
46
+ parameters by instance identity instead of refusing to skip. Prefer
47
+ stable parameters anyway (a primitive, a `data class` of stable members,
48
+ an immutable collection) since identity comparison still recomposes on
49
+ every NEW instance (e.g. a `List` rebuilt each call), where a genuinely
50
+ stable/immutable type can compare equal and skip.
51
+ - Hoist state to the nearest common ancestor that needs it (a caller, or
52
+ the owning `ViewModel`) instead of trapping it inside a leaf composable
53
+ when a sibling or the caller also needs to read or drive it; a leaf
54
+ composable with no external need for its state can still own it locally
55
+ via `remember`.
56
+ - Use `remember { ... }` for state that should survive recomposition but
57
+ not process death or configuration change (a derived value, an
58
+ `Animatable`, a coroutine-backed helper); use `rememberSaveable { ... }`
59
+ when the value must survive activity recreation (rotation, process
60
+ death) — typically UI-only state a `ViewModel` does not already own.
61
+ - Never call a suspend function, fire a one-off event (navigation,
62
+ `Toast`, analytics), or start a coroutine directly in a composable's
63
+ body — route it through `LaunchedEffect(key1, ...) { }` (re-runs when a
64
+ key changes) or `DisposableEffect(key) { onDispose { ... } }` (needs
65
+ cleanup), never as a bare statement that runs on every recomposition.
66
+ - Under strong skipping mode (default since Kotlin 2.0.20), the compiler
67
+ auto-memoizes a lambda declared inside a composable by wrapping it in an
68
+ implicit `remember`, keyed on its captures — a lambda passed inline to a
69
+ hot list item no longer needs manual hoisting for identity stability in
70
+ the common case. Reach for `@DontMemoize` only to opt a specific lambda
71
+ out (e.g. one that must run fresh every recomposition), and still hoist
72
+ a lambda explicitly when it closes over a value from an outer scope that
73
+ changes on a schedule the auto-memoization's capture-based keying
74
+ wouldn't track correctly.
75
+
76
+ ## State exposure from a ViewModel
77
+
78
+ - Expose state from a `ViewModel` as a read-only `StateFlow`/`SharedFlow`
79
+ backed by a private mutable holder — `private val _state =
80
+ MutableStateFlow(...)` with `val state = _state.asStateFlow()` — never
81
+ the mutable type itself, so only the `ViewModel` can drive state
82
+ changes and the UI layer can only observe.
83
+ - Prefer `StateFlow` for "current value" UI state (a screen's latest
84
+ render state, always has a value) and `SharedFlow`/`Channel` for one-off
85
+ events (a snackbar, a navigation trigger) that should not replay a
86
+ stale value to a newly-subscribed collector.
87
+ - `LiveData` is legacy for new Compose/coroutine-first code: prefer
88
+ `StateFlow`/`SharedFlow` exposed from the `ViewModel` and collected with
89
+ `collectAsStateWithLifecycle()` in Compose. Keep `LiveData` only where a
90
+ project has already standardized on it or where an unconverted
91
+ `View`-based screen still consumes it.
92
+ - Collect a `ViewModel`'s flow in Compose with
93
+ `collectAsStateWithLifecycle()`, not a bare `collectAsState()` — the
94
+ lifecycle-aware variant stops collecting while the screen is stopped
95
+ instead of continuing to observe (and potentially update state) behind
96
+ a backgrounded UI.
@@ -0,0 +1,90 @@
1
+ ---
2
+ extends: common
3
+ paths: ["**/*.kt", "**/*.kts"]
4
+ metadata:
5
+ origin: authored
6
+ ---
7
+
8
+ # Kotlin/Android security
9
+
10
+ Narrows `core-common-rules`' stack-agnostic security rules to Android-
11
+ specific, OWASP Mobile-relevant risks. Applies only to `*.kt`/`*.kts`
12
+ files.
13
+
14
+ ## Secrets and credential storage
15
+
16
+ - Never store a credential, auth token, or API secret in plain
17
+ `SharedPreferences` — plain `SharedPreferences` is an unencrypted XML
18
+ file readable on a rooted device or via backup extraction. Prefer the
19
+ Android Keystore directly (generate/store the key in the Keystore, use
20
+ it to encrypt the value yourself) over `EncryptedSharedPreferences`
21
+ (Jetpack `androidx.security:security-crypto`) for new code — that
22
+ library has known, unresolved reliability issues (a corrupted-keyset
23
+ failure mode with no clean recovery path) and Google's own guidance has
24
+ moved toward not recommending it for new applications; verify the
25
+ library's current status in the Android docs before reaching for it in
26
+ an existing codebase that already depends on it.
27
+ - Never hard-code an API key, client secret, or signing credential as a
28
+ Kotlin string literal or a `BuildConfig` field, whether the value is
29
+ committed or injected at build time from a local, untracked properties
30
+ file (`local.properties`) or a CI secret — either way, a `BuildConfig`
31
+ field is compiled into the APK exactly like a string literal and is
32
+ just as recoverable by decompiling it. Keeping the value out of source
33
+ control (via `local.properties`/a CI secret) only stops it leaking
34
+ through the repository; it does not keep it off the device. For a
35
+ value that must actually stay off the device, fetch it from a backend
36
+ at runtime instead of baking it into the build at all.
37
+ - Generate and store cryptographic keys through `AndroidKeyStore`
38
+ (`KeyGenParameterSpec`) rather than deriving or hard-coding a key in
39
+ application code — the Keystore keeps key material out of the app's
40
+ own process memory and out of extractable storage.
41
+
42
+ ## Manifest and component exposure
43
+
44
+ - Every `<activity>`, `<service>`, `<receiver>`, and `<provider>` in
45
+ `AndroidManifest.xml` that does not need to be reachable from other
46
+ apps should have `android:exported="false"` (or no `exported`
47
+ attribute with no intent-filter, matching the modern default); a
48
+ component left `exported="true"` with no permission check is callable
49
+ by any app on the device, including one crafted to pass malformed
50
+ input.
51
+ - An exported component that must stay exported (a deep-link entry point,
52
+ a share target) validates every piece of the incoming `Intent` before
53
+ acting on it — never trusts an extra, a URI path segment, or a
54
+ `ContentProvider` query purely because the intent-filter matched.
55
+ - Guard a `ContentProvider` and any exported component with a
56
+ signature-level custom permission when the caller is meant to be your
57
+ own other app, not "any app that asks."
58
+
59
+ ## Secure intent and deep-link handling
60
+
61
+ - Treat every value pulled from an incoming `Intent` (extras, deep-link
62
+ URI segments/query params, a `ClipData` from a share) as untrusted
63
+ input — validate its shape and range before using it to navigate, query
64
+ a database, or construct a file path; do not assume it matches what
65
+ your own app would have sent.
66
+ - Prefer an explicit `Intent` (naming the target component) for
67
+ inter-component communication within your own app; use an implicit
68
+ `Intent` only when you genuinely need another app to handle it, and
69
+ set the package (`setPackage`) when you only want your own app to
70
+ respond, to avoid intent interception by another installed app.
71
+ - Validate a deep-link's scheme/host/path against an allowlist before
72
+ using any part of it to load content or trigger an action —
73
+ `App Links`/verified `https` deep links reduce but do not eliminate
74
+ this: a query parameter or path segment can still carry attacker-
75
+ controlled data even through a verified link.
76
+
77
+ ## Data and transport
78
+
79
+ - Use `HttpsURLConnection`/an HTTP client configured to require TLS for
80
+ every network call carrying credentials or personal data; do not add a
81
+ custom `TrustManager`/`HostnameVerifier` that accepts all certificates
82
+ outside of a short-lived local debug build, and never ship one that
83
+ way.
84
+ - Mark a view/field that must not appear in screenshots or the recent-
85
+ apps app-switcher preview (a PIN entry, payment details) with
86
+ `FLAG_SECURE` on the window, rather than relying on the OS default.
87
+ - Avoid logging PII, tokens, or full request/response bodies with
88
+ `Log.d`/`Log.i` in release builds — strip or gate verbose logging
89
+ behind a debug-only build flag so production logs (which can be pulled
90
+ from a device or bug report) do not leak sensitive data.
@@ -0,0 +1,89 @@
1
+ ---
2
+ extends: common
3
+ paths: ["**/*.kt", "**/*.kts"]
4
+ metadata:
5
+ origin: authored
6
+ ---
7
+
8
+ # Kotlin/Android testing
9
+
10
+ Narrows `core-common-rules`' stack-agnostic testing rules to Kotlin
11
+ coroutine testing, Compose UI testing, and idiomatic mocking on Android.
12
+ Applies only to `*.kt`/`*.kts` files.
13
+
14
+ ## Coroutine test dispatchers
15
+
16
+ - Test a `suspend` function or a `ViewModel`'s coroutine-driven behavior
17
+ with `kotlinx-coroutines-test`'s `runTest { }`, not a real `delay` or a
18
+ blocking sleep — `runTest` runs on a `TestScope` whose virtual clock
19
+ skips real delay time, so a function that internally waits still runs
20
+ the test instantly.
21
+ - Inject a `TestDispatcher` (`StandardTestDispatcher`/
22
+ `UnconfinedTestDispatcher`) in place of `Dispatchers.Main`/
23
+ `Dispatchers.IO` for the code under test rather than letting it resolve
24
+ the real platform dispatcher — set `Dispatchers.setMain(testDispatcher)`
25
+ in a `@Before` and `Dispatchers.resetMain()` in an `@After` (or a JUnit
26
+ rule that wraps this) so `viewModelScope`/`Dispatchers.Main` resolves to
27
+ the test dispatcher instead of throwing or running on a background
28
+ thread the test cannot observe.
29
+ - Prefer `StandardTestDispatcher` (queues work, you advance it explicitly
30
+ with `advanceUntilIdle()`/`runCurrent()`) when a test needs to assert an
31
+ intermediate state (e.g. a loading state before a result arrives); use
32
+ `UnconfinedTestDispatcher` when the test only cares about the end state
33
+ and wants dispatched work to run eagerly.
34
+ - Never synchronize a coroutine test with `Thread.sleep`/a fixed `delay`
35
+ in the test body waiting for background work to finish — that is
36
+ exactly what `runTest`'s virtual time and `advanceUntilIdle()` replace.
37
+
38
+ ## Compose UI testing
39
+
40
+ - Drive Compose UI assertions through a `ComposeTestRule`
41
+ (`createComposeRule()` for a standalone composable, or
42
+ `createAndroidComposeRule<Activity>()` when the test needs a hosting
43
+ Activity) — never reach into the underlying `View` tree.
44
+ - Find nodes with semantics-based finders
45
+ (`onNodeWithText`, `onNodeWithContentDescription`,
46
+ `onNodeWithTag(testTag)`) rather than a fragile structural/index-based
47
+ query; add an explicit `Modifier.testTag("...")` to a composable that
48
+ has no stable text/content-description to key off.
49
+ - Call `composeTestRule.waitUntil { ... }` (or
50
+ `composeTestRule.awaitIdle()`/`mainClock.advanceTimeUntil { }` under
51
+ manual clock control) to wait for an async UI update instead of a
52
+ fixed-duration sleep in the test.
53
+ - Assert both structure and behavior: a node's presence/text via a
54
+ semantics finder, and an interaction's effect (`performClick()` then
55
+ assert the resulting state/text), not just that a composable rendered
56
+ without throwing.
57
+
58
+ ## Mocking at the interface boundary
59
+
60
+ - Mock a `Repository`/`ApiService`/`DataSource` interface (or another
61
+ clearly-owned collaborator boundary) that the class under test depends
62
+ on — never reach inside the class under test to stub its own private
63
+ methods or fields; a test that mocks internals stops testing real
64
+ behavior and breaks on any internal refactor.
65
+ - Prefer MockK for Kotlin code under test — it understands Kotlin
66
+ constructs Mockito's Java-first API handles awkwardly (`final` classes
67
+ by default, coroutines via `coEvery`/`coVerify`, extension functions,
68
+ object mocks via `mockkObject`) — unless a project has already
69
+ standardized on Mockito with `mockito-kotlin`; match what the project
70
+ already uses rather than introducing a second mocking library.
71
+ - Use `coEvery { repo.fetch() } returns result` /
72
+ `coVerify { repo.fetch() }` (MockK's coroutine-aware variants) for a
73
+ suspend function on a mock, not the plain `every`/`verify` forms, which
74
+ do not handle a `suspend` member correctly.
75
+ - Fake network/database responses at the repository or data-source
76
+ interface (a fake or mocked implementation returning canned data), not
77
+ by hitting a real network or an on-device database from a unit test —
78
+ reserve a real backing store for an instrumented/integration test.
79
+
80
+ ## Layout and lifecycle
81
+
82
+ - Unit tests for a `ViewModel`/plain Kotlin class live under
83
+ `src/test/`; instrumented tests needing an Android context or a real
84
+ device/emulator (Compose UI tests, `Instrumentation`-backed tests) live
85
+ under `src/androidTest/` — match whichever the project's existing
86
+ layout uses, do not invent a third location.
87
+ - Use JUnit rules (`MainDispatcherRule` for coroutines,
88
+ `createComposeRule()` for Compose) to keep dispatcher/UI setup and
89
+ teardown out of every individual test method.
@@ -0,0 +1,150 @@
1
+ ---
2
+ name: compose-implementation
3
+ description: "Use when implementing or extending a feature in a Kotlin/Android app with Jetpack Compose -- composable state hoisting, coroutine scopes (viewModelScope/lifecycleScope), StateFlow exposure from a ViewModel, and recomposition-safe side effects."
4
+ triggers:
5
+ - "add this screen to our Android app with Compose"
6
+ - "hook up this ViewModel to the new composable"
7
+ - "wire a network call into this Android feature"
8
+ - "build a form screen with validation for the app"
9
+ - "expose loading/error state from the ViewModel to the UI"
10
+ - "add a button that launches a coroutine to save data"
11
+ - "implement this ticket in the mobile app's feature module"
12
+ metadata:
13
+ origin: authored
14
+ category: implement
15
+ version: "1.0.0"
16
+ compatible_harnesses: "claude,codex,cursor,zed,opencode"
17
+ license: "MIT"
18
+ ---
19
+
20
+ # Kotlin/Android (Jetpack Compose) implementation
21
+
22
+ Implement or extend a feature in a Kotlin/Android codebase built on
23
+ Jetpack Compose: composable structure, state hoisting, coroutine scope
24
+ choice, and state exposure from a `ViewModel`. Scoped to Kotlin/Android
25
+ specifically — `rules/coding-style.mdc`, `rules/patterns.mdc`, and
26
+ `rules/security.mdc` carry the full stack-specific rule set this skill
27
+ draws its checklist from; read them before writing code, not just this
28
+ summary.
29
+
30
+ ## Workflow
31
+
32
+ ### Step 1: Discover the project's own conventions
33
+
34
+ 1. Read the module's `build.gradle(.kts)` for the Kotlin/AGP/Compose
35
+ Compiler versions and which architecture libraries are already
36
+ dependencies (`androidx.lifecycle:lifecycle-viewmodel-compose`,
37
+ `kotlinx-coroutines-android`, Hilt/Koin/Dagger, Compose Navigation).
38
+ 2. Find the existing layout: is this a single-module app or a
39
+ feature-module setup (`:feature:profile`, `:core:ui`)? Match it; do
40
+ not invent a different module boundary for one change.
41
+ 3. Read 1-2 neighboring screens/`ViewModel`s in the feature you are
42
+ touching for: state-holder naming (`UiState`, `ViewState`), whether
43
+ state is exposed as `StateFlow` or still `LiveData`, the DI pattern in
44
+ use, and whether a design-system component set already exists to
45
+ reuse instead of building raw `Text`/`Button` composables.
46
+
47
+ ### Step 2: Design before writing
48
+
49
+ - Decide, per new piece of state: does it belong in the `ViewModel`
50
+ (survives configuration change, drives business logic) or hoisted only
51
+ as far as the nearest composable ancestor that needs it (purely
52
+ presentational, e.g. whether a dropdown is expanded)? Do not default
53
+ everything into the `ViewModel` when a leaf composable's own `remember`
54
+ is enough, and do not trap logic-relevant state in a composable when a
55
+ sibling or rotation needs it to survive.
56
+ - Model the screen's state as a `sealed interface`/`data class` (a single
57
+ `UiState` with a `sealed` status field, or a small sealed hierarchy for
58
+ distinct screens like `Loading`/`Content`/`Error`) rather than several
59
+ independent nullable/boolean fields the UI has to reconcile by hand.
60
+ - Decide the coroutine scope for any new asynchronous work before writing
61
+ it: `viewModelScope` for anything driven from a `ViewModel`,
62
+ `lifecycleScope`/`rememberCoroutineScope()` only for UI-only work with
63
+ no `ViewModel` involved (an animation trigger, a one-off scroll). Never
64
+ `GlobalScope`.
65
+ - Trace where any new side effect (navigation, a snackbar/analytics
66
+ event, a suspend call) needs to run from — inside a composable body it
67
+ belongs in `LaunchedEffect`/`DisposableEffect`, never as a bare
68
+ statement.
69
+
70
+ ### Step 3: Implement
71
+
72
+ 1. Expose new `ViewModel` state as `private val _state =
73
+ MutableStateFlow(...)` / `val state = _state.asStateFlow()`; collect
74
+ it in Compose with `collectAsStateWithLifecycle()`.
75
+ 2. Keep composable parameters stable (primitives, `data class`es of
76
+ stable members, immutable collections) — strong skipping mode (default
77
+ since Kotlin 2.0.20) lets the compiler skip even an unstable parameter
78
+ via instance-identity comparison, but only a genuinely stable/immutable
79
+ type can compare EQUAL across calls and actually skip when nothing
80
+ changed; an unstable type rebuilt fresh each call still recomposes.
81
+ 3. Hoist state per `rules/patterns.mdc`; use `remember`/`rememberSaveable`
82
+ for composable-local state, never a `var` mutated directly inside the
83
+ composable body outside of a `remember` holder.
84
+ 4. Route any new side effect through `LaunchedEffect(key1, ...) { }` (with
85
+ the correct key so it re-runs exactly when it should) or
86
+ `DisposableEffect(key) { onDispose { ... } }` when cleanup is needed.
87
+ 5. Launch new coroutine work in `viewModelScope`/`lifecycleScope` as
88
+ decided in Step 2; wrap concurrent suspend calls in `coroutineScope { }`
89
+ with `async`/`await` rather than firing untracked `launch` calls.
90
+ 6. Avoid `!!`; use `?.`/`?:`/`requireNotNull(x) { "..." }` for any value
91
+ that can be null (a nullable response field, an `Intent` extra, a
92
+ `savedStateHandle` lookup).
93
+
94
+ ### Step 4: Verify
95
+
96
+ ```bash
97
+ ./gradlew assembleDebug
98
+ ./gradlew testDebugUnitTest
99
+ ./gradlew lint
100
+ ```
101
+
102
+ Run `./gradlew ktlintCheck` or `./gradlew detekt` if the project has one
103
+ configured. Fix findings at the root cause per `rules/security.mdc` and
104
+ `rules/coding-style.mdc`; a build/test/lint failure here is a signal to
105
+ fix the implementation, not to reach for `kotlin-android-build-fix`'s
106
+ scope unless the failure is purely a build/dependency/toolchain problem
107
+ unrelated to the feature logic.
108
+
109
+ ### Step 5: Report
110
+
111
+ ```
112
+ Implemented: feature/profile/ProfileViewModel.kt, feature/profile/ProfileScreen.kt
113
+ - New ProfileUiState sealed hierarchy (Loading/Content/Error)
114
+ - State exposed as StateFlow, collected with collectAsStateWithLifecycle()
115
+ - Save action launched in viewModelScope
116
+ - ./gradlew assembleDebug/testDebugUnitTest/lint all pass
117
+ ```
118
+
119
+ ## Rules
120
+
121
+ - Never launch a coroutine with `GlobalScope` — use `viewModelScope`,
122
+ `lifecycleScope`, or a scope passed in by the caller.
123
+ - Never call a suspend function or fire a one-off UI event directly in a
124
+ composable's body — use `LaunchedEffect`/`DisposableEffect`.
125
+ - Never expose a mutable `MutableStateFlow`/`MutableLiveData` from a
126
+ `ViewModel`'s public surface — expose the read-only view.
127
+ - Never use `!!` to route around a null-safety warning; use a safe call,
128
+ Elvis operator, or `requireNotNull` with a message.
129
+
130
+ ## Red Flags
131
+
132
+ | Rationalization | Why it is wrong |
133
+ |---|---|
134
+ | "I'll just launch this in GlobalScope, it's a quick one-off network call" | Nothing cancels a GlobalScope coroutine when the screen/ViewModel is destroyed; it can still update state or navigate on a gone screen |
135
+ | "I'll call this suspend function right in the composable body, it only runs once" | A composable body can re-run on every recomposition; an uncontrolled suspend call outside LaunchedEffect can fire far more than once, or not track the right restart key |
136
+ | "I'll add `!!` here since this value is always set by the time we get here" | "Always" is an assumption the compiler cannot verify; a safe call or `requireNotNull` with a message documents and enforces the same assumption instead of crashing silently when it's wrong |
137
+ | "I'll just expose the MutableStateFlow directly, it saves a line" | Lets any collector outside the ViewModel push a new value, breaking the single-writer invariant the pattern exists to guarantee |
138
+
139
+ ## Verification
140
+
141
+ Do not report the work done until all of the following hold:
142
+
143
+ - `./gradlew assembleDebug`, `./gradlew testDebugUnitTest`, and
144
+ `./gradlew lint` all exit 0.
145
+ - Every new coroutine launch uses `viewModelScope`/`lifecycleScope`/a
146
+ passed-in scope, never `GlobalScope`.
147
+ - Every new side effect inside a composable body runs through
148
+ `LaunchedEffect`/`DisposableEffect`, not as a bare statement.
149
+ - Every new/touched `ViewModel`-exposed state is read-only from the UI's
150
+ perspective (`StateFlow`/`SharedFlow`, not the mutable type).