@mmerterden/multi-agent-pipeline 20.1.0 → 20.2.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 (49) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/docs/facts.json +5 -5
  3. package/manifest.json +52 -31
  4. package/package.json +1 -1
  5. package/pipeline/skills/.skill-manifest.json +36 -20
  6. package/pipeline/skills/.skills-index.json +75 -9
  7. package/pipeline/skills/shared/README.md +13 -7
  8. package/pipeline/skills/shared/external/android-architecture/SKILL.md +71 -0
  9. package/pipeline/skills/shared/external/android-architecture/references/patterns.md +142 -0
  10. package/pipeline/skills/shared/external/android-build-quality-gates/SKILL.md +314 -0
  11. package/pipeline/skills/shared/external/android-build-quality-gates/references/patterns.md +432 -0
  12. package/pipeline/skills/shared/external/android-datastore/SKILL.md +236 -0
  13. package/pipeline/skills/shared/external/android-datastore/references/patterns.md +297 -0
  14. package/pipeline/skills/shared/external/android-design-tokens-codegen/SKILL.md +249 -0
  15. package/pipeline/skills/shared/external/android-design-tokens-codegen/references/patterns.md +270 -0
  16. package/pipeline/skills/shared/external/android-jetpack-compose-expert/SKILL.md +62 -0
  17. package/pipeline/skills/shared/external/android-mvi-viewmodel/SKILL.md +255 -0
  18. package/pipeline/skills/shared/external/android-mvi-viewmodel/references/patterns.md +257 -0
  19. package/pipeline/skills/shared/external/android-performance/SKILL.md +86 -602
  20. package/pipeline/skills/shared/external/android-performance/references/patterns.md +659 -0
  21. package/pipeline/skills/shared/external/android-security/SKILL.md +117 -430
  22. package/pipeline/skills/shared/external/android-security/references/patterns.md +690 -0
  23. package/pipeline/skills/shared/external/{android_ui_verification → android-ui-verification}/SKILL.md +1 -1
  24. package/pipeline/skills/shared/external/api-security-best-practices/SKILL.md +35 -733
  25. package/pipeline/skills/shared/external/api-security-best-practices/references/auth.md +299 -0
  26. package/pipeline/skills/shared/external/api-security-best-practices/references/input-validation.md +255 -0
  27. package/pipeline/skills/shared/external/api-security-best-practices/references/rate-limiting.md +167 -0
  28. package/pipeline/skills/shared/external/app-intents/SKILL.md +39 -174
  29. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +178 -0
  30. package/pipeline/skills/shared/external/compose-components/SKILL.md +48 -0
  31. package/pipeline/skills/shared/external/compose-components/references/patterns.md +200 -0
  32. package/pipeline/skills/shared/external/compose-navigation/SKILL.md +66 -3
  33. package/pipeline/skills/shared/external/compose-navigation/references/patterns.md +191 -0
  34. package/pipeline/skills/shared/external/compose-testing/SKILL.md +107 -397
  35. package/pipeline/skills/shared/external/compose-testing/references/patterns.md +631 -0
  36. package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +121 -449
  37. package/pipeline/skills/shared/external/gradle-kotlin-dsl/references/patterns.md +715 -0
  38. package/pipeline/skills/shared/external/kotlin-coroutines-expert/SKILL.md +143 -0
  39. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +27 -102
  40. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +42 -0
  41. package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +94 -383
  42. package/pipeline/skills/shared/external/retrofit-networking/references/patterns.md +640 -0
  43. package/pipeline/skills/shared/external/room-database/SKILL.md +101 -440
  44. package/pipeline/skills/shared/external/room-database/references/patterns.md +614 -0
  45. package/pipeline/skills/shared/external/storekit/SKILL.md +69 -343
  46. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +371 -0
  47. package/pipeline/skills/shared/external/widgetkit/SKILL.md +25 -101
  48. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +107 -0
  49. package/pipeline/skills/skills-index.md +8 -2
@@ -2,24 +2,24 @@
2
2
 
3
3
  Single source of truth for skills delivered to both Claude Code (`~/.claude/skills/`) and Copilot CLI (`~/.copilot/skills/`) by the installer.
4
4
 
5
- **Total:** 213 skills (60 core + 153 external). Auto-generated by `scripts/gen-skills-index.mjs` - do not edit by hand.
5
+ **Total:** 219 skills (61 core + 158 external). Auto-generated by `scripts/gen-skills-index.mjs` - do not edit by hand.
6
6
 
7
7
  ## Directory layout
8
8
 
