What the agent does
The agent reads where the versions live, because in a Ktor project they are spread across the plugin block and a properties file rather than one catalog. In ktorio/ktor-samples, the kweet sample's build.gradle.kts applies kotlin("jvm") and the serialization plugin at 2.4.20 and io.ktor.plugin at 3.6.0, and that plugin supplies the Ktor BOM, so the io.ktor:ktor-server-* lines carry no version at all. kweet/gradle.properties holds kotlin_version=2.4.20, logback_version=1.5.21 and exposed_version=1.0.0-rc-3, read by the build script with by project; the rest are inline: H2 2.4.240, c3p0 0.11.2, Joda-Time 2.14.0, FreeMarker 2.3.34, Ehcache 3.11.1 and MockK 1.14.6. The jwt-auth-tests sample has the same plugin versions, Logback inline, and kotlin("test") with useJUnitPlatform(). Both run on the Gradle 9.2.1 wrapper and declare jvmToolchain(17). Only ktor-client-wasm has a libs.versions.toml; these two do not.
Then it reads the checks. kweet/src/test/kotlin/KweetApplicationTest.kt has four testApplication tests against a MockK DAOFacade: an empty home page, a home page with kweets, a failed login and a successful one. jwt-auth-tests/src/test/kotlin/io/ktor/samples/jwtauth/ApplicationTest.kt has one test that generates an RSA key pair, injects a fake JWK provider and checks a signed login round trip. At the repository root, build-all.sh runs ./gradlew clean check --console=plain in every sample directory and .github/workflows/build-all.yml runs it on pull requests with JDK 24; renovate.json extends the recommended config, so bot bumps already arrive and the point of this run is to land them tested.
The groups follow what moves together. The io.ktor.plugin version is the Ktor version, so a Ktor bump is one plugin line per sample, and the BOM moves every ktor-server-* and the ktor-server-test-host test dependency with it. The two Kotlin plugins move together with kotlin_version in gradle.properties, and a Kotlin bump is checked against what the Ktor plugin release supports. Exposed in kweet sits on a release-candidate line and the code already imports org.jetbrains.exposed.v1.*, so a move to a final release is tried on its own, since the exposed-jodatime module and the DAO code in dao/DAOFacadeDatabase.kt are where a rename would show. H2, c3p0, Logback, FreeMarker, Ehcache and MockK move as a library group. The agent installs a JDK that satisfies jvmToolchain(17) before the first build.
After each group the agent runs ./gradlew test in the sample, and ./gradlew check as build-all.sh does, with --console=plain so the output is readable in the record. A group that passes is a commit; a group that fails, for example a Ktor release that changes a testApplication API the kweet test uses, is reverted and its compiler or test error kept. The pull request lists each bump with old and new versions per sample, the skips with their reason, and the test summaries. It works only in kweet and jwt-auth-tests, as the prompt says, and leaves the other samples alone.
Start it with one API call
POST https://api.prix.dev/o/{org}/sessions with this body:
{
"agent": "claude",
"input": "Upgrade the Gradle dependencies of the kweet and jwt-auth-tests samples in this repository, in groups: the io.ktor.plugin version first, then the Kotlin plugins together with kotlin_version in gradle.properties, then Exposed on its own, then the remaining libraries together. Versions live in build.gradle.kts and gradle.properties; there is no version catalog in these two samples. Install a JDK that satisfies jvmToolchain(17). After each group run ./gradlew test and ./gradlew check --console=plain in each sample. Keep a group only if both pass in both samples; otherwise revert it and record the error. Do not touch other samples. Open one pull request with one commit per group, one line per bump with old and new versions, and every upgrade left out with the reason.",
"resources": [
{
"url": "https://github.com/your-org/your-app"
}
]
}- 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 pull request that changes kweet/build.gradle.kts, kweet/gradle.properties and jwt-auth-tests/build.gradle.kts, in one commit per group that passed. Source files change only when a library group required it, for example an Exposed rename, and then the description names the file. The Gradle wrapper is unchanged unless the prompt asked for it.
The description is one line per bump per sample with old and new versions, a list of upgrades left out with the compiler error or failing test that excluded them, and the ./gradlew test and ./gradlew check summaries for both samples. The trajectory keeps the full Gradle output for every group, including the reverted ones.
Before merging, a reviewer checks that the io.ktor.plugin version is the same in both samples, that kotlin_version and the plugin versions agree, and that a left-out Ktor release is not a security fix that should go in with a code change.
Run it on every Monday morning
Create an automation in the Project with this prompt and a schedule trigger (cron 0 9 * * 1 in your timezone). Each week the run opens one pull request with that week's bumps, or none when nothing moved. Because each group is its own commit, dropping one bump before merge is a revert of one commit.
To have the link reach your server, start the weekly run from your own scheduler instead, with POST /o/{org}/sessions and an API key that has a webhook URL: Rush then POSTs run.completed with the session_url when the turn ends, and a handler on your side can post the pull request link where the Ktor maintainers read. A run that ends with no safe group opens no pull request and says why in its result.
The use case this job follows, with its automation and trigger: Upgrade dependencies weekly.
Why run it on Rush?
A Gradle upgrade is slow on a laptop: a JDK, a wrapper download, a dependency resolution and a compile per attempt, and a Kotlin or Ktor plugin that disagrees with the other shows up only after all of that. On Rush the agent does each attempt in its own sandbox, keeps what passes, reverts what fails, and records every build log, so the pull request that reaches you is already green and the failures are written down with their errors.
Running two samples times four groups is eight full builds, which is why bot bumps sit unmerged. The record shows each build, and the same request runs on every Ktor service you maintain at the same time.
Frequently asked questions
Why is there no version catalog change?
Because kweet and jwt-auth-tests do not use one. Their versions are in the plugins block, inline dependency strings and kweet/gradle.properties. A project with a gradle/libs.versions.toml gets the same groups applied to the catalog instead; the agent reads the build to find out which.
How does it bump Ktor itself?
By changing the io.ktor.plugin version in each build.gradle.kts. The plugin supplies the BOM, so the unversioned io.ktor:ktor-server-* and ktor-server-test-host dependencies follow it. There is no separate Ktor version property to keep in step.
Which JDK does it build with?
One that satisfies jvmToolchain(17), which both samples declare. The repository's CI installs JDK 24 and relies on Gradle's toolchain handling; the agent installs a 17 so the build does not depend on a resolver that is not configured.
Renovate already opens bumps here. What does this add?
Renovate opens one pull request per dependency and lets CI say red or green. This run tries the bumps in groups, in a sandbox, reverts the ones that break the tests, and opens a single pull request that is already green, with the failures explained instead of left as red checks.