Splitting Pre-release QA Across Four Antigravity Subagents — A Design Note from a Solo Mobile Developer
A design note from a solo indie developer running six iOS / Android apps in parallel, sharing how Antigravity subagents are split into four parallel paths for pre-release QA. Covers the boundary decisions, prompt templates, termination rules, and merge logic with concrete examples.
I am Masaki Hirokawa, an indie developer. I have been building iOS and Android apps on my own since 2014, and these days I operate six apps in parallel — mostly wallpaper, healing, and law-of-attraction style apps. Once cumulative downloads passed 50 million, the workload of a release week became too much to run sequentially. For the last six months I have been pushing on how much of the pre-release QA work I can hand off to Antigravity subagents. This article is the design note that came out of that experiment. The short answer: I split the work into four parallel paths and let four subagents run side by side.
When I ran release weeks sequentially, the order was static analysis first, then Simulator, then asset diff, then store submission review. Each app took about 90 minutes, so six apps was a 9-hour slog. In practice I did most of it late at night, and decision fatigue meant the last checks of the night were always the weakest. Once I handed sliced responsibilities to subagents and designed an explicit merge point, the total time for all six apps came down to about two and a half hours, and the dependence on late-night focus disappeared.
Why four paths
I started with the obvious idea: hand everything to a single agent and let Plan Mode walk through it. Antigravity's Plan Mode is excellent for high-level planning, but for repetitive QA across six apps the context grows too large, and the agent's judgment gets sloppy partway through. On my own setup, by the third app the agent would start saying things like "this is the same as the previous app, so we can skip," and twice I caught it missing a real diff that way.
So I drew the boundaries based on the kind of resource each subagent has to read. If the read surface is restricted, the context stays small. The four paths I settled on are:
Path 1: source code and git diff — static analysis and diff review
Path 3: assets and localization resources — diff inspection
Path 4: store submissions and external configuration — metadata, AdMob, Crashlytics, and so on
These four paths almost never touch the same files. Path 1 reads src/ and app/src/main/, while Path 3 reads Assets.xcassets/ and res/drawable*/. Because the context surfaces do not collide, I can run the four Antigravity subagents in parallel and each one stays focused on its own slice.
Where I had to be careful was the grey area between paths. For example "did the icon get swapped correctly" belongs to Path 3, but "is CFBundleIconName in Info.plist updated" touches both Path 1 and Path 3. For grey-zone items I write a rule up front — Path 1 owns it — so the merge step never has to resolve a contradiction. Having both paths look at the same item sounds safer but actually pushes complexity into the merge prompt.
Path 1: static analysis and diff review
The Path 1 subagent gets the diff from the previous release tag to the candidate commit and looks for risky patterns that might have slipped in. Because my codebase is a mix of Swift, Kotlin, and TypeScript, I split the checklist by language.
These are the patterns that bite most often:
New try! or as! in Swift (a frequent crash culprit)
New !! in Kotlin (same)
Leftover debug output like console.log, print, Log.d
Feature flags whose default was flipped to true accidentally
UserDefaults / SharedPreferences keys that collide with the previous version
Some of these can be caught by lint, but my lint severity policy varies project by project. "Be stricter only right before release" is not something a lint config expresses cleanly. A subagent, on the other hand, can take a temporary stricter threshold via a natural language instruction.
The Path 1 subagent definition looks like this. Antigravity supports YAML subagent definitions, and I keep mine in a Markdown-ish prompt template per project because I tweak them often.
# .antigravity/subagents/path1-static-diff.yamlname: path1-static-diffrole: static analysis and diff reviewread_paths: - src/** - app/src/main/** - ios/**/*.swift - android/**/*.ktwrite_paths: [] # Path 1 is read-onlytermination: on_finding_count_exceeds: 30 # too many findings → switch to manual review on_critical_severity: true # 1 critical → terminate immediatelyoutput_format: json_schema_v1
Here is the prompt template I use when Plan Mode dispatches this subagent. The most important part is pinning the output format. Since four paths get merged downstream, any drift in the output schema explodes the merge logic.
You are the Path 1 subagent. For {REPO_NAME}, evaluate the diff between{PREV_TAG}...HEAD against the following criteria. Do not suggest anything else.Criteria:- New force unwraps / force casts (Swift: try!/as!, Kotlin: !!)- Leftover debug output (console.log, print, Log.d)- Feature flags whose default is true in the commit- UserDefaults/SharedPreferences key collisions with prior versions- Breaking changes to public APIs (function signatures, enum cases)Return only this JSON:{ "path": "path1-static-diff", "status": "pass" | "warn" | "fail", "findings": [ { "severity": "info|warn|critical", "file": "...", "line": 0, "summary": "..." } ], "took_seconds": 0}
I deliberately impose the status rule from outside. fail if any critical, warn if five or more warnings, pass otherwise. When I let the subagent decide its own status, the same project flipped between warn and pass between runs, so externalising the threshold is more stable.
One concrete win that only Path 1 could catch: in one of my wallpaper apps, a piece of code that was supposed to clear the icon cache only in debug builds had an #if DEBUG block that ran past a missing #endif, so the code ran in production too. At the git diff level the only visible change was the position of #endif, and that is virtually impossible to catch by eye.
Path 1 also does one less obvious job: it audits dependency lockfiles. Package.resolved, Gemfile.lock, and package-lock.json sometimes pull in unintended minor bumps, and surfacing the diff here keeps everything in one context window with the code that depends on those libraries.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦The reasoning behind splitting pre-release QA for six iOS / Android apps into four parallel paths
✦Prompt templates, termination rules, and JSON output schemas for the four subagents
✦Merge logic and rollback criteria covering AdMob mediation, dSYM uploads, and store metadata
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Path 2: Simulator / Emulator smoke and deeplink checks
The Path 2 subagent runs the built app on Simulator / Emulator and verifies the bare-minimum happy path. Doing this manually for six apps is the most exhausting part of a release night, so it is high on the list of things to delegate.
That said, this path does not automate as cleanly as Path 1. The Simulator only gives you snapshots of the UI state, so judging whether a screen looks broken requires visual comparison. I have Antigravity subagents shell out to xcrun simctl and adb, capture three screenshots (launch, home, settings) on each platform, and compute pixel diffs against the previous release.
For deeplinks I keep a fixed test set in deeplinks.yaml. The subagent reads the YAML and exercises one entry at a time. The most common failure mode is Universal Links / App Links quietly changing semantics on a new major iOS or Android version — that is something lint and static analysis can never catch.
The JSON shape Path 2 returns matches Path 1. The findings look like this:
severity: critical: crash within 3 seconds of launch (and the stack trace is already known in Crashlytics)
severity: warn: launches, but the main screen takes longer than 5 seconds to render
severity: warn: the upper half of the deeplink screen differs from the previous version by more than 8% pixels
Pixel-diff thresholds can devolve into number tweaking, so I just lock 8% as warn and 20% as critical. Empirically, intentional design changes never come in below 8%, and changes under 8% are almost always layout regressions.
Right after a new iPhone resolution lands, Path 2 is where most of the value gets created. My apps have moved from Storyboard to SwiftUI, but even so there are versions where the assumed safeAreaInsets shift silently. In the past, twice I only noticed at the final review submission. Now booting a single simulator on the new resolution and looking at the diff catches text that has wandered outside the safe area.
Path 3: asset and localization diff inspection
Path 3 is the asset diff subagent. It looks at Assets.xcassets, Contents.json, res/drawable-*/, res/values-*/strings.xml, and for React Native projects everything under assets/.
The two things I lean on it most for are icon/OG image integrity and localization key gaps. The first will get rejected at submission. The second only shows up on a real device as missing.translation text in the UI — the kind of bug you only find after release.
My apps support 8 languages with Japanese and English as the reference languages. The instruction I give the subagent is:
You are the Path 3 subagent. Compare the localization resources and assetsof {REPO_NAME} against the reference languages (ja, en) and list any gaps orintegrity violations.Criteria:- Keys present in ja, en but missing in other languages- Values still in English that should have been translated (heuristic: ASCII only, ≥3 words)- Resolution violations in Assets.xcassets / drawable-* / mipmap-*- AppIcon size completeness (iOS: 20pt~1024pt, Android: mdpi~xxxhdpi)- OG image aspect ratio (1.91:1) and minimum size (1200x630)Output format: same JSON as Path 1.
The "value still in English" check over-fires if you take it strictly. Brand names and product names should stay in English. As a heuristic, I treat "ASCII-only, three or more words, not starting with a capital letter" as a warn. Imperfect, but as a "you probably forgot to translate this" alarm at the eleventh hour it works well enough.
If you let a subagent rewrite localizations, you get tonal drift — that is its own can of worms and I will write about it separately. For pre-release QA I deliberately do not look at translation quality. The subagent only catches gaps and leftovers.
One real example: in one of my law-of-attraction apps, a specific custom font's bold variant was missing for one locale, and the home screen silently fell back to Helvetica because UIFont(name:) returned nil. A human who knows that quirk can spot it, but doing it across six apps and eight locales is unrealistic — exactly the sort of work to delegate.
Path 4: store submissions and external configuration
Path 4 is less about code and more about operational documents and dashboards. Specifically, App Store Connect metadata, Google Play Console release notes, AdMob mediation settings, Firebase Crashlytics thresholds, and dSYM upload status.
At this layer, text and screenshots are not enough information, so this path is designed to work alongside the Antigravity browser tools. I have a separate setup that uses the Claude in Chrome integration to read the AdMob and Crashlytics dashboards. The output (latest eCPM, fill rate, crash counts) is dropped into JSON files that Path 4 reads.
The items Path 4 looks at:
App Store Connect title length (≤30 chars) and keyword field length (≤100 chars)
Screenshot resolution violations (the new iPhone resolution gets forgotten more than anything else)
Release notes present in all 8 languages
Any priority-0 network dead in AdMob mediation
Crashlytics crash count over the last 7 days more than 1.5× the previous version
dSYM uploaded to Crashlytics (manual upload is often required since Bitcode was deprecated)
The subagent returns one JSON object summarising these. The rule I pin from outside is: if any item has severity: critical, the release does not proceed.
For AdMob mediation, my six apps run a mix of AdMob, Facebook Audience Network, Unity Ads, AppLovin, and Pangle. A surprisingly common release-week trap is a fresh SDK version not getting along with one of the networks. Test ads still load fine, but live ads come up blank. Path 4 flags any network whose 24-hour fill rate drops to 30% or lower as critical. The rationale is simple: my apps' average fill rate is around 75%, so anything at 40% of that is broken.
The 1.5× Crashlytics threshold is tuned for the DAU range of my apps. For smaller apps, ten unlucky new users can push the ratio over 1.5×, so I have a per-app override table that Path 4 consults by app ID before deciding.
The merge point and rollback criteria
Once the four subagents finish in parallel, the main Antigravity agent merges their results. The main agent gets a "final decision prompt" that emits JSON to be consumed by my downstream git tag automation script.
You will receive result JSON from four paths. Decide if the release can proceed.Decision rules:1. If any path has status == "fail" → release_decision = "block"2. If status == "warn" in two or more paths → release_decision = "review"3. Otherwise → release_decision = "go"4. Aggregate all severity=="critical" findings into critical_findings.Output format:{ "release_decision": "go" | "review" | "block", "block_reason": "...", // block only "critical_findings": [{ "path": "...", "summary": "..." }], "warn_count_per_path": { "path1-...": 0, "path2-...": 0, "path3-...": 0, "path4-...": 0 }, "decided_at": "ISO8601"}
The most important thing in this prompt is preventing the main agent from overriding the conclusion. Antigravity's main agent is more capable than the subagents, and at the merge step it sometimes gets clever — "Path 1 said fail, but reading the commit message it looks intentional, so I'll flip it to pass." That kind of judgment is helpful in other contexts; for a release gate it is dangerous. The rules are pinned and rewriting them is not allowed.
In practice, release_decision == "block" halts the git tag automation entirely and pings me. review only notifies and lets me look at it. go flows straight into the fastlane release pipeline.
Six months into this setup, the number of cases where go was decided and the production version then saw a crash spike is zero so far. Of the block decisions, roughly 30% were AdMob-related from Path 4. Just catching broken mediation configuration right before submission has paid for the cost of running four parallel subagents.
I store the decision history in .antigravity/release-decisions/YYYY-MM-DD.json. Six months in, I have about 70 entries, and aggregating them reveals where my (one-person) team breaks things most often. The aggregator runs as its own separate operational script and drops a monthly report into Slack. Modest, but it closes a feedback loop.
Six months in
I tried several intermediate designs before this one stuck. I started with six paths, but the inter-subagent communication overhead got out of hand and I collapsed to four. I also tried compressing to three, but then Path 1 became too fat and context overflows came back. Four is the local optimum I have found.
My grandfathers on both sides were temple carpenters in Japan, and I grew up taking it for granted that "structures other people can understand" matter. That bias shows up here: I keep slicing subagents to a count that fits on a single diagram. When my art work has been recognised at international competitions, the citations have often praised "structural clarity," and I have come to feel that software design lives by the same standards.
What I want to improve next is the false positive rate in Path 2 screenshot comparisons. When an OS version bumps system font hinting changes ever so slightly and unintended diffs ride in. I am experimenting with handing the visual diff to a VLM-based subagent that judges whether the diff is "semantically meaningful," and that direction looks promising.
Another piece I want to add is region-specific legal compliance in Path 4 — the EU DSA, US children's privacy law, the Japanese Specified Commercial Transactions Act, and so on. I am not a lawyer, so rather than delegate the judgment to AI, the right shape is probably a separate weekly subagent that watches for regulatory change announcements and surfaces them.
Running this scale of operation as a solo indie developer is only possible because Antigravity's subagents truly behave according to the responsibilities I assign. With a single do-everything agent, I do not think I could have gotten this far.
Release weeks are dramatically lighter than they were six months ago. Removing the late-night decision-fatigue failure mode alone makes solo development a lot more sustainable. If you are wrestling with the same problem, the number four is less important than the underlying idea — slice by the kind of resource each subagent has to read. Thank you for reading.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.