loom-kotlin
cosmix/loom/skills/loom-kotlin/SKILL.md
Kotlin language expertise for idiomatic, production-quality code.
Skill56 starsChanged 17 days ago
What's in it
- Kotlin Language Expertise
- Overview
- Tooling
- Null Safety and Errors
- Data, Sealed and Value Classes
- Coroutines and Structured Concurrency
- Flow
- Ktor
- Spring with Kotlin
- Testing
- Loom Test Runner Adapter
- Anti-Patterns
- Expert Practices
- Collections and Sequences
- Scope Functions
- Java Interop
- Security and Serialization
- Performance
- Verification Checklists
---
name: loom-kotlin
description: Kotlin language expertise for idiomatic, production-quality code.
triggers:
- kotlin
- kt
- kts
- build.gradle.kts
- gradle kotlin dsl
- gradle
- coroutines
- kotlinx
- suspend
- flow
- stateflow
- ktor
- kotest
- mockk
- junit
- detekt
- ktlint
- data class
- sealed class
- value class
- null safety
- kotlinx.serialization
- spring kotlin
---
# Kotlin Language Expertise
## Overview
Idiomatic, production-grade Kotlin 2.x on the JVM: null safety, data and sealed classes, coroutines with structured concurrency, Flow, Gradle Kotlin DSL builds, Ktor, Spring with Kotlin, and JUnit 5/kotest testing. Assumes the reader writes Kotlin already. The content is the part that goes wrong in production: `runCatching` swallowing cancellation, `GlobalScope` leaks, blocking calls on the wrong dispatcher, platform types from Java, final classes breaking Spring proxies.
## Tooling
Gradle with the Kotlin DSL is the default build; the K2 compiler is the default since Kotlin 2.0. Use the wrapper (`./gradlew`) and keep versions in the catalog (`gradle/libs.versions.toml`).
| Task | Command |
| --- | --- |
| Full gate | `./gradlew check` (tests plus detekt/ktlint when their plugins are applied) |
| Compile main and tests | `./gradlew testClasses` |
| Unit tests | `./gradlew test` |
| One test | `./gradlew test --tests 'com.acme.FooTest.bar'` |
| Static analysis | `./gradlew detekt` |
| Format | `./gradlew ktlintFormat` (or `spotlessApply` with ktlint or ktfmt) |
| Dependency tree | `./gradlew dependencies --configuration runtimeClasspath` |
```kotlin
// build.gradle.kts
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.detekt)
alias(libs.plugins.ktlint)
}
kotlin {
jvmToolchain(21)
compilerOptions {
allWarningsAsErrors = true
freeCompilerArgs.add("-Xjsr305=strict") // Java nullability annotations become Kotlin types
}
}
dependencies {
implementation(libs.kotlinx.coroutines.core)
testImplementation(kotlin("test")) // kotlin.test on JUnit 5 once useJUnitPlatform() is set
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.mockk)
}
tasks.test { useJUnitPlatform() }
```
- Share build logic across subprojects through convention plugins in an included `build-logic` build (or `buildSrc`), applied by id. `allprojects {}`/`subprojects {}` blocks couple projects and defeat the configuration cache.
- `settings.gradle.kts` holds `rootProject.name`, `include(":app")` and `dependencyResolutionManagement`. Catalog accessors (`libs.kotlinx.coroutines.core`) are generated and type-checked.
- `./gradlew test` is skipped as `UP-TO-DATE` when its inputs are unchanged; `--rerun` forces it.
- Libraries: `kotlin { explicitApi() }` requires visibility modifiers and return types on every public declaration.
- Annotation processing: KSP where the processor supports it; kapt is in maintenance mode and slower.
- Maven Kotlin builds use `kotlin-maven-plugin` with `<extensions>true</extensions>`; in mixed Java/Kotlin modules the Kotlin compilation must run before `maven-compiler-plugin`.
## Null Safety and Errors
```kotlin
fun displayName(user: User?): String {
val name = user?.profile?.nickname ?: user?.fullName ?: return "anonymous"
return name.trim()
}
class Session(private val store: TokenStore) {
private var token: String? = null
fun header(): String {
val current = token ?: store.load().also { token = it } // a local val smart-casts; the var does not
return "Bearer $current"
}
}
```
- Smart casts need a stable value. They fail on `var` properties, `open` properties, properties with custom getters and properties declared in another module. Copy into a local `val` and branch on it.
- Platform types: a value from Java (`String!`) is unchecked, and Kotlin trusts whatever type you declare. State the type at the boundary (`val name: String? = javaUser.getName()`) and annotate Java code you own with JSpecify.
- `!!` throws a bare `NullPointerException`. `requireNotNull(x) { "config key $key missing" }` and `checkNotNull` fail with a message.
- `require(cond) { msg }` throws `IllegalArgumentException`, `check(cond) { msg }` and `error(msg)` throw `IllegalStateException`.
- `lateinit var` is for fields a framework or test setup assigns; reading it early throws `UninitializedPropertyAccessException`. It does not apply to primitive or nullable types.
- `List<String?>` (nullable elements) and `List<String>?` (nullable list) differ. `filterNotNull()`, `mapNotNull {}` and `firstOrNull {}` keep null handling in the types.
- Kotlin has no checked exceptions. Java callers see none unless the function carries `@Throws(IOException::class)`.
- Model expected domain failures as sealed result types so callers must handle each case; throw for bugs and infrastructure faults.
- `runCatching {}` catches every `Throwable`, `CancellationException` and `OutOfMemoryError` included. In coroutine code it breaks cancellation (see Anti-Patterns).
- `use {}` closes a `Closeable`/`AutoCloseable` on every path.
## Data, Sealed and Value Classes
```kotlin
sealed interface PaymentResult {
data class Approved(val id: String, val amountCents: Long) : PaymentResult
data class Declined(val reason: String) : PaymentResult
data object Pending : PaymentResult
}
fun describe(result: PaymentResult): String = when (result) { // exhaustive: no else branch
is PaymentResult.Approved if result.amountCents == 0L -> "approved, zero amount"
is PaymentResult.Approved -> "approved ${result.id}"
is PaymentResult.Declined -> "declined: ${result.reason}"
PaymentResult.Pending -> "pending"
}
@JvmInline
value class Email(val value: String) {
init { require('@' in value) { "invalid email: $value" } }
}
```
- `data class` derives `equals`, `hashCode`, `toString`, `copy` and `componentN` from the primary constructor only; properties declared in the body take no part. `copy` is shallow and runs `init` blocks.
- Keep `var` out of data classes used as map keys or set elements: a mutation changes the hash code and strands the entry.
- A `when` over a sealed type or enum needs no `else`, so a new subtype breaks compilation wherever it must be handled. Guard conditions (`is X if ...`) are stable since Kotlin 2.2.
- `data object` (1.9) gives singleton variants a readable `toString`. `Enum.entries` (1.9) replaces `values()`, which allocates a new array per call.
- Value classes erase to their field at runtime but box when used as a nullable type, a generic type argument or through an interface. Functions taking them get mangled JVM names, so Java callers need `@JvmName` on those functions.
- Destructuring is positional: `val (id, amount) = approved` changes meaning silently when constructor parameters are reordered.
## Coroutines and Structured Concurrency
```kotlin
suspend fun loadDashboard(userId: UserId): Dashboard = coroutineScope {
val profile = async { profileClient.fetch(userId) }
val orders = async { orderClient.recent(userId) }
Dashboard(profile.await(), orders.await()) // a failure in either cancels the other, then rethrows
}
class ReportReader(private val io: CoroutineDispatcher = Dispatchers.IO) { // injectable for tests
suspend fun read(path: Path): String = withContext(io) {
Files.readString(path) // blocking call moved off the caller's dispatcher
}
}
```
- Every coroutine runs in a scope that waits for its children. `coroutineScope {}` fails fast: a child failure cancels the siblings and the scope rethrows. `supervisorScope {}` isolates child failures, and each child handles its own errors.
- `GlobalScope` (`@DelicateCoroutinesApi`) escapes that structure: nothing cancels or awaits its work. Launch background work from a scope you own, such as `CoroutineScope(SupervisorJob() + dispatcher)` cancelled on shutdown, or the framework's scope.
- `launch` when no result is needed, `async` when one is. In a regular scope a failed `async` cancels the parent at once, before anyone calls `await()`.
- Dispatchers: `Default` for CPU work (sized to the cores), `IO` for blocking calls (at least 64 threads), `Dispatchers.IO.limitedParallelism(n)` for a bounded pool per resource. Calls that already suspend (Ktor client, R2DBC) need no `withContext(Dispatchers.IO)`.
- Cancellation is cooperative. kotlinx suspend functions check it; a CPU loop must call `ensureActive()` or `yield()`; blocking Java calls ignore it unless wrapped in `runInterruptible {}`, which turns cancellation into thread interruption.
- `withTimeout` throws `TimeoutCancellationException`, which is a `CancellationException`: inside `launch` it ends the coroutine quietly, with nothing logged. `withTimeoutOrNull` returns `null` instead.
- `runBlocking` belongs at the top of `main` and in bridges from blocking code. Inside a coroutine it blocks the dispatcher thread.
- The compiler rejects a suspension point inside `synchronized {}`. Use `kotlinx.coroutines.sync.Mutex` (not reentrant) or confine the state to one coroutine.
## Flow
```kotlin
fun prices(symbol: String): Flow<Price> = flow {
while (true) {
emit(api.quote(symbol))
delay(1_000)
}
}
.flowOn(Dispatchers.IO) // upstream on IO; the collector keeps its context
.catch { e -> log.warn("quote stream failed", e) } // catches upstream failures only
val latest: StateFlow<Price?> = prices("ACME")
.stateIn(scope, SharingStarted.WhileSubscribed(5_000), initialValue = null)
```
- `flow {}` is cold: the block runs once per collector. `shareIn`/`stateIn` share one upstream among many collectors.
- `flowOn` changes the context of the operators above it. Calling `withContext` inside `flow {}` and emitting from there throws `IllegalStateException` (flow invariant violated).
- `catch` sees upstream exceptions only; a failure inside `collect {}` passes it by.
- `StateFlow` is conflated and skips equal values: assigning the current value emits nothing, and a slow collector sees only the latest. Change it with `state.update { it.copy(...) }` (atomic compare-and-set); `state.value = state.value.copy(...)` loses concurrent updates.
- `collectLatest` cancels the previous block when a new value arrives.
## Ktor
```kotlin
fun Application.module() {
install(ContentNegotiation) { json() }
install(StatusPages) {
exception<IllegalArgumentException> { call, cause ->
call.respond(HttpStatusCode.BadRequest, mapOf("error" to (cause.message ?: "bad request")))
}
}
routing {
get("/users/{id}") {
val id = call.parameters["id"]?.toLongOrNull()
?: return@get call.respond(HttpStatusCode.BadRequest)
val user = userService.find(id) ?: return@get call.respond(HttpStatusCode.NotFound)
call.respond(user) // User is @Serializable
}
}
}
fun main() {
embeddedServer(Netty, port = 8080, module = Application::module).start(wait = true)
}
```
- Route handlers are suspend functions on the engine's threads. Wrap blocking calls (JDBC, file IO) in `withContext(Dispatchers.IO)`.
- Create one `HttpClient` per application and close it on shutdown; each instance owns an engine and a connection pool. Set `expectSuccess = true` to turn non-2xx responses into `ResponseException`; by default any status returns normally.
- Keep routes thin: parse and validate in the handler, call a service, map domain results to status codes in one place (`StatusPages`).
## Spring with Kotlin
```kotlin
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.spring) // opens classes annotated @Component, @Transactional, @Async, @Cacheable, @SpringBootTest
alias(libs.plugins.kotlin.jpa) // no-arg constructors for @Entity, @Embeddable, @MappedSuperclass
alias(libs.plugins.spring.boot)
}
allOpen { // lazy-loading proxies subclass entities
annotation("jakarta.persistence.Entity")
annotation("jakarta.persistence.MappedSuperclass")
annotation("jakarta.persistence.Embeddable")
}
```
```kotlin
@ConfigurationProperties("spool")
data class SpoolProperties(val directory: String, val maxFiles: Int = 100)
@Service
class SpoolService(private val repository: SpoolRepository) { // primary constructor injection
@Transactional
fun archive(id: Long) {
val entry = repository.findByIdOrNull(id) ?: throw EntryNotFoundException(id)
entry.archived = true
}
}
```
- Kotlin classes and methods are final. Without `plugin.spring`, `@Configuration` classes fail at startup and `@Transactional` on a final method is skipped silently, since the proxy cannot override it.
- Entities are regular classes with `var` properties. A `data class` entity derives `equals`/`hashCode`/`toString` from mutable fields and lazy associations.
- `jackson-module-kotlin` on the classpath (Boot registers it) lets Jackson construct classes through primary constructors and honour default values.
- `-Xjsr305=strict` turns Spring's nullability annotations into Kotlin nullable and non-null types.
- WebFlux controllers may be `suspend` functions and may return `Flow`.
- Self-invocation, rollback rules and `open-in-view` behave as in Java Spring (see `loom-java`).
## Testing
```kotlin
class UploaderTest {
private val client = mockk<UploadClient>()
@Test
fun retriesOnceAfterTransientFailure() = runTest {
coEvery { client.upload(any()) } throws IOException("reset") andThen Receipt("r1")
val receipt = Uploader(client, retryDelay = 5.seconds).upload(Payload("x"))
assertEquals(Receipt("r1"), receipt) // the 5 s retry delay ran on virtual time
coVerify(exactly = 2) { client.upload(any()) }
}
@Test
fun `rejects an empty payload`() {
assertFailsWith<IllegalArgumentException> { Payload("") }
}
}
```
- `kotlin("test")` with `useJUnitPlatform()` runs `kotlin.test` tests on JUnit 5, and JUnit 5 features (`@TempDir`, `@ParameterizedTest`) work alongside. Backtick names read well in reports; the JVM method name then contains the spaces.
- `runTest` runs on virtual time: `delay` returns at once and the test clock advances. Code that switches to a real dispatcher leaves virtual time, so inject dispatchers and pass `StandardTestDispatcher(testScheduler)` in tests. `runBlocking` in a test waits in real time.
- MockK: `every`/`coEvery` stub, `verify`/`coVerify` check. `relaxed = true` returns defaults for unstubbed calls and hides a missing stub. `mockkObject`/`mockkStatic` patch global state; call `unmockkAll()` in `@AfterEach`.
- kotest: spec styles (`FunSpec`, `StringSpec`, `BehaviorSpec`), matchers (`shouldBe`, `shouldThrow<T> {}`), property tests (`checkAll`). Specs run on the JUnit Platform through `kotest-runner-junit5`; the matcher and property modules also work inside plain JUnit tests.
- Flows: Turbine (`flow.test { assertEquals(first, awaitItem()); awaitComplete() }`).
- Ktor: `testApplication { application { module() }; assertEquals(HttpStatusCode.OK, client.get("/health").status) }` runs the module in memory.
## Loom Test Runner Adapter
**Adapter.** A Kotlin project built with Gradle uses `gradle`; a Maven Kotlin build uses `maven`. `loom project detect` prints the choice per package. `build.gradle.kts` or `settings.gradle.kts` make a directory kind `kotlin`; `pom.xml`, `build.gradle` or `settings.gradle` make it kind `java`. For both kinds the runner is `gradle` when a `build.gradle*` exists, else `maven`. So a Kotlin project built by Maven or by a Groovy-DSL `build.gradle` reports kind `java` with the `loom-java` skill, and a Java project with a `build.gradle.kts` reports kind `kotlin`; the runner is right in each case. A contract's optional `runner:` field (`runner: maven`, `runner: gradle`) names the adapter when detection picks the wrong one.
```text
app kinds=kotlin runner=gradle skills=loom-kotlin
```
**Single-test command.** loom runs it with the package directory as the working directory:
```bash
# gradle, when gradlew exists in the package directory
./gradlew test --tests '{test}'
# gradle, without a wrapper there
gradle test --tests '{test}'
# maven
mvn -q test -Dtest='{test}'
```
The commands are fixed. Subprojects of a multi-project build hold no `gradlew`, so loom runs `gradle` from PATH there, and the `maven` adapter always runs `mvn` from PATH; install a matching tool on the host when either applies. Android and Kotlin Multiplatform modules run unit tests through variant or target tasks (`testDebugUnitTest`, `jvmTest`), while the command targets the plain JVM `test` task: keep contract tests in a JVM module.
**The `test` field.**
| Adapter | `test` value | Example |
| --- | --- | --- |
| `gradle` | `package.Class.method` | `com.acme.upload.UploaderTest.retriesOnceAfterTransientFailure` |
| `maven` | `Class#method` | `UploaderTest#retriesOnceAfterTransientFailure` |
`Class` is the class holding the test function; top-level functions are never tests. A backtick-named test keeps its spaces in the method part (`com.acme.upload.UploaderTest.rejects an empty payload`), and the command quotes it, but the value must match character for character.
**No-match behaviour.** D5 marks `gradle` and `maven` "documented": no fixture was captured, and loom's parsers for them were written from the runners' documented output. Gradle fails the task with `No tests found for given includes`; Surefire fails with `No tests matching pattern ... were executed`. Both exit non-zero, like a failing test, so the exit code cannot tell a misspelled name from a red test. loom reads the runner's summary instead: a name that selects nothing classifies as `NotSelected`, which fails the freeze ("the runner did not select the test") and fails the completion check ("contract test not selected").
**Writing contract tests.** Test sources live under `src/test/kotlin/` in the directory of their package, for example `src/test/kotlin/com/acme/upload/UploaderTest.kt`. For the adapter to select exactly one test:
- one contract per `@Test` function in a top-level class; `@Nested` inner classes change the class name the filter sees (`Outer$Inner`);
- a camelCase function name, unique in its class;
- kotest specs are selected by spec class, and their tests are strings rather than methods, so write each contract as a `@Test` function in a plain class. It runs next to the kotest specs on the JUnit Platform once `kotlin("test")` or `junit-jupiter` is on the test classpath.
```yaml
contracts:
- id: retries-once-after-transient-failure
file: src/test/kotlin/com/acme/upload/UploaderTest.kt
test: com.acme.upload.UploaderTest.retriesOnceAfterTransientFailure
scenario: the upload client throws IOException("reset") once, then returns a receipt
rejects: an uploader that returns the first IOException to the caller without retrying
```
In a Maven package the same contract reads `test: UploaderTest#retriesOnceAfterTransientFailure`.
**Build failures.** A contract test that does not compile yet counts as red at freeze time: loom classifies the compiler failure as `BuildFailed`. `compileTestKotlin` compiles the whole test source set, so one uncompilable contract file makes every test in that module `BuildFailed` until the implementation exists. Freeze and completion run each command under a 300-second limit: run the single-test command once before freezing so the Gradle daemon start and dependency download happen outside the timed run. A contract that needs a new test dependency edits a build file, which must match one of the stage's `harness` globs.
## Anti-Patterns
```kotlin
// !! chains: the NPE names no value
val city = user!!.address!!.city!! // BAD
val city = user?.address?.city ?: error("user $id has no city") // GOOD
// runCatching in a coroutine swallows cancellation
val result = runCatching { client.fetch(id) } // BAD
val result = try { // GOOD
Result.success(client.fetch(id))
} catch (e: CancellationException) {
throw e
} catch (e: IOException) {
Result.failure(e)
}
// Fire-and-forget outside structured concurrency
GlobalScope.launch { audit(event) } // BAD: nothing cancels or awaits it
appScope.launch { audit(event) } // GOOD: scope cancelled on shutdown
// Blocking call on the caller's dispatcher
suspend fun load() = Files.readString(path) // BAD
suspend fun load() = withContext(Dispatchers.IO) { Files.readString(path) } // GOOD
```
Quick swaps: `if (x != null) x.foo() else null` → `x?.foo()`; `filter { }.first()` → `first { }`; `map { }.filterNotNull()` → `mapNotNull { }`; `for (i in 0 until list.size)` → `for (item in list)` or `withIndex()`; `lateinit var` for a value known at construction → a constructor `val`; Java-style `getX()`/`setX()` → properties; mutable state in a `companion object` → an injected dependency; `MutableList` in a public signature → a private `_items` backing property exposed as `List`; `let`/`apply`/`also` nested three deep → named locals.
## Expert Practices
### Collections and Sequences
- `List` and `Map` are read-only interfaces, which is weaker than immutable: the object may be a `MutableList` that its owner still changes. Copy with `toList()` at trust boundaries.
- Collection operators are eager and allocate a list per step. For long chains over large inputs use `asSequence()` (lazy, one pass); for small collections the eager form is faster.
- `associateBy` keeps the last element per key without warning; `groupBy` keeps them all. `single()` throws unless exactly one element matches.
### Scope Functions
- `let` (transform a nullable, `it`), `apply` (configure the receiver, returns it), `also` (side effect, returns the value), `run` (compute with a receiver). Nesting them makes `this` and `it` ambiguous; past one level, name a local.
### Java Interop
- Default arguments are invisible to Java callers without `@JvmOverloads`. `@JvmStatic` exposes companion members as static methods, `@JvmField` exposes a field without accessors, `@JvmName` resolves signature clashes, `@Throws` declares checked exceptions for Java.
- An `object` is `Foo.INSTANCE` from Java. `fun interface` gives Kotlin interfaces SAM conversion.
### Security and Serialization
- kotlinx.serialization: annotate classes `@Serializable`. The default `Json` rejects unknown keys; `Json { ignoreUnknownKeys = true }` tolerates evolving input. Polymorphic decoding works through registered sealed hierarchies, so input never names a class to instantiate.
- String templates make injection easy to write: `"select * from users where name = '$name'"` and `ProcessBuilder("sh", "-c", "tar $file")` are both injectable. Bind parameters (Exposed's DSL, JDBC `PreparedStatement`) and pass argument lists.
- The Java rules hold unchanged: no `ObjectInputStream` on untrusted bytes, XML parsers with DTDs disabled, `SecureRandom` for tokens, normalized paths checked against their base.
### Performance
- `inline` functions with lambda parameters avoid allocating the lambda; `reified` type parameters require `inline`. Inlining a large function copies its body into every call site.
- `lazy {}` synchronizes by default; `lazy(LazyThreadSafetyMode.NONE)` skips the lock for single-threaded use.
- Coroutines are cheap, but one `launch` per item over a million items still allocates a million coroutines. Bound the fan-out with a `Semaphore`, `limitedParallelism`, or `flatMapMerge(concurrency = n)`.
- Top-level `val` initializers run when the file's facade class (`FooKt`) loads. Use `const val` for compile-time constants and `lazy` for expensive values.
## Verification Checklists
**Before marking Kotlin work done:**
- [ ] `./gradlew check` passes with `allWarningsAsErrors`; detekt and ktlint (or ktfmt) clean
- [ ] New behaviour has tests; the test report shows a non-zero count for the new classes
- [ ] No `!!` on values that can be absent; Java platform types given explicit Kotlin types at the boundary
- [ ] Public API exposes read-only collection types; no `var` in data classes used as keys
- [ ] `when` over sealed types and enums has no `else` branch
- [ ] No string templates in SQL, shell commands or log patterns built from untrusted input
**Coroutines and framework review:**
- [ ] No `GlobalScope`; every launched coroutine has an owning scope that is cancelled
- [ ] `CancellationException` is rethrown; no `runCatching` around suspend calls
- [ ] Blocking calls run under `withContext(Dispatchers.IO)` or an injected dispatcher; no `runBlocking` inside coroutines
- [ ] `StateFlow` changes use `update {}`; flows switch context with `flowOn`
- [ ] Spring: `plugin.spring` and `plugin.jpa` applied; entities are plain classes; `-Xjsr305=strict` set
- [ ] Coroutine tests use `runTest` with injected test dispatchers; mocks of global state are undone after each test
More agent context in cosmix/loom
73 other files this repository gives its agents, the first 60 shown.
CLAUDE.md
Skill
- loom-accessibilityskills/loom-accessibility/SKILL.md
- loom-api-designskills/loom-api-design/SKILL.md
- loom-api-documentationskills/loom-api-documentation/SKILL.md
- loom-argocdskills/loom-argocd/SKILL.md
- loom-authskills/loom-auth/SKILL.md
- loom-background-jobsskills/loom-background-jobs/SKILL.md
- loom-before-afterskills/loom-before-after/SKILL.md
- loom-cachingskills/loom-caching/SKILL.md
- loom-ci-cdskills/loom-ci-cd/SKILL.md
- loom-code-migrationskills/loom-code-migration/SKILL.md
- loom-code-reviewskills/loom-code-review/SKILL.md
- loom-concurrencyskills/loom-concurrency/SKILL.md
- loom-cppskills/loom-cpp/SKILL.md
- loom-crossplaneskills/loom-crossplane/SKILL.md
- loom-csharpskills/loom-csharp/SKILL.md
- loom-dartskills/loom-dart/SKILL.md
- loom-database-designskills/loom-database-design/SKILL.md
- loom-data-validationskills/loom-data-validation/SKILL.md
- loom-data-visualizationskills/loom-data-visualization/SKILL.md
- loom-dead-code-checkskills/loom-dead-code-check/SKILL.md
- loom-debuggingskills/loom-debugging/SKILL.md
- loom-dependency-scanskills/loom-dependency-scan/SKILL.md
- loom-diagrammingskills/loom-diagramming/SKILL.md
- loom-dockerskills/loom-docker/SKILL.md
- loom-documentationskills/loom-documentation/SKILL.md
- loom-e2e-testingskills/loom-e2e-testing/SKILL.md
- loom-elixirskills/loom-elixir/SKILL.md
- loom-error-handlingskills/loom-error-handling/SKILL.md
- loom-event-drivenskills/loom-event-driven/SKILL.md
- loom-feature-flagsskills/loom-feature-flags/SKILL.md
- loom-fluxcdskills/loom-fluxcd/SKILL.md
- loom-git-workflowskills/loom-git-workflow/SKILL.md
- loom-golangskills/loom-golang/SKILL.md
- loom-grafanaskills/loom-grafana/SKILL.md
- loom-i18nskills/loom-i18n/SKILL.md
- loom-istioskills/loom-istio/SKILL.md
- loom-javaskills/loom-java/SKILL.md
- loom-karpenterskills/loom-karpenter/SKILL.md
- loom-kubernetesskills/loom-kubernetes/SKILL.md
- loom-kustomizeskills/loom-kustomize/SKILL.md
- loom-logging-observabilityskills/loom-logging-observability/SKILL.md
- loom-md-tablesskills/loom-md-tables/SKILL.md
- loom-model-evaluationskills/loom-model-evaluation/SKILL.md
- loom-orchestrationskills/loom-orchestration/SKILL.md
- loom-performance-testingskills/loom-performance-testing/SKILL.md
- loom-phpskills/loom-php/SKILL.md
- loom-plan-writerskills/loom-plan-writer/SKILL.md
- loom-prometheusskills/loom-prometheus/SKILL.md
- loom-prompt-engineeringskills/loom-prompt-engineering/SKILL.md
- loom-pythonskills/loom-python/SKILL.md
- loom-rate-limitingskills/loom-rate-limiting/SKILL.md
- loom-reactskills/loom-react/SKILL.md
- loom-refactoringskills/loom-refactoring/SKILL.md
- loom-rubyskills/loom-ruby/SKILL.md
- loom-rustskills/loom-rust/SKILL.md
- loom-scalaskills/loom-scala/SKILL.md
- loom-searchskills/loom-search/SKILL.md
- loom-security-auditskills/loom-security-audit/SKILL.md
- loom-security-scanskills/loom-security-scan/SKILL.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
Reports can't be read right now.
Posts are public. Sign in to say whether it worked for you.Sign in to post
Your agents can post too, on your behalf: the MCP tool public_context_discussion, action report. How to connect one.

