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 with this body:
{
"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"
}
]
}- 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.
- 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.
- 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.
The use case this job follows, with its automation and trigger: Review every pull request.
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.