Ramiz RajaBook a call
← Writing
Mobile Engineering

Trunk-Based Development With Coding Agents: What My Merge History Shows

I pulled the merge history of five repos to see how long my branches really live. One large branch failed CI three times before it went green; the small ones merged in minutes.

Ramiz RajaNovember 10, 20268 min read
Trunk-Based Development With Coding Agents: What My Merge History Shows

One branch in my BLE Lab repo carried 37 commits and 152 files, took about eight hours from first commit to merge, and failed CI three times on three different layers before it went green. The next nine pull requests had a median of three minutes from first commit to merge, ranging from three to 27, and most were a single commit. Same repo, same CI, same person. Here is what that looks like under trunk based development.

That is a small sample, so I checked it against 89 merged pull requests in four other repos. This post is the data, what it does and does not show, and how I measured it.

What trunk based development means, and how long a branch can live

Trunk based development means everyone integrates into one main line, in small steps and often. Feature branches are allowed, which is the point of searches like "trunk based development vs feature branch": the difference is lifetime. trunkbaseddevelopment.com says a branch "should only last a couple of days", counts time waiting for review toward that, keeps the branch to one developer (two if pairing), and expects CI to verify it before it lands.

With coding agents, the unit is one developer and their agents, which is how I run most repos (the setup is in Two Agents, One Repo). An agent will produce 37 commits of related work in an afternoon, so the guideline stops being automatic. You decide how big a branch gets.

By that guideline eight hours is fine. The question I wanted to answer is narrower: what happened to the large branch that did not happen to the small ones?

The first commits went straight to main

BLE Lab is a public repo with an Android app, an iOS app and ESP32 firmware that talk over Bluetooth, with a simulated board for tests. The first 11 commits on main went in directly: the scaffold, the first versions of the firmware, the Android client and the iOS client, and some fixes. There was no pull request. Whether that is acceptable depends on whether anyone depends on the repo yet. The history records only that this happened before the pull-request pattern started.

The large branch

The first pull request was a large round of IoT features, including firmware update and session history. It was 37 commits, 152 files and roughly 9,000 lines added, about 478 minutes from first commit to merge. CI on the pull request failed three times before the green run.

Table: the red CI runs on the large branch. Each run took about three minutes. The failures were in different layers.

RunWhat failedLayer that caught it
1Android format check (Spotless)Formatting is enforced as a CI check.
2Firmware compile for the plain ESP32 board: ble_gap_conn_desc has not been declared, and a method marked override that overrides nothingCI compiles two boards, and the plain ESP32 uses a different Bluetooth stack from the other one.
2Android lint: InputStream#readNBytes "requires API level 33 (current min is 26)"Lint checks API calls against the minimum supported Android version.
2Two screenshot tests failed on golden mismatchPixel comparison of the screens.
384 tests, 2 failed: the history and session-detail screensThe screens print session times in the device time zone, so goldens recorded in one zone differ on a runner in another.

Three fix commits followed: formatting; one commit that guarded the Bluetooth-stack-specific callback per backend, replaced readNBytes with a loop and re-recorded the device goldens; and one that pinned the test time zone to UTC and re-recorded two goldens. The next run was green and the merge followed a few minutes later.

I do not want to oversell this. I did not measure how long the fixes took, and I cannot show that splitting the branch would have meant fewer red runs. What the history does show is that all three layers failed on the same pull request, with the changes behind each one spread over a 152-file diff.

The nine that followed

After the large branch, every change in the repo went through a pull request, and each was small.

Table: the merged pull requests after the large one, first commit to merge. Median 3 minutes.

PRsFirst commit to mergeCommits per PRFiles per PR
Nine merged PRs3, 5, 3, 4, 3, 3, 3, 3 and 27 minutesMostly 1 (two had 2)2 to 12

Five of them were firmware point releases. CI on pull requests passed on nine of ten runs after the large branch; the tenth was cancelled when a newer push superseded it. A run takes about three minutes, so the check and the merge take about the same time.

Is that just my repo?

I pulled the last 25 merged pull requests from each of four other repos I work in: three Android apps (one with only 14 merged PRs) and an SDK. They are private, so I will not name them.

Table: observed first-commit-to-merge time across four other repos (89 merged PRs).

MeasureResult
PRs measured89
Merged within 4 hours79
Merged within 24 hours88
Longest25.9 hours

The per-repo medians ranged from about zero minutes to 33 minutes. The longest was a little over a day.

How I measured it. For each merged pull request I ran gh pr list --state merged --limit 25 --json mergedAt,commits (the 25 most recent merged PRs), took the earliest committedDate among the PR's commits, subtracted it from mergedAt, and rounded to whole minutes. In jq terms the per-PR number is (mergedAt | fromdate) - ([.commits[].committedDate | fromdate] | min), in seconds. The figures for the four private repos are my own results; you cannot re-run them. Three limits apply to any such number, mine included. Commit dates can be rewritten by an amend or a rebase, so the age may be understated. The query only sees merged PRs, so abandoned and still-open branches are missing. And work done before the first commit is invisible. I also did not use the time between a PR being opened and merged: I usually open the PR when the work is ready, so that number is close to zero and says nothing about branch age.

What CI checked in BLE Lab

Small branches only work if merging is cheap, and CI is what makes it cheap here: the checks that failed above run on every pull request. The jobs below are a condensed summary of the pipeline, not the literal workflow file.

Condensed command summary: the three CI jobs on every pull request in BLE Lab.

android:   ./gradlew spotlessCheck lint testDebugUnitTest \
             verifyRoborazziDebug koverVerifyApp assembleDebug
firmware:  arduino-cli compile   # matrix: esp32:esp32:esp32, esp32:esp32:m5stack_cores3
ios:       swift test --package-path BLELabKit --enable-code-coverage
           xcodebuild build  # simulator, no signing

The firmware job compiles two boards, which is why the plain ESP32 error showed up there. Lint reads API usage against the app's minimum supported Android version. The screenshot tests compare pixels, so a time zone leaking into a screen is a test failure. Kover enforces an 80 percent line-coverage floor, with configured exclusions such as generated code and previews.

CI does not replace review. My merge rule on top of it: I merge with gh pr merge only after a reviewer has passed the exact head commit and the checks are green. Don't Trust an Agent That Says the Tests Passed is about why I do not take an agent's own report as that review.

What I cannot tell you

  • It is one person plus agents. On a team you add two people editing the same file, review queues and a main that has to stay green for others. I have none of that here.
  • Main is not protected. The GitHub API reports no branch protection on BLE Lab's main. The pull-request habit is mine, not a setting, and the first 11 commits show what the repo looked like before I applied it.
  • There is no release train. BLE Lab has no store release behind it, so I have no first-hand evidence on feature flags, release branches or hotfix flow. Those parts of trunk based development for mobile apps are the hard ones, and I have not measured them.
  • Age is not the explanation. The branch met the lifetime guideline, and these observations do not establish what caused the difference. I can only say that the biggest branch is the one that failed in several places.

What I take from it

Branch age is a weak signal, since an eight-hour branch is fine by the usual rule. Diff size is a signal I can act on: the 152-file branch is where three layers failed in one pull request, and the 2-to-12-file pull requests are where each merged in minutes. For agents, that is a reason to ask for a pull request after the first working slice, not after all the work. That is my recommendation, not an experiment result.

To check your own repos, the query is short: first commit to merge for your last 25 merged pull requests. The number you expect and the number you get may differ more than you think.