Symptom

Step 3 of openspec/changes/upgrade-agp-9-1-1 is the major-version hop: 8.13.2 → 9.0.x (followed by 9.0.x → 9.1.1 at Step 4). The realised sequence after the Step 2 collapse is 8.12.3 → 8.13.2 → 9.0.x → 9.1.1. On opening the step, :androidApp:assembleDebug and :composeApp:tasks :composeApp:desktopJar succeed cleanly under AGP 9.0.0, 9.0.1, and 9.1.1 (with the two source edits the AGP 9 release notes call out — see Fix below). The ProGuard/R8 step (:androidApp:assembleRelease:androidApp:minifyReleaseWithR8) fails identically on all three AGP versions:

1
2
3
4
5
6
7
8
9
ERROR: …/transforms/…/transformed/core-1.18.0-runtime.jar:
  R8: java.lang.NullPointerException:
    Cannot invoke "com.android.tools.r8.androidapi.f$a.a()"
    because the return value of "com.android.tools.r8.androidapi.f.H()" is null

> Task :androidApp:minifyReleaseWithR8 FAILED
> Compilation failed to complete,
  position: Landroidx/core/app/ActivityCompat;->$r8$lambda$PEvuXAyy2Ld2s8FnpyspwjlByy4(Landroid/app/Activity;)V,
  origin: …/core-1.18.0-runtime.jar:androidx/core/app/ActivityCompat.class

The exact failing lambda rotates between ActivityCompat, LocationManagerCompat, and ViewGroupCompat from invocation to invocation (it’s whichever class R8 reaches first), but the crash site is always the same: an R8 internal Android-API DB lookup (com.android.tools.r8.androidapi.f.H()) returning null for one of the prebuilt $r8$lambda$* synthetic dispatch methods that ship inside androidx.core:core:1.18.0. Stack trace identifies the R8 build as R8_9.1.31_… when overridden via the buildscript classpath, or the AGP-bundled R8 (9.0.32 for AGP 9.0.x, 9.1.31 for AGP 9.1.1) otherwise — the bug is the same in both.

:androidApp:assembleDebug is unaffected because debug builds skip R8 (isMinifyEnabled = false). The release build is mandatory: the Verify Agent Ghost workflow installs and smoke-tests the release .deb outputs.

Root cause

The bundled R8 in AGP 9.0.x and 9.1.1 has a regression in its Android-API database lookup for synthetic lambda dispatch methods that androidx.core:core 1.18.0 generates as part of its own R8 processing. The R8 method com.android.tools.r8.androidapi.f.H() returns null for those $r8$lambda$* symbols, and the immediate caller dereferences the return value without a null check, throwing the NPE quoted above. This is in R8’s internal code path, not in anything our androidApp/proguard-rules.pro controls.

Empirical investigation (run against this repo’s actual dependency surface — Compose Multiplatform 1.10.3, androidx-core-ktx = 1.18.0, compileSdk = 36, minSdk = 30, JDK 21):

Workarounds tested (each tried independently — none change the failure):

The rational landing point given those constraints is AGP 8.13.2 — the final 8.x release per Section 3’s lessons entry — until upstream ships an R8 patch that handles androidx.core 1.18.0’s prebuilt lambdas. This matches the resolution path Open Question Q1 in design.md already authorised: “ceiling-out at 8.13.2 — the final 8.x release per Section 3’s empirical finding — is an acceptable landing point”.

Fix

Documentation-only PR. No gradle/libs.versions.toml edit, no source or build-script change.

For the avoidance of doubt, the source edits that the AGP 9 release notes do call for — and that would be in scope if the R8 bug weren’t blocking the bump — were verified to apply cleanly during the investigation:

These edits stay in this lessons entry rather than the build files, so the next dev-agent run that reaches Step 3 (after R8 is fixed) can grab them as a starting point.

Sanity check on the spec-correction branch: JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 ./gradlew help --warning-mode=all is BUILD SUCCESSFUL against the unchanged AGP 8.13.2. The full test gate, :androidApp:assembleDebug, :composeApp:desktopJar, and :androidApp:assembleRelease are unchanged from main (PR #226’s verification) and not re-run for this docs-only PR.

Prevention

  1. Always run :androidApp:assembleRelease before assuming an AGP bump is green. :androidApp:assembleDebug skips R8 entirely (isMinifyEnabled = false), so it cannot detect bundled-R8 regressions. This is exactly why tasks.md task 4.9 calls out the release build as its own checkpoint — without it, the R8 NPE would have shipped through to QA verification on Ghost’s runner, which would have failed the same way one stage later with much harder context to recover. The pattern from the staged plan is correct; this finding validates it.
  2. Bundled R8 is not consumer-overridable in AGP 9.x. The classic buildscript { classpath("com.android.tools:r8:X.Y.Z") } recipe that worked through AGP 7/8 no longer overrides AGP’s bundled copy. If a future major-version bump is blocked on an R8 bug, the options are (a) wait for an AGP patch, (b) downgrade the offending transitive library if the surface allows, or (c) disable isMinifyEnabled and accept the loss of obfuscation. There is no fourth path involving an out-of-band R8 swap. Don’t waste an investigation cycle re-trying the buildscript-classpath recipe at the next AGP cliff.
  3. An “AGP 9 issuetracker watch” is the right shape of follow-up. File this as a [friction] issue against krill-agents (this PR does not — Section 7 of tasks.md already plans the krill-agents follow-up “after series merges”, and an agp-r8-watch line item belongs there once a future series re-opens) and re-check dl.google.com/android/maven2/com/android/tools/r8/maven-metadata.xml plus the Google issue tracker every few weeks. When R8 ships a fix, the deferred Section 5 sub-tasks become executable.
  4. Treat the staged plan’s “this hop is realisable” assumption as a hypothesis at every step, not just at Step 2. The plan’s “always one-minor-at-a-time” property has now been collapsed twice — first by AGP’s calendar (no 8.14 line) and then by R8’s bug surface at the major boundary. Each PR in this series should re-confirm not only that the AGP version exists, but that :androidApp:assembleRelease against the current dependency surface actually completes, before writing any code change. The exploratory bumps in this PR are what surfaced the blocker; a bump-first-test-later approach would have wasted churn on plugin edits that ultimately rolled back.