9
- - **`core/`** - 60 `multi-agent*` orchestration skills that are pipeline-critical. Edits here are core-code changes.
10
- - **`external/`** - 153 iOS / Android / generic skills imported from the upstream skill library. Mirrors of third-party guidance.
9
+ - **`core/`** - 61 `multi-agent*` orchestration skills that are pipeline-critical. Edits here are core-code changes.
10
+ - **`external/`** - 158 iOS / Android / generic skills imported from the upstream skill library. Mirrors of third-party guidance.
11
11
  - Install destinations (ADR-0009): Claude Code gets NO local copy of `external/` - it loads those skills from the `multi-agent-plugins` marketplace, namespaced (`ai-<stack>-toolkit:<name>`); only the two compliance catalogs from `core/` land in `~/.claude/skills/`. Copilot CLI and Codex CLI receive a flat copy filtered to the enabled stacks. `external/` remains the single authoring source that `build-stack-plugins.mjs` publishes from.
12
12
 
13
13
  Source layout is logical grouping only - skill discovery at runtime is unchanged.
14
14
 
15
15
  ## Categories
16
16
 
17
- - [Pipeline Orchestration](#pipeline-orchestration) - 60
17
+ - [Pipeline Orchestration](#pipeline-orchestration) - 61
18
18
  - [iOS / Apple Ecosystem](#ios-apple-ecosystem) - 90
19
- - [Android / Kotlin](#android-kotlin) - 13
19
+ - [Android / Kotlin](#android-kotlin) - 17
20
20
  - [Web](#web) - 10
21
21
  - [Backend / API](#backend-api) - 11
22
- - [Cross-cutting](#cross-cutting) - 29
22
+ - [Cross-cutting](#cross-cutting) - 30
23
23
 
24
24
  ## Pipeline Orchestration
25
25
 
@@ -71,6 +71,7 @@ Source layout is logical grouping only - skill discovery at runtime is unchang
71
71
  | [`multi-agent-save`](./core/multi-agent-save/) | `core` | Save a recurring job as a reusable /multi-agent:<name> command. Reviews the conversation + your CLAUDE.md for candidate routines, you pick o |
72
72
  | [`multi-agent-scan`](./core/multi-agent-scan/) | `core` | Skill security scan: walks local skill directories against a tiered pattern catalog. Use when local skill directories need checking for unsa |
73
73
  | [`multi-agent-search`](./core/multi-agent-search/) | `core` | Log search across every agent-log.md with smart ranking and filters. Optional --semantic flag queries the per-repo triage corpus. Use when s |
74
+ | [`multi-agent-security-review`](./core/multi-agent-security-review/) | `core` | Run a standalone defensive, static security review of a diff, branch or repo: run-scoped threat model, reviewer-shaped findings joined to OW |
74
75
  | [`multi-agent-setup`](./core/multi-agent-setup/) | `core` | First-run setup wizard: keychain token discovery, Git Identity onboarding, and pipeline preparation. Use when the pipeline is being set up f |
75
76
  | [`multi-agent-stack`](./core/multi-agent-stack/) | `core` | Select the active stack(s) for this repo by enabling the matching marketplace plugin(s) in .claude/settings.json. Multi-select: pass several |
76
77
  | [`multi-agent-status`](./core/multi-agent-status/) | `core` | Show every multi-agent task's ID, phase, branch, and status. Use when asked what is running, or for an overview of every task. |
@@ -185,11 +186,15 @@ Source layout is logical grouping only - skill discovery at runtime is unchang
185
186
 
186
187
  | Skill | Group | Description |
187
188
  |-------|-------|-------------|
188
- | [`android_ui_verification`](./external/android_ui_verification/) | `external` | Automated end-to-end UI testing and verification on an Android Emulator using ADB. Use when an Android build needs driving on an emulator to |
189
189
  | [`android-architecture`](./external/android-architecture/) | `external` | Design and implement Clean Architecture with multi-module structure on Android. Covers domain/data/presentation layers, Hilt dependency inje |
190
+ | [`android-build-quality-gates`](./external/android-build-quality-gates/) | `external` | Encode Android module and quality rules as build-failing Gradle tasks: layer-dependency checks, coverage floors, a Compose-stability gate, l |
191
+ | [`android-datastore`](./external/android-datastore/) | `external` | Typed Jetpack DataStore as the modern SharedPreferences replacement: Preferences and typed stores, serializer-layer encryption, and migratio |
192
+ | [`android-design-tokens-codegen`](./external/android-design-tokens-codegen/) | `external` | A single-source JSON to typed-Kotlin codegen pipeline for design tokens, UI-test ids, analytics events and deeplink manifests, run as Gradle |
190
193
  | [`android-jetpack-compose-expert`](./external/android-jetpack-compose-expert/) | `external` | Expert guidance for building modern Android UIs with Jetpack Compose, covering state management, navigation, performance, and Material Desig |
194
+ | [`android-mvi-viewmodel`](./external/android-mvi-viewmodel/) | `external` | A reusable generic BaseViewModel<State, Intent, Event> for MVI Compose apps: StateFlow state, one-shot events over a rendezvous Channel, a s |
191
195
  | [`android-performance`](./external/android-performance/) | `external` | Optimize Android app performance with Baseline Profiles for startup, Compose stability annotations (@Stable, @Immutable, strong skipping), r |
192
196
  | [`android-security`](./external/android-security/) | `external` | Secure Android apps with ProGuard/R8 obfuscation, OkHttp certificate pinning, BiometricPrompt authentication, EncryptedSharedPreferences, Pl |
197
+ | [`android-ui-verification`](./external/android-ui-verification/) | `external` | Automated end-to-end UI testing and verification on an Android Emulator using ADB. Use when an Android build needs driving on an emulator to |
193
198
  | [`compose-components`](./external/compose-components/) | `external` | Build Material 3 UI components and custom composables with Jetpack Compose. Covers TopAppBar, Scaffold, BottomSheet, SnackbarHost, slot-base |
194
199
  | [`compose-navigation`](./external/compose-navigation/) | `external` | Implement type-safe navigation in Jetpack Compose using Navigation 2.8+ with @Serializable routes, NavHost, composable<Route>, nested naviga |
195
200
  | [`compose-testing`](./external/compose-testing/) | `external` | Test Jetpack Compose UI with createComposeRule, semantic matchers (onNodeWithText, onNodeWithTag, onNodeWithContentDescription), actions (pe |
@@ -255,6 +260,7 @@ Source layout is logical grouping only - skill discovery at runtime is unchang
255
260
  | [`localization-reuse-map`](./external/localization-reuse-map/) | `external` | >- |
256
261
  | [`pdfkit`](./external/pdfkit/) | `external` | Display and manipulate PDF documents using PDFKit. Use when embedding PDFView to show PDF files, creating or modifying PDFDocument instances |
257
262
  | [`search-first`](./external/search-first/) | `external` | \| |
263
+ | [`security-review`](./external/security-review/) | `external` | Run a defensive, static security review of a diff or a repo: build a run-scoped threat model, find vulnerabilities joined to OWASP + CWE wit |
258
264
  | [`signal-community`](./external/signal-community/) | `external` | Check whether other people hit the same problem, via Stack Overflow and Hacker News (keyless) plus opt-in Reddit and X. Use when a bug repor |
259
265
  | [`skill-creator`](./external/skill-creator/) | `external` | Author and refine skills for a plugin/toolkit efficiently. Use when creating a new skill, trimming or splitting an existing one, writing a s |
260
266
  | [`spm-build-analysis`](./external/spm-build-analysis/) | `external` | Analyze Swift Package Manager dependencies, package plugins, module variants, and CI-oriented build overhead that slow Xcode builds. Use whe |
@@ -12,10 +12,12 @@ and Jetpack libraries from 2024-2025.
12
12
  ## Contents
13
13
 
14
14
  - [Multi-Module Structure](#multi-module-structure)
15
+ - [Feature api/impl Split](#feature-apiimpl-split)
15
16
  - [Domain Layer](#domain-layer)
16
17
  - [Data Layer](#data-layer)
17
18
  - [Presentation Layer](#presentation-layer)
18
19
  - [Hilt Dependency Injection](#hilt-dependency-injection)
20
+ - [Cross-Module Navigation Graph Aggregation](#cross-module-navigation-graph-aggregation)
19
21
  - [Mapper Pattern](#mapper-pattern)
20
22
  - [Do's and Don'ts](#dos-and-donts)
21
23
  - [Troubleshooting](#troubleshooting)
@@ -77,6 +79,33 @@ include(":feature:search")
77
79
  include(":feature:detail")
78
80
  ```
79
81
 
82
+ ## Feature api/impl Split
83
+
84
+ For features that navigate to each other, split each one into two modules:
85
+
86
+ - `:feature:x:api` -- only the cross-feature contract: `@Serializable` route
87
+ definitions and the entry-param models a caller needs to launch the feature.
88
+ - `:feature:x:impl` -- screens, ViewModels, UI state, and DI wiring.
89
+
90
+ A feature depends only on the `:api` of the features it navigates to, never
91
+ their `:impl`:
92
+
93
+ ```kotlin
94
+ // feature:home:impl/build.gradle.kts
95
+ dependencies {
96
+ implementation(project(":feature:home:api"))
97
+ implementation(project(":feature:detail:api")) // route + entry params only
98
+ // NEVER depend on :feature:detail:impl
99
+ }
100
+ ```
101
+
102
+ Because no `impl` module can see another `impl`, they stay mutually invisible,
103
+ compile in parallel, and cannot form dependency cycles. The `:app` module wires
104
+ them together (see
105
+ [Cross-Module Navigation Graph Aggregation](#cross-module-navigation-graph-aggregation)).
106
+ Full module layout and route contract:
107
+ [references/patterns.md#feature-apiimpl-split](references/patterns.md#feature-apiimpl-split).
108
+
80
109
  ## Domain Layer
81
110
 
82
111
  The domain layer contains business logic with zero Android dependencies. It
@@ -231,6 +260,11 @@ sealed interface HomeUiState {
231
260
  }
232
261
  ```
233
262
 
263
+ For the reusable base ViewModel seam these screen ViewModels extend -- immutable
264
+ state as `StateFlow`, one-shot events, a single intent entry point, and
265
+ centralized use-case execution -- see the `android-mvi-viewmodel` skill. Do not
266
+ reimplement that base class here.
267
+
234
268
  ## Hilt Dependency Injection
235
269
 
236
270
  ### Application Setup
@@ -324,6 +358,36 @@ abstract class DispatcherModule {
324
358
  }
325
359
  ```
326
360
 
361
+ ## Cross-Module Navigation Graph Aggregation
362
+
363
+ Each `:impl` module contributes its destinations to a shared `Set<NavGraph>` via
364
+ Hilt multibinding; the host registers the whole set into one `NavHost`. The
365
+ `:app` module never depends on any `:impl`.
366
+
367
+ ```kotlin
368
+ fun interface NavGraph {
369
+ fun NavGraphBuilder.build(navController: NavHostController)
370
+ }
371
+
372
+ // In each :impl module
373
+ @Module
374
+ @InstallIn(SingletonComponent::class)
375
+ abstract class HomeNavModule {
376
+ @Binds
377
+ @IntoSet
378
+ abstract fun bindHomeNavGraph(impl: HomeNavGraph): NavGraph
379
+ }
380
+
381
+ // In :app -- register every contributed graph, naming no feature
382
+ NavHost(navController, startDestination = HomeRoute) {
383
+ navGraphs.forEach { graph -> with(graph) { build(navController) } }
384
+ }
385
+ ```
386
+
387
+ This wires many feature modules into a single `NavHost` without the app module
388
+ depending on every `impl`. Full host injection and per-feature contribution:
389
+ [references/patterns.md#cross-module-navigation-graph-aggregation](references/patterns.md#cross-module-navigation-graph-aggregation).
390
+
327
391
  ## Mapper Pattern
328
392
 
329
393
  Mappers isolate layer boundaries. Each layer has its own model type.
@@ -372,9 +436,13 @@ class FlightMapper @Inject constructor() {
372
436
  - Define a `DispatcherProvider` interface for testable coroutine dispatchers
373
437
  - One use case per business operation
374
438
  - Use `sealed interface` for UI state to guarantee exhaustive `when` handling
439
+ - Split each feature into `:api` (routes + entry params) and `:impl` (screens, ViewModels, DI)
440
+ - Aggregate feature nav graphs into the host with Hilt `@IntoSet` multibinding
375
441
 
376
442
  ### Don'ts
377
443
  - Do not let feature modules depend on each other directly
444
+ - Do not depend on another feature's `:impl` module -- depend on its `:api` only
445
+ - Do not make the `:app` module reference feature `impl` modules in code
378
446
  - Do not put Android framework classes in the domain layer
379
447
  - Do not expose `MutableStateFlow` from ViewModels (expose read-only `StateFlow`)
380
448
  - Do not inject `Activity` or `Fragment` into ViewModels
@@ -402,6 +470,9 @@ class FlightMapper @Inject constructor() {
402
470
  - [ ] UI state is a `sealed interface` with Loading/Success/Error
403
471
  - [ ] Hilt modules use `@Binds` for interfaces, `@Provides` for instances
404
472
  - [ ] Feature modules do not depend on each other
473
+ - [ ] Each feature is split into `:api` (routes/entry params) and `:impl` (screens/ViewModels/DI)
474
+ - [ ] Feature `impl` modules depend only on other features' `api`, never their `impl`
475
+ - [ ] Feature nav graphs aggregate into the host via Hilt `@IntoSet` multibinding
405
476
  - [ ] Mappers separate DTO/Entity/Domain models
406
477
  - [ ] Coroutine dispatchers are injectable via `DispatcherProvider`
407
478
  - [ ] No `GlobalScope` usage; all coroutines use structured concurrency
@@ -0,0 +1,142 @@
1
+ # Android Clean Architecture -- Reference Patterns
2
+
3
+ Full code for the patterns summarized in `SKILL.md`. Load the section you need
4
+ when implementing that specific structure.
5
+
6
+ ## Contents
7
+
8
+ - [Feature api/impl Split](#feature-apiimpl-split)
9
+ - [Cross-Module Navigation Graph Aggregation](#cross-module-navigation-graph-aggregation)
10
+
11
+ ## Feature api/impl Split
12
+
13
+ Split every feature into an `:api` module (the cross-feature contract) and an
14
+ `:impl` module (everything private to the feature).
15
+
16
+ ```
17
+ :feature:home:api # Serializable routes + entry-param models only
18
+ :feature:home:impl # Screens, ViewModels, UI state, DI
19
+ :feature:search:api
20
+ :feature:search:impl
21
+ :feature:detail:api
22
+ :feature:detail:impl
23
+ ```
24
+
25
+ ### The api module holds only the contract
26
+
27
+ ```kotlin
28
+ // :feature:detail:api
29
+ @Serializable
30
+ data class DetailRoute(val id: String)
31
+
32
+ @Serializable
33
+ data class DetailEntryParams(val id: String, val source: String)
34
+ ```
35
+
36
+ No screens, no ViewModels, no Compose dependency -- just the route and the
37
+ models a caller needs to launch the feature.
38
+
39
+ ### The impl module holds everything private
40
+
41
+ ```kotlin
42
+ // :feature:detail:impl -- not visible to any other feature
43
+ @HiltViewModel
44
+ class DetailViewModel @Inject constructor(
45
+ savedStateHandle: SavedStateHandle,
46
+ private val getDetailUseCase: GetDetailUseCase,
47
+ ) : ViewModel() {
48
+ private val route = savedStateHandle.toRoute<DetailRoute>()
49
+ // ... state, intents, events
50
+ }
51
+
52
+ @Composable
53
+ internal fun DetailScreen(viewModel: DetailViewModel = hiltViewModel()) { /* ... */ }
54
+ ```
55
+
56
+ ### Dependency direction
57
+
58
+ ```kotlin
59
+ // feature:home:impl/build.gradle.kts
60
+ dependencies {
61
+ implementation(project(":feature:home:api"))
62
+ implementation(project(":feature:detail:api")) // route + entry params only
63
+ implementation(project(":core:model"))
64
+ implementation(project(":core:ui"))
65
+ // NEVER implementation(project(":feature:detail:impl"))
66
+ }
67
+ ```
68
+
69
+ Because no `impl` module can see another `impl`, the graph of feature modules is
70
+ acyclic by construction, the modules compile in parallel, and a caller can only
71
+ couple to a feature through its published route and entry params.
72
+
73
+ ## Cross-Module Navigation Graph Aggregation
74
+
75
+ Each `:impl` module contributes its destinations to a shared `Set<NavGraph>` via
76
+ Hilt multibinding. The host module (`:app`) injects the full set and registers
77
+ every contribution into one `NavHost`, without depending on any `:impl`.
78
+
79
+ ### The contribution contract
80
+
81
+ ```kotlin
82
+ // :core:ui (or a :core:navigation module)
83
+ fun interface NavGraph {
84
+ fun NavGraphBuilder.build(navController: NavHostController)
85
+ }
86
+ ```
87
+
88
+ ### Each feature contributes its graph
89
+
90
+ ```kotlin
91
+ // :feature:home:impl
92
+ class HomeNavGraph @Inject constructor() : NavGraph {
93
+ override fun NavGraphBuilder.build(navController: NavHostController) {
94
+ composable<HomeRoute> {
95
+ HomeScreen()
96
+ }
97
+ }
98
+ }
99
+
100
+ @Module
101
+ @InstallIn(SingletonComponent::class)
102
+ abstract class HomeNavModule {
103
+ @Binds
104
+ @IntoSet
105
+ abstract fun bindHomeNavGraph(impl: HomeNavGraph): NavGraph
106
+ }
107
+ ```
108
+
109
+ ### The host registers the whole set
110
+
111
+ ```kotlin
112
+ // :app
113
+ @Composable
114
+ fun AppNavHost(
115
+ navGraphs: Set<@JvmSuppressWildcards NavGraph>,
116
+ navController: NavHostController = rememberNavController(),
117
+ startDestination: Any = HomeRoute,
118
+ ) {
119
+ NavHost(navController = navController, startDestination = startDestination) {
120
+ navGraphs.forEach { graph -> with(graph) { build(navController) } }
121
+ }
122
+ }
123
+ ```
124
+
125
+ Injecting the set into the composable is done with a Hilt entry point or by
126
+ passing it from an `@AndroidEntryPoint` activity:
127
+
128
+ ```kotlin
129
+ @AndroidEntryPoint
130
+ class MainActivity : ComponentActivity() {
131
+ @Inject lateinit var navGraphs: Set<@JvmSuppressWildcards NavGraph>
132
+
133
+ override fun onCreate(savedInstanceState: Bundle?) {
134
+ super.onCreate(savedInstanceState)
135
+ setContent { AppNavHost(navGraphs = navGraphs) }
136
+ }
137
+ }
138
+ ```
139
+
140
+ Adding a feature to the app is now: add its `:impl` to the `:app` runtime
141
+ classpath and its `@IntoSet` binding registers automatically. The `:app` module
142
+ never names the feature in code.
@@ -0,0 +1,314 @@
1
+ ---
2
+ name: android-build-quality-gates
3
+ description: "Encode Android module and quality rules as build-failing Gradle tasks: layer-dependency checks, coverage floors, a Compose-stability gate, lint-on-diff and screenshot validation. Use when a multi-module app needs enforceable boundaries and quality gates in the build, not in review."
4
+ risk: safe
5
+ source: multi-agent-pipeline
6
+ date_added: "2026-09-21"
7
+ ---
8
+
9
+ # Android Build Quality Gates
10
+
11
+ A convention holds only while everyone remembers it. A gate holds because the
12
+ build turns red without it. This skill covers the quality rules worth promoting
13
+ from review comments into Gradle tasks that fail: layer boundaries, coverage
14
+ floors, Compose stability, lint on changed files, and screenshot validation.
15
+
16
+ Targets AGP 8.x, Gradle 8.x with the configuration cache, Kover 0.8+, and the
17
+ Compose compiler Gradle plugin (Kotlin 2.0+). Copy-ready, full-length code for
18
+ every gate lives in [references/patterns.md](references/patterns.md); this file
19
+ carries the decisions and short snippets.
20
+
21
+ ## Contents
22
+
23
+ - [Where gates live](#where-gates-live)
24
+ - [Layer-dependency gate](#layer-dependency-gate)
25
+ - [Coverage floor with Kover](#coverage-floor-with-kover)
26
+ - [Compose-stability gate](#compose-stability-gate)
27
+ - [Lint on diff](#lint-on-diff)
28
+ - [Screenshot validation task](#screenshot-validation-task)
29
+ - [Ratchet, do not wall](#ratchet-do-not-wall)
30
+ - [Wiring gates into check](#wiring-gates-into-check)
31
+ - [Do's and Don'ts](#dos-and-donts)
32
+ - [Review Checklist](#review-checklist)
33
+
34
+ ## Where gates live
35
+
36
+ Author gates once in a convention plugin under `build-logic`, then apply that
37
+ plugin from each module. A gate copied into twenty `build.gradle.kts` files
38
+ drifts; a gate in one convention plugin is a single source of truth. See
39
+ `gradle-kotlin-dsl` for how the `build-logic` module and version catalog are
40
+ set up; this skill only adds tasks inside it.
41
+
42
+ Every gate follows the same three-part shape:
43
+
44
+ 1. A task (or a `verify`/`check`-typed configuration) that computes a fact about
45
+ the module: its dependency set, its coverage ratio, its unskippable
46
+ composables.
47
+ 2. An assertion that throws `GradleException` when the fact violates the rule.
48
+ 3. A wire into the `check` lifecycle (or a `finalizedBy` on an existing task) so
49
+ it runs without anyone remembering to call it.
50
+
51
+ Keep the assertion message actionable: name the offending dependency, the
52
+ current versus required number, the exact composable. A gate that fails with
53
+ "verification failed" gets suppressed; one that says "module `feature-x` depends
54
+ on `data-y`, which is not in its allowed set" gets fixed.
55
+
56
+ ## Layer-dependency gate
57
+
58
+ Clean layering (feature -> domain -> data, never feature -> data directly, never
59
+ domain -> feature) is the rule most often stated and least often enforced. Encode
60
+ it by walking a module's resolved project dependencies and asserting each is in
61
+ an allowed set for that module's layer.
62
+
63
+ Register a task that reads the `implementation`/`api` configurations, keeps only
64
+ `ProjectDependency` entries, and compares their paths against an allowlist:
65
+
66
+ ```kotlin
67
+ val checkLayerDependencies by tasks.registering {
68
+ group = "verification"
69
+ val layer = project.moduleLayer()
70
+ val projectDeps = configurations
71
+ .matching { it.name in setOf("implementation", "api") }
72
+ .flatMap { it.dependencies.withType<ProjectDependency>() }
73
+ .map { it.path }
74
+ doLast {
75
+ val forbidden = projectDeps.filterNot { layer.allows(it) }
76
+ if (forbidden.isNotEmpty()) {
77
+ throw GradleException(
78
+ "$path ($layer) declares forbidden dependencies: $forbidden"
79
+ )
80
+ }
81
+ }
82
+ }
83
+ tasks.named("check").configure { dependsOn(checkLayerDependencies) }
84
+ ```
85
+
86
+ Derive the layer from the module path or a convention (`feature-*`, `domain-*`,
87
+ `data-*`) rather than a hand-maintained map, so a new module is governed the day
88
+ it is created. The full version, including the layer enum, its `allows` matrix,
89
+ and configuration-cache-safe capture of dependency paths, is in
90
+ [references/patterns.md#layer-dependency-gate](references/patterns.md#layer-dependency-gate).
91
+
92
+ Read dependency paths at configuration time into a local (as above), not inside
93
+ `doLast` off the live `configurations` object, so the task stays compatible with
94
+ the configuration cache.
95
+
96
+ ## Coverage floor with Kover
97
+
98
+ A coverage number nobody enforces only decorates a report. Kover's verification
99
+ rule turns it into a gate: below the floor, the build fails.
100
+
101
+ Apply `org.jetbrains.kotlinx.kover` and declare a bound:
102
+
103
+ ```kotlin
104
+ kover {
105
+ reports {
106
+ verify {
107
+ rule("Line coverage floor") {
108
+ minBound(60)
109
+ }
110
+ }
111
+ }
112
+ }
113
+ ```
114
+
115
+ Split instrumentation from aggregation. Each module applies the Kover plugin so
116
+ its own classes are instrumented; a single aggregation module (or the root)
117
+ depends on every measured module and owns the `verify` rule over the merged
118
+ report. Verifying per module produces noisy, uneven thresholds; verifying the
119
+ aggregate gives one honest project number. The aggregation wiring and per-module
120
+ exclusion of generated code (`*_Factory`, `*Args`, `BuildConfig`, Hilt
121
+ components) are in
122
+ [references/patterns.md#coverage-floor-with-kover](references/patterns.md#coverage-floor-with-kover).
123
+
124
+ Gate the floor behind a property so local `assembleDebug` stays fast and only CI
125
+ (or an explicit run) pays for instrumented coverage:
126
+
127
+ ```kotlin
128
+ tasks.named("check").configure {
129
+ if (providers.gradleProperty("enableCoverageVerification").isPresent) {
130
+ dependsOn("koverVerify")
131
+ }
132
+ }
133
+ ```
134
+
135
+ Run it in CI with `-PenableCoverageVerification`. Set the floor to the current
136
+ measured value, not an aspirational one; see [Ratchet, do not wall](#ratchet-do-not-wall).
137
+
138
+ ## Compose-stability gate
139
+
140
+ Unstable parameters silently defeat recomposition skipping and cause jank that
141
+ no unit test catches. The Compose compiler can emit a stability report; parse it
142
+ and fail when a composable that should skip cannot.
143
+
144
+ First point the compiler's metrics and reports at a build directory via the
145
+ Compose compiler Gradle plugin:
146
+
147
+ ```kotlin
148
+ composeCompiler {
149
+ val dir = layout.buildDirectory.dir("compose-metrics")
150
+ metricsDestination.set(dir)
151
+ reportsDestination.set(dir)
152
+ }
153
+ ```
154
+
155
+ This produces `*-composables.txt` per module. Register a task that parses those
156
+ files, collects every `restartable` composable marked `skippable` as its
157
+ negation (i.e. the `restartable` but NOT `skippable` ones), and fails on any that
158
+ is not in a per-module ignore-list. Finalize each Kotlin compile with it so the
159
+ report is always fresh:
160
+
161
+ ```kotlin
162
+ val checkComposeStability by tasks.registering(ComposeStabilityCheck::class) {
163
+ reportDir.set(layout.buildDirectory.dir("compose-metrics"))
164
+ ignoreFile.set(layout.projectDirectory.file("compose-stability-ignore.txt"))
165
+ }
166
+ tasks.withType<KotlinCompile>().configureEach {
167
+ finalizedBy(checkComposeStability)
168
+ }
169
+ ```
170
+
171
+ The ignore-list file holds fully-qualified composable names that are accepted
172
+ exceptions (a composable that takes a third-party unstable type you cannot
173
+ annotate). Each line is one accepted composable; anything unskippable and not
174
+ listed fails the build. The full `ComposeStabilityCheck` task with the report
175
+ parser and ignore-list matching is in
176
+ [references/patterns.md#compose-stability-gate](references/patterns.md#compose-stability-gate).
177
+
178
+ The report is only emitted when the compiler runs, which is why the check is
179
+ `finalizedBy` the compile rather than a standalone task: a cached compile with no
180
+ report means nothing changed, and the check reads the last report.
181
+
182
+ ## Lint on diff
183
+
184
+ Full lint across a large multi-module project is minutes of wall time; running it
185
+ on every commit trains people to skip the hook. Split it: a fast pre-commit gate
186
+ over only the changed files, and full analysis in CI.
187
+
188
+ At commit time, feed the static analyzer (detekt or Android Lint) the diff:
189
+
190
+ ```bash
191
+ CHANGED=$(git diff --cached --name-only --diff-filter=d -- '*.kt' '*.kts')
192
+ [ -z "$CHANGED" ] && exit 0
193
+ ./gradlew detektDiff -PdetektInput="$CHANGED"
194
+ ```
195
+
196
+ Wire `detektDiff` to accept a file list and set the detekt `source` to just those
197
+ paths, so the analyzer never walks the whole tree at commit time. In CI, run the
198
+ full `detekt` / `lint` task with the baseline so nothing escapes. The pre-commit
199
+ hook and the `detektDiff` task definition are in
200
+ [references/patterns.md#lint-on-diff](references/patterns.md#lint-on-diff).
201
+
202
+ Keep the two consistent: the same ruleset and the same baseline file drive both
203
+ the diff run and the full run, so a commit that passes the hook does not surprise
204
+ the author in CI.
205
+
206
+ ## Screenshot validation task
207
+
208
+ Screenshot tests are only a gate when a mismatch fails the build. Author the
209
+ tests with the mechanics in `compose-testing`; here, register the verification
210
+ task and put it on the `check` path.
211
+
212
+ Expose a single umbrella task so CI and local calls agree:
213
+
214
+ ```kotlin
215
+ tasks.register("verifyScreenshots") {
216
+ group = "verification"
217
+ dependsOn(tasks.matching { it.name == "verifyRoborazziDebug" })
218
+ }
219
+ tasks.named("check").configure { dependsOn("verifyScreenshots") }
220
+ ```
221
+
222
+ Record and verify are separate entry points: `recordScreenshots` regenerates
223
+ golden images (run deliberately, reviewed in the diff), `verifyScreenshots`
224
+ compares and fails. Never let `verify` fall back to recording on a missing
225
+ golden, or the gate passes on the very case it exists to catch. The record/verify
226
+ task pair and the CI upload of diff images on failure are in
227
+ [references/patterns.md#screenshot-validation-task](references/patterns.md#screenshot-validation-task).
228
+
229
+ ## Ratchet, do not wall
230
+
231
+ The failure mode of every gate above is the same: set the bar higher than the
232
+ codebase currently clears, and the team disables the gate instead of meeting it.
233
+ A gate that is always red is worse than no gate, because it also hides the
234
+ regressions it was meant to catch.
235
+
236
+ Seed each gate at the current state and let it only tighten:
237
+
238
+ - Coverage: set `minBound` to the value measured today, then raise it in small
239
+ steps as tests land. Never open at a round aspirational number.
240
+ - Compose stability: seed the ignore-list with every composable currently
241
+ unskippable, so the gate passes on day one and only new offenders fail. Shrink
242
+ the list over time; adding to it needs review.
243
+ - Lint: generate a baseline of existing findings; the gate fails only on new
244
+ ones. Regenerating the baseline to bury a fresh finding is a review smell.
245
+ - Layers: if a forbidden edge exists today, either fix it before turning the gate
246
+ on, or record it as a temporary allowed exception with a tracking reference,
247
+ never widen the whole allow-matrix.
248
+
249
+ A ceiling that only moves down (ignore-lists, baselines) and a floor that only
250
+ moves up (coverage) is the shape that survives contact with a real team.
251
+
252
+ ## Wiring gates into check
253
+
254
+ Gates that are not on a lifecycle path do not run. Attach each to `check` (or a
255
+ custom `qualityGates` aggregate task that `check` depends on) so a single
256
+ `./gradlew check` runs every gate, and CI needs no bespoke task list.
257
+
258
+ ```kotlin
259
+ val qualityGates by tasks.registering {
260
+ group = "verification"
261
+ dependsOn(
262
+ "checkLayerDependencies",
263
+ "checkComposeStability",
264
+ "verifyScreenshots",
265
+ )
266
+ }
267
+ tasks.named("check").configure { dependsOn(qualityGates) }
268
+ ```
269
+
270
+ Keep the expensive, environment-dependent gates (instrumented coverage, full
271
+ lint) behind properties so `check` stays usable locally, and turn them on in CI
272
+ via `-P` flags. The full convention-plugin assembly that registers all gates is
273
+ in [references/patterns.md#wiring-gates-into-check](references/patterns.md#wiring-gates-into-check).
274
+
275
+ ## Do's and Don'ts
276
+
277
+ - Do author every gate in a `build-logic` convention plugin, applied per module.
278
+ - Do make failure messages name the offender, the actual value, and the required
279
+ value.
280
+ - Do capture configuration values (dependency paths, file lists) into locals at
281
+ configuration time for configuration-cache compatibility.
282
+ - Do seed ignore-lists and baselines from the current state so the gate opens
283
+ green.
284
+ - Don't verify coverage per module; aggregate, then verify once.
285
+ - Don't let a screenshot `verify` task record missing goldens.
286
+ - Don't set a floor above what the code clears today; a permanently red gate gets
287
+ switched off.
288
+ - Don't duplicate the gate logic across module build files.
289
+ - Don't run full lint at commit time; run it on the diff, full in CI.
290
+
291
+ ## Ownership Map
292
+
293
+ This skill owns build-failing quality-gate tasks. It does not re-explain:
294
+
295
+ - Typed `BuildConfig`, convention-plugin composition, version catalog, and
296
+ general build setup -> `gradle-kotlin-dsl`.
297
+ - Authoring Compose screenshot tests, semantic matchers, and ViewModel tests ->
298
+ `compose-testing` (this skill only registers the verification task).
299
+ - Codegen `--check` CI verification of generated design-token sources ->
300
+ `android-design-tokens-codegen`.
301
+
302
+ ## Review Checklist
303
+
304
+ - [ ] Each gate lives in a convention plugin, not copied per module.
305
+ - [ ] Layer gate derives layer from convention, governs new modules automatically.
306
+ - [ ] Coverage uses a single aggregation report with one `verify` bound, gated by
307
+ a property.
308
+ - [ ] Compose stability report points at a build dir, check is `finalizedBy` the
309
+ compile, ignore-list holds accepted exceptions only.
310
+ - [ ] Lint runs on the diff at commit time, full with baseline in CI.
311
+ - [ ] Screenshot `verify` fails on missing/changed goldens, never records.
312
+ - [ ] Every gate is reachable from `check`; expensive ones sit behind `-P` flags.
313
+ - [ ] Floors seeded at current state; ignore-lists/baselines only ratchet down.
314
+ - [ ] Failure messages are actionable (offender + actual + required).