# Review Pull Requests on a Ktor Server

Yes. An agent checks out the pull request, runs ./gradlew check in each touched sample in its sandbox, and posts one review on plugin order and coroutines.

[Run this on Rush](https://cloud.getrush.ai/run/review-pull-requests/ktor/), in a Project that has your repository.

## What the agent does

The agent reads how the repository builds before it reads the diff, because ktorio/ktor-samples is not one Gradle project. Each sample directory carries its own `build.gradle.kts`, `gradlew` and `gradle/wrapper/gradle-wrapper.properties`, all thirty at Gradle 9.2.1, with Kotlin `2.4.20` and either the `io.ktor.plugin` at `3.6.0` or `ktor-bom:3.6.0` as an enforced platform, and `jvmToolchain(17)`; one sample, `maven-google-appengine-standard`, builds with Maven. `build-all.sh` at the root walks every directory, picks `./mvnw clean test` when it finds a `pom.xml` and `./gradlew clean check --console=plain` otherwise, prints a pass, fail and skip summary and exits 1 if any sample failed. `.github/workflows/build-all.yml` runs that script on every pull request to `main` under JDK 24, so a change to one sample is proven only by that sample's own `check`. The agent runs `./gradlew clean check --console=plain` in each touched sample rather than the whole script, and uses JDK 24 as CI does while the toolchain block compiles for 17.

The tests that exist tell the agent what a new one should look like. `httpbin` has the most, sixteen classes such as `StatusCodesTest`, `HeadersTest`, `RedirectsTest` and `AuthTest`, each a `testApplication { application { module() } ... }` block using the test `client`, and `createClient { followRedirects = false }` where a redirect's `Location` is the thing under test. `chat/src/backendTest/kotlin/ChatApplicationTest.kt` installs `WebSockets` on the test client, opens `/ws`, sends `HELLO` and asserts the `[server] Member joined` and echoed frames, using a `hashSetOf` for the two-client case because frame order is not fixed. `jwt-auth-tests` builds a `MapApplicationConfig` with `jwt.privateKey`, `jwt.issuer`, `jwt.audience` and `jwt.realm`, passes a fake `JwkProvider` to `main`, posts to `/login` and reads `/hello` with the returned bearer token. Samples like `sse`, `postgres`, `mvc-web`, `openapi` and the two reverse proxies have no `src/test` at all, so for those the agent's `check` proves compilation and detekt only, and the review says so.

Then it reads the diff with the three questions the prompt names. Plugin installation order: in a Ktor module, `install(ContentNegotiation)`, `install(Authentication)` and `install(StatusPages)` must be installed before the routes that depend on them, and `httpbin`'s dependency list, `ktor-server-auth`, `ktor-server-compression`, `ktor-server-conditional-headers`, `ktor-server-cors`, `ktor-server-sessions`, `ktor-server-status-pages`, `ktor-server-partial-content` and more, is a long enough list that a plugin added at the end of `module()` after `routing { }` is a plausible mistake; the agent checks each touched `module` function in order. Serialization: `httpbin` uses `ktor-serialization-kotlinx-json` in the server and `ktor-serialization-gson` in tests, `postgres` uses `kotlinx-json-jvm` with `ktor-server-config-yaml`, and `chat` uses `kotlin("plugin.serialization")` on a multiplatform build, so a new data class returned from a route needs `@Serializable` and the matching `ContentNegotiation` registration, or the test client gets a plain-text body. Coroutine usage: a `runBlocking` inside a route handler, a `GlobalScope.launch` for work that should be tied to the call, or a WebSocket loop that swallows `ClosedReceiveChannelException` is a finding; `ChatApplicationTest` is the pattern for proving the WebSocket case.

For each candidate finding the agent checks whether a test already pins the behaviour and, where it can, writes a short `testApplication` case in the sandbox to confirm it before posting, keeping that case out of the branch. One GitHub review is posted per run: the summary quotes each touched sample's `./gradlew check` result, including the test task's pass and fail counts and the detekt outcome, separates blocking issues from suggestions, and cites file and line for every finding. The agent pushes nothing.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Review this pull request on a Ktor samples repository. Check it out and, in each sample directory the diff touches, run ./gradlew clean check --console=plain with that sample's own wrapper, or ./mvnw clean test for a Maven sample, as build-all.sh does. Read the diff with the sample's Application module, its routing, its build.gradle.kts and any src/test it has. Look for plugins installed after the routing block or after routes that depend on them, data classes returned from routes without @Serializable or without a ContentNegotiation registration, runBlocking or GlobalScope inside handlers, WebSocket loops that swallow close exceptions, Ktor or Kotlin versions bumped in one sample but not its wrapper or BOM, and a new sample missing a README.md entry in the root README. Where a finding is about request behaviour, write a short testApplication case in the sandbox to confirm it, but do not add it to the branch. Post one GitHub review that quotes each sample's check result, separates blocking issues from suggestions, and cites file and line for every finding. Do not push commits.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app/pull/42"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a review on the pull request. The summary names each sample the diff touched and quotes its `./gradlew clean check` result, with test counts where a `src/test` exists and a note where none does. Findings follow with file and line in the sample's `src/main/kotlin`, marked blocking or suggestion, and a plugin-order finding names the `install` call and the route it must precede.

The transcript keeps the Gradle distribution download for each wrapper, every command with its output, the `build/reports/tests` directory the check wrote, and the `testApplication` cases the agent wrote to confirm a finding, so the response a route gave is there to read.

Before merging, a reviewer runs the touched sample's `check` on the branch, opens each cited line, and decides which suggestions to take. A failing test or a plugin installed after its routes is blocking by default; a request to add tests to a sample that never had them is a suggestion unless the pull request introduced the behaviour.

## Run it on every pull request opened

Create an automation in the Project with a `github` trigger on `pull_request.opened` and `pull_request.synchronize`, `branch_patterns` set to `main`, and this prompt. Each fire adds the pull request after the prompt, so every pull request and every push to it gets its own sandbox, its own Gradle caches and its own review. Mark it `read_only` so the runs comment and never push.

The review is the result on the pull request and the run is listed in the Project. `max_runs_per_hour`, 1 to 30, keeps a Renovate batch, which `renovate.json` enables with `config:recommended`, to a bounded number of Gradle runs; a fire past the cap is skipped as `rate_limited` and the pull request is reviewed on its next push.

## Why run it on Rush?

A Ktor sample is proven by its own wrapper and its own `check`, which means a Gradle distribution download, a Kotlin compile for JVM and sometimes JS, and whatever `testApplication` cases the sample has, repeated per sample the diff touches. The repository's CI does all thirty on every pull request; a reviewer at a laptop does none. On Rush the agent runs exactly the touched samples in its own sandbox, keeps the test reports, and quotes them, so the review of a plugin-order change rests on an executed request rather than on reading `module()` top to bottom.

One sandbox per pull request keeps reviews parallel when several samples change in a week, each with its own Gradle daemon, and nothing is pushed to the author's branch. The reviewer gets findings with the `check` output attached and the author gets the same verdict `build-all.sh` would give, per sample, before CI runs the lot.

## Frequently asked questions

### Which JDK does the agent use?

JDK 24, as `.github/workflows/build-all.yml` installs, with each sample's `jvmToolchain(17)` deciding the bytecode target. A diff that raises the toolchain in one sample or uses a JDK API past 17 is a finding.

### Does it run detekt?

The repository's detekt workflow runs the standalone CLI at `v1.15.0` with `continue-on-error`, uploading SARIF rather than failing the build. The agent runs the same CLI over the touched sample and quotes its output, but treats a detekt finding as a suggestion unless the pull request introduced it.

### What about the multiplatform samples?

`chat` compiles a JS frontend beside its JVM backend, and its `backendProcessResources` task depends on the frontend webpack build, so `check` builds both and the agent runs it in full. `client-mpp` targets Android, iOS, macOS and JS from its `shared` module, which needs the Android SDK and, for the Apple targets, a Mac; when the sandbox cannot build it, the review says so and reads the change instead.

### Does it check the Postgres sample against a database?

`postgres` has a `databaseInstance` task that runs `docker-compose up` and no `src/test`, so its `check` compiles only. If the diff changes a query there, the review says the behaviour was read, not executed, and suggests a `testApplication` case with the YAML config pointed at a test database.
