@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,77 @@
1
+ {
2
+ "triggers": {
3
+ "positive": [
4
+ "I need to add a save button on this screen that persists a form to the backend",
5
+ "Add an expandable settings section with a toggle to this app screen",
6
+ "Hook up loading and error states from the repository call to the UI",
7
+ "Build a new profile edit screen that reads from and writes to our data layer",
8
+ "This screen needs a snackbar to show up once after a background save finishes",
9
+ "Add pagination to this list screen backed by a paging data source",
10
+ "Wire a search field on this screen to filter a list as the user types"
11
+ ],
12
+ "negative": [
13
+ "Implement this feature with a SwiftUI view backed by @State and @Published",
14
+ "Add a StatefulWidget in Flutter that manages this form's local state",
15
+ "Implement this ASP.NET Core minimal API endpoint for saving a record",
16
+ "Add a new React component with useState and useEffect for this form",
17
+ "Write a Django view that handles this form submission",
18
+ "Fix this failing Gradle build after bumping the Compose Compiler version",
19
+ "Review this Kotlin diff for goroutine-style concurrency leaks before merge"
20
+ ]
21
+ },
22
+ "scenarios": [
23
+ {
24
+ "id": "state-hoisting-scope",
25
+ "prompt": "On a settings screen I'm building, there's a card with an arrow icon that expands to reveal a couple of extra options underneath. Nothing else on the screen needs to know whether it's expanded. Where should that expanded/collapsed flag live?",
26
+ "strictness": "high",
27
+ "expected_behavior": [
28
+ {
29
+ "grader": "judge",
30
+ "rubric": "A correct answer keeps the expanded/collapsed flag as local composable state, created with `remember { mutableStateOf(false) } ` inside the card composable itself, rather than pushing it up into a ViewModel or a parent screen's own state holder -- since nothing else needs to read or drive it, hoisting it further only adds indirection with no benefit. It should not leave the flag as a bare `var` with no `remember`, since that value would be lost on every recomposition.",
31
+ "pass_criteria": [
32
+ "States the flag should be local to the card composable via `remember` (or `rememberSaveable` if the answer wants it to survive rotation), not pushed into the ViewModel.",
33
+ "Explains that hoisting further (to a parent screen or the ViewModel) is unnecessary here specifically because nothing else needs to read or change the value.",
34
+ "Shows or names the concrete construct (`remember { mutableStateOf(false) }`), not just 'keep it local'."
35
+ ],
36
+ "fail_criteria": [
37
+ "Recommends putting the expanded/collapsed flag into the ViewModel or a shared/hoisted state holder even though nothing else needs it.",
38
+ "Describes storing the flag as a bare `var` inside the composable with no `remember` wrapper, which would reset on every recomposition."
39
+ ]
40
+ }
41
+ ],
42
+ "calibration": {
43
+ "known_right": "Since nothing outside this card needs to read or drive that flag, keep it local: `var expanded by remember { mutableStateOf(false) }` right inside the card's own composable function, and flip it in the arrow icon's `onClick`. There's no reason to hoist it to a parent screen or push it into the ViewModel -- hoisting only earns its keep when a sibling composable or the caller actually needs to observe or control the value, and here nothing does. If you later want the card to stay expanded across a screen rotation, swap `remember` for `rememberSaveable`, but that's a separate decision from where the state lives.",
44
+ "known_wrong": "Put it in the ViewModel as part of the screen's UiState, something like `isSectionExpanded: Boolean` on the state data class, and toggle it through a `ViewModel` function the composable calls on click. That way everything about the screen's state lives in one place and is easier to find later, even though only this one card actually reads it right now.",
45
+ "vague": "Keep the state close to where it's used and make sure it's remembered properly across recompositions.",
46
+ "subtle_wrong": "You can just declare `var expanded = false` at the top of the card's composable function and flip it in the click handler -- that's simpler than wiring up `remember`, and since it's just a UI toggle it doesn't need to go through the ViewModel."
47
+ }
48
+ },
49
+ {
50
+ "id": "viewmodel-save-coroutine-scope",
51
+ "prompt": "I'm wiring up a save button in this ViewModel. Clicking it should call a suspend function on the repository to persist some data. How should I launch that call?",
52
+ "strictness": "high",
53
+ "expected_behavior": [
54
+ { "grader": "regex", "value": "viewModelScope" },
55
+ {
56
+ "grader": "judge",
57
+ "rubric": "A correct answer launches the repository save call inside `viewModelScope.launch { ... }` from the ViewModel, not `GlobalScope.launch`, because viewModelScope is cancelled automatically when the ViewModel is cleared, while a GlobalScope coroutine keeps running (and can still mutate state) after the screen and ViewModel are gone.",
58
+ "pass_criteria": [
59
+ "Names `viewModelScope.launch` (or `viewModelScope.launch { }` with the repository call inside it) as the concrete launch mechanism.",
60
+ "Explains why: viewModelScope ties the coroutine's lifetime to the ViewModel, cancelling it when the ViewModel is cleared, unlike an unscoped or globally-scoped coroutine."
61
+ ],
62
+ "fail_criteria": [
63
+ "Recommends `GlobalScope.launch` (or an equivalent unstructured, unscoped coroutine) for the save call. Mentioning GlobalScope only to explain why it should be avoided here is not a failure.",
64
+ "Recommends launching the coroutine with no scope tied to the ViewModel's lifecycle at all, or launching it from a Composable's `rememberCoroutineScope()` instead of the ViewModel that owns the save action."
65
+ ]
66
+ }
67
+ ],
68
+ "calibration": {
69
+ "known_right": "Launch it inside `viewModelScope.launch { repository.save(data) }` right in the ViewModel function the button click calls. `viewModelScope` is already tied to this ViewModel's lifecycle -- Jetpack's `ViewModel` cancels it automatically in `onCleared()`, so if the user navigates away mid-save the coroutine gets cancelled along with everything else, instead of continuing to run against a repository call whose result no screen is left to observe. Don't reach for `GlobalScope.launch` here: nothing would cancel it when the ViewModel goes away, so it can keep running -- and potentially push a state update or trigger a side effect -- well after the screen that started it is gone.",
70
+ "known_wrong": "Just fire it with `GlobalScope.launch { repository.save(data) }` inside the click handler -- it's a simple save call, and using GlobalScope means you don't have to worry about scope lifetimes at all, it'll just run in the background regardless of what the ViewModel or screen is doing.",
71
+ "vague": "Launch the save call in a coroutine tied to the ViewModel's own lifecycle so it doesn't leak.",
72
+ "subtle_wrong": "Since this ViewModel already exposes a `CoroutineScope` field you constructed yourself with `CoroutineScope(Dispatchers.IO)` in its init block, just launch the save call on that: `scope.launch { repository.save(data) }`. It keeps the save logic off viewModelScope so a slow save doesn't interact with anything else the ViewModel launches on viewModelScope."
73
+ },
74
+ "anti_patterns": ["GlobalScope"]
75
+ }
76
+ ]
77
+ }
@@ -0,0 +1,151 @@
1
+ ---
2
+ name: kotlin-android-build-fix
3
+ description: "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."
4
+ triggers:
5
+ - "the Android build is failing"
6
+ - "gradle sync is broken after this change"
7
+ - "detekt is failing on this module"
8
+ - "lint is flagging this Android change"
9
+ - "this Kotlin file won't compile"
10
+ - "resolve this dependency conflict in build.gradle"
11
+ metadata:
12
+ origin: authored
13
+ category: build-fix
14
+ version: "1.0.0"
15
+ compatible_harnesses: "claude,codex,cursor,zed,opencode"
16
+ license: "MIT"
17
+ ---
18
+
19
+ # Kotlin/Android build fix
20
+
21
+ Resolve a Gradle/Android build failure: a Kotlin compile error, a
22
+ Gradle/AGP/Kotlin/Compose-Compiler version mismatch, an unresolved
23
+ dependency, or a `ktlint`/`detekt`/`lint` failure — with the smallest
24
+ change that fixes the actual root cause. `rules/coding-style.mdc` and
25
+ `rules/security.mdc` govern what a "correct" fix looks like; this skill
26
+ never reaches for a suppression instead of a fix.
27
+
28
+ ## Workflow
29
+
30
+ ### Step 1: Reproduce and classify
31
+
32
+ ```bash
33
+ ./gradlew assembleDebug
34
+ ./gradlew lint
35
+ ```
36
+
37
+ Run the project's configured static-analysis task if present
38
+ (`./gradlew ktlintCheck` or `./gradlew detekt`, checked via a
39
+ `.editorconfig`/`ktlint`/`detekt.yml` config file). Read the exact error
40
+ text and classify it:
41
+
42
+ - **Compile error** (unresolved reference, type mismatch, wrong argument
43
+ count, an unhandled `when` branch on a sealed type).
44
+ - **Version/toolchain mismatch** (Kotlin/AGP/Gradle/Compose-Compiler
45
+ versions incompatible with each other, or with the project's declared
46
+ `compileSdk`/`minSdk`).
47
+ - **Dependency resolution failure** (a missing/conflicting artifact
48
+ version in `build.gradle(.kts)`, a version-catalog entry that doesn't
49
+ resolve).
50
+ - **Lint finding** (`./gradlew lint`'s own Android Lint checks).
51
+ - **ktlint/detekt finding** (a style or static-analysis rule violation).
52
+
53
+ ### Step 2: Fix by category
54
+
55
+ **Version/toolchain mismatch:** since Kotlin 2.0, the Compose Compiler ships
56
+ via the `org.jetbrains.kotlin.plugin.compose` Gradle plugin versioned
57
+ IDENTICALLY to the Kotlin version itself (`version.ref = "kotlin"` in the
58
+ version catalog) — there is no separate compiler-to-Kotlin compatibility
59
+ matrix to consult on a current project; a mismatch here usually means the
60
+ plugin's declared version drifted from the Kotlin version, or (pre-2.0
61
+ project) an old `composeOptions { kotlinCompilerExtensionVersion }`
62
+ declaration left over from before the plugin migration. Align the plugin
63
+ version to match Kotlin exactly, or migrate off `kotlinCompilerExtensionVersion`
64
+ to the plugin. Only bump `compileSdk`/`minSdk` when the failure actually
65
+ requires the newer API, and say so in the report.
66
+
67
+ **Dependency resolution failure:** run `./gradlew :module:dependencies
68
+ --configuration <config>` to see the actual resolved tree before pinning
69
+ a version by hand; prefer aligning versions through the project's version
70
+ catalog (`libs.versions.toml`) over an ad hoc `resolutionStrategy` force,
71
+ unless the conflict is a genuine, documented incompatibility that needs
72
+ one.
73
+
74
+ **Lint finding:** fix the underlying issue the finding names (a real
75
+ resource/API-level issue, a missing content description, an exported
76
+ component with no permission). Never add a blanket `lint {
77
+ abortOnError = false }`/`disable` entry (the current Android Gradle Plugin
78
+ DSL block is `lint { }`; the older `lintOptions { }` block is deprecated)
79
+ or a file-level `@Suppress` whose only purpose is to make the check stop
80
+ complaining without addressing what it found; a narrowly-scoped, justified
81
+ suppression at the single call site (with a comment explaining why) is the
82
+ last resort, not the first move.
83
+
84
+ **ktlint/detekt finding:** fix the actual style/complexity issue the rule
85
+ names. Never disable the rule project-wide in `.editorconfig`/
86
+ `detekt.yml` to silence one occurrence without discussing it in the
87
+ report.
88
+
89
+ **Compile error (sealed `when` not exhaustive):** add the missing branch
90
+ handling the new/overlooked case — never an `else -> {}` branch that
91
+ silently swallows a case a sealed hierarchy was specifically designed to
92
+ force you to handle.
93
+
94
+ ### Step 3: Verify
95
+
96
+ ```bash
97
+ ./gradlew assembleDebug
98
+ ./gradlew testDebugUnitTest
99
+ ./gradlew lint
100
+ ```
101
+
102
+ Re-run the project's `ktlintCheck`/`detekt` task if it was part of the
103
+ original failure. All must exit 0 before reporting done.
104
+
105
+ ### Step 4: Report
106
+
107
+ ```
108
+ Fixed: Compose Compiler plugin version mismatch in gradle/libs.versions.toml
109
+ - Root cause: org.jetbrains.kotlin.plugin.compose left pinned to the old
110
+ Kotlin version after a Kotlin bump (the plugin must track Kotlin exactly
111
+ since Kotlin 2.0, not a separate compatibility pairing)
112
+ - Set the plugin's version.ref to the same catalog entry as kotlin
113
+ - ./gradlew assembleDebug/testDebugUnitTest/lint all pass
114
+ ```
115
+
116
+ State the root cause in one sentence, not just "build now passes."
117
+
118
+ ## Rules
119
+
120
+ - Find and fix the smallest change that addresses the actual root cause —
121
+ never widen a fix beyond what the failure requires.
122
+ - NEVER add a blanket `@Suppress`, `lint { abortOnError = false }`,
123
+ or a project-wide `detekt`/`ktlint` rule disable to silence a finding
124
+ instead of fixing what it found.
125
+ - NEVER bump `compileSdk`, `minSdk`, or a major Gradle/Kotlin/AGP version
126
+ just to make an error disappear without understanding why it changed.
127
+ - NEVER replace a non-exhaustive sealed `when`'s missing branch with a
128
+ catch-all `else` that discards the case it exists to force you to
129
+ handle.
130
+ - NEVER delete or skip a failing test to reach a green build.
131
+
132
+ ## Red Flags
133
+
134
+ | Rationalization | Why it is wrong |
135
+ |---|---|
136
+ | "I'll add `@Suppress(\"..\")` at the top of the file so detekt stops complaining" | Silences every finding of that type in the whole file instead of fixing the one the build actually flagged; fix the underlying issue at the specific site instead |
137
+ | "I'll bump compileSdk to the latest to make this dependency resolve" | Changes the module's target API surface for every consumer to dodge one dependency conflict; check the actual required version first |
138
+ | "I'll add an `else -> {}` branch so this `when` compiles" | Defeats the reason the state was modeled as sealed in the first place — a genuinely new case now silently does nothing instead of failing to compile |
139
+ | "This lint check is annoying, I'll just disable it in the module's lint block" | Turns off the check for every future file in the module, not just this one finding; fix the finding or scope a suppression narrowly with a reason |
140
+
141
+ ## Verification
142
+
143
+ Do not report the fix done until all of the following hold:
144
+
145
+ - `./gradlew assembleDebug`, `./gradlew testDebugUnitTest`, and
146
+ `./gradlew lint` all exit 0.
147
+ - The project's `ktlintCheck`/`detekt` task (if configured) exits 0.
148
+ - The change is the smallest one that addresses the stated root cause —
149
+ no unrelated files touched.
150
+ - The report states the root cause in one sentence, not just "build now
151
+ passes."
@@ -0,0 +1,76 @@
1
+ {
2
+ "triggers": {
3
+ "positive": [
4
+ "Gradle sync is red after I touched this module's build file",
5
+ "The build broke right after bumping a library version",
6
+ "assembleDebug is failing and I can't tell why",
7
+ "Something in this module won't compile since the merge",
8
+ "CI is red on the Android job, can you sort it out",
9
+ "This module's static analysis task is blocking the build",
10
+ "The app won't build after pulling the latest changes"
11
+ ],
12
+ "negative": [
13
+ "Xcode build is failing after this Swift package bump",
14
+ "Flutter's pub get is failing after this dependency change",
15
+ "dotnet build is failing after this NuGet package update",
16
+ "npm run build is failing after this dependency bump",
17
+ "pip install is failing after this requirements.txt change",
18
+ "Implement a new screen for the checkout flow",
19
+ "Review this diff for goroutine-style leaks before merging"
20
+ ]
21
+ },
22
+ "scenarios": [
23
+ {
24
+ "id": "non-exhaustive-sealed-when",
25
+ "prompt": "After adding a new `ProfileUiState.RateLimited` variant to our sealed interface, the `when (state)` in ProfileScreen.kt no longer compiles because it's not exhaustive. What's the right way to fix it?",
26
+ "strictness": "high",
27
+ "expected_behavior": [
28
+ {
29
+ "grader": "judge",
30
+ "rubric": "A correct fix adds a real branch to the when block that handles the new RateLimited state with its own appropriate UI/behavior, keeping the when exhaustive by design; it does not add a catch-all else branch that silently does nothing or reuses an unrelated existing branch, since that would defeat the reason the state is modeled as a sealed hierarchy in the first place -- to force every case to be handled.",
31
+ "pass_criteria": [
32
+ "States that a dedicated branch for RateLimited should be added to the when, not a generic else/catch-all.",
33
+ "Names or sketches what that branch should actually do (show a rate-limit specific message/UI, or otherwise something specific to that state), not just 'handle it'.",
34
+ "Explains why an else/catch-all branch would be the wrong fix here: it would silently swallow this case (and any future new case) instead of forcing it to be handled."
35
+ ],
36
+ "fail_criteria": [
37
+ "Recommends adding an `else -> {}` (or equivalent catch-all) branch to make the when compile without giving RateLimited its own real handling.",
38
+ "Recommends reverting or removing the new RateLimited variant just to make the existing when compile again."
39
+ ]
40
+ }
41
+ ],
42
+ "calibration": {
43
+ "known_right": "Add a real `is ProfileUiState.RateLimited ->` branch to the `when (state)` in `ProfileScreen.kt` that renders whatever this state should actually show -- most likely a message telling the user to try again later, similar in shape to how the `Error` branch is handled but with rate-limit-specific copy. Don't reach for `else -> {}` to make the compiler stop complaining: the whole point of modeling `ProfileUiState` as a sealed interface is that adding a new variant forces every `when` over it to be updated, and an `else` branch throws that guarantee away -- the next new state added later would silently fall into the same catch-all with no one noticing.",
44
+ "known_wrong": "Easiest fix: add `else -> {}` at the bottom of the `when (state)` block. It makes the compiler happy immediately and you can come back and add real handling for `RateLimited` later once there's time to design the UI for it.",
45
+ "vague": "Add a branch for the new state so the when compiles properly.",
46
+ "subtle_wrong": "Add `is ProfileUiState.RateLimited -> { /* reuse existing error UI for now */ }` that just falls through to whatever the `Error` branch already renders, without adding a distinct case -- that satisfies the exhaustiveness check without having to design new UI right now, and you can split it out into its own real branch in a follow-up."
47
+ }
48
+ },
49
+ {
50
+ "id": "detekt-suppress-vs-root-cause",
51
+ "prompt": "detekt is failing on a new `ProfileRepository.kt` file with a `TooGenericExceptionCaught` finding on a broad `catch (e: Exception)` block that just logs and returns a default value. How should this get fixed?",
52
+ "strictness": "high",
53
+ "expected_behavior": [
54
+ {
55
+ "grader": "judge",
56
+ "rubric": "A correct fix narrows the catch to the specific exception type(s) the call can actually throw (or a small, deliberate set of them) instead of catching the broad Exception type, so the underlying issue detekt is flagging -- a catch clause wide enough to also swallow unrelated bugs -- is actually addressed; it does not add a blanket @Suppress(\"TooGenericExceptionCaught\") annotation or disable the rule in detekt.yml to make the finding disappear without changing the catch itself.",
57
+ "pass_criteria": [
58
+ "States that the catch clause should be narrowed to the specific exception type(s) the call can actually throw, not left as a broad catch (e: Exception).",
59
+ "Explicitly rejects silencing the finding via @Suppress or a detekt.yml rule disable as the fix.",
60
+ "The fix addresses the actual catch block detekt flagged, not an unrelated part of the file."
61
+ ],
62
+ "fail_criteria": [
63
+ "Recommends adding @Suppress(\"TooGenericExceptionCaught\") to the function or catch block to make the finding disappear.",
64
+ "Recommends disabling the TooGenericExceptionCaught rule in detekt.yml (project-wide or for this file) instead of narrowing the catch."
65
+ ]
66
+ }
67
+ ],
68
+ "calibration": {
69
+ "known_right": "Narrow the `catch (e: Exception)` to whatever this call can actually throw -- for a repository call that's usually something like `IOException`/`HttpException` from the network client, possibly alongside a serialization exception if parsing happens in the same block. Kotlin has no multi-catch syntax (unlike Java's `catch (IOException | HttpException e)`), so catch each specific type in its own `catch` block, or catch a real common supertype the thrown types actually share -- not `Exception` as a stand-in for one. That way a genuine, unrelated bug elsewhere in that block (a `NullPointerException` from a real logic error) doesn't get silently swallowed and turned into 'just return the default' along with the network failures you actually meant to handle. Don't add `@Suppress(\"TooGenericExceptionCaught\")` here -- that keeps the broad catch exactly as risky as detekt flagged it, just with the warning turned off.",
70
+ "known_wrong": "Add `@Suppress(\"TooGenericExceptionCaught\")` right above the function -- the broad catch is intentional here since we want to return a default value no matter what goes wrong, and the annotation tells detekt that's a deliberate choice.",
71
+ "vague": "Tighten up the exception handling so it's not catching more than it should.",
72
+ "subtle_wrong": "Leave the broad `catch (e: Exception)` as is, but add a `detekt.yml` exclusion for this one file under the `TooGenericExceptionCaught` rule's `excludes` list -- that keeps the rule active for the rest of the module while acknowledging this specific file's catch is intentionally broad."
73
+ }
74
+ }
75
+ ]
76
+ }
@@ -0,0 +1,139 @@
1
+ ---
2
+ name: kotlin-android-code-review
3
+ description: "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."
4
+ triggers:
5
+ - "review this Android diff for coroutine leaks"
6
+ - "check this Compose change for recomposition bugs"
7
+ - "any null-safety issues in this Kotlin PR"
8
+ - "review this ViewModel change for state leaks"
9
+ - "check the manifest changes in this Android diff"
10
+ - "review this mobile feature branch before merge"
11
+ metadata:
12
+ origin: authored
13
+ category: review
14
+ version: "1.0.0"
15
+ compatible_harnesses: "claude,codex,cursor,zed,opencode"
16
+ license: "MIT"
17
+ ---
18
+
19
+ # Kotlin/Android code review
20
+
21
+ Read-only review of a Kotlin/Android change for coroutine, Compose, and
22
+ null-safety risks specific to this stack: unstructured coroutine scopes,
23
+ side effects fired outside `LaunchedEffect`/`DisposableEffect`, forced
24
+ null-unwraps on external data, leaked mutable state, and manifest
25
+ exposure. This skill never edits code — it reports findings.
26
+ `rules/coding-style.mdc`, `rules/patterns.mdc`, and `rules/security.mdc`
27
+ are the rule set findings are checked against.
28
+
29
+ ## Workflow
30
+
31
+ ### Step 1: Scope the review
32
+
33
+ 1. Identify the changed files (`git diff` against the review base) —
34
+ review only `*.kt`/`*.kts` files (and `AndroidManifest.xml` when
35
+ touched) in the diff, not the whole repository.
36
+ 2. Read enough of the surrounding, unchanged code to know whether a
37
+ flagged pattern is new in this diff or pre-existing; note pre-existing
38
+ issues separately from ones the diff introduces.
39
+
40
+ ### Step 2: Check each changed file against the focus list
41
+
42
+ **Coroutines**
43
+ - Any `GlobalScope.launch`/`GlobalScope.async` in the diff — flag it;
44
+ the fix direction is `viewModelScope`/`lifecycleScope`/a passed-in
45
+ scope.
46
+ - A new coroutine (`launch`/`async`) with no scope tied to a lifecycle
47
+ owner, or a fire-and-forget `launch` whose result/exception is silently
48
+ dropped.
49
+ - A suspend call awaited with no cancellation awareness inside a loop
50
+ that should stop early (missing `ensureActive()`/`isActive` check in a
51
+ long-running or CPU-bound loop).
52
+
53
+ **Compose**
54
+ - A suspend call, navigation event, `Toast`, or analytics call written
55
+ directly in a composable's body instead of inside `LaunchedEffect`/
56
+ `DisposableEffect` — flag it; it can re-fire on every recomposition or
57
+ run at the wrong time.
58
+ - A composable parameter of an unstable type (a plain mutable `List`, a
59
+ class with public `var`s and no `@Stable`/`@Immutable`) in a
60
+ performance-sensitive position (a `LazyColumn` item, a frequently
61
+ recomposing parent) — under the strong skipping mode default since
62
+ Kotlin 2.0.20 this no longer defeats skipping outright (the compiler
63
+ falls back to instance-identity comparison), but a NEW instance built on
64
+ every call (a `List` rebuilt each recomposition) still can't compare
65
+ equal and skip; flag it as a likely unnecessary-recomposition risk, not
66
+ a hard skip defeat.
67
+ - State read/written with a bare `var` inside a composable body with no
68
+ `remember`/`rememberSaveable` wrapper — flag it; the value will not
69
+ survive recomposition as intended.
70
+
71
+ **Null safety**
72
+ - `!!` on a value sourced from outside this function's own prior checks —
73
+ a network response field, an `Intent` extra, a `savedStateHandle`
74
+ lookup, a `find`/`firstOrNull` result — flag it; the fix direction is a
75
+ safe call, Elvis operator, or `requireNotNull` with a message.
76
+
77
+ **State exposure**
78
+ - A `ViewModel` exposing a `MutableStateFlow`/`MutableLiveData` (not the
79
+ read-only view) on its public surface — flag it; any collector outside
80
+ the `ViewModel` can then push a new value.
81
+
82
+ **Manifest and security**
83
+ - A new or changed `<activity>`/`<service>`/`<receiver>`/`<provider>`
84
+ entry with `android:exported="true"` and no permission attribute — flag
85
+ it, and check whether the component actually needs to be reachable
86
+ from other apps.
87
+ - A new credential/token written to plain `SharedPreferences` — flag it;
88
+ the fix direction is the Android Keystore directly, not
89
+ `EncryptedSharedPreferences` (known reliability issues, no longer
90
+ recommended for new code per `rules/security.mdc`).
91
+
92
+ ### Step 3: Report
93
+
94
+ For each finding: file:line, the pattern, why it matters (leak, crash,
95
+ recomposition bug, data exposure), and the fix direction — but do not
96
+ apply it.
97
+
98
+ ```
99
+ feature/profile/ProfileViewModel.kt:28 — GlobalScope.launch used to save
100
+ the profile. Risk: outlives the ViewModel, can write state after the
101
+ screen is gone. Fix direction: replace with viewModelScope.launch.
102
+ ```
103
+
104
+ ## Rules
105
+
106
+ - NEVER edit code — findings and fix direction only.
107
+ - Flag GlobalScope usage, side effects outside LaunchedEffect/
108
+ DisposableEffect, unstable composable parameters, `!!` on external
109
+ data, leaked mutable ViewModel state, and unguarded exported manifest
110
+ components; do not report generic style nits already covered by
111
+ `ktlint`/`detekt`/`lint` (those are noise here).
112
+ - Distinguish a finding the diff introduces from a pre-existing one in
113
+ code the diff merely touches.
114
+ - When a suspected recomposition or leak issue is not certain from
115
+ reading alone, say so and suggest a way to confirm (a Compose layout
116
+ inspector trace, a `LeakCanary` run) rather than asserting it without
117
+ evidence.
118
+
119
+ ## Red Flags
120
+
121
+ | Rationalization | Why it is wrong |
122
+ |---|---|
123
+ | "This GlobalScope call is for a quick analytics ping, it's harmless" | Harmless intent doesn't change that nothing cancels it; flag it regardless of what the call does |
124
+ | "The `!!` here is fine, this field is always populated by the backend" | "Always" is an external contract, not a compiler-verified guarantee; report it as a finding, the API can still return the field as null once |
125
+ | "I'll just fix the exported manifest attribute myself, it's a one-line change" | This skill is read-only; report the finding and its fix direction, do not edit the file |
126
+ | "The composable parameter type is a List, that's fine for now" | A raw `List` is unstable, and strong skipping's instance-identity fallback still won't skip a NEW list instance built every call; flag it, especially in a list item or frequently-recomposing position |
127
+
128
+ ## Verification
129
+
130
+ Do not report the review done until all of the following hold:
131
+
132
+ - Every changed `*.kt`/`*.kts` file (and `AndroidManifest.xml`, if
133
+ touched) in the diff was read, not just files named in the PR
134
+ description.
135
+ - Every finding names a concrete file:line, the specific risk category
136
+ from Step 2, and a fix direction.
137
+ - No source file was modified by this review.
138
+ - Findings distinguish diff-introduced issues from pre-existing ones in
139
+ touched files.
@@ -0,0 +1,78 @@
1
+ {
2
+ "triggers": {
3
+ "positive": [
4
+ "Take a look at this diff before I open the PR for the mobile app",
5
+ "Anything risky in these ViewModel changes I should fix before merge",
6
+ "Can you spot problems in this screen's new composable before I push",
7
+ "Double-check this Kotlin diff doesn't introduce any leaks",
8
+ "Give this Android pull request a once-over for correctness",
9
+ "Flag anything sketchy in this profile screen's changes",
10
+ "Is this manifest change safe to ship"
11
+ ],
12
+ "negative": [
13
+ "Review this SwiftUI diff for retain cycles before merging",
14
+ "Check this Flutter pull request for setState misuse",
15
+ "Review this ASP.NET Core controller diff for missing authorization",
16
+ "Check this React diff for missing hook dependencies",
17
+ "Review this Python diff for unhandled exceptions",
18
+ "Write tests covering this screen's new error state",
19
+ "Fix the compile error in this ViewModel after the merge"
20
+ ]
21
+ },
22
+ "scenarios": [
23
+ {
24
+ "id": "globalscope-in-viewmodel-diff",
25
+ "prompt": "Here's a diff to a ProfileViewModel: it adds a new `refresh()` function whose body is `GlobalScope.launch { val data = repository.fetch(); _state.value = ProfileUiState.Content(data) }`, called from the screen's pull-to-refresh handler. What do you flag in review?",
26
+ "strictness": "high",
27
+ "expected_behavior": [
28
+ { "grader": "regex", "value": "viewModelScope" },
29
+ {
30
+ "grader": "judge",
31
+ "rubric": "A correct review flags the GlobalScope.launch in refresh() as the primary finding: it should be viewModelScope.launch instead, because GlobalScope is not cancelled when the ViewModel is cleared, so a slow fetch can still resolve and write to _state.value after the screen (and this ViewModel) are gone, and the finding names the actual risk (a completed background call writing state, or the exposed StateFlow's writer outliving its owner) rather than only restating that GlobalScope is discouraged.",
32
+ "pass_criteria": [
33
+ "Names the exact line/construct (GlobalScope.launch in refresh()) as the finding, not a generic 'watch your coroutines' comment.",
34
+ "States the concrete fix direction: replace GlobalScope.launch with viewModelScope.launch.",
35
+ "Explains the actual risk in this diff: an in-flight fetch can complete and write to _state.value after the ViewModel/screen is gone, since nothing cancels a GlobalScope coroutine when the ViewModel is cleared."
36
+ ],
37
+ "fail_criteria": [
38
+ "Does not flag the GlobalScope.launch call as a finding at all.",
39
+ "Flags it only as a vague style nit ('avoid GlobalScope, it's not idiomatic') with no explanation of the actual lifecycle/leak risk it causes in this specific diff."
40
+ ]
41
+ }
42
+ ],
43
+ "calibration": {
44
+ "known_right": "Flag `refresh()`'s use of `GlobalScope.launch` as the main issue in this diff: it should be `viewModelScope.launch` instead. As written, if the user pulls to refresh and then immediately backs out of the screen, `GlobalScope.launch` keeps running -- nothing about it is tied to this ViewModel's lifecycle -- so `repository.fetch()` can still complete and write to `_state.value` well after the ViewModel has been cleared and the screen is gone. `viewModelScope` is cancelled automatically in `onCleared()`, which is exactly the guarantee this refresh call needs. Fix direction: `viewModelScope.launch { val data = repository.fetch(); _state.value = ProfileUiState.Content(data) }`.",
45
+ "known_wrong": "This looks fine -- `refresh()` is a short, one-shot network call triggered by a manual pull-to-refresh gesture, so `GlobalScope.launch` here is low-risk; users don't typically leave a screen mid-refresh, and even if they do, updating a `StateFlow` no one is collecting anymore doesn't really cause a problem.",
46
+ "vague": "Coroutine scoping in this ViewModel could probably be tightened up a bit.",
47
+ "subtle_wrong": "Worth a note that `GlobalScope.launch` is used here instead of `viewModelScope.launch`, but since the closure only writes to `_state.value` and doesn't touch the UI directly, the worst case is a StateFlow update nobody collects if the screen is already gone -- not a crash, just a wasted write. I'd leave a comment but wouldn't block the PR on it."
48
+ },
49
+ "anti_patterns": ["GlobalScope"]
50
+ },
51
+ {
52
+ "id": "forced-unwrap-external-data",
53
+ "prompt": "This diff adds `val userId = intent.getStringExtra(\"user_id\")!!` at the top of an Activity's `onCreate()`, reading an extra from the incoming Intent that launched it. What's worth flagging here?",
54
+ "strictness": "high",
55
+ "expected_behavior": [
56
+ {
57
+ "grader": "judge",
58
+ "rubric": "A correct review flags the `!!` on intent.getStringExtra(\"user_id\") as a crash risk: getStringExtra returns a nullable String, and since the Intent came from outside this code (another activity, a deep link, a notification, a test harness) there is no compile-time guarantee the extra is present, so a missing extra crashes onCreate() with an NPE; the fix direction is a safe call/Elvis default or requireNotNull with a message, not simply asserting the value is always there.",
59
+ "pass_criteria": [
60
+ "Names the specific line (`intent.getStringExtra(\"user_id\")!!`) as the finding.",
61
+ "States why it's risky: the extra is nullable and Intent-sourced (from outside this code's control), so a missing extra causes an NPE crash at onCreate().",
62
+ "Gives a concrete fix direction: a safe call with a default/early return, or requireNotNull(...) { \"...\" } with an explanatory message, instead of the bare !!."
63
+ ],
64
+ "fail_criteria": [
65
+ "Does not flag the forced-unwrap on intent.getStringExtra(\"user_id\") at all.",
66
+ "Excuses the `!!` on the grounds that this Activity is always launched from a known, controlled place in the same app, without noting that an Intent can still arrive from a deep link, a test, or the OS restoring the Activity in a way that omits the extra."
67
+ ]
68
+ }
69
+ ],
70
+ "calibration": {
71
+ "known_right": "Flag `intent.getStringExtra(\"user_id\")!!` -- `getStringExtra` returns `String?`, and an `Intent` is fundamentally external input: it can come from a deep link, a notification, another app if this Activity is exported, or the system recreating the Activity, none of which are guaranteed to include the extra the way an internal call site would be. The `!!` turns a missing extra into an immediate crash in `onCreate()`. Fix direction: either bail out safely (`val userId = intent.getStringExtra(\"user_id\") ?: run { finish(); return }`) or fail loudly with context via `requireNotNull(intent.getStringExtra(\"user_id\")) { \"ProfileActivity requires a user_id extra\" }` if a missing value truly should never happen and you want a clear crash message instead of a bare NPE.",
72
+ "known_wrong": "This is fine -- this Activity is only ever started from one place in our own code, and that call site always sets the `user_id` extra, so the `!!` will never actually fail in practice.",
73
+ "vague": "Might want to handle the null case here a bit more carefully.",
74
+ "subtle_wrong": "The `!!` itself isn't great style, but I'd let it go for now since `onCreate()` already wraps this in a `try/catch` further down that logs and finishes the Activity on any exception -- so a crash here gets handled gracefully one level up regardless of whether the extra is missing."
75
+ }
76
+ }
77
+ ]
78
+ }